diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml new file mode 100644 index 00000000..de4d0aa7 --- /dev/null +++ b/.github/workflows/native.yml @@ -0,0 +1,139 @@ +name: Native compiler and installed packages + +on: + push: + branches: [main, codex/native-codegen] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: native-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_PROFILE_TEST_DEBUG: "0" + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + rustflags: "" + - os: windows-latest + target: x86_64-pc-windows-msvc + rustflags: -Ctarget-feature=+crt-static + runs-on: ${{ matrix.os }} + timeout-minutes: 180 + env: + RUSTFLAGS: ${{ matrix.rustflags }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install tested Rust toolchain + run: rustup toolchain install 1.97.1 --profile minimal + - run: rustup override set 1.97.1 + - name: Package release compiler and both runtime profiles + run: python tools/package_native.py --output dist/package + - name: Check compiler and native conformance + env: + JETT_ACTOR_GRAPH_BUNDLE_MANIFEST: ${{ github.workspace }}/dist/package/lib/jett/runtime/${{ matrix.target }}/debug/launcher.json + JETT_ACTOR_GRAPH_EVIDENCE_DIR: ${{ github.workspace }}/target/native-actor-graph + run: cargo test --locked --workspace --no-fail-fast --target ${{ matrix.target }} + - name: Check public graph execution with the Release runtime + env: + JETT_ACTOR_GRAPH_BUNDLE_MANIFEST: ${{ github.workspace }}/dist/package/lib/jett/runtime/${{ matrix.target }}/release/launcher.json + JETT_ACTOR_GRAPH_EVIDENCE_DIR: ${{ github.workspace }}/target/native-actor-graph + run: >- + cargo test --locked --release -p jett_driver --test actor_graph_native + --target ${{ matrix.target }} + actor_graph_public_driver_reference_and_native_without_source + -- --exact + - name: Check protected graph hooks with the Release runtime + env: + JETT_ACTOR_GRAPH_BUNDLE_MANIFEST: ${{ github.workspace }}/dist/package/lib/jett/runtime/${{ matrix.target }}/release/launcher.json + JETT_ACTOR_GRAPH_EVIDENCE_DIR: ${{ github.workspace }}/target/native-actor-graph + run: >- + cargo test --locked --release -p jett_driver --lib + --target ${{ matrix.target }} + native::actor_graph_hooks_tests::actor_graph_checked_hook_original_initializer_and_capture_native + -- --exact + - name: Retain compact graph execution evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-actor-graph-${{ matrix.target }}-${{ github.sha }} + path: | + target/native-actor-graph/*.json + target/native-actor-graph/**/*.stdout + target/native-actor-graph/**/*.stderr + if-no-files-found: warn + retention-days: 7 + - name: Check GNU Resource measurement controls + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: python -m unittest tools.tests.test_measure_resource_native_archives + - name: Measure fresh GNU private Resource runtime archives + id: resource_archives + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: >- + python tools/measure_resource_native_archives.py prepare + --output target/native-resource-gnu + --github-output "$GITHUB_OUTPUT" + - name: Execute complete GNU Resource corpus with compiled receipt pin + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: >- + python tools/measure_resource_native_archives.py run + --receipt "${{ steps.resource_archives.outputs.receipt }}" + --expected-sha256 "${{ steps.resource_archives.outputs.sha256 }}" + --target-dir target/native-resource-gnu-gate + - name: Retain GNU Resource measurements and execution evidence + if: always() && matrix.target == 'x86_64-unknown-linux-gnu' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-resource-${{ matrix.target }}-${{ github.sha }} + path: | + target/native-resource-gnu/*.json + target/native-resource-gnu/*.jsonl + target/native-resource-gnu/*.log + target/native-resource-gnu/*-crt-probe + target/native-resource-gnu/*-crt-probe.c + target/native-resource-gnu/*-build/x86_64-unknown-linux-gnu/*/libjett_runtime.a + if-no-files-found: warn + retention-days: 7 + - name: Check installed compiler + run: python tools/smoke_native_package.py dist/package + - name: Archive package with executable permissions + run: tar -czf native-package.tar.gz -C dist package + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-${{ matrix.target }} + path: native-package.tar.gz + if-no-files-found: error + retention-days: 7 + + installed: + needs: build + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: windows-latest + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + # Deliberately no checkout: this job has no compiler sources or build tree. + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-${{ matrix.target }} + - run: tar -xzf native-package.tar.gz + - name: Build and execute with the relocated package + run: python package/tools/smoke_native_package.py package diff --git a/.gitignore b/.gitignore index 26217624..bea5ce69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .todo /target -Cargo.lock .claude __pycache__/ *.pyc diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..93b76af2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2391 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d521bdbc6098937af83ef4ab6d5c07398126bc71878f7ef4ea9499977978ef5" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dde0b83164d4a497860af4236271178bc640512b067101e0853e4f74eaa4df5" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0111d110b72b4efad69a372e29e21628652fd0bcab66967e5c8350ab679affd5" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf01ecc92fc5499789d79c3b817299d5a8ddd828d31dcf0f9cc3cc66f38dbb36" +dependencies = [ + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cd2563bead0090c3879a7ff7327f7550c9d59bb5d05ecc8cd7886977aa3125a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9640d250d26f9381a73dc7f9862b27928d4809119aec73be0e556612e67b6a99" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07f156b90efc94371ddb3536f76e4ab671ad2e093bfa5511e10198cadbb0c47" + +[[package]] +name = "cranelift-control" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a76fd9dd37dcbc3d2d2e00abe3281a7e88adc035fba3b8114cc69981576ec" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eea22522144ba08c7e7ef94bfc25d474ef016c2771974b8ab1986734ac85965" +dependencies = [ + "cranelift-bitset", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa2826c80dff1d93b19b3cfbcaf9fae44c78d739a98d146dd5bd89c51e14367" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e238a69b95c5415456f22189494a12db94f313bb72b6ec9cc88ddf2f1056e28e" + +[[package]] +name = "cranelift-module" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985df7eceefc91cb75bf7f51b32b7f1739d0fc0e25ddf023b1de407cb3c93d77" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", +] + +[[package]] +name = "cranelift-object" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a476f25c5740a43ad0ba7ac398fb5484f1e9dfd662bba127a111d591e90350d" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-module", + "log", + "object", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.132.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004643f39a7bec553de5263d650db30e5b9caec1d5cbe065fd732b7ec9ae40d0" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "intrusive-collections" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be" + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jett_ast" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", + "jett_parser", +] + +[[package]] +name = "jett_cli" +version = "0.1.0" +dependencies = [ + "clap", + "jett_diagnostics", + "jett_driver", + "jett_lsp", + "jett_profiler", + "jett_resolve", + "tokio", +] + +[[package]] +name = "jett_codegen_cranelift" +version = "0.1.0" +dependencies = [ + "cranelift-codegen", + "cranelift-frontend", + "cranelift-module", + "cranelift-object", + "jett_common", + "jett_comptime", + "jett_diagnostics", + "jett_hir", + "jett_mir", + "jett_parser", + "jett_resolve", + "jett_runtime", + "jett_typecheck", + "jett_types", + "object", + "sha2", + "target-lexicon", +] + +[[package]] +name = "jett_common" +version = "0.1.0" + +[[package]] +name = "jett_comptime" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", + "jett_intrinsics", + "jett_parser", + "jett_resolve", + "jett_runtime", + "jett_typecheck", + "jett_types", + "rand", + "subtle", + "unicode-segmentation", +] + +[[package]] +name = "jett_diagnostics" +version = "0.1.0" +dependencies = [ + "jett_common", +] + +[[package]] +name = "jett_driver" +version = "0.1.0" +dependencies = [ + "find-msvc-tools", + "jett_ast", + "jett_codegen_cranelift", + "jett_common", + "jett_comptime", + "jett_diagnostics", + "jett_fmt", + "jett_hir", + "jett_lexer", + "jett_mir", + "jett_parser", + "jett_project", + "jett_query", + "jett_resolve", + "jett_runtime", + "jett_typecheck", + "jett_types", + "same-file", + "serde_json", + "tempfile", + "windows-sys", +] + +[[package]] +name = "jett_fmt" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_lexer", +] + +[[package]] +name = "jett_hir" +version = "0.1.0" +dependencies = [ + "insta", + "jett_common", + "jett_comptime", + "jett_diagnostics", + "jett_intrinsics", + "jett_parser", + "jett_resolve", + "jett_typecheck", + "jett_types", +] + +[[package]] +name = "jett_intrinsics" +version = "0.1.0" + +[[package]] +name = "jett_lexer" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", +] + +[[package]] +name = "jett_lsp" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", + "jett_driver", + "jett_fmt", + "jett_lexer", + "jett_resolve", + "serde_json", + "tokio", + "tower-lsp", +] + +[[package]] +name = "jett_mir" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_comptime", + "jett_diagnostics", + "jett_hir", + "jett_parser", + "jett_resolve", + "jett_typecheck", + "jett_types", +] + +[[package]] +name = "jett_native_launcher" +version = "0.1.0" +dependencies = [ + "jett_runtime", +] + +[[package]] +name = "jett_parser" +version = "0.1.0" +dependencies = [ + "insta", + "jett_common", + "jett_diagnostics", + "jett_lexer", +] + +[[package]] +name = "jett_profiler" +version = "0.1.0" + +[[package]] +name = "jett_project" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", +] + +[[package]] +name = "jett_query" +version = "0.1.0" +dependencies = [ + "hmac", + "jett_common", + "jett_diagnostics", + "jett_parser", + "salsa", + "salsa-macro-rules", + "sha2", + "subtle", +] + +[[package]] +name = "jett_regex" +version = "0.1.0" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "jett_resolve" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", + "jett_parser", + "jett_types", +] + +[[package]] +name = "jett_runtime" +version = "0.1.0" +dependencies = [ + "font8x8", + "minifb", + "rand", + "serde_json", + "subtle", + "unicode-segmentation", + "windows-sys", +] + +[[package]] +name = "jett_typecheck" +version = "0.1.0" +dependencies = [ + "jett_common", + "jett_diagnostics", + "jett_intrinsics", + "jett_parser", + "jett_resolve", + "jett_types", +] + +[[package]] +name = "jett_types" +version = "0.1.0" +dependencies = [ + "jett_common", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minifb" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1a093126f2ed9012fc0b146934c97eb0273e54983680a8bf5309b6b4a365b32" +dependencies = [ + "cc", + "console_error_panic_hook", + "dlib", + "futures", + "instant", + "js-sys", + "lazy_static", + "libc", + "orbclient", + "raw-window-handle", + "serde", + "serde_derive", + "tempfile", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "web-sys", + "winapi", + "x11-dl", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", + "sdl2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "salsa" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" +dependencies = [ + "boxcar", + "crossbeam-queue", + "crossbeam-utils", + "hashbrown 0.17.1", + "hashlink", + "indexmap", + "intrusive-collections", + "inventory", + "parking_lot", + "portable-atomic", + "rayon", + "rustc-hash", + "salsa-macro-rules", + "salsa-macros", + "smallvec", + "thin-vec", + "tracing", + "typeid", +] + +[[package]] +name = "salsa-macro-rules" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" + +[[package]] +name = "salsa-macros" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdl2" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thin-vec" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "serde_json", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.3", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmtime-internal-core" +version = "45.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3073c03f97f871fe6400e68621c863b93ba79296f8e570285231952d23fcc804" +dependencies = [ + "hashbrown 0.17.1", + "libm", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index cd1a3670..7be02dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,12 @@ members = [ "crates/jett_ast", "crates/jett_resolve", "crates/jett_types", + "crates/jett_intrinsics", "crates/jett_typecheck", "crates/jett_hir", "crates/jett_mir", + "crates/jett_codegen_cranelift", + "crates/jett_native_launcher", "crates/jett_comptime", "crates/jett_fmt", "crates/jett_query", @@ -38,9 +41,12 @@ jett_parser = { path = "crates/jett_parser" } jett_ast = { path = "crates/jett_ast" } jett_resolve = { path = "crates/jett_resolve" } jett_types = { path = "crates/jett_types" } +jett_intrinsics = { path = "crates/jett_intrinsics" } jett_typecheck = { path = "crates/jett_typecheck" } jett_hir = { path = "crates/jett_hir" } jett_mir = { path = "crates/jett_mir" } +jett_codegen_cranelift = { path = "crates/jett_codegen_cranelift" } +jett_native_launcher = { path = "crates/jett_native_launcher" } jett_comptime = { path = "crates/jett_comptime" } jett_fmt = { path = "crates/jett_fmt" } jett_query = { path = "crates/jett_query" } @@ -61,3 +67,13 @@ subtle = "2" unicode-segmentation = "1" sha2 = "0.10" hmac = "0.12" +cranelift-codegen = { version = "=0.132.3", default-features = false, features = ["std", "unwind", "x86", "arm64"] } +cranelift-frontend = { version = "=0.132.3", default-features = false, features = ["std"] } +cranelift-module = { version = "=0.132.3", default-features = false, features = ["std"] } +cranelift-object = { version = "=0.132.3", default-features = false } +target-lexicon = "=0.13.3" +find-msvc-tools = "=0.1.12" +tempfile = "=3.27.0" + +[profile.test] +opt-level = 1 diff --git a/README.md b/README.md index e0804325..e675ea57 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Core ideas already present in the implementation include: Jett is experimental, but the compiler front half is substantial. - The lexer, parser, formatter, resolver, typechecker, ownership checker, comptime interpreter, runtime interpreter, CLI, fixture suite, and VS Code extension are working. -- `jett build` currently validates and type-checks programs. Native LLVM code generation is planned but not implemented yet. +- `jett build` emits native executables through Cranelift on x86-64 Linux GNU and Windows MSVC. `build --check` validates source without an executable or runtime bundle. - `jett run` executes programs through the tree-walking interpreter. - `jett test` runs `verify` and `property` blocks. - `graphics.run` can open a native 2D window through an explicit `Graphics` @@ -89,7 +89,7 @@ cargo run -p jett_cli -- run tests/run_pass/hello_print.jett Type-check a file: ```bash -cargo run -p jett_cli -- build tests/run_pass/hello_print.jett +cargo run -p jett_cli -- build --check tests/run_pass/hello_print.jett ``` Format a file: @@ -117,7 +117,7 @@ The main binary is `jett`. ```text jett format [--check] [--agent] -jett build [--release] [--agent] [--target ] +jett build [--check] [--release] [--agent] [--target ] [-o ] [--runtime-bundle ] jett run [--agent] jett test [--agent] [file.jett] jett bundle [--agent] --output [project-file-or-directory] @@ -127,7 +127,26 @@ jett lsp `format`, `build`, `run`, `test`, `bundle`, and `query` accept `--agent` to emit structured TOON output that a coding agent can parse mechanically. `query` requires exactly one of the listed query options. -`build` currently type-checks without generating native code; `--release` and `--target` are accepted but currently have no effect and reserve the intended code-generation interface. +Package the compiler, stdlib, debug/release runtime archives, and dependency notices: + +```bash +python3 tools/package_native.py --output target/package +python3 tools/smoke_native_package.py target/package +target/package/bin/jett build tests/run_pass/hello_print.jett +``` + +Native builds require the host C linker and SDK. Keep the package's `bin` and +`lib` directories together when relocating it. The installed compiler never +invokes Cargo. The default output is `target///` +(with `.exe` on Windows); `-o` selects another destination. `--release` optimizes +code, rejects debug printing, and removes traces and breakpoint conditions. +`--target` accepts only the current supported host triple; cross-compilation +is not implemented. + +`--runtime-bundle` or `JETT_NATIVE_RUNTIME_BUNDLE` can select an explicit +`launcher.json`; the driver validates its compiler version, target, ABI, CRT, +profile, and ordered system libraries. Stdlib discovery uses `JETT_STDLIB_DIR` +when set, then the package's `lib/jett/stdlib`, then the development checkout. ## Repository Layout @@ -177,6 +196,25 @@ cargo test -p jett_comptime Compile-fail fixtures live in `tests/compile_fail` and assert specific diagnostics. Run-pass fixtures live in `tests/run_pass` and exercise interpreter behavior. +### Private Resource native gates + +The archive measurement helper builds fresh compiler-test runtimes and runs +pinned Source execution gates. On Linux GNU, its default target measures the +dynamic CRT. On Windows MSVC, use an x64 Visual Studio Developer PowerShell +with `link.exe` on PATH and LLVM's `llvm-nm` and `llvm-readobj` installed: + +```powershell +python tools/measure_resource_native_archives.py prepare --target x86_64-pc-windows-msvc --nm llvm-nm --readobj llvm-readobj --linker link.exe --output target/native/private-resource +python tools/measure_resource_native_archives.py run --target x86_64-pc-windows-msvc --suite occupied-structs --reuse-target-dir --receipt RECEIPT_PATH --expected-sha256 MEASURED_DIGEST --target-dir target/native +``` + +Preparation returns the receipt path and measured digest. The MSVC path checks +static CRT, exact runtime exports, compiler and linker identity, and the SDK LIB +environment. The two occupied-struct gates use both fresh archive profiles; +ordinary Runtime test counts do not establish their linked Source execution. +Reusing the MSVC test target requires a newly compiled gate with that receipt +pin. The default `original` suite retains the complete shared Source gate. + The language intentionally enforces function complexity limits. The current checker caps functions at 100 statements, nesting depth 4, and cyclomatic complexity 10. ## License diff --git a/benchmarks/tasks/account_state_evolution/task.json b/benchmarks/tasks/account_state_evolution/task.json index d3bcb20d..5cbf0680 100644 --- a/benchmarks/tasks/account_state_evolution/task.json +++ b/benchmarks/tasks/account_state_evolution/task.json @@ -17,7 +17,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden; handle every Jett enum variant explicitly"}] }, "python": { diff --git a/benchmarks/tasks/bounded_weighted_sum/task.json b/benchmarks/tasks/bounded_weighted_sum/task.json index 84991371..8f6dc42a 100644 --- a/benchmarks/tasks/bounded_weighted_sum/task.json +++ b/benchmarks/tasks/bounded_weighted_sum/task.json @@ -5,7 +5,7 @@ "statement": "Implement bounded_weighted_sum. For each value at zero-based index i, clamp the value to the inclusive range [-cap, cap], multiply it by i + 1, and return the sum. The empty list returns 0.", "constraints": ["0 <= cap <= 100000", "values has at most 1000 elements", "all inputs and intermediate results fit in signed 64-bit integers", "do not perform input/output"], "adapters": { - "jett": {"signature": "namespace benchmark\n\nfunction bounded_weighted_sum(values: list[int64], cap: int64) returns int64", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, + "jett": {"signature": "namespace benchmark\n\nfunction bounded_weighted_sum(values: list[int64], cap: int64) returns int64", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, "python": {"signature": "def bounded_weighted_sum(values: list[int], cap: int) -> int", "baseline": "baseline.py", "hidden": "hidden.py", "candidate": "solution.py", "support_files": ["pyrightconfig.json"], "grade_mode": "separate", "commands": [["{pyright}", "--project", "pyrightconfig.json"], ["python", "hidden.py"]]}, "typescript": {"signature": "export function boundedWeightedSum(values: readonly bigint[], cap: bigint): bigint", "baseline": "baseline.ts", "hidden": "hidden.ts", "candidate": "solution.ts", "grade_mode": "separate", "commands": [["npx", "tsc", "--strict", "--target", "ES2022", "--module", "NodeNext", "--moduleResolution", "NodeNext", "solution.ts", "hidden.ts", "--outDir", "out"], ["node", "out/hidden.js"]]}, "go": {"signature": "package benchmark\n\nfunc BoundedWeightedSum(values []int64, cap int64) int64", "baseline": "baseline.go", "hidden": "hidden_test.go", "candidate": "solution.go", "grade_mode": "separate", "commands": [["go", "test", "-run", "^$", "solution.go", "hidden_test.go"], ["go", "test", "solution.go", "hidden_test.go"]]}, diff --git a/benchmarks/tasks/data_change_log/task.json b/benchmarks/tasks/data_change_log/task.json index c4c613bb..d4e38819 100644 --- a/benchmarks/tasks/data_change_log/task.json +++ b/benchmarks/tasks/data_change_log/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_dependency_layers/task.json b/benchmarks/tasks/data_dependency_layers/task.json index 98d6e7a8..cfe15c2a 100644 --- a/benchmarks/tasks/data_dependency_layers/task.json +++ b/benchmarks/tasks/data_dependency_layers/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_graph_components/task.json b/benchmarks/tasks/data_graph_components/task.json index b7a4ed7d..9a08bdab 100644 --- a/benchmarks/tasks/data_graph_components/task.json +++ b/benchmarks/tasks/data_graph_components/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_graph_degrees/task.json b/benchmarks/tasks/data_graph_degrees/task.json index 298120e4..f49d2db9 100644 --- a/benchmarks/tasks/data_graph_degrees/task.json +++ b/benchmarks/tasks/data_graph_degrees/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_graph_reachability/task.json b/benchmarks/tasks/data_graph_reachability/task.json index 10799fcc..5b6dfc7c 100644 --- a/benchmarks/tasks/data_graph_reachability/task.json +++ b/benchmarks/tasks/data_graph_reachability/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_graph_shortest_hops/task.json b/benchmarks/tasks/data_graph_shortest_hops/task.json index 8d10ae78..3d4627dc 100644 --- a/benchmarks/tasks/data_graph_shortest_hops/task.json +++ b/benchmarks/tasks/data_graph_shortest_hops/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_invoice_reconciliation/task.json b/benchmarks/tasks/data_invoice_reconciliation/task.json index 091a815b..16096fd3 100644 --- a/benchmarks/tasks/data_invoice_reconciliation/task.json +++ b/benchmarks/tasks/data_invoice_reconciliation/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_job_readiness/task.json b/benchmarks/tasks/data_job_readiness/task.json index 99b44845..dc1281e0 100644 --- a/benchmarks/tasks/data_job_readiness/task.json +++ b/benchmarks/tasks/data_job_readiness/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_join_unmatched/task.json b/benchmarks/tasks/data_join_unmatched/task.json index 24ae79f7..ea1c95a7 100644 --- a/benchmarks/tasks/data_join_unmatched/task.json +++ b/benchmarks/tasks/data_join_unmatched/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_ledger_audit/task.json b/benchmarks/tasks/data_ledger_audit/task.json index 1249cb02..5690dedc 100644 --- a/benchmarks/tasks/data_ledger_audit/task.json +++ b/benchmarks/tasks/data_ledger_audit/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_patient_triage/task.json b/benchmarks/tasks/data_patient_triage/task.json index b381ec06..0431e2a5 100644 --- a/benchmarks/tasks/data_patient_triage/task.json +++ b/benchmarks/tasks/data_patient_triage/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_permission_rules/task.json b/benchmarks/tasks/data_permission_rules/task.json index 4d19ec73..e5f21b7f 100644 --- a/benchmarks/tasks/data_permission_rules/task.json +++ b/benchmarks/tasks/data_permission_rules/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_ranking_ties/task.json b/benchmarks/tasks/data_ranking_ties/task.json index 81b39cc0..3cb2131d 100644 --- a/benchmarks/tasks/data_ranking_ties/task.json +++ b/benchmarks/tasks/data_ranking_ties/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_reservation_gate/task.json b/benchmarks/tasks/data_reservation_gate/task.json index 878d0dec..3cc3e62a 100644 --- a/benchmarks/tasks/data_reservation_gate/task.json +++ b/benchmarks/tasks/data_reservation_gate/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_resource_leases/task.json b/benchmarks/tasks/data_resource_leases/task.json index e609fb9b..f1b0c79e 100644 --- a/benchmarks/tasks/data_resource_leases/task.json +++ b/benchmarks/tasks/data_resource_leases/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_restock_projection/task.json b/benchmarks/tasks/data_restock_projection/task.json index 2736447d..a7cbf1e9 100644 --- a/benchmarks/tasks/data_restock_projection/task.json +++ b/benchmarks/tasks/data_restock_projection/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_retention_sweep/task.json b/benchmarks/tasks/data_retention_sweep/task.json index d93cc133..8a4db307 100644 --- a/benchmarks/tasks/data_retention_sweep/task.json +++ b/benchmarks/tasks/data_retention_sweep/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_route_cost/task.json b/benchmarks/tasks/data_route_cost/task.json index 31a24235..e51ffd3a 100644 --- a/benchmarks/tasks/data_route_cost/task.json +++ b/benchmarks/tasks/data_route_cost/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_schedule_conflicts/task.json b/benchmarks/tasks/data_schedule_conflicts/task.json index 8a78767a..d01739a6 100644 --- a/benchmarks/tasks/data_schedule_conflicts/task.json +++ b/benchmarks/tasks/data_schedule_conflicts/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_schedule_deadline_audit/task.json b/benchmarks/tasks/data_schedule_deadline_audit/task.json index f7514d6e..36934f25 100644 --- a/benchmarks/tasks/data_schedule_deadline_audit/task.json +++ b/benchmarks/tasks/data_schedule_deadline_audit/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_schedule_peak_load/task.json b/benchmarks/tasks/data_schedule_peak_load/task.json index 612fd260..5b957e38 100644 --- a/benchmarks/tasks/data_schedule_peak_load/task.json +++ b/benchmarks/tasks/data_schedule_peak_load/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_schedule_room_reuse/task.json b/benchmarks/tasks/data_schedule_room_reuse/task.json index 45dc719b..b9ba24e1 100644 --- a/benchmarks/tasks/data_schedule_room_reuse/task.json +++ b/benchmarks/tasks/data_schedule_room_reuse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_seat_assignments/task.json b/benchmarks/tasks/data_seat_assignments/task.json index f450b563..fcff0f7d 100644 --- a/benchmarks/tasks/data_seat_assignments/task.json +++ b/benchmarks/tasks/data_seat_assignments/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_sensor_sessions/task.json b/benchmarks/tasks/data_sensor_sessions/task.json index e0d05d18..e57c908a 100644 --- a/benchmarks/tasks/data_sensor_sessions/task.json +++ b/benchmarks/tasks/data_sensor_sessions/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_tree_depths/task.json b/benchmarks/tasks/data_tree_depths/task.json index ce497de5..b61755cb 100644 --- a/benchmarks/tasks/data_tree_depths/task.json +++ b/benchmarks/tasks/data_tree_depths/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_tree_lowest_ancestor/task.json b/benchmarks/tasks/data_tree_lowest_ancestor/task.json index 5fc2cb19..897fd64c 100644 --- a/benchmarks/tasks/data_tree_lowest_ancestor/task.json +++ b/benchmarks/tasks/data_tree_lowest_ancestor/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_tree_subtree_weight/task.json b/benchmarks/tasks/data_tree_subtree_weight/task.json index 8a447faa..ed9e6655 100644 --- a/benchmarks/tasks/data_tree_subtree_weight/task.json +++ b/benchmarks/tasks/data_tree_subtree_weight/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_union_coverage/task.json b/benchmarks/tasks/data_union_coverage/task.json index c3aa5197..a27c957e 100644 --- a/benchmarks/tasks/data_union_coverage/task.json +++ b/benchmarks/tasks/data_union_coverage/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_versioned_register/task.json b/benchmarks/tasks/data_versioned_register/task.json index efee33d1..42da2c52 100644 --- a/benchmarks/tasks/data_versioned_register/task.json +++ b/benchmarks/tasks/data_versioned_register/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/data_vote_quorum/task.json b/benchmarks/tasks/data_vote_quorum/task.json index 535ea24d..fb43ec57 100644 --- a/benchmarks/tasks/data_vote_quorum/task.json +++ b/benchmarks/tasks/data_vote_quorum/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/first_duplicate/task.json b/benchmarks/tasks/first_duplicate/task.json index 6fbc295b..95a7e6b0 100644 --- a/benchmarks/tasks/first_duplicate/task.json +++ b/benchmarks/tasks/first_duplicate/task.json @@ -16,7 +16,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden"}] }, "python": { diff --git a/benchmarks/tasks/inventory_batch/task.json b/benchmarks/tasks/inventory_batch/task.json index 5b9a7773..226e7918 100644 --- a/benchmarks/tasks/inventory_batch/task.json +++ b/benchmarks/tasks/inventory_batch/task.json @@ -17,7 +17,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden; handle every Jett enum variant explicitly"}] }, "python": { diff --git a/benchmarks/tasks/merge_sorted_intervals/task.json b/benchmarks/tasks/merge_sorted_intervals/task.json index aa272cdc..52e6e43b 100644 --- a/benchmarks/tasks/merge_sorted_intervals/task.json +++ b/benchmarks/tasks/merge_sorted_intervals/task.json @@ -16,7 +16,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden"}] }, "python": { diff --git a/benchmarks/tasks/num_binomial/task.json b/benchmarks/tasks/num_binomial/task.json index 20489e7f..10629302 100644 --- a/benchmarks/tasks/num_binomial/task.json +++ b/benchmarks/tasks/num_binomial/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_coin_change/task.json b/benchmarks/tasks/num_coin_change/task.json index 85f3997e..6a6fe25b 100644 --- a/benchmarks/tasks/num_coin_change/task.json +++ b/benchmarks/tasks/num_coin_change/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_collatz_steps/task.json b/benchmarks/tasks/num_collatz_steps/task.json index 27989850..e0cb6020 100644 --- a/benchmarks/tasks/num_collatz_steps/task.json +++ b/benchmarks/tasks/num_collatz_steps/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_digit_checksum/task.json b/benchmarks/tasks/num_digit_checksum/task.json index 998e23d7..0b64cab1 100644 --- a/benchmarks/tasks/num_digit_checksum/task.json +++ b/benchmarks/tasks/num_digit_checksum/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_divisor_sum/task.json b/benchmarks/tasks/num_divisor_sum/task.json index b725bced..ffaf6951 100644 --- a/benchmarks/tasks/num_divisor_sum/task.json +++ b/benchmarks/tasks/num_divisor_sum/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_equilibrium_index/task.json b/benchmarks/tasks/num_equilibrium_index/task.json index 64131af4..e1ba80db 100644 --- a/benchmarks/tasks/num_equilibrium_index/task.json +++ b/benchmarks/tasks/num_equilibrium_index/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_fibonacci/task.json b/benchmarks/tasks/num_fibonacci/task.json index e53525ef..cca20236 100644 --- a/benchmarks/tasks/num_fibonacci/task.json +++ b/benchmarks/tasks/num_fibonacci/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_histogram_mode/task.json b/benchmarks/tasks/num_histogram_mode/task.json index 8557509d..a51fcf10 100644 --- a/benchmarks/tasks/num_histogram_mode/task.json +++ b/benchmarks/tasks/num_histogram_mode/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_integer_sqrt/task.json b/benchmarks/tasks/num_integer_sqrt/task.json index 880c9e22..614526b5 100644 --- a/benchmarks/tasks/num_integer_sqrt/task.json +++ b/benchmarks/tasks/num_integer_sqrt/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_inversion_count/task.json b/benchmarks/tasks/num_inversion_count/task.json index c69311ee..f4191b15 100644 --- a/benchmarks/tasks/num_inversion_count/task.json +++ b/benchmarks/tasks/num_inversion_count/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_knapsack_value/task.json b/benchmarks/tasks/num_knapsack_value/task.json index 302db026..29ab437f 100644 --- a/benchmarks/tasks/num_knapsack_value/task.json +++ b/benchmarks/tasks/num_knapsack_value/task.json @@ -24,6 +24,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_lis_length/task.json b/benchmarks/tasks/num_lis_length/task.json index d091799f..d459d1d8 100644 --- a/benchmarks/tasks/num_lis_length/task.json +++ b/benchmarks/tasks/num_lis_length/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_longest_positive_run/task.json b/benchmarks/tasks/num_longest_positive_run/task.json index 2e626fd5..7c57208e 100644 --- a/benchmarks/tasks/num_longest_positive_run/task.json +++ b/benchmarks/tasks/num_longest_positive_run/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_matrix_trace/task.json b/benchmarks/tasks/num_matrix_trace/task.json index aabe411a..9fc98c0f 100644 --- a/benchmarks/tasks/num_matrix_trace/task.json +++ b/benchmarks/tasks/num_matrix_trace/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_max_product_pair/task.json b/benchmarks/tasks/num_max_product_pair/task.json index 37285c7a..5e451987 100644 --- a/benchmarks/tasks/num_max_product_pair/task.json +++ b/benchmarks/tasks/num_max_product_pair/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_max_subarray/task.json b/benchmarks/tasks/num_max_subarray/task.json index 1419a113..57708223 100644 --- a/benchmarks/tasks/num_max_subarray/task.json +++ b/benchmarks/tasks/num_max_subarray/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_modular_power/task.json b/benchmarks/tasks/num_modular_power/task.json index bb2cf2e7..c6768962 100644 --- a/benchmarks/tasks/num_modular_power/task.json +++ b/benchmarks/tasks/num_modular_power/task.json @@ -24,6 +24,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_pair_sum_count/task.json b/benchmarks/tasks/num_pair_sum_count/task.json index 5398bee5..e0b7fbb2 100644 --- a/benchmarks/tasks/num_pair_sum_count/task.json +++ b/benchmarks/tasks/num_pair_sum_count/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_polynomial_value/task.json b/benchmarks/tasks/num_polynomial_value/task.json index c5987fcf..c4da17bf 100644 --- a/benchmarks/tasks/num_polynomial_value/task.json +++ b/benchmarks/tasks/num_polynomial_value/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_prefix_balances/task.json b/benchmarks/tasks/num_prefix_balances/task.json index 522ee009..4c9ee349 100644 --- a/benchmarks/tasks/num_prefix_balances/task.json +++ b/benchmarks/tasks/num_prefix_balances/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_prime_status/task.json b/benchmarks/tasks/num_prime_status/task.json index 573e06cf..b48c6ae4 100644 --- a/benchmarks/tasks/num_prime_status/task.json +++ b/benchmarks/tasks/num_prime_status/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_rotate_left/task.json b/benchmarks/tasks/num_rotate_left/task.json index c4bf3407..51b6b349 100644 --- a/benchmarks/tasks/num_rotate_left/task.json +++ b/benchmarks/tasks/num_rotate_left/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_search_range/task.json b/benchmarks/tasks/num_search_range/task.json index de3a8093..49568b0d 100644 --- a/benchmarks/tasks/num_search_range/task.json +++ b/benchmarks/tasks/num_search_range/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_sorted_insert/task.json b/benchmarks/tasks/num_sorted_insert/task.json index 7d2ad1a9..67165e61 100644 --- a/benchmarks/tasks/num_sorted_insert/task.json +++ b/benchmarks/tasks/num_sorted_insert/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_stable_partition/task.json b/benchmarks/tasks/num_stable_partition/task.json index 8a573ad9..46976ca3 100644 --- a/benchmarks/tasks/num_stable_partition/task.json +++ b/benchmarks/tasks/num_stable_partition/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_staircase_blocked/task.json b/benchmarks/tasks/num_staircase_blocked/task.json index 34a3ad7f..b73da2c2 100644 --- a/benchmarks/tasks/num_staircase_blocked/task.json +++ b/benchmarks/tasks/num_staircase_blocked/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_totient/task.json b/benchmarks/tasks/num_totient/task.json index 93ab0e75..5fe404ee 100644 --- a/benchmarks/tasks/num_totient/task.json +++ b/benchmarks/tasks/num_totient/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_trapped_water/task.json b/benchmarks/tasks/num_trapped_water/task.json index 35105956..2d52c7fa 100644 --- a/benchmarks/tasks/num_trapped_water/task.json +++ b/benchmarks/tasks/num_trapped_water/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_unique_sorted/task.json b/benchmarks/tasks/num_unique_sorted/task.json index 1dfb33a2..0b066ef0 100644 --- a/benchmarks/tasks/num_unique_sorted/task.json +++ b/benchmarks/tasks/num_unique_sorted/task.json @@ -22,6 +22,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/num_window_peak/task.json b/benchmarks/tasks/num_window_peak/task.json index 40e9d3a1..685f9714 100644 --- a/benchmarks/tasks/num_window_peak/task.json +++ b/benchmarks/tasks/num_window_peak/task.json @@ -23,6 +23,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/order_lifecycle/task.json b/benchmarks/tasks/order_lifecycle/task.json index 43d0b040..b22f889f 100644 --- a/benchmarks/tasks/order_lifecycle/task.json +++ b/benchmarks/tasks/order_lifecycle/task.json @@ -16,7 +16,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden; handle every Jett enum variant explicitly"}] }, "python": { diff --git a/benchmarks/tasks/recursive_expression/task.json b/benchmarks/tasks/recursive_expression/task.json index 136759ab..fc273f7b 100644 --- a/benchmarks/tasks/recursive_expression/task.json +++ b/benchmarks/tasks/recursive_expression/task.json @@ -16,7 +16,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden; handle every Jett enum variant explicitly"}] }, "python": { diff --git a/benchmarks/tasks/score_lines/task.json b/benchmarks/tasks/score_lines/task.json index 9a084502..f66fee86 100644 --- a/benchmarks/tasks/score_lines/task.json +++ b/benchmarks/tasks/score_lines/task.json @@ -17,7 +17,7 @@ "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], "forbidden_patterns": [{"pattern": "(?m)^\\s*(?:other|_)\\s*:", "message": "catch-all match arms are forbidden; handle every Jett enum variant explicitly"}] }, "python": { diff --git a/benchmarks/tasks/signed_gcd/task.json b/benchmarks/tasks/signed_gcd/task.json index 4098e371..c9e60086 100644 --- a/benchmarks/tasks/signed_gcd/task.json +++ b/benchmarks/tasks/signed_gcd/task.json @@ -5,7 +5,7 @@ "statement": "Implement signed_gcd using the Euclidean definition. Return the non-negative greatest common divisor of a and b. The gcd of 0 and 0 is 0.", "constraints": ["inputs exclude the minimum signed 64-bit value", "the result fits in a signed 64-bit integer", "do not perform input/output"], "adapters": { - "jett": {"signature": "namespace benchmark\n\nfunction signed_gcd(a: int64, b: int64) returns int64", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, + "jett": {"signature": "namespace benchmark\n\nfunction signed_gcd(a: int64, b: int64) returns int64", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, "python": {"signature": "def signed_gcd(a: int, b: int) -> int", "baseline": "baseline.py", "hidden": "hidden.py", "candidate": "solution.py", "support_files": ["pyrightconfig.json"], "grade_mode": "separate", "commands": [["{pyright}", "--project", "pyrightconfig.json"], ["python", "hidden.py"]]}, "typescript": {"signature": "export function signedGcd(a: bigint, b: bigint): bigint", "baseline": "baseline.ts", "hidden": "hidden.ts", "candidate": "solution.ts", "grade_mode": "separate", "commands": [["npx", "tsc", "--strict", "--target", "ES2022", "--module", "NodeNext", "--moduleResolution", "NodeNext", "solution.ts", "hidden.ts", "--outDir", "out"], ["node", "out/hidden.js"]]}, "go": {"signature": "package benchmark\n\nfunc SignedGCD(a int64, b int64) int64", "baseline": "baseline.go", "hidden": "hidden_test.go", "candidate": "solution.go", "grade_mode": "separate", "commands": [["go", "test", "-run", "^$", "solution.go", "hidden_test.go"], ["go", "test", "solution.go", "hidden_test.go"]]}, diff --git a/benchmarks/tasks/text_ascii_identifier/task.json b/benchmarks/tasks/text_ascii_identifier/task.json index 7d877f94..28b34bfc 100644 --- a/benchmarks/tasks/text_ascii_identifier/task.json +++ b/benchmarks/tasks/text_ascii_identifier/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_binary_parse/task.json b/benchmarks/tasks/text_binary_parse/task.json index 0c6480a1..a5812f7a 100644 --- a/benchmarks/tasks/text_binary_parse/task.json +++ b/benchmarks/tasks/text_binary_parse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_bracket_balance/task.json b/benchmarks/tasks/text_bracket_balance/task.json index 2ddd1c48..7b783de4 100644 --- a/benchmarks/tasks/text_bracket_balance/task.json +++ b/benchmarks/tasks/text_bracket_balance/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_camel_to_snake/task.json b/benchmarks/tasks/text_camel_to_snake/task.json index 89e3e0d2..74625e2b 100644 --- a/benchmarks/tasks/text_camel_to_snake/task.json +++ b/benchmarks/tasks/text_camel_to_snake/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_collapse_spaces/task.json b/benchmarks/tasks/text_collapse_spaces/task.json index ff8dadc4..e8e621cf 100644 --- a/benchmarks/tasks/text_collapse_spaces/task.json +++ b/benchmarks/tasks/text_collapse_spaces/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_common_prefix/task.json b/benchmarks/tasks/text_common_prefix/task.json index 701bf823..606944e0 100644 --- a/benchmarks/tasks/text_common_prefix/task.json +++ b/benchmarks/tasks/text_common_prefix/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_count_overlaps/task.json b/benchmarks/tasks/text_count_overlaps/task.json index d57f8cbe..823f7a87 100644 --- a/benchmarks/tasks/text_count_overlaps/task.json +++ b/benchmarks/tasks/text_count_overlaps/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_csv_record_parse/task.json b/benchmarks/tasks/text_csv_record_parse/task.json index 4d60d836..2b03f60a 100644 --- a/benchmarks/tasks/text_csv_record_parse/task.json +++ b/benchmarks/tasks/text_csv_record_parse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_decimal_canonical/task.json b/benchmarks/tasks/text_decimal_canonical/task.json index 9dd5f895..e9fa7b63 100644 --- a/benchmarks/tasks/text_decimal_canonical/task.json +++ b/benchmarks/tasks/text_decimal_canonical/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_escape_html/task.json b/benchmarks/tasks/text_escape_html/task.json index 3bd818d8..7875a6d7 100644 --- a/benchmarks/tasks/text_escape_html/task.json +++ b/benchmarks/tasks/text_escape_html/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_escape_json_string/task.json b/benchmarks/tasks/text_escape_json_string/task.json index 58e4a6f9..672ce4c3 100644 --- a/benchmarks/tasks/text_escape_json_string/task.json +++ b/benchmarks/tasks/text_escape_json_string/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_extension_split/task.json b/benchmarks/tasks/text_extension_split/task.json index 2f987ecb..c0d46f04 100644 --- a/benchmarks/tasks/text_extension_split/task.json +++ b/benchmarks/tasks/text_extension_split/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_glob_match/task.json b/benchmarks/tasks/text_glob_match/task.json index 488a001e..4be40662 100644 --- a/benchmarks/tasks/text_glob_match/task.json +++ b/benchmarks/tasks/text_glob_match/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_hex_decode/task.json b/benchmarks/tasks/text_hex_decode/task.json index 08f0280a..e85d2030 100644 --- a/benchmarks/tasks/text_hex_decode/task.json +++ b/benchmarks/tasks/text_hex_decode/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_ipv4_parse/task.json b/benchmarks/tasks/text_ipv4_parse/task.json index 4a2eff74..3d3b773b 100644 --- a/benchmarks/tasks/text_ipv4_parse/task.json +++ b/benchmarks/tasks/text_ipv4_parse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_key_value_parse/task.json b/benchmarks/tasks/text_key_value_parse/task.json index 9b75504d..b510b86c 100644 --- a/benchmarks/tasks/text_key_value_parse/task.json +++ b/benchmarks/tasks/text_key_value_parse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_longest_word/task.json b/benchmarks/tasks/text_longest_word/task.json index e0b36b6e..738a32b0 100644 --- a/benchmarks/tasks/text_longest_word/task.json +++ b/benchmarks/tasks/text_longest_word/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_palindrome_filter/task.json b/benchmarks/tasks/text_palindrome_filter/task.json index f2fbf869..c822e8eb 100644 --- a/benchmarks/tasks/text_palindrome_filter/task.json +++ b/benchmarks/tasks/text_palindrome_filter/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_path_normalize/task.json b/benchmarks/tasks/text_path_normalize/task.json index b77845fb..36842281 100644 --- a/benchmarks/tasks/text_path_normalize/task.json +++ b/benchmarks/tasks/text_path_normalize/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_percent_decode/task.json b/benchmarks/tasks/text_percent_decode/task.json index 9d16ee22..e97438a1 100644 --- a/benchmarks/tasks/text_percent_decode/task.json +++ b/benchmarks/tasks/text_percent_decode/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_reverse_words/task.json b/benchmarks/tasks/text_reverse_words/task.json index 775c0359..c298ec79 100644 --- a/benchmarks/tasks/text_reverse_words/task.json +++ b/benchmarks/tasks/text_reverse_words/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_rle_decode/task.json b/benchmarks/tasks/text_rle_decode/task.json index 678da4ff..046ce3b6 100644 --- a/benchmarks/tasks/text_rle_decode/task.json +++ b/benchmarks/tasks/text_rle_decode/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_rle_encode/task.json b/benchmarks/tasks/text_rle_encode/task.json index 99291bc7..d7ac6ed2 100644 --- a/benchmarks/tasks/text_rle_encode/task.json +++ b/benchmarks/tasks/text_rle_encode/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_strip_line_comments/task.json b/benchmarks/tasks/text_strip_line_comments/task.json index eb1d56ab..2d6631c3 100644 --- a/benchmarks/tasks/text_strip_line_comments/task.json +++ b/benchmarks/tasks/text_strip_line_comments/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_time_parse/task.json b/benchmarks/tasks/text_time_parse/task.json index d553cce4..fe942115 100644 --- a/benchmarks/tasks/text_time_parse/task.json +++ b/benchmarks/tasks/text_time_parse/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_title_words/task.json b/benchmarks/tasks/text_title_words/task.json index a1d607cb..823335fa 100644 --- a/benchmarks/tasks/text_title_words/task.json +++ b/benchmarks/tasks/text_title_words/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_unescape_backslash/task.json b/benchmarks/tasks/text_unescape_backslash/task.json index 2b537f9d..e63d293d 100644 --- a/benchmarks/tasks/text_unescape_backslash/task.json +++ b/benchmarks/tasks/text_unescape_backslash/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_version_compare/task.json b/benchmarks/tasks/text_version_compare/task.json index 5b9d2498..af063a71 100644 --- a/benchmarks/tasks/text_version_compare/task.json +++ b/benchmarks/tasks/text_version_compare/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_word_initials/task.json b/benchmarks/tasks/text_word_initials/task.json index 77b37eff..41590c85 100644 --- a/benchmarks/tasks/text_word_initials/task.json +++ b/benchmarks/tasks/text_word_initials/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/text_wrap_words/task.json b/benchmarks/tasks/text_wrap_words/task.json index 99e4208f..8b3d4e88 100644 --- a/benchmarks/tasks/text_wrap_words/task.json +++ b/benchmarks/tasks/text_wrap_words/task.json @@ -21,6 +21,7 @@ [ "{jett}", "build", + "--check", "{candidate}" ], [ diff --git a/benchmarks/tasks/triangle_kind/task.json b/benchmarks/tasks/triangle_kind/task.json index a06bf527..dd36e8e4 100644 --- a/benchmarks/tasks/triangle_kind/task.json +++ b/benchmarks/tasks/triangle_kind/task.json @@ -5,7 +5,7 @@ "statement": "Implement triangle_kind for three integer side lengths. Return invalid when any side is non-positive or the three sides cannot form a non-degenerate triangle. Otherwise return equilateral, isosceles, or scalene.", "constraints": ["-10000 <= each side <= 10000", "use exact lowercase output spellings", "do not perform input/output"], "adapters": { - "jett": {"signature": "namespace benchmark\n\nfunction triangle_kind(a: int64, b: int64, c: int64) returns string", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, + "jett": {"signature": "namespace benchmark\n\nfunction triangle_kind(a: int64, b: int64, c: int64) returns string", "baseline": "baseline.jett", "hidden": "hidden.jett", "candidate": "solution.jett", "grade_mode": "append", "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]]}, "python": {"signature": "def triangle_kind(a: int, b: int, c: int) -> str", "baseline": "baseline.py", "hidden": "hidden.py", "candidate": "solution.py", "support_files": ["pyrightconfig.json"], "grade_mode": "separate", "commands": [["{pyright}", "--project", "pyrightconfig.json"], ["python", "hidden.py"]]}, "typescript": {"signature": "export function triangleKind(a: number, b: number, c: number): string", "baseline": "baseline.ts", "hidden": "hidden.ts", "candidate": "solution.ts", "grade_mode": "separate", "commands": [["npx", "tsc", "--strict", "--target", "ES2022", "--module", "NodeNext", "--moduleResolution", "NodeNext", "solution.ts", "hidden.ts", "--outDir", "out"], ["node", "out/hidden.js"]]}, "go": {"signature": "package benchmark\n\nfunc TriangleKind(a int64, b int64, c int64) string", "baseline": "baseline.go", "hidden": "hidden_test.go", "candidate": "solution.go", "grade_mode": "separate", "commands": [["go", "test", "-run", "^$", "solution.go", "hidden_test.go"], ["go", "test", "solution.go", "hidden_test.go"]]}, diff --git a/crates/jett_cli/src/main.rs b/crates/jett_cli/src/main.rs index d56e0350..8d450f3a 100644 --- a/crates/jett_cli/src/main.rs +++ b/crates/jett_cli/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process; /// The Jett programming language compiler @@ -39,8 +39,20 @@ enum Command { #[arg(long)] agent: bool, - /// Target triple for cross-compilation + /// Validate source without emitting an executable + #[arg(long, conflicts_with_all = ["output", "runtime_bundle"])] + check: bool, + + /// Executable output path (default: target///) + #[arg(short = 'o', long)] + output: Option, + + /// Launcher manifest (otherwise use the installed runtime bundle) #[arg(long)] + runtime_bundle: Option, + + /// Target triple (only the current supported native host is accepted) + #[arg(long, value_parser = parse_build_target)] target: Option, }, @@ -137,6 +149,22 @@ enum Command { Lsp, } +fn parse_build_target(target: &str) -> Result { + use jett_driver::native::{LINUX_GNU_NATIVE_TARGET, WINDOWS_MSVC_NATIVE_TARGET, host_target}; + let host = host_target(); + if target != host + || !matches!( + host.as_str(), + LINUX_GNU_NATIVE_TARGET | WINDOWS_MSVC_NATIVE_TARGET + ) + { + return Err(format!( + "unsupported native target `{target}` on host `{host}`; cross-compilation is not supported (native hosts: {LINUX_GNU_NATIVE_TARGET}, {WINDOWS_MSVC_NATIVE_TARGET})" + )); + } + Ok(target.to_owned()) +} + fn main() { let cli = Cli::parse(); @@ -243,46 +271,20 @@ fn main() { file, release, agent, + check, + output, + runtime_bundle, target: _, } => { - let path = Path::new(&file); - let result = - jett_driver::build_file_with_options(path, jett_driver::BuildOptions { release }); - - if agent { - // TOON agent output mode - let toon_output = jett_diagnostics::toon::render_toon( - &result.diagnostics, - &result.source, - &result.file_path, - ); - print!("{toon_output}"); - - if result.has_errors { - process::exit(1); - } - } else { - // Human-readable output mode - for diag in &result.diagnostics { - let rendered = jett_diagnostics::render::render_diagnostic( - diag, - &result.source, - &result.file_path, - ); - eprint!("{rendered}"); - } - - if result.has_errors { - let error_count = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .count(); - eprintln!("build failed: {error_count} error(s)"); - process::exit(1); - } else { - println!("build ok: {file} (type checked, no codegen yet)"); - } + if !build_command( + &file, + release, + agent, + check, + output.as_deref(), + runtime_bundle.as_deref(), + ) { + process::exit(1); } } Command::Run { @@ -320,47 +322,32 @@ fn main() { if profile_request.is_some() { let result = jett_driver::build_file(Path::new(&file)); if result.has_errors { - if agent { - print!( - "{}", - jett_diagnostics::toon::render_toon( - &result.diagnostics, - &result.source, - &result.file_path - ) - ); - } else { - for diagnostic in &result.diagnostics { - eprint!( - "{}", - jett_diagnostics::render::render_diagnostic( - diagnostic, - &result.source, - &result.file_path - ) - ); - } - } + render_build_diagnostics(&result, agent); process::exit(1); } if agent { print!( "{}", - render_run_agent_error(&file, "profiler: backend unsupported") + render_observed_agent_error( + &file, + "profiler: backend unsupported", + &result.debug_observations, + ) ); } else { + emit_debug_observations(&result.debug_observations); eprintln!("profiler: backend unsupported"); } process::exit(1); } let path = Path::new(&file); if agent { - match jett_driver::run_file_capture_output(path) { + match jett_driver::run_file_capture_outcome(path) { Ok(output) => { print!("{}", render_run_agent_output(&file, &output)); } Err(e) => { - print!("{}", render_run_agent_error(&file, &e)); + print!("{}", render_run_agent_failure(&file, &e)); process::exit(1); } } @@ -385,6 +372,7 @@ fn main() { if agent { print!("{}", render_bundle_agent_output(&result)); } else { + emit_debug_observations(&result.debug_observations); println!( "bundled {} files into {}", result.files.len(), @@ -396,6 +384,7 @@ fn main() { if agent { print!("{}", render_bundle_agent_error(&start_path, &output, &e)); } else { + emit_debug_observations(e.debug_observations()); eprintln!("bundle error: {e}"); } process::exit(1); @@ -647,7 +636,7 @@ fn main() { if let Some(f) = file { // Test a single file let path = Path::new(&f); - match jett_driver::test_file(path) { + match jett_driver::test_file_capture_outcome(path) { Ok(result) => { if agent { print!("{}", render_test_agent_file_output(&result)); @@ -664,8 +653,9 @@ fn main() { } Err(e) => { if agent { - print!("{}", render_test_agent_error(Some(&f), &e)); + print!("{}", render_test_agent_failure(&e)); } else { + print_test_failure_output(&e); eprintln!("error testing {f}: {e}"); } process::exit(1); @@ -674,7 +664,7 @@ fn main() { } else { // Discover project and test all files let cwd = std::env::current_dir().unwrap_or_default(); - match jett_driver::test_project(&cwd) { + match jett_driver::test_project_capture_outcome(&cwd) { Ok(result) => { if agent { print!("{}", render_test_agent_project_output(&result)); @@ -698,8 +688,9 @@ fn main() { } Err(e) => { if agent { - print!("{}", render_test_agent_error(None, &e)); + print!("{}", render_test_agent_failure(&e)); } else { + print_test_failure_output(&e); eprintln!("error: {e}"); } process::exit(1); @@ -711,34 +702,95 @@ fn main() { } fn render_run_agent_output(file: &str, output: &jett_driver::RunOutput) -> String { - let mut out = String::new(); - out.push_str("status: ok\n"); + render_run_agent_outcome(file, output, None) +} + +fn render_run_agent_failure(file: &str, failure: &jett_driver::RunFailure) -> String { + render_run_agent_outcome(file, &failure.output, Some(&failure.message)) +} + +fn render_run_agent_outcome( + file: &str, + output: &jett_driver::RunOutput, + error: Option<&str>, +) -> String { + let mut out = format!("status: {}\n", if error.is_some() { "error" } else { "ok" }); out.push_str(&format!("file: {}\n", escape_toon_scalar(file))); + if let Some(error) = error { + out.push_str(&format!("error: {}\n", escape_toon_scalar(error))); + } out.push_str(&format!("stdout: {}\n", escape_toon_scalar(&output.stdout))); out.push_str(&format!( - "debug[{}]{{kind,message}}:\n", - output.debug_output.len() + "debug[{}]{{phase,kind,text}}:\n", + output.frontend_debug_observations.len() + output.debug_events.len(), )); - for line in &output.debug_output { - out.push_str(&format!( - " {},{}\n", - debug_line_kind(line), - escape_toon_scalar(line) - )); + for observation in &output.frontend_debug_observations { + append_debug_agent_row(&mut out, observation.phase, &observation.event); + } + for event in &output.debug_events { + append_debug_agent_row(&mut out, jett_driver::DebugPhase::Runtime, event); } out } -fn debug_line_kind(line: &str) -> &'static str { - if line.starts_with("trace ") { - "trace" - } else if line.starts_with("breakpoint hit") { - "breakpoint" - } else { - "debug" +fn append_debug_agent_row( + out: &mut String, + phase: jett_driver::DebugPhase, + event: &jett_driver::DebugEvent, +) { + out.push_str(&format!( + " {},{},{}\n", + phase.as_str(), + event.kind.as_str(), + escape_toon_scalar(&event.text), + )); +} + +fn append_debug_agent_observations( + out: &mut String, + observations: &[jett_driver::DebugObservation], +) { + out.push_str(&format!( + "debug[{}]{{phase,kind,text}}:\n", + observations.len() + )); + for observation in observations { + append_debug_agent_row(out, observation.phase, &observation.event); + } +} + +fn render_observed_agent_error( + file: &str, + error: &str, + observations: &[jett_driver::DebugObservation], +) -> String { + let mut out = render_run_agent_error(file, error); + append_debug_agent_observations(&mut out, observations); + out +} + +fn emit_debug_observations(observations: &[jett_driver::DebugObservation]) { + for observation in observations { + eprint!("{}", observation.event.text); + } +} + +fn emit_test_observations(result: &jett_driver::TestResult) { + emit_debug_observations(&result.debug_observations); + for block in &result.blocks { + eprint!("{}", jett_driver::render_debug_events(&block.debug_events)); } } +fn print_test_failure_output(failure: &jett_driver::TestFailure) { + for result in &failure.completed_files { + println!("--- {} ---", result.file_path); + print_file_results(result); + println!(); + } + emit_debug_observations(&failure.debug_observations); +} + fn render_format_agent_output(file: &str, mode: &str, changed: bool, errors: &[String]) -> String { let status = if !errors.is_empty() || (mode == "check" && changed) { "error" @@ -787,6 +839,7 @@ fn render_test_agent_file_output(result: &jett_driver::TestResult) -> String { out.push_str(&format!("passed: {}\n", result.passed)); out.push_str(&format!("failed: {}\n", result.failed)); append_test_agent_blocks(&mut out, std::slice::from_ref(result)); + append_test_agent_observations(&mut out, std::slice::from_ref(result), None); out } @@ -805,6 +858,7 @@ fn render_test_agent_project_output(result: &jett_driver::ProjectTestResult) -> out.push_str(&format!("passed: {}\n", result.total_passed)); out.push_str(&format!("failed: {}\n", result.total_failed)); append_test_agent_blocks(&mut out, &result.file_results); + append_test_agent_observations(&mut out, &result.file_results, None); out } @@ -818,6 +872,17 @@ fn render_test_agent_error(file: Option<&str>, error: &str) -> String { out } +fn render_test_agent_failure(failure: &jett_driver::TestFailure) -> String { + let mut out = render_test_agent_error(Some(&failure.file_path), &failure.message); + append_test_agent_blocks(&mut out, &failure.completed_files); + append_test_agent_observations( + &mut out, + &failure.completed_files, + Some((&failure.file_path, &failure.debug_observations)), + ); + out +} + fn render_bundle_agent_output(result: &jett_driver::BundleResult) -> String { let mut out = String::new(); out.push_str("status: ok\n"); @@ -842,6 +907,7 @@ fn render_bundle_agent_output(result: &jett_driver::BundleResult) -> String { file.end_line )); } + append_debug_agent_observations(&mut out, &result.debug_observations); out } @@ -876,6 +942,7 @@ fn render_bundle_agent_error( escape_toon_scalar(&error.to_string()) )); } + append_debug_agent_observations(&mut out, error.debug_observations()); out } @@ -1366,6 +1433,70 @@ fn append_test_agent_blocks(out: &mut String, file_results: &[jett_driver::TestR } } +fn append_test_agent_observations( + out: &mut String, + file_results: &[jett_driver::TestResult], + fatal: Option<(&str, &[jett_driver::DebugObservation])>, +) { + let count: usize = file_results + .iter() + .map(|result| { + result.debug_observations.len() + + result + .blocks + .iter() + .map(|block| block.debug_events.len()) + .sum::() + }) + .sum::() + + fatal.map_or(0, |(_, observations)| observations.len()); + out.push_str(&format!("debug[{count}]{{file,block,phase,kind,text}}:\n")); + for result in file_results { + for observation in &result.debug_observations { + append_test_agent_debug_row( + out, + &result.file_path, + "", + observation.phase, + &observation.event, + ); + } + for block in &result.blocks { + for event in &block.debug_events { + append_test_agent_debug_row( + out, + &result.file_path, + &block.name, + jett_driver::DebugPhase::FrontendVerify, + event, + ); + } + } + } + if let Some((file, observations)) = fatal { + for observation in observations { + append_test_agent_debug_row(out, file, "", observation.phase, &observation.event); + } + } +} + +fn append_test_agent_debug_row( + out: &mut String, + file: &str, + block: &str, + phase: jett_driver::DebugPhase, + event: &jett_driver::DebugEvent, +) { + out.push_str(&format!( + " {},{},{},{},{}\n", + escape_toon_scalar(file), + escape_toon_scalar(block), + phase.as_str(), + event.kind.as_str(), + escape_toon_scalar(&event.text), + )); +} + fn escape_toon_scalar(value: &str) -> String { value .replace('\\', "\\\\") @@ -1375,6 +1506,7 @@ fn escape_toon_scalar(value: &str) -> String { } fn print_file_results(result: &jett_driver::TestResult) { + emit_test_observations(result); for block in &result.blocks { let name = &block.name; if block.is_property { @@ -1398,21 +1530,185 @@ fn print_file_results(result: &jett_driver::TestResult) { mod tests { use super::*; + fn event(kind: jett_driver::DebugEventKind, text: &str) -> jett_driver::DebugEvent { + jett_driver::DebugEvent { + kind, + text: text.into(), + } + } + + fn observation( + phase: jett_driver::DebugPhase, + kind: jett_driver::DebugEventKind, + text: &str, + ) -> jett_driver::DebugObservation { + jett_driver::DebugObservation { + phase, + event: event(kind, text), + } + } + + #[test] + fn run_agent_capture_preserves_phase_kind_empty_fragments_and_protocol_like_text() { + let output = jett_driver::RunOutput { + stdout: "application\n".into(), + frontend_debug_observations: vec![ + observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Print, + "", + ), + observation( + jett_driver::DebugPhase::FrontendVerify, + jett_driver::DebugEventKind::Println, + "verify\n", + ), + ], + debug_events: vec![ + event(jett_driver::DebugEventKind::Print, "trace fake"), + event( + jett_driver::DebugEventKind::Println, + "status: error\nrows[1]{x}:\r\n\u{03bb},\\\n", + ), + event(jett_driver::DebugEventKind::Trace, "trace actual\n"), + ], + }; + assert_eq!( + render_run_agent_output("app,one.jett", &output), + concat!( + "status: ok\nfile: app\\,one.jett\nstdout: application\\n\n", + "debug[5]{phase,kind,text}:\n", + " comptime,print,\n", + " frontend_verify,println,verify\\n\n", + " runtime,print,trace fake\n", + " runtime,println,status: error\\nrows[1]{x}:\\r\\n\u{03bb}\\,\\\\\\n\n", + " runtime,trace,trace actual\\n\n", + ) + ); + } + + #[test] + fn run_agent_failure_preserves_application_and_debug_output_before_terminal_error() { + let failure = jett_driver::RunFailure { + message: "runtime error: failed\nargument".into(), + output: jett_driver::RunOutput { + stdout: "before\n".into(), + frontend_debug_observations: vec![observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Println, + "baked\n", + )], + debug_events: vec![event(jett_driver::DebugEventKind::Print, "partial:")], + }, + }; + assert_eq!( + render_run_agent_failure("app.jett", &failure), + concat!( + "status: error\nfile: app.jett\nerror: runtime error: failed\\nargument\n", + "stdout: before\\n\ndebug[2]{phase,kind,text}:\n", + " comptime,println,baked\\n\n runtime,print,partial:\n", + ) + ); + } + + #[test] + fn build_and_setup_reports_retain_actual_frontend_observations() { + let observations = vec![observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Print, + "diagnostics[0]{fake}:\n", + )]; + let report = render_build_agent_report(&[], "", "app.jett", &observations); + assert_eq!( + report, + concat!( + "status: ok\nfile: app.jett\ntotal: 0\nerrors: 0\nwarnings: 0\ninfos: 0\n", + "diagnostics[0]{code,severity,message,file,line,column,end_line,end_column}:\n", + "labels[0]{code,message,file,line,column,end_line,end_column}:\n", + "suggested_fixes[0]{code,file,line,column,end_line,end_column,old_text,new_text,explanation}:\n", + "debug[1]{phase,kind,text}:\n comptime,print,diagnostics[0]{fake}:\\n\n", + ) + ); + assert_eq!( + render_observed_agent_error("app.jett", "profiler: backend unsupported", &observations), + concat!( + "status: error\nfile: app.jett\nerror: profiler: backend unsupported\n", + "debug[1]{phase,kind,text}:\n comptime,print,diagnostics[0]{fake}:\\n\n", + ) + ); + } + + #[test] + fn fatal_project_test_reports_prior_blocks_and_current_failed_comptime_capture_once() { + let completed = jett_driver::TestResult { + total: 1, + passed: 0, + failed: 1, + file_path: "first,one.jett".into(), + debug_observations: vec![observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Print, + "first:", + )], + blocks: vec![jett_driver::TestBlockResult { + name: "failed,block".into(), + passed: false, + error: Some("expected".into()), + is_property: false, + iterations: None, + line: 1, + column: 1, + end_line: 1, + end_column: 2, + debug_events: vec![event(jett_driver::DebugEventKind::Println, "trace fake\n")], + }], + }; + let failure = jett_driver::TestFailure { + message: "comptime errors:\nfailed".into(), + file_path: "second.jett".into(), + completed_files: vec![completed], + debug_observations: vec![observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Print, + "last:", + )], + }; + assert_eq!( + render_test_agent_failure(&failure), + concat!( + "status: error\nfile: second.jett\nerror: comptime errors:\\nfailed\n", + "blocks[1]{file,name,kind,status,iterations,line,column,end_line,end_column,error}:\n", + " first\\,one.jett,failed\\,block,verify,failed,,1,1,1,2,expected\n", + "debug[3]{file,block,phase,kind,text}:\n", + " first\\,one.jett,,comptime,print,first:\n", + " first\\,one.jett,failed\\,block,frontend_verify,println,trace fake\\n\n", + " second.jett,,comptime,print,last:\n", + ) + ); + } + #[test] fn run_agent_output_includes_stdout_and_debug_rows() { let output = jett_driver::RunOutput { stdout: "hello\n".to_string(), - debug_output: vec![ - "trace total: int64 = 42".to_string(), - "breakpoint hit: total: int64 = 42".to_string(), + debug_events: vec![ + event( + jett_driver::DebugEventKind::Trace, + "trace total: int64 = 42\n", + ), + event( + jett_driver::DebugEventKind::Breakpoint, + "breakpoint hit: total: int64 = 42\n", + ), ], + frontend_debug_observations: Vec::new(), }; let rendered = render_run_agent_output("app.jett", &output); assert_eq!( rendered, - "status: ok\nfile: app.jett\nstdout: hello\\n\ndebug[2]{kind,message}:\n trace,trace total: int64 = 42\n breakpoint,breakpoint hit: total: int64 = 42\n" + "status: ok\nfile: app.jett\nstdout: hello\\n\ndebug[2]{phase,kind,text}:\n runtime,trace,trace total: int64 = 42\\n\n runtime,breakpoint,breakpoint hit: total: int64 = 42\\n\n" ); } @@ -1454,6 +1750,7 @@ mod tests { passed: 1, failed: 1, file_path: "tests/sample.jett".to_string(), + debug_observations: Vec::new(), blocks: vec![ jett_driver::TestBlockResult { name: "adds".to_string(), @@ -1465,6 +1762,7 @@ mod tests { column: 8, end_line: 3, end_column: 12, + debug_events: Vec::new(), }, jett_driver::TestBlockResult { name: "roundtrip".to_string(), @@ -1476,6 +1774,7 @@ mod tests { column: 10, end_line: 8, end_column: 19, + debug_events: Vec::new(), }, ], }; @@ -1484,7 +1783,7 @@ mod tests { assert_eq!( rendered, - "status: error\nfiles: 1\ntotal: 2\npassed: 1\nfailed: 1\nblocks[2]{file,name,kind,status,iterations,line,column,end_line,end_column,error}:\n tests/sample.jett,adds,verify,passed,,3,8,3,12,\n tests/sample.jett,roundtrip,property,failed,42,8,10,8,19,expected ok\\, got bad\\ncase\n" + "status: error\nfiles: 1\ntotal: 2\npassed: 1\nfailed: 1\nblocks[2]{file,name,kind,status,iterations,line,column,end_line,end_column,error}:\n tests/sample.jett,adds,verify,passed,,3,8,3,12,\n tests/sample.jett,roundtrip,property,failed,42,8,10,8,19,expected ok\\, got bad\\ncase\ndebug[0]{file,block,phase,kind,text}:\n" ); } @@ -1501,6 +1800,7 @@ mod tests { passed: 0, failed: 0, file_path: "tests/empty.jett".to_string(), + debug_observations: Vec::new(), blocks: Vec::new(), }, jett_driver::TestResult { @@ -1508,6 +1808,7 @@ mod tests { passed: 1, failed: 0, file_path: "tests/checks.jett".to_string(), + debug_observations: Vec::new(), blocks: vec![jett_driver::TestBlockResult { name: "ok".to_string(), passed: true, @@ -1518,6 +1819,7 @@ mod tests { column: 8, end_line: 4, end_column: 10, + debug_events: Vec::new(), }], }, ], @@ -1527,7 +1829,7 @@ mod tests { assert_eq!( rendered, - "status: ok\nfiles: 2\ntotal: 1\npassed: 1\nfailed: 0\nblocks[1]{file,name,kind,status,iterations,line,column,end_line,end_column,error}:\n tests/checks.jett,ok,verify,passed,,4,8,4,10,\n" + "status: ok\nfiles: 2\ntotal: 1\npassed: 1\nfailed: 0\nblocks[1]{file,name,kind,status,iterations,line,column,end_line,end_column,error}:\n tests/checks.jett,ok,verify,passed,,4,8,4,10,\ndebug[0]{file,block,phase,kind,text}:\n" ); } @@ -1546,6 +1848,18 @@ mod tests { let result = jett_driver::BundleResult { project_root: "project".to_string(), output_path: "dist/lib.jett".to_string(), + debug_observations: vec![ + observation( + jett_driver::DebugPhase::Comptime, + jett_driver::DebugEventKind::Print, + "bundle:", + ), + observation( + jett_driver::DebugPhase::FrontendVerify, + jett_driver::DebugEventKind::Println, + "trace fake\n", + ), + ], files: vec![jett_driver::BundleFileResult { path: "src/core.jett".to_string(), start_line: 4, @@ -1557,7 +1871,7 @@ mod tests { assert_eq!( rendered, - "status: ok\nproject_root: project\noutput: dist/lib.jett\nfiles: 1\nbundled_files[1]{path,start_line,end_line}:\n src/core.jett,4,12\n" + "status: ok\nproject_root: project\noutput: dist/lib.jett\nfiles: 1\nbundled_files[1]{path,start_line,end_line}:\n src/core.jett,4,12\ndebug[2]{phase,kind,text}:\n comptime,print,bundle:\n frontend_verify,println,trace fake\\n\n" ); } @@ -1771,10 +2085,17 @@ mod tests { .expect_err("private cross-file use should fail resolution"); let rendered = render_query_diagnostic_agent_error(&error); + let canonical_support = + std::fs::canonicalize(&support_file).expect("canonical support file"); + let support_path = canonical_support.display().to_string(); + let support_path = support_path.strip_prefix(r"\\?\").unwrap_or(&support_path); std::fs::remove_dir_all(&root).expect("temporary query directory should be removed"); assert!(rendered.contains("labels[1]{code,message,file,line,column,end_line,end_column}:")); - assert!(rendered.contains(&escape_toon_scalar(&support_file.display().to_string()))); + assert!( + rendered.contains(&escape_toon_scalar(&support_path)), + "{rendered}" + ); assert!(!rendered.contains("labels[0]")); } @@ -1935,3 +2256,174 @@ mod tests { ); } } + +fn render_build_agent_report( + diagnostics: &[jett_diagnostics::Diagnostic], + source: &str, + file: &str, + observations: &[jett_driver::DebugObservation], +) -> String { + let mut out = jett_diagnostics::toon::render_toon(diagnostics, source, file); + append_debug_agent_observations(&mut out, observations); + out +} + +fn render_build_diagnostics(result: &jett_driver::BuildResult, agent: bool) { + if agent { + print!( + "{}", + render_build_agent_report( + &result.diagnostics, + &result.source, + &result.file_path, + &result.debug_observations, + ) + ); + } else { + emit_debug_observations(&result.debug_observations); + for diagnostic in &result.diagnostics { + eprint!( + "{}", + jett_diagnostics::render::render_diagnostic( + diagnostic, + &result.source, + &result.file_path + ) + ); + } + } +} + +fn report_build_error( + file: &str, + message: &str, + agent: bool, + observations: &[jett_driver::DebugObservation], +) -> bool { + if agent { + print!( + "{}", + render_observed_agent_error(file, message, observations) + ); + } else { + emit_debug_observations(observations); + eprintln!("build failed: {message}"); + } + false +} + +fn build_command( + file: &str, + release: bool, + agent: bool, + check: bool, + output: Option<&Path>, + runtime_bundle: Option<&Path>, +) -> bool { + let options = jett_driver::BuildOptions { release }; + if check { + let result = jett_driver::build_file_with_options(Path::new(file), options); + render_build_diagnostics(&result, agent); + if !agent && !result.has_errors { + println!("check ok: {file}"); + } + return !result.has_errors; + } + let object = match jett_driver::native::emit_host_program_object_for_file_with_options( + Path::new(file), + options, + ) { + Ok(object) => object, + Err(error) => { + if let Some(result) = error.build_result() { + render_build_diagnostics(result, agent); + return false; + } + return report_build_error(file, &error.to_string(), agent, error.debug_observations()); + } + }; + let report_error = + |message: &str| report_build_error(file, message, agent, object.debug_observations()); + if !agent { + for diagnostic in object.diagnostics() { + eprint!( + "{}", + jett_diagnostics::render::render_diagnostic(diagnostic, object.source(), file) + ); + } + } + let host = jett_driver::native::host_target(); + let profile = if release { "release" } else { "debug" }; + let manifest = runtime_bundle + .map(Path::to_path_buf) + .or_else(|| std::env::var_os("JETT_NATIVE_RUNTIME_BUNDLE").map(PathBuf::from)) + .or_else(|| { + std::env::current_exe().ok().and_then(|executable| { + executable.parent().and_then(Path::parent).map(|prefix| { + prefix + .join("lib") + .join("jett") + .join("runtime") + .join(&host) + .join(profile) + .join("launcher.json") + }) + }) + }); + let Some(manifest) = manifest else { + return report_error("cannot locate the native runtime; use --runtime-bundle"); + }; + let bundle = match jett_driver::native::NativeLauncherBundle::from_manifest(&manifest, release) + { + Ok(bundle) => bundle, + Err(error) => return report_error(&error), + }; + let destination = if let Some(output) = output { + output.to_path_buf() + } else { + let stem = Path::new(file) + .file_stem() + .unwrap_or_else(|| std::ffi::OsStr::new("program")); + let mut destination = PathBuf::from("target").join(&host).join(profile).join(stem); + if cfg!(windows) { + destination.set_extension("exe"); + } + destination + }; + if let Some(parent) = destination + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + if let Err(error) = std::fs::create_dir_all(parent) { + return report_error(&format!( + "cannot create output directory `{}`: {error}", + parent.display() + )); + } + } + match jett_driver::native::link_host_object(&object, &bundle, &destination) { + Ok(artifact) => { + if agent { + print!( + "{}", + render_build_agent_report( + object.diagnostics(), + object.source(), + file, + object.debug_observations(), + ) + ); + println!( + "artifact: {}\ntarget: {}", + escape_toon_scalar(&artifact.path.display().to_string()), + escape_toon_scalar(&artifact.target) + ); + } else { + emit_debug_observations(object.debug_observations()); + println!("build ok: {}", artifact.path.display()); + } + true + } + Err(error) => report_error(&error.to_string()), + } +} diff --git a/crates/jett_cli/tests/build_release.rs b/crates/jett_cli/tests/build_release.rs index 1605f1b2..d1a704c3 100644 --- a/crates/jett_cli/tests/build_release.rs +++ b/crates/jett_cli/tests/build_release.rs @@ -14,6 +14,7 @@ fn build_release_rejects_debug_printing() { let debug = Command::new(env!("CARGO_BIN_EXE_jett")) .arg("build") + .arg("--check") .arg(&fixture) .output() .expect("debug build command should run"); @@ -25,6 +26,7 @@ fn build_release_rejects_debug_printing() { let release = Command::new(env!("CARGO_BIN_EXE_jett")) .arg("build") + .arg("--check") .arg("--release") .arg(&fixture) .output() diff --git a/crates/jett_cli/tests/build_target.rs b/crates/jett_cli/tests/build_target.rs new file mode 100644 index 00000000..56f11de5 --- /dev/null +++ b/crates/jett_cli/tests/build_target.rs @@ -0,0 +1,44 @@ +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn build_rejects_cross_target_before_reading_source() { + for target in ["wasm32-unknown-unknown", "not-a-target"] { + let output = Command::new(env!("CARGO_BIN_EXE_jett")) + .args(["build", "absent-source.jett", "--target", target]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("unsupported native target"), "{stderr}"); + assert!(stderr.contains(target), "{stderr}"); + assert!( + stderr.contains(&jett_driver::native::host_target()), + "{stderr}" + ); + assert!(!stderr.contains("No such file"), "{stderr}"); + assert!(output.stdout.is_empty()); + } +} + +#[test] +fn build_accepts_explicit_supported_host() { + let host = jett_driver::native::host_target(); + if !matches!( + host.as_str(), + jett_driver::native::LINUX_GNU_NATIVE_TARGET + | jett_driver::native::WINDOWS_MSVC_NATIVE_TARGET + ) { + return; + } + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/native_scalar_entry.jett"); + let output = Command::new(env!("CARGO_BIN_EXE_jett")) + .arg("build") + .arg("--check") + .arg(source) + .args(["--target", &host]) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); +} diff --git a/crates/jett_cli/tests/debug_capture.rs b/crates/jett_cli/tests/debug_capture.rs new file mode 100644 index 00000000..4f56d280 --- /dev/null +++ b/crates/jett_cli/tests/debug_capture.rs @@ -0,0 +1,393 @@ +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +struct Fixture(PathBuf); + +impl Fixture { + fn new(source: &str) -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + let directory = std::env::temp_dir().join(format!( + "jett-cli-debug-capture-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("app.jett"), source).unwrap(); + Self(directory) + } + + fn source(&self) -> PathBuf { + self.0.join("app.jett") + } + + fn run(&self, command: &str, flags: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_jett")) + .arg(command) + .arg(self.source()) + .args(flags) + .output() + .unwrap() + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.0).unwrap(); + } +} + +fn report(output: &Output) -> String { + String::from_utf8(output.stdout.clone()).unwrap() +} + +fn escaped_path(path: &Path) -> String { + path.display() + .to_string() + .replace('\\', "\\\\") + .replace(',', "\\,") +} + +const FRONTEND_SOURCE: &str = r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("constant:") +function main(stdout: Stdout) returns nothing: + int64 baked = comptime observe("expression:") + Stdout.write(view stdout, string.from_int64(cached + baked)) +verify checking: + print("verify:") + assert true +"#; + +#[test] +fn run_agent_and_human_output_keep_application_and_exact_debug_channels_separate() { + let fixture = Fixture::new( + r#"namespace app +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "application\n") + print() + print("head:") + int64 total = 7 + trace total + println("trace fake\nstatus: error\r\n", total) +"#, + ); + let agent = fixture.run("run", &["--agent"]); + assert!(agent.status.success(), "{agent:?}"); + assert!(agent.stderr.is_empty(), "{agent:?}"); + let text = report(&agent); + assert!(text.starts_with("status: ok\n"), "{text}"); + assert!(text.contains("stdout: application\\n\n"), "{text}"); + assert!( + text.contains(concat!( + "debug[4]{phase,kind,text}:\n", + " runtime,print,\n", + " runtime,print,head:\n", + " runtime,trace,trace total: int64 = 7\\n\n", + " runtime,println,trace fake\\nstatus: error\\r\\n 7\\n\n", + )), + "{text}" + ); + assert_eq!( + text.lines() + .filter(|line| line.starts_with("status:")) + .count(), + 1 + ); + let human = fixture.run("run", &[]); + assert!(human.status.success(), "{human:?}"); + assert_eq!(human.stdout, b"application\n"); + assert_eq!( + human.stderr, + b"head:trace total: int64 = 7\ntrace fake\nstatus: error\r\n 7\n" + ); +} + +#[test] +fn run_agent_failure_retains_successful_observations_and_no_error_text_is_reclassified() { + let fixture = Fixture::new( + r#"namespace app +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + print("partial:") + string rejected = string.repeat("ab", 9223372036854775807) +"#, + ); + let agent = fixture.run("run", &["--agent"]); + assert!(!agent.status.success(), "{agent:?}"); + assert!(agent.stderr.is_empty(), "{agent:?}"); + let text = report(&agent); + assert!(text.starts_with("status: error\n"), "{text}"); + assert!( + text.contains("string.repeat: requested output is too large"), + "{text}" + ); + assert!(text.contains("stdout: before\\n\n"), "{text}"); + assert!( + text.contains("debug[1]{phase,kind,text}:\n runtime,print,partial:\n"), + "{text}" + ); + let human = fixture.run("run", &[]); + assert!(!human.status.success(), "{human:?}"); + assert_eq!(human.stdout, b"before\n"); + let stderr = String::from_utf8(human.stderr).unwrap(); + assert!( + stderr.starts_with("partial:error: runtime error:"), + "{stderr}" + ); + assert!( + stderr.contains("string.repeat: requested output is too large"), + "{stderr}" + ); +} + +#[test] +fn build_check_and_profile_setup_keep_actual_comptime_and_verify_events_once() { + let fixture = Fixture::new(FRONTEND_SOURCE); + let expected = concat!( + "debug[3]{phase,kind,text}:\n", + " comptime,print,constant:\n", + " comptime,print,expression:\n", + " frontend_verify,print,verify:\n", + ); + let check = fixture.run("build", &["--check", "--agent"]); + assert!(check.status.success(), "{check:?}"); + assert!(check.stderr.is_empty(), "{check:?}"); + let text = report(&check); + assert!(text.contains(expected), "{text}"); + assert_eq!(text.matches("comptime,print,constant:").count(), 1); + let human = fixture.run("build", &["--check"]); + assert!(human.status.success(), "{human:?}"); + assert_eq!(human.stderr, b"constant:expression:verify:"); + assert!(report(&human).starts_with("check ok: ")); + let profile = fixture.run("run", &["--profile", "--agent"]); + assert!(!profile.status.success(), "{profile:?}"); + assert!(profile.stderr.is_empty(), "{profile:?}"); + let text = report(&profile); + assert!( + text.contains("error: profiler: backend unsupported\n"), + "{text}" + ); + assert!(text.contains(expected), "{text}"); + assert!( + !text.contains("stdout: 14"), + "main must not execute: {text}" + ); + let human_profile = fixture.run("run", &["--profile"]); + assert!(!human_profile.status.success(), "{human_profile:?}"); + assert!(human_profile.stdout.is_empty(), "{human_profile:?}"); + assert_eq!( + human_profile.stderr, + b"constant:expression:verify:profiler: backend unsupported\n" + ); +} + +#[test] +fn test_agent_associates_comptime_and_block_events_while_human_summary_stays_on_stdout() { + let fixture = Fixture::new(FRONTEND_SOURCE); + let agent = fixture.run("test", &["--agent"]); + assert!(agent.status.success(), "{agent:?}"); + assert!(agent.stderr.is_empty(), "{agent:?}"); + let text = report(&agent); + let file = escaped_path(&fixture.source()); + assert!( + text.contains("debug[3]{file,block,phase,kind,text}:\n"), + "{text}" + ); + for row in [ + format!(" {file},,comptime,print,constant:\n"), + format!(" {file},,comptime,print,expression:\n"), + format!(" {file},checking,frontend_verify,print,verify:\n"), + ] { + assert!(text.contains(&row), "{text}"); + } + let human = fixture.run("test", &[]); + assert!(human.status.success(), "{human:?}"); + assert_eq!(human.stderr, b"constant:expression:verify:"); + assert!(report(&human).contains("verify checking: ok")); +} + +#[test] +fn failed_comptime_observations_survive_build_and_test_error_envelopes() { + let fixture = Fixture::new( + r#"namespace app +function reject(label: string) returns int64: + print(view label) + string rejected = string.repeat("ab", 9223372036854775807) + return string.char_count(rejected) +function main() returns nothing: + int64 value = comptime reject("failed:") + return nothing +"#, + ); + for (command, flags) in [ + ("build", vec!["--check", "--agent"]), + ("test", vec!["--agent"]), + ] { + let output = fixture.run(command, &flags); + assert!(!output.status.success(), "{command}: {output:?}"); + assert!(output.stderr.is_empty(), "{command}: {output:?}"); + let text = report(&output); + assert!(text.starts_with("status: error\n"), "{text}"); + assert!( + text.contains("string.repeat: requested output is too large"), + "{text}" + ); + assert_eq!(text.matches("comptime,print,failed:").count(), 1, "{text}"); + } +} + +#[test] +fn native_setup_failure_keeps_the_object_builds_captured_frontend_events() { + let fixture = Fixture::new(FRONTEND_SOURCE); + let missing = fixture.0.join("missing-launcher.json"); + let output = Command::new(env!("CARGO_BIN_EXE_jett")) + .arg("build") + .arg(fixture.source()) + .arg("--agent") + .arg("--runtime-bundle") + .arg(missing) + .output() + .unwrap(); + assert!(!output.status.success(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); + let text = report(&output); + assert!(text.starts_with("status: error\n"), "{text}"); + assert!(text.contains("missing-launcher.json"), "{text}"); + assert!( + text.contains(concat!( + "debug[3]{phase,kind,text}:\n", + " comptime,print,constant:\n", + " comptime,print,expression:\n", + " frontend_verify,print,verify:\n", + )), + "{text}" + ); +} + +#[test] +fn release_rejection_preserves_existing_publication_and_emits_no_debug_events() { + let fixture = Fixture::new( + r#"namespace app +function main() returns nothing: + println("must not execute") +"#, + ); + let destination = fixture.0.join("program"); + std::fs::write(&destination, b"existing publication").unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_jett")) + .arg("build") + .arg(fixture.source()) + .arg("--release") + .arg("--agent") + .arg("--output") + .arg(&destination) + .output() + .unwrap(); + assert!(!output.status.success(), "{output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); + let text = report(&output); + assert!(text.contains("E0362"), "{text}"); + assert!(text.contains("debug[0]{phase,kind,text}:\n"), "{text}"); + assert_eq!(std::fs::read(destination).unwrap(), b"existing publication"); +} + +const BUNDLE_SOURCE: &str = r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("bundle,\r\n:") +verify checking: + println("trace public", cached) + assert cached == 7 +"#; + +fn bundle(fixture: &Fixture, destination: &Path, agent: bool) -> Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_jett")); + command + .arg("bundle") + .arg(&fixture.0) + .arg("--output") + .arg(destination); + if agent { + command.arg("--agent"); + } + command.output().unwrap() +} + +#[test] +fn bundle_success_renders_validation_observations_without_protocol_or_stdout_leakage() { + let fixture = Fixture::new(BUNDLE_SOURCE); + std::fs::write( + fixture.0.join("jett.proj"), + "name: bundle_debug\nentry: app.jett\n", + ) + .unwrap(); + let destination = fixture.0.join("target").join("library.jett"); + let agent = bundle(&fixture, &destination, true); + assert!(agent.status.success(), "{agent:?}"); + assert!(agent.stderr.is_empty(), "{agent:?}"); + let text = report(&agent); + assert!(text.starts_with("status: ok\n"), "{text}"); + assert!( + text.contains(concat!( + "debug[2]{phase,kind,text}:\n", + " comptime,print,bundle\\,\\r\\n:\n", + " frontend_verify,println,trace public 7\\n\n", + )), + "{text}" + ); + assert_eq!( + text.lines() + .filter(|line| line.starts_with("status:")) + .count(), + 1 + ); + assert!(destination.is_file()); + // The second actual bundle excludes its output in the skipped target directory. + let human = bundle(&fixture, &destination, false); + assert!(human.status.success(), "{human:?}"); + assert_eq!(human.stderr, b"bundle,\r\n:trace public 7\n"); + assert!(report(&human).starts_with("bundled 1 files into ")); +} + +#[test] +fn bundle_publication_failure_retains_validation_capture_and_operational_error_identity() { + let fixture = Fixture::new(BUNDLE_SOURCE); + std::fs::write( + fixture.0.join("jett.proj"), + "name: bundle_debug\nentry: app.jett\n", + ) + .unwrap(); + let destination = fixture.0.join("target").join("blocked.jett"); + std::fs::create_dir_all(&destination).unwrap(); + let agent = bundle(&fixture, &destination, true); + assert!(!agent.status.success(), "{agent:?}"); + assert!(agent.stderr.is_empty(), "{agent:?}"); + let text = report(&agent); + assert!(text.starts_with("status: error\n"), "{text}"); + assert!(text.contains("error: failed to write "), "{text}"); + assert!(!text.contains("kind: validation"), "{text}"); + assert!(!text.contains("diagnostics["), "{text}"); + assert!( + text.contains(concat!( + "debug[2]{phase,kind,text}:\n", + " comptime,print,bundle\\,\\r\\n:\n", + " frontend_verify,println,trace public 7\\n\n", + )), + "{text}" + ); + assert!(destination.is_dir()); + assert_eq!(std::fs::read_dir(&destination).unwrap().count(), 0); + let human = bundle(&fixture, &destination, false); + assert!(!human.status.success(), "{human:?}"); + assert!(human.stdout.is_empty(), "{human:?}"); + let stderr = String::from_utf8(human.stderr).unwrap(); + assert!( + stderr.starts_with("bundle,\r\n:trace public 7\nbundle error: failed to write "), + "{stderr}" + ); + assert!(destination.is_dir()); +} diff --git a/crates/jett_codegen_cranelift/Cargo.toml b/crates/jett_codegen_cranelift/Cargo.toml new file mode 100644 index 00000000..494a264b --- /dev/null +++ b/crates/jett_codegen_cranelift/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "jett_codegen_cranelift" +description = "Cranelift ahead-of-time object generation for Jett MIR" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +cranelift-codegen.workspace = true +cranelift-frontend.workspace = true +cranelift-module.workspace = true +cranelift-object.workspace = true +jett_common.workspace = true +jett_comptime.workspace = true +jett_hir.workspace = true +jett_mir.workspace = true +jett_types.workspace = true +jett_runtime.workspace = true +sha2.workspace = true +target-lexicon.workspace = true + +[dev-dependencies] +jett_diagnostics.workspace = true +jett_parser.workspace = true +jett_resolve.workspace = true +jett_typecheck.workspace = true +object = { version = "=0.39.1", default-features = false, features = ["read", "std"] } diff --git a/crates/jett_codegen_cranelift/src/emit.rs b/crates/jett_codegen_cranelift/src/emit.rs new file mode 100644 index 00000000..9c1385ab --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit.rs @@ -0,0 +1,6623 @@ +mod actor_graph; +pub(crate) mod debug; +mod field_updates; +mod graph_constructors; +mod graphics; +pub(super) mod ordinary_borrowed_sums; +mod resource_execution; +mod resource_layout; +mod values; +use jett_mir::move_values::{ + MoveValuePlan, is_copy_owned, is_function, is_linear, is_string, representation_type, +}; +use jett_runtime::native_abi::values::{ + DEBUG_BYTES_KIND, DEBUG_NOTHING_KIND, NATIVE_FUNCTION_CODE_FIELD, + NATIVE_FUNCTION_ENVIRONMENT_FIELD, NATIVE_FUNCTION_FIELD_COUNT, NATIVE_FUNCTION_LABEL_FIELD, + NativeLeaf, NativePendingBinaryOp, NativeSequenceKind, NativeSortKind, +}; +use std::str::FromStr; + +use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; +use cranelift_codegen::ir::immediates::{Ieee32, Ieee64}; +use cranelift_codegen::ir::{self, AbiParam, InstBuilder, TrapCode, Value}; +use cranelift_codegen::{Context, isa, settings}; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; +use cranelift_module::{FuncId, Linkage, Module, default_libcall_names}; +use cranelift_object::{ObjectBuilder, ObjectModule}; +use jett_common::Span; +use jett_hir::{BinaryOp, Expression, ExpressionKind, FunctionId, UnaryOp}; +use jett_mir::{ + ControlFlowGraph, Function, Program, Statement, StatementKind, Terminator, TerminatorKind, +}; +use jett_types::{ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use target_lexicon::{HOST, Triple}; + +use crate::CodegenError; +use crate::verify::{ + ScalarKind, VerifiedProgram, known_unit_enum_variant, native_storage_kind, scalar_kind, + verify_program, +}; + +/// One emitted target object and its deterministic defined symbol table. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObjectArtifact { + pub target: String, + pub symbols: Vec, + pub bytes: Vec, +} + +/// Backend policy independent of source-language checking. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CodegenOptions { + pub optimize: bool, +} + +/// Version 1 exported C symbol used by a launcher to enter one AOT Jett object. +pub const JETT_AOT_ENTRY_SYMBOL_V1: &str = "jett_aot_v1_entry"; + +/// Success status returned by [`JETT_AOT_ENTRY_SYMBOL_V1`]. +pub const JETT_AOT_ENTRY_SUCCESS_V1: u32 = 0; + +struct DeclaredFunction { + mir_id: FunctionId, + native_id: FuncId, + symbol: String, + signature: ir::Signature, + modes: Vec, + parameter_types: Vec, + debug_label: String, +} + +/// Sparse original-MIR-index mapping. Only backend-reachable functions have a +/// slot, and every lookup checks the preserved `FunctionId` before returning a +/// Cranelift identity. +struct DeclaredFunctions { + ordered: Vec, + by_mir_index: Vec>, +} + +impl DeclaredFunctions { + fn new(function_count: usize, reachable_count: usize) -> Self { + Self { + ordered: Vec::with_capacity(reachable_count), + by_mir_index: vec![None; function_count], + } + } + + fn insert(&mut self, function: DeclaredFunction) -> Result<(), CodegenError> { + let index = usize::try_from(function.mir_id.index()).map_err(|_| { + CodegenError::Backend("MIR function index does not fit this host".to_string()) + })?; + let Some(slot) = self.by_mir_index.get_mut(index) else { + return Err(CodegenError::Backend( + "reachable MIR function index is absent from the sparse declaration table" + .to_string(), + )); + }; + if slot.is_some() { + return Err(CodegenError::Backend( + "reachable MIR function was declared more than once".to_string(), + )); + } + *slot = Some(self.ordered.len()); + self.ordered.push(function); + Ok(()) + } + + fn get(&self, id: FunctionId) -> Option<&DeclaredFunction> { + let index = usize::try_from(id.index()).ok()?; + let declared_index = self.by_mir_index.get(index).copied().flatten()?; + self.ordered + .get(declared_index) + .filter(|function| function.mir_id == id) + } + + fn iter(&self) -> impl Iterator { + self.ordered.iter() + } + + fn symbols(&self) -> Vec { + self.ordered + .iter() + .map(|function| function.symbol.clone()) + .collect() + } +} + +/// The one target accepted by this host-only backend slice. +pub fn host_target() -> Triple { + HOST +} + +/// Emit an object for the compiler host target. +pub fn emit_host_object( + program: &Program, + types: &TypeInterner, +) -> Result { + emit_for_triple(program, types, HOST, None, CodegenOptions::default()) +} + +/// Emit a host object with one exported versioned program-entry wrapper. +/// +/// `entry` is an exact checked MIR identity; this API never selects an entry by +/// source name. The wrapper has the target C ABI `uint32_t(void *context)`, +/// forwards its opaque runtime context to a `nothing`-returning Jett function, +/// supplies explicit tokens for checked Stdout, Clock, Random, and Environment parameters, and returns the +/// terminal failure status (or [`JETT_AOT_ENTRY_SUCCESS_V1`]). +pub fn emit_host_program_object( + program: &Program, + types: &TypeInterner, + entry: FunctionId, +) -> Result { + emit_host_program_object_with_options(program, types, entry, CodegenOptions::default()) +} + +pub fn emit_host_program_object_with_options( + program: &Program, + types: &TypeInterner, + entry: FunctionId, + options: CodegenOptions, +) -> Result { + emit_for_triple(program, types, HOST, Some(entry), options) +} + +/// Emit an object only when `requested_target` exactly matches the compiler +/// host. Cross-target layouts and linker inputs are intentionally not guessed. +pub fn emit_object_for_target( + program: &Program, + types: &TypeInterner, + requested_target: &str, +) -> Result { + let requested = + Triple::from_str(requested_target).map_err(|error| CodegenError::InvalidTarget { + target: requested_target.to_string(), + message: error.to_string(), + })?; + if requested != HOST { + return Err(CodegenError::UnsupportedTarget { + requested: requested.to_string(), + supported: HOST.to_string(), + }); + } + emit_for_triple(program, types, requested, None, CodegenOptions::default()) +} + +/// Emit a host-target object with an explicit checked MIR program entry. +/// +/// Cross-target layouts remain unsupported, matching +/// [`emit_object_for_target`]. +pub fn emit_program_object_for_target( + program: &Program, + types: &TypeInterner, + requested_target: &str, + entry: FunctionId, +) -> Result { + let requested = + Triple::from_str(requested_target).map_err(|error| CodegenError::InvalidTarget { + target: requested_target.to_string(), + message: error.to_string(), + })?; + if requested != HOST { + return Err(CodegenError::UnsupportedTarget { + requested: requested.to_string(), + supported: HOST.to_string(), + }); + } + emit_for_triple( + program, + types, + requested, + Some(entry), + CodegenOptions::default(), + ) +} + +fn emit_for_triple( + program: &Program, + types: &TypeInterner, + target: Triple, + entry: Option, + options: CodegenOptions, +) -> Result { + if let Some(entry) = entry { + validate_program_entry_contract(program, types, entry)?; + } + if !program.resource_manifest.kinds().is_empty() { + return resource_execution::emit(program, types, target, entry, options); + } + jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + ordinary_borrowed_sums::validate_public_program(program, types)?; + for function in &program.functions { + jett_mir::move_values::validate_local_view_initializers(function, types).map_err( + |message| CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message, + }, + )?; + } + // Graph imports can require protected ordinary storage even when their + // manifest has no Resource kinds. This plan supplies storage evidence only; + // the ordinary path still emits no Resource layout or hidden Scope. + let original_storage = graph_storage_plan(program, types)?; + if let Some(plan) = original_storage.as_ref() { + crate::verify::verify_resource_descriptor_bodies(plan)?; + } else { + crate::verify::verify_descriptor_bodies(program, types)?; + } + let mut prepared = program.clone(); + jett_mir::prepare_native_sequences(&mut prepared, types); + jett_mir::prepare_native_uninhabited_sums(&mut prepared, types); + jett_mir::prepare_native_generated_functions(&mut prepared, types); + let program = &prepared; + let storage = graph_storage_plan(program, types)?; + let verified = match storage.as_ref() { + Some(plan) => crate::verify::verify_resource_program(plan)?, + None => verify_program(program, types)?, + }; + if let Some(entry) = entry + && verified.get(entry).is_none() + { + return Err(CodegenError::UnreachableProgramEntry { + function_id: entry.index(), + }); + } + let mut flag_builder = settings::builder(); + if options.optimize { + use cranelift_codegen::settings::Configurable; + flag_builder + .set("opt_level", "speed") + .map_err(|error| CodegenError::Backend(error.to_string()))?; + } + let flags = settings::Flags::new(flag_builder); + let isa_builder = isa::lookup(target.clone()).map_err(|error| { + CodegenError::Backend(format!( + "Cranelift does not support target `{target}`: {error}" + )) + })?; + let isa = isa_builder.finish(flags).map_err(|error| { + CodegenError::Backend(format!( + "failed to configure Cranelift target `{target}`: {error}" + )) + })?; + let builder = + ObjectBuilder::new(isa, b"jett".to_vec(), default_libcall_names()).map_err(|error| { + CodegenError::Backend(format!("failed to create object module: {error}")) + })?; + let mut module = ObjectModule::new(builder); + + let declarations = declare_reachable_functions(&mut module, program, types, &verified)?; + for declaration in declarations.iter() { + let function = program_function(program, declaration.mir_id)?; + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function_with_storage( + &mut module, + &declarations, + program, + function, + types, + &declaration.symbol, + &mut context, + None, + storage.as_ref(), + )?; + module + .define_function(declaration.native_id, &mut context) + .map_err(|error| { + CodegenError::Backend(format!( + "failed to define native function `{}`: {error}; details: {error:?}", + declaration.symbol + )) + })?; + } + + if let Some(entry) = entry { + define_program_entry_wrapper(&mut module, &declarations, entry)?; + } + + let mut symbols = declarations.symbols(); + if entry.is_some() { + symbols.push(JETT_AOT_ENTRY_SYMBOL_V1.to_string()); + } + let bytes = module + .finish() + .emit() + .map_err(|error| CodegenError::Backend(format!("failed to serialize object: {error}")))?; + Ok(ObjectArtifact { + target: target.to_string(), + symbols, + bytes, + }) +} + +fn graph_storage_plan<'p>( + program: &'p Program, + types: &'p TypeInterner, +) -> Result>, CodegenError> { + if !program + .functions + .iter() + .any(|function| jett_mir::native_graph_storage_has_records(function, types)) + { + return Ok(None); + } + jett_mir::validate_resource_ownership(program, types) + .map(Some) + .map_err(CodegenError::InvalidMir) +} + +fn declare_reachable_functions( + module: &mut ObjectModule, + program: &Program, + types: &TypeInterner, + verified: &VerifiedProgram, +) -> Result { + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for verified_function in verified.functions() { + let function = program_function(program, verified_function.mir_id)?; + let signature = signature(module, function, types)?; + let native_id = module + .declare_function(&verified_function.symbol, Linkage::Local, &signature) + .map_err(|error| { + CodegenError::Backend(format!( + "failed to declare native function `{}`: {error}", + verified_function.symbol + )) + })?; + declarations.insert(DeclaredFunction { + mir_id: verified_function.mir_id, + native_id, + symbol: verified_function.symbol.clone(), + signature, + modes: function.params.iter().map(|p| p.mode).collect(), + parameter_types: function.params.iter().map(|p| p.ty).collect(), + debug_label: debug::function_label(function).map_err(|message| { + contract_error(&verified_function.symbol, function.span, message) + })?, + })?; + } + Ok(declarations) +} + +fn program_function(program: &Program, id: FunctionId) -> Result<&Function, CodegenError> { + let index = usize::try_from(id.index()).map_err(|_| { + CodegenError::Backend("MIR function index does not fit this host".to_string()) + })?; + program + .functions + .get(index) + .filter(|function| function.id == id) + .ok_or_else(|| { + CodegenError::Backend( + "reachable function is absent from the original MIR function table".to_string(), + ) + }) +} + +fn validate_program_entry_contract( + program: &Program, + types: &TypeInterner, + entry: FunctionId, +) -> Result<(), CodegenError> { + let mut matches = program + .functions + .iter() + .filter(|function| function.id == entry); + let Some(function) = matches.next() else { + return Err(CodegenError::MissingProgramEntry { + function_id: entry.index(), + }); + }; + if matches.next().is_some() { + return Err(CodegenError::DuplicateProgramEntry { + function_id: entry.index(), + }); + } + let type_count = u32::try_from(types.len()).unwrap_or(u32::MAX); + let unsupported = function + .params + .iter() + .filter(|p| { + !matches!( + p.ty, + TypeInterner::STDOUT + | TypeInterner::STDERR + | TypeInterner::STDIN + | TypeInterner::FILESYSTEM + | TypeInterner::NETWORK + | TypeInterner::PROCESS + | TypeInterner::LOG + | TypeInterner::CLOCK + | TypeInterner::RANDOM + | TypeInterner::ENVIRONMENT + | TypeInterner::GRAPHICS + ) + }) + .map(|p| { + if p.ty.index() < type_count { + types.type_name(p.ty) + } else { + format!("", p.ty.index()) + } + }) + .collect::>(); + if !unsupported.is_empty() { + return Err(CodegenError::IncompatibleProgramEntry { + function_id: entry.index(), + message: format!( + "unsupported native entry capability parameter(s): {}", + unsupported.join(", ") + ), + }); + } + + let returns_nothing = function.return_type.index() < type_count + && matches!(types.resolve(function.return_type), Type::Nothing); + if !returns_nothing { + let return_type = if function.return_type.index() < type_count { + types.type_name(function.return_type) + } else { + format!("", function.return_type.index()) + }; + return Err(CodegenError::IncompatibleProgramEntry { + function_id: entry.index(), + message: format!("expected return type `nothing`, found `{return_type}`"), + }); + } + Ok(()) +} + +fn define_program_entry_wrapper( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + entry: FunctionId, +) -> Result<(), CodegenError> { + let entry_function = + declarations + .get(entry) + .ok_or_else(|| CodegenError::UnreachableProgramEntry { + function_id: entry.index(), + })?; + if module.get_name(JETT_AOT_ENTRY_SYMBOL_V1).is_some() { + return Err(CodegenError::DuplicateSymbol( + JETT_AOT_ENTRY_SYMBOL_V1.to_string(), + )); + } + + let signature = program_entry_signature(module); + let wrapper_id = module + .declare_function(JETT_AOT_ENTRY_SYMBOL_V1, Linkage::Export, &signature) + .map_err(|error| { + CodegenError::Backend(format!( + "failed to declare program entry `{JETT_AOT_ENTRY_SYMBOL_V1}`: {error}" + )) + })?; + + let mut context = module.make_context(); + context.func.signature = signature; + translate_program_entry_wrapper(module, entry_function, entry, &mut context)?; + + module + .define_function(wrapper_id, &mut context) + .map_err(|error| { + CodegenError::Backend(format!( + "failed to define program entry `{JETT_AOT_ENTRY_SYMBOL_V1}`: {error}; details: {error:?}" + )) + })?; + Ok(()) +} + +fn program_entry_signature(module: &ObjectModule) -> ir::Signature { + let mut signature = module.make_signature(); + signature.params.push(runtime_context_abi_param(module)); + signature.returns.push(AbiParam::new(ir::types::I32)); + signature +} + +fn runtime_context_abi_param(module: &ObjectModule) -> AbiParam { + AbiParam::new(module.target_config().pointer_type()) +} + +fn translate_program_entry_wrapper( + module: &mut ObjectModule, + entry_function: &DeclaredFunction, + entry: FunctionId, + context: &mut Context, +) -> Result<(), CodegenError> { + let mut builder_context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_context); + let block = builder.create_block(); + builder.append_block_params_for_function_params(block); + builder.switch_to_block(block); + let runtime_context = builder + .block_params(block) + .first() + .copied() + .ok_or_else(|| CodegenError::IncompatibleProgramEntry { + function_id: entry.index(), + message: "exported entry wrapper is missing its runtime-context pointer".to_string(), + })?; + let entry_reference = module.declare_func_in_func(entry_function.native_id, builder.func); + let mut args = vec![runtime_context, builder.ins().iconst(ir::types::I64, 0)]; + for parameter in &entry_function.parameter_types { + let grant = match *parameter { + TypeInterner::STDOUT => NativeLeaf::GrantStdout, + TypeInterner::STDERR => NativeLeaf::GrantStderr, + TypeInterner::STDIN => NativeLeaf::GrantStdin, + TypeInterner::FILESYSTEM => NativeLeaf::GrantFilesystem, + TypeInterner::NETWORK => NativeLeaf::GrantNetwork, + TypeInterner::PROCESS => NativeLeaf::GrantProcess, + TypeInterner::LOG => NativeLeaf::GrantLog, + TypeInterner::CLOCK => NativeLeaf::GrantClock, + TypeInterner::RANDOM => NativeLeaf::GrantRandom, + TypeInterner::ENVIRONMENT => NativeLeaf::GrantEnvironment, + TypeInterner::GRAPHICS => NativeLeaf::GrantGraphics, + _ => { + return Err(CodegenError::IncompatibleProgramEntry { + function_id: entry.index(), + message: "unsupported native entry capability".to_string(), + }); + } + }; + let leaf = crate::values::declare_leaf(module, grant)?; + let leaf = module.declare_func_in_func(leaf, builder.func); + let call = builder.ins().call(leaf, &[runtime_context]); + let authority = builder.func.dfg.inst_results(call)[0]; + args.push(authority); + } + builder.ins().call(entry_reference, &args); + let leaf = crate::values::declare_leaf(module, NativeLeaf::Status)?; + let leaf = module.declare_func_in_func(leaf, builder.func); + let call = builder.ins().call(leaf, &[runtime_context]); + let status = builder.func.dfg.inst_results(call)[0]; + builder.ins().return_(&[status]); + builder.seal_all_blocks(); + builder.finalize(); + Ok(()) +} + +fn signature( + module: &ObjectModule, + function: &Function, + types: &TypeInterner, +) -> Result { + let mut signature = module.make_signature(); + signature.params.push(runtime_context_abi_param(module)); + signature.params.push(AbiParam::new(ir::types::I64)); + if crate::verify::descriptor_only_function(function) { + // This non-null descriptor entry cannot be invoked by checked code. + // Its internal trap ABI contains no Never parameter or return carrier. + return Ok(signature); + } + for parameter in function.params.iter().skip(function.capture_count) { + if let Some(ty) = clif_type(types, parameter.ty, "function parameter")? { + signature.params.push(AbiParam::new(ty)); + if is_task_scalar(types, parameter.ty)? { + signature.params.push(AbiParam::new(ir::types::I64)); + } + } + } + if let Some(ty) = clif_type(types, function.return_type, "function return")? { + signature.returns.push(AbiParam::new(ty)); + if is_task_scalar(types, function.return_type)? { + signature.returns.push(AbiParam::new(ir::types::I64)); + } + } + Ok(signature) +} + +fn is_task_scalar(types: &TypeInterner, ty: TypeId) -> Result { + Ok(matches!( + native_storage_kind(types, ty, "primitive task value")?, + ScalarKind::SignedInteger(_) + | ScalarKind::UnsignedInteger(_) + | ScalarKind::Float(_) + | ScalarKind::Bool + )) +} + +fn task_scalar_debug_kind( + types: &TypeInterner, + ty: TypeId, +) -> Result, CodegenError> { + Ok(Some( + match native_storage_kind(types, ty, "primitive task value")? { + ScalarKind::SignedInteger(8) => NativeSortKind::Int8, + ScalarKind::SignedInteger(16) => NativeSortKind::Int16, + ScalarKind::SignedInteger(32) => NativeSortKind::Int32, + ScalarKind::SignedInteger(64) => NativeSortKind::Int64, + ScalarKind::UnsignedInteger(8) => NativeSortKind::Uint8, + ScalarKind::UnsignedInteger(16) => NativeSortKind::Uint16, + ScalarKind::UnsignedInteger(32) => NativeSortKind::Uint32, + ScalarKind::UnsignedInteger(64) => NativeSortKind::Uint64, + ScalarKind::Float(32) => NativeSortKind::Float32, + ScalarKind::Float(64) => NativeSortKind::Float64, + ScalarKind::Bool => NativeSortKind::Bool, + _ => return Ok(None), + }, + )) +} + +fn native_pending_binary_op(op: BinaryOp) -> NativePendingBinaryOp { + match op { + BinaryOp::Add => NativePendingBinaryOp::Add, + BinaryOp::Subtract => NativePendingBinaryOp::Sub, + BinaryOp::Multiply => NativePendingBinaryOp::Mul, + BinaryOp::Divide => NativePendingBinaryOp::Div, + BinaryOp::Modulo => NativePendingBinaryOp::Modulo, + BinaryOp::Equal => NativePendingBinaryOp::Eq, + BinaryOp::NotEqual => NativePendingBinaryOp::NotEq, + BinaryOp::Less => NativePendingBinaryOp::Lt, + BinaryOp::Greater => NativePendingBinaryOp::Gt, + BinaryOp::LessEqual => NativePendingBinaryOp::LtEq, + BinaryOp::GreaterEqual => NativePendingBinaryOp::GtEq, + BinaryOp::And => NativePendingBinaryOp::And, + BinaryOp::Or => NativePendingBinaryOp::Or, + } +} + +fn clif_type( + types: &TypeInterner, + ty: TypeId, + context: &str, +) -> Result, CodegenError> { + let ty = match native_storage_kind(types, ty, context)? { + ScalarKind::SignedInteger(8) | ScalarKind::UnsignedInteger(8) | ScalarKind::Bool => { + Some(ir::types::I8) + } + ScalarKind::SignedInteger(16) | ScalarKind::UnsignedInteger(16) => Some(ir::types::I16), + ScalarKind::SignedInteger(32) | ScalarKind::UnsignedInteger(32) => Some(ir::types::I32), + ScalarKind::SignedInteger(64) | ScalarKind::UnsignedInteger(64) => Some(ir::types::I64), + ScalarKind::Float(32) => Some(ir::types::F32), + ScalarKind::Float(64) => Some(ir::types::F64), + // Unit values carry the current sequential task's Pending nesting. + // Zero is ordinary nothing; each enclosing Pending adds one. + ScalarKind::Nothing => Some(ir::types::I64), + ScalarKind::String + | ScalarKind::Bytes + | ScalarKind::Sum + | ScalarKind::List + | ScalarKind::Set + | ScalarKind::Map + | ScalarKind::Struct + | ScalarKind::Enum + | ScalarKind::Bitfield + | ScalarKind::Machine + | ScalarKind::Construction + | ScalarKind::Function + | ScalarKind::Actor + | ScalarKind::Stdout + | ScalarKind::Clock + | ScalarKind::Random + | ScalarKind::Environment + | ScalarKind::Graphics + | ScalarKind::OpaqueCapability => Some(ir::types::I64), + ScalarKind::Uninhabited => { + return Err(CodegenError::UnsupportedType { + type_name: types.type_name(ty), + context: context.to_string(), + }); + } + ScalarKind::SignedInteger(bits) + | ScalarKind::UnsignedInteger(bits) + | ScalarKind::Float(bits) => { + return Err(CodegenError::UnsupportedType { + type_name: format!("{bits}-bit scalar"), + context: context.to_string(), + }); + } + }; + Ok(ty) +} + +fn actor_handler_state_range( + function: &Function, + types: &TypeInterner, + symbol: &str, +) -> Result, CodegenError> { + let entry = function + .actor_handler_entry() + .map_err(|message| contract_error(symbol, function.span, message))?; + let Some(entry) = entry else { + return Ok(None); + }; + let definition = types.resolve_actor(entry.target().actor); + let start = definition.capability_params.len(); + let end = start + definition.state_fields.len(); + if function.capture_count != end || entry.roles().len() != function.params.len() { + return Err(contract_error( + symbol, + function.span, + "actor original capture projection changed", + )); + } + for (index, role) in entry.roles().iter().enumerate() { + if role.local().index() as usize != index + || function.params[index].local != role.local() + || function.params[index].ty != role.fact().ty + { + return Err(contract_error( + symbol, + function.span, + "actor original role/local projection changed", + )); + } + } + Ok(Some((start, end))) +} + +fn translate_function( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + program: &Program, + function: &Function, + types: &TypeInterner, + symbol: &str, + context: &mut Context, +) -> Result<(), CodegenError> { + translate_function_inner( + module, + declarations, + program, + function, + types, + symbol, + context, + None, + ) +} + +fn translate_function_inner( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + program: &Program, + function: &Function, + types: &TypeInterner, + symbol: &str, + context: &mut Context, + resource_layout: Option<&resource_layout::EmittedResourceLayout<'_>>, +) -> Result<(), CodegenError> { + translate_function_with_storage( + module, + declarations, + program, + function, + types, + symbol, + context, + resource_layout, + resource_layout.map(|layout| layout.plan()), + ) +} + +/// A proof-only graph storage companion retains the whole immutable plan without +/// adding the hidden Scope or Resource calling convention to an ordinary body. +fn translate_function_with_storage( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + program: &Program, + function: &Function, + types: &TypeInterner, + symbol: &str, + context: &mut Context, + resource_layout: Option<&resource_layout::EmittedResourceLayout<'_>>, + storage_plan: Option<&jett_mir::ResourceOwnershipPlan<'_>>, +) -> Result<(), CodegenError> { + if crate::verify::descriptor_only_function(function) { + let mut builder_context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_context); + let entry = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + builder.ins().trap(TrapCode::unwrap_user(1)); + builder.seal_all_blocks(); + builder.finalize(); + return Ok(()); + } + let actor_state_range = actor_handler_state_range(function, types, symbol)?; + let mut builder_context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_context); + let blocks = function + .blocks + .iter() + .map(|_| builder.create_block()) + .collect::>(); + let entry = block_for(&blocks, function.entry.index(), function.span, symbol)?; + builder.append_block_params_for_function_params(entry); + + let runtime_context = builder.declare_var(module.target_config().pointer_type()); + let environment_variable = builder.declare_var(ir::types::I64); + let companion = storage_plan + .filter(|plan| { + plan.function(function.id).is_some() + || jett_mir::native_graph_storage_has_records(function, types) + }) + .map(|plan| jett_mir::ResourceCompanionPlan::analyze(plan, function.id)) + .transpose() + .map_err(|message| contract_error(symbol, function.span, message))?; + let ordinary_acquisitions = if companion.is_none() { + Some( + jett_mir::validate_caller_acquisitions(program, function, types) + .map_err(|message| contract_error(symbol, function.span, message))?, + ) + } else { + None + }; + let caller_acquisitions = companion + .as_ref() + .map(|plan| plan.caller_acquisitions()) + .or(ordinary_acquisitions.as_ref()) + .ok_or_else(|| { + contract_error(symbol, function.span, "missing exact Source acquisitions") + })?; + let ordinary_ownership = if companion.is_none() { + Some( + MoveValuePlan::analyze(program, function, types) + .map_err(|message| contract_error(symbol, function.span, message))?, + ) + } else { + None + }; + let ownership = companion + .as_ref() + .map(|plan| plan.storage()) + .or(ordinary_ownership.as_ref()) + .ok_or_else(|| { + contract_error( + symbol, + function.span, + "function has no exact ownership storage proof", + ) + })?; + let local_slots = function + .locals + .iter() + .map(|l| { + if ownership.owned_locals.contains(&(l.id.index() as usize)) { + Some(builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + ))) + } else { + None + } + }) + .collect::>(); + let actor_extra = if actor_state_range.is_some() { + function.capture_count + 2 + } else { + 0 + }; + let temporary_slots = (0..ownership.temporary_slots + actor_extra) + .map(|_| { + builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )) + }) + .collect::>(); + let graph_constructor_slots = graph_constructors::storage(function, types, &mut builder)?; + let mut generation_slots = Vec::new(); + let mut generation_ids = std::collections::BTreeSet::new(); + let mut escrow_ids = std::collections::BTreeSet::new(); + for slot in ownership.generation_storage().slots() { + let root = function.local(slot.root()).ok_or_else(|| { + contract_error( + symbol, + function.span, + "call owner generation root is absent", + ) + })?; + let root_slot = local_slots + .get(slot.root().index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + contract_error( + symbol, + function.span, + "call owner generation has no owning root storage", + ) + })?; + if !generation_ids.insert(slot.generation().index()) + || !escrow_ids.insert(slot.escrow().index()) + || root.ty != slot.ty() + || root.ty.index() as usize >= types.len() + || !matches!(types.resolve(root.ty), Type::TypeConstruction) + || root.view_source.is_some() + { + return Err(contract_error( + symbol, + function.span, + "call owner generation storage differs from its exact plan", + )); + } + generation_slots.push(NativeCallGenerationStorage { + generation: slot.generation(), + root: slot.root(), + ty: slot.ty(), + root_slot, + owner_slot: builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )), + retired_slot: builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )), + }); + } + let failure_block = builder.create_block(); + let resource_failure = resource_layout.map(|_| { + let block = builder.create_block(); + builder.append_block_param(block, ir::types::I32); + block + }); + let resource_scope = builder.declare_var(ir::types::I64); + let resource_slots = resource_layout.map(|_| { + let returned = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + let finished = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + (returned, finished) + }); + let resource_status = builder.declare_var(ir::types::I32); + let mut resource_frames = Vec::new(); + let mut resource_owners = Vec::new(); + let mut resource_loans = Vec::new(); + let mut carrier_owners = Vec::new(); + let mut carrier_builders = Vec::new(); + let mut carrier_loans = Vec::new(); + if let Some(layout) = resource_layout { + let plan = layout.plan().function(function.id).ok_or_else(|| { + resource_layout::pending("function is outside its selected Resource family") + })?; + for storage in [ + &mut resource_frames, + &mut resource_owners, + &mut resource_loans, + &mut carrier_owners, + &mut carrier_builders, + &mut carrier_loans, + ] + .into_iter() + .zip([ + (plan.frames().len(), 16), + (plan.owner_slots().len(), 8), + (plan.loans().len(), 8), + (plan.carriers().map_or(0, |plan| plan.slots().len()), 8), + (plan.carriers().map_or(0, |plan| plan.slots().len()), 8), + (plan.carriers().map_or(0, |plan| plan.loans().len()), 8), + ]) { + for _ in 0..storage.1.0 { + storage + .0 + .push(builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + storage.1.1, + 3, + ))); + } + } + } + + let mut variables = Vec::with_capacity(function.locals.len()); + let mut pending_variables = Vec::with_capacity(function.locals.len()); + for local in &function.locals { + variables.push( + resource_execution::clif_type(resource_layout, types, local.ty, "function local")? + .map(|ty| builder.declare_var(ty)), + ); + pending_variables.push( + if resource_execution::task_scalar(resource_layout, types, local.ty)? { + Some(builder.declare_var(ir::types::I64)) + } else { + None + }, + ); + } + + let control_flow = ControlFlowGraph::analyze(function).map_err(|errors| { + let details = errors + .into_iter() + .map(|error| error.message) + .collect::>() + .join("; "); + contract_error( + symbol, + function.span, + format!("cannot analyze MIR control flow: {details}"), + ) + })?; + for block_id in control_flow.reverse_postorder() { + let block_index = usize::try_from(block_id.index()).map_err(|_| { + contract_error(symbol, function.span, "block index does not fit this host") + })?; + let block = function.blocks.get(block_index).ok_or_else(|| { + contract_error(symbol, function.span, "reachable block is absent from MIR") + })?; + let native_block = block_for(&blocks, block.id.index(), function.span, symbol)?; + builder.switch_to_block(native_block); + if block.id == function.entry { + let incoming = builder.block_params(native_block).to_vec(); + let context_value = incoming.first().copied().ok_or_else(|| { + contract_error( + symbol, + function.span, + "missing hidden native runtime-context parameter", + ) + })?; + builder + .try_def_var(runtime_context, context_value) + .map_err(|error| { + contract_error( + symbol, + function.span, + format!("cannot bind native runtime context: {error}"), + ) + })?; + let zero = builder.ins().iconst(ir::types::I64, 0); + for slot in local_slots.iter().flatten().chain(&temporary_slots) { + builder.ins().stack_store(zero, *slot, 0); + } + for storage in &graph_constructor_slots { + builder.ins().stack_store(zero, storage.slot, 0); + } + for storage in &generation_slots { + builder.ins().stack_store(zero, storage.owner_slot, 0); + builder.ins().stack_store(zero, storage.retired_slot, 0); + } + if let Some((returned, finished)) = resource_slots { + builder.ins().stack_store(zero, returned, 0); + builder.ins().stack_store(zero, finished, 0); + for slot in resource_frames + .iter() + .chain(&resource_owners) + .chain(&resource_loans) + .chain(&carrier_owners) + .chain(&carrier_builders) + .chain(&carrier_loans) + { + builder.ins().stack_store(zero, *slot, 0); + } + for slot in &resource_frames { + builder.ins().stack_store(zero, *slot, 8); + } + let failed = builder.ins().iconst(ir::types::I32, 1); + builder.def_var(resource_status, failed); + } + } + + let mut translator = Translator { + builder: &mut builder, + module, + declarations, + blocks: &blocks, + variables: &variables, + pending_variables: &pending_variables, + runtime_context, + environment_variable, + actor_state_range, + types, + equality_methods: &program.equality_methods, + symbol, + function, + local_types: &function.locals, + caller_acquisitions, + taking_binding: None, + test_ownership: matches!( + function.identity.declaration.kind, + jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property + ), + local_slots: &local_slots, + temporary_slots: &temporary_slots, + generation_slots: &generation_slots, + graph_constructor_slots: &graph_constructor_slots, + next_temporary: 0, + failure_block, + resource: None, + }; + translator.resource = resource_layout.map(|layout| resource_execution::ResourceEmission { + layout, + function: function.id, + scope: resource_scope, + return_companion: resource_slots.expect("selected Resource storage").0, + scope_finished: resource_slots.expect("selected Resource storage").1, + status: resource_status, + frames: &resource_frames, + owners: &resource_owners, + loans: &resource_loans, + carrier_owners: &carrier_owners, + carrier_builders: &carrier_builders, + carrier_loans: &carrier_loans, + failure: resource_failure.expect("selected Resource failure block"), + block: block.id, + position: jett_mir::ResourcePosition::Terminator, + }); + if block.id == function.entry { + let incoming = translator.builder.block_params(native_block).to_vec(); + if let Some(resource) = translator.resource { + let scope = incoming.get(2).copied().ok_or_else(|| { + resource_layout::pending("dedicated function lost its hidden Scope") + })?; + translator.builder.def_var(resource.scope, scope); + translator.resource_validate_scope()?; + } + let environment = incoming.get(1).copied().ok_or_else(|| { + contract_error( + symbol, + function.span, + "missing hidden native environment parameter", + ) + })?; + translator + .builder + .try_def_var(environment_variable, environment) + .map_err(|error| { + contract_error( + symbol, + function.span, + format!("cannot bind native actor environment: {error}"), + ) + })?; + let mut incoming_index = if resource_layout.is_some() { + 3_usize + } else { + 2_usize + }; + if actor_state_range.is_some() { + let mut message_index = incoming_index; + for parameter in function.params.iter().skip(function.capture_count) { + if variables[parameter.local.index() as usize].is_none() { + continue; + } + let value = *incoming.get(message_index).ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "actor incoming message value absent", + ) + })?; + message_index += 1; + if pending_variables[parameter.local.index() as usize].is_some() { + message_index += 1; + } + if is_linear(types, parameter.ty) { + let slot = + local_slots[parameter.local.index() as usize].ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "actor incoming owner lacks cleanup storage", + ) + })?; + translator.builder.ins().stack_store(value, slot, 0); + } + } + } + for (index, parameter) in function.params.iter().enumerate() { + let variable = + variable_for(&variables, parameter.local.index(), parameter.span, symbol)?; + let Some(variable) = variable else { + continue; + }; + let (value, pending_depth) = if index < function.capture_count { + let field = translator + .builder + .ins() + .iconst(ir::types::I64, index as i64); + let bits = + translator.leaf(NativeLeaf::StructField, &[environment, field], true)?; + let native = + clif_type(types, parameter.ty, "closure capture")?.ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "nothing capture has no native value", + ) + })?; + let value = if native == ir::types::F64 { + translator + .builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native == ir::types::F32 { + let bits = translator.builder.ins().ireduce(ir::types::I32, bits); + translator + .builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native != ir::types::I64 { + translator.builder.ins().ireduce(native, bits) + } else { + bits + }; + let pending_depth = + if pending_variables[parameter.local.index() as usize].is_some() { + if actor_state_range.is_some() { + translator.leaf( + NativeLeaf::StructFieldPendingDepth, + &[environment, field], + true, + )? + } else { + let depth_index = function.capture_count + index; + let depth_field = translator + .builder + .ins() + .iconst(ir::types::I64, depth_index as i64); + translator.leaf( + NativeLeaf::StructField, + &[environment, depth_field], + true, + )? + } + } else { + translator.builder.ins().iconst(ir::types::I64, 0) + }; + (value, pending_depth) + } else { + let value = incoming.get(incoming_index).copied().ok_or_else(|| { + contract_error(symbol, parameter.span, "missing native function parameter") + })?; + incoming_index += 1; + let pending_depth = + if pending_variables[parameter.local.index() as usize].is_some() { + let depth = incoming.get(incoming_index).copied().ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "missing native primitive pending depth", + ) + })?; + incoming_index += 1; + depth + } else { + translator.builder.ins().iconst(ir::types::I64, 0) + }; + (value, pending_depth) + }; + translator + .builder + .try_def_var(variable, value) + .map_err(|error| { + contract_error( + symbol, + parameter.span, + format!("cannot bind native function parameter: {error}"), + ) + })?; + if let Some(pending_variable) = pending_variables[parameter.local.index() as usize] + { + translator + .builder + .try_def_var(pending_variable, pending_depth) + .map_err(|error| { + contract_error( + symbol, + parameter.span, + format!("cannot bind primitive pending depth: {error}"), + ) + })?; + } + } + if translator.resource.is_some() { + translator.resource_bind_parameters(function)?; + } + for parameter in &function.params { + if let Some(slot) = local_slots[parameter.local.index() as usize] { + let v = translator + .builder + .use_var(variables[parameter.local.index() as usize].unwrap()); + let owned = if is_linear(types, parameter.ty) { + if actor_state_range.is_some() + && function + .params + .iter() + .take(function.capture_count) + .any(|capture| capture.local == parameter.local) + { + translator.clone_linear_handle(v, parameter.ty)? + } else { + v + } + } else if is_function(types, parameter.ty) { + translator.leaf(NativeLeaf::StructClone, &[v], true)? + } else { + translator.leaf(NativeLeaf::Retain, &[v], true)? + }; + translator.builder.ins().stack_store(owned, slot, 0); + } + } + if actor_state_range.is_some() { + for parameter in function.params.iter().take(function.capture_count) { + if function + .actor_state_view(parameter.local) + .map_err(|message| contract_error(symbol, parameter.span, message))? + { + let initializer = function + .actor_handler_entry() + .map_err(|message| contract_error(symbol, parameter.span, message))? + .and_then(|entry| { + entry + .roles() + .iter() + .find(|role| role.local() == parameter.local) + }) + .and_then(|role| role.state_view()) + .map(|view| view.initializer()) + .ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "actor state alias lost its original typed initializer", + ) + })?; + let lowered = translator.expression(initializer)?; + let value = translator.scalar(lowered, parameter.span)?; + translator.builder.def_var( + variables[parameter.local.index() as usize].ok_or_else(|| { + contract_error( + symbol, + parameter.span, + "actor alias variable absent", + ) + })?, + value, + ); + } + } + } + } + translator.drop_dead_locals(&ownership.live_in[block_index])?; + for (index, statement) in block.statements.iter().enumerate() { + if let Some(resource) = &mut translator.resource { + resource.position = jett_mir::ResourcePosition::Statement(index); + } + translator.statement(block.id, index, statement)?; + translator.drop_temporaries()?; + translator.drop_dead_locals(&ownership.live_after_statement[block_index][index])?; + } + if let Some(resource) = &mut translator.resource { + resource.position = jett_mir::ResourcePosition::Terminator; + } + translator.terminator(&block.terminator)?; + } + + if let Some(resource_failure) = resource_failure { + builder.switch_to_block(resource_failure); + let status = builder.block_params(resource_failure)[0]; + builder.def_var(resource_status, status); + builder.ins().jump(failure_block, &[]); + } + builder.switch_to_block(failure_block); + let mut translator = Translator { + builder: &mut builder, + module, + declarations, + blocks: &blocks, + variables: &variables, + pending_variables: &pending_variables, + runtime_context, + environment_variable, + actor_state_range, + types, + equality_methods: &program.equality_methods, + symbol, + function, + local_types: &function.locals, + caller_acquisitions, + taking_binding: None, + test_ownership: matches!( + function.identity.declaration.kind, + jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property + ), + local_slots: &local_slots, + temporary_slots: &temporary_slots, + generation_slots: &generation_slots, + graph_constructor_slots: &graph_constructor_slots, + next_temporary: temporary_slots.len(), + failure_block, + resource: None, + }; + translator.resource = resource_layout.map(|layout| resource_execution::ResourceEmission { + layout, + function: function.id, + scope: resource_scope, + return_companion: resource_slots.expect("selected Resource storage").0, + scope_finished: resource_slots.expect("selected Resource storage").1, + status: resource_status, + frames: &resource_frames, + owners: &resource_owners, + loans: &resource_loans, + carrier_owners: &carrier_owners, + carrier_builders: &carrier_builders, + carrier_loans: &carrier_loans, + failure: resource_failure.expect("selected Resource failure block"), + block: function.entry, + position: jett_mir::ResourcePosition::Terminator, + }); + translator.drop_all()?; + translator.resource_failure_cleanup()?; + let mut results = match resource_execution::clif_type( + resource_layout, + types, + function.return_type, + "failure return", + )? { + None => vec![], + Some(ir::types::F32) => vec![translator.builder.ins().f32const(0.0)], + Some(ir::types::F64) => vec![translator.builder.ins().f64const(0.0)], + Some(t) => vec![translator.builder.ins().iconst(t, 0)], + }; + if resource_execution::task_scalar(resource_layout, types, function.return_type)? { + results.push(translator.builder.ins().iconst(ir::types::I64, 0)); + } + translator.builder.ins().return_(&results); + builder.seal_all_blocks(); + builder.finalize(); + Ok(()) +} + +#[derive(Clone, Copy)] +enum LoweredValue { + Scalar(Value), + ScalarTask(Value, Value), + Owned(Value, ir::StackSlot), +} + +/// Internal owning storage from one freshly validated Function plan. Neither +/// this handle slot nor its retired state can be addressed as a Jett Local. +#[derive(Clone, Copy)] +struct NativeCallGenerationStorage { + generation: jett_mir::CallOwnerGenerationId, + root: jett_mir::LocalId, + ty: TypeId, + root_slot: ir::StackSlot, + owner_slot: ir::StackSlot, + retired_slot: ir::StackSlot, +} + +struct Translator<'a, 'builder> { + equality_methods: &'a std::collections::HashMap, + builder: &'a mut FunctionBuilder<'builder>, + module: &'a mut ObjectModule, + declarations: &'a DeclaredFunctions, + blocks: &'a [ir::Block], + variables: &'a [Option], + pending_variables: &'a [Option], + runtime_context: Variable, + environment_variable: Variable, + actor_state_range: Option<(usize, usize)>, + types: &'a TypeInterner, + symbol: &'a str, + function: &'a Function, + local_types: &'a [jett_mir::Local], + caller_acquisitions: &'a jett_mir::CallerAcquisitions<'a>, + taking_binding: Option, + test_ownership: bool, + local_slots: &'a [Option], + temporary_slots: &'a [ir::StackSlot], + generation_slots: &'a [NativeCallGenerationStorage], + graph_constructor_slots: &'a [graph_constructors::Storage], + next_temporary: usize, + failure_block: ir::Block, + resource: Option>, +} + +impl Translator<'_, '_> { + fn statement( + &mut self, + block: jett_mir::BlockId, + index: usize, + statement: &Statement, + ) -> Result<(), CodegenError> { + if self.ordinary_borrowed_sum_statement(block, index, statement)? { + return Ok(()); + } + if self.graph_constructor_statement(statement)? { + return Ok(()); + } + if self.resource_statement(statement)? { + return Ok(()); + } + match &statement.kind { + StatementKind::ResourceCall(_) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::ResourceConstructor(_) => Err(resource_layout::pending( + "Resource node has no constructor-owned native family", + )), + StatementKind::ReflectedContainerReady { source, kind } => { + let ty = self.local_types[source.index() as usize].ty; + let expression = Expression { + kind: ExpressionKind::Local(*source), + ty, + span: statement.span, + }; + let handle = self.argument(&expression, true)?; + let handle = self.scalar(handle, statement.span)?; + // Extend only dispatch tags of the existing custom-message leaf. + let tag = match kind { + jett_mir::ReflectedContainerKind::List => 2, + jett_mir::ReflectedContainerKind::Set => 5, + jett_mir::ReflectedContainerKind::Map => 6, + jett_mir::ReflectedContainerKind::Optional + | jett_mir::ReflectedContainerKind::Result => 7, + }; + let tag = self.builder.ins().iconst(ir::types::I32, tag); + let (pointer, length) = self.static_data(kind.message().as_bytes())?; + self.leaf( + NativeLeaf::RejectPendingHandle, + &[handle, tag, pointer, length], + true, + )?; + Ok(()) + } + StatementKind::ReflectedValueReady { + source, + inspect_pending, + } => { + if !*inspect_pending { + return Ok(()); + } + let ty = self.local_types[source.index() as usize].ty; + let expression = Expression { + kind: ExpressionKind::Local(*source), + ty, + span: statement.span, + }; + let value = self.argument(&expression, true)?; + self.reflected_value_ready(value, ty, statement.span) + } + StatementKind::IterationBorrow { .. } => Ok(()), + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + let ty = match source { + jett_mir::SequenceSource::Local(local) => { + self.local_types[local.index() as usize].ty + } + jett_mir::SequenceSource::Projected { ty, .. } => *ty, + }; + let set = matches!(self.types.resolve(ty), Type::Set(_)); + let map = matches!(self.types.resolve(ty), Type::Map(..)); + let string = matches!(self.types.resolve(ty), Type::String); + let value = match source { + jett_mir::SequenceSource::Local(local) if string => { + let slot = self.local_slots[local.index() as usize].ok_or_else(|| { + self.unsupported(statement.span, "string iteration source") + })?; + self.builder.ins().stack_load(ir::types::I64, slot, 0) + } + jett_mir::SequenceSource::Local(local) => { + let source_expr = Expression { + kind: ExpressionKind::Local(*local), + ty, + span: statement.span, + }; + let value = self.argument(&source_expr, true)?; + self.scalar(value, statement.span)? + } + jett_mir::SequenceSource::Projected { owner, path, .. } => { + let owner_ty = self.local_types[owner.index() as usize].ty; + let owner_expr = Expression { + kind: ExpressionKind::Local(*owner), + ty: owner_ty, + span: statement.span, + }; + let borrowed = self.argument(&owner_expr, true)?; + let mut value = self.scalar(borrowed, statement.span)?; + for step in path { + self.check_struct_pending_access( + value, + step.owner_type, + "field access is not supported on ", + true, + )?; + let index = self + .builder + .ins() + .iconst(ir::types::I64, i64::from(step.field.index())); + value = self.leaf(NativeLeaf::StructField, &[value, index], true)?; + } + value + } + }; + let output = if let StatementKind::SequenceGet { + index, + consume, + part, + .. + } = statement.kind + { + let index = self + .builder + .use_var(self.variables[index.index() as usize].unwrap()); + let leaf = if string { + NativeLeaf::StringScalarAt + } else if map { + match (part, consume) { + (jett_mir::SequencePart::Key, true) => NativeLeaf::MapKeyTake, + (jett_mir::SequencePart::Key, false) => NativeLeaf::MapKeyClone, + (jett_mir::SequencePart::Value, true) => NativeLeaf::MapValueTake, + (jett_mir::SequencePart::Value, false) => NativeLeaf::MapValueClone, + _ => { + return Err( + self.unsupported(statement.span, "map iteration projection") + ); + } + } + } else if set { + if consume { + NativeLeaf::SetElementTake + } else { + NativeLeaf::SetElementClone + } + } else if consume { + NativeLeaf::ListElementTake + } else { + NativeLeaf::ListElementClone + }; + let target_ty = self.local_types[target.index() as usize].ty; + let depth = if !string && is_task_scalar(self.types, target_ty)? { + if map { + let key = self.builder.ins().iconst( + ir::types::I32, + i64::from(matches!(part, jett_mir::SequencePart::Key)), + ); + Some(self.leaf( + NativeLeaf::MapElementPendingDepth, + &[value, index, key], + true, + )?) + } else { + let depth_leaf = if set { + NativeLeaf::SetElementPendingDepth + } else { + NativeLeaf::ListElementPendingDepth + }; + Some(self.leaf(depth_leaf, &[value, index], true)?) + } + } else { + None + }; + let bits = self.leaf(leaf, &[value, index], true)?; + let output = self.unpack_payload(bits, target_ty, statement.span)?; + match depth { + Some(depth) => { + LoweredValue::ScalarTask(self.scalar(output, statement.span)?, depth) + } + None => output, + } + } else { + let kind = if string { + NativeSequenceKind::String + } else if map { + NativeSequenceKind::Map + } else if set { + NativeSequenceKind::Set + } else { + NativeSequenceKind::List + }; + let kind = self.builder.ins().iconst(ir::types::I32, kind as i64); + self.leaf(NativeLeaf::SequenceCheckIterable, &[value, kind], true)?; + let leaf = if string { + NativeLeaf::StringScalarCount + } else if map { + NativeLeaf::MapLength + } else if set { + NativeLeaf::SetLength + } else { + NativeLeaf::ListLength + }; + LoweredValue::Scalar(self.leaf(leaf, &[value], true)?) + }; + self.define_local(*target, output, statement.span) + } + + StatementKind::SumTag { source, target } => { + let slot = self.local_slots[source.index() as usize] + .ok_or_else(|| self.unsupported(statement.span, "sum view tag place"))?; + let v = self.builder.ins().stack_load(ir::types::I64, slot, 0); + let ty = self.local_types[source.index() as usize].ty; + let layout = debug::debug_layout(self.types, ty) + .ok_or_else(|| self.unsupported(statement.span, "sum handle debug layout"))?; + let (pointer, length) = self.static_data(&layout)?; + let tag = self.leaf(NativeLeaf::SumHandleTag, &[v, pointer, length], true)?; + let flag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(*target, LoweredValue::Scalar(flag), statement.span) + } + StatementKind::SumTake { + source, + target, + success, + } => { + let slot = self.local_slots[source.index() as usize] + .ok_or_else(|| self.unsupported(statement.span, "take borrowed sum"))?; + let v = self.builder.ins().stack_load(ir::types::I64, slot, 0); + let tag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(*success)); + let depth = self.leaf(NativeLeaf::SumPayloadPendingDepth, &[v], true)?; + let bits = self.leaf(NativeLeaf::SumTake, &[v, tag], true)?; + self.clear_slot(slot); + let ty = self.local_types[target.index() as usize].ty; + let value = self.unpack_payload(bits, ty, statement.span)?; + let value = if is_task_scalar(self.types, ty)? { + LoweredValue::ScalarTask(self.scalar(value, statement.span)?, depth) + } else { + value + }; + self.define_local(*target, value, statement.span) + } + + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + let borrowed = self.local_types[local.index() as usize] + .view_source + .is_some(); + let value = if borrowed { + self.argument(value, true)? + } else { + let source = self + .caller_acquisitions + .owner_initializer(*local) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, source); + let result = self.expression(value); + self.taking_binding = previous; + result? + }; + self.define_local(*local, value, statement.span) + } + StatementKind::EndCallView { .. } => Ok(()), + StatementKind::OpenCallOwnerGeneration { generation, root } => { + let storage = + self.call_generation_storage(*generation, Some(*root), statement.span)?; + self.clear_slot(storage.owner_slot); + self.clear_slot(storage.retired_slot); + Ok(()) + } + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => self.replace_call_generation(*generation, *root, *rhs_owner, statement.span), + StatementKind::CloseCallOwnerGeneration { generation } => { + let storage = self.call_generation_storage(*generation, None, statement.span)?; + self.drop_slot(storage.owner_slot)?; + self.clear_slot(storage.retired_slot); + Ok(()) + } + StatementKind::CheckRefinement { + local, + call, + type_name, + } => { + let slot = self.local_slots[local.index() as usize].ok_or_else(|| { + self.unsupported(statement.span, "refinement error text place") + })?; + let caught = self.builder.create_block(); + let accepted = self.builder.create_block(); + let rejected = self.builder.create_block(); + let continuation = self.builder.create_block(); + let original_failure = std::mem::replace(&mut self.failure_block, caught); + let result = self.expression(call)?; + self.failure_block = original_failure; + let (passed, depth) = self.scalar_task(result, statement.span)?; + let pending = self.builder.create_block(); + let ready = self.builder.create_block(); + let is_pending = self.builder.ins().icmp_imm(IntCC::NotEqual, depth, 0); + self.builder + .ins() + .brif(is_pending, pending, &[], ready, &[]); + + self.builder.switch_to_block(ready); + self.builder + .ins() + .brif(passed, accepted, &[], rejected, &[]); + + self.builder.switch_to_block(pending); + let (pointer, length) = self.static_bytes(type_name)?; + let bits = self.builder.ins().uextend(ir::types::I64, passed); + let text = self.leaf( + NativeLeaf::RefinementPendingBoolText, + &[bits, depth, pointer, length], + true, + )?; + self.drop_slot(slot)?; + self.builder.ins().stack_store(text, slot, 0); + self.builder.ins().jump(continuation, &[]); + + self.builder.switch_to_block(accepted); + let (pointer, length) = self.static_bytes("")?; + let empty = self.leaf(NativeLeaf::Literal, &[pointer, length], true)?; + self.drop_slot(slot)?; + self.builder.ins().stack_store(empty, slot, 0); + self.builder.ins().jump(continuation, &[]); + + self.builder.switch_to_block(rejected); + let message = format!("refinement type constraint failed for '{type_name}'"); + let (pointer, length) = self.static_bytes(&message)?; + let text = self.leaf(NativeLeaf::Literal, &[pointer, length], true)?; + self.drop_slot(slot)?; + self.builder.ins().stack_store(text, slot, 0); + self.builder.ins().jump(continuation, &[]); + + self.builder.switch_to_block(caught); + let prefix = format!("error evaluating refinement constraint for '{type_name}': "); + let (pointer, length) = self.static_bytes(&prefix)?; + let text = self.leaf( + NativeLeaf::FailureTakePrefixedText, + &[pointer, length], + true, + )?; + self.drop_slot(slot)?; + self.builder.ins().stack_store(text, slot, 0); + self.builder.ins().jump(continuation, &[]); + + self.builder.switch_to_block(continuation); + Ok(()) + } + StatementKind::PrepareFieldUpdate { place, begin } => { + if !*begin { + return Ok(()); + } + self.field_update_parent(place)?; + Ok(()) + } + StatementKind::UpdateField { value, place, .. } => { + self.commit_field_update(place, value, statement.span) + } + StatementKind::Assign { target, value } => { + let ExpressionKind::Local(local) = &target.kind else { + return Err(self.unsupported(statement.span, "non-local assignment")); + }; + let value = self.expression(value)?; + self.define_local(*local, value, statement.span) + } + StatementKind::Evaluate(value) => { + self.expression(value)?; + Ok(()) + } + StatementKind::HandleDefault(_) => { + Err(self.unsupported(statement.span, "handle default")) + } + StatementKind::Assert { condition, message } => { + let value = self.expression(condition)?; + self.check_pending_bool_condition(value, 1, condition.span)?; + let flag = self.scalar(value, condition.span)?; + let passed = self.builder.create_block(); + let failed = self.builder.create_block(); + self.builder.ins().brif(flag, passed, &[], failed, &[]); + self.builder.switch_to_block(failed); + if let Some(message) = message { + let value = self.expression(message)?; + let handle = self.scalar(value, message.span)?; + self.leaf(NativeLeaf::AssertFailMessage, &[handle], true)?; + } else { + self.leaf(NativeLeaf::AssertFail, &[], true)?; + } + self.builder.ins().jump(passed, &[]); + self.builder.switch_to_block(passed); + Ok(()) + } + StatementKind::Trace(local) => self.debug_line("trace ", &[*local], statement.span), + StatementKind::Breakpoint { + condition, + bindings, + } => { + let enabled = match condition { + Some(condition) => { + let value = self.expression(condition)?; + self.check_pending_bool_condition(value, 2, condition.span)?; + self.scalar(value, statement.span)? + } + None => self.builder.ins().iconst(ir::types::I8, 1), + }; + match bindings.as_slice() { + [] => { + let enabled = self.builder.ins().uextend(ir::types::I32, enabled); + self.leaf(NativeLeaf::BreakpointEmpty, &[enabled], true)?; + } + _ => { + let hit = self.builder.create_block(); + let done = self.builder.create_block(); + self.builder.ins().brif(enabled, hit, &[], done, &[]); + self.builder.switch_to_block(hit); + self.debug_line("breakpoint hit: ", bindings, statement.span)?; + self.builder.ins().jump(done, &[]); + self.builder.switch_to_block(done); + } + } + Ok(()) + } + } + } + + fn debug_line( + &mut self, + prefix: &str, + bindings: &[jett_mir::LocalId], + span: Span, + ) -> Result<(), CodegenError> { + let (pointer, length) = self.static_bytes(prefix)?; + let text = self.leaf(NativeLeaf::Literal, &[pointer, length], true)?; + let owned = self.own(text)?; + for (index, binding) in bindings.iter().enumerate() { + let local = self + .local_types + .get(binding.index() as usize) + .ok_or_else(|| contract_error(self.symbol, span, "debug binding is absent"))?; + let kind = crate::values::list_sort_kind(self.types, local.ty) + .map(|kind| kind as u32) + .or_else(|| match self.types.resolve(local.ty) { + Type::Nothing => Some(DEBUG_NOTHING_KIND), + Type::Bytes => Some(DEBUG_BYTES_KIND), + _ => None, + }); + let type_name = local + .debug_type_name + .clone() + .unwrap_or_else(|| self.types.type_name(local.debug_ty)); + let label = format!( + "{}{}: {} = ", + if index == 0 { "" } else { ", " }, + local.name, + type_name + ); + let value = if let Some(slot) = self.local_slots[binding.index() as usize] { + self.builder.ins().stack_load(ir::types::I64, slot, 0) + } else { + let variable = variable_for(self.variables, binding.index(), span, self.symbol)? + .ok_or_else(|| { + self.unsupported(span, "debug binding has no native variable") + })?; + self.builder.try_use_var(variable).map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot read native debug binding: {error}"), + ) + })? + }; + let bits = self.payload_bits(LoweredValue::Scalar(value)).0; + let (label_pointer, label_length) = self.static_bytes(&label)?; + if let Some(kind) = kind { + let kind = self.builder.ins().iconst(ir::types::I32, i64::from(kind)); + if let Some(pending_variable) = self.pending_variables[binding.index() as usize] { + let depth = self.builder.use_var(pending_variable); + self.leaf( + NativeLeaf::DebugAppendPendingScalar, + &[text, label_pointer, label_length, bits, kind, depth], + true, + )?; + } else { + self.leaf( + NativeLeaf::DebugAppend, + &[text, label_pointer, label_length, bits, kind], + true, + )?; + } + } else { + let layout = debug::debug_layout(self.types, local.ty) + .ok_or_else(|| self.unsupported(span, "aggregate debug value"))?; + let (layout_pointer, layout_length) = self.static_data(&layout)?; + self.leaf( + NativeLeaf::DebugAppendAggregate, + &[ + text, + label_pointer, + label_length, + bits, + layout_pointer, + layout_length, + ], + true, + )?; + } + } + self.leaf(NativeLeaf::DebugEmit, &[text], true)?; + if let LoweredValue::Owned(_, slot) = owned { + self.clear_slot(slot); + } + Ok(()) + } + + fn flush_actor_state(&mut self, span: Span) -> Result<(), CodegenError> { + let Some((start, end)) = self.actor_state_range else { + return Ok(()); + }; + let actor = self.builder.use_var(self.environment_variable); + let mut displaced = Vec::new(); + for index in start..end { + let (value, owned) = if let Some(slot) = self.local_slots[index] { + (self.builder.ins().stack_load(ir::types::I64, slot, 0), true) + } else { + let variable = self.variables[index].ok_or_else(|| { + contract_error(self.symbol, span, "actor state has no native variable") + })?; + (self.builder.use_var(variable), false) + }; + let bits = if owned { + value + } else { + self.payload_bits(LoweredValue::Scalar(value)).0 + }; + let depth = if let Some(variable) = self.pending_variables[index] { + self.builder.use_var(variable) + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let field = self.builder.ins().iconst(ir::types::I64, index as i64); + let owns = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let old = self.leaf( + NativeLeaf::ActorReplaceOccupied, + &[actor, field, bits, owns, depth], + true, + )?; + // Successful swap transfers incoming ownership before ANY fallible cleanup. + if let Some(slot) = self.local_slots[index] { + self.clear_slot(slot); + } + if owned { + displaced.push(self.own(old)?); + } + } + // All persistent aliases now point at the new owner generation. Keep old + // owners alive until every dependent nonowning actor slot is published. + for value in displaced { + if let LoweredValue::Owned(_, slot) = value { + self.drop_slot(slot)?; + } + } + Ok(()) + } + + fn return_value(&mut self, value: Option<&Expression>, span: Span) -> Result<(), CodegenError> { + if self.resource.is_some() { + return self.resource_return(value, span); + } + let mut result = match value { + Some(value) => self.expression(value)?, + None => self.nothing(), + }; + if value.is_some_and(|value| is_copy_owned(self.types, value.ty)) { + let v = self.scalar(result, span)?; + let leaf = if value.is_some_and(|value| is_function(self.types, value.ty)) { + NativeLeaf::StructClone + } else { + NativeLeaf::Retain + }; + let acquired = self.leaf(leaf, &[v], true)?; + result = if self.actor_state_range.is_some() { + self.own(acquired)? + } else { + LoweredValue::Scalar(acquired) + }; + } + let actor_reply = if self.actor_state_range.is_some() { + match result { + LoweredValue::Owned(_, slot) => Some(slot), + _ => None, + } + } else { + None + }; + if actor_reply.is_none() { + if let LoweredValue::Owned(v, slot) = result { + self.clear_slot(slot); + result = LoweredValue::Scalar(v); + } + } + let primitive_return = match value { + Some(expression) => is_task_scalar(self.types, expression.ty)?, + None => false, + }; + let primitive = if primitive_return { + Some(self.scalar_task(result, span)?) + } else { + None + }; + self.flush_actor_state(span)?; + if let Some(slot) = actor_reply { + self.drop_all_except(slot)?; + self.clear_slot(slot); + } else { + self.drop_all()?; + } + if let Some((bits, depth)) = primitive { + self.builder.ins().return_(&[bits, depth]); + } else { + let value = self.scalar(result, span)?; + self.builder.ins().return_(&[value]); + } + Ok(()) + } + + fn terminator(&mut self, terminator: &Terminator) -> Result<(), CodegenError> { + self.carrier_iteration_boundary(terminator.span)?; + match &terminator.kind { + TerminatorKind::Return(value) => self.return_value(value.as_ref(), terminator.span), + TerminatorKind::Goto(target) => { + self.drop_temporaries()?; + let target = block_for(self.blocks, target.index(), terminator.span, self.symbol)?; + self.builder.ins().jump(target, &[]); + Ok(()) + } + TerminatorKind::Branch { + condition, + then_block, + else_block, + } => { + let lowered_condition = self.expression(condition)?; + self.check_pending_bool_condition(lowered_condition, 0, condition.span)?; + let condition = self.scalar(lowered_condition, condition.span)?; + self.drop_temporaries()?; + let then_block = block_for( + self.blocks, + then_block.index(), + terminator.span, + self.symbol, + )?; + let else_block = block_for( + self.blocks, + else_block.index(), + terminator.span, + self.symbol, + )?; + self.builder + .ins() + .brif(condition, then_block, &[], else_block, &[]); + Ok(()) + } + TerminatorKind::Unreachable => { + self.builder.ins().trap(TrapCode::unwrap_user(1)); + Ok(()) + } + TerminatorKind::Respond(value) => self.return_value(Some(value), terminator.span), + TerminatorKind::Switch { + scrutinee, + variants, + otherwise, + } => { + if self.carrier_switch(scrutinee, variants, *otherwise, terminator.span)? { + return Ok(()); + } + let lowered = self.expression(scrutinee)?; + let handle = self.scalar(lowered, scrutinee.span)?; + self.check_struct_pending_access( + handle, + scrutinee.ty, + "match requires an enum value, got ", + true, + )?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.leaf(NativeLeaf::StructField, &[handle, zero], true)?; + let switch_temporaries = self.next_temporary; + for (variant, target, bindings) in variants { + let match_tag = + self.builder + .ins() + .icmp_imm(IntCC::Equal, tag, i64::from(variant.index())); + let target = + block_for(self.blocks, target.index(), terminator.span, self.symbol)?; + let selected = self.builder.create_block(); + let next = self.builder.create_block(); + self.builder.ins().brif(match_tag, selected, &[], next, &[]); + self.builder.switch_to_block(selected); + for (index, binding) in bindings.iter().enumerate() { + let field = self.builder.ins().iconst(ir::types::I64, index as i64 + 1); + let ty = self.local_types[binding.index() as usize].ty; + let depth = if is_task_scalar(self.types, ty)? { + Some(self.leaf( + NativeLeaf::StructFieldPendingDepth, + &[handle, field], + true, + )?) + } else { + None + }; + let bits = self.leaf(NativeLeaf::StructTake, &[handle, field], true)?; + let value = self.unpack_payload(bits, ty, terminator.span)?; + let value = if let Some(depth) = depth { + LoweredValue::ScalarTask(self.scalar(value, terminator.span)?, depth) + } else { + value + }; + self.define_local(*binding, value, terminator.span)?; + } + self.drop_temporaries()?; + self.builder.ins().jump(target, &[]); + self.next_temporary = switch_temporaries; + self.builder.switch_to_block(next); + } + self.drop_temporaries()?; + if let Some(otherwise) = otherwise { + let target = + block_for(self.blocks, otherwise.index(), terminator.span, self.symbol)?; + self.builder.ins().jump(target, &[]); + } else { + self.builder.ins().trap(TrapCode::unwrap_user(1)); + } + Ok(()) + } + TerminatorKind::ForEach { .. } => { + Err(self.unsupported(terminator.span, "for-each loop")) + } + TerminatorKind::ReflectedTypeDispatch { + type_info, + arms, + otherwise, + } => { + let lowered = self.argument(type_info, true)?; + let type_info = self.scalar(lowered, terminator.span)?; + let dispatch_temporaries = self.next_temporary; + for arm in arms { + let (expected, length) = self.static_bytes(&arm.reflection_identity)?; + let matches = self.leaf( + NativeLeaf::TypeInfoReflectionMatches, + &[type_info, expected, length], + true, + )?; + let selected = self.builder.create_block(); + let next = self.builder.create_block(); + self.builder.ins().brif(matches, selected, &[], next, &[]); + self.builder.switch_to_block(selected); + self.drop_temporaries()?; + let target = block_for( + self.blocks, + arm.target.index(), + terminator.span, + self.symbol, + )?; + self.builder.ins().jump(target, &[]); + self.next_temporary = dispatch_temporaries; + self.builder.switch_to_block(next); + } + self.drop_temporaries()?; + let otherwise = + block_for(self.blocks, otherwise.index(), terminator.span, self.symbol)?; + self.builder.ins().jump(otherwise, &[]); + Ok(()) + } + } + } + + fn expression(&mut self, expression: &Expression) -> Result { + let source = self.caller_acquisitions.argument_binding(expression); + let previous = self.taking_binding; + if source.is_some() { + self.taking_binding = source; + } + let result = self.expression_inner(expression); + self.taking_binding = previous; + result + } + + fn expression_inner(&mut self, expression: &Expression) -> Result { + if let Some(value) = self.resource_expression(expression)? { + return Ok(value); + } + let kind = if self.resource_is_named_value(expression)? { + ScalarKind::Function + } else { + native_storage_kind(self.types, expression.ty, "native expression")? + }; + match &expression.kind { + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => { + Err(contract_error( + self.symbol, + expression.span, + "pending ResourceOwnershipPlan: native Resource custody and descriptor emission are not admitted", + )) + } + ExpressionKind::Int(value) => { + let ty = self.required_clif_type(expression.ty, expression.span)?; + let immediate = integer_immediate(*value, kind).ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "integer literal is out of range", + ) + })?; + Ok(LoweredValue::Scalar( + self.builder.ins().iconst(ty, immediate), + )) + } + ExpressionKind::Float(value) => { + let value = match kind { + ScalarKind::Float(32) => self + .builder + .ins() + .f32const(Ieee32::with_float(*value as f32)), + ScalarKind::Float(64) => { + self.builder.ins().f64const(Ieee64::with_float(*value)) + } + _ => { + return Err(contract_error( + self.symbol, + expression.span, + "float literal has a non-float type", + )); + } + }; + Ok(LoweredValue::Scalar(value)) + } + ExpressionKind::Bool(value) => Ok(LoweredValue::Scalar( + self.builder.ins().iconst(ir::types::I8, i64::from(*value)), + )), + ExpressionKind::Nothing => Ok(self.nothing()), + ExpressionKind::Local(local) => { + let variable = + variable_for(self.variables, local.index(), expression.span, self.symbol)?; + let variable = variable.ok_or_else(|| { + contract_error(self.symbol, expression.span, "local has no native variable") + })?; + if let Some(slot) = self.local_slots[local.index() as usize] { + let v = self.builder.ins().stack_load(ir::types::I64, slot, 0); + self.expect_machine_state(local.index() as usize, expression.ty, v)?; + if self.taking_binding == Some(*local) { + self.clear_slot(slot); + return if is_linear(self.types, expression.ty) { + self.own_linear(v) + } else { + self.own(v) + }; + } + if is_linear(self.types, expression.ty) { + if self.test_ownership { + return self.clone_linear( + LoweredValue::Scalar(v), + expression.ty, + expression.span, + ); + } + self.clear_slot(slot); + return self.own_linear(v); + } + let leaf = if is_function(self.types, expression.ty) { + NativeLeaf::StructClone + } else { + NativeLeaf::Retain + }; + let v = self.leaf(leaf, &[v], true)?; + return self.own(v); + } + let value = self.builder.try_use_var(variable).map_err(|error| { + contract_error( + self.symbol, + expression.span, + format!("cannot read native local: {error}"), + ) + })?; + if is_copy_owned(self.types, expression.ty) { + return self.own_copy_value( + LoweredValue::Scalar(value), + expression.ty, + expression.span, + ); + } + if let Some(pending_variable) = self.pending_variables[local.index() as usize] { + let depth = self + .builder + .try_use_var(pending_variable) + .map_err(|error| { + contract_error( + self.symbol, + expression.span, + format!("cannot read primitive pending depth: {error}"), + ) + })?; + Ok(LoweredValue::ScalarTask(value, depth)) + } else { + Ok(LoweredValue::Scalar(value)) + } + } + ExpressionKind::FunctionRef(function) => { + self.resource_validate_named_producer(expression, *function)?; + self.function_descriptor(*function, None, expression.span) + } + ExpressionKind::ClosureRef { function, captures } => { + let field_count = captures.len().checked_mul(2).ok_or_else(|| { + contract_error(self.symbol, expression.span, "too many closure captures") + })?; + let field_count = i64::try_from(field_count).map_err(|_| { + contract_error(self.symbol, expression.span, "too many closure captures") + })?; + let count = self.builder.ins().iconst(ir::types::I64, field_count); + let environment = self.leaf(NativeLeaf::StructNew, &[count], true)?; + let environment_owned = self.own(environment)?; + for (index, capture) in captures.iter().enumerate() { + let ty = self + .local_types + .get(capture.index() as usize) + .ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "closure capture is absent", + ) + })? + .ty; + let captured = self.expression(&Expression { + kind: ExpressionKind::Local(*capture), + ty, + span: expression.span, + })?; + let depth = if is_task_scalar(self.types, ty)? { + self.scalar_task(captured, expression.span)?.1 + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let (bits, owned) = self.payload_bits(captured); + let field_index = self.builder.ins().iconst(ir::types::I64, index as i64); + let owned_flag = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + self.leaf( + NativeLeaf::StructInit, + &[environment, field_index, bits, owned_flag], + true, + )?; + let depth_index = self + .builder + .ins() + .iconst(ir::types::I64, (captures.len() + index) as i64); + let false_flag = self.builder.ins().iconst(ir::types::I32, 0); + self.leaf( + NativeLeaf::StructInit, + &[environment, depth_index, depth, false_flag], + true, + )?; + if let LoweredValue::Owned(_, slot) = captured { + self.clear_slot(slot); + } + } + self.function_descriptor(*function, Some(environment_owned), expression.span) + } + ExpressionKind::Unary { op, value } => { + let lowered_value = self.expression(value)?; + if is_task_scalar(self.types, value.ty)? { + let (_, depth) = self.scalar_task(lowered_value, value.span)?; + let negate = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(matches!(op, UnaryOp::Negate))); + self.leaf(NativeLeaf::ScalarPendingUnaryCheck, &[depth, negate], true)?; + } + let value = self.scalar(lowered_value, value.span)?; + let value = match op { + UnaryOp::Not => self.builder.ins().bxor_imm(value, 1), + UnaryOp::Negate if kind.is_integer() => self.builder.ins().ineg(value), + UnaryOp::Negate => self.builder.ins().fneg(value), + }; + Ok(LoweredValue::Scalar(value)) + } + ExpressionKind::Binary { left, op, right } => { + let operand_kind = native_storage_kind(self.types, left.ty, "binary operand")?; + let enum_equality = operand_kind == ScalarKind::Enum + && matches!(op, BinaryOp::Equal | BinaryOp::NotEqual); + let lowered_left = self.argument(left, enum_equality)?; + let left_value = self.scalar(lowered_left, left.span)?; + if matches!(op, BinaryOp::And | BinaryOp::Or) { + let value = self.short_circuit_boolean( + lowered_left, + *op, + right, + operand_kind, + expression.span, + )?; + return Ok(LoweredValue::Scalar(value)); + } + let lowered_right = self.argument(right, enum_equality)?; + let right_value = self.scalar(lowered_right, right.span)?; + self.check_pending_scalar_binary( + lowered_left, + left.ty, + *op, + lowered_right, + right.ty, + expression.span, + )?; + if operand_kind == ScalarKind::Actor { + let not_equal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(*op == BinaryOp::NotEqual)); + let value = self.leaf( + NativeLeaf::ActorIdentityEqual, + &[left_value, right_value, not_equal], + true, + )?; + let value = self.builder.ins().ireduce(ir::types::I8, value); + return Ok(LoweredValue::Scalar(value)); + } + if operand_kind == ScalarKind::String { + let leaf = if *op == BinaryOp::NotEqual { + NativeLeaf::StringNotEqual + } else { + NativeLeaf::Equal + }; + let value = self.leaf(leaf, &[left_value, right_value], true)?; + let value = self.builder.ins().ireduce(ir::types::I8, value); + return Ok(LoweredValue::Scalar(value)); + } + if operand_kind == ScalarKind::Enum { + let debug_layout = debug::debug_layout(self.types, left.ty) + .ok_or_else(|| self.unsupported(expression.span, "enum debug layout"))?; + let (debug_pointer, debug_length) = self.static_data(&debug_layout)?; + let not_equal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(*op == BinaryOp::NotEqual)); + self.leaf( + NativeLeaf::EnumPendingComparisonCheck, + &[ + left_value, + right_value, + not_equal, + debug_pointer, + debug_length, + ], + true, + )?; + let Type::Enum(enum_id) = + self.types.resolve(representation_type(self.types, left.ty)) + else { + return Err(self.unsupported(expression.span, "enum equality type")); + }; + let variants = &self.types.resolve_enum(*enum_id).variants; + if variants.iter().any(|variant| !variant.fields.is_empty()) + && !known_unit_enum_variant(left) + && !known_unit_enum_variant(right) + { + let aggregate = variants.iter().flat_map(|variant| &variant.fields).any( + |(_, field_ty)| { + !matches!( + native_storage_kind( + self.types, + *field_ty, + "enum equality payload" + ), + Ok(ScalarKind::SignedInteger(_) + | ScalarKind::UnsignedInteger(_) + | ScalarKind::Float(32 | 64) + | ScalarKind::Bool + | ScalarKind::String) + ) + }, + ); + let (layout, leaf) = if aggregate { + let layout = + debug::equality_layout(self.types, left.ty, self.equality_methods) + .ok_or_else(|| { + self.unsupported( + expression.span, + "enum equality payload type", + ) + })?; + if !layout.custom.is_empty() { + let value = self.custom_enum_equality( + left_value, + right_value, + &layout, + expression.span, + )?; + let value = if *op == BinaryOp::NotEqual { + self.builder.ins().bxor_imm(value, 1) + } else { + value + }; + return Ok(LoweredValue::Scalar(value)); + } + (layout.bytes, NativeLeaf::EnumEqualAggregate) + } else { + let mut layout = b"JE\x01".to_vec(); + let count = u32::try_from(variants.len()).map_err(|_| { + self.unsupported(expression.span, "enum variant count") + })?; + layout.extend_from_slice(&count.to_le_bytes()); + for variant in variants { + let count = u32::try_from(variant.fields.len()).map_err(|_| { + self.unsupported(expression.span, "enum equality field count") + })?; + layout.extend_from_slice(&count.to_le_bytes()); + for (_, field_ty) in &variant.fields { + let kind = match native_storage_kind( + self.types, + *field_ty, + "enum equality payload", + )? { + ScalarKind::SignedInteger(_) + | ScalarKind::UnsignedInteger(_) + | ScalarKind::Bool => b'i', + ScalarKind::Float(32) => b'f', + ScalarKind::Float(64) => b'd', + ScalarKind::String => b's', + _ => { + return Err(self.unsupported( + expression.span, + "enum equality payload type", + )); + } + }; + layout.push(kind); + } + } + (layout, NativeLeaf::EnumEqual) + }; + let (pointer, length) = self.static_data(&layout)?; + let value = + self.leaf(leaf, &[left_value, right_value, pointer, length], true)?; + let value = self.builder.ins().ireduce(ir::types::I8, value); + let value = if *op == BinaryOp::NotEqual { + self.builder.ins().bxor_imm(value, 1) + } else { + value + }; + return Ok(LoweredValue::Scalar(value)); + } + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let left_tag = self.leaf(NativeLeaf::StructField, &[left_value, zero], true)?; + let right_tag = + self.leaf(NativeLeaf::StructField, &[right_value, zero], true)?; + let condition = if *op == BinaryOp::Equal { + IntCC::Equal + } else { + IntCC::NotEqual + }; + let value = self.builder.ins().icmp(condition, left_tag, right_tag); + return Ok(LoweredValue::Scalar(value)); + } + let value = + self.binary(left_value, *op, right_value, operand_kind, expression.span)?; + Ok(LoweredValue::Scalar(value)) + } + ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } => self.call(*function, args, evaluation_order, expression), + ExpressionKind::FunctionAdapter { value, function } => { + let lowered = self.expression(value)?; + let source = self.scalar(lowered, value.span)?; + let declared = self.declarations.get(*function).ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "function adapter is not reachable", + ) + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let pointer_type = self.module.target_config().pointer_type(); + let address = self.builder.ins().func_addr(pointer_type, reference); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().uextend(ir::types::I64, address) + }; + let adapted = self.leaf(NativeLeaf::FunctionAdapter, &[source, address], true)?; + self.own(adapted) + } + ExpressionKind::InterfaceCoerce { value, adapters } => { + self.interface_coerce(value, expression.ty, false, adapters) + } + ExpressionKind::InterfaceType(value) => { + let lowered = self.argument(value, true)?; + let handle = self.scalar(lowered, value.span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::InterfaceType, + &[handle], + true, + )?)) + } + ExpressionKind::PropertyCaseContext(case) => { + if let Some(case) = case { + let lowered = + self.literal(&format!("property '{}' trial {}: ", case.name, case.trial))?; + let handle = self.scalar(lowered, expression.span)?; + self.leaf(NativeLeaf::PropertyCaseSet, &[handle], true)?; + } else { + self.leaf(NativeLeaf::PropertyCaseClear, &[], true)?; + } + Ok(self.nothing()) + } + ExpressionKind::RuntimeFailureMessage(message) => { + let lowered = self.argument(message, true)?; + let handle = self.scalar(lowered, message.span)?; + self.leaf(NativeLeaf::RuntimeFailMessage, &[handle], true)?; + Ok(self.nothing()) + } + ExpressionKind::RuntimeFailure(message) => { + let lowered = self.literal(message)?; + let handle = self.scalar(lowered, expression.span)?; + self.leaf(NativeLeaf::RuntimeFailMessage, &[handle], true)?; + let native_ty = self.required_clif_type(expression.ty, expression.span)?; + let zero = if native_ty == ir::types::F32 { + self.builder.ins().f32const(0.0) + } else if native_ty == ir::types::F64 { + self.builder.ins().f64const(0.0) + } else { + self.builder.ins().iconst(native_ty, 0) + }; + Ok(LoweredValue::Scalar(zero)) + } + ExpressionKind::Comptime { .. } => { + Err(self.unsupported(expression.span, "unbaked comptime expression")) + } + ExpressionKind::Constant { .. } => { + Err(self.unsupported(expression.span, "unbaked namespace constant")) + } + ExpressionKind::View(value) => { + let value = self.argument(value, true)?; + if is_copy_owned(self.types, expression.ty) { + self.own_copy_value(value, expression.ty, expression.span) + } else { + Ok(value) + } + } + ExpressionKind::Clone(value) if is_linear(self.types, value.ty) => { + let borrowed = self.argument(value, true)?; + self.clone_linear(borrowed, value.ty, value.span) + } + ExpressionKind::Clone(value) => self.expression(value), + ExpressionKind::String(text) => self.literal(text), + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + field_validation, + args, + evaluation_order, + .. + } => self.intrinsic( + *intrinsic, + type_arguments, + reflection_arguments, + field_validation.as_ref(), + args, + evaluation_order, + expression.ty, + expression.span, + ), + ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + } => self.indirect_call(callee, args, evaluation_order, expression), + ExpressionKind::StructConstruct { + fields, + evaluation_order, + validates_refinements, + .. + } => { + let record = self.construct_struct(fields, evaluation_order, expression.span)?; + if *validates_refinements { + self.construct_sum_value(true, record, expression.span) + } else { + Ok(record) + } + } + ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + } => { + if *validates_widths { + self.construct_validated_bitfield( + *bitfield_type, + fields, + evaluation_order, + expression.span, + ) + } else { + self.construct_struct(fields, evaluation_order, expression.span) + } + } + ExpressionKind::MachineConstruct { + state, payloads, .. + } => self.construct_tagged_record( + state.index(), + payloads, + 0..payloads.len(), + expression.span, + ), + ExpressionKind::MachineTransition { + source, + state_type, + target, + payloads, + } => { + let lowered = self.expression(source)?; + let value = self.scalar(lowered, source.span)?; + let machine = match self + .types + .resolve(representation_type(self.types, *state_type)) + { + Type::Machine(machine) | Type::MachineState { machine, .. } => *machine, + _ => return Err(self.unsupported(expression.span, "machine transition type")), + }; + let message = format!( + "{}.transition: first argument must be a machine value", + self.types.resolve_machine(machine).name + ); + self.check_struct_pending_access(value, source.ty, &message, false)?; + self.construct_tagged_record( + target.index(), + payloads, + 0..payloads.len(), + expression.span, + ) + } + ExpressionKind::ListConstruct { elements } => { + self.construct_list(elements, expression.ty, expression.span) + } + ExpressionKind::MapConstruct { entries } => { + self.construct_map(entries, expression.ty, expression.span) + } + ExpressionKind::ResultOk(value) | ExpressionKind::OptionalSome(value) => { + self.construct_sum(true, Some(value), expression.span) + } + ExpressionKind::ResultFail(value) => { + self.construct_sum(false, Some(value), expression.span) + } + ExpressionKind::OptionalNone => self.construct_sum(false, None, expression.span), + ExpressionKind::Handle { .. } => { + Err(self.unsupported(expression.span, "failure handler")) + } + ExpressionKind::EnumConstruct { + variant, + payloads, + evaluation_order, + .. + } => self.construct_tagged_record( + variant.index(), + payloads, + evaluation_order.iter().copied(), + expression.span, + ), + ExpressionKind::StringInterpolation(segments) => { + self.interpolate(segments, expression.span) + } + ExpressionKind::DisplayResult(value) => { + let lowered = self.expression(value)?; + let handle = self.scalar(lowered, value.span)?; + self.leaf(NativeLeaf::DisplayResultCheck, &[handle], true)?; + Ok(lowered) + } + ExpressionKind::EquatableResult(value) => { + let lowered = self.expression(value)?; + self.reject_pending_scalars(&[lowered], "Equatable.equals must return bool")?; + Ok(lowered) + } + ExpressionKind::Declassify(value) => self.expression(value), + ExpressionKind::Coarsen(value) => self.expression(value), + ExpressionKind::RefinementValidated(value) => self.expression(value), + ExpressionKind::StateIs { value, state } => { + let borrowed = self.argument(value, true)?; + let machine = self.scalar(borrowed, value.span)?; + self.check_struct_pending_access( + machine, + value.ty, + "'at' requires a machine value, got ", + true, + )?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.leaf(NativeLeaf::StructField, &[machine, zero], true)?; + let expected = self + .builder + .ins() + .iconst(ir::types::I64, i64::from(state.index())); + Ok(LoweredValue::Scalar(self.builder.ins().icmp( + IntCC::Equal, + tag, + expected, + ))) + } + // Tasks currently execute immediately. Unit values retain Pending + // nesting because joining bare nothing reports cancellation. + ExpressionKind::Run(value) => { + let lowered = self.expression(value)?; + let representation = representation_type(self.types, value.ty); + if native_storage_kind(self.types, value.ty, "task value")? == ScalarKind::Nothing { + let depth = self.scalar(lowered, value.span)?; + let depth = self.leaf(NativeLeaf::NothingRun, &[depth], true)?; + Ok(LoweredValue::Scalar(depth)) + } else if matches!(self.types.resolve(representation), Type::String) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::StringRun, &[source], true)?; + self.own(pending) + } else if matches!(self.types.resolve(representation), Type::Bytes) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::BytesRun, &[source], true)?; + self.own_linear(pending) + } else if matches!(self.types.resolve(representation), Type::List(_)) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::ListRun, &[source], true)?; + self.own_linear(pending) + } else if matches!(self.types.resolve(representation), Type::Set(_)) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::SetRun, &[source], true)?; + self.own_linear(pending) + } else if matches!(self.types.resolve(representation), Type::Map(..)) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::MapRun, &[source], true)?; + self.own_linear(pending) + } else if matches!( + self.types.resolve(representation), + Type::Optional(_) | Type::Result(..) + ) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::SumRun, &[source], true)?; + self.own_linear(pending) + } else if matches!( + self.types.resolve(representation), + Type::Struct(_) + | Type::Enum(_) + | Type::Bitfield(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Function { .. } + | Type::Interface(_) + | Type::TypeConstruction + ) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::RecordRun, &[source], true)?; + self.own_linear(pending) + } else if matches!(self.types.resolve(representation), Type::Actor(_)) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::ActorRun, &[source], true)?; + Ok(LoweredValue::Scalar(pending)) + } else if matches!(self.types.resolve(representation), Type::Capability(_)) { + let source = self.scalar(lowered, value.span)?; + let pending = self.leaf(NativeLeaf::CapabilityRun, &[source], true)?; + Ok(LoweredValue::Scalar(pending)) + } else if is_task_scalar(self.types, value.ty)? { + let (bits, depth) = self.scalar_task(lowered, value.span)?; + let depth = self.leaf(NativeLeaf::NothingRun, &[depth], true)?; + Ok(LoweredValue::ScalarTask(bits, depth)) + } else { + Ok(lowered) + } + } + ExpressionKind::Join(value) => { + let result = self.expression(value)?; + let representation = representation_type(self.types, value.ty); + if native_storage_kind(self.types, value.ty, "task value")? == ScalarKind::Nothing { + let depth = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::NothingJoin, &[depth], true)?; + self.own_linear(joined) + } else if matches!(self.types.resolve(representation), Type::String) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::StringTaskJoin, &[source], true)?; + let joined = self.own(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::Bytes) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::BytesTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::List(_)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::ListTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::Set(_)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::SetTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::Map(..)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::MapTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::Result(..)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::SumTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + if value.ty == expression.ty { + Ok(joined) + } else { + self.construct_sum_value(true, joined, expression.span) + } + } else if matches!(self.types.resolve(representation), Type::Optional(_)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::SumTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!( + self.types.resolve(representation), + Type::Struct(_) + | Type::Enum(_) + | Type::Bitfield(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Function { .. } + | Type::Interface(_) + | Type::TypeConstruction + ) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::RecordTaskJoin, &[source], true)?; + let joined = self.own_linear(joined)?; + self.construct_sum_value(true, joined, expression.span) + } else if matches!(self.types.resolve(representation), Type::Actor(_)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::ActorTaskJoin, &[source], true)?; + self.construct_sum_value(true, LoweredValue::Scalar(joined), expression.span) + } else if matches!(self.types.resolve(representation), Type::Capability(_)) { + let source = self.scalar(result, value.span)?; + let joined = self.leaf(NativeLeaf::CapabilityTaskJoin, &[source], true)?; + self.own_linear(joined) + } else if is_task_scalar(self.types, value.ty)? { + let (bits, depth) = self.scalar_task(result, value.span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let empty = self.builder.ins().icmp(IntCC::Equal, depth, zero); + let decreased = self.builder.ins().iadd_imm(depth, -1); + let depth = self.builder.ins().select(empty, zero, decreased); + self.construct_sum_value( + true, + LoweredValue::ScalarTask(bits, depth), + expression.span, + ) + } else if value.ty == expression.ty { + Ok(result) + } else { + self.construct_sum_value(true, result, expression.span) + } + } + ExpressionKind::Cancel(value) => { + self.expression(value)?; + Ok(self.nothing()) + } + ExpressionKind::InlineFunction { .. } => { + Err(self.unsupported(expression.span, "inline function")) + } + ExpressionKind::ActorGraphImport(_) => { + Err(self.unsupported(expression.span, "unexpanded actor graph import")) + } + ExpressionKind::ActorGraphStep { instruction, args } => { + self.actor_graph_step(instruction, args, expression) + } + ExpressionKind::ActorSpawn { + args, + evaluation_order, + constructor: Some(constructor), + .. + } => self.call(*constructor, args, evaluation_order, expression), + ExpressionKind::ActorSpawn { + args, + evaluation_order, + constructor: None, + .. + } => { + let record = self.construct_struct(args, evaluation_order, expression.span)?; + let handle = self.scalar(record, expression.span)?; + let actor = self.leaf(NativeLeaf::ActorRegister, &[handle], true)?; + if let LoweredValue::Owned(_, slot) = record { + self.clear_slot(slot); + } + Ok(LoweredValue::Scalar(actor)) + } + ExpressionKind::ActorMessage { + actor, + message, + handler, + args, + evaluation_order, + kind, + proof, + } => { + let Type::Actor(actor_id) = self.types.resolve(actor.ty) else { + return Err(contract_error( + self.symbol, + expression.span, + "actor message has no actor type", + )); + }; + let definition = self.types.resolve_actor(*actor_id); + if proof.target().actor != *actor_id { + return Err(contract_error( + self.symbol, + expression.span, + "actor message changed original actor target", + )); + } + let responds = definition + .messages + .get(proof.target().handler_index) + .filter(|entry| entry.name == *message) + .ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "actor original message ordinal absent", + ) + })? + .responds; + let captured = definition.capability_params.len() + definition.state_fields.len(); + let lowered_actor = self.expression(actor)?; + let environment = self.scalar(lowered_actor, actor.span)?; + self.leaf(NativeLeaf::ActorMessageCheck, &[environment], true)?; + let result = self.call_with_environment( + *handler, + args, + evaluation_order, + expression, + environment, + captured, + responds, + Some(proof), + )?; + if *kind == jett_hir::ActorMessageKind::Send { + Ok(self.nothing()) + } else { + Ok(result) + } + } + ExpressionKind::Field { base, field, .. } => { + self.project_field(base, *field, expression.ty, expression.span, false) + } + } + } + + fn custom_enum_equality( + &mut self, + left: Value, + right: Value, + layout: &debug::EqualityLayout, + span: Span, + ) -> Result { + let mut bytes = b"JQ\x01".to_vec(); + let count = u32::try_from(layout.custom.len()) + .map_err(|_| self.unsupported(span, "equality method count"))?; + bytes.extend_from_slice(&count.to_le_bytes()); + for (node, _) in &layout.custom { + bytes.extend_from_slice(&node.to_le_bytes()); + } + bytes.extend_from_slice(&layout.bytes); + let (pointer, length) = self.static_data(&bytes)?; + let cursor = self.leaf( + NativeLeaf::EnumEqualityStart, + &[left, right, pointer, length], + true, + )?; + let owned = self.own_linear(cursor)?; + let next = self.builder.create_block(); + let dispatch = self.builder.create_block(); + let done = self.builder.create_block(); + self.builder.append_block_param(done, ir::types::I32); + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(next); + let status = self.leaf(NativeLeaf::EnumEqualityNext, &[cursor], true)?; + let complete = self + .builder + .ins() + .icmp_imm(IntCC::UnsignedLessThanOrEqual, status, 1); + self.builder + .ins() + .brif(complete, done, &[status.into()], dispatch, &[]); + self.builder.switch_to_block(dispatch); + for (ordinal, (_, method)) in layout.custom.iter().enumerate() { + let invoke = self.builder.create_block(); + let following = self.builder.create_block(); + let selected = self + .builder + .ins() + .icmp_imm(IntCC::Equal, status, ordinal as i64 + 2); + self.builder + .ins() + .brif(selected, invoke, &[], following, &[]); + self.builder.switch_to_block(invoke); + let zero = self.builder.ins().iconst(ir::types::I32, 0); + let one = self.builder.ins().iconst(ir::types::I32, 1); + let left = self.leaf(NativeLeaf::EnumEqualityArgument, &[cursor, zero], true)?; + let right = self.leaf(NativeLeaf::EnumEqualityArgument, &[cursor, one], true)?; + let declared = self.declarations.get(*method).ok_or_else(|| { + contract_error(self.symbol, span, "enum equality method is not reachable") + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let context = self.builder.use_var(self.runtime_context); + let environment = self.builder.ins().iconst(ir::types::I64, 0); + let call = self + .builder + .ins() + .call(reference, &[context, environment, left, right]); + let results = self.builder.func.dfg.inst_results(call).to_vec(); + self.check_failure()?; + let [equal, depth] = results.as_slice() else { + return Err(contract_error( + self.symbol, + span, + "enum equality method has invalid bool ABI", + )); + }; + let equal = self.builder.ins().uextend(ir::types::I32, *equal); + self.leaf( + NativeLeaf::EnumEqualityAnswer, + &[cursor, equal, *depth], + true, + )?; + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(following); + } + // An invalid cursor response must take the normal cleanup path. + let invalid_argument = self.builder.ins().iconst(ir::types::I32, 2); + self.leaf( + NativeLeaf::EnumEqualityArgument, + &[cursor, invalid_argument], + true, + )?; + let zero = self.builder.ins().iconst(ir::types::I32, 0); + self.builder.ins().jump(done, &[zero.into()]); + self.builder.switch_to_block(done); + let result = self.builder.block_params(done)[0]; + self.leaf(NativeLeaf::DropValue, &[cursor], true)?; + if let LoweredValue::Owned(_, slot) = owned { + self.clear_slot(slot); + } + Ok(self.builder.ins().ireduce(ir::types::I8, result)) + } + + fn call( + &mut self, + function: jett_mir::FunctionId, + args: &[Expression], + evaluation_order: &[usize], + expression: &Expression, + ) -> Result { + if let ExpressionKind::Call { + ownership: jett_hir::CallOwnership::Generated(generated), + .. + } = &expression.kind + && matches!( + generated.operation, + jett_hir::GeneratedOperation::InterfaceDispatch { .. } + ) + { + for argument in args { + self.preflight_interface_dispatch_argument(argument)?; + } + } + let environment = self.builder.ins().iconst(ir::types::I64, 0); + self.call_with_environment( + function, + args, + evaluation_order, + expression, + environment, + 0, + expression.ty, + None, + ) + } + + fn call_with_environment( + &mut self, + function: jett_mir::FunctionId, + args: &[Expression], + evaluation_order: &[usize], + expression: &Expression, + environment: Value, + captured: usize, + result_type: TypeId, + actor_message: Option<&jett_hir::ActorMessageProof>, + ) -> Result { + let invalid_order = || { + contract_error( + self.symbol, + expression.span, + "call evaluation order is not a permutation", + ) + }; + let all_modes = self + .declarations + .get(function) + .ok_or_else(invalid_order)? + .modes + .clone(); + let modes = all_modes.get(captured..).ok_or_else(invalid_order)?; + if modes.len() != args.len() { + return Err(invalid_order()); + } + let indexed = args.iter().enumerate().collect::>(); + let evaluated = reordered_map( + &indexed, + evaluation_order, + |(index, argument)| { + if actor_message.is_some() { + self.argument(argument, false) + } else if modes[*index] == jett_mir::ParamMode::Owned + && is_linear(self.types, argument.ty) + && matches!(argument.kind, ExpressionKind::View(_)) + { + let cloned = Expression { + kind: ExpressionKind::Clone(Box::new((*argument).clone())), + ty: argument.ty, + span: argument.span, + }; + self.expression(&cloned) + } else { + self.argument(argument, modes[*index] == jett_mir::ParamMode::View) + } + }, + invalid_order, + )?; + let runtime_context = self + .builder + .try_use_var(self.runtime_context) + .map_err(|error| { + contract_error( + self.symbol, + expression.span, + format!("cannot read native runtime context: {error}"), + ) + })?; + let mut native_args = Vec::with_capacity(evaluated.len() + 2); + native_args.push(runtime_context); + native_args.push(environment); + for (index, (argument, value)) in args.iter().zip(evaluated).enumerate() { + if is_task_scalar(self.types, argument.ty)? { + let (bits, depth) = self.scalar_task(value, argument.span)?; + native_args.push(bits); + native_args.push(depth); + continue; + } + match value { + LoweredValue::Scalar(value) => native_args.push(value), + LoweredValue::ScalarTask(value, _) => native_args.push(value), + LoweredValue::Owned(value, slot) => { + if (actor_message.is_some() || modes[index] == jett_mir::ParamMode::Owned) + && is_linear(self.types, argument.ty) + { + self.clear_slot(slot); + } + native_args.push(value); + } + } + } + let function_id = self + .declarations + .get(function) + .map(|function| function.native_id) + .ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "direct call target is absent from the reachable function table", + ) + })?; + let reference = self + .module + .declare_func_in_func(function_id, self.builder.func); + let call = self.builder.ins().call(reference, &native_args); + let results = self.builder.func.dfg.inst_results(call).to_vec(); + self.check_failure()?; + let value = results.first().copied().ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "value-returning call produced no native value", + ) + })?; + if is_task_scalar(self.types, result_type)? { + let depth = results.get(1).copied().ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "primitive call produced no pending depth", + ) + })?; + return Ok(LoweredValue::ScalarTask(value, depth)); + } + if is_linear(self.types, result_type) { + self.own_linear(value) + } else if is_copy_owned(self.types, result_type) { + self.own(value) + } else { + Ok(LoweredValue::Scalar(value)) + } + } + + fn function_descriptor( + &mut self, + function: FunctionId, + environment: Option, + span: Span, + ) -> Result { + let declared = self.declarations.get(function).ok_or_else(|| { + contract_error(self.symbol, span, "function value target is not reachable") + })?; + let label = declared.debug_label.clone(); + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let pointer_type = self.module.target_config().pointer_type(); + let address = self.builder.ins().func_addr(pointer_type, reference); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().uextend(ir::types::I64, address) + }; + let count = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_FIELD_COUNT as i64); + let descriptor = self.leaf(NativeLeaf::StructNew, &[count], true)?; + let descriptor_owned = self.own(descriptor)?; + let code_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_CODE_FIELD as i64); + let borrowed = self.builder.ins().iconst(ir::types::I32, 0); + self.leaf( + NativeLeaf::StructInit, + &[descriptor, code_index, address, borrowed], + true, + )?; + let environment_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64); + let environment_value = match environment { + Some(value) => self.scalar(value, span)?, + None => self.builder.ins().iconst(ir::types::I64, 0), + }; + let owned_flag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(environment.is_some())); + self.leaf( + NativeLeaf::StructInit, + &[descriptor, environment_index, environment_value, owned_flag], + true, + )?; + if let Some(LoweredValue::Owned(_, slot)) = environment { + self.clear_slot(slot); + } + let label = self.literal(&label)?; + let label_value = self.scalar(label, span)?; + let label_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_LABEL_FIELD as i64); + let owned_flag = self.builder.ins().iconst(ir::types::I32, 1); + self.leaf( + NativeLeaf::StructInit, + &[descriptor, label_index, label_value, owned_flag], + true, + )?; + if let LoweredValue::Owned(_, slot) = label { + self.clear_slot(slot); + } + Ok(descriptor_owned) + } + + fn indirect_call( + &mut self, + callee: &Expression, + args: &[Expression], + evaluation_order: &[usize], + expression: &Expression, + ) -> Result { + let Type::Function { + params, + view_params, + return_type, + } = self.types.resolve(callee.ty) + else { + return Err(contract_error( + self.symbol, + expression.span, + "indirect call has no function type", + )); + }; + let params = params.clone(); + let view_params = view_params.clone(); + let return_type = *return_type; + if params.len() != args.len() || view_params.len() != args.len() { + return Err(contract_error( + self.symbol, + expression.span, + "indirect call arguments disagree with checked parameter modes", + )); + } + let invalid_order = || { + contract_error( + self.symbol, + expression.span, + "indirect call evaluation order is not a permutation", + ) + }; + let indexed = args.iter().enumerate().collect::>(); + let evaluated = reordered_map( + &indexed, + evaluation_order, + |(index, argument)| self.argument(argument, view_params[*index]), + invalid_order, + )?; + // The interpreter resolves a function-valued local after evaluating + // source arguments, which can rebind that local through a handler. + let lowered_callee = self.expression(callee)?; + let descriptor = self.scalar(lowered_callee, callee.span)?; + self.leaf(NativeLeaf::FunctionCallableCheck, &[descriptor], true)?; + let code_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_CODE_FIELD as i64); + let address = self.leaf(NativeLeaf::StructField, &[descriptor, code_index], true)?; + let environment_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64); + let environment = self.leaf( + NativeLeaf::StructField, + &[descriptor, environment_index], + true, + )?; + let pointer_type = self.module.target_config().pointer_type(); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().ireduce(pointer_type, address) + }; + let context = self + .builder + .try_use_var(self.runtime_context) + .map_err(|error| { + contract_error( + self.symbol, + expression.span, + format!("cannot read native runtime context: {error}"), + ) + })?; + let mut native_args = vec![context, environment]; + for ((argument, value), view) in args.iter().zip(evaluated).zip(&view_params) { + if is_task_scalar(self.types, argument.ty)? { + let (bits, depth) = self.scalar_task(value, argument.span)?; + native_args.push(bits); + native_args.push(depth); + continue; + } + match value { + LoweredValue::Scalar(value) => native_args.push(value), + LoweredValue::ScalarTask(value, _) => native_args.push(value), + LoweredValue::Owned(value, slot) => { + if !view && is_linear(self.types, argument.ty) { + self.clear_slot(slot); + } + native_args.push(value); + } + } + } + let mut signature = self.module.make_signature(); + signature + .params + .push(runtime_context_abi_param(self.module)); + signature.params.push(AbiParam::new(ir::types::I64)); + for param in params { + if let Some(ty) = clif_type(self.types, param, "indirect call parameter")? { + signature.params.push(AbiParam::new(ty)); + if is_task_scalar(self.types, param)? { + signature.params.push(AbiParam::new(ir::types::I64)); + } + } + } + if let Some(ty) = clif_type(self.types, return_type, "indirect call result")? { + signature.returns.push(AbiParam::new(ty)); + if is_task_scalar(self.types, return_type)? { + signature.returns.push(AbiParam::new(ir::types::I64)); + } + } + let signature = self.builder.import_signature(signature); + let call = self + .builder + .ins() + .call_indirect(signature, address, &native_args); + let results = self.builder.func.dfg.inst_results(call).to_vec(); + self.check_failure()?; + let value = results.first().copied().ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "value-returning indirect call produced no value", + ) + })?; + if is_task_scalar(self.types, expression.ty)? { + let depth = results.get(1).copied().ok_or_else(|| { + contract_error( + self.symbol, + expression.span, + "primitive indirect call produced no pending depth", + ) + })?; + return Ok(LoweredValue::ScalarTask(value, depth)); + } + if is_linear(self.types, expression.ty) { + self.own_linear(value) + } else if is_copy_owned(self.types, expression.ty) { + self.own(value) + } else { + Ok(LoweredValue::Scalar(value)) + } + } + + fn binary( + &mut self, + left: Value, + op: BinaryOp, + right: Value, + kind: ScalarKind, + span: Span, + ) -> Result { + let value = match kind { + ScalarKind::SignedInteger(_) => match op { + BinaryOp::Add => self.builder.ins().iadd(left, right), + BinaryOp::Subtract => self.builder.ins().isub(left, right), + BinaryOp::Multiply => self.builder.ins().imul(left, right), + BinaryOp::Divide => self.signed_division(left, right), + BinaryOp::Modulo => self.signed_remainder(left, right), + BinaryOp::Equal => self.builder.ins().icmp(IntCC::Equal, left, right), + BinaryOp::NotEqual => self.builder.ins().icmp(IntCC::NotEqual, left, right), + BinaryOp::Less => self.builder.ins().icmp(IntCC::SignedLessThan, left, right), + BinaryOp::Greater => self + .builder + .ins() + .icmp(IntCC::SignedGreaterThan, left, right), + BinaryOp::LessEqual => { + self.builder + .ins() + .icmp(IntCC::SignedLessThanOrEqual, left, right) + } + BinaryOp::GreaterEqual => { + self.builder + .ins() + .icmp(IntCC::SignedGreaterThanOrEqual, left, right) + } + BinaryOp::And | BinaryOp::Or => { + return Err(contract_error( + self.symbol, + span, + "boolean operator has a signed integer operand", + )); + } + }, + ScalarKind::UnsignedInteger(_) => match op { + BinaryOp::Add => self.builder.ins().iadd(left, right), + BinaryOp::Subtract => self.builder.ins().isub(left, right), + BinaryOp::Multiply => self.builder.ins().imul(left, right), + BinaryOp::Divide => self.builder.ins().udiv(left, right), + BinaryOp::Modulo => self.builder.ins().urem(left, right), + BinaryOp::Equal => self.builder.ins().icmp(IntCC::Equal, left, right), + BinaryOp::NotEqual => self.builder.ins().icmp(IntCC::NotEqual, left, right), + BinaryOp::Less => self + .builder + .ins() + .icmp(IntCC::UnsignedLessThan, left, right), + BinaryOp::Greater => { + self.builder + .ins() + .icmp(IntCC::UnsignedGreaterThan, left, right) + } + BinaryOp::LessEqual => { + self.builder + .ins() + .icmp(IntCC::UnsignedLessThanOrEqual, left, right) + } + BinaryOp::GreaterEqual => { + self.builder + .ins() + .icmp(IntCC::UnsignedGreaterThanOrEqual, left, right) + } + BinaryOp::And | BinaryOp::Or => { + return Err(contract_error( + self.symbol, + span, + "boolean operator has an unsigned integer operand", + )); + } + }, + ScalarKind::Float(bits) => match op { + BinaryOp::Add => self.builder.ins().fadd(left, right), + BinaryOp::Subtract => self.builder.ins().fsub(left, right), + BinaryOp::Multiply => self.builder.ins().fmul(left, right), + BinaryOp::Divide => self.builder.ins().fdiv(left, right), + BinaryOp::Modulo => { + // Match the interpreter's binary64 carrier, rounding back + // to binary32 at the checked expression boundary. + let (left, right) = if bits == 32 { + ( + self.builder.ins().fpromote(ir::types::F64, left), + self.builder.ins().fpromote(ir::types::F64, right), + ) + } else { + (left, right) + }; + let result = self.leaf(NativeLeaf::FloatRemainder, &[left, right], true)?; + if bits == 32 { + self.builder.ins().fdemote(ir::types::F32, result) + } else { + result + } + } + BinaryOp::Equal => self.builder.ins().fcmp(FloatCC::Equal, left, right), + BinaryOp::NotEqual => self.builder.ins().fcmp(FloatCC::NotEqual, left, right), + BinaryOp::Less => self.builder.ins().fcmp(FloatCC::LessThan, left, right), + BinaryOp::Greater => self.builder.ins().fcmp(FloatCC::GreaterThan, left, right), + BinaryOp::LessEqual => { + self.builder + .ins() + .fcmp(FloatCC::LessThanOrEqual, left, right) + } + BinaryOp::GreaterEqual => { + self.builder + .ins() + .fcmp(FloatCC::GreaterThanOrEqual, left, right) + } + BinaryOp::And | BinaryOp::Or => { + return Err(contract_error( + self.symbol, + span, + "unsupported operator has a floating-point operand", + )); + } + }, + ScalarKind::Bool => match op { + BinaryOp::Equal => self.builder.ins().icmp(IntCC::Equal, left, right), + BinaryOp::NotEqual => self.builder.ins().icmp(IntCC::NotEqual, left, right), + BinaryOp::And | BinaryOp::Or => { + return Err(contract_error( + self.symbol, + span, + "short-circuit boolean operator reached eager binary lowering", + )); + } + _ => { + return Err(contract_error( + self.symbol, + span, + "arithmetic or ordering operator has a bool operand", + )); + } + }, + ScalarKind::Nothing if matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) => { + let not_equal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(op == BinaryOp::NotEqual)); + let value = self.leaf(NativeLeaf::NothingEqual, &[left, right, not_equal], true)?; + self.builder.ins().ireduce(ir::types::I8, value) + } + ScalarKind::Nothing + | ScalarKind::String + | ScalarKind::Bytes + | ScalarKind::Sum + | ScalarKind::List + | ScalarKind::Set + | ScalarKind::Map + | ScalarKind::Struct + | ScalarKind::Enum + | ScalarKind::Bitfield + | ScalarKind::Machine + | ScalarKind::Construction + | ScalarKind::Function + | ScalarKind::Actor + | ScalarKind::Stdout + | ScalarKind::Clock + | ScalarKind::Random + | ScalarKind::Environment + | ScalarKind::Graphics + | ScalarKind::OpaqueCapability + | ScalarKind::Uninhabited => { + return Err(contract_error( + self.symbol, + span, + "binary operator has a nothing operand", + )); + } + }; + Ok(value) + } + + fn short_circuit_boolean( + &mut self, + left: LoweredValue, + op: BinaryOp, + right: &Expression, + kind: ScalarKind, + span: Span, + ) -> Result { + if kind != ScalarKind::Bool { + return Err(contract_error( + self.symbol, + span, + "short-circuit boolean operator has a non-bool operand", + )); + } + + let (left_value, left_depth) = self.scalar_task(left, span)?; + let pending = self.builder.ins().icmp_imm(IntCC::NotEqual, left_depth, 0); + + let right_block = self.builder.create_block(); + let merge_block = self.builder.create_block(); + let result = self.builder.append_block_param(merge_block, ir::types::I8); + let short_value = match op { + BinaryOp::And => self.builder.ins().iconst(ir::types::I8, 0), + BinaryOp::Or => self.builder.ins().iconst(ir::types::I8, 1), + _ => { + return Err(contract_error( + self.symbol, + span, + "non-short-circuit operator reached boolean CFG lowering", + )); + } + }; + + match op { + BinaryOp::And => { + let evaluate_right = self.builder.ins().bor(left_value, pending); + self.builder.ins().brif( + evaluate_right, + right_block, + &[], + merge_block, + &[short_value.into()], + ) + } + BinaryOp::Or => { + let plain_true = self.builder.ins().band_not(left_value, pending); + self.builder.ins().brif( + plain_true, + merge_block, + &[short_value.into()], + right_block, + &[], + ) + } + _ => unreachable!("operator checked above"), + }; + + self.builder.switch_to_block(right_block); + let lowered_right = self.expression(right)?; + let right_value = self.scalar(lowered_right, right.span)?; + self.check_pending_scalar_binary( + left, + TypeInterner::BOOL, + op, + lowered_right, + right.ty, + span, + )?; + self.builder.ins().jump(merge_block, &[right_value.into()]); + self.builder.switch_to_block(merge_block); + Ok(result) + } + + fn signed_division(&mut self, dividend: Value, divisor: Value) -> Value { + let ty = self.builder.func.dfg.value_type(dividend); + let zero = self.builder.ins().iconst(ty, 0); + let dividend_negative = self + .builder + .ins() + .icmp(IntCC::SignedLessThan, dividend, zero); + let divisor_negative = self + .builder + .ins() + .icmp(IntCC::SignedLessThan, divisor, zero); + let negated_dividend = self.builder.ins().ineg(dividend); + let negated_divisor = self.builder.ins().ineg(divisor); + let dividend_magnitude = + self.builder + .ins() + .select(dividend_negative, negated_dividend, dividend); + let divisor_magnitude = + self.builder + .ins() + .select(divisor_negative, negated_divisor, divisor); + let magnitude = self + .builder + .ins() + .udiv(dividend_magnitude, divisor_magnitude); + let negative = self.builder.ins().bxor(dividend_negative, divisor_negative); + let negated = self.builder.ins().ineg(magnitude); + self.builder.ins().select(negative, negated, magnitude) + } + + fn signed_remainder(&mut self, dividend: Value, divisor: Value) -> Value { + let ty = self.builder.func.dfg.value_type(dividend); + let zero = self.builder.ins().iconst(ty, 0); + let dividend_negative = self + .builder + .ins() + .icmp(IntCC::SignedLessThan, dividend, zero); + let divisor_negative = self + .builder + .ins() + .icmp(IntCC::SignedLessThan, divisor, zero); + let negated_dividend = self.builder.ins().ineg(dividend); + let negated_divisor = self.builder.ins().ineg(divisor); + let dividend_magnitude = + self.builder + .ins() + .select(dividend_negative, negated_dividend, dividend); + let divisor_magnitude = + self.builder + .ins() + .select(divisor_negative, negated_divisor, divisor); + let magnitude = self + .builder + .ins() + .urem(dividend_magnitude, divisor_magnitude); + let negated = self.builder.ins().ineg(magnitude); + self.builder + .ins() + .select(dividend_negative, negated, magnitude) + } + + fn define_local( + &mut self, + local: jett_mir::LocalId, + value: LoweredValue, + span: Span, + ) -> Result<(), CodegenError> { + if let Some(slot) = self.local_slots[local.index() as usize] { + let owned = if is_linear(self.types, self.local_types[local.index() as usize].ty) { + let LoweredValue::Owned(v, source) = value else { + return Err(self.unsupported(span, "borrow escaping into owner")); + }; + self.clear_slot(source); + v + } else if is_function(self.types, self.local_types[local.index() as usize].ty) { + let value = self.scalar(value, span)?; + self.leaf(NativeLeaf::StructClone, &[value], true)? + } else { + let value = self.scalar(value, span)?; + self.leaf(NativeLeaf::Retain, &[value], true)? + }; + self.drop_slot(slot)?; + self.builder.ins().stack_store(owned, slot, 0); + return Ok(()); + } + let variable = variable_for(self.variables, local.index(), span, self.symbol)?; + if let (Some(variable), Some(pending_variable)) = + (variable, self.pending_variables[local.index() as usize]) + { + let (bits, depth) = self.scalar_task(value, span)?; + self.builder.try_def_var(variable, bits).map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot define primitive native local: {error}"), + ) + })?; + return self + .builder + .try_def_var(pending_variable, depth) + .map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot define primitive pending depth: {error}"), + ) + }); + } + match (variable, value) { + (Some(variable), LoweredValue::Scalar(value)) => { + self.builder.try_def_var(variable, value).map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot define native local: {error}"), + ) + }) + } + _ => Err(contract_error( + self.symbol, + span, + "native local and value representations differ", + )), + } + } + + fn required_clif_type(&self, ty: TypeId, span: Span) -> Result { + clif_type(self.types, ty, "native expression")?.ok_or_else(|| { + contract_error( + self.symbol, + span, + "expression requires a native scalar value", + ) + }) + } + + fn nothing(&mut self) -> LoweredValue { + LoweredValue::Scalar(self.builder.ins().iconst(ir::types::I64, 0)) + } + + fn scalar(&self, value: LoweredValue, _span: Span) -> Result { + match value { + LoweredValue::Scalar(value) + | LoweredValue::ScalarTask(value, _) + | LoweredValue::Owned(value, _) => Ok(value), + } + } + + fn scalar_task( + &mut self, + value: LoweredValue, + span: Span, + ) -> Result<(Value, Value), CodegenError> { + match value { + LoweredValue::ScalarTask(bits, depth) => Ok((bits, depth)), + LoweredValue::Scalar(bits) => { + let depth = self.builder.ins().iconst(ir::types::I64, 0); + Ok((bits, depth)) + } + LoweredValue::Owned(_, _) => Err(self.unsupported(span, "owned primitive task")), + } + } + + fn check_pending_scalar_binary( + &mut self, + left: LoweredValue, + left_ty: TypeId, + op: BinaryOp, + right: LoweredValue, + right_ty: TypeId, + span: Span, + ) -> Result<(), CodegenError> { + let (Some(left_kind), Some(right_kind)) = ( + task_scalar_debug_kind(self.types, left_ty)?, + task_scalar_debug_kind(self.types, right_ty)?, + ) else { + return Ok(()); + }; + let (_, left_depth) = self.scalar_task(left, span)?; + let (_, right_depth) = self.scalar_task(right, span)?; + let left_bits = self.payload_bits(left).0; + let right_bits = self.payload_bits(right).0; + let left_kind = self.builder.ins().iconst(ir::types::I32, left_kind as i64); + let right_kind = self.builder.ins().iconst(ir::types::I32, right_kind as i64); + let op = self + .builder + .ins() + .iconst(ir::types::I32, native_pending_binary_op(op) as i64); + self.leaf( + NativeLeaf::ScalarPendingBinaryCheck, + &[ + left_bits, + left_kind, + left_depth, + right_bits, + right_kind, + right_depth, + op, + ], + true, + )?; + Ok(()) + } + + fn check_pending_bool_condition( + &mut self, + value: LoweredValue, + context: u32, + span: Span, + ) -> Result<(), CodegenError> { + let (_, depth) = self.scalar_task(value, span)?; + let bits = self.payload_bits(value).0; + let context = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(context)); + self.leaf( + NativeLeaf::ScalarPendingBoolConditionCheck, + &[bits, depth, context], + true, + )?; + Ok(()) + } + + fn unsupported(&self, span: Span, construct: &str) -> CodegenError { + CodegenError::UnsupportedMir { + function: self.symbol.to_string(), + span, + construct: construct.to_string(), + } + } +} + +fn integer_immediate(value: i128, kind: ScalarKind) -> Option { + match kind { + ScalarKind::SignedInteger(bits) => { + let value = i64::try_from(value).ok()?; + let encoded = u64::from_ne_bytes(value.to_ne_bytes()); + let mask = match bits { + 1..=63 => (1_u64 << u32::from(bits)) - 1, + 64 => u64::MAX, + _ => return None, + }; + Some(i64::from_ne_bytes((encoded & mask).to_ne_bytes())) + } + ScalarKind::UnsignedInteger(_) => { + let value = u64::try_from(value).ok()?; + Some(i64::from_ne_bytes(value.to_ne_bytes())) + } + _ => None, + } +} + +fn variable_for( + variables: &[Option], + index: u32, + span: Span, + symbol: &str, +) -> Result, CodegenError> { + let index = usize::try_from(index) + .map_err(|_| contract_error(symbol, span, "local index does not fit this host"))?; + variables + .get(index) + .copied() + .ok_or_else(|| contract_error(symbol, span, "local is absent from the native local table")) +} + +fn block_for( + blocks: &[ir::Block], + index: u32, + span: Span, + symbol: &str, +) -> Result { + let index = usize::try_from(index) + .map_err(|_| contract_error(symbol, span, "block index does not fit this host"))?; + blocks + .get(index) + .copied() + .ok_or_else(|| contract_error(symbol, span, "block is absent from the native block table")) +} + +fn contract_error(symbol: &str, span: Span, message: impl Into) -> CodegenError { + CodegenError::InvalidMirContract { + function: symbol.to_string(), + span, + message: message.into(), + } +} + +fn reordered_map( + items: &[T], + order: &[usize], + mut map: impl FnMut(&T) -> Result, + invalid_order: impl Fn() -> E, +) -> Result, E> { + if order.len() != items.len() { + return Err(invalid_order()); + } + let mut values = (0..items.len()).map(|_| None).collect::>>(); + for &index in order { + let Some(item) = items.get(index) else { + return Err(invalid_order()); + }; + if values[index].is_some() { + return Err(invalid_order()); + } + values[index] = Some(map(item)?); + } + values + .into_iter() + .map(|value| value.ok_or_else(&invalid_order)) + .collect() +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use jett_common::{FileId, SourceOrigin}; + + use super::*; + use jett_mir::copy_values::CopyValuePlan; + + fn lower_source(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "check errors: {:?}", + checked.diagnostics + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + let mir = jett_mir::lower(&hir, &checked.interner).expect("MIR lowering"); + (mir, checked.interner) + } + + const UNINHABITED_DESCRIPTOR_SOURCE: &str = r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + function(T) returns int64 callback = function(ignored: T) returns int64: return 7 + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#; + + #[test] + fn uninhabited_input_descriptor_emits_nonnull_trap_entry_without_value_abi() { + let (mut program, types) = lower_source(UNINHABITED_DESCRIPTOR_SOURCE); + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("original descriptor metadata"); + jett_mir::prepare_native_sequences(&mut program, &types); + jett_mir::prepare_native_uninhabited_sums(&mut program, &types); + jett_mir::prepare_native_generated_functions(&mut program, &types); + let verified = verify_program(&program, &types).expect("prepared native program"); + let mut module = test_object_module(); + let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) + .expect("retained descriptor declaration"); + let descriptor = program + .functions + .iter() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("inferred Never-input inline target"); + assert_eq!(descriptor.capture_count, 0); + assert_eq!(descriptor.params[0].ty, TypeInterner::NEVER); + let declaration = declarations + .get(descriptor.id) + .expect("retained descriptor entry"); + assert_eq!(declaration.signature.params.len(), 2); + assert!(declaration.signature.returns.is_empty()); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + descriptor, + &types, + &declaration.symbol, + &mut context, + ) + .expect("descriptor entry translation"); + let opcodes = context + .func + .layout + .blocks() + .flat_map(|block| context.func.layout.block_insts(block)) + .map(|instruction| context.func.dfg.insts[instruction].opcode()) + .collect::>(); + assert_eq!(opcodes, [ir::Opcode::Trap]); + let object = emit_host_object(&program, &types).expect("owned stored descriptor object"); + assert!( + object + .symbols + .iter() + .any(|symbol| symbol == &declaration.symbol) + ); + assert!(!object.bytes.is_empty()); + } + + #[test] + fn uninhabited_input_descriptors_keep_captures_and_never_result_metadata() { + for source in [ + r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + int64 size = 7 + function(T) returns int64 callback = function(ignored: T) returns int64: return size + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#, + r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns T]: + function(T) returns T callback = function(value: T) returns T: return value + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#, + ] { + let (program, types) = lower_source(source); + let descriptor = program + .functions + .iter() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("inferred descriptor-only target"); + assert!( + descriptor.params[descriptor.capture_count..] + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) + ); + if descriptor.capture_count != 0 { + assert_eq!(descriptor.capture_count, 1); + assert_eq!(descriptor.params[0].ty, TypeInterner::INT64); + } else { + assert_eq!(descriptor.return_type, TypeInterner::NEVER); + } + let object = emit_host_object(&program, &types) + .expect("capture environment or uncallable Never-result descriptor"); + assert!(!object.bytes.is_empty()); + } + } + + #[test] + fn descriptor_original_loop_body_validates_before_native_preparation() { + let source = r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + function(T) returns int64 callback = function(ignored: T) returns int64: + mutable int64 total = 0 + for item in list(1, 2): + total = total + item + return total + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#; + let (program, types) = lower_source(source); + let descriptor = program + .functions + .iter() + .find(|function| crate::verify::descriptor_only_function(function)) + .unwrap(); + assert!( + descriptor + .blocks + .iter() + .any(|block| { matches!(block.terminator.kind, TerminatorKind::ForEach { .. }) }) + ); + assert!( + !emit_host_object(&program, &types) + .expect("original typed loop needs no callable frame") + .bytes + .is_empty() + ); + let (mut invalid, types) = lower_source(source); + let descriptor = invalid + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .unwrap(); + let key = descriptor + .blocks + .iter() + .find_map(|block| { + if let TerminatorKind::ForEach { key, .. } = block.terminator.kind { + Some(key) + } else { + None + } + }) + .unwrap(); + descriptor.locals[key.index() as usize].ty = TypeInterner::BOOL; + descriptor.locals[key.index() as usize].debug_ty = TypeInterner::BOOL; + assert!(matches!( + emit_host_object(&invalid, &types), + Err(CodegenError::InvalidMirContract { message, .. }) + if message == "original descriptor loop key type does not match its iterable" + )); + } + + #[test] + fn descriptor_original_never_equality_keeps_exact_operand_and_result_proof() { + for operator in ["==", "!="] { + let source = format!( + r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns bool]: + function(T) returns bool callback = function(value: T) returns bool: return value {operator} value + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"# + ); + let (program, types) = lower_source(&source); + let descriptor = program + .functions + .iter() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("inferred Never equality target"); + assert_eq!(descriptor.return_type, TypeInterner::BOOL); + assert_eq!(descriptor.params[0].ty, TypeInterner::NEVER); + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("exact Never equality is valid original metadata"); + assert!( + !emit_host_object(&program, &types) + .expect("uncallable equality needs no Never value carrier") + .bytes + .is_empty() + ); + + for corruption in 0..2 { + let (mut invalid, types) = lower_source(&source); + let descriptor = invalid + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .unwrap(); + let value = descriptor + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) + .expect("inline equality return"); + if corruption == 0 { + value.ty = TypeInterner::INT64; + } else { + let ExpressionKind::Binary { right, .. } = &mut value.kind else { + panic!("inline equality expression"); + }; + right.ty = TypeInterner::INT64; + right.kind = ExpressionKind::Int(7); + } + let expected = if corruption == 0 { + "binary operation is inconsistent with checked type `int64`" + } else { + "binary operand types differ" + }; + assert!( + matches!( + emit_host_object(&invalid, &types), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + ), + "{operator} corruption {corruption}" + ); + } + } + } + + #[test] + fn enclosing_descriptor_compaction_preserves_original_metadata_and_surviving_uses() { + for corruption in 0..4 { + let (mut program, types) = lower_source(UNINHABITED_DESCRIPTOR_SOURCE); + let factory = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "stored") + .expect("checked enclosing factory"); + let detached = factory + .locals + .iter() + .position(|local| local.ty == TypeInterner::NEVER) + .expect("unused extracted inline formal"); + assert!( + !factory + .params + .iter() + .any(|param| { param.local == factory.locals[detached].id }) + ); + let mut invalid_metadata = None; + if corruption == 2 { + factory.blocks[0].statements.push(jett_mir::Statement { + kind: StatementKind::Trace(factory.locals[detached].id), + span: factory.span, + }); + } else if corruption == 3 { + factory.blocks[0].statements.push(jett_mir::Statement { + kind: StatementKind::Let { + local: factory.locals[detached].id, + value: Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::NEVER, + span: factory.span, + }, + }, + span: factory.span, + }); + } else { + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for _ in 0..=types.len() { + invalid = foreign.intern(jett_types::Type::List(invalid)); + } + assert!(invalid.index() as usize >= types.len()); + invalid_metadata = Some(invalid); + if corruption == 0 { + factory.locals[detached].ty = invalid; + } else { + factory.locals[detached].debug_ty = invalid; + } + } + let result = emit_host_object(&program, &types); + if let Some(invalid) = invalid_metadata { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "Resource manifest type visitor is outside its interner"), + "enclosing metadata corruption {corruption}" + ); + assert!(matches!( + crate::verify::scalar_kind(&types, invalid, "enclosing original metadata"), + Err(CodegenError::UnsupportedType { type_name, context }) + if type_name == format!("", invalid.index()) + && context == "enclosing original metadata" + )); + } else { + // These live Trace/Let controls still demand a native Never + // carrier, independently of detached-slot compaction. + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "enclosing live slot corruption {corruption}" + ); + } + } + } + + #[test] + fn descriptor_pruning_preserves_original_body_and_metadata_validation() { + for corruption in 0..6 { + let (mut program, types) = lower_source(UNINHABITED_DESCRIPTOR_SOURCE); + let function_count = program.functions.len(); + let source_call = program + .functions + .iter() + .flat_map(|function| &function.blocks) + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) + if matches!(value.kind, ExpressionKind::Call { .. }) => + { + Some(value.kind.clone()) + } + _ => block + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::Call { .. }) => + { + Some(value.kind.clone()) + } + _ => None, + }), + }) + .expect("fixture retains a checked source call packet"); + let descriptor = program + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("descriptor target"); + match corruption { + 0 => { + let Some(value) = descriptor.blocks.iter_mut().find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) else { + panic!("inline value return"); + }; + value.kind = ExpressionKind::Bool(true); + } + 1 => { + let local = descriptor.params[0].local; + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for _ in 0..=types.len() { + invalid = foreign.intern(jett_types::Type::List(invalid)); + } + assert!(invalid.index() as usize >= types.len()); + let unused = descriptor.locals.len(); + let mut copied = descriptor.locals[local.index() as usize].clone(); + copied.id = jett_hir::LocalId::new(unused as u32); + copied.name = "unused".into(); + copied.ty = invalid; + copied.debug_ty = invalid; + descriptor.locals.push(copied); + } + 2 => descriptor.capture_count = descriptor.params.len() + 1, + 3 => { + descriptor.debug_kind = jett_hir::FunctionDebugKind::Inline; + descriptor.params[0].name = "bad\nname".into(); + let local = descriptor.params[0].local; + descriptor.locals[local.index() as usize].name = "bad\nname".into(); + } + 4 => { + let Some(value) = descriptor.blocks.iter_mut().find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) else { + panic!("inline value return"); + }; + value.kind = source_call; + let ExpressionKind::Call { function, .. } = &mut value.kind else { + panic!("source call fixture"); + }; + *function = FunctionId::new(function_count as u32 + 1); + } + 5 => { + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for _ in 0..=types.len() { + invalid = foreign.intern(jett_types::Type::List(invalid)); + } + assert!(invalid.index() as usize >= types.len()); + let local = descriptor.params[0].local.index() as usize; + descriptor.locals[local].debug_ty = invalid; + } + _ => unreachable!(), + } + assert!( + emit_host_object(&program, &types).is_err(), + "corruption {corruption}" + ); + } + } + + #[test] + fn callable_direct_edges_cannot_invoke_an_exact_never_input_descriptor() { + let (mut program, types) = lower_source( + r#"namespace app +function target(value: int64) returns int64: + return 7 +function root() returns int64: + return target(1) +"#, + ); + let target = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "target") + .unwrap(); + target.params[0].ty = TypeInterner::NEVER; + let argument = target.params[0].local.index() as usize; + target.locals[argument].ty = TypeInterner::NEVER; + target.locals[argument].debug_ty = TypeInterner::NEVER; + assert!(crate::verify::descriptor_only_function(target)); + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "source function ownership signature differs from the exact HIR declaration" + )); + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked caller"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original callable return expression"); + assert!(matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), + Err(CodegenError::InvalidMirContract { message, .. }) + if message == "callable direct edge requires an uninhabited parameter" + )); + } + + #[test] + fn callable_indirect_edges_cannot_invoke_an_exact_never_input_descriptor() { + let (mut program, mut types) = lower_source( + r#"namespace app +function root() returns int64: + function(int64) returns int64 callback = function(value: int64) returns int64: return 7 + return callback(1) +"#, + ); + let callback_type = types.intern(jett_types::Type::Function { + params: vec![TypeInterner::NEVER], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + let descriptor = program + .functions + .iter_mut() + .find(|function| function.debug_kind == jett_hir::FunctionDebugKind::Inline) + .unwrap(); + descriptor.params[0].ty = TypeInterner::NEVER; + let argument = descriptor.params[0].local.index() as usize; + descriptor.locals[argument].ty = TypeInterner::NEVER; + descriptor.locals[argument].debug_ty = TypeInterner::NEVER; + let root = &mut program.functions[0]; + let StatementKind::Let { local, value } = &mut root.blocks[0].statements[0].kind else { + panic!("callback binding"); + }; + value.ty = callback_type; + root.locals[local.index() as usize].ty = callback_type; + root.locals[local.index() as usize].debug_ty = callback_type; + let TerminatorKind::Return(Some(value)) = &mut root.blocks[0].terminator.kind else { + panic!("indirect return"); + }; + let ExpressionKind::IndirectCall { callee, .. } = &mut value.kind else { + panic!("indirect callback use"); + }; + callee.ty = callback_type; + if let ExpressionKind::View(inner) | ExpressionKind::Clone(inner) = &mut callee.kind { + inner.ty = callback_type; + } + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "indirect call ownership signature differs from its actual callee" + )); + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked caller"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original callable return expression"); + assert!(matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), + Err(CodegenError::InvalidMirContract { message, .. }) + if message == "callable indirect edge requires an uninhabited parameter" + )); + } + + #[test] + fn descriptor_metadata_cannot_manufacture_never_values_or_capture_them() { + for phantom in [ExpressionKind::Nothing, ExpressionKind::Int(7)] { + let (mut program, types) = lower_source( + r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns T]: + function(T) returns T callback = function(value: T) returns T: return value + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#, + ); + let descriptor = program + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .unwrap(); + let value = descriptor + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) + .unwrap(); + assert_eq!(value.ty, TypeInterner::NEVER); + value.kind = phantom; + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMirContract { .. }) + )); + } + for never_capture in [true, false] { + let (mut program, types) = lower_source( + r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + int64 size = 7 + function(T) returns int64 callback = function(ignored: T) returns int64: return size + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#, + ); + let descriptor = program + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .unwrap(); + assert_eq!(descriptor.capture_count, 1); + if never_capture { + descriptor.params[0].ty = TypeInterner::NEVER; + let capture = descriptor.params[0].local.index() as usize; + descriptor.locals[capture].ty = TypeInterner::NEVER; + descriptor.locals[capture].debug_ty = TypeInterner::NEVER; + } else { + descriptor.params[0].mode = jett_mir::ParamMode::View; + } + let expected_message = if never_capture { + "descriptor capture cannot be uninhabited" + } else { + "descriptor capture must use owned environment storage" + }; + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMirContract { message, .. }) + if message == expected_message + )); + } + } + + #[test] + fn descriptor_authority_rejects_return_only_never_and_mismatched_view_modes() { + for mismatch in [false, true] { + let (mut program, mut types) = lower_source(UNINHABITED_DESCRIPTOR_SOURCE); + let descriptor = program + .functions + .iter_mut() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("inline descriptor"); + if mismatch { + descriptor.params[0].mode = jett_mir::ParamMode::View; + } else { + // A result-only Never cannot erase effects from a callable body. + descriptor.params[0].ty = TypeInterner::INT64; + let argument = descriptor.params[0].local.index() as usize; + descriptor.locals[argument].ty = TypeInterner::INT64; + descriptor.locals[argument].debug_ty = TypeInterner::INT64; + descriptor.return_type = TypeInterner::NEVER; + assert!(!crate::verify::descriptor_only_function(descriptor)); + let result_only = types.intern(jett_types::Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: TypeInterner::NEVER, + }); + assert!(matches!( + scalar_kind(&types, result_only, "result-only descriptor"), + Err(CodegenError::UnsupportedType { .. }) + )); + } + assert!(emit_host_object(&program, &types).is_err()); + } + } + + const ABSENT_CAPTURED_RESULT_SOURCE: &str = r#"namespace app +function capture_optional[T](candidate: optional[T]) returns nothing: + T item = candidate handle: + return nothing + function() returns T callback = function() returns T: return item + T copied = callback() + return nothing +function capture_success[T, E](candidate: result[T, E]) returns nothing: + T item = candidate handle error: + return nothing + function() returns T callback = function() returns T: return item + T copied = callback() + return nothing +function capture_failure[T, E](candidate: result[T, E]) returns nothing: + T item = candidate handle error: + function() returns E callback = function() returns E: return error + E copied = callback() + return nothing + return nothing +function root() returns nothing: + capture_optional(none) + capture_success(fail("absent")) + capture_failure(ok(8)) + return nothing +"#; + + #[test] + fn absent_sum_arms_keep_original_function_result_metadata_without_native_carriers() { + let (mut program, mut types) = lower_source(ABSENT_CAPTURED_RESULT_SOURCE); + let absent_closures = program + .functions + .iter() + .filter(|function| { + function.debug_kind == jett_hir::FunctionDebugKind::Inline + && function.return_type == TypeInterner::NEVER + }) + .collect::>(); + assert_eq!(absent_closures.len(), 3); + assert!(absent_closures.iter().all(|function| { + function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::NEVER + && !crate::verify::descriptor_only_function(function) + })); + let absent_symbols = absent_closures + .iter() + .map(|function| crate::symbol_name(&function.identity, &types).unwrap()) + .collect::>(); + let result_only = types.intern(jett_types::Type::Function { + params: Vec::new(), + view_params: Vec::new(), + return_type: TypeInterner::NEVER, + }); + let optional = types.intern(jett_types::Type::Optional(result_only)); + let nested = types.intern(jett_types::Type::List(optional)); + assert!(matches!( + scalar_kind(&types, nested, "nested runtime result"), + Err(CodegenError::UnsupportedType { .. }) + )); + let factory = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "capture_optional") + .unwrap(); + let mut unused = factory.locals[0].clone(); + unused.id = jett_hir::LocalId::new(u32::try_from(factory.locals.len()).unwrap()); + unused.name = "unused_nested_result_metadata".into(); + unused.ty = nested; + unused.debug_ty = nested; + factory.locals.push(unused); + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("valid original metadata in absent sum arms"); + jett_mir::prepare_native_sequences(&mut program, &types); + jett_mir::prepare_native_uninhabited_sums(&mut program, &types); + jett_mir::prepare_native_generated_functions(&mut program, &types); + let verified = verify_program(&program, &types).expect("prepared inhabited sum arms"); + assert!( + verified + .functions() + .iter() + .all(|function| !absent_symbols.contains(&function.symbol)) + ); + let object = emit_host_object(&program, &types).expect("absent closure object"); + assert!(!object.bytes.is_empty()); + assert!( + object + .symbols + .iter() + .all(|symbol| !absent_symbols.contains(symbol)) + ); + } + + #[test] + fn absent_function_results_do_not_hide_malformed_original_type_metadata() { + for debug_type in [false, true] { + for corruption in 0..4 { + let (mut program, mut types) = lower_source(ABSENT_CAPTURED_RESULT_SOURCE); + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for _ in 0..=types.len() + 2 { + invalid = foreign.intern(jett_types::Type::List(invalid)); + } + assert!(invalid.index() as usize > types.len() + 1); + let malformed = match corruption { + 0 => types.intern(jett_types::Type::Function { + params: vec![TypeInterner::INT64], + view_params: Vec::new(), + return_type: TypeInterner::NEVER, + }), + 1 => types.intern(jett_types::Type::Function { + params: vec![invalid], + view_params: vec![false], + return_type: TypeInterner::NEVER, + }), + 2 => types.intern(jett_types::Type::Function { + params: Vec::new(), + view_params: Vec::new(), + return_type: invalid, + }), + 3 => types.intern(jett_types::Type::Function { + params: Vec::new(), + view_params: Vec::new(), + return_type: TypeInterner::ERROR, + }), + _ => unreachable!(), + }; + let wrapped = types.intern(jett_types::Type::List(malformed)); + let local = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "capture_optional") + .expect("optional specialization") + .locals + .iter_mut() + .find(|local| { + matches!(types.resolve(local.ty), + jett_types::Type::Function { return_type, .. } + if *return_type == TypeInterner::NEVER) + }) + .expect("absent-arm callback local"); + if debug_type { + local.debug_ty = wrapped; + } else { + local.ty = wrapped; + } + let result = emit_host_object(&program, &types); + if matches!(corruption, 1 | 2) { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "Resource manifest type visitor is outside its interner"), + "debug={debug_type}, corruption={corruption}" + ); + } else { + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "debug={debug_type}, corruption={corruption}" + ); + } + assert!( + matches!( + crate::verify::scalar_kind(&types, wrapped, "absent original metadata"), + Err(CodegenError::UnsupportedType { .. }) + ), + "native wrapped metadata debug={debug_type}, corruption={corruption}" + ); + } + } + } + + #[test] + fn live_return_only_never_function_result_still_requires_native_representation() { + let (mut program, mut types) = lower_source( + "namespace app\nfunction root() returns nothing:\n function() returns int64 callback = function() returns int64: return 7\n trace callback\n return nothing\n", + ); + let result_only = types.intern(jett_types::Type::Function { + params: Vec::new(), + view_params: Vec::new(), + return_type: TypeInterner::NEVER, + }); + let root = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "root") + .unwrap(); + let local = root + .locals + .iter_mut() + .find(|local| matches!(types.resolve(local.ty), jett_types::Type::Function { .. })) + .unwrap(); + local.ty = result_only; + local.debug_ty = result_only; + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("return-only Never is valid type metadata"); + assert!(matches!( + scalar_kind(&types, result_only, "live return-only function"), + Err(CodegenError::UnsupportedType { .. }) + )); + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::UnsupportedType { .. }) + )); + } + + const LATENT_NESTED_CALLBACK_SOURCE: &str = r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns T]: + function(T) returns T callback = function(value: T) returns T: + function() returns T inner = function() returns T: return value + return inner() + return list(callback) +function root() returns int64: + return consume_value(stored(list())) + consume_value(stored(list(7))) +"#; + + const SECRET_UNINHABITED_CALLBACK_SOURCE: &str = r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns secret[T]]: + function(T) returns secret[T] callback = function(value: T) returns secret[T]: return value + return list(clone callback) +function root() returns int64: + return consume_value(stored(list())) + consume_value(stored(list(7))) +"#; + + #[test] + fn retained_never_input_descriptor_checks_latent_closure_without_emitting_it() { + let (program, types) = lower_source(LATENT_NESTED_CALLBACK_SOURCE); + let latent = program + .functions + .iter() + .find(|function| { + function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::NEVER + && function.return_type == TypeInterner::NEVER + }) + .expect("zero-argument nested closure with a latent Never capture"); + assert!(!crate::verify::descriptor_only_function(latent)); + let latent_symbol = crate::symbol_name(&latent.identity, &types).unwrap(); + let descriptor = program + .functions + .iter() + .find(|function| { + crate::verify::descriptor_only_function(function) + && function.return_type == TypeInterner::NEVER + }) + .expect("retained exact Never-input outer descriptor"); + let descriptor_symbol = crate::symbol_name(&descriptor.identity, &types).unwrap(); + let concrete = program + .functions + .iter() + .find(|function| { + function.debug_kind == jett_hir::FunctionDebugKind::Inline + && function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::INT64 + && function.return_type == TypeInterner::INT64 + }) + .expect("actually callable nested int64 specialization"); + let concrete_symbol = crate::symbol_name(&concrete.identity, &types).unwrap(); + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("complete original nested typed contracts"); + let object = + emit_host_object(&program, &types).expect("descriptor and concrete nested object"); + assert!(!object.bytes.is_empty()); + assert!(object.symbols.contains(&descriptor_symbol)); + assert!(!object.symbols.contains(&latent_symbol)); + assert!(object.symbols.contains(&concrete_symbol)); + } + + #[test] + fn never_input_secret_result_descriptor_does_not_create_a_secret_never_carrier() { + let (program, mut types) = lower_source(SECRET_UNINHABITED_CALLBACK_SOURCE); + let hidden_never = types.intern(jett_types::Type::Secret(TypeInterner::NEVER)); + let descriptor_type = types.intern(jett_types::Type::Function { + params: vec![TypeInterner::NEVER], + view_params: vec![false], + return_type: hidden_never, + }); + assert_eq!( + scalar_kind(&types, descriptor_type, "secret result descriptor").unwrap(), + ScalarKind::Function + ); + assert!(matches!( + scalar_kind(&types, hidden_never, "live secret Never"), + Err(CodegenError::UnsupportedType { .. }) + )); + let descriptor = program + .functions + .iter() + .find(|function| { + crate::verify::descriptor_only_function(function) + && function.return_type == hidden_never + }) + .expect("secret-qualified Never result retains exact input authority"); + let symbol = crate::symbol_name(&descriptor.identity, &types).unwrap(); + let object = + emit_host_object(&program, &types).expect("stored secret-result descriptor object"); + assert!(object.symbols.contains(&symbol)); + assert!(!object.bytes.is_empty()); + } + + #[test] + fn latent_nested_original_bodies_cannot_fabricate_uninhabited_results() { + for phantom in [ExpressionKind::Nothing, ExpressionKind::Int(7)] { + let (mut program, types) = lower_source(LATENT_NESTED_CALLBACK_SOURCE); + let latent = program + .functions + .iter_mut() + .find(|function| { + function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::NEVER + && function.return_type == TypeInterner::NEVER + }) + .unwrap(); + let value = latent + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) + .unwrap(); + assert_eq!(value.ty, TypeInterner::NEVER); + value.kind = phantom; + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMirContract { .. }) + )); + } + for phantom in [ExpressionKind::Nothing, ExpressionKind::Int(7)] { + let (mut program, types) = lower_source(SECRET_UNINHABITED_CALLBACK_SOURCE); + let descriptor = program.functions.iter_mut().find(|function| { + crate::verify::descriptor_only_function(function) + && matches!(types.resolve(function.return_type), jett_types::Type::Secret(inner) if *inner == TypeInterner::NEVER) + }).unwrap(); + let value = descriptor + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + Some(value) + } else { + None + } + }) + .unwrap(); + assert!( + matches!(types.resolve(value.ty), jett_types::Type::Secret(inner) if *inner == TypeInterner::NEVER) + ); + value.kind = phantom; + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::InvalidMirContract { .. }) + )); + } + } + + #[test] + fn latent_never_capture_edges_keep_exact_owned_capture_contracts() { + for corruption in 0..3 { + let (mut program, types) = lower_source(LATENT_NESTED_CALLBACK_SOURCE); + let latent = program + .functions + .iter_mut() + .find(|function| { + function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::NEVER + && function.return_type == TypeInterner::NEVER + }) + .unwrap(); + match corruption { + 0 => latent.params[0].mode = jett_mir::ParamMode::View, + 1 => { + latent.params[0].ty = TypeInterner::INT64; + let local = latent.params[0].local.index() as usize; + latent.locals[local].ty = TypeInterner::INT64; + latent.locals[local].debug_ty = TypeInterner::INT64; + } + 2 => latent.return_type = TypeInterner::INT64, + _ => unreachable!(), + } + assert!( + emit_host_object(&program, &types).is_err(), + "capture corruption {corruption}" + ); + } + } + + #[test] + fn latent_nested_type_metadata_cannot_hide_foreign_children_or_view_modes() { + for debug_type in [false, true] { + for corruption in 0..3 { + let (mut program, mut types) = lower_source(LATENT_NESTED_CALLBACK_SOURCE); + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for _ in 0..=types.len() + 2 { + invalid = foreign.intern(jett_types::Type::List(invalid)); + } + assert!(invalid.index() as usize > types.len() + 1); + let malformed = types.intern(jett_types::Type::Function { + params: if corruption == 0 { + vec![TypeInterner::INT64] + } else { + Vec::new() + }, + view_params: Vec::new(), + return_type: match corruption { + 0 => TypeInterner::NEVER, + 1 => invalid, + 2 => TypeInterner::ERROR, + _ => unreachable!(), + }, + }); + let wrapped = types.intern(jett_types::Type::List(malformed)); + let latent = program + .functions + .iter_mut() + .find(|function| { + function.capture_count == 1 + && function.params.len() == 1 + && function.params[0].ty == TypeInterner::NEVER + && function.return_type == TypeInterner::NEVER + }) + .unwrap(); + let mut local = latent.locals[0].clone(); + local.id = jett_hir::LocalId::new(u32::try_from(latent.locals.len()).unwrap()); + local.name = "unused_latent_nested_metadata".into(); + if debug_type { + local.debug_ty = wrapped; + } else { + local.ty = wrapped; + } + latent.locals.push(local); + let result = emit_host_object(&program, &types); + if corruption == 1 { + assert!( + matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "Resource manifest type visitor is outside its interner"), + "debug={debug_type}, corruption={corruption}" + ); + } else { + assert!( + matches!(result, Err(CodegenError::UnsupportedType { .. })), + "debug={debug_type}, corruption={corruption}" + ); + } + assert!( + matches!( + crate::verify::scalar_kind(&types, wrapped, "latent original metadata"), + Err(CodegenError::UnsupportedType { .. }) + ), + "native wrapped metadata debug={debug_type}, corruption={corruption}" + ); + } + } + } + + #[test] + fn latent_secret_result_support_does_not_grant_live_return_only_authority() { + let (mut program, mut types) = lower_source( + "namespace app\nfunction root() returns nothing:\n function() returns int64 callback = function() returns int64: return 7\n trace callback\n return nothing\n", + ); + let hidden_never = types.intern(jett_types::Type::Secret(TypeInterner::NEVER)); + let result_only = types.intern(jett_types::Type::Function { + params: Vec::new(), + view_params: Vec::new(), + return_type: hidden_never, + }); + assert!(matches!( + scalar_kind(&types, result_only, "live secret result-only function"), + Err(CodegenError::UnsupportedType { .. }) + )); + let root = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "root") + .unwrap(); + let local = root + .locals + .iter_mut() + .find(|local| matches!(types.resolve(local.ty), jett_types::Type::Function { .. })) + .unwrap(); + local.ty = result_only; + local.debug_ty = result_only; + crate::verify::verify_descriptor_bodies(&program, &types) + .expect("latent type shape is metadata only"); + assert!(matches!( + emit_host_object(&program, &types), + Err(CodegenError::UnsupportedType { .. }) + )); + } + + fn secret_gate_returned_call(function: &mut Function) -> &mut Expression { + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(value)) = &mut function.blocks[entry].terminator.kind + else { + panic!("source returned invocation"); + }; + assert!(matches!( + value.kind, + ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. } + )); + value + } + + fn assert_checked_native_call_gate(program: &Program, types: &TypeInterner, expected: &str) { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "caller") + .expect("source caller"); + let expression = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("source return expression"); + assert!( + matches!( + crate::verify::verify_callable_expression_for_test(program, types, function, expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + ), + "native expression must retain its precise gate: {expected}" + ); + } + + #[test] + fn native_secret_call_result_gate_remains_independent_of_source_certificates() { + for (callee, expected) in [ + ( + "callback", + "direct call result type does not match its signature", + ), + ("factory()", "indirect call result type mismatch"), + ] { + let source = format!( + "function callback(value: int64) returns int64:\n return value\n\ + function factory() returns function(int64) returns int64:\n return callback\n\ + function caller(value: secret[int64]) returns secret[int64]:\n return {callee}(value)\n" + ); + let (mut program, types) = lower_source(&source); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + caller.return_type = TypeInterner::INT64; + secret_gate_returned_call(caller).ty = TypeInterner::INT64; + assert_checked_native_call_gate(&program, &types, expected); + } + } + + #[test] + fn native_secret_call_payload_gate_preserves_exact_primitive_and_nominal_types() { + for (callee, expected) in [ + ( + "callback", + "direct call argument type does not match its parameter", + ), + ("factory()", "indirect call argument type mismatch"), + ] { + for parameter in ["int64", "Positive"] { + let source = format!( + "type Positive = int64 where value > 0\n\ + function callback(value: {parameter}) returns {parameter}:\n return value\n\ + function factory() returns function({parameter}) returns {parameter}:\n return callback\n\ + function caller(value: secret[{parameter}]) returns secret[{parameter}]:\n return {callee}(value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let payload = if parameter == "Positive" { + TypeInterner::INT64 + } else { + TypeInterner::INT32 + }; + let hidden = types.intern(jett_types::Type::Secret(payload)); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + let expression = secret_gate_returned_call(caller); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } + | ExpressionKind::IndirectCall { args, .. } => args, + _ => unreachable!(), + }; + args[0].ty = hidden; + args[0].kind = ExpressionKind::Int(7); + assert_checked_native_call_gate(&program, &types, expected); + } + } + } + + #[test] + fn native_secret_call_impurity_gate_remains_independent_of_source_certificates() { + for (callee, expected) in [ + ( + "callback", + "direct call argument type does not match its parameter", + ), + ("factory()", "indirect call argument type mismatch"), + ] { + let source = format!( + "function callback(view stdout: Stdout, value: int64) returns int64:\n return value\n\ + function factory() returns function(view Stdout, int64) returns int64:\n return callback\n\ + function caller(view stdout: Stdout, value: int64) returns int64:\n return {callee}(view stdout, value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let hidden = types.intern(jett_types::Type::Secret(TypeInterner::INT64)); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + let expression = secret_gate_returned_call(caller); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::IndirectCall { args, .. } => { + args + } + _ => unreachable!(), + }; + args[1].ty = hidden; + args[1].kind = ExpressionKind::Int(7); + assert_checked_native_call_gate(&program, &types, expected); + } + } + + #[test] + fn native_reflected_metadata_expression_gates_preserve_arity_and_exact_types() { + let source = "enum Event:\n empty = 17\n content(text: string, values: list[optional[int64]])\n\ + machine Session:\n states:\n empty\n content(text: string, values: list[optional[int64]])\n transitions:\n empty to content\n\ + function variant_pipe(view source: Event) returns TypeVariant:\n TypeVariant selected = source into view type.variant_value[Event]()\n return selected\n\ + function state_pipe(view source: Session) returns TypeMachineState:\n TypeMachineState selected = source into view type.machine_state_value[Session]()\n return selected\n\ + function narrowed_pipe(view source: Session at content) returns TypeMachineState:\n TypeMachineState selected = source into view type.machine_state_value[Session at content]()\n return selected\n\ + function map_pipe(index: int64) returns TypeInfo:\n TypeInfo selected = index into type.arg[map[string, list[optional[int64]]]]()\n return selected\n"; + for (name, expected) in [ + ( + "variant_pipe", + "invalid checked type.variant_value operands", + ), + ( + "state_pipe", + "invalid checked type.machine_state_value operands", + ), + ( + "narrowed_pipe", + "invalid checked type.machine_state_value operands", + ), + ("map_pipe", "invalid checked type.arg operands"), + ] { + for remove in [true, false] { + let (program, types) = lower_source(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("source reflected observer"); + let mut expression = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::Intrinsic { .. }) => + { + Some(value.clone()) + } + _ => None, + }) + .expect("exact checked observer initializer"); + let ExpressionKind::Intrinsic { + args, + evaluation_order, + .. + } = &mut expression.kind + else { + unreachable!() + }; + assert!(args.len() > 1); + if remove { + args.pop(); + evaluation_order.pop(); + } else { + let metadata = args.last_mut().unwrap(); + metadata.kind = ExpressionKind::Bool(false); + metadata.ty = TypeInterner::BOOL; + } + assert!( + matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, function, &expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + ), + "{name}/remove={remove}: native metadata expression gate remains exact" + ); + } + } + } + + #[test] + fn custom_assertion_message_in_verify_body_emits_native_entry() { + let file = FileId::new(0); + let parsed = jett_parser::parse( + "namespace test\nverify failure:\n assert false \"custom failure\"\n", + file, + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let hir = jett_hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("test-body HIR lowering"); + let program = jett_mir::lower(&hir, &checked.interner).expect("test-body MIR lowering"); + let entry = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == jett_hir::DeclarationKind::Verify + }) + .expect("verify body") + .id; + let object = emit_host_program_object(&program, &checked.interner, entry) + .expect("custom assertion message must emit native code"); + assert!(!object.bytes.is_empty()); + } + + fn test_object_module() -> ObjectModule { + let flags = settings::Flags::new(settings::builder()); + let isa = isa::lookup(HOST) + .expect("host ISA builder") + .finish(flags) + .expect("host ISA"); + let object_builder = + ObjectBuilder::new(isa, b"jett-test".to_vec(), default_libcall_names()) + .expect("object builder"); + ObjectModule::new(object_builder) + } + + fn declared_program(source: &str) -> (Program, TypeInterner, ObjectModule, DeclaredFunctions) { + let (program, types) = lower_source(source); + let verified = verify_program(&program, &types).expect("supported scalar MIR"); + let mut module = test_object_module(); + let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) + .expect("function declarations"); + (program, types, module, declarations) + } + + fn translated_clif(source: &str) -> HashMap { + let (program, types, mut module, declarations) = declared_program(source); + + let mut translated = HashMap::new(); + for declaration in declarations.iter() { + let function = program_function(&program, declaration.mir_id).expect("MIR function"); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + function, + &types, + &declaration.symbol, + &mut context, + ) + .expect("function translation"); + translated.insert( + function.identity.declaration.name.clone(), + context.func.display().to_string(), + ); + } + translated + } + + fn translated_function(source: &str, function_name: &str) -> ir::Function { + let (program, types, mut module, declarations) = declared_program(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == function_name) + .expect("requested MIR function"); + let declaration = declarations + .get(function.id) + .expect("requested native declaration"); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + function, + &types, + &declaration.symbol, + &mut context, + ) + .expect("function translation"); + context.func + } + + fn direct_call_arguments(function: &ir::Function) -> Vec> { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| function.dfg.insts[*instruction].opcode() == ir::Opcode::Call) + .map(|instruction| function.dfg.inst_args(instruction).to_vec()) + .collect() + } + + #[test] + fn evaluates_arguments_in_lexical_order_and_returns_parameter_order() { + let arguments = ["first parameter", "second parameter"]; + let mut evaluated = Vec::new(); + let values = reordered_map( + &arguments, + &[1, 0], + |argument| { + evaluated.push(*argument); + Ok::<_, ()>(*argument) + }, + || (), + ) + .expect("valid permutation"); + + assert_eq!(evaluated, ["second parameter", "first parameter"]); + assert_eq!(values, ["first parameter", "second parameter"]); + } + + #[test] + fn signed_minimum_immediates_use_width_correct_bit_patterns() { + assert_eq!( + integer_immediate(-128, ScalarKind::SignedInteger(8)), + Some(128) + ); + assert_eq!( + integer_immediate(-32_768, ScalarKind::SignedInteger(16)), + Some(32_768) + ); + assert_eq!( + integer_immediate(-2_147_483_648, ScalarKind::SignedInteger(32)), + Some(2_147_483_648) + ); + assert_eq!( + integer_immediate(i128::from(i64::MIN), ScalarKind::SignedInteger(64)), + Some(i64::MIN) + ); + } + + #[test] + fn emitted_jett_signatures_prepend_runtime_context_and_environment() { + let (program, types, module, declarations) = declared_program( + r#"namespace app +function leaf(value: int64, enabled: bool) returns int64: + if enabled: + return value + return 0 +function root() returns int64: + return leaf(7, true) +"#, + ); + let pointer_type = module.target_config().pointer_type(); + + for declaration in declarations.iter() { + let function = program_function(&program, declaration.mir_id).expect("MIR function"); + let pending_parameters = function + .params + .iter() + .skip(function.capture_count) + .filter(|parameter| is_task_scalar(&types, parameter.ty).unwrap()) + .count(); + assert_eq!( + declaration.signature.params.len(), + function.params.len() - function.capture_count + pending_parameters + 2, + "{} must gain context, environment, and primitive depth parameters", + function.identity.declaration.name + ); + assert_eq!( + declaration.signature.params[0], + AbiParam::new(pointer_type), + "{} must receive an ordinary runtime-context pointer first", + function.identity.declaration.name + ); + assert_eq!( + declaration.signature.params[1], + AbiParam::new(ir::types::I64) + ); + } + + let leaf = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "leaf") + .expect("leaf MIR function"); + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("root MIR function"); + assert_eq!(leaf.params.len(), 2, "source parameters stay unchanged"); + assert!(root.params.is_empty(), "source parameters stay unchanged"); + } + + #[test] + fn direct_jett_calls_forward_the_exact_current_runtime_context() { + let function = translated_function( + r#"namespace app +function leaf(value: int64) returns int64: + return value +function caller(value: int64, choose_original: bool) returns int64: + if choose_original: + return leaf(value) + return leaf(value + 1) +"#, + "caller", + ); + let entry = function.layout.entry_block().expect("native entry block"); + let runtime_context = function.dfg.block_params(entry)[0]; + let calls = direct_call_arguments(&function); + + assert!( + calls.len() >= 4, + "Jett calls and failure checks remain present" + ); + for arguments in calls { + assert!( + matches!(arguments.len(), 1 | 4 | 8), + "status and scalar checks receive context; Jett calls also receive environment, scalar bits, and depth" + ); + assert_eq!( + function.dfg.resolve_aliases(arguments[0]), + runtime_context, + "direct calls must forward the caller's exact context value" + ); + } + } + + #[test] + fn unreachable_direct_call_blocks_are_not_emitted_with_an_undefined_context() { + let (mut program, types) = lower_source( + r#"namespace app +function leaf(value: int64) returns int64: + return value +function caller(value: int64, choose_original: bool) returns int64: + if choose_original: + return leaf(value) + return leaf(value + 1) +"#, + ); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .expect("caller MIR function"); + let mut immediate_return = caller + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(expression)) + if matches!(expression.kind, ExpressionKind::Call { .. }) => + { + Some(expression.clone()) + } + _ => None, + }) + .expect("branch-local direct call"); + immediate_return.kind = ExpressionKind::Int(0); + let entry_index = + usize::try_from(caller.entry.index()).expect("entry index fits this host"); + caller.blocks[entry_index].terminator.kind = TerminatorKind::Return(Some(immediate_return)); + + let control_flow = ControlFlowGraph::analyze(caller).expect("valid mutated control flow"); + assert_eq!(control_flow.reverse_postorder(), [caller.entry]); + assert!( + caller.blocks.iter().any(|block| { + matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: ExpressionKind::Call { .. }, + .. + })) + ) + }), + "the test must retain a direct call in a disconnected MIR block" + ); + + let verified = verify_program(&program, &types).expect("valid mutated scalar MIR"); + let mut module = test_object_module(); + let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) + .expect("function declarations"); + let caller = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "caller") + .expect("caller MIR function"); + let declaration = declarations.get(caller.id).expect("caller declaration"); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + caller, + &types, + &declaration.symbol, + &mut context, + ) + .expect("reachable-only function translation"); + + assert_eq!(context.func.layout.blocks().count(), 2); // plus terminal-failure epilogue + assert!(direct_call_arguments(&context.func).is_empty()); + } + + #[test] + fn exported_entry_wrapper_forwards_its_incoming_runtime_context() { + let (program, _types, mut module, declarations) = declared_program( + r#"namespace app +function selected_entry() returns nothing: + return nothing +"#, + ); + let entry = program.functions[0].id; + let entry_function = declarations.get(entry).expect("native entry declaration"); + let mut context = module.make_context(); + context.func.signature = program_entry_signature(&module); + translate_program_entry_wrapper(&mut module, entry_function, entry, &mut context) + .expect("entry wrapper translation"); + + let block = context + .func + .layout + .entry_block() + .expect("wrapper entry block"); + let incoming_context = context.func.dfg.block_params(block)[0]; + let calls = direct_call_arguments(&context.func); + assert_eq!(calls.len(), 2); // entry and failure status + assert_eq!(calls[0].len(), 2); + assert_eq!(calls[1], vec![incoming_context]); + assert_eq!(calls[0][0], incoming_context); + assert_eq!( + context.func.signature.params, + [AbiParam::new(module.target_config().pointer_type())] + ); + assert!(program.functions[0].params.is_empty()); + } + + #[test] + fn boolean_and_or_branch_before_emitting_rhs_calls() { + let functions = translated_clif( + r#"namespace app +function rhs() returns bool: + return true +function and_value(left: bool) returns bool: + return left && rhs() +function or_value(left: bool) returns bool: + return left || rhs() +"#, + ); + + for name in ["and_value", "or_value"] { + let clif = functions.get(name).expect("translated test function"); + let branch = clif.find("brif ").expect("short-circuit branch"); + let call = clif.find(" = call ").expect("RHS call"); + assert!( + branch < call, + "{name} eagerly emits its RHS before branching:\n{clif}" + ); + } + } + + #[test] + fn native_ownership_rejects_read_before_initialization() { + let (mut program, types) = lower_source( + r#" +function selected_entry() returns string: + string value = "live" + return value +"#, + ); + program.functions[0].blocks[0].statements.clear(); + let error = + emit_host_object(&program, &types).expect_err("missing initializer is invalid MIR"); + assert!( + error.to_string().contains("definite initialization"), + "{error}" + ); + } + + const CALL_VIEW_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "member" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return Named.name(view item) +function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + string answer = report(view packet.member, incoming handle: + return packet + ) + return packet +"#; + + #[test] + fn native_call_views_support_post_call_and_aborted_call_owner_returns() { + let prefix = CALL_VIEW_SOURCE.split("function exercise(").next().unwrap(); + let mutable = format!( + r#"{prefix}function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + mutable Packet current = packet + string answer = report(view current.member, incoming handle: + default 3 + ) + current = Packet(member: 9) + return current +"# + ); + let temporary = format!( + r#"{prefix}function make() returns Packet: + return Packet(member: 7) +function exercise(incoming: optional[int64]) returns string: + return report(view make().member, incoming handle: + default 3 + ) +"# + ); + let direct_mutable = format!( + r#"{prefix}function exercise(incoming: optional[int64]) returns Named: + mutable Named current = 7 + string answer = report(view current, incoming handle: + default 3 + ) + current = 9 + return current +"# + ); + let direct_temporary = format!( + r#"{prefix}function make_named() returns Named: + return 7 +function exercise(incoming: optional[int64]) returns string: + return report(view make_named(), incoming handle: + default 3 + ) +"# + ); + for source in [ + CALL_VIEW_SOURCE.to_owned(), + mutable, + temporary, + direct_mutable, + direct_temporary, + ] { + let (program, types) = lower_source(&source); + let exercise = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + assert_eq!( + exercise + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!( + statement.kind, + StatementKind::BeginCallView { .. } + )) + .count(), + 1 + ); + let object = emit_host_object(&program, &types).expect("exact borrowed field supports unchanged owner, post-call rebinding, owned temporary and aborted call cleanup"); + assert!(!object.bytes.is_empty()); + } + } + + #[test] + fn native_call_view_scope_metadata_is_checked_before_dead_code_removal() { + for fault in ["no begin", "no end", "orphan end", "unused field metadata"] { + let (mut program, types) = lower_source(CALL_VIEW_SOURCE); + let exercise = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + let stage = exercise + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + StatementKind::BeginCallView { local, .. } => Some(local), + _ => None, + }) + .unwrap(); + match fault { + "no begin" | "no end" => { + for block in &mut exercise.blocks { + block.statements.retain(|statement| { + if fault == "no begin" { + !matches!(statement.kind, StatementKind::BeginCallView { .. }) + } else { + !matches!(statement.kind, StatementKind::EndCallView { .. }) + } + }); + } + } + "orphan end" => { + exercise.blocks[exercise.entry.index() as usize] + .statements + .push(Statement { + kind: StatementKind::EndCallView { + local: exercise.params[0].local, + }, + span: exercise.span, + }); + } + _ => { + let value = exercise + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::BeginCallView { local, value } if *local == stage => { + Some(value) + } + _ => None, + }) + .unwrap(); + let ExpressionKind::View(inner) = &mut value.kind else { + panic!("exact projected view") + }; + let ExpressionKind::Field { owner_type, .. } = &mut inner.kind else { + panic!("field metadata") + }; + *owner_type = TypeInterner::STRING; + exercise.blocks[exercise.entry.index() as usize] + .terminator + .kind = TerminatorKind::Unreachable; + } + } + let error = emit_host_object(&program, &types) + .expect_err("malformed internal stage cannot be pruned into validity"); + assert!( + matches!(error, CodegenError::InvalidMirContract { .. }), + "{fault}: {error}" + ); + } + } + + #[test] + fn native_projected_aliases_preserve_typed_path_proof_before_compaction() { + let source = r#"struct Packet: + items: list[int64] +struct Envelope: + packet: Packet +function selected_entry(view source: Envelope) returns list[int64]: + list[int64] borrowed = view source.packet.items + list[int64] forwarded = borrowed + for element in list(): + trace forwarded + return clone forwarded +"#; + let (program, types) = lower_source(source); + emit_host_object(&program, &types) + .expect("immutable struct projection has a native object"); + for invalid in ["orphan", "unreachable path"] { + let (mut program, types) = lower_source(source); + let function = &mut program.functions[0]; + let borrowed = function + .locals + .iter() + .find(|local| local.name == "borrowed") + .unwrap() + .id; + if invalid == "orphan" { + for block in &mut function.blocks { + block.statements.retain(|statement| !matches!(statement.kind, StatementKind::Let { local, .. } if local == borrowed)); + } + } else { + let mut value = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == borrowed => { + Some(value.clone()) + } + _ => None, + }) + .unwrap(); + let ExpressionKind::View(field) = &mut value.kind else { + panic!("explicit projected view"); + }; + let ExpressionKind::Field { owner_type, .. } = &mut field.kind else { + panic!("nested projected field"); + }; + *owner_type = TypeInterner::STRING; + let span = value.span; + let body = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + TerminatorKind::ForEach { body, .. } => Some(body), + _ => None, + }) + .unwrap(); + // The empty-list body is removed by native preparation. Its + // forged typed path must fail before that removal occurs. + function.blocks[body.index() as usize] + .statements + .push(jett_mir::Statement { + kind: StatementKind::Let { + local: borrowed, + value, + }, + span, + }); + } + jett_mir::validate(&program).expect("forged path uses valid structural IDs"); + let error = emit_host_object(&program, &types) + .expect_err("unproven projected alias must not be emitted"); + assert!( + error.to_string().contains("borrowed alias") + || error.to_string().contains("borrowed projection"), + "{invalid}: {error}" + ); + } + } + + #[test] + fn actor_handler_state_is_initialized_for_native_ownership_analysis() { + let (program, types) = lower_source( + r#"namespace test +actor ByteCounter(seed: uint8): + mutable uint8 count = seed + 1 + receive add(delta: uint8): + count = count + delta + receive current responds uint8: + respond count + 1 +"#, + ); + for handler in program.functions.iter().filter(|function| { + function.identity.declaration.kind == jett_hir::DeclarationKind::ActorHandler + }) { + assert_eq!(handler.capture_count, 2); + assert_eq!(handler.params[0].name, "seed"); + assert_eq!(handler.params[1].name, "count"); + MoveValuePlan::analyze(&program, handler, &types) + .expect("actor input state has a valid native ownership plan"); + } + } + + #[test] + fn native_ownership_plan_keeps_loop_carried_strings_live() { + let (program, types) = lower_source( + r#" +function selected_entry(keep: bool) returns string: + mutable string value = "live" + while keep: + value = "next" + return value +"#, + ); + let function = &program.functions[0]; + let plan = CopyValuePlan::analyze(function, &types).unwrap(); + let local = function + .locals + .iter() + .find(|l| l.name == "value") + .unwrap() + .id + .index() as usize; + assert!(plan.owned_locals.contains(&local)); + for block in &function.blocks { + if matches!(block.terminator.kind, TerminatorKind::Branch { .. }) { + assert!(plan.live_in[block.id.index() as usize].contains(&local)); + assert!(plan.live_out[block.id.index() as usize].contains(&local)); + } + } + } + + #[test] + fn copy_value_plan_does_not_claim_move_only_ownership() { + let (program, types) = lower_source( + r#" +function discard(view value: bytes) returns nothing: + return nothing +"#, + ); + let error = CopyValuePlan::analyze(&program.functions[0], &types) + .expect_err("bytes needs move/borrow/drop analysis"); + assert!(error.contains("move/borrow/drop"), "{error}"); + } + #[test] + fn native_linear_plan_rejects_move_across_loop_backedge() { + let (program, types) = lower_source( + r#" +function consume(value: bytes) returns nothing: + return nothing +function loop_move(value: bytes, repeat: bool) returns nothing: + while repeat: + consume(value) +"#, + ); + let error = emit_host_object(&program, &types) + .expect_err("loop reads moved place on next iteration"); + assert!( + error.to_string().contains("moved or uninitialized"), + "{error}" + ); + } + #[test] + fn native_linear_plan_rejects_borrow_escape_and_argument_alias() { + let (mut program, types) = lower_source( + r#" +function identity(value: bytes) returns bytes: + return value +"#, + ); + program.functions[0].params[0].mode = jett_mir::ParamMode::View; + let error = + emit_host_object(&program, &types).expect_err("view cannot be returned as owner"); + assert!( + error.to_string().contains("cannot move borrowed"), + "{error}" + ); + let (program, types) = lower_source( + r#" +function mixed(view first: bytes, second: bytes) returns nothing: + return nothing +function alias(value: bytes) returns nothing: + mixed(view value, value) +"#, + ); + let error = emit_host_object(&program, &types) + .expect_err("loan must survive all argument evaluation"); + assert!(error.to_string().contains("while borrowed"), "{error}"); + } + #[test] + fn iteration_loan_survives_nested_loop_and_rejects_owner_escape() { + let (program, types) = lower_source( + r#" +function escaped(items: list[int64]) returns list[int64]: + for item in view items: + for other in view items: + break + return items + return items +"#, + ); + let error = + emit_host_object(&program, &types).expect_err("outer iteration view remains active"); + assert!(error.to_string().contains("while borrowed"), "{error}"); + } + #[test] + fn consuming_sequence_cannot_take_from_a_borrowed_parameter() { + let (mut program, types) = lower_source( + r#" +function visit(view items: list[int64]) returns nothing: + for item in view items: + break +"#, + ); + jett_mir::prepare_native_sequences(&mut program, &types); + emit_host_object(&program, &types) + .expect("checked borrowed iteration is valid before corruption"); + for block in &mut program.functions[0].blocks { + for statement in &mut block.statements { + if let StatementKind::SequenceGet { consume, .. } = &mut statement.kind { + *consume = true; + } + } + } + let error = emit_host_object(&program, &types).expect_err("cannot take a borrowed element"); + let CodegenError::InvalidMir(errors) = error else { + panic!("prepared borrowed extraction must fail before emission: {error:?}"); + }; + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!( + errors[0].message, + "call ownership viewed extraction changed source/cursor/target/role or consumption" + ); + } + #[test] + fn sequence_preheader_is_selected_by_cfg_not_block_position() { + let (mut program, types) = lower_source( + r#" +function visit(items: list[int64]) returns int64: + mutable int64 total = 0 + for item in items: + total = total + item + return total +"#, + ); + let function = &mut program.functions[0]; + let old_entry = function.entry; + let last = function.blocks.last().unwrap().id; + let remap = |id| { + if id == old_entry { + last + } else if id == last { + old_entry + } else { + id + } + }; + function + .blocks + .swap(old_entry.index() as usize, last.index() as usize); + function.entry = remap(old_entry); + for block in &mut function.blocks { + block.id = remap(block.id); + match &mut block.terminator.kind { + TerminatorKind::Goto(target) => *target = remap(*target), + TerminatorKind::ForEach { body, exit, .. } => { + *body = remap(*body); + *exit = remap(*exit); + } + TerminatorKind::Return(_) | TerminatorKind::Unreachable => {} + other => panic!("unexpected fixture terminator {other:?}"), + } + } + emit_host_object(&program, &types).expect("late-numbered unique loop preheader"); + } + + const CALL_OWNER_GENERATION_SOURCE: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + return extra +function maybe(flag: bool) returns optional[int64]: + if flag: + return some(11) + return none +function exercise(flag: bool) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, maybe(flag) handle: + switchable = type.construct_start[Alternate]() + switchable = type.construct_start[User]() + default 7 + ) + return selected +"#; + + fn emit_call_owner_generation(source: &str) -> (Program, TypeInterner) { + let (program, types) = lower_source(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .expect("actual source exercise function"); + let ownership = MoveValuePlan::analyze(&program, function, &types) + .expect("fresh generation ownership proof"); + let slots = ownership.generation_storage().slots(); + assert_eq!(slots.len(), 1, "one source captured generation"); + let root = function.local(slots[0].root()).expect("generation root"); + assert!(matches!(types.resolve(root.ty), Type::TypeConstruction)); + assert_eq!(slots[0].ty(), root.ty); + assert!(root.mutable && root.view_source.is_none()); + let verified = verify_program(&program, &types).expect("full typed generation program"); + let mut module = test_object_module(); + let declarations = declare_reachable_functions(&mut module, &program, &types, &verified) + .expect("native declarations"); + let declaration = declarations.get(function.id).expect("exercise declaration"); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &program, + function, + &types, + &declaration.symbol, + &mut context, + ) + .expect("generation native translation"); + // This function handles a produced sum, so there is no independent + // stable-sum snapshot to obscure a call-capture clone regression. + assert!( + module.get_name(NativeLeaf::StructClone.symbol()).is_none(), + "generation capture must stack-move its owner without cloning" + ); + let object = emit_host_object(&program, &types).expect("generation object"); + assert!(!object.bytes.is_empty()); + (program, types) + } + + #[test] + fn native_call_owner_generation_emits_repeated_replacement_without_capture_clone() { + let (program, _types) = emit_call_owner_generation(CALL_OWNER_GENERATION_SOURCE); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + let replacements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!( + statement.kind, + StatementKind::ReplaceCallOwnerGeneration { .. } + ) + }) + .count(); + assert_eq!( + replacements, 2, + "both independent replacements share one escrow" + ); + } + + #[test] + fn native_call_owner_generation_emits_conditional_and_early_abort_cleanup() { + let conditional = CALL_OWNER_GENERATION_SOURCE + .replace(" switchable = type.construct_start[User]()\n", ""); + emit_call_owner_generation(&conditional); + let abort = conditional.replace(" default 7\n", " return 13\n"); + emit_call_owner_generation(&abort); + } + + #[test] + fn native_call_owner_generation_rejects_changed_storage_protocol_before_emission() { + let (program, types) = emit_call_owner_generation(CALL_OWNER_GENERATION_SOURCE); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "exercise") + .unwrap(); + for corruption in 0..4 { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let mut applied = false; + for block in &mut function.blocks { + if applied { + break; + } + let position = block + .statements + .iter() + .position(|statement| match corruption { + 0 => matches!( + statement.kind, + StatementKind::OpenCallOwnerGeneration { .. } + ), + 1 => matches!( + statement.kind, + StatementKind::ReplaceCallOwnerGeneration { .. } + ), + 2 | 3 => matches!( + statement.kind, + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + ), + _ => unreachable!(), + }); + let Some(position) = position else { + continue; + }; + match corruption { + 0 | 2 => { + block.statements.remove(position); + } + 1 => { + let StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } = &mut block.statements[position].kind + else { + unreachable!(); + }; + *rhs_owner = *root; + } + 3 => { + let duplicate = block.statements[position].clone(); + block.statements.insert(position + 1, duplicate); + } + _ => unreachable!(), + } + applied = true; + } + assert!( + applied, + "corruption {corruption} found its canonical operation" + ); + crate::verify::verify_descriptor_bodies(&changed, &types) + .expect_err("original metadata gate must retain generation integrity"); + emit_host_object(&changed, &types) + .expect_err("changed generation storage protocol must fail before emission"); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/actor_graph.rs b/crates/jett_codegen_cranelift/src/emit/actor_graph.rs new file mode 100644 index 00000000..6114a5a1 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/actor_graph.rs @@ -0,0 +1,918 @@ +//! Execute a sealed required actor graph without Source or predicate replay. +use super::*; +use debug::{DebugGraph, DebugNode}; +use jett_comptime::Value as FrozenValue; +use jett_hir::evaluated_values::{function_value_candidates, selected_function}; +use jett_hir::{ActorGraphInstruction, ActorGraphOperation}; +use std::collections::HashMap; + +enum Supplement { + Hidden(u32), + Function(FunctionId, Option), + Interface(TypeId, u32), + Builder(String, u32, Vec), + Resource, + List(Vec), + Map(Vec<(u32, u32)>), + CheckedHook(u32, u32, u32), +} +struct ImportGraph<'a> { + debug: DebugGraph<'a>, + instruction: &'a ActorGraphInstruction, + supplements: Vec<(u32, Supplement)>, + hooks: HashMap, +} +impl ImportGraph<'_> { + fn add(&mut self, node: DebugNode) -> Result { + let index = u32::try_from(self.debug.nodes.len()) + .map_err(|_| "native actor import schema exhausted")?; + self.debug.nodes.push(Some(node)); + Ok(index) + } + fn typed(&mut self, ty: TypeId) -> Result { + if let Some(index) = self.debug.indexes.get(&ty) { + return Ok(*index); + } + let index = + u32::try_from(self.debug.nodes.len()).map_err(|_| "native import schema exhausted")?; + self.debug.indexes.insert(ty, index); + self.debug.nodes.push(None); + let node = if let Some(kind) = crate::values::list_sort_kind(self.debug.types, ty) { + DebugNode::Primitive(kind as u32) + } else { + match self.debug.types.resolve(ty).clone() { + Type::Nothing => DebugNode::Nothing, + Type::Bytes => DebugNode::Bytes, + Type::List(c) => DebugNode::List(self.typed(c)?), + Type::Set(c) => DebugNode::Set(self.typed(c)?), + Type::Map(a, b) => DebugNode::Map(self.typed(a)?, self.typed(b)?), + Type::Optional(c) => DebugNode::Optional(self.typed(c)?), + Type::Result(a, b) => DebugNode::Result(self.typed(a)?, self.typed(b)?), + Type::Struct(id) => { + let definition = self.debug.types.resolve_struct(id).clone(); + let fields = definition + .fields + .into_iter() + .map(|(name, ty)| Ok((name, self.typed(ty)?))) + .collect::>()?; + DebugNode::Record(definition.name, fields) + } + Type::Bitfield(id) => { + let definition = self.debug.types.resolve_bitfield(id).clone(); + let fields = definition + .fields + .into_iter() + .map(|field| Ok((field.name, self.typed(field.ty)?))) + .collect::>()?; + DebugNode::Record(definition.name, fields) + } + Type::Enum(id) => { + let definition = self.debug.types.resolve_enum(id).clone(); + let variants = definition + .variants + .into_iter() + .map(|variant| { + Ok(( + variant.name, + variant + .fields + .into_iter() + .map(|(_, ty)| self.typed(ty)) + .collect::>()?, + )) + }) + .collect::>()?; + DebugNode::Enum(definition.name, variants) + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + let definition = self.debug.types.resolve_machine(id).clone(); + let states = definition + .states + .into_iter() + .map(|state| { + Ok(( + state.name, + state + .fields + .into_iter() + .map(|(_, ty)| self.typed(ty)) + .collect::>()?, + )) + }) + .collect::>()?; + DebugNode::Machine(definition.name, states) + } + Type::Refinement { base, .. } => DebugNode::Alias(self.typed(base)?), + Type::Secret(base) => { + let child = self.typed(base)?; + self.supplements.push((index, Supplement::Hidden(child))); + DebugNode::Redacted + } + Type::Resource(_) => { + self.supplements.push((index, Supplement::Resource)); + DebugNode::Redacted + } + Type::Capability(_) => DebugNode::Capability(self.debug.types.type_name(ty)), + Type::Actor(_) => DebugNode::Actor, + Type::Function { .. } => DebugNode::Function, + Type::Interface(_) => DebugNode::Interface, + Type::TypeConstruction => DebugNode::TypeConstruction, + Type::Never => DebugNode::Uninhabited, + _ => return Err("native graph type lacks its checked value carrier layout".into()), + } + }; + self.debug.nodes[index as usize] = Some(node); + Ok(index) + } + fn value(&mut self, ty: TypeId, mut value: &FrozenValue) -> Result { + // Pending depth belongs to the runtime carrier. It never changes the + // checked value/owner schema or imports a saved native wrapper ID. + while let FrozenValue::Pending(inner) = value { + value = inner; + } + let types = self.debug.types; + if let Type::Secret(inner) = types.resolve(ty) { + let child = self.value(*inner, value)?; + let node = self.add(DebugNode::Redacted)?; + self.supplements.push((node, Supplement::Hidden(child))); + return Ok(node); + } + if let Type::Refinement { base, .. } = types.resolve(ty) { + if let FrozenValue::Typed { + type_name, + value: inner, + } = value + { + if *type_name != types.type_name(ty) { + return Err("frozen refinement lost its exact nominal owner".into()); + } + value = inner; + } + let child = self.value(*base, value)?; + return self.add(DebugNode::Alias(child)); + } + if let Type::Interface(_) = types.resolve(ty) { + let concrete = self.concrete(value)?; + let child = self.value(concrete, value)?; + let node = self.add(DebugNode::Interface)?; + self.supplements + .push((node, Supplement::Interface(concrete, child))); + return Ok(node); + } + if let FrozenValue::Typed { value: inner, .. } = value { + return self.value(ty, inner); + } + match (types.resolve(ty), value) { + (Type::Function { .. }, FrozenValue::ResourceHook(_)) => self.function(ty, value), + (Type::Function { .. }, FrozenValue::ReflectedCallable(_)) => self.function(ty, value), + ( + Type::Function { .. }, + FrozenValue::Function { .. } | FrozenValue::NamedFunction(_), + ) => self.function(ty, value), + (Type::List(inner), FrozenValue::List(values)) => { + let default = self.typed(*inner)?; + let mut children = Vec::new(); + for value in values { + children.push(self.value(*inner, value)?); + } + let node = self.add(DebugNode::List(default))?; + self.supplements.push((node, Supplement::List(children))); + Ok(node) + } + (Type::Map(key, mapped), FrozenValue::Map(values)) => { + let default_key = self.typed(*key)?; + let default_value = self.typed(*mapped)?; + let mut children = Vec::new(); + for (left, right) in values { + children.push((self.value(*key, left)?, self.value(*mapped, right)?)); + } + let node = self.add(DebugNode::Map(default_key, default_value))?; + self.supplements.push((node, Supplement::Map(children))); + Ok(node) + } + (Type::Set(inner), FrozenValue::Set(_)) => { + let child = self.typed(*inner)?; + self.add(DebugNode::Set(child)) + } + (Type::Optional(inner), FrozenValue::OptionalSome(value)) => { + let child = self.value(*inner, value)?; + self.add(DebugNode::Optional(child)) + } + (Type::Optional(inner), FrozenValue::OptionalNone) => { + let child = self.typed(*inner)?; + self.add(DebugNode::Optional(child)) + } + (Type::Result(ok, error), FrozenValue::ResultOk(value)) => { + let a = self.value(*ok, value)?; + let b = self.typed(*error)?; + self.add(DebugNode::Result(a, b)) + } + (Type::Result(ok, error), FrozenValue::ResultFail(value)) => { + let a = self.typed(*ok)?; + let b = self.value(*error, value)?; + self.add(DebugNode::Result(a, b)) + } + (Type::Struct(id), FrozenValue::Struct { fields, .. }) => { + let definition = types.resolve_struct(*id); + let mut children = Vec::new(); + for (name, ty) in &definition.fields { + let value = fields + .iter() + .find(|(n, _)| n == name) + .ok_or("frozen record field missing")?; + children.push((name.clone(), self.value(*ty, &value.1)?)); + } + self.add(DebugNode::Record(definition.name.clone(), children)) + } + (Type::Bitfield(id), FrozenValue::Struct { fields, .. }) => { + let definition = types.resolve_bitfield(*id); + let mut children = Vec::new(); + for field in &definition.fields { + let value = fields + .iter() + .find(|(n, _)| n == &field.name) + .ok_or("frozen bitfield field missing")?; + children.push((field.name.clone(), self.value(field.ty, &value.1)?)); + } + self.add(DebugNode::Record(definition.name.clone(), children)) + } + ( + Type::Enum(id), + FrozenValue::Enum { + variant, fields, .. + }, + ) => { + let definition = types.resolve_enum(*id); + let mut variants = Vec::new(); + for candidate in &definition.variants { + let mut children = Vec::new(); + for (index, (_, ty)) in candidate.fields.iter().enumerate() { + children.push(if &candidate.name == variant { + self.value( + *ty, + fields.get(index).ok_or("frozen enum payload missing")?, + )? + } else { + self.typed(*ty)? + }); + } + variants.push((candidate.name.clone(), children)); + } + self.add(DebugNode::Enum(definition.name.clone(), variants)) + } + ( + Type::Machine(id) | Type::MachineState { machine: id, .. }, + FrozenValue::Machine { state, fields, .. }, + ) => { + let definition = types.resolve_machine(*id); + let mut states = Vec::new(); + for candidate in &definition.states { + let mut children = Vec::new(); + for (index, (_, ty)) in candidate.fields.iter().enumerate() { + children.push(if &candidate.name == state { + self.value( + *ty, + fields.get(index).ok_or("frozen machine payload missing")?, + )? + } else { + self.typed(*ty)? + }); + } + states.push((candidate.name.clone(), children)); + } + self.add(DebugNode::Machine(definition.name.clone(), states)) + } + (Type::TypeConstruction, FrozenValue::TypeConstruction { .. }) => self.builder(value), + (Type::Actor(_), FrozenValue::Actor(seed)) => { + if self.instruction.actor_seed_type(*seed) != Some(ty) { + return Err("native graph field actor has foreign nominal seed".into()); + } + self.add(DebugNode::Actor) + } + (Type::Capability(_) | Type::Resource(_), _) => { + Err("native graph import cannot clone runtime authority or Resource custody".into()) + } + _ => self.typed(ty), + } + } + fn concrete(&self, value: &FrozenValue) -> Result { + let types = self.debug.types; + if let FrozenValue::Typed { type_name, .. } = value { + return checked_type(types, type_name); + } + Ok(match value { + FrozenValue::Actor(seed) => self + .instruction + .actor_seed_type(*seed) + .ok_or("frozen interface actor lost its exact seed type")?, + FrozenValue::Int64(_) => TypeInterner::INT64, + FrozenValue::Uint64(_) => TypeInterner::UINT64, + FrozenValue::Float64(_) => TypeInterner::FLOAT64, + FrozenValue::Bool(_) => TypeInterner::BOOL, + FrozenValue::String(_) => TypeInterner::STRING, + FrozenValue::Bytes(_) => TypeInterner::BYTES, + FrozenValue::Nothing => TypeInterner::NOTHING, + FrozenValue::TypeConstruction { .. } => TypeInterner::TYPE_CONSTRUCTION, + FrozenValue::Struct { + type_name, + concrete_type, + .. + } => checked_type(types, concrete_type.as_deref().unwrap_or(type_name))?, + FrozenValue::Enum { type_name, .. } | FrozenValue::Machine { type_name, .. } => { + checked_type(types, type_name)? + } + _ => return Err("frozen erased graph field has no exact concrete identity".into()), + }) + } + fn function(&mut self, ty: TypeId, value: &FrozenValue) -> Result { + if matches!(value, FrozenValue::ResourceHook(_)) { + let (operation, hook, signature) = self + .hooks + .get(&std::ptr::from_ref(value).addr()) + .copied() + .ok_or("graph hook field lost its exact emitted Descriptor projection")?; + let projection = self + .instruction + .required_proof() + .project_actor_graph_hook(value, ty)? + .ok_or("graph hook field lost its required path/signature")?; + projection.checked_hook(self.instruction.checked())?; + let node = self.add(DebugNode::Function)?; + self.supplements + .push((node, Supplement::CheckedHook(operation, hook, signature))); + return Ok(node); + } + + if let FrozenValue::ReflectedCallable(admitted) = value { + let plan = self + .instruction + .reflected_callable(admitted) + .ok_or("frozen graph reflected callback has no exact sealed adapter occurrence")?; + plan.validate_types(self.debug.types) + .map_err(|message| message.to_string())?; + if !plan.is_materialized() + || !plan.is_sealed() + || !plan.validate_original() + || plan.checked_plan() != admitted.checked_plan() + || plan.source_type() != admitted.source_type() + || plan.requested_type() != admitted.requested_type() + || admitted.requested_type() != ty + { + return Err( + "frozen graph reflected callback changed its checked adapter contract".into(), + ); + } + // The private HIR selector joins this exact retained leaf to its + // already sealed adapter and original source snapshot. Import the + // genuine source descriptor recursively; never select by label or + // substitute it for the requested callable return contract. + let adapter = plan.function(); + let canonical = plan + .canonical_adapter() + .ok_or("frozen graph reflected callback lost its canonical adapter")?; + let declaration = self + .instruction + .functions() + .iter() + .find(|function| function.id == adapter) + .ok_or("frozen graph reflected callback adapter declaration is absent")?; + if declaration.id != canonical.id + || declaration.identity != canonical.identity + || declaration.debug_kind != canonical.debug_kind + || declaration.source_definition != canonical.source_definition + || declaration.params != canonical.params + || declaration.capture_count != canonical.capture_count + || declaration.return_type != canonical.return_type + || declaration.locals != canonical.locals + || declaration.span != canonical.span + || declaration.capture_count != 1 + || declaration.params.first().is_none_or(|param| { + param.ty != admitted.source_type() || param.mode != jett_hir::ParamMode::Owned + }) + { + return Err( + "frozen graph reflected callback changed its canonical adapter declaration" + .into(), + ); + } + let signature = Type::Function { + params: declaration.params[1..] + .iter() + .map(|param| param.ty) + .collect(), + view_params: declaration.params[1..] + .iter() + .map(|param| param.mode == jett_hir::ParamMode::View) + .collect(), + return_type: declaration.return_type, + }; + if self.debug.types.resolve(ty) != &signature { + return Err("frozen graph reflected callback adapter signature changed".into()); + } + // Mandatory HIR/MIR reflected callable validation authenticates the + // canonical body, return conversion and predicates before emission. + // Its retained sealed metadata is preserved rather than erased here. + let source = self.value(admitted.source_type(), admitted.source())?; + let depth = self.typed(TypeInterner::UINT64)?; + let environment = self.add(DebugNode::Record( + "$checked_adapter_environment".into(), + vec![("$source".into(), source), ("$pending0".into(), depth)], + ))?; + let node = self.add(DebugNode::Function)?; + self.supplements + .push((node, Supplement::Function(adapter, Some(environment)))); + return Ok(node); + } + + let checked = self.instruction.checked(); + let candidates = function_value_candidates( + self.instruction.functions(), + &checked.checked().method_value_definitions, + ); + let selected = selected_function(value, ty, self.debug.types, &candidates)?; + let mut children = Vec::new(); + for (name, ty) in selected.captures() { + let FrozenValue::Function { captures, .. } = value else { + return Err("frozen named callback has unexpected environment".into()); + }; + let value = captures + .get(name) + .ok_or("frozen callback capture is absent")?; + children.push((name.clone(), self.value(*ty, value)?)); + } + let environment = if children.is_empty() { + None + } else { + for index in 0..selected.captures().len() { + children.push(( + format!("$pending{index}"), + self.typed(TypeInterner::UINT64)?, + )); + } + Some(self.add(DebugNode::Record( + "$checked_capture_environment".into(), + children, + ))?) + }; + let source = self.add(DebugNode::Function)?; + self.supplements + .push((source, Supplement::Function(selected.id(), environment))); + let declaration = self + .instruction + .functions() + .iter() + .find(|function| function.id == selected.id()) + .ok_or("frozen callback declaration absent")?; + let signature = Type::Function { + params: declaration.params[declaration.capture_count..] + .iter() + .map(|param| param.ty) + .collect(), + view_params: declaration.params[declaration.capture_count..] + .iter() + .map(|param| param.mode == jett_hir::ParamMode::View) + .collect(), + return_type: declaration.return_type, + }; + let source_type = self + .debug + .types + .type_ids() + .find(|candidate| self.debug.types.resolve(*candidate) == &signature) + .ok_or("frozen callback source signature absent")?; + if source_type == ty { + return Ok(source); + } + let mut adapters = self.instruction.functions().iter().filter(|function| { + function + .identity + .declaration + .name + .starts_with("$interface.adapter.") + && function.capture_count == 1 + && function + .params + .first() + .is_some_and(|param| param.ty == source_type) + && self.debug.types.resolve(ty) + == &Type::Function { + params: function.params[1..].iter().map(|param| param.ty).collect(), + view_params: function.params[1..] + .iter() + .map(|param| param.mode == jett_hir::ParamMode::View) + .collect(), + return_type: function.return_type, + } + }); + let adapter = adapters + .next() + .ok_or("frozen callback conversion lost its checked generated adapter")? + .id; + if adapters.next().is_some() { + return Err("frozen callback conversion is ambiguous".into()); + } + let depth = self.typed(TypeInterner::UINT64)?; + let environment = self.add(DebugNode::Record( + "$checked_adapter_environment".into(), + vec![("$source".into(), source), ("$pending0".into(), depth)], + ))?; + let node = self.add(DebugNode::Function)?; + self.supplements + .push((node, Supplement::Function(adapter, Some(environment)))); + Ok(node) + } + fn builder(&mut self, value: &FrozenValue) -> Result { + let FrozenValue::TypeConstruction { + type_name, + variant, + state, + fields, + } = value + else { + unreachable!() + }; + let checked = self.instruction.checked(); + let reflection = &checked.checked().reflection_metadata; + let mut selected = if let Some(name) = variant { + reflection + .get_type_variants(type_name) + .ok_or("frozen builder variant metadata absent")? + .iter() + .find(|v| &v.name == name) + .ok_or("frozen builder variant absent")? + .fields + .clone() + } else if let Some(name) = state { + reflection + .get_machine(type_name) + .ok_or("frozen builder machine metadata absent")? + .states + .iter() + .find(|v| &v.name == name) + .ok_or("frozen builder state absent")? + .fields + .clone() + } else { + reflection + .get_type_fields(type_name) + .unwrap_or(&[]) + .to_vec() + }; + let mut children = Vec::new(); + for field in selected.drain(..) { + let mut ty = checked_type(self.debug.types, &field.type_info.type_name)?; + if !matches!( + self.debug + .types + .resolve(checked_type(self.debug.types, type_name)?), + Type::Bitfield(_) + ) { + while let Type::Refinement { base, .. } = self.debug.types.resolve(ty) { + ty = *base; + } + } + let actual = fields.iter().find(|(_, name, _, _)| name == &field.name); + children.push(if let Some((_, _, _, value)) = actual { + self.value(ty, value)? + } else { + self.typed(ty)? + }); + } + let node = self.add(DebugNode::TypeConstruction)?; + self.supplements.push(( + node, + Supplement::Builder( + type_name.clone(), + u32::from(variant.is_some() || state.is_some()), + children, + ), + )); + Ok(node) + } + fn encode(self, root: u32) -> Result<(Vec, Vec<(usize, u64)>), String> { + let mut debug = b"JD\x01".to_vec(); + put(&mut debug, self.debug.nodes.len())?; + debug.extend(root.to_le_bytes()); + for node in self.debug.nodes { + let mut bytes = Vec::new(); + debug::encode_node(&mut bytes, node.ok_or("incomplete private graph schema")?) + .ok_or("cannot encode private graph node")?; + put(&mut debug, bytes.len())?; + debug.extend(bytes); + } + let mut bytes = b"JA\x01".to_vec(); + put(&mut bytes, debug.len())?; + bytes.extend(debug); + put(&mut bytes, self.supplements.len())?; + let mut functions = Vec::new(); + for (node, supplement) in self.supplements { + bytes.extend(node.to_le_bytes()); + match supplement { + Supplement::Hidden(child) => { + bytes.push(0); + bytes.extend(child.to_le_bytes()); + } + Supplement::Function(function, env) => { + bytes.push(1); + functions.push((bytes.len(), u64::from(function.index()))); + bytes.extend(0u64.to_le_bytes()); + bytes.extend(env.unwrap_or(u32::MAX).to_le_bytes()); + } + Supplement::Interface(ty, child) => { + bytes.push(2); + bytes.extend(u64::from(ty.index()).to_le_bytes()); + bytes.extend(child.to_le_bytes()); + } + Supplement::Builder(owner, offset, fields) => { + bytes.push(3); + put(&mut bytes, owner.len())?; + bytes.extend(owner.as_bytes()); + bytes.extend(offset.to_le_bytes()); + put(&mut bytes, fields.len())?; + for child in fields { + bytes.extend(child.to_le_bytes()); + } + } + Supplement::CheckedHook(operation, hook, signature) => { + bytes.push(7); + bytes.extend(operation.to_le_bytes()); + bytes.extend(hook.to_le_bytes()); + bytes.extend(signature.to_le_bytes()); + } + Supplement::Resource => bytes.push(4), + Supplement::List(children) => { + bytes.push(5); + put(&mut bytes, children.len())?; + for child in children { + bytes.extend(child.to_le_bytes()); + } + } + Supplement::Map(children) => { + bytes.push(6); + put(&mut bytes, children.len())?; + for (a, b) in children { + bytes.extend(a.to_le_bytes()); + bytes.extend(b.to_le_bytes()); + } + } + } + } + Ok((bytes, functions)) + } +} +fn put(bytes: &mut Vec, count: usize) -> Result<(), String> { + bytes.extend( + u32::try_from(count) + .map_err(|_| "native import schema exhausted")? + .to_le_bytes(), + ); + Ok(()) +} +fn checked_type(types: &TypeInterner, name: &str) -> Result { + let mut candidates = types.type_ids().filter(|ty| types.type_name(*ty) == name); + let selected = candidates.next().ok_or("frozen import type absent")?; + if candidates.next().is_some() { + return Err("frozen import type ambiguous".into()); + } + Ok(selected) +} +fn import_layout( + instruction: &ActorGraphInstruction, + types: &TypeInterner, + ty: TypeId, + hooks: HashMap, +) -> Result<(Vec, Vec<(usize, u64)>), String> { + let mut graph = ImportGraph { + debug: DebugGraph { + types, + equality_methods: None, + custom: Vec::new(), + indexes: HashMap::new(), + nodes: Vec::new(), + }, + instruction, + supplements: Vec::new(), + hooks, + }; + let root = graph.value(ty, instruction.field_value()?)?; + graph.encode(root) +} + +impl Translator<'_, '_> { + fn actor_graph_hook_rows( + &self, + instruction: &ActorGraphInstruction, + ) -> Result, CodegenError> { + let mut rows = HashMap::new(); + if instruction.projected_hooks().is_empty() { + return Ok(rows); + } + let resource = self.resource.ok_or_else(|| { + contract_error( + self.symbol, + instruction.original().span, + "graph hook import has no Resource family", + ) + })?; + let plan = resource.function_plan()?; + for (projection, hook) in instruction.projected_hooks() { + let mut selected = plan.graph_descriptor_operations().filter(|(producer, _)| { + producer.same_import(instruction) + && producer.projected_hook().is_some_and(|current| { + current.root() == projection.root() + && current.path() == projection.path() + && current.signature() == projection.signature() + }) + }); + let (producer, operation) = selected.next().ok_or_else(|| { + contract_error( + self.symbol, + instruction.original().span, + "graph hook import lost its current Descriptor occurrence", + ) + })?; + if selected.next().is_some() + || !matches!(producer.operation(),ActorGraphOperation::HookBox {hook:current,..} if current==hook) + { + return Err(contract_error( + self.symbol, + instruction.original().span, + "graph hook Descriptor projection is not unique", + )); + } + let (hook_id, signature) = resource.layout.graph_hook(hook).ok_or_else(|| { + contract_error( + self.symbol, + instruction.original().span, + "graph hook has no exact installed nominal signature", + ) + })?; + rows.insert( + std::ptr::from_ref(projection.value().map_err(|message| { + contract_error(self.symbol, instruction.original().span, message) + })?) + .addr(), + (resource.ordinal(operation)?, hook_id, signature), + ); + } + Ok(rows) + } + pub(super) fn actor_graph_step( + &mut self, + instruction: &ActorGraphInstruction, + args: &[Expression], + expression: &Expression, + ) -> Result { + let ticket = if let Some(arg) = args.first() { + let value = self.expression(arg)?; + Some(self.scalar(value, arg.span)?) + } else { + None + }; + match instruction.operation() { + ActorGraphOperation::Begin => Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::ActorGraphBegin, + &[], + true, + )?)), + ActorGraphOperation::ReserveBase { fields, .. } => { + let count = self.builder.ins().iconst(ir::types::I64, *fields as i64); + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::ActorGraphReserveBase, + &[ticket.expect("sealed ticket"), count], + true, + )?)) + } + ActorGraphOperation::ReservePending { depth, .. } => { + let base = self.expression(&args[1])?; + let base = self.scalar(base, args[1].span)?; + let depth = self.builder.ins().iconst(ir::types::I64, *depth as i64); + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::ActorGraphReservePending, + &[ticket.expect("sealed ticket"), base, depth], + true, + )?)) + } + ActorGraphOperation::Fill { ty, field, .. } => { + let base = self.expression(&args[1])?; + let base = self.scalar(base, args[1].span)?; + // Earlier completed values now live in local slots or the + // unpublished batch. Retire expression snapshots before transfer. + self.drop_temporaries()?; + let ExpressionKind::Local(local) = args[2].kind else { + return Err(self.unsupported(expression.span, "unsealed graph field place")); + }; + let value = if let Some(slot) = self.local_slots[local.index() as usize] { + let bits = self.builder.ins().stack_load(ir::types::I64, slot, 0); + LoweredValue::Owned(bits, slot) + } else { + self.expression(&args[2])? + }; + let depth = if let LoweredValue::ScalarTask(_, depth) = value { + depth + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let (bits, owned) = self.payload_bits(value); + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let field = self.builder.ins().iconst(ir::types::I64, *field as i64); + let (bytes, functions) = import_layout( + instruction, + self.types, + *ty, + self.actor_graph_hook_rows(instruction)?, + ) + .map_err(|message| contract_error(self.symbol, expression.span, message))?; + let (layout, length) = self.static_data_with_functions(&bytes, &functions)?; + self.leaf( + NativeLeaf::ActorGraphFill, + &[ + ticket.expect("sealed ticket"), + base, + field, + bits, + owned, + depth, + layout, + length, + ], + true, + )?; + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + Ok(self.nothing()) + } + ActorGraphOperation::RootFill { ty } => { + self.drop_temporaries()?; + let ExpressionKind::Local(local) = args[1].kind else { + return Err(self.unsupported(expression.span, "unsealed graph root place")); + }; + let value = if let Some(slot) = self.local_slots[local.index() as usize] { + LoweredValue::Owned( + self.builder.ins().stack_load(ir::types::I64, slot, 0), + slot, + ) + } else { + self.expression(&args[1])? + }; + let depth = if let LoweredValue::ScalarTask(_, depth) = value { + depth + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let (bits, owned) = self.payload_bits(value); + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let (bytes, functions) = import_layout( + instruction, + self.types, + *ty, + self.actor_graph_hook_rows(instruction)?, + ) + .map_err(|message| contract_error(self.symbol, expression.span, message))?; + let (layout, length) = self.static_data_with_functions(&bytes, &functions)?; + self.leaf( + NativeLeaf::ActorGraphRootFill, + &[ + ticket.expect("sealed ticket"), + bits, + owned, + depth, + layout, + length, + ], + true, + )?; + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + Ok(LoweredValue::Scalar(bits)) + } + ActorGraphOperation::RootPublish { ty } => { + let bits = self.expression(&args[1])?; + let bits = self.scalar(bits, args[1].span)?; + if is_copy_owned(self.types, *ty) || is_linear(self.types, *ty) { + self.own(bits) + } else { + Ok(LoweredValue::Scalar(bits)) + } + } + ActorGraphOperation::HookBox { .. } => Err(contract_error( + self.symbol, + expression.span, + "graph hook producer lost its exact Resource Descriptor operation", + )), + ActorGraphOperation::Commit => { + self.leaf( + NativeLeaf::ActorGraphCommit, + &[ticket.expect("sealed ticket")], + true, + )?; + Ok(self.nothing()) + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_codegen_cranelift/src/emit/actor_graph/tests.rs b/crates/jett_codegen_cranelift/src/emit/actor_graph/tests.rs new file mode 100644 index 00000000..6979854c --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/actor_graph/tests.rs @@ -0,0 +1,225 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::{collections::HashMap, sync::Arc}; +fn prepared(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ) +} +fn baked(checked: &Arc) -> jett_hir::Program { + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + let mut hir = jett_hir::lower_checked_ordinary_program(checked).unwrap(); + jett_hir::bake_required_values( + &mut hir, + &evaluation.values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + hir +} +#[test] +fn original_direct_actor_import_emits_an_object_from_checked_graph_steps() { + let checked = prepared( + r#"namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = comptime spawn Counter() + int64 observed = ask worker.read + println(observed) + return nothing +"#, + ); + let hir = baked(&checked); + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + let bytes = crate::emit_host_object(&mir, &checked.checked().interner).unwrap(); + assert!(!bytes.bytes.is_empty()); +} +#[test] +fn nested_state_graph_emits_typed_nonowning_actor_references() { + let checked = prepared( + "namespace app\nactor Counter:\n mutable int64 count = 1\nactor Holder:\n app.Counter child = spawn app.Counter()\nfunction main() returns nothing:\n Holder worker = comptime spawn Holder()\n return nothing\n", + ); + let hir = baked(&checked); + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + let bytes = crate::emit_host_object(&mir, &checked.checked().interner).unwrap(); + assert!(!bytes.bytes.is_empty()); +} +#[test] +fn emitted_field_schema_uses_exact_ja1_and_jd1_headers() { + let checked = prepared( + "namespace app\nactor Counter:\n mutable int64 count = 1\nfunction main() returns nothing:\n Counter worker = comptime spawn Counter()\n return nothing\n", + ); + let hir = baked(&checked); + let mut plans = Vec::new(); + for function in &hir.functions { + let mut body = function.body.clone(); + jett_hir::visit_actor_graph_plans(&mut body, |plan| plans.push(plan.clone())).unwrap(); + } + let fill = plans[0] + .instructions() + .iter() + .find(|step| matches!(step.operation(), ActorGraphOperation::Fill { .. })) + .unwrap(); + let (bytes, functions) = import_layout( + fill, + &checked.checked().interner, + TypeInterner::INT64, + HashMap::new(), + ) + .unwrap(); + assert_eq!(&bytes[..3], b"JA\x01"); + assert_eq!(&bytes[7..10], b"JD\x01"); + assert!(functions.is_empty()); +} + +#[test] +fn actor_callback_field_keeps_exact_admitted_primitive_captures_and_compiled_code() { + let checked = prepared( + "namespace app\nfunction create_callback() returns function() returns string:\n string text = \"baked\"\n return function() returns string: return text\nactor Keeper:\n mutable function() returns string callback = create_callback()\n receive read responds string:\n respond callback()\nfunction main() returns nothing:\n Keeper worker = comptime spawn Keeper()\n string text = ask worker.read\n println(text)\n return nothing\n", + ); + let hir = baked(&checked); + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + let artifact = crate::emit_host_object(&mir, &checked.checked().interner).unwrap(); + assert!(!artifact.bytes.is_empty()); +} + +#[test] +fn ordinary_shared_graph_factory_emits_without_resource_scope_or_layout() { + let checked = prepared(include_str!( + "../../../../jett_driver/tests/actor_graph/fixtures/02_shared_fresh.jett" + )); + let hir = baked(&checked); + let types = &checked.checked().interner; + let mir = jett_mir::lower(&hir, types).unwrap(); + assert!(mir.resource_manifest.kinds().is_empty()); + let ownership = jett_mir::validate_resource_ownership(&mir, types).unwrap(); + assert!(ownership.entry_scope().is_none()); + assert!(ownership.functions().is_empty()); + let entry = mir + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() + .id; + for optimize in [false, true] { + let artifact = crate::emit_host_program_object_with_options( + &mir, + types, + entry, + crate::CodegenOptions { optimize }, + ) + .unwrap(); + assert!(!artifact.bytes.is_empty()); + assert!( + artifact + .symbols + .iter() + .all(|symbol| symbol != "jett_aot_resource_v1_manifest") + ); + } + let mut forged = mir; + let function = forged + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "baked") + .unwrap(); + function.locals[0].span.end += 1; + assert!(crate::emit_host_program_object(&forged, types, entry).is_err()); +} + +#[test] +fn map_shared_graph_iteration_emits_both_ordinary_object_options() { + let source = + include_str!("../../../../jett_driver/tests/actor_graph/fixtures/10_map_shared.jett"); + let mut parsed = jett_parser::parse(source, FileId::new(0)); + let mut origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + for (index, support) in [ + include_str!("../../../../../stdlib/map.jett"), + include_str!("../../../../../stdlib/list.jett"), + ] + .into_iter() + .enumerate() + { + let file = FileId::new(10_000 + index as u32); + let support = jett_parser::parse(support, file); + assert!(support.errors.is_empty()); + let mut items = support.module.items; + items.append(&mut parsed.module.items); + parsed.module.items = items; + origins.insert(file, SourceOrigin::Stdlib); + } + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + origins, + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let hir = baked(&checked); + let types = &checked.checked().interner; + let mir = jett_mir::lower(&hir, types).unwrap(); + assert!(mir.resource_manifest.kinds().is_empty()); + let ownership = jett_mir::validate_resource_ownership(&mir, types).unwrap(); + assert!(ownership.entry_scope().is_none()); + assert!(ownership.functions().is_empty()); + let entry = mir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() + .id; + for optimize in [false, true] { + let artifact = crate::emit_host_program_object_with_options( + &mir, + types, + entry, + crate::CodegenOptions { optimize }, + ) + .unwrap(); + assert!(!artifact.bytes.is_empty()); + assert!( + artifact + .symbols + .iter() + .all(|symbol| symbol != "jett_aot_resource_v1_manifest") + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/debug.rs b/crates/jett_codegen_cranelift/src/emit/debug.rs new file mode 100644 index 00000000..4a1a60e5 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/debug.rs @@ -0,0 +1,421 @@ +use std::collections::{HashMap, HashSet}; + +use jett_runtime::native_abi::values::NativeDebugTag; +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::values::list_sort_kind; + +pub(super) enum DebugNode { + Primitive(u32), + Nothing, + Bytes, + List(u32), + Set(u32), + Map(u32, u32), + Optional(u32), + Result(u32, u32), + Record(String, Vec<(String, u32)>), + Enum(String, Vec<(String, Vec)>), + Machine(String, Vec<(String, Vec)>), + Capability(String), + Function, + Actor, + TypeConstruction, + Redacted, + Interface, + Uninhabited, + Alias(u32), +} + +pub(super) struct DebugGraph<'a> { + pub(super) types: &'a TypeInterner, + pub(super) equality_methods: Option<&'a HashMap>, + pub(super) custom: Vec<(u32, jett_hir::FunctionId)>, + pub(super) indexes: HashMap, + pub(super) nodes: Vec>, +} + +impl DebugGraph<'_> { + pub(super) fn node(&mut self, ty: TypeId) -> Option { + if let Some(&index) = self.indexes.get(&ty) { + return Some(index); + } + let index = u32::try_from(self.nodes.len()).ok()?; + self.indexes.insert(ty, index); + self.nodes.push(None); + let node = if let Some(kind) = list_sort_kind(self.types, ty) { + DebugNode::Primitive(kind as u32) + } else { + match self.types.resolve(ty).clone() { + Type::Nothing => DebugNode::Nothing, + Type::Bytes => DebugNode::Bytes, + Type::List(element) => DebugNode::List(self.node(element)?), + Type::Set(element) => DebugNode::Set(self.node(element)?), + Type::Map(key, value) => DebugNode::Map(self.node(key)?, self.node(value)?), + Type::Optional(element) => DebugNode::Optional(self.node(element)?), + Type::Result(ok, error) => DebugNode::Result(self.node(ok)?, self.node(error)?), + Type::Struct(id) => { + let definition = self.types.resolve_struct(id).clone(); + let mut fields = Vec::with_capacity(definition.fields.len()); + if let Some(methods) = self.equality_methods { + self.custom.push((index, *methods.get(&ty)?)); + } else { + for (name, ty) in definition.fields { + fields.push((name, self.node(ty)?)); + } + } + DebugNode::Record(value_type_name(&definition.name), fields) + } + Type::Bitfield(id) => { + let definition = self.types.resolve_bitfield(id).clone(); + let mut fields = Vec::with_capacity(definition.fields.len()); + for field in definition.fields { + fields.push((field.name, self.node(field.ty)?)); + } + DebugNode::Record(value_type_name(&definition.name), fields) + } + Type::Enum(id) => { + let definition = self.types.resolve_enum(id).clone(); + let mut variants = Vec::with_capacity(definition.variants.len()); + for variant in definition.variants { + let mut fields = Vec::with_capacity(variant.fields.len()); + for (_, ty) in variant.fields { + fields.push(self.node(ty)?); + } + variants.push((variant.name, fields)); + } + DebugNode::Enum(value_type_name(&definition.name), variants) + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + let definition = self.types.resolve_machine(id).clone(); + let mut states = Vec::with_capacity(definition.states.len()); + for state in definition.states { + let mut fields = Vec::with_capacity(state.fields.len()); + for (_, ty) in state.fields { + fields.push(self.node(ty)?); + } + states.push((state.name, fields)); + } + DebugNode::Machine(value_type_name(&definition.name), states) + } + Type::Capability(_) => DebugNode::Capability(self.types.type_name(ty)), + Type::Function { .. } => DebugNode::Function, + Type::Actor(_) => DebugNode::Actor, + Type::TypeConstruction => DebugNode::TypeConstruction, + Type::Refinement { base, .. } => DebugNode::Alias(self.node(base)?), + Type::Secret(_) => DebugNode::Redacted, + Type::Interface(_) => DebugNode::Interface, + Type::Never => DebugNode::Uninhabited, + Type::Resource(_) | Type::Error => return None, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool => return None, + } + }; + self.nodes[index as usize] = Some(node); + Some(index) + } +} + +// Value displays use the nominal declaration name; the binding label carries +// the complete instantiated type, including declared aliases. +fn value_type_name(name: &str) -> String { + name.split_once('[') + .map_or(name, |(base, _)| base) + .to_owned() +} + +fn number(bytes: &mut Vec, value: usize) -> Option<()> { + bytes.extend_from_slice(&u32::try_from(value).ok()?.to_le_bytes()); + Some(()) +} + +fn name(bytes: &mut Vec, value: &str) -> Option<()> { + number(bytes, value.len())?; + bytes.extend_from_slice(value.as_bytes()); + Some(()) +} + +pub(super) fn encode_node(bytes: &mut Vec, node: DebugNode) -> Option<()> { + match node { + DebugNode::Primitive(kind) => { + bytes.push(NativeDebugTag::Primitive as u8); + bytes.extend_from_slice(&kind.to_le_bytes()); + } + DebugNode::Nothing => bytes.push(NativeDebugTag::Nothing as u8), + DebugNode::Bytes => bytes.push(NativeDebugTag::Bytes as u8), + DebugNode::Function => bytes.push(NativeDebugTag::Function as u8), + DebugNode::Actor => bytes.push(NativeDebugTag::Actor as u8), + DebugNode::TypeConstruction => bytes.push(NativeDebugTag::TypeConstruction as u8), + DebugNode::Redacted => bytes.push(NativeDebugTag::Redacted as u8), + DebugNode::Interface => bytes.push(NativeDebugTag::Interface as u8), + DebugNode::Uninhabited => bytes.push(NativeDebugTag::Uninhabited as u8), + DebugNode::List(child) => child_node(bytes, NativeDebugTag::List, child), + DebugNode::Set(child) => child_node(bytes, NativeDebugTag::Set, child), + DebugNode::Optional(child) => child_node(bytes, NativeDebugTag::Optional, child), + DebugNode::Alias(child) => child_node(bytes, NativeDebugTag::Alias, child), + DebugNode::Map(key, value) => pair_node(bytes, NativeDebugTag::Map, key, value), + DebugNode::Result(ok, error) => pair_node(bytes, NativeDebugTag::Result, ok, error), + DebugNode::Record(type_name, fields) => { + bytes.push(NativeDebugTag::Record as u8); + name(bytes, &type_name)?; + number(bytes, fields.len())?; + for (field_name, child) in fields { + name(bytes, &field_name)?; + bytes.extend_from_slice(&child.to_le_bytes()); + } + } + DebugNode::Enum(type_name, variants) => { + variants_node(bytes, NativeDebugTag::Enum, &type_name, variants)?; + } + DebugNode::Machine(type_name, variants) => { + variants_node(bytes, NativeDebugTag::Machine, &type_name, variants)?; + } + DebugNode::Capability(type_name) => { + bytes.push(NativeDebugTag::Capability as u8); + name(bytes, &type_name)?; + } + } + Some(()) +} + +pub(crate) fn function_label(function: &jett_mir::Function) -> Result { + let parameters = function + .params + .get(function.capture_count..) + .ok_or("function debug capture count exceeds its parameter metadata")?; + let identity = match &function.debug_kind { + jett_hir::FunctionDebugKind::Named(name) => { + if name.is_empty() || name.chars().any(char::is_control) { + return Err("named function debug identity has invalid source name metadata"); + } + name.clone() + } + jett_hir::FunctionDebugKind::Inline => { + if parameters.iter().any(|parameter| { + parameter.name.is_empty() || parameter.name.chars().any(char::is_control) + }) { + return Err("inline function debug parameter has invalid source name metadata"); + } + parameters + .iter() + .map(|parameter| parameter.name.as_str()) + .collect::>() + .join(", ") + } + }; + Ok(format!("function({identity})")) +} + +fn child_node(bytes: &mut Vec, tag: NativeDebugTag, child: u32) { + bytes.push(tag as u8); + bytes.extend_from_slice(&child.to_le_bytes()); +} + +fn pair_node(bytes: &mut Vec, tag: NativeDebugTag, first: u32, second: u32) { + bytes.push(tag as u8); + bytes.extend_from_slice(&first.to_le_bytes()); + bytes.extend_from_slice(&second.to_le_bytes()); +} + +fn variants_node( + bytes: &mut Vec, + tag: NativeDebugTag, + type_name: &str, + variants: Vec<(String, Vec)>, +) -> Option<()> { + bytes.push(tag as u8); + name(bytes, type_name)?; + number(bytes, variants.len())?; + for (variant_name, fields) in variants { + name(bytes, &variant_name)?; + number(bytes, fields.len())?; + for child in fields { + bytes.extend_from_slice(&child.to_le_bytes()); + } + } + Some(()) +} + +pub(crate) fn debug_layout(types: &TypeInterner, ty: TypeId) -> Option> { + let mut graph = DebugGraph { + types, + equality_methods: None, + custom: Vec::new(), + indexes: HashMap::new(), + nodes: Vec::new(), + }; + let root = graph.node(ty)?; + let mut bytes = b"JD\x01".to_vec(); + number(&mut bytes, graph.nodes.len())?; + bytes.extend_from_slice(&root.to_le_bytes()); + for node in graph.nodes { + let mut encoded = Vec::new(); + encode_node(&mut encoded, node?)?; + number(&mut bytes, encoded.len())?; + bytes.extend_from_slice(&encoded); + } + Some(bytes) +} + +pub(crate) struct EqualityLayout { + pub bytes: Vec, + pub custom: Vec<(u32, jett_hir::FunctionId)>, +} + +pub(crate) fn equality_layout( + types: &TypeInterner, + ty: TypeId, + methods: &HashMap, +) -> Option { + // Outer secrecy taints the checked comparison result, not its data layout. + // Nested secret payloads still follow the supported graph rules below. + let ty = jett_mir::move_values::representation_type(types, ty); + fn supported( + types: &TypeInterner, + ty: TypeId, + seen: &mut HashSet, + methods: &HashMap, + ) -> bool { + if !seen.insert(ty) { + return true; + } + match types.resolve(ty) { + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Actor(_) => true, + Type::List(element) | Type::Set(element) | Type::Optional(element) => { + supported(types, *element, seen, methods) + } + Type::Map(key, value) | Type::Result(key, value) => { + supported(types, *key, seen, methods) && supported(types, *value, seen, methods) + } + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, field_type)| supported(types, *field_type, seen, methods)) + }), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, field_type)| supported(types, *field_type, seen, methods)) + }) + } + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| supported(types, field.ty, seen, methods)), + Type::Refinement { base, .. } => supported(types, *base, seen, methods), + Type::Struct(_) => methods.contains_key(&ty), + // Exact Equatable dispatch must be retained for user structs. + Type::Secret(_) + | Type::TypeConstruction + | Type::Never + | Type::Interface(_) + | Type::Resource(_) + | Type::Capability(_) + | Type::Function { .. } + | Type::Error => false, + } + } + + if !supported(types, ty, &mut HashSet::new(), methods) { + return None; + } + let mut graph = DebugGraph { + types, + indexes: HashMap::new(), + nodes: Vec::new(), + equality_methods: Some(methods), + custom: Vec::new(), + }; + let root = graph.node(ty)?; + let mut bytes = b"JD\x01".to_vec(); + number(&mut bytes, graph.nodes.len())?; + bytes.extend_from_slice(&root.to_le_bytes()); + for node in graph.nodes { + let mut encoded = Vec::new(); + encode_node(&mut encoded, node?)?; + number(&mut bytes, encoded.len())?; + bytes.extend_from_slice(&encoded); + } + Some(EqualityLayout { + bytes, + custom: graph.custom, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn outer_secret_equality_uses_the_value_layout_without_changing_debug_redaction() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let secret = types.intern(Type::Secret(list)); + let methods = HashMap::new(); + assert_eq!( + equality_layout(&types, secret, &methods).unwrap().bytes, + equality_layout(&types, list, &methods).unwrap().bytes, + ); + assert_eq!( + debug_layout(&types, secret).unwrap().last(), + Some(&(NativeDebugTag::Redacted as u8)) + ); + let secret_element = types.intern(Type::Secret(TypeInterner::INT64)); + let nested = types.intern(Type::List(secret_element)); + assert!(equality_layout(&types, nested, &methods).is_none()); + } + + #[test] + fn function_debug_layout_is_opaque_and_secret_values_are_redacted() { + let mut types = TypeInterner::new(); + let function = types.intern(Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + let mut expected = b"JD\x01".to_vec(); + expected.extend_from_slice(&1u32.to_le_bytes()); + expected.extend_from_slice(&0u32.to_le_bytes()); + expected.extend_from_slice(&1u32.to_le_bytes()); + expected.push(NativeDebugTag::Function as u8); + assert_eq!(debug_layout(&types, function), Some(expected)); + let optional = types.intern(Type::Optional(function)); + let list = types.intern(Type::List(optional)); + for ty in [function, optional, list] { + assert!(debug_layout(&types, ty).is_some()); + assert!(equality_layout(&types, ty, &HashMap::new()).is_none()); + } + let secret = types.intern(Type::Secret(function)); + let layout = debug_layout(&types, secret).unwrap(); + assert_eq!(layout.last(), Some(&(NativeDebugTag::Redacted as u8))); + assert!(equality_layout(&types, secret, &HashMap::new()).is_none()); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/field_updates.rs b/crates/jett_codegen_cranelift/src/emit/field_updates.rs new file mode 100644 index 00000000..b9a1fe22 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/field_updates.rs @@ -0,0 +1,140 @@ +//! Preflight the selected ancestors before RHS execution; commit never Takes. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn field_update_parent( + &mut self, + place: &jett_hir::FieldUpdatePlace, + ) -> Result<(Value, Value), CodegenError> { + let index = place.root().index() as usize; + let slot = self + .local_slots + .get(index) + .copied() + .flatten() + .ok_or_else(|| { + self.unsupported(place.span(), "field update root has no owning storage") + })?; + let mut handle = self.builder.ins().stack_load(ir::types::I64, slot, 0); + for (position, projection) in place.projections().iter().enumerate() { + self.check_struct_pending_access( + handle, + projection.owner_type, + "field access is not supported on ", + true, + )?; + let offset = match projection.owner { + jett_hir::FieldUpdateOwner::Struct(_) => 0, + jett_hir::FieldUpdateOwner::MachineState { state, .. } => { + let expected = self + .builder + .ins() + .iconst(ir::types::I64, i64::from(state.index())); + self.leaf(NativeLeaf::MachineExpectState, &[handle, expected], true)?; + 1 + } + }; + let ordinal = projection + .field_index + .checked_add(offset) + .and_then(|index| i64::try_from(index).ok()) + .ok_or_else(|| { + self.unsupported(place.span(), "field update ordinal is not representable") + })?; + let field = self.builder.ins().iconst(ir::types::I64, ordinal); + if position + 1 == place.projections().len() { + return Ok((handle, field)); + } + handle = self.leaf(NativeLeaf::StructField, &[handle, field], true)?; + } + Err(self.unsupported(place.span(), "field update path is empty")) + } + + pub(super) fn commit_field_update( + &mut self, + place: &jett_hir::FieldUpdatePlace, + expression: &Expression, + span: Span, + ) -> Result<(), CodegenError> { + // Revalidation can still fail without transferring the staged RHS. + let (parent, field) = self.field_update_parent(place)?; + let value = self.expression(expression)?; + let owned = + is_linear(self.types, expression.ty) || is_copy_owned(self.types, expression.ty); + let value = if is_copy_owned(self.types, expression.ty) { + self.own_copy_value(value, expression.ty, span)? + } else { + value + }; + if owned && !matches!(value, LoweredValue::Owned(..)) { + return Err( + self.unsupported(span, "field update replacement has no acquired ownership") + ); + } + let (bits, actual_owned) = self.payload_bits(value); + if actual_owned != owned { + return Err(self.unsupported( + span, + "field update replacement ownership differs from checked field", + )); + } + let depth = match value { + LoweredValue::ScalarTask(_, depth) => depth, + _ => self.builder.ins().iconst(ir::types::I64, 0), + }; + let owns = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + // The leaf validates every fallible condition first and performs one + // occupied swap. It returns the old bits and performs no cleanup. + let displaced = self.leaf( + NativeLeaf::StructReplaceOccupied, + &[parent, field, bits, owns, depth], + true, + )?; + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + if owned { + let LoweredValue::Owned(_, slot) = self.own_linear(displaced)? else { + return Err(self.unsupported(span, "displaced field has no retirement slot")); + }; + // Existing terminal cleanup precedence; the new field stays owned + // by its root even if destruction reports a failure. + self.drop_slot(slot)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn checked_source_field_update_emits_atomic_occupied_leaf() { + let source = "struct Record:\n value: int64\n label: string\nfunction change(mutable record: Record) returns Record:\n record.value = record.value + 1\n record.label = \"after\"\n return record\n"; + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &std::collections::HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("normally checked HIR"); + let program = jett_mir::lower(&high, &checked.interner).expect("checked-place MIR"); + let object = emit_host_object(&program, &checked.interner) + .expect("native occupied field replacement"); + let leaf = b"jett_rt_v1_struct_replace_occupied"; + assert!(object.bytes.windows(leaf.len()).any(|bytes| bytes == leaf)); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/graph_constructors.rs b/crates/jett_codegen_cranelift/src/emit/graph_constructors.rs new file mode 100644 index 00000000..f963c5a3 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/graph_constructors.rs @@ -0,0 +1,147 @@ +//! Ordinary owned construction selected by exact protected Graph output proofs. +use super::*; + +#[derive(Clone, Copy)] +pub(super) struct Storage { + constructor: jett_mir::ResourceConstructorId, + pub(super) slot: ir::StackSlot, +} + +pub(super) fn storage( + function: &Function, + types: &TypeInterner, + builder: &mut FunctionBuilder<'_>, +) -> Result, CodegenError> { + let mut slots = Vec::new(); + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + let StatementKind::ResourceConstructor( + node @ jett_mir::ResourceConstructorNode::Begin { .. }, + ) = &statement.kind + else { + continue; + }; + if jett_mir::native_graph_storage_for_constructor(function, types, node).is_none() { + continue; + } + function + .resource_constructor(node.constructor()) + .map_err(CodegenError::Backend)?; + if slots + .iter() + .any(|storage: &Storage| storage.constructor == node.constructor()) + { + return Err(CodegenError::Backend( + "Graph constructor has copied Begin storage".into(), + )); + } + slots.push(Storage { + constructor: node.constructor(), + slot: builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )), + }); + } + Ok(slots) +} + +impl Translator<'_, '_> { + pub(super) fn graph_constructor_statement( + &mut self, + statement: &Statement, + ) -> Result { + let StatementKind::ResourceConstructor(node) = &statement.kind else { + return Ok(false); + }; + let Some(proof) = + jett_mir::native_graph_storage_for_constructor(self.function, self.types, node) + else { + return Ok(false); + }; + let original = self + .function + .resource_constructor(node.constructor()) + .map_err(|message| contract_error(self.symbol, statement.span, message))?; + if proof.ty() != original.original().ty { + return Err(contract_error( + self.symbol, + statement.span, + "Graph constructor changed its exact output type", + )); + } + let storage = self + .graph_constructor_slots + .iter() + .find(|storage| storage.constructor == node.constructor()) + .copied() + .ok_or_else(|| { + contract_error( + self.symbol, + statement.span, + "Graph constructor has no protected native builder storage", + ) + })?; + match node { + jett_mir::ResourceConstructorNode::Begin { .. } => { + let count = i64::try_from(original.field_types().len()).map_err(|_| { + contract_error( + self.symbol, + statement.span, + "Graph constructor field count exceeds native storage", + ) + })?; + let count = self.builder.ins().iconst(ir::types::I64, count); + let handle = self.leaf(NativeLeaf::StructNew, &[count], true)?; + // This private owner spans physical statements; ordinary temporary + // and local liveness do not describe an incomplete constructor. + self.builder.ins().stack_store(handle, storage.slot, 0); + } + jett_mir::ResourceConstructorNode::Stage { field, value, .. } => { + let handle = self + .builder + .ins() + .stack_load(ir::types::I64, storage.slot, 0); + self.initialize_struct_field(handle, *field, value)?; + } + jett_mir::ResourceConstructorNode::Commit { output, .. } => { + let (_, header) = original.commit().ok_or_else(|| { + contract_error( + self.symbol, + statement.span, + "Graph constructor has no exact Commit output", + ) + })?; + if header.id != *output + || self + .local_slots + .get(output.index() as usize) + .copied() + .flatten() + .is_none() + { + return Err(contract_error( + self.symbol, + statement.span, + "Graph constructor Commit has no ordinary owning output", + )); + } + let handle = self + .builder + .ins() + .stack_load(ir::types::I64, storage.slot, 0); + self.define_local( + *output, + LoweredValue::Owned(handle, storage.slot), + statement.span, + )?; + } + jett_mir::ResourceConstructorNode::End { .. } => { + // Commit cleared this slot on success. An interrupted field prefix + // retains its complete ordinary ownership until this exact End. + self.drop_slot(storage.slot)?; + } + } + Ok(true) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/graphics.rs b/crates/jett_codegen_cranelift/src/emit/graphics.rs new file mode 100644 index 00000000..2f01b583 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/graphics.rs @@ -0,0 +1,515 @@ +use super::*; + +impl Translator<'_, '_> { + fn graphics_callback( + &mut self, + descriptor: Value, + params: &[TypeId], + result_type: TypeId, + arguments: &[Option], + transfers: &[ir::StackSlot], + span: Span, + ) -> Result { + if params.len() != arguments.len() { + return Err(contract_error( + self.symbol, + span, + "Graphics callback arity changed", + )); + } + let code_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_CODE_FIELD as i64); + let address = self.leaf(NativeLeaf::StructField, &[descriptor, code_index], true)?; + let environment_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64); + let environment = self.leaf( + NativeLeaf::StructField, + &[descriptor, environment_index], + true, + )?; + let pointer_type = self.module.target_config().pointer_type(); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().ireduce(pointer_type, address) + }; + let mut signature = self.module.make_signature(); + signature + .params + .push(runtime_context_abi_param(self.module)); + signature.params.push(AbiParam::new(ir::types::I64)); + let mut native_args = vec![self.builder.use_var(self.runtime_context), environment]; + for (parameter, argument) in params.iter().zip(arguments) { + if let Some(ty) = clif_type(self.types, *parameter, "Graphics callback parameter")? { + signature.params.push(AbiParam::new(ty)); + let argument = argument.ok_or_else(|| { + contract_error(self.symbol, span, "Graphics callback value is absent") + })?; + if is_task_scalar(self.types, *parameter)? { + let (bits, depth) = self.scalar_task(argument, span)?; + native_args.push(bits); + signature.params.push(AbiParam::new(ir::types::I64)); + native_args.push(depth); + } else { + native_args.push(self.scalar(argument, span)?); + } + } else if argument.is_some() { + return Err(contract_error( + self.symbol, + span, + "nothing callback argument has a value", + )); + } + } + if let Some(ty) = clif_type(self.types, result_type, "Graphics callback result")? { + signature.returns.push(AbiParam::new(ty)); + if is_task_scalar(self.types, result_type)? { + signature.returns.push(AbiParam::new(ir::types::I64)); + } + } + let signature = self.builder.import_signature(signature); + for &slot in transfers { + self.clear_slot(slot); + } + let call = self + .builder + .ins() + .call_indirect(signature, address, &native_args); + let results = self.builder.func.dfg.inst_results(call).to_vec(); + self.check_failure()?; + let value = results.first().copied().ok_or_else(|| { + contract_error(self.symbol, span, "Graphics callback produced no value") + })?; + if is_task_scalar(self.types, result_type)? { + let depth = results.get(1).copied().ok_or_else(|| { + contract_error( + self.symbol, + span, + "Graphics callback produced no pending depth", + ) + })?; + return Ok(LoweredValue::ScalarTask(value, depth)); + } + if is_linear(self.types, result_type) { + self.own_linear(value) + } else if is_copy_owned(self.types, result_type) { + self.own(value) + } else { + Ok(LoweredValue::Scalar(value)) + } + } + + fn graphics_finish( + &mut self, + value: LoweredValue, + result_slot: ir::StackSlot, + result_var: Variable, + done: ir::Block, + span: Span, + ) -> Result<(), CodegenError> { + let handle = self.scalar(value, span)?; + self.builder.ins().stack_store(handle, result_slot, 0); + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + self.builder.def_var(result_var, handle); + self.builder.ins().jump(done, &[]); + Ok(()) + } + + fn graphics_close_session(&mut self, session: Value) -> Result<(), CodegenError> { + self.leaf(NativeLeaf::GraphicsClose, &[session], true)?; + Ok(()) + } + + fn graphics_failure_result( + &mut self, + value: LoweredValue, + session: Option, + result_slot: ir::StackSlot, + result_var: Variable, + done: ir::Block, + span: Span, + ) -> Result<(), CodegenError> { + if let Some(session) = session { + self.graphics_close_session(session)?; + } + let handle = self.scalar(value, span)?; + let failure = self.builder.ins().iconst( + ir::types::I32, + i64::from(jett_runtime::native_abi::values::SUM_FAILURE), + ); + let message = self.leaf(NativeLeaf::SumTake, &[handle, failure], true)?; + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + let message = self.own(message)?; + let message_handle = self.scalar(message, span)?; + let owns = self.builder.ins().iconst(ir::types::I32, 1); + let result = self.leaf(NativeLeaf::SumNew, &[failure, message_handle, owns], true)?; + if let LoweredValue::Owned(_, slot) = message { + self.clear_slot(slot); + } + self.graphics_finish( + LoweredValue::Scalar(result), + result_slot, + result_var, + done, + span, + ) + } + + fn graphics_tag(&mut self, value: LoweredValue, span: Span) -> Result { + let handle = self.scalar(value, span)?; + self.leaf(NativeLeaf::SumTag, &[handle], true) + } + + pub(super) fn graphics_run( + &mut self, + type_arguments: &[TypeId], + args: &[Expression], + evaluated: &[LoweredValue], + span: Span, + ) -> Result { + let [state_type] = type_arguments else { + return Err(contract_error( + self.symbol, + span, + "Graphics state type is absent", + )); + }; + let [_, config_arg, _, update_arg, render_arg] = args else { + return Err(contract_error( + self.symbol, + span, + "Graphics call arity changed", + )); + }; + let [display, config, initial, update, render] = evaluated else { + return Err(contract_error( + self.symbol, + span, + "Graphics operands are absent", + )); + }; + let Type::Function { + params: update_params, + return_type: update_result, + .. + } = self.types.resolve(update_arg.ty) + else { + return Err(contract_error( + self.symbol, + span, + "Graphics update is not a function", + )); + }; + let update_params = update_params.clone(); + let update_result = *update_result; + let Type::Function { + params: render_params, + return_type: scene_type, + .. + } = self.types.resolve(render_arg.ty) + else { + return Err(contract_error( + self.symbol, + span, + "Graphics render is not a function", + )); + }; + let render_params = render_params.clone(); + let scene_type = *scene_type; + let authority = self.scalar(*display, span)?; + let config_handle = self.scalar(*config, config_arg.span)?; + let update_descriptor = self.scalar(*update, update_arg.span)?; + let render_descriptor = self.scalar(*render, render_arg.span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + + let result_slot = *self + .temporary_slots + .get(self.next_temporary) + .ok_or_else(|| { + contract_error( + self.symbol, + span, + "Graphics result ownership slot is absent", + ) + })?; + self.next_temporary += 1; + let result_var = self.builder.declare_var(ir::types::I64); + let done = self.builder.create_block(); + + let state_var = clif_type(self.types, *state_type, "Graphics state")? + .map(|ty| self.builder.declare_var(ty)); + let state_depth_var = if is_task_scalar(self.types, *state_type)? { + Some(self.builder.declare_var(ir::types::I64)) + } else { + None + }; + let state_slot = match initial { + LoweredValue::Owned(_, slot) => Some(*slot), + _ => None, + }; + if let Some(variable) = state_var { + self.builder.def_var(variable, self.scalar(*initial, span)?); + } + if let Some(variable) = state_depth_var { + let (_, depth) = self.scalar_task(*initial, span)?; + self.builder.def_var(variable, depth); + } + + let validated_config = + self.leaf(NativeLeaf::GraphicsValidateConfig, &[config_handle], true)?; + let validated_config = self.own_linear(validated_config)?; + let config_ok = self.builder.create_block(); + let config_bad = self.builder.create_block(); + let tag = self.graphics_tag(validated_config, span)?; + self.builder + .ins() + .brif(tag, config_ok, &[], config_bad, &[]); + self.builder.switch_to_block(config_bad); + self.graphics_finish(validated_config, result_slot, result_var, done, span)?; + self.builder.switch_to_block(config_ok); + if let LoweredValue::Owned(_, slot) = validated_config { + self.drop_slot(slot)?; + } + + // Decode the complete config before observing dimensions. Pending + // owners and fields must fail in reference field order, before domain + // validation or any render/provider operation. + let width_index = self.builder.ins().iconst(ir::types::I64, 1); + let height_index = self.builder.ins().iconst(ir::types::I64, 2); + let width = self.leaf(NativeLeaf::StructField, &[config_handle, width_index], true)?; + let height = self.leaf( + NativeLeaf::StructField, + &[config_handle, height_index], + true, + )?; + + let current_state = state_var.map(|variable| { + let bits = self.builder.use_var(variable); + state_depth_var.map_or(LoweredValue::Scalar(bits), |depth_variable| { + LoweredValue::ScalarTask(bits, self.builder.use_var(depth_variable)) + }) + }); + let first_scene = self.graphics_callback( + render_descriptor, + &render_params, + scene_type, + &[current_state], + &[], + span, + )?; + let first_scene_handle = self.scalar(first_scene, span)?; + let validated_scene = self.leaf( + NativeLeaf::GraphicsValidateScene, + &[width, height, first_scene_handle], + true, + )?; + let validated_scene = self.own_linear(validated_scene)?; + let scene_ok = self.builder.create_block(); + let scene_bad = self.builder.create_block(); + let tag = self.graphics_tag(validated_scene, span)?; + self.builder.ins().brif(tag, scene_ok, &[], scene_bad, &[]); + self.builder.switch_to_block(scene_bad); + self.graphics_finish(validated_scene, result_slot, result_var, done, span)?; + self.builder.switch_to_block(scene_ok); + if let LoweredValue::Owned(_, slot) = validated_scene { + self.drop_slot(slot)?; + } + + let opened = self.leaf(NativeLeaf::GraphicsOpen, &[authority, config_handle], true)?; + let opened = self.own_linear(opened)?; + let open_ok = self.builder.create_block(); + let open_bad = self.builder.create_block(); + let tag = self.graphics_tag(opened, span)?; + self.builder.ins().brif(tag, open_ok, &[], open_bad, &[]); + self.builder.switch_to_block(open_bad); + self.graphics_failure_result(opened, None, result_slot, result_var, done, span)?; + self.builder.switch_to_block(open_ok); + let success = self.builder.ins().iconst( + ir::types::I32, + i64::from(jett_runtime::native_abi::values::SUM_SUCCESS), + ); + let session = self.leaf( + NativeLeaf::SumTake, + &[self.scalar(opened, span)?, success], + true, + )?; + if let LoweredValue::Owned(_, slot) = opened { + self.clear_slot(slot); + } + + let presented = self.leaf( + NativeLeaf::GraphicsPresent, + &[session, first_scene_handle], + true, + )?; + let presented = self.own_linear(presented)?; + let first_present_ok = self.builder.create_block(); + let first_present_bad = self.builder.create_block(); + let tag = self.graphics_tag(presented, span)?; + self.builder + .ins() + .brif(tag, first_present_ok, &[], first_present_bad, &[]); + self.builder.switch_to_block(first_present_bad); + self.graphics_close_session(session)?; + self.graphics_finish(presented, result_slot, result_var, done, span)?; + self.builder.switch_to_block(first_present_ok); + if let LoweredValue::Owned(_, slot) = presented { + self.drop_slot(slot)?; + } + if let LoweredValue::Owned(_, slot) = first_scene { + self.drop_slot(slot)?; + } + + let loop_head = self.builder.create_block(); + self.builder.ins().jump(loop_head, &[]); + self.builder.switch_to_block(loop_head); + let next = self.leaf(NativeLeaf::GraphicsNextKey, &[session], true)?; + let next = self.own_linear(next)?; + let next_ok = self.builder.create_block(); + let next_bad = self.builder.create_block(); + let tag = self.graphics_tag(next, span)?; + self.builder.ins().brif(tag, next_ok, &[], next_bad, &[]); + self.builder.switch_to_block(next_bad); + self.graphics_failure_result(next, Some(session), result_slot, result_var, done, span)?; + self.builder.switch_to_block(next_ok); + let optional = self.leaf( + NativeLeaf::SumTake, + &[self.scalar(next, span)?, success], + true, + )?; + if let LoweredValue::Owned(_, slot) = next { + self.clear_slot(slot); + } + let optional = self.own_linear(optional)?; + let key_ready = self.builder.create_block(); + let closed = self.builder.create_block(); + let tag = self.graphics_tag(optional, span)?; + self.builder.ins().brif(tag, key_ready, &[], closed, &[]); + self.builder.switch_to_block(closed); + if let LoweredValue::Owned(_, slot) = optional { + self.drop_slot(slot)?; + } + self.graphics_close_session(session)?; + let nothing = self.builder.ins().iconst(ir::types::I64, 0); + let borrowed = self.builder.ins().iconst(ir::types::I32, 0); + let completed = self.leaf(NativeLeaf::SumNew, &[success, nothing, borrowed], true)?; + self.graphics_finish( + LoweredValue::Scalar(completed), + result_slot, + result_var, + done, + span, + )?; + + self.builder.switch_to_block(key_ready); + let borrowed = self.builder.ins().iconst(ir::types::I32, 0); + let key_index = self.leaf( + NativeLeaf::SumTake, + &[self.scalar(optional, span)?, success], + true, + )?; + if let LoweredValue::Owned(_, slot) = optional { + self.clear_slot(slot); + } + let one = self.builder.ins().iconst(ir::types::I64, 1); + let key = self.leaf(NativeLeaf::StructNew, &[one], true)?; + let key = self.own_linear(key)?; + let key_handle = self.scalar(key, span)?; + self.leaf( + NativeLeaf::StructInit, + &[key_handle, zero, key_index, borrowed], + true, + )?; + let mut transfers = Vec::new(); + if is_linear(self.types, *state_type) { + transfers.push(state_slot.ok_or_else(|| { + contract_error( + self.symbol, + span, + "linear Graphics state has no owning slot", + ) + })?); + } + if let LoweredValue::Owned(_, slot) = key { + transfers.push(slot); + } + let current_state = state_var.map(|variable| { + let bits = self.builder.use_var(variable); + state_depth_var.map_or(LoweredValue::Scalar(bits), |depth_variable| { + LoweredValue::ScalarTask(bits, self.builder.use_var(depth_variable)) + }) + }); + let updated = self.graphics_callback( + update_descriptor, + &update_params, + update_result, + &[current_state, Some(LoweredValue::Scalar(key_handle))], + &transfers, + span, + )?; + if let Some(variable) = state_var { + let value = self.scalar(updated, span)?; + if let Some(state_slot) = state_slot { + if is_copy_owned(self.types, *state_type) { + self.drop_slot(state_slot)?; + } + self.builder.ins().stack_store(value, state_slot, 0); + if let LoweredValue::Owned(_, slot) = updated { + self.clear_slot(slot); + } + } + self.builder.def_var(variable, value); + } + if let Some(variable) = state_depth_var { + let (_, depth) = self.scalar_task(updated, span)?; + self.builder.def_var(variable, depth); + } + + let current_state = state_var.map(|variable| { + let bits = self.builder.use_var(variable); + state_depth_var.map_or(LoweredValue::Scalar(bits), |depth_variable| { + LoweredValue::ScalarTask(bits, self.builder.use_var(depth_variable)) + }) + }); + let scene = self.graphics_callback( + render_descriptor, + &render_params, + scene_type, + &[current_state], + &[], + span, + )?; + let scene_handle = self.scalar(scene, span)?; + let presented = self.leaf(NativeLeaf::GraphicsPresent, &[session, scene_handle], true)?; + let presented = self.own_linear(presented)?; + let present_ok = self.builder.create_block(); + let present_bad = self.builder.create_block(); + let tag = self.graphics_tag(presented, span)?; + self.builder + .ins() + .brif(tag, present_ok, &[], present_bad, &[]); + self.builder.switch_to_block(present_bad); + self.graphics_close_session(session)?; + self.graphics_finish(presented, result_slot, result_var, done, span)?; + self.builder.switch_to_block(present_ok); + if let LoweredValue::Owned(_, slot) = presented { + self.drop_slot(slot)?; + } + if let LoweredValue::Owned(_, slot) = scene { + self.drop_slot(slot)?; + } + self.builder.ins().jump(loop_head, &[]); + + self.builder.switch_to_block(done); + let result = self.builder.use_var(result_var); + Ok(LoweredValue::Owned(result, result_slot)) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..ae259983 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums.rs @@ -0,0 +1,239 @@ +//! Nonconsuming ordinary sum payloads require their original checked Handle proof. +use super::*; +use jett_mir::{BlockId, OrdinaryBorrowedSumProjection, OrdinarySumPayloadPath}; + +#[cfg(test)] +mod tests; + +/// Public admission checks the private current graph before any ordinary +/// initializer or consuming sum path can reinterpret a stale projection. +pub(super) fn validate_public_program( + program: &Program, + types: &TypeInterner, +) -> Result<(), CodegenError> { + let mut errors = Vec::new(); + for function in &program.functions { + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + let result = validate_statement(function, types, block.id, index, statement) + .and_then(|()| match statement.kind { + StatementKind::SumTag { source, .. } + | StatementKind::SumTake { source, .. } + if function.is_view_local(source) => + { + function + .ordinary_borrowed_sum_projection(block.id, index)? + .ok_or_else(|| { + "borrowed ordinary sum has no original projection proof".into() + }) + .map(|_| ()) + } + _ => Ok(()), + }); + if let Err(message) = result { + errors.push(jett_mir::ValidationError { + span: statement.span, + message, + }); + } + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(CodegenError::InvalidMir(errors)) + } +} + +pub(crate) fn validate_statement( + function: &Function, + types: &TypeInterner, + block: BlockId, + index: usize, + statement: &Statement, +) -> Result<(), String> { + match &statement.kind { + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } => { + if let Some(row) = function.ordinary_borrowed_sum_projection(block, index)? { + validate_types(function, types, row)?; + } + } + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + if function.ordinary_borrowed_sum_alias(*local, value)? + && function.local(*local).is_none() + { + return Err("ordinary sum alias lost its exact local header".into()); + } + } + _ => {} + } + Ok(()) +} + +fn validate_types( + function: &Function, + types: &TypeInterner, + row: &OrdinaryBorrowedSumProjection, +) -> Result<(), String> { + let source = function + .local(row.source()) + .ok_or("ordinary sum source is absent")?; + let backing = function + .local(row.backing()) + .ok_or("ordinary sum backing is absent")?; + let output = function + .local(row.output()) + .ok_or("ordinary sum output is absent")?; + let tag = function + .local(row.tag()) + .ok_or("ordinary sum tag is absent")?; + if source.ty != row.sum_type() + || backing.ty != row.sum_type() + || output.ty != row.payload_type() + || tag.ty != TypeInterner::BOOL + || !function.is_view_local(row.source()) + || !function.is_view_local(row.output()) + || source.view_source != Some(row.backing()) + || output.view_source != Some(row.backing()) + { + return Err("ordinary sum projection changed its exact types or backing origin".into()); + } + match (types.resolve(row.sum_type()), row.path()) { + (Type::Optional(payload), OrdinarySumPayloadPath::OptionalSome) + if *payload == row.payload_type() && row.error().is_none() => {} + (Type::Result(payload, error), OrdinarySumPayloadPath::ResultOk) + if *payload == row.payload_type() && *error == TypeInterner::STRING => + { + let error = row + .error() + .and_then(|local| function.local(local)) + .ok_or("ordinary Result projection lost its String companion")?; + if error.ty != TypeInterner::STRING || function.is_view_local(error.id) { + return Err("ordinary Result failure must own one String copy".into()); + } + } + _ => return Err("ordinary sum projection changed its selected nominal payload".into()), + } + Ok(()) +} + +impl Translator<'_, '_> { + pub(super) fn ordinary_borrowed_sum_statement( + &mut self, + block: BlockId, + index: usize, + statement: &Statement, + ) -> Result { + let symbol = self.symbol; + let fail = |message| contract_error(symbol, statement.span, message); + match &statement.kind { + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + if self + .function + .ordinary_borrowed_sum_alias(*local, value) + .map_err(fail)? + { + // The original header selects either a nonowning alias or + // an ordinary copyable final binding. Projected bits stay + // borrowed until that exact destination is initialized. + if self.local_types[local.index() as usize] + .view_source + .is_some() + && self.local_slots[local.index() as usize].is_some() + { + return Err(fail( + "ordinary borrowed payload gained owning storage".into(), + )); + } + let value = self.argument(value, true)?; + self.define_local(*local, value, statement.span)?; + return Ok(true); + } + return Ok(false); + } + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } => {} + _ => return Ok(false), + } + let Some(row) = self + .function + .ordinary_borrowed_sum_projection(block, index) + .map_err(fail)? + else { + return Ok(false); + }; + validate_types(self.function, self.types, row).map_err(fail)?; + let sum_type = row.sum_type(); + let source = Expression { + kind: ExpressionKind::Local(row.source()), + ty: sum_type, + span: statement.span, + }; + let value = self.argument(&source, true)?; + let value = self.scalar(value, statement.span)?; + match statement.kind { + StatementKind::SumTag { target, .. } => { + let layout = debug::debug_layout(self.types, sum_type) + .ok_or_else(|| self.unsupported(statement.span, "ordinary sum debug layout"))?; + let (pointer, length) = self.static_data(&layout)?; + let tag = self.leaf(NativeLeaf::SumHandleTag, &[value, pointer, length], true)?; + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(target, LoweredValue::Scalar(tag), statement.span)?; + } + StatementKind::SumTake { + target, success, .. + } => { + if success && self.local_slots[target.index() as usize].is_some() { + return Err(fail( + "ordinary success payload gained owning storage".into(), + )); + } + if !success && self.local_slots[target.index() as usize].is_none() { + return Err(fail("ordinary failure String lost owning storage".into())); + } + let tag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(success)); + let depth = self.leaf(NativeLeaf::SumPayloadPendingDepth, &[value], true)?; + let bits = self.leaf(NativeLeaf::SumPayloadBorrow, &[value, tag], true)?; + let ty = self.local_types[target.index() as usize].ty; + let value = self.ordinary_borrowed_payload(bits, ty, depth, statement.span)?; + self.define_local(target, value, statement.span)?; + } + _ => unreachable!("projection query admits only tag and payload rows"), + } + Ok(true) + } + + /// Keep opaque carriers borrowed; only primitive payload bits need a native + /// bitcast or width reduction. No temporary owner is created here. + fn ordinary_borrowed_payload( + &mut self, + bits: Value, + ty: TypeId, + depth: Value, + span: Span, + ) -> Result { + let native = self.required_clif_type(ty, span)?; + let value = if native == ir::types::F64 { + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native == ir::types::F32 { + let bits = self.builder.ins().ireduce(ir::types::I32, bits); + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native != ir::types::I64 { + self.builder.ins().ireduce(native, bits) + } else { + bits + }; + if is_task_scalar(self.types, ty)? { + Ok(LoweredValue::ScalarTask(value, depth)) + } else { + Ok(LoweredValue::Scalar(value)) + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs new file mode 100644 index 00000000..e3754b80 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/ordinary_borrowed_sums/tests.rs @@ -0,0 +1,518 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const SOURCE: &str = r#"namespace app +function project_optional(view source: optional[list[int64]]) returns int64: + list[int64] alias = view((view source) handle: + return 0 + ) + list[int64] forwarded = alias + return 1 +function project_result(view source: result[list[int64], string]) returns int64: + list[int64] alias = view((view source) handle error: + return 0 + ) + return 1 +function main() returns nothing: + optional[list[int64]] present = some(list(2, 3)) + optional[list[int64]] absent = none + result[list[int64], string] success = ok(list(5, 7)) + result[list[int64], string] failure = fail("absent") + int64 first = project_optional(view present) + int64 second = project_optional(view absent) + int64 third = project_result(view success) + int64 fourth = project_result(view failure) + int64 combined = first + second + third + fourth + return nothing +"#; + +fn lower(source: &str, release: bool) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let mut hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + jett_hir::complete_value_conversions(&mut hir, &checked.interner).unwrap(); + let mir = jett_mir::lower(&hir, &checked.interner).unwrap(); + (mir, checked.interner) +} + +fn object( + program: &Program, + types: &TypeInterner, + release: bool, +) -> Result { + let entry = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() + .id; + crate::emit_host_program_object_with_options( + program, + types, + entry, + CodegenOptions { optimize: release }, + ) +} + +fn projection(program: &Program, name: &str) -> (FunctionId, BlockId, usize) { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + && function + .ordinary_borrowed_sum_projection(block.id, index) + .unwrap() + .is_some() + { + return (function.id, block.id, index); + } + } + } + panic!("genuine {name} has no projected success"); +} + +/// Public lowering constructs an ordinary empty seed. Copying editable fields +/// into it cannot mint the private original borrowed-Handle proof. +fn without_proof(original: &Function, release: bool) -> Function { + let (mut seed, _) = lower( + "namespace app\nfunction main() returns nothing:\n return nothing\n", + release, + ); + let mut function = seed.functions.remove(0); + function.id = original.id; + function.identity = original.identity.clone(); + function.debug_kind = original.debug_kind.clone(); + function.params = original.params.clone(); + function.capture_count = original.capture_count; + function.return_type = original.return_type; + function.locals = original.locals.clone(); + function.entry = original.entry; + function.blocks = original.blocks.clone(); + function.span = original.span; + function +} + +#[derive(Debug, Clone, Copy)] +enum Mutation { + Copy, + Retarget, + Source, + OutputOrigin, + PayloadType, + Guard, + MissingProof, +} + +#[test] +fn ordinary_borrowed_sum_public_object_rejects_forged_current_projections() { + for release in [false, true] { + let (baseline, types) = lower(SOURCE, release); + let emitted = object(&baseline, &types, release).expect("genuine checked Source object"); + assert!(!emitted.bytes.is_empty()); + let (function, block, index) = projection(&baseline, "project_optional"); + for mutation in [ + Mutation::Copy, + Mutation::Retarget, + Mutation::Source, + Mutation::OutputOrigin, + Mutation::PayloadType, + Mutation::Guard, + Mutation::MissingProof, + ] { + let mut forged = baseline.clone(); + let function = &mut forged.functions[function.index() as usize]; + let row = function + .ordinary_borrowed_sum_projection(block, index) + .unwrap() + .unwrap(); + let output = row.output(); + let tag = row.tag(); + match mutation { + Mutation::Copy => { + let copy = function.blocks[block.index() as usize].statements[index].clone(); + function.blocks[block.index() as usize] + .statements + .insert(index, copy); + } + Mutation::Retarget => { + let moved = function.blocks[block.index() as usize] + .statements + .remove(index); + function.blocks[function.entry.index() as usize] + .statements + .insert(0, moved); + } + Mutation::Source => { + let StatementKind::SumTake { source, .. } = + &mut function.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = output; + } + Mutation::OutputOrigin => { + function.locals[output.index() as usize].view_source = None + } + Mutation::PayloadType => { + function.locals[output.index() as usize].ty = TypeInterner::INT64 + } + Mutation::Guard => { + let branch = function.blocks.iter_mut().find(|block| + matches!(&block.terminator.kind, TerminatorKind::Branch { condition, .. } + if matches!(condition.kind, ExpressionKind::Local(local) if local == tag))).unwrap(); + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + else { + unreachable!() + }; + std::mem::swap(then_block, else_block); + } + Mutation::MissingProof => *function = without_proof(function, release), + } + match object(&forged, &types, release) { + Err(CodegenError::InvalidMir(errors)) => { + assert!(!errors.is_empty(), "{mutation:?} release={release}") + } + result => panic!( + "{mutation:?} release={release}: public emission did not reject private-proof corruption: {result:?}" + ), + } + } + } +} + +fn translated( + program: &Program, + types: &TypeInterner, + name: &str, +) -> (ir::Function, HashMap) { + let mut prepared = program.clone(); + jett_mir::prepare_native_sequences(&mut prepared, types); + jett_mir::prepare_native_uninhabited_sums(&mut prepared, types); + jett_mir::prepare_native_generated_functions(&mut prepared, types); + let verified = verify_program(&prepared, types).unwrap(); + let flags = settings::Flags::new(settings::builder()); + let isa = isa::lookup(HOST).unwrap().finish(flags).unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new( + isa, + b"ordinary-projection-test".to_vec(), + default_libcall_names(), + ) + .unwrap(), + ); + let declarations = + declare_reachable_functions(&mut module, &prepared, types, &verified).unwrap(); + let function = prepared + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let declaration = declarations.get(function.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function( + &mut module, + &declarations, + &prepared, + function, + types, + &declaration.symbol, + &mut context, + ) + .unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected import name") + }; + let name = &context.func.params.user_named_funcs()[user]; + ( + reference, + module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("declared runtime symbol"), + ) + }) + .collect(); + (context.func, names) +} + +fn calls(function: &ir::Function, names: &HashMap) -> Vec<(ir::Inst, String)> { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter_map(|instruction| { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names + .get(&func_ref) + .cloned() + .map(|name| (instruction, name)) + }) + .collect() +} + +#[test] +fn ordinary_borrowed_sum_emission_keeps_success_borrowed_and_copies_only_failure_string() { + for release in [false, true] { + let (program, types) = lower(SOURCE, release); + object(&program, &types, release).expect("checked public baseline"); + for (name, copies) in [("project_optional", 0), ("project_result", 1)] { + let (function, names) = translated(&program, &types, name); + let calls = calls(&function, &names); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_handle_tag") + ); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + ); + assert_eq!( + calls + .iter() + .filter(|(_, name)| name == NativeLeaf::Retain.symbol()) + .count(), + copies, + "{name} release={release} calls={calls:?}\n{}", + function.display() + ); + assert!( + !calls.iter().any( + |(_, name)| name == "jett_rt_v1_sum_take" || name == "jett_rt_v1_sum_clone" + ) + ); + // The payload leaf consumes the borrowed reader's opaque bits and + // never clears/stores its source slot; View formals have no slot. + let takes = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + .count(); + assert_eq!(takes, if copies == 0 { 1 } else { 2 }); + } + } +} + +#[test] +fn ordinary_borrowed_sum_scalar_width_and_pending_metadata_emit_without_owner_adoption() { + for release in [false, true] { + for (ty, literal, opcode) in [ + ("uint8", "7", ir::Opcode::Ireduce), + ("int16", "7", ir::Opcode::Ireduce), + ("float32", "1.5", ir::Opcode::Bitcast), + ("float64", "1.5", ir::Opcode::Bitcast), + ("int64", "7", ir::Opcode::Iconst), + ] { + let source = format!( + "namespace app\nfunction payload(view source: optional[{ty}]) returns {ty}:\n {ty} alias = view((view source) handle:\n return {literal}\n )\n return alias\nfunction main() returns nothing:\n {ty} value = {literal}\n optional[{ty}] source = some(run run value)\n {ty} read = payload(view source)\n return nothing\n" + ); + let (program, types) = lower(&source, release); + object(&program, &types, release) + .unwrap_or_else(|error| panic!("{ty} release={release}: {error:?}")); + let (function, names) = translated(&program, &types, "payload"); + let calls = calls(&function, &names); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_pending_depth") + ); + assert!( + calls + .iter() + .any(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + ); + assert!( + !calls + .iter() + .any(|(_, name)| name == NativeLeaf::Retain.symbol() + || name == "jett_rt_v1_sum_take") + ); + assert!( + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .any(|instruction| function.dfg.insts[instruction].opcode() == opcode) + ); + assert_eq!( + function.signature.returns.len(), + 2, + "primitive pending depth remains in the actual ABI" + ); + let depths = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_pending_depth") + .map(|(instruction, _)| function.dfg.inst_results(*instruction)[0]) + .collect::>(); + assert!( + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::Return) + .any( + |instruction| function.dfg.inst_args(instruction).get(1).is_some_and( + |depth| depths + .iter() + .any(|original| function.dfg.resolve_aliases(*depth) + == function.dfg.resolve_aliases(*original)) + ) + ), + "{ty}: actual payload pending depth reaches the return; it is not reset to zero" + ); + } + } +} + +const STRING_SUCCESS: &str = r#"namespace app +function copied_optional(view source: optional[string]) returns string: + string alias = view((view source) handle: + return "missing" + ) + return alias +function copied_result(view source: result[string, string]) returns string: + string alias = view((view source) handle error: + return error + ) + return alias +function main() returns nothing: + optional[string] optional_source = some("optional") + result[string, string] result_source = ok("result") + string first = copied_optional(view optional_source) + string second = copied_optional(view optional_source) + string third = copied_result(view result_source) + string fourth = copied_result(view result_source) + return nothing +"#; + +fn integer_constant(function: &ir::Function, value: Value) -> Option { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + return None; + }; + match function.dfg.insts[instruction] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} + +#[test] +fn ordinary_borrowed_sum_string_success_copies_final_binding_once_from_nonowning_projection() { + for release in [false, true] { + let (program, types) = lower(STRING_SUCCESS, release); + let emitted = object(&program, &types, release) + .expect("genuine checked String-success public object"); + assert!(!emitted.bytes.is_empty()); + for name in ["copied_optional", "copied_result"] { + let (id, block, index) = projection(&program, name); + let source = &program.functions[id.index() as usize]; + let row = source + .ordinary_borrowed_sum_projection(block, index) + .unwrap() + .unwrap(); + let final_alias = source.local(row.alias()).unwrap(); + assert_eq!(final_alias.ty, TypeInterner::STRING); + assert_eq!( + final_alias.view_source, None, + "preserve the checked String copy header" + ); + let storage = MoveValuePlan::analyze(&program, source, &types).unwrap(); + assert!( + storage + .owned_locals + .contains(&(row.alias().index() as usize)) + ); + assert!( + !storage + .owned_locals + .contains(&(row.output().index() as usize)), + "projected temporary must remain nonowning" + ); + assert_eq!( + source.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + + let (function, names) = translated(&program, &types, name); + let calls = calls(&function, &names); + assert!( + !calls.iter().any( + |(_, name)| name == "jett_rt_v1_sum_take" || name == "jett_rt_v1_sum_clone" + ) + ); + let success = calls + .iter() + .filter(|(_, name)| name == "jett_rt_v1_sum_payload_borrow") + .filter(|(instruction, _)| { + integer_constant(&function, function.dfg.inst_args(*instruction)[2]) == Some(1) + }) + .map(|(instruction, _)| function.dfg.inst_results(*instruction)[0]) + .collect::>(); + assert_eq!(success.len(), 1, "one guarded success projection"); + let payload = function.dfg.resolve_aliases(success[0]); + let alias_copies = calls + .iter() + .filter(|(_, name)| name == NativeLeaf::Retain.symbol()) + .filter(|(instruction, _)| { + function + .dfg + .resolve_aliases(function.dfg.inst_args(*instruction)[1]) + == payload + }) + .count(); + assert_eq!( + alias_copies, + 1, + "the final String binding retains exactly once directly from borrowed bits; later Return copies are distinct; calls={calls:?}\n{}", + function.display() + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution.rs new file mode 100644 index 00000000..4cf29762 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution.rs @@ -0,0 +1,2290 @@ +//! Dedicated Source family. The layout constructor owns all entry and custody decisions. +use super::resource_layout::{EmittedResourceLayout, pending}; +use super::*; +use jett_mir::{ + ResourceCallOperand as Operand, ResourceCallResult as CallResult, + ResourceFrameRole as FrameRole, ResourceOperationRole as Role, ResourceSlotStorage as Storage, +}; +mod carriers; +mod leaves; +mod lexical_borrows; +mod named_indirect; +mod native_suite; +mod replacement; +mod returned_hooks; +mod sum_views; +#[cfg(test)] +mod tests; +use leaves::Leaf; + +#[derive(Clone, Copy)] +pub(super) struct ResourceEmission<'a> { + pub(super) layout: &'a EmittedResourceLayout<'a>, + pub(super) function: FunctionId, + pub(super) scope: Variable, + pub(super) status: Variable, + pub(super) frames: &'a [ir::StackSlot], + pub(super) owners: &'a [ir::StackSlot], + pub(super) return_companion: ir::StackSlot, + pub(super) scope_finished: ir::StackSlot, + pub(super) loans: &'a [ir::StackSlot], + pub(super) carrier_owners: &'a [ir::StackSlot], + pub(super) carrier_builders: &'a [ir::StackSlot], + pub(super) carrier_loans: &'a [ir::StackSlot], + pub(super) failure: ir::Block, + pub(super) block: jett_mir::BlockId, + pub(super) position: jett_mir::ResourcePosition, +} +impl<'a> ResourceEmission<'a> { + pub(in crate::emit) fn function_plan( + self, + ) -> Result<&'a jett_mir::ResourceFunctionPlan, CodegenError> { + self.layout + .plan() + .function(self.function) + .ok_or_else(|| pending("native family lost its exact function plan")) + } + fn at_site(self) -> Result, CodegenError> { + Ok(self + .function_plan()? + .operations() + .iter() + .filter(|operation| { + operation.site().block() == self.block + && operation.site().position() == self.position + }) + .collect()) + } + pub(in crate::emit) fn ordinal( + self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + self.layout + .operation(self.function, operation.id()) + .ok_or_else(|| pending("native operation has no exact installed row")) + } + fn local_slot( + self, + local: jett_mir::LocalId, + ) -> Result, CodegenError> { + Ok(self + .function_plan()? + .owner_slots() + .iter() + .find_map(|slot| match slot.storage() { + Storage::Local { header } if header.id == local => Some(slot.id()), + _ => None, + })) + } +} + +pub(super) fn clif_type( + layout: Option<&EmittedResourceLayout<'_>>, + types: &TypeInterner, + ty: TypeId, + role: &str, +) -> Result, CodegenError> { + if layout.is_some_and(|layout| layout.plan().type_requires_custody(ty)) { + Ok(Some(ir::types::I64)) + } else { + super::clif_type(types, ty, role) + } +} +pub(super) fn task_scalar( + layout: Option<&EmittedResourceLayout<'_>>, + types: &TypeInterner, + ty: TypeId, +) -> Result { + if layout.is_some_and(|layout| layout.plan().type_requires_custody(ty)) { + Ok(false) + } else { + is_task_scalar(types, ty) + } +} +fn signature( + module: &ObjectModule, + function: &Function, + types: &TypeInterner, + layout: &EmittedResourceLayout<'_>, +) -> Result { + if layout.plan().function(function.id).is_none() { + return super::signature(module, function, types); + } + let mut signature = module.make_signature(); + signature.params.extend([ + AbiParam::new(ir::types::I64), + AbiParam::new(ir::types::I64), + AbiParam::new(ir::types::I64), + ]); + for parameter in &function.params { + if let Some(ty) = clif_type( + Some(layout), + types, + parameter.ty, + "Resource-family parameter", + )? { + signature.params.push(AbiParam::new(ty)); + if task_scalar(Some(layout), types, parameter.ty)? { + signature.params.push(AbiParam::new(ir::types::I64)); + } + } + } + if let Some(ty) = clif_type( + Some(layout), + types, + function.return_type, + "Resource-family return", + )? { + signature.returns.push(AbiParam::new(ty)); + if task_scalar(Some(layout), types, function.return_type)? { + signature.returns.push(AbiParam::new(ir::types::I64)); + } + } + Ok(signature) +} + +pub(super) fn emit( + program: &Program, + types: &TypeInterner, + target: Triple, + entry: Option, + options: CodegenOptions, +) -> Result { + let entry = entry.ok_or_else(|| { + pending("Resource executable requires the exact driver-selected entry FunctionId") + })?; + jett_mir::validate(program).map_err(CodegenError::InvalidMir)?; + let original_plan = jett_mir::validate_resource_ownership_for_entry(program, types, entry) + .map_err(CodegenError::InvalidMir)?; + for function in &program.functions { + jett_mir::move_values::validate_local_view_initializers(function, types).map_err( + |message| contract_error(&function.identity.declaration.name, function.span, message), + )?; + } + crate::verify::verify_resource_descriptor_bodies(&original_plan)?; + let mut prepared = program.clone(); + jett_mir::prepare_native_sequences(&mut prepared, types); + jett_mir::prepare_native_uninhabited_sums(&mut prepared, types); + jett_mir::prepare_native_generated_functions(&mut prepared, types); + let program = &prepared; + let layout = EmittedResourceLayout::from_program(program, types, entry)?; + let verified = crate::verify::verify_resource_program(layout.plan())?; + let mut flags = settings::builder(); + if options.optimize { + use cranelift_codegen::settings::Configurable; + flags + .set("opt_level", "speed") + .map_err(|error| CodegenError::Backend(error.to_string()))?; + } + let isa = isa::lookup(target.clone()) + .map_err(|error| CodegenError::Backend(error.to_string()))? + .finish(settings::Flags::new(flags)) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, b"jett-resource".to_vec(), default_libcall_names()) + .map_err(|error| CodegenError::Backend(error.to_string()))?, + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for verified in verified.functions() { + let function = program_function(program, verified.mir_id)?; + let signature = signature(&module, function, types, &layout)?; + let native_id = module + .declare_function(&verified.symbol, Linkage::Local, &signature) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + declarations.insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: verified.symbol.clone(), + signature, + modes: function.params.iter().map(|param| param.mode).collect(), + parameter_types: function.params.iter().map(|param| param.ty).collect(), + debug_label: debug::function_label(function).map_err(|message| pending(message))?, + })?; + } + for declaration in declarations.iter() { + let function = program_function(program, declaration.mir_id)?; + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + let selected = layout + .plan() + .function(function.id) + .is_some() + .then_some(&layout); + translate_function_with_storage( + &mut module, + &declarations, + program, + function, + types, + &declaration.symbol, + &mut context, + selected, + Some(layout.plan()), + )?; + module + .define_function(declaration.native_id, &mut context) + .map_err(|error| { + CodegenError::Backend(format!("Resource-family {}: {error:?}", declaration.symbol)) + })?; + } + layout.define_accessor(&mut module)?; + define_entry(&mut module, &declarations, &layout)?; + let mut symbols = declarations.symbols(); + symbols.push("jett_aot_resource_v1_manifest".into()); + symbols.push(JETT_AOT_ENTRY_SYMBOL_V1.into()); + let bytes = module + .finish() + .emit() + .map_err(|error| CodegenError::Backend(error.to_string()))?; + Ok(ObjectArtifact { + target: target.to_string(), + symbols, + bytes, + }) +} + +impl Translator<'_, '_> { + pub(super) fn resource_validate_scope(&mut self) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("Scope prologue has no selected family"))?; + let scope = self.builder.use_var(resource.scope); + let context = self.builder.use_var(self.runtime_context); + let function = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.function.index())); + let signature = self.builder.ins().iconst( + ir::types::I32, + i64::from( + resource + .layout + .function_signature(resource.function) + .ok_or_else(|| pending("Scope lacks its containing function signature"))?, + ), + ); + let template = self.builder.ins().iconst( + ir::types::I32, + i64::from( + resource + .layout + .frame( + resource.function, + resource.function_plan()?.root_scope().id(), + ) + .ok_or_else(|| pending("Scope template missing"))?, + ), + ); + Leaf::ScopeValidate.checked( + self.module, + self.builder, + &[context, scope, function, signature, template], + resource.failure, + ) + } + pub(super) fn resource_bind_parameters( + &mut self, + function: &Function, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + for parameter in &function.params { + if let Some(slot) = resource.carrier_local_slot(parameter.local)? { + let variable = self.variables[parameter.local.index() as usize] + .ok_or_else(|| pending("carrier formal has no exact native variable"))?; + let value = self.builder.use_var(variable); + self.builder + .ins() + .stack_store(value, resource.carrier_owners[slot.index()], 0); + } + if let Some(slot) = resource.local_slot(parameter.local)? { + let variable = self.variables[parameter.local.index() as usize] + .ok_or_else(|| pending("Resource formal has no carrier variable"))?; + let value = self.builder.use_var(variable); + self.builder + .ins() + .stack_store(value, resource.owners[slot.index()], 0); + } + } + Ok(()) + } + fn resource_context(&mut self) -> Value { + self.builder.use_var(self.runtime_context) + } + fn resource_frame(&mut self, id: jett_mir::ResourceFrameId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("frame lookup has no fresh native family"))?; + let header = resource + .function_plan()? + .frames() + .get(id.index()) + .ok_or_else(|| pending("frame lookup is outside the fresh plan"))?; + match header.role() { + FrameRole::Scope | FrameRole::Return => Ok(self.builder.use_var(resource.scope)), + FrameRole::Operation => { + Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.frames[id.index()], 0)) + } + } + } + fn resource_owner(&mut self, id: jett_mir::ResourceOwnerSlotId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("owner lookup has no fresh native family"))?; + let slot = resource + .owners + .get(id.index()) + .copied() + .ok_or_else(|| pending("owner storage is outside the exact plan"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, slot, 0)) + } + fn resource_store( + &mut self, + id: jett_mir::ResourceOwnerSlotId, + value: Value, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("owner publication has no fresh native family"))?; + self.builder + .ins() + .stack_store(value, resource.owners[id.index()], 0); + Ok(()) + } + fn resource_operation( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(Value, Value, Value), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("operation has no fresh native family"))?; + let context = self.resource_context(); + let frame = self.resource_frame(operation.frame())?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + Ok((context, frame, ordinal)) + } + fn resource_destination( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("destination has no fresh native family"))?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + Leaf::DestinationFrame.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + ) + } + fn resource_transfer( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("transfer has no fresh native family"))?; + let (source, destination) = match operation.role() { + Role::Transfer { + source, + destination, + } => (*source, *destination), + _ => return Err(pending("transfer selected a different exact role")), + }; + let value = self.resource_owner(source)?; + let target = self.resource_destination(operation)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::Transfer.output( + self.module, + self.builder, + &[context, frame, ordinal, value, target], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.resource_store(destination, value)?; + Ok(value) + } + fn resource_loan(&mut self, id: jett_mir::ResourceLoanId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("loan lookup has no fresh native family"))?; + match resource.function_plan()?.loans()[id.index()].source() { + jett_mir::ResourceLoanSource::Owner(_) + | jett_mir::ResourceLoanSource::ProjectedSumPayload { .. } + | jett_mir::ResourceLoanSource::CarrierSumProjection { .. } => Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.loans[id.index()], 0)), + jett_mir::ResourceLoanSource::IncomingViewFormal { parameter, .. } => { + let header = &resource.function_plan()?.parameters()[parameter]; + let variable = self.variables[header.local.index() as usize] + .ok_or_else(|| pending("resident formal has no exact variable"))?; + Ok(self.builder.use_var(variable)) + } + } + } + fn resource_begin_borrow( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("borrow has no fresh native family"))?; + let (loan, leaf) = match operation.role() { + Role::Borrow { loan } => (loan, Leaf::BorrowBegin), + Role::BorrowSum { loan } => (loan, Leaf::SumBorrow), + _ => return Err(pending("borrow selected another role")), + }; + let jett_mir::ResourceLoanSource::Owner(source) = + resource.function_plan()?.loans()[loan.index()].source() + else { + return Err(pending( + "resident forwarding cannot create a child core lease", + )); + }; + let owner = self.resource_owner(source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(value, resource.loans[loan.index()], 0); + Ok(value) + } +} + +impl Translator<'_, '_> { + fn resource_open_operation( + &mut self, + frame: jett_mir::ResourceFrameId, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("operation has no selected family"))?; + let header = &resource.function_plan()?.frames()[frame.index()]; + if header.role() != FrameRole::Operation { + return Err(pending("invocation did not select its Operation frame")); + } + let parent = self.resource_frame( + header + .parent() + .ok_or_else(|| pending("Operation frame has no parent"))?, + )?; + let template = resource + .layout + .frame(resource.function, frame) + .ok_or_else(|| pending("Operation template missing"))?; + let template = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(template)); + let context = self.resource_context(); + let value = Leaf::OperationBegin.output( + self.module, + self.builder, + &[context, parent, template], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(value, resource.frames[frame.index()], 0); + Ok(()) + } + fn resource_drop( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("drop has no selected family"))?; + let Role::Drop { source, .. } = operation.role() else { + return Err(pending("drop selected another role")); + }; + let owner = self.resource_owner(*source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + // A plain owner and a conditional shell have separate runtime protocols. + let leaf = if !matches!( + resource.function_plan()?.owner_slots()[source.index()].shape(), + jett_mir::ResourceShape::Plain { .. } + ) { + Leaf::SumDrop + } else { + Leaf::Close + }; + leaf.checked( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + )?; + self.clear_slot(resource.owners[source.index()]); + Ok(()) + } + fn resource_end_borrow( + &mut self, + operation: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("loan retirement has no selected family"))?; + let (loan, leaf) = match operation.role() { + Role::EndBorrow { loan } => (*loan, Leaf::BorrowEnd), + Role::EndSumBorrow { loan } => (*loan, Leaf::SumBorrowEnd), + _ => { + return Err(pending( + "loan retirement selected a different operation role", + )); + } + }; + let value = self.resource_loan(loan)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + leaf.checked( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + )?; + self.clear_slot(resource.loans[loan.index()]); + Ok(()) + } + fn resource_finish_operation( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("completion has no selected family"))?; + for operation in operations + .iter() + .copied() + .filter(|operation| operation.frame() == frame) + { + match operation.role() { + Role::EndBorrow { .. } | Role::EndSumBorrow { .. } => { + self.resource_end_borrow(operation)?; + } + Role::Drop { .. } => self.resource_drop(operation)?, + Role::Carrier { .. } => self.carrier_finish_operation(operation)?, + Role::Complete { .. } => { + let (context, active, ordinal) = self.resource_operation(operation)?; + // Clear compiler storage before the fallible retirement. Runtime owns any refusal. + self.clear_slot(resource.frames[frame.index()]); + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder + .ins() + .stack_store(zero, resource.frames[frame.index()], 8); + Leaf::OperationComplete.checked( + self.module, + self.builder, + &[context, active, ordinal], + resource.failure, + )?; + } + _ => {} + } + } + Ok(()) + } + fn resource_stage_actual( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + parameter: usize, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("actual staging has no selected family"))?; + self.carrier_stage_actual(operations, frame, parameter)?; + for operation in operations + .iter() + .copied() + .filter(|operation| operation.frame() == frame) + { + match operation.role() { + Role::Transfer { destination, .. } if matches!(resource.function_plan()?.owner_slots()[destination.index()].storage(), Storage::Argument { parameter: selected, .. } if *selected == parameter) => + { + self.resource_transfer(operation)?; + } + Role::Borrow { loan } | Role::BorrowSum { loan } => { + let for_parameter = resource + .function_plan()? + .loans() + .get(loan.index()) + .is_some_and(|record| record.parameter() == Some(parameter)); + if for_parameter + && !matches!( + resource.function_plan()?.loans()[loan.index()].source(), + jett_mir::ResourceLoanSource::CarrierSumProjection { .. } + ) + { + self.resource_begin_borrow(operation)?; + } + } + _ => {} + } + } + Ok(()) + } + fn resource_operand( + &mut self, + operand: &Operand, + ordinary: &[Option], + _span: Span, + ) -> Result { + match operand { + Operand::CarrierOwned { slot, .. } => self.carrier_owner(*slot), + Operand::CarrierBorrowed { loan, .. } => self.carrier_loan(*loan), + Operand::Owned { slot, .. } => self.resource_owner(*slot), + Operand::Borrowed { loan, .. } => self.resource_loan(*loan), + Operand::Ordinary { parameter, .. } => { + let value = + ordinary.get(*parameter).copied().flatten().ok_or_else(|| { + pending("ordinary actual has not completed in source order") + })?; + Ok(self.payload_bits(value).0) + } + } + } + fn resource_prepared_call( + &mut self, + frame: jett_mir::ResourceFrameId, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("staged call has no selected family"))?; + Ok(self + .builder + .ins() + .stack_load(ir::types::I64, resource.frames[frame.index()], 8)) + } + + fn resource_staged_call( + &mut self, + node: &jett_mir::ResourceCallNode, + span: Span, + operations: &[&jett_mir::ResourceOperation], + ) -> Result<(), CodegenError> { + use jett_mir::ResourceCallNode as Node; + let resource = self + .resource + .ok_or_else(|| pending("staged call has no selected family"))?; + let region_id = match node { + Node::Begin { region } + | Node::Stage { region, .. } + | Node::Invoke { region, .. } + | Node::End { region, .. } => *region, + }; + let current = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .ok_or_else(|| pending("staged call current function is absent"))?; + let region = current + .resource_call_region(region_id) + .ok_or_else(|| pending("staged call lost its private constructor region"))?; + let mut preparations = resource.function_plan()?.operations().iter().filter(|operation| + matches!(operation.role(), Role::BeginSourceFunction { region, .. } if *region == region_id)); + let begin = preparations + .next() + .ok_or_else(|| pending("staged region has no complete prepared tuple"))?; + if preparations.next().is_some() { + return Err(pending("staged region has duplicate prepared tuples")); + } + let Role::BeginSourceFunction { + function, + formals, + result, + .. + } = begin.role() + else { + unreachable!() + }; + let context = self.resource_context(); + match node { + Node::Begin { .. } => { + if !operations + .iter() + .any(|operation| operation.id() == begin.id()) + { + return Err(pending("staged Begin differs from its exact current site")); + } + self.resource_open_operation(begin.frame())?; + let (context, frame, ordinal) = self.resource_operation(begin)?; + let call = Leaf::SourcePrepare.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(call, resource.frames[begin.frame().index()], 8); + Ok(()) + } + Node::Stage { + source_index, + parameter, + value, + ordinary, + .. + } => { + let mut stages = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::StageSourceActual { region, source_index: index, parameter: selected, ordinary: endpoint, .. } + if *region == region_id && index == source_index && selected == parameter && endpoint == ordinary)); + let stage = stages + .next() + .ok_or_else(|| pending("Stage has no exact reached-prefix role"))?; + if stages.next().is_some() || stage.frame() != begin.frame() { + return Err(pending( + "Stage has ambiguous or foreign operation authority", + )); + } + let Role::StageSourceActual { operand, .. } = stage.role() else { + unreachable!() + }; + let formal = formals + .iter() + .find(|formal| formal.parameter() == *parameter) + .ok_or_else(|| pending("Stage lost its sealed original formal"))?; + if formal.source_index() != *source_index { + return Err(pending("Stage changed its lexical/formal mapping")); + } + let evaluated = if resource.layout.plan().type_requires_custody(value.ty) { + self.expression(value)? + } else { + let borrowed = formal.access() == jett_mir::ParamMode::View + && formal.effect() != jett_mir::ResourceArgumentEffect::RelinquishOwned; + let binding = ordinary + .and_then(|local| self.caller_acquisitions.owner_initializer(local)) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, binding); + let evaluated = self.argument(value, borrowed); + self.taking_binding = previous; + evaluated? + }; + let mut ordinary_values = vec![None; formals.len()]; + if let Some(local) = ordinary { + self.define_local(*local, evaluated, span)?; + let endpoint = Expression { + kind: ExpressionKind::Local(*local), + ty: self.local_types[local.index() as usize].ty, + span, + }; + ordinary_values[*parameter] = Some(self.argument(&endpoint, true)?); + } + self.resource_stage_actual(operations, begin.frame(), *parameter)?; + let bits = self.resource_operand(operand, &ordinary_values, span)?; + let call = self.resource_prepared_call(begin.frame())?; + let index = self + .builder + .ins() + .iconst(ir::types::I32, *source_index as i64); + let parameter = self.builder.ins().iconst(ir::types::I32, *parameter as i64); + Leaf::SourceActual.checked( + self.module, + self.builder, + &[context, call, index, parameter, bits], + resource.failure, + ) + } + Node::Invoke { output, .. } => { + let mut invocations = operations.iter().copied().filter(|operation| + operation.frame() == begin.frame() && matches!(operation.role(), Role::InvokeSourceFunction { function: target, .. } if target == function)); + let invocation = invocations + .next() + .ok_or_else(|| pending("staged Invoke has no exact reached role"))?; + if invocations.next().is_some() + || region.invocation().map(|(_, local)| local) != Some(*output) + { + return Err(pending("staged Invoke changed its constructor output")); + } + // The writer independently joins the full Begin/Invoke tuple to one row. + resource.ordinal(invocation)?; + let call = self.resource_prepared_call(begin.frame())?; + let scope = Leaf::SourceEnter.output( + self.module, + self.builder, + &[context, call], + resource.failure, + 8, + )?; + let environment = self.builder.ins().iconst(ir::types::I64, 0); + let mut native = vec![context, environment, scope]; + for parameter in 0..formals.len() { + let formal = formals + .iter() + .find(|formal| formal.parameter() == parameter) + .ok_or_else(|| pending("staged Invoke lost a complete formal"))?; + let index = self.builder.ins().iconst(ir::types::I32, parameter as i64); + let installed = Leaf::SourceParameter.output( + self.module, + self.builder, + &[context, scope, index], + resource.failure, + 8, + )?; + let ty = formal.parameter_type(); + if resource.layout.plan().type_requires_custody(ty) { + native.push(installed); + } else { + let unpacked = self.unpack_payload(installed, ty, span)?; + native.push(self.scalar(unpacked, span)?); + let local = region + .actuals() + .iter() + .find(|actual| actual.parameter() == parameter) + .and_then(|actual| actual.ordinary()) + .ok_or_else(|| { + pending("staged ordinary formal has no current endpoint") + })?; + let endpoint = Expression { + kind: ExpressionKind::Local(local), + ty: self.local_types[local.index() as usize].ty, + span, + }; + if task_scalar(Some(resource.layout), self.types, ty)? { + let ordinary = self.argument(&endpoint, true)?; + native.push(self.scalar_task(ordinary, span)?.1); + } + if formal.access() == jett_mir::ParamMode::Owned + && is_linear(self.types, ty) + { + let slot = + self.local_slots[local.index() as usize].ok_or_else(|| { + pending("staged ordinary owned argument has no companion slot") + })?; + self.clear_slot(slot); + } + } + } + let declared = self.declarations.get(*function).ok_or_else(|| { + pending("staged Source target is outside its declared family") + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let native_call = self.builder.ins().call(reference, &native); + let returned = self.builder.inst_results(native_call).to_vec(); + Leaf::SourceStatus.checked( + self.module, + self.builder, + &[context, call], + resource.failure, + )?; + self.check_failure()?; + let bits = returned + .first() + .copied() + .ok_or_else(|| pending("staged Source result is absent"))?; + let opaque_output = resource.function_plan()?.descriptor_local(*output); + if let Some(hook) = opaque_output { + let selected = resource.layout.plan().function(*function).ok_or_else(|| { + pending("staged descriptor target has no exact Source plan") + })?; + if selected.descriptor_return() != Some(hook) + || self.local_types[output.index() as usize].ty != hook.function_type() + || !matches!(result, CallResult::Ordinary { ty } if *ty == hook.function_type()) + { + return Err(pending( + "staged descriptor output changes its exact callee Return or current endpoint", + )); + } + } + let lowered = match *result { + CallResult::Carrier { slot } => { + if resource.carrier_local_slot(*output)? != Some(slot) { + return Err(pending( + "staged carrier output differs from its exact Local slot", + )); + } + self.carrier_store(slot, bits)?; + LoweredValue::Scalar(bits) + } + CallResult::Owned { slot } => { + if resource.local_slot(*output)? != Some(slot) { + return Err(pending( + "staged owned output differs from its exact Local slot", + )); + } + self.resource_store(slot, bits)?; + LoweredValue::Scalar(bits) + } + CallResult::Ordinary { .. } if opaque_output.is_some() => { + LoweredValue::Scalar(bits) + } + CallResult::Ordinary { ty } + if task_scalar(Some(resource.layout), self.types, ty)? => + { + let depth = returned + .get(1) + .copied() + .ok_or_else(|| pending("staged primitive result lost Pending depth"))?; + LoweredValue::ScalarTask(bits, depth) + } + CallResult::Ordinary { ty } if is_linear(self.types, ty) => { + self.own_linear(bits)? + } + CallResult::Ordinary { ty } if is_copy_owned(self.types, ty) => { + self.own(bits)? + } + CallResult::Ordinary { .. } => LoweredValue::Scalar(bits), + }; + self.define_local(*output, lowered, span) + } + Node::End { outcome, .. } => { + let mut completions = operations.iter().copied().filter(|operation| + operation.frame() == begin.frame() && matches!(operation.role(), Role::Complete { outcome: selected } if selected == outcome)); + if completions.next().is_none() || completions.next().is_some() { + return Err(pending( + "staged End lost its exact normal/abandoned completion", + )); + } + self.resource_finish_operation(operations, begin.frame()) + } + } + } + + fn resource_invoke( + &mut self, + expression: &Expression, + args: &[Expression], + invocation: &jett_mir::ResourceOperation, + operations: &[&jett_mir::ResourceOperation], + descriptor: Option<&Expression>, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("invocation has no selected family"))?; + let (order, operands, result, source) = match invocation.role() { + Role::InvokeHook { + evaluation_order, + operands, + result, + source, + .. + } + | Role::InvokeSourceFunction { + evaluation_order, + operands, + result, + source, + .. + } => ( + evaluation_order.as_slice(), + operands.as_slice(), + *result, + source, + ), + _ => return Err(pending("invocation selected another exact role")), + }; + let indirect_hook = + descriptor.is_some() && matches!(invocation.role(), Role::InvokeHook { .. }); + self.resource_validate_named_invocation(invocation, descriptor)?; + self.resource_validate_hook_invocation(invocation, descriptor)?; + let opaque_result = self.resource_is_hook_descriptor(expression)?; + self.resource_open_operation(invocation.frame())?; + let physical = match invocation.role() { + Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkFactory => + { + operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Acquire { .. }) + }) + .ok_or_else(|| pending("Factory has no exact Acquire wire role"))? + } + Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::Finalize => + { + operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Close { .. }) + }) + .ok_or_else(|| pending("Finalize has no exact Close wire role"))? + } + _ => invocation, + }; + let (context, frame, ordinal) = self.resource_operation(physical)?; + let call = match invocation.role() { + Role::InvokeSourceFunction { .. } => Some(Leaf::SourcePrepare.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?), + Role::InvokeHook { .. } if !indirect_hook => { + let no_descriptor = self.builder.ins().iconst(ir::types::I64, 0); + Some(Leaf::HookPrepare.output( + self.module, + self.builder, + &[context, frame, ordinal, no_descriptor], + resource.failure, + 8, + )?) + } + Role::InvokeHook { .. } => None, + _ => unreachable!(), + }; + let mut evaluated = vec![None; args.len()]; + for ¶meter in order { + let argument = args + .get(parameter) + .ok_or_else(|| pending("exact invocation actual is absent"))?; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or_else(|| pending("invocation lost an original Source formal"))?; + let value = if resource.layout.plan().type_requires_custody(argument.ty) { + self.expression(argument)? + } else { + self.argument( + argument, + resource_argument_view(invocation, parameter, self.types)?, + )? + }; + evaluated[parameter] = Some(value); + self.resource_stage_actual(operations, invocation.frame(), parameter)?; + if matches!(invocation.role(), Role::InvokeSourceFunction { .. }) { + let operand = operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { + parameter: selected, + .. + } + | Operand::Owned { + parameter: selected, + .. + } + | Operand::Borrowed { + parameter: selected, + .. + } + | Operand::CarrierOwned { + parameter: selected, + .. + } + | Operand::CarrierBorrowed { + parameter: selected, + .. + } => *selected == parameter, + }) + .ok_or_else(|| pending("Source actual has no exact operand role"))?; + let bits = self.resource_operand(operand, &evaluated, argument.span)?; + let source_index = self + .builder + .ins() + .iconst(ir::types::I32, fact.source_index as i64); + let parameter = self.builder.ins().iconst(ir::types::I32, parameter as i64); + Leaf::SourceActual.checked( + self.module, + self.builder, + &[ + context, + call.expect("Source call was prepared"), + source_index, + parameter, + bits, + ], + resource.failure, + )?; + } + } + // Original indirect semantics evaluate the callable once after all actuals. + // Opaque hook metadata and ordinary named descriptors use distinct checks. + let hook_descriptor = if let Some(callee) = descriptor { + let lowered = self.expression(callee)?; + let descriptor = self.scalar(lowered, callee.span)?; + if !indirect_hook { + self.resource_validate_named_descriptor(descriptor, invocation, callee.span)?; + } + descriptor + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let call = if indirect_hook { + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(invocation)?)); + Leaf::HookPrepare.output( + self.module, + self.builder, + &[context, frame, ordinal, hook_descriptor], + resource.failure, + 8, + )? + } else { + call.expect("direct hook or Source call was prepared") + }; + let result_value = match invocation.role() { + Role::InvokeHook { hook, .. } => { + let find = |parameter| { + operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { + parameter: selected, + .. + } + | Operand::Owned { + parameter: selected, + .. + } + | Operand::Borrowed { + parameter: selected, + .. + } + | Operand::CarrierOwned { + parameter: selected, + .. + } + | Operand::CarrierBorrowed { + parameter: selected, + .. + } => *selected == parameter, + }) + .ok_or_else(|| pending("hook recipe lost an exact operand")) + }; + match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => { + let net = self.resource_operand(find(0)?, &evaluated, expression.span)?; + let label = self.resource_operand(find(1)?, &evaluated, expression.span)?; + let prepared = Leaf::FactoryPrepare.output( + self.module, + self.builder, + &[context, call, net], + resource.failure, + 8, + )?; + Leaf::FactoryCommit.output( + self.module, + self.builder, + &[context, prepared, net, label], + resource.failure, + 16, + )? + } + jett_types::ResourceKernelRecipe::NetworkBorrow => { + let net = self.resource_operand(find(0)?, &evaluated, expression.span)?; + let loan = self.resource_operand(find(1)?, &evaluated, expression.span)?; + Leaf::BorrowCommit.output( + self.module, + self.builder, + &[context, call, net, loan], + resource.failure, + 8, + )? + } + jett_types::ResourceKernelRecipe::Finalize => { + let close = operations + .iter() + .copied() + .find(|operation| { + operation.frame() == invocation.frame() + && matches!(operation.role(), Role::Close { .. }) + }) + .ok_or_else(|| pending("Finalize has no exact consumed owner role"))?; + let Role::Close { source, .. } = close.role() else { + unreachable!(); + }; + let owner = self.resource_owner(*source)?; + if indirect_hook { + Leaf::DescriptorClose.checked( + self.module, + self.builder, + &[context, call, owner], + resource.failure, + )?; + } else { + let (context, frame, ordinal) = self.resource_operation(close)?; + Leaf::Close.checked( + self.module, + self.builder, + &[context, frame, ordinal, owner], + resource.failure, + )?; + } + self.clear_slot(resource.owners[source.index()]); + self.builder.ins().iconst(ir::types::I64, 0) + } + } + } + Role::InvokeSourceFunction { function, .. } => { + let scope = Leaf::SourceEnter.output( + self.module, + self.builder, + &[context, call], + resource.failure, + 8, + )?; + let mut native = vec![context, self.builder.ins().iconst(ir::types::I64, 0), scope]; + for (parameter, argument) in args.iter().enumerate() { + let index = self.builder.ins().iconst(ir::types::I32, parameter as i64); + let installed = Leaf::SourceParameter.output( + self.module, + self.builder, + &[context, scope, index], + resource.failure, + 8, + )?; + if resource.layout.plan().type_requires_custody(argument.ty) { + native.push(installed); + } else { + let unpacked = + self.unpack_payload(installed, argument.ty, argument.span)?; + native.push(self.scalar(unpacked, argument.span)?); + if task_scalar(Some(resource.layout), self.types, argument.ty)? { + native.push( + self.scalar_task( + evaluated[parameter].ok_or_else(|| { + pending("ordinary Source argument is absent") + })?, + argument.span, + )? + .1, + ); + } + if !resource_argument_view(invocation, parameter, self.types)? + && is_linear(self.types, argument.ty) + { + if let Some(LoweredValue::Owned(_, slot)) = evaluated[parameter] { + self.clear_slot(slot); + } + } + } + } + let declared = self + .declarations + .get(*function) + .ok_or_else(|| pending("Source family callee is absent"))?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let native_call = self.builder.ins().call(reference, &native); + let results = self.builder.inst_results(native_call).to_vec(); + Leaf::SourceStatus.checked( + self.module, + self.builder, + &[context, call], + resource.failure, + )?; + self.check_failure()?; + let bits = results + .first() + .copied() + .ok_or_else(|| pending("Source family call has no domain result"))?; + // Primitive Pending depth remains part of the ordinary result ABI. + if task_scalar(Some(resource.layout), self.types, expression.ty)? { + let depth = results.get(1).copied().ok_or_else(|| { + pending("ordinary Source primitive lost its Pending depth") + })?; + self.resource_finish_operation(operations, invocation.frame())?; + return Ok(LoweredValue::ScalarTask(bits, depth)); + } + bits + } + _ => unreachable!(), + }; + let lowered = match result { + CallResult::Carrier { slot } => { + self.carrier_store(slot, result_value)?; + LoweredValue::Scalar(result_value) + } + CallResult::Owned { slot } => { + self.resource_store(slot, result_value)?; + LoweredValue::Scalar(result_value) + } + CallResult::Ordinary { .. } if opaque_result => LoweredValue::Scalar(result_value), + CallResult::Ordinary { ty } if is_linear(self.types, ty) => { + self.own_linear(result_value)? + } + CallResult::Ordinary { ty } if is_copy_owned(self.types, ty) => { + self.own(result_value)? + } + CallResult::Ordinary { .. } => LoweredValue::Scalar(result_value), + }; + self.resource_finish_operation(operations, invocation.frame())?; + Ok(lowered) + } + pub(super) fn resource_expression( + &mut self, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(resource) = self.resource else { + return Ok(None); + }; + let operations = resource + .function_plan()? + .operations_for_expression(expression) + .collect::>(); + let invocation = operations.iter().copied().find(|operation| { + matches!( + operation.role(), + Role::InvokeHook { .. } + | Role::InvokeSourceFunction { .. } + | Role::InvokeNativeSuite { .. } + ) + }); + if let Some(invocation) = invocation { + if matches!(invocation.role(), Role::InvokeNativeSuite { .. }) { + return self + .resource_native_suite(expression, invocation, &operations) + .map(Some); + } + let (args, descriptor) = match &expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::ResourceInvoke { args, .. } => { + (args.as_slice(), None) + } + ExpressionKind::IndirectCall { callee, args, .. } => { + (args.as_slice(), Some(callee.as_ref())) + } + _ => return Err(pending("exact invocation role changed its expression kind")), + }; + return self + .resource_invoke(expression, args, invocation, &operations, descriptor) + .map(Some); + } + if operations.iter().any(|operation| { + operation.is_expression_operation() + && matches!( + operation.role(), + Role::Complete { + outcome: jett_mir::ResourceCompletion::Abort + } + ) + && resource.function_plan().is_ok_and(|plan| { + plan.frames()[operation.frame().index()].role() == FrameRole::Operation + }) + }) { + return Err(pending( + "statically aborted Resource actual needs its exact custody CFG emitter", + )); + } + if let Some(value) = self.carrier_expression(expression)? { + return Ok(Some(value)); + } + if self.resource_is_hook_descriptor(expression)? { + return self.resource_hook_value(expression, &operations).map(Some); + } + // Current constructor lowering can replace a protected original aggregate + // with an exact Commit Local. Its fresh physical expression proof keeps + // ordinary ownership transport; Source type alone never selects it. + if jett_mir::native_graph_storage_for_expression(self.function, self.types, expression) + .is_some() + || jett_mir::native_graph_metadata_expression(self.function, self.types, expression) + { + return Ok(None); + } + if self.resource_is_named_value(expression)? + || !resource.layout.plan().type_requires_custody(expression.ty) + { + return Ok(None); + } + let value = match &expression.kind { + ExpressionKind::Local(local) => { + if let Some(slot) = resource.local_slot(*local)? { + self.resource_owner(slot)? + } else { + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("resident Resource has no exact native variable"))?; + self.builder.use_var(variable) + } + } + ExpressionKind::View(inner) => return self.expression(inner).map(Some), + ExpressionKind::ResourceHookValue { .. } => { + return Err(pending("hook value has no exact current descriptor proof")); + } + ExpressionKind::OptionalSome(inner) | ExpressionKind::ResultOk(inner) => { + self.expression(inner)?; + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::SumAdopt { .. })) + .ok_or_else(|| pending("occupied Resource sum lost its exact adoption"))?; + let Role::SumAdopt { + source, + destination, + } = operation.role() + else { + unreachable!(); + }; + let owner = self.resource_owner(*source)?; + let target = self.resource_destination(operation)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::SumAdopt.output( + self.module, + self.builder, + &[context, frame, ordinal, owner, target], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.resource_store(*destination, value)?; + value + } + ExpressionKind::OptionalNone => { + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CreateAbsentSum { .. })) + .ok_or_else(|| pending("absent Resource sum lost its exact constructor"))?; + let Role::CreateAbsentSum { destination } = operation.role() else { + unreachable!(); + }; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let value = Leaf::AbsentSum.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.resource_store(*destination, value)?; + value + } + ExpressionKind::ResultFail(inner) => { + let evaluated = self.expression(inner)?; + let operation = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CreateFailureSum { .. })) + .ok_or_else(|| { + pending("Resource Fail lost its exact ordinary companion constructor") + })?; + let Role::CreateFailureSum { destination, .. } = operation.role() else { + unreachable!(); + }; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let owned = + if is_copy_owned(self.types, inner.ty) || is_linear(self.types, inner.ty) { + self.own_copy_value(evaluated, inner.ty, inner.span)? + } else { + evaluated + }; + let (companion, _) = self.payload_bits(owned); + let value = Leaf::FailureSum.output( + self.module, + self.builder, + &[context, frame, ordinal, companion], + resource.failure, + 8, + )?; + if let LoweredValue::Owned(_, slot) = owned { + self.clear_slot(slot); + } + self.resource_store(*destination, value)?; + value + } + _ => { + return Err(pending( + "native Resource expression requires its next finite custody emitter", + )); + } + }; + Ok(Some(LoweredValue::Scalar(value))) + } +} + +fn resource_argument_view( + invocation: &jett_mir::ResourceOperation, + parameter: usize, + types: &TypeInterner, +) -> Result { + match invocation.role() { + Role::InvokeHook { hook, .. } => match types.resolve(hook.function_type()) { + Type::Function { view_params, .. } => view_params + .get(parameter) + .copied() + .ok_or_else(|| pending("hook physical access has no exact formal")), + _ => Err(pending("hook has no exact checked physical signature")), + }, + Role::InvokeSourceFunction { formals, .. } => formals + .iter() + .find(|formal| formal.parameter() == parameter) + .map(|formal| formal.access() == jett_mir::ParamMode::View) + .ok_or_else(|| pending("Source call physical access has no exact original formal")), + _ => Err(pending("formal access requires an exact invocation role")), + } +} + +impl Translator<'_, '_> { + pub(super) fn resource_statement( + &mut self, + statement: &Statement, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if let StatementKind::Let { local, .. } = &statement.kind + && self.function.local(*local).is_some_and(|header| { + jett_mir::native_graph_storage_for_local(self.function, self.types, header) + .is_some() + }) + { + return Ok(false); + } + if self.carrier_statement(statement)? { + return Ok(true); + } + let operations = resource.at_site()?; + let site_only = operations + .iter() + .copied() + .filter(|operation| !operation.is_expression_operation()) + .collect::>(); + if matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) { + if !self.resource_lexical_retirement(statement.span)? { + return Err(pending( + "lexical exit node has no exact constructor retirement", + )); + } + return Ok(true); + } + if self.resource_borrowed_sum_statement(statement, &site_only)? { + return Ok(true); + } + match &statement.kind { + StatementKind::ResourceCall(node) => { + self.resource_staged_call(node, statement.span, &site_only)?; + Ok(true) + } + StatementKind::Let { local, value } + if resource.layout.plan().type_requires_custody(value.ty) + && !self.resource_is_named_value(value)? => + { + let evaluated = self.expression(value)?; + let bits = if let Some(destination) = resource.local_slot(*local)? { + let operation = site_only.iter().copied().find(|operation| matches!(operation.role(), Role::Transfer { destination: selected, .. } if *selected == destination)); + if let Some(operation) = operation { + self.resource_transfer(operation)? + } else { + self.scalar(evaluated, value.span)? + } + } else { + if let Some(operation) = site_only.iter().copied().find(|operation| { + matches!( + operation.role(), + Role::Borrow { .. } | Role::BorrowSum { .. } + ) + }) { + self.resource_begin_borrow(operation)? + } else { + self.scalar(evaluated, value.span)? + } + }; + self.define_local(*local, LoweredValue::Scalar(bits), statement.span)?; + Ok(true) + } + StatementKind::Assign { target, value } + if resource.layout.plan().type_requires_custody(target.ty) => + { + let ExpressionKind::Local(local) = target.kind else { + return Err(pending( + "Resource projected assignment transport is pending", + )); + }; + self.expression(value)?; + let destination = resource + .local_slot(local)? + .ok_or_else(|| pending("Resource assignment lost its owning local"))?; + let mut replacements = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::Replace { destination: selected, .. } if *selected == destination)); + let mut reseats = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::SelfRebind { slot } if *slot == destination)); + let mut transfers = site_only.iter().copied().filter(|operation| + matches!(operation.role(), Role::Transfer { destination: selected, .. } if *selected == destination)); + let replace = replacements.next(); + let reseat = reseats.next(); + let transfer = transfers.next(); + if replacements.next().is_some() + || reseats.next().is_some() + || transfers.next().is_some() + { + return Err(pending( + "Resource assignment has ambiguous exact endpoint roles", + )); + } + let bits = if let Some(replace) = replace { + let Role::Replace { replacement, .. } = replace.role() else { + return Err(pending("Resource replacement lost its exact constructor")); + }; + if reseat.is_some() { + return Err(pending( + "Resource replacement cannot also reseat its destination", + )); + } + replacement::validate_rows( + resource.function_plan()?, + &site_only, + replace, + transfer.ok_or_else(|| { + pending("Resource replacement lost its exact evaluated RHS transfer") + })?, + )?; + let old = self.resource_owner(destination)?; + let rhs = self.resource_owner(*replacement)?; + let (context, frame, ordinal) = self.resource_operation(replace)?; + let bits = Leaf::Replace.output( + self.module, + self.builder, + &[context, frame, ordinal, old, rhs], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[replacement.index()]); + self.resource_store(destination, bits)?; + bits + } else if reseat.is_some() { + if transfer.is_some() + || !matches!(value.kind, ExpressionKind::Local(source) if source == local) + || value.ty != target.ty + { + return Err(pending( + "Resource self-rebind lost its exact current Local occurrence", + )); + } + self.resource_owner(destination)? + } else { + self.resource_transfer(transfer.ok_or_else(|| { + pending("vacant Resource assignment has no exact evaluated RHS transfer") + })?)? + }; + self.define_local(local, LoweredValue::Scalar(bits), statement.span)?; + Ok(true) + } + StatementKind::Evaluate(expression) | StatementKind::HandleDefault(expression) + if resource.layout.plan().type_requires_custody(expression.ty) => + { + self.expression(expression)?; + for operation in site_only { + if matches!(operation.role(), Role::Drop { .. }) { + self.resource_drop(operation)?; + } + } + Ok(true) + } + StatementKind::SumTag { source, target } + if resource + .layout + .plan() + .type_requires_custody(self.local_types[source.index() as usize].ty) => + { + let slot = resource + .local_slot(*source)? + .ok_or_else(|| pending("Resource sum tag has no exact owner"))?; + let value = self.resource_owner(slot)?; + let context = self.resource_context(); + let execution = resource + .function_plan()? + .execution_frame(resource.block, resource.position) + .ok_or_else(|| { + pending("Resource sum tag lost its exact current execution frame") + })?; + let frame = self.resource_frame(execution)?; + let output = self.builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 4, + 2, + )); + let zero = self.builder.ins().iconst(ir::types::I32, 0); + self.builder.ins().stack_store(zero, output, 0); + let address = self.builder.ins().stack_addr(ir::types::I64, output, 0); + Leaf::SumTag.checked( + self.module, + self.builder, + &[context, frame, value, address], + resource.failure, + )?; + let tag = self.builder.ins().stack_load(ir::types::I32, output, 0); + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(*target, LoweredValue::Scalar(tag), statement.span)?; + Ok(true) + } + StatementKind::SumTake { + source, + target, + success, + } if resource + .layout + .plan() + .type_requires_custody(self.local_types[source.index() as usize].ty) => + { + let operation = operations + .iter() + .copied() + .find(|operation| { + if *success { + matches!(operation.role(), Role::SumTake { .. }) + } else { + matches!(operation.role(), Role::TakeFailureCompanion { .. }) + } + }) + .ok_or_else(|| { + pending("Resource sum extraction lost its exact selected-edge role") + })?; + let source_slot = resource + .local_slot(*source)? + .ok_or_else(|| pending("Resource sum extraction lost its shell owner"))?; + let value = self.resource_owner(source_slot)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let lowered = if let Role::SumTake { destination, .. } = operation.role() { + let destination_frame = self.resource_destination(operation)?; + let bits = Leaf::SumTake.output( + self.module, + self.builder, + &[context, frame, ordinal, value, destination_frame], + resource.failure, + 8, + )?; + self.resource_store(*destination, bits)?; + LoweredValue::Scalar(bits) + } else { + let bits = Leaf::FailureCompanionTake.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 8, + )?; + let ty = self.local_types[target.index() as usize].ty; + self.unpack_payload(bits, ty, statement.span)? + }; + self.clear_slot(resource.owners[source_slot.index()]); + self.define_local(*target, lowered, statement.span)?; + Ok(true) + } + _ => Ok(false), + } + } + fn resource_scope_completion( + &mut self, + operation: &jett_mir::ResourceOperation, + body: Value, + checked: bool, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("completion has no selected family"))?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + let one = self.builder.ins().iconst(ir::types::I64, 1); + self.builder + .ins() + .stack_store(one, resource.scope_finished, 0); + if checked { + Leaf::ScopeComplete.checked( + self.module, + self.builder, + &[context, scope, ordinal, body], + resource.failure, + ) + } else { + Leaf::ScopeComplete + .call(self.module, self.builder, &[context, scope, ordinal, body]) + .map(|_| ()) + } + } + pub(super) fn resource_return( + &mut self, + expression: Option<&Expression>, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("return has no selected family"))?; + let operations = resource.at_site()?; + let opaque_return = match (resource.function_plan()?.descriptor_return(), expression) { + (Some(hook), Some(value)) => { + if resource.function_plan()?.descriptor_value(value) != Some(hook) + || value.ty != hook.function_type() + { + return Err(pending( + "returned hook metadata changes its sealed Return proof", + )); + } + true + } + (Some(_), None) => { + return Err(pending("hook descriptor Return omitted its exact value")); + } + (None, _) => false, + }; + let mut result = if let Some(expression) = expression { + self.expression(expression)? + } else { + self.nothing() + }; + // The original operand may observe a nested alias. Its evaluation + // precedes certified lexical retirement and provisional publication. + self.resource_lexical_retirement(span)?; + let completion = operations + .iter() + .copied() + .find(|operation| { + operation.frame() + == resource + .function_plan() + .expect("selected plan") + .root_scope() + .id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("return has no exact Scope completion"))?; + let carrier_publication = resource.function_plan()?.carriers().and_then(|plan| { + plan.operations().iter().find(|operation| { + operation.site().block() == resource.block + && operation.site().position() == resource.position + && operation.edge().is_none() + && matches!( + operation.role(), + jett_mir::ResourceCarrierOperationRole::PublishReturn { .. } + ) + }) + }); + if let Some(publication) = carrier_publication { + let jett_mir::ResourceCarrierOperationRole::PublishReturn { source } = + publication.role() + else { + unreachable!(); + }; + let transfer = resource.function_plan()?.carriers().and_then(|plan| plan.operations().iter().find(|operation| + operation.site().block() == resource.block && operation.site().position() == resource.position + && !operation.is_expression_operation() && operation.edge().is_none() + && matches!(operation.role(), jett_mir::ResourceCarrierOperationRole::Transfer { destination, .. } + | jett_mir::ResourceCarrierOperationRole::QualifyMachine { destination, .. } if destination == source))) + .ok_or_else(|| pending("carrier Return lost its exact provisional transfer"))?; + self.carrier_transfer(transfer)?; + } + let publication = operations + .iter() + .copied() + .find(|operation| matches!(operation.role(), Role::CompleteReturnAfterCleanup { .. })); + if let Some(publication) = publication { + let Role::CompleteReturnAfterCleanup { source } = publication.role() else { + unreachable!(); + }; + let transfer = operations.iter().copied().find(|operation| matches!(operation.role(), Role::Transfer { destination, .. } if destination == source)).ok_or_else(|| pending("owned return has no exact provisional transfer"))?; + self.resource_transfer(transfer)?; + } else if carrier_publication.is_none() + && !opaque_return + && expression.is_some_and(|expression| is_copy_owned(self.types, expression.ty)) + { + let value = self.scalar(result, span)?; + let leaf = + if expression.is_some_and(|expression| is_function(self.types, expression.ty)) { + NativeLeaf::StructClone + } else { + NativeLeaf::Retain + }; + let retained = self.leaf(leaf, &[value], true)?; + self.builder + .ins() + .stack_store(retained, resource.return_companion, 0); + result = LoweredValue::Scalar(retained); + } else if let LoweredValue::Owned(value, slot) = result { + // Preserve the ordinary owner through fallible Scope cleanup. + self.builder + .ins() + .stack_store(value, resource.return_companion, 0); + self.clear_slot(slot); + result = LoweredValue::Scalar(value); + } + self.flush_actor_state(span)?; + self.drop_all()?; + let body = self.builder.ins().iconst(ir::types::I32, 0); + self.resource_scope_completion(completion, body, true)?; + if let Some(publication) = carrier_publication { + let jett_mir::ResourceCarrierOperationRole::PublishReturn { source } = + publication.role() + else { + unreachable!(); + }; + let value = self.carrier_owner(*source)?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = resource + .layout + .carrier_operation(resource.function, publication.id()) + .ok_or_else(|| pending("carrier Return publication has no exact installed row"))?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(ordinal)); + let result = Leaf::CarrierPublish.output( + self.module, + self.builder, + &[context, scope, ordinal, value], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + self.builder.ins().return_(&[result]); + } else if let Some(publication) = publication { + let Role::CompleteReturnAfterCleanup { source } = publication.role() else { + unreachable!(); + }; + let value = self.resource_owner(*source)?; + let context = self.resource_context(); + let scope = self.builder.use_var(resource.scope); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(publication)?)); + let result = Leaf::ReturnPublish.output( + self.module, + self.builder, + &[context, scope, ordinal, value], + resource.failure, + 8, + )?; + self.clear_slot(resource.owners[source.index()]); + self.builder.ins().return_(&[result]); + } else if expression.is_some_and(|expression| { + task_scalar(Some(resource.layout), self.types, expression.ty).unwrap_or(false) + }) { + let (bits, depth) = self.scalar_task(result, span)?; + self.builder.ins().return_(&[bits, depth]); + } else { + let bits = self.scalar(result, span)?; + self.clear_slot(resource.return_companion); + self.builder.ins().return_(&[bits]); + } + Ok(()) + } + pub(super) fn resource_failure_cleanup(&mut self) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + // Ordinary return custody remains ordinary even when a Resource cleanup fails. + self.drop_slot(resource.return_companion)?; + for header in resource + .function_plan()? + .frames() + .iter() + .rev() + .filter(|header| header.role() == FrameRole::Operation) + { + let active = self.builder.ins().stack_load( + ir::types::I64, + resource.frames[header.id().index()], + 0, + ); + let retire = self.builder.create_block(); + let next = self.builder.create_block(); + let live = self.builder.ins().icmp_imm(IntCC::NotEqual, active, 0); + self.builder.ins().brif(live, retire, &[], next, &[]); + self.builder.switch_to_block(retire); + let operation = resource + .function_plan()? + .operations() + .iter() + .find(|operation| { + operation.frame() == header.id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("active Operation has no sealed completion row"))?; + let context = self.resource_context(); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + self.clear_slot(resource.frames[header.id().index()]); + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder + .ins() + .stack_store(zero, resource.frames[header.id().index()], 8); + // Attempt all enclosing retirement even after the first infrastructure failure. + Leaf::OperationComplete.call(self.module, self.builder, &[context, active, ordinal])?; + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(next); + } + let finished = self + .builder + .ins() + .stack_load(ir::types::I64, resource.scope_finished, 0); + let retire = self.builder.create_block(); + let next = self.builder.create_block(); + let live = self.builder.ins().icmp_imm(IntCC::Equal, finished, 0); + self.builder.ins().brif(live, retire, &[], next, &[]); + self.builder.switch_to_block(retire); + let operation = resource + .function_plan()? + .operations() + .iter() + .find(|operation| { + operation.frame() + == resource + .function_plan() + .expect("selected plan") + .root_scope() + .id() + && matches!(operation.role(), Role::Complete { .. }) + }) + .ok_or_else(|| pending("failure Scope has no original completion projection"))?; + let body = self.builder.use_var(resource.status); + self.resource_scope_completion(operation, body, false)?; + self.builder.ins().jump(next, &[]); + self.builder.switch_to_block(next); + Ok(()) + } +} + +/// One C entry wrapper owns its exact installation and runtime attempt. +fn define_entry( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + layout: &EmittedResourceLayout<'_>, +) -> Result<(), CodegenError> { + let (id, mut context) = build_entry(module, declarations, layout)?; + module + .define_function(id, &mut context) + .map_err(|error| CodegenError::Backend(format!("Resource C entry: {error:?}"))) +} + +fn build_entry( + module: &mut ObjectModule, + declarations: &DeclaredFunctions, + layout: &EmittedResourceLayout<'_>, +) -> Result<(FuncId, Context), CodegenError> { + let selected = layout.entry(); + let entry = declarations + .get(FunctionId::new(selected.function)) + .ok_or_else(|| pending("driver entry has no declared native family"))?; + let signature = program_entry_signature(module); + let id = module + .declare_function(JETT_AOT_ENTRY_SYMBOL_V1, Linkage::Export, &signature) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let mut context = module.make_context(); + context.func.signature = signature; + let mut frontend = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut frontend); + let initial = builder.create_block(); + builder.append_block_params_for_function_params(initial); + builder.switch_to_block(initial); + let ctx = builder.block_params(initial)[0]; + let startup_refused = builder.create_block(); + builder.append_block_param(startup_refused, ir::types::I32); + let refused = builder.create_block(); + builder.append_block_param(refused, ir::types::I32); + let scope_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + let result_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 24, + 3, + )); + let zero = builder.ins().iconst(ir::types::I64, 0); + builder.ins().stack_store(zero, scope_output, 0); + builder.ins().stack_store(zero, result_output, 0); + builder.ins().stack_store(zero, result_output, 8); + builder.ins().stack_store(zero, result_output, 16); + let scope_address = builder.ins().stack_addr(ir::types::I64, scope_output, 0); + let result_address = builder.ins().stack_addr(ir::types::I64, result_output, 0); + let manifest_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 32, + 3, + )); + let attempt_output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + builder.ins().stack_store(zero, attempt_output, 0); + let manifest_address = builder.ins().stack_addr(ir::types::I64, manifest_output, 0); + let attempt_address = builder.ins().stack_addr(ir::types::I64, attempt_output, 0); + let mut manifest_signature = module.make_signature(); + manifest_signature + .params + .push(AbiParam::new(ir::types::I64)); + manifest_signature + .returns + .push(AbiParam::new(ir::types::I32)); + let manifest = module + .declare_function( + "jett_aot_resource_v1_manifest", + Linkage::Export, + &manifest_signature, + ) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let manifest = module.declare_func_in_func(manifest, builder.func); + let manifest_call = builder.ins().call(manifest, &[manifest_address]); + let manifest_status = builder.inst_results(manifest_call)[0]; + let manifest_ready = builder.create_block(); + let manifest_ok = builder.ins().icmp_imm(IntCC::Equal, manifest_status, 0); + builder.ins().brif( + manifest_ok, + manifest_ready, + &[], + startup_refused, + &[manifest_status.into()], + ); + builder.switch_to_block(manifest_ready); + Leaf::ProgramBegin.checked( + module, + &mut builder, + &[ctx, manifest_address, attempt_address, result_address], + startup_refused, + )?; + Leaf::EntryScope.checked( + module, + &mut builder, + &[ctx, scope_address, result_address], + refused, + )?; + let root = builder.ins().stack_load(ir::types::I64, scope_output, 0); + let bridge_completion = layout.entry_bridge_completion(); + let mut native = vec![ctx, zero]; + if bridge_completion.is_some() { + if layout.plan().function(entry.mir_id).is_some() { + return Err(pending( + "ordinary entry bridge acquired a Source-family body ABI", + )); + } + let function = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.function)); + let signature = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.signature)); + let template = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.scope)); + Leaf::ScopeValidate.checked( + module, + &mut builder, + &[ctx, root, function, signature, template], + refused, + )?; + } else { + native.push(root); + } + for (parameter, ty) in entry.parameter_types.iter().enumerate() { + if *ty != TypeInterner::NETWORK { + return Err(pending( + "native root capability differs from the exact selected Network entry", + )); + } + let output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 8, + 3, + )); + builder.ins().stack_store(zero, output, 0); + builder.ins().stack_store(zero, result_output, 0); + builder.ins().stack_store(zero, result_output, 8); + builder.ins().stack_store(zero, result_output, 16); + let address = builder.ins().stack_addr(ir::types::I64, output, 0); + let parameter = builder.ins().iconst(ir::types::I32, parameter as i64); + Leaf::EntryNetwork.checked( + module, + &mut builder, + &[ctx, parameter, address, result_address], + refused, + )?; + native.push(builder.ins().stack_load(ir::types::I64, output, 0)); + } + let reference = module.declare_func_in_func(entry.native_id, builder.func); + builder.ins().call(reference, &native); + if let Some(ordinal) = bridge_completion { + let status = crate::values::declare_leaf(module, NativeLeaf::Status)?; + let status = module.declare_func_in_func(status, builder.func); + let call = builder.ins().call(status, &[ctx]); + let body = builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| pending("ordinary entry has no original native body status"))?; + let ordinal = builder.ins().iconst(ir::types::I32, i64::from(ordinal)); + // Completion can report body failure after actual retirement. Observe the + // retained root outcome next, rather than replacing the original status. + Leaf::ScopeComplete.call(module, &mut builder, &[ctx, root, ordinal, body])?; + } + let output = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 4, + 2, + )); + let zero32 = builder.ins().iconst(ir::types::I32, 0); + builder.ins().stack_store(zero32, output, 0); + let function = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.function)); + let signature = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.signature)); + let template = builder + .ins() + .iconst(ir::types::I32, i64::from(selected.scope)); + let address = builder.ins().stack_addr(ir::types::I64, output, 0); + Leaf::EntryOutcome.checked( + module, + &mut builder, + &[ctx, root, function, signature, template, address], + refused, + )?; + let body_status = builder.ins().stack_load(ir::types::I32, output, 0); + let finish = builder.create_block(); + builder.append_block_param(finish, ir::types::I32); + builder.ins().jump(finish, &[body_status.into()]); + builder.switch_to_block(refused); + let status = builder.block_params(refused)[0]; + builder.ins().jump(finish, &[status.into()]); + builder.switch_to_block(finish); + let status = builder.block_params(finish)[0]; + let attempt = builder.ins().stack_load(ir::types::I64, attempt_output, 0); + let borrowed = builder.ins().icmp_imm(IntCC::Equal, attempt, 0); + let external_finish = builder.create_block(); + let generated_finish = builder.create_block(); + builder + .ins() + .brif(borrowed, external_finish, &[], generated_finish, &[]); + builder.switch_to_block(external_finish); + builder.ins().return_(&[status]); + builder.switch_to_block(generated_finish); + let terminal = Leaf::ProgramComplete.call(module, &mut builder, &[ctx, attempt, status])?; + builder.ins().return_(&[terminal]); + builder.switch_to_block(startup_refused); + let status = builder.block_params(startup_refused)[0]; + builder.ins().return_(&[status]); + builder.seal_all_blocks(); + builder.finalize(); + Ok((id, context)) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs new file mode 100644 index 00000000..ce40e5fc --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/carriers.rs @@ -0,0 +1,1296 @@ +//! Native execution of constructor-owned typed carrier operations. Ordinary +//! container leaves never receive a carrier root, loan, or Resource sum shell. +use super::*; +use jett_mir::{ + ResourceCarrierEdge as Edge, ResourceCarrierIndex as Index, + ResourceCarrierLoanSource as LoanSource, ResourceCarrierObservation as Observation, + ResourceCarrierOperation as Operation, ResourceCarrierOperationRole as CarrierRole, + ResourceCarrierProjectionPath as Path, ResourceCarrierSlotId as SlotId, + ResourceCarrierValue as CarrierValue, +}; + +impl<'a> ResourceEmission<'a> { + fn carrier_plan(self) -> Result<&'a jett_mir::ResourceCarrierFunctionPlan, CodegenError> { + self.function_plan()? + .carriers() + .ok_or_else(|| pending("carrier execution has no fresh private plan")) + } + fn carrier_at_site(self) -> Result, CodegenError> { + Ok(self + .carrier_plan()? + .operations() + .iter() + .filter(|row| { + row.site().block() == self.block + && row.site().position() == self.position + && row.edge().is_none() + && !row.is_expression_operation() + }) + .collect()) + } + pub(super) fn carrier_local_slot( + self, + local: jett_mir::LocalId, + ) -> Result, CodegenError> { + let Some(plan) = self.function_plan()?.carriers() else { + return Ok(None); + }; + let mut slots = plan.slots().iter().filter( + |slot| matches!(slot.storage(), Storage::Local { header } if header.id == local), + ); + let selected = slots.next().map(|slot| slot.id()); + if slots.next().is_some() { + return Err(pending("carrier local has ambiguous current storage")); + } + Ok(selected) + } +} + +impl Translator<'_, '_> { + fn carrier_staged_constructor( + &mut self, + node: &jett_mir::ResourceConstructorNode, + span: Span, + ) -> Result<(), CodegenError> { + use jett_mir::ResourceConstructorNode as Node; + let resource = self + .resource + .ok_or_else(|| pending("staged carrier constructor has no selected family"))?; + let function = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|f| f.id == resource.function) + .ok_or_else(|| pending("staged carrier constructor lost its exact function"))?; + let original = function + .resource_constructor(node.constructor()) + .map_err(|error| pending(&error))?; + let operations = resource.carrier_at_site()?; + let wrappers = resource.at_site()?; + match node { + Node::Begin { .. } => { + let mut rows = operations + .iter() + .copied() + .filter(|row| matches!(row.role(), CarrierRole::BeginConstructor { .. })); + let row = rows + .next() + .ok_or_else(|| pending("staged constructor has no exact Begin"))?; + if rows.next().is_some() || row.site() != original.begin() { + return Err(pending("staged constructor Begin is ambiguous or foreign")); + } + let CarrierRole::BeginConstructor { destination, .. } = row.role() else { + unreachable!() + }; + self.resource_open_operation(row.frame())?; + let (context, frame, ordinal) = self.carrier_operation(row)?; + let builder = Leaf::CarrierConstructBegin.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder.ins().stack_store( + builder, + resource.carrier_builders[destination.index()], + 0, + ); + } + Node::Stage { + source_index, + field, + value, + .. + } => { + let mut rows = operations.iter().copied().filter(|row| matches!(row.role(),CarrierRole::ConstructorChild { child, .. } if child.index == *field)); + let row = rows + .next() + .ok_or_else(|| pending("staged constructor lost its exact Child"))?; + if rows.next().is_some() + || original + .fields() + .get(*source_index) + .is_none_or(|f| f.stage() != row.site() || f.field() != *field) + { + return Err(pending( + "staged constructor Child changed its exact lexical occurrence", + )); + } + let CarrierRole::ConstructorChild { destination, child } = row.role() else { + unreachable!() + }; + self.carrier_stage_child(*destination, child.value, *source_index, value, span)?; + } + Node::Commit { output, .. } => { + let mut commits = operations + .iter() + .copied() + .filter(|row| matches!(row.role(), CarrierRole::CommitConstructor { .. })); + let commit = commits + .next() + .ok_or_else(|| pending("staged constructor has no exact Commit"))?; + if commits.next().is_some() + || original + .commit() + .is_none_or(|(site, header)| site != commit.site() || header.id != *output) + { + return Err(pending( + "staged constructor Commit changed its exact output", + )); + } + let CarrierRole::CommitConstructor { destination } = commit.role() else { + unreachable!() + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[destination.index()], + 0, + ); + let context = self.resource_context(); + let bits = Leaf::CarrierCommit.output( + self.module, + self.builder, + &[context, builder], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_builders[destination.index()]); + self.carrier_store(*destination, bits)?; + let target = resource.carrier_local_slot(*output)?.ok_or_else(|| { + pending("staged constructor output has no exact parent holder") + })?; + let mut transfers = operations.iter().copied().filter(|row| matches!(row.role(),CarrierRole::Transfer { source, destination:to } if source == destination && *to == target)); + let transfer = transfers.next().ok_or_else(|| { + pending("staged constructor Commit did not migrate its output before End") + })?; + if transfers.next().is_some() { + return Err(pending("staged constructor output migration is ambiguous")); + } + let bits = self.carrier_transfer(transfer)?; + self.define_local(*output, LoweredValue::Scalar(bits), span)?; + } + Node::End { outcome, .. } => { + let mut completions = wrappers.iter().copied().filter( + |row| matches!(row.role(),Role::Complete { outcome:found } if found == outcome), + ); + let complete = completions + .next() + .ok_or_else(|| pending("staged constructor has no exact completion"))?; + if completions.next().is_some() + || !original.exits().contains(&(complete.site(), *outcome)) + { + return Err(pending( + "staged constructor End changed its reached exact prefix", + )); + } + // Core already owns every acquired ticket. Clear compiler storage + // before fallible cleanup, including an incomplete builder prefix. + for slot in resource + .carrier_plan()? + .slots() + .iter() + .filter(|slot| slot.frame() == complete.frame()) + { + self.clear_slot(resource.carrier_owners[slot.id().index()]); + self.clear_slot(resource.carrier_builders[slot.id().index()]); + } + self.resource_finish_operation(&wrappers, complete.frame())?; + } + } + Ok(()) + } + + fn carrier_stage_child( + &mut self, + destination: SlotId, + child: CarrierValue, + arrival: usize, + actual: &Expression, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier Child has no selected family"))?; + let evaluated = self.expression(actual)?; + let value = match child { + CarrierValue::Ordinary { ty } if ty == actual.ty => { + if is_copy_owned(self.types, ty) || is_linear(self.types, ty) { + self.own_copy_value(evaluated, ty, span)? + } else { + evaluated + } + } + CarrierValue::Ordinary { .. } => { + return Err(pending("carrier Child changed its exact ordinary type")); + } + _ => evaluated, + }; + let (bits, depth) = match child { + CarrierValue::Owned { slot } => (self.carrier_owner(slot)?, None), + CarrierValue::LeafOwned { slot } => (self.resource_owner(slot)?, None), + CarrierValue::Ordinary { ty } => self.carrier_companion_payload(value, ty)?, + _ => return Err(pending("carrier Child cannot adopt a borrowed endpoint")), + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[destination.index()], + 0, + ); + let context = self.resource_context(); + let arrival = u32::try_from(arrival) + .map_err(|_| pending("carrier lexical arrival exceeds wire width"))?; + let arrival = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(arrival)); + if let Some(depth) = depth { + Leaf::CarrierChildScalarTask.checked( + self.module, + self.builder, + &[context, builder, arrival, bits, depth], + resource.failure, + )?; + } else { + Leaf::CarrierChild.checked( + self.module, + self.builder, + &[context, builder, arrival, bits], + resource.failure, + )?; + } + match child { + CarrierValue::Owned { slot } => self.clear_slot(resource.carrier_owners[slot.index()]), + CarrierValue::LeafOwned { slot } => self.clear_slot(resource.owners[slot.index()]), + CarrierValue::Ordinary { .. } => { + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + } + _ => unreachable!(), + } + Ok(()) + } + fn carrier_companion_payload( + &mut self, + value: LoweredValue, + ty: TypeId, + ) -> Result<(Value, Option), CodegenError> { + if ty == TypeInterner::NOTHING { + // Nothing's existing native scalar word is task depth, not payload. + let LoweredValue::Scalar(depth) = value else { + return Err(pending( + "Nothing companion changed its exact native depth representation", + )); + }; + if self.builder.func.dfg.value_type(depth) != ir::types::I64 { + return Err(pending( + "Nothing companion changed its exact native depth width", + )); + } + let bits = self.builder.ins().iconst(ir::types::I64, 0); + return Ok((bits, Some(depth))); + } + let bits = self.payload_bits(value).0; + let depth = match value { + LoweredValue::ScalarTask(_, depth) => Some(depth), + _ => None, + }; + Ok((bits, depth)) + } + fn carrier_operation( + &mut self, + operation: &Operation, + ) -> Result<(Value, Value, Value), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier operation has no selected family"))?; + let context = self.resource_context(); + let frame = self.resource_frame(operation.frame())?; + let ordinal = resource + .layout + .carrier_operation(resource.function, operation.id()) + .ok_or_else(|| pending("carrier occurrence lost its exact installed row"))?; + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(ordinal)); + Ok((context, frame, ordinal)) + } + pub(super) fn carrier_owner(&mut self, slot: SlotId) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier owner has no selected family"))?; + let storage = *resource + .carrier_owners + .get(slot.index()) + .ok_or_else(|| pending("carrier owner lost its exact storage"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, storage, 0)) + } + pub(super) fn carrier_store(&mut self, slot: SlotId, value: Value) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier store has no selected family"))?; + let storage = *resource + .carrier_owners + .get(slot.index()) + .ok_or_else(|| pending("carrier store lost its exact storage"))?; + self.builder.ins().stack_store(value, storage, 0); + Ok(()) + } + pub(super) fn carrier_loan( + &mut self, + loan: jett_mir::ResourceCarrierLoanId, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier loan has no selected family"))?; + let record = resource + .carrier_plan()? + .loans() + .get(loan.index()) + .filter(|row| row.id() == loan) + .ok_or_else(|| pending("carrier loan changed its exact current row"))?; + if let LoanSource::IncomingViewFormal { scope, parameter } = record.source() { + if scope != resource.function_plan()?.root_scope().id() || !record.path().is_empty() { + return Err(pending( + "incoming carrier view changes its original scope/path", + )); + } + let formal = resource + .function_plan()? + .parameters() + .get(parameter) + .ok_or_else(|| pending("incoming carrier view lost its exact formal"))?; + let variable = self.variables[formal.local.index() as usize] + .ok_or_else(|| pending("incoming carrier view has no native parameter"))?; + return Ok(self.builder.use_var(variable)); + } + let storage = *resource + .carrier_loans + .get(loan.index()) + .ok_or_else(|| pending("carrier loan lost its native storage"))?; + Ok(self.builder.ins().stack_load(ir::types::I64, storage, 0)) + } + fn carrier_source(&mut self, source: LoanSource) -> Result { + match source { + LoanSource::Slot(slot) => self.carrier_owner(slot), + LoanSource::Loan(loan) => self.carrier_loan(loan), + LoanSource::IncomingViewFormal { scope, parameter } => { + let resource = self + .resource + .ok_or_else(|| pending("carrier formal source has no family"))?; + if scope != resource.function_plan()?.root_scope().id() { + return Err(pending("carrier formal uses a foreign scope")); + } + let formal = resource + .function_plan()? + .parameters() + .get(parameter) + .ok_or_else(|| pending("carrier formal is absent"))?; + let variable = self.variables[formal.local.index() as usize] + .ok_or_else(|| pending("carrier formal has no native variable"))?; + Ok(self.builder.use_var(variable)) + } + } + } + fn carrier_owned_root( + &self, + loan: jett_mir::ResourceCarrierLoanId, + ) -> Result<(SlotId, Vec), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier extraction has no family"))?; + let plan = resource.carrier_plan()?; + let mut visited = std::collections::BTreeSet::new(); + let mut cursor = loan; + let mut segments = Vec::new(); + loop { + if !visited.insert(cursor.index()) { + return Err(pending("carrier extraction has a cyclic parent loan")); + } + let row = plan + .loans() + .get(cursor.index()) + .filter(|row| row.id() == cursor) + .ok_or_else(|| pending("carrier extraction lost its exact parent loan"))?; + segments.push(row.path().to_vec()); + match row.source() { + LoanSource::Slot(slot) => { + let mut path = Vec::new(); + for segment in segments.iter().rev() { + path.extend_from_slice(segment); + } + return Ok((slot, path)); + } + LoanSource::Loan(parent) => cursor = parent, + LoanSource::IncomingViewFormal { .. } => { + return Err(pending( + "owning projection cannot consume an incoming carrier view", + )); + } + } + } + } + fn carrier_selection(&mut self, path: &[Path]) -> Result { + let mut selected = None; + for step in path { + let index = match step { + Path::Element { index } | Path::MapKey { index } | Path::MapValue { index } => { + *index + } + _ => continue, + }; + let local = match index { + Index::Constant(_) => continue, + Index::Local(local) => local, + Index::CurrentIteration { header } => { + let resource = self + .resource + .ok_or_else(|| pending("iteration selection has no family"))?; + let mut loops = resource + .carrier_plan()? + .iterations() + .iter() + .filter(|row| row.header == header); + let selected = loops.next().ok_or_else(|| { + pending("iteration selection has no original/canonical loop") + })?; + if loops.next().is_some() { + return Err(pending("iteration selection is ambiguous")); + } + selected.cursor.ok_or_else(|| { + pending("raw carrier loop has no authenticated native cursor") + })? + } + }; + if selected.replace(local).is_some() { + return Err(pending( + "carrier path needs its exact multiple-selection transport", + )); + } + } + if let Some(local) = selected { + if self.local_types[local.index() as usize].ty != TypeInterner::INT64 { + return Err(pending("carrier selector changed its exact Int64 local")); + } + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("carrier selector has no native variable"))?; + Ok(self.builder.use_var(variable)) + } else { + Ok(self.builder.ins().iconst(ir::types::I64, 0)) + } + } + pub(super) fn carrier_transfer( + &mut self, + operation: &Operation, + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier transfer has no family"))?; + let (source, destination, leaf) = match operation.role() { + CarrierRole::Transfer { + source, + destination, + } => (*source, *destination, Leaf::CarrierTransfer), + CarrierRole::QualifyMachine { + source, + destination, + .. + } => (*source, *destination, Leaf::CarrierQualify), + CarrierRole::BeginIteration { + source: CarrierValue::Owned { slot }, + destination, + .. + } => (*slot, *destination, Leaf::CarrierTransfer), + _ => return Err(pending("carrier transfer selected another role")), + }; + let input = self.carrier_owner(source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + self.carrier_store(destination, bits)?; + Ok(bits) + } + fn carrier_begin_borrow(&mut self, operation: &Operation) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier borrow has no family"))?; + let loan = match operation.role() { + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => *loan, + _ => return Err(pending("carrier borrow selected another role")), + }; + let record = resource + .carrier_plan()? + .loans() + .get(loan.index()) + .ok_or_else(|| pending("carrier borrow lost its exact loan"))?; + let input = self.carrier_source(record.source())?; + let selection = self.carrier_selection(record.path())?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierBorrow.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + self.builder + .ins() + .stack_store(bits, resource.carrier_loans[loan.index()], 0); + Ok(bits) + } + fn carrier_end_borrow(&mut self, operation: &Operation) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier end borrow has no family"))?; + let CarrierRole::EndBorrow { loan } = operation.role() else { + return Err(pending("carrier end selected another role")); + }; + let input = self.carrier_loan(*loan)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierEndBorrow.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + self.clear_slot(resource.carrier_loans[loan.index()]); + Ok(()) + } + /// Execute one exact occurrence; expression child evaluation stays with the + /// constructor emitter and publication stays after ScopeComplete. + fn carrier_effect( + &mut self, + operation: &Operation, + span: Span, + ) -> Result, CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier effect has no family"))?; + match operation.role() { + CarrierRole::Transfer { .. } + | CarrierRole::QualifyMachine { .. } + | CarrierRole::BeginIteration { .. } => self + .carrier_transfer(operation) + .map(|bits| Some(LoweredValue::Scalar(bits))), + CarrierRole::Borrow { .. } | CarrierRole::Project { .. } => self + .carrier_begin_borrow(operation) + .map(|bits| Some(LoweredValue::Scalar(bits))), + CarrierRole::EndBorrow { .. } => { + self.carrier_end_borrow(operation)?; + Ok(None) + } + CarrierRole::Observe { + source, + observation, + target, + } => { + let input = self.carrier_loan(*source)?; + let selected = match observation { + Observation::Ordinary { path, .. } => path.as_slice(), + _ => &[], + }; + let selection = self.carrier_selection(selected)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let scalar_task = matches!(observation, Observation::Ordinary { ty, .. } + if *ty == TypeInterner::NOTHING || super::task_scalar(Some(resource.layout), self.types, *ty)?); + let (bits, depth) = if scalar_task { + let (bits, depth) = Leaf::CarrierObserveScalarTask.scalar_task_output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + )?; + (bits, Some(depth)) + } else { + ( + Leaf::CarrierObserve.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?, + None, + ) + }; + let result = match observation { + Observation::Length | Observation::Tag => LoweredValue::Scalar(bits), + Observation::State { state } => { + let selected = i64::try_from(*state) + .map_err(|_| pending("checked carrier state is outside tag width"))?; + LoweredValue::Scalar(self.builder.ins().icmp_imm( + IntCC::Equal, + bits, + selected, + )) + } + Observation::Ordinary { ty, .. } if *ty == TypeInterner::NOTHING => { + LoweredValue::Scalar(depth.ok_or_else(|| { + pending("Nothing observation lost its exact retained depth") + })?) + } + Observation::Ordinary { ty, .. } => { + let value = self.unpack_payload(bits, *ty, span)?; + if let Some(depth) = depth { + let bits = self.scalar(value, span)?; + LoweredValue::ScalarTask(bits, depth) + } else { + value + } + } + }; + if let Some(target) = target { + let bits = self.scalar(result, span)?; + let result = if self.local_types[target.index() as usize].ty + == TypeInterner::BOOL + && self.builder.func.dfg.value_type(bits) != ir::types::I8 + { + LoweredValue::Scalar(self.builder.ins().ireduce(ir::types::I8, bits)) + } else { + result + }; + self.define_local(*target, result, span)?; + } + Ok(Some(result)) + } + CarrierRole::Extract { + source, + path, + destination, + } => { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + let input = self.carrier_owner(root)?; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierExtract.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + self.carrier_store(*destination, bits)?; + if let Storage::Local { header } = + resource.carrier_plan()?.slots()[destination.index()].storage() + { + self.define_local(header.id, LoweredValue::Scalar(bits), span)?; + } + Ok(Some(LoweredValue::Scalar(bits))) + } + CarrierRole::ExtractOrdinary { + source, + path, + target, + ty, + } => { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + let input = self.carrier_owner(root)?; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierExtract.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + let value = self.unpack_payload(bits, *ty, span)?; + self.define_local(*target, value, span)?; + Ok(Some(value)) + } + CarrierRole::AdaptSum { + source, + path, + destination, + loan, + .. + } => { + let (input, full) = if loan.is_some() { + (self.carrier_loan(*source)?, path.clone()) + } else { + let (root, mut full) = self.carrier_owned_root(*source)?; + full.extend_from_slice(path); + (self.carrier_owner(root)?, full) + }; + let selection = self.carrier_selection(&full)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let bits = Leaf::CarrierAdaptSum.output( + self.module, + self.builder, + &[context, frame, ordinal, input, selection], + resource.failure, + 8, + )?; + if let Some(loan) = loan { + // The adapter returns the exact SumLoan. Its parent-bound + // shell is owned solely by runtime, never an ordinary owner. + self.builder + .ins() + .stack_store(bits, resource.loans[loan.index()], 0); + } else { + self.resource_store(*destination, bits)?; + if let Storage::Local { header } = + resource.function_plan()?.owner_slots()[destination.index()].storage() + { + self.define_local(header.id, LoweredValue::Scalar(bits), span)?; + } + } + Ok(Some(LoweredValue::Scalar(bits))) + } + CarrierRole::Retire { source } => { + let input = self.carrier_owner(*source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierRetire.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + self.clear_slot(resource.carrier_owners[source.index()]); + Ok(None) + } + CarrierRole::RetireIteration { source, binders } => { + let input = self.carrier_owner(*source)?; + let (context, frame, ordinal) = self.carrier_operation(operation)?; + Leaf::CarrierResetIteration.checked( + self.module, + self.builder, + &[context, frame, ordinal, input], + resource.failure, + )?; + for binder in binders { + if let Some(slot) = resource.carrier_local_slot(*binder)? { + self.clear_slot(resource.carrier_owners[slot.index()]); + } + if let Some(slot) = resource.local_slot(*binder)? { + self.clear_slot(resource.owners[slot.index()]); + } + } + Ok(None) + } + CarrierRole::BeginConstructor { .. } + | CarrierRole::ConstructorChild { .. } + | CarrierRole::CommitConstructor { .. } + | CarrierRole::PublishReturn { .. } => Err(pending( + "carrier effect must use its exact constructor/publication phase", + )), + } + } + fn carrier_construct( + &mut self, + expression: &Expression, + operations: &[&Operation], + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("carrier constructor has no family"))?; + let mut output = None; + let mut destination = None; + let mut arrival = 0usize; + for operation in operations { + match operation.role() { + CarrierRole::BeginConstructor { + destination: slot, .. + } => { + if destination.replace(*slot).is_some() { + return Err(pending( + "carrier expression contains multiple outer constructors", + )); + } + let (context, frame, ordinal) = self.carrier_operation(operation)?; + let builder = Leaf::CarrierConstructBegin.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + self.builder.ins().stack_store( + builder, + resource.carrier_builders[slot.index()], + 0, + ); + } + CarrierRole::ConstructorChild { + destination: slot, + child, + } => { + if destination != Some(*slot) { + return Err(pending( + "carrier child changed its exact constructor destination", + )); + } + let actual = constructor_child(expression, child.index)?; + let evaluated = self.expression(actual)?; + let value = match child.value { + CarrierValue::Ordinary { ty } => { + if ty != actual.ty { + return Err(pending( + "carrier ordinary child changed its exact source type", + )); + } + if is_copy_owned(self.types, ty) || is_linear(self.types, ty) { + self.own_copy_value(evaluated, ty, actual.span)? + } else { + evaluated + } + } + _ => evaluated, + }; + let (bits, depth) = match child.value { + CarrierValue::Owned { slot } => (self.carrier_owner(slot)?, None), + CarrierValue::LeafOwned { slot } => (self.resource_owner(slot)?, None), + CarrierValue::Ordinary { ty } => { + self.carrier_companion_payload(value, ty)? + } + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + return Err(pending( + "carrier constructor cannot adopt a borrowed child", + )); + } + }; + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[slot.index()], + 0, + ); + let context = self.resource_context(); + let index = u32::try_from(arrival) + .map_err(|_| pending("carrier child arrival is outside wire width"))?; + arrival = arrival + .checked_add(1) + .ok_or_else(|| pending("carrier child arrival overflow"))?; + let index = self.builder.ins().iconst(ir::types::I32, i64::from(index)); + if let Some(depth) = depth { + Leaf::CarrierChildScalarTask.checked( + self.module, + self.builder, + &[context, builder, index, bits, depth], + resource.failure, + )?; + } else { + Leaf::CarrierChild.checked( + self.module, + self.builder, + &[context, builder, index, bits], + resource.failure, + )?; + } + match child.value { + CarrierValue::Owned { slot } => { + self.clear_slot(resource.carrier_owners[slot.index()]) + } + CarrierValue::LeafOwned { slot } => { + self.clear_slot(resource.owners[slot.index()]) + } + CarrierValue::Ordinary { .. } => { + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + } + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + unreachable!() + } + } + } + CarrierRole::CommitConstructor { destination: slot } => { + if destination != Some(*slot) || output.is_some() { + return Err(pending( + "carrier constructor changed its exact commit phase", + )); + } + let builder = self.builder.ins().stack_load( + ir::types::I64, + resource.carrier_builders[slot.index()], + 0, + ); + let context = self.resource_context(); + let bits = Leaf::CarrierCommit.output( + self.module, + self.builder, + &[context, builder], + resource.failure, + 8, + )?; + self.clear_slot(resource.carrier_builders[slot.index()]); + self.carrier_store(*slot, bits)?; + output = Some(LoweredValue::Scalar(bits)); + } + _ => { + return Err(pending( + "carrier constructor selected an unrelated outer expression role", + )); + } + } + } + output.ok_or_else(|| pending("carrier constructor omitted its exact commit")) + } + pub(super) fn carrier_expression( + &mut self, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(resource) = self.resource else { + return Ok(None); + }; + let Some(plan) = resource.function_plan()?.carriers() else { + return Ok(None); + }; + let operations = plan + .operations_for_expression(expression) + .collect::>(); + if operations + .iter() + .any(|row| matches!(row.role(), CarrierRole::BeginConstructor { .. })) + { + return self.carrier_construct(expression, &operations).map(Some); + } + if !operations.is_empty() { + match &expression.kind { + ExpressionKind::Field { base, .. } => { + self.expression(base)?; + } + ExpressionKind::StateIs { value, .. } => { + self.expression(value)?; + } + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + self.expression(value)?; + } + ExpressionKind::Intrinsic { args, .. } => { + for argument in args { + self.expression(argument)?; + } + } + _ => {} + } + } + let mut value = None; + for operation in operations { + if let Some(result) = self.carrier_effect(operation, expression.span)? { + value = Some(result); + } + } + if value.is_some() { + return Ok(value); + } + if !super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + expression.ty, + ) { + return Ok(None); + } + match &expression.kind { + ExpressionKind::Local(local) => { + let bits = if let Some(slot) = resource.carrier_local_slot(*local)? { + self.carrier_owner(slot)? + } else { + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("carrier view local has no current variable"))?; + self.builder.use_var(variable) + }; + Ok(Some(LoweredValue::Scalar(bits))) + } + ExpressionKind::View(inner) => self.expression(inner).map(Some), + _ => Err(pending( + "carrier expression has no exact constructor/projection/current-local occurrence", + )), + } + } + pub(super) fn carrier_stage_actual( + &mut self, + operations: &[&jett_mir::ResourceOperation], + frame: jett_mir::ResourceFrameId, + parameter: usize, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + let Some(plan) = resource.function_plan()?.carriers() else { + return Ok(()); + }; + for wrapper in operations + .iter() + .copied() + .filter(|row| row.frame() == frame) + { + let Role::Carrier { operation } = wrapper.role() else { + continue; + }; + let row = plan + .operations() + .get(operation.index()) + .ok_or_else(|| pending("carrier staging lost its exact occurrence"))?; + match row.role() { + CarrierRole::Transfer { destination, .. } + | CarrierRole::QualifyMachine { destination, .. } + if matches!(plan.slots()[destination.index()].storage(), Storage::Argument { parameter: selected, .. } if *selected == parameter) => + { + self.carrier_transfer(row)?; + } + CarrierRole::Borrow { loan } => { + let selected = operations.iter().any(|wrapper| match wrapper.role() { + Role::InvokeSourceFunction { operands, .. } => operands.iter().any(|operand| matches!(operand, Operand::CarrierBorrowed { parameter: actual, loan: selected } if *actual == parameter && selected == loan)), + _ => false, + }); + if selected { + self.carrier_begin_borrow(row)?; + } + } + _ => {} + } + } + Ok(()) + } + pub(super) fn carrier_finish_operation( + &mut self, + wrapper: &jett_mir::ResourceOperation, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("carrier finish has no family"))?; + let Role::Carrier { operation } = wrapper.role() else { + return Ok(()); + }; + let row = resource + .carrier_plan()? + .operations() + .get(operation.index()) + .ok_or_else(|| pending("carrier finish lost its exact occurrence"))?; + if matches!( + row.role(), + CarrierRole::EndBorrow { .. } | CarrierRole::Retire { .. } + ) { + let span = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|function| function.id == resource.function) + .ok_or_else(|| pending("carrier completion lost its original function"))? + .span; + self.carrier_effect(row, span)?; + } + Ok(()) + } + pub(super) fn carrier_statement( + &mut self, + statement: &Statement, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if resource.function_plan()?.carriers().is_none() { + return Ok(false); + } + if let StatementKind::ResourceConstructor(node) = &statement.kind { + self.carrier_staged_constructor(node, statement.span)?; + return Ok(true); + } + let operations = resource.carrier_at_site()?; + let mut claimed = false; + match &statement.kind { + StatementKind::Let { local, value } + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + value.ty, + ) => + { + let mut evaluated = self.expression(value)?; + for operation in &operations { + if let Some(value) = self.carrier_effect(operation, statement.span)? { + evaluated = value; + } + } + self.define_local(*local, evaluated, statement.span)?; + return Ok(true); + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + value.ty, + ) => + { + self.expression(value)?; + claimed = true; + } + StatementKind::SequenceLength { source, .. } + | StatementKind::SequenceGet { source, .. } + if source + .ty(resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .ok_or_else(|| pending("carrier sequence lost its current function"))?) + .is_some_and(|ty| { + super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + ty, + ) + }) => + { + claimed = true; + } + StatementKind::SumTag { source, .. } | StatementKind::SumTake { source, .. } + if super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + self.local_types[source.index() as usize].ty, + ) => + { + claimed = true; + } + _ => {} + } + if claimed { + for operation in operations { + self.carrier_effect(operation, statement.span)?; + } + } + Ok(claimed) + } + pub(in crate::emit) fn carrier_iteration_boundary( + &mut self, + span: Span, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + if resource.function_plan()?.carriers().is_none() { + return Ok(()); + } + for operation in resource.carrier_at_site()? { + if matches!(operation.role(), CarrierRole::RetireIteration { .. }) { + self.carrier_effect(operation, span)?; + } + } + Ok(()) + } + pub(in crate::emit) fn carrier_switch( + &mut self, + scrutinee: &Expression, + variants: &[( + jett_hir::VariantId, + jett_mir::BlockId, + Vec, + )], + otherwise: Option, + span: Span, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + if !super::super::resource_layout::carrier_operations::is_carrier_type( + resource.layout.plan(), + scrutinee.ty, + ) { + return Ok(false); + } + self.expression(scrutinee)?; + let plan = resource.carrier_plan()?; + let mut tag = None; + for row in resource.carrier_at_site()? { + let output = self.carrier_effect(row, span)?; + if matches!( + row.role(), + CarrierRole::Observe { + observation: Observation::Tag, + .. + } + ) { + if tag.is_some() { + return Err(pending("carrier Switch has multiple tag selectors")); + } + let output = output.ok_or_else(|| pending("carrier Switch omitted tag output"))?; + tag = Some(self.scalar(output, span)?); + } + } + let tag = tag.ok_or_else(|| pending("carrier Switch has no exact tag observation"))?; + let Type::Enum(owner) = self.types.resolve(scrutinee.ty) else { + return Err(pending("carrier Switch lost its exact enum type")); + }; + let definition = self.types.resolve_enum(*owner); + for (variant, target, _) in variants { + let discriminator = definition + .variants + .get(variant.index() as usize) + .ok_or_else(|| pending("carrier Switch selected a foreign variant"))? + .discriminant; + let matches = self + .builder + .ins() + .icmp_imm(IntCC::Equal, tag, discriminator); + let selected = self.builder.create_block(); + let next = self.builder.create_block(); + self.builder.ins().brif(matches, selected, &[], next, &[]); + self.builder.switch_to_block(selected); + let edge = Edge::SwitchVariant { + source: resource.block, + variant: *variant, + target: *target, + }; + for operation in plan.operations_on_edge(edge) { + self.carrier_effect(operation, span)?; + } + let target = block_for(self.blocks, target.index(), span, self.symbol)?; + self.drop_temporaries()?; + self.builder.ins().jump(target, &[]); + self.builder.switch_to_block(next); + } + if let Some(target) = otherwise { + for operation in plan.operations_on_edge(Edge::SwitchOtherwise { + source: resource.block, + target, + }) { + self.carrier_effect(operation, span)?; + } + self.drop_temporaries()?; + let target = block_for(self.blocks, target.index(), span, self.symbol)?; + self.builder.ins().jump(target, &[]); + } else { + self.builder.ins().trap(TrapCode::unwrap_user(1)); + } + Ok(true) + } +} + +fn constructor_child(expression: &Expression, index: usize) -> Result<&Expression, CodegenError> { + let child = match &expression.kind { + ExpressionKind::ListConstruct { elements } => elements.get(index), + ExpressionKind::MapConstruct { entries } => entries.get(index / 2).map(|entry| { + if index % 2 == 0 { + &entry.key + } else { + &entry.value + } + }), + ExpressionKind::StructConstruct { fields, .. } => fields.get(index), + ExpressionKind::EnumConstruct { payloads, .. } + | ExpressionKind::MachineConstruct { payloads, .. } => payloads.get(index), + ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) + if index == 0 => + { + Some(inner.as_ref()) + } + _ => None, + }; + child + .ok_or_else(|| pending("carrier child lost its exact original/current constructor operand")) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs new file mode 100644 index 00000000..5bc6b6af --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/leaves.rs @@ -0,0 +1,349 @@ +//! Exact selected039a imports for the dedicated Resource family. +//! No leaf declaration grants Source authority or creates a raw runtime key. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Leaf { + ProgramBegin, + ProgramComplete, + EntryScope, + EntryNetwork, + EntryOutcome, + ScopeValidate, + OperationBegin, + OperationComplete, + HookPrepare, + FactoryPrepare, + FactoryCommit, + BorrowBegin, + BorrowCommit, + BorrowEnd, + Close, + DescriptorClose, + Descriptor, + GraphDescriptorBox, + Transfer, + DestinationFrame, + SumAdopt, + SumTag, + SumBorrow, + SumViewTag, + SumViewProject, + SumFailureRead, + SumBorrowEnd, + SumTake, + SumDrop, + FailureCompanionTake, + AbsentSum, + FailureSum, + Replace, + SourcePrepare, + SourceActual, + SourceEnter, + SourceParameter, + SourceStatus, + ScopeComplete, + ReturnPublish, + CarrierConstructBegin, + CarrierChild, + CarrierChildScalarTask, + CarrierCommit, + CarrierTransfer, + CarrierBorrow, + CarrierExtract, + CarrierAdaptSum, + CarrierObserve, + CarrierObserveScalarTask, + CarrierEndBorrow, + CarrierRetire, + CarrierResetIteration, + CarrierQualify, + CarrierPublish, +} +impl Leaf { + fn spec(self) -> (&'static str, &'static [ir::Type]) { + use Leaf::*; + use ir::types::{I32, I64}; + match self { + ProgramBegin => ("jett_rt_v1_resource_program_begin", &[I64, I64, I64, I64]), + ProgramComplete => ("jett_rt_v1_resource_program_complete", &[I64, I64, I32]), + EntryScope => ("jett_rt_v1_resource_entry_scope", &[I64, I64, I64]), + EntryNetwork => ("jett_rt_v1_resource_entry_network", &[I64, I32, I64, I64]), + EntryOutcome => ( + "jett_rt_v1_resource_entry_outcome", + &[I64, I64, I32, I32, I32, I64], + ), + ScopeValidate => ( + "jett_rt_v1_resource_scope_validate", + &[I64, I64, I32, I32, I32], + ), + OperationBegin => ("jett_rt_v1_resource_operation_begin", &[I64, I64, I32, I64]), + OperationComplete => ("jett_rt_v1_resource_operation_complete", &[I64, I64, I32]), + HookPrepare => ( + "jett_rt_v1_resource_hook_prepare", + &[I64, I64, I32, I64, I64], + ), + FactoryPrepare => ("jett_rt_v1_resource_factory_prepare", &[I64, I64, I64, I64]), + FactoryCommit => ( + "jett_rt_v1_resource_factory_commit", + &[I64, I64, I64, I64, I64], + ), + BorrowBegin => ( + "jett_rt_v1_resource_borrow_begin", + &[I64, I64, I32, I64, I64], + ), + BorrowCommit => ( + "jett_rt_v1_resource_borrow_commit", + &[I64, I64, I64, I64, I64], + ), + BorrowEnd => ("jett_rt_v1_resource_borrow_end", &[I64, I64, I32, I64]), + Close => ("jett_rt_v1_resource_close", &[I64, I64, I32, I64]), + DescriptorClose => ("jett_rt_v1_resource_descriptor_close", &[I64, I64, I64]), + Descriptor => ("jett_rt_v1_resource_descriptor", &[I64, I32, I64]), + GraphDescriptorBox => ( + "jett_rt_v1_resource_graph_descriptor_box", + &[I64, I64, I32, I64, I64], + ), + Transfer => ( + "jett_rt_v1_resource_transfer", + &[I64, I64, I32, I64, I64, I64], + ), + DestinationFrame => ( + "jett_rt_v1_resource_destination_frame", + &[I64, I64, I32, I64], + ), + SumAdopt => ( + "jett_rt_v1_resource_sum_adopt", + &[I64, I64, I32, I64, I64, I64], + ), + SumTag => ("jett_rt_v1_resource_sum_tag", &[I64, I64, I64, I64]), + SumBorrow => ("jett_rt_v1_resource_sum_borrow", &[I64, I64, I32, I64, I64]), + SumViewTag => ( + "jett_rt_v1_resource_sum_view_tag", + &[I64, I64, I32, I64, I64], + ), + SumViewProject => ( + "jett_rt_v1_resource_sum_view_project", + &[I64, I64, I32, I64, I64], + ), + SumFailureRead => ( + "jett_rt_v1_resource_sum_failure_read", + &[I64, I64, I32, I64, I64], + ), + SumBorrowEnd => ("jett_rt_v1_resource_sum_borrow_end", &[I64, I64, I32, I64]), + SumTake => ( + "jett_rt_v1_resource_sum_take", + &[I64, I64, I32, I64, I64, I64], + ), + SumDrop => ("jett_rt_v1_resource_sum_drop", &[I64, I64, I32, I64]), + FailureCompanionTake => ( + "jett_rt_v1_resource_failure_companion_take", + &[I64, I64, I32, I64, I64], + ), + AbsentSum => ("jett_rt_v1_resource_absent_sum", &[I64, I64, I32, I64]), + FailureSum => ( + "jett_rt_v1_resource_failure_sum", + &[I64, I64, I32, I64, I64], + ), + Replace => ( + "jett_rt_v1_resource_replace", + &[I64, I64, I32, I64, I64, I64], + ), + SourcePrepare => ("jett_rt_v1_resource_source_prepare", &[I64, I64, I32, I64]), + SourceActual => ( + "jett_rt_v1_resource_source_actual", + &[I64, I64, I32, I32, I64], + ), + SourceEnter => ("jett_rt_v1_resource_source_enter", &[I64, I64, I64]), + SourceParameter => ( + "jett_rt_v1_resource_source_parameter", + &[I64, I64, I32, I64], + ), + SourceStatus => ("jett_rt_v1_resource_source_status", &[I64, I64]), + ScopeComplete => ("jett_rt_v1_resource_scope_complete", &[I64, I64, I32, I32]), + CarrierConstructBegin => ( + "jett_rt_v1_resource_carrier_construct_begin", + &[I64, I64, I32, I64], + ), + CarrierChild => ("jett_rt_v1_resource_carrier_child", &[I64, I64, I32, I64]), + CarrierChildScalarTask => ( + "jett_rt_v1_resource_carrier_child_scalar_task", + &[I64, I64, I32, I64, I64], + ), + CarrierCommit => ("jett_rt_v1_resource_carrier_commit", &[I64, I64, I64]), + CarrierTransfer => ( + "jett_rt_v1_resource_carrier_transfer", + &[I64, I64, I32, I64, I64], + ), + CarrierBorrow => ( + "jett_rt_v1_resource_carrier_borrow", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierExtract => ( + "jett_rt_v1_resource_carrier_extract", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierAdaptSum => ( + "jett_rt_v1_resource_carrier_adapt", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierObserve => ( + "jett_rt_v1_resource_carrier_observe", + &[I64, I64, I32, I64, I64, I64], + ), + CarrierObserveScalarTask => ( + "jett_rt_v1_resource_carrier_observe_scalar_task", + &[I64, I64, I32, I64, I64, I64, I64], + ), + CarrierEndBorrow => ("jett_rt_v1_resource_carrier_end", &[I64, I64, I32, I64]), + CarrierRetire => ("jett_rt_v1_resource_carrier_retire", &[I64, I64, I32, I64]), + CarrierResetIteration => ( + "jett_rt_v1_resource_carrier_reset_iteration", + &[I64, I64, I32, I64], + ), + CarrierQualify => ( + "jett_rt_v1_resource_carrier_qualify", + &[I64, I64, I32, I64, I64], + ), + CarrierPublish => ( + "jett_rt_v1_resource_carrier_publish", + &[I64, I64, I32, I64, I64], + ), + ReturnPublish => ( + "jett_rt_v1_resource_return_publish", + &[I64, I64, I32, I64, I64], + ), + } + } + pub(super) fn declare( + self, + module: &mut ObjectModule, + ) -> Result { + if module.target_config().pointer_type() != ir::types::I64 { + return Err(pending( + "dedicated Resource leaves require selected64-bit context pointers", + )); + } + let (symbol, params) = self.spec(); + let mut signature = module.make_signature(); + signature + .params + .extend(params.iter().copied().map(AbiParam::new)); + signature.returns.push(AbiParam::new(ir::types::I32)); + module + .declare_function(symbol, Linkage::Import, &signature) + .map_err(|error| CodegenError::Backend(format!("Resource leaf {symbol}: {error}"))) + } + pub(super) fn call( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + ) -> Result { + let (_, params) = self.spec(); + if arguments.len() != params.len() + || arguments + .iter() + .zip(params) + .any(|(value, ty)| builder.func.dfg.value_type(*value) != *ty) + { + return Err(pending( + "Resource leaf actual ABI differs from its selected signature", + )); + } + let id = self.declare(module)?; + let reference = module.declare_func_in_func(id, builder.func); + let call = builder.ins().call(reference, arguments); + builder + .inst_results(call) + .first() + .copied() + .ok_or_else(|| pending("Resource leaf has no exact Status result")) + } + /// The failure block receives this exact Status and runs the full ordinary + /// companion and Resource cleanup path. A success output is read only on0. + pub(super) fn checked( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + failure: ir::Block, + ) -> Result<(), CodegenError> { + let status = self.call(module, builder, arguments)?; + let accepted = builder.create_block(); + let success = builder.ins().icmp_imm(IntCC::Equal, status, 0); + builder + .ins() + .brif(success, accepted, &[], failure, &[status.into()]); + builder.switch_to_block(accepted); + Ok(()) + } + pub(super) fn output( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + failure: ir::Block, + result_bytes: u32, + ) -> Result { + if !matches!(result_bytes, 4 | 8 | 16) || (result_bytes == 4) != (self == Leaf::SumViewTag) + { + return Err(pending( + "Resource out-storage differs from its exact selected leaf layout", + )); + } + let output_type = if result_bytes == 4 { + ir::types::I32 + } else { + ir::types::I64 + }; + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + result_bytes, + 3, + )); + let zero = builder.ins().iconst(output_type, 0); + builder.ins().stack_store(zero, slot, 0); + if result_bytes == 16 { + builder.ins().stack_store(zero, slot, 8); + } + let address = builder.ins().stack_addr(ir::types::I64, slot, 0); + let mut arguments = arguments.to_vec(); + arguments.push(address); + self.checked(module, builder, &arguments, failure)?; + Ok(builder + .ins() + .stack_load(output_type, slot, if result_bytes == 16 { 8 } else { 0 })) + } + + pub(super) fn scalar_task_output( + self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder<'_>, + arguments: &[Value], + failure: ir::Block, + ) -> Result<(Value, Value), CodegenError> { + if self != Leaf::CarrierObserveScalarTask { + return Err(pending( + "scalar transport outputs require their exact selected leaf", + )); + } + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 16, + 3, + )); + let zero = builder.ins().iconst(ir::types::I64, 0); + builder.ins().stack_store(zero, slot, 0); + builder.ins().stack_store(zero, slot, 8); + let bits = builder.ins().stack_addr(ir::types::I64, slot, 0); + let depth = builder.ins().stack_addr(ir::types::I64, slot, 8); + let mut arguments = arguments.to_vec(); + arguments.extend([bits, depth]); + self.checked(module, builder, &arguments, failure)?; + Ok(( + builder.ins().stack_load(ir::types::I64, slot, 0), + builder.ins().stack_load(ir::types::I64, slot, 8), + )) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs new file mode 100644 index 00000000..70dbc079 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/lexical_borrows.rs @@ -0,0 +1,80 @@ +//! Emit constructor-certified lexical loan ends and owner drops; keep RootScope active. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn resource_lexical_retirement(&mut self, span: Span) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("lexical retirement has no selected Resource family"))?; + let plan = resource.function_plan()?; + let Some(exit) = plan.lexical_exit(resource.block, resource.position) else { + return Ok(false); + }; + let function = resource + .layout + .plan() + .program() + .functions + .get(resource.function.index() as usize) + .filter(|function| function.id == resource.function) + .ok_or_else(|| pending("lexical retirement lost its exact current function"))?; + let original = function + .resource_lexical_exit(resource.block, resource.position) + .map_err(|message| pending(&message))? + .ok_or_else(|| pending("lexical retirement lost its private constructor exit"))?; + if original.kind() != exit.kind() + || (exit.kind() == jett_mir::ResourceLexicalExitKind::Return) + != (resource.position == jett_mir::ResourcePosition::Terminator) + { + return Err(pending("lexical retirement changed its exact exit kind")); + } + for id in exit.operation_ids() { + let operation = plan + .operations() + .get(id.index()) + .filter(|operation| { + operation.id() == *id + && operation.site().function() == resource.function + && operation.site().block() == resource.block + && operation.site().position() == resource.position + && !operation.is_expression_operation() + }) + .ok_or_else(|| pending("lexical retirement lost an exact ordered end row"))?; + match operation.role() { + Role::EndBorrow { .. } | Role::EndSumBorrow { .. } => { + self.resource_end_borrow(operation)?; + } + Role::Drop { .. } => self.resource_drop(operation)?, + _ => return Err(pending("lexical retirement selected a non-retirement row")), + } + } + for local in exit.clear_locals() { + let index = local.index() as usize; + let header = self + .local_types + .get(index) + .filter(|header| header.id == *local) + .ok_or_else(|| pending("lexical retirement lost a current alias header"))?; + if !resource.layout.plan().type_requires_custody(header.ty) + || self.local_slots[index].is_some() + { + return Err(pending("lexical retirement selected ordinary storage")); + } + // A consumed owner can be the pending Return operand. Its exact + // Transfer clears the Resource slot after lexical retirement; Drop + // rows above clear slots for owners actually retired here. Only the + // dead Source variable is cleared unconditionally. + let variable = variable_for(self.variables, local.index(), span, self.symbol)? + .ok_or_else(|| pending("lexical retirement lost its nonowning scalar local"))?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder.try_def_var(variable, zero).map_err(|error| { + contract_error( + self.symbol, + span, + format!("cannot clear retired Resource alias: {error}"), + ) + })?; + } + Ok(true) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs new file mode 100644 index 00000000..dd58b59d --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect.rs @@ -0,0 +1,231 @@ +//! The ordinary descriptor selects only the constructor-proved Source body. +//! It never supplies a Resource family's hidden Scope through call_indirect. +use super::*; + +fn descriptor_identity( + builder: &mut FunctionBuilder<'_>, + code: Value, + environment: Value, + expected: Value, +) -> Value { + let selected = builder + .ins() + .icmp(ir::condcodes::IntCC::Equal, code, expected); + let empty = builder + .ins() + .icmp_imm(ir::condcodes::IntCC::Equal, environment, 0); + builder.ins().band(selected, empty) +} + +impl Translator<'_, '_> { + pub(in crate::emit) fn resource_is_named_value( + &self, + expression: &Expression, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + let Some(proof) = resource.function_plan()?.named_callable_value(expression) else { + return Ok(false); + }; + let selected = program_function(resource.layout.plan().program(), proof.function())?; + if proof.signature_type() != expression.ty + || proof.identity() != &selected.identity + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(pending( + "ordinary named descriptor occurrence changes its sealed proof", + )); + } + Ok(true) + } + + pub(in crate::emit) fn resource_validate_named_producer( + &self, + expression: &Expression, + target: FunctionId, + ) -> Result<(), CodegenError> { + let Some(resource) = self.resource else { + return Ok(()); + }; + if resource.layout.plan().function(target).is_none() { + return Ok(()); + } + let proof = resource + .function_plan()? + .named_callable_producer(expression) + .ok_or_else(|| { + pending("Resource family descriptor has no exact current named producer") + })?; + let selected = program_function(resource.layout.plan().program(), target)?; + if !matches!(expression.kind, ExpressionKind::FunctionRef(function) if function == target) + || proof.function() != target + || proof.signature_type() != expression.ty + || proof.identity() != &selected.identity + { + return Err(pending( + "Resource named descriptor changes its sealed producer", + )); + } + Ok(()) + } + + pub(super) fn resource_validate_named_invocation( + &self, + invocation: &jett_mir::ResourceOperation, + callee: Option<&Expression>, + ) -> Result<(), CodegenError> { + let Role::InvokeSourceFunction { + function, source, .. + } = invocation.role() + else { + return Ok(()); + }; + match (&source.target, callee, invocation.named_indirect_target()) { + (jett_hir::CallTarget::Indirect { signature_type }, Some(callee), Some(proof)) => { + let resource = self + .resource + .ok_or_else(|| pending("named Source call lost its family"))?; + let selected = program_function(resource.layout.plan().program(), *function)?; + if proof.function() != *function + || proof.signature_type() != *signature_type + || callee.ty != *signature_type + || proof.identity() != &selected.identity + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(pending( + "named indirect Source call changes its sealed body or tuple", + )); + } + Ok(()) + } + (jett_hir::CallTarget::Indirect { .. }, _, _) => Err(pending( + "indirect Source call has no exact current named target proof", + )), + (_, None, None) => Ok(()), + _ => Err(pending( + "named indirect target proof is attached to a different Source call", + )), + } + } + + pub(super) fn resource_validate_named_descriptor( + &mut self, + descriptor: Value, + invocation: &jett_mir::ResourceOperation, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("named descriptor has no current family"))?; + let proof = invocation + .named_indirect_target() + .ok_or_else(|| pending("named descriptor has no exact invocation proof"))?; + self.leaf(NativeLeaf::FunctionCallableCheck, &[descriptor], true)?; + let code_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_CODE_FIELD as i64); + let code = self.leaf(NativeLeaf::StructField, &[descriptor, code_index], true)?; + let environment_index = self + .builder + .ins() + .iconst(ir::types::I64, NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64); + let environment = self.leaf( + NativeLeaf::StructField, + &[descriptor, environment_index], + true, + )?; + let declaration = self.declarations.get(proof.function()).ok_or_else(|| { + contract_error(self.symbol, span, "named Resource target is not reachable") + })?; + let reference = self + .module + .declare_func_in_func(declaration.native_id, self.builder.func); + let pointer = self.module.target_config().pointer_type(); + let expected = self.builder.ins().func_addr(pointer, reference); + let expected = if pointer == ir::types::I64 { + expected + } else { + self.builder.ins().uextend(ir::types::I64, expected) + }; + let valid = descriptor_identity(self.builder, code, environment, expected); + let accepted = self.builder.create_block(); + let refused = self.builder.ins().iconst(ir::types::I32, 1); + self.builder + .ins() + .brif(valid, accepted, &[], resource.failure, &[refused.into()]); + self.builder.switch_to_block(accepted); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cranelift_codegen::{control::ControlPlane, settings::Configurable}; + + // Optimize the actual emitted predicate, rather than a Rust copy of it. + // Constant inputs expose a code substitution and a nonempty environment + // independently; these controls do not claim native Source execution. + fn optimized_identity(code: i64, environment: i64, expected: i64) -> i64 { + let mut flags = settings::builder(); + flags.set("opt_level", "speed").unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(flags)) + .unwrap(); + let mut context = Context::new(); + context.func.signature = ir::Signature::new(isa.default_call_conv()); + context + .func + .signature + .returns + .push(AbiParam::new(ir::types::I8)); + let mut frontend = FunctionBuilderContext::new(); + { + let mut builder = FunctionBuilder::new(&mut context.func, &mut frontend); + let entry = builder.create_block(); + builder.switch_to_block(entry); + let code = builder.ins().iconst(ir::types::I64, code); + let environment = builder.ins().iconst(ir::types::I64, environment); + let expected = builder.ins().iconst(ir::types::I64, expected); + let accepted = descriptor_identity(&mut builder, code, environment, expected); + builder.ins().return_(&[accepted]); + builder.seal_all_blocks(); + builder.finalize(); + } + context.verify(isa.as_ref()).unwrap(); + context + .optimize(isa.as_ref(), &mut ControlPlane::default()) + .unwrap(); + context.verify(isa.as_ref()).unwrap(); + let returned = context + .func + .layout + .blocks() + .flat_map(|block| context.func.layout.block_insts(block)) + .find(|instruction| context.func.dfg.insts[*instruction].opcode() == ir::Opcode::Return) + .unwrap(); + let result = context + .func + .dfg + .resolve_aliases(context.func.dfg.inst_args(returned)[0]); + let ir::ValueDef::Result(instruction, _) = context.func.dfg.value_def(result) else { + panic!("identity predicate did not reduce to a constant"); + }; + let ir::InstructionData::UnaryImm { imm, .. } = context.func.dfg.insts[instruction] else { + panic!("identity predicate did not reduce to an immediate"); + }; + imm.bits() + } + + #[test] + fn resource_named_indirect_code_identity_and_environment_are_independent_guards() { + assert_eq!(optimized_identity(701, 0, 701), 1); + assert_eq!(optimized_identity(702, 0, 701), 0); + assert_eq!(optimized_identity(701, 522, 701), 0); + assert_eq!(optimized_identity(702, 522, 701), 0); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett new file mode 100644 index 00000000..b75d5af7 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/18_indirect_named_close.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett new file mode 100644 index 00000000..eaa7e5a8 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/19_indirect_named_terminal_failure.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = wrong_target + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett new file mode 100644 index 00000000..c0aa00e0 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/named_indirect/20_indirect_named_alias.jett @@ -0,0 +1,17 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = close_selected + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/native_suite.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/native_suite.rs new file mode 100644 index 00000000..73b18da9 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/native_suite.rs @@ -0,0 +1,162 @@ +//! A closed factory receipt, distinct from Source ownership, enters each suite +//! target through its hidden Scope using the existing ordinary activation ABI. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn resource_native_suite( + &mut self, + expression: &Expression, + invocation: &jett_mir::ResourceOperation, + operations: &[&jett_mir::ResourceOperation], + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("native suite invocation has no selected family"))?; + let Role::InvokeNativeSuite { invocation: suite } = invocation.role() else { + return Err(pending("native suite invocation selected another role")); + }; + let function = suite.function(); + let proof = suite.proof(); + let formals = suite.formals(); + let evaluation_order = suite.evaluation_order(); + let operands = suite.operands(); + let result = suite.result(); + let ExpressionKind::Call { + function: target, + args, + evaluation_order: actual_order, + .. + } = &expression.kind + else { + return Err(pending("native suite receipt lost its exact direct Call")); + }; + if *target != function + || proof.target() != *target + || !proof.matches(self.function.id, expression, proof.expression_ordinal()) + || evaluation_order != actual_order.as_slice() + || args.len() != formals.len() + || operands.len() != args.len() + || expression.ty != TypeInterner::NOTHING + || !matches!(result, CallResult::Ordinary { ty } if ty == TypeInterner::NOTHING) + { + return Err(pending( + "native suite Call changed its exact factory receipt", + )); + } + self.resource_open_operation(invocation.frame())?; + let (context, frame, ordinal) = self.resource_operation(invocation)?; + let call = Leaf::SourcePrepare.output( + self.module, + self.builder, + &[context, frame, ordinal], + resource.failure, + 8, + )?; + let mut evaluated = vec![None; args.len()]; + for (source_index, ¶meter) in evaluation_order.iter().enumerate() { + let argument = args + .get(parameter) + .ok_or_else(|| pending("native suite actual is outside its exact order"))?; + let formal = formals + .get(parameter) + .filter(|formal| formal.parameter() == parameter) + .ok_or_else(|| pending("native suite actual has no exact generated formal"))?; + if formal.source_index() != source_index + || formal.actual_type() != argument.ty + || formal.parameter_type() != argument.ty + || formal.access() != jett_mir::ParamMode::Owned + || formal.effect() != jett_mir::ResourceArgumentEffect::Copy + || formal.syntax() != jett_mir::ResourceArgumentSyntax::Bare + || !matches!(operands.get(parameter), + Some(Operand::Ordinary { parameter: actual, ty }) + if *actual == parameter && *ty == argument.ty) + || resource.layout.plan().type_requires_custody(argument.ty) + || evaluated[parameter].is_some() + { + return Err(pending( + "native suite actual changes its ordinary copy contract", + )); + } + let value = self.argument(argument, false)?; + evaluated[parameter] = Some(value); + let bits = self.payload_bits(value).0; + let source_index = self.builder.ins().iconst( + ir::types::I32, + i64::try_from(source_index) + .map_err(|_| pending("native suite source order exceeds its wire domain"))?, + ); + let parameter = self.builder.ins().iconst( + ir::types::I32, + i64::try_from(parameter) + .map_err(|_| pending("native suite parameter exceeds its wire domain"))?, + ); + Leaf::SourceActual.checked( + self.module, + self.builder, + &[context, call, source_index, parameter, bits], + resource.failure, + )?; + } + let scope = Leaf::SourceEnter.output( + self.module, + self.builder, + &[context, call], + resource.failure, + 8, + )?; + let environment = self.builder.ins().iconst(ir::types::I64, 0); + let mut native = vec![context, environment, scope]; + for (parameter, argument) in args.iter().enumerate() { + let index = self.builder.ins().iconst( + ir::types::I32, + i64::try_from(parameter) + .map_err(|_| pending("native suite parameter exceeds its wire domain"))?, + ); + let installed = Leaf::SourceParameter.output( + self.module, + self.builder, + &[context, scope, index], + resource.failure, + 8, + )?; + // The callee retains its ordinary copy parameter itself. A String + // installation observes the evaluated handle; it mints no second + // caller owner for that same handle. + let scalar = if is_copy_owned(self.types, argument.ty) { + installed + } else { + let unpacked = self.unpack_payload(installed, argument.ty, argument.span)?; + self.scalar(unpacked, argument.span)? + }; + native.push(scalar); + if task_scalar(Some(resource.layout), self.types, argument.ty)? { + let evaluated = evaluated[parameter] + .ok_or_else(|| pending("native suite ordinary argument was not evaluated"))?; + native.push(self.scalar_task(evaluated, argument.span)?.1); + } + } + let declared = self + .declarations + .get(function) + .ok_or_else(|| pending("native suite target has no exact family declaration"))?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let native_call = self.builder.ins().call(reference, &native); + let results = self.builder.inst_results(native_call).to_vec(); + Leaf::SourceStatus.checked( + self.module, + self.builder, + &[context, call], + resource.failure, + )?; + self.check_failure()?; + let [nothing] = results.as_slice() else { + return Err(pending( + "native suite Nothing call changed its single-word ABI", + )); + }; + self.resource_finish_operation(operations, invocation.frame())?; + Ok(LoweredValue::Scalar(*nothing)) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement.rs new file mode 100644 index 00000000..dc79ee49 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement.rs @@ -0,0 +1,70 @@ +//! Exact statement endpoints for the one compound whole-binding operation. +use super::*; + +fn index( + operations: &[&jett_mir::ResourceOperation], + selected: &jett_mir::ResourceOperation, +) -> Result { + operations + .iter() + .position(|operation| std::ptr::eq(*operation, selected)) + .ok_or_else(|| pending("Resource replacement endpoint is outside its exact current site")) +} + +pub(super) fn validate_rows( + function: &jett_mir::ResourceFunctionPlan, + operations: &[&jett_mir::ResourceOperation], + replace: &jett_mir::ResourceOperation, + transfer: &jett_mir::ResourceOperation, +) -> Result<(), CodegenError> { + let expected = function + .operations() + .iter() + .filter(|operation| { + operation.site() == replace.site() && !operation.is_expression_operation() + }) + .collect::>(); + if operations.len() != expected.len() + || operations + .iter() + .zip(&expected) + .any(|(actual, expected)| !std::ptr::eq(*actual, *expected)) + { + return Err(pending( + "Resource replacement changed its complete current endpoint order", + )); + } + let Role::Replace { + destination, + replacement, + old, + } = replace.role() + else { + return Err(pending( + "Resource replacement lacks its exact Replace constructor", + )); + }; + if !matches!(transfer.role(), Role::Transfer { source, destination: selected } + if source == replacement && selected == destination) + { + return Err(pending( + "Resource replacement lost its exact evaluated RHS transfer", + )); + } + let mut drops = operations.iter().copied().filter( + |operation| matches!(operation.role(), Role::Drop { source, .. } if source == destination), + ); + let drop = drops + .next() + .ok_or_else(|| pending("Resource replacement lost its displaced-value cleanup"))?; + if drops.next().is_some() + || !matches!(drop.role(), Role::Drop { occupancy, .. } if occupancy == old) + || index(operations, replace)? >= index(operations, drop)? + || index(operations, drop)? >= index(operations, transfer)? + { + return Err(pending( + "Resource replacement changed its exact cleanup/transfer correspondence", + )); + } + Ok(()) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_optional.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_optional.jett new file mode 100644 index 00000000..d8ff63ab --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_optional.jett @@ -0,0 +1,26 @@ +namespace app +function select(view net: Network, label: int64, present: bool) returns optional[resource_probe.TestHandle]: + use resource_probe + if present: + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return none + return some(owner) + return none +export function main(net: Network) returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = select(view net, 2801, true) + token = select(view net, 2899, false) + mutable int64 index = 0 + while index < 2: + if index == 0: + token = none + else: + token = select(view net, 2802, true) + index = index + 1 + resource_probe.TestHandle final = token handle: + return nothing + int64 observed = resource_probe.borrow(view net, view final) handle error: + return nothing + resource_probe.close(final) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_result.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_result.jett new file mode 100644 index 00000000..7a46e264 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_result.jett @@ -0,0 +1,12 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable result[resource_probe.TestHandle, string] token = fail("before") + token = resource_probe.create(view net, 2901) + token = resource_probe.create(view net, 2902) + resource_probe.TestHandle final = token handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view final) handle error: + return nothing + resource_probe.close(final) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/emission_sums_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/emission_sums_tests.rs new file mode 100644 index 00000000..1ccd5180 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/emission_sums_tests.rs @@ -0,0 +1,319 @@ +//! New whole Sources, actual private endpoints, and canonical pure suites. +use super::*; + +const ORIGINAL: &str = include_str!("original_optional_assignment.jett"); +const OPTIONAL: &str = include_str!("optional_transitions.jett"); +const RESULT: &str = include_str!("result_transitions.jett"); +const PRIMITIVE: &str = include_str!("primitive_failure.jett"); +const PURE: &str = include_str!("pure_replacement_suites.jett"); +const CONDITIONAL_OPTIONAL: &str = include_str!("conditional_optional.jett"); +const CONDITIONAL_RESULT: &str = include_str!("conditional_result.jett"); + +#[test] +fn resource_native_direct_sum_replacements_emit_the_existing_compound_leaf() { + for release in [false, true] { + for source in [ + ORIGINAL, + OPTIONAL, + RESULT, + PRIMITIVE, + CONDITIONAL_OPTIONAL, + CONDITIONAL_RESULT, + ] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_replace") + ); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_scope_complete") + ); + assert!(!artifact.bytes.is_empty()); + } + } +} + +#[test] +fn resource_native_replacement_endpoint_projection_refuses_foreign_dropped_duplicated_reordered_rows() + { + for release in [false, true] { + let checked = checked(ORIGINAL, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let function = layout.plan().function(entry(&program)).unwrap(); + let replace = function + .operations() + .iter() + .find(|operation| matches!(operation.role(), Role::Replace { .. })) + .unwrap(); + let Role::Replace { + destination, + replacement, + .. + } = replace.role() + else { + unreachable!(); + }; + let rows = function + .operations() + .iter() + .filter(|operation| { + operation.site() == replace.site() && !operation.is_expression_operation() + }) + .collect::>(); + let transfer = rows + .iter() + .copied() + .find(|operation| { + matches!(operation.role(), Role::Transfer { source, destination: selected } + if source == replacement && selected == destination) + }) + .unwrap(); + let drop_index = rows.iter().position(|operation| + matches!(operation.role(), Role::Drop { source, .. } if source == destination) + ).unwrap(); + let transfer_index = rows + .iter() + .position(|operation| std::ptr::eq(*operation, transfer)) + .unwrap(); + let validate = |current: &[&jett_mir::ResourceOperation]| { + super::super::replacement::validate_rows(function, current, replace, transfer) + }; + validate(&rows).unwrap(); + let mut changed = rows.clone(); + changed.remove(drop_index); + assert!(validate(&changed).is_err()); + let mut changed = rows.clone(); + changed.push(rows[drop_index]); + assert!(validate(&changed).is_err()); + let mut changed = rows.clone(); + changed.swap(drop_index, transfer_index); + assert!(validate(&changed).is_err()); + + let independent_checked = super::checked(ORIGINAL, release); + let independent_program = lower_source(&independent_checked); + let independent_layout = EmittedResourceLayout::from_program( + &independent_program, + &independent_checked.checked().interner, + entry(&independent_program), + ) + .unwrap(); + let independent = independent_layout + .plan() + .function(entry(&independent_program)) + .unwrap(); + let foreign_rows = independent + .operations() + .iter() + .filter(|operation| { + operation.site() == replace.site() && !operation.is_expression_operation() + }) + .collect::>(); + assert!(!foreign_rows.is_empty()); + assert!(validate(&foreign_rows).is_err()); + } +} + +#[test] +fn resource_native_direct_sum_replacement_refuses_changed_mutable_header_and_rhs() { + for release in [false, true] { + let checked = checked(ORIGINAL, release); + let program = lower_source(&checked); + let selected = entry(&program); + let mut changed = program.clone(); + let local = changed.functions[selected.index() as usize] + .locals + .iter_mut() + .find(|local| local.mutable) + .unwrap(); + local.mutable = false; + assert!( + super::super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .is_err() + ); + let mut changed = program.clone(); + let statement = changed.functions[selected.index() as usize] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, StatementKind::Assign { .. })) + .unwrap(); + let StatementKind::Assign { target, value } = &mut statement.kind else { + unreachable!(); + }; + *value = target.clone(); + assert!( + super::super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .is_err() + ); + } +} + +#[test] +fn resource_native_pure_sum_replacements_use_real_required_verify_and_property_entries() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new( + jett_comptime::checked_types::CheckedExpressionTypes::from_checked_program( + &checked, + ), + ), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + assert_eq!(evaluation.values.len(), 6); + let mut constants = 0; + for item in &checked.module().items { + let Item::VarDecl(declaration) = item else { + continue; + }; + if declaration.name.span.file != FileId::new(0) { + continue; + } + let expected = match declaration.name.name.as_str() { + "baked_optional" => jett_comptime::Value::Int64(7), + "baked_string" => jett_comptime::Value::String("after".into()), + "baked_integer" => jett_comptime::Value::Int64(23), + name => panic!("unexpected Source constant: {name}"), + }; + let jett_parser::ast::Expr::Comptime(_, span) = &declaration.value else { + panic!("original required initializer"); + }; + assert_eq!( + evaluation + .values + .get(*span, &jett_comptime::ComptimeContext::default()), + Some(&expected), + ); + constants += 1; + } + assert_eq!(constants, 3); + evaluation.values.checked_required_values(&checked).unwrap(); + let values = Arc::new(evaluation.values); + let mut hir = jett_hir::lower_checked_resource_program_with_test_bodies(&checked).unwrap(); + jett_hir::bake_required_values( + &mut hir, + &values, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + let verify = + jett_hir::append_checked_native_verify_suite(&mut hir, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + let cases = jett_comptime::verify::collect_checked_property_cases_with_checked_values( + checked.clone(), + values.clone(), + ) + .unwrap(); + assert!(values.same_checked_cache(cases.required_values())); + let property = jett_hir::append_checked_native_property_suite( + &mut hir, + &checked, + FileId::new(0), + &cases, + ) + .unwrap() + .unwrap(); + assert_eq!(property.trials().len(), 100); + jett_hir::complete_value_conversions(&mut hir, types).unwrap(); + jett_hir::validate(&hir).unwrap(); + let program = jett_mir::lower(&hir, types).unwrap(); + // The exact MIR companion proof distinguishes primitive bits from an + // ordinary owning handle. Emission must honor that bound, rather than + // allocate a fake owner for the integer failure payload. + let ownership = jett_mir::validate_resource_ownership(&program, types).unwrap(); + for (name, expected_type, owns_payload) in [ + ("integer_marker", TypeInterner::INT64, false), + ("string_marker", TypeInterner::STRING, true), + ] { + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap(); + let companion = + jett_mir::ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + let targets = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + StatementKind::SumTake { + target, + success: false, + .. + } => Some(target), + _ => None, + }) + .collect::>(); + assert_eq!(targets.len(), 1, "one genuine {name} failure extraction"); + let target = targets[0]; + assert_eq!(function.local(target).unwrap().ty, expected_type); + assert_eq!( + companion + .storage() + .owned_locals + .contains(&(target.index() as usize)), + owns_payload, + "{name} payload storage must follow its type" + ); + if owns_payload { + assert!(companion.storage().temporary_slots > 0); + } else { + assert_eq!(companion.storage().temporary_slots, 0); + } + } + for selected in [verify, property.entry()] { + let artifact = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (_, imports) = symbols(&artifact); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_replace") + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/layout_sums_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/layout_sums_tests.rs new file mode 100644 index 00000000..1f525e22 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/layout_sums_tests.rs @@ -0,0 +1,87 @@ +//! Genuine direct-sum sources retain their exact typed replacement rows. +use super::*; + +const SOURCES: &[&str] = &[ + include_str!("optional_transitions.jett"), + include_str!("result_transitions.jett"), + include_str!("primitive_failure.jett"), + include_str!("conditional_optional.jett"), + include_str!("conditional_result.jett"), +]; + +#[test] +fn resource_layout_direct_sum_replacements_preserve_shape_occupancy_and_scope() { + for release in [false, true] { + let mut saw_empty = false; + let mut saw_occupied = false; + let mut saw_conditional = false; + let mut saw_optional = false; + let mut saw_result = false; + for source in SOURCES { + let checked = checked(source, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut replacements = 0; + for function in layout.plan().functions() { + for operation in function.operations() { + let Role::Replace { + destination, + replacement, + old, + } = operation.role() + else { + continue; + }; + replacements += 1; + let destination_slot = &function.owner_slots()[destination.index()]; + let rhs_slot = &function.owner_slots()[replacement.index()]; + assert_ne!(destination, replacement); + assert_eq!(destination_slot.shape(), rhs_slot.shape()); + assert_eq!(destination_slot.frame(), operation.frame()); + assert_eq!(rhs_slot.frame(), operation.frame()); + assert_eq!( + function.frames()[operation.frame().index()].role(), + custody::ResourceFrameRole::Scope + ); + assert!(matches!(destination_slot.storage(), + custody::ResourceSlotStorage::Local { header } if header.mutable)); + match old { + custody::ResourceOccupancy::Empty => saw_empty = true, + custody::ResourceOccupancy::Occupied => saw_occupied = true, + custody::ResourceOccupancy::Conditional => saw_conditional = true, + } + match destination_slot.shape() { + custody::ResourceShape::Optional { .. } => saw_optional = true, + custody::ResourceShape::Result { .. } => saw_result = true, + custody::ResourceShape::Plain { .. } => { + panic!("sum Source unexpectedly uses Plain replacement") + } + } + let ordinal = layout + .operation(function.function(), operation.id()) + .unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1, + [ + 12, + layout + .frame(function.function(), operation.frame()) + .unwrap(), + layout.slot(function.function(), *destination).unwrap(), + layout.slot(function.function(), *replacement).unwrap(), + ] + ); + } + } + assert!(replacements >= 2); + } + assert!(saw_empty && saw_occupied && saw_conditional && saw_optional && saw_result); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett new file mode 100644 index 00000000..8fc9b31a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett @@ -0,0 +1,44 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function replace_live(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_after_close(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_self(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing +export function failed_rhs_keeps_owner(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + replace_live(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/optional_transitions.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/optional_transitions.jett new file mode 100644 index 00000000..89d64e4a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/optional_transitions.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = resource_probe.empty() + resource_probe.TestHandle first = resource_probe.create(view net, 2501) handle error: + return nothing + token = some(first) + token = none + resource_probe.TestHandle second = resource_probe.create(view net, 2502) handle error: + return nothing + token = some(second) + resource_probe.TestHandle third = resource_probe.create(view net, 2503) handle error: + return nothing + token = some(third) + resource_probe.TestHandle final = token handle: + return nothing + int64 observed = resource_probe.borrow(view net, view final) handle error: + return nothing + resource_probe.close(final) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/original_optional_assignment.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/original_optional_assignment.jett new file mode 100644 index 00000000..6f0d7bf3 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/original_optional_assignment.jett @@ -0,0 +1,6 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = resource_probe.empty() + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/primitive_failure.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/primitive_failure.jett new file mode 100644 index 00000000..92463da6 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/primitive_failure.jett @@ -0,0 +1,10 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable result[resource_probe.TestHandle, int64] token = fail(1) + token = fail(2) + resource_probe.TestHandle first = resource_probe.create(view net, 2701) handle error: + return nothing + token = ok(first) + token = fail(3) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/pure_replacement_suites.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/pure_replacement_suites.jett new file mode 100644 index 00000000..13e18434 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/pure_replacement_suites.jett @@ -0,0 +1,43 @@ +namespace app +function optional_marker() returns int64: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = resource_probe.empty() + resource_probe.TestHandle unexpected = token handle: + return 7 + return 9 +function string_marker() returns string: + use resource_probe + mutable result[resource_probe.TestHandle, string] token = fail("before") + token = fail("after") + resource_probe.TestHandle unexpected = token handle error: + return error + return "unexpected" +function integer_marker() returns int64: + use resource_probe + mutable result[resource_probe.TestHandle, int64] token = fail(11) + token = fail(23) + resource_probe.TestHandle unexpected = token handle error: + return error + return 0 +int64 baked_optional = comptime optional_marker() +string baked_string = comptime string_marker() +int64 baked_integer = comptime integer_marker() +export function main(net: Network) returns nothing: + int64 selected = optional_marker() + string text = string_marker() + int64 number = integer_marker() + return nothing +verify pure_replacements: + assert optional_marker() == baked_optional + assert string_marker() == baked_string + assert integer_marker() == baked_integer + assert baked_optional == 7 + assert baked_string == "after" + assert baked_integer == 23 +property pure_replacements_trials: + given input: int64 + assert optional_marker() == 7 + assert string_marker() == "after" + assert integer_marker() == 23 + assert input == input diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/result_transitions.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/result_transitions.jett new file mode 100644 index 00000000..2ad9a0e5 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/replacement/result_transitions.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable result[resource_probe.TestHandle, string] token = fail("before") + token = fail("after") + resource_probe.TestHandle first = resource_probe.create(view net, 2601) handle error: + return nothing + token = ok(first) + token = fail("displaced") + resource_probe.TestHandle second = resource_probe.create(view net, 2602) handle error: + return nothing + token = ok(second) + resource_probe.TestHandle third = resource_probe.create(view net, 2603) handle error: + return nothing + token = ok(third) + resource_probe.TestHandle final = token handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view final) handle error: + return nothing + resource_probe.close(final) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs new file mode 100644 index 00000000..f0e3dec4 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/returned_hooks.rs @@ -0,0 +1,171 @@ +//! Opaque hook metadata uses exact constructor proof, never ordinary callable storage. +use super::*; + +impl Translator<'_, '_> { + pub(in crate::emit) fn resource_is_hook_descriptor( + &self, + expression: &Expression, + ) -> Result { + let Some(resource) = self.resource else { + return Ok(false); + }; + let Some(hook) = resource.function_plan()?.descriptor_value(expression) else { + return Ok(false); + }; + if hook.function_type() != expression.ty + || !resource + .layout + .plan() + .program() + .resource_manifest + .contains_hook(hook) + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(pending( + "opaque descriptor occurrence changes its exact hook proof", + )); + } + Ok(true) + } + + pub(super) fn resource_validate_hook_invocation( + &self, + invocation: &jett_mir::ResourceOperation, + callee: Option<&Expression>, + ) -> Result<(), CodegenError> { + let Role::InvokeHook { hook, source, .. } = invocation.role() else { + return Ok(()); + }; + match (&source.target, callee, invocation.indirect_hook_target()) { + (jett_hir::CallTarget::Indirect { signature_type }, Some(callee), Some(proof)) => { + if proof != hook + || proof.function_type() != *signature_type + || callee.ty != *signature_type + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + || !self.resource_is_hook_descriptor(callee)? + || self + .resource + .expect("selected family") + .function_plan()? + .descriptor_value(callee) + != Some(proof) + { + return Err(pending( + "indirect hook changes its exact descriptor, signature or Source tuple", + )); + } + Ok(()) + } + (jett_hir::CallTarget::Indirect { .. }, _, _) => Err(pending( + "indirect hook has no exact current descriptor target proof", + )), + (_, None, None) => Ok(()), + _ => Err(pending( + "hook descriptor proof is attached to a different original call", + )), + } + } + + pub(super) fn resource_hook_value( + &mut self, + expression: &Expression, + operations: &[&jett_mir::ResourceOperation], + ) -> Result { + let resource = self + .resource + .ok_or_else(|| pending("descriptor has no family"))?; + let hook = resource + .function_plan()? + .descriptor_value(expression) + .ok_or_else(|| pending("descriptor has no exact current occurrence"))?; + let value = match &expression.kind { + ExpressionKind::Local(local) => { + if resource.function_plan()?.descriptor_local(*local) != Some(hook) + || self.local_slots[local.index() as usize].is_some() + { + return Err(pending( + "opaque descriptor Local gained ordinary ownership or changed its header", + )); + } + let variable = self.variables[local.index() as usize] + .ok_or_else(|| pending("opaque descriptor Local has no native variable"))?; + self.builder.try_use_var(variable).map_err(|error| { + pending(&format!( + "opaque descriptor Local is not initialized: {error}" + )) + })? + } + ExpressionKind::Clone(inner) => { + if resource.function_plan()?.descriptor_value(inner) != Some(hook) { + return Err(pending( + "opaque descriptor alias changes its exact producer", + )); + } + let lowered = self.expression(inner)?; + self.scalar(lowered, expression.span)? + } + ExpressionKind::ActorGraphStep { instruction, args } if matches!(instruction.operation(),jett_hir::ActorGraphOperation::HookBox {hook:producer,..} if producer == hook) => + { + let mut selected=operations.iter().copied().filter(|operation| + matches!(operation.role(),Role::Descriptor {hook:current} if current == hook)); + let operation = selected + .next() + .ok_or_else(|| pending("graph hook lost its original Descriptor row"))?; + if selected.next().is_some() { + return Err(pending("graph hook has duplicate Descriptor rows")); + } + let context = self.resource_context(); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + let descriptor = Leaf::Descriptor.output( + self.module, + self.builder, + &[context, ordinal], + resource.failure, + 8, + )?; + let ticket = self.expression(&args[0])?; + let ticket = self.scalar(ticket, args[0].span)?; + let boxed = Leaf::GraphDescriptorBox.output( + self.module, + self.builder, + &[context, ticket, ordinal, descriptor], + resource.failure, + 8, + )?; + return self.own(boxed); + } + ExpressionKind::ResourceHookValue { hook: producer } if producer == hook => { + let mut selected = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::Descriptor { hook: current } if current == hook)); + let operation = selected + .next() + .ok_or_else(|| pending("hook value lost its exact descriptor row"))?; + if selected.next().is_some() { + return Err(pending("hook value has duplicate descriptor rows")); + } + let context = self.resource_context(); + let ordinal = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(resource.ordinal(operation)?)); + Leaf::Descriptor.output( + self.module, + self.builder, + &[context, ordinal], + resource.failure, + 8, + )? + } + _ => { + return Err(pending( + "opaque descriptor transport has no exact native carrier route", + )); + } + }; + Ok(LoweredValue::Scalar(value)) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett new file mode 100644 index 00000000..97660978 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/09_later_handle_minimal.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 502) handle error: + return nothing + accept_staged(token: token, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett new file mode 100644 index 00000000..a56e1c27 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/10_later_handle_original.jett @@ -0,0 +1,24 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett new file mode 100644 index 00000000..0e951718 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/11_later_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 503) handle error: + return nothing + accept_staged(token: token, marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett new file mode 100644 index 00000000..b0e4afff --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/12_later_handle_written_view.jett @@ -0,0 +1,22 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function observe_retained(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 retained = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 505) handle error: + return nothing + accept_staged(token: view token, marker: fail_marker() handle error: + observe_retained(view net, view token) + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett new file mode 100644 index 00000000..c7bd4031 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/13_first_handle_failure.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 506) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett new file mode 100644 index 00000000..fb4e94f8 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/staged_call/14_first_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 507) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs new file mode 100644 index 00000000..5f5117b2 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/sum_views.rs @@ -0,0 +1,101 @@ +//! Nonconsuming conditional Resource projections from the fresh current-CFG plan. +use super::*; + +impl Translator<'_, '_> { + pub(super) fn resource_borrowed_sum_statement( + &mut self, + statement: &Statement, + operations: &[&jett_mir::ResourceOperation], + ) -> Result { + match &statement.kind { + StatementKind::SumTag { target, .. } => { + let mut matching = operations.iter().copied().filter(|operation| + matches!(operation.role(), Role::ObserveSumView { target: exact, .. } if exact == target)); + let Some(operation) = matching.next() else { + return Ok(false); + }; + if matching.next().is_some() { + return Err(pending("sum view tag has ambiguous current authority")); + } + self.resource_sum_view_tag(operation, *target, statement.span)?; + Ok(true) + } + StatementKind::SumTake { + target, success, .. + } => { + let mut matching = operations.iter().copied().filter(|operation| { + if *success { matches!(operation.role(), Role::ProjectSumView { .. }) } + else { matches!(operation.role(), Role::ReadFailureCompanion { target: exact, .. } if exact.id == *target) } + }); + let Some(operation) = matching.next() else { + return Ok(false); + }; + if matching.next().is_some() { + return Err(pending( + "sum view projection has ambiguous current authority", + )); + } + self.resource_sum_view_projection(operation, *target, statement.span)?; + Ok(true) + } + _ => Ok(false), + } + } + fn resource_sum_view_tag( + &mut self, + operation: &jett_mir::ResourceOperation, + target: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("sum view tag has no exact family"))?; + let Role::ObserveSumView { source, .. } = operation.role() else { + return Err(pending("sum view tag selected another role")); + }; + let value = self.resource_loan(*source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let tag = Leaf::SumViewTag.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 4, + )?; + let tag = self.builder.ins().ireduce(ir::types::I8, tag); + self.define_local(target, LoweredValue::Scalar(tag), span) + } + fn resource_sum_view_projection( + &mut self, + operation: &jett_mir::ResourceOperation, + target: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let resource = self + .resource + .ok_or_else(|| pending("sum view extraction has no exact family"))?; + let (source, leaf) = match operation.role() { + Role::ProjectSumView { source, .. } => (*source, Leaf::SumViewProject), + Role::ReadFailureCompanion { source, .. } => (*source, Leaf::SumFailureRead), + _ => return Err(pending("sum view extraction selected another role")), + }; + let value = self.resource_loan(source)?; + let (context, frame, ordinal) = self.resource_operation(operation)?; + let bits = leaf.output( + self.module, + self.builder, + &[context, frame, ordinal, value], + resource.failure, + 8, + )?; + let lowered = if let Role::ProjectSumView { destination, .. } = operation.role() { + self.builder + .ins() + .stack_store(bits, resource.loans[destination.index()], 0); + LoweredValue::Scalar(bits) + } else { + self.own(bits)? + }; + self.define_local(target, lowered, span) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs new file mode 100644 index 00000000..97433fab --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests.rs @@ -0,0 +1,666 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; +mod graph_metadata_constructors; +mod lexical_borrows; +mod named_indirect; +mod occupied_structs; +mod ordinary_entry; +mod program_entry; +#[path = "replacement/emission_sums_tests.rs"] +mod replacement_sums; +mod returned_hooks; +mod sum_views; +const SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +fn checked(source: &str, release: bool) -> Arc { + checked_support(source, SUPPORT, release) +} + +fn checked_companion(source: &str, release: bool) -> Arc { + let support = format!( + "{SUPPORT}\n{}", + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett" + ) + ); + checked_support(source, &support, release) +} + +fn checked_support(source: &str, support: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +const SOURCE: &str = include_str!("../resource_layout/connected_entry.jett"); +fn lower_source(checked: &Arc) -> Program { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_mir::lower(&hir, &checked.checked().interner).unwrap() +} +fn entry(program: &Program) -> FunctionId { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() + .id +} + +fn emitted(source: &str, release: bool) -> ObjectArtifact { + let checked = checked(source, release); + let program = lower_source(&checked); + super::super::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap() +} +fn symbols(artifact: &ObjectArtifact) -> (Vec, Vec) { + use object::{Object, ObjectSymbol}; + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let mut definitions = Vec::new(); + let mut imports = Vec::new(); + for symbol in file.symbols() { + if let Ok(name) = symbol.name() { + if symbol.is_definition() { + definitions.push(name.into()); + } else if symbol.is_undefined() { + imports.push(name.into()); + } + } + } + (definitions, imports) +} +#[test] +fn resource_native_body_emits_original_connected_source_lifecycle_and_selected_leaves() { + for release in [false, true] { + let artifact = emitted(SOURCE, release); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + definitions + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + for required in [ + "entry_scope", + "entry_network", + "entry_outcome", + "scope_validate", + "source_enter", + "source_parameter", + "source_status", + "factory_commit", + "borrow_commit", + "close", + "sum_take", + "failure_companion_take", + "scope_complete", + "return_publish", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")), + "{required}: {imports:?}" + ); + } + } +} +#[test] +fn resource_native_body_preserves_domain_abi_plus_exact_hidden_scope_without_public_scalar_admission() + { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let helper = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pass_owner" + }) + .unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let module = ObjectModule::new( + ObjectBuilder::new(isa, "resource_signature", default_libcall_names()).unwrap(), + ); + let selected = signature(&module, helper, types, &layout).unwrap(); + assert_eq!(selected.params.len(), 4); + assert!( + selected + .params + .iter() + .all(|parameter| parameter.value_type == ir::types::I64) + ); + assert_eq!(selected.returns.len(), 1); + assert_eq!(selected.returns[0].value_type, ir::types::I64); + assert!(scalar_kind(types, helper.return_type, "public Resource scalar gate").is_err()); + assert!(MoveValuePlan::analyze(&program, helper, types).is_err()); + } +} +#[test] +fn resource_native_body_stages_owned_actual_before_a_later_fallible_source_actual() { + let source = r#"namespace app +function consume(token: resource_probe.TestHandle, label: int64) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + consume(token, resource_probe.terminal_label()) + return nothing +"#; + for release in [false, true] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "transfer", + "operation_begin", + "operation_complete", + "scope_complete", + "source_actual", + "source_parameter", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")) + ); + } + } +} +#[test] +fn resource_native_body_authenticates_original_source_before_emission_and_keeps_absent_failure_shells_typed() + { + let source = r#"namespace app +function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] absent = resource_probe.empty() + result[resource_probe.TestHandle, string] rejected = fail("label") + return nothing +"#; + for release in [false, true] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "absent_sum", + "failure_sum", + "scope_complete", + "return_publish", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{required}")) + ); + } + let checked = checked(SOURCE, release); + let original = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&original); + for mutation in 0..3 { + let mut changed = original.clone(); + match mutation { + 0 => { + changed.functions[selected.index() as usize].params[0].mode = + jett_mir::ParamMode::View + } + 1 => changed.functions[selected.index() as usize].blocks[0] + .statements + .clear(), + 2 => changed.resource_manifest = jett_hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + super::super::emit_for_triple( + &changed, + types, + HOST, + Some(selected), + CodegenOptions::default() + ) + .is_err() + ); + } + } +} + +#[test] +fn resource_native_body_refuses_ordinary_descriptor_abi_for_an_exact_hidden_scope_family_target() { + let source = r#"namespace app +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + function(view Network) returns nothing callback = lifecycle + callback(view net) + return nothing +"#; + for release in [false, true] { + let checked = checked(source, release); + let program = lower_source(&checked); + let error = super::super::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap_err(); + assert!( + matches!(error, CodegenError::UnsupportedMir { construct, .. } if construct == "pending Resource family descriptor hidden Scope ABI") + ); + } +} + +#[test] +fn resource_native_replacement_original_source_emits_replace_reseat_and_vacant_routes() { + const SOURCE: &str = include_str!("replacement/mutable_assignment.jett"); + for release in [false, true] { + for (selected, replaces) in [ + ("replace_live", true), + ("rebind_self", false), + ("rebind_after_close", false), + ("failed_rhs_keeps_owner", true), + ] { + // Native verification deliberately emits every fresh plan function. + // Isolate this exact actual Source function so an unused live-replace + // helper cannot supply a misleading Replace import for a reseat. + let mut declarations = SOURCE.split("export function "); + let prefix = declarations.next().unwrap(); + let body = declarations + .find(|body| body.starts_with(&format!("{selected}("))) + .unwrap(); + let source = format!( + "{prefix}export function {body}export function main(net: Network) returns nothing: + {selected}(view net) + return nothing +" + ); + let artifact = emitted(&source, release); + let (_, imports) = symbols(&artifact); + assert_eq!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_replace"), + replaces, + "{selected}: {imports:?}" + ); + for leaf in ["source_enter", "scope_complete", "transfer", "close"] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")), + "{selected}/{leaf}: {imports:?}" + ); + } + } + } +} + +#[test] +fn resource_native_replacement_ordinary_rhs_failure_keeps_original_owner_until_cleanup() { + const SOURCE: &str = r#"namespace app +function replacement_label() returns int64: + use resource_probe + return resource_probe.terminal_label() +export function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2441) handle error: + return nothing + token = resource_probe.create(view net, replacement_label()) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let main = layout.plan().function(entry(&program)).unwrap(); + assert!( + main.operations() + .iter() + .any(|operation| matches!(operation.role(), Role::Replace { .. })) + ); + let artifact = emitted(SOURCE, release); + let (_, imports) = symbols(&artifact); + for leaf in ["replace", "operation_complete", "scope_complete"] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")) + ); + } + assert!(imports.iter().any(|name| name.contains("failure"))); + } +} + +#[test] +fn resource_staged_real_source_emits_cross_block_prepare_stage_invoke_and_retirement_leaves() { + for release in [false, true] { + for source in [ + include_str!("staged_call/09_later_handle_minimal.jett"), + include_str!("staged_call/10_later_handle_original.jett"), + include_str!("staged_call/11_later_handle_success.jett"), + include_str!("staged_call/12_later_handle_written_view.jett"), + include_str!("staged_call/13_first_handle_failure.jett"), + include_str!("staged_call/14_first_handle_success.jett"), + ] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + assert!( + original + .functions + .iter() + .any(|function| function.blocks.iter().any(|block| block + .statements + .iter() + .any(|statement| matches!( + statement.kind, + StatementKind::ResourceCall(jett_mir::ResourceCallNode::End { + outcome: jett_mir::ResourceCompletion::Abort, + .. + }) + )))) + ); + for function in &original.functions { + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + value, + .. + }) = &statement.kind + { + assert!(!matches!(value.kind, ExpressionKind::RuntimeFailure(_))); + } + } + } + } + let artifact = super::super::emit_for_triple( + &original, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (_, imports) = symbols(&artifact); + for leaf in [ + "source_prepare", + "source_actual", + "source_enter", + "source_parameter", + "source_status", + "borrow_begin", + "borrow_end", + "operation_begin", + "operation_complete", + "scope_complete", + "entry_outcome", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")), + "{leaf}: {imports:?}" + ); + } + // This inspects an emitted object only. Provider events and all-before-destroy + // outcomes belong to Root's linked Source-deleted conformance gate. + } + } +} + +#[test] +fn resource_staged_object_refuses_altered_ordinary_endpoint_and_forged_current_node() { + let source = include_str!("staged_call/09_later_handle_minimal.jett"); + for release in [false, true] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + let (function, block, index, local) = original + .functions + .iter() + .find_map(|function| { + function.blocks.iter().find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| match &statement.kind { + StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + ordinary: Some(local), + .. + }) => Some((function.id, block.id, index, *local)), + _ => None, + }) + }) + }) + .unwrap(); + for mutation in 0..3 { + let mut changed = original.clone(); + let current = &mut changed.functions[function.index() as usize]; + match mutation { + 0 => { + assert!(current.locals[local.index() as usize].mutable); + current.locals[local.index() as usize].mutable = false; + } + 1 => { + let StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + ordinary, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *ordinary = Some(current.params[0].local); + } + 2 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .push(copied); + } + _ => unreachable!(), + } + assert!( + super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ) + .is_err(), + "mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_staged_scope_sum_keeps_active_operation_rows_and_forgery_refusal() { + for release in [false, true] { + for source in [ + include_str!("staged_call/13_first_handle_failure.jett"), + include_str!("staged_call/14_first_handle_success.jett"), + ] { + let checked = checked(source, release); + let original = lower_source(&checked); + let selected = entry(&original); + let layout = EmittedResourceLayout::from_program( + &original, + &checked.checked().interner, + selected, + ) + .unwrap(); + let flow = layout.plan().function(selected).unwrap(); + let active = flow + .operations() + .iter() + .find(|operation| matches!(operation.role(), Role::BeginSourceFunction { .. })) + .unwrap() + .frame(); + let mut extractions = 0; + for operation in flow.operations() { + if matches!( + operation.role(), + Role::SumTake { .. } | Role::TakeFailureCompanion { .. } + ) { + extractions += 1; + assert_eq!(operation.frame(), active); + assert!(layout.operation(selected, operation.id()).is_some()); + } + } + assert_eq!(extractions, 2); + let main = &original.functions[selected.index() as usize]; + let (block, index) = main + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::SumTag { .. }) + .then_some((block.id, index)) + }) + }) + .unwrap(); + assert_eq!( + flow.execution_frame(block, jett_mir::ResourcePosition::Statement(index)), + Some(active) + ); + super::super::emit_for_triple( + &original, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + for copied in [false, true] { + let mut changed = original.clone(); + let current = &mut changed.functions[selected.index() as usize]; + if copied { + let statement = + current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, statement); + } else { + let StatementKind::SumTag { source, .. } = + &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = current.params[0].local; + } + assert!(matches!( + super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ), + Err(CodegenError::InvalidMir(_)) + )); + } + } + } +} + +// Shared source probe; each shape is an independently registered test. +fn check_direct_sum_self_reseat_object(source: &str) { + for release in [false, true] { + let object = emitted(source, release); + assert!(!object.bytes.is_empty()); + } +} + +#[test] +fn resource_native_direct_sum_self_reseat_emits_from_the_same_checked_sources_optional() { + check_direct_sum_self_reseat_object(include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett" + )); +} + +#[test] +fn resource_native_direct_sum_self_reseat_emits_from_the_same_checked_sources_result() { + check_direct_sum_self_reseat_object(include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett" + )); +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs new file mode 100644 index 00000000..ff663db3 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs @@ -0,0 +1,226 @@ +// Exact additive suffixes from the frozen Source17 wrapper packet. +// The complete original fixture is included independently and never pruned. +const WRAPPERS: [(&str, &str); 14] = [ + ( + "01_empty_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "02_absent_tokens", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + if list.length[optional[resource_probe.TestHandle]](view values) != 2: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 2: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "03_empty_map", + r###" +export function main(net: Network) returns nothing: + map[string, resource_probe.TestHandle] values = empty_map() + if map.length[string, resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "04_absent_map", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + if map.length[string, optional[resource_probe.TestHandle]](view values) != 1: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + int64 ignored = resource_probe.terminal_label() + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 1: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "05_empty_result_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_result_tokens() handle error: + int64 ignored = resource_probe.terminal_label() + return nothing + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "06_failed_result_tokens", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = failed_result_tokens() handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "07_absent_envelope", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenEnvelope value = absent_envelope() + if value.marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view value.tokens) != 0: + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "08_absent_box", + r###" +export function main(net: Network) returns nothing: + TokenBox[resource_probe.TestHandle] value = absent_box() + if list.length[resource_probe.TestHandle](view value.items) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "09_empty_choice", + r###" +export function main(net: Network) returns nothing: + TokenChoice value = empty_choice() + match value: + empty: + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "10_absent_choice", + r###" +export function main(net: Network) returns nothing: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + if marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view tokens) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "11_absent_state", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "12_absent_exact_state", + r###" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "13_wrong_empty_length_control", + r###" +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 1: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), + ( + "14_required_primitive_controls", + r###" +export function main(net: Network) returns nothing: + int64 answer = absent_required_controls() + if answer != 34: + int64 ignored = resource_probe.terminal_label() + if absent_namespace_value != 17: + int64 ignored = resource_probe.terminal_label() + return nothing +"###, + ), +]; +const LIST_LENGTH: &str = r###"namespace list +export function length[T](view items: list[T]) returns int64: + return list.__length[T](view items) +"###; +const MAP_LENGTH: &str = r###"namespace map +export function length[K, V](view items: map[K, V]) returns int64: + return map.__length[K, V](view items) +"###; diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/graph_metadata_constructors.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/graph_metadata_constructors.rs new file mode 100644 index 00000000..78c4220c --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/graph_metadata_constructors.rs @@ -0,0 +1,459 @@ +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_mir::{ + Function, ResourceCarrierOperationRole as CarrierRole, ResourceCompletion, + ResourceConstructorNode as Node, +}; + +const SOURCE: &str = r#"namespace app +function create_callback() returns function() returns string: + string captured = "baked" + return function() returns string: return captured +actor Keeper: + mutable function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + receive alive responds bool: + respond true +struct Bundle: + keeper: Keeper + dispose: function(resource_probe.TestHandle) returns nothing + callback: function() returns string + count: int64 +export function make() returns Bundle: + return comptime Bundle(keeper: spawn Keeper(), dispose: resource_probe.descriptor(), callback: create_callback(), count: run 7) +function relay(selected: Bundle) returns Bundle: + Bundle alias = selected + return clone alias +function main() returns nothing: + Bundle selected = make() + Bundle alias = clone selected + Bundle forwarded = relay(alias) + function() returns string callback = forwarded.callback + string observed = callback() + bool alive = ask forwarded.keeper.alive + return nothing +"#; + +fn baked_source(checked: &Arc) -> Program { + let evaluated = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + let mut hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_hir::bake_required_values( + &mut hir, + &evaluated.values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + let types = &checked.checked().interner; + let mut program = jett_mir::lower(&hir, types).unwrap(); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + program +} + +fn factory(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "make" + }) + .unwrap() +} + +fn emit_baked( + program: &Program, + checked: &Arc, + release: bool, +) -> Result { + super::super::super::emit_for_triple( + program, + &checked.checked().interner, + HOST, + Some(entry(program)), + CodegenOptions { optimize: release }, + ) +} + +fn constructor_sites(function: &Function) -> Vec<(usize, usize)> { + function + .blocks + .iter() + .enumerate() + .flat_map(|(block, body)| { + body.statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + matches!(statement.kind, StatementKind::ResourceConstructor(_)) + .then_some((block, index)) + }) + }) + .collect() +} + +#[test] +fn graph_metadata_constructor_keeps_actor_hook_and_primitive_callback_in_both_profiles() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = baked_source(&checked); + let make = factory(&program); + let sites = constructor_sites(make); + assert_eq!(sites.len(), 7, "Begin, four fields, Commit and End survive"); + let mut fields = Vec::new(); + let mut output = None; + for (block, index) in sites { + let StatementKind::ResourceConstructor(node) = + &make.blocks[block].statements[index].kind + else { + unreachable!() + }; + let proof = jett_mir::native_graph_storage_for_constructor(make, types, node).unwrap(); + assert_eq!(proof.ty(), make.return_type); + assert!( + jett_mir::native_graph_storage_for_constructor(make, types, &node.clone()) + .is_none(), + "a copied node cannot select the protected physical occurrence" + ); + match node { + Node::Stage { field, .. } => fields.push(*field), + Node::Commit { output: local, .. } => output = Some(*local), + _ => {} + } + } + assert_eq!(fields, [0, 1, 2, 3]); + let header = make.local(output.unwrap()).unwrap(); + assert!(jett_mir::native_graph_storage_for_local(make, types, header).is_some()); + assert!(jett_mir::native_graph_storage_for_local(make, types, &header.clone()).is_none()); + let ownership = + jett_mir::validate_resource_ownership_for_entry(&program, types, entry(&program)) + .unwrap(); + assert!(ownership.function(make.id).is_none_or(|family| { + family.carriers().is_none_or(|carrier| { + !carrier.operations().iter().any(|operation| { + matches!( + operation.role(), + CarrierRole::BeginConstructor { .. } + | CarrierRole::ConstructorChild { .. } + | CarrierRole::CommitConstructor { .. } + ) + }) + }) + })); + let relay = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "relay" + }) + .unwrap(); + let formal = &relay.params[0]; + assert!(jett_mir::native_graph_storage_for_formal(relay, types, formal).is_some()); + assert!( + jett_mir::native_graph_storage_for_formal(relay, types, &formal.clone()).is_none(), + "an equal-type copied formal cannot qualify a wire shape" + ); + let main = &program.functions[entry(&program).index() as usize]; + let call = main.blocks.iter().flat_map(|block| &block.statements).find_map(|statement| { + let StatementKind::Let { value, .. } = &statement.kind else { return None; }; + matches!(&value.kind, jett_hir::ExpressionKind::Call { function, .. } if *function == relay.id) + .then_some(value) + }).unwrap(); + assert!(jett_mir::native_graph_source_edge(main, types, call, 0).is_some()); + assert!( + jett_mir::native_graph_source_edge(main, types, &call.clone(), 0).is_none(), + "an equal-type copied whole call cannot qualify an ordinary actual" + ); + let artifact = emit_baked(&program, &checked, release).unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + for required in [ + "jett_rt_v1_actor_graph_begin", + "jett_rt_v1_actor_graph_commit", + "jett_rt_v1_resource_graph_descriptor_box", + "jett_rt_v1_struct_new", + "jett_rt_v1_struct_init", + "jett_rt_v1_struct_init_scalar_task", + ] { + assert!( + imports.iter().any(|name| name == required), + "{required}: {imports:?}" + ); + } + for forbidden in [ + "jett_rt_v1_resource_carrier_construct_begin", + "jett_rt_v1_resource_carrier_child", + "jett_rt_v1_resource_carrier_commit", + ] { + assert!( + !imports.iter().any(|name| name == forbidden), + "metadata-only constructors must not enter {forbidden}" + ); + } + } +} + +#[derive(Clone, Copy, Debug)] +enum Mutation { + SameTypeOutput, + OutputHeader, + FieldIndex, + FieldEndpoint, + CopiedStage, + MovedStage, + MissingEnd, + ChangedEnd, +} + +fn forge(function: &mut Function, mutation: Mutation) { + let sites = constructor_sites(function); + let (stage_block, stage_index) = sites + .iter() + .copied() + .find(|(block, index)| { + matches!( + function.blocks[*block].statements[*index].kind, + StatementKind::ResourceConstructor(Node::Stage { .. }) + ) + }) + .unwrap(); + let (commit_block, commit_index) = sites + .iter() + .copied() + .find(|(block, index)| { + matches!( + function.blocks[*block].statements[*index].kind, + StatementKind::ResourceConstructor(Node::Commit { .. }) + ) + }) + .unwrap(); + let (end_block, end_index) = sites + .iter() + .copied() + .find(|(block, index)| { + matches!( + function.blocks[*block].statements[*index].kind, + StatementKind::ResourceConstructor(Node::End { .. }) + ) + }) + .unwrap(); + let StatementKind::ResourceConstructor(Node::Commit { output, .. }) = + function.blocks[commit_block].statements[commit_index].kind + else { + unreachable!() + }; + match mutation { + Mutation::SameTypeOutput => { + let ty = function.local(output).unwrap().ty; + let alternative = function + .locals + .iter() + .find(|header| header.ty == ty && header.id != output) + .unwrap() + .id; + let StatementKind::ResourceConstructor(Node::Commit { output, .. }) = + &mut function.blocks[commit_block].statements[commit_index].kind + else { + unreachable!() + }; + *output = alternative; + } + Mutation::OutputHeader => { + function.locals[output.index() as usize] + .name + .push_str("_forged"); + } + Mutation::FieldIndex | Mutation::FieldEndpoint => { + let StatementKind::ResourceConstructor(Node::Stage { field, value, .. }) = + &mut function.blocks[stage_block].statements[stage_index].kind + else { + unreachable!() + }; + match mutation { + Mutation::FieldIndex => *field = (*field + 1) % 4, + Mutation::FieldEndpoint => value.span.end += 1, + _ => unreachable!(), + } + } + Mutation::CopiedStage => { + let copy = function.blocks[stage_block].statements[stage_index].clone(); + function.blocks[stage_block] + .statements + .insert(stage_index + 1, copy); + } + Mutation::MovedStage => { + assert_eq!(stage_block, commit_block); + let statement = function.blocks[stage_block].statements.remove(stage_index); + function.blocks[stage_block] + .statements + .insert(commit_index - 1, statement); + } + Mutation::MissingEnd => { + function.blocks[end_block].statements.remove(end_index); + } + Mutation::ChangedEnd => { + let StatementKind::ResourceConstructor(Node::End { outcome, .. }) = + &mut function.blocks[end_block].statements[end_index].kind + else { + unreachable!() + }; + *outcome = ResourceCompletion::Abort; + } + } +} + +#[test] +fn graph_metadata_constructor_forged_output_header_field_or_lifecycle_refuses_emission() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let original = baked_source(&checked); + for mutation in [ + Mutation::SameTypeOutput, + Mutation::OutputHeader, + Mutation::FieldIndex, + Mutation::FieldEndpoint, + Mutation::CopiedStage, + Mutation::MovedStage, + Mutation::MissingEnd, + Mutation::ChangedEnd, + ] { + let mut program = original.clone(); + let function = program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "make" + }) + .unwrap(); + forge(function, mutation); + for (block, index) in constructor_sites(function) { + let StatementKind::ResourceConstructor(node) = + &function.blocks[block].statements[index].kind + else { + unreachable!() + }; + assert!( + jett_mir::native_graph_storage_for_constructor( + function, + &checked.checked().interner, + node + ) + .is_none(), + "{mutation:?} must not retain metadata constructor authority" + ); + } + assert!( + matches!( + emit_baked(&program, &checked, release), + Err(CodegenError::InvalidMir(_)) + ), + "{mutation:?} must fail before object emission" + ); + } + } +} + +#[test] +fn graph_metadata_and_occupied_resource_constructors_keep_separate_routes_in_both_profiles() { + let source = SOURCE + .replace( + "function main() returns nothing:", + r#"struct OwnedPair: + first: resource_probe.TestHandle + second: resource_probe.TestHandle + marker: int64 +function retire_pair(first: resource_probe.TestHandle, second: resource_probe.TestHandle) returns nothing: + OwnedPair held = OwnedPair(second: second, first: first, marker: 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe"#, + ) + .replace( + " bool alive = ask forwarded.keeper.alive", + r#" resource_probe.TestHandle first = resource_probe.create(view net, 1002) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1003) handle error: + return nothing + retire_pair(first, second)"#, + ); + for release in [false, true] { + let checked = checked(&source, release); + let types = &checked.checked().interner; + let program = baked_source(&checked); + let occupied = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "retire_pair" + }) + .unwrap(); + let sites = constructor_sites(occupied); + assert!(!sites.is_empty()); + for (block, index) in sites { + let StatementKind::ResourceConstructor(node) = + &occupied.blocks[block].statements[index].kind + else { + unreachable!() + }; + assert!( + jett_mir::native_graph_storage_for_constructor(occupied, types, node).is_none() + ); + } + let ownership = + jett_mir::validate_resource_ownership_for_entry(&program, types, entry(&program)) + .unwrap(); + let carrier = ownership.function(occupied.id).unwrap().carriers().unwrap(); + assert!( + carrier + .operations() + .iter() + .any(|operation| matches!(operation.role(), CarrierRole::CommitConstructor { .. })) + ); + let artifact = emit_baked(&program, &checked, release).unwrap(); + let (_, imports) = symbols(&artifact); + for required in [ + "jett_rt_v1_actor_graph_begin", + "jett_rt_v1_resource_graph_descriptor_box", + "jett_rt_v1_resource_carrier_construct_begin", + "jett_rt_v1_resource_carrier_child", + "jett_rt_v1_resource_carrier_commit", + "jett_rt_v1_resource_carrier_transfer", + ] { + assert!( + imports.iter().any(|name| name == required), + "{required}: {imports:?}" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs new file mode 100644 index 00000000..cb91dfdb --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/lexical_borrows.rs @@ -0,0 +1,193 @@ +//! Public object admission must not infer lexical custody from editable MIR. +use super::*; + +const SOURCES: [(&str, &str); 3] = [ + ( + "Source58 if", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett" + ), + ), + ( + "Source74 while", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett" + ), + ), + ( + "Source89 sibling scopes", + include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett" + ), + ), +]; + +#[derive(Debug, Clone, Copy)] +enum Mutation { + CopiedExit, + RetargetedExit, + MissingWitness, +} + +fn public_object( + program: &Program, + checked: &Arc, + release: bool, +) -> Result { + crate::emit_host_program_object_with_options( + program, + &checked.checked().interner, + entry(program), + CodegenOptions { optimize: release }, + ) +} + +/// Construct an ordinary function through public lowering, then copy only the +/// editable MIR fields. The private Resource witness and other private records +/// stay absent; no test-only authority constructor or representation cast is used. +fn without_resource_witness( + original: &jett_mir::Function, + checked: &Arc, +) -> jett_mir::Function { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + let mut seed = hir.functions[original.id.index() as usize].clone(); + seed.id = FunctionId::new(0); + seed.params.clear(); + seed.capture_count = 0; + seed.return_type = TypeInterner::NOTHING; + seed.locals.clear(); + seed.body.statements.clear(); + let ordinary = jett_hir::Program { + resource_manifest: jett_hir::ResourceManifest::empty(), + resource_source: jett_hir::ResourceSourceArchive::empty(), + functions: vec![seed], + equality_methods: HashMap::new(), + }; + let mut lowered = jett_mir::lower(&ordinary, &checked.checked().interner).unwrap(); + let mut unwitnessed = lowered.functions.remove(0); + unwitnessed.id = original.id; + unwitnessed.identity = original.identity.clone(); + unwitnessed.debug_kind = original.debug_kind.clone(); + unwitnessed.params = original.params.clone(); + unwitnessed.capture_count = original.capture_count; + unwitnessed.return_type = original.return_type; + unwitnessed.locals = original.locals.clone(); + unwitnessed.entry = original.entry; + unwitnessed.blocks = original.blocks.clone(); + unwitnessed.span = original.span; + unwitnessed +} + +#[test] +fn resource_lexical_borrows_public_object_refuses_copied_retargeted_and_unwitnessed_exits() { + for release in [false, true] { + for (label, source) in SOURCES { + let checked = checked(source, release); + let baseline = lower_source(&checked); + // Establish that this exact checked Source reaches public object + // emission before attributing any refusal to the mutation. + let artifact = public_object(&baseline, &checked, release) + .unwrap_or_else(|error| panic!("{label} release={release} baseline: {error:?}")); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_borrow_end") + ); + + let (function_id, block_id, index) = baseline + .functions + .iter() + .find_map(|function| { + if function.identity.declaration.namespace != "app" { + return None; + } + function.blocks.iter().find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + .then_some((function.id, block.id, index)) + }) + }) + }) + .expect("genuine fixture has an app lexical-exit node"); + let position = jett_mir::ResourcePosition::Statement(index); + let helper = &baseline.functions[function_id.index() as usize]; + assert!( + helper + .resource_lexical_exit(block_id, position) + .unwrap() + .is_some() + ); + assert_ne!( + (block_id, index), + (helper.entry, 0), + "retarget must change the site" + ); + + for mutation in [ + Mutation::CopiedExit, + Mutation::RetargetedExit, + Mutation::MissingWitness, + ] { + let mut forged = baseline.clone(); + let helper = &mut forged.functions[function_id.index() as usize]; + match mutation { + Mutation::CopiedExit => { + let copied = + helper.blocks[block_id.index() as usize].statements[index].clone(); + helper.blocks[block_id.index() as usize] + .statements + .insert(index, copied); + } + Mutation::RetargetedExit => { + let moved = helper.blocks[block_id.index() as usize] + .statements + .remove(index); + helper.blocks[helper.entry.index() as usize] + .statements + .insert(0, moved); + } + Mutation::MissingWitness => { + *helper = without_resource_witness(helper, &checked); + assert!( + helper + .resource_lexical_exit(block_id, position) + .unwrap() + .is_none() + ); + } + } + jett_mir::validate(&forged).unwrap_or_else(|errors| + panic!("{label} release={release} {mutation:?} must retain structural MIR: {errors:?}")); + match public_object(&forged, &checked, release) { + Err(CodegenError::InvalidMir(errors)) => { + assert!(!errors.is_empty(), "{label} release={release} {mutation:?}"); + if matches!(mutation, Mutation::MissingWitness) { + assert!( + errors.iter().any(|error| error.message.contains( + "no initially authenticated Source constructor witness" + )), + "{label} release={release} {mutation:?}: {errors:?}" + ); + } + } + Err(error) => panic!( + "{label} release={release} {mutation:?}: wrong public refusal {error:?}" + ), + Ok(_) => panic!( + "{label} release={release} {mutation:?}: forged MIR produced an object" + ), + } + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs new file mode 100644 index 00000000..203c3cea --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/named_indirect.rs @@ -0,0 +1,421 @@ +use super::*; + +const NAMED: &str = include_str!("../named_indirect/18_indirect_named_close.jett"); +const WRONG: &str = include_str!("../named_indirect/19_indirect_named_terminal_failure.jett"); +const ALIAS: &str = include_str!("../named_indirect/20_indirect_named_alias.jett"); +const STDLIB: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing +"#; + +fn named_clif(source: &str, release: bool) -> (ir::Function, HashMap, String) { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let selected = entry(&program); + let layout = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "named_source_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let plan = layout.plan().function(selected).unwrap(); + let target = plan + .operations() + .iter() + .find_map(|operation| operation.named_indirect_target()) + .unwrap(); + let target_symbol = declarations.get(target.function()).unwrap().symbol.clone(); + let function = program_function(&program, selected).unwrap(); + let declaration = declarations.get(selected).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function_inner( + &mut module, + &declarations, + &program, + function, + types, + &declaration.symbol, + &mut context, + Some(&layout), + ) + .unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected external name"); + }; + let name = &context.func.params.user_named_funcs()[user]; + ( + reference, + module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("translated Source and runtime imports have declared symbols"), + ) + }) + .collect(); + (context.func, names, target_symbol) +} + +fn defining_instruction(function: &ir::Function, value: Value) -> ir::Inst { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + panic!("expected an instruction result"); + }; + instruction +} + +fn call_name<'a>( + function: &ir::Function, + names: &'a HashMap, + instruction: ir::Inst, +) -> Option<&'a str> { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names.get(&func_ref).map(String::as_str) +} + +fn constant(function: &ir::Function, value: Value) -> Option { + let instruction = defining_instruction(function, value); + match function.dfg.insts[instruction] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} + +#[test] +fn resource_named_indirect_original_source_emits_selected_bridge_and_retains_named_targets() { + for release in [false, true] { + for source in [NAMED, WRONG, ALIAS, STDLIB] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + for required in [ + "jett_rt_v1_function_callable_check", + "jett_rt_v1_struct_field", + "jett_rt_v1_resource_source_enter", + "jett_rt_v1_resource_source_status", + ] { + assert!( + imports.iter().any(|name| name == required), + "{required}: {imports:?}" + ); + } + let checked = checked(source, release); + let program = lower_source(&checked); + let plan = jett_mir::validate_resource_ownership(&program, &checked.checked().interner) + .unwrap(); + let selected_body = plan + .function(entry(&program)) + .unwrap() + .operations() + .iter() + .find_map(|operation| operation.named_indirect_target()) + .unwrap(); + let target = selected_body.function(); + assert!( + scalar_kind( + &checked.checked().interner, + selected_body.signature_type(), + "public Resource-bearing callable signature" + ) + .is_err() + ); + let reachable = crate::reachability::reachable_function_ids_with_types( + &program, + &checked.checked().interner, + ) + .unwrap(); + assert!(reachable.contains(&target)); + if source == STDLIB { + assert_eq!( + program.functions[target.index() as usize] + .identity + .declaration + .origin, + SourceOrigin::Stdlib + ); + } + } + } +} + +#[test] +fn resource_named_indirect_checks_original_actuals_then_code_and_environment_before_source_entry() { + use cranelift_codegen::{dominator_tree::DominatorTree, flowgraph::ControlFlowGraph}; + for release in [false, true] { + for source in [NAMED, WRONG, ALIAS, STDLIB] { + let (function, names, target) = named_clif(source, release); + let instructions = function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .collect::>(); + assert!( + !instructions + .iter() + .any(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::CallIndirect) + ); + let guard = instructions + .iter() + .copied() + .find(|instruction| { + let ir::InstructionData::Brif { arg, .. } = function.dfg.insts[*instruction] + else { + return false; + }; + function.dfg.insts[defining_instruction(&function, arg)].opcode() + == ir::Opcode::Band + }) + .unwrap(); + let ir::InstructionData::Brif { arg, blocks, .. } = function.dfg.insts[guard] else { + unreachable!(); + }; + let checks = function.dfg.inst_args(defining_instruction(&function, arg)); + let ir::InstructionData::IntCompare { + cond, args: code, .. + } = function.dfg.insts[defining_instruction(&function, checks[0])] + else { + panic!("missing code comparison"); + }; + assert_eq!(cond, IntCC::Equal); + let ir::InstructionData::FuncAddr { func_ref, .. } = + function.dfg.insts[defining_instruction(&function, code[1])] + else { + panic!("missing selected native address"); + }; + assert_eq!(names[&func_ref], target); + assert_eq!( + call_name(&function, &names, defining_instruction(&function, code[0])), + Some("jett_rt_v1_struct_field") + ); + let code_field = function + .dfg + .inst_args(defining_instruction(&function, code[0])); + assert_eq!( + constant(&function, code_field[2]), + Some(NATIVE_FUNCTION_CODE_FIELD as i64) + ); + let ir::InstructionData::IntCompareImm { + cond, + arg: environment, + imm, + .. + } = function.dfg.insts[defining_instruction(&function, checks[1])] + else { + panic!("missing environment comparison"); + }; + assert_eq!(cond, IntCC::Equal); + assert_eq!(imm.bits(), 0); + assert_eq!( + call_name( + &function, + &names, + defining_instruction(&function, environment) + ), + Some("jett_rt_v1_struct_field") + ); + let environment_field = function + .dfg + .inst_args(defining_instruction(&function, environment)); + assert_eq!( + constant(&function, environment_field[2]), + Some(NATIVE_FUNCTION_ENVIRONMENT_FIELD as i64) + ); + assert_eq!( + code_field[1], environment_field[1], + "code and environment must be read from the same evaluated descriptor" + ); + let refused = blocks[1] + .args(&function.dfg.value_lists) + .collect::>(); + let ir::BlockArg::Value(status) = refused[0] else { + panic!("missing refusal status"); + }; + assert_eq!(constant(&function, status), Some(1)); + let source_entry = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_enter") + }) + .unwrap(); + let actual = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_actual") + }) + .unwrap(); + let callable = instructions + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_function_callable_check") + }) + .unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dominators = DominatorTree::new(); + dominators.compute(&function, &cfg); + assert!(dominators.dominates(actual, callable, &function.layout)); + assert!(dominators.dominates(callable, guard, &function.layout)); + assert!(dominators.dominates( + blocks[0].block(&function.dfg.value_lists), + source_entry, + &function.layout + )); + assert!(!dominators.dominates( + blocks[1].block(&function.dfg.value_lists), + source_entry, + &function.layout + )); + let mut suffix = vec![blocks[1].block(&function.dfg.value_lists)]; + let mut refused_blocks = std::collections::HashSet::new(); + while let Some(block) = suffix.pop() { + if refused_blocks.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(source_entry)); + suffix.extend(cfg.succ_iter(block)); + } + } + assert!(refused_blocks.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + let selected_call = instructions + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some(target.as_str()) + }) + .unwrap(); + assert!(dominators.dominates(source_entry, selected_call, &function.layout)); + let actuals = function.dfg.inst_args(selected_call); + assert_eq!( + actuals.len(), + 4, + "context, empty environment, hidden Scope and owned actual" + ); + assert_eq!(constant(&function, actuals[1]), Some(0)); + let body_status = instructions + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .unwrap(); + assert!(dominators.dominates(selected_call, body_status, &function.layout)); + } + } +} + +#[test] +fn resource_named_indirect_refuses_same_signature_producer_swap_and_escaped_family_descriptor() { + for release in [false, true] { + let checked = checked(NAMED, release); + let original = lower_source(&checked); + let selected = entry(&original); + let wrong = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "wrong_target") + .unwrap() + .id; + let mut changed = original.clone(); + let producer = changed.functions[selected.index() as usize] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let StatementKind::Let { value, .. } = &mut statement.kind else { + return None; + }; + matches!(value.kind, ExpressionKind::FunctionRef(_)).then_some(value) + }) + .unwrap(); + producer.kind = ExpressionKind::FunctionRef(wrong); + assert!(matches!( + super::super::super::emit_for_triple( + &changed, + &checked.checked().interner, + HOST, + Some(selected), + CodegenOptions { optimize: release } + ), + Err(CodegenError::InvalidMir(_)) + )); + let escaped = NAMED.replace("export function main", "function retain(view callback: function(resource_probe.TestHandle) returns nothing) returns nothing:\n return nothing\nexport function main") + .replace(" dispose(token)", " retain(view dispose)\n dispose(token)"); + let escaped_checked = super::checked(&escaped, release); + let escaped_program = lower_source(&escaped_checked); + assert!( + super::super::super::emit_for_triple( + &escaped_program, + &escaped_checked.checked().interner, + HOST, + Some(entry(&escaped_program)), + CodegenOptions { optimize: release } + ) + .is_err() + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs new file mode 100644 index 00000000..67c5798c --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/occupied_structs.rs @@ -0,0 +1,231 @@ +use super::*; +use jett_mir::{ResourceCarrierOperationRole as CarrierRole, ResourceCarrierValue, ResourceShape}; + +const SOURCES: [&str; 3] = [ + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" + ), +]; + +#[test] +fn resource_occupied_struct_actual_sources_emit_carrier_and_plain_owner_adoption_in_both_profiles() +{ + for release in [false, true] { + for source in SOURCES { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + jett_mir::validate_call_ownership(&program, types).unwrap(); + let selected = + jett_mir::validate_resource_ownership_for_entry(&program, types, entry(&program)) + .unwrap(); + let helper = program + .functions + .iter() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap(); + let plan = selected.function(helper.id).unwrap(); + let carrier = plan.carriers().unwrap(); + assert!(carrier.operations().iter().any(|op| matches!(op.role(),CarrierRole::ConstructorChild {child,..} if matches!(child.value,ResourceCarrierValue::LeafOwned {slot} if matches!(plan.owner_slots()[slot.index()].shape(),ResourceShape::Plain {..}))))); + let artifact = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!(definitions.iter().any(|n| n == JETT_AOT_ENTRY_SYMBOL_V1)); + for leaf in [ + "carrier_construct_begin", + "carrier_child", + "carrier_commit", + "carrier_transfer", + "operation_begin", + "operation_complete", + "scope_complete", + "source_status", + ] { + assert!( + imports + .iter() + .any(|n| n == &format!("jett_rt_v1_resource_{leaf}")), + "missing {leaf}" + ); + } + } + } +} + +#[test] +fn occupied_struct_primitive_companions_objects_keep_checked_carriers_and_scalar_task_imports() { + let sources = [ + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/39_owned_struct_pending_int64_inspection.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/43_owned_struct_pending_nothing_inspection.jett" + ), + include_str!( + "../../../../../jett_comptime/src/resource_execution/fixtures/44_owned_struct_pending_string_inspection.jett" + ), + ]; + for release in [false, true] { + for (index, source) in sources.into_iter().enumerate() { + let checked = checked_companion(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + jett_mir::validate_call_ownership(&program, types).unwrap(); + jett_mir::validate_resource_ownership_for_entry(&program, types, entry(&program)) + .unwrap(); + let artifact = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|name| name == JETT_AOT_ENTRY_SYMBOL_V1) + ); + if index != 7 { + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_carrier_observe_scalar_task") + ); + } + if (2..=6).contains(&index) { + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_carrier_child_scalar_task") + ); + } + for leaf in [ + "carrier_construct_begin", + "carrier_child", + "carrier_commit", + "carrier_transfer", + "operation_complete", + "scope_complete", + ] { + assert!( + imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_{leaf}")), + "missing exact {leaf}" + ); + } + } + } +} + +#[test] +fn resource_occupied_struct_public_copied_field_or_header_cannot_reach_ordinary_emission() { + for release in [false, true] { + let checked = checked(SOURCES[1], release); + let types = &checked.checked().interner; + let original = lower_source(&checked); + for mutation in 0..3 { + let mut program = original.clone(); + let function = program + .functions + .iter_mut() + .find(|f| { + f.identity.declaration.namespace == "app" + && f.identity.declaration.name == "retire_pair" + }) + .unwrap(); + let (b, i) = function + .blocks + .iter() + .enumerate() + .find_map(|(b, block)| { + block.statements.iter().enumerate().find_map(|(i, s)| { + matches!( + s.kind, + StatementKind::ResourceConstructor( + jett_mir::ResourceConstructorNode::Stage { + source_index: 0, + .. + } + ) + ) + .then_some((b, i)) + }) + }) + .unwrap(); + match mutation { + 0 => { + let StatementKind::ResourceConstructor( + jett_mir::ResourceConstructorNode::Stage { field, .. }, + ) = &mut function.blocks[b].statements[i].kind + else { + unreachable!() + }; + *field = 0; + } + 1 => { + let copy = function.blocks[b].statements[i].clone(); + function.blocks[b].statements.insert(i + 1, copy); + } + 2 => { + function.locals[function.params[0].local.index() as usize].view_source = + Some(function.params[1].local); + } + _ => unreachable!(), + } + let selected = entry(&program); + let result = super::super::super::emit_for_triple( + &program, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ); + assert!( + matches!(result, Err(CodegenError::InvalidMir(_))), + "forged constructor must refuse at fresh ownership validation" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs new file mode 100644 index 00000000..be82d62e --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/ordinary_entry.rs @@ -0,0 +1,554 @@ +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use object::{Object, ObjectSection, ObjectSymbol, RelocationTarget, SymbolKind}; + +include!("carrier_wrapper_sources.rs"); + +const WHOLE_SOURCE: &str = include_str!( + "../../../../../jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett" +); +const MINIMAL_SUPPORT: &str = "namespace resource_probe\nexport resource TestHandle\n"; +const PURE: &str = r#"namespace app +function helper() returns int64: + return 7 +export function main(net: Network) returns nothing: + int64 answer = helper() + if answer != 7: + return nothing + return nothing +"#; +const CALLEE: &str = r#"namespace app +function helper() returns int64: + return 7 +export function main() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + if total != 3: + int64 ignored = helper() + return nothing +function reenter() returns nothing: + main() + return nothing +"#; + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn checked_support(source: &str, support: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +fn materialized(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + let types = &checked.checked().interner; + let mut hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + jett_hir::complete_value_conversions(&mut hir, types).unwrap(); + jett_mir::lower(&hir, types).unwrap() +} + +fn prepare(program: &mut Program, types: &TypeInterner) { + jett_mir::prepare_native_sequences(program, types); + jett_mir::prepare_native_uninhabited_sums(program, types); + jett_mir::prepare_native_generated_functions(program, types); +} + +fn ordinary_clif( + program: &Program, + types: &TypeInterner, + selected: FunctionId, + name: &str, +) -> (ir::Function, HashMap) { + let layout = EmittedResourceLayout::from_program(program, types, selected).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "ordinary_entry_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let function = named(program, name); + assert!(layout.plan().function(function.id).is_none()); + let declaration = declarations.get(function.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + let selected_layout = layout + .plan() + .function(function.id) + .is_some() + .then_some(&layout); + assert!( + selected_layout.is_none(), + "ordinary body cannot select ResourceEmission" + ); + translate_function_inner( + &mut module, + &declarations, + program, + function, + types, + &declaration.symbol, + &mut context, + selected_layout, + ) + .unwrap(); + context.verify(module.isa()).unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("declared external function") + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .unwrap(); + (reference, symbol) + }) + .collect(); + (context.func, names) +} + +fn calls_to( + function: &ir::Function, + names: &HashMap, + symbol: &str, +) -> Vec { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| { + matches!(function.dfg.insts[*instruction], ir::InstructionData::Call { func_ref, .. } + if names.get(&func_ref).is_some_and(|name| name == symbol)) + }) + .collect() +} + +fn assert_ordinary_function( + program: &Program, + types: &TypeInterner, + selected: FunctionId, + name: &str, +) -> (ir::Function, HashMap) { + let function = named(program, name); + let (clif, names) = ordinary_clif(program, types, selected, name); + assert_eq!(clif.signature.params.len(), function.params.len() + 2); + assert!( + clif.signature + .params + .iter() + .all(|parameter| parameter.value_type == ir::types::I64) + ); + assert!( + !names + .values() + .any(|symbol| symbol.starts_with("jett_rt_v1_resource_")), + "ordinary body imports Resource leaves: {names:?}" + ); + let storage = MoveValuePlan::analyze(program, function, types).unwrap(); + if !function + .locals + .iter() + .any(|local| jett_mir::move_values::is_linear(types, local.ty)) + { + let copy = jett_mir::copy_values::CopyValuePlan::analyze(function, types).unwrap(); + assert_eq!(copy.owned_locals, storage.owned_locals); + } + for local in function.locals.iter().filter(|local| { + matches!(types.resolve(local.ty), Type::List(_)) + && local.view_source.is_none() + && !function.is_view_local(local.id) + }) { + assert!( + storage.owned_locals.contains(&(local.id.index() as usize)), + "ordinary consuming-list storage must own its actual holder" + ); + } + jett_mir::validate_call_ownership(program, types).unwrap(); + (clif, names) +} + +fn import_callers(artifact: &ObjectArtifact, import: &str) -> Vec { + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let target = file + .symbols() + .find(|symbol| symbol.is_undefined() && symbol.name().ok() == Some(import)) + .expect("exact imported leaf") + .index(); + let mut callers = Vec::new(); + for section in file.sections() { + for (offset, relocation) in section.relocations() { + if relocation.target() != RelocationTarget::Symbol(target) { + continue; + } + let address = section.address().checked_add(offset).unwrap(); + let owner = file + .symbols() + .filter(|symbol| { + symbol.is_definition() + && symbol.kind() == SymbolKind::Text + && symbol.section_index() == Some(section.index()) + && symbol.address() <= address + && (symbol.size() == 0 + || address < symbol.address().checked_add(symbol.size()).unwrap()) + }) + .max_by_key(|symbol| symbol.address()) + .expect("relocation belongs to an exact defined text symbol"); + callers.push(owner.name().unwrap().to_owned()); + } + } + callers +} + +fn assert_wrapper_only_bridge(artifact: &ObjectArtifact) { + let (definitions, imports) = symbols(artifact); + assert!( + definitions + .iter() + .any(|symbol| symbol == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + definitions + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + for leaf in [ + "entry_scope", + "scope_validate", + "scope_complete", + "entry_outcome", + ] { + let leaf = format!("jett_rt_v1_resource_{leaf}"); + assert_eq!( + imports + .iter() + .filter(|symbol| symbol.as_str() == leaf) + .count(), + 1 + ); + assert_eq!( + import_callers(artifact, &leaf), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } + assert!( + imports + .iter() + .any(|symbol| symbol == NativeLeaf::Status.symbol()) + ); + for leaf in [ + "source_enter", + "source_parameter", + "source_status", + "source_actual", + "source_result", + "return_publish", + ] { + assert!( + !imports + .iter() + .any(|symbol| symbol == &format!("jett_rt_v1_resource_{leaf}")) + ); + } +} + +#[test] +fn ordinary_entry_genuine_pure_main_emits_only_wrapper_scope_with_two_hidden_body_parameters() { + for release in [false, true] { + let checked = checked_support(PURE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.required_materializations().is_empty()); + assert_eq!(hir.resource_manifest.hooks().count(), 3); + let baseline = jett_mir::lower(&hir, types).unwrap(); + let selected = entry(&baseline); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + let layout = EmittedResourceLayout::from_program(&prepared, types, selected).unwrap(); + let bridge = layout.plan().entry_scope().unwrap(); + assert_eq!(bridge.function(), selected); + assert!(layout.entry_bridge_completion().is_some()); + assert!(layout.function_signature(selected).is_none()); + assert!(layout.frame(selected, bridge.root_scope().id()).is_none()); + assert!( + layout + .operation(selected, bridge.completion().id()) + .is_none() + ); + let (main, names) = assert_ordinary_function(&prepared, types, selected, "main"); + let helper_symbol = + crate::symbol_name(&named(&prepared, "helper").identity, types).unwrap(); + let calls = calls_to(&main, &names, &helper_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(main.dfg.inst_args(calls[0]).len(), 2); + assert_ordinary_function(&prepared, types, selected, "helper"); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + assert_wrapper_only_bridge(&artifact); + assert_eq!( + import_callers(&artifact, "jett_rt_v1_resource_entry_network"), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } +} + +#[test] +fn ordinary_entry_main_as_callee_and_consuming_for_keep_the_ordinary_abi() { + for release in [false, true] { + let checked = checked_support(CALLEE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lower_source(&checked); + let selected = entry(&baseline); + assert!(named(&baseline, "main").blocks.iter().any(|block| matches!( + block.terminator.kind, + jett_mir::TerminatorKind::ForEach { .. } + ))); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + assert!( + !named(&prepared, "main").blocks.iter().any(|block| matches!( + block.terminator.kind, + jett_mir::TerminatorKind::ForEach { .. } + )) + ); + assert_ordinary_function(&prepared, types, selected, "main"); + assert!( + named(&prepared, "main") + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| { + matches!( + statement.kind, + jett_mir::StatementKind::SequenceGet { consume: true, .. } + ) + }), + "the genuine For must retain its consuming element extraction" + ); + let (caller, names) = assert_ordinary_function(&prepared, types, selected, "reenter"); + let main_symbol = crate::symbol_name(&named(&prepared, "main").identity, types).unwrap(); + let calls = calls_to(&caller, &names, &main_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(caller.dfg.inst_args(calls[0]).len(), 2); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + assert_wrapper_only_bridge(&artifact); + let (_, imports) = symbols(&artifact); + assert!( + !imports + .iter() + .any(|symbol| symbol == "jett_rt_v1_resource_entry_network") + ); + } +} + +#[test] +fn ordinary_entry_complete_source17_wrapper14_emits_main_and_required_helper_with_ordinary_signatures() + { + let (name, suffix) = WRAPPERS[13]; + assert_eq!(name, "14_required_primitive_controls"); + let source = format!("{WHOLE_SOURCE}{suffix}"); + for release in [false, true] { + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let baseline = materialized(&checked); + let selected = entry(&baseline); + let mut prepared = baseline.clone(); + prepare(&mut prepared, types); + let (main, names) = assert_ordinary_function(&prepared, types, selected, "main"); + let helper_symbol = crate::symbol_name( + &named(&prepared, "absent_required_controls").identity, + types, + ) + .unwrap(); + let calls = calls_to(&main, &names, &helper_symbol); + assert_eq!(calls.len(), 1); + assert_eq!(main.dfg.inst_args(calls[0]).len(), 2); + assert_ordinary_function(&prepared, types, selected, "absent_required_controls"); + let artifact = super::super::super::emit_for_triple( + &baseline, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert!( + definitions + .iter() + .any(|symbol| symbol == JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert_eq!( + imports + .iter() + .filter(|symbol| symbol.as_str() == "jett_rt_v1_resource_scope_complete") + .count(), + 1 + ); + let callers = import_callers(&artifact, "jett_rt_v1_resource_scope_complete"); + assert_eq!( + callers + .iter() + .filter(|symbol| symbol.as_str() == JETT_AOT_ENTRY_SYMBOL_V1) + .count(), + 1 + ); + for name in ["main", "absent_required_controls"] { + let symbol = crate::symbol_name(&named(&prepared, name).identity, types).unwrap(); + assert!(!callers.contains(&symbol)); + } + } +} + +#[test] +fn ordinary_entry_current_header_body_and_manifest_mutations_refuse_real_object_emission() { + for release in [false, true] { + let checked = checked_support(PURE, MINIMAL_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lower_source(&checked); + let selected = entry(&baseline); + for mutation in 0..4 { + let mut changed = baseline.clone(); + let index = usize::try_from(selected.index()).unwrap(); + match mutation { + 0 => changed.functions[index].params[0].mode = jett_mir::ParamMode::View, + 1 => changed.functions[index].blocks[0].statements.clear(), + 2 => changed.functions[index].capture_count = 1, + 3 => changed.resource_manifest = jett_hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + super::super::super::emit_for_triple( + &changed, + types, + HOST, + Some(selected), + CodegenOptions { optimize: release }, + ) + .is_err(), + "mutation={mutation}; release={release}" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs new file mode 100644 index 00000000..44881b1b --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/program_entry.rs @@ -0,0 +1,354 @@ +//! Actual generated-entry IR/object controls; no linked native execution. +use super::*; +use object::{Object, ObjectSection, ObjectSymbol, RelocationTarget, SymbolKind}; +use std::collections::HashSet; + +const BEGIN: &str = "jett_rt_v1_resource_program_begin"; +const COMPLETE: &str = "jett_rt_v1_resource_program_complete"; +const MANIFEST: &str = "jett_aot_resource_v1_manifest"; + +fn actual_entry(release: bool) -> (ir::Function, HashMap) { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let target = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(target, "actual_resource_entry", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let (_, context) = build_entry(&mut module, &declarations, &layout).unwrap(); + context.verify(module.isa()).unwrap(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("declared external function") + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .unwrap(); + (reference, symbol) + }) + .collect(); + (context.func, names) +} + +fn call(function: &ir::Function, names: &HashMap, symbol: &str) -> ir::Inst { + let calls = function.layout.blocks().flat_map(|block| function.layout.block_insts(block)) + .filter(|instruction| matches!(function.dfg.insts[*instruction], + ir::InstructionData::Call { func_ref, .. } if names.get(&func_ref).is_some_and(|name| name == symbol))) + .collect::>(); + assert_eq!(calls.len(), 1, "one actual call to {symbol}"); + calls[0] +} + +fn destinations(function: &ir::Function, block: ir::Block) -> Vec { + let terminal = function.layout.last_inst(block).unwrap(); + function.dfg.insts[terminal] + .branch_destination(&function.dfg.jump_tables, &function.dfg.exception_tables) + .into_iter() + .map(|branch| branch.block(&function.dfg.value_lists)) + .collect() +} + +fn reachable_returns(function: &ir::Function, start: ir::Block) -> HashSet { + let mut pending = vec![start]; + let mut visited = HashSet::new(); + let mut returns = HashSet::new(); + while let Some(block) = pending.pop() { + if !visited.insert(block) { + continue; + } + let terminal = function.layout.last_inst(block).unwrap(); + if function.dfg.insts[terminal].opcode() == ir::Opcode::Return { + returns.insert(block); + } else { + pending.extend(destinations(function, block)); + } + } + returns +} + +fn returned(function: &ir::Function, block: ir::Block) -> &[Value] { + function + .dfg + .inst_args(function.layout.last_inst(block).unwrap()) +} + +#[test] +fn generated_entry_uses_own_manifest_exact_context_and_separate_startup_outputs() { + for release in [false, true] { + let (function, names) = actual_entry(release); + assert_eq!(function.signature.params, [AbiParam::new(ir::types::I64)]); + assert_eq!(function.signature.returns, [AbiParam::new(ir::types::I32)]); + let incoming = function + .dfg + .block_params(function.layout.entry_block().unwrap())[0]; + let manifest = call(&function, &names, MANIFEST); + let begin = call(&function, &names, BEGIN); + let complete = call(&function, &names, COMPLETE); + let scope = call(&function, &names, "jett_rt_v1_resource_entry_scope"); + let manifest_args = function.dfg.inst_args(manifest); + let begin_args = function.dfg.inst_args(begin); + assert_eq!(manifest_args, [begin_args[1]]); + assert_eq!(begin_args.len(), 4); + assert_eq!(begin_args[0], incoming); + assert_eq!(function.dfg.inst_args(scope)[0], incoming); + assert_eq!(function.dfg.inst_args(complete)[0], incoming); + assert_eq!( + begin_args[1..] + .iter() + .copied() + .collect::>() + .len(), + 3 + ); + for (instruction, expected) in [ + (manifest, vec![ir::types::I64]), + (begin, vec![ir::types::I64; 4]), + ( + complete, + vec![ir::types::I64, ir::types::I64, ir::types::I32], + ), + ] { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + unreachable!() + }; + let signature = &function.dfg.signatures[function.dfg.ext_funcs[func_ref].signature]; + assert_eq!( + signature + .params + .iter() + .map(|parameter| parameter.value_type) + .collect::>(), + expected + ); + assert_eq!(signature.returns, [AbiParam::new(ir::types::I32)]); + } + let manifest_block = function.layout.inst_block(manifest).unwrap(); + let begin_block = function.layout.inst_block(begin).unwrap(); + let scope_block = function.layout.inst_block(scope).unwrap(); + assert_eq!(destinations(&function, manifest_block)[0], begin_block); + assert_eq!(destinations(&function, begin_block)[0], scope_block); + assert_eq!( + destinations(&function, manifest_block)[1], + destinations(&function, begin_block)[1] + ); + } +} + +#[test] +fn every_post_begin_return_uses_the_single_finish_gate_and_exact_terminal_status() { + for release in [false, true] { + let (function, names) = actual_entry(release); + let begin = call(&function, &names, BEGIN); + let complete = call(&function, &names, COMPLETE); + let begin_block = function.layout.inst_block(begin).unwrap(); + let generated = function.layout.inst_block(complete).unwrap(); + assert_eq!( + returned(&function, generated), + function.dfg.inst_results(complete) + ); + let gates = function + .layout + .blocks() + .filter(|block| destinations(&function, *block).contains(&generated)) + .collect::>(); + assert_eq!(gates.len(), 1); + let finish = gates[0]; + let successors = destinations(&function, finish); + assert_eq!(successors.len(), 2); + assert_eq!(successors[1], generated); + let borrowed = successors[0]; + assert_eq!( + returned(&function, borrowed), + &[function.dfg.block_params(finish)[0]] + ); + assert_eq!( + function.dfg.inst_args(complete)[2], + function.dfg.block_params(finish)[0] + ); + let terminal = function.layout.last_inst(finish).unwrap(); + let condition = function.dfg.inst_args(terminal)[0]; + let ir::ValueDef::Result(compare, _) = function.dfg.value_def(condition) else { + panic!("borrowed sentinel test") + }; + let ir::InstructionData::IntCompareImm { cond, arg, imm, .. } = function.dfg.insts[compare] + else { + panic!("exact zero sentinel comparison") + }; + assert_eq!(cond, IntCC::Equal); + assert_eq!(i64::from(imm), 0); + assert_eq!(arg, function.dfg.inst_args(complete)[1]); + let begin_success = destinations(&function, begin_block)[0]; + assert_eq!( + reachable_returns(&function, begin_success), + HashSet::from([borrowed, generated]) + ); + let startup_refusal = destinations(&function, begin_block)[1]; + assert_eq!( + reachable_returns(&function, startup_refusal), + HashSet::from([startup_refusal]) + ); + assert_eq!( + function + .layout + .blocks() + .filter( + |block| function.dfg.insts[function.layout.last_inst(*block).unwrap()].opcode() + == ir::Opcode::Return + ) + .count(), + 3 + ); + } +} + +fn relocation_callers(artifact: &ObjectArtifact, target_name: &str) -> Vec { + let file = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let target = file + .symbols() + .find(|symbol| symbol.name().ok() == Some(target_name)) + .unwrap() + .index(); + let mut callers = Vec::new(); + for section in file.sections() { + for (offset, relocation) in section.relocations() { + if relocation.target() != RelocationTarget::Symbol(target) { + continue; + } + let address = section.address().checked_add(offset).unwrap(); + let owner = file + .symbols() + .filter(|symbol| { + symbol.is_definition() + && symbol.kind() == SymbolKind::Text + && symbol.section_index() == Some(section.index()) + && symbol.address() <= address + && (symbol.size() == 0 + || address < symbol.address().checked_add(symbol.size()).unwrap()) + }) + .max_by_key(|symbol| symbol.address()) + .unwrap(); + callers.push(owner.name().unwrap().to_owned()); + } + } + callers +} + +#[test] +fn real_resource_object_links_startup_only_from_its_generated_c_entry() { + for release in [false, true] { + let artifact = emitted(SOURCE, release); + let (definitions, imports) = symbols(&artifact); + assert_eq!( + definitions + .iter() + .filter(|name| name.as_str() == MANIFEST) + .count(), + 1 + ); + assert!(!imports.iter().any(|name| name == MANIFEST)); + for target in [BEGIN, COMPLETE, MANIFEST] { + assert_eq!( + relocation_callers(&artifact, target), + [JETT_AOT_ENTRY_SYMBOL_V1.to_owned()] + ); + } + for observer in ["unwind", "failure", "failure_copy"] { + assert!( + !imports + .iter() + .any(|name| name == &format!("jett_rt_v1_resource_program_{observer}")) + ); + } + } +} + +#[test] +fn ordinary_objects_keep_the_same_entry_without_resource_startup_or_manifest() { + const ORDINARY: &str = + "namespace app\nexport function main() returns nothing:\n return nothing\n"; + for release in [false, true] { + let file = FileId::new(0); + let parsed = parse(ORDINARY, file); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error) + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + let program = jett_mir::lower(&hir, &checked.interner).unwrap(); + let artifact = super::super::super::emit_for_triple( + &program, + &checked.interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let (definitions, imports) = symbols(&artifact); + assert_eq!( + definitions + .iter() + .filter(|name| name.as_str() == JETT_AOT_ENTRY_SYMBOL_V1) + .count(), + 1 + ); + assert!( + !definitions + .iter() + .chain(&imports) + .any(|name| name == MANIFEST || name.starts_with("jett_rt_v1_resource_")) + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs new file mode 100644 index 00000000..76a6918b --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hook_order.rs @@ -0,0 +1,108 @@ +use super::*; + +#[test] +fn resource_returned_hook_all_recipes_and_immediate_callees_prepare_exact_descriptor_before_commit() +{ + for release in [false, true] { + for (source, effect) in [ + (CLOSE, "jett_rt_v1_resource_descriptor_close"), + (IMMEDIATE, "jett_rt_v1_resource_descriptor_close"), + (FACTORY, "jett_rt_v1_resource_factory_prepare"), + (BORROW, "jett_rt_v1_resource_borrow_commit"), + (ORDER, "jett_rt_v1_resource_factory_prepare"), + (CALLEE_FAILURE, "jett_rt_v1_resource_factory_prepare"), + (ACTUAL_FAILURE, "jett_rt_v1_resource_factory_prepare"), + ] { + let (function, names, indirect) = hook_clif(source, release, "main"); + assert_eq!(indirect.len(), 1); + let all = instructions(&function); + let prepare = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_hook_prepare") + && constant(&function, function.dfg.inst_args(*instruction)[2]) + == Some(i64::from(indirect[0])) + }) + .unwrap(); + assert_ne!( + constant(&function, function.dfg.inst_args(prepare)[3]), + Some(0) + ); + let effect = all + .iter() + .copied() + .find(|instruction| call_name(&function, &names, *instruction) == Some(effect)) + .unwrap(); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(prepare)[0]); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + assert!(dom.dominates(accepted, effect, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(effect)); + pending.extend(cfg.succ_iter(block)); + } + } + assert!(seen.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + if [ORDER, CALLEE_FAILURE, ACTUAL_FAILURE].contains(&source) { + let argument = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some("argument_label") + }) + .unwrap(); + let callee = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) == Some("select_create") + }) + .unwrap(); + assert!(dom.dominates(argument, callee, &function.layout)); + assert!(dom.dominates(callee, prepare, &function.layout)); + let statuses = all + .iter() + .copied() + .filter(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .collect::>(); + for (called, next) in [(argument, callee), (callee, prepare)] { + let status = statuses + .iter() + .copied() + .find(|status| { + dom.dominates(called, *status, &function.layout) + && dom.dominates(*status, next, &function.layout) + }) + .unwrap(); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(status)[0]); + assert!(dom.dominates(accepted, next, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(next)); + pending.extend(cfg.succ_iter(block)); + } + } + } + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs new file mode 100644 index 00000000..8d7ebedb --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/returned_hooks.rs @@ -0,0 +1,464 @@ +use super::*; +use cranelift_codegen::{dominator_tree::DominatorTree, flowgraph::ControlFlowGraph}; +#[path = "returned_hook_order.rs"] +mod ordering; +const FACTORY: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" +); +const BORROW: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" +); +const IMMEDIATE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" +); +const ORDER: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" +); +const CALLEE_FAILURE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" +); +const ACTUAL_FAILURE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" +); + +const CLOSE: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett" +); +const ALIAS: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett" +); +const RELAY: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett" +); +const UNUSED: &str = include_str!( + "../../../../../jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett" +); + +fn hook_clif( + source: &str, + release: bool, + selected_name: &str, +) -> (ir::Function, HashMap, Vec) { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let isa = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let mut module = ObjectModule::new( + ObjectBuilder::new(isa, "returned_hook_clif", default_libcall_names()).unwrap(), + ); + let mut declarations = + DeclaredFunctions::new(program.functions.len(), verified.functions().len()); + for item in verified.functions() { + let function = program_function(&program, item.mir_id).unwrap(); + let signature = signature(&module, function, types, &layout).unwrap(); + let native_id = module + .declare_function(&item.symbol, Linkage::Local, &signature) + .unwrap(); + declarations + .insert(DeclaredFunction { + mir_id: function.id, + native_id, + symbol: item.symbol.clone(), + signature, + modes: function + .params + .iter() + .map(|parameter| parameter.mode) + .collect(), + parameter_types: function + .params + .iter() + .map(|parameter| parameter.ty) + .collect(), + debug_label: debug::function_label(function).unwrap(), + }) + .unwrap(); + } + let selected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == selected_name) + .unwrap(); + let declaration = declarations.get(selected.id).unwrap(); + let mut context = module.make_context(); + context.func.signature = declaration.signature.clone(); + translate_function_inner( + &mut module, + &declarations, + &program, + selected, + types, + &declaration.symbol, + &mut context, + Some(&layout), + ) + .unwrap(); + context.verify(module.isa()).unwrap(); + let indirect = layout + .plan() + .function(selected.id) + .unwrap() + .operations() + .iter() + .filter(|operation| operation.indirect_hook_target().is_some()) + .map(|operation| layout.operation(selected.id, operation.id()).unwrap()) + .collect(); + let names = context + .func + .dfg + .ext_funcs + .iter() + .map(|(reference, data)| { + let ir::ExternalName::User(user) = data.name else { + panic!("unexpected external name"); + }; + let name = &context.func.params.user_named_funcs()[user]; + let symbol = module + .declarations() + .get_function_decl(FuncId::from_u32(name.index)) + .name + .clone() + .expect("Source/runtime import has its declared symbol"); + let name = declarations + .iter() + .find(|declaration| declaration.symbol == symbol) + .map(|declaration| { + program_function(&program, declaration.mir_id) + .unwrap() + .identity + .declaration + .name + .clone() + }) + .unwrap_or(symbol); + (reference, name) + }) + .collect(); + (context.func, names, indirect) +} +fn instructions(function: &ir::Function) -> Vec { + function + .layout + .blocks() + .flat_map(|block| function.layout.block_insts(block)) + .collect() +} +fn call_name<'a>( + function: &ir::Function, + names: &'a HashMap, + instruction: ir::Inst, +) -> Option<&'a str> { + let ir::InstructionData::Call { func_ref, .. } = function.dfg.insts[instruction] else { + return None; + }; + names.get(&func_ref).map(String::as_str) +} +fn defining_instruction(function: &ir::Function, value: Value) -> ir::Inst { + let ir::ValueDef::Result(instruction, _) = + function.dfg.value_def(function.dfg.resolve_aliases(value)) + else { + panic!("expected instruction result"); + }; + instruction +} +fn constant(function: &ir::Function, value: Value) -> Option { + match function.dfg.insts[defining_instruction(function, value)] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + .. + } => Some(imm.bits()), + _ => None, + } +} +fn checked_branch(function: &ir::Function, status: Value) -> (ir::Block, ir::Block) { + for instruction in instructions(function) { + let ir::InstructionData::Brif { arg, blocks, .. } = function.dfg.insts[instruction] else { + continue; + }; + if let ir::InstructionData::IntCompareImm { + cond, + arg: checked, + imm, + .. + } = function.dfg.insts[defining_instruction(function, arg)] + { + if checked == status && cond == IntCC::Equal && imm.bits() == 0 { + return ( + blocks[0].block(&function.dfg.value_lists), + blocks[1].block(&function.dfg.value_lists), + ); + } + } + } + panic!("exact status-zero output guard is absent"); +} + +#[test] +fn resource_returned_hook_unused_selector_retains_its_transitive_ordinary_helper_declaration() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let types = &checked.checked().interner; + let mut program = lower_source(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + jett_mir::prepare_native_uninhabited_sums(&mut program, types); + jett_mir::prepare_native_generated_functions(&mut program, types); + let selector = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "select_create" + }) + .unwrap(); + let helper = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "terminal_label" + }) + .unwrap(); + let ordinary = + crate::reachability::reachable_function_ids_with_types(&program, types).unwrap(); + assert!(!ordinary.contains(&selector.id)); + assert!(!ordinary.contains(&helper.id)); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + assert!(layout.plan().function(selector.id).is_some()); + assert!(layout.plan().function(helper.id).is_none()); + let verified = crate::verify::verify_resource_program(layout.plan()).unwrap(); + let helper_symbol = &verified + .get(helper.id) + .expect("an executable Resource root retains its ordinary helper declaration") + .symbol; + let (definitions, _) = symbols(&emitted(CLOSE, release)); + assert!(definitions.iter().any(|symbol| symbol == helper_symbol)); + + let (function, names, indirect) = hook_clif(CLOSE, release, "select_create"); + assert!(indirect.is_empty()); + let calls = instructions(&function) + .into_iter() + .filter(|instruction| { + call_name(&function, &names, *instruction) == Some("terminal_label") + }) + .collect::>(); + assert_eq!(calls.len(), 1); + // This pure helper has no Resource operation or hidden Scope operand. + assert_eq!(function.dfg.inst_args(calls[0]).len(), 2); + } +} + +#[test] +fn resource_returned_hook_original_sources_use_opaque_metadata_without_ordinary_callable_ownership() +{ + for release in [false, true] { + for source in [ + CLOSE, + ALIAS, + RELAY, + UNUSED, + FACTORY, + BORROW, + IMMEDIATE, + ORDER, + CALLEE_FAILURE, + ACTUAL_FAILURE, + ] { + let artifact = emitted(source, release); + let (_, imports) = symbols(&artifact); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_descriptor") + ); + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_source_status") + ); + for forbidden in [ + "jett_rt_v1_struct_clone", + "jett_rt_v1_struct_drop", + "jett_rt_v1_function_callable_check", + "jett_rt_v1_struct_field", + ] { + assert!( + !imports.iter().any(|name| name == forbidden), + "{forbidden}: {imports:?}" + ); + } + let checked = checked(source, release); + let program = lower_source(&checked); + let plan = jett_mir::validate_resource_ownership(&program, &checked.checked().interner) + .unwrap(); + for function in plan.functions() { + if let Some(hook) = function.descriptor_return() { + assert!( + scalar_kind( + &checked.checked().interner, + hook.function_type(), + "public hook signature" + ) + .is_err() + ); + let companion = + jett_mir::ResourceCompanionPlan::analyze(&plan, function.function()) + .unwrap(); + for local in &program.functions[function.function().index() as usize].locals { + if function.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + } + } + } + } + } + } +} + +#[test] +fn resource_returned_hook_close_checks_descriptor_after_owner_stage_and_completed_source_before_effect() + { + for release in [false, true] { + for source in [CLOSE, ALIAS, RELAY] { + let (function, names, indirect) = hook_clif(source, release, "main"); + let all = instructions(&function); + assert!( + !all.iter() + .any(|instruction| function.dfg.insts[*instruction].opcode() + == ir::Opcode::CallIndirect) + ); + let close = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_descriptor_close") + }) + .unwrap(); + let prepare = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_hook_prepare") + && constant(&function, function.dfg.inst_args(*instruction)[3]) != Some(0) + }) + .unwrap(); + assert_eq!(indirect.len(), 1); + assert_eq!( + constant(&function, function.dfg.inst_args(prepare)[2]), + Some(i64::from(indirect[0])) + ); + let stage = all + .iter() + .copied() + .rfind(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_transfer") + }) + .unwrap(); + let complete = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_source_status") + }) + .unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + assert!(dom.dominates(stage, prepare, &function.layout)); + assert!(dom.dominates(complete, prepare, &function.layout)); + let status = function.dfg.inst_results(prepare)[0]; + let (accepted, refused) = checked_branch(&function, status); + assert!(dom.dominates(accepted, close, &function.layout)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(close)); + pending.extend(cfg.succ_iter(block)); + } + } + assert!(seen.iter().any(|block| { + function.layout.block_insts(*block).any(|instruction| { + call_name(&function, &names, instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + })); + } + } +} + +#[test] +fn resource_returned_hook_scope_completion_guards_real_return_and_failure_cannot_publish_metadata() +{ + for release in [false, true] { + for (source, selected) in [(CLOSE, "descriptor"), (RELAY, "relay")] { + let (function, names, _) = hook_clif(source, release, selected); + let all = instructions(&function); + let complete = all + .iter() + .copied() + .find(|instruction| { + call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_scope_complete") + }) + .unwrap(); + let body = function.dfg.inst_args(complete)[3]; + assert_eq!(constant(&function, body), Some(0)); + let (accepted, refused) = + checked_branch(&function, function.dfg.inst_results(complete)[0]); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&function); + let mut dom = DominatorTree::new(); + dom.compute(&function, &cfg); + let returned = all + .iter() + .copied() + .find(|instruction| { + function.dfg.insts[*instruction].opcode() == ir::Opcode::Return + && dom.dominates(accepted, *instruction, &function.layout) + }) + .unwrap(); + let value = function.dfg.inst_args(returned)[0]; + assert_ne!(constant(&function, value), Some(0)); + let mut pending = vec![refused]; + let mut seen = std::collections::HashSet::new(); + while let Some(block) = pending.pop() { + if seen.insert(block) { + assert_ne!(Some(block), function.layout.inst_block(returned)); + for instruction in function.layout.block_insts(block) { + if function.dfg.insts[instruction].opcode() == ir::Opcode::Return { + assert_eq!( + constant(&function, function.dfg.inst_args(instruction)[0]), + Some(0) + ); + } + } + pending.extend(cfg.succ_iter(block)); + } + } + assert!( + !all.iter() + .any(|instruction| call_name(&function, &names, *instruction) + == Some("jett_rt_v1_resource_return_publish")) + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs new file mode 100644 index 00000000..19985ab9 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_execution/tests/sum_views.rs @@ -0,0 +1,41 @@ +use super::*; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; + +#[test] +fn resource_borrowed_sum_objects_emit_checked_nonconsuming_leaves_in_both_profiles() { + let cases = cases::CASES + .iter() + .filter(|case| { + case.name.starts_with("view_optional_") || case.name.starts_with("view_result_") + }) + .collect::>(); + assert_eq!(cases.len(), 12); + for release in [false, true] { + for case in &cases { + let artifact = emitted(case.source, release); + let (_, imports) = symbols(&artifact); + for leaf in [ + "jett_rt_v1_resource_sum_borrow", + "jett_rt_v1_resource_sum_view_tag", + "jett_rt_v1_resource_sum_view_project", + "jett_rt_v1_resource_sum_borrow_end", + ] { + assert!( + imports.iter().any(|name| name == leaf), + "{} release={release} missing {leaf}", + case.name + ); + } + if case.name.starts_with("view_result_") { + assert!( + imports + .iter() + .any(|name| name == "jett_rt_v1_resource_sum_failure_read"), + "{} release={release}", + case.name + ); + } + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout.rs new file mode 100644 index 00000000..f03edf51 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout.rs @@ -0,0 +1,1747 @@ +//! Constructor-owned projection of a fresh Source custody proof into immutable wire rows. +//! Bytes and ordinals are consistency metadata; only the runtime issuer creates custody. +use super::*; +use jett_mir::{ + self as custody, ResourceCallOperand as Operand, ResourceCallResult as CallResult, + ResourceOperationRole as Role, +}; +use std::collections::BTreeMap; +#[path = "resource_layout/carrier_operations.rs"] +pub(super) mod carrier_operations; +#[path = "resource_layout/carriers.rs"] +mod carriers; +#[path = "resource_layout/graph_shapes.rs"] +mod graph_shapes; +#[path = "resource_layout/native_suite.rs"] +mod native_suite; + +const LIMIT: usize = 4096; +const BYTE_LIMIT: usize = 1024 * 1024; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct SelectedEntry { + pub(super) function: u32, + pub(super) signature: u32, + pub(super) scope: u32, +} +#[derive(Debug)] +pub(super) struct EmittedResourceLayout<'p> { + plan: custody::ResourceOwnershipPlan<'p>, + entry: SelectedEntry, + entry_bridge_completion: Option, + wire: Vec, + graph_hooks: Vec<(jett_hir::ResourceHookRef, u32)>, + functions: BTreeMap, + frames: BTreeMap<(u32, usize), u32>, + slots: BTreeMap<(u32, usize), u32>, + operations: BTreeMap<(u32, usize), u32>, + carrier_slots: BTreeMap<(u32, usize), u32>, + carrier_operations: BTreeMap<(u32, usize), u32>, +} +impl<'p> EmittedResourceLayout<'p> { + /// The driver-selected identity is an input; no function name or block chooses entry. + pub(super) fn from_program( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, + ) -> Result { + let plan = custody::validate_resource_ownership_for_entry(program, types, entry) + .map_err(CodegenError::InvalidMir)?; + let selected = program + .functions + .get(entry.index() as usize) + .filter(|function| function.id == entry) + .ok_or_else(|| pending("driver entry has no exact current function"))?; + let original = plan + .authenticated_entry(entry) + .ok_or_else(|| pending("driver entry has no original Source execution closure"))?; + let headers_match = selected.params.len() == original.params.len() + && selected + .params + .iter() + .zip(&original.params) + .all(|(current, archived)| { + current.name == archived.name + && current.ty == archived.ty + && current.mode == archived.mode + && current.mutable == archived.mutable + && current.span == archived.span + }); + // Current local IDs are independently authenticated by the constructor witness; + // the immutable original IDs stay archival through exact canonical compaction. + if selected.identity != original.identity + || !headers_match + || selected.return_type != original.return_type + || selected.capture_count != 0 + || selected.return_type != TypeInterner::NOTHING + || selected + .params + .iter() + .any(|param| param.ty != TypeInterner::NETWORK) + { + return Err(pending( + "driver entry does not have the original closed Network/Nothing header", + )); + } + let mut rows = Rows::new(&plan)?; + rows.populate()?; + let (entry, entry_bridge_completion) = if let Some(bridge) = plan.entry_scope() { + let (selected, completion) = rows.entry_bridge(bridge)?; + (selected, Some(completion)) + } else { + let signature = rows.function_signature(entry)?; + let function = plan + .function(entry) + .ok_or_else(|| pending("entry has no fresh custody function plan"))?; + let scope = rows.frame(entry, function.root_scope().id())?; + let frame = &mut rows.frames[scope as usize]; + if function.provisional_return().is_some() || !frame.parents.is_empty() { + return Err(pending( + "entry Scope cannot also be activated as a Source callee", + )); + } + frame.parents.push(Parent::Root); + ( + SelectedEntry { + function: entry.index(), + signature, + scope, + }, + None, + ) + }; + let graph_hooks = rows + .hook_ids + .iter() + .cloned() + .zip(rows.hooks.iter().map(|hook| hook.2)) + .collect(); + let wire = rows.encode()?; + let functions = rows.functions.clone(); + let frames = rows.frame_ids.clone(); + let slots = rows.slot_ids.clone(); + let operations = rows.operation_ids.clone(); + let carrier_slots = rows.carrier_slot_ids.clone(); + let carrier_operations = rows.carrier_operation_ids.clone(); + drop(rows); + Ok(Self { + plan, + entry, + entry_bridge_completion, + wire, + graph_hooks, + functions, + frames, + slots, + operations, + carrier_slots, + carrier_operations, + }) + } + pub(super) fn graph_hook(&self, hook: &jett_hir::ResourceHookRef) -> Option<(u32, u32)> { + self.graph_hooks + .iter() + .enumerate() + .find_map(|(index, (current, signature))| { + if current == hook { + Some((u32::try_from(index).ok()?, *signature)) + } else { + None + } + }) + } + pub(super) fn entry(&self) -> SelectedEntry { + self.entry + } + pub(super) fn entry_bridge_completion(&self) -> Option { + self.entry_bridge_completion + } + pub(super) fn bytes(&self) -> &[u8] { + &self.wire + } + pub(super) fn plan(&self) -> &custody::ResourceOwnershipPlan<'p> { + &self.plan + } + pub(super) fn function_signature(&self, function: FunctionId) -> Option { + self.functions.get(&function.index()).copied() + } + pub(super) fn frame( + &self, + function: FunctionId, + frame: custody::ResourceFrameId, + ) -> Option { + self.frames.get(&(function.index(), frame.index())).copied() + } + pub(super) fn slot( + &self, + function: FunctionId, + slot: custody::ResourceOwnerSlotId, + ) -> Option { + self.slots.get(&(function.index(), slot.index())).copied() + } + pub(super) fn operation( + &self, + function: FunctionId, + operation: custody::ResourceOperationId, + ) -> Option { + self.operations + .get(&(function.index(), operation.index())) + .copied() + } + + pub(super) fn carrier_slot( + &self, + function: FunctionId, + slot: custody::ResourceCarrierSlotId, + ) -> Option { + self.carrier_slots + .get(&(function.index(), slot.index())) + .copied() + } + pub(super) fn carrier_operation( + &self, + function: FunctionId, + operation: custody::ResourceCarrierOperationId, + ) -> Option { + self.carrier_operations + .get(&(function.index(), operation.index())) + .copied() + } + + /// Emits only an immutable accessor; this does not emit or admit a Source body. + pub(super) fn define_accessor(&self, module: &mut ObjectModule) -> Result<(), CodegenError> { + const SYMBOL: &str = "jett_aot_resource_v1_manifest"; + if module.target_config().pointer_type() != ir::types::I64 { + return Err(pending( + "Resource manifest accessor requires the selected 64-bit layout", + )); + } + if module.get_name(SYMBOL).is_some() { + return Err(CodegenError::DuplicateSymbol(SYMBOL.into())); + } + let data_id = module + .declare_data("__jett_resource_layout_v2", Linkage::Local, false, false) + .map_err(|error| { + CodegenError::Backend(format!("Resource layout data declaration: {error}")) + })?; + let mut data = cranelift_module::DataDescription::new(); + data.define(self.wire.clone().into_boxed_slice()); + module.define_data(data_id, &data).map_err(|error| { + CodegenError::Backend(format!("Resource layout data definition: {error}")) + })?; + let mut signature = module.make_signature(); + signature.params.push(AbiParam::new(ir::types::I64)); + signature.returns.push(AbiParam::new(ir::types::I32)); + let id = module + .declare_function(SYMBOL, Linkage::Export, &signature) + .map_err(|error| { + CodegenError::Backend(format!("Resource manifest accessor declaration: {error}")) + })?; + let mut context = module.make_context(); + context.func.signature = signature; + let mut builder_context = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut context.func, &mut builder_context); + let entry = builder.create_block(); + let valid = builder.create_block(); + let refused = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + let output = builder.block_params(entry)[0]; + let global = module.declare_data_in_func(data_id, builder.func); + let address = builder.ins().global_value(ir::types::I64, global); + let not_null = builder.ins().icmp_imm(IntCC::NotEqual, output, 0); + let low = builder.ins().band_imm(output, 7); + let aligned = builder.ins().icmp_imm(IntCC::Equal, low, 0); + let output_end = builder.ins().iadd_imm(output, 32); + let no_overflow = builder + .ins() + .icmp(IntCC::UnsignedGreaterThan, output_end, output); + let data_end = builder.ins().iadd_imm( + address, + i64::try_from(self.wire.len()) + .map_err(|_| pending("manifest data length exceeds selected address width"))?, + ); + let before = builder + .ins() + .icmp(IntCC::UnsignedLessThanOrEqual, output_end, address); + let after = builder + .ins() + .icmp(IntCC::UnsignedGreaterThanOrEqual, output, data_end); + let separate = builder.ins().bor(before, after); + let valid_alignment = builder.ins().band(not_null, aligned); + let valid_range = builder.ins().band(no_overflow, separate); + let accepted = builder.ins().band(valid_alignment, valid_range); + builder.ins().brif(accepted, valid, &[], refused, &[]); + builder.switch_to_block(valid); + let flags = ir::MemFlags::new(); + builder.ins().store(flags, address, output, 0); + let length = builder.ins().iconst( + ir::types::I64, + i64::try_from(self.wire.len()) + .map_err(|_| pending("manifest data length exceeds selected address width"))?, + ); + builder.ins().store(flags, length, output, 8); + for (offset, value) in [ + (16, self.entry.function), + (20, self.entry.signature), + (24, self.entry.scope), + (28, 0), + ] { + let value = builder.ins().iconst(ir::types::I32, i64::from(value)); + builder.ins().store(flags, value, output, offset); + } + let zero = builder.ins().iconst(ir::types::I32, 0); + builder.ins().return_(&[zero]); + builder.switch_to_block(refused); + let refusal = builder.ins().iconst(ir::types::I32, 1); + builder.ins().return_(&[refusal]); + builder.seal_all_blocks(); + builder.finalize(); + module.define_function(id, &mut context).map_err(|error| { + CodegenError::Backend(format!("Resource manifest accessor definition: {error}")) + })?; + Ok(()) + } +} +pub(super) fn pending(message: &str) -> CodegenError { + CodegenError::Backend(format!("pending native Resource execution: {message}")) +} +fn ordinal(value: usize) -> Result { + if value >= LIMIT { + return Err(pending( + "layout row capacity exceeds the bounded wire domain", + )); + } + u32::try_from(value).map_err(|_| pending("layout ordinal exceeds u32")) +} +fn count(value: usize) -> Result { + if value > LIMIT { + return Err(pending("layout vector exceeds the bounded wire domain")); + } + u32::try_from(value).map_err(|_| pending("layout vector exceeds u32")) +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Signature { + result: u32, + parameters: Vec<(u32, u32)>, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Parent { + Root, + Frame(u32), +} +#[derive(Debug)] +struct Frame { + role: u32, + site: custody::ResourceSite, + signature: u32, + parents: Vec, +} +struct Rows<'a, 'p> { + plan: &'a custody::ResourceOwnershipPlan<'p>, + hooks: Vec<(u32, u32, u32)>, + hook_ids: Vec, + signatures: Vec, + shapes: Vec>, + shape_ids: BTreeMap, + functions: BTreeMap, + frames: Vec, + frame_ids: BTreeMap<(u32, usize), u32>, + slots: Vec>, + slot_ids: BTreeMap<(u32, usize), u32>, + operations: Vec<(custody::ResourceSite, Vec)>, + operation_ids: BTreeMap<(u32, usize), u32>, + borrow_ids: BTreeMap<(u32, usize), u32>, + descriptor_borrow_targets: BTreeMap<(u32, usize), u32>, + carrier_graph: carriers::CarrierGraphRows, + carrier_slots: Vec>, + carrier_slot_ids: BTreeMap<(u32, usize), u32>, + carrier_operations: Vec>, + carrier_operation_ids: BTreeMap<(u32, usize), u32>, + carrier_record_ids: BTreeMap<(u32, usize), u32>, + carrier_borrow_ids: BTreeMap<(u32, usize), u32>, +} +impl<'a, 'p> Rows<'a, 'p> { + fn new(plan: &'a custody::ResourceOwnershipPlan<'p>) -> Result { + count(plan.program().resource_manifest.kinds().len())?; + Ok(Self { + plan, + hooks: Vec::new(), + hook_ids: Vec::new(), + signatures: Vec::new(), + shapes: Vec::new(), + shape_ids: BTreeMap::new(), + functions: BTreeMap::new(), + frames: Vec::new(), + frame_ids: BTreeMap::new(), + slots: Vec::new(), + slot_ids: BTreeMap::new(), + operations: Vec::new(), + operation_ids: BTreeMap::new(), + borrow_ids: BTreeMap::new(), + descriptor_borrow_targets: BTreeMap::new(), + carrier_graph: carriers::CarrierGraphRows::default(), + carrier_slots: Vec::new(), + carrier_slot_ids: BTreeMap::new(), + carrier_operations: Vec::new(), + carrier_operation_ids: BTreeMap::new(), + carrier_record_ids: BTreeMap::new(), + carrier_borrow_ids: BTreeMap::new(), + }) + } + fn shape(&mut self, ty: TypeId) -> Result { + if let Some(id) = self.shape_ids.get(&ty.index()) { + return Ok(*id); + } + if carrier_operations::is_carrier_type(self.plan, ty) { + let node = self.carrier_node(ty)?; + let id = ordinal(self.shapes.len())?; + self.shapes.push(vec![11, node]); + self.shape_ids.insert(ty.index(), id); + return Ok(id); + } + let words = match self.plan.types().resolve(ty) { + Type::Int8 => vec![1, 8, 1], + Type::Int16 => vec![1, 16, 1], + Type::Int32 => vec![1, 32, 1], + Type::Int64 => vec![1, 64, 1], + Type::Uint8 => vec![1, 8, 0], + Type::Uint16 => vec![1, 16, 0], + Type::Uint32 => vec![1, 32, 0], + Type::Uint64 => vec![1, 64, 0], + Type::Float32 => vec![2, 32], + Type::Float64 => vec![2, 64], + Type::Bool => vec![3], + Type::String => vec![4], + Type::Nothing => vec![5], + Type::Capability(jett_types::CapabilityKind::Network) => vec![6], + Type::Resource(_) => vec![ + 7, + self.plan + .program() + .resource_manifest + .kind_for_type(ty) + .ok_or_else(|| pending("shape has no original manifest kind"))? + .id() + .index(), + ], + Type::Optional(child) => { + let child = *child; + vec![8, self.shape(child)?] + } + Type::Result(ok, fail) => { + let (ok, fail) = (*ok, *fail); + vec![9, self.shape(ok)?, self.shape(fail)?] + } + _ => { + return Err(pending( + "exact shape needs its dedicated native qualification, aggregate or callable transport", + )); + } + }; + let id = if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + ordinal(index)? + } else { + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + id + }; + self.shape_ids.insert(ty.index(), id); + Ok(id) + } + fn custody_shape(&mut self, shape: &custody::ResourceShape) -> Result { + let plain = self.shape(shape.kind().ty())?; + let words = match shape { + custody::ResourceShape::Plain { .. } => return Ok(plain), + custody::ResourceShape::Optional { .. } => vec![8, plain], + custody::ResourceShape::Result { failure, .. } => vec![9, plain, self.shape(*failure)?], + }; + if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + return ordinal(index); + } + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + Ok(id) + } + fn signature( + &mut self, + params: &[(TypeId, jett_mir::ParamMode)], + result: TypeId, + ) -> Result { + let result = self.shape(result)?; + self.signature_result(params, result) + } + fn signature_result( + &mut self, + params: &[(TypeId, jett_mir::ParamMode)], + result: u32, + ) -> Result { + let mut parameters = Vec::new(); + for (ty, mode) in params { + parameters.push(( + self.shape(*ty)?, + match mode { + jett_mir::ParamMode::Owned => 1, + jett_mir::ParamMode::View => 2, + }, + )); + } + self.shaped_signature(parameters, result) + } + fn shaped_signature( + &mut self, + parameters: Vec<(u32, u32)>, + result: u32, + ) -> Result { + let signature = Signature { result, parameters }; + if let Some(index) = self + .signatures + .iter() + .position(|existing| existing == &signature) + { + return ordinal(index); + } + let id = ordinal(self.signatures.len())?; + self.signatures.push(signature); + Ok(id) + } + fn function_signature(&self, function: FunctionId) -> Result { + self.functions + .get(&function.index()) + .copied() + .ok_or_else(|| pending("callee has no fresh execution-family signature")) + } + fn entry_bridge( + &mut self, + bridge: &custody::ResourceEntryScopePlan, + ) -> Result<(SelectedEntry, u32), CodegenError> { + let root = bridge.root_scope(); + let complete = bridge.completion(); + if self.plan.function(bridge.function()).is_some() + || root.role() != custody::ResourceFrameRole::Scope + || root.parent().is_some() + || root.site().function() != bridge.function() + || complete.frame() != root.id() + || complete.site() != root.site() + || complete.is_expression_operation() + || !matches!(complete.role(), Role::Complete { .. }) + { + return Err(pending( + "ordinary entry bridge differs from its exact wrapper Scope", + )); + } + let params = bridge + .parameters() + .iter() + .map(|param| (param.ty, param.mode)) + .collect::>(); + let signature = self.signature(¶ms, bridge.return_type())?; + let scope = ordinal(self.frames.len())?; + self.frames.push(Frame { + role: 1, + site: root.site(), + signature, + parents: vec![Parent::Root], + }); + let completion = ordinal(self.operations.len())?; + self.operations.push((complete.site(), vec![13, scope])); + // Entry-only rows do not populate Source-family signature/frame/operation maps. + Ok(( + SelectedEntry { + function: bridge.function().index(), + signature, + scope, + }, + completion, + )) + } + fn frame( + &self, + function: FunctionId, + frame: custody::ResourceFrameId, + ) -> Result { + self.frame_ids + .get(&(function.index(), frame.index())) + .copied() + .ok_or_else(|| pending("frame is outside its exact function plan")) + } + fn slot( + &self, + function: FunctionId, + slot: custody::ResourceOwnerSlotId, + ) -> Result { + self.slot_ids + .get(&(function.index(), slot.index())) + .copied() + .ok_or_else(|| pending("slot is outside its exact function plan")) + } + fn hook(&self, hook: &jett_hir::ResourceHookRef) -> Result { + self.hook_ids + .iter() + .position(|candidate| candidate == hook) + .map(ordinal) + .transpose()? + .ok_or_else(|| pending("operation has a foreign manifest hook")) + } + fn loan( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceLoanId, + ) -> Result, CodegenError> { + let source = function + .loans() + .get(loan.index()) + .filter(|record| record.id() == loan) + .ok_or_else(|| pending("formal has no exact plan loan"))?; + Ok(match source.source() { + custody::ResourceLoanSource::Owner(_) => vec![ + 1, + *self + .borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| pending("loan has no unique actual Borrow operation"))?, + ], + custody::ResourceLoanSource::IncomingViewFormal { scope, parameter } => vec![ + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ], + custody::ResourceLoanSource::CarrierSumProjection { operation } => vec![ + 3, + *self + .carrier_operation_ids + .get(&(function.function().index(), operation.index())) + .ok_or_else(|| { + pending("carrier sum adapter has no exact installed operation") + })?, + ], + custody::ResourceLoanSource::ProjectedSumPayload { .. } => vec![ + 3, + *self + .borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| { + pending("projected payload has no unique constructor projection operation") + })?, + ], + }) + } + fn populate(&mut self) -> Result<(), CodegenError> { + let plan = self.plan; + // Every original hook is retained, including unused descriptors. + for hook in plan.program().resource_manifest.hooks() { + let Type::Function { + params, + view_params, + return_type, + } = self.plan.types().resolve(hook.function_type()) + else { + return Err(pending("manifest hook lost its exact function signature")); + }; + if params.len() != view_params.len() { + return Err(pending("hook access vector is malformed")); + } + let parameters = params + .iter() + .zip(view_params) + .map(|(ty, view)| { + ( + *ty, + if *view { + jett_mir::ParamMode::View + } else { + jett_mir::ParamMode::Owned + }, + ) + }) + .collect::>(); + let result = *return_type; + let signature = self.signature(¶meters, result)?; + let recipe = match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => 1, + jett_types::ResourceKernelRecipe::NetworkBorrow => 2, + jett_types::ResourceKernelRecipe::Finalize => 3, + }; + ordinal(self.hooks.len())?; + self.hooks.push((hook.kind().index(), recipe, signature)); + self.hook_ids.push(hook); + } + for function in plan.functions() { + let params = function + .parameters() + .iter() + .map(|parameter| (parameter.ty, parameter.mode)) + .collect::>(); + let result = if let Some(shape) = self.graph_function_result(function)? { + shape + } else if matches!( + plan.types().resolve(function.return_type()), + Type::Function { .. } + ) { + let hook = function.descriptor_return().ok_or_else(|| { + pending("descriptor result has no exact original/current Return proof") + })?; + if hook.function_type() != function.return_type() { + return Err(pending( + "descriptor Return proof changes its function signature", + )); + } + let words = vec![10, self.hook(&hook)?]; + if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + ordinal(index)? + } else { + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + id + } + } else { + self.shape(function.return_type()).map_err(|error| { + pending(&format!( + "function {} result `{}`: {error}", + function.function().index(), + self.plan.types().type_name(function.return_type()) + )) + })? + }; + let signature = self + .function_signature_result(function, result) + .map_err(|error| { + pending(&format!( + "function {} formal tuple {:?}: {error}", + function.function().index(), + params + .iter() + .map(|(ty, _)| self.plan.types().type_name(*ty)) + .collect::>() + )) + })?; + self.functions + .insert(function.function().index(), signature); + for frame in function.frames() { + let id = ordinal(self.frames.len())?; + self.frame_ids + .insert((function.function().index(), frame.id().index()), id); + self.frames.push(Frame { + role: match frame.role() { + custody::ResourceFrameRole::Scope => 1, + custody::ResourceFrameRole::Operation => 2, + custody::ResourceFrameRole::Return => 3, + }, + site: frame.site(), + signature, + parents: Vec::new(), + }); + } + } + for function in plan.functions() { + for frame in function.frames() { + if let Some(parent) = frame.parent() { + let (id, parent) = ( + self.frame(function.function(), frame.id())?, + self.frame(function.function(), parent)?, + ); + self.frames[id as usize].parents.push(Parent::Frame(parent)); + } + } + if let Some(carriers) = function.carriers() { + for slot in carriers.slots() { + let id = ordinal(self.carrier_slots.len())?; + let frame = self.frame(function.function(), slot.frame())?; + let ty = carriers + .shape(slot.shape()) + .ok_or_else(|| pending("carrier slot lost its exact shape"))? + .ty(); + let node = self.carrier_node(ty)?; + self.carrier_slot_ids + .insert((function.function().index(), slot.id().index()), id); + self.carrier_slots.push(vec![frame, node]); + } + } + for slot in function.owner_slots() { + let id = ordinal(self.slots.len())?; + let frame = self.frame(function.function(), slot.frame())?; + let shape = self.custody_shape(slot.shape())?; + if matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + && shape + != self.signatures[self.function_signature(function.function())? as usize] + .result + { + return Err(pending( + "Return slot differs from its containing function signature result", + )); + } + let mut row = vec![frame, shape, count(slot.shape().active_path().len())?]; + row.extend(slot.shape().active_path().iter().map(|step| match step { + custody::ResourcePath::Some => 1, + custody::ResourcePath::Ok => 2, + })); + self.slot_ids + .insert((function.function().index(), slot.id().index()), id); + self.slots.push(row); + } + } + // Fix all operation ordinals before projecting references to Borrow rows. + for function in plan.functions() { + for operation in function.operations() { + if (!emitted(operation.role()) && operation.indirect_hook_target().is_none()) + || staged_alias(function, operation)?.is_some() + || carrier_operations::alias(function, operation)?.is_some() + { + continue; + } + let id = ordinal(self.operations.len())?; + self.operation_ids + .insert((function.function().index(), operation.id().index()), id); + if let Role::Borrow { loan } + | Role::PrepareSourceBorrow { loan, .. } + | Role::BorrowSum { loan } + | Role::PrepareSourceSumBorrow { loan, .. } + | Role::ProjectSumView { + destination: loan, .. + } = operation.role() + { + if self + .borrow_ids + .insert((function.function().index(), loan.index()), id) + .is_some() + { + return Err(pending("loan has duplicate constructor Borrow operations")); + } + } + if let Role::Carrier { operation: carrier } = operation.role() { + self.allocate_carrier_operation(function, *carrier, id)?; + } + self.operations.push((operation.site(), Vec::new())); + if operation.indirect_hook_target().is_some() + && matches!(operation.role(), Role::InvokeHook { hook, .. } + if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow) + { + let target = ordinal(self.operations.len())?; + self.descriptor_borrow_targets.insert( + (function.function().index(), operation.id().index()), + target, + ); + self.operations.push((operation.site(), Vec::new())); + } + } + } + for function in plan.functions() { + for operation in function.operations() { + let Some(canonical) = carrier_operations::alias(function, operation)? else { + continue; + }; + let id = *self + .operation_ids + .get(&(function.function().index(), canonical.id().index())) + .ok_or_else(|| pending("carrier activation lost its constructor row"))?; + if self + .operation_ids + .insert((function.function().index(), operation.id().index()), id) + .is_some() + { + return Err(pending("carrier activation already owns another wire row")); + } + if let Role::Carrier { operation: carrier } = operation.role() { + self.carrier_operation_ids + .insert((function.function().index(), carrier.index()), id); + } + } + } + for function in plan.functions() { + for operation in function.operations() { + if carrier_operations::alias(function, operation)?.is_some() { + continue; + } + let Some(id) = self + .operation_ids + .get(&(function.function().index(), operation.id().index())) + .copied() + else { + continue; + }; + let row = self.operation_row(function, operation)?; + self.operations[id as usize].1 = row; + if let Some(target) = self + .descriptor_borrow_targets + .get(&(function.function().index(), operation.id().index())) + .copied() + { + self.operations[target as usize].1 = + self.borrow_hook_row(function, operation)?; + } + } + } + // Prepared metadata owns the single wire row. Reached activations use its + // exact ordinal; the private region witness owns their cross-block sites. + for function in plan.functions() { + for operation in function.operations() { + let Some(prepared) = staged_alias(function, operation)? else { + continue; + }; + let id = *self + .operation_ids + .get(&(function.function().index(), prepared.id().index())) + .ok_or_else(|| pending("staged activation lost its prepared row"))?; + if self + .operation_ids + .insert((function.function().index(), operation.id().index()), id) + .is_some() + { + return Err(pending("staged activation already owns another wire row")); + } + } + } + for frame in &mut self.frames { + frame.parents.sort(); + frame.parents.dedup(); + count(frame.parents.len())?; + } + Ok(()) + } + fn operation_row( + &mut self, + function: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + ) -> Result, CodegenError> { + let f = function.function(); + let frame = self.frame(f, operation.frame())?; + if let Some(proof) = operation.indirect_hook_target() { + let Role::InvokeHook { hook, source, .. } = operation.role() else { + return Err(pending( + "descriptor target proof is not an exact hook invocation", + )); + }; + if hook != proof + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } + if signature_type == hook.function_type()) + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(pending( + "descriptor wire projection changes its exact original hook tuple", + )); + } + let target = if hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow { + *self + .descriptor_borrow_targets + .get(&(f.index(), operation.id().index())) + .ok_or_else(|| pending("indirect Borrow has no separate physical target row"))? + } else { + let mut targets = function.operations().iter().filter(|physical| { + physical.frame() == operation.frame() + && match physical.role() { + Role::Acquire { hook: current, .. } => { + current == hook + && hook.recipe() + == jett_types::ResourceKernelRecipe::NetworkFactory + } + Role::Close { hook: current, .. } => { + current == hook + && hook.recipe() == jett_types::ResourceKernelRecipe::Finalize + } + _ => false, + } + }); + let target = targets + .next() + .ok_or_else(|| pending("indirect hook has no exact physical target"))?; + if targets.next().is_some() { + return Err(pending("indirect hook has multiple physical targets")); + } + *self + .operation_ids + .get(&(f.index(), target.id().index())) + .ok_or_else(|| pending("indirect hook physical target has no fixed row"))? + }; + let hook = self.hook(hook)?; + return Ok(vec![15, hook, self.hooks[hook as usize].2, target]); + } + Ok(match operation.role() { + Role::Acquire { hook, destination } => { + vec![1, frame, self.hook(hook)?, self.slot(f, *destination)?] + } + Role::Transfer { + source, + destination, + } => { + let slot = function + .owner_slots() + .get(destination.index()) + .ok_or_else(|| { + pending("Transfer destination is outside its fresh function plan") + })?; + let execution_frame = + if matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) { + let returning = function.provisional_return().ok_or_else(|| { + pending("Return transfer has no exact provisional frame") + })?; + if operation.frame() != returning.id() || slot.frame() != returning.id() { + return Err(pending( + "Return transfer differs from its exact provisional storage", + )); + } + // Execution precedes Scope cleanup; storage remains parent-lived Return. + self.frame(f, function.root_scope().id())? + } else { + frame + }; + vec![ + 2, + execution_frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ] + } + Role::Borrow { loan } | Role::PrepareSourceBorrow { loan, .. } => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("Borrow loan is absent"))?; + let custody::ResourceLoanSource::Owner(source) = record.source() else { + return Err(pending("resident formals cannot mint child core loans")); + }; + vec![ + 3, + frame, + self.slot(f, source)?, + self.frame(f, record.frame())?, + ] + } + Role::EndBorrow { loan } => { + let source = self.loan(function, *loan)?; + if !matches!(source[0], 1 | 3) { + return Err(pending( + "resident formal cannot retire the caller's core loan", + )); + } + vec![5, frame, source[1]] + } + Role::BorrowSum { loan } | Role::PrepareSourceSumBorrow { loan, .. } => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("sum borrow has no exact loan"))?; + let custody::ResourceLoanSource::Owner(source) = record.source() else { + return Err(pending( + "sum borrow cannot mint authority from an incoming formal", + )); + }; + if matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + return Err(pending("sum borrow requires an exact conditional shape")); + } + vec![ + 21, + frame, + self.slot(f, source)?, + self.frame(f, record.frame())?, + ] + } + Role::ObserveSumView { source, .. } => { + let mut row = vec![22, frame]; + row.extend(self.loan(function, *source)?); + row + } + Role::ProjectSumView { + source, + destination, + path, + .. + } => { + let record = function + .loans() + .get(destination.index()) + .ok_or_else(|| pending("sum projection has no exact child loan"))?; + if !matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + return Err(pending("sum projection child is not a plain Resource loan")); + } + let mut row = vec![23, frame]; + row.extend(self.loan(function, *source)?); + row.push(match path { + custody::ResourceSumPayloadPath::OptionalSome => 1, + custody::ResourceSumPayloadPath::ResultOk => 2, + }); + row.push(self.frame(f, record.frame())?); + row + } + Role::ReadFailureCompanion { + source, failure, .. + } => { + let mut row = vec![24, frame]; + row.extend(self.loan(function, *source)?); + row.push(self.shape(*failure)?); + row + } + Role::EndSumBorrow { loan } => { + let source = self.loan(function, *loan)?; + if !matches!(source[0], 1 | 3) { + return Err(pending( + "incoming sum formal cannot end its caller's shell lease", + )); + } + vec![25, frame, source[1]] + } + Role::InvokeHook { hook, .. } => { + if hook.recipe() != jett_types::ResourceKernelRecipe::NetworkBorrow { + return Err(pending("unexpected emitted hook metadata role")); + } + self.borrow_hook_row(function, operation)? + } + Role::Close { hook, source } => { + vec![7, frame, self.hook(hook)?, self.slot(f, *source)?] + } + Role::Drop { source, .. } => { + let slot = function + .owner_slots() + .get(source.index()) + .ok_or_else(|| pending("Drop slot is absent"))?; + vec![ + if matches!(slot.shape(), custody::ResourceShape::Plain { .. }) { + 8 + } else { + 11 + }, + frame, + self.slot(f, *source)?, + ] + } + Role::SumAdopt { + source, + destination, + } => vec![ + 9, + frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ], + Role::SumTake { + source, + destination, + success, + .. + } => { + if !*success { + return Err(pending("failure extraction cannot adopt a Resource owner")); + } + vec![ + 10, + frame, + self.slot(f, *source)?, + self.slot(f, *destination)?, + ] + } + Role::Replace { + destination, + replacement, + old, + } => { + let old_slot = function + .owner_slots() + .get(destination.index()) + .ok_or_else(|| pending("Replace destination is outside its exact plan"))?; + let rhs_slot = function + .owner_slots() + .get(replacement.index()) + .ok_or_else(|| pending("Replace RHS is outside its exact plan"))?; + if destination == replacement + || (matches!(old_slot.shape(), custody::ResourceShape::Plain { .. }) + && *old != custody::ResourceOccupancy::Occupied) + || old_slot.shape() != rhs_slot.shape() + || !matches!(old_slot.storage(), custody::ResourceSlotStorage::Local { header } if header.mutable) + || old_slot.frame() != operation.frame() + || rhs_slot.frame() != operation.frame() + || function.frames()[operation.frame().index()].role() + != custody::ResourceFrameRole::Scope + { + return Err(pending( + "mutable replacement requires distinct matching owned slots in the exact Scope", + )); + } + vec![ + 12, + frame, + self.slot(f, *destination)?, + self.slot(f, *replacement)?, + ] + } + Role::SelfRebind { .. } => { + return Err(pending("SelfRebind has no runtime operation row")); + } + Role::Complete { .. } => vec![13, frame], + Role::Descriptor { hook } => vec![14, self.hook(hook)?], + Role::BeginSourceFunction { + function: callee, + formals, + evaluation_order, + operands, + result, + .. + } + | Role::InvokeSourceFunction { + function: callee, + formals, + evaluation_order, + operands, + result, + .. + } => self.source_row( + function, + operation, + frame, + *callee, + formals, + evaluation_order, + operands, + *result, + )?, + Role::InvokeNativeSuite { invocation } => { + self.native_suite_row(function, frame, invocation)? + } + Role::TakeFailureCompanion { + source, failure, .. + } => vec![17, frame, self.slot(f, *source)?, self.shape(*failure)?], + Role::CompleteReturnAfterCleanup { source } => vec![ + 18, + self.frame(f, function.root_scope().id())?, + self.slot(f, *source)?, + ], + Role::CreateAbsentSum { destination } => vec![19, frame, self.slot(f, *destination)?], + Role::CreateFailureSum { + destination, + failure, + } => vec![ + 20, + frame, + self.slot(f, *destination)?, + self.shape(*failure)?, + ], + Role::Carrier { operation: carrier } => { + let record = self.encode_carrier_operation(function, *carrier)?; + vec![26, record] + } + Role::StageSourceActual { .. } | Role::BoundedBorrowUse { .. } => { + return Err(pending( + "bounded use is joined by the actual invocation row", + )); + } + }) + } + fn borrow_hook_row( + &mut self, + function: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + ) -> Result, CodegenError> { + let Role::InvokeHook { hook, operands, .. } = operation.role() else { + return Err(pending( + "physical Borrow target is not an exact hook operation", + )); + }; + if hook.recipe() != jett_types::ResourceKernelRecipe::NetworkBorrow { + return Err(pending("physical Borrow target changes its hook recipe")); + } + let mut loans = operands.iter().filter_map(|operand| { + if let Operand::Borrowed { loan, .. } = operand { + Some(*loan) + } else { + None + } + }); + let loan = loans + .next() + .ok_or_else(|| pending("Borrow hook has no exact resource loan"))?; + if loans.next().is_some() { + return Err(pending("Borrow hook has multiple resource loan operands")); + } + let mut row = vec![ + 6, + self.frame(function.function(), operation.frame())?, + self.hook(hook)?, + ]; + row.extend(self.loan(function, loan)?); + Ok(row) + } + fn source_row( + &mut self, + caller: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + frame: u32, + target: FunctionId, + formals: &[custody::ResourceCallFormal], + order: &[usize], + operands: &[Operand], + result: CallResult, + ) -> Result, CodegenError> { + let plan = self.plan; + let callee = plan + .function(target) + .ok_or_else(|| pending("Source target is outside the fresh execution closure"))?; + if formals.len() != callee.parameters().len() + || order.len() != formals.len() + || operands.len() != formals.len() + { + return Err(pending("Source formal tuple is incomplete")); + } + let scope = self.frame(target, callee.root_scope().id())?; + let returning = callee + .provisional_return() + .map(|record| self.frame(target, record.id())) + .transpose()?; + let parent = returning.unwrap_or(scope); + self.frames[parent as usize] + .parents + .push(Parent::Frame(frame)); + let mut row = vec![ + 16, + frame, + target.index(), + self.function_signature(target)?, + scope, + ]; + if let Some(returning) = returning { + row.extend([1, returning]); + } else { + row.push(0); + } + row.push(count(order.len())?); + for parameter in order { + row.push(count(*parameter)?); + } + row.push(count(formals.len())?); + for (parameter, param) in callee.parameters().iter().enumerate() { + let fact = formals + .iter() + .find(|fact| fact.parameter() == parameter) + .ok_or_else(|| pending("Source parameter has no original formal fact"))?; + if fact.parameter_type() != param.ty + || fact.access() != param.mode + || order.get(fact.source_index()) != Some(¶meter) + { + return Err(pending( + "Source target header/permutation differs from its sealed formal", + )); + } + let syntax = match fact.syntax() { + custody::ResourceArgumentSyntax::Bare => 1, + custody::ResourceArgumentSyntax::WrittenView => 2, + }; + let effect = match fact.effect() { + custody::ResourceArgumentEffect::Copy => 1, + custody::ResourceArgumentEffect::TransferOwned => 2, + custody::ResourceArgumentEffect::RelinquishOwned => 3, + custody::ResourceArgumentEffect::RetainBorrow => 4, + custody::ResourceArgumentEffect::ObserveData => 5, + }; + let operand = operands + .iter() + .find(|operand| match operand { + Operand::Ordinary { parameter: p, .. } + | Operand::Owned { parameter: p, .. } + | Operand::Borrowed { parameter: p, .. } + | Operand::CarrierOwned { parameter: p, .. } + | Operand::CarrierBorrowed { parameter: p, .. } => *p == parameter, + }) + .ok_or_else(|| pending("Source formal has no exact evaluated operand"))?; + let (actual_shape, formal_shape) = + self.source_shapes(caller, operation, callee, param, fact, operand, order)?; + row.extend([ + count(parameter)?, + count(fact.source_index())?, + actual_shape, + formal_shape, + syntax, + effect, + if param.mode == jett_mir::ParamMode::View { + 2 + } else { + 1 + }, + ]); + match operand { + Operand::Ordinary { .. } => row.push(1), + Operand::CarrierOwned { slot, .. } => { + let carriers = callee + .carriers() + .ok_or_else(|| pending("carrier formal has no callee plan"))?; + let destinations = carriers.slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == param.local)).collect::>(); + if destinations.len() != 1 + || destinations[0].frame() != callee.root_scope().id() + { + return Err(pending("carrier owned formal has no exact parameter slot")); + } + row.extend([ + 5, + self.carrier_slot(caller.function(), *slot)?, + self.carrier_slot(target, destinations[0].id())?, + ]); + } + Operand::CarrierBorrowed { loan, .. } => { + row.push(6); + row.extend(self.carrier_loan(caller, *loan)?); + } + Operand::Owned { slot, .. } => { + let destinations=callee.owner_slots().iter().filter(|slot| matches!(slot.storage(),custody::ResourceSlotStorage::Local {header} if header.id==param.local)).collect::>(); + if destinations.len() != 1 + || destinations[0].frame() != callee.root_scope().id() + { + return Err(pending("owned formal has no unique exact parameter slot")); + } + row.extend([ + 2, + self.slot(caller.function(), *slot)?, + self.slot(target, destinations[0].id())?, + ]); + } + Operand::Borrowed { loan, .. } => { + let record = caller + .loans() + .get(loan.index()) + .ok_or_else(|| pending("Source formal has no exact borrowed shape"))?; + row.push( + if matches!(record.shape(), custody::ResourceShape::Plain { .. }) { + 3 + } else { + 4 + }, + ); + row.extend(self.loan(caller, *loan)?); + } + } + } + match result { + CallResult::Ordinary { ty } => { + if ty != callee.return_type() || returning.is_some() { + return Err(pending( + "ordinary Source result changes the callee return contract", + )); + } + row.extend([ + 1, + self.signatures[self.function_signature(target)? as usize].result, + ]); + } + CallResult::Carrier { slot } => { + let returning = + returning.ok_or_else(|| pending("carrier result has no provisional Return"))?; + let carriers = caller + .carriers() + .ok_or_else(|| pending("carrier result has no caller plan"))?; + let destination = carriers + .slots() + .get(slot.index()) + .filter(|row| row.id() == slot) + .ok_or_else(|| pending("carrier caller result slot is absent"))?; + let ty = carriers + .shape(destination.shape()) + .ok_or_else(|| pending("carrier result shape is absent"))? + .ty(); + let callee_carriers = callee + .carriers() + .ok_or_else(|| pending("carrier result has no callee plan"))?; + let mut returns = callee_carriers + .slots() + .iter() + .filter(|slot| { + matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + }) + .map(|slot| self.carrier_slot(target, slot.id())) + .collect::, _>>()?; + returns.sort(); + returns.dedup(); + if returns.is_empty() { + return Err(pending("carrier callee has no exact Return publication")); + } + row.extend([ + 3, + self.shape(ty)?, + self.frame(caller.function(), destination.frame())?, + self.carrier_slot(caller.function(), slot)?, + returning, + count(returns.len())?, + ]); + row.extend(returns); + } + CallResult::Owned { slot } => { + let returning = returning.ok_or_else(|| { + pending("owned Source result has no callee provisional Return") + })?; + let destination = caller + .owner_slots() + .get(slot.index()) + .ok_or_else(|| pending("caller result slot is absent"))?; + let mut returns = callee + .owner_slots() + .iter() + .filter(|slot| { + matches!(slot.storage(), custody::ResourceSlotStorage::Return { .. }) + }) + .map(|slot| self.slot(target, slot.id())) + .collect::, _>>()?; + returns.sort(); + returns.dedup(); + if returns.is_empty() { + return Err(pending("callee has no actual Return publication site")); + } + row.extend([ + 2, + self.custody_shape(destination.shape())?, + self.frame(caller.function(), destination.frame())?, + self.slot(caller.function(), slot)?, + returning, + count(returns.len())?, + ]); + row.extend(returns); + } + } + Ok(row) + } + fn encode(&self) -> Result, CodegenError> { + let mut output = Vec::new(); + output.extend_from_slice(b"JTRSC001"); + word( + &mut output, + if self.carrier_graph.is_empty() { 2 } else { 3 }, + ); + word(&mut output, 0); + output.extend_from_slice(&0u64.to_le_bytes()); + for size in [ + self.plan.program().resource_manifest.kinds().len(), + self.hooks.len(), + self.signatures.len(), + self.shapes.len(), + self.frames.len(), + self.slots.len(), + self.operations.len(), + ] { + word(&mut output, count(size)?); + } + word(&mut output, 0); + for i in 0..self.plan.program().resource_manifest.kinds().len() { + word(&mut output, ordinal(i)?); + } + for (i, (kind, recipe, signature)) in self.hooks.iter().enumerate() { + words(&mut output, &[ordinal(i)?, *kind, *recipe, *signature]); + } + for (i, signature) in self.signatures.iter().enumerate() { + words( + &mut output, + &[ + ordinal(i)?, + count(signature.parameters.len())?, + signature.result, + ], + ); + for (shape, access) in &signature.parameters { + words(&mut output, &[*shape, *access]); + } + } + for (i, shape) in self.shapes.iter().enumerate() { + word(&mut output, ordinal(i)?); + words(&mut output, shape); + } + for (i, frame) in self.frames.iter().enumerate() { + words(&mut output, &[ordinal(i)?, frame.role]); + site(&mut output, frame.site)?; + words(&mut output, &[frame.signature, count(frame.parents.len())?]); + for parent in &frame.parents { + match parent { + Parent::Root => words(&mut output, &[0, 0]), + Parent::Frame(index) => words(&mut output, &[1, *index]), + } + } + } + for (i, slot) in self.slots.iter().enumerate() { + word(&mut output, ordinal(i)?); + words(&mut output, slot); + } + for (i, (at, operation)) in self.operations.iter().enumerate() { + word(&mut output, ordinal(i)?); + site(&mut output, *at)?; + words(&mut output, operation); + } + if !self.carrier_graph.is_empty() { + carriers::append_extension( + &mut output, + &self.carrier_graph, + &self.carrier_slots, + &self.carrier_operations, + )?; + } + if output.len() > BYTE_LIMIT { + return Err(pending( + "layout byte capacity exceeds the bounded wire domain", + )); + } + let length = + u64::try_from(output.len()).map_err(|_| pending("layout length exceeds u64"))?; + output[16..24].copy_from_slice(&length.to_le_bytes()); + Ok(output) + } +} +fn staged_alias<'a>( + function: &'a custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, +) -> Result, CodegenError> { + let mut prepared = function.operations().iter().filter(|candidate| { + match (operation.role(), candidate.role()) { + (Role::Borrow { loan }, Role::PrepareSourceBorrow { loan: expected, .. }) + | (Role::BorrowSum { loan }, Role::PrepareSourceSumBorrow { loan: expected, .. }) => { + loan == expected + } + (Role::InvokeSourceFunction { .. }, Role::BeginSourceFunction { .. }) => { + operation.frame() == candidate.frame() + } + _ => false, + } + }); + let Some(selected) = prepared.next() else { + return Ok(None); + }; + if prepared.next().is_some() || selected.frame() != operation.frame() { + return Err(pending( + "staged activation has ambiguous or foreign prepared metadata", + )); + } + match (operation.role(), selected.role()) { + (Role::Borrow { loan }, Role::PrepareSourceBorrow { parameter, .. }) + | (Role::BorrowSum { loan }, Role::PrepareSourceSumBorrow { parameter, .. }) => { + let record = function + .loans() + .get(loan.index()) + .ok_or_else(|| pending("staged activation loan is absent"))?; + if record.frame() != operation.frame() || record.parameter() != Some(*parameter) { + return Err(pending("staged activation changes its sealed loan/formal")); + } + } + ( + Role::InvokeSourceFunction { + function: current, + formals, + evaluation_order, + operands, + result, + .. + }, + Role::BeginSourceFunction { + function: expected, + formals: originals, + evaluation_order: order, + operands: planned, + result: output, + .. + }, + ) => { + if current != expected + || evaluation_order != order + || result != output + || formals.len() != originals.len() + || operands.len() != planned.len() + || !formals.iter().zip(originals).all(|(a, b)| { + a.parameter() == b.parameter() + && a.source_index() == b.source_index() + && a.actual_type() == b.actual_type() + && a.parameter_type() == b.parameter_type() + && a.syntax() == b.syntax() + && a.effect() == b.effect() + && a.access() == b.access() + }) + || !operands + .iter() + .zip(planned) + .all(|(a, b)| same_operand(a, b)) + { + return Err(pending( + "staged Invoke changes its complete prepared Source tuple", + )); + } + } + _ => return Err(pending("staged activation selected another prepared role")), + } + Ok(Some(selected)) +} + +fn same_operand(a: &Operand, b: &Operand) -> bool { + match (a, b) { + ( + Operand::Ordinary { + parameter: p, + ty: a, + }, + Operand::Ordinary { + parameter: q, + ty: b, + }, + ) => p == q && a == b, + ( + Operand::Owned { + parameter: p, + slot: a, + }, + Operand::Owned { + parameter: q, + slot: b, + }, + ) => p == q && a == b, + ( + Operand::Borrowed { + parameter: p, + loan: a, + }, + Operand::Borrowed { + parameter: q, + loan: b, + }, + ) => p == q && a == b, + ( + Operand::CarrierOwned { + parameter: p, + slot: a, + }, + Operand::CarrierOwned { + parameter: q, + slot: b, + }, + ) => p == q && a == b, + ( + Operand::CarrierBorrowed { + parameter: p, + loan: a, + }, + Operand::CarrierBorrowed { + parameter: q, + loan: b, + }, + ) => p == q && a == b, + _ => false, + } +} + +fn emitted(role: &Role) -> bool { + match role { + Role::StageSourceActual { .. } + | Role::BoundedBorrowUse { .. } + | Role::SelfRebind { .. } => false, + Role::InvokeHook { hook, .. } => { + hook.recipe() == jett_types::ResourceKernelRecipe::NetworkBorrow + } + _ => true, + } +} +fn word(output: &mut Vec, value: u32) { + output.extend_from_slice(&value.to_le_bytes()); +} +fn words(output: &mut Vec, values: &[u32]) { + for value in values { + word(output, *value); + } +} +fn site(output: &mut Vec, at: custody::ResourceSite) -> Result<(), CodegenError> { + words(output, &[at.function().index(), at.block().index()]); + match at.position() { + custody::ResourcePosition::Statement(index) => words(output, &[0, count(index)?]), + custody::ResourcePosition::Terminator => words(output, &[1, 0]), + }; + Ok(()) +} + +#[cfg(test)] +#[path = "resource_layout/tests.rs"] +mod tests; diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs new file mode 100644 index 00000000..d9887d35 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_operations.rs @@ -0,0 +1,711 @@ +//! Exact v3 operation projection. The fresh private plan, not a wire type/name, +//! selects every carrier slot, path, constructor child, and lifetime edge. +use super::*; +use custody::{ + ResourceCarrierConstructor as Constructor, ResourceCarrierLoanSource as LoanSource, + ResourceCarrierObservation as Observation, ResourceCarrierOperationRole as CarrierRole, + ResourceCarrierProjectionPath as Path, ResourceCarrierValue as CarrierValue, +}; + +pub(in crate::emit) fn is_carrier_type( + plan: &custody::ResourceOwnershipPlan<'_>, + ty: TypeId, +) -> bool { + if !plan + .carrier_shapes() + .iter() + .any(|shape| shape.ty() == ty && shape.contains_resource()) + { + return false; + } + match plan.types().resolve(ty) { + Type::Resource(_) | Type::Function { .. } => false, + Type::Optional(inner) if matches!(plan.types().resolve(*inner), Type::Resource(_)) => false, + Type::Result(ok, fail) + if matches!(plan.types().resolve(*ok), Type::Resource(_)) + && !plan.type_requires_custody(*fail) => + { + false + } + _ => true, + } +} + +/// Child/commit and the explicit borrowed-sum activation use the one row owned by +/// their constructor. A dead frame or matching signature cannot create an alias. +pub(super) fn alias<'a>( + function: &'a custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, +) -> Result, CodegenError> { + let Some(carriers) = function.carriers() else { + return Ok(None); + }; + let authority = match operation.role() { + Role::Carrier { operation: id } => { + let current = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == *id) + .ok_or_else(|| pending("carrier wrapper lost its exact private operation"))?; + let destination = match current.role() { + CarrierRole::ConstructorChild { destination, .. } + | CarrierRole::CommitConstructor { destination } => *destination, + _ => return Ok(None), + }; + let mut starts = carriers.operations().iter().filter(|row| + matches!(row.role(), CarrierRole::BeginConstructor { destination: slot, .. } if *slot == destination)); + let start = starts + .next() + .ok_or_else(|| pending("carrier activation has no constructor begin"))?; + if starts.next().is_some() + || (start.site() != current.site() + && (start.is_expression_operation() || current.is_expression_operation())) + || start.frame() != current.frame() + || start.id().index() >= current.id().index() + { + return Err(pending( + "carrier activation changes its exact constructor/site/order", + )); + } + start.id() + } + Role::BorrowSum { loan } => { + let record = function + .loans() + .get(loan.index()) + .filter(|row| row.id() == *loan) + .ok_or_else(|| pending("carrier sum activation lost its exact loan"))?; + let custody::ResourceLoanSource::CarrierSumProjection { operation: id } = + record.source() + else { + return Ok(None); + }; + let adapter = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier sum loan lost its exact adapter"))?; + if adapter.frame() != operation.frame() + || !matches!(adapter.role(), CarrierRole::AdaptSum { loan: Some(expected), .. } if *expected == *loan) + { + return Err(pending( + "carrier sum activation changes its parent/lease/loan", + )); + } + id + } + _ => return Ok(None), + }; + let mut wrappers = function.operations().iter().filter(|candidate| + matches!(candidate.role(), Role::Carrier { operation } if *operation == authority)); + let selected = wrappers + .next() + .ok_or_else(|| pending("carrier authority has no exact wrapper"))?; + if wrappers.next().is_some() { + return Err(pending("carrier authority has duplicate wrappers")); + } + Ok(Some(selected)) +} + +impl Rows<'_, '_> { + pub(super) fn carrier_node(&mut self, ty: TypeId) -> Result { + if let Some(node) = self.carrier_graph.node_for_type(ty) { + return Ok(node); + } + let plan = self.plan; + let carriers = plan + .functions() + .iter() + .find_map(|function| function.carriers()) + .ok_or_else(|| pending("carrier shape has no constructor-owned graph"))?; + let shape = carriers + .shape_for_type(ty) + .ok_or_else(|| pending("carrier shape is outside its exact checked graph"))?; + let mut graph = std::mem::take(&mut self.carrier_graph); + let result = graph.project( + carriers, + plan.types(), + shape.id(), + &mut |ty| self.shape(ty), + &mut |ty| { + let predicate = carriers + .shape_for_type(ty) + .and_then(|shape| shape.refinement_predicate()) + .ok_or_else(|| { + pending("refinement graph lost its original checked predicate/body") + })?; + // The private getter authenticates the unique retained header and full + // body against the original checked archive/current witness. These + // bytes remain consistency metadata; runtime never grants a predicate + // or nominal proof because a type spelling happens to match. + use sha2::{Digest, Sha256}; + let mut bytes = predicate.id.index().to_le_bytes().to_vec(); + bytes.extend_from_slice(&Sha256::digest(format!("{:?}", predicate).as_bytes())); + Ok(bytes) + }, + ); + self.carrier_graph = graph; + result + } + + pub(super) fn carrier_slot( + &self, + function: FunctionId, + slot: custody::ResourceCarrierSlotId, + ) -> Result { + self.carrier_slot_ids + .get(&(function.index(), slot.index())) + .copied() + .ok_or_else(|| pending("carrier slot is outside its exact function plan")) + } + + pub(super) fn carrier_loan( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result, CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier loan has no current plan"))?; + let row = carriers + .loans() + .get(loan.index()) + .filter(|row| row.id() == loan) + .ok_or_else(|| pending("carrier loan is outside its exact plan"))?; + if let Some(operation) = self + .carrier_borrow_ids + .get(&(function.function().index(), loan.index())) + { + return Ok(vec![1, *operation]); + } + match row.source() { + LoanSource::IncomingViewFormal { scope, parameter } if row.path().is_empty() => { + Ok(vec![ + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ]) + } + _ => Err(pending( + "carrier loan has no unique Borrow or exact incoming formal", + )), + } + } + + fn carrier_source( + &self, + function: &custody::ResourceFunctionPlan, + source: LoanSource, + ) -> Result, CodegenError> { + let mut words = Vec::new(); + match source { + LoanSource::Slot(slot) => { + words.extend([1, self.carrier_slot(function.function(), slot)?]) + } + LoanSource::Loan(loan) => { + words.push(2); + words.extend(self.carrier_loan(function, loan)?); + } + LoanSource::IncomingViewFormal { scope, parameter } => words.extend([ + 2, + 2, + self.frame(function.function(), scope)?, + count(parameter)?, + ]), + } + Ok(words) + } + + fn borrowed_source( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result, CodegenError> { + let mut row = vec![2]; + row.extend(self.carrier_loan(function, loan)?); + Ok(row) + } + + /// Owning extraction must resolve to the exact root, rather than converting + /// an incoming or sibling loan into ownership. Paths stay in original order. + fn owned_root( + &self, + function: &custody::ResourceFunctionPlan, + loan: custody::ResourceCarrierLoanId, + ) -> Result<(custody::ResourceCarrierSlotId, Vec), CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier extraction has no plan"))?; + let mut visited = std::collections::BTreeSet::new(); + let mut cursor = loan; + let mut paths = Vec::new(); + loop { + if !visited.insert(cursor.index()) { + return Err(pending("carrier extraction has a cyclic loan origin")); + } + let row = carriers + .loans() + .get(cursor.index()) + .filter(|row| row.id() == cursor) + .ok_or_else(|| pending("carrier extraction lost its parent loan"))?; + paths.push(row.path().to_vec()); + match row.source() { + LoanSource::Slot(slot) => { + let mut path = Vec::new(); + for segment in paths.iter().rev() { + path.extend_from_slice(segment); + } + return Ok((slot, path)); + } + LoanSource::Loan(parent) => cursor = parent, + LoanSource::IncomingViewFormal { .. } => { + return Err(pending("owning extraction cannot consume an incoming view")); + } + } + } + } + + pub(super) fn allocate_carrier_operation( + &mut self, + function: &custody::ResourceFunctionPlan, + id: custody::ResourceCarrierOperationId, + wrapper: u32, + ) -> Result<(), CodegenError> { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier wrapper has no current plan"))?; + let row = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier wrapper changed its exact operation"))?; + let record = ordinal(self.carrier_operations.len())?; + if self + .carrier_operation_ids + .insert((function.function().index(), id.index()), wrapper) + .is_some() + || self + .carrier_record_ids + .insert((function.function().index(), id.index()), record) + .is_some() + { + return Err(pending("carrier operation already has a wire row")); + } + match row.role() { + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => { + if self + .carrier_borrow_ids + .insert((function.function().index(), loan.index()), wrapper) + .is_some() + { + return Err(pending("carrier loan has duplicate exact Borrow rows")); + } + } + CarrierRole::AdaptSum { + loan: Some(loan), .. + } => { + if self + .borrow_ids + .insert((function.function().index(), loan.index()), wrapper) + .is_some() + { + return Err(pending("carrier sum loan has duplicate adapter rows")); + } + } + _ => {} + } + self.carrier_operations.push(Vec::new()); + Ok(()) + } + + pub(super) fn encode_carrier_operation( + &mut self, + function: &custody::ResourceFunctionPlan, + id: custody::ResourceCarrierOperationId, + ) -> Result { + let carriers = function + .carriers() + .ok_or_else(|| pending("carrier wrapper lost its private plan"))?; + let operation = carriers + .operations() + .get(id.index()) + .filter(|row| row.id() == id) + .ok_or_else(|| pending("carrier wrapper lost its exact occurrence"))?; + let record = *self + .carrier_record_ids + .get(&(function.function().index(), id.index())) + .ok_or_else(|| pending("carrier occurrence has no allocated row"))?; + let frame = self.frame(function.function(), operation.frame())?; + let mut row = Vec::new(); + site(&mut row, operation.site())?; + let slot = |this: &Self, slot| this.carrier_slot(function.function(), slot); + match operation.role() { + CarrierRole::BeginConstructor { + destination, + constructor, + } => { + words( + &mut row, + &[ + 1, + frame, + slot(self, *destination)?, + constructor_selector(*constructor)?, + ], + ); + let mut children = Vec::new(); + let mut committed = false; + for candidate in carriers.operations().iter().skip(id.index() + 1) { + match candidate.role() { + CarrierRole::ConstructorChild { + destination: target, + child, + } if target == destination => { + if (candidate.site() != operation.site() + && (candidate.is_expression_operation() + || operation.is_expression_operation())) + || candidate.frame() != operation.frame() + || committed + { + return Err(pending( + "constructor child changes exact frame/site/commit order", + )); + } + children.push(child); + } + CarrierRole::CommitConstructor { + destination: target, + } if target == destination => { + if (candidate.site() != operation.site() + && (candidate.is_expression_operation() + || operation.is_expression_operation())) + || candidate.frame() != operation.frame() + || committed + { + return Err(pending( + "constructor commit is duplicated or disconnected", + )); + } + committed = true; + } + _ => {} + } + } + if !committed { + return Err(pending("carrier constructor has no exact commit")); + } + word(&mut row, count(children.len())?); + for child in children { + let ty = carriers + .shape(child.shape) + .ok_or_else(|| pending("constructor child lost its exact node"))? + .ty(); + words(&mut row, &[count(child.index)?, self.carrier_node(ty)?]); + match child.value { + CarrierValue::Ordinary { ty } => words(&mut row, &[1, self.shape(ty)?]), + CarrierValue::Owned { slot: source } => { + words(&mut row, &[2, slot(self, source)?]) + } + CarrierValue::LeafOwned { slot: source } => { + let tag = match function + .owner_slots() + .get(source.index()) + .ok_or_else(|| { + pending("constructor child lost its exact leaf slot") + })? + .shape() + { + custody::ResourceShape::Plain { .. } => 4, + custody::ResourceShape::Optional { .. } + | custody::ResourceShape::Result { .. } => 3, + }; + words(&mut row, &[tag, self.slot(function.function(), source)?]) + } + CarrierValue::Borrowed { .. } | CarrierValue::LeafBorrowed { .. } => { + return Err(pending( + "owning carrier constructor cannot adopt a borrowed child", + )); + } + } + } + } + CarrierRole::Transfer { + source, + destination, + } => words( + &mut row, + &[2, frame, slot(self, *source)?, slot(self, *destination)?], + ), + CarrierRole::QualifyMachine { + source, + destination, + machine, + state, + } => { + let ty = carriers + .shape(*machine) + .ok_or_else(|| pending("carrier qualification lost its exact machine"))? + .ty(); + words( + &mut row, + &[ + 11, + frame, + slot(self, *source)?, + slot(self, *destination)?, + self.carrier_node(ty)?, + count(*state)?, + ], + ); + } + CarrierRole::Borrow { loan } + | CarrierRole::Project { + destination: loan, .. + } => { + let loan = carriers + .loans() + .get(loan.index()) + .ok_or_else(|| pending("carrier Borrow lost its exact loan"))?; + words(&mut row, &[3, frame]); + row.extend(words_bytes(&self.carrier_source(function, loan.source())?)); + path(&mut row, loan.path())?; + let ty = carriers + .shape(loan.shape()) + .ok_or_else(|| pending("carrier Borrow lost its exact shape"))? + .ty(); + words( + &mut row, + &[ + self.carrier_node(ty)?, + self.frame(function.function(), loan.frame())?, + ], + ); + } + CarrierRole::EndBorrow { loan } => words( + &mut row, + &[ + 4, + frame, + *self + .carrier_borrow_ids + .get(&(function.function().index(), loan.index())) + .ok_or_else(|| pending("carrier EndBorrow has no exact activation row"))?, + ], + ), + CarrierRole::Observe { + source, + observation, + .. + } => { + words(&mut row, &[5, frame]); + row.extend(words_bytes(&self.borrowed_source(function, *source)?)); + match observation { + Observation::Length => { + path(&mut row, &[])?; + word(&mut row, 1); + } + Observation::Tag => { + path(&mut row, &[])?; + word(&mut row, 2); + } + Observation::State { .. } => { + path(&mut row, &[])?; + word(&mut row, 3); + } + Observation::Ordinary { path: selected, ty } => { + path(&mut row, selected)?; + let clone = match self.plan.types().resolve(*ty) { + Type::String => 1, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::Bool + | Type::Nothing => 0, + _ => { + return Err(pending( + "carrier ordinary observation needs its exact owning clone transport", + )); + } + }; + words(&mut row, &[4, self.shape(*ty)?, clone]); + } + } + } + CarrierRole::Extract { + source, + path: selected, + destination, + } => { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[6, frame, slot(self, root)?]); + path(&mut row, &full)?; + words(&mut row, &[1, slot(self, *destination)?]); + } + CarrierRole::ExtractOrdinary { + source, + path: selected, + ty, + .. + } => { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[6, frame, slot(self, root)?]); + path(&mut row, &full)?; + words(&mut row, &[2, self.shape(*ty)?]); + } + CarrierRole::AdaptSum { + source, + path: selected, + destination, + loan, + lease_frame, + } => { + words(&mut row, &[7, frame]); + if loan.is_some() { + row.extend(words_bytes(&self.borrowed_source(function, *source)?)); + path(&mut row, selected)?; + } else { + let (root, mut full) = self.owned_root(function, *source)?; + full.extend_from_slice(selected); + words(&mut row, &[1, slot(self, root)?]); + path(&mut row, &full)?; + } + words( + &mut row, + &[ + self.slot(function.function(), *destination)?, + self.frame(function.function(), *lease_frame)?, + u32::from(loan.is_some()), + ], + ); + } + CarrierRole::BeginIteration { + source, + destination, + .. + } => { + let CarrierValue::Owned { slot: source } = source else { + return Err(pending( + "consuming carrier loop requires its exact owned source", + )); + }; + words( + &mut row, + &[2, frame, slot(self, *source)?, slot(self, *destination)?], + ); + } + CarrierRole::RetireIteration { source, binders } => { + let mut carrier_binders = Vec::new(); + let mut leaf_binders = Vec::new(); + for binder in binders { + let mut carrier = carriers.slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == *binder)); + if let Some(selected) = carrier.next() { + if carrier.next().is_some() { + return Err(pending("iteration binder has ambiguous carrier storage")); + } + carrier_binders.push(slot(self, selected.id())?); + continue; + } + let mut leaf = function.owner_slots().iter().filter(|slot| matches!(slot.storage(), custody::ResourceSlotStorage::Local { header } if header.id == *binder)); + if let Some(selected) = leaf.next() { + if leaf.next().is_some() { + return Err(pending("iteration binder has ambiguous sum storage")); + } + leaf_binders.push(self.slot(function.function(), selected.id())?); + } + } + words( + &mut row, + &[ + 10, + frame, + slot(self, *source)?, + count(carrier_binders.len())?, + ], + ); + words(&mut row, &carrier_binders); + word(&mut row, count(leaf_binders.len())?); + words(&mut row, &leaf_binders); + } + CarrierRole::PublishReturn { source } => { + words(&mut row, &[9, frame, slot(self, *source)?]) + } + CarrierRole::Retire { source } => words(&mut row, &[8, frame, slot(self, *source)?]), + CarrierRole::ConstructorChild { .. } | CarrierRole::CommitConstructor { .. } => { + return Err(pending( + "constructor activation must alias its one Begin row", + )); + } + } + self.carrier_operations[record as usize] = row; + Ok(record) + } +} + +fn constructor_selector(constructor: Constructor) -> Result { + Ok(match constructor { + Constructor::List + | Constructor::Map + | Constructor::Struct + | Constructor::OptionalNone + | Constructor::ResultFail => 0, + Constructor::OptionalSome | Constructor::ResultOk => 1, + Constructor::Enum { variant } => variant.index(), + Constructor::Machine { state } => count(state)?, + }) +} +fn words_bytes(input: &[u32]) -> Vec { + let mut bytes = Vec::new(); + words(&mut bytes, input); + bytes +} +fn path(output: &mut Vec, input: &[Path]) -> Result<(), CodegenError> { + word(output, count(input.len())?); + for step in input { + match step { + Path::Field { field, .. } => words(output, &[1, field.index()]), + Path::Variant { variant, field, .. } => { + words(output, &[2, variant.index(), count(*field)?]) + } + Path::State { state, field, .. } => words(output, &[3, count(*state)?, count(*field)?]), + Path::Element { index } => { + word(output, 4); + selector(output, *index)?; + } + Path::MapKey { index } => { + word(output, 5); + selector(output, *index)?; + } + Path::MapValue { index } => { + word(output, 6); + selector(output, *index)?; + } + Path::OptionalSome => word(output, 7), + Path::ResultOk => word(output, 8), + Path::ResultFail => word(output, 9), + } + } + Ok(()) +} +fn selector( + output: &mut Vec, + index: custody::ResourceCarrierIndex, +) -> Result<(), CodegenError> { + match index { + custody::ResourceCarrierIndex::Constant(index) => words(output, &[0, count(index)?]), + custody::ResourceCarrierIndex::Local(_) + | custody::ResourceCarrierIndex::CurrentIteration { .. } => word(output, 1), + } + Ok(()) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs new file mode 100644 index 00000000..d3358783 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carrier_tests.rs @@ -0,0 +1,544 @@ +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_mir::{ResourceCarrierNode, ResourceCarrierOperationRole, ResourceCarrierProjectionPath}; +use object::{Object, ObjectSymbol}; + +include!("../resource_execution/tests/carrier_wrapper_sources.rs"); + +const WHOLE_SOURCE: &str = include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett" +); + +fn checked_wrapper(suffix: &str, release: bool) -> Arc { + checked_wrapper_source(WHOLE_SOURCE, suffix, release) +} + +fn checked_wrapper_source( + prefix: &str, + suffix: &str, + release: bool, +) -> Arc { + let mut parsed = parse(SUPPORT, FileId::new(10_000)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + let source = format!("{prefix}{suffix}"); + assert!(source.starts_with(prefix)); + let mut origins = HashMap::from([(FileId::new(10_000), SourceOrigin::Stdlib)]); + for (text, file, origin) in [ + (LIST_LENGTH, FileId::new(10_001), SourceOrigin::Stdlib), + (MAP_LENGTH, FileId::new(10_002), SourceOrigin::Stdlib), + (source.as_str(), FileId::new(0), SourceOrigin::Project), + ] { + let next = parse(text, file); + assert!(next.errors.is_empty(), "{:?}", next.errors); + parsed.module.items.extend(next.module.items); + origins.insert(file, origin); + } + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare(parsed, origins, &catalog, CheckOptions { release }) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +fn lowered_wrapper(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + assert!( + required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + ); + let types = &checked.checked().interner; + let mut hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + jett_hir::complete_value_conversions(&mut hir, types).unwrap(); + jett_mir::lower(&hir, types).unwrap() +} + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +#[test] +fn resource_carrier_whole_source17_all_fourteen_wrappers_emit_real_objects_in_both_profiles() { + for release in [false, true] { + for (name, suffix) in WRAPPERS { + let checked = checked_wrapper(suffix, release); + let types = &checked.checked().interner; + let program = lowered_wrapper(&checked); + // The unused refined formal and latent occupied constructors remain in + // the original source and in its authenticated closure for every row. + let refined = named(&program, "refined_shape_contract"); + assert_eq!(refined.params.len(), 1); + assert_eq!(refined.return_type, TypeInterner::NOTHING); + for constructor in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + ] { + named(&program, constructor); + } + let artifact = crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error:?}")); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let symbols = object + .symbols() + .filter_map(|symbol| { + symbol + .name() + .ok() + .map(|name| (name.to_owned(), symbol.is_definition())) + }) + .collect::>(); + assert!( + symbols + .iter() + .any(|(name, defined)| name == "jett_aot_resource_v1_manifest" && *defined) + ); + for leaf in [ + "carrier_construct_begin", + "carrier_child", + "carrier_commit", + "carrier_publish", + "source_enter", + "source_status", + "scope_complete", + ] { + assert!( + symbols.iter().any(|(name, defined)| name + == &format!("jett_rt_v1_resource_{leaf}") + && !*defined), + "{name}, release={release}, missing {leaf}: {symbols:?}" + ); + } + } + } +} + +#[test] +fn resource_carrier_v3_layout_retains_latent_shapes_refinement_and_exact_source_rows() { + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[6].1, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let first = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let second = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + assert_eq!(first.bytes(), second.bytes()); + assert_eq!( + u32::from_le_bytes(first.bytes()[8..12].try_into().unwrap()), + 3 + ); + assert_eq!( + u64::from_le_bytes(first.bytes()[16..24].try_into().unwrap()), + u64::try_from(first.bytes().len()).unwrap() + ); + assert!(first.bytes().windows(8).any(|bytes| bytes == b"JTCAR003")); + let mut rows = Rows::new(first.plan()).unwrap(); + rows.populate().unwrap(); + assert!(!rows.carrier_slots.is_empty()); + assert!(!rows.carrier_operations.is_empty()); + assert!(rows.shapes.iter().any(|row| row.first() == Some(&11))); + for tag in [2, 7, 8, 9, 10, 11] { + assert!( + rows.carrier_graph + .rows() + .iter() + .any(|row| u32::from_le_bytes(row[..4].try_into().unwrap()) == tag), + "retained node {tag}" + ); + } + let refined = first + .plan() + .function(named(&program, "refined_shape_contract").id) + .unwrap(); + let graph = refined.carriers().unwrap(); + assert!(graph.shapes().iter().any(|shape| matches!( + shape.node(), + ResourceCarrierNode::Refinement { .. } + ) && shape.refinement_predicate().is_some())); + // A carrier is never installed into the legacy leaf-owner table merely + // because its static graph contains a Resource in an inactive arm. + assert!( + rows.operations + .iter() + .any(|(_, row)| row.first() == Some(&26)) + ); + } +} + +#[test] +fn resource_carrier_prepared_map_keeps_key_before_value_reset_and_legacy_optional_adapter() { + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[3].1, release); + let types = &checked.checked().interner; + let mut program = lowered_wrapper(&checked); + jett_mir::prepare_native_sequences(&mut program, types); + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let function = layout.plan().function(entry(&program)).unwrap(); + let graph = function.carriers().unwrap(); + let [iteration] = graph.iterations() else { + panic!("one original map loop") + }; + assert!(iteration.cursor.is_some()); + let current = named(&program, "main"); + let gets = current.blocks[usize::try_from(iteration.body.index()).unwrap()] + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::SequenceGet { target, part, .. } => Some((*target, *part)), + _ => None, + }) + .collect::>(); + assert_eq!( + gets, + vec![ + (iteration.binders[0], jett_mir::SequencePart::Key), + (iteration.binders[1], jett_mir::SequencePart::Value) + ] + ); + assert!(graph.operations().iter().any(|operation| + matches!(operation.role(), ResourceCarrierOperationRole::RetireIteration { binders, .. } + if binders == &iteration.binders))); + assert!(graph.operations().iter().any(|operation| + matches!(operation.role(), ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::MapValue { .. }])))); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + assert!( + rows.carrier_operations + .iter() + .any(|row| u32::from_le_bytes(row[16..20].try_into().unwrap()) == 10) + ); + assert!( + rows.operations + .iter() + .any(|(_, row)| row.first() == Some(&16)), + "the ordinary Source body keeps its exact v2 formal tuple inside wire v3" + ); + } +} + +#[test] +fn resource_carrier_object_gate_rejects_public_return_header_and_unused_refined_formal_substitution() + { + let checked = checked_wrapper(WRAPPERS[0].1, false); + let types = &checked.checked().interner; + let program = lowered_wrapper(&checked); + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + // Clone a publicly available Function from a separately authenticated Source. + // Its private witness remains opaque; the receiving program keeps its own + // original archive and manifest. Both complete originals are valid alone. + let foreign_source = WHOLE_SOURCE.replacen("marker: 17)", "marker: 19)", 1); + assert_ne!(foreign_source, WHOLE_SOURCE); + let foreign_checked = checked_wrapper_source(&foreign_source, WRAPPERS[0].1, false); + let foreign_program = lowered_wrapper(&foreign_checked); + EmittedResourceLayout::from_program( + &foreign_program, + &foreign_checked.checked().interner, + entry(&foreign_program), + ) + .unwrap(); + let foreign_function = named(&foreign_program, "absent_envelope"); + let original_function = named(&program, "absent_envelope"); + assert_eq!(foreign_function.id, original_function.id); + assert_eq!(foreign_function.identity, original_function.identity); + assert_eq!(foreign_function.return_type, original_function.return_type); + assert_ne!(foreign_function.blocks, original_function.blocks); + for mutation in 0..3 { + let mut changed = program.clone(); + let map = named(&changed, "empty_map").return_type; + let envelope = named(&changed, "absent_envelope").return_type; + match mutation { + 0 => named_mut(&mut changed, "empty_tokens").return_type = map, + 1 => { + let function = named_mut(&mut changed, "refined_shape_contract"); + let local = function.params[0].local; + function.locals[usize::try_from(local.index()).unwrap()].ty = envelope; + } + 2 => { + *named_mut(&mut changed, "absent_envelope") = foreign_function.clone(); + let errors = jett_mir::validate_resource_ownership(&changed, types).unwrap_err(); + assert!( + errors.iter().any(|error| error.message + == "Resource ownership mixes foreign original/materialized archives"), + "a public Function clone cannot transplant its opaque archive: {errors:?}" + ); + } + _ => unreachable!(), + } + assert!( + EmittedResourceLayout::from_program(&changed, types, entry(&changed)).is_err(), + "fresh archive/type/CFG authentication, mutation {mutation}" + ); + assert!( + crate::emit::emit_for_triple( + &changed, + types, + HOST, + Some(entry(&changed)), + CodegenOptions { optimize: false } + ) + .is_err(), + "no object, mutation {mutation}" + ); + } +} + +#[test] +fn resource_carrier_source13_and14_keep_original_required_evaluation_and_bounds_control() { + assert!(WRAPPERS[12].1.contains("view values) != 1")); + assert!(WRAPPERS[12].1.contains("resource_probe.terminal_label()")); + assert!(WRAPPERS[13].1.contains("answer != 34")); + assert!(WRAPPERS[13].1.contains("absent_namespace_value != 17")); + for release in [false, true] { + let checked = checked_wrapper(WRAPPERS[13].1, release); + let program = lowered_wrapper(&checked); + assert_eq!( + named(&program, "absent_required_controls").return_type, + TypeInterner::INT64 + ); + crate::emit::emit_for_triple( + &program, + &checked.checked().interner, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + } + // Runtime status/message goldens for these two exact Sources remain in the + // full driver gate: wrong empty length reaches list.__remove_at bounds error; + // required controls return 34. Object tests do not claim native execution. +} + +#[test] +fn resource_carrier_observation_rows_distinguish_scalars_from_owned_strings_and_exact_adapter_imports() + { + for release in [false, true] { + let mut observed_scalar = false; + let mut observed_string = false; + let mut imported_adapt = false; + let mut imported_end = false; + for suffix in [WRAPPERS[6].1, WRAPPERS[10].1] { + let checked = checked_wrapper(suffix, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + for function in &program.functions { + let Some(carriers) = layout + .plan() + .function(function.id) + .and_then(|plan| plan.carriers()) + else { + continue; + }; + for operation in carriers.operations() { + let ResourceCarrierOperationRole::Observe { + observation: jett_mir::ResourceCarrierObservation::Ordinary { ty, .. }, + .. + } = operation.role() + else { + continue; + }; + let record = + rows.carrier_record_ids[&(function.id.index(), operation.id().index())]; + let row = &rows.carrier_operations[usize::try_from(record).unwrap()]; + let tail = &row[row.len() - 12..]; + assert_eq!(u32::from_le_bytes(tail[..4].try_into().unwrap()), 4); + let shape = usize::try_from(u32::from_le_bytes(tail[4..8].try_into().unwrap())) + .unwrap(); + let clone = u32::from_le_bytes(tail[8..12].try_into().unwrap()); + match types.resolve(*ty) { + Type::Int64 => { + assert_eq!(rows.shapes[shape][0], 1); + assert_eq!(clone, 0, "a scalar is never a NativeValues handle"); + observed_scalar = true; + } + Type::String => { + assert_eq!(rows.shapes[shape][0], 4); + assert_eq!( + clone, 1, + "a String observation needs independent ownership" + ); + observed_string = true; + } + _ => {} + } + } + } + let artifact = crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + for symbol in object.symbols().filter(|symbol| !symbol.is_definition()) { + match symbol.name().unwrap() { + "jett_rt_v1_resource_carrier_adapt" => imported_adapt = true, + "jett_rt_v1_resource_carrier_end" => imported_end = true, + "jett_rt_v1_resource_carrier_adapt_sum" + | "jett_rt_v1_resource_carrier_end_borrow" => { + panic!("native import must name the exact existing exported leaf") + } + _ => {} + } + } + } + assert!(observed_scalar && observed_string); + assert!(imported_adapt && imported_end); + } +} + +#[test] +fn resource_carrier_lexical_constructor_order_and_local_machine_qualification_emit_from_original_source() + { + let original = "TokenEnvelope(tokens: list(), fallback: none, marker: 17)"; + assert_eq!(WHOLE_SOURCE.matches(original).count(), 1); + let reordered = WHOLE_SOURCE.replacen( + original, + "TokenEnvelope(marker: 17, tokens: list(), fallback: none)", + 1, + ); + for release in [false, true] { + let checked = checked_wrapper_source(&reordered, WRAPPERS[6].1, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let plan = layout + .plan() + .function(named(&program, "absent_envelope").id) + .unwrap() + .carriers() + .unwrap(); + let storage_indices = plan + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::ConstructorChild { child, .. } => Some(child.index), + _ => None, + }) + .collect::>(); + assert_eq!( + storage_indices, + vec![2, 0, 1], + "declared storage order differs from lexical arrival 0,1,2" + ); + crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + + let suffix = format!( + "\nexport function source17_qualified_local() returns TokenState:\n TokenState value = TokenState(vacant, none, \"empty\")\n return value\n{}", + WRAPPERS[10].1 + ); + let checked = checked_wrapper(&suffix, release); + let program = lowered_wrapper(&checked); + let types = &checked.checked().interner; + let layout = EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let plan = layout + .plan() + .function(named(&program, "source17_qualified_local").id) + .unwrap() + .carriers() + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::QualifyMachine { .. } + ))); + crate::emit::emit_for_triple( + &program, + types, + HOST, + Some(entry(&program)), + CodegenOptions { optimize: release }, + ) + .unwrap(); + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs new file mode 100644 index 00000000..ae1a078a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/carriers.rs @@ -0,0 +1,287 @@ +//! V3 typed graph projection from a fresh constructor-owned carrier plan. +//! Serialized identities are consistency metadata, never Source/Scope authority. +use super::{BYTE_LIMIT, LIMIT, count, pending, word, words}; +use crate::CodegenError; +use jett_mir::{ResourceCarrierFunctionPlan, ResourceCarrierNode, ResourceCarrierShapeId}; +use jett_types::{TypeId, TypeInterner}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Default)] +pub(super) struct CarrierGraphRows { + rows: Vec>, + /// Canonical TypeIds are only keys within the already authenticated session. + ids: BTreeMap, + building: BTreeSet, +} + +impl CarrierGraphRows { + pub(super) fn is_empty(&self) -> bool { + self.rows.is_empty() + } + pub(super) fn len(&self) -> usize { + self.rows.len() + } + pub(super) fn rows(&self) -> &[Vec] { + &self.rows + } + pub(super) fn node_for_type(&self, ty: TypeId) -> Option { + self.ids.get(&ty.index()).copied() + } + + /// Project only nodes reachable from original carrier slots/formals/results. + /// Unrelated ordinary signature/never metadata does not acquire a carrier ABI. + pub(super) fn project( + &mut self, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + id: ResourceCarrierShapeId, + ordinary_shape: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result { + let shape = plan + .shape(id) + .ok_or_else(|| pending("carrier graph lost its exact node"))?; + let ty = shape.ty(); + if let Some(row) = self.ids.get(&ty.index()).copied() { + return Ok(row); + } + if self.rows.len() >= LIMIT || !self.building.insert(ty.index()) { + return Err(pending( + "carrier graph needs its exact finite recursive transport", + )); + } + let node = shape.node().clone(); + let mut data = Vec::new(); + let result = (|| { + match node { + ResourceCarrierNode::Ordinary => words(&mut data, &[1, ordinary_shape(ty)?]), + ResourceCarrierNode::Resource { kind } => words(&mut data, &[2, kind.id().index()]), + ResourceCarrierNode::Optional { payload } => { + let child = self.project(plan, types, payload, ordinary_shape, predicate)?; + words(&mut data, &[3, child]); + } + ResourceCarrierNode::Result { success, failure } => { + let ok = self.project(plan, types, success, ordinary_shape, predicate)?; + let fail = self.project(plan, types, failure, ordinary_shape, predicate)?; + words(&mut data, &[4, ok, fail]); + } + ResourceCarrierNode::List { element } => { + let child = self.project(plan, types, element, ordinary_shape, predicate)?; + words(&mut data, &[5, child]); + } + ResourceCarrierNode::Map { key, value } => { + let key = self.project(plan, types, key, ordinary_shape, predicate)?; + let value = self.project(plan, types, value, ordinary_shape, predicate)?; + words(&mut data, &[6, key, value]); + } + ResourceCarrierNode::Struct { + arguments, fields, .. + } => { + word(&mut data, 7); + identity(&mut data, ty, types)?; + self.arguments( + &mut data, + plan, + types, + &arguments, + ordinary_shape, + predicate, + )?; + self.fields(&mut data, plan, types, &fields, ordinary_shape, predicate)?; + } + ResourceCarrierNode::Enum { + arguments, + variants, + .. + } => { + word(&mut data, 8); + identity(&mut data, ty, types)?; + self.arguments( + &mut data, + plan, + types, + &arguments, + ordinary_shape, + predicate, + )?; + word(&mut data, count(variants.len())?); + for (index, variant) in variants.iter().enumerate() { + if variant.ordinal != index { + return Err(pending( + "carrier enum changed its original ordered variant", + )); + } + blob(&mut data, variant.name.as_bytes())?; + data.extend_from_slice(&variant.discriminant.to_le_bytes()); + self.fields( + &mut data, + plan, + types, + &variant.fields, + ordinary_shape, + predicate, + )?; + } + } + ResourceCarrierNode::Machine { + states, + transitions, + .. + } => { + word(&mut data, 9); + identity(&mut data, ty, types)?; + word(&mut data, count(states.len())?); + for (index, state) in states.iter().enumerate() { + if state.ordinal != index { + return Err(pending( + "carrier machine changed its original ordered state", + )); + } + blob(&mut data, state.name.as_bytes())?; + data.extend_from_slice( + &i64::try_from(index) + .map_err(|_| { + pending("carrier state ordinal exceeds checked tag width") + })? + .to_le_bytes(), + ); + self.fields( + &mut data, + plan, + types, + &state.fields, + ordinary_shape, + predicate, + )?; + } + word(&mut data, count(transitions.len())?); + for (source, target) in transitions { + words(&mut data, &[count(source)?, count(target)?]); + } + } + ResourceCarrierNode::MachineState { machine, state } => { + let owner = self.project(plan, types, machine, ordinary_shape, predicate)?; + words(&mut data, &[10, owner, count(state)?]); + } + ResourceCarrierNode::Refinement { base, .. } => { + let base = self.project(plan, types, base, ordinary_shape, predicate)?; + word(&mut data, 11); + identity(&mut data, ty, types)?; + word(&mut data, base); + // This callback selects the exact authenticated predicate + // association. A type name or numerically valid base is not proof. + let original = predicate(ty)?; + if original.is_empty() { + return Err(pending( + "carrier refinement lost its original predicate association", + )); + } + blob(&mut data, &original)?; + } + } + if data.len() > BYTE_LIMIT { + return Err(pending("carrier graph row exceeds wire capacity")); + } + Ok(()) + })(); + self.building.remove(&ty.index()); + result?; + let ordinal = count(self.rows.len())?; + self.rows.push(data); + self.ids.insert(ty.index(), ordinal); + Ok(ordinal) + } + + fn arguments( + &mut self, + out: &mut Vec, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + arguments: &[ResourceCarrierShapeId], + ordinary: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result<(), CodegenError> { + word(out, count(arguments.len())?); + for argument in arguments { + word( + out, + self.project(plan, types, *argument, ordinary, predicate)?, + ); + } + Ok(()) + } + + fn fields( + &mut self, + out: &mut Vec, + plan: &ResourceCarrierFunctionPlan, + types: &TypeInterner, + fields: &[jett_mir::ResourceCarrierField], + ordinary: &mut impl FnMut(TypeId) -> Result, + predicate: &mut impl FnMut(TypeId) -> Result, CodegenError>, + ) -> Result<(), CodegenError> { + word(out, count(fields.len())?); + for (index, field) in fields.iter().enumerate() { + if field.ordinal != index { + return Err(pending("carrier graph changed its original ordered field")); + } + blob(out, field.name.as_bytes())?; + word( + out, + self.project(plan, types, field.shape, ordinary, predicate)?, + ); + } + Ok(()) + } +} + +fn blob(out: &mut Vec, value: &[u8]) -> Result<(), CodegenError> { + if value.len() > LIMIT { + return Err(pending("carrier identity exceeds bounded wire blob domain")); + } + word(out, count(value.len())?); + out.extend_from_slice(value); + Ok(()) +} + +fn identity(out: &mut Vec, ty: TypeId, types: &TypeInterner) -> Result<(), CodegenError> { + // This stamp is emitted only from the authenticated graph. The runtime binds + // it to its installed node and issues constructor identities independently. + let mut bytes = ty.index().to_le_bytes().to_vec(); + bytes.extend_from_slice(types.type_name(ty).as_bytes()); + blob(out, &bytes) +} + +pub(super) fn append_extension( + out: &mut Vec, + graph: &CarrierGraphRows, + slots: &[Vec], + operations: &[Vec], +) -> Result<(), CodegenError> { + out.extend_from_slice(b"JTCAR003"); + words( + out, + &[ + count(graph.len())?, + count(slots.len())?, + count(operations.len())?, + 0, + ], + ); + for (index, node) in graph.rows().iter().enumerate() { + word(out, count(index)?); + out.extend_from_slice(node); + } + for (index, slot) in slots.iter().enumerate() { + word(out, count(index)?); + words(out, slot); + } + for (index, operation) in operations.iter().enumerate() { + word(out, count(index)?); + out.extend_from_slice(operation); + } + if out.len() > BYTE_LIMIT { + return Err(pending("carrier extension exceeds bounded wire capacity")); + } + Ok(()) +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett b/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/graph_shapes.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/graph_shapes.rs new file mode 100644 index 00000000..67945686 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/graph_shapes.rs @@ -0,0 +1,143 @@ +//! Ordinary native word representation selected only by protected Graph rows. +use super::*; + +#[path = "source_edge_locator.rs"] +mod source_edges; + +fn current_function<'p>( + plan: &custody::ResourceOwnershipPlan<'p>, + selected: &custody::ResourceFunctionPlan, +) -> Result<&'p Function, CodegenError> { + plan.program() + .functions + .get(selected.function().index() as usize) + .filter(|current| { + current.id == selected.function() + && ¤t.identity == selected.identity() + && current.params.as_slice() == selected.parameters() + && current.return_type == selected.return_type() + }) + .ok_or_else(|| pending("Graph shape has no exact current fresh function header")) +} + +impl Rows<'_, '_> { + fn graph_storage_shape( + &mut self, + proof: &custody::GraphStorageProof, + ty: TypeId, + ) -> Result { + if proof.ty() != ty { + return Err(pending("Graph shape proof changes its exact Source type")); + } + let kind = + crate::verify::native_storage_kind(self.plan.types(), ty, "Graph ordinary word")?; + if !matches!( + kind, + ScalarKind::Struct + | ScalarKind::Enum + | ScalarKind::Bitfield + | ScalarKind::Machine + | ScalarKind::Function + | ScalarKind::Actor + | ScalarKind::List + | ScalarKind::Set + | ScalarKind::Map + | ScalarKind::Sum + | ScalarKind::Construction + ) { + return self.shape(ty); + } + // This ordinary word grants no custody. Do not cache it by Source TypeId: + // another value of that type must select its own protected Graph proof. + let words = vec![1, 64, 0]; + if let Some(index) = self.shapes.iter().position(|shape| shape == &words) { + return ordinal(index); + } + let id = ordinal(self.shapes.len())?; + self.shapes.push(words); + Ok(id) + } + + pub(super) fn graph_function_result( + &mut self, + function: &custody::ResourceFunctionPlan, + ) -> Result, CodegenError> { + let current = current_function(self.plan, function)?; + custody::native_graph_storage_for_return(current, self.plan.types()) + .map(|proof| self.graph_storage_shape(&proof, function.return_type())) + .transpose() + } + + pub(super) fn function_signature_result( + &mut self, + function: &custody::ResourceFunctionPlan, + result: u32, + ) -> Result { + let current = current_function(self.plan, function)?; + let mut parameters = Vec::new(); + for param in ¤t.params { + let shape = + match custody::native_graph_storage_for_formal(current, self.plan.types(), param) { + Some(proof) => self.graph_storage_shape(&proof, param.ty)?, + None => self.shape(param.ty)?, + }; + parameters.push(( + shape, + if param.mode == jett_mir::ParamMode::View { + 2 + } else { + 1 + }, + )); + } + self.shaped_signature(parameters, result) + } + + pub(super) fn source_shapes( + &mut self, + caller: &custody::ResourceFunctionPlan, + operation: &custody::ResourceOperation, + callee: &custody::ResourceFunctionPlan, + param: &jett_mir::Param, + fact: &custody::ResourceCallFormal, + operand: &Operand, + order: &[usize], + ) -> Result<(u32, u32), CodegenError> { + let current = current_function(self.plan, callee)?; + let physical = current + .params + .get(fact.parameter()) + .filter(|physical| *physical == param) + .ok_or_else(|| pending("Graph Source formal is not the exact fresh header"))?; + let Some(formal) = + custody::native_graph_storage_for_formal(current, self.plan.types(), physical) + else { + return Ok((self.shape(fact.actual_type())?, self.shape(param.ty)?)); + }; + let edge = source_edges::source_edge( + current_function(self.plan, caller)?, + self.plan.types(), + caller, + operation, + callee.function(), + order, + fact.parameter(), + )? + .ok_or_else(|| pending("Graph Source shape lacks its exact whole-call edge proof"))?; + if !matches!(operand, Operand::Ordinary { parameter, ty } + if *parameter == fact.parameter() && *ty == fact.actual_type()) + || edge.parameter_index() != fact.parameter() + || edge.original_formal().ty != param.ty + || edge.original_formal().mode != param.mode + || edge.original_actual().ty != fact.actual_type() + { + return Err(pending( + "Graph Source edge changes its exact ordinary formal/actual tuple", + )); + } + Ok(( + self.graph_storage_shape(edge.storage(), fact.actual_type())?, + self.graph_storage_shape(&formal, param.ty)?, + )) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/native_suite.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/native_suite.rs new file mode 100644 index 00000000..eeba08fc --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/native_suite.rs @@ -0,0 +1,108 @@ +//! Factory-authenticated Verify/Property transitions use ordinary copy words. +//! The shared runtime activation row grants no Source call certificate. +use super::*; + +impl Rows<'_, '_> { + pub(super) fn native_suite_row( + &mut self, + caller: &custody::ResourceFunctionPlan, + frame: u32, + invocation: &custody::ResourceNativeSuiteInvocation, + ) -> Result, CodegenError> { + let target = invocation.function(); + let proof = invocation.proof(); + let formals = invocation.formals(); + let order = invocation.evaluation_order(); + let operands = invocation.operands(); + let result = invocation.result(); + let callee = self + .plan + .function(target) + .ok_or_else(|| pending("native suite target is outside its exact fresh family"))?; + if proof.owner() != caller.function() + || proof.target() != target + || proof.evaluation_order() != order + || proof.arguments().len() != formals.len() + || formals.len() != callee.parameters().len() + || order.len() != formals.len() + || operands.len() != formals.len() + || callee.provisional_return().is_some() + || callee.return_type() != TypeInterner::NOTHING + || !matches!(result, CallResult::Ordinary { ty } if ty == TypeInterner::NOTHING) + { + return Err(pending( + "native suite invocation changes its factory header or ordinary result", + )); + } + let scope = self.frame(target, callee.root_scope().id())?; + self.frames[scope as usize] + .parents + .push(Parent::Frame(frame)); + // Wire row 16 is only a runtime activation protocol. Its compiler + // authority remains the distinct InvokeNativeSuite receipt above. + let signature = self.function_signature(target)?; + let mut row = vec![16, frame, target.index(), signature, scope, 0]; + row.push(count(order.len())?); + for parameter in order { + row.push(count(*parameter)?); + } + row.push(count(formals.len())?); + for (parameter, param) in callee.parameters().iter().enumerate() { + let fact = formals + .get(parameter) + .filter(|fact| fact.parameter() == parameter) + .ok_or_else(|| pending("native suite formal is absent or reordered"))?; + let original = proof + .arguments() + .get(parameter) + .ok_or_else(|| pending("native suite has no exact generated argument"))?; + if param.mode != jett_mir::ParamMode::Owned + || fact.access() != jett_mir::ParamMode::Owned + || fact.effect() != custody::ResourceArgumentEffect::Copy + || fact.syntax() != custody::ResourceArgumentSyntax::Bare + || fact.actual_type() != param.ty + || fact.parameter_type() != param.ty + || original.ty != param.ty + || order.get(fact.source_index()) != Some(¶meter) + || !matches!(operands.get(parameter), + Some(Operand::Ordinary { parameter: actual, ty }) + if *actual == parameter && *ty == param.ty) + || !matches!( + self.plan.types().resolve(param.ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::Bool + | Type::String + | Type::Nothing + ) + { + return Err(pending( + "native suite formal is not its exact ordinary copy parameter", + )); + } + // No Graph Source locator or Resource carrier adaptation participates + // in this generated bridge. Both ordinary types must be exact. + let shape = self.shape(param.ty)?; + row.extend([ + count(parameter)?, + count(fact.source_index())?, + shape, + shape, + 1, // bare syntax + 1, // copy effect + 1, // owned formal access + 1, // ordinary value + ]); + } + row.extend([1, self.signatures[signature as usize].result]); + Ok(row) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs new file mode 100644 index 00000000..ff4ad9ad --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/returned_hooks_tests.rs @@ -0,0 +1,217 @@ +use super::*; +const FACTORY: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" +); +const BORROW: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" +); +const IMMEDIATE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" +); +const ORDER: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" +); +const CALLEE_FAILURE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" +); +const ACTUAL_FAILURE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" +); + +const CLOSE: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett" +); +const ALIAS: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett" +); +const RELAY: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett" +); +const UNUSED: &str = include_str!( + "../../../../jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett" +); + +#[test] +fn resource_returned_hook_layout_joins_return_shape_and_distinct_indirect_close_target() { + for release in [false, true] { + for source in [ + CLOSE, + ALIAS, + RELAY, + UNUSED, + FACTORY, + BORROW, + IMMEDIATE, + ORDER, + CALLEE_FAILURE, + ACTUAL_FAILURE, + ] { + let checked = checked(source, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let layout = + EmittedResourceLayout::from_program(&program, types, entry(&program)).unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut indirect = 0; + for function in layout.plan().functions() { + if let Some(hook) = function.descriptor_return() { + let signature = rows.function_signature(function.function()).unwrap(); + let result = rows.signatures[signature as usize].result; + let hook_id = rows.hook(hook).unwrap(); + assert_eq!(rows.shapes[result as usize], [10, hook_id]); + assert_eq!(function.return_type(), hook.function_type()); + assert!(function.provisional_return().is_none()); + } + for operation in function.operations() { + let Some(hook) = operation.indirect_hook_target() else { + continue; + }; + indirect += 1; + let ordinal = layout + .operation(function.function(), operation.id()) + .unwrap(); + let row = &rows.operations[ordinal as usize].1; + assert_eq!(row.len(), 4); + assert_eq!(row[0], 15); + assert_eq!(&rows.hook_ids[row[1] as usize], hook); + assert_eq!(row[2], rows.hooks[row[1] as usize].2); + assert_ne!( + row[3], ordinal, + "descriptor row cannot replace its physical target" + ); + let target = &rows.operations[row[3] as usize]; + assert_eq!(target.0, operation.site()); + assert_eq!( + target.1[0], + match hook.recipe() { + ResourceKernelRecipe::NetworkFactory => 1, + ResourceKernelRecipe::NetworkBorrow => 6, + ResourceKernelRecipe::Finalize => 7, + } + ); + assert_eq!( + target.1[1], + layout + .frame(function.function(), operation.frame()) + .unwrap() + ); + assert_eq!(target.1[2], row[1]); + let Role::InvokeHook { source, .. } = operation.role() else { + panic!("hook role"); + }; + assert_eq!( + source.target, + jett_hir::CallTarget::Indirect { + signature_type: hook.function_type() + } + ); + assert_eq!(source.bridge, jett_hir::CallBridge::Direct); + } + } + assert_eq!(indirect, usize::from(source != UNUSED)); + } + } +} + +#[test] +fn resource_returned_hook_layout_refuses_current_producer_signature_target_and_source_frame_substitution() + { + for release in [false, true] { + let checked = checked(CLOSE, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let selected = entry(&program); + let descriptor = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "descriptor") + .unwrap() + .id; + for mutation in 0..6 { + let mut changed = program.clone(); + match mutation { + 0 => { + let function = &mut changed.functions[descriptor.index() as usize]; + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = ExpressionKind::ResourceHookValue { + hook: program + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkBorrow) + .unwrap() + .clone(), + }; + } + 1 => { + changed.functions[descriptor.index() as usize].return_type = TypeInterner::INT64 + } + 2 => { + let function = &mut changed.functions[descriptor.index() as usize]; + function.identity.declaration.name.push_str("Foreign"); + } + 3 => { + let function = &mut changed.functions[selected.index() as usize]; + let mut found = false; + for block in &mut function.blocks { + for statement in &mut block.statements { + if let StatementKind::Evaluate(Expression { + kind: + ExpressionKind::IndirectCall { + ownership: jett_hir::CallOwnership::Source(source), + .. + }, + .. + }) = &mut statement.kind + { + source.target = jett_hir::CallTarget::Indirect { + signature_type: TypeInterner::INT64, + }; + found = true; + } + } + } + assert!(found, "authentic original Indirect site must be mutated"); + } + 4 => { + changed.functions[selected.index() as usize].params[0].ty = TypeInterner::INT64 + } + 5 => { + let function = &mut changed.functions[descriptor.index() as usize]; + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = ExpressionKind::Nothing; + } + _ => unreachable!(), + } + assert!( + matches!( + EmittedResourceLayout::from_program(&changed, types, selected), + Err(CodegenError::InvalidMir(_)) + ), + "mutation {mutation}" + ); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/source_edge_locator.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/source_edge_locator.rs new file mode 100644 index 00000000..78e6a56a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/source_edge_locator.rs @@ -0,0 +1,409 @@ +//! Locate an authenticated ordinary Graph operand at its exact current Source call. +//! This module is installed below resource_layout::graph_shapes. +use super::*; +use jett_hir as hir; +use jett_mir::{GraphSourceEdgeProof, ResourceFunctionPlan, ResourceOperation}; + +pub(super) fn source_edge( + function: &Function, + types: &TypeInterner, + caller: &ResourceFunctionPlan, + operation: &ResourceOperation, + target: FunctionId, + order: &[usize], + parameter: usize, +) -> Result, CodegenError> { + let (expected_target, source, formals, expected_order) = match operation.role() { + custody::ResourceOperationRole::InvokeSourceFunction { + function, + source, + formals, + evaluation_order, + .. + } + | custody::ResourceOperationRole::BeginSourceFunction { + function, + source, + formals, + evaluation_order, + .. + } => (*function, source, formals, evaluation_order), + _ => { + return Err(pending( + "Graph Source edge requires a Source function operation", + )); + } + }; + let site = operation.site(); + if caller.function() != function.id + || site.function() != function.id + || !caller + .operations() + .iter() + .any(|row| std::ptr::eq(row, operation)) + || expected_target != target + || expected_order != order + || parameter >= formals.len() + { + return Err(pending( + "Graph Source edge operation has no exact caller/target tuple", + )); + } + // Staged ResourceCall regions retain an archive, not a physical current Call. + // That archive cannot establish an ordinary Graph edge occurrence. + if !operation.is_expression_operation() { + return Ok(None); + } + let mut blocks = function + .blocks + .iter() + .filter(|block| block.id == site.block()); + let block = blocks + .next() + .ok_or_else(|| pending("Graph Source edge site block is absent"))?; + if blocks.next().is_some() { + return Err(pending("Graph Source edge site block is ambiguous")); + } + let mut walk = Walk { + pending: Vec::new(), + queued: 0, + }; + match site.position() { + custody::ResourcePosition::Statement(index) => { + let statement = block + .statements + .get(index) + .ok_or_else(|| pending("Graph Source edge statement is absent"))?; + walk.mir_statement(statement)?; + } + custody::ResourcePosition::Terminator => walk.terminator(&block.terminator)?, + } + let mut selected = None; + while let Some(work) = walk.pending.pop() { + match work { + Work::Expression(expression) => { + if caller + .operations_for_expression(expression) + .any(|row| std::ptr::eq(row, operation)) + { + if selected.is_some() { + return Err(pending("Graph Source edge has ambiguous current calls")); + } + let ExpressionKind::Call { + function: actual_target, + args, + evaluation_order, + ownership: hir::CallOwnership::Source(actual_source), + .. + } = &expression.kind + else { + return Err(pending( + "Graph Source edge operation lost its physical Source Call", + )); + }; + if *actual_target != target + || actual_source != source + || evaluation_order != order + || args.len() != formals.len() + || source.arguments.len() != formals.len() + || args.len() > LIMIT + || order.len() != args.len() + { + return Err(pending( + "Graph Source edge current Call changed its sealed tuple", + )); + } + let mut seen = vec![false; args.len()]; + for index in order { + let entry = seen.get_mut(*index).ok_or_else(|| { + pending("Graph Source edge argument order is out of range") + })?; + if *entry { + return Err(pending("Graph Source edge argument order is repeated")); + } + *entry = true; + } + for (index, ((argument, formal), actual)) in + source.arguments.iter().zip(formals).zip(args).enumerate() + { + if argument.parameter_index != index + || formal.parameter() != index + || argument.source_index != formal.source_index() + || argument.actual_type != formal.actual_type() + || argument.parameter_type != formal.parameter_type() + || actual.ty != formal.actual_type() + { + return Err(pending( + "Graph Source edge current Call changed a formal/actual tuple", + )); + } + } + selected = Some(expression); + } + walk.expression(expression)?; + } + Work::Block(block) => { + for statement in &block.statements { + walk.push(Work::HirStatement(statement))?; + } + } + Work::HirStatement(statement) => walk.hir_statement(statement)?, + } + } + let expression = + selected.ok_or_else(|| pending("Graph Source edge has no exact current Call"))?; + Ok(custody::native_graph_source_edge( + function, types, expression, parameter, + )) +} + +enum Work<'a> { + Expression(&'a Expression), + Block(&'a hir::Block), + HirStatement(&'a hir::Statement), +} +struct Walk<'a> { + pending: Vec>, + queued: usize, +} +impl<'a> Walk<'a> { + fn push(&mut self, work: Work<'a>) -> Result<(), CodegenError> { + if self.queued >= LIMIT { + return Err(pending( + "Graph Source edge current subtree exceeds the bounded row domain", + )); + } + self.queued += 1; + self.pending.push(work); + Ok(()) + } + fn value(&mut self, value: &'a Expression) -> Result<(), CodegenError> { + self.push(Work::Expression(value)) + } + fn optional(&mut self, value: &'a Option) -> Result<(), CodegenError> { + if let Some(value) = value { + self.value(value)?; + } + Ok(()) + } + fn values(&mut self, values: &'a [Expression]) -> Result<(), CodegenError> { + for value in values { + self.value(value)?; + } + Ok(()) + } + fn mir_statement(&mut self, statement: &'a jett_mir::Statement) -> Result<(), CodegenError> { + match &statement.kind { + StatementKind::ResourceCall(custody::ResourceCallNode::Stage { value, .. }) + | StatementKind::ResourceConstructor(custody::ResourceConstructorNode::Stage { + value, + .. + }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => self.value(value)?, + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + self.value(target)?; + self.value(value)?; + } + StatementKind::Assert { condition, message } => { + self.value(condition)?; + self.optional(message)?; + } + StatementKind::Breakpoint { condition, .. } => self.optional(condition)?, + StatementKind::ResourceCall( + custody::ResourceCallNode::Begin { .. } + | custody::ResourceCallNode::Invoke { .. } + | custody::ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + custody::ResourceConstructorNode::Begin { .. } + | custody::ResourceConstructorNode::Commit { .. } + | custody::ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + Ok(()) + } + fn terminator(&mut self, terminator: &'a jett_mir::Terminator) -> Result<(), CodegenError> { + match &terminator.kind { + TerminatorKind::Return(value) => self.optional(value)?, + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => self.value(value)?, + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } + Ok(()) + } + fn hir_statement(&mut self, statement: &'a hir::Statement) -> Result<(), CodegenError> { + match &statement.kind { + hir::StatementKind::Let { value, .. } + | hir::StatementKind::HandleDefault(value) + | hir::StatementKind::Expression(value) + | hir::StatementKind::Respond(value) => self.value(value)?, + hir::StatementKind::Assign { target, value } + | hir::StatementKind::UpdateField { target, value, .. } => { + self.value(target)?; + self.value(value)?; + } + hir::StatementKind::Return(value) => self.optional(value)?, + hir::StatementKind::If { + condition, + then_block, + else_block, + } => { + self.value(condition)?; + self.push(Work::Block(then_block))?; + if let Some(block) = else_block { + self.push(Work::Block(block))?; + } + } + hir::StatementKind::While { condition, body } => { + self.value(condition)?; + self.push(Work::Block(body))?; + } + hir::StatementKind::For { iterable, body, .. } => { + self.value(iterable)?; + self.push(Work::Block(body))?; + } + hir::StatementKind::Match { scrutinee, arms } => { + self.value(scrutinee)?; + for arm in arms { + self.push(Work::Block(&arm.body))?; + } + } + hir::StatementKind::Assert { condition, message } => { + self.value(condition)?; + self.optional(message)?; + } + hir::StatementKind::Breakpoint { condition, .. } => self.optional(condition)?, + hir::StatementKind::Scope(block) => self.push(Work::Block(block))?, + hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.value(type_info)?; + for arm in arms { + self.push(Work::Block(&arm.body))?; + } + } + hir::StatementKind::Break + | hir::StatementKind::Continue + | hir::StatementKind::Trace(_) => {} + } + Ok(()) + } + fn expression(&mut self, expression: &'a Expression) -> Result<(), CodegenError> { + // Exhaustive over current MIR-carried expressions. ActorGraphImport's + // factory archive is intentionally not a current physical subtree. + match &expression.kind { + ExpressionKind::Binary { left, right, .. } => { + self.value(left)?; + self.value(right)?; + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::StateIs { value, .. } + | ExpressionKind::Field { base: value, .. } => self.value(value)?, + ExpressionKind::ActorGraphStep { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } + | ExpressionKind::Call { args, .. } + | ExpressionKind::Intrinsic { args, .. } + | ExpressionKind::ActorSpawn { args, .. } => self.values(args)?, + ExpressionKind::IndirectCall { callee, args, .. } => { + self.value(callee)?; + self.values(args)?; + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::BitfieldConstruct { fields, .. } => self.values(fields)?, + ExpressionKind::MachineConstruct { payloads, .. } + | ExpressionKind::EnumConstruct { payloads, .. } => self.values(payloads)?, + ExpressionKind::MachineTransition { + source, payloads, .. + } => { + self.value(source)?; + self.values(payloads)?; + } + ExpressionKind::ListConstruct { elements } => self.values(elements)?, + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.value(&entry.key)?; + self.value(&entry.value)?; + } + } + ExpressionKind::Handle { + target, failure, .. + } => { + self.value(target)?; + self.push(Work::Block(failure))?; + } + ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.value(value)?; + } + } + } + ExpressionKind::InlineFunction { body, .. } => self.push(Work::Block(body))?, + ExpressionKind::ActorMessage { actor, args, .. } => { + self.value(actor)?; + self.values(args)?; + } + ExpressionKind::ActorGraphImport(_) + | ExpressionKind::ResourceHookValue { .. } + | ExpressionKind::FunctionRef(_) + | ExpressionKind::ClosureRef { .. } + | ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::Local(_) + | ExpressionKind::OptionalNone => {} + } + Ok(()) + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs new file mode 100644 index 00000000..5224522f --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/sum_views_tests.rs @@ -0,0 +1,84 @@ +use super::*; +#[path = "../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; + +#[test] +fn resource_borrowed_sum_layout_retains_exact_incoming_shape_and_projection_issuer() { + let cases = cases::CASES + .iter() + .filter(|case| { + case.name.starts_with("view_optional_") || case.name.starts_with("view_result_") + }) + .collect::>(); + assert_eq!(cases.len(), 12); + for release in [false, true] { + for case in &cases { + let checked = checked(case.source, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap_or_else(|error| panic!("{} release={release}: {error}", case.name)); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let helper = layout + .plan() + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name.starts_with("observe_") + }) + .unwrap(); + let (parameter, header) = helper + .parameters() + .iter() + .enumerate() + .find(|(_, header)| header.name == "outcome") + .unwrap(); + assert_eq!(header.mode, jett_mir::ParamMode::View); + assert!(helper.owner_slots().iter().all(|slot| + !matches!(slot.storage(),custody::ResourceSlotStorage::Local { header: stored } if stored.id == header.local))); + let incoming = helper.loans().iter().find(|loan| + matches!(loan.source(),custody::ResourceLoanSource::IncomingViewFormal { parameter: original, .. } if original == parameter)).unwrap(); + assert!(!matches!( + incoming.shape(), + custody::ResourceShape::Plain { .. } + )); + let projections = helper + .operations() + .iter() + .filter(|operation| matches!(operation.role(), Role::ProjectSumView { .. })) + .collect::>(); + assert_eq!(projections.len(), 1, "{} release={release}", case.name); + let projection = projections[0]; + let Role::ProjectSumView { + source, + destination, + path, + .. + } = projection.role() + else { + unreachable!() + }; + assert_eq!(*source, incoming.id()); + assert!(matches!(helper.loans()[destination.index()].source(), + custody::ResourceLoanSource::ProjectedSumPayload { parent, path: selected, .. } + if parent == *source && selected == *path)); + let row_id = layout + .operation(helper.function(), projection.id()) + .unwrap(); + assert_eq!(rows.loan(helper, *destination).unwrap(), [3, row_id]); + let row = &rows.operations[row_id as usize].1; + assert_eq!(row[0], 23); + assert_eq!(row[2], 2); + assert_eq!(row[4], parameter as u32); + assert!(helper.operations().iter().any(|operation| + matches!(operation.role(),Role::ObserveSumView { source: observed, .. } if observed == source))); + assert!(helper.operations().iter().all(|operation| + !matches!(operation.role(),Role::EndSumBorrow { loan } if *loan == incoming.id()))); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs b/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs new file mode 100644 index 00000000..2d9c3be3 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/resource_layout/tests.rs @@ -0,0 +1,677 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; +#[path = "carrier_tests.rs"] +mod carrier_tests; +#[path = "../resource_execution/replacement/layout_sums_tests.rs"] +mod replacement_sums; +#[path = "returned_hooks_tests.rs"] +mod returned_hooks; +#[path = "sum_views_tests.rs"] +mod sum_views; +const SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +fn checked(source: &str, release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +const SOURCE: &str = include_str!("connected_entry.jett"); +fn lower_source(checked: &Arc) -> Program { + let hir = jett_hir::lower_checked_resource_program(checked).unwrap(); + jett_mir::lower(&hir, &checked.checked().interner).unwrap() +} +fn entry(program: &Program) -> FunctionId { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() + .id +} +#[test] +fn resource_emitted_layout_joins_actual_driver_identity_and_unused_hooks_deterministically() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&program); + let first = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let second = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + assert_eq!(first.entry(), second.entry()); + assert_eq!(first.bytes(), second.bytes()); + assert_eq!(&first.bytes()[..8], b"JTRSC001"); + assert_eq!( + u32::from_le_bytes(first.bytes()[8..12].try_into().unwrap()), + 2 + ); + assert_eq!( + u64::from_le_bytes(first.bytes()[16..24].try_into().unwrap()), + first.bytes().len() as u64 + ); + assert_eq!(first.entry().function, selected.index()); + let mut rows = Rows::new(first.plan()).unwrap(); + rows.populate().unwrap(); + assert_eq!(rows.hooks.len(), 3); + assert_eq!( + rows.hook_ids.len(), + program.resource_manifest.hooks().count() + ); + for function in first.plan().functions() { + let signature = first.function_signature(function.function()).unwrap(); + for frame in function.frames() { + assert_eq!( + rows.frames[first.frame(function.function(), frame.id()).unwrap() as usize] + .signature, + signature + ); + } + } + let wrong = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pass_owner" + }) + .unwrap(); + let error = EmittedResourceLayout::from_program(&program, types, wrong.id).unwrap_err(); + match error { + CodegenError::InvalidMir(errors) => assert!(errors.iter().any(|error| { + error.span == wrong.span + && error.message + == "native Resource entry changed its closed Network/Nothing Source header" + })), + other => panic!("wrong selected entry used an unexpected refusal boundary: {other}"), + } + } +} +#[test] +fn resource_emitted_layout_keeps_call_site_result_destinations_separate_from_callee_return() { + let source = SOURCE.replace("result[int64, string] observed = resource_probe.borrow(view net, view moved)", "resource_probe.TestHandle final = pass_owner(moved)\n result[int64, string] observed = resource_probe.borrow(view net, view final)").replace("resource_probe.close(moved)", "resource_probe.close(final)"); + for release in [false, true] { + let checked = checked(&source, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let helper = layout + .plan() + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == "pass_owner" + }) + .unwrap(); + let mut calls = Vec::new(); + for function in layout.plan().functions() { + for operation in function.operations() { + if let Role::InvokeSourceFunction { + function: target, + result: CallResult::Owned { slot }, + .. + } = operation.role() + && *target == helper.function() + { + calls.push(( + layout + .operation(function.function(), operation.id()) + .unwrap(), + layout.slot(function.function(), *slot).unwrap(), + helper.provisional_return().unwrap().id(), + )); + assert_ne!( + function.owner_slots()[slot.index()].frame(), + operation.frame() + ); + } + } + } + assert_eq!(calls.len(), 2); + assert_ne!(calls[0].0, calls[1].0); + assert_ne!(calls[0].1, calls[1].1); + assert_eq!(calls[0].2, calls[1].2); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let scope = layout + .frame(helper.function(), helper.root_scope().id()) + .unwrap(); + for operation in helper.operations() { + if let Role::Transfer { + source, + destination, + } = operation.role() + && matches!( + helper.owner_slots()[destination.index()].storage(), + custody::ResourceSlotStorage::Return { .. } + ) + { + let id = layout.operation(helper.function(), operation.id()).unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [ + 2, + scope, + layout.slot(helper.function(), *source).unwrap(), + layout.slot(helper.function(), *destination).unwrap() + ] + ); + assert_eq!(operation.frame(), helper.provisional_return().unwrap().id()); + assert_eq!( + helper.owner_slots()[destination.index()].frame(), + operation.frame() + ); + } + if let Role::CompleteReturnAfterCleanup { source } = operation.role() { + let id = layout.operation(helper.function(), operation.id()).unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [18, scope, layout.slot(helper.function(), *source).unwrap()] + ); + assert_ne!( + layout.frame(helper.function(), operation.frame()).unwrap(), + scope + ); + assert_eq!( + helper.owner_slots()[source.index()].frame(), + helper.provisional_return().unwrap().id() + ); + } + } + } +} +#[test] +fn resource_emitted_layout_refuses_changed_current_source_graph_before_projecting_rows() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lower_source(&checked); + let selected = entry(&program); + for mutation in 0..3 { + let mut changed = program.clone(); + let function = &mut changed.functions[selected.index() as usize]; + match mutation { + 0 => function.params[0].mode = jett_mir::ParamMode::View, + 1 => function.identity.declaration.name.push_str("Foreign"), + 2 => function.blocks[function.entry.index() as usize] + .statements + .clear(), + _ => unreachable!(), + } + let error = EmittedResourceLayout::from_program(&changed, types, selected).unwrap_err(); + assert!(matches!(error, CodegenError::InvalidMir(_)), "{error:?}"); + } + } +} + +#[test] +fn resource_manifest_accessor_emits_only_the_immutable_compiler_owned_projection() { + use object::{Object, ObjectSymbol}; + let checked = checked(SOURCE, false); + let program = lower_source(&checked); + let layout = + EmittedResourceLayout::from_program(&program, &checked.checked().interner, entry(&program)) + .unwrap(); + let target = isa::lookup(HOST) + .unwrap() + .finish(settings::Flags::new(settings::builder())) + .unwrap(); + let object = ObjectBuilder::new( + target, + "resource_manifest_accessor", + default_libcall_names(), + ) + .unwrap(); + let mut module = ObjectModule::new(object); + layout.define_accessor(&mut module).unwrap(); + assert!(matches!( + layout.define_accessor(&mut module), + Err(CodegenError::DuplicateSymbol(_)) + )); + let bytes = module.finish().emit().unwrap(); + let object = object::File::parse(bytes.as_slice()).unwrap(); + assert!(object.symbols().any(|symbol| symbol.is_definition() + && symbol.name().ok() == Some("jett_aot_resource_v1_manifest"))); + assert!(!object.symbols().any(|symbol| symbol.is_definition() + && symbol.name().ok() == Some(super::super::JETT_AOT_ENTRY_SYMBOL_V1))); + // This control emits metadata/accessor only: no body, leaf or provider execution. +} + +#[test] +fn resource_layout_replacement_uses_exact_rhs_tag12_and_reseat_has_no_row() { + const SOURCE: &str = include_str!("../resource_execution/replacement/mutable_assignment.jett"); + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut replacements = 0; + let mut reseats = 0; + for function in layout.plan().functions() { + for operation in function.operations() { + match operation.role() { + Role::Replace { + destination, + replacement, + old, + } => { + replacements += 1; + assert_eq!(*old, custody::ResourceOccupancy::Occupied); + assert_ne!(destination, replacement); + let ordinal = layout + .operation(function.function(), operation.id()) + .unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1, + [ + 12, + layout + .frame(function.function(), operation.frame()) + .unwrap(), + layout.slot(function.function(), *destination).unwrap(), + layout.slot(function.function(), *replacement).unwrap(), + ] + ); + } + Role::SelfRebind { .. } => { + reseats += 1; + assert!( + layout + .operation(function.function(), operation.id()) + .is_none() + ); + } + _ => {} + } + } + } + assert!(replacements >= 2); + assert_eq!(reseats, 1); + } +} + +#[test] +fn resource_layout_original_optional_assignment_uses_owned_sum_replacement() { + const SOURCE: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = resource_probe.empty() + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lower_source(&checked); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + entry(&program), + ) + .unwrap(); + let function = layout.plan().function(entry(&program)).unwrap(); + let replacements = function + .operations() + .iter() + .filter(|operation| matches!(operation.role(), Role::Replace { .. })) + .collect::>(); + assert_eq!(replacements.len(), 1); + let Role::Replace { + destination, + replacement, + old, + } = replacements[0].role() + else { + unreachable!(); + }; + assert_eq!(*old, custody::ResourceOccupancy::Empty); + assert!(matches!( + function.owner_slots()[destination.index()].shape(), + custody::ResourceShape::Optional { .. } + )); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let ordinal = layout + .operation(function.function(), replacements[0].id()) + .unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1, + [ + 12, + layout + .frame(function.function(), replacements[0].frame()) + .unwrap(), + layout.slot(function.function(), *destination).unwrap(), + layout.slot(function.function(), *replacement).unwrap(), + ] + ); + } +} + +const STAGED_SOURCES: &[&str] = &[ + include_str!("../resource_execution/staged_call/09_later_handle_minimal.jett"), + include_str!("../resource_execution/staged_call/10_later_handle_original.jett"), + include_str!("../resource_execution/staged_call/11_later_handle_success.jett"), + include_str!("../resource_execution/staged_call/12_later_handle_written_view.jett"), + include_str!("../resource_execution/staged_call/13_first_handle_failure.jett"), + include_str!("../resource_execution/staged_call/14_first_handle_success.jett"), +]; + +#[test] +fn resource_staged_layout_keeps_complete_source_tuple_and_exact_activation_aliases() { + for release in [false, true] { + for &source in STAGED_SOURCES { + let checked = checked(source, release); + let program = lower_source(&checked); + let types = &checked.checked().interner; + let selected = entry(&program); + let staged_target = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "accept_staged" + }) + .unwrap() + .id; + let layout = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + let repeated = EmittedResourceLayout::from_program(&program, types, selected).unwrap(); + assert_eq!(layout.bytes(), repeated.bytes()); + assert_eq!( + u32::from_le_bytes(layout.bytes()[8..12].try_into().unwrap()), + 2 + ); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut regions = 0; + let mut aliases = 0; + let mut abandoned = 0; + for function in layout.plan().functions() { + for operation in function.operations() { + if let Role::BeginSourceFunction { + function: target, + formals, + evaluation_order, + operands, + .. + } = operation.role() + { + // Ordinary Resource-family calls now have their own exact + // regions too. Preserve every original mixed-call assertion + // for this fixture's checked accept_staged target. + if *target == staged_target { + regions += 1; + assert_eq!(evaluation_order, &[1, 0, 2]); + assert_eq!(formals.len(), 3); + assert_eq!(operands.len(), 3); + } + let id = layout + .operation(function.function(), operation.id()) + .unwrap(); + let row = &rows.operations[id as usize].1; + assert_eq!(row[0], 16); + assert_eq!(row[2], target.index()); + assert_eq!(row[3], layout.function_signature(*target).unwrap()); + assert_eq!( + rows.frames[row[1] as usize].signature, + layout.function_signature(function.function()).unwrap() + ); + // Full formal metadata exists on Begin before any reached Stage/Invoke. + for operand in operands { + if let Operand::Borrowed { parameter, loan } = operand { + let record = &function.loans()[loan.index()]; + if let custody::ResourceLoanSource::Owner(_) = record.source() { + assert_eq!(record.parameter(), Some(*parameter)); + let mut prepared = function.operations().iter().filter(|candidate| + matches!(candidate.role(), Role::PrepareSourceBorrow { loan: expected, parameter: p } if expected == loan && p == parameter)); + let prepared = prepared.next().unwrap(); + let borrow_id = layout + .operation(function.function(), prepared.id()) + .unwrap(); + assert_eq!(rows.operations[borrow_id as usize].1[0], 3); + assert_eq!(prepared.site(), operation.site()); + } + } + } + } + if let Some(prepared) = staged_alias(function, operation).unwrap() { + aliases += 1; + assert_eq!( + layout.operation(function.function(), operation.id()), + layout.operation(function.function(), prepared.id()) + ); + } + if matches!(operation.role(), Role::StageSourceActual { .. }) { + assert!( + layout + .operation(function.function(), operation.id()) + .is_none() + ); + } + if matches!( + operation.role(), + Role::Complete { + outcome: custody::ResourceCompletion::Abort + } + ) && function.operations().iter().any(|begin| { + begin.frame() == operation.frame() + && matches!(begin.role(), Role::BeginSourceFunction { .. }) + }) { + abandoned += 1; + let id = layout + .operation(function.function(), operation.id()) + .unwrap(); + assert_eq!( + rows.operations[id as usize].1, + [ + 13, + layout + .frame(function.function(), operation.frame()) + .unwrap() + ] + ); + } + } + } + assert_eq!(regions, 1); + assert!(aliases >= 2); + assert!(abandoned >= 1); + } + } +} + +#[test] +fn resource_staged_layout_refuses_edited_and_copied_public_nodes_before_rows() { + for release in [false, true] { + let checked = checked(STAGED_SOURCES[0], release); + let original = lower_source(&checked); + let selected = entry(&original); + let stages = original + .functions + .iter() + .flat_map(|function| { + function.blocks.iter().flat_map(move |block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + matches!( + statement.kind, + StatementKind::ResourceCall( + custody::ResourceCallNode::Stage { .. } + ) + ) + .then_some((function.id, block.id, index)) + }) + }) + }) + .collect::>(); + assert!(stages.len() >= 3); + for mutation in 0..5 { + let mut changed = original.clone(); + let (function, block, index) = stages[0]; + let current = &mut changed.functions[function.index() as usize]; + match mutation { + 0 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + source_index, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source_index += 1; + } + 1 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + parameter, + .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *parameter = 0; + } + 2 => { + let StatementKind::ResourceCall(custody::ResourceCallNode::Stage { + value, .. + }) = &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + value.ty = TypeInterner::INT64; + } + 3 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, copied); + } + 4 => { + let copied = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[current.entry.index() as usize] + .statements + .push(copied); + } + _ => unreachable!(), + } + assert!( + matches!( + EmittedResourceLayout::from_program( + &changed, + &checked.checked().interner, + selected + ), + Err(CodegenError::InvalidMir(_)) + ), + "mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_staged_sum_rows_preserve_scope_storage_and_active_execution() { + for release in [false, true] { + for &source in &STAGED_SOURCES[4..] { + let checked = checked(source, release); + let program = lower_source(&checked); + let selected = entry(&program); + let layout = EmittedResourceLayout::from_program( + &program, + &checked.checked().interner, + selected, + ) + .unwrap(); + let flow = layout.plan().function(selected).unwrap(); + let active = flow + .operations() + .iter() + .find(|operation| matches!(operation.role(), Role::BeginSourceFunction { .. })) + .unwrap() + .frame(); + let scope = flow.root_scope().id(); + let mut rows = Rows::new(layout.plan()).unwrap(); + rows.populate().unwrap(); + let mut extractions = 0; + for operation in flow.operations() { + let source = match operation.role() { + Role::SumTake { + source, + destination, + .. + } => { + assert_eq!(flow.owner_slots()[destination.index()].frame(), scope); + *source + } + Role::TakeFailureCompanion { source, .. } => *source, + _ => continue, + }; + extractions += 1; + assert_eq!(flow.owner_slots()[source.index()].frame(), scope); + assert_eq!(operation.frame(), active); + let ordinal = layout.operation(selected, operation.id()).unwrap(); + assert_eq!( + rows.operations[ordinal as usize].1[1], + layout.frame(selected, active).unwrap() + ); + } + assert_eq!(extractions, 2); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/emit/values.rs b/crates/jett_codegen_cranelift/src/emit/values.rs new file mode 100644 index 00000000..9f079586 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/emit/values.rs @@ -0,0 +1,3543 @@ +use super::*; +use jett_hir::{ + InterfaceFunctionAdapter, IntrinsicId, MapEntry, ReflectedFieldAction, + ReflectedFieldValidation, StringSegment, +}; +use jett_mir::move_values::is_erased_interface; +use jett_runtime::native_abi::values::interface_conversion::NativeInterfaceConversion; +use jett_types::BitfieldFieldKind; +use std::collections::BTreeSet; + +fn append_builder_debug_layout( + layout: &mut Vec, + types: &TypeInterner, + ty: TypeId, +) -> Result<(), CodegenError> { + let debug = super::debug::debug_layout(types, ty).unwrap_or_default(); + let length = u32::try_from(debug.len()) + .map_err(|_| CodegenError::Backend("reflected field debug layout is too long".into()))?; + layout.extend_from_slice(&length.to_le_bytes()); + layout.extend_from_slice(&debug); + Ok(()) +} + +fn interface_conversion( + types: &TypeInterner, + source: TypeId, + target: TypeId, + adapters: &[InterfaceFunctionAdapter], +) -> Option { + use jett_runtime::native_abi::values::interface_conversion::NativeInterfaceConversion as C; + let source_rep = representation_type(types, source); + let target_rep = representation_type(types, target); + let source_erased = is_erased_interface(types, source); + let target_erased = is_erased_interface(types, target); + let boxed_owner = interface_box_owner(types, source, target); + let owned = |ty| is_copy_owned(types, ty) || is_linear(types, ty); + if (source_rep == target_rep && source_erased == target_erased) || source == TypeInterner::NEVER + { + return Some(C::Copy { + owned: owned(target), + }); + } + Some( + match (types.resolve(source_rep), types.resolve(target_rep)) { + (_, _) if target_erased => C::Box { + concrete: boxed_owner.index() as u64, + owned: owned(source), + nothing: source_rep == TypeInterner::NOTHING, + layout: debug::debug_layout(types, boxed_owner)?, + }, + (_, _) if source_erased => C::Unbox { + concrete: target.index() as u64, + owned: owned(target), + nothing: target_rep == TypeInterner::NOTHING, + }, + (Type::List(a), Type::List(b)) => { + C::List(Box::new(interface_conversion(types, *a, *b, adapters)?)) + } + (Type::Optional(a), Type::Optional(b)) => { + C::Optional(Box::new(interface_conversion(types, *a, *b, adapters)?)) + } + (Type::Result(a, b), Type::Result(c, d)) => C::Result( + Box::new(interface_conversion(types, *a, *c, adapters)?), + Box::new(interface_conversion(types, *b, *d, adapters)?), + ), + (Type::Map(ak, av), Type::Map(bk, bv)) if ak == bk || *ak == TypeInterner::NEVER => { + C::Map( + Box::new(interface_conversion(types, *av, *bv, adapters)?), + is_string(types, *bk), + ) + } + (Type::Function { .. }, Type::Function { .. }) => C::FunctionAdapter { + code: adapters + .iter() + .find(|entry| entry.source == source_rep && entry.target == target_rep)? + .function + .index() as u64, + }, + _ => return None, + }, + ) +} + +fn interface_box_owner(types: &TypeInterner, mut source: TypeId, mut target: TypeId) -> TypeId { + // Shared outer secret qualifiers protect the destination rather than + // naming its concrete implementation. An unqualified interface destination + // can instead select an explicit implementation for the secret source. + while let Type::Secret(inner) = types.resolve(target) { + target = *inner; + if let Type::Secret(inner) = types.resolve(source) { + source = *inner; + } + } + source +} + +fn reflected_field_types_compatible( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, +) -> bool { + reflected_field_carriers_compatible(types, actual, requested, true, types.len()) +} + +fn reflected_field_carriers_compatible( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + root: bool, + budget: usize, +) -> bool { + if budget == 0 + || actual.index() as usize >= types.len() + || requested.index() as usize >= types.len() + { + return false; + } + if actual == requested { + return true; + } + // Only the original root read knows how to box an interface refinement. + // A collection clone cannot manufacture nested interface conversions. + if !root && is_erased_interface(types, actual) != is_erased_interface(types, requested) { + return false; + } + let Some(actual) = reflected_field_carrier(types, actual) else { + return false; + }; + let Some(requested) = reflected_field_carrier(types, requested) else { + return false; + }; + let child = + |a, b, at_root| reflected_field_carriers_compatible(types, a, b, at_root, budget - 1); + match (types.resolve(actual), types.resolve(requested)) { + // Match every secrecy layer rather than comparing only an outer flag. + (Type::Secret(a), Type::Secret(b)) => child(*a, *b, root), + (Type::Secret(_), _) | (_, Type::Secret(_)) => false, + (Type::List(a), Type::List(b)) + | (Type::Set(a), Type::Set(b)) + | (Type::Optional(a), Type::Optional(b)) => child(*a, *b, false), + (Type::Map(a, b), Type::Map(c, d)) | (Type::Result(a, b), Type::Result(c, d)) => { + child(*a, *c, false) && child(*b, *d, false) + } + // Canonical nominal identity, callable signatures and primitive widths + // remain exact. Equal machine words or field layouts are not casts. + _ => actual == requested, + } +} + +fn reflected_field_carrier(types: &TypeInterner, mut ty: TypeId) -> Option { + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Refinement { base, .. } => ty = *base, + _ => return Some(ty), + } + } + None +} + +fn reflected_field_needs_interface_box( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, +) -> bool { + reflected_field_types_compatible(types, actual, requested) + && is_erased_interface(types, requested) + && !is_erased_interface(types, actual) +} + +#[derive(Clone, Copy)] +struct ReflectedFieldConversion { + layout: Value, + length: Value, +} + +impl Translator<'_, '_> { + pub(super) fn preflight_interface_dispatch_argument( + &mut self, + expression: &Expression, + ) -> Result<(), CodegenError> { + match &expression.kind { + ExpressionKind::View(inner) | ExpressionKind::Clone(inner) => { + self.preflight_interface_dispatch_argument(inner) + } + ExpressionKind::InterfaceCoerce { value, adapters } => { + let conversion = + interface_conversion(self.types, value.ty, expression.ty, adapters) + .ok_or_else(|| { + self.unsupported( + expression.span, + "interface dispatch conversion preflight", + ) + })?; + if !conversion.requires_owner_validation() { + return Ok(()); + } + // Generated dispatch operands are the already evaluated formal + // values. Observe them without taking custody or converting. + let input = self.argument(value, true)?; + let input = self.scalar(input, value.span)?; + let (mut bytes, functions) = conversion.encode_with_function_offsets(); + for (offset, _) in &functions { + bytes[*offset..*offset + 8].fill(0); + } + let (layout, length) = self.static_data_with_functions(&bytes, &functions)?; + self.leaf( + NativeLeaf::InterfaceValidate, + &[input, layout, length], + true, + )?; + Ok(()) + } + _ => Ok(()), + } + } + + pub(super) fn interface_coerce( + &mut self, + expression: &Expression, + target: TypeId, + borrowed: bool, + adapters: &[InterfaceFunctionAdapter], + ) -> Result { + let source = representation_type(self.types, expression.ty); + let target_representation = representation_type(self.types, target); + let source_erased = is_erased_interface(self.types, expression.ty); + let target_erased = is_erased_interface(self.types, target); + if source == target_representation && source_erased == target_erased { + return self.argument(expression, borrowed); + } + if target_erased { + let value = self.argument(expression, borrowed)?; + let depth = if source == TypeInterner::NOTHING { + self.scalar(value, expression.span)? + } else { + match value { + LoweredValue::ScalarTask(_, depth) => depth, + _ => self.builder.ins().iconst(ir::types::I64, 0), + } + }; + let bits = if source == TypeInterner::NOTHING { + self.builder.ins().iconst(ir::types::I64, 0) + } else { + self.payload_bits(value).0 + }; + let owned = + is_copy_owned(self.types, expression.ty) || is_linear(self.types, expression.ty); + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let boxed_owner = interface_box_owner(self.types, expression.ty, target); + let identity = self + .builder + .ins() + .iconst(ir::types::I64, boxed_owner.index() as i64); + let layout = debug::debug_layout(self.types, boxed_owner).ok_or_else(|| { + self.unsupported(expression.span, "interface payload debug layout") + })?; + let (layout, length) = self.static_data(&layout)?; + let result = self.leaf( + NativeLeaf::InterfaceBox, + &[identity, bits, owned, depth, layout, length], + true, + )?; + return self.own(result); + } + if source_erased { + let value = self.argument(expression, true)?; + let value = self.scalar(value, expression.span)?; + let identity = self + .builder + .ins() + .iconst(ir::types::I64, target.index() as i64); + let bits = self.leaf(NativeLeaf::InterfaceUnbox, &[value, identity], true)?; + if target_representation == TypeInterner::NOTHING { + let depth = self.leaf(NativeLeaf::InterfacePendingDepth, &[value], true)?; + return Ok(LoweredValue::Scalar(depth)); + } + let unpacked = self.unpack_payload(bits, target, expression.span)?; + if is_task_scalar(self.types, target)? { + let depth = self.leaf(NativeLeaf::InterfacePendingDepth, &[value], true)?; + return Ok(LoweredValue::ScalarTask( + self.scalar(unpacked, expression.span)?, + depth, + )); + } + return Ok(unpacked); + } + let conversion = interface_conversion(self.types, expression.ty, target, adapters) + .ok_or_else(|| { + self.unsupported(expression.span, "nested interface-compatible conversion") + })?; + let value = self.argument(expression, borrowed)?; + let value = self.scalar(value, expression.span)?; + let (mut bytes, functions) = conversion.encode_with_function_offsets(); + // Relocations supply addresses. Never leave a function ID in the object. + for (offset, _) in &functions { + bytes[*offset..*offset + 8].fill(0); + } + let (layout, length) = self.static_data_with_functions(&bytes, &functions)?; + let output = self.leaf(NativeLeaf::InterfaceConvert, &[value, layout, length], true)?; + self.own_linear(output) + } + + pub(super) fn leaf( + &mut self, + leaf: NativeLeaf, + args: &[Value], + check: bool, + ) -> Result { + let id = crate::values::declare_leaf(self.module, leaf)?; + let reference = self.module.declare_func_in_func(id, self.builder.func); + let context = self.builder.use_var(self.runtime_context); + let mut arguments = vec![context]; + arguments.extend_from_slice(args); + let call = self.builder.ins().call(reference, &arguments); + let result = self.builder.func.dfg.inst_results(call)[0]; + if check { + self.check_failure()?; + } + Ok(result) + } + pub(super) fn check_failure(&mut self) -> Result<(), CodegenError> { + let status = self.leaf(NativeLeaf::Status, &[], false)?; + let success = self.builder.create_block(); + if let Some(resource) = self.resource { + self.builder + .ins() + .brif(status, resource.failure, &[status.into()], success, &[]); + } else { + self.builder + .ins() + .brif(status, self.failure_block, &[], success, &[]); + } + self.builder.switch_to_block(success); + Ok(()) + } + pub(super) fn own(&mut self, value: Value) -> Result { + let slot = self + .temporary_slots + .get(self.next_temporary) + .ok_or_else(|| { + CodegenError::Backend(format!( + "MIR temporary ownership bound exceeded in `{}`: needed at least {}, planned {}", + self.symbol, + self.next_temporary + 1, + self.temporary_slots.len() + )) + })?; + self.next_temporary += 1; + self.builder.ins().stack_store(value, *slot, 0); + Ok(LoweredValue::Owned(value, *slot)) + } + pub(super) fn construct_struct( + &mut self, + fields: &[Expression], + evaluation_order: &[usize], + span: Span, + ) -> Result { + let count = self + .builder + .ins() + .iconst(ir::types::I64, fields.len() as i64); + let handle = self.leaf(NativeLeaf::StructNew, &[count], true)?; + // Own the partially initialized record before evaluating any field. + let record = self.own_linear(handle)?; + self.initialize_struct(handle, fields, evaluation_order)?; + let _ = span; + Ok(record) + } + fn initialize_struct( + &mut self, + handle: Value, + fields: &[Expression], + evaluation_order: &[usize], + ) -> Result<(), CodegenError> { + for &index in evaluation_order { + self.initialize_struct_field(handle, index, &fields[index])?; + } + Ok(()) + } + pub(super) fn initialize_struct_field( + &mut self, + handle: Value, + index: usize, + expression: &Expression, + ) -> Result<(), CodegenError> { + let value = self.expression(expression)?; + let (bits, owned) = self.payload_bits(value); + let index = self.builder.ins().iconst(ir::types::I64, index as i64); + if let LoweredValue::ScalarTask(_, depth) = value { + self.leaf( + NativeLeaf::StructInitScalarTask, + &[handle, index, bits, depth], + true, + )?; + } else { + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + self.leaf(NativeLeaf::StructInit, &[handle, index, bits, owned], true)?; + } + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + Ok(()) + } + pub(super) fn construct_validated_bitfield( + &mut self, + bitfield_type: TypeId, + fields: &[Expression], + evaluation_order: &[usize], + span: Span, + ) -> Result { + let Type::Bitfield(id) = self.types.resolve(bitfield_type) else { + return Err(self.unsupported(span, "bitfield construction type")); + }; + let layout = self.types.resolve_bitfield(*id).clone(); + let mut data = b"JC\x03".to_vec(); + fn name(data: &mut Vec, value: &str) -> Result<(), CodegenError> { + let length = u32::try_from(value.len()) + .map_err(|_| CodegenError::Backend("bitfield layout name is too long".into()))?; + data.extend_from_slice(&length.to_le_bytes()); + data.extend_from_slice(value.as_bytes()); + Ok(()) + } + name(&mut data, &layout.name)?; + let count = u32::try_from(layout.fields.len()) + .map_err(|_| CodegenError::Backend("too many bitfield fields".into()))?; + data.extend_from_slice(&count.to_le_bytes()); + for field in &layout.fields { + name(&mut data, &field.name)?; + let field_type = self.types.type_name(field.ty); + name(&mut data, &field_type)?; + name(&mut data, &field_type)?; + } + for field in &layout.fields { + match field.kind { + BitfieldFieldKind::Payload => data.push(0), + BitfieldFieldKind::Bits { width } => { + let kind = match self.types.resolve(field.ty) { + Type::Int64 => 1, + Type::Uint64 => 2, + Type::Enum(_) => 3, + _ => return Err(self.unsupported(span, "bitfield field validation")), + }; + data.push(kind); + data.extend_from_slice(&u32::from(width).to_le_bytes()); + if let Type::Enum(enum_id) = self.types.resolve(field.ty) { + let definition = self.types.resolve_enum(*enum_id); + name(&mut data, &definition.name)?; + let variants = u32::try_from(definition.variants.len()) + .map_err(|_| self.unsupported(span, "bitfield enum variants"))?; + data.extend_from_slice(&variants.to_le_bytes()); + for variant in &definition.variants { + name(&mut data, &variant.name)?; + data.extend_from_slice(&variant.discriminant.to_le_bytes()); + } + } + } + } + } + let (pointer, length) = self.static_data(&data)?; + let handle = self.leaf(NativeLeaf::BuilderNew, &[pointer, length], true)?; + let builder = self.own_linear(handle)?; + self.initialize_struct(handle, fields, evaluation_order)?; + let (owner_pointer, owner_length) = self.static_bytes(&layout.name)?; + let result = self.leaf( + NativeLeaf::BuilderFinish, + &[handle, owner_pointer, owner_length], + true, + )?; + if let LoweredValue::Owned(_, slot) = builder { + self.clear_slot(slot); + } + self.own_linear(result) + } + pub(super) fn construct_tagged_record( + &mut self, + tag: u32, + payloads: &[Expression], + evaluation_order: impl IntoIterator, + span: Span, + ) -> Result { + let count = i64::try_from(payloads.len() + 1) + .map_err(|_| self.unsupported(span, "enum payload count"))?; + let count = self.builder.ins().iconst(ir::types::I64, count); + let handle = self.leaf(NativeLeaf::StructNew, &[count], true)?; + let record = self.own_linear(handle)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.builder.ins().iconst(ir::types::I64, i64::from(tag)); + let borrowed = self.builder.ins().iconst(ir::types::I32, 0); + self.leaf(NativeLeaf::StructInit, &[handle, zero, tag, borrowed], true)?; + for field in evaluation_order { + let value = self.expression(&payloads[field])?; + let (bits, owned) = self.payload_bits(value); + let index = self.builder.ins().iconst(ir::types::I64, field as i64 + 1); + if let LoweredValue::ScalarTask(_, depth) = value { + self.leaf( + NativeLeaf::StructInitScalarTask, + &[handle, index, bits, depth], + true, + )?; + } else { + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + self.leaf(NativeLeaf::StructInit, &[handle, index, bits, owned], true)?; + } + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + } + Ok(record) + } + pub(super) fn encode_bitfield( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result { + let Type::Bitfield(id) = self.types.resolve(ty) else { + return Err(self.unsupported(span, "bitfield encoding type")); + }; + let layout = self.types.resolve_bitfield(*id).clone(); + let message = format!("{}.to_bytes expects a {} value", layout.name, layout.name); + self.reject_pending_handle(value, 4, &message, span)?; + let record = self.scalar(value, span)?; + let bytes = self.leaf(NativeLeaf::BytesNew, &[], true)?; + let output = self.own_linear(bytes)?; + let mut bit_offset = 0_u64; + for (field_index, field) in layout.fields.iter().enumerate() { + let index = self + .builder + .ins() + .iconst(ir::types::I64, field_index as i64); + let bits = self.leaf(NativeLeaf::StructField, &[record, index], true)?; + match &field.kind { + BitfieldFieldKind::Bits { width } => { + let numeric = if let Type::Enum(enum_id) = self.types.resolve(field.ty) { + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.leaf(NativeLeaf::StructField, &[bits, zero], true)?; + let variants = self.types.resolve_enum(*enum_id).variants.clone(); + let mut numeric = zero; + for (index, variant) in variants.iter().enumerate() { + let matches = + self.builder.ins().icmp_imm(IntCC::Equal, tag, index as i64); + let discriminant = self + .builder + .ins() + .iconst(ir::types::I64, variant.discriminant); + numeric = self.builder.ins().select(matches, discriminant, numeric); + } + numeric + } else { + bits + }; + let width_value = self.builder.ins().iconst(ir::types::I32, i64::from(*width)); + let network = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(layout.network_order)); + let offset = self.builder.ins().iconst(ir::types::I64, bit_offset as i64); + self.leaf( + NativeLeaf::BitfieldWriteBits, + &[bytes, numeric, width_value, network, offset], + true, + )?; + bit_offset += u64::from(*width); + } + BitfieldFieldKind::Payload => { + let prefix = format!("bitfield '{}' field '{}': ", layout.name, field.name); + let (pointer, length) = self.static_bytes(&prefix)?; + self.leaf( + NativeLeaf::BitfieldCheckPayloadPending, + &[bits, pointer, length], + true, + )?; + self.leaf(NativeLeaf::BitfieldExtendPayload, &[bytes, bits], true)?; + } + } + } + Ok(output) + } + pub(super) fn decode_bitfield( + &mut self, + value: LoweredValue, + result_type: TypeId, + span: Span, + ) -> Result { + let Type::Result(bitfield_type, _) = self.types.resolve(result_type) else { + return Err(self.unsupported(span, "bitfield decoding result")); + }; + let Type::Bitfield(id) = self.types.resolve(*bitfield_type) else { + return Err(self.unsupported(span, "bitfield decoding type")); + }; + let layout = self.types.resolve_bitfield(*id).clone(); + let message = format!("{}.from_bytes expects a bytes argument", layout.name); + self.reject_pending_handle(value, 1, &message, span)?; + fn name(data: &mut Vec, value: &str) -> Result<(), CodegenError> { + let length = u32::try_from(value.len()) + .map_err(|_| CodegenError::Backend("bitfield layout name is too long".into()))?; + data.extend_from_slice(&length.to_le_bytes()); + data.extend_from_slice(value.as_bytes()); + Ok(()) + } + let mut data = vec![b'J', b'B', 1, u8::from(layout.network_order)]; + name(&mut data, &layout.name)?; + let field_count = u32::try_from(layout.fields.len()) + .map_err(|_| CodegenError::Backend("too many bitfield fields".into()))?; + data.extend_from_slice(&field_count.to_le_bytes()); + for field in &layout.fields { + match &field.kind { + BitfieldFieldKind::Bits { width } => { + let width = u8::try_from(*width) + .map_err(|_| self.unsupported(span, "bitfield decoding width"))?; + if let Type::Enum(enum_id) = self.types.resolve(field.ty) { + data.extend_from_slice(&[1, width]); + name(&mut data, &field.name)?; + let enum_layout = self.types.resolve_enum(*enum_id); + name(&mut data, &enum_layout.name)?; + let variant_count = u32::try_from(enum_layout.variants.len()) + .map_err(|_| CodegenError::Backend("too many enum variants".into()))?; + data.extend_from_slice(&variant_count.to_le_bytes()); + for variant in &enum_layout.variants { + data.extend_from_slice(&variant.discriminant.to_le_bytes()); + } + } else { + data.extend_from_slice(&[0, width]); + name(&mut data, &field.name)?; + } + } + BitfieldFieldKind::Payload => { + data.extend_from_slice(&[2, 0]); + name(&mut data, &field.name)?; + } + } + } + let length = i64::try_from(data.len()) + .map_err(|_| CodegenError::Backend("bitfield layout is too large".into()))?; + let item = self + .module + .declare_anonymous_data(false, false) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let mut description = cranelift_module::DataDescription::new(); + description.define(data.into_boxed_slice()); + self.module + .define_data(item, &description) + .map_err(|error| CodegenError::Backend(error.to_string()))?; + let reference = self.module.declare_data_in_func(item, self.builder.func); + let pointer = self.builder.ins().global_value(ir::types::I64, reference); + let length = self.builder.ins().iconst(ir::types::I64, length); + let value = self.scalar(value, span)?; + let decoded = self.leaf(NativeLeaf::BitfieldDecode, &[value, pointer, length], true)?; + self.own_linear(decoded) + } + pub(super) fn project_field( + &mut self, + base: &Expression, + field: jett_hir::FieldId, + ty: TypeId, + span: Span, + borrowed: bool, + ) -> Result { + let index = if matches!( + self.types.resolve(representation_type(self.types, base.ty)), + Type::MachineState { .. } + ) { + field.index() + 1 + } else { + field.index() + }; + self.struct_field(base, index, ty, span, borrowed) + } + pub(super) fn clone_linear( + &mut self, + borrowed: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result { + let value = self.scalar(borrowed, span)?; + let cloned = self.clone_linear_handle(value, ty)?; + self.own_linear(cloned) + } + pub(super) fn clone_linear_handle( + &mut self, + value: Value, + ty: TypeId, + ) -> Result { + let leaf = match self.types.resolve(representation_type(self.types, ty)) { + Type::Bytes => NativeLeaf::BytesClone, + Type::List(_) => NativeLeaf::ListClone, + Type::Set(_) => NativeLeaf::SetClone, + Type::Map(..) => NativeLeaf::MapClone, + Type::Struct(_) | Type::Enum(_) | Type::Bitfield(_) | Type::TypeConstruction => { + NativeLeaf::StructClone + } + Type::Machine(_) | Type::MachineState { .. } | Type::Interface(_) => { + NativeLeaf::StructClone + } + _ => NativeLeaf::SumClone, + }; + self.leaf(leaf, &[value], true) + } + pub(super) fn check_struct_pending_access( + &mut self, + value: Value, + owner_type: TypeId, + message: &str, + render: bool, + ) -> Result<(), CodegenError> { + let layout = super::debug::debug_layout(self.types, owner_type).unwrap_or_default(); + let (layout_pointer, layout_length) = self.static_data(&layout)?; + let (message_pointer, message_length) = self.static_bytes(message)?; + let render = self.builder.ins().iconst(ir::types::I32, i64::from(render)); + self.leaf( + NativeLeaf::StructPendingAccessCheck, + &[ + value, + layout_pointer, + layout_length, + message_pointer, + message_length, + render, + ], + true, + )?; + Ok(()) + } + + fn struct_field( + &mut self, + base: &Expression, + index: u32, + ty: TypeId, + span: Span, + borrowed: bool, + ) -> Result { + let parent = self.argument(base, true)?; + let parent = self.scalar(parent, span)?; + self.check_struct_pending_access( + parent, + base.ty, + "field access is not supported on ", + true, + )?; + let index = self.builder.ins().iconst(ir::types::I64, i64::from(index)); + let bits = self.leaf(NativeLeaf::StructField, &[parent, index], true)?; + if is_linear(self.types, ty) { + // A view keeps the owning root live; an owned read deep-clones the + // field before full-expression cleanup releases the root. + let value = LoweredValue::Scalar(bits); + return if borrowed { + Ok(value) + } else { + self.clone_linear(value, ty, span) + }; + } + if is_function(self.types, ty) { + return if borrowed { + Ok(LoweredValue::Scalar(bits)) + } else { + let cloned = self.leaf(NativeLeaf::StructClone, &[bits], true)?; + self.own(cloned) + }; + } + let bits = if is_string(self.types, ty) { + self.leaf(NativeLeaf::Retain, &[bits], true)? + } else { + bits + }; + let value = self.unpack_payload(bits, ty, span)?; + if is_task_scalar(self.types, ty)? { + let depth = self.leaf(NativeLeaf::StructFieldPendingDepth, &[parent, index], true)?; + Ok(LoweredValue::ScalarTask(self.scalar(value, span)?, depth)) + } else { + Ok(value) + } + } + pub(super) fn construct_sum( + &mut self, + success: bool, + payload: Option<&Expression>, + span: Span, + ) -> Result { + let value = if let Some(payload) = payload { + self.expression(payload)? + } else { + self.nothing() + }; + self.construct_sum_value(success, value, span) + } + pub(super) fn construct_sum_value( + &mut self, + success: bool, + value: LoweredValue, + span: Span, + ) -> Result { + if let LoweredValue::ScalarTask(bits, depth) = value { + let (bits, _) = self.payload_bits(LoweredValue::Scalar(bits)); + let tag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(success)); + let sum = self.leaf(NativeLeaf::SumNewScalarTask, &[tag, bits, depth], true)?; + return self.own_linear(sum); + } + let (bits, owned) = self.payload_bits(value); + let tag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(success)); + let owns = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let sum = self.leaf(NativeLeaf::SumNew, &[tag, bits, owns], true)?; + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + let _ = span; + self.own_linear(sum) + } + pub(super) fn payload_bits(&mut self, value: LoweredValue) -> (Value, bool) { + match value { + LoweredValue::Owned(v, _) => (v, true), + LoweredValue::Scalar(v) | LoweredValue::ScalarTask(v, _) => { + let ty = self.builder.func.dfg.value_type(v); + let bits = if ty == ir::types::F64 { + self.builder + .ins() + .bitcast(ir::types::I64, ir::MemFlags::new(), v) + } else if ty == ir::types::F32 { + let bits = self + .builder + .ins() + .bitcast(ir::types::I32, ir::MemFlags::new(), v); + self.builder.ins().uextend(ir::types::I64, bits) + } else if ty != ir::types::I64 { + self.builder.ins().uextend(ir::types::I64, v) + } else { + v + }; + (bits, false) + } + } + } + pub(super) fn unpack_payload( + &mut self, + bits: Value, + ty: TypeId, + span: Span, + ) -> Result { + if is_copy_owned(self.types, ty) || is_linear(self.types, ty) { + return self.own(bits); + } + let native = self.required_clif_type(ty, span)?; + let value = if native == ir::types::F64 { + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native == ir::types::F32 { + let bits = self.builder.ins().ireduce(ir::types::I32, bits); + self.builder + .ins() + .bitcast(native, ir::MemFlags::new(), bits) + } else if native != ir::types::I64 { + self.builder.ins().ireduce(native, bits) + } else { + bits + }; + let _ = span; + Ok(LoweredValue::Scalar(value)) + } + fn list_new(&mut self, ty: TypeId, span: Span) -> Result { + let Type::List(element) = self.types.resolve(ty) else { + return Err(self.unsupported(span, "invalid list layout")); + }; + let owned = is_copy_owned(self.types, *element) || is_linear(self.types, *element); + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let value = self.leaf(NativeLeaf::ListNew, &[owned], true)?; + self.own_linear(value) + } + fn list_append( + &mut self, + list: LoweredValue, + value: LoweredValue, + span: Span, + ) -> Result<(), CodegenError> { + let handle = self.scalar(list, span)?; + let (bits, _) = self.payload_bits(value); + if let LoweredValue::ScalarTask(_, depth) = value { + self.leaf( + NativeLeaf::ListAppendScalarTask, + &[handle, bits, depth], + true, + )?; + } else { + self.leaf(NativeLeaf::ListAppend, &[handle, bits], true)?; + } + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + Ok(()) + } + pub(super) fn construct_list( + &mut self, + elements: &[Expression], + ty: TypeId, + span: Span, + ) -> Result { + let list = self.list_new(ty, span)?; + for element in elements { + let value = self.expression(element)?; + self.list_append(list, value, span)?; + } + Ok(list) + } + fn map_new(&mut self, ty: TypeId, span: Span) -> Result { + let Type::Map(key, value) = self.types.resolve(ty) else { + return Err(self.unsupported(span, "invalid map layout")); + }; + let key_strings = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(is_string(self.types, *key))); + let value_owned = self.builder.ins().iconst( + ir::types::I32, + i64::from(is_copy_owned(self.types, *value) || is_linear(self.types, *value)), + ); + let result = self.leaf(NativeLeaf::MapNew, &[key_strings, value_owned], true)?; + self.own_linear(result) + } + fn map_insert( + &mut self, + map: LoweredValue, + key: LoweredValue, + value: LoweredValue, + literal: bool, + span: Span, + ) -> Result<(), CodegenError> { + let handle = self.scalar(map, span)?; + let (key_bits, _) = self.payload_bits(key); + let (value_bits, _) = self.payload_bits(value); + let key_depth = if let LoweredValue::ScalarTask(_, depth) = key { + depth + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + let value_depth = if let LoweredValue::ScalarTask(_, depth) = value { + depth + } else { + self.builder.ins().iconst(ir::types::I64, 0) + }; + self.leaf( + if literal { + NativeLeaf::MapAppendLiteralScalarTasks + } else { + NativeLeaf::MapInsertScalarTasks + }, + &[handle, key_bits, key_depth, value_bits, value_depth], + true, + )?; + if let LoweredValue::Owned(_, slot) = key { + self.clear_slot(slot); + } + if let LoweredValue::Owned(_, slot) = value { + self.clear_slot(slot); + } + Ok(()) + } + pub(super) fn construct_map( + &mut self, + entries: &[MapEntry], + ty: TypeId, + span: Span, + ) -> Result { + let map = self.map_new(ty, span)?; + for entry in entries { + let key = self.expression(&entry.key)?; + let value = self.expression(&entry.value)?; + self.map_insert(map, key, value, true, span)?; + } + Ok(map) + } + fn map_intrinsic( + &mut self, + id: IntrinsicId, + values: &[LoweredValue], + result_type: TypeId, + span: Span, + ) -> Result { + match id { + IntrinsicId::MapNew => self.map_new(result_type, span), + IntrinsicId::MapLength => { + let map = self.scalar(values[0], span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::MapLength, + &[map], + true, + )?)) + } + IntrinsicId::MapHas => { + let map = self.scalar(values[0], span)?; + let (key, _) = self.payload_bits(values[1]); + let found = if let LoweredValue::ScalarTask(_, depth) = values[1] { + self.leaf(NativeLeaf::MapHasScalarTask, &[map, key, depth], true)? + } else { + self.leaf(NativeLeaf::MapHas, &[map, key], true)? + }; + Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, found), + )) + } + IntrinsicId::MapGet => { + let map = self.scalar(values[0], span)?; + let (key, _) = self.payload_bits(values[1]); + let result = if let LoweredValue::ScalarTask(_, depth) = values[1] { + self.leaf(NativeLeaf::MapGetScalarTask, &[map, key, depth], true)? + } else { + self.leaf(NativeLeaf::MapGet, &[map, key], true)? + }; + self.own_linear(result) + } + IntrinsicId::MapInsert => { + let LoweredValue::Owned(map, slot) = values[0] else { + return Err(self.unsupported(span, "map insert requires owner")); + }; + self.map_insert(values[0], values[1], values[2], false, span)?; + self.clear_slot(slot); + self.own_linear(map) + } + IntrinsicId::MapRemove => { + let LoweredValue::Owned(map, slot) = values[0] else { + return Err(self.unsupported(span, "map remove requires owner")); + }; + let (key, _) = self.payload_bits(values[1]); + let result = if let LoweredValue::ScalarTask(_, depth) = values[1] { + self.leaf(NativeLeaf::MapRemoveScalarTask, &[map, key, depth], true)? + } else { + self.leaf(NativeLeaf::MapRemove, &[map, key], true)? + }; + self.clear_slot(slot); + self.own_linear(result) + } + IntrinsicId::MapFromLists => { + let Type::Map(key, value) = self.types.resolve(result_type) else { + return Err(self.unsupported(span, "invalid map result layout")); + }; + let key_strings = is_string(self.types, *key); + let value_owned = + is_copy_owned(self.types, *value) || is_linear(self.types, *value); + let LoweredValue::Owned(keys, key_slot) = values[0] else { + return Err(self.unsupported(span, "map keys require owning list")); + }; + let LoweredValue::Owned(items, value_slot) = values[1] else { + return Err(self.unsupported(span, "map values require owning list")); + }; + let key_flag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(key_strings)); + let value_flag = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(value_owned)); + let result = self.leaf( + NativeLeaf::MapFromLists, + &[keys, items, key_flag, value_flag], + true, + )?; + self.clear_slot(key_slot); + self.clear_slot(value_slot); + self.own_linear(result) + } + _ => Err(self.unsupported(span, "map intrinsic")), + } + } + fn list_intrinsic( + &mut self, + id: IntrinsicId, + args: &[Expression], + values: &[LoweredValue], + result_type: TypeId, + span: Span, + ) -> Result { + let check_kind = match id { + IntrinsicId::ListLength => Some(0), + IntrinsicId::ListAppend => Some(1), + IntrinsicId::ListGetClone => Some(2), + IntrinsicId::ListInsertAt => Some(3), + IntrinsicId::ListRemoveAt => Some(4), + IntrinsicId::ListSort => Some(5), + IntrinsicId::ListSortByIndex => Some(6), + IntrinsicId::ListIsSorted => Some(7), + IntrinsicId::ListSwap => Some(8), + _ => None, + }; + if let Some(check_kind) = check_kind { + let list = self.scalar(values[0], span)?; + let kind = self.builder.ins().iconst(ir::types::I32, check_kind); + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let indexed = matches!( + id, + IntrinsicId::ListGetClone + | IntrinsicId::ListInsertAt + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListSwap + ); + let first_depth = if indexed { + match values[1] { + LoweredValue::ScalarTask(_, depth) => depth, + _ => zero, + } + } else { + zero + }; + let second_depth = if id == IntrinsicId::ListSwap { + match values[2] { + LoweredValue::ScalarTask(_, depth) => depth, + _ => zero, + } + } else { + zero + }; + self.leaf( + NativeLeaf::ListCheckArguments, + &[list, kind, first_depth, second_depth], + true, + )?; + } + match id { + IntrinsicId::ListNew => self.list_new(result_type, span), + IntrinsicId::ListSum => { + let list = self.scalar(values[0], span)?; + let kind = crate::values::list_sum_kind(self.types, result_type) + .ok_or_else(|| self.unsupported(span, "invalid numeric list sum type"))?; + let kind = self.builder.ins().iconst(ir::types::I32, kind as i64); + let bits = self.leaf(NativeLeaf::ListSumPrimitive, &[list, kind], true)?; + self.unpack_payload(bits, result_type, span) + } + IntrinsicId::ListLength => { + let list = self.scalar(values[0], span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::ListLength, + &[list], + true, + )?)) + } + IntrinsicId::ListGetClone => { + let list = self.scalar(values[0], span)?; + let index = self.scalar(values[1], span)?; + let value = self.leaf(NativeLeaf::ListGet, &[list, index], true)?; + self.own_linear(value) + } + IntrinsicId::ListAppend => { + self.list_append(values[0], values[1], span)?; + let LoweredValue::Owned(value, slot) = values[0] else { + return Err(self.unsupported(span, "append requires owning list")); + }; + self.clear_slot(slot); + self.own_linear(value) + } + IntrinsicId::ListInsertAt => { + let LoweredValue::Owned(list, list_slot) = values[0] else { + return Err(self.unsupported(span, "insert requires owning list")); + }; + let index = self.scalar(values[1], span)?; + let (bits, _) = self.payload_bits(values[2]); + let inserted = if let LoweredValue::ScalarTask(_, depth) = values[2] { + self.leaf( + NativeLeaf::ListInsertAtScalarTask, + &[list, index, bits, depth], + true, + )? + } else { + self.leaf(NativeLeaf::ListInsertAt, &[list, index, bits], true)? + }; + self.clear_slot(list_slot); + if let LoweredValue::Owned(_, value_slot) = values[2] { + self.clear_slot(value_slot); + } + self.own_linear(inserted) + } + IntrinsicId::ListRemoveAt => { + let LoweredValue::Owned(list, slot) = values[0] else { + return Err(self.unsupported(span, "remove requires owning list")); + }; + let index = self.scalar(values[1], span)?; + let removed = self.leaf(NativeLeaf::ListRemoveAt, &[list, index], true)?; + self.clear_slot(slot); + self.own_linear(removed) + } + IntrinsicId::ListSort => { + let Type::List(element) = self.types.resolve(result_type) else { + return Err(self.unsupported(span, "sort requires list result")); + }; + let kind = crate::values::list_sort_kind(self.types, *element) + .ok_or_else(|| self.unsupported(span, "sort element type"))?; + let LoweredValue::Owned(list, slot) = values[0] else { + return Err(self.unsupported(span, "sort requires owning list")); + }; + let kind = self.builder.ins().iconst(ir::types::I32, kind as i64); + let sorted = self.leaf(NativeLeaf::ListSort, &[list, kind], true)?; + self.clear_slot(slot); + self.own_linear(sorted) + } + IntrinsicId::ListSortByIndex => { + let Type::List(row) = self.types.resolve(result_type) else { + return Err(self.unsupported(span, "indexed sort requires list result")); + }; + let Type::List(element) = self.types.resolve(*row) else { + return Err(self.unsupported(span, "indexed sort requires row lists")); + }; + let kind = crate::values::list_comparison_kind(self.types, *element) + .map_or(-1, |kind| kind as i64); + let LoweredValue::Owned(list, slot) = values[0] else { + return Err(self.unsupported(span, "indexed sort requires owning list")); + }; + let index = self.scalar(values[1], span)?; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + let sorted = self.leaf(NativeLeaf::ListSortByIndex, &[list, index, kind], true)?; + self.clear_slot(slot); + self.own_linear(sorted) + } + IntrinsicId::ListIsSorted => { + let Type::List(element) = self.types.resolve(args[0].ty) else { + return Err(self.unsupported(span, "sortedness requires a list")); + }; + let kind = crate::values::list_comparison_kind(self.types, *element) + .map_or(-1, |kind| kind as i64); + let list = self.scalar(values[0], span)?; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + let sorted = self.leaf(NativeLeaf::ListIsSorted, &[list, kind], true)?; + Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, sorted), + )) + } + IntrinsicId::ListSwap => { + let LoweredValue::Owned(list, slot) = values[0] else { + return Err(self.unsupported(span, "swap requires owning list")); + }; + let first = self.scalar(values[1], span)?; + let second = self.scalar(values[2], span)?; + let swapped = self.leaf(NativeLeaf::ListSwap, &[list, first, second], true)?; + self.clear_slot(slot); + self.own_linear(swapped) + } + _ => Err(self.unsupported(span, "list intrinsic")), + } + } + fn set_intrinsic( + &mut self, + id: IntrinsicId, + values: &[LoweredValue], + result_type: TypeId, + span: Span, + ) -> Result { + match id { + IntrinsicId::SetNew => { + let Type::Set(element) = self.types.resolve(result_type) else { + return Err(self.unsupported(span, "set result layout")); + }; + let strings = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(is_string(self.types, *element))); + let value = self.leaf(NativeLeaf::SetNew, &[strings], true)?; + self.own_linear(value) + } + IntrinsicId::SetLength => { + let value = self.scalar(values[0], span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::SetLength, + &[value], + true, + )?)) + } + IntrinsicId::SetContains => { + let value = self.scalar(values[0], span)?; + let (key, _) = self.payload_bits(values[1]); + let found = if let LoweredValue::ScalarTask(_, depth) = values[1] { + self.leaf( + NativeLeaf::SetContainsScalarTask, + &[value, key, depth], + true, + )? + } else { + self.leaf(NativeLeaf::SetContains, &[value, key], true)? + }; + Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, found), + )) + } + IntrinsicId::SetAdd | IntrinsicId::SetRemove => { + let LoweredValue::Owned(set, slot) = values[0] else { + return Err(self.unsupported(span, "set mutation requires owner")); + }; + let (key, _) = self.payload_bits(values[1]); + let value = if let LoweredValue::ScalarTask(_, depth) = values[1] { + let leaf = if id == IntrinsicId::SetAdd { + NativeLeaf::SetAddScalarTask + } else { + NativeLeaf::SetRemoveScalarTask + }; + self.leaf(leaf, &[set, key, depth], true)? + } else { + let leaf = if id == IntrinsicId::SetAdd { + NativeLeaf::SetAdd + } else { + NativeLeaf::SetRemove + }; + self.leaf(leaf, &[set, key], true)? + }; + self.clear_slot(slot); + if id == IntrinsicId::SetAdd { + if let LoweredValue::Owned(_, key_slot) = values[1] { + self.clear_slot(key_slot); + } + } + self.own_linear(value) + } + _ => Err(self.unsupported(span, "set intrinsic")), + } + } + pub(super) fn clear_slot(&mut self, slot: ir::StackSlot) { + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder.ins().stack_store(zero, slot, 0); + } + pub(super) fn own_linear(&mut self, value: Value) -> Result { + let index = self.next_temporary; + self.own(value)?; + Ok(LoweredValue::Owned(value, self.temporary_slots[index])) + } + pub(super) fn own_copy_value( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result { + if matches!(value, LoweredValue::Owned(..)) { + return Ok(value); + } + let bits = self.scalar(value, span)?; + let leaf = if is_function(self.types, ty) { + NativeLeaf::StructClone + } else { + NativeLeaf::Retain + }; + let owned = self.leaf(leaf, &[bits], true)?; + self.own(owned) + } + pub(super) fn argument( + &mut self, + expression: &Expression, + borrowed: bool, + ) -> Result { + if borrowed + && let ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) + | ExpressionKind::RefinementValidated(inner) = &expression.kind + { + return self.argument(inner, true); + } + if borrowed + && let ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + } = &expression.kind + { + return self.interface_coerce(inner, expression.ty, true, adapters); + } + if borrowed + && (is_linear(self.types, expression.ty) || is_copy_owned(self.types, expression.ty)) + { + match &expression.kind { + ExpressionKind::View(inner) => return self.argument(inner, true), + ExpressionKind::Field { base, field, .. } => { + return self.project_field(base, *field, expression.ty, expression.span, true); + } + ExpressionKind::Local(local) => { + let index = local.index() as usize; + let v = + if let Some(slot) = self.local_slots[index] { + self.builder.ins().stack_load(ir::types::I64, slot, 0) + } else { + self.builder.use_var(self.variables[index].ok_or_else(|| { + CodegenError::Backend("borrow has no place".into()) + })?) + }; + self.expect_machine_state(local.index() as usize, expression.ty, v)?; + return Ok(LoweredValue::Scalar(v)); + } + _ => {} + } + } + self.expression(expression) + } + pub(super) fn expect_machine_state( + &mut self, + local_index: usize, + expression_type: TypeId, + value: Value, + ) -> Result<(), CodegenError> { + if let ( + Type::Machine(machine), + Type::MachineState { + machine: narrowed, + state, + }, + ) = ( + self.types.resolve(self.local_types[local_index].ty), + self.types.resolve(expression_type), + ) && machine == narrowed + { + let expected = self + .builder + .ins() + .iconst(ir::types::I64, i64::from(state.index())); + self.leaf(NativeLeaf::MachineExpectState, &[value, expected], true)?; + } + Ok(()) + } + pub(super) fn drop_slot(&mut self, slot: ir::StackSlot) -> Result<(), CodegenError> { + let value = self.builder.ins().stack_load(ir::types::I64, slot, 0); + self.leaf(NativeLeaf::DropValue, &[value], false)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + self.builder.ins().stack_store(zero, slot, 0); + Ok(()) + } + pub(super) fn call_generation_storage( + &self, + generation: jett_mir::CallOwnerGenerationId, + root: Option, + span: Span, + ) -> Result { + let mut matches = self + .generation_slots + .iter() + .filter(|storage| storage.generation == generation); + let storage = matches.next().copied().ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation has no validated native storage", + ) + })?; + if matches.next().is_some() || root.is_some_and(|root| root != storage.root) { + return Err(contract_error( + self.symbol, + span, + "call owner generation changes its exact root or storage", + )); + } + Ok(storage) + } + pub(super) fn replace_call_generation( + &mut self, + generation: jett_mir::CallOwnerGenerationId, + root: jett_mir::LocalId, + rhs_owner: jett_mir::LocalId, + span: Span, + ) -> Result<(), CodegenError> { + let storage = self.call_generation_storage(generation, Some(root), span)?; + let rhs = self + .local_types + .get(rhs_owner.index() as usize) + .ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation RHS local is absent", + ) + })?; + let rhs_slot = self + .local_slots + .get(rhs_owner.index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + contract_error( + self.symbol, + span, + "call owner generation RHS has no owning storage", + ) + })?; + if rhs.id != rhs_owner + || rhs.ty != storage.ty + || rhs.view_source.is_some() + || rhs_owner == root + || rhs_slot == storage.root_slot + { + return Err(contract_error( + self.symbol, + span, + "call owner generation RHS is not its independent exact owner", + )); + } + // RHS acquisition already completed at its unique checked owning Let. + // Its slot is transferred exactly once; no source expression runs here. + let next = self.builder.ins().stack_load(ir::types::I64, rhs_slot, 0); + self.clear_slot(rhs_slot); + let retired = self + .builder + .ins() + .stack_load(ir::types::I64, storage.retired_slot, 0); + let has_retired = self.builder.ins().icmp_imm(IntCC::NotEqual, retired, 0); + let attached = self.builder.create_block(); + let replaced = self.builder.create_block(); + let installed = self.builder.create_block(); + self.builder + .ins() + .brif(has_retired, replaced, &[], attached, &[]); + + self.builder.switch_to_block(attached); + let captured = self + .builder + .ins() + .stack_load(ir::types::I64, storage.root_slot, 0); + self.clear_slot(storage.root_slot); + self.builder + .ins() + .stack_store(captured, storage.owner_slot, 0); + let one = self.builder.ins().iconst(ir::types::I64, 1); + self.builder.ins().stack_store(one, storage.retired_slot, 0); + self.builder.ins().jump(installed, &[]); + + self.builder.switch_to_block(replaced); + // The old captured owner stays in escrow. Only the independent current + // generation is dropped before its ordinary replacement is installed. + self.drop_slot(storage.root_slot)?; + self.builder.ins().jump(installed, &[]); + + self.builder.switch_to_block(installed); + self.builder.ins().stack_store(next, storage.root_slot, 0); + Ok(()) + } + pub(super) fn drop_temporaries(&mut self) -> Result<(), CodegenError> { + for &slot in &self.temporary_slots[..self.next_temporary] { + self.drop_slot(slot)?; + } + self.next_temporary = 0; + Ok(()) + } + pub(super) fn drop_dead_locals(&mut self, live: &BTreeSet) -> Result<(), CodegenError> { + for (i, slot) in self.local_slots.iter().enumerate() { + if !live.contains(&i) + && let Some(slot) = slot + { + self.drop_slot(*slot)?; + } + } + Ok(()) + } + pub(super) fn drop_all(&mut self) -> Result<(), CodegenError> { + self.drop_temporaries()?; + self.drop_dead_locals(&BTreeSet::new())?; + for index in (0..self.graph_constructor_slots.len()).rev() { + self.drop_slot(self.graph_constructor_slots[index].slot)?; + } + for index in 0..self.generation_slots.len() { + let storage = self.generation_slots[index]; + self.drop_slot(storage.owner_slot)?; + self.clear_slot(storage.retired_slot); + } + Ok(()) + } + pub(super) fn drop_all_except(&mut self, reply: ir::StackSlot) -> Result<(), CodegenError> { + for &slot in &self.temporary_slots[..self.next_temporary] { + if slot != reply { + self.drop_slot(slot)?; + } + } + for &slot in self.local_slots.iter().flatten() { + if slot != reply { + self.drop_slot(slot)?; + } + } + for index in (0..self.graph_constructor_slots.len()).rev() { + let slot = self.graph_constructor_slots[index].slot; + if slot != reply { + self.drop_slot(slot)?; + } + } + for index in 0..self.generation_slots.len() { + let storage = self.generation_slots[index]; + if storage.owner_slot != reply { + self.drop_slot(storage.owner_slot)?; + } + self.clear_slot(storage.retired_slot); + } + self.next_temporary = 0; + Ok(()) + } + pub(super) fn static_data(&mut self, bytes: &[u8]) -> Result<(Value, Value), CodegenError> { + self.static_data_with_functions(bytes, &[]) + } + pub(super) fn static_data_with_functions( + &mut self, + bytes: &[u8], + functions: &[(usize, u64)], + ) -> Result<(Value, Value), CodegenError> { + let data = self + .module + .declare_anonymous_data(false, false) + .map_err(|e| CodegenError::Backend(e.to_string()))?; + let mut desc = cranelift_module::DataDescription::new(); + // Object formats require storage even for an empty literal. + desc.define(if bytes.is_empty() { + vec![0].into_boxed_slice() + } else { + bytes.into() + }); + for (offset, function) in functions { + if self.module.target_config().pointer_type() != ir::types::I64 { + return Err(CodegenError::Backend( + "callback conversion requires 64-bit pointers".into(), + )); + } + let function = u32::try_from(*function).map_err(|_| { + CodegenError::Backend("invalid callback conversion function ID".into()) + })?; + let declared = self + .declarations + .get(FunctionId::new(function)) + .ok_or_else(|| { + CodegenError::Backend("callback conversion function is unreachable".into()) + })?; + let reference = self + .module + .declare_func_in_data(declared.native_id, &mut desc); + let offset = u32::try_from(*offset).map_err(|_| { + CodegenError::Backend("callback conversion descriptor is too large".into()) + })?; + desc.write_function_addr(offset, reference); + } + self.module + .define_data(data, &desc) + .map_err(|e| CodegenError::Backend(e.to_string()))?; + let reference = self.module.declare_data_in_func(data, self.builder.func); + let pointer = self.builder.ins().global_value(ir::types::I64, reference); + let length = self + .builder + .ins() + .iconst(ir::types::I64, bytes.len() as i64); + Ok((pointer, length)) + } + pub(super) fn static_bytes(&mut self, text: &str) -> Result<(Value, Value), CodegenError> { + self.static_data(text.as_bytes()) + } + fn construct_builder( + &mut self, + owner: TypeId, + reflection_arguments: &[ReflectionTypeInfo], + span: Span, + ) -> Result { + fn encode_name(layout: &mut Vec, name: &str) -> Result<(), CodegenError> { + let length = u32::try_from(name.len()).map_err(|_| { + CodegenError::Backend("reflected construction name is too long".into()) + })?; + layout.extend_from_slice(&length.to_le_bytes()); + layout.extend_from_slice(name.as_bytes()); + Ok(()) + } + if let [info] = reflection_arguments + && !matches!(info.kind.as_str(), "struct" | "bitfield") + { + // Generic reflected code can instantiate this builder for a type + // whose kind is not constructible. Preserve the interpreter's + // handled put/finish error without inventing a record layout. + let mut layout = b"JC\x05".to_vec(); + encode_name(&mut layout, &info.type_name)?; + encode_name(&mut layout, &info.kind)?; + let (pointer, length) = self.static_data(&layout)?; + let builder = self.leaf(NativeLeaf::BuilderNew, &[pointer, length], true)?; + return self.own_linear(builder); + } + let fields = match self.types.resolve(owner) { + Type::Struct(id) => self + .types + .resolve_struct(*id) + .fields + .iter() + .map(|(name, ty)| (name.clone(), *ty)) + .collect::>(), + Type::Bitfield(id) => self + .types + .resolve_bitfield(*id) + .fields + .iter() + .map(|field| (field.name.clone(), field.ty)) + .collect::>(), + _ => return Err(self.unsupported(span, "reflected construction owner")), + }; + if reflection_arguments.len() != fields.len() + 1 { + return Err(self.unsupported(span, "checked construction field metadata")); + } + let bitfield = matches!(self.types.resolve(owner), Type::Bitfield(_)); + let mut layout = if bitfield { + b"JC\x07".to_vec() + } else { + b"JC\x0a".to_vec() + }; + encode_name( + &mut layout, + &self.construction_owner_name(owner, &reflection_arguments[0]), + )?; + if !bitfield { + encode_name(&mut layout, &reflection_arguments[0].type_name)?; + } + let count = u32::try_from(fields.len()) + .map_err(|_| CodegenError::Backend("too many reflected construction fields".into()))?; + layout.extend_from_slice(&count.to_le_bytes()); + for ((name, field_ty), field_info) in fields.iter().zip(&reflection_arguments[1..]) { + encode_name(&mut layout, name)?; + encode_name(&mut layout, &field_info.type_name)?; + let mut storage_type = *field_ty; + if !bitfield { + while let Type::Refinement { base, .. } = self.types.resolve(storage_type) { + storage_type = *base; + } + } + encode_name(&mut layout, &self.types.type_name(storage_type))?; + } + if let Type::Bitfield(id) = self.types.resolve(owner) { + for field in &self.types.resolve_bitfield(*id).fields { + match field.kind { + BitfieldFieldKind::Payload => layout.push(0), + BitfieldFieldKind::Bits { width } => { + let kind = match self.types.resolve(field.ty) { + Type::Int64 => 1, + Type::Uint64 => 2, + Type::Enum(_) => 3, + _ => return Err(self.unsupported(span, "reflected bitfield field")), + }; + layout.push(kind); + layout.extend_from_slice(&u32::from(width).to_le_bytes()); + if let Type::Enum(enum_id) = self.types.resolve(field.ty) { + let definition = self.types.resolve_enum(*enum_id); + encode_name(&mut layout, &definition.name)?; + let count = u32::try_from(definition.variants.len()) + .map_err(|_| self.unsupported(span, "bitfield enum variants"))?; + layout.extend_from_slice(&count.to_le_bytes()); + for variant in &definition.variants { + encode_name(&mut layout, &variant.name)?; + layout.extend_from_slice(&variant.discriminant.to_le_bytes()); + } + } + } + } + } + } + for (_, field_ty) in &fields { + append_builder_debug_layout(&mut layout, self.types, *field_ty)?; + } + let (pointer, length) = self.static_data(&layout)?; + let builder = self.leaf(NativeLeaf::BuilderNew, &[pointer, length], true)?; + self.own_linear(builder) + } + fn construction_owner_name(&self, owner: TypeId, info: &ReflectionTypeInfo) -> String { + if info.kind == "struct" { + self.types.type_name(owner) + } else { + info.type_name.clone() + } + } + + fn construct_variant_builder( + &mut self, + owner: TypeId, + reflection_arguments: &[ReflectionTypeInfo], + metadata: LoweredValue, + metadata_type: TypeId, + span: Span, + ) -> Result { + let Type::Enum(id) = self.types.resolve(owner) else { + return Err(self.unsupported(span, "reflected variant construction owner")); + }; + let variants = &self.types.resolve_enum(*id).variants; + let fields = variants + .iter() + .map(|variant| variant.fields.len()) + .sum::(); + if reflection_arguments.len() != fields + 1 { + return Err(self.unsupported(span, "checked variant construction metadata")); + } + fn name(layout: &mut Vec, value: &str) -> Result<(), CodegenError> { + let length = u32::try_from(value.len()) + .map_err(|_| CodegenError::Backend("reflected variant name is too long".into()))?; + layout.extend_from_slice(&length.to_le_bytes()); + layout.extend_from_slice(value.as_bytes()); + Ok(()) + } + let mut layout = b"JC\x08".to_vec(); + name(&mut layout, &reflection_arguments[0].type_name)?; + let count = u32::try_from(variants.len()) + .map_err(|_| self.unsupported(span, "reflected variant count"))?; + layout.extend_from_slice(&count.to_le_bytes()); + let mut field_info = reflection_arguments[1..].iter(); + for variant in variants { + name(&mut layout, &variant.name)?; + layout.extend_from_slice(&variant.discriminant.to_le_bytes()); + let count = u32::try_from(variant.fields.len()) + .map_err(|_| self.unsupported(span, "reflected payload field count"))?; + layout.extend_from_slice(&count.to_le_bytes()); + for (field_name, field_ty) in &variant.fields { + name(&mut layout, field_name)?; + name( + &mut layout, + &field_info + .next() + .ok_or_else(|| self.unsupported(span, "checked variant payload type"))? + .type_name, + )?; + let mut storage_type = *field_ty; + while let Type::Refinement { base, .. } = self.types.resolve(storage_type) { + storage_type = *base; + } + name(&mut layout, &self.types.type_name(storage_type))?; + append_builder_debug_layout(&mut layout, self.types, *field_ty)?; + } + } + let (pointer, length) = self.static_data(&layout)?; + let metadata = self.scalar(metadata, span)?; + let debug_layout = super::debug::debug_layout(self.types, metadata_type) + .ok_or_else(|| self.unsupported(span, "variant metadata debug layout"))?; + let (debug_pointer, debug_length) = self.static_data(&debug_layout)?; + let result = self.leaf( + NativeLeaf::BuilderVariantNew, + &[pointer, length, metadata, debug_pointer, debug_length], + true, + )?; + self.own_linear(result) + } + fn construct_machine_builder( + &mut self, + owner: TypeId, + reflection_arguments: &[ReflectionTypeInfo], + metadata: LoweredValue, + metadata_type: TypeId, + span: Span, + ) -> Result { + let (machine_id, state_id) = match self.types.resolve(owner) { + Type::Machine(id) => (*id, None), + Type::MachineState { machine, state } => (*machine, Some(*state)), + _ => return Err(self.unsupported(span, "reflected machine construction owner")), + }; + let machine = self.types.resolve_machine(machine_id); + let fields = machine + .states + .iter() + .map(|state| state.fields.len()) + .sum::(); + if reflection_arguments.len() != fields + 1 { + return Err(self.unsupported(span, "checked machine construction metadata")); + } + fn name(layout: &mut Vec, value: &str) -> Result<(), CodegenError> { + let length = u32::try_from(value.len()) + .map_err(|_| CodegenError::Backend("reflected machine name is too long".into()))?; + layout.extend_from_slice(&length.to_le_bytes()); + layout.extend_from_slice(value.as_bytes()); + Ok(()) + } + let mut layout = b"JC\x09".to_vec(); + name(&mut layout, &reflection_arguments[0].type_name)?; + name(&mut layout, &machine.name)?; + let target_state = state_id + .and_then(|id| machine.state(id)) + .map(|state| state.name.as_str()) + .unwrap_or(""); + name(&mut layout, target_state)?; + let count = u32::try_from(machine.states.len()) + .map_err(|_| self.unsupported(span, "reflected machine state count"))?; + layout.extend_from_slice(&count.to_le_bytes()); + let mut field_info = reflection_arguments[1..].iter(); + for state in &machine.states { + name(&mut layout, &state.name)?; + let count = u32::try_from(state.fields.len()) + .map_err(|_| self.unsupported(span, "reflected state payload count"))?; + layout.extend_from_slice(&count.to_le_bytes()); + for (field_name, field_ty) in &state.fields { + name(&mut layout, field_name)?; + name( + &mut layout, + &field_info + .next() + .ok_or_else(|| self.unsupported(span, "checked machine payload type"))? + .type_name, + )?; + let mut storage_type = *field_ty; + while let Type::Refinement { base, .. } = self.types.resolve(storage_type) { + storage_type = *base; + } + name(&mut layout, &self.types.type_name(storage_type))?; + append_builder_debug_layout(&mut layout, self.types, *field_ty)?; + } + } + let (pointer, length) = self.static_data(&layout)?; + let metadata = self.scalar(metadata, span)?; + let debug_layout = super::debug::debug_layout(self.types, metadata_type) + .ok_or_else(|| self.unsupported(span, "machine metadata debug layout"))?; + let (debug_pointer, debug_length) = self.static_data(&debug_layout)?; + let result = self.leaf( + NativeLeaf::BuilderMachineNew, + &[pointer, length, metadata, debug_pointer, debug_length], + true, + )?; + self.own_linear(result) + } + pub(super) fn literal(&mut self, text: &str) -> Result { + let (pointer, length) = self.static_bytes(text)?; + let value = self.leaf(NativeLeaf::Literal, &[pointer, length], true)?; + self.own(value) + } + fn format_debug_value( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result { + if crate::values::is_formattable(self.types, representation_type(self.types, ty)) { + return self.format_value(value, ty, span); + } + let layout = super::debug::debug_layout(self.types, ty) + .ok_or_else(|| self.unsupported(span, "debug print value layout"))?; + let (layout_pointer, layout_length) = self.static_data(&layout)?; + let (label_pointer, label_length) = self.static_bytes("")?; + let text = self.literal("")?; + let text_handle = self.scalar(text, span)?; + let bits = self.payload_bits(value).0; + self.leaf( + NativeLeaf::DebugAppendAggregate, + &[ + text_handle, + label_pointer, + label_length, + bits, + layout_pointer, + layout_length, + ], + true, + )?; + Ok(text) + } + + fn format_value( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result { + let kind = native_storage_kind(self.types, ty, "format")?; + if kind == ScalarKind::Nothing { + let depth = self.scalar(value, span)?; + let text = self.leaf(NativeLeaf::NothingFormat, &[depth], true)?; + return self.own(text); + } + if kind == ScalarKind::String { + let source = self.scalar(value, span)?; + let text = self.leaf(NativeLeaf::StringTaskFormat, &[source], true)?; + return self.own(text); + } + if let LoweredValue::ScalarTask(_, depth) = value { + let (bits, _) = self.payload_bits(value); + let kind = task_scalar_debug_kind(self.types, ty)? + .ok_or_else(|| self.unsupported(span, "pending scalar formatting"))?; + let kind = self.builder.ins().iconst(ir::types::I32, kind as i64); + let text = self.leaf(NativeLeaf::ScalarTaskFormat, &[bits, kind, depth], true)?; + return self.own(text); + } + let value = self.scalar(value, span)?; + let (leaf, value) = match kind { + ScalarKind::SignedInteger(bits) => ( + NativeLeaf::FromInt, + if bits < 64 { + self.builder.ins().sextend(ir::types::I64, value) + } else { + value + }, + ), + ScalarKind::UnsignedInteger(bits) => ( + NativeLeaf::FromUint, + if bits < 64 { + self.builder.ins().uextend(ir::types::I64, value) + } else { + value + }, + ), + ScalarKind::Float(bits) => ( + NativeLeaf::FromFloat, + if bits < 64 { + self.builder.ins().fpromote(ir::types::F64, value) + } else { + value + }, + ), + ScalarKind::Bool => ( + NativeLeaf::FromBool, + self.builder.ins().uextend(ir::types::I32, value), + ), + _ => return Err(self.unsupported(span, "native formatting")), + }; + let result = self.leaf(leaf, &[value], true)?; + self.own(result) + } + fn concatenate( + &mut self, + left: LoweredValue, + right: LoweredValue, + span: Span, + ) -> Result { + let left = self.scalar(left, span)?; + let right = self.scalar(right, span)?; + let result = self.leaf(NativeLeaf::Concat, &[left, right], true)?; + self.own(result) + } + pub(super) fn interpolate( + &mut self, + segments: &[StringSegment], + span: Span, + ) -> Result { + let mut result = self.literal("")?; + for segment in segments { + let next = match segment { + StringSegment::Text(text) => self.literal(text)?, + StringSegment::Value(expr) => { + let v = self.expression(expr)?; + self.format_value(v, expr.ty, expr.span)? + } + }; + result = self.concatenate(result, next, span)?; + } + Ok(result) + } + fn check_index_count_arguments( + &mut self, + id: IntrinsicId, + evaluated: &[LoweredValue], + span: Span, + ) -> Result<(), CodegenError> { + let (kind, offset) = match id { + IntrinsicId::Range => (0, 0), + IntrinsicId::BytesGet => (1, 1), + IntrinsicId::BytesSlice => (2, 1), + IntrinsicId::StringSlice => (3, 1), + IntrinsicId::StringRepeat => (4, 1), + _ => return Ok(()), + }; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let receiver = if offset == 0 { + zero + } else { + self.scalar(evaluated[0], span)? + }; + let mut depths = [zero; 3]; + for (slot, value) in evaluated.iter().skip(offset).enumerate() { + if let LoweredValue::ScalarTask(_, depth) = value { + depths[slot] = *depth; + } + } + let kind = self.builder.ins().iconst(ir::types::I32, kind); + self.leaf( + NativeLeaf::CheckIndexCountArguments, + &[receiver, kind, depths[0], depths[1], depths[2]], + true, + )?; + Ok(()) + } + pub(super) fn reject_pending_scalars( + &mut self, + evaluated: &[LoweredValue], + message: &str, + ) -> Result<(), CodegenError> { + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let mut depths = [zero; 3]; + let mut has_task = false; + for (slot, value) in evaluated.iter().enumerate() { + if let LoweredValue::ScalarTask(_, depth) = value { + depths[slot] = *depth; + has_task = true; + } + } + if has_task { + let (pointer, length) = self.static_bytes(message)?; + self.leaf( + NativeLeaf::RejectPendingScalars, + &[depths[0], depths[1], depths[2], pointer, length], + true, + )?; + } + Ok(()) + } + fn reject_pending_handle( + &mut self, + value: LoweredValue, + kind: i64, + message: &str, + span: Span, + ) -> Result<(), CodegenError> { + let handle = self.scalar(value, span)?; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + let (pointer, length) = self.static_bytes(message)?; + self.leaf( + NativeLeaf::RejectPendingHandle, + &[handle, kind, pointer, length], + true, + )?; + Ok(()) + } + fn check_string_arguments( + &mut self, + id: IntrinsicId, + evaluated: &[LoweredValue], + span: Span, + ) -> Result<(), CodegenError> { + let (expected, kinds): (&str, &[i64]) = match id { + IntrinsicId::StringJoin => ("a list and a string separator", &[2, 0]), + IntrinsicId::StringReplace => ("three string arguments", &[0, 0, 0]), + IntrinsicId::StringSplit | IntrinsicId::StringIndexOf | IntrinsicId::StringCount => { + ("two string arguments", &[0, 0]) + } + IntrinsicId::StringCharCount + | IntrinsicId::StringChars + | IntrinsicId::StringWords + | IntrinsicId::StringLines + | IntrinsicId::StringLower + | IntrinsicId::StringUpper + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimStart + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringSlugify + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringToLowerFirst + | IntrinsicId::StringIsAlpha + | IntrinsicId::StringIsNumeric => ("a string argument", &[0]), + _ => return Ok(()), + }; + let message = format!("{} expects {expected}", id.canonical_name()); + for (&kind, &value) in kinds.iter().zip(evaluated) { + self.reject_pending_handle(value, kind, &message, span)?; + } + Ok(()) + } + fn check_reflected_field_owner( + &mut self, + metadata: Value, + metadata_type: TypeId, + owner_name: &str, + members: Option<(&[String], Value)>, + kind: i64, + span: Span, + ) -> Result<(), CodegenError> { + let (owner_pointer, owner_length) = self.static_bytes(owner_name)?; + let layout = super::debug::debug_layout(self.types, metadata_type) + .ok_or_else(|| self.unsupported(span, "reflected field metadata debug layout"))?; + let (layout_pointer, layout_length) = self.static_data(&layout)?; + let pointer_type = self.module.target_config().pointer_type(); + let mut member_pointer = self.builder.ins().iconst(pointer_type, 0); + let mut member_length = self.builder.ins().iconst(ir::types::I64, 0); + let has_member = if let Some((names, tag)) = members { + for (index, name) in names.iter().enumerate() { + let index = i64::try_from(index) + .map_err(|_| self.unsupported(span, "reflected member index"))?; + let matches = self.builder.ins().icmp_imm(IntCC::Equal, tag, index); + let (pointer, length) = self.static_bytes(name)?; + member_pointer = self.builder.ins().select(matches, pointer, member_pointer); + member_length = self.builder.ins().select(matches, length, member_length); + } + 1 + } else { + 0 + }; + let has_member = self.builder.ins().iconst(ir::types::I32, has_member); + let kind = self.builder.ins().iconst(ir::types::I32, kind); + self.leaf( + NativeLeaf::ReflectedFieldOwnerCheck, + &[ + metadata, + owner_pointer, + owner_length, + member_pointer, + member_length, + has_member, + kind, + layout_pointer, + layout_length, + ], + true, + )?; + Ok(()) + } + + fn check_reflected_owner_pending( + &mut self, + value: Value, + owner_type: TypeId, + owner_name: &str, + kind: i64, + ) -> Result<(), CodegenError> { + let (name_pointer, name_length) = self.static_bytes(owner_name)?; + // Secret-bearing owners deliberately have no debug layout. Their + // ordinary reflected operations still compile; a pending owner is + // rejected by the runtime without exposing its secret payload. + let layout = super::debug::debug_layout(self.types, owner_type).unwrap_or_default(); + let (layout_pointer, layout_length) = self.static_data(&layout)?; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + self.leaf( + NativeLeaf::ReflectedOwnerPendingCheck, + &[ + value, + name_pointer, + name_length, + layout_pointer, + layout_length, + kind, + ], + true, + )?; + Ok(()) + } + + fn check_reflected_field_metadata_pending( + &mut self, + metadata: Value, + metadata_type: TypeId, + kind: i64, + span: Span, + ) -> Result<(), CodegenError> { + let layout = super::debug::debug_layout(self.types, metadata_type) + .ok_or_else(|| self.unsupported(span, "reflected field metadata debug layout"))?; + let (layout_pointer, layout_length) = self.static_data(&layout)?; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + self.leaf( + NativeLeaf::ReflectedFieldMetadataPendingCheck, + &[metadata, layout_pointer, layout_length, kind], + true, + )?; + Ok(()) + } + + fn select_reflected_field_conversion( + &mut self, + selected: &mut Option, + matches: Value, + actual: TypeId, + requested: TypeId, + span: Span, + ) -> Result<(), CodegenError> { + if !reflected_field_needs_interface_box(self.types, actual, requested) { + return Ok(()); + } + let conversion = interface_conversion(self.types, actual, requested, &[]) + .ok_or_else(|| self.unsupported(span, "reflected interface field conversion"))?; + let (layout, length) = self.static_data(&conversion.encode())?; + let previous = match *selected { + Some(previous) => previous, + None => { + // Other compatible fields already contain an erased interface. + // The descriptor is selected before validation, but conversion + // runs only after the original metadata and pending checks. + let copy = NativeInterfaceConversion::Copy { owned: true }; + let (layout, length) = self.static_data(©.encode())?; + ReflectedFieldConversion { layout, length } + } + }; + *selected = Some(ReflectedFieldConversion { + layout: self.builder.ins().select(matches, layout, previous.layout), + length: self.builder.ins().select(matches, length, previous.length), + }); + Ok(()) + } + + fn reflected_field_value( + &mut self, + owner: Value, + index: Value, + result_type: TypeId, + conversion: Option, + callable: Option, + span: Span, + ) -> Result { + let bits = self.leaf(NativeLeaf::StructField, &[owner, index], true)?; + if let Some(address) = callable { + // The selector has completed its original metadata, schema and + // pending checks. Both leaves borrow the stored descriptor and + // create exactly one owned result; neither invokes its body. + let adapt = self.builder.create_block(); + let exact = self.builder.create_block(); + let joined = self.builder.create_block(); + self.builder.append_block_param(joined, ir::types::I64); + let changed = self.builder.ins().icmp_imm(IntCC::NotEqual, address, 0); + self.builder.ins().brif(changed, adapt, &[], exact, &[]); + self.builder.switch_to_block(adapt); + let adapted = self.leaf(NativeLeaf::FunctionAdapter, &[bits, address], true)?; + self.builder.ins().jump(joined, &[adapted.into()]); + self.builder.switch_to_block(exact); + let cloned = self.leaf(NativeLeaf::StructClone, &[bits], true)?; + self.builder.ins().jump(joined, &[cloned.into()]); + self.builder.switch_to_block(joined); + let owned = self.builder.block_params(joined)[0]; + return self.own(owned); + } + if let Some(conversion) = conversion { + // InterfaceConvert borrows and clones the stored owner. A nominal + // interface-backed refinement needs its own outer identity box; + // cloning the bits as the requested interface would lose that owner. + let owned = self.leaf( + NativeLeaf::InterfaceConvert, + &[bits, conversion.layout, conversion.length], + true, + )?; + return self.own_linear(owned); + } + if is_linear(self.types, result_type) { + return self.clone_linear(LoweredValue::Scalar(bits), result_type, span); + } + if is_string(self.types, result_type) { + let owned = self.leaf(NativeLeaf::Retain, &[bits], true)?; + return self.own(owned); + } + if is_function(self.types, result_type) { + let owned = self.leaf(NativeLeaf::StructClone, &[bits], true)?; + return self.own(owned); + } + let value = self.unpack_payload(bits, result_type, span)?; + if is_task_scalar(self.types, result_type)? { + let depth = self.leaf(NativeLeaf::StructFieldPendingDepth, &[owner, index], true)?; + Ok(LoweredValue::ScalarTask(self.scalar(value, span)?, depth)) + } else { + Ok(value) + } + } + + fn select_reflected_callable( + &mut self, + selected: &mut Option, + matches: Value, + ordinal: usize, + actual: TypeId, + requested: TypeId, + validation: Option<&ReflectedFieldValidation>, + span: Span, + ) -> Result { + let Some(ReflectedFieldValidation::ReadCallable(plans)) = validation else { + return Ok(false); + }; + let row = plans.get(ordinal).ok_or_else(|| { + contract_error( + self.symbol, + span, + "reflected callable field ordinal is absent", + ) + })?; + if row.source_type != actual || row.requested_type != requested { + return Err(contract_error( + self.symbol, + span, + "reflected callable field tuple changed", + )); + } + let ReflectedFieldAction::CallableReturn(plan) = &row.action else { + return Ok(false); + }; + plan.validate_types(self.types) + .map_err(|message| contract_error(self.symbol, span, message))?; + if !plan.validate_original() || plan.is_materialized() { + return Err(contract_error( + self.symbol, + span, + "reflected getter has no exact callable constructor proof", + )); + } + let declared = self.declarations.get(plan.function()).ok_or_else(|| { + contract_error( + self.symbol, + span, + "reflected callable adapter is not reachable", + ) + })?; + let reference = self + .module + .declare_func_in_func(declared.native_id, self.builder.func); + let pointer_type = self.module.target_config().pointer_type(); + let address = self.builder.ins().func_addr(pointer_type, reference); + let address = if pointer_type == ir::types::I64 { + address + } else { + self.builder.ins().uextend(ir::types::I64, address) + }; + let previous = selected.unwrap_or_else(|| self.builder.ins().iconst(ir::types::I64, 0)); + *selected = Some(self.builder.ins().select(matches, address, previous)); + Ok(true) + } + + pub(super) fn reflected_value_ready( + &mut self, + value: LoweredValue, + ty: TypeId, + span: Span, + ) -> Result<(), CodegenError> { + let message = "reflected callback return: expected a ready value"; + if is_task_scalar(self.types, ty)? { + return self.reject_pending_scalars(&[value], message); + } + let kind = match scalar_kind(self.types, ty, "reflected callback return")? { + ScalarKind::Nothing => { + // Unit's I64 value is already its Pending depth, unlike the + // separate depth lane used by numeric and boolean scalars. + let depth = self.scalar(value, span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let (pointer, length) = self.static_bytes(message)?; + self.leaf( + NativeLeaf::RejectPendingScalars, + &[depth, zero, zero, pointer, length], + true, + )?; + return Ok(()); + } + ScalarKind::String => 0, + ScalarKind::Bytes => 1, + ScalarKind::List => 2, + ScalarKind::Set => 5, + ScalarKind::Map => 6, + ScalarKind::Sum => 7, + ScalarKind::Struct + | ScalarKind::Enum + | ScalarKind::Machine + | ScalarKind::Bitfield + | ScalarKind::Function + | ScalarKind::Construction => 4, + _ => return Err(self.unsupported(span, "reflected callback return readiness carrier")), + }; + self.reject_pending_handle(value, kind, message, span) + } + + pub(super) fn intrinsic( + &mut self, + id: IntrinsicId, + type_arguments: &[TypeId], + reflection_arguments: &[ReflectionTypeInfo], + field_validation: Option<&ReflectedFieldValidation>, + args: &[Expression], + order: &[usize], + result_type: TypeId, + span: Span, + ) -> Result { + let mut evaluated = vec![None; args.len()]; + for &index in order { + evaluated[index] = Some(self.argument( + &args[index], + jett_mir::move_values::intrinsic_borrows(id, index, &args[index]), + )?); + } + let evaluated = evaluated + .into_iter() + .map(|argument| { + argument.ok_or_else(|| { + contract_error(self.symbol, span, "intrinsic argument was not evaluated") + }) + }) + .collect::, _>>()?; + let conversion_expected = match id { + IntrinsicId::Float32FromFloat64 | IntrinsicId::Int64FromFloat64 => { + Some("a float64 argument") + } + IntrinsicId::Float64FromInt64 | IntrinsicId::StringFromInt64 => { + Some("an int64 argument") + } + IntrinsicId::StringFromUint64 => Some("a uint64 argument"), + IntrinsicId::StringFromFloat64 => Some("a float64 argument"), + IntrinsicId::StringFromBool => Some("a bool argument"), + _ => None, + }; + if let Some(expected) = conversion_expected { + let message = format!("{} expects {expected}", id.canonical_name()); + self.reject_pending_scalars(&evaluated, &message)?; + } + let conversion_handle_kind = match id { + IntrinsicId::Int64FromString + | IntrinsicId::Uint64FromString + | IntrinsicId::Float64FromString + | IntrinsicId::BytesFromString + | IntrinsicId::BytesFromHex => Some((0, "a string argument")), + IntrinsicId::BytesToString | IntrinsicId::BytesToHex => Some((1, "a bytes argument")), + _ => None, + }; + if let Some((kind, expected)) = conversion_handle_kind { + let message = format!("{} expects {expected}", id.canonical_name()); + self.reject_pending_handle(evaluated[0], kind, &message, span)?; + } + if id == IntrinsicId::GraphicsRun { + let display = evaluated.first().copied().ok_or_else(|| { + contract_error(self.symbol, span, "Graphics authority operand is absent") + })?; + self.reject_pending_handle( + display, + 3, + "graphics.__run expects a Graphics capability", + span, + )?; + return self.graphics_run(type_arguments, args, &evaluated, span); + } + if matches!( + id, + IntrinsicId::TypeName | IntrinsicId::TypeKind | IntrinsicId::TypeHasSecret + ) { + let info = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked reflection operand"))?; + return match id { + IntrinsicId::TypeName => self.literal(&info.type_name), + IntrinsicId::TypeKind => self.literal(&info.kind), + IntrinsicId::TypeHasSecret => Ok(LoweredValue::Scalar( + self.builder + .ins() + .iconst(ir::types::I8, i64::from(info.has_secret)), + )), + _ => unreachable!(), + }; + } + if id == IntrinsicId::TypeConstructStart { + let owner = *type_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked construction type"))?; + return self.construct_builder(owner, reflection_arguments, span); + } + if id == IntrinsicId::TypeConstructVariantStart { + let owner = *type_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked variant construction type"))?; + return self.construct_variant_builder( + owner, + reflection_arguments, + evaluated[0], + args[0].ty, + span, + ); + } + if id == IntrinsicId::TypeConstructMachineStart { + let owner = *type_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked machine construction type"))?; + return self.construct_machine_builder( + owner, + reflection_arguments, + evaluated[0], + args[0].ty, + span, + ); + } + if id == IntrinsicId::TypeConstructPut { + let builder = self.scalar(evaluated[0], span)?; + let field = self.scalar(evaluated[1], span)?; + let (bits, owned) = self.payload_bits(evaluated[2]); + let pending_depth = if let LoweredValue::ScalarTask(_, depth) = evaluated[2] { + Some(depth) + } else { + None + }; + let owned = self.builder.ins().iconst(ir::types::I32, i64::from(owned)); + let owner = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked construction owner"))?; + let field_type = reflection_arguments + .get(1) + .ok_or_else(|| self.unsupported(span, "checked construction field type"))?; + let owner_name = self.construction_owner_name(type_arguments[0], owner); + let (owner_pointer, owner_length) = self.static_bytes(&owner_name)?; + let (type_pointer, type_length) = self.static_bytes(&field_type.type_name)?; + let mut value_type = type_arguments[1]; + if matches!( + self.types.resolve(type_arguments[0]), + Type::Struct(_) | Type::Enum(_) | Type::Machine(_) | Type::MachineState { .. } + ) { + while let Type::Refinement { base, .. } = self.types.resolve(value_type) { + value_type = *base; + } + } + let canonical_type = self.types.type_name(value_type); + let (canonical_pointer, canonical_length) = self.static_bytes(&canonical_type)?; + let metadata_layout = + super::debug::debug_layout(self.types, args[1].ty).ok_or_else(|| { + self.unsupported(span, "construction field metadata debug layout") + })?; + let (layout_pointer, layout_length) = self.static_data(&metadata_layout)?; + let mut arguments = vec![builder, field, bits]; + if let Some(depth) = pending_depth { + arguments.push(depth); + } + arguments.extend([ + owned, + owner_pointer, + owner_length, + type_pointer, + type_length, + canonical_pointer, + canonical_length, + layout_pointer, + layout_length, + ]); + let leaf = if pending_depth.is_some() { + NativeLeaf::BuilderPutScalarTask + } else { + NativeLeaf::BuilderPut + }; + let result = self.leaf(leaf, &arguments, true)?; + for transferred in [evaluated[0], evaluated[2]] { + if let LoweredValue::Owned(_, slot) = transferred { + self.clear_slot(slot); + } + } + return self.own_linear(result); + } + if id == IntrinsicId::TypeConstructFinish { + let builder = self.scalar(evaluated[0], span)?; + let owner = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked construction owner"))?; + let owner_type = type_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked construction type"))?; + let owner_name = self.construction_owner_name(*owner_type, owner); + let (owner_pointer, owner_length) = self.static_bytes(&owner_name)?; + let result = self.leaf( + NativeLeaf::BuilderFinish, + &[builder, owner_pointer, owner_length], + true, + )?; + if let LoweredValue::Owned(_, slot) = evaluated[0] { + self.clear_slot(slot); + } + return self.own_linear(result); + } + if id == IntrinsicId::TypeArg { + let (requested, depth) = self.scalar_task(evaluated[0], span)?; + let count = i64::try_from(evaluated.len() - 1) + .map_err(|_| self.unsupported(span, "reflected type argument count"))?; + let count = self.builder.ins().iconst(ir::types::I64, count); + let reflected = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked type.arg owner"))?; + let (name_pointer, name_length) = self.static_bytes(&reflected.type_name)?; + let checked = self.leaf( + NativeLeaf::TypeArgCheckedIndex, + &[requested, count, depth, name_pointer, name_length], + true, + )?; + let mut selected = self.builder.ins().iconst(ir::types::I64, 0); + for (index, candidate) in evaluated.iter().enumerate().skip(1) { + let index = i64::try_from(index - 1) + .map_err(|_| self.unsupported(span, "reflected type argument index"))?; + let matches = self.builder.ins().icmp_imm(IntCC::Equal, checked, index); + let candidate = self.scalar(*candidate, span)?; + selected = self.builder.ins().select(matches, candidate, selected); + } + let cloned = self.leaf(NativeLeaf::StructClone, &[selected], true)?; + return self.own_linear(cloned); + } + if matches!( + id, + IntrinsicId::TypeVariantValue | IntrinsicId::TypeMachineStateValue + ) { + let value = self.scalar(evaluated[0], span)?; + let owner = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked reflected owner"))?; + self.check_reflected_owner_pending( + value, + args[0].ty, + &owner.type_name, + i64::from(id == IntrinsicId::TypeMachineStateValue), + )?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.leaf(NativeLeaf::StructField, &[value, zero], true)?; + let mut selected = self.scalar(evaluated[1], span)?; + for (index, variant) in evaluated.iter().enumerate().skip(2) { + let index = i64::try_from(index - 1) + .map_err(|_| self.unsupported(span, "reflected variant or state index"))?; + let matches = self.builder.ins().icmp_imm(IntCC::Equal, tag, index); + let variant = self.scalar(*variant, span)?; + selected = self.builder.ins().select(matches, variant, selected); + } + let cloned = self.leaf(NativeLeaf::StructClone, &[selected], true)?; + return self.own_linear(cloned); + } + if id == IntrinsicId::TypeFieldValue { + let field_types = match self.types.resolve(args[0].ty) { + Type::Struct(id) => self + .types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(), + Type::Bitfield(id) => self + .types + .resolve_bitfield(*id) + .fields + .iter() + .map(|field| field.ty) + .collect::>(), + _ => return Err(self.unsupported(span, "reflected field owner")), + }; + let metadata = self.scalar(evaluated[1], span)?; + let owner_name = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked reflected field owner"))? + .type_name + .clone(); + self.check_reflected_field_owner(metadata, args[1].ty, &owner_name, None, 0, span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let requested_index = self.leaf(NativeLeaf::StructField, &[metadata, zero], true)?; + let mut expected = zero; + let mut compatible = zero; + let mut conversion = None; + let mut callable = None; + for (index, field_ty) in field_types.iter().enumerate() { + let candidate = self.scalar(evaluated[index + 2], span)?; + let ordinal = index; + let index = i64::try_from(index) + .map_err(|_| self.unsupported(span, "reflected field index"))?; + let matches = self + .builder + .ins() + .icmp_imm(IntCC::Equal, requested_index, index); + expected = self.builder.ins().select(matches, candidate, expected); + let adapted = self.select_reflected_callable( + &mut callable, + matches, + ordinal, + *field_ty, + result_type, + field_validation, + span, + )?; + if adapted || reflected_field_types_compatible(self.types, *field_ty, result_type) { + compatible = self.builder.ins().select(matches, candidate, compatible); + } + self.select_reflected_field_conversion( + &mut conversion, + matches, + *field_ty, + result_type, + span, + )?; + } + let requested_type = reflection_arguments + .get(1) + .ok_or_else(|| self.unsupported(span, "checked reflected field type"))?; + let (name_pointer, name_length) = self.static_bytes(&requested_type.type_name)?; + let kind = self.builder.ins().iconst(ir::types::I32, 0); + let checked_index = self.leaf( + NativeLeaf::ReflectedCheckedFieldIndex, + &[ + metadata, + expected, + compatible, + name_pointer, + name_length, + kind, + ], + true, + )?; + let owner = self.scalar(evaluated[0], span)?; + self.check_reflected_owner_pending(owner, args[0].ty, &owner_name, 2)?; + return self.reflected_field_value( + owner, + checked_index, + result_type, + conversion, + callable, + span, + ); + } + if matches!( + id, + IntrinsicId::TypeVariantFieldValue | IntrinsicId::TypeMachineFieldValue + ) { + let (field_groups, owner_name, member_names) = + match (id, self.types.resolve(args[0].ty)) { + (IntrinsicId::TypeVariantFieldValue, Type::Enum(enum_id)) => { + let definition = self.types.resolve_enum(*enum_id); + let owner_name = reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked reflected enum owner"))? + .type_name + .clone(); + ( + definition + .variants + .iter() + .map(|variant| { + variant.fields.iter().map(|(_, ty)| *ty).collect::>() + }) + .collect::>(), + owner_name, + definition + .variants + .iter() + .map(|variant| variant.name.clone()) + .collect::>(), + ) + } + (IntrinsicId::TypeMachineFieldValue, Type::Machine(machine_id)) + | ( + IntrinsicId::TypeMachineFieldValue, + Type::MachineState { + machine: machine_id, + .. + }, + ) => { + let definition = self.types.resolve_machine(*machine_id); + ( + definition + .states + .iter() + .map(|state| { + state.fields.iter().map(|(_, ty)| *ty).collect::>() + }) + .collect::>(), + definition.name.clone(), + definition + .states + .iter() + .map(|state| state.name.clone()) + .collect::>(), + ) + } + _ => return Err(self.unsupported(span, "reflected payload field owner")), + }; + let owner = self.scalar(evaluated[0], span)?; + let metadata = self.scalar(evaluated[1], span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let tag = self.leaf(NativeLeaf::StructField, &[owner, zero], true)?; + let kind = if id == IntrinsicId::TypeMachineFieldValue { + 2 + } else { + 1 + }; + self.check_reflected_field_metadata_pending(metadata, args[1].ty, kind, span)?; + let reflected_owner_name = &reflection_arguments + .first() + .ok_or_else(|| self.unsupported(span, "checked reflected payload owner"))? + .type_name; + self.check_reflected_owner_pending( + owner, + args[0].ty, + reflected_owner_name, + if id == IntrinsicId::TypeMachineFieldValue { + 4 + } else { + 3 + }, + )?; + self.check_reflected_field_owner( + metadata, + args[1].ty, + &owner_name, + Some((&member_names, tag)), + kind, + span, + )?; + let requested_index = self.leaf(NativeLeaf::StructField, &[metadata, zero], true)?; + let mut expected = zero; + let mut compatible = zero; + let mut conversion = None; + let mut callable = None; + let mut argument_index = 2; + for (variant_index, fields) in field_groups.iter().enumerate() { + for (field_index, field_ty) in fields.iter().enumerate() { + let ordinal = argument_index - 2; + let candidate = self.scalar(evaluated[argument_index], span)?; + argument_index += 1; + let variant_index = i64::try_from(variant_index) + .map_err(|_| self.unsupported(span, "reflected variant index"))?; + let field_index = i64::try_from(field_index) + .map_err(|_| self.unsupported(span, "reflected field index"))?; + let variant_matches = + self.builder + .ins() + .icmp_imm(IntCC::Equal, tag, variant_index); + let field_matches = + self.builder + .ins() + .icmp_imm(IntCC::Equal, requested_index, field_index); + let matches = self.builder.ins().band(variant_matches, field_matches); + expected = self.builder.ins().select(matches, candidate, expected); + let adapted = self.select_reflected_callable( + &mut callable, + matches, + ordinal, + *field_ty, + result_type, + field_validation, + span, + )?; + if adapted + || reflected_field_types_compatible(self.types, *field_ty, result_type) + { + compatible = self.builder.ins().select(matches, candidate, compatible); + } + self.select_reflected_field_conversion( + &mut conversion, + matches, + *field_ty, + result_type, + span, + )?; + } + } + let requested_type = reflection_arguments + .get(1) + .ok_or_else(|| self.unsupported(span, "checked reflected payload field type"))?; + let (name_pointer, name_length) = self.static_bytes(&requested_type.type_name)?; + let kind = if id == IntrinsicId::TypeMachineFieldValue { + 2 + } else { + 1 + }; + let kind = self.builder.ins().iconst(ir::types::I32, kind); + let checked_index = self.leaf( + NativeLeaf::ReflectedCheckedFieldIndex, + &[ + metadata, + expected, + compatible, + name_pointer, + name_length, + kind, + ], + true, + )?; + let one = self.builder.ins().iconst(ir::types::I64, 1); + let slot = self.builder.ins().iadd(checked_index, one); + return self.reflected_field_value( + owner, + slot, + result_type, + conversion, + callable, + span, + ); + } + if id == IntrinsicId::Range { + self.check_index_count_arguments(id, &evaluated, span)?; + let zero = self.builder.ins().iconst(ir::types::I64, 0); + let one = self.builder.ins().iconst(ir::types::I64, 1); + let mut native = evaluated + .iter() + .map(|v| self.scalar(*v, span)) + .collect::, _>>()?; + if native.len() == 1 { + native.insert(0, zero); + } + if native.len() == 2 { + native.push(one); + } + let value = self.leaf(NativeLeaf::Range, &native, true)?; + return self.own_linear(value); + } + if crate::values::list_intrinsic(id) { + return self.list_intrinsic(id, args, &evaluated, result_type, span); + } + if crate::values::set_intrinsic(id) { + return self.set_intrinsic(id, &evaluated, result_type, span); + } + if crate::values::map_intrinsic(id) { + return self.map_intrinsic(id, &evaluated, result_type, span); + } + if let Some(leaf) = crate::values::bytes_leaf(id) { + self.check_index_count_arguments(id, &evaluated, span)?; + let pending_check = match id { + IntrinsicId::BytesLength + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CryptoMd5 => Some(("a bytes argument", 1)), + IntrinsicId::BytesConcat => Some(("two bytes arguments", 1)), + IntrinsicId::CryptoHmacSha256 => Some(("bytes arguments", 1)), + IntrinsicId::EncodingBase64Decode + | IntrinsicId::EncodingHexDecode + | IntrinsicId::EncodingUrlEncode + | IntrinsicId::EncodingUrlDecode + | IntrinsicId::EncodingFormEncode + | IntrinsicId::EncodingFormDecode + | IntrinsicId::CsvParse + | IntrinsicId::CsvParseWithHeader => Some(("a string argument", 0)), + IntrinsicId::CsvStringify => Some(("a list argument", 2)), + _ => None, + }; + if let Some((expected, kind)) = pending_check { + let message = format!("{} expects {expected}", id.canonical_name()); + for &value in &evaluated { + self.reject_pending_handle(value, kind, &message, span)?; + } + } + let arguments = evaluated + .iter() + .map(|v| self.scalar(*v, span)) + .collect::, _>>()?; + let v = self.leaf(leaf, &arguments, true)?; + return match leaf { + NativeLeaf::BytesLength => Ok(LoweredValue::Scalar(v)), + NativeLeaf::BytesToHex + | NativeLeaf::EncodingBase64Encode + | NativeLeaf::EncodingUrlEncode + | NativeLeaf::EncodingFormEncode => self.own(v), + NativeLeaf::CsvStringify => self.own(v), + _ => self.own_linear(v), + }; + } + if matches!(id, IntrinsicId::MathAverage | IntrinsicId::MathMedian) { + let list = self.scalar(evaluated[0], span)?; + let kind = crate::values::math_aggregate_kind(self.types, args[0].ty) + .ok_or_else(|| self.unsupported(span, "numeric list kind"))?; + let kind = self + .builder + .ins() + .iconst(ir::types::I32, i64::from(kind as u32)); + let leaf = if id == IntrinsicId::MathAverage { + NativeLeaf::MathAverage + } else { + NativeLeaf::MathMedian + }; + return Ok(LoweredValue::Scalar(self.leaf( + leaf, + &[list, kind], + true, + )?)); + } + if let Some(leaf) = crate::values::math_leaf(id, args.first().map(|a| a.ty)) { + let expected = match id { + IntrinsicId::MathKernelAbs + | IntrinsicId::MathAbs + | IntrinsicId::MathSqrt + | IntrinsicId::MathFloor + | IntrinsicId::MathCeil + | IntrinsicId::MathRound + | IntrinsicId::MathLog + | IntrinsicId::MathLog2 + | IntrinsicId::MathLog10 + | IntrinsicId::MathSin + | IntrinsicId::MathCos + | IntrinsicId::MathTan => Some("a numeric argument"), + IntrinsicId::MathKernelMin + | IntrinsicId::MathMin + | IntrinsicId::MathKernelMax + | IntrinsicId::MathMax => Some("two arguments of the same numeric type"), + IntrinsicId::MathPow => Some("numeric arguments"), + IntrinsicId::MathClamp => Some("three arguments of the same numeric type"), + IntrinsicId::MathMod | IntrinsicId::MathGcd | IntrinsicId::MathLcm => { + Some("two int64 arguments") + } + IntrinsicId::MathFactorial => Some("an int64 argument"), + _ => None, + }; + if let Some(expected) = expected { + let message = format!("{} expects {expected}", id.canonical_name()); + self.reject_pending_scalars(&evaluated, &message)?; + } + let arguments = evaluated + .iter() + .map(|v| self.scalar(*v, span)) + .collect::, _>>()?; + return Ok(LoweredValue::Scalar(self.leaf(leaf, &arguments, true)?)); + } + if let Some(leaf) = crate::values::string_leaf(id) { + self.check_index_count_arguments(id, &evaluated, span)?; + self.check_string_arguments(id, &evaluated, span)?; + let native_args = evaluated + .iter() + .map(|v| self.scalar(*v, span)) + .collect::, _>>()?; + let value = self.leaf(leaf, &native_args, true)?; + return match leaf { + NativeLeaf::CharCount | NativeLeaf::StringCount => Ok(LoweredValue::Scalar(value)), + NativeLeaf::StringIndexOf => self.own_linear(value), + NativeLeaf::IsAlpha | NativeLeaf::IsNumeric => Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, value), + )), + _ => self.own(value), + }; + } + match id { + IntrinsicId::UuidNew => { + let value = self.leaf(NativeLeaf::UuidNew, &[], true)?; + self.own(value) + } + IntrinsicId::ClockNow => { + let message = format!("{} expects Clock", id.canonical_name()); + self.reject_pending_handle(evaluated[0], 3, &message, span)?; + let authority = self.scalar(evaluated[0], span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::ClockNow, + &[authority], + true, + )?)) + } + IntrinsicId::RandomBounded => { + let message = format!( + "{} expects Random and two int64 arguments", + id.canonical_name() + ); + self.reject_pending_handle(evaluated[0], 3, &message, span)?; + self.reject_pending_scalars(&evaluated[1..], &message)?; + let arguments = evaluated + .iter() + .map(|value| self.scalar(*value, span)) + .collect::, _>>()?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::RandomBounded, + &arguments, + true, + )?)) + } + IntrinsicId::RandomUnitFloat64 => { + let message = format!("{} expects Random", id.canonical_name()); + self.reject_pending_handle(evaluated[0], 3, &message, span)?; + let authority = self.scalar(evaluated[0], span)?; + Ok(LoweredValue::Scalar(self.leaf( + NativeLeaf::RandomUnit53, + &[authority], + true, + )?)) + } + IntrinsicId::RandomBool => { + let message = format!("{} expects Random", id.canonical_name()); + self.reject_pending_handle(evaluated[0], 3, &message, span)?; + let authority = self.scalar(evaluated[0], span)?; + let value = self.leaf(NativeLeaf::RandomBool, &[authority], true)?; + Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, value), + )) + } + IntrinsicId::EnvironmentGet => { + let authority_error = format!("{} expects Environment", id.canonical_name()); + self.reject_pending_handle(evaluated[0], 3, &authority_error, span)?; + let key_error = format!("{} expects a string key", id.canonical_name()); + self.reject_pending_handle(evaluated[1], 0, &key_error, span)?; + let authority = self.scalar(evaluated[0], span)?; + let key = self.scalar(evaluated[1], span)?; + let value = self.leaf(NativeLeaf::EnvironmentGet, &[authority, key], true)?; + self.own_linear(value) + } + IntrinsicId::EnvironmentArgs => { + let message = format!("{} expects Environment", id.canonical_name()); + self.reject_pending_handle(evaluated[0], 3, &message, span)?; + let authority = self.scalar(evaluated[0], span)?; + let value = self.leaf(NativeLeaf::EnvironmentArgs, &[authority], true)?; + self.own_linear(value) + } + IntrinsicId::SecretCompare => { + let message = "secret.compare expects two strings or two byte strings"; + let kind = if is_string(self.types, args[0].ty) { + 0 + } else { + 1 + }; + for &value in &evaluated { + self.reject_pending_handle(value, kind, message, span)?; + } + let left = self.scalar(evaluated[0], span)?; + let right = self.scalar(evaluated[1], span)?; + let leaf = if is_string(self.types, args[0].ty) { + NativeLeaf::SecretCompareString + } else { + NativeLeaf::SecretCompareBytes + }; + let result = self.leaf(leaf, &[left, right], true)?; + Ok(LoweredValue::Scalar( + self.builder.ins().ireduce(ir::types::I8, result), + )) + } + IntrinsicId::SecretRedact => self.literal("***"), + IntrinsicId::BitfieldToBytes => self.encode_bitfield(evaluated[0], args[0].ty, span), + IntrinsicId::BitfieldFromBytes => self.decode_bitfield(evaluated[0], result_type, span), + IntrinsicId::Float32FromFloat64 => { + let input = self.scalar(evaluated[0], span)?; + Ok(LoweredValue::Scalar( + self.builder.ins().fdemote(ir::types::F32, input), + )) + } + IntrinsicId::Float64FromInt64 | IntrinsicId::Int64FromFloat64 => { + let input = self.scalar(evaluated[0], span)?; + let leaf = if id == IntrinsicId::Float64FromInt64 { + NativeLeaf::FloatFromInt + } else { + NativeLeaf::IntFromFloat + }; + let result = self.leaf(leaf, &[input], true)?; + self.own_linear(result) + } + IntrinsicId::StringFromInt64 + | IntrinsicId::StringFromUint64 + | IntrinsicId::StringFromFloat64 + | IntrinsicId::StringFromBool => self.format_value(evaluated[0], args[0].ty, span), + IntrinsicId::StdoutWrite => { + let authority = self.scalar(evaluated[0], span)?; + let text = self.scalar(evaluated[1], span)?; + self.leaf(NativeLeaf::Stdout, &[authority, text], true)?; + Ok(self.nothing()) + } + IntrinsicId::Print | IntrinsicId::Println => { + // Evaluate every argument before the first output effect. + let mut output = self.literal("")?; + for (index, (arg, value)) in args.iter().zip(evaluated).enumerate() { + if index != 0 { + let space = self.literal(" ")?; + output = self.concatenate(output, space, span)?; + } + let text = self.format_debug_value(value, arg.ty, arg.span)?; + output = self.concatenate(output, text, span)?; + } + if id == IntrinsicId::Println { + let newline = self.literal("\n")?; + output = self.concatenate(output, newline, span)?; + } + let output = self.scalar(output, span)?; + self.leaf(NativeLeaf::DebugPrint, &[output], true)?; + Ok(self.nothing()) + } + _ => Err(self.unsupported(span, "runtime intrinsic")), + } + } +} + +#[cfg(test)] +mod reflected_field_tests { + use super::*; + use jett_types::InterfaceDef; + + fn interface(types: &mut TypeInterner, name: &str) -> TypeId { + let id = types.add_interface(InterfaceDef { + name: name.into(), + methods: vec![], + }); + types.intern(Type::Interface(id)) + } + + fn refinement(types: &mut TypeInterner, name: &str, base: TypeId) -> TypeId { + types.intern(Type::Refinement { + name: name.into(), + base, + }) + } + + #[test] + fn reflected_interface_conversion_retains_the_declared_refinement_owner() { + let mut types = TypeInterner::new(); + let named = interface(&mut types, "app.Named"); + let selected = refinement(&mut types, "app.Selected", named); + let nested = refinement(&mut types, "app.Nested", selected); + for actual in [selected, nested] { + assert!(reflected_field_types_compatible(&types, actual, named)); + assert!(reflected_field_needs_interface_box(&types, actual, named)); + assert_eq!( + interface_conversion(&types, actual, named, &[]), + Some(NativeInterfaceConversion::Box { + concrete: u64::from(actual.index()), + owned: true, + nothing: false, + layout: debug::debug_layout(&types, actual).unwrap(), + }), + ); + } + assert!(!reflected_field_needs_interface_box(&types, named, named)); + assert_eq!( + interface_conversion(&types, named, named, &[]), + Some(NativeInterfaceConversion::Copy { owned: true }), + ); + } + + #[test] + fn reflected_interface_conversion_does_not_add_nominal_casts_or_admission() { + let mut types = TypeInterner::new(); + let named = interface(&mut types, "app.Named"); + let other = interface(&mut types, "app.Other"); + let selected = refinement(&mut types, "app.Selected", named); + let sibling = refinement(&mut types, "app.Sibling", named); + let nested = refinement(&mut types, "app.Nested", selected); + let count = refinement(&mut types, "app.Count", TypeInterner::INT64); + for (actual, requested) in [ + (selected, selected), + (selected, sibling), + (nested, selected), + (named, selected), + (count, TypeInterner::INT64), + ] { + assert!(reflected_field_types_compatible(&types, actual, requested)); + assert!(!reflected_field_needs_interface_box( + &types, actual, requested + )); + } + for (actual, requested) in [ + (selected, other), + (TypeInterner::INT64, named), + (TypeInterner::STRING, named), + ] { + assert!(!reflected_field_types_compatible(&types, actual, requested)); + assert!(!reflected_field_needs_interface_box( + &types, actual, requested + )); + } + let actual = types.intern(Type::List(selected)); + let requested = types.intern(Type::List(named)); + assert!(!reflected_field_types_compatible(&types, actual, requested)); + assert!(!reflected_field_needs_interface_box( + &types, actual, requested + )); + } + + #[test] + fn reflected_interface_conversion_preserves_secret_admission_and_layout() { + let mut types = TypeInterner::new(); + let named = interface(&mut types, "app.Named"); + let selected = refinement(&mut types, "app.Selected", named); + let secret_named = types.intern(Type::Secret(named)); + let secret_selected = types.intern(Type::Secret(selected)); + assert!(reflected_field_needs_interface_box( + &types, + secret_selected, + secret_named, + )); + assert_eq!( + interface_conversion(&types, secret_selected, secret_named, &[]), + Some(NativeInterfaceConversion::Box { + concrete: u64::from(selected.index()), + owned: true, + nothing: false, + layout: debug::debug_layout(&types, selected).unwrap(), + }), + ); + for (actual, requested) in [(selected, secret_named), (secret_selected, named)] { + assert!(!reflected_field_types_compatible(&types, actual, requested)); + assert!(!reflected_field_needs_interface_box( + &types, actual, requested + )); + } + } + + fn wrapper(types: &mut TypeInterner, kind: usize, child: TypeId) -> TypeId { + types.intern(match kind { + 0 => Type::List(child), + 1 => Type::Set(child), + 2 => Type::Map(child, child), + 3 => Type::Optional(child), + 4 => Type::Result(child, child), + _ => panic!("test wrapper kind"), + }) + } + + #[test] + fn reflected_builtin_carriers_recurse_through_all_shapes_and_repeated_children() { + let mut types = TypeInterner::new(); + let positive = refinement(&mut types, "app.Positive", TypeInterner::INT64); + let higher = refinement(&mut types, "app.Higher", positive); + let sibling = refinement(&mut types, "app.Sibling", TypeInterner::INT64); + let text = refinement(&mut types, "app.Text", TypeInterner::STRING); + for kind in 0..5 { + for (actual, requested) in [ + (TypeInterner::INT64, positive), + (higher, positive), + (sibling, higher), + (positive, TypeInterner::INT64), + (TypeInterner::STRING, text), + ] { + let actual = wrapper(&mut types, kind, actual); + let requested = wrapper(&mut types, kind, requested); + assert!( + reflected_field_types_compatible(&types, actual, requested), + "shape {kind}" + ); + assert!(!reflected_field_needs_interface_box( + &types, actual, requested + )); + } + } + let mut actual = TypeInterner::INT64; + let mut requested = higher; + // Four full rounds distinguish arbitrary recursion from a one-level fix. + for _ in 0..4 { + for kind in 0..5 { + actual = wrapper(&mut types, kind, actual); + requested = wrapper(&mut types, kind, requested); + } + } + assert!(reflected_field_types_compatible(&types, actual, requested)); + let actual_root = refinement(&mut types, "app.ActualTree", actual); + let requested_root = refinement(&mut types, "app.RequestedTree", requested); + assert!(reflected_field_types_compatible( + &types, + actual_root, + requested_root + )); + assert!(reflected_field_types_compatible( + &types, + actual_root, + requested + )); + } + + #[test] + fn reflected_builtin_carriers_preserve_secrecy_and_refuse_nested_adapters_and_nominal_casts() { + let mut types = TypeInterner::new(); + let positive = refinement(&mut types, "app.Positive", TypeInterner::INT64); + let secret_plain = types.intern(Type::Secret(TypeInterner::INT64)); + let secret_positive = types.intern(Type::Secret(positive)); + let twice_secret = types.intern(Type::Secret(secret_plain)); + let named = interface(&mut types, "app.Named"); + let selected = refinement(&mut types, "app.Selected", named); + let callback_plain = types.intern(Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + let callback_refined = types.intern(Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: positive, + }); + let first = types.add_struct(jett_types::StructDef { + name: "app.Holder[int64]".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: vec![], + }); + let first = types.intern(Type::Struct(first)); + let second = types.add_struct(jett_types::StructDef { + name: "app.Holder[app.Positive]".into(), + fields: vec![("value".into(), positive)], + methods: vec![], + }); + let second = types.intern(Type::Struct(second)); + for kind in [0, 2, 3, 4] { + let actual = wrapper(&mut types, kind, secret_plain); + let requested = wrapper(&mut types, kind, secret_positive); + // Same secrecy placement is raw-compatible; checked planning then + // refuses to establish a new invariant underneath that secret. + assert!(reflected_field_types_compatible(&types, actual, requested)); + for (a, b) in [ + (secret_plain, positive), + (positive, secret_positive), + (twice_secret, secret_positive), + (selected, named), + (named, selected), + (callback_plain, callback_refined), + (first, second), + (TypeInterner::INT32, positive), + ] { + let a = wrapper(&mut types, kind, a); + let b = wrapper(&mut types, kind, b); + assert!( + !reflected_field_types_compatible(&types, a, b), + "shape {kind}" + ); + } + } + let list = types.intern(Type::List(positive)); + let set = types.intern(Type::Set(positive)); + let optional = types.intern(Type::Optional(positive)); + assert!(!reflected_field_types_compatible(&types, list, set)); + assert!(!reflected_field_types_compatible(&types, list, optional)); + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for index in 0..=types.len() { + invalid = refinement( + &mut foreign, + &format!("app.Foreign{index}"), + TypeInterner::INT64, + ); + } + assert!(invalid.index() as usize >= types.len()); + assert!(!reflected_field_types_compatible(&types, invalid, positive)); + assert!(!reflected_field_types_compatible(&types, positive, invalid)); + } + + #[test] + fn interface_box_owner_removes_only_shared_outer_secret_qualifiers() { + let mut types = TypeInterner::new(); + let named = interface(&mut types, "app.Named"); + let selected = refinement(&mut types, "app.Selected", named); + let nested = refinement(&mut types, "app.Nested", selected); + let secret_named = types.intern(Type::Secret(named)); + let secret_nested = types.intern(Type::Secret(nested)); + let twice_secret_nested = types.intern(Type::Secret(secret_nested)); + let secret_backed = refinement(&mut types, "app.Hidden", secret_named); + for (source, target, owner) in [ + (nested, secret_named, nested), + (secret_nested, secret_named, nested), + (secret_nested, named, secret_nested), + (twice_secret_nested, secret_named, secret_nested), + (secret_backed, secret_named, secret_backed), + ] { + assert_eq!(interface_box_owner(&types, source, target), owner); + assert_eq!( + interface_conversion(&types, source, target, &[]), + Some(NativeInterfaceConversion::Box { + concrete: u64::from(owner.index()), + owned: true, + nothing: false, + layout: debug::debug_layout(&types, owner).unwrap(), + }), + ); + } + } + + #[test] + fn qualified_interface_box_layout_keeps_payload_secrets_redacted() { + let mut types = TypeInterner::new(); + let named = interface(&mut types, "app.Named"); + let secret_named = types.intern(Type::Secret(named)); + let secret_string = types.intern(Type::Secret(TypeInterner::STRING)); + let id = types.add_struct(jett_types::StructDef { + name: "app.Record".into(), + fields: vec![("token".into(), secret_string)], + methods: vec![], + }); + let record = types.intern(Type::Struct(id)); + let selected = refinement(&mut types, "app.Selected", record); + let secret_selected = types.intern(Type::Secret(selected)); + let layout = debug::debug_layout(&types, selected).unwrap(); + assert_eq!( + layout.last(), + Some(&(jett_runtime::native_abi::values::NativeDebugTag::Redacted as u8)), + ); + assert_eq!( + interface_conversion(&types, secret_selected, secret_named, &[]), + Some(NativeInterfaceConversion::Box { + concrete: u64::from(selected.index()), + owned: true, + nothing: false, + layout, + }), + ); + assert_eq!( + debug::debug_layout(&types, secret_named).unwrap().last(), + Some(&(jett_runtime::native_abi::values::NativeDebugTag::Redacted as u8)), + ); + assert_eq!( + interface_conversion(&types, secret_string, named, &[]), + Some(NativeInterfaceConversion::Box { + concrete: u64::from(secret_string.index()), + owned: true, + nothing: false, + layout: debug::debug_layout(&types, secret_string).unwrap(), + }), + ); + } +} diff --git a/crates/jett_codegen_cranelift/src/error.rs b/crates/jett_codegen_cranelift/src/error.rs new file mode 100644 index 00000000..99f1675f --- /dev/null +++ b/crates/jett_codegen_cranelift/src/error.rs @@ -0,0 +1,120 @@ +use std::fmt; + +use jett_common::Span; +use jett_mir::ValidationError; + +/// A failure at the MIR-to-object boundary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CodegenError { + InvalidTarget { + target: String, + message: String, + }, + UnsupportedTarget { + requested: String, + supported: String, + }, + InvalidMir(Vec), + InvalidMirContract { + function: String, + span: Span, + message: String, + }, + UnsupportedType { + type_name: String, + context: String, + }, + UnsupportedMir { + function: String, + span: Span, + construct: String, + }, + MissingProgramEntry { + function_id: u32, + }, + DuplicateProgramEntry { + function_id: u32, + }, + UnreachableProgramEntry { + function_id: u32, + }, + IncompatibleProgramEntry { + function_id: u32, + message: String, + }, + DuplicateSymbol(String), + Backend(String), +} + +impl fmt::Display for CodegenError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidTarget { target, message } => { + write!(formatter, "invalid target `{target}`: {message}") + } + Self::UnsupportedTarget { + requested, + supported, + } => write!( + formatter, + "target `{requested}` is unsupported; this compiler supports only `{supported}`" + ), + Self::InvalidMir(errors) => { + write!( + formatter, + "MIR validation failed with {} error(s)", + errors.len() + ) + } + Self::InvalidMirContract { + function, + span, + message, + } => write!( + formatter, + "invalid MIR contract in `{function}` at {span:?}: {message}" + ), + Self::UnsupportedType { type_name, context } => { + write!(formatter, "type `{type_name}` is unsupported in {context}") + } + Self::UnsupportedMir { + function, + span, + construct, + } => write!( + formatter, + "unsupported MIR construct `{construct}` in `{function}` at {span:?}" + ), + Self::MissingProgramEntry { function_id } => { + write!( + formatter, + "program entry function {function_id} is absent from the MIR program" + ) + } + Self::DuplicateProgramEntry { function_id } => write!( + formatter, + "program entry function ID {function_id} occurs more than once in the MIR program" + ), + Self::UnreachableProgramEntry { function_id } => write!( + formatter, + "program entry function {function_id} is not reachable from a project function" + ), + Self::IncompatibleProgramEntry { + function_id, + message, + } => write!( + formatter, + "program entry function {function_id} has an incompatible contract: {message}" + ), + Self::DuplicateSymbol(symbol) => { + write!( + formatter, + "multiple MIR functions map to native symbol `{symbol}`" + ) + } + Self::Backend(message) => formatter.write_str(message), + } + } +} + +impl std::error::Error for CodegenError {} diff --git a/crates/jett_codegen_cranelift/src/lib.rs b/crates/jett_codegen_cranelift/src/lib.rs new file mode 100644 index 00000000..8535ead2 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/lib.rs @@ -0,0 +1,21 @@ +//! Cranelift ahead-of-time object generation for validated Jett MIR. +//! +//! This crate deliberately consumes MIR rather than source or AST nodes. The +//! current subset supports scalar primitives, owned immutable strings, typed +//! runtime leaves, and explicit control flow. Unsupported MIR is rejected +//! before object generation; there is no interpreter fallback. + +mod emit; +mod error; +mod mangle; +mod reachability; +mod values; +mod verify; + +pub use emit::{ + CodegenOptions, JETT_AOT_ENTRY_SUCCESS_V1, JETT_AOT_ENTRY_SYMBOL_V1, ObjectArtifact, + emit_host_object, emit_host_program_object, emit_host_program_object_with_options, + emit_object_for_target, emit_program_object_for_target, host_target, +}; +pub use error::CodegenError; +pub use mangle::symbol_name; diff --git a/crates/jett_codegen_cranelift/src/mangle.rs b/crates/jett_codegen_cranelift/src/mangle.rs new file mode 100644 index 00000000..cc900448 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/mangle.rs @@ -0,0 +1,246 @@ +use std::fmt::Write; + +use jett_common::SourceOrigin; +use jett_hir::{DeclarationKind, FunctionIdentity}; +use jett_types::{ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use sha2::{Digest, Sha256}; + +use crate::CodegenError; + +/// Produce the stable native symbol for one concrete Jett function. +/// +/// The encoding contains canonical declaration and structural type identities; +/// it never embeds session-local definition or type indexes. A versioned +/// SHA-256 digest keeps adversarially nested generic names within object-format +/// and linker symbol-length limits. +pub fn symbol_name( + identity: &FunctionIdentity, + types: &TypeInterner, +) -> Result { + let mut canonical = String::new(); + encode_origin(&mut canonical, &identity.declaration.origin); + push_component(&mut canonical, &identity.declaration.namespace); + push_component(&mut canonical, &identity.declaration.name); + canonical.push(match identity.declaration.kind { + DeclarationKind::Function => 'f', + DeclarationKind::Verify => 'v', + DeclarationKind::Property => 'p', + DeclarationKind::Method => 'm', + DeclarationKind::ActorConstructor => 'a', + DeclarationKind::ActorHandler => 'h', + DeclarationKind::RefinementPredicate => 'r', + }); + canonical.push_str(&identity.type_arguments.len().to_string()); + canonical.push('_'); + for argument in &identity.type_arguments { + let encoded = canonical_type(*argument, types)?; + push_component(&mut canonical, &encoded); + } + push_category( + &mut canonical, + "type-argument-kinds", + identity.specialization.type_argument_kinds.len(), + ); + for kind in &identity.specialization.type_argument_kinds { + push_component(&mut canonical, kind); + } + if !identity.specialization.type_argument_reflections.is_empty() { + push_category( + &mut canonical, + "type-argument-reflections", + identity.specialization.type_argument_reflections.len(), + ); + for info in &identity.specialization.type_argument_reflections { + encode_reflection_type_info(&mut canonical, info); + } + } + push_category( + &mut canonical, + "type-info-kinds", + identity.specialization.type_info_kinds.len(), + ); + for (index, kind) in &identity.specialization.type_info_kinds { + push_component(&mut canonical, &index.to_string()); + push_component(&mut canonical, kind); + } + push_category( + &mut canonical, + "type-info-primitives", + identity.specialization.type_info_primitives.len(), + ); + for (index, primitive) in &identity.specialization.type_info_primitives { + push_component(&mut canonical, &index.to_string()); + match primitive { + Some(primitive) => { + push_component(&mut canonical, "some"); + push_component(&mut canonical, primitive); + } + None => push_component(&mut canonical, "none"), + } + } + push_category( + &mut canonical, + "type-kind-values", + identity.specialization.type_kind_values.len(), + ); + for (index, kind) in &identity.specialization.type_kind_values { + push_component(&mut canonical, &index.to_string()); + push_component(&mut canonical, kind); + } + push_category( + &mut canonical, + "type-primitive-values", + identity.specialization.type_primitive_values.len(), + ); + for (index, primitive) in &identity.specialization.type_primitive_values { + push_component(&mut canonical, &index.to_string()); + push_component(&mut canonical, primitive); + } + + if !identity.scoped_type_bindings.is_empty() { + push_category( + &mut canonical, + "scoped-type-bindings", + identity.scoped_type_bindings.len(), + ); + for binding in &identity.scoped_type_bindings { + push_component(&mut canonical, &binding.name); + push_component(&mut canonical, &canonical_type(binding.ty, types)?); + encode_reflection_type_info(&mut canonical, &binding.reflection); + } + } + + let digest = Sha256::digest(canonical.as_bytes()); + let mut symbol = String::from("jett_v0_"); + for byte in digest { + write!(&mut symbol, "{byte:02x}").expect("writing to a String cannot fail"); + } + Ok(symbol) +} + +fn encode_origin(output: &mut String, origin: &SourceOrigin) { + match origin { + SourceOrigin::Project => output.push_str("project_"), + SourceOrigin::Dependency(name) => { + output.push_str("dependency_"); + push_component(output, name); + } + SourceOrigin::Stdlib => output.push_str("stdlib_"), + } +} + +fn push_component(output: &mut String, component: &str) { + output.push_str(&component.len().to_string()); + output.push('_'); + output.push_str(component); + output.push('_'); +} + +fn push_category(output: &mut String, name: &str, item_count: usize) { + push_component(output, name); + push_component(output, &item_count.to_string()); +} + +fn encode_reflection_type_info(output: &mut String, info: &ReflectionTypeInfo) { + push_component(output, &info.type_name); + push_component(output, &info.kind); + match &info.primitive_tag { + Some(tag) => { + push_component(output, "some"); + push_component(output, tag); + } + None => push_component(output, "none"), + } + push_component(output, if info.has_secret { "true" } else { "false" }); + push_component(output, &info.args.len().to_string()); + for arg in &info.args { + encode_reflection_type_info(output, arg); + } +} + +fn canonical_type(id: TypeId, types: &TypeInterner) -> Result { + let Some(ty) = resolve_type(types, id) else { + return Err(CodegenError::UnsupportedType { + type_name: format!("", id.index()), + context: "native symbol mangling".to_string(), + }); + }; + let encoded = match ty { + Type::Int8 => "i8".to_string(), + Type::Int16 => "i16".to_string(), + Type::Int32 => "i32".to_string(), + Type::Int64 => "i64".to_string(), + Type::Uint8 => "u8".to_string(), + Type::Uint16 => "u16".to_string(), + Type::Uint32 => "u32".to_string(), + Type::Uint64 => "u64".to_string(), + Type::Float32 => "f32".to_string(), + Type::Float64 => "f64".to_string(), + Type::String => "string".to_string(), + Type::Bool => "bool".to_string(), + Type::Bytes => "bytes".to_string(), + Type::Nothing => "nothing".to_string(), + Type::TypeConstruction => "type-construction".to_string(), + Type::Never => "never".to_string(), + Type::Capability(kind) => format!("capability({})", kind.name()), + Type::List(inner) => format!("list({})", canonical_type(*inner, types)?), + Type::Map(key, value) => format!( + "map({},{})", + canonical_type(*key, types)?, + canonical_type(*value, types)? + ), + Type::Set(inner) => format!("set({})", canonical_type(*inner, types)?), + Type::Optional(inner) => format!("optional({})", canonical_type(*inner, types)?), + Type::Result(ok, error) => format!( + "result({},{})", + canonical_type(*ok, types)?, + canonical_type(*error, types)? + ), + Type::Secret(inner) => format!("secret({})", canonical_type(*inner, types)?), + Type::Struct(id) => format!("struct({})", types.resolve_struct(*id).name), + Type::Bitfield(id) => format!("bitfield({})", types.resolve_bitfield(*id).name), + Type::Enum(id) => format!("enum({})", types.resolve_enum(*id).name), + Type::Interface(id) => format!("interface({})", types.resolve_interface(*id).name), + Type::Actor(id) => format!("actor({})", types.resolve_actor(*id).name), + Type::Resource(name) => format!("resource({name})"), + Type::Machine(id) => format!("machine({})", types.resolve_machine(*id).name), + Type::MachineState { machine, state } => { + let machine = types.resolve_machine(*machine); + let state = machine + .state(*state) + .map(|state| state.name.as_str()) + .unwrap_or(""); + format!("machine-state({},{state})", machine.name) + } + Type::Function { + params, + view_params, + return_type, + } => { + let mut result = String::from("function("); + for (param, view) in params.iter().zip(view_params) { + result.push(if *view { 'v' } else { 'o' }); + push_component(&mut result, &canonical_type(*param, types)?); + } + result.push_str("returns-"); + result.push_str(&canonical_type(*return_type, types)?); + result.push(')'); + result + } + Type::Refinement { name, base } => { + format!("refinement({name},{})", canonical_type(*base, types)?) + } + Type::Error => { + return Err(CodegenError::UnsupportedType { + type_name: "".to_string(), + context: "native symbol mangling".to_string(), + }); + } + }; + Ok(encoded) +} + +fn resolve_type(types: &TypeInterner, id: TypeId) -> Option<&Type> { + let type_count = u32::try_from(types.len()).unwrap_or(u32::MAX); + (id.index() < type_count).then(|| types.resolve(id)) +} diff --git a/crates/jett_codegen_cranelift/src/reachability.rs b/crates/jett_codegen_cranelift/src/reachability.rs new file mode 100644 index 00000000..70742b94 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/reachability.rs @@ -0,0 +1,911 @@ +use jett_common::{SourceOrigin, Span}; +use jett_hir::{self as hir, Expression, ExpressionKind, FunctionId}; +use jett_mir::{Function, Program, StatementKind, TerminatorKind}; + +use crate::CodegenError; + +/// Find the backend-reachable MIR functions without changing their original +/// `FunctionId` identity. The returned IDs are in original program order so +/// declaration and object-symbol order stay deterministic. +#[cfg(test)] +fn reachable_function_ids(program: &Program) -> Result, CodegenError> { + reachable_functions(program, None, &[], &[]) +} + +pub(crate) fn reachable_function_ids_with_types( + program: &Program, + types: &jett_types::TypeInterner, +) -> Result, CodegenError> { + reachable_functions(program, Some(types), &[], &[]) +} + +/// Constructor-selected families are executable roots even when ordinary +/// project reachability did not use their descriptor-producing bodies. +pub(crate) fn reachable_resource_function_ids( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result, CodegenError> { + let mut roots = plan + .functions() + .iter() + .map(|function| function.function()) + .collect::>(); + if let Some(entry) = plan.entry_scope() { + roots.push(entry.function()); + } + reachable_functions( + plan.program(), + Some(plan.types()), + &roots, + plan.required_only_function_ids(), + ) +} + +struct References<'a> { + functions: Vec<(FunctionId, Span)>, + program: &'a Program, + types: Option<&'a jett_types::TypeInterner>, +} +impl References<'_> { + fn push(&mut self, reference: (FunctionId, Span)) { + self.functions.push(reference); + } +} + +fn reachable_functions( + program: &Program, + types: Option<&jett_types::TypeInterner>, + additional_roots: &[FunctionId], + required_only: &[FunctionId], +) -> Result, CodegenError> { + let mut reachable = vec![false; program.functions.len()]; + let mut pending = Vec::new(); + + for function in &program.functions { + if matches!(function.identity.declaration.origin, SourceOrigin::Project) + && function.identity.declaration.kind != hir::DeclarationKind::RefinementPredicate + && function.debug_kind != hir::FunctionDebugKind::Inline + && !uninhabited_specialization(function) + && !required_only.contains(&function.id) + { + mark_reachable( + program, + function.id, + function.span, + function, + &mut reachable, + &mut pending, + )?; + } + } + + for &id in additional_roots { + if required_only.contains(&id) { + return Err(CodegenError::Backend( + "required-only helper cannot become an executable Resource root".into(), + )); + } + let function = resolve_function(program, id).ok_or_else(|| { + CodegenError::Backend( + "authenticated Resource root is absent from the current MIR function table".into(), + ) + })?; + mark_reachable( + program, + id, + function.span, + function, + &mut reachable, + &mut pending, + )?; + } + + while let Some((function_id, function_span)) = pending.pop() { + let function = resolve_function(program, function_id) + .ok_or_else(|| invalid_target(program.functions.first(), function_span, function_id))?; + let mut references = References { + functions: Vec::new(), + program, + types, + }; + // Retain its identity for descriptor storage, but no valid call can + // enter this original body or make its callees executable roots. + if !crate::verify::descriptor_only_function(function) { + collect_function_references(function, &mut references); + } + for (target, span) in references.functions { + if required_only.contains(&target) { + return Err(CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span, + message: "runtime reference reaches a helper authenticated only for required evaluation" + .into(), + }); + } + mark_reachable( + program, + target, + span, + function, + &mut reachable, + &mut pending, + )?; + } + } + + Ok(program + .functions + .iter() + .enumerate() + .filter_map(|(index, function)| reachable[index].then_some(function.id)) + .collect()) +} + +// Checking an unreachable empty-collection body can instantiate a project +// generic with a bare `never` parameter or result. Such an eager instantiation +// is not an independent callable root: no value can inhabit that signature. +// A retained call or function reference still reaches it and must pass the +// ordinary verifier. Containers and absent sum arms around `never` are inhabited. +fn uninhabited_specialization(function: &Function) -> bool { + !function.identity.type_arguments.is_empty() + && (function.return_type == jett_types::TypeInterner::NEVER + || function + .params + .iter() + .any(|param| param.ty == jett_types::TypeInterner::NEVER)) +} + +fn mark_reachable( + program: &Program, + target: FunctionId, + span: Span, + referring_function: &Function, + reachable: &mut [bool], + pending: &mut Vec<(FunctionId, Span)>, +) -> Result<(), CodegenError> { + let Some(target_function) = resolve_function(program, target) else { + return Err(invalid_target(Some(referring_function), span, target)); + }; + let index = usize::try_from(target.index()) + .map_err(|_| invalid_target(Some(referring_function), span, target))?; + if !reachable[index] { + reachable[index] = true; + pending.push((target_function.id, target_function.span)); + } + Ok(()) +} + +fn resolve_function(program: &Program, id: FunctionId) -> Option<&Function> { + let index = usize::try_from(id.index()).ok()?; + program + .functions + .get(index) + .filter(|function| function.id == id) +} + +fn invalid_target( + referring_function: Option<&Function>, + span: Span, + target: FunctionId, +) -> CodegenError { + let function = referring_function + .map(function_label) + .unwrap_or_else(|| "".to_string()); + CodegenError::InvalidMirContract { + function, + span, + message: format!( + "reached function target {} is absent from the original MIR function table", + target.index() + ), + } +} + +fn function_label(function: &Function) -> String { + let declaration = &function.identity.declaration; + format!("{}::{}", declaration.namespace, declaration.name) +} + +fn collect_function_references(function: &Function, references: &mut References<'_>) { + for block in &function.blocks { + for statement in &block.statements { + match &statement.kind { + StatementKind::ResourceCall(jett_mir::ResourceCallNode::Stage { + value, .. + }) + | StatementKind::ResourceConstructor(jett_mir::ResourceConstructorNode::Stage { + value, + .. + }) => { + collect_expression_references(value, references); + } + StatementKind::ResourceCall( + jett_mir::ResourceCallNode::Begin { region } + | jett_mir::ResourceCallNode::Invoke { region, .. }, + ) => { + if let Some(region) = function.resource_call_region(*region) { + references.push((region.function(), statement.span)); + } + } + StatementKind::ResourceCall(jett_mir::ResourceCallNode::End { .. }) => {} + StatementKind::ResourceConstructor(_) => {} + StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => { + collect_expression_references(value, references); + } + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + collect_expression_references(target, references); + collect_expression_references(value, references); + } + StatementKind::Assert { condition, message } => { + collect_expression_references(condition, references); + if let Some(message) = message { + collect_expression_references(message, references); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + collect_expression_references(condition, references); + } + } + // Generation operations use current Local operands only; + // their immutable Source archives are not callable roots. + StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ResourceLexicalExit(_) + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + collect_expression_references(value, references); + } + } + TerminatorKind::Respond(value) => collect_expression_references(value, references), + TerminatorKind::Branch { condition, .. } => { + collect_expression_references(condition, references); + } + TerminatorKind::Switch { scrutinee, .. } => { + collect_expression_references(scrutinee, references); + } + TerminatorKind::ForEach { iterable, .. } => { + collect_expression_references(iterable, references); + } + TerminatorKind::ReflectedTypeDispatch { type_info, .. } => { + collect_expression_references(type_info, references); + } + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } + } +} + +fn collect_hir_block_references(block: &hir::Block, references: &mut References<'_>) { + for statement in &block.statements { + match &statement.kind { + hir::StatementKind::Let { value, .. } + | hir::StatementKind::HandleDefault(value) + | hir::StatementKind::Expression(value) + | hir::StatementKind::Respond(value) => { + collect_expression_references(value, references); + } + hir::StatementKind::Assign { target, value } + | hir::StatementKind::UpdateField { target, value, .. } => { + collect_expression_references(target, references); + collect_expression_references(value, references); + } + hir::StatementKind::Return(value) => { + if let Some(value) = value { + collect_expression_references(value, references); + } + } + hir::StatementKind::If { + condition, + then_block, + else_block, + } => { + collect_expression_references(condition, references); + collect_hir_block_references(then_block, references); + if let Some(else_block) = else_block { + collect_hir_block_references(else_block, references); + } + } + hir::StatementKind::While { condition, body } => { + collect_expression_references(condition, references); + collect_hir_block_references(body, references); + } + hir::StatementKind::For { iterable, body, .. } => { + collect_expression_references(iterable, references); + collect_hir_block_references(body, references); + } + hir::StatementKind::Match { scrutinee, arms } => { + collect_expression_references(scrutinee, references); + for arm in arms { + collect_hir_block_references(&arm.body, references); + } + } + hir::StatementKind::Assert { condition, message } => { + collect_expression_references(condition, references); + if let Some(message) = message { + collect_expression_references(message, references); + } + } + hir::StatementKind::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + collect_expression_references(condition, references); + } + } + hir::StatementKind::Scope(block) => { + collect_hir_block_references(block, references); + } + hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { + collect_expression_references(type_info, references); + for arm in arms { + collect_hir_block_references(&arm.body, references); + } + } + hir::StatementKind::Break + | hir::StatementKind::Continue + | hir::StatementKind::Trace(_) => {} + } + } +} + +/// Deliberately match every expression variant without a wildcard. Adding a +/// new MIR-carried HIR expression cannot compile until reachability decides how +/// to traverse it. +fn collect_expression_references(expression: &Expression, references: &mut References<'_>) { + match &expression.kind { + ExpressionKind::ActorGraphImport(plan) => { + collect_hir_block_references(plan.body(), references); + } + ExpressionKind::ActorGraphStep { args, .. } => { + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::ResourceHookValue { .. } => {} + ExpressionKind::ResourceInvoke { args, .. } => { + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::FunctionRef(function) => references.push((*function, expression.span)), + ExpressionKind::FunctionAdapter { value, function } => { + references.push((*function, expression.span)); + collect_expression_references(value, references); + } + ExpressionKind::ClosureRef { function, .. } => { + references.push((*function, expression.span)); + } + ExpressionKind::Call { function, args, .. } => { + references.push((*function, expression.span)); + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::Binary { left, op, right } => { + if matches!(op, hir::BinaryOp::Equal | hir::BinaryOp::NotEqual) { + if let Some(types) = references.types { + if let Some(layout) = crate::emit::debug::equality_layout( + types, + left.ty, + &references.program.equality_methods, + ) { + for (_, method) in layout.custom { + references.push((method, expression.span)); + } + } + } + } + collect_expression_references(left, references); + collect_expression_references(right, references); + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => collect_expression_references(value, references), + ExpressionKind::InterfaceCoerce { value, adapters } => { + collect_expression_references(value, references); + references.functions.extend( + adapters + .iter() + .map(|entry| (entry.function, expression.span)), + ); + } + ExpressionKind::Intrinsic { + intrinsic: _, + type_arguments: _, + reflection_arguments: _, + refinement_predicates: _, + field_validation, + args, + evaluation_order: _, + .. + } => { + if let Some(jett_hir::ReflectedFieldValidation::ReadCallable(plans)) = field_validation + { + for plan in plans { + if let jett_hir::ReflectedFieldAction::CallableReturn(callable) = &plan.action { + references.push((callable.function(), expression.span)); + } + } + } + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::ActorSpawn { + args, constructor, .. + } => { + if let Some(constructor) = constructor { + references.push((*constructor, expression.span)); + } + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::IndirectCall { callee, args, .. } => { + collect_expression_references(callee, references); + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::BitfieldConstruct { fields, .. } => { + for field in fields { + collect_expression_references(field, references); + } + } + ExpressionKind::MachineConstruct { payloads, .. } + | ExpressionKind::EnumConstruct { payloads, .. } => { + for payload in payloads { + collect_expression_references(payload, references); + } + } + ExpressionKind::MachineTransition { + source, payloads, .. + } => { + collect_expression_references(source, references); + for payload in payloads { + collect_expression_references(payload, references); + } + } + ExpressionKind::ListConstruct { elements } => { + for element in elements { + collect_expression_references(element, references); + } + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + collect_expression_references(&entry.key, references); + collect_expression_references(&entry.value, references); + } + } + ExpressionKind::Handle { + target, failure, .. + } => { + collect_expression_references(target, references); + collect_hir_block_references(failure, references); + } + ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + collect_expression_references(value, references); + } + } + } + ExpressionKind::StateIs { value, .. } | ExpressionKind::Field { base: value, .. } => { + collect_expression_references(value, references); + } + ExpressionKind::InlineFunction { body, .. } => { + collect_hir_block_references(body, references); + } + ExpressionKind::ActorMessage { + actor, + args, + handler, + .. + } => { + references.push((*handler, expression.span)); + collect_expression_references(actor, references); + for argument in args { + collect_expression_references(argument, references); + } + } + ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::Local(_) + | ExpressionKind::OptionalNone => {} + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use jett_common::FileId; + use jett_mir::Program; + + use super::*; + + fn lower_source(source: &str) -> Program { + lower_source_with_types(source).0 + } + + fn lower_source_with_types(source: &str) -> (Program, jett_types::TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "check errors: {:?}", + checked.diagnostics + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + let mir = jett_mir::lower(&hir, &checked.interner).expect("MIR lowering"); + (mir, checked.interner) + } + + fn set_origin(program: &mut Program, name: &str, origin: SourceOrigin) { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == name) + .unwrap_or_else(|| panic!("missing test function `{name}`")) + .identity + .declaration + .origin = origin; + } + + fn reachable_names(program: &Program) -> Vec<&str> { + reachable_function_ids(program) + .expect("reachable functions") + .into_iter() + .map(|id| { + resolve_function(program, id) + .expect("reachable function exists") + .identity + .declaration + .name + .as_str() + }) + .collect() + } + + #[test] + fn direct_calls_and_function_refs_reach_stdlib_and_dependency_functions() { + let mut program = lower_source( + r#"namespace app +function dependency_leaf(value: int64) returns int64: + return value +function stdlib_factory() returns function(int64) returns int64: + return dependency_leaf +function root() returns function(int64) returns int64: + return stdlib_factory() +"#, + ); + set_origin( + &mut program, + "dependency_leaf", + SourceOrigin::Dependency("dep.math".to_string()), + ); + set_origin(&mut program, "stdlib_factory", SourceOrigin::Stdlib); + + assert_eq!( + reachable_names(&program), + ["dependency_leaf", "stdlib_factory", "root"] + ); + } + + #[test] + fn exhaustive_visitors_follow_calls_inside_inline_hir_bodies() { + let mut program = lower_source( + r#"namespace app +function stdlib_leaf() returns int64: + return 1 +function root(seed: int64) returns function(bool) returns int64: + return function(flag: bool) returns int64: + if flag: + return stdlib_leaf() + seed + return 0 +"#, + ); + set_origin(&mut program, "stdlib_leaf", SourceOrigin::Stdlib); + + // The production visitors use wildcard-free matches for every current + // MIR statement/terminator and MIR-carried HIR statement/expression. + // This exercises the extra nested HIR body that a shallow MIR walk + // would miss; adding an enum variant also fails those matches to build. + let names = reachable_names(&program); + assert_eq!(names[0..2], ["stdlib_leaf", "root"]); + assert!(names[2].starts_with("root$inline")); + } + + #[test] + fn capture_free_inline_function_reaches_its_extracted_body() { + let mut program = lower_source( + r#"namespace app +function stdlib_leaf() returns int64: + return 1 +function root() returns function(bool) returns int64: + return function(flag: bool) returns int64: + if flag: + return stdlib_leaf() + return 0 +"#, + ); + set_origin(&mut program, "stdlib_leaf", SourceOrigin::Stdlib); + + let names = reachable_names(&program); + assert_eq!(&names[..2], ["stdlib_leaf", "root"]); + assert_eq!(names.len(), 3); + assert!(names[2].starts_with("root$inline")); + } + + #[test] + fn empty_loop_only_uninhabited_specializations_are_not_implicit_roots() { + let program = lower_source( + r#"namespace app +function pass[T](value: T) returns T: + return value +function identity[T](value: T) returns T: + return pass[T](value) +function main() returns nothing: + for value in list(): + println(identity(value)) +"#, + ); + let dead = program + .functions + .iter() + .filter(|function| uninhabited_specialization(function)) + .collect::>(); + assert_eq!(dead.len(), 2, "eager checked specializations remain in MIR"); + for function in dead { + assert_eq!( + function.identity.type_arguments, + [jett_types::TypeInterner::NEVER] + ); + } + assert_eq!(reachable_names(&program), ["main"]); + } + + #[test] + fn descriptor_only_entry_does_not_make_original_body_callees_callable() { + let (mut program, types) = lower_source_with_types( + r#"namespace app +function source_leaf() returns int64: + return 99 +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + function(T) returns int64 callback = function(ignored: T) returns int64: return source_leaf() + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#, + ); + set_origin(&mut program, "source_leaf", SourceOrigin::Stdlib); + let descriptor = program + .functions + .iter() + .find(|function| crate::verify::descriptor_only_function(function)) + .expect("Never-input descriptor identity"); + let descriptor_id = descriptor.id; + let reachable = reachable_function_ids_with_types(&program, &types).unwrap(); + assert!(reachable.contains(&descriptor_id)); + let source_leaf = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "source_leaf") + .unwrap(); + assert!(!reachable.contains(&source_leaf.id)); + let object = crate::emit_host_object(&program, &types) + .expect("metadata validates original call without emitting it"); + let leaf_symbol = crate::symbol_name(&source_leaf.identity, &types).unwrap(); + assert!(!object.symbols.contains(&leaf_symbol)); + } + + #[test] + fn retained_calls_and_references_still_reach_uninhabited_specializations() { + for (case, source) in [ + r#"namespace app +function identity(value: int64) returns int64: + return value +function root() returns int64: + return identity(1) +"#, + r#"namespace app +function identity(value: int64) returns int64: + return value +function root() returns function(int64) returns int64: + return identity +"#, + ] + .into_iter() + .enumerate() + { + let (mut program, types) = lower_source_with_types(source); + let callee = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "identity") + .unwrap(); + callee.identity.type_arguments = vec![jett_types::TypeInterner::NEVER]; + callee.params[0].ty = jett_types::TypeInterner::NEVER; + let parameter = callee.params[0].local.index() as usize; + callee.locals[parameter].ty = jett_types::TypeInterner::NEVER; + callee.locals[parameter].debug_ty = jett_types::TypeInterner::NEVER; + assert!(uninhabited_specialization(callee)); + assert_eq!(reachable_names(&program), ["identity", "root"]); + // Reachability keeps both identities, but an earlier source + // packet rejection is independent of the native callable gate. + let result = crate::emit_host_object(&program, &types); + if case == 0 { + assert!(matches!(result, Err(CodegenError::InvalidMir(errors)) + if errors.len() == 1 && errors[0].message + == "source function ownership signature differs from the exact HIR declaration")); + } else { + // Preserve the original reference-only refusal until its + // actual validation path is characterized separately. + assert!(matches!( + result, + Err(CodegenError::InvalidMirContract { .. }) + )); + } + let root = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "root") + .expect("original checked root"); + let expression = root + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("retained original call or reference"); + let expected = if case == 0 { + "callable direct edge requires an uninhabited parameter" + } else { + "function value signature does not match target" + }; + assert!(matches!( + crate::verify::verify_callable_expression_for_test(&program, &types, root, expression), + Err(CodegenError::InvalidMirContract { message, .. }) if message == expected + )); + } + } + + #[test] + fn never_container_specializations_remain_inhabited_project_roots() { + let program = lower_source( + r#"namespace app +function pass[T](values: list[T]) returns list[T]: + return values +function main() returns nothing: + for value in pass(list()): + println(value) +"#, + ); + let callee = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "pass") + .unwrap(); + assert_eq!( + callee.identity.type_arguments, + [jett_types::TypeInterner::NEVER] + ); + assert!(!uninhabited_specialization(callee)); + assert_eq!(reachable_names(&program), ["main", "pass"]); + let mut plain = callee.clone(); + plain.identity.type_arguments.clear(); + plain.params[0].ty = jett_types::TypeInterner::NEVER; + plain.return_type = jett_types::TypeInterner::NEVER; + assert!(!uninhabited_specialization(&plain)); + } + + #[test] + fn generated_inline_functions_require_a_retained_descriptor_reference() { + let (mut program, types) = lower_source_with_types( + r#"namespace app +function root() returns int64: + function() returns int64 callback = function() returns int64: return 1 + return 7 +"#, + ); + assert_eq!(reachable_names(&program).len(), 2); + let root = &mut program.functions[0]; + assert!(matches!( + root.blocks[0].statements[0].kind, + StatementKind::Let { .. } + )); + // Model the descriptor being removed with an impossible handler arm. + // The generated function retains its identity and table position. + root.blocks[0].statements.remove(0); + assert_eq!(program.functions.len(), 2); + assert_eq!( + program.functions[1].debug_kind, + hir::FunctionDebugKind::Inline + ); + assert_eq!(reachable_names(&program), ["root"]); + let object = crate::emit_host_object(&program, &types).unwrap(); + assert_eq!(object.symbols.len(), 1); + } + + #[test] + fn retained_inline_capture_with_never_metadata_still_fails_verification() { + let (mut program, types) = lower_source_with_types( + r#"namespace app +function root(seed: int64) returns function() returns int64: + return function() returns int64: return seed +"#, + ); + let inline = program + .functions + .iter_mut() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .unwrap(); + assert_eq!(inline.capture_count, 1); + inline.params[0].ty = jett_types::TypeInterner::NEVER; + let capture = inline.params[0].local.index() as usize; + inline.locals[capture].ty = jett_types::TypeInterner::NEVER; + inline.locals[capture].debug_ty = jett_types::TypeInterner::NEVER; + assert_eq!(reachable_names(&program).len(), 2); + assert!(matches!( + crate::emit_host_object(&program, &types), + Err(CodegenError::InvalidMirContract { message, .. }) + if message == "closure capture type does not match target" + )); + } +} diff --git a/crates/jett_codegen_cranelift/src/values.rs b/crates/jett_codegen_cranelift/src/values.rs new file mode 100644 index 00000000..2d179173 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/values.rs @@ -0,0 +1,1075 @@ +use crate::CodegenError; +use cranelift_codegen::ir::{self, AbiParam}; +use cranelift_module::{FuncId, Linkage, Module}; +use cranelift_object::ObjectModule; +use jett_hir::{Expression, IntrinsicId}; +use jett_runtime::native_abi::values::{AbiScalar, NativeLeaf, NativeSortKind}; +use jett_types::{Type, TypeId, TypeInterner}; + +pub(crate) fn abi_type(ty: AbiScalar) -> ir::Type { + match ty { + AbiScalar::Pointer | AbiScalar::I64 => ir::types::I64, + AbiScalar::I32 => ir::types::I32, + AbiScalar::F64 => ir::types::F64, + } +} +pub(crate) fn declare_leaf( + module: &mut ObjectModule, + leaf: NativeLeaf, +) -> Result { + let mut signature = module.make_signature(); + signature.params.extend( + leaf.parameters() + .iter() + .map(|t| AbiParam::new(abi_type(*t))), + ); + signature + .returns + .push(AbiParam::new(abi_type(leaf.result()))); + module + .declare_function(leaf.symbol(), Linkage::Import, &signature) + .map_err(|e| CodegenError::Backend(e.to_string())) +} +pub(crate) fn is_formattable(types: &TypeInterner, ty: TypeId) -> bool { + matches!( + types.resolve(ty), + Type::String + | Type::Bool + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::Nothing + ) +} +pub(crate) fn verify_intrinsic( + id: IntrinsicId, + args: &[Expression], + result: TypeId, + types: &TypeInterner, +) -> Result<(), String> { + use TypeInterner as T; + if id == IntrinsicId::GraphicsRun { + let valid = if let [display, config, state, update, render] = args { + let state_type = state.ty; + let config_is_checked = matches!(types.resolve(config.ty), Type::Struct(id) + if types.resolve_struct(*id).name == "graphics.Config"); + let key = types.type_ids().find(|ty| { + matches!(types.resolve(*ty), Type::Enum(id) + if types.resolve_enum(*id).name == "graphics.Key") + }); + let scene = types.type_ids().find(|ty| { + matches!(types.resolve(*ty), Type::Struct(id) + if types.resolve_struct(*id).name == "graphics.Scene") + }); + display.ty == T::GRAPHICS + && config_is_checked + && key.is_some_and(|key| { + matches!(types.resolve(update.ty), Type::Function { params, view_params, return_type } + if params.as_slice() == [state_type, key] + && view_params.as_slice() == [false, false] + && *return_type == state_type) + }) + && scene.is_some_and(|scene| { + matches!(types.resolve(render.ty), Type::Function { params, view_params, return_type } + if params.as_slice() == [state_type] + && view_params.as_slice() == [true] + && *return_type == scene) + }) + && matches!(types.resolve(result), Type::Result(ok, error) + if *ok == T::NOTHING && *error == T::STRING) + } else { + false + }; + return if valid { + Ok(()) + } else { + Err("invalid checked Graphics runtime intrinsic".into()) + }; + } + if id == IntrinsicId::EnvironmentGet { + return if args.len() == 2 + && args[0].ty == T::ENVIRONMENT + && args[1].ty == T::STRING + && matches!(types.resolve(result), Type::Result(ok, error) + if *error == T::STRING && matches!(types.resolve(*ok), Type::Optional(inner) if *inner == T::STRING)) + { + Ok(()) + } else { + Err("invalid native Environment.get signature".into()) + }; + } + if id == IntrinsicId::EnvironmentArgs { + return if args.len() == 1 + && args[0].ty == T::ENVIRONMENT + && matches!(types.resolve(result), Type::List(inner) if *inner == T::STRING) + { + Ok(()) + } else { + Err("invalid native Environment.args signature".into()) + }; + } + if id == IntrinsicId::BitfieldToBytes { + return if args.len() == 1 + && matches!(types.resolve(args[0].ty), Type::Bitfield(_)) + && result == T::BYTES + { + Ok(()) + } else { + Err("invalid native bitfield encoding signature".into()) + }; + } + if id == IntrinsicId::BitfieldFromBytes { + return if args.len() == 1 + && args[0].ty == T::BYTES + && matches!(types.resolve(result), Type::Result(ok, error) if matches!(types.resolve(*ok), Type::Bitfield(_)) && *error == T::STRING) + { + Ok(()) + } else { + Err("invalid native bitfield decoding signature".into()) + }; + } + if id == IntrinsicId::Range { + return if (1..=3).contains(&args.len()) + && args.iter().all(|a| a.ty == T::INT64) + && matches!(types.resolve(result), Type::List(inner) if *inner == T::INT64) + { + Ok(()) + } else { + Err("invalid native range signature".into()) + }; + } + if id == IntrinsicId::SecretRedact { + return if args.len() == 1 + && jett_mir::move_values::is_secret(types, args[0].ty) + && result == T::STRING + { + Ok(()) + } else { + Err("invalid native secret redaction signature".into()) + }; + } + if id == IntrinsicId::SecretCompare { + let same_payload = args.len() == 2 + && jett_mir::move_values::representation_type(types, args[0].ty) + == jett_mir::move_values::representation_type(types, args[1].ty); + let supported_payload = args.first().is_some_and(|arg| { + matches!( + types.resolve(jett_mir::move_values::representation_type(types, arg.ty)), + Type::String | Type::Bytes + ) + }); + return if same_payload + && supported_payload + && args + .iter() + .all(|arg| jett_mir::move_values::is_secret(types, arg.ty)) + && result == T::BOOL + { + Ok(()) + } else { + Err("invalid native secret comparison signature".into()) + }; + } + if matches!( + id, + IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoHmacSha256 + ) { + let valid = if id == IntrinsicId::CryptoHmacSha256 { + args.len() == 2 + && jett_mir::move_values::is_secret(types, args[0].ty) + && jett_mir::move_values::representation_type(types, args[0].ty) == T::BYTES + && args[1].ty == T::BYTES + && matches!(types.resolve(result), Type::Secret(inner) if *inner == T::BYTES) + } else { + args.len() == 1 && args[0].ty == T::BYTES && result == T::BYTES + }; + return if valid { + Ok(()) + } else { + Err("invalid native crypto intrinsic signature".into()) + }; + } + if matches!( + id, + IntrinsicId::CsvParse | IntrinsicId::CsvParseWithHeader | IntrinsicId::CsvStringify + ) { + let string_rows = |ty| matches!(types.resolve(ty), Type::List(row) if matches!(types.resolve(*row), Type::List(field) if *field == T::STRING)); + let map_rows = |ty| matches!(types.resolve(ty), Type::List(row) if matches!(types.resolve(*row), Type::Map(key, value) if *key == T::STRING && *value == T::STRING)); + let valid = match id { + IntrinsicId::CsvParse => { + args.len() == 1 + && args[0].ty == T::STRING + && matches!(types.resolve(result), Type::Result(rows, error) if *error == T::STRING && string_rows(*rows)) + } + IntrinsicId::CsvParseWithHeader => { + args.len() == 1 + && args[0].ty == T::STRING + && matches!(types.resolve(result), Type::Result(rows, error) if *error == T::STRING && map_rows(*rows)) + } + IntrinsicId::CsvStringify => { + args.len() == 1 && string_rows(args[0].ty) && result == T::STRING + } + _ => false, + }; + return if valid { + Ok(()) + } else { + Err("invalid native CSV intrinsic signature".into()) + }; + } + if matches!(id, IntrinsicId::MathAverage | IntrinsicId::MathMedian) { + return if args.len() == 1 + && result == T::FLOAT64 + && math_aggregate_kind(types, args[0].ty).is_some() + { + Ok(()) + } else { + Err(format!("invalid native numeric list signature for {id}")) + }; + } + if let Some(leaf) = math_leaf(id, args.first().map(|a| a.ty)) { + let parameters = &leaf.parameters()[1..]; + let native_type = |ty| match ty { + AbiScalar::I64 => T::INT64, + AbiScalar::F64 => T::FLOAT64, + _ => T::ERROR, + }; + if result == native_type(leaf.result()) + && args + .iter() + .map(|a| a.ty) + .eq(parameters.iter().copied().map(native_type)) + { + return Ok(()); + } + return Err(format!("invalid native numeric signature for {id}")); + } + if matches!( + id, + IntrinsicId::StringChars + | IntrinsicId::StringWords + | IntrinsicId::StringLines + | IntrinsicId::StringSplit + | IntrinsicId::StringJoin + ) { + let list = |ty| matches!(types.resolve(ty), Type::List(inner) if *inner == T::STRING); + let valid = if id == IntrinsicId::StringJoin { + args.len() == 2 && list(args[0].ty) && args[1].ty == T::STRING && result == T::STRING + } else { + args.len() == if id == IntrinsicId::StringSplit { 2 } else { 1 } + && args.iter().all(|a| a.ty == T::STRING) + && list(result) + }; + return if valid { + Ok(()) + } else { + Err("invalid native string-list signature".into()) + }; + } + if list_intrinsic(id) { + let element = list_element(id, args, result, types) + .ok_or_else(|| "invalid native list type".to_string())?; + let list = |ty| matches!(types.resolve(ty), Type::List(inner) if *inner == element); + let valid = match id { + IntrinsicId::ListSum => { + args.len() == 1 + && list(args[0].ty) + && list_sum_kind(types, element).is_some() + && result == element + } + IntrinsicId::ListNew => args.is_empty() && list(result), + IntrinsicId::ListAppend => { + args.len() == 2 && list(args[0].ty) && args[1].ty == element && list(result) + } + IntrinsicId::ListInsertAt => { + args.len() == 3 + && list(args[0].ty) + && args[1].ty == T::INT64 + && args[2].ty == element + && list(result) + } + IntrinsicId::ListRemoveAt => { + args.len() == 2 && list(args[0].ty) && args[1].ty == T::INT64 && list(result) + } + IntrinsicId::ListLength => args.len() == 1 && list(args[0].ty) && result == T::INT64, + IntrinsicId::ListGetClone => { + args.len() == 2 + && list(args[0].ty) + && args[1].ty == T::INT64 + && matches!(types.resolve(result), Type::Optional(inner) if *inner == element) + } + IntrinsicId::ListSort => { + args.len() == 1 + && list(args[0].ty) + && list(result) + && list_sort_kind(types, element).is_some() + } + IntrinsicId::ListSortByIndex => { + args.len() == 2 + && matches!(types.resolve(element), Type::List(_)) + && list(args[0].ty) + && args[1].ty == T::INT64 + && list(result) + } + IntrinsicId::ListIsSorted => args.len() == 1 && list(args[0].ty) && result == T::BOOL, + IntrinsicId::ListSwap => { + args.len() == 3 + && list(args[0].ty) + && args[1].ty == T::INT64 + && args[2].ty == T::INT64 + && list(result) + } + _ => false, + }; + return if valid { + Ok(()) + } else { + Err(format!("invalid native list signature for {id}")) + }; + } + if set_intrinsic(id) { + let element = set_element(id, args, result, types) + .ok_or_else(|| "invalid native set type".to_string())?; + if !set_element_supported(types, element) { + return Err("unsupported native set element".into()); + } + let set = |ty| matches!(types.resolve(ty), Type::Set(inner) if *inner == element); + let valid = match id { + IntrinsicId::SetNew => args.is_empty() && set(result), + IntrinsicId::SetAdd | IntrinsicId::SetRemove => { + args.len() == 2 && set(args[0].ty) && args[1].ty == element && set(result) + } + IntrinsicId::SetContains => { + args.len() == 2 && set(args[0].ty) && args[1].ty == element && result == T::BOOL + } + IntrinsicId::SetLength => args.len() == 1 && set(args[0].ty) && result == T::INT64, + _ => false, + }; + return if valid { + Ok(()) + } else { + Err(format!("invalid native set signature for {id}")) + }; + } + if map_intrinsic(id) { + let (key, value) = map_types(id, args, result, types) + .ok_or_else(|| "invalid native map type".to_string())?; + if !set_element_supported(types, key) { + return Err("unsupported native map key".into()); + } + let map = |ty| matches!(types.resolve(ty), Type::Map(k, v) if *k == key && *v == value); + let valid = match id { + IntrinsicId::MapNew => args.is_empty() && map(result), + IntrinsicId::MapLength => args.len() == 1 && map(args[0].ty) && result == T::INT64, + IntrinsicId::MapHas => { + args.len() == 2 && map(args[0].ty) && args[1].ty == key && result == T::BOOL + } + IntrinsicId::MapGet => { + args.len() == 2 + && map(args[0].ty) + && args[1].ty == key + && matches!(types.resolve(result), Type::Optional(inner) if *inner == value) + } + IntrinsicId::MapInsert => { + args.len() == 3 + && map(args[0].ty) + && args[1].ty == key + && args[2].ty == value + && map(result) + } + IntrinsicId::MapRemove => { + args.len() == 2 && map(args[0].ty) && args[1].ty == key && map(result) + } + IntrinsicId::MapFromLists => { + args.len() == 2 + && matches!(types.resolve(args[0].ty), Type::List(inner) if *inner == key) + && matches!(types.resolve(args[1].ty), Type::List(inner) if *inner == value) + && map(result) + } + _ => false, + }; + return if valid { + Ok(()) + } else { + Err(format!("invalid native map signature for {id}")) + }; + } + let sum_signature = match id { + IntrinsicId::Float64FromInt64 => { + Some((vec![T::INT64], Type::Result(T::FLOAT64, T::STRING))) + } + IntrinsicId::Int64FromFloat64 => { + Some((vec![T::FLOAT64], Type::Result(T::INT64, T::STRING))) + } + IntrinsicId::Int64FromString => Some((vec![T::STRING], Type::Result(T::INT64, T::STRING))), + IntrinsicId::Uint64FromString => { + Some((vec![T::STRING], Type::Result(T::UINT64, T::STRING))) + } + IntrinsicId::Float64FromString => { + Some((vec![T::STRING], Type::Result(T::FLOAT64, T::STRING))) + } + IntrinsicId::BytesGet => Some((vec![T::BYTES, T::INT64], Type::Optional(T::INT64))), + IntrinsicId::BytesToString => Some((vec![T::BYTES], Type::Result(T::STRING, T::STRING))), + IntrinsicId::BytesFromHex => Some((vec![T::STRING], Type::Result(T::BYTES, T::STRING))), + IntrinsicId::EncodingBase64Decode | IntrinsicId::EncodingHexDecode => { + Some((vec![T::STRING], Type::Result(T::BYTES, T::STRING))) + } + IntrinsicId::EncodingUrlDecode | IntrinsicId::EncodingFormDecode => { + Some((vec![T::STRING], Type::Result(T::STRING, T::STRING))) + } + _ => None, + }; + if let Some((parameters, expected)) = sum_signature { + if types.resolve(result) == &expected && args.iter().map(|a| a.ty).eq(parameters) { + return Ok(()); + } + return Err(format!("invalid native sum leaf signature for {id}")); + } + let (parameters, expected): (&[TypeId], TypeId) = match id { + IntrinsicId::Float32FromFloat64 => (&[T::FLOAT64], T::FLOAT32), + IntrinsicId::BytesNew => (&[], T::BYTES), + IntrinsicId::BytesLength => (&[T::BYTES], T::INT64), + IntrinsicId::BytesFromString => (&[T::STRING], T::BYTES), + IntrinsicId::BytesConcat => (&[T::BYTES, T::BYTES], T::BYTES), + IntrinsicId::BytesSlice => (&[T::BYTES, T::INT64, T::INT64], T::BYTES), + IntrinsicId::BytesToHex => (&[T::BYTES], T::STRING), + IntrinsicId::EncodingBase64Encode => (&[T::BYTES], T::STRING), + IntrinsicId::EncodingUrlEncode | IntrinsicId::EncodingFormEncode => { + (&[T::STRING], T::STRING) + } + IntrinsicId::StringCharCount => (&[T::STRING], T::INT64), + IntrinsicId::StringIndexOf => { + if args.len() == 2 + && args.iter().all(|arg| arg.ty == T::STRING) + && matches!(types.resolve(result), Type::Optional(inner) if *inner == T::INT64) + { + return Ok(()); + } + return Err("invalid native string index signature".into()); + } + IntrinsicId::StringCount => (&[T::STRING, T::STRING], T::INT64), + IntrinsicId::StringReplace => (&[T::STRING, T::STRING, T::STRING], T::STRING), + IntrinsicId::StringSlice => (&[T::STRING, T::INT64, T::INT64], T::STRING), + IntrinsicId::StringUpper + | IntrinsicId::StringLower + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimStart + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringSlugify + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringToLowerFirst => (&[T::STRING], T::STRING), + IntrinsicId::StringIsAlpha | IntrinsicId::StringIsNumeric => (&[T::STRING], T::BOOL), + IntrinsicId::StringRepeat => (&[T::STRING, T::INT64], T::STRING), + IntrinsicId::StdoutWrite => (&[T::STDOUT, T::STRING], T::NOTHING), + IntrinsicId::ClockNow => (&[T::CLOCK], T::INT64), + IntrinsicId::RandomBounded => (&[T::RANDOM, T::INT64, T::INT64], T::INT64), + IntrinsicId::RandomUnitFloat64 => (&[T::RANDOM], T::FLOAT64), + IntrinsicId::RandomBool => (&[T::RANDOM], T::BOOL), + IntrinsicId::UuidNew => (&[], T::STRING), + IntrinsicId::StringFromInt64 => (&[T::INT64], T::STRING), + IntrinsicId::StringFromUint64 => (&[T::UINT64], T::STRING), + IntrinsicId::StringFromFloat64 => (&[T::FLOAT64], T::STRING), + IntrinsicId::StringFromBool => (&[T::BOOL], T::STRING), + IntrinsicId::Print | IntrinsicId::Println => { + if result == T::NOTHING + && args.iter().all(|a| { + !jett_mir::move_values::is_secret(types, a.ty) + && crate::emit::debug::debug_layout(types, a.ty).is_some() + }) + { + return Ok(()); + } + return Err("invalid native print signature".into()); + } + _ => return Err(format!("unsupported native intrinsic {id}")), + }; + if result != expected || args.iter().map(|a| a.ty).ne(parameters.iter().copied()) { + return Err(format!("invalid native signature for {id}")); + } + Ok(()) +} + +/// Exact checked identities, not canonical-name inference. +pub(crate) fn string_leaf(id: IntrinsicId) -> Option { + Some(match id { + IntrinsicId::StringChars => NativeLeaf::StringChars, + IntrinsicId::StringWords => NativeLeaf::StringWords, + IntrinsicId::StringLines => NativeLeaf::StringLines, + IntrinsicId::StringSplit => NativeLeaf::StringSplit, + IntrinsicId::StringJoin => NativeLeaf::StringJoin, + IntrinsicId::StringCharCount => NativeLeaf::CharCount, + IntrinsicId::StringIndexOf => NativeLeaf::StringIndexOf, + IntrinsicId::StringCount => NativeLeaf::StringCount, + IntrinsicId::StringReplace => NativeLeaf::StringReplace, + IntrinsicId::StringSlugify => NativeLeaf::StringSlugify, + IntrinsicId::StringToUpperFirst => NativeLeaf::StringToUpperFirst, + IntrinsicId::StringToLowerFirst => NativeLeaf::StringToLowerFirst, + IntrinsicId::StringSlice => NativeLeaf::Slice, + IntrinsicId::StringUpper => NativeLeaf::Upper, + IntrinsicId::StringLower => NativeLeaf::Lower, + IntrinsicId::StringTrim => NativeLeaf::Trim, + IntrinsicId::StringTrimStart => NativeLeaf::TrimStart, + IntrinsicId::StringTrimEnd => NativeLeaf::TrimEnd, + IntrinsicId::StringIsAlpha => NativeLeaf::IsAlpha, + IntrinsicId::StringIsNumeric => NativeLeaf::IsNumeric, + IntrinsicId::StringRepeat => NativeLeaf::Repeat, + _ => return None, + }) +} + +pub(crate) fn math_leaf(id: IntrinsicId, first: Option) -> Option { + Some(match id { + IntrinsicId::MathKernelAbs | IntrinsicId::MathAbs => match first? { + TypeInterner::INT64 => NativeLeaf::IntAbs, + TypeInterner::FLOAT64 => NativeLeaf::FloatAbs, + _ => return None, + }, + IntrinsicId::MathKernelMin | IntrinsicId::MathMin => match first? { + TypeInterner::INT64 => NativeLeaf::IntMin, + TypeInterner::FLOAT64 => NativeLeaf::FloatMin, + _ => return None, + }, + IntrinsicId::MathKernelMax | IntrinsicId::MathMax => match first? { + TypeInterner::INT64 => NativeLeaf::IntMax, + TypeInterner::FLOAT64 => NativeLeaf::FloatMax, + _ => return None, + }, + IntrinsicId::MathSqrt => NativeLeaf::Sqrt, + IntrinsicId::MathFloor => NativeLeaf::Floor, + IntrinsicId::MathCeil => NativeLeaf::Ceil, + IntrinsicId::MathRound => NativeLeaf::Round, + IntrinsicId::MathLog => NativeLeaf::Log, + IntrinsicId::MathLog2 => NativeLeaf::Log2, + IntrinsicId::MathLog10 => NativeLeaf::Log10, + IntrinsicId::MathSin => NativeLeaf::Sin, + IntrinsicId::MathCos => NativeLeaf::Cos, + IntrinsicId::MathTan => NativeLeaf::Tan, + IntrinsicId::MathPow => NativeLeaf::Pow, + IntrinsicId::MathPi => NativeLeaf::Pi, + IntrinsicId::MathE => NativeLeaf::E, + IntrinsicId::MathClamp => NativeLeaf::Clamp, + IntrinsicId::MathMod => NativeLeaf::Mod, + IntrinsicId::MathGcd => NativeLeaf::Gcd, + IntrinsicId::MathLcm => NativeLeaf::Lcm, + IntrinsicId::MathFactorial => NativeLeaf::Factorial, + _ => return None, + }) +} + +pub(crate) fn bytes_leaf(id: IntrinsicId) -> Option { + Some(match id { + IntrinsicId::Int64FromString => NativeLeaf::ParseInt, + IntrinsicId::Uint64FromString => NativeLeaf::ParseUint, + IntrinsicId::Float64FromString => NativeLeaf::ParseFloat, + IntrinsicId::BytesNew => NativeLeaf::BytesNew, + IntrinsicId::BytesLength => NativeLeaf::BytesLength, + IntrinsicId::BytesFromString => NativeLeaf::BytesFromString, + IntrinsicId::BytesConcat => NativeLeaf::BytesConcat, + IntrinsicId::BytesSlice => NativeLeaf::BytesSlice, + IntrinsicId::BytesToHex => NativeLeaf::BytesToHex, + IntrinsicId::BytesGet => NativeLeaf::BytesGet, + IntrinsicId::BytesToString => NativeLeaf::BytesToString, + IntrinsicId::BytesFromHex => NativeLeaf::BytesFromHex, + IntrinsicId::EncodingBase64Encode => NativeLeaf::EncodingBase64Encode, + IntrinsicId::EncodingBase64Decode => NativeLeaf::EncodingBase64Decode, + IntrinsicId::EncodingHexDecode => NativeLeaf::EncodingHexDecode, + IntrinsicId::EncodingUrlEncode => NativeLeaf::EncodingUrlEncode, + IntrinsicId::EncodingUrlDecode => NativeLeaf::EncodingUrlDecode, + IntrinsicId::EncodingFormEncode => NativeLeaf::EncodingFormEncode, + IntrinsicId::EncodingFormDecode => NativeLeaf::EncodingFormDecode, + IntrinsicId::CsvParse => NativeLeaf::CsvParse, + IntrinsicId::CsvParseWithHeader => NativeLeaf::CsvParseWithHeader, + IntrinsicId::CsvStringify => NativeLeaf::CsvStringify, + IntrinsicId::CryptoSha256 => NativeLeaf::CryptoSha256, + IntrinsicId::CryptoSha512 => NativeLeaf::CryptoSha512, + IntrinsicId::CryptoMd5 => NativeLeaf::CryptoMd5, + IntrinsicId::CryptoHmacSha256 => NativeLeaf::CryptoHmacSha256, + _ => return None, + }) +} + +pub(crate) fn list_intrinsic(id: IntrinsicId) -> bool { + matches!( + id, + IntrinsicId::ListNew + | IntrinsicId::ListLength + | IntrinsicId::ListAppend + | IntrinsicId::ListInsertAt + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListGetClone + | IntrinsicId::ListSum + | IntrinsicId::ListSort + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListIsSorted + | IntrinsicId::ListSwap + ) +} +pub(crate) fn set_intrinsic(id: IntrinsicId) -> bool { + matches!( + id, + IntrinsicId::SetNew + | IntrinsicId::SetAdd + | IntrinsicId::SetRemove + | IntrinsicId::SetContains + | IntrinsicId::SetLength + ) +} +pub(crate) fn map_intrinsic(id: IntrinsicId) -> bool { + matches!( + id, + IntrinsicId::MapNew + | IntrinsicId::MapLength + | IntrinsicId::MapHas + | IntrinsicId::MapGet + | IntrinsicId::MapInsert + | IntrinsicId::MapRemove + | IntrinsicId::MapFromLists + ) +} +pub(crate) fn map_types( + id: IntrinsicId, + args: &[Expression], + result: TypeId, + types: &TypeInterner, +) -> Option<(TypeId, TypeId)> { + let ty = if matches!(id, IntrinsicId::MapNew | IntrinsicId::MapFromLists) { + result + } else { + args.first()?.ty + }; + if let Type::Map(key, value) = types.resolve(ty) { + Some((*key, *value)) + } else { + None + } +} +pub(crate) fn set_element( + id: IntrinsicId, + args: &[Expression], + result: TypeId, + types: &TypeInterner, +) -> Option { + let ty = if id == IntrinsicId::SetNew { + result + } else { + args.first()?.ty + }; + if let Type::Set(inner) = types.resolve(ty) { + Some(*inner) + } else { + None + } +} +fn set_element_supported(types: &TypeInterner, element: TypeId) -> bool { + let mut current = element; + for _ in 0..types.len() { + match types.resolve(current) { + Type::Refinement { base, .. } => current = *base, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Bool + | Type::String => return true, + _ => return false, + } + } + false +} +pub(crate) fn list_sort_kind(types: &TypeInterner, element: TypeId) -> Option { + let mut current = element; + for _ in 0..types.len() { + match types.resolve(current) { + Type::Refinement { base, .. } => current = *base, + _ => return primitive_list_kind(types, current), + } + } + None +} + +fn primitive_list_kind(types: &TypeInterner, element: TypeId) -> Option { + Some(match types.resolve(element) { + Type::Int8 => NativeSortKind::Int8, + Type::Int16 => NativeSortKind::Int16, + Type::Int32 => NativeSortKind::Int32, + Type::Int64 => NativeSortKind::Int64, + Type::Uint8 => NativeSortKind::Uint8, + Type::Uint16 => NativeSortKind::Uint16, + Type::Uint32 => NativeSortKind::Uint32, + Type::Uint64 => NativeSortKind::Uint64, + Type::Float32 => NativeSortKind::Float32, + Type::Float64 => NativeSortKind::Float64, + Type::Bool => NativeSortKind::Bool, + Type::String => NativeSortKind::String, + _ => return None, + }) +} + +pub(crate) fn list_sum_kind(types: &TypeInterner, element: TypeId) -> Option { + // Sorting preserves existing refined values; summing can violate their predicates. + primitive_numeric_kind(types, element) +} + +fn primitive_numeric_kind(types: &TypeInterner, element: TypeId) -> Option { + let kind = primitive_list_kind(types, element)?; + match kind { + NativeSortKind::Int8 + | NativeSortKind::Int16 + | NativeSortKind::Int32 + | NativeSortKind::Int64 + | NativeSortKind::Uint8 + | NativeSortKind::Uint16 + | NativeSortKind::Uint32 + | NativeSortKind::Uint64 + | NativeSortKind::Float32 + | NativeSortKind::Float64 => Some(kind), + NativeSortKind::Bool | NativeSortKind::String => None, + } +} + +// Narrow numeric values use the interpreter's wider value variants but retain +// their checked carrier width here. Unsupported shapes compare equal/sorted. +pub(crate) fn list_comparison_kind( + types: &TypeInterner, + element: TypeId, +) -> Option { + list_sort_kind(types, element) +} + +pub(crate) fn math_aggregate_kind(types: &TypeInterner, list: TypeId) -> Option { + let Type::List(element) = types.resolve(list) else { + return None; + }; + primitive_numeric_kind(types, *element) +} +pub(crate) fn list_element( + id: IntrinsicId, + args: &[Expression], + result: TypeId, + types: &TypeInterner, +) -> Option { + let ty = if id == IntrinsicId::ListNew { + result + } else { + args.first()?.ty + }; + if let Type::List(inner) = types.resolve(ty) { + Some(*inner) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, Span}; + use jett_hir::ExpressionKind; + + #[test] + fn list_sort_preserves_primitive_carriers_through_refinements() { + let mut types = TypeInterner::new(); + for (base, expected) in [ + (TypeInterner::INT8, NativeSortKind::Int8), + (TypeInterner::INT16, NativeSortKind::Int16), + (TypeInterner::INT32, NativeSortKind::Int32), + (TypeInterner::INT64, NativeSortKind::Int64), + (TypeInterner::UINT8, NativeSortKind::Uint8), + (TypeInterner::UINT16, NativeSortKind::Uint16), + (TypeInterner::UINT32, NativeSortKind::Uint32), + (TypeInterner::UINT64, NativeSortKind::Uint64), + (TypeInterner::FLOAT32, NativeSortKind::Float32), + (TypeInterner::FLOAT64, NativeSortKind::Float64), + (TypeInterner::BOOL, NativeSortKind::Bool), + (TypeInterner::STRING, NativeSortKind::String), + ] { + let refined = types.intern(Type::Refinement { + name: "Refined".into(), + base, + }); + let nested = types.intern(Type::Refinement { + name: "Nested".into(), + base: refined, + }); + for element in [base, refined, nested] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + assert_eq!(list_sort_kind(&types, element), Some(expected)); + assert_eq!(list_comparison_kind(&types, element), Some(expected)); + assert_eq!( + verify_intrinsic( + IntrinsicId::ListSort, + std::slice::from_ref(&arg), + arg.ty, + &types + ), + Ok(()) + ); + assert_eq!( + verify_intrinsic( + IntrinsicId::ListIsSorted, + std::slice::from_ref(&arg), + TypeInterner::BOOL, + &types, + ), + Ok(()) + ); + let rows = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(arg.ty)), + span: arg.span, + }; + let index = Expression { + kind: ExpressionKind::Nothing, + ty: TypeInterner::INT64, + span: arg.span, + }; + let result = rows.ty; + assert_eq!( + verify_intrinsic(IntrinsicId::ListSortByIndex, &[rows, index], result, &types), + Ok(()) + ); + if element != base { + assert_eq!(list_sum_kind(&types, element), None); + assert!( + verify_intrinsic( + IntrinsicId::ListSum, + std::slice::from_ref(&arg), + element, + &types, + ) + .is_err() + ); + } + } + } + } + + #[test] + fn list_sort_requires_the_identical_nominal_list_result() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "Positive".into(), + base: TypeInterner::INT64, + }); + let nonnegative = types.intern(Type::Refinement { + name: "Nonnegative".into(), + base: TypeInterner::INT64, + }); + let nested = types.intern(Type::Refinement { + name: "SmallPositive".into(), + base: positive, + }); + for element in [TypeInterner::INT64, positive, nonnegative, nested] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + for result_element in [TypeInterner::INT64, positive, nonnegative, nested] { + let result = types.intern(Type::List(result_element)); + assert_eq!( + verify_intrinsic( + IntrinsicId::ListSort, + std::slice::from_ref(&arg), + result, + &types + ) + .is_ok(), + element == result_element + ); + } + } + } + + #[test] + fn list_sort_rejects_secret_and_aggregate_carriers_through_refinements() { + let mut types = TypeInterner::new(); + let secret = types.intern(Type::Secret(TypeInterner::INT64)); + let list = types.intern(Type::List(TypeInterner::INT64)); + let optional = types.intern(Type::Optional(TypeInterner::STRING)); + for base in [ + TypeInterner::NOTHING, + TypeInterner::BYTES, + secret, + list, + optional, + ] { + let refined = types.intern(Type::Refinement { + name: "Unsupported".into(), + base, + }); + for element in [base, refined] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + assert_eq!(list_sort_kind(&types, element), None); + assert_eq!(list_comparison_kind(&types, element), None); + assert!( + verify_intrinsic( + IntrinsicId::ListSort, + std::slice::from_ref(&arg), + arg.ty, + &types, + ) + .is_err() + ); + } + } + } + + #[test] + fn math_aggregates_admit_all_primitive_numeric_lists_with_float64_results() { + let mut types = TypeInterner::new(); + for (element, kind) in [ + (TypeInterner::INT8, NativeSortKind::Int8), + (TypeInterner::INT16, NativeSortKind::Int16), + (TypeInterner::INT32, NativeSortKind::Int32), + (TypeInterner::INT64, NativeSortKind::Int64), + (TypeInterner::UINT8, NativeSortKind::Uint8), + (TypeInterner::UINT16, NativeSortKind::Uint16), + (TypeInterner::UINT32, NativeSortKind::Uint32), + (TypeInterner::UINT64, NativeSortKind::Uint64), + (TypeInterner::FLOAT32, NativeSortKind::Float32), + (TypeInterner::FLOAT64, NativeSortKind::Float64), + ] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + assert_eq!(math_aggregate_kind(&types, arg.ty), Some(kind)); + for id in [IntrinsicId::MathAverage, IntrinsicId::MathMedian] { + assert_eq!( + verify_intrinsic( + id, + std::slice::from_ref(&arg), + TypeInterner::FLOAT64, + &types + ), + Ok(()) + ); + assert!( + verify_intrinsic(id, std::slice::from_ref(&arg), TypeInterner::INT64, &types) + .is_err() + ); + assert!(verify_intrinsic(id, &[], TypeInterner::FLOAT64, &types).is_err()); + } + } + } + + #[test] + fn math_aggregates_reject_refined_secret_and_nonnumeric_elements() { + let mut types = TypeInterner::new(); + let refined = types.intern(Type::Refinement { + name: "Positive".into(), + base: TypeInterner::INT64, + }); + let nested = types.intern(Type::Refinement { + name: "SmallPositive".into(), + base: refined, + }); + let secret = types.intern(Type::Secret(TypeInterner::FLOAT64)); + let list = types.intern(Type::List(TypeInterner::INT64)); + let secret_list = types.intern(Type::Secret(list)); + let mut rejected = vec![TypeInterner::INT64, secret_list]; + for element in [ + refined, + nested, + secret, + TypeInterner::BOOL, + TypeInterner::STRING, + TypeInterner::BYTES, + list, + ] { + rejected.push(types.intern(Type::List(element))); + } + for ty in rejected { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty, + span: Span::new(FileId::new(0), 0, 1), + }; + assert_eq!(math_aggregate_kind(&types, ty), None); + for id in [IntrinsicId::MathAverage, IntrinsicId::MathMedian] { + assert!( + verify_intrinsic( + id, + std::slice::from_ref(&arg), + TypeInterner::FLOAT64, + &types + ) + .is_err() + ); + } + } + } + + #[test] + fn list_sum_admits_only_matching_primitive_numeric_results() { + let mut types = TypeInterner::new(); + for element in [ + TypeInterner::INT8, + TypeInterner::INT16, + TypeInterner::INT32, + TypeInterner::INT64, + TypeInterner::UINT8, + TypeInterner::UINT16, + TypeInterner::UINT32, + TypeInterner::UINT64, + TypeInterner::FLOAT32, + TypeInterner::FLOAT64, + ] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + assert!(list_sum_kind(&types, element).is_some()); + assert_eq!( + verify_intrinsic(IntrinsicId::ListSum, &[arg.clone()], element, &types), + Ok(()) + ); + assert!( + verify_intrinsic(IntrinsicId::ListSum, &[arg], TypeInterner::BOOL, &types).is_err() + ); + } + let refinement = types.intern(Type::Refinement { + name: "Positive".into(), + base: TypeInterner::INT64, + }); + for element in [ + TypeInterner::BOOL, + TypeInterner::STRING, + TypeInterner::BYTES, + refinement, + ] { + let arg = Expression { + kind: ExpressionKind::Nothing, + ty: types.intern(Type::List(element)), + span: Span::new(FileId::new(0), 0, 1), + }; + assert_eq!(list_sum_kind(&types, element), None); + assert!(verify_intrinsic(IntrinsicId::ListSum, &[arg], element, &types).is_err()); + } + } +} diff --git a/crates/jett_codegen_cranelift/src/verify.rs b/crates/jett_codegen_cranelift/src/verify.rs new file mode 100644 index 00000000..b0bbc200 --- /dev/null +++ b/crates/jett_codegen_cranelift/src/verify.rs @@ -0,0 +1,4520 @@ +use std::{cell::RefCell, collections::HashSet}; +mod carriers; + +use jett_common::{SourceOrigin, Span}; +use jett_hir::{ + BinaryOp, Expression, ExpressionKind, FunctionId, InterfaceFunctionAdapter, UnaryOp, +}; +use jett_mir::{ + Function, Program, SequenceSource, Statement, StatementKind, Terminator, TerminatorKind, +}; +use jett_types::{BitfieldFieldKind, Type, TypeId, TypeInterner}; + +use crate::reachability::reachable_function_ids_with_types; +use crate::{CodegenError, symbol_name}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ScalarKind { + SignedInteger(u16), + UnsignedInteger(u16), + Float(u16), + Bool, + Nothing, + String, + Bytes, + Sum, + List, + Set, + Map, + Struct, + Enum, + Bitfield, + Machine, + Construction, + Function, + Actor, + Stdout, + Clock, + Random, + Environment, + Graphics, + OpaqueCapability, + // Only original-body integrity checking of a descriptor-only function uses + // this kind. scalar_kind never supplies a runtime carrier for bare Never. + Uninhabited, +} + +pub(crate) fn known_unit_enum_variant(expression: &Expression) -> bool { + match &expression.kind { + ExpressionKind::EnumConstruct { payloads, .. } => payloads.is_empty(), + ExpressionKind::View(inner) | ExpressionKind::Clone(inner) => { + known_unit_enum_variant(inner) + } + _ => false, + } +} + +impl ScalarKind { + pub(crate) fn is_integer(self) -> bool { + matches!(self, Self::SignedInteger(_) | Self::UnsignedInteger(_)) + } + + fn is_numeric(self) -> bool { + self.is_integer() || matches!(self, Self::Float(_)) + } +} + +fn native_constructible_struct(types: &TypeInterner, ty: TypeId) -> bool { + matches!(types.resolve(ty), Type::Struct(_)) +} + +fn native_constructible_bitfield(types: &TypeInterner, ty: TypeId) -> bool { + let Type::Bitfield(id) = types.resolve(ty) else { + return false; + }; + types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| match field.kind { + BitfieldFieldKind::Bits { width } if width <= 64 => match types.resolve(field.ty) { + Type::Int64 | Type::Uint64 => true, + Type::Enum(enum_id) => types + .resolve_enum(*enum_id) + .variants + .iter() + .all(|variant| variant.fields.is_empty()), + _ => false, + }, + BitfieldFieldKind::Payload => { + !matches!(types.resolve(field.ty), Type::Refinement { .. }) + } + _ => false, + }) +} + +fn native_constructible_record(types: &TypeInterner, ty: TypeId) -> bool { + native_constructible_struct(types, ty) || native_constructible_bitfield(types, ty) +} + +fn native_builder_value_type_supported(types: &TypeInterner, owner: TypeId, value: TypeId) -> bool { + if matches!( + types.resolve(owner), + Type::Struct(_) | Type::Enum(_) | Type::Machine(_) | Type::MachineState { .. } + ) { + // Reflected completion now validates the checked field predicates. + return true; + } + let fields: Vec = match types.resolve(owner) { + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect(), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .map(|field| field.ty) + .collect(), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|(_, ty)| *ty)) + .collect(), + Type::Machine(id) | Type::MachineState { machine: id, .. } => types + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|(_, ty)| *ty)) + .collect(), + _ => return true, + }; + if fields + .iter() + .any(|field_ty| matches!(types.resolve(*field_ty), Type::Refinement { .. })) + && !fields.contains(&value) + { + return false; + } + // The native builder receives already validated refinement values. A base + // value can only be accepted once finish can invoke the refinement predicate. + fields.iter().all(|field_ty| { + let mut current = *field_ty; + while let Type::Refinement { base, .. } = types.resolve(current) { + if *base == value { + return false; + } + current = *base; + } + true + }) +} + +fn native_constructible_enum(types: &TypeInterner, ty: TypeId) -> bool { + matches!(types.resolve(ty), Type::Enum(_)) +} + +fn native_constructible_machine(types: &TypeInterner, ty: TypeId) -> bool { + matches!( + types.resolve(ty), + Type::Machine(_) | Type::MachineState { .. } + ) +} + +fn native_constructible_builder_kind(types: &TypeInterner, ty: TypeId) -> Option<&'static str> { + match types.resolve(ty) { + Type::Struct(_) if native_constructible_struct(types, ty) => Some("struct"), + Type::Bitfield(_) if native_constructible_bitfield(types, ty) => Some("bitfield"), + Type::Enum(_) if native_constructible_enum(types, ty) => Some("enum"), + Type::Machine(_) if native_constructible_machine(types, ty) => Some("machine"), + Type::MachineState { .. } if native_constructible_machine(types, ty) => { + Some("machine_state") + } + _ => None, + } +} + +/// A real function descriptor can exist although its invocation needs an +/// impossible argument. Captures are values, so they must never supply this proof. +pub(crate) fn descriptor_only_function(function: &Function) -> bool { + function.capture_count <= function.params.len() + && !function.params[..function.capture_count] + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) + && function.params[function.capture_count..] + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) +} + +/// Check the original uncallable bodies before native preparation can erase a +/// malformed local, function reference or capture. Their source bodies will not +/// be emitted, but their typed contracts still apply. +pub(crate) fn verify_descriptor_bodies( + program: &Program, + types: &TypeInterner, +) -> Result<(), CodegenError> { + verify_descriptor_bodies_inner(program, types, None) +} + +pub(crate) fn verify_resource_descriptor_bodies( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result<(), CodegenError> { + verify_descriptor_bodies_inner(plan.program(), plan.types(), Some(plan)) +} + +fn verify_descriptor_bodies_inner<'a>( + program: &'a Program, + types: &'a TypeInterner, + resource: Option<&'a jett_mir::ResourceOwnershipPlan<'a>>, +) -> Result<(), CodegenError> { + jett_mir::validate_native_actor_graphs(program).map_err(CodegenError::Backend)?; + jett_mir::validate_field_updates(program, types).map_err(CodegenError::Backend)?; + jett_mir::validate_call_ownership(program, types).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_reflected_callable_plans(program, types) + .map_err(CodegenError::InvalidMir)?; + // Shared inline tables and impossible sum arms contain valid Never metadata + // that need not allocate a native frame slot. Validate original type shapes + // without demanding a carrier for an absent function result; every surviving + // callable Never use is still rejected after preparation. + for function in &program.functions { + for local in &function.locals { + for (ty, role) in [(local.ty, "local"), (local.debug_ty, "debug local")] { + if !resource.is_some_and(|plan| plan.type_requires_custody(ty)) + && !exact_uninhabited_metadata_type(types, ty) + { + if let Some(proof) = + jett_mir::native_graph_storage_for_local(function, types, local) + { + graph_storage_kind( + types, + &proof, + format!("original {role} `{}`", local.name), + )?; + } else { + scalar_kind_inner( + types, + ty, + format!("original {role} `{}`", local.name), + &mut HashSet::new(), + TypeValidationPhase::OriginalMetadata, + )?; + } + } + } + } + } + let mut functions = Vec::new(); + let mut by_mir_index = vec![None; program.functions.len()]; + for function in &program.functions { + if !function.params[function.capture_count..] + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) + { + continue; + } + if function.params[..function.capture_count] + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) + { + return Err(CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: "descriptor capture cannot be uninhabited".into(), + }); + } + if function.params[..function.capture_count] + .iter() + .any(|parameter| parameter.mode != jett_mir::ParamMode::Owned) + { + return Err(CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: "descriptor capture must use owned environment storage".into(), + }); + } + super::emit::debug::function_label(function).map_err(|message| { + CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: message.into(), + } + })?; + by_mir_index[function.id.index() as usize] = Some(functions.len()); + functions.push(VerifiedFunction { + mir_id: function.id, + symbol: symbol_name(&function.identity, types)?, + }); + } + let verified = VerifiedProgram { + by_mir_index, + functions, + }; + let verifier = Verifier { + program, + types, + verified: &verified, + phase: VerifierPhase::DescriptorMetadata, + metadata_functions: RefCell::new(HashSet::new()), + resource, + }; + for function in &program.functions { + if descriptor_only_function(function) { + verifier.function(function)?; + } + } + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct VerifiedFunction { + pub(crate) mir_id: FunctionId, + pub(crate) symbol: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct VerifiedProgram { + functions: Vec, + by_mir_index: Vec>, +} + +impl VerifiedProgram { + pub(crate) fn functions(&self) -> &[VerifiedFunction] { + &self.functions + } + + pub(crate) fn get(&self, id: FunctionId) -> Option<&VerifiedFunction> { + let index = usize::try_from(id.index()).ok()?; + let verified_index = self.by_mir_index.get(index).copied().flatten()?; + self.functions + .get(verified_index) + .filter(|function| function.mir_id == id) + } +} + +/// Unit-only access to the existing native expression integrity gate. +/// Outer emission independently checks the original caller ownership packet; +/// this helper supplies no preparation, callable frame or emission authority. +#[cfg(test)] +pub(crate) fn verify_callable_expression_for_test( + program: &Program, + types: &TypeInterner, + function: &Function, + expression: &Expression, +) -> Result<(), CodegenError> { + let verified = VerifiedProgram { + functions: Vec::new(), + by_mir_index: vec![None; program.functions.len()], + }; + let verifier = Verifier { + program, + types, + verified: &verified, + phase: VerifierPhase::CallableNative, + metadata_functions: RefCell::new(HashSet::new()), + resource: None, + }; + verifier.expression(function, expression) +} + +pub(crate) fn verify_program( + program: &Program, + types: &TypeInterner, +) -> Result { + jett_mir::validate_native_actor_graphs(program).map_err(CodegenError::Backend)?; + jett_mir::validate_field_updates(program, types).map_err(CodegenError::Backend)?; + jett_mir::validate_call_ownership(program, types).map_err(CodegenError::InvalidMir)?; + jett_mir::validate_reflected_callable_plans(program, types) + .map_err(CodegenError::InvalidMir)?; + + // Reachability may omit lifted bodies, but malformed source metadata must + // not disappear with an unused descriptor. + for function in &program.functions { + super::emit::debug::function_label(function).map_err(|message| { + CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: message.into(), + } + })?; + } + + for (&owner, &method) in &program.equality_methods { + let (_, target) = function_by_id(program, method)?; + if owner.index() as usize >= types.len() + || !matches!(types.resolve(owner), Type::Struct(_)) + || target.capture_count != 0 + || target.params.len() != 2 + || target.return_type != TypeInterner::BOOL + || target.params.iter().any(|parameter| { + parameter.ty != owner || parameter.mode != jett_mir::ParamMode::View + }) + { + return Err(CodegenError::InvalidMirContract { + function: target.identity.declaration.name.clone(), + span: target.span, + message: "enum equality target must have two exact struct views and return bool" + .into(), + }); + } + } + let reachable = reachable_function_ids_with_types(program, types)?; + let mut functions = Vec::with_capacity(reachable.len()); + let mut by_mir_index = vec![None; program.functions.len()]; + let mut unique_symbols = HashSet::with_capacity(reachable.len()); + for function_id in reachable { + let (function_index, function) = function_by_id(program, function_id)?; + let symbol = symbol_name(&function.identity, types)?; + if !unique_symbols.insert(symbol.clone()) { + return Err(CodegenError::DuplicateSymbol(symbol)); + } + let verified_index = functions.len(); + by_mir_index[function_index] = Some(verified_index); + functions.push(VerifiedFunction { + mir_id: function_id, + symbol, + }); + } + + let verified = VerifiedProgram { + functions, + by_mir_index, + }; + + for verified_function in verified.functions() { + let (_, function) = function_by_id(program, verified_function.mir_id)?; + let verifier = Verifier { + program, + types, + verified: &verified, + phase: if descriptor_only_function(function) { + VerifierPhase::DescriptorMetadata + } else { + VerifierPhase::CallableNative + }, + metadata_functions: RefCell::new(HashSet::new()), + resource: None, + }; + verifier.function(function)?; + } + Ok(verified) +} + +pub(crate) fn verify_resource_program( + plan: &jett_mir::ResourceOwnershipPlan<'_>, +) -> Result { + let program = plan.program(); + let types = plan.types(); + // Reachability may omit lifted bodies, but malformed source metadata must + // not disappear with an unused descriptor. + for function in &program.functions { + super::emit::debug::function_label(function).map_err(|message| { + CodegenError::InvalidMirContract { + function: function.identity.declaration.name.clone(), + span: function.span, + message: message.into(), + } + })?; + } + + for (&owner, &method) in &program.equality_methods { + let (_, target) = function_by_id(program, method)?; + if owner.index() as usize >= types.len() + || !matches!(types.resolve(owner), Type::Struct(_)) + || target.capture_count != 0 + || target.params.len() != 2 + || target.return_type != TypeInterner::BOOL + || target.params.iter().any(|parameter| { + parameter.ty != owner || parameter.mode != jett_mir::ParamMode::View + }) + { + return Err(CodegenError::InvalidMirContract { + function: target.identity.declaration.name.clone(), + span: target.span, + message: "enum equality target must have two exact struct views and return bool" + .into(), + }); + } + } + let selected = crate::reachability::reachable_resource_function_ids(plan)?; + let mut functions = Vec::new(); + let mut by_mir_index = vec![None; program.functions.len()]; + let mut symbols = HashSet::new(); + for id in selected { + let (index, function) = function_by_id(program, id)?; + let symbol = symbol_name(&function.identity, types)?; + if !symbols.insert(symbol.clone()) { + return Err(CodegenError::DuplicateSymbol(symbol)); + } + by_mir_index[index] = Some(functions.len()); + functions.push(VerifiedFunction { mir_id: id, symbol }); + } + let verified = VerifiedProgram { + functions, + by_mir_index, + }; + + for function in &program.functions { + // The plan's archive authenticates all original Resource type graphs; + // unrelated ordinary metadata keeps the previous complete type visitor. + for local in &function.locals { + for ty in [local.ty, local.debug_ty] { + if !plan.type_requires_custody(ty) && !exact_uninhabited_metadata_type(types, ty) { + if let Some(proof) = + jett_mir::native_graph_storage_for_local(function, types, local) + { + graph_storage_kind(types, &proof, "original ordinary companion local")?; + } else { + scalar_kind_inner( + types, + ty, + "original ordinary companion local".into(), + &mut HashSet::new(), + TypeValidationPhase::OriginalMetadata, + )?; + } + } + } + } + if verified.get(function.id).is_some() { + let verifier = Verifier { + program, + types, + verified: &verified, + phase: if descriptor_only_function(function) { + VerifierPhase::DescriptorMetadata + } else { + VerifierPhase::CallableNative + }, + metadata_functions: RefCell::new(HashSet::new()), + resource: Some(plan), + }; + verifier.function(function)?; + } + } + Ok(verified) +} + +fn function_by_id(program: &Program, id: FunctionId) -> Result<(usize, &Function), CodegenError> { + let index = usize::try_from(id.index()).map_err(|_| { + CodegenError::Backend("reached MIR function index does not fit this host".to_string()) + })?; + let function = program + .functions + .get(index) + .filter(|function| function.id == id) + .ok_or_else(|| { + CodegenError::Backend( + "verified reachable function is absent from the original MIR table".to_string(), + ) + })?; + Ok((index, function)) +} + +/// Only these exact checked shapes describe an absent value in latent bodies. +/// This does not grant either shape a runtime carrier or peel other qualifiers. +fn exact_uninhabited_metadata_type(types: &TypeInterner, ty: TypeId) -> bool { + ty == TypeInterner::NEVER + || ((ty.index() as usize) < types.len() + && matches!(types.resolve(ty), Type::Secret(inner) if *inner == TypeInterner::NEVER)) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TypeValidationPhase { + OriginalMetadata, + NativeValue, + // Machine representation only, after the strict verifier and any private + // occurrence-selected graph storage proof have authenticated execution. + NativeStorage, +} + +pub(crate) fn scalar_kind( + types: &TypeInterner, + ty: TypeId, + context: impl Into, +) -> Result { + scalar_kind_inner( + types, + ty, + context.into(), + &mut HashSet::new(), + TypeValidationPhase::NativeValue, + ) +} +/// Machine storage mapping is independent of recursive Source type admission. +/// Emission consumes a VerifiedProgram/fresh Resource plan; this helper cannot +/// create either and is never used as the default verifier type visitor. +pub(crate) fn native_storage_kind( + types: &TypeInterner, + ty: TypeId, + context: impl Into, +) -> Result { + scalar_kind_inner( + types, + ty, + context.into(), + &mut HashSet::new(), + TypeValidationPhase::NativeStorage, + ) +} + +/// Only an opaque exact occurrence/header proof selects graph metadata storage. +fn graph_storage_kind( + types: &TypeInterner, + proof: &jett_mir::GraphStorageProof, + context: impl Into, +) -> Result { + native_storage_kind(types, proof.ty(), context) +} + +fn scalar_kind_inner( + types: &TypeInterner, + ty: TypeId, + context: String, + seen: &mut HashSet, + phase: TypeValidationPhase, +) -> Result { + let type_count = u32::try_from(types.len()).unwrap_or(u32::MAX); + if ty.index() >= type_count { + return Err(CodegenError::UnsupportedType { + type_name: format!("", ty.index()), + context, + }); + } + if phase == TypeValidationPhase::NativeStorage { + // Containers and actor/function handles have one fixed native scalar + // storage representation. Their payload/signature integrity was checked + // before emission; no runtime Resource leaf is admitted here. + let opaque = match types.resolve(ty) { + Type::Struct(_) | Type::Interface(_) => Some(ScalarKind::Struct), + Type::Enum(_) => Some(ScalarKind::Enum), + Type::Bitfield(_) => Some(ScalarKind::Bitfield), + Type::Machine(_) | Type::MachineState { .. } => Some(ScalarKind::Machine), + Type::Function { .. } => Some(ScalarKind::Function), + Type::Actor(_) => Some(ScalarKind::Actor), + Type::List(_) => Some(ScalarKind::List), + Type::Set(_) => Some(ScalarKind::Set), + Type::Map(_, _) => Some(ScalarKind::Map), + Type::Optional(_) | Type::Result(_, _) => Some(ScalarKind::Sum), + Type::TypeConstruction => Some(ScalarKind::Construction), + _ => None, + }; + if let Some(kind) = opaque { + return Ok(kind); + } + } + let kind = match types.resolve(ty) { + Type::Int8 => ScalarKind::SignedInteger(8), + Type::Int16 => ScalarKind::SignedInteger(16), + Type::Int32 => ScalarKind::SignedInteger(32), + Type::Int64 => ScalarKind::SignedInteger(64), + Type::Uint8 => ScalarKind::UnsignedInteger(8), + Type::Uint16 => ScalarKind::UnsignedInteger(16), + Type::Uint32 => ScalarKind::UnsignedInteger(32), + Type::Uint64 => ScalarKind::UnsignedInteger(64), + Type::Float32 => ScalarKind::Float(32), + Type::Float64 => ScalarKind::Float(64), + Type::Bool => ScalarKind::Bool, + Type::Nothing => ScalarKind::Nothing, + Type::String => ScalarKind::String, + Type::Bytes => ScalarKind::Bytes, + Type::Secret(inner) | Type::Refinement { base: inner, .. } => { + return scalar_kind_inner(types, *inner, context, seen, phase); + } + Type::Struct(id) => { + if seen.insert(ty) { + for (_, field) in &types.resolve_struct(*id).fields { + scalar_kind_inner(types, *field, "struct field".into(), seen, phase)?; + } + } + ScalarKind::Struct + } + Type::Enum(id) => { + if seen.insert(ty) { + for variant in &types.resolve_enum(*id).variants { + for (_, field) in &variant.fields { + scalar_kind_inner(types, *field, "enum payload".into(), seen, phase)?; + } + } + } + ScalarKind::Enum + } + Type::Bitfield(id) => { + if seen.insert(ty) { + for field in &types.resolve_bitfield(*id).fields { + scalar_kind_inner(types, field.ty, "bitfield field".into(), seen, phase)?; + } + } + ScalarKind::Bitfield + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + if seen.insert(ty) { + for state in &types.resolve_machine(*id).states { + for (_, field) in &state.fields { + scalar_kind_inner(types, *field, "machine payload".into(), seen, phase)?; + } + } + } + ScalarKind::Machine + } + Type::Function { + params, + view_params, + return_type, + } => { + if params.len() != view_params.len() { + return Err(CodegenError::UnsupportedType { + type_name: types.type_name(ty), + context: "function value view parameter metadata".into(), + }); + } + // A real descriptor needs an impossible input to omit its result + // carrier. Original metadata may also describe a return-only Never + // function in an arm that native preparation proves absent. This + // phase grants neither descriptor authority nor a runtime carrier. + let descriptor_only = params.contains(&TypeInterner::NEVER); + if seen.insert(ty) { + for param in params { + if !descriptor_only || *param != TypeInterner::NEVER { + scalar_kind_inner( + types, + *param, + "function value parameter".into(), + seen, + phase, + )?; + } + } + if !exact_uninhabited_metadata_type(types, *return_type) + || (!descriptor_only && phase == TypeValidationPhase::NativeValue) + { + scalar_kind_inner( + types, + *return_type, + "function value result".into(), + seen, + phase, + )?; + } + } + ScalarKind::Function + } + Type::Actor(id) => { + if seen.insert(ty) { + let actor = types.resolve_actor(*id); + for (_, field) in actor.capability_params.iter().chain(&actor.state_fields) { + scalar_kind_inner(types, *field, "actor state field".into(), seen, phase)?; + } + for message in &actor.messages { + for (_, parameter) in &message.params { + scalar_kind_inner( + types, + *parameter, + "actor message parameter".into(), + seen, + phase, + )?; + } + scalar_kind_inner( + types, + message.responds, + "actor message response".into(), + seen, + phase, + )?; + } + } + ScalarKind::Actor + } + Type::List(inner) => { + if *inner != TypeInterner::NEVER { + scalar_kind_inner(types, *inner, "list element".into(), seen, phase)?; + } + ScalarKind::List + } + Type::Set(inner) => { + scalar_kind_inner(types, *inner, "set element".into(), seen, phase)?; + ScalarKind::Set + } + Type::Map(key, value) => { + if *key != TypeInterner::NEVER { + scalar_kind_inner(types, *key, "map key".into(), seen, phase)?; + } + if *value != TypeInterner::NEVER { + scalar_kind_inner(types, *value, "map value".into(), seen, phase)?; + } + ScalarKind::Map + } + Type::TypeConstruction => ScalarKind::Construction, + Type::Interface(_) => ScalarKind::Struct, + Type::Optional(inner) => { + if *inner != TypeInterner::NEVER { + scalar_kind_inner(types, *inner, "optional payload".into(), seen, phase)?; + } + ScalarKind::Sum + } + Type::Result(ok, error) => { + if *ok != TypeInterner::NEVER { + scalar_kind_inner(types, *ok, "result success payload".into(), seen, phase)?; + } + if *error != TypeInterner::NEVER { + scalar_kind_inner(types, *error, "result failure payload".into(), seen, phase)?; + } + ScalarKind::Sum + } + Type::Capability(jett_types::CapabilityKind::Stdout) => ScalarKind::Stdout, + Type::Capability(jett_types::CapabilityKind::Clock) => ScalarKind::Clock, + Type::Capability(jett_types::CapabilityKind::Random) => ScalarKind::Random, + Type::Capability(jett_types::CapabilityKind::Environment) => ScalarKind::Environment, + Type::Capability(jett_types::CapabilityKind::Graphics) => ScalarKind::Graphics, + Type::Capability( + jett_types::CapabilityKind::Stderr + | jett_types::CapabilityKind::Stdin + | jett_types::CapabilityKind::Filesystem + | jett_types::CapabilityKind::Network + | jett_types::CapabilityKind::Process + | jett_types::CapabilityKind::Log, + ) => ScalarKind::OpaqueCapability, + unsupported => { + return Err(CodegenError::UnsupportedType { + type_name: types.type_name(ty), + context: format!("{context} ({unsupported:?})"), + }); + } + }; + Ok(kind) +} + +// An empty collection or an absent sum arm has no value of its inferred +// `never` element type. Its runtime representation is the same as the checked +// contextual type, so embedding it does not require a conversion. +fn never_value_compatible(types: &TypeInterner, expected: TypeId, actual: TypeId) -> bool { + if expected == actual { + return true; + } + if actual == TypeInterner::NEVER { + return true; + } + match (types.resolve(expected), types.resolve(actual)) { + (Type::List(expected), Type::List(actual)) + | (Type::Optional(expected), Type::Optional(actual)) + | (Type::Secret(expected), Type::Secret(actual)) => { + never_value_compatible(types, *expected, *actual) + } + (Type::Map(expected_key, expected_value), Type::Map(actual_key, actual_value)) + | (Type::Result(expected_key, expected_value), Type::Result(actual_key, actual_value)) => { + never_value_compatible(types, *expected_key, *actual_key) + && never_value_compatible(types, *expected_value, *actual_value) + } + _ => false, + } +} + +fn interface_conversion_shape( + types: &TypeInterner, + source: TypeId, + target: TypeId, + adapters: &[InterfaceFunctionAdapter], +) -> bool { + if jett_mir::move_values::is_secret(types, source) + && !jett_mir::move_values::is_secret(types, target) + && !matches!(types.resolve(target), Type::Interface(_)) + { + return false; + } + let source = jett_mir::move_values::representation_type(types, source); + let target = jett_mir::move_values::representation_type(types, target); + if source == target || source == TypeInterner::NEVER { + return true; + } + match (types.resolve(source), types.resolve(target)) { + (Type::Interface(_), _) | (_, Type::Interface(_)) => true, + (Type::List(a), Type::List(b)) | (Type::Optional(a), Type::Optional(b)) => { + interface_conversion_shape(types, *a, *b, adapters) + } + (Type::Map(ak, av), Type::Map(bk, bv)) | (Type::Result(ak, av), Type::Result(bk, bv)) => { + interface_conversion_shape(types, *ak, *bk, adapters) + && interface_conversion_shape(types, *av, *bv, adapters) + } + (Type::Function { .. }, Type::Function { .. }) => adapters + .iter() + .any(|entry| entry.source == source && entry.target == target), + _ => false, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum VerifierPhase { + /// Typed original-body integrity, with no callable frame or Never carrier. + DescriptorMetadata, + /// Prepared operations that will actually execute through the native ABI. + CallableNative, +} + +struct Verifier<'a> { + program: &'a Program, + types: &'a TypeInterner, + verified: &'a VerifiedProgram, + phase: VerifierPhase, + // This is an original-body integrity traversal, not callable reachability. + metadata_functions: RefCell>, + resource: Option<&'a jett_mir::ResourceOwnershipPlan<'a>>, +} + +impl Verifier<'_> { + fn value_kind( + &self, + _function: &Function, + ty: TypeId, + context: impl Into, + ) -> Result { + if self.phase == VerifierPhase::DescriptorMetadata { + if exact_uninhabited_metadata_type(self.types, ty) { + return Ok(ScalarKind::Uninhabited); + } + scalar_kind_inner( + self.types, + ty, + context.into(), + &mut HashSet::new(), + TypeValidationPhase::OriginalMetadata, + ) + } else { + scalar_kind(self.types, ty, context) + } + } + + fn local_kind( + &self, + function: &Function, + local: &jett_mir::Local, + context: impl Into, + ) -> Result { + if let Some(proof) = jett_mir::native_graph_storage_for_local(function, self.types, local) { + graph_storage_kind(self.types, &proof, context) + } else { + self.value_kind(function, local.ty, context) + } + } + fn expression_kind( + &self, + function: &Function, + expression: &Expression, + context: impl Into, + ) -> Result { + if let Some(proof) = + jett_mir::native_graph_storage_for_expression(function, self.types, expression) + { + graph_storage_kind(self.types, &proof, context) + } else { + self.value_kind(function, expression.ty, context) + } + } + fn function(&self, function: &Function) -> Result<(), CodegenError> { + if self.phase == VerifierPhase::DescriptorMetadata { + super::emit::debug::function_label(function) + .map_err(|message| self.contract_error(function, function.span, message))?; + symbol_name(&function.identity, self.types)?; + if !self.metadata_functions.borrow_mut().insert(function.id) { + return Ok(()); + } + } + self.reject_entry_predecessors(function)?; + + let name = self.function_name(function); + for param in &function.params { + if self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(param.ty) + }) { + continue; + } + let header = function.local(param.local).ok_or_else(|| { + self.contract_error( + function, + param.span, + "parameter has no exact current local header", + ) + })?; + self.local_kind( + function, + header, + format!("parameter `{}` of `{name}`", param.name), + )?; + } + if !(self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(function.return_type) + })) { + if let Some(proof) = jett_mir::native_graph_storage_for_return(function, self.types) { + graph_storage_kind(self.types, &proof, format!("return type of `{name}`"))?; + } else { + self.value_kind( + function, + function.return_type, + format!("return type of `{name}`"), + )?; + } + } + for local in &function.locals { + if self.resource.is_some_and(|plan| { + plan.function(function.id).is_some() && plan.type_requires_custody(local.ty) + }) { + continue; + } + self.local_kind( + function, + local, + format!("local `{}` of `{name}`", local.name), + )?; + if self.phase == VerifierPhase::DescriptorMetadata { + self.value_kind( + function, + local.debug_ty, + format!("debug type of original local `{}` of `{name}`", local.name), + )?; + } + } + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + self.statement(function, block.id, index, statement)?; + } + self.terminator(function, &block.terminator)?; + } + // Descriptor-only bodies retain their original typed contracts, but + // never execute and need neither a native sequence form nor an owning + // machine-code frame. Their caller still validates capture evaluation, + // initialization, ownership and cleanup normally. + if self.phase == VerifierPhase::CallableNative { + if let Some(plan) = self.resource.filter(|plan| { + plan.function(function.id).is_some() + || jett_mir::native_graph_storage_has_records(function, self.types) + }) { + jett_mir::ResourceCompanionPlan::analyze(plan, function.id) + .map_err(|message| self.contract_error(function, function.span, message))?; + } else { + jett_mir::move_values::MoveValuePlan::analyze(self.program, function, self.types) + .map_err(|message| self.contract_error(function, function.span, message))?; + } + } + Ok(()) + } + + /// Call only after the referencing edge's identity/signature/captures pass. + /// An uncallable outer body can describe an inner callable body without + /// allocating its environment or adding the target to native reachability. + fn latent_function(&self, target: FunctionId) -> Result<(), CodegenError> { + if self.phase == VerifierPhase::DescriptorMetadata { + let (_, target) = function_by_id(self.program, target)?; + self.function(target)?; + } + Ok(()) + } + + fn reject_entry_predecessors(&self, function: &Function) -> Result<(), CodegenError> { + for block in &function.blocks { + let targets_entry = match &block.terminator.kind { + TerminatorKind::Goto(target) => *target == function.entry, + TerminatorKind::Branch { + then_block, + else_block, + .. + } => *then_block == function.entry || *else_block == function.entry, + TerminatorKind::Switch { + variants, + otherwise, + .. + } => { + variants + .iter() + .any(|(_, target, _)| *target == function.entry) + || otherwise.is_some_and(|target| target == function.entry) + } + TerminatorKind::ForEach { body, exit, .. } => { + *body == function.entry || *exit == function.entry + } + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + arms.iter().any(|arm| arm.target == function.entry) + || *otherwise == function.entry + } + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => false, + }; + if targets_entry { + return Err(self.contract_error( + function, + block.terminator.span, + format!( + "control-flow edge from block {} targets ABI entry block {}", + block.id.index(), + function.entry.index() + ), + )); + } + } + Ok(()) + } + + fn graph_constructor_statement( + &self, + function: &Function, + node: &jett_mir::ResourceConstructorNode, + span: Span, + ) -> Result { + let Some(proof) = + jett_mir::native_graph_storage_for_constructor(function, self.types, node) + else { + return Ok(false); + }; + let original = function + .resource_constructor(node.constructor()) + .map_err(|message| self.contract_error(function, span, message))?; + if proof.ty() != original.original().ty { + return Err(self.contract_error( + function, + span, + "Graph constructor changed its exact output type", + )); + } + graph_storage_kind(self.types, &proof, "checked Graph constructor output")?; + match node { + jett_mir::ResourceConstructorNode::Stage { value, .. } => { + self.expression(function, value)? + } + jett_mir::ResourceConstructorNode::Commit { output, .. } => { + let (_, header) = original.commit().ok_or_else(|| { + self.contract_error( + function, + span, + "Graph constructor lost its exact Commit output", + ) + })?; + let current = function + .local(*output) + .filter(|current| *current == header) + .ok_or_else(|| { + self.contract_error( + function, + span, + "Graph constructor changed its exact Commit output", + ) + })?; + if header.id != *output { + return Err(self.contract_error( + function, + span, + "Graph constructor changed its exact Commit output", + )); + } + self.local_kind(function, current, "checked Graph constructor Commit output")?; + } + jett_mir::ResourceConstructorNode::Begin { .. } + | jett_mir::ResourceConstructorNode::End { .. } => {} + } + Ok(true) + } + + fn statement( + &self, + function: &Function, + block: jett_mir::BlockId, + index: usize, + statement: &Statement, + ) -> Result<(), CodegenError> { + super::emit::ordinary_borrowed_sums::validate_statement( + function, self.types, block, index, statement, + ) + .map_err(|message| self.contract_error(function, statement.span, message))?; + match &statement.kind { + StatementKind::ResourceConstructor(node) => { + if self.graph_constructor_statement(function, node, statement.span)? { + return Ok(()); + } + use jett_mir::ResourceConstructorNode as Node; + let original = function + .resource_constructor(node.constructor()) + .map_err(|message| self.contract_error(function, statement.span, message))?; + let family = self + .resource + .and_then(|p| p.function(function.id)) + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "Resource constructor has no fresh carrier family", + ) + })?; + let plan = family.carriers().ok_or_else(|| { + self.contract_error( + function, + statement.span, + "Resource constructor has no fresh carrier family", + ) + })?; + let position = jett_mir::ResourcePosition::Statement(index); + let planned = match node { + Node::End { outcome, .. } => { + let begin = plan.operations().iter().find(|operation| { + operation.site() == original.begin() + && matches!( + operation.role(), + jett_mir::ResourceCarrierOperationRole::BeginConstructor { .. } + ) + }); + begin.is_some_and(|begin| { + family.operations().iter().filter(|operation| { + operation.site().block() == block + && operation.site().position() == position + && operation.frame() == begin.frame() + && matches!(operation.role(), jett_mir::ResourceOperationRole::Complete { outcome: found } if found == outcome) + && original.exits().contains(&(operation.site(), *outcome)) + }).count() == 1 + }) + } + _ => plan.operations().iter().any(|operation| { + operation.site().block() == block && operation.site().position() == position + }), + }; + if !planned { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor has no exact planned occurrence", + )); + } + match node { + Node::Stage { + source_index, + field, + value, + .. + } => { + if original.fields().get(*source_index).is_none_or(|f| { + f.field() != *field + || f.stage().block() != block + || f.stage().position() != position + }) { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor changed its exact field site", + )); + } + self.expression(function, value)?; + } + Node::Commit { output, .. } + if original.commit().is_none_or(|(site, header)| { + site.block() != block + || site.position() != position + || header.id != *output + }) => + { + return Err(self.contract_error( + function, + statement.span, + "Resource constructor changed its exact Commit site", + )); + } + _ => {} + } + Ok(()) + } + StatementKind::ResourceLexicalExit(_) => { + let position = jett_mir::ResourcePosition::Statement(index); + let ownership = self.resource.ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node has no ownership proof", + ) + })?; + let plan = ownership + .function(function.id) + .and_then(|function| function.lexical_exit(block, position)) + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node lost its exact planned site", + ) + })?; + let original = function + .resource_lexical_exit(block, position) + .map_err(|message| self.contract_error(function, statement.span, message))? + .ok_or_else(|| { + self.contract_error( + function, + statement.span, + "lexical Resource node lost its private constructor", + ) + })?; + if original.kind() != plan.kind() + || plan.kind() == jett_mir::ResourceLexicalExitKind::Return + { + return Err(self.contract_error( + function, + statement.span, + "lexical Resource node changed its exact exit kind", + )); + } + Ok(()) + } + StatementKind::ResourceCall(node) => { + use jett_mir::ResourceCallNode as Node; + let ownership = self.resource.ok_or_else(|| { + self.unsupported( + function, + statement.span, + "staged Resource node has no constructor-owned ownership proof", + ) + })?; + let region_id = match node { + Node::Begin { region } + | Node::Stage { region, .. } + | Node::Invoke { region, .. } + | Node::End { region, .. } => *region, + }; + let region = function.resource_call_region(region_id).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged Resource node lost its private region", + ) + })?; + let plan = ownership.function(function.id).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged region is outside the fresh execution family", + ) + })?; + if !plan.operations().iter().any(|operation| matches!(operation.role(), + jett_mir::ResourceOperationRole::BeginSourceFunction { region, .. } if *region == region_id)) { + return Err(self.contract_error(function, statement.span, + "staged region has no complete fresh Begin projection")); + } + match node { + Node::Stage { + value, ordinary, .. + } => { + self.expression(function, value)?; + if let Some(local) = ordinary { + let endpoint = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged ordinary endpoint is absent", + ) + })?; + self.require_same_type( + function, + statement.span, + endpoint.ty, + value.ty, + "staged ordinary endpoint changes its evaluated type", + )?; + } + } + Node::Invoke { output, .. } => { + let endpoint = function.local(*output).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "staged invocation output is absent", + ) + })?; + self.require_same_type( + function, + statement.span, + endpoint.ty, + region.original().ty, + "staged invocation changes its original result type", + )?; + } + Node::Begin { .. } | Node::End { .. } => {} + } + Ok(()) + } + StatementKind::IterationBorrow { source, token, .. } => { + if !matches!( + self.types.resolve(self.sequence_source_type( + function, + source, + statement.span + )?), + Type::List(_) | Type::Set(_) | Type::Map(..) | Type::String + ) || function.local(*token).unwrap().ty != TypeInterner::INT64 + { + return Err(self.contract_error( + function, + statement.span, + "invalid iteration loan", + )); + } + Ok(()) + } + StatementKind::ReflectedContainerReady { source, kind } => { + let source = function.local(*source).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "missing reflected container place", + ) + })?; + if !kind.matches_type(self.types, source.ty) { + return Err(self.contract_error( + function, + statement.span, + "reflected container readiness kind disagrees with checked wrapper", + )); + } + // Diagnostic text is canonical to the typed enum, never a source operand. + Ok(()) + } + StatementKind::ReflectedValueReady { source, .. } => { + let source = function.local(*source).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "missing reflected callback result place", + ) + })?; + self.value_kind(function, source.ty, "reflected callback result")?; + Ok(()) + } + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + if matches!( + statement.kind, + StatementKind::SequenceGet { consume: true, .. } + ) && matches!(source, SequenceSource::Projected { .. }) + { + return Err(self.contract_error( + function, + statement.span, + "cannot consume a projected sequence field", + )); + } + let sequence_ty = self.sequence_source_type(function, source, statement.span)?; + let element = match self.types.resolve(sequence_ty) { + Type::List(element) | Type::Set(element) => { + if matches!(statement.kind, StatementKind::SequenceGet { part, .. } if part != jett_mir::SequencePart::Element) + { + return Err(self.contract_error( + function, + statement.span, + "invalid sequence projection", + )); + } + *element + } + Type::Map(key, value) => match statement.kind { + StatementKind::SequenceGet { + part: jett_mir::SequencePart::Key, + .. + } => *key, + StatementKind::SequenceGet { + part: jett_mir::SequencePart::Value, + .. + } => *value, + StatementKind::SequenceLength { .. } => *key, + _ => { + return Err(self.contract_error( + function, + statement.span, + "invalid map projection", + )); + } + }, + Type::String => { + if matches!(statement.kind, StatementKind::SequenceGet { part, .. } if part != jett_mir::SequencePart::Element) + { + return Err(self.contract_error( + function, + statement.span, + "invalid string iteration projection", + )); + } + TypeInterner::STRING + } + _ => { + return Err(self.contract_error( + function, + statement.span, + "sequence requires list or set", + )); + } + }; + let expected = if let StatementKind::SequenceGet { index, .. } = statement.kind { + self.require_same_type( + function, + statement.span, + TypeInterner::INT64, + function.local(index).unwrap().ty, + "sequence index must be int64", + )?; + element + } else { + TypeInterner::INT64 + }; + self.require_same_type( + function, + statement.span, + expected, + function.local(*target).unwrap().ty, + &format!( + "sequence target type mismatch: expected {}, got {}", + self.types.type_name(expected), + self.types.type_name(function.local(*target).unwrap().ty) + ), + ) + } + + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + let source = function.local(*source).ok_or_else(|| { + self.contract_error(function, statement.span, "missing sum place") + })?; + let target = function.local(*target).ok_or_else(|| { + self.contract_error(function, statement.span, "missing payload place") + })?; + let payload = match (self.types.resolve(source.ty), &statement.kind) { + (Type::Result(..) | Type::Optional(_), StatementKind::SumTag { .. }) => { + TypeInterner::BOOL + } + (Type::Result(ok, error), StatementKind::SumTake { success, .. }) => { + if *success { *ok } else { *error } + } + (Type::Optional(inner), StatementKind::SumTake { success: true, .. }) => *inner, + _ => { + return Err(self.contract_error( + function, + statement.span, + "invalid sum projection", + )); + } + }; + if matches!(self.types.resolve(target.ty), Type::Secret(inner) if *inner == payload) + { + return Ok(()); + } + self.require_same_type( + function, + statement.span, + payload, + target.ty, + "sum payload type mismatch", + ) + } + + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + let local = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "let target is absent from the local table", + ) + })?; + self.expression(function, value)?; + self.require_same_type( + function, + statement.span, + local.ty, + value.ty, + "let initializer type does not match its local", + ) + } + StatementKind::EndCallView { local } => { + function.local(*local).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "call view end target is absent from the local table", + ) + })?; + Ok(()) + } + StatementKind::OpenCallOwnerGeneration { root, .. } + | StatementKind::ReplaceCallOwnerGeneration { root, .. } => { + let owner = function.local(*root).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "call owner generation root is absent from the local table", + ) + })?; + if owner.ty.index() as usize >= self.types.len() + || !matches!(self.types.resolve(owner.ty), Type::TypeConstruction) + || !owner.mutable + || function.is_view_local(*root) + || function.parameter_for_local(*root).is_some() + { + return Err(self.contract_error( + function, + statement.span, + "call owner generation root is not its exact mutable builder owner", + )); + } + self.value_kind(function, owner.ty, "call owner generation root")?; + if let StatementKind::ReplaceCallOwnerGeneration { rhs_owner, .. } = &statement.kind + { + let rhs = function.local(*rhs_owner).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "call owner generation RHS is absent from the local table", + ) + })?; + if rhs_owner == root + || rhs.ty != owner.ty + || function.is_view_local(*rhs_owner) + || function.parameter_for_local(*rhs_owner).is_some() + { + return Err(self.contract_error( + function, + statement.span, + "call owner generation RHS is not its independent owning builder", + )); + } + self.value_kind(function, rhs.ty, "call owner generation RHS")?; + } + // Exact sites, private Source joins and Open/Replace/Close CFG + // authority were checked by the original all-body MIR gate. + Ok(()) + } + StatementKind::CloseCallOwnerGeneration { .. } => Ok(()), + StatementKind::PrepareFieldUpdate { place, .. } => place + .validate(place.target(), &function.locals, self.types) + .map_err(|message| self.contract_error(function, statement.span, message)), + StatementKind::UpdateField { + target, + value, + place, + } => { + place + .validate(target, &function.locals, self.types) + .map_err(|message| self.contract_error(function, statement.span, message))?; + if function.is_view_local(place.root()) { + return Err(self.contract_error( + function, + statement.span, + "field update targets a view", + )); + } + self.expression(function, target)?; + self.expression(function, value)?; + self.require_same_type( + function, + statement.span, + target.ty, + value.ty, + "field update value type changed", + ) + } + StatementKind::CheckRefinement { local, call, .. } => { + let local = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "refinement error target is absent from the local table", + ) + })?; + self.expression(function, call)?; + self.require_same_type( + function, + statement.span, + TypeInterner::STRING, + local.ty, + "refinement error target must be string", + )?; + self.require_same_type( + function, + statement.span, + TypeInterner::BOOL, + call.ty, + "refinement predicate must return bool", + ) + } + StatementKind::Assign { target, value } => { + let ExpressionKind::Local(local_id) = target.kind else { + return Err(self.unsupported( + function, + statement.span, + "assignment target other than a local", + )); + }; + let local = function.local(local_id).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "assignment target is absent from the local table", + ) + })?; + if !local.mutable { + return Err(self.contract_error( + function, + statement.span, + "assignment targets an immutable local", + )); + } + self.expression(function, target)?; + self.expression(function, value)?; + self.require_same_type( + function, + statement.span, + local.ty, + value.ty, + "assigned value type does not match its local", + )?; + // Storage can be a bare machine while a visible guard makes + // this assignment target exact. Preserve that checked state. + if matches!(self.types.resolve(target.ty), Type::MachineState { .. }) { + self.require_same_type( + function, + statement.span, + target.ty, + value.ty, + "assigned value type does not match its exact machine target", + )?; + } + Ok(()) + } + StatementKind::Evaluate(value) => self.expression(function, value), + StatementKind::HandleDefault(_) => { + Err(self.unsupported(function, statement.span, "handle default")) + } + StatementKind::Assert { condition, message } => { + self.expression(function, condition)?; + if condition.ty != TypeInterner::BOOL { + return Err(self.contract_error( + function, + statement.span, + "assert condition is not bool", + )); + } + if let Some(message) = message { + self.expression(function, message)?; + self.require_same_type( + function, + statement.span, + TypeInterner::STRING, + message.ty, + "assert message must be string", + )?; + } + Ok(()) + } + StatementKind::Trace(local) => { + let Some(local) = function.local(*local) else { + return Err(self.contract_error( + function, + statement.span, + "trace target is absent from the local table", + )); + }; + if super::emit::debug::debug_layout(self.types, local.ty).is_some() { + Ok(()) + } else { + Err(self.unsupported(function, statement.span, "aggregate trace value")) + } + } + StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + self.expression(function, condition)?; + self.require_same_type( + function, + statement.span, + TypeInterner::BOOL, + condition.ty, + "breakpoint condition must be bool", + )?; + } + for binding in bindings { + let local = function.local(*binding).ok_or_else(|| { + self.contract_error( + function, + statement.span, + "breakpoint binding is absent from local table", + ) + })?; + if super::emit::debug::debug_layout(self.types, local.ty).is_none() { + return Err(self.unsupported( + function, + statement.span, + "aggregate breakpoint binding", + )); + } + } + Ok(()) + } + } + } + + fn function_adapter_signature( + &self, + function: &Function, + span: Span, + source: TypeId, + target: TypeId, + adapter: FunctionId, + ) -> Result<(), CodegenError> { + if !self.types.type_ids().any(|ty| ty == source) + || !self.types.type_ids().any(|ty| ty == target) + { + return Err(self.contract_error(function, span, "function adapter type is absent")); + } + let callee = self + .program + .functions + .get(adapter.index() as usize) + .filter(|callee| callee.id == adapter) + .ok_or_else(|| { + self.contract_error(function, span, "function adapter target is absent") + })?; + let Type::Function { + params, + view_params, + return_type, + } = self.types.resolve(target) + else { + return Err(self.contract_error(function, span, "function adapter type mismatch")); + }; + if !matches!(self.types.resolve(source), Type::Function { .. }) + || callee.capture_count != 1 + || callee.params.len() != params.len() + 1 + || callee.params[0].ty != source + || callee.params[0].mode != jett_mir::ParamMode::Owned + || callee.return_type != *return_type + || params.len() != view_params.len() + || callee.params[1..] + .iter() + .zip(params.iter().zip(view_params)) + .any(|(actual, (ty, view))| { + actual.ty != *ty || (actual.mode == jett_mir::ParamMode::View) != *view + }) + { + return Err(self.contract_error(function, span, "function adapter signature mismatch")); + } + Ok(()) + } + + fn sequence_source_type( + &self, + function: &Function, + source: &SequenceSource, + span: Span, + ) -> Result { + match source { + SequenceSource::Local(local) => function + .local(*local) + .map(|local| local.ty) + .ok_or_else(|| self.contract_error(function, span, "missing sequence source")), + SequenceSource::Projected { owner, path, ty } => { + if path.is_empty() { + return Err(self.contract_error( + function, + span, + "projected sequence has no field path", + )); + } + let mut current = + function + .local(*owner) + .map(|local| local.ty) + .ok_or_else(|| { + self.contract_error(function, span, "missing projected sequence owner") + })?; + for step in path { + if current != step.owner_type { + return Err(self.contract_error( + function, + span, + "projected sequence owner type mismatch", + )); + } + let Type::Struct(struct_id) = self.types.resolve(current) else { + return Err(self.contract_error( + function, + span, + "projected sequence owner is not a struct", + )); + }; + current = self + .types + .resolve_struct(*struct_id) + .fields + .get(step.field.index() as usize) + .map(|(_, field_ty)| *field_ty) + .ok_or_else(|| { + self.contract_error( + function, + span, + "projected sequence field is absent", + ) + })?; + } + if current != *ty { + return Err(self.contract_error( + function, + span, + "projected sequence field type mismatch", + )); + } + Ok(*ty) + } + } + } + + fn terminator(&self, function: &Function, terminator: &Terminator) -> Result<(), CodegenError> { + match &terminator.kind { + TerminatorKind::Return(value) => match value { + Some(value) => { + self.expression(function, value)?; + self.require_same_type( + function, + terminator.span, + function.return_type, + value.ty, + "returned value does not match the function return type", + ) + } + None => { + if scalar_kind(self.types, function.return_type, "return without a value")? + == ScalarKind::Nothing + { + Ok(()) + } else { + Err(self.contract_error( + function, + terminator.span, + "non-nothing function returns without a value", + )) + } + } + }, + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => Ok(()), + TerminatorKind::Branch { condition, .. } => { + self.expression(function, condition)?; + if scalar_kind(self.types, condition.ty, "branch condition")? == ScalarKind::Bool { + Ok(()) + } else { + Err(self.contract_error( + function, + terminator.span, + "branch condition is not bool", + )) + } + } + TerminatorKind::Respond(value) => { + if function.identity.declaration.kind != jett_hir::DeclarationKind::ActorHandler { + return Err(self.contract_error( + function, + terminator.span, + "response outside actor handler", + )); + } + self.expression(function, value)?; + self.require_same_type( + function, + terminator.span, + function.return_type, + value.ty, + "actor response type does not match handler return type", + ) + } + TerminatorKind::Switch { + scrutinee, + variants, + otherwise, + } => { + self.expression(function, scrutinee)?; + let Type::Enum(enum_id) = self.types.resolve(scrutinee.ty) else { + return Err(self.contract_error( + function, + terminator.span, + "variant switch scrutinee is not an enum", + )); + }; + let definition = self.types.resolve_enum(*enum_id); + let mut seen = std::collections::BTreeSet::new(); + for (variant, _, bindings) in variants { + let index = usize::try_from(variant.index()).map_err(|_| { + self.contract_error(function, terminator.span, "variant index overflow") + })?; + let Some(variant_definition) = definition.variants.get(index) else { + return Err(self.contract_error( + function, + terminator.span, + "variant switch has an invalid arm", + )); + }; + if !seen.insert(index) { + return Err(self.contract_error( + function, + terminator.span, + "variant switch has a duplicate arm", + )); + } + if bindings.len() != variant_definition.fields.len() && !bindings.is_empty() { + return Err(self.contract_error( + function, + terminator.span, + "variant binding count mismatch", + )); + } + let mut unique_bindings = std::collections::BTreeSet::new(); + for (binding, (_, expected)) in bindings.iter().zip(&variant_definition.fields) + { + let Some(local) = function.local(*binding) else { + return Err(self.contract_error( + function, + terminator.span, + "variant binding is not a local", + )); + }; + if local.ty != *expected || !unique_bindings.insert(binding.index()) { + return Err(self.contract_error( + function, + terminator.span, + "variant binding has an invalid type or duplicate local", + )); + } + } + } + if otherwise.is_none() && seen.len() != definition.variants.len() { + return Err(self.contract_error( + function, + terminator.span, + "variant switch is not exhaustive", + )); + } + Ok(()) + } + TerminatorKind::ForEach { + key, + value, + iterable, + .. + } if self.phase == VerifierPhase::DescriptorMetadata => { + // Original typed loops are valid in an uncallable body. Check + // their binding schema before native preparation may erase it; + // do not demand a prepared native loop or execute the iterable. + self.expression(function, iterable)?; + let (key_type, value_type) = match self.types.resolve(iterable.ty) { + Type::List(element) | Type::Set(element) if value.is_none() => (*element, None), + Type::Map(key, value_type) if value.is_some() => (*key, Some(*value_type)), + Type::String if value.is_none() => (TypeInterner::STRING, None), + _ => { + return Err(self.contract_error( + function, + terminator.span, + "original descriptor loop has an invalid iterable or binding shape", + )); + } + }; + self.require_same_type( + function, + terminator.span, + key_type, + function.local(*key).unwrap().ty, + "original descriptor loop key type does not match its iterable", + )?; + if let (Some(value), Some(value_type)) = (value, value_type) { + self.require_same_type( + function, + terminator.span, + value_type, + function.local(*value).unwrap().ty, + "original descriptor loop value type does not match its iterable", + )?; + } + Ok(()) + } + TerminatorKind::ForEach { .. } => { + Err(self.unsupported(function, terminator.span, "for-each loop")) + } + TerminatorKind::ReflectedTypeDispatch { + type_info, arms, .. + } => { + let valid_type_info = matches!(self.types.resolve(type_info.ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeInfo"); + let identities = arms + .iter() + .map(|arm| arm.reflection_identity.as_str()) + .collect::>(); + if valid_type_info + && !arms.is_empty() + && arms.iter().all(|arm| !arm.reflection_identity.is_empty()) + && identities.len() == arms.len() + { + Ok(()) + } else { + Err(self.contract_error( + function, + terminator.span, + "invalid checked reflected type dispatch", + )) + } + } + } + } + + fn expression(&self, function: &Function, expression: &Expression) -> Result<(), CodegenError> { + let graph_storage = + jett_mir::native_graph_storage_for_expression(function, self.types, expression); + if graph_storage.is_none() && self.carrier_expression(function, expression)?.is_some() { + return Ok(()); + } + let named_value = self + .resource + .and_then(|ownership| ownership.function(function.id)) + .and_then(|plan| plan.named_callable_value(expression)); + let descriptor_value = self + .resource + .and_then(|ownership| ownership.function(function.id)) + .and_then(|plan| plan.descriptor_value(expression)); + if let Some(hook) = descriptor_value { + if hook.function_type() != expression.ty + || !self.resource.is_some_and(|ownership| { + ownership.program().resource_manifest.contains_hook(hook) + }) + { + return Err(self.contract_error( + function, + expression.span, + "opaque descriptor occurrence changes its exact hook proof", + )); + } + if let ExpressionKind::Call { + function: target, .. + } = &expression.kind + { + if self + .resource + .and_then(|ownership| ownership.function(*target)) + .and_then(|callee| callee.descriptor_return()) + != Some(hook) + { + return Err(self.contract_error( + function, + expression.span, + "returned descriptor changes its exact Source callee Return proof", + )); + } + } + } + if let Some(ownership) = self.resource { + // Source signatures do not describe the selected family's hidden Scope. + // Descriptor and adapter emission requires its distinct authenticated ABI. + let family_descriptor = match &expression.kind { + ExpressionKind::FunctionRef(target) + | ExpressionKind::ClosureRef { + function: target, .. + } + | ExpressionKind::FunctionAdapter { + function: target, .. + } => ownership.function(*target).is_some(), + ExpressionKind::InterfaceCoerce { adapters, .. } => adapters + .iter() + .any(|adapter| ownership.function(adapter.function).is_some()), + _ => false, + }; + let named_producer = ownership + .function(function.id) + .and_then(|plan| plan.named_callable_producer(expression)); + let proved_named_descriptor = matches!(&expression.kind, + ExpressionKind::FunctionRef(target) if named_producer.is_some_and(|proof| + proof.function() == *target && proof.signature_type() == expression.ty)); + if family_descriptor && !proved_named_descriptor { + return Err(self.unsupported( + function, + expression.span, + "pending Resource family descriptor hidden Scope ABI", + )); + } + if let Some(plan) = ownership.function(function.id) { + for operation in plan.operations_for_expression(expression) { + if let jett_mir::ResourceOperationRole::InvokeNativeSuite { invocation } = + operation.role() + { + let proof = invocation.proof(); + if proof.target() != invocation.function() + || !proof.matches(function.id, expression, proof.expression_ordinal()) + { + return Err(self.contract_error( + function, + expression.span, + "Resource native suite edge changed its exact factory receipt", + )); + } + } + } + } + if let ExpressionKind::Call { + function: target, .. + } = &expression.kind + { + let activation = ownership.function(function.id).is_some_and(|plan| { + plan.operations_for_expression(expression).any(|operation| { + match operation.role() { + jett_mir::ResourceOperationRole::InvokeSourceFunction { + function: callee, + .. + } => callee == target, + jett_mir::ResourceOperationRole::InvokeNativeSuite { invocation } => { + invocation.function() == *target + } + _ => false, + } + }) + }); + if ownership.function(*target).is_some() && !activation { + return Err(self.unsupported( + function, + expression.span, + "Resource family direct edge has no exact scope activation", + )); + } + } + } + if let Some(plan) = self.resource.and_then(|plan| plan.function(function.id)) { + let invocation = plan.operations_for_expression(expression).any(|operation| { + matches!( + operation.role(), + jett_mir::ResourceOperationRole::InvokeHook { .. } + | jett_mir::ResourceOperationRole::InvokeSourceFunction { .. } + | jett_mir::ResourceOperationRole::InvokeNativeSuite { .. } + ) + }); + if invocation + || (named_value.is_none() + && graph_storage.is_none() + && self + .resource + .is_some_and(|proof| proof.type_requires_custody(expression.ty))) + { + match &expression.kind { + ExpressionKind::Local(local) => { + let local = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + expression.span, + "custody local is outside its exact current header", + ) + })?; + return self.require_same_type( + function, + expression.span, + local.ty, + expression.ty, + "custody local type changed", + ); + } + ExpressionKind::View(inner) + | ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) => return self.expression(function, inner), + ExpressionKind::Clone(inner) if descriptor_value.is_some() => { + if plan.descriptor_value(inner) != descriptor_value { + return Err(self.contract_error( + function, + expression.span, + "opaque descriptor alias changes its exact producer", + )); + } + return self.expression(function, inner); + } + ExpressionKind::ResourceHookValue { hook } => { + if descriptor_value != Some(hook) { + return Err(self.contract_error( + function, + expression.span, + "opaque hook value has no exact current producer proof", + )); + } + return Ok(()); + } + ExpressionKind::OptionalNone => { + return Ok(()); + } + ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } => { + if !invocation { + return Err(self.contract_error( + function, + expression.span, + "Resource native call lost its exact fresh operation", + )); + } + for actual in args { + self.expression(function, actual)?; + } + if !self + .resource + .is_some_and(|proof| proof.type_requires_custody(expression.ty)) + { + self.value_kind( + function, + expression.ty, + "ordinary custody call result", + )?; + } + return Ok(()); + } + ExpressionKind::IndirectCall { callee, args, .. } => { + if !invocation { + return Err(self.contract_error( + function, + expression.span, + "Resource indirect hook lost its exact fresh operation", + )); + } + for operation in plan.operations_for_expression(expression) { + if let jett_mir::ResourceOperationRole::InvokeHook { + hook, + source, + .. + } = operation.role() + { + if operation.indirect_hook_target() != Some(hook) + || plan.descriptor_value(callee) != Some(hook) + || callee.ty != hook.function_type() + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } + if signature_type == callee.ty) + || source.bridge != jett_hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err(self.contract_error(function, expression.span, + "Resource indirect hook changes its sealed descriptor target or Source tuple")); + } + } + if let jett_mir::ResourceOperationRole::InvokeSourceFunction { + function: target, + source, + .. + } = operation.role() + { + let proof = operation.named_indirect_target().ok_or_else(|| self.contract_error( + function, expression.span, "Resource indirect Source call lost its exact named target proof"))?; + if proof.function() != *target + || proof.signature_type() != callee.ty + || !matches!(source.target, jett_hir::CallTarget::Indirect { signature_type } if signature_type == callee.ty) + { + return Err(self.contract_error(function, expression.span, + "Resource indirect Source call changes its sealed selected target")); + } + } + } + self.expression(function, callee)?; + for actual in args { + self.expression(function, actual)?; + } + return Ok(()); + } + _ => { + return Err(self.unsupported( + function, + expression.span, + "dedicated Resource expression transport", + )); + } + } + } + } + let kind = if let Some(proof) = named_value { + if proof.signature_type() != expression.ty + || !matches!(self.types.resolve(expression.ty), Type::Function { .. }) + { + return Err(self.contract_error( + function, + expression.span, + "named descriptor occurrence changes its sealed signature", + )); + } + ScalarKind::Function + } else { + self.expression_kind( + function, + expression, + format!("expression in `{}`", self.function_name(function)), + )? + }; + match &expression.kind { + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => Err(self.contract_error(function, expression.span, "pending ResourceOwnershipPlan: native Resource descriptor and invocation ABI are not admitted")), + ExpressionKind::Int(value) => self.integer_literal(function, expression, *value, kind), + ExpressionKind::Float(_) => { + if matches!(kind, ScalarKind::Float(_)) { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "float literal")) + } + } + ExpressionKind::Bool(_) => { + if kind == ScalarKind::Bool { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "bool literal")) + } + } + ExpressionKind::Nothing => { + if kind == ScalarKind::Nothing { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "nothing literal")) + } + } + ExpressionKind::Local(local) => { + let local = function.local(*local).ok_or_else(|| { + self.contract_error( + function, + expression.span, + "expression local is absent from the local table", + ) + })?; + if let ( + Type::Machine(machine), + Type::MachineState { + machine: narrowed, .. + }, + ) = ( + self.types.resolve(local.ty), + self.types.resolve(expression.ty), + ) && machine == narrowed + { + return Ok(()); + } + if let ( + Type::MachineState { + machine: narrowed, .. + }, + Type::Machine(machine), + ) = ( + self.types.resolve(local.ty), + self.types.resolve(expression.ty), + ) && machine == narrowed + { + return Ok(()); + } + if matches!(self.types.resolve(expression.ty), Type::Secret(inner) if *inner == local.ty) + { + return Ok(()); + } + self.require_same_type( + function, + expression.span, + local.ty, + expression.ty, + "local expression type does not match local metadata", + ) + } + ExpressionKind::FunctionAdapter { + value, + function: target, + } => { + self.expression(function, value)?; + self.function_adapter_signature( + function, + expression.span, + value.ty, + expression.ty, + *target, + )?; + self.latent_function(*target) + } + ExpressionKind::FunctionRef(target) => { + let Some(callee) = self + .program + .functions + .get(target.index() as usize) + .filter(|callee| callee.id == *target) + else { + return Err(self.contract_error( + function, + expression.span, + "function value target is absent", + )); + }; + let Type::Function { + params, + view_params, + return_type, + } = self.types.resolve(expression.ty) + else { + return Err(self.expression_kind_error(function, expression, "function value")); + }; + if callee.capture_count != 0 + || params.len() != callee.params.len() + || view_params.len() != params.len() + || params.iter().zip(view_params).zip(&callee.params).any( + |((expected, view), actual)| { + *expected != actual.ty + || *view != (actual.mode == jett_mir::ParamMode::View) + }, + ) + || *return_type != callee.return_type + { + return Err(self.contract_error( + function, + expression.span, + "function value signature does not match target", + )); + } + self.latent_function(*target) + } + ExpressionKind::ClosureRef { + function: target, + captures, + } => { + let Some(callee) = self + .program + .functions + .get(target.index() as usize) + .filter(|callee| callee.id == *target) + else { + return Err(self.contract_error( + function, + expression.span, + "closure target is absent", + )); + }; + let Type::Function { + params, + view_params, + return_type, + } = self.types.resolve(expression.ty) + else { + return Err(self.expression_kind_error(function, expression, "closure value")); + }; + if callee.capture_count == 0 + || callee.capture_count != captures.len() + || callee.params.len() != captures.len() + params.len() + || view_params.len() != params.len() + || *return_type != callee.return_type + || callee + .params + .iter() + .skip(callee.capture_count) + .zip(params.iter().zip(view_params)) + .any(|(actual, (expected, view))| { + actual.ty != *expected + || (actual.mode == jett_mir::ParamMode::View) != *view + }) + { + return Err(self.contract_error( + function, + expression.span, + "closure signature does not match target", + )); + } + for (capture, parameter) in captures.iter().zip(&callee.params) { + let Some(local) = function.local(*capture) else { + return Err(self.contract_error( + function, + expression.span, + "closure capture local is absent", + )); + }; + if local.ty != parameter.ty { + return Err(self.contract_error( + function, + expression.span, + "closure capture type does not match target", + )); + } + if parameter.mode != jett_mir::ParamMode::Owned { + return Err(self.contract_error( + function, + expression.span, + "closure capture must use owned environment storage", + )); + } + let latent_never = self.phase == VerifierPhase::DescriptorMetadata + && local.ty == TypeInterner::NEVER; + if !latent_never + && !matches!( + self.types.resolve(local.ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Nothing + ) + { + return Err(self.contract_error( + function, + expression.span, + "closure capture is not an implicitly copyable source value", + )); + } + } + self.latent_function(*target) + } + ExpressionKind::Unary { op, value } => { + self.expression(function, value)?; + self.require_same_type( + function, + expression.span, + expression.ty, + value.ty, + "unary operand and result types differ", + )?; + let supported = match op { + UnaryOp::Not => kind == ScalarKind::Bool, + UnaryOp::Negate => { + matches!(kind, ScalarKind::SignedInteger(_) | ScalarKind::Float(_)) + } + }; + if supported { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "unary operation")) + } + } + ExpressionKind::Binary { left, op, right } => { + self.binary(function, expression, left, *op, right) + } + ExpressionKind::Call { + function: callee, + args, + .. + } => { + let callee_index = usize::try_from(callee.index()).map_err(|_| { + self.contract_error( + function, + expression.span, + "direct call target index does not fit this host", + ) + })?; + let Some(callee) = self.program.functions.get(callee_index) else { + return Err(self.contract_error( + function, + expression.span, + "direct call target is absent from the function table", + )); + }; + if self.phase == VerifierPhase::CallableNative + && callee + .params + .iter() + .any(|parameter| parameter.ty == TypeInterner::NEVER) + { + return Err(self.contract_error( + function, + expression.span, + "callable direct edge requires an uninhabited parameter", + )); + } + if args.len() != callee.params.len() { + return Err(self.contract_error( + function, + expression.span, + "direct call argument count does not match its signature", + )); + } + let pure = callee.params.iter().all(|parameter| { + !matches!(self.types.resolve(parameter.ty), Type::Capability(_)) + }); + let mut lifted_secret = false; + for (argument, parameter) in args.iter().zip(&callee.params) { + self.expression(function, argument)?; + lifted_secret |= self + .call_argument_taints_result(parameter.ty, argument.ty, pure) + .ok_or_else(|| { + self.contract_error( + function, + argument.span, + "direct call argument type does not match its parameter", + ) + })?; + } + self.require_call_result_type( + function, + expression.span, + callee.return_type, + expression.ty, + lifted_secret, + "direct call result type does not match its signature", + ) + } + ExpressionKind::Comptime { .. } => { + Err(self.unsupported(function, expression.span, "unbaked comptime expression")) + } + ExpressionKind::Constant { .. } => { + Err(self.unsupported(function, expression.span, "unbaked namespace constant")) + } + ExpressionKind::View(value) | ExpressionKind::Clone(value) => { + self.expression(function, value)?; + self.require_call_result_type( + function, + expression.span, + value.ty, + expression.ty, + false, + "scalar wrapper changes its value type", + ) + } + ExpressionKind::String(_) if kind == ScalarKind::String => Ok(()), + ExpressionKind::String(_) => { + Err(self.expression_kind_error(function, expression, "string literal")) + } + ExpressionKind::Intrinsic { + intrinsic, + args, + type_arguments, + reflection_arguments, + field_validation, + .. + } => { + if jett_hir::is_reflected_field_intrinsic(*intrinsic) { + if !matches!( + field_validation, + Some(jett_hir::ReflectedFieldValidation::Read + | jett_hir::ReflectedFieldValidation::ReadCallable(_)) + ) { + return Err(self.contract_error( + function, + expression.span, + "reflected selector proof plans were not lowered", + )); + } + } else if field_validation.is_some() { + return Err(self.contract_error( + function, + expression.span, + "non-selector intrinsic carries reflected validation plans", + )); + } + if type_arguments + .iter() + .any(|ty| ty.index() as usize >= self.types.len()) + { + return Err(self.contract_error( + function, + expression.span, + "intrinsic type operand is absent from checked type table", + )); + } + for arg in args { + self.expression(function, arg)?; + } + if matches!( + intrinsic, + jett_hir::IntrinsicId::TypeName + | jett_hir::IntrinsicId::TypeKind + | jett_hir::IntrinsicId::TypeHasSecret + ) { + let expected = if *intrinsic == jett_hir::IntrinsicId::TypeHasSecret { + TypeInterner::BOOL + } else { + TypeInterner::STRING + }; + if !args.is_empty() + || type_arguments.len() != 1 + || reflection_arguments.len() != 1 + || expression.ty != expected + { + return Err(self.contract_error( + function, + expression.span, + "invalid checked reflection intrinsic operands", + )); + } + return Ok(()); + } + if *intrinsic == jett_hir::IntrinsicId::TypeConstructStart { + let unsupported_kind = reflection_arguments.len() == 1 + && !matches!(reflection_arguments[0].kind.as_str(), "struct" | "bitfield"); + let record_fields = + type_arguments + .first() + .and_then(|ty| match self.types.resolve(*ty) { + Type::Struct(id) => { + Some(("struct", self.types.resolve_struct(*id).fields.len())) + } + Type::Bitfield(id) => Some(( + "bitfield", + self.types.resolve_bitfield(*id).fields.len(), + )), + _ => None, + }); + let valid = args.is_empty() + && type_arguments.len() == 1 + && (unsupported_kind + || (record_fields.is_some_and(|(kind, count)| { + reflection_arguments.len() == count + 1 + && reflection_arguments[0].kind == kind + }) && native_constructible_record(self.types, type_arguments[0]))) + && expression.ty == TypeInterner::TYPE_CONSTRUCTION; + return if valid { + Ok(()) + } else { + Err(self.unsupported( + function, + expression.span, + "type.construct_start target requiring unsupported construction validation", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeConstructVariantStart { + let fields = + type_arguments + .first() + .and_then(|ty| match self.types.resolve(*ty) { + Type::Enum(id) => Some( + self.types + .resolve_enum(*id) + .variants + .iter() + .map(|variant| variant.fields.len()) + .sum::(), + ), + _ => None, + }); + let valid = args.len() == 1 + && type_arguments.len() == 1 + && fields.is_some_and(|count| reflection_arguments.len() == count + 1) + && reflection_arguments[0].kind == "enum" + && native_constructible_enum(self.types, type_arguments[0]) + && matches!(self.types.resolve(args[0].ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeVariant") + && matches!(self.types.resolve(expression.ty), Type::Result(ok, err) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *err == TypeInterner::STRING); + return if valid { + Ok(()) + } else { + Err(self.unsupported(function, expression.span, + "type.construct_variant_start target requiring unsupported construction validation")) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeConstructMachineStart { + let fields = type_arguments.first().and_then(|ty| { + let id = match self.types.resolve(*ty) { + Type::Machine(id) | Type::MachineState { machine: id, .. } => *id, + _ => return None, + }; + Some( + self.types + .resolve_machine(id) + .states + .iter() + .map(|state| state.fields.len()) + .sum::(), + ) + }); + let valid = args.len() == 1 + && type_arguments.len() == 1 + && fields.is_some_and(|count| reflection_arguments.len() == count + 1) + && native_constructible_builder_kind(self.types, type_arguments[0]) + == Some(reflection_arguments[0].kind.as_str()) + && matches!(self.types.resolve(args[0].ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeMachineState") + && matches!(self.types.resolve(expression.ty), Type::Result(ok, err) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *err == TypeInterner::STRING); + return if valid { + Ok(()) + } else { + Err(self.unsupported(function, expression.span, + "type.construct_machine_start target requiring unsupported construction validation")) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeConstructPut { + // Trusted decoders bind Field from each reflected field. + // Runtime BuilderPut checks the field index, name, and + // exact canonical type, so an unrelated refinement field + // sharing a base type cannot reject this specialization. + let checked_json_reflected_decoder = function.identity.declaration.origin + == SourceOrigin::Stdlib + && function.identity.declaration.namespace == "json" + && matches!( + function.identity.declaration.name.as_str(), + "json_decode_tree_record_reflected" + | "json_decode_tree_enum_payload_reflected" + | "json_decode_tree_machine_reflected" + ); + let unsupported_kind = reflection_arguments.first().is_some_and(|info| { + !matches!( + info.kind.as_str(), + "struct" | "bitfield" | "enum" | "machine" | "machine_state" + ) + }); + let valid = args.len() == 3 + && type_arguments.len() == 2 + && reflection_arguments.len() == 2 + && (unsupported_kind + || native_constructible_builder_kind(self.types, type_arguments[0]) + == Some(reflection_arguments[0].kind.as_str())) + && (unsupported_kind + || checked_json_reflected_decoder + || native_builder_value_type_supported( + self.types, + type_arguments[0], + type_arguments[1], + )) + && args[0].ty == TypeInterner::TYPE_CONSTRUCTION + && matches!(self.types.resolve(args[1].ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeField") + && args[2].ty == type_arguments[1] + && matches!(self.types.resolve(expression.ty), Type::Result(ok, err) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *err == TypeInterner::STRING); + return if valid { + Ok(()) + } else { + Err(self.unsupported( + function, + expression.span, + "type.construct_put target requiring unsupported construction validation", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeConstructFinish { + let unsupported_kind = reflection_arguments.first().is_some_and(|info| { + !matches!( + info.kind.as_str(), + "struct" | "bitfield" | "enum" | "machine" | "machine_state" + ) + }); + let valid = args.len() == 1 + && type_arguments.len() == 1 + && reflection_arguments.len() == 1 + && (unsupported_kind + || native_constructible_builder_kind(self.types, type_arguments[0]) + == Some(reflection_arguments[0].kind.as_str())) + && args[0].ty == TypeInterner::TYPE_CONSTRUCTION + && matches!(self.types.resolve(expression.ty), Type::Result(ok, err) + if *ok == type_arguments[0] && *err == TypeInterner::STRING); + return if valid { + Ok(()) + } else { + Err(self.unsupported( + function, + expression.span, + "type.construct_finish target requiring unsupported construction validation", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeVariantValue { + let Some(&owner_ty) = type_arguments.first() else { + return Err(self.contract_error( + function, + expression.span, + "type.variant_value has no checked enum type", + )); + }; + let Type::Enum(enum_id) = self.types.resolve(owner_ty) else { + return Err(self.unsupported( + function, + expression.span, + "type.variant_value of non-enum type", + )); + }; + let variants = &self.types.resolve_enum(*enum_id).variants; + let valid = type_arguments.len() == 1 + && reflection_arguments.len() == 1 + && reflection_arguments[0].kind == "enum" + && !variants.is_empty() + && args.len() == variants.len() + 1 + && args[0].ty == owner_ty + && args[1..].iter().all(|arg| arg.ty == expression.ty) + && matches!(self.types.resolve(expression.ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeVariant"); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.variant_value operands", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeArg { + let valid = type_arguments.len() == 1 + && reflection_arguments.len() == 1 + && args.len() == reflection_arguments[0].args.len() + 1 + && args[0].ty == TypeInterner::INT64 + && args[1..].iter().all(|arg| arg.ty == expression.ty) + && matches!(self.types.resolve(expression.ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeInfo"); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.arg operands", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeMachineStateValue { + let Some(&owner_ty) = type_arguments.first() else { + return Err(self.contract_error( + function, + expression.span, + "type.machine_state_value has no checked machine type", + )); + }; + let (machine_id, owner_kind) = match self.types.resolve(owner_ty) { + Type::Machine(id) => (*id, "machine"), + Type::MachineState { machine, .. } => (*machine, "machine_state"), + _ => { + return Err(self.unsupported( + function, + expression.span, + "type.machine_state_value of non-machine type", + )); + } + }; + let states = &self.types.resolve_machine(machine_id).states; + let valid = type_arguments.len() == 1 + && reflection_arguments.len() == 1 + && reflection_arguments[0].kind == owner_kind + && !states.is_empty() + && args.len() == states.len() + 1 + && args[0].ty == owner_ty + && args[1..].iter().all(|arg| arg.ty == expression.ty) + && matches!(self.types.resolve(expression.ty), Type::Struct(id) + if self.types.resolve_struct(*id).name == "TypeMachineState"); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.machine_state_value operands", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeFieldValue { + let Some(&owner_ty) = type_arguments.first() else { + return Err(self.contract_error( + function, + expression.span, + "type.field_value has no checked owner type", + )); + }; + let (field_count, owner_kind) = match self.types.resolve(owner_ty) { + Type::Struct(id) => (self.types.resolve_struct(*id).fields.len(), "struct"), + Type::Bitfield(id) => { + (self.types.resolve_bitfield(*id).fields.len(), "bitfield") + } + _ => { + return Err(self.unsupported( + function, + expression.span, + "type.field_value of non-struct type", + )); + } + }; + let field_ty = args.get(1).map(|arg| arg.ty); + let valid_field_ty = field_ty.is_some_and(|ty| { + jett_hir::valid_reflected_field_metadata_type(self.types, ty) + }); + let valid = type_arguments.len() == 2 + && type_arguments[1] == expression.ty + && reflection_arguments.len() == 2 + && reflection_arguments[0].kind == owner_kind + && args.len() == field_count + 2 + && args[0].ty == owner_ty + && valid_field_ty + && args[2..].iter().all(|arg| Some(arg.ty) == field_ty); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.field_value operands", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeVariantFieldValue { + let Some(&owner_ty) = type_arguments.first() else { + return Err(self.contract_error( + function, + expression.span, + "type.variant_field_value has no checked enum type", + )); + }; + let Type::Enum(enum_id) = self.types.resolve(owner_ty) else { + return Err(self.unsupported( + function, + expression.span, + "type.variant_field_value of non-enum type", + )); + }; + let field_count = self + .types + .resolve_enum(*enum_id) + .variants + .iter() + .map(|variant| variant.fields.len()) + .sum::(); + let field_ty = args.get(1).map(|arg| arg.ty); + let valid_field_ty = field_ty.is_some_and(|ty| { + jett_hir::valid_reflected_field_metadata_type(self.types, ty) + }); + let valid = type_arguments.len() == 2 + && type_arguments[1] == expression.ty + && reflection_arguments.len() == 2 + && reflection_arguments[0].kind == "enum" + && args.len() == field_count + 2 + && args[0].ty == owner_ty + && valid_field_ty + && args[2..].iter().all(|arg| Some(arg.ty) == field_ty); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.variant_field_value operands", + )) + }; + } + if *intrinsic == jett_hir::IntrinsicId::TypeMachineFieldValue { + let Some(&owner_ty) = type_arguments.first() else { + return Err(self.contract_error( + function, + expression.span, + "type.machine_field_value has no checked machine type", + )); + }; + let (machine_id, owner_kind) = match self.types.resolve(owner_ty) { + Type::Machine(id) => (*id, "machine"), + Type::MachineState { machine, .. } => (*machine, "machine_state"), + _ => { + return Err(self.unsupported( + function, + expression.span, + "type.machine_field_value of non-machine type", + )); + } + }; + let field_count = self + .types + .resolve_machine(machine_id) + .states + .iter() + .map(|state| state.fields.len()) + .sum::(); + let field_ty = args.get(1).map(|arg| arg.ty); + let valid_field_ty = field_ty.is_some_and(|ty| { + jett_hir::valid_reflected_field_metadata_type(self.types, ty) + }); + let valid = type_arguments.len() == 2 + && type_arguments[1] == expression.ty + && reflection_arguments.len() == 2 + && reflection_arguments[0].kind == owner_kind + && args.len() == field_count + 2 + && args[0].ty == owner_ty + && valid_field_ty + && args[2..].iter().all(|arg| Some(arg.ty) == field_ty); + return if valid { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "invalid checked type.machine_field_value operands", + )) + }; + } + let numeric_generic = matches!( + intrinsic, + jett_hir::IntrinsicId::MathKernelAbs + | jett_hir::IntrinsicId::MathKernelMin + | jett_hir::IntrinsicId::MathKernelMax + ); + if numeric_generic + && type_arguments.as_slice() + != [args.first().map_or(TypeInterner::ERROR, |a| a.ty)] + { + return Err(self.contract_error( + function, + expression.span, + "numeric intrinsic type argument differs from operand", + )); + } + let list_generic = crate::values::list_intrinsic(*intrinsic); + let math_aggregate_generic = matches!( + intrinsic, + jett_hir::IntrinsicId::MathAverage | jett_hir::IntrinsicId::MathMedian + ); + if math_aggregate_generic + && type_arguments.as_slice() + != [args + .first() + .and_then(|arg| match self.types.resolve(arg.ty) { + Type::List(element) => Some(*element), + _ => None, + }) + .unwrap_or(TypeInterner::ERROR)] + { + return Err(self.contract_error( + function, + expression.span, + "numeric list intrinsic type argument differs from element", + )); + } + let list_element = + crate::values::list_element(*intrinsic, args, expression.ty, self.types) + .unwrap_or(TypeInterner::ERROR); + let list_type_argument = if *intrinsic == jett_hir::IntrinsicId::ListSortByIndex { + match self.types.resolve(list_element) { + Type::List(inner) => *inner, + _ => TypeInterner::ERROR, + } + } else { + list_element + }; + if list_generic && type_arguments.as_slice() != [list_type_argument] { + return Err(self.contract_error( + function, + expression.span, + "list intrinsic type argument differs from element", + )); + } + let set_generic = crate::values::set_intrinsic(*intrinsic); + if set_generic + && type_arguments.as_slice() + != [ + crate::values::set_element(*intrinsic, args, expression.ty, self.types) + .unwrap_or(TypeInterner::ERROR), + ] + { + return Err(self.contract_error( + function, + expression.span, + "set intrinsic type argument differs from element", + )); + } + let map_generic = crate::values::map_intrinsic(*intrinsic); + if map_generic + && type_arguments.as_slice() + != crate::values::map_types(*intrinsic, args, expression.ty, self.types) + .map(|(k, v)| [k, v]) + .unwrap_or([TypeInterner::ERROR; 2]) + { + return Err(self.contract_error( + function, + expression.span, + "map intrinsic type arguments differ from key and value", + )); + } + let graphics_generic = *intrinsic == jett_hir::IntrinsicId::GraphicsRun; + if graphics_generic + && (args.len() != 5 || type_arguments.as_slice() != [args[2].ty]) + { + return Err(self.contract_error( + function, + expression.span, + "graphics state type argument differs from its initial value", + )); + } + if !numeric_generic + && !list_generic + && !math_aggregate_generic + && !set_generic + && !map_generic + && !graphics_generic + && !type_arguments.is_empty() + { + return Err(self.unsupported( + function, + expression.span, + format!("generic native intrinsic {intrinsic}"), + )); + } + crate::values::verify_intrinsic(*intrinsic, args, expression.ty, self.types) + .map_err(|message| self.contract_error(function, expression.span, message)) + } + ExpressionKind::IndirectCall { callee, args, .. } => { + self.expression(function, callee)?; + let Type::Function { + params, + view_params, + return_type, + } = self.types.resolve(callee.ty) + else { + return Err(self.expression_kind_error(function, expression, "indirect call")); + }; + if self.phase == VerifierPhase::CallableNative + && params.contains(&TypeInterner::NEVER) + { + return Err(self.contract_error( + function, + expression.span, + "callable indirect edge requires an uninhabited parameter", + )); + } + if params.len() != args.len() || view_params.len() != params.len() { + return Err(self.contract_error( + function, + expression.span, + "indirect call argument count does not match function type", + )); + } + let pure = params + .iter() + .all(|param| !matches!(self.types.resolve(*param), Type::Capability(_))); + let mut lifted_secret = false; + for ((argument, expected), view) in args.iter().zip(params).zip(view_params) { + self.expression(function, argument)?; + if !view && matches!(argument.kind, ExpressionKind::View(_)) { + return Err(self.contract_error( + function, + argument.span, + "owned indirect call parameter cannot accept a view", + )); + } + lifted_secret |= self + .call_argument_taints_result(*expected, argument.ty, pure) + .ok_or_else(|| { + self.contract_error( + function, + argument.span, + "indirect call argument type mismatch", + ) + })?; + } + self.require_call_result_type( + function, + expression.span, + *return_type, + expression.ty, + lifted_secret, + "indirect call result type mismatch", + ) + } + ExpressionKind::StructConstruct { + struct_type, + fields, + validates_refinements, + .. + } => { + if *validates_refinements { + let valid_result = matches!(self.types.resolve(expression.ty), + Type::Result(ok, err) if *ok == *struct_type && *err == TypeInterner::STRING); + if !valid_result { + return Err(self.contract_error( + function, + expression.span, + "refinement-validating struct construction must return result[Struct, string]", + )); + } + } else { + self.require_same_type( + function, + expression.span, + *struct_type, + expression.ty, + "struct construction type mismatch", + )?; + } + let Type::Struct(id) = self.types.resolve(*struct_type) else { + return Err(self.expression_kind_error( + function, + expression, + "struct construction", + )); + }; + let layout = &self.types.resolve_struct(*id).fields; + if *validates_refinements + && !layout + .iter() + .any(|(_, ty)| matches!(self.types.resolve(*ty), Type::Refinement { .. })) + { + return Err(self.contract_error( + function, + expression.span, + "refinement-validating struct has no refinement field", + )); + } + if fields.len() != layout.len() { + return Err(self.contract_error( + function, + expression.span, + "struct field count mismatch", + )); + } + for (field, (_, ty)) in fields.iter().zip(layout) { + self.expression(function, field)?; + // An exact refinement value passed its predicate at the + // earlier boundary. Base values still need native predicate + // invocation and must not silently acquire that type. + if *validates_refinements + && matches!(self.types.resolve(*ty), Type::Refinement { .. }) + && field.ty != *ty + { + return Err(self.unsupported( + function, + field.span, + "struct refinement validation from a base value", + )); + } + self.require_same_type( + function, + field.span, + *ty, + field.ty, + "struct field type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + validates_widths, + .. + } => { + if *validates_widths { + let Type::Result(ok, error) = self.types.resolve(expression.ty) else { + return Err(self.expression_kind_error( + function, + expression, + "validated bitfield construction result", + )); + }; + self.require_same_type( + function, + expression.span, + *bitfield_type, + *ok, + "bitfield construction success type mismatch", + )?; + self.require_same_type( + function, + expression.span, + TypeInterner::STRING, + *error, + "bitfield construction failure type mismatch", + )?; + } else { + self.require_same_type( + function, + expression.span, + *bitfield_type, + expression.ty, + "bitfield construction type mismatch", + )?; + } + let Type::Bitfield(id) = self.types.resolve(*bitfield_type) else { + return Err(self.expression_kind_error( + function, + expression, + "bitfield construction", + )); + }; + let layout = &self.types.resolve_bitfield(*id).fields; + if fields.len() != layout.len() { + return Err(self.contract_error( + function, + expression.span, + "bitfield field count mismatch", + )); + } + for (field, definition) in fields.iter().zip(layout) { + self.expression(function, field)?; + self.require_same_type( + function, + field.span, + definition.ty, + field.ty, + "bitfield field type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::MachineConstruct { + state_type, + state, + payloads, + } => { + self.require_same_type( + function, + expression.span, + *state_type, + expression.ty, + "machine construction type mismatch", + )?; + let Type::MachineState { + machine, + state: declared_state, + } = self.types.resolve(*state_type) + else { + return Err(self.expression_kind_error( + function, + expression, + "machine construction", + )); + }; + if declared_state.index() != state.index() { + return Err(self.contract_error( + function, + expression.span, + "machine constructor state mismatch", + )); + } + let definition = self + .types + .resolve_machine(*machine) + .state(*declared_state) + .ok_or_else(|| { + self.contract_error( + function, + expression.span, + "machine constructor state is missing", + ) + })?; + if payloads.len() != definition.fields.len() { + return Err(self.contract_error( + function, + expression.span, + "machine constructor payload count mismatch", + )); + } + for (payload, (_, expected)) in payloads.iter().zip(&definition.fields) { + self.expression(function, payload)?; + self.require_same_type( + function, + payload.span, + *expected, + payload.ty, + "machine constructor payload type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::MachineTransition { + source, + state_type, + target, + payloads, + } => { + self.expression(function, source)?; + self.require_same_type( + function, + expression.span, + *state_type, + expression.ty, + "machine transition type mismatch", + )?; + let Type::MachineState { + machine, + state: declared_target, + } = self.types.resolve(*state_type) + else { + return Err(self.expression_kind_error( + function, + expression, + "machine transition", + )); + }; + if declared_target.index() != target.index() { + return Err(self.contract_error( + function, + expression.span, + "machine transition target mismatch", + )); + } + let Type::MachineState { + machine: source_machine, + state: source_state, + } = self.types.resolve(source.ty) + else { + return Err(self.unsupported( + function, + expression.span, + "transition from unqualified machine state", + )); + }; + let definition = self.types.resolve_machine(*machine); + if source_machine != machine + || !definition.has_transition(*source_state, *declared_target) + { + return Err(self.contract_error( + function, + expression.span, + "machine transition edge is not declared", + )); + } + let target_definition = definition.state(*declared_target).ok_or_else(|| { + self.contract_error( + function, + expression.span, + "machine transition target is missing", + ) + })?; + if payloads.len() != target_definition.fields.len() { + return Err(self.contract_error( + function, + expression.span, + "machine transition payload count mismatch", + )); + } + for (payload, (_, expected)) in payloads.iter().zip(&target_definition.fields) { + self.expression(function, payload)?; + self.require_same_type( + function, + payload.span, + *expected, + payload.ty, + "machine transition payload type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::ListConstruct { elements } => { + let Type::List(element) = self.types.resolve(expression.ty) else { + return Err(self.expression_kind_error( + function, + expression, + "list construction", + )); + }; + for value in elements { + self.expression(function, value)?; + self.require_same_type( + function, + value.span, + *element, + value.ty, + "list element type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::MapConstruct { entries } => { + let Type::Map(key, value) = self.types.resolve(expression.ty) else { + return Err(self.expression_kind_error( + function, + expression, + "map construction", + )); + }; + for entry in entries { + self.expression(function, &entry.key)?; + self.require_same_type( + function, + entry.key.span, + *key, + entry.key.ty, + "map key type mismatch", + )?; + self.expression(function, &entry.value)?; + self.require_same_type( + function, + entry.value.span, + *value, + entry.value.ty, + "map value type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) => { + let payload = match (&expression.kind, self.types.resolve(expression.ty)) { + (ExpressionKind::ResultOk(_), Type::Result(ok, _)) => *ok, + (ExpressionKind::ResultFail(_), Type::Result(_, error)) => *error, + (ExpressionKind::OptionalSome(_), Type::Optional(inner)) => *inner, + _ => { + return Err(self.expression_kind_error( + function, + expression, + "sum constructor", + )); + } + }; + self.expression(function, value)?; + self.require_same_type( + function, + expression.span, + payload, + value.ty, + "sum constructor payload type mismatch", + ) + } + ExpressionKind::OptionalNone => { + if matches!(self.types.resolve(expression.ty), Type::Optional(_)) { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "optional none")) + } + } + ExpressionKind::Handle { .. } => { + Err(self.unsupported(function, expression.span, "failure handler")) + } + ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + .. + } => { + self.require_call_result_type( + function, + expression.span, + *enum_type, + expression.ty, + false, + "enum construction type mismatch", + )?; + let Type::Enum(enum_id) = self.types.resolve(*enum_type) else { + return Err(self.expression_kind_error( + function, + expression, + "enum construction", + )); + }; + let index = usize::try_from(variant.index()).map_err(|_| { + self.contract_error(function, expression.span, "variant index overflow") + })?; + let Some(definition) = self.types.resolve_enum(*enum_id).variants.get(index) else { + return Err(self.contract_error( + function, + expression.span, + "enum constructor variant is missing", + )); + }; + if payloads.len() != definition.fields.len() { + return Err(self.contract_error( + function, + expression.span, + "enum constructor payload count mismatch", + )); + } + for (payload, (_, expected)) in payloads.iter().zip(&definition.fields) { + self.expression(function, payload)?; + self.require_same_type( + function, + payload.span, + *expected, + payload.ty, + "enum constructor payload type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::StringInterpolation(segments) => { + if kind != ScalarKind::String { + return Err(self.expression_kind_error(function, expression, "interpolation")); + } + for segment in segments { + if let jett_hir::StringSegment::Value(value) = segment { + self.expression(function, value)?; + if !crate::values::is_formattable(self.types, value.ty) { + return Err(self.unsupported(function, value.span, "format value")); + } + } + } + Ok(()) + } + ExpressionKind::DisplayResult(value) => { + self.expression(function, value)?; + if expression.ty == TypeInterner::STRING && value.ty == TypeInterner::STRING { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "display result")) + } + } + ExpressionKind::EquatableResult(value) => { + self.expression(function, value)?; + let mut result = expression.ty; + while let Type::Secret(inner) = self.types.resolve(result) { + result = *inner; + } + if value.ty == expression.ty && result == TypeInterner::BOOL { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "equality result")) + } + } + ExpressionKind::Declassify(value) => { + self.expression(function, value)?; + if matches!(self.types.resolve(value.ty), Type::Secret(inner) if *inner == expression.ty) + { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "declassification")) + } + } + ExpressionKind::Coarsen(value) => { + self.expression(function, value)?; + let mut ancestor = value.ty; + let mut valid = false; + while let Type::Refinement { base, .. } = self.types.resolve(ancestor) { + ancestor = *base; + if ancestor == expression.ty { + valid = true; + break; + } + } + if valid { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "coarsen")) + } + } + ExpressionKind::InterfaceCoerce { value, adapters } => { + self.expression(function, value)?; + let mut signatures = HashSet::new(); + for entry in adapters { + if !signatures.insert((entry.source, entry.target)) { + return Err(self.contract_error( + function, + expression.span, + "duplicate container callback adapter", + )); + } + self.function_adapter_signature( + function, + expression.span, + entry.source, + entry.target, + entry.function, + )?; + } + if interface_conversion_shape(self.types, value.ty, expression.ty, adapters) { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "interface conversion")) + } + } + ExpressionKind::InterfaceType(value) => { + self.expression(function, value)?; + if matches!(self.types.resolve(value.ty), Type::Interface(_)) + && expression.ty == TypeInterner::UINT64 + { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "interface type identity")) + } + } + ExpressionKind::RuntimeFailureMessage(value) => { + self.expression(function, value)?; + if value.ty == TypeInterner::STRING && expression.ty == TypeInterner::NOTHING { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "dynamic runtime failure")) + } + } + ExpressionKind::RuntimeFailure(_) => Ok(()), + ExpressionKind::PropertyCaseContext(case) => { + if expression.ty == TypeInterner::NOTHING + && case + .as_ref() + .is_none_or(|case| !case.name.is_empty() && case.trial > 0) + { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "property case context")) + } + } + ExpressionKind::RefinementValidated(value) => { + self.expression(function, value)?; + let mut current = expression.ty; + let mut valid = false; + while let Type::Refinement { base, .. } = self.types.resolve(current) { + if *base == value.ty + || matches!(self.types.resolve(*base), Type::Secret(inner) if *inner == value.ty) + { + valid = true; + break; + } + current = *base; + } + if valid { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "validated refinement")) + } + } + ExpressionKind::StateIs { value, state } => { + self.expression(function, value)?; + let machine = match self.types.resolve(value.ty) { + Type::Machine(machine) | Type::MachineState { machine, .. } => *machine, + _ => { + return Err(self.expression_kind_error( + function, + expression, + "machine state test", + )); + } + }; + if kind != ScalarKind::Bool + || self + .types + .resolve_machine(machine) + .states + .get(state.index() as usize) + .is_none() + { + return Err(self.contract_error( + function, + expression.span, + "machine state test has an invalid result or state", + )); + } + Ok(()) + } + ExpressionKind::Run(value) => { + self.expression(function, value)?; + if value.ty == expression.ty + || matches!(self.types.resolve(expression.ty), Type::Secret(inner) if *inner == value.ty) + { + Ok(()) + } else { + Err(self.contract_error(function, expression.span, "run result type mismatch")) + } + } + ExpressionKind::Join(value) => { + self.expression(function, value)?; + let Type::Result(ok, error) = self.types.resolve(expression.ty) else { + return Err(self.expression_kind_error(function, expression, "task join")); + }; + if value.ty == expression.ty || (*ok == value.ty && *error == TypeInterner::STRING) + { + Ok(()) + } else { + Err(self.contract_error( + function, + expression.span, + "join result does not match task value", + )) + } + } + ExpressionKind::Cancel(value) => { + self.expression(function, value)?; + self.require_same_type( + function, + expression.span, + TypeInterner::NOTHING, + expression.ty, + "cancel must return nothing", + ) + } + ExpressionKind::InlineFunction { .. } => { + Err(self.unsupported(function, expression.span, "inline function")) + } + ExpressionKind::ActorGraphImport(_) => Err(self.contract_error(function, expression.span, "actor graph import was not expanded before native verification")), + ExpressionKind::ActorGraphStep { instruction, args } => { + instruction.validate(function.id, &function.identity, args, expression.ty, self.types).map_err(|message| self.contract_error(function, expression.span, &message))?; + for arg in args { self.expression(function, arg)?; } + Ok(()) + } + ExpressionKind::ActorSpawn { + actor_type, + args, + constructor, + .. + } => { + let Type::Actor(actor_id) = self.types.resolve(expression.ty) else { + return Err(self.expression_kind_error(function, expression, "actor spawn")); + }; + let actor = self.types.resolve_actor(*actor_id); + let expected = if let Some(constructor) = constructor { + let (_, target) = function_by_id(self.program, *constructor)?; + if target.identity.declaration.kind + != jett_hir::DeclarationKind::ActorConstructor + || target.return_type != expression.ty + { + return Err(self.contract_error( + function, + expression.span, + "actor spawn target is not its checked constructor", + )); + } + target + .params + .iter() + .map(|param| param.ty) + .collect::>() + } else { + if function.identity.declaration.kind + != jett_hir::DeclarationKind::ActorConstructor + || function.return_type != expression.ty + || actor_type != &actor.name + { + return Err(self.contract_error( + function, + expression.span, + "actor allocation is outside its checked constructor", + )); + } + actor + .capability_params + .iter() + .chain(&actor.state_fields) + .map(|(_, ty)| *ty) + .collect::>() + }; + if args.len() != expected.len() { + return Err(self.contract_error( + function, + expression.span, + "actor spawn argument count does not match its checked fields", + )); + } + for (argument, expected) in args.iter().zip(expected) { + self.expression(function, argument)?; + self.require_same_type( + function, + argument.span, + expected, + argument.ty, + "actor spawn argument type mismatch", + )?; + } + Ok(()) + } + ExpressionKind::ActorMessage { + actor, + message, + handler, + args, + kind: message_kind, + .. + } => { + self.expression(function, actor)?; + let Type::Actor(actor_id) = self.types.resolve(actor.ty) else { + return Err(self.expression_kind_error(function, expression, "actor message")); + }; + let definition = self.types.resolve_actor(*actor_id); + let Some(checked_message) = definition.messages.iter().find(|m| m.name == *message) + else { + return Err(self.contract_error( + function, + expression.span, + "actor message is absent from its checked actor type", + )); + }; + let (_, target) = function_by_id(self.program, *handler)?; + let capture_count = + definition.capability_params.len() + definition.state_fields.len(); + let (expected_namespace, actor_name) = definition + .name + .rsplit_once('.') + .unwrap_or(("", &definition.name)); + let expected_name = format!("{actor_name}.{message}"); + if target.identity.declaration.kind != jett_hir::DeclarationKind::ActorHandler + || target.identity.declaration.name != expected_name + || target.identity.declaration.namespace != expected_namespace + || target.capture_count != capture_count + || target.return_type != checked_message.responds + || target.params.len() != capture_count + args.len() + || args.len() != checked_message.params.len() + || target.params[..capture_count] + .iter() + .zip( + definition + .capability_params + .iter() + .chain(&definition.state_fields), + ) + .any(|(parameter, (_, expected))| parameter.ty != *expected) + { + return Err(self.contract_error( + function, + expression.span, + "actor message target does not match its checked handler", + )); + } + for ((argument, parameter), (_, checked_type)) in args + .iter() + .zip(&target.params[capture_count..]) + .zip(&checked_message.params) + { + self.expression(function, argument)?; + self.require_same_type( + function, + argument.span, + parameter.ty, + argument.ty, + "actor message argument type mismatch", + )?; + self.require_same_type( + function, + argument.span, + *checked_type, + parameter.ty, + "actor handler parameter type mismatch", + )?; + } + let expected = if *message_kind == jett_hir::ActorMessageKind::Send { + TypeInterner::NOTHING + } else { + checked_message.responds + }; + self.require_same_type( + function, + expression.span, + expected, + expression.ty, + "actor message result type mismatch", + ) + } + ExpressionKind::Field { + base, + owner_type, + field, + } => { + self.expression(function, base)?; + // The checker exposes one direct secret-qualified aggregate's + // fields through its exact nominal layout. Keep the receiver's + // checked type for pending diagnostics and ownership handling. + let secret_owner = matches!( + self.types.resolve(base.ty), + Type::Secret(inner) + if *inner == *owner_type + && matches!( + self.types.resolve(*inner), + Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } + ) + ); + if base.ty != *owner_type && !secret_owner { + return Err(self.contract_error( + function, + expression.span, + "field owner mismatch", + )); + } + let ty = match self.types.resolve(*owner_type) { + Type::Struct(id) => self + .types + .resolve_struct(*id) + .fields + .get(field.index() as usize) + .map(|(_, ty)| *ty), + Type::Bitfield(id) => self + .types + .resolve_bitfield(*id) + .fields + .get(field.index() as usize) + .map(|field| field.ty), + Type::MachineState { machine, state } => self + .types + .resolve_machine(*machine) + .state(*state) + .and_then(|state| state.fields.get(field.index() as usize)) + .map(|(_, ty)| *ty), + _ => { + return Err(self.unsupported( + function, + expression.span, + "non-aggregate field access", + )); + } + } + .ok_or_else(|| { + self.contract_error(function, expression.span, "invalid aggregate field index") + })?; + // Match maybe_wrap_secret exactly: nothing stays public and an + // already secret field keeps its full nominal type, including + // refinements over secret values. Other fields gain one wrapper. + let valid_result = if secret_owner + && ty != TypeInterner::NOTHING + && self.secret_inner_type(ty).is_none() + { + matches!(self.types.resolve(expression.ty), Type::Secret(inner) if *inner == ty) + } else { + expression.ty == ty + }; + if !valid_result { + return Err(self.contract_error( + function, + expression.span, + "projected field type mismatch", + )); + } + Ok(()) + } + } + } + + fn integer_literal( + &self, + function: &Function, + expression: &Expression, + value: i128, + kind: ScalarKind, + ) -> Result<(), CodegenError> { + let in_range = match kind { + ScalarKind::SignedInteger(bits) => { + let magnitude = 1_i128 << (bits - 1); + (-magnitude..magnitude).contains(&value) + } + ScalarKind::UnsignedInteger(bits) => { + let limit = 1_i128 << bits; + (0..limit).contains(&value) + } + _ => false, + }; + if in_range { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "integer literal")) + } + } + + fn binary( + &self, + function: &Function, + expression: &Expression, + left: &Expression, + op: BinaryOp, + right: &Expression, + ) -> Result<(), CodegenError> { + self.expression(function, left)?; + self.expression(function, right)?; + let operand = self.expression_kind(function, left, "binary operand")?; + let right_operand = self.expression_kind(function, right, "binary operand")?; + let left_type = self.secret_inner_type(left.ty).unwrap_or(left.ty); + let right_type = self.secret_inner_type(right.ty).unwrap_or(right.ty); + let refined_divisor = operand.is_integer() + && matches!(op, BinaryOp::Divide | BinaryOp::Modulo) + && matches!( + self.types.resolve(right_type), + Type::Refinement { base, .. } if *base == left_type + ) + && operand == right_operand; + if left_type != right_type && !refined_divisor { + return Err(self.contract_error( + function, + expression.span, + "binary operand types differ", + )); + } + let result = self.expression_kind(function, expression, "binary result")?; + if (self.secret_inner_type(left.ty).is_some() || self.secret_inner_type(right.ty).is_some()) + && self.secret_inner_type(expression.ty).is_none() + { + return Err(self.contract_error( + function, + expression.span, + "secret binary operands require a secret result", + )); + } + if operand == ScalarKind::Enum + && matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) + && !known_unit_enum_variant(left) + && !known_unit_enum_variant(right) + && let Type::Enum(id) = self + .types + .resolve(jett_mir::move_values::representation_type( + self.types, left.ty, + )) + && self + .types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| &variant.fields) + .any(|(_, field_ty)| { + !matches!( + scalar_kind(self.types, *field_ty, "enum equality payload"), + Ok(ScalarKind::SignedInteger(_) + | ScalarKind::UnsignedInteger(_) + | ScalarKind::Float(32 | 64) + | ScalarKind::Bool + | ScalarKind::String) + ) + }) + && super::emit::debug::equality_layout( + self.types, + left.ty, + &self.program.equality_methods, + ) + .is_none() + { + return Err(self.unsupported(function, expression.span, "enum equality payload type")); + } + if matches!(op, BinaryOp::Divide | BinaryOp::Modulo) + && operand.is_integer() + && integer_expression_is_statically_zero(right) + { + return Err(self.contract_error( + function, + right.span, + "integer division or modulo has a statically zero divisor", + )); + } + let supported = match op { + BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide => { + operand.is_numeric() && result == operand + } + BinaryOp::Modulo => operand.is_numeric() && result == operand, + BinaryOp::Equal | BinaryOp::NotEqual => { + !matches!( + operand, + ScalarKind::Stdout + | ScalarKind::Clock + | ScalarKind::Random + | ScalarKind::Environment + | ScalarKind::Graphics + | ScalarKind::OpaqueCapability + | ScalarKind::Bytes + | ScalarKind::Sum + | ScalarKind::List + | ScalarKind::Map + | ScalarKind::Struct + | ScalarKind::Bitfield + | ScalarKind::Machine + | ScalarKind::Function + ) && result == ScalarKind::Bool + } + BinaryOp::Less | BinaryOp::Greater | BinaryOp::LessEqual | BinaryOp::GreaterEqual => { + operand.is_numeric() && result == ScalarKind::Bool + } + BinaryOp::And | BinaryOp::Or => { + operand == ScalarKind::Bool && result == ScalarKind::Bool + } + }; + if supported { + Ok(()) + } else { + Err(self.expression_kind_error(function, expression, "binary operation")) + } + } + + /// Match the checker's directional secret argument compatibility without + /// treating equal native representations as interchangeable source types. + fn call_argument_types_compatible(&self, expected: TypeId, actual: TypeId) -> bool { + if expected == actual { + return true; + } + match (self.types.resolve(expected), self.types.resolve(actual)) { + (Type::Secret(expected), Type::Secret(actual)) + | (Type::List(expected), Type::List(actual)) + | (Type::Optional(expected), Type::Optional(actual)) + | (Type::Set(expected), Type::Set(actual)) => { + self.call_argument_types_compatible(*expected, *actual) + } + (Type::Secret(expected), _) => self.call_argument_types_compatible(*expected, actual), + (Type::Map(expected_key, expected_value), Type::Map(actual_key, actual_value)) + | ( + Type::Result(expected_key, expected_value), + Type::Result(actual_key, actual_value), + ) => { + self.call_argument_types_compatible(*expected_key, *actual_key) + && self.call_argument_types_compatible(*expected_value, *actual_value) + } + ( + Type::Function { + params: expected_params, + view_params: expected_views, + return_type: expected_return, + }, + Type::Function { + params: actual_params, + view_params: actual_views, + return_type: actual_return, + }, + ) => { + expected_params.len() == actual_params.len() + && expected_views == actual_views + && expected_params + .iter() + .zip(actual_params) + .all(|(expected, actual)| { + self.call_argument_types_compatible(*expected, *actual) + }) + && self.call_argument_types_compatible(*expected_return, *actual_return) + } + ( + Type::Machine(expected), + Type::MachineState { + machine: actual, .. + }, + ) => expected == actual, + _ => false, + } + } + + fn secret_inner_type(&self, ty: TypeId) -> Option { + match self.types.resolve(ty) { + Type::Secret(inner) => Some(*inner), + Type::Refinement { base, .. } => self.secret_inner_type(*base), + _ => None, + } + } + + fn call_argument_taints_result( + &self, + expected: TypeId, + actual: TypeId, + pure: bool, + ) -> Option { + if self.call_argument_types_compatible(expected, actual) { + return Some(false); + } + if pure + && let Some(inner) = self.secret_inner_type(actual) + && self.call_argument_types_compatible(expected, inner) + { + return Some(true); + } + None + } + + fn require_call_result_type( + &self, + function: &Function, + span: Span, + expected: TypeId, + actual: TypeId, + lifted_secret: bool, + message: &str, + ) -> Result<(), CodegenError> { + // A surrounding secret context may add secrecy independently of a call. + if matches!(self.types.resolve(actual), Type::Secret(inner) if *inner == expected) { + return Ok(()); + } + if lifted_secret + && expected != TypeInterner::NOTHING + && self.secret_inner_type(expected).is_none() + { + return Err(self.contract_error(function, span, message)); + } + self.require_same_type(function, span, expected, actual, message) + } + + fn require_same_type( + &self, + function: &Function, + span: Span, + expected: TypeId, + actual: TypeId, + message: &str, + ) -> Result<(), CodegenError> { + if never_value_compatible(self.types, expected, actual) + || matches!( + (self.types.resolve(expected), self.types.resolve(actual)), + (Type::Machine(expected), Type::MachineState { machine: actual, .. }) + if expected == actual + ) + { + Ok(()) + } else { + Err(self.contract_error(function, span, message)) + } + } + + fn expression_kind_error( + &self, + function: &Function, + expression: &Expression, + construct: &str, + ) -> CodegenError { + self.contract_error( + function, + expression.span, + format!( + "{construct} is inconsistent with checked type `{}`", + self.types.type_name(expression.ty) + ), + ) + } + + fn function_name(&self, function: &Function) -> &str { + self.verified + .get(function.id) + .map(|function| function.symbol.as_str()) + .unwrap_or("") + } + + fn contract_error( + &self, + function: &Function, + span: Span, + message: impl Into, + ) -> CodegenError { + CodegenError::InvalidMirContract { + function: self.function_name(function).to_string(), + span, + message: message.into(), + } + } + + fn unsupported( + &self, + function: &Function, + span: Span, + construct: impl Into, + ) -> CodegenError { + CodegenError::UnsupportedMir { + function: self.function_name(function).to_string(), + span, + construct: construct.into(), + } + } +} + +fn integer_expression_is_statically_zero(expression: &Expression) -> bool { + match &expression.kind { + ExpressionKind::Int(0) => true, + ExpressionKind::Unary { + op: UnaryOp::Negate, + value, + } + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => integer_expression_is_statically_zero(value), + _ => false, + } +} diff --git a/crates/jett_codegen_cranelift/src/verify/carriers.rs b/crates/jett_codegen_cranelift/src/verify/carriers.rs new file mode 100644 index 00000000..c691784a --- /dev/null +++ b/crates/jett_codegen_cranelift/src/verify/carriers.rs @@ -0,0 +1,211 @@ +//! The generic native value visitor does not grant an aggregate Resource ABI. +//! Only exact occurrences in the freshly reconstructed carrier plan reach the +//! dedicated emitter; complete current-body/type seals are checked before here. +use super::*; +use jett_hir::IntrinsicId; +use jett_mir::ResourceCarrierOperationRole as Role; + +impl Verifier<'_> { + pub(super) fn carrier_expression( + &self, + function: &Function, + expression: &Expression, + ) -> Result, CodegenError> { + let Some(ownership) = self.resource else { + return Ok(None); + }; + let Some(plan) = ownership + .function(function.id) + .and_then(|plan| plan.carriers()) + else { + return Ok(None); + }; + let operations = plan + .operations_for_expression(expression) + .collect::>(); + if operations.is_empty() { + return Ok(None); + } + // Source invocation continues through the existing exact original tuple, + // callee/signature checks. A carrier row cannot replace that authority. + if ownership.function(function.id).is_some_and(|plan| { + plan.operations_for_expression(expression).any(|operation| { + matches!( + operation.role(), + jett_mir::ResourceOperationRole::InvokeSourceFunction { .. } + | jett_mir::ResourceOperationRole::InvokeHook { .. } + ) + }) + }) { + return Ok(None); + } + match &expression.kind { + ExpressionKind::ListConstruct { elements } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in elements { + self.expression(function, child)?; + } + } + ExpressionKind::MapConstruct { entries } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in entries { + self.expression(function, &child.key)?; + self.expression(function, &child.value)?; + } + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::EnumConstruct { + payloads: fields, .. + } + | ExpressionKind::MachineConstruct { + payloads: fields, .. + } => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + for child in fields { + self.expression(function, child)?; + } + } + ExpressionKind::OptionalNone => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + } + ExpressionKind::OptionalSome(inner) + | ExpressionKind::ResultOk(inner) + | ExpressionKind::ResultFail(inner) => { + self.require_carrier_constructor(function, expression, plan, &operations)?; + self.expression(function, inner)?; + } + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + let transfers = operations + .iter() + .filter_map(|operation| match operation.role() { + Role::Transfer { + source, + destination, + } + | Role::QualifyMachine { + source, + destination, + .. + } => Some((*source, *destination)), + _ => None, + }) + .collect::>(); + let [(source, destination)] = transfers.as_slice() else { + return Err(self.contract_error( + function, + expression.span, + "carrier qualification lacks its single exact private transfer", + )); + }; + let source_type = plan + .slots() + .get(source.index()) + .and_then(|slot| plan.shape(slot.shape())) + .map(|shape| shape.ty()); + let destination_type = plan + .slots() + .get(destination.index()) + .and_then(|slot| plan.shape(slot.shape())) + .map(|shape| shape.ty()); + if source_type != Some(value.ty) || destination_type != Some(expression.ty) { + return Err(self.contract_error( + function, + expression.span, + "carrier qualification changes its exact source/destination type", + )); + } + self.expression(function, value)?; + } + ExpressionKind::Field { base, .. } => { + if !operations.iter().any(|operation| { + matches!( + operation.role(), + Role::Extract { .. } + | Role::AdaptSum { .. } + | Role::Observe { .. } + | Role::Borrow { .. } + ) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier field lacks its exact projection/parent lease", + )); + } + self.expression(function, base)?; + } + ExpressionKind::StateIs { value, state } => { + if !operations.iter().any(|operation| { + matches!(operation.role(), Role::Observe { + observation: jett_mir::ResourceCarrierObservation::State { state: expected }, .. + } if u32::try_from(*expected).ok() == Some(state.index())) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier state observation changes its checked selected state", + )); + } + self.expression(function, value)?; + } + ExpressionKind::Intrinsic { + intrinsic, args, .. + } if matches!(intrinsic, IntrinsicId::ListLength | IntrinsicId::MapLength) + && args.len() == 1 => + { + if !operations.iter().any(|operation| { + matches!( + operation.role(), + Role::Observe { + observation: jett_mir::ResourceCarrierObservation::Length, + .. + } + ) + }) { + return Err(self.contract_error( + function, + expression.span, + "carrier length lacks its exact observation row", + )); + } + self.expression(function, &args[0])?; + } + _ => { + return Err(self.contract_error( + function, + expression.span, + "carrier occurrence does not have its dedicated exact native value transport", + )); + } + } + Ok(Some(())) + } + fn require_carrier_constructor( + &self, + function: &Function, + expression: &Expression, + plan: &jett_mir::ResourceCarrierFunctionPlan, + operations: &[&jett_mir::ResourceCarrierOperation], + ) -> Result<(), CodegenError> { + let mut begins = operations + .iter() + .filter_map(|operation| match operation.role() { + Role::BeginConstructor { destination, .. } => Some(*destination), + _ => None, + }); + let destination = begins.next().ok_or_else(|| { + self.contract_error( + function, + expression.span, + "carrier constructor lost its exact private Begin", + ) + })?; + if begins.next().is_some() + || plan.slots().get(destination.index()).and_then(|slot| plan.shape(slot.shape())).map(|shape| shape.ty()) != Some(expression.ty) + || operations.iter().filter(|operation| matches!(operation.role(), Role::CommitConstructor { destination: selected } if *selected == destination)).count() != 1 + { + return Err(self.contract_error(function, expression.span, "carrier constructor changes its exact node/destination/commit")); + } + Ok(()) + } +} diff --git a/crates/jett_codegen_cranelift/tests/object_emission.rs b/crates/jett_codegen_cranelift/tests/object_emission.rs new file mode 100644 index 00000000..8800e8de --- /dev/null +++ b/crates/jett_codegen_cranelift/tests/object_emission.rs @@ -0,0 +1,3002 @@ +use std::collections::HashMap; + +use jett_codegen_cranelift::{ + CodegenError, JETT_AOT_ENTRY_SUCCESS_V1, JETT_AOT_ENTRY_SYMBOL_V1, emit_host_object, + emit_host_program_object, emit_object_for_target, emit_program_object_for_target, host_target, + symbol_name, +}; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::{DeclarationId, DeclarationKind, FunctionIdentity}; +use jett_mir::{Program, ReflectedTypeDispatchArm, TerminatorKind}; +use jett_typecheck::CheckedGenericSpecialization; +use jett_types::{Type, TypeInterner}; +use object::{Object, ObjectSection, ObjectSymbol, RelocationTarget, SymbolKind, SymbolScope}; + +fn lower_source(source: &str) -> (Program, TypeInterner) { + lower_source_with_equatable(source, false) +} + +fn lower_source_with_equatable(source: &str, include_equatable: bool) -> (Program, TypeInterner) { + let file = FileId::new(0); + let mut parsed = jett_parser::parse(source, file); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let mut origins = HashMap::from([(file, SourceOrigin::Project)]); + if include_equatable { + let prelude_file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut prelude = jett_parser::parse( + "namespace stdlib\nexport interface Equatable:\n function equals(view self: Equatable, view other: Equatable) returns bool\n", + prelude_file, + ); + assert!(prelude.errors.is_empty()); + prelude.module.items.append(&mut parsed.module.items); + parsed.module.items = prelude.module.items; + origins.insert(prelude_file, SourceOrigin::Stdlib); + } + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "check errors: {:?}", + checked.diagnostics + ); + let hir = jett_hir::lower(&parsed.module, &resolved, &checked, &origins).expect("HIR lowering"); + let mir = jett_mir::lower(&hir, &checked.interner).expect("MIR lowering"); + (mir, checked.interner) +} + +#[test] +fn native_resource_kind_tags_emit_without_resource_values() { + let file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut parsed = jett_parser::parse( + r#"namespace opaque_metadata +export resource FileHandle +type HandleAlias = FileHandle +export function resource_kind() returns bool: + return type.kind_tag[FileHandle]() == TypeKind.resource_type +export function resource_info() returns bool: + return type.info[FileHandle]().kind_tag == TypeKind.resource_type +export function alias_kind() returns bool: + return type.kind_tag[HandleAlias]() == TypeKind.alias_type +"#, + file, + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let project_file = FileId::new(0); + let mut caller = jett_parser::parse( + r#"namespace app +function main() returns bool: + use opaque_metadata + bool kind = opaque_metadata.resource_kind() + bool info = opaque_metadata.resource_info() + bool alias = opaque_metadata.alias_kind() + return kind and info and alias +"#, + project_file, + ); + assert!(caller.errors.is_empty(), "{:?}", caller.errors); + parsed.module.items.append(&mut caller.module.items); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (file, SourceOrigin::Stdlib), + (project_file, SourceOrigin::Project), + ]); + let hir = jett_hir::lower(&parsed.module, &resolved, &checked, &origins) + .expect("resource metadata HIR"); + let mir = jett_mir::lower(&hir, &checked.interner).expect("resource metadata MIR"); + let artifact = emit_host_object(&mir, &checked.interner).expect("type-only resource object"); + assert!(!artifact.bytes.is_empty()); + for name in ["resource_kind", "resource_info", "alias_kind"] { + let function = mir + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + assert!( + artifact + .symbols + .contains(&symbol_name(&function.identity, &checked.interner).unwrap()) + ); + } +} + +#[test] +fn native_reflected_root_reads_retain_only_selected_predicates_and_failure_leaf() { + let (mut program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +type Sibling = int64 where value >= 0 +struct Record: + raw: int64 + positive: Positive + higher: Higher + sibling: Sibling +enum Event: + first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling) + second(higher: Higher, raw: int64) +machine Session: + states: + first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling) + second(higher: Higher, raw: int64) + transitions: + first to second +function record_read(view source: Record, view field: TypeField) returns Higher: + return type.field_value[Record, Higher](view source, view field) +function event_read(view source: Event, view field: TypeField) returns Higher: + return type.variant_field_value[Event, Higher](view source, view field) +function session_read(view source: Session, view field: TypeField) returns Higher: + return type.machine_field_value[Session, Higher](view source, view field) +"#, + ); + let mut predicate_symbols = HashMap::new(); + for function in &mut program.functions { + if function.identity.declaration.kind == DeclarationKind::RefinementPredicate { + function.identity.declaration.origin = SourceOrigin::Stdlib; + predicate_symbols.insert( + function.identity.declaration.name.clone(), + symbol_name(&function.identity, &types).unwrap(), + ); + } + } + let artifact = emit_host_object(&program, &types).expect("root reflected producer object"); + for name in ["Positive", "Higher"] { + assert!( + artifact.symbols.contains(&predicate_symbols[name]), + "selected predicate {name} remains reachable" + ); + } + assert!( + !artifact.symbols.contains(&predicate_symbols["Sibling"]), + "declared sibling proof is not an extra requested predicate" + ); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let failure = jett_runtime::native_abi::values::NativeLeaf::RuntimeFailMessage.symbol(); + assert!( + object.symbols().any(|s| s.name().ok() == Some(failure)), + "dynamic first predicate error uses existing borrowed failure ABI" + ); +} + +#[test] +fn native_reflected_root_emission_rejects_missing_plans_and_invalid_type_operands() { + let source = r#"namespace app +type Positive = int64 where value > 0 +struct Record: + raw: int64 +function main(view source: Record, view field: TypeField) returns Positive: + return type.field_value[Record, Positive](view source, view field) +"#; + for mutation in 0..4 { + let (mut program, types) = lower_source(source); + let mut foreign = TypeInterner::new(); + let mut invalid_type = TypeInterner::INT64; + for index in 0..types.len() + 1 { + invalid_type = foreign.intern(Type::Refinement { + name: format!("Foreign{index}"), + base: TypeInterner::INT64, + }); + } + assert!(invalid_type.index() as usize >= types.len()); + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "main") + .unwrap(); + let kind = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find_map(|s| match &mut s.kind { + jett_mir::StatementKind::Let { + value: + jett_hir::Expression { + kind: kind @ jett_hir::ExpressionKind::Intrinsic { .. }, + .. + }, + .. + } => Some(kind), + _ => None, + }) + .unwrap(); + let jett_hir::ExpressionKind::Intrinsic { + field_validation, + type_arguments, + .. + } = kind + else { + unreachable!(); + }; + match mutation { + 0 => *field_validation = None, + 1 => *field_validation = Some(jett_hir::ReflectedFieldValidation::Validate(Vec::new())), + 2 => type_arguments[0] = invalid_type, + 3 => { + type_arguments.pop(); + } + _ => unreachable!(), + } + let error = emit_host_object(&program, &types).expect_err("malformed reflected selector"); + if mutation < 2 { + assert!( + matches!(error, CodegenError::InvalidMir(ref errors) if errors.iter().any(|e| e.message.contains("proof plans were not lowered"))), + "{error:?}" + ); + } else { + assert!( + matches!(error, CodegenError::InvalidMirContract { .. }), + "{error:?}" + ); + } + } +} + +#[test] +fn native_reflected_root_emission_rejects_wrong_same_signature_predicate() { + let (mut program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type Sibling = int64 where value >= 0 +struct Record: + raw: int64 +function main(view source: Record, view field: TypeField) returns Positive: + return type.field_value[Record, Positive](view source, view field) +"#, + ); + let sibling = program + .functions + .iter() + .find(|f| { + f.identity.declaration.kind == DeclarationKind::RefinementPredicate + && f.identity.declaration.name == "Sibling" + }) + .unwrap() + .id; + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "main") + .unwrap(); + let call = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find_map(|s| match &mut s.kind { + jett_mir::StatementKind::CheckRefinement { call, .. } => Some(call), + _ => None, + }) + .unwrap(); + let jett_hir::ExpressionKind::Call { function, .. } = &mut call.kind else { + panic!("canonical predicate call"); + }; + *function = sibling; + let error = + emit_host_object(&program, &types).expect_err("predicate identity is part of proof"); + assert!( + matches!(error, CodegenError::InvalidMir(ref errors) if errors.iter().any(|e| e.message.contains("exact predicate declaration"))), + "{error:?}" + ); +} + +#[test] +fn native_return_refinement_rejections_use_the_existing_borrowed_failure_leaf() { + let (program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +function source(raw: int64) returns int64: + return run raw +function main(raw: int64) returns Higher: + return source(raw) +"#, + ); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let checks = main + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| { + if let jett_mir::StatementKind::CheckRefinement { type_name, .. } = &statement.kind { + Some(type_name.as_str()) + } else { + None + } + }) + .collect::>(); + assert_eq!(checks, ["app.Positive", "app.Higher"]); + let artifact = emit_host_object(&program, &types).expect("return predicate object"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let leaf = jett_runtime::native_abi::values::NativeLeaf::RuntimeFailMessage; + assert!( + object + .symbols() + .any(|symbol| symbol.name().ok() == Some(leaf.symbol())) + ); + assert_eq!( + leaf.parameters(), + &[ + jett_runtime::native_abi::values::AbiScalar::Pointer, + jett_runtime::native_abi::values::AbiScalar::I64 + ] + ); + assert_eq!( + leaf.result(), + jett_runtime::native_abi::values::AbiScalar::I32 + ); +} + +#[test] +fn native_return_dynamic_failure_keeps_its_message_producer_reachable() { + let (mut program, types) = lower_source( + r#"namespace app +function message() returns string: + return "dynamic error" +function unreferenced_message() returns string: + return "unused error" +function main() returns nothing: + message() + return nothing +"#, + ); + let message = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "message") + .unwrap(); + message.identity.declaration.origin = SourceOrigin::Stdlib; + let message_id = message.id; + let message_symbol = symbol_name(&message.identity, &types).unwrap(); + let unreferenced = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "unreferenced_message") + .unwrap(); + unreferenced.identity.declaration.origin = SourceOrigin::Stdlib; + let unreferenced_symbol = symbol_name(&unreferenced.identity, &types).unwrap(); + let main = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let statement = main.blocks[main.entry.index() as usize] + .statements + .iter_mut() + .find(|statement| { + matches!(&statement.kind, + jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::Call { function, .. }, .. + }) if *function == message_id) + }) + .expect("checked message call statement"); + let jett_mir::StatementKind::Evaluate(value) = &mut statement.kind else { + unreachable!("selected Evaluate statement"); + }; + assert!(matches!(&value.kind, + jett_hir::ExpressionKind::Call { + function, + ownership: jett_hir::CallOwnership::Source(_), + .. + } if *function == message_id)); + let span = value.span; + // Reparent the checked call without changing its source certificate. + let message_call = std::mem::replace( + value, + jett_hir::Expression { + kind: jett_hir::ExpressionKind::Nothing, + ty: TypeInterner::NOTHING, + span, + }, + ); + *value = jett_hir::Expression { + kind: jett_hir::ExpressionKind::RuntimeFailureMessage(Box::new(message_call)), + ty: TypeInterner::NOTHING, + span, + }; + let artifact = emit_host_object(&program, &types).expect("dynamic message producer"); + assert!(artifact.symbols.contains(&message_symbol)); + assert!(!artifact.symbols.contains(&unreferenced_symbol)); +} + +#[test] +fn native_return_dynamic_failure_rejects_malformed_types_before_emission() { + for (input, output) in [ + (TypeInterner::INT64, TypeInterner::NOTHING), + (TypeInterner::STRING, TypeInterner::STRING), + ] { + let (mut program, types) = + lower_source("function main() returns nothing:\n return nothing\n"); + let function = &mut program.functions[0]; + let span = function.span; + function.blocks[function.entry.index() as usize] + .statements + .insert( + 0, + jett_mir::Statement { + kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::RuntimeFailureMessage(Box::new( + jett_hir::Expression { + kind: if input == TypeInterner::STRING { + jett_hir::ExpressionKind::String("error".into()) + } else { + jett_hir::ExpressionKind::Int(7) + }, + ty: input, + span, + }, + )), + ty: output, + span, + }), + span, + }, + ); + let error = emit_host_object(&program, &types).expect_err("invalid dynamic failure"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("dynamic runtime failure")), + "{error:?}" + ); + } +} + +#[test] +fn display_result_checks_retain_the_selected_method_and_borrowed_runtime_leaf() { + let (mut program, types) = lower_source( + r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return run "shown:{self.value}" +function main() returns string: + Item item = Item(value: 7) + return "value:{item}" +"#, + ); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let TerminatorKind::Return(Some(value)) = + &main.blocks[main.entry.index() as usize].terminator.kind + else { + panic!("expected interpolation return"); + }; + let jett_hir::ExpressionKind::StringInterpolation(segments) = &value.kind else { + panic!("expected interpolation"); + }; + let selected = segments + .iter() + .find_map(|segment| match segment { + jett_hir::StringSegment::Value(jett_hir::Expression { + kind: jett_hir::ExpressionKind::DisplayResult(value), + .. + }) => match value.kind { + jett_hir::ExpressionKind::Call { function, .. } => Some(function), + _ => None, + }, + _ => None, + }) + .expect("selected display call has a result check"); + for function in &mut program.functions { + if function.identity.declaration.name != "main" { + function.identity.declaration.origin = SourceOrigin::Stdlib; + } + } + let selected_symbol = symbol_name( + &program.functions[selected.index() as usize].identity, + &types, + ) + .unwrap(); + let artifact = emit_host_object(&program, &types).expect("checked display object"); + assert!(artifact.symbols.contains(&selected_symbol)); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let check = jett_runtime::native_abi::values::NativeLeaf::DisplayResultCheck; + assert!( + object + .symbols() + .any(|symbol| symbol.is_undefined() && symbol.name().ok() == Some(check.symbol())) + ); + assert_eq!( + check.parameters(), + &[ + jett_runtime::native_abi::values::AbiScalar::Pointer, + jett_runtime::native_abi::values::AbiScalar::I64, + ] + ); + assert_eq!( + check.result(), + jett_runtime::native_abi::values::AbiScalar::I32 + ); +} + +#[test] +fn rejects_malformed_display_result_types_before_emission() { + for (child_type, result_type) in [ + (TypeInterner::INT64, TypeInterner::STRING), + (TypeInterner::STRING, TypeInterner::INT64), + ] { + let (mut program, types) = + lower_source("function main() returns nothing:\n return nothing\n"); + let function = &mut program.functions[0]; + let child = jett_hir::Expression { + kind: if child_type == TypeInterner::STRING { + jett_hir::ExpressionKind::String("ready".into()) + } else { + jett_hir::ExpressionKind::Int(7) + }, + ty: child_type, + span: function.span, + }; + function.blocks[function.entry.index() as usize] + .statements + .insert( + 0, + jett_mir::Statement { + kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::DisplayResult(Box::new(child)), + ty: result_type, + span: function.span, + }), + span: function.span, + }, + ); + let error = emit_host_object(&program, &types).expect_err("invalid display result"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("display result")), + "{error:?}" + ); + } +} + +#[test] +fn equality_result_checks_preserve_qualified_calls_and_method_reachability() { + for operator in ["==", "!="] { + for qualified in [false, true] { + let input = if qualified { "secret[Item]" } else { "Item" }; + let result = if qualified { "secret[bool]" } else { "bool" }; + let (mut program, types) = lower_source_with_equatable( + &format!( + "namespace app\nstruct Item:\n value: int64\nimplement Equatable for Item:\n function equals(view self: Item, view other: Item) returns bool:\n return run (self.value == other.value)\nfunction compare(view left: {input}, view right: Item) returns {result}:\n return left {operator} right\n" + ), + true, + ); + let compare = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "compare") + .unwrap(); + let TerminatorKind::Return(Some(value)) = &compare.blocks + [compare.entry.index() as usize] + .terminator + .kind + else { + panic!("expected comparison return"); + }; + let checked = if operator == "!=" { + let jett_hir::ExpressionKind::Unary { + op: jett_hir::UnaryOp::Not, + value, + } = &value.kind + else { + panic!("inequality must negate the checked method result"); + }; + value.as_ref() + } else { + value + }; + let jett_hir::ExpressionKind::EquatableResult(call) = &checked.kind else { + panic!("implicit equality call must check its result"); + }; + assert_eq!(call.ty, checked.ty); + assert_eq!(checked.ty, compare.return_type); + let jett_hir::ExpressionKind::Call { function, .. } = call.kind else { + panic!("expected selected equality method"); + }; + for candidate in &mut program.functions { + if candidate.identity.declaration.name != "compare" { + candidate.identity.declaration.origin = SourceOrigin::Stdlib; + } + } + let method_symbol = symbol_name( + &program.functions[function.index() as usize].identity, + &types, + ) + .unwrap(); + let artifact = emit_host_object(&program, &types).expect("checked equality object"); + assert!(artifact.symbols.contains(&method_symbol)); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let check = jett_runtime::native_abi::values::NativeLeaf::RejectPendingScalars; + assert!( + object + .symbols() + .any(|symbol| symbol.is_undefined() + && symbol.name().ok() == Some(check.symbol())) + ); + } + } +} + +#[test] +fn rejects_malformed_equality_result_types_before_emission() { + for mutation in ["integer", "secret mismatch", "refinement"] { + let (mut program, mut types) = lower_source( + "namespace app\ntype Truth = bool where true\nfunction main() returns nothing:\n return nothing\n", + ); + let refined = types + .type_ids() + .find(|id| matches!(types.resolve(*id), Type::Refinement { .. })) + .unwrap(); + let secret = types.intern(Type::Secret(TypeInterner::BOOL)); + let (input, output) = match mutation { + "integer" => (TypeInterner::INT64, TypeInterner::BOOL), + "secret mismatch" => (TypeInterner::BOOL, secret), + "refinement" => (refined, refined), + _ => unreachable!(), + }; + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let child = jett_hir::Expression { + kind: if input == TypeInterner::INT64 { + jett_hir::ExpressionKind::Int(7) + } else { + jett_hir::ExpressionKind::Bool(true) + }, + ty: input, + span: function.span, + }; + function.blocks[function.entry.index() as usize] + .statements + .insert( + 0, + jett_mir::Statement { + kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::EquatableResult(Box::new(child)), + ty: output, + span: function.span, + }), + span: function.span, + }, + ); + let error = emit_host_object(&program, &types).expect_err("invalid equality result"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("equality result")), + "{mutation}: {error:?}" + ); + } +} + +#[test] +fn rejects_malformed_property_case_metadata_before_emission() { + for (name, trial, ty) in [ + ("", 1, TypeInterner::NOTHING), + ("property", 0, TypeInterner::NOTHING), + ("property", 1, TypeInterner::INT64), + ] { + let (mut program, types) = + lower_source("function main() returns nothing:\n return nothing\n"); + let function = &mut program.functions[0]; + function.blocks[0].statements.insert( + 0, + jett_mir::Statement { + kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::PropertyCaseContext(Some( + jett_hir::NativePropertyCase { + name: name.to_owned(), + trial, + }, + )), + ty, + span: function.span, + }), + span: function.span, + }, + ); + let error = emit_host_object(&program, &types).expect_err("invalid case metadata"); + assert!( + error.to_string().contains("property case context"), + "{error}" + ); + } +} + +#[test] +fn emits_function_debug_descriptors_with_source_labels() { + let (program, types) = lower_source( + r#"namespace display +function named(first: int64, second: int64) returns int64: + return first + second +function factory[T](value: T) returns function(T) returns T: + return function(input: T) returns T: return value +struct Holder: + callback: function(int64, int64) returns int64 +function main() returns int64: + function(int64, int64) returns int64 declared = named + function(int64, int64) returns int64 plain = function(left: int64, right: int64) returns int64: return left + right + int64 delta = 3 + function(int64) returns int64 captured = function(argument: int64) returns int64: return argument + delta + function(int64) returns int64 generic = factory[int64](9) + Holder holder = Holder(callback: clone declared) + list[optional[function(int64, int64) returns int64]] nested = list(some(clone plain)) + trace declared + trace plain + trace captured + trace generic + trace holder + trace nested + breakpoint + return declared(1, 2) + plain(1, 2) + captured(4) + generic(8) +"#, + ); + let artifact = emit_host_object(&program, &types).expect("function debug object emission"); + for label in [ + "function(display.named)", + "function(left, right)", + "function(argument)", + "function(input)", + ] { + assert!( + artifact + .bytes + .windows(label.len()) + .any(|bytes| bytes == label.as_bytes()), + "missing source label {label}" + ); + } + for function in &program.functions { + let label_is_inline = matches!(function.debug_kind, jett_hir::FunctionDebugKind::Inline); + if function.capture_count > 0 { + assert!( + label_is_inline, + "captured source closure must retain inline identity" + ); + } + } +} + +#[test] +fn rejects_malformed_function_debug_metadata_before_emission() { + let (program, types) = lower_source( + "namespace app\nfunction identity(value: int64) returns int64:\n return value\n", + ); + for name in ["", "app.identity\n"] { + let mut malformed = program.clone(); + malformed.functions[0].debug_kind = jett_hir::FunctionDebugKind::Named(name.into()); + let error = emit_host_object(&malformed, &types).expect_err("invalid named metadata"); + assert!( + error + .to_string() + .contains("debug identity has invalid source name metadata"), + "{error}" + ); + } + let mut malformed = program.clone(); + malformed.functions[0].debug_kind = jett_hir::FunctionDebugKind::Inline; + malformed.functions[0].params[0].name.clear(); + malformed.functions[0].locals[0].name.clear(); + let error = emit_host_object(&malformed, &types).expect_err("invalid inline metadata"); + assert!( + error + .to_string() + .contains("debug parameter has invalid source name metadata"), + "{error}" + ); + + let mut malformed = program; + malformed.functions[0].capture_count = 2; + assert!(matches!( + emit_host_object(&malformed, &types), + Err(CodegenError::InvalidMir(_)) + )); +} + +#[test] +fn rejects_function_references_that_omit_a_capture_environment() { + let (mut program, types) = lower_source( + "namespace app\nfunction identity(value: int64) returns int64:\n return value\nfunction factory() returns function(int64) returns int64:\n return identity\n", + ); + program.functions[0].capture_count = 1; + let error = + emit_host_object(&program, &types).expect_err("function reference needs environment"); + assert!( + error + .to_string() + .contains("function value signature does not match target"), + "{error}" + ); +} + +#[test] +fn emits_checked_secret_arguments_for_direct_and_indirect_calls() { + for callee in ["callback", "factory()"] { + for (parameter, returned, body, argument, result) in [ + ("int64", "int64", "value", "secret[int64]", "secret[int64]"), + ("secret[int64]", "int64", "7", "int64", "int64"), + ("secret[int64]", "int64", "7", "secret[int64]", "int64"), + ( + "int64", + "secret[int64]", + "value", + "secret[int64]", + "secret[int64]", + ), + ("int64", "nothing", "nothing", "secret[int64]", "nothing"), + ( + "Positive", + "Positive", + "value", + "secret[Positive]", + "secret[Positive]", + ), + ("Count", "Count", "value", "secret[Count]", "secret[Count]"), + ("int64", "int64", "value", "Classified", "secret[int64]"), + ( + "Classified", + "Classified", + "value", + "secret[Classified]", + "Classified", + ), + ] { + let source = format!( + "namespace app\n\ + type Positive = int64 where value > 0\n\ + type Count = int64\n\ + type Classified = secret[int64] where true\n\ + function callback(value: {parameter}) returns {returned}:\n return {body}\n\ + function factory() returns function({parameter}) returns {returned}:\n return callback\n\ + function caller(value: {argument}) returns {result}:\n return {callee}(value)\n" + ); + let (program, types) = lower_source(&source); + emit_host_object(&program, &types) + .unwrap_or_else(|error| panic!("{source}\nobject emission failed: {error:?}")); + } + } +} + +fn returned_call_mut(function: &mut jett_mir::Function) -> &mut jett_hir::Expression { + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(expression)) = &mut function.blocks[entry].terminator.kind + else { + panic!("expected returned call"); + }; + assert!(matches!( + expression.kind, + jett_hir::ExpressionKind::Call { .. } | jett_hir::ExpressionKind::IndirectCall { .. } + )); + expression +} + +#[test] +fn rejects_secret_lifted_calls_with_untainted_mir_results() { + for callee in ["callback", "factory()"] { + let source = format!( + "namespace app\n\ + function callback(value: int64) returns int64:\n return value\n\ + function factory() returns function(int64) returns int64:\n return callback\n\ + function caller(value: secret[int64]) returns secret[int64]:\n return {callee}(value)\n" + ); + let (mut program, types) = lower_source(&source); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .expect("caller"); + caller.return_type = TypeInterner::INT64; + returned_call_mut(caller).ty = TypeInterner::INT64; + + let error = emit_host_object(&program, &types) + .expect_err("secret lifting must retain the checked result taint"); + assert!( + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == "call ownership result differs from its checked source certificate")), + "{callee}: {error:?}" + ); + } +} + +#[test] +fn container_callback_adapters_have_native_data_relocations() { + let (mut program, types) = lower_source(CONTAINER_CALLBACK_SOURCE); + let adapter = program + .functions + .iter_mut() + .find(|function| { + function + .identity + .declaration + .name + .starts_with("$interface.adapter.") + }) + .expect("generated container callback adapter"); + // It must remain reachable through the descriptor, independently of project roots. + adapter.identity.declaration.origin = SourceOrigin::Stdlib; + let symbol = symbol_name(&adapter.identity, &types).unwrap(); + let target = host_target().to_string(); + { + let emitted = emit_object_for_target(&program, &types, &target).unwrap(); + let object = object::File::parse(emitted.bytes.as_slice()).unwrap(); + let mut found = false; + for section in object + .sections() + .filter(|section| section.kind() != object::SectionKind::Text) + { + for (_, relocation) in section.relocations() { + if let RelocationTarget::Symbol(index) = relocation.target() + && object.symbol_by_index(index).unwrap().name().unwrap() == symbol + { + assert_eq!(relocation.size(), 64); + found = true; + } + } + } + assert!( + found, + "{target} omitted the callback descriptor relocation for {symbol}" + ); + } +} + +const CONTAINER_CALLBACK_SOURCE: &str = "type Source = function(secret[int64]) returns secret[int64]\ntype Target = function(int64) returns secret[int64]\nfunction convert(items: list[Source]) returns list[Target]:\n return items\n"; + +#[test] +fn container_callback_adapters_reject_missing_or_invalid_checked_metadata() { + for mutation in 0..4 { + let (mut program, types) = lower_source(CONTAINER_CALLBACK_SOURCE); + let convert = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "convert") + .unwrap(); + let TerminatorKind::Return(Some(expression)) = &mut convert.blocks + [convert.entry.index() as usize] + .terminator + .kind + else { + panic!("expected a returned container conversion"); + }; + let jett_hir::ExpressionKind::InterfaceCoerce { adapters, .. } = &mut expression.kind + else { + panic!("expected container callback metadata"); + }; + assert_eq!(adapters.len(), 1); + match mutation { + 0 => adapters.clear(), + 1 => adapters[0].function = convert.id, + 2 => adapters[0].target = adapters[0].source, + _ => adapters.push(adapters[0].clone()), + } + let error = emit_host_object(&program, &types).expect_err("malformed callback conversion"); + assert!( + matches!(error, CodegenError::InvalidMirContract { .. }), + "{mutation}: {error:?}" + ); + } +} + +#[test] +fn interface_coercion_cannot_remove_secret_call_result_taint() { + let source = "function identity(value: int64) returns int64:\n return value\nfunction caller(value: secret[int64]) returns secret[int64]:\n return identity(value)\n"; + let (mut program, types) = lower_source(source); + let caller = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .unwrap(); + caller.return_type = TypeInterner::INT64; + let value = returned_call_mut(caller); + value.kind = jett_hir::ExpressionKind::interface_coerce(Box::new(value.clone())); + value.ty = TypeInterner::INT64; + let error = emit_host_object(&program, &types) + .expect_err("interface coercion cannot declassify a secret"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } if message.contains("interface conversion")), + "{error:?}" + ); +} + +#[test] +fn rejects_secret_lifting_with_mismatched_argument_types() { + for callee in ["callback", "factory()"] { + for parameter in ["int64", "Positive"] { + let source = format!( + "namespace app\n\ + type Positive = int64 where value > 0\n\ + function callback(value: {parameter}) returns {parameter}:\n return value\n\ + function factory() returns function({parameter}) returns {parameter}:\n return callback\n\ + function caller(value: secret[{parameter}]) returns secret[{parameter}]:\n return {callee}(value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let expression = returned_call_mut( + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .expect("caller"), + ); + let (jett_hir::ExpressionKind::Call { args, .. } + | jett_hir::ExpressionKind::IndirectCall { args, .. }) = &mut expression.kind + else { + unreachable!("returned_call_mut validates the expression kind"); + }; + // Neither an unrelated primitive nor the refinement's bare base is + // a valid replacement for its checked secret payload type. + let payload_type = if parameter == "Positive" { + TypeInterner::INT64 + } else { + TypeInterner::INT32 + }; + args[0].ty = types.intern(Type::Secret(payload_type)); + args[0].kind = jett_hir::ExpressionKind::Int(7); + + let error = emit_host_object(&program, &types) + .expect_err("secret lifting must preserve the expected payload type"); + assert!( + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == if callee == "callback" { + "call ownership converted operand differs from its physical parameter" + } else { "call ownership indirect converted operand differs from its physical parameter" })), + "{callee}({parameter}): {error:?}" + ); + } + } +} + +#[test] +fn rejects_secret_lifting_into_impure_call_parameters() { + for callee in ["callback", "factory()"] { + let source = format!( + "namespace app\n\ + function callback(view stdout: Stdout, value: int64) returns int64:\n return value\n\ + function factory() returns function(view Stdout, int64) returns int64:\n return callback\n\ + function caller(view stdout: Stdout, value: int64) returns int64:\n return {callee}(view stdout, value)\n" + ); + let (mut program, mut types) = lower_source(&source); + let expression = returned_call_mut( + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "caller") + .expect("caller"), + ); + let (jett_hir::ExpressionKind::Call { args, .. } + | jett_hir::ExpressionKind::IndirectCall { args, .. }) = &mut expression.kind + else { + unreachable!("returned_call_mut validates the expression kind"); + }; + args[1].ty = types.intern(Type::Secret(TypeInterner::INT64)); + args[1].kind = jett_hir::ExpressionKind::Int(7); + + let error = emit_host_object(&program, &types) + .expect_err("impure calls cannot accept secret-lifted arguments"); + assert!( + matches!(error, CodegenError::InvalidMir(ref errors) + if errors.iter().any(|error| error.message == if callee == "callback" { + "call ownership converted operand differs from its physical parameter" + } else { "call ownership indirect converted operand differs from its physical parameter" })), + "{callee}: {error:?}" + ); + } +} + +#[test] +fn emits_arithmetic_with_a_refined_integer_operand() { + let (program, types) = lower_source( + r#"namespace app +type NonZeroInt = int64 where value != 0 +function quotient(value: int64, divisor: NonZeroInt) returns int64: + return value / divisor +function remainder(value: int64, divisor: NonZeroInt) returns int64: + return value modulo divisor +"#, + ); + + let object = emit_host_object(&program, &types).expect("refined integer object emission"); + assert_eq!(object.symbols.len(), 2); +} + +#[test] +fn emits_coarsening_for_scalar_and_owned_refinements() { + let (program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type AboveTen = Positive where value > 10 +type NonEmpty = string where value != "" +function coarsen_positive(value: Positive) returns int64: + return coarsen value +function coarsen_to_ancestor(value: AboveTen) returns int64: + return coarsen value +function coarsen_non_empty(value: NonEmpty) returns string: + return coarsen value +"#, + ); + + let object = emit_host_object(&program, &types).expect("refinement coarsen object emission"); + assert_eq!(object.symbols.len(), 3); +} + +#[test] +fn emits_a_deterministic_host_object_for_scalar_control_flow() { + let (program, types) = lower_source( + r#"namespace app +function adjust(value: int64, delta: int64) returns int64: + mutable int64 current = value + delta + if current > 10: + current = current - 1 + else: + current = current + 1 + return current +function main() returns int64: + return adjust(delta: 2, value: 10) +"#, + ); + + let caller = &program.functions[1]; + let jett_mir::TerminatorKind::Return(Some(call)) = + &caller.blocks[caller.entry.index() as usize].terminator.kind + else { + panic!("expected direct call return"); + }; + let jett_hir::ExpressionKind::Call { + evaluation_order, .. + } = &call.kind + else { + panic!("expected direct call"); + }; + assert_eq!( + evaluation_order, + &[1, 0], + "MIR arguments stay in parameter order while evaluation order stays lexical" + ); + + let first = emit_host_object(&program, &types).expect("first object emission"); + let second = emit_host_object(&program, &types).expect("second object emission"); + let explicit = emit_object_for_target(&program, &types, &host_target().to_string()) + .expect("explicit host object emission"); + + assert_eq!(first.target, host_target().to_string()); + assert_eq!(first.symbols, second.symbols); + assert_eq!(first.bytes, second.bytes); + assert_eq!(first, explicit); + assert_eq!(first.symbols.len(), 2); + let object = object::File::parse(first.bytes.as_slice()).expect("parse emitted object"); + let object_symbols = object + .symbols() + .filter_map(|symbol| symbol.name().ok()) + .collect::>(); + for symbol in &first.symbols { + assert!( + object_symbols.contains(&symbol.as_str()), + "object does not define {symbol}" + ); + } + let mut relocated_symbols = Vec::new(); + for section in object.sections() { + for (_, relocation) in section.relocations() { + if let RelocationTarget::Symbol(symbol_index) = relocation.target() + && let Ok(symbol) = object.symbol_by_index(symbol_index) + && let Ok(name) = symbol.name() + { + relocated_symbols.push(name.to_string()); + } + } + } + assert!( + relocated_symbols.contains(&first.symbols[0]), + "the caller must relocate against callee symbol {}; relocation targets: {relocated_symbols:?}", + first.symbols[0] + ); +} + +#[test] +fn omits_an_unreached_unsupported_stdlib_function() { + let (mut program, types) = lower_source( + r#"namespace app +function unused_text() returns string: + return "unused" +function main() returns int64: + return 7 +"#, + ); + let unused = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "unused_text") + .expect("unused function"); + unused.identity.declaration.origin = SourceOrigin::Stdlib; + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("project root"); + let main_symbol = symbol_name(&main.identity, &types).expect("main symbol"); + + let artifact = emit_host_object(&program, &types) + .expect("unreached unsupported stdlib MIR must not block emission"); + + assert_eq!(artifact.symbols, [main_symbol]); + object::File::parse(artifact.bytes.as_slice()).expect("parse reachable-only object"); +} + +#[test] +fn emits_dependency_and_stdlib_functions_reached_from_a_project_root() { + let (mut program, types) = lower_source( + r#"namespace app +function dependency_leaf(value: int64) returns int64: + return value + 1 +function unrelated() returns int64: + return 99 +function stdlib_helper(value: int64) returns int64: + return dependency_leaf(value) +function main() returns int64: + return stdlib_helper(41) +"#, + ); + let dependency_leaf = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "dependency_leaf") + .expect("dependency function"); + dependency_leaf.identity.declaration.origin = SourceOrigin::Dependency("dep.math".to_string()); + let dependency_symbol = + symbol_name(&dependency_leaf.identity, &types).expect("dependency symbol"); + let stdlib_helper = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "stdlib_helper") + .expect("stdlib function"); + stdlib_helper.identity.declaration.origin = SourceOrigin::Stdlib; + let stdlib_symbol = symbol_name(&stdlib_helper.identity, &types).expect("stdlib symbol"); + let unrelated = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "unrelated") + .expect("unrelated function"); + unrelated.identity.declaration.origin = SourceOrigin::Stdlib; + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("project root"); + let main_symbol = symbol_name(&main.identity, &types).expect("main symbol"); + + let first = emit_host_object(&program, &types).expect("reachable stdlib object emission"); + let second = emit_host_object(&program, &types).expect("deterministic repeated emission"); + + assert_eq!(first, second); + assert_eq!( + first.symbols, + [ + dependency_symbol.clone(), + stdlib_symbol.clone(), + main_symbol + ] + ); + let object = object::File::parse(first.bytes.as_slice()).expect("parse reachable object"); + let relocated_symbols = object + .sections() + .flat_map(|section| section.relocations().map(|(_, relocation)| relocation)) + .filter_map(|relocation| match relocation.target() { + RelocationTarget::Symbol(index) => object.symbol_by_index(index).ok(), + _ => None, + }) + .filter_map(|symbol| symbol.name().ok()) + .collect::>(); + assert!( + relocated_symbols.contains(&dependency_symbol.as_str()) + && relocated_symbols.contains(&stdlib_symbol.as_str()), + "reached dependency and stdlib functions must retain their original relocation symbols; relocation targets: {relocated_symbols:?}" + ); +} + +#[test] +fn emits_a_deterministic_exported_program_entry_wrapper() { + let (program, types) = lower_source( + r#"namespace app +function helper() returns nothing: + return nothing +function selected_entry() returns nothing: + return helper() +"#, + ); + let entry = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "selected_entry") + .expect("selected entry"); + let entry_id = entry.id; + let entry_symbol = symbol_name(&entry.identity, &types).expect("entry symbol"); + + let first = + emit_host_program_object(&program, &types, entry_id).expect("program object emission"); + let second = + emit_host_program_object(&program, &types, entry_id).expect("repeated program emission"); + let explicit = + emit_program_object_for_target(&program, &types, &host_target().to_string(), entry_id) + .expect("explicit-target program emission"); + assert_eq!(first, second); + assert_eq!(first, explicit); + assert_eq!(JETT_AOT_ENTRY_SUCCESS_V1, 0); + assert_eq!( + first.symbols.last().map(String::as_str), + Some(JETT_AOT_ENTRY_SYMBOL_V1) + ); + + let ordinary = emit_host_object(&program, &types).expect("ordinary object emission"); + assert!( + !ordinary + .symbols + .iter() + .any(|symbol| symbol == JETT_AOT_ENTRY_SYMBOL_V1), + "ordinary object emission must remain wrapper-free" + ); + + let object = object::File::parse(first.bytes.as_slice()).expect("parse program object"); + let wrapper = object + .symbols() + .find(|symbol| symbol.name() == Ok(JETT_AOT_ENTRY_SYMBOL_V1)) + .expect("exported entry wrapper symbol"); + assert!(!wrapper.is_undefined()); + assert!(wrapper.is_global()); + assert_eq!(wrapper.kind(), SymbolKind::Text); + assert!(matches!( + wrapper.scope(), + SymbolScope::Linkage | SymbolScope::Dynamic + )); + + let relocated_symbols = object + .sections() + .flat_map(|section| section.relocations().map(|(_, relocation)| relocation)) + .filter_map(|relocation| match relocation.target() { + RelocationTarget::Symbol(index) => object.symbol_by_index(index).ok(), + _ => None, + }) + .filter_map(|symbol| symbol.name().ok()) + .collect::>(); + assert!( + relocated_symbols.contains(&entry_symbol.as_str()), + "entry wrapper must relocate against the exact selected MIR function symbol {entry_symbol}; relocation targets: {relocated_symbols:?}" + ); +} + +#[test] +fn rejects_an_absent_program_entry_id() { + let (mut program, types) = lower_source( + r#"namespace app +function entry() returns nothing: + return nothing +"#, + ); + let entry = program.functions[0].id; + program.functions.clear(); + + let error = emit_host_program_object(&program, &types, entry) + .expect_err("an absent MIR entry must be rejected explicitly"); + + assert_eq!( + error, + CodegenError::MissingProgramEntry { + function_id: entry.index() + } + ); +} + +#[test] +fn rejects_a_duplicate_program_entry_id() { + let (mut program, types) = lower_source( + r#"namespace app +function entry() returns nothing: + return nothing +"#, + ); + let entry = program.functions[0].id; + program.functions.push(program.functions[0].clone()); + + let error = emit_host_program_object(&program, &types, entry) + .expect_err("a duplicate MIR entry ID must be rejected explicitly"); + + assert_eq!( + error, + CodegenError::DuplicateProgramEntry { + function_id: entry.index() + } + ); +} + +#[test] +fn rejects_incompatible_program_entry_signatures() { + let cases = [ + ( + r#"namespace app +function entry(value: int64) returns nothing: + return nothing +"#, + "unsupported native entry capability parameter(s): int64", + ), + ( + r#"namespace app +function entry() returns int64: + return 1 +"#, + "expected return type `nothing`", + ), + ]; + for (source, expected_message) in cases { + let (program, types) = lower_source(source); + let entry = program.functions[0].id; + + let error = emit_host_program_object(&program, &types, entry) + .expect_err("an incompatible entry signature must be rejected explicitly"); + + assert!(matches!( + error, + CodegenError::IncompatibleProgramEntry { + function_id, + message, + } if function_id == entry.index() && message.contains(expected_message) + )); + } +} + +#[test] +fn rejects_an_unreachable_program_entry() { + let (mut program, types) = lower_source( + r#"namespace app +function library_entry() returns nothing: + return nothing +function project_root() returns nothing: + return nothing +"#, + ); + let library_entry = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "library_entry") + .expect("library entry"); + library_entry.identity.declaration.origin = SourceOrigin::Stdlib; + let entry = library_entry.id; + + let error = emit_host_program_object(&program, &types, entry) + .expect_err("an unreachable library function cannot be selected as the program entry"); + + assert_eq!( + error, + CodegenError::UnreachableProgramEntry { + function_id: entry.index() + } + ); +} + +#[test] +fn honors_a_valid_nonzero_mir_entry_block() { + let (mut program, types) = lower_source( + r#"namespace app +function choose() returns int64: + if true: + return 1 + else: + return 2 +"#, + ); + program.functions[0].entry = program.functions[0].blocks[1].id; + program.functions[0].blocks[0].terminator.kind = + program.functions[0].blocks[2].terminator.kind.clone(); + + emit_host_object(&program, &types).expect("nonzero entry block object emission"); +} + +#[test] +fn rejects_a_control_flow_predecessor_to_the_abi_entry_block() { + let (mut program, types) = lower_source( + r#"namespace app +function identity(value: int64) returns int64: + return value +"#, + ); + let function = &mut program.functions[0]; + let entry = function.entry; + function.blocks[entry.index() as usize].terminator.kind = TerminatorKind::Goto(entry); + + let error = emit_host_object(&program, &types) + .expect_err("an ABI entry block cannot have a MIR predecessor"); + + assert!(matches!( + error, + CodegenError::InvalidMirContract { message, .. } + if message.contains("targets ABI entry block") + )); +} + +#[test] +fn emits_all_scalar_primitive_widths_and_operators() { + let (program, types) = lower_source( + r#"namespace app +function signed8(value: int8) returns bool: + int8 added = value + 1 + int8 subtracted = added - 1 + int8 multiplied = subtracted * 2 + int8 divided = multiplied / 2 + int8 remainder = multiplied modulo 2 + return divided >= remainder +function signed16(value: int16) returns int16: + return -value +function signed32(value: int32) returns int32: + return value +function signed64_min_division() returns int64: + return ((-9223372036854775807) - 1) / (-1) +function unsigned8(value: uint8) returns bool: + uint8 added = value + 255 + uint8 divided = added / 2 + uint8 remainder = added modulo 2 + return divided < remainder +function unsigned16(value: uint16) returns uint16: + return value + 65535 +function unsigned32(value: uint32) returns uint32: + return value + 4294967295 +function unsigned64_max() returns uint64: + return 18446744073709551615 +function float32_ops(value: float32) returns bool: + float32 calculated = ((value + 1.0) - 1.0) * 2.0 / 2.0 + return calculated != value +function float64_ops(value: float64) returns bool: + return -value <= value +function bool_ops(left: bool, right: bool) returns bool: + return !left || (left && right) +function count_down(value: int64) returns int64: + mutable int64 current = value + while current > 0: + current = current - 1 + return current +function consume_nothing(value: nothing) returns nothing: + return value +function call_nothing() returns nothing: + return consume_nothing(nothing) +"#, + ); + + let artifact = emit_host_object(&program, &types).expect("scalar object emission"); + + assert_eq!(artifact.symbols.len(), 14); + object::File::parse(artifact.bytes.as_slice()).expect("parse scalar object"); +} + +#[test] +fn emits_pending_nothing_through_parameters_returns_and_the_entry_wrapper() { + let (program, types) = lower_source( + r#"namespace app +function empty() returns nothing: + return nothing +function forward(value: nothing) returns nothing: + return value +function main() returns nothing: + nothing pending = run forward(empty()) + nothing forwarded = forward(pending) + nothing joined = join forwarded handle error: + return nothing + bool equal = joined == nothing + bool different = joined != nothing + trace pending + println(joined) +"#, + ); + let entry = program.functions.last().expect("main function").id; + let artifact = emit_host_program_object(&program, &types, entry) + .expect("pending nothing must retain its runtime depth through calls"); + let object = object::File::parse(artifact.bytes.as_slice()).expect("parse task object"); + let undefined = object + .symbols() + .filter(|symbol| symbol.is_undefined()) + .filter_map(|symbol| symbol.name().ok()) + .collect::>(); + for leaf in [ + jett_runtime::native_abi::values::NativeLeaf::NothingRun, + jett_runtime::native_abi::values::NativeLeaf::NothingJoin, + jett_runtime::native_abi::values::NativeLeaf::NothingFormat, + jett_runtime::native_abi::values::NativeLeaf::NothingEqual, + ] { + assert!( + undefined.contains(&leaf.symbol()), + "missing typed task leaf {}", + leaf.symbol() + ); + } + assert_eq!( + artifact.symbols.last().map(String::as_str), + Some(JETT_AOT_ENTRY_SYMBOL_V1) + ); +} + +#[test] +fn emits_direct_signed_minimum_literals_at_every_width() { + let (program, types) = lower_source( + r#"namespace app +function minimum8() returns int8: + return -128 +function minimum16() returns int16: + return -32768 +function minimum32() returns int32: + return -2147483648 +function minimum64() returns int64: + return -9223372036854775808 +"#, + ); + + let artifact = emit_host_object(&program, &types).expect("signed minimum object emission"); + + assert_eq!(artifact.symbols.len(), 4); + object::File::parse(artifact.bytes.as_slice()).expect("parse signed minimum object"); +} + +#[test] +fn rejects_statically_zero_integer_divisors() { + for operator in ["/", "modulo"] { + let source = format!( + "namespace app\nfunction invalid(value: int64) returns int64:\n return value {operator} 1\n" + ); + let (mut program, types) = lower_source(&source); + let function = &mut program.functions[0]; + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(expression)) = &mut function.blocks[entry].terminator.kind + else { + panic!("expected returned expression"); + }; + let jett_hir::ExpressionKind::Binary { right, .. } = &mut expression.kind else { + panic!("expected binary expression"); + }; + right.kind = jett_hir::ExpressionKind::Int(0); + + let error = emit_host_object(&program, &types) + .expect_err("a statically zero integer divisor must be rejected"); + + assert!(matches!( + error, + CodegenError::InvalidMirContract { message, .. } + if message.contains("statically zero divisor") + )); + } +} + +#[test] +fn rejects_an_explicit_non_host_target_before_object_generation() { + let program = Program { + resource_manifest: jett_hir::ResourceManifest::empty(), + functions: Vec::new(), + equality_methods: HashMap::new(), + }; + let types = TypeInterner::new(); + + let error = emit_object_for_target(&program, &types, "wasm32-unknown-unknown") + .expect_err("cross-target emission must be rejected"); + + assert!(matches!( + error, + CodegenError::UnsupportedTarget { requested, supported } + if requested == "wasm32-unknown-unknown" && supported == host_target().to_string() + )); +} + +#[test] +fn rejects_malformed_target_text() { + let program = Program { + resource_manifest: jett_hir::ResourceManifest::empty(), + functions: Vec::new(), + equality_methods: HashMap::new(), + }; + let types = TypeInterner::new(); + + let error = emit_object_for_target(&program, &types, "not a target") + .expect_err("malformed target must be rejected"); + + assert!( + matches!(error, CodegenError::InvalidTarget { target, .. } if target == "not a target") + ); +} + +#[test] +fn symbols_use_structural_types_instead_of_session_local_type_ids() { + let mut first_types = TypeInterner::new(); + let first_argument = first_types.intern(Type::List(TypeInterner::INT64)); + let mut second_types = TypeInterner::new(); + second_types.intern(Type::Optional(TypeInterner::BOOL)); + let second_argument = second_types.intern(Type::List(TypeInterner::INT64)); + assert_ne!(first_argument, second_argument); + + let declaration = DeclarationId { + origin: SourceOrigin::Dependency("example.math".to_string()), + namespace: "numbers".to_string(), + name: "identity".to_string(), + kind: DeclarationKind::Function, + }; + let first = FunctionIdentity { + declaration: declaration.clone(), + scoped_type_bindings: Vec::new(), + type_arguments: vec![first_argument], + specialization: CheckedGenericSpecialization::default(), + }; + let second = FunctionIdentity { + declaration, + scoped_type_bindings: Vec::new(), + type_arguments: vec![second_argument], + specialization: CheckedGenericSpecialization::default(), + }; + + let first_symbol = symbol_name(&first, &first_types).expect("first symbol"); + let second_symbol = symbol_name(&second, &second_types).expect("second symbol"); + assert_eq!(first_symbol, second_symbol); + assert_eq!( + first_symbol, + "jett_v0_e2a99a2b07c19f4355c2c76e50f4ff613045299da68e13af7a842a035e453591" + ); + + let mut first_scoped = first.clone(); + first_scoped.scoped_type_bindings = vec![jett_hir::ScopedTypeBinding { + name: "Field".into(), + ty: first_argument, + reflection: jett_types::ReflectionTypeInfo::new( + "list[int64]", + "list", + None, + false, + Vec::new(), + ), + }]; + let mut second_scoped = second.clone(); + second_scoped.scoped_type_bindings = vec![jett_hir::ScopedTypeBinding { + ty: second_argument, + ..first_scoped.scoped_type_bindings[0].clone() + }]; + assert_eq!( + symbol_name(&first_scoped, &first_types).unwrap(), + symbol_name(&second_scoped, &second_types).unwrap(), + ); + assert_ne!( + symbol_name(&first_scoped, &first_types).unwrap(), + first_symbol + ); + let mut aliased = first_scoped.clone(); + aliased.scoped_type_bindings[0].reflection = jett_types::ReflectionTypeInfo::new( + "Items", + "alias", + None, + false, + vec![first_scoped.scoped_type_bindings[0].reflection.clone()], + ); + assert_ne!( + symbol_name(&first_scoped, &first_types).unwrap(), + symbol_name(&aliased, &first_types).unwrap() + ); + second_scoped.scoped_type_bindings[0].ty = TypeInterner::STRING; + assert_ne!( + symbol_name(&first_scoped, &first_types).unwrap(), + symbol_name(&second_scoped, &second_types).unwrap(), + ); + + let specialized = FunctionIdentity { + declaration: second.declaration.clone(), + scoped_type_bindings: Vec::new(), + type_arguments: second.type_arguments.clone(), + specialization: CheckedGenericSpecialization { + type_argument_kinds: vec!["alias".to_string()], + ..CheckedGenericSpecialization::default() + }, + }; + assert_ne!( + symbol_name(&second, &second_types).expect("unspecialized symbol"), + symbol_name(&specialized, &second_types).expect("specialized symbol") + ); +} + +#[test] +fn emits_custom_assert_message() { + let (mut program, types) = lower_source( + r#"namespace app +function checked(value: bool) returns bool: + bool copy = value + return value +"#, + ); + let statement = &mut program.functions[0].blocks[0].statements[0]; + let condition = match &statement.kind { + jett_mir::StatementKind::Let { value, .. } => value.clone(), + other => panic!("expected lowered let, got {other:?}"), + }; + statement.kind = jett_mir::StatementKind::Assert { + message: Some(jett_hir::Expression { + kind: jett_hir::ExpressionKind::String("detail".to_owned()), + ty: TypeInterner::STRING, + span: condition.span, + }), + condition, + }; + + let object = emit_host_object(&program, &types).expect("custom assert message emits"); + assert!(!object.bytes.is_empty()); +} + +#[test] +fn rejects_reflected_type_dispatch_without_type_info() { + let (mut program, types) = lower_source( + r#"namespace app +function choose(value: bool) returns int64: + if value: + return 1 + else: + return 2 +"#, + ); + let function = &mut program.functions[0]; + let entry = function.entry.index() as usize; + let (type_info, target, otherwise) = match function.blocks[entry].terminator.kind.clone() { + TerminatorKind::Branch { + condition, + then_block, + else_block, + } => (condition, then_block, else_block), + other => panic!("expected lowered branch, got {other:?}"), + }; + function.blocks[entry].terminator.kind = TerminatorKind::ReflectedTypeDispatch { + type_info, + arms: vec![ReflectedTypeDispatchArm { + iteration_index: 0, + bound_type: TypeInterner::BOOL, + reflection_identity: "4:bool0:0:0".to_string(), + target, + }], + otherwise, + }; + + let error = emit_host_object(&program, &types) + .expect_err("reflected dispatch must select a checked TypeInfo"); + + assert!(matches!( + error, + CodegenError::InvalidMirContract { message, .. } + if message == "invalid checked reflected type dispatch" + )); +} + +#[test] +fn rejects_invalid_mir_before_object_generation() { + let (mut program, types) = lower_source( + r#"namespace app +function callee() returns int64: + return 1 +function caller() returns int64: + return callee() +"#, + ); + program.functions.remove(0); + + let error = emit_host_object(&program, &types).expect_err("invalid MIR must be rejected"); + + assert!(matches!(error, CodegenError::InvalidMir(_))); +} + +#[test] +fn rejects_unbaked_comptime_instead_of_executing_it_at_runtime() { + let (program, types) = lower_source("function main() returns int64:\n return comptime 42\n"); + let error = emit_host_object(&program, &types) + .expect_err("unbaked comptime must not become runtime code"); + assert!(error.to_string().contains("unbaked comptime"), "{error}"); +} + +#[test] +fn rejects_unbaked_namespace_constants_before_object_generation() { + let (program, types) = lower_source( + "namespace app\nint64 answer = 42\nfunction main() returns int64:\n return answer\n", + ); + let error = emit_host_object(&program, &types) + .expect_err("constant reads require the compiler-produced value table"); + assert!( + error.to_string().contains("unbaked namespace constant"), + "{error}" + ); +} + +#[test] +fn rejects_secret_binary_results_and_wrappers_that_remove_taint() { + for expression in ["hidden == plain", "clone hidden"] { + let source = format!( + "namespace app\nfunction compare(hidden: secret[bool], plain: bool) returns secret[bool]:\n return {expression}\n" + ); + let (mut program, types) = lower_source(&source); + emit_host_object(&program, &types).expect("checked secret binary or wrapper"); + let function = &mut program.functions[0]; + function.return_type = TypeInterner::BOOL; + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(value)) = &mut function.blocks[entry].terminator.kind + else { + panic!("expected returned expression"); + }; + value.ty = TypeInterner::BOOL; + let error = emit_host_object(&program, &types) + .expect_err("a compiler handoff cannot silently declassify"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("secret result") || message.contains("wrapper")), + "{expression}: {error:?}" + ); + } +} + +#[test] +fn wrapped_enum_equality_rejects_missing_exact_payload_methods() { + let (mut program, types) = lower_source_with_equatable( + "namespace app\nstruct Item:\n id: int64\nimplement Equatable for Item:\n function equals(view self: Item, view other: Item) returns bool:\n return self.id == other.id\nenum Choice:\n item(value: Item)\nfunction compare(view hidden: secret[Choice], view plain: Choice) returns secret[bool]:\n return hidden == plain\n", + true, + ); + let &method = program.equality_methods.values().next().unwrap(); + program.functions[method.index() as usize] + .identity + .declaration + .origin = SourceOrigin::Stdlib; + let symbol = symbol_name(&program.functions[method.index() as usize].identity, &types).unwrap(); + assert!( + emit_host_object(&program, &types) + .expect("checked wrapped equality") + .symbols + .contains(&symbol), + "wrapped enum payload methods remain reachable" + ); + program.equality_methods.clear(); + let error = emit_host_object(&program, &types) + .expect_err("secrecy must not bypass explicit payload equality"); + assert!( + error.to_string().contains("enum equality payload type"), + "{error}" + ); +} + +#[test] +fn emits_exact_enum_payload_method_and_rejects_broken_handoff() { + let (mut program, types) = lower_source_with_equatable( + r#"namespace app +struct Item: + id: int64 +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return self.id == other.id +enum Value: + empty + item(value: Item) +function main() returns bool: + Value left = Value.item(Item(id: 1)) + Value right = Value.item(Item(id: 1)) + return left == right +"#, + true, + ); + let (&owner, &method) = program + .equality_methods + .iter() + .next() + .expect("checked exact target"); + assert_eq!(program.equality_methods.len(), 1); + program.functions[method.index() as usize] + .identity + .declaration + .origin = SourceOrigin::Stdlib; + let expected_symbol = + symbol_name(&program.functions[method.index() as usize].identity, &types).unwrap(); + let first = emit_host_object(&program, &types).expect("checked enum method emission"); + assert!( + first.symbols.contains(&expected_symbol), + "implicitly called stdlib methods remain reachable" + ); + assert_eq!(first, emit_host_object(&program, &types).unwrap()); + for broken in 0..3 { + let mut invalid = program.clone(); + match broken { + 0 => { + invalid.functions[method.index() as usize].params[0].mode = + jett_mir::ParamMode::Owned + } + 1 => invalid.functions[method.index() as usize].return_type = TypeInterner::INT64, + _ => { + invalid.equality_methods.insert(TypeInterner::BOOL, method); + } + } + assert!( + emit_host_object(&invalid, &types).is_err(), + "invalid handoff {broken} must fail" + ); + } + program.equality_methods.remove(&owner); + let error = emit_host_object(&program, &types) + .expect_err("nested user structs must not gain structural equality"); + assert!( + error.to_string().contains("enum equality payload type"), + "{error}" + ); +} + +#[test] +fn malformed_sequence_element_type_returns_error_without_panicking() { + for by_view in [true, false] { + let source = format!( + "function length(items: list[int64]) returns int64:\n for item in {}items:\n return item\n return 0\n", + if by_view { "view " } else { "" } + ); + let (mut program, mut types) = lower_source(&source); + emit_host_object(&program, &types).expect("valid baseline"); + let mut foreign = TypeInterner::new(); + let mut inner = TypeInterner::INT64; + for _ in 0..1000 { + inner = foreign.intern(jett_types::Type::List(inner)); + } + assert!(inner.index() as usize > types.len()); + let invalid = types.intern(jett_types::Type::List(inner)); + for function in &mut program.functions { + for block in &mut function.blocks { + if let jett_mir::TerminatorKind::ForEach { iterable, .. } = + &mut block.terminator.kind + { + iterable.ty = invalid; + } + } + } + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + emit_host_object(&program, &types) + })); + assert!( + matches!(result, Ok(Err(_))), + "must reject malformed element without panic: {result:?}" + ); + } +} + +#[test] +fn secret_field_reads_preserve_exact_qualification_and_nominal_contracts() { + use jett_hir::{ExpressionKind, FieldId}; + let (baseline, mut types) = lower_source( + r#"namespace app +type Hidden = secret[int64] where true +struct Item: + number: int64 + text: string + hidden: secret[int64] + nominal: Hidden + unit: nothing +struct Other: + number: int64 +function read(view item: secret[Item]) returns secret[int64]: + return item.number +function text(view item: secret[Item]) returns secret[string]: + return item.text +function already(view item: secret[Item]) returns secret[int64]: + return item.hidden +function nominal(view item: secret[Item]) returns Hidden: + return item.nominal +function unit(view item: secret[Item]) returns nothing: + return item.unit +function public_read(view item: Item) returns int64: + return item.number +"#, + ); + emit_host_object(&baseline, &types).expect("exact source-qualified field reads"); + let secret_int = types.intern(Type::Secret(TypeInterner::INT64)); + let twice_secret_int = types.intern(Type::Secret(secret_int)); + let secret_nothing = types.intern(Type::Secret(TypeInterner::NOTHING)); + let other = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Struct(id) + if types.resolve_struct(*id).name == "app.Other") + }) + .unwrap(); + for (name, corruption) in [ + ("read", "declassify"), + ("public_read", "classify"), + ("already", "double"), + ("nominal", "erase nominal"), + ("unit", "qualify nothing"), + ("read", "different owner"), + ("read", "invalid index"), + ("read", "nested owner"), + ] { + let mut program = baseline.clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + if corruption == "nested owner" { + let nested = types.intern(Type::Secret(function.params[0].ty)); + function.params[0].ty = nested; + function.locals[function.params[0].local.index() as usize].ty = nested; + } + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("field return"); + }; + let ExpressionKind::Field { + base, + owner_type, + field, + } = &mut value.kind + else { + panic!("nominal field read"); + }; + match corruption { + "declassify" => value.ty = TypeInterner::INT64, + "classify" | "erase nominal" => value.ty = secret_int, + "double" => value.ty = twice_secret_int, + "qualify nothing" => value.ty = secret_nothing, + "different owner" => *owner_type = other, + "invalid index" => *field = FieldId::new(99), + "nested owner" => base.ty = function.params[0].ty, + _ => unreachable!(), + } + function.return_type = value.ty; + let error = emit_host_object(&program, &types).expect_err("invalid field metadata"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("field")), + "{corruption}: {error:?}" + ); + } +} + +#[test] +fn qualified_struct_constructors_keep_strict_nominal_contracts() { + use jett_hir::ExpressionKind; + let (baseline, types) = lower_source( + "namespace app\nstruct Item:\n number: int64\nstruct Other:\n number: int64\nfunction make() returns secret[Item]:\n return Item(number: 7)\n", + ); + emit_host_object(&baseline, &types).expect("qualified nominal struct construction"); + let other = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Struct(id) + if types.resolve_struct(*id).name == "app.Other") + }) + .unwrap(); + for corruption in 0..4 { + let mut program = baseline.clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "make") + .unwrap(); + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("qualified constructor return"); + }; + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &mut value.kind else { + panic!("separate checked secret qualification"); + }; + if corruption == 0 { + value.kind = inner.kind.clone(); + } else { + let ExpressionKind::StructConstruct { + struct_type, + validates_refinements, + .. + } = &mut inner.kind + else { + panic!("exact nominal constructor"); + }; + match corruption { + 1 => inner.ty = value.ty, + 2 => *struct_type = other, + 3 => *validates_refinements = true, + _ => unreachable!(), + } + } + let error = emit_host_object(&program, &types).expect_err("invalid nominal constructor"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("struct")), + "corruption {corruption}: {error:?}" + ); + } +} + +#[test] +fn qualified_validating_struct_constructors_keep_exact_result_contracts() { + use jett_hir::ExpressionKind; + let (baseline, mut types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +struct Item: + number: Positive +struct Other: + number: Positive +function make(value: Positive) returns secret[result[Item, string]]: + return Item(number: value) +"#, + ); + emit_host_object(&baseline, &types) + .expect("qualified validating struct without new predicates"); + let item = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Struct(id) + if types.resolve_struct(*id).name == "app.Item") + }) + .unwrap(); + let other = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Struct(id) + if types.resolve_struct(*id).name == "app.Other") + }) + .unwrap(); + let bad_error = types.intern(Type::Result(item, TypeInterner::BOOL)); + let bad_success = types.intern(Type::Result(other, TypeInterner::STRING)); + for corruption in 0..7 { + let mut program = baseline.clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "make") + .unwrap(); + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("validating constructor return"); + }; + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &mut value.kind else { + panic!("result qualification boundary"); + }; + if corruption == 0 { + value.kind = inner.kind.clone(); + } else { + let ExpressionKind::StructConstruct { + struct_type, + validates_refinements, + .. + } = &mut inner.kind + else { + panic!("empty predicate chain remains a validating constructor"); + }; + match corruption { + 1 => inner.ty = value.ty, + 2 => *struct_type = other, + 3 => *validates_refinements = false, + 4 => inner.ty = bad_error, + 5 => inner.ty = bad_success, + 6 => inner.ty = item, + _ => unreachable!(), + } + } + let error = + emit_host_object(&program, &types).expect_err("invalid validating struct result"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("struct")), + "corruption {corruption}: {error:?}" + ); + } +} + +#[test] +fn qualified_bitfield_and_machine_constructors_keep_exact_contracts() { + use jett_hir::{Expression, ExpressionKind, StateId}; + let (baseline, mut types) = lower_source( + r#"namespace app +bitfield Header: + value: 4 bits +bitfield Other: + value: 4 bits +machine Session: + states: + active(label: string, count: int8) + closed + transitions: + active to closed +machine Another: + states: + active(label: string, count: int8) + closed + transitions: + active to closed +function plain() returns secret[Header]: + return Header(value: 7) +function checked(value: int64) returns secret[result[Header, string]]: + return Header(value: value) +function state() returns secret[Session at active]: + return Session(active, "value", 7) +function another() returns Another at active: + return Another(active, "value", 7) +"#, + ); + emit_host_object(&baseline, &types).expect("exact qualified bitfield and machine constructors"); + let header = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Bitfield(id) + if types.resolve_bitfield(*id).name == "app.Header") + }) + .unwrap(); + let other = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Bitfield(id) + if types.resolve_bitfield(*id).name == "app.Other") + }) + .unwrap(); + let another_state = types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::MachineState { machine, .. } + if types.resolve_machine(*machine).name == "app.Another") + }) + .unwrap(); + let bad_error = types.intern(Type::Result(header, TypeInterner::BOOL)); + let bad_success = types.intern(Type::Result(other, TypeInterner::STRING)); + let qualified_header = types.intern(Type::Secret(header)); + for (name, corruption) in [ + ("plain", "raw qualified constructor"), + ("plain", "qualified inner"), + ("plain", "different bitfield"), + ("plain", "flipped validation"), + ("plain", "wrong field type"), + ("checked", "raw qualified constructor"), + ("checked", "qualified inner"), + ("checked", "different bitfield"), + ("checked", "flipped validation"), + ("checked", "wrong result error"), + ("checked", "wrong result success"), + ("checked", "erased result"), + ("checked", "qualified bitfield target"), + ("state", "raw qualified constructor"), + ("state", "qualified inner"), + ("state", "different state owner"), + ("state", "wrong state index"), + ("state", "missing payload"), + ("state", "swapped payloads"), + ] { + let mut program = baseline.clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("constructor return"); + }; + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &mut value.kind else { + panic!("separate qualification coercion"); + }; + if corruption == "raw qualified constructor" { + value.kind = inner.kind.clone(); + } else if corruption == "qualified inner" { + inner.ty = value.ty; + } else { + match &mut inner.kind { + ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + validates_widths, + .. + } => match corruption { + "different bitfield" => *bitfield_type = other, + "flipped validation" => *validates_widths = !*validates_widths, + "wrong field type" => { + fields[0] = Expression { + kind: ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span: fields[0].span, + }; + } + "wrong result error" => inner.ty = bad_error, + "wrong result success" => inner.ty = bad_success, + "erased result" => inner.ty = header, + "qualified bitfield target" => *bitfield_type = qualified_header, + _ => unreachable!(), + }, + ExpressionKind::MachineConstruct { + state_type, + state, + payloads, + } => match corruption { + "different state owner" => *state_type = another_state, + "wrong state index" => *state = StateId::new(state.index() + 1), + "missing payload" => { + payloads.pop(); + } + "swapped payloads" => payloads.swap(0, 1), + _ => unreachable!(), + }, + _ => panic!("exact inner constructor"), + } + } + let error = emit_host_object(&program, &types).expect_err("invalid constructor metadata"); + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } + if message.contains("bitfield") || message.contains("machine")), + "{name}/{corruption}: {error:?}" + ); + } +} + +#[test] +fn struct_layout_and_projection_contracts_are_validated_before_emission() { + use jett_hir::ExpressionKind; + let (baseline, types) = lower_source( + r#" +struct Pair: + number: int64 + text: string +function make() returns Pair: + return Pair(number: 7, text: "value") +function read(view pair: Pair) returns string: + return pair.text +"#, + ); + emit_host_object(&baseline, &types).expect("valid struct layout"); + for corruption in 0..6 { + let mut program = baseline.clone(); + if corruption < 4 { + let TerminatorKind::Return(Some(value)) = + &mut program.functions[0].blocks[0].terminator.kind + else { + panic!("constructor return"); + }; + let ExpressionKind::StructConstruct { + fields, + struct_type, + evaluation_order, + validates_refinements, + .. + } = &mut value.kind + else { + panic!("constructor"); + }; + match corruption { + 0 => fields[0].ty = TypeInterner::BOOL, + 1 => *struct_type = TypeInterner::INT64, + 2 => evaluation_order.swap(0, 1), // valid reordering, but duplicate below + 3 => *validates_refinements = true, + _ => unreachable!(), + } + if corruption == 2 { + evaluation_order[1] = evaluation_order[0]; + } + } else { + let TerminatorKind::Return(Some(value)) = + &mut program.functions[1].blocks[0].terminator.kind + else { + panic!("projection return"); + }; + let ExpressionKind::Field { owner_type, .. } = &mut value.kind else { + panic!("field"); + }; + if corruption == 4 { + *owner_type = TypeInterner::INT64; + } else { + value.ty = TypeInterner::BOOL; + } + } + let rejection = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + emit_host_object(&program, &types) + })); + assert!( + matches!(rejection, Ok(Err(_))), + "corruption {corruption}: {rejection:?}" + ); + } + // Copy-only public plan must not become a back door for struct ownership. + assert!(jett_mir::copy_values::CopyValuePlan::analyze(&baseline.functions[0], &types).is_err()); +} + +#[test] +fn native_reflected_getter_pipelines_emit_checked_root_and_owned_reads() { + let (program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +struct Record: + raw: int64 + positive: Positive + higher: Higher +enum Event: + first(raw: int64, higher: Higher) + second(higher: Higher, raw: int64) +machine Session: + states: + first(raw: int64, higher: Higher) + second(higher: Higher, raw: int64) + transitions: + first to second +bitfield Header: + first: 4 bits + second: 8 bits +struct Parcel: + text: string + values: list[int64] +enum Packet: + first(text: string) + second(values: list[int64]) +machine Store: + states: + first(text: string) + second(values: list[int64]) + transitions: + first to second +function record_pipe(view source: Record, view field: TypeField) returns Higher: + return source into view type.field_value[Record, Higher](view field) +function event_pipe(view source: Event, view field: TypeField) returns Higher: + return source into view type.variant_field_value[Event, Higher](view field) +function session_pipe(view source: Session, view field: TypeField) returns Higher: + return source into view type.machine_field_value[Session, Higher](view field) +function narrowed_pipe(view source: Session at first, view field: TypeField) returns Higher: + return source into view type.machine_field_value[Session at first, Higher](view field) +function header_pipe(view source: Header, view field: TypeField) returns int64: + return source into view type.field_value[Header, int64](view field) +function parcel_text(view source: Parcel, view field: TypeField) returns string: + return source into view type.field_value[Parcel, string](view field) +function parcel_values(view source: Parcel, view field: TypeField) returns list[int64]: + return source into view type.field_value[Parcel, list[int64]](view field) +function packet_text(view source: Packet, view field: TypeField) returns string: + return source into view type.variant_field_value[Packet, string](view field) +function packet_values(view source: Packet, view field: TypeField) returns list[int64]: + return source into view type.variant_field_value[Packet, list[int64]](view field) +function store_text(view source: Store, view field: TypeField) returns string: + return source into view type.machine_field_value[Store, string](view field) +function store_values(view source: Store, view field: TypeField) returns list[int64]: + return source into view type.machine_field_value[Store, list[int64]](view field) +"#, + ); + let artifact = emit_host_object(&program, &types).expect("checked reflected getter pipelines"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let failure = jett_runtime::native_abi::values::NativeLeaf::RuntimeFailMessage.symbol(); + assert!(object.symbols().any(|s| s.name().ok() == Some(failure))); +} + +const REFLECTED_VALUE_PIPELINES: &str = r#"namespace app +struct Holder[T]: + value: T +type Positive = int64 where value > 0 +enum Event: + empty = 17 + content(text: string, values: list[optional[int64]]) +machine Session: + states: + empty + content(text: string, values: list[optional[int64]]) + transitions: + empty to content +function variant_pipe(view source: Event) returns TypeVariant: + TypeVariant selected = source into view type.variant_value[Event]() + return selected +function state_pipe(view source: Session) returns TypeMachineState: + TypeMachineState selected = source into view type.machine_state_value[Session]() + return selected +function narrowed_pipe(view source: Session at content) returns TypeMachineState: + TypeMachineState selected = source into view type.machine_state_value[Session at content]() + return selected +function map_pipe(index: int64) returns TypeInfo: + TypeInfo selected = index into type.arg[map[string, list[optional[int64]]]]() + return selected +function holder_pipe(index: int64) returns TypeInfo: + return index into type.arg[Holder[list[optional[int64]]]]() +function refinement_pipe(index: int64) returns TypeInfo: + return index into type.arg[Positive]() +function primitive_pipe(index: int64) returns TypeInfo: + return index into type.arg[int64]() +"#; + +#[test] +fn native_reflected_value_pipelines_emit_owned_metadata_and_existing_observation_guards() { + let (program, types) = lower_source(REFLECTED_VALUE_PIPELINES); + let artifact = emit_host_object(&program, &types).expect("checked reflected value pipelines"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + for leaf in [ + jett_runtime::native_abi::values::NativeLeaf::TypeArgCheckedIndex, + jett_runtime::native_abi::values::NativeLeaf::ReflectedOwnerPendingCheck, + jett_runtime::native_abi::values::NativeLeaf::StructClone, + ] { + assert!( + object + .symbols() + .any(|symbol| symbol.name().ok() == Some(leaf.symbol())), + "{} must retain its existing checked ownership/observation ABI", + leaf.symbol() + ); + } +} + +#[test] +fn native_reflected_value_pipelines_reject_missing_and_wrong_typed_metadata() { + for name in ["variant_pipe", "state_pipe", "narrowed_pipe", "map_pipe"] { + for remove in [true, false] { + let (mut program, types) = lower_source(REFLECTED_VALUE_PIPELINES); + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + jett_mir::StatementKind::Let { value, .. } + if matches!(value.kind, jett_hir::ExpressionKind::Intrinsic { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("lowered observer initializer"); + let jett_hir::ExpressionKind::Intrinsic { + args, + evaluation_order, + .. + } = &mut value.kind + else { + panic!("observer intrinsic"); + }; + assert!(args.len() > 1, "{name}: checked hidden metadata"); + if remove { + args.pop(); + evaluation_order.pop(); + } else { + let metadata = args.last_mut().unwrap(); + metadata.kind = jett_hir::ExpressionKind::Bool(false); + metadata.ty = TypeInterner::BOOL; + } + let rejection = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + emit_host_object(&program, &types) + })); + assert!( + matches!(rejection, Ok(Err(CodegenError::InvalidMir(ref errors))) + if errors.iter().any(|error| error.message == if remove { + "call ownership operand count is invalid" + } else { "generated operand has no checked actual/conversion type" })), + "{name}, remove={remove}: malformed metadata must be rejected without panicking: {rejection:?}" + ); + } + } +} + +const MACHINE_REBINDING: &str = r#"namespace app +machine Session: + states: + empty + content(values: list[int64]) + transitions: + empty to content +machine Other: + states: + empty + content(values: list[int64]) + transitions: + empty to content +function other() returns Other at empty: + return Other(empty) +function replace(values: list[int64]) returns Session: + mutable Session source = Session(empty) + source = Session(content, values) + source = Session(empty) + return source +function copied(view source: Session at content) returns Session: + mutable Session destination = Session(empty) + destination = clone source + return destination +function pending() returns Session: + mutable Session source = run Session(empty) + source = run run Session(content, list(7)) + return source +function same_guard() returns Session: + mutable Session source = Session(content, list(1)) + if source at content: + source = Session(content, list(2)) + return source +"#; + +#[test] +fn native_machine_rebinding_emits_whole_owner_storage_and_owned_cleanup() { + let (program, types) = lower_source(MACHINE_REBINDING); + for name in ["replace", "copied", "pending", "same_guard"] { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let local = function.locals.iter().find(|local| local.mutable).unwrap(); + assert!( + matches!(types.resolve(local.ty), Type::Machine(_)), + "{name}: bare annotation owns the whole machine" + ); + } + let artifact = emit_host_object(&program, &types).expect("checked whole-machine rebinding"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + let drop = jett_runtime::native_abi::values::NativeLeaf::DropValue.symbol(); + assert!( + object + .symbols() + .any(|symbol| symbol.name().ok() == Some(drop)), + "replaced owned payloads retain cleanup" + ); +} + +#[test] +fn native_machine_rebinding_rejects_foreign_owner_and_forged_exact_targets() { + for mutation in ["foreign_owner", "explicit_target", "guarded_target"] { + let (mut program, types) = lower_source(MACHINE_REBINDING); + let foreign = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "other") + .unwrap() + .return_type; + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "replace") + .unwrap(); + let initial = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + jett_mir::StatementKind::Let { local, value } + if matches!( + value.kind, + jett_hir::ExpressionKind::MachineConstruct { .. } + ) => + { + Some((*local, value.ty)) + } + _ => None, + }) + .unwrap(); + if mutation == "explicit_target" { + function.locals[initial.0.index() as usize].ty = initial.1; + } + let (target, value) = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + jett_mir::StatementKind::Assign { target, value } => Some((target, value)), + _ => None, + }) + .unwrap(); + if mutation == "foreign_owner" { + value.kind = jett_hir::ExpressionKind::MachineConstruct { + state_type: foreign, + state: jett_hir::StateId::new(0), + payloads: vec![], + }; + value.ty = foreign; + } else if mutation == "guarded_target" { + // The storage remains the full machine; only this checked target + // is exact, as it is inside a visible source-level state guard. + target.ty = initial.1; + } + let error = emit_host_object(&program, &types).expect_err("forged machine assignment"); + let expected = if mutation == "guarded_target" { + "exact machine target" + } else { + "match its local" + }; + assert!( + matches!(error, CodegenError::InvalidMirContract { ref message, .. } if message.contains(expected)), + "{mutation}: {error:?}" + ); + } +} + +const RECURSIVE_REFLECTED_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Record: + values: list[optional[result[map[int64, list[int64]], set[int64]]]] +function inspect(view source: Record, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return source into view type.field_value[Record, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) +"#; + +#[test] +fn native_recursive_reflected_producers_emit_existing_projection_and_failure_leaves() { + let (program, types) = lower_source(RECURSIVE_REFLECTED_SOURCE); + let artifact = emit_host_object(&program, &types).expect("recursive reflected checked CFG"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + for leaf in [ + jett_runtime::native_abi::values::NativeLeaf::RejectPendingHandle, + jett_runtime::native_abi::values::NativeLeaf::ListElementClone, + jett_runtime::native_abi::values::NativeLeaf::SetElementClone, + jett_runtime::native_abi::values::NativeLeaf::MapKeyClone, + jett_runtime::native_abi::values::NativeLeaf::MapValueClone, + jett_runtime::native_abi::values::NativeLeaf::SumClone, + jett_runtime::native_abi::values::NativeLeaf::SumTake, + jett_runtime::native_abi::values::NativeLeaf::RuntimeFailMessage, + ] { + assert!( + object + .symbols() + .any(|symbol| symbol.name().ok() == Some(leaf.symbol())), + "{} must use the existing native ABI", + leaf.symbol() + ); + } +} + +#[test] +fn native_recursive_reflected_readiness_rejects_wrong_kind_and_missing_source() { + for corruption in [0, 1, 2] { + let (mut program, types) = lower_source(RECURSIVE_REFLECTED_SOURCE); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let scalar = function + .locals + .iter() + .find(|local| local.ty == TypeInterner::BOOL) + .unwrap() + .id; + let statement = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!( + statement.kind, + jett_mir::StatementKind::ReflectedContainerReady { .. } + ) + }) + .expect("typed reflected readiness"); + let jett_mir::StatementKind::ReflectedContainerReady { source, kind } = &mut statement.kind + else { + unreachable!() + }; + match corruption { + 0 => *kind = jett_mir::ReflectedContainerKind::Result, + 1 => *source = jett_hir::LocalId::new(u32::MAX), + 2 => *source = scalar, + _ => unreachable!(), + } + let rejection = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + emit_host_object(&program, &types) + })); + let expected = if corruption == 1 { + matches!(rejection, Ok(Err(CodegenError::InvalidMir(_)))) + } else { + matches!(rejection, Ok(Err(CodegenError::InvalidMirContract { .. }))) + }; + assert!( + expected, + "readiness corruption {corruption} must be rejected without panicking: {rejection:?}" + ); + } +} + +#[test] +fn native_recursive_reflected_new_root_keeps_original_declared_base_preflight() { + let (program, types) = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +type Values = list[Positive] where true +type Independent = list[Positive] where true +struct Record: + values: Values +function inspect(view source: Record, view field: TypeField) returns Independent: + return source into view type.field_value[Record, Independent](view field) +"#, + ); + let artifact = emit_host_object(&program, &types).expect("original declared root base CFG"); + let object = object::File::parse(artifact.bytes.as_slice()).unwrap(); + assert!(object.symbols().any(|symbol| { + symbol.name().ok() + == Some(jett_runtime::native_abi::values::NativeLeaf::RejectPendingHandle.symbol()) + })); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let checks: Vec<_> = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + jett_mir::StatementKind::CheckRefinement { type_name, .. } => Some(type_name.as_str()), + _ => None, + }) + .collect(); + assert_eq!(checks, ["app.Independent"]); +} + +#[test] +fn actor_identity_emits_a_checked_leaf_and_nested_layout_without_structural_state_comparison() { + for source in [ + include_str!("../../../tests/native/actor_enum_equality.jett"), + include_str!("../../../tests/native/actor_pending_alias_baseline.jett"), + include_str!("../../../tests/native/actor_pending_alias_identity.jett"), + include_str!("../../../tests/native/actor_direct_identity.jett"), + include_str!("../../../tests/native/actor_nested_identity.jett"), + ] { + let (program, types) = lower_source_with_equatable(source, true); + emit_host_object(&program, &types).expect("selected checked actor identity"); + } +} + +#[test] +fn actor_identity_rejects_a_different_nominal_mir_operand() { + let source = "namespace app\nactor First:\n receive read responds int64:\n respond 1\nactor Second:\n receive read responds int64:\n respond 1\nfunction compare(view first: First, view second: First) returns bool:\n return first == second\n"; + let (mut program, types) = lower_source(source); + emit_host_object(&program, &types).expect("same checked nominal actor type"); + let wrong_actor = types.type_ids() + .find(|id| matches!(types.resolve(*id), Type::Actor(actor) if types.resolve_actor(*actor).name == "app.Second")) + .expect("second nominal actor type"); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name.ends_with("compare")) + .unwrap(); + let entry = function.entry.index() as usize; + let TerminatorKind::Return(Some(value)) = &mut function.blocks[entry].terminator.kind else { + panic!("returned comparison"); + }; + let jett_hir::ExpressionKind::Binary { right, .. } = &mut value.kind else { + panic!("binary comparison"); + }; + right.ty = wrong_actor; + let error = + emit_host_object(&program, &types).expect_err("exact nominal actor handoff is required"); + assert!( + matches!(error, CodegenError::InvalidMirContract { .. }), + "{error:?}" + ); +} + +#[test] +fn actor_identity_frontend_rejects_different_nominal_owners() { + let parsed = jett_parser::parse( + "namespace app\nactor First:\n receive read responds int64:\n respond 1\nactor Second:\n receive read responds int64:\n respond 1\nfunction compare(view first: First, view second: Second) returns bool:\n return first == second\n", + FileId::new(0), + ); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked.diagnostics.iter().any(|diagnostic| { + diagnostic.severity == jett_diagnostics::Severity::Error + && diagnostic.code.code() == 301 + }), + "different actor declarations cannot share an equality operand type" + ); +} diff --git a/crates/jett_comptime/Cargo.toml b/crates/jett_comptime/Cargo.toml index 0792580b..00fb5353 100644 --- a/crates/jett_comptime/Cargo.toml +++ b/crates/jett_comptime/Cargo.toml @@ -8,12 +8,12 @@ license.workspace = true [dependencies] jett_common.workspace = true jett_diagnostics.workspace = true +jett_intrinsics.workspace = true jett_parser.workspace = true jett_runtime.workspace = true +jett_resolve.workspace = true +jett_typecheck.workspace = true jett_types.workspace = true rand.workspace = true subtle.workspace = true unicode-segmentation.workspace = true - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.61.2", features = ["Win32_Globalization"] } diff --git a/crates/jett_comptime/src/checked_types.rs b/crates/jett_comptime/src/checked_types.rs new file mode 100644 index 00000000..f81b145f --- /dev/null +++ b/crates/jett_comptime/src/checked_types.rs @@ -0,0 +1,405 @@ +//! Checked expression types retained separately for each generic body. +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_types::ReflectionTypeInfo; + +use crate::value::{ClosureTypeArgument, Value}; + +#[derive(Debug, Clone, Default)] +pub struct CheckedExpressionTypes { + /// Exact compiler session. Ordinary type-name projections confer no hook authority. + pub resource_program: Option>, + pub bindings: CheckedScopedBindings, + pub expressions: HashMap, + pub functions: HashMap>>, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CheckedFunctionTypes { + pub bindings: CheckedScopedBindings, + pub type_arguments: Vec, + pub type_argument_reflections: Vec, + pub type_info_kinds: Vec<(usize, String)>, + pub type_info_primitives: Vec<(usize, Option)>, + pub type_kind_values: Vec<(usize, String)>, + pub type_primitive_values: Vec<(usize, String)>, + pub expressions: Arc>, +} + +/// Recursive checked facts for one concrete lexical type binding. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CheckedScopedTypes { + pub bound_type: String, + pub reflection: Option, + pub expressions: HashMap, + pub bindings: CheckedScopedBindings, +} + +pub type CheckedScopedBindings = HashMap>>; + +pub(crate) fn select_scoped_types( + bindings: &CheckedScopedBindings, + span: Span, + bound_type: &str, + reflection: Option<&ReflectionTypeInfo>, +) -> Result>, String> { + let Some(candidates) = bindings.get(&span) else { + return Ok(None); + }; + let mut matching = candidates.iter().filter(|types| { + types.bound_type == bound_type + && types + .reflection + .as_ref() + .is_none_or(|expected| Some(expected) == reflection) + }); + let Some(selected) = matching.next() else { + return Ok(None); + }; + if matching.any(|other| other != selected) { + return Err(format!( + "scoped type `{bound_type}` has conflicting checked expression types" + )); + } + Ok(Some(selected.clone())) +} + +impl CheckedExpressionTypes { + /// Canonical mutable projections confer no execution or cache provenance. + pub fn from_checked_program(program: &Arc) -> Self { + Self::from_original_program(program) + } + + /// Internal canonical projection for a worker that owns this checked Source. + pub(crate) fn from_original_program( + program: &Arc, + ) -> Self { + let checked = program.checked(); + let (mut expressions, bindings) = original_body_types( + &checked.type_map, + &checked.comptime_type_bindings, + &checked.interner, + ); + let mut functions: HashMap>> = HashMap::new(); + for instance in &checked.generic_function_instantiations { + for span in instance.type_map.keys() { + expressions.remove(span); + } + remove_original_scoped_types(&mut expressions, &instance.comptime_type_bindings); + let (names, scopes) = original_body_types( + &instance.type_map, + &instance.comptime_type_bindings, + &checked.interner, + ); + let facts = &instance.specialization; + functions + .entry(program.resolved().scope_table.def(instance.definition).span) + .or_default() + .push(Arc::new(CheckedFunctionTypes { + bindings: scopes, + type_arguments: instance + .concrete_args + .iter() + .map(|ty| checked.interner.type_name(*ty)) + .collect(), + type_argument_reflections: facts.type_argument_reflections.clone(), + type_info_kinds: facts.type_info_kinds.clone(), + type_info_primitives: facts.type_info_primitives.clone(), + type_kind_values: facts.type_kind_values.clone(), + type_primitive_values: facts.type_primitive_values.clone(), + expressions: Arc::new(names), + })); + } + Self { + resource_program: Some(program.clone()), + bindings, + expressions, + functions, + } + } + + pub(crate) fn matches_original_program( + &self, + program: &Arc, + ) -> bool { + let expected = Self::from_original_program(program); + self.resource_program + .as_ref() + .is_some_and(|current| Arc::ptr_eq(current, program)) + && self.expressions == expected.expressions + && self.bindings == expected.bindings + && self.functions == expected.functions + } + + pub fn get(&self, span: &Span) -> Option<&String> { + self.expressions.get(span) + } + + pub(crate) fn select( + &self, + declaration: Span, + types: &[ClosureTypeArgument], + args: &[Value], + ) -> Result>, String> { + let Some(candidates) = self.functions.get(&declaration) else { + return Ok(None); + }; + let matching = candidates + .iter() + .filter(|candidate| candidate.matches(types, args)); + let Some(specificity) = matching + .clone() + .map(|candidate| candidate.specificity()) + .max() + else { + // Compiler-owned facades can execute interpreter source bodies + // whose checked call uses a builtin or a different native helper. + // Their source specialization is absent, just as for an unchecked + // standalone interpreter. Never borrow another instance's map. + return Ok(None); + }; + let mut matching = matching.filter(|candidate| candidate.specificity() == specificity); + let selected = matching + .next() + .expect("matching candidate has maximum specificity"); + if matching.any(|other| { + other.expressions != selected.expressions || other.bindings != selected.bindings + }) { + return Err("generic invocation has conflicting checked expression types".into()); + } + Ok(Some(selected.clone())) + } +} + +pub(crate) fn original_body_types( + types: &HashMap, + bindings: &HashMap>, + interner: &jett_types::TypeInterner, +) -> (HashMap, CheckedScopedBindings) { + let mut expressions = types + .iter() + .map(|(span, ty)| (*span, interner.type_name(*ty))) + .collect(); + remove_original_scoped_types(&mut expressions, bindings); + let nested = bindings + .values() + .flatten() + .flat_map(|binding| binding.body.comptime_type_bindings.keys().copied()) + .collect::>(); + let scopes = bindings + .iter() + .filter(|(span, _)| !nested.contains(span)) + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|binding| { + let (expressions, bindings) = original_body_types( + &binding.body.type_map, + &binding.body.comptime_type_bindings, + interner, + ); + Arc::new(CheckedScopedTypes { + bound_type: interner.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) + }) + .collect(), + ) + }) + .collect(); + (expressions, scopes) +} + +fn remove_original_scoped_types( + expressions: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + expressions.remove(span); + } + remove_original_scoped_types(expressions, &binding.body.comptime_type_bindings); + } +} + +impl CheckedFunctionTypes { + fn specificity(&self) -> usize { + self.type_info_kinds.len() + + self.type_info_primitives.len() + + self.type_kind_values.len() + + self.type_primitive_values.len() + } + + fn matches(&self, types: &[ClosureTypeArgument], args: &[Value]) -> bool { + self.type_arguments.len() == types.len() + && self + .type_arguments + .iter() + .zip(types) + .all(|(expected, actual)| expected == &actual.canonical_name) + && (self.type_argument_reflections.is_empty() + || (self.type_argument_reflections.len() == types.len() + && self + .type_argument_reflections + .iter() + .zip(types) + .all(|(expected, actual)| actual.reflection.as_ref() == Some(expected)))) + && self.type_info_kinds.iter().all(|(index, expected)| { + args.get(*index) + .and_then(|value| field(value, "kind_tag")) + .and_then(|value| variant(value, "TypeKind")) + == Some(expected.as_str()) + }) + && self.type_info_primitives.iter().all(|(index, expected)| { + let actual = args + .get(*index) + .and_then(|value| field(value, "primitive_tag")); + match (actual, expected) { + (Some(Value::OptionalNone), None) => true, + (Some(Value::OptionalSome(value)), Some(expected)) => { + variant(value, "TypePrimitive") == Some(expected.as_str()) + } + _ => false, + } + }) + && self.type_kind_values.iter().all(|(index, expected)| { + args.get(*index) + .and_then(|value| variant(value, "TypeKind")) + == Some(expected.as_str()) + }) + && self.type_primitive_values.iter().all(|(index, expected)| { + args.get(*index) + .and_then(|value| variant(value, "TypePrimitive")) + == Some(expected.as_str()) + }) + } +} + +fn field<'a>(value: &'a Value, name: &str) -> Option<&'a Value> { + let Value::Struct { + type_name, fields, .. + } = value + else { + return None; + }; + (type_name == "TypeInfo") + .then(|| fields.iter().find(|(field, _)| field == name))? + .map(|(_, value)| value) +} + +fn variant<'a>(value: &'a Value, expected_type: &str) -> Option<&'a str> { + let Value::Enum { + type_name, variant, .. + } = value + else { + return None; + }; + (type_name == expected_type).then_some(variant.as_str()) +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::FileId; + + #[test] + fn scoped_alias_and_base_select_distinct_facts() { + let span = Span::new(FileId::new(0), 1, 2); + let base_info = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".into()), + false, + vec![], + ); + let alias_info = + ReflectionTypeInfo::new("Label", "alias", None, false, vec![base_info.clone()]); + let base = Arc::new(CheckedScopedTypes { + bound_type: "string".into(), + reflection: Some(base_info.clone()), + ..Default::default() + }); + let alias = Arc::new(CheckedScopedTypes { + bound_type: "string".into(), + reflection: Some(alias_info.clone()), + ..Default::default() + }); + for candidates in [ + vec![base.clone(), alias.clone()], + vec![alias.clone(), base.clone()], + ] { + let bindings = HashMap::from([(span, candidates)]); + assert!(Arc::ptr_eq( + &select_scoped_types(&bindings, span, "string", Some(&base_info)) + .unwrap() + .unwrap(), + &base + )); + assert!(Arc::ptr_eq( + &select_scoped_types(&bindings, span, "string", Some(&alias_info)) + .unwrap() + .unwrap(), + &alias + )); + } + } + + #[test] + fn conflicting_scoped_widths_are_rejected_in_either_registration_order() { + let span = Span::new(FileId::new(0), 1, 2); + let narrow = Arc::new(CheckedScopedTypes { + bound_type: "Record".into(), + expressions: HashMap::from([(span, "int8".into())]), + ..Default::default() + }); + let wide = Arc::new(CheckedScopedTypes { + bound_type: "Record".into(), + expressions: HashMap::from([(span, "int64".into())]), + ..Default::default() + }); + for candidates in [vec![narrow.clone(), wide.clone()], vec![wide, narrow]] { + let types = HashMap::from([(span, candidates)]); + assert!( + select_scoped_types(&types, span, "Record", None) + .unwrap_err() + .contains("conflicting") + ); + assert!( + select_scoped_types(&types, span, "Other", None) + .unwrap() + .is_none() + ); + } + } + + #[test] + fn conflicting_checked_widths_are_rejected_in_either_registration_order() { + let span = Span::new(FileId::new(0), 1, 2); + let narrow = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([(span, "int8".into())])), + ..Default::default() + }); + let wide = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([(span, "int64".into())])), + ..Default::default() + }); + for candidates in [vec![narrow.clone(), wide.clone()], vec![wide, narrow]] { + let types = CheckedExpressionTypes { + functions: HashMap::from([(span, candidates)]), + ..Default::default() + }; + assert!( + types + .select(span, &[], &[]) + .unwrap_err() + .contains("conflicting") + ); + } + } +} diff --git a/crates/jett_comptime/src/debug.rs b/crates/jett_comptime/src/debug.rs new file mode 100644 index 00000000..417e295c --- /dev/null +++ b/crates/jett_comptime/src/debug.rs @@ -0,0 +1,42 @@ +//! Typed compiler-owned debug observations, separate from program stdout. + +/// The source operation that produced a debug observation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DebugEventKind { + Trace, + Breakpoint, + Print, + Println, +} + +impl DebugEventKind { + /// Canonical kind for structured tool output, independent of event text. + pub const fn as_str(self) -> &'static str { + match self { + Self::Trace => "trace", + Self::Breakpoint => "breakpoint", + Self::Print => "print", + Self::Println => "println", + } + } +} + +/// One complete observation in execution order. +/// +/// `text` contains the exact emitted bytes as UTF-8, including only the newline +/// deliberately supplied by the operation. A `Print` can be empty, partial, or +/// contain embedded newlines. Its text never determines its kind. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DebugEvent { + pub kind: DebugEventKind, + pub text: String, +} + +/// Render a diagnostic stream without adding separators or splitting events. +pub fn render_debug_events(events: &[DebugEvent]) -> String { + let mut output = String::new(); + for event in events { + output.push_str(&event.text); + } + output +} diff --git a/crates/jett_comptime/src/explicit.rs b/crates/jett_comptime/src/explicit.rs index 3a11ea6f..7a2f403b 100644 --- a/crates/jett_comptime/src/explicit.rs +++ b/crates/jett_comptime/src/explicit.rs @@ -1,54 +1,702 @@ -use std::collections::HashMap; +use crate::checked_types::{CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedTypes}; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use jett_common::{FileId, Span}; use jett_diagnostics::Diagnostic; use jett_parser::ast::{Block, Expr, Item, Module, Stmt, StringPart}; -use jett_types::ReflectionMetadata; +use jett_types::{ReflectionMetadata, ReflectionTypeInfo}; -use crate::{Interpreter, Value}; +use crate::interpreter::actor_graph::ActorGraphTemplate; +pub use crate::resource_execution::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, +}; +use crate::resource_execution::{CheckedAttemptKey, PreparedRequiredExpression}; +use crate::value::ClosureScopedTypeBinding; +use crate::{DebugEvent, Interpreter, Value}; -/// Evaluate every explicit `comptime` expression in a checked module. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct ComptimeContext { + pub type_arguments: Vec, + pub type_argument_reflections: Vec, + pub type_info_kinds: Vec<(usize, String)>, + pub type_info_primitives: Vec<(usize, Option)>, + pub type_kind_values: Vec<(usize, String)>, + pub type_primitive_values: Vec<(usize, String)>, + pub scoped_type_bindings: Vec, +} + +impl ComptimeContext { + pub fn from_checked(function: Option<&CheckedFunctionTypes>) -> Self { + let Some(function) = function else { + return Self::default(); + }; + Self { + type_arguments: function.type_arguments.clone(), + type_argument_reflections: function.type_argument_reflections.clone(), + type_info_kinds: function.type_info_kinds.clone(), + type_info_primitives: function.type_info_primitives.clone(), + type_kind_values: function.type_kind_values.clone(), + type_primitive_values: function.type_primitive_values.clone(), + scoped_type_bindings: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct ExplicitComptimeValues { + values: HashMap<(Span, ComptimeContext), Value>, + constants: HashMap, + // Private authority association. Each successful insertion below also + // mirrors the value into values or constants in that same branch, so the + // existing public collection views count reusable values once. + checked_values: HashMap, + // Private associations, never minted by a public span/context insertion. + actor_graphs: HashMap<(Span, ComptimeContext), Arc>, + checked_actor_graphs: HashMap>, + // Only an authentic, fully successful evaluator installs this immutable view. + successful: Option>, +} + +#[derive(Debug)] +struct SuccessfulCache { + program: Arc, + values: HashMap<(Span, ComptimeContext), Value>, + constants: HashMap, + checked_values: HashMap, + actor_graphs: HashMap<(Span, ComptimeContext), Arc>, + checked_actor_graphs: HashMap>, +} + +impl ExplicitComptimeValues { + /// Readonly identity of the successful private cache, including empty success. + pub fn same_checked_cache(&self, other: &Self) -> bool { + matches!((&self.successful, &other.successful), (Some(left), Some(right)) if Arc::ptr_eq(left, right)) + } + + fn seal_success(&mut self, program: Arc) { + self.successful = Some(Arc::new(SuccessfulCache { + program, + values: self.values.clone(), + constants: self.constants.clone(), + checked_values: self.checked_values.clone(), + actor_graphs: self.actor_graphs.clone(), + checked_actor_graphs: self.checked_actor_graphs.clone(), + })); + } + + /// A private worker reads the frozen successful maps, never public mirrors. + pub(crate) fn successful_checked_view( + &self, + program: &Arc, + ) -> Result { + let snapshot = self + .successful + .as_ref() + .filter(|snapshot| Arc::ptr_eq(&snapshot.program, program)) + .ok_or("checked property cases require the original successful required-value cache")?; + Ok(Self { + values: snapshot.values.clone(), + constants: snapshot.constants.clone(), + checked_values: snapshot.checked_values.clone(), + actor_graphs: snapshot.actor_graphs.clone(), + checked_actor_graphs: snapshot.checked_actor_graphs.clone(), + successful: Some(snapshot.clone()), + }) + } + + pub fn has_actor_graphs(&self) -> bool { + !self.checked_actor_graphs.is_empty() + } + pub fn len(&self) -> usize { + self.values.len() + self.constants.len() + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() && self.constants.is_empty() + } + + pub fn values(&self) -> impl Iterator { + self.values.values().chain(self.constants.values()) + } + + pub fn get(&self, span: Span, context: &ComptimeContext) -> Option<&Value> { + self.values.get(&(span, context.clone())) + } + + pub fn insert(&mut self, span: Span, context: ComptimeContext, value: Value) { + // Public mirrors cannot replace a privately captured frozen graph. + self.values.insert((span, context), value); + } + + pub fn actor_graph( + &self, + span: Span, + context: &ComptimeContext, + ) -> Option<&Arc> { + self.actor_graphs.get(&(span, context.clone())) + } + pub(crate) fn checked_actor_graph( + &self, + key: &CheckedAttemptKey, + ) -> Option<&Arc> { + self.checked_actor_graphs.get(key) + } + + /// Authenticate successful private required entries for this exact checked + /// program. Public span/context mirrors are never used to mint proof. + pub fn checked_required_values( + &self, + program: &Arc, + ) -> Result, String> { + self.checked_values + .keys() + .map(|key| CheckedRequiredValue::from_cache(self, key, program)) + .collect() + } + + /// Readonly hook projections of the same private successful entries. + pub fn checked_resource_hook_values( + &self, + program: &Arc, + ) -> Result, String> { + Ok(self + .checked_required_values(program)? + .into_iter() + .filter_map(CheckedRequiredResourceHook::from_value) + .collect()) + } + pub(crate) fn checked_get(&self, key: &CheckedAttemptKey) -> Option<&Value> { + self.checked_values.get(key) + } + + fn checked_insert(&mut self, key: CheckedAttemptKey, value: Value) -> Result<(), String> { + if value.contains_live_resource_or_grant() { + return Err( + "required value cannot cache runtime authority or Resource custody".to_string(), + ); + } + self.checked_actor_graphs.remove(&key); + self.checked_values.insert(key, value); + Ok(()) + } + + #[cfg(test)] + pub(crate) fn checked_values_are_mirrored(&self) -> bool { + self.checked_values.values().all(|checked| { + self.values() + .any(|value| match (value.payload(), checked.payload()) { + (Value::ResourceHook(left), Value::ResourceHook(right)) => { + crate::resource_execution::tests::same_resource_hook_descriptor_identity( + left, right, + ) + } + _ => value == checked, + }) + }) + } + + /// A checked namespace constant, independent of any function instantiation. + pub fn constant(&self, declaration: Span) -> Option<&Value> { + self.constants.get(&declaration) + } +} + +/// Actual required-evaluation observations, separate from the reusable baked values. +/// Reading a baked value must never replay these events. +#[derive(Debug, Clone, Default)] +pub struct ExplicitComptimeEvaluation { + pub values: ExplicitComptimeValues, + pub diagnostics: Vec, + pub debug_events: Vec, +} + +struct CollectedExpression<'a> { + namespace: Option, + aliases: HashMap, + expression: &'a Expr, + span: Span, + owner: Option<(Span, Vec)>, + bindings: Vec<(Span, String)>, +} + +#[derive(Clone, Default)] +struct EvaluationContext { + required: Option, + function: Option>, + scope: Option>, + bindings: Vec, +} + +impl EvaluationContext { + fn key(&self) -> ComptimeContext { + let mut key = ComptimeContext::from_checked(self.function.as_deref()); + key.scoped_type_bindings = self.bindings.clone(); + key + } +} + +/// Evaluate namespace constants and every explicit `comptime` expression in a +/// checked module. /// -/// The expressions are evaluated with an empty lexical environment. This is -/// deliberate: an explicit comptime value must be closed and reproducible at -/// build time, rather than depending on a runtime parameter or local. +/// The expressions are evaluated without runtime locals or parameters. Visible +/// `use` aliases are lexical name bindings and are carried into evaluation. +/// An explicit comptime value remains closed and reproducible at build time. pub fn evaluate_explicit_comptime_expressions( module: &Module, reflection_metadata: Arc, - checked_expression_types: Arc>, -) -> (HashMap, Vec) { + checked_expression_types: Arc, + breakpoint_exclusions: Arc>>, +) -> (ExplicitComptimeValues, Vec) { + let captured = evaluate_explicit_comptime_expressions_capture( + module, + reflection_metadata, + checked_expression_types, + breakpoint_exclusions, + ); + (captured.values, captured.diagnostics) +} + +/// Evaluate required values once and retain ordered debug observations, including +/// events emitted before a failed expression or namespace constant initializer. +/// The worker records silently; callers select whether and how to render events. +pub fn evaluate_explicit_comptime_expressions_capture( + module: &Module, + reflection_metadata: Arc, + checked_expression_types: Arc, + breakpoint_exclusions: Arc>>, +) -> ExplicitComptimeEvaluation { + // This is an optional stamp, not a stronger rejection policy for old callers. + let authenticated = checked_expression_types + .resource_program + .as_ref() + .filter(|program| { + std::ptr::eq(module, program.module()) + && Arc::ptr_eq(&reflection_metadata, &program.checked().reflection_metadata) + && breakpoint_exclusions.as_ref() == &program.checked().breakpoint_exclusions + && checked_expression_types.matches_original_program(program) + }) + .cloned(); let mut expressions = Vec::new(); collect_module_expressions(module, &mut expressions); + let span = expressions + .first() + .map(|expression| expression.span) + .or_else(|| { + module.items.iter().find_map(|item| { + if let Item::VarDecl(decl) = item { + Some(decl.name.span) + } else { + None + } + }) + }); + let Some(span) = span else { + let mut captured = ExplicitComptimeEvaluation::default(); + if let Some(program) = authenticated { + captured.values.seal_success(program); + } + return captured; + }; + // Compiler callers may have a smaller stack than reference execution. + // Keep required comptime evaluation on the same fixed interpreter budget. + let mut captured = std::thread::scope(|scope| { + match std::thread::Builder::new() + .name("jett-comptime".into()) + .stack_size(crate::INTERPRETER_STACK_SIZE) + .spawn_scoped(scope, move || { + evaluate_collected_expressions( + module, + expressions, + reflection_metadata, + checked_expression_types, + breakpoint_exclusions, + ) + }) { + Ok(handle) => match handle.join() { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + }, + Err(error) => ExplicitComptimeEvaluation { + diagnostics: vec![Diagnostic::error( + 9001, + format!("cannot create comptime evaluation worker: {error}"), + span, + )], + ..ExplicitComptimeEvaluation::default() + }, + } + }); + if captured.diagnostics.is_empty() { + if let Some(program) = authenticated { + captured.values.seal_success(program); + } + } + captured +} +fn evaluate_collected_expressions( + module: &Module, + expressions: Vec>, + reflection_metadata: Arc, + checked_expression_types: Arc, + breakpoint_exclusions: Arc>>, +) -> ExplicitComptimeEvaluation { + if let Some(program) = &checked_expression_types.resource_program { + if !std::ptr::eq(module, program.module()) { + return ExplicitComptimeEvaluation { + diagnostics: vec![Diagnostic::error( + 0, + "required worker received a foreign checked source module", + module.span, + )], + ..ExplicitComptimeEvaluation::default() + }; + } + } let mut interpreter = Interpreter::new(); + if let Some(program) = &checked_expression_types.resource_program { + if let Err(error) = interpreter + .install_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ExplicitComptime, + ) + .and_then(|()| { + interpreter.authorize_checked_resource_worker( + module, + crate::ExecutionPurpose::ExplicitComptime, + ) + }) + { + return ExplicitComptimeEvaluation { + diagnostics: vec![Diagnostic::error(0, error, module.span)], + ..ExplicitComptimeEvaluation::default() + }; + } + } interpreter.set_reflection_metadata(reflection_metadata); - interpreter.set_checked_expression_types(checked_expression_types); + interpreter.set_checked_expression_types(checked_expression_types.clone()); + interpreter.set_breakpoint_exclusions(breakpoint_exclusions); interpreter.register_module(module); - let mut values = HashMap::new(); + let mut values = ExplicitComptimeValues::default(); let mut diagnostics = Vec::new(); - for (namespace, expression, span) in expressions { - match interpreter.eval_expr_in_namespace(namespace.as_deref(), expression) { + let previous_purpose = + interpreter.checked_execution_purpose(crate::ExecutionPurpose::NamespaceConstant); + let mut attempted_checked = HashSet::new(); + let mut attempted_expressions = evaluate_constants( + module, + &checked_expression_types, + &mut interpreter, + &mut values, + &mut diagnostics, + &mut attempted_checked, + ); + if let Some(purpose) = previous_purpose { + interpreter.checked_execution_purpose(purpose); + } + for collected in expressions { + let contexts = match interpreter.checked_explicit_contexts( + collected.expression, + collected.span, + collected.owner.as_ref().map(|(span, _)| *span), + &collected.bindings, + ) { + Ok(Some(contexts)) => contexts + .into_iter() + .map(|required| EvaluationContext { + function: required.function_projection(), + scope: required.scope_projection(), + bindings: required.bindings(), + required: Some(required), + }) + .collect(), + Ok(None) => evaluation_contexts(&collected, &checked_expression_types), + Err(error) => { + diagnostics.push(Diagnostic::error(9001, error, collected.span)); + continue; + } + }; + let parameters = collected + .owner + .as_ref() + .map_or(&[][..], |(_, names)| names.as_slice()); + for context in contexts { + let key = context.key(); + let checked_key = context.required.as_ref().map(|required| required.key()); + if let Some(identity) = &checked_key { + if values.checked_get(identity).is_some() + || !attempted_checked.insert(identity.clone()) + { + continue; + } + } else { + if values.get(collected.span, &key).is_some() { + continue; + } + if !attempted_expressions.insert((collected.span, key.clone())) { + continue; + } + } + let evaluated = if let Some(required) = &context.required { + interpreter.eval_checked_required_expression( + required, + collected.namespace.as_deref(), + &collected.aliases, + parameters, + ) + } else { + interpreter.eval_closed_comptime_expression( + collected.namespace.as_deref(), + &collected.aliases, + collected.expression, + parameters, + context.function, + context.scope, + context.bindings, + ) + }; + match evaluated { + Ok(value) => { + let graph = match interpreter.freeze_actor_graph(&value, context.required.as_ref()) { + Ok(graph) => graph, + Err(error) => { diagnostics.push(Diagnostic::error(9001, error, collected.span)); continue; } + }; + if let Some(identity) = checked_key { + if let Err(error) = values.checked_insert(identity.clone(), value.clone()) { + diagnostics.push(Diagnostic::error(9001, error, collected.span)); continue; + } + if let Some(graph) = &graph { values.checked_actor_graphs.insert(identity, graph.clone()); } + } + values.insert(collected.span, key.clone(), value); + if let Some(graph) = graph { values.actor_graphs.insert((collected.span, key), graph); } + } + Err(error) => diagnostics.push(Diagnostic::error( + 9001, + format!("`comptime` expression must be closed and evaluable during compilation: {error}"), + collected.span, + )), + } + } + } + ExplicitComptimeEvaluation { + values, + diagnostics, + debug_events: interpreter.take_debug_events(), + } +} + +fn evaluate_constants( + module: &Module, + checked: &CheckedExpressionTypes, + interpreter: &mut Interpreter, + values: &mut ExplicitComptimeValues, + diagnostics: &mut Vec, + attempted_checked: &mut HashSet, +) -> HashSet<(Span, ComptimeContext)> { + let mut attempted = HashSet::new(); + let mut current_file = None; + let mut namespace = None; + for item in &module.items { + let file = item_file(item); + if current_file.is_some_and(|previous| previous != file) { + namespace = None; + } + current_file = Some(file); + if let Item::Namespace(declaration) = item { + namespace = Some(declaration.name.name.clone()); + } + let Item::VarDecl(declaration) = item else { + continue; + }; + let ty = checked.get(&declaration.name.span).map(String::as_str); + if !matches!( + ty, + Some( + "int8" + | "int16" + | "int32" + | "int64" + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "float32" + | "float64" + | "string" + | "bool" + | "nothing" + ) + ) { + diagnostics.push(Diagnostic::error( + 9001, + "constant execution currently requires an implicitly copyable primitive type; move-only constant ownership is unresolved", + declaration.name.span, + )); + continue; + } + let expression = match &declaration.value { + Expr::Comptime(inner, _) => inner.as_ref(), + expression => expression, + }; + // The same explicit root is also in the collected-expression pass. + // Record an actual attempt independently of whether it yields a value, + // so error recovery does not rerun it or repeat its observations. + if let Expr::Comptime(_, span) = &declaration.value { + attempted.insert((*span, ComptimeContext::default())); + } + let prepared = match interpreter.checked_namespace_initializer(declaration) { + Ok(prepared) => prepared, + Err(error) => { + diagnostics.push(Diagnostic::error(9001, error, declaration.value.span())); + continue; + } + }; + if let Some(prepared) = &prepared { + attempted_checked.insert(prepared.key()); + } + let evaluated = if let Some(prepared) = &prepared { + interpreter.eval_checked_required_expression( + prepared, + namespace.as_deref(), + &HashMap::new(), + &[], + ) + } else { + interpreter.eval_closed_comptime_expression( + namespace.as_deref(), + &HashMap::new(), + expression, + &[], + None, + None, + Vec::new(), + ) + }; + match evaluated { Ok(value) => { - values.insert(span, value); + if !constant_value_matches_type(&value, ty) { + diagnostics.push(Diagnostic::error( + 9001, + "global constant value does not match its declared primitive type; pending tasks and other hidden runtime state cannot be baked as primitive constants", + declaration.value.span(), + )); + continue; + } + if let Some(prepared) = &prepared { + if let Err(error) = values.checked_insert(prepared.key(), value.clone()) { + diagnostics.push(Diagnostic::error(9001, error, declaration.value.span())); + continue; + } + } + interpreter.register_constant_in_namespace( + namespace.as_deref(), + declaration, + value.clone(), + ); + if let Expr::Comptime(_, span) = declaration.value { + values.insert(span, ComptimeContext::default(), value.clone()); + } + values.constants.insert(declaration.name.span, value); } Err(error) => diagnostics.push(Diagnostic::error( 9001, - format!( - "`comptime` expression must be closed and evaluable during compilation: {error}" - ), - span, + format!("global constant must be evaluable during compilation: {error}"), + declaration.value.span(), )), } } + attempted +} + +fn constant_value_matches_type(value: &Value, ty: Option<&str>) -> bool { + // Sized primitives retain their checked identity around the scalar carrier. + // Removing that metadata must not also unwrap a pending task. + match (value.payload(), ty) { + (Value::Int64(value), Some("int8")) => i8::try_from(*value).is_ok(), + (Value::Int64(value), Some("int16")) => i16::try_from(*value).is_ok(), + (Value::Int64(value), Some("int32")) => i32::try_from(*value).is_ok(), + (Value::Int64(_), Some("int64")) => true, + // The interpreter uses its signed carrier for these unsigned widths. + (Value::Int64(value), Some("uint8")) => u8::try_from(*value).is_ok(), + (Value::Int64(value), Some("uint16")) => u16::try_from(*value).is_ok(), + (Value::Int64(value), Some("uint32")) => u32::try_from(*value).is_ok(), + (Value::Uint64(_), Some("uint64")) => true, + (Value::Float64(_), Some("float32" | "float64")) + | (Value::String(_), Some("string")) + | (Value::Bool(_), Some("bool")) + | (Value::Nothing, Some("nothing")) => true, + _ => false, + } +} - (values, diagnostics) +fn evaluation_contexts( + expression: &CollectedExpression<'_>, + checked: &CheckedExpressionTypes, +) -> Vec { + let mut contexts = if let Some((owner, parameters)) = &expression.owner { + match checked.functions.get(owner) { + Some(instances) => instances + .iter() + .map(|instance| EvaluationContext { + function: Some(instance.clone()), + ..EvaluationContext::default() + }) + .collect(), + None if !parameters.is_empty() => Vec::new(), + None => vec![EvaluationContext::default()], + } + } else { + vec![EvaluationContext::default()] + }; + for (span, name) in &expression.bindings { + contexts = contexts + .into_iter() + .flat_map(|context| { + let bindings = context + .scope + .as_ref() + .map(|scope| &scope.bindings) + .or_else(|| context.function.as_ref().map(|function| &function.bindings)) + .unwrap_or(&checked.bindings); + bindings + .get(span) + .into_iter() + .flatten() + .map(|scope| { + let mut next = context.clone(); + next.scope = Some(scope.clone()); + next.bindings.push(ClosureScopedTypeBinding { + name: name.clone(), + canonical_name: scope.bound_type.clone(), + reflection: scope.reflection.clone(), + }); + next + }) + .collect::>() + }) + .collect(); + } + contexts.retain(|context| { + let expressions = context + .scope + .as_ref() + .map(|scope| &scope.expressions) + .or_else(|| { + context + .function + .as_ref() + .map(|function| function.expressions.as_ref()) + }); + expressions.is_none_or(|expressions| expressions.contains_key(&expression.span)) + }); + contexts } fn collect_module_expressions<'a>( module: &'a Module, - expressions: &mut Vec<(Option, &'a Expr, Span)>, + expressions: &mut Vec>, ) { let mut current_file = None; let mut current_namespace = None; @@ -88,34 +736,45 @@ fn item_file(item: &Item) -> FileId { fn collect_item<'a>( item: &'a Item, namespace: Option<&str>, - expressions: &mut Vec<(Option, &'a Expr, Span)>, + expressions: &mut Vec>, ) { + let aliases = &HashMap::new(); match item { - Item::Function(function) => collect_block(&function.body, namespace, expressions), + Item::Function(function) => { + collect_function(function, &[], namespace, aliases, expressions) + } Item::Implement(implementation) => { for method in &implementation.methods { - collect_block(&method.body, namespace, expressions); + collect_function(method, &[], namespace, aliases, expressions); } } Item::Struct(structure) => { for method in &structure.methods { - collect_block(&method.body, namespace, expressions); + collect_function( + method, + &structure.type_params, + namespace, + aliases, + expressions, + ); } } Item::Actor(actor) => { for field in &actor.state_fields { - collect_expr(&field.value, namespace, expressions); + collect_expr(&field.value, namespace, aliases, expressions); } for handler in &actor.handlers { - collect_block(&handler.body, namespace, expressions); + collect_block(&handler.body, namespace, aliases, expressions); } } - Item::VarDecl(declaration) => collect_expr(&declaration.value, namespace, expressions), - Item::Verify(verify) => collect_block(&verify.body, namespace, expressions), - Item::Property(property) => collect_block(&property.body, namespace, expressions), + Item::VarDecl(declaration) => { + collect_expr(&declaration.value, namespace, aliases, expressions) + } + Item::Verify(verify) => collect_block(&verify.body, namespace, aliases, expressions), + Item::Property(property) => collect_block(&property.body, namespace, aliases, expressions), Item::TypeAlias(alias) => { if let Some(constraint) = &alias.constraint { - collect_expr(constraint, namespace, expressions); + collect_expr(constraint, namespace, aliases, expressions); } } Item::Namespace(_) @@ -128,90 +787,136 @@ fn collect_item<'a>( } } +fn collect_function<'a>( + function: &'a jett_parser::ast::FunctionDef, + outer_parameters: &[jett_parser::ast::Ident], + namespace: Option<&str>, + aliases: &HashMap, + expressions: &mut Vec>, +) { + let start = expressions.len(); + collect_block(&function.body, namespace, aliases, expressions); + let parameters = outer_parameters + .iter() + .chain(&function.type_params) + .map(|parameter| parameter.name.clone()) + .collect::>(); + for expression in &mut expressions[start..] { + expression.owner = Some((function.name.span, parameters.clone())); + } +} + fn collect_block<'a>( block: &'a Block, namespace: Option<&str>, - expressions: &mut Vec<(Option, &'a Expr, Span)>, + aliases: &HashMap, + expressions: &mut Vec>, ) { + let mut visible_aliases = aliases.clone(); for statement in &block.stmts { - collect_stmt(statement, namespace, expressions); + collect_stmt(statement, namespace, &mut visible_aliases, expressions); } } fn collect_stmt<'a>( statement: &'a Stmt, namespace: Option<&str>, - expressions: &mut Vec<(Option, &'a Expr, Span)>, + aliases: &mut HashMap, + expressions: &mut Vec>, ) { match statement { - Stmt::VarDecl(declaration) => collect_expr(&declaration.value, namespace, expressions), + Stmt::VarDecl(declaration) => { + collect_expr(&declaration.value, namespace, aliases, expressions) + } Stmt::Assign(assignment) => { - collect_expr(&assignment.target, namespace, expressions); - collect_expr(&assignment.value, namespace, expressions); + collect_expr(&assignment.target, namespace, aliases, expressions); + collect_expr(&assignment.value, namespace, aliases, expressions); } Stmt::Return(statement) => { if let Some(value) = &statement.value { - collect_expr(value, namespace, expressions); + collect_expr(value, namespace, aliases, expressions); } } - Stmt::Respond(statement) => collect_expr(&statement.value, namespace, expressions), + Stmt::Respond(statement) => collect_expr(&statement.value, namespace, aliases, expressions), Stmt::ComptimeTypeBind(binding) => { - collect_expr(&binding.value, namespace, expressions); - collect_block(&binding.body, namespace, expressions); + collect_expr(&binding.value, namespace, aliases, expressions); + let start = expressions.len(); + collect_block(&binding.body, namespace, aliases, expressions); + for expression in &mut expressions[start..] { + expression + .bindings + .insert(0, (binding.span, binding.name.name.clone())); + } } Stmt::If(statement) => { - collect_expr(&statement.condition, namespace, expressions); - collect_block(&statement.then_block, namespace, expressions); + collect_expr(&statement.condition, namespace, aliases, expressions); + collect_block(&statement.then_block, namespace, aliases, expressions); for (condition, block) in &statement.else_ifs { - collect_expr(condition, namespace, expressions); - collect_block(block, namespace, expressions); + collect_expr(condition, namespace, aliases, expressions); + collect_block(block, namespace, aliases, expressions); } if let Some(block) = &statement.else_block { - collect_block(block, namespace, expressions); + collect_block(block, namespace, aliases, expressions); } } Stmt::For(statement) => { - collect_expr(&statement.iterable, namespace, expressions); - collect_block(&statement.body, namespace, expressions); + collect_expr(&statement.iterable, namespace, aliases, expressions); + collect_block(&statement.body, namespace, aliases, expressions); } Stmt::While(statement) => { - collect_expr(&statement.condition, namespace, expressions); - collect_block(&statement.body, namespace, expressions); + collect_expr(&statement.condition, namespace, aliases, expressions); + collect_block(&statement.body, namespace, aliases, expressions); } Stmt::Match(statement) => { - collect_expr(&statement.expr, namespace, expressions); + collect_expr(&statement.expr, namespace, aliases, expressions); for arm in &statement.arms { - collect_block(&arm.body, namespace, expressions); + collect_block(&arm.body, namespace, aliases, expressions); } } - Stmt::Expr(statement) => collect_expr(&statement.expr, namespace, expressions), + Stmt::Expr(statement) => collect_expr(&statement.expr, namespace, aliases, expressions), Stmt::Assert(statement) => { - collect_expr(&statement.condition, namespace, expressions); + collect_expr(&statement.condition, namespace, aliases, expressions); if let Some(message) = &statement.message { - collect_expr(message, namespace, expressions); + collect_expr(message, namespace, aliases, expressions); } } Stmt::Breakpoint(statement) => { if let Some(condition) = &statement.condition { - collect_expr(condition, namespace, expressions); + collect_expr(condition, namespace, aliases, expressions); } } - Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + Stmt::Use(declaration) => { + let bound_name = + Interpreter::use_bound_name(&declaration.path.name, declaration.alias.as_ref()); + aliases.insert(bound_name, declaration.path.name.clone()); + } + Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} } } fn collect_expr<'a>( expression: &'a Expr, namespace: Option<&str>, - expressions: &mut Vec<(Option, &'a Expr, Span)>, + aliases: &HashMap, + expressions: &mut Vec>, ) { match expression { Expr::Comptime(inner, span) => { - expressions.push((namespace.map(str::to_string), inner, *span)); + expressions.push(CollectedExpression { + namespace: namespace.map(str::to_string), + aliases: aliases.clone(), + expression: inner, + span: *span, + owner: None, + bindings: Vec::new(), + }); + // A baked inline function can still execute its body later. Its + // nested explicit expressions need independent closed values too. + collect_expr(inner, namespace, aliases, expressions); } Expr::Binary(left, _, right, _) => { - collect_expr(left, namespace, expressions); - collect_expr(right, namespace, expressions); + collect_expr(left, namespace, aliases, expressions); + collect_expr(right, namespace, aliases, expressions); } Expr::Unary(_, inner, _) | Expr::FieldAccess(inner, _, _) @@ -230,54 +935,54 @@ fn collect_expr<'a>( | Expr::Clone(inner, _) | Expr::Run(inner, _) | Expr::Join(inner, _) - | Expr::Cancel(inner, _) => collect_expr(inner, namespace, expressions), + | Expr::Cancel(inner, _) => collect_expr(inner, namespace, aliases, expressions), Expr::Call(callee, arguments, _) => { - collect_expr(callee, namespace, expressions); + collect_expr(callee, namespace, aliases, expressions); for argument in arguments { - collect_expr(&argument.value, namespace, expressions); + collect_expr(&argument.value, namespace, aliases, expressions); } } Expr::GenericCall(callee, _, arguments, _) => { - collect_expr(callee, namespace, expressions); + collect_expr(callee, namespace, aliases, expressions); for argument in arguments { - collect_expr(&argument.value, namespace, expressions); + collect_expr(&argument.value, namespace, aliases, expressions); } } Expr::ListConstruct(items, _) => { for item in items { - collect_expr(item, namespace, expressions); + collect_expr(item, namespace, aliases, expressions); } } Expr::MapConstruct(entries, _) => { for (key, value) in entries { - collect_expr(key, namespace, expressions); - collect_expr(value, namespace, expressions); + collect_expr(key, namespace, aliases, expressions); + collect_expr(value, namespace, aliases, expressions); } } Expr::Handle(target, _, block, _) => { - collect_expr(target, namespace, expressions); - collect_block(block, namespace, expressions); + collect_expr(target, namespace, aliases, expressions); + collect_block(block, namespace, aliases, expressions); } Expr::StringInterpolation(parts, _) => { for part in parts { if let StringPart::Expr(expression) = part { - collect_expr(expression, namespace, expressions); + collect_expr(expression, namespace, aliases, expressions); } } } Expr::Pipeline(initial, steps, _) => { - collect_expr(initial, namespace, expressions); + collect_expr(initial, namespace, aliases, expressions); for step in steps { - collect_expr(&step.function, namespace, expressions); + collect_expr(&step.function, namespace, aliases, expressions); for argument in &step.extra_args { - collect_expr(&argument.value, namespace, expressions); + collect_expr(&argument.value, namespace, aliases, expressions); } if let Some(handle) = &step.handle { - collect_block(&handle.body, namespace, expressions); + collect_block(&handle.body, namespace, aliases, expressions); } } } - Expr::InlineFn(_, _, block, _) => collect_block(block, namespace, expressions), + Expr::InlineFn(_, _, block, _) => collect_block(block, namespace, aliases, expressions), Expr::IntLiteral(_, _) | Expr::FloatLiteral(_, _) | Expr::StringLiteral(_, _) @@ -289,3 +994,259 @@ fn collect_expr<'a>( | Expr::Error(_) => {} } } + +#[cfg(test)] +#[path = "explicit/checked_constant_tests.rs"] +mod checked_constant_tests; + +#[cfg(test)] +mod debug_capture_tests { + use super::*; + use crate::DebugEventKind; + + fn parse(source: &str) -> Module { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + parsed.module + } + + fn capture(module: &Module, checked: CheckedExpressionTypes) -> ExplicitComptimeEvaluation { + evaluate_explicit_comptime_expressions_capture( + module, + Arc::new(ReflectionMetadata::new()), + Arc::new(checked), + Arc::new(HashMap::new()), + ) + } + + fn primitive_constants(module: &Module) -> CheckedExpressionTypes { + let mut checked = CheckedExpressionTypes::default(); + for item in &module.items { + if let Item::VarDecl(declaration) = item { + checked + .expressions + .insert(declaration.name.span, "int64".into()); + } + } + checked + } + + #[test] + fn required_evaluation_captures_constants_and_expressions_once_without_baked_replay() { + let module = parse( + r#" +function observe(label: string) returns int64: + print(view label) + println("!", view label) + return 7 +int64 cached = comptime observe("constant") +function main() returns int64: + int64 first = comptime observe("expression") + return cached + first +"#, + ); + let captured = capture(&module, primitive_constants(&module)); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert_eq!(captured.values.len(), 3); + assert_eq!( + captured.debug_events, + vec![ + DebugEvent { + kind: DebugEventKind::Print, + text: "constant".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "! constant\n".into() + }, + DebugEvent { + kind: DebugEventKind::Print, + text: "expression".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "! expression\n".into() + }, + ] + ); + let mut interpreter = Interpreter::new(); + interpreter.enable_stdout_capture(); + interpreter.set_explicit_comptime_values(Arc::new(captured.values)); + interpreter.register_module(&module); + assert_eq!( + interpreter.call_function("main", vec![]).unwrap(), + Value::Int64(14) + ); + assert!(interpreter.take_debug_events().is_empty()); + assert!(interpreter.take_stdout_output().is_empty()); + } + + #[test] + fn failed_required_expressions_and_constants_retain_prior_events_in_actual_order() { + let module = parse( + r#" +function reject(label: string) returns int64: + println(view label) + list[int64] rejected = range(0, 9223372036854775807) + return 0 +function accept() returns int64: + print("last") + return 1 +int64 failed = comptime reject("constant failure") +function main() returns nothing: + int64 first = comptime reject("expression failure") + int64 second = comptime accept() + return nothing +"#, + ); + let captured = capture(&module, primitive_constants(&module)); + assert_eq!(captured.diagnostics.len(), 2); + assert!( + captured + .diagnostics + .iter() + .all(|d| d.message.contains("range: requested output is too large")) + ); + assert_eq!(captured.values.len(), 1); + let failed = module + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(declaration) if declaration.name.name == "failed" => { + Some(declaration) + } + _ => None, + }) + .expect("failed namespace declaration"); + let Expr::Comptime(_, span) = &failed.value else { + panic!("explicit namespace initializer") + }; + assert!(captured.values.constant(failed.name.span).is_none()); + assert!( + captured + .values + .get(*span, &ComptimeContext::default()) + .is_none() + ); + assert_eq!( + captured.debug_events, + vec![ + DebugEvent { + kind: DebugEventKind::Println, + text: "constant failure\n".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "expression failure\n".into() + }, + DebugEvent { + kind: DebugEventKind::Print, + text: "last".into() + }, + ] + ); + } + + #[test] + fn duplicate_checked_contexts_reuse_only_values_and_do_not_repeat_events() { + let module = parse( + r#" +function observe() returns int64: + println("once") + return 7 +function main() returns int64: + int64 value = comptime observe() + return value +"#, + ); + let Item::Function(function) = &module.items[1] else { + panic!("main") + }; + let Stmt::VarDecl(declaration) = &function.body.stmts[0] else { + panic!("local") + }; + let Expr::Comptime(_, span) = &declaration.value else { + panic!("comptime") + }; + let instance = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([(*span, "int64".into())])), + ..CheckedFunctionTypes::default() + }); + let mut checked = CheckedExpressionTypes::default(); + checked + .functions + .insert(function.name.span, vec![instance.clone(), instance]); + let captured = capture(&module, checked); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert_eq!(captured.values.len(), 1); + assert_eq!( + captured.debug_events, + vec![DebugEvent { + kind: DebugEventKind::Println, + text: "once\n".into(), + }] + ); + } + + #[test] + fn duplicate_failed_checked_contexts_evaluate_once_without_baked_values() { + let module = parse( + r#" +function reject() returns int64: + println("failed once") + list[int64] rejected = range(0, 9223372036854775807) + return 0 +function main() returns int64: + int64 value = comptime reject() + return value +"#, + ); + let Item::Function(function) = &module.items[1] else { + panic!("main") + }; + let Stmt::VarDecl(declaration) = &function.body.stmts[0] else { + panic!("local") + }; + let Expr::Comptime(_, span) = &declaration.value else { + panic!("comptime") + }; + let instance = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([(*span, "int64".into())])), + ..CheckedFunctionTypes::default() + }); + let mut checked = CheckedExpressionTypes::default(); + checked + .functions + .insert(function.name.span, vec![instance.clone(), instance]); + let captured = capture(&module, checked); + assert_eq!(captured.diagnostics.len(), 1); + assert_eq!(captured.diagnostics[0].code.code(), 9001); + assert_eq!(captured.diagnostics[0].span, *span); + assert_eq!( + captured.diagnostics[0].message, + "`comptime` expression must be closed and evaluable during compilation: range: requested output is too large" + ); + assert!(captured.values.is_empty()); + assert!( + captured + .values + .get(*span, &ComptimeContext::default()) + .is_none() + ); + assert_eq!( + captured.debug_events, + vec![DebugEvent { + kind: DebugEventKind::Println, + text: "failed once\n".into(), + }] + ); + } +} diff --git a/crates/jett_comptime/src/explicit/checked_constant_tests.rs b/crates/jett_comptime/src/explicit/checked_constant_tests.rs new file mode 100644 index 00000000..74acdb6c --- /dev/null +++ b/crates/jett_comptime/src/explicit/checked_constant_tests.rs @@ -0,0 +1,230 @@ +//! Ordinary checked programs must share one original namespace initializer attempt. + +use super::*; +use crate::ExecutionPurpose; +use jett_common::SourceOrigin; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +fn checked_program(source: &str, release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .expect("ordinary checked constant source"), + ) +} + +fn capture(program: &Arc) -> ExplicitComptimeEvaluation { + evaluate_explicit_comptime_expressions_capture( + program.module(), + program.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ) +} + +fn declaration(program: &CheckedResourceProgram) -> &jett_parser::ast::VarDecl { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(declaration) => Some(declaration), + _ => None, + }) + .expect("namespace constant") +} + +#[test] +fn ordinary_checked_constant_and_local_comptime_observations_run_once() { + let program = checked_program( + r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("constant:") +function main() returns int64: + int64 baked = comptime observe("expression:") + return cached + baked +"#, + false, + ); + let captured = capture(&program); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert_eq!( + captured.debug_events, + vec![ + DebugEvent { + kind: crate::DebugEventKind::Print, + text: "constant:".into() + }, + DebugEvent { + kind: crate::DebugEventKind::Print, + text: "expression:".into() + }, + ] + ); + assert_eq!(captured.values.len(), 3); + assert_eq!( + captured + .values + .checked_required_values(&program) + .unwrap() + .len(), + 2 + ); + assert!(captured.values.checked_values_are_mirrored()); + assert_eq!( + captured + .values + .constant(declaration(&program).name.span) + .unwrap() + .payload(), + &Value::Int64(7) + ); + + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_explicit_comptime_values(Arc::new(captured.values)); + interpreter.register_module(program.module()); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap_err(), + "missing checked Resource transport" + ); + assert_eq!( + interpreter + .call_function("app.main", vec![]) + .unwrap() + .payload(), + &Value::Int64(14) + ); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn ordinary_failed_constant_preserves_one_attempt_before_later_comptime() { + let program = checked_program( + r#"namespace app +function reject() returns int64: + println("constant failure") + list[int64] rejected = range(0, 9223372036854775807) + return 0 +function accept() returns int64: + print("later:") + return 1 +int64 failed = comptime reject() +function main() returns int64: + return comptime accept() +"#, + false, + ); + let captured = capture(&program); + assert_eq!(captured.diagnostics.len(), 1); + assert!( + captured.diagnostics[0] + .message + .contains("range: requested output is too large") + ); + assert_eq!( + captured.diagnostics[0].span, + declaration(&program).value.span() + ); + assert_eq!( + captured.debug_events, + vec![ + DebugEvent { + kind: crate::DebugEventKind::Println, + text: "constant failure\n".into() + }, + DebugEvent { + kind: crate::DebugEventKind::Print, + text: "later:".into() + }, + ] + ); + assert!( + captured + .values + .constant(declaration(&program).name.span) + .is_none() + ); + assert_eq!(captured.values.len(), 1); + assert_eq!( + captured + .values + .checked_required_values(&program) + .unwrap() + .len(), + 1 + ); +} + +#[test] +fn ordinary_constant_preparation_requires_original_program_node_and_purpose() { + for release in [false, true] { + let program = checked_program("namespace app\nint64 cached = comptime (3 + 4)\n", release); + let original = declaration(&program); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::NamespaceConstant, + ) + .unwrap(); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap_err(), + "missing checked Resource transport" + ); + let prepared = interpreter + .checked_namespace_initializer(original) + .unwrap() + .unwrap(); + assert_eq!(prepared.purpose(), ExecutionPurpose::NamespaceConstant); + let Expr::Comptime(inner, span) = &original.value else { + panic!("explicit initializer") + }; + interpreter.checked_execution_purpose(ExecutionPurpose::ExplicitComptime); + assert!(interpreter.checked_namespace_initializer(original).is_err()); + let contexts = interpreter + .checked_explicit_contexts(inner, *span, None, &[]) + .unwrap() + .unwrap(); + assert_eq!(contexts.len(), 1); + assert_eq!(prepared.key(), contexts[0].key()); + interpreter.checked_execution_purpose(ExecutionPurpose::NamespaceConstant); + assert!( + interpreter + .checked_namespace_initializer(&original.clone()) + .is_err() + ); + let foreign = checked_program("namespace app\nint64 cached = comptime (3 + 4)\n", release); + assert!( + interpreter + .checked_namespace_initializer(declaration(&foreign)) + .is_err() + ); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap_err(), + "missing checked Resource transport" + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} diff --git a/crates/jett_comptime/src/interpreter.rs b/crates/jett_comptime/src/interpreter.rs index 0a71b3da..92ed01e0 100644 --- a/crates/jett_comptime/src/interpreter.rs +++ b/crates/jett_comptime/src/interpreter.rs @@ -1,26 +1,65 @@ +#[path = "resource_execution/capture.rs"] +mod resource_capture; +#[path = "resource_execution/interpreter_transport.rs"] +mod resource_transport; +use crate::resource_execution::{ + EvaluatedValue, PreparedReflectedFieldIteration, ResourceTransport, +}; + +use crate::checked_types::{ + CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedTypes, select_scoped_types, +}; use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; -use rand::{RngCore, SeedableRng, rngs::StdRng}; use subtle::ConstantTimeEq; use unicode_segmentation::UnicodeSegmentation; use jett_common::{FileId, Span, is_json_raw_facade, json_public_bridge_spec}; +use jett_intrinsics::IntrinsicId; use jett_parser::ast::{ ActorDef, BinOp, BitfieldDef, BitfieldFieldKind, Block, CallArg, EnumDef, Expr, FunctionDef, Ident, ImplementBlock, InterfaceDecl, Item, MachineDef, Module, Param, Pattern, PipelineStep, PipelineStepHandle, Stmt, StringPart, StructDef, TypeAlias, TypeExpr, UnaryOp, }; +pub use jett_runtime::clock::ClockTestSample; +#[cfg(test)] +use jett_runtime::clock::raw_wall_clock_sample; +use jett_runtime::clock::{checked_clock_milliseconds, production_wall_clock_sample}; +use jett_runtime::crypto::{hmac_sha256_digest, md5_digest, sha256_digest, sha512_digest}; +use jett_runtime::csv::{csv_quote_field, parse_csv_records, parse_csv_with_header}; +use jett_runtime::encoding::{ + base64_decode, base64_encode, encoding_hex_decode, percent_decode, percent_encode, +}; +use jett_runtime::environment::LaunchEnvironmentSnapshot; +pub use jett_runtime::environment::{ + EnvironmentTestEntry, EnvironmentTestSnapshot, EnvironmentTestText, +}; +use jett_runtime::math::{float_average, float_midpoint}; +use jett_runtime::random::RandomProvider; +pub use jett_runtime::random::RandomTestSample; +#[cfg(test)] +use jett_runtime::random::unbiased_bounded_offset; use jett_types::{ ReflectionBitfieldFieldInfo, ReflectionBitfieldInfo, ReflectionFieldInfo, ReflectionMachineInfo, ReflectionMachineStateInfo, ReflectionMachineTransitionInfo, ReflectionMetadata, ReflectionTypeInfo, ReflectionVariantInfo, }; -use crate::value::Value; +use crate::debug::{DebugEvent, DebugEventKind}; +use crate::value::{ClosureScopedTypeBinding, ClosureTypeArgument, ClosureTypeContext, Value}; +pub mod actor_graph; +mod actor_handlers; +mod debug; +mod field_updates; +mod generic_inference; mod graphics; +mod interface_arguments; +mod nominal_types; +mod reflected_callables; +mod temporary_local_views; use graphics::GraphicsProvider; pub use graphics::{GraphicsTestEvent, GraphicsTestObservation}; @@ -118,6 +157,296 @@ macro_rules! require_args { }; } +// Closed decline sets for the specialized intrinsic dispatchers. These are +// deliberately exhaustive: extending IntrinsicId must update each dispatcher. +macro_rules! non_type_arg_intrinsics { + () => { + IntrinsicId::BitfieldFromBytes + | IntrinsicId::BitfieldToBytes + | IntrinsicId::BytesConcat + | IntrinsicId::BytesFromHex + | IntrinsicId::BytesFromString + | IntrinsicId::BytesGet + | IntrinsicId::BytesLength + | IntrinsicId::BytesNew + | IntrinsicId::BytesSlice + | IntrinsicId::BytesToHex + | IntrinsicId::BytesToString + | IntrinsicId::ClockNow + | IntrinsicId::CryptoHmacSha256 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CsvParse + | IntrinsicId::CsvParseWithHeader + | IntrinsicId::CsvStringify + | IntrinsicId::EncodingBase64Decode + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::EncodingFormDecode + | IntrinsicId::EncodingFormEncode + | IntrinsicId::EncodingHexDecode + | IntrinsicId::EncodingUrlDecode + | IntrinsicId::EncodingUrlEncode + | IntrinsicId::EnvironmentArgs + | IntrinsicId::EnvironmentGet + | IntrinsicId::FilesystemReadFile + | IntrinsicId::FilesystemWriteFile + | IntrinsicId::Float32FromFloat64 + | IntrinsicId::Float64FromInt64 + | IntrinsicId::Float64FromString + | IntrinsicId::GraphicsRun + | IntrinsicId::Int64FromFloat64 + | IntrinsicId::Int64FromString + | IntrinsicId::ListAppend + | IntrinsicId::ListGetClone + | IntrinsicId::ListGroupBy + | IntrinsicId::ListInsertAt + | IntrinsicId::ListIsSorted + | IntrinsicId::ListLength + | IntrinsicId::ListNew + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListSort + | IntrinsicId::ListSortBy + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListSum + | IntrinsicId::ListSwap + | IntrinsicId::LogEmit + | IntrinsicId::MapFromLists + | IntrinsicId::MapGet + | IntrinsicId::MapHas + | IntrinsicId::MapInsert + | IntrinsicId::MapLength + | IntrinsicId::MapNew + | IntrinsicId::MapRemove + | IntrinsicId::MathAbs + | IntrinsicId::MathAverage + | IntrinsicId::MathCeil + | IntrinsicId::MathClamp + | IntrinsicId::MathCos + | IntrinsicId::MathE + | IntrinsicId::MathFactorial + | IntrinsicId::MathFloor + | IntrinsicId::MathGcd + | IntrinsicId::MathKernelAbs + | IntrinsicId::MathKernelMax + | IntrinsicId::MathKernelMin + | IntrinsicId::MathLcm + | IntrinsicId::MathLog + | IntrinsicId::MathLog10 + | IntrinsicId::MathLog2 + | IntrinsicId::MathMax + | IntrinsicId::MathMedian + | IntrinsicId::MathMin + | IntrinsicId::MathMod + | IntrinsicId::MathPi + | IntrinsicId::MathPow + | IntrinsicId::MathRound + | IntrinsicId::MathSin + | IntrinsicId::MathSqrt + | IntrinsicId::MathTan + | IntrinsicId::Print + | IntrinsicId::Println + | IntrinsicId::RandomBool + | IntrinsicId::RandomBounded + | IntrinsicId::RandomUnitFloat64 + | IntrinsicId::Range + | IntrinsicId::SecretCompare + | IntrinsicId::SecretRedact + | IntrinsicId::SetAdd + | IntrinsicId::SetContains + | IntrinsicId::SetLength + | IntrinsicId::SetNew + | IntrinsicId::SetRemove + | IntrinsicId::StdoutWrite + | IntrinsicId::StringCharCount + | IntrinsicId::StringChars + | IntrinsicId::StringCount + | IntrinsicId::StringFromBool + | IntrinsicId::StringFromFloat64 + | IntrinsicId::StringFromInt64 + | IntrinsicId::StringFromUint64 + | IntrinsicId::StringIndexOf + | IntrinsicId::StringIsAlpha + | IntrinsicId::StringIsNumeric + | IntrinsicId::StringJoin + | IntrinsicId::StringLines + | IntrinsicId::StringLower + | IntrinsicId::StringRepeat + | IntrinsicId::StringReplace + | IntrinsicId::StringSlice + | IntrinsicId::StringSlugify + | IntrinsicId::StringSplit + | IntrinsicId::StringToLowerFirst + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringTrimStart + | IntrinsicId::StringUpper + | IntrinsicId::StringWords + | IntrinsicId::TestMockClock + | IntrinsicId::TestMockEnvironment + | IntrinsicId::TestMockRandom + | IntrinsicId::Uint64FromString + | IntrinsicId::UuidNew + }; +} + +macro_rules! non_higher_order_intrinsics { + () => { + IntrinsicId::BitfieldFromBytes + | IntrinsicId::BitfieldToBytes + | IntrinsicId::BytesConcat + | IntrinsicId::BytesFromHex + | IntrinsicId::BytesFromString + | IntrinsicId::BytesGet + | IntrinsicId::BytesLength + | IntrinsicId::BytesNew + | IntrinsicId::BytesSlice + | IntrinsicId::BytesToHex + | IntrinsicId::BytesToString + | IntrinsicId::ClockNow + | IntrinsicId::CryptoHmacSha256 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CsvParse + | IntrinsicId::CsvParseWithHeader + | IntrinsicId::CsvStringify + | IntrinsicId::EncodingBase64Decode + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::EncodingFormDecode + | IntrinsicId::EncodingFormEncode + | IntrinsicId::EncodingHexDecode + | IntrinsicId::EncodingUrlDecode + | IntrinsicId::EncodingUrlEncode + | IntrinsicId::EnvironmentArgs + | IntrinsicId::EnvironmentGet + | IntrinsicId::FilesystemReadFile + | IntrinsicId::FilesystemWriteFile + | IntrinsicId::Float32FromFloat64 + | IntrinsicId::Float64FromInt64 + | IntrinsicId::Float64FromString + | IntrinsicId::Int64FromFloat64 + | IntrinsicId::Int64FromString + | IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + | IntrinsicId::ListAppend + | IntrinsicId::ListGetClone + | IntrinsicId::ListInsertAt + | IntrinsicId::ListIsSorted + | IntrinsicId::ListLength + | IntrinsicId::ListNew + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListSort + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListSwap + | IntrinsicId::LogEmit + | IntrinsicId::MapFromLists + | IntrinsicId::MapGet + | IntrinsicId::MapHas + | IntrinsicId::MapInsert + | IntrinsicId::MapLength + | IntrinsicId::MapNew + | IntrinsicId::MapRemove + | IntrinsicId::MathAbs + | IntrinsicId::MathAverage + | IntrinsicId::MathCeil + | IntrinsicId::MathClamp + | IntrinsicId::MathCos + | IntrinsicId::MathE + | IntrinsicId::MathFactorial + | IntrinsicId::MathFloor + | IntrinsicId::MathGcd + | IntrinsicId::MathKernelAbs + | IntrinsicId::MathKernelMax + | IntrinsicId::MathKernelMin + | IntrinsicId::MathLcm + | IntrinsicId::MathLog + | IntrinsicId::MathLog10 + | IntrinsicId::MathLog2 + | IntrinsicId::MathMax + | IntrinsicId::MathMedian + | IntrinsicId::MathMin + | IntrinsicId::MathMod + | IntrinsicId::MathPi + | IntrinsicId::MathPow + | IntrinsicId::MathRound + | IntrinsicId::MathSin + | IntrinsicId::MathSqrt + | IntrinsicId::MathTan + | IntrinsicId::Print + | IntrinsicId::Println + | IntrinsicId::RandomBool + | IntrinsicId::RandomBounded + | IntrinsicId::RandomUnitFloat64 + | IntrinsicId::Range + | IntrinsicId::SecretCompare + | IntrinsicId::SecretRedact + | IntrinsicId::SetAdd + | IntrinsicId::SetContains + | IntrinsicId::SetLength + | IntrinsicId::SetNew + | IntrinsicId::SetRemove + | IntrinsicId::StdoutWrite + | IntrinsicId::StringCharCount + | IntrinsicId::StringChars + | IntrinsicId::StringCount + | IntrinsicId::StringFromBool + | IntrinsicId::StringFromFloat64 + | IntrinsicId::StringFromInt64 + | IntrinsicId::StringFromUint64 + | IntrinsicId::StringIndexOf + | IntrinsicId::StringIsAlpha + | IntrinsicId::StringIsNumeric + | IntrinsicId::StringJoin + | IntrinsicId::StringLines + | IntrinsicId::StringLower + | IntrinsicId::StringRepeat + | IntrinsicId::StringReplace + | IntrinsicId::StringSlice + | IntrinsicId::StringSlugify + | IntrinsicId::StringSplit + | IntrinsicId::StringToLowerFirst + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringTrimStart + | IntrinsicId::StringUpper + | IntrinsicId::StringWords + | IntrinsicId::TestMockClock + | IntrinsicId::TestMockEnvironment + | IntrinsicId::TestMockRandom + | IntrinsicId::TypeArg + | IntrinsicId::TypeBitfieldFields + | IntrinsicId::TypeBitfieldLayout + | IntrinsicId::TypeConstructFinish + | IntrinsicId::TypeConstructMachineStart + | IntrinsicId::TypeConstructPut + | IntrinsicId::TypeConstructStart + | IntrinsicId::TypeConstructVariantStart + | IntrinsicId::TypeFieldValue + | IntrinsicId::TypeFields + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeMachineLayout + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeMachineStates + | IntrinsicId::TypeMachineTransitions + | IntrinsicId::TypeName + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeVariants + | IntrinsicId::Uint64FromString + | IntrinsicId::UuidNew + }; +} + // --------------------------------------------------------------------------- // Environment // --------------------------------------------------------------------------- @@ -133,16 +462,36 @@ pub type Environment = HashMap; /// the recursive interpreter. #[derive(Debug)] enum Signal { - Return(Value), + Return(Value, Option), + ResourceReturn(EvaluatedValue, Option), Default(Value), + ResourceDefault(EvaluatedValue), Respond(Value), Break, Continue, } +#[derive(Debug, Clone)] +enum ProducedValueStep { + Typed, + Sequence(usize), + MapKey(usize), + MapValue(usize), + Some, + Ok, + Fail, +} + +struct ProducedRefinementCheck { + path: Vec, + requested: TypeExpr, + source: Option, +} + #[derive(Debug)] enum ExprFlow { Value(Value), + Resource(EvaluatedValue), Signal(Signal), } @@ -150,6 +499,12 @@ macro_rules! value_or_signal { ($self:expr, $expr:expr) => { match $self.eval_expr_flow($expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ) + } ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), } }; @@ -273,7 +628,24 @@ struct ReflectionMachineTransition { target: String, } +#[derive(Clone)] +struct RegisteredFunction { + definition: Arc, + namespace: Option, + trusted_stdlib: bool, +} + pub struct Interpreter { + temporary_local_views: Option, + field_update_context: Option, + /// One existing checked entry may start ordinary executable identity. + /// Source callees then require their authenticated caller context. + field_update_entry_allowed: bool, + resource_transport: Option, + retained_resource_program: Option<( + Arc, + crate::ExecutionPurpose, + )>, /// Stack of lexical scopes. The last element is the innermost scope. scopes: Vec, /// Runtime type annotations for variables declared in the matching scope. @@ -281,9 +653,7 @@ pub struct Interpreter { /// Stack of block-scoped namespace aliases introduced by `use`. namespace_alias_scopes: Vec>, /// User-defined functions available for calling. - functions: HashMap>, - /// Function registry entries that came from compiler-shipped stdlib files. - trusted_stdlib_functions: HashSet, + functions: HashMap, /// Registered user-defined structs available for construction and field access. structs: HashMap, /// Registered user-defined bitfields available for construction and field access. @@ -292,6 +662,10 @@ pub struct Interpreter { enums: HashMap, /// Interface dotted name -> concrete runtime type -> concrete dotted function name. interface_methods: HashMap>, + /// Concrete implementation owners requiring identity across interface erasure. + interface_owners: HashSet, + /// Interface names participate in canonical type argument qualification. + interfaces: HashSet, /// Registered type alias base expressions. type_alias_bases: HashMap, /// Registered type aliases: name -> (base_type_name, optional constraint). @@ -302,12 +676,20 @@ pub struct Interpreter { actor_defs: HashMap, /// Active generic type argument substitutions for interpreted generic functions. type_arg_scopes: Vec>, + scoped_type_bindings: Vec, + /// Ordered arguments of the lexical function, retained by nested closures. + current_type_arguments: Vec, /// Namespace of the qualified function body currently executing. current_namespace: Option, + /// First scope belonging to the current function or closure invocation. + /// The initial global environment remains visible across calls. + lexical_scope_floor: usize, /// Whether the current function body came from compiler-shipped stdlib source. current_function_trusted_stdlib: bool, /// Trusted field metadata currently produced by direct `type.fields[T]()` loops. reflected_field_scopes: Vec>, + /// Private selectors for exact checked Source For iterations; never Value authority. + checked_reflected_fields: Vec, /// Trusted TypeInfo metadata currently produced by direct reflected `args` loops. reflected_type_info_scopes: Vec>, /// Trusted TypeVariant metadata currently produced by direct `type.variants[T]()` loops. @@ -318,16 +700,26 @@ pub struct Interpreter { reflection_metadata: Option>, /// Checked expression type names keyed by source span, when supplied by /// the driver after type checking. - checked_expression_types: Option>>, + checked_expression_types: Option>, + active_checked_function: Option>, + active_checked_scope: Option>, + active_reflected_capture_origin: Option, + /// Checked source identities prove invariants only in checked invocation trees. + /// Raw callers may supply aggregate payloads with stale refined children. + allow_checked_refinement_proofs: bool, + breakpoint_exclusions: Arc>>, /// Values produced for explicit `comptime` expressions by the build. - explicit_comptime_values: Option>>, + explicit_comptime_values: Option>, + /// Compiler-produced immutable constants, separate from runtime scopes. + constants: HashMap, + constant_types: HashMap, /// Live actor instances keyed by unique ID. actor_instances: HashMap, /// Next actor instance ID. next_actor_id: u64, - /// Recorded debug output lines (`trace`, `breakpoint`). - debug_output: Vec, - /// Whether debug output should print as the program runs. + /// Ordered compiler-owned debug events, separate from application stdout. + debug_events: Vec, + /// Whether captured events should also emit to diagnostic stderr. emit_runtime_debug: bool, /// Optional captured stdout for driver tests. stdout_capture: Option, @@ -343,221 +735,11 @@ pub struct Interpreter { graphics_session_active: bool, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RandomTestSample { - Bounded(u64), - Unit53(u64), - Boolean(bool), -} - -enum RandomProvider { - Production(StdRng), - Scripted(std::collections::VecDeque), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ClockTestSample { - Wall { - unix_seconds: i128, - subsecond_nanoseconds: u32, - }, - Unavailable, -} - enum ClockProvider { Production, Scripted(std::collections::VecDeque), } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EnvironmentTestText { - Unicode(String), - InvalidUnicode, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EnvironmentTestEntry { - pub name: EnvironmentTestText, - pub value: EnvironmentTestText, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EnvironmentTestSnapshot { - pub arguments: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone)] -enum FrozenLaunchText { - Unicode(String), - InvalidUnicode, -} - -#[derive(Debug, Clone)] -struct FrozenEnvironmentEntry { - name: FrozenLaunchText, - value: FrozenLaunchText, -} - -#[derive(Debug, Clone)] -struct LaunchEnvironmentSnapshot { - arguments: Vec, - entries: Vec, -} - -impl LaunchEnvironmentSnapshot { - fn production() -> Result { - let arguments = std::env::args_os() - .skip(1) - .map(frozen_launch_text_from_os) - .map(|argument| match argument { - FrozenLaunchText::Unicode(argument) => Ok(argument), - FrozenLaunchText::InvalidUnicode => { - Err("Environment: argument is not valid Unicode".to_string()) - } - }) - .collect::, _>>()?; - let entries = std::env::vars_os() - .map(|(name, value)| FrozenEnvironmentEntry { - name: frozen_launch_text_from_os(name), - value: frozen_launch_text_from_os(value), - }) - .collect(); - Ok(Self { arguments, entries }) - } - - fn injected(snapshot: EnvironmentTestSnapshot) -> Result { - let arguments = snapshot - .arguments - .into_iter() - .map(|argument| match argument { - EnvironmentTestText::Unicode(argument) => Ok(argument), - EnvironmentTestText::InvalidUnicode => { - Err("Environment: argument is not valid Unicode".to_string()) - } - }) - .collect::, _>>()?; - let entries = snapshot - .entries - .into_iter() - .map(|entry| FrozenEnvironmentEntry { - name: match entry.name { - EnvironmentTestText::Unicode(name) => FrozenLaunchText::Unicode(name), - EnvironmentTestText::InvalidUnicode => FrozenLaunchText::InvalidUnicode, - }, - value: match entry.value { - EnvironmentTestText::Unicode(value) => FrozenLaunchText::Unicode(value), - EnvironmentTestText::InvalidUnicode => FrozenLaunchText::InvalidUnicode, - }, - }) - .collect(); - Ok(Self { arguments, entries }) - } - - fn get(&self, key: &str) -> Result, String> { - if key.is_empty() || key.contains('=') || key.contains('\0') { - return Err("Environment.get: invalid variable name".to_string()); - } - let matching_entry = self.entries.iter().find(|entry| match &entry.name { - FrozenLaunchText::Unicode(name) => environment_names_equal(name, key), - FrozenLaunchText::InvalidUnicode => false, - }); - match matching_entry.map(|entry| &entry.value) { - None => Ok(None), - Some(FrozenLaunchText::Unicode(value)) => Ok(Some(value.clone())), - Some(FrozenLaunchText::InvalidUnicode) => { - Err("Environment.get: value is not valid Unicode".to_string()) - } - } - } -} - -fn frozen_launch_text_from_os(value: std::ffi::OsString) -> FrozenLaunchText { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStringExt; - return match String::from_utf8(value.into_vec()) { - Ok(value) => FrozenLaunchText::Unicode(value), - Err(_) => FrozenLaunchText::InvalidUnicode, - }; - } - #[cfg(windows)] - { - use std::os::windows::ffi::OsStrExt; - return match String::from_utf16(&value.encode_wide().collect::>()) { - Ok(value) => FrozenLaunchText::Unicode(value), - Err(_) => FrozenLaunchText::InvalidUnicode, - }; - } -} - -fn environment_names_equal(captured: &str, requested: &str) -> bool { - #[cfg(windows)] - { - use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal}; - - let captured = captured.encode_utf16().collect::>(); - let requested = requested.encode_utf16().collect::>(); - let result = unsafe { - CompareStringOrdinal( - captured.as_ptr(), - captured.len() as i32, - requested.as_ptr(), - requested.len() as i32, - 1, - ) - }; - result == CSTR_EQUAL - } - #[cfg(not(windows))] - { - captured == requested - } -} - -fn unbiased_bounded_offset(width: u64, mut next_word: impl FnMut() -> u64) -> u64 { - debug_assert!(width > 0); - let rejection_threshold = width.wrapping_neg() % width; - loop { - let word = next_word(); - if word >= rejection_threshold { - return word % width; - } - } -} - -fn checked_clock_milliseconds(unix_seconds: i128, nanoseconds: u32) -> Result { - if nanoseconds >= 1_000_000_000 { - return Err("Clock.now: invalid test sample".to_string()); - } - let milliseconds = unix_seconds - .checked_mul(1_000) - .and_then(|value| value.checked_add(i128::from(nanoseconds / 1_000_000))) - .ok_or_else(|| "Clock.now: timestamp is outside int64 millisecond range".to_string())?; - i64::try_from(milliseconds) - .map_err(|_| "Clock.now: timestamp is outside int64 millisecond range".to_string()) -} - -fn raw_wall_clock_sample(value: std::time::SystemTime) -> (i128, u32) { - match value.duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => (i128::from(duration.as_secs()), duration.subsec_nanos()), - Err(error) => { - let duration = error.duration(); - let seconds = i128::from(duration.as_secs()); - let nanoseconds = duration.subsec_nanos(); - if nanoseconds == 0 { - (-seconds, 0) - } else { - (-seconds - 1, 1_000_000_000 - nanoseconds) - } - } - } -} - -fn production_wall_clock_sample() -> (i128, u32) { - raw_wall_clock_sample(std::time::SystemTime::now()) -} - /// Runtime state of a spawned actor instance. struct ActorInstance { /// Name of the actor type (e.g. `"Counter"`). @@ -566,38 +748,56 @@ struct ActorInstance { state: HashMap, /// Capability values passed at spawn time. capabilities: HashMap, + checked_origin: Option, } impl Interpreter { /// Create a new interpreter with an empty global scope. pub fn new() -> Self { Self { + temporary_local_views: None, + field_update_context: None, + field_update_entry_allowed: false, + resource_transport: None, + retained_resource_program: None, scopes: vec![HashMap::new()], variable_type_scopes: vec![HashMap::new()], namespace_alias_scopes: vec![HashMap::new()], functions: HashMap::new(), - trusted_stdlib_functions: HashSet::new(), structs: HashMap::new(), bitfields: HashMap::new(), enums: HashMap::new(), interface_methods: HashMap::new(), + interface_owners: HashSet::new(), + interfaces: HashSet::new(), type_alias_bases: HashMap::new(), type_aliases: HashMap::new(), machines: HashMap::new(), actor_defs: HashMap::new(), type_arg_scopes: Vec::new(), + scoped_type_bindings: Vec::new(), + current_type_arguments: Vec::new(), current_namespace: None, + lexical_scope_floor: 0, current_function_trusted_stdlib: false, reflected_field_scopes: Vec::new(), + checked_reflected_fields: Vec::new(), reflected_type_info_scopes: Vec::new(), reflected_variant_scopes: Vec::new(), reflected_machine_state_scopes: Vec::new(), reflection_metadata: None, checked_expression_types: None, + active_checked_function: None, + active_checked_scope: None, + active_reflected_capture_origin: None, + allow_checked_refinement_proofs: true, + breakpoint_exclusions: Arc::new(HashMap::new()), explicit_comptime_values: None, + constants: HashMap::new(), + constant_types: HashMap::new(), actor_instances: HashMap::new(), next_actor_id: 0, - debug_output: Vec::new(), + debug_events: Vec::new(), emit_runtime_debug: false, stdout_capture: None, random_provider: None, @@ -608,7 +808,7 @@ impl Interpreter { } } - /// Create an interpreter that emits debug output during execution. + /// Create an interpreter that captures events and also emits them to stderr. pub fn new_runtime() -> Self { let mut interp = Self::new(); interp.emit_runtime_debug = true; @@ -617,24 +817,19 @@ impl Interpreter { /// Initialize one production generator for this runtime context. pub fn initialize_random_provider(&mut self) -> Result<(), String> { - let mut seed = ::Seed::default(); - rand::rngs::OsRng - .try_fill_bytes(seed.as_mut()) - .map_err(|_| "Random: entropy unavailable".to_string())?; - self.random_provider = Some(RandomProvider::Production(StdRng::from_seed(seed))); + self.random_provider = Some(RandomProvider::production().map_err(str::to_owned)?); Ok(()) } /// Install deterministic normalized samples for runtime conformance tests. pub fn set_random_test_samples(&mut self, samples: Vec) { - self.random_provider = Some(RandomProvider::Scripted(samples.into())); + self.random_provider = Some(RandomProvider::scripted(samples.into())); } pub fn random_test_samples_remaining(&self) -> Option { - match &self.random_provider { - Some(RandomProvider::Scripted(samples)) => Some(samples.len()), - _ => None, - } + self.random_provider + .as_ref() + .and_then(RandomProvider::scripted_samples_remaining) } /// Install the system wall clock for this runtime context. @@ -656,7 +851,8 @@ impl Interpreter { /// Freeze this process's launch data for one runtime context. pub fn initialize_environment_provider(&mut self) -> Result<(), String> { - self.launch_environment = Some(LaunchEnvironmentSnapshot::production()?); + self.launch_environment = + Some(LaunchEnvironmentSnapshot::production().map_err(str::to_owned)?); Ok(()) } @@ -665,7 +861,8 @@ impl Interpreter { &mut self, snapshot: EnvironmentTestSnapshot, ) -> Result<(), String> { - self.launch_environment = Some(LaunchEnvironmentSnapshot::injected(snapshot)?); + self.launch_environment = + Some(LaunchEnvironmentSnapshot::injected(snapshot).map_err(str::to_owned)?); Ok(()) } @@ -675,18 +872,40 @@ impl Interpreter { } /// Attach checked expression type names produced by the typechecker. - pub fn set_checked_expression_types(&mut self, types: Arc>) { + pub fn set_checked_expression_types(&mut self, types: Arc) { self.checked_expression_types = Some(types); } /// Attach values produced for explicit `comptime` expressions by the build. - pub fn set_explicit_comptime_values(&mut self, values: Arc>) { + pub fn set_explicit_comptime_values(&mut self, values: Arc) { self.explicit_comptime_values = Some(values); } - /// Drain any debug lines recorded so far. + /// Drain every debug event in its original execution order. + pub fn take_debug_events(&mut self) -> Vec { + std::mem::take(&mut self.debug_events) + } + + /// Legacy trace/breakpoint line drain. + /// + /// This drains the canonical event buffer and discards Print/Println + /// events. Capture entrypoints must use `take_debug_events` instead. + /// Embedded newlines remain intact; only the observation's final + /// intentional newline is removed from each legacy line. pub fn take_debug_output(&mut self) -> Vec { - std::mem::take(&mut self.debug_output) + self.take_debug_events() + .into_iter() + .filter_map(|event| match event.kind { + DebugEventKind::Trace | DebugEventKind::Breakpoint => { + let mut text = event.text; + if text.ends_with('\n') { + text.pop(); + } + Some(text) + } + DebugEventKind::Print | DebugEventKind::Println => None, + }) + .collect() } /// Capture runtime stdout writes instead of printing them directly. @@ -705,18 +924,41 @@ impl Interpreter { // -- Scope management --------------------------------------------------- fn push_scope(&mut self) { + if let Some(context) = &mut self.temporary_local_views { + context.push_scope(); + } + if let Some(context) = &mut self.field_update_context { + context.push_scope(); + } + if let Some(transport) = &mut self.resource_transport { + transport.push_scope(); + } self.scopes.push(HashMap::new()); self.variable_type_scopes.push(HashMap::new()); self.namespace_alias_scopes.push(HashMap::new()); } fn pop_scope(&mut self) { + if let Some(context) = &mut self.temporary_local_views { + context.pop_scope(); + } + if let Some(context) = &mut self.field_update_context { + context.pop_scope(); + } + if let Some(transport) = &mut self.resource_transport { + transport.pop_scope(); + } self.scopes.pop(); self.variable_type_scopes.pop(); self.namespace_alias_scopes.pop(); } fn set_variable(&mut self, name: &str, value: Value) { + if let Some(context) = &mut self.field_update_context + && let Some(index) = self.scopes.len().checked_sub(1) + { + context.notice_write(index, name); + } if let Some(scope) = self.scopes.last_mut() { scope.insert(name.to_string(), value); } @@ -730,12 +972,23 @@ impl Interpreter { } fn get_variable(&self, name: &str) -> Option<&Value> { - for scope in self.scopes.iter().rev() { + for (index, scope) in self.scopes.iter().enumerate().rev() { + if index != 0 && index < self.lexical_scope_floor { + continue; + } if let Some(v) = scope.get(name) { + if self + .temporary_local_views + .as_ref() + .is_some_and(|context| context.placeholder(index, name)) + { + return None; + } return Some(v); } } - None + self.registry_name(&self.constants, name) + .and_then(|name| self.constants.get(&name)) } /// Locate an existing struct field without cloning its enclosing values. @@ -745,7 +998,8 @@ impl Interpreter { Expr::Ident(ident) => self.get_variable(&ident.name), Expr::Paren(inner, _) | Expr::View(inner, _) => self.borrowed_struct_place(inner), Expr::FieldAccess(base, field, _) => { - let Value::Struct { fields, .. } = self.borrowed_struct_place(base)? else { + let Value::Struct { fields, .. } = self.borrowed_struct_place(base)?.payload() + else { return None; }; fields @@ -757,18 +1011,25 @@ impl Interpreter { } fn get_variable_type(&self, name: &str) -> Option<&TypeExpr> { - for scope in self.variable_type_scopes.iter().rev() { + for (index, scope) in self.variable_type_scopes.iter().enumerate().rev() { + if index != 0 && index < self.lexical_scope_floor { + continue; + } if let Some(ty) = scope.get(name) { return Some(ty); } } - None + self.registry_name(&self.constant_types, name) + .and_then(|name| self.constant_types.get(&name)) } /// Reassign an existing variable in the nearest enclosing scope that /// contains it. Returns `Err` if the variable was never declared. fn assign_variable(&mut self, name: &str, value: Value) -> Result<(), String> { for index in (0..self.scopes.len()).rev() { + if index != 0 && index < self.lexical_scope_floor { + continue; + } if self.scopes[index].contains_key(name) { let ty = self.variable_type_scopes[index].get(name).cloned(); let value = if let Some(ty) = ty { @@ -777,16 +1038,21 @@ impl Interpreter { value }; self.scopes[index].insert(name.to_string(), value); + if let Some(context) = &mut self.field_update_context { + context.notice_write(index, name); + } return Ok(()); } } Err(format!("undefined variable '{name}'")) } - fn emit_debug_line(&mut self, line: String) { - self.debug_output.push(line.clone()); - if self.emit_runtime_debug { - println!("{line}"); + fn emit_debug_event(&mut self, kind: DebugEventKind, text: String) { + self.debug_events.push(DebugEvent { kind, text }); + if self.emit_runtime_debug + && let Some(event) = self.debug_events.last() + { + eprint!("{}", event.text); } } @@ -798,40 +1064,24 @@ impl Interpreter { } } - fn write_stdout_line(&mut self, text: &str) { - if let Some(capture) = self.stdout_capture.as_mut() { - capture.push_str(text); - capture.push('\n'); - } else { - println!("{text}"); - } - } - fn trace_variable(&mut self, name: &str) -> Result<(), String> { let value = self .get_variable(name) .cloned() .ok_or_else(|| format!("undefined variable '{name}'"))?; let label = self.debug_binding_label(name, &value); - self.emit_debug_line(format!("trace {label}")); + self.emit_debug_event(DebugEventKind::Trace, format!("trace {label}\n")); Ok(()) } fn debug_binding_label(&self, name: &str, value: &Value) -> String { - self.get_variable_type(name) - .map(|ty| format!("{name}: {} = {value}", type_expr_display(ty))) + let ty = self.get_variable_type(name); + let value = self.format_debug_value(value, ty); + ty.map(|ty| format!("{name}: {} = {value}", type_expr_display(ty))) .unwrap_or_else(|| format!("{name} = {value}")) } - fn function_namespace(name: &str) -> Option { - name.rsplit_once('.') - .map(|(namespace, _)| namespace.to_string()) - } - fn current_qualified_name(&self, name: &str) -> Option { - if name.contains('.') { - return None; - } self.current_namespace .as_ref() .map(|namespace| format!("{namespace}.{name}")) @@ -860,7 +1110,7 @@ impl Interpreter { }) } - fn use_bound_name(path: &str, alias: Option<&Ident>) -> String { + pub(crate) fn use_bound_name(path: &str, alias: Option<&Ident>) -> String { alias .map(|ident| ident.name.clone()) .unwrap_or_else(|| path.rsplit('.').next().unwrap_or(path).to_string()) @@ -876,38 +1126,134 @@ impl Interpreter { let (prefix, suffix) = name.split_once('.')?; self.namespace_alias_scopes .iter() + .enumerate() .rev() - .find_map(|scope| scope.get(prefix)) + .filter(|(index, _)| *index == 0 || *index >= self.lexical_scope_floor) + .find_map(|(_, scope)| scope.get(prefix)) .map(|target| format!("{target}.{suffix}")) } + fn visible_namespace_aliases(&self) -> HashMap { + let mut aliases = HashMap::new(); + for (index, scope) in self.namespace_alias_scopes.iter().enumerate() { + if index != 0 && index < self.lexical_scope_floor { + continue; + } + aliases.extend( + scope + .iter() + .map(|(name, target)| (name.clone(), target.clone())), + ); + } + aliases + } + + fn with_resolved_type_aliases(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { + let blank_aliases = vec![HashMap::new(); self.namespace_alias_scopes.len()]; + let saved_aliases = std::mem::replace(&mut self.namespace_alias_scopes, blank_aliases); + let result = action(self); + self.namespace_alias_scopes = saved_aliases; + result + } + fn runtime_name(&self, name: &str) -> String { - self.registry_name(&self.functions, name) + self.registry_name(&self.interface_methods, name) + .or_else(|| self.registry_name(&self.functions, name)) + .or_else(|| self.type_alias_method_name(name)) .or_else(|| self.expand_namespace_alias_name(name)) .unwrap_or_else(|| name.to_string()) } - fn hit_breakpoint(&mut self) { + fn type_alias_method_name(&self, name: &str) -> Option { + let (owner, method) = name.rsplit_once('.')?; + let alias = self.registry_name(&self.type_alias_bases, owner)?; + // Refinements have their own implementation identity. + if !matches!(self.type_aliases.get(&alias), Some(None)) { + return None; + } + let owner = self + .declared_interface_alias_target_name(&alias, &mut HashSet::new()) + .unwrap_or_else(|| { + let namespace = + Self::type_name_namespace(&alias).or(self.current_namespace.as_deref()); + let base = self + .substitute_type_expr_in_namespace(&self.type_alias_bases[&alias], namespace); + self.concrete_type_display(&base) + }); + let method = format!("{owner}.{method}"); + (self.functions.contains_key(&method) || self.interface_methods.contains_key(&method)) + .then_some(method) + } + + fn declared_interface_alias_target_name( + &self, + alias: &str, + aliases: &mut HashSet, + ) -> Option { + if !matches!(self.type_aliases.get(alias), Some(None)) || !aliases.insert(alias.to_string()) + { + return None; + } + let TypeExpr::Named(target) = self.type_alias_bases.get(alias)? else { + return None; + }; + // Namespace aliases and type bindings belong to the calling function; + // neither can change the static target of a namespace declaration. + let owner = if target.name.contains('.') { + target.name.clone() + } else { + Self::type_name_namespace(alias) + .map(|namespace| format!("{namespace}.{}", target.name)) + .filter(|name| self.type_name_is_registered(name)) + .unwrap_or_else(|| target.name.clone()) + }; + if self.interfaces.contains(&owner) { + Some(owner) + } else { + self.declared_interface_alias_target_name(&owner, aliases) + } + } + + pub fn set_breakpoint_exclusions(&mut self, exclusions: Arc>>) { + self.breakpoint_exclusions = exclusions; + } + + fn hit_breakpoint(&mut self, span: Span) { let mut bindings = BTreeMap::new(); - for (scope, type_scope) in self.scopes.iter().zip(self.variable_type_scopes.iter()) { + for (index, (scope, type_scope)) in self + .scopes + .iter() + .zip(self.variable_type_scopes.iter()) + .enumerate() + { + if index != 0 && index < self.lexical_scope_floor { + continue; + } for (name, value) in scope { bindings.insert(name.clone(), (value.clone(), type_scope.get(name).cloned())); } } + if let Some(excluded) = self.breakpoint_exclusions.get(&span) { + bindings.retain(|name, _| !excluded.contains(name)); + } if bindings.is_empty() { - self.emit_debug_line("breakpoint hit".to_string()); + self.emit_debug_event(DebugEventKind::Breakpoint, "breakpoint hit\n".into()); return; } let fields: Vec = bindings .into_iter() .map(|(name, (value, ty))| { + let value = self.format_debug_value(&value, ty.as_ref()); ty.map(|ty| format!("{name}: {} = {value}", type_expr_display(&ty))) .unwrap_or_else(|| format!("{name} = {value}")) }) .collect(); - self.emit_debug_line(format!("breakpoint hit: {}", fields.join(", "))); + self.emit_debug_event( + DebugEventKind::Breakpoint, + format!("breakpoint hit: {}\n", fields.join(", ")), + ); } // -- Public scope management (for property-based testing) --------------- @@ -932,17 +1278,24 @@ impl Interpreter { /// Register a function definition so it can be called later. pub fn register_function(&mut self, func: &FunctionDef) { - self.register_function_named(&func.name.name, func, func.span.file.is_stdlib()); + self.register_function_in_namespace(None, func); } - fn register_function_named(&mut self, name: &str, func: &FunctionDef, trusted_stdlib: bool) { - self.functions - .insert(name.to_string(), Arc::new(func.clone())); - if trusted_stdlib { - self.trusted_stdlib_functions.insert(name.to_string()); - } else { - self.trusted_stdlib_functions.remove(name); - } + fn register_function_named( + &mut self, + name: &str, + namespace: Option<&str>, + func: &FunctionDef, + trusted_stdlib: bool, + ) { + self.functions.insert( + name.to_string(), + RegisteredFunction { + definition: Arc::new(func.clone()), + namespace: namespace.map(str::to_string), + trusted_stdlib, + }, + ); } /// Register a function under its canonical runtime name. @@ -951,10 +1304,11 @@ impl Interpreter { match namespace { Some(namespace) => self.register_function_named( &format!("{namespace}.{}", func.name.name), + Some(namespace), func, trusted_stdlib, ), - None => self.register_function_named(&func.name.name, func, trusted_stdlib), + None => self.register_function_named(&func.name.name, None, func, trusted_stdlib), } } @@ -965,13 +1319,29 @@ impl Interpreter { for item in &module.items { Self::update_current_namespace(item, &mut current_file, &mut current_namespace); match item { + Item::VarDecl(decl) => { + if let Some(value) = self + .explicit_comptime_values + .as_ref() + .and_then(|values| values.constant(decl.name.span)) + .cloned() + { + self.register_constant_in_namespace( + current_namespace.as_deref(), + decl, + value, + ); + } + } Item::Function(func) => { self.register_function_in_namespace(current_namespace.as_deref(), func) } Item::TypeAlias(alias) => { self.register_type_alias_in_namespace(current_namespace.as_deref(), alias); } - Item::Interface(interface) => self.register_interface(interface), + Item::Interface(interface) => { + self.register_interface_in_namespace(current_namespace.as_deref(), interface); + } Item::Implement(block) => { self.register_implement_block_in_namespace(current_namespace.as_deref(), block) } @@ -995,6 +1365,21 @@ impl Interpreter { } } + pub(crate) fn register_constant_in_namespace( + &mut self, + namespace: Option<&str>, + declaration: &jett_parser::ast::VarDecl, + value: Value, + ) { + let name = namespace.map_or_else( + || declaration.name.name.clone(), + |namespace| format!("{namespace}.{}", declaration.name.name), + ); + let ty = self.substitute_type_expr_in_namespace(&declaration.ty, namespace); + self.constant_types.insert(name.clone(), ty); + self.constants.insert(name, value); + } + fn item_file(item: &Item) -> FileId { match item { Item::Namespace(ns) => ns.span.file, @@ -1034,24 +1419,23 @@ impl Interpreter { /// Register a user-defined struct so it can be constructed and its methods /// called with dotted syntax like `Point.total(view p)`. pub fn register_struct(&mut self, strukt: &StructDef) { - self.structs - .insert(strukt.name.name.clone(), strukt.clone()); - - for method in &strukt.methods { - self.functions.insert( - format!("{}.{}", strukt.name.name, method.name.name), - Arc::new(method.clone()), - ); - } + self.register_struct_in_namespace(None, strukt); } pub fn register_struct_in_namespace(&mut self, namespace: Option<&str>, strukt: &StructDef) { - if let Some(namespace) = namespace { - let mut qualified = strukt.clone(); - qualified.name.name = format!("{namespace}.{}", strukt.name.name); - self.register_struct(&qualified); - } else { - self.register_struct(strukt); + let owner_name = namespace + .map(|namespace| format!("{namespace}.{}", strukt.name.name)) + .unwrap_or_else(|| strukt.name.name.clone()); + let mut qualified = strukt.clone(); + qualified.name.name = owner_name.clone(); + self.structs.insert(owner_name.clone(), qualified); + for method in &strukt.methods { + self.register_function_named( + &format!("{owner_name}.{}", method.name.name), + namespace, + method, + method.span.file.is_stdlib(), + ); } } @@ -1108,9 +1492,27 @@ impl Interpreter { } } - /// Register an interface declaration. Interfaces carry no runtime state, - /// but keeping the entry point makes module registration symmetric. - pub fn register_interface(&mut self, _interface: &InterfaceDecl) {} + /// Retain interface names and declared dispatch slots before implementations + /// are registered, so methods can also be used as named function values. + pub fn register_interface(&mut self, interface: &InterfaceDecl) { + self.register_interface_in_namespace(None, interface); + } + + pub fn register_interface_in_namespace( + &mut self, + namespace: Option<&str>, + interface: &InterfaceDecl, + ) { + let owner_name = namespace + .map(|namespace| format!("{namespace}.{}", interface.name.name)) + .unwrap_or_else(|| interface.name.name.clone()); + self.interfaces.insert(owner_name.clone()); + for method in &interface.methods { + self.interface_methods + .entry(format!("{owner_name}.{}", method.name.name)) + .or_default(); + } + } /// Register an `implement Interface for Type` block so interface-qualified /// calls can dispatch to the concrete method body at runtime. @@ -1123,24 +1525,49 @@ impl Interpreter { namespace: Option<&str>, block: &ImplementBlock, ) { - let interface_name = block.interface_name.name.clone(); - let local_owner_name = type_expr_name(&block.for_type); - let owner_name = namespace - .filter(|_| !local_owner_name.contains('.')) - .map(|namespace| format!("{namespace}.{local_owner_name}")) - .filter(|qualified| self.structs.contains_key(qualified)) - .unwrap_or(local_owner_name); + let interface_name = self + .expand_type_ident(&block.interface_name, namespace) + .name; + let owner_type = self.substitute_type_expr_in_namespace(&block.for_type, namespace); + let owner_type = self.concrete_type_expr(&owner_type, &mut HashSet::new()); + let concrete_owner = type_expr_display(&owner_type); + let base_owner = concrete_owner.split('[').next().unwrap_or(&concrete_owner); + let owner_name = concrete_owner.clone(); + self.interface_owners.insert(owner_name.clone()); + let base_owner = if self.structs.contains_key(base_owner) { + base_owner + } else { + &owner_name + }; for method in &block.methods { let concrete_name = format!("{}.{}", owner_name, method.name.name); let interface_method_name = format!("{}.{}", interface_name, method.name.name); - - self.functions - .insert(concrete_name.clone(), Arc::new(method.clone())); + let implementation_name = format!("{owner_name} as {interface_method_name}"); + self.register_function_named( + &implementation_name, + namespace, + method, + method.span.file.is_stdlib(), + ); + let has_inherent = self.structs.get(base_owner).is_some_and(|owner| { + owner + .methods + .iter() + .any(|candidate| candidate.name.name == method.name.name) + }); + if !has_inherent { + self.register_function_named( + &concrete_name, + namespace, + method, + method.span.file.is_stdlib(), + ); + } self.interface_methods .entry(interface_method_name) .or_default() - .insert(owner_name.clone(), concrete_name); + .insert(owner_name.clone(), implementation_name); } } @@ -1188,25 +1615,124 @@ impl Interpreter { /// Check a value against a refinement type's constraint. /// Returns `Ok(())` if valid, or `Err(message)` if the constraint fails. fn check_refinement(&mut self, type_name: &str, value: &Value) -> Result<(), String> { + self.check_refinement_from_source(type_name, value, None) + } + + fn check_refinement_from_resolved_source( + &mut self, + type_name: &str, + value: &Value, + source_type: Option<&str>, + ) -> Result<(), String> { + self.with_resolved_type_aliases(|interpreter| { + interpreter.check_refinement_from_source(type_name, value, source_type) + }) + } + + /// Reuse only an original checked expression's established refinement. + /// A runtime `Typed` label is not proof: property shrinking and raw API + /// callers can retain that label while replacing the payload. + fn check_refinement_from_source( + &mut self, + type_name: &str, + value: &Value, + source_type: Option<&str>, + ) -> Result<(), String> { + let source_type = source_type.filter(|_| self.allow_checked_refinement_proofs); let type_name = self .registry_name(&self.type_aliases, type_name) .unwrap_or_else(|| type_name.to_string()); - let constraint_value_ty = self - .type_alias_bases - .get(&type_name) - .cloned() - .map(|base_ty| { - let namespace = - Self::type_name_namespace(&type_name).or(self.current_namespace.as_deref()); - self.substitute_type_expr_in_namespace(&base_ty, namespace) + let mut resolved = value; + while let Value::Pending(inner) = resolved { + resolved = inner; + } + if source_type.is_none() + && matches!(value, Value::Pending(_)) + && matches!(resolved, Value::Typed { type_name: owner, .. } if *owner == type_name) + { + // Preserve the metadata-free API's existing pending behavior. + // Checked boundaries instead use the original expression type, + // including the proof deliberately removed by an explicit coarsen. + return Ok(()); + } + + if !self.type_aliases.contains_key(&type_name) { + return Ok(()); + } + + // Recursion reaches only the destination's actual ancestor chain. + // An unrelated refinement with the same carrier never reaches this + // equality, and an explicit coarsen contributes its checked output. + if source_type == Some(type_name.as_str()) { + return Ok(()); + } + + // Predicates use declaration facts, never the validating caller's + // lexical names or checked generic/reflection specialization. + let saved_namespace = std::mem::replace( + &mut self.current_namespace, + Self::type_name_namespace(&type_name).map(str::to_owned), + ); + let saved_floor = std::mem::replace(&mut self.lexical_scope_floor, self.scopes.len()); + let saved_arguments = std::mem::take(&mut self.current_type_arguments); + let saved_type_scopes = std::mem::take(&mut self.type_arg_scopes); + let saved_bindings = std::mem::take(&mut self.scoped_type_bindings); + let saved_function = self.active_checked_function.take(); + let saved_scope = self.active_checked_scope.take(); + let saved_trusted = self.current_function_trusted_stdlib; + self.current_function_trusted_stdlib = self + .type_aliases + .get(&type_name) + .and_then(Option::as_ref) + .is_some_and(|definition| definition.constraint.span().file.is_stdlib()); + + let saved_proofs = self.allow_checked_refinement_proofs; + self.allow_checked_refinement_proofs &= source_type.is_some(); + let result = self.check_refinement_in_declaration(&type_name, value, source_type); + self.allow_checked_refinement_proofs = saved_proofs; + + self.current_function_trusted_stdlib = saved_trusted; + self.active_checked_scope = saved_scope; + self.active_checked_function = saved_function; + self.scoped_type_bindings = saved_bindings; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_arguments; + self.lexical_scope_floor = saved_floor; + self.current_namespace = saved_namespace; + result + } + + fn check_refinement_in_declaration( + &mut self, + type_name: &str, + value: &Value, + source_type: Option<&str>, + ) -> Result<(), String> { + // A predicate's `value` has the base type. Rechecking a value that + // already carries an interface-backed refinement must not dispatch + // through that refinement again while validating its own predicate. + let coarsened = self + .interfaces + .contains(self.primitive_base_type_name(type_name).as_ref()) + .then(|| self.coarsen_interface_value(value.clone())); + let value = coarsened.as_ref().unwrap_or(value); + + let constraint_value_ty = self + .type_alias_bases + .get(type_name) + .cloned() + .map(|base_ty| { + let namespace = + Self::type_name_namespace(type_name).or(self.current_namespace.as_deref()); + self.qualify_declared_type_expr(&base_ty, namespace) }); if let Some(base_ty) = &constraint_value_ty { let base_type_name = type_expr_name(base_ty); - self.check_refinement(&base_type_name, value)?; + self.check_refinement_from_resolved_source(&base_type_name, value, source_type)?; } - let def = match self.type_aliases.get(&type_name) { + let def = match self.type_aliases.get(type_name) { Some(Some(def)) => def.clone(), Some(None) => return Ok(()), // simple alias, no constraint None => return Ok(()), // not a known type alias @@ -1214,7 +1740,16 @@ impl Interpreter { self.push_scope(); if let Some(base_ty) = constraint_value_ty { - self.set_variable_with_type("value", value.clone(), base_ty); + let predicate_ty = self.resolved_inference_base_type(&base_ty); + let predicate_ty = match predicate_ty { + TypeExpr::Generic(name, mut args, _) + if name.name == "secret" && args.len() == 1 => + { + args.remove(0) + } + ty => ty, + }; + self.set_variable_with_type("value", value.clone(), predicate_ty); } else { self.set_variable("value", value.clone()); } @@ -1245,12 +1780,15 @@ impl Interpreter { &mut self, type_name: &str, value: Value, + source_type: Option<&str>, bind_name: Option<&Ident>, body: &Block, ) -> Result { - match self.check_refinement(type_name, &value) { + match self.check_refinement_from_resolved_source(type_name, &value, source_type) { Ok(()) => Ok(ExprFlow::Value(value)), - Err(message) => self.exec_handle_block(bind_name, Some(Value::String(message)), body), + Err(message) => { + self.exec_handle_block(bind_name, Some(Value::String(message)), body, None) + } } } @@ -1260,7 +1798,34 @@ impl Interpreter { value: Value, ) -> Result { let primitive_type_name = self.primitive_base_type_name(type_name); + let value = if matches!( + primitive_type_name.as_ref(), + "int8" + | "int16" + | "int32" + | "int64" + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "float32" + | "float64" + | "bool" + | "string" + | "bytes" + | "nothing" + ) { + value.into_payload() + } else { + value + }; match primitive_type_name.as_ref() { + // Value::Float64 is an f64 carrier, not permission to retain f64 + // precision in a checked float32 expression or typed boundary. + "float32" => match value { + Value::Float64(value) => Ok(Value::Float64((value as f32) as f64)), + other => Ok(other), + }, "int8" => Self::normalize_sized_integer( &primitive_type_name, value, @@ -1339,7 +1904,7 @@ impl Interpreter { }; let namespace = Self::type_name_namespace(&alias_name).or(self.current_namespace.as_deref()); - let base_ty = self.substitute_type_expr_in_namespace(base_ty, namespace); + let base_ty = self.qualify_declared_type_expr(base_ty, namespace); current = Self::unwrapped_secret_type_expr_name(&base_ty); } @@ -1406,7 +1971,207 @@ impl Interpreter { } fn normalize_value_for_type(&self, ty: &TypeExpr, value: Value) -> Result { - self.normalize_value_for_type_name(&type_expr_name(ty), value) + let name = self.concrete_type_display(ty); + let value = self.normalize_value_for_type_name(&name, value)?; + Ok(self.retain_checked_identity(&name, value)) + } + + fn normalize_value_for_resolved_type( + &mut self, + ty: &TypeExpr, + value: Value, + ) -> Result { + self.with_resolved_type_aliases(|interpreter| { + let name = interpreter.resolved_concrete_type_display(ty); + let value = interpreter.normalize_value_for_type_name(&name, value)?; + Ok(interpreter.retain_checked_identity(&name, value)) + }) + } + + fn normalize_and_validate_value( + &mut self, + ty: &TypeExpr, + value: Value, + ) -> Result { + self.normalize_and_validate_value_from_source(ty, value, None) + } + + fn normalize_and_validate_value_from_source( + &mut self, + ty: &TypeExpr, + value: Value, + source_type: Option<&str>, + ) -> Result { + // Every caller resolves its written target in the owning lexical + // context first. Canonical owners must not pass through local namespace + // aliases again during normalization, predicate lookup, or identity + // retention. Preserve the stack shape for predicates' own imports. + let declaration_aliases = vec![HashMap::new(); self.namespace_alias_scopes.len()]; + let caller_aliases = + std::mem::replace(&mut self.namespace_alias_scopes, declaration_aliases); + let result = (|| { + let ty = self.resolve_installed_type_expr(ty); + let name = self.resolved_concrete_type_display(&ty); + let value = self.normalize_value_for_type_name(&name, value)?; + // A written view describes the access mode, not a separate + // refinement owner. Keep its existing normalization and retained + // identity while checking the underlying declared invariant. + let validation_name = Self::refinement_validation_owner(&name); + let source_type = source_type.map(Self::refinement_validation_owner); + self.check_refinement_from_source(validation_name, &value, source_type)?; + // Attach the destination's nominal identity only after its invariant + // holds, especially when the incoming value is still pending. + Ok(self.retain_checked_identity(&name, value)) + })(); + self.namespace_alias_scopes = caller_aliases; + result + } + + fn refinement_validation_owner(mut name: &str) -> &str { + while let Some(inner) = name.strip_prefix("view ") { + name = inner; + } + name + } + + fn retain_checked_identity(&self, name: &str, value: Value) -> Value { + if !matches!( + self.primitive_base_type_name(name).as_ref(), + "int8" + | "int16" + | "int32" + | "int64" + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "float32" + | "float64" + | "bool" + | "string" + | "bytes" + | "nothing" + ) { + return self.retain_aggregate_identity(name, value); + } + if let Value::Pending(inner) = value { + return Value::Pending(Box::new(self.retain_checked_identity(name, *inner))); + } + if !matches!( + value.payload(), + Value::Int64(_) + | Value::Uint64(_) + | Value::Float64(_) + | Value::Bool(_) + | Value::String(_) + | Value::Bytes(_) + | Value::Nothing + ) { + return value; + } + if value.primitive_carrier_name() == Some(name) { + return value; + } + let name = if matches!( + name, + "int8" | "int16" | "int32" | "uint8" | "uint16" | "uint32" | "float32" + ) { + name.to_owned() + } else { + Self::debug_type(name) + .map(|ty| self.concrete_type_display(&ty)) + .unwrap_or_else(|| name.to_owned()) + }; + let value = value.into_payload(); + if value.primitive_carrier_name() == Some(name.as_str()) { + value + } else { + Value::Typed { + type_name: name, + value: Box::new(value), + } + } + } + + fn retain_aggregate_identity(&self, name: &str, value: Value) -> Value { + if !matches!(value, Value::Typed { .. } | Value::Pending(_)) + && !self.interface_owners.contains(name) + && !self.type_aliases.contains_key(name) + { + return value; + } + let base = self.primitive_base_type_name(name); + if self.interfaces.contains(base.as_ref()) && self.type_name_has_refinement(name) { + if let Value::Pending(inner) = value { + return Value::Pending(Box::new(self.retain_aggregate_identity(name, *inner))); + } + let canonical = Self::debug_type(name) + .map(|ty| self.concrete_type_display(&ty)) + .unwrap_or_else(|| name.to_owned()); + if runtime_type_name(&value).as_deref() == Some(canonical.as_str()) { + return value; + } + // The underlying erased value still needs its own concrete owner + // when the refinement is explicitly coarsened later. + return Value::Typed { + type_name: canonical, + value: Box::new(value), + }; + } + let owner = base.split('[').next().unwrap_or(&base); + let owner = owner + .split_once(" at ") + .map_or(owner, |(machine, _)| machine); + let is_concrete = base.starts_with("function(") + || matches!( + owner, + "list" | "map" | "set" | "optional" | "result" | "TypeConstruction" | "function" + ) + || self.structs.contains_key(owner) + || self.enums.contains_key(owner) + || self.machines.contains_key(owner) + || self.bitfields.contains_key(owner) + || self.actor_defs.contains_key(owner) + || jett_types::CapabilityKind::from_name(owner).is_some(); + // An interface destination retains the incoming concrete owner. A + // concrete conversion replaces it, including covariant containers. + if !is_concrete { + return value; + } + if let Value::Pending(inner) = value { + return Value::Pending(Box::new(self.retain_aggregate_identity(name, *inner))); + } + let canonical = Self::debug_type(name) + .map(|ty| self.concrete_type_display(&ty)) + .unwrap_or_else(|| name.to_owned()); + let value = value.into_payload(); + if self.interface_owners.contains(&canonical) + && runtime_type_name(&value).as_deref() != Some(canonical.as_str()) + { + Value::Typed { + type_name: canonical, + value: Box::new(value), + } + } else { + value + } + } + + fn coarsen_interface_value(&self, value: Value) -> Value { + match value { + Value::Pending(inner) => Value::Pending(Box::new(self.coarsen_interface_value(*inner))), + Value::Typed { type_name, value } + if self.type_name_has_refinement(&type_name) + && self + .interfaces + .contains(self.primitive_base_type_name(&type_name).as_ref()) => + { + // Expression normalization restores the checked ancestor if + // coarsen targets an intermediate refinement in the chain. + self.coarsen_interface_value(*value) + } + value => value, + } } fn type_name_has_refinement(&self, type_name: &str) -> bool { @@ -1418,8 +2183,7 @@ impl Interpreter { Some(None) => self.type_alias_bases.get(&type_name).is_some_and(|base| { let namespace = Self::type_name_namespace(&type_name).or(self.current_namespace.as_deref()); - let base_name = - type_expr_name(&self.substitute_type_expr_in_namespace(base, namespace)); + let base_name = type_expr_name(&self.qualify_declared_type_expr(base, namespace)); self.type_name_has_refinement(&base_name) }), None => false, @@ -1437,11 +2201,17 @@ impl Interpreter { /// Evaluate an expression, returning its [`Value`]. pub fn eval_expr(&mut self, expr: &Expr) -> Result { match self.eval_expr_flow(expr)? { - ExprFlow::Value(value) => Ok(value), + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => Ok(value), + ExprFlow::Value(_) | ExprFlow::Resource(_) => { + Err("raw expression API cannot export live Resource custody".to_string()) + } + ExprFlow::Signal(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("Resource control flow cannot escape raw expression evaluation".to_string()) + } ExprFlow::Signal(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } - ExprFlow::Signal(Signal::Return(_)) => { + ExprFlow::Signal(Signal::Return(..)) => { Err("`return` cannot escape expression evaluation".to_string()) } ExprFlow::Signal(Signal::Break) => { @@ -1469,22 +2239,396 @@ impl Interpreter { result } + /// Evaluate a closed expression with only its lexically visible `use` + /// aliases. The temporary scope is removed even when evaluation fails. + pub(crate) fn eval_expr_in_namespace_with_aliases( + &mut self, + namespace: Option<&str>, + aliases: &HashMap, + expr: &Expr, + ) -> Result { + self.push_scope(); + for (name, target) in aliases { + self.set_namespace_alias(name.clone(), target.clone()); + } + let result = self.eval_expr_in_namespace(namespace, expr); + self.pop_scope(); + result + } + + pub(crate) fn eval_closed_comptime_expression( + &mut self, + namespace: Option<&str>, + aliases: &HashMap, + expression: &Expr, + parameters: &[String], + function: Option>, + checked_scope: Option>, + bindings: Vec, + ) -> Result { + let mut arguments = Vec::new(); + let mut parameter_scope = HashMap::new(); + if let Some(function) = &function { + if function.type_arguments.len() != parameters.len() { + return Err( + "checked comptime type argument arity differs from its declaration".into(), + ); + } + for (index, (parameter, canonical)) in + parameters.iter().zip(&function.type_arguments).enumerate() + { + let name = function + .type_argument_reflections + .get(index) + .map_or(canonical.as_str(), |info| info.type_name.as_str()); + let ty = Self::simple_type_expr_from_name(name, expression.span()) + .ok_or_else(|| format!("cannot reconstruct checked comptime type `{name}`"))?; + arguments.push(ty.clone()); + parameter_scope.insert(parameter.clone(), ty); + } + } + let mut scopes = vec![parameter_scope]; + for binding in &bindings { + let name = binding + .reflection + .as_ref() + .map_or(binding.canonical_name.as_str(), |info| { + info.type_name.as_str() + }); + let ty = Self::simple_type_expr_from_name(name, expression.span()) + .ok_or_else(|| format!("cannot reconstruct checked lexical type `{name}`"))?; + scopes.push(HashMap::from([(binding.name.clone(), ty)])); + } + let saved_arguments = std::mem::replace(&mut self.current_type_arguments, arguments); + let saved_function = std::mem::replace(&mut self.active_checked_function, function); + let saved_scope = std::mem::replace(&mut self.active_checked_scope, checked_scope); + let saved_bindings = std::mem::replace(&mut self.scoped_type_bindings, bindings); + let saved_scopes = std::mem::replace(&mut self.type_arg_scopes, scopes); + let result = self.eval_expr_in_namespace_with_aliases(namespace, aliases, expression); + self.type_arg_scopes = saved_scopes; + self.scoped_type_bindings = saved_bindings; + self.active_checked_scope = saved_scope; + self.active_checked_function = saved_function; + self.current_type_arguments = saved_arguments; + result + } + fn eval_expr_flow(&mut self, expr: &Expr) -> Result { - let flow = self.eval_expr_flow_inner(expr)?; + if self.resource_transport.is_some() { + if let Some(flow) = self.eval_resource_transport(expr)? { + return Ok(flow); + } + } + // Only the original checked call route can return a custody envelope. + // Retain that caller fact before actuals or a callee switch body cursors. + let checked_resource_call = match (&self.resource_transport, expr) { + (Some(transport), Expr::Call(..) | Expr::GenericCall(..)) + if transport.checked_source_active => + { + transport + .checked + .has_invocation(expr.span()) + .map_err(|error| error.to_string())? + } + _ => false, + }; + // Source calls can recurse through many small stdlib functions. Dispatch + // them before entering the general expression match, whose aggregate + // temporaries otherwise remain on the stack throughout each call. + let flow = match expr { + Expr::Call(callee, args, span) => self.eval_call_flow(callee, &[], args, *span), + Expr::GenericCall(callee, type_args, args, span) => { + self.eval_call_flow(callee, type_args, args, *span) + } + _ => self.eval_expr_flow_inner(expr), + }?; match flow { ExprFlow::Value(value) => Ok(ExprFlow::Value( self.normalize_value_for_checked_expr(expr, value)?, )), + ExprFlow::Resource(value) if checked_resource_call => Ok(ExprFlow::Resource(value)), + ExprFlow::Resource(_) => { + Err("Resource transport escaped its checked expression boundary".to_string()) + } ExprFlow::Signal(signal) => Ok(ExprFlow::Signal(signal)), } } - fn normalize_value_for_checked_expr(&self, expr: &Expr, value: Value) -> Result { - let Some(type_name) = self - .checked_expression_types + fn exec_comptime_type_bind( + &mut self, + bind: &jett_parser::ast::ComptimeTypeBindStmt, + ) -> Result, String> { + if self + .resource_transport + .as_ref() + .is_some_and(|transport| transport.checked_source_active) + { + return self.exec_checked_resource_type_bind(bind); + } + let prepared_field_scope = self.checked_field_scope_body(bind)?; + let (bound_type_expr, bound_type, reflection, checked_scope) = if let Some(( + _, + projection, + reflection, + )) = &prepared_field_scope + { + let expression = + Self::simple_type_expr_from_name(&reflection.type_name, bind.value.span()) + .ok_or("checked field scope has no canonical interpreter syntax")?; + ( + expression, + projection.bound_type.clone(), + Some(reflection.clone()), + Some(projection.clone()), + ) + } else { + let bound_type_expr = if let Some(bound_type_expr) = + comptime_type_info_binding(&bind.value) + { + self.substitute_type_expr(bound_type_expr) + } else if let Some((source_ty, index)) = comptime_type_arg_binding(&bind.value) { + let source_ty = self.substitute_type_expr(source_ty); + self.with_resolved_type_aliases(|interpreter| { + interpreter + .checked_type_info_arg_types(&source_ty) + .unwrap_or_else(|| interpreter.type_info_arg_types(&source_ty)) + }) + .get(index) + .cloned() + .ok_or_else(|| { + format!( + "`comptime type` type.arg index {index} is out of range for type '{}'", + type_expr_display(&source_ty) + ) + })? + } else if let Some(field_name) = reflected_field_type_info_binding(&bind.value) { + self.bound_reflected_field_type(field_name)? + } else if let Some(info_name) = reflected_type_info_binding(&bind.value) { + self.bound_reflected_type_info_type(info_name)? + } else { + return Err("`comptime type` currently requires a direct `type.info[T]()` initializer or trusted reflected metadata".to_string()); + }; + let (bound_type, reflection, checked_scope) = + self.with_resolved_type_aliases(|interpreter| { + let bound_type = interpreter.resolved_concrete_type_display(&bound_type_expr); + let reflection = Some(interpreter.reflection_type_argument(&bound_type_expr)?); + let checked_scope = interpreter.checked_scoped_types( + bind.span, + &bound_type, + reflection.as_ref(), + )?; + Ok::<_, String>((bound_type, reflection, checked_scope)) + })?; + (bound_type_expr, bound_type, reflection, checked_scope) + }; + let field_body = prepared_field_scope + .as_ref() + .map(|(reference, _, _)| reference); + let saved_field_cursor = self.enter_checked_field_body(field_body)?; + let saved_checked_scope = std::mem::replace(&mut self.active_checked_scope, checked_scope); + self.scoped_type_bindings.push(ClosureScopedTypeBinding { + name: bind.name.name.clone(), + canonical_name: bound_type, + reflection, + }); + let mut scope = HashMap::new(); + scope.insert(bind.name.name.clone(), bound_type_expr); + self.type_arg_scopes.push(scope); + let result = self.exec_block_inner(&bind.body); + self.active_checked_scope = saved_checked_scope; + self.scoped_type_bindings.pop(); + self.type_arg_scopes.pop(); + let field_restore = self.restore_checked_field_body(saved_field_cursor); + match (result, field_restore) { + (Err(error), _) | (_, Err(error)) => Err(error), + (Ok(value), Ok(())) => Ok(value), + } + } + + fn checked_expression_type(&self, span: Span) -> Option<&String> { + self.active_checked_scope .as_ref() - .and_then(|types| types.get(&expr.span())) + .and_then(|types| types.expressions.get(&span)) + .or_else(|| { + self.active_checked_function + .as_ref() + .and_then(|types| types.expressions.get(&span)) + }) + .or_else(|| { + self.checked_expression_types + .as_ref() + .and_then(|types| types.get(&span)) + }) + } + + fn checked_refinement_source_type(&self, span: Span) -> Option<&String> { + self.allow_checked_refinement_proofs + .then(|| self.checked_expression_type(span)) + .flatten() + } + + fn checked_sum_success_type(source_type: Option<&str>, span: Span) -> Option { + let TypeExpr::Generic(owner, arguments, _) = + Self::simple_type_expr_from_name(source_type?, span)? else { + return None; + }; + match (owner.name.as_str(), arguments.as_slice()) { + ("optional", [success]) | ("result", [success, _]) => Some(type_expr_display(success)), + // Secret and nominal refinements are not transparent sum owners. + _ => None, + } + } + + fn join_preserves_result(&self, expression: &Expr) -> Option { + if let Some(name) = self.checked_expression_type(expression.span()) { + // Checked names are canonical and already select the concrete + // generic/reflected body. Secret and nominal refinement wrappers + // must not be erased when selecting the outer join result. + return Some(matches!( + Self::simple_type_expr_from_name(name, expression.span()), + Some(TypeExpr::Generic(owner, args, _)) + if owner.name == "result" && args.len() == 2 + )); + } + if let Some(ty) = self.call_argument_type(expression) { + return Some(self.declared_join_type_is_result(&ty)); + } + match expression { + Expr::Run(inner, _) + | Expr::Clone(inner, _) + | Expr::Paren(inner, _) + | Expr::View(inner, _) => self.join_preserves_result(inner), + _ => None, + } + } + + fn declared_join_type_is_result(&self, ty: &TypeExpr) -> bool { + let mut ty = self.substitute_type_expr(ty); + let mut aliases = HashSet::new(); + loop { + match ty { + TypeExpr::Generic(owner, args, _) => { + return owner.name == "result" && args.len() == 2; + } + TypeExpr::View(inner, _) => ty = *inner, + TypeExpr::Named(ref name) => { + let Some(alias) = self.registry_name(&self.type_alias_bases, &name.name) else { + return false; + }; + if !matches!(self.type_aliases.get(&alias), Some(None)) + || !aliases.insert(alias.clone()) + { + return false; + } + let Some(base) = self.type_alias_bases.get(&alias) else { + return false; + }; + let namespace = + Self::type_name_namespace(&alias).or(self.current_namespace.as_deref()); + ty = self.substitute_type_expr_in_namespace(base, namespace); + } + TypeExpr::StateQualified(..) | TypeExpr::Function(..) => return false, + } + } + } + + fn checked_scoped_types( + &self, + span: Span, + bound_type: &str, + reflection: Option<&ReflectionTypeInfo>, + ) -> Result>, String> { + let bindings = if let Some(scope) = &self.active_checked_scope { + &scope.bindings + } else if let Some(function) = &self.active_checked_function { + &function.bindings + } else if let Some(types) = &self.checked_expression_types { + &types.bindings + } else { + return Ok(None); + }; + select_scoped_types(bindings, span, bound_type, reflection) + } + + fn reflection_type_argument(&self, ty: &TypeExpr) -> Result { + if let Some(info) = self.checked_type_info(ty) { + return Ok(info.clone()); + } + let name = type_expr_display(ty); + if let Some(info) = self + .scoped_type_bindings + .iter() + .rev() + .filter_map(|binding| binding.reflection.as_ref()) + .find(|info| info.type_name == name) + { + return Ok(info.clone()); + } + // Nested source spellings such as list[Alias] may have no global + // TypeInfo entry. Retain their source metadata for callee selection. + Self::reflection_info_from_value(&self.type_info_value(ty)) + } + + fn captured_type_arguments(&mut self, args: &[TypeExpr]) -> Vec { + self.with_resolved_type_aliases(|interpreter| { + args.iter() + .map(|ty| { + let ty = interpreter.resolve_installed_type_expr(ty); + ClosureTypeArgument { + canonical_name: interpreter.resolved_concrete_type_display(&ty), + reflection: interpreter.reflection_type_argument(&ty).ok(), + ty, + } + }) + .collect() + }) + } + + fn normalize_value_for_checked_expr( + &mut self, + expr: &Expr, + value: Value, + ) -> Result { + self.with_resolved_type_aliases(|interpreter| { + interpreter.normalize_value_for_checked_expr_resolved(expr, value) + }) + } + + fn normalize_value_for_checked_expr_resolved( + &self, + expr: &Expr, + value: Value, + ) -> Result { + if let Some(type_name) = self.resource_companion_expression_name(expr)? { + return self.normalize_resource_companion_expression(expr, &type_name, value); + } + // A live binding also has its invocation's type when a compiler-owned + // facade has no checked source instance. Other expressions use the + // selected body map rather than another generic instantiation. + let mut binding_expr = expr; + while let Expr::View(inner, _) | Expr::Paren(inner, _) = binding_expr { + binding_expr = inner; + } + let binding_type = if let Expr::Ident(ident) = binding_expr { + self.get_variable_type(&ident.name) + .map(|ty| self.resolved_concrete_type_display(ty)) + } else { + None + }; + let checked_type = self + .checked_expression_type(expr.span()) + .map(String::as_str); + // A state guard narrows an expression without changing the binding's + // declared type. Keep that checked owner when the value is erased. + let narrowed_state = binding_type.as_deref().and_then(|binding| { + checked_type.filter(|checked| { + checked + .strip_prefix(binding) + .is_some_and(|suffix| suffix.starts_with(" at ")) + }) + }); + let Some(type_name) = narrowed_state.or(binding_type.as_deref()).or(checked_type) else { return Ok(value); }; if matches!( @@ -1496,20 +2640,142 @@ impl Interpreter { _ ) | Expr::Unary(UnaryOp::Neg, _, _) ) { - self.wrap_integer_value_for_type_name(type_name, value) + self.wrap_integer_value_for_type_name(type_name, value.into_payload()) } else { self.normalize_value_for_type_name(type_name, value) } + .map(|value| self.retain_checked_identity(type_name, value)) } - fn eval_expr_flow_inner(&mut self, expr: &Expr) -> Result { - match expr { - // Literals - Expr::IntLiteral(n, _) => { - if (i64::MIN as i128..=i64::MAX as i128).contains(n) { - Ok(ExprFlow::Value(Value::Int64(*n as i64))) - } else if (0..=u64::MAX as i128).contains(n) { - Ok(ExprFlow::Value(Value::Uint64(*n as u64))) + fn actor_identity_equal(&self, left: u64, right: u64) -> Result { + let left_actor = self + .actor_instances + .get(&left) + .ok_or_else(|| format!("actor equality: unknown actor instance #{left}"))?; + let right_actor = self + .actor_instances + .get(&right) + .ok_or_else(|| format!("actor equality: unknown actor instance #{right}"))?; + if left_actor.type_name != right_actor.type_name { + return Err("actor equality requires matching nominal actor types".into()); + } + Ok(left == right) + } + + fn enum_payload_equal(&mut self, left: &Value, right: &Value) -> Result { + let mut work = vec![(left, right)]; + while let Some((left, right)) = work.pop() { + let (left, right) = (left.payload(), right.payload()); + match (left, right) { + ( + Value::Struct { + type_name: a, + fields: af, + .. + }, + Value::Struct { + type_name: b, + fields: bf, + .. + }, + ) => { + if a != b { + return Ok(false); + } + if self.bitfields.contains_key(a) { + if af.len() != bf.len() { + return Ok(false); + } + for ((an, av), (bn, bv)) in af.iter().zip(bf).rev() { + if an != bn { + return Ok(false); + } + work.push((av, bv)); + } + } else { + let equal = self.call_function_with_type_args( + "Equatable.equals", + &[], + vec![left.clone(), right.clone()], + )?; + match equal.into_payload() { + Value::Bool(true) => {} + Value::Bool(false) => return Ok(false), + _ => return Err("Equatable.equals must return bool".into()), + } + } + } + ( + Value::Enum { + type_name: a, + variant: av, + fields: af, + }, + Value::Enum { + type_name: b, + variant: bv, + fields: bf, + }, + ) + | ( + Value::Machine { + type_name: a, + state: av, + fields: af, + }, + Value::Machine { + type_name: b, + state: bv, + fields: bf, + }, + ) => { + if a != b || av != bv || af.len() != bf.len() { + return Ok(false); + } + work.extend(af.iter().zip(bf).rev()); + } + (Value::List(a), Value::List(b)) | (Value::Set(a), Value::Set(b)) => { + if a.len() != b.len() { + return Ok(false); + } + work.extend(a.iter().zip(b).rev()); + } + (Value::Map(a), Value::Map(b)) => { + if a.len() != b.len() { + return Ok(false); + } + for ((ak, av), (bk, bv)) in a.iter().zip(b).rev() { + work.push((av, bv)); + work.push((ak, bk)); + } + } + (Value::Pending(a), Value::Pending(b)) + | (Value::OptionalSome(a), Value::OptionalSome(b)) + | (Value::ResultOk(a), Value::ResultOk(b)) + | (Value::ResultFail(a), Value::ResultFail(b)) => work.push((a, b)), + (Value::Actor(left), Value::Actor(right)) => { + if !self.actor_identity_equal(*left, *right)? { + return Ok(false); + } + } + _ if left != right => return Ok(false), + _ => {} + } + } + Ok(true) + } + + fn eval_expr_flow_inner(&mut self, expr: &Expr) -> Result { + if let Some(flow) = self.eval_temporary_read(expr)? { + return Ok(flow); + } + match expr { + // Literals + Expr::IntLiteral(n, _) => { + if (i64::MIN as i128..=i64::MAX as i128).contains(n) { + Ok(ExprFlow::Value(Value::Int64(*n as i64))) + } else if (0..=u64::MAX as i128).contains(n) { + Ok(ExprFlow::Value(Value::Uint64(*n as u64))) } else { Err(format!("integer literal '{n}' is out of runtime range")) } @@ -1523,7 +2789,7 @@ impl Interpreter { StringPart::Literal(s) => result.push_str(s), StringPart::Expr(expr) => { let val = value_or_signal!(self, expr); - result.push_str(&val.to_string()); + result.push_str(&self.display_interpolation_value(expr, val)?); } } } @@ -1564,15 +2830,41 @@ impl Interpreter { Expr::Paren(inner, _) => self.eval_expr_flow(inner), Expr::View(inner, _) => self.eval_expr_flow(inner), Expr::Comptime(inner, span) => { - if let Some(value) = self - .explicit_comptime_values - .as_ref() - .and_then(|values| values.get(span)) - { - Ok(ExprFlow::Value(value.clone())) - } else { - self.eval_expr_flow(inner) + if let Some(values) = &self.explicit_comptime_values { + if let Some(transport) = &self.resource_transport { + if transport.checked_source_active { + let identity = transport + .checked + .original_comptime_key(expr) + .map_err(|error| error.to_string())?; + let value = values.checked_get(&identity).cloned() + .ok_or_else(|| "explicit comptime expression has no exact checked original value".to_string())?; + let graph = values.checked_actor_graph(&identity).cloned(); + return graph + .as_deref() + .map_or(Ok(value), |graph| self.instantiate_actor_graph(graph)) + .map(ExprFlow::Value); + } + } + let mut context = crate::ComptimeContext::from_checked( + self.active_checked_function.as_deref(), + ); + context.scoped_type_bindings = self.scoped_type_bindings.clone(); + let value = values.get(*span, &context).cloned() + .ok_or_else(|| "explicit comptime expression has no evaluated value for its checked type context".to_string())?; + let graph = values.actor_graph(*span, &context).cloned(); + if graph.is_none() && actor_graph::contains_actor(&value) { + return Err( + "explicit comptime actor value has no private checked frozen graph" + .into(), + ); + } + return graph + .as_deref() + .map_or(Ok(value), |graph| self.instantiate_actor_graph(graph)) + .map(ExprFlow::Value); } + self.eval_expr_flow(inner) } Expr::Declassify(inner, _) => self.eval_expr_flow(inner), @@ -1582,14 +2874,14 @@ impl Interpreter { // Short-circuit for logical operators match op { BinOp::And => { - if let Value::Bool(false) = left { + if let Value::Bool(false) = left.payload() { return Ok(ExprFlow::Value(Value::Bool(false))); } let right = value_or_signal!(self, rhs); return Ok(ExprFlow::Value(eval_binary_op(&left, *op, &right)?)); } BinOp::Or => { - if let Value::Bool(true) = left { + if let Value::Bool(true) = left.payload() { return Ok(ExprFlow::Value(Value::Bool(true))); } let right = value_or_signal!(self, rhs); @@ -1598,9 +2890,33 @@ impl Interpreter { _ => {} } let right = value_or_signal!(self, rhs); + if matches!(op, BinOp::Eq | BinOp::NotEq) + && let (Value::Actor(left), Value::Actor(right)) = + (left.payload(), right.payload()) + { + let equal = self.actor_identity_equal(*left, *right)?; + return Ok(ExprFlow::Value(Value::Bool(if *op == BinOp::Eq { + equal + } else { + !equal + }))); + } + if matches!(op, BinOp::Eq | BinOp::NotEq) + && matches!( + (left.payload(), right.payload()), + (Value::Enum { .. }, Value::Enum { .. }) + ) + { + let equal = self.enum_payload_equal(&left, &right)?; + return Ok(ExprFlow::Value(Value::Bool(if *op == BinOp::Eq { + equal + } else { + !equal + }))); + } if matches!(op, BinOp::Eq | BinOp::NotEq) && matches!( - (&left, &right), + (left.payload(), right.payload()), (Value::Struct { .. }, Value::Struct { .. }) ) { @@ -1626,7 +2942,7 @@ impl Interpreter { Expr::Unary(op, operand, _) => match op { UnaryOp::Not => { let val = value_or_signal!(self, operand); - match val { + match val.into_payload() { Value::Bool(b) => Ok(ExprFlow::Value(Value::Bool(!b))), _ => Err("'not' requires a boolean operand".to_string()), } @@ -1635,9 +2951,9 @@ impl Interpreter { }, // Function / method calls - Expr::Call(callee, args, _) => self.eval_call_flow(callee, &[], args), - Expr::GenericCall(callee, type_args, args, _) => { - self.eval_call_flow(callee, type_args, args) + Expr::Call(callee, args, span) => self.eval_call_flow(callee, &[], args, *span), + Expr::GenericCall(callee, type_args, args, span) => { + self.eval_call_flow(callee, type_args, args, *span) } // List construction @@ -1660,17 +2976,24 @@ impl Interpreter { } Expr::Handle(target, bind_name, body, _) => { + let target_type = self + .checked_expression_type(target.span()) + .and_then(|name| Self::debug_type(name)) + .or_else(|| self.debug_expression_type(target)) + .map(|ty| self.substitute_type_expr(&ty)); let target_value = value_or_signal!(self, target); - match target_value { + self.check_handle_receiver(&target_value, target_type.as_ref())?; + match target_value.into_payload() { Value::ResultOk(value) => Ok(ExprFlow::Value(*value)), - Value::ResultFail(error) => { - self.exec_handle_block(bind_name.as_ref(), Some(*error), body) - } + Value::ResultFail(error) => self.exec_handle_block( + bind_name.as_ref(), + Some(*error), + body, + self.debug_expression_args(target).get(1), + ), Value::OptionalSome(value) => Ok(ExprFlow::Value(*value)), - Value::OptionalNone => self.exec_handle_block(None, None, body), - other => Err(format!( - "handle block requires a result or optional value, got {other}" - )), + Value::OptionalNone => self.exec_handle_block(None, None, body, None), + _ => unreachable!("sum receiver checked before wrapper consumption"), } } @@ -1685,12 +3008,20 @@ impl Interpreter { // Field access: struct field access, or enum variant like `Color.red` Expr::FieldAccess(obj, field, _) => { - if let Some(name) = Self::dotted_expr_name(expr) { - if let Some(function) = self.registry_name(&self.functions, &name) { + if !self.is_value_call_target(expr) + && let Some(name) = Self::dotted_expr_name(expr) + { + let function = self.runtime_name(&name); + if self.interface_methods.contains_key(&function) { + return Ok(ExprFlow::Value(Value::NamedFunction(function))); + } + if self.functions.contains_key(&function) { return Ok(ExprFlow::Value(Value::NamedFunction(function))); } } - if let Some(owner_name) = Self::dotted_expr_name(obj) { + if !self.is_value_call_target(expr) + && let Some(owner_name) = Self::dotted_expr_name(obj) + { if let Some(enum_name) = self.registry_name(&self.enums, &owner_name) { return Ok(ExprFlow::Value(Value::Enum { type_name: enum_name, @@ -1702,10 +3033,14 @@ impl Interpreter { if let Some(value) = self.borrowed_struct_place(expr) { return Ok(ExprFlow::Value(value.clone())); } + let receiver_type = self + .checked_expression_type(obj.span()) + .and_then(|name| Self::debug_type(name)) + .or_else(|| self.debug_expression_type(obj)); match obj.as_ref() { Expr::Ident(ident) => { if let Some(value) = self.get_variable(&ident.name).cloned() { - self.eval_value_field_access(value, &field.name) + self.eval_value_field_access(value, &field.name, receiver_type.as_ref()) } else { // Treat as enum variant: Type.variant if let Some(enum_name) = self.registry_name(&self.enums, &ident.name) { @@ -1725,27 +3060,43 @@ impl Interpreter { } _ => { let base = value_or_signal!(self, obj); - self.eval_value_field_access(base, &field.name) + self.eval_value_field_access(base, &field.name, receiver_type.as_ref()) } } } - // Coarsen: strip refinement type, returning the underlying value. - // In the interpreter, the value is already the base type at - // runtime, so coarsen is a no-op. - Expr::Coarsen(inner, _) => self.eval_expr_flow(inner), + // Preserve the underlying erased owner when removing refinement + // metadata. Checked normalization restores a requested ancestor. + Expr::Coarsen(inner, _) => { + let value = value_or_signal!(self, inner); + Ok(ExprFlow::Value(self.coarsen_interface_value(value))) + } // Pipeline: `expr into f into g(extra)` // Evaluate the initial expression, then for each step, call the // function with the accumulated value as the first argument plus // any extra args. Expr::Pipeline(initial, steps, _) => { + let mut value_type = self.debug_expression_type(initial); let mut value = value_or_signal!(self, initial); for step in steps { - value = match self.eval_pipeline_step(&step, value)? { + let result_type = self.debug_pipeline_result_type(step, value_type.as_ref()); + let args = self.debug_type_args(result_type.as_ref()); + value = match self.eval_pipeline_step( + &step, + value, + result_type.as_ref(), + args.get(1), + )? { ExprFlow::Value(next) => next, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), }; + value_type = if step.handle.is_some() { + args.first().cloned() + } else { + result_type + }; } Ok(ExprFlow::Value(value)) } @@ -1753,11 +3104,11 @@ impl Interpreter { // State check: `expr at state_name` Expr::At(expr, state_name, _) => { let val = value_or_signal!(self, expr); - match val { + match val.into_payload() { Value::Machine { state, .. } => { Ok(ExprFlow::Value(Value::Bool(state == state_name.name))) } - _ => Err(format!("'at' requires a machine value, got {val}")), + other => Err(format!("'at' requires a machine value, got {other}")), } } @@ -1766,88 +3117,9 @@ impl Interpreter { self.eval_expr_flow(inner) } - Expr::Spawn(inner, _) => { - // `spawn ActorType(cap1: val1, ...)` — create a new actor instance. - let (source_actor_name, args) = match inner.as_ref() { - Expr::Call(callee, args, _) => match callee.as_ref() { - Expr::Ident(ident) => (ident.name.clone(), args), - Expr::FieldAccess(_, _, _) => { - let name = Self::dotted_expr_name(callee) - .ok_or_else(|| "spawn: expected actor type name".to_string())?; - (name, args) - } - _ => return Err("spawn: expected actor type name".to_string()), - }, - _ => return Err("spawn: expected call expression".to_string()), - }; - let actor_name = self - .registry_name(&self.actor_defs, &source_actor_name) - .unwrap_or(source_actor_name); - - let actor_def = self - .actor_defs - .get(&actor_name) - .ok_or_else(|| format!("unknown actor type '{actor_name}'"))? - .clone(); - - // Evaluate capability args. - let mut capabilities = HashMap::new(); - let parameter_indices = - Self::actor_argument_parameters(args, &actor_def.capability_params)?; - for (arg, index) in args.iter().zip(parameter_indices) { - let param = &actor_def.capability_params[index]; - let val = value_or_signal!(self, &arg.value); - let param_ty = self.substitute_type_expr(¶m.ty); - let val = self.normalize_value_for_type(¶m_ty, val)?; - capabilities.insert(param.name.name.clone(), val); - } - - // Evaluate state field initializers in a temp scope with capabilities in scope. - self.push_scope(); - for (name, val) in &capabilities { - if let Some(param) = actor_def - .capability_params - .iter() - .find(|param| param.name.name == *name) - { - let param_ty = self.substitute_type_expr(¶m.ty); - self.set_variable_with_type(name, val.clone(), param_ty); - } else { - self.set_variable(name, val.clone()); - } - } - let mut state = HashMap::new(); - for field in &actor_def.state_fields { - let val = value_or_signal!(self, &field.value); - let field_ty = self.substitute_type_expr(&field.ty); - let val = self.normalize_value_for_type(&field_ty, val)?; - state.insert(field.name.name.clone(), val); - } - self.pop_scope(); - - let id = self.next_actor_id; - self.next_actor_id += 1; - self.actor_instances.insert( - id, - ActorInstance { - type_name: actor_name.clone(), - state, - capabilities, - }, - ); - - Ok(ExprFlow::Value(Value::Actor(id))) - } - - Expr::Send(inner, _) => { - self.eval_actor_message(inner, false)?; - Ok(ExprFlow::Value(Value::Nothing)) - } + Expr::Spawn(_, _) => self.eval_spawn_flow(expr), - Expr::Ask(inner, _) => { - let val = self.eval_actor_message(inner, true)?; - Ok(ExprFlow::Value(val)) - } + Expr::Send(_, _) | Expr::Ask(_, _) => self.eval_actor_message_flow(expr), // Structured concurrency — sequential simulation: // `run call` evaluates immediately and wraps in Pending. @@ -1859,16 +3131,34 @@ impl Interpreter { // `join pending` unwraps the Pending, returning result[T, error] // so that a `handle error:` block can handle failures. Expr::Join(inner, _) => { + let preserves_result = self.join_preserves_result(inner); let val = value_or_signal!(self, inner); + let was_pending = matches!(&val, Value::Pending(_)); let result = match val { - Value::Pending(inner_val) => match *inner_val { - Value::ResultOk(_) | Value::ResultFail(_) => *inner_val, - other => Value::ResultOk(Box::new(other)), - }, Value::Nothing => { Value::ResultFail(Box::new(Value::String("task was cancelled".to_string()))) } - other => Value::ResultOk(Box::new(other)), + other => { + let joined = match other { + Value::Pending(value) => *value, + value => value, + }; + // Raw interpreter callers may provide neither checked + // nor declared types. Preserve their historical fallback; + // checked source never selects behavior from its payload. + let preserves_result = preserves_result.unwrap_or_else(|| { + was_pending + && matches!( + joined.payload(), + Value::ResultOk(_) | Value::ResultFail(_) + ) + }); + if preserves_result { + joined + } else { + Value::ResultOk(Box::new(joined)) + } + } }; Ok(ExprFlow::Value(result)) } @@ -1880,21 +3170,9 @@ impl Interpreter { Ok(ExprFlow::Value(Value::Nothing)) } - Expr::InlineFn(params, _return_type, body, _) => { - // Capture the current environment (all visible variables) for closure semantics. - let mut captures = HashMap::new(); - for scope in &self.scopes { - for (name, value) in scope { - captures.insert(name.clone(), value.clone()); - } - } - Ok(ExprFlow::Value(Value::Function { - params: params.clone(), - body: body.clone(), - captures, - namespace: self.current_namespace.clone(), - })) - } + Expr::InlineFn(params, return_type, body, _) => Ok(ExprFlow::Value( + self.capture_closure(params, return_type.as_ref(), body)?, + )), // Unsupported expressions produce a clear error. _ => Err(format!( @@ -1904,6 +3182,191 @@ impl Interpreter { } } + fn eval_spawn_flow_unchecked(&mut self, inner: &Expr) -> Result { + // `spawn ActorType(cap1: val1, ...)` — create a new actor instance. + let (source_actor_name, args) = match inner { + Expr::Call(callee, args, _) => match callee.as_ref() { + Expr::Ident(ident) => (ident.name.clone(), args), + Expr::FieldAccess(_, _, _) => { + let name = Self::dotted_expr_name(callee) + .ok_or_else(|| "spawn: expected actor type name".to_string())?; + (name, args) + } + _ => return Err("spawn: expected actor type name".to_string()), + }, + _ => return Err("spawn: expected call expression".to_string()), + }; + let actor_name = self + .registry_name(&self.actor_defs, &source_actor_name) + .unwrap_or(source_actor_name); + + let actor_def = self + .actor_defs + .get(&actor_name) + .ok_or_else(|| format!("unknown actor type '{actor_name}'"))? + .clone(); + + let checked_initializers = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + .map(|transport| { + transport + .checked + .prepare_actor_initializers(&actor_def) + .map_err(|error| error.to_string()) + }) + .transpose()?; + + // Evaluate capability args. + let mut capabilities = HashMap::new(); + let parameter_indices = + Self::actor_argument_parameters(args, &actor_def.capability_params)?; + for (arg, index) in args.iter().zip(parameter_indices) { + let param = &actor_def.capability_params[index]; + let val = value_or_signal!(self, &arg.value); + let param_ty = self.substitute_type_expr(¶m.ty); + let val = self.normalize_value_for_type(¶m_ty, val)?; + capabilities.insert(param.name.name.clone(), val); + } + + // Evaluate state field initializers in a temp scope with capabilities in scope. + self.push_scope(); + for (name, val) in &capabilities { + if let Some(param) = actor_def + .capability_params + .iter() + .find(|param| param.name.name == *name) + { + let param_ty = self.substitute_type_expr(¶m.ty); + self.set_variable_with_type(name, val.clone(), param_ty); + } else { + self.set_variable(name, val.clone()); + } + } + let mut state = HashMap::new(); + for (field_index, field) in actor_def.state_fields.iter().enumerate() { + let val = if let Some(references) = &checked_initializers { + match self.eval_checked_actor_initializer(&references[field_index])? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), + ExprFlow::Resource(_) => { + return Err("actor Resource custody has no checked transport proof".into()); + } + } + } else { + value_or_signal!(self, &field.value) + }; + let field_ty = self.substitute_type_expr(&field.ty); + let val = self.normalize_value_for_type(&field_ty, val)?; + state.insert(field.name.name.clone(), val); + } + self.pop_scope(); + + let id = self.next_actor_id; + self.next_actor_id += 1; + self.actor_instances.insert( + id, + ActorInstance { + type_name: actor_name.clone(), + state, + capabilities, + checked_origin: None, + }, + ); + + Ok(ExprFlow::Value(Value::Actor(id))) + } + + fn capture_closure( + &mut self, + params: &[Param], + return_type: Option<&TypeExpr>, + body: &Block, + ) -> Result { + // Opaque carriers require custody before any capture copy. + // Ordinary lexical captures preserve their existing semantics. + let mut captures = HashMap::new(); + for (index, scope) in self.scopes.iter().enumerate() { + if index != 0 && index < self.lexical_scope_floor { + continue; + } + for (name, value) in scope { + if value.contains_live_resource_or_grant() { + let referenced = self + .resource_transport + .as_ref() + .and_then(|transport| { + transport.captured_resource_reference(name, body.span) + }) + .unwrap_or_else(|| { + resource_capture::raw_closure_uses_binding(params, body, name) + }); + if referenced { + return Err( + "opaque capture has no checked custody transport proof".to_string() + ); + } + continue; + } + captures.insert(name.clone(), value.clone()); + } + } + if let Some((checked, purpose)) = &self.retained_resource_program + && matches!( + purpose, + crate::ExecutionPurpose::ExplicitComptime + | crate::ExecutionPurpose::NamespaceConstant + ) + { + crate::value::closure_capture::validate_required_capture_environment( + checked, + params, + return_type, + body, + &captures, + )?; + } + let capture_types = captures + .keys() + .filter_map(|name| { + self.get_variable_type(name) + .map(|ty| (name.clone(), ty.clone())) + }) + .collect(); + let type_arguments = self.current_type_arguments.clone(); + let captured_arguments = self.captured_type_arguments(&type_arguments); + let mut value = Value::Function { + type_context: Box::new(ClosureTypeContext { + return_type: return_type.cloned(), + checked_scope: self.active_checked_scope.clone(), + scoped_type_bindings: self.scoped_type_bindings.clone(), + checked_function: self.active_checked_function.clone(), + bindings: self + .type_arg_scopes + .iter() + .flat_map(|scope| scope.iter()) + .map(|(name, ty)| (name.clone(), self.resolve_installed_type_expr(ty))) + .collect(), + arguments: captured_arguments, + reflected_capture: None, + }), + params: params.to_vec(), + body: body.clone(), + captures, + capture_types, + namespace_aliases: self.visible_namespace_aliases(), + namespace: self.current_namespace.clone(), + }; + if let Some(origin) = &self.active_reflected_capture_origin { + let proof = crate::value::closure_capture::CheckedClosureCapture::seal(origin, &value); + if let Value::Function { type_context, .. } = &mut value { + type_context.reflected_capture = proof; + } + } + Ok(value) + } + fn eval_negation_flow(&mut self, operand: &Expr) -> Result { if let Expr::IntLiteral(value, _) = operand { let negated = value @@ -1918,7 +3381,7 @@ impl Interpreter { } let val = value_or_signal!(self, operand); - match val { + match val.into_payload() { Value::Int64(n) => Ok(ExprFlow::Value(Value::Int64(n.wrapping_neg()))), Value::Float64(n) => Ok(ExprFlow::Value(Value::Float64(-n))), _ => Err("unary '-' requires a numeric operand".to_string()), @@ -1952,50 +3415,85 @@ impl Interpreter { args: &[CallArg], piped: bool, ) -> Result>, String> { - if args.iter().all(|arg| arg.name.is_none()) { + let callee = Self::unparenthesized(callee); + if self.is_value_call_target(callee) || args.iter().all(|arg| arg.name.is_none()) { return Ok(None); } - let Some(source_name) = Self::dotted_expr_name(callee) else { - return Ok(None); - }; - let name = self.runtime_name(&source_name); - let Some(function) = self.functions.get(&name) else { + let Some((callable, parameter_names)) = self.source_call_parameters(callee) else { return Ok(None); }; let offset = usize::from(piped); - if function.params.len() != args.len() + offset { + if parameter_names.len() != args.len() + offset { return Err(format!( - "function '{name}' argument count does not match parameter count" + "{callable} argument count does not match parameter count" )); } - let mut order = vec![None; function.params.len()]; + let mut order = vec![None; parameter_names.len()]; if piped { order[0] = Some(0); } for (index, arg) in args.iter().enumerate() { let source_index = index + offset; let parameter_index = match &arg.name { - Some(argument_name) => function - .params + Some(argument_name) => parameter_names .iter() - .position(|param| param.name.name == argument_name.name) + .position(|param| *param == argument_name.name) .ok_or_else(|| { - format!( - "unknown argument '{}' for function '{name}'", - argument_name.name - ) + format!("unknown argument '{}' for {callable}", argument_name.name) })?, - None => source_index, + None => order + .iter() + .position(Option::is_none) + .ok_or_else(|| format!("too many arguments for {callable}"))?, }; if order[parameter_index].replace(source_index).is_some() { - return Err(format!("duplicate argument for function '{name}'")); + return Err(format!("duplicate argument for {callable}")); } } order .into_iter() .collect::>>() .map(Some) - .ok_or_else(|| format!("missing argument for function '{name}'")) + .ok_or_else(|| format!("missing argument for {callable}")) + } + + fn source_call_parameters(&self, callee: &Expr) -> Option<(String, Vec<&str>)> { + if let Some(source_name) = Self::dotted_expr_name(callee) { + let name = self.runtime_name(&source_name); + if let Some(function) = self.functions.get(&name) { + return Some(( + format!("function '{name}'"), + function + .definition + .params + .iter() + .map(|param| param.name.name.as_str()) + .collect(), + )); + } + } + let (owner, variant_name) = match callee { + Expr::EnumVariant(owner, variant, _) => (owner.name.clone(), variant.name.as_str()), + Expr::FieldAccess(owner, variant, _) => { + (Self::dotted_expr_name(owner)?, variant.name.as_str()) + } + _ => return None, + }; + let enum_name = self.registry_name(&self.enums, &owner)?; + let variant = self + .enums + .get(&enum_name)? + .variants + .iter() + .find(|variant| variant.name.name == variant_name)?; + Some(( + format!("enum variant '{enum_name}.{variant_name}'"), + variant + .fields + .iter() + .map(|field| field.name.name.as_str()) + .collect(), + )) } fn reorder_function_arguments( @@ -2026,8 +3524,8 @@ impl Interpreter { let function = self.generic_function_for_inference(callee, type_args)?; let mut inferred = HashMap::new(); for (param, actual) in function.params.iter().zip(actual_types) { - let actual = self.inference_base_type(actual); - Self::infer_type_arguments(¶m.ty, &actual, &function.type_params, &mut inferred); + let actual = self.resolved_inference_base_type(actual); + self.infer_type_arguments(¶m.ty, &actual, &function.type_params, &mut inferred); } function .type_params @@ -2041,7 +3539,8 @@ impl Interpreter { callee: &Expr, type_args: &[TypeExpr], ) -> Option<&FunctionDef> { - if !type_args.is_empty() { + let callee = Self::unparenthesized(callee); + if self.is_value_call_target(callee) || !type_args.is_empty() { return None; } let source_name = match callee { @@ -2050,51 +3549,53 @@ impl Interpreter { _ => None, }?; let function_name = self.runtime_name(&source_name); - let function = self.functions.get(&function_name)?; + let function = &self.functions.get(&function_name)?.definition; (!function.type_params.is_empty()).then_some(function.as_ref()) } - fn inference_base_type(&self, ty: &TypeExpr) -> TypeExpr { - let mut current = self.substitute_type_expr(ty); + fn resolved_inference_base_type(&self, ty: &TypeExpr) -> TypeExpr { + let mut current = ty.clone(); let mut seen = HashSet::new(); loop { let TypeExpr::Named(ident) = ¤t else { return current; }; - let Some(alias_name) = self.registry_name(&self.type_alias_bases, &ident.name) else { + let alias_name = ident.name.clone(); + let Some(base) = self.type_alias_bases.get(&alias_name) else { return current; }; if !seen.insert(alias_name.clone()) { return current; } - let Some(base) = self.type_alias_bases.get(&alias_name) else { - return current; - }; let namespace = Self::type_name_namespace(&alias_name).or(self.current_namespace.as_deref()); - current = self.substitute_type_expr_in_namespace(base, namespace); + current = self.qualify_declared_type_expr(base, namespace); } } fn call_argument_type(&self, expression: &Expr) -> Option { - match expression { + let source = Self::type_witness_source(expression); + match source { Expr::Ident(argument) if self.get_variable_type(&argument.name).is_some() => { self.get_variable_type(&argument.name).cloned() } - Expr::InlineFn(params, return_type, _, span) => Some(TypeExpr::Function( - params.iter().map(|param| param.ty.clone()).collect(), - Box::new(return_type.clone().unwrap_or_else(|| { - TypeExpr::Named(Ident { - name: "nothing".to_string(), - span: *span, - }) - })), - *span, - )), - _ => self.named_function_argument_type(expression).or_else(|| { - self.checked_expression_types - .as_ref() - .and_then(|types| types.get(&expression.span())) + Expr::InlineFn(params, return_type, _, span) => Some( + self.substitute_type_expr(&TypeExpr::Function( + params + .iter() + .map(|param| Self::type_witness_parameter(param, param.ty.clone())) + .collect(), + Box::new(return_type.clone().unwrap_or_else(|| { + TypeExpr::Named(Ident { + name: "nothing".to_string(), + span: *span, + }) + })), + *span, + )), + ), + _ => self.named_function_argument_type(source).or_else(|| { + self.checked_expression_type(expression.span()) .and_then(|type_name| { Self::simple_type_expr_from_name(type_name, expression.span()) }) @@ -2103,29 +3604,34 @@ impl Interpreter { } fn named_function_argument_type(&self, expression: &Expr) -> Option { - if let Expr::Ident(ident) = expression - && self.get_variable(&ident.name).is_some() - { + let expression = Self::type_witness_source(expression); + if self.is_value_call_target(expression) { // Captures can omit runtime type metadata and shadow a namespaced // function. Their checked expression type remains authoritative. return None; } let source_name = Self::dotted_expr_name(expression)?; let name = self.registry_name(&self.functions, &source_name)?; - let function = self.functions.get(&name)?; + let registered = self.functions.get(&name)?; + let function = ®istered.definition; if !function.type_params.is_empty() { return None; } - let namespace = Self::function_namespace(&name); + let namespace = registered.namespace.as_deref(); let params = function .params .iter() - .map(|param| self.substitute_type_expr_in_namespace(¶m.ty, namespace.as_deref())) + .map(|param| { + Self::type_witness_parameter( + param, + self.qualify_declared_type_expr(¶m.ty, namespace), + ) + }) .collect(); let return_type = function .return_type .as_ref() - .map(|ty| self.substitute_type_expr_in_namespace(ty, namespace.as_deref())) + .map(|ty| self.qualify_declared_type_expr(ty, namespace)) .unwrap_or_else(|| { TypeExpr::Named(Ident { name: "nothing".to_string(), @@ -2140,6 +3646,7 @@ impl Interpreter { } fn infer_type_arguments( + &self, expected: &TypeExpr, actual: &TypeExpr, type_params: &[Ident], @@ -2149,38 +3656,69 @@ impl Interpreter { (TypeExpr::Named(expected), actual) if type_params.iter().any(|param| param.name == expected.name) => { - inferred - .entry(expected.name.clone()) - .or_insert_with(|| actual.clone()); + match inferred.get(&expected.name) { + Some(previous) => { + if let Some(merged) = self.merge_inferred_never(previous, actual, false) { + inferred.insert(expected.name.clone(), merged); + } + } + None => { + inferred.insert(expected.name.clone(), actual.clone()); + } + } } ( TypeExpr::Generic(expected_owner, expected_args, _), TypeExpr::Generic(actual_owner, actual_args, _), ) if expected_owner.name == actual_owner.name => { for (expected, actual) in expected_args.iter().zip(actual_args) { - Self::infer_type_arguments(expected, actual, type_params, inferred); + self.infer_type_arguments(expected, actual, type_params, inferred); } } + (TypeExpr::View(expected, _), TypeExpr::View(actual, _)) => { + self.infer_type_arguments(expected, actual, type_params, inferred); + } (TypeExpr::View(expected, _), actual) => { - Self::infer_type_arguments(expected, actual, type_params, inferred); + self.infer_type_arguments(expected, actual, type_params, inferred); } (expected, TypeExpr::View(actual, _)) => { - Self::infer_type_arguments(expected, actual, type_params, inferred); + self.infer_type_arguments(expected, actual, type_params, inferred); } ( TypeExpr::Function(expected_params, expected_return, _), TypeExpr::Function(actual_params, actual_return, _), ) => { for (expected, actual) in expected_params.iter().zip(actual_params) { - Self::infer_type_arguments(expected, actual, type_params, inferred); + self.infer_type_arguments(expected, actual, type_params, inferred); } - Self::infer_type_arguments(expected_return, actual_return, type_params, inferred); + self.infer_type_arguments(expected_return, actual_return, type_params, inferred); } _ => {} } } fn simple_type_expr_from_name(type_name: &str, span: Span) -> Option { + if let Some(inner) = type_name.strip_prefix("view ") { + return Some(TypeExpr::View( + Box::new(Self::simple_type_expr_from_name(inner, span)?), + span, + )); + } + if let Some((params, result)) = Self::split_function_type_display(type_name) { + let params = if params.is_empty() { + Vec::new() + } else { + Self::split_type_display_args(params) + .into_iter() + .map(|param| Self::simple_type_expr_from_name(param.trim(), span)) + .collect::>>()? + }; + return Some(TypeExpr::Function( + params, + Box::new(Self::simple_type_expr_from_name(result, span)?), + span, + )); + } if let Some((owner, args)) = Self::split_generic_type_display(type_name) { let args = Self::split_type_display_args(args) .into_iter() @@ -2201,16 +3739,56 @@ impl Interpreter { })) } - fn eval_call_flow( + fn unparenthesized(mut expression: &Expr) -> &Expr { + while let Expr::Paren(inner, _) = expression { + expression = inner; + } + expression + } + + /// Locals and computed expressions denote values; an unbound dotted root + /// can instead denote a namespace, type, or registered function. + fn is_value_call_target(&self, expression: &Expr) -> bool { + match expression { + Expr::Ident(ident) => self.get_variable(&ident.name).is_some(), + Expr::FieldAccess(base, _, _) => self.is_value_call_target(base), + Expr::EnumVariant(_, _, _) => false, + _ => true, + } + } + + fn eval_call_flow( &mut self, callee: &Expr, type_args: &[TypeExpr], args: &[CallArg], + call_span: Span, ) -> Result { + if let Some(transport) = &self.resource_transport { + if transport.checked_source_active { + if transport + .checked + .source_call_dispatch(callee, args, call_span) + .map_err(|error| error.to_string())? + { + return self.eval_resource_call(callee, type_args, args, call_span); + } + } + } + if let Some(flow) = self.eval_temporary_call(callee, type_args, args, call_span)? { + return Ok(flow); + } + // Only a fact at this actual result can be reused after the call; an + // unrelated installed map must not alter metadata-free API validation. + let checked_producer = self.checked_refinement_source_type(call_span).is_some(); + let checked_field_owner = + self.checked_field_intrinsic_owner(callee, type_args, args, call_span)?; + let callee = Self::unparenthesized(callee); // Check for machine construction/transition BEFORE evaluating args, // since state-name arguments are bare identifiers (not variables) and // would fail evaluation. match callee { + _ if self.is_value_call_target(callee) => {} Expr::Ident(ident) if self.registry_name(&self.structs, &ident.name).is_some() => {} Expr::Ident(ident) => { if let Some(machine_name) = self.registry_name(&self.machines, &ident.name) { @@ -2248,20 +3826,77 @@ impl Interpreter { args, argument_order.as_deref(), ); - let type_args = inferred_type_args.as_deref().unwrap_or(type_args); - let mut arg_values = Vec::with_capacity(args.len()); - for arg in args { - arg_values.push(value_or_signal!(self, &arg.value)); + // Constructor and intrinsic parameters belong to the selected call. Resolve + // actual arguments in the caller before evaluating nested expressions. + let mut resolved_type_args = inferred_type_args.unwrap_or_else(|| { + type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect() + }); + if let Some(owner) = checked_field_owner { + let selected = resolved_type_args + .first_mut() + .ok_or("checked field intrinsic lost its selected owner operand")?; + *selected = owner; + } + // Capture caller facts before evaluating any argument or entering a + // nested generic/reflected body. Reorder each witness with its value; + // destination parameter types never supply caller proof. + let debug_print_kind = self.debug_print_call_kind(callee); + let debug_argument_types = if debug_print_kind.is_some() { + Self::reorder_function_arguments( + args.iter() + .map(|arg| self.debug_print_expression_type(&arg.value)) + .collect(), + argument_order.as_deref(), + )? + } else { + Vec::new() + }; + let argument_source_types: Vec<_> = args + .iter() + .map(|arg| { + self.checked_refinement_source_type(arg.value.span()) + .cloned() + }) + .collect(); + let mut evaluated_args = Vec::with_capacity(args.len()); + for (arg, source_type) in args.iter().zip(argument_source_types) { + evaluated_args.push((value_or_signal!(self, &arg.value), source_type)); + } + let (arg_values, argument_source_types): (Vec<_>, Vec<_>) = + Self::reorder_function_arguments(evaluated_args, argument_order.as_deref())? + .into_iter() + .unzip(); + + if let Some(kind) = debug_print_kind { + return Ok(ExprFlow::Value(self.emit_typed_debug_print( + kind, + &arg_values, + &debug_argument_types, + ))); + } + if self.is_value_call_target(callee) { + let function = value_or_signal!(self, callee); + return Ok(ExprFlow::Value(self.call_fn_value_from_source( + function, + arg_values, + Some(&argument_source_types), + )?)); } - let arg_values = Self::reorder_function_arguments(arg_values, argument_order.as_deref())?; match callee { Expr::Ident(ident) => { if let Some(name) = self.registry_name(&self.structs, &ident.name) { - return Ok(ExprFlow::Value( - self.construct_struct(&name, args, arg_values)?, - )); + return Ok(ExprFlow::Value(self.construct_struct( + &name, + &resolved_type_args, + args, + arg_values, + &argument_source_types, + )?)); } if let Some(name) = self.registry_name(&self.bitfields, &ident.name) { return Ok(ExprFlow::Value( @@ -2271,8 +3906,11 @@ impl Interpreter { let name = self .registry_name(&self.functions, &ident.name) .unwrap_or_else(|| ident.name.clone()); - Ok(ExprFlow::Value(self.call_function_with_type_args( - &name, type_args, arg_values, + Ok(ExprFlow::Value(self.call_function_from_resolved_source( + &name, + &resolved_type_args, + arg_values, + Some(&argument_source_types), )?)) } // Handle enum variant construction: Type.variant(args) @@ -2294,8 +3932,10 @@ impl Interpreter { if let Some(struct_name) = self.registry_name(&self.structs, name) { return Ok(ExprFlow::Value(self.construct_struct( &struct_name, + &resolved_type_args, args, arg_values, + &argument_source_types, )?)); } if let Some(bitfield_name) = self.registry_name(&self.bitfields, name) { @@ -2306,21 +3946,30 @@ impl Interpreter { )?)); } // Try higher-order built-ins first (require &mut self). - if let Some(result) = self.call_higher_order_builtin(name, &arg_values) { + if let Some(result) = self.call_higher_order_builtin_with_type_args( + name, + &resolved_type_args, + &arg_values, + checked_producer, + ) { return Ok(ExprFlow::Value(result?)); } let runtime_name = self.runtime_name(name); if self.is_trusted_stdlib_first_function(&runtime_name) { - return Ok(ExprFlow::Value(self.call_user_function_with_type_args( + return Ok(ExprFlow::Value(self.call_user_function_from_source( &runtime_name, - type_args, + &resolved_type_args, arg_values, + Some(&argument_source_types), )?)); } // Try type-reflection built-ins before ordinary built-ins. - if let Some(result) = - self.call_builtin_with_type_args(name, type_args, &arg_values) - { + if let Some(result) = self.call_builtin_with_type_args_from_source( + name, + &resolved_type_args, + &arg_values, + checked_producer, + ) { return Ok(ExprFlow::Value(result?)); } if let Some(result) = self.call_builtin(name, &arg_values) { @@ -2332,10 +3981,11 @@ impl Interpreter { .resolve_interface_dispatch(&runtime_name, &arg_values) .is_some() { - return Ok(ExprFlow::Value(self.call_function_with_type_args( + return Ok(ExprFlow::Value(self.call_function_from_resolved_source( &runtime_name, - type_args, + &resolved_type_args, arg_values, + Some(&argument_source_types), )?)); } } @@ -2360,13 +4010,30 @@ impl Interpreter { } } match dotted { - Some(name) => Ok(ExprFlow::Value( - self.call_function_with_type_args(&name, type_args, arg_values)?, - )), - None => Err("only named function calls are supported in comptime".to_string()), + Some(name) => Ok(ExprFlow::Value(self.call_function_from_resolved_source( + &name, + &resolved_type_args, + arg_values, + Some(&argument_source_types), + )?)), + None => { + let function = value_or_signal!(self, callee); + Ok(ExprFlow::Value(self.call_fn_value_from_source( + function, + arg_values, + Some(&argument_source_types), + )?)) + } } } - _ => Err("only named function calls are supported in comptime".to_string()), + _ => { + let function = value_or_signal!(self, callee); + Ok(ExprFlow::Value(self.call_fn_value_from_source( + function, + arg_values, + Some(&argument_source_types), + )?)) + } } } @@ -2412,10 +4079,29 @@ impl Interpreter { }) } + fn check_handle_receiver(&self, value: &Value, ty: Option<&TypeExpr>) -> Result<(), String> { + if matches!( + value.payload(), + Value::ResultOk(_) + | Value::ResultFail(_) + | Value::OptionalSome(_) + | Value::OptionalNone + ) { + return Ok(()); + } + // Observe the original wrappers without joining a pending receiver. + Err(format!( + "handle block requires a result or optional value, got {}", + self.format_debug_value(value, ty) + )) + } + fn eval_pipeline_step( &mut self, step: &PipelineStep, piped_value: Value, + result_type: Option<&TypeExpr>, + error_type: Option<&TypeExpr>, ) -> Result { let flow = self.eval_pipeline_step_call(step, piped_value)?; let Some(handle) = &step.handle else { @@ -2423,26 +4109,36 @@ impl Interpreter { }; let value = match flow { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ); + } ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), }; - self.eval_pipeline_step_handle(value, handle) + self.eval_pipeline_step_handle(value, handle, result_type, error_type) } fn eval_pipeline_step_handle( &mut self, step_value: Value, handle: &PipelineStepHandle, + result_type: Option<&TypeExpr>, + error_type: Option<&TypeExpr>, ) -> Result { - match step_value { + self.check_handle_receiver(&step_value, result_type)?; + match step_value.into_payload() { Value::ResultOk(value) => Ok(ExprFlow::Value(*value)), - Value::ResultFail(error) => { - self.exec_handle_block(handle.error_name.as_ref(), Some(*error), &handle.body) - } + Value::ResultFail(error) => self.exec_handle_block( + handle.error_name.as_ref(), + Some(*error), + &handle.body, + error_type, + ), Value::OptionalSome(value) => Ok(ExprFlow::Value(*value)), - Value::OptionalNone => self.exec_handle_block(None, None, &handle.body), - other => Err(format!( - "handle block requires a result or optional value, got {other}" - )), + Value::OptionalNone => self.exec_handle_block(None, None, &handle.body, None), + _ => unreachable!("sum receiver checked before wrapper consumption"), } } @@ -2451,6 +4147,11 @@ impl Interpreter { step: &PipelineStep, piped_value: Value, ) -> Result { + // The step fact describes its input, not its result. Its presence only + // establishes checked execution for fresh intrinsic producers; their + // validation target still comes from the selected actual type argument. + let checked_producer = self.checked_refinement_source_type(step.span).is_some(); + let checked_field_owner = self.checked_field_pipeline_intrinsic_owner(step)?; let function = match &step.function { Expr::View(inner, _) => inner.as_ref(), _ => &step.function, @@ -2460,13 +4161,12 @@ impl Interpreter { Expr::Call(callee, args, _) => (callee, &[], args), _ => (function, &[], &step.extra_args), }; + let function = Self::unparenthesized(function); let argument_order = self.source_function_argument_order(function, extra_args, true)?; let mut actual_types = Vec::with_capacity(extra_args.len() + 1); if let Some(piped_type) = self - .checked_expression_types - .as_ref() - .and_then(|types| types.get(&step.span)) + .checked_expression_type(step.span) .and_then(|type_name| Self::simple_type_expr_from_name(type_name, step.span)) { actual_types.push(piped_type); @@ -2486,63 +4186,155 @@ impl Interpreter { } let inferred_type_args = self.inferred_user_function_type_args_from_types(function, type_args, &actual_types); - let type_args = inferred_type_args.as_deref().unwrap_or(type_args); - // Build argument list: piped value first, then extra args. - let mut arg_values = vec![piped_value]; - for arg in extra_args { - arg_values.push(value_or_signal!(self, &arg.value)); + let mut resolved_type_args = inferred_type_args.unwrap_or_else(|| { + type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect() + }); + if let Some(owner) = checked_field_owner { + let selected = resolved_type_args + .first_mut() + .ok_or("checked field intrinsic lost its selected owner operand")?; + *selected = owner; + } + + // step.span records the checked inbound type, including a previous + // step's handled success type. Capture extra argument facts before any + // evaluation, then permute each proof together with its value. + let debug_print_kind = self.debug_print_call_kind(function); + let debug_argument_types = if debug_print_kind.is_some() { + let mut types = vec![ + self.checked_expression_type(step.span) + .and_then(|name| Self::debug_type(name)) + .map(|ty| self.substitute_type_expr(&ty)), + ]; + types.extend( + extra_args + .iter() + .map(|arg| self.debug_print_expression_type(&arg.value)), + ); + Self::reorder_function_arguments(types, argument_order.as_deref())? + } else { + Vec::new() + }; + let mut evaluated_args = vec![( + piped_value, + self.checked_refinement_source_type(step.span).cloned(), + )]; + let extra_source_types: Vec<_> = extra_args + .iter() + .map(|arg| { + self.checked_refinement_source_type(arg.value.span()) + .cloned() + }) + .collect(); + for (arg, source_type) in extra_args.iter().zip(extra_source_types) { + evaluated_args.push((value_or_signal!(self, &arg.value), source_type)); + } + let (arg_values, argument_source_types): (Vec<_>, Vec<_>) = + Self::reorder_function_arguments(evaluated_args, argument_order.as_deref())? + .into_iter() + .unzip(); + + if let Some(kind) = debug_print_kind { + return Ok(ExprFlow::Value(self.emit_typed_debug_print( + kind, + &arg_values, + &debug_argument_types, + ))); + } + if self.is_value_call_target(function) { + let function = value_or_signal!(self, function); + return Ok(ExprFlow::Value(self.call_fn_value_from_source( + function, + arg_values, + Some(&argument_source_types), + )?)); } - let arg_values = Self::reorder_function_arguments(arg_values, argument_order.as_deref())?; // Resolve the function name from the expression. match function { + Expr::EnumVariant(type_name, variant, _) => { + let enum_name = self + .registry_name(&self.enums, &type_name.name) + .unwrap_or_else(|| type_name.name.clone()); + Ok(ExprFlow::Value(self.construct_enum_variant( + &enum_name, + &variant.name, + arg_values, + )?)) + } Expr::Ident(ident) => { let name = self .registry_name(&self.functions, &ident.name) .unwrap_or_else(|| ident.name.clone()); - Ok(ExprFlow::Value(self.call_function_with_type_args( - &name, type_args, arg_values, + Ok(ExprFlow::Value(self.call_function_from_resolved_source( + &name, + &resolved_type_args, + arg_values, + Some(&argument_source_types), )?)) } Expr::FieldAccess(obj, field, _) => { let dotted = Self::extract_dotted_name(obj, &field.name); if let Some(ref name) = dotted { // Check higher-order builtins first (need &mut self). - if let Some(result) = self.call_higher_order_builtin(name, &arg_values) { + if let Some(result) = self.call_higher_order_builtin_with_type_args( + name, + &resolved_type_args, + &arg_values, + checked_producer, + ) { return Ok(ExprFlow::Value(result?)); } let runtime_name = self.runtime_name(name); if self.is_trusted_stdlib_first_function(&runtime_name) { - return Ok(ExprFlow::Value(self.call_user_function_with_type_args( + return Ok(ExprFlow::Value(self.call_user_function_from_source( &runtime_name, - type_args, + &resolved_type_args, arg_values, + Some(&argument_source_types), )?)); } - if let Some(result) = - self.call_builtin_with_type_args(name, type_args, &arg_values) - { + if let Some(result) = self.call_builtin_with_type_args_from_source( + name, + &resolved_type_args, + &arg_values, + checked_producer, + ) { return Ok(ExprFlow::Value(result?)); } if let Some(result) = self.call_builtin(name, &arg_values) { return Ok(ExprFlow::Value(result?)); } if self.functions.contains_key(runtime_name.as_str()) { - return Ok(ExprFlow::Value(self.call_function_with_type_args( + return Ok(ExprFlow::Value(self.call_function_from_resolved_source( &runtime_name, - type_args, + &resolved_type_args, arg_values, + Some(&argument_source_types), )?)); } } + if let Some(owner_name) = Self::dotted_expr_name(obj) + && let Some(enum_name) = self.registry_name(&self.enums, &owner_name) + { + return Ok(ExprFlow::Value(self.construct_enum_variant( + &enum_name, + &field.name, + arg_values, + )?)); + } match dotted { Some(name) => { let runtime_name = self.runtime_name(&name); - Ok(ExprFlow::Value(self.call_function_with_type_args( + Ok(ExprFlow::Value(self.call_function_from_resolved_source( &runtime_name, - type_args, + &resolved_type_args, arg_values, + Some(&argument_source_types), )?)) } None => { @@ -2559,23 +4351,28 @@ impl Interpreter { bind_name: Option<&Ident>, bind_value: Option, body: &Block, + bind_type: Option<&TypeExpr>, ) -> Result { self.push_scope(); if let (Some(name), Some(value)) = (bind_name, bind_value) { - self.set_variable(&name.name, value); + self.set_inferred_debug_binding(name, value, bind_type); } - let mut signal = None; - for stmt in &body.stmts { - if let Some(next) = self.exec_stmt_inner(stmt)? { - signal = Some(next); - break; + let result = (|| { + let mut signal = None; + for stmt in &body.stmts { + if let Some(next) = self.exec_stmt_inner(stmt)? { + signal = Some(next); + break; + } } - } + Ok::<_, String>(signal) + })(); self.pop_scope(); - - match signal { + self.check_resource_cleanup()?; + match result? { Some(Signal::Default(value)) => Ok(ExprFlow::Value(value)), + Some(Signal::ResourceDefault(value)) => Ok(ExprFlow::Resource(value)), Some(other) => Ok(ExprFlow::Signal(other)), None => Err("handle block must end with return or default".to_string()), } @@ -2586,96 +4383,167 @@ impl Interpreter { /// Execute a single statement. Returns `Ok(None)` for normal flow, or /// a [`Signal`] if control flow must be altered. fn exec_stmt_inner(&mut self, stmt: &Stmt) -> Result, String> { + if let (Some(transport), Stmt::VarDecl(declaration)) = (&self.resource_transport, stmt) { + let fact = transport + .checked + .binding(declaration.name.span) + .map_err(|error| error.to_string())?; + if transport + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + // A sealed named descriptor has a Resource signature but no + // physical Resource payload or capture environment. + if declaration.mutable + || fact.mutable + || transport + .checked + .prepare_named_callable(&declaration.value) + .map_err(|error| error.to_string())? + .is_none() + { + return self.exec_resource_binding(declaration); + } + } + } match stmt { Stmt::VarDecl(decl) => { + if let Some(result) = self.exec_temporary_binding(decl)? { + return Ok(result); + } let declared_ty = self.substitute_type_expr(&decl.ty); let type_name = type_expr_name(&declared_ty); let val = if self.type_aliases.contains_key(&type_name) { match &decl.value { Expr::Handle(target, bind_name, body, _) => { + let source_type = + self.checked_refinement_source_type(target.span()).cloned(); + let success_source_type = Self::checked_sum_success_type( + source_type.as_deref(), + target.span(), + ); + let target_type = self.debug_expression_type(target); let target_value = match self.eval_expr_flow(target)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; - let flow = match target_value { - Value::ResultOk(value) | Value::OptionalSome(value) => self - .finish_refinement_boundary( + let sum_payload = matches!( + target_value.payload(), + Value::ResultOk(_) + | Value::ResultFail(_) + | Value::OptionalSome(_) + | Value::OptionalNone + ); + let handles_sum = self.with_resolved_type_aliases(|interpreter| { + let refines_whole_sum = interpreter.type_name_has_refinement(&type_name) + && target_type.as_ref().is_some_and(|ty| { + interpreter.resolved_concrete_type_display( + &interpreter.resolved_inference_base_type(&declared_ty), + ) == interpreter.resolved_concrete_type_display( + &interpreter.resolved_inference_base_type(ty), + ) + }); + sum_payload && !refines_whole_sum + && target_type.as_ref().is_none_or(|ty| { + !interpreter.type_name_has_refinement(&interpreter.resolved_concrete_type_display(ty)) + && matches!(interpreter.resolved_inference_base_type(ty), TypeExpr::Generic(name, _, _) + if matches!(name.name.as_str(), "result" | "optional")) + }) + }); + let flow = if !handles_sum { + self.finish_refinement_boundary( + &type_name, + target_value, + source_type.as_deref(), + bind_name.as_ref(), + body, + )? + } else { + match target_value.into_payload() { + Value::ResultOk(value) | Value::OptionalSome(value) => self + .finish_refinement_boundary( + &type_name, + *value, + success_source_type.as_deref(), + bind_name.as_ref(), + body, + )?, + Value::ResultFail(error) => self.exec_handle_block( + bind_name.as_ref(), + Some(*error), + body, + self.debug_expression_args(target).get(1), + )?, + Value::OptionalNone => { + self.exec_handle_block(None, None, body, None)? + } + value => self.finish_refinement_boundary( &type_name, - *value, + value, + source_type.as_deref(), bind_name.as_ref(), body, )?, - Value::ResultFail(error) => { - self.exec_handle_block(bind_name.as_ref(), Some(*error), body)? } - Value::OptionalNone => self.exec_handle_block(None, None, body)?, - value => self.finish_refinement_boundary( - &type_name, - value, - bind_name.as_ref(), - body, - )?, }; match flow { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), } } _ => { + let source_type = self + .checked_refinement_source_type(decl.value.span()) + .cloned(); let val = match self.eval_expr_flow(&decl.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; - self.check_refinement(&type_name, &val)?; + self.check_refinement_from_resolved_source( + &type_name, + &val, + source_type.as_deref(), + )?; val } } } else { match self.eval_expr_flow(&decl.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), } }; - let val = self.normalize_value_for_type(&declared_ty, val)?; + let val = self.normalize_value_for_resolved_type(&declared_ty, val)?; self.set_variable_with_type(&decl.name.name, val, declared_ty); + self.install_checked_field_binding(&decl.name)?; Ok(None) } - Stmt::ComptimeTypeBind(bind) => { - let bound_type_expr = if let Some(bound_type_expr) = - comptime_type_info_binding(&bind.value) - { - self.substitute_type_expr(bound_type_expr) - } else if let Some((source_ty, index)) = comptime_type_arg_binding(&bind.value) { - let source_ty = self.substitute_type_expr(source_ty); - self.checked_type_info_arg_types(&source_ty) - .unwrap_or_else(|| self.type_info_arg_types(&source_ty)) - .get(index) - .cloned() - .ok_or_else(|| { - format!( - "`comptime type` type.arg index {index} is out of range for type '{}'", - type_expr_display(&source_ty) - ) - })? - } else if let Some(field_name) = reflected_field_type_info_binding(&bind.value) { - self.bound_reflected_field_type(field_name)? - } else if let Some(info_name) = reflected_type_info_binding(&bind.value) { - self.bound_reflected_type_info_type(info_name)? - } else { - return Err("`comptime type` currently requires a direct `type.info[T]()` initializer or trusted reflected metadata".to_string()); - }; - let mut scope = HashMap::new(); - scope.insert(bind.name.name.clone(), bound_type_expr); - self.type_arg_scopes.push(scope); - let result = self.exec_block_inner(&bind.body); - self.type_arg_scopes.pop(); - result - } + Stmt::ComptimeTypeBind(bind) => self.exec_comptime_type_bind(bind), Stmt::Assign(assign) => { + if self + .resource_transport + .as_ref() + .is_some_and(|transport| transport.checked_source_active) + && self.resource_type_at(assign.target.span())? + { + return self.exec_resource_assignment(assign); + } + if field_updates::is_projected_target(&assign.target) { + return self.exec_checked_field_assignment(assign); + } let val = match self.eval_expr_flow(&assign.value)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; match &assign.target { @@ -2698,19 +4566,44 @@ impl Interpreter { } Stmt::Return(ret) => { + if let Some(transport) = self + .resource_transport + .as_mut() + .filter(|transport| transport.checked_source_active) + { + transport + .retire_return_operations() + .map_err(|error| error.to_string())?; + } + let source_type = ret.value.as_ref().and_then(|expression| { + self.checked_refinement_source_type(expression.span()) + .cloned() + }); let val = match &ret.value { Some(expr) => match self.eval_expr_flow(expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(mut value) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .preserve_return(&mut value) + .map_err(|error| error.to_string())?; + return Ok(Some(Signal::ResourceReturn(value, source_type))); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), }, None => Value::Nothing, }; - Ok(Some(Signal::Return(val))) + Ok(Some(Signal::Return(val, source_type))) } Stmt::If(if_stmt) => { let cond = match self.eval_expr_flow(&if_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if is_truthy(&cond)? { @@ -2719,6 +4612,7 @@ impl Interpreter { for (else_if_cond, else_if_block) in &if_stmt.else_ifs { let val = match self.eval_expr_flow(else_if_cond)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if is_truthy(&val)? { @@ -2732,8 +4626,25 @@ impl Interpreter { } Stmt::For(for_stmt) => { - let reflected_field_bindings = - self.reflected_field_loop_bindings(&for_stmt.iterable)?; + let checked_field_loop = self.prepare_checked_resource_field_loop(for_stmt)?; + let reflected_field_bindings = match &checked_field_loop { + Some(proof) => { + let owner_type = proof.owner_name(); + Some( + proof + .fields() + .map(|field| ReflectedFieldBinding { + index: field.index, + owner_type: owner_type.clone(), + owner_member: None, + name: field.name.clone(), + ty: Self::reflection_type_info_type_expr(&field.type_info), + }) + .collect::>(), + ) + } + None => self.reflected_field_loop_bindings(&for_stmt.iterable)?, + }; let reflected_variant_bindings = self.reflected_variant_loop_bindings(&for_stmt.iterable)?; let reflected_machine_state_bindings = @@ -2744,16 +4655,53 @@ impl Interpreter { self.reflected_machine_field_loop_owner(&for_stmt.iterable)?; let reflected_type_info_bindings = self.reflected_type_info_arg_loop_bindings(&for_stmt.iterable)?; - let iterable = match self.eval_expr_flow(&for_stmt.iterable)? { + let item_types = self.debug_expression_args(&for_stmt.iterable); + let iterable = match self.eval_checked_absent_for_iterable(for_stmt)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; - match iterable { + let absent_bindings = self.prepare_absent_for_bindings(for_stmt, &iterable)?; + match iterable.into_payload() { Value::List(items) => { + if let Some(proof) = &checked_field_loop { + proof + .validate_count(items.len()) + .map_err(|error| error.to_string())?; + } for (index, item) in items.into_iter().enumerate() { + // Every fallible proof join precedes lexical or debug mutation. + let checked_field_item = checked_field_loop + .as_ref() + .map(|proof| proof.iteration(index)) + .transpose() + .map_err(|error| error.to_string())?; + if let Some(proof) = &checked_field_item { + proof + .validate_value(&item) + .map_err(|error| error.to_string())?; + } + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + &item, + )?; self.push_scope(); let loop_item = item.clone(); - self.set_variable(&for_stmt.variable.name, item); + if let Err(error) = self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + item, + item_types.first(), + ) { + self.pop_scope(); + return Err(error); + } let pushed_field_scope = reflected_field_bindings .as_ref() @@ -2823,7 +4771,14 @@ impl Interpreter { }) .is_some(); + let pushed_checked_field = checked_field_item.is_some(); + if let Some(proof) = checked_field_item { + self.checked_reflected_fields.push(proof); + } let signal = self.exec_block_inner(&for_stmt.body); + if pushed_checked_field { + self.checked_reflected_fields.pop(); + } if pushed_type_info_scope { self.reflected_type_info_scopes.pop(); } @@ -2855,9 +4810,10 @@ impl Interpreter { Value::String(s) => { for ch in s.chars() { self.push_scope(); - self.set_variable( - &for_stmt.variable.name, + self.set_inferred_debug_binding( + &for_stmt.variable, Value::String(ch.to_string()), + item_types.first(), ); let signal = self.exec_block_inner(&for_stmt.body)?; self.pop_scope(); @@ -2871,13 +4827,42 @@ impl Interpreter { } Value::Map(entries) => { for (key, val) in entries { - self.push_scope(); - self.set_variable(&for_stmt.variable.name, key); - if let Some(ref val_var) = for_stmt.value_variable { - self.set_variable(&val_var.name, val); + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + &key, + )?; + if let Some(value) = &for_stmt.value_variable { + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + value, + 1, + &val, + )?; } - let signal = self.exec_block_inner(&for_stmt.body)?; + self.push_scope(); + let signal = (|| { + self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + &for_stmt.variable, + 0, + key, + item_types.first(), + )?; + if let Some(ref val_var) = for_stmt.value_variable { + self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + val_var, + 1, + val, + item_types.get(1), + )?; + } + self.exec_block_inner(&for_stmt.body) + })(); self.pop_scope(); + let signal = signal?; match signal { Some(Signal::Break) => break, Some(Signal::Continue) => continue, @@ -2889,7 +4874,11 @@ impl Interpreter { Value::Set(items) => { for item in items { self.push_scope(); - self.set_variable(&for_stmt.variable.name, item); + self.set_inferred_debug_binding( + &for_stmt.variable, + item, + item_types.first(), + ); let signal = self.exec_block_inner(&for_stmt.body)?; self.pop_scope(); match signal { @@ -2913,6 +4902,7 @@ impl Interpreter { loop { let cond = match self.eval_expr_flow(&while_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err("Resource operand has no checked ownership transport for this operation".to_string()), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; if !is_truthy(&cond)? { @@ -2932,29 +4922,69 @@ impl Interpreter { } Stmt::Match(match_stmt) => { + let owner_type = self.debug_expression_type(&match_stmt.expr); let val = match self.eval_expr_flow(&match_stmt.expr)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; - let (variant_name, fields) = match &val { + let (variant_name, fields) = match val.payload() { Value::Enum { variant, fields, .. } => (variant.clone(), fields.clone()), _ => return Err(format!("match requires an enum value, got {val}")), }; - for arm in &match_stmt.arms { + for (arm_index, arm) in match_stmt.arms.iter().enumerate() { match &arm.pattern { Pattern::Ident(ident) => { if ident.name == variant_name { + self.prepare_absent_match_bindings(match_stmt, arm_index, &val)?; return self.exec_block_inner(&arm.body); } } Pattern::Variant(name, bindings) => { if name.name == variant_name { + let absent_bindings = self + .prepare_absent_match_bindings(match_stmt, arm_index, &val)?; + if absent_bindings + .as_ref() + .is_some_and(|proof| proof.binding_count() != fields.len()) + { + return Err("resource value lost its checked payload custody" + .to_string()); + } + for (index, (binding, field)) in + bindings.iter().zip(&fields).enumerate() + { + self.validate_absent_generated_debug_binding( + absent_bindings.as_ref(), + binding, + index, + field, + )?; + } self.push_scope(); - for (binding, field_val) in bindings.iter().zip(fields.iter()) { - self.set_variable(&binding.name, field_val.clone()); + let field_types = owner_type + .as_ref() + .map(|ty| self.debug_fields(ty, Some((&variant_name, false)))) + .unwrap_or_default(); + for (index, (binding, field_val)) in + bindings.iter().zip(fields.iter()).enumerate() + { + if let Err(error) = self.set_absent_generated_debug_binding( + absent_bindings.as_ref(), + binding, + index, + field_val.clone(), + field_types.get(index).map(|(_, ty)| ty), + ) { + self.pop_scope(); + return Err(error); + } } let result = self.exec_block_inner(&arm.body); self.pop_scope(); @@ -2962,6 +4992,7 @@ impl Interpreter { } } Pattern::Other(_) => { + self.prepare_absent_match_bindings(match_stmt, arm_index, &val)?; return self.exec_block_inner(&arm.body); } } @@ -2976,12 +5007,24 @@ impl Interpreter { } Stmt::Expr(expr_stmt) => match self.eval_expr_flow(&expr_stmt.expr)? { ExprFlow::Value(_) => Ok(None), + ExprFlow::Resource(value) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .discard_value(value) + .map_err(|error| error.to_string())?; + Ok(None) + } ExprFlow::Signal(signal) => Ok(Some(signal)), }, Stmt::Assert(assert_stmt) => { let cond = match self.eval_expr_flow(&assert_stmt.condition)? { ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(signal) => return Ok(Some(signal)), }; match cond { @@ -2991,6 +5034,11 @@ impl Interpreter { match self.eval_expr_flow(msg_expr)? { ExprFlow::Value(Value::String(s)) => s, ExprFlow::Value(other) => other.to_string(), + ExprFlow::Resource(_) => { + return Err( + "Resource cannot become an assertion message".to_string() + ); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), } } else { @@ -3014,6 +5062,9 @@ impl Interpreter { ExprFlow::Value(other) => { return Err(format!("breakpoint condition must be bool, got {other}")); } + ExprFlow::Resource(_) => { + return Err("Resource cannot become a breakpoint condition".to_string()); + } ExprFlow::Signal(signal) => return Ok(Some(signal)), } } else { @@ -3021,7 +5072,7 @@ impl Interpreter { }; if should_break { - self.hit_breakpoint(); + self.hit_breakpoint(breakpoint_stmt.span); } Ok(None) } @@ -3046,7 +5097,12 @@ impl Interpreter { /// public-facing result. pub fn exec_stmt(&mut self, stmt: &Stmt) -> Result<(), String> { match self.exec_stmt_inner(stmt)? { - None | Some(Signal::Break) | Some(Signal::Continue) | Some(Signal::Return(_)) => Ok(()), + None | Some(Signal::Break) | Some(Signal::Continue) | Some(Signal::Return(..)) => { + Ok(()) + } + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("raw statement API cannot export live Resource custody".to_string()) + } Some(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } @@ -3060,23 +5116,51 @@ impl Interpreter { /// Execute a block (list of statements), propagating control-flow signals. fn exec_block_inner(&mut self, block: &Block) -> Result, String> { + self.exec_block_with_temporary_output(block) + .map(temporary_local_views::BlockOutput::into_signal) + } + + fn exec_block_with_temporary_output( + &mut self, + block: &Block, + ) -> Result { self.push_scope(); - let mut result = None; - for stmt in &block.stmts { - if let Some(signal) = self.exec_stmt_inner(stmt)? { - result = Some(signal); - break; + let activation = match self.begin_temporary_block(block) { + Ok(activation) => activation, + Err(error) => { + self.pop_scope(); + self.check_resource_cleanup()?; + return Err(error); } - } + }; + let result = (|| { + for stmt in &block.stmts { + if let Some(signal) = self.exec_stmt_inner(stmt)? { + return Ok(Some(signal)); + } + } + Ok(None) + })(); + let result = self.acquire_temporary_output(activation, result); + let local_cleanup = self.complete_temporary_block(activation); self.pop_scope(); - Ok(result) + // Preserve the pre-existing Resource-cleanup precedence over a primary + // block error. Ordinary retirement cannot replace an existing error. + self.check_resource_cleanup()?; + match (result, local_cleanup) { + (Err(error), _) | (_, Err(error)) => Err(error), + (Ok(result), Ok(())) => Ok(result), + } } /// Execute a block, returning the value produced by a `return` statement /// (if any). pub fn exec_block(&mut self, block: &Block) -> Result, String> { match self.exec_block_inner(block)? { - Some(Signal::Return(v)) => Ok(Some(v)), + Some(Signal::Return(v, _)) if !v.contains_live_resource_or_grant() => Ok(Some(v)), + Some(Signal::Return(..) | Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + Err("raw block API cannot export live Resource custody".to_string()) + } Some(Signal::Default(_)) => { Err("`default` can only be used inside a `handle` block".to_string()) } @@ -3142,7 +5226,11 @@ impl Interpreter { /// /// When `is_ask` is true, a `respond value` inside the handler returns /// `value` as the result; for `send` the result is always `nothing`. - fn eval_actor_message(&mut self, inner: &Expr, is_ask: bool) -> Result { + fn eval_actor_message_unchecked( + &mut self, + inner: &Expr, + is_ask: bool, + ) -> Result { // Decompose `actor_expr.handler_name` or `actor_expr.handler_name(args)`. let (actor_expr, handler_name, call_args) = match inner { Expr::Call(callee, args, _) => match callee.as_ref() { @@ -3162,12 +5250,18 @@ impl Interpreter { // Evaluate actor handle. let actor_val = match self.eval_expr_flow(actor_expr)? { ExprFlow::Value(v) => v, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ); + } ExprFlow::Signal(s) => { return Err(format!("send/ask: actor expression returned signal: {s:?}")); } }; - let actor_id = match actor_val { - Value::Actor(id) => id, + let actor_id = match actor_val.payload() { + Value::Actor(id) => *id, _ => return Err(format!("send/ask: expected actor value, got {actor_val}")), }; @@ -3177,6 +5271,10 @@ impl Interpreter { for arg in args { let val = match self.eval_expr_flow(&arg.value)? { ExprFlow::Value(v) => v, + ExprFlow::Resource(_) => return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ), ExprFlow::Signal(s) => { return Err(format!("send/ask: arg expression returned signal: {s:?}")); } @@ -3215,65 +5313,85 @@ impl Interpreter { normalized_args[index] = self.normalize_value_for_type(¶m_ty, value)?; } - // Execute handler body in a new scope with state + caps + params. + // An actor handler is a separate lexical call frame. + // Its real original handler-origin/ownership adapter is still pending. + let saved_field_cursor = self.enter_checked_field_body(None)?; + let scope_base = self.scopes.len(); + let saved_floor = std::mem::replace(&mut self.lexical_scope_floor, scope_base); self.push_scope(); - for (name, val) in &state_snapshot { - if let Some(field) = actor_def - .state_fields - .iter() - .find(|field| field.name.name == *name) - { - let field_ty = self.substitute_type_expr(&field.ty); - self.set_variable_with_type(name, val.clone(), field_ty); - } else { - self.set_variable(name, val.clone()); + let outcome = (|| -> Result<_, String> { + for (name, val) in &state_snapshot { + if let Some(field) = actor_def + .state_fields + .iter() + .find(|field| field.name.name == *name) + { + let field_ty = self.substitute_type_expr(&field.ty); + self.set_variable_with_type(name, val.clone(), field_ty); + } else { + self.set_variable(name, val.clone()); + } } - } - for (name, val) in &caps_snapshot { - if let Some(param) = actor_def - .capability_params - .iter() - .find(|param| param.name.name == *name) - { + for (name, val) in &caps_snapshot { + if let Some(param) = actor_def + .capability_params + .iter() + .find(|param| param.name.name == *name) + { + let param_ty = self.substitute_type_expr(¶m.ty); + self.set_variable_with_type(name, val.clone(), param_ty); + } else { + self.set_variable(name, val.clone()); + } + } + for (param, val) in handler.params.iter().zip(normalized_args) { let param_ty = self.substitute_type_expr(¶m.ty); - self.set_variable_with_type(name, val.clone(), param_ty); - } else { - self.set_variable(name, val.clone()); + self.set_variable_with_type(¶m.name.name, val, param_ty); } - } - for (param, val) in handler.params.iter().zip(normalized_args) { - let param_ty = self.substitute_type_expr(¶m.ty); - self.set_variable_with_type(¶m.name.name, val, param_ty); - } - // Execute the handler body, collecting signals. - let mut respond_value = Value::Nothing; - for stmt in &handler.body.stmts { - match self.exec_stmt_inner(stmt)? { - Some(Signal::Respond(val)) => { - respond_value = val; - break; + // Execute the handler body, collecting signals. + let mut respond_value = Value::Nothing; + for stmt in &handler.body.stmts { + match self.exec_stmt_inner(stmt)? { + Some(Signal::Respond(val)) => { + respond_value = val; + break; + } + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err( + "actor Resource custody has no checked transport proof".to_string() + ); + } + Some(Signal::Return(..)) => break, + Some(Signal::Break) | Some(Signal::Continue) => break, + Some(Signal::Default(_)) => break, + None => {} } - Some(Signal::Return(_)) => break, - Some(Signal::Break) | Some(Signal::Continue) => break, - Some(Signal::Default(_)) => break, - None => {} } - } - // Collect updated state field values before popping scope. - let mut updated_state = state_snapshot; - for field in &actor_def.state_fields { - let name = &field.name.name; - // Check innermost scope(s) for the updated value. - if let Some(val) = self.scopes.last().and_then(|s| s.get(name)).cloned() { - let field_ty = self.substitute_type_expr(&field.ty); - let val = self.normalize_value_for_type(&field_ty, val)?; - updated_state.insert(name.clone(), val); + // Collect updated state field values before popping scope. + let mut updated_state = state_snapshot; + for field in &actor_def.state_fields { + let name = &field.name.name; + // Check innermost scope(s) for the updated value. + if let Some(val) = self.scopes.last().and_then(|s| s.get(name)).cloned() { + let field_ty = self.substitute_type_expr(&field.ty); + let val = self.normalize_value_for_type(&field_ty, val)?; + updated_state.insert(name.clone(), val); + } } - } - self.pop_scope(); + Ok((respond_value, updated_state)) + })(); + while self.scopes.len() > scope_base { + self.pop_scope(); + } + self.lexical_scope_floor = saved_floor; + let field_restore = self.restore_checked_field_body(saved_field_cursor); + let (mut respond_value, updated_state) = match (outcome, field_restore) { + (Err(error), _) | (_, Err(error)) => return Err(error), + (Ok(value), Ok(())) => value, + }; // Write updated state back to the actor instance. if let Some(instance) = self.actor_instances.get_mut(&actor_id) { @@ -3291,13 +5409,18 @@ impl Interpreter { } } - fn call_bitfield_builtin(&self, name: &str, args: &[Value]) -> Option> { - let (source_bitfield_name, method_name) = name.rsplit_once('.')?; + fn call_bitfield_builtin( + &self, + intrinsic: IntrinsicId, + name: &str, + args: &[Value], + ) -> Option> { + let (source_bitfield_name, _) = name.rsplit_once('.')?; let bitfield_name = self.registry_name(&self.bitfields, source_bitfield_name)?; - match method_name { - "to_bytes" => Some(self.bitfield_to_bytes(&bitfield_name, args)), - "from_bytes" => Some(self.bitfield_from_bytes(&bitfield_name, args)), + match intrinsic { + IntrinsicId::BitfieldToBytes => Some(self.bitfield_to_bytes(&bitfield_name, args)), + IntrinsicId::BitfieldFromBytes => Some(self.bitfield_from_bytes(&bitfield_name, args)), _ => None, } } @@ -3348,7 +5471,13 @@ impl Interpreter { bitfield: &BitfieldDef, value: &Value, ) -> Result, String> { - let Value::Struct { type_name, fields } = value else { + let value = value.payload(); + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{}.to_bytes expects a {} value", bitfield.name.name, bitfield.name.name @@ -3507,6 +5636,7 @@ impl Interpreter { } Ok(Value::Struct { + concrete_type: None, type_name: bitfield.name.name.clone(), fields, }) @@ -3701,7 +5831,7 @@ impl Interpreter { )); } - let numeric = match value { + let numeric = match value.payload() { Value::Int64(int_value) if *int_value >= 0 => *int_value as u64, Value::Int64(int_value) => { return Err(format!( @@ -3763,11 +5893,12 @@ impl Interpreter { } fn value_to_byte_list(&self, value: &Value) -> Result, String> { + let value = value.payload(); match value { Value::Bytes(bytes) => Ok(bytes.clone()), Value::List(items) => items .iter() - .map(|item| match item { + .map(|item| match item.payload() { Value::Int64(value) if (0..=255).contains(value) => Ok(*value as u8), Value::Int64(value) => Err(format!("byte value out of range: {}", value)), other => Err(format!("payload expects list[uint8], found {}", other)), @@ -3843,69 +5974,120 @@ impl Interpreter { Some(value) } + #[cfg(test)] fn call_builtin_with_type_args( &mut self, name: &str, type_args: &[TypeExpr], args: &[Value], ) -> Option> { - let is_typed_builtin = matches!( - name, - "type.name" - | "type.kind" - | "type.kind_tag" - | "type.primitive_tag" - | "type.has_secret" - | "type.info" - | "type.arg" - | "type.fields" - | "type.bitfield_layout" - | "type.bitfield_fields" - | "type.machine_layout" - | "type.machine_states" - | "type.machine_transitions" - | "type.machine_state_value" - | "type.variants" - | "type.variant_value" - | "type.field_value" - | "type.machine_field_value" - | "type.variant_field_value" - | "type.construct_start" - | "type.construct_variant_start" - | "type.construct_machine_start" - | "type.construct_put" - | "type.construct_finish" - | "json.parse" - | "json.parse_exact" - | "json.serialize" - | "json.serialize_public" - ); - if !is_typed_builtin { - return None; - } - let expected_type_arg_count = if matches!( - name, - "type.field_value" | "type.machine_field_value" | "type.variant_field_value" - ) { - 2 - } else if matches!(name, "type.construct_put") { - 2 - } else { - 1 - }; - if type_args.len() != expected_type_arg_count { - return Some(Err(format!( + let resolved_type_args: Vec<_> = type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect(); + // Preserve metadata-free declaration lookup in its caller context. + // Only the checked AST path owns already-canonical field metadata. + self.call_builtin_with_resolved_type_args(name, &resolved_type_args, args, false) + } + + /// Type arguments already name their selected caller-resolved owners. + fn call_builtin_with_type_args_from_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: &[Value], + checked_producer: bool, + ) -> Option> { + let intrinsic = IntrinsicId::from_callable_name(name)?; + // JSON facades execute source bodies and keep their lexical imports. + // Reflection leaves consume canonical type names; caller aliases must + // not reinterpret the prefix of an already-selected owner. + // Metadata-free field readers still resolve declaration field spelling + // through their existing lexical lookup. This is distinct from the + // canonical type arguments used by pure metadata operations. + let lexical_field_lookup = matches!( + intrinsic, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeVariantFieldValue + ) && (!checked_producer + || !self.allow_checked_refinement_proofs); + if lexical_field_lookup + || matches!( + intrinsic, + IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + ) + { + return self.call_builtin_with_resolved_type_args( + name, + type_args, + args, + checked_producer, + ); + } + let blank_aliases = vec![HashMap::new(); self.namespace_alias_scopes.len()]; + let saved_aliases = std::mem::replace(&mut self.namespace_alias_scopes, blank_aliases); + let result = + self.call_builtin_with_resolved_type_args(name, type_args, args, checked_producer); + self.namespace_alias_scopes = saved_aliases; + result + } + + fn call_builtin_with_resolved_type_args( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: &[Value], + checked_producer: bool, + ) -> Option> { + let intrinsic = IntrinsicId::from_callable_name(name)?; + let expected_type_arg_count = match intrinsic { + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeConstructPut => 2, + IntrinsicId::TypeName + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo + | IntrinsicId::TypeArg + | IntrinsicId::TypeFields + | IntrinsicId::TypeBitfieldLayout + | IntrinsicId::TypeBitfieldFields + | IntrinsicId::TypeMachineLayout + | IntrinsicId::TypeMachineStates + | IntrinsicId::TypeMachineTransitions + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeVariants + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeConstructStart + | IntrinsicId::TypeConstructVariantStart + | IntrinsicId::TypeConstructMachineStart + | IntrinsicId::TypeConstructFinish + | IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic => 1, + non_type_arg_intrinsics!() => return None, + }; + if type_args.len() != expected_type_arg_count { + return Some(Err(format!( "{name} expects {expected_type_arg_count} type argument(s), got {}", type_args.len() ))); } - let ty = self.substitute_type_expr(&type_args[0]); + let ty = type_args[0].clone(); if let Some(error) = self.missing_checked_type_info_error(&ty) { return Some(Err(error)); } - Some(match name { - "type.name" => { + Some(match intrinsic { + IntrinsicId::TypeName => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3915,7 +6097,7 @@ impl Interpreter { .unwrap_or_else(|| type_expr_display(&ty)), )) } - "type.kind" => { + IntrinsicId::TypeKind => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3925,7 +6107,7 @@ impl Interpreter { .to_string(), )) } - "type.kind_tag" => { + IntrinsicId::TypeKindTag => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3934,7 +6116,7 @@ impl Interpreter { .unwrap_or_else(|| self.type_expr_kind(&ty)); Ok(Self::type_kind_tag_value(kind)) } - "type.primitive_tag" => { + IntrinsicId::TypePrimitiveTag => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3943,7 +6125,7 @@ impl Interpreter { .map(|info| Self::primitive_tag_value(info.primitive_tag.as_deref())) .unwrap_or_else(|| self.type_primitive_tag_value(&ty))) } - "type.has_secret" => { + IntrinsicId::TypeHasSecret => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3952,7 +6134,7 @@ impl Interpreter { .unwrap_or_else(|| self.type_expr_has_secret(&ty)), )) } - "type.info" => { + IntrinsicId::TypeInfo => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3960,7 +6142,7 @@ impl Interpreter { .checked_type_info_value(&ty) .unwrap_or_else(|| self.type_info_value(&ty))) } - "type.arg" => { + IntrinsicId::TypeArg => { if let Some(err) = check_args(name, 1, args) { return Some(err); } @@ -3985,7 +6167,7 @@ impl Interpreter { }; Ok(self.type_info_value(arg_ty)) } - "type.construct_start" => { + IntrinsicId::TypeConstructStart => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -3996,32 +6178,32 @@ impl Interpreter { fields: Vec::new(), }) } - "type.construct_variant_start" => { + IntrinsicId::TypeConstructVariantStart => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.reflected_construct_variant_start(&ty, &args[0]) } - "type.construct_machine_start" => { + IntrinsicId::TypeConstructMachineStart => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.reflected_construct_machine_start(&ty, &args[0]) } - "type.construct_put" => { + IntrinsicId::TypeConstructPut => { if let Some(err) = check_args(name, 3, args) { return Some(err); } - let expected_field_ty = self.substitute_type_expr(&type_args[1]); + let expected_field_ty = type_args[1].clone(); self.reflected_construct_put(&ty, &expected_field_ty, &args[0], &args[1], &args[2]) } - "type.construct_finish" => { + IntrinsicId::TypeConstructFinish => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.reflected_construct_finish(&ty, &args[0]) } - "type.fields" => { + IntrinsicId::TypeFields => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4044,7 +6226,7 @@ impl Interpreter { .collect(), )) } - "type.bitfield_layout" => { + IntrinsicId::TypeBitfieldLayout => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4059,7 +6241,7 @@ impl Interpreter { } Ok(self.type_bitfield_value(self.type_expr_bitfield(&ty))) } - "type.bitfield_fields" => { + IntrinsicId::TypeBitfieldFields => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4080,7 +6262,7 @@ impl Interpreter { .collect(), )) } - "type.machine_layout" => { + IntrinsicId::TypeMachineLayout => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4095,7 +6277,7 @@ impl Interpreter { } Ok(self.type_machine_value(&type_expr_name(&ty), self.type_expr_machine(&ty))) } - "type.machine_states" => { + IntrinsicId::TypeMachineStates => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4119,7 +6301,7 @@ impl Interpreter { .collect(), )) } - "type.machine_transitions" => { + IntrinsicId::TypeMachineTransitions => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4143,13 +6325,13 @@ impl Interpreter { .collect(), )) } - "type.machine_state_value" => { + IntrinsicId::TypeMachineStateValue => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.reflected_machine_state_value(&args[0], &ty) } - "type.variants" => { + IntrinsicId::TypeVariants => { if let Some(err) = check_args(name, 0, args) { return Some(err); } @@ -4172,44 +6354,62 @@ impl Interpreter { .collect(), )) } - "type.variant_value" => { + IntrinsicId::TypeVariantValue => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.reflected_variant_value(&args[0], &ty) } - "type.field_value" => { + IntrinsicId::TypeFieldValue => { if let Some(err) = check_args(name, 2, args) { return Some(err); } - let expected_field_ty = self.substitute_type_expr(&type_args[1]); - self.reflected_field_value(&args[0], &ty, &args[1], &expected_field_ty) + let expected_field_ty = type_args[1].clone(); + self.reflected_field_value( + &args[0], + &ty, + &args[1], + &expected_field_ty, + checked_producer, + ) } - "type.machine_field_value" => { + IntrinsicId::TypeMachineFieldValue => { if let Some(err) = check_args(name, 2, args) { return Some(err); } - let expected_field_ty = self.substitute_type_expr(&type_args[1]); - self.reflected_machine_field_value(&args[0], &ty, &args[1], &expected_field_ty) + let expected_field_ty = type_args[1].clone(); + self.reflected_machine_field_value( + &args[0], + &ty, + &args[1], + &expected_field_ty, + checked_producer, + ) } - "type.variant_field_value" => { + IntrinsicId::TypeVariantFieldValue => { if let Some(err) = check_args(name, 2, args) { return Some(err); } - let expected_field_ty = self.substitute_type_expr(&type_args[1]); - self.reflected_variant_field_value(&args[0], &ty, &args[1], &expected_field_ty) + let expected_field_ty = type_args[1].clone(); + self.reflected_variant_field_value( + &args[0], + &ty, + &args[1], + &expected_field_ty, + checked_producer, + ) } - "json.parse" | "json.parse_exact" => { + IntrinsicId::JsonParse | IntrinsicId::JsonParseExact => { if let Some(err) = check_args(name, 1, args) { return Some(err); } self.call_trusted_json_public_bridge(name, type_args, args) } - "json.serialize" | "json.serialize_public" => { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => { if let Some(err) = check_args(name, 1, args) { return Some(err); } - if name == "json.serialize" + if intrinsic == IntrinsicId::JsonSerialize && self .checked_type_has_secret(&ty) .unwrap_or_else(|| self.type_expr_has_secret(&ty)) @@ -4221,7 +6421,7 @@ impl Interpreter { } self.call_trusted_json_public_bridge(name, type_args, args) } - _ => return None, + non_type_arg_intrinsics!() => return None, }) } @@ -4254,6 +6454,71 @@ impl Interpreter { .find_map(|scope| scope.get(name).cloned()) } + /// Installed arguments already have their lexical declaration identity. + /// Follow binding-name chains without resolving terminal names as source + /// spellings again. The cycle guard also bounds metadata-free API maps. + fn resolve_installed_type_expr(&self, ty: &TypeExpr) -> TypeExpr { + self.resolve_installed_type_expr_with_map(ty, &HashMap::new(), &mut HashSet::new()) + } + + fn resolve_installed_type_expr_with_map( + &self, + ty: &TypeExpr, + substitutions: &HashMap, + seen: &mut HashSet, + ) -> TypeExpr { + match ty { + TypeExpr::Named(ident) => { + // This is a canonical value, so an exact registered terminal + // remains its selected type even if a caller binding uses the + // same name. Written source substitution resolves its binding + // before entering here and retains lexical priority. + if self.type_name_is_registered(&ident.name) { + return ty.clone(); + } + if let Some(bound) = substitutions + .get(&ident.name) + .cloned() + .or_else(|| self.current_type_binding(&ident.name)) + && seen.insert(ident.name.clone()) + { + let resolved = + self.resolve_installed_type_expr_with_map(&bound, substitutions, seen); + seen.remove(&ident.name); + resolved + } else { + ty.clone() + } + } + TypeExpr::Generic(ident, args, span) => TypeExpr::Generic( + ident.clone(), + args.iter() + .map(|arg| self.resolve_installed_type_expr_with_map(arg, substitutions, seen)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.resolve_installed_type_expr_with_map(inner, substitutions, seen)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.resolve_installed_type_expr_with_map(inner, substitutions, seen)), + state.clone(), + *span, + ), + TypeExpr::Function(params, result, span) => TypeExpr::Function( + params + .iter() + .map(|param| { + self.resolve_installed_type_expr_with_map(param, substitutions, seen) + }) + .collect(), + Box::new(self.resolve_installed_type_expr_with_map(result, substitutions, seen)), + *span, + ), + } + } + fn substitute_type_expr(&self, ty: &TypeExpr) -> TypeExpr { self.substitute_type_expr_in_namespace(ty, self.current_namespace.as_deref()) } @@ -4266,7 +6531,7 @@ impl Interpreter { match ty { TypeExpr::Named(ident) => { if let Some(bound) = self.current_type_binding(&ident.name) { - self.substitute_type_expr_in_namespace(&bound, namespace) + self.resolve_installed_type_expr(&bound) } else { TypeExpr::Named(self.expand_type_ident(ident, namespace)) } @@ -4299,10 +6564,18 @@ impl Interpreter { } fn expand_type_ident(&self, ident: &Ident, namespace: Option<&str>) -> Ident { - let mut expanded = ident.clone(); if let Some(name) = self.expand_namespace_alias_name(&ident.name) { + let mut expanded = ident.clone(); expanded.name = name; - } else if !ident.name.contains('.') && !Self::is_builtin_type_name(&ident.name) { + expanded + } else { + self.qualify_declared_type_ident(ident, namespace) + } + } + + fn qualify_declared_type_ident(&self, ident: &Ident, namespace: Option<&str>) -> Ident { + let mut expanded = ident.clone(); + if !ident.name.contains('.') && !Self::is_builtin_type_name(&ident.name) { if let Some(namespace) = namespace { let qualified = format!("{namespace}.{}", ident.name); if self.type_name_is_registered(&qualified) { @@ -4313,6 +6586,40 @@ impl Interpreter { expanded } + /// Namespace-level alias bases are declaration templates, not references + /// to a validating caller's generic bindings or local namespace imports. + fn qualify_declared_type_expr(&self, ty: &TypeExpr, namespace: Option<&str>) -> TypeExpr { + match ty { + TypeExpr::Named(ident) => { + TypeExpr::Named(self.qualify_declared_type_ident(ident, namespace)) + } + TypeExpr::Generic(ident, args, span) => TypeExpr::Generic( + self.qualify_declared_type_ident(ident, namespace), + args.iter() + .map(|arg| self.qualify_declared_type_expr(arg, namespace)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.qualify_declared_type_expr(inner, namespace)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.qualify_declared_type_expr(inner, namespace)), + state.clone(), + *span, + ), + TypeExpr::Function(params, result, span) => TypeExpr::Function( + params + .iter() + .map(|param| self.qualify_declared_type_expr(param, namespace)) + .collect(), + Box::new(self.qualify_declared_type_expr(result, namespace)), + *span, + ), + } + } + fn is_builtin_type_name(name: &str) -> bool { matches!( name, @@ -4336,6 +6643,7 @@ impl Interpreter { fn type_name_is_registered(&self, name: &str) -> bool { self.structs.contains_key(name) + || self.interfaces.contains(name) || self.bitfields.contains_key(name) || self.enums.contains_key(name) || self.type_alias_bases.contains_key(name) @@ -4344,18 +6652,6 @@ impl Interpreter { || self.actor_defs.contains_key(name) } - fn substitute_type_expr_with_map( - &self, - ty: &TypeExpr, - substitutions: &HashMap, - ) -> TypeExpr { - self.substitute_type_expr_with_map_in_namespace( - ty, - substitutions, - self.current_namespace.as_deref(), - ) - } - fn substitute_type_expr_with_map_in_namespace( &self, ty: &TypeExpr, @@ -4369,10 +6665,10 @@ impl Interpreter { .cloned() .or_else(|| self.current_type_binding(&ident.name)) { - self.substitute_type_expr_with_map_in_namespace( + self.resolve_installed_type_expr_with_map( &bound, substitutions, - namespace, + &mut HashSet::new(), ) } else { TypeExpr::Named(self.expand_type_ident(ident, namespace)) @@ -4487,24 +6783,7 @@ impl Interpreter { } fn type_kind_tag_value(kind: &str) -> Value { - let variant = match kind { - "primitive" => "primitive_type", - "alias" => "alias_type", - "refinement" => "refinement_type", - "struct" => "struct_type", - "bitfield" => "bitfield_type", - "enum" => "enum_type", - "list" => "list_type", - "set" => "set_type", - "map" => "map_type", - "optional" => "optional_type", - "result" => "result_type", - "secret" => "secret_type", - "function" => "function_type", - "machine" => "machine_type", - "machine_state" => "machine_state_type", - _ => "unknown_type", - }; + let variant = ReflectionTypeInfo::kind_tag_variant(kind); Value::Enum { type_name: "TypeKind".to_string(), variant: variant.to_string(), @@ -4962,6 +7241,7 @@ impl Interpreter { .map(|(index, transition)| Self::type_machine_transition_value(index, transition)) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeMachine".to_string(), fields: vec![ ("states".to_string(), Value::List(states)), @@ -4990,6 +7270,7 @@ impl Interpreter { }) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeMachineState".to_string(), fields: vec![ ("index".to_string(), Value::Int64(index as i64)), @@ -5009,6 +7290,7 @@ impl Interpreter { transition: ReflectionMachineTransition, ) -> Value { Value::Struct { + concrete_type: None, type_name: "TypeMachineTransition".to_string(), fields: vec![ ("index".to_string(), Value::Int64(index as i64)), @@ -5110,6 +7392,7 @@ impl Interpreter { let has_secret = self.type_expr_has_secret(&field.ty); let type_info = self.type_info_value(&field.ty); Value::Struct { + concrete_type: None, type_name: "TypeField".to_string(), fields: vec![ ("index".to_string(), Value::Int64(index as i64)), @@ -5139,7 +7422,7 @@ impl Interpreter { } fn reflected_field_loop_bindings( - &self, + &mut self, iterable: &Expr, ) -> Result>, String> { let Some(owner_ty) = comptime_type_fields_binding(iterable) else { @@ -5166,7 +7449,7 @@ impl Interpreter { } Ok(Some( - self.type_expr_fields(&owner_ty) + self.with_resolved_type_aliases(|interpreter| interpreter.type_expr_fields(&owner_ty)) .into_iter() .enumerate() .map(|(index, field)| ReflectedFieldBinding { @@ -5513,7 +7796,7 @@ impl Interpreter { } fn reflected_type_info_arg_loop_bindings( - &self, + &mut self, iterable: &Expr, ) -> Result>, String> { let Some(source_ty) = reflected_type_info_args_source(iterable) else { @@ -5534,13 +7817,14 @@ impl Interpreter { ty } }; - Ok(Some( - self.checked_type_info_arg_types(&ty) - .unwrap_or_else(|| self.type_info_arg_types(&ty)) + Ok(Some(self.with_resolved_type_aliases(|interpreter| { + interpreter + .checked_type_info_arg_types(&ty) + .unwrap_or_else(|| interpreter.type_info_arg_types(&ty)) .into_iter() .map(|ty| ReflectedTypeInfoBinding { ty }) - .collect(), - )) + .collect() + }))) } fn bound_reflected_field_type(&self, field_name: &str) -> Result { @@ -5587,7 +7871,7 @@ impl Interpreter { metadata.type_name )); } - Ok(Some(self.substitute_type_expr(&binding.ty))) + Ok(Some(binding.ty.clone())) } fn bound_reflected_type_info_type(&self, info_name: &str) -> Result { @@ -5621,7 +7905,7 @@ impl Interpreter { if type_name != expected_type_name { return Err("`comptime type` reflected TypeInfo metadata no longer matches the trusted `args` loop item".to_string()); } - Ok(Some(self.substitute_type_expr(&binding.ty))) + Ok(Some(binding.ty.clone())) } fn type_info_arg_types(&self, ty: &TypeExpr) -> Vec { @@ -5634,7 +7918,12 @@ impl Interpreter { TypeExpr::Named(ident) if self.type_aliases.contains_key(&ident.name) => self .type_alias_bases .get(&ident.name) - .map(|base_ty| vec![self.substitute_type_expr(base_ty)]) + .map(|base_ty| { + vec![self.qualify_declared_type_expr( + base_ty, + Self::type_name_namespace(&ident.name), + )] + }) .unwrap_or_default(), TypeExpr::Generic(_, args, _) => args .iter() @@ -5662,23 +7951,24 @@ impl Interpreter { return type_name.to_string(); } - if let Some(base_ty) = self.type_alias_bases.get(type_name) { + let compared = if let Some(base_ty) = self.type_alias_bases.get(type_name) { let namespace = Self::type_name_namespace(type_name).or(self.current_namespace.as_deref()); let base_ty = self.substitute_type_expr_in_namespace(base_ty, namespace); - return self.reflection_compare_type_expr_inner(&base_ty, visited); - } - - if let Some((owner, args)) = Self::split_generic_type_display(type_name) { + self.reflection_compare_type_expr_inner(&base_ty, visited) + } else if let Some((owner, args)) = Self::split_generic_type_display(type_name) { let owner = self.reflection_compare_type_name_inner(owner, visited); let args = Self::split_type_display_args(args) .into_iter() .map(|arg| self.reflection_compare_type_name_inner(arg.trim(), visited)) .collect::>(); - return format!("{owner}[{}]", args.join(", ")); - } - - type_name.to_string() + format!("{owner}[{}]", args.join(", ")) + } else { + type_name.to_string() + }; + // Only an active expansion is a cycle; sibling arguments are independent. + visited.remove(type_name); + compared } fn reflection_compare_type_expr(&self, ty: &TypeExpr) -> String { @@ -5736,6 +8026,25 @@ impl Interpreter { self.reflection_compare_type_name(actual) == self.reflection_compare_type_expr(expected) } + fn split_function_type_display(type_name: &str) -> Option<(&str, &str)> { + let rest = type_name.strip_prefix("function(")?; + let mut depth = 1; + for (index, ch) in rest.char_indices() { + match ch { + '(' => depth += 1, + ')' => { + depth -= 1; + if depth == 0 { + let result = rest[index + 1..].strip_prefix(" returns ")?; + return (!result.is_empty()).then_some((&rest[..index], result)); + } + } + _ => {} + } + } + None + } + fn split_generic_type_display(type_name: &str) -> Option<(&str, &str)> { let open = type_name.find('[')?; if !type_name.ends_with(']') { @@ -5792,6 +8101,7 @@ impl Interpreter { .map(|(index, field)| self.type_bitfield_field_value(index, field)) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeBitfield".to_string(), fields: vec![ ( @@ -5810,6 +8120,7 @@ impl Interpreter { .map(|ty| Value::OptionalSome(Box::new(self.type_info_value(ty)))) .unwrap_or(Value::OptionalNone); Value::Struct { + concrete_type: None, type_name: "TypeBitfieldField".to_string(), fields: vec![ ("index".to_string(), Value::Int64(index as i64)), @@ -5847,6 +8158,7 @@ impl Interpreter { .collect::>(); Value::Struct { + concrete_type: None, type_name: "TypeVariant".to_string(), fields: vec![ ("index".to_string(), Value::Int64(index as i64)), @@ -6015,6 +8327,14 @@ impl Interpreter { } fn checked_type_fields_value(&self, ty: &TypeExpr) -> Option { + if self + .reflection_metadata + .as_ref()? + .get_source_type_info(&type_expr_display(ty)) + .is_some_and(|info| !matches!(info.kind.as_str(), "struct" | "bitfield")) + { + return Some(Value::List(Vec::new())); + } let fields = self.checked_type_fields(ty)?; let owner_type = type_expr_display(ty); Some(Value::List( @@ -6025,12 +8345,14 @@ impl Interpreter { )) } - fn reflection_field_info_value( + /// Encode checked field metadata for reflection and native constant baking. + pub fn reflection_field_info_value( owner_type: &str, owner_member: Option<&str>, field: &ReflectionFieldInfo, ) -> Value { Value::Struct { + concrete_type: None, type_name: "TypeField".to_string(), fields: vec![ ("index".to_string(), Value::Int64(field.index as i64)), @@ -6109,6 +8431,7 @@ impl Interpreter { .map(Self::reflection_bitfield_field_info_value) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeBitfield".to_string(), fields: vec![ ( @@ -6127,6 +8450,7 @@ impl Interpreter { .map(|info| Value::OptionalSome(Box::new(Self::reflection_type_info_value(info)))) .unwrap_or(Value::OptionalNone); Value::Struct { + concrete_type: None, type_name: "TypeBitfieldField".to_string(), fields: vec![ ("index".to_string(), Value::Int64(field.index as i64)), @@ -6202,6 +8526,7 @@ impl Interpreter { .map(Self::reflection_machine_transition_info_value) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeMachine".to_string(), fields: vec![ ("states".to_string(), Value::List(states)), @@ -6210,7 +8535,8 @@ impl Interpreter { } } - fn reflection_machine_state_info_value( + /// Encode checked state metadata without evaluating source code. + pub fn reflection_machine_state_info_value( owner_type: &str, state: &ReflectionMachineStateInfo, ) -> Value { @@ -6221,6 +8547,7 @@ impl Interpreter { .map(|field| Self::reflection_field_info_value(owner_type, Some(owner_member), field)) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeMachineState".to_string(), fields: vec![ ("index".to_string(), Value::Int64(state.index as i64)), @@ -6239,6 +8566,7 @@ impl Interpreter { transition: &ReflectionMachineTransitionInfo, ) -> Value { Value::Struct { + concrete_type: None, type_name: "TypeMachineTransition".to_string(), fields: vec![ ("index".to_string(), Value::Int64(transition.index as i64)), @@ -6279,7 +8607,11 @@ impl Interpreter { )) } - fn reflection_variant_info_value(owner_type: &str, variant: &ReflectionVariantInfo) -> Value { + /// Encode checked variant metadata without evaluating source code. + pub fn reflection_variant_info_value( + owner_type: &str, + variant: &ReflectionVariantInfo, + ) -> Value { let owner_member = variant.name.as_str(); let fields = variant .fields @@ -6287,6 +8619,7 @@ impl Interpreter { .map(|field| Self::reflection_field_info_value(owner_type, Some(owner_member), field)) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeVariant".to_string(), fields: vec![ ("index".to_string(), Value::Int64(variant.index as i64)), @@ -6312,6 +8645,7 @@ impl Interpreter { .map(Self::reflection_type_info_value) .collect(); Value::Struct { + concrete_type: None, type_name: "TypeInfo".to_string(), fields: vec![ ( @@ -6358,6 +8692,7 @@ impl Interpreter { }; Value::Struct { + concrete_type: None, type_name: "TypeInfo".to_string(), fields: vec![ ( @@ -6386,12 +8721,14 @@ impl Interpreter { } fn reflected_field_value( - &self, + &mut self, value: &Value, owner_ty: &TypeExpr, field_metadata: &Value, expected_field_ty: &TypeExpr, + checked_producer: bool, ) -> Result { + let value = value.payload(); let metadata = Self::type_field_metadata_for(field_metadata, "type.field_value")?; let expected_owner = type_expr_display(owner_ty); Self::validate_field_metadata_owner(&metadata, &expected_owner, None, "type.field_value")?; @@ -6460,24 +8797,35 @@ impl Interpreter { }; let expected_type_name = type_expr_display(expected_field_ty); - if !self.reflection_type_name_matches_expr(&actual_type_name, expected_field_ty) { + if !self.reflection_type_name_matches_expr(&actual_type_name, expected_field_ty) + && !(checked_producer + && self.allow_checked_refinement_proofs + && self.has_checked_reflected_callable_plan(&actual_type_name, expected_field_ty)) + { return Err(format!( "type.field_value: field '{}' has type '{}', requested '{}'", metadata.name, actual_type_name, expected_type_name )); } - match value { + let selected = match value { Value::Struct { fields, .. } => fields .iter() .find(|(name, _)| name == &field_name) .map(|(_, field_value)| field_value.clone()) .ok_or_else(|| format!("type.field_value: value is missing field '{field_name}'")), other => Err(format!( - "type.field_value: expected struct value for '{}', got {other}", - type_expr_display(owner_ty) + "type.field_value: expected struct value for '{}', got {}", + type_expr_display(owner_ty), + self.format_debug_value(other, Some(owner_ty)) )), - } + }?; + self.establish_reflected_field_refinement( + selected, + expected_field_ty, + &actual_type_name, + checked_producer, + ) } fn reflected_machine_state_value( @@ -6485,13 +8833,15 @@ impl Interpreter { value: &Value, owner_ty: &TypeExpr, ) -> Result { + let value = value.payload(); let Value::Machine { type_name, state, .. } = value else { return Err(format!( - "type.machine_state_value: expected machine value for '{}', got {value}", - type_expr_display(owner_ty) + "type.machine_state_value: expected machine value for '{}', got {}", + type_expr_display(owner_ty), + self.format_debug_value(value, Some(owner_ty)) )); }; @@ -6547,12 +8897,14 @@ impl Interpreter { } fn reflected_machine_field_value( - &self, + &mut self, value: &Value, owner_ty: &TypeExpr, field_metadata: &Value, expected_field_ty: &TypeExpr, + checked_producer: bool, ) -> Result { + let value = value.payload(); let metadata = Self::type_field_metadata_for(field_metadata, "type.machine_field_value")?; let Value::Machine { type_name, @@ -6561,8 +8913,9 @@ impl Interpreter { } = value else { return Err(format!( - "type.machine_field_value: expected machine value for '{}', got {value}", - type_expr_display(owner_ty) + "type.machine_field_value: expected machine value for '{}', got {}", + type_expr_display(owner_ty), + self.format_debug_value(value, Some(owner_ty)) )); }; @@ -6667,19 +9020,27 @@ impl Interpreter { )); } - fields.get(metadata.index).cloned().ok_or_else(|| { + let selected = fields.get(metadata.index).cloned().ok_or_else(|| { format!("type.machine_field_value: value is missing payload field '{field_name}'") - }) + })?; + self.establish_reflected_field_refinement( + selected, + expected_field_ty, + &actual_type_name, + checked_producer, + ) } fn reflected_variant_value(&self, value: &Value, owner_ty: &TypeExpr) -> Result { + let value = value.payload(); let Value::Enum { type_name, variant, .. } = value else { return Err(format!( - "type.variant_value: expected enum value for '{}', got {value}", - type_expr_display(owner_ty) + "type.variant_value: expected enum value for '{}', got {}", + type_expr_display(owner_ty), + self.format_debug_value(value, Some(owner_ty)) )); }; @@ -6724,12 +9085,14 @@ impl Interpreter { } fn reflected_variant_field_value( - &self, + &mut self, value: &Value, owner_ty: &TypeExpr, field_metadata: &Value, expected_field_ty: &TypeExpr, + checked_producer: bool, ) -> Result { + let value = value.payload(); let metadata = Self::type_field_metadata_for(field_metadata, "type.variant_field_value")?; let Value::Enum { type_name, @@ -6738,8 +9101,9 @@ impl Interpreter { } = value else { return Err(format!( - "type.variant_field_value: expected enum value for '{}', got {value}", - type_expr_display(owner_ty) + "type.variant_field_value: expected enum value for '{}', got {}", + type_expr_display(owner_ty), + self.format_debug_value(value, Some(owner_ty)) )); }; @@ -6797,12 +9161,19 @@ impl Interpreter { )); } - return fields.get(metadata.index).cloned().ok_or_else(|| { + let selected = fields.get(metadata.index).cloned().ok_or_else(|| { format!( "type.variant_field_value: value is missing payload field '{}'", field.name ) - }); + })?; + let actual_type_name = field.type_name.clone(); + return self.establish_reflected_field_refinement( + selected, + expected_field_ty, + &actual_type_name, + checked_producer, + ); } if self.checked_metadata_kind_is(owner_ty, &["enum"]) { @@ -6849,80 +9220,414 @@ impl Interpreter { )); } - fields.get(metadata.index).cloned().ok_or_else(|| { + let selected = fields.get(metadata.index).cloned().ok_or_else(|| { format!( "type.variant_field_value: value is missing payload field '{}'", field.name ) - }) + })?; + self.establish_reflected_field_refinement( + selected, + expected_field_ty, + &actual_type_name, + checked_producer, + ) } - fn reflected_construct_put( - &self, - owner_ty: &TypeExpr, - expected_field_ty: &TypeExpr, - builder: &Value, - field_metadata: &Value, - value: &Value, + /// Reflection's existing requested-type compatibility can erase refinement + /// identities. A requested refinement therefore needs its remaining checks + /// before its declared result becomes reusable proof in checked source. + fn establish_reflected_field_refinement( + &mut self, + mut value: Value, + requested: &TypeExpr, + actual_type_name: &str, + checked_producer: bool, ) -> Result { - let Value::TypeConstruction { - type_name, - variant, - state, - fields: existing_fields, - } = builder - else { - return Err(format!( - "type.construct_put: first argument must be TypeConstruction, got {builder}" - )); - }; - - let expected_owner = type_expr_display(owner_ty); - if type_name != &expected_owner { - return Ok(result_fail(format!( - "type.construct_put: builder for '{}' cannot construct '{}'", - type_name, expected_owner - ))); + if !checked_producer || !self.allow_checked_refinement_proofs { + return Ok(value); } - let owner_kind = self - .checked_construction_kind(owner_ty) - .unwrap_or_else(|| self.type_expr_kind(owner_ty)); - if !matches!( - owner_kind, - "struct" | "bitfield" | "enum" | "machine" | "machine_state" - ) { - return Ok(result_fail(format!( - "type.construct_put supports only structs, bitfields, enums, and machines, got '{}'", - type_expr_display(owner_ty) - ))); + let requested = self.concrete_resolved_type_expr(requested, &mut HashSet::new()); + if !self.producer_type_has_refinement(&requested) { + return Ok(value); + } + if matches!(requested, TypeExpr::Function(..)) { + // New callable result proof is lazy; retain its original descriptor. + return self.prepare_reflected_callable(value, &requested, actual_type_name); + } + let actual = Self::simple_type_expr_from_name(actual_type_name, requested.span()) + .map(|actual| self.concrete_resolved_type_expr(&actual, &mut HashSet::new())); + if matches!(&requested, TypeExpr::Named(_)) { + if let Some(actual) = &actual { + if let Some(proven_prefix) = + self.producer_declared_refinement_proof(actual, &requested) + { + // The actual declared root chain establishes this exact + // requested prefix. Normalize and retain its result identity + // without evaluating that already established predicate. + return self.normalize_and_validate_value_from_source( + &requested, + value, + Some(&proven_prefix), + ); + } + } + } + let mut checks = Vec::new(); + self.plan_produced_refinements( + &requested, + actual.as_ref(), + &value, + &mut Vec::new(), + &mut checks, + )?; + // Finish structural preflight before any predicate can execute. The + // plan follows occupied arms and collection order; it never joins. + for check in checks { + let slot = Self::produced_value_slot(&mut value, &check.path).ok_or_else(|| { + "reflected field refinement: checked payload shape changed during validation" + .to_string() + })?; + *slot = self.normalize_and_validate_value_from_source( + &check.requested, + slot.clone(), + check.source.as_deref(), + )?; } + Ok(value) + } - let metadata = match Self::type_field_metadata_for(field_metadata, "type.construct_put") { - Ok(metadata) => metadata, - Err(message) => return Ok(result_fail(message)), + fn producer_type_has_refinement(&self, ty: &TypeExpr) -> bool { + match ty { + TypeExpr::Named(name) => self.type_name_has_refinement(&name.name), + TypeExpr::Generic(_, args, _) => { + args.iter().any(|ty| self.producer_type_has_refinement(ty)) + } + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + self.producer_type_has_refinement(inner) + } + TypeExpr::Function(params, result, _) => params + .iter() + .chain(std::iter::once(result.as_ref())) + .any(|ty| self.producer_type_has_refinement(ty)), + } + } + + fn producer_refinement_base(&self, name: &str) -> Option { + let base = self.type_alias_bases.get(name)?; + // Refinement declarations have no caller type parameters. Resolve the + // stored spelling in its declaration, using the existing declaration- + // only resolver rather than the caller's generic bindings. + Some(self.struct_field_type(base, &HashMap::new(), Self::type_name_namespace(name))) + } + + /// Recover an established prefix only from the actual declared root + /// refinement chain. Outer qualifiers and nominal child fields are not + /// root proofs, and runtime value labels never enter this lookup. + fn producer_declared_refinement_proof( + &self, + actual: &TypeExpr, + requested: &TypeExpr, + ) -> Option { + let TypeExpr::Named(requested) = requested else { + return None; }; + // Both owners were made canonical by the producer's declaration-only + // resolver. Do not reinterpret them through a caller namespace alias. + if !self + .type_aliases + .get(&requested.name) + .is_some_and(Option::is_some) + { + return None; + } + let mut actual = actual.clone(); + let mut visited = HashSet::new(); + while let TypeExpr::Named(name) = &actual { + if name.name == requested.name { + return Some(name.name.clone()); + } + if !visited.insert(name.name.clone()) + || !self + .type_aliases + .get(&name.name) + .is_some_and(Option::is_some) + { + return None; + } + let base = self.producer_refinement_base(&name.name)?; + actual = self.concrete_resolved_type_expr(&base, &mut HashSet::new()); + } + None + } - let (field_name, actual_type_name) = if owner_kind == "enum" { - if state.is_some() { - return Ok(result_fail(format!( - "type.construct_put: machine state builder cannot construct '{}'", - expected_owner - ))); + fn producer_schema_carrier(&self, ty: &TypeExpr) -> TypeExpr { + let mut ty = self.concrete_resolved_type_expr(ty, &mut HashSet::new()); + let mut visited = HashSet::new(); + while let TypeExpr::Named(name) = &ty { + if !visited.insert(name.name.clone()) { + break; } - let Some(variant_name) = variant.as_ref() else { - return Ok(result_fail( - "type.construct_put: enum construction requires type.construct_variant_start" - .to_string(), - )); + let Some(base) = self.producer_refinement_base(&name.name) else { + break; }; - if let Err(message) = Self::validate_field_metadata_owner( - &metadata, - &expected_owner, - Some(variant_name), - "type.construct_put", - ) { - return Ok(result_fail(message)); - } + ty = self.concrete_resolved_type_expr(&base, &mut HashSet::new()); + } + ty + } + + fn plan_produced_refinements( + &self, + requested: &TypeExpr, + actual: Option<&TypeExpr>, + value: &Value, + path: &mut Vec, + checks: &mut Vec, + ) -> Result<(), String> { + let requested = self.concrete_resolved_type_expr(requested, &mut HashSet::new()); + let actual = actual.map(|ty| self.concrete_resolved_type_expr(ty, &mut HashSet::new())); + if actual.as_ref().is_some_and(|actual| { + type_expr_display(actual) == type_expr_display(&requested) + || self + .producer_declared_refinement_proof(actual, &requested) + .is_some() + }) || !self.producer_type_has_refinement(&requested) + { + return Ok(()); + } + if let TypeExpr::Named(name) = &requested { + let Some(base) = self.producer_refinement_base(&name.name) else { + return Ok(()); + }; + self.plan_produced_refinements(&base, actual.as_ref(), value, path, checks)?; + checks.push(ProducedRefinementCheck { + path: path.clone(), + requested, + // The base was either already established by the actual schema + // or established by an earlier check in this private plan. + source: Some(self.resolved_concrete_type_display(&base)), + }); + return Ok(()); + } + if let TypeExpr::View(inner, _) = &requested { + let actual = actual.as_ref().map(|ty| match ty { + TypeExpr::View(inner, _) => inner.as_ref(), + ty => ty, + }); + return self.plan_produced_refinements(inner, actual, value, path, checks); + } + let TypeExpr::Generic(owner, requested_args, _) = &requested else { + return Err( + "reflected field refinement: unsupported callable or nominal refinement conversion" + .into(), + ); + }; + if owner.name == "secret" { + return Err( + "reflected field refinement: cannot establish new invariants beneath secret".into(), + ); + } + let expected_arity = match owner.name.as_str() { + "list" | "set" | "optional" => 1, + "map" | "result" => 2, + _ => return Err( + "reflected field refinement: unsupported callable or nominal refinement conversion" + .into(), + ), + }; + if requested_args.len() != expected_arity { + return Err("reflected field refinement: unsupported requested wrapper shape".into()); + } + let actual_carrier = actual.as_ref().map(|ty| self.producer_schema_carrier(ty)); + let actual_args = match &actual_carrier { + Some(TypeExpr::Generic(actual_owner, args, _)) + if actual_owner.name == owner.name && args.len() == expected_arity => + { + Some(args.as_slice()) + } + _ => None, + }; + if let Value::Typed { value, .. } = value { + path.push(ProducedValueStep::Typed); + let result = + self.plan_produced_refinements(&requested, actual.as_ref(), value, path, checks); + path.pop(); + return result; + } + let child_actual = |index| actual_args.and_then(|args| args.get(index)); + match (owner.name.as_str(), value) { + ("list", Value::List(items)) | ("set", Value::Set(items)) => { + for (index, item) in items.iter().enumerate() { + path.push(ProducedValueStep::Sequence(index)); + self.plan_produced_refinements( + &requested_args[0], + child_actual(0), + item, + path, + checks, + )?; + path.pop(); + } + } + ("map", Value::Map(items)) => { + for (index, (key, value)) in items.iter().enumerate() { + path.push(ProducedValueStep::MapKey(index)); + self.plan_produced_refinements( + &requested_args[0], + child_actual(0), + key, + path, + checks, + )?; + path.pop(); + path.push(ProducedValueStep::MapValue(index)); + self.plan_produced_refinements( + &requested_args[1], + child_actual(1), + value, + path, + checks, + )?; + path.pop(); + } + } + ("optional", Value::OptionalNone) => {} + ("optional", Value::OptionalSome(inner)) => { + path.push(ProducedValueStep::Some); + self.plan_produced_refinements( + &requested_args[0], + child_actual(0), + inner, + path, + checks, + )?; + path.pop(); + } + ("result", Value::ResultOk(inner)) => { + path.push(ProducedValueStep::Ok); + self.plan_produced_refinements( + &requested_args[0], + child_actual(0), + inner, + path, + checks, + )?; + path.pop(); + } + ("result", Value::ResultFail(inner)) => { + path.push(ProducedValueStep::Fail); + self.plan_produced_refinements( + &requested_args[1], + child_actual(1), + inner, + path, + checks, + )?; + path.pop(); + } + // Pending owners are not joined or inspected to prove children. + _ => { + return Err(format!( + "reflected field refinement: expected a ready {} value", + owner.name + )); + } + } + Ok(()) + } + + fn produced_value_slot<'a>( + mut value: &'a mut Value, + path: &[ProducedValueStep], + ) -> Option<&'a mut Value> { + for step in path { + value = match (step, value) { + (ProducedValueStep::Typed, Value::Typed { value, .. }) => value.as_mut(), + (ProducedValueStep::Sequence(index), Value::List(items) | Value::Set(items)) => { + items.get_mut(*index)? + } + (ProducedValueStep::MapKey(index), Value::Map(items)) => { + &mut items.get_mut(*index)?.0 + } + (ProducedValueStep::MapValue(index), Value::Map(items)) => { + &mut items.get_mut(*index)?.1 + } + (ProducedValueStep::Some, Value::OptionalSome(inner)) + | (ProducedValueStep::Ok, Value::ResultOk(inner)) + | (ProducedValueStep::Fail, Value::ResultFail(inner)) => inner.as_mut(), + _ => return None, + }; + } + Some(value) + } + + fn reflected_construct_put( + &self, + owner_ty: &TypeExpr, + expected_field_ty: &TypeExpr, + builder: &Value, + field_metadata: &Value, + value: &Value, + ) -> Result { + let Value::TypeConstruction { + type_name, + variant, + state, + fields: existing_fields, + } = builder.payload() + else { + return Err(format!( + "type.construct_put: first argument must be TypeConstruction, got {}", + self.format_debug_value(builder, None) + )); + }; + + let expected_owner = type_expr_display(owner_ty); + if type_name != &expected_owner { + return Ok(result_fail(format!( + "type.construct_put: builder for '{}' cannot construct '{}'", + type_name, expected_owner + ))); + } + let owner_kind = self + .checked_construction_kind(owner_ty) + .unwrap_or_else(|| self.type_expr_kind(owner_ty)); + if !matches!( + owner_kind, + "struct" | "bitfield" | "enum" | "machine" | "machine_state" + ) { + return Ok(result_fail(format!( + "type.construct_put supports only structs, bitfields, enums, and machines, got '{}'", + type_expr_display(owner_ty) + ))); + } + + let metadata = match Self::type_field_metadata_for(field_metadata, "type.construct_put") { + Ok(metadata) => metadata, + Err(message) => return Ok(result_fail(message)), + }; + + let (field_name, actual_type_name) = if owner_kind == "enum" { + if state.is_some() { + return Ok(result_fail(format!( + "type.construct_put: machine state builder cannot construct '{}'", + expected_owner + ))); + } + let Some(variant_name) = variant.as_ref() else { + return Ok(result_fail( + "type.construct_put: enum construction requires type.construct_variant_start" + .to_string(), + )); + }; + if let Err(message) = Self::validate_field_metadata_owner( + &metadata, + &expected_owner, + Some(variant_name), + "type.construct_put", + ) { + return Ok(result_fail(message)); + } if let Some(variants) = self.checked_type_variants(owner_ty) { let Some(variant) = variants @@ -7440,10 +10145,11 @@ impl Interpreter { variant, state, fields, - } = builder + } = builder.payload() else { return Err(format!( - "type.construct_finish: first argument must be TypeConstruction, got {builder}" + "type.construct_finish: first argument must be TypeConstruction, got {}", + self.format_debug_value(builder, None) )); }; @@ -7561,18 +10267,15 @@ impl Interpreter { }; let field_ty = self.substitute_type_expr(&field.ty); - let type_name = type_expr_name(&field_ty); - let value = match self.normalize_value_for_type(&field_ty, value.clone()) { + let value = match self.normalize_and_validate_value(&field_ty, value.clone()) { Ok(value) => value, Err(message) => return Ok(result_fail(message)), }; - if let Err(message) = self.check_refinement(&type_name, &value) { - return Ok(result_fail(message)); - } struct_fields.push((field.name.clone(), value)); } Ok(result_ok(Value::Struct { + concrete_type: Some(self.concrete_type_display(owner_ty)), type_name: type_expr_name(owner_ty), fields: struct_fields, })) @@ -7609,6 +10312,7 @@ impl Interpreter { } Ok(result_ok(Value::Struct { + concrete_type: Some(self.concrete_type_display(owner_ty)), type_name: type_expr_name(owner_ty), fields: struct_fields, })) @@ -7702,14 +10406,10 @@ impl Interpreter { }; let field_ty = self.substitute_type_expr(&field.ty); - let type_name = type_expr_name(&field_ty); - let value = match self.normalize_value_for_type(&field_ty, value.clone()) { + let value = match self.normalize_and_validate_value(&field_ty, value.clone()) { Ok(value) => value, Err(message) => return Ok(result_fail(message)), }; - if let Err(message) = self.check_refinement(&type_name, &value) { - return Ok(result_fail(message)); - } enum_fields.push(value); } @@ -7818,14 +10518,10 @@ impl Interpreter { }; let field_ty = self.substitute_type_expr(&field.ty); - let type_name = type_expr_name(&field_ty); - let value = match self.normalize_value_for_type(&field_ty, value.clone()) { + let value = match self.normalize_and_validate_value(&field_ty, value.clone()) { Ok(value) => value, Err(message) => return Ok(result_fail(message)), }; - if let Err(message) = self.check_refinement(&type_name, &value) { - return Ok(result_fail(message)); - } machine_fields.push(value); } @@ -7885,6 +10581,7 @@ impl Interpreter { } return Ok(result_ok(Value::Struct { + concrete_type: None, type_name: bitfield_name, fields: bitfield_fields, })); @@ -7947,6 +10644,7 @@ impl Interpreter { } Ok(result_ok(Value::Struct { + concrete_type: None, type_name: bitfield_name, fields: bitfield_fields, })) @@ -7960,7 +10658,12 @@ impl Interpreter { value: &Value, caller: &str, ) -> Result<(usize, String, String, i64), String> { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{caller}: argument must be TypeVariant, got {value}" )); @@ -8019,7 +10722,12 @@ impl Interpreter { value: &Value, caller: &str, ) -> Result, String> { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{caller}: argument must be TypeVariant, got {value}" )); @@ -8050,7 +10758,12 @@ impl Interpreter { value: &Value, caller: &str, ) -> Result<(usize, String, String), String> { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{caller}: argument must be TypeMachineState, got {value}" )); @@ -8101,7 +10814,12 @@ impl Interpreter { value: &Value, caller: &str, ) -> Result, String> { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{caller}: argument must be TypeMachineState, got {value}" )); @@ -8129,7 +10847,12 @@ impl Interpreter { } fn type_field_metadata_for(value: &Value, caller: &str) -> Result { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!( "{caller}: second argument must be TypeField, got {value}" )); @@ -8235,8 +10958,55 @@ impl Interpreter { )) } + fn reflection_info_from_value(value: &Value) -> Result { + let type_name = Self::type_info_metadata(value)?; + let Value::Struct { fields, .. } = value else { + unreachable!() + }; + let field = |name: &str| { + fields + .iter() + .find(|(key, _)| key == name) + .map(|(_, value)| value) + .ok_or_else(|| format!("TypeInfo is missing '{name}'")) + }; + let Value::String(kind) = field("kind")? else { + return Err("TypeInfo.kind must be string".into()); + }; + let Value::Bool(has_secret) = field("has_secret")? else { + return Err("TypeInfo.has_secret must be bool".into()); + }; + let Value::List(args) = field("args")? else { + return Err("TypeInfo.args must be a list".into()); + }; + let primitive_tag = match field("primitive_tag")? { + Value::OptionalNone => None, + Value::OptionalSome(value) => match value.as_ref() { + Value::Enum { + type_name, variant, .. + } if type_name == "TypePrimitive" => Some(variant.clone()), + _ => return Err("TypeInfo.primitive_tag must contain TypePrimitive".into()), + }, + _ => return Err("TypeInfo.primitive_tag must be optional".into()), + }; + Ok(ReflectionTypeInfo::new( + type_name, + kind, + primitive_tag, + *has_secret, + args.iter() + .map(Self::reflection_info_from_value) + .collect::>()?, + )) + } + fn type_info_metadata(value: &Value) -> Result { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!("expected TypeInfo, got {value}")); }; if type_name != "TypeInfo" { @@ -8295,53 +11065,21 @@ impl Interpreter { let Some(provider) = self.random_provider.as_mut() else { return Err("Random: entropy unavailable".to_string()); }; - match provider { - RandomProvider::Production(rng) => { - Ok(unbiased_bounded_offset(width, || rng.next_u64())) - } - RandomProvider::Scripted(samples) => match samples.front().copied() { - None => Err("Random: test provider exhausted".to_string()), - Some(RandomTestSample::Bounded(offset)) if offset < width => { - samples.pop_front(); - Ok(offset) - } - Some(_) => Err("Random: invalid test sample".to_string()), - }, - } + provider.bounded_offset(width).map_err(str::to_owned) } fn random_unit53(&mut self) -> Result { let Some(provider) = self.random_provider.as_mut() else { return Err("Random: entropy unavailable".to_string()); }; - match provider { - RandomProvider::Production(rng) => Ok(rng.next_u64() >> 11), - RandomProvider::Scripted(samples) => match samples.front().copied() { - None => Err("Random: test provider exhausted".to_string()), - Some(RandomTestSample::Unit53(bits)) if bits < (1_u64 << 53) => { - samples.pop_front(); - Ok(bits) - } - Some(_) => Err("Random: invalid test sample".to_string()), - }, - } + provider.unit53().map_err(str::to_owned) } fn random_boolean(&mut self) -> Result { let Some(provider) = self.random_provider.as_mut() else { return Err("Random: entropy unavailable".to_string()); }; - match provider { - RandomProvider::Production(rng) => Ok(rng.next_u32() & 1 == 1), - RandomProvider::Scripted(samples) => match samples.front().copied() { - None => Err("Random: test provider exhausted".to_string()), - Some(RandomTestSample::Boolean(value)) => { - samples.pop_front(); - Ok(value) - } - Some(_) => Err("Random: invalid test sample".to_string()), - }, - } + provider.boolean().map_err(str::to_owned) } fn clock_now_milliseconds(&mut self) -> Result { @@ -8360,27 +11098,32 @@ impl Interpreter { unix_seconds, subsecond_nanoseconds, }) => { - checked_clock_milliseconds(unix_seconds, subsecond_nanoseconds)?; + checked_clock_milliseconds(unix_seconds, subsecond_nanoseconds) + .map_err(str::to_owned)?; samples.pop_front(); (unix_seconds, subsecond_nanoseconds) } }, }; - checked_clock_milliseconds(seconds, nanoseconds) + checked_clock_milliseconds(seconds, nanoseconds).map_err(str::to_owned) } fn call_builtin(&mut self, name: &str, args: &[Value]) -> Option> { - if let Some(result) = self.call_bitfield_builtin(name, args) { - return Some(result); + let intrinsic = IntrinsicId::from_callable_name(name)?; + if matches!( + intrinsic, + IntrinsicId::BitfieldFromBytes | IntrinsicId::BitfieldToBytes + ) { + return self.call_bitfield_builtin(intrinsic, name, args); } - match name { + match intrinsic { // ================================================================= // COMPILER PRIMITIVES // ================================================================= // -- I/O (capability-simulated) ----------------------------------- - "Stdout.write" => { + IntrinsicId::StdoutWrite => { // Stdout.write(stdout, message) — ignore capability, print message if args.len() < 2 { return Some(Err(format!( @@ -8394,14 +11137,14 @@ impl Interpreter { } // -- Secret-safe operations -------------------------------------- - "secret.redact" => { + IntrinsicId::SecretRedact => { require_args!(name, 1, args); Some(Ok(Value::String("***".to_string()))) } - "secret.compare" => { + IntrinsicId::SecretCompare => { require_args!(name, 2, args); - let equal = match (&args[0], &args[1]) { + let equal = match (args[0].payload(), args[1].payload()) { (Value::String(lhs), Value::String(rhs)) => { constant_time_secret_bytes_equal(lhs.as_bytes(), rhs.as_bytes()) } @@ -8418,9 +11161,9 @@ impl Interpreter { } // -- Random operations (stdlib/random.jett) ----------------------- - "random.__bounded" if self.current_function_trusted_stdlib => { + IntrinsicId::RandomBounded if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::Nothing, Value::Int64(lower), Value::Int64(upper)) => { if lower >= upper { return Some(Err( @@ -8441,7 +11184,7 @@ impl Interpreter { } } - "random.__unit_float64" if self.current_function_trusted_stdlib => { + IntrinsicId::RandomUnitFloat64 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); if !matches!(args[0], Value::Nothing) { return Some(Err(format!("{name} expects Random"))); @@ -8452,7 +11195,7 @@ impl Interpreter { } } - "random.__bool" if self.current_function_trusted_stdlib => { + IntrinsicId::RandomBool if self.current_function_trusted_stdlib => { require_args!(name, 1, args); if !matches!(args[0], Value::Nothing) { return Some(Err(format!("{name} expects Random"))); @@ -8464,41 +11207,41 @@ impl Interpreter { } // -- Private string kernels (stdlib/string.jett) ------------------ - "string.__char_count" if self.current_function_trusted_stdlib => { + IntrinsicId::StringCharCount if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::Int64(string_grapheme_count(s) as i64))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__trim" if self.current_function_trusted_stdlib => { + IntrinsicId::StringTrim if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(s.trim().to_string()))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__upper" if self.current_function_trusted_stdlib => { + IntrinsicId::StringUpper if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(s.to_uppercase()))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__lower" if self.current_function_trusted_stdlib => { + IntrinsicId::StringLower if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(s.to_lowercase()))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__replace" if self.current_function_trusted_stdlib => { + IntrinsicId::StringReplace if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::String(s), Value::String(from), Value::String(to)) => { let replaced = string_split_grapheme_matches(s, from).join(to); Some(Ok(Value::String(replaced))) @@ -8507,9 +11250,9 @@ impl Interpreter { } } - "string.__split" if self.current_function_trusted_stdlib => { + IntrinsicId::StringSplit if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::String(s), Value::String(delim)) => { let parts: Vec = string_split_grapheme_matches(s, delim) .into_iter() @@ -8521,9 +11264,9 @@ impl Interpreter { } } - "string.__join" if self.current_function_trusted_stdlib => { + IntrinsicId::StringJoin if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(items), Value::String(sep)) => { let strs: Result, String> = items .iter() @@ -8541,9 +11284,9 @@ impl Interpreter { } } - "string.__slice" if self.current_function_trusted_stdlib => { + IntrinsicId::StringSlice if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::String(s), Value::Int64(start), Value::Int64(end)) => { let graphemes = string_graphemes(s); let len = graphemes.len() as i64; @@ -8558,9 +11301,9 @@ impl Interpreter { } } - "string.__repeat" if self.current_function_trusted_stdlib => { + IntrinsicId::StringRepeat if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::String(s), Value::Int64(n)) => { Some(repeat_string_checked(s, *n).map(Value::String)) } @@ -8569,25 +11312,25 @@ impl Interpreter { } // -- Type conversions (stdlib/string.jett, stdlib/int64.jett) ----- - "string.__from_int64" if self.current_function_trusted_stdlib => { + IntrinsicId::StringFromInt64 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Int64(n) => Some(Ok(Value::String(n.to_string()))), _ => Some(Err(format!("{name} expects an int64 argument"))), } } - "string.__from_uint64" if self.current_function_trusted_stdlib => { + IntrinsicId::StringFromUint64 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Uint64(n) => Some(Ok(Value::String(n.to_string()))), Value::Int64(n) if *n >= 0 => Some(Ok(Value::String(n.to_string()))), _ => Some(Err(format!("{name} expects a uint64 argument"))), } } - "string.__slugify" if self.current_function_trusted_stdlib => { + IntrinsicId::StringSlugify if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let slug: String = s .to_lowercase() @@ -8605,9 +11348,9 @@ impl Interpreter { } // -- int64 / float64 conversions ---------------------------------- - "int64.from_float64" => { + IntrinsicId::Int64FromFloat64 => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) if n.is_finite() && n.fract() == 0.0 @@ -8623,9 +11366,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a float64 argument"))), } } - "int64.from_string" => { + IntrinsicId::Int64FromString => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match s.parse::() { Ok(n) => Some(Ok(Value::ResultOk(Box::new(Value::Int64(n))))), Err(_) => Some(Ok(Value::ResultFail(Box::new(Value::String(format!( @@ -8635,9 +11378,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a string argument"))), } } - "uint64.from_string" => { + IntrinsicId::Uint64FromString => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match s.parse::() { Ok(n) => Some(Ok(Value::ResultOk(Box::new(Value::Uint64(n))))), Err(_) => Some(Ok(Value::ResultFail(Box::new(Value::String(format!( @@ -8649,9 +11392,16 @@ impl Interpreter { } // -- float64 conversions ------------------------------------------ - "float64.from_int64" => { + IntrinsicId::Float32FromFloat64 => { + require_args!(name, 1, args); + match args[0].payload() { + Value::Float64(value) => Some(Ok(Value::Float64((*value as f32) as f64))), + _ => Some(Err(format!("{name} expects a float64 argument"))), + } + } + IntrinsicId::Float64FromInt64 => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Int64(n) => { let converted = *n as f64; if converted as i128 == i128::from(*n) { @@ -8666,9 +11416,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects an int64 argument"))), } } - "float64.from_string" => { + IntrinsicId::Float64FromString => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match s.parse::() { Ok(n) => Some(Ok(Value::ResultOk(Box::new(Value::Float64(n))))), Err(_) => Some(Ok(Value::ResultFail(Box::new(Value::String(format!( @@ -8680,50 +11430,50 @@ impl Interpreter { } // -- Additional string conversions -------------------------------- - "string.__from_float64" if self.current_function_trusted_stdlib => { + IntrinsicId::StringFromFloat64 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::String(format!("{n}")))), _ => Some(Err(format!("{name} expects a float64 argument"))), } } - "string.__from_bool" if self.current_function_trusted_stdlib => { + IntrinsicId::StringFromBool if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bool(b) => Some(Ok(Value::String(format!("{b}")))), _ => Some(Err(format!("{name} expects a bool argument"))), } } // -- print (debugging helper) ------------------------------------- - "print" => { - let output: Vec = args.iter().map(|v| format!("{v}")).collect(); - self.write_stdout(&output.join(" ")); - Some(Ok(Value::Nothing)) - } - "println" => { - let output: Vec = args.iter().map(|v| format!("{v}")).collect(); - self.write_stdout_line(&output.join(" ")); - Some(Ok(Value::Nothing)) - } + IntrinsicId::Print => Some(Ok(self.emit_typed_debug_print( + DebugEventKind::Print, + args, + &[], + ))), + IntrinsicId::Println => Some(Ok(self.emit_typed_debug_print( + DebugEventKind::Println, + args, + &[], + ))), // -- Private list kernels (stdlib/list.jett) ---------------------- - "list.__new" if self.current_function_trusted_stdlib => { + IntrinsicId::ListNew if self.current_function_trusted_stdlib => { require_args!(name, 0, args); Some(Ok(Value::List(vec![]))) } - "list.__length" if self.current_function_trusted_stdlib => { + IntrinsicId::ListLength if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(items) => Some(Ok(Value::Int64(items.len() as i64))), _ => Some(Err(format!("{name} expects a list argument"))), } } - "list.__append" if self.current_function_trusted_stdlib => { + IntrinsicId::ListAppend if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match &args[0] { + match args[0].payload() { Value::List(items) => { let mut new_list = items.clone(); new_list.push(args[1].clone()); @@ -8733,9 +11483,9 @@ impl Interpreter { } } - "list.__get_clone" if self.current_function_trusted_stdlib => { + IntrinsicId::ListGetClone if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(items), Value::Int64(index)) => { let Some(idx) = usize::try_from(*index).ok() else { return Some(Ok(Value::OptionalNone)); @@ -8750,12 +11500,12 @@ impl Interpreter { } } - "list.__sort" if self.current_function_trusted_stdlib => { + IntrinsicId::ListSort if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(items) => { let mut sorted = items.clone(); - sorted.sort_by(|a, b| match (a, b) { + sorted.sort_by(|a, b| match (a.payload(), b.payload()) { (Value::Int64(x), Value::Int64(y)) => x.cmp(y), (Value::Uint64(x), Value::Uint64(y)) => x.cmp(y), (Value::Float64(x), Value::Float64(y)) => { @@ -8772,31 +11522,33 @@ impl Interpreter { } // -- Private map kernels (stdlib/map.jett) ------------------------ - "map.__new" if self.current_function_trusted_stdlib => Some(Ok(Value::Map(Vec::new()))), - "map.__length" if self.current_function_trusted_stdlib => { + IntrinsicId::MapNew if self.current_function_trusted_stdlib => { + Some(Ok(Value::Map(Vec::new()))) + } + IntrinsicId::MapLength if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Map(entries) => Some(Ok(Value::Int64(entries.len() as i64))), _ => Some(Err(format!("{name} expects a map argument"))), } } - "map.__has" if self.current_function_trusted_stdlib => { + IntrinsicId::MapHas if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match &args[0] { + match args[0].payload() { Value::Map(entries) => { - let found = entries.iter().any(|(k, _)| k == &args[1]); + let found = entries.iter().any(|(k, _)| k == args[1].payload()); Some(Ok(Value::Bool(found))) } _ => Some(Err(format!("{name} expects a map as first argument"))), } } - "map.__get" if self.current_function_trusted_stdlib => { + IntrinsicId::MapGet if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match &args[0] { + match args[0].payload() { Value::Map(entries) => { let val = entries .iter() - .find(|(k, _)| k == &args[1]) + .find(|(k, _)| k == args[1].payload()) .map(|(_, v)| v.clone()); Some(Ok(match val { Some(v) => Value::OptionalSome(Box::new(v)), @@ -8806,9 +11558,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a map as first argument"))), } } - "map.__insert" if self.current_function_trusted_stdlib => { + IntrinsicId::MapInsert if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match args[0].clone() { + match args[0].payload().clone() { Value::Map(mut entries) => { let key = args[1].clone(); let val = args[2].clone(); @@ -8822,19 +11574,19 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a map as first argument"))), } } - "map.__remove" if self.current_function_trusted_stdlib => { + IntrinsicId::MapRemove if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match args[0].clone() { + match args[0].payload().clone() { Value::Map(mut entries) => { - entries.retain(|(k, _)| k != &args[1]); + entries.retain(|(k, _)| k != args[1].payload()); Some(Ok(Value::Map(entries))) } _ => Some(Err(format!("{name} expects a map as first argument"))), } } - "map.__from_lists" if self.current_function_trusted_stdlib => { + IntrinsicId::MapFromLists if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(keys), Value::List(values)) => { let mut entries = Vec::new(); for (key, value) in keys.iter().zip(values.iter()) { @@ -8853,10 +11605,12 @@ impl Interpreter { } // -- Private set kernels (stdlib/set.jett) ------------------------ - "set.__new" if self.current_function_trusted_stdlib => Some(Ok(Value::Set(Vec::new()))), - "set.__add" if self.current_function_trusted_stdlib => { + IntrinsicId::SetNew if self.current_function_trusted_stdlib => { + Some(Ok(Value::Set(Vec::new()))) + } + IntrinsicId::SetAdd if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match args[0].clone() { + match args[0].payload().clone() { Value::Set(mut items) => { let val = args[1].clone(); if !items.contains(&val) { @@ -8867,44 +11621,44 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a set as first argument"))), } } - "set.__remove" if self.current_function_trusted_stdlib => { + IntrinsicId::SetRemove if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match args[0].clone() { + match args[0].payload().clone() { Value::Set(mut items) => { - items.retain(|v| v != &args[1]); + items.retain(|v| v != args[1].payload()); Some(Ok(Value::Set(items))) } _ => Some(Err(format!("{name} expects a set as first argument"))), } } - "set.__contains" if self.current_function_trusted_stdlib => { + IntrinsicId::SetContains if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match &args[0] { - Value::Set(items) => Some(Ok(Value::Bool(items.contains(&args[1])))), + match args[0].payload() { + Value::Set(items) => Some(Ok(Value::Bool(items.contains(args[1].payload())))), _ => Some(Err(format!("{name} expects a set as first argument"))), } } - "set.__length" if self.current_function_trusted_stdlib => { + IntrinsicId::SetLength if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Set(items) => Some(Ok(Value::Int64(items.len() as i64))), _ => Some(Err(format!("{name} expects a set argument"))), } } - "list.__sort_by_index" if self.current_function_trusted_stdlib => { + IntrinsicId::ListSortByIndex if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(items), Value::Int64(idx)) => { let idx = usize::try_from(*idx).unwrap_or(usize::MAX); let mut sorted = items.clone(); sorted.sort_by(|a, b| { - let va = match a { - Value::List(l) => l.get(idx).cloned(), + let va = match a.payload() { + Value::List(l) => l.get(idx).map(|value| value.payload().clone()), _ => None, }; - let vb = match b { - Value::List(l) => l.get(idx).cloned(), + let vb = match b.payload() { + Value::List(l) => l.get(idx).map(|value| value.payload().clone()), _ => None, }; match (va, vb) { @@ -8926,18 +11680,21 @@ impl Interpreter { } } - "list.__is_sorted" if self.current_function_trusted_stdlib => { + IntrinsicId::ListIsSorted if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(items) => { - let sorted = items.windows(2).all(|w| match (&w[0], &w[1]) { - (Value::Int64(a), Value::Int64(b)) => a <= b, - (Value::Uint64(a), Value::Uint64(b)) => a <= b, - (Value::Float64(a), Value::Float64(b)) => a <= b, - (Value::String(a), Value::String(b)) => a <= b, - (Value::Bool(a), Value::Bool(b)) => a <= b, - _ => true, - }); + let sorted = + items + .windows(2) + .all(|w| match (w[0].payload(), w[1].payload()) { + (Value::Int64(a), Value::Int64(b)) => a <= b, + (Value::Uint64(a), Value::Uint64(b)) => a <= b, + (Value::Float64(a), Value::Float64(b)) => a <= b, + (Value::String(a), Value::String(b)) => a <= b, + (Value::Bool(a), Value::Bool(b)) => a <= b, + _ => true, + }); Some(Ok(Value::Bool(sorted))) } _ => Some(Err(format!("{name} expects a list argument"))), @@ -8945,18 +11702,18 @@ impl Interpreter { } // -- Private math kernels (stdlib/math.jett) ---------------------- - "math.__abs" if self.current_function_trusted_stdlib => { + IntrinsicId::MathKernelAbs if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Int64(n) => Some(Ok(Value::Int64(n.wrapping_abs()))), Value::Float64(n) => Some(Ok(Value::Float64(n.abs()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__min" if self.current_function_trusted_stdlib => { + IntrinsicId::MathKernelMin if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Int64(a), Value::Int64(b)) => Some(Ok(Value::Int64(*a.min(b)))), (Value::Float64(a), Value::Float64(b)) => Some(Ok(Value::Float64(a.min(*b)))), _ => Some(Err(format!( @@ -8965,9 +11722,9 @@ impl Interpreter { } } - "math.__max" if self.current_function_trusted_stdlib => { + IntrinsicId::MathKernelMax if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Int64(a), Value::Int64(b)) => Some(Ok(Value::Int64(*a.max(b)))), (Value::Float64(a), Value::Float64(b)) => Some(Ok(Value::Float64(a.max(*b)))), _ => Some(Err(format!( @@ -8976,18 +11733,18 @@ impl Interpreter { } } - "math.__sqrt" if self.current_function_trusted_stdlib => { + IntrinsicId::MathSqrt if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.sqrt()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).sqrt()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__pow" if self.current_function_trusted_stdlib => { + IntrinsicId::MathPow if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Float64(base), Value::Float64(exp)) => { Some(Ok(Value::Float64(base.powf(*exp)))) } @@ -9007,36 +11764,36 @@ impl Interpreter { } } - "math.__floor" if self.current_function_trusted_stdlib => { + IntrinsicId::MathFloor if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.floor()))), Value::Int64(n) => Some(Ok(Value::Int64(*n))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__ceil" if self.current_function_trusted_stdlib => { + IntrinsicId::MathCeil if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.ceil()))), Value::Int64(n) => Some(Ok(Value::Int64(*n))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__round" if self.current_function_trusted_stdlib => { + IntrinsicId::MathRound if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.round()))), Value::Int64(n) => Some(Ok(Value::Int64(*n))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__clamp" if self.current_function_trusted_stdlib => { + IntrinsicId::MathClamp if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::Int64(v), Value::Int64(lo), Value::Int64(hi)) => { Some(Ok(Value::Int64((*v).clamp(*lo, *hi)))) } @@ -9057,40 +11814,40 @@ impl Interpreter { } } - "math.__log" if self.current_function_trusted_stdlib => { + IntrinsicId::MathLog if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.ln()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).ln()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__log2" if self.current_function_trusted_stdlib => { + IntrinsicId::MathLog2 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.log2()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).log2()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__log10" if self.current_function_trusted_stdlib => { + IntrinsicId::MathLog10 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.log10()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).log10()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__average" if self.current_function_trusted_stdlib => { + IntrinsicId::MathAverage if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(items) if !items.is_empty() => { let numbers: Result, String> = items .iter() - .map(|value| match value { + .map(|value| match value.payload() { Value::Int64(n) => Ok(*n as f64), Value::Uint64(n) => Ok(*n as f64), Value::Float64(n) => Ok(*n), @@ -9106,13 +11863,13 @@ impl Interpreter { } } - "math.__median" if self.current_function_trusted_stdlib => { + IntrinsicId::MathMedian if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(items) if !items.is_empty() => { let nums: Result, String> = items .iter() - .map(|value| match value { + .map(|value| match value.payload() { Value::Int64(n) => Ok(*n as f64), Value::Uint64(n) => Ok(*n as f64), Value::Float64(n) => Ok(*n), @@ -9140,41 +11897,41 @@ impl Interpreter { } // -- Math constants and extras ----------------------------------------- - "math.__pi" if self.current_function_trusted_stdlib => { + IntrinsicId::MathPi if self.current_function_trusted_stdlib => { require_args!(name, 0, args); Some(Ok(Value::Float64(std::f64::consts::PI))) } - "math.__e" if self.current_function_trusted_stdlib => { + IntrinsicId::MathE if self.current_function_trusted_stdlib => { require_args!(name, 0, args); Some(Ok(Value::Float64(std::f64::consts::E))) } - "math.__sin" if self.current_function_trusted_stdlib => { + IntrinsicId::MathSin if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.sin()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).sin()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__cos" if self.current_function_trusted_stdlib => { + IntrinsicId::MathCos if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.cos()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).cos()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__tan" if self.current_function_trusted_stdlib => { + IntrinsicId::MathTan if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Float64(n) => Some(Ok(Value::Float64(n.tan()))), Value::Int64(n) => Some(Ok(Value::Float64((*n as f64).tan()))), _ => Some(Err(format!("{name} expects a numeric argument"))), } } - "math.__mod" if self.current_function_trusted_stdlib => { + IntrinsicId::MathMod if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Int64(a), Value::Int64(b)) => { if *b == 0 { Some(Err("math.mod: division by zero".to_string())) @@ -9185,9 +11942,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects two int64 arguments"))), } } - "math.__gcd" if self.current_function_trusted_stdlib => { + IntrinsicId::MathGcd if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Int64(a), Value::Int64(b)) => { let (mut x, mut y) = (a.unsigned_abs(), b.unsigned_abs()); while y != 0 { @@ -9200,9 +11957,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects two int64 arguments"))), } } - "math.__lcm" if self.current_function_trusted_stdlib => { + IntrinsicId::MathLcm if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Int64(a), Value::Int64(b)) => { if *a == 0 || *b == 0 { Some(Ok(Value::Int64(0))) @@ -9222,9 +11979,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects two int64 arguments"))), } } - "math.__factorial" if self.current_function_trusted_stdlib => { + IntrinsicId::MathFactorial if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Int64(n) => { if *n < 0 { Some(Err( @@ -9241,9 +11998,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects an int64 argument"))), } } - "list.__insert_at" if self.current_function_trusted_stdlib => { + IntrinsicId::ListInsertAt if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(items), Value::Int64(index)) => { let Some(idx) = usize::try_from(*index).ok() else { return Some(Err(format!("{name}: index {index} out of bounds"))); @@ -9261,9 +12018,9 @@ impl Interpreter { ))), } } - "list.__remove_at" if self.current_function_trusted_stdlib => { + IntrinsicId::ListRemoveAt if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::List(items), Value::Int64(index)) => { let Some(idx) = usize::try_from(*index).ok() else { return Some(Err(format!("{name}: index {index} out of bounds"))); @@ -9279,9 +12036,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a list and an int64 index"))), } } - "list.__swap" if self.current_function_trusted_stdlib => { + IntrinsicId::ListSwap if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::List(items), Value::Int64(i), Value::Int64(j)) => { let (Some(a), Some(b)) = (usize::try_from(*i).ok(), usize::try_from(*j).ok()) @@ -9300,26 +12057,26 @@ impl Interpreter { } } - "string.__trim_start" if self.current_function_trusted_stdlib => { + IntrinsicId::StringTrimStart if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(s.trim_start().to_string()))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__trim_end" if self.current_function_trusted_stdlib => { + IntrinsicId::StringTrimEnd if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(s.trim_end().to_string()))), _ => Some(Err(format!("{name} expects a string argument"))), } } // Private string segmentation kernels yield list[string]. - "string.__chars" if self.current_function_trusted_stdlib => { + IntrinsicId::StringChars if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let chars: Vec = string_graphemes(s) .into_iter() @@ -9331,9 +12088,9 @@ impl Interpreter { } } - "string.__words" if self.current_function_trusted_stdlib => { + IntrinsicId::StringWords if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let words: Vec = s .split_whitespace() @@ -9345,9 +12102,9 @@ impl Interpreter { } } - "string.__lines" if self.current_function_trusted_stdlib => { + IntrinsicId::StringLines if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let mut lines = Vec::new(); let bytes = s.as_bytes(); @@ -9375,43 +12132,18 @@ impl Interpreter { } // -- UUID operations (stdlib/uuid.jett) --------------------------- - "uuid.new" => { + IntrinsicId::UuidNew => { require_args!(name, 0, args); - // Generate a UUID v4 using rand - let mut rng = rand::thread_rng(); - let mut b = [0u8; 16]; - for byte in b.iter_mut() { - *byte = rand::Rng::r#gen(&mut rng); - } - // Set version 4 bits - b[6] = (b[6] & 0x0F) | 0x40; - // Set variant bits - b[8] = (b[8] & 0x3F) | 0x80; - let uuid = format!( - "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", - b[0], - b[1], - b[2], - b[3], - b[4], - b[5], - b[6], - b[7], - b[8], - b[9], - b[10], - b[11], - b[12], - b[13], - b[14], - b[15] - ); - Some(Ok(Value::String(uuid))) + Some( + jett_runtime::uuid::new_v4() + .map(Value::String) + .map_err(str::to_owned), + ) } - "string.__index_of" if self.current_function_trusted_stdlib => { + IntrinsicId::StringIndexOf if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::String(haystack), Value::String(needle)) => { let result = match string_find_grapheme_match(haystack, needle) { Some((_, _, index)) => { @@ -9424,9 +12156,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects two string arguments"))), } } - "string.__count" if self.current_function_trusted_stdlib => { + IntrinsicId::StringCount if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::String(haystack), Value::String(needle)) => { let count = string_count_grapheme_matches(haystack, needle) as i64; Some(Ok(Value::Int64(count))) @@ -9434,9 +12166,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects two string arguments"))), } } - "string.__to_upper_first" if self.current_function_trusted_stdlib => { + IntrinsicId::StringToUpperFirst if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let graphemes = string_graphemes(s); let result = match graphemes.split_first() { @@ -9455,9 +12187,9 @@ impl Interpreter { _ => Some(Err(format!("{name} expects a string argument"))), } } - "string.__to_lower_first" if self.current_function_trusted_stdlib => { + IntrinsicId::StringToLowerFirst if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => { let graphemes = string_graphemes(s); let result = match graphemes.split_first() { @@ -9477,9 +12209,9 @@ impl Interpreter { } } - "string.__is_numeric" if self.current_function_trusted_stdlib => { + IntrinsicId::StringIsNumeric if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::Bool( !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()), ))), @@ -9487,9 +12219,9 @@ impl Interpreter { } } - "string.__is_alpha" if self.current_function_trusted_stdlib => { + IntrinsicId::StringIsAlpha if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::Bool( !s.is_empty() && s.chars().all(|c| c.is_alphabetic()), ))), @@ -9498,9 +12230,9 @@ impl Interpreter { } // -- Encoding operations (stdlib/encoding.jett) ------------------- - "encoding.__base64_encode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingBase64Encode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(bytes) => { let encoded = base64_encode(bytes); Some(Ok(Value::String(encoded))) @@ -9509,9 +12241,9 @@ impl Interpreter { } } - "encoding.__base64_decode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingBase64Decode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match base64_decode(s) { Ok(bytes) => Some(Ok(Value::ResultOk(Box::new(Value::Bytes(bytes))))), Err(error) => Some(Ok(Value::ResultFail(Box::new(Value::String( @@ -9522,9 +12254,9 @@ impl Interpreter { } } - "encoding.__hex_decode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingHexDecode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match encoding_hex_decode(s) { Ok(bytes) => Some(Ok(Value::ResultOk(Box::new(Value::Bytes(bytes))))), Err(error) => Some(Ok(Value::ResultFail(Box::new(Value::String( @@ -9535,17 +12267,17 @@ impl Interpreter { } } - "encoding.__url_encode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingUrlEncode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(percent_encode(s, false)))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "encoding.__url_decode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingUrlDecode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(percent_decode(s, false) .map(|decoded| Value::ResultOk(Box::new(Value::String(decoded)))) .unwrap_or_else(|error| { @@ -9555,17 +12287,17 @@ impl Interpreter { } } - "encoding.__form_encode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingFormEncode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::String(percent_encode(s, true)))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "encoding.__form_decode" if self.current_function_trusted_stdlib => { + IntrinsicId::EncodingFormDecode if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(percent_decode(s, true) .map(|decoded| Value::ResultOk(Box::new(Value::String(decoded)))) .unwrap_or_else(|error| { @@ -9576,33 +12308,33 @@ impl Interpreter { } // -- Crypto operations (stdlib/crypto.jett) ----------------------- - "crypto.__sha256" if self.current_function_trusted_stdlib => { + IntrinsicId::CryptoSha256 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(bytes) => Some(Ok(Value::Bytes(sha256_digest(bytes)))), _ => Some(Err(format!("{name} expects a bytes argument"))), } } - "crypto.__sha512" if self.current_function_trusted_stdlib => { + IntrinsicId::CryptoSha512 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(bytes) => Some(Ok(Value::Bytes(sha512_digest(bytes)))), _ => Some(Err(format!("{name} expects a bytes argument"))), } } - "crypto.__md5" if self.current_function_trusted_stdlib => { + IntrinsicId::CryptoMd5 if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(bytes) => Some(Ok(Value::Bytes(md5_digest(bytes)))), _ => Some(Err(format!("{name} expects a bytes argument"))), } } - "crypto.__hmac_sha256" if self.current_function_trusted_stdlib => { + IntrinsicId::CryptoHmacSha256 if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Bytes(key), Value::Bytes(message)) => { Some(Ok(Value::Bytes(hmac_sha256_digest(key, message)))) } @@ -9611,7 +12343,7 @@ impl Interpreter { } // -- Wall-clock operation (stdlib/time.jett) ---------------------- - "Clock.__now" if self.current_function_trusted_stdlib => { + IntrinsicId::ClockNow if self.current_function_trusted_stdlib => { require_args!(name, 1, args); if !matches!(args[0], Value::Nothing) { return Some(Err(format!("{name} expects Clock"))); @@ -9623,13 +12355,13 @@ impl Interpreter { } // -- Launch environment operations (stdlib/environment.jett) ------- - "Environment.__get" if self.current_function_trusted_stdlib => { + IntrinsicId::EnvironmentGet if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - if !matches!(&args[0], Value::Capability(capability) if capability == "Environment") + if !matches!(args[0].payload(), Value::Capability(capability) if capability == "Environment") { return Some(Err(format!("{name} expects Environment"))); } - let Value::String(key) = &args[1] else { + let Value::String(key) = args[1].payload() else { return Some(Err(format!("{name} expects a string key"))); }; let Some(environment) = &self.launch_environment else { @@ -9640,12 +12372,12 @@ impl Interpreter { Value::String(value), )))), Ok(None) => Value::ResultOk(Box::new(Value::OptionalNone)), - Err(error) => Value::ResultFail(Box::new(Value::String(error))), + Err(error) => Value::ResultFail(Box::new(Value::String(error.to_owned()))), })) } - "Environment.__args" if self.current_function_trusted_stdlib => { + IntrinsicId::EnvironmentArgs if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - if !matches!(&args[0], Value::Capability(capability) if capability == "Environment") + if !matches!(args[0].payload(), Value::Capability(capability) if capability == "Environment") { return Some(Err(format!("{name} expects Environment"))); } @@ -9654,7 +12386,7 @@ impl Interpreter { }; Some(Ok(Value::List( environment - .arguments + .arguments() .iter() .cloned() .map(Value::String) @@ -9663,9 +12395,9 @@ impl Interpreter { } // -- CSV operations (stdlib/csv.jett) -------------------------- - "csv.__parse" if self.current_function_trusted_stdlib => { + IntrinsicId::CsvParse if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => match parse_csv_records(s) { Ok(records) => { let rows: Vec = records @@ -9682,9 +12414,9 @@ impl Interpreter { } } - "csv.__stringify" if self.current_function_trusted_stdlib => { + IntrinsicId::CsvStringify if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::List(rows) => { let mut lines = Vec::new(); for row in rows { @@ -9710,54 +12442,24 @@ impl Interpreter { } } - "csv.__parse_with_header" if self.current_function_trusted_stdlib => { + IntrinsicId::CsvParseWithHeader if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { - Value::String(s) => match parse_csv_records(s) { + match args[0].payload() { + Value::String(s) => match parse_csv_with_header(s) { Ok(records) => { - let mut records = records.into_iter(); - let headers: Vec = match records.next() { - Some(header_row) => header_row, - None => { - return Some(Ok(result_ok(Value::List(Vec::new())))); - } - }; - - let mut seen_headers = HashSet::new(); - for (index, header) in headers.iter().enumerate() { - if header.is_empty() { - return Some(Ok(result_fail(format!( - "CSV header error at field {}: header must not be empty", - index + 1 - )))); - } - if !seen_headers.insert(header.as_str()) { - return Some(Ok(result_fail(format!( - "CSV header error at field {}: duplicate header '{}'", - index + 1, - header - )))); - } - } - - let mut rows = Vec::new(); - for (index, cols) in records.enumerate() { - if cols.len() != headers.len() { - return Some(Ok(result_fail(format!( - "CSV header error at record {}: expected {} fields, got {}", - index + 2, - headers.len(), - cols.len() - )))); - } - - let entries: Vec<(Value, Value)> = headers - .iter() - .zip(cols.into_iter()) - .map(|(h, c)| (Value::String(h.clone()), Value::String(c))) - .collect(); - rows.push(Value::Map(entries)); - } + let rows = records + .into_iter() + .map(|entries| { + Value::Map( + entries + .into_iter() + .map(|(key, value)| { + (Value::String(key), Value::String(value)) + }) + .collect(), + ) + }) + .collect(); Some(Ok(result_ok(Value::List(rows)))) } Err(message) => Some(Ok(result_fail(message))), @@ -9767,24 +12469,24 @@ impl Interpreter { } // -- Range generation --------------------------------------------------- - "range" => { + IntrinsicId::Range => { match args.len() { // range(end) — 0 to end exclusive - 1 => match &args[0] { + 1 => match args[0].payload() { Value::Int64(end) => { Some(range_values_checked(0, *end, 1).map(Value::List)) } _ => Some(Err(format!("{name} expects int64 arguments"))), }, // range(start, end) — start to end exclusive - 2 => match (&args[0], &args[1]) { + 2 => match (args[0].payload(), args[1].payload()) { (Value::Int64(start), Value::Int64(end)) => { Some(range_values_checked(*start, *end, 1).map(Value::List)) } _ => Some(Err(format!("{name} expects int64 arguments"))), }, // range(start, end, step) - 3 => match (&args[0], &args[1], &args[2]) { + 3 => match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::Int64(start), Value::Int64(end), Value::Int64(step)) => { Some(range_values_checked(*start, *end, *step).map(Value::List)) } @@ -9795,22 +12497,22 @@ impl Interpreter { } // -- Bytes operations (stdlib/bytes.jett) --------------------------- - "bytes.__new" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesNew if self.current_function_trusted_stdlib => { require_args!(name, 0, args); Some(Ok(Value::Bytes(Vec::new()))) } - "bytes.__length" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesLength if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(b) => Some(Ok(Value::Int64(b.len() as i64))), _ => Some(Err(format!("{name} expects a bytes argument"))), } } - "bytes.__slice" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesSlice if self.current_function_trusted_stdlib => { require_args!(name, 3, args); - match (&args[0], &args[1], &args[2]) { + match (args[0].payload(), args[1].payload(), args[2].payload()) { (Value::Bytes(b), Value::Int64(start), Value::Int64(end)) => { let len = b.len() as i64; let start = (*start).clamp(0, len) as usize; @@ -9824,9 +12526,9 @@ impl Interpreter { } } - "bytes.__concat" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesConcat if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Bytes(a), Value::Bytes(b)) => { let mut result = a.clone(); result.extend(b.iter()); @@ -9836,17 +12538,17 @@ impl Interpreter { } } - "bytes.__from_string" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesFromString if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(s) => Some(Ok(Value::Bytes(s.as_bytes().to_vec()))), _ => Some(Err(format!("{name} expects a string argument"))), } } - "bytes.__to_string" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesToString if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(b) => match String::from_utf8(b.clone()) { Ok(s) => Some(Ok(Value::ResultOk(Box::new(Value::String(s))))), Err(e) => Some(Ok(Value::ResultFail(Box::new(Value::String(format!( @@ -9857,9 +12559,9 @@ impl Interpreter { } } - "bytes.__to_hex" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesToHex if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::Bytes(b) => { let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect(); Some(Ok(Value::String(hex))) @@ -9868,9 +12570,9 @@ impl Interpreter { } } - "bytes.__from_hex" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesFromHex if self.current_function_trusted_stdlib => { require_args!(name, 1, args); - match &args[0] { + match args[0].payload() { Value::String(raw) => match Self::parse_hex_bytes( raw, "bytes.from_hex: expected even-length hex string", @@ -9883,9 +12585,9 @@ impl Interpreter { } } - "bytes.__get" if self.current_function_trusted_stdlib => { + IntrinsicId::BytesGet if self.current_function_trusted_stdlib => { require_args!(name, 2, args); - match (&args[0], &args[1]) { + match (args[0].payload(), args[1].payload()) { (Value::Bytes(b), Value::Int64(index)) => { let Some(idx) = usize::try_from(*index).ok() else { return Some(Ok(Value::OptionalNone)); @@ -9904,8 +12606,155 @@ impl Interpreter { } } - // Not a built-in - _ => None, + // Private stdlib kernels are intentionally unavailable to direct + // project calls. Keep this list explicit so a new intrinsic must + // choose a comptime dispatch path. + IntrinsicId::BytesConcat + | IntrinsicId::BytesFromHex + | IntrinsicId::BytesFromString + | IntrinsicId::BytesGet + | IntrinsicId::BytesLength + | IntrinsicId::BytesNew + | IntrinsicId::BytesSlice + | IntrinsicId::BytesToHex + | IntrinsicId::BytesToString + | IntrinsicId::ClockNow + | IntrinsicId::CryptoHmacSha256 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CsvParse + | IntrinsicId::CsvParseWithHeader + | IntrinsicId::CsvStringify + | IntrinsicId::EncodingBase64Decode + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::EncodingFormDecode + | IntrinsicId::EncodingFormEncode + | IntrinsicId::EncodingHexDecode + | IntrinsicId::EncodingUrlDecode + | IntrinsicId::EncodingUrlEncode + | IntrinsicId::EnvironmentArgs + | IntrinsicId::EnvironmentGet + | IntrinsicId::ListAppend + | IntrinsicId::ListGetClone + | IntrinsicId::ListInsertAt + | IntrinsicId::ListIsSorted + | IntrinsicId::ListLength + | IntrinsicId::ListNew + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListSort + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListSwap + | IntrinsicId::MapFromLists + | IntrinsicId::MapGet + | IntrinsicId::MapHas + | IntrinsicId::MapInsert + | IntrinsicId::MapLength + | IntrinsicId::MapNew + | IntrinsicId::MapRemove + | IntrinsicId::MathAverage + | IntrinsicId::MathCeil + | IntrinsicId::MathClamp + | IntrinsicId::MathCos + | IntrinsicId::MathE + | IntrinsicId::MathFactorial + | IntrinsicId::MathFloor + | IntrinsicId::MathGcd + | IntrinsicId::MathKernelAbs + | IntrinsicId::MathKernelMax + | IntrinsicId::MathKernelMin + | IntrinsicId::MathLcm + | IntrinsicId::MathLog + | IntrinsicId::MathLog10 + | IntrinsicId::MathLog2 + | IntrinsicId::MathMedian + | IntrinsicId::MathMod + | IntrinsicId::MathPi + | IntrinsicId::MathPow + | IntrinsicId::MathRound + | IntrinsicId::MathSin + | IntrinsicId::MathSqrt + | IntrinsicId::MathTan + | IntrinsicId::RandomBool + | IntrinsicId::RandomBounded + | IntrinsicId::RandomUnitFloat64 + | IntrinsicId::SetAdd + | IntrinsicId::SetContains + | IntrinsicId::SetLength + | IntrinsicId::SetNew + | IntrinsicId::SetRemove + | IntrinsicId::StringCharCount + | IntrinsicId::StringChars + | IntrinsicId::StringCount + | IntrinsicId::StringFromBool + | IntrinsicId::StringFromFloat64 + | IntrinsicId::StringFromInt64 + | IntrinsicId::StringFromUint64 + | IntrinsicId::StringIndexOf + | IntrinsicId::StringIsAlpha + | IntrinsicId::StringIsNumeric + | IntrinsicId::StringJoin + | IntrinsicId::StringLines + | IntrinsicId::StringLower + | IntrinsicId::StringRepeat + | IntrinsicId::StringReplace + | IntrinsicId::StringSlice + | IntrinsicId::StringSlugify + | IntrinsicId::StringSplit + | IntrinsicId::StringToLowerFirst + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringTrimStart + | IntrinsicId::StringUpper + | IntrinsicId::StringWords => None, + + // Routed before this ordinary dispatcher. + IntrinsicId::BitfieldFromBytes | IntrinsicId::BitfieldToBytes => None, + IntrinsicId::GraphicsRun + | IntrinsicId::ListGroupBy + | IntrinsicId::ListSortBy + | IntrinsicId::ListSum => None, + IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + | IntrinsicId::TypeArg + | IntrinsicId::TypeBitfieldFields + | IntrinsicId::TypeBitfieldLayout + | IntrinsicId::TypeConstructFinish + | IntrinsicId::TypeConstructMachineStart + | IntrinsicId::TypeConstructPut + | IntrinsicId::TypeConstructStart + | IntrinsicId::TypeConstructVariantStart + | IntrinsicId::TypeFieldValue + | IntrinsicId::TypeFields + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeMachineLayout + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeMachineStates + | IntrinsicId::TypeMachineTransitions + | IntrinsicId::TypeName + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeVariants => None, + + // These are implemented by source wrappers or require runtime-only + // capabilities, so the ordinary comptime dispatcher declines them. + IntrinsicId::FilesystemReadFile + | IntrinsicId::FilesystemWriteFile + | IntrinsicId::LogEmit + | IntrinsicId::MathAbs + | IntrinsicId::MathMax + | IntrinsicId::MathMin + | IntrinsicId::TestMockClock + | IntrinsicId::TestMockEnvironment + | IntrinsicId::TestMockRandom => None, } } @@ -9915,6 +12764,12 @@ impl Interpreter { /// Built-in standard library functions are checked first; if the name /// does not match a built-in, user-defined functions are consulted. pub fn call_function(&mut self, name: &str, args: Vec) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + self.call_function_with_type_args(name, &[], args) } @@ -9926,6 +12781,12 @@ impl Interpreter { name: &str, args: Vec, ) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + let saved_namespace = self.current_namespace.clone(); self.current_namespace = namespace.map(str::to_string); let runtime_name = self @@ -9936,6 +12797,40 @@ impl Interpreter { result } + /// Enter a checked source function after the embedding compiler has + /// established every input invariant, including refined aggregate children. + /// Main capabilities and zero-argument verify entries satisfy this contract; + /// arbitrary host values and generated/shrunk candidates do not by default. + /// Entry parameters still undergo ordinary normalization and validation: no + /// source proof is synthesized from their destination annotations. The + /// checked body may reuse its own source facts, unless nested under a raw call. + pub fn call_checked_function_in_namespace( + &mut self, + namespace: Option<&str>, + name: &str, + args: Vec, + ) -> Result { + if args.iter().any(Value::contains_live_resource_or_grant) { + return Err( + "raw call API cannot adopt Resource custody or runtime authority".to_string(), + ); + } + + let saved_namespace = self.current_namespace.clone(); + self.current_namespace = namespace.map(str::to_string); + let runtime_name = self + .registry_name(&self.functions, name) + .unwrap_or_else(|| name.to_string()); + let source_types = vec![None; args.len()]; + let previous_field_entry = std::mem::replace(&mut self.field_update_entry_allowed, true); + let previous_temporary_entry = self.set_temporary_entry_allowed(true); + let result = self.call_function_from_source(&runtime_name, &[], args, Some(&source_types)); + self.set_temporary_entry_allowed(previous_temporary_entry); + self.field_update_entry_allowed = previous_field_entry; + self.current_namespace = saved_namespace; + result + } + /// Execute a block while resolving unqualified names as if execution is /// currently inside `namespace`. pub fn exec_block_in_namespace( @@ -9956,23 +12851,68 @@ impl Interpreter { type_args: &[TypeExpr], args: Vec, ) -> Result { - if self.is_trusted_stdlib_first_function(name) { - return self.call_user_function_with_type_args(name, type_args, args); - } + self.call_function_from_source(name, type_args, args, None) + } - // Check higher-order built-ins first (require &mut self). - if let Some(result) = self.call_higher_order_builtin(name, &args) { - return result; - } - if let Some(result) = self.call_builtin_with_type_args(name, type_args, &args) { - return result; - } - // Check built-in functions first. + fn call_function_from_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + let resolved_type_args: Vec<_> = type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect(); + self.call_function_from_resolved_source(name, &resolved_type_args, args, source_types) + } + + fn call_function_from_resolved_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + // Raw higher-order and actor builtins can execute source bodies before + // reaching a registered function. Keep their complete dispatch raw too. + let saved_proofs = self.allow_checked_refinement_proofs; + self.allow_checked_refinement_proofs &= source_types.is_some(); + let source_types = source_types.filter(|_| self.allow_checked_refinement_proofs); + let result = self.dispatch_function_from_source(name, type_args, args, source_types); + self.allow_checked_refinement_proofs = saved_proofs; + result + } + + fn dispatch_function_from_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + if self.is_trusted_stdlib_first_function(name) { + return self.call_user_function_from_source(name, type_args, args, source_types); + } + + // Check higher-order built-ins first (require &mut self). + if let Some(result) = + self.call_higher_order_builtin_with_type_args(name, type_args, &args, false) + { + return result; + } + if let Some(result) = + self.call_builtin_with_type_args_from_source(name, type_args, &args, false) + { + return result; + } + // Check built-in functions first. if let Some(result) = self.call_builtin(name, &args) { return result; } - self.call_user_function_with_type_args(name, type_args, args) + self.call_user_function_from_source(name, type_args, args, source_types) } fn is_trusted_stdlib_first_function(&self, name: &str) -> bool { @@ -9981,7 +12921,9 @@ impl Interpreter { } fn has_trusted_stdlib_function(&self, name: &str) -> bool { - self.trusted_stdlib_functions.contains(name) && self.functions.contains_key(name) + self.functions + .get(name) + .is_some_and(|function| function.trusted_stdlib) } fn call_user_function_with_type_args( @@ -9989,24 +12931,119 @@ impl Interpreter { name: &str, type_args: &[TypeExpr], args: Vec, + ) -> Result { + // Compiler-owned JSON facade forwarding inherits its caller's trust. + // Its destination inputs remain forced; a raw dispatch already disabled + // proofs and cannot regain them through this internal bridge. + let source_types = vec![None; args.len()]; + self.call_user_function_from_source(name, type_args, args, Some(&source_types)) + } + + fn call_user_function_from_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, ) -> Result { // Check if the name refers to a variable holding a function value (closure). if let Some(fn_val) = self.get_variable(name).cloned() { - if matches!(fn_val, Value::Function { .. } | Value::NamedFunction(_)) { - return self.call_fn_value(fn_val, args); + if matches!( + fn_val.payload(), + Value::Function { .. } | Value::NamedFunction(_) | Value::ReflectedCallable(_) + ) { + return self.call_fn_value_from_source(fn_val, args, source_types); } } - let resolved_name = self - .resolve_interface_dispatch(name, &args) - .unwrap_or_else(|| name.to_string()); + self.call_registered_function_from_resolved_source(name, type_args, args, source_types) + } + + fn call_registered_function_from_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + let resolved_type_args: Vec<_> = type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect(); + self.call_registered_function_from_resolved_source( + name, + &resolved_type_args, + args, + source_types, + ) + } + + fn call_registered_function_from_resolved_source( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + self.call_registered_function_with_temporary_frame( + name, + type_args, + args, + source_types, + None, + ) + } + + fn call_registered_function_with_temporary_frame( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + source_types: Option<&[Option]>, + temporary_frame: Option, + ) -> Result { + let resolved_name = match ( + self.resolve_interface_dispatch(name, &args), + self.interface_dispatch_name(name), + ) { + (Some(resolved), _) => resolved, + (None, Some(interface)) => return Err(format!("undefined function '{interface}'")), + (None, None) => name.to_string(), + }; // Look up the function definition. - let func = self + let registered = self .functions .get(&resolved_name) - .ok_or_else(|| format!("undefined function '{name}'"))? + .ok_or_else(|| { + let name = self + .registry_name(&self.interface_methods, name) + .unwrap_or_else(|| name.to_string()); + format!("undefined function '{name}'") + })? .clone(); + let original_capture_origin = + if crate::value::body_may_mint_inline_function(®istered.definition.body) { + self.checked_expression_types + .as_ref() + .and_then(|types| { + types.resource_program.as_ref().filter(|program| { + crate::value::closure_capture::original_root_types(program, types) + }) + }) + .and_then(|program| { + crate::value::closure_capture::OriginalClosureOwner::registered( + program, + &resolved_name, + ®istered.definition, + ®istered.namespace, + ) + }) + } else { + None + }; + let registered_definition = registered.definition.clone(); + let func = registered_definition.as_ref(); if args.len() != func.params.len() { return Err(format!( @@ -10018,50 +13055,180 @@ impl Interpreter { } let type_scope = self.type_scope_for_function(&func, type_args)?; - self.type_arg_scopes.push(type_scope); + if let Some(interface) = self.interface_dispatch_name(name) { + self.validate_interface_argument_owners( + &interface, + &func, + registered.namespace.as_deref(), + &type_scope, + &args, + )?; + } + let arguments: Vec = func + .type_params + .iter() + .map(|param| type_scope[¶m.name].clone()) + .collect(); + let captured_arguments = self.captured_type_arguments(&arguments); + let expression_types = match &self.checked_expression_types { + Some(types) => match temporary_frame.as_ref() { + Some(frame) => frame + .select_registered_projection( + types, + &resolved_name, + ®istered.namespace, + func, + &captured_arguments, + &args, + ) + .map_err(|error| format!("{resolved_name}: {error}"))?, + None => types + .select(func.name.span, &captured_arguments, &args) + .map_err(|error| format!("{resolved_name}: {error}"))?, + }, + None => None, + }; + let field_body = self.prepare_checked_field_function( + &resolved_name, + ®istered.namespace, + func, + expression_types.as_deref(), + &captured_arguments, + source_types.is_some(), + )?; + let temporary_function = self.prepare_temporary_function( + &resolved_name, + ®istered.namespace, + func, + expression_types.as_deref(), + &captured_arguments, + source_types.is_some(), + temporary_frame.as_ref(), + )?; + let func = match (&temporary_function, &field_body) { + (Some(receipt), _) => receipt.original()?, + (None, Some(reference)) => reference.function().map_err(|error| error.to_string())?, + (None, None) => func, + }; + let saved_field_cursor = self.enter_checked_field_body(field_body.as_ref())?; + let temporary_activation = match self.enter_temporary_function(temporary_function.clone()) { + Ok(activation) => activation, + Err(error) => { + let _field_restore = self.restore_checked_field_body(saved_field_cursor); + return Err(error); + } + }; + let saved_expression_types = + std::mem::replace(&mut self.active_checked_function, expression_types); + let saved_type_arguments = std::mem::replace(&mut self.current_type_arguments, arguments); + let saved_scoped_types = std::mem::take(&mut self.scoped_type_bindings); + let saved_checked_scope = self.active_checked_scope.take(); + let saved_capture_origin = std::mem::replace( + &mut self.active_reflected_capture_origin, + original_capture_origin, + ); + // Resolve arguments above in the caller, then keep its type bindings + // out of the callee's lexical scope, including non-generic callees. + let saved_type_scopes = std::mem::replace(&mut self.type_arg_scopes, vec![type_scope]); let saved_namespace = self.current_namespace.clone(); let saved_trusted_stdlib = self.current_function_trusted_stdlib; - self.current_namespace = Self::function_namespace(&resolved_name); - self.current_function_trusted_stdlib = - self.trusted_stdlib_functions.contains(&resolved_name); + self.current_namespace = registered.namespace; + self.current_function_trusted_stdlib = registered.trusted_stdlib; let scope_depth = self.scopes.len(); + let saved_scope_floor = self.lexical_scope_floor; + self.lexical_scope_floor = scope_depth; self.push_scope(); + let ordinary_return_context = self + .resource_transport + .as_mut() + .is_some_and(|transport| transport.enter_ordinary_return()); + let saved_proofs = self.allow_checked_refinement_proofs; + self.allow_checked_refinement_proofs &= source_types.is_some(); + let source_types = source_types.filter(|_| self.allow_checked_refinement_proofs); let call_result = (|| { - for (param, arg) in func.params.iter().zip(args) { - let param_ty = self.substitute_type_expr(¶m.ty); - let type_name = type_expr_name(¶m_ty); - let arg = self.normalize_value_for_type(¶m_ty, arg)?; - self.check_refinement(&type_name, &arg)?; - self.set_variable_with_type(¶m.name.name, arg, param_ty); + if let Some(frame) = temporary_frame.as_ref() { + let staged = + self.stage_temporary_parameters(func, args, source_types, Some(frame))?; + for (index, (param, (arg, param_ty))) in func.params.iter().zip(staged).enumerate() + { + self.set_variable_with_type(¶m.name.name, arg, param_ty); + self.install_temporary_parameter(index, param, Some(frame))?; + self.install_checked_field_binding(¶m.name)?; + } + } else { + for (index, (param, arg)) in func.params.iter().zip(args).enumerate() { + let param_ty = self.substitute_type_expr(¶m.ty); + let source_type = source_types + .and_then(|types| types.get(index)) + .and_then(Option::as_deref); + let arg = + self.normalize_and_validate_value_from_source(¶m_ty, arg, source_type)?; + self.set_variable_with_type(¶m.name.name, arg, param_ty); + self.install_checked_field_binding(¶m.name)?; + } } - let result = self.exec_block_inner(&func.body)?; - let mut value = match result { - Some(Signal::Return(v)) => v, + let result = match self.exec_block_with_temporary_output(&func.body)? { + temporary_local_views::BlockOutput::Acquired(output) => { + return output.into_registered_value( + self, + temporary_activation, + temporary_function.as_ref(), + ); + } + temporary_local_views::BlockOutput::Ordinary(result) => result, + }; + let (mut value, return_source_type) = match result { + Some(Signal::Return(value, source_type)) => (value, source_type), + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err("raw call API cannot return Resource custody".to_string()); + } Some(Signal::Default(_)) => { return Err("`default` can only be used inside a `handle` block".to_string()); } - _ => Value::Nothing, + _ => (Value::Nothing, None), }; if let Some(return_type) = &func.return_type { let return_type = self.substitute_type_expr(return_type); - let type_name = type_expr_name(&return_type); - value = self.normalize_value_for_type(&return_type, value)?; - self.check_refinement(&type_name, &value)?; + // Public/raw API invocations remain forced boundaries even + // when a checked body happens to be installed on the runtime. + let source_type = source_types.and(return_source_type.as_deref()); + value = self.normalize_and_validate_value_from_source( + &return_type, + value, + source_type, + )?; } Ok(value) })(); + if ordinary_return_context { + if let Some(transport) = self.resource_transport.as_mut() { + transport.leave_return(); + } + } + self.allow_checked_refinement_proofs = saved_proofs; while self.scopes.len() > scope_depth { self.pop_scope(); } - self.type_arg_scopes.pop(); + self.lexical_scope_floor = saved_scope_floor; + self.active_checked_scope = saved_checked_scope; + self.scoped_type_bindings = saved_scoped_types; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_type_arguments; + self.active_checked_function = saved_expression_types; self.current_namespace = saved_namespace; self.current_function_trusted_stdlib = saved_trusted_stdlib; - call_result + self.active_reflected_capture_origin = saved_capture_origin; + let temporary_restore = self.leave_temporary_function(temporary_activation); + let field_restore = self.restore_checked_field_body(saved_field_cursor); + match (call_result, temporary_restore, field_restore) { + (Err(error), _, _) | (_, Err(error), _) | (_, _, Err(error)) => Err(error), + (Ok(value), Ok(()), Ok(())) => Ok(value), + } } fn type_scope_for_function( @@ -10081,39 +13248,181 @@ impl Interpreter { )); } - let raw: HashMap = func + Ok(func .type_params .iter() .zip(type_args.iter()) - .map(|(param, arg)| (param.name.clone(), self.substitute_type_expr(arg))) - .collect(); - - Ok(raw - .iter() - .map(|(name, ty)| (name.clone(), self.substitute_type_expr_with_map(ty, &raw))) + .map(|(param, arg)| (param.name.clone(), arg.clone())) .collect()) } + fn sum_primitive_list( + &self, + name: &str, + type_name: &str, + items: &[Value], + ) -> Result { + let invalid_element = |index| { + if index == 0 { + format!("{name}: list elements must be int64 or float64") + } else { + format!("{name}: mixed types") + } + }; + if matches!(type_name, "float32" | "float64") { + let mut total = 0.0_f64; + for (index, item) in items.iter().enumerate() { + let Value::Float64(number) = item.payload() else { + return Err(invalid_element(index)); + }; + total = if type_name == "float32" { + f64::from(total as f32 + *number as f32) + } else { + total + number + }; + } + return Ok(Value::Float64(total)); + } + let mut total = 0_u64; + // Reduction modulo the selected width commutes with every addition. + // Keep the raw accumulator and apply that width before returning. + for (index, item) in items.iter().enumerate() { + let bits = match (type_name, item.payload()) { + ("uint64", Value::Uint64(number)) => *number, + (_, Value::Int64(number)) if type_name != "uint64" => *number as u64, + _ => return Err(invalid_element(index)), + }; + total = total.wrapping_add(bits); + } + self.wrap_integer_value_for_type_name(type_name, Value::Uint64(total)) + } + + fn sum_fallback_list(name: &str, items: &[Value]) -> Result { + if items.is_empty() { + return Ok(Value::Int64(0)); + } + // Keep the existing fallback's int64/float64 arithmetic and errors. + match items[0].payload() { + Value::Int64(_) => { + let mut total = 0i64; + for item in items { + match item.payload() { + Value::Int64(n) => total = total.wrapping_add(*n), + _ => return Err(format!("{name}: mixed types")), + } + } + Ok(Value::Int64(total)) + } + Value::Float64(_) => { + let mut total = 0.0f64; + for item in items { + match item.payload() { + Value::Float64(n) => total += n, + _ => return Err(format!("{name}: mixed types")), + } + } + Ok(Value::Float64(total)) + } + _ => Err(format!("{name}: list elements must be int64 or float64")), + } + } + + fn sum_resolved_list( + &mut self, + name: &str, + element_type: Option, + args: &[Value], + checked_producer: bool, + ) -> Result { + if args.len() != 1 { + return Err(format!("{name} expects 1 argument, got {}", args.len())); + } + let Value::List(items) = args[0].payload() else { + return Err(format!("{name}: argument must be a list")); + }; + let element = element_type + .as_ref() + .map(|ty| self.resolved_concrete_type_display(ty)); + if let Some(type_name) = element + && matches!( + type_name.as_str(), + "int8" + | "int16" + | "int32" + | "int64" + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "float32" + | "float64" + ) + { + return self.sum_primitive_list(name, &type_name, items); + } + let value = Self::sum_fallback_list(name, items)?; + match element_type { + // Fresh fallback output needs the selected invariant before any + // checked local, argument or return can reuse its result type. + Some(ty) if self.allow_checked_refinement_proofs && checked_producer => { + let canonical = self.concrete_resolved_type_expr(&ty, &mut HashSet::new()); + let mut unqualified = &canonical; + let mut qualified = false; + while let TypeExpr::Generic(owner, arguments, _) = unqualified { + if owner.name != "secret" || arguments.len() != 1 { + break; + } + qualified = true; + unqualified = &arguments[0]; + } + if qualified + && matches!(unqualified, TypeExpr::Named(name) if self.type_name_has_refinement(&name.name)) + { + return Err(format!( + "{name}: cannot establish new invariants beneath secret" + )); + } + self.normalize_and_validate_value(&ty, value) + } + // Raw/no-fact facades retain their one existing return check. + _ => Ok(value), + } + } + /// Try to call a higher-order built-in that requires `&mut self` (because - /// it needs to invoke a user-supplied function value). Returns `None` if + /// it needs to invoke a user-supplied function value). Returns `None` if /// the name is not a higher-order built-in. + #[cfg(test)] fn call_higher_order_builtin( &mut self, name: &str, args: &[Value], ) -> Option> { - match name { - "graphics.__run" if self.current_function_trusted_stdlib => { + self.call_higher_order_builtin_with_type_args(name, &[], args, false) + } + + /// Explicit arguments are resolved; absent arguments retain the legacy + /// metadata-free enclosing-generic fallback. + fn call_higher_order_builtin_with_type_args( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: &[Value], + checked_producer: bool, + ) -> Option> { + let intrinsic = IntrinsicId::from_callable_name(name)?; + match intrinsic { + IntrinsicId::GraphicsRun if self.current_function_trusted_stdlib => { Some(self.run_graphics_builtin(args.to_vec())) } - "list.__sort_by" if self.current_function_trusted_stdlib => { + IntrinsicId::ListSortBy if self.current_function_trusted_stdlib => { if args.len() != 2 { return Some(Err(format!( "list.__sort_by expects 2 arguments, got {}", args.len() ))); } - let items = match &args[0] { + let items = match args[0].payload() { Value::List(v) => v.clone(), _ => return Some(Err("list.__sort_by: first argument must be a list".into())), }; @@ -10121,7 +13430,10 @@ impl Interpreter { // Compute keys for each item. let mut keyed: Vec<(i64, Value)> = Vec::new(); for item in items { - match self.call_fn_value(fn_val.clone(), vec![item.clone()]) { + match self + .call_fn_value(fn_val.clone(), vec![item.clone()]) + .map(Value::into_payload) + { Ok(Value::Int64(k)) => keyed.push((k, item)), Ok(other) => { return Some(Err(format!( @@ -10134,65 +13446,37 @@ impl Interpreter { keyed.sort_by_key(|(k, _)| *k); Some(Ok(Value::List(keyed.into_iter().map(|(_, v)| v).collect()))) } - "list.__sum" if self.current_function_trusted_stdlib => { - if args.len() != 1 { - return Some(Err(format!( - "{name} expects 1 argument, got {}", - args.len() - ))); - } - match &args[0] { - Value::List(items) => { - if items.is_empty() { - return Some(Ok(Value::Int64(0))); - } - // Detect int64 vs float64 from first element. - match &items[0] { - Value::Int64(_) => { - let mut total = 0i64; - for item in items { - match item { - Value::Int64(n) => { - total = total.wrapping_add(*n); - } - _ => return Some(Err(format!("{name}: mixed types"))), - } - } - Some(Ok(Value::Int64(total))) - } - Value::Float64(_) => { - let mut total = 0.0f64; - for item in items { - match item { - Value::Float64(n) => total += n, - _ => return Some(Err(format!("{name}: mixed types"))), - } - } - Some(Ok(Value::Float64(total))) - } - _ => Some(Err(format!( - "{name}: list elements must be int64 or float64" - ))), - } - } - _ => Some(Err(format!("{name}: argument must be a list"))), - } - } - "list.__group_by" if self.current_function_trusted_stdlib => { + IntrinsicId::ListSum if self.current_function_trusted_stdlib => { + let selected = type_args.first().cloned().or_else(|| { + self.current_type_arguments + .first() + .map(|ty| self.resolve_installed_type_expr(ty)) + }); + let blank_aliases = vec![HashMap::new(); self.namespace_alias_scopes.len()]; + let saved_aliases = + std::mem::replace(&mut self.namespace_alias_scopes, blank_aliases); + let result = self.sum_resolved_list(name, selected, args, checked_producer); + self.namespace_alias_scopes = saved_aliases; + Some(result) + } + IntrinsicId::ListGroupBy if self.current_function_trusted_stdlib => { if args.len() != 2 { return Some(Err(format!( "{name} expects 2 arguments, got {}", args.len() ))); } - let items = match &args[0] { + let items = match args[0].payload() { Value::List(v) => v.clone(), _ => return Some(Err(format!("{name}: first argument must be a list"))), }; let fn_val = args[1].clone(); let mut groups: Vec<(Value, Value)> = Vec::new(); for item in items { - let key = match self.call_fn_value(fn_val.clone(), vec![item.clone()]) { + let key = match self + .call_fn_value(fn_val.clone(), vec![item.clone()]) + .map(Value::into_payload) + { Ok(k) => k, Err(e) => return Some(Err(e)), }; @@ -10207,20 +13491,73 @@ impl Interpreter { Some(Ok(Value::Map(groups))) } - _ => None, + // The four guarded variants fall through here for untrusted + // callers; every other intrinsic is intentionally declined. + IntrinsicId::GraphicsRun + | IntrinsicId::ListGroupBy + | IntrinsicId::ListSortBy + | IntrinsicId::ListSum => None, + non_higher_order_intrinsics!() => None, } } /// Call a `Value::Function` (inline function) with the given arguments. fn call_fn_value(&mut self, fn_val: Value, args: Vec) -> Result { - match fn_val { - Value::NamedFunction(name) => self.call_function(&name, args), - Value::Function { - params, - body, - captures, - namespace, - } => { + self.call_fn_value_from_source(fn_val, args, None) + } + + fn call_fn_value_from_source( + &mut self, + fn_val: Value, + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + match fn_val.into_payload() { + Value::ReflectedCallable(adapter) => { + self.call_reflected_callable(*adapter, args, source_types) + } + Value::NamedFunction(name) => { + self.call_registered_function_from_source(&name, &[], args, source_types) + } + source @ Value::Function { .. } => { + let field_body = self.prepare_checked_field_inline(&source)?; + let original_capture_origin = match &source { + Value::Function { body, .. } + if crate::value::body_may_mint_inline_function(body) => + { + self.checked_expression_types + .as_ref() + .and_then(|types| { + types.resource_program.as_ref().filter(|program| { + crate::value::closure_capture::original_root_types( + program, types, + ) + }) + }) + .and_then(|program| { + crate::value::closure_capture::invocation_origin(&source, program) + }) + } + _ => None, + }; + let Value::Function { + type_context, + params, + body, + captures, + capture_types, + namespace_aliases, + namespace, + } = source + else { + unreachable!("inline call matched its function value above"); + }; + let (execution_params, execution_body) = match &field_body { + Some(reference) => reference + .field_inline_parts() + .map_err(|error| error.to_string())?, + None => (params.as_slice(), &body), + }; if args.len() != params.len() { return Err(format!( "inline function expects {} argument(s), got {}", @@ -10228,70 +13565,285 @@ impl Interpreter { args.len() )); } - let mut normalized_args = Vec::with_capacity(args.len()); - for (param, arg) in params.iter().zip(args) { - let param_ty = self.substitute_type_expr(¶m.ty); - normalized_args.push(self.normalize_value_for_type(¶m_ty, arg)?); - } - - // Push the captured environment as a scope, then the parameter scope on top. + let saved_capture_origin = std::mem::replace( + &mut self.active_reflected_capture_origin, + original_capture_origin, + ); + let saved_field_cursor = self.enter_checked_field_body(field_body.as_ref())?; + let saved_expression_types = std::mem::replace( + &mut self.active_checked_function, + type_context.checked_function, + ); + let saved_checked_scope = + std::mem::replace(&mut self.active_checked_scope, type_context.checked_scope); + let saved_scoped_types = std::mem::replace( + &mut self.scoped_type_bindings, + type_context.scoped_type_bindings, + ); + let saved_type_scopes = + std::mem::replace(&mut self.type_arg_scopes, vec![type_context.bindings]); + let saved_type_arguments = std::mem::replace( + &mut self.current_type_arguments, + type_context + .arguments + .into_iter() + .map(|arg| arg.ty) + .collect(), + ); + let saved_namespace = std::mem::replace(&mut self.current_namespace, namespace); + let saved_trusted_stdlib = self.current_function_trusted_stdlib; + self.current_function_trusted_stdlib = false; let scope_depth = self.scopes.len(); + let saved_scope_floor = self.lexical_scope_floor; + self.lexical_scope_floor = scope_depth; self.push_scope(); for (name, value) in &captures { - self.set_variable(name, value.clone()); + if let Some(ty) = capture_types.get(name) { + self.set_variable_with_type(name, value.clone(), ty.clone()); + } else { + self.set_variable(name, value.clone()); + } + } + for (name, target) in namespace_aliases { + self.set_namespace_alias(name, target); } self.push_scope(); - for (param, arg) in params.iter().zip(normalized_args) { - let param_ty = self.substitute_type_expr(¶m.ty); - self.set_variable_with_type(¶m.name.name, arg, param_ty); + let ordinary_return_context = self + .resource_transport + .as_mut() + .is_some_and(|transport| transport.enter_ordinary_return()); + let saved_proofs = self.allow_checked_refinement_proofs; + self.allow_checked_refinement_proofs &= source_types.is_some(); + let source_types = source_types.filter(|_| self.allow_checked_refinement_proofs); + let result = (|| { + for (index, (param, arg)) in execution_params.iter().zip(args).enumerate() { + let param_ty = self.substitute_type_expr(¶m.ty); + let source_type = source_types + .and_then(|types| types.get(index)) + .and_then(Option::as_deref); + let arg = self.normalize_and_validate_value_from_source( + ¶m_ty, + arg, + source_type, + )?; + self.set_variable_with_type(¶m.name.name, arg, param_ty); + self.install_checked_field_binding(¶m.name)?; + } + let (mut value, return_source_type) = + match self.exec_block_inner(execution_body)? { + Some(Signal::Return(value, source_type)) => (value, source_type), + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err( + "captured Resource return has no checked custody transport proof" + .to_string(), + ); + } + Some(Signal::Default(_)) => { + return Err("`default` can only be used inside a `handle` block" + .to_string()); + } + _ => (Value::Nothing, None), + }; + if let Some(return_type) = &type_context.return_type { + let return_type = self.substitute_type_expr(return_type); + let source_type = source_types.and(return_source_type.as_deref()); + value = self.normalize_and_validate_value_from_source( + &return_type, + value, + source_type, + )?; + } + Ok(value) + })(); + if ordinary_return_context { + if let Some(transport) = self.resource_transport.as_mut() { + transport.leave_return(); + } } - let saved_trusted_stdlib = self.current_function_trusted_stdlib; - let saved_namespace = self.current_namespace.clone(); - self.current_function_trusted_stdlib = false; - self.current_namespace = namespace; - let result = self.exec_block_inner(&body); - self.current_function_trusted_stdlib = saved_trusted_stdlib; - self.current_namespace = saved_namespace; + self.allow_checked_refinement_proofs = saved_proofs; while self.scopes.len() > scope_depth { self.pop_scope(); } - let result = result?; - Ok(match result { - Some(Signal::Return(v)) => v, - _ => Value::Nothing, - }) + self.lexical_scope_floor = saved_scope_floor; + self.current_function_trusted_stdlib = saved_trusted_stdlib; + self.current_namespace = saved_namespace; + self.active_checked_scope = saved_checked_scope; + self.scoped_type_bindings = saved_scoped_types; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_type_arguments; + self.active_checked_function = saved_expression_types; + self.active_reflected_capture_origin = saved_capture_origin; + let field_restore = self.restore_checked_field_body(saved_field_cursor); + match (result, field_restore) { + (Err(error), _) | (_, Err(error)) => Err(error), + (Ok(value), Ok(())) => Ok(value), + } } other => Err(format!("expected function value, got {other}")), } } fn resolve_interface_dispatch(&self, name: &str, args: &[Value]) -> Option { - if self.functions.contains_key(name) { - return Some(name.to_string()); - } + if let Some(interface) = self.interface_dispatch_name(name) { + let receiver = args.first()?; + if matches!(receiver.payload(), Value::Pending(_)) { + return None; + } + let receiver_type = runtime_type_name(receiver)?; + return self.interface_methods[&interface] + .get(&receiver_type) + .cloned(); + } + self.functions.contains_key(name).then(|| name.to_string()) + } - let receiver_type = runtime_type_name(args.first()?)?; + fn interface_dispatch_name(&self, name: &str) -> Option { self.interface_methods - .get(name) - .and_then(|methods| methods.get(&receiver_type)) + .contains_key(name) + .then(|| name.to_string()) + .or_else(|| { + self.current_qualified_name(name) + .filter(|qualified| self.interface_methods.contains_key(qualified)) + }) + } + + fn display_interpolation_value(&mut self, expr: &Expr, value: Value) -> Result { + let receiver = self + .checked_expression_type(expr.span()) .cloned() + .or_else(|| { + self.call_argument_type(expr) + .map(|ty| self.concrete_type_display(&ty)) + }) + .or_else(|| runtime_type_name(&value)); + let display_method = receiver.and_then(|receiver| { + self.interface_methods + .get("Displayable.display") + .and_then(|methods| methods.get(&receiver)) + .cloned() + }); + let Some(display_method) = display_method else { + return Ok(value.to_string()); + }; + match self.call_function(&display_method, vec![value])? { + Value::String(text) => Ok(text), + other => Err(format!( + "Displayable.display returned {other} instead of string" + )), + } + } + + fn struct_field_type( + &self, + ty: &TypeExpr, + arguments: &HashMap, + namespace: Option<&str>, + ) -> TypeExpr { + match ty { + TypeExpr::Named(name) => arguments + .get(&name.name) + // Actual arguments were fully resolved in the caller. They + // are values, not templates to substitute through this map. + .cloned() + .unwrap_or_else(|| TypeExpr::Named(self.expand_type_ident(name, namespace))), + TypeExpr::Generic(name, children, span) => TypeExpr::Generic( + self.expand_type_ident(name, namespace), + children + .iter() + .map(|child| self.struct_field_type(child, arguments, namespace)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.struct_field_type(inner, arguments, namespace)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.struct_field_type(inner, arguments, namespace)), + state.clone(), + *span, + ), + TypeExpr::Function(params, result, span) => TypeExpr::Function( + params + .iter() + .map(|param| self.struct_field_type(param, arguments, namespace)) + .collect(), + Box::new(self.struct_field_type(result, arguments, namespace)), + *span, + ), + } } fn construct_struct( &mut self, struct_name: &str, + type_args: &[TypeExpr], args: &[CallArg], arg_values: Vec, + source_types: &[Option], ) -> Result { let strukt = self .structs .get(struct_name) .ok_or_else(|| format!("undefined struct '{struct_name}'"))? .clone(); - let validates_refinements = strukt + // Stored field spellings belong to the declaration. Caller imports + // must not rewrite even a canonical owner during normalization, + // predicate lookup, or retained field identity. Keep the alias stack's + // shape so predicates can still establish their own lexical imports. + let declaration_aliases = vec![HashMap::new(); self.namespace_alias_scopes.len()]; + let caller_aliases = + std::mem::replace(&mut self.namespace_alias_scopes, declaration_aliases); + let caller_type_scopes = std::mem::take(&mut self.type_arg_scopes); + let caller_namespace = std::mem::replace( + &mut self.current_namespace, + Self::type_name_namespace(struct_name).map(str::to_owned), + ); + let result = self.construct_struct_in_declaration( + &strukt, + type_args, + args, + arg_values, + source_types, + ); + self.current_namespace = caller_namespace; + self.type_arg_scopes = caller_type_scopes; + self.namespace_alias_scopes = caller_aliases; + result + } + + fn construct_struct_in_declaration( + &mut self, + strukt: &StructDef, + type_args: &[TypeExpr], + args: &[CallArg], + arg_values: Vec, + source_types: &[Option], + ) -> Result { + let struct_name = &strukt.name.name; + let substitutions: HashMap<_, _> = strukt + .type_params + .iter() + .zip(type_args) + .map(|(parameter, argument)| (parameter.name.clone(), argument.clone())) + .collect(); + // Arguments above are already resolved in the caller. Field spelling + // now belongs solely to the struct's declaration and its own map, not + // an unrelated caller parameter with the same name. + let field_types: Vec<_> = strukt .fields .iter() - .any(|field| self.type_name_has_refinement(&type_expr_name(&field.ty))); + .map(|field| { + self.struct_field_type( + &field.ty, + &substitutions, + Self::type_name_namespace(struct_name), + ) + }) + .collect(); + let validates_refinements = field_types + .iter() + .any(|ty| self.type_name_has_refinement(&type_expr_name(ty))); if args.len() > strukt.fields.len() { return Err(format!( @@ -10303,7 +13855,7 @@ impl Interpreter { } let mut fields: Vec> = vec![None; strukt.fields.len()]; - for (arg, value) in args.iter().zip(arg_values) { + for (source_index, (arg, value)) in args.iter().zip(arg_values).enumerate() { let field_index = if let Some(name) = &arg.name { strukt .fields @@ -10329,16 +13881,19 @@ impl Interpreter { )); } - let field_ty = self.substitute_type_expr(&strukt.fields[field_index].ty); - let type_name = type_expr_name(&field_ty); - let value = self.normalize_value_for_type(&field_ty, value)?; - if let Err(message) = self.check_refinement(&type_name, &value) { + let type_name = type_expr_name(&field_types[field_index]); + let value = self.normalize_value_for_type_name(&type_name, value)?; + if let Err(message) = self.check_refinement_from_source( + &type_name, + &value, + source_types.get(source_index).and_then(Option::as_deref), + ) { if validates_refinements { return Ok(Value::ResultFail(Box::new(Value::String(message)))); } return Err(message); } - fields[field_index] = Some(value); + fields[field_index] = Some(self.retain_checked_identity(&type_name, value)); } for (index, field) in strukt.fields.iter().enumerate() { @@ -10357,7 +13912,15 @@ impl Interpreter { .map(|(field, value)| (field.name.name.clone(), value.unwrap())) .collect(); + let concrete_type = (!type_args.is_empty()).then(|| { + let args = type_args + .iter() + .map(|ty| self.concrete_type_display(ty)) + .collect::>(); + format!("{struct_name}[{}]", args.join(", ")) + }); let value = Value::Struct { + concrete_type, type_name: struct_name.to_string(), fields, }; @@ -10458,6 +14021,7 @@ impl Interpreter { } let value = Value::Struct { + concrete_type: None, type_name: bitfield_name.to_string(), fields: bitfield .fields @@ -10474,9 +14038,27 @@ impl Interpreter { } } - fn eval_value_field_access(&self, value: Value, field_name: &str) -> Result { - match value { - Value::Struct { type_name, fields } => fields + fn eval_value_field_access( + &self, + value: Value, + field_name: &str, + receiver_type: Option<&TypeExpr>, + ) -> Result { + if !matches!( + value.payload(), + Value::Struct { .. } | Value::Machine { .. } + ) { + return Err(format!( + "field access is not supported on {}", + self.format_debug_value(&value, receiver_type) + )); + } + match value.into_payload() { + Value::Struct { + concrete_type: _, + type_name, + fields, + } => fields .into_iter() .find(|(name, _)| name == field_name) .map(|(_, value)| ExprFlow::Value(value)) @@ -10508,7 +14090,7 @@ impl Interpreter { ) }) } - other => Err(format!("field access is not supported on {other}")), + _ => unreachable!("field receiver shape checked before consuming its wrappers"), } } @@ -10573,7 +14155,7 @@ impl Interpreter { // arg 0 is evaluated — it should be the current machine value let current_value = self.eval_expr(&args[0].value)?; - let current_state = match ¤t_value { + let current_state = match current_value.payload() { Value::Machine { type_name, state, .. } => { @@ -10650,7 +14232,7 @@ impl Default for Interpreter { // --------------------------------------------------------------------------- fn is_truthy(val: &Value) -> Result { - match val { + match val.payload() { Value::Bool(b) => Ok(*b), _ => Err(format!("expected boolean, got {val}")), } @@ -10785,36 +14367,6 @@ fn string_graphemes(s: &str) -> Vec<&str> { UnicodeSegmentation::graphemes(s, true).collect() } -fn float_midpoint(left: f64, right: f64) -> f64 { - if left == right { - return left; - } - if !left.is_finite() || !right.is_finite() { - return (left + right) / 2.0; - } - if left.is_sign_negative() == right.is_sign_negative() { - left + (right - left) / 2.0 - } else { - left / 2.0 + right / 2.0 - } -} - -fn float_average(values: &[f64]) -> f64 { - let sum = values.iter().sum::(); - // Rescaling can underflow small residuals after large values cancel. - // Keep ordinary summation whenever it did not overflow. - if sum.is_finite() { - return sum / values.len() as f64; - } - let scale = values.iter().map(|value| value.abs()).fold(0.0, f64::max); - if scale == 0.0 || !scale.is_finite() { - return sum / values.len() as f64; - } - - let scaled_sum = values.iter().map(|value| value / scale).sum::(); - scaled_sum / values.len() as f64 * scale -} - fn uint64_arithmetic_operand(value: i64) -> Result { if value >= 0 { Ok(value as u64) @@ -10894,6 +14446,7 @@ fn compare_i64_uint64(left: i64, op: BinOp, right: u64) -> Result } fn eval_binary_op(left: &Value, op: BinOp, right: &Value) -> Result { + let (left, right) = (left.payload(), right.payload()); match (left, op, right) { // -- Integer arithmetic ----------------------------------------------- (Value::Int64(a), BinOp::Add, Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))), @@ -11275,6 +14828,7 @@ fn type_expr_display(ty: &TypeExpr) -> String { fn runtime_type_name(value: &Value) -> Option { match value { + Value::Typed { type_name, .. } => Some(type_name.clone()), Value::Int64(_) => Some("int64".to_string()), Value::Uint64(_) => Some("uint64".to_string()), Value::Float64(_) => Some("float64".to_string()), @@ -11286,16 +14840,24 @@ fn runtime_type_name(value: &Value) -> Option { Value::OptionalSome(_) | Value::OptionalNone => Some("optional".to_string()), Value::Nothing => Some("nothing".to_string()), Value::Capability(name) => Some(name.clone()), + Value::Resource(_) => None, + Value::ResourceHook(_) => Some("function".to_string()), + Value::GrantedNetwork(_) => Some("Network".to_string()), Value::TypeConstruction { .. } => Some("TypeConstruction".to_string()), - Value::Struct { type_name, .. } - | Value::Enum { type_name, .. } - | Value::Machine { type_name, .. } => Some(type_name.clone()), + Value::Struct { + type_name, + concrete_type, + .. + } => Some(concrete_type.as_ref().unwrap_or(type_name).clone()), + Value::Enum { type_name, .. } | Value::Machine { type_name, .. } => Some(type_name.clone()), Value::Error(_) => None, Value::Actor(_) => Some("actor".to_string()), Value::Pending(_) => Some("pending".to_string()), Value::Map(_) => Some("map".to_string()), Value::Set(_) => Some("set".to_string()), - Value::Function { .. } | Value::NamedFunction(_) => Some("function".to_string()), + Value::Function { .. } | Value::NamedFunction(_) | Value::ReflectedCallable(_) => { + Some("function".to_string()) + } } } @@ -11422,1822 +14984,7032 @@ mod tests { use super::*; #[test] - fn grapheme_split_handles_large_near_matches() { - let haystack = "a".repeat(1024 * 1024); - let delimiter = format!("{}b", "a".repeat(512 * 1024)); + fn reflected_comparison_normalizes_repeated_aliases_without_erasing_boundaries() { + let mut interpreter = reuse_interpreter(); + interpreter.register_type_alias(&type_alias("IntegerAlias", "int64", None)); + for (actual, requested) in [ + ("map[int64, int64]", "map[IntegerAlias, IntegerAlias]"), + ("map[int64, int64]", "map[models.Positive, models.Positive]"), + ( + "result[int64, int64]", + "result[models.Positive, models.Positive]", + ), + ( + "map[list[int64], list[int64]]", + "map[list[models.Positive], list[models.Positive]]", + ), + ("Box[int64, int64]", "Box[models.Positive, models.Positive]"), + ( + "secret[map[int64, int64]]", + "secret[map[models.Positive, models.Positive]]", + ), + ] { + let requested_ty = Interpreter::simple_type_expr_from_name(requested, sp()).unwrap(); + assert!(interpreter.reflection_type_names_match(actual, requested)); + assert!(interpreter.reflection_type_name_matches_expr(actual, &requested_ty)); + } + for (actual, requested) in [ + ("map[int64, int64]", "map[models.Positive, bool]"), + ( + "Box[int64, int64]", + "OtherBox[models.Positive, models.Positive]", + ), + ( + "map[int64, int64]", + "secret[map[models.Positive, models.Positive]]", + ), + ( + "secret[map[int64, int64]]", + "map[models.Positive, models.Positive]", + ), + ] { + let requested_ty = Interpreter::simple_type_expr_from_name(requested, sp()).unwrap(); + assert!(!interpreter.reflection_type_names_match(actual, requested)); + assert!(!interpreter.reflection_type_name_matches_expr(actual, &requested_ty)); + } + let repeated = + Interpreter::simple_type_expr_from_name("map[models.Positive, models.Positive]", sp()) + .unwrap(); assert_eq!( - string_split_grapheme_matches(&haystack, &delimiter), - vec![haystack.as_str()] + interpreter + .reflection_compare_type_expr(&TypeExpr::View(Box::new(repeated.clone()), sp(),)), + "view map[int64, int64]" + ); + assert_eq!( + interpreter.reflection_compare_type_expr(&TypeExpr::StateQualified( + Box::new(repeated), + ident("active"), + sp(), + )), + "map[int64, int64] at active" + ); + assert_eq!( + interpreter.reflection_compare_type_expr(&TypeExpr::Function( + vec![type_named("models.Positive"), type_named("models.Positive")], + Box::new(type_named("models.Positive")), + sp(), + )), + "function(int64, int64) returns int64" ); + assert!(reuse_markers(&mut interpreter).is_empty()); } - const JSON_RAW_FACADE_NAMES: &[&str] = &[ - "json.parse_raw", - "json.serialize_raw", - "json.kind", - "json.is_null", - "json.is_bool", - "json.is_number", - "json.is_string", - "json.is_array", - "json.is_object", - "json.field", - "json.index", - "json.array_length", - "json.object_keys", - "json.as_string", - "json.as_int64", - "json.as_uint64", - "json.as_float64", - "json.as_bool", - "json.object_field", - "json.array_index", - "json.require_field", - "json.require_index", - ]; + #[test] + fn reflected_comparison_retains_declaration_context_and_cycle_stopping() { + let mut interpreter = reuse_interpreter(); + interpreter + .register_type_alias_in_namespace(Some("models"), &type_alias("Local", "int64", None)); + interpreter + .register_type_alias_in_namespace(Some("shadow"), &type_alias("Local", "bool", None)); + let mut pair = type_alias("Pair", "int64", None); + pair.base_type = + Interpreter::simple_type_expr_from_name("map[Local, Local]", sp()).unwrap(); + interpreter.register_type_alias_in_namespace(Some("models"), &pair); + interpreter.current_namespace = Some("shadow".into()); + assert_eq!( + interpreter.reflection_compare_type_name("models.Pair"), + "map[int64, int64]" + ); + assert!( + !interpreter + .reflection_type_names_match("map[int64, int64]", "map[models.Pair, models.Pair]") + ); + assert!(interpreter.reflection_type_names_match( + "map[map[int64, int64], map[int64, int64]]", + "map[models.Pair, models.Pair]" + )); - /// Helper: create a dummy span for test AST nodes. - fn sp() -> Span { - Span::new(FileId::new(0), 0, 0) + // Malformed metadata-free alias graphs remain bounded and do not become integers. + for (name, base) in [ + ("SelfCycle", "SelfCycle"), + ("CycleA", "CycleB"), + ("CycleB", "CycleA"), + ("GenericCycle", "list[GenericCycle]"), + ] { + let mut alias = type_alias(name, "int64", None); + alias.base_type = Interpreter::simple_type_expr_from_name(base, sp()).unwrap(); + interpreter.register_type_alias(&alias); + } + for (name, expected) in [ + ("SelfCycle", "SelfCycle"), + ("CycleA", "CycleA"), + ("CycleB", "CycleB"), + ("GenericCycle", "list[GenericCycle]"), + ] { + assert_eq!(interpreter.reflection_compare_type_name(name), expected); + assert!(!interpreter.reflection_type_names_match("int64", name)); + } + assert_eq!( + interpreter.reflection_compare_type_name("map[CycleA, models.Positive]"), + "map[CycleA, int64]" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); } - fn stdlib_sp() -> Span { - Span::new(FileId::new(STDLIB_FILE_ID_START), 0, 0) + #[test] + fn repeated_reflected_leaf_requests_validate_map_slots_and_occupied_result_arms() { + for shape in ["record", "enum", "machine"] { + for (actual, requested, payload, expected_markers) in [ + ( + "map[int64, int64]", + "map[models.Positive, models.Positive]", + Value::Map(vec![ + (Value::Int64(1), Value::Int64(7)), + (Value::Int64(2), Value::Int64(9)), + ]), + vec!["trace positive"; 4], + ), + ( + "result[int64, int64]", + "result[models.Positive, models.Positive]", + Value::ResultOk(Box::new(Value::Int64(7))), + vec!["trace positive"], + ), + ( + "result[int64, int64]", + "result[models.Positive, models.Positive]", + Value::ResultFail(Box::new(Value::Int64(9))), + vec!["trace positive"], + ), + ] { + let (mut interpreter, expression) = + nested_reflected_case(shape, actual, requested, payload.clone()); + let source = interpreter.get_variable("input").unwrap().clone(); + assert_eq!( + interpreter.eval_expr(&expression).unwrap().payload(), + &payload, + "{shape}/{requested}" + ); + assert_eq!(reuse_markers(&mut interpreter), expected_markers); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + assert!(interpreter.allow_checked_refinement_proofs); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "map[int64, int64]", + "map[models.Positive, models.Positive]", + Value::Map(vec![ + (Value::Int64(1), Value::Int64(-2)), + (Value::Int64(3), Value::Int64(4)), + ]), + ); + let source = interpreter.get_variable("input").unwrap().clone(); + assert_eq!( + interpreter.eval_expr(&expression), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"; 2]); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + + for depth in [0, 1, 2] { + let mut pending = Value::Map(vec![(Value::Int64(1), Value::Int64(7))]); + for _ in 0..depth { + pending = Value::Pending(Box::new(pending)); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "map[models.Positive, models.Positive]", + "map[models.Positive, models.Positive]", + pending.clone(), + ); + assert_eq!(interpreter.eval_expr(&expression), Ok(pending.clone())); + assert!(reuse_markers(&mut interpreter).is_empty()); + if depth != 0 { + let (mut interpreter, expression) = nested_reflected_case( + shape, + "map[int64, int64]", + "map[models.Positive, models.Positive]", + pending, + ); + let source = interpreter.get_variable("input").unwrap().clone(); + assert_eq!( + interpreter.eval_expr(&expression), + Err("reflected field refinement: expected a ready map value".into()) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + } + } + } } - /// Helper: create an Ident. - fn ident(name: &str) -> Ident { - Ident { - name: name.to_string(), - span: sp(), + #[test] + fn repeated_reflected_requests_keep_selector_precedence_and_protected_refusals() { + for shape in ["record", "enum", "machine"] { + for (name, replacement, error_fragment) in [ + ( + "owner_type", + Value::String("Other".into()), + "field metadata belongs to", + ), + ( + "owner_member", + Value::OptionalSome(Box::new(Value::String("other".into()))), + "field metadata belongs to", + ), + ("index", Value::Int64(9), "at index 9"), + ("name", Value::String("other".into()), "does not match"), + ("type_name", Value::String("bool".into()), "has type 'bool'"), + ] { + let (mut interpreter, expression) = nested_reflected_case( + shape, + "map[int64, int64]", + "map[models.Positive, models.Positive]", + Value::Map(vec![(Value::Int64(-1), Value::Int64(-2))]), + ); + let source = interpreter.get_variable("input").unwrap().clone(); + let mut selector = interpreter.get_variable("selector").unwrap().clone(); + let Value::Struct { fields, .. } = &mut selector else { + unreachable!() + }; + fields + .iter_mut() + .find(|(field, _)| field == name) + .unwrap() + .1 = replacement; + interpreter.set_variable("selector", selector.clone()); + let error = interpreter.eval_expr(&expression).unwrap_err(); + assert!(error.contains(error_fragment), "{shape}/{name}: {error}"); + assert!(reuse_markers(&mut interpreter).is_empty()); + interpreter.checked_expression_types = None; + assert_eq!(interpreter.eval_expr(&expression), Err(error)); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + assert_eq!(interpreter.get_variable("selector"), Some(&selector)); + } + for (actual, requested, expected) in [ + ( + "secret[map[int64, int64]]", + "secret[map[models.Positive, models.Positive]]", + "reflected field refinement: cannot establish new invariants beneath secret", + ), + ( + "Box[int64, int64]", + "Box[models.Positive, models.Positive]", + "reflected field refinement: unsupported callable or nominal refinement conversion", + ), + ] { + let (mut interpreter, expression) = nested_reflected_case( + shape, + actual, + requested, + Value::Map(vec![(Value::Int64(-913), Value::Int64(-914))]), + ); + let source = interpreter.get_variable("input").unwrap().clone(); + let error = interpreter.eval_expr(&expression).unwrap_err(); + assert_eq!(error, expected, "{shape}/{requested}"); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + } } } - /// Helper: create an int literal expression. - fn int(n: i64) -> Expr { - Expr::IntLiteral(n.into(), sp()) + fn nested_reflected_case( + shape: &str, + actual: &str, + requested: &str, + payload: Value, + ) -> (Interpreter, Expr) { + let (interpreter, mut expression) = + reflected_proof_case(shape, actual, requested, payload, true); + let Expr::GenericCall(_, types, _, _) = &mut expression else { + unreachable!() + }; + types[1] = Interpreter::simple_type_expr_from_name(requested, sp()).unwrap(); + (interpreter, expression) } - /// Helper: create a float literal expression. - fn float(n: f64) -> Expr { - Expr::FloatLiteral(n, sp()) + #[test] + fn nested_reflected_producers_validate_ready_wrappers_in_payload_order() { + for shape in ["record", "enum", "machine"] { + for (actual, requested, payload, markers) in [ + ( + "list[int64]", + "list[models.Positive]", + Value::List(vec![Value::Int64(1), Value::Int64(2)]), + vec!["trace positive", "trace positive"], + ), + ( + "set[int64]", + "set[models.Positive]", + Value::Set(vec![Value::Int64(1), Value::Int64(2)]), + vec!["trace positive", "trace positive"], + ), + ( + "map[int64, int64]", + "map[int64, models.Large]", + Value::Map(vec![ + (Value::Int64(1), Value::Int64(7)), + (Value::Int64(2), Value::Int64(9)), + ]), + vec![ + "trace positive", + "trace large", + "trace positive", + "trace large", + ], + ), + ( + "optional[int64]", + "optional[models.Positive]", + Value::OptionalNone, + vec![], + ), + ( + "optional[int64]", + "optional[models.Positive]", + Value::OptionalSome(Box::new(Value::Int64(2))), + vec!["trace positive"], + ), + ( + "result[int64, int64]", + "result[models.Positive, int64]", + Value::ResultOk(Box::new(Value::Int64(2))), + vec!["trace positive"], + ), + ( + "result[int64, int64]", + "result[int64, models.Large]", + Value::ResultFail(Box::new(Value::Int64(7))), + vec!["trace positive", "trace large"], + ), + ( + "list[models.Positive]", + "list[models.Large]", + Value::List(vec![Value::Int64(7), Value::Int64(9)]), + vec!["trace large", "trace large"], + ), + ( + "list[optional[result[int64, int64]]]", + "list[optional[result[int64, models.Large]]]", + Value::List(vec![ + Value::OptionalNone, + Value::OptionalSome(Box::new(Value::ResultFail(Box::new(Value::Int64(7))))), + Value::OptionalSome(Box::new(Value::ResultOk(Box::new(Value::Int64(2))))), + ]), + vec!["trace positive", "trace large"], + ), + ] { + let (mut interpreter, expression) = + nested_reflected_case(shape, actual, requested, payload.clone()); + let original = format!("{:?}", interpreter.get_variable("input").unwrap()); + assert_eq!( + interpreter.eval_expr(&expression).unwrap().payload(), + &payload, + "{shape}/{requested}" + ); + assert_eq!( + reuse_markers(&mut interpreter), + markers, + "{shape}/{requested}" + ); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + original + ); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[int64]", + "list[models.Positive]", + Value::List(vec![Value::Int64(1), Value::Int64(-2), Value::Int64(3)]), + ); + assert_eq!( + interpreter.eval_expr(&expression), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive", "trace positive"] + ); + } } #[test] - fn explicit_comptime_expression_uses_baked_value() { - let span = sp(); - let expression = Expr::Comptime( - Box::new(Expr::Binary( - Box::new(int(1)), - BinOp::Div, - Box::new(int(0)), - span, - )), - span, - ); - let mut values = HashMap::new(); - values.insert(span, Value::Int64(42)); - - let mut interpreter = Interpreter::new(); - interpreter.set_explicit_comptime_values(Arc::new(values)); - - assert_eq!(interpreter.eval_expr(&expression), Ok(Value::Int64(42))); + fn nested_reflected_preflight_preserves_pending_and_shape_failure_precedence() { + for shape in ["record", "enum", "machine"] { + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[optional[int64]]", + "list[optional[models.Positive]]", + Value::List(vec![ + Value::OptionalSome(Box::new(Value::Int64(7))), + Value::Bool(false), + ]), + ); + assert_eq!( + interpreter.eval_expr(&expression), + Err("reflected field refinement: expected a ready optional value".into()) + ); + assert!( + reuse_markers(&mut interpreter).is_empty(), + "preflight precedes first predicate" + ); + for depth in [1, 2] { + let mut pending = Value::List(vec![Value::Int64(7)]); + for _ in 0..depth { + pending = Value::Pending(Box::new(pending)); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[int64]", + "list[models.Positive]", + pending.clone(), + ); + let joined = Expr::Join(Box::new(expression), Span::new(FileId::new(0), 941, 942)); + assert_eq!( + interpreter.eval_expr(&joined), + Err("reflected field refinement: expected a ready list value".into()) + ); + assert!( + reuse_markers(&mut interpreter).is_empty(), + "no implicit join" + ); + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[models.Positive]", + "list[models.Positive]", + pending.clone(), + ); + assert_eq!(interpreter.eval_expr(&expression), Ok(pending)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[int64]", + "list[models.Positive]", + Value::List(vec![Value::Pending(Box::new(Value::Int64(7)))]), + ); + let error = interpreter.eval_expr(&expression).unwrap_err(); + assert!(error.contains("error evaluating refinement constraint for 'models.Positive'")); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + } } - /// Helper: create a string literal expression. - fn string(s: &str) -> Expr { - Expr::StringLiteral(s.to_string(), sp()) + #[test] + fn nested_reflected_refusals_stop_future_secret_callable_and_nominal_consumers() { + for shape in ["record", "enum", "machine"] { + for route in ["secret", "callable", "nominal"] { + let (actual, requested, payload, expected) = match route { + "secret" => ( + "secret[int64]", + "secret[models.Positive]", + Value::Int64(-913), + "reflected field refinement: cannot establish new invariants beneath secret", + ), + "callable" => ( + "function() returns int64", + "function() returns models.Positive", + Value::NamedFunction("plain".into()), + "reflected field refinement: unsupported callable or nominal refinement conversion", + ), + _ => ( + "Box[int64]", + "Box[models.Positive]", + Value::Struct { + concrete_type: Some("Box[int64]".into()), + type_name: "Box".into(), + fields: vec![("value".into(), Value::Int64(-913))], + }, + "reflected field refinement: unsupported callable or nominal refinement conversion", + ), + }; + let (mut interpreter, expression) = + nested_reflected_case(shape, actual, requested, payload); + let consumer_span = Span::new(FileId::new(0), 943, 944); + let exposed = match route { + "secret" => Expr::Declassify(Box::new(expression.clone()), consumer_span), + "callable" => Expr::Call(Box::new(expression.clone()), vec![], consumer_span), + _ => Expr::FieldAccess( + Box::new(expression.clone()), + ident("value"), + consumer_span, + ), + }; + let mut plain = func_def("plain", vec![], block(vec![return_stmt(int(-913))])); + plain.return_type = Some(type_named("int64")); + interpreter.register_function(&plain); + let mut echo = func_def( + "echo", + vec![("value", "models.Positive")], + block(vec![return_stmt(Expr::Coarsen( + Box::new(var("value")), + sp(), + ))]), + ); + echo.return_type = Some(type_named("int64")); + interpreter.register_function(&echo); + reuse_facts( + &mut interpreter, + &[ + (expression.span(), requested), + (consumer_span, "models.Positive"), + ], + ); + let error = interpreter + .eval_expr(&call("echo", vec![exposed])) + .unwrap_err(); + assert_eq!(error, expected, "{shape}/{route}"); + assert!(!error.contains("913")); + assert!( + reuse_markers(&mut interpreter).is_empty(), + "no hidden predicate or observer" + ); + assert!(interpreter.allow_checked_refinement_proofs); + } + let hidden = Value::Pending(Box::new(Value::Pending(Box::new(Value::Int64(7))))); + let (mut interpreter, expression) = nested_reflected_case( + shape, + "secret[models.Positive]", + "secret[models.Positive]", + hidden.clone(), + ); + assert_eq!(interpreter.eval_expr(&expression), Ok(hidden)); + assert!(reuse_markers(&mut interpreter).is_empty()); + // Inactive hidden arms cannot expose any unchecked value. + let (mut interpreter, expression) = nested_reflected_case( + shape, + "result[secret[int64], int64]", + "result[secret[models.Positive], int64]", + Value::ResultFail(Box::new(Value::Int64(7))), + ); + assert_eq!( + interpreter.eval_expr(&expression), + Ok(Value::ResultFail(Box::new(Value::Int64(7)))) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + } } - /// Helper: create a bool literal expression. - fn bool_expr(b: bool) -> Expr { - Expr::BoolLiteral(b, sp()) + #[test] + fn nested_reflected_checks_remain_private_and_validate_refinement_bases() { + for shape in ["record", "enum", "machine"] { + for proof_mode in [false, true] { + let payload = Value::List(vec![Value::Int64(-1)]); + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[int64]", + "list[models.Positive]", + payload.clone(), + ); + interpreter.allow_checked_refinement_proofs = proof_mode; + interpreter.checked_expression_types = None; + assert_eq!(interpreter.eval_expr(&expression), Ok(payload)); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.allow_checked_refinement_proofs, proof_mode); + } + let (mut interpreter, expression) = nested_reflected_case( + shape, + "list[int64]", + "ValidatedList", + Value::List(vec![Value::Int64(-1)]), + ); + let mut alias = type_alias("ValidatedList", "int64", Some(bool_expr(true))); + alias.base_type = + Interpreter::simple_type_expr_from_name("list[models.Positive]", sp()).unwrap(); + interpreter.register_type_alias(&alias); + assert_eq!( + interpreter.eval_expr(&expression), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + } } - /// Helper: create an identifier expression. - fn var(name: &str) -> Expr { - Expr::Ident(ident(name)) + #[test] + fn qualified_sum_refinement_refusal_preserves_primitive_and_domain_errors() { + let mut interpreter = reuse_interpreter(); + interpreter.current_function_trusted_stdlib = true; + for selected in ["secret[models.Positive]", "secret[secret[models.Positive]]"] { + let ty = Interpreter::simple_type_expr_from_name(selected, sp()).unwrap(); + for values in [vec![], vec![Value::Int64(1), Value::Int64(2)]] { + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + std::slice::from_ref(&ty), + &[Value::List(values)], + true + ), + Some(Err( + "list.__sum: cannot establish new invariants beneath secret".into() + )) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + std::slice::from_ref(&ty), + &[Value::List(vec![Value::String("hidden".into())])], + true + ), + Some(Err( + "list.__sum: list elements must be int64 or float64".into() + )) + ); + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + std::slice::from_ref(&ty), + &[Value::Bool(false)], + true + ), + Some(Err("list.__sum: argument must be a list".into())) + ); + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + std::slice::from_ref(&ty), + &[Value::List(vec![])], + false + ), + Some(Ok(Value::Int64(0))) + ); + } + for selected in [ + "secret[int64]", + "list[models.Positive]", + "secret[list[models.Positive]]", + ] { + let ty = Interpreter::simple_type_expr_from_name(selected, sp()).unwrap(); + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &[ty], + &[Value::List(vec![])], + true + ), + Some(Ok(Value::Int64(0))), + "existing empty fallback domain: {selected}" + ); + } + assert!(reuse_markers(&mut interpreter).is_empty()); } - /// Helper: create a binary expression. - fn binary(lhs: Expr, op: BinOp, rhs: Expr) -> Expr { - Expr::Binary(Box::new(lhs), op, Box::new(rhs), sp()) + fn install_colliding_type_aliases(interpreter: &mut Interpreter) { + for owner in ["Positive", "Large"] { + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias(owner, "int64", Some(bool_expr(false))), + ); + } + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); } - /// Helper: create a simple type expression. - fn type_named(name: &str) -> TypeExpr { - TypeExpr::Named(ident(name)) + #[test] + fn resolved_reflected_field_types_do_not_reexpand_caller_imports() { + for shape in ["record", "enum", "machine"] { + for (actual, requested, number, markers) in [ + ("int64", "Positive", 7, vec!["trace positive"]), + ("int64", "Positive", -1, vec!["trace positive"]), + ("models.Positive", "Positive", 7, vec![]), + ("models.Positive", "Large", 7, vec!["trace large"]), + ("models.Positive", "Large", 3, vec!["trace large"]), + ] { + let target = format!("models.{requested}"); + let (mut interpreter, mut expression) = + reflected_proof_case(shape, actual, &target, Value::Int64(number), true); + install_colliding_type_aliases(&mut interpreter); + let aliases = interpreter.namespace_alias_scopes.clone(); + let Expr::GenericCall(callee, types, args, span) = &mut expression else { + unreachable!() + }; + types[1] = type_named(&format!("selected.{requested}")); + // Inspect this call boundary directly: its output is canonical. + // General expression-normalization alias handling is separate. + let output = interpreter.eval_call_flow(callee, types, args, *span); + if number < 0 || (requested == "Large" && number <= 5) { + assert!( + matches!(output, Err(ref error) if error == &format!("refinement type constraint failed for '{target}'")), + "{shape}/{actual}/{requested}: {output:?}" + ); + } else { + assert!( + matches!(output, Ok(ExprFlow::Value(Value::Typed { ref type_name, ref value })) if type_name == &target && value.payload() == &Value::Int64(number)), + "{shape}/{actual}/{requested}: {output:?}" + ); + } + assert_eq!(reuse_markers(&mut interpreter), markers); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert!(interpreter.allow_checked_refinement_proofs); + } + } } - fn type_alias(name: &str, base: &str, constraint: Option) -> TypeAlias { - TypeAlias { - name: ident(name), - base_type: type_named(base), - constraint, - exported: false, - root_exported: false, - span: sp(), + #[test] + fn exact_reflected_root_refinements_retain_identity_without_predicates() { + for shape in ["record", "enum", "machine"] { + for depth in 0..=2 { + let mut payload = Value::Int64(7); + for _ in 0..depth { + payload = Value::Pending(Box::new(payload)); + } + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Positive", + "models.Positive", + payload, + true, + ); + let original = format!("{:?}", interpreter.get_variable("input").unwrap()); + let Expr::GenericCall(callee, types, args, span) = &expression else { + unreachable!() + }; + let ExprFlow::Value(output) = interpreter + .eval_call_flow(callee, types, args, *span) + .unwrap() + else { + panic!("field read must produce a value") + }; + let mut ready = &output; + for _ in 0..depth { + let Value::Pending(inner) = ready else { + panic!("{shape}/{depth}: pending depth changed: {output:?}") + }; + ready = inner; + } + assert!( + matches!(ready, Value::Typed { type_name, value } + if type_name == "models.Positive" && value.payload() == &Value::Int64(7)), + "{shape}/{depth}: {output:?}" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + original + ); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } } } - /// Helper: create a variable declaration statement. - fn var_decl(name: &str, value: Expr) -> Stmt { - Stmt::VarDecl(VarDecl { - mutable: true, - ty: type_named("int64"), - name: ident(name), - value, - span: sp(), - }) + #[test] + fn metadata_free_reflected_declaration_aliases_keep_their_lexical_lookup() { + for shape in ["record", "enum", "machine"] { + for direct_api in [false, true] { + let (mut interpreter, expression) = reflected_proof_case( + shape, + "selected.Positive", + "models.Positive", + Value::Int64(7), + false, + ); + interpreter.set_namespace_alias("selected".into(), "models".into()); + let mut selector = interpreter.get_variable("selector").unwrap().clone(); + let Value::Struct { fields, .. } = &mut selector else { + unreachable!() + }; + fields + .iter_mut() + .find(|(name, _)| name == "type_name") + .unwrap() + .1 = Value::String("models.Positive".into()); + interpreter.set_variable("selector", selector); + interpreter.checked_expression_types = None; + let aliases = interpreter.namespace_alias_scopes.clone(); + let result = if direct_api { + let Expr::GenericCall(callee, types, _, _) = &expression else { + unreachable!() + }; + let Expr::FieldAccess(owner, member, _) = callee.as_ref() else { + unreachable!() + }; + let name = Interpreter::extract_dotted_name(owner, &member.name).unwrap(); + let values = vec![ + interpreter.get_variable("input").unwrap().clone(), + interpreter.get_variable("selector").unwrap().clone(), + ]; + interpreter + .call_builtin_with_type_args(&name, types, &values) + .unwrap() + } else { + interpreter.eval_expr(&expression) + }; + assert_eq!(result, Ok(Value::Int64(7)), "{shape}/{direct_api}"); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + } + } } - fn typed_var_decl(type_name: &str, name: &str, value: Expr) -> Stmt { - Stmt::VarDecl(VarDecl { - mutable: true, - ty: type_named(type_name), - name: ident(name), - value, - span: sp(), - }) + #[test] + fn raw_and_resolved_typed_builtin_entries_restore_aliases_on_all_results() { + let mut interpreter = reuse_interpreter(); + install_colliding_type_aliases(&mut interpreter); + let aliases = interpreter.namespace_alias_scopes.clone(); + assert_eq!( + interpreter.call_builtin_with_type_args( + "type.name", + &[type_named("selected.Positive")], + &[] + ), + Some(Ok(Value::String("models.Positive".into()))) + ); + assert_eq!( + interpreter.call_builtin_with_type_args_from_source( + "type.name", + &[type_named("models.Positive")], + &[], + false + ), + Some(Ok(Value::String("models.Positive".into()))) + ); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert!(matches!( + interpreter.call_builtin_with_type_args_from_source("type.field_value", &[], &[], true), + Some(Err(_)) + )); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert!( + interpreter + .call_builtin_with_type_args_from_source("not.a.builtin", &[], &[], true) + .is_none() + ); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert!(reuse_markers(&mut interpreter).is_empty()); } - /// Helper: create an assignment statement. - fn assign(name: &str, value: Expr) -> Stmt { - Stmt::Assign(AssignStmt { - target: var(name), - value, - span: sp(), - }) + #[test] + fn resolved_sum_actual_type_keeps_canonical_owner_and_callback_imports() { + let mut interpreter = reuse_interpreter(); + install_colliding_type_aliases(&mut interpreter); + interpreter.current_function_trusted_stdlib = true; + let aliases = interpreter.namespace_alias_scopes.clone(); + let call_span = Span::new(FileId::new(0), 921, 922); + reuse_facts(&mut interpreter, &[(call_span, "models.Positive")]); + for values in [vec![int(1), int(2)], vec![]] { + let output = interpreter.eval_call_flow( + &field_access(var("list"), "__sum"), + &[type_named("selected.Positive")], + &[CallArg { + name: None, + value: Expr::ListConstruct(values.clone(), sp()), + span: sp(), + }], + call_span, + ); + if values.is_empty() { + assert!( + matches!(output, Err(ref error) if error == "refinement type constraint failed for 'models.Positive'") + ); + } else { + assert!( + matches!(output, Ok(ExprFlow::Value(Value::Typed { ref type_name, ref value })) if type_name == "models.Positive" && value.payload() == &Value::Int64(3)) + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert!(interpreter.current_function_trusted_stdlib); + } + let mut rank = func_def( + "rank", + vec![("number", "int64")], + block(vec![return_stmt(var("number"))]), + ); + rank.return_type = Some(type_named("int64")); + interpreter.register_function_in_namespace(Some("helpers"), &rank); + let mut key = func_def( + "key", + vec![("number", "int64")], + block(vec![return_stmt(dotted_call( + "chosen", + "rank", + vec![var("number")], + ))]), + ); + key.return_type = Some(type_named("int64")); + interpreter.register_function(&key); + interpreter.set_namespace_alias("chosen".into(), "helpers".into()); + let aliases = interpreter.namespace_alias_scopes.clone(); + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sort_by", + &[], + &[ + Value::List(vec![Value::Int64(2), Value::Int64(1)]), + Value::NamedFunction("key".into()) + ], + false, + ), + Some(Ok(Value::List(vec![Value::Int64(1), Value::Int64(2)]))) + ); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + } + + fn reflected_proof_case( + shape: &str, + actual: &str, + requested: &str, + payload: Value, + checked_layout: bool, + ) -> (Interpreter, Expr) { + let mut interpreter = reuse_interpreter(); + interpreter.register_type_alias(&type_alias("PositiveAlias", "models.Positive", None)); + let kind = if actual == "int64" { + "primitive" + } else if actual == "PositiveAlias" { + "alias" + } else { + "refinement" + }; + let primitive = ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".into()), + false, + vec![], + ); + let children = match kind { + "primitive" => vec![], + "alias" => vec![ReflectionTypeInfo::new( + "models.Positive", + "refinement", + Some("int64_type".into()), + false, + vec![primitive], + )], + _ => vec![primitive], + }; + let field = ReflectionFieldInfo::new( + 0, + "value", + actual, + kind, + "value", + false, + ReflectionTypeInfo::new(actual, kind, Some("int64_type".into()), false, children), + ); + let mut metadata = ReflectionMetadata::new(); + let (owner, builtin, member, value) = match shape { + "record" => { + interpreter.register_struct(&struct_def("Holder", vec![("value", actual)], vec![])); + metadata.insert_type_fields("Holder", vec![field.clone()]); + ( + "Holder", + "field_value", + None, + Value::Struct { + concrete_type: None, + type_name: "Holder".into(), + fields: vec![("value".into(), payload)], + }, + ) + } + "enum" => { + interpreter.register_enum(&enum_def_with_field("Choice", "item", "value", actual)); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "item", + 0, + false, + vec![field.clone()], + )], + ); + ( + "Choice", + "variant_field_value", + Some("item"), + Value::Enum { + type_name: "Choice".into(), + variant: "item".into(), + fields: vec![payload], + }, + ) + } + "machine" => { + interpreter.register_machine(&machine_def( + "Session", + vec![("active", vec![("value", type_named(actual))])], + vec![], + )); + metadata.insert_machine( + "Session", + ReflectionMachineInfo::new( + vec![ReflectionMachineStateInfo::new( + 0, + "active", + false, + vec![field.clone()], + )], + vec![], + ), + ); + ( + "Session", + "machine_field_value", + Some("active"), + Value::Machine { + type_name: "Session".into(), + state: "active".into(), + fields: vec![payload], + }, + ) + } + _ => unreachable!(), + }; + if checked_layout { + interpreter.set_reflection_metadata(Arc::new(metadata)); + } + interpreter.set_variable("input", value); + interpreter.set_variable( + "selector", + Interpreter::reflection_field_info_value(owner, member, &field), + ); + let expression = Expr::GenericCall( + Box::new(field_access(var("type"), builtin)), + vec![type_named(owner), type_named(requested)], + vec![ + named_arg("value", var("input")), + named_arg("field", var("selector")), + ], + Span::new(FileId::new(0), 701, 702), + ); + reuse_facts(&mut interpreter, &[(expression.span(), requested)]); + (interpreter, expression) } - /// Helper: create an assert statement. - fn assert_stmt(condition: Expr, message: Option) -> Stmt { - Stmt::Assert(AssertStmt { - condition, - message, - span: sp(), - }) + #[test] + fn reflected_field_producers_establish_new_requested_refinements_for_all_shapes() { + for shape in ["record", "enum", "machine"] { + for checked_layout in [false, true] { + for payload in [-1, 7] { + // A runtime label cannot turn an actual int64 field into + // refinement proof, even if it claims the requested owner. + let raw = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(payload)), + }; + let (mut interpreter, expression) = reflected_proof_case( + shape, + "int64", + "models.Positive", + raw, + checked_layout, + ); + let original = interpreter.get_variable("input").unwrap().clone(); + let output = interpreter.eval_expr(&expression); + if payload < 0 { + assert_eq!( + output, + Err("refinement type constraint failed for 'models.Positive'".into()), + "{shape}/{checked_layout}" + ); + } else { + assert_eq!( + output.unwrap(), + Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(7)) + } + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.get_variable("input"), Some(&original)); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } + } + } } - /// Helper: create a block from statements. - fn block(stmts: Vec) -> Block { - Block { stmts, span: sp() } + #[test] + fn reflected_field_producers_reuse_exact_and_ancestor_declared_types_only() { + for shape in ["record", "enum", "machine"] { + for checked_layout in [false, true] { + for (actual, requested, markers) in [ + ("models.Positive", "models.Positive", vec![]), + ("PositiveAlias", "models.Positive", vec![]), + ("models.Positive", "PositiveAlias", vec![]), + ("models.Positive", "models.Large", vec!["trace large"]), + ("PositiveAlias", "models.Large", vec!["trace large"]), + ("models.Positive", "models.Sibling", vec!["trace sibling"]), + ] { + let (mut interpreter, expression) = reflected_proof_case( + shape, + actual, + requested, + Value::Int64(7), + checked_layout, + ); + assert_eq!( + interpreter.eval_expr(&expression).unwrap().payload(), + &Value::Int64(7) + ); + assert_eq!( + reuse_markers(&mut interpreter), + markers, + "{shape}/{actual}/{requested}/{checked_layout}" + ); + } + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Positive", + "models.Large", + Value::Int64(3), + checked_layout, + ); + assert_eq!( + interpreter.eval_expr(&expression), + Err("refinement type constraint failed for 'models.Large'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + + // Ordinary requested types retain the original value and owner. + let original = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(7)), + }; + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Positive", + "int64", + original.clone(), + checked_layout, + ); + assert_eq!(interpreter.eval_expr(&expression), Ok(original)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + } } - /// Helper: create a return statement. - fn return_stmt(value: Expr) -> Stmt { - Stmt::Return(ReturnStmt { - value: Some(value), - span: sp(), - }) + #[test] + fn reflected_declared_ancestor_proofs_preserve_all_selector_results_and_pending_depths() { + for shape in ["record", "enum", "machine"] { + for checked_layout in [false, true] { + for (actual, requested) in [ + ("models.Large", "models.Positive"), + ("LargeAlias", "models.Positive"), + ("models.Large", "PositiveAlias"), + ] { + for depth in [0, 1, 2] { + let mut payload = Value::Typed { + type_name: "models.Large".into(), + value: Box::new(Value::Int64(7)), + }; + let mut expected = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(7)), + }; + for _ in 0..depth { + payload = Value::Pending(Box::new(payload)); + expected = Value::Pending(Box::new(expected)); + } + let (mut interpreter, expression) = + reflected_proof_case(shape, actual, requested, payload, checked_layout); + interpreter.register_type_alias(&type_alias( + "LargeAlias", + "models.Large", + None, + )); + let source = interpreter.get_variable("input").unwrap().clone(); + let selector = interpreter.get_variable("selector").unwrap().clone(); + let output = interpreter.eval_expr(&expression).unwrap(); + assert_eq!( + format!("{output:?}"), + format!("{expected:?}"), + "{shape}/{checked_layout}/{actual}/{requested}/{depth}" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + assert_eq!( + format!("{:?}", interpreter.get_variable("selector").unwrap()), + format!("{selector:?}"), + ); + assert!(interpreter.allow_checked_refinement_proofs); + } + } + } + } } - fn respond_stmt(value: Expr) -> Stmt { - Stmt::Respond(RespondStmt { value, span: sp() }) + #[test] + fn reflected_declared_ancestor_proofs_preserve_owned_unicode_sources() { + for shape in ["record", "enum", "machine"] { + for checked_layout in [false, true] { + for depth in [0, 1, 2] { + let mut payload = Value::Typed { + type_name: "models.Long".into(), + value: Box::new(Value::String("Agent-???".into())), + }; + let mut expected = Value::Typed { + type_name: "models.Text".into(), + value: Box::new(Value::String("Agent-???".into())), + }; + for _ in 0..depth { + payload = Value::Pending(Box::new(payload)); + expected = Value::Pending(Box::new(expected)); + } + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Long", + "models.Text", + payload, + checked_layout, + ); + for (owner, base, marker) in + [("Text", "string", "text"), ("Long", "Text", "long")] + { + let mut predicate = func_def( + marker, + vec![(marker, "string")], + block(vec![ + Stmt::Trace(TraceStmt { + name: ident(marker), + span: sp(), + }), + return_stmt(Expr::BoolLiteral(true, sp())), + ]), + ); + predicate.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("models"), &predicate); + interpreter.register_type_alias_in_namespace( + Some("models"), + &type_alias(owner, base, Some(call(marker, vec![var("value")]))), + ); + } + let source = interpreter.get_variable("input").unwrap().clone(); + let output = interpreter.eval_expr(&expression).unwrap(); + assert_eq!( + format!("{output:?}"), + format!("{expected:?}"), + "{shape}/{checked_layout}/{depth}" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + format!("{:?}", interpreter.get_variable("input").unwrap()), + format!("{source:?}"), + ); + } + } + } } - /// Helper: create a function call expression. - fn call(name: &str, args: Vec) -> Expr { - Expr::Call( - Box::new(var(name)), - args.into_iter() - .map(|value| CallArg { - name: None, - value, - span: sp(), - }) - .collect(), - sp(), - ) + #[test] + fn reflected_declared_ancestor_proofs_do_not_escape_root_schema_or_checked_mode() { + for shape in ["record", "enum", "machine"] { + for mode in ["no_fact", "unrelated_fact", "raw", "disabled"] { + let original = Value::Typed { + type_name: "models.Large".into(), + value: Box::new(Value::Int64(7)), + }; + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Large", + "models.Positive", + original.clone(), + true, + ); + if mode == "no_fact" { + interpreter.checked_expression_types = None; + } else if mode == "unrelated_fact" { + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 970, 971), "models.Positive")], + ); + } else if mode == "disabled" { + interpreter.allow_checked_refinement_proofs = false; + } + let source = interpreter.get_variable("input").unwrap().clone(); + let output = if mode == "raw" { + let Expr::GenericCall(callee, types, _, _) = &expression else { + unreachable!() + }; + let Expr::FieldAccess(owner, member, _) = callee.as_ref() else { + unreachable!() + }; + let name = Interpreter::extract_dotted_name(owner, &member.name).unwrap(); + interpreter.call_function_with_type_args( + &name, + types, + vec![ + source.clone(), + interpreter.get_variable("selector").unwrap().clone(), + ], + ) + } else { + interpreter.eval_expr(&expression) + }; + assert_eq!(output, Ok(original), "{shape}/{mode}"); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.get_variable("input"), Some(&source)); + } + // Runtime labels on an actual primitive field cannot supply proof. + let (mut interpreter, expression) = reflected_proof_case( + shape, + "int64", + "models.Positive", + Value::Typed { + type_name: "models.Large".into(), + value: Box::new(Value::Int64(-1)), + }, + true, + ); + assert_eq!( + interpreter.eval_expr(&expression), + Err("refinement type constraint failed for 'models.Positive'".into(),) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + } + let mut interpreter = reuse_interpreter(); + interpreter.register_struct(&struct_def( + "Nested", + vec![("value", "models.Large")], + vec![], + )); + for actual in [ + type_named("models.Sibling"), + type_named("Nested"), + TypeExpr::Generic(ident("secret"), vec![type_named("models.Large")], sp()), + TypeExpr::Generic(ident("list"), vec![type_named("models.Large")], sp()), + ] { + assert!( + interpreter + .producer_declared_refinement_proof(&actual, &type_named("models.Positive"),) + .is_none() + ); + } } - /// Helper: create a FunctionDef. - fn func_def(name: &str, params: Vec<(&str, &str)>, body: Block) -> FunctionDef { - FunctionDef { - name: ident(name), - type_params: vec![], - params: params - .into_iter() - .map(|(pname, ptype)| Param { - view: false, - mutable: false, - name: ident(pname), - ty: type_named(ptype), - span: sp(), - }) - .collect(), - return_type: None, - body, - exported: false, - span: sp(), + #[test] + fn reflected_field_producer_gates_preserve_raw_and_metadata_free_boundaries() { + for shape in ["record", "enum", "machine"] { + for mode in ["no_fact", "unrelated_fact", "raw", "disabled"] { + let (mut interpreter, expression) = + reflected_proof_case(shape, "int64", "models.Positive", Value::Int64(-1), true); + if mode == "no_fact" { + interpreter.checked_expression_types = None; + } else if mode == "unrelated_fact" { + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 910, 911), "models.Positive")], + ); + } else if mode == "disabled" { + interpreter.allow_checked_refinement_proofs = false; + } + let output = if mode == "raw" { + let Expr::GenericCall(callee, types, _, _) = &expression else { + unreachable!() + }; + let Expr::FieldAccess(owner, member, _) = callee.as_ref() else { + unreachable!() + }; + let name = Interpreter::extract_dotted_name(owner, &member.name).unwrap(); + let values = vec![ + interpreter.get_variable("input").unwrap().clone(), + interpreter.get_variable("selector").unwrap().clone(), + ]; + interpreter.call_function_with_type_args(&name, types, values) + } else { + interpreter.eval_expr(&expression) + }; + assert_eq!(output, Ok(Value::Int64(-1)), "{shape}/{mode}"); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + interpreter.normalize_and_validate_value( + &type_named("models.Positive"), + output.unwrap() + ), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!( + interpreter.allow_checked_refinement_proofs, + mode != "disabled" + ); + } + // A checked private pipeline must establish its explicit requested + // type even though the step fact records only the inbound owner. + let (mut interpreter, expression) = + reflected_proof_case(shape, "int64", "models.Positive", Value::Int64(-1), true); + let Expr::GenericCall(callee, mut types, mut args, _) = expression else { + unreachable!() + }; + let inbound = type_expr_display(&types[0]); + types[1] = type_named("Requested"); + interpreter.type_arg_scopes.push(HashMap::from([( + "Requested".into(), + type_named("models.Positive"), + )])); + let input = args.remove(0).value; + let step_span = Span::new(FileId::new(0), 703, 704); + let piped = Expr::Pipeline( + Box::new(input), + vec![PipelineStep { + function: Expr::GenericCall(callee, types, args, sp()), + extra_args: vec![], + handle: None, + span: step_span, + }], + sp(), + ); + reuse_facts(&mut interpreter, &[(step_span, &inbound)]); + assert_eq!( + interpreter.eval_expr(&piped), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.type_arg_scopes.len(), 1); + assert!(interpreter.allow_checked_refinement_proofs); } } - fn generic_json_hook(name: &str, body: Block) -> FunctionDef { - FunctionDef { - name: ident(name), - type_params: vec![ident("T")], - params: vec![Param { - view: false, - mutable: false, - name: ident("raw"), - ty: type_named("string"), - span: sp(), - }], - return_type: None, - body, - exported: false, - span: sp(), + #[test] + fn reflected_field_producers_preserve_pending_and_selector_failure_order() { + for shape in ["record", "enum", "machine"] { + for depth in [1, 2] { + let mut pending = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(7)), + }; + for _ in 0..depth { + pending = Value::Pending(Box::new(pending)); + } + let (mut interpreter, expression) = reflected_proof_case( + shape, + "models.Positive", + "models.Positive", + pending.clone(), + true, + ); + assert_eq!(interpreter.eval_expr(&expression), Ok(pending)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + for malformed in [ + "root_pending", + "missing_payload", + "wrong_owner", + "wrong_index", + ] { + let (mut interpreter, expression) = + reflected_proof_case(shape, "int64", "models.Positive", Value::Int64(-1), true); + let mut input = interpreter.get_variable("input").unwrap().clone(); + let mut selector = interpreter.get_variable("selector").unwrap().clone(); + match malformed { + "root_pending" => input = Value::Pending(Box::new(input)), + "missing_payload" => match &mut input { + Value::Struct { fields, .. } => fields.clear(), + Value::Enum { fields, .. } | Value::Machine { fields, .. } => { + fields.clear() + } + _ => unreachable!(), + }, + _ => { + let Value::Struct { fields, .. } = &mut selector else { + unreachable!() + }; + let (name, replacement) = if malformed == "wrong_owner" { + ("owner_type", Value::String("Other".into())) + } else { + ("index", Value::Int64(9)) + }; + fields + .iter_mut() + .find(|(field, _)| field == name) + .unwrap() + .1 = replacement; + } + } + interpreter.set_variable("input", input.clone()); + interpreter.set_variable("selector", selector.clone()); + let checked_error = interpreter.eval_expr(&expression).unwrap_err(); + assert!(reuse_markers(&mut interpreter).is_empty()); + interpreter.checked_expression_types = None; + assert_eq!( + interpreter.eval_expr(&expression), + Err(checked_error), + "{shape}/{malformed}" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.get_variable("input"), Some(&input)); + assert_eq!(interpreter.get_variable("selector"), Some(&selector)); + } + let (mut interpreter, expression) = reflected_proof_case( + shape, + "int64", + "models.Positive", + Value::Pending(Box::new(Value::Int64(7))), + true, + ); + let error = interpreter.eval_expr(&expression).unwrap_err(); + assert!(error.contains("error evaluating refinement constraint for 'models.Positive'")); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); } } - fn json_tree_null() -> Value { - Value::Enum { - type_name: "json.JsonTree".to_string(), - variant: "null".to_string(), - fields: Vec::new(), + #[test] + fn sum_producer_validates_fresh_refinements_before_they_become_source_proofs() { + let mut interpreter = reuse_interpreter(); + interpreter.current_function_trusted_stdlib = true; + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 601, 602), "models.Positive")], + ); + let selected = [type_named("models.Positive")]; + for items in [vec![], vec![Value::Int64(i64::MAX), Value::Int64(1)]] { + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &selected, + &[Value::List(items)], + true + ), + Some(Err( + "refinement type constraint failed for 'models.Positive'".into() + )) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + } + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &selected, + &[Value::List(vec![Value::Int64(1), Value::Int64(2)])], + true + ), + Some(Ok(Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(3)) + })) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + for (value, expected) in [ + ( + Value::Pending(Box::new(Value::List(vec![]))), + "argument must be a list", + ), + ( + Value::List(vec![Value::Pending(Box::new(Value::Int64(1)))]), + "list elements must be int64 or float64", + ), + ( + Value::List(vec![Value::Int64(1), Value::Bool(true)]), + "mixed types", + ), + ] { + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &selected, + &[value], + true + ), + Some(Err(format!("list.__sum: {expected}"))) + ); + assert!( + reuse_markers(&mut interpreter).is_empty(), + "producer errors precede predicates" + ); } } - fn string_type_info() -> ReflectionTypeInfo { - ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ) + #[test] + fn sum_producer_uses_actual_intrinsic_type_arguments_and_preserves_primitive_aliases() { + let mut interpreter = reuse_interpreter(); + interpreter.current_function_trusted_stdlib = true; + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 601, 602), "models.Positive")], + ); + interpreter.current_type_arguments = + vec![type_named("int8"), type_named("models.Positive")]; + interpreter.type_arg_scopes.push(HashMap::from([ + ("X".into(), type_named("int8")), + ("Y".into(), type_named("models.Positive")), + ])); + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &[interpreter.substitute_type_expr(&type_named("Y"))], + &[Value::List(vec![])], + true + ), + Some(Err( + "refinement type constraint failed for 'models.Positive'".into() + )) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + interpreter.current_type_arguments = vec![type_named("models.Positive")]; + interpreter.register_type_alias(&type_alias("Small", "int8", None)); + for selected in ["int8", "Small"] { + assert_eq!( + interpreter.call_higher_order_builtin_with_type_args( + "list.__sum", + &[type_named(selected)], + &[Value::List(vec![Value::Int64(127), Value::Int64(1)])], + true + ), + Some(Ok(Value::Int64(-128))), + "{selected}" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + interpreter.type_arg_scopes.clear(); + interpreter.current_type_arguments.clear(); + assert_eq!( + interpreter.call_higher_order_builtin("list.__sum", &[Value::List(vec![])]), + Some(Ok(Value::Int64(0))) + ); + assert_eq!( + interpreter.call_higher_order_builtin( + "list.__sum", + &[Value::List(vec![Value::Int64(1), Value::Int64(2)])] + ), + Some(Ok(Value::Int64(3))) + ); } - fn uint64_type_info() -> ReflectionTypeInfo { - ReflectionTypeInfo::new( - "uint64", - "primitive", - Some("uint64_type".to_string()), - false, - Vec::new(), - ) - } + #[test] + fn sum_producer_rejects_invalid_results_before_checked_locals_arguments_and_returns() { + for route in ["local", "argument", "return", "pipeline"] { + let mut interpreter = reuse_interpreter(); + let kernel_span = Span::new(FileId::new(0), 601, 602); + let kernel = Expr::GenericCall( + Box::new(field_access(var("list"), "__sum")), + vec![type_named("Y")], + vec![CallArg { + name: None, + value: var("items"), + span: sp(), + }], + kernel_span, + ); + let returned = reuse_source("value", 603); + let mut echo = func_def( + "echo", + vec![("value", "models.Positive")], + block(vec![return_stmt(returned.clone())]), + ); + echo.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&echo); + let body = match route { + "pipeline" => block(vec![return_stmt(Expr::Pipeline( + Box::new(var("items")), + vec![PipelineStep { + function: Expr::GenericCall( + Box::new(field_access(var("list"), "__sum")), + vec![type_named("Y")], + vec![], + sp(), + ), + extra_args: vec![], + handle: None, + span: Span::new(FileId::new(0), 609, 610), + }], + kernel_span, + ))]), + "local" => block(vec![ + typed_var_decl("Y", "selected", kernel), + return_stmt(reuse_source("selected", 606)), + ]), + "argument" => block(vec![return_stmt(Expr::Call( + Box::new(var("echo")), + vec![CallArg { + name: None, + value: kernel, + span: sp(), + }], + Span::new(FileId::new(0), 607, 608), + ))]), + _ => block(vec![return_stmt(kernel)]), + }; + let mut facade = func_def("facade", vec![("items", "list")], body); + facade.span = Span::new(FileId::new(STDLIB_FILE_ID_START), 604, 605); + facade.type_params = vec![ident("X"), ident("Y")]; + facade.params[0].ty = TypeExpr::Generic(ident("list"), vec![type_named("Y")], sp()); + facade.return_type = Some(type_named("Y")); + interpreter.register_function(&facade); + reuse_facts( + &mut interpreter, + &[ + (kernel_span, "models.Positive"), + (returned.span(), "models.Positive"), + (Span::new(FileId::new(0), 606, 607), "models.Positive"), + (Span::new(FileId::new(0), 607, 608), "models.Positive"), + (Span::new(FileId::new(0), 609, 610), "list[models.Positive]"), + ], + ); + let invocation = Expr::GenericCall( + Box::new(var("facade")), + vec![type_named("int8"), type_named("models.Positive")], + vec![CallArg { + name: None, + value: Expr::ListConstruct(vec![], sp()), + span: sp(), + }], + sp(), + ); + assert_eq!( + interpreter.eval_expr(&invocation), + Err("refinement type constraint failed for 'models.Positive'".into()), + "{route}" + ); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive"], + "{route}" + ); + assert!(interpreter.allow_checked_refinement_proofs); + assert!(!interpreter.current_function_trusted_stdlib); + assert!(interpreter.current_type_arguments.is_empty()); + assert!(interpreter.type_arg_scopes.is_empty()); + + let mut successful = invocation.clone(); + let Expr::GenericCall(_, _, args, _) = &mut successful else { + unreachable!() + }; + args[0].value = Expr::ListConstruct(vec![int(1), int(2)], sp()); + assert_eq!( + interpreter.eval_expr(&successful).unwrap().payload(), + &Value::Int64(3), + "{route}" + ); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive"], + "{route}" + ); - fn string_field_info(index: usize, name: &str) -> ReflectionFieldInfo { - ReflectionFieldInfo::new( - index, - name, - "string", - "primitive", - name, - false, - string_type_info(), - ) + if route == "return" { + // Raw calls inherit proof-disabled execution and keep their + // existing single forced facade-return validation. + assert_eq!( + interpreter + .call_function_with_type_args( + "facade", + &[type_named("int8"), type_named("models.Positive")], + vec![Value::List(vec![Value::Int64(1), Value::Int64(2)])], + ) + .unwrap() + .payload(), + &Value::Int64(3), + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + + // No fact at the actual ordinary Call result means no new + // producer check, even when unrelated checked facts exist. + for partial_metadata in [false, true] { + interpreter.checked_expression_types = None; + if partial_metadata { + reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); + } + assert_eq!( + interpreter.eval_expr(&successful).unwrap().payload(), + &Value::Int64(3), + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + } + } + } } - fn uint64_field_info(index: usize, name: &str) -> ReflectionFieldInfo { - ReflectionFieldInfo::new( - index, - name, - "uint64", - "primitive", - name, - false, - uint64_type_info(), + #[test] + fn sum_pipeline_uses_checked_input_only_as_execution_evidence() { + let step_span = Span::new(FileId::new(0), 611, 612); + let expression = Expr::Pipeline( + Box::new(Expr::ListConstruct(vec![int(1), int(2)], sp())), + vec![PipelineStep { + function: Expr::GenericCall( + Box::new(field_access(var("list"), "__sum")), + vec![type_named("models.Positive")], + vec![], + sp(), + ), + extra_args: vec![], + handle: None, + span: step_span, + }], + Span::new(FileId::new(0), 613, 614), + ); + for (has_step_fact, allow_proofs) in [(true, true), (false, true), (true, false)] { + let mut interpreter = reuse_interpreter(); + interpreter.current_function_trusted_stdlib = true; + interpreter.allow_checked_refinement_proofs = allow_proofs; + // Deliberately omit the output fact. The input list type cannot + // serve as the output proof or replace the explicit Positive T. + if has_step_fact { + reuse_facts(&mut interpreter, &[(step_span, "list[models.Positive]")]); + } + let output = interpreter.eval_expr(&expression).unwrap(); + assert_eq!(output.payload(), &Value::Int64(3)); + if has_step_fact && allow_proofs { + assert!( + matches!(output, Value::Typed { ref type_name, .. } if type_name == "models.Positive") + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + } else { + assert_eq!(output, Value::Int64(3)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + assert_eq!(interpreter.allow_checked_refinement_proofs, allow_proofs); + } + } + + fn raw_proof_holder(value: i64) -> Value { + Value::Struct { + concrete_type: None, + type_name: "Holder".into(), + fields: vec![( + "value".into(), + Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(value)), + }, + )], + } + } + + fn raw_proof_echo(value: Expr) -> Expr { + Expr::Call( + Box::new(var("echo")), + vec![named_arg("input", value)], + Span::new(FileId::new(0), 506, 507), ) } - #[test] - fn type_info_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "int64", - "primitive", - Some("int64_type".to_string()), - true, - Vec::new(), + fn raw_proof_interpreter() -> (Interpreter, Expr, Expr) { + let mut interpreter = reuse_interpreter(); + interpreter.register_struct(&struct_def( + "Holder", + vec![("value", "models.Positive")], + vec![], )); + let returned = reuse_source("input", 501); + let mut echo = func_def( + "echo", + vec![("input", "models.Positive")], + block(vec![return_stmt(returned.clone())]), + ); + echo.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&echo); + let field = Expr::FieldAccess( + Box::new(var("holder")), + ident("value"), + Span::new(FileId::new(0), 502, 503), + ); + reuse_facts( + &mut interpreter, + &[ + (returned.span(), "models.Positive"), + (Span::new(FileId::new(0), 506, 507), "models.Positive"), + (Span::new(FileId::new(0), 507, 508), "models.Positive"), + (field.span(), "models.Positive"), + ], + ); + (interpreter, field, returned) + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let value = interp - .call_builtin_with_type_args("type.info", &[type_named("int64")], &[]) - .expect("type.info should be a typed builtin") - .expect("type.info should evaluate"); + #[test] + fn raw_call_disables_nested_field_proofs_for_calls_locals_and_callbacks() { + for route in ["local", "call", "pipeline", "named", "inline"] { + let (mut interpreter, field, _) = raw_proof_interpreter(); + let initializer = match route { + "local" => field, + "pipeline" => Expr::Pipeline( + Box::new(field), + vec![PipelineStep { + function: var("echo"), + extra_args: vec![], + handle: None, + span: Span::new(FileId::new(0), 502, 503), + }], + Span::new(FileId::new(0), 507, 508), + ), + _ => raw_proof_echo(field), + }; + let body = block(vec![ + typed_var_decl("models.Positive", "selected", initializer), + return_stmt(Expr::Coarsen(Box::new(var("selected")), sp())), + ]); + let mut raw = func_def("raw", vec![("holder", "Holder")], body.clone()); + raw.return_type = Some(type_named("int64")); + interpreter.register_function(&raw); + let result = match route { + "named" => interpreter.call_fn_value( + Value::NamedFunction("raw".into()), + vec![raw_proof_holder(-1)], + ), + "inline" => { + let callback = interpreter + .eval_expr(&Expr::InlineFn( + raw.params.clone(), + Some(type_named("int64")), + body, + sp(), + )) + .unwrap(); + interpreter.call_fn_value(callback, vec![raw_proof_holder(-1)]) + } + _ => interpreter.call_function("raw", vec![raw_proof_holder(-1)]), + }; + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()), + "{route}" + ); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive"], + "{route}" + ); + assert!(interpreter.allow_checked_refinement_proofs, "{route}"); + assert_eq!(interpreter.scopes.len(), 1, "{route}"); + assert!( + interpreter + .checked_expression_type(Span::new(FileId::new(0), 502, 503)) + .is_some() + ); + } + } - let Value::Struct { fields, .. } = value else { - panic!("expected TypeInfo struct"); + #[test] + fn raw_call_disables_list_element_and_direct_constructor_field_proofs() { + let (mut interpreter, field, returned) = raw_proof_interpreter(); + let element = reuse_source("item", 504); + reuse_facts( + &mut interpreter, + &[ + (field.span(), "models.Positive"), + (returned.span(), "models.Positive"), + (Span::new(FileId::new(0), 506, 507), "models.Positive"), + (Span::new(FileId::new(0), 507, 508), "models.Positive"), + (element.span(), "models.Positive"), + ], + ); + let mut raw = func_def( + "raw_list", + vec![("items", "list")], + block(vec![ + Stmt::For(ForStmt { + variable: ident("item"), + value_variable: None, + view: false, + iterable: var("items"), + body: block(vec![typed_var_decl( + "models.Positive", + "selected", + raw_proof_echo(element), + )]), + span: sp(), + }), + return_stmt(int(0)), + ]), + ); + raw.params[0].ty = + TypeExpr::Generic(ident("list"), vec![type_named("models.Positive")], sp()); + raw.return_type = Some(type_named("int64")); + interpreter.register_function(&raw); + let Value::Struct { fields, .. } = raw_proof_holder(-1) else { + unreachable!() }; - let has_secret = fields - .iter() - .find_map(|(name, value)| { - if name == "has_secret" { - Some(value) - } else { - None - } - }) - .expect("TypeInfo.has_secret field should exist"); - assert_eq!(has_secret, &Value::Bool(true)); + assert_eq!( + interpreter.call_function("raw_list", vec![Value::List(vec![fields[0].1.clone()])]), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + + let mut rebuild = func_def( + "rebuild", + vec![("holder", "Holder")], + block(vec![return_stmt(call("Holder", vec![field]))]), + ); + rebuild.return_type = Some(TypeExpr::Generic( + ident("result"), + vec![type_named("Holder"), type_named("string")], + sp(), + )); + interpreter.register_function(&rebuild); + assert_eq!( + interpreter.call_function("rebuild", vec![raw_proof_holder(-1)]), + Ok(Value::ResultFail(Box::new(Value::String( + "refinement type constraint failed for 'models.Positive'".into() + )))) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + + // This repair does not recursively validate arbitrary raw aggregates: + // only the already-existing consuming boundaries force their checks. + let mut untouched = func_def( + "untouched", + vec![("holder", "Holder")], + block(vec![return_stmt(int(0))]), + ); + untouched.return_type = Some(type_named("int64")); + interpreter.register_function(&untouched); + assert_eq!( + interpreter.call_function("untouched", vec![raw_proof_holder(-1)]), + Ok(Value::Int64(0)) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); } #[test] - fn bound_type_reflection_reports_missing_checked_type_info() { - let mut metadata = ReflectionMetadata::new(); - metadata.bind_type_name("Box", jett_types::TypeInterner::STRING); - - let mut interp = Interpreter::new(); - interp.register_struct(&struct_def("Box", vec![("value", "string")], vec![])); - interp.set_reflection_metadata(Arc::new(metadata)); + fn checked_entry_reuses_body_proofs_but_forces_inputs_and_restores_raw_mode() { + let (mut interpreter, field, returned) = raw_proof_interpreter(); + let called = Expr::Call( + Box::new(var("echo")), + vec![named_arg("input", field)], + Span::new(FileId::new(0), 505, 506), + ); + reuse_facts( + &mut interpreter, + &[ + (Span::new(FileId::new(0), 502, 503), "models.Positive"), + (returned.span(), "models.Positive"), + (Span::new(FileId::new(0), 506, 507), "models.Positive"), + (Span::new(FileId::new(0), 507, 508), "models.Positive"), + (called.span(), "models.Positive"), + ], + ); + let mut checked = func_def( + "checked", + vec![("holder", "Holder")], + block(vec![ + typed_var_decl("models.Positive", "selected", called), + return_stmt(Expr::Coarsen(Box::new(var("selected")), sp())), + ]), + ); + checked.return_type = Some(type_named("int64")); + interpreter.register_function(&checked); + assert_eq!( + interpreter.call_checked_function_in_namespace( + None, + "checked", + vec![raw_proof_holder(7)] + ), + Ok(Value::Int64(7)) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + interpreter.call_function("checked", vec![raw_proof_holder(7)]), + Ok(Value::Int64(7)) + ); + assert_eq!(reuse_markers(&mut interpreter), vec!["trace positive"; 3]); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!( + interpreter.call_checked_function_in_namespace(None, "echo", vec![Value::Int64(-1)]), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); - let ty = type_named("Box"); - let info_error = interp - .call_builtin_with_type_args("type.info", std::slice::from_ref(&ty), &[]) - .expect("type.info should be a typed builtin") - .expect_err("bound missing checked type info should be an error"); - let kind_error = interp - .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) - .expect("type.kind should be a typed builtin") - .expect_err("bound missing checked type info should be an error"); + // Compiler-owned facade forwarding inherits source trust while keeping + // its own destination inputs forced. + assert_eq!( + interpreter.call_user_function_with_type_args( + "checked", + &[], + vec![raw_proof_holder(7)] + ), + Ok(Value::Int64(7)) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + // A trusted bridge cannot re-enable proofs inside a raw invocation tree. + interpreter.allow_checked_refinement_proofs = false; assert_eq!( - info_error, - "checked reflection metadata for type 'Box' is missing type info metadata" + interpreter.call_checked_function_in_namespace( + None, + "checked", + vec![raw_proof_holder(-1)] + ), + Err("refinement type constraint failed for 'models.Positive'".into()) ); - assert_eq!(kind_error, info_error); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(!interpreter.allow_checked_refinement_proofs); + assert_eq!( + interpreter.call_user_function_with_type_args( + "checked", + &[], + vec![raw_proof_holder(-1)] + ), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(!interpreter.allow_checked_refinement_proofs); + interpreter.allow_checked_refinement_proofs = true; + assert_eq!( + interpreter.call_checked_function_in_namespace( + None, + "checked", + vec![raw_proof_holder(7)] + ), + Ok(Value::Int64(7)) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); } #[test] - fn direct_type_reflection_uses_checked_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Token", - "primitive", - Some("string_type".to_string()), - true, - Vec::new(), + fn forced_refinement_validator_does_not_trust_predicate_body_field_facts() { + let (mut interpreter, field, returned) = raw_proof_interpreter(); + let mut predicate = func_def( + "holder_valid", + vec![("holder", "Holder")], + block(vec![ + typed_var_decl("models.Positive", "selected", raw_proof_echo(field.clone())), + return_stmt(bool_expr(true)), + ]), + ); + predicate.return_type = Some(type_named("bool")); + interpreter.register_function(&predicate); + interpreter.register_type_alias(&type_alias( + "ValidHolder", + "Holder", + Some(call("holder_valid", vec![var("value")])), )); + reuse_facts( + &mut interpreter, + &[ + (field.span(), "models.Positive"), + (returned.span(), "models.Positive"), + (Span::new(FileId::new(0), 506, 507), "models.Positive"), + (Span::new(FileId::new(0), 507, 508), "models.Positive"), + ], + ); + assert_eq!( + interpreter.check_refinement_type("ValidHolder", &raw_proof_holder(-1)), + Err("error evaluating refinement constraint for 'ValidHolder': refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let ty = type_named("Token"); - let name = interp - .call_builtin_with_type_args("type.name", std::slice::from_ref(&ty), &[]) - .expect("type.name should be a typed builtin") - .expect("type.name should evaluate"); - let kind = interp - .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) - .expect("type.kind should be a typed builtin") - .expect("type.kind should evaluate"); - let kind_tag = interp - .call_builtin_with_type_args("type.kind_tag", std::slice::from_ref(&ty), &[]) - .expect("type.kind_tag should be a typed builtin") - .expect("type.kind_tag should evaluate"); - let primitive_tag = interp - .call_builtin_with_type_args("type.primitive_tag", std::slice::from_ref(&ty), &[]) - .expect("type.primitive_tag should be a typed builtin") - .expect("type.primitive_tag should evaluate"); - let has_secret = interp - .call_builtin_with_type_args("type.has_secret", &[ty], &[]) - .expect("type.has_secret should be a typed builtin") - .expect("type.has_secret should evaluate"); + #[test] + fn checked_call_reuses_named_and_named_value_boundaries_but_raw_calls_still_validate() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 100); + let mut echo = func_def( + "echo", + vec![("input", "models.Positive")], + block(vec![return_stmt(returned.clone())]), + ); + echo.params[0].view = true; + echo.return_type = Some(type_named("models.Positive")); + interpreter.register_function_in_namespace(Some("models"), &echo); + interpreter + .exec_stmt(&typed_var_decl("models.Positive", "source", int(7))) + .unwrap(); + reuse_markers(&mut interpreter); + let argument = Expr::View( + Box::new(reuse_source("source", 101)), + Span::new(FileId::new(0), 102, 103), + ); + reuse_facts( + &mut interpreter, + &[ + (returned.span(), "models.Positive"), + (argument.span(), "models.Positive"), + ], + ); + assert_eq!( + interpreter + .eval_expr(&call("models.echo", vec![argument.clone()])) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + interpreter.set_variable("callback", Value::NamedFunction("models.echo".into())); + assert_eq!( + interpreter + .eval_expr(&call("callback", vec![argument])) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); - assert_eq!(name, Value::String("Token".to_string())); - assert_eq!(kind, Value::String("primitive".to_string())); assert_eq!( - kind_tag, - Value::Enum { - type_name: "TypeKind".to_string(), - variant: "primitive_type".to_string(), - fields: Vec::new(), - } + interpreter + .call_function("models.echo", vec![Value::Int64(7)]) + .unwrap() + .payload(), + &Value::Int64(7) ); assert_eq!( - primitive_tag, - Value::OptionalSome(Box::new(Value::Enum { - type_name: "TypePrimitive".to_string(), - variant: "string_type".to_string(), - fields: Vec::new(), - })) + reuse_markers(&mut interpreter), + ["trace positive", "trace positive"] ); - assert_eq!(has_secret, Value::Bool(true)); + let stale = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(-1)), + }; + assert_eq!( + interpreter.call_fn_value( + Value::NamedFunction("models.echo".into()), + vec![stale.clone()] + ), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + interpreter.set_variable("stale", stale); + let returned = reuse_source("stale", 104); + let mut producer = func_def( + "producer", + vec![], + block(vec![return_stmt(returned.clone())]), + ); + producer.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&producer); + reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); + assert_eq!( + interpreter.call_function("producer", vec![]), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); } #[test] - fn direct_has_secret_fallback_uses_state_qualified_machine_payload() { - let mut interp = Interpreter::new(); - let token_ty = TypeExpr::Generic(ident("secret"), vec![type_named("string")], sp()); - interp.register_machine(&machine_def( - "TokenLifecycle", - vec![ - ("issued", vec![("token", token_ty)]), - ("revoked", Vec::new()), + fn checked_call_reorders_proofs_with_named_values_and_evaluates_arguments_first() { + let mut interpreter = reuse_interpreter(); + let mut choose = func_def( + "choose", + vec![("first", "models.Positive"), ("second", "models.Large")], + block(vec![return_stmt(var("first"))]), + ); + choose.params.iter_mut().for_each(|param| param.view = true); + choose.return_type = Some(type_named("int64")); + interpreter.register_function(&choose); + interpreter.set_variable("small", Value::Int64(7)); + interpreter.set_variable("establishedLarge", Value::Int64(9)); + let small = reuse_source("small", 110); + let large = reuse_source("establishedLarge", 111); + reuse_facts( + &mut interpreter, + &[ + (small.span(), "models.Positive"), + (large.span(), "models.Large"), ], - vec![("issued", "revoked")], - )); - - let machine_has_secret = interp - .call_builtin_with_type_args("type.has_secret", &[type_named("TokenLifecycle")], &[]) - .expect("type.has_secret should be a typed builtin") - .expect("machine secret reflection should evaluate"); - let issued_ty = TypeExpr::StateQualified( - Box::new(type_named("TokenLifecycle")), - ident("issued"), - sp(), ); - let issued_has_secret = interp - .call_builtin_with_type_args("type.has_secret", &[issued_ty], &[]) - .expect("type.has_secret should be a typed builtin") - .expect("issued state secret reflection should evaluate"); - let revoked_ty = TypeExpr::StateQualified( - Box::new(type_named("TokenLifecycle")), - ident("revoked"), + let reordered = Expr::Call( + Box::new(var("choose")), + vec![named_arg("second", large), named_arg("first", small)], sp(), ); - let revoked_has_secret = interp - .call_builtin_with_type_args("type.has_secret", &[revoked_ty], &[]) - .expect("type.has_secret should be a typed builtin") - .expect("revoked state secret reflection should evaluate"); + assert_eq!(interpreter.eval_expr(&reordered), Ok(Value::Int64(7))); + assert!(reuse_markers(&mut interpreter).is_empty()); - assert_eq!(machine_has_secret, Value::Bool(true)); - assert_eq!(issued_has_secret, Value::Bool(true)); - assert_eq!(revoked_has_secret, Value::Bool(false)); + for name in ["one", "two"] { + let mut producer = func_def( + name, + vec![], + block(vec![ + var_decl(name, int(7)), + Stmt::Trace(TraceStmt { + name: ident(name), + span: sp(), + }), + return_stmt(var(name)), + ]), + ); + producer.return_type = Some(type_named("int64")); + interpreter.register_function(&producer); + } + reuse_facts(&mut interpreter, &[]); + let ordered = Expr::Call( + Box::new(var("choose")), + vec![ + named_arg("second", call("one", vec![])), + named_arg("first", call("two", vec![])), + ], + sp(), + ); + assert_eq!(interpreter.eval_expr(&ordered), Ok(Value::Int64(7))); + assert_eq!( + reuse_markers(&mut interpreter), + [ + "trace one", + "trace two", + "trace positive", + "trace positive", + "trace large" + ] + ); } #[test] - fn direct_reflection_fallback_normalizes_current_namespace_types() { - let mut interp = Interpreter::new(); - interp.register_struct_in_namespace( - Some("models"), - &struct_def("User", vec![("name", "string")], vec![]), + fn checked_call_return_proofs_preserve_ancestor_suffix_and_unvalidated_inputs() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 120); + let mut extend = func_def( + "extend", + vec![("input", "models.Positive")], + block(vec![return_stmt(returned.clone())]), + ); + extend.params[0].view = true; + extend.return_type = Some(type_named("models.Large")); + interpreter.register_function(&extend); + let argument = reuse_source("source", 121); + interpreter.set_variable("source", Value::Int64(7)); + reuse_facts( + &mut interpreter, + &[ + (returned.span(), "models.Positive"), + (argument.span(), "models.Positive"), + ], ); - interp.register_enum_in_namespace( - Some("models"), - &enum_def_with_values("Status", vec![("active", 0)]), + assert_eq!( + interpreter + .eval_expr(&call("extend", vec![argument.clone()])) + .unwrap() + .payload(), + &Value::Int64(7) ); - interp.register_bitfield_in_namespace( - Some("models"), - &bitfield_def( - "Header", - vec![( - "version", - BitfieldFieldKind::Bits { - width: 4, - as_type: None, - }, - )], - false, - ), + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + interpreter.set_variable("source", Value::Int64(2)); + assert_eq!( + interpreter.eval_expr(&call("extend", vec![argument])), + Err("refinement type constraint failed for 'models.Large'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + + let returned = Expr::IntLiteral(2, Span::new(FileId::new(0), 122, 123)); + let mut create = func_def("create", vec![], block(vec![return_stmt(returned.clone())])); + create.return_type = Some(type_named("models.Large")); + interpreter.register_function(&create); + reuse_facts(&mut interpreter, &[(returned.span(), "int64")]); + let caller_scope = Arc::new(CheckedScopedTypes { + bound_type: "models.Large".into(), + expressions: HashMap::from([(returned.span(), "models.Large".into())]), + ..Default::default() + }); + interpreter.active_checked_scope = Some(caller_scope.clone()); + interpreter.current_namespace = Some("app".into()); + assert_eq!( + interpreter.eval_expr(&call("create", vec![])), + Err("refinement type constraint failed for 'models.Large'".into()) ); - interp.current_namespace = Some("models".to_string()); - - let user_ty = type_named("User"); - assert_eq!(interp.type_expr_kind(&user_ty), "struct"); - let user_fields = interp.type_expr_fields(&user_ty); - assert_eq!(user_fields.len(), 1); - assert_eq!(user_fields[0].name, "name"); - - let status_ty = type_named("Status"); - assert_eq!(interp.type_expr_kind(&status_ty), "enum"); - let variants = interp.type_expr_variants(&status_ty); - assert_eq!(variants.len(), 1); - assert_eq!(variants[0].name, "active"); - - let header_ty = type_named("Header"); - assert_eq!(interp.type_expr_kind(&header_ty), "bitfield"); - let bitfield = interp.type_expr_bitfield(&header_ty); - assert_eq!(bitfield.fields.len(), 1); - assert_eq!(bitfield.fields[0].name, "version"); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive", "trace large"] + ); + assert!(Arc::ptr_eq( + interpreter.active_checked_scope.as_ref().unwrap(), + &caller_scope + )); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); + assert_eq!(interpreter.scopes.len(), 1); + assert!(interpreter.type_arg_scopes.is_empty()); } #[test] - fn registry_lookup_preserves_root_only_outside_namespace() { - let mut interp = Interpreter::new(); - interp.register_function(&func_def( - "helper", - vec![], - block(vec![return_stmt(string("root"))]), - )); - + fn checked_call_generic_body_and_caller_scoped_proofs_remain_separate() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 130); + let mut identity = func_def( + "identity", + vec![("input", "T")], + block(vec![return_stmt(returned.clone())]), + ); + identity.name.span = Span::new(FileId::new(0), 131, 132); + identity.type_params = vec![ident("T")]; + identity.params[0].view = true; + identity.return_type = Some(type_named("T")); + interpreter.register_function(&identity); + let generic = Arc::new(CheckedFunctionTypes { + type_arguments: vec!["models.Positive".into()], + expressions: Arc::new(HashMap::from([(returned.span(), "models.Positive".into())])), + ..Default::default() + }); + let argument = reuse_source("source", 132); + interpreter.set_variable("source", Value::Int64(7)); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([ + (argument.span(), "int64".into()), + (returned.span(), "int64".into()), + ]), + functions: HashMap::from([(identity.name.span, vec![generic])]), + ..Default::default() + })); + let caller_scope = Arc::new(CheckedScopedTypes { + bound_type: "models.Positive".into(), + expressions: HashMap::from([(argument.span(), "models.Positive".into())]), + ..Default::default() + }); + interpreter.active_checked_scope = Some(caller_scope.clone()); + let expression = Expr::GenericCall( + Box::new(var("identity")), + vec![type_named("models.Positive")], + vec![named_arg("input", argument)], + sp(), + ); assert_eq!( - interp.registry_name(&interp.functions, "helper").as_deref(), - Some("helper") + interpreter.eval_expr(&expression).unwrap().payload(), + &Value::Int64(7) ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert!(Arc::ptr_eq( + interpreter.active_checked_scope.as_ref().unwrap(), + &caller_scope + )); + assert!(interpreter.active_checked_function.is_none()); + assert!(interpreter.type_arg_scopes.is_empty()); + } - interp.current_namespace = Some("alpha".to_string()); - assert_eq!(interp.registry_name(&interp.functions, "helper"), None); - - interp.register_function_in_namespace( - Some("alpha"), - &func_def( - "helper", - vec![], - block(vec![return_stmt(string("namespace"))]), - ), + #[test] + fn checked_call_nested_returns_cannot_lend_proof_to_the_outer_expression() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("ready", 140); + let mut inner = func_def( + "inner", + vec![], + block(vec![ + typed_var_decl("models.Large", "ready", reuse_handle(int(9), int(9), 141)), + return_stmt(returned.clone()), + ]), + ); + inner.return_type = Some(type_named("models.Large")); + interpreter.register_function(&inner); + let derived = Expr::Binary( + Box::new(Expr::Coarsen( + Box::new(call("inner", vec![])), + Span::new(FileId::new(0), 142, 143), + )), + BinOp::Sub, + Box::new(int(7)), + Span::new(FileId::new(0), 144, 145), + ); + let mut outer = func_def("outer", vec![], block(vec![return_stmt(derived.clone())])); + outer.return_type = Some(type_named("models.Large")); + interpreter.register_function(&outer); + reuse_facts( + &mut interpreter, + &[(returned.span(), "models.Large"), (derived.span(), "int64")], ); assert_eq!( - interp.registry_name(&interp.functions, "helper").as_deref(), - Some("alpha.helper") + interpreter.eval_expr(&call("outer", vec![])), + Err("refinement type constraint failed for 'models.Large'".into()) ); + assert_eq!( + reuse_markers(&mut interpreter), + [ + "trace positive", + "trace large", + "trace positive", + "trace large" + ] + ); + assert_eq!(interpreter.scopes.len(), 1); } #[test] - fn direct_alias_reflection_prefers_registered_alias() { - let mut interp = Interpreter::new(); - interp.register_enum(&enum_def_with_values("json.JsonTree", vec![("null", 0)])); - interp.register_type_alias(&type_alias("RawAlias", "json.JsonTree", None)); + fn checked_call_pipeline_and_pending_values_keep_their_original_proofs() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 150); + let mut echo = func_def( + "echo", + vec![("input", "models.Positive")], + block(vec![return_stmt(returned.clone())]), + ); + echo.params[0].view = true; + echo.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&echo); + let mut choose = func_def( + "choose", + vec![("input", "models.Positive"), ("other", "models.Large")], + block(vec![return_stmt(returned.clone())]), + ); + choose.params.iter_mut().for_each(|param| param.view = true); + choose.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&choose); + interpreter.set_variable("other", Value::Int64(9)); + let source = reuse_source("source", 151); + let extra = reuse_source("other", 152); + let first_step = Span::new(FileId::new(0), 153, 154); + let second_step = Span::new(FileId::new(0), 155, 156); + reuse_facts( + &mut interpreter, + &[ + (source.span(), "models.Positive"), + (extra.span(), "models.Large"), + (returned.span(), "models.Positive"), + (first_step, "models.Positive"), + (second_step, "models.Positive"), + ], + ); + let pipeline = Expr::Pipeline( + Box::new(source), + vec![ + PipelineStep { + function: var("choose"), + extra_args: vec![named_arg("other", extra)], + handle: None, + span: first_step, + }, + PipelineStep { + function: var("echo"), + extra_args: vec![], + handle: None, + span: second_step, + }, + ], + sp(), + ); + for depth in 0..=2 { + let mut input = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(7)), + }; + for _ in 0..depth { + input = Value::Pending(Box::new(input)); + } + interpreter.set_variable("source", input.clone()); + assert_eq!(interpreter.eval_expr(&pipeline), Ok(input.clone())); + assert_eq!(interpreter.get_variable("source"), Some(&input)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + } - let ty = type_named("RawAlias"); - let kind = interp - .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) - .expect("type.kind should be a typed builtin") - .expect("type.kind should evaluate"); - let kind_tag = interp - .call_builtin_with_type_args("type.kind_tag", std::slice::from_ref(&ty), &[]) - .expect("type.kind_tag should be a typed builtin") - .expect("type.kind_tag should evaluate"); - let primitive_tag = interp - .call_builtin_with_type_args("type.primitive_tag", std::slice::from_ref(&ty), &[]) - .expect("type.primitive_tag should be a typed builtin") - .expect("type.primitive_tag should evaluate"); - let info = interp - .call_builtin_with_type_args("type.info", &[ty], &[]) - .expect("type.info should be a typed builtin") - .expect("type.info should evaluate"); + #[test] + fn checked_call_handle_returns_carry_the_selected_return_witness() { + for returns_from_handler in [false, true] { + let mut interpreter = reuse_interpreter(); + let copied = Expr::Clone( + Box::new(reuse_source("input", 160)), + Span::new(FileId::new(0), 161, 162), + ); + let handler_statement = if returns_from_handler { + return_stmt(copied.clone()) + } else { + Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(copied.clone()), sp()), + span: sp(), + }) + }; + let handled = Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![handler_statement]), + Span::new(FileId::new(0), 163, 164), + ); + let mut select = func_def( + "select", + vec![("input", "models.Positive")], + block(vec![return_stmt(handled.clone())]), + ); + select.params[0].view = true; + select.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&select); + let source = reuse_source("source", 165); + interpreter.set_variable("source", Value::Int64(7)); + reuse_facts( + &mut interpreter, + &[ + (source.span(), "models.Positive"), + (copied.span(), "models.Positive"), + // A Return escaping the handler supplies its own source fact; + // the unreachable outer Return expression must not replace it. + ( + handled.span(), + if returns_from_handler { + "int64" + } else { + "models.Positive" + }, + ), + ], + ); + assert_eq!( + interpreter + .eval_expr(&call("select", vec![source])) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.scopes.len(), 1); + } + } - assert_eq!(kind, Value::String("alias".to_string())); + #[test] + fn canonical_type_bindings_follow_chains_without_redirecting_terminal_owners() { + let mut interpreter = reuse_interpreter(); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter.type_arg_scopes = vec![HashMap::from([ + ("T".into(), type_named("models.Positive")), + ("Field".into(), type_named("T")), + ])]; + for name in ["T", "Field"] { + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr(&type_named(name))), + "models.Positive" + ); + } assert_eq!( - kind_tag, - Value::Enum { - type_name: "TypeKind".to_string(), - variant: "alias_type".to_string(), - fields: Vec::new(), - } + type_expr_display(&interpreter.substitute_type_expr(&type_named("models.Positive"))), + "shadow.Positive" ); - assert_eq!(primitive_tag, Value::OptionalNone); + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr(&type_named("selected.Positive"))), + "models.Positive" + ); + let template = TypeExpr::Generic( + ident("map"), + vec![type_named("T"), type_named("selected.Positive")], + sp(), + ); + assert_eq!( + interpreter.concrete_type_display(&template), + "map[models.Positive, models.Positive]" + ); + let actual = HashMap::from([("Item".into(), type_named("models.Positive"))]); + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr_with_map_in_namespace( + &type_named("Item"), + &actual, + Some("app") + )), + "models.Positive" + ); + interpreter.type_arg_scopes[0].insert("Cycle".into(), type_named("Cycle")); + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr(&type_named("Cycle"))), + "Cycle" + ); + interpreter.register_struct(&struct_def("T", vec![("value", "int64")], vec![])); + interpreter.type_arg_scopes[0].insert("T".into(), type_named("bool")); + interpreter.type_arg_scopes[0].insert("Actual".into(), type_named("T")); + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr(&type_named("Actual"))), + "T" + ); + assert_eq!( + type_expr_display(&interpreter.resolve_installed_type_expr(&type_named("T"))), + "T" + ); + assert_eq!( + type_expr_display(&interpreter.substitute_type_expr(&type_named("T"))), + "bool" + ); + assert_eq!(interpreter.visible_namespace_aliases()["models"], "shadow"); + } - let Value::Struct { fields, .. } = info else { - panic!("expected TypeInfo struct"); - }; - let field = |field_name: &str| { - fields - .iter() - .find_map(|(name, value)| (name == field_name).then_some(value)) - .expect("TypeInfo field should exist") - }; - assert_eq!(field("kind"), &Value::String("alias".to_string())); - assert_eq!(field("primitive_tag"), &Value::OptionalNone); + #[test] + fn canonical_type_arguments_select_callee_without_caller_parameter_or_alias_rebinding() { + let mut interpreter = reuse_interpreter(); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter.type_arg_scopes = vec![HashMap::from([("T".into(), type_named("bool"))])]; + let returned = reuse_source("input", 710); + let mut identity = func_def( + "identity", + vec![("input", "T")], + block(vec![return_stmt(returned.clone())]), + ); + identity.name.span = Span::new(FileId::new(0), 711, 712); + identity.type_params = vec![ident("T")]; + identity.return_type = Some(type_named("T")); + interpreter.register_function(&identity); + let body = Arc::new(CheckedFunctionTypes { + type_arguments: vec!["models.Positive".into()], + expressions: Arc::new(HashMap::from([(returned.span(), "models.Positive".into())])), + ..Default::default() + }); + let argument = reuse_source("source", 712); + interpreter.set_variable_with_type( + "source", + Value::Int64(7), + type_named("models.Positive"), + ); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(argument.span(), "models.Positive".into())]), + functions: HashMap::from([(identity.name.span, vec![body])]), + ..Default::default() + })); + let invocation = Expr::GenericCall( + Box::new(var("identity")), + vec![type_named("selected.Positive")], + vec![named_arg("input", argument)], + sp(), + ); + let result = interpreter.eval_expr(&invocation).unwrap(); + assert!( + matches!(result, Value::Typed { ref type_name, ref value } if type_name == "models.Positive" && value.payload() == &Value::Int64(7)) + ); + assert_eq!( + type_expr_display(&interpreter.debug_expression_type(&invocation).unwrap()), + "models.Positive" + ); + interpreter.set_variable_with_type( + "sources", + Value::List(vec![Value::Int64(7)]), + TypeExpr::Generic(ident("list"), vec![type_named("models.Positive")], sp()), + ); + let inferred = call("identity", vec![var("sources")]); + assert_eq!( + type_expr_display(&interpreter.debug_expression_type(&inferred).unwrap()), + "list[models.Positive]" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + type_expr_display(&interpreter.type_arg_scopes[0]["T"]), + "bool" + ); + assert_eq!(interpreter.visible_namespace_aliases()["models"], "shadow"); + assert!(interpreter.active_checked_function.is_none()); + assert_eq!(interpreter.scopes.len(), 1); + } - let Value::List(args) = field("args") else { - panic!("expected TypeInfo.args list"); - }; - assert_eq!(args.len(), 1); - let Value::Struct { - fields: arg_fields, .. - } = &args[0] + #[test] + fn canonical_closure_capture_preserves_generic_reflected_metadata_and_imports() { + let mut interpreter = reuse_interpreter(); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter.type_arg_scopes = vec![HashMap::from([ + ("T".into(), type_named("models.Positive")), + ("Field".into(), type_named("T")), + ])]; + interpreter.current_type_arguments = vec![type_named("models.Positive")]; + interpreter.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "models.Positive".into(), + reflection: None, + }]; + let returned = reuse_source("input", 720); + reuse_facts(&mut interpreter, &[(returned.span(), "int64")]); + let closure = interpreter + .capture_closure( + &func_def("unused", vec![("input", "int64")], block(vec![])).params, + Some(&type_named("Field")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); + let Value::Function { + ref type_context, + ref namespace_aliases, + .. + } = closure else { - panic!("expected nested TypeInfo"); - }; - let arg_field = |field_name: &str| { - arg_fields - .iter() - .find_map(|(name, value)| (name == field_name).then_some(value)) - .expect("nested TypeInfo field should exist") + panic!("expected closure") }; assert_eq!( - arg_field("type_name"), - &Value::String("json.JsonTree".to_string()) + type_expr_display(&type_context.bindings["T"]), + "models.Positive" ); - assert_eq!(arg_field("kind"), &Value::String("enum".to_string())); + assert_eq!( + type_expr_display(&type_context.bindings["Field"]), + "models.Positive" + ); + assert_eq!(type_context.arguments[0].canonical_name, "models.Positive"); + assert_eq!( + type_context.scoped_type_bindings[0].canonical_name, + "models.Positive" + ); + assert_eq!(namespace_aliases["models"], "shadow"); + for input in [7, -1] { + let result = interpreter.call_fn_value_from_source( + closure.clone(), + vec![Value::Int64(input)], + Some(&[None]), + ); + if input > 0 { + assert!( + matches!(result, Ok(Value::Typed { ref type_name, ref value }) if type_name == "models.Positive" && value.payload() == &Value::Int64(7)) + ); + } else { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.visible_namespace_aliases()["models"], "shadow"); + assert_eq!( + interpreter.scoped_type_bindings[0].canonical_name, + "models.Positive" + ); + assert_eq!(interpreter.scopes.len(), 1); + } } #[test] - fn unknown_direct_type_has_no_alias_reflection() { - let mut interp = Interpreter::new(); - let ty = type_named("UnknownRaw"); - - let kind = interp - .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) - .expect("type.kind should be a typed builtin") - .expect("type.kind should evaluate"); - let primitive_tag = interp - .call_builtin_with_type_args("type.primitive_tag", &[ty], &[]) - .expect("type.primitive_tag should be a typed builtin") - .expect("type.primitive_tag should evaluate"); - - assert_eq!(kind, Value::String("named".to_string())); - assert_eq!(primitive_tag, Value::OptionalNone); + fn canonical_locals_and_checked_expressions_keep_owner_and_primitive_width() { + let mut interpreter = reuse_interpreter(); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter + .register_type_alias_in_namespace(Some("models"), &type_alias("Small", "int8", None)); + interpreter + .register_type_alias_in_namespace(Some("shadow"), &type_alias("Small", "uint64", None)); + let source = reuse_source("source", 730); + interpreter.set_variable_with_type( + "source", + Value::Int64(7), + type_named("models.Positive"), + ); + reuse_facts(&mut interpreter, &[(source.span(), "models.Positive")]); + interpreter + .exec_stmt(&typed_var_decl( + "selected.Positive", + "copied", + source.clone(), + )) + .unwrap(); + assert!( + matches!(interpreter.get_variable("copied"), Some(Value::Typed { type_name, value }) if type_name == "models.Positive" && value.payload() == &Value::Int64(7)) + ); + assert_eq!( + type_expr_display(interpreter.get_variable_type("copied").unwrap()), + "models.Positive" + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + let base = Expr::IntLiteral(-1, Span::new(FileId::new(0), 731, 732)); + reuse_facts(&mut interpreter, &[(base.span(), "int64")]); + assert_eq!( + interpreter.exec_stmt(&typed_var_decl("selected.Positive", "rejected", base)), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.get_variable("rejected").is_none()); + let field = Expr::IntLiteral(127, Span::new(FileId::new(0), 732, 733)); + reuse_facts(&mut interpreter, &[(field.span(), "models.Small")]); + assert_eq!(interpreter.eval_expr(&field), Ok(Value::Int64(127))); + interpreter + .exec_stmt(&typed_var_decl("selected.Small", "small", field)) + .unwrap(); + assert_eq!(interpreter.get_variable("small"), Some(&Value::Int64(127))); + assert_eq!(interpreter.visible_namespace_aliases()["models"], "shadow"); + assert_eq!(interpreter.scopes.len(), 1); } #[test] - fn json_serialize_secret_gate_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "SecretBox", - "struct", - None, - true, - Vec::new(), - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let value = Value::Struct { - type_name: "SecretBox".to_string(), - fields: Vec::new(), - }; - let result = interp - .call_builtin_with_type_args("json.serialize", &[type_named("SecretBox")], &[value]) - .expect("json.serialize should be a typed builtin"); + fn canonical_declaration_alias_bases_ignore_caller_generic_and_namespace_bindings() { + let mut interpreter = Interpreter::new(); + interpreter.register_struct_in_namespace( + Some("models"), + &struct_def("T", vec![("value", "int64")], vec![]), + ); + interpreter + .register_type_alias_in_namespace(Some("models"), &type_alias("Wrapped", "T", None)); + interpreter + .register_type_alias_in_namespace(Some("shadow"), &type_alias("Wrapped", "bool", None)); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.type_arg_scopes = vec![HashMap::from([("T".into(), type_named("bool"))])]; + assert_eq!( + interpreter.concrete_type_display(&type_named("selected.Wrapped")), + "models.T" + ); + assert_eq!( + interpreter.resolved_concrete_type_display(&type_named("models.Wrapped")), + "models.T" + ); + assert_eq!( + interpreter.concrete_type_display(&type_named("models.Wrapped")), + "bool" + ); + assert!( + interpreter + .merge_inferred_never(&type_named("models.Wrapped"), &type_named("bool"), false) + .is_none() + ); + assert_eq!( + interpreter.with_resolved_type_aliases(|interpreter| interpreter + .primitive_base_type_name("models.Wrapped") + .into_owned()), + "models.T" + ); + assert_eq!( + type_expr_display(&interpreter.type_arg_scopes[0]["T"]), + "bool" + ); + } + #[test] + fn canonical_source_locals_closures_and_reflected_bindings_work_in_closed_comptime() { + let source = r#"namespace models +export type Positive = int64 where value > 0 +export type Alias = Positive +export function identity[T](value: T) returns T: + return value +export function make[T]() returns function(int64) returns T: + return function(value: int64) returns T: return value +namespace shadow +export type Positive = int64 where value < 0 +export type Alias = Positive +namespace app +function report() returns string: + use models as selected + use shadow as models + selected.Positive first = 7 handle error: + return error + selected.Alias copied = first + selected.Positive generic = selected.identity[selected.Positive](clone first) + function(int64) returns selected.Positive callback = selected.make[selected.Positive]() + selected.Positive returned = callback(9) + comptime type Bound = type.info[selected.Alias](): + function(int64) returns Bound reflected = function(value: int64) returns Bound: return value + Bound scoped = reflected(11) + return "{coarsen first}:{coarsen copied}:{coarsen generic}:{coarsen returned}:{coarsen scoped}:{type.name[Bound]()}" +function main() returns string: + return comptime report() +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); assert_eq!( - result, - Err("json.serialize cannot serialize secret-containing type 'SecretBox'".to_string()) + interpreter.call_function("app.main", vec![]), + Ok(Value::String("7:7:7:9:11:models.Alias".into())) ); + assert_eq!(interpreter.scopes.len(), 1); + assert_eq!(interpreter.lexical_scope_floor, 0); + assert!(interpreter.type_arg_scopes.is_empty()); + assert!(interpreter.scoped_type_bindings.is_empty()); + assert!(interpreter.visible_namespace_aliases().is_empty()); } #[test] - fn type_arg_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "list[int64]", - "list", - None, - false, - vec![ReflectionTypeInfo::new( - "int64", - "primitive", - Some("int64_type".to_string()), - true, - Vec::new(), - )], - )); + fn canonical_reflected_field_and_argument_loop_bindings_keep_selected_owners() { + let source = r#"namespace models +export type Positive = int64 where value > 0 +export struct Scalar: + value: Positive +export struct Items: + values: list[Positive] +namespace shadow +export type Positive = int64 where value < 0 +namespace app +function field_name() returns string: + use models as selected + use shadow as models + mutable string found = "missing" + for field in type.fields[selected.Scalar](): + comptime type Field = field.type_info: + found = type.name[Field]() + return found +function direct_argument() returns string: + use models as selected + use shadow as models + mutable string found = "missing" + for arg in type.info[list[selected.Positive]]().args: + comptime type Arg = arg: + found = type.name[Arg]() + return found +function field_argument() returns string: + use models as selected + use shadow as models + mutable string found = "missing" + for field in type.fields[selected.Items](): + for arg in field.type_info.args: + comptime type Arg = arg: + found = type.name[Arg]() + return found +function nested_argument() returns string: + use models as selected + use shadow as models + mutable string found = "missing" + for outer in type.info[list[optional[selected.Positive]]]().args: + comptime type Outer = outer: + for inner in outer.args: + comptime type Inner = inner: + found = type.name[Inner]() + return found +function main() returns string: + return comptime "{field_name()}|{direct_argument()}|{field_argument()}|{nested_argument()}" +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + assert_eq!( + interpreter.call_function("app.main", vec![]), + Ok(Value::String( + "models.Positive|models.Positive|models.Positive|models.Positive".into() + )) + ); + assert_eq!(interpreter.scopes.len(), 1); + assert!(interpreter.type_arg_scopes.is_empty()); + assert!(interpreter.scoped_type_bindings.is_empty()); + assert!(interpreter.reflected_field_scopes.is_empty()); + assert!(interpreter.reflected_type_info_scopes.is_empty()); + assert!(interpreter.visible_namespace_aliases().is_empty()); + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + #[test] + fn call_view_refinement_parameters_validate_without_changing_binding_metadata() { + for inline in [false, true] { + for depth in 1..=2 { + let mut interpreter = reuse_interpreter(); + let mut parameter_type = type_named("models.Positive"); + for _ in 0..depth { + parameter_type = TypeExpr::View(Box::new(parameter_type), sp()); + } + let mut function = func_def( + "capture", + vec![("input", "models.Positive")], + block(vec![return_stmt(Expr::InlineFn( + vec![], + None, + block(vec![return_stmt(var("input"))]), + sp(), + ))]), + ); + function.params[0].ty = parameter_type.clone(); + interpreter.register_function(&function); + let closure = interpreter + .capture_closure(&function.params, None, &function.body) + .unwrap(); + let wrapped_fact = type_expr_display(¶meter_type); + for (number, checked) in [(-1, false), (7, false), (7, true)] { + let input = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(number)), + }; + let facts = [Some(wrapped_fact.clone())]; + let result = if inline { + interpreter.call_fn_value_from_source( + closure.clone(), + vec![input], + checked.then_some(facts.as_slice()), + ) + } else { + interpreter.call_registered_function_from_source( + "capture", + &[], + vec![input], + checked.then_some(facts.as_slice()), + ) + }; + if number < 0 { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + } else { + let Value::Function { + capture_types, + captures, + .. + } = result.unwrap() + else { + panic!("capture body did not return a closure"); + }; + assert_eq!(type_expr_display(&capture_types["input"]), wrapped_fact); + assert_eq!(captures["input"].payload(), &Value::Int64(number)); + } + assert_eq!( + reuse_markers(&mut interpreter), + if checked { + vec![] + } else { + vec!["trace positive"] + } + ); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } + } + } + } - let list_int = TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()); - let value = interp - .call_builtin_with_type_args("type.arg", &[list_int], &[Value::Int64(0)]) - .expect("type.arg should be a typed builtin") - .expect("type.arg should evaluate"); + #[test] + fn call_canonical_refinement_targets_keep_written_alias_resolution_and_restore_imports() { + let mut interpreter = reuse_interpreter(); + interpreter + .register_type_alias_in_namespace(Some("models"), &type_alias("Small", "int8", None)); + interpreter + .register_type_alias_in_namespace(Some("shadow"), &type_alias("Small", "uint64", None)); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias( + "Positive", + "int64", + Some(binary(var("value"), BinOp::Lt, int(0))), + ), + ); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + let original_aliases = interpreter.visible_namespace_aliases(); + let positive = interpreter + .capture_closure( + &func_def("unused", vec![("input", "int64")], block(vec![])).params, + Some(&type_named("selected.Positive")), + &block(vec![return_stmt(var("input"))]), + ) + .unwrap(); + let small = interpreter + .capture_closure( + &[], + Some(&type_named("selected.Small")), + &block(vec![return_stmt(int(42))]), + ) + .unwrap(); + for input in [7, -1] { + let result = interpreter.call_fn_value_from_source( + positive.clone(), + vec![Value::Int64(input)], + Some(&[None]), + ); + if input > 0 { + assert!(matches!( + result, + Ok(Value::Typed { ref type_name, ref value }) + if type_name == "models.Positive" && value.payload() == &Value::Int64(7) + )); + } else { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.visible_namespace_aliases(), original_aliases); + assert_eq!(interpreter.scopes.len(), 1); + } + assert_eq!( + interpreter.call_fn_value_from_source(small, vec![], Some(&[])), + Ok(Value::Int64(42)) + ); + assert_eq!(interpreter.visible_namespace_aliases(), original_aliases); + let overflow = interpreter + .capture_closure( + &[], + Some(&type_named("selected.Small")), + &block(vec![return_stmt(int(128))]), + ) + .unwrap(); + assert!( + interpreter + .call_fn_value_from_source(overflow, vec![], Some(&[])) + .unwrap_err() + .contains("int8 value 128 is outside range") + ); + assert_eq!(interpreter.visible_namespace_aliases(), original_aliases); + assert_eq!(interpreter.scopes.len(), 1); + } - let Value::Struct { fields, .. } = value else { - panic!("expected TypeInfo struct"); - }; - let has_secret = fields - .iter() - .find_map(|(name, value)| { - if name == "has_secret" { - Some(value) + #[test] + fn call_canonical_refinement_normalization_keeps_bound_generic_and_reflected_owners() { + let mut interpreter = reuse_interpreter(); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.type_arg_scopes = vec![HashMap::from([ + ("T".into(), type_named("models.Positive")), + ("Field".into(), type_named("models.Positive")), + ])]; + interpreter.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "models.Positive".into(), + reflection: None, + }]; + let aliases = interpreter.visible_namespace_aliases(); + for binding in ["T", "Field"] { + for input in [7, -1] { + let result = interpreter + .normalize_and_validate_value(&type_named(binding), Value::Int64(input)); + if input > 0 { + assert!(matches!( + result, + Ok(Value::Typed { ref type_name, ref value }) + if type_name == "models.Positive" && value.payload() == &Value::Int64(7) + )); } else { - None + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()) + ); } - }) - .expect("TypeInfo.has_secret field should exist"); - assert_eq!(has_secret, &Value::Bool(true)); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.visible_namespace_aliases(), aliases); + assert_eq!( + type_expr_display(&interpreter.type_arg_scopes[0][binding]), + "models.Positive" + ); + assert_eq!( + interpreter.scoped_type_bindings[0].canonical_name, + "models.Positive" + ); + } + } } #[test] - fn type_info_arg_loop_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "list[int64]", - "list", - None, - false, - vec![ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - )], - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let list_int = TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()); - let type_info_call = Expr::GenericCall( - Box::new(field_access(var("type"), "info")), - vec![list_int], - Vec::new(), - sp(), - ); - let args_iterable = field_access(type_info_call, "args"); - let bindings = interp - .reflected_type_info_arg_loop_bindings(&args_iterable) - .expect("type.info args loop should be recognized") - .expect("type.info args loop should produce bindings"); - - assert_eq!(bindings.len(), 1); - assert_eq!(type_expr_display(&bindings[0].ty), "string"); + fn inline_refinement_new_return_checks_before_checked_consumers() { + for route in ["direct", "local", "argument", "return"] { + let mut interpreter = reuse_interpreter(); + let returned = Expr::IntLiteral(-1, Span::new(FileId::new(0), 610, 611)); + let inline = Expr::InlineFn( + vec![], + Some(type_named("models.Positive")), + block(vec![return_stmt(returned.clone())]), + Span::new(FileId::new(0), 611, 612), + ); + let invocation = Expr::Call( + Box::new(inline), + vec![], + Span::new(FileId::new(0), 612, 613), + ); + let echoed = reuse_source("input", 613); + let mut echo = func_def( + "echo", + vec![("input", "models.Positive")], + block(vec![return_stmt(echoed.clone())]), + ); + echo.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&echo); + let mut relay = func_def( + "relay", + vec![], + block(vec![return_stmt(invocation.clone())]), + ); + relay.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&relay); + reuse_facts( + &mut interpreter, + &[ + (returned.span(), "int64"), + (invocation.span(), "models.Positive"), + (echoed.span(), "models.Positive"), + ], + ); + let error = match route { + "local" => interpreter + .exec_stmt(&typed_var_decl("models.Positive", "ready", invocation)) + .unwrap_err(), + "argument" => interpreter + .eval_expr(&call("echo", vec![invocation])) + .unwrap_err(), + "return" => interpreter.eval_expr(&call("relay", vec![])).unwrap_err(), + _ => interpreter.eval_expr(&invocation).unwrap_err(), + }; + assert_eq!( + error, "refinement type constraint failed for 'models.Positive'", + "{route}" + ); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive"], + "{route}" + ); + assert!(interpreter.get_variable("ready").is_none(), "{route}"); + assert!(interpreter.allow_checked_refinement_proofs, "{route}"); + assert_eq!(interpreter.scopes.len(), 1, "{route}"); + } } #[test] - fn comptime_type_arg_binding_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "list[int64]", - "list", - None, - false, - vec![ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), + fn inline_refinement_parameters_force_unproven_values_and_reuse_exact_facts() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 620); + let params = func_def("unused", vec![("input", "models.Positive")], block(vec![])).params; + reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named("models.Positive")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); + for (input, facts, expected_markers, succeeds) in [ + (7, Some(vec![Some("models.Positive".into())]), vec![], true), + ( + 7, + Some(vec![Some("int64".into())]), + vec!["trace positive"], + true, + ), + ( + -1, + Some(vec![Some("int64".into())]), + vec!["trace positive"], false, - Vec::new(), - )], - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - interp.set_variable("bound_name", Value::String("unset".to_string())); - - let stmt = Stmt::ComptimeTypeBind(ComptimeTypeBindStmt { - name: ident("Item"), - value: Expr::GenericCall( - Box::new(field_access(var("type"), "arg")), - vec![TypeExpr::Generic( - ident("list"), - vec![type_named("int64")], - sp(), - )], - vec![CallArg { - name: None, - value: Expr::IntLiteral(0, sp()), - span: sp(), - }], - sp(), ), - body: block(vec![assign( - "bound_name", - Expr::GenericCall( - Box::new(field_access(var("type"), "name")), - vec![type_named("Item")], - Vec::new(), - sp(), - ), - )]), - span: sp(), - }); + (-1, None, vec!["trace positive"], false), + ] { + let value = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(input)), + }; + let result = interpreter.call_fn_value_from_source( + closure.clone(), + vec![value], + facts.as_deref(), + ); + if succeeds { + assert_eq!(result.unwrap().payload(), &Value::Int64(input)); + } else { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + } + assert_eq!(reuse_markers(&mut interpreter), expected_markers); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + } + } - interp.exec_stmt(&stmt).expect("binding should execute"); - assert_eq!( - interp - .get_variable("bound_name") - .expect("bound_name should be set by the comptime type body"), - &Value::String("string".to_string()) - ); + #[test] + fn inline_refinement_returns_check_only_the_unproven_ancestor_suffix() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("input", 630); + let params = func_def("unused", vec![("input", "models.Positive")], block(vec![])).params; + reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named("models.Large")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); + let facts = [Some("models.Positive".into())]; + for input in [7, 2] { + let result = interpreter.call_fn_value_from_source( + closure.clone(), + vec![Value::Int64(input)], + Some(&facts), + ); + if input == 7 { + assert!(matches!( + result, + Ok(Value::Typed { ref type_name, ref value }) + if type_name == "models.Large" && value.payload() == &Value::Int64(7) + )); + } else { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Large'".into()) + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + assert_eq!(interpreter.scopes.len(), 1); + } } #[test] - fn type_fields_uses_checked_reflection_metadata_when_available() { - let value_info = ReflectionTypeInfo::new( - "secret[string]", - "secret", - None, - true, - vec![ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - )], - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_fields( - "Box", - vec![ReflectionFieldInfo::new( - 0, - "value", - "secret[string]", - "secret", - "jsonValue", - true, - value_info, - )], - ); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let value = interp - .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - - let Value::List(fields) = value else { - panic!("expected list of TypeField values"); - }; - assert_eq!(fields.len(), 1); - let Value::Struct { fields, .. } = &fields[0] else { - panic!("expected TypeField struct"); - }; - let serialize_name = fields - .iter() - .find_map(|(name, value)| { - if name == "serialize_name" { - Some(value) - } else { - None - } - }) - .expect("TypeField.serialize_name should exist"); - let has_secret = fields - .iter() - .find_map(|(name, value)| { - if name == "has_secret" { - Some(value) - } else { - None - } - }) - .expect("TypeField.has_secret should exist"); - assert_eq!(serialize_name, &Value::String("jsonValue".to_string())); - assert_eq!(has_secret, &Value::Bool(true)); + fn inline_refinement_return_context_restores_generic_reflected_and_alias_scopes() { + let mut interpreter = reuse_interpreter(); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Large", "int64", Some(bool_expr(false))), + ); + interpreter.current_namespace = Some("models".into()); + interpreter.set_namespace_alias("selected".into(), "models".into()); + interpreter.type_arg_scopes = vec![HashMap::from([ + ("T".into(), type_named("models.Large")), + ("Field".into(), type_named("T")), + ])]; + interpreter.current_type_arguments = vec![type_named("models.Large")]; + interpreter.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "models.Large".into(), + reflection: None, + }]; + let returned = Expr::IntLiteral(7, Span::new(FileId::new(0), 640, 641)); + let captured_function = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([(returned.span(), "int64".into())])), + ..Default::default() + }); + let captured_scope = Arc::new(CheckedScopedTypes { + bound_type: "models.Large".into(), + expressions: HashMap::from([(returned.span(), "int64".into())]), + ..Default::default() + }); + interpreter.active_checked_function = Some(captured_function); + interpreter.active_checked_scope = Some(captured_scope); + let successful = interpreter + .capture_closure( + &[], + Some(&type_named("Field")), + &block(vec![return_stmt(returned.clone())]), + ) + .unwrap(); + let failed = interpreter + .capture_closure( + &[], + Some(&type_named("Field")), + &block(vec![return_stmt(Expr::IntLiteral(2, returned.span()))]), + ) + .unwrap(); + let caller_function = Arc::new(CheckedFunctionTypes::default()); + let caller_scope = Arc::new(CheckedScopedTypes::default()); + interpreter.active_checked_function = Some(caller_function.clone()); + interpreter.active_checked_scope = Some(caller_scope.clone()); + interpreter.current_namespace = Some("app".into()); + interpreter.set_namespace_alias("selected".into(), "shadow".into()); + interpreter.type_arg_scopes = vec![HashMap::from([ + ("T".into(), type_named("bool")), + ("Field".into(), type_named("string")), + ])]; + interpreter.current_type_arguments = vec![type_named("bool")]; + interpreter.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "string".into(), + reflection: None, + }]; + interpreter.current_function_trusted_stdlib = true; + for (closure, succeeds) in [(successful, true), (failed, false)] { + let result = interpreter.call_fn_value_from_source(closure, vec![], Some(&[])); + if succeeds { + assert_eq!(result.unwrap().payload(), &Value::Int64(7)); + } else { + assert_eq!( + result, + Err("refinement type constraint failed for 'models.Large'".into()) + ); + } + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive", "trace large"] + ); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); + assert_eq!( + interpreter.visible_namespace_aliases()["selected"], + "shadow" + ); + assert_eq!( + type_expr_display(&interpreter.type_arg_scopes[0]["T"]), + "bool" + ); + assert_eq!( + type_expr_display(&interpreter.type_arg_scopes[0]["Field"]), + "string" + ); + assert_eq!( + type_expr_display(&interpreter.current_type_arguments[0]), + "bool" + ); + assert_eq!(interpreter.scoped_type_bindings[0].canonical_name, "string"); + assert!(Arc::ptr_eq( + interpreter.active_checked_function.as_ref().unwrap(), + &caller_function + )); + assert!(Arc::ptr_eq( + interpreter.active_checked_scope.as_ref().unwrap(), + &caller_scope + )); + assert!(interpreter.current_function_trusted_stdlib); + assert!(interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.scopes.len(), 1); + assert_eq!(interpreter.lexical_scope_floor, 0); + } } #[test] - fn type_fields_reports_missing_checked_struct_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); + fn inline_refinement_exact_owned_and_pending_returns_preserve_values() { + for (owner, base, input) in [ + ( + "Text", + type_named("string"), + Value::String("Agent-λ🙂".into()), + ), + ( + "Items", + TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()), + Value::List(vec![Value::Int64(7), Value::Int64(9)]), + ), + ] { + let mut interpreter = Interpreter::new(); + let mut predicate = func_def( + "accept", + vec![("observed", "int64")], + block(vec![ + Stmt::Trace(TraceStmt { + name: ident("observed"), + span: sp(), + }), + return_stmt(bool_expr(true)), + ]), + ); + predicate.params[0].ty = base.clone(); + predicate.params[0].view = true; + predicate.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("models"), &predicate); + let mut alias = type_alias(owner, "int64", Some(call("accept", vec![var("value")]))); + alias.base_type = base; + interpreter.register_type_alias_in_namespace(Some("models"), &alias); + let canonical = format!("models.{owner}"); + let returned = reuse_source("input", 650); + let mut params = func_def("unused", vec![("input", &canonical)], block(vec![])).params; + params[0].view = true; + reuse_facts(&mut interpreter, &[(returned.span(), &canonical)]); + let closure = interpreter + .capture_closure( + ¶ms, + Some(&type_named(&canonical)), + &block(vec![return_stmt(Expr::Clone( + Box::new(returned), + Span::new(FileId::new(0), 651, 652), + ))]), + ) + .unwrap(); + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 651, 652), &canonical)], + ); + for depth in 0..=2 { + let mut value = Value::Typed { + type_name: canonical.clone(), + value: Box::new(input.clone()), + }; + for _ in 0..depth { + value = Value::Pending(Box::new(value)); + } + let facts = [Some(canonical.clone())]; + assert_eq!( + interpreter.call_fn_value_from_source( + closure.clone(), + vec![value.clone()], + Some(&facts) + ), + Ok(value) + ); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!(interpreter.scopes.len(), 1); + } + } + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + #[test] + fn inline_refinement_declared_primitive_returns_normalize_without_inventing_annotations() { + let mut interpreter = Interpreter::new(); + for (annotation, expected) in [ + (Some(type_named("uint64")), Value::Uint64(42)), + (None, Value::Int64(42)), + ] { + let expression = + Expr::InlineFn(vec![], annotation, block(vec![return_stmt(int(42))]), sp()); + let closure = interpreter.eval_expr(&expression).unwrap(); + assert_eq!(interpreter.call_fn_value(closure, vec![]), Ok(expected)); + assert_eq!(interpreter.scopes.len(), 1); + } + } - let err = interp - .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) - .expect("type.fields should be a typed builtin") - .expect_err("missing checked fields should be an error"); + #[test] + fn inline_refinement_raw_callbacks_force_return_and_preserve_disabled_mode() { + let mut interpreter = reuse_interpreter(); + let returned = reuse_source("forged", 660); + interpreter.set_variable( + "forged", + Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(-1)), + }, + ); + reuse_facts(&mut interpreter, &[(returned.span(), "models.Positive")]); + let closure = interpreter + .capture_closure( + &[], + Some(&type_named("models.Positive")), + &block(vec![return_stmt(returned)]), + ) + .unwrap(); + for previously_enabled in [true, false] { + interpreter.allow_checked_refinement_proofs = previously_enabled; + assert_eq!( + interpreter.call_fn_value(closure.clone(), vec![]), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!( + interpreter.allow_checked_refinement_proofs, + previously_enabled + ); + assert_eq!(interpreter.scopes.len(), 1); + } + assert_eq!( + interpreter.call_fn_value_from_source(closure, vec![], Some(&[])), + Err("refinement type constraint failed for 'models.Positive'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(!interpreter.allow_checked_refinement_proofs); + } - assert!(err.contains("missing field metadata")); + fn reuse_source(name: &str, offset: u32) -> Expr { + Expr::Ident(Ident { + name: name.into(), + span: Span::new(FileId::new(0), offset, offset + 1), + }) } - #[test] - fn type_field_value_reports_missing_checked_field_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); + fn reuse_facts(interpreter: &mut Interpreter, facts: &[(Span, &str)]) { + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: facts + .iter() + .map(|(span, name)| (*span, (*name).to_owned())) + .collect(), + ..Default::default() + })); + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + fn reuse_interpreter() -> Interpreter { + let mut interpreter = Interpreter::new(); + for (owner, base, marker, minimum) in [ + ("Positive", "int64", "positive", 0), + ("Large", "Positive", "large", 5), + ("Sibling", "int64", "sibling", 0), + ] { + let mut predicate = func_def( + marker, + vec![(marker, "int64")], + block(vec![ + Stmt::Trace(TraceStmt { + name: ident(marker), + span: sp(), + }), + return_stmt(binary(var(marker), BinOp::Gt, int(minimum))), + ]), + ); + predicate.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("models"), &predicate); + interpreter.register_type_alias_in_namespace( + Some("models"), + &type_alias(owner, base, Some(call(marker, vec![var("value")]))), + ); + } + interpreter + } - let field = - Interpreter::reflection_field_info_value("Box", None, &string_field_info(0, "value")); - let value = Value::Struct { - type_name: "Box".to_string(), - fields: vec![("value".to_string(), Value::String("ok".to_string()))], - }; - let err = interp - .call_builtin_with_type_args( - "type.field_value", - &[type_named("Box"), type_named("string")], - &[value, field], - ) - .expect("type.field_value should be a typed builtin") - .expect_err("missing checked field metadata should be an error"); + fn reuse_markers(interpreter: &mut Interpreter) -> Vec { + interpreter + .take_debug_output() + .into_iter() + .map(|line| line.split(':').next().unwrap().to_owned()) + .collect() + } - assert!(err.contains("missing field metadata")); + fn reuse_handle(target: Expr, fallback: Expr, offset: u32) -> Expr { + Expr::Handle( + Box::new(target), + None, + block(vec![Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(fallback), sp()), + span: sp(), + })]), + Span::new(FileId::new(0), offset, offset + 1), + ) } #[test] - fn reflected_field_loop_uses_checked_reflection_metadata_when_available() { - let value_info = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), + fn refinement_reuse_does_not_trust_typed_values_at_forced_boundaries() { + let mut interpreter = reuse_interpreter(); + let stale = Value::Typed { + type_name: "models.Positive".into(), + value: Box::new(Value::Int64(-1)), + }; + let error = "refinement type constraint failed for 'models.Positive'"; + assert_eq!( + interpreter.check_refinement_type("models.Positive", &stale), + Err(error.into()) ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_fields( - "Box", - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - value_info, - )], + interpreter.set_variable("stale", stale.clone()); + assert_eq!( + interpreter.exec_stmt(&typed_var_decl("models.Positive", "copy", var("stale"))), + Err(error.into()) ); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let iterable = Expr::GenericCall( - Box::new(field_access(var("type"), "fields")), - vec![type_named("Box")], - Vec::new(), - sp(), + let mut accept = func_def( + "accept", + vec![("input", "models.Positive")], + block(vec![return_stmt(var("input"))]), ); - let bindings = interp - .reflected_field_loop_bindings(&iterable) - .expect("type.fields loop should not error") - .expect("type.fields loop should be recognized"); - - assert_eq!(bindings.len(), 1); - assert_eq!(bindings[0].index, 0); - assert_eq!(bindings[0].name, "value"); - assert_eq!(type_expr_display(&bindings[0].ty), "string"); + accept.return_type = Some(type_named("int64")); + interpreter.register_function(&accept); + assert_eq!( + interpreter.call_function("accept", vec![stale]), + Err(error.into()) + ); + let mut produce = func_def("produce", vec![], block(vec![return_stmt(var("stale"))])); + produce.return_type = Some(type_named("models.Positive")); + interpreter.register_function(&produce); + assert_eq!( + interpreter.call_function("produce", vec![]), + Err(error.into()) + ); + assert_eq!(reuse_markers(&mut interpreter), vec!["trace positive"; 4]); } #[test] - fn reflected_field_loop_reports_missing_checked_field_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + fn refinement_reuse_locals_skip_only_the_checked_ancestor_prefix() { + let mut interpreter = reuse_interpreter(); + interpreter + .exec_stmt(&typed_var_decl("models.Positive", "source", int(7))) + .unwrap(); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + let source = reuse_source("source", 10); + reuse_facts(&mut interpreter, &[(source.span(), "models.Positive")]); + interpreter + .exec_stmt(&typed_var_decl( + "models.Large", + "large_value", + reuse_handle(source, int(9), 11), + )) + .unwrap(); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); - let iterable = Expr::GenericCall( - Box::new(field_access(var("type"), "fields")), - vec![type_named("Box")], - Vec::new(), - sp(), + let exact = Expr::Clone( + Box::new(reuse_source("large_value", 12)), + Span::new(FileId::new(0), 13, 14), ); - let err = interp - .reflected_field_loop_bindings(&iterable) - .expect_err("missing checked fields should be an error"); + reuse_facts(&mut interpreter, &[(exact.span(), "models.Large")]); + interpreter + .exec_stmt(&typed_var_decl("models.Large", "copied", exact)) + .unwrap(); + assert!(reuse_markers(&mut interpreter).is_empty()); + + let coarsened = Expr::Coarsen( + Box::new(reuse_source("large_value", 15)), + Span::new(FileId::new(0), 16, 17), + ); + reuse_facts(&mut interpreter, &[(coarsened.span(), "models.Positive")]); + interpreter + .exec_stmt(&typed_var_decl( + "models.Large", + "rechecked", + reuse_handle(coarsened, int(9), 18), + )) + .unwrap(); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); - assert!(err.contains("missing field metadata")); + interpreter + .exec_stmt(&typed_var_decl("models.Sibling", "sibling_value", int(7))) + .unwrap(); + reuse_markers(&mut interpreter); + let sibling = reuse_source("sibling_value", 19); + reuse_facts(&mut interpreter, &[(sibling.span(), "models.Sibling")]); + // This low-level boundary control proves carrier identity is not proof. + interpreter + .exec_stmt(&typed_var_decl("models.Large", "unrelated", sibling)) + .unwrap(); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive", "trace large"] + ); } #[test] - fn type_field_value_uses_checked_reflection_metadata_when_available() { - let value_info = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_fields( - "Box", - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - value_info, - )], - ); + fn refinement_reuse_handles_use_target_or_exact_success_payload_facts() { + for (source_type, input, expected_markers) in [ + ( + "result[models.Positive, string]", + Value::ResultOk(Box::new(Value::Int64(7))), + vec!["trace large"], + ), + ( + "optional[models.Large]", + Value::OptionalSome(Box::new(Value::Int64(7))), + vec![], + ), + ( + "result[int64, string]", + Value::ResultOk(Box::new(Value::Int64(7))), + vec!["trace positive", "trace large"], + ), + ( + "result[models.Large, string]", + Value::ResultFail(Box::new(Value::String("no".into()))), + vec![], + ), + ("optional[models.Large]", Value::OptionalNone, vec![]), + ("int64", Value::Int64(-1), vec!["trace positive"]), + ] { + let mut interpreter = reuse_interpreter(); + let source = reuse_source("source", 20); + let initializer = reuse_handle(source.clone(), int(9), 21); + // The handle's contextual output is Large even when its target + // still needs every predicate. It must never supply the proof. + reuse_facts( + &mut interpreter, + &[ + (source.span(), source_type), + (initializer.span(), "models.Large"), + ], + ); + interpreter.set_variable("source", input); + interpreter + .exec_stmt(&typed_var_decl("models.Large", "selected", initializer)) + .unwrap(); + assert_eq!( + reuse_markers(&mut interpreter), + expected_markers, + "{source_type}" + ); + } + for source in ["secret[result[models.Large, string]]", "models.Large"] { + assert_eq!( + Interpreter::checked_sum_success_type(Some(source), sp()), + None + ); + } + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + #[test] + fn refinement_reuse_selected_generic_and_reflected_facts_restore_after_failure() { + let mut interpreter = reuse_interpreter(); + let source = reuse_source("source", 30); + interpreter.set_variable("source", Value::Int64(7)); + reuse_facts(&mut interpreter, &[(source.span(), "int64")]); + let generic = Arc::new(CheckedFunctionTypes { + type_arguments: vec!["models.Positive".into()], + expressions: Arc::new(HashMap::from([(source.span(), "models.Positive".into())])), + ..Default::default() + }); + let reflected = Arc::new(CheckedScopedTypes { + bound_type: "models.Large".into(), + expressions: HashMap::from([(source.span(), "models.Large".into())]), + ..Default::default() + }); + interpreter.active_checked_function = Some(generic.clone()); + interpreter.active_checked_scope = Some(reflected.clone()); + interpreter.current_namespace = Some("app".into()); + interpreter + .exec_stmt(&typed_var_decl("models.Large", "reflected", source.clone())) + .unwrap(); + assert!(reuse_markers(&mut interpreter).is_empty()); + interpreter.active_checked_scope = None; + interpreter + .exec_stmt(&typed_var_decl("models.Large", "generic", source.clone())) + .unwrap(); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + assert!(Arc::ptr_eq( + interpreter.active_checked_function.as_ref().unwrap(), + &generic + )); - let field = interp - .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - let Value::List(fields) = field else { - panic!("expected list of TypeField values"); - }; - let value = Value::Struct { - type_name: "Box".to_string(), - fields: vec![("value".to_string(), Value::String("ok".to_string()))], - }; - let reflected = interp - .call_builtin_with_type_args( - "type.field_value", - &[type_named("Box"), type_named("string")], - &[value, fields[0].clone()], - ) - .expect("type.field_value should be a typed builtin") - .expect("type.field_value should evaluate"); + interpreter.set_variable("source", Value::Int64(2)); + assert_eq!( + interpreter.exec_stmt(&typed_var_decl("models.Large", "invalid", source.clone())), + Err("refinement type constraint failed for 'models.Large'".into()) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); + assert!(Arc::ptr_eq( + interpreter.active_checked_function.as_ref().unwrap(), + &generic + )); + interpreter.active_checked_function = None; + interpreter.set_variable("source", Value::Int64(7)); + interpreter + .exec_stmt(&typed_var_decl("models.Large", "root", source)) + .unwrap(); + assert_eq!( + reuse_markers(&mut interpreter), + ["trace positive", "trace large"] + ); + } - assert_eq!(reflected, Value::String("ok".to_string())); + #[test] + fn refinement_reuse_pending_exact_depth_is_preserved_but_suffix_still_checks() { + for depth in [1, 2] { + let mut interpreter = reuse_interpreter(); + let mut value = Value::Typed { + type_name: "models.Large".into(), + value: Box::new(Value::Int64(7)), + }; + for _ in 0..depth { + value = Value::Pending(Box::new(value)); + } + let source = reuse_source("source", 40); + interpreter.set_variable("source", value.clone()); + reuse_facts(&mut interpreter, &[(source.span(), "models.Large")]); + interpreter + .exec_stmt(&typed_var_decl("models.Large", "copy", source.clone())) + .unwrap(); + assert_eq!(interpreter.get_variable("copy"), Some(&value)); + assert_eq!(interpreter.get_variable("source"), Some(&value)); + assert!(reuse_markers(&mut interpreter).is_empty()); + // Coarsening a pending value to Positive does not preserve the + // discarded Large proof, even if a raw label still says Large. + reuse_facts(&mut interpreter, &[(source.span(), "models.Positive")]); + assert!( + interpreter + .exec_stmt(&typed_var_decl("models.Large", "invalid", source)) + .is_err() + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + } } #[test] - fn type_bitfield_uses_checked_reflection_metadata_when_available() { - let int_info = ReflectionTypeInfo::new( - "int64", - "primitive", - Some("int64_type".to_string()), - false, - Vec::new(), - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_bitfield( - "Header", - ReflectionBitfieldInfo::new( - true, - vec![ReflectionBitfieldFieldInfo::new( - 0, "version", "bits", 3, int_info, None, - )], + fn refinement_reuse_owned_string_and_list_proofs_survive_normalization() { + for (owner, base, input) in [ + ("Text", type_named("string"), string("ready")), + ( + "Items", + TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()), + Expr::ListConstruct(vec![int(7)], sp()), ), + ] { + let mut interpreter = Interpreter::new(); + let mut predicate = func_def( + "valid", + vec![("observed", "int64")], + block(vec![ + Stmt::Trace(TraceStmt { + name: ident("observed"), + span: sp(), + }), + return_stmt(bool_expr(true)), + ]), + ); + predicate.params[0].ty = base.clone(); + predicate.params[0].view = true; + predicate.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("models"), &predicate); + let mut alias = type_alias(owner, "int64", Some(call("valid", vec![var("value")]))); + alias.base_type = base; + interpreter.register_type_alias_in_namespace(Some("models"), &alias); + let canonical = format!("models.{owner}"); + interpreter + .exec_stmt(&typed_var_decl(&canonical, "source", input)) + .unwrap(); + assert_eq!(reuse_markers(&mut interpreter), ["trace observed"]); + let source = Expr::Clone( + Box::new(reuse_source("source", 45)), + Span::new(FileId::new(0), 46, 47), + ); + reuse_facts(&mut interpreter, &[(source.span(), &canonical)]); + let original = interpreter.get_variable("source").cloned().unwrap(); + interpreter + .exec_stmt(&typed_var_decl(&canonical, "copy", source)) + .unwrap(); + assert_eq!(interpreter.get_variable("source"), Some(&original)); + assert_eq!(interpreter.get_variable("copy"), Some(&original)); + assert!(reuse_markers(&mut interpreter).is_empty()); + } + } + + #[test] + fn refinement_reuse_struct_fields_keep_namespace_result_flags_and_argument_order() { + let mut interpreter = reuse_interpreter(); + interpreter.register_struct_in_namespace( + Some("models"), + &struct_def("Pair", vec![("left", "Large"), ("right", "Large")], vec![]), + ); + // A caller-local spelling must not redefine a declaration's fields. + interpreter.register_type_alias_in_namespace( + Some("app"), + &type_alias("Large", "int64", Some(bool_expr(false))), + ); + interpreter.current_namespace = Some("app".into()); + for marker in ["first", "second"] { + let mut function = func_def( + marker, + vec![], + block(vec![ + var_decl(marker, int(7)), + Stmt::Trace(TraceStmt { + name: ident(marker), + span: sp(), + }), + return_stmt(var(marker)), + ]), + ); + function.return_type = Some(type_named("int64")); + interpreter.register_function(&function); + } + let first = Expr::Call( + Box::new(var("first")), + vec![], + Span::new(FileId::new(0), 50, 51), + ); + let second = Expr::Call( + Box::new(var("second")), + vec![], + Span::new(FileId::new(0), 52, 53), ); + reuse_facts( + &mut interpreter, + &[(first.span(), "int64"), (second.span(), "int64")], + ); + let constructor = Expr::Call( + Box::new(var("models.Pair")), + vec![named_arg("right", first), named_arg("left", second)], + sp(), + ); + let value = interpreter.eval_expr(&constructor).unwrap(); + assert!(matches!(value, Value::ResultOk(_)), "{value:?}"); + assert_eq!( + reuse_markers(&mut interpreter), + [ + "trace first", + "trace second", + "trace positive", + "trace large", + "trace positive", + "trace large" + ] + ); + let Value::ResultOk(value) = value else { + unreachable!() + }; + let Value::Struct { + type_name, fields, .. + } = *value + else { + unreachable!() + }; + assert_eq!(type_name, "models.Pair"); + assert_eq!( + fields + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + ["left", "right"] + ); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + let ready = reuse_source("ready", 54); + interpreter.set_variable("ready", Value::Int64(7)); + reuse_facts(&mut interpreter, &[(ready.span(), "models.Large")]); + let exact = Expr::Call( + Box::new(var("models.Pair")), + vec![named_arg("left", ready.clone()), named_arg("right", ready)], + sp(), + ); + assert!(matches!( + interpreter.eval_expr(&exact), + Ok(Value::ResultOk(_)) + )); + assert!( + reuse_markers(&mut interpreter).is_empty(), + "empty predicate chains still return result" + ); - let layout = interp - .call_builtin_with_type_args("type.bitfield_layout", &[type_named("Header")], &[]) - .expect("type.bitfield_layout should be a typed builtin") - .expect("type.bitfield_layout should evaluate"); - let Value::Struct { fields, .. } = layout else { - panic!("expected TypeBitfield struct"); - }; - let network_order = fields - .iter() - .find_map(|(name, value)| { - if name == "network_order" { - Some(value) - } else { - None - } - }) - .expect("TypeBitfield.network_order should exist"); - assert_eq!(network_order, &Value::Bool(true)); + interpreter.set_variable("ready", Value::Int64(2)); + reuse_facts( + &mut interpreter, + &[(Span::new(FileId::new(0), 54, 55), "models.Positive")], + ); + assert_eq!( + interpreter.eval_expr(&exact), + Ok(Value::ResultFail(Box::new(Value::String( + "refinement type constraint failed for 'models.Large'".into(), + )))) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace large"]); + } - let fields = interp - .call_builtin_with_type_args("type.bitfield_fields", &[type_named("Header")], &[]) - .expect("type.bitfield_fields should be a typed builtin") - .expect("type.bitfield_fields should evaluate"); - let Value::List(fields) = fields else { - panic!("expected list of TypeBitfieldField values"); + #[test] + fn refinement_reuse_generic_struct_flags_follow_explicit_concrete_fields() { + let mut interpreter = reuse_interpreter(); + let mut boxed = struct_def("Box", vec![("value", "T")], vec![]); + boxed.type_params = vec![ident("T")]; + interpreter.register_struct_in_namespace(Some("models"), &boxed); + // The caller's T deliberately disagrees with the constructor's T. + interpreter + .type_arg_scopes + .push(HashMap::from([("T".into(), type_named("bool"))])); + let source = reuse_source("source", 60); + interpreter.set_variable("source", Value::Int64(7)); + reuse_facts(&mut interpreter, &[(source.span(), "models.Positive")]); + let constructor = Expr::GenericCall( + Box::new(var("models.Box")), + vec![type_named("models.Positive")], + vec![named_arg("value", source.clone())], + sp(), + ); + let Value::ResultOk(value) = interpreter.eval_expr(&constructor).unwrap() else { + panic!( + "a concretely refined field always returns result, even with no predicates left" + ); }; - let Value::Struct { fields, .. } = &fields[0] else { - panic!("expected TypeBitfieldField struct"); + let Value::Struct { + concrete_type, + fields, + .. + } = *value + else { + unreachable!() }; - let width = fields - .iter() - .find_map(|(name, value)| if name == "width" { Some(value) } else { None }) - .expect("TypeBitfieldField.width should exist"); - assert_eq!(width, &Value::Int64(3)); + assert_eq!( + concrete_type.as_deref(), + Some("models.Box[models.Positive]") + ); + assert_eq!(fields[0].1.payload(), &Value::Int64(7)); + assert!(reuse_markers(&mut interpreter).is_empty()); + assert_eq!( + type_expr_display(&interpreter.current_type_binding("T").unwrap()), + "bool" + ); + + interpreter.set_variable("source", Value::Int64(-1)); + reuse_facts(&mut interpreter, &[(source.span(), "int64")]); + assert_eq!( + interpreter.eval_expr(&constructor), + Ok(Value::ResultFail(Box::new(Value::String( + "refinement type constraint failed for 'models.Positive'".into(), + )))) + ); + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + + interpreter + .type_arg_scopes + .last_mut() + .unwrap() + .insert("T".into(), type_named("models.Positive")); + interpreter.set_variable("source", Value::Int64(7)); + reuse_facts(&mut interpreter, &[(source.span(), "models.Positive")]); + let forwarded = Expr::GenericCall( + Box::new(var("models.Box")), + vec![type_named("T")], + vec![named_arg("value", source)], + sp(), + ); + assert!(matches!( + interpreter.eval_expr(&forwarded), + Ok(Value::ResultOk(_)) + )); + assert!(reuse_markers(&mut interpreter).is_empty()); } #[test] - fn type_bitfield_reports_missing_checked_bitfield_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Header", - "bitfield", - None, - false, - Vec::new(), + fn refinement_reuse_generic_caller_cannot_change_box_bool_fields() { + let mut interpreter = reuse_interpreter(); + let mut boxed = struct_def("Box", vec![("value", "T")], vec![]); + boxed.type_params = vec![ident("T")]; + interpreter.register_struct_in_namespace(Some("models"), &boxed); + let constructor = Expr::GenericCall( + Box::new(var("models.Box")), + vec![type_named("bool")], + vec![named_arg("value", bool_expr(true))], + sp(), + ); + let mut make = func_def( + "make", + vec![("ignored", "T")], + block(vec![return_stmt(constructor)]), + ); + make.type_params = vec![ident("T")]; + make.params[0].view = true; + make.return_type = Some(TypeExpr::Generic( + ident("models.Box"), + vec![type_named("bool")], + sp(), )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let err = interp - .call_builtin_with_type_args("type.bitfield_layout", &[type_named("Header")], &[]) - .expect("type.bitfield_layout should be a typed builtin") - .expect_err("missing checked bitfield layout should be an error"); - - assert!(err.contains("missing bitfield metadata")); + interpreter.register_function(&make); + let Value::Struct { + concrete_type, + fields, + .. + } = interpreter + .call_function_with_type_args( + "make", + &[type_named("models.Positive")], + vec![Value::Int64(7)], + ) + .unwrap() + else { + panic!("Box[bool] is not a validating constructor"); + }; + assert_eq!(concrete_type.as_deref(), Some("models.Box[bool]")); + assert_eq!(fields, vec![("value".into(), Value::Bool(true))]); + // Only the unchanged raw-call parameter boundary validates Positive. + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert!(interpreter.type_arg_scopes.is_empty()); } #[test] - fn type_machine_uses_checked_reflection_metadata_when_available() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_machine( - "Session", - ReflectionMachineInfo::new( - vec![ - ReflectionMachineStateInfo::new(0, "guest", false, Vec::new()), - ReflectionMachineStateInfo::new( - 1, - "logged_in", - false, - vec![string_field_info(0, "user_id")], - ), - ], - vec![ReflectionMachineTransitionInfo::new( - 0, - 0, - "guest", - 1, - "logged_in", - )], + fn refinement_reuse_struct_type_arguments_resolve_in_caller_namespace() { + let mut interpreter = Interpreter::new(); + let mut boxed = struct_def("Box", vec![("value", "T")], vec![]); + boxed.type_params = vec![ident("T")]; + interpreter.register_struct_in_namespace(Some("models"), &boxed); + interpreter.register_type_alias_in_namespace( + Some("models"), + &type_alias("Input", "int64", Some(bool_expr(false))), + ); + interpreter.register_type_alias_in_namespace( + Some("app"), + &type_alias( + "Input", + "int64", + Some(binary(var("value"), BinOp::Gt, int(0))), ), ); + interpreter.current_namespace = Some("app".into()); + let constructor = Expr::GenericCall( + Box::new(var("models.Box")), + vec![type_named("Input")], + vec![named_arg("value", int(7))], + sp(), + ); + let Value::ResultOk(value) = interpreter.eval_expr(&constructor).unwrap() else { + panic!("caller Input must be validated, not declaration-neighbor models.Input"); + }; + let Value::Struct { + concrete_type, + fields, + .. + } = *value + else { + unreachable!() + }; + assert_eq!(concrete_type.as_deref(), Some("models.Box[app.Input]")); + assert_eq!(fields[0].1.payload(), &Value::Int64(7)); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + // Unresolved raw-API arguments are not recursively reinterpreted as + // struct-parameter templates (which could create T -> T cycles). + let raw_map = HashMap::from([("T".into(), type_named("T"))]); + assert_eq!( + type_expr_display(&interpreter.struct_field_type( + &type_named("T"), + &raw_map, + Some("models") + )), + "T" + ); + } - let layout = interp - .call_builtin_with_type_args("type.machine_layout", &[type_named("Session")], &[]) - .expect("type.machine_layout should be a typed builtin") - .expect("type.machine_layout should evaluate"); - let Value::Struct { fields, .. } = layout else { - panic!("expected TypeMachine struct"); - }; - let edges = fields - .iter() - .find_map(|(name, value)| if name == "edges" { Some(value) } else { None }) - .expect("TypeMachine.edges should exist"); - let Value::List(edges) = edges else { - panic!("expected TypeMachine.edges to be a list"); - }; - assert_eq!(edges.len(), 1); - - let states = interp - .call_builtin_with_type_args("type.machine_states", &[type_named("Session")], &[]) - .expect("type.machine_states should be a typed builtin") - .expect("type.machine_states should evaluate"); - let Value::List(states) = states else { - panic!("expected list of TypeMachineState values"); - }; - let Value::Struct { fields, .. } = &states[1] else { - panic!("expected TypeMachineState struct"); - }; - let state_fields = fields - .iter() - .find_map(|(name, value)| if name == "fields" { Some(value) } else { None }) - .expect("TypeMachineState.fields should exist"); - let Value::List(state_fields) = state_fields else { - panic!("expected TypeMachineState.fields to be a list"); - }; - assert_eq!(state_fields.len(), 1); - - let edges = interp - .call_builtin_with_type_args("type.machine_transitions", &[type_named("Session")], &[]) - .expect("type.machine_transitions should be a typed builtin") - .expect("type.machine_transitions should evaluate"); - let Value::List(edges) = edges else { - panic!("expected list of TypeMachineTransition values"); - }; - let Value::Struct { fields, .. } = &edges[0] else { - panic!("expected TypeMachineTransition struct"); - }; - let target = fields - .iter() - .find_map(|(name, value)| if name == "target" { Some(value) } else { None }) - .expect("TypeMachineTransition.target should exist"); - assert_eq!(target, &Value::String("logged_in".to_string())); + #[test] + fn refinement_reuse_struct_fields_isolate_caller_aliases_and_restore_every_exit() { + let mut interpreter = reuse_interpreter(); + interpreter.register_type_alias_in_namespace( + Some("shadow"), + &type_alias("Positive", "int64", Some(bool_expr(false))), + ); + interpreter.register_struct_in_namespace( + Some("models"), + &struct_def("Holder", vec![("value", "models.Positive")], vec![]), + ); + assert_eq!( + type_expr_name(&interpreter.structs["models.Holder"].fields[0].ty), + "models.Positive" + ); + + // Predicates still need working local imports after the caller's + // aliases are hidden. More than one caller scope pins stack alignment. + let mut rule = func_def( + "check", + vec![("value", "int64")], + block(vec![return_stmt(binary(var("value"), BinOp::Gt, int(0)))]), + ); + rule.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("rules"), &rule); + let mut predicate = func_def( + "positive", + vec![("positive", "int64")], + block(vec![ + Stmt::Use(UseDecl { + path: ident("rules"), + alias: Some(ident("selected")), + span: sp(), + }), + Stmt::Trace(TraceStmt { + name: ident("positive"), + span: sp(), + }), + return_stmt(call("selected.check", vec![var("positive")])), + ]), + ); + predicate.return_type = Some(type_named("bool")); + interpreter.register_function_in_namespace(Some("models"), &predicate); + interpreter.push_scope(); + interpreter.push_scope(); + interpreter.set_namespace_alias("original".into(), "models".into()); + interpreter.set_namespace_alias("models".into(), "shadow".into()); + interpreter.set_namespace_alias("selected".into(), "shadow".into()); + interpreter + .type_arg_scopes + .push(HashMap::from([("T".into(), type_named("bool"))])); + interpreter.current_namespace = Some("app".into()); + let aliases = interpreter.namespace_alias_scopes.clone(); + let source = reuse_source("source", 70); + reuse_facts(&mut interpreter, &[(source.span(), "int64")]); + let constructor = Expr::Call( + Box::new(var("original.Holder")), + vec![named_arg("value", source.clone())], + sp(), + ); + for (input, succeeds) in [(7, true), (-1, false)] { + interpreter.set_variable("source", Value::Int64(input)); + let outcome = interpreter.eval_expr(&constructor).unwrap(); + if succeeds { + let Value::ResultOk(value) = outcome else { + panic!("{outcome:?}") + }; + let Value::Struct { fields, .. } = *value else { + unreachable!() + }; + assert!( + matches!(&fields[0].1, Value::Typed { type_name, .. } if type_name == "models.Positive") + ); + } else { + assert_eq!( + outcome, + Value::ResultFail(Box::new(Value::String( + "refinement type constraint failed for 'models.Positive'".into(), + ))) + ); + } + assert_eq!(reuse_markers(&mut interpreter), ["trace positive"]); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert_eq!( + type_expr_display(&interpreter.current_type_binding("T").unwrap()), + "bool" + ); + assert_eq!(interpreter.scopes.len(), 3); + } + let missing = Expr::Call(Box::new(var("original.Holder")), vec![], sp()); + assert_eq!( + interpreter.eval_expr(&missing), + Err("struct 'models.Holder' is missing required field 'value'".into()) + ); + assert_eq!(interpreter.namespace_alias_scopes, aliases); + assert_eq!(interpreter.current_namespace.as_deref(), Some("app")); + assert_eq!( + type_expr_display(&interpreter.current_type_binding("T").unwrap()), + "bool" + ); - let state_ty = - TypeExpr::StateQualified(Box::new(type_named("Session")), ident("logged_in"), sp()); - let layout = interp - .call_builtin_with_type_args("type.machine_layout", &[state_ty], &[]) - .expect("type.machine_layout should be a typed builtin") - .expect("state-qualified machine layout should evaluate"); - let Value::Struct { fields, .. } = layout else { - panic!("expected state-qualified TypeMachine struct"); - }; - let states = fields - .iter() - .find_map(|(name, value)| if name == "states" { Some(value) } else { None }) - .expect("TypeMachine.states should exist"); - let Value::List(states) = states else { - panic!("expected TypeMachine.states to be a list"); - }; - assert_eq!(states.len(), 2); + let mut boxed = struct_def("Box", vec![("value", "T")], vec![]); + boxed.type_params = vec![ident("T")]; + interpreter.register_struct_in_namespace(Some("models"), &boxed); + let caller_argument = Expr::GenericCall( + Box::new(var("original.Box")), + vec![type_named("models.Positive")], + vec![named_arg("value", int(7))], + sp(), + ); + assert_eq!( + interpreter.eval_expr(&caller_argument), + Ok(Value::ResultFail(Box::new(Value::String( + "refinement type constraint failed for 'shadow.Positive'".into(), + )))), + "explicit actual arguments must still resolve through caller imports" + ); + assert_eq!(interpreter.namespace_alias_scopes, aliases); } #[test] - fn reflected_machine_field_binding_requires_state_owner_member() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_machine( - "Session", - ReflectionMachineInfo::new( - vec![ReflectionMachineStateInfo::new( - 0, - "logged_in", - false, - vec![string_field_info(0, "user_id")], - )], - Vec::new(), - ), + fn grapheme_split_handles_large_near_matches() { + let haystack = "a".repeat(1024 * 1024); + let delimiter = format!("{}b", "a".repeat(512 * 1024)); + assert_eq!( + string_split_grapheme_matches(&haystack, &delimiter), + vec![haystack.as_str()] ); + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + const JSON_RAW_FACADE_NAMES: &[&str] = &[ + "json.parse_raw", + "json.serialize_raw", + "json.kind", + "json.is_null", + "json.is_bool", + "json.is_number", + "json.is_string", + "json.is_array", + "json.is_object", + "json.field", + "json.index", + "json.array_length", + "json.object_keys", + "json.as_string", + "json.as_int64", + "json.as_uint64", + "json.as_float64", + "json.as_bool", + "json.object_field", + "json.array_index", + "json.require_field", + "json.require_index", + ]; - let field = Interpreter::reflection_field_info_value( - "Session", - None, - &string_field_info(0, "user_id"), - ); - let err = interp - .reflected_machine_field_binding_for_value(&type_named("Session"), &field) - .expect_err("machine payload field metadata should include a state owner member"); + /// Helper: create a dummy span for test AST nodes. + fn sp() -> Span { + Span::new(FileId::new(0), 0, 0) + } - assert!(err.contains("expected state payload field metadata")); + fn stdlib_sp() -> Span { + Span::new(FileId::new(STDLIB_FILE_ID_START), 0, 0) } - #[test] - fn type_machine_reports_missing_checked_machine_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Session", - "machine", - None, - false, - Vec::new(), - )); + /// Helper: create an Ident. + fn ident(name: &str) -> Ident { + Ident { + name: name.to_string(), + span: sp(), + } + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + /// Helper: create an int literal expression. + fn int(n: i64) -> Expr { + Expr::IntLiteral(n.into(), sp()) + } - let err = interp - .call_builtin_with_type_args("type.machine_layout", &[type_named("Session")], &[]) - .expect("type.machine_layout should be a typed builtin") - .expect_err("missing checked machine layout should be an error"); + /// Helper: create a float literal expression. + fn float(n: f64) -> Expr { + Expr::FloatLiteral(n, sp()) + } - assert!(err.contains("missing machine metadata")); + #[test] + fn checked_join_preserves_exact_result_shape_and_one_pending_level() { + let operand_span = Span::new(FileId::new(0), 1, 2); + let operand = Expr::Ident(Ident { + name: "task".into(), + span: operand_span, + }); + let expression = Expr::Join(Box::new(operand.clone()), Span::new(FileId::new(0), 3, 4)); + for (type_name, preserves_result) in [ + ("result[int64, string]", true), + ("secret[result[int64, string]]", false), + ("secret[secret[result[int64, string]]]", false), + ] { + for ready in [ + Value::ResultOk(Box::new(Value::Int64(7))), + Value::ResultFail(Box::new(Value::String("failed".into()))), + ] { + for depth in 0..=2 { + let mut input = ready.clone(); + for _ in 0..depth { + input = Value::Pending(Box::new(input)); + } + let mut expected = ready.clone(); + for _ in 1..depth { + expected = Value::Pending(Box::new(expected)); + } + if !preserves_result { + expected = Value::ResultOk(Box::new(expected)); + } + let mut interpreter = Interpreter::new(); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(operand_span, type_name.into())]), + ..Default::default() + })); + interpreter.set_variable("task", input.clone()); + assert_eq!( + interpreter.join_preserves_result(&operand), + Some(preserves_result) + ); + assert_eq!( + interpreter.eval_expr(&expression), + Ok(expected), + "{type_name}, depth {depth}, {ready:?}" + ); + assert_eq!(interpreter.get_variable("task"), Some(&input)); + } + } + } + // The cancellation sentinel is only the resolved bare unit value. + for (input, expected) in [ + ( + Value::Nothing, + Value::ResultFail(Box::new(Value::String("task was cancelled".into()))), + ), + ( + Value::Pending(Box::new(Value::Nothing)), + Value::ResultOk(Box::new(Value::Nothing)), + ), + ( + Value::Pending(Box::new(Value::Pending(Box::new(Value::Nothing)))), + Value::ResultOk(Box::new(Value::Pending(Box::new(Value::Nothing)))), + ), + ] { + let mut interpreter = Interpreter::new(); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(operand_span, "nothing".into())]), + ..Default::default() + })); + interpreter.set_variable("task", input); + assert_eq!(interpreter.eval_expr(&expression), Ok(expected)); + } } #[test] - fn type_variants_uses_checked_reflection_metadata_when_available() { - let field_info = ReflectionTypeInfo::new( - "secret[string]", - "secret", - None, - true, - vec![ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - )], - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( - 1, - "token", - 7, - true, - vec![ReflectionFieldInfo::new( - 0, - "value", - "secret[string]", - "secret", - "jsonValue", - true, - field_info, - )], - )], + fn checked_join_uses_active_generic_and_reflected_facts_before_declared_types() { + let operand_span = Span::new(FileId::new(0), 1, 2); + let operand = Expr::Ident(Ident { + name: "task".into(), + span: operand_span, + }); + let expression = Expr::Join(Box::new(operand.clone()), Span::new(FileId::new(0), 3, 4)); + let ready = Value::ResultOk(Box::new(Value::Int64(7))); + let mut interpreter = Interpreter::new(); + let declared = + Interpreter::simple_type_expr_from_name("secret[result[int64, string]]", operand_span) + .unwrap(); + interpreter.set_variable_with_type("task", ready.clone(), declared); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(operand_span, "result[int64, string]".into())]), + ..Default::default() + })); + assert_eq!(interpreter.eval_expr(&expression), Ok(ready.clone())); + interpreter.active_checked_function = Some(Arc::new(CheckedFunctionTypes { + type_arguments: vec!["secret[result[int64, string]]".into()], + expressions: Arc::new(HashMap::from([( + operand_span, + "secret[result[int64, string]]".into(), + )])), + ..Default::default() + })); + assert_eq!( + interpreter.eval_expr(&expression), + Ok(Value::ResultOk(Box::new(ready.clone()))) + ); + interpreter.active_checked_scope = Some(Arc::new(CheckedScopedTypes { + bound_type: "result[int64, string]".into(), + expressions: HashMap::from([(operand_span, "result[int64, string]".into())]), + ..Default::default() + })); + assert_eq!(interpreter.eval_expr(&expression), Ok(ready.clone())); + interpreter.active_checked_scope = None; + assert_eq!( + interpreter.eval_expr(&expression), + Ok(Value::ResultOk(Box::new(ready.clone()))) ); + interpreter.active_checked_function = None; + assert_eq!(interpreter.eval_expr(&expression), Ok(ready)); - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let value = interp - .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) - .expect("type.variants should be a typed builtin") - .expect("type.variants should evaluate"); + // A nominal checked owner is not a result even when its payload is. + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(operand_span, "models.Validated".into())]), + ..Default::default() + })); + assert_eq!(interpreter.join_preserves_result(&operand), Some(false)); + } - let Value::List(variants) = value else { - panic!("expected list of TypeVariant values"); - }; - let Value::Struct { fields, .. } = &variants[0] else { - panic!("expected TypeVariant struct"); - }; - let discriminant = fields - .iter() - .find_map(|(name, value)| { - if name == "discriminant" { - Some(value) - } else { - None - } - }) - .expect("TypeVariant.discriminant should exist"); - let has_secret = fields - .iter() - .find_map(|(name, value)| { - if name == "has_secret" { - Some(value) - } else { - None - } - }) - .expect("TypeVariant.has_secret should exist"); - assert_eq!(discriminant, &Value::Int64(7)); - assert_eq!(has_secret, &Value::Bool(true)); + #[test] + fn join_declared_alias_fallback_stops_at_secret_and_refinement_owners() { + let parsed = jett_parser::parse( + r#"namespace models +type Public = result[int64, string] +type Forwarded = Public +type Hidden = secret[Public] +type Nested = secret[Hidden] +type Validated = Public where true +type ValidatedAlias = Validated +"#, + FileId::new(0), + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + interpreter.current_namespace = Some("unrelated".into()); + for (name, expected) in [ + ("models.Public", true), + ("models.Forwarded", true), + ("models.Hidden", false), + ("models.Nested", false), + ("models.Validated", false), + ("models.ValidatedAlias", false), + ] { + interpreter.set_variable_with_type( + "task", + Value::ResultOk(Box::new(Value::Int64(7))), + type_named(name), + ); + for expression in [ + var("task"), + Expr::Run(Box::new(var("task")), sp()), + Expr::Clone(Box::new(Expr::Paren(Box::new(var("task")), sp())), sp()), + ] { + assert_eq!( + interpreter.join_preserves_result(&expression), + Some(expected), + "{name}" + ); + } + } + interpreter.type_arg_scopes.push(HashMap::from([( + "T".into(), + type_named("models.Forwarded"), + )])); + interpreter.set_variable_with_type( + "task", + Value::ResultOk(Box::new(Value::Int64(7))), + type_named("T"), + ); + assert_eq!(interpreter.join_preserves_result(&var("task")), Some(true)); + interpreter + .type_arg_scopes + .last_mut() + .unwrap() + .insert("T".into(), type_named("models.Hidden")); + assert_eq!(interpreter.join_preserves_result(&var("task")), Some(false)); } #[test] - fn type_variants_reports_missing_checked_enum_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Shape", - "enum", - None, - false, - Vec::new(), - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let err = interp - .call_builtin_with_type_args("type.variants", &[type_named("Shape")], &[]) - .expect("type.variants should be a typed builtin") - .expect_err("missing checked enum variants should be an error"); - - assert!(err.contains("missing variant metadata")); + fn join_without_checked_or_declared_metadata_keeps_raw_api_behavior() { + let ready = Value::ResultOk(Box::new(Value::Int64(7))); + for (input, expected) in [ + (ready.clone(), Value::ResultOk(Box::new(ready.clone()))), + (Value::Pending(Box::new(ready.clone())), ready.clone()), + ( + Value::Pending(Box::new(Value::Pending(Box::new(ready.clone())))), + Value::ResultOk(Box::new(Value::Pending(Box::new(ready)))), + ), + ] { + let mut interpreter = Interpreter::new(); + interpreter.set_variable("task", input); + assert_eq!(interpreter.join_preserves_result(&var("task")), None); + assert_eq!( + interpreter.eval_expr(&Expr::Join(Box::new(var("task")), sp())), + Ok(expected) + ); + } } #[test] - fn type_variant_value_reports_missing_checked_variant_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, - false, - Vec::new(), - )); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let value = Value::Enum { - type_name: "Choice".to_string(), - variant: "token".to_string(), - fields: Vec::new(), - }; - let err = interp - .call_builtin_with_type_args("type.variant_value", &[type_named("Choice")], &[value]) - .expect("type.variant_value should be a typed builtin") - .expect_err("missing checked variant metadata should be an error"); - - assert!(err.contains("missing variant metadata")); + fn join_debug_types_keep_exact_outer_shapes_and_declared_alias_labels() { + let parsed = jett_parser::parse( + r#"namespace models +type Public = result[int64, string] +type Forwarded = Public +type Hidden = secret[Public] +type Validated = Public where true +type ValidatedAlias = Validated +"#, + FileId::new(0), + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + for (declared, expected) in [ + ("result[int64, string]", "result[int64, string]"), + ("models.Public", "models.Public"), + ("models.Forwarded", "models.Forwarded"), + ("models.Hidden", "result[models.Hidden, string]"), + ("models.Validated", "result[models.Validated, string]"), + ( + "models.ValidatedAlias", + "result[models.ValidatedAlias, string]", + ), + ( + "secret[result[int64, string]]", + "result[secret[result[int64, string]], string]", + ), + ] { + let ty = Interpreter::simple_type_expr_from_name(declared, sp()).unwrap(); + interpreter.set_variable_with_type( + "task", + Value::ResultOk(Box::new(Value::Int64(7))), + ty, + ); + let expression = Expr::Join(Box::new(var("task")), sp()); + let actual = interpreter.debug_expression_type(&expression).unwrap(); + assert_eq!(type_expr_display(&actual), expected, "{declared}"); + } } #[test] - fn reflected_variant_loops_use_checked_reflection_metadata_when_available() { - let field_info = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( - 0, - "token", - 7, - false, - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - field_info, - )], - )], - ); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let variant_iterable = Expr::GenericCall( - Box::new(field_access(var("type"), "variants")), - vec![type_named("Choice")], - Vec::new(), - sp(), - ); - let variant_bindings = interp - .reflected_variant_loop_bindings(&variant_iterable) - .expect("type.variants loop should not error") - .expect("type.variants loop should be recognized"); - assert_eq!(variant_bindings.len(), 1); - assert_eq!(variant_bindings[0].name, "token"); - assert_eq!(variant_bindings[0].discriminant, 7); - - let variants = interp - .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) - .expect("type.variants should be a typed builtin") - .expect("type.variants should evaluate"); - let Value::List(variants) = variants else { - panic!("expected list of TypeVariant values"); - }; - let Value::Struct { - fields: variant_fields, - .. - } = &variants[0] - else { - panic!("expected TypeVariant"); - }; - let payload_fields = variant_fields - .iter() - .find_map( - |(name, value)| { - if name == "fields" { Some(value) } else { None } - }, - ) - .expect("TypeVariant.fields should exist"); - let Value::List(payload_fields) = payload_fields else { - panic!("expected TypeVariant.fields list"); + fn join_debug_types_prefer_checked_target_facts_in_active_context_order() { + let operand_span = Span::new(FileId::new(0), 1, 2); + let target_span = Span::new(FileId::new(0), 3, 4); + let operand = Expr::Ident(Ident { + name: "task".into(), + span: operand_span, + }); + let expression = Expr::Join(Box::new(operand), target_span); + let mut interpreter = Interpreter::new(); + interpreter.set_variable_with_type( + "task", + Value::ResultOk(Box::new(Value::Int64(7))), + type_named("T"), + ); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + expressions: HashMap::from([(target_span, "result[models.Validated, string]".into())]), + ..Default::default() + })); + let debug_name = |interpreter: &Interpreter| { + type_expr_display(&interpreter.debug_expression_type(&expression).unwrap()) }; + assert_eq!(debug_name(&interpreter), "result[models.Validated, string]"); + interpreter.active_checked_function = Some(Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([( + target_span, + "result[secret[result[int64, string]], string]".into(), + )])), + ..Default::default() + })); + assert_eq!( + debug_name(&interpreter), + "result[secret[result[int64, string]], string]" + ); + interpreter.active_checked_scope = Some(Arc::new(CheckedScopedTypes { + expressions: HashMap::from([(target_span, "result[int64, string]".into())]), + ..Default::default() + })); + assert_eq!(debug_name(&interpreter), "result[int64, string]"); + interpreter.active_checked_scope = None; + assert_eq!( + debug_name(&interpreter), + "result[secret[result[int64, string]], string]" + ); + interpreter.active_checked_function = None; + assert_eq!(debug_name(&interpreter), "result[models.Validated, string]"); + } - let field_binding = interp - .reflected_variant_field_binding_for_value( - &type_named("Choice"), - Some("token"), - &payload_fields[0], - ) - .expect("payload field binding should be checked"); - assert_eq!(field_binding.index, 0); - assert_eq!(field_binding.name, "value"); - assert_eq!(type_expr_display(&field_binding.ty), "string"); + #[test] + fn refined_join_handles_extract_outer_results_before_refinement_validation() { + let parsed = jett_parser::parse( + r#"namespace app +type RefinedOutcome = result[int64, string] where true +function joined(candidate: result[int64, string]) returns int64: + RefinedOutcome checked = candidate handle error: + return -1 + RefinedOutcome pending = run run checked + RefinedOutcome once = join pending handle error: + return -3 + RefinedOutcome ready = join once handle error: + return -4 + result[int64, string] base = coarsen ready + int64 answer = base handle error: + return -2 + return answer +"#, + FileId::new(0), + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + for (candidate, expected) in [ + (Value::ResultOk(Box::new(Value::Int64(7))), 7), + ( + Value::ResultFail(Box::new(Value::String("inner failure".into()))), + -2, + ), + ] { + assert_eq!( + interpreter.call_function("app.joined", vec![candidate]), + Ok(Value::Int64(expected)) + ); + } } #[test] - fn reflected_variant_field_binding_requires_variant_owner_member() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( - 0, - "token", - 7, - false, - vec![string_field_info(0, "value")], - )], + fn explicit_comptime_expression_uses_baked_value() { + let span = sp(); + let expression = Expr::Comptime( + Box::new(Expr::Binary( + Box::new(int(1)), + BinOp::Div, + Box::new(int(0)), + span, + )), + span, ); + let mut values = crate::ExplicitComptimeValues::default(); + values.insert(span, crate::ComptimeContext::default(), Value::Int64(42)); - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let field = Interpreter::reflection_field_info_value( - "Choice", - None, - &string_field_info(0, "value"), - ); - let err = interp - .reflected_variant_field_binding_for_value(&type_named("Choice"), None, &field) - .expect_err("enum payload field metadata should include a variant owner member"); + let mut interpreter = Interpreter::new(); + interpreter.set_explicit_comptime_values(Arc::new(values)); - assert!(err.contains("expected payload field metadata")); + assert_eq!(interpreter.eval_expr(&expression), Ok(Value::Int64(42))); } - #[test] - fn reflected_variant_loop_reports_missing_checked_variant_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, - false, - Vec::new(), - )); + /// Helper: create a string literal expression. + fn string(s: &str) -> Expr { + Expr::StringLiteral(s.to_string(), sp()) + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + /// Helper: create a bool literal expression. + fn bool_expr(b: bool) -> Expr { + Expr::BoolLiteral(b, sp()) + } - let variant_iterable = Expr::GenericCall( - Box::new(field_access(var("type"), "variants")), - vec![type_named("Choice")], - Vec::new(), - sp(), - ); - let err = interp - .reflected_variant_loop_bindings(&variant_iterable) - .expect_err("missing checked variants should be an error"); + /// Helper: create an identifier expression. + fn var(name: &str) -> Expr { + Expr::Ident(ident(name)) + } - assert!(err.contains("missing variant metadata")); + /// Helper: create a binary expression. + fn binary(lhs: Expr, op: BinOp, rhs: Expr) -> Expr { + Expr::Binary(Box::new(lhs), op, Box::new(rhs), sp()) } - #[test] - fn reflected_variant_field_binding_reports_missing_checked_variant_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, + /// Helper: create a simple type expression. + fn type_named(name: &str) -> TypeExpr { + TypeExpr::Named(ident(name)) + } + + fn type_alias(name: &str, base: &str, constraint: Option) -> TypeAlias { + TypeAlias { + name: ident(name), + base_type: type_named(base), + constraint, + exported: false, + root_exported: false, + span: sp(), + } + } + + /// Helper: create a variable declaration statement. + fn var_decl(name: &str, value: Expr) -> Stmt { + Stmt::VarDecl(VarDecl { + mutable: true, + ty: type_named("int64"), + name: ident(name), + value, + span: sp(), + }) + } + + fn typed_var_decl(type_name: &str, name: &str, value: Expr) -> Stmt { + Stmt::VarDecl(VarDecl { + mutable: true, + ty: type_named(type_name), + name: ident(name), + value, + span: sp(), + }) + } + + /// Helper: create an assignment statement. + fn assign(name: &str, value: Expr) -> Stmt { + Stmt::Assign(AssignStmt { + target: var(name), + value, + span: sp(), + }) + } + + /// Helper: create an assert statement. + fn assert_stmt(condition: Expr, message: Option) -> Stmt { + Stmt::Assert(AssertStmt { + condition, + message, + span: sp(), + }) + } + + /// Helper: create a block from statements. + fn block(stmts: Vec) -> Block { + Block { stmts, span: sp() } + } + + /// Helper: create a return statement. + fn return_stmt(value: Expr) -> Stmt { + Stmt::Return(ReturnStmt { + value: Some(value), + span: sp(), + }) + } + + fn respond_stmt(value: Expr) -> Stmt { + Stmt::Respond(RespondStmt { value, span: sp() }) + } + + /// Helper: create a function call expression. + fn call(name: &str, args: Vec) -> Expr { + call_expression(var(name), args) + } + + fn call_expression(callee: Expr, args: Vec) -> Expr { + Expr::Call( + Box::new(callee), + args.into_iter() + .map(|value| CallArg { + name: None, + value, + span: sp(), + }) + .collect(), + sp(), + ) + } + + /// Helper: create a FunctionDef. + fn func_def(name: &str, params: Vec<(&str, &str)>, body: Block) -> FunctionDef { + FunctionDef { + name: ident(name), + type_params: vec![], + params: params + .into_iter() + .map(|(pname, ptype)| Param { + view: false, + mutable: false, + name: ident(pname), + ty: type_named(ptype), + span: sp(), + }) + .collect(), + return_type: None, + body, + exported: false, + span: sp(), + } + } + + fn generic_json_hook(name: &str, body: Block) -> FunctionDef { + FunctionDef { + name: ident(name), + type_params: vec![ident("T")], + params: vec![Param { + view: false, + mutable: false, + name: ident("raw"), + ty: type_named("string"), + span: sp(), + }], + return_type: None, + body, + exported: false, + span: sp(), + } + } + + fn json_tree_null() -> Value { + Value::Enum { + type_name: "json.JsonTree".to_string(), + variant: "null".to_string(), + fields: Vec::new(), + } + } + + fn string_type_info() -> ReflectionTypeInfo { + ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), false, Vec::new(), - )); + ) + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + fn uint64_type_info() -> ReflectionTypeInfo { + ReflectionTypeInfo::new( + "uint64", + "primitive", + Some("uint64_type".to_string()), + false, + Vec::new(), + ) + } - let field = Interpreter::reflection_field_info_value( - "Choice", - Some("token"), - &string_field_info(0, "value"), - ); - let err = interp - .reflected_variant_field_binding_for_value(&type_named("Choice"), Some("token"), &field) - .expect_err("missing checked variants should be an error"); + fn string_field_info(index: usize, name: &str) -> ReflectionFieldInfo { + ReflectionFieldInfo::new( + index, + name, + "string", + "primitive", + name, + false, + string_type_info(), + ) + } - assert!(err.contains("missing variant metadata")); + fn uint64_field_info(index: usize, name: &str) -> ReflectionFieldInfo { + ReflectionFieldInfo::new( + index, + name, + "uint64", + "primitive", + name, + false, + uint64_type_info(), + ) } #[test] - fn type_variant_value_uses_checked_reflection_metadata_when_available() { + fn type_info_uses_checked_reflection_metadata_when_available() { let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new(0, "token", 7, false, Vec::new())], - ); + metadata.insert_type_info(ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".to_string()), + true, + Vec::new(), + )); let mut interp = Interpreter::new(); interp.set_reflection_metadata(Arc::new(metadata)); - let value = Value::Enum { - type_name: "Choice".to_string(), - variant: "token".to_string(), - fields: Vec::new(), - }; - let variant = interp - .call_builtin_with_type_args("type.variant_value", &[type_named("Choice")], &[value]) - .expect("type.variant_value should be a typed builtin") - .expect("type.variant_value should evaluate"); + let value = interp + .call_builtin_with_type_args("type.info", &[type_named("int64")], &[]) + .expect("type.info should be a typed builtin") + .expect("type.info should evaluate"); - let Value::Struct { fields, .. } = variant else { - panic!("expected TypeVariant struct"); + let Value::Struct { fields, .. } = value else { + panic!("expected TypeInfo struct"); }; - let discriminant = fields + let has_secret = fields .iter() .find_map(|(name, value)| { - if name == "discriminant" { + if name == "has_secret" { Some(value) } else { None } }) - .expect("TypeVariant.discriminant should exist"); - assert_eq!(discriminant, &Value::Int64(7)); + .expect("TypeInfo.has_secret field should exist"); + assert_eq!(has_secret, &Value::Bool(true)); } #[test] - fn type_variant_field_value_uses_checked_reflection_metadata_when_available() { - let field_type = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); + fn bound_type_reflection_reports_missing_checked_type_info() { let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( - 0, - "token", - 7, - false, - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - field_type, - )], - )], - ); + metadata.bind_type_name("Box", jett_types::TypeInterner::STRING); let mut interp = Interpreter::new(); + interp.register_struct(&struct_def("Box", vec![("value", "string")], vec![])); interp.set_reflection_metadata(Arc::new(metadata)); - let value = Value::Enum { - type_name: "Choice".to_string(), - variant: "token".to_string(), - fields: vec![Value::String("ok".to_string())], - }; - let variant = interp - .call_builtin_with_type_args( - "type.variant_value", - &[type_named("Choice")], - &[value.clone()], - ) - .expect("type.variant_value should be a typed builtin") - .expect("type.variant_value should evaluate"); - let Value::Struct { fields, .. } = variant else { - panic!("expected TypeVariant struct"); - }; - let field = fields - .iter() - .find_map( - |(name, value)| { - if name == "fields" { Some(value) } else { None } - }, - ) - .expect("TypeVariant.fields should exist"); - let Value::List(fields) = field else { - panic!("expected TypeVariant.fields list"); - }; - - let reflected = interp - .call_builtin_with_type_args( - "type.variant_field_value", - &[type_named("Choice"), type_named("string")], - &[value, fields[0].clone()], - ) - .expect("type.variant_field_value should be a typed builtin") - .expect("type.variant_field_value should evaluate"); + let ty = type_named("Box"); + let info_error = interp + .call_builtin_with_type_args("type.info", std::slice::from_ref(&ty), &[]) + .expect("type.info should be a typed builtin") + .expect_err("bound missing checked type info should be an error"); + let kind_error = interp + .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) + .expect("type.kind should be a typed builtin") + .expect_err("bound missing checked type info should be an error"); - assert_eq!(reflected, Value::String("ok".to_string())); + assert_eq!( + info_error, + "checked reflection metadata for type 'Box' is missing type info metadata" + ); + assert_eq!(kind_error, info_error); } #[test] - fn type_construct_put_and_finish_use_checked_reflection_metadata_when_available() { - let field_type = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); + fn checked_resource_kind_tag_preserves_opaque_type_metadata() { let mut metadata = ReflectionMetadata::new(); metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", + "io.FileHandle", + "resource", None, false, Vec::new(), )); - metadata.insert_type_fields( - "Box", - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - field_type, - )], - ); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let type_fields = interp - .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - let Value::List(type_fields) = type_fields else { - panic!("expected list of TypeField values"); - }; - - let updated = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Box"), type_named("string")], - &[ - builder, - type_fields[0].clone(), - Value::String("ok".to_string()), - ], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(updated) = updated else { - panic!("expected successful construction update"); - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Box")], - &[(*updated).clone()], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); - let Value::ResultOk(finished) = finished else { - panic!("expected successful construction finish"); + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(Arc::new(metadata)); + let ty = type_named("io.FileHandle"); + let expected_tag = Value::Enum { + type_name: "TypeKind".into(), + variant: "resource_type".into(), + fields: Vec::new(), }; - let Value::Struct { - type_name: finished_type_name, - fields: finished_fields, - } = *finished - else { - panic!("expected constructed struct"); + let tag = interpreter + .call_builtin_with_type_args("type.kind_tag", std::slice::from_ref(&ty), &[]) + .expect("typed kind builtin") + .expect("resource kind metadata"); + assert_eq!(tag, expected_tag); + let info = interpreter + .call_builtin_with_type_args("type.info", std::slice::from_ref(&ty), &[]) + .expect("typed info builtin") + .expect("resource info metadata"); + let Value::Struct { fields, .. } = info else { + panic!("expected TypeInfo") }; - assert_eq!(finished_type_name, "Box"); + assert!( + fields + .iter() + .any(|(name, value)| name == "kind_tag" && *value == expected_tag) + ); + for builtin in ["type.fields", "type.variants", "type.machine_states"] { + assert_eq!( + interpreter + .call_builtin_with_type_args(builtin, std::slice::from_ref(&ty), &[],) + .expect("typed shape builtin") + .expect("empty resource shape"), + Value::List(Vec::new()) + ); + } assert_eq!( - finished_fields, - vec![("value".to_string(), Value::String("ok".to_string()))] + interpreter + .call_builtin_with_type_args("type.primitive_tag", &[ty], &[],) + .expect("typed primitive builtin") + .expect("resource has no primitive"), + Value::OptionalNone ); - - let Value::TypeConstruction { - type_name, - variant, - state, - fields, - } = *updated - else { - panic!("expected TypeConstruction"); - }; - - assert_eq!(type_name, "Box"); - assert_eq!(variant, None); - assert_eq!(state, None); - assert_eq!(fields.len(), 1); - assert_eq!(fields[0].0, 0); - assert_eq!(fields[0].1, "value"); - assert_eq!(fields[0].2, "string"); - assert_eq!(fields[0].3, Value::String("ok".to_string())); } #[test] - fn type_construct_put_and_finish_normalize_checked_uint64_struct_field() { + fn direct_type_reflection_uses_checked_metadata_when_available() { let mut metadata = ReflectionMetadata::new(); metadata.insert_type_info(ReflectionTypeInfo::new( - "Counter", - "struct", - None, - false, + "Token", + "primitive", + Some("string_type".to_string()), + true, Vec::new(), )); - metadata.insert_type_fields("Counter", vec![uint64_field_info(0, "value")]); let mut interp = Interpreter::new(); interp.set_reflection_metadata(Arc::new(metadata)); - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Counter")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let type_fields = interp - .call_builtin_with_type_args("type.fields", &[type_named("Counter")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - let Value::List(type_fields) = type_fields else { - panic!("expected list of TypeField values"); - }; - - let updated = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Counter"), type_named("uint64")], - &[builder, type_fields[0].clone(), Value::Int64(42)], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(updated) = updated else { - panic!("expected successful construction update"); - }; - let Value::TypeConstruction { fields, .. } = updated.as_ref() else { - panic!("expected TypeConstruction"); - }; - assert_eq!(fields[0].3, Value::Uint64(42)); - - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Counter")], - &[(*updated).clone()], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); - let Value::ResultOk(finished) = finished else { - panic!("expected successful construction finish"); - }; + let ty = type_named("Token"); + let name = interp + .call_builtin_with_type_args("type.name", std::slice::from_ref(&ty), &[]) + .expect("type.name should be a typed builtin") + .expect("type.name should evaluate"); + let kind = interp + .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) + .expect("type.kind should be a typed builtin") + .expect("type.kind should evaluate"); + let kind_tag = interp + .call_builtin_with_type_args("type.kind_tag", std::slice::from_ref(&ty), &[]) + .expect("type.kind_tag should be a typed builtin") + .expect("type.kind_tag should evaluate"); + let primitive_tag = interp + .call_builtin_with_type_args("type.primitive_tag", std::slice::from_ref(&ty), &[]) + .expect("type.primitive_tag should be a typed builtin") + .expect("type.primitive_tag should evaluate"); + let has_secret = interp + .call_builtin_with_type_args("type.has_secret", &[ty], &[]) + .expect("type.has_secret should be a typed builtin") + .expect("type.has_secret should evaluate"); + assert_eq!(name, Value::String("Token".to_string())); + assert_eq!(kind, Value::String("primitive".to_string())); assert_eq!( - *finished, - Value::Struct { - type_name: "Counter".to_string(), - fields: vec![("value".to_string(), Value::Uint64(42))], + kind_tag, + Value::Enum { + type_name: "TypeKind".to_string(), + variant: "primitive_type".to_string(), + fields: Vec::new(), } ); + assert_eq!( + primitive_tag, + Value::OptionalSome(Box::new(Value::Enum { + type_name: "TypePrimitive".to_string(), + variant: "string_type".to_string(), + fields: Vec::new(), + })) + ); + assert_eq!(has_secret, Value::Bool(true)); } #[test] - fn type_construct_enum_payload_and_finish_use_checked_reflection_metadata_when_available() { - let field_type = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), + fn direct_has_secret_fallback_uses_state_qualified_machine_payload() { + let mut interp = Interpreter::new(); + let token_ty = TypeExpr::Generic(ident("secret"), vec![type_named("string")], sp()); + interp.register_machine(&machine_def( + "TokenLifecycle", + vec![ + ("issued", vec![("token", token_ty)]), + ("revoked", Vec::new()), + ], + vec![("issued", "revoked")], + )); + + let machine_has_secret = interp + .call_builtin_with_type_args("type.has_secret", &[type_named("TokenLifecycle")], &[]) + .expect("type.has_secret should be a typed builtin") + .expect("machine secret reflection should evaluate"); + let issued_ty = TypeExpr::StateQualified( + Box::new(type_named("TokenLifecycle")), + ident("issued"), + sp(), + ); + let issued_has_secret = interp + .call_builtin_with_type_args("type.has_secret", &[issued_ty], &[]) + .expect("type.has_secret should be a typed builtin") + .expect("issued state secret reflection should evaluate"); + let revoked_ty = TypeExpr::StateQualified( + Box::new(type_named("TokenLifecycle")), + ident("revoked"), + sp(), + ); + let revoked_has_secret = interp + .call_builtin_with_type_args("type.has_secret", &[revoked_ty], &[]) + .expect("type.has_secret should be a typed builtin") + .expect("revoked state secret reflection should evaluate"); + + assert_eq!(machine_has_secret, Value::Bool(true)); + assert_eq!(issued_has_secret, Value::Bool(true)); + assert_eq!(revoked_has_secret, Value::Bool(false)); + } + + #[test] + fn direct_reflection_fallback_normalizes_current_namespace_types() { + let mut interp = Interpreter::new(); + interp.register_struct_in_namespace( + Some("models"), + &struct_def("User", vec![("name", "string")], vec![]), + ); + interp.register_enum_in_namespace( + Some("models"), + &enum_def_with_values("Status", vec![("active", 0)]), + ); + interp.register_bitfield_in_namespace( + Some("models"), + &bitfield_def( + "Header", + vec![( + "version", + BitfieldFieldKind::Bits { + width: 4, + as_type: None, + }, + )], + false, + ), + ); + interp.current_namespace = Some("models".to_string()); + + let user_ty = type_named("User"); + assert_eq!(interp.type_expr_kind(&user_ty), "struct"); + let user_fields = interp.type_expr_fields(&user_ty); + assert_eq!(user_fields.len(), 1); + assert_eq!(user_fields[0].name, "name"); + + let status_ty = type_named("Status"); + assert_eq!(interp.type_expr_kind(&status_ty), "enum"); + let variants = interp.type_expr_variants(&status_ty); + assert_eq!(variants.len(), 1); + assert_eq!(variants[0].name, "active"); + + let header_ty = type_named("Header"); + assert_eq!(interp.type_expr_kind(&header_ty), "bitfield"); + let bitfield = interp.type_expr_bitfield(&header_ty); + assert_eq!(bitfield.fields.len(), 1); + assert_eq!(bitfield.fields[0].name, "version"); + } + + #[test] + fn registry_lookup_preserves_root_only_outside_namespace() { + let mut interp = Interpreter::new(); + interp.register_function(&func_def( + "helper", + vec![], + block(vec![return_stmt(string("root"))]), + )); + + assert_eq!( + interp.registry_name(&interp.functions, "helper").as_deref(), + Some("helper") + ); + + interp.current_namespace = Some("alpha".to_string()); + assert_eq!(interp.registry_name(&interp.functions, "helper"), None); + + interp.register_function_in_namespace( + Some("alpha"), + &func_def( + "helper", + vec![], + block(vec![return_stmt(string("namespace"))]), + ), + ); + assert_eq!( + interp.registry_name(&interp.functions, "helper").as_deref(), + Some("alpha.helper") + ); + } + + #[test] + fn direct_alias_reflection_prefers_registered_alias() { + let mut interp = Interpreter::new(); + interp.register_enum(&enum_def_with_values("json.JsonTree", vec![("null", 0)])); + interp.register_type_alias(&type_alias("RawAlias", "json.JsonTree", None)); + + let ty = type_named("RawAlias"); + let kind = interp + .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) + .expect("type.kind should be a typed builtin") + .expect("type.kind should evaluate"); + let kind_tag = interp + .call_builtin_with_type_args("type.kind_tag", std::slice::from_ref(&ty), &[]) + .expect("type.kind_tag should be a typed builtin") + .expect("type.kind_tag should evaluate"); + let primitive_tag = interp + .call_builtin_with_type_args("type.primitive_tag", std::slice::from_ref(&ty), &[]) + .expect("type.primitive_tag should be a typed builtin") + .expect("type.primitive_tag should evaluate"); + let info = interp + .call_builtin_with_type_args("type.info", &[ty], &[]) + .expect("type.info should be a typed builtin") + .expect("type.info should evaluate"); + + assert_eq!(kind, Value::String("alias".to_string())); + assert_eq!( + kind_tag, + Value::Enum { + type_name: "TypeKind".to_string(), + variant: "alias_type".to_string(), + fields: Vec::new(), + } + ); + assert_eq!(primitive_tag, Value::OptionalNone); + + let Value::Struct { fields, .. } = info else { + panic!("expected TypeInfo struct"); + }; + let field = |field_name: &str| { + fields + .iter() + .find_map(|(name, value)| (name == field_name).then_some(value)) + .expect("TypeInfo field should exist") + }; + assert_eq!(field("kind"), &Value::String("alias".to_string())); + assert_eq!(field("primitive_tag"), &Value::OptionalNone); + + let Value::List(args) = field("args") else { + panic!("expected TypeInfo.args list"); + }; + assert_eq!(args.len(), 1); + let Value::Struct { + fields: arg_fields, .. + } = &args[0] + else { + panic!("expected nested TypeInfo"); + }; + let arg_field = |field_name: &str| { + arg_fields + .iter() + .find_map(|(name, value)| (name == field_name).then_some(value)) + .expect("nested TypeInfo field should exist") + }; + assert_eq!( + arg_field("type_name"), + &Value::String("json.JsonTree".to_string()) + ); + assert_eq!(arg_field("kind"), &Value::String("enum".to_string())); + } + + #[test] + fn unknown_direct_type_has_no_alias_reflection() { + let mut interp = Interpreter::new(); + let ty = type_named("UnknownRaw"); + + let kind = interp + .call_builtin_with_type_args("type.kind", std::slice::from_ref(&ty), &[]) + .expect("type.kind should be a typed builtin") + .expect("type.kind should evaluate"); + let primitive_tag = interp + .call_builtin_with_type_args("type.primitive_tag", &[ty], &[]) + .expect("type.primitive_tag should be a typed builtin") + .expect("type.primitive_tag should evaluate"); + + assert_eq!(kind, Value::String("named".to_string())); + assert_eq!(primitive_tag, Value::OptionalNone); + } + + #[test] + fn json_serialize_secret_gate_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "SecretBox", + "struct", + None, + true, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = Value::Struct { + concrete_type: None, + type_name: "SecretBox".to_string(), + fields: Vec::new(), + }; + let result = interp + .call_builtin_with_type_args("json.serialize", &[type_named("SecretBox")], &[value]) + .expect("json.serialize should be a typed builtin"); + + assert_eq!( + result, + Err("json.serialize cannot serialize secret-containing type 'SecretBox'".to_string()) + ); + } + + #[test] + fn type_arg_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "list[int64]", + "list", + None, + false, + vec![ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".to_string()), + true, + Vec::new(), + )], + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let list_int = TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()); + let value = interp + .call_builtin_with_type_args("type.arg", &[list_int], &[Value::Int64(0)]) + .expect("type.arg should be a typed builtin") + .expect("type.arg should evaluate"); + + let Value::Struct { fields, .. } = value else { + panic!("expected TypeInfo struct"); + }; + let has_secret = fields + .iter() + .find_map(|(name, value)| { + if name == "has_secret" { + Some(value) + } else { + None + } + }) + .expect("TypeInfo.has_secret field should exist"); + assert_eq!(has_secret, &Value::Bool(true)); + } + + #[test] + fn type_info_arg_loop_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "list[int64]", + "list", + None, + false, + vec![ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + )], + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let list_int = TypeExpr::Generic(ident("list"), vec![type_named("int64")], sp()); + let type_info_call = Expr::GenericCall( + Box::new(field_access(var("type"), "info")), + vec![list_int], + Vec::new(), + sp(), + ); + let args_iterable = field_access(type_info_call, "args"); + let bindings = interp + .reflected_type_info_arg_loop_bindings(&args_iterable) + .expect("type.info args loop should be recognized") + .expect("type.info args loop should produce bindings"); + + assert_eq!(bindings.len(), 1); + assert_eq!(type_expr_display(&bindings[0].ty), "string"); + } + + #[test] + fn scoped_type_facts_restore_after_return_and_error() { + let mut interp = Interpreter::new(); + let inner = Arc::new(CheckedScopedTypes { + bound_type: "int8".into(), + expressions: HashMap::from([(sp(), "int8".into())]), + ..Default::default() + }); + let outer = Arc::new(CheckedScopedTypes { + bindings: HashMap::from([(sp(), vec![inner])]), + ..Default::default() + }); + interp.active_checked_scope = Some(outer.clone()); + interp + .type_arg_scopes + .push(HashMap::from([("Outer".into(), type_named("int64"))])); + interp.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Outer".into(), + canonical_name: "int64".into(), + reflection: None, + }]; + for failing in [false, true] { + let body = if failing { + var("missing") + } else { + Expr::Binary( + Box::new(Expr::IntLiteral(127, sp())), + BinOp::Add, + Box::new(Expr::IntLiteral(1, sp())), + sp(), + ) + }; + let binding = ComptimeTypeBindStmt { + name: ident("Field"), + value: Expr::GenericCall( + Box::new(field_access(var("type"), "info")), + vec![type_named("int8")], + Vec::new(), + sp(), + ), + body: block(vec![return_stmt(body)]), + span: sp(), + }; + let result = interp.exec_comptime_type_bind(&binding); + if failing { + assert!(result.unwrap_err().contains("missing")); + } else { + assert!(matches!( + result.unwrap(), + Some(Signal::Return(Value::Typed { type_name, value }, _)) + if type_name == "int8" && *value == Value::Int64(-128) + )); + } + assert!(Arc::ptr_eq( + interp.active_checked_scope.as_ref().unwrap(), + &outer + )); + assert_eq!(interp.type_arg_scopes.len(), 1); + assert_eq!( + interp.scoped_type_bindings, + vec![ClosureScopedTypeBinding { + name: "Outer".into(), + canonical_name: "int64".into(), + reflection: None + }] + ); + } + } + + #[test] + fn comptime_type_arg_binding_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "list[int64]", + "list", + None, + false, + vec![ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + )], + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + interp.set_variable("bound_name", Value::String("unset".to_string())); + + let stmt = Stmt::ComptimeTypeBind(ComptimeTypeBindStmt { + name: ident("Item"), + value: Expr::GenericCall( + Box::new(field_access(var("type"), "arg")), + vec![TypeExpr::Generic( + ident("list"), + vec![type_named("int64")], + sp(), + )], + vec![CallArg { + name: None, + value: Expr::IntLiteral(0, sp()), + span: sp(), + }], + sp(), + ), + body: block(vec![assign( + "bound_name", + Expr::GenericCall( + Box::new(field_access(var("type"), "name")), + vec![type_named("Item")], + Vec::new(), + sp(), + ), + )]), + span: sp(), + }); + + interp.exec_stmt(&stmt).expect("binding should execute"); + assert_eq!( + interp + .get_variable("bound_name") + .expect("bound_name should be set by the comptime type body"), + &Value::String("string".to_string()) + ); + } + + #[test] + fn type_fields_uses_checked_reflection_metadata_when_available() { + let value_info = ReflectionTypeInfo::new( + "secret[string]", + "secret", + None, + true, + vec![ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + )], + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_fields( + "Box", + vec![ReflectionFieldInfo::new( + 0, + "value", + "secret[string]", + "secret", + "jsonValue", + true, + value_info, + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = interp + .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + + let Value::List(fields) = value else { + panic!("expected list of TypeField values"); + }; + assert_eq!(fields.len(), 1); + let Value::Struct { fields, .. } = &fields[0] else { + panic!("expected TypeField struct"); + }; + let serialize_name = fields + .iter() + .find_map(|(name, value)| { + if name == "serialize_name" { + Some(value) + } else { + None + } + }) + .expect("TypeField.serialize_name should exist"); + let has_secret = fields + .iter() + .find_map(|(name, value)| { + if name == "has_secret" { + Some(value) + } else { + None + } + }) + .expect("TypeField.has_secret should exist"); + assert_eq!(serialize_name, &Value::String("jsonValue".to_string())); + assert_eq!(has_secret, &Value::Bool(true)); + } + + #[test] + fn type_fields_reports_missing_checked_struct_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let err = interp + .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) + .expect("type.fields should be a typed builtin") + .expect_err("missing checked fields should be an error"); + + assert!(err.contains("missing field metadata")); + } + + #[test] + fn type_field_value_reports_missing_checked_field_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let field = + Interpreter::reflection_field_info_value("Box", None, &string_field_info(0, "value")); + let value = Value::Struct { + concrete_type: None, + type_name: "Box".to_string(), + fields: vec![("value".to_string(), Value::String("ok".to_string()))], + }; + let err = interp + .call_builtin_with_type_args( + "type.field_value", + &[type_named("Box"), type_named("string")], + &[value, field], + ) + .expect("type.field_value should be a typed builtin") + .expect_err("missing checked field metadata should be an error"); + + assert!(err.contains("missing field metadata")); + } + + #[test] + fn reflected_field_loop_uses_checked_reflection_metadata_when_available() { + let value_info = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_fields( + "Box", + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + value_info, + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let iterable = Expr::GenericCall( + Box::new(field_access(var("type"), "fields")), + vec![type_named("Box")], + Vec::new(), + sp(), + ); + let bindings = interp + .reflected_field_loop_bindings(&iterable) + .expect("type.fields loop should not error") + .expect("type.fields loop should be recognized"); + + assert_eq!(bindings.len(), 1); + assert_eq!(bindings[0].index, 0); + assert_eq!(bindings[0].name, "value"); + assert_eq!(type_expr_display(&bindings[0].ty), "string"); + } + + #[test] + fn reflected_field_loop_reports_missing_checked_field_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let iterable = Expr::GenericCall( + Box::new(field_access(var("type"), "fields")), + vec![type_named("Box")], + Vec::new(), + sp(), + ); + let err = interp + .reflected_field_loop_bindings(&iterable) + .expect_err("missing checked fields should be an error"); + + assert!(err.contains("missing field metadata")); + } + + #[test] + fn type_field_value_uses_checked_reflection_metadata_when_available() { + let value_info = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_fields( + "Box", + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + value_info, + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let field = interp + .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + let Value::List(fields) = field else { + panic!("expected list of TypeField values"); + }; + let value = Value::Struct { + concrete_type: None, + type_name: "Box".to_string(), + fields: vec![("value".to_string(), Value::String("ok".to_string()))], + }; + let reflected = interp + .call_builtin_with_type_args( + "type.field_value", + &[type_named("Box"), type_named("string")], + &[value, fields[0].clone()], + ) + .expect("type.field_value should be a typed builtin") + .expect("type.field_value should evaluate"); + + assert_eq!(reflected, Value::String("ok".to_string())); + } + + #[test] + fn type_bitfield_uses_checked_reflection_metadata_when_available() { + let int_info = ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_bitfield( + "Header", + ReflectionBitfieldInfo::new( + true, + vec![ReflectionBitfieldFieldInfo::new( + 0, "version", "bits", 3, int_info, None, + )], + ), + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let layout = interp + .call_builtin_with_type_args("type.bitfield_layout", &[type_named("Header")], &[]) + .expect("type.bitfield_layout should be a typed builtin") + .expect("type.bitfield_layout should evaluate"); + let Value::Struct { fields, .. } = layout else { + panic!("expected TypeBitfield struct"); + }; + let network_order = fields + .iter() + .find_map(|(name, value)| { + if name == "network_order" { + Some(value) + } else { + None + } + }) + .expect("TypeBitfield.network_order should exist"); + assert_eq!(network_order, &Value::Bool(true)); + + let fields = interp + .call_builtin_with_type_args("type.bitfield_fields", &[type_named("Header")], &[]) + .expect("type.bitfield_fields should be a typed builtin") + .expect("type.bitfield_fields should evaluate"); + let Value::List(fields) = fields else { + panic!("expected list of TypeBitfieldField values"); + }; + let Value::Struct { fields, .. } = &fields[0] else { + panic!("expected TypeBitfieldField struct"); + }; + let width = fields + .iter() + .find_map(|(name, value)| if name == "width" { Some(value) } else { None }) + .expect("TypeBitfieldField.width should exist"); + assert_eq!(width, &Value::Int64(3)); + } + + #[test] + fn type_bitfield_reports_missing_checked_bitfield_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Header", + "bitfield", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let err = interp + .call_builtin_with_type_args("type.bitfield_layout", &[type_named("Header")], &[]) + .expect("type.bitfield_layout should be a typed builtin") + .expect_err("missing checked bitfield layout should be an error"); + + assert!(err.contains("missing bitfield metadata")); + } + + #[test] + fn type_machine_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_machine( + "Session", + ReflectionMachineInfo::new( + vec![ + ReflectionMachineStateInfo::new(0, "guest", false, Vec::new()), + ReflectionMachineStateInfo::new( + 1, + "logged_in", + false, + vec![string_field_info(0, "user_id")], + ), + ], + vec![ReflectionMachineTransitionInfo::new( + 0, + 0, + "guest", + 1, + "logged_in", + )], + ), + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let layout = interp + .call_builtin_with_type_args("type.machine_layout", &[type_named("Session")], &[]) + .expect("type.machine_layout should be a typed builtin") + .expect("type.machine_layout should evaluate"); + let Value::Struct { fields, .. } = layout else { + panic!("expected TypeMachine struct"); + }; + let edges = fields + .iter() + .find_map(|(name, value)| if name == "edges" { Some(value) } else { None }) + .expect("TypeMachine.edges should exist"); + let Value::List(edges) = edges else { + panic!("expected TypeMachine.edges to be a list"); + }; + assert_eq!(edges.len(), 1); + + let states = interp + .call_builtin_with_type_args("type.machine_states", &[type_named("Session")], &[]) + .expect("type.machine_states should be a typed builtin") + .expect("type.machine_states should evaluate"); + let Value::List(states) = states else { + panic!("expected list of TypeMachineState values"); + }; + let Value::Struct { fields, .. } = &states[1] else { + panic!("expected TypeMachineState struct"); + }; + let state_fields = fields + .iter() + .find_map(|(name, value)| if name == "fields" { Some(value) } else { None }) + .expect("TypeMachineState.fields should exist"); + let Value::List(state_fields) = state_fields else { + panic!("expected TypeMachineState.fields to be a list"); + }; + assert_eq!(state_fields.len(), 1); + + let edges = interp + .call_builtin_with_type_args("type.machine_transitions", &[type_named("Session")], &[]) + .expect("type.machine_transitions should be a typed builtin") + .expect("type.machine_transitions should evaluate"); + let Value::List(edges) = edges else { + panic!("expected list of TypeMachineTransition values"); + }; + let Value::Struct { fields, .. } = &edges[0] else { + panic!("expected TypeMachineTransition struct"); + }; + let target = fields + .iter() + .find_map(|(name, value)| if name == "target" { Some(value) } else { None }) + .expect("TypeMachineTransition.target should exist"); + assert_eq!(target, &Value::String("logged_in".to_string())); + + let state_ty = + TypeExpr::StateQualified(Box::new(type_named("Session")), ident("logged_in"), sp()); + let layout = interp + .call_builtin_with_type_args("type.machine_layout", &[state_ty], &[]) + .expect("type.machine_layout should be a typed builtin") + .expect("state-qualified machine layout should evaluate"); + let Value::Struct { fields, .. } = layout else { + panic!("expected state-qualified TypeMachine struct"); + }; + let states = fields + .iter() + .find_map(|(name, value)| if name == "states" { Some(value) } else { None }) + .expect("TypeMachine.states should exist"); + let Value::List(states) = states else { + panic!("expected TypeMachine.states to be a list"); + }; + assert_eq!(states.len(), 2); + } + + #[test] + fn reflected_machine_field_binding_requires_state_owner_member() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_machine( + "Session", + ReflectionMachineInfo::new( + vec![ReflectionMachineStateInfo::new( + 0, + "logged_in", + false, + vec![string_field_info(0, "user_id")], + )], + Vec::new(), + ), + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let field = Interpreter::reflection_field_info_value( + "Session", + None, + &string_field_info(0, "user_id"), + ); + let err = interp + .reflected_machine_field_binding_for_value(&type_named("Session"), &field) + .expect_err("machine payload field metadata should include a state owner member"); + + assert!(err.contains("expected state payload field metadata")); + } + + #[test] + fn type_machine_reports_missing_checked_machine_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Session", + "machine", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let err = interp + .call_builtin_with_type_args("type.machine_layout", &[type_named("Session")], &[]) + .expect("type.machine_layout should be a typed builtin") + .expect_err("missing checked machine layout should be an error"); + + assert!(err.contains("missing machine metadata")); + } + + #[test] + fn type_variants_uses_checked_reflection_metadata_when_available() { + let field_info = ReflectionTypeInfo::new( + "secret[string]", + "secret", + None, + true, + vec![ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + )], + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 1, + "token", + 7, + true, + vec![ReflectionFieldInfo::new( + 0, + "value", + "secret[string]", + "secret", + "jsonValue", + true, + field_info, + )], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = interp + .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) + .expect("type.variants should be a typed builtin") + .expect("type.variants should evaluate"); + + let Value::List(variants) = value else { + panic!("expected list of TypeVariant values"); + }; + let Value::Struct { fields, .. } = &variants[0] else { + panic!("expected TypeVariant struct"); + }; + let discriminant = fields + .iter() + .find_map(|(name, value)| { + if name == "discriminant" { + Some(value) + } else { + None + } + }) + .expect("TypeVariant.discriminant should exist"); + let has_secret = fields + .iter() + .find_map(|(name, value)| { + if name == "has_secret" { + Some(value) + } else { + None + } + }) + .expect("TypeVariant.has_secret should exist"); + assert_eq!(discriminant, &Value::Int64(7)); + assert_eq!(has_secret, &Value::Bool(true)); + } + + #[test] + fn type_variants_reports_missing_checked_enum_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Shape", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let err = interp + .call_builtin_with_type_args("type.variants", &[type_named("Shape")], &[]) + .expect("type.variants should be a typed builtin") + .expect_err("missing checked enum variants should be an error"); + + assert!(err.contains("missing variant metadata")); + } + + #[test] + fn type_variant_value_reports_missing_checked_variant_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = Value::Enum { + type_name: "Choice".to_string(), + variant: "token".to_string(), + fields: Vec::new(), + }; + let err = interp + .call_builtin_with_type_args("type.variant_value", &[type_named("Choice")], &[value]) + .expect("type.variant_value should be a typed builtin") + .expect_err("missing checked variant metadata should be an error"); + + assert!(err.contains("missing variant metadata")); + } + + #[test] + fn reflected_variant_loops_use_checked_reflection_metadata_when_available() { + let field_info = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "token", + 7, + false, + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + field_info, + )], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let variant_iterable = Expr::GenericCall( + Box::new(field_access(var("type"), "variants")), + vec![type_named("Choice")], + Vec::new(), + sp(), + ); + let variant_bindings = interp + .reflected_variant_loop_bindings(&variant_iterable) + .expect("type.variants loop should not error") + .expect("type.variants loop should be recognized"); + assert_eq!(variant_bindings.len(), 1); + assert_eq!(variant_bindings[0].name, "token"); + assert_eq!(variant_bindings[0].discriminant, 7); + + let variants = interp + .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) + .expect("type.variants should be a typed builtin") + .expect("type.variants should evaluate"); + let Value::List(variants) = variants else { + panic!("expected list of TypeVariant values"); + }; + let Value::Struct { + fields: variant_fields, + .. + } = &variants[0] + else { + panic!("expected TypeVariant"); + }; + let payload_fields = variant_fields + .iter() + .find_map( + |(name, value)| { + if name == "fields" { Some(value) } else { None } + }, + ) + .expect("TypeVariant.fields should exist"); + let Value::List(payload_fields) = payload_fields else { + panic!("expected TypeVariant.fields list"); + }; + + let field_binding = interp + .reflected_variant_field_binding_for_value( + &type_named("Choice"), + Some("token"), + &payload_fields[0], + ) + .expect("payload field binding should be checked"); + assert_eq!(field_binding.index, 0); + assert_eq!(field_binding.name, "value"); + assert_eq!(type_expr_display(&field_binding.ty), "string"); + } + + #[test] + fn reflected_variant_field_binding_requires_variant_owner_member() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "token", + 7, + false, + vec![string_field_info(0, "value")], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let field = Interpreter::reflection_field_info_value( + "Choice", + None, + &string_field_info(0, "value"), + ); + let err = interp + .reflected_variant_field_binding_for_value(&type_named("Choice"), None, &field) + .expect_err("enum payload field metadata should include a variant owner member"); + + assert!(err.contains("expected payload field metadata")); + } + + #[test] + fn reflected_variant_loop_reports_missing_checked_variant_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let variant_iterable = Expr::GenericCall( + Box::new(field_access(var("type"), "variants")), + vec![type_named("Choice")], + Vec::new(), + sp(), + ); + let err = interp + .reflected_variant_loop_bindings(&variant_iterable) + .expect_err("missing checked variants should be an error"); + + assert!(err.contains("missing variant metadata")); + } + + #[test] + fn reflected_variant_field_binding_reports_missing_checked_variant_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let field = Interpreter::reflection_field_info_value( + "Choice", + Some("token"), + &string_field_info(0, "value"), + ); + let err = interp + .reflected_variant_field_binding_for_value(&type_named("Choice"), Some("token"), &field) + .expect_err("missing checked variants should be an error"); + + assert!(err.contains("missing variant metadata")); + } + + #[test] + fn type_variant_value_uses_checked_reflection_metadata_when_available() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new(0, "token", 7, false, Vec::new())], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = Value::Enum { + type_name: "Choice".to_string(), + variant: "token".to_string(), + fields: Vec::new(), + }; + let variant = interp + .call_builtin_with_type_args("type.variant_value", &[type_named("Choice")], &[value]) + .expect("type.variant_value should be a typed builtin") + .expect("type.variant_value should evaluate"); + + let Value::Struct { fields, .. } = variant else { + panic!("expected TypeVariant struct"); + }; + let discriminant = fields + .iter() + .find_map(|(name, value)| { + if name == "discriminant" { + Some(value) + } else { + None + } + }) + .expect("TypeVariant.discriminant should exist"); + assert_eq!(discriminant, &Value::Int64(7)); + } + + #[test] + fn type_variant_field_value_uses_checked_reflection_metadata_when_available() { + let field_type = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "token", + 7, + false, + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + field_type, + )], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let value = Value::Enum { + type_name: "Choice".to_string(), + variant: "token".to_string(), + fields: vec![Value::String("ok".to_string())], + }; + let variant = interp + .call_builtin_with_type_args( + "type.variant_value", + &[type_named("Choice")], + &[value.clone()], + ) + .expect("type.variant_value should be a typed builtin") + .expect("type.variant_value should evaluate"); + let Value::Struct { fields, .. } = variant else { + panic!("expected TypeVariant struct"); + }; + let field = fields + .iter() + .find_map( + |(name, value)| { + if name == "fields" { Some(value) } else { None } + }, + ) + .expect("TypeVariant.fields should exist"); + let Value::List(fields) = field else { + panic!("expected TypeVariant.fields list"); + }; + + let reflected = interp + .call_builtin_with_type_args( + "type.variant_field_value", + &[type_named("Choice"), type_named("string")], + &[value, fields[0].clone()], + ) + .expect("type.variant_field_value should be a typed builtin") + .expect("type.variant_field_value should evaluate"); + + assert_eq!(reflected, Value::String("ok".to_string())); + } + + #[test] + fn type_construct_put_and_finish_use_checked_reflection_metadata_when_available() { + let field_type = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + metadata.insert_type_fields( + "Box", + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + field_type, + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let type_fields = interp + .call_builtin_with_type_args("type.fields", &[type_named("Box")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + let Value::List(type_fields) = type_fields else { + panic!("expected list of TypeField values"); + }; + + let updated = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Box"), type_named("string")], + &[ + builder, + type_fields[0].clone(), + Value::String("ok".to_string()), + ], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(updated) = updated else { + panic!("expected successful construction update"); + }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Box")], + &[(*updated).clone()], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + let Value::ResultOk(finished) = finished else { + panic!("expected successful construction finish"); + }; + let Value::Struct { + concrete_type: _, + type_name: finished_type_name, + fields: finished_fields, + } = *finished + else { + panic!("expected constructed struct"); + }; + assert_eq!(finished_type_name, "Box"); + assert_eq!( + finished_fields, + vec![("value".to_string(), Value::String("ok".to_string()))] + ); + + let Value::TypeConstruction { + type_name, + variant, + state, + fields, + } = *updated + else { + panic!("expected TypeConstruction"); + }; + + assert_eq!(type_name, "Box"); + assert_eq!(variant, None); + assert_eq!(state, None); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].0, 0); + assert_eq!(fields[0].1, "value"); + assert_eq!(fields[0].2, "string"); + assert_eq!(fields[0].3, Value::String("ok".to_string())); + } + + #[test] + fn type_construct_put_and_finish_normalize_checked_uint64_struct_field() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Counter", + "struct", + None, + false, + Vec::new(), + )); + metadata.insert_type_fields("Counter", vec![uint64_field_info(0, "value")]); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Counter")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let type_fields = interp + .call_builtin_with_type_args("type.fields", &[type_named("Counter")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + let Value::List(type_fields) = type_fields else { + panic!("expected list of TypeField values"); + }; + + let updated = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Counter"), type_named("uint64")], + &[builder, type_fields[0].clone(), Value::Int64(42)], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(updated) = updated else { + panic!("expected successful construction update"); + }; + let Value::TypeConstruction { fields, .. } = updated.as_ref() else { + panic!("expected TypeConstruction"); + }; + assert_eq!(fields[0].3, Value::Uint64(42)); + + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Counter")], + &[(*updated).clone()], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + let Value::ResultOk(finished) = finished else { + panic!("expected successful construction finish"); + }; + + assert_eq!( + *finished, + Value::Struct { + concrete_type: None, + type_name: "Counter".to_string(), + fields: vec![("value".to_string(), Value::Uint64(42))], + } + ); + } + + #[test] + fn type_construct_enum_payload_and_finish_use_checked_reflection_metadata_when_available() { + let field_type = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "token", + 7, + false, + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + field_type, + )], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let variants = interp + .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) + .expect("type.variants should be a typed builtin") + .expect("type.variants should evaluate"); + let Value::List(variants) = variants else { + panic!("expected list of TypeVariant values"); + }; + let Value::Struct { + fields: variant_fields, + .. + } = &variants[0] + else { + panic!("expected TypeVariant"); + }; + let payload_fields = variant_fields + .iter() + .find_map( + |(name, value)| { + if name == "fields" { Some(value) } else { None } + }, + ) + .expect("TypeVariant.fields should exist"); + let Value::List(payload_fields) = payload_fields else { + panic!("expected TypeVariant.fields list"); + }; + + let builder = interp + .call_builtin_with_type_args( + "type.construct_variant_start", + &[type_named("Choice")], + &[variants[0].clone()], + ) + .expect("type.construct_variant_start should be a typed builtin") + .expect("type.construct_variant_start should evaluate"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful enum construction start"); + }; + + let updated = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Choice"), type_named("string")], + &[ + *builder, + payload_fields[0].clone(), + Value::String("ok".to_string()), + ], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(updated) = updated else { + panic!("expected successful enum payload update"); + }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Choice")], + &[(*updated).clone()], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + let Value::ResultOk(finished) = finished else { + panic!("expected successful enum construction finish"); + }; + let Value::Enum { + type_name: finished_type_name, + variant: finished_variant, + fields: finished_fields, + } = *finished + else { + panic!("expected constructed enum"); + }; + assert_eq!(finished_type_name, "Choice"); + assert_eq!(finished_variant, "token"); + assert_eq!(finished_fields, vec![Value::String("ok".to_string())]); + + let Value::TypeConstruction { + type_name, + variant, + state, + fields, + } = *updated + else { + panic!("expected TypeConstruction"); + }; + + assert_eq!(type_name, "Choice"); + assert_eq!(variant, Some("token".to_string())); + assert_eq!(state, None); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].0, 0); + assert_eq!(fields[0].1, "value"); + assert_eq!(fields[0].2, "string"); + assert_eq!(fields[0].3, Value::String("ok".to_string())); + } + + #[test] + fn type_construct_put_and_finish_normalize_checked_uint64_enum_payload() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + metadata.insert_type_variants( + "Choice", + vec![ReflectionVariantInfo::new( + 0, + "token", + 7, + false, + vec![uint64_field_info(0, "value")], + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let variants = interp + .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) + .expect("type.variants should be a typed builtin") + .expect("type.variants should evaluate"); + let Value::List(variants) = variants else { + panic!("expected list of TypeVariant values"); + }; + let Value::Struct { + fields: variant_fields, + .. + } = &variants[0] + else { + panic!("expected TypeVariant"); + }; + let payload_fields = variant_fields + .iter() + .find_map( + |(name, value)| { + if name == "fields" { Some(value) } else { None } + }, + ) + .expect("TypeVariant.fields should exist"); + let Value::List(payload_fields) = payload_fields else { + panic!("expected TypeVariant.fields list"); + }; + + let builder = interp + .call_builtin_with_type_args( + "type.construct_variant_start", + &[type_named("Choice")], + &[variants[0].clone()], + ) + .expect("type.construct_variant_start should be a typed builtin") + .expect("type.construct_variant_start should evaluate"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful enum construction start"); + }; + let updated = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Choice"), type_named("uint64")], + &[*builder, payload_fields[0].clone(), Value::Int64(42)], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(updated) = updated else { + panic!("expected successful enum payload update"); + }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Choice")], + &[(*updated).clone()], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + let Value::ResultOk(finished) = finished else { + panic!("expected successful enum construction finish"); + }; + + assert_eq!( + *finished, + Value::Enum { + type_name: "Choice".to_string(), + variant: "token".to_string(), + fields: vec![Value::Uint64(42)], + } + ); + } + + #[test] + fn type_construct_finish_checked_struct_reports_missing_field() { + let field_type = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), + ); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + metadata.insert_type_fields( + "Box", + vec![ReflectionFieldInfo::new( + 0, + "value", + "string", + "primitive", + "value", + false, + field_type, + )], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let finished = interp + .call_builtin_with_type_args("type.construct_finish", &[type_named("Box")], &[builder]) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_fail( + "type.construct_finish: 'Box' is missing required field 'value'".to_string() + ) + ); + } + + #[test] + fn type_construct_put_reports_missing_checked_field_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let field = + Interpreter::reflection_field_info_value("Box", None, &string_field_info(0, "value")); + let updated = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Box"), type_named("string")], + &[builder, field, Value::String("ok".to_string())], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + + assert_eq!( + updated, + result_fail( + "checked reflection metadata for type 'Box' is missing field metadata".to_string() + ) + ); + } + + #[test] + fn type_construct_finish_reports_missing_checked_field_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Box", + "struct", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let finished = interp + .call_builtin_with_type_args("type.construct_finish", &[type_named("Box")], &[builder]) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_fail( + "checked reflection metadata for type 'Box' is missing field metadata".to_string() + ) + ); + } + + #[test] + fn type_construct_finish_checked_enum_reports_missing_payload_field() { + let field_type = ReflectionTypeInfo::new( + "string", + "primitive", + Some("string_type".to_string()), + false, + Vec::new(), ); let mut metadata = ReflectionMetadata::new(); metadata.insert_type_info(ReflectionTypeInfo::new( @@ -13276,1631 +22048,1707 @@ mod tests { let Value::List(variants) = variants else { panic!("expected list of TypeVariant values"); }; - let Value::Struct { - fields: variant_fields, - .. - } = &variants[0] - else { - panic!("expected TypeVariant"); + let builder = interp + .call_builtin_with_type_args( + "type.construct_variant_start", + &[type_named("Choice")], + &[variants[0].clone()], + ) + .expect("type.construct_variant_start should be a typed builtin") + .expect("type.construct_variant_start should evaluate"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful enum construction start"); }; - let payload_fields = variant_fields - .iter() - .find_map( - |(name, value)| { - if name == "fields" { Some(value) } else { None } - }, + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Choice")], + &[*builder], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_fail( + "type.construct_finish: variant 'Choice.token' is missing required payload field 'value'" + .to_string() + ) + ); + } + + #[test] + fn type_construct_variant_start_reports_missing_checked_variant_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let variant = Interpreter::reflection_variant_info_value( + "Choice", + &ReflectionVariantInfo::new(0, "token", 7, false, Vec::new()), + ); + let builder = interp + .call_builtin_with_type_args( + "type.construct_variant_start", + &[type_named("Choice")], + &[variant], + ) + .expect("type.construct_variant_start should be a typed builtin") + .expect("type.construct_variant_start should evaluate"); + + assert_eq!( + builder, + result_fail( + "checked reflection metadata for type 'Choice' is missing variant metadata" + .to_string() + ) + ); + } + + #[test] + fn type_construct_finish_reports_missing_checked_variant_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Choice", + "enum", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = Value::TypeConstruction { + type_name: "Choice".to_string(), + variant: Some("token".to_string()), + state: None, + fields: Vec::new(), + }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Choice")], + &[builder], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_fail( + "checked reflection metadata for type 'Choice' is missing variant metadata" + .to_string() + ) + ); + } + + #[test] + fn type_construct_finish_reports_missing_checked_bitfield_metadata() { + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Header", + "bitfield", + None, + false, + Vec::new(), + )); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Header")], + &[builder], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_fail( + "checked reflection metadata for type 'Header' is missing bitfield metadata" + .to_string() + ) + ); + } + + #[test] + fn type_construct_bitfield_finish_uses_checked_reflection_metadata_when_available() { + let int_info = ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".to_string()), + false, + Vec::new(), + ); + let protocol_info = ReflectionTypeInfo::new("IpProtocol", "enum", None, false, Vec::new()); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Header", + "bitfield", + None, + false, + Vec::new(), + )); + metadata.insert_type_fields( + "Header", + vec![ + ReflectionFieldInfo::new( + 0, + "version", + "int64", + "primitive", + "version", + false, + int_info.clone(), + ), + ReflectionFieldInfo::new( + 1, + "protocol", + "IpProtocol", + "enum", + "protocol", + false, + protocol_info.clone(), + ), + ], + ); + metadata.insert_bitfield( + "Header", + ReflectionBitfieldInfo::new( + true, + vec![ + ReflectionBitfieldFieldInfo::new(0, "version", "bits", 4, int_info, None), + ReflectionBitfieldFieldInfo::new( + 1, + "protocol", + "bits", + 8, + protocol_info.clone(), + Some(protocol_info), + ), + ], + ), + ); + metadata.insert_type_variants( + "IpProtocol", + vec![ReflectionVariantInfo::new(0, "tcp", 6, false, Vec::new())], + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let type_fields = interp + .call_builtin_with_type_args("type.fields", &[type_named("Header")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + let Value::List(type_fields) = type_fields else { + panic!("expected list of TypeField values"); + }; + let protocol = Value::Enum { + type_name: "IpProtocol".to_string(), + variant: "tcp".to_string(), + fields: Vec::new(), + }; + + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); + let builder = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Header"), type_named("int64")], + &[builder, type_fields[0].clone(), Value::Int64(4)], + ) + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful version field update"); + }; + let builder = interp + .call_builtin_with_type_args( + "type.construct_put", + &[type_named("Header"), type_named("IpProtocol")], + &[(*builder).clone(), type_fields[1].clone(), protocol.clone()], ) - .expect("TypeVariant.fields should exist"); - let Value::List(payload_fields) = payload_fields else { - panic!("expected TypeVariant.fields list"); + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful protocol field update"); }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Header")], + &[(*builder).clone()], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_ok(Value::Struct { + concrete_type: None, + type_name: "Header".to_string(), + fields: vec![ + ("version".to_string(), Value::Int64(4)), + ("protocol".to_string(), protocol.clone()), + ], + }) + ); + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); let builder = interp .call_builtin_with_type_args( - "type.construct_variant_start", - &[type_named("Choice")], - &[variants[0].clone()], + "type.construct_put", + &[type_named("Header"), type_named("int64")], + &[builder, type_fields[0].clone(), Value::Int64(16)], ) - .expect("type.construct_variant_start should be a typed builtin") - .expect("type.construct_variant_start should evaluate"); + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); let Value::ResultOk(builder) = builder else { - panic!("expected successful enum construction start"); + panic!("expected successful wide version field update"); }; - - let updated = interp + let builder = interp .call_builtin_with_type_args( "type.construct_put", - &[type_named("Choice"), type_named("string")], - &[ - *builder, - payload_fields[0].clone(), - Value::String("ok".to_string()), - ], + &[type_named("Header"), type_named("IpProtocol")], + &[(*builder).clone(), type_fields[1].clone(), protocol], ) .expect("type.construct_put should be a typed builtin") .expect("type.construct_put should evaluate"); - let Value::ResultOk(updated) = updated else { - panic!("expected successful enum payload update"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful wide protocol field update"); }; let finished = interp .call_builtin_with_type_args( "type.construct_finish", - &[type_named("Choice")], - &[(*updated).clone()], + &[type_named("Header")], + &[*builder], ) .expect("type.construct_finish should be a typed builtin") .expect("type.construct_finish should evaluate"); - let Value::ResultOk(finished) = finished else { - panic!("expected successful enum construction finish"); - }; - let Value::Enum { - type_name: finished_type_name, - variant: finished_variant, - fields: finished_fields, - } = *finished - else { - panic!("expected constructed enum"); - }; - assert_eq!(finished_type_name, "Choice"); - assert_eq!(finished_variant, "token"); - assert_eq!(finished_fields, vec![Value::String("ok".to_string())]); - - let Value::TypeConstruction { - type_name, - variant, - state, - fields, - } = *updated - else { - panic!("expected TypeConstruction"); - }; - assert_eq!(type_name, "Choice"); - assert_eq!(variant, Some("token".to_string())); - assert_eq!(state, None); - assert_eq!(fields.len(), 1); - assert_eq!(fields[0].0, 0); - assert_eq!(fields[0].1, "value"); - assert_eq!(fields[0].2, "string"); - assert_eq!(fields[0].3, Value::String("ok".to_string())); + assert_eq!( + finished, + result_fail( + "bitfield 'Header' field 'version' is 4 bit(s) wide and cannot hold '16'" + .to_string() + ) + ); } #[test] - fn type_construct_put_and_finish_normalize_checked_uint64_enum_payload() { + fn type_construct_bitfield_finish_normalizes_checked_uint64_bits() { let mut metadata = ReflectionMetadata::new(); metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", + "Header", + "bitfield", None, false, Vec::new(), )); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( + metadata.insert_bitfield( + "Header", + ReflectionBitfieldInfo::new( + true, + vec![ReflectionBitfieldFieldInfo::new( + 0, + "wide", + "bits", + 64, + uint64_type_info(), + None, + )], + ), + ); + + let mut interp = Interpreter::new(); + interp.set_reflection_metadata(Arc::new(metadata)); + + let builder = Value::TypeConstruction { + type_name: "Header".to_string(), + variant: None, + state: None, + fields: vec![( 0, - "token", - 7, - false, - vec![uint64_field_info(0, "value")], + "wide".to_string(), + "uint64".to_string(), + Value::Int64(42), )], + }; + let finished = interp + .call_builtin_with_type_args( + "type.construct_finish", + &[type_named("Header")], + &[builder], + ) + .expect("type.construct_finish should be a typed builtin") + .expect("type.construct_finish should evaluate"); + + assert_eq!( + finished, + result_ok(Value::Struct { + concrete_type: None, + type_name: "Header".to_string(), + fields: vec![("wide".to_string(), Value::Uint64(42))], + }) + ); + } + + #[test] + fn type_construct_bitfield_finish_requires_checked_enum_variant_metadata() { + let int_info = ReflectionTypeInfo::new( + "int64", + "primitive", + Some("int64_type".to_string()), + false, + Vec::new(), + ); + let protocol_info = ReflectionTypeInfo::new("IpProtocol", "enum", None, false, Vec::new()); + let mut metadata = ReflectionMetadata::new(); + metadata.insert_type_info(ReflectionTypeInfo::new( + "Header", + "bitfield", + None, + false, + Vec::new(), + )); + metadata.insert_type_info(protocol_info.clone()); + metadata.insert_type_fields( + "Header", + vec![ + ReflectionFieldInfo::new( + 0, + "version", + "int64", + "primitive", + "version", + false, + int_info.clone(), + ), + ReflectionFieldInfo::new( + 1, + "protocol", + "IpProtocol", + "enum", + "protocol", + false, + protocol_info.clone(), + ), + ], + ); + metadata.insert_bitfield( + "Header", + ReflectionBitfieldInfo::new( + true, + vec![ + ReflectionBitfieldFieldInfo::new(0, "version", "bits", 4, int_info, None), + ReflectionBitfieldFieldInfo::new( + 1, + "protocol", + "bits", + 8, + protocol_info.clone(), + Some(protocol_info), + ), + ], + ), ); let mut interp = Interpreter::new(); interp.set_reflection_metadata(Arc::new(metadata)); - let variants = interp - .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) - .expect("type.variants should be a typed builtin") - .expect("type.variants should evaluate"); - let Value::List(variants) = variants else { - panic!("expected list of TypeVariant values"); - }; - let Value::Struct { - fields: variant_fields, - .. - } = &variants[0] - else { - panic!("expected TypeVariant"); + let type_fields = interp + .call_builtin_with_type_args("type.fields", &[type_named("Header")], &[]) + .expect("type.fields should be a typed builtin") + .expect("type.fields should evaluate"); + let Value::List(type_fields) = type_fields else { + panic!("expected list of TypeField values"); }; - let payload_fields = variant_fields - .iter() - .find_map( - |(name, value)| { - if name == "fields" { Some(value) } else { None } - }, - ) - .expect("TypeVariant.fields should exist"); - let Value::List(payload_fields) = payload_fields else { - panic!("expected TypeVariant.fields list"); + let protocol = Value::Enum { + type_name: "IpProtocol".to_string(), + variant: "tcp".to_string(), + fields: Vec::new(), }; + let builder = interp + .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) + .expect("type.construct_start should be a typed builtin") + .expect("type.construct_start should evaluate"); let builder = interp .call_builtin_with_type_args( - "type.construct_variant_start", - &[type_named("Choice")], - &[variants[0].clone()], + "type.construct_put", + &[type_named("Header"), type_named("int64")], + &[builder, type_fields[0].clone(), Value::Int64(4)], ) - .expect("type.construct_variant_start should be a typed builtin") - .expect("type.construct_variant_start should evaluate"); + .expect("type.construct_put should be a typed builtin") + .expect("type.construct_put should evaluate"); let Value::ResultOk(builder) = builder else { - panic!("expected successful enum construction start"); + panic!("expected successful version field update"); }; - let updated = interp + let builder = interp .call_builtin_with_type_args( "type.construct_put", - &[type_named("Choice"), type_named("uint64")], - &[*builder, payload_fields[0].clone(), Value::Int64(42)], + &[type_named("Header"), type_named("IpProtocol")], + &[(*builder).clone(), type_fields[1].clone(), protocol], ) .expect("type.construct_put should be a typed builtin") .expect("type.construct_put should evaluate"); - let Value::ResultOk(updated) = updated else { - panic!("expected successful enum payload update"); + let Value::ResultOk(builder) = builder else { + panic!("expected successful protocol field update"); }; let finished = interp .call_builtin_with_type_args( "type.construct_finish", - &[type_named("Choice")], - &[(*updated).clone()], + &[type_named("Header")], + &[*builder], ) .expect("type.construct_finish should be a typed builtin") .expect("type.construct_finish should evaluate"); - let Value::ResultOk(finished) = finished else { - panic!("expected successful enum construction finish"); - }; assert_eq!( - *finished, - Value::Enum { - type_name: "Choice".to_string(), - variant: "token".to_string(), - fields: vec![Value::Uint64(42)], - } + finished, + result_fail( + "checked reflection metadata for type 'IpProtocol' is missing variant metadata" + .to_string() + ) ); } - #[test] - fn type_construct_finish_checked_struct_reports_missing_field() { - let field_type = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); - metadata.insert_type_fields( - "Box", - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - field_type, - )], - ); + /// Helper: create a field access expression. + fn field_access(base: Expr, field: &str) -> Expr { + Expr::FieldAccess(Box::new(base), ident(field), sp()) + } - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + /// Helper: create a named call argument. + fn named_arg(name: &str, value: Expr) -> CallArg { + CallArg { + name: Some(ident(name)), + value, + span: sp(), + } + } - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let finished = interp - .call_builtin_with_type_args("type.construct_finish", &[type_named("Box")], &[builder]) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + /// Helper: create a dotted call expression like `Point.total(view point)`. + fn dotted_call(module: &str, func_name: &str, args: Vec) -> Expr { + Expr::Call( + Box::new(field_access(var(module), func_name)), + args.into_iter() + .map(|value| CallArg { + name: None, + value, + span: sp(), + }) + .collect(), + sp(), + ) + } + + /// Helper: create a simple struct definition. + fn struct_def(name: &str, fields: Vec<(&str, &str)>, methods: Vec) -> StructDef { + StructDef { + name: ident(name), + type_params: vec![], + fields: fields + .into_iter() + .map(|(field_name, field_ty)| FieldDef { + name: ident(field_name), + ty: type_named(field_ty), + serialize_name: None, + span: sp(), + }) + .collect(), + methods, + exported: false, + span: sp(), + } + } + + fn machine_def( + name: &str, + states: Vec<(&str, Vec<(&str, TypeExpr)>)>, + transitions: Vec<(&str, &str)>, + ) -> MachineDef { + MachineDef { + name: ident(name), + exported: false, + states: states + .into_iter() + .map(|(state_name, fields)| jett_parser::ast::MachineState { + name: ident(state_name), + fields: fields + .into_iter() + .map(|(field_name, ty)| FieldDef { + name: ident(field_name), + ty, + serialize_name: None, + span: sp(), + }) + .collect(), + span: sp(), + }) + .collect(), + transitions: transitions + .into_iter() + .map(|(from, to)| jett_parser::ast::MachineTransition { + from: ident(from), + to: ident(to), + span: sp(), + }) + .collect(), + span: sp(), + } + } + + fn enum_def_with_values(name: &str, variants: Vec<(&str, i64)>) -> EnumDef { + EnumDef { + name: ident(name), + variants: variants + .into_iter() + .map(|(variant_name, discriminant)| jett_parser::ast::Variant { + name: ident(variant_name), + fields: vec![], + discriminant: Some(discriminant), + span: sp(), + }) + .collect(), + exported: false, + span: sp(), + } + } + + fn enum_def_with_field( + name: &str, + variant_name: &str, + field_name: &str, + field_ty: &str, + ) -> EnumDef { + EnumDef { + name: ident(name), + variants: vec![jett_parser::ast::Variant { + name: ident(variant_name), + fields: vec![FieldDef { + name: ident(field_name), + ty: type_named(field_ty), + serialize_name: None, + span: sp(), + }], + discriminant: None, + span: sp(), + }], + exported: false, + span: sp(), + } + } + + /// Helper: create a simple bitfield definition. + fn bitfield_def( + name: &str, + fields: Vec<(&str, BitfieldFieldKind)>, + network_order: bool, + ) -> BitfieldDef { + BitfieldDef { + name: ident(name), + network_order, + fields: fields + .into_iter() + .map(|(field_name, kind)| jett_parser::ast::BitfieldFieldDef { + name: ident(field_name), + kind, + span: sp(), + }) + .collect(), + exported: false, + span: sp(), + } + } + + fn interface_decl( + name: &str, + methods: Vec<(&str, Vec<(&str, &str, bool)>, &str)>, + ) -> InterfaceDecl { + InterfaceDecl { + name: ident(name), + methods: methods + .into_iter() + .map(|(method_name, params, return_type)| FunctionDecl { + name: ident(method_name), + type_params: vec![], + params: params + .into_iter() + .map(|(param_name, param_ty, view)| Param { + view, + mutable: false, + name: ident(param_name), + ty: type_named(param_ty), + span: sp(), + }) + .collect(), + return_type: Some(type_named(return_type)), + exported: false, + span: sp(), + }) + .collect(), + exported: false, + span: sp(), + } + } - assert_eq!( - finished, - result_fail( - "type.construct_finish: 'Box' is missing required field 'value'".to_string() - ) - ); + fn implement_block( + interface_name: &str, + for_type: &str, + methods: Vec, + ) -> ImplementBlock { + ImplementBlock { + interface_name: ident(interface_name), + for_type: type_named(for_type), + methods, + span: sp(), + } } - #[test] - fn type_construct_put_reports_missing_checked_field_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); + // ----------------------------------------------------------------------- + // Arithmetic + // ----------------------------------------------------------------------- + #[test] + fn eval_integer_addition() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let field = - Interpreter::reflection_field_info_value("Box", None, &string_field_info(0, "value")); - let updated = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Box"), type_named("string")], - &[builder, field, Value::String("ok".to_string())], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - - assert_eq!( - updated, - result_fail( - "checked reflection metadata for type 'Box' is missing field metadata".to_string() - ) - ); + let expr = binary(int(2), BinOp::Add, int(3)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(5)); } #[test] - fn type_construct_finish_reports_missing_checked_field_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Box", - "struct", - None, - false, - Vec::new(), - )); - + fn eval_integer_subtraction() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Box")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let finished = interp - .call_builtin_with_type_args("type.construct_finish", &[type_named("Box")], &[builder]) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); - - assert_eq!( - finished, - result_fail( - "checked reflection metadata for type 'Box' is missing field metadata".to_string() - ) - ); + let expr = binary(int(10), BinOp::Sub, int(4)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(6)); } #[test] - fn type_construct_finish_checked_enum_reports_missing_payload_field() { - let field_type = ReflectionTypeInfo::new( - "string", - "primitive", - Some("string_type".to_string()), - false, - Vec::new(), - ); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, - false, - Vec::new(), - )); - metadata.insert_type_variants( - "Choice", - vec![ReflectionVariantInfo::new( - 0, - "token", - 7, - false, - vec![ReflectionFieldInfo::new( - 0, - "value", - "string", - "primitive", - "value", - false, - field_type, - )], - )], - ); - + fn eval_integer_multiplication() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let variants = interp - .call_builtin_with_type_args("type.variants", &[type_named("Choice")], &[]) - .expect("type.variants should be a typed builtin") - .expect("type.variants should evaluate"); - let Value::List(variants) = variants else { - panic!("expected list of TypeVariant values"); - }; - let builder = interp - .call_builtin_with_type_args( - "type.construct_variant_start", - &[type_named("Choice")], - &[variants[0].clone()], - ) - .expect("type.construct_variant_start should be a typed builtin") - .expect("type.construct_variant_start should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful enum construction start"); - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Choice")], - &[*builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); - - assert_eq!( - finished, - result_fail( - "type.construct_finish: variant 'Choice.token' is missing required payload field 'value'" - .to_string() - ) - ); + let expr = binary(int(3), BinOp::Mul, int(7)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(21)); } #[test] - fn type_construct_variant_start_reports_missing_checked_variant_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, - false, - Vec::new(), - )); - + fn eval_integer_division() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let variant = Interpreter::reflection_variant_info_value( - "Choice", - &ReflectionVariantInfo::new(0, "token", 7, false, Vec::new()), - ); - let builder = interp - .call_builtin_with_type_args( - "type.construct_variant_start", - &[type_named("Choice")], - &[variant], - ) - .expect("type.construct_variant_start should be a typed builtin") - .expect("type.construct_variant_start should evaluate"); + let expr = binary(int(15), BinOp::Div, int(3)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(5)); + } - assert_eq!( - builder, - result_fail( - "checked reflection metadata for type 'Choice' is missing variant metadata" - .to_string() - ) - ); + #[test] + fn eval_integer_modulo() { + let mut interp = Interpreter::new(); + let expr = binary(int(17), BinOp::Modulo, int(5)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(2)); } #[test] - fn type_construct_finish_reports_missing_checked_variant_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Choice", - "enum", - None, - false, - Vec::new(), - )); + fn eval_float_addition() { + let mut interp = Interpreter::new(); + let expr = binary(float(1.5), BinOp::Add, float(2.5)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Float64(4.0)); + } + #[test] + fn eval_complex_arithmetic() { + // (2 + 3) * 4 == 20 let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); + let sum = binary(int(2), BinOp::Add, int(3)); + let product = binary(sum, BinOp::Mul, int(4)); + assert_eq!(interp.eval_expr(&product).unwrap(), Value::Int64(20)); + } - let builder = Value::TypeConstruction { - type_name: "Choice".to_string(), - variant: Some("token".to_string()), - state: None, - fields: Vec::new(), - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Choice")], - &[builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + #[test] + fn eval_division_by_zero() { + let mut interp = Interpreter::new(); + let expr = binary(int(10), BinOp::Div, int(0)); + assert!(interp.eval_expr(&expr).is_err()); + } - assert_eq!( - finished, - result_fail( - "checked reflection metadata for type 'Choice' is missing variant metadata" - .to_string() - ) - ); + #[test] + fn eval_integer_division_wraps_overflow() { + let mut interp = Interpreter::new(); + let expr = binary(int(i64::MIN), BinOp::Div, int(-1)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(i64::MIN)); } #[test] - fn type_construct_finish_reports_missing_checked_bitfield_metadata() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Header", - "bitfield", - None, - false, - Vec::new(), - )); - + fn eval_integer_modulo_wraps_overflow() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Header")], - &[builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + let expr = binary(int(i64::MIN), BinOp::Modulo, int(-1)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(0)); + } + #[test] + fn normalize_float32_alias_preserves_only_binary32_precision() { + let mut interp = Interpreter::new(); + interp.register_type_alias(&type_alias("SmallFloat", "float32", None)); + interp.register_type_alias(&type_alias("Reading", "SmallFloat", None)); + for type_name in ["float32", "SmallFloat", "Reading", "secret[float32]"] { + assert_eq!( + interp.normalize_value_for_type_name(type_name, Value::Float64(0.1)), + Ok(Value::Float64(0.1_f32 as f64)) + ); + } assert_eq!( - finished, - result_fail( - "checked reflection metadata for type 'Header' is missing bitfield metadata" - .to_string() - ) + interp.normalize_value_for_type_name("float64", Value::Float64(0.1)), + Ok(Value::Float64(0.1)) ); } #[test] - fn type_construct_bitfield_finish_uses_checked_reflection_metadata_when_available() { - let int_info = ReflectionTypeInfo::new( - "int64", - "primitive", - Some("int64_type".to_string()), - false, - Vec::new(), + fn float32_declared_boundaries_normalize_without_checked_expression_map() { + let mut interp = Interpreter::new(); + interp + .exec_stmt(&typed_var_decl("float32", "x", float(0.1))) + .unwrap(); + assert_eq!( + interp.eval_expr(&var("x")).unwrap(), + Value::Float64(0.1_f32 as f64) ); - let protocol_info = ReflectionTypeInfo::new("IpProtocol", "enum", None, false, Vec::new()); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Header", - "bitfield", - None, - false, - Vec::new(), - )); - metadata.insert_type_fields( - "Header", - vec![ - ReflectionFieldInfo::new( - 0, - "version", - "int64", - "primitive", - "version", - false, - int_info.clone(), - ), - ReflectionFieldInfo::new( - 1, - "protocol", - "IpProtocol", - "enum", - "protocol", - false, - protocol_info.clone(), - ), - ], + interp.exec_stmt(&assign("x", float(0.2))).unwrap(); + assert_eq!( + interp.eval_expr(&var("x")).unwrap(), + Value::Float64(0.2_f32 as f64) ); - metadata.insert_bitfield( - "Header", - ReflectionBitfieldInfo::new( - true, - vec![ - ReflectionBitfieldFieldInfo::new(0, "version", "bits", 4, int_info, None), - ReflectionBitfieldFieldInfo::new( - 1, - "protocol", - "bits", - 8, - protocol_info.clone(), - Some(protocol_info), - ), - ], - ), + let mut echo = func_def( + "echo_f32", + vec![("value", "float32")], + block(vec![return_stmt(var("value"))]), ); - metadata.insert_type_variants( - "IpProtocol", - vec![ReflectionVariantInfo::new(0, "tcp", 6, false, Vec::new())], + echo.return_type = Some(type_named("float32")); + interp.register_function(&echo); + assert_eq!( + interp + .call_function("echo_f32", vec![Value::Float64(0.1)]) + .unwrap(), + Value::Float64(0.1_f32 as f64) ); - - let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let type_fields = interp - .call_builtin_with_type_args("type.fields", &[type_named("Header")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - let Value::List(type_fields) = type_fields else { - panic!("expected list of TypeField values"); - }; - let protocol = Value::Enum { - type_name: "IpProtocol".to_string(), - variant: "tcp".to_string(), - fields: Vec::new(), - }; - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("int64")], - &[builder, type_fields[0].clone(), Value::Int64(4)], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful version field update"); - }; - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("IpProtocol")], - &[(*builder).clone(), type_fields[1].clone(), protocol.clone()], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful protocol field update"); - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Header")], - &[(*builder).clone()], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); - + let mut literal = func_def("literal_f32", vec![], block(vec![return_stmt(float(0.1))])); + literal.return_type = Some(type_named("float32")); + interp.register_function(&literal); assert_eq!( - finished, - result_ok(Value::Struct { - type_name: "Header".to_string(), - fields: vec![ - ("version".to_string(), Value::Int64(4)), - ("protocol".to_string(), protocol.clone()), - ], - }) + interp.call_function("literal_f32", vec![]).unwrap(), + Value::Float64(0.1_f32 as f64) ); + } - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("int64")], - &[builder, type_fields[0].clone(), Value::Int64(16)], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful wide version field update"); - }; - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("IpProtocol")], - &[(*builder).clone(), type_fields[1].clone(), protocol], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful wide protocol field update"); - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Header")], - &[*builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + #[test] + fn normalize_sized_integers_accepts_declared_boundaries() { + let interp = Interpreter::new(); + let cases = [ + ("int8", i8::MIN.into(), i8::MAX.into()), + ("int16", i16::MIN.into(), i16::MAX.into()), + ("int32", i32::MIN.into(), i32::MAX.into()), + ("uint8", 0, u8::MAX.into()), + ("uint16", 0, u16::MAX.into()), + ("uint32", 0, u32::MAX.into()), + ]; + + for (type_name, min, max) in cases { + assert_eq!( + interp.normalize_value_for_type_name(type_name, Value::Int64(min)), + Ok(Value::Int64(min)) + ); + assert_eq!( + interp.normalize_value_for_type_name(type_name, Value::Int64(max)), + Ok(Value::Int64(max)) + ); + } + } + + #[test] + fn normalize_sized_integers_rejects_values_outside_declared_boundaries() { + let interp = Interpreter::new(); + let cases = [ + ("int8", -129, "int8 value -129 is outside range -128..127"), + ("int8", 128, "int8 value 128 is outside range -128..127"), + ( + "int16", + -32769, + "int16 value -32769 is outside range -32768..32767", + ), + ( + "int16", + 32768, + "int16 value 32768 is outside range -32768..32767", + ), + ( + "int32", + -2147483649, + "int32 value -2147483649 is outside range -2147483648..2147483647", + ), + ( + "int32", + 2147483648, + "int32 value 2147483648 is outside range -2147483648..2147483647", + ), + ("uint8", -1, "uint8 value -1 is outside range 0..255"), + ("uint8", 256, "uint8 value 256 is outside range 0..255"), + ("uint16", -1, "uint16 value -1 is outside range 0..65535"), + ( + "uint16", + 65536, + "uint16 value 65536 is outside range 0..65535", + ), + ( + "uint32", + -1, + "uint32 value -1 is outside range 0..4294967295", + ), + ( + "uint32", + 4294967296, + "uint32 value 4294967296 is outside range 0..4294967295", + ), + ]; - assert_eq!( - finished, - result_fail( - "bitfield 'Header' field 'version' is 4 bit(s) wide and cannot hold '16'" - .to_string() - ) - ); + for (type_name, value, expected) in cases { + assert_eq!( + interp + .normalize_value_for_type_name(type_name, Value::Int64(value)) + .unwrap_err(), + expected + ); + } } #[test] - fn type_construct_bitfield_finish_normalizes_checked_uint64_bits() { - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Header", - "bitfield", - None, - false, - Vec::new(), - )); - metadata.insert_bitfield( - "Header", - ReflectionBitfieldInfo::new( - true, - vec![ReflectionBitfieldFieldInfo::new( - 0, - "wide", - "bits", - 64, - uint64_type_info(), - None, - )], - ), - ); - + fn normalize_sized_integer_aliases_uses_primitive_boundary() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let builder = Value::TypeConstruction { - type_name: "Header".to_string(), - variant: None, - state: None, - fields: vec![( - 0, - "wide".to_string(), - "uint64".to_string(), - Value::Int64(42), - )], - }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Header")], - &[builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + interp.register_type_alias(&type_alias("Byte", "uint8", None)); + interp.register_type_alias(&type_alias("Count", "Byte", None)); assert_eq!( - finished, - result_ok(Value::Struct { - type_name: "Header".to_string(), - fields: vec![("wide".to_string(), Value::Uint64(42))], - }) + interp.normalize_value_for_type_name("Count", Value::Int64(255)), + Ok(Value::Int64(255)) + ); + assert_eq!( + interp + .normalize_value_for_type_name("Count", Value::Int64(256)) + .unwrap_err(), + "uint8 value 256 is outside range 0..255" ); } #[test] - fn type_construct_bitfield_finish_requires_checked_enum_variant_metadata() { - let int_info = ReflectionTypeInfo::new( - "int64", - "primitive", - Some("int64_type".to_string()), - false, - Vec::new(), - ); - let protocol_info = ReflectionTypeInfo::new("IpProtocol", "enum", None, false, Vec::new()); - let mut metadata = ReflectionMetadata::new(); - metadata.insert_type_info(ReflectionTypeInfo::new( - "Header", - "bitfield", - None, - false, - Vec::new(), - )); - metadata.insert_type_info(protocol_info.clone()); - metadata.insert_type_fields( - "Header", - vec![ - ReflectionFieldInfo::new( - 0, - "version", - "int64", - "primitive", - "version", - false, - int_info.clone(), - ), - ReflectionFieldInfo::new( - 1, - "protocol", - "IpProtocol", - "enum", - "protocol", - false, - protocol_info.clone(), - ), - ], - ); - metadata.insert_bitfield( - "Header", - ReflectionBitfieldInfo::new( - true, - vec![ - ReflectionBitfieldFieldInfo::new(0, "version", "bits", 4, int_info, None), - ReflectionBitfieldFieldInfo::new( - 1, - "protocol", - "bits", - 8, - protocol_info.clone(), - Some(protocol_info), - ), - ], - ), - ); - + fn normalize_secret_sized_integer_alias_uses_primitive_boundary() { let mut interp = Interpreter::new(); - interp.set_reflection_metadata(Arc::new(metadata)); - - let type_fields = interp - .call_builtin_with_type_args("type.fields", &[type_named("Header")], &[]) - .expect("type.fields should be a typed builtin") - .expect("type.fields should evaluate"); - let Value::List(type_fields) = type_fields else { - panic!("expected list of TypeField values"); - }; - let protocol = Value::Enum { - type_name: "IpProtocol".to_string(), - variant: "tcp".to_string(), - fields: Vec::new(), - }; - - let builder = interp - .call_builtin_with_type_args("type.construct_start", &[type_named("Header")], &[]) - .expect("type.construct_start should be a typed builtin") - .expect("type.construct_start should evaluate"); - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("int64")], - &[builder, type_fields[0].clone(), Value::Int64(4)], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful version field update"); - }; - let builder = interp - .call_builtin_with_type_args( - "type.construct_put", - &[type_named("Header"), type_named("IpProtocol")], - &[(*builder).clone(), type_fields[1].clone(), protocol], - ) - .expect("type.construct_put should be a typed builtin") - .expect("type.construct_put should evaluate"); - let Value::ResultOk(builder) = builder else { - panic!("expected successful protocol field update"); + let alias = TypeAlias { + name: ident("SecretByte"), + base_type: TypeExpr::Generic(ident("secret"), vec![type_named("uint8")], sp()), + constraint: None, + exported: false, + root_exported: false, + span: sp(), }; - let finished = interp - .call_builtin_with_type_args( - "type.construct_finish", - &[type_named("Header")], - &[*builder], - ) - .expect("type.construct_finish should be a typed builtin") - .expect("type.construct_finish should evaluate"); + interp.register_type_alias(&alias); assert_eq!( - finished, - result_fail( - "checked reflection metadata for type 'IpProtocol' is missing variant metadata" - .to_string() - ) + interp + .normalize_value_for_type_name("secret[uint8]", Value::Int64(256)) + .unwrap_err(), + "uint8 value 256 is outside range 0..255" + ); + assert_eq!( + interp + .normalize_value_for_type_name("SecretByte", Value::Int64(256)) + .unwrap_err(), + "uint8 value 256 is outside range 0..255" ); } - /// Helper: create a field access expression. - fn field_access(base: Expr, field: &str) -> Expr { - Expr::FieldAccess(Box::new(base), ident(field), sp()) - } + // ----------------------------------------------------------------------- + // Boolean logic + // ----------------------------------------------------------------------- - /// Helper: create a named call argument. - fn named_arg(name: &str, value: Expr) -> CallArg { - CallArg { - name: Some(ident(name)), - value, - span: sp(), - } + #[test] + fn eval_and_true_true() { + let mut interp = Interpreter::new(); + let expr = binary(bool_expr(true), BinOp::And, bool_expr(true)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } - /// Helper: create a dotted call expression like `Point.total(view point)`. - fn dotted_call(module: &str, func_name: &str, args: Vec) -> Expr { - Expr::Call( - Box::new(field_access(var(module), func_name)), - args.into_iter() - .map(|value| CallArg { - name: None, - value, - span: sp(), - }) - .collect(), - sp(), - ) + #[test] + fn eval_and_true_false() { + let mut interp = Interpreter::new(); + let expr = binary(bool_expr(true), BinOp::And, bool_expr(false)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); } - /// Helper: create a simple struct definition. - fn struct_def(name: &str, fields: Vec<(&str, &str)>, methods: Vec) -> StructDef { - StructDef { - name: ident(name), - type_params: vec![], - fields: fields - .into_iter() - .map(|(field_name, field_ty)| FieldDef { - name: ident(field_name), - ty: type_named(field_ty), - serialize_name: None, - span: sp(), - }) - .collect(), - methods, - exported: false, - span: sp(), - } + #[test] + fn eval_or_false_true() { + let mut interp = Interpreter::new(); + let expr = binary(bool_expr(false), BinOp::Or, bool_expr(true)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } - fn machine_def( - name: &str, - states: Vec<(&str, Vec<(&str, TypeExpr)>)>, - transitions: Vec<(&str, &str)>, - ) -> MachineDef { - MachineDef { - name: ident(name), - exported: false, - states: states - .into_iter() - .map(|(state_name, fields)| jett_parser::ast::MachineState { - name: ident(state_name), - fields: fields - .into_iter() - .map(|(field_name, ty)| FieldDef { - name: ident(field_name), - ty, - serialize_name: None, - span: sp(), - }) - .collect(), - span: sp(), - }) - .collect(), - transitions: transitions - .into_iter() - .map(|(from, to)| jett_parser::ast::MachineTransition { - from: ident(from), - to: ident(to), - span: sp(), - }) - .collect(), - span: sp(), - } + #[test] + fn eval_or_false_false() { + let mut interp = Interpreter::new(); + let expr = binary(bool_expr(false), BinOp::Or, bool_expr(false)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); } - fn enum_def_with_values(name: &str, variants: Vec<(&str, i64)>) -> EnumDef { - EnumDef { - name: ident(name), - variants: variants - .into_iter() - .map(|(variant_name, discriminant)| jett_parser::ast::Variant { - name: ident(variant_name), - fields: vec![], - discriminant: Some(discriminant), - span: sp(), - }) - .collect(), - exported: false, - span: sp(), - } + #[test] + fn eval_not_true() { + let mut interp = Interpreter::new(); + let expr = Expr::Unary(UnaryOp::Not, Box::new(bool_expr(true)), sp()); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); } - fn enum_def_with_field( - name: &str, - variant_name: &str, - field_name: &str, - field_ty: &str, - ) -> EnumDef { - EnumDef { - name: ident(name), - variants: vec![jett_parser::ast::Variant { - name: ident(variant_name), - fields: vec![FieldDef { - name: ident(field_name), - ty: type_named(field_ty), - serialize_name: None, - span: sp(), - }], - discriminant: None, - span: sp(), - }], - exported: false, - span: sp(), - } + #[test] + fn eval_not_false() { + let mut interp = Interpreter::new(); + let expr = Expr::Unary(UnaryOp::Not, Box::new(bool_expr(false)), sp()); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } - /// Helper: create a simple bitfield definition. - fn bitfield_def( - name: &str, - fields: Vec<(&str, BitfieldFieldKind)>, - network_order: bool, - ) -> BitfieldDef { - BitfieldDef { - name: ident(name), - network_order, - fields: fields - .into_iter() - .map(|(field_name, kind)| jett_parser::ast::BitfieldFieldDef { - name: ident(field_name), - kind, - span: sp(), - }) - .collect(), - exported: false, - span: sp(), - } + #[test] + fn eval_comparison_eq() { + let mut interp = Interpreter::new(); + let expr = binary(int(5), BinOp::Eq, int(5)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } - fn interface_decl( - name: &str, - methods: Vec<(&str, Vec<(&str, &str, bool)>, &str)>, - ) -> InterfaceDecl { - InterfaceDecl { - name: ident(name), - methods: methods - .into_iter() - .map(|(method_name, params, return_type)| FunctionDecl { - name: ident(method_name), - type_params: vec![], - params: params - .into_iter() - .map(|(param_name, param_ty, view)| Param { - view, - mutable: false, - name: ident(param_name), - ty: type_named(param_ty), - span: sp(), - }) - .collect(), - return_type: Some(type_named(return_type)), - exported: false, - span: sp(), - }) - .collect(), - exported: false, - span: sp(), - } + #[test] + fn eval_comparison_neq() { + let mut interp = Interpreter::new(); + let expr = binary(int(5), BinOp::NotEq, int(3)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } - fn implement_block( - interface_name: &str, - for_type: &str, - methods: Vec, - ) -> ImplementBlock { - ImplementBlock { - interface_name: ident(interface_name), - for_type: type_named(for_type), - methods, - span: sp(), - } + #[test] + fn eval_comparison_lt() { + let mut interp = Interpreter::new(); + let expr = binary(int(3), BinOp::Lt, int(5)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); + } + + #[test] + fn eval_comparison_gte() { + let mut interp = Interpreter::new(); + let expr = binary(int(5), BinOp::GtEq, int(5)); + assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); } // ----------------------------------------------------------------------- - // Arithmetic + // String literals // ----------------------------------------------------------------------- #[test] - fn eval_integer_addition() { + fn eval_string_literal() { let mut interp = Interpreter::new(); - let expr = binary(int(2), BinOp::Add, int(3)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(5)); + let expr = string("hello"); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("hello".to_string()) + ); } #[test] - fn eval_integer_subtraction() { + fn eval_string_concatenation() { let mut interp = Interpreter::new(); - let expr = binary(int(10), BinOp::Sub, int(4)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(6)); + let expr = binary(string("hello "), BinOp::Add, string("world")); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("hello world".to_string()) + ); } + // ----------------------------------------------------------------------- + // String interpolation + // ----------------------------------------------------------------------- + #[test] - fn eval_integer_multiplication() { + fn string_interpolation_simple() { + // "hello {name}" with name = "world" let mut interp = Interpreter::new(); - let expr = binary(int(3), BinOp::Mul, int(7)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(21)); + interp + .exec_stmt(&var_decl("name", string("world"))) + .unwrap(); + let expr = Expr::StringInterpolation( + vec![ + StringPart::Literal("hello ".to_string()), + StringPart::Expr(Box::new(var("name"))), + ], + sp(), + ); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("hello world".to_string()) + ); } #[test] - fn eval_integer_division() { + fn string_interpolation_multiple() { + // "{a} + {b} = {c}" with a=2, b=3, c=5 let mut interp = Interpreter::new(); - let expr = binary(int(15), BinOp::Div, int(3)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(5)); + interp.exec_stmt(&var_decl("a", int(2))).unwrap(); + interp.exec_stmt(&var_decl("b", int(3))).unwrap(); + interp.exec_stmt(&var_decl("c", int(5))).unwrap(); + let expr = Expr::StringInterpolation( + vec![ + StringPart::Expr(Box::new(var("a"))), + StringPart::Literal(" + ".to_string()), + StringPart::Expr(Box::new(var("b"))), + StringPart::Literal(" = ".to_string()), + StringPart::Expr(Box::new(var("c"))), + ], + sp(), + ); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("2 + 3 = 5".to_string()) + ); } #[test] - fn eval_integer_modulo() { + fn string_interpolation_with_function_call() { + // "result: {add(2, 3)}" with a function add(a, b) returns a + b let mut interp = Interpreter::new(); - let expr = binary(int(17), BinOp::Modulo, int(5)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(2)); + let add_fn = func_def( + "add", + vec![("a", "int64"), ("b", "int64")], + Block { + stmts: vec![Stmt::Return(ReturnStmt { + value: Some(binary(var("a"), BinOp::Add, var("b"))), + span: sp(), + })], + span: sp(), + }, + ); + interp.register_function(&add_fn); + let expr = Expr::StringInterpolation( + vec![ + StringPart::Literal("result: ".to_string()), + StringPart::Expr(Box::new(call("add", vec![int(2), int(3)]))), + ], + sp(), + ); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("result: 5".to_string()) + ); } #[test] - fn eval_float_addition() { + fn string_interpolation_plain_string_still_works() { + // "plain string" with no interpolation should still work as StringLiteral let mut interp = Interpreter::new(); - let expr = binary(float(1.5), BinOp::Add, float(2.5)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Float64(4.0)); + let expr = string("plain string"); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("plain string".to_string()) + ); } #[test] - fn eval_complex_arithmetic() { - // (2 + 3) * 4 == 20 + fn declassify_is_a_runtime_no_op() { let mut interp = Interpreter::new(); - let sum = binary(int(2), BinOp::Add, int(3)); - let product = binary(sum, BinOp::Mul, int(4)); - assert_eq!(interp.eval_expr(&product).unwrap(), Value::Int64(20)); - } + interp.set_variable_public("api_key", Value::String("abc".to_string())); - #[test] - fn eval_division_by_zero() { - let mut interp = Interpreter::new(); - let expr = binary(int(10), BinOp::Div, int(0)); - assert!(interp.eval_expr(&expr).is_err()); + let expr = Expr::Declassify(Box::new(var("api_key")), sp()); + assert_eq!( + interp.eval_expr(&expr).unwrap(), + Value::String("abc".to_string()) + ); } + // ----------------------------------------------------------------------- + // Trusted stdlib hooks + // ----------------------------------------------------------------------- + #[test] - fn eval_integer_division_wraps_overflow() { + fn json_parse_bridge_requires_trusted_stdlib_hook() { let mut interp = Interpreter::new(); - let expr = binary(int(i64::MIN), BinOp::Div, int(-1)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(i64::MIN)); + let fake_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("fake"))]), + ); + interp.register_function_named( + "json.json_parse_reflected", + Some("json"), + &fake_hook, + false, + ); + + let err = interp + .call_function_with_type_args( + "json.parse", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap_err(); + + assert_eq!( + err, + "json.parse requires trusted stdlib hook 'json.json_parse_reflected'" + ); } #[test] - fn eval_integer_modulo_wraps_overflow() { + fn json_parse_bridge_requires_trusted_public_wrapper() { let mut interp = Interpreter::new(); - let expr = binary(int(i64::MIN), BinOp::Modulo, int(-1)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(0)); - } + let mut trusted_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("private hook"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); - #[test] - fn normalize_sized_integers_accepts_declared_boundaries() { - let interp = Interpreter::new(); - let cases = [ - ("int8", i8::MIN.into(), i8::MAX.into()), - ("int16", i16::MIN.into(), i16::MAX.into()), - ("int32", i32::MIN.into(), i32::MAX.into()), - ("uint8", 0, u8::MAX.into()), - ("uint16", 0, u16::MAX.into()), - ("uint32", 0, u32::MAX.into()), - ]; + let err = interp + .call_function_with_type_args( + "json.parse", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap_err(); - for (type_name, min, max) in cases { - assert_eq!( - interp.normalize_value_for_type_name(type_name, Value::Int64(min)), - Ok(Value::Int64(min)) - ); - assert_eq!( - interp.normalize_value_for_type_name(type_name, Value::Int64(max)), - Ok(Value::Int64(max)) - ); - } + assert_eq!( + err, + "json.parse requires trusted stdlib wrapper 'json.parse'" + ); } #[test] - fn normalize_sized_integers_rejects_values_outside_declared_boundaries() { - let interp = Interpreter::new(); + fn json_public_bridges_all_require_trusted_public_wrapper() { let cases = [ - ("int8", -129, "int8 value -129 is outside range -128..127"), - ("int8", 128, "int8 value 128 is outside range -128..127"), - ( - "int16", - -32769, - "int16 value -32769 is outside range -32768..32767", - ), - ( - "int16", - 32768, - "int16 value 32768 is outside range -32768..32767", - ), - ( - "int32", - -2147483649, - "int32 value -2147483649 is outside range -2147483648..2147483647", - ), - ( - "int32", - 2147483648, - "int32 value 2147483648 is outside range -2147483648..2147483647", - ), - ("uint8", -1, "uint8 value -1 is outside range 0..255"), - ("uint8", 256, "uint8 value 256 is outside range 0..255"), - ("uint16", -1, "uint16 value -1 is outside range 0..65535"), - ( - "uint16", - 65536, - "uint16 value 65536 is outside range 0..65535", - ), + ("json.parse", "json_parse_reflected", "\"value\""), ( - "uint32", - -1, - "uint32 value -1 is outside range 0..4294967295", + "json.parse_exact", + "json_parse_exact_reflected", + "\"value\"", ), + ("json.serialize", "json_serialize_reflected", "value"), ( - "uint32", - 4294967296, - "uint32 value 4294967296 is outside range 0..4294967295", + "json.serialize_public", + "json_serialize_public_reflected", + "value", ), ]; - for (type_name, value, expected) in cases { - assert_eq!( - interp - .normalize_value_for_type_name(type_name, Value::Int64(value)) - .unwrap_err(), - expected - ); - } - } - - #[test] - fn normalize_sized_integer_aliases_uses_primitive_boundary() { - let mut interp = Interpreter::new(); - interp.register_type_alias(&type_alias("Byte", "uint8", None)); - interp.register_type_alias(&type_alias("Count", "Byte", None)); - - assert_eq!( - interp.normalize_value_for_type_name("Count", Value::Int64(255)), - Ok(Value::Int64(255)) - ); - assert_eq!( - interp - .normalize_value_for_type_name("Count", Value::Int64(256)) - .unwrap_err(), - "uint8 value 256 is outside range 0..255" - ); - } - - #[test] - fn normalize_secret_sized_integer_alias_uses_primitive_boundary() { - let mut interp = Interpreter::new(); - let alias = TypeAlias { - name: ident("SecretByte"), - base_type: TypeExpr::Generic(ident("secret"), vec![type_named("uint8")], sp()), - constraint: None, - exported: false, - root_exported: false, - span: sp(), - }; - interp.register_type_alias(&alias); - - assert_eq!( - interp - .normalize_value_for_type_name("secret[uint8]", Value::Int64(256)) - .unwrap_err(), - "uint8 value 256 is outside range 0..255" - ); - assert_eq!( - interp - .normalize_value_for_type_name("SecretByte", Value::Int64(256)) - .unwrap_err(), - "uint8 value 256 is outside range 0..255" - ); - } + for (public_name, hook_name, arg) in cases { + let mut interp = Interpreter::new(); + let mut trusted_hook = + generic_json_hook(hook_name, block(vec![return_stmt(string("private hook"))])); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); - // ----------------------------------------------------------------------- - // Boolean logic - // ----------------------------------------------------------------------- + let err = interp + .call_function_with_type_args( + public_name, + &[type_named("string")], + vec![Value::String(arg.to_string())], + ) + .unwrap_err(); - #[test] - fn eval_and_true_true() { - let mut interp = Interpreter::new(); - let expr = binary(bool_expr(true), BinOp::And, bool_expr(true)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); + assert_eq!( + err, + format!("{public_name} requires trusted stdlib wrapper '{public_name}'") + ); + } } #[test] - fn eval_and_true_false() { - let mut interp = Interpreter::new(); - let expr = binary(bool_expr(true), BinOp::And, bool_expr(false)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); - } + fn json_public_bridges_all_reject_untrusted_public_wrapper() { + let cases = [ + ("json.parse", "json_parse_reflected", "parse", "\"value\""), + ( + "json.parse_exact", + "json_parse_exact_reflected", + "parse_exact", + "\"value\"", + ), + ( + "json.serialize", + "json_serialize_reflected", + "serialize", + "value", + ), + ( + "json.serialize_public", + "json_serialize_public_reflected", + "serialize_public", + "value", + ), + ]; - #[test] - fn eval_or_false_true() { - let mut interp = Interpreter::new(); - let expr = binary(bool_expr(false), BinOp::Or, bool_expr(true)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); - } + for (public_name, hook_name, wrapper_name, arg) in cases { + let mut interp = Interpreter::new(); + let mut trusted_hook = + generic_json_hook(hook_name, block(vec![return_stmt(string("private hook"))])); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let fake_wrapper = + generic_json_hook(wrapper_name, block(vec![return_stmt(string("fake"))])); + interp.register_function_named(public_name, Some("json"), &fake_wrapper, false); - #[test] - fn eval_or_false_false() { - let mut interp = Interpreter::new(); - let expr = binary(bool_expr(false), BinOp::Or, bool_expr(false)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); - } + let err = interp + .call_function_with_type_args( + public_name, + &[type_named("string")], + vec![Value::String(arg.to_string())], + ) + .unwrap_err(); - #[test] - fn eval_not_true() { - let mut interp = Interpreter::new(); - let expr = Expr::Unary(UnaryOp::Not, Box::new(bool_expr(true)), sp()); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(false)); + assert_eq!( + err, + format!("{public_name} requires trusted stdlib wrapper '{public_name}'") + ); + } } #[test] - fn eval_not_false() { + fn json_parse_bridge_uses_trusted_public_wrapper() { let mut interp = Interpreter::new(); - let expr = Expr::Unary(UnaryOp::Not, Box::new(bool_expr(false)), sp()); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); - } + let mut trusted_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("private hook"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let mut public_wrapper = + generic_json_hook("parse", block(vec![return_stmt(string("public wrapper"))])); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); - #[test] - fn eval_comparison_eq() { - let mut interp = Interpreter::new(); - let expr = binary(int(5), BinOp::Eq, int(5)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); - } + let value = interp + .call_function_with_type_args( + "json.parse", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap(); - #[test] - fn eval_comparison_neq() { - let mut interp = Interpreter::new(); - let expr = binary(int(5), BinOp::NotEq, int(3)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); + assert_eq!(value, Value::String("public wrapper".to_string())); } #[test] - fn eval_comparison_lt() { + fn untrusted_json_public_wrapper_cannot_satisfy_bridge() { let mut interp = Interpreter::new(); - let expr = binary(int(3), BinOp::Lt, int(5)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); - } + let mut trusted_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("private hook"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); - #[test] - fn eval_comparison_gte() { - let mut interp = Interpreter::new(); - let expr = binary(int(5), BinOp::GtEq, int(5)); - assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Bool(true)); - } + let fake_wrapper = generic_json_hook("parse", block(vec![return_stmt(string("fake"))])); + interp.register_function_named("json.parse", Some("json"), &fake_wrapper, false); - // ----------------------------------------------------------------------- - // String literals - // ----------------------------------------------------------------------- + let err = interp + .call_function_with_type_args( + "json.parse", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap_err(); - #[test] - fn eval_string_literal() { - let mut interp = Interpreter::new(); - let expr = string("hello"); assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("hello".to_string()) + err, + "json.parse requires trusted stdlib wrapper 'json.parse'" ); } #[test] - fn eval_string_concatenation() { + fn trusted_json_public_wrapper_still_requires_trusted_private_hook() { let mut interp = Interpreter::new(); - let expr = binary(string("hello "), BinOp::Add, string("world")); + let fake_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("fake"))]), + ); + interp.register_function_named( + "json.json_parse_reflected", + Some("json"), + &fake_hook, + false, + ); + let mut public_wrapper = + generic_json_hook("parse", block(vec![return_stmt(string("public wrapper"))])); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); + + let err = interp + .call_function_with_type_args( + "json.parse", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap_err(); + assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("hello world".to_string()) + err, + "json.parse requires trusted stdlib hook 'json.json_parse_reflected'" ); } - // ----------------------------------------------------------------------- - // String interpolation - // ----------------------------------------------------------------------- - #[test] - fn string_interpolation_simple() { - // "hello {name}" with name = "world" + fn json_parse_exact_bridge_requires_trusted_stdlib_hook() { let mut interp = Interpreter::new(); - interp - .exec_stmt(&var_decl("name", string("world"))) - .unwrap(); - let expr = Expr::StringInterpolation( - vec![ - StringPart::Literal("hello ".to_string()), - StringPart::Expr(Box::new(var("name"))), - ], - sp(), + let fake_hook = generic_json_hook( + "json_parse_exact_reflected", + block(vec![return_stmt(string("fake exact"))]), ); + interp.register_function_named( + "json.json_parse_exact_reflected", + Some("json"), + &fake_hook, + false, + ); + + let err = interp + .call_function_with_type_args( + "json.parse_exact", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap_err(); + assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("hello world".to_string()) + err, + "json.parse_exact requires trusted stdlib hook 'json.json_parse_exact_reflected'" ); } #[test] - fn string_interpolation_multiple() { - // "{a} + {b} = {c}" with a=2, b=3, c=5 + fn json_parse_exact_bridge_uses_trusted_public_wrapper() { let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("a", int(2))).unwrap(); - interp.exec_stmt(&var_decl("b", int(3))).unwrap(); - interp.exec_stmt(&var_decl("c", int(5))).unwrap(); - let expr = Expr::StringInterpolation( - vec![ - StringPart::Expr(Box::new(var("a"))), - StringPart::Literal(" + ".to_string()), - StringPart::Expr(Box::new(var("b"))), - StringPart::Literal(" = ".to_string()), - StringPart::Expr(Box::new(var("c"))), - ], - sp(), + let mut trusted_hook = generic_json_hook( + "json_parse_exact_reflected", + block(vec![return_stmt(string("private exact"))]), ); - assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("2 + 3 = 5".to_string()) + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let mut public_wrapper = generic_json_hook( + "parse_exact", + block(vec![return_stmt(string("public exact"))]), ); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); + + let value = interp + .call_function_with_type_args( + "json.parse_exact", + &[type_named("string")], + vec![Value::String("\"value\"".to_string())], + ) + .unwrap(); + + assert_eq!(value, Value::String("public exact".to_string())); } #[test] - fn string_interpolation_with_function_call() { - // "result: {add(2, 3)}" with a function add(a, b) returns a + b + fn json_serialize_bridge_requires_trusted_stdlib_hook() { let mut interp = Interpreter::new(); - let add_fn = func_def( - "add", - vec![("a", "int64"), ("b", "int64")], - Block { - stmts: vec![Stmt::Return(ReturnStmt { - value: Some(binary(var("a"), BinOp::Add, var("b"))), - span: sp(), - })], - span: sp(), - }, + let fake_hook = generic_json_hook( + "json_serialize_reflected", + block(vec![return_stmt(string("fake"))]), ); - interp.register_function(&add_fn); - let expr = Expr::StringInterpolation( - vec![ - StringPart::Literal("result: ".to_string()), - StringPart::Expr(Box::new(call("add", vec![int(2), int(3)]))), - ], - sp(), + interp.register_function_named( + "json.json_serialize_reflected", + Some("json"), + &fake_hook, + false, ); + + let err = interp + .call_function_with_type_args( + "json.serialize", + &[type_named("string")], + vec![Value::String("value".to_string())], + ) + .unwrap_err(); + assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("result: 5".to_string()) + err, + "json.serialize requires trusted stdlib hook 'json.json_serialize_reflected'" ); } #[test] - fn string_interpolation_plain_string_still_works() { - // "plain string" with no interpolation should still work as StringLiteral + fn json_serialize_bridge_uses_trusted_public_wrapper() { let mut interp = Interpreter::new(); - let expr = string("plain string"); - assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("plain string".to_string()) + let mut trusted_hook = generic_json_hook( + "json_serialize_reflected", + block(vec![return_stmt(string("private serialized"))]), ); - } + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let mut public_wrapper = generic_json_hook( + "serialize", + block(vec![return_stmt(string("public serialized"))]), + ); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); - #[test] - fn declassify_is_a_runtime_no_op() { - let mut interp = Interpreter::new(); - interp.set_variable_public("api_key", Value::String("abc".to_string())); + let value = interp + .call_function_with_type_args( + "json.serialize", + &[type_named("string")], + vec![Value::String("value".to_string())], + ) + .unwrap(); - let expr = Expr::Declassify(Box::new(var("api_key")), sp()); - assert_eq!( - interp.eval_expr(&expr).unwrap(), - Value::String("abc".to_string()) - ); + assert_eq!(value, Value::String("public serialized".to_string())); } - // ----------------------------------------------------------------------- - // Trusted stdlib hooks - // ----------------------------------------------------------------------- - #[test] - fn json_parse_bridge_requires_trusted_stdlib_hook() { + fn json_serialize_public_bridge_requires_trusted_stdlib_hook() { let mut interp = Interpreter::new(); let fake_hook = generic_json_hook( - "json_parse_reflected", + "json_serialize_public_reflected", block(vec![return_stmt(string("fake"))]), ); - interp.register_function_named("json.json_parse_reflected", &fake_hook, false); + interp.register_function_named( + "json.json_serialize_public_reflected", + Some("json"), + &fake_hook, + false, + ); let err = interp .call_function_with_type_args( - "json.parse", + "json.serialize_public", &[type_named("string")], - vec![Value::String("\"value\"".to_string())], + vec![Value::String("value".to_string())], ) .unwrap_err(); assert_eq!( err, - "json.parse requires trusted stdlib hook 'json.json_parse_reflected'" + "json.serialize_public requires trusted stdlib hook 'json.json_serialize_public_reflected'" ); } #[test] - fn json_parse_bridge_requires_trusted_public_wrapper() { + fn json_serialize_public_bridge_uses_trusted_public_wrapper() { let mut interp = Interpreter::new(); let mut trusted_hook = generic_json_hook( - "json_parse_reflected", - block(vec![return_stmt(string("private hook"))]), + "json_serialize_public_reflected", + block(vec![return_stmt(string("private public serialized"))]), ); trusted_hook.span = stdlib_sp(); interp.register_function_in_namespace(Some("json"), &trusted_hook); + let mut public_wrapper = generic_json_hook( + "serialize_public", + block(vec![return_stmt(string("public serialized"))]), + ); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); - let err = interp + let value = interp .call_function_with_type_args( - "json.parse", + "json.serialize_public", &[type_named("string")], - vec![Value::String("\"value\"".to_string())], + vec![Value::String("value".to_string())], ) - .unwrap_err(); + .unwrap(); - assert_eq!( - err, - "json.parse requires trusted stdlib wrapper 'json.parse'" - ); + assert_eq!(value, Value::String("public serialized".to_string())); } #[test] - fn json_public_bridges_all_require_trusted_public_wrapper() { - let cases = [ - ("json.parse", "json_parse_reflected", "\"value\""), - ( - "json.parse_exact", - "json_parse_exact_reflected", - "\"value\"", - ), - ("json.serialize", "json_serialize_reflected", "value"), - ( - "json.serialize_public", - "json_serialize_public_reflected", - "value", - ), - ]; + fn json_parse_raw_without_stdlib_wrapper_is_undefined() { + let mut interp = Interpreter::new(); - for (public_name, hook_name, arg) in cases { - let mut interp = Interpreter::new(); - let mut trusted_hook = - generic_json_hook(hook_name, block(vec![return_stmt(string("private hook"))])); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); + let err = interp + .call_function("json.parse_raw", vec![Value::String("null".to_string())]) + .unwrap_err(); - let err = interp - .call_function_with_type_args( - public_name, - &[type_named("string")], - vec![Value::String(arg.to_string())], - ) - .unwrap_err(); + assert_eq!(err, "undefined function 'json.parse_raw'"); + } - assert_eq!( - err, - format!("{public_name} requires trusted stdlib wrapper '{public_name}'") - ); + #[test] + fn json_raw_facades_without_stdlib_wrappers_are_undefined() { + let mut interp = Interpreter::new(); + + for name in JSON_RAW_FACADE_NAMES { + let err = interp.call_function(name, Vec::new()).unwrap_err(); + assert_eq!(err, format!("undefined function '{name}'")); } } #[test] - fn json_public_bridges_all_reject_untrusted_public_wrapper() { - let cases = [ - ("json.parse", "json_parse_reflected", "parse", "\"value\""), - ( - "json.parse_exact", - "json_parse_exact_reflected", - "parse_exact", - "\"value\"", - ), - ( - "json.serialize", - "json_serialize_reflected", - "serialize", - "value", - ), - ( - "json.serialize_public", - "json_serialize_public_reflected", - "serialize_public", - "value", - ), - ]; + fn json_parse_raw_uses_public_stdlib_wrapper() { + let mut interp = Interpreter::new(); + let mut wrapper = func_def( + "parse_raw", + vec![("raw", "string")], + block(vec![return_stmt(string("public raw"))]), + ); + wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &wrapper); - for (public_name, hook_name, wrapper_name, arg) in cases { - let mut interp = Interpreter::new(); - let mut trusted_hook = - generic_json_hook(hook_name, block(vec![return_stmt(string("private hook"))])); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); - let fake_wrapper = - generic_json_hook(wrapper_name, block(vec![return_stmt(string("fake"))])); - interp.register_function_named(public_name, &fake_wrapper, false); + let value = interp + .call_function( + "json.parse_raw", + vec![Value::String("not json".to_string())], + ) + .unwrap(); - let err = interp - .call_function_with_type_args( - public_name, - &[type_named("string")], - vec![Value::String(arg.to_string())], - ) - .unwrap_err(); + assert_eq!(value, Value::String("public raw".to_string())); + } - assert_eq!( - err, - format!("{public_name} requires trusted stdlib wrapper '{public_name}'") + #[test] + fn json_raw_helpers_use_public_stdlib_wrappers() { + let mut interp = Interpreter::new(); + + for name in JSON_RAW_FACADE_NAMES { + let short_name = name + .strip_prefix("json.") + .expect("raw facade names should be qualified"); + let mut wrapper = func_def( + short_name, + vec![("value", "JsonTree")], + block(vec![return_stmt(string("public tree"))]), + ); + wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &wrapper); + + assert!( + interp.is_trusted_stdlib_first_function(name), + "{name} should run through the trusted stdlib wrapper path" ); } } #[test] - fn json_parse_bridge_uses_trusted_public_wrapper() { + fn public_raw_facade_wrapper_runs_before_private_helper() { + let mut interp = Interpreter::new(); + let mut public_wrapper = func_def( + "kind", + vec![("value", "JsonTree")], + block(vec![return_stmt(string("public wrapper"))]), + ); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); + + let mut trusted_hook = func_def( + "json_tree_kind", + vec![("value", "JsonTree")], + block(vec![return_stmt(string("hook fallback"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + + let value = interp + .call_function("json.kind", vec![json_tree_null()]) + .unwrap(); + + assert_eq!(value, Value::String("public wrapper".to_string())); + } + + #[test] + fn dotted_raw_facade_expr_runs_public_wrapper() { + let mut interp = Interpreter::new(); + interp.set_variable_public("tree", json_tree_null()); + + let mut public_wrapper = func_def( + "kind", + vec![("value", "JsonTree")], + block(vec![return_stmt(string("public wrapper"))]), + ); + public_wrapper.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &public_wrapper); + + let value = interp + .eval_expr(&dotted_call("json", "kind", vec![var("tree")])) + .unwrap(); + + assert_eq!(value, Value::String("public wrapper".to_string())); + } + + #[test] + fn pipeline_raw_facade_step_runs_public_wrapper() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_parse_reflected", - block(vec![return_stmt(string("private hook"))]), + interp.set_variable_public("tree", json_tree_null()); + + let mut public_wrapper = func_def( + "kind", + vec![("value", "JsonTree")], + block(vec![return_stmt(string("public wrapper"))]), ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); - let mut public_wrapper = - generic_json_hook("parse", block(vec![return_stmt(string("public wrapper"))])); public_wrapper.span = stdlib_sp(); interp.register_function_in_namespace(Some("json"), &public_wrapper); let value = interp - .call_function_with_type_args( - "json.parse", - &[type_named("string")], - vec![Value::String("\"value\"".to_string())], - ) + .eval_expr(&Expr::Pipeline( + Box::new(var("tree")), + vec![PipelineStep { + function: field_access(var("json"), "kind"), + extra_args: Vec::new(), + handle: None, + span: sp(), + }], + sp(), + )) .unwrap(); assert_eq!(value, Value::String("public wrapper".to_string())); } #[test] - fn untrusted_json_public_wrapper_cannot_satisfy_bridge() { + fn raw_facade_wrapper_is_ordinary_function_without_builtin_fallback() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_parse_reflected", - block(vec![return_stmt(string("private hook"))]), + let wrapper = func_def( + "kind", + vec![("value", "JsonTree")], + block(vec![return_stmt(string("ordinary wrapper"))]), ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); + interp.register_function_named("json.kind", Some("json"), &wrapper, false); - let fake_wrapper = generic_json_hook("parse", block(vec![return_stmt(string("fake"))])); - interp.register_function_named("json.parse", &fake_wrapper, false); - - let err = interp - .call_function_with_type_args( - "json.parse", - &[type_named("string")], - vec![Value::String("\"value\"".to_string())], - ) - .unwrap_err(); + let value = interp + .call_function("json.kind", vec![json_tree_null()]) + .unwrap(); - assert_eq!( - err, - "json.parse requires trusted stdlib wrapper 'json.parse'" - ); + assert_eq!(value, Value::String("ordinary wrapper".to_string())); } #[test] - fn trusted_json_public_wrapper_still_requires_trusted_private_hook() { + fn untrusted_registration_removes_previous_json_hook_trust() { let mut interp = Interpreter::new(); + let mut trusted_hook = generic_json_hook( + "json_parse_reflected", + block(vec![return_stmt(string("trusted"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let fake_hook = generic_json_hook( "json_parse_reflected", block(vec![return_stmt(string("fake"))]), ); - interp.register_function_named("json.json_parse_reflected", &fake_hook, false); - let mut public_wrapper = - generic_json_hook("parse", block(vec![return_stmt(string("public wrapper"))])); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); + interp.register_function_named( + "json.json_parse_reflected", + Some("json"), + &fake_hook, + false, + ); let err = interp .call_function_with_type_args( @@ -14917,13 +23765,25 @@ mod tests { } #[test] - fn json_parse_exact_bridge_requires_trusted_stdlib_hook() { + fn untrusted_registration_removes_previous_json_parse_exact_hook_trust() { let mut interp = Interpreter::new(); + let mut trusted_hook = generic_json_hook( + "json_parse_exact_reflected", + block(vec![return_stmt(string("trusted exact"))]), + ); + trusted_hook.span = stdlib_sp(); + interp.register_function_in_namespace(Some("json"), &trusted_hook); + let fake_hook = generic_json_hook( "json_parse_exact_reflected", block(vec![return_stmt(string("fake exact"))]), ); - interp.register_function_named("json.json_parse_exact_reflected", &fake_hook, false); + interp.register_function_named( + "json.json_parse_exact_reflected", + Some("json"), + &fake_hook, + false, + ); let err = interp .call_function_with_type_args( @@ -14940,40 +23800,25 @@ mod tests { } #[test] - fn json_parse_exact_bridge_uses_trusted_public_wrapper() { + fn untrusted_registration_removes_previous_json_serialize_hook_trust() { let mut interp = Interpreter::new(); let mut trusted_hook = generic_json_hook( - "json_parse_exact_reflected", - block(vec![return_stmt(string("private exact"))]), + "json_serialize_reflected", + block(vec![return_stmt(string("trusted"))]), ); trusted_hook.span = stdlib_sp(); interp.register_function_in_namespace(Some("json"), &trusted_hook); - let mut public_wrapper = generic_json_hook( - "parse_exact", - block(vec![return_stmt(string("public exact"))]), - ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); - - let value = interp - .call_function_with_type_args( - "json.parse_exact", - &[type_named("string")], - vec![Value::String("\"value\"".to_string())], - ) - .unwrap(); - - assert_eq!(value, Value::String("public exact".to_string())); - } - #[test] - fn json_serialize_bridge_requires_trusted_stdlib_hook() { - let mut interp = Interpreter::new(); let fake_hook = generic_json_hook( "json_serialize_reflected", block(vec![return_stmt(string("fake"))]), ); - interp.register_function_named("json.json_serialize_reflected", &fake_hook, false); + interp.register_function_named( + "json.json_serialize_reflected", + Some("json"), + &fake_hook, + false, + ); let err = interp .call_function_with_type_args( @@ -14990,766 +23835,1664 @@ mod tests { } #[test] - fn json_serialize_bridge_uses_trusted_public_wrapper() { + fn untrusted_registration_removes_previous_json_serialize_public_hook_trust() { let mut interp = Interpreter::new(); let mut trusted_hook = generic_json_hook( - "json_serialize_reflected", - block(vec![return_stmt(string("private serialized"))]), + "json_serialize_public_reflected", + block(vec![return_stmt(string("trusted"))]), ); trusted_hook.span = stdlib_sp(); interp.register_function_in_namespace(Some("json"), &trusted_hook); - let mut public_wrapper = generic_json_hook( - "serialize", - block(vec![return_stmt(string("public serialized"))]), + + let fake_hook = generic_json_hook( + "json_serialize_public_reflected", + block(vec![return_stmt(string("fake"))]), + ); + interp.register_function_named( + "json.json_serialize_public_reflected", + Some("json"), + &fake_hook, + false, ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); - let value = interp + let err = interp .call_function_with_type_args( - "json.serialize", + "json.serialize_public", &[type_named("string")], vec![Value::String("value".to_string())], ) + .unwrap_err(); + + assert_eq!( + err, + "json.serialize_public requires trusted stdlib hook 'json.json_serialize_public_reflected'" + ); + } + + // ----------------------------------------------------------------------- + // Variable declaration and lookup + // ----------------------------------------------------------------------- + + #[test] + fn variable_declaration_and_lookup() { + let mut interp = Interpreter::new(); + let decl = var_decl("x", int(42)); + interp.exec_stmt(&decl).unwrap(); + let result = interp.eval_expr(&var("x")).unwrap(); + assert_eq!(result, Value::Int64(42)); + } + + #[test] + fn uint64_variable_declaration_normalizes_small_literal_carrier() { + let mut interp = Interpreter::new(); + interp + .exec_stmt(&typed_var_decl("uint64", "x", int(42))) .unwrap(); - assert_eq!(value, Value::String("public serialized".to_string())); + assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Uint64(42)); } #[test] - fn json_serialize_public_bridge_requires_trusted_stdlib_hook() { + fn uint64_function_boundaries_normalize_small_literal_carrier() { let mut interp = Interpreter::new(); - let fake_hook = generic_json_hook( - "json_serialize_public_reflected", - block(vec![return_stmt(string("fake"))]), + let mut echo = func_def( + "echo_u64", + vec![("value", "uint64")], + block(vec![return_stmt(var("value"))]), ); - interp.register_function_named("json.json_serialize_public_reflected", &fake_hook, false); + echo.return_type = Some(type_named("uint64")); + interp.register_function(&echo); - let err = interp - .call_function_with_type_args( - "json.serialize_public", - &[type_named("string")], - vec![Value::String("value".to_string())], - ) - .unwrap_err(); + let mut make = func_def("make_u64", vec![], block(vec![return_stmt(int(7))])); + make.return_type = Some(type_named("uint64")); + interp.register_function(&make); + + assert_eq!( + interp + .call_function("echo_u64", vec![Value::Int64(42)]) + .unwrap(), + Value::Uint64(42) + ); + assert_eq!( + interp.call_function("make_u64", Vec::new()).unwrap(), + Value::Uint64(7) + ); + } + + #[test] + fn named_function_type_context_restores_caller_after_argument_failure() { + let mut interp = Interpreter::new(); + let caller_types = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([( + Span::new(FileId::new(0), 99, 100), + "uint64".into(), + )])), + ..Default::default() + }); + interp.active_checked_function = Some(caller_types.clone()); + let caller_scope = Arc::new(CheckedScopedTypes::default()); + interp.active_checked_scope = Some(caller_scope.clone()); + interp.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "string".into(), + reflection: None, + }]; + let echo = func_def( + "echo", + vec![("value", "int8")], + block(vec![return_stmt(var("value"))]), + ); + interp.register_function_in_namespace(Some("callee"), &echo); + interp.type_arg_scopes = vec![HashMap::from([("T".into(), type_named("string"))])]; + interp.current_type_arguments = vec![type_named("uint64")]; + interp.current_namespace = Some("caller".into()); + interp.current_function_trusted_stdlib = true; + for value in [128, 127] { + let result = interp.call_function("callee.echo", vec![Value::Int64(value)]); + if value == 128 { + assert!( + result + .unwrap_err() + .contains("int8 value 128 is outside range") + ); + } else { + assert_eq!(result.unwrap(), Value::Int64(127)); + } + assert_eq!( + interp.scoped_type_bindings, + vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "string".into(), + reflection: None + }] + ); + assert!(Arc::ptr_eq( + interp.active_checked_scope.as_ref().unwrap(), + &caller_scope + )); + assert_eq!(interp.scopes.len(), 1); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.type_arg_scopes.len(), 1); + assert_eq!(type_expr_display(&interp.type_arg_scopes[0]["T"]), "string"); + assert_eq!( + type_expr_display(&interp.current_type_arguments[0]), + "uint64" + ); + assert_eq!(interp.current_namespace.as_deref(), Some("caller")); + assert!(interp.current_function_trusted_stdlib); + assert!(Arc::ptr_eq( + interp.active_checked_function.as_ref().unwrap(), + &caller_types + )); + } + } + + #[test] + fn closure_type_context_restores_caller_after_argument_failure() { + let mut interp = Interpreter::new(); + let caller_types = Arc::new(CheckedFunctionTypes { + expressions: Arc::new(HashMap::from([( + Span::new(FileId::new(0), 99, 100), + "uint64".into(), + )])), + ..Default::default() + }); + interp.active_checked_function = Some(caller_types.clone()); + let caller_scope = Arc::new(CheckedScopedTypes::default()); + interp.active_checked_scope = Some(caller_scope.clone()); + interp.scoped_type_bindings = vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "string".into(), + reflection: None, + }]; + interp + .type_arg_scopes + .push(HashMap::from([("T".into(), type_named("string"))])); + interp.current_type_arguments = vec![type_named("uint64")]; + interp.current_namespace = Some("caller".into()); + interp.current_function_trusted_stdlib = true; + let closure = Value::Function { + type_context: Box::new(ClosureTypeContext { + return_type: None, + checked_scope: None, + scoped_type_bindings: Vec::new(), + checked_function: None, + bindings: HashMap::from([("T".into(), type_named("int8"))]), + arguments: Vec::new(), + reflected_capture: None, + }), + params: vec![Param { + view: false, + mutable: false, + name: ident("value"), + ty: type_named("T"), + span: sp(), + }], + body: block(vec![return_stmt(var("value"))]), + captures: HashMap::new(), + capture_types: HashMap::new(), + namespace_aliases: HashMap::new(), + namespace: Some("closure".into()), + }; + for value in [128, 127] { + let result = interp.call_fn_value(closure.clone(), vec![Value::Int64(value)]); + if value == 128 { + assert!( + result + .unwrap_err() + .contains("int8 value 128 is outside range") + ); + } else { + assert_eq!(result.unwrap(), Value::Int64(127)); + } + assert_eq!( + interp.scoped_type_bindings, + vec![ClosureScopedTypeBinding { + name: "Field".into(), + canonical_name: "string".into(), + reflection: None + }] + ); + assert!(Arc::ptr_eq( + interp.active_checked_scope.as_ref().unwrap(), + &caller_scope + )); + assert_eq!(interp.scopes.len(), 1); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.type_arg_scopes.len(), 1); + assert_eq!(type_expr_display(&interp.type_arg_scopes[0]["T"]), "string"); + assert_eq!( + type_expr_display(&interp.current_type_arguments[0]), + "uint64" + ); + assert_eq!(interp.current_namespace.as_deref(), Some("caller")); + assert!(interp.current_function_trusted_stdlib); + assert!(Arc::ptr_eq( + interp.active_checked_function.as_ref().unwrap(), + &caller_types + )); + } + } + + #[test] + fn uint64_inline_function_parameter_normalizes_small_literal_carrier() { + let mut interp = Interpreter::new(); + let fn_value = Value::Function { + type_context: Box::default(), + params: vec![Param { + view: false, + mutable: false, + name: ident("value"), + ty: type_named("uint64"), + span: sp(), + }], + body: block(vec![return_stmt(var("value"))]), + captures: HashMap::new(), + capture_types: HashMap::new(), + namespace_aliases: HashMap::new(), + namespace: None, + }; assert_eq!( - err, - "json.serialize_public requires trusted stdlib hook 'json.json_serialize_public_reflected'" + interp + .call_fn_value(fn_value, vec![Value::Int64(42)]) + .unwrap(), + Value::Uint64(42) ); } #[test] - fn json_serialize_public_bridge_uses_trusted_public_wrapper() { + fn uint64_actor_boundaries_normalize_small_literal_carriers() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_serialize_public_reflected", - block(vec![return_stmt(string("private public serialized"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); - let mut public_wrapper = generic_json_hook( - "serialize_public", - block(vec![return_stmt(string("public serialized"))]), - ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); + let uint64_param = |name: &str| Param { + view: false, + mutable: false, + name: ident(name), + ty: type_named("uint64"), + span: sp(), + }; + let actor = ActorDef { + name: ident("Probe"), + capability_params: vec![uint64_param("seed")], + state_fields: vec![VarDecl { + mutable: true, + ty: type_named("uint64"), + name: ident("stored"), + value: var("seed"), + span: sp(), + }], + handlers: vec![ + ReceiveHandler { + name: ident("echo"), + params: vec![uint64_param("value")], + responds: Some(type_named("uint64")), + body: block(vec![respond_stmt(var("value"))]), + span: sp(), + }, + ReceiveHandler { + name: ident("stored"), + params: vec![], + responds: Some(type_named("uint64")), + body: block(vec![respond_stmt(var("stored"))]), + span: sp(), + }, + ReceiveHandler { + name: ident("reset"), + params: vec![], + responds: Some(type_named("uint64")), + body: block(vec![assign("stored", int(7)), respond_stmt(var("stored"))]), + span: sp(), + }, + ], + exported: false, + span: sp(), + }; + interp.register_actor(&actor); - let value = interp - .call_function_with_type_args( - "json.serialize_public", - &[type_named("string")], - vec![Value::String("value".to_string())], - ) + let actor_value = interp + .eval_expr(&Expr::Spawn( + Box::new(Expr::Call( + Box::new(var("Probe")), + vec![named_arg("seed", int(42))], + sp(), + )), + sp(), + )) .unwrap(); + interp.set_variable("probe", actor_value); - assert_eq!(value, Value::String("public serialized".to_string())); + let ask_echo = Expr::Ask( + Box::new(Expr::Call( + Box::new(field_access(var("probe"), "echo")), + vec![CallArg { + name: None, + value: int(5), + span: sp(), + }], + sp(), + )), + sp(), + ); + let ask_stored = Expr::Ask(Box::new(field_access(var("probe"), "stored")), sp()); + let ask_reset = Expr::Ask(Box::new(field_access(var("probe"), "reset")), sp()); + + assert_eq!(interp.eval_expr(&ask_echo).unwrap(), Value::Uint64(5)); + assert_eq!(interp.eval_expr(&ask_stored).unwrap(), Value::Uint64(42)); + assert_eq!(interp.eval_expr(&ask_reset).unwrap(), Value::Uint64(7)); + assert_eq!(interp.eval_expr(&ask_stored).unwrap(), Value::Uint64(7)); } #[test] - fn json_parse_raw_without_stdlib_wrapper_is_undefined() { + fn variable_assignment() { let mut interp = Interpreter::new(); - - let err = interp - .call_function("json.parse_raw", vec![Value::String("null".to_string())]) - .unwrap_err(); - - assert_eq!(err, "undefined function 'json.parse_raw'"); + interp.exec_stmt(&var_decl("x", int(1))).unwrap(); + interp.exec_stmt(&assign("x", int(99))).unwrap(); + assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Int64(99)); } #[test] - fn json_raw_facades_without_stdlib_wrappers_are_undefined() { + fn uint64_variable_assignment_normalizes_small_literal_carrier() { let mut interp = Interpreter::new(); + interp + .exec_stmt(&typed_var_decl("uint64", "x", int(1))) + .unwrap(); + interp.exec_stmt(&assign("x", int(42))).unwrap(); - for name in JSON_RAW_FACADE_NAMES { - let err = interp.call_function(name, Vec::new()).unwrap_err(); - assert_eq!(err, format!("undefined function '{name}'")); - } + assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Uint64(42)); } #[test] - fn json_parse_raw_uses_public_stdlib_wrapper() { + fn uint64_parameter_assignment_normalizes_small_literal_carrier() { let mut interp = Interpreter::new(); - let mut wrapper = func_def( - "parse_raw", - vec![("raw", "string")], - block(vec![return_stmt(string("public raw"))]), + let reassign = func_def( + "reassign_u64", + vec![("value", "uint64")], + block(vec![assign("value", int(7)), return_stmt(var("value"))]), ); - wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &wrapper); - - let value = interp - .call_function( - "json.parse_raw", - vec![Value::String("not json".to_string())], - ) - .unwrap(); + interp.register_function(&reassign); - assert_eq!(value, Value::String("public raw".to_string())); + assert_eq!( + interp + .call_function("reassign_u64", vec![Value::Int64(1)]) + .unwrap(), + Value::Uint64(7) + ); } #[test] - fn json_raw_helpers_use_public_stdlib_wrappers() { + fn trace_stmt_records_current_value() { let mut interp = Interpreter::new(); + interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - for name in JSON_RAW_FACADE_NAMES { - let short_name = name - .strip_prefix("json.") - .expect("raw facade names should be qualified"); - let mut wrapper = func_def( - short_name, - vec![("value", "JsonTree")], - block(vec![return_stmt(string("public tree"))]), - ); - wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &wrapper); + interp + .exec_stmt(&Stmt::Trace(TraceStmt { + name: ident("total"), + span: sp(), + })) + .unwrap(); - assert!( - interp.is_trusted_stdlib_first_function(name), - "{name} should run through the trusted stdlib wrapper path" - ); - } + assert_eq!(interp.take_debug_output(), vec!["trace total: int64 = 42"]); } #[test] - fn public_raw_facade_wrapper_runs_before_private_helper() { + fn breakpoint_stmt_records_visible_bindings() { let mut interp = Interpreter::new(); - let mut public_wrapper = func_def( - "kind", - vec![("value", "JsonTree")], - block(vec![return_stmt(string("public wrapper"))]), - ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); - - let mut trusted_hook = func_def( - "json_tree_kind", - vec![("value", "JsonTree")], - block(vec![return_stmt(string("hook fallback"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); + interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - let value = interp - .call_function("json.kind", vec![json_tree_null()]) + interp + .exec_stmt(&Stmt::Breakpoint(BreakpointStmt { + condition: Some(bool_expr(true)), + span: sp(), + })) .unwrap(); - assert_eq!(value, Value::String("public wrapper".to_string())); + assert_eq!( + interp.take_debug_output(), + vec!["breakpoint hit: total: int64 = 42"] + ); } #[test] - fn dotted_raw_facade_expr_runs_public_wrapper() { + fn breakpoint_stmt_skips_when_condition_is_false() { let mut interp = Interpreter::new(); - interp.set_variable_public("tree", json_tree_null()); - - let mut public_wrapper = func_def( - "kind", - vec![("value", "JsonTree")], - block(vec![return_stmt(string("public wrapper"))]), - ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); + interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - let value = interp - .eval_expr(&dotted_call("json", "kind", vec![var("tree")])) + interp + .exec_stmt(&Stmt::Breakpoint(BreakpointStmt { + condition: Some(bool_expr(false)), + span: sp(), + })) .unwrap(); - assert_eq!(value, Value::String("public wrapper".to_string())); + assert!(interp.take_debug_output().is_empty()); } #[test] - fn pipeline_raw_facade_step_runs_public_wrapper() { + fn undefined_variable_error() { let mut interp = Interpreter::new(); - interp.set_variable_public("tree", json_tree_null()); - - let mut public_wrapper = func_def( - "kind", - vec![("value", "JsonTree")], - block(vec![return_stmt(string("public wrapper"))]), - ); - public_wrapper.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &public_wrapper); - - let value = interp - .eval_expr(&Expr::Pipeline( - Box::new(var("tree")), - vec![PipelineStep { - function: field_access(var("json"), "kind"), - extra_args: Vec::new(), - handle: None, - span: sp(), - }], - sp(), - )) - .unwrap(); - - assert_eq!(value, Value::String("public wrapper".to_string())); + assert!(interp.eval_expr(&var("nonexistent")).is_err()); } + // ----------------------------------------------------------------------- + // If/else branching + // ----------------------------------------------------------------------- + #[test] - fn raw_facade_wrapper_is_ordinary_function_without_builtin_fallback() { + fn if_then_branch_taken() { let mut interp = Interpreter::new(); - let wrapper = func_def( - "kind", - vec![("value", "JsonTree")], - block(vec![return_stmt(string("ordinary wrapper"))]), - ); - interp.register_function_named("json.kind", &wrapper, false); - - let value = interp - .call_function("json.kind", vec![json_tree_null()]) - .unwrap(); + interp.exec_stmt(&var_decl("result", int(0))).unwrap(); + let if_stmt = Stmt::If(IfStmt { + condition: bool_expr(true), + then_block: block(vec![assign("result", int(1))]), + else_ifs: vec![], + else_block: None, + span: sp(), + }); + interp.exec_stmt(&if_stmt).unwrap(); + assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(1)); + } - assert_eq!(value, Value::String("ordinary wrapper".to_string())); + #[test] + fn if_else_branch_taken() { + let mut interp = Interpreter::new(); + interp.exec_stmt(&var_decl("result", int(0))).unwrap(); + let if_stmt = Stmt::If(IfStmt { + condition: bool_expr(false), + then_block: block(vec![assign("result", int(1))]), + else_ifs: vec![], + else_block: Some(block(vec![assign("result", int(2))])), + span: sp(), + }); + interp.exec_stmt(&if_stmt).unwrap(); + assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(2)); } #[test] - fn untrusted_registration_removes_previous_json_hook_trust() { + fn if_else_if_branch_taken() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_parse_reflected", - block(vec![return_stmt(string("trusted"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); + interp.exec_stmt(&var_decl("result", int(0))).unwrap(); + let if_stmt = Stmt::If(IfStmt { + condition: bool_expr(false), + then_block: block(vec![assign("result", int(1))]), + else_ifs: vec![(bool_expr(true), block(vec![assign("result", int(3))]))], + else_block: Some(block(vec![assign("result", int(2))])), + span: sp(), + }); + interp.exec_stmt(&if_stmt).unwrap(); + assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(3)); + } - let fake_hook = generic_json_hook( - "json_parse_reflected", - block(vec![return_stmt(string("fake"))]), - ); - interp.register_function_named("json.json_parse_reflected", &fake_hook, false); + // ----------------------------------------------------------------------- + // For loop + // ----------------------------------------------------------------------- - let err = interp - .call_function_with_type_args( - "json.parse", - &[type_named("string")], - vec![Value::String("\"value\"".to_string())], - ) - .unwrap_err(); + #[test] + fn for_loop_over_list() { + let mut interp = Interpreter::new(); + interp.exec_stmt(&var_decl("sum", int(0))).unwrap(); - assert_eq!( - err, - "json.parse requires trusted stdlib hook 'json.json_parse_reflected'" - ); + let list = Expr::ListConstruct(vec![int(1), int(2), int(3)], sp()); + let for_stmt = Stmt::For(ForStmt { + variable: ident("item"), + value_variable: None, + view: false, + iterable: list, + body: block(vec![assign( + "sum", + binary(var("sum"), BinOp::Add, var("item")), + )]), + span: sp(), + }); + interp.exec_stmt(&for_stmt).unwrap(); + assert_eq!(interp.eval_expr(&var("sum")).unwrap(), Value::Int64(6)); } #[test] - fn untrusted_registration_removes_previous_json_parse_exact_hook_trust() { + fn for_loop_with_break() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_parse_exact_reflected", - block(vec![return_stmt(string("trusted exact"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); + interp.exec_stmt(&var_decl("count", int(0))).unwrap(); - let fake_hook = generic_json_hook( - "json_parse_exact_reflected", - block(vec![return_stmt(string("fake exact"))]), - ); - interp.register_function_named("json.json_parse_exact_reflected", &fake_hook, false); + let list = Expr::ListConstruct(vec![int(1), int(2), int(3), int(4), int(5)], sp()); + let for_stmt = Stmt::For(ForStmt { + variable: ident("item"), + value_variable: None, + view: false, + iterable: list, + body: block(vec![ + // if item == 4: break + Stmt::If(IfStmt { + condition: binary(var("item"), BinOp::Eq, int(4)), + then_block: block(vec![Stmt::Break(sp())]), + else_ifs: vec![], + else_block: None, + span: sp(), + }), + assign("count", binary(var("count"), BinOp::Add, int(1))), + ]), + span: sp(), + }); + interp.exec_stmt(&for_stmt).unwrap(); + // Should have counted 1, 2, 3 (stopped before 4) + assert_eq!(interp.eval_expr(&var("count")).unwrap(), Value::Int64(3)); + } - let err = interp - .call_function_with_type_args( - "json.parse_exact", - &[type_named("string")], - vec![Value::String("\"value\"".to_string())], - ) - .unwrap_err(); + // ----------------------------------------------------------------------- + // While loop + // ----------------------------------------------------------------------- - assert_eq!( - err, - "json.parse_exact requires trusted stdlib hook 'json.json_parse_exact_reflected'" - ); + #[test] + fn while_loop_with_counter() { + let mut interp = Interpreter::new(); + interp.exec_stmt(&var_decl("i", int(0))).unwrap(); + interp.exec_stmt(&var_decl("sum", int(0))).unwrap(); + + // while i < 5: sum = sum + i; i = i + 1 + let while_stmt = Stmt::While(WhileStmt { + condition: binary(var("i"), BinOp::Lt, int(5)), + body: block(vec![ + assign("sum", binary(var("sum"), BinOp::Add, var("i"))), + assign("i", binary(var("i"), BinOp::Add, int(1))), + ]), + span: sp(), + }); + interp.exec_stmt(&while_stmt).unwrap(); + // sum = 0 + 1 + 2 + 3 + 4 = 10 + assert_eq!(interp.eval_expr(&var("sum")).unwrap(), Value::Int64(10)); } + // ----------------------------------------------------------------------- + // Function calls + // ----------------------------------------------------------------------- + #[test] - fn untrusted_registration_removes_previous_json_serialize_hook_trust() { + fn function_call_add() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_serialize_reflected", - block(vec![return_stmt(string("trusted"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); - let fake_hook = generic_json_hook( - "json_serialize_reflected", - block(vec![return_stmt(string("fake"))]), + // function add(a: int64, b: int64) returns int64: + // return a + b + let add_fn = func_def( + "add", + vec![("a", "int64"), ("b", "int64")], + block(vec![return_stmt(binary(var("a"), BinOp::Add, var("b")))]), ); - interp.register_function_named("json.json_serialize_reflected", &fake_hook, false); - - let err = interp - .call_function_with_type_args( - "json.serialize", - &[type_named("string")], - vec![Value::String("value".to_string())], - ) - .unwrap_err(); + interp.register_function(&add_fn); - assert_eq!( - err, - "json.serialize requires trusted stdlib hook 'json.json_serialize_reflected'" - ); + let result = interp + .eval_expr(&call("add", vec![int(3), int(4)])) + .unwrap(); + assert_eq!(result, Value::Int64(7)); } #[test] - fn untrusted_registration_removes_previous_json_serialize_public_hook_trust() { + fn function_call_no_return() { let mut interp = Interpreter::new(); - let mut trusted_hook = generic_json_hook( - "json_serialize_public_reflected", - block(vec![return_stmt(string("trusted"))]), - ); - trusted_hook.span = stdlib_sp(); - interp.register_function_in_namespace(Some("json"), &trusted_hook); - - let fake_hook = generic_json_hook( - "json_serialize_public_reflected", - block(vec![return_stmt(string("fake"))]), - ); - interp.register_function_named("json.json_serialize_public_reflected", &fake_hook, false); - let err = interp - .call_function_with_type_args( - "json.serialize_public", - &[type_named("string")], - vec![Value::String("value".to_string())], - ) - .unwrap_err(); + // function noop(): + // pass (empty body) + let noop_fn = func_def("noop", vec![], block(vec![])); + interp.register_function(&noop_fn); - assert_eq!( - err, - "json.serialize_public requires trusted stdlib hook 'json.json_serialize_public_reflected'" - ); + let result = interp.eval_expr(&call("noop", vec![])).unwrap(); + assert_eq!(result, Value::Nothing); } - // ----------------------------------------------------------------------- - // Variable declaration and lookup - // ----------------------------------------------------------------------- - #[test] - fn variable_declaration_and_lookup() { + fn function_expression_callees_support_named_captured_and_comptime_values() { let mut interp = Interpreter::new(); - let decl = var_decl("x", int(42)); - interp.exec_stmt(&decl).unwrap(); - let result = interp.eval_expr(&var("x")).unwrap(); - assert_eq!(result, Value::Int64(42)); + interp.register_function(&func_def( + "increment", + vec![("value", "int64")], + block(vec![return_stmt(binary(var("value"), BinOp::Add, int(1)))]), + )); + let callback = Expr::InlineFn( + vec![Param { + view: true, + mutable: false, + name: ident("value"), + ty: type_named("int64"), + span: sp(), + }], + Some(type_named("int64")), + block(vec![return_stmt(binary( + var("value"), + BinOp::Add, + var("delta"), + ))]), + sp(), + ); + interp.register_function(&func_def( + "factory", + vec![("delta", "int64")], + block(vec![return_stmt(callback.clone())]), + )); + interp.set_variable("callback", Value::NamedFunction("increment".into())); + assert_eq!( + interp.eval_expr(&call_expression( + Expr::Paren(Box::new(var("callback")), sp()), + vec![int(4)] + )), + Ok(Value::Int64(5)) + ); + assert_eq!( + interp.eval_expr(&call_expression( + call("factory", vec![int(10)]), + vec![Expr::View(Box::new(int(4)), sp())] + )), + Ok(Value::Int64(14)) + ); + interp.set_variable("delta", Value::Int64(20)); + assert_eq!( + interp.eval_expr(&call_expression(callback, vec![int(4)])), + Ok(Value::Int64(24)) + ); + let computed = Expr::Comptime( + Box::new(call_expression( + call("factory", vec![int(30)]), + vec![int(4)], + )), + sp(), + ); + assert_eq!(interp.eval_expr(&computed), Ok(Value::Int64(34))); } #[test] - fn uint64_variable_declaration_normalizes_small_literal_carrier() { + fn function_expression_fields_prefer_local_values_over_dotted_declarations() { let mut interp = Interpreter::new(); - interp - .exec_stmt(&typed_var_decl("uint64", "x", int(42))) - .unwrap(); - - assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Uint64(42)); + interp.register_function(&func_def( + "increment", + vec![("value", "int64")], + block(vec![return_stmt(binary(var("value"), BinOp::Add, int(1)))]), + )); + interp.register_function_in_namespace( + Some("wrapper"), + &func_def( + "invoke", + vec![("value", "int64")], + block(vec![return_stmt(int(99))]), + ), + ); + let field = Expr::FieldAccess(Box::new(var("wrapper")), ident("invoke"), sp()); + assert_eq!( + interp.eval_expr(&call_expression(field.clone(), vec![int(4)])), + Ok(Value::Int64(99)) + ); + interp.set_variable( + "wrapper", + Value::Struct { + concrete_type: None, + type_name: "Wrapper".into(), + fields: vec![("invoke".into(), Value::NamedFunction("increment".into()))], + }, + ); + assert_eq!( + interp.eval_expr(&call_expression(field.clone(), vec![int(4)])), + Ok(Value::Int64(5)) + ); + assert!( + matches!(interp.eval_expr(&field), Ok(Value::NamedFunction(name)) if name == "increment") + ); + interp.register_function(&func_def( + "wrapper_factory", + vec![], + block(vec![return_stmt(var("wrapper"))]), + )); + let computed_field = Expr::FieldAccess( + Box::new(call("wrapper_factory", vec![])), + ident("invoke"), + sp(), + ); + assert_eq!( + interp.eval_expr(&call_expression(computed_field, vec![int(4)])), + Ok(Value::Int64(5)) + ); } #[test] - fn uint64_function_boundaries_normalize_small_literal_carrier() { + fn function_expression_named_values_keep_their_registered_target_when_shadowed() { let mut interp = Interpreter::new(); - let mut echo = func_def( - "echo_u64", - vec![("value", "uint64")], - block(vec![return_stmt(var("value"))]), - ); - echo.return_type = Some(type_named("uint64")); - interp.register_function(&echo); - - let mut make = func_def("make_u64", vec![], block(vec![return_stmt(int(7))])); - make.return_type = Some(type_named("uint64")); - interp.register_function(&make); - + interp.register_function(&func_def( + "target", + vec![], + block(vec![return_stmt(int(1))]), + )); + interp.register_function(&func_def( + "replacement", + vec![], + block(vec![return_stmt(int(2))]), + )); + let original = interp.eval_expr(&var("target")).unwrap(); + interp.set_variable("target", Value::NamedFunction("replacement".into())); + interp.set_variable("saved", original); assert_eq!( - interp - .call_function("echo_u64", vec![Value::Int64(42)]) - .unwrap(), - Value::Uint64(42) + interp.eval_expr(&call("target", vec![])), + Ok(Value::Int64(2)) ); assert_eq!( - interp.call_function("make_u64", Vec::new()).unwrap(), - Value::Uint64(7) + interp.eval_expr(&call("saved", vec![])), + Ok(Value::Int64(1)) ); } #[test] - fn uint64_inline_function_parameter_normalizes_small_literal_carrier() { + fn function_expression_parenthesized_declarations_keep_named_argument_order() { let mut interp = Interpreter::new(); - let fn_value = Value::Function { - params: vec![Param { - view: false, - mutable: false, - name: ident("value"), - ty: type_named("uint64"), - span: sp(), - }], - body: block(vec![return_stmt(var("value"))]), - captures: HashMap::new(), - namespace: None, - }; - - assert_eq!( - interp - .call_fn_value(fn_value, vec![Value::Int64(42)]) - .unwrap(), - Value::Uint64(42) + interp.register_function(&func_def( + "subtract", + vec![("first", "int64"), ("second", "int64")], + block(vec![return_stmt(binary( + var("first"), + BinOp::Sub, + var("second"), + ))]), + )); + let call = Expr::Call( + Box::new(Expr::Paren(Box::new(var("subtract")), sp())), + vec![named_arg("second", int(4)), named_arg("first", int(10))], + sp(), ); + assert_eq!(interp.eval_expr(&call), Ok(Value::Int64(6))); } #[test] - fn uint64_actor_boundaries_normalize_small_literal_carriers() { + fn function_expression_enum_constructor_labels_preserve_source_evaluation_order() { let mut interp = Interpreter::new(); - let uint64_param = |name: &str| Param { - view: false, - mutable: false, - name: ident(name), - ty: type_named("uint64"), - span: sp(), - }; - let actor = ActorDef { - name: ident("Probe"), - capability_params: vec![uint64_param("seed")], - state_fields: vec![VarDecl { - mutable: true, - ty: type_named("uint64"), - name: ident("stored"), - value: var("seed"), - span: sp(), - }], - handlers: vec![ - ReceiveHandler { - name: ident("echo"), - params: vec![uint64_param("value")], - responds: Some(type_named("uint64")), - body: block(vec![respond_stmt(var("value"))]), - span: sp(), - }, - ReceiveHandler { - name: ident("stored"), - params: vec![], - responds: Some(type_named("uint64")), - body: block(vec![respond_stmt(var("stored"))]), - span: sp(), - }, - ReceiveHandler { - name: ident("reset"), - params: vec![], - responds: Some(type_named("uint64")), - body: block(vec![assign("stored", int(7)), respond_stmt(var("stored"))]), - span: sp(), - }, - ], - exported: false, + let mut pair = enum_def_with_field("Pair", "values", "first", "int64"); + pair.variants[0].fields.push(FieldDef { + name: ident("second"), + ty: type_named("int64"), + serialize_name: None, span: sp(), - }; - interp.register_actor(&actor); - - let actor_value = interp - .eval_expr(&Expr::Spawn( - Box::new(Expr::Call( - Box::new(var("Probe")), - vec![named_arg("seed", int(42))], + }); + interp.register_enum(&pair); + interp.register_enum_in_namespace(Some("types"), &pair); + interp.set_namespace_alias("alias".into(), "types".into()); + for (callee, expected_type) in [ + ( + Expr::EnumVariant(ident("Pair"), ident("values"), sp()), + "Pair", + ), + ( + Expr::Paren( + Box::new(Expr::FieldAccess( + Box::new(var("Pair")), + ident("values"), + sp(), + )), sp(), - )), - sp(), - )) - .unwrap(); - interp.set_variable("probe", actor_value); - - let ask_echo = Expr::Ask( - Box::new(Expr::Call( - Box::new(field_access(var("probe"), "echo")), - vec![CallArg { - name: None, - value: int(5), - span: sp(), - }], + ), + "Pair", + ), + ( + Expr::FieldAccess( + Box::new(Expr::FieldAccess( + Box::new(var("types")), + ident("Pair"), + sp(), + )), + ident("values"), + sp(), + ), + "types.Pair", + ), + ( + Expr::FieldAccess( + Box::new(Expr::FieldAccess( + Box::new(var("alias")), + ident("Pair"), + sp(), + )), + ident("values"), + sp(), + ), + "types.Pair", + ), + ] { + interp.set_variable("events", Value::Int64(0)); + let argument = |value, marker| { + Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![ + assign( + "events", + binary( + binary(var("events"), BinOp::Mul, int(10)), + BinOp::Add, + int(marker), + ), + ), + Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(int(value)), sp()), + span: sp(), + }), + ]), + sp(), + ) + }; + let expression = Expr::Call( + Box::new(callee), + vec![ + named_arg("second", argument(4, 1)), + named_arg("first", argument(10, 2)), + ], sp(), - )), - sp(), - ); - let ask_stored = Expr::Ask(Box::new(field_access(var("probe"), "stored")), sp()); - let ask_reset = Expr::Ask(Box::new(field_access(var("probe"), "reset")), sp()); - - assert_eq!(interp.eval_expr(&ask_echo).unwrap(), Value::Uint64(5)); - assert_eq!(interp.eval_expr(&ask_stored).unwrap(), Value::Uint64(42)); - assert_eq!(interp.eval_expr(&ask_reset).unwrap(), Value::Uint64(7)); - assert_eq!(interp.eval_expr(&ask_stored).unwrap(), Value::Uint64(7)); + ); + assert_eq!( + interp.eval_expr(&expression), + Ok(Value::Enum { + type_name: expected_type.into(), + variant: "values".into(), + fields: vec![Value::Int64(10), Value::Int64(4)], + }) + ); + assert_eq!(interp.get_variable("events"), Some(&Value::Int64(12))); + } } #[test] - fn variable_assignment() { - let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("x", int(1))).unwrap(); - interp.exec_stmt(&assign("x", int(99))).unwrap(); - assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Int64(99)); + fn enum_pipeline_payloads_keep_named_order_and_lexical_effects() { + for constructor in ["Triple.values", "(Triple.values)", "sample.Triple.values"] { + let source = format!( + r#"namespace sample +enum Triple: + values(first: int64, second: int64, third: int64) +function mark(label: string, value: int64) returns int64: + println(label) + return value +function make() returns Triple: + return mark("input", 1) into {constructor}(third: mark("third", 3), second: mark("second", 2)) +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + interp.enable_stdout_capture(); + assert_eq!( + interp.call_function("sample.make", vec![]), + Ok(Value::Enum { + type_name: "sample.Triple".into(), + variant: "values".into(), + fields: vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)], + }) + ); + assert!(interp.take_stdout_output().is_empty()); + assert_eq!( + crate::render_debug_events(&interp.take_debug_events()), + "input\nthird\nsecond\n", + ); + } } #[test] - fn uint64_variable_assignment_normalizes_small_literal_carrier() { - let mut interp = Interpreter::new(); - interp - .exec_stmt(&typed_var_decl("uint64", "x", int(1))) - .unwrap(); - interp.exec_stmt(&assign("x", int(42))).unwrap(); - - assert_eq!(interp.eval_expr(&var("x")).unwrap(), Value::Uint64(42)); + fn mixed_named_and_positional_function_arguments_fill_unassigned_parameters() { + for callee in ["combine", "(combine)", "sample.combine"] { + for piped in [false, true] { + let call = if piped { + format!( + "mark(\"first\", 1) into {callee}(third: mark(\"third\", 3), mark(\"second\", 2))" + ) + } else { + format!( + "{callee}(third: mark(\"third\", 3), mark(\"first\", 1), mark(\"second\", 2))" + ) + }; + let source = format!( + r#"namespace sample +function combine(first: int64, second: int64, third: int64) returns int64: + return first * 100 + second * 10 + third +function mark(label: string, value: int64) returns int64: + println(label) + return value +function make() returns int64: + return {call} +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + interp.enable_stdout_capture(); + assert_eq!( + interp.call_function("sample.make", vec![]), + Ok(Value::Int64(123)), + "{call}" + ); + assert_eq!( + crate::render_debug_events(&interp.take_debug_events()), + if piped { + "first\nthird\nsecond\n" + } else { + "third\nfirst\nsecond\n" + }, + "{call}" + ); + assert!(interp.take_stdout_output().is_empty()); + } + } } #[test] - fn uint64_parameter_assignment_normalizes_small_literal_carrier() { - let mut interp = Interpreter::new(); - let reassign = func_def( - "reassign_u64", - vec![("value", "uint64")], - block(vec![assign("value", int(7)), return_stmt(var("value"))]), - ); - interp.register_function(&reassign); - - assert_eq!( - interp - .call_function("reassign_u64", vec![Value::Int64(1)]) - .unwrap(), - Value::Uint64(7) - ); + fn mixed_named_and_positional_enum_arguments_fill_unassigned_payloads() { + for constructor in ["Triple.values", "(Triple.values)", "sample.Triple.values"] { + for piped in [false, true] { + let call = if piped { + format!( + "mark(\"first\", 1) into {constructor}(third: mark(\"third\", 3), mark(\"second\", 2))" + ) + } else { + format!( + "{constructor}(third: mark(\"third\", 3), mark(\"first\", 1), mark(\"second\", 2))" + ) + }; + let source = format!( + r#"namespace sample +enum Triple: + values(first: int64, second: int64, third: int64) +function mark(label: string, value: int64) returns int64: + println(label) + return value +function make() returns Triple: + return {call} +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + interp.enable_stdout_capture(); + assert_eq!( + interp.call_function("sample.make", vec![]), + Ok(Value::Enum { + type_name: "sample.Triple".into(), + variant: "values".into(), + fields: vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)], + }), + "{call}" + ); + assert_eq!( + crate::render_debug_events(&interp.take_debug_events()), + if piped { + "first\nthird\nsecond\n" + } else { + "third\nfirst\nsecond\n" + }, + "{call}" + ); + assert!(interp.take_stdout_output().is_empty()); + } + } } #[test] - fn trace_stmt_records_current_value() { + fn function_expression_callee_is_resolved_after_argument_handlers() { let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - - interp - .exec_stmt(&Stmt::Trace(TraceStmt { - name: ident("total"), - span: sp(), - })) - .unwrap(); + for (name, delta) in [("first", 10), ("second", 20)] { + interp.register_function(&func_def( + name, + vec![("value", "int64")], + block(vec![return_stmt(binary( + var("value"), + BinOp::Add, + int(delta), + ))]), + )); + } + interp.set_variable("selected", Value::NamedFunction("first".into())); + let argument = Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![ + assign("selected", var("second")), + Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(int(4)), sp()), + span: sp(), + }), + ]), + sp(), + ); + let expression = + call_expression(Expr::Paren(Box::new(var("selected")), sp()), vec![argument]); + assert_eq!(interp.eval_expr(&expression), Ok(Value::Int64(24))); - assert_eq!(interp.take_debug_output(), vec!["trace total: int64 = 42"]); + interp.set_variable("events", Value::Int64(0)); + interp.register_function(&func_def( + "factory", + vec![], + block(vec![ + assign( + "events", + binary( + binary(var("events"), BinOp::Mul, int(10)), + BinOp::Add, + int(2), + ), + ), + return_stmt(var("second")), + ]), + )); + let argument = Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![ + assign("events", int(1)), + Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(int(4)), sp()), + span: sp(), + }), + ]), + sp(), + ); + assert_eq!( + interp.eval_expr(&call_expression(call("factory", vec![]), vec![argument])), + Ok(Value::Int64(24)) + ); + assert_eq!(interp.get_variable("events"), Some(&Value::Int64(12))); } #[test] - fn breakpoint_stmt_records_visible_bindings() { + fn function_expression_signals_propagate_from_arguments_and_callee() { let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - - interp - .exec_stmt(&Stmt::Breakpoint(BreakpointStmt { - condition: Some(bool_expr(true)), - span: sp(), - })) - .unwrap(); - + interp.set_variable("events", Value::Int64(0)); + let signal = Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![return_stmt(int(7))]), + sp(), + ); + let marker = || { + Expr::Handle( + Box::new(Expr::None(sp())), + None, + block(vec![ + assign("events", binary(var("events"), BinOp::Add, int(1))), + Stmt::Expr(ExprStmt { + expr: Expr::Default(Box::new(int(4)), sp()), + span: sp(), + }), + ]), + sp(), + ) + }; + let expression = call_expression(signal.clone(), vec![marker()]); + assert!(matches!( + interp.eval_expr_flow(&expression), + Ok(ExprFlow::Signal(Signal::Return(Value::Int64(7), None))) + )); + assert_eq!(interp.get_variable("events"), Some(&Value::Int64(1))); + let expression = call_expression(marker(), vec![signal]); + assert!(matches!( + interp.eval_expr_flow(&expression), + Ok(ExprFlow::Signal(Signal::Return(Value::Int64(7), None))) + )); assert_eq!( - interp.take_debug_output(), - vec!["breakpoint hit: total: int64 = 42"] + interp.get_variable("events"), + Some(&Value::Int64(1)), + "callee must not run after an argument returns" ); } #[test] - fn breakpoint_stmt_skips_when_condition_is_false() { + fn function_expression_factories_do_not_capture_unrelated_caller_bindings() { let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("total", int(42))).unwrap(); - - interp - .exec_stmt(&Stmt::Breakpoint(BreakpointStmt { - condition: Some(bool_expr(false)), - span: sp(), - })) - .unwrap(); - - assert!(interp.take_debug_output().is_empty()); + interp.register_function(&func_def( + "target", + vec![], + block(vec![return_stmt(int(7))]), + )); + interp.register_function(&func_def( + "named_factory", + vec![], + block(vec![return_stmt(var("target"))]), + )); + interp.register_function(&func_def( + "closure_factory", + vec![], + block(vec![return_stmt(Expr::InlineFn( + vec![], + Some(type_named("int64")), + block(vec![return_stmt(call("target", vec![]))]), + sp(), + ))]), + )); + for factory in ["named_factory", "closure_factory"] { + interp.register_function(&func_def( + "caller", + vec![("target", "int64")], + block(vec![return_stmt(call_expression( + call(factory, vec![]), + vec![], + ))]), + )); + assert_eq!( + interp.eval_expr(&call("caller", vec![int(99)])), + Ok(Value::Int64(7)) + ); + } + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.scopes.len(), 1); } #[test] - fn undefined_variable_error() { - let mut interp = Interpreter::new(); - assert!(interp.eval_expr(&var("nonexistent")).is_err()); + fn declared_functions_do_not_inherit_caller_namespace_aliases() { + for invocation in [ + "return original.read_value()", + "return (original.read_value)()", + "function() returns int64 callback = original.read_value\n return callback()", + "return original.named_factory()()", + "return original.read_alias()", + ] { + let source = format!( + r#"namespace beta +export function value() returns int64: + return 9 +namespace alpha +export function value() returns int64: + return 1 +export function read_value() returns int64: + return alpha.value() +export function read_alias() returns int64: + use alpha as selected + return selected.value() +export function named_factory() returns function() returns int64: + return alpha.read_value +namespace app +function main() returns int64: + use alpha as original + use beta as alpha + use beta as selected + {invocation} +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + assert_eq!( + interp.call_function("app.main", vec![]), + Ok(Value::Int64(1)), + "{invocation}" + ); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.namespace_alias_scopes.len(), 1); + assert!(interp.visible_namespace_aliases().is_empty()); + } } - // ----------------------------------------------------------------------- - // If/else branching - // ----------------------------------------------------------------------- + #[test] + fn source_methods_preserve_their_declaration_namespace() { + for declaration in [ + "export struct Counter:\n value: int64\n function read(view self: Counter) returns int64:\n Payload computed = Payload(value: helper() + self.value)\n return computed.value + models.helper()", + "export interface Reader:\n function read(view self: Reader) returns int64\nexport struct Counter:\n value: int64\nimplement Reader for Counter:\n function read(view self: Counter) returns int64:\n Payload computed = Payload(value: helper() + self.value)\n return computed.value + models.helper()", + ] { + for invocation in [ + "return original.Counter.read(view counter)", + "return (original.Counter.read)(view counter)", + "function(view original.Counter) returns int64 callback = original.Counter.read\n return callback(view counter)", + "return original.make_reader()(view counter)", + "return original.local_read(view counter)", + ] { + let source = format!( + r#"namespace beta +export function helper() returns int64: + return 99 +namespace models +function helper() returns int64: + return 5 +struct Payload: + value: int64 +{declaration} +export function make_reader() returns function(view Counter) returns int64: + return models.Counter.read +export function local_read(view counter: Counter) returns int64: + return Counter.read(view counter) +namespace app +function main() returns int64: + use models as original + use beta as models + original.Counter counter = original.Counter(value: 2) + {invocation} +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + assert_eq!( + interp.call_function("app.main", vec![]), + Ok(Value::Int64(12)), + "{declaration}\n{invocation}" + ); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.namespace_alias_scopes.len(), 1); + assert_eq!(interp.current_namespace, None); + assert!(interp.visible_namespace_aliases().is_empty()); + + let method = field_access(field_access(var("models"), "Counter"), "read"); + let value = interp.eval_expr(&method).expect("method value"); + assert_eq!(value.to_string(), "function(models.Counter.read)"); + let TypeExpr::Function(params, return_type, _) = interp + .named_function_argument_type(&method) + .expect("method signature") + else { + panic!("expected function signature"); + }; + assert_eq!(type_expr_name(¶ms[0]), "models.Counter"); + assert_eq!(type_expr_name(&return_type), "int64"); + } + } + } #[test] - fn if_then_branch_taken() { - let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("result", int(0))).unwrap(); - let if_stmt = Stmt::If(IfStmt { - condition: bool_expr(true), - then_block: block(vec![assign("result", int(1))]), - else_ifs: vec![], - else_block: None, - span: sp(), - }); - interp.exec_stmt(&if_stmt).unwrap(); - assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(1)); + fn comptime_method_values_preserve_names_and_declaration_namespace() { + let source = r#"namespace models +function helper() returns int64: + return 5 +struct Counter: + value: int64 + function read(view self: Counter) returns int64: + return helper() + self.value +function make_reader() returns function(view Counter) returns int64: + return models.Counter.read +function main() returns nothing: + function(view Counter) returns int64 direct = comptime models.Counter.read + function(view Counter) returns int64 returned = comptime make_reader() + int64 answer = comptime models.Counter.read(view Counter(value: 2)) + return nothing +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let (values, diagnostics) = crate::evaluate_explicit_comptime_expressions( + &parsed.module, + Arc::new(ReflectionMetadata::new()), + Arc::new(CheckedExpressionTypes::default()), + Arc::new(HashMap::new()), + ); + assert!(diagnostics.is_empty(), "{diagnostics:?}"); + assert_eq!(values.len(), 3); + assert_eq!( + values + .values() + .filter(|value| matches!(value, Value::NamedFunction(name) if name == "models.Counter.read")) + .count(), + 2 + ); + assert!(values.values().any(|value| *value == Value::Int64(7))); } #[test] - fn if_else_branch_taken() { - let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("result", int(0))).unwrap(); - let if_stmt = Stmt::If(IfStmt { - condition: bool_expr(false), - then_block: block(vec![assign("result", int(1))]), - else_ifs: vec![], - else_block: Some(block(vec![assign("result", int(2))])), - span: sp(), - }); - interp.exec_stmt(&if_stmt).unwrap(); - assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(2)); + fn declared_interface_method_values_exist_before_implementations() { + let declaration = interface_decl( + "Reader", + vec![("read", vec![("self", "Reader", true)], "int64")], + ); + for namespace in [None, Some("models")] { + let mut interp = Interpreter::new(); + if let Some(namespace) = namespace { + interp.register_interface_in_namespace(Some(namespace), &declaration); + } else { + interp.register_interface(&declaration); + } + let owner = namespace.map_or_else( + || var("Reader"), + |namespace| field_access(var(namespace), "Reader"), + ); + let method = field_access(owner, "read"); + let name = namespace.map_or_else( + || "Reader.read".to_string(), + |namespace| format!("{namespace}.Reader.read"), + ); + let callback = interp.eval_expr(&method).expect("declared callback"); + assert!(matches!(&callback, Value::NamedFunction(target) if target == &name)); + assert_eq!(callback.to_string(), format!("function({name})")); + assert!(interp.interface_methods[&name].is_empty()); + assert_eq!( + interp.call_fn_value(callback.clone(), vec![Value::Int64(7)]), + Err(format!("undefined function '{name}'")) + ); + + let namespace_header = namespace + .map(|namespace| format!("namespace {namespace}\n")) + .unwrap_or_default(); + let source = format!( + "{namespace_header}implement Reader for int64:\n function read(view self: int64) returns int64:\n return self + 1\n" + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + interp.register_module(&parsed.module); + interp.register_interface_in_namespace(namespace, &declaration); + assert_eq!( + interp.call_fn_value(callback, vec![Value::Int64(7)]), + Ok(Value::Int64(8)) + ); + } } #[test] - fn if_else_if_branch_taken() { + fn interface_method_values_dispatch_by_owner_with_aliases_and_inherent_collision() { + let source = r#"namespace beta +export function helper() returns int64: + return 99 +export interface Reader: + function read(view self: Reader) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return 99 +namespace models +function helper() returns int64: + return 5 +export interface Reader: + function read(view self: Reader) returns int64 +export type Readable = Reader +export type Chained = Readable +export struct Counter: + value: int64 + function read(view self: Counter) returns int64: + return 1000 + self.value +implement Reader for Counter: + function read(view self: Counter) returns int64: + return helper() + self.value +implement Reader for int64: + function read(view self: int64) returns int64: + return helper() + self +export function make_reader() returns function(view Reader) returns int64: + return Readable.read +namespace app +function main() returns int64: + use models as original + use beta as models + original.Counter counter = original.Counter(value: 2) + original.Reader boxed = clone counter + original.Reader number = 7 + function(view original.Reader) returns int64 callback = original.Reader.read + int64 first = callback(view boxed) + function(view original.Readable) returns int64 aliased = original.Readable.read + int64 second = aliased(view number) + int64 third = original.make_reader()(view boxed) + function(view original.Chained) returns int64 chained = original.Chained.read + int64 fourth = chained(view number) + int64 inherent = original.Counter.read(view counter) + return first + second + third + fourth + inherent +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("result", int(0))).unwrap(); - let if_stmt = Stmt::If(IfStmt { - condition: bool_expr(false), - then_block: block(vec![assign("result", int(1))]), - else_ifs: vec![(bool_expr(true), block(vec![assign("result", int(3))]))], - else_block: Some(block(vec![assign("result", int(2))])), - span: sp(), - }); - interp.exec_stmt(&if_stmt).unwrap(); - assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(3)); + interp.register_module(&parsed.module); + assert_eq!( + interp.call_function("app.main", vec![]), + Ok(Value::Int64(1040)) + ); + let alias_method = field_access(field_access(var("models"), "Readable"), "read"); + assert!(matches!( + interp.eval_expr(&alias_method), + Ok(Value::NamedFunction(name)) if name == "models.Reader.read" + )); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.namespace_alias_scopes.len(), 1); + assert_eq!(interp.current_namespace, None); + assert!(interp.visible_namespace_aliases().is_empty()); } - // ----------------------------------------------------------------------- - // For loop - // ----------------------------------------------------------------------- - #[test] - fn for_loop_over_list() { + fn declared_interface_slots_take_precedence_over_colliding_source_methods() { + let source = r#"namespace models +interface Reader: + function read(view self: Reader) returns int64 +interface Other: + function read(view self: Other) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 +implement Other for Reader: + function read(view self: Reader) returns int64: + return 99 +function main() returns list[int64]: + Reader item = 7 + function(view Reader) returns int64 callback = Reader.read + int64 direct = Reader.read(view item) + int64 indirect = callback(view item) + return list(direct, indirect) +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("sum", int(0))).unwrap(); - - let list = Expr::ListConstruct(vec![int(1), int(2), int(3)], sp()); - let for_stmt = Stmt::For(ForStmt { - variable: ident("item"), - value_variable: None, - view: false, - iterable: list, - body: block(vec![assign( - "sum", - binary(var("sum"), BinOp::Add, var("item")), - )]), - span: sp(), - }); - interp.exec_stmt(&for_stmt).unwrap(); - assert_eq!(interp.eval_expr(&var("sum")).unwrap(), Value::Int64(6)); + interp.register_module(&parsed.module); + assert!(interp.functions.contains_key("models.Reader.read")); + assert_eq!( + interp.call_function("models.main", vec![]), + Ok(Value::List(vec![Value::Int64(8), Value::Int64(8)])) + ); + assert_eq!( + interp.call_function("models.Reader.read", vec![Value::Bool(true)]), + Err("undefined function 'models.Reader.read'".into()) + ); } #[test] - fn for_loop_with_break() { + fn interface_alias_method_values_ignore_callers_generic_type_bindings() { + let source = r#"namespace models +interface Reader: + function read(view self: Reader) returns int64 +type Readable = Reader +struct Counter: + value: int64 +implement Reader for Counter: + function read(view self: Counter) returns int64: + return self.value + 5 +implement Reader for int64: + function read(view self: int64) returns int64: + return 1 +function choose[Reader](unused: Reader) returns function(view models.Reader) returns int64: + return models.Readable.read +function main() returns int64: + Counter item = Counter(value: 2) + function(view models.Reader) returns int64 callback = choose[int64](7) + return callback(view item) +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("count", int(0))).unwrap(); - - let list = Expr::ListConstruct(vec![int(1), int(2), int(3), int(4), int(5)], sp()); - let for_stmt = Stmt::For(ForStmt { - variable: ident("item"), - value_variable: None, - view: false, - iterable: list, - body: block(vec![ - // if item == 4: break - Stmt::If(IfStmt { - condition: binary(var("item"), BinOp::Eq, int(4)), - then_block: block(vec![Stmt::Break(sp())]), - else_ifs: vec![], - else_block: None, - span: sp(), - }), - assign("count", binary(var("count"), BinOp::Add, int(1))), - ]), - span: sp(), - }); - interp.exec_stmt(&for_stmt).unwrap(); - // Should have counted 1, 2, 3 (stopped before 4) - assert_eq!(interp.eval_expr(&var("count")).unwrap(), Value::Int64(3)); + interp.register_module(&parsed.module); + assert!(matches!( + interp.call_function_with_type_args( + "models.choose", + &[type_named("int64")], + vec![Value::Int64(7)] + ), + Ok(Value::NamedFunction(name)) if name == "models.Reader.read" + )); + assert_eq!( + interp.call_function("models.main", vec![]), + Ok(Value::Int64(7)) + ); } - // ----------------------------------------------------------------------- - // While loop - // ----------------------------------------------------------------------- - #[test] - fn while_loop_with_counter() { + fn interface_method_callback_evaluates_arguments_before_callee_once() { + let source = r#"namespace app +interface Reader: + function read(view self: Reader) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 +function receiver(stdout: Stdout) returns Reader: + Stdout.write(view stdout, "argument\n") + return 7 +function select_reader(stdout: Stdout) returns function(view Reader) returns int64: + Stdout.write(view stdout, "callee\n") + return Reader.read +function main(stdout: Stdout) returns int64: + return select_reader(view stdout)(view receiver(view stdout)) +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); let mut interp = Interpreter::new(); - interp.exec_stmt(&var_decl("i", int(0))).unwrap(); - interp.exec_stmt(&var_decl("sum", int(0))).unwrap(); - - // while i < 5: sum = sum + i; i = i + 1 - let while_stmt = Stmt::While(WhileStmt { - condition: binary(var("i"), BinOp::Lt, int(5)), - body: block(vec![ - assign("sum", binary(var("sum"), BinOp::Add, var("i"))), - assign("i", binary(var("i"), BinOp::Add, int(1))), - ]), - span: sp(), - }); - interp.exec_stmt(&while_stmt).unwrap(); - // sum = 0 + 1 + 2 + 3 + 4 = 10 - assert_eq!(interp.eval_expr(&var("sum")).unwrap(), Value::Int64(10)); + interp.register_module(&parsed.module); + interp.enable_stdout_capture(); + assert_eq!( + interp.call_function("app.main", vec![Value::Nothing]), + Ok(Value::Int64(8)) + ); + assert_eq!(interp.take_stdout_output(), "argument\ncallee\n"); } - // ----------------------------------------------------------------------- - // Function calls - // ----------------------------------------------------------------------- + #[test] + fn interface_method_callback_preserves_pending_receiver_failures() { + let source = r#"namespace models +interface Reader: + function read(view self: Reader) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + let method = field_access(field_access(var("models"), "Reader"), "read"); + let callback = interp.eval_expr(&method).expect("interface callback"); + let mut pending = Value::Int64(7); + for _ in 0..2 { + pending = Value::Pending(Box::new(pending)); + let expected = Err("undefined function 'models.Reader.read'".into()); + assert_eq!( + interp.call_fn_value(callback.clone(), vec![pending.clone()]), + expected + ); + assert_eq!( + interp.call_function("models.Reader.read", vec![pending.clone()]), + expected + ); + } + assert_eq!( + interp.call_fn_value(callback, vec![Value::Int64(7)]), + Ok(Value::Int64(8)) + ); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.namespace_alias_scopes.len(), 1); + assert_eq!(interp.current_namespace, None); + } #[test] - fn function_call_add() { - let mut interp = Interpreter::new(); - - // function add(a: int64, b: int64) returns int64: - // return a + b - let add_fn = func_def( - "add", - vec![("a", "int64"), ("b", "int64")], - block(vec![return_stmt(binary(var("a"), BinOp::Add, var("b")))]), - ); - interp.register_function(&add_fn); + fn comptime_interface_method_values_keep_canonical_dispatch_names() { + let source = r#"namespace models +interface Reader: + function read(view self: Reader) returns int64 +type Readable = Reader +interface Reporter: + function emit(view self: Reporter, view stdout: Stdout) returns nothing +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 +function make_reader() returns function(view Reader) returns int64: + return Readable.read +function main() returns nothing: + function(view Reader) returns int64 direct = comptime models.Reader.read + function(view Readable) returns int64 aliased = comptime Readable.read + function(view Reader) returns int64 returned = comptime make_reader() + function(view Reporter, view Stdout) returns nothing reporter = comptime Reporter.emit + int64 answer = comptime (models.Reader.read)(view 7) + return nothing +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let (values, diagnostics) = crate::evaluate_explicit_comptime_expressions( + &parsed.module, + Arc::new(ReflectionMetadata::new()), + Arc::new(CheckedExpressionTypes::default()), + Arc::new(HashMap::new()), + ); + assert!(diagnostics.is_empty(), "{diagnostics:?}"); + assert_eq!(values.len(), 5); + assert_eq!( + values + .values() + .filter(|value| matches!(value, Value::NamedFunction(name) if name == "models.Reader.read")) + .count(), + 3 + ); + assert!(values.values().any( + |value| matches!(value, Value::NamedFunction(name) if name == "models.Reporter.emit") + )); + assert!(values.values().any(|value| *value == Value::Int64(8))); + } - let result = interp - .eval_expr(&call("add", vec![int(3), int(4)])) - .unwrap(); - assert_eq!(result, Value::Int64(7)); + #[test] + fn returned_closures_preserve_lexical_namespace_aliases() { + for (invocation, expected) in [ + ("return original.make_reader()()", 1), + ( + "function() returns int64 callback = original.make_reader()\n return callback()", + 1, + ), + ("return original.invoke(original.make_reader())", 1), + ("return original.make_canonical_reader()()", 1), + ("return original.make_shadowing_reader()()", 9), + ("return comptime original.make_reader()()", 1), + ] { + let source = format!( + r#"namespace beta +export function value() returns int64: + return 9 +namespace alpha +export function value() returns int64: + return 1 +export function make_reader() returns function() returns int64: + use alpha as selected + return function() returns int64: + return selected.value() +export function make_canonical_reader() returns function() returns int64: + return function() returns int64: + return alpha.value() +export function make_shadowing_reader() returns function() returns int64: + use alpha as selected + return function() returns int64: + use beta as selected + return selected.value() +export function invoke(callback: function() returns int64) returns int64: + use beta as selected + return callback() +namespace app +function main() returns int64: + use alpha as original + use beta as alpha + use beta as selected + {invocation} +"# + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + assert_eq!( + interp.call_function("app.main", vec![]), + Ok(Value::Int64(expected)), + "{invocation}" + ); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.namespace_alias_scopes.len(), 1); + assert!(interp.visible_namespace_aliases().is_empty()); + } } #[test] - fn function_call_no_return() { + fn function_expression_errors_restore_scope_visibility() { let mut interp = Interpreter::new(); - - // function noop(): - // pass (empty body) - let noop_fn = func_def("noop", vec![], block(vec![])); - interp.register_function(&noop_fn); - - let result = interp.eval_expr(&call("noop", vec![])).unwrap(); - assert_eq!(result, Value::Nothing); + interp.set_variable("visible", Value::Int64(7)); + let failure = block(vec![assert_stmt(bool_expr(false), None)]); + interp.register_function(&func_def("failure", vec![], failure.clone())); + for expression in [ + call_expression(Expr::Paren(Box::new(var("failure")), sp()), vec![]), + call_expression(Expr::InlineFn(vec![], None, failure, sp()), vec![]), + ] { + assert_eq!( + interp.eval_expr(&expression), + Err("assertion failed".into()) + ); + assert_eq!(interp.lexical_scope_floor, 0); + assert_eq!(interp.scopes.len(), 1); + assert_eq!(interp.get_variable("visible"), Some(&Value::Int64(7))); + } } #[test] @@ -15788,6 +25531,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::Struct { + concrete_type: None, type_name: "Point".to_string(), fields: vec![ ("x".to_string(), Value::Int64(3)), @@ -15811,6 +25555,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::Struct { + concrete_type: None, type_name: "Packet".to_string(), fields: vec![("serial".to_string(), Value::Uint64(42))], } @@ -15836,6 +25581,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::ResultOk(Box::new(Value::Struct { + concrete_type: None, type_name: "User".to_string(), fields: vec![("age".to_string(), Value::Int64(42))], })) @@ -15903,6 +25649,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::Struct { + concrete_type: None, type_name: "TcpFlags".to_string(), fields: vec![ ("syn".to_string(), Value::Int64(0)), @@ -15946,6 +25693,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::ResultOk(Box::new(Value::Struct { + concrete_type: None, type_name: "TcpFlags".to_string(), fields: vec![ ("syn".to_string(), Value::Int64(1)), @@ -16130,6 +25878,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::ResultOk(Box::new(Value::Struct { + concrete_type: None, type_name: "IpHeader".to_string(), fields: vec![ ("version".to_string(), Value::Int64(4)), @@ -16200,6 +25949,7 @@ mod tests { assert_eq!( interp.eval_expr(&expr).unwrap(), Value::ResultOk(Box::new(Value::Struct { + concrete_type: None, type_name: "Header".to_string(), fields: vec![( "protocol".to_string(), @@ -16341,12 +26091,14 @@ mod tests { let mut interp = Interpreter::new(); let items = Value::List(vec![Value::Int64(7)]); let inner = Value::Struct { + concrete_type: None, type_name: "Inner".to_string(), fields: vec![("items".to_string(), items.clone())], }; interp.set_variable( "outer", Value::Struct { + concrete_type: None, type_name: "Outer".to_string(), fields: vec![ ("inner".to_string(), inner), @@ -16729,794 +26481,88 @@ mod tests { Value::Enum { type_name: "Shape".to_string(), variant: "rect".to_string(), - fields: vec![Value::Int64(3), Value::Int64(4)], - }, - ); - - let m = match_stmt( - var("shape"), - vec![ - ( - Pattern::Variant(ident("circle"), vec![ident("r")]), - vec![assign("result", var("r"))], - ), - ( - Pattern::Variant(ident("rect"), vec![ident("w"), ident("h")]), - vec![assign("result", binary(var("w"), BinOp::Add, var("h")))], - ), - ], - ); - interp.exec_stmt(&m).unwrap(); - assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(7)); - } - - #[test] - fn match_other_catch_all() { - // match Color.blue: - // red: result = "red" - // other: result = "other" - let mut interp = Interpreter::new(); - interp - .exec_stmt(&var_decl("result", string("none"))) - .unwrap(); - - let m = match_stmt( - enum_variant("Color", "blue"), - vec![ - ( - Pattern::Ident(ident("red")), - vec![assign("result", string("red"))], - ), - ( - Pattern::Other(sp()), - vec![assign("result", string("other"))], - ), - ], - ); - interp.exec_stmt(&m).unwrap(); - assert_eq!( - interp.eval_expr(&var("result")).unwrap(), - Value::String("other".to_string()) - ); - } - - #[test] - fn match_with_return_signal() { - // Test that return inside a match arm propagates correctly. - let mut interp = Interpreter::new(); - - // function pick(color: ?) returns string: - // match color: - // red: return "red" - // other: return "unknown" - let b = block(vec![match_stmt( - var("color"), - vec![ - ( - Pattern::Ident(ident("red")), - vec![return_stmt(string("red"))], - ), - (Pattern::Other(sp()), vec![return_stmt(string("unknown"))]), - ], - )]); - - // Execute as a block with a variable set up. - interp.set_variable( - "color", - Value::Enum { - type_name: "Color".to_string(), - variant: "red".to_string(), - fields: vec![], - }, - ); - let result = interp.exec_block(&b).unwrap(); - assert_eq!(result, Some(Value::String("red".to_string()))); - } -} - -// --------------------------------------------------------------------------- -// Base64 helpers (no external crate dependency) -// --------------------------------------------------------------------------- - -fn base64_encode(data: &[u8]) -> String { - const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let mut out = String::new(); - let mut i = 0; - while i < data.len() { - let b0 = data[i] as u32; - let b1 = if i + 1 < data.len() { - data[i + 1] as u32 - } else { - 0 - }; - let b2 = if i + 2 < data.len() { - data[i + 2] as u32 - } else { - 0 - }; - let combined = (b0 << 16) | (b1 << 8) | b2; - out.push(ALPHABET[((combined >> 18) & 63) as usize] as char); - out.push(ALPHABET[((combined >> 12) & 63) as usize] as char); - if i + 1 < data.len() { - out.push(ALPHABET[((combined >> 6) & 63) as usize] as char); - } else { - out.push('='); - } - if i + 2 < data.len() { - out.push(ALPHABET[(combined & 63) as usize] as char); - } else { - out.push('='); - } - i += 3; - } - out -} - -// --------------------------------------------------------------------------- -// SHA-256 helper (no external crate dependency) -// --------------------------------------------------------------------------- - -fn sha256_digest(data: &[u8]) -> Vec { - #[rustfmt::skip] - const K: [u32; 64] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, - 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, - 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, - 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, - 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, - 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, - ]; - let mut h: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ]; - let bit_len = (data.len() as u64).wrapping_mul(8); - let mut msg = data.to_vec(); - msg.push(0x80); - while msg.len() % 64 != 56 { - msg.push(0x00); - } - msg.extend_from_slice(&bit_len.to_be_bytes()); - for chunk in msg.chunks(64) { - let mut w = [0u32; 64]; - for i in 0..16 { - w[i] = u32::from_be_bytes([ - chunk[i * 4], - chunk[i * 4 + 1], - chunk[i * 4 + 2], - chunk[i * 4 + 3], - ]); - } - for i in 16..64 { - let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); - let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); - w[i] = w[i - 16] - .wrapping_add(s0) - .wrapping_add(w[i - 7]) - .wrapping_add(s1); - } - let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; - for i in 0..64 { - let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); - let ch = (e & f) ^ ((!e) & g); - let temp1 = hh - .wrapping_add(s1) - .wrapping_add(ch) - .wrapping_add(K[i]) - .wrapping_add(w[i]); - let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); - let maj = (a & b) ^ (a & c) ^ (b & c); - let temp2 = s0.wrapping_add(maj); - hh = g; - g = f; - f = e; - e = d.wrapping_add(temp1); - d = c; - c = b; - b = a; - a = temp1.wrapping_add(temp2); - } - h[0] = h[0].wrapping_add(a); - h[1] = h[1].wrapping_add(b); - h[2] = h[2].wrapping_add(c); - h[3] = h[3].wrapping_add(d); - h[4] = h[4].wrapping_add(e); - h[5] = h[5].wrapping_add(f); - h[6] = h[6].wrapping_add(g); - h[7] = h[7].wrapping_add(hh); - } - h.into_iter() - .flat_map(u32::to_be_bytes) - .collect::>() -} - -fn hmac_sha256_digest(key: &[u8], message: &[u8]) -> Vec { - const BLOCK_SIZE: usize = 64; - let normalized_key = if key.len() > BLOCK_SIZE { - sha256_digest(key) - } else { - key.to_vec() - }; - let mut key_block = [0u8; BLOCK_SIZE]; - key_block[..normalized_key.len()].copy_from_slice(&normalized_key); - - let mut inner = Vec::with_capacity(BLOCK_SIZE + message.len()); - inner.extend(key_block.iter().map(|byte| byte ^ 0x36)); - inner.extend_from_slice(message); - let inner_digest = sha256_digest(&inner); - - let mut outer = Vec::with_capacity(BLOCK_SIZE + inner_digest.len()); - outer.extend(key_block.iter().map(|byte| byte ^ 0x5c)); - outer.extend_from_slice(&inner_digest); - sha256_digest(&outer) -} - -// --------------------------------------------------------------------------- -// SHA-512 helper (no external crate dependency) -// --------------------------------------------------------------------------- - -fn sha512_digest(data: &[u8]) -> Vec { - #[rustfmt::skip] - const K: [u64; 80] = [ - 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, - 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, - 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, - 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, - 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, - 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, - 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, - 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, - 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, - 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, - 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, - 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, - 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, - 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, - 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, - 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, - 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, - 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, - 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, - 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, - ]; - let mut h: [u64; 8] = [ - 0x6a09e667f3bcc908, - 0xbb67ae8584caa73b, - 0x3c6ef372fe94f82b, - 0xa54ff53a5f1d36f1, - 0x510e527fade682d1, - 0x9b05688c2b3e6c1f, - 0x1f83d9abfb41bd6b, - 0x5be0cd19137e2179, - ]; - let bit_len = (data.len() as u128).wrapping_mul(8); - let mut msg = data.to_vec(); - msg.push(0x80); - while msg.len() % 128 != 112 { - msg.push(0x00); - } - msg.extend_from_slice(&bit_len.to_be_bytes()); - for chunk in msg.chunks(128) { - let mut w = [0u64; 80]; - for i in 0..16 { - w[i] = u64::from_be_bytes([ - chunk[i * 8], - chunk[i * 8 + 1], - chunk[i * 8 + 2], - chunk[i * 8 + 3], - chunk[i * 8 + 4], - chunk[i * 8 + 5], - chunk[i * 8 + 6], - chunk[i * 8 + 7], - ]); - } - for i in 16..80 { - let s0 = w[i - 15].rotate_right(1) ^ w[i - 15].rotate_right(8) ^ (w[i - 15] >> 7); - let s1 = w[i - 2].rotate_right(19) ^ w[i - 2].rotate_right(61) ^ (w[i - 2] >> 6); - w[i] = w[i - 16] - .wrapping_add(s0) - .wrapping_add(w[i - 7]) - .wrapping_add(s1); - } - let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; - for i in 0..80 { - let s1 = e.rotate_right(14) ^ e.rotate_right(18) ^ e.rotate_right(41); - let ch = (e & f) ^ ((!e) & g); - let temp1 = hh - .wrapping_add(s1) - .wrapping_add(ch) - .wrapping_add(K[i]) - .wrapping_add(w[i]); - let s0 = a.rotate_right(28) ^ a.rotate_right(34) ^ a.rotate_right(39); - let maj = (a & b) ^ (a & c) ^ (b & c); - let temp2 = s0.wrapping_add(maj); - hh = g; - g = f; - f = e; - e = d.wrapping_add(temp1); - d = c; - c = b; - b = a; - a = temp1.wrapping_add(temp2); - } - h[0] = h[0].wrapping_add(a); - h[1] = h[1].wrapping_add(b); - h[2] = h[2].wrapping_add(c); - h[3] = h[3].wrapping_add(d); - h[4] = h[4].wrapping_add(e); - h[5] = h[5].wrapping_add(f); - h[6] = h[6].wrapping_add(g); - h[7] = h[7].wrapping_add(hh); - } - h.into_iter() - .flat_map(u64::to_be_bytes) - .collect::>() -} - -// --------------------------------------------------------------------------- -// MD5 helper (no external crate dependency) -// --------------------------------------------------------------------------- - -fn md5_digest(data: &[u8]) -> Vec { - #[rustfmt::skip] - const S: [u32; 64] = [ - 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, - 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, - 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, - 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, - ]; - #[rustfmt::skip] - const K: [u32; 64] = [ - 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, - 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, - 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, - 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, - 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, - 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, - 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, - 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, - 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, - 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, - 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, - 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, - 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, - 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, - 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, - 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, - ]; - let mut a0: u32 = 0x67452301; - let mut b0: u32 = 0xefcdab89; - let mut c0: u32 = 0x98badcfe; - let mut d0: u32 = 0x10325476; - let bit_len = (data.len() as u64).wrapping_mul(8); - let mut msg = data.to_vec(); - msg.push(0x80); - while msg.len() % 64 != 56 { - msg.push(0x00); - } - msg.extend_from_slice(&bit_len.to_le_bytes()); - for chunk in msg.chunks(64) { - let mut m = [0u32; 16]; - for i in 0..16 { - m[i] = u32::from_le_bytes([ - chunk[i * 4], - chunk[i * 4 + 1], - chunk[i * 4 + 2], - chunk[i * 4 + 3], - ]); - } - let mut a = a0; - let mut b = b0; - let mut c = c0; - let mut d = d0; - for i in 0..64usize { - let (f, g) = if i < 16 { - ((b & c) | ((!b) & d), i) - } else if i < 32 { - ((d & b) | ((!d) & c), (5 * i + 1) % 16) - } else if i < 48 { - (b ^ c ^ d, (3 * i + 5) % 16) - } else { - (c ^ (b | (!d)), (7 * i) % 16) - }; - let temp = d; - d = c; - c = b; - b = b.wrapping_add( - a.wrapping_add(f) - .wrapping_add(K[i]) - .wrapping_add(m[g]) - .rotate_left(S[i]), - ); - a = temp; - } - a0 = a0.wrapping_add(a); - b0 = b0.wrapping_add(b); - c0 = c0.wrapping_add(c); - d0 = d0.wrapping_add(d); - } - let mut out = [0u8; 16]; - out[0..4].copy_from_slice(&a0.to_le_bytes()); - out[4..8].copy_from_slice(&b0.to_le_bytes()); - out[8..12].copy_from_slice(&c0.to_le_bytes()); - out[12..16].copy_from_slice(&d0.to_le_bytes()); - out.to_vec() -} - -fn base64_decode(s: &str) -> Result, &'static str> { - fn char_val(c: u8) -> u32 { - match c { - b'A'..=b'Z' => (c - b'A') as u32, - b'a'..=b'z' => (c - b'a' + 26) as u32, - b'0'..=b'9' => (c - b'0' + 52) as u32, - b'+' => 62, - b'/' => 63, - b'=' => 0, - _ => unreachable!("alphabet was validated before decoding"), - } - } - - let bytes = s.as_bytes(); - if bytes.len() % 4 != 0 { - return Err("invalid length"); - } - if bytes.iter().any(|byte| { - !matches!( - byte, - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' - ) - }) { - return Err("invalid character"); - } - - let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count(); - if padding > 2 - || bytes[..bytes.len().saturating_sub(padding)].contains(&b'=') - || (padding > 0 && bytes.len() < 4) - { - return Err("invalid padding"); - } - - let mut out = Vec::new(); - for chunk in bytes.chunks_exact(4) { - let chunk_padding = usize::from(chunk[3] == b'=') + usize::from(chunk[2] == b'='); - let v0 = char_val(chunk[0]); - let v1 = char_val(chunk[1]); - let v2 = char_val(chunk[2]); - let v3 = char_val(chunk[3]); - if (chunk_padding == 2 && v1 & 0x0F != 0) || (chunk_padding == 1 && v2 & 0x03 != 0) { - return Err("non-zero trailing bits"); - } - let combined = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3; - out.push(((combined >> 16) & 0xFF) as u8); - if chunk_padding < 2 { - out.push(((combined >> 8) & 0xFF) as u8); - } - if chunk_padding == 0 { - out.push((combined & 0xFF) as u8); - } - } - Ok(out) -} - -fn encoding_hex_decode(s: &str) -> Result, &'static str> { - fn nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } - } - - let raw = s.as_bytes(); - if raw.len() % 2 != 0 { - return Err("odd-length hex string"); - } - raw.chunks_exact(2) - .map(|pair| { - let high = nibble(pair[0]).ok_or("invalid hex characters")?; - let low = nibble(pair[1]).ok_or("invalid hex characters")?; - Ok((high << 4) | low) - }) - .collect() -} - -fn percent_encode(value: &str, form: bool) -> String { - let mut output = String::new(); - for byte in value.bytes() { - let literal = byte.is_ascii_alphanumeric() - || if form { - matches!(byte, b'*' | b'-' | b'.' | b'_') - } else { - matches!(byte, b'-' | b'.' | b'_' | b'~') - }; - if literal { - output.push(byte as char); - } else if form && byte == b' ' { - output.push('+'); - } else { - output.push_str(&format!("%{byte:02X}")); - } - } - output -} - -fn percent_decode(value: &str, form: bool) -> Result { - fn nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } - } - - let malformed = "malformed percent escape"; - let invalid_utf8 = "decoded bytes are not valid UTF-8"; - let input = value.as_bytes(); - let mut output = Vec::with_capacity(input.len()); - let mut index = 0; - while index < input.len() { - if input[index] == b'%' { - if index + 2 >= input.len() { - return Err(malformed); - } - let high = nibble(input[index + 1]).ok_or(malformed)?; - let low = nibble(input[index + 2]).ok_or(malformed)?; - output.push((high << 4) | low); - index += 3; - } else { - output.push(if form && input[index] == b'+' { - b' ' - } else { - input[index] - }); - index += 1; - } - } - String::from_utf8(output).map_err(|_| invalid_utf8) -} - -// --------------------------------------------------------------------------- -// CSV helpers -// --------------------------------------------------------------------------- - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum CsvFieldState { - Start, - Unquoted, - Quoted, - AfterQuote, -} - -/// Parse strict CSV records. Quotes may only open at the start of a field, -/// doubled quotes escape a quote inside a quoted field, and a closing quote -/// must be followed by a delimiter, record terminator, or end of input. Record -/// terminators are LF or CRLF; a bare CR is rejected outside quoted fields. -/// Leading and trailing whitespace in unquoted fields is data and is never -/// trimmed implicitly. -fn parse_csv_records(input: &str) -> Result>, String> { - let input = input.strip_prefix('\u{feff}').unwrap_or(input); - // Empty CSV contains no records. A physical line terminator is different: - // it is consumed below and preserves one explicitly empty record. - if input.is_empty() { - return Ok(Vec::new()); - } - - let mut records = Vec::new(); - let mut row = Vec::new(); - let mut current = String::new(); - let mut state = CsvFieldState::Start; - let mut has_record_data = false; - let mut record_number = 1usize; - let mut chars = input.chars().peekable(); - - while let Some(ch) = chars.next() { - match state { - CsvFieldState::Start => match ch { - '"' => { - has_record_data = true; - state = CsvFieldState::Quoted; - } - ',' => { - has_record_data = true; - row.push(std::mem::take(&mut current)); - } - '\n' => { - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - record_number += 1; - } - '\r' => { - if chars.peek() != Some(&'\n') { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "bare carriage return; use LF or CRLF record endings", - )); - } - chars.next(); - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - record_number += 1; - } - _ => { - has_record_data = true; - current.push(ch); - state = CsvFieldState::Unquoted; - } - }, - CsvFieldState::Unquoted => match ch { - '"' => { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "unexpected quote in unquoted field", - )); - } - ',' => { - row.push(std::mem::take(&mut current)); - state = CsvFieldState::Start; - } - '\n' => { - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - state = CsvFieldState::Start; - record_number += 1; - } - '\r' => { - if chars.peek() != Some(&'\n') { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "bare carriage return; use LF or CRLF record endings", - )); - } - chars.next(); - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - state = CsvFieldState::Start; - record_number += 1; - } - _ => current.push(ch), - }, - CsvFieldState::Quoted => { - if ch == '"' { - if chars.peek() == Some(&'"') { - current.push('"'); - chars.next(); - } else { - state = CsvFieldState::AfterQuote; - } - } else { - current.push(ch); - } - } - CsvFieldState::AfterQuote => match ch { - ',' => { - row.push(std::mem::take(&mut current)); - state = CsvFieldState::Start; - } - '\n' => { - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - state = CsvFieldState::Start; - record_number += 1; - } - '\r' => { - if chars.peek() != Some(&'\n') { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "bare carriage return; use LF or CRLF record endings", - )); - } - chars.next(); - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - true, - ); - state = CsvFieldState::Start; - record_number += 1; - } - _ => { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "unexpected character after closing quote", - )); - } + fields: vec![Value::Int64(3), Value::Int64(4)], }, - } - } + ); - if state == CsvFieldState::Quoted { - return Err(csv_parse_error( - record_number, - row.len() + 1, - "unterminated quoted field", - )); + let m = match_stmt( + var("shape"), + vec![ + ( + Pattern::Variant(ident("circle"), vec![ident("r")]), + vec![assign("result", var("r"))], + ), + ( + Pattern::Variant(ident("rect"), vec![ident("w"), ident("h")]), + vec![assign("result", binary(var("w"), BinOp::Add, var("h")))], + ), + ], + ); + interp.exec_stmt(&m).unwrap(); + assert_eq!(interp.eval_expr(&var("result")).unwrap(), Value::Int64(7)); } - // A trailing record terminator has already preserved its record and must - // not add another empty record at end of input. - push_csv_record( - &mut records, - &mut row, - &mut current, - &mut has_record_data, - false, - ); - Ok(records) -} - -fn csv_parse_error(record: usize, field: usize, message: &str) -> String { - format!("CSV parse error at record {record}, field {field}: {message}") -} + #[test] + fn match_other_catch_all() { + // match Color.blue: + // red: result = "red" + // other: result = "other" + let mut interp = Interpreter::new(); + interp + .exec_stmt(&var_decl("result", string("none"))) + .unwrap(); -fn push_csv_record( - records: &mut Vec>, - row: &mut Vec, - current: &mut String, - has_record_data: &mut bool, - preserve_empty: bool, -) { - if preserve_empty || *has_record_data || !current.is_empty() || !row.is_empty() { - row.push(std::mem::take(current)); - records.push(std::mem::take(row)); - } else { - row.clear(); - current.clear(); + let m = match_stmt( + enum_variant("Color", "blue"), + vec![ + ( + Pattern::Ident(ident("red")), + vec![assign("result", string("red"))], + ), + ( + Pattern::Other(sp()), + vec![assign("result", string("other"))], + ), + ], + ); + interp.exec_stmt(&m).unwrap(); + assert_eq!( + interp.eval_expr(&var("result")).unwrap(), + Value::String("other".to_string()) + ); } - *has_record_data = false; -} -/// Quote a CSV field if it contains commas, quotes, or newlines. -fn csv_quote_field(s: &str) -> String { - if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') { - let escaped = s.replace('"', "\"\""); - format!("\"{escaped}\"") - } else { - s.to_string() + #[test] + fn match_with_return_signal() { + // Test that return inside a match arm propagates correctly. + let mut interp = Interpreter::new(); + + // function pick(color: ?) returns string: + // match color: + // red: return "red" + // other: return "unknown" + let b = block(vec![match_stmt( + var("color"), + vec![ + ( + Pattern::Ident(ident("red")), + vec![return_stmt(string("red"))], + ), + (Pattern::Other(sp()), vec![return_stmt(string("unknown"))]), + ], + )]); + + // Execute as a block with a variable set up. + interp.set_variable( + "color", + Value::Enum { + type_name: "Color".to_string(), + variant: "red".to_string(), + fields: vec![], + }, + ); + let result = interp.exec_block(&b).unwrap(); + assert_eq!(result, Some(Value::String("red".to_string()))); } } @@ -17607,8 +26653,218 @@ mod builtin_tests { assert_eq!(result, Value::Nothing); } + fn expected_debug_event(kind: DebugEventKind, text: &str) -> DebugEvent { + DebugEvent { + kind, + text: text.to_owned(), + } + } + + fn debug_call(name: &str, arguments: Vec) -> Expr { + Expr::Call( + Box::new(var(name)), + arguments + .into_iter() + .map(|value| CallArg { + name: None, + value, + span: sp(), + }) + .collect(), + sp(), + ) + } + + #[test] + fn debug_prints_capture_partial_empty_multiline_and_spoofed_text() { + let mut interp = Interpreter::new(); + interp.enable_stdout_capture(); + for (name, arguments) in [ + ("print", vec![]), + ("print", vec![Value::String("head:".into())]), + ( + "print", + vec![Value::Int64(7), Value::String("λ\n🙂,\\\r".into())], + ), + ("println", vec![]), + ( + "println", + vec![Value::String("trace fake\nbreakpoint hit".into())], + ), + ] { + assert_eq!( + interp.call_builtin(name, &arguments), + Some(Ok(Value::Nothing)), + ); + } + + let events = interp.take_debug_events(); + assert_eq!( + events, + [ + expected_debug_event(DebugEventKind::Print, ""), + expected_debug_event(DebugEventKind::Print, "head:"), + expected_debug_event(DebugEventKind::Print, "7 λ\n🙂,\\\r"), + expected_debug_event(DebugEventKind::Println, "\n"), + expected_debug_event(DebugEventKind::Println, "trace fake\nbreakpoint hit\n"), + ], + ); + assert_eq!( + crate::render_debug_events(&events), + "head:7 λ\n🙂,\\\r\ntrace fake\nbreakpoint hit\n", + ); + assert!(interp.take_stdout_output().is_empty()); + assert!(interp.take_debug_events().is_empty()); + } + + #[test] + fn debug_print_trace_and_breakpoint_preserve_one_ordered_stream() { + let mut interp = Interpreter::new(); + interp.enable_stdout_capture(); + interp.set_variable_with_type("total", Value::Int64(42), TypeExpr::Named(ident("int64"))); + assert_eq!( + interp.call_builtin("print", &[Value::String("partial:".into())]), + Some(Ok(Value::Nothing)), + ); + interp + .exec_stmt(&Stmt::Trace(TraceStmt { + name: ident("total"), + span: sp(), + })) + .unwrap(); + assert_eq!( + interp.call_builtin( + "Stdout.write", + &[Value::Nothing, Value::String("application".into())], + ), + Some(Ok(Value::Nothing)), + ); + interp + .exec_stmt(&Stmt::Breakpoint(BreakpointStmt { + condition: Some(Expr::BoolLiteral(true, sp())), + span: sp(), + })) + .unwrap(); + assert_eq!( + interp.call_builtin("println", &[Value::String("done".into())]), + Some(Ok(Value::Nothing)), + ); + + let events = interp.take_debug_events(); + assert_eq!( + events, + [ + expected_debug_event(DebugEventKind::Print, "partial:"), + expected_debug_event(DebugEventKind::Trace, "trace total: int64 = 42\n"), + expected_debug_event( + DebugEventKind::Breakpoint, + "breakpoint hit: total: int64 = 42\n", + ), + expected_debug_event(DebugEventKind::Println, "done\n"), + ], + ); + assert_eq!( + crate::render_debug_events(&events), + "partial:trace total: int64 = 42\nbreakpoint hit: total: int64 = 42\ndone\n", + ); + assert_eq!(interp.take_stdout_output(), "application"); + assert!(interp.take_stdout_output().is_empty()); + assert!(interp.take_debug_events().is_empty()); + } + + #[test] + fn debug_print_argument_failure_retains_prior_events_without_outer_output() { + let source = r#"namespace app +function argument() returns list[string]: + print("argument:") + return list("held") +function failed() returns int64: + println("failing") + list[int64] rejected = range(0, 9223372036854775807) + return 0 +function main() returns nothing: + print("before:") + println(argument(), failed()) + println("unreachable") +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interp = Interpreter::new(); + interp.register_module(&parsed.module); + interp.enable_stdout_capture(); + assert_eq!( + interp.call_function("app.main", vec![]), + Err("range: requested output is too large".into()), + ); + let events = interp.take_debug_events(); + assert_eq!( + events, + [ + expected_debug_event(DebugEventKind::Print, "before:"), + expected_debug_event(DebugEventKind::Print, "argument:"), + expected_debug_event(DebugEventKind::Println, "failing\n"), + ], + ); + assert_eq!( + crate::render_debug_events(&events), + "before:argument:failing\n", + ); + assert!(interp.take_stdout_output().is_empty()); + assert!(interp.take_debug_events().is_empty()); + } + + #[test] + fn debug_print_view_preserves_the_source_and_existing_public_display() { + let mut interp = Interpreter::new(); + interp.enable_stdout_capture(); + let values = Value::List(vec![Value::String("a".into()), Value::String("b".into())]); + // Host-built values have no checked Source facts. Supply the known + // declared contract; the public-text oracle below stays unchanged. + interp.set_variable_with_type( + "values", + values.clone(), + Interpreter::debug_type("list[string]").unwrap(), + ); + let expression = debug_call("println", vec![Expr::View(Box::new(var("values")), sp())]); + assert_eq!(interp.eval_expr(&expression), Ok(Value::Nothing)); + assert_eq!(interp.get_variable("values"), Some(&values)); + assert_eq!( + interp.take_debug_events(), + [expected_debug_event( + DebugEventKind::Println, + "list(a, b)\n" + )], + ); + assert!(interp.take_stdout_output().is_empty()); + } + + #[test] + fn legacy_debug_line_drain_discards_print_events_without_splitting_trace_text() { + let mut interp = Interpreter::new(); + interp.set_variable_with_type( + "message", + Value::String("line\ntrace fake".into()), + TypeExpr::Named(ident("string")), + ); + assert_eq!( + interp.call_builtin("println", &[Value::String("trace forged".into())]), + Some(Ok(Value::Nothing)), + ); + interp + .exec_stmt(&Stmt::Trace(TraceStmt { + name: ident("message"), + span: sp(), + })) + .unwrap(); + assert_eq!( + interp.take_debug_output(), + ["trace message: string = line\ntrace fake"], + ); + assert!(interp.take_debug_events().is_empty()); + } + #[test] - fn stdout_output_can_be_captured() { + fn stdout_and_debug_events_have_independent_capture() { let mut interp = Interpreter::new(); interp.enable_stdout_capture(); @@ -17640,8 +26896,16 @@ mod builtin_tests { Value::Nothing ); - assert_eq!(interp.take_stdout_output(), "hello score 7true\n"); - assert_eq!(interp.take_stdout_output(), ""); + assert_eq!(interp.take_stdout_output(), "hello "); + assert_eq!( + interp.take_debug_events(), + [ + expected_debug_event(DebugEventKind::Print, "score 7"), + expected_debug_event(DebugEventKind::Println, "true\n"), + ], + ); + assert!(interp.take_stdout_output().is_empty()); + assert!(interp.take_debug_events().is_empty()); } #[test] @@ -17799,19 +27063,19 @@ mod builtin_tests { ); assert_eq!( checked_clock_milliseconds(9_223_372_036_854_775, 808_000_000), - Err("Clock.now: timestamp is outside int64 millisecond range".to_string()) + Err("Clock.now: timestamp is outside int64 millisecond range") ); assert_eq!( checked_clock_milliseconds(-9_223_372_036_854_776, 191_000_000), - Err("Clock.now: timestamp is outside int64 millisecond range".to_string()) + Err("Clock.now: timestamp is outside int64 millisecond range") ); assert_eq!( checked_clock_milliseconds(i128::MAX, 0), - Err("Clock.now: timestamp is outside int64 millisecond range".to_string()) + Err("Clock.now: timestamp is outside int64 millisecond range") ); assert_eq!( checked_clock_milliseconds(0, 1_000_000_000), - Err("Clock.now: invalid test sample".to_string()) + Err("Clock.now: invalid test sample") ); } @@ -18095,6 +27359,157 @@ mod builtin_tests { assert_eq!(interp.eval_expr(&expr).unwrap(), Value::Int64(3)); } + #[test] + fn private_list_sum_wraps_every_primitive_integer_width() { + for (type_name, maximum, increment, expected) in [ + ( + "int8", + Value::Int64(127), + Value::Int64(1), + Value::Int64(-128), + ), + ( + "int16", + Value::Int64(32767), + Value::Int64(1), + Value::Int64(-32768), + ), + ( + "int32", + Value::Int64(i32::MAX.into()), + Value::Int64(1), + Value::Int64(i32::MIN.into()), + ), + ( + "int64", + Value::Int64(i64::MAX), + Value::Int64(1), + Value::Int64(i64::MIN), + ), + ("uint8", Value::Int64(255), Value::Int64(2), Value::Int64(1)), + ( + "uint16", + Value::Int64(65535), + Value::Int64(2), + Value::Int64(1), + ), + ( + "uint32", + Value::Int64(u32::MAX.into()), + Value::Int64(2), + Value::Int64(1), + ), + ( + "uint64", + Value::Uint64(u64::MAX), + Value::Uint64(2), + Value::Uint64(1), + ), + ] { + let mut interp = Interpreter::new(); + interp.current_function_trusted_stdlib = true; + interp.current_type_arguments = vec![TypeExpr::Named(ident(type_name))]; + assert_eq!( + interp.call_higher_order_builtin( + "list.__sum", + &[Value::List(vec![maximum, increment])] + ), + Some(Ok(expected)), + "{type_name}" + ); + let zero = if type_name == "uint64" { + Value::Uint64(0) + } else { + Value::Int64(0) + }; + assert_eq!( + interp.call_higher_order_builtin("list.__sum", &[Value::List(Vec::new())]), + Some(Ok(zero)), + "empty {type_name}" + ); + } + } + + #[test] + fn private_list_sum_preserves_float_width_rounding_and_ieee_values() { + for (type_name, expected) in [("float32", 0.0), ("float64", 1.0)] { + let mut interp = Interpreter::new(); + interp.current_function_trusted_stdlib = true; + interp.current_type_arguments = vec![TypeExpr::Named(ident(type_name))]; + let numbers = vec![ + Value::Float64(16777216.0), + Value::Float64(1.0), + Value::Float64(-16777216.0), + ]; + assert_eq!( + interp.call_higher_order_builtin("list.__sum", &[Value::List(numbers)]), + Some(Ok(Value::Float64(expected))) + ); + assert_eq!( + interp.call_higher_order_builtin("list.__sum", &[Value::List(Vec::new())]), + Some(Ok(Value::Float64(0.0))) + ); + let infinite = Value::List(vec![Value::Float64(f64::INFINITY)]); + assert_eq!( + interp.call_higher_order_builtin("list.__sum", &[infinite]), + Some(Ok(Value::Float64(f64::INFINITY))) + ); + let cancelling = Value::List(vec![ + Value::Float64(f64::INFINITY), + Value::Float64(f64::NEG_INFINITY), + ]); + let Some(Ok(Value::Float64(nan))) = + interp.call_higher_order_builtin("list.__sum", &[cancelling]) + else { + panic!("expected a float result"); + }; + assert!(nan.is_nan()); + } + } + + #[test] + fn private_list_sum_retains_pending_errors_and_requires_trusted_stdlib() { + let mut interp = Interpreter::new(); + assert!( + interp + .call_higher_order_builtin("list.__sum", &[Value::List(Vec::new())]) + .is_none() + ); + interp.current_function_trusted_stdlib = true; + for (type_name, number) in [ + ("int8", Value::Int64(1)), + ("uint64", Value::Uint64(1)), + ("float32", Value::Float64(1.0)), + ("float64", Value::Float64(1.0)), + ] { + interp.current_type_arguments = vec![TypeExpr::Named(ident(type_name))]; + for (items, message) in [ + ( + vec![Value::Pending(Box::new(number.clone()))], + "list elements must be int64 or float64", + ), + ( + vec![number.clone(), Value::Pending(Box::new(number))], + "mixed types", + ), + ] { + assert_eq!( + interp.call_higher_order_builtin("list.__sum", &[Value::List(items)]), + Some(Err(format!("list.__sum: {message}"))), + "{type_name}" + ); + } + assert_eq!( + interp.call_higher_order_builtin( + "list.__sum", + &[Value::Pending(Box::new(Value::List(Vec::new())))] + ), + Some(Err("list.__sum: argument must be a list".into())), + "{type_name}" + ); + } + } + #[test] fn private_list_append_kernel() { let mut interp = Interpreter::new(); @@ -18659,3 +28074,7 @@ mod builtin_tests { ); } } + +#[cfg(test)] +#[path = "interpreter/actor_identity_tests.rs"] +mod actor_identity_tests; diff --git a/crates/jett_comptime/src/interpreter/actor_graph.rs b/crates/jett_comptime/src/interpreter/actor_graph.rs new file mode 100644 index 00000000..545de32a --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_graph.rs @@ -0,0 +1,762 @@ +//! Immutable checked actor snapshots. No Source or provider runs during import. +use super::{ActorInstance, Interpreter, Value}; +use crate::resource_execution::PreparedRequiredExpression; +use jett_parser::ast::TypeExpr; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId, TypeInterner}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +#[derive(Debug, Clone)] +pub struct FrozenActorField { + name: String, + ty: TypeId, + value: Value, +} + +impl FrozenActorField { + pub fn name(&self) -> &str { + &self.name + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn value(&self) -> &Value { + &self.value + } +} + +#[derive(Debug, Clone)] +pub struct FrozenActorNode { + seed: u64, + ty: TypeId, + captures: Vec, + state: Vec, +} + +impl FrozenActorNode { + pub fn seed(&self) -> u64 { + self.seed + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn captures(&self) -> &[FrozenActorField] { + &self.captures + } + pub fn state(&self) -> &[FrozenActorField] { + &self.state + } +} + +/// Private construction and exact checked-program binding. The IDs in this +/// packet name seed nodes only; they are never runtime identities or grants. +#[derive(Debug, Clone)] +pub struct ActorGraphTemplate { + program: Arc, + root: Value, + root_type: TypeId, + nodes: Vec, +} + +impl ActorGraphTemplate { + pub fn belongs_to(&self, program: &Arc) -> bool { + Arc::ptr_eq(&self.program, program) + } + pub fn root(&self) -> &Value { + &self.root + } + pub fn root_type(&self) -> TypeId { + self.root_type + } + pub fn nodes(&self) -> &[FrozenActorNode] { + &self.nodes + } +} + +impl Interpreter { + pub(crate) fn freeze_actor_graph( + &self, + root: &Value, + prepared: Option<&PreparedRequiredExpression>, + ) -> Result>, String> { + if !contains_actor(root) { + return Ok(None); + } + let prepared = prepared.ok_or("actor snapshot has no exact checked required occurrence")?; + let program = prepared.checked_program(); + if !self + .retained_resource_program + .as_ref() + .is_some_and(|(current, _)| Arc::ptr_eq(current, program)) + { + return Err("actor snapshot belongs to a foreign checked program".into()); + } + let types = &program.checked().interner; + let mut pending = vec![(root, prepared.result_type())]; + let mut seen: HashMap = HashMap::new(); + let mut nodes: Vec = Vec::new(); + while let Some((value, ty)) = pending.pop() { + if let (Type::Actor(owner), Value::Actor(seed)) = (types.resolve(ty), value) { + let actor = self + .actor_instances + .get(seed) + .ok_or("actor snapshot has no live seed instance")?; + let definition = types.resolve_actor(*owner); + if actor.type_name != definition.name { + return Err("actor snapshot changed its exact checked nominal owner".into()); + } + if let Some(index) = seen.get(seed) { + if nodes[*index].ty != ty { + return Err( + "actor graph seed was reused under another checked nominal actor type" + .into(), + ); + } + continue; + } + seen.insert(*seed, nodes.len()); + let captures = frozen_fields(&actor.capabilities, &definition.capability_params)?; + let state = frozen_fields(&actor.state, &definition.state_fields)?; + nodes.push(FrozenActorNode { + seed: *seed, + ty, + captures, + state, + }); + // Reverse pushes preserve checked declaration order at visitation. + for (name, field_type) in definition.state_fields.iter().rev() { + pending.push(( + actor + .state + .get(name) + .ok_or("snapshot state field is missing")?, + *field_type, + )); + } + for (name, field_type) in definition.capability_params.iter().rev() { + pending.push(( + actor + .capabilities + .get(name) + .ok_or("snapshot capture is missing")?, + *field_type, + )); + } + } else { + let children = self.actor_graph_children(value, ty, types, program)?; + pending.extend(children.into_iter().rev()); + } + } + // Authenticated evaluator IDs encode seed creation order. Preserve + // relative debug ordinals without replaying constructors or importing IDs. + nodes.sort_by_key(|node| node.seed); + Ok(Some(Arc::new(ActorGraphTemplate { + program: program.clone(), + root: root.clone(), + root_type: prepared.result_type(), + nodes, + }))) + } + + /// Prepare every owner off-registry, then publish only the complete graph. + /// Numeric reservations are retired on failure rather than reused. + pub(crate) fn instantiate_actor_graph( + &mut self, + template: &ActorGraphTemplate, + ) -> Result { + let (program, _) = self + .retained_resource_program + .as_ref() + .ok_or("actor import has no checked program")?; + if !template.belongs_to(program) { + return Err("actor import belongs to a foreign checked program".into()); + } + let count = u64::try_from(template.nodes.len()) + .map_err(|_| "actor graph exceeds identity space")?; + let limit = self + .next_actor_id + .checked_add(count) + .ok_or("actor identity space exhausted")?; + let first = self.next_actor_id; + let mut identities = HashMap::new(); + identities + .try_reserve(template.nodes.len()) + .map_err(|_| "cannot reserve actor import identities")?; + for (offset, node) in template.nodes.iter().enumerate() { + let offset = + u64::try_from(offset).map_err(|_| "actor graph index exceeds identity space")?; + let fresh = first + .checked_add(offset) + .ok_or("actor identity space exhausted")?; + if self.actor_instances.contains_key(&fresh) + || identities.insert(node.seed, fresh).is_some() + { + return Err("actor graph import has a duplicate or occupied identity".into()); + } + } + self.actor_instances + .try_reserve(template.nodes.len()) + .map_err(|_| "cannot reserve actor registry")?; + self.next_actor_id = limit; + let mut unpublished = Vec::new(); + unpublished + .try_reserve_exact(template.nodes.len()) + .map_err(|_| "cannot reserve unpublished actor graph")?; + for node in &template.nodes { + let Type::Actor(owner) = program.checked().interner.resolve(node.ty) else { + return Err("actor graph node has no checked actor type".into()); + }; + let definition = program.checked().interner.resolve_actor(*owner); + let state = import_fields(&node.state, &identities)?; + let capabilities = import_fields(&node.captures, &identities)?; + unpublished.push(( + *identities + .get(&node.seed) + .ok_or("actor seed is not reserved")?, + ActorInstance { + checked_origin: Some( + crate::resource_execution::CheckedActorInstanceOrigin::from_checked_actor( + program.clone(), + *owner, + ) + .map_err(|error| error.to_string())?, + ), + type_name: definition.name.clone(), + state, + capabilities, + }, + )); + } + let root = remap_value(template.root.clone(), &identities)?; + // No fallible operation or Source execution occurs after publication starts. + for (identity, actor) in unpublished { + self.actor_instances.insert(identity, actor); + } + Ok(root) + } + + fn actor_graph_children<'a>( + &'a self, + value: &'a Value, + ty: TypeId, + types: &TypeInterner, + program: &Arc, + ) -> Result, String> { + if let Value::Pending(inner) = value { + return Ok(vec![(inner, ty)]); + } + if let Value::Typed { + type_name, + value: inner, + } = value + { + let concrete = checked_type_name(types, type_name)?; + if let Type::Interface(_) = types.resolve(ty) { + return Ok(vec![(inner, concrete)]); + } + if let Type::Secret(base) = types.resolve(ty) { + if concrete != ty { + return Ok(vec![(value, *base)]); + } + } + if concrete != ty { + return Err("actor graph typed carrier changed its checked owner".into()); + } + let inner_type = match types.resolve(ty) { + Type::Refinement { base, .. } | Type::Secret(base) => *base, + _ => ty, + }; + return Ok(vec![(inner, inner_type)]); + } + if let Type::Secret(inner) = types.resolve(ty) { + return Ok(vec![(value, *inner)]); + } + if let Type::Refinement { base, .. } = types.resolve(ty) { + // The value is already provider-free checked data; import never reruns a predicate. + return Ok(vec![(value, *base)]); + } + if let Type::Interface(_) = types.resolve(ty) { + return Ok(vec![(value, self.actor_graph_concrete_type(value, types)?)]); + } + match (types.resolve(ty), value) { + (_, Value::Capability(_) | Value::Resource(_) | Value::GrantedNetwork(_)) => Err( + "actor graph cannot retain runtime capability authority or Resource custody".into(), + ), + (Type::Function { .. }, Value::ResourceHook(descriptor)) + if descriptor.matches_type(program, ty) => + { + Ok(Vec::new()) + } + (_, Value::ResourceHook(_)) => { + Err("actor graph contains a foreign or mistyped checked hook descriptor".into()) + } + (Type::List(element), Value::List(values)) + | (Type::Set(element), Value::Set(values)) => { + Ok(values.iter().map(|value| (value, *element)).collect()) + } + (Type::Map(key, mapped), Value::Map(entries)) => Ok(entries + .iter() + .flat_map(|(left, right)| [(left, *key), (right, *mapped)]) + .collect()), + (Type::Optional(inner), Value::OptionalSome(value)) => Ok(vec![(value, *inner)]), + (Type::Optional(_), Value::OptionalNone) => Ok(Vec::new()), + (Type::Result(ok, _), Value::ResultOk(value)) => Ok(vec![(value, *ok)]), + (Type::Result(_, fail), Value::ResultFail(value)) => Ok(vec![(value, *fail)]), + ( + Type::Struct(owner), + Value::Struct { + type_name, + concrete_type, + fields, + }, + ) => { + let definition = types.resolve_struct(*owner); + if definition.name != concrete_type.as_deref().unwrap_or(type_name) { + return Err("actor graph record owner mismatch".into()); + } + named_children(fields, &definition.fields) + } + ( + Type::Bitfield(owner), + Value::Struct { + type_name, fields, .. + }, + ) => { + let definition = types.resolve_bitfield(*owner); + if definition.name != *type_name { + return Err("actor graph bitfield owner mismatch".into()); + } + let fields_by_name = definition + .fields + .iter() + .map(|field| (field.name.clone(), field.ty)) + .collect::>(); + named_children(fields, &fields_by_name) + } + ( + Type::Enum(owner), + Value::Enum { + type_name, + variant, + fields, + }, + ) => { + let definition = types.resolve_enum(*owner); + if definition.name != *type_name { + return Err("actor graph enum owner mismatch".into()); + } + let variant = definition + .variants + .iter() + .find(|candidate| candidate.name == *variant) + .ok_or("actor graph variant is absent")?; + positional_children(fields, &variant.fields) + } + ( + Type::Machine(owner) | Type::MachineState { machine: owner, .. }, + Value::Machine { + type_name, + state, + fields, + }, + ) => { + let definition = types.resolve_machine(*owner); + if definition.name != *type_name { + return Err("actor graph machine owner mismatch".into()); + } + let index = definition + .state_id(state) + .ok_or("actor graph machine state is absent")?; + if let Type::MachineState { state: checked, .. } = types.resolve(ty) { + if *checked != index { + return Err("actor graph machine state mismatch".into()); + } + } + positional_children( + fields, + &definition + .state(index) + .ok_or("actor graph machine state is absent")? + .fields, + ) + } + ( + Type::Function { .. }, + Value::Function { + captures, + capture_types, + type_context, + namespace_aliases, + namespace, + .. + }, + ) => { + let mut names = captures.keys().collect::>(); + names.sort(); + names + .into_iter() + .map(|name| { + let written = capture_types + .get(name) + .ok_or("actor graph callback capture has no declared type")?; + let concrete = self.actor_graph_capture_type( + written, + &type_context.bindings, + namespace_aliases, + namespace.as_deref(), + &mut HashSet::new(), + )?; + let name_of_type = self.resolved_concrete_type_display(&concrete); + Ok((&captures[name], checked_type_name(types, &name_of_type)?)) + }) + .collect() + } + (Type::TypeConstruction, Value::TypeConstruction { fields, .. }) => fields + .iter() + .map(|(_, _, field_type, value)| Ok((value, checked_type_name(types, field_type)?))) + .collect(), + (Type::Function { .. }, Value::ReflectedCallable(adapter)) => { + if !Arc::ptr_eq(adapter.checked_program(), program) + || adapter.requested_type() != ty + || !adapter.checked_plan().validate(program.checked()) + { + return Err("actor graph callback changed its checked adapter plan".into()); + } + crate::value::ReflectedCallableValue::authenticate_source( + adapter.source(), + program, + adapter.source_type(), + )?; + Ok(vec![(adapter.source(), adapter.source_type())]) + } + (Type::Function { .. }, Value::NamedFunction(_)) => Ok(Vec::new()), + ( + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64, + Value::Int64(_), + ) + | (Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Uint64(_)) + | (Type::Float32 | Type::Float64, Value::Float64(_)) + | (Type::String, Value::String(_) | Value::Error(_)) + | (Type::Bool, Value::Bool(_)) + | (Type::Bytes, Value::Bytes(_)) + | (Type::Nothing, Value::Nothing) => Ok(Vec::new()), + _ => Err("actor graph carrier disagrees with its checked shape".into()), + } + } + + // Resolve written capture contracts in the closure's retained lexical + // context. Import never borrows aliases or generic bindings from its caller. + fn actor_graph_capture_type( + &self, + written: &TypeExpr, + bindings: &HashMap, + aliases: &HashMap, + namespace: Option<&str>, + seen: &mut HashSet, + ) -> Result { + let name = |ident: &jett_parser::ast::Ident| { + let mut ident = self.qualify_declared_type_ident(ident, namespace); + if let Some((prefix, suffix)) = ident.name.split_once('.') { + if let Some(target) = aliases.get(prefix) { + ident.name = format!("{target}.{suffix}"); + } + } + ident + }; + Ok(match written { + TypeExpr::Named(ident) => { + if let Some(bound) = bindings.get(&ident.name) { + if !seen.insert(ident.name.clone()) { + return Err("actor graph capture type binding is cyclic".into()); + } + // Installed bindings already carry their canonical identity. + let resolved = bound.clone(); + seen.remove(&ident.name); + resolved + } else { + TypeExpr::Named(name(ident)) + } + } + TypeExpr::Generic(ident, args, span) => TypeExpr::Generic( + name(ident), + args.iter() + .map(|arg| { + self.actor_graph_capture_type(arg, bindings, aliases, namespace, seen) + }) + .collect::>()?, + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.actor_graph_capture_type(inner, bindings, aliases, namespace, seen)?), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.actor_graph_capture_type(inner, bindings, aliases, namespace, seen)?), + state.clone(), + *span, + ), + TypeExpr::Function(params, result, span) => TypeExpr::Function( + params + .iter() + .map(|arg| { + self.actor_graph_capture_type(arg, bindings, aliases, namespace, seen) + }) + .collect::>()?, + Box::new( + self.actor_graph_capture_type(result, bindings, aliases, namespace, seen)?, + ), + *span, + ), + }) + } + + fn actor_graph_concrete_type( + &self, + value: &Value, + types: &TypeInterner, + ) -> Result { + let primitive = match value { + Value::Int64(_) => Some(TypeInterner::INT64), + Value::Uint64(_) => Some(TypeInterner::UINT64), + Value::Float64(_) => Some(TypeInterner::FLOAT64), + Value::Bool(_) => Some(TypeInterner::BOOL), + Value::String(_) => Some(TypeInterner::STRING), + Value::Bytes(_) => Some(TypeInterner::BYTES), + Value::Nothing => Some(TypeInterner::NOTHING), + Value::TypeConstruction { .. } => Some(TypeInterner::TYPE_CONSTRUCTION), + _ => None, + }; + if let Some(primitive) = primitive { + return Ok(primitive); + } + let name = match value { + Value::Actor(seed) => self + .actor_instances + .get(seed) + .ok_or("erased actor seed is not live")? + .type_name + .as_str(), + Value::Struct { + type_name, + concrete_type, + .. + } => concrete_type.as_deref().unwrap_or(type_name), + Value::Enum { type_name, .. } | Value::Machine { type_name, .. } => type_name, + _ => { + return Err( + "actor graph interface payload has no retained exact concrete owner".into(), + ); + } + }; + checked_type_name(types, name) + } +} + +fn checked_type_name(types: &TypeInterner, name: &str) -> Result { + let name = name.strip_prefix("view ").unwrap_or(name); + let mut matches = types.type_ids().filter(|ty| types.type_name(*ty) == name); + let ty = matches + .next() + .ok_or("actor graph type is absent from its checked program")?; + if matches.next().is_some() { + return Err("actor graph type identity is ambiguous".into()); + } + Ok(ty) +} + +fn frozen_fields( + values: &HashMap, + expected: &[(String, TypeId)], +) -> Result, String> { + if values.len() != expected.len() { + return Err("actor snapshot field arity mismatch".into()); + } + expected + .iter() + .map(|(name, ty)| { + Ok(FrozenActorField { + name: name.clone(), + ty: *ty, + value: values + .get(name) + .ok_or("actor snapshot field is absent")? + .clone(), + }) + }) + .collect() +} + +fn named_children<'a>( + values: &'a [(String, Value)], + expected: &[(String, TypeId)], +) -> Result, String> { + if values.len() != expected.len() { + return Err("actor graph record field arity mismatch".into()); + } + expected + .iter() + .map(|(name, ty)| { + let mut matching = values.iter().filter(|(actual, _)| actual == name); + let (_, value) = matching + .next() + .ok_or("actor graph record field is absent")?; + if matching.next().is_some() { + return Err("actor graph record field is duplicated".into()); + } + Ok((value, *ty)) + }) + .collect() +} + +fn positional_children<'a>( + values: &'a [Value], + expected: &[(String, TypeId)], +) -> Result, String> { + if values.len() != expected.len() { + return Err("actor graph payload arity mismatch".into()); + } + Ok(values + .iter() + .zip(expected) + .map(|(value, (_, ty))| (value, *ty)) + .collect()) +} + +fn import_fields( + fields: &[FrozenActorField], + identities: &HashMap, +) -> Result, String> { + let mut values = HashMap::new(); + values + .try_reserve(fields.len()) + .map_err(|_| "cannot reserve actor imported fields")?; + for field in fields { + values.insert( + field.name.clone(), + remap_value(field.value.clone(), identities)?, + ); + } + Ok(values) +} + +pub(super) fn contains_actor(value: &Value) -> bool { + let mut pending = vec![value]; + while let Some(value) = pending.pop() { + if matches!(value, Value::Actor(_)) { + return true; + } + pending.extend(carrier_children(value)); + } + false +} + +fn carrier_children(value: &Value) -> Vec<&Value> { + match value { + Value::Typed { value, .. } + | Value::Pending(value) + | Value::ResultOk(value) + | Value::ResultFail(value) + | Value::OptionalSome(value) => vec![value], + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => values.iter().collect(), + Value::Struct { fields, .. } => fields.iter().map(|(_, value)| value).collect(), + Value::Map(entries) => entries + .iter() + .flat_map(|(left, right)| [left, right]) + .collect(), + Value::TypeConstruction { fields, .. } => { + fields.iter().map(|(_, _, _, value)| value).collect() + } + Value::Function { captures, .. } => captures.values().collect(), + Value::ReflectedCallable(adapter) => vec![adapter.source()], + Value::Actor(_) + | Value::Int64(_) + | Value::Uint64(_) + | Value::Float64(_) + | Value::String(_) + | Value::Bool(_) + | Value::Bytes(_) + | Value::OptionalNone + | Value::Nothing + | Value::Capability(_) + | Value::Resource(_) + | Value::ResourceHook(_) + | Value::GrantedNetwork(_) + | Value::Error(_) + | Value::NamedFunction(_) => Vec::new(), + } +} + +fn remap_value(mut value: Value, identities: &HashMap) -> Result { + let mut pending = vec![&mut value]; + while let Some(value) = pending.pop() { + match value { + Value::Actor(seed) => { + *seed = *identities + .get(seed) + .ok_or("actor reference is absent from the frozen graph")? + } + Value::Typed { value, .. } + | Value::Pending(value) + | Value::ResultOk(value) + | Value::ResultFail(value) + | Value::OptionalSome(value) => pending.push(value.as_mut()), + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => pending.extend(values.iter_mut()), + Value::Struct { fields, .. } => { + pending.extend(fields.iter_mut().map(|(_, value)| value)) + } + Value::Map(entries) => { + pending.extend(entries.iter_mut().flat_map(|(left, right)| [left, right])) + } + Value::TypeConstruction { fields, .. } => { + pending.extend(fields.iter_mut().map(|(_, _, _, value)| value)) + } + Value::Function { captures, .. } => pending.extend(captures.values_mut()), + Value::ReflectedCallable(adapter) => { + // Retain the opaque source and its original sealed capture witness. + // Current Source rejects move-only Actor closure captures (E0402). + if contains_actor(adapter.source()) { + return Err( + "actor import cannot rewrite an original reflected callback capture".into(), + ); + } + } + Value::Capability(_) | Value::Resource(_) | Value::GrantedNetwork(_) => { + return Err( + "actor import cannot clone runtime authority or Resource custody".into(), + ); + } + Value::Int64(_) + | Value::Uint64(_) + | Value::Float64(_) + | Value::String(_) + | Value::Bool(_) + | Value::Bytes(_) + | Value::OptionalNone + | Value::Nothing + | Value::ResourceHook(_) + | Value::Error(_) + | Value::NamedFunction(_) => {} + } + } + drop(pending); + Ok(value) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_comptime/src/interpreter/actor_graph/tests.rs b/crates/jett_comptime/src/interpreter/actor_graph/tests.rs new file mode 100644 index 00000000..9086de52 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_graph/tests.rs @@ -0,0 +1,368 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::{ExecutionPurpose, ExplicitComptimeValues}; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::CheckOptions; + +// The existing public probe's source spellings are preserved. It remains a +// debug Source; these tests do not redefine its release-print admission. +const ORIGINAL_ESCAPE: &str = "namespace app\nactor Counter:\n mutable int64 count = 1\n receive read responds int64:\n respond count\nfunction main() returns nothing:\n Counter worker = comptime spawn Counter()\n int64 value = ask worker.read\n println(value)\n return nothing\n"; + +const SHARED: &str = "namespace app\nactor Counter:\n mutable int64 count = 1\n receive add(amount: int64):\n count = count + amount\n receive read responds int64:\n respond count\nstruct Pair:\n left: Counter\n right: Counter\nfunction create() returns Pair:\n Counter worker = spawn Counter()\n send worker.add(4)\n return Pair(left: clone worker, right: worker)\nfunction baked() returns Pair:\n return comptime create()\nfunction main() returns nothing:\n return nothing\n"; + +fn checked_program(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release: false }, + ) + .expect("checked actor graph test Source"), + ) +} + +fn required(program: &Arc) -> ExplicitComptimeValues { + let evaluation = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + program.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + evaluation.values +} + +fn runtime(program: &Arc) -> Interpreter { + let mut interpreter = Interpreter::new(); + interpreter + .install_checked_resource_program(program.clone(), ExecutionPurpose::ReferenceRuntime) + .expect("checked actor import runtime"); + interpreter.register_module(program.module()); + interpreter +} + +fn pair(value: &Value) -> (u64, u64) { + let Value::Struct { fields, .. } = value.payload() else { + panic!("expected imported Pair") + }; + let actor = |name: &str| { + let value = &fields + .iter() + .find(|(field, _)| field == name) + .expect("Pair field") + .1; + let Value::Actor(identity) = value.payload() else { + panic!("expected imported actor") + }; + *identity + }; + (actor("left"), actor("right")) +} + +#[test] +fn original_escape_has_program_bound_frozen_state_without_events() { + let program = checked_program(ORIGINAL_ESCAPE); + let values = required(&program); + let proofs = values.checked_required_values(&program).unwrap(); + assert_eq!(proofs.len(), 1); + let graph = proofs[0].actor_graph().expect("escaping actor graph"); + assert!(graph.belongs_to(&program)); + assert_eq!(graph.nodes().len(), 1); + assert_eq!(graph.nodes()[0].state()[0].value(), &Value::Int64(1)); + let mut interpreter = runtime(&program); + let imported = interpreter.instantiate_actor_graph(graph).unwrap(); + let Value::Actor(identity) = imported.payload() else { + panic!("imported actor") + }; + assert_eq!( + interpreter.actor_instances[identity].state["count"], + Value::Int64(1) + ); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn each_activation_is_fresh_with_shared_aliases_and_frozen_post_message_state() { + let program = checked_program(SHARED); + let values = required(&program); + let proofs = values.checked_required_values(&program).unwrap(); + let graph = proofs[0].actor_graph().unwrap(); + assert_eq!( + graph.nodes().len(), + 1, + "two root positions share one seed actor" + ); + assert_eq!(graph.nodes()[0].state()[0].value(), &Value::Int64(5)); + let mut interpreter = runtime(&program); + // A prior runtime actor must not collide with an evaluation seed ID. + interpreter.actor_instances.insert( + 0, + ActorInstance { + checked_origin: None, + type_name: "app.Counter".into(), + state: HashMap::from([("count".into(), Value::Int64(99))]), + capabilities: HashMap::new(), + }, + ); + interpreter.next_actor_id = 1; + let first = interpreter.instantiate_actor_graph(graph).unwrap(); + let (left, right) = pair(&first); + assert_eq!(left, right); + assert_ne!(left, 0); + interpreter + .actor_instances + .get_mut(&left) + .unwrap() + .state + .insert("count".into(), Value::Int64(41)); + assert_eq!( + interpreter.actor_instances[&right].state["count"], + Value::Int64(41) + ); + let second = interpreter.instantiate_actor_graph(graph).unwrap(); + let (next_left, next_right) = pair(&second); + assert_eq!(next_left, next_right); + assert_ne!(left, next_left); + assert_eq!( + interpreter.actor_instances[&next_left].state["count"], + Value::Int64(5) + ); + assert_eq!(graph.nodes()[0].state()[0].value(), &Value::Int64(5)); + assert_eq!( + interpreter.actor_instances[&0].state["count"], + Value::Int64(99) + ); +} + +#[test] +fn failed_unpublished_import_leaves_existing_actors_and_registry_intact() { + let program = checked_program(SHARED); + let values = required(&program); + let proofs = values.checked_required_values(&program).unwrap(); + let mut malformed = (**proofs[0].actor_graph().unwrap()).clone(); + // Only this private child test can forge template storage; public callers + // have no constructor and cannot create an associated cache proof. + malformed.nodes[0].state[0].value = Value::Actor(u64::MAX); + let mut interpreter = runtime(&program); + assert!(interpreter.instantiate_actor_graph(&malformed).is_err()); + assert!(interpreter.actor_instances.is_empty()); + assert_eq!( + interpreter.next_actor_id, 1, + "failed reservations are retired" + ); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn public_cache_mirror_cannot_mint_or_replace_a_private_actor_graph() { + let program = checked_program(ORIGINAL_ESCAPE); + let mut values = required(&program); + let proofs = values.checked_required_values(&program).unwrap(); + let span = proofs[0].source_span(); + values.insert(span, crate::ComptimeContext::default(), Value::Actor(777)); + assert!( + values + .actor_graph(span, &crate::ComptimeContext::default()) + .is_some(), + "public mirror insertion cannot remove the private runtime snapshot" + ); + let checked = values.checked_required_values(&program).unwrap(); + assert!( + checked[0].actor_graph().is_some(), + "private original association survives a caller mirror" + ); + let mut forged = ExplicitComptimeValues::default(); + forged.insert(span, crate::ComptimeContext::default(), Value::Actor(0)); + assert!(forged.checked_required_values(&program).unwrap().is_empty()); + assert!( + forged + .actor_graph(span, &crate::ComptimeContext::default()) + .is_none() + ); + let mut interpreter = runtime(&program); + interpreter.set_explicit_comptime_values(Arc::new(values.clone())); + assert_eq!( + interpreter.call_function("app.main", vec![]).unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.take_debug_events().len(), + 1, + "original print executes only at runtime" + ); + let mut interpreter = runtime(&program); + interpreter.set_explicit_comptime_values(Arc::new(forged)); + assert!( + interpreter + .call_function("app.main", vec![]) + .unwrap_err() + .contains("no private checked frozen graph") + ); + let foreign = checked_program(ORIGINAL_ESCAPE); + assert!(values.checked_required_values(&foreign).is_err()); +} + +#[test] +fn nested_actor_state_and_root_lists_keep_distinct_seed_nodes() { + let source = "namespace app\nactor Counter:\n mutable int64 count = 1\nactor Holder:\n app.Counter child = spawn app.Counter()\nfunction baked() returns list[Holder]:\n Holder first = spawn Holder()\n Holder second = spawn Holder()\n return list(clone first, first, second)\nfunction main() returns nothing:\n list[Holder] roots = comptime baked()\n return nothing\n"; + let program = checked_program(source); + let values = required(&program); + let proof = values.checked_required_values(&program).unwrap().remove(0); + let graph = proof.actor_graph().unwrap(); + assert_eq!( + graph.nodes().len(), + 4, + "two holders retain two distinct child instances" + ); + assert_eq!( + graph + .nodes() + .iter() + .map(|node| node.seed()) + .collect::>(), + vec![0, 1, 2, 3], + "creation order is child-before-holder despite root-first traversal and a shared first holder" + ); + assert!(graph.nodes()[0].seed() < graph.nodes()[1].seed()); + let mut interpreter = runtime(&program); + let imported = interpreter.instantiate_actor_graph(graph).unwrap(); + let Value::List(roots) = imported.payload() else { + panic!("imported list") + }; + assert_eq!(roots[0], roots[1]); + assert_eq!( + roots[0], + Value::Actor(1), + "first root holder follows its child creation ordinal" + ); + assert_eq!( + roots[2], + Value::Actor(3), + "second root follows its distinct child ordinal" + ); + assert_ne!(roots[0], roots[2]); + let imported_again = interpreter.instantiate_actor_graph(graph).unwrap(); + assert_ne!(imported, imported_again); +} +#[test] +fn actor_capture_keeps_the_existing_move_only_closure_contract() { + let source = "namespace app\nactor Counter:\n mutable int64 count = 1\nfunction create() returns function() returns Counter:\n Counter worker = spawn Counter()\n return function() returns Counter: return clone worker\nfunction main() returns nothing:\n return nothing\n"; + let file = FileId::new(0); + let error = CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release: false }, + ) + .expect_err("move-only actor capture is not admitted by the existing source contract"); + assert!(format!("{error:?}").contains("closure cannot capture move-only")); +} +#[test] +fn actor_callback_state_retains_admitted_primitive_captures_without_initializer_replay() { + let source = "namespace app\nfunction create_callback() returns function() returns string:\n string text = \"baked\"\n return function() returns string: return text\nactor Keeper:\n mutable function() returns string callback = create_callback()\n receive read responds string:\n respond callback()\nfunction main() returns nothing:\n Keeper worker = comptime spawn Keeper()\n string text = ask worker.read\n println(text)\n return nothing\n"; + let program = checked_program(source); + let values = required(&program); + let proof = values.checked_required_values(&program).unwrap().remove(0); + let graph = proof.actor_graph().unwrap(); + assert_eq!(graph.nodes().len(), 1); + let field = &graph.nodes()[0].state()[0]; + let Value::Function { type_context, .. } = field.value().payload() else { + panic!("frozen callback") + }; + let original = type_context + .reflected_capture + .as_ref() + .expect("original sealed capture"); + let returned = type_context + .return_type + .clone() + .expect("callback return type"); + crate::value::ReflectedCallableValue::authenticate_source(field.value(), &program, field.ty()) + .unwrap(); + let mut interpreter = runtime(&program); + let imported = interpreter.instantiate_actor_graph(graph).unwrap(); + let Value::Actor(id) = imported.payload() else { + panic!("actor") + }; + let Value::Function { + captures, + type_context, + .. + } = interpreter.actor_instances[id].state["callback"].payload() + else { + panic!("callback") + }; + assert_eq!(captures["text"], Value::String("baked".into())); + assert!(Arc::ptr_eq( + original, + type_context.reflected_capture.as_ref().unwrap() + )); + + // A private child can compose the same checked source with its exact plan. + // Import must retain that opaque adapter and witness without Source replay. + let adapter = Value::ReflectedCallable(Box::new( + crate::value::ReflectedCallableValue::new( + field.value().clone(), + program.clone(), + field.ty(), + field.ty(), + returned.clone(), + returned, + ) + .unwrap(), + )); + assert_eq!( + interpreter + .actor_graph_children(&adapter, field.ty(), &program.checked().interner, &program) + .unwrap() + .len(), + 1 + ); + let foreign = checked_program(source); + assert!( + interpreter + .actor_graph_children(&adapter, field.ty(), &foreign.checked().interner, &foreign) + .is_err() + ); + let mut adapted = (**graph).clone(); + adapted.nodes[0].state[0].value = adapter; + let imported = interpreter.instantiate_actor_graph(&adapted).unwrap(); + let Value::Actor(id) = imported.payload() else { + panic!("adapted actor") + }; + let Value::ReflectedCallable(adapter) = + interpreter.actor_instances[id].state["callback"].payload() + else { + panic!("retained adapter") + }; + let Value::Function { type_context, .. } = adapter.source().payload() else { + panic!("retained source") + }; + assert!(Arc::ptr_eq( + original, + type_context.reflected_capture.as_ref().unwrap() + )); + crate::value::ReflectedCallableValue::authenticate_source( + adapter.source(), + &program, + adapter.source_type(), + ) + .unwrap(); + assert!(interpreter.take_debug_events().is_empty()); +} diff --git a/crates/jett_comptime/src/interpreter/actor_handlers.rs b/crates/jett_comptime/src/interpreter/actor_handlers.rs new file mode 100644 index 00000000..ee10f919 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_handlers.rs @@ -0,0 +1,479 @@ +//! Exact actor declaration/handler entry, independent of Resource execution. +use super::*; +use crate::resource_execution::{CheckedBodyReference, PreparedActorHandler}; +use jett_typecheck::{CheckedActorBindingRole, CheckedCallerEffect}; + +/// Created only by the active, same-program checked Source Spawn route below. +/// Handler entry remains unable to install any Resource executable cursor. +struct CheckedResourceActorInitializers { + program: Arc, + bodies: Vec, +} + +impl CheckedResourceActorInitializers { + fn evaluate(&self, interpreter: &mut Interpreter, field: usize) -> Result { + let transport = interpreter + .resource_transport + .as_mut() + .filter(|transport| Arc::ptr_eq(transport.checked.program(), &self.program)) + .ok_or("actor initializer lost its original checked Resource program")?; + let reference = self + .bodies + .get(field) + .ok_or("actor initializer lost its original field")?; + let previous_active = std::mem::replace(&mut transport.checked_source_active, true); + // This existing entry verifies and installs the retained initializer, + // evaluates its original expression and restores its Resource cursor. + let result = interpreter.eval_checked_actor_initializer(reference); + let restored = interpreter + .resource_transport + .as_mut() + .filter(|transport| Arc::ptr_eq(transport.checked.program(), &self.program)) + .ok_or_else(|| { + "actor initializer lost its original checked Resource program".to_string() + }) + .map(|transport| transport.checked_source_active = previous_active); + match result { + Err(error) => Err(error), + Ok(value) => restored.map(|()| value), + } + } +} + +impl Interpreter { + fn actor_checked_type( + handler: &PreparedActorHandler, + ty: jett_types::TypeId, + span: Span, + ) -> Result { + Self::actor_program_type(handler.program(), ty, span) + } + + fn actor_program_type( + program: &jett_typecheck::CheckedResourceProgram, + ty: jett_types::TypeId, + span: Span, + ) -> Result { + let name = program.checked().interner.type_name(ty); + Self::simple_type_expr_from_name(&name, span) + .ok_or_else(|| "actor binding has no canonical checked type".into()) + } + + /// Result paths restore every sender context. This does not promise unwind rollback. + fn with_actor_frame( + &mut self, + handler: Option<&PreparedActorHandler>, + namespace: Option, + body: impl FnOnce(&mut Self, usize) -> Result, + ) -> Result { + let projection = handler + .map(|handler| handler.projection().map_err(|error| error.to_string())) + .transpose()?; + let capture = handler + .map(|handler| { + crate::value::closure_capture::OriginalClosureOwner::actor_handler( + handler.program(), + handler.target(), + handler.namespace(), + ) + .ok_or_else(|| "actor handler lost its original capture owner".to_string()) + }) + .transpose()?; + let saved_field = + self.enter_checked_field_body(handler.map(|handler| handler.reference()))?; + let base = self.scopes.len(); + let saved_floor = std::mem::replace(&mut self.lexical_scope_floor, base); + let saved_namespace = std::mem::replace(&mut self.current_namespace, namespace); + let saved_trusted = std::mem::replace(&mut self.current_function_trusted_stdlib, false); + let saved_function = self.active_checked_function.take(); + let saved_scope = std::mem::replace(&mut self.active_checked_scope, projection); + let saved_capture = std::mem::replace(&mut self.active_reflected_capture_origin, capture); + let saved_bindings = std::mem::take(&mut self.scoped_type_bindings); + let saved_type_scopes = std::mem::take(&mut self.type_arg_scopes); + let saved_arguments = std::mem::take(&mut self.current_type_arguments); + let saved_reflected = std::mem::take(&mut self.checked_reflected_fields); + let saved_proofs = std::mem::replace(&mut self.allow_checked_refinement_proofs, true); + let saved_resource = self + .resource_transport + .as_mut() + .map(|transport| std::mem::replace(&mut transport.checked_source_active, false)); + self.push_scope(); + let result = body(self, base); + while self.scopes.len() > base { + self.pop_scope(); + } + self.lexical_scope_floor = saved_floor; + self.current_namespace = saved_namespace; + self.current_function_trusted_stdlib = saved_trusted; + self.active_checked_function = saved_function; + self.active_checked_scope = saved_scope; + self.active_reflected_capture_origin = saved_capture; + self.scoped_type_bindings = saved_bindings; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_arguments; + self.checked_reflected_fields = saved_reflected; + self.allow_checked_refinement_proofs = saved_proofs; + if let (Some(transport), Some(saved)) = (&mut self.resource_transport, saved_resource) { + transport.checked_source_active = saved; + } + let restored = self.restore_checked_field_body(saved_field); + match result { + Err(error) => Err(error), + Ok(value) => restored.map(|()| value), + } + } + + pub(super) fn eval_spawn_flow(&mut self, source: &Expr) -> Result { + let Expr::Spawn(inner, _) = source else { + return Err("spawn: expected original Spawn".into()); + }; + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return self.eval_spawn_flow_unchecked(inner); + }; + let Expr::Call(callee, arguments, _) = inner.as_ref() else { + return Err("spawn: expected call expression".into()); + }; + let name = Self::dotted_expr_name(callee).ok_or("spawn: expected actor type name")?; + let name = self + .registry_name(&self.actor_defs, &name) + .ok_or("spawn: unknown checked actor type")?; + let registered = self + .actor_defs + .get(&name) + .ok_or("spawn: missing checked actor registration")?; + let prepared = context + .checked + .prepare_actor_spawn(source, registered) + .map_err(|error| error.to_string())?; + let actor = prepared.original().map_err(|error| error.to_string())?; + let resource_initializers = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + .map(|transport| -> Result<_, String> { + if !Arc::ptr_eq(transport.checked.program(), prepared.program()) { + return Err( + "actor initializer belongs to a foreign checked Resource program".into(), + ); + } + let bodies = transport + .checked + .prepare_actor_initializers(registered) + .map_err(|error| error.to_string())?; + if bodies.len() != actor.state_fields.len() { + return Err("actor initializer changed its original fields".into()); + } + for (reference, field) in bodies.iter().zip(&actor.state_fields) { + let original = reference + .original_expression() + .map_err(|error| error.to_string())?; + if !std::ptr::eq(original, &field.value) { + return Err("actor initializer changed its original Source".into()); + } + } + Ok(CheckedResourceActorInitializers { + program: prepared.program().clone(), + bodies, + }) + }) + .transpose()?; + let mut capabilities = HashMap::new(); + for (argument, &index) in arguments.iter().zip(prepared.parameter_indices()) { + let value = match self.eval_expr_flow(&argument.value)? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), + ExprFlow::Resource(_) => { + return Err("actor Resource custody has no checked transport proof".into()); + } + }; + if value.contains_live_resource_or_grant() { + return Err("actor Resource custody has no checked transport proof".into()); + } + let parameter = &actor.capability_params[index]; + let ty = Self::actor_program_type( + prepared.program(), + prepared + .capability_type(index) + .map_err(|error| error.to_string())?, + parameter.name.span, + )?; + let value = self.normalize_value_for_type(&ty, value)?; + capabilities.insert(parameter.name.name.clone(), value); + } + let state = + self.with_actor_frame(None, prepared.namespace().map(str::to_owned), |this, _| { + for (index, parameter) in actor.capability_params.iter().enumerate() { + let value = capabilities + .get(¶meter.name.name) + .ok_or("spawn lost a capability argument")? + .clone(); + let ty = Self::actor_program_type( + prepared.program(), + prepared + .capability_type(index) + .map_err(|error| error.to_string())?, + parameter.name.span, + )?; + this.set_variable_with_type(¶meter.name.name, value, ty); + } + let mut state = HashMap::new(); + for (index, (field, reference)) in actor + .state_fields + .iter() + .zip(prepared.initializers()) + .enumerate() + { + let flow = this.with_checked_field_region(reference, |this| { + match &resource_initializers { + Some(initializers) => initializers.evaluate(this, index), + None => this.eval_expr_flow(&field.value), + } + })?; + let value = match flow { + ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "actor Resource custody has no checked transport proof".into() + ); + } + ExprFlow::Signal(_) => { + return Err("actor initializer cannot transfer a control signal".into()); + } + }; + if value.contains_live_resource_or_grant() { + return Err("actor Resource custody has no checked transport proof".into()); + } + let ty = Self::actor_program_type( + prepared.program(), + prepared + .state_type(index) + .map_err(|error| error.to_string())?, + field.name.span, + )?; + let value = this.normalize_value_for_type(&ty, value)?; + // Later initializers resolve actual earlier state in declaration order. + this.set_variable_with_type(&field.name.name, value.clone(), ty); + state.insert(field.name.name.clone(), value); + } + Ok(state) + })?; + let identity = self.next_actor_id; + self.next_actor_id = identity + .checked_add(1) + .ok_or("actor identity space exhausted")?; + self.actor_instances.insert( + identity, + ActorInstance { + type_name: name, + state, + capabilities, + checked_origin: Some(prepared.origin()), + }, + ); + Ok(ExprFlow::Value(Value::Actor(identity))) + } + + pub(super) fn eval_actor_message_flow(&mut self, source: &Expr) -> Result { + let is_ask = matches!(source, Expr::Ask(..)); + let inner = match source { + Expr::Send(inner, _) | Expr::Ask(inner, _) => inner.as_ref(), + _ => return Err("send/ask: missing original message occurrence".into()), + }; + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return self + .eval_actor_message_unchecked(inner, is_ask) + .map(ExprFlow::Value); + }; + let prepared = context + .checked + .prepare_actor_message(source) + .map_err(|error| error.to_string())?; + let receiver = prepared.receiver().map_err(|error| error.to_string())?; + let value = match self.eval_expr_flow(receiver)? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), + ExprFlow::Resource(_) => { + return Err("actor Resource custody has no checked transport proof".into()); + } + }; + let Value::Actor(identity) = value.payload() else { + return Err(format!("send/ask: expected actor value, got {value}")); + }; + let identity = *identity; + let mut values = Vec::new(); + for argument in prepared.arguments().map_err(|error| error.to_string())? { + let value = match self.eval_expr_flow(&argument.value)? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), + ExprFlow::Resource(_) => { + return Err("actor Resource custody has no checked transport proof".into()); + } + }; + if value.contains_live_resource_or_grant() { + return Err("actor Resource custody has no checked transport proof".into()); + } + values.push(value); + } + let context = self + .field_update_context + .as_ref() + .filter(|context| context.active) + .ok_or("message lost its checked caller")?; + context + .checked + .revalidate_actor_message(&prepared) + .map_err(|error| error.to_string())?; + let instance = self + .actor_instances + .get(&identity) + .ok_or("unknown actor instance")?; + if instance + .state + .values() + .chain(instance.capabilities.values()) + .any(Value::contains_live_resource_or_grant) + { + return Err("actor Resource custody has no checked transport proof".into()); + } + let registered = self + .actor_defs + .get(&instance.type_name) + .ok_or("unknown actor registration")?; + let handler = context + .checked + .prepare_actor_handler(&prepared, registered) + .map_err(|error| error.to_string())?; + if !instance + .checked_origin + .as_ref() + .is_some_and(|origin| handler.matches_instance(origin)) + { + return Err("actor instance has no matching checked program origin".into()); + } + let state = instance.state.clone(); + let capabilities = instance.capabilities.clone(); + let (actor, original) = handler.original().map_err(|error| error.to_string())?; + let mut arguments = vec![Value::Nothing; original.params.len()]; + for (fact, value) in prepared.packet().arguments.iter().zip(values) { + if !matches!( + fact.effect, + CheckedCallerEffect::Copy + | CheckedCallerEffect::TransferOwned + | CheckedCallerEffect::RelinquishOwned + ) { + return Err("actor message has no owned caller transfer".into()); + } + let ty = Self::actor_checked_type(&handler, fact.parameter_type, fact.source_span)?; + let source_ty = handler + .program() + .checked() + .interner + .type_name(fact.actual_type); + arguments[fact.parameter_index] = + self.normalize_and_validate_value_from_source(&ty, value, Some(&source_ty))?; + } + let metadata = handler.metadata().map_err(|error| error.to_string())?; + let (response, updated_state) = self.with_actor_frame( + Some(&handler), + handler.namespace().map(str::to_owned), + |this, captured| { + for binding in metadata.bindings() { + let value = match binding.role { + CheckedActorBindingRole::Capability { parameter } => { + capabilities.get(&actor.capability_params[parameter].name.name) + } + CheckedActorBindingRole::State { field } => { + state.get(&actor.state_fields[field].name.name) + } + CheckedActorBindingRole::Message { .. } => continue, + } + .ok_or("actor capture lost its original slot")? + .clone(); + let name = handler + .role_name(binding.role) + .map_err(|error| error.to_string())?; + let ty = Self::actor_checked_type(&handler, binding.fact.ty, name.span)?; + this.set_variable_with_type(&name.name, value, ty); + this.install_checked_actor_binding(&handler, binding.role)?; + } + this.push_scope(); + for (parameter, value) in arguments.into_iter().enumerate() { + let role = CheckedActorBindingRole::Message { parameter }; + let binding = metadata + .bindings() + .iter() + .find(|binding| binding.role == role) + .ok_or("actor message lost its checked parameter role")?; + let name = handler.role_name(role).map_err(|error| error.to_string())?; + let ty = Self::actor_checked_type(&handler, binding.fact.ty, name.span)?; + this.set_variable_with_type(&name.name, value, ty); + this.install_checked_actor_binding(&handler, role)?; + } + let mut response = Value::Nothing; + for statement in &original.body.stmts { + match this.exec_stmt_inner(statement)? { + Some(Signal::Respond(value)) => { + response = value; + break; + } + Some(Signal::ResourceReturn(..) | Signal::ResourceDefault(..)) => { + return Err( + "actor Resource custody has no checked transport proof".into() + ); + } + Some( + Signal::Return(..) + | Signal::Break + | Signal::Continue + | Signal::Default(_), + ) => break, + None => {} + } + } + let mut updated = HashMap::new(); + for binding in metadata.bindings() { + let CheckedActorBindingRole::State { field } = binding.role else { + continue; + }; + let name = &actor.state_fields[field].name; + let value = this + .scopes + .get(captured) + .and_then(|scope| scope.get(&name.name)) + .ok_or("actor state lost its exact captured slot")? + .clone(); + let ty = Self::actor_checked_type(&handler, binding.fact.ty, name.span)?; + updated.insert( + name.name.clone(), + this.normalize_value_for_type(&ty, value)?, + ); + } + Ok((response, updated)) + }, + )?; + // Preserve established publication order: state first, reply normalization afterwards. + self.actor_instances + .get_mut(&identity) + .ok_or("actor instance disappeared before publication")? + .state = updated_state; + if is_ask { + let ty = Self::actor_checked_type(&handler, metadata.response_type(), original.span)?; + self.normalize_value_for_type(&ty, response) + .map(ExprFlow::Value) + } else { + Ok(ExprFlow::Value(Value::Nothing)) + } + } +} + +#[cfg(test)] +#[path = "actor_handlers/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/interpreter/actor_handlers/tests.rs b/crates/jett_comptime/src/interpreter/actor_handlers/tests.rs new file mode 100644 index 00000000..67d6a3c6 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_handlers/tests.rs @@ -0,0 +1,162 @@ +use super::*; + +#[test] +fn actor_result_error_restores_sender_scopes_and_checked_context() { + let mut interpreter = Interpreter::new(); + interpreter.current_namespace = Some("sender".into()); + interpreter.current_function_trusted_stdlib = true; + interpreter.allow_checked_refinement_proofs = false; + let span = Span::new(FileId::new(0), 0, 1); + let ty = TypeExpr::Named(Ident { + name: "int64".into(), + span, + }); + interpreter + .type_arg_scopes + .push(HashMap::from([("SenderType".into(), ty.clone())])); + interpreter.current_type_arguments.push(ty); + interpreter.set_variable("retained", Value::Int64(11)); + let depth = interpreter.scopes.len(); + let result: Result<(), String> = + interpreter.with_actor_frame(None, Some("actor".into()), |this, captured| { + assert_eq!(captured, depth); + assert_eq!(this.current_namespace.as_deref(), Some("actor")); + assert!(this.type_arg_scopes.is_empty()); + assert!(this.current_type_arguments.is_empty()); + this.set_variable("private", Value::Int64(7)); + this.push_scope(); + Err("prepublication failure".into()) + }); + assert_eq!(result.unwrap_err(), "prepublication failure"); + assert_eq!(interpreter.scopes.len(), depth); + assert_eq!(interpreter.current_namespace.as_deref(), Some("sender")); + assert!(interpreter.current_function_trusted_stdlib); + assert!(!interpreter.allow_checked_refinement_proofs); + assert_eq!(interpreter.type_arg_scopes.len(), 1); + assert_eq!(interpreter.current_type_arguments.len(), 1); + assert_eq!( + interpreter.scopes[0].get("retained"), + Some(&Value::Int64(11)) + ); + assert!( + !interpreter + .scopes + .iter() + .any(|scope| scope.contains_key("private")) + ); +} + +#[test] +fn checked_source_actor_initializer_and_capture_preserve_original_hook_descriptors() { + for source in [ + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/actor_graph/fixtures/07_nested_hook_actor.jett" + )), + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/actor_graph/fixtures/08_nested_hook_capture_actor.jett" + )), + ] { + let program = crate::resource_execution::tests::program(source, false); + let definition = program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| { + definition.kind == jett_resolve::DefKind::Function && definition.name == "app.main" + }) + .unwrap() + .id; + // Match the real Driver entry: no optional expression-name projection. + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![]) + .unwrap(), + Value::Nothing + ); + assert_eq!(interpreter.actor_instances.len(), 1); + let actor = interpreter.actor_instances.values().next().unwrap(); + let descriptor = actor + .state + .get("dispose") + .or_else(|| actor.capabilities.get("dispose")) + .unwrap(); + let Value::ResourceHook(descriptor) = descriptor.payload() else { + panic!("exact original initializer/capture must retain a hook descriptor") + }; + let close = program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == jett_types::ResourceHookKind::Close) + .unwrap(); + let expected = interpreter + .resource_transport + .as_ref() + .unwrap() + .checked + .descriptor(close.definition) + .unwrap(); + assert!(descriptor.same_descriptor(&expected)); + let foreign = crate::resource_execution::tests::program(source, false); + let foreign_close = foreign + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == jett_types::ResourceHookKind::Close) + .unwrap(); + let foreign_execution = crate::resource_execution::CheckedExecution::new( + foreign.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let foreign_descriptor = foreign_execution + .descriptor(foreign_close.definition) + .unwrap(); + assert!(!descriptor.same_descriptor(&foreign_descriptor)); + assert!(interpreter.take_debug_events().is_empty()); + let transport = interpreter.resource_transport.as_ref().unwrap(); + assert!( + !transport.checked_source_active, + "checked top-level entry must restore the caller's inactive Source state" + ); + let mut registered = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Actor(actor) => Some(actor.clone()), + _ => None, + }) + .unwrap(); + registered.name.name = "app.Keeper".into(); + let bodies = transport + .checked + .prepare_actor_initializers(®istered) + .unwrap(); + let guarded = CheckedResourceActorInitializers { + program: program.clone(), + bodies, + }; + let mut foreign_interpreter = Interpreter::from_checked_resource_program( + foreign, + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + assert!(guarded.evaluate(&mut foreign_interpreter, 0).is_err()); + assert!( + !foreign_interpreter + .resource_transport + .as_ref() + .unwrap() + .checked_source_active + ); + } +} diff --git a/crates/jett_comptime/src/interpreter/actor_identity_tests.rs b/crates/jett_comptime/src/interpreter/actor_identity_tests.rs new file mode 100644 index 00000000..5e3aa90f --- /dev/null +++ b/crates/jett_comptime/src/interpreter/actor_identity_tests.rs @@ -0,0 +1,67 @@ +use super::*; + +fn actor(type_name: &str, state: i64) -> ActorInstance { + ActorInstance { + checked_origin: None, + type_name: type_name.into(), + state: HashMap::from([("count".into(), Value::Int64(state))]), + capabilities: HashMap::new(), + } +} + +#[test] +fn actor_identity_uses_live_nominal_instances_without_reading_state() { + let mut interpreter = Interpreter::new(); + interpreter + .actor_instances + .insert(0, actor("app.Counter", 7)); + interpreter + .actor_instances + .insert(1, actor("app.Counter", 7)); + interpreter.actor_instances.insert(2, actor("app.Other", 7)); + assert_eq!(interpreter.actor_identity_equal(0, 0), Ok(true)); + assert_eq!(interpreter.actor_identity_equal(0, 1), Ok(false)); + interpreter + .actor_instances + .get_mut(&0) + .unwrap() + .state + .insert("count".into(), Value::Int64(88)); + assert_eq!(interpreter.actor_identity_equal(0, 0), Ok(true)); + assert_eq!(interpreter.actor_identity_equal(0, 1), Ok(false)); + assert_eq!( + interpreter.actor_identity_equal(0, 2), + Err("actor equality requires matching nominal actor types".into()) + ); + assert_eq!( + interpreter.actor_identity_equal(99, 99), + Err("actor equality: unknown actor instance #99".into()) + ); + interpreter.actor_instances.remove(&0); + assert_eq!( + interpreter.actor_identity_equal(0, 0), + Err("actor equality: unknown actor instance #0".into()) + ); +} + +#[test] +fn actor_identity_enum_payloads_validate_liveness_and_preserve_pending_depth() { + let mut interpreter = Interpreter::new(); + interpreter + .actor_instances + .insert(0, actor("app.Counter", 7)); + interpreter + .actor_instances + .insert(1, actor("app.Counter", 7)); + let same = Value::Actor(0); + let distinct = Value::Actor(1); + assert_eq!(interpreter.enum_payload_equal(&same, &same), Ok(true)); + assert_eq!(interpreter.enum_payload_equal(&same, &distinct), Ok(false)); + let pending = Value::Pending(Box::new(same.clone())); + assert_eq!(interpreter.enum_payload_equal(&pending, &pending), Ok(true)); + assert_eq!(interpreter.enum_payload_equal(&same, &pending), Ok(false)); + assert_eq!( + interpreter.enum_payload_equal(&Value::Actor(99), &Value::Actor(99)), + Err("actor equality: unknown actor instance #99".into()) + ); +} diff --git a/crates/jett_comptime/src/interpreter/debug.rs b/crates/jett_comptime/src/interpreter/debug.rs new file mode 100644 index 00000000..620246fc --- /dev/null +++ b/crates/jett_comptime/src/interpreter/debug.rs @@ -0,0 +1,425 @@ +//! Typed, non-destructive rendering for compiler-owned debug observations. +use super::{Expr, FileId, Interpreter, ReflectionFieldInfo, Span, TypeExpr, Value}; + +mod print; + +impl Interpreter { + pub(super) fn set_inferred_debug_binding( + &mut self, + name: &super::Ident, + value: Value, + fallback: Option<&TypeExpr>, + ) { + let ty = self + .checked_expression_type(name.span) + .and_then(|name| Self::debug_type(name)) + .or_else(|| fallback.cloned()); + if let Some(ty) = ty { + self.set_variable_with_type(&name.name, value, ty); + } else { + self.set_variable(&name.name, value); + } + } + + pub(super) fn debug_expression_type(&self, expression: &Expr) -> Option { + match expression { + Expr::Clone(inner, _) + | Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Run(inner, _) => self.debug_expression_type(inner), + Expr::Join(inner, span) => { + if let Some(ty) = self + .checked_expression_type(*span) + .and_then(|name| Self::simple_type_expr_from_name(name, *span)) + { + return Some(ty); + } + let ty = self.debug_expression_type(inner)?; + if self + .join_preserves_result(inner) + .unwrap_or_else(|| self.declared_join_type_is_result(&ty)) + { + Some(ty) + } else { + Some(TypeExpr::Generic( + super::Ident { + name: "result".into(), + span: *span, + }, + vec![ + ty, + TypeExpr::Named(super::Ident { + name: "string".into(), + span: *span, + }), + ], + *span, + )) + } + } + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + let type_args = match expression { + Expr::GenericCall(_, args, _, _) => args.as_slice(), + _ => &[], + }; + let actual = args + .iter() + .map(|arg| self.debug_expression_type(&arg.value)) + .collect::>>() + .and_then(|actual| { + let order = self + .source_function_argument_order(callee, args, false) + .ok()?; + Self::reorder_function_arguments(actual, order.as_deref()).ok() + }); + self.debug_call_result_type(callee, type_args, actual.as_deref().unwrap_or(&[])) + .or_else(|| self.call_argument_type(expression)) + } + Expr::FieldAccess(owner, field, _) => { + let ty = self.debug_expression_type(owner)?; + self.debug_fields(&ty, None) + .into_iter() + .find(|(name, _)| name == &field.name) + .map(|(_, ty)| ty) + .or_else(|| self.call_argument_type(expression)) + } + _ => self.call_argument_type(expression), + } + } + + fn debug_call_result_type( + &self, + callee: &Expr, + type_args: &[TypeExpr], + actual: &[TypeExpr], + ) -> Option { + let callee = Self::unparenthesized(callee); + if self.is_value_call_target(callee) { + if let Some(TypeExpr::Function(_, result, _)) = self.call_argument_type(callee) { + return Some(*result); + } + return None; + } + let source_name = Self::dotted_expr_name(callee)?; + let name = self.registry_name(&self.functions, &source_name)?; + let registered = self.functions.get(&name)?; + let definition = ®istered.definition; + let args = self + .inferred_user_function_type_args_from_types(callee, type_args, actual) + .unwrap_or_else(|| { + type_args + .iter() + .map(|arg| self.substitute_type_expr(arg)) + .collect() + }); + if args.len() != definition.type_params.len() { + return None; + } + let substitutions = definition + .type_params + .iter() + .zip(args) + .map(|(param, arg)| (param.name.clone(), arg)) + .collect(); + definition.return_type.as_ref().map(|ty| { + self.substitute_type_expr_with_map_in_namespace( + ty, + &substitutions, + registered.namespace.as_deref(), + ) + }) + } + + pub(super) fn debug_pipeline_result_type( + &self, + step: &super::PipelineStep, + input: Option<&TypeExpr>, + ) -> Option { + let (callee, type_args, args) = match &step.function { + Expr::GenericCall(callee, types, args, _) => { + (callee.as_ref(), types.as_slice(), args.as_slice()) + } + Expr::Call(callee, args, _) => (callee.as_ref(), &[][..], args.as_slice()), + callee => (callee, &[][..], step.extra_args.as_slice()), + }; + let mut actual = input.cloned().into_iter().collect::>(); + if let Some(extra) = args + .iter() + .map(|arg| self.debug_expression_type(&arg.value)) + .collect::>>() + { + actual.extend(extra); + } else { + actual.clear(); + } + let order = self + .source_function_argument_order(callee, args, true) + .ok()?; + let actual = Self::reorder_function_arguments(actual, order.as_deref()).ok()?; + self.debug_call_result_type(callee, type_args, &actual) + } + + pub(super) fn debug_expression_args(&self, expression: &Expr) -> Vec { + self.debug_type_args(self.debug_expression_type(expression).as_ref()) + } + + pub(super) fn debug_type_args(&self, ty: Option<&TypeExpr>) -> Vec { + match ty.map(|ty| self.resolved_inference_base_type(ty)) { + Some(TypeExpr::Generic(_, args, _)) => args, + Some(ty @ TypeExpr::Named(_)) => vec![ty], + _ => Vec::new(), + } + } + + pub(super) fn debug_type(name: &str) -> Option { + let span = Span::new(FileId::new(0), 0, 0); + if let Some(inner) = name.strip_prefix("view ") { + return Some(TypeExpr::View(Box::new(Self::debug_type(inner)?), span)); + } + if let Some((params, result)) = Self::split_function_type_display(name) { + let params = if params.is_empty() { + Vec::new() + } else { + Self::split_type_display_args(params) + .into_iter() + .map(|param| Self::debug_type(param.trim())) + .collect::>>()? + }; + return Some(TypeExpr::Function( + params, + Box::new(Self::debug_type(result)?), + span, + )); + } + if let Some((owner, args)) = Self::split_generic_type_display(name) { + let args = Self::split_type_display_args(args) + .into_iter() + .map(|arg| Self::debug_type(arg.trim())) + .collect::>>()?; + return Some(TypeExpr::Generic( + super::Ident { + name: owner.to_owned(), + span, + }, + args, + span, + )); + } + if let Some((owner, state)) = name.rsplit_once(" at ") { + return Some(TypeExpr::StateQualified( + Box::new(Self::debug_type(owner)?), + super::Ident { + name: state.to_owned(), + span, + }, + span, + )); + } + Some(TypeExpr::Named(super::Ident { + name: name.to_owned(), + span, + })) + } + + pub(super) fn debug_fields( + &self, + ty: &TypeExpr, + member: Option<(&str, bool)>, + ) -> Vec<(String, TypeExpr)> { + let checked: Option<&[ReflectionFieldInfo]> = match member { + Some((name, true)) => self.checked_machine(ty).and_then(|machine| { + machine + .states + .iter() + .find(|state| state.name == name) + .map(|state| state.fields.as_slice()) + }), + Some((name, false)) => self.checked_type_variants(ty).and_then(|variants| { + variants + .iter() + .find(|variant| variant.name == name) + .map(|variant| variant.fields.as_slice()) + }), + None => self.checked_type_fields(ty), + }; + if let Some(fields) = checked { + return fields + .iter() + .filter_map(|field| { + Self::debug_type(&field.type_name).map(|ty| (field.name.clone(), ty)) + }) + .collect(); + } + let fields = match member { + Some((name, true)) => self + .type_expr_machine(ty) + .states + .into_iter() + .find(|state| state.name == name) + .map(|state| state.fields) + .unwrap_or_default(), + Some((name, false)) => self + .type_expr_variants(ty) + .into_iter() + .find(|variant| variant.name == name) + .map(|variant| variant.fields) + .unwrap_or_default(), + None => self.type_expr_fields(ty), + }; + fields + .into_iter() + .map(|field| (field.name, field.ty)) + .collect() + } + + fn debug_nominal_owner(&self, declared: Option, concrete: &str) -> Option { + fn name(ty: &TypeExpr) -> Option<&str> { + match ty { + TypeExpr::Named(name) | TypeExpr::Generic(name, _, _) => Some(&name.name), + TypeExpr::StateQualified(inner, _, _) | TypeExpr::View(inner, _) => name(inner), + _ => None, + } + } + // Interface bindings retain their declared label, but fields belong to + // the concrete runtime owner. Keep generic arguments when the declared + // and concrete nominal owners agree. + declared + .filter(|ty| name(ty) == Some(concrete)) + .or_else(|| Self::debug_type(concrete)) + } + + pub(super) fn format_debug_value(&self, value: &Value, ty: Option<&TypeExpr>) -> String { + let ty = ty.map(|ty| self.resolved_inference_base_type(ty)); + if let Some(TypeExpr::View(inner, _)) = &ty { + return self.format_debug_value(value, Some(inner)); + } + let args = match &ty { + Some(TypeExpr::Generic(name, _, _)) if name.name == "secret" => { + return "[redacted]".to_owned(); + } + Some(TypeExpr::Generic(_, args, _)) => args.as_slice(), + _ => &[], + }; + match value { + Value::Typed { type_name, value } => { + let concrete = Self::debug_type(type_name); + self.format_debug_value(value, concrete.as_ref().or(ty.as_ref())) + } + Value::Pending(inner) => { + format!("pending({})", self.format_debug_value(inner, ty.as_ref())) + } + Value::List(items) | Value::Set(items) => { + let name = if matches!(value, Value::List(_)) { + "list" + } else { + "set" + }; + let items = items + .iter() + .map(|item| self.format_debug_value(item, args.first())) + .collect::>(); + format!("{name}({})", items.join(", ")) + } + Value::Map(entries) => { + let entries = entries + .iter() + .map(|(key, value)| { + format!( + "{}: {}", + self.format_debug_value(key, args.first()), + self.format_debug_value(value, args.get(1)), + ) + }) + .collect::>(); + format!("map({})", entries.join(", ")) + } + Value::OptionalSome(inner) => { + format!("some({})", self.format_debug_value(inner, args.first())) + } + Value::ResultOk(inner) => { + format!("ok({})", self.format_debug_value(inner, args.first())) + } + Value::ResultFail(inner) => { + format!("fail({})", self.format_debug_value(inner, args.get(1))) + } + Value::Struct { + concrete_type, + type_name, + fields, + } => { + let owner = concrete_type + .as_deref() + .and_then(Self::debug_type) + .or_else(|| self.debug_nominal_owner(ty, type_name)); + let types = owner + .as_ref() + .map(|ty| self.debug_fields(ty, None)) + .unwrap_or_default(); + let fields = fields + .iter() + .map(|(name, value)| { + let ty = types + .iter() + .find(|(field, _)| field == name) + .map(|(_, ty)| ty); + format!("{name}: {}", self.format_debug_value(value, ty)) + }) + .collect::>(); + format!("{type_name}({})", fields.join(", ")) + } + Value::Enum { + type_name, + variant: member, + fields, + } + | Value::Machine { + type_name, + state: member, + fields, + } => { + let machine = matches!(value, Value::Machine { .. }); + let owner = self.debug_nominal_owner(ty, type_name); + let types = owner + .as_ref() + .map(|ty| self.debug_fields(ty, Some((member, machine)))) + .unwrap_or_default(); + let separator = if machine { "@" } else { "." }; + let mut result = format!("{type_name}{separator}{member}"); + if !fields.is_empty() { + let fields = fields + .iter() + .enumerate() + .map(|(index, value)| { + self.format_debug_value(value, types.get(index).map(|(_, ty)| ty)) + }) + .collect::>(); + result.push_str(&format!("({})", fields.join(", "))); + } + result + } + Value::TypeConstruction { + type_name, + variant, + state, + fields, + } => { + let mut owner = type_name.clone(); + if let Some(variant) = variant { + owner.push_str(&format!(".{variant}")); + } + if let Some(state) = state { + owner.push_str(&format!("@{state}")); + } + let fields = fields + .iter() + .map(|(_, name, type_name, value)| { + let ty = Self::debug_type(type_name); + format!("{name}: {}", self.format_debug_value(value, ty.as_ref())) + }) + .collect::>(); + format!("TypeConstruction[{owner}]({})", fields.join(", ")) + } + _ => value.to_string(), + } + } +} diff --git a/crates/jett_comptime/src/interpreter/debug/print.rs b/crates/jett_comptime/src/interpreter/debug/print.rs new file mode 100644 index 00000000..2d7f1a22 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/debug/print.rs @@ -0,0 +1,696 @@ +//! Typed rendering only for accepted compiler-owned Print/Println observations. +use super::*; +use crate::debug::DebugEventKind; +use jett_intrinsics::IntrinsicId; + +enum PrintFrame<'a> { + Value(&'a Value, Option), + Text(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ty(name: &str) -> TypeExpr { + Interpreter::debug_type(name).unwrap() + } + + fn registered() -> Interpreter { + let parsed = jett_parser::parse( + r#"namespace models +type Token = secret[string] +type TokenAlias = Token +type QualifiedToken = Token where true +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + token: TokenAlias + value: T +enum Choice: + item(token: Token, value: int64) + empty +machine Session: + states: + idle + active(token: Token, value: int64) + transitions: + idle to active +"#, + FileId::new(0), + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + interpreter + } + + fn secret() -> Value { + Value::String("unpublished-secret".into()) + } + + #[test] + fn print_redacts_qualified_aliases_before_payload_and_inner_pending_depth() { + let interpreter = registered(); + let value = Value::Pending(Box::new(Value::Pending(Box::new(secret())))); + for name in [ + "secret[string]", + "models.TokenAlias", + "models.QualifiedToken", + ] { + assert_eq!( + interpreter.format_debug_print_value(&value, Some(&ty(name))), + "[redacted]", + "{name}", + ); + } + assert_eq!( + interpreter + .format_debug_print_value(&Value::String("[redacted]".into()), Some(&ty("string"))), + "[redacted]", + ); + assert_eq!( + interpreter.format_debug_print_value( + &Value::Pending(Box::new(Value::String("public".into()))), + Some(&ty("string")) + ), + "pending(public)", + ); + } + + #[test] + fn print_erased_generic_owner_uses_exact_fields_and_keeps_public_values() { + let interpreter = registered(); + let concrete = Value::Struct { + concrete_type: Some("models.Boxed[list[int64]]".into()), + type_name: "models.Boxed".into(), + fields: vec![ + ("token".into(), secret()), + ( + "value".into(), + Value::List(vec![Value::Int64(7), Value::Int64(9)]), + ), + ], + }; + let erased = Value::Typed { + type_name: "models.Boxed[list[int64]]".into(), + value: Box::new(concrete), + }; + assert_eq!( + interpreter.format_debug_print_value(&erased, Some(&ty("models.Named"))), + "models.Boxed(token: [redacted], value: list(7, 9))", + ); + // Rendering is nonowning, including the concrete identity used later. + let Value::Struct { + concrete_type, + fields, + .. + } = erased.payload() + else { + panic!("renderer changed the source carrier"); + }; + assert_eq!(concrete_type.as_deref(), Some("models.Boxed[list[int64]]")); + assert_eq!(fields[0].1, secret()); + } + + #[test] + fn print_recurses_through_each_typed_container_and_sum_payload() { + let interpreter = registered(); + let cases = [ + ( + Value::List(vec![secret()]), + "list[secret[string]]", + "list([redacted])", + ), + ( + Value::Set(vec![secret()]), + "set[secret[string]]", + "set([redacted])", + ), + ( + Value::Map(vec![(Value::String("public".into()), secret())]), + "map[string, secret[string]]", + "map(public: [redacted])", + ), + ( + Value::OptionalSome(Box::new(secret())), + "optional[secret[string]]", + "some([redacted])", + ), + ( + Value::ResultOk(Box::new(secret())), + "result[secret[string], int64]", + "ok([redacted])", + ), + ( + Value::ResultFail(Box::new(secret())), + "result[int64, secret[string]]", + "fail([redacted])", + ), + ( + Value::OptionalSome(Box::new(Value::List(vec![Value::Pending(Box::new( + secret(), + ))]))), + "optional[list[secret[string]]]", + "some(list([redacted]))", + ), + ]; + for (value, name, expected) in cases { + assert_eq!( + interpreter.format_debug_print_value(&value, Some(&ty(name))), + expected, + "{name}" + ); + } + } + + #[test] + fn print_keeps_enum_machine_and_incomplete_builder_order_with_typed_redaction() { + let interpreter = registered(); + let choice = Value::Enum { + type_name: "models.Choice".into(), + variant: "item".into(), + fields: vec![Value::Pending(Box::new(secret())), Value::Int64(3)], + }; + let session = Value::Machine { + type_name: "models.Session".into(), + state: "active".into(), + fields: vec![secret(), Value::Int64(4)], + }; + assert_eq!( + interpreter.format_debug_print_value(&choice, Some(&ty("models.Choice"))), + "models.Choice.item([redacted], 3)" + ); + assert_eq!( + interpreter.format_debug_print_value(&session, Some(&ty("models.Session"))), + "models.Session@active([redacted], 4)" + ); + for (variant, state, label) in [ + (None, None, "models.Boxed[int64]"), + (Some("item".into()), None, "models.Boxed[int64].item"), + (None, Some("active".into()), "models.Boxed[int64]@active"), + ] { + let builder = Value::TypeConstruction { + type_name: "models.Boxed[int64]".into(), + variant, + state, + fields: vec![ + (1, "value".into(), "int64".into(), Value::Int64(7)), + (0, "token".into(), "models.Token".into(), secret()), + ], + }; + assert_eq!( + interpreter.format_debug_print_value(&builder, Some(&ty("TypeConstruction"))), + format!("TypeConstruction[{label}](value: 7, token: [redacted])") + ); + } + } + + #[test] + fn print_missing_field_or_container_metadata_never_falls_back_to_raw_payload() { + let interpreter = Interpreter::new(); + let unknown = Value::Struct { + type_name: "Unknown".into(), + concrete_type: None, + fields: vec![("token".into(), secret())], + }; + assert_eq!( + interpreter.format_debug_print_value(&unknown, None), + "Unknown(token: [redacted])" + ); + assert_eq!( + interpreter.format_debug_print_value(&Value::List(vec![secret()]), None), + "list([redacted])" + ); + let builder = Value::TypeConstruction { + type_name: "Unknown".into(), + variant: None, + state: None, + fields: vec![(0, "token".into(), "".into(), secret())], + }; + assert_eq!( + interpreter.format_debug_print_value(&builder, None), + "TypeConstruction[Unknown](token: [redacted])" + ); + } + + #[test] + fn print_keeps_opaque_labels_without_observing_callback_captures() { + let interpreter = Interpreter::new(); + let function = Value::Function { + type_context: Box::default(), + params: vec![], + body: jett_parser::ast::Block { + stmts: vec![], + span: Span::new(FileId::new(0), 0, 0), + }, + captures: [("token".into(), secret())].into(), + capture_types: [("token".into(), ty("secret[string]"))].into(), + namespace_aliases: Default::default(), + namespace: None, + }; + assert_eq!( + interpreter.format_debug_print_value(&function, None), + "function()" + ); + assert_eq!( + interpreter + .format_debug_print_value(&Value::NamedFunction("models.inspect".into()), None), + "function(models.inspect)" + ); + assert_eq!( + interpreter.format_debug_print_value(&Value::Actor(7), None), + "actor#7" + ); + } + + #[test] + fn print_authentic_reflected_callables_never_invoke_sources_or_observe_captures() { + use crate::checked_types::CheckedExpressionTypes; + use jett_common::SourceOrigin; + use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + use std::collections::HashMap; + use std::sync::Arc; + + let source = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function requested(view callback: function() returns Positive) returns nothing: + return nothing +function named() returns int64: + string marker = "callback" + trace marker + return 7 +function captured(seed: int64) returns function() returns int64: + function() returns int64 callback = function() returns int64: + string marker = "callback" + trace marker + return seed + return callback +"#; + let file = FileId::new(0); + let program = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ); + let mut interpreter = Interpreter::new(); + interpreter.register_module(program.module()); + interpreter.set_reflection_metadata(program.checked().reflection_metadata.clone()); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + })); + let requested = ty("function() returns app.Positive"); + let captured = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { capture_types, .. } = &captured else { + panic!("original source must produce the captured closure"); + }; + assert_eq!(capture_types.len(), 1); + assert_eq!( + super::super::super::type_expr_display(capture_types.get("seed").unwrap()), + "int64" + ); + assert!(interpreter.take_debug_events().is_empty()); + for original in [Value::NamedFunction("app.named".into()), captured] { + let label = original.to_string(); + let observed = interpreter + .prepare_reflected_callable(original, &requested, "function() returns int64") + .unwrap(); + assert!(matches!(observed, Value::ReflectedCallable(_))); + assert!(interpreter.take_debug_events().is_empty()); + interpreter.emit_typed_debug_print( + DebugEventKind::Println, + std::slice::from_ref(&observed), + &[Some(requested.clone())], + ); + interpreter.emit_typed_debug_print( + DebugEventKind::Println, + &[Value::List(vec![observed.clone()])], + &[Some(ty("list[function() returns app.Positive]"))], + ); + assert_eq!( + interpreter.take_debug_events(), + [ + crate::debug::DebugEvent { + kind: DebugEventKind::Println, + text: format!("{label}\n") + }, + crate::debug::DebugEvent { + kind: DebugEventKind::Println, + text: format!("list({label})\n") + }, + ] + ); + // The unchanged authenticated value remains callable after observation. + let value = interpreter.call_fn_value(observed, Vec::new()).unwrap(); + assert_eq!(value.payload(), &Value::Int64(7)); + assert_eq!( + interpreter + .take_debug_events() + .into_iter() + .map(|event| event.text) + .collect::>(), + [ + "trace marker: string = callback\n", + "trace marker: string = predicate\n", + ] + ); + } + } + + #[test] + fn print_finite_values_do_not_acquire_a_rendering_depth_limit() { + let interpreter = Interpreter::new(); + let depth = 4096; + let mut value = Value::Int64(7); + for _ in 0..depth { + value = Value::Pending(Box::new(value)); + } + assert_eq!( + interpreter.format_debug_print_value(&value, Some(&ty("int64"))), + format!("{}7{}", "pending(".repeat(depth), ")".repeat(depth)) + ); + assert_eq!( + interpreter.format_debug_print_value(&value, Some(&ty("secret[int64]"))), + "[redacted]" + ); + // Release this host-built recursive test carrier without recursive Drop. + while let Value::Pending(inner) = value { + value = *inner; + } + } + + #[test] + fn print_metadata_is_paired_with_actuals_and_event_bytes_are_not_reclassified() { + let mut interpreter = registered(); + let args = [ + secret(), + Value::String("trace forged\n[redacted]".into()), + Value::Int64(7), + ]; + let types = [ + Some(ty("models.Token")), + Some(ty("string")), + Some(ty("int64")), + ]; + assert_eq!( + interpreter.emit_typed_debug_print(DebugEventKind::Print, &args, &types), + Value::Nothing + ); + assert_eq!( + interpreter.emit_typed_debug_print(DebugEventKind::Println, &[], &[]), + Value::Nothing + ); + assert_eq!( + interpreter.take_debug_events(), + [ + crate::debug::DebugEvent { + kind: DebugEventKind::Print, + text: "[redacted] trace forged\n[redacted] 7".into() + }, + crate::debug::DebugEvent { + kind: DebugEventKind::Println, + text: "\n".into() + }, + ] + ); + } +} + +impl Interpreter { + pub(in super::super) fn debug_print_expression_type( + &self, + expression: &Expr, + ) -> Option { + self.checked_expression_type(expression.span()) + .and_then(|name| Self::debug_type(name)) + .or_else(|| self.debug_expression_type(expression)) + .map(|ty| self.substitute_type_expr(&ty)) + } + + pub(in super::super) fn debug_print_call_kind(&self, callee: &Expr) -> Option { + if self.is_value_call_target(callee) { + return None; + } + let source_name = Self::dotted_expr_name(callee)?; + let name = self + .registry_name(&self.functions, &source_name) + .unwrap_or(source_name); + Self::debug_print_kind(&name) + } + + pub(in super::super) fn debug_print_kind(name: &str) -> Option { + match IntrinsicId::from_callable_name(name)? { + IntrinsicId::Print => Some(DebugEventKind::Print), + IntrinsicId::Println => Some(DebugEventKind::Println), + _ => None, + } + } + + pub(in super::super) fn emit_typed_debug_print( + &mut self, + kind: DebugEventKind, + args: &[Value], + types: &[Option], + ) -> Value { + let mut text = args + .iter() + .enumerate() + .map(|(index, value)| { + self.format_debug_print_value(value, types.get(index).and_then(Option::as_ref)) + }) + .collect::>() + .join(" "); + if kind == DebugEventKind::Println { + text.push('\n'); + } + self.emit_debug_event(kind, text); + Value::Nothing + } + + fn push_print_child<'a>( + work: &mut Vec>, + value: &'a Value, + ty: Option<&TypeExpr>, + ) { + if let Some(ty) = ty { + work.push(PrintFrame::Value(value, Some(ty.clone()))); + } else { + // A missing field/container contract never authorizes raw display. + work.push(PrintFrame::Text("[redacted]".into())); + } + } + + pub(in super::super) fn format_debug_print_value( + &self, + value: &Value, + ty: Option<&TypeExpr>, + ) -> String { + let mut text = String::new(); + let mut work = vec![PrintFrame::Value(value, ty.cloned())]; + while let Some(frame) = work.pop() { + match frame { + PrintFrame::Text(part) => text.push_str(&part), + PrintFrame::Value(value, ty) => { + self.push_print_value(value, ty, &mut work, &mut text); + } + } + } + text + } + + fn push_print_value<'a>( + &self, + value: &'a Value, + ty: Option, + work: &mut Vec>, + text: &mut String, + ) { + let ty = ty.map(|ty| self.resolved_inference_base_type(&ty)); + let ty = match ty { + Some(TypeExpr::View(inner, _)) => { + work.push(PrintFrame::Value(value, Some(*inner))); + return; + } + ty => ty, + }; + let args = match &ty { + Some(TypeExpr::Generic(owner, _, _)) if owner.name == "secret" => { + // Do not inspect the value, its handles, or inner pending depth. + text.push_str("[redacted]"); + return; + } + Some(TypeExpr::Generic(_, args, _)) => args.as_slice(), + _ => &[], + }; + match value { + Value::Typed { type_name, value } => { + work.push(PrintFrame::Value(value, Self::debug_type(type_name).or(ty))); + } + Value::Pending(inner) => { + text.push_str("pending("); + work.push(PrintFrame::Text(")".into())); + work.push(PrintFrame::Value(inner, ty)); + } + Value::List(items) | Value::Set(items) => { + text.push_str(if matches!(value, Value::List(_)) { + "list(" + } else { + "set(" + }); + work.push(PrintFrame::Text(")".into())); + for (index, item) in items.iter().enumerate().rev() { + Self::push_print_child(work, item, args.first()); + if index != 0 { + work.push(PrintFrame::Text(", ".into())); + } + } + } + Value::Map(entries) => { + text.push_str("map("); + work.push(PrintFrame::Text(")".into())); + for (index, (key, value)) in entries.iter().enumerate().rev() { + Self::push_print_child(work, value, args.get(1)); + work.push(PrintFrame::Text(": ".into())); + Self::push_print_child(work, key, args.first()); + if index != 0 { + work.push(PrintFrame::Text(", ".into())); + } + } + } + Value::OptionalSome(inner) | Value::ResultOk(inner) | Value::ResultFail(inner) => { + let (prefix, index) = match value { + Value::OptionalSome(_) => ("some(", 0), + Value::ResultOk(_) => ("ok(", 0), + Value::ResultFail(_) => ("fail(", 1), + _ => unreachable!("matched sum payload"), + }; + text.push_str(prefix); + work.push(PrintFrame::Text(")".into())); + Self::push_print_child(work, inner, args.get(index)); + } + Value::Struct { + concrete_type, + type_name, + fields, + } => { + let owner = concrete_type + .as_deref() + .and_then(Self::debug_type) + .or_else(|| self.debug_nominal_owner(ty, type_name)); + let types = owner + .as_ref() + .map(|ty| self.debug_fields(ty, None)) + .unwrap_or_default(); + text.push_str(type_name); + text.push('('); + work.push(PrintFrame::Text(")".into())); + for (index, (name, value)) in fields.iter().enumerate().rev() { + let ty = types + .iter() + .find(|(field, _)| field == name) + .map(|(_, ty)| ty); + Self::push_print_child(work, value, ty); + work.push(PrintFrame::Text(format!("{name}: "))); + if index != 0 { + work.push(PrintFrame::Text(", ".into())); + } + } + } + Value::Enum { + type_name, + variant: member, + fields, + } + | Value::Machine { + type_name, + state: member, + fields, + } => { + let machine = matches!(value, Value::Machine { .. }); + let owner = self.debug_nominal_owner(ty, type_name); + let types = owner + .as_ref() + .map(|ty| self.debug_fields(ty, Some((member, machine)))) + .unwrap_or_default(); + text.push_str(type_name); + text.push(if machine { '@' } else { '.' }); + text.push_str(member); + if !fields.is_empty() { + text.push('('); + work.push(PrintFrame::Text(")".into())); + for (index, value) in fields.iter().enumerate().rev() { + Self::push_print_child(work, value, types.get(index).map(|(_, ty)| ty)); + if index != 0 { + work.push(PrintFrame::Text(", ".into())); + } + } + } + } + Value::TypeConstruction { + type_name, + variant, + state, + fields, + } => { + text.push_str("TypeConstruction["); + text.push_str(type_name); + if let Some(variant) = variant { + text.push('.'); + text.push_str(variant); + } + if let Some(state) = state { + text.push('@'); + text.push_str(state); + } + text.push_str("]("); + work.push(PrintFrame::Text(")".into())); + for (index, (_, name, type_name, value)) in fields.iter().enumerate().rev() { + let ty = (!type_name.is_empty()) + .then(|| Self::debug_type(type_name)) + .flatten(); + Self::push_print_child(work, value, ty.as_ref()); + work.push(PrintFrame::Text(format!("{name}: "))); + if index != 0 { + work.push(PrintFrame::Text(", ".into())); + } + } + } + // Primitive and opaque labels never inspect captures, actor state, + // or aggregate fields. Value display remains unchanged elsewhere. + Value::Int64(_) + | Value::Uint64(_) + | Value::Float64(_) + | Value::String(_) + | Value::Bool(_) + | Value::Bytes(_) + | Value::OptionalNone + | Value::Nothing + | Value::Capability(_) + | Value::Resource(_) + | Value::ResourceHook(_) + | Value::GrantedNetwork(_) + | Value::Actor(_) + | Value::Error(_) + | Value::NamedFunction(_) + | Value::ReflectedCallable(_) + | Value::Function { .. } => text.push_str(&value.to_string()), + } + } +} diff --git a/crates/jett_comptime/src/interpreter/field_updates.rs b/crates/jett_comptime/src/interpreter/field_updates.rs new file mode 100644 index 00000000..d368457d --- /dev/null +++ b/crates/jett_comptime/src/interpreter/field_updates.rs @@ -0,0 +1,253 @@ +//! Storage kernel for an authenticated ordinary checked field place. +//! +//! Deliberately not installed in `Interpreter`: its checked-body adapter and +//! exact lexical binding association must exist before Source can reach it. +//! Runtime layout records below validate storage; they never authorize a write. +use crate::value::Value; + +#[path = "field_updates/checked.rs"] +mod checked; +pub(super) use checked::FieldUpdateContext; + +pub(super) fn is_projected_target(target: &jett_parser::ast::Expr) -> bool { + match target { + jett_parser::ast::Expr::Paren(inner, _) => is_projected_target(inner), + jett_parser::ast::Expr::FieldAccess(..) => true, + _ => false, + } +} + +#[cfg(test)] +#[path = "field_updates/tests.rs"] +mod tests; + +/// Allocated from a checked canonical owner before evaluating the RHS. +/// No constructor from AST spelling, type names, or raw Values is provided. +#[derive(Debug)] +pub(super) struct FieldStoragePlan { + projections: Vec, +} + +#[derive(Debug)] +struct StorageProjection { + /// Exact retained wrappers in order. Refined enclosing owners are excluded + /// by checked-place admission, rather than peeled here to obtain permission. + wrappers: Vec, + owner: StorageOwner, + field_index: usize, +} + +#[derive(Debug)] +enum StorageOwner { + Struct { + type_name: String, + concrete_type: Option, + field_names: Vec, + }, + MachineState { + type_name: String, + state: String, + field_count: usize, + }, +} + +/// Only the missing checked adapter may construct this after ordinary RHS +/// normalization/conversion succeeds. This carries no Resource custody. +pub(super) struct StagedOrdinaryReplacement { + value: Value, +} + +impl std::fmt::Debug for StagedOrdinaryReplacement { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("StagedOrdinaryReplacement") + .finish_non_exhaustive() + } +} + +/// Structured internal refusal: no runtime payload or secret value is formatted. +/// The checked adapter must translate pending refusal through the established +/// typed inspection diagnostic, rather than add a second observation policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum FieldStorageError { + EmptyPath, + ResourceCustodyRequired, + PendingAncestor { projection: usize }, + WrapperMismatch { projection: usize }, + OwnerMismatch { projection: usize }, + FieldMismatch { projection: usize }, +} + +/// A rejected staged owner remains available for the existing cleanup path. +/// The kernel neither duplicates it nor replaces the target on failure. +#[derive(Debug)] +pub(super) struct RejectedReplacement { + pub(super) error: FieldStorageError, + pub(super) replacement: StagedOrdinaryReplacement, +} + +impl FieldStoragePlan { + /// The evaluator must call this before evaluating the RHS. A pending target + /// child is transportable: readiness is tested only for projection owners. + pub(super) fn preflight(&self, root: &Value) -> Result<(), FieldStorageError> { + if self.projections.is_empty() { + return Err(FieldStorageError::EmptyPath); + } + // An ordinary scalar child inside a Resource-bearing root must not + // circumvent authenticated aggregate replacement/custody handling. + if root.contains_live_resource_or_grant() { + return Err(FieldStorageError::ResourceCustodyRequired); + } + let mut current = root; + for (index, projection) in self.projections.iter().enumerate() { + current = projection.selected(current, index)?; + } + Ok(()) + } + + /// Called only after the evaluator reauthenticates the original checked + /// assignment and the same live lexical slot/generation after RHS completion. + /// All conversion, allocation and fallible preparation precede this method. + pub(super) fn replace( + &self, + root: &mut Value, + replacement: StagedOrdinaryReplacement, + ) -> Result { + if let Err(error) = self.preflight(root) { + return Err(RejectedReplacement { error, replacement }); + } + if replacement.value.contains_live_resource_or_grant() { + return Err(RejectedReplacement { + error: FieldStorageError::ResourceCustodyRequired, + replacement, + }); + } + let selected = match self.selected_mut(root) { + Ok(selected) => selected, + Err(error) => return Err(RejectedReplacement { error, replacement }), + }; + // This is the first mutation. No allocation, predicate, Source call, + // payload inspection, or other fallible work follows the displacement. + // The caller retires the one displaced ordinary owner through its normal + // cleanup path; all unchanged fields remain in their original storage. + Ok(std::mem::replace(selected, replacement.value)) + } + + fn selected_mut<'value>( + &self, + root: &'value mut Value, + ) -> Result<&'value mut Value, FieldStorageError> { + let mut current = root; + for (index, projection) in self.projections.iter().enumerate() { + current = projection.selected_mut(current, index)?; + } + Ok(current) + } +} + +impl StorageProjection { + fn selected<'value>( + &self, + value: &'value Value, + projection: usize, + ) -> Result<&'value Value, FieldStorageError> { + let owner = self.checked_wrappers(value, projection)?; + match (&self.owner, owner) { + ( + StorageOwner::Struct { + type_name, + concrete_type, + field_names, + }, + Value::Struct { + type_name: actual_name, + concrete_type: actual_concrete, + fields, + }, + ) if type_name == actual_name && concrete_type == actual_concrete => { + if fields.len() != field_names.len() + || fields + .iter() + .zip(field_names) + .any(|((actual, _), expected)| actual != expected) + { + return Err(FieldStorageError::FieldMismatch { projection }); + } + fields + .get(self.field_index) + .map(|(_, value)| value) + .ok_or(FieldStorageError::FieldMismatch { projection }) + } + ( + StorageOwner::MachineState { + type_name, + state, + field_count, + }, + Value::Machine { + type_name: actual_name, + state: actual_state, + fields, + }, + ) if type_name == actual_name && state == actual_state => { + if fields.len() != *field_count { + return Err(FieldStorageError::FieldMismatch { projection }); + } + fields + .get(self.field_index) + .ok_or(FieldStorageError::FieldMismatch { projection }) + } + _ => Err(FieldStorageError::OwnerMismatch { projection }), + } + } + + fn checked_wrappers<'value>( + &self, + mut value: &'value Value, + projection: usize, + ) -> Result<&'value Value, FieldStorageError> { + for expected in &self.wrappers { + match value { + Value::Pending(_) => { + return Err(FieldStorageError::PendingAncestor { projection }); + } + Value::Typed { + type_name, + value: inner, + } if type_name == expected => value = inner, + _ => return Err(FieldStorageError::WrapperMismatch { projection }), + } + } + match value { + Value::Pending(_) => Err(FieldStorageError::PendingAncestor { projection }), + Value::Typed { .. } => Err(FieldStorageError::WrapperMismatch { projection }), + _ => Ok(value), + } + } + + fn selected_mut<'value>( + &self, + value: &'value mut Value, + projection: usize, + ) -> Result<&'value mut Value, FieldStorageError> { + // Reuse exact shape validation before taking mutable descendant access. + self.selected(value, projection)?; + let mut owner = value; + for expected in &self.wrappers { + match owner { + Value::Typed { type_name, value } if type_name == expected => owner = value, + _ => return Err(FieldStorageError::WrapperMismatch { projection }), + } + } + match (&self.owner, owner) { + (StorageOwner::Struct { .. }, Value::Struct { fields, .. }) => fields + .get_mut(self.field_index) + .map(|(_, value)| value) + .ok_or(FieldStorageError::FieldMismatch { projection }), + (StorageOwner::MachineState { .. }, Value::Machine { fields, .. }) => fields + .get_mut(self.field_index) + .ok_or(FieldStorageError::FieldMismatch { projection }), + _ => Err(FieldStorageError::OwnerMismatch { projection }), + } + } +} diff --git a/crates/jett_comptime/src/interpreter/field_updates/checked.rs b/crates/jett_comptime/src/interpreter/field_updates/checked.rs new file mode 100644 index 00000000..b7462254 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/field_updates/checked.rs @@ -0,0 +1,835 @@ +//! Program-bound ordinary storage adapter. No Resource provider or custody. +use super::*; +use crate::interpreter::{ExprFlow, Interpreter, Signal}; +use crate::resource_execution::{ + CheckedBodyCursor, CheckedBodyReference, CheckedExecution, ExecutionPurpose, + PreparedFieldAssignment, PreparedFieldBinding, +}; +use crate::value::ClosureTypeArgument; +use jett_common::Span; +use jett_parser::ast::{AssignStmt, FunctionDef, Ident}; +use jett_resolve::DefId; +use jett_typecheck::assignment_places::{ + CheckedAssignmentPlace, CheckedFieldOwner, CheckedFieldUpdateResourceRequirement, +}; +use jett_typecheck::{CheckedBindingFact, CheckedResourceProgram}; +use jett_types::Type; +use std::collections::HashMap; +use std::sync::Arc; + +struct InstalledFieldBinding { + checked: PreparedFieldBinding, + name: String, + generation: u64, +} + +pub(in crate::interpreter) struct FieldUpdateContext { + pub(in crate::interpreter) checked: CheckedExecution, + pub(in crate::interpreter) active: bool, + slots: Vec>, + next_generation: Option, +} + +pub(in crate::interpreter) struct SavedFieldCursor { + cursor: CheckedBodyCursor, + active: bool, +} + +struct FieldSlotTicket { + scope: usize, + fact: CheckedBindingFact, + name: String, + generation: u64, + commit_generation: u64, +} + +impl FieldUpdateContext { + pub(in crate::interpreter) fn new( + program: Arc, + purpose: ExecutionPurpose, + scope_depth: usize, + ) -> Result, String> { + let checked = program.checked(); + let any = |_: &CheckedAssignmentPlace| true; + if checked + .intrinsic_ids + .values() + .all(|id| !field_reflection_intrinsic(*id)) + && !scoped_reflection(&checked.comptime_type_bindings) + && checked + .generic_function_instantiations + .iter() + .all(|instance| { + instance + .intrinsic_ids + .values() + .all(|id| !field_reflection_intrinsic(*id)) + && !scoped_reflection(&instance.comptime_type_bindings) + }) + && checked.actor_handlers.is_empty() + && checked.assignment_places.is_empty() + && !scoped_place_matches(&checked.comptime_type_bindings, &any) + && !checked + .generic_function_instantiations + .iter() + .any(|instance| { + !instance.assignment_places.is_empty() + || scoped_place_matches(&instance.comptime_type_bindings, &any) + }) + { + return Ok(None); + } + Ok(Some(Self { + checked: CheckedExecution::new(program, purpose).map_err(|error| error.to_string())?, + active: false, + slots: (0..scope_depth).map(|_| HashMap::new()).collect(), + next_generation: Some(0), + })) + } + + pub(in crate::interpreter) fn push_scope(&mut self) { + self.slots.push(HashMap::new()); + } + + pub(in crate::interpreter) fn pop_scope(&mut self) { + self.slots.pop(); + } + + fn fresh_generation(&mut self) -> Result { + let generation = self + .next_generation + .ok_or("ordinary field storage identity exhausted")?; + self.next_generation = generation.checked_add(1); + Ok(generation) + } + + pub(in crate::interpreter) fn notice_write(&mut self, scope: usize, name: &str) { + let Some(bindings) = self.slots.get(scope) else { + return; + }; + if !bindings.values().any(|binding| binding.name == name) { + return; + } + let Ok(generation) = self.fresh_generation() else { + // Setters predate fallible slot metadata. Exhaustion invalidates all + // prepared identities and must never wrap or admit an ABA match. + self.slots.iter_mut().for_each(HashMap::clear); + return; + }; + if let Some(bindings) = self.slots.get_mut(scope) { + for binding in bindings.values_mut().filter(|binding| binding.name == name) { + binding.generation = generation; + } + } + } + + pub(in crate::interpreter) fn replace_purpose(&mut self, purpose: ExecutionPurpose) { + self.checked.replace_purpose(purpose); + } + + fn enter( + &mut self, + reference: Option<&CheckedBodyReference>, + ) -> Result { + let saved = SavedFieldCursor { + cursor: self.checked.cursor(), + active: self.active, + }; + if let Some(reference) = reference { + self.checked + .install_field_function_body(reference) + .map_err(|error| error.to_string())?; + self.active = true; + } else { + self.checked.enter_ordinary(); + self.active = false; + } + Ok(saved) + } + + fn restore(&mut self, saved: SavedFieldCursor) -> Result<(), String> { + self.checked + .restore_field_cursor(saved.cursor) + .map_err(|error| error.to_string())?; + self.active = saved.active; + Ok(()) + } + + fn slot( + &self, + prepared: &PreparedFieldAssignment<'_>, + floor: usize, + ) -> Result<(usize, &InstalledFieldBinding), String> { + let fact = prepared.place().root; + self.slots + .iter() + .enumerate() + .rev() + .filter(|(index, _)| *index >= floor) + .find_map(|(index, bindings)| bindings.get(&fact.definition).map(|slot| (index, slot))) + .filter(|(_, slot)| prepared.accepts_binding(&slot.checked)) + .ok_or_else(|| "field assignment has no exact live declared storage slot".to_string()) + } +} + +fn inside(span: Span, parent: Span) -> bool { + span.file == parent.file && span.start >= parent.start && span.end <= parent.end +} + +fn scoped_place_matches( + scopes: &HashMap>, + predicate: &impl Fn(&CheckedAssignmentPlace) -> bool, +) -> bool { + scopes.values().flatten().any(|scope| { + scope.body.assignment_places.values().any(predicate) + || scoped_place_matches(&scope.body.comptime_type_bindings, predicate) + }) +} + +fn field_reflection_intrinsic(id: jett_intrinsics::IntrinsicId) -> bool { + matches!( + id, + jett_intrinsics::IntrinsicId::TypeFields + | jett_intrinsics::IntrinsicId::TypeFieldValue + | jett_intrinsics::IntrinsicId::TypeConstructStart + | jett_intrinsics::IntrinsicId::TypeConstructPut + | jett_intrinsics::IntrinsicId::TypeConstructFinish + ) +} + +fn scoped_reflection( + scopes: &HashMap>, +) -> bool { + scopes.values().flatten().any(|scope| { + scope + .body + .intrinsic_ids + .values() + .any(|id| field_reflection_intrinsic(*id)) + || scoped_reflection(&scope.body.comptime_type_bindings) + }) +} + +impl Interpreter { + pub(in crate::interpreter) fn checked_field_intrinsic_owner( + &self, + callee: &jett_parser::ast::Expr, + types: &[jett_parser::ast::TypeExpr], + arguments: &[jett_parser::ast::CallArg], + span: Span, + ) -> Result, String> { + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return Ok(None); + }; + let owner = context + .checked + .field_intrinsic_owner(callee, types, arguments, span) + .map_err(|error| error.to_string())?; + self.canonical_field_intrinsic_owner(context, owner) + } + + pub(in crate::interpreter) fn checked_field_pipeline_intrinsic_owner( + &self, + step: &jett_parser::ast::PipelineStep, + ) -> Result, String> { + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return Ok(None); + }; + let owner = context + .checked + .field_pipeline_intrinsic_owner(step) + .map_err(|error| error.to_string())?; + self.canonical_field_intrinsic_owner(context, owner) + } + + fn canonical_field_intrinsic_owner( + &self, + context: &FieldUpdateContext, + owner: Option, + ) -> Result, String> { + let Some(owner) = owner else { + return Ok(None); + }; + let checked = context.checked.program().checked(); + let name = checked.interner.type_name(owner); + let fields = checked + .reflection_metadata + .get_type_fields_for_id(owner) + .ok_or("checked field intrinsic owner has no canonical field metadata")?; + let expected_kind = match checked.interner.resolve(owner) { + Type::Struct(_) => "struct", + Type::Bitfield(_) => "bitfield", + _ => return Err("checked field intrinsic owner is not a struct or bitfield".into()), + }; + let metadata = self + .reflection_metadata + .as_ref() + .ok_or("checked field intrinsic owner has no installed reflection metadata")?; + if metadata.get_type_fields(&name) != Some(fields) + || metadata + .get_type_info(&name) + .is_none_or(|info| info.kind != expected_kind) + { + return Err( + "checked field intrinsic owner has conflicting canonical reflection metadata" + .into(), + ); + } + Self::debug_type(&name) + .map(Some) + .ok_or("checked field intrinsic owner has no canonical interpreter type".into()) + } + + pub(in crate::interpreter) fn prepare_checked_field_inline( + &self, + source: &Value, + ) -> Result, String> { + let Some(context) = &self.field_update_context else { + return Ok(None); + }; + let Value::Function { + body, type_context, .. + } = source + else { + return Ok(None); + }; + let program = context.checked.program(); + let checked = program.checked(); + let relevant = |place: &CheckedAssignmentPlace| inside(place.assignment_span, body.span); + let needed = checked.assignment_places.values().any(relevant) + || scoped_place_matches(&checked.comptime_type_bindings, &relevant) + || checked + .generic_function_instantiations + .iter() + .any(|instance| { + instance.assignment_places.values().any(relevant) + || scoped_place_matches(&instance.comptime_type_bindings, &relevant) + }); + if !context.active { + return if needed { + Err("inline field update requires its checked Source caller".to_string()) + } else { + Ok(None) + }; + } + let proof = crate::value::closure_capture::field_invocation_proof(source, program); + let Some(proof) = proof else { + return if needed { + Err("inline field update has no original capture witness".to_string()) + } else { + Ok(None) + }; + }; + match context + .checked + .prepare_field_inline_body(&proof, type_context) + { + Ok(reference) => Ok(Some(reference)), + Err(error) if needed => Err(error.to_string()), + Err(_) => Ok(None), + } + } + pub(in crate::interpreter) fn prepare_checked_field_reflection_loop( + &self, + source: &jett_parser::ast::ForStmt, + ) -> Result>, String> { + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return Ok(None); + }; + context + .checked + .prepare_field_reflected_loop(source) + .map_err(|error| error.to_string()) + } + + pub(in crate::interpreter) fn checked_field_scope_body( + &self, + original: &jett_parser::ast::ComptimeTypeBindStmt, + ) -> Result< + Option<( + CheckedBodyReference, + Arc, + jett_types::ReflectionTypeInfo, + )>, + String, + > { + let Some(context) = self + .field_update_context + .as_ref() + .filter(|context| context.active) + else { + return Ok(None); + }; + let needed = context + .checked + .field_scope_has_updates(original.span) + .map_err(|error| error.to_string())?; + // A genuine checked scope remains a checked caller even if its writes + // occur in a separately declared callee or a returned inline function. + let attempt = (|| { + let prepared = match &original.value { + jett_parser::ast::Expr::FieldAccess(base, member, _) + if member.name == "type_info" => + { + let jett_parser::ast::Expr::Ident(variable) = base.as_ref() else { + return Err( + "field update scope has no original reflected variable".to_string() + ); + }; + let definition = context + .checked + .program() + .resolved() + .resolutions + .get(&variable.span) + .ok_or("field update scope has no resolved reflected variable")?; + let iteration = self + .checked_reflected_fields + .iter() + .rev() + .find(|proof| proof.variable() == *definition) + .ok_or("field update scope has no original reflected iteration")?; + let current = self + .get_variable(&variable.name) + .ok_or("field update scope lost its live reflected variable")?; + iteration + .validate_value(current) + .map_err(|error| error.to_string())?; + context + .checked + .prepare_field_reflected_scope(original, iteration) + } + _ => context.checked.prepare_field_direct_scope(original), + } + .map_err(|error| error.to_string())?; + Ok::<_, String>(( + prepared.field_reference(), + prepared.projection(), + prepared.reflection().clone(), + )) + })(); + match attempt { + Ok(prepared) => Ok(Some(prepared)), + Err(error) if needed => Err(error), + Err(_) => Ok(None), + } + } + pub(in crate::interpreter) fn checked_field_test_body( + &self, + original: &jett_parser::ast::Block, + purpose: ExecutionPurpose, + ) -> Result, String> { + self.field_update_context + .as_ref() + .map(|context| { + context + .checked + .prepare_field_test_body(original, purpose) + .map_err(|error| error.to_string()) + }) + .transpose() + } + + pub(in crate::interpreter) fn with_checked_field_region( + &mut self, + reference: &CheckedBodyReference, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let saved = self.enter_checked_field_body(Some(reference))?; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self))); + let restore = self.restore_checked_field_body(saved); + match result { + Err(panic) => std::panic::resume_unwind(panic), + Ok(Err(error)) => Err(error), + Ok(Ok(value)) => restore.map(|()| value), + } + } + pub(in crate::interpreter) fn prepare_checked_field_function( + &self, + name: &str, + namespace: &Option, + current: &FunctionDef, + projection: Option<&crate::checked_types::CheckedFunctionTypes>, + arguments: &[ClosureTypeArgument], + checked_call: bool, + ) -> Result, String> { + let Some(context) = &self.field_update_context else { + return Ok(None); + }; + let program = context.checked.program(); + let checked = program.checked(); + let relevant = + |place: &CheckedAssignmentPlace| inside(place.assignment_span, current.body.span); + let needed = checked.assignment_places.values().any(relevant) + || scoped_place_matches(&checked.comptime_type_bindings, &relevant) + || checked + .generic_function_instantiations + .iter() + .any(|instance| { + instance.assignment_places.values().any(relevant) + || scoped_place_matches(&instance.comptime_type_bindings, &relevant) + }); + if !checked_call + || !(context.active || self.field_update_entry_allowed) + || !self.checked_expression_types.as_ref().is_some_and(|types| { + types + .resource_program + .as_ref() + .is_some_and(|installed| Arc::ptr_eq(installed, program)) + && crate::value::closure_capture::original_root_types(program, types) + }) + { + return if needed { + Err("field assignment requires its retained checked Source invocation".to_string()) + } else { + Ok(None) + }; + } + match context + .checked + .prepare_field_function_body(name, namespace, current, projection, arguments) + { + Ok(reference) => Ok(Some(reference)), + Err(error) if needed => Err(error.to_string()), + Err(_) => Ok(None), // Legacy bodies cannot pass field invocation authority onward. + } + } + + pub(in crate::interpreter) fn enter_checked_field_body( + &mut self, + reference: Option<&CheckedBodyReference>, + ) -> Result, String> { + self.field_update_entry_allowed = false; + self.field_update_context + .as_mut() + .map(|context| context.enter(reference)) + .transpose() + } + + pub(in crate::interpreter) fn restore_checked_field_body( + &mut self, + saved: Option, + ) -> Result<(), String> { + match (self.field_update_context.as_mut(), saved) { + (Some(context), Some(saved)) => context.restore(saved), + (None, None) => Ok(()), + _ => Err("ordinary field context lost its checked program".to_string()), + } + } + + pub(in crate::interpreter) fn install_checked_field_binding( + &mut self, + name: &Ident, + ) -> Result<(), String> { + let Some(context) = &mut self.field_update_context else { + return Ok(()); + }; + if !context.active + || !context + .checked + .field_binding_needed(name.span) + .map_err(|error| error.to_string())? + { + return Ok(()); + } + let checked = context + .checked + .prepare_field_binding(name) + .map_err(|error| error.to_string())?; + let generation = context.fresh_generation()?; + let fact = checked.fact(); + let scope = context + .slots + .last_mut() + .ok_or("ordinary field storage has no lexical scope")?; + scope.retain(|_, binding| binding.name != name.name); + scope.insert( + fact.definition, + InstalledFieldBinding { + checked, + name: name.name.clone(), + generation, + }, + ); + Ok(()) + } + + fn checked_field_plan( + &self, + place: &CheckedAssignmentPlace, + root: &Value, + ) -> Result { + let context = self + .field_update_context + .as_ref() + .ok_or("field assignment has no checked context")?; + let types = &context.checked.program().checked().interner; + let mut current = root; + let mut projections = Vec::new(); + projections + .try_reserve_exact(place.projections.len()) + .map_err(|_| "ordinary field path allocation failed")?; + for (index, checked) in place.projections.iter().enumerate() { + let original_owner = current; + let nominal = match types.resolve(checked.owner_type) { + Type::Secret(inner) => *inner, + _ => checked.owner_type, + }; + let canonical = types.type_name(nominal); + let mut wrappers = Vec::new(); + while let Value::Typed { type_name, value } = current { + let base = self.primitive_base_type_name(type_name); + if self.type_name_has_refinement(type_name) + || base + .split_once(" at ") + .map_or(base.as_ref(), |(owner, _)| owner) + != canonical + .split_once(" at ") + .map_or(canonical.as_str(), |(owner, _)| owner) + { + return Err( + "field assignment retained an incompatible owner wrapper".to_string() + ); + } + wrappers.push(type_name.clone()); + current = value; + } + if matches!(current, Value::Pending(_)) { + let ty = Self::simple_type_expr_from_name( + &types.type_name(checked.owner_type), + checked.source_span, + ) + .ok_or("checked field owner has no canonical interpreter type")?; + return Err(format!( + "field access is not supported on {}", + self.format_debug_value(original_owner, Some(&ty)) + )); + } + let owner = match checked.owner { + CheckedFieldOwner::Struct(id) => { + let definition = types.resolve_struct(id); + let name = definition + .name + .split('[') + .next() + .unwrap_or(&definition.name) + .to_string(); + StorageOwner::Struct { + type_name: name, + concrete_type: (!types.nominal_type_arguments(nominal).is_empty()) + .then_some(canonical), + field_names: definition + .fields + .iter() + .map(|(name, _)| name.clone()) + .collect(), + } + } + CheckedFieldOwner::MachineState { machine, state } => { + let definition = types.resolve_machine(machine); + let state = definition + .state(state) + .ok_or("field assignment lost its exact machine state")?; + StorageOwner::MachineState { + type_name: definition.name.clone(), + state: state.name.clone(), + field_count: state.fields.len(), + } + } + }; + let projection = StorageProjection { + wrappers, + owner, + field_index: checked.field_index, + }; + // Select from the original wrapper-bearing ancestor, not the + // temporary inner reference used to validate retained wrapper labels. + current = projection + .selected(original_owner, index) + .map_err(|_| "field assignment has incompatible ordinary storage")?; + projections.push(projection); + } + let plan = FieldStoragePlan { projections }; + plan.preflight(root) + .map_err(|_| "field assignment has incompatible ordinary storage")?; + Ok(plan) + } + + pub(in crate::interpreter) fn exec_checked_field_assignment( + &mut self, + source: &AssignStmt, + ) -> Result, String> { + if let Some(transport) = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + { + let resource = transport + .checked + .prepare_field_assignment(source) + .map_err(|error| error.to_string())?; + if resource.place().resource == CheckedFieldUpdateResourceRequirement::RequiresCustody { + return self.exec_resource_assignment(source); + } + } + let context = self + .field_update_context + .as_ref() + .filter(|context| context.active) + .ok_or("field assignment has no exact checked executable body")?; + let prepared = context + .checked + .prepare_field_assignment(source) + .map_err(|error| error.to_string())?; + if prepared.place().resource == CheckedFieldUpdateResourceRequirement::RequiresCustody { + // Keep the existing authenticated projected-Resource refusal. Do + // not classify a scalar leaf as permission to bypass root custody. + return self.exec_resource_assignment(source); + } + let (scope, slot) = context.slot(&prepared, self.lexical_scope_floor)?; + let mut ticket = FieldSlotTicket { + scope, + fact: slot.checked.fact(), + name: slot.name.clone(), + generation: slot.generation, + commit_generation: 0, + }; + let root = self + .scopes + .get(scope) + .and_then(|scope| scope.get(&ticket.name)) + .ok_or("field assignment lost its declared physical root")?; + let plan = self.checked_field_plan(prepared.place(), root)?; + ticket.commit_generation = self + .field_update_context + .as_mut() + .ok_or("field assignment lost its checked context")? + .fresh_generation()?; + let value = match self.eval_expr_flow(&source.value)? { + ExprFlow::Value(value) => value, + ExprFlow::Resource(_) => { + return Err( + "Resource operand has no checked ownership transport for this operation" + .to_string(), + ); + } + // An early handler return abandons the assignment. The returned + // root is not committed or retired a second time by this adapter. + ExprFlow::Signal(signal) => return Ok(Some(signal)), + }; + let field = prepared + .place() + .projections + .last() + .ok_or("checked field assignment has no projection")?; + let context = self + .field_update_context + .as_ref() + .ok_or("field assignment lost its checked context")?; + let name = context + .checked + .program() + .checked() + .interner + .type_name(field.field_type); + let ty = Self::simple_type_expr_from_name(&name, field.source_span) + .ok_or("checked field destination has no canonical interpreter type")?; + let source_type = self + .checked_refinement_source_type(source.value.span()) + .cloned(); + let value = + self.normalize_and_validate_value_from_source(&ty, value, source_type.as_deref())?; + let context = self + .field_update_context + .as_ref() + .ok_or("field assignment lost its checked context")?; + context + .checked + .revalidate_field_assignment(&prepared) + .map_err(|error| error.to_string())?; + let (scope, slot) = context.slot(&prepared, self.lexical_scope_floor)?; + if scope != ticket.scope + || slot.generation != ticket.generation + || slot.checked.fact() != ticket.fact + { + return Err("field assignment root changed while evaluating its RHS".to_string()); + } + let root = self + .scopes + .get(scope) + .and_then(|scope| scope.get(&ticket.name)) + .ok_or("field assignment lost its declared physical root")?; + // This repeated typed preflight is still before displacement. It also + // shares the established typed direct-field pending error policy. + self.checked_field_plan(prepared.place(), root)?; + let context = self + .field_update_context + .as_mut() + .ok_or("field assignment lost its checked context")?; + let slot = context + .slots + .get_mut(ticket.scope) + .and_then(|scope| scope.get_mut(&ticket.fact.definition)) + .ok_or("field assignment lost its declared storage installation")?; + let root = self + .scopes + .get_mut(ticket.scope) + .and_then(|scope| scope.get_mut(&ticket.name)) + .ok_or("field assignment lost its declared physical root")?; + let displaced = plan + .replace(root, StagedOrdinaryReplacement { value }) + .map_err(|_| "field assignment lost its prepared ordinary storage")?; + slot.generation = ticket.commit_generation; + drop(displaced); + Ok(None) + } +} + +impl Interpreter { + pub(in crate::interpreter) fn install_checked_actor_binding( + &mut self, + handler: &crate::resource_execution::PreparedActorHandler, + role: jett_typecheck::CheckedActorBindingRole, + ) -> Result<(), String> { + let context = self + .field_update_context + .as_mut() + .filter(|context| context.active) + .ok_or("actor binding has no checked handler context")?; + let checked = context + .checked + .prepare_actor_binding(handler, role) + .map_err(|error| error.to_string())?; + let name = handler.role_name(role).map_err(|error| error.to_string())?; + let generation = context.fresh_generation()?; + let fact = checked.fact(); + let scope = context + .slots + .last_mut() + .ok_or("actor binding has no physical scope")?; + if scope.values().any(|binding| binding.name == name.name) { + return Err("actor captured binding repeated a declaration in one scope".to_string()); + } + scope.insert( + fact.definition, + InstalledFieldBinding { + checked, + name: name.name.clone(), + generation, + }, + ); + Ok(()) + } +} diff --git a/crates/jett_comptime/src/interpreter/field_updates/tests.rs b/crates/jett_comptime/src/interpreter/field_updates/tests.rs new file mode 100644 index 00000000..e785d3a8 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/field_updates/tests.rs @@ -0,0 +1,243 @@ +//! Host storage controls do not establish Source admission, +//! checked-place authenticity, evaluator order, ownership proof, or cleanup. +use super::*; + +fn inner(value: Value) -> Value { + Value::Struct { + type_name: "app.Inner".into(), + concrete_type: None, + fields: vec![("value".into(), value)], + } +} + +fn record(value: Value) -> Value { + Value::Struct { + type_name: "app.Record".into(), + concrete_type: None, + fields: vec![ + ("inner".into(), value), + ("label".into(), Value::String("before".into())), + ], + } +} + +fn struct_projection(type_name: &str, fields: &[&str], field_index: usize) -> StorageProjection { + StorageProjection { + wrappers: Vec::new(), + owner: StorageOwner::Struct { + type_name: type_name.into(), + concrete_type: None, + field_names: fields.iter().map(|name| (*name).into()).collect(), + }, + field_index, + } +} + +fn nested_plan() -> FieldStoragePlan { + FieldStoragePlan { + projections: vec![ + struct_projection("app.Record", &["inner", "label"], 0), + struct_projection("app.Inner", &["value"], 0), + ], + } +} + +fn staged(value: Value) -> StagedOrdinaryReplacement { + StagedOrdinaryReplacement { value } +} + +#[test] +fn nested_replacement_returns_displaced_owner_and_preserves_siblings() { + let mut root = record(inner(Value::String("old".into()))); + let displaced = nested_plan() + .replace(&mut root, staged(Value::String("new".into()))) + .unwrap(); + assert_eq!(displaced, Value::String("old".into())); + assert_eq!(root, record(inner(Value::String("new".into())))); +} + +#[test] +fn independently_owned_clone_is_unchanged_by_replacement() { + let original = record(inner(Value::List(vec![Value::Int64(1)]))); + let mut copied = original.clone(); + let displaced = nested_plan() + .replace(&mut copied, staged(Value::List(vec![Value::Int64(9)]))) + .unwrap(); + assert_eq!(original, record(inner(Value::List(vec![Value::Int64(1)])))); + assert_eq!(copied, record(inner(Value::List(vec![Value::Int64(9)])))); + assert_eq!(displaced, Value::List(vec![Value::Int64(1)])); +} + +#[test] +fn pending_root_and_intermediate_are_rejected_without_payload_observation() { + let pending_root = Value::Pending(Box::new(record(inner(Value::Int64(1))))); + assert_eq!( + nested_plan().preflight(&pending_root), + Err(FieldStorageError::PendingAncestor { projection: 0 }) + ); + let mut pending_inner = record(Value::Pending(Box::new(inner(Value::Int64(1))))); + let before = pending_inner.clone(); + let failure = nested_plan() + .replace(&mut pending_inner, staged(Value::Int64(9))) + .unwrap_err(); + assert_eq!( + failure.error, + FieldStorageError::PendingAncestor { projection: 1 } + ); + assert_eq!(failure.replacement.value, Value::Int64(9)); + assert_eq!(pending_inner, before); +} + +#[test] +fn replacing_child_transports_exact_pending_depth() { + let mut root = record(inner(Value::Int64(1))); + let pending = Value::Pending(Box::new(Value::Pending(Box::new(Value::Int64(7))))); + let displaced = nested_plan().replace(&mut root, staged(pending)).unwrap(); + assert_eq!(displaced, Value::Int64(1)); + let Value::Struct { fields, .. } = root else { + panic!("record") + }; + let Value::Struct { fields, .. } = &fields[0].1 else { + panic!("inner") + }; + let Value::Pending(once) = &fields[0].1 else { + panic!("first pending") + }; + let Value::Pending(ready) = once.as_ref() else { + panic!("second pending") + }; + assert_eq!(ready.as_ref(), &Value::Int64(7)); +} + +#[test] +fn untouched_pending_child_keeps_depth_and_storage_metadata() { + let pending = Value::Pending(Box::new(Value::Pending(Box::new(inner(Value::Int64(4)))))); + let mut root = record(pending.clone()); + let plan = FieldStoragePlan { + projections: vec![struct_projection("app.Record", &["inner", "label"], 1)], + }; + plan.replace(&mut root, staged(Value::String("after".into()))) + .unwrap(); + assert_eq!( + root, + Value::Struct { + type_name: "app.Record".into(), + concrete_type: None, + fields: vec![ + ("inner".into(), pending), + ("label".into(), Value::String("after".into())) + ], + } + ); +} + +#[test] +fn exact_typed_wrappers_and_generic_identity_are_preserved() { + let root_value = Value::Struct { + type_name: "app.Boxed".into(), + concrete_type: Some("app.Boxed[int64]".into()), + fields: vec![("value".into(), Value::Int64(1))], + }; + let mut root = Value::Typed { + type_name: "secret[app.Boxed[int64]]".into(), + value: Box::new(root_value), + }; + let plan = FieldStoragePlan { + projections: vec![StorageProjection { + wrappers: vec!["secret[app.Boxed[int64]]".into()], + owner: StorageOwner::Struct { + type_name: "app.Boxed".into(), + concrete_type: Some("app.Boxed[int64]".into()), + field_names: vec!["value".into()], + }, + field_index: 0, + }], + }; + plan.replace(&mut root, staged(Value::Int64(8))).unwrap(); + let Value::Typed { type_name, value } = root else { + panic!("wrapper") + }; + assert_eq!(type_name, "secret[app.Boxed[int64]]"); + let Value::Struct { + concrete_type, + fields, + .. + } = *value + else { + panic!("record") + }; + assert_eq!(concrete_type.as_deref(), Some("app.Boxed[int64]")); + assert_eq!(fields[0].1, Value::Int64(8)); +} + +#[test] +fn exact_machine_state_is_checked_and_unchanged_by_nested_update() { + let mut root = Value::Machine { + type_name: "app.Session".into(), + state: "open".into(), + fields: vec![inner(Value::Int64(1))], + }; + let plan = FieldStoragePlan { + projections: vec![ + StorageProjection { + wrappers: Vec::new(), + owner: StorageOwner::MachineState { + type_name: "app.Session".into(), + state: "open".into(), + field_count: 1, + }, + field_index: 0, + }, + struct_projection("app.Inner", &["value"], 0), + ], + }; + plan.replace(&mut root, staged(Value::Int64(2))).unwrap(); + assert_eq!( + root, + Value::Machine { + type_name: "app.Session".into(), + state: "open".into(), + fields: vec![inner(Value::Int64(2))], + } + ); + let Value::Machine { state, .. } = &mut root else { + panic!("machine") + }; + *state = "closed".into(); + let before = root.clone(); + let failure = plan + .replace(&mut root, staged(Value::Int64(3))) + .unwrap_err(); + assert_eq!( + failure.error, + FieldStorageError::OwnerMismatch { projection: 0 } + ); + assert_eq!(root, before); +} + +#[test] +fn missing_path_mismatched_qualification_owner_or_field_leave_root_intact() { + let root = record(inner(Value::Int64(1))); + let empty = FieldStoragePlan { + projections: Vec::new(), + }; + assert_eq!(empty.preflight(&root), Err(FieldStorageError::EmptyPath)); + let mut wrong_owner = nested_plan(); + wrong_owner.projections[0].owner = StorageOwner::Struct { + type_name: "foreign.Record".into(), + concrete_type: None, + field_names: vec!["inner".into(), "label".into()], + }; + let mut wrong_wrapper = nested_plan(); + wrong_wrapper.projections[0].wrappers = vec!["app.Record".into()]; + let mut wrong_field = nested_plan(); + wrong_field.projections[0].field_index = 5; + for plan in [wrong_owner, wrong_wrapper, wrong_field] { + let mut target = root.clone(); + let failure = plan + .replace(&mut target, staged(Value::Int64(9))) + .unwrap_err(); + assert_eq!(failure.replacement.value, Value::Int64(9)); + assert_eq!(target, root); + } +} diff --git a/crates/jett_comptime/src/interpreter/generic_inference.rs b/crates/jett_comptime/src/interpreter/generic_inference.rs new file mode 100644 index 00000000..2fe34fb1 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/generic_inference.rs @@ -0,0 +1,87 @@ +//! Fill inferred bottom slots without discarding source-visible type aliases. +use super::{Expr, Interpreter, Param, TypeExpr}; + +impl Interpreter { + pub(super) fn type_witness_source(mut expression: &Expr) -> &Expr { + while let Expr::View(inner, _) | Expr::Paren(inner, _) = expression { + expression = inner; + } + expression + } + + pub(super) fn type_witness_parameter(param: &Param, ty: TypeExpr) -> TypeExpr { + if param.view && !matches!(ty, TypeExpr::View(_, _)) { + TypeExpr::View(Box::new(ty), param.span) + } else { + ty + } + } + + pub(super) fn merge_inferred_never( + &self, + first: &TypeExpr, + next: &TypeExpr, + contravariant: bool, + ) -> Option { + // Equal canonical types retain the first parameter's source spelling. + // Root alias peeling happens before inference, never recursively here. + if self.resolved_concrete_type_display(first) == self.resolved_concrete_type_display(next) { + return Some(first.clone()); + } + let first_never = is_never(first); + let next_never = is_never(next); + if first_never || next_never { + return Some(if first_never != contravariant { + next.clone() + } else { + first.clone() + }); + } + match (first, next) { + ( + TypeExpr::Generic(first_owner, first_args, span), + TypeExpr::Generic(next_owner, next_args, _), + ) if first_owner.name == next_owner.name + && first_args.len() == next_args.len() + && matches!( + first_owner.name.as_str(), + "list" | "set" | "optional" | "result" | "map" | "secret" + ) => + { + Some(TypeExpr::Generic( + first_owner.clone(), + first_args + .iter() + .zip(next_args) + .map(|(first, next)| self.merge_inferred_never(first, next, contravariant)) + .collect::>()?, + *span, + )) + } + ( + TypeExpr::Function(first_params, first_return, span), + TypeExpr::Function(next_params, next_return, _), + ) if first_params.len() == next_params.len() => Some(TypeExpr::Function( + first_params + .iter() + .zip(next_params) + .map(|(first, next)| self.merge_inferred_never(first, next, !contravariant)) + .collect::>()?, + Box::new(self.merge_inferred_never(first_return, next_return, contravariant)?), + *span, + )), + (TypeExpr::View(first, span), TypeExpr::View(next, _)) => Some(TypeExpr::View( + Box::new(self.merge_inferred_never(first, next, contravariant)?), + *span, + )), + _ => None, + } + } +} + +fn is_never(ty: &TypeExpr) -> bool { + matches!(ty, TypeExpr::Named(name) if name.name == "") +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_comptime/src/interpreter/generic_inference/tests.rs b/crates/jett_comptime/src/interpreter/generic_inference/tests.rs new file mode 100644 index 00000000..ed667abb --- /dev/null +++ b/crates/jett_comptime/src/interpreter/generic_inference/tests.rs @@ -0,0 +1,440 @@ +use super::*; +use crate::interpreter::{CallArg, Expr, FileId, Ident, Span, Value, type_expr_display}; + +fn span() -> Span { + Span::new(FileId::new(0), 0, 0) +} + +fn ident(name: &str) -> Ident { + Ident { + name: name.into(), + span: span(), + } +} + +fn named(name: &str) -> TypeExpr { + TypeExpr::Named(ident(name)) +} + +fn generic(name: &str, args: Vec) -> TypeExpr { + TypeExpr::Generic(ident(name), args, span()) +} + +fn interpreter(source: &str) -> Interpreter { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + interpreter +} + +fn transparent_wrappers(expression: Expr) -> Vec { + vec![ + expression.clone(), + Expr::Paren(Box::new(expression.clone()), span()), + Expr::View(Box::new(expression.clone()), span()), + Expr::Paren( + Box::new(Expr::View(Box::new(expression.clone()), span())), + span(), + ), + Expr::View( + Box::new(Expr::Paren(Box::new(expression.clone()), span())), + span(), + ), + Expr::Paren( + Box::new(Expr::View( + Box::new(Expr::Paren(Box::new(expression), span())), + span(), + )), + span(), + ), + ] +} + +fn infer_argument(interpreter: &Interpreter, callee: &str, expression: Expr) -> String { + let arguments = [CallArg { + name: None, + value: expression, + span: span(), + }]; + let inferred = interpreter + .inferred_user_function_type_args(&Expr::Ident(ident(callee)), &[], &arguments, None) + .expect("source type witness"); + assert_eq!(inferred.len(), 1); + type_expr_display(&inferred[0]) +} + +#[test] +fn transparent_argument_wrappers_preserve_root_and_nested_alias_inference() { + let mut interpreter = interpreter( + r#"type Count = int64 +function whole[T](view value: T) returns nothing: + return nothing +function payload[T](view value: optional[T]) returns nothing: + return nothing +"#, + ); + for (name, value, ty, callee, expected) in [ + ("count", Value::Int64(7), named("Count"), "whole", "int64"), + ( + "maybe", + Value::OptionalSome(Box::new(Value::Int64(7))), + generic("optional", vec![named("Count")]), + "payload", + "Count", + ), + ( + "values", + Value::List(vec![Value::Int64(7)]), + generic("list", vec![named("Count")]), + "whole", + "list[Count]", + ), + ] { + interpreter.set_variable_with_type(name, value, ty); + for expression in transparent_wrappers(Expr::Ident(ident(name))) { + assert_eq!(infer_argument(&interpreter, callee, expression), expected); + } + } +} + +#[test] +fn transparent_callback_wrappers_preserve_aliases_and_view_parameter_modes() { + let mut interpreter = interpreter( + r#"type Count = int64 +function whole[T](view value: T) returns nothing: + return nothing +function input[T](view callback: function(view T) returns T) returns nothing: + return nothing +function owned(value: Count) returns Count: + return value +function borrowed(view value: Count) returns Count: + return value +function double_view(view value: view Count) returns Count: + return value +"#, + ); + for (name, view, typed_view, expected) in [ + ("owned", false, false, "function(Count) returns Count"), + ( + "borrowed", + true, + false, + "function(view Count) returns Count", + ), + ( + "double_view", + true, + true, + "function(view Count) returns Count", + ), + ] { + let inline = Expr::InlineFn( + vec![Param { + view, + mutable: false, + name: ident("value"), + ty: if typed_view { + TypeExpr::View(Box::new(named("Count")), span()) + } else { + named("Count") + }, + span: span(), + }], + Some(named("Count")), + jett_parser::ast::Block { + stmts: Vec::new(), + span: span(), + }, + span(), + ); + for expression in transparent_wrappers(Expr::Ident(ident(name))) { + assert_eq!( + interpreter + .named_function_argument_type(&expression) + .as_ref() + .map(type_expr_display) + .as_deref(), + Some(expected) + ); + assert_eq!(infer_argument(&interpreter, "whole", expression), expected); + } + for expression in transparent_wrappers(inline) { + assert_eq!(infer_argument(&interpreter, "whole", expression), expected); + } + if view { + for expression in transparent_wrappers(Expr::Ident(ident(name))) { + assert_eq!(infer_argument(&interpreter, "input", expression), "Count"); + } + } + let signature = interpreter + .named_function_argument_type(&Expr::Ident(ident(name))) + .unwrap(); + interpreter.set_variable_with_type("stored", Value::NamedFunction(name.into()), signature); + for expression in transparent_wrappers(Expr::Ident(ident("stored"))) { + assert_eq!(infer_argument(&interpreter, "whole", expression), expected); + } + } +} + +#[test] +fn transparent_witness_lookup_keeps_outer_checked_fallback_and_other_forms_opaque() { + let mut interpreter = Interpreter::new(); + interpreter.set_variable_with_type("known", Value::Int64(7), named("Count")); + let inner_span = Span::new(FileId::new(0), 1, 2); + let outer_span = Span::new(FileId::new(0), 0, 3); + let expression = Expr::View( + Box::new(Expr::Ident(Ident { + name: "unknown".into(), + span: inner_span, + })), + outer_span, + ); + let mut checked = crate::checked_types::CheckedExpressionTypes::default(); + checked.expressions.insert(inner_span, "int8".into()); + checked.expressions.insert(outer_span, "int64".into()); + interpreter.checked_expression_types = Some(std::sync::Arc::new(checked)); + assert_eq!( + interpreter + .call_argument_type(&expression) + .as_ref() + .map(type_expr_display) + .as_deref(), + Some("int64") + ); + for expression in [ + Expr::Clone(Box::new(Expr::Ident(ident("known"))), span()), + Expr::FieldAccess( + Box::new(Expr::Ident(ident("known"))), + ident("field"), + span(), + ), + Expr::Call(Box::new(Expr::Ident(ident("known"))), Vec::new(), span()), + ] { + assert!(interpreter.call_argument_type(&expression).is_none()); + } +} + +#[test] +fn nested_generic_local_fills_missing_never_without_observing_pending_values() { + let mut interpreter = interpreter( + r#"function describe[T](first: T, last: T) returns string: + return type.name[T]() +function outer[T](empty: list[T], ready: list[int64]) returns string: + return describe(empty, ready) +function reversed[T](empty: list[T], ready: list[int64]) returns string: + return describe(ready, empty) +function retain[T](first: T, last: T) returns T: + return first +function pending[T](empty: list[T], ready: list[int64]) returns list[int64]: + return retain(empty, ready) +"#, + ); + for function in ["outer", "reversed"] { + assert_eq!( + interpreter.call_function_with_type_args( + function, + &[named("")], + vec![Value::List(Vec::new()), Value::List(vec![Value::Int64(7)])], + ), + Ok(Value::String("list[int64]".into())) + ); + } + let pending = Value::Pending(Box::new(Value::Pending(Box::new(Value::List(Vec::new()))))); + assert_eq!( + interpreter.call_function_with_type_args( + "pending", + &[named("")], + vec![pending.clone(), Value::List(vec![Value::Int64(7)])], + ), + Ok(pending) + ); +} + +#[test] +fn never_inference_keeps_first_concrete_alias_in_declaration_argument_order() { + let mut interpreter = interpreter( + r#"type Count = int64 +type Other = int64 +function describe[T](first: T, last: T) returns string: + return type.name[T]() +"#, + ); + interpreter.set_variable_with_type( + "count", + Value::ResultOk(Box::new(Value::Int64(7))), + generic("result", vec![named("Count"), named("")]), + ); + interpreter.set_variable_with_type( + "other", + Value::ResultFail(Box::new(Value::String("bad".into()))), + generic("result", vec![named("Other"), named("string")]), + ); + let call = |first: &str, last: &str| { + Expr::Call( + Box::new(Expr::Ident(ident("describe"))), + vec![ + CallArg { + name: Some(ident("last")), + value: Expr::Ident(ident(last)), + span: span(), + }, + CallArg { + name: Some(ident("first")), + value: Expr::Ident(ident(first)), + span: span(), + }, + ], + span(), + ) + }; + assert_eq!( + interpreter.eval_expr(&call("count", "other")), + Ok(Value::String("result[Count, string]".into())) + ); + assert_eq!( + interpreter.eval_expr(&call("other", "count")), + Ok(Value::String("result[Other, string]".into())) + ); +} + +#[test] +fn never_inference_preserves_root_alias_peeling_and_nested_callback_aliases() { + let interpreter = interpreter( + r#"type Count = int64 +type Other = int64 +function describe[T](first: T, last: T) returns nothing: + return nothing +"#, + ); + let inferred = |actual: &[TypeExpr]| { + interpreter + .inferred_user_function_type_args_from_types( + &Expr::Ident(ident("describe")), + &[], + actual, + ) + .unwrap() + .into_iter() + .map(|ty| type_expr_display(&ty)) + .collect::>() + }; + assert_eq!(inferred(&[named("Count"), named("Other")]), ["int64"]); + let callback = |result| TypeExpr::Function(Vec::new(), Box::new(result), span()); + assert_eq!( + inferred(&[ + callback(generic("optional", vec![named("")])), + callback(generic("optional", vec![named("Count")])), + ]), + ["function() returns optional[Count]"] + ); +} + +#[test] +fn never_inference_merges_matching_container_slots_in_both_orders() { + let interpreter = Interpreter::new(); + for owner in ["list", "set", "optional", "secret"] { + let absent = generic(owner, vec![named("")]); + let present = generic(owner, vec![named("int64")]); + for (first, next) in [(&absent, &present), (&present, &absent)] { + assert_eq!( + interpreter + .merge_inferred_never(first, next, false) + .as_ref() + .map(type_expr_display), + Some(type_expr_display(&present)) + ); + } + } + for owner in ["map", "result"] { + let first = generic(owner, vec![named(""), named("string")]); + let next = generic(owner, vec![named("int64"), named("")]); + let merged = generic(owner, vec![named("int64"), named("string")]); + for (first, next) in [(&first, &next), (&next, &first)] { + assert_eq!( + interpreter + .merge_inferred_never(first, next, false) + .as_ref() + .map(type_expr_display), + Some(type_expr_display(&merged)) + ); + } + } +} + +#[test] +fn never_inference_respects_callable_variance_and_view_parameter_modes() { + let interpreter = Interpreter::new(); + let callback = |input, output, view| { + TypeExpr::Function( + vec![if view { + TypeExpr::View(Box::new(input), span()) + } else { + input + }], + Box::new(output), + span(), + ) + }; + for view in [false, true] { + let first = callback( + generic("optional", vec![named("int64")]), + generic("list", vec![named("")]), + view, + ); + let next = callback( + generic("optional", vec![named("")]), + generic("list", vec![named("string")]), + view, + ); + let expected = callback( + generic("optional", vec![named("")]), + generic("list", vec![named("string")]), + view, + ); + for (first, next) in [(&first, &next), (&next, &first)] { + assert_eq!( + interpreter + .merge_inferred_never(first, next, false) + .as_ref() + .map(type_expr_display), + Some(type_expr_display(&expected)) + ); + } + } + assert!( + interpreter + .merge_inferred_never( + &callback(named("int64"), named("nothing"), false), + &callback(named("int64"), named("nothing"), true), + false, + ) + .is_none() + ); +} + +#[test] +fn never_inference_does_not_promote_known_leaves_or_partially_merge_conflicts() { + let interpreter = Interpreter::new(); + for (first, next) in [ + (named("int8"), named("int64")), + ( + generic("result", vec![named(""), named("int8")]), + generic("result", vec![named("string"), named("int64")]), + ), + ( + generic("Record", vec![named("")]), + generic("Record", vec![named("int64")]), + ), + ( + generic("list", vec![named("")]), + generic("optional", vec![named("int64")]), + ), + ] { + assert!( + interpreter + .merge_inferred_never(&first, &next, false) + .is_none() + ); + } +} diff --git a/crates/jett_comptime/src/interpreter/graphics.rs b/crates/jett_comptime/src/interpreter/graphics.rs index 8e9b31ff..637773a5 100644 --- a/crates/jett_comptime/src/interpreter/graphics.rs +++ b/crates/jett_comptime/src/interpreter/graphics.rs @@ -5,12 +5,7 @@ use jett_runtime::graphics::{self, Color, Config, Key, Rect, Scene, Text}; use super::{Interpreter, Value}; /// Input supplied by a deterministic graphics provider. It never opens a window. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum GraphicsTestEvent { - Key(Key), - Close, - HostError(String), -} +pub use jett_runtime::graphics::TestEvent as GraphicsTestEvent; /// Observable graphics operations recorded by a deterministic provider. #[derive(Debug, Clone, PartialEq, Eq)] @@ -229,7 +224,12 @@ fn key_value(key: Key) -> Value { } fn field<'a>(value: &'a Value, expected_type: &str, name: &str) -> Result<&'a Value, String> { - let Value::Struct { type_name, fields } = value else { + let Value::Struct { + concrete_type: _, + type_name, + fields, + } = value + else { return Err(format!("graphics: expected {expected_type}")); }; if type_name != expected_type { diff --git a/crates/jett_comptime/src/interpreter/interface_arguments.rs b/crates/jett_comptime/src/interpreter/interface_arguments.rs new file mode 100644 index 00000000..9cb265a5 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/interface_arguments.rs @@ -0,0 +1,195 @@ +//! Read-only owner preflight before erased dispatch changes argument identities. +use super::{FunctionDef, HashMap, HashSet, Interpreter, TypeExpr, Value, runtime_type_name}; + +const OWNER_MISMATCH: &str = "interface dispatch concrete argument owner mismatch"; + +impl Interpreter { + pub(super) fn validate_interface_argument_owners( + &self, + _interface: &str, + function: &FunctionDef, + namespace: Option<&str>, + type_scope: &HashMap, + arguments: &[Value], + ) -> Result<(), String> { + // Dispatch already established the ready receiver's concrete identity. + let owner = arguments + .first() + .and_then(runtime_type_name) + .ok_or(OWNER_MISMATCH)?; + for (parameter, argument) in function.params.iter().zip(arguments) { + let written = self.interface_parameter_type(¶meter.ty, type_scope, namespace); + let expected = self.concrete_resolved_type_expr(&written, &mut HashSet::new()); + self.validate_interface_owner_occurrence(&expected, &owner, argument)?; + } + Ok(()) + } + + fn interface_parameter_type( + &self, + written: &TypeExpr, + type_scope: &HashMap, + namespace: Option<&str>, + ) -> TypeExpr { + match written { + TypeExpr::Named(name) => type_scope.get(&name.name).cloned().unwrap_or_else(|| { + TypeExpr::Named(self.qualify_declared_type_ident(name, namespace)) + }), + TypeExpr::Generic(name, children, span) => TypeExpr::Generic( + self.qualify_declared_type_ident(name, namespace), + children + .iter() + .map(|child| self.interface_parameter_type(child, type_scope, namespace)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.interface_parameter_type(inner, type_scope, namespace)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.interface_parameter_type(inner, type_scope, namespace)), + state.clone(), + *span, + ), + TypeExpr::Function(parameters, output, span) => TypeExpr::Function( + parameters + .iter() + .map(|parameter| { + self.interface_parameter_type(parameter, type_scope, namespace) + }) + .collect(), + Box::new(self.interface_parameter_type(output, type_scope, namespace)), + *span, + ), + } + } + + fn interface_type_contains_owner(&self, expected: &TypeExpr, owner: &str) -> bool { + if self.resolved_concrete_type_display(expected) == owner { + return true; + } + match expected { + TypeExpr::Named(_) => false, + TypeExpr::Generic(_, children, _) => children + .iter() + .any(|child| self.interface_type_contains_owner(child, owner)), + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + self.interface_type_contains_owner(inner, owner) + } + // A checked callable adapter validates its arguments when invoked; + // the callable itself is not an occurrence of its argument values. + TypeExpr::Function(_, _, _) => false, + } + } + + fn validate_interface_owner_occurrence( + &self, + expected: &TypeExpr, + owner: &str, + argument: &Value, + ) -> Result<(), String> { + if let TypeExpr::View(inner, _) = expected { + return self.validate_interface_owner_occurrence(inner, owner, argument); + } + let expected_name = self.resolved_concrete_type_display(expected); + if expected_name == owner { + // Reading retained identity does not join a task or inspect fields. + return if retained_argument_owner(argument).as_deref() == Some(owner) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + }; + } + if !self.interface_type_contains_owner(expected, owner) { + return Ok(()); + } + if let Value::Pending(_) = argument { + // An unavailable container cannot supply new child-owner facts. + return if retained_argument_owner(argument).as_deref() == Some(expected_name.as_str()) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + }; + } + match (expected, argument.payload()) { + (TypeExpr::Generic(name, children, _), value) + if matches!( + name.name.as_str(), + "secret" | "list" | "set" | "optional" | "result" | "map" + ) => + { + self.validate_interface_owner_children(&name.name, children, owner, value) + } + (TypeExpr::StateQualified(inner, _, _), value) => { + self.validate_interface_owner_occurrence(inner, owner, value) + } + // Nominal generic records are invariant. Their retained identity + // proves all arguments without reinterpreting or inspecting fields. + _ if retained_argument_owner(argument).as_deref() == Some(expected_name.as_str()) => { + Ok(()) + } + _ => Err(OWNER_MISMATCH.into()), + } + } + + fn validate_interface_owner_children( + &self, + constructor: &str, + children: &[TypeExpr], + owner: &str, + value: &Value, + ) -> Result<(), String> { + match (constructor, children, value) { + ("secret", [inner], value) => { + self.validate_interface_owner_occurrence(inner, owner, value) + } + ("list", [element], Value::List(elements)) + | ("set", [element], Value::Set(elements)) => { + for value in elements { + self.validate_interface_owner_occurrence(element, owner, value)?; + } + Ok(()) + } + ("optional", [element], Value::OptionalSome(value)) + | ("result", [element, _], Value::ResultOk(value)) + | ("result", [_, element], Value::ResultFail(value)) => { + self.validate_interface_owner_occurrence(element, owner, value) + } + ("optional", [_], Value::OptionalNone) => Ok(()), + ("map", [key_type, value_type], Value::Map(entries)) => { + for (key, value) in entries { + self.validate_interface_owner_occurrence(key_type, owner, key)?; + self.validate_interface_owner_occurrence(value_type, owner, value)?; + } + Ok(()) + } + _ => { + let expected = format!( + "{}[{}]", + constructor, + children + .iter() + .map(|child| self.resolved_concrete_type_display(child)) + .collect::>() + .join(", ") + ); + if retained_argument_owner(value).as_deref() == Some(expected.as_str()) { + Ok(()) + } else { + Err(OWNER_MISMATCH.into()) + } + } + } + } +} + +fn retained_argument_owner(value: &Value) -> Option { + match value { + Value::Pending(inner) => retained_argument_owner(inner), + value => runtime_type_name(value), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs b/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs new file mode 100644 index 00000000..4b7ae539 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/interface_arguments/tests.rs @@ -0,0 +1,290 @@ +use super::*; +use crate::debug::render_debug_events; +use jett_common::FileId; + +const ORIGINAL_WRONG: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/05_erased_other_owner.jett" +)); +const ORIGINAL_MATCHING: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/12_erased_matching_owner.jett" +)); + +fn interpreter(source: &str) -> Interpreter { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut interpreter = Interpreter::new(); + interpreter.register_module(&parsed.module); + interpreter +} + +fn named(name: &str) -> TypeExpr { + TypeExpr::Named(super::super::Ident { + name: name.into(), + span: super::super::Span::new(FileId::new(0), 0, 0), + }) +} + +fn generic(name: &str, arguments: Vec) -> TypeExpr { + TypeExpr::Generic( + super::super::Ident { + name: name.into(), + span: super::super::Span::new(FileId::new(0), 0, 0), + }, + arguments, + super::super::Span::new(FileId::new(0), 0, 0), + ) +} + +fn record(owner: &str) -> Value { + Value::Struct { + type_name: owner.into(), + concrete_type: None, + fields: vec![("name".into(), Value::String("label".into()))], + } +} + +#[test] +fn erased_owner_original_wrong_source_fails_before_result_print() { + let mut interpreter = interpreter(ORIGINAL_WRONG); + assert_eq!( + interpreter.call_function("app.main", vec![]), + Err(OWNER_MISMATCH.into()) + ); + assert!(interpreter.take_debug_events().is_empty()); + assert_eq!(interpreter.lexical_scope_floor, 0); + assert_eq!(interpreter.current_namespace, None); +} + +#[test] +fn erased_owner_original_matching_source_keeps_views_and_result_print() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + assert_eq!( + interpreter.call_function("app.main", vec![]), + Ok(Value::Nothing) + ); + assert_eq!( + render_debug_events(&interpreter.take_debug_events()), + "left:one:two\n" + ); +} + +#[test] +fn erased_owner_direct_and_named_callback_reject_before_body_entry() { + let source = ORIGINAL_WRONG.replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ); + for callback in [false, true] { + let mut interpreter = interpreter(&source); + let args = vec![record("app.Left"), record("app.Right")]; + let outcome = if callback { + interpreter.call_fn_value(Value::NamedFunction("app.Pairable.combine".into()), args) + } else { + interpreter.call_function("app.Pairable.combine", args) + }; + assert_eq!(outcome, Err(OWNER_MISMATCH.into())); + assert!(interpreter.take_debug_events().is_empty()); + assert_eq!(interpreter.current_namespace, None); + assert_eq!(interpreter.lexical_scope_floor, 0); + } +} + +#[test] +fn erased_owner_owned_formals_validate_before_installing_any_formal() { + let source = ORIGINAL_WRONG + .replace("view self:", "self:") + .replace("view other:", "other:"); + let mut interpreter = interpreter(&source); + let scopes = interpreter.scopes.len(); + assert_eq!( + interpreter.call_function( + "app.Pairable.combine", + vec![record("app.Left"), record("app.Right")] + ), + Err(OWNER_MISMATCH.into()) + ); + assert_eq!(interpreter.scopes.len(), scopes); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn erased_owner_primitive_widths_and_nominal_refinements_are_exact() { + let interpreter = interpreter(ORIGINAL_MATCHING); + for owner in ["int8", "app.Positive"] { + let wrong = Value::Int64(7); + let right = Value::Typed { + type_name: owner.into(), + value: Box::new(wrong.clone()), + }; + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &right), + Ok(()) + ); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); + assert_eq!(runtime_type_name(&right).as_deref(), Some(owner)); + } +} + +#[test] +fn erased_owner_generic_record_arguments_keep_full_nominal_identity() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Box[int64]"; + let mut actual = record("app.Box"); + if let Value::Struct { concrete_type, .. } = &mut actual { + *concrete_type = Some(owner.into()); + } + let expected = generic("app.Box", vec![named("int64")]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Ok(()) + ); + if let Value::Struct { concrete_type, .. } = &mut actual { + *concrete_type = Some("app.Box[string]".into()); + } + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_nested_occupied_payloads_require_matching_owners() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let expected = generic("list", vec![generic("optional", vec![named(owner)])]); + for actual in [ + Value::List(vec![Value::OptionalSome(Box::new(record(owner)))]), + Value::List(vec![Value::OptionalNone]), + Value::List(vec![]), + ] { + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &actual), + Ok(()) + ); + } + let wrong = Value::List(vec![Value::OptionalSome(Box::new(record("app.Right")))]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_map_values_and_active_result_arms_are_checked() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let expected = generic( + "map", + vec![ + named("string"), + generic("result", vec![named(owner), named("string")]), + ], + ); + let unchanged_error = Value::Map(vec![( + Value::String("key".into()), + Value::ResultFail(Box::new(Value::String("earlier".into()))), + )]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &unchanged_error), + Ok(()) + ); + let wrong = Value::Map(vec![( + Value::String("key".into()), + Value::ResultOk(Box::new(record("app.Right"))), + )]); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&expected, owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); +} + +#[test] +fn erased_owner_pending_receiver_keeps_canonical_dispatch_failure() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + let pending = Value::Pending(Box::new(record("app.Left"))); + let typed_pending = Value::Typed { + type_name: "app.Left".into(), + value: Box::new(pending.clone()), + }; + for receiver in [pending, typed_pending] { + for callback in [false, true] { + let args = vec![receiver.clone(), record("app.Right")]; + let actual = if callback { + interpreter.call_fn_value(Value::NamedFunction("app.Pairable.combine".into()), args) + } else { + interpreter.call_function("app.Pairable.combine", args) + }; + assert_eq!( + actual, + Err("undefined function 'app.Pairable.combine'".into()) + ); + } + } + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn erased_owner_pending_argument_uses_identity_without_joining_payload() { + let interpreter = interpreter(ORIGINAL_MATCHING); + let owner = "app.Left"; + let matching = Value::Pending(Box::new(record(owner))); + let wrong = Value::Pending(Box::new(record("app.Right"))); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &matching), + Ok(()) + ); + assert_eq!( + interpreter.validate_interface_owner_occurrence(&named(owner), owner, &wrong), + Err(OWNER_MISMATCH.into()) + ); + assert!(matches!(matching, Value::Pending(_))); + assert!(matches!(wrong, Value::Pending(_))); +} + +#[test] +fn erased_owner_preflight_does_not_expand_caller_namespace_aliases() { + let mut interpreter = interpreter(ORIGINAL_MATCHING); + let parsed = jett_parser::parse( + "namespace fake\nstruct Left:\n name: string\n", + FileId::new(1), + ); + assert!(parsed.errors.is_empty()); + interpreter.register_module(&parsed.module); + interpreter + .namespace_alias_scopes + .last_mut() + .unwrap() + .insert("app".into(), "fake".into()); + let declared = named("app.Left"); + assert_eq!( + interpreter.resolved_concrete_type_display(&interpreter.interface_parameter_type( + &declared, + &HashMap::new(), + Some("app") + )), + "app.Left" + ); + let declared = named("Left"); + assert_eq!( + interpreter.resolved_concrete_type_display(&interpreter.interface_parameter_type( + &declared, + &HashMap::new(), + Some("app") + )), + "app.Left" + ); + assert_eq!( + interpreter + .namespace_alias_scopes + .last() + .unwrap() + .get("app") + .map(String::as_str), + Some("fake") + ); +} diff --git a/crates/jett_comptime/src/interpreter/nominal_types.rs b/crates/jett_comptime/src/interpreter/nominal_types.rs new file mode 100644 index 00000000..6cdafde9 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/nominal_types.rs @@ -0,0 +1,71 @@ +//! Concrete type metadata that survives storing a nominal value as an interface. +use super::{HashSet, Interpreter, TypeExpr, type_expr_display}; + +impl Interpreter { + pub(crate) fn concrete_type_display(&self, ty: &TypeExpr) -> String { + type_expr_display(&self.concrete_type_expr(ty, &mut HashSet::new())) + } + + pub(super) fn resolved_concrete_type_display(&self, ty: &TypeExpr) -> String { + type_expr_display(&self.concrete_resolved_type_expr(ty, &mut HashSet::new())) + } + + pub(super) fn concrete_type_expr( + &self, + ty: &TypeExpr, + aliases: &mut HashSet, + ) -> TypeExpr { + let ty = self.substitute_type_expr(ty); + self.concrete_resolved_type_expr(&ty, aliases) + } + + pub(super) fn concrete_resolved_type_expr( + &self, + ty: &TypeExpr, + aliases: &mut HashSet, + ) -> TypeExpr { + match ty { + TypeExpr::Named(ident) if matches!(self.type_aliases.get(&ident.name), Some(None)) => { + // Transparent aliases normalize to their checked target, while + // refinements retain their nominal identity. Scope the cycle + // guard to this path so repeated arguments expand independently. + if !aliases.insert(ident.name.clone()) { + return ty.clone(); + } + let result = self.type_alias_bases.get(&ident.name).map(|base| { + let namespace = Self::type_name_namespace(&ident.name) + .or(self.current_namespace.as_deref()); + let base = self.qualify_declared_type_expr(base, namespace); + self.concrete_resolved_type_expr(&base, aliases) + }); + aliases.remove(&ident.name); + result.unwrap_or_else(|| ty.clone()) + } + TypeExpr::Generic(ident, args, span) => TypeExpr::Generic( + ident.clone(), + args.iter() + .map(|arg| self.concrete_resolved_type_expr(arg, aliases)) + .collect(), + *span, + ), + TypeExpr::View(inner, span) => TypeExpr::View( + Box::new(self.concrete_resolved_type_expr(inner, aliases)), + *span, + ), + TypeExpr::StateQualified(inner, state, span) => TypeExpr::StateQualified( + Box::new(self.concrete_resolved_type_expr(inner, aliases)), + state.clone(), + *span, + ), + TypeExpr::Function(params, result, span) => TypeExpr::Function( + params + .iter() + .map(|param| self.concrete_resolved_type_expr(param, aliases)) + .collect(), + Box::new(self.concrete_resolved_type_expr(result, aliases)), + *span, + ), + _ => ty.clone(), + } + } +} diff --git a/crates/jett_comptime/src/interpreter/reflected_callables.rs b/crates/jett_comptime/src/interpreter/reflected_callables.rs new file mode 100644 index 00000000..3e9db29b --- /dev/null +++ b/crates/jett_comptime/src/interpreter/reflected_callables.rs @@ -0,0 +1,230 @@ +//! A checked reflected read owns an adapter; its future result owns the check. +use super::*; +use crate::value::ReflectedCallableValue; + +#[cfg(test)] +mod tests; + +impl Interpreter { + fn authenticate_current_reflected_source( + &self, + value: &Value, + program: &Arc, + actual: jett_types::TypeId, + ) -> Result<(), String> { + if !self + .checked_expression_types + .as_ref() + .is_some_and(|installed| { + installed + .resource_program + .as_ref() + .is_some_and(|current| Arc::ptr_eq(current, program)) + && crate::value::closure_capture::original_root_types(program, installed) + }) + { + return Err( + "reflected callback checked expression context changed its original source".into(), + ); + } + ReflectedCallableValue::authenticate_source(value, program, actual)?; + match value.payload() { + Value::Pending(inner) => { + self.authenticate_current_reflected_source(inner, program, actual) + } + Value::NamedFunction(name) => { + let current = self + .functions + .get(name) + .ok_or("reflected callback original function is not registered")?; + if crate::value::closure_capture::OriginalClosureOwner::registered( + program, + name, + ¤t.definition, + ¤t.namespace, + ) + .is_none() + { + return Err( + "reflected callback current function changed its original source".into(), + ); + } + Ok(()) + } + Value::ReflectedCallable(admitted) => self.authenticate_current_reflected_source( + admitted.source(), + program, + admitted.source_type(), + ), + Value::Function { .. } => Ok(()), + _ => Err("reflected callback source is not a callable descriptor".into()), + } + } + + pub(super) fn has_checked_reflected_callable_plan( + &self, + actual_type_name: &str, + requested: &TypeExpr, + ) -> bool { + if !matches!(requested, TypeExpr::Function(..)) { + return false; + } + let Some(program) = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + else { + return false; + }; + let checked = program.checked(); + if !self + .reflection_metadata + .as_ref() + .is_some_and(|metadata| Arc::ptr_eq(metadata, &checked.reflection_metadata)) + { + return false; + } + let actual = checked + .reflection_metadata + .source_type_id_for_name(actual_type_name); + let target = checked + .reflection_metadata + .source_type_id_for_name(&type_expr_display(requested)); + matches!((actual, target), (Some(actual), Some(target)) + if checked.reflected_callable_plan(actual, target).is_some()) + } + + pub(super) fn prepare_reflected_callable( + &self, + value: Value, + requested: &TypeExpr, + actual_type_name: &str, + ) -> Result { + let program = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .ok_or_else(|| { + "reflected field refinement: unsupported callable or nominal refinement conversion" + .to_string() + })?; + let checked = program.checked(); + if !self + .reflection_metadata + .as_ref() + .is_some_and(|metadata| Arc::ptr_eq(metadata, &checked.reflection_metadata)) + { + return Err("reflected callback metadata belongs to another checked program".into()); + } + let requested_name = type_expr_display(requested); + let actual = checked + .reflection_metadata + .source_type_id_for_name(actual_type_name) + .ok_or_else(|| "reflected callback has no declared source signature".to_string())?; + let target = checked + .reflection_metadata + .source_type_id_for_name(&requested_name) + .ok_or_else(|| "reflected callback has no checked requested signature".to_string())?; + self.authenticate_current_reflected_source(&value, program, actual)?; + if actual == target { + // Authenticate the retained descriptor, then transport its exact + // wrappers without invoking, joining, or establishing a new proof. + return Ok(value); + } + let plan = checked + .reflected_callable_plan(actual, target) + .ok_or_else(|| { + "reflected field refinement: unsupported callable or nominal refinement conversion" + .to_string() + })?; + let source_return = Self::simple_type_expr_from_name( + &checked.interner.type_name(plan.source_return()), + requested.span(), + ) + .ok_or_else(|| { + "reflected callback source return has no exact type expression".to_string() + })?; + let requested_return = Self::simple_type_expr_from_name( + &checked.interner.type_name(plan.requested_return()), + requested.span(), + ) + .ok_or_else(|| { + "reflected callback requested return has no exact type expression".to_string() + })?; + Ok(Value::ReflectedCallable(Box::new( + ReflectedCallableValue::new( + value, + program.clone(), + actual, + target, + source_return, + requested_return, + )?, + ))) + } + + pub(super) fn call_reflected_callable( + &mut self, + adapter: ReflectedCallableValue, + args: Vec, + source_types: Option<&[Option]>, + ) -> Result { + let current = self + .checked_expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .ok_or_else(|| { + "reflected callback invocation has no original checked program".to_string() + })?; + if !Arc::ptr_eq(current, adapter.checked_program()) { + return Err("reflected callback invocation belongs to another checked program".into()); + } + if !adapter.checked_plan().validate(current.checked()) { + return Err("reflected callback invocation changed its checked return plan".into()); + } + self.authenticate_current_reflected_source( + adapter.source(), + current, + adapter.source_type(), + )?; + // Retain the original Value, source signature and closure context. A read + // never calls it. Its own call boundary performs the source validations. + let mut result = + self.call_fn_value_from_source(adapter.source().clone(), args, source_types)?; + let actual_name = type_expr_display(adapter.source_return()); + let requested = adapter.requested_return(); + let mut checks = Vec::new(); + self.plan_produced_refinements( + requested, + Some(adapter.source_return()), + &result, + &mut Vec::new(), + &mut checks, + )?; + // Finish every occupied shape/pending preflight before the first new + // predicate. Do not inspect or implicitly join a pending result. + for check in &checks { + let slot = Self::produced_value_slot(&mut result, &check.path).ok_or_else(|| { + "reflected callback return shape changed during preflight".to_string() + })?; + if matches!(slot.payload(), Value::Pending(_)) { + return Err("reflected callback return: expected a ready value".into()); + } + } + let saved_proofs = self.allow_checked_refinement_proofs; + // The opaque adapter retains an authenticated program/type plan even + // when an ordinary higher-order helper has no source argument map. + self.allow_checked_refinement_proofs = true; + let output = self + .establish_reflected_field_refinement(result, requested, &actual_name, true) + .and_then(|value| { + self.normalize_and_validate_value_from_source( + requested, + value, + Some(&type_expr_display(requested)), + ) + }); + self.allow_checked_refinement_proofs = saved_proofs; + output + } +} diff --git a/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs b/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs new file mode 100644 index 00000000..8c88f23e --- /dev/null +++ b/crates/jett_comptime/src/interpreter/reflected_callables/tests.rs @@ -0,0 +1,704 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +const SOURCE: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function requested(view callback: function() returns Positive) returns nothing: + return nothing +function five() returns int64: + string marker = "source" + trace marker + return 5 +function zero() returns int64: + string marker = "source" + trace marker + return 0 +function delayed() returns int64: + string marker = "source" + trace marker + return run 5 +function captured(offset: int64) returns function() returns int64: + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return offset + return callback +function requested_parameter(view callback: function(int64) returns Positive) returns nothing: + return nothing +function captured_parameter(offset: int64) returns function(int64) returns int64: + function(int64) returns int64 callback = function(value: int64) returns int64: + string marker = "source" + trace marker + return value + offset + return callback +function primitive_step(value: int64) returns int64: + string marker = "primitive" + trace marker + return value + 1 +function captured_after_primitive(offset: int64) returns function() returns int64: + int64 adjusted = primitive_step(offset) + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return adjusted + return callback +function captured_factory() returns function(int64) returns function() returns int64: + function(int64) returns function() returns int64 factory = function(offset: int64) returns function() returns int64: + int64 adjusted = primitive_step(offset) + function() returns int64 callback = function() returns int64: + string marker = "source" + trace marker + return adjusted + return callback + return factory +function forward_factory(view factory: function(int64) returns function() returns int64, offset: int64) returns function() returns int64: + return factory(offset) +"#; + +fn prepared() -> (Interpreter, Arc, TypeExpr) { + let file = FileId::new(0); + let program = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ); + let mut interpreter = Interpreter::new(); + interpreter.register_module(program.module()); + interpreter.set_reflection_metadata(program.checked().reflection_metadata.clone()); + interpreter.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + })); + let requested = Interpreter::simple_type_expr_from_name( + "function() returns app.Positive", + Span::new(file, 0, 0), + ) + .unwrap(); + (interpreter, program, requested) +} + +fn adapter(interpreter: &Interpreter, source: Value, requested: &TypeExpr) -> Value { + interpreter + .prepare_reflected_callable(source, requested, "function() returns int64") + .unwrap() +} + +fn traces(interpreter: &mut Interpreter) -> Vec { + interpreter + .take_debug_events() + .into_iter() + .map(|event| event.text) + .collect() +} + +#[test] +fn reflected_callable_field_relation_requires_same_checked_metadata_and_exact_signature_plan() { + let (mut interpreter, _, requested) = prepared(); + assert!( + interpreter.has_checked_reflected_callable_plan("function() returns int64", &requested) + ); + assert!( + !interpreter + .has_checked_reflected_callable_plan("function(int64) returns bool", &requested) + ); + let (_, foreign, _) = prepared(); + interpreter.set_reflection_metadata(foreign.checked().reflection_metadata.clone()); + assert!( + !interpreter.has_checked_reflected_callable_plan("function() returns int64", &requested) + ); + assert!(traces(&mut interpreter).is_empty()); +} + +#[test] +fn reflected_callable_read_is_lazy_and_each_call_owns_one_new_result_check() { + let (mut interpreter, _, requested) = prepared(); + let observed = adapter( + &interpreter, + Value::NamedFunction("app.five".into()), + &requested, + ); + assert!(traces(&mut interpreter).is_empty()); + for _ in 0..2 { + let result = interpreter + .call_fn_value(observed.clone(), Vec::new()) + .unwrap(); + assert_eq!(result.payload(), &Value::Int64(5)); + assert_eq!( + traces(&mut interpreter), + [ + "trace marker: string = source\n", + "trace marker: string = predicate\n", + ] + ); + } +} + +#[test] +fn reflected_callable_keeps_captured_environment_and_original_debug_identity() { + let (mut interpreter, _, requested) = prepared(); + let source = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let label = source.to_string(); + let observed = adapter(&interpreter, source, &requested); + assert_eq!(observed.to_string(), label); + assert!(traces(&mut interpreter).is_empty()); + let result = interpreter.call_fn_value(observed, Vec::new()).unwrap(); + assert_eq!(result.payload(), &Value::Int64(7)); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_callable_failed_or_pending_result_never_publishes_target_proof() { + let (mut interpreter, _, requested) = prepared(); + let failed = adapter( + &interpreter, + Value::NamedFunction("app.zero".into()), + &requested, + ); + let error = interpreter.call_fn_value(failed, Vec::new()).unwrap_err(); + assert!( + error.contains("refinement type constraint failed for 'app.Positive'"), + "{error}" + ); + assert_eq!(traces(&mut interpreter).len(), 2); + let pending = adapter( + &interpreter, + Value::NamedFunction("app.delayed".into()), + &requested, + ); + assert_eq!( + interpreter.call_fn_value(pending, Vec::new()).unwrap_err(), + "reflected callback return: expected a ready value" + ); + assert_eq!( + traces(&mut interpreter), + ["trace marker: string = source\n"] + ); +} + +#[test] +fn reflected_callable_exact_pending_transport_and_foreign_program_refusal_stay_distinct() { + let (mut interpreter, program, requested) = prepared(); + let value = Value::Pending(Box::new(Value::NamedFunction("app.five".into()))); + let exact = + Interpreter::simple_type_expr_from_name("function() returns int64", requested.span()) + .unwrap(); + let unchanged = interpreter + .prepare_reflected_callable(value.clone(), &exact, "function() returns int64") + .unwrap(); + assert_eq!(unchanged.to_string(), value.to_string()); + assert!(!matches!(unchanged, Value::ReflectedCallable(_))); + assert!(traces(&mut interpreter).is_empty()); + assert!( + interpreter + .prepare_reflected_callable(value, &requested, "function() returns app.Positive") + .unwrap_err() + .contains("original source declaration") + ); + let observed = adapter( + &interpreter, + Value::NamedFunction("app.five".into()), + &requested, + ); + let (mut foreign, other, _) = prepared(); + assert!(!Arc::ptr_eq(&program, &other)); + assert_eq!( + foreign.call_fn_value(observed, Vec::new()).unwrap_err(), + "reflected callback invocation belongs to another checked program" + ); + assert!(traces(&mut foreign).is_empty()); + assert!( + interpreter + .prepare_reflected_callable(Value::Int64(5), &requested, "function() returns int64",) + .unwrap_err() + .contains("not a callable descriptor") + ); +} + +fn refuse_forged_capture(interpreter: &mut Interpreter, source: Value, requested: &TypeExpr) { + let error = interpreter + .prepare_reflected_callable(source, requested, "function() returns int64") + .unwrap_err(); + assert!( + error.contains("unchanged original closure capture"), + "{error}" + ); + assert!(traces(interpreter).is_empty()); +} + +#[test] +fn reflected_closure_same_span_body_and_header_forgery_refuse_before_effects() { + let (mut interpreter, program, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let mut forged = genuine.clone(); + let Value::Function { body, .. } = &mut forged else { + panic!("expected closure"); + }; + let Stmt::Return(statement) = body.stmts.last_mut().unwrap() else { + panic!("expected return"); + }; + let span = statement.value.as_ref().unwrap().span(); + statement.value = Some(Expr::IntLiteral(11, span)); + refuse_forged_capture(&mut interpreter, forged, &requested); + + let mut forged = genuine.clone(); + let Value::Function { params, .. } = &mut forged else { + panic!("expected closure"); + }; + let original_parameter = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => { + function.params.first().cloned() + } + _ => None, + }) + .unwrap(); + params.push(original_parameter); + refuse_forged_capture(&mut interpreter, forged, &requested); + + let mut forged = genuine.clone(); + let Value::Function { type_context, .. } = &mut forged else { + panic!("expected closure"); + }; + let TypeExpr::Named(name) = type_context.return_type.as_mut().unwrap() else { + panic!("expected named return"); + }; + name.name = "bool".into(); + refuse_forged_capture(&mut interpreter, forged, &requested); + let observed = adapter(&interpreter, genuine, &requested); + for _ in 0..2 { + assert_eq!( + interpreter + .call_fn_value(observed.clone(), Vec::new()) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 2); + } +} + +#[test] +fn reflected_closure_same_span_parameter_contract_mutation_refuses_before_effects() { + let (mut interpreter, _, requested) = prepared(); + let requested = Interpreter::simple_type_expr_from_name( + "function(int64) returns app.Positive", + requested.span(), + ) + .unwrap(); + let source = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + for mutation in 0..4 { + let mut forged = source.clone(); + let Value::Function { params, .. } = &mut forged else { + panic!("expected closure"); + }; + let param = &mut params[0]; + match mutation { + 0 => param.view = true, + 1 => param.mutable = true, + 2 => param.name.name = "other".into(), + 3 => { + let TypeExpr::Named(name) = &mut param.ty else { + panic!("expected named type"); + }; + name.name = "bool".into(); + } + _ => unreachable!(), + } + let error = interpreter + .prepare_reflected_callable(forged, &requested, "function(int64) returns int64") + .unwrap_err(); + assert!( + error.contains("unchanged original closure capture"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + } + let observed = interpreter + .prepare_reflected_callable(source, &requested, "function(int64) returns int64") + .unwrap(); + assert!(traces(&mut interpreter).is_empty()); + assert_eq!( + interpreter + .call_fn_value(observed, vec![Value::Int64(2)]) + .unwrap() + .payload(), + &Value::Int64(9) + ); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_closure_capture_types_context_and_alias_forgery_refuse_before_effects() { + let (mut interpreter, _, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + for mutation in 0..8 { + let mut forged = genuine.clone(); + let Value::Function { + type_context, + captures, + capture_types, + namespace_aliases, + namespace, + .. + } = &mut forged + else { + panic!("expected closure"); + }; + match mutation { + 0 => { + captures.insert("offset".into(), Value::Int64(0)); + } + 1 => { + captures.insert("extra".into(), Value::Nothing); + } + 2 => { + let TypeExpr::Named(name) = capture_types.get_mut("offset").unwrap() else { + panic!("expected capture type"); + }; + name.name = "bool".into(); + } + 3 => { + type_context.bindings.insert( + "Foreign".into(), + TypeExpr::Named(Ident { + name: "bool".into(), + span: requested.span(), + }), + ); + } + 4 => { + type_context.checked_scope = Some(Arc::new(CheckedScopedTypes::default())); + } + 5 => { + type_context.checked_function = Some(Arc::new(CheckedFunctionTypes::default())); + } + 6 => { + namespace_aliases.insert("changed".into(), "other".into()); + } + 7 => { + *namespace = Some("other".into()); + } + _ => unreachable!(), + } + refuse_forged_capture(&mut interpreter, forged, &requested); + } + let (_, foreign, _) = prepared(); + let Value::Function { type_context, .. } = &genuine else { + panic!("expected closure"); + }; + let actual = foreign + .checked() + .reflection_metadata + .source_type_id_for_name("function() returns int64") + .unwrap(); + assert!( + !type_context + .reflected_capture + .as_ref() + .unwrap() + .validate_source(&foreign, actual, &genuine) + ); + assert!(traces(&mut interpreter).is_empty()); +} + +#[test] +fn reflected_closure_changed_source_owner_cannot_mint_a_witness() { + let (mut interpreter, program, requested) = prepared(); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => Some(function.clone()), + _ => None, + }) + .unwrap(); + let mut changed = original.clone(); + // Keep the original inline node and all source spans while changing its + // enclosing owner. An exact subtree must not authenticate a changed owner. + changed.body.stmts.insert( + 0, + Stmt::Expr(jett_parser::ast::ExprStmt { + expr: Expr::Nothing(changed.body.span), + span: changed.body.span, + }), + ); + interpreter.register_function_in_namespace(Some("app"), &changed); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw.clone(), &requested); + // Ordinary closure execution remains available without reflected authority. + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); + + let mut transplanted = original; + transplanted.name.name = "five".into(); + interpreter.register_function_in_namespace(Some("app"), &transplanted); + let raw = interpreter + .call_function("app.five", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw, &requested); +} + +#[test] +fn reflected_closure_forged_installed_checked_context_cannot_mint_a_witness() { + let (mut interpreter, program, requested) = prepared(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "captured" => { + Some(function.name.span) + } + _ => None, + }) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + installed + .functions + .insert(declaration, vec![Arc::new(CheckedFunctionTypes::default())]); + interpreter.set_checked_expression_types(Arc::new(installed)); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + refuse_forged_capture(&mut interpreter, raw.clone(), &requested); + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); +} + +#[test] +fn reflected_closure_forged_root_type_context_refuses_mint_and_admission() { + for binding_map in [false, true] { + let (mut interpreter, _, requested) = prepared(); + let genuine = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + let foreign_span = Span::new(requested.span().file, u32::MAX - 1, u32::MAX); + if binding_map { + installed + .bindings + .insert(foreign_span, vec![Arc::new(CheckedScopedTypes::default())]); + } else { + installed.expressions.insert(foreign_span, "bool".into()); + } + interpreter.set_checked_expression_types(Arc::new(installed)); + let error = interpreter + .prepare_reflected_callable(genuine, &requested, "function() returns int64") + .unwrap_err(); + assert!( + error.contains("checked expression context changed its original source"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + let raw = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { type_context, .. } = &raw else { + panic!("expected closure"); + }; + assert!(type_context.reflected_capture.is_none()); + assert_eq!( + interpreter.call_fn_value(raw, Vec::new()).unwrap(), + Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 1); + } +} + +#[test] +fn reflected_closure_root_arithmetic_fact_forgery_refuses_before_source_invocation() { + let (mut interpreter, program, requested) = prepared(); + let requested = Interpreter::simple_type_expr_from_name( + "function(int64) returns app.Positive", + requested.span(), + ) + .unwrap(); + let source = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + let observed = interpreter + .prepare_reflected_callable(source, &requested, "function(int64) returns int64") + .unwrap(); + let arithmetic = program + .module() + .items + .iter() + .find_map(|item| { + let Item::Function(function) = item else { + return None; + }; + if function.name.name != "captured_parameter" { + return None; + } + let Stmt::VarDecl(declaration) = function.body.stmts.first()? else { + return None; + }; + let Expr::InlineFn(_, _, body, _) = &declaration.value else { + return None; + }; + let Stmt::Return(statement) = body.stmts.last()? else { + return None; + }; + statement.value.as_ref().map(Expr::span) + }) + .unwrap(); + let mut installed = interpreter + .checked_expression_types + .as_ref() + .unwrap() + .as_ref() + .clone(); + installed.expressions.insert(arithmetic, "int8".into()); + interpreter.set_checked_expression_types(Arc::new(installed)); + let error = interpreter + .call_fn_value(observed, vec![Value::Int64(130)]) + .unwrap_err(); + assert!( + error.contains("checked expression context changed its original source"), + "{error}" + ); + assert!(traces(&mut interpreter).is_empty()); + let raw = interpreter + .call_function("app.captured_parameter", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { type_context, .. } = &raw else { + panic!("expected closure"); + }; + assert!(type_context.reflected_capture.is_none()); + assert_eq!( + interpreter + .call_fn_value(raw, vec![Value::Int64(130)]) + .unwrap(), + Value::Int64(-119) + ); + assert_eq!(traces(&mut interpreter).len(), 1); +} + +#[test] +fn reflected_closure_pending_nothing_snapshot_preserves_layers_without_joining() { + let (mut interpreter, _, requested) = prepared(); + interpreter.set_variable( + "unobserved", + Value::Pending(Box::new(Value::Pending(Box::new(Value::Nothing)))), + ); + let source = interpreter + .call_function("app.captured", vec![Value::Int64(7)]) + .unwrap(); + let Value::Function { captures, .. } = &source else { + panic!("expected closure"); + }; + assert!( + matches!(captures.get("unobserved"), Some(Value::Pending(outer)) if matches!(outer.as_ref(), Value::Pending(inner) if matches!(inner.as_ref(), Value::Nothing))) + ); + let exact = + Interpreter::simple_type_expr_from_name("function() returns int64", requested.span()) + .unwrap(); + let pending = Value::Pending(Box::new(Value::Pending(Box::new(source.clone())))); + let unchanged = interpreter + .prepare_reflected_callable(pending, &exact, "function() returns int64") + .unwrap(); + assert!( + matches!(unchanged, Value::Pending(outer) if matches!(outer.as_ref(), Value::Pending(inner) if matches!(inner.as_ref(), Value::Function { .. }))) + ); + assert!(traces(&mut interpreter).is_empty()); + let mut forged = source.clone(); + let Value::Function { captures, .. } = &mut forged else { + panic!("expected closure"); + }; + captures.insert( + "unobserved".into(), + Value::Pending(Box::new(Value::Nothing)), + ); + refuse_forged_capture(&mut interpreter, forged, &requested); + let observed = adapter(&interpreter, source, &requested); + assert_eq!( + interpreter + .call_fn_value(observed, Vec::new()) + .unwrap() + .payload(), + &Value::Int64(7) + ); + assert_eq!(traces(&mut interpreter).len(), 2); +} + +#[test] +fn reflected_closure_dynamic_factories_restore_mint_origin_after_primitive_calls() { + let (mut interpreter, _, requested) = prepared(); + let captured_factory = interpreter + .call_function("app.captured_factory", Vec::new()) + .unwrap(); + assert!(traces(&mut interpreter).is_empty()); + for factory in [ + Value::NamedFunction("app.captured_after_primitive".into()), + captured_factory, + ] { + let source = interpreter + .call_function("app.forward_factory", vec![factory, Value::Int64(7)]) + .unwrap(); + assert_eq!( + traces(&mut interpreter), + ["trace marker: string = primitive\n"] + ); + assert!(interpreter.active_reflected_capture_origin.is_none()); + assert!(interpreter.active_checked_function.is_none()); + assert!(interpreter.active_checked_scope.is_none()); + assert!(interpreter.allow_checked_refinement_proofs); + let observed = adapter(&interpreter, source, &requested); + assert!(traces(&mut interpreter).is_empty()); + let result = interpreter.call_fn_value(observed, Vec::new()).unwrap(); + assert_eq!(result.payload(), &Value::Int64(8)); + assert_eq!( + traces(&mut interpreter), + [ + "trace marker: string = source\n", + "trace marker: string = predicate\n", + ] + ); + assert!(interpreter.active_reflected_capture_origin.is_none()); + } +} diff --git a/crates/jett_comptime/src/interpreter/temporary_local_views.rs b/crates/jett_comptime/src/interpreter/temporary_local_views.rs new file mode 100644 index 00000000..4dbdff35 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/temporary_local_views.rs @@ -0,0 +1,499 @@ +//! Private ordinary local-view transport. Public Value remains an owned carrier. +//! Exact checked activations retain whole owners through lexical cleanup. +use super::*; +use jett_resolve::DefId; +use jett_typecheck::{CheckedBindingFact, CheckedBindingMode, CheckedViewSource}; +use jett_types::{Type, TypeId}; + +mod calls; +mod receipts; +mod storage; +pub(super) use calls::PreparedNamedFrame; +use receipts::{FunctionReceipt, ProducerReceipt, Projection}; +pub(super) use storage::{ActivationId, Context}; + +/// An owned successful output is carried through cleanup, rather than an +/// ambient flag that could suppress another invocation's return boundary. +pub(in crate::interpreter) enum BlockOutput { + Ordinary(Option), + Acquired(AcquiredOutput), +} +pub(in crate::interpreter) struct AcquiredOutput { + activation: ActivationId, + function: FunctionReceipt, + value: Value, + source: Option, +} +impl BlockOutput { + pub(in crate::interpreter) fn into_signal(self) -> Option { + match self { + Self::Ordinary(signal) => signal, + Self::Acquired(output) => Some(Signal::Return(output.value, output.source)), + } + } +} +impl AcquiredOutput { + pub(in crate::interpreter) fn into_registered_value( + self, + interpreter: &Interpreter, + activation: Option, + function: Option<&FunctionReceipt>, + ) -> Result { + let expected = function.ok_or("temporary output lost its checked function receipt")?; + let current = interpreter + .temporary_local_views + .as_ref() + .and_then(Context::current) + .ok_or("temporary output lost its completed activation")?; + interpreter.temporary_program()?; + if activation != Some(self.activation) + || current.id != self.activation + || current.root_active + || !storage::same_function(expected, &self.function)? + || !storage::same_function(¤t.function, &self.function)? + { + return Err("temporary output belongs to a different function activation".into()); + } + Ok(self.value) + } +} + +impl Interpreter { + pub(in crate::interpreter) fn set_temporary_entry_allowed(&mut self, allowed: bool) -> bool { + self.temporary_local_views + .as_mut() + .is_some_and(|context| std::mem::replace(&mut context.entry_allowed, allowed)) + } + fn temporary_program( + &self, + ) -> Result>, String> { + let Some(context) = &self.temporary_local_views else { + return Ok(None); + }; + let (retained, _) = self + .retained_resource_program + .as_ref() + .ok_or("temporary local views lost the installed checked program")?; + if !Arc::ptr_eq(retained, &context.program) { + return Err("temporary local views reject a foreign installed program context".into()); + } + let Some(types) = &self.checked_expression_types else { + return Err("temporary local views need the canonical checked type projection".into()); + }; + if !types.matches_original_program(&context.program) { + return Err( + "temporary local views reject a foreign or edited checked type projection".into(), + ); + } + Ok(Some(context.program.clone())) + } + + /// Raw entries never gain a checked local-view activation from names or spans. + pub(in crate::interpreter) fn prepare_temporary_function( + &self, + name: &str, + namespace: &Option, + current: &FunctionDef, + projection: Option<&crate::checked_types::CheckedFunctionTypes>, + arguments: &[crate::value::ClosureTypeArgument], + source: bool, + frame: Option<&PreparedNamedFrame>, + ) -> Result, String> { + let Some(context) = &self.temporary_local_views else { + return Ok(None); + }; + let receipt = if let Some(frame) = frame { + frame + .callee + .validate_registered(name, namespace, current, projection, arguments)?; + Some(frame.callee.clone()) + } else { + receipts::function_receipt(&context.program, name, namespace, current)? + }; + let Some(receipt) = receipt else { + if frame.is_some() || Context::body_needed(&context.program, Some(current.body.span)) { + return Err( + "temporary local view generic call transport remains unsupported".into(), + ); + } + return Ok(None); + }; + if storage::scoped_temporary( + &context.program, + receipt.facts()?.scopes, + Some(current.body.span), + ) { + return Err( + "temporary local view reflected/comptime body transport remains unsupported".into(), + ); + } + let admitted = source + && self.allow_checked_refinement_proofs + && (frame.is_some() || context.entry_allowed); + if !admitted { + return if receipt.has_temporary_views()? { + Err("temporary local view body needs its original checked entry or caller".into()) + } else { + Ok(None) + }; + } + self.temporary_program()?; + if let Some(frame) = frame { + frame.validate_target(self, &receipt)?; + } + Ok(Some(receipt)) + } + + pub(in crate::interpreter) fn enter_temporary_function( + &mut self, + receipt: Option, + ) -> Result, String> { + match receipt { + Some(receipt) => self + .temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .enter(receipt) + .map(Some), + None => Ok(None), + } + } + + pub(in crate::interpreter) fn leave_temporary_function( + &mut self, + activation: Option, + ) -> Result<(), String> { + if let Some(activation) = activation { + self.temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .leave(activation)?; + } + Ok(()) + } + + fn temporary_alias(&self, expression: &Expr) -> Result, String> { + let Some(context) = &self.temporary_local_views else { + return Ok(None); + }; + let Some(current) = context.current() else { + return Ok(None); + }; + // A raw/nested unsupported body cannot borrow a caller's placeholder. + if !current.root_active { + return Ok(None); + } + self.temporary_program()?; + context.alias_for(¤t.function, expression, self.lexical_scope_floor) + } + + pub(in crate::interpreter) fn eval_temporary_read( + &mut self, + expression: &Expr, + ) -> Result, String> { + let (operand, explicit_clone) = match expression { + Expr::Clone(inner, _) => (inner.as_ref(), true), + Expr::Ident(_) | Expr::FieldAccess(_, _, _) | Expr::View(_, _) => (expression, false), + _ => return Ok(None), + }; + let Some(ticket) = self.temporary_alias(operand)? else { + return Ok(None); + }; + let context = self + .temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")?; + if explicit_clone { + return Ok(Some(ExprFlow::Value(context.explicit_clone(&ticket)?))); + } + if !receipts::copyable(&context.program.checked().interner, ticket.endpoint) { + return Err("temporary local view needs a checked observer, forwarding binding, or explicit clone".into()); + } + // Only implicitly copyable endpoints reach an owned Value result here. + let value = context.observe(&ticket)?.clone(); + Ok(Some(ExprFlow::Value(value))) + } + + pub(in crate::interpreter) fn exec_temporary_binding( + &mut self, + declaration: &jett_parser::ast::VarDecl, + ) -> Result>, String> { + let Some(context) = &self.temporary_local_views else { + return Ok(None); + }; + let required = Context::body_needed(&context.program, Some(declaration.name.span)); + let unsupported = || { + "temporary local view declaration needs its checked ordinary root-body adapter" + .to_string() + }; + let Some(current) = context.current() else { + return if required { + Err(unsupported()) + } else { + Ok(None) + }; + }; + if !current.root_active { + return if required { + Err(unsupported()) + } else { + Ok(None) + }; + } + let function = current.function.clone(); + self.temporary_program()?; + let Some(fact) = function + .facts()? + .binding_facts + .get(&declaration.name.span) + .copied() + else { + return if required { + Err(unsupported()) + } else { + Ok(None) + }; + }; + if !matches!(fact.mode, CheckedBindingMode::View { .. }) { + return Ok(None); + } + if let Some(ticket) = self.temporary_alias(&declaration.value)? { + let original = function.original()?.body.stmts.iter().any(|statement| matches!(statement, Stmt::VarDecl(original) if std::ptr::eq(original, declaration))); + let source = match fact.mode { + CheckedBindingMode::View { + source: CheckedViewSource::Binding(source), + } => source, + _ => { + return Err( + "temporary local view forwarding lost its checked binding source".into(), + ); + } + }; + let root = alias_root(&declaration.value) + .ok_or("temporary local view forwarding needs its original alias root")?; + if !original + || declaration.mutable + || fact.mutable + || fact.ty != ticket.endpoint + || function.fact(&declaration.name)? != fact + || function.program.resolved().resolutions.get(&root.span) != Some(&source) + { + return Err( + "temporary local view forwarding changed its exact immutable declaration" + .into(), + ); + } + return self + .publish_temporary_alias(fact, &declaration.name.name, ticket, &declaration.ty) + .map(|()| Some(None)); + } + let Some(receipt) = receipts::producer_receipt(&function, declaration)? else { + return Ok(None); + }; + if !matches!( + function.program.checked().interner.resolve(fact.ty), + Type::Bytes | Type::List(_) + ) { + return Err( + "temporary local view endpoint transport remains unsupported beyond bytes/list" + .into(), + ); + } + // Reserve only metadata. This slot cannot be published or observed yet. + let slot = self + .temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .reserve(receipt)?; + // Borrow original producer via a separate Arc; nested calls may mutate context. + let original = function.original()?; + let Stmt::VarDecl(original_declaration) = original.body.stmts.iter() + .find(|statement| matches!(statement, Stmt::VarDecl(value) if std::ptr::eq(value, declaration))) + .ok_or("temporary local view lost its original declaration before evaluation")? + else { return Err("temporary local view lost its original declaration before evaluation".into()); }; + let Expr::View(projected, _) = receipts::unparen(&original_declaration.value) else { + return Err("temporary local view lost its written initializer".into()); + }; + let producer = projection_root(projected); + let value = match self.eval_expr_flow(producer)? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(Some(Some(signal))), + ExprFlow::Resource(_) => { + return Err("temporary local views cannot adopt Resource custody".into()); + } + }; + // The authentic named producer already normalizes its complete declared + // return. acquire validates that exact whole type without taking a field. + let ticket = self + .temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .acquire(slot, value)?; + self.publish_temporary_alias(fact, &declaration.name.name, ticket, &declaration.ty)?; + Ok(Some(None)) + } + + fn publish_temporary_alias( + &mut self, + fact: CheckedBindingFact, + name: &str, + ticket: storage::AliasTicket, + ty: &TypeExpr, + ) -> Result<(), String> { + let scope = self + .scopes + .len() + .checked_sub(1) + .ok_or("temporary local view has no lexical storage")?; + let ty = self.substitute_type_expr(ty); + // Placeholder has no bytes/aggregate payload; only the private ticket + // can observe the owner. get_variable shields it from raw Value APIs. + self.temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .publish(fact, name, ticket, scope)?; + self.set_variable_with_type(name, Value::Nothing, ty); + Ok(()) + } + + pub(in crate::interpreter) fn stage_temporary_parameters( + &mut self, + function: &FunctionDef, + arguments: Vec, + source_types: Option<&[Option]>, + frame: Option<&PreparedNamedFrame>, + ) -> Result, String> { + if let Some(frame) = frame { + frame.validate_values(self, function, &arguments, source_types)?; + } + let mut staged = Vec::with_capacity(arguments.len()); + for (index, (parameter, value)) in function.params.iter().zip(arguments).enumerate() { + let ty = self.substitute_type_expr(¶meter.ty); + let value = if frame.is_some_and(|frame| frame.alias(index).is_some()) { + value // private Nothing placeholder; checked above, no Value clone + } else { + let source = source_types + .and_then(|types| types.get(index)) + .and_then(Option::as_deref); + self.normalize_and_validate_value_from_source(&ty, value, source)? + }; + staged.push((value, ty)); + } + Ok(staged) + } + + pub(in crate::interpreter) fn install_temporary_parameter( + &mut self, + index: usize, + parameter: &Param, + frame: Option<&PreparedNamedFrame>, + ) -> Result<(), String> { + let Some(frame) = frame else { + return Ok(()); + }; + let Some(ticket) = frame.alias(index) else { + return Ok(()); + }; + let fact = frame.callee.fact(¶meter.name)?; + let scope = self + .scopes + .len() + .checked_sub(1) + .ok_or("temporary local view parameter has no storage")?; + self.temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .publish(fact, ¶meter.name.name, ticket.clone(), scope) + } + + pub(in crate::interpreter) fn begin_temporary_block( + &mut self, + block: &Block, + ) -> Result, String> { + let scope = self + .scopes + .len() + .checked_sub(1) + .ok_or("temporary local view body has no storage")?; + self.temporary_local_views + .as_mut() + .map_or(Ok(None), |context| context.begin(block, scope)) + } + + pub(in crate::interpreter) fn acquire_temporary_output( + &mut self, + activation: Option, + result: Result, String>, + ) -> Result { + let Some(activation) = activation else { + return result.map(BlockOutput::Ordinary); + }; + let function = self + .temporary_local_views + .as_ref() + .and_then(Context::current) + .filter(|current| current.id == activation) + .ok_or("temporary local view output has stale activation")? + .function + .clone(); + let result = result?; + let (value, source) = match result { + Some(Signal::Return(value, source)) => (value, source), + None => (Value::Nothing, None), + other => return Ok(BlockOutput::Ordinary(other)), + }; + let value = match &function.original()?.return_type { + Some(ty) => { + let ty = self.substitute_type_expr(ty); + self.normalize_and_validate_value_from_source(&ty, value, source.as_deref())? + } + None => value, + }; + self.temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .audit + .push(storage::AuditEvent::Output { activation }); + Ok(BlockOutput::Acquired(AcquiredOutput { + activation, + function, + value, + source, + })) + } + + pub(in crate::interpreter) fn complete_temporary_block( + &mut self, + activation: Option, + ) -> Result<(), String> { + if let Some(activation) = activation { + self.temporary_local_views + .as_mut() + .ok_or("temporary local view context disappeared")? + .complete(activation)?; + } + Ok(()) + } +} + +fn alias_root(mut expression: &Expr) -> Option<&Ident> { + loop { + match receipts::unparen(expression) { + Expr::View(inner, _) | Expr::FieldAccess(inner, _, _) => expression = inner, + Expr::Ident(root) => return Some(root), + _ => return None, + } + } +} + +fn projection_root(mut expression: &Expr) -> &Expr { + loop { + match receipts::unparen(expression) { + Expr::FieldAccess(base, _, _) => expression = base, + root => return root, + } + } +} + +#[cfg(test)] +mod list_tests; diff --git a/crates/jett_comptime/src/interpreter/temporary_local_views/calls.rs b/crates/jett_comptime/src/interpreter/temporary_local_views/calls.rs new file mode 100644 index 00000000..95d6c64a --- /dev/null +++ b/crates/jett_comptime/src/interpreter/temporary_local_views/calls.rs @@ -0,0 +1,1561 @@ +//! Original named-call envelopes. Alias parameters never enter Vec as bytes. +use super::*; +use jett_typecheck::{ + CheckedCalleeAccess, CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax, + CheckedInvocationShape, CheckedInvocationTarget, +}; + +pub(in crate::interpreter) struct PreparedNamedFrame { + caller_activation: ActivationId, + call: receipts::CallReceipt, + pub(super) callee: FunctionReceipt, + aliases: Vec>, +} + +#[cfg(test)] +mod tests { + //! Private transport controls; native execution evidence is recorded separately. + use super::*; + use jett_common::{FileId, SourceOrigin}; + use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + + const SOURCE: &str = r#"namespace app +struct Packet: + data: bytes + sibling: bytes +function make_packet(data: bytes, sibling: bytes) returns Packet: + return Packet(data: data, sibling: sibling) +function inspect(view data: bytes, marker: int64) returns nothing: + return nothing +function other(view data: bytes, marker: int64) returns nothing: + return nothing +function scenario(data: bytes, sibling: bytes) returns nothing: + bytes borrowed = view make_packet(data, sibling).data + bytes forwarded = borrowed + inspect(marker: 7, data: view forwarded) + return nothing +"#; + + fn program() -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ) + } + fn function(program: &Arc, name: &str) -> FunctionReceipt { + let source = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .unwrap(); + receipts::function_receipt(program, &format!("app.{name}"), &Some("app".into()), source) + .unwrap() + .unwrap() + } + fn declaration<'a>(function: &'a FunctionReceipt, name: &str) -> &'a jett_parser::ast::VarDecl { + function + .original() + .unwrap() + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::VarDecl(declaration) if declaration.name.name == name => Some(declaration), + _ => None, + }) + .unwrap() + } + fn started() -> (Interpreter, FunctionReceipt, ActivationId) { + let program = program(); + let function = function(&program, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.checked_expression_types = Some(Arc::new( + CheckedExpressionTypes::from_checked_program(&program), + )); + interpreter.push_scope(); // synthetic parameter storage, not the source lifetime + let context = interpreter.temporary_local_views.as_mut().unwrap(); + context.entry_allowed = true; + let activation = context.enter(function.clone()).unwrap(); + interpreter.push_scope(); + let scope = interpreter.scopes.len() - 1; + assert_eq!( + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .begin(&function.original().unwrap().body, scope) + .unwrap(), + Some(activation) + ); + (interpreter, function, activation) + } + fn whole(function: &FunctionReceipt) -> Value { + let receipt = receipts::producer_receipt(function, declaration(function, "borrowed")) + .unwrap() + .unwrap(); + let types = &function.program.checked().interner; + let Type::Struct(id) = types.resolve(receipt.whole) else { + panic!("checked Packet"); + }; + Value::Struct { + type_name: types.resolve_struct(*id).name.clone(), + concrete_type: None, + fields: vec![ + ("data".into(), Value::Bytes(vec![65, 66])), + ("sibling".into(), Value::Bytes(vec![89, 90])), + ], + } + } + fn install(interpreter: &mut Interpreter, function: &FunctionReceipt) -> storage::AliasTicket { + let declaration = declaration(function, "borrowed"); + let receipt = receipts::producer_receipt(function, declaration) + .unwrap() + .unwrap(); + let fact = receipt.fact; + let context = interpreter.temporary_local_views.as_mut().unwrap(); + let slot = context.reserve(receipt).unwrap(); + let ticket = context.acquire(slot, whole(function)).unwrap(); + interpreter + .publish_temporary_alias(fact, "borrowed", ticket.clone(), &declaration.ty) + .unwrap(); + ticket + } + fn forwarded(interpreter: &mut Interpreter, function: &FunctionReceipt) { + assert!(matches!( + interpreter + .exec_temporary_binding(declaration(function, "forwarded")) + .unwrap(), + Some(None) + )); + } + fn frame(interpreter: &Interpreter, function: &FunctionReceipt) -> PreparedNamedFrame { + let expression = function + .original() + .unwrap() + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Expr(statement) => Some(&statement.expr), + _ => None, + }) + .unwrap(); + let Expr::Call(callee, arguments, span) = expression else { + panic!("original inspect call"); + }; + let call = receipts::checked_call(function, callee, arguments, *span).unwrap(); + assert_eq!(call.order, vec![1, 0]); + let alias = interpreter + .temporary_alias(&arguments[1].value) + .unwrap() + .unwrap(); + PreparedNamedFrame { + caller_activation: interpreter + .temporary_local_views + .as_ref() + .unwrap() + .current() + .unwrap() + .id, + callee: receipts::named_target(&call).unwrap().unwrap(), + call, + aliases: vec![Some(alias), None], + } + } + fn finish(interpreter: &mut Interpreter, activation: ActivationId) { + let context = interpreter.temporary_local_views.as_mut().unwrap(); + context.complete(activation).unwrap(); + assert!( + context + .current() + .unwrap() + .owners + .iter() + .all(|owner| !matches!(owner.state, storage::OwnerState::Live(_))) + ); + context.leave(activation).unwrap(); + interpreter.pop_scope(); + interpreter.pop_scope(); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .activations + .is_empty() + ); + } + + #[test] + fn whole_owner_keeps_unviewed_sibling_until_root_retirement() { + let (mut interpreter, function, activation) = started(); + let ticket = install(&mut interpreter, &function); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert_eq!( + context.observe(&ticket).unwrap(), + &Value::Bytes(vec![65, 66]) + ); + let storage::OwnerState::Live(Value::Struct { fields, .. }) = + &context.current().unwrap().owners[0].state + else { + panic!("whole Packet retained"); + }; + assert_eq!(fields[1], ("sibling".into(), Value::Bytes(vec![89, 90]))); + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .complete(activation) + .unwrap(); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.observe(&ticket).is_err()); + let ended = context + .audit + .iter() + .position(|event| matches!(event, storage::AuditEvent::AliasEnded { .. })) + .unwrap(); + let retired = context + .audit + .iter() + .position(|event| matches!(event, storage::AuditEvent::OwnerRetired { .. })) + .unwrap(); + assert!(ended < retired); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::OwnerRetired { .. })) + .count(), + 1 + ); + assert!( + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .complete(activation) + .is_err() + ); + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .leave(activation) + .unwrap(); + interpreter.pop_scope(); + interpreter.pop_scope(); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .current() + .is_none() + ); + } + + #[test] + fn forwarding_keeps_one_ticket_and_explicit_clone_is_independent() { + let (mut interpreter, function, activation) = started(); + let ticket = install(&mut interpreter, &function); + forwarded(&mut interpreter, &function); + let frame = frame(&interpreter, &function); + assert!(frame.alias(0).unwrap() == &ticket); + let context = interpreter.temporary_local_views.as_mut().unwrap(); + let Value::Bytes(mut cloned) = context.explicit_clone(&ticket).unwrap() else { + panic!("bytes clone"); + }; + cloned.push(67); + assert_eq!(cloned, vec![65, 66, 67]); + assert_eq!( + context.observe(&ticket).unwrap(), + &Value::Bytes(vec![65, 66]) + ); + assert_eq!(context.current().unwrap().owners.len(), 1); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::ExplicitClone { .. })) + .count(), + 1 + ); + finish(&mut interpreter, activation); + } + + #[test] + fn named_frame_rejects_wrong_foreign_empty_and_stale_contexts() { + let (mut interpreter, function, activation) = started(); + let ticket = install(&mut interpreter, &function); + forwarded(&mut interpreter, &function); + let mut frame = frame(&interpreter, &function); + frame.validate_target(&interpreter, &frame.callee).unwrap(); + assert!( + frame + .validate_target(&interpreter, &self::function(&function.program, "other")) + .is_err() + ); + let foreign = program(); + assert!( + frame + .validate_target(&interpreter, &self::function(&foreign, "inspect")) + .is_err() + ); + let caller = interpreter + .temporary_local_views + .as_mut() + .unwrap() + .activations + .pop() + .unwrap(); + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .activations + .push(caller); + frame.aliases[0].as_mut().unwrap().owner_activation = activation.checked_add(1).unwrap(); + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .observe(&ticket) + .is_ok() + ); + finish(&mut interpreter, activation); + } + + #[test] + fn malformed_alias_arity_and_permutation_refuse_before_publication() { + let (mut interpreter, function, activation) = started(); + install(&mut interpreter, &function); + forwarded(&mut interpreter, &function); + let mut frame = frame(&interpreter, &function); + let before = interpreter + .temporary_local_views + .as_ref() + .unwrap() + .audit + .len(); + let alias = frame.aliases[0].take(); + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + frame.aliases[0] = alias; + frame.aliases[1] = frame.aliases[0].clone(); + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + frame.aliases[1] = None; + frame.aliases.pop(); + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + frame.aliases.push(None); + frame.call.order[0] = frame.call.order[1]; + assert!(frame.validate_target(&interpreter, &frame.callee).is_err()); + assert_eq!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .audit + .len(), + before + ); + finish(&mut interpreter, activation); + } + + #[test] + fn callee_parameter_accepts_only_placeholder_and_preserves_caller_owner() { + let (mut interpreter, function, activation) = started(); + let ticket = install(&mut interpreter, &function); + forwarded(&mut interpreter, &function); + let frame = frame(&interpreter, &function); + frame.validate_target(&interpreter, &frame.callee).unwrap(); + let callee = interpreter + .temporary_local_views + .as_mut() + .unwrap() + .enter(frame.callee.clone()) + .unwrap(); + interpreter.push_scope(); + let source = frame.callee.original().unwrap(); + let sources = [Some("bytes".to_owned()), Some("int64".to_owned())]; + frame + .validate_values( + &interpreter, + source, + &[Value::Nothing, Value::Int64(7)], + Some(&sources), + ) + .unwrap(); + assert!( + frame + .validate_values( + &interpreter, + source, + &[Value::Bytes(vec![65, 66]), Value::Int64(7)], + Some(&sources) + ) + .is_err() + ); + assert!( + frame + .validate_values(&interpreter, source, &[Value::Nothing], Some(&sources)) + .is_err() + ); + interpreter + .install_temporary_parameter(0, &source.params[0], Some(&frame)) + .unwrap(); + interpreter.pop_scope(); + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .leave(callee) + .unwrap(); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .observe(&ticket) + .is_ok() + ); + finish(&mut interpreter, activation); + } + + #[test] + fn failed_acquire_retires_whole_without_publishing_alias() { + let (mut interpreter, function, activation) = started(); + let receipt = receipts::producer_receipt(&function, declaration(&function, "borrowed")) + .unwrap() + .unwrap(); + let mut value = whole(&function); + let Value::Struct { fields, .. } = &mut value else { + panic!("Packet"); + }; + fields[0].1 = Value::Pending(Box::new(Value::Bytes(vec![65, 66]))); + let context = interpreter.temporary_local_views.as_mut().unwrap(); + let slot = context.reserve(receipt).unwrap(); + assert!(context.acquire(slot, value).is_err()); + assert!(matches!( + context.current().unwrap().owners[0].state, + storage::OwnerState::Retired + )); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!( + !context + .audit + .iter() + .any(|event| matches!(event, storage::AuditEvent::AliasPublished { .. })) + ); + let acquired = context + .audit + .iter() + .position(|event| matches!(event, storage::AuditEvent::Acquired { .. })) + .unwrap(); + let retired = context + .audit + .iter() + .position(|event| matches!(event, storage::AuditEvent::OwnerRetired { .. })) + .unwrap(); + assert!(acquired < retired); + finish(&mut interpreter, activation); + } + + #[test] + fn owned_output_packet_is_acquired_before_cleanup_and_bound_to_exact_activation() { + for mutation in 0..5 { + let (mut interpreter, function, activation) = started(); + install(&mut interpreter, &function); + let output = interpreter + .acquire_temporary_output( + Some(activation), + Ok(Some(Signal::Return(Value::Nothing, None))), + ) + .unwrap(); + let BlockOutput::Acquired(output) = output else { + panic!("owned acquired output"); + }; + interpreter + .complete_temporary_block(Some(activation)) + .unwrap(); + let events = &interpreter.temporary_local_views.as_ref().unwrap().audit; + let acquired = events + .iter() + .position(|event| { + matches!(event, + storage::AuditEvent::Output { activation: current } if *current == activation) + }) + .unwrap(); + let retired = events.iter().position(|event| matches!(event, + storage::AuditEvent::OwnerRetired { activation: current, .. } if *current == activation)).unwrap(); + assert!(acquired < retired); + let other = self::function(&function.program, "other"); + let foreign = self::function(&program(), "scenario"); + let (expected_activation, expected_function) = match mutation { + 0 => (Some(activation), Some(&function)), + 1 => (None, Some(&function)), + 2 => (Some(activation.checked_add(1).unwrap()), Some(&function)), + 3 => (Some(activation), Some(&other)), + 4 => (Some(activation), Some(&foreign)), + _ => unreachable!(), + }; + let result = + output.into_registered_value(&interpreter, expected_activation, expected_function); + if mutation == 0 { + assert_eq!(result.unwrap(), Value::Nothing); + } else { + assert!(result.is_err()); + } + interpreter.pop_scope(); + interpreter + .temporary_local_views + .as_mut() + .unwrap() + .leave(activation) + .unwrap(); + } + } + + #[test] + fn ordinary_entry_selection_preserves_interface_dispatch_and_refuses_edited_roots() { + let source = SOURCE.replacen( + "namespace app\n", + r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct NamedPacket: + member: Named +"#, + 1, + ) + r#"function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback() returns int64: + return 3 +function dispatched() returns string: + NamedPacket packet = NamedPacket(member: 7) + optional[int64] absent = none + return report(view packet.member, absent handle: + default fallback() + ) +"#; + let file = FileId::new(0); + let program = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(&source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.checked_expression_types = Some(Arc::new( + CheckedExpressionTypes::from_checked_program(&program), + )); + assert!(interpreter.temporary_local_views.is_some()); + let method_name = "int64 as app.Named.name"; + let method = interpreter.functions.get(method_name).unwrap().clone(); + assert!( + receipts::function_receipt( + &program, + method_name, + &method.namespace, + &method.definition, + ) + .unwrap() + .is_none() + ); + assert!( + interpreter + .prepare_temporary_function( + method_name, + &method.namespace, + &method.definition, + None, + &[], + true, + None, + ) + .unwrap() + .is_none() + ); + let original = function(&program, "inspect"); + let mut edited = original.original().unwrap().clone(); + edited.body.stmts.clear(); + assert!( + receipts::function_receipt(&program, "app.inspect", &Some("app".into()), &edited) + .is_err() + ); + // An edited span or namespace cannot turn a claimed root registration + // into an unrelated body and avoid its exact Source authentication. + edited = original.original().unwrap().clone(); + edited.name.span = method.definition.name.span; + assert!(receipts::function_receipt(&program, "app.inspect", &None, &edited).is_err()); + // An ineligible body still cannot receive a temporary frame or borrow + // a source span whose declaration requires the temporary adapter. + let (mut frame_interpreter, caller, activation) = started(); + install(&mut frame_interpreter, &caller); + forwarded(&mut frame_interpreter, &caller); + let frame = frame(&frame_interpreter, &caller); + assert!( + interpreter + .prepare_temporary_function( + method_name, + &method.namespace, + &method.definition, + None, + &[], + true, + Some(&frame), + ) + .is_err() + ); + finish(&mut frame_interpreter, activation); + let mut required = method.definition.as_ref().clone(); + required.body = function(&program, "scenario") + .original() + .unwrap() + .body + .clone(); + assert!( + interpreter + .prepare_temporary_function( + method_name, + &method.namespace, + &required, + None, + &[], + true, + None, + ) + .is_err() + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.audit.is_empty()); + assert_eq!( + interpreter + .call_checked_function_in_namespace(Some("app"), "dispatched", Vec::new()) + .unwrap(), + Value::String("value:7:3".into()) + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!(!context.audit.iter().any(|event| matches!( + event, + storage::AuditEvent::Reserved { .. } | storage::AuditEvent::Acquired { .. } + ))); + } + const MUTUAL_SOURCE: &str = r#"namespace app +struct Packet: + data: bytes + sibling: bytes +mutual: + function make_packet(data: bytes, sibling: bytes) returns Packet + function inspect(view data: bytes, marker: int64) returns int64 + function scenario(data: bytes, sibling: bytes) returns int64 +function make_packet(data: bytes, sibling: bytes) returns Packet: + return Packet(data: data, sibling: sibling) +function inspect(view data: bytes, marker: int64) returns int64: + return marker +function scenario(data: bytes, sibling: bytes) returns int64: + bytes borrowed = view make_packet(data, sibling).data + bytes forwarded = borrowed + return inspect(marker: 7, data: view forwarded) +"#; + + fn mutual_program(release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(MUTUAL_SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap(), + ) + } + + fn mutual_interpreter(program: &Arc) -> Interpreter { + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.checked_expression_types = Some(Arc::new( + CheckedExpressionTypes::from_checked_program(program), + )); + interpreter + } + + #[test] + fn mutual_declarations_join_exact_original_producer_caller_and_borrowed_callee() { + for release in [false, true] { + let program = mutual_program(release); + for name in ["make_packet", "inspect", "scenario"] { + let receipt = function(&program, name); + let original = receipt.original().unwrap(); + let info = program.resolved().scope_table.def(receipt.definition); + assert_ne!(info.span, original.name.span); + assert_eq!( + program.resolved().resolutions.get(&original.name.span), + Some(&receipt.definition) + ); + assert!(std::ptr::eq( + crate::value::closure_capture::original_named_function( + &program, + receipt.definition, + ) + .unwrap(), + original, + )); + assert!( + crate::value::closure_capture::OriginalClosureOwner::registered( + &program, + &info.name, + &original.clone(), + &info.namespace, + ) + .is_some() + ); + } + let mut interpreter = mutual_interpreter(&program); + assert_eq!( + interpreter + .call_checked_function_in_namespace( + Some("app"), + "scenario", + vec![Value::Bytes(vec![65, 66]), Value::Bytes(vec![89, 90])], + ) + .unwrap(), + Value::Int64(7) + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::Acquired { .. })) + .count(), + 1 + ); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::OwnerRetired { .. })) + .count(), + 1 + ); + } + } + + #[test] + fn mutual_definition_authentication_rejects_edited_body_span_header_and_namespace() { + let program = mutual_program(false); + let receipt = function(&program, "make_packet"); + let original = receipt.original().unwrap(); + let info = program.resolved().scope_table.def(receipt.definition); + for mutation in 0..6 { + let mut current = original.clone(); + let mut namespace = info.namespace.clone(); + let mut name = info.name.clone(); + match mutation { + 0 => current.body.stmts.clear(), + 1 => current.name.span = info.span, + 2 => current.params[0].view = true, + 3 => current.return_type = None, + 4 => namespace = Some("other".into()), + 5 => name = "app.inspect".into(), + _ => unreachable!(), + } + assert!( + crate::value::closure_capture::OriginalClosureOwner::registered( + &program, &name, ¤t, &namespace, + ) + .is_none() + ); + assert!(receipts::function_receipt(&program, &name, &namespace, ¤t).is_err()); + } + let mut interpreter = mutual_interpreter(&program); + Arc::make_mut( + &mut interpreter + .functions + .get_mut("app.make_packet") + .unwrap() + .definition, + ) + .body + .stmts + .clear(); + let error = interpreter + .call_checked_function_in_namespace( + Some("app"), + "scenario", + vec![Value::Bytes(vec![65, 66]), Value::Bytes(vec![89, 90])], + ) + .unwrap_err(); + assert_eq!( + error, + "temporary local view function differs from its retained checked Source" + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!( + !context + .audit + .iter() + .any(|event| matches!(event, storage::AuditEvent::Acquired { .. })) + ); + } + const VIEWED_BINDINGS_SOURCE: &str = r#"namespace app +enum Payload: + empty + bytes_value(data: bytes) + items(values: list[bytes]) +function kind(view value: Payload) returns string: + match value: + empty: + return "empty" + bytes_value(data): + return "bytes" + items(values): + return "items" +function count(view values: list[bytes]) returns int64: + mutable int64 seen = 0 + for item in view values: + seen = seen + 1 + return seen +"#; + + const WRITTEN_VIEWS_SOURCE: &str = r#"struct Packet: + data: bytes + sibling: bytes +function make_packet(data: bytes, sibling: bytes) returns Packet: + return Packet(data: data, sibling: sibling) +function temporary(data: bytes, sibling: bytes) returns nothing: + bytes borrowed = view make_packet(data, sibling).data + return nothing +function nested(data: bytes, sibling: bytes) returns nothing: + if true: + bytes borrowed = view make_packet(data, sibling).data + return nothing +"#; + + fn viewed_binding_program(with_temporaries: bool) -> Arc { + let source = if with_temporaries { + format!("{VIEWED_BINDINGS_SOURCE}{WRITTEN_VIEWS_SOURCE}") + } else { + VIEWED_BINDINGS_SOURCE.to_string() + }; + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(&source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ) + } + + #[test] + fn viewed_pattern_and_iteration_binders_do_not_require_temporary_owner_admission() { + for with_temporaries in [false, true] { + let program = viewed_binding_program(with_temporaries); + assert_eq!(Context::needed(&program), with_temporaries); + for name in ["kind", "count"] { + let receipt = function(&program, name); + let body = &receipt.original().unwrap().body; + let viewed_binders: Vec<_> = program + .checked() + .binding_facts + .values() + .filter(|fact| { + fact.declaration_span.file == body.span.file + && fact.declaration_span.start >= body.span.start + && fact.declaration_span.end <= body.span.end + && matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ) + && program.resolved().scope_table.def(fact.definition).kind + == jett_resolve::DefKind::Variable + }) + .collect(); + assert!( + !viewed_binders.is_empty(), + "genuine checked viewed {name} binders" + ); + for fact in viewed_binders { + assert!( + crate::value::closure_capture::original_variable_declaration( + &program, + fact.declaration_span, + ) + .is_none() + ); + } + assert!(!receipt.has_temporary_views().unwrap()); + assert!(!Context::body_needed(&program, Some(body.span))); + } + let mut interpreter = mutual_interpreter(&program); + assert_eq!( + interpreter.temporary_local_views.is_some(), + with_temporaries + ); + for (variant, fields, expected) in [ + ("empty", vec![], "empty"), + ("bytes_value", vec![Value::Bytes(vec![65, 66])], "bytes"), + ( + "items", + vec![Value::List(vec![Value::Bytes(vec![65, 66])])], + "items", + ), + ] { + assert_eq!( + interpreter + .call_function_in_namespace( + Some("app"), + "kind", + vec![Value::Enum { + type_name: "app.Payload".into(), + variant: variant.into(), + fields, + }], + ) + .unwrap(), + Value::String(expected.into()) + ); + } + assert_eq!( + interpreter + .call_function_in_namespace( + Some("app"), + "count", + vec![Value::List(vec![ + Value::Bytes(vec![65]), + Value::Bytes(vec![66]), + ])], + ) + .unwrap(), + Value::Int64(2) + ); + if let Some(context) = &interpreter.temporary_local_views { + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!(context.audit.is_empty()); + } + } + } + + #[test] + fn genuine_written_views_keep_raw_entry_and_unsupported_nested_region_refusals() { + let program = viewed_binding_program(true); + for name in ["temporary", "nested"] { + let receipt = function(&program, name); + assert!(receipt.has_temporary_views().unwrap()); + assert!(Context::body_needed( + &program, + Some(receipt.original().unwrap().body.span), + )); + let mut interpreter = mutual_interpreter(&program); + assert_eq!( + interpreter + .call_function_in_namespace( + Some("app"), + name, + vec![Value::Bytes(vec![65, 66]), Value::Bytes(vec![89, 90])], + ) + .unwrap_err(), + "temporary local view body needs its original checked entry or caller" + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!(context.audit.is_empty()); + } + let mut interpreter = mutual_interpreter(&program); + assert_eq!( + interpreter + .call_checked_function_in_namespace( + Some("app"), + "nested", + vec![Value::Bytes(vec![65, 66]), Value::Bytes(vec![89, 90])], + ) + .unwrap_err(), + "temporary local view nested declaration transport remains unsupported" + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert!(context.aliases.iter().all(HashMap::is_empty)); + assert!( + !context + .audit + .iter() + .any(|event| matches!(event, storage::AuditEvent::Acquired { .. })) + ); + } +} + +impl PreparedNamedFrame { + pub(super) fn alias(&self, parameter: usize) -> Option<&storage::AliasTicket> { + self.aliases.get(parameter).and_then(Option::as_ref) + } + + /// Resolve the checker table by this exact retained target's declaration, + /// which can differ from the later Source body span after mutual. + pub(in crate::interpreter) fn select_registered_projection( + &self, + types: &CheckedExpressionTypes, + name: &str, + namespace: &Option, + current: &FunctionDef, + arguments: &[crate::value::ClosureTypeArgument], + values: &[Value], + ) -> Result>, String> { + self.callee.validate_declaration(name, namespace, current)?; + if !types.matches_original_program(&self.callee.program) { + return Err( + "temporary local view projection belongs to a foreign checked program".into(), + ); + } + let declaration = self + .callee + .program + .resolved() + .scope_table + .def(self.callee.definition) + .span; + let selected = types.select(declaration, arguments, values)?; + self.callee.validate_registered( + name, + namespace, + current, + selected.as_deref(), + arguments, + )?; + Ok(selected) + } + + pub(super) fn validate_target( + &self, + interpreter: &Interpreter, + target: &FunctionReceipt, + ) -> Result<(), String> { + let context = interpreter + .temporary_local_views + .as_ref() + .ok_or("temporary local view call lost its context")?; + let caller = context + .current() + .ok_or("temporary local view call lost its live caller")?; + if !self.callee.same(target) + || !Arc::ptr_eq(&context.program, &target.program) + || caller.id != self.caller_activation + || !caller.root_active + || !caller.function.same(&self.call.caller) + { + return Err("temporary local view rejects a foreign or stale named frame".into()); + } + let original = self.call.caller.original()?; + let calls: Vec<_> = receipts::direct_expressions(&original.body).into_iter().filter(|expression| { + matches!(expression, Expr::Call(_, _, span) | Expr::GenericCall(_, _, _, span) if *span == self.call.span) + }).collect(); + let [Expr::Call(callee, arguments, span) | Expr::GenericCall(callee, _, arguments, span)] = + calls.as_slice() + else { + return Err("temporary local view frame lost its unique original call".into()); + }; + let current = receipts::checked_call(&self.call.caller, callee, arguments, *span)?; + if current.packet != self.call.packet + || current.order != self.call.order + || receipts::named_target(¤t)?.is_none_or(|selected| !selected.same(target)) + || self.aliases.len() != arguments.len() + { + return Err("temporary local view named frame changed its checked join".into()); + } + for (parameter, source) in current.order.iter().copied().enumerate() { + let required = interpreter.temporary_alias(&arguments[source].value)?; + if required.as_ref() != self.alias(parameter) { + return Err( + "temporary local view named frame lost or invented an alias operand".into(), + ); + } + if let Some(ticket) = self.alias(parameter) { + validate_alias(interpreter, ¤t, source, &arguments[source], ticket)?; + validate_parameter(target, parameter, ticket)?; + } + } + Ok(()) + } + + pub(super) fn validate_values( + &self, + interpreter: &Interpreter, + function: &FunctionDef, + values: &[Value], + sources: Option<&[Option]>, + ) -> Result<(), String> { + // This second check runs before normalization or publication. The callee + // activation may be entered, so validate the recorded caller by ID rather + // than relying on the current stack top. + let context = interpreter + .temporary_local_views + .as_ref() + .ok_or("temporary local view call lost its context")?; + if !std::ptr::eq(function, self.callee.original()?) + || values.len() != self.aliases.len() + || values.len() != function.params.len() + || sources.is_none_or(|sources| sources.len() != values.len()) + || !context.activations.iter().any(|caller| { + caller.id == self.caller_activation + && caller.root_active + && caller.function.same(&self.call.caller) + }) + || context + .current() + .is_none_or(|callee| callee.root_active || !callee.function.same(&self.callee)) + || context + .activations + .len() + .checked_sub(2) + .and_then(|index| context.activations.get(index)) + .is_none_or(|caller| caller.id != self.caller_activation) + { + return Err("temporary local view rejects a malformed parameter frame".into()); + } + for (parameter, value) in values.iter().enumerate() { + if let Some(ticket) = self.alias(parameter) { + if !matches!(value, Value::Nothing) { + return Err( + "temporary local view rejects a payload-bearing view parameter".into(), + ); + } + validate_parameter(&self.callee, parameter, ticket)?; + context.observe(ticket)?; + } + } + Ok(()) + } +} + +fn validate_parameter( + function: &FunctionReceipt, + parameter: usize, + ticket: &storage::AliasTicket, +) -> Result<(), String> { + let original = function.original()?; + let parameter = original + .params + .get(parameter) + .ok_or("temporary local view parameter is absent")?; + let fact = function.fact(¶meter.name)?; + let info = function.program.resolved().scope_table.def(fact.definition); + if !parameter.view + || parameter.mutable + || fact.mutable + || info.kind != jett_resolve::DefKind::Param + || fact.ty != ticket.endpoint + || !matches!( + function.program.checked().interner.resolve(fact.ty), + Type::Bytes | Type::List(_) + ) + || !matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ) + { + return Err("temporary local view parameter changed its exact borrowed contract".into()); + } + Ok(()) +} + +fn validate_alias( + interpreter: &Interpreter, + call: &receipts::CallReceipt, + source: usize, + argument: &CallArg, + ticket: &storage::AliasTicket, +) -> Result<(), String> { + let fact = call + .packet + .arguments + .get(source) + .ok_or("temporary local view argument fact is absent")?; + let Expr::View(inner, _) = receipts::unparen(&argument.value) else { + return Err("temporary local view forwarding call needs its written view".into()); + }; + let Expr::Ident(root) = receipts::unparen(inner) else { + return Err("temporary local view call projection transport remains unsupported".into()); + }; + let definition = call + .caller + .program + .resolved() + .resolutions + .get(&root.span) + .ok_or("temporary local view argument lost its resolved binding use")?; + let info = call.caller.program.resolved().scope_table.def(*definition); + let facts = call.caller.facts()?; + let source_fact = facts + .binding_facts + .get(&info.span) + .copied() + .ok_or("temporary local view argument lost its checked binding declaration")?; + if source_fact.definition != *definition + || source_fact.declaration_span != info.span + || info.name != root.name + { + return Err("temporary local view argument changed its use/declaration join".into()); + } + if fact.actual_type != ticket.endpoint + || fact.parameter_type != ticket.endpoint + || fact.syntax != CheckedCallerSyntax::WrittenView + || fact.callee_access != CheckedCalleeAccess::View + || fact.effect != CheckedCallerEffect::RetainBorrow + || fact.origin != CheckedCallerOrigin::Binding(source_fact) + || !matches!(source_fact.mode, CheckedBindingMode::View { .. }) + || call.caller.program.resolved().resolutions.get(&root.span) + != Some(&source_fact.definition) + { + return Err("temporary local view argument lost its original alias disposition".into()); + } + let current = interpreter + .temporary_alias(&argument.value)? + .ok_or("temporary local view argument lost its live alias")?; + if ¤t != ticket { + return Err("temporary local view argument changed its owner identity or path".into()); + } + interpreter + .temporary_local_views + .as_ref() + .ok_or("temporary local view call lost its context")? + .observe(ticket)?; + Ok(()) +} + +impl Interpreter { + pub(in crate::interpreter) fn eval_temporary_call( + &mut self, + callee: &Expr, + type_args: &[TypeExpr], + arguments: &[CallArg], + span: Span, + ) -> Result, String> { + let Some(context) = &self.temporary_local_views else { + return Ok(None); + }; + let Some(caller) = context.current() else { + return Ok(None); + }; + if !caller.root_active { + return Ok(None); + } + let caller_activation = caller.id; + let function = caller.function.clone(); + self.temporary_program()?; + // No authority is inferred for nested/control-flow regions. An alias + // encountered through those paths is refused by its read adapter. + if function.source_call(callee, arguments, span).is_err() { + return Ok(None); + } + let call = receipts::checked_call(&function, callee, arguments, span)?; + let mut lexical_aliases = Vec::with_capacity(arguments.len()); + for (source, argument) in arguments.iter().enumerate() { + let alias = self.temporary_alias(&argument.value)?; + if let Some(ticket) = &alias { + validate_alias(self, &call, source, argument, ticket)?; + } + lexical_aliases.push(alias); + } + let any_alias = lexical_aliases.iter().any(Option::is_some); + if let CheckedInvocationTarget::Intrinsic(intrinsic) = &call.packet.target { + return if any_alias { + self.eval_temporary_observer(&call, *intrinsic, arguments, lexical_aliases) + .map(Some) + } else { + Ok(None) + }; + } + let Some(target) = receipts::named_target(&call)? else { + return if any_alias { + Err("temporary local view callable transport remains unsupported".into()) + } else { + Ok(None) + }; + }; + // Resolve in the caller before independent operand effects. The exact + // checked target remains the authority; spelling only supplies its + // Source-aware runtime type projection and reflection metadata. + let resolved_type_args = self + .inferred_user_function_type_args(callee, type_args, arguments, Some(&call.order)) + .unwrap_or_else(|| { + type_args + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect() + }); + let captured = self.captured_type_arguments(&resolved_type_args); + let info = target.program.resolved().scope_table.def(target.definition); + let name = info.name.clone(); + let registered = self + .functions + .get(&name) + .ok_or("temporary local view named callee is not registered")? + .clone(); + target.validate_declaration(&name, ®istered.namespace, ®istered.definition)?; + if self.is_value_call_target(receipts::unparen(callee)) { + return Err("temporary local view call changed its registered named target".into()); + } + let mut lexical = Vec::with_capacity(arguments.len()); + for (argument, alias) in arguments.iter().zip(lexical_aliases) { + let source_type = self + .checked_refinement_source_type(argument.value.span()) + .cloned(); + let value = match &alias { + Some(_) => Value::Nothing, + None => match self.eval_expr_flow(&argument.value)? { + ExprFlow::Value(value) => value, + ExprFlow::Signal(signal) => return Ok(Some(ExprFlow::Signal(signal))), + ExprFlow::Resource(_) => { + return Err( + "temporary local view named frame cannot carry Resource custody".into(), + ); + } + }, + }; + lexical.push(Some((value, source_type, alias))); + } + let mut values = Vec::with_capacity(lexical.len()); + let mut source_types = Vec::with_capacity(lexical.len()); + let mut aliases = Vec::with_capacity(lexical.len()); + for source in &call.order { + let (value, source_type, alias) = lexical + .get_mut(*source) + .and_then(Option::take) + .ok_or("temporary local view call repeats or loses a lexical argument")?; + values.push(value); + source_types.push(source_type); + aliases.push(alias); + } + let frame = PreparedNamedFrame { + caller_activation, + call, + callee: target, + aliases, + }; + frame.select_registered_projection( + self.checked_expression_types + .as_ref() + .ok_or("temporary local view lost its checked type projection")?, + &name, + ®istered.namespace, + ®istered.definition, + &captured, + &values, + )?; + frame.validate_target(self, &frame.callee)?; + Ok(Some(ExprFlow::Value( + self.call_registered_function_with_temporary_frame( + &name, + &resolved_type_args, + values, + Some(&source_types), + Some(frame), + )?, + ))) + } + + fn eval_temporary_observer( + &mut self, + call: &receipts::CallReceipt, + intrinsic: IntrinsicId, + arguments: &[CallArg], + aliases: Vec>, + ) -> Result { + if !self.current_function_trusted_stdlib + || !matches!( + intrinsic, + IntrinsicId::BytesLength + | IntrinsicId::BytesToHex + | IntrinsicId::BytesGet + | IntrinsicId::ListLength + | IntrinsicId::ListGetClone + | IntrinsicId::ListSum + ) + { + return Err( + "temporary local view observer transport remains unsupported for this intrinsic" + .into(), + ); + } + let CheckedInvocationShape::Intrinsic { .. } = &call.packet.shape else { + return Err("temporary local view observer has no intrinsic packet".into()); + }; + let indexed = matches!(intrinsic, IntrinsicId::BytesGet | IntrinsicId::ListGetClone); + let expected_arity = if indexed { 2 } else { 1 }; + if arguments.len() != expected_arity + || call.order != (0..expected_arity).collect::>() + || aliases.first().and_then(Option::as_ref).is_none() + || aliases.iter().skip(1).any(Option::is_some) + { + return Err( + "temporary local view byte observer changed its exact operand frame".into(), + ); + } + let ticket = aliases[0] + .as_ref() + .ok_or("temporary local view byte observer lost its alias")?; + // Evaluate independent operands in original lexical order. Borrow the + // bytes only afterward; the evaluator never receives an owned snapshot. + let index = if indexed { + match self.eval_expr_flow(&arguments[1].value)? { + ExprFlow::Value(Value::Int64(index)) => Some(index), + ExprFlow::Signal(signal) => return Ok(ExprFlow::Signal(signal)), + _ => { + return Err( + "temporary local view indexed observer expects an int64 index".into(), + ); + } + } + } else { + None + }; + let context = self + .temporary_local_views + .as_ref() + .ok_or("temporary local view observer lost its context")?; + let observed = context.observe(ticket)?; + let result = if matches!( + intrinsic, + IntrinsicId::ListLength | IntrinsicId::ListGetClone | IntrinsicId::ListSum + ) { + self.observe_temporary_list(call, intrinsic, ticket, observed, index)? + } else { + let Value::Bytes(bytes) = observed else { + return Err("temporary local view observer lost its exact bytes payload".into()); + }; + match intrinsic { + IntrinsicId::BytesLength => Value::Int64( + i64::try_from(bytes.len()).map_err(|_| "bytes.length exceeds int64")?, + ), + IntrinsicId::BytesToHex => { + Value::String(bytes.iter().map(|byte| format!("{byte:02x}")).collect()) + } + IntrinsicId::BytesGet => match index + .and_then(|index| usize::try_from(index).ok()) + .and_then(|index| bytes.get(index)) + { + Some(byte) => Value::OptionalSome(Box::new(Value::Int64(i64::from(*byte)))), + None => Value::OptionalNone, + }, + _ => return Err("temporary local view observer escaped its closed dispatch".into()), + } + }; + Ok(ExprFlow::Value(result)) + } + + fn observe_temporary_list( + &self, + call: &receipts::CallReceipt, + intrinsic: IntrinsicId, + ticket: &storage::AliasTicket, + observed: &Value, + index: Option, + ) -> Result { + let types = &call.caller.program.checked().interner; + let Type::List(inner) = types.resolve(ticket.endpoint) else { + return Err("temporary local view list observer lost its exact list endpoint".into()); + }; + let facts = call.caller.facts()?; + if facts + .intrinsic_type_arguments + .get(&call.span) + .map(Vec::as_slice) + != Some(std::slice::from_ref(inner)) + { + return Err( + "temporary local view list observer changed its checked element operand".into(), + ); + } + let Value::List(items) = observed else { + return Err("temporary local view observer lost its exact list payload".into()); + }; + match intrinsic { + IntrinsicId::ListLength => Ok(Value::Int64( + i64::try_from(items.len()).map_err(|_| "list.length exceeds int64")?, + )), + IntrinsicId::ListGetClone if receipts::copyable(types, *inner) => { + // This bounded observation copies only checked copyable items; + // it never produces an owned snapshot of the backing list. + Ok( + match index + .and_then(|index| usize::try_from(index).ok()) + .and_then(|index| items.get(index)) + { + Some(item) => Value::OptionalSome(Box::new(item.clone())), + None => Value::OptionalNone, + }, + ) + } + IntrinsicId::ListGetClone => Err( + "temporary local view move-only list.get result transport remains unsupported" + .into(), + ), + IntrinsicId::ListSum => { + let primitive = match types.resolve(*inner) { + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 => types.type_name(*inner), + _ => { + return Err( + "temporary local view list.sum lost its checked numeric domain".into(), + ); + } + }; + self.sum_primitive_list("list.__sum", &primitive, items) + } + _ => Err("temporary local view list observer escaped its closed dispatch".into()), + } + } +} diff --git a/crates/jett_comptime/src/interpreter/temporary_local_views/list_tests.rs b/crates/jett_comptime/src/interpreter/temporary_local_views/list_tests.rs new file mode 100644 index 00000000..12db0568 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/temporary_local_views/list_tests.rs @@ -0,0 +1,269 @@ +//! Checked Source controls for borrowed List transport; no native execution credit. +use super::*; +use jett_common::{FileId, STDLIB_FILE_ID_START, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +const SOURCE: &str = r#"namespace app +struct Packet: + left: list[int64] + right: list[string] +function make_packet() returns Packet: + return Packet(left: list(2, 3, 5), right: list("Ada", "Lin")) +mutual: + function observe[T](view items: list[T]) returns int64 +function observe[T](view items: list[T]) returns int64: + return list.length[T](view items) +function scenario() returns int64: + list[int64] numbers = view make_packet().left + list[int64] forwarded = numbers + int64 length = observe[int64](view forwarded) + int64 inferred = observe(view numbers) + int64 total = list.sum[int64](view numbers) + bool empty = list.is_empty[int64](view forwarded) + optional[int64] first = list.get[int64](view numbers, 0) + optional[int64] absent = list.get[int64](view numbers, -1) + list[int64] copied = clone numbers + int64 copied_total = list.sum[int64](view copied) + return length + inferred + total + copied_total +function names() returns int64: + list[string] values = view make_packet().right + optional[string] first = list.get[string](view values, 0) + return observe[string](view values) +function copied_first() returns optional[string]: + list[string] values = view make_packet().right + return list.get[string](view values, 0) +function absent() returns optional[int64]: + list[int64] values = view make_packet().left + return list.get[int64](view values, -1) +"#; + +fn program() -> Arc { + let project = FileId::new(0); + let stdlib = FileId::new(STDLIB_FILE_ID_START); + let mut module = jett_parser::parse( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../stdlib/list.jett" + )), + stdlib, + ); + // Retain actual stdlib declarations and their complete bodies. Unrelated + // list algorithms need other stdlib namespaces and are outside this control. + assert!(module.errors.is_empty(), "{:?}", module.errors); + module.module.items.retain(|item| match item { + Item::Namespace(_) => true, + Item::Function(function) => matches!( + function.name.name.as_str(), + "length" | "get" | "is_empty" | "sum" + ), + _ => false, + }); + let project_source = jett_parser::parse(SOURCE, project); + assert!( + project_source.errors.is_empty(), + "{:?}", + project_source.errors + ); + module.module.items.extend(project_source.module.items); + Arc::new( + CheckedResourceProgram::prepare( + module, + HashMap::from([ + (project, SourceOrigin::Project), + (stdlib, SourceOrigin::Stdlib), + ]), + &[], + CheckOptions::default(), + ) + .expect("checked temporary List Source"), + ) +} + +fn interpreter(program: &Arc) -> Interpreter { + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + crate::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.checked_expression_types = Some(Arc::new( + CheckedExpressionTypes::from_checked_program(program), + )); + interpreter +} + +fn function(program: &Arc, name: &str) -> FunctionReceipt { + let source = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .unwrap(); + receipts::function_receipt(program, &format!("app.{name}"), &Some("app".into()), source) + .unwrap() + .unwrap() +} + +fn calls(function: &FunctionReceipt) -> Vec { + receipts::direct_expressions(&function.original().unwrap().body) + .into_iter() + .filter_map(|expression| match expression { + Expr::Call(callee, arguments, span) | Expr::GenericCall(callee, _, arguments, span) => { + Some(receipts::checked_call(function, callee, arguments, *span).unwrap()) + } + _ => None, + }) + .collect() +} + +#[test] +fn checked_list_views_forward_to_generic_observers_without_snapshotting_owner() { + let program = program(); + let mut interpreter = interpreter(&program); + assert_eq!( + interpreter.call_checked_function_in_namespace(Some("app"), "scenario", vec![]), + Ok(Value::Int64(26)) + ); + let context = interpreter.temporary_local_views.as_ref().unwrap(); + assert!(context.activations.is_empty()); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::Acquired { .. })) + .count(), + 1 + ); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::OwnerRetired { .. })) + .count(), + 1 + ); + assert_eq!( + context + .audit + .iter() + .filter(|event| matches!(event, storage::AuditEvent::ExplicitClone { .. })) + .count(), + 1 + ); +} + +#[test] +fn checked_string_list_get_observes_copyable_items_in_its_exact_generic_body() { + let program = program(); + let mut interpreter = interpreter(&program); + assert_eq!( + interpreter.call_checked_function_in_namespace(Some("app"), "names", vec![]), + Ok(Value::Int64(2)) + ); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .activations + .is_empty() + ); +} + +#[test] +fn generic_receipts_separate_same_template_specializations_and_foreign_programs() { + let program = program(); + let scenario = function(&program, "scenario"); + let names = function(&program, "names"); + let observer = |function: &FunctionReceipt| { + calls(function) + .into_iter() + .find_map(|call| { + let target = receipts::named_target(&call).unwrap()?; + (target.original().unwrap().name.name == "observe").then_some(target) + }) + .unwrap() + }; + let numbers = observer(&scenario); + let strings = observer(&names); + assert!(std::ptr::eq( + numbers.original().unwrap(), + strings.original().unwrap() + )); + for receipt in [&numbers, &strings] { + let original = receipt.original().unwrap(); + let info = program.resolved().scope_table.def(receipt.definition); + assert_ne!(info.span, original.name.span); + assert_eq!( + program.resolved().resolutions.get(&original.name.span), + Some(&receipt.definition) + ); + } + assert!(!numbers.same(&strings)); + assert!(!storage::same_function(&numbers, &strings).unwrap()); + let foreign = self::program(); + let other = observer(&function(&foreign, "scenario")); + assert!(!numbers.same(&other)); + assert!(!storage::same_function(&numbers, &other).unwrap()); + let source = numbers.original().unwrap(); + let info = program.resolved().scope_table.def(numbers.definition); + assert!( + numbers + .validate_registered(&info.name, &info.namespace, source, None, &[]) + .is_err() + ); +} + +#[test] +fn exact_generic_call_rejects_changed_specialization_before_frame_publication() { + let program = program(); + let scenario = function(&program, "scenario"); + let mut call = calls(&scenario) + .into_iter() + .find(|call| { + matches!( + call.packet.target, + jett_typecheck::CheckedInvocationTarget::Generic(_) + ) + }) + .unwrap(); + let jett_typecheck::CheckedInvocationTarget::Generic(target) = &mut call.packet.target else { + unreachable!() + }; + target + .specialization + .type_argument_kinds + .push("foreign".into()); + assert!(receipts::named_target(&call).is_err()); + assert!( + interpreter(&program) + .temporary_local_views + .unwrap() + .audit + .is_empty() + ); +} + +#[test] +fn checked_list_get_returns_owned_copyable_output_and_preserves_bounds() { + let program = program(); + let mut interpreter = interpreter(&program); + assert_eq!( + interpreter.call_checked_function_in_namespace(Some("app"), "copied_first", vec![]), + Ok(Value::OptionalSome(Box::new(Value::String("Ada".into())))) + ); + assert_eq!( + interpreter.call_checked_function_in_namespace(Some("app"), "absent", vec![]), + Ok(Value::OptionalNone) + ); + assert!( + interpreter + .temporary_local_views + .as_ref() + .unwrap() + .activations + .is_empty() + ); +} diff --git a/crates/jett_comptime/src/interpreter/temporary_local_views/receipts.rs b/crates/jett_comptime/src/interpreter/temporary_local_views/receipts.rs new file mode 100644 index 00000000..88812a76 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/temporary_local_views/receipts.rs @@ -0,0 +1,885 @@ +//! Original ordinary function, declaration and producer identity. No runtime ownership. +use super::*; +use jett_parser::ast::{CallArg, StringPart, VarDecl}; +use jett_typecheck::{ + CheckedBindingFact, CheckedBindingMode, CheckedInvocationShape, CheckedInvocationTarget, + CheckedViewSource, +}; +use jett_types::{Type, TypeId}; + +#[derive(Clone)] +pub(in crate::interpreter) struct FunctionReceipt { + pub(super) program: Arc, + pub(super) item: usize, + pub(super) definition: DefId, + /// Exact retained concrete body, never selected by span or runtime payload. + pub(super) instance: Option, +} + +impl FunctionReceipt { + pub(super) fn same(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.program, &other.program) + && self.item == other.item + && self.definition == other.definition + && self.instance == other.instance + } + + pub(super) fn fact(&self, name: &Ident) -> Result { + let fact = self + .facts()? + .binding_facts + .get(&name.span) + .copied() + .ok_or("temporary local view has no checked binding fact")?; + let info = self.program.resolved().scope_table.def(fact.definition); + if info.span != name.span + || info.name != name.name + || fact.declaration_span != name.span + || !matches!( + info.kind, + jett_resolve::DefKind::Variable | jett_resolve::DefKind::Param + ) + { + return Err("temporary local view changed its binding declaration".into()); + } + Ok(fact) + } + + pub(super) fn has_temporary_views(&self) -> Result { + Ok(storage::temporary_facts( + &self.program, + self.facts()?.binding_facts, + Some(self.original()?.body.span), + )) + } + pub(in crate::interpreter) fn original(&self) -> Result<&FunctionDef, String> { + let Some(Item::Function(function)) = self.program.module().items.get(self.item) else { + return Err("temporary local view lost its original function".into()); + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| info.id == self.definition) + .ok_or("temporary local view definition is absent from its program")?; + let qualified = info.namespace.as_ref().map_or_else( + || function.name.name.clone(), + |namespace| format!("{namespace}.{}", function.name.name), + ); + if info.kind != jett_resolve::DefKind::Function + || !is_root_definition(&self.program, self.item, self.definition) + || info.name != qualified + { + return Err("temporary local view changed its retained declaration identity".into()); + } + let checked = self.program.checked(); + match self.instance { + Some(index) => { + let instance = checked + .generic_function_instantiations + .get(index) + .ok_or("temporary local view lost its concrete generic body")?; + if instance.definition != self.definition + || instance.concrete_args.len() != function.type_params.len() + || instance.parameter_types.len() != function.params.len() + || function.type_params.is_empty() + { + return Err("temporary local view changed its exact concrete body".into()); + } + } + None => { + let signature = checked + .definition_types + .get(&self.definition) + .ok_or("temporary local view lost its checked function signature")?; + let Type::Function { + params, + view_params, + .. + } = checked.interner.resolve(*signature) + else { + return Err("temporary local view has an invalid function signature".into()); + }; + if !function.type_params.is_empty() + || params.len() != function.params.len() + || view_params.len() != function.params.len() + || function + .params + .iter() + .zip(view_params) + .any(|(source, view)| source.view != *view) + { + return Err( + "temporary local view changed its original parameter contract".into(), + ); + } + } + } + Ok(function) + } + + pub(super) fn expression(&self, expression: &Expr) -> Result { + let expressions = direct_expressions(&self.original()?.body); + let indices: Vec<_> = expressions + .iter() + .enumerate() + .filter_map(|(index, original)| std::ptr::eq(*original, expression).then_some(index)) + .collect(); + match indices.as_slice() { + [index] => Ok(*index), + _ => Err("temporary local view needs its exact original root-body occurrence".into()), + } + } + + pub(super) fn source_call( + &self, + callee: &Expr, + arguments: &[CallArg], + span: Span, + ) -> Result<&jett_typecheck::CheckedCallOwnership, String> { + let found: Vec<_> = direct_expressions(&self.original()?.body) + .into_iter() + .filter(|source| { + matches!(source, Expr::Call(original, actuals, original_span) + | Expr::GenericCall(original, _, actuals, original_span) + if *original_span == span && std::ptr::eq(original.as_ref(), callee) + && actuals.len() == arguments.len() + && (actuals.is_empty() || std::ptr::eq(actuals.as_ptr(), arguments.as_ptr()))) + }) + .collect(); + if found.len() != 1 { + return Err("temporary local view call has no original checked occurrence".into()); + } + self.facts()? + .call_ownership + .get(&span) + .ok_or_else(|| "temporary local view call has no checked caller packet".into()) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub(super) struct Projection { + pub(super) owner: TypeId, + pub(super) span: Span, + pub(super) name: String, + pub(super) fields: Vec, + pub(super) index: usize, + pub(super) endpoint: TypeId, +} + +pub(super) struct ProducerReceipt { + pub(super) function: FunctionReceipt, + pub(super) declaration: usize, + pub(super) fact: CheckedBindingFact, + pub(super) whole: TypeId, + pub(super) path: Vec, +} + +impl ProducerReceipt { + pub(super) fn producer(&self) -> Result<&Expr, String> { + let Some(Stmt::VarDecl(declaration)) = + self.function.original()?.body.stmts.get(self.declaration) + else { + return Err("temporary local view lost its declaration".into()); + }; + let Expr::View(inner, _) = unparen(&declaration.value) else { + return Err("temporary local view lost its written view".into()); + }; + Ok(projection_root(inner)) + } +} + +pub(super) fn function_receipt( + program: &Arc, + name: &str, + namespace: &Option, + current: &FunctionDef, +) -> Result, String> { + if !current.type_params.is_empty() { + return Ok(None); + } + // This receipt family covers ordinary root declarations only. A method or + // generated body receives no activation; its caller still rejects required + // temporary body/frame transport. Names can only select a refusal here. + let items: Vec<_> = program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| { + matches!(item, Item::Function(function) if function.name.span == current.name.span) + .then_some(index) + }) + .collect(); + if items.is_empty() { + let claims_root = program + .resolved() + .scope_table + .definitions + .iter() + .any(|info| { + info.kind == jett_resolve::DefKind::Function + && info.name == name + && program + .module() + .items + .iter() + .enumerate() + .any(|(item, _)| is_root_definition(program, item, info.id)) + }); + return if claims_root { + Err("temporary local view function differs from its retained checked Source".into()) + } else { + Ok(None) + }; + } + let [item] = items.as_slice() else { + return Err("temporary local view has ambiguous original function storage".into()); + }; + if crate::value::closure_capture::OriginalClosureOwner::registered( + program, name, current, namespace, + ) + .is_none() + { + return Err( + "temporary local view function differs from its retained checked Source".into(), + ); + } + let definitions: Vec<_> = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == jett_resolve::DefKind::Function + && info.name == name + && is_root_definition(program, *item, info.id) + && &info.namespace == namespace + }) + .map(|info| info.id) + .collect(); + let [definition] = definitions.as_slice() else { + return Err("temporary local view function is not an exact checked declaration".into()); + }; + let receipt = FunctionReceipt { + program: program.clone(), + item: *item, + definition: *definition, + instance: None, + }; + receipt.original()?; + Ok(Some(receipt)) +} + +// Metadata lookup alone grants no activation. Every receipt still authenticates +// the complete registered Source definition, exact namespace, and checked signature. +fn is_root_definition( + program: &jett_typecheck::CheckedResourceProgram, + item: usize, + definition: DefId, +) -> bool { + let Some(Item::Function(function)) = program.module().items.get(item) else { + return false; + }; + crate::value::closure_capture::original_named_function(program, definition) + .is_some_and(|original| std::ptr::eq(original, function)) +} + +pub(super) fn producer_receipt( + function: &FunctionReceipt, + declaration: &VarDecl, +) -> Result, String> { + let checked = function.program.checked(); + let Some(fact) = function + .facts()? + .binding_facts + .get(&declaration.name.span) + .copied() + else { + return Ok(None); + }; + if !matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ) { + return Ok(None); + } + let indices: Vec<_> = function + .original()? + .body + .stmts + .iter() + .enumerate() + .filter_map(|(index, source)| { + matches!(source, Stmt::VarDecl(original) if std::ptr::eq(original, declaration)) + .then_some(index) + }) + .collect(); + let [index] = indices.as_slice() else { + return Err("temporary local view nested declaration transport remains unsupported".into()); + }; + let info = function.program.resolved().scope_table.def(fact.definition); + if declaration.mutable + || fact.mutable + || fact.declaration_span != declaration.name.span + || info.id != fact.definition + || info.span != declaration.name.span + || info.kind != jett_resolve::DefKind::Variable + { + return Err("temporary local view changed its exact immutable binding".into()); + } + let Expr::View(inner, _) = unparen(&declaration.value) else { + return Err("temporary local view needs its original written view".into()); + }; + let producer = projection_root(inner); + let Expr::Call(callee, arguments, span) = producer else { + return Err("temporary local view producer transport remains unsupported".into()); + }; + let call = checked_call(function, callee, arguments, *span)?; + let packet = &call.packet; + named_target(&call)? + .ok_or("temporary local view needs an ordinary non-generic named producer")?; + let CheckedInvocationTarget::Resolved(target) = packet.target else { + return Err("temporary local view needs a direct ordinary named producer".into()); + }; + let CheckedInvocationShape::Function { signature_type } = packet.shape else { + return Err("temporary local view producer is not a checked Source function".into()); + }; + let target_info = function.program.resolved().scope_table.def(target); + let Type::Function { return_type, .. } = checked.interner.resolve(signature_type) else { + return Err("temporary local view producer has no checked return contract".into()); + }; + if target_info.kind != jett_resolve::DefKind::Function + || checked.definition_types.get(&target) != Some(&signature_type) + || function + .program + .resolved() + .resolutions + .get(&unparen(callee).span()) + != Some(&target) + || function.facts()?.type_map.get(span) != Some(return_type) + || !ordinary_type(&checked.interner, *return_type, &mut Vec::new()) + || !matches!(checked.interner.resolve(*return_type), Type::Struct(_)) + { + return Err("temporary local view producer lacks its exact ordinary owned return".into()); + } + let mut path = Vec::new(); + let endpoint = checked_path(function, inner, &mut path)?; + if path.is_empty() || endpoint != fact.ty { + return Err("temporary local view changed its checked projection endpoint".into()); + } + Ok(Some(ProducerReceipt { + function: function.clone(), + declaration: *index, + fact, + whole: *return_type, + path, + })) +} + +pub(super) fn checked_path( + function: &FunctionReceipt, + expression: &Expr, + path: &mut Vec, +) -> Result { + let checked = function.program.checked(); + let expression = unparen(expression); + if let Expr::FieldAccess(base, field, span) = expression { + let owner = checked_path(function, base, path)?; + let Type::Struct(id) = checked.interner.resolve(owner) else { + return Err("temporary local view needs finite ordinary struct projections".into()); + }; + let structure = checked.interner.resolve_struct(*id); + if !checked.interner.nominal_type_arguments(owner).is_empty() { + return Err( + "temporary local view generic projection transport remains unsupported".into(), + ); + } + let index = structure + .fields + .iter() + .position(|(name, _)| name == &field.name) + .ok_or("temporary local view field is absent from its checked owner")?; + let endpoint = structure.fields[index].1; + if function.facts()?.type_map.get(span) != Some(&endpoint) { + return Err("temporary local view changed its field type".into()); + } + path.push(Projection { + owner, + span: *span, + name: structure.name.clone(), + fields: structure + .fields + .iter() + .map(|(name, _)| name.clone()) + .collect(), + index, + endpoint, + }); + Ok(endpoint) + } else { + function + .facts()? + .type_map + .get(&expression.span()) + .copied() + .ok_or_else(|| "temporary local view expression has no original checked type".into()) + } +} + +pub(super) struct CallReceipt { + pub(super) caller: FunctionReceipt, + pub(super) span: Span, + pub(super) packet: jett_typecheck::CheckedCallOwnership, + /// Parameter order -> lexical source index, checked before any argument effects. + pub(super) order: Vec, +} + +pub(super) fn checked_call( + function: &FunctionReceipt, + callee: &Expr, + arguments: &[CallArg], + span: Span, +) -> Result { + use jett_typecheck::{CheckedCalleeAccess, CheckedCallerSyntax, CheckedOwnershipContext}; + let packet = function.source_call(callee, arguments, span)?; + let checked = function.program.checked(); + if packet.context != CheckedOwnershipContext::Ordinary + || packet.arguments.len() != arguments.len() + { + return Err("temporary local view call changed its original argument frame".into()); + } + let (parameters, views, result) = match &packet.shape { + CheckedInvocationShape::Function { signature_type } => { + let Type::Function { + params, + view_params, + return_type, + .. + } = checked.interner.resolve(*signature_type) + else { + return Err("temporary local view call lost its checked signature".into()); + }; + if let CheckedInvocationTarget::Resolved(target) = &packet.target { + if checked.definition_types.get(target) != Some(signature_type) + || function + .program + .resolved() + .resolutions + .get(&unparen(callee).span()) + != Some(target) + { + return Err("temporary local view call changed its resolved target".into()); + } + } + if let CheckedInvocationTarget::Generic(target) = &packet.target { + if function.facts()?.generic_calls.get(&span) != Some(target) + || function + .program + .resolved() + .resolutions + .get(&unparen(callee).span()) + != Some(&target.definition) + { + return Err( + "temporary local view generic call changed its checked target".into(), + ); + } + let instance = exact_instance(&function.program, target)?; + let original = &checked.generic_function_instantiations[instance]; + let source = retained_function(&function.program, target.definition)?.1; + if params != &original.parameter_types + || *return_type != original.return_type + || source.params.len() != view_params.len() + || source + .params + .iter() + .zip(view_params) + .any(|(parameter, view)| parameter.view != *view) + { + return Err( + "temporary local view generic call changed its concrete signature".into(), + ); + } + } + (params.clone(), view_params.clone(), *return_type) + } + CheckedInvocationShape::Intrinsic { + intrinsic, + result_type, + operands, + } => { + if packet.target != CheckedInvocationTarget::Intrinsic(*intrinsic) + || function.facts()?.intrinsic_ids.get(&span) != Some(intrinsic) + { + return Err("temporary local view observer changed its closed intrinsic".into()); + } + ( + operands.iter().map(|role| role.ty()).collect(), + operands + .iter() + .enumerate() + .map(|(parameter, role)| match role { + jett_typecheck::CheckedIntrinsicOperandRole::View { .. } => true, + jett_typecheck::CheckedIntrinsicOperandRole::PrintArgument { .. } + if matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) => + { + packet + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .is_some_and(|fact| fact.syntax == CheckedCallerSyntax::WrittenView) + } + _ => false, + }) + .collect(), + *result_type, + ) + } + }; + if parameters.len() != arguments.len() + || views.len() != arguments.len() + || function.facts()?.type_map.get(&span) != Some(&result) + { + return Err("temporary local view call changed its checked arity or result".into()); + } + let order = function + .facts()? + .call_argument_orders + .get(&span) + .map(|order| order.source_indices.clone()) + .unwrap_or_else(|| (0..arguments.len()).collect()); + if order.len() != arguments.len() { + return Err("temporary local view call has an incomplete permutation".into()); + } + let mut seen = vec![false; arguments.len()]; + for (source, (argument, fact)) in arguments.iter().zip(&packet.arguments).enumerate() { + let parameter = fact.parameter_index; + if fact.source_index != source + || fact.source_span != argument.value.span() + || parameter >= parameters.len() + || seen[parameter] + || order.get(parameter) != Some(&source) + || function + .facts()? + .source_type_map + .get(&argument.value.span()) + != Some(&fact.actual_type) + || fact.parameter_type != parameters[parameter] + || (fact.callee_access == CheckedCalleeAccess::View) != views[parameter] + || (fact.syntax == CheckedCallerSyntax::WrittenView) + != matches!(unparen(&argument.value), Expr::View(_, _)) + { + return Err("temporary local view call changed an original lexical/formal join".into()); + } + seen[parameter] = true; + } + Ok(CallReceipt { + caller: function.clone(), + span, + packet: packet.clone(), + order, + }) +} + +pub(super) fn named_target(call: &CallReceipt) -> Result, String> { + let (definition, instance) = match &call.packet.target { + CheckedInvocationTarget::Resolved(definition) => (*definition, None), + CheckedInvocationTarget::Generic(target) => ( + target.definition, + Some(exact_instance(&call.caller.program, target)?), + ), + _ => return Ok(None), + }; + let info = call.caller.program.resolved().scope_table.def(definition); + if info.kind != jett_resolve::DefKind::Function { + return Ok(None); + } + let (item, source) = retained_function(&call.caller.program, definition)?; + if source.type_params.is_empty() != instance.is_none() { + return Err("temporary local view target changed its generic declaration contract".into()); + } + let result = FunctionReceipt { + program: call.caller.program.clone(), + item, + definition, + instance, + }; + result.original()?; + Ok(Some(result)) +} + +fn retained_function( + program: &jett_typecheck::CheckedResourceProgram, + definition: DefId, +) -> Result<(usize, &FunctionDef), String> { + let candidates: Vec<_> = program + .module() + .items + .iter() + .enumerate() + .filter_map(|(item, source)| match source { + Item::Function(function) if is_root_definition(program, item, definition) => { + Some((item, function)) + } + _ => None, + }) + .collect(); + match candidates.as_slice() { + [selected] => Ok(*selected), + _ => Err("temporary local view call lost its retained named function".into()), + } +} + +fn exact_instance( + program: &jett_typecheck::CheckedResourceProgram, + target: &jett_typecheck::CheckedGenericCall, +) -> Result { + let matching: Vec<_> = program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, instance)| { + instance.definition == target.definition + && instance.concrete_args == target.concrete_args + && instance.specialization == target.specialization + }) + .map(|(index, _)| index) + .collect(); + match matching.as_slice() { + [index] => Ok(*index), + _ => Err("temporary local view generic target has no unique exact checked body".into()), + } +} + +pub(super) struct Facts<'a> { + pub(super) type_map: &'a HashMap, + pub(super) source_type_map: &'a HashMap, + pub(super) binding_facts: &'a HashMap, + pub(super) call_ownership: &'a HashMap, + pub(super) generic_calls: &'a HashMap, + pub(super) intrinsic_ids: &'a HashMap, + pub(super) intrinsic_type_arguments: &'a HashMap>, + pub(super) call_argument_orders: &'a HashMap, + pub(super) scopes: &'a HashMap>, +} + +impl FunctionReceipt { + pub(super) fn facts(&self) -> Result, String> { + let checked = self.program.checked(); + if let Some(index) = self.instance { + let body = checked + .generic_function_instantiations + .get(index) + .filter(|body| body.definition == self.definition) + .ok_or("temporary local view lost its exact generic body facts")?; + Ok(Facts { + type_map: &body.type_map, + source_type_map: &body.source_type_map, + binding_facts: &body.binding_facts, + call_ownership: &body.call_ownership, + generic_calls: &body.generic_calls, + intrinsic_ids: &body.intrinsic_ids, + intrinsic_type_arguments: &body.intrinsic_type_arguments, + call_argument_orders: &body.call_argument_orders, + scopes: &body.comptime_type_bindings, + }) + } else { + Ok(Facts { + type_map: &checked.type_map, + source_type_map: &checked.source_type_map, + binding_facts: &checked.binding_facts, + call_ownership: &checked.call_ownership, + generic_calls: &checked.generic_calls, + intrinsic_ids: &checked.intrinsic_ids, + intrinsic_type_arguments: &checked.intrinsic_type_arguments, + call_argument_orders: &checked.call_argument_orders, + scopes: &checked.comptime_type_bindings, + }) + } + } + + pub(super) fn validate_declaration( + &self, + name: &str, + namespace: &Option, + current: &FunctionDef, + ) -> Result<(), String> { + let info = self.program.resolved().scope_table.def(self.definition); + if name != info.name + || namespace != &info.namespace + || crate::value::closure_capture::OriginalClosureOwner::registered( + &self.program, + name, + current, + namespace, + ) + .is_none() + || current.name.span != self.original()?.name.span + { + return Err( + "temporary local view function differs from its retained checked Source".into(), + ); + } + Ok(()) + } + + pub(super) fn validate_registered( + &self, + name: &str, + namespace: &Option, + current: &FunctionDef, + projection: Option<&crate::checked_types::CheckedFunctionTypes>, + arguments: &[crate::value::ClosureTypeArgument], + ) -> Result<(), String> { + self.validate_declaration(name, namespace, current)?; + match self.instance { + Some(index) => { + let body = &self.program.checked().generic_function_instantiations[index]; + if projection.is_none_or(|projection| { + !crate::value::closure_capture::matches_field_function_projection( + projection, + arguments, + body, + &self.program.checked().interner, + ) + }) { + return Err( + "temporary local view generic frame changed its exact concrete projection" + .into(), + ); + } + } + None if projection.is_none() && arguments.is_empty() => {} + None => { + return Err("temporary local view ordinary frame acquired generic metadata".into()); + } + } + Ok(()) + } +} + +pub(super) fn copyable(types: &jett_types::TypeInterner, ty: TypeId) -> bool { + matches!( + types.resolve(ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Nothing + ) +} + +fn ordinary_type( + types: &jett_types::TypeInterner, + ty: TypeId, + ancestors: &mut Vec, +) -> bool { + if ancestors.contains(&ty) { + return false; + } + ancestors.push(ty); + let ordinary = if copyable(types, ty) || matches!(types.resolve(ty), Type::Bytes) { + true + } else { + match types.resolve(ty) { + Type::Struct(id) if types.nominal_type_arguments(ty).is_empty() => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field)| ordinary_type(types, *field, ancestors)), + Type::List(inner) | Type::Set(inner) | Type::Optional(inner) => { + ordinary_type(types, *inner, ancestors) + } + Type::Map(first, second) | Type::Result(first, second) => { + ordinary_type(types, *first, ancestors) && ordinary_type(types, *second, ancestors) + } + _ => false, + } + }; + ancestors.pop(); + ordinary +} + +pub(super) fn unparen(mut expression: &Expr) -> &Expr { + while let Expr::Paren(inner, _) = expression { + expression = inner; + } + expression +} + +fn projection_root(mut expression: &Expr) -> &Expr { + loop { + match unparen(expression) { + Expr::FieldAccess(base, _, _) => expression = base, + root => return root, + } + } +} + +pub(super) fn direct_expressions(block: &Block) -> Vec<&Expr> { + let mut expressions = Vec::new(); + for statement in &block.stmts { + let expression = match statement { + Stmt::VarDecl(value) => Some(&value.value), + Stmt::Return(value) => value.value.as_ref(), + Stmt::Expr(value) => Some(&value.expr), + _ => None, + }; + if let Some(expression) = expression { + visit_expression(expression, &mut expressions); + } + } + expressions +} + +fn visit_expression<'a>(expression: &'a Expr, output: &mut Vec<&'a Expr>) { + output.push(expression); + match expression { + Expr::Paren(inner, _) + | Expr::View(inner, _) + | Expr::Clone(inner, _) + | Expr::Unary(_, inner, _) + | Expr::Ok(inner, _) + | Expr::Fail(inner, _) + | Expr::Some(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) + | Expr::Run(inner, _) + | Expr::Join(inner, _) + | Expr::Cancel(inner, _) => visit_expression(inner, output), + Expr::FieldAccess(base, _, _) => visit_expression(base, output), + Expr::Binary(left, _, right, _) => { + visit_expression(left, output); + visit_expression(right, output); + } + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + visit_expression(callee, output); + for argument in arguments { + visit_expression(&argument.value, output); + } + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + if let StringPart::Expr(inner) = part { + visit_expression(inner, output); + } + } + } + // Handle/inline/control-flow regions need distinct original scope receipts. + _ => {} + } +} diff --git a/crates/jett_comptime/src/interpreter/temporary_local_views/storage.rs b/crates/jett_comptime/src/interpreter/temporary_local_views/storage.rs new file mode 100644 index 00000000..836d5963 --- /dev/null +++ b/crates/jett_comptime/src/interpreter/temporary_local_views/storage.rs @@ -0,0 +1,740 @@ +//! Private logical owners and nonowning tickets. Public Value remains unchanged. +use super::receipts::{ProducerReceipt, checked_path, copyable, producer_receipt, unparen}; +use super::*; +use jett_parser::ast::{Block, Expr, Stmt}; +use jett_typecheck::CheckedBindingMode; + +pub(in crate::interpreter) type ActivationId = u64; + +#[derive(Clone, PartialEq, Eq)] +pub(super) struct AliasTicket { + pub(super) owner_activation: ActivationId, + pub(super) owner_index: usize, + pub(super) whole: TypeId, + pub(super) path: Vec, + pub(super) endpoint: TypeId, +} + +pub(super) struct InstalledAlias { + /// Dynamic alias identity is (function activation, original binding DefId). + pub(super) activation: ActivationId, + pub(super) fact: CheckedBindingFact, + pub(super) name: String, + pub(super) ticket: AliasTicket, +} + +pub(super) enum OwnerState { + Vacant, + Live(Value), + Retired, +} +pub(super) struct OwnerSlot { + pub(super) receipt: ProducerReceipt, + pub(super) state: OwnerState, +} +pub(super) struct Activation { + pub(super) id: ActivationId, + pub(super) function: FunctionReceipt, + pub(super) root_active: bool, + pub(super) root_scope: Option, + pub(super) owners: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum AuditEvent { + Reserved { + activation: ActivationId, + owner: usize, + }, + Acquired { + activation: ActivationId, + owner: usize, + }, + AliasPublished { + scope: usize, + definition: DefId, + alias_activation: ActivationId, + activation: ActivationId, + owner: usize, + }, + AliasEnded { + scope: usize, + definition: DefId, + alias_activation: ActivationId, + activation: ActivationId, + owner: usize, + }, + OwnerRetired { + activation: ActivationId, + owner: usize, + }, + ExplicitClone { + activation: ActivationId, + owner: usize, + }, + Output { + activation: ActivationId, + }, +} + +pub(in crate::interpreter) struct Context { + pub(super) program: Arc, + pub(super) entry_allowed: bool, + pub(super) activations: Vec, + pub(super) aliases: Vec>, + pub(super) next_identity: Option, + pub(super) audit: Vec, +} + +impl Context { + pub(in crate::interpreter) fn needed( + program: &Arc, + ) -> bool { + Self::body_needed(program, None) + } + + pub(super) fn body_needed( + program: &Arc, + body: Option, + ) -> bool { + let checked = program.checked(); + temporary_facts(program, &checked.binding_facts, body) + || scoped_temporary(program, &checked.comptime_type_bindings, body) + || checked.actor_handlers.values().any(|handler| { + temporary_facts(program, &handler.body().binding_facts, body) + || scoped_temporary(program, &handler.body().comptime_type_bindings, body) + }) + || checked + .generic_function_instantiations + .iter() + .any(|instance| { + temporary_facts(program, &instance.binding_facts, body) + || scoped_temporary(program, &instance.comptime_type_bindings, body) + }) + } + + pub(in crate::interpreter) fn new( + program: Arc, + depth: usize, + ) -> Self { + Self { + program, + entry_allowed: false, + activations: Vec::new(), + aliases: (0..depth).map(|_| HashMap::new()).collect(), + next_identity: Some(1), + audit: Vec::new(), + } + } + + pub(in crate::interpreter) fn push_scope(&mut self) { + self.aliases.push(HashMap::new()); + } + + pub(in crate::interpreter) fn pop_scope(&mut self) { + // Exactly paired with Interpreter::push_scope; owner cleanup runs + // separately before this infallible bookkeeping operation. + if let Some(scope) = self.aliases.len().checked_sub(1) + && let Some(aliases) = self.aliases.pop() + { + self.end_aliases(scope, aliases); + } + } + + fn end_aliases(&mut self, scope: usize, aliases: HashMap) { + // Alias end events have no owning cleanup order; owner retirement does. + let mut aliases: Vec<_> = aliases.into_iter().collect(); + aliases.sort_by_key(|(definition, _)| definition.index()); + for (definition, alias) in aliases { + self.audit.push(AuditEvent::AliasEnded { + scope, + definition, + alias_activation: alias.activation, + activation: alias.ticket.owner_activation, + owner: alias.ticket.owner_index, + }); + } + } + + pub(super) fn enter(&mut self, function: FunctionReceipt) -> Result { + if !Arc::ptr_eq(&self.program, &function.program) + || (self.activations.is_empty() && !self.entry_allowed) + { + return Err("temporary local view has no checked invocation entry".into()); + } + function.original()?; + let id = self + .next_identity + .ok_or("temporary local view activation identities exhausted")?; + self.next_identity = id.checked_add(1); + self.entry_allowed = false; + self.activations.push(Activation { + id, + function, + root_active: false, + root_scope: None, + owners: Vec::new(), + }); + Ok(id) + } + + pub(super) fn leave(&mut self, id: ActivationId) -> Result<(), String> { + let current = self + .current() + .ok_or("temporary local view lost its activation")?; + if current.id != id + || current.root_active + || current + .owners + .iter() + .any(|owner| matches!(owner.state, OwnerState::Live(_))) + { + return Err("temporary local view activation still has live obligations".into()); + } + self.activations.pop(); + Ok(()) + } + + pub(super) fn begin( + &mut self, + block: &Block, + scope: usize, + ) -> Result, String> { + let Some(current) = self.current() else { + return Ok(None); + }; + if !std::ptr::eq(¤t.function.original()?.body, block) { + return Ok(None); + } + if current.root_active || current.root_scope.is_some() || scope >= self.aliases.len() { + return Err("temporary local view root activation was reused".into()); + } + let current = self + .activations + .last_mut() + .ok_or("temporary local view lost its activation")?; + current.root_active = true; + current.root_scope = Some(scope); + Ok(Some(current.id)) + } + + pub(super) fn complete(&mut self, id: ActivationId) -> Result<(), String> { + let position = self + .activations + .len() + .checked_sub(1) + .ok_or("temporary local view lost its activation")?; + let current = &self.activations[position]; + if current.id != id || !current.root_active { + return Err("temporary local view root completion is stale".into()); + } + let scope = current + .root_scope + .ok_or("temporary local view lost its root scope")?; + // Scopes below the exact source root belong to parameters or callers. + for index in scope..self.aliases.len() { + let aliases = std::mem::take(&mut self.aliases[index]); + self.end_aliases(index, aliases); + } + for owner in (0..self.activations[position].owners.len()).rev() { + self.retire(position, owner); + } + self.activations[position].root_active = false; + Ok(()) + } + + fn retire(&mut self, activation: usize, owner: usize) { + let state = std::mem::replace( + &mut self.activations[activation].owners[owner].state, + OwnerState::Retired, + ); + if matches!(state, OwnerState::Live(_)) { + self.audit.push(AuditEvent::OwnerRetired { + activation: self.activations[activation].id, + owner, + }); + } + drop(state); + } + + pub(super) fn current(&self) -> Option<&Activation> { + self.activations.last() + } + + pub(super) fn alias_for( + &self, + function: &FunctionReceipt, + expression: &Expr, + floor: usize, + ) -> Result, String> { + let current = self + .current() + .ok_or("temporary local view has no active function")?; + if !current.root_active + || !same_function(¤t.function, function)? + || floor > self.aliases.len() + { + return Err("temporary local view read has a foreign activation".into()); + } + let mut selected = unparen(expression); + if let Expr::View(inner, _) = selected { + selected = unparen(inner); + } + let mut root = selected; + while let Expr::FieldAccess(base, _, _) = root { + root = unparen(base); + } + let Expr::Ident(name) = root else { + return Ok(None); + }; + let Some(definition) = self.program.resolved().resolutions.get(&name.span) else { + return Ok(None); + }; + let Some(alias) = self.aliases[floor..] + .iter() + .rev() + .find_map(|scope| scope.get(definition)) + else { + return Ok(None); + }; + function.expression(expression)?; + let facts = function.facts()?; + let fact = facts + .binding_facts + .get(&alias.fact.declaration_span) + .ok_or("temporary local view lost its checked alias fact")?; + if alias.activation != current.id + || fact != &alias.fact + || fact.definition != *definition + || fact.ty != alias.ticket.endpoint + || alias.name != name.name + || fact.mutable + || !matches!(fact.mode, CheckedBindingMode::View { .. }) + { + return Err("temporary local view read changed its exact alias binding".into()); + } + let mut extension = Vec::new(); + let endpoint = checked_path(function, selected, &mut extension)?; + if extension + .first() + .is_some_and(|projection| projection.owner != alias.ticket.endpoint) + { + return Err("temporary local view read changed its projection root".into()); + } + let mut ticket = alias.ticket.clone(); + ticket.path.extend(extension); + ticket.endpoint = endpoint; + self.observe(&ticket)?; + Ok(Some(ticket)) + } + + pub(in crate::interpreter) fn placeholder(&self, scope: usize, name: &str) -> bool { + self.aliases + .get(scope) + .is_some_and(|scope| scope.values().any(|alias| alias.name == name)) + } + + pub(super) fn reserve( + &mut self, + receipt: ProducerReceipt, + ) -> Result<(ActivationId, usize), String> { + let current = self + .current() + .ok_or("temporary local view has no active owner scope")?; + if !current.root_active || !same_function(¤t.function, &receipt.function)? { + return Err("temporary local view producer has a foreign activation".into()); + } + revalidate_receipt(&receipt)?; + if current + .owners + .iter() + .any(|slot| slot.receipt.declaration == receipt.declaration) + { + return Err("temporary local view declaration initialized twice".into()); + } + let activation = current.id; + let owner = current.owners.len(); + self.activations + .last_mut() + .ok_or("temporary local view lost its activation")? + .owners + .push(OwnerSlot { + receipt, + state: OwnerState::Vacant, + }); + self.audit.push(AuditEvent::Reserved { activation, owner }); + Ok((activation, owner)) + } + + pub(super) fn acquire( + &mut self, + slot: (ActivationId, usize), + value: Value, + ) -> Result { + let position = self + .activations + .len() + .checked_sub(1) + .ok_or("temporary local view has no activation")?; + let current = &self.activations[position]; + let owner = current + .owners + .get(slot.1) + .ok_or("temporary local view owner slot disappeared")?; + if current.id != slot.0 + || !current.root_active + || !matches!(owner.state, OwnerState::Vacant) + { + return Err("temporary local view owner acquisition is stale".into()); + } + revalidate_receipt(&owner.receipt)?; + let ticket = AliasTicket { + owner_activation: slot.0, + owner_index: slot.1, + whole: owner.receipt.whole, + path: owner.receipt.path.clone(), + endpoint: owner.receipt.fact.ty, + }; + self.activations[position].owners[slot.1].state = OwnerState::Live(value); + self.audit.push(AuditEvent::Acquired { + activation: slot.0, + owner: slot.1, + }); + if let Err(error) = self.observe(&ticket) { + self.retire(position, slot.1); + return Err(error); + } + Ok(ticket) + } + + pub(super) fn publish( + &mut self, + fact: CheckedBindingFact, + name: &str, + ticket: AliasTicket, + scope: usize, + ) -> Result<(), String> { + let current = self + .current() + .ok_or("temporary local view alias has no activation")?; + let alias_activation = current.id; + let source = current.function.original()?; + let facts = current.function.facts()?; + let info = self + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| info.id == fact.definition) + .ok_or("temporary local view alias lost its resolved declaration")?; + let parameter = source.params.iter().any(|parameter| { + parameter.name.span == fact.declaration_span + && parameter.name.name == name + && parameter.view + && !parameter.mutable + }); + let declaration = current.root_active && source.body.stmts.iter().any(|statement| { + matches!(statement, Stmt::VarDecl(declaration) if declaration.name.span == fact.declaration_span + && declaration.name.name == name && !declaration.mutable) + }); + if facts.binding_facts.get(&fact.declaration_span) != Some(&fact) + || info.span != fact.declaration_span + || info.name != name + || fact.mutable + || fact.ty != ticket.endpoint + || !matches!(fact.mode, CheckedBindingMode::View { .. }) + || !matches!( + (info.kind, parameter, declaration), + (jett_resolve::DefKind::Param, true, _) + | (jett_resolve::DefKind::Variable, _, true) + ) + || scope.checked_add(1) != Some(self.aliases.len()) + { + return Err("temporary local view alias has no exact immutable declaration".into()); + } + self.observe(&ticket)?; + let aliases = self + .aliases + .get_mut(scope) + .ok_or("temporary local view alias scope disappeared")?; + if aliases.contains_key(&fact.definition) + || aliases.values().any(|alias| alias.name == name) + { + return Err("temporary local view alias declaration was overwritten".into()); + } + self.audit.push(AuditEvent::AliasPublished { + scope, + definition: fact.definition, + alias_activation, + activation: ticket.owner_activation, + owner: ticket.owner_index, + }); + aliases.insert( + fact.definition, + InstalledAlias { + activation: alias_activation, + fact, + name: name.to_owned(), + ticket, + }, + ); + Ok(()) + } + + pub(super) fn observe(&self, ticket: &AliasTicket) -> Result<&Value, String> { + let activation = self + .activations + .iter() + .find(|activation| activation.id == ticket.owner_activation) + .ok_or("temporary local view backing activation is stale")?; + let slot = activation + .owners + .get(ticket.owner_index) + .ok_or("temporary local view backing owner disappeared")?; + revalidate_receipt(&slot.receipt)?; + if !activation.root_active + || !Arc::ptr_eq(&self.program, &activation.function.program) + || !same_function(&activation.function, &slot.receipt.function)? + || ticket.whole != slot.receipt.whole + || !ticket.path.starts_with(&slot.receipt.path) + { + return Err("temporary local view ticket changed its backing identity".into()); + } + let OwnerState::Live(whole_value) = &slot.state else { + return Err("temporary local view backing owner is not acquired".into()); + }; + let mut value = whole_value; + let types = &self.program.checked().interner; + plain_type(types, ticket.whole, &mut Vec::new())?; + validate_value(types, ticket.whole, value)?; + let mut owner = ticket.whole; + for projection in &ticket.path { + let Type::Struct(id) = types.resolve(owner) else { + return Err("temporary local view projection owner is not a struct".into()); + }; + let structure = types.resolve_struct(*id); + let Value::Struct { + type_name, + concrete_type, + fields, + } = value + else { + return Err( + "temporary local view projection cannot inspect wrapped or pending storage" + .into(), + ); + }; + if projection.owner != owner + || !types.nominal_type_arguments(owner).is_empty() + || projection.name != structure.name + || type_name != &structure.name + || concrete_type.is_some() + || projection.fields + != structure + .fields + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + || fields.len() != structure.fields.len() + || structure.fields.get(projection.index).map(|(_, ty)| *ty) + != Some(projection.endpoint) + || slot + .receipt + .function + .facts()? + .type_map + .get(&projection.span) + != Some(&projection.endpoint) + { + return Err( + "temporary local view projection changed its exact checked shape".into(), + ); + } + value = &fields + .get(projection.index) + .ok_or("temporary local view field storage disappeared")? + .1; + owner = projection.endpoint; + } + if owner != ticket.endpoint { + return Err("temporary local view endpoint changed its checked type".into()); + } + validate_value(types, owner, value)?; + Ok(value) + } + + pub(super) fn explicit_clone(&mut self, ticket: &AliasTicket) -> Result { + let value = self.observe(ticket)?.clone(); + self.audit.push(AuditEvent::ExplicitClone { + activation: ticket.owner_activation, + owner: ticket.owner_index, + }); + Ok(value) + } +} + +pub(super) fn temporary_facts( + program: &jett_typecheck::CheckedResourceProgram, + facts: &HashMap, + body: Option, +) -> bool { + facts.values().any(|fact| { + if !matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ) || body.is_some_and(|body| { + fact.declaration_span.file != body.file + || fact.declaration_span.start < body.start + || fact.declaration_span.end > body.end + }) { + return false; + } + let info = program.resolved().scope_table.def(fact.definition); + if info.kind != jett_resolve::DefKind::Variable || info.span != fact.declaration_span { + return false; + } + crate::value::closure_capture::original_variable_declaration(program, fact.declaration_span) + .is_some_and(|declaration| { + declaration.name.name == info.name + && !declaration.mutable + && !fact.mutable + && matches!(unparen(&declaration.value), Expr::View(_, _)) + }) + }) +} + +pub(super) fn scoped_temporary( + program: &jett_typecheck::CheckedResourceProgram, + scopes: &HashMap>, + body: Option, +) -> bool { + scopes.values().flatten().any(|scope| { + temporary_facts(program, &scope.body.binding_facts, body) + || scoped_temporary(program, &scope.body.comptime_type_bindings, body) + }) +} + +pub(super) fn same_function( + left: &FunctionReceipt, + right: &FunctionReceipt, +) -> Result { + Ok(Arc::ptr_eq(&left.program, &right.program) + && left.same(right) + && std::ptr::eq(left.original()?, right.original()?)) +} + +fn revalidate_receipt(receipt: &ProducerReceipt) -> Result<(), String> { + let declaration = match receipt + .function + .original()? + .body + .stmts + .get(receipt.declaration) + { + Some(Stmt::VarDecl(declaration)) => declaration, + _ => return Err("temporary local view receipt lost its original declaration".into()), + }; + let current = producer_receipt(&receipt.function, declaration)? + .ok_or("temporary local view receipt lost its checked producer")?; + if current.fact != receipt.fact + || current.whole != receipt.whole + || current.path != receipt.path + { + return Err("temporary local view receipt changed its checked projection".into()); + } + Ok(()) +} + +fn plain_type( + types: &jett_types::TypeInterner, + ty: TypeId, + ancestors: &mut Vec, +) -> Result<(), String> { + if ancestors.contains(&ty) { + return Err("temporary local view recursive storage remains unsupported".into()); + } + ancestors.push(ty); + let accepted = match types.resolve(ty) { + Type::Struct(id) if types.nominal_type_arguments(ty).is_empty() => types.resolve_struct(*id).fields.iter() + .try_for_each(|(_, field)| plain_type(types, *field, ancestors)), + Type::List(inner) | Type::Set(inner) | Type::Optional(inner) => plain_type(types, *inner, ancestors), + Type::Map(first, second) | Type::Result(first, second) => plain_type(types, *first, ancestors) + .and_then(|()| plain_type(types, *second, ancestors)), + Type::Bytes => Ok(()), + _ if copyable(types, ty) => Ok(()), + _ => Err("temporary local view wrapped, generic or runtime-authority storage remains unsupported".into()), + }; + ancestors.pop(); + accepted +} + +fn validate_value( + types: &jett_types::TypeInterner, + ty: TypeId, + value: &Value, +) -> Result<(), String> { + let valid = match (types.resolve(ty), value) { + (Type::Int8, Value::Int64(value)) => (-128..=127).contains(value), + (Type::Int16, Value::Int64(value)) => (-32768..=32767).contains(value), + (Type::Int32, Value::Int64(value)) => { + (i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(value) + } + (Type::Int64, Value::Int64(_)) + | (Type::Uint64, Value::Uint64(_)) + | (Type::Float32 | Type::Float64, Value::Float64(_)) + | (Type::String, Value::String(_)) + | (Type::Bool, Value::Bool(_)) + | (Type::Bytes, Value::Bytes(_)) + | (Type::Nothing, Value::Nothing) => true, + (Type::Uint8, Value::Int64(value)) => (0..=255).contains(value), + (Type::Uint16, Value::Int64(value)) => (0..=65535).contains(value), + (Type::Uint32, Value::Int64(value)) => (0..=i64::from(u32::MAX)).contains(value), + (Type::Uint8, Value::Uint64(value)) => *value <= u64::from(u8::MAX), + (Type::Uint16, Value::Uint64(value)) => *value <= u64::from(u16::MAX), + (Type::Uint32, Value::Uint64(value)) => *value <= u64::from(u32::MAX), + ( + Type::Struct(id), + Value::Struct { + type_name, + concrete_type, + fields, + }, + ) => { + let structure = types.resolve_struct(*id); + types.nominal_type_arguments(ty).is_empty() + && concrete_type.is_none() + && type_name == &structure.name + && fields.len() == structure.fields.len() + && fields + .iter() + .zip(&structure.fields) + .all(|((name, value), (expected, ty))| { + name == expected && validate_value(types, *ty, value).is_ok() + }) + } + (Type::List(inner), Value::List(values)) | (Type::Set(inner), Value::Set(values)) => values + .iter() + .all(|value| validate_value(types, *inner, value).is_ok()), + (Type::Map(key, item), Value::Map(values)) => values.iter().all(|(first, second)| { + validate_value(types, *key, first).is_ok() + && validate_value(types, *item, second).is_ok() + }), + (Type::Optional(inner), Value::OptionalSome(value)) => { + validate_value(types, *inner, value).is_ok() + } + (Type::Optional(_), Value::OptionalNone) => true, + (Type::Result(ok, _), Value::ResultOk(value)) => validate_value(types, *ok, value).is_ok(), + (Type::Result(_, fail), Value::ResultFail(value)) => { + validate_value(types, *fail, value).is_ok() + } + _ => false, + }; + if valid { + Ok(()) + } else { + Err("temporary local view value disagrees with its exact checked ordinary type".into()) + } +} diff --git a/crates/jett_comptime/src/lib.rs b/crates/jett_comptime/src/lib.rs index 5dd702fb..6b42d650 100644 --- a/crates/jett_comptime/src/lib.rs +++ b/crates/jett_comptime/src/lib.rs @@ -1,17 +1,30 @@ +pub mod checked_types; +pub mod debug; pub mod explicit; pub mod interpreter; +mod resource_execution; pub mod value; pub mod verify; -pub use explicit::evaluate_explicit_comptime_expressions; +/// Shared stack budget for recursive reference and explicit comptime execution. +pub const INTERPRETER_STACK_SIZE: usize = 8 * 1024 * 1024; + +pub use debug::{DebugEvent, DebugEventKind, render_debug_events}; +pub use explicit::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, ComptimeContext, ExplicitComptimeEvaluation, + ExplicitComptimeValues, evaluate_explicit_comptime_expressions, + evaluate_explicit_comptime_expressions_capture, +}; pub use interpreter::{ ClockTestSample, EnvironmentTestEntry, EnvironmentTestSnapshot, EnvironmentTestText, GraphicsTestEvent, GraphicsTestKey, GraphicsTestObservation, Interpreter, RandomTestSample, }; +pub use resource_execution::ExecutionPurpose; pub use value::Value; pub use verify::{ ComptimeError, eval_assert, eval_function, run_verify_blocks, run_verify_blocks_detailed_with_metadata, run_verify_blocks_detailed_with_metadata_and_expression_types, run_verify_blocks_with_metadata, - run_verify_blocks_with_metadata_and_expression_types, + run_verify_blocks_with_metadata_and_expression_types, verify_results_to_diagnostics, }; diff --git a/crates/jett_comptime/src/resource_execution/assignment_tests.rs b/crates/jett_comptime/src/resource_execution/assignment_tests.rs new file mode 100644 index 00000000..bb6e8e19 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/assignment_tests.rs @@ -0,0 +1,139 @@ +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +const SOURCE: &str = include_str!("fixtures/24_mutable_assignment.jett"); + +fn run( + name: &str, + script: Vec, + expected: Vec, + panic_expected: bool, +) { + for release in [false, true] { + let program = program(SOURCE, release); + let definition = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + let result = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant]) + })); + if panic_expected { + assert!( + result.is_err(), + "the old-owner finalizer panic must propagate" + ); + } else { + assert_eq!(result.unwrap().unwrap(), Value::Nothing); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +fn constructed(label: i64) -> ScriptOperation { + ScriptOperation::Construct { + label, + outcome: Ok(()), + } +} + +#[test] +fn mutable_resource_replacement_finalizes_old_before_using_new() { + run( + "replace_live", + vec![ + constructed(2401), + constructed(2402), + ScriptOperation::Borrow { + label: 2402, + outcome: Ok(7), + }, + ], + vec![ + ProviderEvent::Constructed(2401), + ProviderEvent::Constructed(2402), + ProviderEvent::Finalized(2401), + ProviderEvent::Borrowed(2402), + ProviderEvent::Finalized(2402), + ], + false, + ); +} + +#[test] +fn mutable_resource_can_rebind_its_original_consumed_slot() { + run( + "rebind_after_close", + vec![constructed(2411), constructed(2412)], + vec![ + ProviderEvent::Constructed(2411), + ProviderEvent::Finalized(2411), + ProviderEvent::Constructed(2412), + ProviderEvent::Finalized(2412), + ], + false, + ); +} + +#[test] +fn mutable_resource_self_move_retains_exactly_one_cleanup_owner() { + run( + "rebind_self", + vec![constructed(2421)], + vec![ + ProviderEvent::Constructed(2421), + ProviderEvent::Finalized(2421), + ], + false, + ); +} + +#[test] +fn mutable_resource_failed_rhs_handler_can_borrow_previous_owner() { + run( + "failed_rhs_keeps_owner", + vec![ + constructed(2431), + ScriptOperation::Construct { + label: 2432, + outcome: Err("rhs sentinel".to_string()), + }, + ScriptOperation::Borrow { + label: 2431, + outcome: Ok(7), + }, + ], + vec![ + ProviderEvent::Constructed(2431), + ProviderEvent::ConstructionFailed(2432), + ProviderEvent::Borrowed(2431), + ProviderEvent::Finalized(2431), + ], + false, + ); +} + +#[test] +fn mutable_resource_old_finalizer_panic_also_cleans_staged_replacement() { + run( + "replace_live", + vec![ + ScriptOperation::ConstructFinalizerPanic { label: 2401 }, + constructed(2402), + ], + vec![ + ProviderEvent::Constructed(2401), + ProviderEvent::Constructed(2402), + ProviderEvent::Finalized(2401), + ProviderEvent::Finalized(2402), + ], + true, + ); +} diff --git a/crates/jett_comptime/src/resource_execution/capture.rs b/crates/jett_comptime/src/resource_execution/capture.rs new file mode 100644 index 00000000..fff74064 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/capture.rs @@ -0,0 +1,257 @@ +//! Raw lexical references are only a refusal guard, never opaque copy authority. +use super::*; + +pub(super) fn raw_closure_uses_binding(params: &[Param], body: &Block, name: &str) -> bool { + block_uses_binding( + body, + name, + params.iter().any(|param| param.name.name == name), + ) +} + +fn block_uses_binding(body: &Block, name: &str, mut shadowed: bool) -> bool { + for statement in &body.stmts { + let used = match statement { + Stmt::VarDecl(value) => { + let used = expression_uses_binding(&value.value, name, shadowed); + shadowed |= value.name.name == name; + used + } + Stmt::Assign(value) => { + expression_uses_binding(&value.target, name, shadowed) + || expression_uses_binding(&value.value, name, shadowed) + } + Stmt::Return(value) => value + .value + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)), + Stmt::Respond(value) => expression_uses_binding(&value.value, name, shadowed), + Stmt::ComptimeTypeBind(value) => { + expression_uses_binding(&value.value, name, shadowed) + || block_uses_binding(&value.body, name, shadowed || value.name.name == name) + } + Stmt::If(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || block_uses_binding(&value.then_block, name, shadowed) + || value.else_ifs.iter().any(|(condition, body)| { + expression_uses_binding(condition, name, shadowed) + || block_uses_binding(body, name, shadowed) + }) + || value + .else_block + .as_ref() + .is_some_and(|body| block_uses_binding(body, name, shadowed)) + } + Stmt::For(value) => { + expression_uses_binding(&value.iterable, name, shadowed) + || block_uses_binding( + &value.body, + name, + shadowed + || value.variable.name == name + || value + .value_variable + .as_ref() + .is_some_and(|binding| binding.name == name), + ) + } + Stmt::While(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || block_uses_binding(&value.body, name, shadowed) + } + Stmt::Match(value) => { + expression_uses_binding(&value.expr, name, shadowed) + || value.arms.iter().any(|arm| { + let bound = match &arm.pattern { + Pattern::Variant(_, bindings) => { + bindings.iter().any(|binding| binding.name == name) + } + Pattern::Ident(_) | Pattern::Other(_) => false, + }; + block_uses_binding(&arm.body, name, shadowed || bound) + }) + } + Stmt::Expr(value) => expression_uses_binding(&value.expr, name, shadowed), + Stmt::Use(value) => { + shadowed |= + Interpreter::use_bound_name(&value.path.name, value.alias.as_ref()) == name; + false + } + Stmt::Assert(value) => { + expression_uses_binding(&value.condition, name, shadowed) + || value + .message + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)) + } + Stmt::Trace(value) => !shadowed && value.name.name == name, + Stmt::Breakpoint(value) => value + .condition + .as_ref() + .is_some_and(|value| expression_uses_binding(value, name, shadowed)), + Stmt::Break(_) | Stmt::Continue(_) => false, + }; + if used { + return true; + } + } + false +} + +fn expression_uses_binding(value: &Expr, name: &str, shadowed: bool) -> bool { + match value { + Expr::Ident(value) => !shadowed && value.name == name, + Expr::Binary(left, _, right, _) => { + expression_uses_binding(left, name, shadowed) + || expression_uses_binding(right, name, shadowed) + } + Expr::Unary(_, inner, _) + | Expr::FieldAccess(inner, _, _) + | Expr::Paren(inner, _) + | Expr::View(inner, _) + | Expr::Comptime(inner, _) + | Expr::Ok(inner, _) + | Expr::Fail(inner, _) + | Expr::Some(inner, _) + | Expr::Default(inner, _) + | Expr::Declassify(inner, _) + | Expr::Coarsen(inner, _) + | Expr::At(inner, _, _) + | Expr::Spawn(inner, _) + | Expr::Send(inner, _) + | Expr::Ask(inner, _) + | Expr::Clone(inner, _) + | Expr::Run(inner, _) + | Expr::Join(inner, _) + | Expr::Cancel(inner, _) => expression_uses_binding(inner, name, shadowed), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + expression_uses_binding(callee, name, shadowed) + || arguments + .iter() + .any(|argument| expression_uses_binding(&argument.value, name, shadowed)) + } + Expr::ListConstruct(values, _) => values + .iter() + .any(|value| expression_uses_binding(value, name, shadowed)), + Expr::MapConstruct(values, _) => values.iter().any(|(key, value)| { + expression_uses_binding(key, name, shadowed) + || expression_uses_binding(value, name, shadowed) + }), + Expr::Handle(target, error, body, _) => { + expression_uses_binding(target, name, shadowed) + || block_uses_binding( + body, + name, + shadowed || error.as_ref().is_some_and(|error| error.name == name), + ) + } + Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { + StringPart::Expr(value) => expression_uses_binding(value, name, shadowed), + StringPart::Literal(_) => false, + }), + Expr::Pipeline(initial, steps, _) => { + expression_uses_binding(initial, name, shadowed) + || steps.iter().any(|step| { + expression_uses_binding(&step.function, name, shadowed) + || step.extra_args.iter().any(|argument| { + expression_uses_binding(&argument.value, name, shadowed) + }) + || step.handle.as_ref().is_some_and(|handle| { + block_uses_binding( + &handle.body, + name, + shadowed + || handle + .error_name + .as_ref() + .is_some_and(|error| error.name == name), + ) + }) + }) + } + Expr::InlineFn(params, _, body, _) => block_uses_binding( + body, + name, + shadowed || params.iter().any(|param| param.name.name == name), + ), + Expr::IntLiteral(..) + | Expr::FloatLiteral(..) + | Expr::StringLiteral(..) + | Expr::BoolLiteral(..) + | Expr::Nothing(_) + | Expr::None(_) + | Expr::EnumVariant(..) + | Expr::Error(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ExecutionPurpose, tests::program}; + + fn function(source: &str) -> FunctionDef { + let parsed = jett_parser::parse(source, jett_common::FileId::new(0)); + assert!(parsed.errors.is_empty()); + parsed + .module + .items + .into_iter() + .find_map(|item| match item { + Item::Function(function) => Some(function), + _ => None, + }) + .unwrap() + } + + #[test] + fn raw_opaque_capture_refuses_referenced_binding_but_preserves_unrelated_and_shadowed_data() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut authority = Interpreter::from_checked_resource_program( + checked.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = authority.install_resource_test_script(Vec::new()).unwrap(); + let mut raw = Interpreter::new(); + raw.set_variable("net", grant.clone()); + let source = function("namespace app\nfunction f() returns nothing:\n return net\n"); + assert!( + raw.capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap_err() + .contains("opaque capture") + ); + let mut raw_checked = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + raw_checked.set_variable("net", grant); + assert!( + raw_checked + .capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap_err() + .contains("opaque capture") + ); + for source in [ + "namespace app\nfunction f() returns int64:\n return 7\n", + "namespace app\nfunction f(net: int64) returns int64:\n return net\n", + "namespace app\nfunction f() returns int64:\n int64 net = 7\n return net\n", + ] { + let source = function(source); + let closure = raw + .capture_closure(&source.params, source.return_type.as_ref(), &source.body) + .unwrap(); + let Value::Function { captures, .. } = closure else { + panic!("ordinary closure"); + }; + assert!(!captures.contains_key("net")); + assert!( + captures + .values() + .all(|value| !value.contains_live_resource_or_grant()) + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked.rs b/crates/jett_comptime/src/resource_execution/checked.rs new file mode 100644 index 00000000..845643ff --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked.rs @@ -0,0 +1,1193 @@ +#[path = "checked/assignment.rs"] +mod assignment; +pub(crate) use assignment::CheckedAssignment; + +#[path = "checked/body_reference.rs"] +mod body_reference; +pub use body_reference::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, +}; +pub(crate) use body_reference::{ + CheckedActorInstanceOrigin, CheckedAttemptKey, CheckedBodyReference, PreparedAbsentBindings, + PreparedActorHandler, PreparedFieldAssignment, PreparedFieldBinding, + PreparedIntrinsicArguments, PreparedNamedCallable, PreparedOwnedStruct, PreparedPipelineStep, + PreparedReflectedFieldIteration, PreparedReflectedFieldLoop, PreparedRequiredExpression, +}; +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_resolve::DefId; +use jett_runtime::ResourceTypeId; +use jett_typecheck::actor_messages::{CheckedActorHandlerTarget, CheckedActorMessageCall}; +use jett_typecheck::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedBodyFacts, CheckedCallOwnership, + CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, + CheckedInvocationShape, CheckedInvocationTarget, CheckedResourceHook, CheckedResourceProgram, + validate_resource_hooks, +}; +use jett_types::{ReflectionTypeInfo, ResourceHookKind, ResourceKernelRecipe, Type, TypeId}; + +use super::{ResourceExecutionError, ResourceHookDescriptor, ResourceTypeBinding}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionPurpose { + ReferenceRuntime, + NamespaceConstant, + ExplicitComptime, + Verify, + Property, +} + +#[derive(Debug, Clone)] +enum BodyRoot { + Ordinary, + Generic(usize), + ActorHandler(CheckedActorHandlerTarget), +} + +#[derive(Debug, Clone)] +struct ScopedSelection { + owner: Span, + index: usize, + bound_type: TypeId, + selection: CheckedComptimeTypeSelection, + reflection: ReflectionTypeInfo, +} + +#[derive(Debug, Clone)] +pub(crate) struct CheckedBodyCursor { + root: BodyRoot, + scopes: Vec, + executable: Option, +} + +impl CheckedBodyCursor { + fn ordinary() -> Self { + Self { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: None, + } + } +} + +pub(crate) struct CheckedInvocation { + packet: CheckedCallOwnership, + body: CheckedBodyCursor, + span: Span, +} + +impl CheckedInvocation { + pub(crate) fn packet(&self) -> &CheckedCallOwnership { + &self.packet + } + pub(crate) fn arguments(&self) -> &[CheckedArgumentOwnership] { + &self.packet.arguments + } + pub(crate) fn target(&self) -> &CheckedInvocationTarget { + &self.packet.target + } + pub(crate) fn span(&self) -> Span { + self.span + } +} + +pub(crate) struct FunctionParameter { + pub(crate) index: usize, + pub(crate) ty: TypeId, + pub(crate) access: jett_typecheck::CheckedCalleeAccess, +} + +/// External checked entry is not a fabricated source call packet. +pub(crate) enum FunctionInvocation<'a> { + Source(&'a CheckedInvocation), + NamedSource { + source: &'a CheckedInvocation, + target: &'a PreparedNamedCallable, + }, + Entry { + definition: DefId, + signature: TypeId, + }, +} + +impl FunctionInvocation<'_> { + pub(crate) fn source(&self) -> Option<&CheckedInvocation> { + match self { + Self::Source(source) | Self::NamedSource { source, .. } => Some(source), + Self::Entry { .. } => None, + } + } + + pub(crate) fn signature( + &self, + checked: &CheckedExecution, + ) -> Result { + match self { + Self::Source(invocation) + | Self::NamedSource { + source: invocation, .. + } => match &invocation.packet.shape { + CheckedInvocationShape::Function { signature_type } => Ok(*signature_type), + CheckedInvocationShape::Intrinsic { .. } => { + Err(ResourceExecutionError::InvalidInvocation) + } + }, + Self::Entry { + definition, + signature, + } if checked.program.checked().definition_types.get(definition) == Some(signature) => { + Ok(*signature) + } + Self::Entry { .. } => Err(ResourceExecutionError::InvalidInvocation), + } + } + pub(crate) fn parameters( + &self, + checked: &CheckedExecution, + ) -> Result, ResourceExecutionError> { + let signature = self.signature(checked)?; + let Type::Function { + params, + view_params, + .. + } = checked.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + match self { + Self::Source(invocation) + | Self::NamedSource { + source: invocation, .. + } => Ok(invocation + .arguments() + .iter() + .map(|argument| FunctionParameter { + index: argument.parameter_index, + ty: argument.parameter_type, + access: argument.callee_access, + }) + .collect()), + Self::Entry { .. } => Ok(params + .iter() + .zip(view_params) + .enumerate() + .map(|(index, (ty, view))| FunctionParameter { + index, + ty: *ty, + access: if *view { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + }, + }) + .collect()), + } + } +} + +pub(crate) struct CheckedExecution { + program: Arc, + purpose: ExecutionPurpose, + body: CheckedBodyCursor, + resources: HashMap, +} + +struct BodyFacts<'a> { + assignment_places: &'a HashMap, + selections: Option<&'a HashMap>, + calls: &'a HashMap, + actor_messages: &'a HashMap, + argument_orders: &'a HashMap, + bindings: &'a HashMap, + types: &'a HashMap, + source_types: &'a HashMap, + pipeline_inputs: &'a HashMap, + pipeline_calls: &'a HashMap, + scopes: &'a HashMap>, + intrinsics: &'a HashMap, + intrinsic_types: &'a HashMap>, + intrinsic_reflections: &'a HashMap>, + constructions: &'a HashMap, +} + +impl<'a> BodyFacts<'a> { + fn scoped(body: &'a CheckedBodyFacts) -> Self { + Self { + assignment_places: &body.assignment_places, + selections: Some(&body.static_selections), + calls: &body.call_ownership, + actor_messages: &body.actor_message_calls, + argument_orders: &body.call_argument_orders, + bindings: &body.binding_facts, + types: &body.type_map, + source_types: &body.source_type_map, + pipeline_inputs: &body.pipeline_step_input_types, + pipeline_calls: &body.pipeline_step_call_types, + scopes: &body.comptime_type_bindings, + intrinsics: &body.intrinsic_ids, + intrinsic_types: &body.intrinsic_type_arguments, + intrinsic_reflections: &body.intrinsic_reflection_arguments, + constructions: &body.struct_constructions, + } + } +} + +// A present resolution is authoritative only after it rejoins this exact +// retained executable declaration. Ordinary declarations have no such key. +fn retained_declaration_definition( + module: &jett_parser::ast::Module, + definitions: &[jett_resolve::DefInfo], + resolutions: &HashMap, + declaration: Span, +) -> Result { + use jett_parser::ast::Item; + use jett_resolve::DefKind; + + let mut actuals = module + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Function(function) if function.name.span == declaration => { + Some((index, function)) + } + _ => None, + }); + let (index, function) = actuals + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if actuals.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let namespace_at = |index: usize| { + module.items[..index] + .iter() + .filter_map(|item| match item { + Item::Namespace(namespace) if namespace.span.file == declaration.file => { + Some(namespace.name.name.as_str()) + } + _ => None, + }) + .last() + }; + let namespace = namespace_at(index); + let definition = match resolutions.get(&declaration) { + Some(definition) => *definition, + None => { + let mut candidates = definitions + .iter() + .filter(|info| info.kind == DefKind::Function && info.span == declaration); + let info = candidates + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if candidates.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + info.id + } + }; + let info = definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let canonical_name = namespace + .map(|namespace| format!("{namespace}.{}", function.name.name)) + .unwrap_or_else(|| function.name.name.clone()); + if info.id != definition + || info.kind != DefKind::Function + || info.namespace.as_deref() != namespace + || info.name != canonical_name + { + return Err(ResourceExecutionError::InvalidInvocation); + } + if info.span != declaration { + // Mutual definitions resolve to their original forward declaration. + // Rejoin that exact retained node, not another same-signature function. + let forwards = module + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Mutual(block) => Some((index, block)), + _ => None, + }) + .flat_map(|(index, block)| { + block + .declarations + .iter() + .map(move |forward| (index, forward)) + }) + .filter(|(index, forward)| { + forward.name.span == info.span + && forward.name.name == function.name.name + && namespace_at(*index) == namespace + }) + .count(); + if forwards != 1 { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + Ok(definition) +} + +impl CheckedExecution { + pub(crate) fn new( + program: Arc, + purpose: ExecutionPurpose, + ) -> Result { + validate_resource_hooks(program.module(), program.resolved(), program.checked()) + .map_err(|_| ResourceExecutionError::InvalidCheckedProgram)?; + let mut resources: HashMap = HashMap::new(); + for hook in program.checked().resource_hooks.values() { + if let Some(existing) = resources.get(&hook.resource_definition) { + if existing.checked_type != hook.resource_type { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + continue; + } + // A per-program registry type. The registry itself binds a runtime + // context, so equal numeric IDs in independent sessions confer none. + let number = u32::try_from(resources.len()) + .map_err(|_| ResourceExecutionError::IdentityExhausted)?; + resources.insert( + hook.resource_definition, + ResourceTypeBinding { + definition: hook.resource_definition, + checked_type: hook.resource_type, + registry_type: ResourceTypeId::new(number), + }, + ); + } + Ok(Self { + program, + purpose, + body: CheckedBodyCursor::ordinary(), + resources, + }) + } + + pub(crate) fn program(&self) -> &Arc { + &self.program + } + pub(crate) fn purpose(&self) -> ExecutionPurpose { + self.purpose + } + pub(crate) fn replace_purpose(&mut self, purpose: ExecutionPurpose) -> ExecutionPurpose { + std::mem::replace(&mut self.purpose, purpose) + } + pub(crate) fn cursor(&self) -> CheckedBodyCursor { + self.body.clone() + } + + fn facts(&self, cursor: &CheckedBodyCursor) -> Result, ResourceExecutionError> { + let checked = self.program.checked(); + let mut facts = match cursor.root { + BodyRoot::Ordinary => BodyFacts { + assignment_places: &checked.assignment_places, + selections: None, + calls: &checked.call_ownership, + actor_messages: &checked.actor_message_calls, + argument_orders: &checked.call_argument_orders, + bindings: &checked.binding_facts, + types: &checked.type_map, + source_types: &checked.source_type_map, + pipeline_inputs: &checked.pipeline_step_input_types, + pipeline_calls: &checked.pipeline_step_call_types, + scopes: &checked.comptime_type_bindings, + intrinsics: &checked.intrinsic_ids, + intrinsic_types: &checked.intrinsic_type_arguments, + intrinsic_reflections: &checked.intrinsic_reflection_arguments, + constructions: &checked.struct_constructions, + }, + BodyRoot::Generic(index) => { + let body = checked + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + BodyFacts { + assignment_places: &body.assignment_places, + selections: Some(&body.static_selections), + calls: &body.call_ownership, + actor_messages: &body.actor_message_calls, + argument_orders: &body.call_argument_orders, + bindings: &body.binding_facts, + types: &body.type_map, + source_types: &body.source_type_map, + pipeline_inputs: &body.pipeline_step_input_types, + pipeline_calls: &body.pipeline_step_call_types, + scopes: &body.comptime_type_bindings, + intrinsics: &body.intrinsic_ids, + intrinsic_types: &body.intrinsic_type_arguments, + intrinsic_reflections: &body.intrinsic_reflection_arguments, + constructions: &body.struct_constructions, + } + } + BodyRoot::ActorHandler(target) => { + let handler = checked + .actor_handler(target) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + BodyFacts::scoped(handler.body()) + } + }; + for selection in &cursor.scopes { + let body = facts + .scopes + .get(&selection.owner) + .and_then(|scopes| scopes.get(selection.index)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if body.bound_type != selection.bound_type + || body.selection != selection.selection + || body.reflection != selection.reflection + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + facts = BodyFacts::scoped(&body.body); + } + Ok(facts) + } + + pub(crate) fn restore_cursor( + &mut self, + cursor: CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + self.facts(&cursor)?; + if cursor.executable.is_some() { + self.validate_executable_cursor(&cursor)?; + } + self.body = cursor; + Ok(()) + } + + pub(crate) fn enter_ordinary(&mut self) { + self.body = CheckedBodyCursor::ordinary(); + } + + pub(crate) fn enter_generic( + &mut self, + call: &CheckedGenericCall, + ) -> Result<(), ResourceExecutionError> { + let matches = self + .program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = matches.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.body = CheckedBodyCursor { + root: BodyRoot::Generic(*index), + scopes: Vec::new(), + executable: None, + }; + Ok(()) + } + + pub(crate) fn invocation( + &self, + span: Span, + ) -> Result { + let facts = self.facts(&self.body)?; + let packet = facts + .calls + .get(&span) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let mut sources = vec![false; packet.arguments.len()]; + let mut parameters = vec![false; packet.arguments.len()]; + for (index, argument) in packet.arguments.iter().enumerate() { + if argument.source_index != index + || argument.parameter_index >= parameters.len() + || parameters[argument.parameter_index] + { + return Err(ResourceExecutionError::InvalidInvocation); + } + sources[index] = true; + parameters[argument.parameter_index] = true; + if facts.source_types.get(&argument.source_span) != Some(&argument.actual_type) { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + if sources.iter().any(|present| !present) || parameters.iter().any(|present| !present) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(CheckedInvocation { + packet: packet.clone(), + body: self.body.clone(), + span, + }) + } + + pub(crate) fn enum_constructor( + &self, + callee: &jett_parser::ast::Expr, + invocation: &CheckedInvocation, + ) -> Result, ResourceExecutionError> { + use jett_parser::ast::Expr; + use jett_resolve::DefKind; + let CheckedInvocationTarget::Indirect(signature) = invocation.target() else { + return Ok(None); + }; + let facts = self.facts(&invocation.body)?; + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let (owner, variant) = match callee { + Expr::EnumVariant(owner, variant, _) => (owner.span, variant), + Expr::FieldAccess(owner, variant, _) => (owner.span(), variant), + _ => return Ok(None), + }; + let Some(definition) = self.program.resolved().resolutions.get(&owner) else { + return Ok(None); + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != *definition { + return Err(ResourceExecutionError::InvalidInvocation); + } + if !matches!(info.kind, DefKind::Enum | DefKind::Type) { + return Ok(None); + } + if signature.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + return_type, + } = self.program.checked().interner.resolve(*signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if invocation.packet.shape + != (CheckedInvocationShape::Function { + signature_type: *signature, + }) + || facts.source_types.get(&invocation.span) != Some(return_type) + || facts.types.get(&callee.span()) != Some(signature) + || self.program.checked().definition_types.get(definition) != Some(return_type) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Enum(enum_id) = self.program.checked().interner.resolve(*return_type) else { + return Ok(None); + }; + let nominal = self.program.checked().interner.resolve_enum(*enum_id); + let variants = nominal + .variants + .iter() + .filter(|candidate| candidate.name == variant.name) + .collect::>(); + let [selected] = variants.as_slice() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != selected.fields.len() + || view_params.len() != params.len() + || view_params.iter().any(|view| *view) + || params + .iter() + .zip(&selected.fields) + .any(|(parameter, (_, field))| parameter != field) + || invocation.arguments().len() != params.len() + || invocation.arguments().iter().any(|argument| { + params.get(argument.parameter_index) != Some(&argument.parameter_type) + || argument.callee_access != jett_typecheck::CheckedCalleeAccess::Owned + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(Some(( + nominal.name.clone(), + selected.name.clone(), + *return_type, + ))) + } + + pub(crate) fn entry( + &self, + definition: DefId, + ) -> Result, ResourceExecutionError> { + let definition_info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if definition_info.id != definition + || definition_info.kind != jett_resolve::DefKind::Function + || self.has_hook(definition) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let signature = *self + .program + .checked() + .definition_types + .get(&definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(FunctionInvocation::Entry { + definition, + signature, + }) + } + + pub(crate) fn referenced_in(&self, definition: DefId, body: Span) -> bool { + self.program + .resolved() + .resolutions + .iter() + .any(|(span, resolved)| { + *resolved == definition + && span.file == body.file + && span.start >= body.start + && span.end <= body.end + }) + } + + pub(crate) fn declaration_definition( + &self, + declaration: Span, + ) -> Result { + let definition = retained_declaration_definition( + self.program.module(), + &self.program.resolved().scope_table.definitions, + &self.program.resolved().resolutions, + declaration, + )?; + if self.has_hook(definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(definition) + } + + /// Return only the executable node owned by this exact checked program. + /// Runtime registration clones never provide a replacement body. + pub(crate) fn retained_function( + &self, + definition: DefId, + ) -> Result<&jett_parser::ast::FunctionDef, ResourceExecutionError> { + if self.has_hook(definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut matches = self + .program + .module() + .items + .iter() + .filter_map(|item| match item { + jett_parser::ast::Item::Function(function) + if self.declaration_definition(function.name.span) == Ok(definition) => + { + Some(function) + } + _ => None, + }); + let function = matches + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if matches.next().is_some() { + return Err(ResourceExecutionError::InvalidInvocation); + } + if function.type_params.is_empty() { + let signature = self.entry(definition)?.signature(self)?; + let Type::Function { + params, + view_params, + .. + } = self.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != function.params.len() + || view_params.len() != function.params.len() + || !view_params + .iter() + .copied() + .eq(function.params.iter().map(|parameter| parameter.view)) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } else if !self + .program + .checked() + .generic_function_instantiations + .iter() + .any(|body| { + body.definition == definition + && body.concrete_args.len() == function.type_params.len() + && body.parameter_types.len() == function.params.len() + }) + { + // Generic templates have no fabricated definition_types entry. + // Exact concrete args/specialization/signature remain invocation + // and enter_generic obligations, independent of this body join. + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(function) + } + + pub(crate) fn source_function_context( + &self, + declaration: Span, + ) -> Result<(Option, bool), ResourceExecutionError> { + let definition = self.declaration_definition(declaration)?; + let info = &self.program.resolved().scope_table.definitions[definition.index() as usize]; + let trusted = self.program.source_origins().get(&declaration.file) + == Some(&jett_common::SourceOrigin::Stdlib); + Ok((info.namespace.clone(), trusted)) + } + + pub(crate) fn type_contains_resource( + &self, + ty: TypeId, + ) -> Result { + fn visit( + types: &jett_types::TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> Result { + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !seen.insert(ty) { + return Ok(false); + } + let children: Vec = match types.resolve(ty) { + Type::Resource(_) => return Ok(true), + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => vec![*inner], + Type::Map(key, value) | Type::Result(key, value) => vec![*key, *value], + Type::Function { + params, + return_type, + .. + } => params + .iter() + .copied() + .chain(std::iter::once(*return_type)) + .collect(), + Type::Struct(owner) => types + .resolve_struct(*owner) + .fields + .iter() + .map(|field| field.1) + .collect(), + Type::Enum(owner) => types + .resolve_enum(*owner) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|field| field.1)) + .collect(), + Type::Machine(owner) => types + .resolve_machine(*owner) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|field| field.1)) + .collect(), + Type::MachineState { machine, state } => types + .resolve_machine(*machine) + .state(*state) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)? + .fields + .iter() + .map(|field| field.1) + .collect(), + Type::Bitfield(owner) => types + .resolve_bitfield(*owner) + .fields + .iter() + .map(|field| field.ty) + .collect(), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::TypeConstruction + | Type::Never + | Type::Capability(_) + | Type::Interface(_) + | Type::Actor(_) => Vec::new(), + Type::Error => return Err(ResourceExecutionError::InvalidCheckedProgram), + }; + for child in children { + if visit(types, child, seen)? { + return Ok(true); + } + } + Ok(false) + } + visit( + &self.program.checked().interner, + ty, + &mut std::collections::HashSet::new(), + ) + } + + pub(crate) fn expression_type(&self, span: Span) -> Result { + self.facts(&self.body)? + .types + .get(&span) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) + } + + pub(crate) fn resolved_definition(&self, span: Span) -> Result { + self.program + .resolved() + .resolutions + .get(&span) + .copied() + .ok_or(ResourceExecutionError::InvalidInvocation) + } + + pub(crate) fn has_hook(&self, definition: DefId) -> bool { + self.program + .checked() + .resource_hooks + .contains_key(&definition) + } + + pub(crate) fn has_invocation(&self, span: Span) -> Result { + Ok(self.facts(&self.body)?.calls.contains_key(&span)) + } + + pub(crate) fn binding( + &self, + declaration: Span, + ) -> Result { + self.facts(&self.body)? + .bindings + .get(&declaration) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) + } + + pub(crate) fn descriptor( + &self, + definition: DefId, + ) -> Result { + self.program + .checked() + .resource_hooks + .get(&definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + Ok(ResourceHookDescriptor { + program: self.program.clone(), + definition, + }) + } + + pub(crate) fn reached_hook( + &self, + invocation: &CheckedInvocation, + descriptor: &ResourceHookDescriptor, + ) -> Result< + ( + CheckedResourceHook, + ResourceTypeBinding, + ResourceKernelRecipe, + ), + ResourceExecutionError, + > { + if !Arc::ptr_eq(&self.program, &descriptor.program) { + return Err(ResourceExecutionError::ForeignProgram); + } + // Retained lexical cursor is validated independently of the function + // currently running after nested actual evaluation. + let facts = self.facts(&invocation.body)?; + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let hook = self + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let target_matches = match &invocation.packet.target { + CheckedInvocationTarget::Resolved(definition) => *definition == hook.definition, + CheckedInvocationTarget::Indirect(function_type) => { + *function_type == hook.function_type + } + CheckedInvocationTarget::Generic(_) + | CheckedInvocationTarget::Method(_) + | CheckedInvocationTarget::Interface(_) + | CheckedInvocationTarget::Intrinsic(_) => false, + }; + if !target_matches + || invocation.packet.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type, + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let resource = *self + .resources + .get(&hook.resource_definition) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if resource.checked_type != hook.resource_type { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + let recipe = match hook.kind { + ResourceHookKind::Construct => ResourceKernelRecipe::NetworkFactory, + ResourceHookKind::BorrowOperation => ResourceKernelRecipe::NetworkBorrow, + ResourceHookKind::Close => ResourceKernelRecipe::Finalize, + }; + if !recipe.matches_signature( + &self.program.checked().interner, + hook.resource_type, + hook.function_type, + ) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + .. + } = self.program.checked().interner.resolve(hook.function_type) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if invocation.packet.arguments.len() != params.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + for argument in &invocation.packet.arguments { + let index = argument.parameter_index; + let view = argument.callee_access == jett_typecheck::CheckedCalleeAccess::View; + if params[index] != argument.parameter_type || view_params[index] != view { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + if self.purpose != ExecutionPurpose::ReferenceRuntime { + return Err(ResourceExecutionError::WrongPurpose); + } + Ok((hook.clone(), resource, recipe)) + } +} + +#[cfg(test)] +mod declaration_tests { + use super::*; + use jett_parser::ast::Item; + use jett_resolve::DefKind; + + const ORDINARY: &str = r#"namespace other +export function answer() returns int64: + return 99 +namespace app +export function answer() returns int64: + return 7 +"#; + + const MUTUAL: &str = r#"namespace app +mutual: + function first() returns int64 + function second() returns int64 +function first() returns int64: + return second() +function second() returns int64: + return 11 +export function scenario() returns int64: + return first() +"#; + + fn original<'a>( + program: &'a CheckedResourceProgram, + namespace: &str, + name: &str, + ) -> &'a jett_parser::ast::FunctionDef { + let mut functions = program.module().items.iter().filter_map(|item| match item { + Item::Function(function) + if function.name.name == name + && program + .resolved() + .scope_table + .definitions + .iter() + .any(|info| { + info.kind == DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some(namespace) + }) => + { + Some(function) + } + _ => None, + }); + let function = functions + .next() + .expect("unique original ordinary declaration"); + assert!(functions.next().is_none()); + function + } + + #[test] + fn retained_declaration_ordinary_has_no_resolution_and_no_name_fallback() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(ORDINARY, release); + let original = original(&program, "app", "answer"); + assert!( + !program + .resolved() + .resolutions + .contains_key(&original.name.span) + ); + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = execution + .declaration_definition(original.name.span) + .unwrap(); + assert!(std::ptr::eq( + execution.retained_function(definition).unwrap(), + original + )); + let info = &program.resolved().scope_table.definitions[definition.index() as usize]; + assert_eq!(info.span, original.name.span); + assert_eq!(info.name, "app.answer"); + assert_eq!( + execution + .source_function_context(original.name.span) + .unwrap(), + (Some("app".to_string()), false) + ); + } + } + + #[test] + fn retained_declaration_present_malformed_resolution_never_uses_ordinary_fallback() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(ORDINARY, release); + let declaration = original(&program, "app", "answer").name.span; + let foreign = original(&program, "other", "answer").name.span; + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let foreign_definition = execution.declaration_definition(foreign).unwrap(); + for wrong in [foreign_definition, DefId::new(u32::MAX)] { + let mut resolutions = program.resolved().resolutions.clone(); + resolutions.insert(declaration, wrong); + assert_eq!( + retained_declaration_definition( + program.module(), + &program.resolved().scope_table.definitions, + &resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + let mut definitions = program.resolved().scope_table.definitions.clone(); + let definition = execution.declaration_definition(declaration).unwrap(); + let duplicate = definitions[definition.index() as usize].clone(); + definitions.push(duplicate); + assert_eq!( + retained_declaration_definition( + program.module(), + &definitions, + &program.resolved().resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + let mut module = program.module().clone(); + module + .items + .push(Item::Function(original(&program, "app", "answer").clone())); + assert_eq!( + retained_declaration_definition( + &module, + &program.resolved().scope_table.definitions, + &program.resolved().resolutions, + declaration + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + } + + #[test] + fn retained_declaration_mutual_rejoins_exact_forward_and_actual_nodes() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(MUTUAL, release); + let actual = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "first" => Some(function), + _ => None, + }) + .unwrap(); + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = execution.declaration_definition(actual.name.span).unwrap(); + assert_eq!( + program.resolved().resolutions.get(&actual.name.span), + Some(&definition) + ); + assert_ne!( + program.resolved().scope_table.definitions[definition.index() as usize].span, + actual.name.span + ); + assert!(std::ptr::eq( + execution.retained_function(definition).unwrap(), + actual + )); + let mut resolutions = program.resolved().resolutions.clone(); + resolutions.remove(&actual.name.span); + assert_eq!( + retained_declaration_definition( + program.module(), + &program.resolved().scope_table.definitions, + &resolutions, + actual.name.span + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + let mut definitions = program.resolved().scope_table.definitions.clone(); + definitions[definition.index() as usize].span = actual.body.span; + assert_eq!( + retained_declaration_definition( + program.module(), + &definitions, + &program.resolved().resolutions, + actual.name.span + ), + Err(ResourceExecutionError::InvalidInvocation) + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/assignment.rs b/crates/jett_comptime/src/resource_execution/checked/assignment.rs new file mode 100644 index 00000000..493e5eb4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/assignment.rs @@ -0,0 +1,162 @@ +//! A replacement joins the original statement and its complete checked region. +use super::*; +use jett_parser::ast::{AssignStmt, Expr}; +use jett_resolve::DefKind; +use jett_typecheck::CheckedBindingMode; + +pub(crate) struct CheckedAssignment<'source> { + source: &'source AssignStmt, + fact: CheckedBindingFact, + key: CheckedAttemptKey, +} + +impl CheckedAssignment<'_> { + pub(crate) fn fact(&self) -> CheckedBindingFact { + self.fact + } +} + +impl CheckedExecution { + pub(crate) fn prepare_assignment<'source>( + &self, + source: &'source AssignStmt, + ) -> Result, ResourceExecutionError> { + let key = self.original_assignment_key(source)?; + let Expr::Ident(target) = &source.target else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let definition = self.resolved_definition(target.span)?; + let facts = self.facts(&self.body)?; + let mut candidates = facts + .bindings + .iter() + .filter(|(_, fact)| fact.definition == definition); + let (declaration, fact) = candidates + .next() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let definition_info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if candidates.next().is_some() + || *declaration != fact.declaration_span + || definition_info.id != definition + || definition_info.span != fact.declaration_span + || definition_info.name != target.name + || !matches!(definition_info.kind, DefKind::Variable | DefKind::Param) + || !fact.mutable + || fact.mode != CheckedBindingMode::Owned + || self.expression_type(target.span)? != fact.ty + || self.expression_type(source.value.span())? != fact.ty + || !self.type_contains_resource(fact.ty)? + { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(CheckedAssignment { + source, + fact: *fact, + key, + }) + } + + pub(crate) fn revalidate_assignment( + &self, + assignment: &CheckedAssignment<'_>, + ) -> Result<(), ResourceExecutionError> { + let current = self.prepare_assignment(assignment.source)?; + if current.fact != assignment.fact || current.key != assignment.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::FileId; + use jett_parser::ast::{FunctionDef, Item, Stmt}; + + const SOURCE: &str = include_str!("../fixtures/24_mutable_assignment.jett"); + + fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap() + } + + fn assignment(function: &FunctionDef) -> &AssignStmt { + function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Assign(assignment) => Some(assignment), + _ => None, + }) + .unwrap() + } + + fn install(checked: &mut CheckedExecution, function: &FunctionDef) { + let definition = checked.declaration_definition(function.name.span).unwrap(); + let invocation = checked.entry(definition).unwrap(); + let reference = checked.prepare_function_body(&invocation).unwrap(); + checked.install_function_body(&reference).unwrap(); + } + + #[test] + fn checked_resource_assignment_refuses_clones_foreign_programs_and_other_bodies() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let original = function(&program, "replace_live"); + let original_assignment = assignment(original); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, original); + checked.prepare_assignment(original_assignment).unwrap(); + let clone = original_assignment.clone(); + assert!(checked.prepare_assignment(&clone).is_err()); + let foreign = crate::resource_execution::tests::program(SOURCE, release); + assert!( + checked + .prepare_assignment(assignment(function(&foreign, "replace_live"))) + .is_err() + ); + install(&mut checked, function(&program, "rebind_after_close")); + assert!(checked.prepare_assignment(original_assignment).is_err()); + } + } + + #[test] + fn checked_resource_assignment_rejoins_prepared_facts_and_body_after_rhs() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let original = function(&program, "replace_live"); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, original); + let mut prepared = checked.prepare_assignment(assignment(original)).unwrap(); + checked.revalidate_assignment(&prepared).unwrap(); + prepared.fact.mutable = false; + assert!(checked.revalidate_assignment(&prepared).is_err()); + prepared.fact.mutable = true; + install(&mut checked, function(&program, "rebind_after_close")); + assert!(checked.revalidate_assignment(&prepared).is_err()); + checked.enter_ordinary(); + assert!(checked.revalidate_assignment(&prepared).is_err()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference.rs new file mode 100644 index 00000000..3cb26fb1 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference.rs @@ -0,0 +1,1254 @@ +//! Original executable identity for the first checked scoped-entry layer. +//! No runtime ownership or provider authority is carried by these references. + +use jett_intrinsics::IntrinsicId; +use jett_parser::ast::{ + AssignStmt, Block, CallArg, ComptimeTypeBindStmt, Expr, FunctionDef, Item, Stmt, StringPart, +}; + +use super::*; +use crate::checked_types::{CheckedScopedBindings, CheckedScopedTypes}; + +#[path = "body_reference/field_updates.rs"] +mod field_updates; +pub(crate) use field_updates::{PreparedFieldAssignment, PreparedFieldBinding}; +#[path = "body_reference/actor_handlers.rs"] +mod actor_handlers; +#[path = "body_reference/field_inline.rs"] +mod field_inline; +pub(crate) use actor_handlers::{CheckedActorInstanceOrigin, PreparedActorHandler}; + +#[path = "body_reference/absent_generated.rs"] +mod absent_generated; +pub(crate) use absent_generated::PreparedAbsentBindings; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ExecutableOrigin { + Function { + definition: DefId, + item: usize, + }, + ActorInitializer { + item: usize, + field: usize, + }, + ActorHandler { + definition: DefId, + item: usize, + handler: usize, + }, + NamespaceInitializer { + item: usize, + }, + RefinementPredicate { + definition: DefId, + item: usize, + }, + Verify { + item: usize, + }, + Property { + item: usize, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum RegionStep { + Scope(Span), + Expression(usize), + InlineBody(usize), +} + +#[derive(Debug, Clone)] +pub(super) struct ExecutableIdentity { + program: Arc, + origin: ExecutableOrigin, + path: Vec, +} + +#[derive(Clone, Copy)] +enum OriginalRegion<'a> { + Block(&'a Block), + Expression(&'a Expr), +} + +#[path = "body_reference/required.rs"] +mod required; +pub use required::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, +}; +pub(crate) use required::{CheckedAttemptKey, PreparedRequiredExpression}; +#[path = "body_reference/pipeline.rs"] +mod pipeline; +pub(crate) use pipeline::PreparedPipelineStep; +#[path = "body_reference/intrinsic.rs"] +mod intrinsic; +pub(crate) use intrinsic::PreparedIntrinsicArguments; +#[path = "body_reference/named_callable.rs"] +mod named_callable; +#[path = "body_reference/ordinary_field_reflection.rs"] +mod ordinary_field_reflection; +pub(crate) use named_callable::PreparedNamedCallable; +#[path = "body_reference/owned_struct.rs"] +mod owned_struct; +pub(crate) use owned_struct::PreparedOwnedStruct; +#[path = "body_reference/reflected_fields.rs"] +mod reflected_fields; +pub(crate) use reflected_fields::{PreparedReflectedFieldIteration, PreparedReflectedFieldLoop}; + +/// Minted only from one retained program and an exact accepted body selection. +#[derive(Debug, Clone)] +pub(crate) struct CheckedBodyReference { + cursor: CheckedBodyCursor, +} + +pub(crate) struct PreparedDirectScope { + reference: CheckedBodyReference, + projection: Arc, + reflection: ReflectionTypeInfo, +} + +impl CheckedBodyReference { + pub(crate) fn function(&self) -> Result<&FunctionDef, ResourceExecutionError> { + let identity = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let ExecutableOrigin::Function { item, .. } = identity.origin else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + match identity.program.module().items.get(item) { + Some(Item::Function(function)) => Ok(function), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } + + fn region(&self) -> Result, ResourceExecutionError> { + let length = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .len(); + self.region_at_path_len(length) + } + + fn region_at_path_len( + &self, + length: usize, + ) -> Result, ResourceExecutionError> { + let identity = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let mut region = match identity.origin { + ExecutableOrigin::Function { .. } => OriginalRegion::Block(&self.function()?.body), + ExecutableOrigin::NamespaceInitializer { item } => { + match identity.program.module().items.get(item) { + Some(Item::VarDecl(value)) => OriginalRegion::Expression(&value.value), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::RefinementPredicate { item, .. } => { + match identity.program.module().items.get(item) { + Some(Item::TypeAlias(alias)) => OriginalRegion::Expression( + alias + .constraint + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?, + ), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::ActorInitializer { item, field } => { + match identity.program.module().items.get(item) { + Some(Item::Actor(actor)) => OriginalRegion::Expression( + &actor + .state_fields + .get(field) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .value, + ), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::ActorHandler { item, handler, .. } => { + let Some(Item::Actor(actor)) = identity.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + OriginalRegion::Block( + &actor + .handlers + .get(handler) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .body, + ) + } + ExecutableOrigin::Verify { item } => match identity.program.module().items.get(item) { + Some(Item::Verify(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => { + match identity.program.module().items.get(item) { + Some(Item::Property(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + }; + if length > identity.path.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + for step in identity.path.iter().take(length) { + region = match step { + RegionStep::Scope(owner) => { + OriginalRegion::Block(®ion_binding(region, *owner)?.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + RegionStep::InlineBody(index) => { + OriginalRegion::Block(region_inline_body(region, *index)?) + } + }; + } + Ok(region) + } + + fn block(&self) -> Result<&Block, ResourceExecutionError> { + match self.region()? { + OriginalRegion::Block(block) => Ok(block), + OriginalRegion::Expression(_) => Err(ResourceExecutionError::MissingCheckedBody), + } + } + pub(crate) fn original_expression(&self) -> Result<&Expr, ResourceExecutionError> { + match self.region()? { + OriginalRegion::Expression(value) => Ok(value), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } +} + +impl PreparedDirectScope { + pub(crate) fn field_reference(&self) -> CheckedBodyReference { + self.reference.clone() + } + pub(crate) fn projection(&self) -> Arc { + self.projection.clone() + } + pub(crate) fn reflection(&self) -> &ReflectionTypeInfo { + &self.reflection + } + pub(crate) fn bound_name(&self) -> &str { + &self.projection.bound_type + } + pub(crate) fn body(&self) -> Result<&Block, ResourceExecutionError> { + self.reference.block() + } +} + +impl CheckedExecution { + fn refinement_definition( + &self, + alias: &jett_parser::ast::TypeAlias, + ) -> Result { + let original = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| { + matches!(item, Item::TypeAlias(value) if std::ptr::eq(value, alias)) + .then_some(index) + }) + .collect::>(); + let [item] = original.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if alias.constraint.is_none() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let namespace = self.program.module().items[..*item] + .iter() + .filter_map(|item| match item { + Item::Namespace(value) if value.span.file == alias.name.span.file => { + Some(value.name.name.as_str()) + } + _ => None, + }) + .last(); + let canonical = namespace.map_or_else( + || alias.name.name.clone(), + |namespace| format!("{namespace}.{}", alias.name.name), + ); + let definitions = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Type + && definition.span == alias.name.span + && definition.namespace.as_deref() == namespace + && definition.name == canonical + }) + .collect::>(); + let [definition] = definitions.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if self.program.resolved().resolutions.get(&alias.name.span).is_some_and(|resolved| *resolved != definition.id) + || self.program.checked().interner.type_ids().filter(|ty| { + matches!(self.program.checked().interner.resolve(*ty), Type::Refinement { name, .. } if name == &canonical) + }).count() != 1 + { return Err(ResourceExecutionError::MissingCheckedBody); } + Ok(definition.id) + } + + /// Retain original actor initializer nodes before interpreter scope mutation. + /// Registration clones and equal spans never supply executable call authority. + pub(crate) fn prepare_actor_initializers( + &self, + actor: &jett_parser::ast::ActorDef, + ) -> Result, ResourceExecutionError> { + let matches = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(item, value)| match value { + Item::Actor(original) + if original.span == actor.span && original.name.span == actor.name.span => + { + Some((item, original)) + } + _ => None, + }) + .collect::>(); + let [(item, original)] = matches.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let declarations = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Actor + && definition.span == original.name.span + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let mut registered = (*original).clone(); + registered.name.name = declaration.name.clone(); + if format!("{registered:?}") != format!("{actor:?}") { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let mut references = Vec::new(); + for field in 0..original.state_fields.len() { + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::ActorInitializer { item: *item, field }, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + let expression = reference.original_expression()?; + self.facts(&reference.cursor)? + .types + .get(&expression.span()) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + references.push(reference); + } + Ok(references) + } + pub(crate) fn prepare_function_body( + &self, + invocation: &FunctionInvocation, + ) -> Result { + let (definition, root) = match invocation { + FunctionInvocation::Entry { definition, .. } => (*definition, BodyRoot::Ordinary), + FunctionInvocation::NamedSource { source, target } => { + self.validate_named_indirect(source, target)?; + (target.definition(), BodyRoot::Ordinary) + } + FunctionInvocation::Source(invocation) => { + self.validate_executable_cursor(&invocation.body)?; + if self.facts(&invocation.body)?.calls.get(&invocation.span) + != Some(&invocation.packet) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + match invocation.target() { + CheckedInvocationTarget::Resolved(definition) => { + (*definition, BodyRoot::Ordinary) + } + CheckedInvocationTarget::Generic(call) => { + let indices = self + .program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = indices.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + (call.definition, BodyRoot::Generic(*index)) + } + _ => return Err(ResourceExecutionError::InvalidInvocation), + } + } + }; + let function = self.retained_function(definition)?; + let signature = invocation.signature(self)?; + if signature.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Type::Function { + params, + view_params, + return_type, + } = self.program.checked().interner.resolve(signature) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != function.params.len() + || view_params.len() != params.len() + || function + .params + .iter() + .zip(view_params) + .any(|(parameter, view)| parameter.view != *view) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + match root { + BodyRoot::Ordinary => { + if self.program.checked().definition_types.get(&definition) != Some(&signature) { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + BodyRoot::Generic(index) => { + let concrete = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if concrete.parameter_types != *params || concrete.return_type != *return_type { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + BodyRoot::ActorHandler(_) => return Err(ResourceExecutionError::InvalidInvocation), + } + if let Some(source) = invocation.source() { + if source.arguments().len() != params.len() + || source.arguments().iter().any(|argument| { + params.get(argument.parameter_index) != Some(&argument.parameter_type) + || view_params.get(argument.parameter_index).copied() + != Some( + argument.callee_access == jett_typecheck::CheckedCalleeAccess::View, + ) + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| { + matches!(item, Item::Function(original) if std::ptr::eq(original, function)) + .then_some(index) + }) + .collect::>(); + let [function_item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::Function { + definition, + item: *function_item, + }, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + Ok(reference) + } + + pub(super) fn validate_executable_cursor( + &self, + cursor: &CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + let identity = cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if !Arc::ptr_eq(&identity.program, &self.program) { + return Err(ResourceExecutionError::ForeignProgram); + } + if matches!(identity.origin, ExecutableOrigin::ActorHandler { .. }) { + return Err(ResourceExecutionError::WrongPurpose); + } + if (matches!( + identity.origin, + ExecutableOrigin::RefinementPredicate { .. } + ) || identity + .path + .iter() + .any(|step| matches!(step, RegionStep::InlineBody(_)))) + && self.purpose != ExecutionPurpose::ExplicitComptime + { + return Err(ResourceExecutionError::WrongPurpose); + } + self.validate_cursor_identity(cursor) + } + + /// Ordinary field storage has no provider/custody route. Resource APIs keep + /// calling validate_executable_cursor and retain their runtime-inline gate. + pub(super) fn validate_field_executable_cursor( + &self, + cursor: &CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + self.validate_cursor_identity(cursor) + } + + fn validate_cursor_identity( + &self, + cursor: &CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + let identity = cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if !Arc::ptr_eq(&identity.program, &self.program) { + return Err(ResourceExecutionError::ForeignProgram); + } + // Required syntax selection carries no authority to execute an inline + // callable or a refinement predicate as a runtime Resource entry. + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + match identity.origin { + ExecutableOrigin::Function { definition, .. } => { + let function = reference.function()?; + if !std::ptr::eq(self.retained_function(definition)?, function) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + match cursor.root { + BodyRoot::Ordinary if !function.type_params.is_empty() => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + BodyRoot::Generic(index) => { + let instance = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if instance.definition != definition + || instance.concrete_args.len() != function.type_params.len() + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + BodyRoot::ActorHandler(_) => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + BodyRoot::Ordinary => {} + } + } + ExecutableOrigin::ActorHandler { + definition, + item, + handler, + } => { + let BodyRoot::ActorHandler(target) = cursor.root else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if target.actor_definition != definition || target.handler_index != handler { + return Err(ResourceExecutionError::MissingCheckedBody); + } + self.validate_actor_handler_identity(target, item)?; + } + ExecutableOrigin::RefinementPredicate { definition, item } => { + let Some(Item::TypeAlias(alias)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if alias.constraint.is_none() || self.refinement_definition(alias)? != definition { + return Err(ResourceExecutionError::MissingCheckedBody); + } + if !matches!(cursor.root, BodyRoot::Ordinary) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + _ if !matches!(cursor.root, BodyRoot::Ordinary) => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + _ => {} + } + let path_scopes = identity + .path + .iter() + .filter_map(|step| match step { + RegionStep::Scope(owner) => Some(*owner), + _ => None, + }) + .collect::>(); + if path_scopes + != cursor + .scopes + .iter() + .map(|scope| scope.owner) + .collect::>() + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + self.facts(cursor)?; + reference.region()?; + Ok(()) + } + + pub(crate) fn install_function_body( + &mut self, + reference: &CheckedBodyReference, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&reference.cursor)?; + self.body = reference.cursor.clone(); + Ok(()) + } + + pub(crate) fn install_direct_scope( + &mut self, + prepared: &PreparedDirectScope, + ) -> Result<(), ResourceExecutionError> { + self.install_function_body(&prepared.reference) + } + + pub(crate) fn prepare_direct_scope( + &self, + bind: &ComptimeTypeBindStmt, + ) -> Result { + self.validate_executable_cursor(&self.body)?; + let prepared = self.prepare_original_direct_scope(bind)?; + self.validate_executable_cursor(&prepared.reference.cursor)?; + Ok(prepared) + } + + /// Shares physical Source selection, without authorizing Resource entry. + pub(crate) fn prepare_field_direct_scope( + &self, + bind: &ComptimeTypeBindStmt, + ) -> Result { + self.validate_field_executable_cursor(&self.body)?; + let prepared = self.prepare_original_direct_scope(bind)?; + self.validate_field_executable_cursor(&prepared.reference.cursor)?; + Ok(prepared) + } + + fn prepare_original_direct_scope( + &self, + bind: &ComptimeTypeBindStmt, + ) -> Result { + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = region_binding(parent.region()?, bind.span)?; + if !std::ptr::eq(original, bind) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::GenericCall(_, source_types, arguments, occurrence) = &original.value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if source_types.len() != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let facts = self.facts(&self.body)?; + let intrinsic = facts + .intrinsics + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let types = facts + .intrinsic_types + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let reflections = facts + .intrinsic_reflections + .get(occurrence) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let ([source_type], [source_reflection]) = (types.as_slice(), reflections.as_slice()) + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if source_type.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let selected = facts + .scopes + .get(&original.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let candidates = selected + .iter() + .enumerate() + .filter(|(_, candidate)| { + candidate.selection == CheckedComptimeTypeSelection::Unconditional + }) + .collect::>(); + let [(index, candidate)] = candidates.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reflection_matches = match intrinsic { + IntrinsicId::TypeInfo if arguments.is_empty() => { + candidate.bound_type == *source_type && candidate.reflection == *source_reflection + } + IntrinsicId::TypeArg if arguments.len() == 1 && arguments[0].name.is_none() => { + let Expr::IntLiteral(index, _) = &arguments[0].value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let index = usize::try_from(*index) + .map_err(|_| ResourceExecutionError::MissingCheckedBody)?; + source_reflection.args.get(index) == Some(&candidate.reflection) + } + _ => false, + }; + if !reflection_matches { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let mut cursor = self.body.clone(); + cursor.scopes.push(ScopedSelection { + owner: original.span, + index: *index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(original.span)); + Ok(PreparedDirectScope { + reference: CheckedBodyReference { cursor }, + projection: scoped_projection(candidate, &self.program.checked().interner), + reflection: candidate.reflection.clone(), + }) + } + + /// True is mandatory checked dispatch, false is an exact checked constructor. + /// A missing body/packet never selects the raw source-name path. + #[cfg(test)] + pub(crate) fn original_body_scope_depth(&self) -> Option { + self.body + .executable + .as_ref() + .map(|_| self.body.scopes.len()) + } + + pub(crate) fn original_assignment( + &self, + assignment: &AssignStmt, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matched = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |_| {}, + &mut |statement| { + if let Stmt::Assign(original) = statement { + if std::ptr::eq(original, assignment) { + matched += 1; + } + } + }, + ); + if matched == 1 { + Ok(()) + } else { + Err(ResourceExecutionError::MissingCheckedBody) + } + } + + pub(crate) fn source_call_dispatch( + &self, + callee: &Expr, + arguments: &[CallArg], + span: Span, + ) -> Result { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matched = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + if let Expr::Call(original_callee, original_args, original_span) + | Expr::GenericCall(original_callee, _, original_args, original_span) = + expression + { + if *original_span == span + && std::ptr::eq(original_callee.as_ref(), callee) + && original_args.len() == arguments.len() + && (arguments.is_empty() + || std::ptr::eq(original_args.as_ptr(), arguments.as_ptr())) + { + matched += 1; + } + } + }, + &mut |_| {}, + ); + if matched != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + let facts = self.facts(&self.body)?; + if facts.calls.contains_key(&span) { + return Ok(true); + } + if facts.constructions.contains_key(&span) { + return Ok(false); + } + if self.typed_machine_constructor(callee, arguments, span)? { + return Ok(false); + } + Err(ResourceExecutionError::MissingCheckedInvocation) + } + + // The checker records machine construction as an exact nominal/state + // expression rather than a function invocation. Authenticate that existing + // typed data path separately; it grants no live Resource custody transport. + fn typed_machine_constructor( + &self, + callee: &Expr, + arguments: &[CallArg], + span: Span, + ) -> Result { + let definition = match self.resolved_definition(callee.span()) { + Ok(definition) => definition, + Err(_) => return Ok(false), + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != definition || info.kind != jett_resolve::DefKind::Machine { + return Ok(false); + } + let types = &self.program.checked().interner; + let declared = self + .program + .checked() + .definition_types + .get(&definition) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Type::Machine(machine) = types.resolve(*declared) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let facts = self.facts(&self.body)?; + let actual = facts + .source_types + .get(&span) + .or_else(|| facts.types.get(&span)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Type::MachineState { + machine: actual_machine, + state, + } = types.resolve(*actual) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if machine != actual_machine { + return Err(ResourceExecutionError::InvalidInvocation); + } + let state = types + .resolve_machine(*machine) + .state(*state) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let Some(first) = arguments.first() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let Expr::Ident(selected) = &first.value else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if first.name.is_some() + || selected.name != state.name + || arguments.len() != state.fields.len() + 1 + { + return Err(ResourceExecutionError::InvalidInvocation); + } + if arguments[1..] + .iter() + .zip(&state.fields) + .any(|(argument, (_, expected))| { + facts.types.get(&argument.value.span()) != Some(expected) + }) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(true) + } +} + +fn scoped_projection( + binding: &CheckedComptimeTypeBinding, + types: &jett_types::TypeInterner, +) -> Arc { + // Match the existing driver projection: descendants remain under their + // lexical parent, and their expression names are not reused outside it. + let descendants = binding + .body + .comptime_type_bindings + .values() + .flatten() + .flat_map(|nested| nested.body.comptime_type_bindings.keys().copied()) + .collect::>(); + let bindings: CheckedScopedBindings = binding + .body + .comptime_type_bindings + .iter() + .filter(|(owner, _)| !descendants.contains(owner)) + .map(|(owner, candidates)| { + ( + *owner, + candidates + .iter() + .map(|candidate| scoped_projection(candidate, types)) + .collect(), + ) + }) + .collect(); + let mut expressions = binding + .body + .type_map + .iter() + .map(|(span, ty)| (*span, types.type_name(*ty))) + .collect(); + remove_descendant_expression_names(&mut expressions, &binding.body.comptime_type_bindings); + Arc::new(CheckedScopedTypes { + bound_type: types.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) +} + +fn remove_descendant_expression_names( + expressions: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + expressions.remove(span); + } + remove_descendant_expression_names(expressions, &binding.body.comptime_type_bindings); + } +} + +fn walk_region<'a>( + region: OriginalRegion<'a>, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + match region { + OriginalRegion::Block(block) => walk_block_nodes(block, binding, expression, statement), + OriginalRegion::Expression(value) => walk_expr_nodes(value, binding, expression, statement), + } +} + +fn region_binding( + region: OriginalRegion<'_>, + owner: Span, +) -> Result<&ComptimeTypeBindStmt, ResourceExecutionError> { + let mut found = Vec::new(); + walk_region( + region, + &mut |binding| { + if binding.span == owner { + found.push(binding); + } + }, + &mut |_| {}, + &mut |_| {}, + ); + let [binding] = found.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*binding) +} + +fn region_expression( + region: OriginalRegion<'_>, + index: usize, +) -> Result<&Expr, ResourceExecutionError> { + let mut found = Vec::new(); + walk_region( + region, + &mut |_| {}, + &mut |expression| { + found.push(expression); + }, + &mut |_| {}, + ); + found + .get(index) + .copied() + .ok_or(ResourceExecutionError::MissingCheckedBody) +} + +fn region_inline_body( + region: OriginalRegion<'_>, + index: usize, +) -> Result<&Block, ResourceExecutionError> { + match region_expression(region, index)? { + Expr::InlineFn(_, _, body, _) => Ok(body), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } +} + +fn expression_index( + region: OriginalRegion<'_>, + value: &Expr, +) -> Result { + let mut indices = Vec::new(); + let mut ordinal = 0usize; + walk_region( + region, + &mut |_| {}, + &mut |expression| { + if std::ptr::eq(value, expression) { + indices.push(ordinal); + } + ordinal += 1; + }, + &mut |_| {}, + ); + match indices.as_slice() { + [index] => Ok(*index), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } +} + +fn binding_in(block: &Block, owner: Span) -> Result<&ComptimeTypeBindStmt, ResourceExecutionError> { + let mut bindings = Vec::new(); + walk_block( + block, + &mut |binding| { + if binding.span == owner { + bindings.push(binding); + } + }, + &mut |_| {}, + ); + let [binding] = bindings.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*binding) +} + +/// Traverse executable control-flow only; scoped and callable bodies require +/// their own selected reference. Their initializer expressions remain here. +fn walk_block<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), +) { + walk_block_nodes(block, binding, expression, &mut |_| {}); +} + +fn walk_statements<'a>(block: &'a Block, statement: &mut impl FnMut(&'a Stmt)) { + walk_block_nodes(block, &mut |_| {}, &mut |_| {}, statement); +} + +fn walk_block_nodes<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + for node in &block.stmts { + statement(node); + match node { + Stmt::VarDecl(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::Assign(value) => { + walk_expr_nodes(&value.target, binding, expression, statement); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::Return(value) => { + if let Some(value) = &value.value { + walk_expr_nodes(value, binding, expression, statement); + } + } + Stmt::Respond(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::ComptimeTypeBind(value) => { + binding(value); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::If(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.then_block, binding, expression, statement); + for (condition, block) in &value.else_ifs { + walk_expr_nodes(condition, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + if let Some(block) = &value.else_block { + walk_block_nodes(block, binding, expression, statement); + } + } + Stmt::For(value) => { + walk_expr_nodes(&value.iterable, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::While(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::Match(value) => { + walk_expr_nodes(&value.expr, binding, expression, statement); + for arm in &value.arms { + walk_block_nodes(&arm.body, binding, expression, statement); + } + } + Stmt::Expr(value) => walk_expr_nodes(&value.expr, binding, expression, statement), + Stmt::Assert(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + if let Some(message) = &value.message { + walk_expr_nodes(message, binding, expression, statement); + } + } + Stmt::Breakpoint(value) => { + if let Some(condition) = &value.condition { + walk_expr_nodes(condition, binding, expression, statement); + } + } + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } + } +} + +fn walk_expr_nodes<'a>( + value: &'a Expr, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + expression(value); + match value { + Expr::Binary(left, _, right, _) => { + walk_expr_nodes(left, binding, expression, statement); + walk_expr_nodes(right, binding, expression, statement); + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Default(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => walk_expr_nodes(value, binding, expression, statement), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + walk_expr_nodes(callee, binding, expression, statement); + for argument in arguments { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + } + Expr::ListConstruct(values, _) => { + for value in values { + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::MapConstruct(values, _) => { + for (key, value) in values { + walk_expr_nodes(key, binding, expression, statement); + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::Handle(value, _, block, _) => { + walk_expr_nodes(value, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + if let StringPart::Expr(value) = part { + walk_expr_nodes(value, binding, expression, statement); + } + } + } + Expr::Pipeline(value, steps, _) => { + walk_expr_nodes(value, binding, expression, statement); + for step in steps { + walk_expr_nodes(&step.function, binding, expression, statement); + for argument in &step.extra_args { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + if let Some(handle) = &step.handle { + walk_block_nodes(&handle.body, binding, expression, statement); + } + } + } + Expr::InlineFn(_, _, _, _) + | Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => {} + } +} + +#[cfg(test)] +#[path = "body_reference/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs new file mode 100644 index 00000000..1f69e4d7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated.rs @@ -0,0 +1,282 @@ +//! Exact generated control binders; the proof carries no runtime custody. +use super::*; +use crate::Value; +use jett_parser::ast::{ForStmt, Ident, MatchStmt, Pattern}; +use jett_typecheck::{CheckedBindingMode, CheckedViewSource}; + +enum Control<'source> { + For(&'source ForStmt), + Match(&'source MatchStmt, usize, String), +} + +pub(crate) struct PreparedAbsentBindings<'source> { + key: CheckedAttemptKey, + control: Control<'source>, + parent: TypeId, + payload_count: Option, + bindings: Vec<(&'source Ident, CheckedBindingFact)>, +} + +impl PreparedAbsentBindings<'_> { + pub(crate) fn parent_type(&self) -> TypeId { + self.parent + } + pub(crate) fn binding_count(&self) -> usize { + self.bindings.len() + } + + pub(crate) fn validate_binding( + &self, + original: &Ident, + ordinal: usize, + ) -> Result { + let (binder, fact) = self + .bindings + .get(ordinal) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if !std::ptr::eq(*binder, original) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(*fact) + } + + pub(crate) fn validate_parent(&self, value: &Value) -> Result<(), ResourceExecutionError> { + match (&self.control, value.payload()) { + (Control::For(source), Value::List(_)) if source.value_variable.is_none() => Ok(()), + (Control::For(_), Value::Map(_)) => Ok(()), + ( + Control::Match(source, arm, variant), + Value::Enum { + variant: actual, + fields, + .. + }, + ) if (variant.is_empty() || variant == actual) + && self.payload_count.is_none_or(|count| count == fields.len()) + && source + .arms + .iter() + .position(|candidate| match &candidate.pattern { + Pattern::Ident(name) | Pattern::Variant(name, _) => name.name == *actual, + Pattern::Other(_) => true, + }) + == Some(*arm) => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } + } +} + +impl CheckedExecution { + fn original_absent_control( + &self, + source: impl Fn(&Stmt) -> bool, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut count = 0; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |_| {}, + &mut |statement| { + if source(statement) { + count += 1; + } + }, + ); + if count == 1 { + Ok(()) + } else { + Err(ResourceExecutionError::MissingCheckedBody) + } + } + + fn absent_binder( + &self, + original: &Ident, + ty: TypeId, + mode: CheckedBindingMode, + ) -> Result { + let fact = self.binding(original.span)?; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(fact.definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if info.id != fact.definition + || info.span != original.span + || info.name != original.name + || info.kind != jett_resolve::DefKind::Variable + || fact.declaration_span != original.span + || fact.ty != ty + || fact.mode != mode + || fact.mutable + || self.expression_type(original.span)? != ty + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(fact) + } + + pub(crate) fn prepare_absent_for<'source>( + &self, + source: &'source ForStmt, + ) -> Result>, ResourceExecutionError> { + self.original_absent_control( + |statement| matches!(statement, Stmt::For(original) if std::ptr::eq(original, source)), + )?; + let parent = self.expression_type(source.iterable.span())?; + if !self.type_contains_resource(parent)? { + return Ok(None); + } + let types = &self.program.checked().interner; + let mode = |ty| { + if source.view && !jett_typecheck::ownership::is_implicitly_copyable(types, ty) { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned + } + }; + let mut bindings = Vec::new(); + match types.resolve(parent) { + Type::List(item) if source.value_variable.is_none() => { + bindings.push(( + &source.variable, + self.absent_binder(&source.variable, *item, mode(*item))?, + )); + } + Type::Map(key, item) => { + bindings.push(( + &source.variable, + self.absent_binder(&source.variable, *key, mode(*key))?, + )); + if let Some(value) = &source.value_variable { + bindings.push((value, self.absent_binder(value, *item, mode(*item))?)); + } + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + } + Ok(Some(PreparedAbsentBindings { + key: self.attempt_key(&self.body)?, + control: Control::For(source), + parent, + payload_count: None, + bindings, + })) + } + + pub(crate) fn prepare_absent_match<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + ) -> Result>, ResourceExecutionError> { + self.original_absent_control(|statement| matches!(statement, Stmt::Match(original) if std::ptr::eq(original, source)))?; + let parent = self.expression_type(source.expr.span())?; + if !self.type_contains_resource(parent)? { + return Ok(None); + } + let Some(selected) = source.arms.get(arm) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if let Some(selections) = self.facts(&self.body)?.selections { + if let Some(selection) = selections.get(&source.span) { + if *selection != jett_typecheck::CheckedStaticSelection::MatchArm(arm) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + } + let (variant, names) = match &selected.pattern { + Pattern::Variant(name, names) => (Some(name), names.as_slice()), + Pattern::Ident(name) => (Some(name), &[][..]), + Pattern::Other(_) => (None, &[][..]), + }; + let types = &self.program.checked().interner; + let Type::Enum(owner) = types.resolve(parent) else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + let schema = types.resolve_enum(*owner); + let (bindings, payload_count) = if let Some(variant) = variant { + let mut matching = schema + .variants + .iter() + .filter(|candidate| candidate.name == variant.name); + let selected = matching + .next() + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if matching.next().is_some() + || (matches!(source.arms[arm].pattern, Pattern::Variant(..)) + && names.len() != selected.fields.len()) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let bindings = names + .iter() + .zip(&selected.fields) + .map(|(name, (_, ty))| { + self.absent_binder(name, *ty, CheckedBindingMode::Owned) + .map(|fact| (name, fact)) + }) + .collect::, _>>()?; + (bindings, Some(selected.fields.len())) + } else { + (Vec::new(), None) + }; + Ok(Some(PreparedAbsentBindings { + key: self.attempt_key(&self.body)?, + control: Control::Match( + source, + arm, + variant.map(|name| name.name.clone()).unwrap_or_default(), + ), + parent, + payload_count, + bindings, + })) + } + + pub(crate) fn revalidate_absent_bindings( + &self, + proof: &PreparedAbsentBindings<'_>, + ) -> Result<(), ResourceExecutionError> { + if self.attempt_key(&self.body)? != proof.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let current = match &proof.control { + Control::For(source) => self.prepare_absent_for(source)?, + Control::Match(source, arm, _) => self.prepare_absent_match(source, *arm)?, + } + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let same_control = match (¤t.control, &proof.control) { + (Control::For(left), Control::For(right)) => std::ptr::eq(*left, *right), + ( + Control::Match(left, arm, variant), + Control::Match(right, expected_arm, expected_variant), + ) => std::ptr::eq(*left, *right) && arm == expected_arm && variant == expected_variant, + _ => false, + }; + if !same_control + || current.parent != proof.parent + || current.payload_count != proof.payload_count + || current.bindings.len() != proof.bindings.len() + || !current.bindings.iter().zip(&proof.bindings).all( + |((left, fact), (right, expected))| std::ptr::eq(*left, *right) && fact == expected, + ) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "absent_generated/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs new file mode 100644 index 00000000..49ad2941 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/absent_generated/tests.rs @@ -0,0 +1,315 @@ +//! Private rejection controls for exact generated binding proofs. +use super::*; +use jett_common::FileId; +use jett_parser::ast::{FunctionDef, Item}; +use jett_types::TypeInterner; + +#[path = "../../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn checked_program(index: usize, release: bool) -> Arc { + inputs::prepare(&inputs::INPUTS[index], release, &mut Vec::new()) + .unwrap() + .0 +} + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap() +} + +fn install(checked: &mut CheckedExecution, program: &CheckedResourceProgram, name: &str) { + let function = function(program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); +} + +fn for_statement(program: &CheckedResourceProgram) -> &ForStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn match_statement(program: &CheckedResourceProgram) -> &MatchStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Match(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn current_main(checked: &CheckedExecution, program: &CheckedResourceProgram) { + let current = CheckedBodyReference { + cursor: checked.cursor(), + }; + assert!(std::ptr::eq( + current.function().unwrap(), + function(program, "main") + )); + assert!(checked.body.scopes.is_empty()); +} + +#[test] +fn absent_generated_proofs_select_original_list_map_and_variant_binders() { + for release in [false, true] { + for (index, count, parent_name, names, types) in [ + ( + 1, + 1, + "list[optional[resource_probe.TestHandle]]", + vec!["candidate"], + vec!["optional[resource_probe.TestHandle]"], + ), + ( + 3, + 2, + "map[string, optional[resource_probe.TestHandle]]", + vec!["key", "candidate"], + vec!["string", "optional[resource_probe.TestHandle]"], + ), + ( + 9, + 2, + "app.TokenChoice", + vec!["marker", "tokens"], + vec!["int64", "list[resource_probe.TestHandle]"], + ), + ] { + let program = checked_program(index, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let proof = if index == 9 { + checked + .prepare_absent_match(match_statement(&program), 0) + .unwrap() + .unwrap() + } else { + checked + .prepare_absent_for(for_statement(&program)) + .unwrap() + .unwrap() + }; + assert_eq!(proof.binding_count(), count); + assert_eq!( + program.checked().interner.type_name(proof.parent_type()), + parent_name + ); + for (ordinal, (binder, fact)) in proof.bindings.iter().enumerate() { + assert_eq!(binder.name, names[ordinal]); + assert_eq!( + program.checked().interner.type_name(fact.ty), + types[ordinal] + ); + assert_eq!(fact.mode, CheckedBindingMode::Owned); + assert!(!fact.mutable); + assert_eq!(fact.declaration_span, binder.span); + assert_eq!(proof.validate_binding(binder, ordinal).unwrap(), *fact); + } + checked.revalidate_absent_bindings(&proof).unwrap(); + current_main(&checked, &program); + } + } +} + +#[test] +fn absent_generated_proofs_reject_copied_controls_binders_and_wrong_ordinals() { + for release in [false, true] { + for index in [1, 3] { + let program = checked_program(index, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = for_statement(&program); + let copied = statement.clone(); + assert!(checked.prepare_absent_for(&copied).is_err()); + let proof = checked.prepare_absent_for(statement).unwrap().unwrap(); + let binder = statement.variable.clone(); + assert!(proof.validate_binding(&binder, 0).is_err()); + assert!( + proof + .validate_binding(&statement.variable, proof.binding_count()) + .is_err() + ); + if let Some(value) = &statement.value_variable { + assert!(proof.validate_binding(value, 0).is_err()); + assert!(proof.validate_binding(&statement.variable, 1).is_err()); + } + current_main(&checked, &program); + } + let program = checked_program(9, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = match_statement(&program); + let copied = statement.clone(); + assert!(checked.prepare_absent_match(&copied, 0).is_err()); + assert!(checked.prepare_absent_match(statement, usize::MAX).is_err()); + let proof = checked.prepare_absent_match(statement, 0).unwrap().unwrap(); + let Pattern::Variant(_, binders) = &statement.arms[0].pattern else { + panic!("original vacant pattern") + }; + assert!(proof.validate_binding(&binders[0].clone(), 0).is_err()); + assert!(proof.validate_binding(&binders[1], 0).is_err()); + assert!(proof.validate_binding(&binders[0], 1).is_err()); + current_main(&checked, &program); + } +} + +#[test] +fn absent_generated_proofs_reject_foreign_program_changed_body_and_corrupt_facts() { + for release in [false, true] { + let program = checked_program(1, release); + let foreign_program = checked_program(1, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + install(&mut checked, &program, "main"); + install(&mut foreign, &foreign_program, "main"); + let statement = for_statement(&program); + let proof = checked.prepare_absent_for(statement).unwrap().unwrap(); + assert!(foreign.revalidate_absent_bindings(&proof).is_err()); + assert!(foreign.prepare_absent_for(statement).is_err()); + install(&mut checked, &program, "absent_tokens"); + assert!(checked.revalidate_absent_bindings(&proof).is_err()); + install(&mut checked, &program, "main"); + let copied = statement.clone(); + for corruption in 0..8 { + let mut bad = checked.prepare_absent_for(statement).unwrap().unwrap(); + match corruption { + 0 => bad.parent = TypeInterner::STRING, + 1 => bad.bindings[0].1.ty = TypeInterner::STRING, + 2 => bad.bindings[0].1.definition = DefId::new(u32::MAX), + 3 => bad.bindings[0].1.declaration_span = Span::new(FileId::new(0), 0, 0), + 4 => { + bad.bindings[0].1.mode = CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } + 5 => bad.bindings[0].1.mutable = true, + 6 => bad.control = Control::For(&copied), + 7 => bad.key = foreign.attempt_key(&foreign.body).unwrap(), + _ => unreachable!(), + } + assert!( + checked.revalidate_absent_bindings(&bad).is_err(), + "corruption {corruption}" + ); + current_main(&checked, &program); + } + checked.revalidate_absent_bindings(&proof).unwrap(); + } +} + +#[test] +fn absent_generated_match_proof_rejects_corrupt_arm_name_and_binding_order() { + for release in [false, true] { + let program = checked_program(9, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + install(&mut checked, &program, "main"); + let statement = match_statement(&program); + for corruption in 0..4 { + let mut bad = checked.prepare_absent_match(statement, 0).unwrap().unwrap(); + match corruption { + 0 => bad.control = Control::Match(statement, 0, "occupied".into()), + 1 => bad.control = Control::Match(statement, 1, "vacant".into()), + 2 => bad.bindings.swap(0, 1), + 3 => bad.payload_count = Some(usize::MAX), + _ => unreachable!(), + } + assert!( + checked.revalidate_absent_bindings(&bad).is_err(), + "corruption {corruption}" + ); + current_main(&checked, &program); + } + } +} + +#[test] +fn absent_generated_view_loops_keep_copyable_keys_owned_and_exact_occurrences_distinct() { + let input = inputs::Input { + name: "source17_view_continue_break_reuse", + source: include_str!("../../../runtime/absent_generated/view_control.jett"), + reference: inputs::ReferenceExpectation::Nothing, + }; + for release in [false, true] { + let program = inputs::prepare(&input, release, &mut Vec::new()).unwrap().0; + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + for name in ["source17_absent_view_list", "source17_absent_view_map"] { + install(&mut checked, &program, name); + let loops = function(&program, name) + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .collect::>(); + assert_eq!(loops.len(), 2); + assert!(loops.iter().all(|statement| statement.view)); + let proof = checked.prepare_absent_for(loops[0]).unwrap().unwrap(); + let second = checked.prepare_absent_for(loops[1]).unwrap().unwrap(); + assert!(proof.validate_binding(&loops[1].variable, 0).is_err()); + let first_mode = proof.validate_binding(&loops[0].variable, 0).unwrap().mode; + if name == "source17_absent_view_map" { + assert_eq!(first_mode, CheckedBindingMode::Owned); + let value = loops[0].value_variable.as_ref().unwrap(); + assert_eq!( + proof.validate_binding(value, 1).unwrap().mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ); + assert!( + proof + .validate_binding(loops[1].value_variable.as_ref().unwrap(), 1) + .is_err() + ); + } else { + assert_eq!( + first_mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ); + } + assert_eq!(proof.parent_type(), second.parent_type()); + let mut replaced = checked.prepare_absent_for(loops[0]).unwrap().unwrap(); + replaced.control = Control::For(loops[1]); + assert!(checked.revalidate_absent_bindings(&replaced).is_err()); + checked.revalidate_absent_bindings(&proof).unwrap(); + checked.revalidate_absent_bindings(&second).unwrap(); + assert!(checked.body.scopes.is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers.rs new file mode 100644 index 00000000..81f2109a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers.rs @@ -0,0 +1,565 @@ +//! Ordinary actor entry uses original actor/handler identity, never function or Resource authority. +use super::*; +use jett_parser::ast::{ActorDef, Ident, ReceiveHandler}; +use jett_typecheck::actor_messages::{ + CheckedActorBindingRole, CheckedActorHandler, CheckedActorHandlerTarget, + CheckedActorMessageCall, CheckedActorMessageKind, CheckedActorMessageRoot, +}; +use jett_types::ActorId; + +#[derive(Clone)] +pub(crate) struct CheckedActorInstanceOrigin { + program: Arc, + definition: DefId, + actor: ActorId, + item: usize, +} + +pub(crate) struct PreparedActorMessage<'source> { + source: &'source Expr, + key: CheckedAttemptKey, + packet: CheckedActorMessageCall, +} + +pub(crate) struct PreparedActorHandler { + reference: CheckedBodyReference, + origin: CheckedActorInstanceOrigin, +} + +pub(crate) struct PreparedActorSpawn { + origin: CheckedActorInstanceOrigin, + parameter_indices: Vec, + initializers: Vec, +} + +fn message_parts( + source: &Expr, +) -> Result<(CheckedActorMessageKind, &Expr, &[CallArg], &Ident), ResourceExecutionError> { + let (kind, inner) = match source { + Expr::Send(inner, _) => (CheckedActorMessageKind::Send, inner.as_ref()), + Expr::Ask(inner, _) => (CheckedActorMessageKind::Ask, inner.as_ref()), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + let (callee, args) = match inner { + Expr::Call(callee, args, _) => (callee.as_ref(), args.as_slice()), + Expr::FieldAccess(..) => (inner, &[][..]), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + let Expr::FieldAccess(receiver, member, _) = callee else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + Ok((kind, receiver, args, member)) +} + +impl CheckedActorInstanceOrigin { + /// Caller must already own the genuine checked graph import or Spawn occurrence. + /// This brand grants no provider/custody or generic function entry. + pub(crate) fn from_checked_actor( + program: Arc, + actor: ActorId, + ) -> Result { + let canonical = &program.checked().interner.resolve_actor(actor).name; + let matches = program.module().items.iter().enumerate().filter_map(|(item, node)| { + let Item::Actor(original) = node else { return None }; + let mut declarations = program.resolved().scope_table.definitions.iter().filter(|definition| { + definition.kind == jett_resolve::DefKind::Actor + && definition.span == original.name.span && definition.name == *canonical + && program.checked().definition_types.get(&definition.id) + .is_some_and(|ty| matches!(program.checked().interner.resolve(*ty), Type::Actor(actual) if *actual == actor)) + }); + let declaration = declarations.next()?; + if declarations.next().is_some() { return None; } + Some((item, declaration.id)) + }).collect::>(); + let [(item, definition)] = matches.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(Self { + program, + definition: *definition, + actor, + item: *item, + }) + } + + pub(crate) fn original(&self) -> Result<&ActorDef, ResourceExecutionError> { + match self.program.module().items.get(self.item) { + Some(Item::Actor(actor)) => Ok(actor), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } + + pub(crate) fn matches_registration( + &self, + registered: &ActorDef, + ) -> Result<(), ResourceExecutionError> { + let mut original = self.original()?.clone(); + let declaration = self + .program + .resolved() + .scope_table + .definitions + .get(self.definition.index() as usize) + .filter(|definition| { + definition.id == self.definition + && definition.kind == jett_resolve::DefKind::Actor + && definition.span == original.name.span + }) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + original.name.name = declaration.name.clone(); + if format!("{original:?}") != format!("{registered:?}") { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +impl PreparedActorMessage<'_> { + pub(crate) fn packet(&self) -> &CheckedActorMessageCall { + &self.packet + } + pub(crate) fn receiver(&self) -> Result<&Expr, ResourceExecutionError> { + Ok(message_parts(self.source)?.1) + } + pub(crate) fn arguments(&self) -> Result<&[CallArg], ResourceExecutionError> { + Ok(message_parts(self.source)?.2) + } +} + +impl PreparedActorSpawn { + pub(crate) fn original(&self) -> Result<&ActorDef, ResourceExecutionError> { + self.origin.original() + } + pub(crate) fn origin(&self) -> CheckedActorInstanceOrigin { + self.origin.clone() + } + pub(crate) fn program(&self) -> &Arc { + &self.origin.program + } + pub(crate) fn capability_type( + &self, + parameter: usize, + ) -> Result { + self.origin + .program + .checked() + .interner + .resolve_actor(self.origin.actor) + .capability_params + .get(parameter) + .map(|(_, ty)| *ty) + .ok_or(ResourceExecutionError::InvalidInvocation) + } + pub(crate) fn state_type(&self, field: usize) -> Result { + self.origin + .program + .checked() + .interner + .resolve_actor(self.origin.actor) + .state_fields + .get(field) + .map(|(_, ty)| *ty) + .ok_or(ResourceExecutionError::InvalidOwner) + } + pub(crate) fn parameter_indices(&self) -> &[usize] { + &self.parameter_indices + } + pub(crate) fn namespace(&self) -> Option<&str> { + self.origin.program.resolved().scope_table.definitions + [self.origin.definition.index() as usize] + .namespace + .as_deref() + } + pub(crate) fn initializers(&self) -> &[CheckedBodyReference] { + &self.initializers + } +} + +impl PreparedActorHandler { + pub(crate) fn target(&self) -> CheckedActorHandlerTarget { + let BodyRoot::ActorHandler(target) = self.reference.cursor.root else { + unreachable!("private actor handler cursor") + }; + target + } + pub(crate) fn reference(&self) -> &CheckedBodyReference { + &self.reference + } + pub(crate) fn original(&self) -> Result<(&ActorDef, &ReceiveHandler), ResourceExecutionError> { + let actor = self.origin.original()?; + let handler = actor + .handlers + .get(self.target().handler_index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + Ok((actor, handler)) + } + pub(crate) fn metadata(&self) -> Result<&CheckedActorHandler, ResourceExecutionError> { + self.origin + .program + .checked() + .actor_handler(self.target()) + .ok_or(ResourceExecutionError::MissingCheckedBody) + } + pub(crate) fn program(&self) -> &Arc { + &self.origin.program + } + pub(crate) fn namespace(&self) -> Option<&str> { + self.origin.program.resolved().scope_table.definitions + [self.origin.definition.index() as usize] + .namespace + .as_deref() + } + pub(crate) fn projection(&self) -> Result, ResourceExecutionError> { + let body = self.metadata()?.body(); + let types = &self.origin.program.checked().interner; + let (expressions, bindings) = crate::checked_types::original_body_types( + &body.type_map, + &body.comptime_type_bindings, + types, + ); + Ok(Arc::new(CheckedScopedTypes { + bound_type: types.resolve_actor(self.origin.actor).name.clone(), + reflection: None, + expressions, + bindings, + })) + } + pub(crate) fn matches_instance(&self, origin: &CheckedActorInstanceOrigin) -> bool { + Arc::ptr_eq(&self.origin.program, &origin.program) + && self.origin.definition == origin.definition + && self.origin.actor == origin.actor + && self.origin.item == origin.item + } + pub(crate) fn role_name( + &self, + role: CheckedActorBindingRole, + ) -> Result<&Ident, ResourceExecutionError> { + let (actor, handler) = self.original()?; + match role { + CheckedActorBindingRole::Capability { parameter } => actor + .capability_params + .get(parameter) + .map(|param| ¶m.name), + CheckedActorBindingRole::State { field } => { + actor.state_fields.get(field).map(|field| &field.name) + } + CheckedActorBindingRole::Message { parameter } => { + handler.params.get(parameter).map(|param| ¶m.name) + } + } + .ok_or(ResourceExecutionError::MissingCheckedBody) + } +} + +impl CheckedExecution { + fn original_actor_occurrence(&self, source: &Expr) -> Result<(), ResourceExecutionError> { + self.validate_field_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matches = 0; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |node| { + if std::ptr::eq(node, source) { + matches += 1; + } + }, + &mut |_| {}, + ); + if matches != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } + + pub(crate) fn prepare_actor_message<'source>( + &self, + source: &'source Expr, + ) -> Result, ResourceExecutionError> { + self.original_actor_occurrence(source)?; + let (kind, receiver, arguments, member) = message_parts(source)?; + let facts = self.facts(&self.body)?; + let packet = facts + .actor_messages + .get(&source.span()) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let root = match self.body.root { + BodyRoot::Ordinary => CheckedActorMessageRoot::Ordinary, + BodyRoot::Generic(index) => CheckedActorMessageRoot::Generic(index), + BodyRoot::ActorHandler(target) => CheckedActorMessageRoot::ActorHandler(target), + }; + let scopes = self + .body + .scopes + .iter() + .map(|scope| (scope.owner, scope.index)) + .collect::>(); + if packet.source_span != source.span() + || !self.program.checked().validates_actor_message_at( + packet, + root, + &scopes, + source.span(), + ) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let origin = CheckedActorInstanceOrigin::from_checked_actor( + self.program.clone(), + packet.target.actor, + )?; + let actor = origin.original()?; + let handler = actor + .handlers + .get(packet.target.handler_index) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let definition = self + .program + .checked() + .interner + .resolve_actor(packet.target.actor); + let message = definition + .messages + .get(packet.target.handler_index) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if packet.kind != kind + || origin.definition != packet.target.actor_definition + || handler.name.name != member.name + || message.name != member.name + || packet.receiver_span != receiver.span() + || facts.types.get(&receiver.span()) != Some(&packet.receiver_type) + || !matches!(self.program.checked().interner.resolve(packet.receiver_type), Type::Actor(actual) if *actual == packet.target.actor) + || packet.response_type != message.responds + || packet.arguments.len() != arguments.len() + || handler.params.len() != arguments.len() + || message.params.len() != arguments.len() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut used = vec![false; handler.params.len()]; + for (index, (argument, fact)) in arguments.iter().zip(&packet.arguments).enumerate() { + let Some(parameter) = handler.params.get(fact.parameter_index) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if fact.source_index != index + || fact.source_span != argument.value.span() + || facts.source_types.get(&fact.source_span) != Some(&fact.actual_type) + || fact.parameter_type != message.params[fact.parameter_index].1 + || (parameter.view || matches!(parameter.ty, jett_parser::ast::TypeExpr::View(..))) + != (fact.callee_access == jett_typecheck::CheckedCalleeAccess::View) + || std::mem::replace(&mut used[fact.parameter_index], true) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + Ok(PreparedActorMessage { + source, + key: self.field_attempt_key(&self.body)?, + packet: packet.clone(), + }) + } + + pub(crate) fn revalidate_actor_message( + &self, + prepared: &PreparedActorMessage<'_>, + ) -> Result<(), ResourceExecutionError> { + let current = self.prepare_actor_message(prepared.source)?; + if current.key != prepared.key || current.packet != prepared.packet { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } + + pub(crate) fn prepare_actor_handler( + &self, + message: &PreparedActorMessage<'_>, + registered: &ActorDef, + ) -> Result { + self.revalidate_actor_message(message)?; + let target = message.packet.target; + let origin = + CheckedActorInstanceOrigin::from_checked_actor(self.program.clone(), target.actor)?; + origin.matches_registration(registered)?; + self.validate_actor_handler_identity(target, origin.item)?; + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::ActorHandler(target), + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::ActorHandler { + definition: target.actor_definition, + item: origin.item, + handler: target.handler_index, + }, + path: Vec::new(), + }), + }, + }; + self.validate_field_executable_cursor(&reference.cursor)?; + Ok(PreparedActorHandler { reference, origin }) + } + + pub(super) fn validate_actor_handler_identity( + &self, + target: CheckedActorHandlerTarget, + item: usize, + ) -> Result<(), ResourceExecutionError> { + let origin = + CheckedActorInstanceOrigin::from_checked_actor(self.program.clone(), target.actor)?; + if origin.definition != target.actor_definition || origin.item != item { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let actor = origin.original()?; + let handler = actor + .handlers + .get(target.handler_index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let metadata = self + .program + .checked() + .actor_handler(target) + .filter(|checked| { + checked.body_span() == handler.body.span && checked.matches_source_actor(actor) + }) + .ok_or(ResourceExecutionError::InvalidOwner)?; + let definition = self.program.checked().interner.resolve_actor(target.actor); + let message = definition + .messages + .get(target.handler_index) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if definition.capability_params.len() != actor.capability_params.len() + || definition.state_fields.len() != actor.state_fields.len() + || message.params.len() != handler.params.len() + || metadata.response_type() != message.responds + || metadata.bindings().len() + != actor.capability_params.len() + actor.state_fields.len() + handler.params.len() + { + return Err(ResourceExecutionError::InvalidOwner); + } + let mut earlier = Vec::new(); + for (index, binding) in metadata.bindings().iter().enumerate() { + let (name, ty, expected) = if index < actor.capability_params.len() { + ( + &actor.capability_params[index].name, + definition.capability_params[index].1, + CheckedActorBindingRole::Capability { parameter: index }, + ) + } else if index < actor.capability_params.len() + actor.state_fields.len() { + let field = index - actor.capability_params.len(); + ( + &actor.state_fields[field].name, + definition.state_fields[field].1, + CheckedActorBindingRole::State { field }, + ) + } else { + let parameter = index - actor.capability_params.len() - actor.state_fields.len(); + ( + &handler.params[parameter].name, + message.params[parameter].1, + CheckedActorBindingRole::Message { parameter }, + ) + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(binding.fact.definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if binding.role != expected + || binding.fact.declaration_span != name.span + || binding.fact.ty != ty + || info.id != binding.fact.definition + || info.span != name.span + || !matches!( + info.kind, + jett_resolve::DefKind::Variable | jett_resolve::DefKind::Param + ) + { + return Err(ResourceExecutionError::InvalidOwner); + } + if matches!(binding.role, CheckedActorBindingRole::State { .. }) { + if let jett_typecheck::CheckedBindingMode::View { source } = binding.fact.mode { + let jett_typecheck::CheckedViewSource::Binding(owner) = source else { + return Err(ResourceExecutionError::InvalidOwner); + }; + if !earlier.contains(&owner) { + return Err(ResourceExecutionError::InvalidOwner); + } + } + } + earlier.push(binding.fact.definition); + } + Ok(()) + } + + pub(crate) fn prepare_actor_spawn( + &self, + source: &Expr, + registered: &ActorDef, + ) -> Result { + self.original_actor_occurrence(source)?; + let Expr::Spawn(inner, _) = source else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let Expr::Call(_, arguments, _) = inner.as_ref() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let ty = self.expression_type(source.span())?; + let Type::Actor(actor) = self.program.checked().interner.resolve(ty) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let origin = CheckedActorInstanceOrigin::from_checked_actor(self.program.clone(), *actor)?; + origin.matches_registration(registered)?; + let original = origin.original()?; + let order = self + .facts(&self.body)? + .argument_orders + .get(&inner.span()) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + if order.source_indices.len() != arguments.len() + || arguments.len() != original.capability_params.len() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut indices = vec![usize::MAX; arguments.len()]; + for (parameter, &index) in order.source_indices.iter().enumerate() { + let slot = indices + .get_mut(index) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if *slot != usize::MAX { + return Err(ResourceExecutionError::InvalidInvocation); + } + *slot = parameter; + } + let mut initializers = Vec::new(); + for field in 0..original.state_fields.len() { + initializers.push(CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::ActorInitializer { + item: origin.item, + field, + }, + path: Vec::new(), + }), + }, + }); + } + Ok(PreparedActorSpawn { + origin, + parameter_indices: indices, + initializers, + }) + } +} + +#[cfg(test)] +#[path = "actor_handlers/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers/tests.rs new file mode 100644 index 00000000..03216012 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/actor_handlers/tests.rs @@ -0,0 +1,143 @@ +use super::*; + +const SOURCE: &str = r#"namespace app +struct Record: + value: int64 +function change(mutable item: Record, amount: int64) returns Record: + item.value = amount + return item +actor Store: + mutable Record record = Record(value: 1) + receive set(amount: int64): + record = change(record, amount) + receive read responds int64: + respond record.value +function exercise() returns int64: + Store first = spawn Store() + Store second = spawn Store() + send first.set(7) + send first.set(9) + int64 unchanged = ask second.read + int64 changed = ask first.read + return changed + unchanged +"#; + +fn entry(program: &Arc) -> (CheckedExecution, CheckedBodyReference) { + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "exercise" => Some(function), + _ => None, + }) + .unwrap(); + let info = program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| { + info.kind == jett_resolve::DefKind::Function && info.span == function.name.span + }) + .unwrap(); + let reference = execution + .prepare_field_function_body(&info.name, &info.namespace, function, None, &[]) + .unwrap(); + (execution, reference) +} + +fn first_message(reference: &CheckedBodyReference) -> &Expr { + let mut messages = Vec::new(); + walk_region( + reference.region().unwrap(), + &mut |_| {}, + &mut |expression| { + if matches!(expression, Expr::Send(..)) { + messages.push(expression); + } + }, + &mut |_| {}, + ); + messages[0] +} + +#[test] +fn original_actor_message_roles_and_program_origin_cannot_be_substituted() { + let program = crate::resource_execution::tests::program(SOURCE, false); + let (mut execution, reference) = entry(&program); + execution.install_field_function_body(&reference).unwrap(); + let source = first_message(&reference); + assert!(execution.prepare_actor_message(&source.clone()).is_err()); + let message = execution.prepare_actor_message(source).unwrap(); + let origin = CheckedActorInstanceOrigin::from_checked_actor( + program.clone(), + message.packet().target.actor, + ) + .unwrap(); + let mut registered = origin.original().unwrap().clone(); + registered.name.name = program + .checked() + .interner + .resolve_actor(message.packet().target.actor) + .name + .clone(); + let handler = execution + .prepare_actor_handler(&message, ®istered) + .unwrap(); + assert!(handler.matches_instance(&origin)); + let foreign = crate::resource_execution::tests::program(SOURCE, false); + let foreign_origin = + CheckedActorInstanceOrigin::from_checked_actor(foreign, message.packet().target.actor) + .unwrap(); + assert!(!handler.matches_instance(&foreign_origin)); + registered.handlers[0].params[0].mutable = !registered.handlers[0].params[0].mutable; + assert!( + execution + .prepare_actor_handler(&message, ®istered) + .is_err() + ); + execution + .install_field_function_body(handler.reference()) + .unwrap(); + let state = execution + .prepare_actor_binding(&handler, CheckedActorBindingRole::State { field: 0 }) + .unwrap(); + assert_eq!( + state.fact().declaration_span, + handler.original().unwrap().0.state_fields[0].name.span + ); + assert!( + execution + .prepare_actor_binding(&handler, CheckedActorBindingRole::State { field: 1 }) + .is_err() + ); + assert!(matches!( + execution.install_function_body(handler.reference()), + Err(ResourceExecutionError::WrongPurpose) + )); + let mut wrong = handler.reference().clone(); + wrong.cursor.root = BodyRoot::Ordinary; + assert!(execution.install_field_function_body(&wrong).is_err()); +} + +#[test] +fn checked_actor_runtime_preserves_distinct_instances_and_calls_original_helper() { + let program = crate::resource_execution::tests::program(SOURCE, false); + let mut interpreter = crate::Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_checked_expression_types(Arc::new( + crate::checked_types::CheckedExpressionTypes::from_checked_program(&program), + )); + assert_eq!( + interpreter + .call_checked_function_in_namespace(Some("app"), "exercise", vec![]) + .unwrap(), + crate::Value::Int64(10) + ); +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/field_inline.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_inline.rs new file mode 100644 index 00000000..0efe7a11 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_inline.rs @@ -0,0 +1,322 @@ +//! Unique ordinary inline-body selection from the existing capture snapshot. +//! Resource entry validation is deliberately unchanged and still rejects a +//! runtime InlineBody cursor. This module installs no provider or custody. +use super::*; +use crate::value::{ClosureTypeContext, closure_capture::CheckedClosureCapture}; + +fn inline_paths( + region: OriginalRegion<'_>, + target: &Expr, +) -> Result>, ResourceExecutionError> { + let mut scopes = Vec::new(); + let mut inline = Vec::new(); + walk_region( + region, + &mut |scope| scopes.push(scope), + &mut |node| { + if matches!(node, Expr::InlineFn(..)) { + inline.push(node); + } + }, + &mut |_| {}, + ); + let mut paths = Vec::new(); + for scope in scopes { + for mut path in inline_paths(OriginalRegion::Block(&scope.body), target)? { + path.insert(0, RegionStep::Scope(scope.span)); + paths.push(path); + } + } + for node in inline { + let index = expression_index(region, node)?; + if std::ptr::eq(node, target) { + paths.push(vec![RegionStep::InlineBody(index)]); + } else { + for mut path in inline_paths( + OriginalRegion::Block(region_inline_body(region, index)?), + target, + )? { + path.insert(0, RegionStep::InlineBody(index)); + paths.push(path); + } + } + } + Ok(paths) +} + +impl CheckedBodyReference { + pub(crate) fn field_inline_parts( + &self, + ) -> Result<(&[jett_parser::ast::Param], &Block), ResourceExecutionError> { + let identity = self + .cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Some((RegionStep::InlineBody(index), prefix)) = identity.path.split_last() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + match region_expression(self.region_at_path_len(prefix.len())?, *index)? { + Expr::InlineFn(parameters, _, body, _) => Ok((parameters, body)), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } +} + +impl CheckedExecution { + pub(crate) fn prepare_field_inline_body( + &self, + proof: &CheckedClosureCapture, + context: &ClosureTypeContext, + ) -> Result { + // The caller obtained this proof only through validate_source against + // this exact program and current full closure Value snapshot. + let target = proof + .field_original_node() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let mut selected = Vec::new(); + let mut owners = Vec::new(); + for (item, node) in self.program.module().items.iter().enumerate() { + match node { + Item::Function(function) => { + owners.push((item, node, OriginalRegion::Block(&function.body), None)) + } + Item::Verify(block) => { + owners.push((item, node, OriginalRegion::Block(&block.body), None)) + } + Item::Property(block) => { + owners.push((item, node, OriginalRegion::Block(&block.body), None)) + } + Item::Actor(actor) => { + for (handler_index, handler) in actor.handlers.iter().enumerate() { + if inline_paths(OriginalRegion::Block(&handler.body), target)?.is_empty() { + continue; + } + for actor_target in self.program.checked().actor_handlers.keys() { + if actor_target.handler_index != handler_index { + continue; + } + let Some(info) = self + .program + .resolved() + .scope_table + .definitions + .get(actor_target.actor_definition.index() as usize) + else { + continue; + }; + if info.span == actor.name.span { + self.validate_actor_handler_identity(*actor_target, item)?; + owners.push(( + item, + node, + OriginalRegion::Block(&handler.body), + Some(*actor_target), + )); + } + } + } + } + _ => {} + } + } + for (item, node, original_region, actor_target) in owners { + // Identify the retained physical owner before asking it for executable + // authority. Unrelated Resource hook declarations deliberately reject + // ordinary entry and must not block a different function's closure. + let paths = inline_paths(original_region, target)?; + if paths.is_empty() { + continue; + } + let [path] = paths.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let (origin, roots) = match node { + Item::Actor(_) + if context.checked_function.is_none() && context.arguments.is_empty() => + { + let target = actor_target.ok_or(ResourceExecutionError::MissingCheckedBody)?; + ( + ExecutableOrigin::ActorHandler { + definition: target.actor_definition, + item, + handler: target.handler_index, + }, + vec![BodyRoot::ActorHandler(target)], + ) + } + Item::Function(function) => { + let definition = self.declaration_definition(function.name.span)?; + let roots = if function.type_params.is_empty() { + if context.checked_function.is_some() || !context.arguments.is_empty() { + continue; + } + vec![BodyRoot::Ordinary] + } else { + let Some(current) = &context.checked_function else { + continue; + }; + self.program.checked().generic_function_instantiations.iter().enumerate() + .filter(|(_, instance)| instance.definition == definition + && crate::value::closure_capture::matches_field_function_projection( + current, &context.arguments, instance, &self.program.checked().interner, + )) + .map(|(index, _)| BodyRoot::Generic(index)).collect() + }; + (ExecutableOrigin::Function { definition, item }, roots) + } + Item::Verify(_) + if context.checked_function.is_none() && context.arguments.is_empty() => + { + (ExecutableOrigin::Verify { item }, vec![BodyRoot::Ordinary]) + } + Item::Property(_) + if context.checked_function.is_none() && context.arguments.is_empty() => + { + ( + ExecutableOrigin::Property { item }, + vec![BodyRoot::Ordinary], + ) + } + _ => continue, + }; + let structural = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin, + path: Vec::new(), + }), + }, + }; + for root in roots { + let mut base = structural.cursor.clone(); + base.root = root; + let mut candidates = vec![base]; + let mut scope_depth = 0usize; + for step in path { + let mut expanded = Vec::new(); + for cursor in candidates { + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + match step { + RegionStep::InlineBody(index) => { + region_inline_body(reference.region()?, *index)?; + let mut next = cursor; + next.executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(step.clone()); + self.validate_field_executable_cursor(&next)?; + expanded.push(next); + } + RegionStep::Scope(owner) => { + let binding = region_binding(reference.region()?, *owner)?; + let Some(actual) = context.scoped_type_bindings.get(scope_depth) + else { + continue; + }; + let facts = self.facts(&cursor)?; + let Some(choices) = facts.scopes.get(owner) else { + continue; + }; + for (index, choice) in choices.iter().enumerate() { + if actual.name != binding.name.name + || actual.canonical_name + != self + .program + .checked() + .interner + .type_name(choice.bound_type) + || actual.reflection.as_ref() != Some(&choice.reflection) + { + continue; + } + let mut next = cursor.clone(); + next.scopes.push(ScopedSelection { + owner: *owner, + index, + bound_type: choice.bound_type, + selection: choice.selection, + reflection: choice.reflection.clone(), + }); + next.executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(step.clone()); + self.validate_field_executable_cursor(&next)?; + expanded.push(next); + } + } + RegionStep::Expression(_) => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + } + if matches!(step, RegionStep::Scope(_)) { + scope_depth += 1; + } + candidates = expanded; + } + if scope_depth != context.scoped_type_bindings.len() { + continue; + } + for cursor in candidates { + let checked_scope = if let Some(selection) = cursor.scopes.last() { + let mut parent = cursor.clone(); + parent.scopes.pop(); + let facts = self.facts(&parent)?; + let choice = facts + .scopes + .get(&selection.owner) + .and_then(|choices| choices.get(selection.index)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + Some(scoped_projection(choice, &self.program.checked().interner)) + } else if let BodyRoot::ActorHandler(target) = cursor.root { + let handler = self + .program + .checked() + .actor_handler(target) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let (expressions, bindings) = crate::checked_types::original_body_types( + &handler.body().type_map, + &handler.body().comptime_type_bindings, + &self.program.checked().interner, + ); + Some(Arc::new(CheckedScopedTypes { + bound_type: self + .program + .checked() + .interner + .resolve_actor(target.actor) + .name + .clone(), + reflection: None, + expressions, + bindings, + })) + } else { + None + }; + if checked_scope != context.checked_scope { + continue; + } + let reference = CheckedBodyReference { cursor }; + reference.field_inline_parts()?; + selected.push(reference); + } + } + } + let [reference] = selected.as_slice() else { + // Equal callable TypeIds do not collapse distinct generic/scoped + // ownership contexts. A missing unique producer remains unsupported. + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(reference.clone()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates.rs new file mode 100644 index 00000000..485fe3a1 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates.rs @@ -0,0 +1,417 @@ +//! Ordinary field writes reuse original executable identity, never custody. +use super::*; +#[cfg(test)] +#[path = "field_updates/tests.rs"] +mod tests; +use crate::checked_types::CheckedFunctionTypes; +use crate::value::ClosureTypeArgument; +use jett_parser::ast::Ident; +use jett_typecheck::assignment_places::{ + CheckedAssignmentPlace, CheckedFieldUpdateResourceRequirement, checked_field_owner, + validate_assignment_place_shape, +}; + +fn source_field_path<'source>( + expression: &'source Expr, + path: &mut Vec<(&'source Ident, Span)>, +) -> Option<&'source Ident> { + match expression { + Expr::Ident(root) => Some(root), + Expr::Paren(inner, _) => source_field_path(inner, path), + Expr::FieldAccess(base, field, span) => { + let root = source_field_path(base, path)?; + path.push((field, *span)); + Some(root) + } + _ => None, + } +} + +fn nested_binding_needed( + scopes: &HashMap>, + declaration: Span, +) -> bool { + scopes.values().flatten().any(|scope| { + scope + .body + .assignment_places + .values() + .any(|place| place.root.declaration_span == declaration) + || nested_binding_needed(&scope.body.comptime_type_bindings, declaration) + }) +} + +pub(crate) struct PreparedFieldAssignment<'source> { + source: &'source AssignStmt, + place: CheckedAssignmentPlace, + key: CheckedAttemptKey, +} + +impl PreparedFieldAssignment<'_> { + pub(crate) fn place(&self) -> &CheckedAssignmentPlace { + &self.place + } + + pub(crate) fn accepts_binding(&self, binding: &PreparedFieldBinding) -> bool { + self.place.root == binding.fact && self.key.is_lexical_descendant_of(&binding.key) + } +} + +pub(crate) struct PreparedFieldBinding { + fact: CheckedBindingFact, + key: CheckedAttemptKey, +} + +impl PreparedFieldBinding { + pub(crate) fn fact(&self) -> CheckedBindingFact { + self.fact + } +} + +impl CheckedExecution { + pub(crate) fn install_field_function_body( + &mut self, + reference: &CheckedBodyReference, + ) -> Result<(), ResourceExecutionError> { + self.validate_field_executable_cursor(&reference.cursor)?; + self.body = reference.cursor.clone(); + Ok(()) + } + + pub(crate) fn restore_field_cursor( + &mut self, + cursor: CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + self.facts(&cursor)?; + if cursor.executable.is_some() { + self.validate_field_executable_cursor(&cursor)?; + } + self.body = cursor; + Ok(()) + } + + fn original_field_assignment_key( + &self, + assignment: &AssignStmt, + ) -> Result { + self.validate_field_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut found = 0usize; + walk_region(reference.region()?, &mut |_| {}, &mut |_| {}, &mut |node| { + if let Stmt::Assign(original) = node + && std::ptr::eq(original, assignment) + { + found += 1; + } + }); + if found != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + self.field_attempt_key(&self.body) + } + pub(crate) fn field_scope_has_updates( + &self, + span: Span, + ) -> Result { + fn contains(scopes: &HashMap>) -> bool { + scopes.values().flatten().any(|scope| { + !scope.body.assignment_places.is_empty() + || contains(&scope.body.comptime_type_bindings) + }) + } + let facts = self.facts(&self.body)?; + Ok(facts.scopes.get(&span).is_some_and(|candidates| { + candidates.iter().any(|scope| { + !scope.body.assignment_places.is_empty() + || contains(&scope.body.comptime_type_bindings) + }) + })) + } + pub(crate) fn prepare_field_test_body( + &self, + block: &Block, + purpose: ExecutionPurpose, + ) -> Result { + for item in &self.program.module().items { + match (item, purpose) { + (Item::Verify(original), ExecutionPurpose::Verify) + if std::ptr::eq(&original.body, block) => + { + return self.prepare_verify_body(original); + } + (Item::Property(original), ExecutionPurpose::Property) + if std::ptr::eq(&original.body, block) => + { + return self.prepare_property_body(original); + } + _ => {} + } + } + Err(ResourceExecutionError::MissingCheckedBody) + } + pub(crate) fn field_binding_needed( + &self, + declaration: Span, + ) -> Result { + let facts = self.facts(&self.body)?; + Ok(facts + .assignment_places + .values() + .any(|place| place.root.declaration_span == declaration) + || nested_binding_needed(facts.scopes, declaration)) + } + /// A registered clone may identify a retained declaration only after the + /// existing full original/current AST comparison. Execution uses the retained + /// function below; the clone does not become an original-node authority. + pub(crate) fn prepare_field_function_body( + &self, + name: &str, + namespace: &Option, + current: &FunctionDef, + projection: Option<&CheckedFunctionTypes>, + arguments: &[ClosureTypeArgument], + ) -> Result { + crate::value::closure_capture::OriginalClosureOwner::registered( + &self.program, + name, + current, + namespace, + ) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let definition = self.declaration_definition(current.name.span)?; + let original = self.retained_function(definition)?; + let root = if original.type_params.is_empty() { + if projection.is_some() || !arguments.is_empty() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + BodyRoot::Ordinary + } else { + let projection = projection.ok_or(ResourceExecutionError::MissingCheckedBody)?; + let matching = self + .program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, instance)| { + instance.definition == definition + && crate::value::closure_capture::matches_field_function_projection( + projection, + arguments, + instance, + &self.program.checked().interner, + ) + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = matching.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + BodyRoot::Generic(*index) + }; + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(item, node)| { + matches!(node, Item::Function(function) if std::ptr::eq(function, original)) + .then_some(item) + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::Function { + definition, + item: *item, + }, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + Ok(reference) + } + + /// Only an original declaration/parameter under the selected complete body + /// can install the physical lexical slot used by a later field assignment. + pub(crate) fn prepare_field_binding( + &self, + name: &Ident, + ) -> Result { + self.validate_field_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matches = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |_| {}, + &mut |statement| { + if let Stmt::VarDecl(declaration) = statement + && std::ptr::eq(&declaration.name, name) + { + matches += 1; + } + }, + ); + if let Ok((parameters, _)) = reference.field_inline_parts() { + matches += parameters + .iter() + .filter(|parameter| std::ptr::eq(¶meter.name, name)) + .count(); + } else if self.body.scopes.is_empty() + && let Ok(function) = reference.function() + { + matches += function + .params + .iter() + .filter(|parameter| std::ptr::eq(¶meter.name, name)) + .count(); + } + let fact = self.binding(name.span)?; + let info = self + .program + .resolved() + .scope_table + .definitions + .get( + usize::try_from(fact.definition.index()) + .map_err(|_| ResourceExecutionError::InvalidOwner)?, + ) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if matches != 1 + || info.span != name.span + || info.id != fact.definition + || fact.declaration_span != name.span + || !matches!( + info.kind, + jett_resolve::DefKind::Variable | jett_resolve::DefKind::Param + ) + { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(PreparedFieldBinding { + fact, + key: self.field_body_key()?, + }) + } + + pub(crate) fn prepare_field_assignment<'source>( + &self, + source: &'source AssignStmt, + ) -> Result, ResourceExecutionError> { + let key = self.original_field_assignment_key(source)?; + let place = self + .facts(&self.body)? + .assignment_places + .get(&source.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + // This private stamp belongs to this exact selected body/generic/scoped + // record. One same-span specialization cannot bless another one. + if !place.ownership_validated() { + return Err(ResourceExecutionError::InvalidOwner); + } + validate_assignment_place_shape(place, &self.program.checked().interner) + .map_err(|_| ResourceExecutionError::InvalidOwner)?; + let mut source_path = Vec::new(); + let root = source_field_path(&source.target, &mut source_path) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if root.span != place.root_use_span || source_path.len() != place.projections.len() { + return Err(ResourceExecutionError::InvalidOwner); + } + for ((field, span), projection) in source_path.iter().zip(&place.projections) { + let (_, fields) = + checked_field_owner(&self.program.checked().interner, projection.owner_type) + .map_err(|_| ResourceExecutionError::InvalidOwner)?; + if *span != projection.source_span + || fields + .get(projection.field_index) + .map(|(name, _)| name.as_str()) + != Some(field.name.as_str()) + { + return Err(ResourceExecutionError::InvalidOwner); + } + } + if place.assignment_span != source.span + || self.resolved_definition(place.root_use_span)? != place.root.definition + || self.expression_type(place.root_use_span)? != place.root_type + || self.expression_type(source.target.span())? + != place + .projections + .last() + .ok_or(ResourceExecutionError::InvalidOwner)? + .field_type + { + return Err(ResourceExecutionError::InvalidOwner); + } + let contains_resource = self.type_contains_resource(place.root.ty)?; + if contains_resource + != (place.resource == CheckedFieldUpdateResourceRequirement::RequiresCustody) + { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(PreparedFieldAssignment { + source, + place: place.clone(), + key, + }) + } + + pub(crate) fn revalidate_field_assignment( + &self, + prepared: &PreparedFieldAssignment<'_>, + ) -> Result<(), ResourceExecutionError> { + let current = self.prepare_field_assignment(prepared.source)?; + if current.key != prepared.key || current.place != prepared.place { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +impl CheckedExecution { + pub(crate) fn prepare_actor_binding( + &self, + handler: &super::actor_handlers::PreparedActorHandler, + role: jett_typecheck::CheckedActorBindingRole, + ) -> Result { + self.validate_field_executable_cursor(&self.body)?; + let BodyRoot::ActorHandler(target) = self.body.root else { + return Err(ResourceExecutionError::InvalidOwner); + }; + if target != handler.target() + || self.field_body_key()? != self.field_attempt_key(&handler.reference().cursor)? + { + return Err(ResourceExecutionError::InvalidOwner); + } + let mut matching = handler + .metadata()? + .bindings() + .iter() + .filter(|binding| binding.role == role); + let binding = matching + .next() + .ok_or(ResourceExecutionError::InvalidOwner)?; + if matching.next().is_some() + || handler.role_name(role)?.span != binding.fact.declaration_span + { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(PreparedFieldBinding { + fact: binding.fact, + key: self.field_body_key()?, + }) + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates/tests.rs new file mode 100644 index 00000000..3ec81fbf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/field_updates/tests.rs @@ -0,0 +1,174 @@ +//! Checked Source identity and physical cursor controls. +//! These controls do not establish runtime policy or execution. +use super::*; + +const SOURCE: &str = r#"namespace app +struct Record: + value: int64 + label: string +function change(mutable owned: Record) returns Record: + owned.value = 7 + return owned +function factory() returns function(Record) returns Record: + return function(mutable owned: Record) returns Record: + owned.value = 8 + return owned +"#; + +fn source_function<'program>( + program: &'program CheckedResourceProgram, + name: &str, +) -> &'program FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .expect("genuine project function") +} + +fn named_reference(execution: &CheckedExecution, name: &str) -> CheckedBodyReference { + let original = source_function(execution.program(), name); + let definition = execution + .program() + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == original.name.span + }) + .expect("resolved original declaration"); + execution + .prepare_field_function_body(&definition.name, &definition.namespace, original, None, &[]) + .expect("complete ordinary Source body") +} + +#[test] +fn original_parameter_and_assignment_share_one_body_identity() { + let program = crate::resource_execution::tests::program(SOURCE, false); + let mut execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let reference = named_reference(&execution, "change"); + execution.install_field_function_body(&reference).unwrap(); + let function = source_function(&program, "change"); + let binding = execution + .prepare_field_binding(&function.params[0].name) + .unwrap(); + let Stmt::Assign(assignment) = &function.body.stmts[0] else { + panic!("assignment") + }; + let prepared = execution.prepare_field_assignment(assignment).unwrap(); + assert!(prepared.accepts_binding(&binding)); + execution.revalidate_field_assignment(&prepared).unwrap(); + assert!( + execution + .prepare_field_assignment(&assignment.clone()) + .is_err() + ); + assert!( + execution + .prepare_field_binding(&function.params[0].name.clone()) + .is_err() + ); +} + +#[test] +fn retained_body_reference_cannot_cross_programs() { + let first = crate::resource_execution::tests::program(SOURCE, false); + let second = crate::resource_execution::tests::program(SOURCE, false); + let first = CheckedExecution::new(first, ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut second = CheckedExecution::new(second, ExecutionPurpose::ReferenceRuntime).unwrap(); + assert!(matches!( + second.install_field_function_body(&named_reference(&first, "change")), + Err(ResourceExecutionError::ForeignProgram) + )); +} + +#[test] +fn field_only_inline_identity_cannot_authorize_resource_entry() { + let program = crate::resource_execution::tests::program(SOURCE, false); + let mut execution = CheckedExecution::new(program, ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut reference = named_reference(&execution, "factory"); + let mut inline = None; + walk_region( + reference.region().unwrap(), + &mut |_| {}, + &mut |node| { + if matches!(node, Expr::InlineFn(..)) { + inline = Some(node); + } + }, + &mut |_| {}, + ); + let index = expression_index( + reference.region().unwrap(), + inline.expect("original inline node"), + ) + .unwrap(); + reference + .cursor + .executable + .as_mut() + .unwrap() + .path + .push(RegionStep::InlineBody(index)); + execution.install_field_function_body(&reference).unwrap(); + let (parameters, _) = reference.field_inline_parts().unwrap(); + execution + .prepare_field_binding(¶meters[0].name) + .unwrap(); + assert!(matches!( + execution.install_function_body(&reference), + Err(ResourceExecutionError::WrongPurpose) + )); +} + +#[test] +fn inline_selector_ignores_unrelated_resource_hook_declarations() { + let program = crate::resource_execution::tests::program(SOURCE, false); + assert!(!program.checked().resource_hooks.is_empty()); + let mut interpreter = crate::Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_checked_expression_types(Arc::new( + crate::checked_types::CheckedExpressionTypes::from_checked_program(&program), + )); + let closure = interpreter + .call_checked_function_in_namespace(Some("app"), "factory", vec![]) + .expect("genuine checked factory capture"); + let crate::Value::Function { type_context, .. } = &closure else { + panic!("factory must return its inline function"); + }; + let proof = crate::value::closure_capture::field_invocation_proof(&closure, &program) + .expect("runtime capture retains its original Source witness"); + let mut execution = CheckedExecution::new(program, ExecutionPurpose::ReferenceRuntime).unwrap(); + let reference = execution + .prepare_field_inline_body(&proof, type_context) + .expect("unrelated hooks must not affect this original inline body"); + execution.install_field_function_body(&reference).unwrap(); + let (parameters, body) = reference.field_inline_parts().unwrap(); + let binding = execution + .prepare_field_binding(¶meters[0].name) + .unwrap(); + let Stmt::Assign(assignment) = &body.stmts[0] else { + panic!("original inline field assignment"); + }; + let prepared = execution.prepare_field_assignment(assignment).unwrap(); + assert!(prepared.accepts_binding(&binding)); + assert!(matches!( + execution.install_function_body(&reference), + Err(ResourceExecutionError::WrongPurpose) + )); +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs new file mode 100644 index 00000000..512422a7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic.rs @@ -0,0 +1,280 @@ +//! Concrete intrinsic operands from one original call and accepted body frame. +use super::*; +use jett_parser::ast::TypeExpr; + +pub(crate) struct PreparedIntrinsicArguments { + program: Arc, + key: CheckedAttemptKey, + packet: jett_typecheck::CheckedCallOwnership, + span: Span, + intrinsic: IntrinsicId, + types: Vec, + reflections: Vec, +} + +impl PreparedIntrinsicArguments { + pub(crate) fn intrinsic(&self) -> IntrinsicId { + self.intrinsic + } + pub(crate) fn types(&self) -> &[TypeId] { + &self.types + } + pub(crate) fn reflections(&self) -> &[ReflectionTypeInfo] { + &self.reflections + } + pub(crate) fn validate( + &self, + checked: &CheckedExecution, + invocation: &CheckedInvocation, + ) -> Result<(), ResourceExecutionError> { + if !Arc::ptr_eq(&self.program, checked.program()) { + return Err(ResourceExecutionError::ForeignProgram); + } + if self.key != checked.attempt_key(&invocation.body)? + || self.packet != invocation.packet + || self.span != invocation.span + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let facts = checked.facts(&invocation.body)?; + let (intrinsic, types, reflections) = intrinsic_records( + &facts, + invocation, + self.types.len(), + &checked.program.checked().interner, + )?; + if intrinsic != self.intrinsic || types != self.types || reflections != self.reflections { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +impl CheckedExecution { + pub(crate) fn prepare_intrinsic_arguments( + &self, + callee: &Expr, + source_types: &[TypeExpr], + source_arguments: &[CallArg], + invocation: &CheckedInvocation, + ) -> Result { + self.intrinsic_current_body(invocation)?; + let reference = CheckedBodyReference { + cursor: invocation.body.clone(), + }; + let mut matches = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + let (original_callee, types, arguments, span) = match expression { + Expr::Call(callee, arguments, span) => { + (callee.as_ref(), &[][..], arguments.as_slice(), *span) + } + Expr::GenericCall(callee, types, arguments, span) => ( + callee.as_ref(), + types.as_slice(), + arguments.as_slice(), + *span, + ), + _ => return, + }; + if span == invocation.span + && std::ptr::eq(original_callee, callee) + && same_slice(arguments, source_arguments) + && same_slice(types, source_types) + { + matches += 1; + } + }, + &mut |_| {}, + ); + if matches != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + self.prepared_intrinsic(invocation, source_types.len()) + } + + pub(crate) fn prepare_pipeline_intrinsic_arguments( + &self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result { + self.intrinsic_current_body(prepared.invocation())?; + let reference = CheckedBodyReference { + cursor: prepared.invocation().body.clone(), + }; + let mut found = None; + let mut count = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + let Expr::Pipeline(_, steps, _) = expression else { + return; + }; + for (index, step) in steps.iter().enumerate() { + if std::ptr::eq(step, prepared.original()) { + found = Some((expression, index)); + count += 1; + } + } + }, + &mut |_| {}, + ); + if count != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + let (pipeline, index) = found.ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let original = self.prepare_pipeline_step(pipeline, index)?; + if original.invocation().packet != prepared.invocation().packet + || !std::ptr::eq(original.callee(), prepared.callee()) + || !same_slice(original.type_arguments(), prepared.type_arguments()) + || !same_slice(original.extra_arguments(), prepared.extra_arguments()) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + self.prepared_intrinsic(prepared.invocation(), prepared.type_arguments().len()) + } + + fn intrinsic_current_body( + &self, + invocation: &CheckedInvocation, + ) -> Result<(), ResourceExecutionError> { + if self.attempt_key(&self.body)? != self.attempt_key(&invocation.body)? { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } + + fn prepared_intrinsic( + &self, + invocation: &CheckedInvocation, + arity: usize, + ) -> Result { + let facts = self.facts(&invocation.body)?; + let (intrinsic, types, reflections) = + intrinsic_records(&facts, invocation, arity, &self.program.checked().interner)?; + Ok(PreparedIntrinsicArguments { + program: self.program.clone(), + key: self.attempt_key(&invocation.body)?, + packet: invocation.packet.clone(), + span: invocation.span, + intrinsic, + types: types.to_vec(), + reflections: reflections.to_vec(), + }) + } +} + +fn same_slice(left: &[T], right: &[T]) -> bool { + left.len() == right.len() && (left.is_empty() || std::ptr::eq(left.as_ptr(), right.as_ptr())) +} + +pub(super) fn intrinsic_records<'a>( + facts: &BodyFacts<'a>, + invocation: &CheckedInvocation, + arity: usize, + types: &jett_types::TypeInterner, +) -> Result<(IntrinsicId, &'a [TypeId], &'a [ReflectionTypeInfo]), ResourceExecutionError> { + if facts.calls.get(&invocation.span) != Some(&invocation.packet) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let CheckedInvocationTarget::Intrinsic(intrinsic) = invocation.target() else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let CheckedInvocationShape::Intrinsic { + intrinsic: shape, + result_type, + operands, + } = &invocation.packet.shape + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if shape != intrinsic + || facts.intrinsics.get(&invocation.span) != Some(intrinsic) + || operands.len() != invocation.arguments().len() + || result_type.index() as usize >= types.len() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let concrete = facts + .intrinsic_types + .get(&invocation.span) + .map(Vec::as_slice) + .unwrap_or(&[]); + if concrete.len() != arity + || (arity != 0 && !facts.intrinsic_types.contains_key(&invocation.span)) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + for &ty in concrete { + concrete_type(types, ty)?; + } + let reflected = facts + .intrinsic_reflections + .get(&invocation.span) + .map(Vec::as_slice) + .unwrap_or(&[]); + if (metadata_leaf(*intrinsic) && (arity != 1 || reflected.len() != 1)) + || (!reflected.is_empty() && reflected.len() != arity) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok((*intrinsic, concrete, reflected)) +} + +fn metadata_leaf(intrinsic: IntrinsicId) -> bool { + matches!( + intrinsic, + IntrinsicId::TypeName + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo + | IntrinsicId::TypeArg + ) +} + +fn concrete_type( + types: &jett_types::TypeInterner, + ty: TypeId, +) -> Result<(), ResourceExecutionError> { + let mut pending = vec![ty]; + let mut seen = std::collections::HashSet::new(); + while let Some(ty) = pending.pop() { + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !seen.insert(ty) { + continue; + } + pending.extend_from_slice(types.nominal_type_arguments(ty)); + match types.resolve(ty) { + Type::Error => return Err(ResourceExecutionError::MissingCheckedBody), + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => pending.push(*inner), + Type::Map(key, value) | Type::Result(key, value) => { + pending.push(*key); + pending.push(*value); + } + Type::Function { + params, + return_type, + .. + } => { + pending.extend_from_slice(params); + pending.push(*return_type); + } + _ => {} + } + } + Ok(()) +} + +#[cfg(test)] +#[path = "intrinsic/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs new file mode 100644 index 00000000..c8f7e7df --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/intrinsic/tests.rs @@ -0,0 +1,328 @@ +use super::*; +use crate::resource_execution::tests::program as checked_program; +use crate::{Interpreter, Value}; +use jett_types::{ReflectionMetadata, TypeInterner}; + +const SOURCE: &str = r#"namespace app +type Label = int64 +function label[T]() returns string: + return type.name[T]() +export function integer_label() returns string: + return label[int64]() +export function string_label() returns string: + return label[string]() +export function alias_label() returns string: + return label[Label]() +export function alias_kind() returns string: + return type.kind[Label]() +export function alias_info() returns string: + return type.info[Label]().type_name +export function scoped_label() returns string: + comptime type Outer = type.info[list[int64]](): + comptime type Element = type.arg[Outer](0): + return type.name[Element]() + return "unreached" +export function pipeline_label() returns string: + return (0 into type.arg[list[int64]]()).type_name +export function secret_flag() returns bool: + return type.has_secret[secret[int64]]() +export function list_label() returns string: + return type.name[list[int64]]() +"#; + +fn function<'a>(program: &'a Arc, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .expect("one retained fixture function") +} + +fn entry(checked: &mut CheckedExecution, program: &Arc, name: &str) { + let function = function(program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let signature = program.checked().definition_types[&definition]; + let reference = checked + .prepare_function_body(&FunctionInvocation::Entry { + definition, + signature, + }) + .unwrap(); + checked.install_function_body(&reference).unwrap(); +} + +fn first_call(function: &FunctionDef) -> &Expr { + let mut found = None; + walk_block(&function.body, &mut |_| {}, &mut |expression| { + if found.is_none() && matches!(expression, Expr::Call(..) | Expr::GenericCall(..)) { + found = Some(expression); + } + }); + found.expect("actual original call") +} + +fn parts(call: &Expr) -> (&Expr, &[TypeExpr], &[CallArg], Span) { + match call { + Expr::Call(callee, arguments, span) => (callee.as_ref(), &[], arguments, *span), + Expr::GenericCall(callee, types, arguments, span) => { + (callee.as_ref(), types, arguments, *span) + } + _ => panic!("original source call"), + } +} + +fn generic_invocation( + checked: &mut CheckedExecution, + program: &Arc, + wrapper: &str, +) -> CheckedInvocation { + entry(checked, program, wrapper); + let (_, _, _, span) = parts(first_call(function(program, wrapper))); + let caller = checked.invocation(span).unwrap(); + let body = checked + .prepare_function_body(&FunctionInvocation::Source(&caller)) + .unwrap(); + checked.install_function_body(&body).unwrap(); + checked + .invocation(first_call(function(program, "label")).span()) + .unwrap() +} + +#[test] +fn resource_intrinsic_source_calls_preserve_concrete_alias_and_nested_metadata_without_ambient_maps() + { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + // Empty legacy reflection metadata cannot stand in for the exact body records. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + for (name, expected) in [ + ("integer_label", Value::String("int64".to_string())), + ("string_label", Value::String("string".to_string())), + ("alias_label", Value::String("app.Label".to_string())), + ("alias_kind", Value::String("alias".to_string())), + ("alias_info", Value::String("app.Label".to_string())), + ("scoped_label", Value::String("int64".to_string())), + ("pipeline_label", Value::String("int64".to_string())), + ("secret_flag", Value::Bool(true)), + ("list_label", Value::String("list[int64]".to_string())), + ("integer_label", Value::String("int64".to_string())), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + expected, + "{name} release={release}" + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn resource_intrinsic_packet_rejects_cloned_original_foreign_and_other_concrete_contexts() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let integer = generic_invocation(&mut checked, &program, "integer_label"); + let (callee, types, arguments, _) = parts(first_call(function(&program, "label"))); + let prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .unwrap(); + assert_eq!(prepared.types(), &[TypeInterner::INT64]); + assert_eq!(prepared.reflections()[0].type_name, "int64"); + prepared.validate(&checked, &integer).unwrap(); + assert!( + checked + .prepare_intrinsic_arguments(callee, &types.to_vec(), arguments, &integer) + .is_err() + ); + let clone = first_call(function(&program, "label")).clone(); + let (callee_clone, types_clone, arguments_clone, _) = parts(&clone); + assert!( + checked + .prepare_intrinsic_arguments(callee_clone, types_clone, arguments_clone, &integer) + .is_err() + ); + let text = generic_invocation(&mut checked, &program, "string_label"); + assert_eq!(integer.span(), text.span()); + assert_eq!(integer.packet(), text.packet()); // Only the saved concrete body distinguishes this call. + assert!( + checked + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .is_err() + ); + assert!(prepared.validate(&checked, &text).is_err()); + let foreign_program = checked_program(SOURCE, release); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let foreign_call = generic_invocation(&mut foreign, &foreign_program, "integer_label"); + assert_eq!( + prepared.validate(&foreign, &foreign_call), + Err(ResourceExecutionError::ForeignProgram) + ); + assert!( + foreign + .prepare_intrinsic_arguments(callee, types, arguments, &integer) + .is_err() + ); + } +} + +#[test] +fn resource_intrinsic_packet_rejects_missing_records_counts_ids_shapes_and_unresolved_types() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let invocation = generic_invocation(&mut checked, &program, "integer_label"); + for mutation in 0..7 { + let original = checked.facts(&invocation.body).unwrap(); + let mut concrete = original.intrinsic_types.clone(); + let mut reflections = original.intrinsic_reflections.clone(); + let mut ids = original.intrinsics.clone(); + let mut calls = original.calls.clone(); + let mut copied = CheckedInvocation { + packet: invocation.packet.clone(), + body: invocation.body.clone(), + span: invocation.span, + }; + match mutation { + 0 => { + concrete.remove(&invocation.span); + } + 1 => { + reflections.remove(&invocation.span); + } + 2 => { + ids.insert(invocation.span, IntrinsicId::TypeKind); + } + 3 => { + concrete.insert( + invocation.span, + vec![TypeInterner::INT64, TypeInterner::STRING], + ); + } + 4 => { + let extra = reflections[&invocation.span][0].clone(); + reflections.get_mut(&invocation.span).unwrap().push(extra); + } + 5 => { + concrete.insert(invocation.span, vec![TypeInterner::ERROR]); + } + _ => { + let CheckedInvocationShape::Intrinsic { intrinsic, .. } = + &mut copied.packet.shape + else { + unreachable!() + }; + *intrinsic = IntrinsicId::TypeKind; + calls.insert(invocation.span, copied.packet.clone()); + } + } + let facts = BodyFacts { + assignment_places: original.assignment_places, + calls: &calls, + actor_messages: original.actor_messages, + argument_orders: original.argument_orders, + bindings: original.bindings, + types: original.types, + source_types: original.source_types, + pipeline_inputs: original.pipeline_inputs, + pipeline_calls: original.pipeline_calls, + scopes: original.scopes, + intrinsics: &ids, + intrinsic_types: &concrete, + intrinsic_reflections: &reflections, + constructions: original.constructions, + selections: original.selections, + }; + assert!( + intrinsic_records(&facts, &copied, 1, &program.checked().interner).is_err(), + "mutation {mutation}" + ); + } + let (callee, types, arguments, _) = parts(first_call(function(&program, "label"))); + for mutation in 0..3 { + let mut prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &invocation) + .unwrap(); + match mutation { + 0 => prepared.types[0] = TypeInterner::STRING, + 1 => prepared.reflections[0].type_name = "forged".to_string(), + _ => prepared.intrinsic = IntrinsicId::TypeKind, + } + assert!(prepared.validate(&checked, &invocation).is_err()); + } + entry(&mut checked, &program, "list_label"); + let (callee, types, arguments, span) = parts(first_call(function(&program, "list_label"))); + let invocation = checked.invocation(span).unwrap(); + let prepared = checked + .prepare_intrinsic_arguments(callee, types, arguments, &invocation) + .unwrap(); + let empty = TypeInterner::new(); + assert!(prepared.types()[0].index() as usize >= empty.len()); + assert_eq!( + concrete_type(&empty, prepared.types()[0]), + Err(ResourceExecutionError::InvalidCheckedProgram) + ); + } +} + +#[test] +fn resource_intrinsic_pipeline_packet_uses_original_step_and_cannot_accept_a_cloned_step() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + entry(&mut checked, &program, "pipeline_label"); + let mut pipeline = None; + walk_block( + &function(&program, "pipeline_label").body, + &mut |_| {}, + &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + pipeline = Some(expression); + } + }, + ); + let pipeline = pipeline.unwrap(); + let step = checked.prepare_pipeline_step(pipeline, 0).unwrap(); + let prepared = checked.prepare_pipeline_intrinsic_arguments(&step).unwrap(); + assert_eq!(prepared.intrinsic(), IntrinsicId::TypeArg); + assert_eq!(prepared.reflections()[0].args[0].type_name, "int64"); + prepared.validate(&checked, step.invocation()).unwrap(); + let clone = pipeline.clone(); + assert!(checked.prepare_pipeline_step(&clone, 0).is_err()); + let foreign_program = checked_program(SOURCE, release); + let mut foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + entry(&mut foreign, &foreign_program, "pipeline_label"); + assert!(foreign.prepare_pipeline_intrinsic_arguments(&step).is_err()); + entry(&mut checked, &program, "integer_label"); + assert!(checked.prepare_pipeline_intrinsic_arguments(&step).is_err()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs new file mode 100644 index 00000000..c05410e6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable.rs @@ -0,0 +1,218 @@ +//! Exact named Source descriptors contain no physical Resource or capture environment. +//! Their function signature remains Resource-bearing; only original occurrence +//! provenance permits ordinary storage and the dedicated checked indirect bridge. + +use super::*; + +pub(crate) struct PreparedNamedCallable { + key: CheckedAttemptKey, + expression: usize, + definition: DefId, + signature: TypeId, + aliases: Vec<(Span, DefId)>, +} + +impl PreparedNamedCallable { + pub(crate) fn definition(&self) -> DefId { + self.definition + } +} + +impl CheckedExecution { + pub(crate) fn prepare_named_callable( + &self, + expression: &Expr, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + // Scoped/generic callable adaptation needs its own complete body proof. + if !matches!(self.body.root, BodyRoot::Ordinary) || !self.body.scopes.is_empty() { + return Ok(None); + } + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let region = reference.region()?; + let index = expression_index(region, expression)?; + let mut aliases = Vec::new(); + let Some((definition, signature)) = + self.named_callable_in(region, expression, &mut aliases)? + else { + return Ok(None); + }; + Ok(Some(PreparedNamedCallable { + key: self.attempt_key(&self.body)?, + expression: index, + definition, + signature, + aliases, + })) + } + + fn named_callable_in( + &self, + region: OriginalRegion<'_>, + expression: &Expr, + aliases: &mut Vec<(Span, DefId)>, + ) -> Result, ResourceExecutionError> { + if let Expr::Paren(inner, _) = expression { + return self.named_callable_in(region, inner, aliases); + } + let (definition, identifier, member) = match expression { + Expr::Ident(identifier) => ( + self.resolved_definition(identifier.span)?, + Some(identifier), + None, + ), + Expr::FieldAccess(_, member, span) => { + // Namespace resolution authenticates the complete producer, + // not its base/member spelling or a runtime dotted name. + if self.program.checked().method_values.contains_key(span) { + return Ok(None); + } + let Some(definition) = self.program.resolved().resolutions.get(span).copied() + else { + return Ok(None); + }; + (definition, None, Some(member)) + } + _ => return Ok(None), + }; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if info.id != definition { + return Ok(None); + } + match info.kind { + jett_resolve::DefKind::Function if !self.has_hook(definition) => { + let function = self.retained_function(definition)?; + if !function.type_params.is_empty() + || member.is_some_and(|member| { + info.namespace.is_none() || member.name != function.name.name + }) + { + return Ok(None); + } + let signature = self.expression_type(expression.span())?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) || self.program.checked().definition_types.get(&definition) != Some(&signature) + { + return Ok(None); + } + Ok(Some((definition, signature))) + } + jett_resolve::DefKind::Variable => { + let Some(identifier) = identifier else { + return Ok(None); + }; + let signature = self.expression_type(expression.span())?; + if !matches!( + self.program.checked().interner.resolve(signature), + Type::Function { .. } + ) { + return Ok(None); + } + if aliases.iter().any(|(_, seen)| *seen == definition) { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut declarations = Vec::new(); + walk_region(region, &mut |_| {}, &mut |_| {}, &mut |statement| { + if let Stmt::VarDecl(declaration) = statement { + if self + .facts(&self.body) + .ok() + .and_then(|facts| facts.bindings.get(&declaration.name.span)) + .is_some_and(|fact| fact.definition == definition) + { + declarations.push(declaration); + } + } + }); + let [declaration] = declarations.as_slice() else { + return Ok(None); + }; + let fact = self.binding(declaration.name.span)?; + if declaration.mutable + || fact.mutable + || fact.ty != signature + || declaration.span.file != identifier.span.file + || declaration.span.end > identifier.span.start + { + return Ok(None); + } + aliases.push((declaration.name.span, definition)); + let target = self.named_callable_in(region, &declaration.value, aliases)?; + Ok(target.filter(|(_, target_signature)| *target_signature == signature)) + } + _ => Ok(None), + } + } + + pub(crate) fn prepare_named_indirect( + &self, + source: &CheckedInvocation, + callee: &Expr, + ) -> Result { + let target = self + .prepare_named_callable(callee)? + .ok_or(ResourceExecutionError::InvalidInvocation)?; + self.validate_named_indirect(source, &target)?; + Ok(target) + } + + pub(super) fn validate_named_indirect( + &self, + source: &CheckedInvocation, + target: &PreparedNamedCallable, + ) -> Result<(), ResourceExecutionError> { + self.validate_executable_cursor(&source.body)?; + if target.key != self.attempt_key(&source.body)? + || self.facts(&source.body)?.calls.get(&source.span) != Some(&source.packet) + || !matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + || !matches!(&source.packet.shape, CheckedInvocationShape::Function { signature_type } if *signature_type == target.signature) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let reference = CheckedBodyReference { + cursor: source.body.clone(), + }; + let region = reference.region()?; + let expression = region_expression(region, target.expression)?; + let mut calls = 0; + walk_region( + region, + &mut |_| {}, + &mut |original| { + if let Expr::Call(callee, _, span) = original { + let mut bare = callee.as_ref(); + while let Expr::Paren(inner, _) = bare { + bare = inner; + } + if *span == source.span && std::ptr::eq(bare, expression) { + calls += 1; + } + } + }, + &mut |_| {}, + ); + let mut aliases = Vec::new(); + if calls != 1 + || self.named_callable_in(region, expression, &mut aliases)? + != Some((target.definition, target.signature)) + || aliases != target.aliases + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "named_callable_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs new file mode 100644 index 00000000..56a45241 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/named_callable_tests.rs @@ -0,0 +1,516 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; +use jett_common::FileId; + +const SOURCE: &str = "namespace app +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.terminal_failure() + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + alias(token) + return nothing +"; + +fn execution(release: bool) -> (CheckedExecution, CheckedBodyReference, DefId) { + execution_of(SOURCE, "scenario", release) +} + +fn execution_of( + source: &str, + name: &str, + release: bool, +) -> (CheckedExecution, CheckedBodyReference, DefId) { + let program = crate::resource_execution::tests::program(source, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let scenario = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let entry = checked.declaration_definition(scenario.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(entry).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); + (checked, body, entry) +} + +fn indirect(body: &CheckedBodyReference) -> (&Expr, Span) { + let mut found = Vec::new(); + walk_block( + &body.function().unwrap().body, + &mut |_| {}, + &mut |expression| { + if let Expr::Call(callee, _, span) = expression { + if matches!(callee.as_ref(), Expr::Ident(identifier) if identifier.name == "alias") + { + found.push((callee.as_ref(), *span)); + } + } + }, + ); + let [call] = found.as_slice() else { + panic!("exact original indirect call"); + }; + *call +} + +#[test] +fn checked_named_indirect_keeps_original_packet_and_exact_same_signature_body() { + for release in [false, true] { + let (checked, body, _) = execution(release); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + assert!( + matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + ); + assert_eq!(target.aliases.len(), 2); + let selected = checked + .prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target, + }) + .unwrap(); + assert_eq!(selected.function().unwrap().name.name, "close_selected"); + let wrong = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "wrong_target" => Some(function), + _ => None, + }) + .unwrap(); + let wrong_definition = checked.declaration_definition(wrong.name.span).unwrap(); + assert_eq!( + checked + .program + .checked() + .definition_types + .get(&wrong_definition), + Some(&target.signature) + ); + assert_ne!(target.definition(), wrong_definition); + } +} + +#[test] +fn checked_named_indirect_refuses_cloned_foreign_and_same_signature_substituted_authority() { + for release in [false, true] { + let (checked, body, _) = execution(release); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + assert!(matches!( + checked.prepare_named_indirect(&source, &callee.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + let wrong = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "wrong_target" => Some(function), + _ => None, + }) + .unwrap(); + target.definition = checked.declaration_definition(wrong.name.span).unwrap(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let (foreign, foreign_body, _) = execution(release); + let (foreign_callee, foreign_span) = indirect(&foreign_body); + let foreign_source = foreign.invocation(foreign_span).unwrap(); + let foreign_target = foreign + .prepare_named_indirect(&foreign_source, foreign_callee) + .unwrap(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &foreign_target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.aliases.pop(); + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + let mut corrupt = checked.invocation(span).unwrap(); + corrupt.packet.arguments[0].parameter_type = jett_types::TypeInterner::INT64; + assert!(matches!( + checked.prepare_function_body(&FunctionInvocation::NamedSource { + source: &corrupt, + target: &target + }), + Err(ResourceExecutionError::InvalidInvocation) + )); + } +} + +#[test] +fn checked_source_named_indirect_immutable_alias_closes_once() { + for release in [false, true] { + let (checked, _, entry) = execution(release); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 701, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(entry, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(701), + ProviderEvent::Finalized(701) + ], + 0, + 0 + ) + ); + } +} + +const NAMESPACE_SOURCE: &str = "namespace controls +export function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.terminal_failure() + return nothing +export function generic_close[T](token: resource_probe.TestHandle, unused: T) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +namespace app +struct CallbackHolder: + callback: function(resource_probe.TestHandle) returns nothing +struct Observer: + value: int64 + function close(view self: Observer, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function field_control(view holder: CallbackHolder) returns nothing: + function(resource_probe.TestHandle) returns nothing candidate = clone holder.callback + return nothing +function method_control() returns nothing: + function(view Observer, resource_probe.TestHandle) returns nothing candidate = Observer.close + return nothing +function generic_control(token: resource_probe.TestHandle) returns nothing: + use controls + controls.generic_close[int64](token, 7) + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe as probe + function(resource_probe.TestHandle) returns nothing dispose = probe.close + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = probe.create(view net, 711) handle error: + return nothing + alias(token) + return nothing +"; + +fn initializer<'a>(body: &'a CheckedBodyReference, name: &str) -> &'a Expr { + let declarations = body + .function() + .unwrap() + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::VarDecl(declaration) if declaration.name.name == name => Some(&declaration.value), + _ => None, + }) + .collect::>(); + let [expression] = declarations.as_slice() else { + panic!("exact original initializer `{name}`"); + }; + expression +} + +#[test] +fn checked_namespace_callable_joins_full_producer_span_and_retained_body() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let producer = initializer(&body, "dispose"); + let Expr::FieldAccess(base, member, span) = producer else { + panic!("original namespace producer"); + }; + let target = checked.prepare_named_callable(producer).unwrap().unwrap(); + assert_eq!(checked.resolved_definition(*span), Ok(target.definition)); + assert!( + !checked + .program + .resolved() + .resolutions + .contains_key(&base.span()) + ); + assert!( + !checked + .program + .resolved() + .resolutions + .contains_key(&member.span) + ); + assert_eq!(member.name, "close"); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let target = checked.prepare_named_indirect(&source, callee).unwrap(); + assert_eq!(target.aliases.len(), 2); + assert!( + matches!(source.target(), CheckedInvocationTarget::Indirect(signature) if *signature == target.signature) + ); + let selected = checked + .prepare_function_body(&FunctionInvocation::NamedSource { + source: &source, + target: &target, + }) + .unwrap(); + assert_eq!(selected.function().unwrap().name.name, "close"); + assert_eq!( + selected.function().unwrap().name.span.file, + FileId::new(10_000) + ); + let wrong = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "controls.wrong_target") + .unwrap(); + assert_eq!( + checked.program.checked().definition_types.get(&wrong.id), + Some(&target.signature) + ); + assert_ne!(wrong.id, target.definition); + } +} + +#[test] +fn checked_namespace_callable_refuses_cloned_foreign_target_and_signature_substitution() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let producer = initializer(&body, "dispose"); + assert!(matches!( + checked.prepare_named_callable(&producer.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let (callee, span) = indirect(&body); + let source = checked.invocation(span).unwrap(); + let wrong = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "controls.wrong_target") + .unwrap(); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.definition = wrong.id; + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let mut target = checked.prepare_named_indirect(&source, callee).unwrap(); + target.signature = jett_types::TypeInterner::INT64; + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let target = checked.prepare_named_callable(producer).unwrap().unwrap(); + assert!(matches!( + checked.validate_named_indirect(&source, &target), + Err(ResourceExecutionError::InvalidInvocation) + )); + let (foreign, foreign_body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let (foreign_callee, foreign_span) = indirect(&foreign_body); + let foreign_source = foreign.invocation(foreign_span).unwrap(); + let foreign_target = foreign + .prepare_named_indirect(&foreign_source, foreign_callee) + .unwrap(); + assert!(matches!( + checked.validate_named_indirect(&source, &foreign_target), + Err(ResourceExecutionError::InvalidInvocation) + )); + } +} + +#[test] +fn checked_namespace_callable_refuses_original_instance_method_generic_and_hook_producers() { + for release in [false, true] { + let (checked, body, _) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let signature = checked + .expression_type(initializer(&body, "dispose").span()) + .unwrap(); + let (field_checked, field_body, _) = + execution_of(NAMESPACE_SOURCE, "field_control", release); + let Expr::Clone(field, _) = initializer(&field_body, "candidate") else { + panic!("original function field read"); + }; + assert!( + matches!(field.as_ref(), Expr::FieldAccess(_, member, _) if member.name == "callback") + ); + assert_eq!( + field_checked.expression_type(field.span()).unwrap(), + signature + ); + assert!( + !field_checked + .program + .resolved() + .resolutions + .contains_key(&field.span()) + ); + assert!( + field_checked + .prepare_named_callable(field) + .unwrap() + .is_none() + ); + let (method_checked, method_body, _) = + execution_of(NAMESPACE_SOURCE, "method_control", release); + let method = initializer(&method_body, "candidate"); + assert!( + method_checked + .program + .checked() + .method_values + .contains_key(&method.span()) + ); + assert!( + method_checked + .prepare_named_callable(method) + .unwrap() + .is_none() + ); + let (generic_checked, generic_body, _) = + execution_of(NAMESPACE_SOURCE, "generic_control", release); + let Stmt::Expr(statement) = &generic_body.function().unwrap().body.stmts[1] else { + panic!("original generic call"); + }; + let Expr::GenericCall(callee, _, _, _) = &statement.expr else { + panic!("original generic callee"); + }; + let definition = generic_checked.resolved_definition(callee.span()).unwrap(); + assert!( + !generic_checked + .retained_function(definition) + .unwrap() + .type_params + .is_empty() + ); + assert!( + generic_checked + .prepare_named_callable(callee) + .unwrap() + .is_none() + ); + let mut hook_checked = + CheckedExecution::new(checked.program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let hook_function = checked + .program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "close_descriptor" => { + Some(function) + } + _ => None, + }) + .unwrap(); + let entry = hook_checked + .declaration_definition(hook_function.name.span) + .unwrap(); + let hook_body = hook_checked + .prepare_function_body(&hook_checked.entry(entry).unwrap()) + .unwrap(); + hook_checked.install_function_body(&hook_body).unwrap(); + let Stmt::Return(statement) = &hook_body.function().unwrap().body.stmts[0] else { + panic!("original hook descriptor return"); + }; + let hook = statement.value.as_ref().unwrap(); + assert!(hook_checked.has_hook(hook_checked.resolved_definition(hook.span()).unwrap())); + assert!(hook_checked.prepare_named_callable(hook).unwrap().is_none()); + } +} + +#[test] +fn checked_source_namespace_callable_and_immutable_alias_close_once() { + for release in [false, true] { + let (checked, _, entry) = execution_of(NAMESPACE_SOURCE, "scenario", release); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 711, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(entry, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(711), + ProviderEvent::Finalized(711) + ], + 0, + 0 + ) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection.rs new file mode 100644 index 00000000..13a0bde8 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection.rs @@ -0,0 +1,169 @@ +//! Canonical owner data from one authenticated ordinary field intrinsic. +//! No returned TypeId grants Resource custody or refinement proof authority. +use super::*; +use jett_parser::ast::{PipelineStep, TypeExpr}; + +impl CheckedExecution { + pub(crate) fn field_intrinsic_owner( + &self, + callee: &Expr, + source_types: &[TypeExpr], + source_arguments: &[CallArg], + span: Span, + ) -> Result, ResourceExecutionError> { + let facts = self.facts(&self.body)?; + if !facts + .intrinsics + .get(&span) + .copied() + .is_some_and(field_intrinsic) + { + return Ok(None); + } + self.field_attempt_key(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut count = 0; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + let (original, types, arguments, occurrence) = match expression { + Expr::Call(callee, arguments, span) => { + (callee.as_ref(), &[][..], arguments.as_slice(), *span) + } + Expr::GenericCall(callee, types, arguments, span) => ( + callee.as_ref(), + types.as_slice(), + arguments.as_slice(), + *span, + ), + _ => return, + }; + if occurrence == span + && std::ptr::eq(original, callee) + && same_slice(types, source_types) + && same_slice(arguments, source_arguments) + { + count += 1; + } + }, + &mut |_| {}, + ); + if count != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + self.field_intrinsic_owner_at(span, source_types.len()) + } + + pub(crate) fn field_pipeline_intrinsic_owner( + &self, + source: &PipelineStep, + ) -> Result, ResourceExecutionError> { + let facts = self.facts(&self.body)?; + if !facts + .intrinsics + .get(&source.span) + .copied() + .is_some_and(field_intrinsic) + { + return Ok(None); + } + self.field_attempt_key(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut count = 0; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |expression| { + if let Expr::Pipeline(_, steps, _) = expression { + count += steps + .iter() + .filter(|step| std::ptr::eq(*step, source)) + .count(); + } + }, + &mut |_| {}, + ); + if count != 1 { + return Err(ResourceExecutionError::MissingCheckedInvocation); + } + let function = match &source.function { + Expr::View(inner, _) => inner.as_ref(), + other => other, + }; + let arity = match function { + Expr::GenericCall(_, types, _, _) => types.len(), + _ => 0, + }; + self.field_intrinsic_owner_at(source.span, arity) + } + + fn field_intrinsic_owner_at( + &self, + span: Span, + arity: usize, + ) -> Result, ResourceExecutionError> { + let invocation = self.invocation(span)?; + let facts = self.facts(&self.body)?; + let (_, types, reflections) = intrinsic::intrinsic_records( + &facts, + &invocation, + arity, + &self.program.checked().interner, + )?; + let owner = *types + .first() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let reflection = reflections + .first() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if reflections.len() != types.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + checked_field_reflection_owner(&self.program.checked().interner, owner, reflection) + } +} + +fn same_slice(left: &[T], right: &[T]) -> bool { + left.len() == right.len() && (left.is_empty() || std::ptr::eq(left.as_ptr(), right.as_ptr())) +} + +pub(super) fn checked_field_reflection_owner( + types: &jett_types::TypeInterner, + owner: TypeId, + source: &ReflectionTypeInfo, +) -> Result, ResourceExecutionError> { + let kind = match source.kind.as_str() { + "struct" => "struct", + "bitfield" => "bitfield", + _ => return Ok(None), + }; + let canonical = match types.resolve(owner) { + Type::Struct(_) => "struct", + Type::Bitfield(_) => "bitfield", + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }; + if kind != canonical { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(Some(owner)) +} + +pub(super) fn field_intrinsic(intrinsic: IntrinsicId) -> bool { + matches!( + intrinsic, + IntrinsicId::TypeFields + | IntrinsicId::TypeFieldValue + | IntrinsicId::TypeConstructStart + | IntrinsicId::TypeConstructPut + | IntrinsicId::TypeConstructFinish + ) +} + +#[cfg(test)] +#[path = "ordinary_field_reflection/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection/tests.rs new file mode 100644 index 00000000..ec525b9f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/ordinary_field_reflection/tests.rs @@ -0,0 +1,200 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::{Interpreter, Value}; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +const SOURCE: &str = r#"namespace app +type Label = int64 +struct Box[T]: + item: T +type WholeAlias = Box[Label] +type Positive = int64 where value > 0 +function reflected() returns int64: + Box[Label] source = Box[Label](item: 7) + mutable TypeConstruction builder = type.construct_start[Box[Label]]() + for field in type.fields[Box[Label]](): + comptime type Field = field.type_info: + Field value = type.field_value[Box[Label], Field](view source, view field) + builder = type.construct_put[Box[Label], Field](builder, view field, value) handle error: + return -1 + Box[Label] built = type.construct_finish[Box[Label]](builder) handle error: + return -2 + return built.item +function alias_fields() returns int64: + mutable int64 count = 0 + for field in type.fields[WholeAlias](): + count = count + 1 + for field in type.fields[Positive](): + count = count + 1 + return count +function alias_kind() returns string: + return type.arg[Box[Label]](0).kind +function pipeline() returns result[Box[Label], string]: + return type.construct_start[Box[Label]]() into type.construct_finish[Box[Label]]() +function main() returns int64: + return comptime reflected() +"#; + +fn program(release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!("reflection Source prerequisite: {:?}", error.diagnostics()) + }), + ) +} + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .expect("genuine Source function") +} + +fn enter(checked: &mut CheckedExecution, name: &str) { + let original = function(checked.program(), name); + let definition = checked + .program() + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.span == original.name.span) + .unwrap(); + let body = checked + .prepare_field_function_body(&definition.name, &definition.namespace, original, None, &[]) + .unwrap(); + checked.install_field_function_body(&body).unwrap(); +} + +#[test] +fn actorless_reflected_alias_owner_runs_closed_comptime_and_preserves_source_metadata() { + for release in [false, true] { + let program = program(release); + assert!(program.checked().actor_handlers.is_empty()); + assert!(program.checked().assignment_places.is_empty()); + assert!(program.checked().resource_hooks.is_empty()); + let types = Arc::new(CheckedExpressionTypes::from_checked_program(&program)); + let capture = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + program.checked().reflection_metadata.clone(), + types.clone(), + Arc::new(program.checked().breakpoint_exclusions.clone()), + ); + assert!(capture.diagnostics.is_empty(), "{:?}", capture.diagnostics); + assert!( + capture + .values + .values() + .any(|value| value.payload() == &Value::Int64(7)) + ); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(program.checked().reflection_metadata.clone()); + interpreter.set_checked_expression_types(types); + for (name, value) in [ + ("reflected", Value::Int64(7)), + ("alias_fields", Value::Int64(0)), + ("alias_kind", Value::String("alias".into())), + ] { + assert_eq!( + interpreter + .call_checked_function_in_namespace(Some("app"), name, vec![]) + .unwrap() + .payload(), + &value + ); + } + } +} + +#[test] +fn ordinary_field_owner_requires_the_exact_physical_source_call() { + let program = program(false); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + enter(&mut checked, "reflected"); + let mut call = None; + walk_block( + &function(&program, "reflected").body, + &mut |_| {}, + &mut |expression| { + if let Expr::GenericCall(_, _, _, span) = expression { + if program.checked().intrinsic_ids.get(span) + == Some(&IntrinsicId::TypeConstructStart) + { + call = Some(expression); + } + } + }, + ); + let Expr::GenericCall(callee, types, arguments, span) = call.unwrap() else { + unreachable!() + }; + let owner = checked + .field_intrinsic_owner(callee, types, arguments, *span) + .unwrap() + .unwrap(); + assert_eq!( + program.checked().interner.type_name(owner), + "app.Box[int64]" + ); + let copied_callee = callee.clone(); + assert!( + checked + .field_intrinsic_owner(&copied_callee, types, arguments, *span) + .is_err() + ); + assert!( + checked + .field_intrinsic_owner(callee, &types.to_vec(), arguments, *span) + .is_err() + ); +} + +#[test] +fn ordinary_field_owner_requires_the_exact_physical_pipeline_step() { + let program = program(false); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + enter(&mut checked, "pipeline"); + let mut step = None; + walk_block( + &function(&program, "pipeline").body, + &mut |_| {}, + &mut |expression| { + if let Expr::Pipeline(_, steps, _) = expression { + step = steps.first(); + } + }, + ); + let original = step.unwrap(); + let owner = checked + .field_pipeline_intrinsic_owner(original) + .unwrap() + .unwrap(); + assert_eq!( + program.checked().interner.type_name(owner), + "app.Box[int64]" + ); + assert!( + checked + .field_pipeline_intrinsic_owner(&original.clone()) + .is_err() + ); +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs new file mode 100644 index 00000000..d213ff48 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct.rs @@ -0,0 +1,204 @@ +//! Constructor identity only; runtime tickets are supplied by reached actuals. +use super::*; + +pub(crate) struct PreparedOwnedStruct { + key: CheckedAttemptKey, + expression: usize, + definition: DefId, + ty: TypeId, + name: String, + fields: Vec<(String, TypeId)>, + source_fields: Vec, +} + +impl PreparedOwnedStruct { + /// Canonical aliases share these checked primitive kinds. Qualifications + /// remain distinct and cannot acquire constructor authority by peeling. + pub(crate) fn supports_companion(kind: &Type) -> bool { + matches!( + kind, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Nothing + ) + } + + pub(crate) fn ty(&self) -> TypeId { + self.ty + } + + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn fields(&self) -> &[(String, TypeId)] { + &self.fields + } + + pub(crate) fn source_fields(&self) -> &[usize] { + &self.source_fields + } +} + +impl CheckedExecution { + pub(crate) fn prepare_owned_struct( + &self, + expression: &Expr, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let Expr::Call(callee, arguments, span) = expression else { + return Ok(None); + }; + let facts = self.facts(&self.body)?; + let Some(construction) = facts.constructions.get(span) else { + return Ok(None); + }; + let ty = construction.struct_type; + if !self.type_contains_resource(ty)? { + return Ok(None); + } + // Refinement evaluation and generic declaration association need their + // own connected transport. They remain Source requirements. + if construction.validates_refinements + || !matches!(self.body.root, BodyRoot::Ordinary) + || !self.body.scopes.is_empty() + { + return Ok(None); + } + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let index = expression_index(reference.region()?, expression)?; + if self.source_call_dispatch(callee, arguments, *span)? + || self.expression_type(*span)? != ty + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let definition = self.resolved_definition(callee.span())?; + let info = self + .program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .ok_or(ResourceExecutionError::InvalidInvocation)?; + let Type::Struct(owner) = self.program.checked().interner.resolve(ty) else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + let structure = self.program.checked().interner.resolve_struct(*owner); + if info.id != definition + || info.kind != jett_resolve::DefKind::Struct + || info.name != structure.name + || self.program.checked().definition_types.get(&definition) != Some(&ty) + || !self + .program + .checked() + .interner + .nominal_type_arguments(ty) + .is_empty() + { + return Err(ResourceExecutionError::InvalidInvocation); + } + let declarations = self + .program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Struct(declaration) if declaration.name.span == info.span => { + Some(declaration) + } + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if !declaration.type_params.is_empty() { + return Ok(None); + } + if declaration.fields.len() != structure.fields.len() + || declaration + .fields + .iter() + .zip(&structure.fields) + .any(|(original, field)| original.name.name != field.0) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + // This vertical has direct nominal leaves and scalar companions only. + // Do not accidentally grant a nested/capability/refined carrier path. + if structure.fields.iter().any(|(_, field)| { + let kind = self.program.checked().interner.resolve(*field); + !matches!(kind, Type::Resource(_)) && !PreparedOwnedStruct::supports_companion(kind) + }) { + return Ok(None); + } + if arguments.len() != structure.fields.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + let mut assigned = vec![false; structure.fields.len()]; + let mut source_fields = Vec::with_capacity(arguments.len()); + for argument in arguments { + let ordinal = match &argument.name { + Some(name) => structure + .fields + .iter() + .position(|(field, _)| field == &name.name), + None => assigned.iter().position(|assigned| !assigned), + } + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if assigned[ordinal] + || self.expression_type(argument.value.span())? != structure.fields[ordinal].1 + { + return Err(ResourceExecutionError::InvalidInvocation); + } + assigned[ordinal] = true; + source_fields.push(ordinal); + } + Ok(Some(PreparedOwnedStruct { + key: self.attempt_key(&self.body)?, + expression: index, + definition, + ty, + name: structure.name.clone(), + fields: structure.fields.clone(), + source_fields, + })) + } + + pub(crate) fn revalidate_owned_struct( + &self, + proof: &PreparedOwnedStruct, + expression: &Expr, + ) -> Result<(), ResourceExecutionError> { + let current = self + .prepare_owned_struct(expression)? + .ok_or(ResourceExecutionError::InvalidInvocation)?; + if current.key != proof.key + || current.expression != proof.expression + || current.definition != proof.definition + || current.ty != proof.ty + || current.name != proof.name + || current.fields != proof.fields + || current.source_fields != proof.source_fields + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs new file mode 100644 index 00000000..4a40ca9d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/owned_struct_tests.rs @@ -0,0 +1,193 @@ +use super::*; +use jett_common::FileId; + +const SOURCE: &str = include_str!("../../fixtures/33_owned_struct_scope_drop_entry.jett"); + +fn execution(source: &str, name: &str) -> (CheckedExecution, CheckedBodyReference) { + let program = crate::resource_execution::tests::program(source, false); + execution_program(program, name) +} + +fn execution_program( + program: Arc, + name: &str, +) -> (CheckedExecution, CheckedBodyReference) { + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let body = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&body).unwrap(); + (checked, body) +} + +fn constructor(body: &CheckedBodyReference) -> &Expr { + constructor_named(body, "OwnedEnvelope") +} + +fn constructor_named<'a>(body: &'a CheckedBodyReference, selected: &str) -> &'a Expr { + let mut found = Vec::new(); + walk_region( + body.region().unwrap(), + &mut |_| {}, + &mut |expression| { + if let Expr::Call(callee, _, _) = expression { + if matches!(callee.as_ref(), Expr::Ident(name) if name.name == selected) { + found.push(expression); + } + } + }, + &mut |_| {}, + ); + assert_eq!(found.len(), 1); + found[0] +} + +#[test] +fn occupied_struct_primitive_companion_proofs_keep_exact_types_aliases_and_original_order() { + let sources = [ + ( + include_str!("../../fixtures/36_owned_struct_primitive_companions.jett"), + "retire_primitives", + "PrimitiveEnvelope", + ), + ( + include_str!("../../fixtures/37_owned_struct_alias_companions.jett"), + "retire_aliases", + "AliasEnvelope", + ), + ( + include_str!("../../fixtures/38_owned_struct_pending_int64_transport.jett"), + "retire_pending", + "PendingEnvelope", + ), + ( + include_str!("../../fixtures/40_owned_struct_pending_companions.jett"), + "retire_pending_primitives", + "PendingPrimitives", + ), + ( + include_str!("../../fixtures/42_owned_struct_pending_primitive_matrix.jett"), + "retire_pending_matrix", + "PendingMatrix", + ), + ]; + for (source, helper, name) in sources { + let (checked, body) = execution_program( + crate::resource_execution::tests::companion_program(source, false), + helper, + ); + let expression = constructor_named(&body, name); + let proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + checked.revalidate_owned_struct(&proof, expression).unwrap(); + assert!(checked.prepare_owned_struct(&expression.clone()).is_err()); + assert!(proof.fields().iter().all(|(_, ty)| matches!( + checked.program.checked().interner.resolve(*ty), + Type::Resource(_) + ) || PreparedOwnedStruct::supports_companion( + checked.program.checked().interner.resolve(*ty) + ))); + if name == "PrimitiveEnvelope" { + assert_eq!(&proof.source_fields()[..3], &[8, 9, 0]); + assert_eq!(proof.fields().len(), 14); + } + if name == "AliasEnvelope" { + assert_eq!(proof.fields()[1].1, jett_types::TypeInterner::INT8); + assert_eq!(proof.fields()[2].1, jett_types::TypeInterner::UINT64); + assert_eq!(proof.fields()[3].1, jett_types::TypeInterner::FLOAT32); + } + let mut redirected = proof; + redirected.fields[1].1 = jett_types::TypeInterner::STRING; + assert!( + checked + .revalidate_owned_struct(&redirected, expression) + .is_err() + ); + } +} + +#[test] +fn owned_struct_original_constructor_has_exact_type_and_source_field_mapping() { + let (checked, body) = execution(SOURCE, "retire_owned"); + let expression = constructor(&body); + let proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + assert_eq!(proof.source_fields(), &[0, 1]); + assert_eq!(proof.fields()[0].0, "token"); + assert_eq!( + proof.fields()[1], + ("marker".into(), jett_types::TypeInterner::INT64) + ); + checked.revalidate_owned_struct(&proof, expression).unwrap(); +} + +#[test] +fn owned_struct_constructor_copied_foreign_wrong_body_and_redirected_proofs_are_refused() { + let (checked, body) = execution(SOURCE, "retire_owned"); + let expression = constructor(&body); + let proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + assert!(checked.prepare_owned_struct(&expression.clone()).is_err()); + let (foreign, foreign_body) = execution(SOURCE, "retire_owned"); + assert!( + checked + .revalidate_owned_struct(&proof, constructor(&foreign_body)) + .is_err() + ); + assert!( + foreign + .revalidate_owned_struct(&proof, constructor(&foreign_body)) + .is_err() + ); + let (other, _) = execution(SOURCE, "main"); + assert!(other.revalidate_owned_struct(&proof, expression).is_err()); + for mutation in 0..4 { + let mut forged = checked.prepare_owned_struct(expression).unwrap().unwrap(); + match mutation { + 0 => forged.source_fields.swap(0, 1), + 1 => forged.ty = jett_types::TypeInterner::INT64, + 2 => forged.fields[0].1 = jett_types::TypeInterner::INT64, + 3 => forged.expression += 1, + _ => unreachable!(), + } + assert!( + checked + .revalidate_owned_struct(&forged, expression) + .is_err() + ); + } +} + +#[test] +fn occupied_struct_same_layout_declaration_cannot_substitute_for_selected_constructor() { + let source = SOURCE.replace( + "function retire_owned(", + "struct OtherEnvelope:\n token: resource_probe.TestHandle\n marker: int64\nfunction retire_owned(", + ); + let (checked, body) = execution(&source, "retire_owned"); + let expression = constructor(&body); + let mut proof = checked.prepare_owned_struct(expression).unwrap().unwrap(); + let other = checked + .program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| info.kind == jett_resolve::DefKind::Struct && info.name == "app.OtherEnvelope") + .unwrap(); + assert_ne!(other.id, proof.definition); + proof.definition = other.id; + assert!(checked.revalidate_owned_struct(&proof, expression).is_err()); +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs new file mode 100644 index 00000000..a534270a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline.rs @@ -0,0 +1,212 @@ +//! One original checked PipelineStep, distinct from an Expr::Call packet. +use jett_parser::ast::{PipelineStep, TypeExpr}; +use jett_typecheck::{CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax}; + +use super::*; + +pub(crate) struct PreparedPipelineStep<'source> { + original: &'source PipelineStep, + initial: Option<&'source Expr>, + callee: &'source Expr, + type_arguments: &'source [TypeExpr], + extra_arguments: &'source [CallArg], + invocation: CheckedInvocation, + input_type: TypeId, + raw_call_type: TypeId, + output_type: TypeId, + initial_borrow: Option, + constructor: Option<(String, String, TypeId)>, +} + +impl<'source> PreparedPipelineStep<'source> { + pub(crate) fn original(&self) -> &'source PipelineStep { + self.original + } + pub(crate) fn initial(&self) -> Option<&'source Expr> { + self.initial + } + pub(crate) fn callee(&self) -> &'source Expr { + self.callee + } + pub(crate) fn type_arguments(&self) -> &'source [TypeExpr] { + self.type_arguments + } + pub(crate) fn extra_arguments(&self) -> &'source [CallArg] { + self.extra_arguments + } + pub(crate) fn invocation(&self) -> &CheckedInvocation { + &self.invocation + } + pub(crate) fn input_type(&self) -> TypeId { + self.input_type + } + pub(crate) fn raw_call_type(&self) -> TypeId { + self.raw_call_type + } + pub(crate) fn output_type(&self) -> TypeId { + self.output_type + } + pub(crate) fn initial_borrow(&self) -> Option { + self.initial_borrow + } + pub(crate) fn constructor(&self) -> Option<(String, String, TypeId)> { + self.constructor.clone() + } +} + +impl CheckedExecution { + pub(crate) fn prepare_pipeline_step<'source>( + &self, + pipeline: &'source Expr, + index: usize, + ) -> Result, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + let mut matches = 0usize; + walk_region( + reference.region()?, + &mut |_| {}, + &mut |original| { + if matches!(original, Expr::Pipeline(..)) && std::ptr::eq(original, pipeline) { + matches += 1; + } + }, + &mut |_| {}, + ); + if matches != 1 { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::Pipeline(initial, steps, _) = pipeline else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let step = steps + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)?; + let input_span = if index == 0 { + initial.span() + } else { + steps + .get(index - 1) + .ok_or(ResourceExecutionError::MissingCheckedInvocation)? + .span + }; + let (callee, type_arguments, extra_arguments, step_view) = pipeline_parts(step); + let invocation = self.invocation(step.span)?; + if invocation.arguments().len() != extra_arguments.len() + 1 { + return Err(ResourceExecutionError::InvalidInvocation); + } + let source0 = &invocation.arguments()[0]; + let source_view = step_view || (index == 0 && written_view(initial)); + if source0.source_span != input_span + || source0.source_index != 0 + || source0.parameter_index != 0 + || (source0.syntax == CheckedCallerSyntax::WrittenView) != source_view + || (index != 0 && source0.origin != CheckedCallerOrigin::OwnedExpression) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + for (argument, fact) in extra_arguments.iter().zip(&invocation.arguments()[1..]) { + if argument.value.span() != fact.source_span + || (fact.syntax == CheckedCallerSyntax::WrittenView) + != written_view(&argument.value) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let facts = self.facts(&self.body)?; + let input_type = *facts + .pipeline_inputs + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let raw_call_type = *facts + .pipeline_calls + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let output_type = *facts + .source_types + .get(&step.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + for ty in [input_type, raw_call_type, output_type] { + if ty.index() as usize >= self.program.checked().interner.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + } + // A Function signature is its declaration result, independently of + // checked secret propagation at this actual invocation occurrence. + if let CheckedInvocationShape::Intrinsic { result_type, .. } = &invocation.packet().shape { + if raw_call_type != *result_type { + return Err(ResourceExecutionError::InvalidInvocation); + } + } + let initial_borrow = + if index == 0 && source_view && source0.effect == CheckedCallerEffect::RetainBorrow { + let mut bare = initial.as_ref(); + while let Expr::Paren(inner, _) | Expr::View(inner, _) = bare { + bare = inner; + } + if let (Expr::Ident(identifier), CheckedCallerOrigin::Binding(binding)) = + (bare, &source0.origin) + { + if self.resolved_definition(identifier.span)? != binding.definition + || facts.bindings.get(&binding.declaration_span) != Some(binding) + { + return Err(ResourceExecutionError::InvalidInvocation); + } + Some(binding.definition) + } else { + None + } + } else { + None + }; + // Keep root's closed enum preparation before evaluating even source0. + let constructor = self.enum_constructor(callee, &invocation)?; + Ok(PreparedPipelineStep { + original: step, + initial: (index == 0).then_some(initial.as_ref()), + callee, + type_arguments, + extra_arguments, + invocation, + input_type, + raw_call_type, + output_type, + initial_borrow, + constructor, + }) + } +} + +fn written_view(mut expression: &Expr) -> bool { + while let Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) = + expression + { + expression = inner; + } + matches!(expression, Expr::View(..)) +} + +fn pipeline_parts(step: &PipelineStep) -> (&Expr, &[TypeExpr], &[CallArg], bool) { + let (function, view) = match &step.function { + Expr::View(inner, _) => (inner.as_ref(), true), + other => (other, false), + }; + let (callee, types, arguments) = match function { + Expr::Call(callee, arguments, _) => (callee.as_ref(), &[][..], arguments.as_slice()), + Expr::GenericCall(callee, types, arguments, _) => { + (callee.as_ref(), types.as_slice(), arguments.as_slice()) + } + other => (other, &[][..], step.extra_args.as_slice()), + }; + let mut callee = callee; + while let Expr::Paren(inner, _) = callee { + callee = inner; + } + (callee, types, arguments, view) +} + +#[cfg(test)] +#[path = "pipeline/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs new file mode 100644 index 00000000..595f275a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/pipeline/tests.rs @@ -0,0 +1,345 @@ +use super::*; +use crate::resource_execution::tests::program; + +const CONSTRUCT: &str = include_str!("../../../fixtures/18_pipeline_construct_move_close.jett"); +const RETAIN: &str = include_str!("../../../fixtures/19_pipeline_written_view_retains_owner.jett"); +const ABORT: &str = include_str!("../../../fixtures/21_pipeline_named_actual_abort.jett"); + +fn scenario(program: &CheckedResourceProgram) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == jett_common::FileId::new(0) + && function.name.name == "scenario" => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one original source scenario"); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == jett_resolve::DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some("app") + }) + .map(|info| info.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact primary declaration"); + }; + *definition +} + +fn pipelines(reference: &CheckedBodyReference) -> Vec<&Expr> { + let mut found = Vec::new(); + walk_block(reference.block().unwrap(), &mut |_| {}, &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + found.push(expression); + } + }); + found +} + +#[test] +fn resource_pipeline_reference_joins_original_step_inputs_outputs_and_source_permutation() { + for release in [false, true] { + for source in [CONSTRUCT, ABORT] { + let program = program(source, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let entry = checked.entry(scenario(&program)).unwrap(); + let reference = checked.prepare_function_body(&entry).unwrap(); + checked.install_function_body(&reference).unwrap(); + for pipeline in pipelines(&reference) { + let Expr::Pipeline(initial, steps, _) = pipeline else { + unreachable!(); + }; + for index in 0..steps.len() { + let prepared = checked.prepare_pipeline_step(pipeline, index).unwrap(); + let input_span = if index == 0 { + initial.span() + } else { + steps[index - 1].span + }; + assert_eq!(prepared.invocation().arguments()[0].source_span, input_span); + let facts = checked.facts(&checked.body).unwrap(); + assert_eq!( + facts.pipeline_inputs[&steps[index].span], + prepared.input_type() + ); + assert_eq!( + facts.pipeline_calls[&steps[index].span], + prepared.raw_call_type() + ); + assert_eq!( + facts.source_types[&steps[index].span], + prepared.output_type() + ); + if index > 0 { + assert_eq!( + prepared.invocation().arguments()[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + } + if source == ABORT { + assert_eq!( + prepared + .invocation() + .arguments() + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + vec![(0, 0), (1, 2), (2, 1)] + ); + } + } + } + } + } +} + +#[test] +fn resource_pipeline_reference_refuses_foreign_clones_missing_body_and_invalid_step_index() { + for release in [false, true] { + let program = program(RETAIN, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let entry = checked.entry(scenario(&program)).unwrap(); + let reference = checked.prepare_function_body(&entry).unwrap(); + checked.install_function_body(&reference).unwrap(); + let found = pipelines(&reference); + let [original] = found.as_slice() else { + panic!("one exact pipeline source"); + }; + let prepared = checked.prepare_pipeline_step(original, 0).unwrap(); + assert_eq!( + prepared.invocation().arguments()[0].syntax, + CheckedCallerSyntax::WrittenView + ); + assert_eq!( + prepared.invocation().arguments()[0].effect, + CheckedCallerEffect::RetainBorrow + ); + let CheckedCallerOrigin::Binding(binding) = &prepared.invocation().arguments()[0].origin + else { + panic!("exact original root binding"); + }; + assert_eq!(prepared.initial_borrow(), Some(binding.definition)); + let other_program = crate::resource_execution::tests::program(RETAIN, release); + let mut foreign = + CheckedExecution::new(other_program, ExecutionPurpose::ReferenceRuntime).unwrap(); + assert!(matches!( + foreign.install_function_body(&reference), + Err(ResourceExecutionError::ForeignProgram) + )); + let cloned = (**original).clone(); + assert!(matches!( + checked.prepare_pipeline_step(&cloned, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert!(matches!( + checked.prepare_pipeline_step(original, usize::MAX), + Err(ResourceExecutionError::MissingCheckedInvocation) + )); + let fabricated = Expr::Call( + Box::new(prepared.callee().clone()), + prepared.extra_arguments().to_vec(), + prepared.original().span, + ); + assert!(matches!( + checked.prepare_pipeline_step(&fabricated, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + checked.enter_ordinary(); + assert!(matches!( + checked.prepare_pipeline_step(original, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} + +const REQUIRED_PIPELINES: &str = include_str!("../../../fixtures/28_pipeline_absence_regions.jett"); + +fn region_pipelines(reference: &CheckedBodyReference) -> Vec<&Expr> { + let mut found = Vec::new(); + walk_region( + reference.region().unwrap(), + &mut |_| {}, + &mut |expression| { + if matches!(expression, Expr::Pipeline(..)) { + found.push(expression); + } + }, + &mut |_| {}, + ); + found +} + +fn verify_region_steps(checked: &CheckedExecution, reference: &CheckedBodyReference) { + let found = region_pipelines(reference); + assert_eq!(found.len(), 1); + let original = found[0]; + let Expr::Pipeline(initial, steps, _) = original else { + unreachable!(); + }; + assert_eq!(steps.len(), 2); + for index in 0..steps.len() { + let prepared = checked.prepare_pipeline_step(original, index).unwrap(); + let facts = checked.facts(&checked.body).unwrap(); + assert_eq!( + prepared.invocation().arguments()[0].source_span, + if index == 0 { + initial.span() + } else { + steps[index - 1].span + } + ); + assert_eq!( + prepared.input_type(), + facts.pipeline_inputs[&steps[index].span] + ); + assert_eq!( + prepared.raw_call_type(), + facts.pipeline_calls[&steps[index].span] + ); + assert_eq!( + prepared.output_type(), + facts.source_types[&steps[index].span] + ); + } + let cloned = (*original).clone(); + assert!(matches!( + checked.prepare_pipeline_step(&cloned, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); +} + +#[test] +fn resource_pipeline_reference_authenticates_namespace_and_explicit_expression_regions() { + for release in [false, true] { + let program = program(REQUIRED_PIPELINES, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) + if value.name.name == "namespace_marker" + && value.name.span.file == jett_common::FileId::new(0) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + let initializer = checked.prepare_namespace_initializer(declaration).unwrap(); + let reference = initializer.reference(); + assert!(matches!( + reference.region().unwrap(), + OriginalRegion::Expression(_) + )); + checked.install_function_body(reference).unwrap(); + verify_region_steps(&checked, reference); + let namespace_pipeline = region_pipelines(reference)[0]; + + checked.replace_purpose(ExecutionPurpose::ExplicitComptime); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(value) if value.name.name == "explicit_marker" => Some(value), + _ => None, + }) + .unwrap(); + let returned = function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Return(value) => value.value.as_ref(), + _ => None, + }) + .unwrap(); + let Expr::Comptime(expression, span) = returned else { + panic!("original required expression"); + }; + let contexts = checked + .prepare_explicit_contexts(expression, *span, Some(function.name.span), &[]) + .unwrap(); + let [context] = contexts.as_slice() else { + panic!("one exact ordinary required context"); + }; + let explicit = context.reference(); + assert!(matches!( + explicit.region().unwrap(), + OriginalRegion::Expression(_) + )); + checked.install_function_body(explicit).unwrap(); + verify_region_steps(&checked, explicit); + assert!(matches!( + checked.prepare_pipeline_step(namespace_pipeline, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let mut foreign = CheckedExecution::new( + crate::resource_execution::tests::program(REQUIRED_PIPELINES, release), + ExecutionPurpose::ExplicitComptime, + ) + .unwrap(); + assert!(matches!( + foreign.install_function_body(explicit), + Err(ResourceExecutionError::ForeignProgram) + )); + } +} + +#[test] +fn resource_pipeline_reference_authenticates_verify_and_property_original_regions() { + for release in [false, true] { + let program = program(REQUIRED_PIPELINES, release); + let mut checked = CheckedExecution::new(program.clone(), ExecutionPurpose::Verify).unwrap(); + let verify = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Verify(value) => Some(value), + _ => None, + }) + .unwrap(); + let reference = checked.prepare_verify_body(verify).unwrap(); + checked.install_function_body(&reference).unwrap(); + verify_region_steps(&checked, &reference); + let verify_pipeline = region_pipelines(&reference)[0]; + checked.replace_purpose(ExecutionPurpose::Property); + let property = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(value) => Some(value), + _ => None, + }) + .unwrap(); + let property_reference = checked.prepare_property_body(property).unwrap(); + checked.install_function_body(&property_reference).unwrap(); + verify_region_steps(&checked, &property_reference); + assert!(matches!( + checked.prepare_pipeline_step(verify_pipeline, 0), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs new file mode 100644 index 00000000..007f9db3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields.rs @@ -0,0 +1,393 @@ +//! Original reflected field loops select ordinal-specific checked bodies. +//! Runtime metadata values remain data, never executable or provider authority. +use super::*; +use jett_parser::ast::ForStmt; +use jett_resolve::DefKind; +use jett_types::ReflectionFieldInfo; + +#[path = "reflected_fields/ordinary_field_updates.rs"] +mod ordinary_field_updates; + +pub(crate) struct PreparedReflectedFieldLoop { + parent: CheckedBodyReference, + key: CheckedAttemptKey, + loop_span: Span, + variable: DefId, + owner: TypeId, + fields: Vec<(TypeId, ReflectionFieldInfo)>, +} + +#[derive(Clone)] +pub(crate) struct PreparedReflectedFieldIteration { + owner: Arc, + index: usize, +} + +impl PreparedReflectedFieldLoop { + pub(crate) fn owner_name(&self) -> String { + self.parent + .cursor + .executable + .as_ref() + .expect("prepared original field loop") + .program + .checked() + .interner + .type_name(self.owner) + } + + pub(crate) fn fields(&self) -> impl Iterator { + self.fields.iter().map(|(_, field)| field) + } + + pub(crate) fn validate_count(&self, count: usize) -> Result<(), ResourceExecutionError> { + if count != self.fields.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } + + pub(crate) fn iteration( + self: &Arc, + index: usize, + ) -> Result { + if index >= self.fields.len() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(PreparedReflectedFieldIteration { + owner: self.clone(), + index, + }) + } + + fn original(&self) -> Result<&ForStmt, ResourceExecutionError> { + original_for(self.parent.region()?, self.loop_span) + } +} + +impl PreparedReflectedFieldIteration { + pub(crate) fn variable(&self) -> DefId { + self.owner.variable + } + + pub(crate) fn validate_value( + &self, + value: &crate::Value, + ) -> Result<(), ResourceExecutionError> { + let (_, field) = self + .owner + .fields + .get(self.index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let expected = + crate::Interpreter::reflection_field_info_value(&self.owner.owner_name(), None, field); + if value != &expected { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } +} + +impl CheckedExecution { + pub(crate) fn checked_reflection_fields( + &self, + owner: TypeId, + ) -> Result, ResourceExecutionError> { + Ok(checked_owner_fields(self.program(), owner)? + .into_iter() + .map(|(_, field)| field) + .collect()) + } + + pub(crate) fn prepare_reflected_field_loop( + &self, + source: &ForStmt, + ) -> Result>, ResourceExecutionError> { + self.validate_executable_cursor(&self.body)?; + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = original_for(parent.region()?, source.span)?; + if !std::ptr::eq(original, source) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::GenericCall(callee, source_types, arguments, occurrence) = &original.iterable + else { + return Ok(None); + }; + if self.facts(&self.body)?.intrinsics.get(occurrence) != Some(&IntrinsicId::TypeFields) { + return Ok(None); + } + if source_types.len() != 1 || !arguments.is_empty() || original.value_variable.is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let invocation = self.invocation(*occurrence)?; + let prepared = + self.prepare_intrinsic_arguments(callee, source_types, arguments, &invocation)?; + if prepared.intrinsic() != IntrinsicId::TypeFields { + return Err(ResourceExecutionError::InvalidInvocation); + } + let [owner] = prepared.types() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reflection = prepared + .reflections() + .first() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let fields = checked_owner_fields_for_reflection(self.program(), *owner, reflection)?; + let variables = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Variable + && definition.span == original.variable.span + && definition.name == original.variable.name + }) + .map(|definition| definition.id) + .collect::>(); + let [variable] = variables.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let loop_span = original.span; + Ok(Some(Arc::new(PreparedReflectedFieldLoop { + parent, + key: self.attempt_key(&self.body)?, + loop_span, + variable: *variable, + owner: *owner, + fields, + }))) + } + + pub(crate) fn prepare_reflected_field_scope( + &self, + bind: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + ) -> Result { + // Preserve Resource entry eligibility before and after shared Source selection. + self.validate_executable_cursor(&iteration.owner.parent.cursor)?; + self.validate_executable_cursor(&self.body)?; + let prepared = self.prepare_original_reflected_field_scope(bind, iteration)?; + self.validate_executable_cursor(&prepared.reference.cursor)?; + Ok(prepared) + } + + fn prepare_original_reflected_field_scope( + &self, + bind: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + ) -> Result { + let proof = &iteration.owner; + if self.field_attempt_key(&proof.parent.cursor)? != proof.key { + return Err(ResourceExecutionError::MissingCheckedBody); + } + if !self + .field_attempt_key(&self.body)? + .is_lexical_descendant_of(&proof.key) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let source_loop = proof.original()?; + let Expr::GenericCall(_, source_types, arguments, occurrence) = &source_loop.iterable + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let loop_facts = self.facts(&proof.parent.cursor)?; + let variable = self + .program + .resolved() + .scope_table + .definitions + .get(proof.variable.index() as usize) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if source_types.len() != 1 + || !arguments.is_empty() + || loop_facts.intrinsics.get(occurrence) != Some(&IntrinsicId::TypeFields) + || loop_facts + .intrinsic_types + .get(occurrence) + .map(Vec::as_slice) + != Some(&[proof.owner][..]) + || variable.id != proof.variable + || variable.kind != DefKind::Variable + || variable.span != source_loop.variable.span + || variable.name != source_loop.variable.name + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = region_binding(parent.region()?, bind.span)?; + if !std::ptr::eq(original, bind) || !binding_under_loop(&source_loop.body, original) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::FieldAccess(base, member, _) = &original.value else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let Expr::Ident(variable) = base.as_ref() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if member.name != "type_info" + || self.program.resolved().resolutions.get(&variable.span) != Some(&proof.variable) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + // Rejoin the immutable checked owner table, not a cloned runtime TypeInfo. + let fields = checked_owner_fields(self.program(), proof.owner)?; + if fields != proof.fields { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let (bound_type, field) = fields + .get(iteration.index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let facts = self.facts(&self.body)?; + let candidates = facts + .scopes + .get(&original.span) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .iter() + .enumerate() + .filter(|(_, candidate)| { + candidate.selection + == CheckedComptimeTypeSelection::ReflectedIteration(iteration.index) + }) + .collect::>(); + let [(index, candidate)] = candidates.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if candidate.bound_type != *bound_type || candidate.reflection != field.type_info { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let mut cursor = self.body.clone(); + cursor.scopes.push(ScopedSelection { + owner: original.span, + index: *index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(original.span)); + Ok(PreparedDirectScope { + reference: CheckedBodyReference { cursor }, + projection: scoped_projection(candidate, &self.program.checked().interner), + reflection: candidate.reflection.clone(), + }) + } +} + +fn checked_owner_fields_for_reflection( + program: &CheckedResourceProgram, + owner: TypeId, + source: &ReflectionTypeInfo, +) -> Result, ResourceExecutionError> { + if super::ordinary_field_reflection::checked_field_reflection_owner( + &program.checked().interner, + owner, + source, + )? + .is_none() + { + return Ok(Vec::new()); + } + checked_owner_fields(program, owner) +} + +fn checked_owner_fields( + program: &CheckedResourceProgram, + owner: TypeId, +) -> Result, ResourceExecutionError> { + let types = &program.checked().interner; + if owner.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + let field_types: Vec = match types.resolve(owner) { + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect(), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .map(|field| field.ty) + .collect(), + _ => return Ok(Vec::new()), + }; + let fields = program + .checked() + .reflection_metadata + .get_type_fields_for_id(owner) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if fields.len() != field_types.len() + || fields + .iter() + .enumerate() + .any(|(index, field)| field.index != index) + || field_types + .iter() + .any(|ty| ty.index() as usize >= types.len()) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(field_types + .into_iter() + .zip(fields.iter().cloned()) + .collect()) +} + +fn original_for( + region: OriginalRegion<'_>, + span: Span, +) -> Result<&ForStmt, ResourceExecutionError> { + let mut loops = Vec::new(); + walk_region(region, &mut |_| {}, &mut |_| {}, &mut |statement| { + if let Stmt::For(value) = statement { + if value.span == span { + loops.push(value); + } + } + }); + let [original] = loops.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + Ok(*original) +} + +fn binding_under_loop(block: &Block, selected: &ComptimeTypeBindStmt) -> bool { + let mut matches = 0; + walk_block( + block, + &mut |binding| { + if std::ptr::eq(binding, selected) { + matches += 1; + } else if binding_under_loop(&binding.body, selected) { + matches += 1; + } + }, + &mut |_| {}, + ); + matches == 1 +} + +#[cfg(test)] +#[path = "reflected_fields/tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "reflected_fields/recursion_tests.rs"] +mod recursion_tests; + +#[cfg(test)] +#[path = "reflected_fields/metadata_shapes_tests.rs"] +mod metadata_shapes_tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett new file mode 100644 index 00000000..15f0aa22 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/01_reflected_return.jett @@ -0,0 +1,14 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + return path + return 999 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett new file mode 100644 index 00000000..6a794822 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/02_reflected_break.jett @@ -0,0 +1,16 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + break + path = path * 10 + 8 + path = path * 10 + 9 + return path * 10 + 7 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett new file mode 100644 index 00000000..714af610 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/03_reflected_continue.jett @@ -0,0 +1,16 @@ +namespace app +struct FlowShape: + first: int64 + second: int64 + third: int64 +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element digit = field.index + 1 + path = path * 10 + digit + if field.index == 1: + continue + path = path * 10 + 8 + path = path * 10 + 9 + return path * 10 + 7 diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett new file mode 100644 index 00000000..01d23feb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/control_flow/04_reflected_default.jett @@ -0,0 +1,21 @@ +namespace app +type Count = int64 +struct FlowShape: + first: int64 + second: int64 + named: Count +function selected_or_failure(index: int64) returns result[int64, string]: + if index == 1: + return fail("selected middle field") + return ok(index + 1) +export function scenario() returns int64: + mutable int64 path = 0 + for field in type.fields[FlowShape](): + comptime type Element = field.type_info: + Element observed = selected_or_failure(field.index) handle error: + default 9 + Element resumed = observed + path = path * 10 + resumed + if type.name[Element]() == "app.Count": + path = path + 1000 + return path diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett new file mode 100644 index 00000000..582051d9 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes.jett @@ -0,0 +1,31 @@ +namespace app +struct Heterogeneous: + label: string + count: int64 + enabled: bool +bitfield WideCounter: + sequence: 64 bits +struct Pair[T, U]: + first: T + second: U +export function heterogeneous_fields() returns string: + mutable string labels = "" + for field in type.fields[Heterogeneous](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels +export function bitfield_fields() returns string: + mutable string labels = "" + for field in type.fields[WideCounter](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels +export function generic_fields() returns string: + mutable string labels = "" + for field in type.fields[Pair[int64, string]](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{field.name}:{label}|" + return labels diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs new file mode 100644 index 00000000..291a4ef5 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/metadata_shapes_tests.rs @@ -0,0 +1,173 @@ +//! Checked field types for heterogeneous, bitfield and generic owners. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{ComptimeTypeBindStmt, ForStmt, FunctionDef, Item, Stmt}; +use jett_types::{ReflectionMetadata, Type, TypeInterner}; +use std::sync::Arc; + +const SHAPES_SOURCE: &str = include_str!("metadata_shapes.jett"); + +fn original_function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn original_loop(function: &FunctionDef) -> &ForStmt { + function + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .expect("one original direct field loop") +} + +fn original_binding(statement: &ForStmt) -> &ComptimeTypeBindStmt { + statement + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .expect("original field.type_info binding") +} + +#[test] +fn heterogeneous_bitfield_and_concrete_generic_owners_select_original_ordinal_types() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SHAPES_SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + for (name, is_bitfield, expected) in [ + ( + "heterogeneous_fields", + false, + vec![ + TypeInterner::STRING, + TypeInterner::INT64, + TypeInterner::BOOL, + ], + ), + ("bitfield_fields", true, vec![TypeInterner::UINT64]), + ( + "generic_fields", + false, + vec![TypeInterner::INT64, TypeInterner::STRING], + ), + ] { + let function = original_function(&program, name); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let parent = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&parent).unwrap(); + let before = checked.attempt_key(&checked.cursor()).unwrap(); + let source_loop = original_loop(function); + let binding = original_binding(source_loop); + let prepared = checked + .prepare_reflected_field_loop(source_loop) + .unwrap() + .unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + assert_eq!( + prepared + .fields + .iter() + .map(|field| field.0) + .collect::>(), + expected + ); + assert_eq!( + matches!( + program.checked().interner.resolve(prepared.owner), + Type::Bitfield(_) + ), + is_bitfield + ); + if !is_bitfield { + assert!(matches!( + program.checked().interner.resolve(prepared.owner), + Type::Struct(_) + )); + } + for index in 0..prepared.fields.len() { + let scope = checked + .prepare_reflected_field_scope(binding, &prepared.iteration(index).unwrap()) + .unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + assert!(std::ptr::eq(scope.body().unwrap(), &binding.body)); + assert_eq!(scope.reflection(), &prepared.fields[index].1.type_info); + let selected = scope.reference.cursor.scopes.last().unwrap(); + assert_eq!( + selected.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!(selected.bound_type, expected[index]); + checked.install_direct_scope(&scope).unwrap(); + checked.install_function_body(&parent).unwrap(); + assert_eq!(checked.attempt_key(&checked.cursor()).unwrap(), before); + } + } + } +} + +#[test] +fn selected_metadata_shapes_execute_in_source_order_without_runtime_authority() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SHAPES_SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for _ in 0..2 { + for (name, expected) in [ + ( + "heterogeneous_fields", + "label:string|count:int64|enabled:bool|", + ), + ("bitfield_fields", "sequence:uint64|"), + ("generic_fields", "first:int64|second:string|"), + ] { + let definition = checked + .declaration_definition(original_function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::String(expected.to_string()), + "{name}, release={release}" + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/ordinary_field_updates.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/ordinary_field_updates.rs new file mode 100644 index 00000000..9371a97a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/ordinary_field_updates.rs @@ -0,0 +1,85 @@ +//! Ordinary field scope identity reuses genuine reflected loop/iteration proofs. +//! This creates no Resource entry, provider, ledger, or custody authority. +use super::*; + +impl CheckedExecution { + pub(crate) fn prepare_field_reflected_loop( + &self, + source: &ForStmt, + ) -> Result>, ResourceExecutionError> { + self.validate_field_executable_cursor(&self.body)?; + let parent = CheckedBodyReference { + cursor: self.body.clone(), + }; + let original = original_for(parent.region()?, source.span)?; + if !std::ptr::eq(original, source) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let Expr::GenericCall(_, source_types, arguments, occurrence) = &original.iterable else { + return Ok(None); + }; + let facts = self.facts(&self.body)?; + if facts.intrinsics.get(occurrence) != Some(&IntrinsicId::TypeFields) { + return Ok(None); + } + if source_types.len() != 1 || !arguments.is_empty() || original.value_variable.is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + // The retained For node supplies the exact physical call. Rejoin its + // checked packet and concrete operands without Resource entry helpers, + // which deliberately reject runtime InlineBody cursors. + let invocation = self.invocation(*occurrence)?; + let (intrinsic, types, reflections) = super::super::intrinsic::intrinsic_records( + &facts, + &invocation, + source_types.len(), + &self.program.checked().interner, + )?; + if intrinsic != IntrinsicId::TypeFields { + return Err(ResourceExecutionError::InvalidInvocation); + } + let [owner] = types else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let reflection = reflections + .first() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let fields = checked_owner_fields_for_reflection(self.program(), *owner, reflection)?; + let mut variables = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Variable + && definition.span == original.variable.span + && definition.name == original.variable.name + }); + let variable = variables + .next() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + if variables.next().is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let loop_span = original.span; + Ok(Some(Arc::new(PreparedReflectedFieldLoop { + parent, + key: self.field_attempt_key(&self.body)?, + loop_span, + variable: variable.id, + owner: *owner, + fields, + }))) + } + + pub(crate) fn prepare_field_reflected_scope( + &self, + bind: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + ) -> Result { + let prepared = self.prepare_original_reflected_field_scope(bind, iteration)?; + self.validate_field_executable_cursor(&prepared.reference.cursor)?; + Ok(prepared) + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs new file mode 100644 index 00000000..dad18186 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursion_tests.rs @@ -0,0 +1,161 @@ +//! Recursive reflected calls restore each caller's field selector and alias. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{FunctionDef, Item, Stmt}; +use jett_types::ReflectionMetadata; +use std::sync::Arc; + +const RECURSIVE_SOURCE: &str = include_str!("recursive_labels.jett"); +const BASE: &str = concat!( + "first:int64<>first:int64|", + "second:int64<>second:int64|", + "named:app.Count<>named:app.Count|", +); +const DEPTH_ONE: &str = concat!( + "first:int64<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">first:int64|", + "second:int64<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">second:int64|", + "named:app.Count<", + "first:int64<>first:int64|second:int64<>second:int64|named:app.Count<>named:app.Count|", + ">named:app.Count|", +); + +fn depth_two_expected() -> String { + // A fixed golden composition, not a second recursive evaluator. Every + // outer field must observe its own ordinal and alias after the same Source + // function executes all three inner field scopes twice more in depth. + format!( + "first:int64<{DEPTH_ONE}>first:int64|second:int64<{DEPTH_ONE}>second:int64|named:app.Count<{DEPTH_ONE}>named:app.Count|" + ) +} + +fn original_function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn assert_no_resource_effects(interpreter: &mut Interpreter) { + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn same_source_body_recursion_restores_each_callers_field_ordinal_and_alias() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(RECURSIVE_SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let original = original_function(&program, "recursive_labels"); + let definition = checked.declaration_definition(original.name.span).unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + assert_no_resource_effects(&mut interpreter); + for (depth, expected) in [ + (0, BASE.to_string()), + (1, DEPTH_ONE.to_string()), + (2, depth_two_expected()), + (1, DEPTH_ONE.to_string()), + (0, BASE.to_string()), + ] { + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![Value::Int64(depth)]) + .unwrap(), + Value::String(expected), + "depth={depth}, release={release}" + ); + // Frozen observation includes checked reflected selector depth, + // checked body/source state, metadata stacks and lexical scopes. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn closed_pure_same_body_recursion_is_selected_by_required_evaluation() { + let source = format!( + "{RECURSIVE_SOURCE}{}", + r#" +export function required_recursive_labels() returns string: + return comptime recursive_labels(2) +"# + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(&source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "release={release}: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let expected = Value::String(depth_two_expected()); + assert_eq!(required.values.len(), 1); + assert_eq!( + required.values.values().cloned().collect::>(), + vec![expected.clone()] + ); + assert!(required.values.checked_values_are_mirrored()); + assert!( + required + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + let proofs = required.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one authenticated required recursion value"); + }; + let original = original_function(&program, "required_recursive_labels"); + let [Stmt::Return(statement)] = original.body.stmts.as_slice() else { + panic!("one original explicit comptime return"); + }; + assert!(proof.matches_original_comptime(statement.value.as_ref().unwrap())); + assert!(proof.belongs_to(&program)); + assert!(proof.is_explicit_comptime()); + assert_eq!(proof.value(), &expected); + assert!(proof.checked_hook(&program).unwrap().is_none()); + // Required capture creates its own worker without a provider. Its API + // does not expose provider observations or the worker's final snapshot. + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett new file mode 100644 index 00000000..4a26488a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/recursive_labels.jett @@ -0,0 +1,19 @@ +namespace app +type Count = int64 +struct IterationShape: + first: int64 + second: int64 + named: Count +export function recursive_labels(depth: int64) returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string field_before = field.name + string before = type.name[Element]() + mutable string inside = "" + if depth > 0: + inside = recursive_labels(depth - 1) + string field_after = field.name + string after = type.name[Element]() + labels = "{labels}{field_before}:{before}<{inside}>{field_after}:{after}|" + return labels diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs new file mode 100644 index 00000000..f0b40275 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/reflected_fields/tests.rs @@ -0,0 +1,882 @@ +//! Exact original-loop and ordinal-specific checked-body proofs. +//! Deliberate proof corruption remains confined to this private test module. + +use super::*; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{Block, ComptimeTypeBindStmt, ForStmt, FunctionDef, Item, Stmt}; +use jett_types::{ReflectionMetadata, TypeInterner}; +use std::sync::Arc; + +// The first two fields intentionally share both TypeId and reflection. The +// third has the same canonical TypeId but retains an explicit source alias. +// Both loops in `two_loops` use the same owner and variable spelling, so only +// original Source occurrence identity can separate their prepared proofs. +const SOURCE: &str = r#"namespace app +type Count = int64 +struct IterationShape: + first: int64 + second: int64 + named: Count +enum NoFields: + missing +export function reflected() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function other() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function two_loops() returns nothing: + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function ordinary_loop() returns nothing: + for value in list(1): + int64 marker = value + return nothing +export function nested_scope() returns nothing: + for field in type.fields[IterationShape](): + comptime type Outer = type.info[int64](): + comptime type Element = field.type_info: + string label = type.name[Element]() + return nothing +export function reflected_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +export function two_loop_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +function helper_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string label = type.name[Element]() + labels = "{labels}{label}|" + return labels +export function helper_inside_loop_labels() returns string: + mutable string labels = "" + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + string before = type.name[Element]() + string inside = helper_labels() + string after = type.name[Element]() + labels = "{labels}{before}>{inside}>{after}|" + return labels +export function empty_fields() returns int64: + mutable int64 total = 0 + for field in type.fields[int64](): + total = total + 1 + for field in type.fields[NoFields](): + total = total + 1 + for field in type.fields[resource_probe.TestHandle](): + total = total + 1 + return total +"#; + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == name && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .expect("one original project function") +} + +fn loop_at(function: &FunctionDef, index: usize) -> &ForStmt { + function + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .nth(index) + .expect("one original top-level loop at the requested position") +} + +fn field_scope(block: &Block) -> &ComptimeTypeBindStmt { + block + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .expect("original field.type_info binding") +} + +fn install_entry( + checked: &mut CheckedExecution, + program: &CheckedResourceProgram, + name: &str, +) -> CheckedBodyReference { + let source = function(program, name); + let definition = checked.declaration_definition(source.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&reference).unwrap(); + reference +} + +fn execution( + release: bool, + name: &str, +) -> ( + Arc, + CheckedExecution, + CheckedBodyReference, +) { + let program = crate::resource_execution::tests::program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let reference = install_entry(&mut checked, &program, name); + (program, checked, reference) +} + +fn current_key(checked: &CheckedExecution) -> CheckedAttemptKey { + checked.attempt_key(&checked.cursor()).unwrap() +} + +// Deliberate proof forgery is confined to the child test module. No public +// constructor or production Clone requirement is introduced by this helper. +fn copy_loop(source: &PreparedReflectedFieldLoop) -> PreparedReflectedFieldLoop { + PreparedReflectedFieldLoop { + parent: source.parent.clone(), + key: source.key.clone(), + loop_span: source.loop_span, + variable: source.variable, + owner: source.owner, + fields: source.fields.clone(), + } +} + +fn assert_scope_refused_without_mutation( + checked: &CheckedExecution, + binding: &ComptimeTypeBindStmt, + iteration: &PreparedReflectedFieldIteration, + reason: &str, +) { + let before = current_key(checked); + assert!( + checked + .prepare_reflected_field_scope(binding, iteration) + .is_err(), + "{reason}" + ); + assert_eq!(current_key(checked), before, "{reason}: cursor changed"); +} + +#[test] +fn reflected_field_scope_keeps_equal_types_and_source_aliases_in_distinct_iterations() { + for release in [false, true] { + let (program, mut checked, parent) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let before = current_key(&checked); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .expect("trusted direct type.fields loop"); + assert_eq!(current_key(&checked), before); + assert_eq!(prepared.fields.len(), 3); + assert_eq!(prepared.fields[0].0, TypeInterner::INT64); + assert_eq!(prepared.fields[0].0, prepared.fields[1].0); + assert_eq!(prepared.fields[0].0, prepared.fields[2].0); + assert_eq!( + prepared.fields[0].1.type_info, + prepared.fields[1].1.type_info + ); + assert_eq!(prepared.fields[2].1.type_info.kind, "alias"); + assert_eq!(prepared.fields[2].1.type_info.type_name, "app.Count"); + assert_ne!( + prepared.fields[0].1.type_info, + prepared.fields[2].1.type_info + ); + + for index in 0..prepared.fields.len() { + let iteration = prepared.iteration(index).unwrap(); + let scope = checked + .prepare_reflected_field_scope(binding, &iteration) + .unwrap(); + assert_eq!(current_key(&checked), before); + assert!(std::ptr::eq(scope.body().unwrap(), &binding.body)); + assert_eq!(scope.bound_name(), "int64"); + assert_eq!(scope.reflection(), &prepared.fields[index].1.type_info); + let selected = scope.reference.cursor.scopes.last().unwrap(); + assert_eq!( + selected.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!(selected.bound_type, prepared.fields[index].0); + + checked.install_direct_scope(&scope).unwrap(); + assert_ne!(current_key(&checked), before); + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + "parent-loop proof cannot enter from its own selected child body", + ); + checked.install_function_body(&parent).unwrap(); + assert_eq!(current_key(&checked), before); + } + assert!(prepared.iteration(prepared.fields.len()).is_err()); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_loop_and_scope_require_original_nodes_within_the_current_body() { + for release in [false, true] { + let (program, checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let before = current_key(&checked); + let cloned_loop = original_loop.clone(); + assert!(checked.prepare_reflected_field_loop(&cloned_loop).is_err()); + assert_eq!(current_key(&checked), before); + + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let iteration = prepared.iteration(0).unwrap(); + let cloned_binding = binding.clone(); + assert_scope_refused_without_mutation( + &checked, + &cloned_binding, + &iteration, + "a clone preserves spans but has no original binding identity", + ); + + let other_loop = loop_at(function(&program, "other"), 0); + assert!(checked.prepare_reflected_field_loop(other_loop).is_err()); + assert_scope_refused_without_mutation( + &checked, + field_scope(&other_loop.body), + &iteration, + "a different function's original binding is not the selected loop body", + ); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_iteration_cannot_cross_programs_or_changed_parent_frames() { + for release in [false, true] { + let (program, mut checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let iteration = prepared.iteration(0).unwrap(); + + let (foreign_program, foreign, _) = execution(release, "reflected"); + let foreign_loop = loop_at(function(&foreign_program, "reflected"), 0); + assert_scope_refused_without_mutation( + &foreign, + field_scope(&foreign_loop.body), + &iteration, + "identical Source in another compiler session has no shared authority", + ); + + install_entry(&mut checked, &program, "other"); + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + "a valid proof cannot be replayed after the active function changes", + ); + } +} + +#[test] +fn reflected_field_iteration_cannot_cross_equal_looking_original_loops() { + for release in [false, true] { + let (program, checked, _) = execution(release, "two_loops"); + let function = function(&program, "two_loops"); + let first = loop_at(function, 0); + let second = loop_at(function, 1); + let first_proof = checked + .prepare_reflected_field_loop(first) + .unwrap() + .unwrap(); + let second_proof = checked + .prepare_reflected_field_loop(second) + .unwrap() + .unwrap(); + assert_eq!(first_proof.owner, second_proof.owner); + assert_eq!(first_proof.fields, second_proof.fields); + assert_ne!(first_proof.loop_span, second_proof.loop_span); + assert_ne!(first_proof.variable, second_proof.variable); + assert_scope_refused_without_mutation( + &checked, + field_scope(&second.body), + &first_proof.iteration(0).unwrap(), + "same owner, variable spelling, index and type do not identify a Source loop", + ); + assert_scope_refused_without_mutation( + &checked, + field_scope(&first.body), + &second_proof.iteration(0).unwrap(), + "the converse loop replay must also fail", + ); + } +} + +#[test] +fn reflected_field_scope_revalidates_private_iteration_and_loop_proof_contents() { + for release in [false, true] { + let (program, checked, _) = execution(release, "reflected"); + let original_loop = loop_at(function(&program, "reflected"), 0); + let binding = field_scope(&original_loop.body); + let prepared = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let (_, foreign, foreign_parent) = execution(release, "reflected"); + let bad_index = PreparedReflectedFieldIteration { + owner: prepared.clone(), + index: prepared.fields.len(), + }; + assert_scope_refused_without_mutation( + &checked, + binding, + &bad_index, + "out-of-range index must fail even if iteration() was bypassed", + ); + + for corruption in 0..8 { + let mut forged = copy_loop(&prepared); + match corruption { + 0 => forged.variable = DefId::new(u32::MAX), + 1 => forged.owner = TypeInterner::STRING, + 2 => forged.fields[0].0 = TypeInterner::STRING, + 3 => forged.fields[0].1.type_info.kind = "struct".to_string(), + 4 => forged.fields[0].1.name = "different_field".to_string(), + 5 => forged.loop_span = loop_at(function(&program, "other"), 0).span, + 6 => forged.key = current_key(&foreign), + 7 => forged.parent = foreign_parent.clone(), + _ => unreachable!(), + } + let iteration = PreparedReflectedFieldIteration { + owner: Arc::new(forged), + index: 0, + }; + assert_scope_refused_without_mutation( + &checked, + binding, + &iteration, + &format!("private proof corruption {corruption}"), + ); + } + } +} + +#[test] +fn ordinary_source_loop_returns_none_without_minting_a_reflected_proof() { + for release in [false, true] { + let (program, checked, _) = execution(release, "ordinary_loop"); + let original_loop = loop_at(function(&program, "ordinary_loop"), 0); + let before = current_key(&checked); + assert!( + checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .is_none() + ); + assert_eq!(current_key(&checked), before); + } +} + +#[test] +fn reflected_field_scope_accepts_a_direct_lexical_descendant_of_its_original_loop() { + for release in [false, true] { + let (program, mut checked, parent) = execution(release, "nested_scope"); + let original_loop = loop_at(function(&program, "nested_scope"), 0); + let outer_binding = field_scope(&original_loop.body); + let element_binding = field_scope(&outer_binding.body); + let prepared_loop = checked + .prepare_reflected_field_loop(original_loop) + .unwrap() + .unwrap(); + let parent_key = current_key(&checked); + for index in 0..prepared_loop.fields.len() { + let iteration = prepared_loop.iteration(index).unwrap(); + let outer = checked.prepare_direct_scope(outer_binding).unwrap(); + checked.install_direct_scope(&outer).unwrap(); + let outer_key = current_key(&checked); + assert_ne!(outer_key, parent_key); + let element = checked + .prepare_reflected_field_scope(element_binding, &iteration) + .unwrap(); + assert_eq!(current_key(&checked), outer_key); + assert!(std::ptr::eq(element.body().unwrap(), &element_binding.body)); + assert_eq!( + element.reflection(), + &prepared_loop.fields[index].1.type_info + ); + assert_eq!(element.reference.cursor.scopes.len(), 2); + assert_eq!( + element.reference.cursor.scopes[1].selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!( + element.reference.cursor.scopes[1].bound_type, + prepared_loop.fields[index].0 + ); + checked.install_direct_scope(&element).unwrap(); + assert_ne!(current_key(&checked), outer_key); + checked.install_function_body(&parent).unwrap(); + assert_eq!(current_key(&checked), parent_key); + } + } +} + +fn assert_runtime_has_no_resource_effects(interpreter: &mut Interpreter) { + // No script is installed by these pure Source tests. Type-level metadata + // does not create a Network grant or permit a runtime Resource operation. + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +#[test] +fn reflected_field_runtime_uses_retained_metadata_and_retires_each_ordinal_entry_scope() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + // An ambient table cannot supply the retained owner's field metadata. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + assert_runtime_has_no_resource_effects(&mut interpreter); + for (name, expected) in [ + ("reflected", Value::Nothing), + ("two_loops", Value::Nothing), + ( + "reflected_labels", + Value::String("int64|int64|app.Count|".to_string()), + ), + ( + "two_loop_labels", + Value::String("int64|int64|app.Count|int64|int64|app.Count|".to_string()), + ), + ( + "reflected_labels", + Value::String("int64|int64|app.Count|".to_string()), + ), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + expected, + "{name} release={release}" + ); + // The entry observation includes checked reflected selector depth. + // Equality to the fresh-entry baseline proves it returns to zero. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn reflected_field_callable_frames_select_their_own_loops_and_restore_caller_aliases() { + const EXPECTED: &str = concat!( + "int64>int64|int64|app.Count|>int64|", + "int64>int64|int64|app.Count|>int64|", + "app.Count>int64|int64|app.Count|>app.Count|", + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for (name, expected) in [ + ("helper_inside_loop_labels", EXPECTED), + ("helper_inside_loop_labels", EXPECTED), + ("reflected_labels", "int64|int64|app.Count|"), + ] { + let definition = checked + .declaration_definition(function(&program, name).name.span) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::String(expected.to_string()), + "{name} release={release}" + ); + // The helper uses the same field/Element spellings as its caller. + // Its three labels prove its own ordinals were selected; the outer + // labels before and after the call prove caller alias restoration. + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn primitive_enum_and_opaque_resource_field_metadata_are_empty_without_a_provider() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked + .declaration_definition(function(&program, "empty_fields").name.span) + .unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for _ in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(0), + "release={release}" + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_runtime_has_no_resource_effects(&mut interpreter); + } + } +} + +#[test] +fn closed_pure_reflected_labels_are_captured_by_actual_required_evaluation() { + // Preserve every byte of SOURCE and append one required occurrence after + // its pure callee. This is the real required worker, not a runtime entry. + let source = format!( + "{SOURCE}{}", + r#" +export function required_labels() returns string: + return comptime reflected_labels() +"# + ); + for release in [false, true] { + let program = crate::resource_execution::tests::program(&source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "release={release}: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let expected = Value::String("int64|int64|app.Count|".to_string()); + assert_eq!(required.values.len(), 1); + assert_eq!( + required.values.values().cloned().collect::>(), + vec![expected.clone()] + ); + assert!(required.values.checked_values_are_mirrored()); + + // The worker receives no runtime provider, capability argument or + // live Resource value. Its capture API exposes values and debug events, + // not provider observations; do not invent an internal state assertion. + assert!( + required + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + let proofs = required.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("exactly one authenticated required occurrence"); + }; + let original = function(&program, "required_labels"); + let [Stmt::Return(statement)] = original.body.stmts.as_slice() else { + panic!("one original return containing the explicit comptime call"); + }; + let expression = statement.value.as_ref().unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + assert_eq!(proof.value(), &expected); + assert!(proof.belongs_to(&program)); + assert!(proof.is_explicit_comptime()); + assert!(proof.matches_original_comptime(expression)); + assert_eq!( + proof.owner(), + crate::CheckedRequiredOwner::Function { + definition: checked.declaration_definition(original.name.span).unwrap(), + declaration: original.name.span, + } + ); + assert!(proof.checked_hook(&program).unwrap().is_none()); + } +} + +// Original checked Sources exercise signals from selected field bodies. + +mod selected_field_control_flow { + use super::*; + + const CASES: &[(&str, &str, i64)] = &[ + ( + "return_from_second_selected_field", + include_str!("control_flow/01_reflected_return.jett"), + 12, + ), + ( + "break_from_second_selected_field", + include_str!("control_flow/02_reflected_break.jett"), + 18_927, + ), + ( + "continue_from_second_selected_field", + include_str!("control_flow/03_reflected_continue.jett"), + 18_923_897, + ), + ( + "default_then_resume_selected_alias_body", + include_str!("control_flow/04_reflected_default.jett"), + 1_193, + ), + ]; + + fn run_original_source(name: &str, source: &str, expected: i64, release: bool) { + // This helper retains actual checked SourceOrigin and Resource kernel + // declarations through the existing synthetic Support/program path. + let program = crate::resource_execution::tests::program(source, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked + .declaration_definition(function(&program, "scenario").name.span) + .unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + // The checked intrinsic must use retained metadata, not an ambient map. + interpreter.set_reflection_metadata(Arc::new(ReflectionMetadata::new())); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for attempt in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(expected), + "{name} release={release} attempt={attempt}" + ); + // This observation includes private reflected selector depth. + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + baseline, + "{name} release={release} attempt={attempt}: entry state changed" + ); + // No provider script or grant is installed for these pure controls. + assert_eq!( + interpreter.resource_test_observations(), + Err(ResourceExecutionError::ProviderDisabled.to_string()), + "{name} release={release} attempt={attempt}: provider became enabled" + ); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap(), + (0, 0), + "{name} release={release} attempt={attempt}: custody remained" + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{name} release={release} attempt={attempt}: unexpected debug events" + ); + } + } + + #[test] + fn reflected_field_return_break_continue_and_default_execute_original_sources() { + for release in [false, true] { + for &(name, source, expected) in CASES { + run_original_source(name, source, expected, release); + } + } + } +} + +#[test] +fn ordinary_field_reflection_wrappers_rejoin_original_frames_and_ordinal_tables() { + for release in [false, true] { + let (program, mut checked, parent) = execution(release, "two_loops"); + let first = loop_at(function(&program, "two_loops"), 0); + let second = loop_at(function(&program, "two_loops"), 1); + let binding = field_scope(&first.body); + let before = current_key(&checked); + let proof = checked + .prepare_field_reflected_loop(first) + .unwrap() + .unwrap(); + assert!( + checked + .prepare_field_reflected_loop(&first.clone()) + .is_err() + ); + assert!(proof.iteration(proof.fields.len()).is_err()); + for index in 0..proof.fields.len() { + let iteration = proof.iteration(index).unwrap(); + let scope = checked + .prepare_field_reflected_scope(binding, &iteration) + .unwrap(); + assert_eq!( + scope.reference.cursor.scopes.last().unwrap().selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + checked + .install_field_function_body(&scope.field_reference()) + .unwrap(); + assert!( + checked + .prepare_field_reflected_scope(binding, &iteration) + .is_err() + ); + checked.install_field_function_body(&parent).unwrap(); + } + let iteration = proof.iteration(0).unwrap(); + assert!( + checked + .prepare_field_reflected_scope(&binding.clone(), &iteration) + .is_err() + ); + assert!( + checked + .prepare_field_reflected_scope(field_scope(&second.body), &iteration) + .is_err() + ); + let mut swapped = copy_loop(&proof); + swapped.fields.swap(0, 1); // Same types, different immutable ordinal identity. + let swapped = Arc::new(swapped).iteration(0).unwrap(); + assert!( + checked + .prepare_field_reflected_scope(binding, &swapped) + .is_err() + ); + let wrong_item = + Interpreter::reflection_field_info_value(&proof.owner_name(), None, &proof.fields[1].1); + assert!(iteration.validate_value(&wrong_item).is_err()); + let (foreign_program, foreign, _) = execution(release, "two_loops"); + let foreign_binding = + field_scope(&loop_at(function(&foreign_program, "two_loops"), 0).body); + assert!( + foreign + .prepare_field_reflected_scope(foreign_binding, &iteration) + .is_err() + ); + assert_eq!(current_key(&checked), before); + install_entry(&mut checked, &program, "other"); + assert!( + checked + .prepare_field_reflected_scope(binding, &iteration) + .is_err() + ); + } +} + +#[test] +fn checked_scope_without_lexical_writes_preserves_external_field_call_identity() { + let source = r#"namespace app +struct Record: + value: int64 +function update(mutable record: Record) returns Record: + record.value = 7 + return record +export function scoped_call() returns int64: + Record original = Record(value: 1) + comptime type Number = type.info[int64](): + Record changed = update(original) + return changed.value +"#; + for release in [false, true] { + let program = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, FileId::new(0)), + std::collections::HashMap::from([( + FileId::new(0), + jett_common::SourceOrigin::Project, + )]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .expect("genuine checked ordinary-only Source"), + ); + assert!(program.checked().resource_hooks.is_empty()); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_checked_expression_types(Arc::new( + crate::checked_types::CheckedExpressionTypes::from_checked_program(&program), + )); + interpreter.set_reflection_metadata(program.checked().reflection_metadata.clone()); + assert_eq!( + interpreter + .call_checked_function_in_namespace(Some("app"), "scoped_call", vec![]) + .unwrap(), + Value::Int64(7) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs new file mode 100644 index 00000000..9ec3daf0 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required.rs @@ -0,0 +1,677 @@ +//! Required workers select original retained nodes and complete typed contexts. +//! Legacy type-name projections are data only; they cannot mint this identity. + +use super::*; +use crate::checked_types::CheckedFunctionTypes; +use crate::value::ClosureScopedTypeBinding; +use std::hash::{Hash, Hasher}; + +pub use value_proof::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, +}; +#[path = "required/value_proof.rs"] +mod value_proof; + +#[path = "required/actor_owners.rs"] +mod actor_owners; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct AttemptNode { + origin: ExecutableOrigin, + path: Vec, + generic: Option, + actor: Option, + scopes: Vec<(Span, usize, TypeId, Option, ReflectionTypeInfo)>, +} + +/// Private program-bound key; its program Arc keeps numeric/path identity live. +#[derive(Debug, Clone)] +pub(crate) struct CheckedAttemptKey { + program: Arc, + node: AttemptNode, +} + +impl CheckedAttemptKey { + /// A selected lexical scope retains every concrete ancestor selection. + /// Another program, callable, generic instance or sibling is never an ancestor. + pub(super) fn is_lexical_descendant_of(&self, parent: &Self) -> bool { + Arc::ptr_eq(&self.program, &parent.program) + && self.node.origin == parent.node.origin + && self.node.generic == parent.node.generic + && self.node.actor == parent.node.actor + && self + .node + .path + .strip_prefix(parent.node.path.as_slice()) + .is_some_and(|suffix| { + !suffix + .iter() + .any(|step| matches!(step, RegionStep::InlineBody(_))) + }) + && self.node.scopes.starts_with(&parent.node.scopes) + } +} + +impl PartialEq for CheckedAttemptKey { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.program, &other.program) && self.node == other.node + } +} +impl Eq for CheckedAttemptKey {} +impl Hash for CheckedAttemptKey { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.program).hash(state); + self.node.hash(state); + } +} + +#[derive(Clone)] +pub(crate) struct PreparedRequiredExpression { + reference: CheckedBodyReference, + purpose: ExecutionPurpose, + key: CheckedAttemptKey, + result_type: TypeId, + function: Option>, + scope: Option>, + bindings: Vec, +} + +impl PreparedRequiredExpression { + pub(crate) fn purpose(&self) -> ExecutionPurpose { + self.purpose + } + pub(crate) fn key(&self) -> CheckedAttemptKey { + self.key.clone() + } + pub(crate) fn result_type(&self) -> TypeId { + self.result_type + } + pub(crate) fn checked_program(&self) -> &Arc { + &self.key.program + } + pub(crate) fn function_projection(&self) -> Option> { + self.function.clone() + } + pub(crate) fn scope_projection(&self) -> Option> { + self.scope.clone() + } + pub(crate) fn bindings(&self) -> Vec { + self.bindings.clone() + } + pub(crate) fn reference(&self) -> &CheckedBodyReference { + &self.reference + } + pub(crate) fn expression(&self) -> Result<&Expr, ResourceExecutionError> { + match self.reference.region()? { + OriginalRegion::Expression(expression) => Ok(expression), + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } +} + +impl CheckedExecution { + pub(super) fn attempt_key( + &self, + cursor: &CheckedBodyCursor, + ) -> Result { + self.validate_executable_cursor(cursor)?; + self.attempt_key_after_identity(cursor) + } + + pub(super) fn field_attempt_key( + &self, + cursor: &CheckedBodyCursor, + ) -> Result { + self.validate_field_executable_cursor(cursor)?; + self.attempt_key_after_identity(cursor) + } + + fn attempt_key_after_identity( + &self, + cursor: &CheckedBodyCursor, + ) -> Result { + let executable = cursor + .executable + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + Ok(CheckedAttemptKey { + program: self.program.clone(), + node: AttemptNode { + origin: executable.origin.clone(), + path: executable.path.clone(), + generic: match cursor.root { + BodyRoot::Ordinary => None, + BodyRoot::Generic(index) => Some(index), + BodyRoot::ActorHandler(_) => None, + }, + actor: match cursor.root { + BodyRoot::ActorHandler(target) => Some(target), + _ => None, + }, + scopes: cursor + .scopes + .iter() + .map(|selection| { + ( + selection.owner, + selection.index, + selection.bound_type, + match selection.selection { + CheckedComptimeTypeSelection::Unconditional => None, + CheckedComptimeTypeSelection::ReflectedIteration(index) => { + Some(index) + } + }, + selection.reflection.clone(), + ) + }) + .collect(), + }, + }) + } + + fn required_expression( + &self, + mut cursor: CheckedBodyCursor, + expression: &Expr, + bindings: Vec, + ) -> Result { + self.validate_required_source_cursor(&cursor)?; + self.validate_actor_required_expression(&cursor, expression)?; + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + let index = expression_index(reference.region()?, expression)?; + cursor + .executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Expression(index)); + self.validate_required_source_cursor(&cursor)?; + let key = self.attempt_key_after_identity(&cursor)?; + let result_type = *self + .facts(&cursor)? + .types + .get(&expression.span()) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let function = match cursor.root { + BodyRoot::Ordinary => None, + BodyRoot::Generic(index) => Some(function_projection(self, index)?), + BodyRoot::ActorHandler(_) => None, + }; + let scope = if cursor.scopes.is_empty() { + match cursor.root { + BodyRoot::ActorHandler(target) => Some(self.required_actor_projection(target)?), + BodyRoot::Ordinary | BodyRoot::Generic(_) => None, + } + } else { + let mut scopes = match cursor.root { + BodyRoot::Ordinary => &self.program.checked().comptime_type_bindings, + BodyRoot::Generic(index) => { + &self.program.checked().generic_function_instantiations[index] + .comptime_type_bindings + } + BodyRoot::ActorHandler(target) => { + &self + .program + .checked() + .actor_handler(target) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .body() + .comptime_type_bindings + } + }; + let mut selected = None; + for selection in &cursor.scopes { + let candidate = scopes + .get(&selection.owner) + .and_then(|values| values.get(selection.index)) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + selected = Some(scoped_projection( + candidate, + &self.program.checked().interner, + )); + scopes = &candidate.body.comptime_type_bindings; + } + selected + }; + Ok(PreparedRequiredExpression { + reference: CheckedBodyReference { cursor }, + purpose: self.purpose, + key, + result_type, + function, + scope, + bindings, + }) + } + + pub(crate) fn prepare_namespace_initializer( + &self, + declaration: &jett_parser::ast::VarDecl, + ) -> Result { + if self.purpose != ExecutionPurpose::NamespaceConstant { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::VarDecl(original) if std::ptr::eq(original, declaration) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if !self + .program + .checked() + .type_map + .contains_key(&declaration.name.span) + { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let cursor = CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::NamespaceInitializer { item: *item }, + path: Vec::new(), + }), + }; + let expression = match &declaration.value { + Expr::Comptime(value, _) => value.as_ref(), + value => value, + }; + self.required_expression(cursor, expression, Vec::new()) + } + + pub(crate) fn prepare_verify_body( + &self, + original: &jett_parser::ast::VerifyBlock, + ) -> Result { + if self.purpose != ExecutionPurpose::Verify { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Verify(value) if std::ptr::eq(value, original) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.required_body(ExecutableOrigin::Verify { item: *item }) + } + + pub(crate) fn prepare_property_body( + &self, + original: &jett_parser::ast::PropertyBlock, + ) -> Result { + if self.purpose != ExecutionPurpose::Property { + return Err(ResourceExecutionError::WrongPurpose); + } + let items = self + .program + .module() + .items + .iter() + .enumerate() + .filter_map(|(index, item)| match item { + Item::Property(value) if std::ptr::eq(value, original) => Some(index), + _ => None, + }) + .collect::>(); + let [item] = items.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.required_body(ExecutableOrigin::Property { item: *item }) + } + + fn required_body( + &self, + origin: ExecutableOrigin, + ) -> Result { + let reference = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin, + path: Vec::new(), + }), + }, + }; + self.validate_executable_cursor(&reference.cursor)?; + Ok(reference) + } + + pub(crate) fn prepare_explicit_contexts( + &self, + expression: &Expr, + occurrence: Span, + owner: Option, + bindings: &[(Span, String)], + ) -> Result, ResourceExecutionError> { + if self.purpose != ExecutionPurpose::ExplicitComptime { + return Err(ResourceExecutionError::WrongPurpose); + } + let mut bases = Vec::new(); + let mut source_is_original = if owner.is_none() { + self.actor_explicit_bases(expression, occurrence, bindings, &mut bases)? + } else { + false + }; + for (item, original) in self.program.module().items.iter().enumerate() { + let origin = match original { + Item::Function(function) if owner == Some(function.name.span) => { + let definition = self.declaration_definition(function.name.span)?; + ExecutableOrigin::Function { definition, item } + } + Item::VarDecl(_) if owner.is_none() => { + ExecutableOrigin::NamespaceInitializer { item } + } + Item::Verify(_) if owner.is_none() => ExecutableOrigin::Verify { item }, + Item::Property(_) if owner.is_none() => ExecutableOrigin::Property { item }, + Item::TypeAlias(alias) if owner.is_none() && alias.constraint.is_some() => { + ExecutableOrigin::RefinementPredicate { + definition: self.refinement_definition(alias)?, + item, + } + } + _ => continue, + }; + // Authenticate syntax independently of concrete execution eligibility. + // This structural cursor is never installed or used to obtain facts. + let structural = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin, + path: Vec::new(), + }), + }, + }; + let paths = + explicit_region_paths(structural.region()?, expression, occurrence, bindings)?; + if paths.is_empty() { + continue; + } + let [path] = paths.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + source_is_original = true; + let roots = match (origin, original) { + (ExecutableOrigin::Function { definition, .. }, Item::Function(function)) + if !function.type_params.is_empty() => + { + self.program + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, body)| body.definition == definition) + .map(|(index, _)| BodyRoot::Generic(index)) + .collect::>() + } + _ => vec![BodyRoot::Ordinary], + }; + for root in roots { + bases.push(( + CheckedBodyCursor { + root, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: origin.clone(), + path: Vec::new(), + }), + }, + path.clone(), + )); + } + } + let mut prepared = Vec::new(); + for (base, path) in bases { + let mut contexts = vec![(base, Vec::new())]; + for step in path { + let mut expanded = Vec::new(); + for (cursor, prior_bindings) in contexts { + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + match step { + RegionStep::InlineBody(index) => { + region_inline_body(reference.region()?, index)?; + let mut next = cursor; + next.executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::InlineBody(index)); + self.validate_required_source_cursor(&next)?; + expanded.push((next, prior_bindings)); + } + RegionStep::Scope(owner) => { + let binding = region_binding(reference.region()?, owner)?; + let Some(candidates) = self.facts(&cursor)?.scopes.get(&owner) else { + continue; + }; + for (index, candidate) in candidates.iter().enumerate() { + let mut next = cursor.clone(); + next.scopes.push(ScopedSelection { + owner, + index, + bound_type: candidate.bound_type, + selection: candidate.selection, + reflection: candidate.reflection.clone(), + }); + next.executable + .as_mut() + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .path + .push(RegionStep::Scope(owner)); + self.validate_required_source_cursor(&next)?; + let mut projected = prior_bindings.clone(); + projected.push(ClosureScopedTypeBinding { + name: binding.name.name.clone(), + canonical_name: self + .program + .checked() + .interner + .type_name(candidate.bound_type), + reflection: Some(candidate.reflection.clone()), + }); + expanded.push((next, projected)); + } + } + RegionStep::Expression(_) => { + return Err(ResourceExecutionError::MissingCheckedBody); + } + } + } + contexts = expanded; + } + for (cursor, projected) in contexts { + if self.facts(&cursor)?.types.contains_key(&occurrence) { + prepared.push(self.required_expression(cursor, expression, projected)?); + } + } + } + if !source_is_original { + return Err(ResourceExecutionError::MissingCheckedBody); + } + // A retained original expression with no accepted concrete/scoped type + // occurrence has no evaluation attempt, as on the ordinary route. + // Eligible contexts still require exact packets when they execute. + Ok(prepared) + } + + pub(crate) fn original_assignment_key( + &self, + assignment: &AssignStmt, + ) -> Result { + self.original_assignment(assignment)?; + self.attempt_key(&self.body) + } + + pub(super) fn field_body_key(&self) -> Result { + self.field_attempt_key(&self.body) + } + + pub(crate) fn original_comptime_key( + &self, + expression: &Expr, + ) -> Result { + let Expr::Comptime(inner, span) = expression else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.validate_executable_cursor(&self.body)?; + let reference = CheckedBodyReference { + cursor: self.body.clone(), + }; + expression_index(reference.region()?, expression)?; + if !self.facts(&self.body)?.types.contains_key(span) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(self + .required_expression(self.body.clone(), inner, Vec::new())? + .key) + } +} + +/// Select pure required syntax without making callable bodies part of the +/// ordinary executable walker. Every path is to one physical retained node. +fn explicit_region_paths( + region: OriginalRegion<'_>, + expression: &Expr, + occurrence: Span, + bindings: &[(Span, String)], +) -> Result>, ResourceExecutionError> { + let mut scopes = Vec::new(); + let mut inline = Vec::new(); + let mut owns = false; + walk_region( + region, + &mut |scope| scopes.push(scope), + &mut |node| { + if let Expr::Comptime(inner, span) = node { + owns |= *span == occurrence && std::ptr::eq(inner.as_ref(), expression); + } + if matches!(node, Expr::InlineFn(..)) { + inline.push(node); + } + }, + &mut |_| {}, + ); + let mut paths = Vec::new(); + if owns && bindings.is_empty() { + paths.push(Vec::new()); + } + if let Some(((owner, name), rest)) = bindings.split_first() { + for scope in scopes { + if scope.span != *owner { + continue; + } + if scope.name.name != *name { + return Err(ResourceExecutionError::MissingCheckedBody); + } + for mut path in explicit_region_paths( + OriginalRegion::Block(&scope.body), + expression, + occurrence, + rest, + )? { + path.insert(0, RegionStep::Scope(*owner)); + paths.push(path); + } + } + } + for node in inline { + let index = expression_index(region, node)?; + let body = region_inline_body(region, index)?; + for mut path in explicit_region_paths( + OriginalRegion::Block(body), + expression, + occurrence, + bindings, + )? { + path.insert(0, RegionStep::InlineBody(index)); + paths.push(path); + } + } + Ok(paths) +} + +fn function_projection( + checked: &CheckedExecution, + index: usize, +) -> Result, ResourceExecutionError> { + let body = checked + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let types = &checked.program.checked().interner; + let mut expressions = body + .type_map + .iter() + .map(|(span, ty)| (*span, types.type_name(*ty))) + .collect(); + remove_descendant_expression_names(&mut expressions, &body.comptime_type_bindings); + Ok(Arc::new(CheckedFunctionTypes { + bindings: body + .comptime_type_bindings + .iter() + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|candidate| scoped_projection(candidate, types)) + .collect(), + ) + }) + .collect(), + type_arguments: body + .concrete_args + .iter() + .map(|ty| types.type_name(*ty)) + .collect(), + type_argument_reflections: body.specialization.type_argument_reflections.clone(), + type_info_kinds: body.specialization.type_info_kinds.clone(), + type_info_primitives: body.specialization.type_info_primitives.clone(), + type_kind_values: body.specialization.type_kind_values.clone(), + type_primitive_values: body.specialization.type_primitive_values.clone(), + expressions: Arc::new(expressions), + })) +} + +#[cfg(test)] +#[path = "required/tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "required/owner_tests.rs"] +mod owner_tests; + +#[cfg(test)] +#[path = "required/actor_owner_tests.rs"] +mod actor_owner_tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owner_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owner_tests.rs new file mode 100644 index 00000000..d323c232 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owner_tests.rs @@ -0,0 +1,578 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_typecheck::CheckOptions; +use jett_types::TypeInterner; + +const WITNESS: &str = include_str!("../../../fixtures/actor_handler_required_callable.jett"); + +fn checked(source: &str, release: bool) -> Arc { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!( + !parsed + .errors + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "{:?}", + parsed.errors + ); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +fn required(program: &Arc) -> crate::ExplicitComptimeEvaluation { + crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + program.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes::from_checked_program(program)), + Arc::new(program.checked().breakpoint_exclusions.clone()), + ) +} + +fn actor<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a jett_parser::ast::ActorDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Actor(actor) if actor.name.name == name => Some(actor), + _ => None, + }) + .unwrap() +} + +fn occurrence(handler: &jett_parser::ast::ReceiveHandler) -> (&Expr, Span) { + let mut found = Vec::new(); + walk_region( + OriginalRegion::Block(&handler.body), + &mut |_| {}, + &mut |node| { + if let Expr::Comptime(expression, span) = node { + found.push((expression.as_ref(), *span)); + } + }, + &mut |_| {}, + ); + let [value] = found.as_slice() else { + panic!("one original required actor occurrence") + }; + *value +} + +#[test] +fn actor_required_witness_retains_exact_successful_cache_and_callable() { + for release in [false, true] { + let program = checked(WITNESS, release); + assert!(program.checked().resource_hooks.is_empty()); + let evaluated = required(&program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + assert_eq!(evaluated.values.len(), 1); + assert!( + evaluated + .values + .same_checked_cache(&evaluated.values.clone()) + ); + let proofs = evaluated.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one privately cached actor required value") + }; + let actor = actor(&program, "Worker"); + let handler = &actor.handlers[0]; + let (original, span) = occurrence(handler); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let contexts = checked + .prepare_explicit_contexts(original, span, None, &[]) + .unwrap(); + let [context] = contexts.as_slice() else { + panic!("one exact actor required context") + }; + let BodyRoot::ActorHandler(target) = context.reference.cursor.root else { + panic!("actor metadata root") + }; + assert_eq!( + proof.owner(), + CheckedRequiredOwner::ActorHandler { + target, + declaration: handler.name.span + } + ); + assert_eq!(proof.source_span(), span); + assert!(proof.generic_index().is_none()); + assert!(proof.type_arguments().is_empty()); + assert!(proof.scoped_bindings().is_empty()); + assert!(proof.original_inline_function().unwrap().is_none()); + assert!( + matches!(proof.original_comptime().unwrap(), Expr::Comptime(inner, occurrence) + if *occurrence == span && std::ptr::eq(inner.as_ref(), original)) + ); + assert!(!proof.matches_original_comptime(&proof.original_comptime().unwrap().clone())); + let crate::Value::ReflectedCallable(callable) = proof.value().payload() else { + panic!("actual lazily adapted reflected callback") + }; + assert!(Arc::ptr_eq(callable.checked_program(), &program)); + assert_ne!(callable.source_type(), callable.requested_type()); + let definition = callable.source_definition().unwrap(); + assert_eq!( + program.resolved().scope_table.def(definition).name, + "app.five" + ); + assert_eq!( + program.checked().definition_types.get(&definition), + Some(&callable.source_type()) + ); + assert!(std::ptr::eq(context.expression().unwrap(), original)); + assert_eq!(context.result_type(), callable.requested_type()); + assert_eq!( + checked.validate_executable_cursor(&context.reference.cursor), + Err(ResourceExecutionError::WrongPurpose) + ); + } +} + +#[test] +fn actor_required_initializers_keep_distinct_source_owners() { + const SOURCE: &str = "namespace app +function five() returns int64: + return 5 +actor Counter: + mutable int64 first = comptime five() + mutable int64 second = comptime five() + receive read responds int64: + respond first + second +function main() returns nothing: + Counter counter = spawn Counter() + int64 value = ask counter.read + return nothing +"; + for release in [false, true] { + let program = checked(SOURCE, release); + let evaluated = required(&program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + let proofs = evaluated.values.checked_required_values(&program).unwrap(); + assert_eq!(proofs.len(), 2); + let counter = actor(&program, "Counter"); + let execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let item = program + .module() + .items + .iter() + .position( + |item| matches!(item, Item::Actor(original) if std::ptr::eq(original, counter)), + ) + .unwrap(); + let definition = execution.required_actor_declaration(item).unwrap().0; + for (field, initializer) in counter.state_fields.iter().enumerate() { + let proof = proofs + .iter() + .find(|proof| { + proof.owner() + == CheckedRequiredOwner::ActorInitializer { + definition, + declaration: initializer.name.span, + field, + } + }) + .unwrap(); + assert_eq!(proof.value(), &crate::Value::Int64(5)); + assert!(proof.matches_original_comptime(&initializer.value)); + assert!(!proof.matches_original_comptime(&counter.state_fields[1 - field].value)); + assert!(proof.original_inline_function().unwrap().is_none()); + } + } +} + +#[test] +fn actor_required_metadata_refuses_changed_owner_target_clone_foreign_and_runtime_entry() { + const SOURCE: &str = "namespace app +function five() returns int64: + return 5 +actor Worker: + mutable int64 count = 0 + receive first responds int64: + int64 value = comptime five() + respond value + receive second responds int64: + int64 value = comptime five() + respond value +function main() returns nothing: + Worker worker = spawn Worker() + int64 first = ask worker.first + int64 second = ask worker.second + return nothing +"; + for release in [false, true] { + let program = checked(SOURCE, release); + let mut execution = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let original_actor = actor(&program, "Worker"); + let (expression, span) = occurrence(&original_actor.handlers[0]); + let first = execution + .prepare_explicit_contexts(expression, span, None, &[]) + .unwrap() + .remove(0); + let (sibling, sibling_span) = occurrence(&original_actor.handlers[1]); + let second = execution + .prepare_explicit_contexts(sibling, sibling_span, None, &[]) + .unwrap() + .remove(0); + assert_ne!(first.key(), second.key()); + assert!(!first.key().is_lexical_descendant_of(&second.key())); + assert!(!second.key().is_lexical_descendant_of(&first.key())); + assert!( + execution + .prepare_explicit_contexts(&expression.clone(), span, None, &[]) + .is_err() + ); + assert!( + execution + .prepare_explicit_contexts(expression, sibling_span, None, &[]) + .is_err() + ); + assert!( + execution + .prepare_explicit_contexts(expression, span, Some(original_actor.name.span), &[]) + .is_err() + ); + let foreign = + CheckedExecution::new(checked(SOURCE, release), ExecutionPurpose::ExplicitComptime) + .unwrap(); + assert!( + foreign + .prepare_explicit_contexts(expression, span, None, &[]) + .is_err() + ); + assert_eq!( + foreign.validate_required_source_cursor(&first.reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + let mut wrong = first.reference.cursor.clone(); + let BodyRoot::ActorHandler(target) = &mut wrong.root else { + panic!("actor metadata root") + }; + target.handler_index = 1; + assert!(execution.validate_required_source_cursor(&wrong).is_err()); + let mut wrong = first.reference.cursor.clone(); + let BodyRoot::ActorHandler(target) = &mut wrong.root else { + panic!("actor metadata root") + }; + target.actor_definition = program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| { + definition.kind == jett_resolve::DefKind::Function && definition.name == "app.five" + }) + .unwrap() + .id; + assert!(execution.validate_required_source_cursor(&wrong).is_err()); + let mut wrong = first.reference.cursor.clone(); + let ExecutableOrigin::ActorHandler { handler, .. } = + &mut wrong.executable.as_mut().unwrap().origin + else { + panic!("actor source origin") + }; + *handler = usize::MAX; + assert!(execution.validate_required_source_cursor(&wrong).is_err()); + let mut wrong = first.reference.cursor.clone(); + wrong.scopes.push(ScopedSelection { + owner: span, + index: 0, + bound_type: TypeInterner::INT64, + selection: CheckedComptimeTypeSelection::Unconditional, + reflection: ReflectionTypeInfo { + type_name: "int64".into(), + kind: "primitive".into(), + primitive_tag: Some("int64".into()), + has_secret: false, + args: Vec::new(), + }, + }); + assert!(execution.validate_required_source_cursor(&wrong).is_err()); + let mut base = first.reference.cursor.clone(); + base.executable.as_mut().unwrap().path.clear(); + let Stmt::Respond(response) = &original_actor.handlers[0].body.stmts[1] else { + panic!("original runtime response") + }; + assert!( + execution + .required_expression(base.clone(), &response.value, Vec::new()) + .is_err() + ); + let mut wrong = first.reference.cursor.clone(); + *wrong.executable.as_mut().unwrap().path.last_mut().unwrap() = + RegionStep::Expression(usize::MAX); + assert!(execution.validate_required_source_cursor(&wrong).is_err()); + assert_eq!( + execution.install_function_body(&first.reference), + Err(ResourceExecutionError::WrongPurpose) + ); + assert_eq!( + execution.validate_executable_cursor(&base), + Err(ResourceExecutionError::WrongPurpose) + ); + assert!(execution.cursor().executable.is_none()); + execution.replace_purpose(ExecutionPurpose::ReferenceRuntime); + assert_eq!( + execution.validate_required_source_cursor(&first.reference.cursor), + Err(ResourceExecutionError::WrongPurpose) + ); + assert!( + execution + .prepare_explicit_contexts(expression, span, None, &[]) + .is_err() + ); + assert!(execution.cursor().executable.is_none()); + } +} + +#[test] +fn actor_required_closedness_rejects_free_state_and_capability() { + const FREE_STATE: &str = "namespace app +actor Worker: + mutable int64 count = 0 + receive read responds int64: + int64 value = comptime count + respond value +"; + const FREE_CAPABILITY: &str = "namespace app +function read(view net: Network) returns int64: + return 5 +actor Worker(net: Network): + receive value responds int64: + int64 answer = comptime read(view net) + respond answer +"; + for release in [false, true] { + let state = checked(FREE_STATE, release); + let evaluated = required(&state); + assert_eq!( + evaluated.diagnostics.len(), + 1, + "{:?}", + evaluated.diagnostics + ); + assert_eq!(evaluated.diagnostics[0].code.code(), 9001); + assert!( + evaluated.diagnostics[0] + .message + .contains("must be closed and evaluable") + ); + assert!(evaluated.diagnostics[0].message.contains("count")); + assert!(evaluated.values.is_empty()); + assert!(evaluated.debug_events.is_empty()); + let parsed = jett_parser::parse(FREE_CAPABILITY, FileId::new(0)); + assert!( + !parsed + .errors + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "{:?}", + parsed.errors + ); + let rejected = CheckedResourceProgram::prepare( + parsed, + HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .expect_err("comptime cannot call a capability-requiring function"); + assert!( + rejected.diagnostics().is_some_and(|diagnostics| diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 504)), + "{:?}", + rejected.diagnostics() + ); + } +} + +#[test] +fn actor_required_scopes_select_handler_facts() { + const SOURCE: &str = "namespace app +actor Worker: + receive read responds string: + comptime type Bound = type.info[int64](): + string value = comptime type.name[Bound]() + respond value +function main() returns nothing: + Worker worker = spawn Worker() + string value = ask worker.read + return nothing +"; + for release in [false, true] { + let program = checked(SOURCE, release); + let evaluated = required(&program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + let proofs = evaluated.values.checked_required_values(&program).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one scoped actor required value") + }; + assert_eq!(proof.value(), &crate::Value::String("int64".into())); + assert_eq!(proof.scoped_bindings().len(), 1); + assert_eq!(proof.scoped_bindings()[0].name(), "Bound"); + assert_eq!(proof.scoped_bindings()[0].bound_type(), TypeInterner::INT64); + assert!(proof.generic_index().is_none()); + assert!(matches!( + proof.owner(), + CheckedRequiredOwner::ActorHandler { .. } + )); + } +} + +#[test] +fn actor_required_returned_closure_rejects_unavailable_actor_state_capture() { + const SOURCE: &str = include_str!("../../../fixtures/actor_required_free_capture.jett"); + for release in [false, true] { + let program = checked(SOURCE, release); + let evaluated = required(&program); + assert_eq!( + evaluated.diagnostics.len(), + 1, + "{:?}", + evaluated.diagnostics + ); + assert_eq!(evaluated.diagnostics[0].code.code(), 9001); + assert!(evaluated.diagnostics[0].message.contains("count")); + assert!( + evaluated.values.is_empty(), + "unclosed closure must not be reusable" + ); + assert!( + evaluated + .values + .checked_required_values(&program) + .unwrap() + .is_empty() + ); + assert!(evaluated.debug_events.is_empty()); + } +} + +#[test] +fn actor_required_closed_closures_keep_named_constant_local_parameter_and_nested_captures() { + const SOURCE: &str = include_str!("../../../fixtures/actor_required_closed_captures.jett"); + for release in [false, true] { + let program = checked(SOURCE, release); + let evaluated = required(&program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + let proofs = evaluated.values.checked_required_values(&program).unwrap(); + let actor_values = proofs + .iter() + .filter(|proof| matches!(proof.owner(), CheckedRequiredOwner::ActorHandler { .. })) + .collect::>(); + assert_eq!(actor_values.len(), 4); + for handler in 0..4 { + let proof = actor_values + .iter() + .find(|proof| { + matches!(proof.owner(), CheckedRequiredOwner::ActorHandler { target, .. } + if target.handler_index == handler) + }) + .unwrap(); + match handler { + 0 => { + let crate::Value::Function { + captures, params, .. + } = proof.value().payload() + else { + panic!("own-parameter closure must remain a real checked value"); + }; + assert_eq!(params.len(), 1); + assert_eq!(params[0].name.name, "delta"); + assert!(!captures.contains_key("delta")); + assert!(!captures.contains_key("local")); + assert!(!captures.contains_key("bias")); + assert!(!captures.contains_key("app.bias")); + assert!(!captures.contains_key("five")); + assert!(!captures.contains_key("app.five")); + } + 1 | 2 => { + let crate::Value::Function { captures, .. } = proof.value().payload() else { + panic!("pure worker must retain the actual available captures"); + }; + let seed = if handler == 1 { 3 } else { 4 }; + assert_eq!( + captures.get("seed").map(crate::Value::payload), + Some(&crate::Value::Int64(seed)) + ); + assert_eq!( + captures.get("local").map(crate::Value::payload), + Some(&crate::Value::Int64(seed + 1)) + ); + } + 3 => assert_eq!(proof.value().payload(), &crate::Value::Int64(29)), + _ => unreachable!(), + } + } + } +} + +#[test] +fn actor_required_free_captures_refuse_same_name_constant_parameter_and_nested_body() { + const SOURCES: &[(&str, &str)] = &[ + ( + include_str!("../../../fixtures/actor_required_free_capture_same_constant.jett"), + "count", + ), + ( + include_str!("../../../fixtures/actor_required_free_parameter_capture.jett"), + "value", + ), + ( + include_str!("../../../fixtures/actor_required_nested_free_capture.jett"), + "count", + ), + ]; + for release in [false, true] { + for (source, missing) in SOURCES { + let program = checked(source, release); + let evaluated = required(&program); + assert_eq!( + evaluated.diagnostics.len(), + 1, + "{:?}", + evaluated.diagnostics + ); + assert_eq!(evaluated.diagnostics[0].code.code(), 9001); + assert!(evaluated.diagnostics[0].message.contains(missing)); + assert!(evaluated.debug_events.is_empty()); + // The same-name namespace constant may have its own valid primitive + // entry. It must never manufacture a successful actor closure entry. + let proofs = evaluated.values.checked_required_values(&program).unwrap(); + assert!(proofs.iter().all(|proof| { + !matches!(proof.owner(), CheckedRequiredOwner::ActorHandler { .. }) + })); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owners.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owners.rs new file mode 100644 index 00000000..46aa320e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/actor_owners.rs @@ -0,0 +1,212 @@ +//! Pure required-value selection is metadata, never actor Resource execution. +use super::*; +use jett_typecheck::CheckedActorHandlerTarget; +use jett_types::ActorId; + +impl CheckedExecution { + /// Only explicit required workers may project actor facts. This intentionally + /// does not change validate_executable_cursor or install_function_body: a + /// runtime actor handler still cannot install a Resource execution cursor. + pub(super) fn validate_required_source_cursor( + &self, + cursor: &CheckedBodyCursor, + ) -> Result<(), ResourceExecutionError> { + if cursor.executable.as_ref().is_some_and(|identity| { + matches!( + identity.origin, + ExecutableOrigin::ActorInitializer { .. } | ExecutableOrigin::ActorHandler { .. } + ) + }) { + if self.purpose != ExecutionPurpose::ExplicitComptime { + return Err(ResourceExecutionError::WrongPurpose); + } + self.validate_field_executable_cursor(cursor) + } else { + self.validate_executable_cursor(cursor) + } + } + + pub(super) fn validate_actor_required_expression( + &self, + cursor: &CheckedBodyCursor, + expression: &Expr, + ) -> Result<(), ResourceExecutionError> { + let actor_origin = cursor.executable.as_ref().is_some_and(|identity| { + matches!( + identity.origin, + ExecutableOrigin::ActorInitializer { .. } | ExecutableOrigin::ActorHandler { .. } + ) + }); + if !actor_origin { + return Ok(()); + } + self.validate_required_source_cursor(cursor)?; + let reference = CheckedBodyReference { + cursor: cursor.clone(), + }; + let mut occurrences = Vec::new(); + walk_region( + reference.region()?, + &mut |_| {}, + &mut |node| { + if let Expr::Comptime(inner, span) = node { + if std::ptr::eq(inner.as_ref(), expression) { + occurrences.push(*span); + } + } + }, + &mut |_| {}, + ); + let [occurrence] = occurrences.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if !self.facts(cursor)?.types.contains_key(occurrence) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok(()) + } + + pub(super) fn required_actor_declaration( + &self, + item: usize, + ) -> Result<(DefId, ActorId), ResourceExecutionError> { + let Some(Item::Actor(actor)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let declarations = self + .program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Actor + && definition.span == actor.name.span + }) + .collect::>(); + let [definition] = declarations.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let ty = self + .program + .checked() + .definition_types + .get(&definition.id) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let Type::Actor(actor_id) = self.program.checked().interner.resolve(*ty) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let origin = + CheckedActorInstanceOrigin::from_checked_actor(self.program.clone(), *actor_id)?; + if !std::ptr::eq(origin.original()?, actor) { + return Err(ResourceExecutionError::MissingCheckedBody); + } + Ok((definition.id, *actor_id)) + } + + pub(super) fn required_actor_projection( + &self, + target: CheckedActorHandlerTarget, + ) -> Result, ResourceExecutionError> { + let handler = self + .program + .checked() + .actor_handler(target) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + let types = &self.program.checked().interner; + let (expressions, bindings) = crate::checked_types::original_body_types( + &handler.body().type_map, + &handler.body().comptime_type_bindings, + types, + ); + Ok(Arc::new(CheckedScopedTypes { + bound_type: types.resolve_actor(target.actor).name.clone(), + reflection: None, + expressions, + bindings, + })) + } + + /// Authenticate the physical original occurrence before selecting a checked + /// actor ordinal. Equal values, names, spans or caller AST clones confer no key. + pub(super) fn actor_explicit_bases( + &self, + expression: &Expr, + occurrence: Span, + bindings: &[(Span, String)], + bases: &mut Vec<(CheckedBodyCursor, Vec)>, + ) -> Result { + if self.purpose != ExecutionPurpose::ExplicitComptime { + return Err(ResourceExecutionError::WrongPurpose); + } + let mut original = false; + for (item, node) in self.program.module().items.iter().enumerate() { + let Item::Actor(actor) = node else { continue }; + for (field, initializer) in actor.state_fields.iter().enumerate() { + let paths = explicit_region_paths( + OriginalRegion::Expression(&initializer.value), + expression, + occurrence, + bindings, + )?; + if paths.is_empty() { + continue; + } + let [path] = paths.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + self.required_actor_declaration(item)?; + let cursor = CheckedBodyCursor { + root: BodyRoot::Ordinary, + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::ActorInitializer { item, field }, + path: Vec::new(), + }), + }; + self.validate_required_source_cursor(&cursor)?; + bases.push((cursor, path.clone())); + original = true; + } + for (handler_index, handler) in actor.handlers.iter().enumerate() { + let paths = explicit_region_paths( + OriginalRegion::Block(&handler.body), + expression, + occurrence, + bindings, + )?; + if paths.is_empty() { + continue; + } + let [path] = paths.as_slice() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let (definition, actor_id) = self.required_actor_declaration(item)?; + let target = CheckedActorHandlerTarget { + actor_definition: definition, + actor: actor_id, + handler_index, + }; + self.validate_actor_handler_identity(target, item)?; + let cursor = CheckedBodyCursor { + root: BodyRoot::ActorHandler(target), + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: ExecutableOrigin::ActorHandler { + definition, + item, + handler: handler_index, + }, + path: Vec::new(), + }), + }; + self.validate_required_source_cursor(&cursor)?; + bases.push((cursor, path.clone())); + original = true; + } + } + Ok(original) + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/owner_tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/owner_tests.rs new file mode 100644 index 00000000..1023a699 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/owner_tests.rs @@ -0,0 +1,477 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::resource_execution::tests::program as checked_program; +use crate::{Interpreter, Value}; + +const SOURCE: &str = "namespace app +function factory[T]() returns function() returns string: + return function() returns string: return comptime type.name[T]() +function scoped[T]() returns function() returns string: + comptime type Bound = type.info[T](): + return function() returns string: return comptime type.name[Bound]() +type Positive = int64 where value > 0 and (comptime type.name[int64]()) == \"int64\" +export function scenario() returns nothing: + function() returns string first = factory[int8]() + function() returns string second = factory[string]() + function() returns string third = scoped[int8]() + function() returns string fourth = scoped[string]() + return nothing +"; + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(value) if value.name.name == name => Some(value), + _ => None, + }) + .unwrap() +} + +fn inline_comptime(block: &Block) -> (&Expr, &Expr, Span) { + let Some(Stmt::Return(ret)) = block.stmts.first() else { + panic!("original inline return") + }; + let Some(inline @ Expr::InlineFn(_, _, body, _)) = ret.value.as_ref() else { + panic!("original inline body") + }; + let Some(Stmt::Return(ret)) = body.stmts.first() else { + panic!("original required return") + }; + let Some(Expr::Comptime(expression, occurrence)) = ret.value.as_ref() else { + panic!("original required occurrence") + }; + (inline, expression, *occurrence) +} + +fn expression_types(program: &Arc) -> Arc { + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }) +} + +#[test] +fn required_inline_owner_keeps_original_node_concrete_generic_and_scope_keys() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let factory = function(&program, "factory"); + let (inline, expression, occurrence) = inline_comptime(&factory.body); + let contexts = checked + .prepare_explicit_contexts(expression, occurrence, Some(factory.name.span), &[]) + .unwrap(); + assert_eq!(contexts.len(), 2); + assert_ne!(contexts[0].key(), contexts[1].key()); + let mut enclosing_key = contexts[0].key(); + enclosing_key.node.path.clear(); + assert!(!contexts[0].key().is_lexical_descendant_of(&enclosing_key)); + assert!( + contexts[0] + .key() + .is_lexical_descendant_of(&contexts[0].key()) + ); + assert!( + !contexts[0] + .key() + .is_lexical_descendant_of(&contexts[1].key()) + ); + for context in &contexts { + assert!(std::ptr::eq(context.expression().unwrap(), expression)); + assert_eq!(context.reference.cursor.scopes.len(), 0); + let parent = context.reference.cursor.executable.as_ref().unwrap(); + let Some(RegionStep::InlineBody(index)) = parent.path.first() else { + panic!("private inline region step") + }; + let root = CheckedBodyReference { + cursor: CheckedBodyCursor { + root: context.reference.cursor.root.clone(), + scopes: Vec::new(), + executable: Some(ExecutableIdentity { + program: program.clone(), + origin: parent.origin, + path: Vec::new(), + }), + }, + }; + assert!(std::ptr::eq( + region_expression(root.region().unwrap(), *index).unwrap(), + inline + )); + } + let scoped = function(&program, "scoped"); + let Some(Stmt::ComptimeTypeBind(binding)) = scoped.body.stmts.first() else { + panic!("original scope") + }; + let (_, expression, occurrence) = inline_comptime(&binding.body); + let scoped_contexts = checked + .prepare_explicit_contexts( + expression, + occurrence, + Some(scoped.name.span), + &[(binding.span, binding.name.name.clone())], + ) + .unwrap(); + assert_eq!(scoped_contexts.len(), 2); + assert_ne!(scoped_contexts[0].key(), scoped_contexts[1].key()); + for context in scoped_contexts { + assert_eq!(context.bindings.len(), 1); + assert!(matches!( + context + .reference + .cursor + .executable + .as_ref() + .unwrap() + .path + .as_slice(), + [ + RegionStep::Scope(_), + RegionStep::InlineBody(_), + RegionStep::Expression(_) + ] + )); + } + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types(&program), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + let proofs = required.values.checked_required_values(&program).unwrap(); + let inline_proofs = proofs + .iter() + .filter(|proof| proof.original_inline_function().unwrap().is_some()) + .collect::>(); + assert_eq!(inline_proofs.len(), 4); + assert!(inline_proofs.iter().all(|proof| matches!(proof.value(), Value::String(value) if value == "int8" || value == "string"))); + } +} + +#[test] +fn required_inline_owner_refuses_clones_foreign_owner_generic_and_runtime_entry() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let factory = function(&program, "factory"); + let (_, expression, occurrence) = inline_comptime(&factory.body); + let contexts = checked + .prepare_explicit_contexts(expression, occurrence, Some(factory.name.span), &[]) + .unwrap(); + let original_cursor = checked.cursor(); + assert!(matches!( + checked.prepare_explicit_contexts( + &expression.clone(), + occurrence, + Some(factory.name.span), + &[] + ), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert!(matches!( + checked.prepare_explicit_contexts( + expression, + occurrence, + Some(function(&program, "scenario").name.span), + &[] + ), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let foreign = CheckedExecution::new( + checked_program(SOURCE, release), + ExecutionPurpose::ExplicitComptime, + ) + .unwrap(); + assert!(matches!( + foreign.prepare_explicit_contexts(expression, occurrence, Some(factory.name.span), &[]), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert_eq!( + foreign.validate_executable_cursor(&contexts[0].reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + let mut wrong = contexts[0].reference.cursor.clone(); + wrong.executable.as_mut().unwrap().path[0] = RegionStep::InlineBody(usize::MAX); + assert_eq!( + checked.validate_executable_cursor(&wrong), + Err(ResourceExecutionError::MissingCheckedBody) + ); + let other_definition = checked + .declaration_definition(function(&program, "scoped").name.span) + .unwrap(); + let other_index = program + .checked() + .generic_function_instantiations + .iter() + .position(|instance| instance.definition == other_definition) + .unwrap(); + let mut wrong = contexts[0].reference.cursor.clone(); + wrong.root = BodyRoot::Generic(other_index); + assert_eq!( + checked.validate_executable_cursor(&wrong), + Err(ResourceExecutionError::MissingCheckedBody) + ); + assert_eq!( + checked.attempt_key(&original_cursor).err(), + Some(ResourceExecutionError::MissingCheckedBody) + ); + assert!(checked.cursor().executable.is_none()); + checked.replace_purpose(ExecutionPurpose::ReferenceRuntime); + assert_eq!( + checked.validate_executable_cursor(&contexts[0].reference.cursor), + Err(ResourceExecutionError::WrongPurpose) + ); + assert!(matches!( + checked.prepare_explicit_contexts(expression, occurrence, Some(factory.name.span), &[]), + Err(ResourceExecutionError::WrongPurpose) + )); + } +} + +#[test] +fn required_refinement_owner_is_exact_retained_constraint_and_provider_free() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let alias = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::TypeAlias(alias) if alias.name.name == "Positive" => Some(alias), + _ => None, + }) + .unwrap(); + let mut occurrence = None; + walk_region( + OriginalRegion::Expression(alias.constraint.as_ref().unwrap()), + &mut |_| {}, + &mut |node| { + if let Expr::Comptime(expression, span) = node { + occurrence = Some((expression.as_ref(), *span)); + } + }, + &mut |_| {}, + ); + let (expression, span) = occurrence.unwrap(); + let contexts = checked + .prepare_explicit_contexts(expression, span, None, &[]) + .unwrap(); + assert_eq!(contexts.len(), 1); + assert!(std::ptr::eq(contexts[0].expression().unwrap(), expression)); + assert!(matches!( + checked.prepare_explicit_contexts(&expression.clone(), span, None, &[]), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types(&program), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + let proof = required + .values + .checked_required_values(&program) + .unwrap() + .into_iter() + .find(|proof| proof.source_span() == span) + .unwrap(); + assert_eq!( + proof.owner(), + CheckedRequiredOwner::RefinementPredicate { + definition: checked.refinement_definition(alias).unwrap(), + declaration: alias.name.span + } + ); + assert_eq!(proof.value(), &Value::String("int64".into())); + assert!(proof.original_inline_function().unwrap().is_none()); + let mut wrong = contexts[0].reference.cursor.clone(); + let ExecutableOrigin::RefinementPredicate { definition, .. } = + &mut wrong.executable.as_mut().unwrap().origin + else { + panic!("retained refinement owner") + }; + *definition = checked + .declaration_definition(function(&program, "scenario").name.span) + .unwrap(); + assert_eq!( + checked.validate_executable_cursor(&wrong), + Err(ResourceExecutionError::MissingCheckedBody) + ); + checked.replace_purpose(ExecutionPurpose::Verify); + assert_eq!( + checked.validate_executable_cursor(&contexts[0].reference.cursor), + Err(ResourceExecutionError::WrongPurpose) + ); + assert!(checked.cursor().executable.is_none()); + } +} + +#[test] +fn required_inline_scoped_attempt_restores_metadata_before_foreign_refusal_and_reentry() { + const FAILURE_SOURCE: &str = "function required_failure[T]() returns nothing: + string bound = type.name[T]() + resource_probe.terminal_failure() + return nothing +function scoped_failure[T]() returns function() returns nothing: + comptime type Bound = type.info[T](): + return function() returns nothing: return comptime required_failure[Bound]() +function select_failure_contexts() returns nothing: + function() returns nothing first = scoped_failure[int8]() + function() returns nothing second = scoped_failure[string]() + return nothing +"; + let source = format!("{SOURCE}{FAILURE_SOURCE}"); + for release in [false, true] { + let program = checked_program(&source, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let scoped = function(&program, "scoped"); + let Some(Stmt::ComptimeTypeBind(binding)) = scoped.body.stmts.first() else { + panic!("original scope") + }; + let (_, expression, occurrence) = inline_comptime(&binding.body); + let contexts = checked + .prepare_explicit_contexts( + expression, + occurrence, + Some(scoped.name.span), + &[(binding.span, binding.name.name.clone())], + ) + .unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ExplicitComptime, + ) + .unwrap(); + interpreter + .authorize_checked_resource_worker(program.module(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + let baseline = interpreter.resource_test_entry_context().unwrap(); + let foreign_program = checked_program(&source, release); + let foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + let foreign_function = function(&foreign_program, "scoped"); + let Some(Stmt::ComptimeTypeBind(foreign_binding)) = foreign_function.body.stmts.first() + else { + panic!("foreign scope") + }; + let (_, foreign_expression, foreign_occurrence) = inline_comptime(&foreign_binding.body); + let foreign_context = foreign + .prepare_explicit_contexts( + foreign_expression, + foreign_occurrence, + Some(foreign_function.name.span), + &[(foreign_binding.span, foreign_binding.name.name.clone())], + ) + .unwrap(); + assert!( + interpreter + .eval_checked_required_expression( + &foreign_context[0], + Some("app"), + &HashMap::new(), + &["T".into()] + ) + .is_err() + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + let failing = function(&program, "scoped_failure"); + let Some(Stmt::ComptimeTypeBind(failure_binding)) = failing.body.stmts.first() else { + panic!("original failing scope") + }; + let (_, failure_expression, failure_occurrence) = inline_comptime(&failure_binding.body); + let failure_contexts = checked + .prepare_explicit_contexts( + failure_expression, + failure_occurrence, + Some(failing.name.span), + &[(failure_binding.span, failure_binding.name.name.clone())], + ) + .unwrap(); + assert_eq!(failure_contexts.len(), contexts.len()); + let mut observed = Vec::new(); + for (failure, context) in failure_contexts.iter().zip(&contexts) { + assert_eq!(failure.bindings.len(), 1); + assert!(failure.function.is_some()); + assert!(failure.scope.is_some()); + assert!(Arc::ptr_eq(failure.checked_program(), &program)); + assert!(std::ptr::eq( + failure.expression().unwrap(), + failure_expression + )); + assert!(matches!( + failure + .reference + .cursor + .executable + .as_ref() + .unwrap() + .path + .as_slice(), + [ + RegionStep::Scope(_), + RegionStep::InlineBody(_), + RegionStep::Expression(_) + ] + )); + let error = interpreter + .eval_checked_required_expression( + failure, + Some("app"), + &HashMap::new(), + &["T".into()], + ) + .unwrap_err(); + // This ordinary Bounds error is produced only after the original + // required region and its typed generic/scoped metadata are entered. + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + observed.push( + interpreter + .eval_checked_required_expression( + context, + Some("app"), + &HashMap::new(), + &["T".into()], + ) + .unwrap(), + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + observed.sort_by_key(|value| match value { + Value::String(value) => value.clone(), + _ => panic!("required string"), + }); + assert_eq!( + observed, + vec![Value::String("int8".into()), Value::String("string".into())] + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs new file mode 100644 index 00000000..0a331feb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/tests.rs @@ -0,0 +1,238 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::resource_execution::tests::program as checked_program; +use crate::{Interpreter, Value}; + +const SOURCE: &str = include_str!("../../../fixtures/26_original_required_regions.jett"); + +fn types(program: &Arc) -> Arc { + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }) +} + +#[test] +fn resource_original_required_regions_preserve_existing_descriptor_absence_and_primitive_workers() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let types = types(&program); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + assert_eq!(required.values.len(), required.values.values().count()); + let blocks = crate::verify::run_verify_blocks_detailed_with_checked_values( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + Arc::new(required.values.clone()), + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "scenario" => Some(function), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let target = checked.declaration_definition(original.name.span).unwrap(); + // Match the driver: baked constants are installed before module registration. + let mut interpreter = Interpreter::new(); + interpreter.set_checked_expression_types(types); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(program.module()); + interpreter + .install_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn resource_required_reference_refuses_cloned_suite_initializer_and_wrong_purpose_without_installing() + { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let prepared = checked.prepare_namespace_initializer(declaration).unwrap(); + assert_eq!(prepared.purpose(), ExecutionPurpose::NamespaceConstant); + assert!(matches!( + checked.prepare_namespace_initializer(&declaration.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + checked.replace_purpose(ExecutionPurpose::Verify); + assert!(matches!( + checked.prepare_namespace_initializer(declaration), + Err(ResourceExecutionError::WrongPurpose) + )); + let verify = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Verify(value) => Some(value), + _ => None, + }) + .unwrap(); + let reference = checked.prepare_verify_body(verify).unwrap(); + assert!(std::ptr::eq(reference.block().unwrap(), &verify.body)); + assert!(matches!( + checked.prepare_verify_body(&verify.clone()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let foreign = + CheckedExecution::new(checked_program(SOURCE, release), ExecutionPurpose::Verify) + .unwrap(); + assert_eq!( + foreign.validate_executable_cursor(&reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + assert!(matches!( + foreign.attempt_key(&reference.cursor), + Err(ResourceExecutionError::ForeignProgram) + )); + } +} + +#[test] +fn resource_required_cache_key_keeps_exact_program_original_node_and_scope_identity() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::NamespaceConstant).unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let original = checked.prepare_namespace_initializer(declaration).unwrap(); + let foreign_program = checked_program(SOURCE, release); + let foreign = + CheckedExecution::new(foreign_program.clone(), ExecutionPurpose::NamespaceConstant) + .unwrap(); + let foreign_declaration = foreign_program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + let other = foreign + .prepare_namespace_initializer(foreign_declaration) + .unwrap(); + assert_ne!(original.key(), other.key()); + assert_eq!( + original.key(), + checked + .prepare_namespace_initializer(declaration) + .unwrap() + .key() + ); + let clone = original.expression().unwrap().clone(); + assert!(matches!( + checked.required_expression(original.reference.cursor.clone(), &clone, Vec::new()), + Err(ResourceExecutionError::MissingCheckedBody) + )); + } +} + +#[test] +fn resource_required_generic_and_nested_scopes_use_exact_selected_cache_contexts() { + for release in [false, true] { + let program = checked_program( + include_str!("../../../fixtures/27_original_generic_scoped_required.jett"), + release, + ); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types(&program), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + for name in ["generic_label", "scoped_label"] { + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .unwrap(); + let target = checked.declaration_definition(function.name.span).unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::String("int64".to_string()) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs new file mode 100644 index 00000000..cd7752e6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof.rs @@ -0,0 +1,586 @@ +//! Readonly proof of one successful required value in the private checked cache. +//! Source names, spans and mirrored values cannot construct this association. + +use super::*; +use crate::Value; +use crate::explicit::ExplicitComptimeValues; +use jett_typecheck::CheckedGenericSpecialization; +#[path = "value_proof/actor_hook.rs"] +mod actor_hook; +pub use actor_hook::{ActorGraphHookRoot, CheckedActorGraphHook}; + +/// The retained declaration that owns a successful required occurrence. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CheckedRequiredOwner { + Function { + definition: DefId, + declaration: Span, + }, + ActorInitializer { + definition: DefId, + declaration: Span, + field: usize, + }, + ActorHandler { + target: jett_typecheck::CheckedActorHandlerTarget, + declaration: Span, + }, + Verify { + declaration: Span, + }, + Property { + declaration: Span, + }, + NamespaceConstant { + declaration: Span, + }, + RefinementPredicate { + definition: DefId, + declaration: Span, + }, +} + +/// One exact selected scoped type context, in outer-to-inner source order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedRequiredScope { + name: String, + owner: Span, + index: usize, + bound_type: TypeId, + selection: CheckedComptimeTypeSelection, + reflection: ReflectionTypeInfo, +} + +impl CheckedRequiredScope { + pub fn name(&self) -> &str { + &self.name + } + pub fn owner(&self) -> Span { + self.owner + } + pub fn index(&self) -> usize { + self.index + } + pub fn bound_type(&self) -> TypeId { + self.bound_type + } + pub fn selection(&self) -> CheckedComptimeTypeSelection { + self.selection + } + pub fn reflection(&self) -> &ReflectionTypeInfo { + &self.reflection + } +} + +#[derive(Debug, Clone)] +struct RequiredValueSite { + source_span: Span, + owner: CheckedRequiredOwner, + type_arguments: Vec, + specialization: CheckedGenericSpecialization, + scopes: Vec, + explicit: bool, +} + +/// Opaque association between a retained required occurrence, complete checked +/// context, and its successful provider-free value. There is no public mint. +#[derive(Debug, Clone)] +pub struct CheckedRequiredValue { + key: CheckedAttemptKey, + value: Value, + actor_graph: Option>, + site: RequiredValueSite, +} + +impl CheckedRequiredValue { + pub fn checked_program(&self) -> &Arc { + &self.key.program + } + + pub(crate) fn from_cache( + cache: &ExplicitComptimeValues, + key: &CheckedAttemptKey, + program: &Arc, + ) -> Result { + if !Arc::ptr_eq(&key.program, program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let value = cache + .checked_get(key) + .ok_or_else(|| "required value has no successful checked cache entry".to_string())?; + if value.contains_live_resource_or_grant() { + return Err( + "required value contains runtime authority or Resource custody".to_string(), + ); + } + let site = key.value_site().map_err(|error| error.to_string())?; + let proof = Self { + key: key.clone(), + value: value.clone(), + actor_graph: cache.checked_actor_graph(key).cloned(), + site, + }; + // Hook metadata must belong to the same retained program as the worker + // occurrence. Its nominal identity and exact signature stay checked. + proof.checked_hook(program)?; + if proof + .actor_graph + .as_ref() + .is_some_and(|graph| !graph.belongs_to(program)) + { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + Ok(proof) + } + + pub fn value(&self) -> &Value { + &self.value + } + pub fn actor_graph(&self) -> Option<&Arc> { + self.actor_graph.as_ref() + } + pub fn belongs_to(&self, program: &Arc) -> bool { + Arc::ptr_eq(&self.key.program, program) + } + pub fn source_span(&self) -> Span { + self.site.source_span + } + pub fn owner(&self) -> CheckedRequiredOwner { + self.site.owner + } + pub fn generic_index(&self) -> Option { + self.key.node.generic + } + pub fn type_arguments(&self) -> &[TypeId] { + &self.site.type_arguments + } + pub fn specialization(&self) -> &CheckedGenericSpecialization { + &self.site.specialization + } + pub fn scoped_bindings(&self) -> &[CheckedRequiredScope] { + &self.site.scopes + } + pub fn is_explicit_comptime(&self) -> bool { + self.site.explicit + } + + /// Return the original outer `comptime` node retained by this proof. A raw + /// namespace initializer has a required value but no such outer node. + pub fn original_comptime(&self) -> Result<&Expr, String> { + self.key + .original_comptime() + .map_err(|error| error.to_string())? + .ok_or_else(|| { + "required namespace initializer has no explicit comptime node".to_string() + }) + } + + pub fn matches_original_comptime(&self, expression: &Expr) -> bool { + self.original_comptime() + .is_ok_and(|original| std::ptr::eq(original, expression)) + } + + /// Original nearest enclosing inline node, selected only by the private + /// required-region path in this retained checked program. + pub fn original_inline_function(&self) -> Result, String> { + let checked = + CheckedExecution::new(self.key.program.clone(), ExecutionPurpose::ExplicitComptime) + .map_err(|error| error.to_string())?; + checked + .validate_required_source_cursor(&self.key.cursor()) + .map_err(|error| error.to_string())?; + let Some((position, index)) = + self.key + .node + .path + .iter() + .enumerate() + .rev() + .find_map(|(position, step)| match step { + RegionStep::InlineBody(index) => Some((position, *index)), + _ => None, + }) + else { + return Ok(None); + }; + let region = self + .key + .region(&self.key.node.path[..position]) + .map_err(|error| error.to_string())?; + let expression = region_expression(region, index).map_err(|error| error.to_string())?; + if !matches!(expression, Expr::InlineFn(..)) { + return Err(ResourceExecutionError::MissingCheckedBody.to_string()); + } + Ok(Some(expression)) + } + + /// Project only an opaque hook descriptor bound to this exact checked + /// program. Reading the metadata does not invoke a runtime hook. + pub fn checked_hook<'a>( + &'a self, + program: &Arc, + ) -> Result, String> { + if !self.belongs_to(program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let Value::ResourceHook(descriptor) = self.value.payload() else { + return Ok(None); + }; + if !Arc::ptr_eq(&descriptor.program, &self.key.program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + descriptor + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .map(Some) + .ok_or_else(|| ResourceExecutionError::InvalidInvocation.to_string()) + } +} + +/// Hook projection of a successful checked required value. It remains metadata, +/// separate from a physical Resource owner or a runtime capability. +#[derive(Debug, Clone)] +pub struct CheckedRequiredResourceHook { + required: CheckedRequiredValue, +} + +impl CheckedRequiredResourceHook { + pub(crate) fn from_value(required: CheckedRequiredValue) -> Option { + matches!(required.value.payload(), Value::ResourceHook(_)).then_some(Self { required }) + } + pub fn value(&self) -> &Value { + self.required.value() + } + pub fn belongs_to(&self, program: &Arc) -> bool { + self.required.belongs_to(program) + } + pub fn source_span(&self) -> Span { + self.required.source_span() + } + pub fn owner(&self) -> CheckedRequiredOwner { + self.required.owner() + } + pub fn generic_index(&self) -> Option { + self.required.generic_index() + } + pub fn type_arguments(&self) -> &[TypeId] { + self.required.type_arguments() + } + pub fn specialization(&self) -> &CheckedGenericSpecialization { + self.required.specialization() + } + pub fn scoped_bindings(&self) -> &[CheckedRequiredScope] { + self.required.scoped_bindings() + } + pub fn is_explicit_comptime(&self) -> bool { + self.required.is_explicit_comptime() + } + pub fn original_comptime(&self) -> Result<&Expr, String> { + self.required.original_comptime() + } + pub fn matches_original_comptime(&self, expression: &Expr) -> bool { + self.required.matches_original_comptime(expression) + } + pub fn checked_hook<'a>( + &'a self, + program: &Arc, + ) -> Result<&'a CheckedResourceHook, String> { + self.required + .checked_hook(program)? + .ok_or_else(|| "required value is not a checked Resource hook descriptor".to_string()) + } +} + +impl CheckedAttemptKey { + fn cursor(&self) -> CheckedBodyCursor { + CheckedBodyCursor { + root: if let Some(target) = self.node.actor { + BodyRoot::ActorHandler(target) + } else { + match self.node.generic { + Some(index) => BodyRoot::Generic(index), + None => BodyRoot::Ordinary, + } + }, + scopes: self + .node + .scopes + .iter() + .map( + |(owner, index, bound_type, selection, reflection)| ScopedSelection { + owner: *owner, + index: *index, + bound_type: *bound_type, + selection: match selection { + Some(index) => CheckedComptimeTypeSelection::ReflectedIteration(*index), + None => CheckedComptimeTypeSelection::Unconditional, + }, + reflection: reflection.clone(), + }, + ) + .collect(), + executable: Some(ExecutableIdentity { + program: self.program.clone(), + origin: self.node.origin, + path: self.node.path.clone(), + }), + } + } + + fn region(&self, path: &[RegionStep]) -> Result, ResourceExecutionError> { + let mut region = match self.node.origin { + ExecutableOrigin::ActorInitializer { item, field } => { + let Some(Item::Actor(actor)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + OriginalRegion::Expression( + &actor + .state_fields + .get(field) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .value, + ) + } + ExecutableOrigin::ActorHandler { item, handler, .. } => { + let Some(Item::Actor(actor)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + OriginalRegion::Block( + &actor + .handlers + .get(handler) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .body, + ) + } + ExecutableOrigin::Function { item, .. } => { + match self.program.module().items.get(item) { + Some(Item::Function(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::NamespaceInitializer { item } => { + match self.program.module().items.get(item) { + Some(Item::VarDecl(value)) => OriginalRegion::Expression(&value.value), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::RefinementPredicate { item, .. } => { + match self.program.module().items.get(item) { + Some(Item::TypeAlias(alias)) => OriginalRegion::Expression( + alias + .constraint + .as_ref() + .ok_or(ResourceExecutionError::MissingCheckedBody)?, + ), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::Verify { item } => match self.program.module().items.get(item) { + Some(Item::Verify(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => match self.program.module().items.get(item) { + Some(Item::Property(value)) => OriginalRegion::Block(&value.body), + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + }; + for step in path { + region = match step { + RegionStep::Scope(owner) => { + OriginalRegion::Block(®ion_binding(region, *owner)?.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + RegionStep::InlineBody(index) => { + OriginalRegion::Block(region_inline_body(region, *index)?) + } + }; + } + Ok(region) + } + + fn original_comptime(&self) -> Result, ResourceExecutionError> { + let Some((RegionStep::Expression(index), parent)) = self.node.path.split_last() else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let region = self.region(parent)?; + let selected = region_expression(region, *index)?; + let mut original = Vec::new(); + walk_region( + region, + &mut |_| {}, + &mut |expression| { + if let Expr::Comptime(inner, _) = expression { + if std::ptr::eq(inner.as_ref(), selected) { + original.push(expression); + } + } + }, + &mut |_| {}, + ); + match original.as_slice() { + [original] => Ok(Some(*original)), + [] if matches!( + self.node.origin, + ExecutableOrigin::NamespaceInitializer { .. } + ) => + { + Ok(None) + } + _ => Err(ResourceExecutionError::MissingCheckedBody), + } + } + + fn value_site(&self) -> Result { + let checked = + CheckedExecution::new(self.program.clone(), ExecutionPurpose::ExplicitComptime)?; + checked.validate_required_source_cursor(&self.cursor())?; + let owner = match self.node.origin { + ExecutableOrigin::ActorInitializer { item, field } => { + let Some(Item::Actor(actor)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let initializer = actor + .state_fields + .get(field) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + CheckedRequiredOwner::ActorInitializer { + definition: checked.required_actor_declaration(item)?.0, + declaration: initializer.name.span, + field, + } + } + ExecutableOrigin::ActorHandler { item, handler, .. } => { + let Some(Item::Actor(actor)) = self.program.module().items.get(item) else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let BodyRoot::ActorHandler(target) = self.cursor().root else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + CheckedRequiredOwner::ActorHandler { + target, + declaration: actor + .handlers + .get(handler) + .ok_or(ResourceExecutionError::MissingCheckedBody)? + .name + .span, + } + } + ExecutableOrigin::Function { definition, item } => { + match self.program.module().items.get(item) { + Some(Item::Function(value)) => CheckedRequiredOwner::Function { + definition, + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::NamespaceInitializer { item } => { + match self.program.module().items.get(item) { + Some(Item::VarDecl(value)) => CheckedRequiredOwner::NamespaceConstant { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::RefinementPredicate { definition, item } => { + match self.program.module().items.get(item) { + Some(Item::TypeAlias(alias)) if alias.constraint.is_some() => { + CheckedRequiredOwner::RefinementPredicate { + definition, + declaration: alias.name.span, + } + } + _ => return Err(ResourceExecutionError::MissingCheckedBody), + } + } + ExecutableOrigin::Verify { item } => match self.program.module().items.get(item) { + Some(Item::Verify(value)) => CheckedRequiredOwner::Verify { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + ExecutableOrigin::Property { item } => match self.program.module().items.get(item) { + Some(Item::Property(value)) => CheckedRequiredOwner::Property { + declaration: value.name.span, + }, + _ => return Err(ResourceExecutionError::MissingCheckedBody), + }, + }; + let (type_arguments, specialization) = match self.node.generic { + Some(index) => { + let instance = self + .program + .checked() + .generic_function_instantiations + .get(index) + .ok_or(ResourceExecutionError::MissingCheckedBody)?; + ( + instance.concrete_args.clone(), + instance.specialization.clone(), + ) + } + None => (Vec::new(), CheckedGenericSpecialization::default()), + }; + let mut region = self.region(&[])?; + let mut selections = self.node.scopes.iter(); + let mut scopes = Vec::new(); + for step in &self.node.path { + region = match step { + RegionStep::Scope(owner) => { + let binding = region_binding(region, *owner)?; + let Some((selected_owner, index, bound_type, selection, reflection)) = + selections.next() + else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + if owner != selected_owner { + return Err(ResourceExecutionError::MissingCheckedBody); + } + scopes.push(CheckedRequiredScope { + name: binding.name.name.clone(), + owner: *owner, + index: *index, + bound_type: *bound_type, + selection: match selection { + Some(index) => CheckedComptimeTypeSelection::ReflectedIteration(*index), + None => CheckedComptimeTypeSelection::Unconditional, + }, + reflection: reflection.clone(), + }); + OriginalRegion::Block(&binding.body) + } + RegionStep::Expression(index) => { + OriginalRegion::Expression(region_expression(region, *index)?) + } + RegionStep::InlineBody(index) => { + OriginalRegion::Block(region_inline_body(region, *index)?) + } + }; + } + if selections.next().is_some() { + return Err(ResourceExecutionError::MissingCheckedBody); + } + let OriginalRegion::Expression(expression) = region else { + return Err(ResourceExecutionError::MissingCheckedBody); + }; + let original = self.original_comptime()?; + Ok(RequiredValueSite { + source_span: original.map_or_else(|| expression.span(), Expr::span), + owner, + type_arguments, + specialization, + scopes, + explicit: original.is_some(), + }) + } +} + +#[cfg(test)] +#[path = "value_proof/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/actor_hook.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/actor_hook.rs new file mode 100644 index 00000000..6e447028 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/actor_hook.rs @@ -0,0 +1,221 @@ +//! Exact metadata projection from the immutable, privately cached graph. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActorGraphHookRoot { + Root, + Field { node: usize, field: usize }, +} + +/// No public constructor. A caller value/name/type cannot become a producer. +#[derive(Debug, Clone)] +pub struct CheckedActorGraphHook { + required: CheckedRequiredValue, + root: ActorGraphHookRoot, + path: Vec, + signature: TypeId, +} + +fn children(value: &Value) -> Vec<&Value> { + match value { + Value::Typed { value, .. } + | Value::Pending(value) + | Value::OptionalSome(value) + | Value::ResultOk(value) + | Value::ResultFail(value) => vec![value], + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => values.iter().collect(), + Value::Map(values) => values + .iter() + .flat_map(|(key, value)| [key, value]) + .collect(), + Value::Struct { fields, .. } => fields.iter().map(|(_, value)| value).collect(), + Value::TypeConstruction { fields, .. } => { + fields.iter().map(|(_, _, _, value)| value).collect() + } + Value::Function { captures, .. } => { + let mut ordered = captures.iter().collect::>(); + ordered.sort_by(|a, b| a.0.cmp(b.0)); + ordered.into_iter().map(|(_, value)| value).collect() + } + Value::ReflectedCallable(adapter) => vec![adapter.source()], + _ => Vec::new(), + } +} +impl CheckedRequiredValue { + /// Enumerate only authenticated metadata leaves, retaining complete root paths. + pub fn actor_graph_hooks(&self) -> Result, String> { + let Some(graph) = self.actor_graph() else { + return Ok(Vec::new()); + }; + let roots = std::iter::once(graph.root()).chain(graph.nodes().iter().flat_map(|node| { + node.captures() + .iter() + .chain(node.state()) + .map(|field| field.value()) + })); + let mut result = Vec::new(); + for root in roots { + let mut work = vec![root]; + while let Some(value) = work.pop() { + if let Value::ResourceHook(descriptor) = value { + let hook = self + .key + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .ok_or("graph metadata leaf has no exact checked hook")?; + result.push( + self.project_actor_graph_hook(value, hook.function_type)? + .ok_or("graph metadata leaf lost its protected path")?, + ); + } + let descendants = children(value); + work.extend(descendants.into_iter().rev()); + } + } + Ok(result) + } + + /// Associate only the actual retained leaf address with this protected row. + /// The graph exporter has already checked every intermediate typed carrier. + pub fn project_actor_graph_hook( + &self, + value: &Value, + signature: TypeId, + ) -> Result, String> { + if !matches!(value, Value::ResourceHook(_)) { + return Ok(None); + } + let graph = self + .actor_graph() + .ok_or("hook projection has no original checked graph")?; + if !graph.belongs_to(&self.key.program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let mut roots = vec![(ActorGraphHookRoot::Root, graph.root())]; + for (node_index, node) in graph.nodes().iter().enumerate() { + for (field_index, field) in node.captures().iter().chain(node.state()).enumerate() { + roots.push(( + ActorGraphHookRoot::Field { + node: node_index, + field: field_index, + }, + field.value(), + )); + } + } + let mut found = None; + for (root, original) in roots { + let mut pending = vec![(original, Vec::new())]; + while let Some((candidate, path)) = pending.pop() { + if std::ptr::eq(candidate, value) { + if found.is_some() { + return Err("hook projection has ambiguous retained leaf identity".into()); + } + let projection = CheckedActorGraphHook { + required: self.clone(), + root, + path: path.clone(), + signature, + }; + projection.checked_hook(&self.key.program)?; + found = Some(projection); + } + for (index, child) in children(candidate).into_iter().enumerate() { + let mut child_path = path.clone(); + child_path.push(index); + pending.push((child, child_path)); + } + } + } + found.map(Some).ok_or_else(|| { + "hook projection is not an original leaf of this private required graph".into() + }) + } +} +impl CheckedActorGraphHook { + pub fn required(&self) -> &CheckedRequiredValue { + &self.required + } + pub fn root(&self) -> ActorGraphHookRoot { + self.root + } + pub fn path(&self) -> &[usize] { + &self.path + } + pub fn signature(&self) -> TypeId { + self.signature + } + pub fn belongs_to(&self, program: &Arc) -> bool { + self.required.belongs_to(program) + } + pub fn value(&self) -> Result<&Value, String> { + let graph = self + .required + .actor_graph() + .ok_or("projected hook lost its private graph")?; + let mut value = match self.root { + ActorGraphHookRoot::Root => graph.root(), + ActorGraphHookRoot::Field { node, field } => graph + .nodes() + .get(node) + .and_then(|node| node.captures().iter().chain(node.state()).nth(field)) + .map(|field| field.value()) + .ok_or("projected hook lost its exact constructor-owned field")?, + }; + for index in &self.path { + value = children(value) + .get(*index) + .copied() + .ok_or("projected hook changed its retained carrier/capture path")?; + } + Ok(value) + } + pub fn checked_hook<'a>( + &'a self, + program: &Arc, + ) -> Result<&'a CheckedResourceHook, String> { + if !self.belongs_to(program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let graph = self + .required + .actor_graph() + .ok_or("projected hook lost its private graph")?; + if !graph.belongs_to(program) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + let mut value = match self.root { + ActorGraphHookRoot::Root => graph.root(), + ActorGraphHookRoot::Field { node, field } => graph + .nodes() + .get(node) + .and_then(|node| node.captures().iter().chain(node.state()).nth(field)) + .map(|field| field.value()) + .ok_or("projected hook lost its exact constructor-owned field")?, + }; + for index in &self.path { + value = children(value) + .get(*index) + .copied() + .ok_or("projected hook changed its retained carrier/capture path")?; + } + let Value::ResourceHook(descriptor) = value else { + return Err("projected hook leaf changed value kind".into()); + }; + if !descriptor.matches_type(program, self.signature) { + return Err("projected hook changed exact same-program nominal signature".into()); + } + self.required + .key + .program + .checked() + .resource_hooks + .get(&descriptor.definition) + .ok_or_else(|| ResourceExecutionError::InvalidInvocation.to_string()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs new file mode 100644 index 00000000..6130e070 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/required/value_proof/tests.rs @@ -0,0 +1,695 @@ +use super::*; +use crate::checked_types::CheckedExpressionTypes; +use crate::explicit::ComptimeContext; +use crate::resource_execution::tests::program as checked_program; +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_parser::parse; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckOptions; +use jett_types::{ResourceHookKind, ResourceKernelRecipe, TypeInterner}; + +#[test] +fn actor_graph_hook_projection_retains_exact_cache_program_field_and_signature() { + let source = "namespace app\nactor Keeper:\n mutable list[function(resource_probe.TestHandle) returns nothing] hooks = list(resource_probe.close_descriptor())\n receive alive responds bool:\n respond true\nfunction main() returns nothing:\n Keeper keeper = comptime spawn Keeper()\n return nothing\n"; + let program = actor_program(source, false); + let values = successful(&program); + let proof = values.checked_required_values(&program).unwrap().remove(0); + let graph = proof.actor_graph().unwrap(); + let field = &graph.nodes()[0].state()[0]; + let Type::List(signature) = program.checked().interner.resolve(field.ty()) else { + panic!("typed hook list") + }; + let Value::List(items) = field.value() else { + panic!("frozen hook list") + }; + let projected = proof + .project_actor_graph_hook(&items[0], *signature) + .unwrap() + .unwrap(); + assert_eq!( + projected.root(), + ActorGraphHookRoot::Field { node: 0, field: 0 } + ); + assert_eq!(projected.path(), &[0]); + assert_eq!( + projected.checked_hook(&program).unwrap().kind, + ResourceHookKind::Close + ); + assert_eq!(projected.required().owner(), proof.owner()); + assert!( + proof + .project_actor_graph_hook(&items[0].clone(), *signature) + .is_err(), + "a caller clone cannot mint a protected leaf projection" + ); + assert!( + proof + .project_actor_graph_hook(&items[0], TypeInterner::BOOL) + .is_err() + ); + let foreign = actor_program(source, false); + assert!(projected.checked_hook(&foreign).is_err()); + let mut mirror = values.clone(); + mirror.insert( + proof.source_span(), + ComptimeContext::default(), + Value::Actor(999), + ); + assert_eq!( + projected.checked_hook(&program).unwrap().kind, + ResourceHookKind::Close + ); +} + +#[test] +fn actor_graph_initializer_uses_original_body_and_refuses_forged_actor_registration() { + let source = "namespace app\nactor Keeper:\n mutable function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor()\nfunction fresh() returns Keeper:\n return spawn Keeper()\nfunction main() returns nothing:\n Keeper keeper = comptime fresh()\n return nothing\n"; + let program = actor_program(source, false); + let values = successful(&program); + let graph = values + .checked_required_values(&program) + .unwrap() + .remove(0) + .actor_graph() + .unwrap() + .clone(); + assert_eq!(graph.nodes().len(), 1); + let mut interpreter = crate::Interpreter::new(); + interpreter + .install_checked_resource_program(program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.register_module(program.module()); + let mut changed = program + .module() + .items + .iter() + .find_map(|item| { + if let Item::Actor(actor) = item { + Some(actor.clone()) + } else { + None + } + }) + .unwrap(); + changed.state_fields[0].value = Expr::Nothing(changed.state_fields[0].value.span()); + interpreter.register_actor_in_namespace(Some("app"), &changed); + let definition = program + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| { + definition.kind == jett_resolve::DefKind::Function && definition.name == "app.fresh" + }) + .unwrap() + .id; + assert!( + interpreter + .call_checked_program_entry(definition, vec![]) + .unwrap_err() + .contains("no exact checked body") + ); + assert!(interpreter.take_debug_events().is_empty()); +} + +const SOURCE: &str = include_str!("../../../../fixtures/26_original_required_regions.jett"); +const GENERIC_SCOPED: &str = + include_str!("../../../../fixtures/27_original_generic_scoped_required.jett"); +const SHARED_SUPPORT: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/resource_probe.jett" +)); + +fn required(program: &Arc) -> crate::ExplicitComptimeEvaluation { + crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ) +} + +fn successful(program: &Arc) -> ExplicitComptimeValues { + let evaluated = required(program); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + evaluated.values +} + +fn actor_program(source: &str, release: bool) -> Arc { + shared_program_with_support( + source, + release, + include_str!("../../../../fixtures/resource_probe.jett"), + ) +} + +fn shared_program(source: &str, release: bool) -> Arc { + shared_program_with_support(source, release, SHARED_SUPPORT) +} + +fn shared_program_with_support( + source: &str, + release: bool, + support: &str, +) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let application = parse(source, project); + assert!( + !parsed + .errors + .iter() + .chain(&application.errors) + .any(|diagnostic| diagnostic.severity == Severity::Error) + ); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(application.module.items); + parsed.errors.extend(application.errors); + let kernels = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.to_string(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &kernels, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} + +#[test] +fn resource_required_value_proof_keeps_outer_node_owner_primitive_absence_and_hook() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), 5); + for value in &values { + assert!(value.belongs_to(&program)); + assert!(value.is_explicit_comptime()); + let original = value.original_comptime().unwrap(); + assert!(matches!(original, Expr::Comptime(_, _))); + assert_eq!(value.source_span(), original.span()); + assert!(value.matches_original_comptime(original)); + assert!(!value.matches_original_comptime(&original.clone())); + assert_eq!(value.generic_index(), None); + assert!(value.type_arguments().is_empty()); + assert_eq!( + value.specialization(), + &CheckedGenericSpecialization::default() + ); + assert!(value.scoped_bindings().is_empty()); + } + assert_eq!( + values + .iter() + .filter(|value| value.value() == &Value::Int64(7)) + .count(), + 3 + ); + assert_eq!( + values + .iter() + .filter(|value| value.value() == &Value::OptionalNone) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!( + value.owner(), + CheckedRequiredOwner::NamespaceConstant { .. } + )) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!(value.owner(), CheckedRequiredOwner::Verify { .. })) + .count(), + 1 + ); + assert_eq!( + values + .iter() + .filter(|value| matches!(value.owner(), CheckedRequiredOwner::Property { .. })) + .count(), + 1 + ); + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + assert_eq!(hooks.len(), 1); + let hook = &hooks[0]; + assert_eq!( + hook.checked_hook(&program).unwrap().kind, + ResourceHookKind::Close + ); + assert!(matches!(hook.value().payload(), Value::ResourceHook(_))); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(value) if value.name.name == "scenario" => Some(value), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + assert_eq!( + hook.owner(), + CheckedRequiredOwner::Function { + definition: checked.declaration_definition(function.name.span).unwrap(), + declaration: function.name.span, + } + ); + assert!(hook.matches_original_comptime(hook.original_comptime().unwrap())); + assert!(!hook.matches_original_comptime(&hook.original_comptime().unwrap().clone())); + } +} + +#[test] +fn resource_required_value_proof_distinguishes_raw_namespace_initializer() { + for release in [false, true] { + let source = SOURCE.replace("int64 baked = comptime seven()", "int64 baked = 7"); + let program = checked_program(&source, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + let constant = values + .iter() + .find(|value| { + matches!( + value.owner(), + CheckedRequiredOwner::NamespaceConstant { .. } + ) + }) + .unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) => Some(value), + _ => None, + }) + .unwrap(); + assert_eq!(constant.value(), &Value::Int64(7)); + assert_eq!( + constant.owner(), + CheckedRequiredOwner::NamespaceConstant { + declaration: declaration.name.span + } + ); + assert_eq!(constant.source_span(), declaration.value.span()); + assert!(!constant.is_explicit_comptime()); + assert!(constant.original_comptime().is_err()); + assert!(!constant.matches_original_comptime(&declaration.value)); + assert!(constant.checked_hook(&program).unwrap().is_none()); + assert!( + values + .iter() + .filter(|value| value.is_explicit_comptime()) + .all(|value| value.original_comptime().is_ok()) + ); + } +} + +#[test] +fn resource_required_value_proof_refuses_foreign_program_and_public_mirror_replacement() { + for release in [false, true] { + let program = checked_program(SOURCE, release); + let foreign = checked_program(SOURCE, release); + let mut cache = successful(&program); + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + let hook = &hooks[0]; + let checked_hook = hook.checked_hook(&program).unwrap(); + // Same source produces the same numeric definition and signature, but + // those observations cannot authenticate an independent program Arc. + assert_eq!( + foreign + .checked() + .resource_hooks + .get(&checked_hook.definition), + Some(checked_hook) + ); + assert!(!hook.belongs_to(&foreign)); + assert!(hook.checked_hook(&foreign).is_err()); + assert!(cache.checked_required_values(&foreign).is_err()); + assert!(cache.checked_resource_hook_values(&foreign).is_err()); + let foreign_cache = successful(&foreign); + let foreign_hook = foreign_cache + .checked_resource_hook_values(&foreign) + .unwrap() + .remove(0); + assert_eq!(foreign_hook.source_span(), hook.source_span()); + assert!(!hook.matches_original_comptime(foreign_hook.original_comptime().unwrap())); + assert!(foreign_hook.checked_hook(&program).is_err()); + cache.insert( + hook.source_span(), + ComptimeContext::default(), + Value::Int64(99), + ); + assert_eq!( + cache.get(hook.source_span(), &ComptimeContext::default()), + Some(&Value::Int64(99)) + ); + let retained = cache + .checked_resource_hook_values(&program) + .unwrap() + .remove(0); + assert_eq!(retained.checked_hook(&program).unwrap(), checked_hook); + assert!(matches!(retained.value().payload(), Value::ResourceHook(_))); + let mut mirror = ExplicitComptimeValues::default(); + mirror.insert( + hook.source_span(), + ComptimeContext::default(), + hook.value().clone(), + ); + assert_eq!(mirror.len(), 1); + assert!(mirror.checked_required_values(&program).unwrap().is_empty()); + assert!( + mirror + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + } +} + +#[test] +fn resource_required_value_proof_preserves_complete_generic_and_nested_scoped_context() { + for release in [false, true] { + let program = checked_program(GENERIC_SCOPED, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), 2); + let generic = values + .iter() + .find(|value| value.generic_index().is_some()) + .unwrap(); + let instance = + &program.checked().generic_function_instantiations[generic.generic_index().unwrap()]; + assert_eq!(generic.type_arguments(), instance.concrete_args); + assert_eq!(generic.specialization(), &instance.specialization); + assert!( + matches!(generic.owner(), CheckedRequiredOwner::Function { definition, .. } if definition == instance.definition) + ); + assert_eq!(generic.value(), &Value::String("int64".to_string())); + let mut changed_generic = generic.key.clone(); + changed_generic.node.generic = None; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_generic, &program).is_err()); + assert!(changed_generic.value_site().is_err()); + let scoped = values + .iter() + .find(|value| value.scoped_bindings().len() == 2) + .unwrap(); + assert_eq!( + scoped + .scoped_bindings() + .iter() + .map(CheckedRequiredScope::name) + .collect::>(), + ["Outer", "Element"] + ); + let mut bindings = &program.checked().comptime_type_bindings; + for scope in scoped.scoped_bindings() { + let selected = &bindings[&scope.owner()][scope.index()]; + assert_eq!(scope.bound_type(), selected.bound_type); + assert_eq!(scope.selection(), selected.selection); + assert_eq!(scope.reflection(), &selected.reflection); + bindings = &selected.body.comptime_type_bindings; + } + assert_eq!(scoped.value(), &Value::String("int64".to_string())); + let mut changed_scope = scoped.key.clone(); + changed_scope.node.scopes[1].3 = Some(0); + assert!(CheckedRequiredValue::from_cache(&cache, &changed_scope, &program).is_err()); + assert!(changed_scope.value_site().is_err()); + let mut changed_owner = scoped.key.clone(); + changed_owner.node.origin = generic.key.node.origin; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_owner, &program).is_err()); + assert!(changed_owner.value_site().is_err()); + let mut changed_occurrence = scoped.key.clone(); + let Some(RegionStep::Expression(index)) = changed_occurrence.node.path.last_mut() else { + panic!("required expression path"); + }; + *index += 1; + assert!(CheckedRequiredValue::from_cache(&cache, &changed_occurrence, &program).is_err()); + } +} + +#[test] +fn resource_required_value_proof_covers_actual_all_recipe_alias_relay_unused_and_pure_sources() { + let sources = [ + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett" + )), + Some(ResourceHookKind::Close), + 2, + Some(761), + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + )), + Some(ResourceHookKind::Construct), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + )), + Some(ResourceHookKind::BorrowOperation), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + )), + None, + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ( + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + )), + Some(ResourceHookKind::Close), + 1, + None, + ), + ]; + for release in [false, true] { + for (source, expected, expected_values, expected_scalar) in sources { + let program = shared_program(source, release); + let cache = successful(&program); + let values = cache.checked_required_values(&program).unwrap(); + assert_eq!(values.len(), expected_values); + let main = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "main" => Some(function), + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ExplicitComptime).unwrap(); + let owner = CheckedRequiredOwner::Function { + definition: checked.declaration_definition(main.name.span).unwrap(), + declaration: main.name.span, + }; + for value in &values { + assert!(value.belongs_to(&program)); + assert!(value.is_explicit_comptime()); + assert_eq!(value.owner(), owner); + let original = value.original_comptime().unwrap(); + assert!(matches!(original, Expr::Comptime(_, _))); + assert_eq!(value.source_span(), original.span()); + assert!(value.matches_original_comptime(original)); + assert!(!value.matches_original_comptime(&original.clone())); + } + let hooks = cache.checked_resource_hook_values(&program).unwrap(); + match expected { + Some(kind) => { + assert_eq!(hooks.len(), 1); + let hook = hooks[0].checked_hook(&program).unwrap(); + assert_eq!(hook.kind, kind); + let selected = values + .iter() + .filter(|value| value.checked_hook(&program).unwrap() == Some(hook)) + .collect::>(); + assert_eq!(selected.len(), 1); + assert!( + selected[0] + .matches_original_comptime(hooks[0].original_comptime().unwrap()) + ); + if let Some(expected_scalar) = expected_scalar { + let label = main + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::VarDecl(declaration) if declaration.name.name == "label" => { + Some(&declaration.value) + } + _ => None, + }) + .unwrap(); + let scalar = values + .iter() + .filter(|value| value.matches_original_comptime(label)) + .collect::>(); + assert_eq!(scalar.len(), 1); + assert_eq!(scalar[0].value(), &Value::Int64(expected_scalar)); + assert!(scalar[0].checked_hook(&program).unwrap().is_none()); + assert!(matches!( + label, + Expr::Comptime(inner, _) if matches!( + inner.as_ref(), Expr::IntLiteral(value, _) + if *value == i128::from(expected_scalar) + ) + )); + assert_ne!(scalar[0].source_span(), selected[0].source_span()); + assert!(!selected[0].matches_original_comptime(label)); + assert!( + !scalar[0].matches_original_comptime( + selected[0].original_comptime().unwrap() + ) + ); + } + } + None => { + assert_eq!(hooks.len(), 0); + assert_eq!(values[0].value(), &Value::OptionalNone); + assert!(values[0].checked_hook(&program).unwrap().is_none()); + } + } + } + } +} + +#[test] +fn resource_required_value_proof_cannot_mint_without_successful_checked_evaluation() { + const FAILED: &str = "namespace app\nexport function scenario() returns nothing:\n use resource_probe\n nothing failed = comptime resource_probe.terminal_failure()\n return nothing\n"; + for release in [false, true] { + let program = checked_program(FAILED, release); + let evaluated = required(&program); + assert!(!evaluated.diagnostics.is_empty()); + assert!( + evaluated + .values + .checked_required_values(&program) + .unwrap() + .is_empty() + ); + assert!( + evaluated + .values + .checked_resource_hook_values(&program) + .unwrap() + .is_empty() + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs b/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs new file mode 100644 index 00000000..14531f85 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/checked/body_reference/tests.rs @@ -0,0 +1,247 @@ +use super::*; +use jett_common::FileId; + +const SOURCE: &str = include_str!("../../fixtures/13_direct_nested_scoped_lifecycle.jett"); + +fn execution( + release: bool, +) -> ( + Arc, + CheckedExecution, + CheckedBodyReference, +) { + let program = crate::resource_execution::tests::program(SOURCE, release); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "scenario" + && function.name.span.file == FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + assert!(std::ptr::eq(reference.function().unwrap(), function)); + checked.install_function_body(&reference).unwrap(); + (program, checked, reference) +} + +fn first_binding(block: &Block) -> &ComptimeTypeBindStmt { + block + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::ComptimeTypeBind(binding) => Some(binding), + _ => None, + }) + .unwrap() +} + +fn first_call(block: &Block) -> (&Expr, &[CallArg], Span) { + let mut calls = Vec::new(); + walk_block(block, &mut |_| {}, &mut |expression| { + if let Expr::Call(callee, arguments, span) | Expr::GenericCall(callee, _, arguments, span) = + expression + { + calls.push((callee.as_ref(), arguments.as_slice(), *span)); + } + }); + *calls.first().unwrap() +} + +#[test] +fn checked_direct_scope_rejoins_original_nested_type_and_exact_parent_without_mutation() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let outer = first_binding(&original.function().unwrap().body); + let outer_entry = checked.prepare_direct_scope(outer).unwrap(); + assert!( + checked.body.scopes.is_empty(), + "preparation must not install its selection" + ); + assert!(std::ptr::eq(outer_entry.body().unwrap(), &outer.body)); + checked.install_direct_scope(&outer_entry).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + let outer_cursor = checked.cursor(); + let nested = first_binding(outer_entry.body().unwrap()); + let nested_entry = checked.prepare_direct_scope(nested).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + assert!(std::ptr::eq(nested_entry.body().unwrap(), &nested.body)); + assert_eq!(nested_entry.bound_name(), "int64"); + checked.install_direct_scope(&nested_entry).unwrap(); + assert_eq!(checked.body.scopes.len(), 2); + assert!(matches!( + checked.prepare_direct_scope(outer), + Err(ResourceExecutionError::MissingCheckedBody) + )); + assert_eq!( + checked.body.scopes.len(), + 2, + "rejected parent must leave the active cursor unchanged" + ); + checked.restore_cursor(outer_cursor).unwrap(); + assert_eq!(checked.body.scopes.len(), 1); + checked.install_function_body(&original).unwrap(); + assert!(checked.body.scopes.is_empty()); + } +} + +#[test] +fn checked_body_reference_refuses_foreign_program_wrong_root_and_scope_identity() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let (_, foreign, _) = execution(release); + assert_eq!( + foreign.validate_executable_cursor(&original.cursor), + Err(ResourceExecutionError::ForeignProgram) + ); + for corruption in 0..4 { + let mut bad = original.clone(); + match corruption { + 0 => { + bad.cursor.executable.as_mut().unwrap().origin = ExecutableOrigin::Function { + definition: checked + .declaration_definition(original.function().unwrap().name.span) + .unwrap(), + item: usize::MAX, + } + } + 1 => { + bad.cursor.executable.as_mut().unwrap().origin = ExecutableOrigin::Function { + definition: DefId::new(u32::MAX), + item: 0, + } + } + 2 => bad.cursor.root = BodyRoot::Generic(usize::MAX), + 3 => { + let outer = first_binding(&original.function().unwrap().body); + bad = checked.prepare_direct_scope(outer).unwrap().reference; + bad.cursor.scopes[0].bound_type = jett_types::TypeInterner::INT64; + } + _ => unreachable!(), + } + assert!( + checked.install_function_body(&bad).is_err(), + "corruption {corruption}" + ); + assert!(checked.body.scopes.is_empty()); + assert!(std::ptr::eq( + CheckedBodyReference { + cursor: checked.cursor() + } + .function() + .unwrap(), + original.function().unwrap() + )); + } + } +} + +#[test] +fn checked_source_calls_require_original_occurrence_and_exact_scoped_packet() { + for release in [false, true] { + let (_, mut checked, original) = execution(release); + let outer = first_binding(&original.function().unwrap().body); + let cloned_binding = outer.clone(); + assert!(matches!( + checked.prepare_direct_scope(&cloned_binding), + Err(ResourceExecutionError::MissingCheckedBody) + )); + let scoped_call = first_call(&outer.body); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let prepared = checked.prepare_direct_scope(outer).unwrap(); + checked.install_direct_scope(&prepared).unwrap(); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Ok(true) + ); + let cloned_callee = scoped_call.0.clone(); + assert_eq!( + checked.source_call_dispatch(&cloned_callee, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let cloned_arguments = scoped_call.1.to_vec(); + assert_eq!( + checked.source_call_dispatch(scoped_call.0, &cloned_arguments, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let unselected = CheckedExecution::new( + checked.program().clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + assert_eq!( + unselected.source_call_dispatch(scoped_call.0, scoped_call.1, scoped_call.2), + Err(ResourceExecutionError::MissingCheckedBody) + ); + } +} + +#[test] +fn resource_original_typed_machine_constructor_refuses_copied_source_and_wrong_body() { + let source = include_str!("../../fixtures/17_absent_aggregate_shapes.jett"); + for release in [false, true] { + let program = crate::resource_execution::tests::program(source, release); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "absent_state" => Some(function), + _ => None, + }) + .unwrap(); + let mut checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definition = checked.declaration_definition(function.name.span).unwrap(); + let reference = checked + .prepare_function_body(&checked.entry(definition).unwrap()) + .unwrap(); + checked.install_function_body(&reference).unwrap(); + let (callee, arguments, span) = first_call(&function.body); + assert!(!checked.has_invocation(span).unwrap()); + assert_eq!( + checked.source_call_dispatch(callee, arguments, span), + Ok(false) + ); + assert_eq!( + checked.source_call_dispatch(&callee.clone(), arguments, span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + assert_eq!( + checked.source_call_dispatch(callee, &arguments.to_vec(), span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + let wrong = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "empty_tokens" => Some(function), + _ => None, + }) + .unwrap(); + let wrong_definition = checked.declaration_definition(wrong.name.span).unwrap(); + let wrong_reference = checked + .prepare_function_body(&checked.entry(wrong_definition).unwrap()) + .unwrap(); + checked.install_function_body(&wrong_reference).unwrap(); + assert_eq!( + checked.source_call_dispatch(callee, arguments, span), + Err(ResourceExecutionError::MissingCheckedInvocation) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/custody.rs b/crates/jett_comptime/src/resource_execution/custody.rs new file mode 100644 index 00000000..a23126f6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/custody.rs @@ -0,0 +1,697 @@ +use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + +use jett_resolve::DefId; +use jett_runtime::ResourceRegistry; + +use super::{ResourceCarrier, ResourceExecutionError}; +use crate::value::Value; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct FrameId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct OwnerId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OwnerHolder { + Temporary { frame: FrameId, slot: usize }, + Binding { frame: FrameId, definition: DefId }, +} + +impl OwnerHolder { + fn frame(self) -> FrameId { + match self { + Self::Temporary { frame, .. } | Self::Binding { frame, .. } => frame, + } + } +} + +/// One transferable obligation. No Clone, Copy, or finalizing Rust Drop. +#[derive(Debug)] +pub(crate) struct OwnerTicket { + id: OwnerId, + holder_generation: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OwnerState { + Live, + Retired, +} + +struct OwnerRecord { + carrier: ResourceCarrier, + current_holder: OwnerHolder, + current_holder_generation: u64, + state: OwnerState, +} + +#[derive(Debug, Clone, Copy)] +struct AcquisitionEntry { + owner: OwnerId, + holder: OwnerHolder, + holder_generation: u64, +} + +struct FrameLedger { + live: bool, + acquisitions: Vec, + borrows: HashSet, +} + +/// Structural location, never a schema-only request to manufacture an owner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PayloadStep { + Typed, + Some, + Ok, + Fail, + Field(usize), + Element(usize), + MapKey(usize), + MapValue(usize), +} + +#[derive(Debug)] +pub(crate) struct PathOwner { + pub(crate) path: Vec, + pub(crate) ticket: OwnerTicket, +} + +#[derive(Debug, Clone)] +pub(crate) struct PathBorrow { + pub(crate) path: Vec, + backing: OwnerId, + carrier: ResourceCarrier, +} + +/// Physical Value copies cannot clone this ownership bundle. +#[derive(Debug, Default)] +pub(crate) struct ValueCustody { + owned: Vec, + borrowed: Vec, +} + +#[derive(Debug)] +pub(crate) struct EvaluatedValue { + pub(crate) value: Value, + pub(crate) custody: ValueCustody, +} + +impl ValueCustody { + /// Consume tickets in reached lexical order; no physical copy can supply one. + pub(crate) fn append_owned(&mut self, mut child: Self) -> Result<(), ResourceExecutionError> { + if !self.borrowed.is_empty() || !child.borrowed.is_empty() { + return Err(ResourceExecutionError::InvalidOwner); + } + self.owned.append(&mut child.owned); + Ok(()) + } + + pub(crate) fn is_empty(&self) -> bool { + self.owned.is_empty() && self.borrowed.is_empty() + } + pub(crate) fn has_owners(&self) -> bool { + !self.owned.is_empty() + } + pub(crate) fn has_borrows(&self) -> bool { + !self.borrowed.is_empty() + } +} + +impl EvaluatedValue { + pub(crate) fn ordinary(value: Value) -> Self { + Self { + value, + custody: ValueCustody::default(), + } + } + + pub(crate) fn prefix(mut self, step: PayloadStep) -> Self { + for owner in &mut self.custody.owned { + owner.path.insert(0, step.clone()); + } + for borrow in &mut self.custody.borrowed { + borrow.path.insert(0, step.clone()); + } + self + } + + pub(crate) fn remove_prefix( + &mut self, + step: PayloadStep, + ) -> Result<(), ResourceExecutionError> { + if self + .custody + .owned + .iter() + .any(|owner| owner.path.first() != Some(&step)) + || self + .custody + .borrowed + .iter() + .any(|borrow| borrow.path.first() != Some(&step)) + { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for owner in &mut self.custody.owned { + owner.path.remove(0); + } + for borrow in &mut self.custody.borrowed { + borrow.path.remove(0); + } + Ok(()) + } +} + +#[derive(Default)] +pub(crate) struct OwnerLedger { + records: Vec, + frames: Vec, +} + +impl OwnerLedger { + pub(crate) fn frame(&mut self) -> FrameId { + let id = FrameId(self.frames.len()); + self.frames.push(FrameLedger { + live: true, + acquisitions: Vec::new(), + borrows: HashSet::new(), + }); + id + } + + fn live_frame(&self, frame: FrameId) -> Result<(), ResourceExecutionError> { + if self.frames.get(frame.0).is_some_and(|frame| frame.live) { + return Ok(()); + } + Err(ResourceExecutionError::InvalidFrame) + } + + pub(crate) fn frame_is_live(&self, frame: FrameId) -> bool { + self.frames.get(frame.0).is_some_and(|frame| frame.live) + } + + pub(crate) fn acquire( + &mut self, + carrier: ResourceCarrier, + holder: OwnerHolder, + ) -> Result { + self.live_frame(holder.frame())?; + if self + .records + .iter() + .any(|record| record.carrier.key == carrier.key && record.state == OwnerState::Live) + { + return Err(ResourceExecutionError::InvalidOwner); + } + let id = OwnerId(self.records.len()); + let generation = 1; + self.records.push(OwnerRecord { + carrier, + current_holder: holder, + current_holder_generation: generation, + state: OwnerState::Live, + }); + self.frames[holder.frame().0] + .acquisitions + .push(AcquisitionEntry { + owner: id, + holder, + holder_generation: generation, + }); + Ok(ValueCustody { + owned: vec![PathOwner { + path: Vec::new(), + ticket: OwnerTicket { + id, + holder_generation: generation, + }, + }], + borrowed: Vec::new(), + }) + } + + fn record(&self, ticket: &OwnerTicket) -> Result<&OwnerRecord, ResourceExecutionError> { + let record = self + .records + .get(ticket.id.0) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if record.state != OwnerState::Live + || record.current_holder_generation != ticket.holder_generation + { + return Err(ResourceExecutionError::InvalidOwner); + } + self.live_frame(record.current_holder.frame())?; + Ok(record) + } + + fn has_active_borrow(&self, owner: OwnerId) -> bool { + self.frames + .iter() + .any(|frame| frame.live && frame.borrows.contains(&owner)) + } + + pub(crate) fn hold_borrows( + &mut self, + custody: &ValueCustody, + frame: FrameId, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(frame)?; + for borrow in &custody.borrowed { + self.validate_borrow(borrow)?; + } + for borrow in &custody.borrowed { + self.frames[frame.0].borrows.insert(borrow.backing); + } + Ok(()) + } + + pub(crate) fn transfer( + &mut self, + custody: &mut ValueCustody, + destination: OwnerHolder, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(destination.frame())?; + let mut owners = HashSet::new(); + // Validate every member before changing any holder. The vector's order + // is lexical acquisition order, never formal/field/hash ordering. + for owner in &custody.owned { + if !owners.insert(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidOwner); + } + if self.has_active_borrow(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidBorrow); + } + self.record(&owner.ticket)? + .current_holder_generation + .checked_add(1) + .ok_or(ResourceExecutionError::IdentityExhausted)?; + } + for owner in &mut custody.owned { + let record = &mut self.records[owner.ticket.id.0]; + record.current_holder_generation += 1; + record.current_holder = destination; + owner.ticket.holder_generation = record.current_holder_generation; + self.frames[destination.frame().0] + .acquisitions + .push(AcquisitionEntry { + owner: owner.ticket.id, + holder: destination, + holder_generation: record.current_holder_generation, + }); + } + Ok(()) + } + + pub(crate) fn borrow( + &self, + value: &EvaluatedValue, + ) -> Result { + let mut borrowed = value.custody.borrowed.clone(); + for borrow in &borrowed { + self.validate_borrow(borrow)?; + } + for owner in &value.custody.owned { + let record = self.record(&owner.ticket)?; + borrowed.push(PathBorrow { + path: owner.path.clone(), + backing: owner.ticket.id, + carrier: record.carrier.clone(), + }); + } + Ok(EvaluatedValue { + value: value.value.clone(), + custody: ValueCustody { + owned: Vec::new(), + borrowed, + }, + }) + } + + fn validate_borrow(&self, borrow: &PathBorrow) -> Result<(), ResourceExecutionError> { + let record = self + .records + .get(borrow.backing.0) + .ok_or(ResourceExecutionError::InvalidBorrow)?; + if record.state != OwnerState::Live || record.carrier.key != borrow.carrier.key { + return Err(ResourceExecutionError::InvalidBorrow); + } + self.live_frame(record.current_holder.frame())?; + Ok(()) + } + + pub(crate) fn borrowed_carrier( + &self, + custody: &ValueCustody, + ) -> Result<&ResourceCarrier, ResourceExecutionError> { + if !custody.owned.is_empty() { + return Err(ResourceExecutionError::InvalidBorrow); + } + let [borrow] = custody.borrowed.as_slice() else { + return Err(ResourceExecutionError::InvalidBorrow); + }; + if !borrow.path.is_empty() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.validate_borrow(borrow)?; + Ok(&self.records[borrow.backing.0].carrier) + } + + pub(crate) fn validate_positions( + &self, + custody: &ValueCustody, + positions: &[(Vec, &ResourceCarrier)], + ) -> Result<(), ResourceExecutionError> { + if positions.len() != custody.owned.len() + custody.borrowed.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut claimed = HashSet::new(); + for owner in &custody.owned { + let record = self.record(&owner.ticket)?; + let candidates = positions + .iter() + .enumerate() + .filter(|(_, (path, carrier))| { + path.as_slice() == owner.path.as_slice() + && carrier.key == record.carrier.key + && carrier.resource == record.carrier.resource + && std::sync::Arc::ptr_eq(&carrier.program, &record.carrier.program) + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = candidates.as_slice() else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + if !claimed.insert(*index) { + return Err(ResourceExecutionError::InvalidOwner); + } + } + for borrow in &custody.borrowed { + self.validate_borrow(borrow)?; + let candidates = positions + .iter() + .enumerate() + .filter(|(_, (path, carrier))| { + path.as_slice() == borrow.path.as_slice() + && carrier.key == borrow.carrier.key + && carrier.resource == borrow.carrier.resource + && std::sync::Arc::ptr_eq(&carrier.program, &borrow.carrier.program) + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = candidates.as_slice() else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + if !claimed.insert(*index) { + return Err(ResourceExecutionError::InvalidBorrow); + } + } + Ok(()) + } + + pub(crate) fn close( + &mut self, + custody: ValueCustody, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceExecutionError> { + if !custody.borrowed.is_empty() { + return Err(ResourceExecutionError::InvalidOwner); + } + let [owner] = custody.owned.as_slice() else { + return Err(ResourceExecutionError::MissingOwner); + }; + if !owner.path.is_empty() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + if self.has_active_borrow(owner.ticket.id) { + return Err(ResourceExecutionError::InvalidBorrow); + } + let record = self.record(&owner.ticket)?; + let key = record.carrier.key; + let resource_type = record.carrier.resource.registry_type; + // Retire before the finalizer, including if it raises a host panic. + self.records[owner.ticket.id.0].state = OwnerState::Retired; + registry.close(key, resource_type)?; + Ok(()) + } + + pub(crate) fn unwind( + &mut self, + frame: FrameId, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceExecutionError> { + self.live_frame(frame)?; + self.frames[frame.0].borrows.clear(); + let protected = self.frames[frame.0].acquisitions.iter().any(|entry| { + let record = &self.records[entry.owner.0]; + record.state == OwnerState::Live + && record.current_holder == entry.holder + && record.current_holder_generation == entry.holder_generation + && self.has_active_borrow(entry.owner) + }); + if protected { + return Err(ResourceExecutionError::InvalidBorrow); + } + let entries = std::mem::take(&mut self.frames[frame.0].acquisitions); + let mut failure = None; + let mut panic = None; + for entry in entries.into_iter().rev() { + let record = &mut self.records[entry.owner.0]; + if record.state != OwnerState::Live + || record.current_holder != entry.holder + || record.current_holder_generation != entry.holder_generation + { + continue; + } + record.state = OwnerState::Retired; + let finalized = catch_unwind(AssertUnwindSafe(|| { + registry.close(record.carrier.key, record.carrier.resource.registry_type) + })); + match finalized { + Ok(Ok(())) => {} + Ok(Err(error)) => { + failure.get_or_insert(ResourceExecutionError::Registry(error)); + } + Err(payload) if panic.is_none() => panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + self.frames[frame.0].live = false; + if let Some(payload) = panic { + resume_unwind(payload); + } + failure.map_or(Ok(()), Err) + } + + pub(crate) fn has_live_frames(&self) -> bool { + self.frames.iter().any(|frame| frame.live) + } + pub(crate) fn live_frame_count(&self) -> usize { + self.frames.iter().filter(|frame| frame.live).count() + } + pub(crate) fn has_live_borrows(&self) -> bool { + self.frames.iter().any(|frame| !frame.borrows.is_empty()) + } + + pub(crate) fn live_owners(&self) -> usize { + self.records + .iter() + .filter(|record| record.state == OwnerState::Live) + .count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ResourceTypeBinding, tests::program}; + use jett_runtime::{AuthorityProvenance, ResourceTypeId}; + use std::sync::{Arc, Mutex}; + + fn carrier( + registry: &mut ResourceRegistry, + label: i64, + events: Arc>>, + panic: bool, + ) -> ResourceCarrier { + let program = program(include_str!("fixtures/02_reverse_scope_drop.jett"), false); + let hook = program.checked().resource_hooks.values().next().unwrap(); + let resource = ResourceTypeBinding { + definition: hook.resource_definition, + checked_type: hook.resource_type, + registry_type: ResourceTypeId::new(0), + }; + let key = registry + .insert( + resource.registry_type, + label, + AuthorityProvenance::new(1, 1), + move |label| { + events.lock().unwrap().push(label); + if panic { + panic!("selected test finalizer"); + } + }, + ) + .unwrap(); + ResourceCarrier { + program, + resource, + key, + } + } + + #[test] + fn holder_generation_prevents_old_entries_from_finalizing_returned_storage() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let outer = ledger.frame(); + let inner = ledger.frame(); + let original = OwnerHolder::Temporary { + frame: outer, + slot: 0, + }; + let mut first = ledger + .acquire(carrier(&mut registry, 1, events.clone(), false), original) + .unwrap(); + let second = ledger + .acquire( + carrier(&mut registry, 2, events.clone(), false), + OwnerHolder::Temporary { + frame: outer, + slot: 1, + }, + ) + .unwrap(); + ledger + .transfer( + &mut first, + OwnerHolder::Temporary { + frame: inner, + slot: 0, + }, + ) + .unwrap(); + ledger.transfer(&mut first, original).unwrap(); + ledger.unwind(inner, &mut registry).unwrap(); + assert!(events.lock().unwrap().is_empty()); + ledger.unwind(outer, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [1, 2]); // Latest holder acquisitions, not original registry order. + assert_eq!(ledger.live_owners(), 0); + assert_eq!(registry.live_count(), 0); + drop((first, second)); + assert_eq!(*events.lock().unwrap(), [1, 2]); + } + + #[test] + fn borrowed_operation_protects_owner_before_transfer_or_close_effect() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let owner = ledger.frame(); + let operation = ledger.frame(); + let carrier = carrier(&mut registry, 3, events.clone(), false); + let mut value = EvaluatedValue { + value: Value::Resource(carrier.clone()), + custody: ledger + .acquire( + carrier, + OwnerHolder::Temporary { + frame: owner, + slot: 0, + }, + ) + .unwrap(), + }; + let borrowed = ledger.borrow(&value).unwrap(); + ledger.hold_borrows(&borrowed.custody, operation).unwrap(); + assert_eq!( + ledger.transfer( + &mut value.custody, + OwnerHolder::Temporary { + frame: operation, + slot: 0 + } + ), + Err(ResourceExecutionError::InvalidBorrow) + ); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(registry.live_count(), 1); + ledger.unwind(operation, &mut registry).unwrap(); + ledger.close(value.custody, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [3]); + assert_eq!(ledger.live_owners(), 0); + assert_eq!( + ledger.borrowed_carrier(&borrowed.custody).unwrap_err(), + ResourceExecutionError::InvalidBorrow + ); + ledger.unwind(owner, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [3]); + } + + #[test] + fn copied_carrier_or_foreign_payload_cannot_create_custody() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let frame = ledger.frame(); + let carrier = carrier(&mut registry, 4, events.clone(), false); + let custody = ledger + .acquire(carrier.clone(), OwnerHolder::Temporary { frame, slot: 0 }) + .unwrap(); + assert_eq!( + ledger.validate_positions(&ValueCustody::default(), &[(Vec::new(), &carrier)]), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + assert_eq!( + ledger + .acquire(carrier.clone(), OwnerHolder::Temporary { frame, slot: 1 }) + .unwrap_err(), + ResourceExecutionError::InvalidOwner + ); + let foreign = ResourceCarrier { + program: program(include_str!("fixtures/02_reverse_scope_drop.jett"), false), + ..carrier.clone() + }; + assert_eq!( + ledger.validate_positions(&custody, &[(Vec::new(), &foreign)]), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + ledger.unwind(frame, &mut registry).unwrap(); + assert_eq!(*events.lock().unwrap(), [4]); + } + + #[test] + fn finalizer_panic_retires_before_panic_and_continues_every_remaining_owner() { + let mut registry = ResourceRegistry::new(); + let mut ledger = OwnerLedger::default(); + let events = Arc::new(Mutex::new(Vec::new())); + let frame = ledger.frame(); + let first = ledger + .acquire( + carrier(&mut registry, 5, events.clone(), false), + OwnerHolder::Temporary { frame, slot: 0 }, + ) + .unwrap(); + let second = ledger + .acquire( + carrier(&mut registry, 6, events.clone(), true), + OwnerHolder::Temporary { frame, slot: 1 }, + ) + .unwrap(); + assert!(catch_unwind(AssertUnwindSafe(|| ledger.unwind(frame, &mut registry))).is_err()); + assert_eq!(*events.lock().unwrap(), [6, 5]); + assert_eq!(ledger.live_owners(), 0); + assert_eq!(registry.live_count(), 0); + assert_eq!( + ledger.unwind(frame, &mut registry), + Err(ResourceExecutionError::InvalidFrame) + ); + drop((first, second)); + drop(registry); + assert_eq!(*events.lock().unwrap(), [6, 5]); + } +} diff --git a/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett new file mode 100644 index 00000000..7cbfefbf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/01_construct_borrow_move_close.jett @@ -0,0 +1,14 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 101) handle error: + return nothing + int64 before = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 again = resource_probe.borrow(view net, view first) handle error: + return nothing + resource_probe.TestHandle moved = move_owned(first) + resource_probe.close(moved) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett b/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett new file mode 100644 index 00000000..15e93563 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/02_reverse_scope_drop.jett @@ -0,0 +1,12 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 201) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 202) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 later = resource_probe.borrow(view net, view second) handle error: + return nothing + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett b/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett new file mode 100644 index 00000000..28e1e6cb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/03_domain_failures.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 301) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + default -1 + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett b/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett new file mode 100644 index 00000000..cede9b07 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/04_bare_to_view_operation_cleanup.jett @@ -0,0 +1,12 @@ +namespace app +function consume_at_view_boundary(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 401) handle error: + return nothing + consume_at_view_boundary(view net, token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett new file mode 100644 index 00000000..329e8a8a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure.jett @@ -0,0 +1,13 @@ +namespace app +function receive(marker: int64, view token: resource_probe.TestHandle) returns nothing: + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + receive(token: token, marker: fail_marker() handle error: + return nothing + ) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett new file mode 100644 index 00000000..7a258cbd --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett @@ -0,0 +1,21 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett b/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett new file mode 100644 index 00000000..990f8c0b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/06_return_occupied_result.jett @@ -0,0 +1,15 @@ +namespace app +function carry_result(outcome: result[resource_probe.TestHandle, string]) returns result[resource_probe.TestHandle, string]: + return outcome +function acquire(view net: Network) returns result[resource_probe.TestHandle, string]: + use resource_probe + return carry_result(resource_probe.create(view net, 601)) +export function scenario(view net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = acquire(view net) + resource_probe.TestHandle token = held handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett new file mode 100644 index 00000000..90692c68 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/07_indirect_close.jett @@ -0,0 +1,8 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett b/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett new file mode 100644 index 00000000..aab76330 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/08_required_descriptor_and_absence.jett @@ -0,0 +1,21 @@ +namespace app +export function descriptor_storage() returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.close_descriptor() + return nothing +export function absent_close_path() returns nothing: + use resource_probe + optional[resource_probe.TestHandle] empty = comptime resource_probe.empty() + resource_probe.TestHandle token = empty handle: + return nothing + resource_probe.close(token) + return nothing +verify descriptor_verify_control: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert true +property descriptor_property_control: + given marker: int64 + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett b/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett new file mode 100644 index 00000000..43916baf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/09_capability_free_close_target.jett @@ -0,0 +1,11 @@ +namespace app +export function close_entry(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 901) handle error: + return nothing + close_entry(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett b/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett new file mode 100644 index 00000000..ec75a3af --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/10_ordinary_closure_unreferenced_resource.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return -1 + function() returns int64 ordinary = function() returns int64: return 7 + int64 observed = ordinary() + resource_probe.close(token) + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett b/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett new file mode 100644 index 00000000..f11b39e7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/11_terminal_assertion_cleanup.jett @@ -0,0 +1,9 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1101) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett b/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett new file mode 100644 index 00000000..f0eb7004 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/12_mixed_failure_cleanup_continues.jett @@ -0,0 +1,16 @@ +namespace app +function consume_at_view_boundary(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1201) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1202) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view first) handle error: + return nothing + consume_at_view_boundary(view net, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett b/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett new file mode 100644 index 00000000..fc646d9a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/13_direct_nested_scoped_lifecycle.jett @@ -0,0 +1,26 @@ +namespace app +function move_scoped_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token first = resource_probe.create(view net, 1301) handle error: + return nothing + int64 before = resource_probe.borrow(view net, view first) handle error: + return nothing + Token moved = move_scoped_owner(first) + resource_probe.close(moved) + comptime type Element = type.arg[list[int64]](0): + Element label = 1302 + resource_probe.TestHandle second = resource_probe.create(view net, label) handle error: + return nothing + Element observed = resource_probe.borrow(view net, view second) handle error: + return nothing + resource_probe.close(second) + Token restored = resource_probe.create(view net, 1304) handle error: + return nothing + resource_probe.close(restored) + resource_probe.TestHandle after = resource_probe.create(view net, 1303) handle error: + return nothing + resource_probe.close(after) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett b/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett new file mode 100644 index 00000000..546e7bfb --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/14_direct_scoped_descriptor_absence.jett @@ -0,0 +1,15 @@ +namespace app +export function descriptor_storage() returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + function(Token) returns nothing finalizer = resource_probe.close_descriptor() + comptime type Nested = type.arg[optional[Token]](0): + optional[Nested] absent = resource_probe.empty() + return nothing +export function absent_close_path() returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token missing = resource_probe.empty() handle: + return nothing + resource_probe.close(missing) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett b/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett new file mode 100644 index 00000000..172f5404 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/15_direct_scoped_ordinary_error.jett @@ -0,0 +1,10 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token owner = resource_probe.create(view net, 1501) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view owner) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett b/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett new file mode 100644 index 00000000..4e7b47e7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/16_direct_scoped_return_default.jett @@ -0,0 +1,12 @@ +namespace app +function observed_or_default(view net: Network, label: int64) returns int64: + use resource_probe + comptime type Element = type.info[int64](): + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return -1 + Element observed = resource_probe.borrow(view net, view owner) handle error: + default 17 + return observed + return -2 +export function scenario(view net: Network) returns int64: + return observed_or_default(view net, 1601) diff --git a/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett b/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett b/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett new file mode 100644 index 00000000..18865303 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/18_pipeline_construct_move_close.jett @@ -0,0 +1,9 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(1801) handle error: + return nothing + token into move_owned() into resource_probe.close() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett b/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett new file mode 100644 index 00000000..4e02320b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/19_pipeline_written_view_retains_owner.jett @@ -0,0 +1,12 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett b/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett new file mode 100644 index 00000000..f7d76a77 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/20_pipeline_bare_owner_view_operation.jett @@ -0,0 +1,11 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed diff --git a/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett b/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett new file mode 100644 index 00000000..afb926ea --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/21_pipeline_named_actual_abort.jett @@ -0,0 +1,22 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(token: resource_probe.TestHandle, marker: int64, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2101) handle error: + return nothing + stage_owned(view net, token) into accept_staged(net: view net, marker: fail_marker() handle error: + return nothing + ) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett b/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett new file mode 100644 index 00000000..92cd42f4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/22_pipeline_domain_failure_stops_next_step.jett @@ -0,0 +1,7 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 diff --git a/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett b/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett new file mode 100644 index 00000000..2076d3fe --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/23_pipeline_indirect_close_descriptor.jett @@ -0,0 +1,8 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing close_token = resource_probe.close_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 2301) handle error: + return nothing + token into close_token + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett b/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett new file mode 100644 index 00000000..acc73b8c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/24_mutable_assignment.jett @@ -0,0 +1,41 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +export function replace_live(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_after_close(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing +export function rebind_self(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing +export function failed_rhs_keeps_owner(view net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett new file mode 100644 index 00000000..22420b4a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/25_repeated_scoped_entry.jett @@ -0,0 +1,20 @@ +namespace app +export function clean(view net: Network) returns int64: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token token = resource_probe.create(view net, 1801) handle error: + return -1 + int64 observed = resource_probe.borrow(view net, view token) handle error: + return -2 + resource_probe.close(token) + return observed + return -3 +export function terminal_error(view net: Network) returns nothing: + use resource_probe + comptime type Token = type.info[resource_probe.TestHandle](): + Token token = resource_probe.create(view net, 1802) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.terminal_failure() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett b/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett new file mode 100644 index 00000000..f9d18505 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/26_original_required_regions.jett @@ -0,0 +1,21 @@ +namespace app +function seven() returns int64: + return 7 +int64 baked = comptime seven() +export function scenario() returns int64: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.close_descriptor() + optional[resource_probe.TestHandle] absent = comptime resource_probe.empty() + return baked +verify original_required_verify: + use resource_probe + int64 observed = comptime seven() + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + assert observed == 7 +property original_required_property: + given marker: int64 + use resource_probe + int64 observed = comptime seven() + optional[resource_probe.TestHandle] absent = resource_probe.empty() + assert observed == 7 + assert marker == marker diff --git a/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett b/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett new file mode 100644 index 00000000..3a22c964 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett @@ -0,0 +1,13 @@ +namespace app +function unused[T]() returns string: + return comptime type.name[T]() +function label[T]() returns string: + string label_value = comptime type.name[T]() + return label_value +export function generic_label() returns string: + return label[int64]() +export function scoped_label() returns string: + comptime type Outer = type.info[list[int64]](): + comptime type Element = type.arg[Outer](0): + return comptime type.name[Element]() + return "unreached" diff --git a/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett b/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett new file mode 100644 index 00000000..6da904dc --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/28_pipeline_absence_regions.jett @@ -0,0 +1,16 @@ +namespace app +function absent_token() returns optional[resource_probe.TestHandle]: + return none +function forward_absent(token: optional[resource_probe.TestHandle]) returns optional[resource_probe.TestHandle]: + return token +function marker(token: optional[resource_probe.TestHandle]) returns int64: + return 17 +int64 namespace_marker = comptime(absent_token() into forward_absent() into marker()) +export function explicit_marker() returns int64: + return comptime(absent_token() into forward_absent() into marker()) +verify absent_pipeline_verify: + assert(absent_token() into forward_absent() into marker()) == 17 +property absent_pipeline_property: + given count: int64 + assert(absent_token() into forward_absent() into marker()) == 17 + assert count == count diff --git a/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett b/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett new file mode 100644 index 00000000..2bf7e86d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett @@ -0,0 +1,13 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: 17) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett b/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett new file mode 100644 index 00000000..03016231 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett @@ -0,0 +1,16 @@ +namespace app +struct OwnedPair: + first: resource_probe.TestHandle + second: resource_probe.TestHandle + marker: int64 +function retire_pair(first: resource_probe.TestHandle, second: resource_probe.TestHandle) returns nothing: + OwnedPair held = OwnedPair(second: second, first: first, marker: 17) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1002) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1003) handle error: + return nothing + retire_pair(first, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett b/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett new file mode 100644 index 00000000..3ccedf99 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett @@ -0,0 +1,15 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function fail_marker() returns result[int64, string]: + return fail("selected constructor marker failure") +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: fail_marker() handle error: return nothing) + return nothing +export function main(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1004) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett new file mode 100644 index 00000000..4f825e21 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett @@ -0,0 +1,13 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1001) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett new file mode 100644 index 00000000..128b8398 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett @@ -0,0 +1,16 @@ +namespace app +struct OwnedPair: + first: resource_probe.TestHandle + second: resource_probe.TestHandle + marker: int64 +function retire_pair(first: resource_probe.TestHandle, second: resource_probe.TestHandle) returns nothing: + OwnedPair held = OwnedPair(second: second, first: first, marker: 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 1002) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 1003) handle error: + return nothing + retire_pair(first, second) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett b/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett new file mode 100644 index 00000000..36252ec3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett @@ -0,0 +1,15 @@ +namespace app +struct OwnedEnvelope: + token: resource_probe.TestHandle + marker: int64 +function fail_marker() returns result[int64, string]: + return fail("selected constructor marker failure") +function retire_owned(token: resource_probe.TestHandle) returns nothing: + OwnedEnvelope held = OwnedEnvelope(token: token, marker: fail_marker() handle error: return nothing) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1004) handle error: + return nothing + retire_owned(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett b/crates/jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett new file mode 100644 index 00000000..c2812c8f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett @@ -0,0 +1,55 @@ +namespace app +struct PrimitiveEnvelope: + token: resource_probe.TestHandle + signed8: int8 + signed16: int16 + signed32: int32 + signed64: int64 + unsigned8: uint8 + unsigned16: uint16 + unsigned32: uint32 + unsigned64: uint64 + rounded: float32 + wide: float64 + marker: string + enabled: bool + unit: nothing +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_primitives(token: resource_probe.TestHandle) returns nothing: + PrimitiveEnvelope held = PrimitiveEnvelope(unsigned64: 18446744073709551615, rounded: 16777217.0, token: token, signed8: -128, signed16: -32768, signed32: -2147483648, signed64: -9223372036854775808, unsigned8: 255, unsigned16: 65535, unsigned32: 4294967295, wide: 16777217.0, marker: "retained", enabled: true, unit: nothing) + int8 expected_signed8 = -128 + expect(held.signed8 == expected_signed8) + int16 expected_signed16 = -32768 + expect(held.signed16 == expected_signed16) + int32 expected_signed32 = -2147483648 + expect(held.signed32 == expected_signed32) + int64 expected_signed64 = -9223372036854775808 + expect(held.signed64 == expected_signed64) + uint8 expected_unsigned8 = 255 + expect(held.unsigned8 == expected_unsigned8) + uint16 expected_unsigned16 = 65535 + expect(held.unsigned16 == expected_unsigned16) + uint32 expected_unsigned32 = 4294967295 + expect(held.unsigned32 == expected_unsigned32) + uint64 expected_unsigned64 = 18446744073709551615 + expect(held.unsigned64 == expected_unsigned64) + float32 expected_rounded = 16777216.0 + expect(held.rounded == expected_rounded) + float64 expected_wide = 16777217.0 + expect(held.wide == expected_wide) + string expected_marker = "retained" + expect(held.marker == expected_marker) + expect(held.enabled) + nothing expected_unit = nothing + expect(held.unit == expected_unit) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1101) handle error: + return nothing + retire_primitives(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett b/crates/jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett new file mode 100644 index 00000000..eb240da7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett @@ -0,0 +1,29 @@ +namespace app +type SmallSigned = int8 +type LargeUnsigned = uint64 +type Rounded = float32 +struct AliasEnvelope: + token: resource_probe.TestHandle + small: SmallSigned + large: LargeUnsigned + rounded: Rounded +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_aliases(token: resource_probe.TestHandle) returns nothing: + AliasEnvelope held = AliasEnvelope(rounded: 0.1, small: 127, token: token, large: 18446744073709551615) + SmallSigned expected_small = 127 + expect(held.small == expected_small) + LargeUnsigned expected_large = 18446744073709551615 + expect(held.large == expected_large) + Rounded expected_rounded = 0.1 + expect(held.rounded == expected_rounded) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1102) handle error: + return nothing + retire_aliases(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett b/crates/jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett new file mode 100644 index 00000000..3d9ebaec --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett @@ -0,0 +1,25 @@ +namespace app +struct PendingEnvelope: + token: resource_probe.TestHandle + marker: int64 +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_pending(token: resource_probe.TestHandle) returns nothing: + PendingEnvelope held = PendingEnvelope(token: token, marker: run run 17) + int64 once = join held.marker handle error: + expect(false) + return nothing + int64 ready = join once handle error: + expect(false) + return nothing + expect(ready == 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1103) handle error: + return nothing + retire_pending(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/39_owned_struct_pending_int64_inspection.jett b/crates/jett_comptime/src/resource_execution/fixtures/39_owned_struct_pending_int64_inspection.jett new file mode 100644 index 00000000..76e95382 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/39_owned_struct_pending_int64_inspection.jett @@ -0,0 +1,22 @@ +namespace app +struct PendingEnvelope: + token: resource_probe.TestHandle + marker: int64 +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_pending(token: resource_probe.TestHandle) returns nothing: + PendingEnvelope held = PendingEnvelope(token: token, marker: run run 17) + int64 once = join held.marker handle error: + expect(false) + return nothing + expect(once == 17) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1103) handle error: + return nothing + retire_pending(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett b/crates/jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett new file mode 100644 index 00000000..768cccdc --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett @@ -0,0 +1,58 @@ +namespace app +struct PendingPrimitives: + token: resource_probe.TestHandle + small: int8 + large: uint64 + rounded: float32 + wide: float64 + marker: string + enabled: bool + unit: nothing +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function ready_small(value: int8) returns int8: + int8 once = join value handle error: default 0 + return join once handle error: default 0 +function ready_large(value: uint64) returns uint64: + uint64 once = join value handle error: default 0 + return join once handle error: default 0 +function ready_rounded(value: float32) returns float32: + float32 once = join value handle error: default 0.0 + return join once handle error: default 0.0 +function ready_wide(value: float64) returns float64: + float64 once = join value handle error: default 0.0 + return join once handle error: default 0.0 +function ready_marker(value: string) returns string: + string once = join value handle error: default "missing" + return join once handle error: default "missing" +function ready_enabled(value: bool) returns bool: + bool once = join value handle error: default false + return join once handle error: default false +function ready_unit(value: nothing) returns nothing: + nothing once = join value handle error: default nothing + return join once handle error: default nothing +function retire_pending_primitives(token: resource_probe.TestHandle) returns nothing: + PendingPrimitives held = PendingPrimitives(marker: run run "retained", small: run run -128, unit: run run nothing, token: token, large: run run 18446744073709551615, rounded: run run 0.1, wide: run run 1.5, enabled: run run true) + int8 expected_small = -128 + expect(ready_small(held.small) == expected_small) + uint64 expected_large = 18446744073709551615 + expect(ready_large(held.large) == expected_large) + float32 expected_rounded = 0.1 + expect(ready_rounded(held.rounded) == expected_rounded) + float64 expected_wide = 1.5 + expect(ready_wide(held.wide) == expected_wide) + string expected_marker = "retained" + expect(ready_marker(held.marker) == expected_marker) + expect(ready_enabled(held.enabled)) + nothing expected_unit = nothing + expect(ready_unit(held.unit) == expected_unit) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1104) handle error: + return nothing + retire_pending_primitives(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/41_owned_struct_pending_prior_failure.jett b/crates/jett_comptime/src/resource_execution/fixtures/41_owned_struct_pending_prior_failure.jett new file mode 100644 index 00000000..3562fdd6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/41_owned_struct_pending_prior_failure.jett @@ -0,0 +1,18 @@ +namespace app +struct PendingPrefix: + token: resource_probe.TestHandle + pending: int64 + marker: int64 +function fail_marker() returns int64: + use resource_companion_probe + resource_companion_probe.terminal_failure() + return 0 +function retire_prefix(token: resource_probe.TestHandle) returns nothing: + PendingPrefix held = PendingPrefix(token: token, pending: run run 17, marker: fail_marker()) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1105) handle error: + return nothing + retire_prefix(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett b/crates/jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett new file mode 100644 index 00000000..1a9e494c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett @@ -0,0 +1,58 @@ +namespace app +struct PendingMatrix: + token: resource_probe.TestHandle + signed8: int8 + signed16: int16 + signed32: int32 + signed64: int64 + unsigned8: uint8 + unsigned16: uint16 + unsigned32: uint32 + unsigned64: uint64 + rounded: float32 + wide: float64 + marker: string + enabled: bool + unit: nothing +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function ready[T](value: T) returns T: + T once = join value handle error: default value + return join once handle error: default once +function retire_pending_matrix(token: resource_probe.TestHandle) returns nothing: + PendingMatrix held = PendingMatrix(unsigned64: run run 18446744073709551615, rounded: run run 16777217.0, token: token, signed8: run run -128, signed16: run run -32768, signed32: run run -2147483648, signed64: run run -9223372036854775808, unsigned8: run run 255, unsigned16: run run 65535, unsigned32: run run 4294967295, wide: run run 16777217.0, marker: run run "retained", enabled: run run true, unit: run run nothing) + int8 expected_signed8 = -128 + expect(ready[int8](held.signed8) == expected_signed8) + int16 expected_signed16 = -32768 + expect(ready[int16](held.signed16) == expected_signed16) + int32 expected_signed32 = -2147483648 + expect(ready[int32](held.signed32) == expected_signed32) + int64 expected_signed64 = -9223372036854775808 + expect(ready[int64](held.signed64) == expected_signed64) + uint8 expected_unsigned8 = 255 + expect(ready[uint8](held.unsigned8) == expected_unsigned8) + uint16 expected_unsigned16 = 65535 + expect(ready[uint16](held.unsigned16) == expected_unsigned16) + uint32 expected_unsigned32 = 4294967295 + expect(ready[uint32](held.unsigned32) == expected_unsigned32) + uint64 expected_unsigned64 = 18446744073709551615 + expect(ready[uint64](held.unsigned64) == expected_unsigned64) + float32 expected_rounded = 16777216.0 + expect(ready[float32](held.rounded) == expected_rounded) + float64 expected_wide = 16777217.0 + expect(ready[float64](held.wide) == expected_wide) + string expected_marker = "retained" + expect(ready[string](held.marker) == expected_marker) + expect(ready[bool](held.enabled)) + nothing expected_unit = nothing + expect(ready[nothing](held.unit) == expected_unit) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1106) handle error: + return nothing + retire_pending_matrix(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/43_owned_struct_pending_nothing_inspection.jett b/crates/jett_comptime/src/resource_execution/fixtures/43_owned_struct_pending_nothing_inspection.jett new file mode 100644 index 00000000..e33fd436 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/43_owned_struct_pending_nothing_inspection.jett @@ -0,0 +1,21 @@ +namespace app +struct PendingUnitEnvelope: + token: resource_probe.TestHandle + unit: nothing +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_pending_unit(token: resource_probe.TestHandle) returns nothing: + PendingUnitEnvelope held = PendingUnitEnvelope(token: token, unit: run run nothing) + nothing once = join held.unit handle error: + return nothing + expect(once == nothing) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1107) handle error: + return nothing + retire_pending_unit(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/44_owned_struct_pending_string_inspection.jett b/crates/jett_comptime/src/resource_execution/fixtures/44_owned_struct_pending_string_inspection.jett new file mode 100644 index 00000000..2909a5ee --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/44_owned_struct_pending_string_inspection.jett @@ -0,0 +1,21 @@ +namespace app +struct PendingTextEnvelope: + token: resource_probe.TestHandle + marker: string +function expect(condition: bool) returns nothing: + use resource_companion_probe + if not condition: + resource_companion_probe.terminal_failure() + return nothing +function retire_pending_text(token: resource_probe.TestHandle) returns nothing: + PendingTextEnvelope held = PendingTextEnvelope(token: token, marker: run run "retained") + string once = join held.marker handle error: + return nothing + expect(once == "retained") + return nothing +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1108) handle error: + return nothing + retire_pending_text(token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett new file mode 100644 index 00000000..2eff3b71 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett @@ -0,0 +1,21 @@ +namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett new file mode 100644 index 00000000..d2f39bc3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett @@ -0,0 +1,25 @@ +namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +function stored() returns function() returns Positive: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = positive + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + return selected +actor Worker: + mutable int64 count = 0 + receive read responds int64: + function() returns Positive selected = comptime stored() + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_temporary_owner_getter.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_temporary_owner_getter.jett new file mode 100644 index 00000000..b1290dfc --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_handler_temporary_owner_getter.jett @@ -0,0 +1,27 @@ +namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +struct Packet: + data: list[int64] + sibling: list[int64] +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +function make_packet() returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + list[int64] borrowed = view make_packet().data + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_required_closed_captures.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_closed_captures.jett new file mode 100644 index 00000000..ece4a196 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_closed_captures.jett @@ -0,0 +1,36 @@ +namespace app +function five() returns int64: + return 5 +int64 bias = comptime five() +function make_own() returns function(int64) returns int64: + return function(delta: int64) returns int64: + int64 local = delta + 1 + return local + bias + five() +function make_capture(seed: int64) returns function() returns int64: + int64 local = seed + 1 + return function() returns int64: + return seed + local +function make_nested(seed: int64) returns function() returns int64: + int64 local = seed + 1 + function() returns function() returns int64 outer = function() returns function() returns int64: + return function() returns int64: + return seed + local + return outer() +function evaluate() returns int64: + function(int64) returns int64 own = make_own() + function() returns int64 captured = make_capture(3) + function() returns int64 nested = make_nested(4) + return own(2) + captured() + nested() +actor Worker: + receive own responds int64: + function(int64) returns int64 selected = comptime make_own() + respond selected(2) + receive captured responds int64: + function() returns int64 selected = comptime make_capture(3) + respond selected() + receive nested responds int64: + function() returns int64 selected = comptime make_nested(4) + respond selected() + receive evaluate responds int64: + int64 answer = comptime evaluate() + respond answer diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture.jett new file mode 100644 index 00000000..802846f2 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture.jett @@ -0,0 +1,7 @@ +namespace app +actor Worker: + mutable int64 count = 0 + receive read responds int64: + function() returns int64 selected = comptime function() returns int64: + return count + respond selected() diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture_same_constant.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture_same_constant.jett new file mode 100644 index 00000000..72d7ae78 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_capture_same_constant.jett @@ -0,0 +1,8 @@ +namespace app +int64 count = 17 +actor Worker: + mutable int64 count = 0 + receive read responds int64: + function() returns int64 selected = comptime function() returns int64: + return count + respond selected() diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_parameter_capture.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_parameter_capture.jett new file mode 100644 index 00000000..af863a7d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_free_parameter_capture.jett @@ -0,0 +1,8 @@ +namespace app +function value() returns int64: + return 17 +actor Worker: + receive read(value: int64) responds int64: + function() returns int64 selected = comptime function() returns int64: + return value + respond selected() diff --git a/crates/jett_comptime/src/resource_execution/fixtures/actor_required_nested_free_capture.jett b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_nested_free_capture.jett new file mode 100644 index 00000000..add5121e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/actor_required_nested_free_capture.jett @@ -0,0 +1,9 @@ +namespace app +actor Worker: + mutable int64 count = 0 + receive read responds int64: + function() returns function() returns int64 selected = comptime function() returns function() returns int64: + return function() returns int64: + return count + function() returns int64 inner = selected() + respond inner() diff --git a/crates/jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett b/crates/jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett new file mode 100644 index 00000000..5b26d39b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett @@ -0,0 +1,5 @@ +namespace resource_companion_probe +export function terminal_failure() returns nothing: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett b/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett new file mode 100644 index 00000000..abcd67ae --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/fixtures/resource_probe.jett @@ -0,0 +1,17 @@ +namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(view net, label) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function close_descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function empty() returns optional[TestHandle]: + return none +export function terminal_failure() returns nothing: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport.rs new file mode 100644 index 00000000..87a5faaa --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport.rs @@ -0,0 +1,1860 @@ +//! Envelope transport in the existing checked source evaluator. +#[path = "interpreter_transport/assignment.rs"] +mod assignment; + +#[path = "interpreter_transport/absent_generated.rs"] +mod absent_generated; + +#[path = "interpreter_transport/owned_struct.rs"] +mod owned_struct; + +use super::*; +use crate::resource_execution::FunctionInvocation; +use crate::resource_execution::{ + CheckedInvocation, EvaluatedValue, ExecutionPurpose, FrameId, OperationFrame, PayloadStep, + PreparedIntrinsicArguments, ResourceHookDescriptor, ResourceTransport, +}; +use jett_parser::ast::VarDecl; +use jett_resolve::DefId; +use jett_typecheck::{CheckedCalleeAccess, CheckedInvocationTarget}; +use jett_types::Type; + +#[path = "interpreter_transport/entry_context.rs"] +mod entry_context; +use entry_context::SavedResourceEntryContext; + +#[path = "interpreter_transport/intrinsic.rs"] +mod intrinsic; +#[path = "interpreter_transport/pipeline.rs"] +mod pipeline; +#[path = "interpreter_transport/required.rs"] +mod required; + +// Runtime registration identifies a retained declaration; its cloned body is +// deliberately excluded because only the retained checked body is executed. +fn same_registered_function_header(left: &FunctionDef, right: &FunctionDef) -> bool { + fn ident(left: &Ident, right: &Ident) -> bool { + left.name == right.name && left.span == right.span + } + fn types(left: &[TypeExpr], right: &[TypeExpr]) -> bool { + left.len() == right.len() && left.iter().zip(right).all(|(left, right)| ty(left, right)) + } + fn ty(left: &TypeExpr, right: &TypeExpr) -> bool { + match (left, right) { + (TypeExpr::Named(left), TypeExpr::Named(right)) => ident(left, right), + ( + TypeExpr::Generic(left, left_args, left_span), + TypeExpr::Generic(right, right_args, right_span), + ) => left_span == right_span && ident(left, right) && types(left_args, right_args), + (TypeExpr::View(left, left_span), TypeExpr::View(right, right_span)) => { + left_span == right_span && ty(left, right) + } + ( + TypeExpr::Function(left, left_result, left_span), + TypeExpr::Function(right, right_result, right_span), + ) => left_span == right_span && types(left, right) && ty(left_result, right_result), + ( + TypeExpr::StateQualified(left, left_state, left_span), + TypeExpr::StateQualified(right, right_state, right_span), + ) => left_span == right_span && ty(left, right) && ident(left_state, right_state), + _ => false, + } + } + left.span == right.span + && ident(&left.name, &right.name) + && left.exported == right.exported + && left.body.span == right.body.span + && left.type_params.len() == right.type_params.len() + && left + .type_params + .iter() + .zip(&right.type_params) + .all(|(left, right)| ident(left, right)) + && left.params.len() == right.params.len() + && left.params.iter().zip(&right.params).all(|(left, right)| { + left.span == right.span + && left.view == right.view + && left.mutable == right.mutable + && ident(&left.name, &right.name) + && ty(&left.ty, &right.ty) + }) + && match (&left.return_type, &right.return_type) { + (None, None) => true, + (Some(left), Some(right)) => ty(left, right), + _ => false, + } +} + +impl Interpreter { + pub(super) fn check_resource_cleanup(&mut self) -> Result<(), String> { + match &mut self.resource_transport { + Some(transport) => transport.check_cleanup().map_err(|error| error.to_string()), + None => Ok(()), + } + } + + pub(super) fn prepare_checked_resource_field_loop( + &self, + source: &jett_parser::ast::ForStmt, + ) -> Result>, String> { + let Some(transport) = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + else { + return self.prepare_checked_field_reflection_loop(source); + }; + transport + .checked + .prepare_reflected_field_loop(source) + .map_err(|error| error.to_string()) + } + + pub(super) fn exec_checked_resource_type_bind( + &mut self, + bind: &jett_parser::ast::ComptimeTypeBindStmt, + ) -> Result, String> { + // Every fallible source/type/body join precedes interpreter mutation. + let checked = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked; + let prepared = match &bind.value { + Expr::FieldAccess(base, member, _) if member.name == "type_info" => { + let Expr::Ident(variable) = base.as_ref() else { + return Err("resource execution has no exact checked body".to_string()); + }; + let definition = checked + .program() + .resolved() + .resolutions + .get(&variable.span) + .ok_or("resource execution has no exact checked body")?; + let iteration = self + .checked_reflected_fields + .iter() + .rev() + .find(|proof| proof.variable() == *definition) + .ok_or("resource execution has no exact checked body")?; + let current = self + .get_variable(&variable.name) + .ok_or("resource execution has no exact checked body")?; + iteration + .validate_value(current) + .map_err(|error| error.to_string())?; + checked.prepare_reflected_field_scope(bind, iteration) + } + _ => checked.prepare_direct_scope(bind), + } + .map_err(|error| error.to_string())?; + let body = prepared.body().map_err(|error| error.to_string())?; + let bound_type_expr = + Self::simple_type_expr_from_name(&prepared.reflection().type_name, bind.value.span()) + .ok_or("checked scoped type has no canonical interpreter syntax")?; + let projection = prepared.projection(); + let binding = ClosureScopedTypeBinding { + name: bind.name.name.clone(), + canonical_name: prepared.bound_name().to_string(), + reflection: Some(prepared.reflection().clone()), + }; + let saved_cursor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .cursor(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_direct_scope(&prepared) + .map_err(|error| error.to_string())?; + let saved_field_cursor = + match self.enter_checked_field_body(Some(&prepared.field_reference())) { + Ok(saved) => saved, + Err(error) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .restore_cursor(saved_cursor) + .map_err(|restore| restore.to_string())?; + return Err(error); + } + }; + let saved_checked_scope = self.active_checked_scope.replace(projection); + self.scoped_type_bindings.push(binding); + self.type_arg_scopes + .push(HashMap::from([(bind.name.name.clone(), bound_type_expr)])); + let result = self.exec_block_inner(body); + self.type_arg_scopes.pop(); + self.scoped_type_bindings.pop(); + self.active_checked_scope = saved_checked_scope; + let cleanup = self.check_resource_cleanup(); + let restore = match &mut self.resource_transport { + Some(transport) => transport + .checked + .restore_cursor(saved_cursor) + .map_err(|error| error.to_string()), + None => Err("missing checked Resource transport".to_string()), + }; + let field_restore = self.restore_checked_field_body(saved_field_cursor); + match (result, cleanup, restore, field_restore) { + (_, Err(error), _, _) => Err(error), + (Err(error), Ok(()), _, _) + | (_, Ok(()), Err(error), _) + | (_, Ok(()), Ok(()), Err(error)) => Err(error), + (Ok(value), Ok(()), Ok(()), Ok(())) => Ok(value), + } + } + + fn resource_flow(value: EvaluatedValue) -> ExprFlow { + if value.custody.is_empty() && !value.value.contains_live_resource_or_grant() { + ExprFlow::Value(value.value) + } else { + ExprFlow::Resource(value) + } + } + + fn resource_envelope(flow: ExprFlow) -> Result, String> { + match flow { + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => { + Ok(Ok(EvaluatedValue::ordinary(value))) + } + ExprFlow::Value(_) => { + Err("physical Resource carrier has no owning or borrowed envelope".to_string()) + } + ExprFlow::Resource(value) => Ok(Ok(value)), + ExprFlow::Signal(signal) => Ok(Err(signal)), + } + } + + pub(super) fn resource_type_at(&self, span: Span) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let ty = transport + .checked + .expression_type(span) + .map_err(|error| error.to_string())?; + transport + .checked + .type_contains_resource(ty) + .map_err(|error| error.to_string()) + } + + pub(super) fn eval_resource_transport( + &mut self, + expression: &Expr, + ) -> Result, String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + if !transport.checked_source_active { + return Ok(None); + } + if matches!(expression, Expr::Call(..)) { + if let Some(flow) = self.eval_resource_owned_struct(expression)? { + return Ok(Some(flow)); + } + } + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + match expression { + Expr::Ident(identifier) => { + let definition = transport + .checked + .resolved_definition(identifier.span) + .map_err(|error| error.to_string())?; + if transport.checked.has_hook(definition) { + let descriptor = transport + .checked + .descriptor(definition) + .map_err(|error| error.to_string())?; + return Ok(Some(ExprFlow::Value(Value::ResourceHook(descriptor)))); + } + if transport.has_binding(definition) { + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .binding(definition, false) + .map_err(|error| error.to_string())?; + return Ok(Some(Self::resource_flow(value))); + } + if self.resource_type_at(identifier.span)? + && transport + .checked + .prepare_named_callable(expression) + .map_err(|error| error.to_string())? + .is_none() + { + return Err("Resource source binding has no live cleanup custody".to_string()); + } + Ok(None) + } + Expr::View(inner, _) => { + let bare = Self::unparenthesized(inner); + if let Expr::Ident(identifier) = bare { + let definition = transport + .checked + .resolved_definition(identifier.span) + .map_err(|error| error.to_string())?; + if transport.has_binding(definition) { + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Some(Self::resource_flow(value))); + } + } + // A written view of an owned producer retains one operation + // backing ticket; formal binding creates only the borrow. + let flow = self.eval_expr_flow(inner)?; + Ok(Some(flow)) + } + Expr::Paren(inner, _) => self.eval_expr_flow(inner).map(Some), + Expr::Ok(inner, _) | Expr::Fail(inner, _) | Expr::Some(inner, _) + if self.resource_type_at(expression.span())? => + { + let mut value = match Self::resource_envelope(self.eval_expr_flow(inner)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + let step = match expression { + Expr::Ok(..) => PayloadStep::Ok, + Expr::Fail(..) => PayloadStep::Fail, + Expr::Some(..) => PayloadStep::Some, + _ => return Err("invalid checked sum transport".to_string()), + }; + value = value.prefix(step.clone()); + value.value = match step { + PayloadStep::Ok => Value::ResultOk(Box::new(value.value)), + PayloadStep::Fail => Value::ResultFail(Box::new(value.value)), + PayloadStep::Some => Value::OptionalSome(Box::new(value.value)), + _ => return Err("invalid checked sum transport".to_string()), + }; + Ok(Some(Self::resource_flow(value))) + } + Expr::Handle(target, binding, body, _) if self.resource_type_at(target.span())? => { + let mut target = match Self::resource_envelope(self.eval_expr_flow(target)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + // Typed storage is a separate custody path; this operation does + // not erase nominal/secret policy. Checker-selected sum typing + // already establishes the exact occupied arm. + while let Value::Typed { value, .. } = target.value { + target.value = *value; + target + .remove_prefix(PayloadStep::Typed) + .map_err(|error| error.to_string())?; + } + match target.value { + Value::ResultOk(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Ok) + .map_err(|error| error.to_string())?; + Ok(Some(Self::resource_flow(target))) + } + Value::OptionalSome(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Some) + .map_err(|error| error.to_string())?; + Ok(Some(Self::resource_flow(target))) + } + Value::ResultFail(error) => { + if !target.custody.is_empty() { + return Err( + "Resource-bearing error custody has no selected handler transport" + .to_string(), + ); + } + self.exec_resource_handle(binding.as_ref(), Some(*error), body) + .map(Some) + } + Value::OptionalNone => { + if !target.custody.is_empty() { + return Err("absent Resource sum retained a live owner".to_string()); + } + self.exec_resource_handle(None, None, body).map(Some) + } + _ => Err("checked Resource handle has no exact sum carrier".to_string()), + } + } + Expr::Default(inner, _) if self.resource_type_at(inner.span())? => { + let mut value = match Self::resource_envelope(self.eval_expr_flow(inner)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(ExprFlow::Signal(signal))), + }; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .preserve_default(&mut value) + .map_err(|error| error.to_string())?; + Ok(Some(ExprFlow::Signal(Signal::ResourceDefault(value)))) + } + Expr::Comptime(inner, _) => { + let previous = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .replace_purpose(ExecutionPurpose::ExplicitComptime); + let result = self.eval_expr_flow(inner); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .replace_purpose(previous); + let flow = result?; + if matches!(&flow, ExprFlow::Resource(_)) { + return Err( + "required evaluation cannot produce live Resource custody".to_string() + ); + } + Ok(Some(flow)) + } + Expr::Pipeline(..) => self.eval_resource_pipeline(expression).map(Some), + // All other ordinary expressions remain the established evaluator. + // Its operand extraction rejects a transported Resource envelope. + _ => Ok(None), + } + } + + pub(super) fn exec_resource_binding( + &mut self, + declaration: &VarDecl, + ) -> Result, String> { + let value = match Self::resource_envelope(self.eval_expr_flow(&declaration.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(Some(signal)), + }; + // The checked source type and occupied payload paths remain unchanged; + // opaque carriers do not enter ordinary clone/retag normalization. + let physical = value.value.clone(); + let ty = self.substitute_type_expr(&declaration.ty); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_binding(declaration.name.span, value) + .map_err(|error| error.to_string())?; + self.set_variable_with_type(&declaration.name.name, physical, ty); + Ok(None) + } + + fn exec_resource_handle( + &mut self, + binding: Option<&Ident>, + value: Option, + body: &Block, + ) -> Result { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .enter_default() + .map_err(|error| error.to_string())?; + let result = self.exec_handle_block(binding, value, body, None); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .leave_default(); + result + } + + fn finish_resource_operation( + &mut self, + operation: OperationFrame, + result: Result, + ) -> Result { + // A handler Return already retired its exact acquired suffix before + // evaluating its operand. Its suspended envelopes acknowledge once; + // ordinary success and output adoption never consume that authority. + if matches!( + &result, + Err(_) + | Ok(ExprFlow::Signal( + Signal::Return(..) | Signal::ResourceReturn(..) + )) + ) && self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .acknowledge_return_operation(&operation) + .map_err(|error| error.to_string())? + { + return result; + } + let mut result = result; + let value = match &mut result { + Ok(ExprFlow::Resource(value)) => Some(value), + _ => None, + }; + let cleanup = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .end_operation(operation, value) + .map_err(|error| error.to_string()); + match (result, cleanup) { + (_, Err(error)) => Err(error), + (Err(error), Ok(())) => Err(error), + (Ok(flow), Ok(())) => Ok(flow), + } + } + + pub(super) fn eval_resource_call( + &mut self, + callee: &Expr, + type_arguments: &[TypeExpr], + arguments: &[CallArg], + span: Span, + ) -> Result { + let invocation = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .invocation(span) + .map_err(|error| error.to_string())?; + if invocation.arguments().len() != arguments.len() + || invocation + .arguments() + .iter() + .zip(arguments) + .any(|(fact, argument)| fact.source_span != argument.value.span()) + { + return Err("checked call lost its original source argument occurrence".to_string()); + } + let intrinsic = if matches!(invocation.target(), CheckedInvocationTarget::Intrinsic(_)) { + Some( + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_intrinsic_arguments(callee, type_arguments, arguments, &invocation) + .map_err(|error| error.to_string())?, + ) + } else { + None + }; + // Variant construction has a checked function-shaped packet, but its + // Source endpoint is not a runtime callable value. Prepare its exact + // nominal schema before evaluating any actuals. + let constructor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .enum_constructor(Self::unparenthesized(callee), &invocation) + .map_err(|error| error.to_string())?; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut actuals = Vec::with_capacity(arguments.len()); + for (argument, fact) in arguments.iter().zip(invocation.arguments()) { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_actual(&value, fact.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + self.dispatch_resource_actuals( + &invocation, + callee, + type_arguments, + actuals, + constructor, + operation.frame, + intrinsic.as_ref(), + ) + })(); + self.finish_resource_operation(operation, result) + } + + // Both routes provide original checked identity and already-evaluated + // source-order envelopes. This dispatcher never re-evaluates an actual. + fn dispatch_resource_actuals( + &mut self, + invocation: &CheckedInvocation, + callee: &Expr, + type_arguments: &[TypeExpr], + actuals: Vec, + constructor: Option<(String, String, jett_types::TypeId)>, + return_destination: FrameId, + intrinsic: Option<&PreparedIntrinsicArguments>, + ) -> Result { + if matches!(invocation.target(), CheckedInvocationTarget::Intrinsic(_)) + != intrinsic.is_some() + { + return Err("checked intrinsic lost its prepared concrete argument packet".to_string()); + } + if let Some(prepared) = intrinsic { + prepared + .validate( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + invocation, + ) + .map_err(|error| error.to_string())?; + } + if actuals.len() != invocation.arguments().len() { + return Err("checked call lost its evaluated source argument count".to_string()); + } + // Source lexical order is retained above. Ownership adoption below + // follows that same order even when destination parameters permute. + let mut formal: Vec> = (0..actuals.len()).map(|_| None).collect(); + for (actual, fact) in actuals.into_iter().zip(invocation.arguments()) { + let borrowed = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .formal_value(&actual, fact.callee_access) + .map_err(|error| error.to_string())?; + formal[fact.parameter_index] = Some(borrowed.unwrap_or(actual)); + } + let formal = formal + .into_iter() + .map(|value| value.ok_or("checked call has no exact parameter value".to_string())) + .collect::, _>>()?; + if let Some((type_name, variant, ty)) = constructor { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err( + "occupied Resource enum construction has no selected custody transport" + .to_string(), + ); + } + let value = EvaluatedValue::ordinary(Value::Enum { + type_name, + variant, + fields: formal.into_iter().map(|value| value.value).collect(), + }); + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, ty) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(value)); + } + let bare_callee = Self::unparenthesized(callee); + match invocation.target() { + CheckedInvocationTarget::Resolved(definition) + if self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .has_hook(*definition) => + { + let resolved = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .resolved_definition(bare_callee.span()) + .map_err(|error| error.to_string())?; + if resolved != *definition { + return Err("checked Resource hook lost its exact declaration".to_string()); + } + let descriptor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .descriptor(*definition) + .map_err(|error| error.to_string())?; + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .invoke_hook(invocation, &descriptor, formal) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + } + CheckedInvocationTarget::Resolved(definition) + | CheckedInvocationTarget::Generic(jett_typecheck::CheckedGenericCall { + definition, + .. + }) => { + let name = self.exact_registered_resource_function(*definition)?; + let resolved_arguments = match invocation.target() { + CheckedInvocationTarget::Generic(call) => call + .concrete_args + .iter() + .map(|ty| { + let types = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .program() + .checked() + .interner; + Self::debug_type(&types.type_name(*ty)).ok_or( + "checked generic argument has no canonical interpreter type" + .to_string(), + ) + }) + .collect::, String>>()?, + _ => type_arguments + .iter() + .map(|ty| self.substitute_type_expr(ty)) + .collect(), + }; + let value = self.call_registered_function_envelopes( + &name, + &resolved_arguments, + formal, + &FunctionInvocation::Source(invocation), + return_destination, + )?; + Ok(Self::resource_flow(value)) + } + CheckedInvocationTarget::Indirect(_) => { + let function = match Self::resource_envelope(self.eval_expr_flow(bare_callee)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + match function.value { + Value::ResourceHook(descriptor) if function.custody.is_empty() => { + let value = self.resource_transport.as_mut().ok_or("missing checked Resource transport")? + .invoke_hook(invocation, &descriptor, formal).map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + } + Value::NamedFunction(name) if function.custody.is_empty() + && (formal.iter().any(|argument| !argument.custody.is_empty() || argument.value.contains_live_resource_or_grant()) + || self.resource_type_at(bare_callee.span())?) => { + let target = self.resource_transport.as_ref().ok_or("missing checked Resource transport")? + .checked.prepare_named_indirect(invocation, bare_callee).map_err(|error| error.to_string())?; + let expected = self.exact_registered_resource_function(target.definition())?; + if name != expected { + return Err("named Resource callable differs from its exact original target".to_string()); + } + let value = self.call_registered_function_envelopes( + &expected, &[], formal, + &FunctionInvocation::NamedSource { source: invocation, target: &target }, + return_destination, + )?; + Ok(Self::resource_flow(value)) + } + value if function.custody.is_empty() && !value.contains_live_resource_or_grant() + && formal.iter().all(|argument| argument.custody.is_empty() && !argument.value.contains_live_resource_or_grant()) => { + Ok(ExprFlow::Value(self.call_fn_value_from_source(value, formal.into_iter().map(|value| value.value).collect(), None)?)) + } + _ => Err("Resource-bearing captured invocation has no checked custody transfer proof".to_string()), + } + } + CheckedInvocationTarget::Intrinsic(_) => { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err( + "Resource-bearing intrinsic transport has no selected acquisition proof" + .to_string(), + ); + } + let prepared = + intrinsic.ok_or("checked intrinsic has no prepared concrete arguments")?; + let values = formal.into_iter().map(|value| value.value).collect(); + Ok(ExprFlow::Value( + self.call_resource_intrinsic(invocation, prepared, values)?, + )) + } + CheckedInvocationTarget::Method(_) | CheckedInvocationTarget::Interface(_) => { + if formal.iter().any(|value| { + !value.custody.is_empty() || value.value.contains_live_resource_or_grant() + }) { + return Err("Resource-bearing intrinsic/method transport has no selected acquisition proof".to_string()); + } + // Evaluate the selected ordinary operation with the already + // evaluated actual values. No source actual is evaluated twice. + let values = formal + .into_iter() + .map(|value| value.value) + .collect::>(); + let name = Self::dotted_expr_name(bare_callee) + .ok_or("ordinary checked intrinsic has no source operation")?; + Ok(ExprFlow::Value(self.call_function_from_resolved_source( + &name, + type_arguments, + values, + None, + )?)) + } + } + } + + fn exact_registered_resource_function(&self, definition: DefId) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let original = transport + .checked + .retained_function(definition) + .map_err(|error| error.to_string())?; + let (namespace, _) = transport + .checked + .source_function_context(original.name.span) + .map_err(|error| error.to_string())?; + let mut matches = Vec::new(); + for (name, registered) in &self.functions { + if transport + .checked + .declaration_definition(registered.definition.name.span) + .ok() + != Some(definition) + { + continue; + } + if registered.namespace != namespace + || !same_registered_function_header(®istered.definition, original) + { + return Err( + "checked registration differs from its retained source declaration".to_string(), + ); + } + matches.push(name.clone()); + } + let [name] = matches.as_slice() else { + return Err("checked invocation has no exact retained source function".to_string()); + }; + Ok(name.clone()) + } +} + +impl Interpreter { + fn call_registered_function_envelopes( + &mut self, + name: &str, + type_args: &[TypeExpr], + args: Vec, + invocation: &FunctionInvocation, + return_destination: FrameId, + ) -> Result { + let resolved_name = name.to_string(); + let physical_arguments = args + .iter() + .map(|argument| argument.value.clone()) + .collect::>(); + + // Look up the function definition. + let registered = self + .functions + .get(&resolved_name) + .ok_or_else(|| { + let name = self + .registry_name(&self.interface_methods, name) + .unwrap_or_else(|| name.to_string()); + format!("undefined function '{name}'") + })? + .clone(); + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let definition = match invocation { + FunctionInvocation::Entry { definition, .. } => *definition, + FunctionInvocation::NamedSource { target, .. } => target.definition(), + FunctionInvocation::Source(invocation) => match invocation.target() { + CheckedInvocationTarget::Resolved(definition) => *definition, + CheckedInvocationTarget::Generic(call) => call.definition, + _ => { + return Err("checked source call has no exact retained declaration".to_string()); + } + }, + }; + let original = transport + .checked + .retained_function(definition) + .map_err(|error| error.to_string())?; + let (namespace, trusted_stdlib) = transport + .checked + .source_function_context(original.name.span) + .map_err(|error| error.to_string())?; + if transport + .checked + .declaration_definition(registered.definition.name.span) + .map_err(|error| error.to_string())? + != definition + || registered.namespace != namespace + || !same_registered_function_header(®istered.definition, original) + { + return Err( + "checked registration differs from its retained source declaration".to_string(), + ); + } + // The original checked AST supplies both header and executable body. + // A host-edited registration clone is never executable authority. + let body_reference = transport + .checked + .prepare_function_body(invocation) + .map_err(|error| error.to_string())?; + let func = body_reference + .function() + .map_err(|error| error.to_string())?; + + if args.len() != func.params.len() { + return Err(format!( + "function '{}' expects {} argument(s), got {}", + resolved_name, + func.params.len(), + args.len() + )); + } + + let type_scope = self.type_scope_for_function(&func, type_args)?; + let arguments: Vec = func + .type_params + .iter() + .map(|param| type_scope[¶m.name].clone()) + .collect(); + let captured_arguments = self.captured_type_arguments(&arguments); + let expression_types = match &self.checked_expression_types { + Some(types) => types + .select(func.name.span, &captured_arguments, &physical_arguments) + .map_err(|error| format!("{resolved_name}: {error}"))?, + None => None, + }; + let saved_resource_cursor = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .cursor(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_function_body(&body_reference) + .map_err(|error| error.to_string())?; + let saved_field_cursor = match self.enter_checked_field_body(Some(&body_reference)) { + Ok(saved) => saved, + Err(error) => { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .restore_cursor(saved_resource_cursor) + .map_err(|restore| restore.to_string())?; + return Err(error); + } + }; + let saved_expression_types = + std::mem::replace(&mut self.active_checked_function, expression_types); + let saved_type_arguments = std::mem::replace(&mut self.current_type_arguments, arguments); + let saved_scoped_types = std::mem::take(&mut self.scoped_type_bindings); + let saved_checked_scope = self.active_checked_scope.take(); + let saved_reflected_fields = std::mem::take(&mut self.checked_reflected_fields); + // Resolve arguments above in the caller, then keep its type bindings + // out of the callee's lexical scope, including non-generic callees. + let saved_type_scopes = std::mem::replace(&mut self.type_arg_scopes, vec![type_scope]); + + let saved_namespace = self.current_namespace.clone(); + let saved_trusted_stdlib = self.current_function_trusted_stdlib; + self.current_namespace = namespace; + self.current_function_trusted_stdlib = trusted_stdlib; + + let scope_depth = self.scopes.len(); + let saved_scope_floor = self.lexical_scope_floor; + self.lexical_scope_floor = scope_depth; + self.push_scope(); + let saved_proofs = self.allow_checked_refinement_proofs; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .enter_return(return_destination); + let call_result = (|| { + let mut arguments = args.into_iter().map(Some).collect::>(); + // Install each formal holder in SOURCE acquisition order. Formal + // permutation affects lookup only, never reverse cleanup ordering. + let contracts = invocation + .parameters( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + ) + .map_err(|error| error.to_string())?; + for fact in &contracts { + let index = fact.index; + let param = func + .params + .get(index) + .ok_or("checked call has no exact source parameter")?; + if param.view != (fact.access == CheckedCalleeAccess::View) { + return Err( + "checked source parameter access differs from retained declaration" + .to_string(), + ); + } + let mut arg = arguments + .get_mut(index) + .and_then(Option::take) + .ok_or("checked call repeated a formal value")?; + let param_ty = self.substitute_type_expr(¶m.ty); + if arg.value.contains_live_resource_or_grant() + || !arg.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&arg, fact.ty) + .map_err(|error| error.to_string())?; + } else { + let source_type = match invocation { + FunctionInvocation::Source(call) + | FunctionInvocation::NamedSource { source: call, .. } => call + .arguments() + .iter() + .find(|argument| argument.parameter_index == index) + .map(|argument| { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport") + .map(|transport| { + transport + .checked + .program() + .checked() + .interner + .type_name(argument.actual_type) + }) + }) + .transpose()?, + FunctionInvocation::Entry { .. } => None, + }; + arg.value = self.normalize_and_validate_value_from_source( + ¶m_ty, + arg.value, + source_type.as_deref(), + )?; + } + let physical = arg.value.clone(); + if arg.value.contains_live_resource_or_grant() + || !arg.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(fact.ty) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_binding(param.name.span, arg) + .map_err(|error| error.to_string())?; + } + self.set_variable_with_type(¶m.name.name, physical, param_ty); + self.install_checked_field_binding(¶m.name)?; + } + + let result = self.exec_block_inner(&func.body)?; + let (mut value, return_source_type) = match result { + Some(Signal::ResourceReturn(value, source_type)) => (value, source_type), + Some(Signal::Return(value, source_type)) => { + (EvaluatedValue::ordinary(value), source_type) + } + Some(Signal::Default(_) | Signal::ResourceDefault(_)) => { + return Err("`default` can only be used inside a `handle` block".to_string()); + } + _ => (EvaluatedValue::ordinary(Value::Nothing), None), + }; + if let Some(return_type) = &func.return_type { + let return_type = self.substitute_type_expr(return_type); + let signature_type = invocation + .signature( + &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked, + ) + .map_err(|error| error.to_string())?; + let Type::Function { + return_type: checked_return, + .. + } = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .program() + .checked() + .interner + .resolve(signature_type) + else { + return Err("source function has no exact checked result type".to_string()); + }; + let checked_return = *checked_return; + if value.value.contains_live_resource_or_grant() + || !value.custody.is_empty() + || self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .type_contains_resource(checked_return) + .map_err(|error| error.to_string())? + { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, checked_return) + .map_err(|error| error.to_string())?; + } else { + value.value = self.normalize_and_validate_value_from_source( + &return_type, + value.value, + return_source_type.as_deref(), + )?; + } + } + + Ok(value) + })(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .leave_return(); + self.allow_checked_refinement_proofs = saved_proofs; + while self.scopes.len() > scope_depth { + self.pop_scope(); + } + self.lexical_scope_floor = saved_scope_floor; + self.active_checked_scope = saved_checked_scope; + self.checked_reflected_fields = saved_reflected_fields; + self.scoped_type_bindings = saved_scoped_types; + self.type_arg_scopes = saved_type_scopes; + self.current_type_arguments = saved_type_arguments; + self.active_checked_function = saved_expression_types; + self.current_namespace = saved_namespace; + self.current_function_trusted_stdlib = saved_trusted_stdlib; + let cleanup = self.check_resource_cleanup(); + let restore = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .restore_cursor(saved_resource_cursor) + .map_err(|error| error.to_string()); + let field_restore = self.restore_checked_field_body(saved_field_cursor); + match (call_result, cleanup, restore, field_restore) { + (_, Err(error), _, _) => Err(error), + (Err(error), Ok(()), _, _) + | (_, Ok(()), Err(error), _) + | (_, Ok(()), Ok(()), Err(error)) => Err(error), + (Ok(value), Ok(()), Ok(()), Ok(())) => Ok(value), + } + } +} + +impl Interpreter { + /// Install one compiler-derived snapshot. Production providers remain disabled. + /// Debug emission is configured independently and grants no runtime purpose. + pub fn from_checked_resource_program( + program: Arc, + purpose: ExecutionPurpose, + ) -> Result { + let mut interpreter = Self::new(); + interpreter.register_module(program.module()); + interpreter.install_checked_resource_program(program, purpose)?; + Ok(interpreter) + } + + pub fn install_checked_resource_program( + &mut self, + program: Arc, + purpose: ExecutionPurpose, + ) -> Result<(), String> { + if self.retained_resource_program.is_some() { + return Err("checked Resource program is already installed".to_string()); + } + let field_context = crate::interpreter::field_updates::FieldUpdateContext::new( + program.clone(), + purpose, + self.scopes.len(), + )?; + if !program.checked().resource_hooks.is_empty() { + self.resource_transport = Some( + ResourceTransport::checked_only(program.clone(), purpose, self.scopes.len()) + .map_err(|error| error.to_string())?, + ); + } + self.temporary_local_views = + crate::interpreter::temporary_local_views::Context::needed(&program).then(|| { + crate::interpreter::temporary_local_views::Context::new( + program.clone(), + self.scopes.len(), + ) + }); + self.retained_resource_program = Some((program, purpose)); + self.field_update_context = field_context; + Ok(()) + } + + pub(crate) fn authorize_checked_resource_worker( + &mut self, + module: &Module, + purpose: ExecutionPurpose, + ) -> Result<(), String> { + let (program, selected) = self + .retained_resource_program + .as_ref() + .ok_or("worker has no retained checked program")?; + if purpose == ExecutionPurpose::ReferenceRuntime + || *selected != purpose + || !std::ptr::eq(module, program.module()) + { + return Err( + "required worker has no exact retained source identity and purpose".to_string(), + ); + } + if let Some(transport) = &mut self.resource_transport { + transport.checked_source_active = true; + } + Ok(()) + } + + pub(crate) fn checked_execution_purpose( + &mut self, + purpose: ExecutionPurpose, + ) -> Option { + let (_, current) = self.retained_resource_program.as_mut()?; + let previous = std::mem::replace(current, purpose); + if let Some(transport) = &mut self.resource_transport { + transport.checked.replace_purpose(purpose); + } + if let Some(context) = &mut self.field_update_context { + context.replace_purpose(purpose); + } + Some(previous) + } + + /// Checked entry is an exact declaration, not a source-name hook dispatcher. + /// Incoming raw live Resource carriers are never accepted as owning inputs. + pub fn call_checked_program_entry( + &mut self, + definition: DefId, + arguments: Vec, + ) -> Result { + if arguments.iter().any(|argument| { + matches!(argument, Value::Resource(_)) + || (argument.contains_live_resource_or_grant() + && !matches!(argument, Value::GrantedNetwork(_))) + }) { + return Err( + "checked entry cannot adopt custody from a copied Resource carrier".to_string(), + ); + } + let transport = self + .resource_transport + .as_ref() + .ok_or("checked entry has no retained program")?; + if transport.checked.purpose() != ExecutionPurpose::ReferenceRuntime + && arguments.iter().any(Value::contains_live_resource_or_grant) + { + return Err( + "required worker cannot import live runtime authority or Resource custody" + .to_string(), + ); + } + transport + .validate_entry_scopes(self.scopes.len()) + .map_err(|error| error.to_string())?; + let invocation = transport + .checked + .entry(definition) + .map_err(|error| error.to_string())?; + let name = self.exact_registered_resource_function(definition)?; + let saved_context = SavedResourceEntryContext::capture(self)?; + let scope_depth = saved_context.scope_depth(); + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked_source_active = true; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let arguments = arguments + .into_iter() + .map(EvaluatedValue::ordinary) + .collect(); + let value = self.call_registered_function_envelopes( + &name, + &[], + arguments, + &invocation, + operation.frame, + )?; + let flow = self.finish_resource_operation(operation, Ok(Self::resource_flow(value)))?; + match flow { + ExprFlow::Value(value) if !value.contains_live_resource_or_grant() => Ok(value), + _ => Err( + "checked entry cannot export live custody through a raw Value result" + .to_string(), + ), + } + })); + let cleanup = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .unwind_all() + .map_err(|error| error.to_string()) + })); + // A panic may skip ordinary callee/scope restoration. Restore the exact + // entry metadata before choosing/resuming any result or cleanup outcome. + let restore = saved_context.restore(self); + let completion = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .restore_entry_scopes(scope_depth) + .map_err(|error| error.to_string()) + })); + let cleanup = Self::complete_resource_cleanup(cleanup, completion); + let result = match (result, restore) { + (Ok(Ok(_)), Err(error)) => Ok(Err(error)), + (result, _) => result, + }; + Self::complete_resource_entry(result, cleanup) + } + + fn complete_resource_cleanup( + cleanup: std::thread::Result>, + completion: std::thread::Result>, + ) -> std::thread::Result> { + match (cleanup, completion) { + (Err(panic), completion) => { + if let Err(other) = completion { + std::mem::forget(other); + } + Err(panic) + } + (Ok(Err(error)), completion) => { + if let Err(panic) = completion { + std::mem::forget(panic); + } + Ok(Err(error)) + } + (Ok(Ok(())), completion) => completion, + } + } + + fn complete_resource_entry( + result: std::thread::Result>, + cleanup: std::thread::Result>, + ) -> Result { + match (result, cleanup) { + (Err(entry_panic), Err(cleanup_panic)) => { + std::mem::forget(entry_panic); + std::panic::resume_unwind(cleanup_panic) + } + (Ok(_), Err(cleanup_panic)) => std::panic::resume_unwind(cleanup_panic), + (Err(entry_panic), Ok(Err(cleanup_error))) => { + std::mem::forget(entry_panic); + Err(cleanup_error) + } + (Ok(_), Ok(Err(cleanup_error))) => Err(cleanup_error), + (Err(entry_panic), Ok(Ok(()))) => std::panic::resume_unwind(entry_panic), + (Ok(result), Ok(Ok(()))) => result, + } + } +} + +#[cfg(test)] +impl Interpreter { + pub(crate) fn install_resource_test_script( + &mut self, + operations: Vec, + ) -> Result { + let grant = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .install_script(operations) + .map_err(|error| error.to_string())?; + Ok(Value::GrantedNetwork(grant)) + } + pub(crate) fn resource_test_scoped_context( + &self, + ) -> Result<(bool, usize, usize, usize, Option), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(( + self.active_checked_scope.is_some(), + self.scoped_type_bindings.len(), + self.current_type_arguments.len(), + self.type_arg_scopes.len(), + transport.checked.original_body_scope_depth(), + )) + } + pub(crate) fn resource_test_custody_counts(&self) -> Result<(usize, usize), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok((transport.live_owners(), transport.registry_live_count())) + } + pub(crate) fn resource_test_observations( + &self, + ) -> Result<(Vec, usize, usize), String> { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(( + transport + .provider_events() + .map_err(|error| error.to_string())?, + transport.live_owners(), + transport.registry_live_count(), + )) + } +} + +#[cfg(test)] +mod completion_tests { + use super::*; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + #[derive(Clone, Copy)] + enum Outcome { + Success, + Error, + Panic, + } + + #[test] + fn checked_entry_completion_selects_cleanup_before_every_entry_outcome() { + for entry in [Outcome::Success, Outcome::Error, Outcome::Panic] { + for cleanup in [Outcome::Success, Outcome::Error, Outcome::Panic] { + let entry_result: std::thread::Result> = match entry { + Outcome::Success => Ok(Ok(Value::Nothing)), + Outcome::Error => Ok(Err("entry sentinel".to_string())), + Outcome::Panic => Err(Box::new("entry panic")), + }; + let cleanup_result: std::thread::Result> = match cleanup { + Outcome::Success => Ok(Ok(())), + Outcome::Error => Ok(Err("cleanup sentinel".to_string())), + Outcome::Panic => Err(Box::new("cleanup panic")), + }; + let completed = catch_unwind(AssertUnwindSafe(|| { + Interpreter::complete_resource_entry(entry_result, cleanup_result) + })); + match (entry, cleanup) { + (_, Outcome::Panic) => { + let panic = completed.expect_err("cleanup panic must win"); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"cleanup panic")); + } + (_, Outcome::Error) => assert_eq!( + completed.expect("cleanup error is ordinary"), + Err("cleanup sentinel".to_string()) + ), + (Outcome::Panic, Outcome::Success) => { + let panic = completed.expect_err("clean cleanup preserves the entry panic"); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"entry panic")); + } + (Outcome::Error, Outcome::Success) => assert_eq!( + completed.expect("entry error is ordinary"), + Err("entry sentinel".to_string()) + ), + (Outcome::Success, Outcome::Success) => { + assert_eq!(completed.unwrap().unwrap(), Value::Nothing) + } + } + } + } + } +} + +#[cfg(test)] +mod declaration_registration_tests { + use super::*; + use crate::resource_execution::CheckedExecution; + use jett_common::FileId; + + const QUALIFIED: &str = r#"namespace helper +export function answer() returns int64: + return 7 +namespace other +export function scenario() returns int64: + return 99 +namespace app +export function scenario() returns int64: + use helper + return helper.answer() +"#; + + const MUTUAL: &str = r#"namespace app +mutual: + function first() returns int64 + function second() returns int64 +function first() returns int64: + return second() +function second() returns int64: + return 11 +export function scenario() returns int64: + return first() +"#; + + fn target( + program: &Arc, + namespace: &str, + name: &str, + ) -> DefId { + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let definitions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + let definition = checked.declaration_definition(function.name.span).unwrap(); + (program.resolved().scope_table.definitions[definition.index() as usize] + .namespace + .as_deref() + == Some(namespace)) + .then_some(definition) + } + _ => None, + }) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("unique exact retained test entry"); + }; + *definition + } + + fn runtime(source: &str, release: bool) -> (Interpreter, DefId) { + let program = crate::resource_execution::tests::program(source, release); + let definition = target(&program, "app", "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + (interpreter, definition) + } + + fn empty_before_teardown(interpreter: &mut Interpreter) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + + #[test] + fn checked_registration_calls_original_qualified_and_mutual_bodies() { + for release in [false, true] { + for (source, result) in [(QUALIFIED, 7), (MUTUAL, 11)] { + let (mut interpreter, definition) = runtime(source, release); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(result) + ); + empty_before_teardown(&mut interpreter); + } + } + } + + #[test] + fn checked_registration_refuses_namespace_header_missing_wrong_and_duplicate_identity() { + for release in [false, true] { + for corruption in 0..5 { + let (mut interpreter, definition) = runtime(QUALIFIED, release); + match corruption { + 0 => { + interpreter + .functions + .get_mut("app.scenario") + .unwrap() + .namespace = Some("other".to_string()) + } + 1 => { + let registered = interpreter.functions.get_mut("app.scenario").unwrap(); + let function = Arc::make_mut(&mut registered.definition); + let Some(TypeExpr::Named(result)) = &mut function.return_type else { + panic!("named source result"); + }; + result.name = "string".to_string(); + } + 2 => { + interpreter.functions.remove("app.scenario"); + } + 3 => { + let wrong = interpreter.functions.get("other.scenario").unwrap().clone(); + interpreter + .functions + .insert("app.scenario".to_string(), wrong); + } + 4 => { + let duplicate = interpreter.functions.get("app.scenario").unwrap().clone(); + interpreter + .functions + .insert("host.duplicate".to_string(), duplicate); + } + _ => unreachable!(), + } + let error = interpreter + .call_checked_program_entry(definition, Vec::new()) + .expect_err("malformed registration must refuse before provider activity"); + assert_eq!( + error, + if corruption < 2 { + "checked registration differs from its retained source declaration" + } else { + "checked invocation has no exact retained source function" + } + ); + empty_before_teardown(&mut interpreter); + } + } + } + + #[test] + fn checked_registration_generic_body_uses_accepted_concrete_selection() { + const SOURCE: &str = r#"namespace app +function echo[T](value: T) returns T: + return value +export function scenario() returns int64: + return echo[int64](7) +"#; + for release in [false, true] { + let (mut interpreter, definition) = runtime(SOURCE, release); + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + empty_before_teardown(&mut interpreter); + } + } + + #[test] + fn checked_registration_cloned_body_cannot_replace_retained_executable_source() { + for release in [false, true] { + let (mut interpreter, definition) = runtime(QUALIFIED, release); + let registered = interpreter.functions.get_mut("helper.answer").unwrap(); + let function = Arc::make_mut(&mut registered.definition); + let Stmt::Return(result) = &mut function.body.stmts[0] else { + panic!("original return"); + }; + let original_span = result.value.as_ref().unwrap().span(); + result.value = Some(Expr::IntLiteral(99, original_span)); + // Header/body spans are unchanged; executable authority remains the + // exact retained program rather than this host-edited clone. + assert_eq!( + interpreter + .call_checked_program_entry(definition, Vec::new()) + .unwrap(), + Value::Int64(7) + ); + empty_before_teardown(&mut interpreter); + } + } +} + +#[cfg(test)] +mod entry_readiness_tests { + use super::*; + use crate::resource_execution::{ + ProviderEvent, ResourceExecutionError, ScriptOperation, tests::program, + }; + use jett_types::ResourceHookKind; + + #[test] + fn checked_host_entry_refusal_preserves_an_outer_operation_and_real_owner() { + for release in [false, true] { + let program = program( + include_str!("fixtures/25_repeated_scoped_entry.jett"), + release, + ); + let mut interpreter = Interpreter::from_checked_resource_program( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "clean" + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let target = interpreter + .resource_transport + .as_ref() + .unwrap() + .checked + .declaration_definition(original.name.span) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(21), + }, + ]) + .unwrap(); + let Value::GrantedNetwork(network) = &grant else { + panic!("actual private grant"); + }; + let runtime = interpreter.resource_transport.as_mut().unwrap(); + runtime.checked_source_active = true; + let operation = runtime.begin_operation().unwrap(); + let hook = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let occurrence = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(hook)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(occurrence).unwrap(); + let descriptor = runtime.checked.descriptor(hook).unwrap(); + let owner = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(network.clone())), + EvaluatedValue::ordinary(Value::Int64(1801)), + ], + ) + .unwrap(); + assert!(!owner.custody.is_empty()); + let context = interpreter.resource_test_entry_context().unwrap(); + assert_eq!( + interpreter.call_checked_program_entry(target, vec![grant.clone()]), + Err(ResourceExecutionError::InvalidFrame.to_string()) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (vec![ProviderEvent::Constructed(1801)], 1, 1) + ); + interpreter + .resource_transport + .as_mut() + .unwrap() + .end_operation(operation, None) + .unwrap(); + drop(owner); + interpreter + .resource_transport + .as_mut() + .unwrap() + .checked_source_active = false; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(21) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801), + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs new file mode 100644 index 00000000..7ef81da4 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/absent_generated.rs @@ -0,0 +1,104 @@ +//! The ordinary generated variable/debug copy carries no cleanup authority. +use super::*; +use crate::resource_execution::PreparedAbsentBindings; +use jett_parser::ast::{ForStmt, MatchStmt}; + +impl Interpreter { + pub(in crate::interpreter) fn eval_checked_absent_for_iterable( + &mut self, + source: &ForStmt, + ) -> Result { + if source.view { + if let Some(transport) = self + .resource_transport + .as_mut() + .filter(|transport| transport.checked_source_active) + { + // Authenticate the original For before using its stripped view spelling. + if transport + .checked + .prepare_absent_for(source) + .map_err(|error| error.to_string())? + .is_some() + { + if let Expr::Ident(name) = Self::unparenthesized(&source.iterable) { + let definition = transport + .checked + .resolved_definition(name.span) + .map_err(|error| error.to_string())?; + if transport.has_binding(definition) { + let borrowed = transport + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(borrowed)); + } + } + } + } + } + self.eval_expr_flow(&source.iterable) + } + + pub(in crate::interpreter) fn prepare_absent_for_bindings<'source>( + &self, + source: &'source ForStmt, + value: &Value, + ) -> Result>, String> { + match &self.resource_transport { + Some(transport) => transport + .prepare_absent_for(source, value) + .map_err(|error| error.to_string()), + None => Ok(None), + } + } + + pub(in crate::interpreter) fn prepare_absent_match_bindings<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + value: &Value, + ) -> Result>, String> { + match &self.resource_transport { + Some(transport) => transport + .prepare_absent_match(source, arm, value) + .map_err(|error| error.to_string()), + None => Ok(None), + } + } + + pub(in crate::interpreter) fn set_absent_generated_debug_binding( + &mut self, + proof: Option<&PreparedAbsentBindings<'_>>, + name: &Ident, + ordinal: usize, + value: Value, + fallback: Option<&TypeExpr>, + ) -> Result<(), String> { + if let Some(proof) = proof { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .publish_absent_binding(proof, name, ordinal, value.clone()) + .map_err(|error| error.to_string())?; + } + self.set_inferred_debug_binding(name, value, fallback); + Ok(()) + } + + pub(in crate::interpreter) fn validate_absent_generated_debug_binding( + &self, + proof: Option<&PreparedAbsentBindings<'_>>, + name: &Ident, + ordinal: usize, + value: &Value, + ) -> Result<(), String> { + if let Some(proof) = proof { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_absent_binding(proof, name, ordinal, value) + .map_err(|error| error.to_string())?; + } + Ok(()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs new file mode 100644 index 00000000..8559f4d6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/assignment.rs @@ -0,0 +1,60 @@ +//! Evaluate an owning replacement once, after preparing its exact lexical slot. +use super::*; +use jett_parser::ast::AssignStmt; + +impl Interpreter { + pub(in crate::interpreter) fn exec_resource_assignment( + &mut self, + source: &AssignStmt, + ) -> Result, String> { + let destination = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .prepare_resource_assignment(source) + .map_err(|error| error.to_string())?; + let scope_index = destination.scope_index(); + let Expr::Ident(target) = &source.target else { + return Err("Resource assignment has no original binding target".to_string()); + }; + if scope_index < self.lexical_scope_floor + || !self + .scopes + .get(scope_index) + .is_some_and(|scope| scope.contains_key(&target.name)) + { + return Err("Resource assignment has no exact lexical storage slot".to_string()); + } + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let value = match Self::resource_envelope(self.eval_expr_flow(&source.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + // Physical mirroring carries no owning custody and deliberately + // bypasses ordinary clone/retag/refinement normalization. + let physical = value.value.clone(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .replace_resource_assignment(destination, value) + .map_err(|error| error.to_string())?; + let scope = self + .scopes + .get_mut(scope_index) + .ok_or("Resource assignment lost its exact lexical storage slot")?; + scope.insert(target.name.clone(), physical); + Ok(ExprFlow::Value(Value::Nothing)) + })(); + match self.finish_resource_operation(operation, result)? { + ExprFlow::Value(Value::Nothing) => Ok(None), + ExprFlow::Signal(signal) => Ok(Some(signal)), + _ => Err("Resource assignment returned an unexpected expression flow".to_string()), + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs new file mode 100644 index 00000000..e727ce12 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/entry_context.rs @@ -0,0 +1,174 @@ +//! Metadata restoration at the checked top-level entry boundary. +//! This snapshot contains no Value, Environment or cleanup ticket. +use super::*; +use crate::resource_execution::CheckedBodyCursor; + +pub(super) struct SavedResourceEntryContext { + program: Arc, + retained_purpose: ExecutionPurpose, + checked_purpose: ExecutionPurpose, + source_active: bool, + cursor: CheckedBodyCursor, + scope_depth: usize, + variable_depth: usize, + alias_depth: usize, + namespace: Option, + trusted: bool, + floor: usize, + proofs: bool, + checked_function: Option>, + checked_scope: Option>, + arguments: Vec, + type_scopes: Vec>, + scoped_bindings: Vec, + fields: Vec>, + checked_fields: Vec, + type_infos: Vec>, + variants: Vec>, + states: Vec>, +} + +impl SavedResourceEntryContext { + pub(super) fn capture(interpreter: &Interpreter) -> Result { + let transport = interpreter + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + let (program, retained_purpose) = interpreter + .retained_resource_program + .as_ref() + .ok_or("checked entry has no retained program")?; + if !Arc::ptr_eq(program, transport.checked.program()) { + return Err("checked entry context belongs to another retained program".to_string()); + } + Ok(Self { + program: program.clone(), + retained_purpose: *retained_purpose, + checked_purpose: transport.checked.purpose(), + source_active: transport.checked_source_active, + cursor: transport.cursor(), + scope_depth: interpreter.scopes.len(), + variable_depth: interpreter.variable_type_scopes.len(), + alias_depth: interpreter.namespace_alias_scopes.len(), + namespace: interpreter.current_namespace.clone(), + trusted: interpreter.current_function_trusted_stdlib, + floor: interpreter.lexical_scope_floor, + proofs: interpreter.allow_checked_refinement_proofs, + checked_function: interpreter.active_checked_function.clone(), + checked_scope: interpreter.active_checked_scope.clone(), + arguments: interpreter.current_type_arguments.clone(), + type_scopes: interpreter.type_arg_scopes.clone(), + scoped_bindings: interpreter.scoped_type_bindings.clone(), + fields: interpreter.reflected_field_scopes.clone(), + checked_fields: interpreter.checked_reflected_fields.clone(), + type_infos: interpreter.reflected_type_info_scopes.clone(), + variants: interpreter.reflected_variant_scopes.clone(), + states: interpreter.reflected_machine_state_scopes.clone(), + }) + } + + pub(super) fn scope_depth(&self) -> usize { + self.scope_depth + } + + pub(super) fn restore(self, interpreter: &mut Interpreter) -> Result<(), String> { + let underflow = interpreter.scopes.len() < self.scope_depth + || interpreter.variable_type_scopes.len() < self.variable_depth + || interpreter.namespace_alias_scopes.len() < self.alias_depth; + interpreter.scopes.truncate(self.scope_depth); + interpreter + .variable_type_scopes + .truncate(self.variable_depth); + interpreter + .namespace_alias_scopes + .truncate(self.alias_depth); + interpreter.current_namespace = self.namespace; + interpreter.current_function_trusted_stdlib = self.trusted; + interpreter.lexical_scope_floor = self.floor; + interpreter.allow_checked_refinement_proofs = self.proofs; + interpreter.active_checked_function = self.checked_function; + interpreter.active_checked_scope = self.checked_scope; + interpreter.current_type_arguments = self.arguments; + interpreter.type_arg_scopes = self.type_scopes; + interpreter.scoped_type_bindings = self.scoped_bindings; + interpreter.reflected_field_scopes = self.fields; + interpreter.checked_reflected_fields = self.checked_fields; + interpreter.reflected_type_info_scopes = self.type_infos; + interpreter.reflected_variant_scopes = self.variants; + interpreter.reflected_machine_state_scopes = self.states; + let retained = match interpreter.retained_resource_program.as_mut() { + Some((program, purpose)) if Arc::ptr_eq(program, &self.program) => { + *purpose = self.retained_purpose; + Ok(()) + } + _ => Err("checked entry lost its exact retained program".to_string()), + }; + let cursor = match interpreter.resource_transport.as_mut() { + Some(transport) if Arc::ptr_eq(transport.checked.program(), &self.program) => { + transport.checked_source_active = self.source_active; + transport.checked.replace_purpose(self.checked_purpose); + transport + .checked + .restore_cursor(self.cursor) + .map_err(|error| error.to_string()) + } + _ => Err("checked entry lost its exact Resource transport".to_string()), + }; + if underflow { + return Err("checked entry removed a preexisting interpreter scope".to_string()); + } + retained.and(cursor) + } +} + +#[cfg(test)] +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct EntryContextObservation { + namespace: Option, + trusted: bool, + floor: usize, + proofs: bool, + scope_depths: [usize; 3], + metadata_depths: [usize; 8], + checked_function: bool, + checked_scope: bool, + source_active: bool, + purpose: ExecutionPurpose, + original_scope_depth: Option, +} + +#[cfg(test)] +impl Interpreter { + pub(crate) fn resource_test_entry_context(&self) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")?; + Ok(EntryContextObservation { + namespace: self.current_namespace.clone(), + trusted: self.current_function_trusted_stdlib, + floor: self.lexical_scope_floor, + proofs: self.allow_checked_refinement_proofs, + scope_depths: [ + self.scopes.len(), + self.variable_type_scopes.len(), + self.namespace_alias_scopes.len(), + ], + metadata_depths: [ + self.current_type_arguments.len(), + self.type_arg_scopes.len(), + self.scoped_type_bindings.len(), + self.reflected_field_scopes.len(), + self.checked_reflected_fields.len(), + self.reflected_type_info_scopes.len(), + self.reflected_variant_scopes.len(), + self.reflected_machine_state_scopes.len(), + ], + checked_function: self.active_checked_function.is_some(), + checked_scope: self.active_checked_scope.is_some(), + source_active: transport.checked_source_active, + purpose: transport.checked.purpose(), + original_scope_depth: transport.checked.original_body_scope_depth(), + }) + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs new file mode 100644 index 00000000..86b2deae --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/intrinsic.rs @@ -0,0 +1,109 @@ +//! Exact checked intrinsic metadata, without a mutable ambient reflection map. +use super::*; +use crate::resource_execution::PreparedIntrinsicArguments; + +impl Interpreter { + pub(super) fn call_resource_intrinsic( + &mut self, + invocation: &CheckedInvocation, + prepared: &PreparedIntrinsicArguments, + args: Vec, + ) -> Result { + let checked = &self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked; + prepared + .validate(checked, invocation) + .map_err(|error| error.to_string())?; + let intrinsic = prepared.intrinsic(); + let name = intrinsic.canonical_name(); + let info = prepared.reflections().first(); + match intrinsic { + IntrinsicId::TypeName + | IntrinsicId::TypeKind + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeHasSecret + | IntrinsicId::TypeInfo => { + if let Some(result) = check_args(name, 0, &args) { + return result; + } + let info = info.ok_or("checked intrinsic has no exact reflection argument")?; + Ok(match intrinsic { + IntrinsicId::TypeName => Value::String(info.type_name.clone()), + IntrinsicId::TypeKind => Value::String(info.kind.clone()), + IntrinsicId::TypeKindTag => Self::type_kind_tag_value(&info.kind), + IntrinsicId::TypePrimitiveTag => { + Self::primitive_tag_value(info.primitive_tag.as_deref()) + } + IntrinsicId::TypeHasSecret => Value::Bool(info.has_secret), + IntrinsicId::TypeInfo => Self::reflection_type_info_value(info), + _ => return Err("invalid checked metadata operation".to_string()), + }) + } + IntrinsicId::TypeArg => { + if let Some(result) = check_args(name, 1, &args) { + return result; + } + let info = info.ok_or("checked intrinsic has no exact reflection argument")?; + let index = match args.first() { + Some(Value::Int64(index)) if *index >= 0 => *index as usize, + Some(other) => { + return Err(format!( + "type.arg expects a non-negative int64 index, got {other}" + )); + } + None => return Err("checked type.arg has no index".to_string()), + }; + info.args + .get(index) + .map(Self::reflection_type_info_value) + .ok_or_else(|| { + format!( + "type.arg index {index} is out of range for type '{}'", + info.type_name + ) + }) + } + IntrinsicId::TypeFields => { + if let Some(result) = check_args(name, 0, &args) { + return result; + } + let [owner] = prepared.types() else { + return Err("checked type.fields has no exact owner type".to_string()); + }; + let source = + info.ok_or("checked type.fields has no exact source reflection argument")?; + if !matches!(source.kind.as_str(), "struct" | "bitfield") { + return Ok(Value::List(Vec::new())); + } + let owner_name = checked.program().checked().interner.type_name(*owner); + let fields = checked + .checked_reflection_fields(*owner) + .map_err(|error| error.to_string())?; + Ok(Value::List( + fields + .iter() + .map(|field| Self::reflection_field_info_value(&owner_name, None, field)) + .collect(), + )) + } + _ => { + let resolved = prepared + .types() + .iter() + .map(|&ty| { + Self::debug_type(&checked.program().checked().interner.type_name(ty)) + .ok_or_else(|| { + "checked intrinsic argument has no canonical interpreter type" + .to_string() + }) + }) + .collect::, String>>()?; + self.call_function_from_resolved_source(name, &resolved, args, None) + } + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs new file mode 100644 index 00000000..f67c11d9 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct.rs @@ -0,0 +1,122 @@ +//! Reached owned actual envelopes stay inside the constructor operation. +use super::*; + +impl Interpreter { + /// The retained body, rather than an optional ordinary interpreter map, + /// supplies primitive expression typing inside checked Resource execution. + pub(in crate::interpreter) fn resource_companion_expression_name( + &self, + expression: &Expr, + ) -> Result, String> { + let Some(transport) = self + .resource_transport + .as_ref() + .filter(|transport| transport.checked_source_active) + else { + return Ok(None); + }; + let ty = transport + .checked + .expression_type(expression.span()) + .map_err(|error| error.to_string())?; + let types = &transport.checked.program().checked().interner; + if !crate::resource_execution::PreparedOwnedStruct::supports_companion(types.resolve(ty)) { + return Ok(None); + } + Ok(Some(types.type_name(ty))) + } + + pub(in crate::interpreter) fn normalize_resource_companion_expression( + &self, + expression: &Expr, + type_name: &str, + mut value: Value, + ) -> Result { + let mut depth = 0usize; + while let Value::Pending(inner) = value { + value = *inner; + depth = depth + .checked_add(1) + .ok_or("pending companion depth exceeds the physical carrier")?; + } + value = if matches!( + expression, + Expr::Binary( + _, + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Modulo, + _, + _ + ) | Expr::Unary(UnaryOp::Neg, _, _) + ) { + self.wrap_integer_value_for_type_name(type_name, value.into_payload())? + } else { + self.normalize_value_for_type_name(type_name, value)? + }; + value = self.retain_checked_identity(type_name, value); + for _ in 0..depth { + value = Value::Pending(Box::new(value)); + } + Ok(value) + } + + pub(super) fn eval_resource_owned_struct( + &mut self, + expression: &Expr, + ) -> Result, String> { + let proof = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_owned_struct(expression) + .map_err(|error| error.to_string())?; + let Some(proof) = proof else { + return Ok(None); + }; + let Expr::Call(_, arguments, _) = expression else { + return Err("owned struct lost its exact original call".to_string()); + }; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut actuals = Vec::with_capacity(arguments.len()); + for (argument, ordinal) in arguments.iter().zip(proof.source_fields()) { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_value_type(&value, proof.fields()[*ordinal].1) + .map_err(|error| error.to_string())?; + if value.custody.has_borrows() { + return Err("owned struct cannot acquire a borrowed payload".to_string()); + } + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + let value = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .assemble_owned_struct(&proof, expression, actuals) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + })(); + self.finish_resource_operation(operation, result).map(Some) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs new file mode 100644 index 00000000..3fd6f365 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/owned_struct_tests.rs @@ -0,0 +1,382 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; +use jett_common::FileId; + +const SINGLE: &str = include_str!("../fixtures/33_owned_struct_scope_drop_entry.jett"); +const REVERSE: &str = include_str!("../fixtures/34_owned_struct_reverse_entry.jett"); +const PREFIX: &str = include_str!("../fixtures/35_owned_struct_abandoned_prefix_entry.jett"); +const PRIMITIVES: &str = include_str!("../fixtures/36_owned_struct_primitive_companions.jett"); +const ALIASES: &str = include_str!("../fixtures/37_owned_struct_alias_companions.jett"); +const PENDING: &str = include_str!("../fixtures/38_owned_struct_pending_int64_transport.jett"); +const PENDING_INSPECTION: &str = + include_str!("../fixtures/39_owned_struct_pending_int64_inspection.jett"); +const PENDING_PRIMITIVES: &str = + include_str!("../fixtures/40_owned_struct_pending_companions.jett"); +const PENDING_PREFIX: &str = include_str!("../fixtures/41_owned_struct_pending_prior_failure.jett"); +const PENDING_MATRIX: &str = + include_str!("../fixtures/42_owned_struct_pending_primitive_matrix.jett"); +const PENDING_UNIT_INSPECTION: &str = + include_str!("../fixtures/43_owned_struct_pending_nothing_inspection.jett"); +const PENDING_TEXT_INSPECTION: &str = + include_str!("../fixtures/44_owned_struct_pending_string_inspection.jett"); + +fn entry(program: &jett_typecheck::CheckedResourceProgram) -> DefId { + program + .resolved() + .scope_table + .definitions + .iter() + .find(|info| { + info.kind == jett_resolve::DefKind::Function + && info.namespace.as_deref() == Some("app") + && info.name == "app.main" + && info.span.file == FileId::new(0) + }) + .unwrap() + .id +} + +fn run(source: &str, script: Vec, events: Vec) { + run_program( + source, + script, + events, + crate::resource_execution::tests::program, + ); +} + +fn run_program( + source: &str, + script: Vec, + events: Vec, + prepare: fn(&str, bool) -> Arc, +) { + for release in [false, true] { + let program = prepare(source, release); + let target = entry(&program); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + for invocation in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant.clone()]) + .unwrap(), + Value::Nothing, + "release={release}, invocation={invocation}" + ); + let expected = events + .iter() + .cloned() + .cycle() + .take(events.len() * (invocation + 1)) + .collect::>(); + // The tuple's third field is registry_live_count, never remaining + // provider script. Both custody counts are zero at this entry boundary. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected, 0, 0) + ); + } + } +} + +#[test] +fn occupied_struct_owned_formal_moves_into_checked_root_and_reenters_without_teardown() { + run( + SINGLE, + vec![ + ScriptOperation::Construct { + label: 1001, + outcome: Ok(()) + }; + 2 + ], + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001), + ], + ); + run( + SINGLE, + vec![ + ScriptOperation::Construct { + label: 1001, + outcome: Err("denied".into()) + }; + 2 + ], + vec![ProviderEvent::ConstructionFailed(1001)], + ); +} + +#[test] +fn occupied_struct_primitive_companions_keep_checked_widths_aliases_and_pending_layers() { + for (source, label) in [ + (PRIMITIVES, 1101), + (ALIASES, 1102), + (PENDING, 1103), + (PENDING_PRIMITIVES, 1104), + (PENDING_MATRIX, 1106), + ] { + run_program( + source, + vec![ + ScriptOperation::Construct { + label, + outcome: Ok(()) + }; + 2 + ], + vec![ + ProviderEvent::Constructed(label), + ProviderEvent::Finalized(label), + ], + crate::resource_execution::tests::companion_program, + ); + } +} + +#[test] +fn occupied_struct_pending_inspection_and_prior_failure_retire_before_reentry() { + for release in [false, true] { + for (source, label, expected) in [ + ( + PENDING_INSPECTION, + 1103, + "unsupported binary operation: pending(17) Eq 17", + ), + ( + PENDING_PREFIX, + 1105, + "list.__remove_at: index -1 out of bounds", + ), + ( + PENDING_UNIT_INSPECTION, + 1107, + "unsupported binary operation: pending(nothing) Eq nothing", + ), + ( + PENDING_TEXT_INSPECTION, + 1108, + "unsupported binary operation: pending(retained) Eq retained", + ), + ] { + let program = crate::resource_execution::tests::companion_program(source, release); + let target = entry(&program); + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label, + outcome: Ok(()) + }; + 2 + ]) + .unwrap(); + for invocation in 0..2 { + let error = interpreter + .call_checked_program_entry(target, vec![grant.clone()]) + .unwrap_err(); + assert_eq!( + error, expected, + "release={release}, invocation={invocation}" + ); + let expected_events = [ + ProviderEvent::Constructed(label), + ProviderEvent::Finalized(label), + ] + .into_iter() + .cycle() + .take((invocation + 1) * 2) + .collect::>(); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected_events, 0, 0) + ); + } + } + } +} + +#[test] +fn occupied_struct_reverse_cleanup_follows_lexical_actuals_not_field_positions() { + run( + REVERSE, + vec![ + ScriptOperation::Construct { + label: 1002, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1003, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1002, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1003, + outcome: Ok(()), + }, + ], + vec![ + ProviderEvent::Constructed(1002), + ProviderEvent::Constructed(1003), + ProviderEvent::Finalized(1002), + ProviderEvent::Finalized(1003), + ], + ); +} + +#[test] +fn occupied_struct_later_handler_return_retires_acquired_prefix_and_reenters() { + run( + PREFIX, + vec![ + ScriptOperation::Construct { + label: 1004, + outcome: Ok(()) + }; + 2 + ], + vec![ + ProviderEvent::Constructed(1004), + ProviderEvent::Finalized(1004), + ], + ); +} + +#[test] +fn occupied_struct_finalizer_panic_retires_owner_before_error_and_keeps_ready_reentry() { + for release in [false, true] { + let program = crate::resource_execution::tests::program(SINGLE, release); + let target = entry(&program); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::ConstructFinalizerPanic { label: 1001 }, + ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }, + ]) + .unwrap(); + let first = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })); + assert!(first.is_err()); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001), + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + } +} + +/// Historical drafts retain their actual E0503 boundary. Owning field custody +/// must never be inferred by weakening the checked external entry policy. +#[test] +fn occupied_struct_original_view_main_is_still_rejected_before_entry() { + use jett_common::SourceOrigin; + use jett_parser::{ast::Item, parse}; + use jett_resolve::ResourceKernelSpec; + use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + use jett_types::ResourceKernelRecipe; + use std::collections::HashMap; + const SUPPORT: &str = include_str!("../fixtures/resource_probe.jett"); + const INVALID: [&str; 3] = [ + include_str!("../fixtures/30_owned_struct_scope_drop.jett"), + include_str!("../fixtures/31_owned_struct_reverse.jett"), + include_str!("../fixtures/32_owned_struct_abandoned_prefix.jett"), + ]; + for release in [false, true] { + for source in INVALID { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let original = parse(source, project); + assert!(parsed.errors.is_empty() && original.errors.is_empty()); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(original.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.into(), + recipe, + }) + .collect::>(); + let error = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .err() + .expect("original viewed main remains rejected"); + let diagnostics = error + .diagnostics() + .expect("original E0503 checker diagnostics"); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 503 + && diagnostic.span.file == project + && diagnostic.message + == "`main` must own runtime capability parameter `net`; remove `view`"), + "release={release}: {diagnostics:?}" + ); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs new file mode 100644 index 00000000..29adc43e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline.rs @@ -0,0 +1,234 @@ +//! Checked pipeline envelopes; source expressions are evaluated once. +use crate::resource_execution::PreparedPipelineStep; + +use super::*; + +impl Interpreter { + pub(super) fn eval_resource_pipeline(&mut self, pipeline: &Expr) -> Result { + let Expr::Pipeline(_, steps, _) = pipeline else { + return Err("checked pipeline lost its original node".to_string()); + }; + let first = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_step(pipeline, 0) + .map_err(|error| error.to_string())?; + let first_intrinsic = self.prepare_resource_pipeline_intrinsic(&first)?; + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let mut current = + match Self::resource_envelope(self.eval_resource_pipeline_initial(&first)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let mut first = Some((first, first_intrinsic)); + for index in 0..steps.len() { + let (prepared, intrinsic) = if index == 0 { + first.take().ok_or("checked pipeline lost its first step")? + } else { + let prepared = self + .resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_step(pipeline, index) + .map_err(|error| error.to_string())?; + let intrinsic = self.prepare_resource_pipeline_intrinsic(&prepared)?; + (prepared, intrinsic) + }; + let flow = + self.eval_resource_pipeline_step(&prepared, current, intrinsic.as_ref())?; + current = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + } + Ok(Self::resource_flow(current)) + })(); + self.finish_resource_operation(operation, result) + } + + fn prepare_resource_pipeline_intrinsic( + &self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result, String> { + if matches!( + prepared.invocation().target(), + CheckedInvocationTarget::Intrinsic(_) + ) { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .checked + .prepare_pipeline_intrinsic_arguments(prepared) + .map(Some) + .map_err(|error| error.to_string()) + } else { + Ok(None) + } + } + + fn eval_resource_pipeline_initial( + &mut self, + prepared: &PreparedPipelineStep<'_>, + ) -> Result { + let original = prepared + .initial() + .ok_or("checked pipeline has no original first input")?; + if let Some(definition) = prepared.initial_borrow() { + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + if transport.has_binding(definition) { + let value = transport + .binding(definition, true) + .map_err(|error| error.to_string())?; + return Ok(Self::resource_flow(value)); + } + } + self.eval_expr_flow(original) + } + + fn eval_resource_pipeline_step( + &mut self, + prepared: &PreparedPipelineStep<'_>, + mut input: EvaluatedValue, + intrinsic: Option<&PreparedIntrinsicArguments>, + ) -> Result { + let operation = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .begin_operation() + .map_err(|error| error.to_string())?; + let result = (|| { + let invocation = prepared.invocation(); + let source0 = invocation + .arguments() + .first() + .ok_or("checked pipeline has no original input occurrence")?; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_value_type(&input, prepared.input_type()) + .map_err(|error| error.to_string())?; + transport + .validate_actual(&input, source0.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut input) + .map_err(|error| error.to_string())?; + let mut actuals = Vec::with_capacity(invocation.arguments().len()); + actuals.push(input); + for (argument, fact) in prepared + .extra_arguments() + .iter() + .zip(&invocation.arguments()[1..]) + { + let mut value = + match Self::resource_envelope(self.eval_expr_flow(&argument.value)?)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let transport = self + .resource_transport + .as_mut() + .ok_or("missing checked Resource transport")?; + transport + .validate_actual(&value, fact.effect) + .map_err(|error| error.to_string())?; + transport + .hold_actual(&mut value) + .map_err(|error| error.to_string())?; + actuals.push(value); + } + let flow = self.dispatch_resource_actuals( + invocation, + prepared.callee(), + prepared.type_arguments(), + actuals, + prepared.constructor(), + operation.frame, + intrinsic, + )?; + let value = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(&value, prepared.raw_call_type()) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(value)) + })(); + let flow = self.finish_resource_operation(operation, result)?; + let value = match Self::resource_envelope(flow)? { + Ok(value) => value, + Err(signal) => return Ok(ExprFlow::Signal(signal)), + }; + let flow = if let Some(handle) = &prepared.original().handle { + self.eval_resource_pipeline_handle(value, handle)? + } else { + Self::resource_flow(value) + }; + if let ExprFlow::Resource(value) = &flow { + self.resource_transport + .as_ref() + .ok_or("missing checked Resource transport")? + .validate_value_type(value, prepared.output_type()) + .map_err(|error| error.to_string())?; + } + Ok(flow) + } + + fn eval_resource_pipeline_handle( + &mut self, + mut target: EvaluatedValue, + handle: &jett_parser::ast::PipelineStepHandle, + ) -> Result { + while let Value::Typed { value, .. } = target.value { + target.value = *value; + target + .remove_prefix(PayloadStep::Typed) + .map_err(|error| error.to_string())?; + } + match target.value { + Value::ResultOk(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Ok) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(target)) + } + Value::OptionalSome(value) => { + target.value = *value; + target + .remove_prefix(PayloadStep::Some) + .map_err(|error| error.to_string())?; + Ok(Self::resource_flow(target)) + } + Value::ResultFail(error) if target.custody.is_empty() => { + self.exec_resource_handle(handle.error_name.as_ref(), Some(*error), &handle.body) + } + Value::OptionalNone if target.custody.is_empty() => { + self.exec_resource_handle(None, None, &handle.body) + } + _ => Err("checked pipeline handle has no selected sum custody transport".to_string()), + } + } +} + +#[cfg(test)] +#[path = "pipeline/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs new file mode 100644 index 00000000..cd94f311 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/pipeline/tests.rs @@ -0,0 +1,348 @@ +use jett_common::FileId; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; + +use super::*; +use crate::resource_execution::tests::program; +use crate::resource_execution::{ProviderEvent, ScriptOperation}; + +const CONSTRUCT: &str = include_str!("../../fixtures/18_pipeline_construct_move_close.jett"); +const RETAIN: &str = include_str!("../../fixtures/19_pipeline_written_view_retains_owner.jett"); +const RELINQUISH: &str = include_str!("../../fixtures/20_pipeline_bare_owner_view_operation.jett"); +const ABORT: &str = include_str!("../../fixtures/21_pipeline_named_actual_abort.jett"); +const DOMAIN: &str = include_str!("../../fixtures/22_pipeline_domain_failure_stops_next_step.jett"); +const INDIRECT: &str = include_str!("../../fixtures/23_pipeline_indirect_close_descriptor.jett"); + +fn entry(checked: &jett_typecheck::CheckedResourceProgram) -> DefId { + let functions = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) + && function.name.name == "scenario" => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one original primary scenario"); + }; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.span == function.name.span + && info.namespace.as_deref() == Some("app") + }) + .map(|info| info.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact original declaration"); + }; + *definition +} + +fn execute( + source: &str, + script: Vec, + returned: Value, + events: Vec, +) { + for release in [false, true] { + let checked = program(source, release); + let target = entry(&checked); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + returned, + "release={release}" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn resource_pipeline_constructs_moves_and_closes_one_source_owner() { + execute( + CONSTRUCT, + vec![ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }], + Value::Nothing, + vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Finalized(1801), + ], + ); +} + +#[test] +fn resource_pipeline_written_view_keeps_root_for_later_explicit_close() { + execute( + RETAIN, + vec![ + ScriptOperation::Construct { + label: 1901, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1901, + outcome: Ok(5), + }, + ], + Value::Int64(5), + vec![ + ProviderEvent::Constructed(1901), + ProviderEvent::Borrowed(1901), + ProviderEvent::Finalized(1901), + ], + ); +} + +#[test] +fn resource_pipeline_bare_owner_backs_view_only_until_operation_cleanup() { + execute( + RELINQUISH, + vec![ + ScriptOperation::Construct { + label: 2001, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 2001, + outcome: Ok(5), + }, + ], + Value::Int64(5), + vec![ + ProviderEvent::Constructed(2001), + ProviderEvent::Borrowed(2001), + ProviderEvent::Finalized(2001), + ], + ); +} + +#[test] +fn resource_pipeline_input_precedes_named_actual_abort_and_callee_never_enters() { + execute( + ABORT, + vec![ + ScriptOperation::Construct { + label: 2101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 2101, + outcome: Ok(1), + }, + ], + Value::Nothing, + vec![ + ProviderEvent::Constructed(2101), + ProviderEvent::Borrowed(2101), + ProviderEvent::Finalized(2101), + ], + ); +} + +#[test] +fn resource_pipeline_domain_failure_and_borrow_failure_keep_exact_source_handlers() { + execute( + DOMAIN, + vec![ScriptOperation::Construct { + label: 2201, + outcome: Err("denied".into()), + }], + Value::Int64(-1), + vec![ProviderEvent::ConstructionFailed(2201)], + ); + execute( + RETAIN, + vec![ + ScriptOperation::Construct { + label: 1901, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1901, + outcome: Err("unavailable".into()), + }, + ], + Value::Int64(-2), + vec![ + ProviderEvent::Constructed(1901), + ProviderEvent::BorrowFailed(1901), + ProviderEvent::Finalized(1901), + ], + ); +} + +#[test] +fn resource_pipeline_indirect_hook_descriptor_executes_only_at_original_step() { + execute( + INDIRECT, + vec![ScriptOperation::Construct { + label: 2301, + outcome: Ok(()), + }], + Value::Nothing, + vec![ + ProviderEvent::Constructed(2301), + ProviderEvent::Finalized(2301), + ], + ); +} + +#[test] +fn resource_pipeline_provider_panic_unwinds_real_call_backing_before_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program(RELINQUISH, release); + let target = entry(&checked); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 2001, + outcome: Ok(()), + }, + ScriptOperation::BorrowPanic { label: 2001 }, + ]) + .unwrap(); + assert!( + catch_unwind(AssertUnwindSafe( + || interpreter.call_checked_program_entry(target, vec![grant]) + )) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(2001), + ProviderEvent::Borrowed(2001), + ProviderEvent::Finalized(2001) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} +#[test] +fn resource_pipeline_required_regions_execute_absence_without_runtime_authority() { + use crate::checked_types::CheckedExpressionTypes; + use crate::resource_execution::CheckedExecution; + use jett_parser::ast::Item; + + const REQUIRED: &str = include_str!("../../fixtures/28_pipeline_absence_regions.jett"); + for release in [false, true] { + let program = program(REQUIRED, release); + let expression_types = std::sync::Arc::new(CheckedExpressionTypes { + resource_program: Some(program.clone()), + expressions: program + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, program.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + std::sync::Arc::new(jett_types::ReflectionMetadata::new()), + expression_types.clone(), + std::sync::Arc::new(std::collections::HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + assert!(required.values.checked_values_are_mirrored()); + let declaration = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(value) + if value.name.name == "namespace_marker" + && value.name.span.file == jett_common::FileId::new(0) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + assert_eq!( + required.values.constant(declaration.name.span), + Some(&Value::Int64(17)) + ); + let suites = crate::verify::run_verify_blocks_detailed_with_checked_values( + program.module(), + std::sync::Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + std::sync::Arc::new(std::collections::HashMap::new()), + std::sync::Arc::new(required.values.clone()), + ); + assert_eq!(suites.len(), 2); + assert!( + suites + .iter() + .all(|suite| suite.passed && suite.debug_events.is_empty()), + "{suites:?}" + ); + let original = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.name == "explicit_marker" + && function.name.span.file == jett_common::FileId::new(0) => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let checked = + CheckedExecution::new(program.clone(), ExecutionPurpose::ReferenceRuntime).unwrap(); + let target = checked.declaration_definition(original.name.span).unwrap(); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + interpreter.set_explicit_comptime_values(std::sync::Arc::new(required.values)); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Int64(17) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs b/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs new file mode 100644 index 00000000..80a879f6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/interpreter_transport/required.rs @@ -0,0 +1,261 @@ +//! Original required-region transactions. No provider or ownership permission. + +use super::*; +use crate::resource_execution::{ + CheckedBodyReference, PreparedRequiredExpression, ResourceExecutionError, +}; + +impl Interpreter { + pub(in crate::interpreter) fn eval_checked_actor_initializer( + &mut self, + reference: &CheckedBodyReference, + ) -> Result { + let saved = self + .resource_transport + .as_ref() + .ok_or("actor initializer has no checked transport")? + .cursor(); + self.resource_transport + .as_mut() + .ok_or("actor initializer has no checked transport")? + .checked + .install_function_body(reference) + .map_err(|error| error.to_string())?; + let result = reference + .original_expression() + .map_err(|error| error.to_string()) + .and_then(|expression| self.eval_expr_flow(expression)); + let restore = self + .resource_transport + .as_mut() + .ok_or("actor initializer lost checked transport")? + .checked + .restore_cursor(saved) + .map_err(|error| error.to_string()); + match (result, restore) { + (Err(error), _) | (_, Err(error)) => Err(error), + (Ok(value), Ok(())) => Ok(value), + } + } + pub(crate) fn checked_explicit_contexts( + &self, + expression: &Expr, + occurrence: Span, + owner: Option, + bindings: &[(Span, String)], + ) -> Result>, String> { + if let Some(transport) = &self.resource_transport { + return transport + .checked + .prepare_explicit_contexts(expression, occurrence, owner, bindings) + .map(Some) + .map_err(|error| error.to_string()); + } + let Some((program, purpose)) = &self.retained_resource_program else { + return Ok(None); + }; + // Retained ordinary programs need the same exact original required key, + // but do not install Resource transport, a provider, or custody state. + crate::resource_execution::CheckedExecution::new(program.clone(), *purpose) + .and_then(|checked| { + checked.prepare_explicit_contexts(expression, occurrence, owner, bindings) + }) + .map(Some) + .map_err(|error| error.to_string()) + } + + pub(crate) fn checked_namespace_initializer( + &self, + declaration: &VarDecl, + ) -> Result, String> { + if let Some(transport) = &self.resource_transport { + return transport + .checked + .prepare_namespace_initializer(declaration) + .map(Some) + .map_err(|error| error.to_string()); + } + let Some((program, purpose)) = &self.retained_resource_program else { + return Ok(None); + }; + // Match the explicit-expression route: ordinary retained programs use + // the exact original required key without Resource transport or custody. + crate::resource_execution::CheckedExecution::new(program.clone(), *purpose) + .and_then(|checked| checked.prepare_namespace_initializer(declaration)) + .map(Some) + .map_err(|error| error.to_string()) + } + + pub(crate) fn eval_checked_required_expression( + &mut self, + prepared: &PreparedRequiredExpression, + namespace: Option<&str>, + aliases: &HashMap, + parameters: &[String], + ) -> Result { + if self.resource_transport.is_none() { + let (program, purpose) = self + .retained_resource_program + .as_ref() + .ok_or("required region has no retained checked program")?; + if !Arc::ptr_eq(program, prepared.checked_program()) { + return Err(ResourceExecutionError::ForeignProgram.to_string()); + } + if *purpose != prepared.purpose() || !program.checked().resource_hooks.is_empty() { + return Err(ResourceExecutionError::WrongPurpose.to_string()); + } + return self.with_checked_field_region(prepared.reference(), |interpreter| { + interpreter.eval_closed_comptime_expression( + namespace, + aliases, + prepared.expression().map_err(|error| error.to_string())?, + parameters, + prepared.function_projection(), + prepared.scope_projection(), + prepared.bindings(), + ) + }); + } + if self + .resource_transport + .as_ref() + .ok_or("required region has no checked program")? + .checked + .purpose() + != prepared.purpose() + { + return Err(ResourceExecutionError::WrongPurpose.to_string()); + } + self.with_checked_required_region(prepared.reference(), |interpreter| { + interpreter.eval_closed_comptime_expression( + namespace, + aliases, + prepared.expression().map_err(|error| error.to_string())?, + parameters, + prepared.function_projection(), + prepared.scope_projection(), + prepared.bindings(), + ) + }) + } + + pub(crate) fn exec_checked_verify_region( + &mut self, + namespace: Option<&str>, + original: &jett_parser::ast::VerifyBlock, + ) -> Result, String> { + self.with_original_test_capture( + &original.body, + ExecutionPurpose::Verify, + namespace, + |interpreter| { + let Some(transport) = &interpreter.resource_transport else { + return interpreter.exec_block_in_namespace(namespace, &original.body); + }; + let reference = transport + .checked + .prepare_verify_body(original) + .map_err(|error| error.to_string())?; + interpreter.with_checked_required_region(&reference, |interpreter| { + interpreter.exec_block_in_namespace(namespace, &original.body) + }) + }, + ) + } + + pub(crate) fn with_checked_property_region( + &mut self, + original: &jett_parser::ast::PropertyBlock, + namespace: Option<&str>, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.with_original_test_capture( + &original.body, + ExecutionPurpose::Property, + namespace, + |interpreter| { + let Some(transport) = &interpreter.resource_transport else { + return body(interpreter); + }; + let reference = transport + .checked + .prepare_property_body(original) + .map_err(|error| error.to_string())?; + interpreter.with_checked_required_region(&reference, body) + }, + ) + } + + fn with_original_test_capture( + &mut self, + original: &jett_parser::ast::Block, + purpose: ExecutionPurpose, + namespace: Option<&str>, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let Some((program, selected)) = &self.retained_resource_program else { + return body(self); + }; + if *selected != purpose { + return Err(ResourceExecutionError::WrongPurpose.to_string()); + } + let origin = crate::value::closure_capture::OriginalClosureOwner::test_body( + program, original, purpose, namespace, + ) + .ok_or("checked test capture has no exact original body/purpose/namespace")?; + let field_body = self.checked_field_test_body(original, purpose)?; + let saved_field = self.enter_checked_field_body(field_body.as_ref())?; + let previous = self.active_reflected_capture_origin.replace(origin); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self))); + let field_restore = self.restore_checked_field_body(saved_field); + self.active_reflected_capture_origin = previous; + match result { + Ok(Err(error)) => Err(error), + Ok(Ok(value)) => field_restore.map(|()| value), + Err(panic) => std::panic::resume_unwind(panic), + } + } + + fn with_checked_required_region( + &mut self, + reference: &CheckedBodyReference, + body: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let transport = self + .resource_transport + .as_ref() + .ok_or("required region has no checked program")?; + transport + .validate_entry_scopes(self.scopes.len()) + .map_err(|error| error.to_string())?; + let saved = SavedResourceEntryContext::capture(self)?; + let depth = saved.scope_depth(); + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .checked + .install_function_body(reference) + .map_err(|error| error.to_string())?; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self))); + let cleanup = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .unwind_all() + .map_err(|error| error.to_string()) + })); + let restore = saved.restore(self); + let completion = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.resource_transport + .as_mut() + .ok_or("missing checked Resource transport")? + .restore_entry_scopes(depth) + .map_err(|error| error.to_string()) + })); + let result = match (result, restore) { + (Ok(Ok(_)), Err(error)) => Ok(Err(error)), + (result, _) => result, + }; + Self::complete_resource_entry(result, Self::complete_resource_cleanup(cleanup, completion)) + } +} diff --git a/crates/jett_comptime/src/resource_execution/mod.rs b/crates/jett_comptime/src/resource_execution/mod.rs new file mode 100644 index 00000000..40bb153e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/mod.rs @@ -0,0 +1,143 @@ +//! Checked source Resource execution. A carrier is never a cleanup owner. + +mod checked; +mod custody; +mod provider; +mod runtime; + +use std::fmt; +use std::sync::Arc; + +use jett_resolve::DefId; +use jett_runtime::{RegistryError, ResourceKey, ResourceTypeId}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::TypeId; + +pub use checked::ExecutionPurpose; +pub use checked::{ + ActorGraphHookRoot, CheckedActorGraphHook, CheckedRequiredOwner, CheckedRequiredResourceHook, + CheckedRequiredScope, CheckedRequiredValue, +}; +pub(crate) use checked::{ + CheckedActorInstanceOrigin, CheckedAttemptKey, CheckedBodyCursor, CheckedBodyReference, + CheckedExecution, CheckedInvocation, FunctionInvocation, PreparedAbsentBindings, + PreparedActorHandler, PreparedFieldAssignment, PreparedFieldBinding, + PreparedIntrinsicArguments, PreparedOwnedStruct, PreparedPipelineStep, + PreparedReflectedFieldIteration, PreparedReflectedFieldLoop, PreparedRequiredExpression, +}; +pub(crate) use custody::{ + EvaluatedValue, FrameId, OwnerHolder, OwnerLedger, PayloadStep, ValueCustody, +}; +pub use provider::GrantedNetwork; +pub(crate) use runtime::{OperationFrame, ResourceTransport}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ResourceTypeBinding { + definition: DefId, + checked_type: TypeId, + registry_type: ResourceTypeId, +} + +/// Opaque physical identity. Cloning this value cannot create a cleanup ticket. +#[derive(Clone)] +pub struct ResourceCarrier { + program: Arc, + resource: ResourceTypeBinding, + key: ResourceKey, +} + +impl fmt::Debug for ResourceCarrier { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ResourceCarrier()") + } +} + +/// A closed callable descriptor is data; construction does not execute a hook. +#[derive(Clone)] +pub struct ResourceHookDescriptor { + program: Arc, + definition: DefId, +} + +impl ResourceHookDescriptor { + pub(crate) fn matches_type( + &self, + program: &Arc, + ty: jett_types::TypeId, + ) -> bool { + Arc::ptr_eq(&self.program, program) + && self + .program + .checked() + .resource_hooks + .get(&self.definition) + .is_some_and(|hook| hook.function_type == ty) + } +} + +impl fmt::Debug for ResourceHookDescriptor { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ResourceHookDescriptor()") + } +} + +impl ResourceHookDescriptor { + pub(crate) fn same_descriptor(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.program, &other.program) && self.definition == other.definition + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceExecutionError { + InvalidCheckedProgram, + MissingCheckedBody, + MissingCheckedInvocation, + InvalidInvocation, + ForeignProgram, + WrongPurpose, + ProviderDisabled, + InvalidGrant, + MissingOwner, + InvalidOwner, + InvalidBorrow, + InvalidPayloadPath, + InvalidFrame, + IdentityExhausted, + Registry(RegistryError), +} + +impl fmt::Display for ResourceExecutionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InvalidCheckedProgram => "resource execution requires a valid checked program", + Self::MissingCheckedBody => "resource execution has no exact checked body", + Self::MissingCheckedInvocation => "resource execution has no checked call occurrence", + Self::InvalidInvocation => "resource execution has invalid checked call metadata", + Self::ForeignProgram => "resource execution belongs to another checked program", + Self::WrongPurpose => "resource operation requires runtime execution", + Self::ProviderDisabled => "resource runtime provider is not installed", + Self::InvalidGrant => "resource operation has no matching runtime authority", + Self::MissingOwner => "resource value has no owning cleanup obligation", + Self::InvalidOwner => "resource cleanup obligation is not available", + Self::InvalidBorrow => "resource view has no live checked backing owner", + Self::InvalidPayloadPath => "resource value lost its checked payload custody", + Self::InvalidFrame => "resource execution has no live owning frame", + Self::IdentityExhausted => "resource execution identity space is exhausted", + Self::Registry(_) => "resource carrier is not valid for this runtime operation", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for ResourceExecutionError {} + +impl From for ResourceExecutionError { + fn from(error: RegistryError) -> Self { + Self::Registry(error) + } +} + +#[cfg(test)] +pub(crate) use provider::{ProviderEvent, ScriptOperation}; +#[cfg(test)] +pub(crate) mod tests; diff --git a/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs b/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs new file mode 100644 index 00000000..a970f585 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/prerequisite_tests.rs @@ -0,0 +1,44 @@ +use super::*; + +#[test] +fn checked_source_mutable_assignment_frontend_prerequisites() { + for release in [false, true] { + let checked = program(include_str!("fixtures/24_mutable_assignment.jett"), release); + for name in [ + "replace_live", + "rebind_after_close", + "rebind_self", + "failed_rhs_keeps_owner", + ] { + let _ = entry(&checked, name); + } + } +} + +#[test] +fn checked_source_pipeline_frontend_prerequisites() { + for release in [false, true] { + for source in [ + include_str!("fixtures/18_pipeline_construct_move_close.jett"), + include_str!("fixtures/19_pipeline_written_view_retains_owner.jett"), + include_str!("fixtures/20_pipeline_bare_owner_view_operation.jett"), + include_str!("fixtures/21_pipeline_named_actual_abort.jett"), + include_str!("fixtures/22_pipeline_domain_failure_stops_next_step.jett"), + include_str!("fixtures/23_pipeline_indirect_close_descriptor.jett"), + ] { + let checked = program(source, release); + let _ = entry(&checked, "scenario"); + } + } +} + +#[test] +fn checked_source_pipeline_required_regions_frontend_prerequisites() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/28_pipeline_absence_regions.jett"), + release, + ); + let _ = entry(&checked, "explicit_marker"); + } +} diff --git a/crates/jett_comptime/src/resource_execution/provider.rs b/crates/jett_comptime/src/resource_execution/provider.rs new file mode 100644 index 00000000..ef426247 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/provider.rs @@ -0,0 +1,255 @@ +use std::fmt; + +use jett_runtime::{AuthorityProvenance, ResourceRegistry}; + +use super::{ResourceExecutionError, ResourceTypeBinding}; + +/// This carrier can be copied as capability authority, never as a Resource owner. +#[derive(Clone)] +pub struct GrantedNetwork { + context: u64, + authority: AuthorityProvenance, +} + +impl fmt::Debug for GrantedNetwork { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("GrantedNetwork()") + } +} + +pub(crate) enum InstalledResourceProvider { + Disabled, + #[cfg(test)] + Scripted(ScriptedResourceProvider), +} + +impl InstalledResourceProvider { + pub(crate) fn validate_grant( + &self, + grant: &GrantedNetwork, + ) -> Result { + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) + if provider.grant.context == grant.context + && provider.grant.authority == grant.authority => + { + Ok(grant.authority) + } + #[cfg(test)] + Self::Scripted(_) => Err(ResourceExecutionError::InvalidGrant), + } + } + + pub(crate) fn construct( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + grant: &GrantedNetwork, + label: i64, + ) -> Result, ResourceExecutionError> { + let authority = self.validate_grant(grant)?; + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) => provider.construct(registry, resource, authority, label), + } + } + + pub(crate) fn borrow( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + grant: &GrantedNetwork, + key: jett_runtime::ResourceKey, + ) -> Result, ResourceExecutionError> { + let authority = self.validate_grant(grant)?; + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(provider) => provider.borrow(registry, resource, authority, key), + } + } + + pub(crate) fn ensure_installed(&self) -> Result<(), ResourceExecutionError> { + match self { + Self::Disabled => Err(ResourceExecutionError::ProviderDisabled), + #[cfg(test)] + Self::Scripted(_) => Ok(()), + } + } +} + +// No production provider, public provider spelling or host-hook callback map. +// Compiler unit tests alone can supply the selected exact three recipe script. +#[cfg(test)] +use std::collections::VecDeque; +#[cfg(test)] +use std::sync::atomic::{AtomicU64, Ordering}; +#[cfg(test)] +use std::sync::{Arc, Mutex}; + +#[cfg(test)] +static NEXT_TEST_GRANT: AtomicU64 = AtomicU64::new(1); + +#[cfg(test)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ProviderEvent { + Constructed(i64), + ConstructionFailed(i64), + Borrowed(i64), + BorrowFailed(i64), + Finalized(i64), +} + +#[cfg(test)] +#[derive(Clone)] +pub(crate) enum ScriptOperation { + Construct { + label: i64, + outcome: Result<(), String>, + }, + ConstructFinalizerPanic { + label: i64, + }, + Borrow { + label: i64, + outcome: Result, + }, + BorrowPanic { + label: i64, + }, +} + +#[cfg(test)] +struct FakePayload { + label: i64, +} + +#[cfg(test)] +pub(crate) struct ScriptedResourceProvider { + grant: GrantedNetwork, + operations: VecDeque, + events: Arc>>, +} + +#[cfg(test)] +impl ScriptedResourceProvider { + pub(crate) fn new(operations: Vec) -> Result { + let context = NEXT_TEST_GRANT + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| { + value.checked_add(1) + }) + .map_err(|_| ResourceExecutionError::IdentityExhausted)?; + Ok(Self { + grant: GrantedNetwork { + context, + authority: AuthorityProvenance::new(context, 1), + }, + operations: operations.into(), + events: Arc::new(Mutex::new(Vec::new())), + }) + } + + pub(crate) fn grant(&self) -> GrantedNetwork { + self.grant.clone() + } + + pub(crate) fn events(&self) -> Result, ResourceExecutionError> { + self.events + .lock() + .map(|events| events.clone()) + .map_err(|_| ResourceExecutionError::InvalidGrant) + } + + pub(crate) fn remaining(&self) -> usize { + self.operations.len() + } + + fn record(&self, event: ProviderEvent) -> Result<(), ResourceExecutionError> { + self.events + .lock() + .map_err(|_| ResourceExecutionError::InvalidGrant)? + .push(event); + Ok(()) + } + + fn construct( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + authority: AuthorityProvenance, + label: i64, + ) -> Result, ResourceExecutionError> { + let (expected, outcome, finalizer_panic) = match self.operations.front() { + Some(ScriptOperation::Construct { label, outcome }) => (*label, outcome.clone(), false), + Some(ScriptOperation::ConstructFinalizerPanic { label }) => (*label, Ok(()), true), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + if expected != label { + return Err(ResourceExecutionError::InvalidInvocation); + } + if let Err(message) = outcome { + self.record(ProviderEvent::ConstructionFailed(label))?; + self.operations.pop_front(); + return Ok(Err(message)); + } + // Record bookkeeping before publication; a poisoned test log cannot + // publish an owner and then report construction failure. + self.record(ProviderEvent::Constructed(label))?; + let finalized_events = Arc::clone(&self.events); + let key = registry.insert( + resource.registry_type, + FakePayload { label }, + authority, + move |payload| { + // Test telemetry is separate from source stdout/debug surfaces. + if let Ok(mut events) = finalized_events.lock() { + events.push(ProviderEvent::Finalized(payload.label)); + } + if finalizer_panic { + panic!("selected test resource cleanup panic"); + } + }, + )?; + self.operations.pop_front(); + Ok(Ok(key)) + } + + fn borrow( + &mut self, + registry: &mut ResourceRegistry, + resource: ResourceTypeBinding, + authority: AuthorityProvenance, + key: jett_runtime::ResourceKey, + ) -> Result, ResourceExecutionError> { + // Carrier/provenance validation occurs before consuming script or event. + let label = registry.access( + key, + resource.registry_type, + &authority, + |payload: &mut FakePayload| payload.label, + )?; + let (expected, outcome) = match self.operations.front() { + Some(ScriptOperation::Borrow { label, outcome }) => (*label, Some(outcome.clone())), + Some(ScriptOperation::BorrowPanic { label }) => (*label, None), + _ => return Err(ResourceExecutionError::InvalidInvocation), + }; + if expected != label { + return Err(ResourceExecutionError::InvalidInvocation); + } + let Some(outcome) = outcome else { + self.record(ProviderEvent::Borrowed(label))?; + self.operations.pop_front(); + panic!("selected test borrow provider panic"); + }; + self.record(if outcome.is_ok() { + ProviderEvent::Borrowed(label) + } else { + ProviderEvent::BorrowFailed(label) + })?; + self.operations.pop_front(); + Ok(outcome) + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime.rs b/crates/jett_comptime/src/resource_execution/runtime.rs new file mode 100644 index 00000000..75943f0f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime.rs @@ -0,0 +1,1330 @@ +#[path = "runtime/assignment.rs"] +mod assignment; + +#[path = "runtime/absent_generated.rs"] +mod absent_generated; + +#[path = "runtime/owned_struct.rs"] +mod owned_struct; + +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_resolve::DefId; +use jett_runtime::ResourceRegistry; +use jett_typecheck::{ + CheckedBindingFact, CheckedCalleeAccess, CheckedCallerEffect, CheckedInvocationTarget, + CheckedResourceProgram, +}; +use jett_types::ResourceKernelRecipe; + +use super::checked::{CheckedBodyCursor, CheckedInvocation, FunctionInvocation}; +use super::custody::PayloadStep; +use super::provider::InstalledResourceProvider; +use super::{ + CheckedExecution, EvaluatedValue, ExecutionPurpose, FrameId, OwnerHolder, OwnerLedger, + ResourceCarrier, ResourceExecutionError, ResourceHookDescriptor, +}; +use crate::value::Value; + +pub(crate) struct OperationFrame { + pub(crate) frame: FrameId, + parent: FrameId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ActiveFrame { + Scope(FrameId), + Operation(FrameId), +} + +impl ActiveFrame { + fn id(self) -> FrameId { + match self { + Self::Scope(frame) | Self::Operation(frame) => frame, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct ReturnContext { + destination: Option, + operation_floor: usize, +} + +/// The lexical declaration survives consuming its value. No copied physical +/// carrier can repopulate this slot or restore an owning cleanup ticket. +struct ResourceBindingSlot { + fact: CheckedBindingFact, + value: Option, +} + +/// Internal transport only. Neither ordinary Value copies nor a raw interpreter +/// constructor can install a provider or mint an owning ticket. +pub(crate) struct ResourceTransport { + pub(crate) checked: CheckedExecution, + pub(crate) checked_source_active: bool, + ledger: OwnerLedger, + registry: ResourceRegistry, + provider: InstalledResourceProvider, + scopes: Vec<(FrameId, HashMap)>, + operations: Vec, + operation_parents: HashMap, + abandoned_operations: HashMap, + active_frames: Vec, + returns: Vec, + defaults: Vec, + next_temporary: usize, + cleanup_error: Option, +} + +impl ResourceTransport { + pub(crate) fn checked_only( + program: Arc, + purpose: ExecutionPurpose, + scope_depth: usize, + ) -> Result { + let checked = CheckedExecution::new(program, purpose)?; + let mut transport = Self { + checked, + checked_source_active: false, + ledger: OwnerLedger::default(), + registry: ResourceRegistry::new(), + provider: InstalledResourceProvider::Disabled, + scopes: Vec::new(), + operations: Vec::new(), + operation_parents: HashMap::new(), + abandoned_operations: HashMap::new(), + active_frames: Vec::new(), + returns: Vec::new(), + defaults: Vec::new(), + next_temporary: 0, + cleanup_error: None, + }; + for _ in 0..scope_depth { + transport.push_scope(); + } + Ok(transport) + } + + pub(crate) fn push_scope(&mut self) { + let frame = self.ledger.frame(); + self.scopes.push((frame, HashMap::new())); + self.active_frames.push(ActiveFrame::Scope(frame)); + } + + pub(crate) fn pop_scope(&mut self) { + let Some((frame, _)) = self.scopes.last() else { + self.cleanup_error + .get_or_insert(ResourceExecutionError::InvalidFrame); + return; + }; + let frame = *frame; + if self.active_frames.last() != Some(&ActiveFrame::Scope(frame)) { + self.cleanup_error + .get_or_insert(ResourceExecutionError::InvalidFrame); + return; + } + let result = self.ledger.unwind(frame, &mut self.registry); + if !self.ledger.frame_is_live(frame) { + self.scopes.pop(); + self.active_frames.pop(); + } + if let Err(error) = result { + self.cleanup_error.get_or_insert(error); + } + } + + pub(crate) fn check_cleanup(&mut self) -> Result<(), ResourceExecutionError> { + self.cleanup_error.take().map_or(Ok(()), Err) + } + + fn scope_frame(&self) -> Result { + self.scopes + .last() + .map(|(frame, _)| *frame) + .ok_or(ResourceExecutionError::InvalidFrame) + } + + fn current_frame(&self) -> Result { + self.operations + .last() + .copied() + .map_or_else(|| self.scope_frame(), Ok) + } + + fn temporary(&mut self, frame: FrameId) -> Result { + let slot = self.next_temporary; + self.next_temporary = slot + .checked_add(1) + .ok_or(ResourceExecutionError::IdentityExhausted)?; + Ok(OwnerHolder::Temporary { frame, slot }) + } + + pub(crate) fn begin_operation(&mut self) -> Result { + let parent = self.current_frame()?; + let frame = self.ledger.frame(); + self.operations.push(frame); + self.operation_parents.insert(frame, parent); + self.active_frames.push(ActiveFrame::Operation(frame)); + Ok(OperationFrame { frame, parent }) + } + + pub(crate) fn end_operation( + &mut self, + operation: OperationFrame, + value: Option<&mut EvaluatedValue>, + ) -> Result<(), ResourceExecutionError> { + if self.operations.last() != Some(&operation.frame) + || self.operation_parents.get(&operation.frame) != Some(&operation.parent) + || self.active_frames.last() != Some(&ActiveFrame::Operation(operation.frame)) + { + return Err(ResourceExecutionError::InvalidFrame); + } + let preserved = match value { + Some(value) => match self.temporary(operation.parent) { + Ok(holder) => self.ledger.transfer(&mut value.custody, holder), + Err(error) => Err(error), + }, + None => Ok(()), + }; + // A failed output adoption still unwinds the original frame. Never + // remove its only cleanup path before a fallible holder transfer. + let cleanup = self.ledger.unwind(operation.frame, &mut self.registry); + if !self.ledger.frame_is_live(operation.frame) { + self.operations.pop(); + self.operation_parents.remove(&operation.frame); + self.active_frames.pop(); + } + match (preserved, cleanup, self.check_cleanup()) { + (_, Err(error), _) | (_, Ok(()), Err(error)) => Err(error), + (Err(error), Ok(()), Ok(())) => Err(error), + (Ok(()), Ok(()), Ok(())) => Ok(()), + } + } + + pub(crate) fn discard_value( + &mut self, + mut value: EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + let operation = self.begin_operation()?; + let adopted = self.hold_actual(&mut value); + let completed = self.end_operation(operation, None); + match (adopted, completed) { + (_, Err(error)) => Err(error), + (Err(error), Ok(())) => Err(error), + (Ok(()), Ok(())) => Ok(()), + } + } + + pub(crate) fn hold_actual( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + let holder = self.temporary(self.current_frame()?)?; + self.ledger.transfer(&mut value.custody, holder)?; + self.ledger + .hold_borrows(&value.custody, self.current_frame()?) + } + + pub(crate) fn borrow( + &self, + value: &EvaluatedValue, + ) -> Result { + self.ledger.borrow(value) + } + + pub(crate) fn captured_resource_reference(&self, name: &str, body: Span) -> Option { + let mut found = false; + for (_, bindings) in self.scopes.iter().rev() { + for definition in bindings.keys() { + if self + .checked + .program() + .resolved() + .scope_table + .definitions + .get(definition.index() as usize) + .is_some_and(|info| info.name == name) + { + found = true; + if self.checked.referenced_in(*definition, body) { + return Some(true); + } + } + } + } + found.then_some(false) + } + + pub(crate) fn has_binding(&self, definition: DefId) -> bool { + self.scopes + .iter() + .rev() + .any(|(_, bindings)| bindings.contains_key(&definition)) + } + + pub(crate) fn binding( + &mut self, + definition: DefId, + borrow: bool, + ) -> Result { + let index = self + .scopes + .iter() + .rposition(|(_, bindings)| bindings.contains_key(&definition)) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if borrow { + let value = self.scopes[index] + .1 + .get(&definition) + .and_then(|slot| slot.value.as_ref()) + .ok_or(ResourceExecutionError::InvalidOwner)?; + self.ledger.borrow(value) + } else { + let mut value = self.scopes[index] + .1 + .get_mut(&definition) + .and_then(|slot| slot.value.take()) + .ok_or(ResourceExecutionError::InvalidOwner)?; + // A borrowed alias is a physical carrier only; it cannot mint a + // ticket merely by being used with owned syntax. + if !value.custody.is_empty() && !value.custody.has_owners() { + self.scopes[index] + .1 + .get_mut(&definition) + .ok_or(ResourceExecutionError::InvalidOwner)? + .value = Some(value); + return Err(ResourceExecutionError::InvalidOwner); + } + self.hold_actual(&mut value)?; + Ok(value) + } + } + + pub(crate) fn install_binding( + &mut self, + declaration: Span, + mut value: EvaluatedValue, + ) -> Result { + let fact = self.checked.binding(declaration)?; + self.validate_value_type(&value, fact.ty)?; + let frame = self.scope_frame()?; + if self + .scopes + .last() + .is_some_and(|(_, bindings)| bindings.contains_key(&fact.definition)) + { + return Err(ResourceExecutionError::InvalidOwner); + } + self.ledger.transfer( + &mut value.custody, + OwnerHolder::Binding { + frame, + definition: fact.definition, + }, + )?; + self.ledger.hold_borrows(&value.custody, frame)?; + self.scopes + .last_mut() + .ok_or(ResourceExecutionError::InvalidFrame)? + .1 + .insert( + fact.definition, + ResourceBindingSlot { + fact, + value: Some(value), + }, + ); + Ok(fact.definition) + } + + pub(crate) fn enter_return(&mut self, destination: FrameId) { + self.returns.push(ReturnContext { + destination: Some(destination), + operation_floor: self.operations.len(), + }); + } + + /// Ordinary callable bodies cannot inherit a caller's custody destination. + pub(crate) fn enter_ordinary_return(&mut self) -> bool { + if !self.checked_source_active { + return false; + } + self.returns.push(ReturnContext { + destination: None, + operation_floor: self.operations.len(), + }); + true + } + + /// Cancel only calls opened in this checked function, before Return effects. + pub(crate) fn retire_return_operations(&mut self) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + + let context = self + .returns + .last() + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + if !self.checked_source_active || context.operation_floor > self.operations.len() { + return Err(ResourceExecutionError::InvalidFrame); + } + let suffix = self.operations[context.operation_floor..] + .iter() + .rev() + .copied() + .collect::>(); + let mut first_error = None; + let mut first_panic = None; + for frame in suffix { + match catch_unwind(AssertUnwindSafe(|| self.retire_return_operation(frame))) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + if let Some(payload) = first_panic { + resume_unwind(payload); + } + first_error.map_or_else(|| self.check_cleanup(), Err) + } + + fn retire_return_operation(&mut self, frame: FrameId) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + + let parent = self + .operation_parents + .get(&frame) + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + if !self.ledger.frame_is_live(frame) + || self.abandoned_operations.contains_key(&frame) + || self.operations.iter().filter(|&&id| id == frame).count() != 1 + || self + .active_frames + .iter() + .filter(|&&active| active == ActiveFrame::Operation(frame)) + .count() + != 1 + { + return Err(ResourceExecutionError::InvalidFrame); + } + let cleanup = catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })); + // Handler/block Scopes may be interleaved. Retain their exact owners + // and loans; only the physically retired Operation leaves this stack. + if !self.ledger.frame_is_live(frame) { + self.operations.retain(|&id| id != frame); + self.operation_parents.remove(&frame); + self.active_frames + .retain(|&active| active != ActiveFrame::Operation(frame)); + self.abandoned_operations.insert(frame, parent); + } + match cleanup { + Ok(result) => result, + Err(payload) => resume_unwind(payload), + } + } + + /// Exact one-shot completion; called only for a propagated Return or error. + pub(crate) fn acknowledge_return_operation( + &mut self, + operation: &OperationFrame, + ) -> Result { + let Some(parent) = self.abandoned_operations.get(&operation.frame) else { + return Ok(false); + }; + if *parent != operation.parent + || self.ledger.frame_is_live(operation.frame) + || self.operation_parents.contains_key(&operation.frame) + || self.operations.contains(&operation.frame) + || self + .active_frames + .contains(&ActiveFrame::Operation(operation.frame)) + { + return Err(ResourceExecutionError::InvalidFrame); + } + self.abandoned_operations.remove(&operation.frame); + Ok(true) + } + pub(crate) fn leave_return(&mut self) { + self.returns.pop(); + } + pub(crate) fn preserve_return( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + let destination = self + .returns + .last() + .and_then(|context| context.destination) + .ok_or(ResourceExecutionError::InvalidFrame)?; + let holder = self.temporary(destination)?; + self.ledger.transfer(&mut value.custody, holder) + } + + pub(crate) fn enter_default(&mut self) -> Result<(), ResourceExecutionError> { + self.defaults.push(self.current_frame()?); + Ok(()) + } + pub(crate) fn leave_default(&mut self) { + self.defaults.pop(); + } + pub(crate) fn preserve_default( + &mut self, + value: &mut EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + let destination = self + .defaults + .last() + .copied() + .ok_or(ResourceExecutionError::InvalidFrame)?; + let holder = self.temporary(destination)?; + self.ledger.transfer(&mut value.custody, holder) + } + + pub(crate) fn cursor(&self) -> CheckedBodyCursor { + self.checked.cursor() + } + pub(crate) fn enter_callee( + &mut self, + invocation: &FunctionInvocation, + ) -> Result<(), ResourceExecutionError> { + let FunctionInvocation::Source(invocation) = invocation else { + self.checked.enter_ordinary(); + return Ok(()); + }; + match invocation.target() { + CheckedInvocationTarget::Resolved(_) => { + self.checked.enter_ordinary(); + Ok(()) + } + CheckedInvocationTarget::Generic(call) => self.checked.enter_generic(call), + CheckedInvocationTarget::Method(_) + | CheckedInvocationTarget::Interface(_) + | CheckedInvocationTarget::Indirect(_) + | CheckedInvocationTarget::Intrinsic(_) => { + Err(ResourceExecutionError::InvalidInvocation) + } + } + } + + pub(crate) fn formal_value( + &self, + value: &EvaluatedValue, + access: CheckedCalleeAccess, + ) -> Result, ResourceExecutionError> { + match access { + CheckedCalleeAccess::View => self.ledger.borrow(value).map(Some), + CheckedCalleeAccess::Owned => Ok(None), + } + } + + pub(crate) fn validate_actual( + &self, + value: &EvaluatedValue, + effect: CheckedCallerEffect, + ) -> Result<(), ResourceExecutionError> { + if value.custody.is_empty() { + return Ok(()); + } + match effect { + CheckedCallerEffect::Copy | CheckedCallerEffect::ObserveData => { + Err(ResourceExecutionError::InvalidOwner) + } + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned => { + if value.custody.has_owners() { + Ok(()) + } else { + Err(ResourceExecutionError::InvalidOwner) + } + } + CheckedCallerEffect::RetainBorrow => { + // Written view of a producer may still have an operation-owned + // backing; the callee receives only the derived borrow. + self.ledger.borrow(value).map(|_| ()) + } + } + } + + pub(crate) fn invoke_hook( + &mut self, + invocation: &CheckedInvocation, + descriptor: &ResourceHookDescriptor, + arguments: Vec, + ) -> Result { + if !self.checked_source_active { + return Err(ResourceExecutionError::InvalidInvocation); + } + let (hook, resource, recipe) = self.checked.reached_hook(invocation, descriptor)?; + let jett_types::Type::Function { params, .. } = self + .checked + .program() + .checked() + .interner + .resolve(hook.function_type) + else { + return Err(ResourceExecutionError::InvalidInvocation); + }; + if params.len() != arguments.len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + for (argument, ty) in arguments.iter().zip(params) { + self.validate_value_type(argument, *ty)?; + } + self.provider.ensure_installed()?; + if recipe == ResourceKernelRecipe::Finalize { + let [token] = <[EvaluatedValue; 1]>::try_from(arguments) + .map_err(|_| ResourceExecutionError::InvalidInvocation)?; + self.ledger.close(token.custody, &mut self.registry)?; + return Ok(EvaluatedValue::ordinary(Value::Nothing)); + } + match (recipe, arguments.as_slice()) { + (ResourceKernelRecipe::NetworkFactory, [network, label]) => { + let (Value::GrantedNetwork(grant), Value::Int64(label)) = + (network.value.payload(), label.value.payload()) + else { + return Err(ResourceExecutionError::InvalidGrant); + }; + // All fallible holder allocation precedes provider publication. + // A successful key is adopted immediately or explicitly closed. + let holder = self.temporary(self.current_frame()?)?; + match self + .provider + .construct(&mut self.registry, resource, grant, *label)? + { + Ok(key) => { + let carrier = ResourceCarrier { + program: self.checked.program().clone(), + resource, + key, + }; + let custody = match self.ledger.acquire(carrier.clone(), holder) { + Ok(custody) => custody, + Err(error) => { + self.registry.close(key, resource.registry_type)?; + return Err(error); + } + }; + Ok(EvaluatedValue { + value: Value::ResultOk(Box::new(Value::Resource(carrier))), + custody, + } + .prefix(PayloadStep::Ok)) + } + Err(error) => Ok(EvaluatedValue::ordinary(Value::ResultFail(Box::new( + Value::String(error), + )))), + } + } + (ResourceKernelRecipe::NetworkBorrow, [network, token]) => { + let Value::GrantedNetwork(grant) = network.value.payload() else { + return Err(ResourceExecutionError::InvalidGrant); + }; + let carrier = self.ledger.borrowed_carrier(&token.custody)?; + if !Arc::ptr_eq(&carrier.program, self.checked.program()) + || carrier.resource != resource + { + return Err(ResourceExecutionError::ForeignProgram); + } + let outcome = + self.provider + .borrow(&mut self.registry, resource, grant, carrier.key)?; + Ok(EvaluatedValue::ordinary(match outcome { + Ok(value) => Value::ResultOk(Box::new(Value::Int64(value))), + Err(error) => Value::ResultFail(Box::new(Value::String(error))), + })) + } + _ => Err(ResourceExecutionError::InvalidInvocation), + } + } + + pub(crate) fn validate_value_type( + &self, + value: &EvaluatedValue, + ty: jett_types::TypeId, + ) -> Result<(), ResourceExecutionError> { + fn positions<'a>( + value: &'a Value, + path: &mut Vec, + out: &mut Vec<(Vec, &'a ResourceCarrier)>, + ) -> Result<(), ResourceExecutionError> { + let (step, child) = match value { + Value::Resource(carrier) => { + out.push((path.clone(), carrier)); + return Ok(()); + } + Value::Struct { fields, .. } => { + for (ordinal, (_, child)) in fields.iter().enumerate() { + path.push(PayloadStep::Field(ordinal)); + positions(child, path, out)?; + path.pop(); + } + return Ok(()); + } + Value::Typed { value, .. } => (PayloadStep::Typed, value.as_ref()), + Value::ResultOk(value) => (PayloadStep::Ok, value.as_ref()), + Value::ResultFail(value) => (PayloadStep::Fail, value.as_ref()), + Value::OptionalSome(value) => (PayloadStep::Some, value.as_ref()), + value + if !value.contains_live_resource_or_grant() + || matches!(value, Value::GrantedNetwork(_)) => + { + return Ok(()); + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + }; + path.push(step); + positions(child, path, out)?; + path.pop(); + Ok(()) + } + fn shape( + value: &Value, + ty: jett_types::TypeId, + checked: &CheckedExecution, + ) -> Result<(), ResourceExecutionError> { + use jett_types::{CapabilityKind, Type, TypeInterner}; + let types = &checked.program().checked().interner; + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !value.contains_live_resource_or_grant() && checked.type_contains_resource(ty)? { + return absent_generated::absent_shape(value, ty, checked); + } + match (value, types.resolve(ty)) { + (Value::Resource(carrier), Type::Resource(_)) + if Arc::ptr_eq(&carrier.program, checked.program()) + && carrier.resource.checked_type == ty => + { + Ok(()) + } + ( + Value::Struct { + type_name, + concrete_type, + fields, + }, + Type::Struct(owner), + ) if checked.type_contains_resource(ty)? => { + let definition = types.resolve_struct(*owner); + if type_name != &definition.name + || concrete_type + .as_ref() + .is_some_and(|name| name != &definition.name) + || !types.nominal_type_arguments(ty).is_empty() + || fields.len() != definition.fields.len() + { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for ((name, value), (expected_name, field_type)) in + fields.iter().zip(&definition.fields) + { + if name != expected_name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + match types.resolve(*field_type) { + Type::Resource(_) => shape(value, *field_type, checked)?, + kind if super::checked::PreparedOwnedStruct::supports_companion( + kind, + ) => + { + owned_struct::companion_shape(value, *field_type, checked)?; + } + _ => return Err(ResourceExecutionError::InvalidPayloadPath), + } + } + Ok(()) + } + (Value::GrantedNetwork(_), Type::Capability(CapabilityKind::Network)) => Ok(()), + (Value::ResultOk(value), Type::Result(success, _)) => { + shape(value, *success, checked) + } + (Value::ResultFail(value), Type::Result(_, error)) => shape(value, *error, checked), + (Value::OptionalSome(value), Type::Optional(success)) => { + shape(value, *success, checked) + } + (Value::OptionalNone, Type::Optional(_)) => Ok(()), + (Value::ResourceHook(descriptor), Type::Function { .. }) + if Arc::ptr_eq(&descriptor.program, checked.program()) + && checked + .program() + .checked() + .resource_hooks + .get(&descriptor.definition) + .is_some_and(|hook| hook.function_type == ty) => + { + Ok(()) + } + (Value::Int64(_), _) if ty == TypeInterner::INT64 => Ok(()), + (Value::String(_), _) if ty == TypeInterner::STRING => Ok(()), + (Value::Nothing, _) if ty == TypeInterner::NOTHING => Ok(()), + (value, kind) if super::checked::PreparedOwnedStruct::supports_companion(kind) => { + owned_struct::companion_shape(value, ty, checked) + } + (value, _) + if !value.contains_live_resource_or_grant() + && !checked.type_contains_resource(ty)? => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } + } + shape(&value.value, ty, &self.checked)?; + let mut carriers = Vec::new(); + positions(&value.value, &mut Vec::new(), &mut carriers)?; + self.ledger.validate_positions(&value.custody, &carriers) + } + + pub(crate) fn unwind_all(&mut self) -> Result<(), ResourceExecutionError> { + use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; + let mut first_panic = None; + let mut first_error = None; + let mut retry = Vec::new(); + // One chronological stack preserves the scope/operation interleaving. + // A callee's borrowed formal is released before its caller owner. + while let Some(active) = self.active_frames.pop() { + let frame = active.id(); + let role_matches = match active { + ActiveFrame::Scope(_) => { + if self.scopes.last().map(|(id, _)| *id) == Some(frame) { + self.scopes.pop(); + true + } else { + false + } + } + ActiveFrame::Operation(_) => { + if self.operations.last() == Some(&frame) { + self.operations.pop(); + self.operation_parents.remove(&frame); + true + } else { + false + } + } + }; + if !role_matches { + first_error.get_or_insert(ResourceExecutionError::InvalidFrame); + } + // A finalizer panic can leave a physically retired frame on the + // activation stack; do not finalize any member a second time. + if !self.ledger.frame_is_live(frame) { + continue; + } + match catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + if self.ledger.frame_is_live(frame) { + retry.push(frame); + } + } + // Retain failed cleanup obligations until older exact frames have + // released their leases. This does not make the invalid relation valid. + for frame in retry { + match catch_unwind(AssertUnwindSafe(|| { + self.ledger.unwind(frame, &mut self.registry) + })) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + first_error.get_or_insert(error); + } + Err(payload) if first_panic.is_none() => first_panic = Some(payload), + Err(payload) => std::mem::forget(payload), + } + } + if !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.scopes.is_empty() + { + first_error.get_or_insert(ResourceExecutionError::InvalidFrame); + } + if let Some(payload) = first_panic { + resume_unwind(payload); + } + first_error.map_or_else(|| self.check_cleanup(), Err) + } + + pub(crate) fn validate_entry_scopes( + &self, + scope_depth: usize, + ) -> Result<(), ResourceExecutionError> { + if !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.abandoned_operations.is_empty() + || !self.returns.is_empty() + || !self.defaults.is_empty() + || self.ledger.live_owners() != 0 + || self.ledger.has_live_borrows() + || self.registry.live_count() != 0 + || self.scopes.len() != scope_depth + || self.active_frames.len() != scope_depth + || self.ledger.live_frame_count() != scope_depth + { + return Err(ResourceExecutionError::InvalidFrame); + } + for ((frame, bindings), active) in self.scopes.iter().zip(&self.active_frames) { + if !bindings.is_empty() + || *active != ActiveFrame::Scope(*frame) + || !self.ledger.frame_is_live(*frame) + { + return Err(ResourceExecutionError::InvalidFrame); + } + } + Ok(()) + } + pub(crate) fn restore_entry_scopes( + &mut self, + scope_depth: usize, + ) -> Result<(), ResourceExecutionError> { + if !self.active_frames.is_empty() + || !self.operations.is_empty() + || !self.operation_parents.is_empty() + || !self.scopes.is_empty() + || self.ledger.has_live_frames() + || self.ledger.live_owners() != 0 + || self.registry.live_count() != 0 + { + return Err(ResourceExecutionError::InvalidFrame); + } + self.returns.clear(); + self.defaults.clear(); + // Panics skip suspended envelopes. Receipts expire only after the + // complete entry has no remaining frame, owner or registry obligation. + self.abandoned_operations.clear(); + let prior_cleanup = self.check_cleanup(); + for _ in 0..scope_depth { + self.push_scope(); + } + prior_cleanup + } + + pub(crate) fn live_owners(&self) -> usize { + self.ledger.live_owners() + } + pub(crate) fn registry_live_count(&self) -> usize { + self.registry.live_count() + } + + #[cfg(test)] + pub(crate) fn install_script( + &mut self, + operations: Vec, + ) -> Result { + if self.checked.purpose() != ExecutionPurpose::ReferenceRuntime { + return Err(ResourceExecutionError::WrongPurpose); + } + if !matches!(self.provider, InstalledResourceProvider::Disabled) + || self.ledger.live_owners() != 0 + { + return Err(ResourceExecutionError::InvalidGrant); + } + let provider = super::provider::ScriptedResourceProvider::new(operations)?; + let grant = provider.grant(); + self.provider = InstalledResourceProvider::Scripted(provider); + Ok(grant) + } + + #[cfg(test)] + pub(crate) fn provider_events( + &self, + ) -> Result, ResourceExecutionError> { + match &self.provider { + InstalledResourceProvider::Disabled => Err(ResourceExecutionError::ProviderDisabled), + InstalledResourceProvider::Scripted(provider) => provider.events(), + } + } +} + +#[cfg(test)] +mod absence_tests; +#[cfg(test)] +mod return_prefix_tests; + +#[cfg(test)] +mod tests { + use super::*; + use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; + use jett_types::ResourceHookKind; + + fn constructed(runtime: &mut ResourceTransport, label: i64) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("selected private provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(label)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("occupied real construction"); + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value + } + + #[test] + fn failed_discard_adoption_completes_its_frame_without_retiring_protected_owner() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let value = constructed(&mut runtime, 401); + let outer = runtime.begin_operation().unwrap(); + let mut borrow = runtime.borrow(&value).unwrap(); + runtime.hold_actual(&mut borrow).unwrap(); + assert_eq!( + runtime.discard_value(value), + Err(ResourceExecutionError::InvalidBorrow) + ); + assert_eq!(runtime.operations, [outer.frame]); + assert_eq!( + runtime.active_frames.last(), + Some(&ActiveFrame::Operation(outer.frame)) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(401)] + ); + assert_eq!(runtime.live_owners(), 1); + runtime.end_operation(outer, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + } + + #[test] + fn interleaved_scope_and_operation_cleanup_preserves_reverse_holder_order() { + let checked = program(include_str!("fixtures/02_reverse_scope_drop.jett"), false); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ + ScriptOperation::Construct { + label: 201, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 202, + outcome: Ok(()), + }, + ]) + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let first = constructed(&mut runtime, 201); + runtime.push_scope(); + let mut second = constructed(&mut runtime, 202); + let holder = runtime.temporary(runtime.scope_frame().unwrap()).unwrap(); + runtime + .ledger + .transfer(&mut second.custody, holder) + .unwrap(); + assert!(matches!( + runtime.active_frames.last(), + Some(ActiveFrame::Scope(_)) + )); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(201), + ProviderEvent::Constructed(202), + ProviderEvent::Finalized(202), + ProviderEvent::Finalized(201) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert!( + runtime.operations.is_empty() + && runtime.scopes.is_empty() + && runtime.active_frames.is_empty() + ); + drop((operation, first, second)); + } + + #[test] + fn reached_capability_free_close_refuses_worker_purpose_before_owner_retirement() { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + let program = program( + include_str!("fixtures/09_capability_free_close_target.jett"), + false, + ); + let mut runtime = ResourceTransport::checked_only( + program.clone(), + ExecutionPurpose::ReferenceRuntime, + 1, + ) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 901, + outcome: Ok(()), + }]) + .unwrap(); + let definition = |kind| { + program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == kind) + .unwrap() + .definition + }; + let occurrence = |definition| { + *program + .checked() + .call_ownership + .iter() + .find(|(_, packet)| { + packet.target == CheckedInvocationTarget::Resolved(definition) + }) + .unwrap() + .0 + }; + // Exact source kernel packets and a real registry publication create + // custody; no Resource carrier or owning ticket is fabricated. + let create = runtime + .checked + .invocation(occurrence(definition(ResourceHookKind::Construct))) + .unwrap(); + let create_descriptor = runtime + .checked + .descriptor(definition(ResourceHookKind::Construct)) + .unwrap(); + let mut token = runtime + .invoke_hook( + &create, + &create_descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(901)), + ], + ) + .unwrap(); + let Value::ResultOk(value) = token.value else { + panic!("occupied factory result"); + }; + token.value = *value; + token.remove_prefix(PayloadStep::Ok).unwrap(); + let close = runtime + .checked + .invocation(occurrence(definition(ResourceHookKind::Close))) + .unwrap(); + let close_descriptor = runtime + .checked + .descriptor(definition(ResourceHookKind::Close)) + .unwrap(); + runtime.checked.replace_purpose(purpose); + assert_eq!( + runtime + .invoke_hook(&close, &close_descriptor, vec![token]) + .unwrap_err(), + ResourceExecutionError::WrongPurpose + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(901)] + ); + assert_eq!(runtime.live_owners(), 1); + assert_eq!(runtime.registry_live_count(), 1); + runtime.unwind_all().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Finalized(901) + ] + ); + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } + } + + #[test] + fn operation_cleanup_error_overrides_failed_output_preservation_and_retires_once() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let mut value = constructed(&mut runtime, 401); + let Value::Resource(carrier) = &value.value else { + panic!("actual occupied resource construction"); + }; + // Private corruption control: retire the actual registry entry without + // retiring the ledger obligation. No source program can perform this. + runtime + .registry + .close(carrier.key, carrier.resource.registry_type) + .unwrap(); + runtime.next_temporary = usize::MAX; + assert_eq!( + runtime.end_operation(operation, Some(&mut value)), + Err(ResourceExecutionError::Registry( + jett_runtime::RegistryError::StaleGeneration + )) + ); + assert!(runtime.operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + runtime.unwind_all().unwrap(); + assert!( + runtime.active_frames.is_empty() + && runtime.operations.is_empty() + && runtime.scopes.is_empty() + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + } + + #[test] + fn discard_cleanup_error_overrides_adoption_error_without_losing_parent_custody() { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + runtime + .install_script(vec![ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }]) + .unwrap(); + let value = constructed(&mut runtime, 401); + // Private completion fault: holder allocation fails while a distinct + // earlier cleanup error remains queued. Neither is provider authority. + runtime.next_temporary = usize::MAX; + runtime.cleanup_error = Some(ResourceExecutionError::InvalidFrame); + assert_eq!( + runtime.discard_value(value), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.operations.is_empty()); + assert_eq!(runtime.active_frames.len(), 1); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(401)] + ); + runtime.unwind_all().unwrap(); + assert!(runtime.active_frames.is_empty() && runtime.scopes.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ] + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs new file mode 100644 index 00000000..564dacc7 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absence_tests.rs @@ -0,0 +1,579 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::FileId; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; +use jett_types::{Type, TypeId}; + +use super::*; +use crate::resource_execution::tests::program; +use crate::{Interpreter, Value}; + +const SOURCE: &str = include_str!("../fixtures/17_absent_aggregate_shapes.jett"); + +fn source_entry(program: &CheckedResourceProgram, name: &str) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!("one primary ordinary source entry {name}"); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("one exact retained ordinary declaration {name}"); + }; + *definition +} + +fn returned_type(program: &CheckedResourceProgram, name: &str) -> TypeId { + let definition = source_entry(program, name); + let signature = program.checked().definition_types[&definition]; + let Type::Function { return_type, .. } = program.checked().interner.resolve(signature) else { + panic!("exact checked signature"); + }; + *return_type +} + +fn absent_values() -> Vec<(&'static str, Value)> { + vec![ + ("empty_tokens", Value::List(Vec::new())), + ( + "absent_tokens", + Value::List(vec![Value::OptionalNone, Value::OptionalNone]), + ), + ("empty_map", Value::Map(Vec::new())), + ( + "absent_map", + Value::Map(vec![(Value::String("empty".into()), Value::OptionalNone)]), + ), + ( + "empty_result_tokens", + Value::ResultOk(Box::new(Value::List(Vec::new()))), + ), + ( + "failed_result_tokens", + Value::ResultFail(Box::new(Value::String("empty".into()))), + ), + ( + "absent_envelope", + Value::Struct { + type_name: "app.TokenEnvelope".into(), + concrete_type: None, + fields: vec![ + ("tokens".into(), Value::List(Vec::new())), + ("fallback".into(), Value::OptionalNone), + ("marker".into(), Value::Int64(17)), + ], + }, + ), + ( + "absent_box", + Value::Struct { + type_name: "app.TokenBox".into(), + concrete_type: Some("app.TokenBox[resource_probe.TestHandle]".into()), + fields: vec![("items".into(), Value::List(Vec::new()))], + }, + ), + ( + "empty_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "empty".into(), + fields: Vec::new(), + }, + ), + ( + "absent_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + ), + ( + "absent_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "vacant".into(), + fields: vec![Value::OptionalNone, Value::String("empty".into())], + }, + ), + ( + "absent_exact_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "vacant".into(), + fields: vec![Value::OptionalNone, Value::String("empty".into())], + }, + ), + ] +} + +#[test] +fn resource_absent_aggregates_execute_exact_checked_source_without_provider_events() { + for release in [false, true] { + let checked = program(SOURCE, release); + for (name, expected) in absent_values() { + let target = source_entry(&checked, name); + let mut interpreter = Interpreter::from_checked_resource_program( + checked.clone(), + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + expected, + "{name}, release={release}" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn resource_absent_aggregates_remain_pure_in_all_required_purposes_and_workers() { + for release in [false, true] { + let checked = program(SOURCE, release); + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for (name, expected) in absent_values() { + let target = source_entry(&checked, name); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose).unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + expected + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let explicit = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + explicit.diagnostics.is_empty(), + "{:?}", + explicit.diagnostics + ); + assert!(explicit.debug_events.is_empty()); + let declaration = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::VarDecl(declaration) if declaration.name.name == "absent_namespace_value" => { + Some(declaration.name.span) + } + _ => None, + }) + .expect("one actual source namespace constant"); + assert_eq!( + explicit.values.constant(declaration), + Some(&Value::Int64(17)) + ); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + checked.module(), + None, + Some(types), + None, + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + assert_eq!(blocks.iter().filter(|block| block.is_property).count(), 1); + assert_eq!( + blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations, + Some(100) + ); + } +} + +#[test] +fn resource_absent_aggregate_shape_checks_reject_wrong_elements_keys_and_sum_payloads() { + for release in [false, true] { + let checked = program(SOURCE, release); + let runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + let bad = vec![ + ("empty_tokens", Value::String("not a list".into())), + ("empty_tokens", Value::List(vec![Value::Nothing])), + ("absent_tokens", Value::List(vec![Value::Int64(1)])), + ( + "absent_tokens", + Value::List(vec![Value::OptionalSome(Box::new(Value::Nothing))]), + ), + ("empty_map", Value::List(Vec::new())), + ( + "absent_map", + Value::Map(vec![(Value::Int64(1), Value::OptionalNone)]), + ), + ( + "absent_map", + Value::Map(vec![(Value::String("empty".into()), Value::Nothing)]), + ), + ( + "empty_result_tokens", + Value::ResultOk(Box::new(Value::String("not a list".into()))), + ), + ( + "failed_result_tokens", + Value::ResultFail(Box::new(Value::Int64(1))), + ), + ( + "absent_tokens", + Value::Typed { + type_name: "list[optional[string]]".into(), + value: Box::new(Value::List(Vec::new())), + }, + ), + ]; + for (name, value) in bad { + assert_eq!( + runtime.validate_value_type( + &EvaluatedValue::ordinary(value), + returned_type(&checked, name) + ), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + let signature = + checked.checked().definition_types[&source_entry(&checked, "refined_shape_contract")]; + let Type::Function { params, .. } = checked.checked().interner.resolve(signature) else { + panic!("exact refined shape contract"); + }; + let refined = params[0]; + for marker in [0, 17] { + let unproved = Value::Struct { + type_name: "app.RefinedEnvelope".into(), + concrete_type: None, + fields: vec![ + ( + "marker".into(), + Value::Typed { + type_name: "app.PositiveMarker".into(), + value: Box::new(Value::Int64(marker)), + }, + ), + ("tokens".into(), Value::List(Vec::new())), + ], + }; + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(unproved), refined), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + } + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[test] +fn resource_absent_nominal_shape_checks_reject_foreign_names_fields_variants_states_and_generics() { + for release in [false, true] { + let checked = program(SOURCE, release); + let runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + for (name, value) in absent_values() + .into_iter() + .filter(|(name, _)| name.starts_with("absent_") || *name == "empty_choice") + { + let ty = returned_type(&checked, name); + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(value.clone()), ty), + Ok(()) + ); + let mut corruptions = Vec::new(); + match &value { + Value::Struct { .. } => { + let mut wrong = value.clone(); + if let Value::Struct { type_name, .. } = &mut wrong { + *type_name = "foreign.SameShape".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields[0].0 = "foreign".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.pop(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.push(fields[0].clone()); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { concrete_type, .. } = &mut wrong { + *concrete_type = Some("app.TokenBox[string]".into()); + } + corruptions.push(wrong); + if name == "absent_envelope" { + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields.swap(0, 1); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Struct { fields, .. } = &mut wrong { + fields[2].1 = Value::String("17".into()); + } + corruptions.push(wrong); + } + } + Value::Enum { .. } => { + let mut wrong = value.clone(); + if let Value::Enum { type_name, .. } = &mut wrong { + *type_name = "foreign.TokenChoice".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { variant, .. } = &mut wrong { + *variant = "missing".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { fields, .. } = &mut wrong { + fields.push(Value::Nothing); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Enum { + variant, fields, .. + } = &mut wrong + { + *variant = "occupied".into(); + *fields = vec![Value::Nothing]; + } + corruptions.push(wrong); + } + Value::Machine { .. } => { + let mut wrong = value.clone(); + if let Value::Machine { type_name, .. } = &mut wrong { + *type_name = "foreign.TokenState".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { state, .. } = &mut wrong { + *state = "missing".into(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { fields, .. } = &mut wrong { + fields.pop(); + } + corruptions.push(wrong); + let mut wrong = value.clone(); + if let Value::Machine { state, fields, .. } = &mut wrong { + *state = "occupied".into(); + *fields = vec![Value::Nothing]; + } + corruptions.push(wrong); + if name == "absent_exact_state" { + let mut wrong = value.clone(); + if let Value::Machine { state, .. } = &mut wrong { + *state = "waiting".into(); + } + corruptions.push(wrong); + } + } + _ => continue, + } + for wrong in corruptions { + assert_eq!( + runtime.validate_value_type(&EvaluatedValue::ordinary(wrong), ty), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + } + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[test] +fn resource_absent_shape_validation_keeps_occupied_aggregate_custody_refused() { + use crate::resource_execution::{ProviderEvent, ScriptOperation}; + use jett_types::ResourceHookKind; + + for release in [false, true] { + let checked = program(SOURCE, release); + let mut runtime = + ResourceTransport::checked_only(checked.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 1701, + outcome: Ok(()), + }]) + .unwrap(); + let definition = checked + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *checked + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let produced = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant.clone())), + EvaluatedValue::ordinary(Value::Int64(1701)), + ], + ) + .unwrap(); + let Value::ResultOk(token) = &produced.value else { + panic!("occupied factory result"); + }; + // Physical test copies have no custody; no aggregate owner path is + // authorized by the new absent-layout proof. + let malformed = vec![ + ("empty_tokens", Value::List(vec![token.as_ref().clone()])), + ( + "absent_map", + Value::Map(vec![( + Value::String("occupied".into()), + Value::OptionalSome(token.clone()), + )]), + ), + ( + "absent_envelope", + Value::Struct { + type_name: "app.TokenEnvelope".into(), + concrete_type: None, + fields: vec![ + ("tokens".into(), Value::List(vec![token.as_ref().clone()])), + ("fallback".into(), Value::OptionalNone), + ("marker".into(), Value::Int64(17)), + ], + }, + ), + ( + "empty_choice", + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "occupied".into(), + fields: vec![token.as_ref().clone()], + }, + ), + ( + "absent_state", + Value::Machine { + type_name: "app.TokenState".into(), + state: "occupied".into(), + fields: vec![token.as_ref().clone()], + }, + ), + ( + "empty_tokens", + Value::List(vec![Value::GrantedNetwork(grant)]), + ), + ]; + for (name, value) in malformed { + assert_eq!( + runtime.validate_value_type( + &EvaluatedValue::ordinary(value), + returned_type(&checked, name) + ), + Err(ResourceExecutionError::InvalidPayloadPath), + "{name}" + ); + } + assert_eq!(runtime.live_owners(), 1); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ + ProviderEvent::Constructed(1701), + ProviderEvent::Finalized(1701) + ] + ); + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + } +} + +#[path = "absence_tests/augmented_baseline.rs"] +mod augmented_baseline; diff --git a/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs b/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs new file mode 100644 index 00000000..c4342c49 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absence_tests/augmented_baseline.rs @@ -0,0 +1,258 @@ +//! Distinct original-constructor/worker gate under the existing absence tests. +//! Reuse its twelve exact typed Value goldens, rather than the new main guards. +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn original_calls( + checked: &Arc, + purpose: ExecutionPurpose, + release: bool, + entered: &mut Vec, +) -> Result<(), inputs::Failure> { + use inputs::{Phase, enter, failure}; + let mut failures = Vec::new(); + let mut calls = 0; + for (name, expected) in absent_values() { + enter(entered, Phase::WorkerEligibility); + let setup = inputs::attempt(|stages| { + enter(stages, Phase::WorkerEligibility); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose) + .map_err(|error| failure(Phase::WorkerEligibility, error))?; + let installation = interpreter.install_resource_test_script(Vec::new()); + if (purpose == ExecutionPurpose::ReferenceRuntime) != installation.is_ok() { + return Err(failure(Phase::WorkerEligibility, installation)); + } + let baseline = interpreter + .resource_test_entry_context() + .map_err(|error| failure(Phase::WorkerEligibility, error))?; + enter(stages, Phase::OriginalConstructorCall); + let target = source_entry(checked, name); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, Vec::new()) + })); + let actual_text = match &actual { + Ok(result) => format!("{result:?}"), + Err(payload) => format!( + "UnexpectedPanic({:?})", + inputs::panic_message(payload.as_ref()) + ), + }; + let call_outcome = match actual { + Ok(Ok(actual)) if actual == expected => Ok(()), + Ok(actual) => Err(failure(Phase::OriginalConstructorCall, (actual, expected))), + Err(payload) => Err(inputs::Failure { + phase: Phase::OriginalConstructorCall, + kind: inputs::FailureKind::UnexpectedPanic, + detail: inputs::panic_message(payload.as_ref()), + }), + }; + enter(stages, Phase::ReferenceObservations); + let effects = interpreter.resource_test_observations(); + let custody = interpreter.resource_test_custody_counts(); + let context = interpreter.resource_test_entry_context(); + let debug = interpreter.take_debug_events(); + eprintln!( + "ABSENT_ORIGINAL name={name:?} purpose={purpose:?} release={release} observed={actual_text:?} effects={effects:?} custody={custody:?} context={context:?} debug={debug:?}" + ); + let effects_match = if purpose == ExecutionPurpose::ReferenceRuntime { + effects == Ok((Vec::new(), 0, 0)) + } else { + effects == Err(ResourceExecutionError::ProviderDisabled.to_string()) + }; + if !effects_match + || custody != Ok((0, 0)) + || context.as_ref() != Ok(&baseline) + || !debug.is_empty() + { + if let Err(mut first) = call_outcome { + first.detail.push_str("; constructor effects/custody/context/debug invariant also failed; see actual log"); + return Err(first); + } + return Err(failure( + Phase::ReferenceObservations, + "constructor effects/custody/context/debug changed; see log", + )); + } + call_outcome + }); + calls += 1; + if let Err(error) = setup.outcome { + eprintln!( + "ABSENT_ORIGINAL_FAILURE name={name:?} purpose={purpose:?} release={release} entered={:?} failure={error:?}", + setup.entered + ); + failures.push((name, error)); + } + } + eprintln!( + "ABSENT_ORIGINAL_TOTAL purpose={purpose:?} release={release} calls={calls} failed={}", + failures.len() + ); + if calls != 12 || !failures.is_empty() { + if let Some((_, first)) = failures.first() { + return Err(inputs::Failure { + phase: first.phase, + kind: first.kind, + detail: format!("{calls} original constructor attempts; failures={failures:#?}"), + }); + } + return Err(failure(Phase::OriginalConstructorCall, calls)); + } + Ok(()) +} + +#[test] +fn source17_augmented_original_constructors_keep_exact_values_in_all_five_purposes() { + use inputs::{Phase, failure}; + let input = inputs::INPUTS.last().unwrap(); // Same entire prefix, simple required main wrapper. + let mut attempted = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::ReferenceRuntime, + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + attempted += 1; + let attempt = inputs::attempt(|entered| { + let (checked, _) = inputs::prepare(input, release, entered)?; + original_calls(&checked, purpose, release, entered) + }); + if let Err(error) = attempt.outcome { + eprintln!( + "ABSENT_PURPOSE_FAILURE purpose={purpose:?} release={release} entered={:?} failure={error:?}", + attempt.entered + ); + failures.push((purpose, release, error)); + } + } + } + assert_eq!(attempted, 10); + assert!( + failures.is_empty(), + "{}", + failure(Phase::OriginalConstructorCall, failures).detail + ); + // Ten accepted preparations imply 120 separately logged exact typed calls. +} + +#[test] +fn source17_augmented_required_capture_and_original_verify_property100_are_separate() { + use inputs::{Phase, enter, failure}; + let input = inputs::INPUTS.last().unwrap(); + let mut failures = Vec::new(); + let mut attempts = 0; + for release in [false, true] { + attempts += 1; + let attempt = inputs::attempt(|entered| { + let (checked, _) = inputs::prepare(input, release, entered)?; + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + enter(entered, Phase::RequiredCapture); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + eprintln!( + "ABSENT_REQUIRED release={release} diagnostics={:?} debug={:?} values={:?}", + required.diagnostics, + required.debug_events, + required.values.values().collect::>() + ); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(&checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + let declarations = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "missing original namespace constant", + )); + }; + if required.values.constant(*declaration) != Some(&Value::Int64(17)) + || !required.values.checked_values_are_mirrored() + || required + .values + .values() + .any(Value::contains_live_resource_or_grant) + { + return Err(failure( + Phase::RequiredCache, + "constant/mirror/authority contract failed", + )); + } + enter(entered, Phase::VerifyPropertyWorkers); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + checked.module(), + None, + Some(types), + None, + ); + eprintln!("ABSENT_VERIFY_PROPERTY release={release} blocks={blocks:#?}"); + if blocks.len() != 2 + || !blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()) + || blocks.iter().filter(|block| block.is_property).count() != 1 + || blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations + != Some(100) + { + return Err(failure(Phase::VerifyPropertyWorkers, blocks)); + } + Ok(()) + }); + if let Err(error) = attempt.outcome { + eprintln!( + "ABSENT_WORKERS_FAILURE release={release} entered={:?} failure={error:?}", + attempt.entered + ); + failures.push((release, error)); + } + } + assert_eq!(attempts, 2); + assert!( + failures.is_empty(), + "separate required/verify/property failure: {failures:#?}" + ); + // This reference-worker result is not native suite execution evidence. +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs b/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs new file mode 100644 index 00000000..427b4ff0 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated.rs @@ -0,0 +1,291 @@ +//! Publication admits checked inactive data, never occupied aggregate custody. +use super::super::checked::PreparedAbsentBindings; +use super::*; +use jett_parser::ast::{ForStmt, Ident, MatchStmt}; + +impl ResourceTransport { + fn validate_absent_parent( + &self, + proof: &PreparedAbsentBindings<'_>, + value: &Value, + ) -> Result<(), ResourceExecutionError> { + if !self.checked_source_active || value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.checked.revalidate_absent_bindings(proof)?; + self.validate_value_type( + &EvaluatedValue::ordinary(value.clone()), + proof.parent_type(), + )?; + proof.validate_parent(value) + } + + pub(crate) fn prepare_absent_for<'source>( + &self, + source: &'source ForStmt, + value: &Value, + ) -> Result>, ResourceExecutionError> { + if !self.checked_source_active { + return Ok(None); + } + let proof = self.checked.prepare_absent_for(source)?; + if let Some(proof) = &proof { + self.validate_absent_parent(proof, value)?; + } + Ok(proof) + } + + pub(crate) fn prepare_absent_match<'source>( + &self, + source: &'source MatchStmt, + arm: usize, + value: &Value, + ) -> Result>, ResourceExecutionError> { + if !self.checked_source_active { + return Ok(None); + } + let proof = self.checked.prepare_absent_match(source, arm)?; + if let Some(proof) = &proof { + self.validate_absent_parent(proof, value)?; + } + Ok(proof) + } + + pub(crate) fn validate_absent_binding( + &self, + proof: &PreparedAbsentBindings<'_>, + binder: &Ident, + ordinal: usize, + value: &Value, + ) -> Result { + if !self.checked_source_active || value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + self.checked.revalidate_absent_bindings(proof)?; + let fact = proof.validate_binding(binder, ordinal)?; + absent_shape(value, fact.ty, &self.checked)?; + let ordinary = EvaluatedValue::ordinary(value.clone()); + self.validate_value_type(&ordinary, fact.ty)?; + Ok(fact) + } + + pub(crate) fn publish_absent_binding( + &mut self, + proof: &PreparedAbsentBindings<'_>, + binder: &Ident, + ordinal: usize, + value: Value, + ) -> Result<(), ResourceExecutionError> { + let fact = self.validate_absent_binding(proof, binder, ordinal, &value)?; + if self.checked.type_contains_resource(fact.ty)? { + self.install_binding(fact.declaration_span, EvaluatedValue::ordinary(value))?; + } + Ok(()) + } +} + +// Absence is a value/layout proof, not an exemption based on the lack +// of a carrier. Recurse through every occupied slot of the exact type. +pub(super) fn absent_shape( + value: &Value, + ty: jett_types::TypeId, + checked: &CheckedExecution, +) -> Result<(), ResourceExecutionError> { + use jett_types::Type; + let types = &checked.program().checked().interner; + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if value.contains_live_resource_or_grant() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let kind = types.resolve(ty); + // A layout match cannot replace the runtime predicate boundary. + // Resource-bearing return normalization does not prove it here. + if matches!(kind, Type::Refinement { .. }) { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + if let Value::Typed { type_name, value } = value { + if type_name != &types.type_name(ty) { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let inner = match kind { + Type::Secret(inner) => *inner, + _ => ty, + }; + return absent_shape(value, inner, checked); + } + let fields = |values: &[(String, Value)], expected: &[(String, jett_types::TypeId)]| { + if values.len() != expected.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for ((name, value), (expected_name, field_type)) in values.iter().zip(expected) { + if name != expected_name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + absent_shape(value, *field_type, checked)?; + } + Ok(()) + }; + let payloads = |values: &[Value], expected: &[(String, jett_types::TypeId)]| { + if values.len() != expected.len() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + for (value, (_, field_type)) in values.iter().zip(expected) { + absent_shape(value, *field_type, checked)?; + } + Ok(()) + }; + match (value, kind) { + (value, Type::Secret(inner)) => absent_shape(value, *inner, checked), + (Value::Int64(number), Type::Int8) if i8::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Int16) if i16::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Int32) if i32::try_from(*number).is_ok() => Ok(()), + (Value::Int64(_), Type::Int64) => Ok(()), + (Value::Int64(number), Type::Uint8) if u8::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Uint16) if u16::try_from(*number).is_ok() => Ok(()), + (Value::Int64(number), Type::Uint32) if u32::try_from(*number).is_ok() => Ok(()), + (Value::Uint64(_), Type::Uint64) + | (Value::Float64(_), Type::Float32 | Type::Float64) + | (Value::String(_), Type::String) + | (Value::Bool(_), Type::Bool) + | (Value::Bytes(_), Type::Bytes) + | (Value::Nothing, Type::Nothing) => Ok(()), + (Value::List(values), Type::List(inner)) | (Value::Set(values), Type::Set(inner)) => { + for value in values { + absent_shape(value, *inner, checked)?; + } + Ok(()) + } + (Value::Map(values), Type::Map(key, item)) => { + for (key_value, item_value) in values { + absent_shape(key_value, *key, checked)?; + absent_shape(item_value, *item, checked)?; + } + Ok(()) + } + (Value::OptionalNone, Type::Optional(_)) => Ok(()), + (Value::OptionalSome(value), Type::Optional(inner)) => absent_shape(value, *inner, checked), + (Value::ResultOk(value), Type::Result(success, _)) => { + absent_shape(value, *success, checked) + } + (Value::ResultFail(value), Type::Result(_, error)) => absent_shape(value, *error, checked), + ( + Value::Struct { + type_name, + concrete_type, + fields: values, + }, + Type::Struct(owner), + ) => { + let definition = types.resolve_struct(*owner); + let arguments = types.nominal_type_arguments(ty); + let header_matches = if arguments.is_empty() { + type_name == &definition.name + && concrete_type + .as_ref() + .is_none_or(|name| name == &definition.name) + } else { + let physical_instance = format!( + "{}[{}]", + type_name, + arguments + .iter() + .map(|argument| types.type_name(*argument)) + .collect::>() + .join(", ") + ); + physical_instance == definition.name + && concrete_type.as_deref() == Some(definition.name.as_str()) + }; + if !header_matches { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + fields(values, &definition.fields) + } + ( + Value::Enum { + type_name, + variant, + fields: values, + }, + Type::Enum(owner), + ) => { + let definition = types.resolve_enum(*owner); + if type_name != &definition.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut variants = definition + .variants + .iter() + .filter(|candidate| candidate.name == *variant); + let selected = variants + .next() + .ok_or(ResourceExecutionError::InvalidPayloadPath)?; + if variants.next().is_some() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + payloads(values, &selected.fields) + } + ( + Value::Machine { + type_name, + state, + fields: values, + }, + Type::Machine(owner), + ) => { + let definition = types.resolve_machine(*owner); + if type_name != &definition.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + let mut states = definition + .states + .iter() + .filter(|candidate| candidate.name == *state); + let selected = states + .next() + .ok_or(ResourceExecutionError::InvalidPayloadPath)?; + if states.next().is_some() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + payloads(values, &selected.fields) + } + ( + Value::Machine { + type_name, + state, + fields: values, + }, + Type::MachineState { + machine, + state: expected, + }, + ) => { + let definition = types.resolve_machine(*machine); + let selected = definition + .state(*expected) + .ok_or(ResourceExecutionError::InvalidCheckedProgram)?; + if type_name != &definition.name || state != &selected.name { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + payloads(values, &selected.fields) + } + (Value::ResourceHook(descriptor), Type::Function { .. }) + if Arc::ptr_eq(&descriptor.program, checked.program()) + && checked + .program() + .checked() + .resource_hooks + .get(&descriptor.definition) + .is_some_and(|hook| hook.function_type == ty) => + { + Ok(()) + } + _ => Err(ResourceExecutionError::InvalidPayloadPath), + } +} + +#[cfg(test)] +#[path = "absent_generated/tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs new file mode 100644 index 00000000..c7c2e23c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/tests.rs @@ -0,0 +1,549 @@ +//! Source17 executions remain distinct from the private malformed-value controls. +use super::*; +use crate::resource_execution::{ExecutionPurpose, ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; +use jett_common::FileId; +use jett_parser::ast::{ForStmt, FunctionDef, Item, MatchStmt, Pattern, Stmt}; +use jett_types::ResourceHookKind; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn checked_program(index: usize, release: bool) -> Arc { + inputs::prepare(&inputs::INPUTS[index], release, &mut Vec::new()) + .unwrap() + .0 +} + +fn function<'a>(program: &'a CheckedResourceProgram, name: &str) -> &'a FunctionDef { + program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .unwrap() +} + +fn install_main(runtime: &mut ResourceTransport, program: &CheckedResourceProgram) { + let definition = runtime + .checked + .declaration_definition(function(program, "main").name.span) + .unwrap(); + let body = runtime + .checked + .prepare_function_body(&runtime.checked.entry(definition).unwrap()) + .unwrap(); + runtime.checked.install_function_body(&body).unwrap(); +} + +fn for_statement(program: &CheckedResourceProgram) -> &ForStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::For(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn match_statement(program: &CheckedResourceProgram) -> &MatchStmt { + function(program, "main") + .body + .stmts + .iter() + .find_map(|statement| match statement { + Stmt::Match(statement) => Some(statement), + _ => None, + }) + .unwrap() +} + +fn parent(index: usize) -> Value { + match index { + 1 => Value::List(vec![Value::OptionalNone, Value::OptionalNone]), + 3 => Value::Map(vec![(Value::String("empty".into()), Value::OptionalNone)]), + 9 => Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + _ => unreachable!(), + } +} + +fn absent_runtime(index: usize, release: bool) -> (Arc, ResourceTransport) { + let program = checked_program(index, release); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + install_main(&mut runtime, &program); + (program, runtime) +} + +fn clean(runtime: &ResourceTransport) { + assert_eq!(runtime.live_owners(), 0); + assert_eq!(runtime.registry_live_count(), 0); + assert_eq!( + runtime.provider_events(), + Err(ResourceExecutionError::ProviderDisabled) + ); +} + +fn execute_twice(input: &inputs::Input, release: bool) -> usize { + let (checked, _) = inputs::prepare(input, release, &mut Vec::new()).unwrap(); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + assert!( + required.diagnostics.is_empty(), + "{} release={release}: {:?}", + input.name, + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(&checked).unwrap(); + assert!(required.values.checked_values_are_mirrored()); + assert!( + required + .values + .values() + .all(|value| !value.contains_live_resource_or_grant()) + ); + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let main_span = function(&checked, "main").name.span; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == main_span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("unique original main identity") + }; + let grant = interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + let baseline = interpreter.resource_test_entry_context().unwrap(); + for repeat in 0..2 { + let actual = interpreter.call_checked_program_entry(*definition, vec![grant.clone()]); + match (input.reference, actual) { + (inputs::ReferenceExpectation::Nothing, Ok(Value::Nothing)) => {} + (inputs::ReferenceExpectation::Error(expected), Err(actual)) if actual == expected => {} + (_, actual) => panic!( + "{} release={release} repeat={repeat}: {actual:?}", + input.name + ), + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), baseline); + assert!(interpreter.take_debug_events().is_empty()); + } + 2 +} + +#[test] +fn absent_generated_unchanged_source17_wrappers_repeat_exact_outcomes_and_restore_entry() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut entries = 0; + for release in [false, true] { + for input in inputs::INPUTS { + entries += execute_twice(input, release); + } + } + assert_eq!(entries, 56); +} + +#[test] +fn absent_generated_view_continue_break_and_post_loop_reuse_restore_every_entry() { + let input = inputs::Input { + name: "source17_view_continue_break_reuse", + source: include_str!("view_control.jett"), + reference: inputs::ReferenceExpectation::Nothing, + }; + let mut entries = 0; + for release in [false, true] { + entries += execute_twice(&input, release); + } + assert_eq!(entries, 4); +} + +#[test] +fn absent_generated_parent_child_shapes_and_duplicate_publication_remain_checked() { + for release in [false, true] { + for index in [1, 3, 9] { + let (program, mut runtime) = absent_runtime(index, release); + let original = parent(index); + if index == 1 { + let types = &program.checked().interner; + let refined = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.RefinedEnvelope") + .expect("original retained RefinedEnvelope"); + let jett_types::Type::Struct(owner) = types.resolve(refined) else { + panic!("original RefinedEnvelope struct"); + }; + let marker = types + .resolve_struct(*owner) + .fields + .iter() + .find(|(name, _)| name == "marker") + .unwrap() + .1; + assert_eq!(types.type_name(marker), "app.PositiveMarker"); + let claimed = Value::Typed { + type_name: "app.PositiveMarker".into(), + value: Box::new(Value::Int64(17)), + }; + for value in [Value::Int64(17), claimed.clone()] { + assert_eq!( + absent_shape(&value, marker, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + } + let unproved = Value::Struct { + type_name: "app.RefinedEnvelope".into(), + concrete_type: None, + fields: vec![ + ("marker".into(), claimed), + ("tokens".into(), Value::List(Vec::new())), + ], + }; + assert_eq!( + absent_shape(&unproved, refined, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + clean(&runtime); + } + let bad_parents = match index { + 1 => vec![ + Value::List(vec![Value::Nothing]), + Value::List(vec![Value::Int64(1)]), + Value::List(vec![Value::OptionalSome(Box::new(Value::Nothing))]), + Value::Typed { + type_name: "list[optional[string]]".into(), + value: Box::new(original.clone()), + }, + ], + 3 => vec![ + Value::Map(vec![(Value::Int64(1), Value::OptionalNone)]), + Value::Map(vec![(Value::String("empty".into()), Value::Nothing)]), + Value::List(Vec::new()), + ], + 9 => vec![ + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "empty".into(), + fields: Vec::new(), + }, + Value::Enum { + type_name: "foreign.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17), Value::List(Vec::new())], + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "missing".into(), + fields: Vec::new(), + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::List(Vec::new()), Value::Int64(17)], + }, + Value::Enum { + type_name: "app.TokenChoice".into(), + variant: "vacant".into(), + fields: vec![Value::Int64(17)], + }, + ], + _ => unreachable!(), + }; + for wrong in bad_parents { + let result = if index == 9 { + runtime.prepare_absent_match(match_statement(&program), 0, &wrong) + } else { + runtime.prepare_absent_for(for_statement(&program), &wrong) + }; + assert!(result.is_err(), "index={index}, malformed parent={wrong:?}"); + clean(&runtime); + } + if index == 9 { + assert!( + runtime + .prepare_absent_match(match_statement(&program), 1, &original) + .is_err(), + "vacant belongs to the earlier original variant arm, not Other" + ); + clean(&runtime); + } + let proof = if index == 9 { + runtime + .prepare_absent_match(match_statement(&program), 0, &original) + .unwrap() + .unwrap() + } else { + runtime + .prepare_absent_for(for_statement(&program), &original) + .unwrap() + .unwrap() + }; + let (binder, ordinal, valid) = match index { + 1 => (&for_statement(&program).variable, 0, Value::OptionalNone), + 3 => ( + for_statement(&program).value_variable.as_ref().unwrap(), + 1, + Value::OptionalNone, + ), + 9 => { + let Pattern::Variant(_, names) = &match_statement(&program).arms[0].pattern + else { + unreachable!() + }; + (&names[1], 1, Value::List(Vec::new())) + } + _ => unreachable!(), + }; + let fact = proof.validate_binding(binder, ordinal).unwrap(); + runtime.push_scope(); + if index != 1 { + let (scalar_binder, scalar, wrong_scalar) = if index == 3 { + ( + &for_statement(&program).variable, + Value::String("empty".into()), + Value::Int64(17), + ) + } else { + let Pattern::Variant(_, names) = &match_statement(&program).arms[0].pattern + else { + unreachable!() + }; + (&names[0], Value::Int64(17), Value::String("17".into())) + }; + let scalar_fact = proof.validate_binding(scalar_binder, 0).unwrap(); + assert!( + runtime + .validate_absent_binding(&proof, scalar_binder, 0, &wrong_scalar) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, scalar_binder, 0, wrong_scalar) + .is_err() + ); + runtime + .publish_absent_binding(&proof, scalar_binder, 0, scalar) + .unwrap(); + assert!( + !runtime.has_binding(scalar_fact.definition), + "ordinary scalar child must not acquire a Resource slot" + ); + } + assert!( + runtime + .publish_absent_binding(&proof, &binder.clone(), ordinal, valid.clone()) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, binder, usize::MAX, valid.clone()) + .is_err() + ); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, Value::Nothing) + .is_err() + ); + assert!(!runtime.has_binding(fact.definition)); + runtime + .publish_absent_binding(&proof, binder, ordinal, valid.clone()) + .unwrap(); + assert!(runtime.has_binding(fact.definition)); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, valid.clone()) + .is_err() + ); + let borrowed = runtime.binding(fact.definition, true).unwrap(); + assert_eq!(borrowed.value, valid); + assert!(borrowed.custody.is_empty()); + assert_eq!( + runtime.binding(fact.definition, false).unwrap().value, + valid + ); + assert!(runtime.binding(fact.definition, false).is_err()); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert!(!runtime.has_binding(fact.definition)); + runtime.checked_source_active = false; + let inactive = if index == 9 { + runtime.prepare_absent_match(match_statement(&program), 0, &original) + } else { + runtime.prepare_absent_for(for_statement(&program), &original) + }; + assert!(matches!(inactive, Ok(None))); + assert!( + runtime + .publish_absent_binding(&proof, binder, ordinal, valid) + .is_err() + ); + clean(&runtime); + } + } +} + +#[test] +fn absent_generated_binding_does_not_require_unrelated_live_owners_to_be_zero() { + for release in [false, true] { + let program = checked_program(1, release); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let grant = runtime + .install_script(vec![ScriptOperation::Construct { + label: 1717, + outcome: Ok(()), + }]) + .unwrap(); + let definition = program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *program + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let owner = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant.clone())), + EvaluatedValue::ordinary(Value::Int64(1717)), + ], + ) + .unwrap(); + let Value::ResultOk(token) = &owner.value else { + panic!("original factory success") + }; + assert!(!owner.custody.is_empty()); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + install_main(&mut runtime, &program); + let statement = for_statement(&program); + let original = parent(1); + let proof = runtime + .prepare_absent_for(statement, &original) + .unwrap() + .unwrap(); + let fact = proof.validate_binding(&statement.variable, 0).unwrap(); + runtime.push_scope(); + for live in [ + Value::OptionalSome(token.clone()), + Value::GrantedNetwork(grant), + ] { + assert!( + runtime + .publish_absent_binding(&proof, &statement.variable, 0, live.clone()) + .is_err() + ); + assert!( + runtime + .prepare_absent_for(statement, &Value::List(vec![live])) + .is_err() + ); + assert!(!runtime.has_binding(fact.definition)); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + } + runtime + .publish_absent_binding(&proof, &statement.variable, 0, Value::OptionalNone) + .unwrap(); + assert_eq!( + runtime.binding(fact.definition, true).unwrap().value, + Value::OptionalNone + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ProviderEvent::Constructed(1717)] + ); + runtime.pop_scope(); + runtime.check_cleanup().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + vec![ + ProviderEvent::Constructed(1717), + ProviderEvent::Finalized(1717) + ] + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett new file mode 100644 index 00000000..2dd17ff5 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/absent_generated/view_control.jett @@ -0,0 +1,129 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +function source17_absent_view_list() returns bool: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + mutable int64 continued = 0 + for candidate in view values: + if source17_absent_optional(view candidate): + continued = continued + 1 + continue + return false + if continued != 2 || list.length[optional[resource_probe.TestHandle]](view values) != 2: + return false + mutable int64 broken = 0 + for candidate in view values: + if !source17_absent_optional(view candidate): + return false + broken = broken + 1 + break + if broken != 1 || list.length[optional[resource_probe.TestHandle]](view values) != 2: + return false + return true +function source17_absent_map_item(key: string, view candidate: optional[resource_probe.TestHandle]) returns bool: + return key == "empty" && source17_absent_optional(view candidate) +function source17_absent_view_map() returns bool: + map[string, optional[resource_probe.TestHandle]] entries = absent_map() + mutable int64 continued = 0 + for key, candidate in view entries: + if source17_absent_map_item(key, view candidate): + continued = continued + 1 + continue + return false + if continued != 1 || map.length[string, optional[resource_probe.TestHandle]](view entries) != 1: + return false + mutable int64 broken = 0 + for key, candidate in view entries: + if !source17_absent_map_item(key, view candidate): + return false + broken = broken + 1 + break + if broken != 1 || map.length[string, optional[resource_probe.TestHandle]](view entries) != 1: + return false + return true +export function main(net: Network) returns nothing: + if !source17_absent_view_list(): + int64 ignored = resource_probe.terminal_label() + if !source17_absent_view_map(): + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/runtime/assignment.rs b/crates/jett_comptime/src/resource_execution/runtime/assignment.rs new file mode 100644 index 00000000..988eca79 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/assignment.rs @@ -0,0 +1,94 @@ +//! A declared slot can outlive its current owner; replacement never copies one. +use super::super::checked::CheckedAssignment; +use super::*; +use jett_parser::ast::AssignStmt; + +pub(crate) struct PreparedResourceAssignment<'source> { + checked: CheckedAssignment<'source>, + scope_index: usize, + frame: FrameId, +} + +impl PreparedResourceAssignment<'_> { + pub(crate) fn scope_index(&self) -> usize { + self.scope_index + } +} + +impl ResourceTransport { + pub(crate) fn prepare_resource_assignment<'source>( + &self, + source: &'source AssignStmt, + ) -> Result, ResourceExecutionError> { + let checked = self.checked.prepare_assignment(source)?; + let fact = checked.fact(); + let scope_index = self + .scopes + .iter() + .rposition(|(_, bindings)| bindings.contains_key(&fact.definition)) + .ok_or(ResourceExecutionError::InvalidOwner)?; + let (frame, bindings) = &self.scopes[scope_index]; + let slot = bindings + .get(&fact.definition) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if slot.fact != fact || !self.ledger.frame_is_live(*frame) { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(PreparedResourceAssignment { + checked, + scope_index, + frame: *frame, + }) + } + + fn assignment_slot_mut( + &mut self, + destination: &PreparedResourceAssignment<'_>, + ) -> Result<&mut ResourceBindingSlot, ResourceExecutionError> { + let fact = destination.checked.fact(); + let (frame, bindings) = self + .scopes + .get_mut(destination.scope_index) + .ok_or(ResourceExecutionError::InvalidFrame)?; + if *frame != destination.frame || !self.ledger.frame_is_live(*frame) { + return Err(ResourceExecutionError::InvalidFrame); + } + let slot = bindings + .get_mut(&fact.definition) + .ok_or(ResourceExecutionError::InvalidOwner)?; + if slot.fact != fact { + return Err(ResourceExecutionError::InvalidOwner); + } + Ok(slot) + } + + pub(crate) fn replace_resource_assignment( + &mut self, + destination: PreparedResourceAssignment<'_>, + mut value: EvaluatedValue, + ) -> Result<(), ResourceExecutionError> { + self.checked.revalidate_assignment(&destination.checked)?; + self.assignment_slot_mut(&destination)?; + let fact = destination.checked.fact(); + self.validate_value_type(&value, fact.ty)?; + if value.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + // Keep the new owner in the active RHS operation until old-value + // cleanup completes. A finalizer panic leaves both cleanup paths live. + self.hold_actual(&mut value)?; + let previous = self.assignment_slot_mut(&destination)?.value.take(); + if let Some(previous) = previous { + self.discard_value(previous)?; + } + self.ledger.transfer( + &mut value.custody, + OwnerHolder::Binding { + frame: destination.frame, + definition: fact.definition, + }, + )?; + self.assignment_slot_mut(&destination)?.value = Some(value); + Ok(()) + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs b/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs new file mode 100644 index 00000000..2434874d --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/owned_struct.rs @@ -0,0 +1,104 @@ +//! An aggregate is a new holder root, never a collection of copied raw tokens. +use super::super::checked::PreparedOwnedStruct; +use super::*; +use jett_parser::ast::Expr; + +/// Validate the exact checked carrier, without rounding a supplied value or +/// granting a predicate transition to a merely layout-compatible payload. +pub(super) fn companion_shape( + value: &Value, + ty: jett_types::TypeId, + checked: &CheckedExecution, +) -> Result<(), ResourceExecutionError> { + let types = &checked.program().checked().interner; + if ty.index() as usize >= types.len() { + return Err(ResourceExecutionError::InvalidCheckedProgram); + } + if !PreparedOwnedStruct::supports_companion(types.resolve(ty)) { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + // Pending layers belong to the transported field, not to an inspection of + // it. Validate the stored carrier without consuming or rewriting any layer. + let mut payload = value; + loop { + match payload { + Value::Pending(inner) => payload = inner, + Value::Typed { type_name, value } if type_name == &types.type_name(ty) => { + payload = value; + } + _ => break, + } + } + super::absent_generated::absent_shape(payload, ty, checked)?; + if matches!(types.resolve(ty), jett_types::Type::Float32) { + let Value::Float64(number) = payload.payload() else { + return Err(ResourceExecutionError::InvalidPayloadPath); + }; + // This cast is the language's binary32 rounding boundary. Validate its + // exact widening instead of modifying an untrusted physical carrier. + let widened = f64::from(*number as f32); + if !number.is_nan() && number.to_bits() != widened.to_bits() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + } + Ok(()) +} + +impl ResourceTransport { + pub(crate) fn assemble_owned_struct( + &mut self, + proof: &PreparedOwnedStruct, + expression: &Expr, + actuals: Vec, + ) -> Result { + self.checked.revalidate_owned_struct(proof, expression)?; + if actuals.len() != proof.source_fields().len() { + return Err(ResourceExecutionError::InvalidInvocation); + } + // Validate every child before combining or migrating any holder. The + // operation retains each reached child's cleanup path on any refusal. + for (actual, ordinal) in actuals.iter().zip(proof.source_fields()) { + if actual.custody.has_borrows() { + return Err(ResourceExecutionError::InvalidOwner); + } + self.validate_value_type(actual, proof.fields()[*ordinal].1)?; + } + let mut fields: Vec> = (0..actuals.len()).map(|_| None).collect(); + let mut custody = super::super::ValueCustody::default(); + // Preserve lexical acquisition order even when field destinations are + // permuted. Struct storage order never dictates finalizer ordering. + for (actual, ordinal) in actuals.into_iter().zip(proof.source_fields()) { + let actual = actual.prefix(PayloadStep::Field(*ordinal)); + if fields[*ordinal].replace(actual.value).is_some() { + return Err(ResourceExecutionError::InvalidPayloadPath); + } + custody.append_owned(actual.custody)?; + } + let fields = proof + .fields() + .iter() + .zip(fields) + .map(|((name, _), value)| { + value + .map(|value| (name.clone(), value)) + .ok_or(ResourceExecutionError::InvalidPayloadPath) + }) + .collect::, _>>()?; + let mut value = EvaluatedValue { + value: Value::Struct { + type_name: proof.name().to_string(), + concrete_type: None, + fields, + }, + custody, + }; + self.validate_value_type(&value, proof.ty())?; + let holder = self.temporary(self.current_frame()?)?; + self.ledger.transfer(&mut value.custody, holder)?; + Ok(value) + } +} + +#[cfg(test)] +#[path = "owned_struct_tests.rs"] +mod tests; diff --git a/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs new file mode 100644 index 00000000..420b4a85 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/owned_struct_tests.rs @@ -0,0 +1,312 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; +use jett_common::FileId; +use jett_parser::ast::{Item, Stmt}; +use jett_types::ResourceHookKind; + +const SOURCE: &str = include_str!("../fixtures/33_owned_struct_scope_drop_entry.jett"); + +fn setup() -> ( + ResourceTransport, + crate::resource_execution::CheckedBodyReference, +) { + let program = program(SOURCE, false); + let function = program + .module() + .items + .iter() + .find_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) + && function.name.name == "retire_owned" => + { + Some(function) + } + _ => None, + }) + .unwrap(); + let mut runtime = + ResourceTransport::checked_only(program.clone(), ExecutionPurpose::ReferenceRuntime, 1) + .unwrap(); + runtime.checked_source_active = true; + let target = runtime + .checked + .declaration_definition(function.name.span) + .unwrap(); + let body = runtime + .checked + .prepare_function_body(&runtime.checked.entry(target).unwrap()) + .unwrap(); + runtime.checked.install_function_body(&body).unwrap(); + runtime + .install_script(vec![ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }]) + .unwrap(); + (runtime, body) +} + +fn constructed(runtime: &mut ResourceTransport) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("private scripted provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(1001)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("actual constructed owner") + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value +} + +fn declaration( + body: &crate::resource_execution::CheckedBodyReference, +) -> &jett_parser::ast::VarDecl { + let Stmt::VarDecl(declaration) = &body.function().unwrap().body.stmts[0] else { + panic!("exact occupied struct binding") + }; + declaration +} + +#[test] +fn occupied_struct_root_paths_migrate_through_operation_and_binding_without_old_finalization() { + let (mut runtime, body) = setup(); + let declaration = declaration(&body); + let expression = &declaration.value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let token = constructed(&mut runtime); + let mut aggregate = runtime + .assemble_owned_struct( + &proof, + expression, + vec![token, EvaluatedValue::ordinary(Value::Int64(17))], + ) + .unwrap(); + assert_eq!(runtime.live_owners(), 1); + let physical_copy = EvaluatedValue::ordinary(aggregate.value.clone()); + assert!(matches!( + runtime.validate_value_type(&physical_copy, proof.ty()), + Err(ResourceExecutionError::InvalidPayloadPath) + )); + runtime + .end_operation(operation, Some(&mut aggregate)) + .unwrap(); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(1001)] + ); + runtime + .install_binding(declaration.name.span, aggregate) + .unwrap(); + let binding = runtime.checked.binding(declaration.name.span).unwrap(); + let viewed = runtime.binding(binding.definition, true).unwrap(); + runtime.validate_value_type(&viewed, proof.ty()).unwrap(); + assert!(viewed.custody.has_borrows()); + assert!(!viewed.custody.has_owners()); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ] + ); +} + +#[test] +fn occupied_struct_raw_or_borrowed_members_cannot_mint_owner_paths() { + for raw_copy in [false, true] { + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + let bad = if raw_copy { + EvaluatedValue::ordinary(owner.value.clone()) + } else { + runtime.borrow(&owner).unwrap() + }; + assert!( + runtime + .assemble_owned_struct( + &proof, + expression, + vec![bad, EvaluatedValue::ordinary(Value::Int64(17)),] + ) + .is_err() + ); + assert_eq!(runtime.live_owners(), 1); + assert_eq!( + runtime.provider_events().unwrap(), + [ProviderEvent::Constructed(1001)] + ); + runtime.end_operation(operation, None).unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + runtime.unwind_all().unwrap(); + } +} + +#[test] +fn occupied_struct_wrong_scalar_and_redirected_physical_field_fail_before_publication() { + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + assert!( + runtime + .assemble_owned_struct( + &proof, + expression, + vec![ + owner, + EvaluatedValue::ordinary(Value::String("wrong scalar".into())), + ] + ) + .is_err() + ); + assert_eq!(runtime.live_owners(), 1); + runtime.end_operation(operation, None).unwrap(); + runtime.unwind_all().unwrap(); + + let (mut runtime, body) = setup(); + let expression = &declaration(&body).value; + let proof = runtime + .checked + .prepare_owned_struct(expression) + .unwrap() + .unwrap(); + let operation = runtime.begin_operation().unwrap(); + let owner = constructed(&mut runtime); + let mut aggregate = runtime + .assemble_owned_struct( + &proof, + expression, + vec![owner, EvaluatedValue::ordinary(Value::Int64(17))], + ) + .unwrap(); + let Value::Struct { fields, .. } = &mut aggregate.value else { + panic!("occupied struct") + }; + fields.swap(0, 1); + assert!(runtime.validate_value_type(&aggregate, proof.ty()).is_err()); + runtime.end_operation(operation, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} + +#[test] +fn occupied_struct_companion_validation_keeps_width_carriers_binary32_and_layers() { + use jett_types::TypeInterner as Types; + let (runtime, _) = setup(); + let accepted = [ + (Types::INT8, Value::Int64(-128)), + (Types::INT16, Value::Int64(-32768)), + (Types::INT32, Value::Int64(i64::from(i32::MIN))), + (Types::INT64, Value::Int64(i64::MIN)), + (Types::UINT8, Value::Int64(255)), + (Types::UINT16, Value::Int64(65535)), + (Types::UINT32, Value::Int64(i64::from(u32::MAX))), + (Types::UINT64, Value::Uint64(u64::MAX)), + (Types::FLOAT32, Value::Float64(f64::from(0.1f32))), + (Types::FLOAT64, Value::Float64(0.1)), + (Types::BOOL, Value::Bool(true)), + (Types::STRING, Value::String("retained".into())), + (Types::NOTHING, Value::Nothing), + ]; + for (ty, value) in accepted { + for depth in 0..=3 { + let mut transported = value.clone(); + for _ in 0..depth { + transported = Value::Pending(Box::new(transported)); + } + let before = format!("{transported:?}"); + companion_shape(&transported, ty, &runtime.checked).unwrap(); + assert_eq!( + format!("{transported:?}"), + before, + "validation must not consume a layer" + ); + } + } + for number in [-0.0, 0.0, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] { + companion_shape(&Value::Float64(number), Types::FLOAT32, &runtime.checked).unwrap(); + } + for (ty, value) in [ + (Types::INT8, Value::Int64(128)), + (Types::INT16, Value::Int64(-32769)), + (Types::INT32, Value::Int64(i64::from(i32::MAX) + 1)), + (Types::UINT8, Value::Int64(-1)), + (Types::UINT16, Value::Int64(65536)), + (Types::UINT32, Value::Int64(i64::from(u32::MAX) + 1)), + (Types::UINT64, Value::Int64(17)), + (Types::INT64, Value::Uint64(17)), + (Types::FLOAT32, Value::Float64(0.1)), + (Types::FLOAT32, Value::Float64(16777217.0)), + (Types::BOOL, Value::Int64(1)), + (Types::NOTHING, Value::Bool(false)), + ] { + assert_eq!( + companion_shape(&value, ty, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + assert_eq!( + companion_shape(&Value::Pending(Box::new(value)), ty, &runtime.checked), + Err(ResourceExecutionError::InvalidPayloadPath) + ); + } +} diff --git a/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs b/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs new file mode 100644 index 00000000..c12e728a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/runtime/return_prefix_tests.rs @@ -0,0 +1,371 @@ +use super::*; +use crate::resource_execution::{ProviderEvent, ScriptOperation, tests::program}; +use jett_types::ResourceHookKind; + +fn runtime(script: Vec) -> ResourceTransport { + let checked = program( + include_str!("../fixtures/04_bare_to_view_operation_cleanup.jett"), + false, + ); + let mut runtime = + ResourceTransport::checked_only(checked, ExecutionPurpose::ReferenceRuntime, 1).unwrap(); + runtime.checked_source_active = true; + runtime.install_script(script).unwrap(); + runtime +} + +fn construct(label: i64) -> ScriptOperation { + ScriptOperation::Construct { + label, + outcome: Ok(()), + } +} + +fn constructed(runtime: &mut ResourceTransport, label: i64) -> EvaluatedValue { + let definition = runtime + .checked + .program() + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == ResourceHookKind::Construct) + .unwrap() + .definition; + let span = *runtime + .checked + .program() + .checked() + .call_ownership + .iter() + .find(|(_, packet)| packet.target == CheckedInvocationTarget::Resolved(definition)) + .unwrap() + .0; + let invocation = runtime.checked.invocation(span).unwrap(); + let descriptor = runtime.checked.descriptor(definition).unwrap(); + let grant = match &runtime.provider { + InstalledResourceProvider::Scripted(provider) => provider.grant(), + _ => panic!("selected private provider"), + }; + let mut value = runtime + .invoke_hook( + &invocation, + &descriptor, + vec![ + EvaluatedValue::ordinary(Value::GrantedNetwork(grant)), + EvaluatedValue::ordinary(Value::Int64(label)), + ], + ) + .unwrap(); + let Value::ResultOk(payload) = value.value else { + panic!("occupied real construction"); + }; + value.value = *payload; + value.remove_prefix(PayloadStep::Ok).unwrap(); + value +} + +#[test] +fn return_prefix_retires_interleaved_operations_and_retains_scope_owner_and_loan() { + let mut runtime = runtime(vec![construct(903), construct(901), construct(902)]); + let retained = constructed(&mut runtime, 903); + let retained_borrow = runtime.borrow(&retained).unwrap(); + let scope = runtime.scope_frame().unwrap(); + runtime + .ledger + .hold_borrows(&retained_borrow.custody, scope) + .unwrap(); + let caller = runtime.begin_operation().unwrap(); + runtime.push_scope(); + runtime.enter_return(caller.frame); + let first = runtime.begin_operation().unwrap(); + let _first_owner = constructed(&mut runtime, 901); + runtime.push_scope(); + let second = runtime.begin_operation().unwrap(); + let _second_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.scopes.len(), 4); + assert!(runtime.ledger.frame_is_live(scope)); + assert!(runtime.ledger.has_live_borrows()); + assert!( + runtime + .ledger + .borrowed_carrier(&retained_borrow.custody) + .is_ok() + ); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (1, 1) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(903), + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901), + ] + ); + assert!(runtime.acknowledge_return_operation(&second).unwrap()); + assert!(runtime.acknowledge_return_operation(&first).unwrap()); + runtime.leave_return(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap().last(), + Some(&ProviderEvent::Finalized(903)) + ); +} + +#[test] +fn return_prefix_nested_callee_boundary_preserves_callers_pending_actuals() { + let mut runtime = runtime(vec![construct(901), construct(902)]); + let caller = runtime.begin_operation().unwrap(); + let _caller_owner = constructed(&mut runtime, 901); + runtime.enter_return(caller.frame); + let pending = runtime.begin_operation().unwrap(); + let _pending_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + runtime.enter_return(pending.frame); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame, pending.frame]); + assert_eq!(runtime.live_owners(), 2); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ] + ); + runtime.leave_return(); + runtime.pop_scope(); + runtime.end_operation(pending, None).unwrap(); + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.live_owners(), 1); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} + +#[test] +fn return_prefix_receipt_rejects_wrong_parent_output_adoption_and_reuse() { + let mut runtime = runtime(vec![construct(901)]); + let scope = runtime.scope_frame().unwrap(); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let pending = runtime.begin_operation().unwrap(); + let _owner = constructed(&mut runtime, 901); + runtime.retire_return_operations().unwrap(); + + let wrong = OperationFrame { + frame: pending.frame, + parent: scope, + }; + assert_eq!( + runtime.acknowledge_return_operation(&wrong), + Err(ResourceExecutionError::InvalidFrame) + ); + let copied = OperationFrame { + frame: pending.frame, + parent: pending.parent, + }; + let mut output = EvaluatedValue::ordinary(Value::Nothing); + assert_eq!( + runtime.end_operation(copied, Some(&mut output)), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.acknowledge_return_operation(&pending).unwrap()); + assert!(!runtime.acknowledge_return_operation(&pending).unwrap()); + assert_eq!( + runtime.end_operation(pending, None), + Err(ResourceExecutionError::InvalidFrame) + ); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); +} + +#[test] +fn return_prefix_cleanup_panic_continues_suffix_and_expires_receipts_after_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + let mut runtime = runtime(vec![ + construct(903), + construct(901), + ScriptOperation::ConstructFinalizerPanic { label: 902 }, + ]); + let _retained = constructed(&mut runtime, 903); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let _first = runtime.begin_operation().unwrap(); + let _first_owner = constructed(&mut runtime, 901); + runtime.push_scope(); + let _second = runtime.begin_operation().unwrap(); + let _second_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + + let panic = catch_unwind(AssertUnwindSafe(|| runtime.retire_return_operations())) + .expect_err("actual prefix cleanup panic"); + assert_eq!( + panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()), + Some("selected test resource cleanup panic") + ); + assert_eq!(runtime.operations, [caller.frame]); + assert_eq!(runtime.abandoned_operations.len(), 2); + assert_eq!(runtime.live_owners(), 1); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(903), + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901), + ] + ); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!(runtime.abandoned_operations.len(), 2); + runtime.restore_entry_scopes(1).unwrap(); + runtime.validate_entry_scopes(1).unwrap(); + assert!(runtime.abandoned_operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap().last(), + Some(&ProviderEvent::Finalized(903)) + ); +} + +#[test] +fn return_prefix_ordinary_callable_has_own_floor_and_no_resource_destination() { + let mut runtime = runtime(vec![construct(901), construct(902)]); + let mut retained = constructed(&mut runtime, 901); + let caller = runtime.begin_operation().unwrap(); + runtime.enter_return(caller.frame); + let invocation = runtime.begin_operation().unwrap(); + let _invocation_owner = constructed(&mut runtime, 902); + runtime.push_scope(); + assert!(runtime.enter_ordinary_return()); + + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame, invocation.frame]); + assert_eq!(runtime.live_owners(), 2); + assert_eq!(runtime.returns.last().unwrap().destination, None); + assert_eq!(runtime.returns.last().unwrap().operation_floor, 2); + assert_eq!( + runtime.preserve_return(&mut retained), + Err(ResourceExecutionError::InvalidFrame) + ); + assert!(runtime.borrow(&retained).is_ok()); + assert!(runtime.abandoned_operations.is_empty()); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902) + ] + ); + + runtime.leave_return(); + runtime.pop_scope(); + runtime.end_operation(invocation, None).unwrap(); + runtime.retire_return_operations().unwrap(); + assert_eq!(runtime.operations, [caller.frame]); + runtime.leave_return(); + runtime.end_operation(caller, None).unwrap(); + runtime.unwind_all().unwrap(); + assert_eq!( + (runtime.live_owners(), runtime.registry_live_count()), + (0, 0) + ); + assert_eq!( + runtime.provider_events().unwrap(), + [ + ProviderEvent::Constructed(901), + ProviderEvent::Constructed(902), + ProviderEvent::Finalized(902), + ProviderEvent::Finalized(901) + ] + ); +} + +#[test] +fn return_prefix_checked_source_named_ordinary_callable_retains_independent_owner() { + const SOURCE: &str = "namespace app\nfunction read_value() returns int64:\n return 7\nexport function scenario(view net: Network) returns int64:\n use resource_probe\n resource_probe.TestHandle token = resource_probe.create(view net, 1002) handle error:\n return -1\n function() returns int64 callback = read_value\n int64 observed = callback()\n resource_probe.close(token)\n return observed\n"; + for release in [false, true] { + let checked = program(SOURCE, release); + let declaration = checked + .module() + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Function(function) if function.name.name == "scenario" => { + Some(function.name.span) + } + _ => None, + }) + .unwrap(); + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == declaration + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [target] = definitions.as_slice() else { + panic!("unique checked scenario") + }; + let mut interpreter = crate::Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![construct(1002)]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(*target, vec![grant]) + .unwrap(), + Value::Int64(7) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1002), + ProviderEvent::Finalized(1002) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests.rs b/crates/jett_comptime/src/resource_execution/tests.rs new file mode 100644 index 00000000..8b0dbc2a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests.rs @@ -0,0 +1,1172 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_parser::{ast::Item, parse}; +use jett_resolve::{DefId, DefKind, ResourceKernelSpec}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; + +use super::{ExecutionPurpose, ProviderEvent, ScriptOperation}; +use crate::{Interpreter, Value}; + +const SUPPORT: &str = include_str!("fixtures/resource_probe.jett"); + +pub(crate) fn program(source: &str, release: bool) -> Arc { + program_support(source, SUPPORT, release) +} + +pub(crate) fn companion_program(source: &str, release: bool) -> Arc { + let support = format!( + "{SUPPORT}\n{}", + include_str!("fixtures/resource_companion_probe.jett") + ); + program_support(source, &support, release) +} + +fn program_support(source: &str, support: &str, release: bool) -> Arc { + let stdlib_file = FileId::new(10_000); + let project_file = FileId::new(0); + let mut parsed = parse(support, stdlib_file); + let project = parse(source, project_file); + assert!( + !parsed + .errors + .iter() + .chain(&project.errors) + .any(|diagnostic| diagnostic.severity == Severity::Error), + "Resource source parse prerequisite failed (release={release}): support diagnostics={:?}; project diagnostics={:?}", + parsed.errors, + project.errors, + ); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .expect("actual source Resource declaration"); + parsed.module.items.extend(project.module.items); + parsed.errors.extend(project.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.to_string(), + recipe, + }) + .collect::>(); + Arc::new(CheckedResourceProgram::prepare(parsed, HashMap::from([(stdlib_file, SourceOrigin::Stdlib), + (project_file, SourceOrigin::Project)]), &catalog, CheckOptions { release }).unwrap_or_else(|error| { + panic!("Resource source check prerequisite failed (release={release}): {error:?}; retained diagnostics={:?}", + error.diagnostics()); + })) +} + +fn entry(program: &CheckedResourceProgram, name: &str) -> DefId { + let functions = program + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.span.file == FileId::new(0) && function.name.name == name => + { + Some(function) + } + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + panic!( + "primary source entry `{name}` must be unique; found {}", + functions.len() + ); + }; + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!( + "primary source entry `{name}` has no unique exact ordinary declaration: {:?}", + definitions + ); + }; + *definition +} + +fn lifecycle(source: &str, script: Vec, expected: Vec) { + for release in [false, true] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(script.clone()) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing + ); + // Inspect before teardown. Registry::Drop is not the proof of source cleanup. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_constructs_borrows_moves_returns_and_closes_one_owner() { + lifecycle( + include_str!("fixtures/01_construct_borrow_move_close.jett"), + vec![ + ScriptOperation::Construct { + label: 101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 101, + outcome: Ok(7), + }, + ScriptOperation::Borrow { + label: 101, + outcome: Ok(8), + }, + ], + vec![ + ProviderEvent::Constructed(101), + ProviderEvent::Borrowed(101), + ProviderEvent::Borrowed(101), + ProviderEvent::Finalized(101), + ], + ); +} + +#[test] +fn checked_source_scope_cleanup_is_reverse_holder_acquisition_before_teardown() { + lifecycle( + include_str!("fixtures/02_reverse_scope_drop.jett"), + vec![ + ScriptOperation::Construct { + label: 201, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 202, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 201, + outcome: Ok(1), + }, + ScriptOperation::Borrow { + label: 202, + outcome: Ok(2), + }, + ], + vec![ + ProviderEvent::Constructed(201), + ProviderEvent::Constructed(202), + ProviderEvent::Borrowed(201), + ProviderEvent::Borrowed(202), + ProviderEvent::Finalized(202), + ProviderEvent::Finalized(201), + ], + ); +} + +#[test] +fn checked_source_domain_failures_publish_no_extra_owner() { + let source = include_str!("fixtures/03_domain_failures.jett"); + lifecycle( + source, + vec![ScriptOperation::Construct { + label: 301, + outcome: Err("denied".to_string()), + }], + vec![ProviderEvent::ConstructionFailed(301)], + ); + lifecycle( + source, + vec![ + ScriptOperation::Construct { + label: 301, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 301, + outcome: Err("unavailable".to_string()), + }, + ], + vec![ + ProviderEvent::Constructed(301), + ProviderEvent::BorrowFailed(301), + ProviderEvent::Finalized(301), + ], + ); +} + +#[test] +fn checked_source_bare_owned_actual_has_one_view_operation_backing() { + lifecycle( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + vec![ + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 401, + outcome: Ok(4), + }, + ], + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401), + ], + ); +} + +#[test] +fn checked_source_occupied_result_moves_across_owned_calls_and_returns() { + lifecycle( + include_str!("fixtures/06_return_occupied_result.jett"), + vec![ + ScriptOperation::Construct { + label: 601, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 601, + outcome: Ok(6), + }, + ], + vec![ + ProviderEvent::Constructed(601), + ProviderEvent::Borrowed(601), + ProviderEvent::Finalized(601), + ], + ); +} + +#[test] +fn checked_source_indirect_hook_descriptor_does_not_finalize_until_invoked() { + lifecycle( + include_str!("fixtures/07_indirect_close.jett"), + vec![ScriptOperation::Construct { + label: 701, + outcome: Ok(()), + }], + vec![ + ProviderEvent::Constructed(701), + ProviderEvent::Finalized(701), + ], + ); +} + +#[test] +fn checked_source_capability_free_close_wrapper_still_requires_runtime_purpose() { + lifecycle( + include_str!("fixtures/09_capability_free_close_target.jett"), + vec![ScriptOperation::Construct { + label: 901, + outcome: Ok(()), + }], + vec![ + ProviderEvent::Constructed(901), + ProviderEvent::Finalized(901), + ], + ); +} + +#[test] +fn checked_required_purposes_allow_closed_descriptors_and_absence_without_provider() { + let source = include_str!("fixtures/08_required_descriptor_and_absence.jett"); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for name in ["descriptor_storage", "absent_close_path"] { + let program = program(source, release); + let target = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program, purpose).unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Nothing + ); + } + } + } +} + +#[test] +fn checked_source_later_actual_failure_preserves_lexical_borrow_and_skips_callee() { + lifecycle( + include_str!("fixtures/05_later_named_argument_failure_v2.jett"), + vec![ + ScriptOperation::Construct { + label: 501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 501, + outcome: Ok(5), + }, + ], + vec![ + ProviderEvent::Constructed(501), + ProviderEvent::Borrowed(501), + ProviderEvent::Finalized(501), + ], + ); +} + +#[test] +fn checked_worker_apis_evaluate_closed_descriptors_and_property_trials_without_a_provider() { + let source = include_str!("fixtures/08_required_descriptor_and_absence.jett"); + for release in [false, true] { + let program = program(source, release); + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(program.clone()), + ..Default::default() + }); + let required = crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + types.clone(), + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let blocks = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + program.module(), + None, + Some(types.clone()), + None, + ); + assert_eq!(blocks.len(), 2); + assert!( + blocks + .iter() + .all(|block| block.passed && block.debug_events.is_empty()), + "{blocks:?}" + ); + assert_eq!(blocks.iter().filter(|block| block.is_property).count(), 1); + assert_eq!( + blocks + .iter() + .find(|block| block.is_property) + .unwrap() + .iterations, + Some(100) + ); + let foreign = program.module().clone(); + let rejected = crate::run_verify_blocks_detailed_with_metadata_and_expression_types( + &foreign, + None, + Some(types), + None, + ); + assert_eq!(rejected.len(), 1); + assert!(!rejected[0].passed); + assert!( + rejected[0] + .error + .as_deref() + .unwrap() + .contains("foreign checked source module") + ); + } +} + +#[test] +fn raw_runtime_names_and_debug_configuration_do_not_grant_resource_dispatch() { + let program = program( + include_str!("fixtures/09_capability_free_close_target.jett"), + false, + ); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(Vec::new()) + .unwrap(); + assert!( + interpreter + .call_function("app.scenario", vec![grant]) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (Vec::new(), 0, 0) + ); + let mut raw = Interpreter::new_runtime(); + assert!( + raw.call_function( + "resource_probe.kernel_create", + vec![Value::Capability("Network".to_string()), Value::Int64(1)] + ) + .is_err() + ); + assert!(raw.take_debug_events().is_empty()); +} + +#[test] +fn checked_source_bare_owned_view_provider_panic_unwinds_callee_before_parent_owner() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + }, + ScriptOperation::BorrowPanic { label: 401 }, + ]) + .unwrap(); + assert!( + catch_unwind(AssertUnwindSafe( + || interpreter.call_checked_program_entry(target, vec![grant]) + )) + .is_err() + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_ordinary_closure_ignores_unreferenced_live_owner_and_grant() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/10_ordinary_closure_unreferenced_resource.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ScriptOperation::Construct { + label: 1001, + outcome: Ok(()), + }]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(7) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1001), + ProviderEvent::Finalized(1001) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_terminal_runtime_error_cleans_owner_before_teardown() { + for release in [false, true] { + let checked = program( + include_str!("fixtures/11_terminal_assertion_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1101, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1101, + outcome: Ok(7), + }, + ]) + .unwrap(); + let error = interpreter + .call_checked_program_entry(target, vec![grant]) + .expect_err("the checked source kernel must return an ordinary terminal error"); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + // This same interpreter is still alive: teardown cannot satisfy these counts. + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1101), + ProviderEvent::Borrowed(1101), + ProviderEvent::Finalized(1101) + ], + 0, + 0, + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +fn mixed_failure_panic_message(payload: &(dyn std::any::Any + Send)) -> &str { + if let Some(message) = payload.downcast_ref::<&str>() { + return message; + } + payload + .downcast_ref::() + .map(String::as_str) + .expect("selected private fault has a textual panic payload") +} + +#[test] +fn checked_source_cleanup_panic_overrides_provider_panic_after_real_view_cleanup() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + for cleanup_panics in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let construction = if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 401 } + } else { + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + } + }; + let grant = interpreter + .install_resource_test_script(vec![ + construction, + ScriptOperation::BorrowPanic { label: 401 }, + ]) + .unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })) + .expect_err("private provider fault must unwind"); + let expected = if cleanup_panics { + "selected test resource cleanup panic" + } else { + "selected test borrow provider panic" + }; + assert_eq!(mixed_failure_panic_message(panic.as_ref()), expected); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Borrowed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_source_mixed_failure_continues_older_owner_cleanup_before_teardown() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + let checked = program( + include_str!("fixtures/12_mixed_failure_cleanup_continues.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1201, + outcome: Ok(()), + }, + ScriptOperation::ConstructFinalizerPanic { label: 1202 }, + ScriptOperation::Borrow { + label: 1201, + outcome: Ok(1), + }, + ScriptOperation::BorrowPanic { label: 1202 }, + ]) + .unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })) + .expect_err("both selected private faults unwind"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + "selected test resource cleanup panic" + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1201), + ProviderEvent::Constructed(1202), + ProviderEvent::Borrowed(1201), + ProviderEvent::Borrowed(1202), + ProviderEvent::Finalized(1202), + ProviderEvent::Finalized(1201) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } +} + +#[test] +fn checked_source_clean_cleanup_preserves_ordinary_protocol_error() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + for release in [false, true] { + for cleanup_panics in [false, true] { + let checked = program( + include_str!("fixtures/04_bare_to_view_operation_cleanup.jett"), + release, + ); + let target = entry(&checked, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + checked, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let construction = if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 401 } + } else { + ScriptOperation::Construct { + label: 401, + outcome: Ok(()), + } + }; + // Deliberately missing Borrow is a private harness protocol fault, + // not a source result.fail or a newly permitted provider behavior. + let grant = interpreter + .install_resource_test_script(vec![construction]) + .unwrap(); + let outcome = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + if cleanup_panics { + let panic = + outcome.expect_err("cleanup panic overrides an ordinary operation error"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + "selected test resource cleanup panic" + ); + } else { + assert_eq!( + outcome.expect("protocol error remains an ordinary Err"), + Err("resource execution has invalid checked call metadata".to_string()) + ); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(401), + ProviderEvent::Finalized(401) + ], + 0, + 0 + ) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[path = "prerequisite_tests.rs"] +mod frontend_prerequisites; + +#[test] +fn checked_direct_and_nested_type_scopes_execute_original_lifecycle_and_restore_caller() { + lifecycle( + include_str!("fixtures/13_direct_nested_scoped_lifecycle.jett"), + vec![ + ScriptOperation::Construct { + label: 1301, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1301, + outcome: Ok(1), + }, + ScriptOperation::Construct { + label: 1302, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1302, + outcome: Ok(2), + }, + ScriptOperation::Construct { + label: 1304, + outcome: Ok(()), + }, + ScriptOperation::Construct { + label: 1303, + outcome: Ok(()), + }, + ], + vec![ + ProviderEvent::Constructed(1301), + ProviderEvent::Borrowed(1301), + ProviderEvent::Finalized(1301), + ProviderEvent::Constructed(1302), + ProviderEvent::Borrowed(1302), + ProviderEvent::Finalized(1302), + ProviderEvent::Constructed(1304), + ProviderEvent::Finalized(1304), + ProviderEvent::Constructed(1303), + ProviderEvent::Finalized(1303), + ], + ); +} + +#[test] +fn checked_direct_scopes_keep_named_worker_descriptor_and_absence_entries_provider_free() { + let source = include_str!("fixtures/14_direct_scoped_descriptor_absence.jett"); + for release in [false, true] { + for purpose in [ + ExecutionPurpose::NamespaceConstant, + ExecutionPurpose::ExplicitComptime, + ExecutionPurpose::Verify, + ExecutionPurpose::Property, + ] { + for name in ["descriptor_storage", "absent_close_path"] { + let program = program(source, release); + let target = entry(&program, name); + let mut interpreter = + Interpreter::from_checked_resource_program(program.clone(), purpose).unwrap(); + interpreter + .authorize_checked_resource_worker(program.module(), purpose) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + assert!( + interpreter + .install_resource_test_script(Vec::new()) + .is_err() + ); + assert_eq!( + interpreter + .call_checked_program_entry(target, Vec::new()) + .unwrap(), + Value::Nothing + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } + // This is a checked named-entry test. Collected required expressions and + // callable/cache body references remain a separate mandatory later layer. +} + +#[test] +fn checked_direct_scope_error_restores_typed_and_legacy_context_before_reentry() { + let source = include_str!("fixtures/15_direct_scoped_ordinary_error.jett"); + for release in [false, true] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = + Interpreter::from_checked_resource_program(program, ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1501, + outcome: Ok(1), + }, + ScriptOperation::Construct { + label: 1501, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1501, + outcome: Ok(2), + }, + ]) + .unwrap(); + for attempt in 0..2 { + let error = interpreter + .call_checked_program_entry(target, vec![grant.clone()]) + .unwrap_err(); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + let mut expected = vec![ + ProviderEvent::Constructed(1501), + ProviderEvent::Borrowed(1501), + ProviderEvent::Finalized(1501), + ]; + if attempt == 1 { + expected.extend(expected.clone()); + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected, 0, 0) + ); + assert_eq!( + interpreter.resource_test_scoped_context().unwrap(), + (false, 0, 0, 0, None) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_direct_scopes_restore_after_return_and_default_with_live_owner() { + let source = include_str!("fixtures/16_direct_scoped_return_default.jett"); + for release in [false, true] { + for outcome in [Ok(19), Err("unavailable".to_string())] { + let program = program(source, release); + let target = entry(&program, "scenario"); + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: 1601, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1601, + outcome: outcome.clone(), + }, + ]) + .unwrap(); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(if outcome.is_ok() { 19 } else { 17 }) + ); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + ( + vec![ + ProviderEvent::Constructed(1601), + if outcome.is_ok() { + ProviderEvent::Borrowed(1601) + } else { + ProviderEvent::BorrowFailed(1601) + }, + ProviderEvent::Finalized(1601) + ], + 0, + 0 + ) + ); + assert_eq!( + interpreter.resource_test_scoped_context().unwrap(), + (false, 0, 0, 0, None) + ); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_entries_reuse_the_same_provider_after_clean_and_ordinary_error_completion() { + let source = include_str!("fixtures/25_repeated_scoped_entry.jett"); + for release in [false, true] { + for first_errors in [false, true] { + let program = program(source, release); + let clean = entry(&program, "clean"); + let first = if first_errors { + entry(&program, "terminal_error") + } else { + clean + }; + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let first_label = if first_errors { 1802 } else { 1801 }; + let grant = interpreter + .install_resource_test_script(vec![ + ScriptOperation::Construct { + label: first_label, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: first_label, + outcome: Ok(11), + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(12), + }, + ]) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let first_result = interpreter.call_checked_program_entry(first, vec![grant.clone()]); + if first_errors { + assert_eq!( + first_result, + Err("list.__remove_at: index -1 out of bounds".to_string()) + ); + } else { + assert_eq!(first_result.unwrap(), Value::Int64(11)); + } + let mut events = vec![ + ProviderEvent::Constructed(first_label), + ProviderEvent::Borrowed(first_label), + ProviderEvent::Finalized(first_label), + ]; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter + .call_checked_program_entry(clean, vec![grant]) + .unwrap(), + Value::Int64(12) + ); + events.extend([ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events, 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +#[test] +fn checked_entries_restore_after_provider_or_finalizer_panic_before_same_grant_reentry() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + let source = include_str!("fixtures/25_repeated_scoped_entry.jett"); + for release in [false, true] { + for cleanup_panics in [false, true] { + let program = program(source, release); + let target = entry(&program, "clean"); + let mut interpreter = Interpreter::from_checked_resource_program( + program, + ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + let grant = interpreter + .install_resource_test_script(vec![ + if cleanup_panics { + ScriptOperation::ConstructFinalizerPanic { label: 1801 } + } else { + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + } + }, + if cleanup_panics { + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(7), + } + } else { + ScriptOperation::BorrowPanic { label: 1801 } + }, + ScriptOperation::Construct { + label: 1801, + outcome: Ok(()), + }, + ScriptOperation::Borrow { + label: 1801, + outcome: Ok(19), + }, + ]) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })) + .expect_err("private scripted host panic remains observable"); + assert_eq!( + mixed_failure_panic_message(panic.as_ref()), + if cleanup_panics { + "selected test resource cleanup panic" + } else { + "selected test borrow provider panic" + } + ); + let mut events = vec![ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]; + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Int64(19) + ); + events.extend([ + ProviderEvent::Constructed(1801), + ProviderEvent::Borrowed(1801), + ProviderEvent::Finalized(1801), + ]); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (events, 0, 0) + ); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert!(interpreter.take_debug_events().is_empty()); + } + } +} + +pub(crate) fn same_resource_hook_descriptor_identity( + left: &super::ResourceHookDescriptor, + right: &super::ResourceHookDescriptor, +) -> bool { + Arc::ptr_eq(&left.program, &right.program) && left.definition == right.definition +} + +#[test] +fn closed_descriptor_mirror_identity_preserves_function_inequality_and_program_boundary() { + use super::CheckedExecution; + use jett_types::ResourceHookKind; + const SOURCE: &str = include_str!("fixtures/27_original_generic_scoped_required.jett"); + for release in [false, true] { + let checked_program = program(SOURCE, release); + let checked = + CheckedExecution::new(checked_program.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + let hook = |kind| { + checked_program + .checked() + .resource_hooks + .values() + .find(|hook| hook.kind == kind) + .unwrap() + .definition + }; + let closed = checked.descriptor(hook(ResourceHookKind::Close)).unwrap(); + assert!(same_resource_hook_descriptor_identity( + &closed, + &closed.clone() + )); + assert!(!same_resource_hook_descriptor_identity( + &closed, + &checked + .descriptor(hook(ResourceHookKind::BorrowOperation)) + .unwrap() + )); + let foreign = + CheckedExecution::new(program(SOURCE, release), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert!(!same_resource_hook_descriptor_identity( + &closed, + &foreign.descriptor(hook(ResourceHookKind::Close)).unwrap() + )); + assert_ne!( + Value::ResourceHook(closed.clone()), + Value::ResourceHook(closed) + ); + } +} + +#[path = "assignment_tests.rs"] +mod mutable_assignment; + +#[path = "tests/native_source_oracles.rs"] +mod native_source_oracles; diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs new file mode 100644 index 00000000..d88a6801 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles.rs @@ -0,0 +1,386 @@ +//! Real original checked-source counterpart to the linked private archive cases. +#[path = "../../../../jett_driver/tests/native_conformance/resource_cases.rs"] +mod cases; +use super::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +#[path = "../../../../jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs"] +mod occupied_struct_cases; +#[path = "native_source_oracles/occupied_struct_reentry.rs"] +mod occupied_struct_reentry; +#[path = "../../../../jett_driver/tests/native_conformance/resource_primitive_companion_cases.rs"] +mod primitive_companion_cases; + +fn companion_support() -> String { + format!( + "{}\n{}", + cases::SUPPORT, + include_str!("../fixtures/resource_companion_probe.jett") + ) +} + +#[test] +fn native_resource_primitive_companions_shared_scripts_match_real_reference() { + assert_eq!(primitive_companion_cases::CASES.len(), 23); + run_reference_cases_with_support(primitive_companion_cases::CASES, &companion_support()); +} + +#[test] +fn native_resource_primitive_companions_shared_reentry_inputs_match_real_reference() { + for release in [false, true] { + for first in primitive_companion_cases::CASES { + occupied_struct_reentry::original_then_ready_with_support( + first, + primitive_companion_cases::ready_for(first), + release, + &companion_support(), + ); + } + } +} + +#[test] +fn native_resource_occupied_struct_shared_scripts_match_real_reference() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_reference_cases(occupied_struct_cases::CASES); +} +#[test] +fn native_resource_occupied_struct_shared_reentry_inputs_match_real_reference() { + for release in [false, true] { + for first in occupied_struct_cases::CASES { + occupied_struct_reentry::original_then_ready( + first, + occupied_struct_cases::ready_for(first), + release, + ); + } + } +} + +fn checked_case(case: &cases::Case, release: bool) -> Arc { + checked_case_with_support(case, release, cases::SUPPORT) +} + +fn checked_case_with_support( + case: &cases::Case, + release: bool, + support: &str, +) -> Arc { + let stdlib = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = parse(support, stdlib); + let project = parse(case.source, primary); + assert!(parsed.errors.is_empty(), "support: {:?}", parsed.errors); + assert!( + project.errors.is_empty(), + "{}: {:?}", + case.name, + project.errors + ); + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(item) => Some(item.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = resources.as_slice() else { + panic!("exact synthetic Resource declaration is not unique"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "{} reference prerequisite: {error:?}; {:?}", + case.name, + error.diagnostics() + ) + }), + ) +} +fn script(input: cases::Script) -> ScriptOperation { + match input { + cases::Script::Construct(label) => ScriptOperation::Construct { + label, + outcome: Ok(()), + }, + cases::Script::ConstructFail(label, error) => ScriptOperation::Construct { + label, + outcome: Err(error.into()), + }, + cases::Script::FinalizerPanic(label) => ScriptOperation::ConstructFinalizerPanic { label }, + cases::Script::BorrowPanic(label) => ScriptOperation::BorrowPanic { label }, + cases::Script::Borrow(label, value) => ScriptOperation::Borrow { + label, + outcome: Ok(value), + }, + cases::Script::BorrowFail(label, error) => ScriptOperation::Borrow { + label, + outcome: Err(error.into()), + }, + } +} +fn event(input: &cases::Event) -> ProviderEvent { + match *input { + cases::Event::Constructed(label) => ProviderEvent::Constructed(label), + cases::Event::ConstructionFailed(label) => ProviderEvent::ConstructionFailed(label), + cases::Event::Borrowed(label) => ProviderEvent::Borrowed(label), + cases::Event::BorrowFailed(label) => ProviderEvent::BorrowFailed(label), + cases::Event::Finalized(label) => ProviderEvent::Finalized(label), + } +} + +fn reference_with_required_values( + checked: &Arc, + case: &cases::Case, + release: bool, +) -> Interpreter { + // Check the worker's existing purpose boundary without executing Source or + // installing a provider. The real required worker below receives no grant. + let mut worker_guard = Interpreter::new(); + worker_guard + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + worker_guard + .authorize_checked_resource_worker(checked.module(), ExecutionPurpose::ExplicitComptime) + .unwrap(); + assert!( + worker_guard + .install_resource_test_script(Vec::new()) + .is_err(), + "{} release={release}: required purpose must refuse a runtime provider", + case.name + ); + assert_eq!(worker_guard.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(worker_guard.resource_test_observations().is_err()); + + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + assert!( + required.diagnostics.is_empty(), + "{} release={release}: required evaluation failed: {:?}", + case.name, + required.diagnostics + ); + assert!( + required.debug_events.is_empty(), + "{} release={release}: unexpected required-phase debug observations: {:?}", + case.name, + required.debug_events + ); + assert!(required.values.checked_values_are_mirrored()); + assert_eq!(required.values.len(), required.values.values().count()); + assert!( + required + .values + .values() + .all(|value| !value.contains_live_resource_or_grant()), + "{} release={release}: required values cannot import runtime authority", + case.name + ); + + // Install even an empty checked cache. Expr::Comptime then refuses a missing + // exact checked key instead of falling back to evaluating its runtime body. + // Match the driver's order so namespace constants cannot replay at register. + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .unwrap(); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.resource_test_observations().is_err()); + interpreter +} + +fn run_reference_cases(selected: &[cases::Case]) { + run_reference_cases_with_support(selected, cases::SUPPORT); +} + +fn run_reference_cases_with_support(selected: &[cases::Case], support: &str) { + for release in [false, true] { + for case in selected { + let checked = checked_case_with_support(case, release, support); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, case, release); + let grant = interpreter + .install_resource_test_script(case.script.iter().copied().map(script).collect()) + .unwrap(); + let outcome = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant]) + })); + match case.reference { + cases::ReferenceOutcome::Clean => assert_eq!( + outcome + .unwrap() + .unwrap_or_else(|error| panic!("{} release={release}: {error}", case.name)), + Value::Nothing, + "{} release={release}", + case.name + ), + cases::ReferenceOutcome::Error(expected) => assert_eq!( + outcome.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ), + cases::ReferenceOutcome::ProviderPanic(expected) => { + let panic = outcome.expect_err("actual Source borrow provider panic"); + let text = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap(); + assert_eq!(text, expected, "{} release={release}", case.name); + } + cases::ReferenceOutcome::CleanupPanic(expected) => { + let panic = outcome.expect_err("actual Source finalizer panic"); + let text = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap(); + assert_eq!(text, expected, "{} release={release}", case.name); + } + } + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (case.events.iter().map(event).collect(), 0, 0), + "{} release={release}", + case.name + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{} release={release}", + case.name + ); + } + } +} + +#[test] +fn native_resource_source_cases_match_real_reference_events_and_cleanup_before_teardown() { + run_reference_cases(cases::CASES); +} + +#[test] +fn native_resource_pipeline_source_cases_match_real_reference_events_and_cleanup_before_teardown() { + run_reference_cases(cases::pipeline_cases()); +} + +#[test] +fn native_resource_control_flow_source_cases_match_real_reference_events_and_cleanup_before_teardown() + { + run_reference_cases(cases::control_flow_cases()); +} + +#[path = "native_source_oracles/source06_exceptional.rs"] +mod source06_exceptional; + +#[path = "native_source_oracles/reflected_field_positive.rs"] +mod reflected_field_positive; + +#[path = "native_source_oracles/absent_aggregate_baseline.rs"] +mod absent_aggregate_baseline; + +#[path = "../../../../jett_driver/tests/native_conformance/resource_sum_replacement_cases.rs"] +mod sum_replacement_cases; + +#[test] +fn native_resource_direct_sum_replacement_shared_scripts_match_real_reference() { + assert_eq!(sum_replacement_cases::CASES.len(), 19); + run_reference_cases(sum_replacement_cases::CASES); +} + +#[test] +fn native_resource_direct_sum_replacement_shared_reentry_inputs_match_real_reference() { + for release in [false, true] { + for first in sum_replacement_cases::CASES { + occupied_struct_reentry::original_then_ready( + first, + sum_replacement_cases::ready_for(first), + release, + ); + } + } +} + +// Append inside resource_execution/tests/native_source_oracles.rs. +#[test] +fn native_resource_direct_sum_self_reseat_sources_match_real_reference() { + const CASES: &[cases::Case] = &[ + cases::Case { + name: "direct_optional_self_reseat_absent", + source: include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett" + ), + script: &[], + events: &[], + reference: cases::ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + cases::Case { + name: "direct_result_self_reseat_failure", + source: include_str!( + "../../../../jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett" + ), + script: &[], + events: &[], + reference: cases::ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + ]; + run_reference_cases(CASES); +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs new file mode 100644 index 00000000..7ad54ea6 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/absent_aggregate_baseline.rs @@ -0,0 +1,235 @@ +//! Strict new-wrapper reference baseline; existing twelve typed returns are separate. +use super::*; +#[path = "../../../../../jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn runtime( + checked: &Arc, + entered: &mut Vec, +) -> Result { + use inputs::{Phase, enter, failure}; + let types = Arc::new(crate::checked_types::CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let metadata = Arc::new(jett_types::ReflectionMetadata::new()); + let exclusions = Arc::new(HashMap::new()); + enter(entered, Phase::RequiredCapture); + let required = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata.clone(), + types.clone(), + exclusions.clone(), + ); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + if !required.values.checked_values_are_mirrored() + || required + .values + .values() + .any(Value::contains_live_resource_or_grant) + { + return Err(failure( + Phase::RequiredCache, + "unmirrored or runtime-authority required value", + )); + } + let constants = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [constant] = constants.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "original namespace constant is not unique", + )); + }; + if required.values.constant(*constant) != Some(&Value::Int64(17)) { + return Err(failure( + Phase::RequiredCache, + required.values.constant(*constant), + )); + } + enter(entered, Phase::ReferenceSetup); + let mut interpreter = Interpreter::new(); + interpreter.set_reflection_metadata(metadata); + interpreter.set_checked_expression_types(types); + interpreter.set_breakpoint_exclusions(exclusions); + interpreter.set_explicit_comptime_values(Arc::new(required.values)); + // Baked values precede registration; no namespace initializer fallback. + interpreter.register_module(checked.module()); + interpreter + .install_checked_resource_program(checked.clone(), ExecutionPurpose::ReferenceRuntime) + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + Ok(interpreter) +} + +fn execute( + input: &inputs::Input, + release: bool, + entered: &mut Vec, +) -> Result<(), inputs::Failure> { + use inputs::{Phase, enter, failure}; + let (checked, span) = inputs::prepare(input, release, entered)?; + let mut interpreter = runtime(&checked, entered)?; + enter(entered, Phase::ReferenceEntryIdentity); + let functions = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) if function.span == span => Some(function), + _ => None, + }) + .collect::>(); + let [function] = functions.as_slice() else { + return Err(failure( + Phase::ReferenceEntryIdentity, + "original primary main body missing", + )); + }; + let definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Function + && definition.span == function.name.span + && definition.namespace.as_deref() == Some("app") + }) + .map(|definition| definition.id) + .collect::>(); + let [definition] = definitions.as_slice() else { + return Err(failure( + Phase::ReferenceEntryIdentity, + "original main has no unique checked definition", + )); + }; + let grant = interpreter + .install_resource_test_script(Vec::new()) + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + let baseline = interpreter + .resource_test_entry_context() + .map_err(|error| failure(Phase::ReferenceSetup, error))?; + enter(entered, Phase::ReferenceCall); + // Retain the interpreter for observations even if the Source panics. + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(*definition, vec![grant]) + })); + let actual_text = match &actual { + Ok(result) => format!("{result:?}"), + Err(payload) => format!( + "UnexpectedPanic({:?})", + inputs::panic_message(payload.as_ref()) + ), + }; + let call_outcome = match actual { + Ok(Ok(Value::Nothing)) + if matches!(input.reference, inputs::ReferenceExpectation::Nothing) => + { + Ok(()) + } + Ok(Err(actual)) => match input.reference { + inputs::ReferenceExpectation::Error(expected) if actual == expected => Ok(()), + _ => Err(failure(Phase::ReferenceCall, actual)), + }, + Ok(other) => Err(failure(Phase::ReferenceCall, other)), + Err(payload) => Err(inputs::Failure { + phase: Phase::ReferenceCall, + kind: inputs::FailureKind::UnexpectedPanic, + detail: inputs::panic_message(payload.as_ref()), + }), + }; + enter(entered, Phase::ReferenceObservations); + let observations = interpreter.resource_test_observations(); + let custody = interpreter.resource_test_custody_counts(); + let context = interpreter.resource_test_entry_context(); + let debug = interpreter.take_debug_events(); + eprintln!( + "ABSENT_REFERENCE name={:?} release={release} observed={actual_text:?} observations={observations:?} custody={custody:?} context={context:?} debug={debug:?}", + input.name + ); + if observations != Ok((Vec::new(), 0, 0)) + || custody != Ok((0, 0)) + || context.as_ref() != Ok(&baseline) + || !debug.is_empty() + { + if let Err(mut first) = call_outcome { + first.detail.push_str( + "; exact empty effects/custody/context/debug invariant also failed; see actual log", + ); + return Err(first); + } + return Err(failure( + Phase::ReferenceObservations, + "exact empty effects/custody/context/debug invariant failed; see actual log", + )); + } + call_outcome +} + +#[test] +fn native_resource_absent_aggregate_new_wrappers_record_all_reference_outcomes() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut attempts = 0; + let mut matched = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for input in inputs::INPUTS { + attempts += 1; + let attempt = inputs::attempt(|entered| execute(input, release, entered)); + match attempt.outcome { + Ok(()) => { + matched += 1; + eprintln!( + "ABSENT_REFERENCE_MATCH name={:?} release={release} entered={:?}", + input.name, attempt.entered + ); + } + Err(failure) => { + eprintln!( + "ABSENT_REFERENCE_FAILURE name={:?} release={release} entered={:?} failure={failure:?}", + input.name, attempt.entered + ); + failures.push((input.name, release, failure)); + } + } + } + } + eprintln!( + "ABSENT_REFERENCE_TOTAL attempts={attempts} matched={matched} failed={}", + failures.len() + ); + assert_eq!(attempts, 28); + assert!( + failures.is_empty(), + "strict new-wrapper reference failures: {failures:#?}" + ); + assert_eq!(matched, 28); +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs new file mode 100644 index 00000000..51994e1f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/occupied_struct_reentry.rs @@ -0,0 +1,89 @@ +//! Genuine reference counterpart to the shared native original-then-READY matrix. +use super::*; + +fn first_outcome( + actual: std::thread::Result>, + case: &cases::Case, + release: bool, +) { + match case.reference { + cases::ReferenceOutcome::Clean => { + assert_eq!( + actual.unwrap().unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::Error(expected) => { + assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the selected Source panic stays observable"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("the scripted provider/finalizer panic has a string payload"); + assert_eq!(message, expected, "{} release={release}", case.name); + } + } +} + +fn empty_before_teardown(interpreter: &mut Interpreter, expected: &[ProviderEvent]) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +pub(super) fn original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + original_then_ready_with_support(first, ready, release, cases::SUPPORT); +} + +pub(super) fn original_then_ready_with_support( + first: &cases::Case, + ready: &cases::Case, + release: bool, + support: &str, +) { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + let checked = checked_case_with_support(first, release, support); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, first, release); + let combined = first + .script + .iter() + .chain(ready.script.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant.clone()]) + })); + first_outcome(actual, first, release); + let mut expected = first.events.iter().map(event).collect::>(); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant READY", + first.name + ); + expected.extend(ready.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs new file mode 100644 index 00000000..229727dd --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive.rs @@ -0,0 +1,73 @@ +//! Reference counterpart of the standalone four-Source clean native rows. +use super::*; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_field_cases.rs"] +mod reflected_field_cases; + +#[test] +fn reflected_field_four_positive_sources_match_real_reference() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_reference_cases(reflected_field_cases::CASES); +} + +#[test] +fn reflected_field_four_positive_sources_clean_same_grant_reentry() { + for release in [false, true] { + for case in reflected_field_cases::CASES { + let checked = checked_case(case, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, case, release); + let grant = interpreter + .install_resource_test_script( + case.script + .iter() + .chain(case.script.iter()) + .copied() + .map(script) + .collect(), + ) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let mut expected = Vec::new(); + for _ in 0..2 { + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant.clone()]) + .unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + expected.extend(case.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.clone(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); + } + } + } +} + +#[test] +fn reflected_field_source06_exceptional_native_inputs_match_real_reference() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_reference_cases(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES); +} + +#[path = "reflected_field_positive/reentry.rs"] +mod source06_native_reentry; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs"] +mod reflected_derived_cases; + +#[path = "reflected_field_positive/derived_controls.rs"] +mod derived_controls; + +#[path = "../../../../../jett_driver/tests/native_conformance/resource_reflected_return_cases.rs"] +mod reflected_return_cases; + +#[path = "reflected_field_positive/reflected_returns.rs"] +mod reflected_returns; diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs new file mode 100644 index 00000000..ecddfe61 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/derived_controls.rs @@ -0,0 +1,20 @@ +//! Genuine checked reference counterpart to every shared derived native input. +use super::*; + +#[test] +fn reflected_derived_native_single_inputs_match_real_reference() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + run_reference_cases(reflected_derived_cases::CASES); +} + +#[test] +fn reflected_derived_native_reentry_inputs_match_real_reference() { + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + for release in [false, true] { + for first in reflected_derived_cases::original_cases() { + let ready = reflected_derived_cases::ready_for(first); + source06_native_reentry::original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs new file mode 100644 index 00000000..0e0ae34b --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reentry.rs @@ -0,0 +1,96 @@ +//! Genuine reference counterpart to the shared native original-then-READY matrix. +use super::*; + +fn first_outcome( + actual: std::thread::Result>, + case: &cases::Case, + release: bool, +) { + match case.reference { + cases::ReferenceOutcome::Clean => { + assert_eq!( + actual.unwrap().unwrap(), + Value::Nothing, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::Error(expected) => { + assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{} release={release}", + case.name + ); + } + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the selected Source panic stays observable"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("the scripted provider/finalizer panic has a string payload"); + assert_eq!(message, expected, "{} release={release}", case.name); + } + } +} + +fn empty_before_teardown(interpreter: &mut Interpreter, expected: &[ProviderEvent]) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0) + ); + assert_eq!(interpreter.resource_test_custody_counts().unwrap(), (0, 0)); + assert!(interpreter.take_debug_events().is_empty()); +} + +pub(super) fn original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + let checked = checked_case(first, release); + let definition = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, first, release); + let combined = first + .script + .iter() + .chain(ready.script.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(definition, vec![grant.clone()]) + })); + first_outcome(actual, first, release); + let mut expected = first.events.iter().map(event).collect::>(); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); + assert_eq!( + interpreter + .call_checked_program_entry(definition, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant READY", + first.name + ); + expected.extend(ready.events.iter().map(event)); + assert_eq!(interpreter.resource_test_entry_context().unwrap(), context); + empty_before_teardown(&mut interpreter, &expected); +} + +#[test] +fn reflected_field_source06_native_reentry_inputs_match_real_reference() { + let ready = &reflected_field_cases::CASES[0]; + assert_eq!(ready.name, "reflected_field_original"); + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + for release in [false, true] { + let first_scripts = std::iter::once(ready) + .chain(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.iter()) + .collect::>(); + assert_eq!(first_scripts.len(), 9); + for first in first_scripts { + original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs new file mode 100644 index 00000000..573fce3a --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/reflected_field_positive/reflected_returns.rs @@ -0,0 +1,19 @@ +//! Genuine checked reference counterpart of all shared dedicated Return inputs. +use super::*; + +#[test] +fn reflected_body_and_owned_return_single_inputs_match_real_reference() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_reference_cases(reflected_return_cases::CASES); +} + +#[test] +fn reflected_body_and_owned_return_reentry_inputs_match_real_reference() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + for release in [false, true] { + for first in reflected_return_cases::CASES { + let ready = reflected_return_cases::ready_for(first); + source06_native_reentry::original_then_ready(first, ready, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs new file mode 100644 index 00000000..7914214f --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional.rs @@ -0,0 +1,489 @@ +//! Reflected field Resource execution, failure cleanup and same-grant reentry. +//! These assertions describe reference execution; native admission is separate. +use super::*; + +const SOURCE: &str = + include_str!("source06_exceptional/06_reflected_field_owned_optional_reuse.jett"); +const BREAK_SOURCE: &str = + include_str!("source06_exceptional/07_reflected_field_optional_break.jett"); +const CONTINUE_SOURCE: &str = + include_str!("source06_exceptional/08_reflected_field_optional_continue.jett"); +const DEFAULT_SOURCE: &str = + include_str!("source06_exceptional/09_reflected_field_optional_scalar_default.jett"); + +struct Input { + name: &'static str, + script: &'static [cases::Script], + events: &'static [cases::Event], + outcome: cases::ReferenceOutcome, +} + +const READY_SCRIPT: &[cases::Script] = &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), +]; +const READY_EVENTS: &[cases::Event] = &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), +]; + +const INPUTS: &[Input] = &[ + Input { + name: "reflected_field_ready", + script: READY_SCRIPT, + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_construct_fail", + script: &[cases::Script::ConstructFail(871, "nested factory sentinel")], + events: &[cases::Event::ConstructionFailed(871)], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + Input { + name: "reflected_field_ready_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + Input { + name: "reflected_field_first_borrow_provider_panic", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowPanic(871), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + }, + Input { + name: "reflected_field_later_borrow_provider_panic", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowPanic(871), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + }, + Input { + name: "reflected_field_cleanup_wins_provider_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowPanic(871), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + Input { + name: "reflected_field_cleanup_wins_domain_failure_return", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, +]; + +struct DerivedInput { + source: &'static str, + input: Input, +} + +// These are explicit adaptations of Source06, never claims of unchanged Source. +const DERIVED_INPUTS: &[DerivedInput] = &[ + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_ready", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: BREAK_SOURCE, + input: Input { + name: "reflected_resource_break_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_ready", + script: READY_SCRIPT, + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: CONTINUE_SOURCE, + input: Input { + name: "reflected_resource_continue_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::Borrow(871, 17), + cases::Script::Borrow(871, 17), + ], + events: READY_EVENTS, + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_first_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_later_borrow_fail", + script: &[ + cases::Script::Construct(871), + cases::Script::Borrow(871, 17), + cases::Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::Borrowed(871), + cases::Event::BorrowFailed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::Clean, + }, + }, + DerivedInput { + source: DEFAULT_SOURCE, + input: Input { + name: "reflected_resource_scalar_default_cleanup_panic", + script: &[ + cases::Script::FinalizerPanic(871), + cases::Script::BorrowFail(871, "borrow sentinel"), + cases::Script::Borrow(871, 17), + ], + events: &[ + cases::Event::Constructed(871), + cases::Event::BorrowFailed(871), + cases::Event::Borrowed(871), + cases::Event::Finalized(871), + ], + outcome: cases::ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + }, + }, +]; + +fn reference_case(source: &'static str, input: &Input) -> cases::Case { + // checked_case/reference_with_required_values read only Source/script/name. + // This reference-only module never supplies these placeholders to a native + // Case gate. Native assertions must come from an independent linked run. + cases::Case { + name: input.name, + source, + script: input.script, + events: input.events, + reference: input.outcome, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} + +fn panic_text(payload: &(dyn std::any::Any + Send)) -> &str { + payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .expect("selected provider/finalizer panic has a string payload") +} + +fn assert_outcome( + actual: std::thread::Result>, + expected: cases::ReferenceOutcome, + name: &str, + release: bool, +) { + match expected { + cases::ReferenceOutcome::Clean => assert_eq!( + actual + .unwrap() + .unwrap_or_else(|error| panic!("{name} release={release}: {error}")), + Value::Nothing, + "{name} release={release}", + ), + cases::ReferenceOutcome::Error(expected) => assert_eq!( + actual.unwrap().unwrap_err(), + expected, + "{name} release={release}", + ), + cases::ReferenceOutcome::ProviderPanic(expected) + | cases::ReferenceOutcome::CleanupPanic(expected) => { + let payload = actual.expect_err("the actual selected Source panic remains observable"); + assert_eq!( + panic_text(payload.as_ref()), + expected, + "{name} release={release}" + ); + } + } +} + +fn assert_empty_before_teardown( + interpreter: &mut Interpreter, + expected: &[ProviderEvent], + name: &str, + release: bool, +) { + assert_eq!( + interpreter.resource_test_observations().unwrap(), + (expected.to_vec(), 0, 0), + "{name} release={release}" + ); + assert_eq!( + interpreter.resource_test_custody_counts().unwrap(), + (0, 0), + "{name} release={release}" + ); + assert!( + interpreter.take_debug_events().is_empty(), + "{name} release={release}" + ); +} + +#[test] +fn reflected_field_source06_exceptional_paths_restore_before_teardown() { + assert_eq!(INPUTS.len(), 9); + for release in [false, true] { + for input in INPUTS { + let case = reference_case(SOURCE, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let grant = interpreter + .install_resource_test_script(input.script.iter().copied().map(script).collect()) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + assert_outcome(actual, input.outcome, input.name, release); + let expected = input.events.iter().map(event).collect::>(); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: exact entry metadata/cursor restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} + +#[test] +fn reflected_field_source06_reenters_after_every_exception_with_the_same_provider_grant() { + for release in [false, true] { + for input in INPUTS { + let case = reference_case(SOURCE, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let combined = input + .script + .iter() + .chain(READY_SCRIPT.iter()) + .copied() + .map(script) + .collect(); + let grant = interpreter.install_resource_test_script(combined).unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let first = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant.clone()]) + })); + assert_outcome(first, input.outcome, input.name, release); + let mut expected = input.events.iter().map(event).collect::>(); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: first exceptional entry restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + + assert_eq!( + interpreter + .call_checked_program_entry(target, vec![grant]) + .unwrap(), + Value::Nothing, + "{} release={release}: same-grant ready reentry", + input.name + ); + expected.extend(READY_EVENTS.iter().map(event)); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: same-grant ready entry restored", + input.name + ); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} + +#[test] +fn reflected_field_resource_aliases_retire_at_break_continue_and_scalar_default() { + assert_eq!(DERIVED_INPUTS.len(), 10); + for release in [false, true] { + for control in DERIVED_INPUTS { + let input = &control.input; + let case = reference_case(control.source, input); + let checked = checked_case(&case, release); + let target = entry(&checked, "main"); + let mut interpreter = reference_with_required_values(&checked, &case, release); + let grant = interpreter + .install_resource_test_script(input.script.iter().copied().map(script).collect()) + .unwrap(); + let context = interpreter.resource_test_entry_context().unwrap(); + let actual = catch_unwind(AssertUnwindSafe(|| { + interpreter.call_checked_program_entry(target, vec![grant]) + })); + assert_outcome(actual, input.outcome, input.name, release); + assert_eq!( + interpreter.resource_test_entry_context().unwrap(), + context, + "{} release={release}: selected field control restored", + input.name + ); + let expected = input.events.iter().map(event).collect::>(); + assert_empty_before_teardown(&mut interpreter, &expected, input.name, release); + } + } +} diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/06_reflected_field_owned_optional_reuse.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett new file mode 100644 index 00000000..b772f6f3 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett @@ -0,0 +1,25 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + break + if total != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett new file mode 100644 index 00000000..c747f992 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett @@ -0,0 +1,26 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + continue + total = total + 1000 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett new file mode 100644 index 00000000..f4880684 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + default 17 + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett new file mode 100644 index 00000000..ff7db23c --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett @@ -0,0 +1,25 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 + third: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 881) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed == 17: + return nothing + if observed == 17: + int64 skipped_after_return = resource_probe.terminal_label() + if field.index != 0: + int64 skipped_after_body = resource_probe.terminal_label() + int64 skipped_after_loop = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett new file mode 100644 index 00000000..1a6a5662 --- /dev/null +++ b/crates/jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett @@ -0,0 +1,36 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 + third: int64 +function return_selected(view net: Network, survivor: resource_probe.TestHandle, outer: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + optional[resource_probe.TestHandle] held = some(outer) + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return survivor + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return survivor + if observed == 17: + return survivor + if observed == 17: + int64 skipped_after_return = resource_probe.terminal_label() + if field.index != 0: + int64 skipped_after_body = resource_probe.terminal_label() + int64 skipped_after_loop = resource_probe.terminal_label() + return survivor +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle outer = resource_probe.create(view net, 891) handle error: + return nothing + resource_probe.TestHandle survivor = resource_probe.create(view net, 892) handle error: + return nothing + resource_probe.TestHandle returned = return_selected(view net, survivor, outer) + int64 observed = resource_probe.borrow(view net, view returned) handle error: + return nothing + if observed != 29: + int64 wrong_return = resource_probe.terminal_label() + resource_probe.close(returned) + return nothing diff --git a/crates/jett_comptime/src/value.rs b/crates/jett_comptime/src/value.rs index bd819559..2c97a7c5 100644 --- a/crates/jett_comptime/src/value.rs +++ b/crates/jett_comptime/src/value.rs @@ -1,11 +1,56 @@ +use crate::checked_types::{CheckedFunctionTypes, CheckedScopedTypes}; use std::collections::HashMap; use std::fmt; +use std::sync::Arc; -use jett_parser::ast::{Block, Param}; +use jett_parser::ast::{Block, Param, TypeExpr}; +use jett_types::ReflectionTypeInfo; + +/// Lexical generic context retained by an evaluated closure. +#[derive(Debug, Clone, Default)] +pub struct ClosureTypeContext { + /// The source return contract is resolved in the captured lexical context. + pub return_type: Option, + pub checked_scope: Option>, + pub scoped_type_bindings: Vec, + pub checked_function: Option>, + pub bindings: HashMap, + pub arguments: Vec, + /// Constructor-owned source witness; ordinary closure values need none. + pub(crate) reflected_capture: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ClosureScopedTypeBinding { + pub name: String, + pub canonical_name: String, + pub reflection: Option, +} + +#[derive(Debug, Clone)] +pub struct ClosureTypeArgument { + pub ty: TypeExpr, + pub canonical_name: String, + pub reflection: Option, +} + +#[path = "value/reflected_callable.rs"] +mod reflected_callable; +pub use reflected_callable::ReflectedCallableValue; +pub(crate) use reflected_callable::original_source::body_may_mint_inline_function; + +#[path = "value/closure_capture.rs"] +pub(crate) mod closure_capture; /// Runtime value for the compile-time interpreter. #[derive(Debug, Clone)] pub enum Value { + /// Checked concrete identity for a payload whose storage carrier erases it. + /// Arithmetic and formatting use the payload; interface dispatch uses the name. + Typed { + type_name: String, + value: Box, + }, /// 64-bit signed integer. Int64(i64), /// 64-bit unsigned integer. @@ -32,6 +77,12 @@ pub enum Value { Nothing, /// An opaque runtime-provided capability handle. Capability(String), + /// Physical carrier only; cleanup custody is never part of Value::Clone. + Resource(crate::resource_execution::ResourceCarrier), + /// Program-bound closed hook descriptor; storing it does not invoke a hook. + ResourceHook(crate::resource_execution::ResourceHookDescriptor), + /// Compiler-installed provenance-bearing authority, never a capability string. + GrantedNetwork(crate::resource_execution::GrantedNetwork), /// An opaque reflected construction builder. TypeConstruction { type_name: String, @@ -42,6 +93,9 @@ pub enum Value { /// A user-defined struct instance: `Point(x: 1, y: 2)`. Struct { type_name: String, + /// Concrete generic identity retained across interface erasure. This + /// metadata does not change source display or value equality. + concrete_type: Option, fields: Vec<(String, Value)>, }, /// An enum instance: `Color.red` or `Shape.circle(5.0)`. @@ -68,18 +122,113 @@ pub enum Value { Set(Vec), /// A named function reference retaining its canonical declaration identity. NamedFunction(String), + /// Checked lazy adapter retaining the original evaluated callback. + ReflectedCallable(Box), /// A captured inline function expression (closure). Function { + /// Keep generic metadata off the stack: inline storage enlarges every + /// Value variant and recursive interpreter frame, even without closures. + type_context: Box, params: Vec, body: Block, captures: HashMap, + capture_types: HashMap, + namespace_aliases: HashMap, namespace: Option, }, } +impl Value { + /// Exact readonly observation; ordinary Value equality intentionally erases tags. + pub fn same_checked_snapshot(&self, other: &Self) -> bool { + closure_capture::same_snapshot(self, other) + } + + /// Authenticate a callable descriptor without executing or joining it. + pub fn validate_checked_callable_source( + &self, + program: &Arc, + actual: jett_types::TypeId, + ) -> Result<(), String> { + ReflectedCallableValue::authenticate_source(self, program, actual) + } + + pub(crate) fn contains_live_resource_or_grant(&self) -> bool { + match self { + Self::Resource(_) | Self::GrantedNetwork(_) => true, + Self::Typed { value, .. } + | Self::ResultOk(value) + | Self::ResultFail(value) + | Self::OptionalSome(value) + | Self::Pending(value) => value.contains_live_resource_or_grant(), + Self::List(values) + | Self::Set(values) + | Self::Enum { fields: values, .. } + | Self::Machine { fields: values, .. } => { + values.iter().any(Self::contains_live_resource_or_grant) + } + Self::Struct { fields, .. } => fields + .iter() + .any(|(_, value)| value.contains_live_resource_or_grant()), + Self::Map(values) => values.iter().any(|(key, value)| { + key.contains_live_resource_or_grant() || value.contains_live_resource_or_grant() + }), + Self::TypeConstruction { fields, .. } => fields + .iter() + .any(|(_, _, _, value)| value.contains_live_resource_or_grant()), + Self::ReflectedCallable(value) => value.source().contains_live_resource_or_grant(), + Self::Function { captures, .. } => { + captures.values().any(Self::contains_live_resource_or_grant) + } + Self::Int64(_) + | Self::Uint64(_) + | Self::Float64(_) + | Self::String(_) + | Self::Bool(_) + | Self::Bytes(_) + | Self::OptionalNone + | Self::Nothing + | Self::Capability(_) + | Self::Actor(_) + | Self::Error(_) + | Self::NamedFunction(_) + | Self::ResourceHook(_) => false, + } + } + + pub(crate) fn primitive_carrier_name(&self) -> Option<&'static str> { + match self { + Self::Int64(_) => Some("int64"), + Self::Uint64(_) => Some("uint64"), + Self::Float64(_) => Some("float64"), + Self::String(_) => Some("string"), + Self::Bool(_) => Some("bool"), + Self::Bytes(_) => Some("bytes"), + Self::Nothing => Some("nothing"), + _ => None, + } + } + + /// Borrow storage without discarding the identity retained by its owner. + pub fn payload(&self) -> &Self { + match self { + Self::Typed { value, .. } => value.payload(), + value => value, + } + } + + /// Consume a value at a concrete operation or conversion boundary. + pub fn into_payload(self) -> Self { + match self { + Self::Typed { value, .. } => value.into_payload(), + value => value, + } + } +} + impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { - match (self, other) { + match (self.payload(), other.payload()) { (Value::Int64(a), Value::Int64(b)) => a == b, (Value::Uint64(a), Value::Uint64(b)) => a == b, (Value::Float64(a), Value::Float64(b)) => a == b, @@ -109,10 +258,12 @@ impl PartialEq for Value { ) => t1 == t2 && v1 == v2 && s1 == s2 && f1 == f2, ( Value::Struct { + concrete_type: _, type_name: t1, fields: f1, }, Value::Struct { + concrete_type: _, type_name: t2, fields: f2, }, @@ -157,6 +308,7 @@ impl PartialEq for Value { impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Value::Typed { value, .. } => value.fmt(f), Value::Int64(n) => write!(f, "{n}"), Value::Uint64(n) => write!(f, "{n}"), Value::Float64(n) => write!(f, "{n}"), @@ -188,6 +340,9 @@ impl fmt::Display for Value { Value::OptionalNone => write!(f, "none"), Value::Nothing => write!(f, "nothing"), Value::Capability(name) => write!(f, "<{name} capability>"), + Value::Resource(_) => f.write_str(""), + Value::ResourceHook(_) => f.write_str(""), + Value::GrantedNetwork(_) => f.write_str(""), Value::TypeConstruction { type_name, variant, @@ -210,7 +365,11 @@ impl fmt::Display for Value { } write!(f, ")") } - Value::Struct { type_name, fields } => { + Value::Struct { + concrete_type: _, + type_name, + fields, + } => { write!(f, "{type_name}(")?; for (i, (name, value)) in fields.iter().enumerate() { if i > 0 { @@ -257,6 +416,7 @@ impl fmt::Display for Value { Ok(()) } Value::NamedFunction(name) => write!(f, "function({name})"), + Value::ReflectedCallable(value) => fmt::Display::fmt(value.source(), f), Value::Function { params, .. } => { write!(f, "function(")?; for (i, p) in params.iter().enumerate() { diff --git a/crates/jett_comptime/src/value/closure_capture.rs b/crates/jett_comptime/src/value/closure_capture.rs new file mode 100644 index 00000000..74e5db08 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture.rs @@ -0,0 +1,431 @@ +//! Private capture-time proof. Admission cannot mint or reseal this witness. +use std::sync::Arc; + +use jett_common::Span; +use jett_parser::ast::{Block, Expr, FunctionDef, Item}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId}; + +use super::{Value, reflected_callable::original_source}; +pub(crate) use original_source::named_function as original_named_function; +pub(crate) use original_source::variable_declaration as original_variable_declaration; + +#[path = "closure_capture/ast_equal.rs"] +mod ast_equal; +#[path = "closure_capture/checked_context.rs"] +mod checked_context; +#[path = "closure_capture/value_equal.rs"] +mod value_equal; + +#[cfg(test)] +#[path = "closure_capture/test_owner_tests.rs"] +mod test_owner_tests; + +#[derive(Debug, Clone, Copy)] +enum Owner { + Named(DefId), + Closure(Span), + Verify(usize), + Property(usize), + ActorHandler(jett_typecheck::CheckedActorHandlerTarget), + ActorClosure(jett_typecheck::CheckedActorHandlerTarget, Span), +} + +#[derive(Debug, Clone)] +pub(crate) struct OriginalClosureOwner { + checked: Arc, + owner: Owner, + namespace: Option, + definition: Option, +} + +impl OriginalClosureOwner { + pub(crate) fn registered( + checked: &Arc, + name: &str, + current: &FunctionDef, + namespace: &Option, + ) -> Option { + let mut candidates = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.name == name + && &info.namespace == namespace + && original_source::named_function(checked, info.id) + .is_some_and(|original| ast_equal::function(original, current)) + }); + let selected = candidates.next()?; + if candidates.next().is_some() { + return None; + } + Some(Self { + checked: checked.clone(), + owner: Owner::Named(selected.id), + namespace: namespace.clone(), + definition: Some(selected.id), + }) + } + + pub(crate) fn test_body( + checked: &Arc, + original: &Block, + purpose: crate::ExecutionPurpose, + namespace: Option<&str>, + ) -> Option { + let mut lexical_namespace = None; + let mut namespace_file = None; + for (index, item) in checked.module().items.iter().enumerate() { + if let Item::Namespace(declaration) = item { + lexical_namespace = Some(declaration.name.name.clone()); + namespace_file = Some(declaration.name.span.file); + } + let (body, owner) = match (item, purpose) { + (Item::Verify(block), crate::ExecutionPurpose::Verify) => { + (&block.body, Owner::Verify(index)) + } + (Item::Property(block), crate::ExecutionPurpose::Property) => { + (&block.body, Owner::Property(index)) + } + _ => continue, + }; + if !std::ptr::eq(body, original) { + continue; + } + let selected_namespace = if namespace_file == Some(body.span.file) { + lexical_namespace + } else { + None + }; + if selected_namespace.as_deref() != namespace { + return None; + } + return Some(Self { + checked: checked.clone(), + owner, + namespace: selected_namespace, + definition: None, + }); + } + None + } + + pub(crate) fn actor_handler( + checked: &Arc, + target: jett_typecheck::CheckedActorHandlerTarget, + namespace: Option<&str>, + ) -> Option { + let info = checked + .resolved() + .scope_table + .definitions + .get(target.actor_definition.index() as usize)?; + if info.id != target.actor_definition + || info.kind != DefKind::Actor + || info.namespace.as_deref() != namespace + { + return None; + } + let metadata = checked.checked().actor_handler(target)?; + let mut matching = checked.module().items.iter().filter_map(|item| { + let Item::Actor(actor) = item else { + return None; + }; + let handler = actor.handlers.get(target.handler_index)?; + (actor.name.span == info.span + && handler.body.span == metadata.body_span() + && metadata.matches_source_actor(actor)) + .then_some(handler) + }); + matching.next()?; + if matching.next().is_some() { + return None; + } + Some(Self { + checked: checked.clone(), + owner: Owner::ActorHandler(target), + namespace: namespace.map(str::to_owned), + definition: None, + }) + } + + fn actor_target(&self) -> Option { + match self.owner { + Owner::ActorHandler(target) | Owner::ActorClosure(target, _) => Some(target), + _ => None, + } + } + + fn body(&self) -> Option<&Block> { + match self.owner { + Owner::Named(definition) => original_source::named_function(&self.checked, definition) + .map(|function| &function.body), + Owner::Closure(span) => match original_source::closure_node(&self.checked, span)? { + Expr::InlineFn(_, _, body, _) => Some(body), + _ => None, + }, + Owner::Verify(index) => match self.checked.module().items.get(index)? { + Item::Verify(block) => Some(&block.body), + _ => None, + }, + Owner::ActorHandler(target) => { + let info = self + .checked + .resolved() + .scope_table + .definitions + .get(target.actor_definition.index() as usize)?; + let metadata = self.checked.checked().actor_handler(target)?; + self.checked.module().items.iter().find_map(|item| { + let Item::Actor(actor) = item else { + return None; + }; + let handler = actor.handlers.get(target.handler_index)?; + (actor.name.span == info.span + && handler.body.span == metadata.body_span() + && metadata.matches_source_actor(actor)) + .then_some(&handler.body) + }) + } + Owner::ActorClosure(_, span) => { + match original_source::closure_node(&self.checked, span)? { + Expr::InlineFn(_, _, body, _) => Some(body), + _ => None, + } + } + Owner::Property(index) => match self.checked.module().items.get(index)? { + Item::Property(block) => Some(&block.body), + _ => None, + }, + } + } +} + +#[derive(Debug)] +pub(crate) struct CheckedClosureCapture { + origin: OriginalClosureOwner, + node_span: Span, + body_span: Span, + source_type: TypeId, + snapshot: Box, +} + +impl CheckedClosureCapture { + pub(crate) fn field_original_node(&self) -> Option<&Expr> { + let node = original_source::closure_node_in_body(self.origin.body()?, self.body_span)?; + let global = original_source::closure_node(&self.origin.checked, self.body_span)?; + std::ptr::eq(node, global).then_some(node) + } + pub(crate) fn seal(origin: &OriginalClosureOwner, value: &Value) -> Option> { + let Value::Function { + type_context, + params, + body, + namespace, + .. + } = value + else { + return None; + }; + if namespace != &origin.namespace || type_context.reflected_capture.is_some() { + return None; + } + let original = original_source::closure_node_in_body(origin.body()?, body.span)?; + let global = original_source::closure_node(&origin.checked, body.span)?; + if !std::ptr::eq(original, global) { + return None; + } + let Expr::InlineFn(original_params, original_return, original_body, node_span) = original + else { + return None; + }; + if !ast_equal::params(params, original_params) + || !ast_equal::optional( + &type_context.return_type, + original_return, + ast_equal::type_expr, + ) + || !ast_equal::block(body, original_body) + { + return None; + } + let source_type = checked_context::callable_type(origin, type_context, *node_span)?; + if !matches!( + origin.checked.checked().interner.resolve(source_type), + Type::Function { .. } + ) { + return None; + } + if value.contains_live_resource_or_grant() { + return None; + } + Some(Arc::new(Self { + origin: origin.clone(), + node_span: *node_span, + body_span: body.span, + source_type, + snapshot: Box::new(value.clone()), + })) + } + + pub(crate) fn validate_source( + &self, + checked: &Arc, + actual: TypeId, + value: &Value, + ) -> bool { + Arc::ptr_eq(&self.origin.checked, checked) + && actual == self.source_type + && matches!(value, Value::Function { type_context, .. } if checked_context::callable_type(&self.origin, type_context, self.node_span) == Some(actual)) + && original_source::closure_node(checked, self.body_span).is_some_and(|node| { + let Expr::InlineFn(params, result, body, span) = node else { + return false; + }; + let Value::Function { + type_context, + params: current, + body: current_body, + .. + } = value + else { + return false; + }; + *span == self.node_span + && ast_equal::params(params, current) + && ast_equal::optional(result, &type_context.return_type, ast_equal::type_expr) + && ast_equal::block(body, current_body) + }) + && value_equal::closure(&self.snapshot, value, false) + } +} + +pub(crate) fn invocation_origin( + value: &Value, + checked: &Arc, +) -> Option { + let Value::Function { type_context, .. } = value.payload() else { + return None; + }; + let proof = type_context.reflected_capture.as_ref()?; + proof + .validate_source(checked, proof.source_type, value.payload()) + .then(|| OriginalClosureOwner { + checked: checked.clone(), + owner: proof + .origin + .actor_target() + .map_or(Owner::Closure(proof.body_span), |target| { + Owner::ActorClosure(target, proof.body_span) + }), + namespace: proof.origin.namespace.clone(), + definition: proof.origin.definition, + }) +} + +/// Reuse the existing capture snapshot; no span/name-only closure authority. +pub(crate) fn field_invocation_proof( + value: &Value, + checked: &Arc, +) -> Option> { + let Value::Function { type_context, .. } = value else { + return None; + }; + let proof = type_context.reflected_capture.as_ref()?; + proof + .validate_source(checked, proof.source_type, value) + .then(|| proof.clone()) +} + +pub(crate) fn same_snapshot(a: &Value, b: &Value) -> bool { + value_equal::value(a, b) +} +pub(crate) fn same_type_expr( + a: &jett_parser::ast::TypeExpr, + b: &jett_parser::ast::TypeExpr, +) -> bool { + ast_equal::type_expr(a, b) +} +pub(crate) fn original_root_types( + checked: &CheckedResourceProgram, + installed: &crate::checked_types::CheckedExpressionTypes, +) -> bool { + checked_context::root_matches(checked, installed) +} + +/// Compare data projections against one retained concrete body. This does not +/// grant executable identity or field-write authority by itself. +pub(crate) fn matches_field_function_projection( + current: &crate::checked_types::CheckedFunctionTypes, + arguments: &[super::ClosureTypeArgument], + original: &jett_typecheck::CheckedGenericFunctionInstantiation, + types: &jett_types::TypeInterner, +) -> bool { + let context = super::ClosureTypeContext { + arguments: arguments.to_vec(), + ..Default::default() + }; + checked_context::function_matches(current, &context, original, types) +} + +/// Required values must be closed even when constructing a lazy closure does not +/// execute its body. Only actual lexical captures satisfy Variable/Param reads; +/// named functions, constants and type declarations remain registry references. +pub(crate) fn validate_required_capture_environment( + checked: &Arc, + params: &[jett_parser::ast::Param], + return_type: Option<&jett_parser::ast::TypeExpr>, + body: &Block, + captures: &std::collections::HashMap, +) -> Result<(), String> { + let original = original_source::closure_node(checked, body.span) + .ok_or("required closure has no exact original checked Source")?; + let Expr::InlineFn(original_params, original_return, original_body, _) = original else { + return Err("required closure changed its original Source kind".into()); + }; + if !ast_equal::params(params, original_params) + || !ast_equal::optional(&return_type.cloned(), original_return, ast_equal::type_expr) + || !ast_equal::block(body, original_body) + { + return Err("required closure differs from its original checked Source".into()); + } + let inside = |span: Span| { + span.file == original_body.span.file + && span.start >= original_body.span.start + && span.end <= original_body.span.end + }; + let resolved = checked.resolved(); + let mut references = resolved + .resolutions + .iter() + .filter(|(span, _)| inside(**span)) + .collect::>(); + references.sort_by_key(|(span, _)| (span.start, span.end)); + for (_, definition) in references { + let binding = resolved + .scope_table + .definitions + .get(definition.index() as usize) + .filter(|binding| binding.id == *definition) + .ok_or("required closure reference lost its exact checked definition")?; + if !matches!(binding.kind, DefKind::Variable | DefKind::Param) + || inside(binding.span) + || original_params.iter().any(|param| param.name.span == binding.span) + || checked.module().items.iter().any(|item| { + matches!(item, Item::VarDecl(declaration) if declaration.name.span == binding.span) + }) + { + continue; + } + if !captures.contains_key(&binding.name) { + return Err(format!( + "required closure must be closed: lexical capture '{}' is unavailable during compilation", + binding.name + )); + } + } + Ok(()) +} diff --git a/crates/jett_comptime/src/value/closure_capture/ast_equal.rs b/crates/jett_comptime/src/value/closure_capture/ast_equal.rs new file mode 100644 index 00000000..9d2d1fc5 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/ast_equal.rs @@ -0,0 +1,212 @@ +//! Typed source comparison for producer custody; no display/hash authority. +use jett_parser::ast::*; + +pub(super) fn list(a: &[T], b: &[T], mut same: impl FnMut(&T, &T) -> bool) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same(a, b)) +} +pub(super) fn optional(a: &Option, b: &Option, same: impl FnOnce(&T, &T) -> bool) -> bool { + match (a, b) { + (Some(a), Some(b)) => same(a, b), + (None, None) => true, + _ => false, + } +} +fn ident(a: &Ident, b: &Ident) -> bool { + a.name == b.name && a.span == b.span +} +pub(super) fn type_expr(a: &TypeExpr, b: &TypeExpr) -> bool { + if a.span() != b.span() { + return false; + } + match (a, b) { + (TypeExpr::Named(a), TypeExpr::Named(b)) => ident(a, b), + (TypeExpr::Generic(an, aa, _), TypeExpr::Generic(bn, ba, _)) => { + ident(an, bn) && list(aa, ba, type_expr) + } + (TypeExpr::View(a, _), TypeExpr::View(b, _)) => type_expr(a, b), + (TypeExpr::Function(ap, ar, _), TypeExpr::Function(bp, br, _)) => { + list(ap, bp, type_expr) && type_expr(ar, br) + } + (TypeExpr::StateQualified(a, an, _), TypeExpr::StateQualified(b, bn, _)) => { + type_expr(a, b) && ident(an, bn) + } + _ => false, + } +} +pub(super) fn params(a: &[Param], b: &[Param]) -> bool { + list(a, b, |a, b| { + a.view == b.view + && a.mutable == b.mutable + && a.span == b.span + && ident(&a.name, &b.name) + && type_expr(&a.ty, &b.ty) + }) +} +pub(super) fn function(a: &FunctionDef, b: &FunctionDef) -> bool { + a.span == b.span + && a.exported == b.exported + && ident(&a.name, &b.name) + && list(&a.type_params, &b.type_params, ident) + && params(&a.params, &b.params) + && optional(&a.return_type, &b.return_type, type_expr) + && block(&a.body, &b.body) +} +pub(super) fn block(a: &Block, b: &Block) -> bool { + a.span == b.span && list(&a.stmts, &b.stmts, stmt) +} +fn pattern(a: &Pattern, b: &Pattern) -> bool { + match (a, b) { + (Pattern::Ident(a), Pattern::Ident(b)) => ident(a, b), + (Pattern::Variant(an, aa), Pattern::Variant(bn, ba)) => { + ident(an, bn) && list(aa, ba, ident) + } + (Pattern::Other(a), Pattern::Other(b)) => a == b, + _ => false, + } +} +fn stmt(a: &Stmt, b: &Stmt) -> bool { + match (a, b) { + (Stmt::VarDecl(a), Stmt::VarDecl(b)) => { + a.span == b.span + && a.mutable == b.mutable + && type_expr(&a.ty, &b.ty) + && ident(&a.name, &b.name) + && expr(&a.value, &b.value) + } + (Stmt::Assign(a), Stmt::Assign(b)) => { + a.span == b.span && expr(&a.target, &b.target) && expr(&a.value, &b.value) + } + (Stmt::Return(a), Stmt::Return(b)) => { + a.span == b.span && optional(&a.value, &b.value, expr) + } + (Stmt::Respond(a), Stmt::Respond(b)) => a.span == b.span && expr(&a.value, &b.value), + (Stmt::ComptimeTypeBind(a), Stmt::ComptimeTypeBind(b)) => { + a.span == b.span + && ident(&a.name, &b.name) + && expr(&a.value, &b.value) + && block(&a.body, &b.body) + } + (Stmt::If(a), Stmt::If(b)) => { + a.span == b.span + && expr(&a.condition, &b.condition) + && block(&a.then_block, &b.then_block) + && list(&a.else_ifs, &b.else_ifs, |(ac, ab), (bc, bb)| { + expr(ac, bc) && block(ab, bb) + }) + && optional(&a.else_block, &b.else_block, block) + } + (Stmt::For(a), Stmt::For(b)) => { + a.span == b.span + && a.view == b.view + && ident(&a.variable, &b.variable) + && optional(&a.value_variable, &b.value_variable, ident) + && expr(&a.iterable, &b.iterable) + && block(&a.body, &b.body) + } + (Stmt::While(a), Stmt::While(b)) => { + a.span == b.span && expr(&a.condition, &b.condition) && block(&a.body, &b.body) + } + (Stmt::Match(a), Stmt::Match(b)) => { + a.span == b.span + && expr(&a.expr, &b.expr) + && list(&a.arms, &b.arms, |a, b| { + a.span == b.span && pattern(&a.pattern, &b.pattern) && block(&a.body, &b.body) + }) + } + (Stmt::Expr(a), Stmt::Expr(b)) => a.span == b.span && expr(&a.expr, &b.expr), + (Stmt::Use(a), Stmt::Use(b)) => { + a.span == b.span && ident(&a.path, &b.path) && optional(&a.alias, &b.alias, ident) + } + (Stmt::Assert(a), Stmt::Assert(b)) => { + a.span == b.span + && expr(&a.condition, &b.condition) + && optional(&a.message, &b.message, expr) + } + (Stmt::Trace(a), Stmt::Trace(b)) => a.span == b.span && ident(&a.name, &b.name), + (Stmt::Breakpoint(a), Stmt::Breakpoint(b)) => { + a.span == b.span && optional(&a.condition, &b.condition, expr) + } + (Stmt::Break(a), Stmt::Break(b)) | (Stmt::Continue(a), Stmt::Continue(b)) => a == b, + _ => false, + } +} +fn args(a: &[CallArg], b: &[CallArg]) -> bool { + list(a, b, |a, b| { + a.span == b.span && optional(&a.name, &b.name, ident) && expr(&a.value, &b.value) + }) +} +pub(super) fn expr(a: &Expr, b: &Expr) -> bool { + if a.span() != b.span() { + return false; + } + match (a, b) { + (Expr::IntLiteral(a, _), Expr::IntLiteral(b, _)) => a == b, + (Expr::FloatLiteral(a, _), Expr::FloatLiteral(b, _)) => a.to_bits() == b.to_bits(), + (Expr::StringLiteral(a, _), Expr::StringLiteral(b, _)) => a == b, + (Expr::BoolLiteral(a, _), Expr::BoolLiteral(b, _)) => a == b, + (Expr::Nothing(_), Expr::Nothing(_)) + | (Expr::None(_), Expr::None(_)) + | (Expr::Error(_), Expr::Error(_)) => true, + (Expr::Ident(a), Expr::Ident(b)) => ident(a, b), + (Expr::Binary(al, ao, ar, _), Expr::Binary(bl, bo, br, _)) => { + ao == bo && expr(al, bl) && expr(ar, br) + } + (Expr::Unary(ao, a, _), Expr::Unary(bo, b, _)) => ao == bo && expr(a, b), + (Expr::FieldAccess(a, an, _), Expr::FieldAccess(b, bn, _)) + | (Expr::At(a, an, _), Expr::At(b, bn, _)) => expr(a, b) && ident(an, bn), + (Expr::Call(a, aa, _), Expr::Call(b, ba, _)) => expr(a, b) && args(aa, ba), + (Expr::GenericCall(a, at, aa, _), Expr::GenericCall(b, bt, ba, _)) => { + expr(a, b) && list(at, bt, type_expr) && args(aa, ba) + } + (Expr::Paren(a, _), Expr::Paren(b, _)) + | (Expr::View(a, _), Expr::View(b, _)) + | (Expr::Comptime(a, _), Expr::Comptime(b, _)) + | (Expr::Ok(a, _), Expr::Ok(b, _)) + | (Expr::Fail(a, _), Expr::Fail(b, _)) + | (Expr::Some(a, _), Expr::Some(b, _)) + | (Expr::Default(a, _), Expr::Default(b, _)) + | (Expr::Declassify(a, _), Expr::Declassify(b, _)) + | (Expr::Coarsen(a, _), Expr::Coarsen(b, _)) + | (Expr::Spawn(a, _), Expr::Spawn(b, _)) + | (Expr::Send(a, _), Expr::Send(b, _)) + | (Expr::Ask(a, _), Expr::Ask(b, _)) + | (Expr::Clone(a, _), Expr::Clone(b, _)) + | (Expr::Run(a, _), Expr::Run(b, _)) + | (Expr::Join(a, _), Expr::Join(b, _)) + | (Expr::Cancel(a, _), Expr::Cancel(b, _)) => expr(a, b), + (Expr::ListConstruct(a, _), Expr::ListConstruct(b, _)) => list(a, b, expr), + (Expr::MapConstruct(a, _), Expr::MapConstruct(b, _)) => { + list(a, b, |(ak, av), (bk, bv)| expr(ak, bk) && expr(av, bv)) + } + (Expr::Handle(a, an, ab, _), Expr::Handle(b, bn, bb, _)) => { + expr(a, b) && optional(an, bn, ident) && block(ab, bb) + } + (Expr::EnumVariant(at, av, _), Expr::EnumVariant(bt, bv, _)) => { + ident(at, bt) && ident(av, bv) + } + (Expr::StringInterpolation(a, _), Expr::StringInterpolation(b, _)) => { + list(a, b, |a, b| match (a, b) { + (StringPart::Literal(a), StringPart::Literal(b)) => a == b, + (StringPart::Expr(a), StringPart::Expr(b)) => expr(a, b), + _ => false, + }) + } + (Expr::Pipeline(a, aa, _), Expr::Pipeline(b, ba, _)) => { + expr(a, b) + && list(aa, ba, |a, b| { + a.span == b.span + && expr(&a.function, &b.function) + && args(&a.extra_args, &b.extra_args) + && optional(&a.handle, &b.handle, |a, b| { + a.span == b.span + && optional(&a.error_name, &b.error_name, ident) + && block(&a.body, &b.body) + }) + }) + } + (Expr::InlineFn(ap, ar, ab, _), Expr::InlineFn(bp, br, bb, _)) => { + params(ap, bp) && optional(ar, br, type_expr) && block(ab, bb) + } + _ => false, + } +} diff --git a/crates/jett_comptime/src/value/closure_capture/checked_context.rs b/crates/jett_comptime/src/value/closure_capture/checked_context.rs new file mode 100644 index 00000000..aaa9246f --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/checked_context.rs @@ -0,0 +1,228 @@ +//! Join interpreter projections to complete immutable checker facts. +use super::OriginalClosureOwner; +use crate::checked_types::{ + CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedBindings, CheckedScopedTypes, +}; +use crate::value::ClosureTypeContext; +use jett_common::Span; +use jett_typecheck::{CheckedComptimeTypeBinding, CheckedGenericFunctionInstantiation}; +use jett_types::{TypeId, TypeInterner}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +fn remove_scoped( + names: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + names.remove(span); + } + remove_scoped(names, &binding.body.comptime_type_bindings); + } +} +fn projection( + types: &HashMap, + bindings: &HashMap>, + interner: &TypeInterner, +) -> (HashMap, CheckedScopedBindings) { + let mut names = types + .iter() + .map(|(span, ty)| (*span, interner.type_name(*ty))) + .collect(); + remove_scoped(&mut names, bindings); + let nested: HashSet<_> = bindings + .values() + .flatten() + .flat_map(|binding| binding.body.comptime_type_bindings.keys().copied()) + .collect(); + let scopes = bindings + .iter() + .filter(|(span, _)| !nested.contains(span)) + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|binding| { + let (expressions, bindings) = projection( + &binding.body.type_map, + &binding.body.comptime_type_bindings, + interner, + ); + Arc::new(CheckedScopedTypes { + bound_type: interner.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) + }) + .collect(), + ) + }) + .collect(); + (names, scopes) +} +pub(super) fn root_matches( + checked: &jett_typecheck::CheckedResourceProgram, + installed: &CheckedExpressionTypes, +) -> bool { + let checked = checked.checked(); + let (mut expressions, bindings) = projection( + &checked.type_map, + &checked.comptime_type_bindings, + &checked.interner, + ); + for instance in &checked.generic_function_instantiations { + for span in instance.type_map.keys() { + expressions.remove(span); + } + remove_scoped(&mut expressions, &instance.comptime_type_bindings); + } + installed.expressions == expressions && installed.bindings == bindings +} +pub(super) fn function_matches( + current: &CheckedFunctionTypes, + context: &ClosureTypeContext, + original: &CheckedGenericFunctionInstantiation, + interner: &TypeInterner, +) -> bool { + let (names, bindings) = projection( + &original.type_map, + &original.comptime_type_bindings, + interner, + ); + let args: Vec<_> = original + .concrete_args + .iter() + .map(|ty| interner.type_name(*ty)) + .collect(); + let facts = &original.specialization; + current.expressions.as_ref() == &names + && current.bindings == bindings + && current.type_arguments == args + && current.type_argument_reflections == facts.type_argument_reflections + && current.type_info_kinds == facts.type_info_kinds + && current.type_info_primitives == facts.type_info_primitives + && current.type_kind_values == facts.type_kind_values + && current.type_primitive_values == facts.type_primitive_values + && context.arguments.len() == args.len() + && context + .arguments + .iter() + .zip(&args) + .all(|(actual, expected)| &actual.canonical_name == expected) + && (facts.type_argument_reflections.is_empty() + || context + .arguments + .iter() + .zip(&facts.type_argument_reflections) + .all(|(actual, expected)| actual.reflection.as_ref() == Some(expected))) +} +fn scoped_types( + current: &CheckedScopedTypes, + span: Span, + bindings: &HashMap>, + interner: &TypeInterner, + found: &mut Vec, +) { + for original in bindings.values().flatten() { + let (expressions, bindings) = projection( + &original.body.type_map, + &original.body.comptime_type_bindings, + interner, + ); + let expected = CheckedScopedTypes { + bound_type: interner.type_name(original.bound_type), + reflection: Some(original.reflection.clone()), + expressions, + bindings, + }; + if current == &expected { + if let Some(ty) = original.body.type_map.get(&span) { + found.push(*ty); + } + } + scoped_types( + current, + span, + &original.body.comptime_type_bindings, + interner, + found, + ); + } +} +pub(super) fn callable_type( + origin: &OriginalClosureOwner, + context: &ClosureTypeContext, + span: Span, +) -> Option { + let checked = origin.checked.checked(); + let mut found = Vec::new(); + if let Some(target) = origin.actor_target() { + if context.checked_function.is_some() || !context.arguments.is_empty() { + return None; + } + let handler = checked.actor_handler(target)?; + let current = context.checked_scope.as_ref()?; + let (expressions, bindings) = projection( + &handler.body().type_map, + &handler.body().comptime_type_bindings, + &checked.interner, + ); + let expected = CheckedScopedTypes { + bound_type: checked.interner.resolve_actor(target.actor).name.clone(), + reflection: None, + expressions, + bindings, + }; + if current.as_ref() == &expected { + if let Some(ty) = handler.body().type_map.get(&span) { + found.push(*ty); + } + } else { + scoped_types( + current, + span, + &handler.body().comptime_type_bindings, + &checked.interner, + &mut found, + ); + } + } else if let Some(current) = &context.checked_function { + for original in &checked.generic_function_instantiations { + if Some(original.definition) != origin.definition + || !function_matches(current, context, original, &checked.interner) + { + continue; + } + if let Some(scope) = &context.checked_scope { + scoped_types( + scope, + span, + &original.comptime_type_bindings, + &checked.interner, + &mut found, + ); + } else if let Some(ty) = original.type_map.get(&span) { + found.push(*ty); + } + } + } else if context.arguments.is_empty() { + if let Some(scope) = &context.checked_scope { + scoped_types( + scope, + span, + &checked.comptime_type_bindings, + &checked.interner, + &mut found, + ); + } else if let Some(ty) = checked.type_map.get(&span) { + found.push(*ty); + } + } + let selected = *found.first()?; + found.iter().all(|ty| *ty == selected).then_some(selected) +} diff --git a/crates/jett_comptime/src/value/closure_capture/test_owner_tests.rs b/crates/jett_comptime/src/value/closure_capture/test_owner_tests.rs new file mode 100644 index 00000000..98fb105e --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/test_owner_tests.rs @@ -0,0 +1,104 @@ +use super::*; +use crate::value::ClosureTypeContext; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::ast::Stmt; +use jett_typecheck::CheckOptions; +use std::collections::HashMap; + +const SOURCE: &str = "namespace app\nverify captured_checked:\n int64 seed = 7\n function() returns int64 callback = function() returns int64: return seed\n assert callback() == 7\nproperty captured_samples:\n given repeat: bool\n int64 seed = 7\n function() returns int64 callback = function() returns int64: return seed\n assert callback() == 7\n"; +fn checked() -> Arc { + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, FileId::new(0)), + HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(), + ) +} +fn blocks(checked: &CheckedResourceProgram) -> Vec<(&Block, crate::ExecutionPurpose)> { + checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Verify(block) => Some((&block.body, crate::ExecutionPurpose::Verify)), + Item::Property(block) => Some((&block.body, crate::ExecutionPurpose::Property)), + _ => None, + }) + .collect() +} +#[test] +fn original_test_capture_owner_rejects_cloned_foreign_wrong_purpose_and_namespace_blocks() { + let checked = checked(); + let foreign = self::checked(); + for (body, purpose) in blocks(&checked) { + assert!(OriginalClosureOwner::test_body(&checked, body, purpose, Some("app")).is_some()); + assert!( + OriginalClosureOwner::test_body(&checked, &body.clone(), purpose, Some("app")) + .is_none() + ); + assert!(OriginalClosureOwner::test_body(&foreign, body, purpose, Some("app")).is_none()); + assert!( + OriginalClosureOwner::test_body( + &checked, + body, + crate::ExecutionPurpose::ReferenceRuntime, + Some("app") + ) + .is_none() + ); + assert!(OriginalClosureOwner::test_body(&checked, body, purpose, Some("other")).is_none()); + assert!(OriginalClosureOwner::test_body(&checked, body, purpose, None).is_none()); + } +} +#[test] +fn original_test_capture_witness_preserves_original_body_context_and_capture_snapshot() { + let checked = checked(); + for (body, purpose) in blocks(&checked) { + let owner = OriginalClosureOwner::test_body(&checked, body, purpose, Some("app")).unwrap(); + let Stmt::VarDecl(callback) = &body.stmts[1] else { + panic!("original callable") + }; + let Expr::InlineFn(params, result, body, span) = &callback.value else { + panic!("original closure") + }; + let actual = checked.checked().type_map[span]; + let value = Value::Function { + type_context: Box::new(ClosureTypeContext { + return_type: result.clone(), + ..ClosureTypeContext::default() + }), + params: params.clone(), + body: body.clone(), + captures: HashMap::from([("seed".into(), Value::Int64(7))]), + capture_types: HashMap::new(), + namespace_aliases: HashMap::new(), + namespace: Some("app".into()), + }; + let witness = CheckedClosureCapture::seal(&owner, &value).unwrap(); + assert!(witness.validate_source(&checked, actual, &value)); + for mutation in 0..3 { + let mut changed = value.clone(); + let Value::Function { + body, + captures, + type_context, + .. + } = &mut changed + else { + unreachable!() + }; + match mutation { + 0 => body.stmts.clear(), + 1 => { + captures.insert("seed".into(), Value::Int64(8)); + } + 2 => type_context.return_type = None, + _ => unreachable!(), + } + assert!(!witness.validate_source(&checked, actual, &changed)); + } + } +} diff --git a/crates/jett_comptime/src/value/closure_capture/value_equal.rs b/crates/jett_comptime/src/value/closure_capture/value_equal.rs new file mode 100644 index 00000000..aff26813 --- /dev/null +++ b/crates/jett_comptime/src/value/closure_capture/value_equal.rs @@ -0,0 +1,163 @@ +//! Snapshot comparison retains tags, pending layers, and callable contexts. +use super::{Value, ast_equal}; +use crate::value::ClosureTypeContext; +use jett_parser::ast::TypeExpr; +use std::{collections::HashMap, sync::Arc}; + +fn map(a: &HashMap, b: &HashMap, same: impl Fn(&T, &T) -> bool) -> bool { + a.len() == b.len() + && a.iter() + .all(|(key, a)| b.get(key).is_some_and(|b| same(a, b))) +} +fn shared(a: &Option>, b: &Option>) -> bool { + ast_equal::optional(a, b, Arc::ptr_eq) +} +fn context(a: &ClosureTypeContext, b: &ClosureTypeContext, proof: bool) -> bool { + (!proof || shared(&a.reflected_capture, &b.reflected_capture)) + && ast_equal::optional(&a.return_type, &b.return_type, ast_equal::type_expr) + && shared(&a.checked_scope, &b.checked_scope) + && shared(&a.checked_function, &b.checked_function) + && a.scoped_type_bindings == b.scoped_type_bindings + && map(&a.bindings, &b.bindings, ast_equal::type_expr) + && ast_equal::list(&a.arguments, &b.arguments, |a, b| { + a.canonical_name == b.canonical_name + && a.reflection == b.reflection + && ast_equal::type_expr(&a.ty, &b.ty) + }) +} +pub(super) fn closure(a: &Value, b: &Value, proof: bool) -> bool { + let ( + Value::Function { + type_context: ac, + params: ap, + body: ab, + captures: av, + capture_types: at, + namespace_aliases: aa, + namespace: an, + }, + Value::Function { + type_context: bc, + params: bp, + body: bb, + captures: bv, + capture_types: bt, + namespace_aliases: ba, + namespace: bn, + }, + ) = (a, b) + else { + return false; + }; + context(ac, bc, proof) + && ast_equal::params(ap, bp) + && ast_equal::block(ab, bb) + && map(av, bv, value) + && map::(at, bt, ast_equal::type_expr) + && aa == ba + && an == bn +} +pub(super) fn value(a: &Value, b: &Value) -> bool { + match (a, b) { + ( + Value::Typed { + type_name: an, + value: a, + }, + Value::Typed { + type_name: bn, + value: b, + }, + ) => an == bn && value(a, b), + (Value::Int64(a), Value::Int64(b)) => a == b, + (Value::Uint64(a), Value::Uint64(b)) => a == b, + (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(), + (Value::String(a), Value::String(b)) + | (Value::Capability(a), Value::Capability(b)) + | (Value::Error(a), Value::Error(b)) + | (Value::NamedFunction(a), Value::NamedFunction(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Bytes(a), Value::Bytes(b)) => a == b, + (Value::Actor(a), Value::Actor(b)) => a == b, + (Value::OptionalNone, Value::OptionalNone) | (Value::Nothing, Value::Nothing) => true, + (Value::ResultOk(a), Value::ResultOk(b)) + | (Value::ResultFail(a), Value::ResultFail(b)) + | (Value::OptionalSome(a), Value::OptionalSome(b)) + | (Value::Pending(a), Value::Pending(b)) => value(a, b), + (Value::List(a), Value::List(b)) | (Value::Set(a), Value::Set(b)) => { + ast_equal::list(a, b, value) + } + (Value::Map(a), Value::Map(b)) => { + ast_equal::list(a, b, |(ak, av), (bk, bv)| value(ak, bk) && value(av, bv)) + } + ( + Value::TypeConstruction { + type_name: an, + variant: ar, + state: as_, + fields: a, + }, + Value::TypeConstruction { + type_name: bn, + variant: br, + state: bs, + fields: b, + }, + ) => { + an == bn + && ar == br + && as_ == bs + && ast_equal::list(a, b, |(ai, an, at, av), (bi, bn, bt, bv)| { + ai == bi && an == bn && at == bt && value(av, bv) + }) + } + ( + Value::Struct { + type_name: an, + concrete_type: ac, + fields: a, + }, + Value::Struct { + type_name: bn, + concrete_type: bc, + fields: b, + }, + ) => { + an == bn + && ac == bc + && ast_equal::list(a, b, |(an, av), (bn, bv)| an == bn && value(av, bv)) + } + ( + Value::Enum { + type_name: an, + variant: ar, + fields: a, + }, + Value::Enum { + type_name: bn, + variant: br, + fields: b, + }, + ) + | ( + Value::Machine { + type_name: an, + state: ar, + fields: a, + }, + Value::Machine { + type_name: bn, + state: br, + fields: b, + }, + ) => an == bn && ar == br && ast_equal::list(a, b, value), + (Value::ResourceHook(a), Value::ResourceHook(b)) => a.same_descriptor(b), + (Value::ReflectedCallable(a), Value::ReflectedCallable(b)) => a.same_snapshot(b), + (Value::Function { .. }, Value::Function { .. }) => closure(a, b, true), + // Capture construction already refuses live custody; never copy or + // inspect it to manufacture equality for this unrelated capability. + (Value::Resource(_), Value::Resource(_)) + | (Value::GrantedNetwork(_), Value::GrantedNetwork(_)) => false, + _ => false, + } +} diff --git a/crates/jett_comptime/src/value/reflected_callable.rs b/crates/jett_comptime/src/value/reflected_callable.rs new file mode 100644 index 00000000..ba352580 --- /dev/null +++ b/crates/jett_comptime/src/value/reflected_callable.rs @@ -0,0 +1,201 @@ +//! Owned lazy callback conversion. No public constructor can relabel a callable. +use std::sync::Arc; + +use jett_common::Span; +use jett_parser::ast::{Block, TypeExpr}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::{CheckedReflectedCallablePlan, CheckedResourceProgram}; +use jett_types::TypeId; + +use super::Value; + +#[path = "reflected_callable_source.rs"] +pub(crate) mod original_source; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SourceIdentity { + Named(DefId), + Closure(Span), +} + +#[derive(Clone)] +pub struct ReflectedCallableValue { + source: Box, + checked: Arc, + plan: CheckedReflectedCallablePlan, + identity: SourceIdentity, + source_return: TypeExpr, + requested_return: TypeExpr, +} + +impl std::fmt::Debug for ReflectedCallableValue { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ReflectedCallableValue") + .field("source_label", &self.source.to_string()) + .field("source_type", &self.source_type()) + .field("requested_type", &self.requested_type()) + .finish_non_exhaustive() + } +} + +impl ReflectedCallableValue { + pub(crate) fn new( + source: Value, + checked: Arc, + actual: TypeId, + requested: TypeId, + source_return: TypeExpr, + requested_return: TypeExpr, + ) -> Result { + let plan = checked + .checked() + .reflected_callable_plan(actual, requested) + .ok_or_else(|| { + "reflected callback has no checked return conversion plan".to_string() + })?; + let identity = Self::source_identity(&source, &checked, actual)?; + Ok(Self { + source: Box::new(source), + checked, + plan, + identity, + source_return, + requested_return, + }) + } + + pub(crate) fn authenticate_source( + source: &Value, + checked: &Arc, + actual: TypeId, + ) -> Result<(), String> { + Self::source_identity(source, checked, actual).map(|_| ()) + } + + fn source_identity( + source: &Value, + checked: &Arc, + actual: TypeId, + ) -> Result { + if source.contains_live_resource_or_grant() { + return Err("reflected callback cannot copy live Resource custody".into()); + } + if !matches!( + source.payload(), + Value::NamedFunction(_) + | Value::Function { .. } + | Value::ReflectedCallable(_) + | Value::Pending(_) + ) { + return Err("reflected callback source is not a callable descriptor".into()); + } + fn descriptor(value: &Value) -> &Value { + // Retain descriptor identity without joining or invoking it. This + // never observes a pending callback result or establishes a proof. + match value.payload() { + Value::Pending(inner) => descriptor(inner), + value => value, + } + } + let identity = match descriptor(source) { + Value::NamedFunction(name) => { + let mut definitions = + checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|info| { + info.kind == DefKind::Function + && info.name == *name + && checked.checked().definition_types.get(&info.id) == Some(&actual) + && original_source::named_body(&checked, info.id).is_some() + }); + let selected = definitions + .next() + .ok_or("reflected callback has no exact original source declaration")?; + if definitions.next().is_some() { + return Err( + "reflected callback original source declaration is ambiguous".into(), + ); + } + SourceIdentity::Named(selected.id) + } + Value::Function { + type_context, body, .. + } => { + if !type_context + .reflected_capture + .as_ref() + .is_some_and(|proof| proof.validate_source(checked, actual, descriptor(source))) + { + return Err( + "reflected callback has no unchanged original closure capture".into(), + ); + } + SourceIdentity::Closure(body.span) + } + Value::ReflectedCallable(admitted) + if Arc::ptr_eq(&checked, admitted.checked_program()) + && admitted.requested_type() == actual => + { + admitted.identity + } + _ => { + return Err( + "reflected callback source belongs to another checked callable context".into(), + ); + } + }; + Ok(identity) + } + + pub fn source(&self) -> &Value { + &self.source + } + pub fn source_type(&self) -> TypeId { + self.plan.source_type() + } + pub fn requested_type(&self) -> TypeId { + self.plan.requested_type() + } + pub fn checked_program(&self) -> &Arc { + &self.checked + } + pub fn checked_plan(&self) -> &CheckedReflectedCallablePlan { + &self.plan + } + pub fn source_definition(&self) -> Option { + match self.identity { + SourceIdentity::Named(definition) => Some(definition), + _ => None, + } + } + pub fn source_body(&self) -> Option<&Block> { + match self.identity { + SourceIdentity::Named(definition) => { + original_source::named_body(&self.checked, definition) + } + SourceIdentity::Closure(span) => original_source::closure_body(&self.checked, span), + } + } + pub(crate) fn source_return(&self) -> &TypeExpr { + &self.source_return + } + pub(crate) fn requested_return(&self) -> &TypeExpr { + &self.requested_return + } + + pub(crate) fn same_snapshot(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.checked, &other.checked) + && self.plan == other.plan + && self.identity == other.identity + && super::closure_capture::same_type_expr(&self.source_return, &other.source_return) + && super::closure_capture::same_type_expr( + &self.requested_return, + &other.requested_return, + ) + && super::closure_capture::same_snapshot(&self.source, &other.source) + } +} diff --git a/crates/jett_comptime/src/value/reflected_callable_source.rs b/crates/jett_comptime/src/value/reflected_callable_source.rs new file mode 100644 index 00000000..29ad0f82 --- /dev/null +++ b/crates/jett_comptime/src/value/reflected_callable_source.rs @@ -0,0 +1,393 @@ +//! Exact original Source body lookup; names choose a resolved declaration only. +use jett_parser::ast::{ + Block, ComptimeTypeBindStmt, Expr, FunctionDef, Item, Stmt, StringPart, VarDecl, +}; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::CheckedResourceProgram; + +pub(super) fn named_body(program: &CheckedResourceProgram, definition: DefId) -> Option<&Block> { + named_function(program, definition).map(|function| &function.body) +} + +pub(crate) fn named_function( + program: &CheckedResourceProgram, + definition: DefId, +) -> Option<&FunctionDef> { + let info = program + .resolved() + .scope_table + .definitions + .get(definition.index() as usize)?; + if info.id != definition || info.kind != DefKind::Function { + return None; + } + let mut matches = Vec::new(); + for item in &program.module().items { + match item { + // A mutual declaration owns the DefId and declaration span. The + // resolver joins its later, immutable root definition to that same + // DefId; the body span must not be confused with the declaration. + Item::Function(function) + if function.name.span == info.span + || program.resolved().resolutions.get(&function.name.span) + == Some(&definition) => + { + matches.push(function); + } + Item::Implement(block) => { + for method in &block.methods { + if method.name.span == info.span { + matches.push(method); + } + } + } + Item::Struct(structure) => { + for method in &structure.methods { + if method.name.span == info.span { + matches.push(method); + } + } + } + _ => {} + } + } + match matches.as_slice() { + [function] => Some(*function), + _ => None, + } +} + +/// Locate actual declaration storage, excluding pattern and iteration binders. +/// The immutable module remains the authority; this lookup grants no execution. +pub(crate) fn variable_declaration<'a>( + program: &'a CheckedResourceProgram, + span: jett_common::Span, +) -> Option<&'a VarDecl> { + let contains = |outer: jett_common::Span| { + outer.file == span.file && outer.start <= span.start && outer.end >= span.end + }; + let mut matches = Vec::new(); + let mut blocks = Vec::new(); + let mut expressions = Vec::new(); + for item in &program.module().items { + match item { + Item::Function(function) => blocks.push(&function.body), + Item::Implement(block) => { + blocks.extend(block.methods.iter().map(|method| &method.body)); + } + Item::Struct(structure) => { + blocks.extend(structure.methods.iter().map(|method| &method.body)); + } + Item::Verify(block) => blocks.push(&block.body), + Item::Property(block) => blocks.push(&block.body), + Item::VarDecl(declaration) => { + if declaration.name.span == span { + matches.push(declaration); + } + expressions.push(&declaration.value); + } + Item::Actor(actor) => { + for declaration in &actor.state_fields { + if declaration.name.span == span { + matches.push(declaration); + } + expressions.push(&declaration.value); + } + blocks.extend(actor.handlers.iter().map(|handler| &handler.body)); + } + Item::TypeAlias(alias) => expressions.extend(alias.constraint.iter()), + Item::Namespace(_) + | Item::Mutual(_) + | Item::Interface(_) + | Item::Bitfield(_) + | Item::Enum(_) + | Item::Machine(_) + | Item::Resource(_) => {} + } + } + let mut declaration = |statement: &'a Stmt| { + if let Stmt::VarDecl(value) = statement { + if value.name.span == span { + matches.push(value); + } + } + }; + for block in blocks.into_iter().filter(|block| contains(block.span)) { + walk_block_nodes(block, &mut |_| {}, &mut |_| {}, &mut declaration); + } + for expression in expressions + .into_iter() + .filter(|expression| contains(expression.span())) + { + walk_expr_nodes(expression, &mut |_| {}, &mut |_| {}, &mut declaration); + } + match matches.as_slice() { + [declaration] => Some(*declaration), + _ => None, + } +} + +pub(super) fn closure_body<'a>( + program: &'a CheckedResourceProgram, + span: jett_common::Span, +) -> Option<&'a Block> { + match closure_node(program, span)? { + Expr::InlineFn(_, _, body, _) => Some(body), + _ => None, + } +} + +pub(crate) fn closure_node<'a>( + program: &'a CheckedResourceProgram, + span: jett_common::Span, +) -> Option<&'a Expr> { + let mut matches = Vec::new(); + let mut expression = |value: &'a Expr| { + if let Expr::InlineFn(_, _, body, _) = value { + if body.span == span { + matches.push(value); + } + } + }; + for item in &program.module().items { + match item { + Item::Function(function) => { + walk_block_nodes(&function.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::Implement(block) => { + for method in &block.methods { + walk_block_nodes(&method.body, &mut |_| {}, &mut expression, &mut |_| {}); + } + } + Item::Struct(structure) => { + for method in &structure.methods { + walk_block_nodes(&method.body, &mut |_| {}, &mut expression, &mut |_| {}); + } + } + Item::Verify(block) => { + walk_block_nodes(&block.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::Property(block) => { + walk_block_nodes(&block.body, &mut |_| {}, &mut expression, &mut |_| {}) + } + Item::VarDecl(declaration) => walk_expr_nodes( + &declaration.value, + &mut |_| {}, + &mut expression, + &mut |_| {}, + ), + Item::Actor(actor) => { + for state in &actor.state_fields { + walk_expr_nodes(&state.value, &mut |_| {}, &mut expression, &mut |_| {}); + } + for handler in &actor.handlers { + walk_block_nodes(&handler.body, &mut |_| {}, &mut expression, &mut |_| {}); + } + } + _ => {} + } + } + match matches.as_slice() { + [node] => Some(*node), + _ => None, + } +} + +pub(crate) fn closure_node_in_body<'a>( + body: &'a Block, + span: jett_common::Span, +) -> Option<&'a Expr> { + let mut matches = Vec::new(); + walk_block_nodes( + body, + &mut |_| {}, + &mut |value| { + if let Expr::InlineFn(_, _, body, _) = value { + if body.span == span { + matches.push(value); + } + } + }, + &mut |_| {}, + ); + match matches.as_slice() { + [node] => Some(*node), + _ => None, + } +} + +/// A negative result proves this body cannot directly construct an inline value. +/// Required materialization and scoped type binding remain conservative positives; +/// a dynamic factory call installs its own origin at its separate call boundary. +pub(crate) fn body_may_mint_inline_function(body: &Block) -> bool { + let mut found_expression = false; + let mut found_binding = false; + walk_block_nodes( + body, + &mut |_| {}, + &mut |expression| { + found_expression |= matches!( + expression, + Expr::InlineFn(..) | Expr::Comptime(..) | Expr::Error(..) + ); + }, + &mut |statement| { + found_binding |= matches!(statement, Stmt::ComptimeTypeBind(_)); + }, + ); + found_expression || found_binding +} + +fn walk_block_nodes<'a>( + block: &'a Block, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + for node in &block.stmts { + statement(node); + match node { + Stmt::VarDecl(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::Assign(value) => { + walk_expr_nodes(&value.target, binding, expression, statement); + walk_expr_nodes(&value.value, binding, expression, statement); + } + Stmt::Return(value) => { + if let Some(value) = &value.value { + walk_expr_nodes(value, binding, expression, statement); + } + } + Stmt::Respond(value) => walk_expr_nodes(&value.value, binding, expression, statement), + Stmt::ComptimeTypeBind(value) => { + binding(value); + walk_expr_nodes(&value.value, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::If(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.then_block, binding, expression, statement); + for (condition, block) in &value.else_ifs { + walk_expr_nodes(condition, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + if let Some(block) = &value.else_block { + walk_block_nodes(block, binding, expression, statement); + } + } + Stmt::For(value) => { + walk_expr_nodes(&value.iterable, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::While(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + walk_block_nodes(&value.body, binding, expression, statement); + } + Stmt::Match(value) => { + walk_expr_nodes(&value.expr, binding, expression, statement); + for arm in &value.arms { + walk_block_nodes(&arm.body, binding, expression, statement); + } + } + Stmt::Expr(value) => walk_expr_nodes(&value.expr, binding, expression, statement), + Stmt::Assert(value) => { + walk_expr_nodes(&value.condition, binding, expression, statement); + if let Some(message) = &value.message { + walk_expr_nodes(message, binding, expression, statement); + } + } + Stmt::Breakpoint(value) => { + if let Some(condition) = &value.condition { + walk_expr_nodes(condition, binding, expression, statement); + } + } + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } + } +} + +fn walk_expr_nodes<'a>( + value: &'a Expr, + binding: &mut impl FnMut(&'a ComptimeTypeBindStmt), + expression: &mut impl FnMut(&'a Expr), + statement: &mut impl FnMut(&'a Stmt), +) { + expression(value); + match value { + Expr::Binary(left, _, right, _) => { + walk_expr_nodes(left, binding, expression, statement); + walk_expr_nodes(right, binding, expression, statement); + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Default(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => walk_expr_nodes(value, binding, expression, statement), + Expr::Call(callee, arguments, _) | Expr::GenericCall(callee, _, arguments, _) => { + walk_expr_nodes(callee, binding, expression, statement); + for argument in arguments { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + } + Expr::ListConstruct(values, _) => { + for value in values { + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::MapConstruct(values, _) => { + for (key, value) in values { + walk_expr_nodes(key, binding, expression, statement); + walk_expr_nodes(value, binding, expression, statement); + } + } + Expr::Handle(value, _, block, _) => { + walk_expr_nodes(value, binding, expression, statement); + walk_block_nodes(block, binding, expression, statement); + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + match part { + StringPart::Expr(value) => { + walk_expr_nodes(value, binding, expression, statement); + } + StringPart::Literal(_) => {} + } + } + } + Expr::Pipeline(value, steps, _) => { + walk_expr_nodes(value, binding, expression, statement); + for step in steps { + walk_expr_nodes(&step.function, binding, expression, statement); + for argument in &step.extra_args { + walk_expr_nodes(&argument.value, binding, expression, statement); + } + if let Some(handle) = &step.handle { + walk_block_nodes(&handle.body, binding, expression, statement); + } + } + } + Expr::InlineFn(_, _, body, _) => walk_block_nodes(body, binding, expression, statement), + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => {} + } +} diff --git a/crates/jett_comptime/src/verify.rs b/crates/jett_comptime/src/verify.rs index e0951a45..167c40f3 100644 --- a/crates/jett_comptime/src/verify.rs +++ b/crates/jett_comptime/src/verify.rs @@ -1,3 +1,4 @@ +use crate::checked_types::CheckedExpressionTypes; use jett_common::{FileId, Span}; use jett_diagnostics::Diagnostic; use jett_parser::ast::{ @@ -6,9 +7,10 @@ use jett_parser::ast::{ }; use jett_types::ReflectionMetadata; use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use crate::DebugEvent; use crate::interpreter::Interpreter; use crate::value::Value; @@ -52,7 +54,7 @@ fn update_current_namespace( // Default iteration count for property-based testing // --------------------------------------------------------------------------- -const PROPERTY_DEFAULT_ITERATIONS: usize = 100; +pub const PROPERTY_DEFAULT_ITERATIONS: usize = 100; // Backstop recursion whose generic arguments change on every expansion. const PROPERTY_GENERATION_MAX_DEPTH: usize = 32; const PROPERTY_GENERATION_MAX_TYPE_NODES: usize = 1024; @@ -188,6 +190,29 @@ pub struct VerifyResult { pub iterations: Option, /// If this was a property block, whether it is a property (true) or verify (false). pub is_property: bool, + /// Generation and actual block/trial events in execution order, including + /// the original failure. Shrinking and case-collection replay remain private. + pub debug_events: Vec, +} + +/// Inputs chosen by the existing property generator for one test iteration. +/// The compiler can embed these checked cases into a native property runner. +#[derive(Debug, Clone)] +pub struct PropertyCase { + pub property_span: Span, + pub iteration: usize, + pub arguments: Vec, +} + +#[path = "verify/checked_property_cases.rs"] +mod checked_property_cases; +pub use checked_property_cases::{ + CheckedPropertyCases, collect_checked_property_cases_with_checked_values, +}; + +struct VerificationRun { + results: Vec, + property_cases: Vec, } // --------------------------------------------------------------------------- @@ -203,7 +228,7 @@ pub struct VerifyResult { /// `assert` statements (kept for backward compatibility). pub fn run_verify_blocks(module: &Module) -> Vec { let results = run_verify_blocks_detailed(module); - verify_results_to_diagnostics(results) + verify_results_to_diagnostics(&results) } /// Run all verify blocks with checked reflection metadata from type checking. @@ -212,27 +237,87 @@ pub fn run_verify_blocks_with_metadata( metadata: Arc, ) -> Vec { let results = run_verify_blocks_detailed_with_metadata(module, Some(metadata)); - verify_results_to_diagnostics(results) + verify_results_to_diagnostics(&results) } /// Run all verify blocks with checked metadata and expression type facts from /// type checking. +/// Namespace constants require `run_verify_blocks_with_checked_values`, using +/// the values produced by the compiler's required evaluation phase. pub fn run_verify_blocks_with_metadata_and_expression_types( module: &Module, metadata: Arc, - expression_types: Arc>, + expression_types: Arc, + breakpoint_exclusions: Arc>>, ) -> Vec { let results = run_verify_blocks_detailed_with_metadata_and_expression_types( module, Some(metadata), Some(expression_types), + Some(breakpoint_exclusions), + ); + verify_results_to_diagnostics(&results) +} + +/// Execute checked tests using the same immutable values as the compiler handoff. +pub fn run_verify_blocks_with_checked_values( + module: &Module, + metadata: Arc, + expression_types: Arc, + breakpoint_exclusions: Arc>>, + values: Arc, +) -> Vec { + let results = run_verify_blocks_detailed_with_checked_values( + module, + metadata, + expression_types, + breakpoint_exclusions, + values, ); - verify_results_to_diagnostics(results) + verify_results_to_diagnostics(&results) +} + +pub fn run_verify_blocks_detailed_with_checked_values( + module: &Module, + metadata: Arc, + expression_types: Arc, + breakpoint_exclusions: Arc>>, + values: Arc, +) -> Vec { + run_verification( + module, + Some(metadata), + Some(expression_types), + Some(breakpoint_exclusions), + Some(values), + false, + ) + .results +} + +pub fn collect_property_cases_with_checked_values( + module: &Module, + metadata: Arc, + expression_types: Arc, + breakpoint_exclusions: Arc>>, + values: Arc, +) -> Vec { + run_verification( + module, + Some(metadata), + Some(expression_types), + Some(breakpoint_exclusions), + Some(values), + true, + ) + .property_cases } -fn verify_results_to_diagnostics(results: Vec) -> Vec { +/// Convert a captured verification pass to diagnostics without executing it again +/// or consuming its debug events. +pub fn verify_results_to_diagnostics(results: &[VerifyResult]) -> Vec { results - .into_iter() + .iter() .filter_map(|r| { if r.passed { None @@ -242,7 +327,7 @@ fn verify_results_to_diagnostics(results: Vec) -> Vec format!( "comptime verify failed in '{}': {}", r.name, - r.error.unwrap_or_default() + r.error.as_deref().unwrap_or_default() ), r.span, )) @@ -261,41 +346,158 @@ pub fn run_verify_blocks_detailed_with_metadata( module: &Module, metadata: Option>, ) -> Vec { - run_verify_blocks_detailed_with_metadata_and_expression_types(module, metadata, None) + run_verify_blocks_detailed_with_metadata_and_expression_types(module, metadata, None, None) } pub fn run_verify_blocks_detailed_with_metadata_and_expression_types( module: &Module, metadata: Option>, - expression_types: Option>>, + expression_types: Option>, + breakpoint_exclusions: Option>>>, ) -> Vec { + run_verification( + module, + metadata, + expression_types, + breakpoint_exclusions, + None, + false, + ) + .results +} + +/// Reuse the interpreter's deterministic property pools for native test input +/// generation. The source has already passed frontend verification before the +/// driver calls this, so generated cases are valid values of their `given` +/// types; the native runner still executes the property bodies itself. +pub fn collect_property_cases_with_metadata_and_expression_types( + module: &Module, + metadata: Arc, + expression_types: Arc, + breakpoint_exclusions: Arc>>, +) -> Vec { + run_verification( + module, + Some(metadata), + Some(expression_types), + Some(breakpoint_exclusions), + None, + true, + ) + .property_cases +} + +fn run_verification( + module: &Module, + metadata: Option>, + expression_types: Option>, + breakpoint_exclusions: Option>>>, + values: Option>, + collect_cases: bool, +) -> VerificationRun { + // A checked worker retains the same immutable AST rather than attaching + // DefId facts to a freshly cloned same-span module. + if expression_types + .as_ref() + .and_then(|types| types.resource_program.as_ref()) + .is_some_and(|program| !std::ptr::eq(module, program.module())) + { + return run_verify_blocks_detailed_inner( + module, + metadata, + expression_types, + breakpoint_exclusions, + values, + collect_cases, + ); + } let module_for_thread = module.clone(); let thread_metadata = metadata.clone(); let thread_expression_types = expression_types.clone(); + let thread_exclusions = breakpoint_exclusions.clone(); + let thread_values = values.clone(); match std::thread::Builder::new() .name("jett-verify".to_string()) .stack_size(VERIFY_STACK_SIZE) .spawn(move || { + let retained = thread_expression_types + .as_ref() + .and_then(|types| types.resource_program.clone()); + let worker_module = retained + .as_ref() + .map_or(&module_for_thread, |program| program.module()); run_verify_blocks_detailed_inner( - &module_for_thread, + worker_module, thread_metadata, thread_expression_types, + thread_exclusions, + thread_values, + collect_cases, ) }) { Ok(handle) => match handle.join() { Ok(results) => results, Err(payload) => std::panic::resume_unwind(payload), }, - Err(_) => run_verify_blocks_detailed_inner(module, metadata, expression_types), + Err(_) => run_verify_blocks_detailed_inner( + module, + metadata, + expression_types, + breakpoint_exclusions, + values, + collect_cases, + ), } } fn run_verify_blocks_detailed_inner( module: &Module, metadata: Option>, - expression_types: Option>>, -) -> Vec { + expression_types: Option>, + breakpoint_exclusions: Option>>>, + values: Option>, + collect_cases: bool, +) -> VerificationRun { + let resource_program = expression_types + .as_ref() + .and_then(|types| types.resource_program.clone()); + let setup_failure = |message: String| VerificationRun { + results: vec![VerifyResult { + name: "checked resource worker".to_string(), + span: module.span, + passed: false, + error: Some(message), + iterations: None, + is_property: false, + debug_events: Vec::new(), + }], + property_cases: Vec::new(), + }; + if resource_program + .as_ref() + .is_some_and(|program| !std::ptr::eq(module, program.module())) + { + return setup_failure( + "required worker received a foreign checked source module".to_string(), + ); + } let mut interp = Interpreter::new(); + if let Some(program) = resource_program { + if let Err(error) = interp + .install_checked_resource_program(program, crate::ExecutionPurpose::Verify) + .and_then(|()| { + interp.authorize_checked_resource_worker(module, crate::ExecutionPurpose::Verify) + }) + { + return setup_failure(error); + } + } + if let Some(values) = values { + interp.set_explicit_comptime_values(values); + } + if let Some(exclusions) = breakpoint_exclusions { + interp.set_breakpoint_exclusions(exclusions); + } if let Some(metadata) = metadata { interp.set_reflection_metadata(metadata); } @@ -303,13 +505,14 @@ fn run_verify_blocks_detailed_inner( interp.set_checked_expression_types(expression_types); } let mut results = Vec::new(); + let mut property_cases = Vec::new(); // First pass: register all functions and type aliases so verify blocks // can call them and use refinement types. interp.register_module(module); - let mut legacy_verify_functions: Vec<(Option, FunctionDef)> = Vec::new(); - let mut verify_blocks: Vec<(Option, VerifyBlock)> = Vec::new(); - let mut property_blocks: Vec<(Option, PropertyBlock)> = Vec::new(); + let mut legacy_verify_functions: Vec<(Option, &FunctionDef)> = Vec::new(); + let mut verify_blocks: Vec<(Option, &VerifyBlock)> = Vec::new(); + let mut property_blocks: Vec<(Option, &PropertyBlock)> = Vec::new(); let mut property_enums: Vec = Vec::new(); let mut property_structs: Vec = Vec::new(); let mut property_bitfields: Vec = Vec::new(); @@ -365,14 +568,14 @@ fn run_verify_blocks_detailed_inner( } Item::Function(func) => { if has_assert_stmts(func) && func.params.is_empty() && func.name.name != "main" { - legacy_verify_functions.push((current_namespace.clone(), func.clone())); + legacy_verify_functions.push((current_namespace.clone(), func)); } } Item::Verify(vb) => { - verify_blocks.push((current_namespace.clone(), vb.clone())); + verify_blocks.push((current_namespace.clone(), vb)); } Item::Property(pb) => { - property_blocks.push((current_namespace.clone(), pb.clone())); + property_blocks.push((current_namespace.clone(), pb)); } _ => {} } @@ -380,7 +583,7 @@ fn run_verify_blocks_detailed_inner( // Execute proper verify blocks. for (namespace, vb) in &verify_blocks { - match interp.exec_block_in_namespace(namespace.as_deref(), &vb.body) { + match interp.exec_checked_verify_region(namespace.as_deref(), vb) { Ok(_) => { results.push(VerifyResult { name: vb.name.name.clone(), @@ -389,6 +592,7 @@ fn run_verify_blocks_detailed_inner( error: None, iterations: None, is_property: false, + debug_events: interp.take_debug_events(), }); } Err(msg) => { @@ -399,14 +603,20 @@ fn run_verify_blocks_detailed_inner( error: Some(msg), iterations: None, is_property: false, + debug_events: interp.take_debug_events(), }); } } } - // Execute legacy verify functions (zero-arg functions with asserts). + // Zero-argument legacy verify entries carry no unvalidated host payload. + // Keep checked source facts available inside their bodies. for (namespace, func) in &legacy_verify_functions { - match interp.call_function_in_namespace(namespace.as_deref(), &func.name.name, vec![]) { + match interp.call_checked_function_in_namespace( + namespace.as_deref(), + &func.name.name, + vec![], + ) { Ok(_) => { results.push(VerifyResult { name: func.name.name.clone(), @@ -415,6 +625,7 @@ fn run_verify_blocks_detailed_inner( error: None, iterations: None, is_property: false, + debug_events: interp.take_debug_events(), }); } Err(msg) => { @@ -425,6 +636,7 @@ fn run_verify_blocks_detailed_inner( error: Some(msg), iterations: None, is_property: false, + debug_events: interp.take_debug_events(), }); } } @@ -436,15 +648,21 @@ fn run_verify_blocks_detailed_inner( &mut interp, namespace.as_deref(), pb, - &property_enums, - &property_structs, - &property_bitfields, - &property_type_aliases, + PropertyDefinitions { + enums: &property_enums, + structs: &property_structs, + bitfields: &property_bitfields, + aliases: &property_type_aliases, + }, + collect_cases.then_some(&mut property_cases), ); results.push(result); } - results + VerificationRun { + results, + property_cases, + } } /// Evaluate a single pure function at compile time with the given arguments. @@ -480,12 +698,34 @@ pub fn eval_assert( // Property-based testing // --------------------------------------------------------------------------- -/// Run a single property block for `PROPERTY_DEFAULT_ITERATIONS` iterations. -/// Each iteration generates random values for each `given` parameter, binds -/// them into the interpreter, and executes the body. -/// Generate candidate shrunk versions of a value (ordered from simplest to most complex). +/// Generate candidates in the property runner's established shrink order. fn shrink_value(value: &Value) -> Vec { match value { + Value::Typed { type_name, value } if narrow_integer_bounds(type_name).is_some() => { + let (min, max) = narrow_integer_bounds(type_name).unwrap(); + shrink_value(value) + .into_iter() + .filter(|candidate| matches!(candidate, Value::Int64(n) if (min..=max).contains(n))) + .map(|candidate| Value::Typed { + type_name: type_name.clone(), + value: Box::new(candidate), + }) + .collect() + } + Value::Typed { type_name, value } => shrink_value(value) + .into_iter() + .map(|candidate| { + if type_name == "float32" { + if let Value::Float64(number) = candidate { + return property_float32_value(number); + } + } + Value::Typed { + type_name: type_name.clone(), + value: Box::new(candidate), + } + }) + .collect(), Value::Int64(n) => { let mut candidates = Vec::new(); if *n != 0 { @@ -659,13 +899,18 @@ fn shrink_value(value: &Value) -> Vec { } candidates } - Value::Struct { type_name, fields } if !fields.is_empty() => { + Value::Struct { + concrete_type, + type_name, + fields, + } if !fields.is_empty() => { let mut candidates = Vec::new(); for (i, (_, field)) in fields.iter().enumerate() { for shrunk_field in shrink_value(field) { let mut new_fields = fields.clone(); new_fields[i].1 = shrunk_field; candidates.push(Value::Struct { + concrete_type: concrete_type.clone(), type_name: type_name.clone(), fields: new_fields, }); @@ -695,46 +940,140 @@ fn shrink_inputs( pb: &PropertyBlock, failing: Vec, ) -> Vec { - let mut current = failing; + let result = shrink_property_inputs(failing, |attempt| { + if !attempt + .iter() + .all(|value| valid_shrink_refinements(interp, value)) + { + return Ok::<_, std::convert::Infallible>(false); + } + interp.push_scope_public(); + for (given, value) in pb.givens.iter().zip(attempt.iter()) { + interp.set_variable_public(&given.name.name, value.clone()); + } + let result = interp.exec_block_in_namespace(namespace, &pb.body); + interp.pop_scope_public(); + Ok::<_, std::convert::Infallible>(result.is_err()) + }); + match result { + Ok(values) => values, + Err(never) => match never {}, + } +} + +fn valid_shrink_refinements(interp: &mut Interpreter, value: &Value) -> bool { + match value { + Value::Typed { type_name, value } => { + valid_shrink_refinements(interp, value) + && interp.check_refinement_type(type_name, value).is_ok() + } + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => values + .iter() + .all(|value| valid_shrink_refinements(interp, value)), + Value::Struct { fields, .. } => fields + .iter() + .all(|(_, value)| valid_shrink_refinements(interp, value)), + Value::Map(entries) => entries.iter().all(|(key, value)| { + valid_shrink_refinements(interp, key) && valid_shrink_refinements(interp, value) + }), + Value::OptionalSome(value) + | Value::ResultOk(value) + | Value::ResultFail(value) + | Value::Pending(value) => valid_shrink_refinements(interp, value), + _ => true, + } +} +/// Apply the interpreter's ordered, bounded shrink search using a supplied +/// execution backend. `Ok(true)` accepts a failing candidate; backend errors +/// abort the search and must not be treated as property counterexamples. +pub fn shrink_property_inputs( + failing: Vec, + mut fails: impl FnMut(&[Value]) -> Result, +) -> Result, E> { + let mut current = failing; 'outer: for _ in 0..SHRINK_MAX_STEPS { - // Try to shrink each input one at a time. for i in 0..current.len() { - let candidates = shrink_value(¤t[i]); - for candidate in candidates { + for candidate in shrink_value(¤t[i]) { let mut attempt = current.clone(); attempt[i] = candidate; - - // Run the property with the candidate inputs. - interp.push_scope_public(); - for (given, value) in pb.givens.iter().zip(attempt.iter()) { - interp.set_variable_public(&given.name.name, value.clone()); - } - let result = interp.exec_block_in_namespace(namespace, &pb.body); - interp.pop_scope_public(); - - if result.is_err() { - // Still fails — use the simpler version. + if fails(&attempt)? { current = attempt; continue 'outer; } } } - // No further shrinking possible. break; } + Ok(current) +} - current +struct PropertyDefinitions<'a> { + enums: &'a [PropertyEnumDef], + structs: &'a [PropertyStructDef], + bitfields: &'a [PropertyBitfieldDef], + aliases: &'a [PropertyTypeAliasDef], +} + +fn with_property_execution_purpose( + interp: &mut Interpreter, + body: impl FnOnce(&mut Interpreter) -> T, +) -> T { + let previous_purpose = interp.checked_execution_purpose(crate::ExecutionPurpose::Property); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(interp))); + if let Some(purpose) = previous_purpose { + interp.checked_execution_purpose(purpose); + } + match result { + Ok(result) => result, + Err(panic) => std::panic::resume_unwind(panic), + } } +#[cfg(test)] +#[path = "verify/property_purpose_tests.rs"] +mod property_purpose_tests; + fn run_property_block( interp: &mut Interpreter, namespace: Option<&str>, pb: &PropertyBlock, - enum_defs: &[PropertyEnumDef], - struct_defs: &[PropertyStructDef], - bitfield_defs: &[PropertyBitfieldDef], - type_alias_defs: &[PropertyTypeAliasDef], + definitions: PropertyDefinitions<'_>, + collected_cases: Option<&mut Vec>, +) -> VerifyResult { + with_property_execution_purpose(interp, |interp| { + match interp.with_checked_property_region(pb, namespace, |interp| { + Ok(run_property_block_inner( + interp, + namespace, + pb, + definitions, + collected_cases, + )) + }) { + Ok(result) => result, + Err(error) => VerifyResult { + name: pb.name.name.clone(), + span: pb.name.span, + passed: false, + error: Some(error), + iterations: Some(0), + is_property: true, + debug_events: interp.take_debug_events(), + }, + } + }) +} + +fn run_property_block_inner( + interp: &mut Interpreter, + namespace: Option<&str>, + pb: &PropertyBlock, + definitions: PropertyDefinitions<'_>, + mut collected_cases: Option<&mut Vec>, ) -> VerifyResult { let iterations = PROPERTY_DEFAULT_ITERATIONS; @@ -747,10 +1086,10 @@ fn run_property_block( interp, &g.ty, namespace, - enum_defs, - struct_defs, - bitfield_defs, - type_alias_defs, + definitions.enums, + definitions.structs, + definitions.bitfields, + definitions.aliases, ) }) .collect(); @@ -768,6 +1107,7 @@ fn run_property_block( )), iterations: Some(0), is_property: true, + debug_events: interp.take_debug_events(), }; } } @@ -784,6 +1124,14 @@ fn run_property_block( }) .collect(); + if let Some(cases) = collected_cases.as_deref_mut() { + cases.push(PropertyCase { + property_span: pb.span, + iteration, + arguments: chosen.iter().map(|(_, value)| value.clone()).collect(), + }); + } + // Push a scope, bind the given values, execute the body. interp.push_scope_public(); for (given, value) in &chosen { @@ -796,7 +1144,11 @@ fn run_property_block( if let Err(msg) = exec_result { // Shrink the failing inputs to find a simpler counterexample. let failing_values: Vec = chosen.iter().map(|(_, v)| v.clone()).collect(); + // Preserve generation and original trials before replay. Refinement + // checks inside shrinking are replay observations too. + let debug_events = interp.take_debug_events(); let shrunk = shrink_inputs(interp, namespace, pb, failing_values); + let _ = interp.take_debug_events(); let input_desc: Vec = pb .givens @@ -815,6 +1167,7 @@ fn run_property_block( )), iterations: Some(iteration + 1), is_property: true, + debug_events, }; } } @@ -826,6 +1179,7 @@ fn run_property_block( error: None, iterations: Some(iterations), is_property: true, + debug_events: interp.take_debug_events(), } } @@ -836,6 +1190,34 @@ fn generate_values_for_type(ty: &TypeExpr) -> Vec { generate_values_for_type_in_namespace(&mut interp, ty, None, &[], &[], &[], &[]) } +fn narrow_integer_bounds(type_name: &str) -> Option<(i64, i64)> { + match type_name { + "int8" => Some((i8::MIN.into(), i8::MAX.into())), + "int16" => Some((i16::MIN.into(), i16::MAX.into())), + "int32" => Some((i32::MIN.into(), i32::MAX.into())), + "uint8" => Some((0, u8::MAX.into())), + "uint16" => Some((0, u16::MAX.into())), + "uint32" => Some((0, u32::MAX.into())), + _ => None, + } +} + +fn generate_narrow_integer_values(type_name: &str) -> Vec { + let (min, max) = narrow_integer_bounds(type_name).expect("a checked narrow integer primitive"); + let values = if min < 0 { + generate_signed_integer_values(min, max) + } else { + generate_unsigned_integer_values(max) + }; + values + .into_iter() + .map(|value| Value::Typed { + type_name: type_name.to_owned(), + value: Box::new(value), + }) + .collect() +} + fn generate_signed_integer_values(min: i64, max: i64) -> Vec { unique_values( [0, 1, -1, 42, -42, 100, max, min] @@ -868,14 +1250,21 @@ fn generate_uint64_values() -> Vec { ]) } -fn generate_float_values() -> Vec { - vec![ - Value::Float64(0.0), - Value::Float64(1.0), - Value::Float64(-1.0), - Value::Float64(3.14), - Value::Float64(-0.0), - ] +fn generate_float_values(type_name: &str) -> Vec { + [0.0, 1.0, -1.0, 3.14, -0.0] + .into_iter() + .map(|number| match type_name { + "float32" => property_float32_value(number), + _ => Value::Float64(number), + }) + .collect() +} + +fn property_float32_value(number: f64) -> Value { + Value::Typed { + type_name: "float32".to_owned(), + value: Box::new(Value::Float64(f64::from(number as f32))), + } } fn generate_values_for_type_in_namespace( @@ -904,13 +1293,10 @@ fn generate_values_for_type_in_namespace( match ty { TypeExpr::Named(ident) => match ident.name.as_str() { - "int8" => generate_signed_integer_values(i8::MIN as i64, i8::MAX as i64), - "int16" => generate_signed_integer_values(i16::MIN as i64, i16::MAX as i64), - "int32" => generate_signed_integer_values(i32::MIN as i64, i32::MAX as i64), + "int8" | "int16" | "int32" | "uint8" | "uint16" | "uint32" => { + generate_narrow_integer_values(&ident.name) + } "int64" => generate_signed_integer_values(i64::MIN, i64::MAX), - "uint8" => generate_unsigned_integer_values(u8::MAX as i64), - "uint16" => generate_unsigned_integer_values(u16::MAX as i64), - "uint32" => generate_unsigned_integer_values(u32::MAX as i64), "uint64" => generate_uint64_values(), "string" => vec![ Value::String(String::new()), @@ -920,7 +1306,7 @@ fn generate_values_for_type_in_namespace( Value::String("123".to_string()), ], "bool" => vec![Value::Bool(true), Value::Bool(false)], - "float32" | "float64" => generate_float_values(), + "float32" | "float64" => generate_float_values(&ident.name), "bytes" => vec![ Value::Bytes(Vec::new()), Value::Bytes(vec![0]), @@ -1440,6 +1826,10 @@ fn generate_type_alias_values( .check_refinement_type(&alias_def.type_name, value) .is_ok() }) + .map(|value| Value::Typed { + type_name: alias_def.type_name.clone(), + value: Box::new(value), + }) .collect() } @@ -1628,6 +2018,7 @@ fn generate_struct_values( if struct_def.def.fields.is_empty() { return vec![Value::Struct { + concrete_type: None, type_name: struct_def.type_name.clone(), fields: vec![], }]; @@ -1662,6 +2053,7 @@ fn generate_struct_values( }) .collect(); values.push(Value::Struct { + concrete_type: None, type_name: struct_def.type_name.clone(), fields, }); @@ -1726,6 +2118,13 @@ fn generate_generic_struct_values( bitfield_defs: &[PropertyBitfieldDef], type_alias_defs: &[PropertyTypeAliasDef], ) -> Vec { + let arguments = struct_def + .def + .type_params + .iter() + .map(|param| interp.concrete_type_display(&substitutions[¶m.name])) + .collect::>(); + let concrete_type = format!("{}[{}]", struct_def.type_name, arguments.join(", ")); let field_namespace = struct_def.namespace.as_deref(); let substituted_fields: Vec = struct_def .def @@ -1741,6 +2140,7 @@ fn generate_generic_struct_values( if substituted_fields.is_empty() { return vec![Value::Struct { + concrete_type: Some(concrete_type), type_name: struct_def.type_name.clone(), fields: vec![], }]; @@ -1773,6 +2173,7 @@ fn generate_generic_struct_values( }) .collect(); values.push(Value::Struct { + concrete_type: Some(concrete_type.clone()), type_name: struct_def.type_name.clone(), fields, }); @@ -1867,6 +2268,7 @@ fn generate_bitfield_values( ) -> Vec { if bitfield_def.def.fields.is_empty() { return vec![Value::Struct { + concrete_type: None, type_name: bitfield_def.type_name.clone(), fields: vec![], }]; @@ -1901,6 +2303,7 @@ fn generate_bitfield_values( }) .collect(); values.push(Value::Struct { + concrete_type: None, type_name: bitfield_def.type_name.clone(), fields, }); @@ -2209,6 +2612,188 @@ mod tests { use super::*; + fn debug_capture_module(source: &str) -> Module { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + parsed.module + } + + #[test] + fn verify_capture_retains_failed_block_events_and_isolates_following_blocks() { + let module = debug_capture_module( + r#" +verify first: + print("trace public") + assert false "first failure" +verify second: + println("second") + assert true +"#, + ); + let results = run_verify_blocks_detailed(&module); + assert_eq!(results.len(), 2); + assert!(!results[0].passed); + assert!(results[1].passed); + assert_eq!( + results[0].debug_events, + vec![crate::DebugEvent { + kind: crate::DebugEventKind::Print, + text: "trace public".into(), + }] + ); + assert_eq!( + results[1].debug_events, + vec![crate::DebugEvent { + kind: crate::DebugEventKind::Println, + text: "second\n".into(), + }] + ); + let diagnostics = verify_results_to_diagnostics(&results); + assert_eq!(diagnostics.len(), 1); + assert_eq!( + results[0].debug_events.len(), + 1, + "diagnostic conversion does not consume capture" + ); + } + + #[test] + fn property_capture_keeps_generation_and_original_failure_while_shrinking_stays_private() { + let module = debug_capture_module( + r#" +function positive(value: int64) returns bool: + println("pool", value) + return value > 0 +type Positive = int64 where positive(value) +property failure: + given number: Positive + println("trial", coarsen clone number) + assert false "trial failure" +"#, + ); + let results = run_verify_blocks_detailed(&module); + assert_eq!(results.len(), 1); + assert!(!results[0].passed); + assert_eq!(results[0].iterations, Some(1)); + let expected = vec![ + "pool 0\n", + "pool 1\n", + "pool -1\n", + "pool 42\n", + "pool -42\n", + "pool 100\n", + "pool 9223372036854775807\n", + "pool -9223372036854775808\n", + "trial 1\n", + ]; + assert_eq!( + results[0] + .debug_events + .iter() + .map(|e| e.text.as_str()) + .collect::>(), + expected + ); + assert!( + results[0] + .debug_events + .iter() + .all(|e| e.kind == crate::DebugEventKind::Println) + ); + assert!( + results[0] + .error + .as_deref() + .unwrap() + .contains("counterexample: number = 1") + ); + } + + #[test] + fn empty_property_pool_retains_failed_generation_events_without_trial_events() { + let module = debug_capture_module( + r#" +function rejected(value: int64) returns bool: + print("generation") + return false +type Impossible = int64 where rejected(value) +property empty: + given number: Impossible + println("unreachable trial") + assert true +"#, + ); + let results = run_verify_blocks_detailed(&module); + assert_eq!(results.len(), 1); + assert!(!results[0].passed); + assert_eq!(results[0].iterations, Some(0)); + assert_eq!(results[0].debug_events.len(), 8); + assert!( + results[0] + .debug_events + .iter() + .all(|event| event.kind == crate::DebugEventKind::Print + && event.text == "generation") + ); + } + + #[test] + fn passing_property_capture_retains_all_hundred_actual_trials_in_order() { + let module = debug_capture_module( + r#" +property order: + given number: int64 + println(number) + assert number == number +"#, + ); + let results = run_verify_blocks_detailed(&module); + assert_eq!(results.len(), 1); + assert!(results[0].passed); + assert_eq!(results[0].iterations, Some(100)); + assert_eq!(results[0].debug_events.len(), 100); + let prefix = [ + "0\n", + "1\n", + "-1\n", + "42\n", + "-42\n", + "100\n", + "9223372036854775807\n", + "-9223372036854775808\n", + "0\n", + ]; + assert_eq!( + results[0].debug_events[..9] + .iter() + .map(|e| e.text.as_str()) + .collect::>(), + prefix + ); + // Native-input collection runs its own verification pass. Its observations + // stay private rather than being appended to this already captured pass. + let replay = run_verification(&module, None, None, None, None, true); + assert_eq!(replay.property_cases.len(), 100); + assert_eq!(results[0].debug_events.len(), 100); + } + + #[test] + fn native_property_cases_reuse_the_interpreter_pool_and_iteration_order() { + let parsed = jett_parser::parse( + "namespace app\nproperty identity:\n given n: int64\n assert n == n\n", + FileId::new(0), + ); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let run = run_verification(&parsed.module, None, None, None, None, true); + assert_eq!(run.results.len(), 1); + assert!(run.results[0].passed); + assert_eq!(run.property_cases.len(), PROPERTY_DEFAULT_ITERATIONS); + assert_eq!(run.property_cases[0].iteration, 0); + assert_eq!(run.property_cases[0].arguments, vec![Value::Int64(0)]); + assert_eq!(run.property_cases[1].arguments, vec![Value::Int64(1)]); + assert_eq!(run.property_cases[8].arguments, vec![Value::Int64(0)]); + } + fn sp() -> Span { Span::new(FileId::new(0), 0, 0) } @@ -3362,26 +3947,34 @@ mod tests { fn property_generator_supports_sized_numeric_primitives() { let int8_values = generate_values_for_type(&type_named("int8")); assert!(!int8_values.is_empty()); + assert!(int8_values.iter().all(|value| matches!(value, + Value::Typed { type_name, .. } if type_name == "int8"))); assert!(int8_values.iter().all(|value| { - matches!(value, Value::Int64(n) if (i8::MIN as i64..=i8::MAX as i64).contains(n)) + matches!(value.payload(), Value::Int64(n) if (i8::MIN as i64..=i8::MAX as i64).contains(n)) })); let int16_values = generate_values_for_type(&type_named("int16")); assert!(!int16_values.is_empty()); + assert!(int16_values.iter().all(|value| matches!(value, + Value::Typed { type_name, .. } if type_name == "int16"))); assert!(int16_values.iter().all(|value| { - matches!(value, Value::Int64(n) if (i16::MIN as i64..=i16::MAX as i64).contains(n)) + matches!(value.payload(), Value::Int64(n) if (i16::MIN as i64..=i16::MAX as i64).contains(n)) })); let int32_values = generate_values_for_type(&type_named("int32")); assert!(!int32_values.is_empty()); + assert!(int32_values.iter().all(|value| matches!(value, + Value::Typed { type_name, .. } if type_name == "int32"))); assert!(int32_values.iter().all(|value| { - matches!(value, Value::Int64(n) if (i32::MIN as i64..=i32::MAX as i64).contains(n)) + matches!(value.payload(), Value::Int64(n) if (i32::MIN as i64..=i32::MAX as i64).contains(n)) })); let uint32_values = generate_values_for_type(&type_named("uint32")); assert!(!uint32_values.is_empty()); + assert!(uint32_values.iter().all(|value| matches!(value, + Value::Typed { type_name, .. } if type_name == "uint32"))); assert!(uint32_values.iter().all(|value| { - matches!(value, Value::Int64(n) if (0..=u32::MAX as i64).contains(n)) + matches!(value.payload(), Value::Int64(n) if (0..=u32::MAX as i64).contains(n)) })); let uint64_values = generate_values_for_type(&type_named("uint64")); @@ -3402,7 +3995,8 @@ mod tests { assert!( float32_values .iter() - .all(|value| matches!(value, Value::Float64(_))) + .all(|value| matches!(value, Value::Typed { type_name, value } + if type_name == "float32" && matches!(value.as_ref(), Value::Float64(_)))) ); } @@ -3478,7 +4072,7 @@ mod tests { values.iter().any(|value| { matches!( value, - Value::Struct { type_name, fields } + Value::Struct { concrete_type: _, type_name, fields } if type_name == "app.PropertyUser" && fields.iter().any(|(name, value)| { name == "name" && matches!(value, Value::String(_)) @@ -3746,7 +4340,7 @@ mod tests { values.iter().any(|value| { matches!( value, - Value::Struct { type_name, fields } + Value::Struct { concrete_type: _, type_name, fields } if type_name == "app.Header" && fields.iter().any(|(name, value)| { name == "version" @@ -3767,7 +4361,7 @@ mod tests { Value::List(items) if !items.is_empty() && items.iter().all(|item| { - matches!(item, Value::Int64(n) if (0..=255).contains(n)) + matches!(item, Value::Typed { type_name, value } if type_name == "uint8" && matches!(value.as_ref(), Value::Int64(n) if (0..=255).contains(n))) }) ) }) @@ -3827,7 +4421,7 @@ mod tests { assert!( refined_values .iter() - .all(|value| matches!(value, Value::Int64(n) if *n > 0)), + .all(|value| matches!(value, Value::Typed { type_name, value } if type_name == "app.Positive" && matches!(value.payload(), Value::Int64(n) if *n > 0))), "expected refinement generation to keep only values accepted by the constraint" ); } @@ -3863,7 +4457,7 @@ mod tests { matches!( value, Value::Struct { fields, .. } - if matches!(fields.as_slice(), [(_, Value::Int64(n))] if *n > 0) + if matches!(fields.as_slice(), [(_, Value::Typed { type_name, value })] if type_name == "app.Positive" && matches!(value.payload(), Value::Int64(n) if *n > 0)) ) }), "expected refined struct fields to be generated through refinement filtering" @@ -3892,7 +4486,7 @@ mod tests { values.iter().any(|value| { matches!( value, - Value::Struct { type_name, fields } + Value::Struct { concrete_type: _, type_name, fields } if type_name == "app.Box" && matches!(fields.as_slice(), [(_, Value::Int64(_))]) ) @@ -3932,13 +4526,293 @@ mod tests { matches!( value, Value::Struct { fields, .. } - if matches!(fields.as_slice(), [(_, Value::Int64(n))] if *n > 0) + if matches!(fields.as_slice(), [(_, Value::Typed { type_name, value })] if type_name == "app.Positive" && matches!(value.payload(), Value::Int64(n) if *n > 0)) ) }), "expected generic struct type arguments to resolve in the use-site namespace" ); } + #[test] + fn property_generator_retains_generic_arguments_in_empty_structs() { + let mut interp = Interpreter::new(); + let definitions = vec![PropertyStructDef { + type_name: "app.Empty".to_owned(), + namespace: Some("app".to_owned()), + def: generic_struct_def("Empty", vec!["First", "Second"], vec![]), + }]; + let values = generate_values_for_type_in_namespace( + &mut interp, + &type_generic("Empty", vec![type_named("string"), type_named("int8")]), + Some("app"), + &[], + &definitions, + &[], + &[], + ); + assert!( + matches!(values.as_slice(), [Value::Struct { concrete_type, fields, .. }] + if concrete_type.as_deref() == Some("app.Empty[string, int8]") && fields.is_empty()) + ); + } + + #[test] + fn property_generated_generic_owners_survive_erasure_and_shrinking() { + let source = include_str!("../../../tests/native/property_generic_owners.jett"); + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty()); + let results = run_verify_blocks_detailed(&parsed.module); + assert!(results.iter().all(|result| result.passed), "{results:?}"); + let parsed = jett_parser::parse(&source.replace("<= 3", "<= 1"), FileId::new(0)); + let results = run_verify_blocks_detailed(&parsed.module); + assert!(results[0].passed); + assert_eq!(results[1].iterations, Some(3)); + assert_eq!( + results[1].error.as_deref(), + Some( + "length sentinel (counterexample: records = list(app.Boxed(value: 0), app.Boxed(value: 0)), texts = list())" + ) + ); + } + + #[test] + fn property_float32_inputs_and_shrink_candidates_keep_binary32_precision() { + let pool = generate_values_for_type(&type_named("float32")); + assert_eq!(*pool[3].payload(), Value::Float64(f64::from(3.14_f32))); + assert_ne!(*pool[3].payload(), Value::Float64(3.14)); + assert!(matches!(pool[4].payload(), Value::Float64(number) + if number.to_bits() == (-0.0_f64).to_bits())); + for number in [3.14_f32, f32::MAX, f32::MIN, f32::from_bits(3)] { + let value = property_float32_value(f64::from(number)); + for candidate in shrink_value(&value) { + let Value::Typed { type_name, value } = candidate else { + panic!("lost binary32 type identity"); + }; + assert_eq!(type_name, "float32"); + let Value::Float64(number) = *value else { + panic!("lost float payload"); + }; + assert_eq!(number.to_bits(), f64::from(number as f32).to_bits()); + } + } + let parsed = jett_parser::parse( + "property domain:\n given number: float32\n assert number <= 3.0 \"float32 sentinel\"\n", + FileId::new(0), + ); + assert!(parsed.errors.is_empty()); + let results = run_verify_blocks_detailed(&parsed.module); + assert_eq!(results[0].iterations, Some(4)); + assert_eq!( + results[0].error.as_deref(), + Some("float32 sentinel (counterexample: number = 3.140000104904175)"), + ); + assert_eq!( + generate_values_for_type(&type_named("float64"))[3], + Value::Float64(3.14), + ); + } + + #[test] + fn property_shrinker_does_not_report_an_invalid_refinement_counterexample() { + let parsed = jett_parser::parse( + "type Positive = int64 where value > 0\nproperty domain:\n given number: Positive\n assert false \"refinement sentinel\"\n", + FileId::new(0), + ); + assert!(parsed.errors.is_empty()); + let results = run_verify_blocks_detailed(&parsed.module); + assert_eq!(results.len(), 1); + assert_eq!(results[0].iterations, Some(1)); + assert_eq!( + results[0].error.as_deref(), + Some("refinement sentinel (counterexample: number = 1)") + ); + } + + #[test] + fn property_shrinker_checks_refined_payloads_in_every_aggregate_shape() { + let mut interp = Interpreter::new(); + let constraint = binary(var("value"), BinOp::Gt, int(0)); + interp.register_type_alias_in_namespace( + Some("app"), + &type_alias("Positive", "int64", Some(constraint)), + ); + let wrap = |number| Value::Typed { + type_name: "app.Positive".to_owned(), + value: Box::new(Value::Int64(number)), + }; + for number in [0, 1] { + let value = wrap(number); + let shapes = [ + value.clone(), + Value::List(vec![value.clone()]), + Value::Set(vec![value.clone()]), + Value::Map(vec![(value.clone(), wrap(1))]), + Value::Map(vec![(wrap(1), value.clone())]), + Value::OptionalSome(Box::new(value.clone())), + Value::ResultOk(Box::new(value.clone())), + Value::ResultFail(Box::new(value.clone())), + Value::Pending(Box::new(value.clone())), + Value::Enum { + type_name: "Choice".to_owned(), + variant: "number".to_owned(), + fields: vec![value.clone()], + }, + Value::Machine { + type_name: "State".to_owned(), + state: "ready".to_owned(), + fields: vec![value.clone()], + }, + Value::Struct { + concrete_type: None, + type_name: "Box[app.Positive]".to_owned(), + fields: vec![("value".to_owned(), value)], + }, + ]; + for shape in shapes { + assert_eq!( + valid_shrink_refinements(&mut interp, &shape), + number > 0, + "{shape:?}" + ); + } + } + let shrunk = shrink_property_inputs(vec![wrap(42)], |values| { + Ok::<_, ()>( + values + .iter() + .all(|value| valid_shrink_refinements(&mut interp, value)), + ) + }) + .unwrap(); + assert_eq!( + shrunk, + vec![wrap(1)], + "refined values must still shrink within their domain" + ); + } + + #[test] + fn property_shrinker_keeps_narrow_integer_domains_at_every_boundary() { + for name in ["int8", "int16", "int32", "uint8", "uint16", "uint32"] { + let (min, max) = narrow_integer_bounds(name).unwrap(); + for number in [min, max] { + let input = Value::Typed { + type_name: name.to_owned(), + value: Box::new(Value::Int64(number)), + }; + let candidates = shrink_value(&input); + assert!(!candidates.is_empty() || number == 0, "{name}: {number}"); + for candidate in candidates { + assert!(matches!(candidate, Value::Typed { type_name, value } + if type_name == name && matches!(value.as_ref(), Value::Int64(n) if (min..=max).contains(n)))); + } + } + } + // The same magnitude is a valid int64 candidate; do not suppress it + // globally just to make narrow native literals compile. + assert!(shrink_value(&Value::Int64(-128)).contains(&Value::Int64(128))); + } + + #[test] + fn property_shrinker_retains_integer_domains_inside_aggregate_candidates() { + let narrow = Value::Typed { + type_name: "int8".to_owned(), + value: Box::new(Value::Int64(-128)), + }; + let aggregates = [ + Value::List(vec![narrow.clone()]), + Value::Set(vec![narrow.clone()]), + Value::Map(vec![(narrow.clone(), narrow.clone())]), + Value::OptionalSome(Box::new(narrow.clone())), + Value::ResultOk(Box::new(narrow.clone())), + Value::ResultFail(Box::new(narrow.clone())), + Value::Enum { + type_name: "Choice".to_owned(), + variant: "number".to_owned(), + fields: vec![narrow.clone()], + }, + Value::Struct { + concrete_type: None, + type_name: "Box[int8]".to_owned(), + fields: vec![("value".to_owned(), narrow)], + }, + ]; + fn check(value: &Value, count: &mut usize) { + match value { + Value::Typed { type_name, value } => { + assert_eq!(type_name, "int8"); + assert!(matches!(value.as_ref(), Value::Int64(n) if (-128..=127).contains(n))); + *count += 1; + } + Value::List(values) | Value::Set(values) | Value::Enum { fields: values, .. } => { + for value in values { + check(value, count); + } + } + Value::Map(entries) => { + for (key, value) in entries { + check(key, count); + check(value, count); + } + } + Value::Struct { fields, .. } => { + for (_, value) in fields { + check(value, count); + } + } + Value::OptionalSome(value) | Value::ResultOk(value) | Value::ResultFail(value) => { + check(value, count) + } + Value::OptionalNone => {} + other => panic!("lost checked integer identity: {other:?}"), + } + } + for aggregate in aggregates { + let mut count = 0; + for candidate in shrink_value(&aggregate) { + check(&candidate, &mut count); + } + assert!(count > 0, "expected typed payload candidates"); + } + } + + #[test] + fn shared_property_shrinker_preserves_search_order_and_counterexamples() { + let mut attempted = Vec::new(); + let result = shrink_property_inputs( + vec![Value::Int64(42), Value::List(vec![Value::Int64(99)])], + |values| { + attempted.push(values.to_vec()); + Ok::<_, ()>(matches!(values[0], Value::Int64(n) if n >= 2)) + }, + ) + .unwrap(); + assert_eq!(result, vec![Value::Int64(2), Value::List(vec![])]); + assert_eq!(attempted[0][0], Value::Int64(0)); + assert_eq!(attempted[1][0], Value::Int64(21)); + } + + #[test] + fn shared_property_shrinker_propagates_backend_errors_and_keeps_the_step_bound() { + let mut attempts = 0; + let result = shrink_property_inputs(vec![Value::Int64(42)], |_| { + attempts += 1; + Err::("native cleanup failed") + }); + assert_eq!(result.unwrap_err(), "native cleanup failed"); + assert_eq!(attempts, 1); + // The existing float search can propose the same integral value again. + // Preserve its bounded behavior even when that candidate still fails. + attempts = 0; + let result = shrink_property_inputs(vec![Value::Float64(2.0)], |values| { + attempts += 1; + Ok::<_, ()>(values[0] == Value::Float64(2.0)) + }) + .unwrap(); + assert_eq!(result, vec![Value::Float64(2.0)]); + assert_eq!(attempts, SHRINK_MAX_STEPS * 3); + } + #[test] fn property_shrinker_simplifies_sets_and_maps() { let set_candidates = shrink_value(&Value::Set(vec![Value::Int64(5)])); @@ -4005,6 +4879,7 @@ mod tests { #[test] fn property_shrinker_simplifies_struct_fields() { let candidates = shrink_value(&Value::Struct { + concrete_type: None, type_name: "PropertyUser".to_string(), fields: vec![ ("name".to_string(), Value::String("Ada".to_string())), @@ -4012,6 +4887,7 @@ mod tests { ], }); assert!(candidates.contains(&Value::Struct { + concrete_type: None, type_name: "PropertyUser".to_string(), fields: vec![ ("name".to_string(), Value::String(String::new())), @@ -4019,6 +4895,7 @@ mod tests { ], })); assert!(candidates.contains(&Value::Struct { + concrete_type: None, type_name: "PropertyUser".to_string(), fields: vec![ ("name".to_string(), Value::String("Ada".to_string())), @@ -4258,4 +5135,136 @@ mod tests { ); assert_eq!(results[1].iterations, Some(100)); } + + #[test] + fn checked_property_purpose_restores_previous_purpose_after_pass_and_failed_trial() { + use crate::ExecutionPurpose; + for release in [false, true] { + for purpose in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + for passes in [true, false] { + let source = format!( + "namespace app\nproperty restored_purpose:\n given sample: int64\n assert {passes}\n" + ); + let checked = crate::resource_execution::tests::program(&source, release); + let property = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(property) => Some(property), + _ => None, + }) + .expect("exact checked property"); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose) + .unwrap(); + let result = run_property_block( + &mut interpreter, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert_eq!(result.passed, passes, "{:?}", result.error); + assert_eq!( + result.iterations, + Some(if passes { + PROPERTY_DEFAULT_ITERATIONS + } else { + 1 + }) + ); + if !passes { + assert!(result.error.as_deref().unwrap().contains("counterexample:")); + } + assert!(result.debug_events.is_empty()); + assert_eq!( + interpreter.checked_execution_purpose(purpose), + Some(purpose) + ); + + // Metadata-free execution still has no retained purpose. + let mut raw = Interpreter::new(); + let raw_result = run_property_block( + &mut raw, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert_eq!(raw_result.passed, result.passed); + assert_eq!(raw_result.error, result.error); + assert_eq!(raw_result.iterations, result.iterations); + assert!(raw_result.debug_events.is_empty()); + assert_eq!(raw.checked_execution_purpose(purpose), None); + } + } + } + } + + #[test] + fn checked_property_purpose_restores_previous_purpose_after_unsupported_pool() { + use crate::ExecutionPurpose; + let source = "namespace app\nstruct UnsupportedPurposeProbe:\n marker: int64\nproperty restored_pool:\n given sample: UnsupportedPurposeProbe\n assert true\n"; + for release in [false, true] { + for purpose in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + let checked = crate::resource_execution::tests::program(source, release); + let property = checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(property) => Some(property), + _ => None, + }) + .expect("checked generator control"); + // Retain the original checked property and omit its generator + // definition below to exercise unsupported-pool recovery. + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), purpose).unwrap(); + let result = run_property_block( + &mut interpreter, + Some("app"), + property, + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert!(!result.passed); + assert_eq!(result.iterations, Some(0)); + assert!( + result + .error + .as_deref() + .unwrap() + .contains("cannot generate values") + ); + assert!(result.debug_events.is_empty()); + assert_eq!( + interpreter.checked_execution_purpose(purpose), + Some(purpose) + ); + } + } + } } diff --git a/crates/jett_comptime/src/verify/checked_property_cases.rs b/crates/jett_comptime/src/verify/checked_property_cases.rs new file mode 100644 index 00000000..0fc36945 --- /dev/null +++ b/crates/jett_comptime/src/verify/checked_property_cases.rs @@ -0,0 +1,327 @@ +//! Opaque deterministic cases from the original successful checked worker. +use std::sync::Arc; + +use jett_diagnostics::Diagnostic; +use jett_parser::ast::Item; +use jett_typecheck::CheckedResourceProgram; + +use super::{ + PROPERTY_DEFAULT_ITERATIONS, PropertyCase, run_verification, verify_results_to_diagnostics, +}; +use crate::ExplicitComptimeValues; +use crate::checked_types::CheckedExpressionTypes; + +#[derive(Debug, Clone)] +pub struct CheckedPropertyCases { + data: Arc, +} + +#[derive(Debug)] +struct Cases { + program: Arc, + values: Arc, + rows: Vec, +} + +impl CheckedPropertyCases { + pub fn cases(&self) -> &[PropertyCase] { + &self.data.rows + } + + pub fn belongs_to(&self, program: &Arc) -> bool { + Arc::ptr_eq(&self.data.program, program) + } + + pub fn required_values(&self) -> &ExplicitComptimeValues { + &self.data.values + } +} + +/// No source tree, metadata projection, raw case values, or trial order is supplied. +/// The existing worker and deterministic sampler retain their execution behavior. +pub fn collect_checked_property_cases_with_checked_values( + program: Arc, + values: Arc, +) -> Result> { + let values = Arc::new( + values + .successful_checked_view(&program) + .map_err(|message| vec![Diagnostic::error(9000, message, program.module().span)])?, + ); + let run = run_verification( + program.module(), + Some(program.checked().reflection_metadata.clone()), + Some(Arc::new(CheckedExpressionTypes::from_original_program( + &program, + ))), + Some(Arc::new(program.checked().breakpoint_exclusions.clone())), + Some(values.clone()), + true, + ); + let diagnostics = verify_results_to_diagnostics(&run.results); + if !diagnostics.is_empty() { + return Err(diagnostics); + } + let mut rows = run.property_cases.iter(); + for item in &program.module().items { + let Item::Property(property) = item else { + continue; + }; + for iteration in 0..PROPERTY_DEFAULT_ITERATIONS { + if !rows.next().is_some_and(|row| { + row.property_span == property.span + && row.iteration == iteration + && row.arguments.len() == property.givens.len() + }) { + return Err(vec![Diagnostic::error( + 9000, + "checked property sampler lost its original property/iteration/parameter order", + property.span, + )]); + } + } + } + if rows.next().is_some() { + return Err(vec![Diagnostic::error( + 9000, + "checked property sampler retained an extra trial", + program.module().span, + )]); + } + Ok(CheckedPropertyCases { + data: Arc::new(Cases { + program, + values, + rows: run.property_cases, + }), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ComptimeContext, Value}; + use jett_common::{FileId, SourceOrigin, Span}; + use std::collections::HashMap; + + fn program(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .unwrap(), + ) + } + + fn capture(program: &Arc) -> crate::ExplicitComptimeEvaluation { + crate::evaluate_explicit_comptime_expressions_capture( + program.module(), + program.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes::from_checked_program(program)), + Arc::new(program.checked().breakpoint_exclusions.clone()), + ) + } + + const SOURCE: &str = + "namespace app\nproperty scalar:\n given value: int64\n assert value == value\n"; + + #[test] + fn empty_success_retains_exact_program_cache_and_one_hundred_ordered_trials() { + let checked = program(SOURCE); + let evaluation = capture(&checked); + assert!(evaluation.diagnostics.is_empty()); + assert!(evaluation.values.is_empty()); + let values = Arc::new(evaluation.values); + let cases = + collect_checked_property_cases_with_checked_values(checked.clone(), values.clone()) + .unwrap(); + assert!(cases.belongs_to(&checked)); + assert!(cases.required_values().same_checked_cache(&values)); + assert!(!capture(&checked).values.same_checked_cache(&values)); + assert_eq!(cases.cases().len(), PROPERTY_DEFAULT_ITERATIONS); + for (iteration, case) in cases.cases().iter().enumerate() { + assert_eq!(case.iteration, iteration); + assert_eq!(case.arguments.len(), 1); + } + let foreign = program(SOURCE); + assert!(!cases.belongs_to(&foreign)); + assert!(collect_checked_property_cases_with_checked_values(foreign, values).is_err()); + } + + #[test] + fn public_mirror_inserts_do_not_change_the_private_successful_worker_view() { + let checked = program(SOURCE); + let original = capture(&checked).values; + let mut mirrors = original.clone(); + let span = Span::new(FileId::new(0), u32::MAX - 1, u32::MAX); + mirrors.insert(span, ComptimeContext::default(), Value::Int64(42)); + assert!(mirrors.get(span, &ComptimeContext::default()).is_some()); + assert!(mirrors.same_checked_cache(&original)); + let cases = + collect_checked_property_cases_with_checked_values(checked, Arc::new(mirrors)).unwrap(); + assert!(cases.required_values().same_checked_cache(&original)); + assert!( + cases + .required_values() + .get(span, &ComptimeContext::default()) + .is_none() + ); + } + + #[test] + fn unstamped_or_forged_projections_cannot_mint_cases_but_old_reads_still_work() { + let checked = program(SOURCE); + let mut public = ExplicitComptimeValues::default(); + public.insert( + checked.module().span, + ComptimeContext::default(), + Value::Int64(9), + ); + assert!( + public + .get(checked.module().span, &ComptimeContext::default()) + .is_some() + ); + assert!(public.checked_required_values(&checked).unwrap().is_empty()); + assert!( + collect_checked_property_cases_with_checked_values(checked.clone(), Arc::new(public)) + .is_err() + ); + for projection in 0..5 { + let mut types = CheckedExpressionTypes::from_original_program(&checked); + let mut metadata = checked.checked().reflection_metadata.clone(); + let mut exclusions = checked.checked().breakpoint_exclusions.clone(); + match projection { + 0 => { + types + .expressions + .insert(checked.module().span, "bool".into()); + } + 1 => { + types.functions.insert( + checked.module().span, + vec![Arc::new( + crate::checked_types::CheckedFunctionTypes::default(), + )], + ); + } + 2 => { + metadata = Arc::new(jett_types::ReflectionMetadata::new()); + } + 3 => { + exclusions.insert(checked.module().span, std::collections::HashSet::new()); + } + 4 => { + types.bindings.insert( + checked.module().span, + vec![Arc::new(crate::checked_types::CheckedScopedTypes::default())], + ); + } + _ => unreachable!(), + } + let evaluation = crate::evaluate_explicit_comptime_expressions_capture( + checked.module(), + metadata, + Arc::new(types), + Arc::new(exclusions), + ); + // Empty old evaluation still succeeds; it simply grants no new proof. + assert!(evaluation.diagnostics.is_empty()); + assert!( + collect_checked_property_cases_with_checked_values( + checked.clone(), + Arc::new(evaluation.values) + ) + .is_err() + ); + } + } + + #[test] + fn failed_required_evaluation_and_failed_property_do_not_mint_opaque_cases() { + let failed = program( + "namespace app\nfunction fail_required() returns int64:\n list[int64] rejected = range(0, 9223372036854775807)\n return 0\nint64 cached = comptime fail_required()\nproperty scalar:\n given value: int64\n assert value == value\n", + ); + let evaluation = capture(&failed); + assert!( + evaluation.diagnostics.iter().any(|diagnostic| diagnostic + .message + .contains("range: requested output is too large")), + "{:?}", + evaluation.diagnostics + ); + assert!( + collect_checked_property_cases_with_checked_values(failed, Arc::new(evaluation.values)) + .is_err() + ); + let failed_property = + program("namespace app\nproperty failing:\n given value: int64\n assert false\n"); + let values = capture(&failed_property).values; + assert!( + collect_checked_property_cases_with_checked_values(failed_property, Arc::new(values)) + .is_err() + ); + } + + #[test] + fn snapshot_observer_preserves_float_bits_typed_tags_and_pending_depth() { + let first = Value::Float64(f64::from_bits(0x7ff8_0000_0000_0123)); + assert!(first.same_checked_snapshot(&first.clone())); + assert!( + !first.same_checked_snapshot(&Value::Float64(f64::from_bits(0x7ff8_0000_0000_0124))) + ); + let tagged = Value::Pending(Box::new(Value::Typed { + type_name: "app.Positive".into(), + value: Box::new(Value::Int64(5)), + })); + assert!(tagged.same_checked_snapshot(&tagged.clone())); + assert!( + !tagged.same_checked_snapshot(&Value::Pending(Box::new(Value::Pending(Box::new( + Value::Typed { + type_name: "app.Positive".into(), + value: Box::new(Value::Int64(5)), + } + ))))) + ); + assert!( + !tagged.same_checked_snapshot(&Value::Pending(Box::new(Value::Typed { + type_name: "app.Foreign".into(), + value: Box::new(Value::Int64(5)), + }))) + ); + } + + #[test] + fn callable_observer_authenticates_only_the_original_checked_signature() { + let checked = program( + "namespace app\nfunction five() returns int64:\n return 5\nproperty scalar:\n given value: int64\n assert value == value\n", + ); + let definition = checked + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "app.five") + .unwrap(); + let actual = checked.checked().definition_types[&definition.id]; + assert!( + Value::NamedFunction("app.five".into()) + .validate_checked_callable_source(&checked, actual) + .is_ok() + ); + assert!( + Value::NamedFunction("app.absent".into()) + .validate_checked_callable_source(&checked, actual) + .is_err() + ); + assert!( + Value::Int64(5) + .validate_checked_callable_source(&checked, actual) + .is_err() + ); + } +} diff --git a/crates/jett_comptime/src/verify/property_purpose_tests.rs b/crates/jett_comptime/src/verify/property_purpose_tests.rs new file mode 100644 index 00000000..fcae1044 --- /dev/null +++ b/crates/jett_comptime/src/verify/property_purpose_tests.rs @@ -0,0 +1,231 @@ +use super::*; +use crate::ExecutionPurpose; +use jett_common::SourceOrigin; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use std::cell::Cell; + +const SOURCE: &str = + "namespace app\nproperty original_purpose:\n given sample: bool\n assert true\n"; + +fn program(release: bool, with_hooks: bool) -> Arc { + if with_hooks { + return crate::resource_execution::tests::program(SOURCE, release); + } + let file = FileId::new(0); + let checked = CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .expect("genuine ordinary checked property"); + assert!(checked.checked().resource_hooks.is_empty()); + Arc::new(checked) +} + +fn property(checked: &CheckedResourceProgram) -> &PropertyBlock { + checked + .module() + .items + .iter() + .find_map(|item| match item { + Item::Property(property) => Some(property), + _ => None, + }) + .expect("physical original property") +} + +fn assert_restored_and_reenter( + interpreter: &mut Interpreter, + checked: &Arc, + prior: ExecutionPurpose, +) { + assert_eq!(interpreter.checked_execution_purpose(prior), Some(prior)); + interpreter + .authorize_checked_resource_worker(checked.module(), prior) + .expect("same retained program and purpose restored"); + assert!(interpreter.take_debug_events().is_empty()); + let result = run_property_block( + interpreter, + Some("app"), + property(checked), + PropertyDefinitions { + enums: &[], + structs: &[], + bitfields: &[], + aliases: &[], + }, + None, + ); + assert!(result.passed, "{:?}", result.error); + assert_eq!(result.iterations, Some(PROPERTY_DEFAULT_ITERATIONS)); + assert!(result.debug_events.is_empty()); + assert_eq!(interpreter.checked_execution_purpose(prior), Some(prior)); + interpreter + .authorize_checked_resource_worker(checked.module(), prior) + .unwrap(); +} + +#[test] +fn property_execution_purpose_restores_error_and_original_checked_identity() { + for release in [false, true] { + for with_hooks in [false, true] { + for prior in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + let checked = program(release, with_hooks); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), prior).unwrap(); + interpreter + .authorize_checked_resource_worker(checked.module(), prior) + .unwrap(); + let attempts = Cell::new(0); + let result = with_property_execution_purpose(&mut interpreter, |interpreter| { + assert_eq!( + interpreter.checked_execution_purpose(ExecutionPurpose::Property), + Some(ExecutionPurpose::Property) + ); + interpreter.with_checked_property_region( + property(&checked), + Some("app"), + |_| -> Result<(), String> { + attempts.set(attempts.get() + 1); + Err("original property body error".to_owned()) + }, + ) + }); + assert_eq!(result, Err("original property body error".to_owned())); + assert_eq!(attempts.get(), 1); + assert_restored_and_reenter(&mut interpreter, &checked, prior); + } + } + } +} + +#[derive(Debug)] +struct PanicMarker(u64); + +#[test] +fn property_execution_purpose_restores_original_panic_box_before_reentry() { + for release in [false, true] { + for with_hooks in [false, true] { + for prior in [ + ExecutionPurpose::Verify, + ExecutionPurpose::NamespaceConstant, + ] { + let checked = program(release, with_hooks); + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), prior).unwrap(); + interpreter + .authorize_checked_resource_worker(checked.module(), prior) + .unwrap(); + let attempts = Cell::new(0); + let payload = Box::new(PanicMarker(73)); + let identity = std::ptr::from_ref(payload.as_ref()); + let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + with_property_execution_purpose(&mut interpreter, |interpreter| { + interpreter.with_checked_property_region( + property(&checked), + Some("app"), + |_| -> Result<(), String> { + attempts.set(attempts.get() + 1); + std::panic::resume_unwind(payload) + }, + ) + }) + })); + let payload = caught.err().expect("original panic must propagate"); + let payload = payload + .downcast::() + .unwrap_or_else(|_| panic!("panic payload type changed")); + assert_eq!(payload.0, 73); + assert!(std::ptr::eq(identity, std::ptr::from_ref(payload.as_ref()))); + assert_eq!(attempts.get(), 1); + assert_restored_and_reenter(&mut interpreter, &checked, prior); + } + } + } +} + +#[derive(Clone, Copy)] +enum Refusal { + ClonedBlock, + WrongNamespace, +} + +#[test] +fn property_execution_purpose_refuses_foreign_association_before_effects() { + for release in [false, true] { + for with_hooks in [false, true] { + for refusal in [Refusal::ClonedBlock, Refusal::WrongNamespace] { + let checked = program(release, with_hooks); + let prior = ExecutionPurpose::Verify; + let mut interpreter = + Interpreter::from_checked_resource_program(checked.clone(), prior).unwrap(); + interpreter + .authorize_checked_resource_worker(checked.module(), prior) + .unwrap(); + let original = property(&checked); + let cloned = original.clone(); + let (selected, namespace) = match refusal { + Refusal::ClonedBlock => (&cloned, Some("app")), + Refusal::WrongNamespace => (original, Some("other")), + }; + let attempts = Cell::new(0); + let result = with_property_execution_purpose(&mut interpreter, |interpreter| { + interpreter.with_checked_property_region(selected, namespace, |_| { + attempts.set(attempts.get() + 1); + Ok(()) + }) + }); + assert_eq!( + result.as_ref().map_err(String::as_str), + Err("checked test capture has no exact original body/purpose/namespace") + ); + assert_eq!(attempts.get(), 0); + assert_restored_and_reenter(&mut interpreter, &checked, prior); + } + } + } +} + +#[test] +fn property_execution_purpose_keeps_metadata_free_error_and_panic_unchecked() { + let mut interpreter = Interpreter::new(); + assert_eq!( + interpreter.checked_execution_purpose(ExecutionPurpose::Verify), + None + ); + let error = with_property_execution_purpose(&mut interpreter, |interpreter| { + assert_eq!( + interpreter.checked_execution_purpose(ExecutionPurpose::Property), + None + ); + Err::<(), _>("ordinary error") + }); + assert_eq!(error, Err("ordinary error")); + assert_eq!( + interpreter.checked_execution_purpose(ExecutionPurpose::Verify), + None + ); + let payload = Box::new(PanicMarker(91)); + let identity = std::ptr::from_ref(payload.as_ref()); + let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + with_property_execution_purpose(&mut interpreter, |_| -> () { + std::panic::resume_unwind(payload) + }) + })); + let payload = caught + .err() + .expect("raw panic remains a panic") + .downcast::() + .unwrap_or_else(|_| panic!("raw panic payload changed")); + assert_eq!(payload.0, 91); + assert!(std::ptr::eq(identity, std::ptr::from_ref(payload.as_ref()))); + assert_eq!( + interpreter.checked_execution_purpose(ExecutionPurpose::Verify), + None + ); + assert!(interpreter.take_debug_events().is_empty()); +} diff --git a/crates/jett_driver/Cargo.toml b/crates/jett_driver/Cargo.toml index 2e8a099c..5ee598b4 100644 --- a/crates/jett_driver/Cargo.toml +++ b/crates/jett_driver/Cargo.toml @@ -15,9 +15,17 @@ jett_ast.workspace = true jett_resolve.workspace = true jett_types.workspace = true jett_typecheck.workspace = true +jett_hir.workspace = true +jett_mir.workspace = true jett_comptime.workspace = true +jett_runtime.workspace = true jett_fmt.workspace = true jett_query.workspace = true +jett_codegen_cranelift.workspace = true +tempfile.workspace = true +serde_json.workspace = true [target.'cfg(windows)'.dependencies] +find-msvc-tools.workspace = true same-file = "1" +windows-sys = { version = "=0.61.2", features = ["Win32_Storage_FileSystem"] } diff --git a/crates/jett_driver/examples/native_bundle_manifest.rs b/crates/jett_driver/examples/native_bundle_manifest.rs new file mode 100644 index 00000000..fac48e5f --- /dev/null +++ b/crates/jett_driver/examples/native_bundle_manifest.rs @@ -0,0 +1,42 @@ +//! Packaging helper: serialize the driver's canonical linker contract. +use jett_driver::native::{self, NativeLauncherBundle}; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let mut args = std::env::args_os().skip(1); + let archive = PathBuf::from(args.next().ok_or("expected archive path")?); + let profile = args.next().ok_or("expected debug or release profile")?; + let profile = profile.to_str().ok_or("profile must be UTF-8")?; + if !matches!(profile, "debug" | "release") || args.next().is_some() { + return Err("usage: native_bundle_manifest ARCHIVE debug|release".into()); + } + let bundle = match native::host_target().as_str() { + native::LINUX_GNU_NATIVE_TARGET => NativeLauncherBundle::linux_gnu_v1(&archive), + native::WINDOWS_MSVC_NATIVE_TARGET => { + NativeLauncherBundle::windows_msvc_static_v1(&archive) + } + _ => return Err("unsupported packaging host".into()), + }; + let libraries = bundle + .native_library_args + .iter() + .map(|arg| arg.to_str().ok_or("library argument must be UTF-8")) + .collect::, _>>()?; + let value = serde_json::json!({ + "manifest_version": 1, + "compiler_version": env!("CARGO_PKG_VERSION"), + "target": bundle.target, + "runtime_abi_version": bundle.runtime_abi_version, + "crt_mode": bundle.crt_mode.to_string(), + "profile": profile, + "archive": archive.file_name().and_then(|name| name.to_str()).ok_or("archive name must be UTF-8")?, + "native_library_args": libraries, + }); + let manifest = archive.with_file_name("launcher.json"); + std::fs::write( + &manifest, + format!("{}\n", serde_json::to_string_pretty(&value)?), + )?; + NativeLauncherBundle::from_manifest(&manifest, profile == "release")?; + Ok(()) +} diff --git a/crates/jett_driver/examples/native_parity.rs b/crates/jett_driver/examples/native_parity.rs new file mode 100644 index 00000000..45c976be --- /dev/null +++ b/crates/jett_driver/examples/native_parity.rs @@ -0,0 +1,419 @@ +//! Exhaustive native fixture parity probe (separate from the full release audit). +//! Usage: cargo run -p jett_driver --example native_parity -- LAUNCHER REPORT.json +//! Every inventory row is attempted, irrespective of staged object_emit markers. +#[path = "../tests/support/native_manifest_inputs.rs"] +mod manifest_inputs; +use manifest_inputs::{clock_samples, environment_snapshot, graphics_events, random_samples}; + +use jett_common::FileId; +use jett_driver::native::{self, NativeLauncherBundle}; +use jett_parser::ast::Item; +use jett_runtime::clock::{ClockTestSample, TEST_SCRIPT_ENV, encode_test_script}; +use jett_runtime::environment::{self, EnvironmentTestSnapshot}; +use jett_runtime::graphics::{self, TestEvent as GraphicsTestEvent}; +use jett_runtime::random::{ + RandomTestSample, TEST_SCRIPT_ENV as RANDOM_TEST_SCRIPT_ENV, + encode_test_script as encode_random_test_script, +}; +use serde_json::{Value, json}; +use std::collections::BTreeSet; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode, Output, Stdio}; +use std::time::{Duration, Instant}; + +fn execute( + binary: &Path, + directory: &Path, + clock_samples: Option<&[ClockTestSample]>, + random_samples: Option<&[RandomTestSample]>, + environment_snapshot: Option<&EnvironmentTestSnapshot>, + graphics_events: Option<&[GraphicsTestEvent]>, +) -> Result { + let mut command = Command::new(binary); + command + .current_dir(directory) + .env_clear() + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(samples) = clock_samples { + command.env(TEST_SCRIPT_ENV, encode_test_script(samples)); + } + if let Some(samples) = random_samples { + command.env(RANDOM_TEST_SCRIPT_ENV, encode_random_test_script(samples)); + } + if let Some(snapshot) = environment_snapshot { + command.env( + environment::TEST_SNAPSHOT_ENV, + environment::encode_test_snapshot(snapshot), + ); + } + if let Some(events) = graphics_events { + command.env( + graphics::TEST_SCRIPT_ENV, + graphics::encode_test_script(events), + ); + } + let mut child = command.spawn().map_err(|e| e.to_string())?; + let mut stdout = child.stdout.take().unwrap(); + let mut stderr = child.stderr.take().unwrap(); + let out = std::thread::spawn(move || { + let mut b = Vec::new(); + stdout.read_to_end(&mut b).map(|_| b) + }); + let err = std::thread::spawn(move || { + let mut b = Vec::new(); + stderr.read_to_end(&mut b).map(|_| b) + }); + let start = Instant::now(); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) if start.elapsed() < Duration::from_secs(60) => { + std::thread::sleep(Duration::from_millis(10)) + } + outcome => { + let _ = child.kill(); + let _ = child.wait(); + let _ = out.join(); + let _ = err.join(); + return Err(format!( + "native execution failed or exceeded 60s: {outcome:?}" + )); + } + } + }; + Ok(Output { + status, + stdout: out + .join() + .map_err(|_| "stdout reader panic")? + .map_err(|e| e.to_string())?, + stderr: err + .join() + .map_err(|_| "stderr reader panic")? + .map_err(|e| e.to_string())?, + }) +} + +fn behavior( + source: &Path, + binary: &Path, + directory: &Path, + expected_failure: bool, + clock_samples: Option<&[ClockTestSample]>, + random_samples: Option<&[RandomTestSample]>, + environment_snapshot: Option<&EnvironmentTestSnapshot>, + graphics_events: Option<&[GraphicsTestEvent]>, +) -> Result { + let actual = execute( + binary, + directory, + clock_samples, + random_samples, + environment_snapshot, + graphics_events, + )?; + let stdout = String::from_utf8(actual.stdout).map_err(|e| e.to_string())?; + let stderr = String::from_utf8(actual.stderr).map_err(|e| e.to_string())?; + if usize::from(clock_samples.is_some()) + + usize::from(random_samples.is_some()) + + usize::from(environment_snapshot.is_some()) + + usize::from(graphics_events.is_some()) + > 1 + { + return Err("multiple scripted capability providers in one fixture".into()); + } + let oracle_result = if let Some(samples) = clock_samples { + jett_driver::run_file_capture_outcome_with_clock_test_samples(source, samples.to_vec()) + } else if let Some(samples) = random_samples { + jett_driver::run_file_capture_outcome_with_random_test_samples(source, samples.to_vec()) + } else if let Some(snapshot) = environment_snapshot { + jett_driver::run_file_capture_outcome_with_environment_test_snapshot( + source, + snapshot.clone(), + ) + } else if let Some(events) = graphics_events { + jett_driver::run_file_capture_outcome_with_graphics_test_events(source, events.to_vec()) + } else { + jett_driver::run_file_capture_outcome(source) + }; + let (oracle, failure) = match oracle_result { + Ok(output) => (output, None), + Err(failure) => (failure.output, Some(failure.message)), + }; + if failure.is_some() != expected_failure { + return Err("interpreter outcome contradicts manifest".into()); + } + let mut expected_stderr = jett_driver::render_debug_events(&oracle.debug_events); + if let Some(message) = &failure { + expected_stderr.push_str(message); + expected_stderr.push('\n'); + } + let expected_status = if failure.is_some() { 71 } else { 0 }; + let matched = actual.status.code() == Some(expected_status) + && stdout == oracle.stdout + && stderr == expected_stderr; + let interpreter_debug = oracle + .debug_events + .iter() + .map(|event| json!({"kind": event.kind.as_str(), "text": &event.text})) + .collect::>(); + Ok( + json!({"behavior_matches":matched,"exit_code":actual.status.code(),"stdout":stdout,"stderr":stderr, + "interpreter_stdout":oracle.stdout,"interpreter_debug":interpreter_debug,"interpreter_failure":failure, + // Current scalar/string ABI destruction checks its live owning-value + // registry. Entry-failure 71 requires successful destruction; a leak + // overrides it with 72. Launcher tests enforce both outcomes. Future + // resource families also need finalizer/effect oracles before acceptance. + "failure_cleanup_verified":expected_failure && matched && actual.status.code() == Some(71), + "cleanup_contract":"native owning-value registry empty at checked context destruction" + }), + ) +} + +/// Discover suites from syntax, so adding a test block automatically extends +/// the execution denominator even when its manifest row only names lowering. +fn suite_counts(source: &str) -> [usize; 2] { + let parsed = jett_parser::parse(source, FileId::new(0)); + let mut counts = [0, 0]; + for item in &parsed.module.items { + match item { + Item::Verify(_) => counts[0] += 1, + Item::Property(_) => counts[1] += 1, + _ => {} + } + } + counts +} + +fn suite_behavior( + source: &Path, + launcher: &NativeLauncherBundle, + property: bool, +) -> Result { + if property { + let result = native::run_host_property_suite( + source, + launcher, + native::NativePropertyOptions::default(), + ) + .map_err(|error| error.to_string())?; + let failure = result.failure.as_ref().map(|failure| json!({ + "property": failure.name, "trial": failure.trial, "counterexample": failure.counterexample, + })); + return Ok(json!({ + "passed": result.failure.is_none(), + "exit_code": if result.failure.is_none() { 0 } else { 71 }, + "stdout": String::from_utf8_lossy(&result.stdout), + "stderr": String::from_utf8_lossy(&result.stderr), + "cleanup_verified": true, + "trials": result.trials, + "failure": failure, + })); + } + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + let binary = directory + .path() + .join(if cfg!(windows) { "suite.exe" } else { "suite" }); + native::build_host_verify_suite_executable(source, launcher, &binary) + .map_err(|error| error.to_string())?; + let actual = execute(&binary, directory.path(), None, None, None, None)?; + Ok(json!({ + "passed": actual.status.success(), + "exit_code": actual.status.code(), + "stdout": String::from_utf8_lossy(&actual.stdout), + "stderr": String::from_utf8_lossy(&actual.stderr), + "cleanup_verified": actual.status.success(), + })) +} + +fn main() -> ExitCode { + let args = std::env::args_os().skip(1).collect::>(); + assert_eq!(args.len(), 2, "usage: native_parity LAUNCHER REPORT.json"); + let archive = fs::canonicalize(&args[0]).expect("launcher archive must exist"); + let launcher = match native::host_target().as_str() { + native::LINUX_GNU_NATIVE_TARGET => NativeLauncherBundle::linux_gnu_v1(archive), + native::WINDOWS_MSVC_NATIVE_TARGET => NativeLauncherBundle::windows_msvc_static_v1(archive), + target => panic!("unsupported native execution host: {target}"), + }; + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .unwrap(); + let manifest: Value = + serde_json::from_slice(&fs::read(root.join("tests/native_parity.json")).unwrap()).unwrap(); + let fixtures = manifest["fixtures"].as_array().unwrap(); + let paths = fixtures + .iter() + .map(|f| f["path"].as_str().unwrap().to_owned()) + .collect::>(); + assert_eq!(paths.len(), fixtures.len(), "duplicate manifest rows"); + let discovered = ["run_pass", "runtime_fail"] + .into_iter() + .flat_map(|category| { + fs::read_dir(root.join("tests").join(category)) + .unwrap() + .map(move |p| (category, p.unwrap().path())) + }) + .filter(|(_, p)| p.extension().is_some_and(|e| e == "jett")) + .map(|(c, p)| format!("tests/{c}/{}", p.file_name().unwrap().to_str().unwrap())) + .collect::>(); + assert_eq!(paths, discovered, "no inventory omissions permitted"); + let mut rows = Vec::new(); + let (mut lower_pass, mut object_pass, mut main_pass, mut runtime_pass) = (0, 0, 0, 0); + let (mut lower_total, mut main_total, mut runtime_total) = (0, 0, 0); + let mut suite_total = [0, 0]; + let mut suite_pass = [0, 0]; + let mut suite_blocks = [0, 0]; + for fixture in fixtures { + let path = fixture["path"].as_str().unwrap(); + let obligations = fixture["obligations"].as_array().unwrap(); + let lower = obligations.iter().any(|o| o == "lower"); + let main = obligations.iter().any(|o| o == "main_execute"); + let runtime = obligations.iter().any(|o| o == "runtime_contract"); + lower_total += usize::from(lower); + main_total += usize::from(main); + runtime_total += usize::from(runtime); + let source = root.join(path); + let mut row = json!({"path":path,"obligations":obligations}); + if lower { + let blocks = suite_counts(&fs::read_to_string(&source).expect("fixture source")); + for (index, label) in ["verify", "property"].into_iter().enumerate() { + if blocks[index] == 0 { + continue; + } + suite_total[index] += 1; + suite_blocks[index] += blocks[index]; + row[label] = match suite_behavior(&source, &launcher, index == 1) { + Ok(result) => { + suite_pass[index] += usize::from(result["passed"] == true); + result + } + Err(error) => json!({"passed":false,"error":error}), + }; + row[label]["blocks"] = json!(blocks[index]); + if index == 1 { + row[label]["trials_per_block"] = json!(100); + } + } + } + match jett_driver::lower_file_for_native_tests(&source) { + Err(error) => row["lower_error"] = json!(error.to_string()), + Ok(lowered) => { + row["lowered"] = json!(true); + lower_pass += usize::from(lower); + match jett_codegen_cranelift::emit_host_object(&lowered.mir, &lowered.interner) { + Ok(object) if !object.bytes.is_empty() && !object.symbols.is_empty() => { + row["object_emitted"] = json!(true); + row["object_bytes"] = json!(object.bytes.len()); + row["defined_symbols"] = json!(object.symbols.len()); + object_pass += usize::from(lower); + } + Ok(_) => { + row["object_error"] = json!("empty object or no reachable code symbols") + } + Err(error) => row["object_error"] = json!(error.to_string()), + } + } + } + if main || runtime { + let directory = tempfile::tempdir().unwrap(); + let output = directory.path().join(if cfg!(windows) { + "program.exe" + } else { + "program" + }); + let isolated_source = directory.path().join("main.jett"); + fs::copy(&source, &isolated_source).expect("copy inventory source"); + match native::build_host_executable(&isolated_source, &launcher, &output) { + Err(error) => row["execution_error"] = json!(error.to_string()), + Ok(_) => { + fs::remove_file(&isolated_source) + .expect("remove linked source before execution"); + row["linked"] = json!(true); + let failure = fixture["expected_outcome"] == "expected_failure"; + let samples = clock_samples(fixture).expect("valid Clock sample manifest"); + let random_samples = + random_samples(fixture).expect("valid Random sample manifest"); + let environment_snapshot = + environment_snapshot(fixture).expect("valid Environment snapshot manifest"); + let graphics_events = + graphics_events(fixture).expect("valid Graphics event manifest"); + match behavior( + &source, + &output, + directory.path(), + failure, + samples.as_deref(), + random_samples.as_deref(), + environment_snapshot.as_ref(), + graphics_events.as_deref(), + ) { + Err(error) => row["execution_error"] = json!(error), + Ok(result) => { + let pass = execution_passes(&result, failure); + main_pass += usize::from(main && pass); + runtime_pass += usize::from(runtime && pass); + row["execution"] = result; + } + } + } + } + } + eprintln!("{} / {}: {path}", rows.len() + 1, fixtures.len()); + rows.push(row); + // Persist every batch; a failed or interrupted gate never erases evidence. + fs::write( + &args[1], + serde_json::to_vec_pretty(&json!({"complete":false,"fixtures":rows})).unwrap(), + ) + .unwrap(); + } + let counts = json!({"lower":[lower_pass,lower_total],"object":[object_pass,lower_total], + "main":[main_pass,main_total],"runtime_contract":[runtime_pass,runtime_total], + "verify":[suite_pass[0],suite_total[0]],"property":[suite_pass[1],suite_total[1]]}); + let fixture_gates_complete = lower_pass == lower_total + && object_pass == lower_total + && main_pass == main_total + && runtime_pass == runtime_total + && suite_pass == suite_total; + let report = json!({"complete":false,"fixture_gates_complete":fixture_gates_complete, + "target":native::host_target(),"counts":counts,"fixtures":rows, + "suite_blocks":{"verify":suite_blocks[0],"property":suite_blocks[1]}, + "pending_release_gates":["unresolved equality and erased-call contracts", "actor handles escaping comptime", "remaining semantic and value-composition audit", "full workspace and clean Linux GNU/Windows MSVC distribution at the release revision"]}); + fs::write(&args[1], serde_json::to_vec_pretty(&report).unwrap()).unwrap(); + println!("{counts}"); + // Successful fixture gates do not certify the outstanding release gates. + if fixture_gates_complete { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + } +} + +fn execution_passes(result: &Value, expected_failure: bool) -> bool { + result["behavior_matches"] == true + && (!expected_failure || result["failure_cleanup_verified"] == true) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn expected_failures_count_only_with_cleanup_evidence() { + let verified = json!({"behavior_matches":true, "failure_cleanup_verified":true}); + assert!(execution_passes(&verified, true)); + assert!(!execution_passes( + &json!({"behavior_matches":true, "failure_cleanup_verified":false}), + true + )); + assert!(!execution_passes( + &json!({"behavior_matches":false, "failure_cleanup_verified":true}), + true + )); + assert!(execution_passes(&json!({"behavior_matches":true}), false)); + } +} diff --git a/crates/jett_driver/examples/native_probe.rs b/crates/jett_driver/examples/native_probe.rs new file mode 100644 index 00000000..52dabdad --- /dev/null +++ b/crates/jett_driver/examples/native_probe.rs @@ -0,0 +1,35 @@ +//! Inspect one fixture's first native object blocker with its source function. +//! Usage: cargo run -p jett_driver --example native_probe -- FILE.jett +use jett_codegen_cranelift::{emit_host_object, symbol_name}; +use jett_driver::lower_file_for_backend; +use std::path::Path; + +fn main() { + let path = std::env::args_os() + .nth(1) + .expect("usage: native_probe FILE.jett"); + let lowered = lower_file_for_backend(Path::new(&path)).expect("checked lowering"); + match emit_host_object(&lowered.mir, &lowered.interner) { + Ok(object) => println!( + "native object: {} bytes, {} symbols", + object.bytes.len(), + object.symbols.len() + ), + Err(error) => { + let message = error.to_string(); + println!("{message}"); + for function in &lowered.mir.functions { + let symbol = + symbol_name(&function.identity, &lowered.interner).expect("native symbol"); + if message.contains(&symbol) { + let declaration = &function.identity.declaration; + println!( + "source function: {}::{}", + declaration.namespace, declaration.name + ); + break; + } + } + } + } +} diff --git a/crates/jett_driver/src/lib.rs b/crates/jett_driver/src/lib.rs index dab71bae..81a552cf 100644 --- a/crates/jett_driver/src/lib.rs +++ b/crates/jett_driver/src/lib.rs @@ -1,13 +1,20 @@ +use jett_comptime::checked_types::{ + CheckedExpressionTypes, CheckedFunctionTypes, CheckedScopedBindings, CheckedScopedTypes, +}; +mod native_builder_constants; +mod native_constants; +mod native_property_cases; +mod native_resource_constants; use jett_common::{FileId, STDLIB_FILE_ID_START, Span}; -use jett_comptime::evaluate_explicit_comptime_expressions; +use jett_comptime::evaluate_explicit_comptime_expressions_capture; use jett_comptime::value::Value; use jett_comptime::verify::{ - run_verify_blocks_detailed_with_metadata_and_expression_types, - run_verify_blocks_with_metadata_and_expression_types, + collect_property_cases_with_checked_values, run_verify_blocks_detailed_with_checked_values, + verify_results_to_diagnostics, }; pub use jett_comptime::{ - ClockTestSample, EnvironmentTestEntry, EnvironmentTestSnapshot, EnvironmentTestText, - GraphicsTestEvent, GraphicsTestKey, RandomTestSample, + ClockTestSample, DebugEvent, DebugEventKind, EnvironmentTestEntry, EnvironmentTestSnapshot, + EnvironmentTestText, GraphicsTestEvent, GraphicsTestKey, RandomTestSample, render_debug_events, }; use jett_diagnostics::Diagnostic; use jett_fmt::{FormatResult, format_source}; @@ -15,8 +22,14 @@ use jett_parser::ast::{FunctionDecl, FunctionDef, Item, Module, Param, TypeExpr} use jett_parser::{ParseResult, parse}; use jett_query::{QueryDatabase, SourceOrigin}; use jett_resolve::resolve; -use jett_typecheck::{CheckOptions, CheckResult, check, check_with_options}; +use jett_typecheck::{ + CheckOptions, CheckResult, CheckedResourceProgram, ResourceProgramError, check, +}; use jett_types::ReflectionMetadata; +pub use native_property_cases::NativePropertyPlan; +pub use native_resource_constants::{ + bake_checked_resource_values, evaluate_checked_resource_required_values, +}; use std::borrow::Cow; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs; @@ -24,13 +37,16 @@ use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use std::thread; -const RUNTIME_STACK_SIZE: usize = 8 * 1024 * 1024; +pub mod native; + +const RUNTIME_STACK_SIZE: usize = jett_comptime::INTERPRETER_STACK_SIZE; struct DiscoveredModules { modules: Vec, diagnostics: Vec, files: HashMap, sources: HashMap, + origins: HashMap, } impl DiscoveredModules { @@ -39,11 +55,50 @@ impl DiscoveredModules { self.diagnostics.extend(other.diagnostics); self.files.extend(other.files); self.sources.extend(other.sources); + self.origins.extend(other.origins); + } +} + +/// The actual compiler or execution stage that produced an observation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DebugPhase { + Comptime, + FrontendVerify, + Runtime, +} + +impl DebugPhase { + pub const fn as_str(self) -> &'static str { + match self { + Self::Comptime => "comptime", + Self::FrontendVerify => "frontend_verify", + Self::Runtime => "runtime", + } } } +/// One ordered observation, kept outside cached and baked language values. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DebugObservation { + pub phase: DebugPhase, + pub event: DebugEvent, +} + +fn append_debug_events( + observations: &mut Vec, + phase: DebugPhase, + events: impl IntoIterator, +) { + observations.extend( + events + .into_iter() + .map(|event| DebugObservation { phase, event }), + ); +} + /// Result of compiling a single file. pub struct BuildResult { + pub breakpoint_exclusions: Option>>>, pub diagnostics: Vec, pub has_errors: bool, /// The source text that was compiled (for diagnostic rendering). @@ -53,9 +108,176 @@ pub struct BuildResult { /// Checked reflection metadata for runtime reflection/JSON hooks. pub reflection_metadata: Option>, /// Checked expression type names for runtime normalization at expression-only sites. - pub checked_expression_types: Option>>, + pub checked_expression_types: Option>, /// Values baked by explicit `comptime` expressions. - pub explicit_comptime_values: Option>>, + pub explicit_comptime_values: Option>, + pub debug_observations: Vec, +} + +/// Private reference handoff; public builds may discard this retained session. +struct PreparedReference { + build: BuildResult, + program: Option>, + entry: Option, +} + +struct PreparedReferenceEntry { + namespace: Option, + span: Span, +} + +impl PreparedReference { + fn rejected(build: BuildResult) -> Self { + Self { + build, + program: None, + entry: None, + } + } +} + +/// Backend-neutral programs and checked data produced for a valid source file. +/// +/// This staging result owns the type interner whose session-local IDs appear in +/// HIR and MIR. Source origins are supplied explicitly during lowering and are +/// retained so later backend stages never need to infer authority from numeric +/// file IDs. +#[derive(Debug)] +pub struct BackendLoweringResult { + /// Frontend observations retained for successful artifact builds. + pub diagnostics: Vec, + pub source: String, + pub hir: jett_hir::Program, + pub mir: jett_mir::Program, + /// Exact checked HIR/MIR identity of the primary source file's `main` + /// function. Backend consumers must use this identity rather than + /// rediscovering an entry point from source or symbol names. + pub program_entry: Option, + /// Compiler-owned entry that calls the primary source file's checked + /// `verify` bodies in declaration order, when native suite mode is used. + pub native_verify_entry: Option, + /// Compiler-owned entry for generated native `property` trials. + pub native_property_entry: Option, + /// Retained checked-session inputs for native property replay. + pub native_property_plan: Option, + pub interner: BackendTypes, + pub source_origins: HashMap, + pub reflection_metadata: Arc, + pub checked_expression_types: Arc, + pub explicit_comptime_values: Arc, + pub debug_observations: Vec, +} + +/// Readonly ownership of the exact checked type session used by this backend. +/// Retaining its program keeps graph proofs and session-local IDs coherent. +#[derive(Debug, Clone)] +pub struct BackendTypes(Arc); + +impl std::ops::Deref for BackendTypes { + type Target = jett_types::TypeInterner; + + fn deref(&self) -> &Self::Target { + &self.0.checked().interner + } +} + +/// Failure while validating or lowering a file for a backend. +pub enum BackendLoweringError { + /// The frontend or compile-time validation pipeline rejected the source. + Build(BuildResult), + /// Typed AST to HIR lowering failed. + Hir(Vec), + /// HIR to MIR lowering failed. + Mir(Vec), + /// MIR structural validation failed. + MirValidation(Vec), + /// A later lowering failure with observations from the completed frontend. + Captured { + source: Box, + debug_observations: Vec, + }, +} + +impl BackendLoweringError { + pub fn debug_observations(&self) -> &[DebugObservation] { + match self { + Self::Build(result) => &result.debug_observations, + Self::Captured { + debug_observations, .. + } => debug_observations, + _ => &[], + } + } + + pub fn build_result(&self) -> Option<&BuildResult> { + match self { + Self::Build(result) => Some(result), + Self::Captured { source, .. } => source.build_result(), + _ => None, + } + } + + fn with_debug_observations(self, observations: &[DebugObservation]) -> Self { + if observations.is_empty() || !self.debug_observations().is_empty() { + self + } else { + Self::Captured { + source: Box::new(self), + debug_observations: observations.to_vec(), + } + } + } +} + +impl std::fmt::Display for BackendLoweringError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Captured { source, .. } => std::fmt::Display::fmt(source, f), + Self::Build(result) => { + f.write_str("build failed before backend lowering")?; + for message in error_messages_from_diagnostics(&result.diagnostics) { + write!(f, "\n{message}")?; + } + Ok(()) + } + Self::Hir(errors) => { + f.write_str("HIR lowering failed")?; + for error in errors { + write!(f, "\n{:?}: {}", error.span, error.message)?; + } + Ok(()) + } + Self::Mir(errors) => { + f.write_str("MIR lowering failed")?; + for error in errors { + write!(f, "\n{:?}: {}", error.span, error.message)?; + } + Ok(()) + } + Self::MirValidation(errors) => { + f.write_str("MIR validation failed")?; + for error in errors { + write!(f, "\n{:?}: {}", error.span, error.message)?; + } + Ok(()) + } + } + } +} + +impl std::fmt::Debug for BackendLoweringError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +impl std::error::Error for BackendLoweringError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Captured { source, .. } => Some(source.as_ref()), + _ => None, + } + } } /// Mode-specific options for a build. @@ -69,9 +291,59 @@ pub struct BuildOptions { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RunOutput { pub stdout: String, - pub debug_output: Vec, + pub debug_events: Vec, + pub frontend_debug_observations: Vec, +} + +/// A failed interpreter run together with output produced before the failure. +/// +/// The message uses the same text returned by the legacy `String`-based run +/// APIs. Captured application stdout and debug events are retained so another backend can +/// compare partial observable behavior as well as terminal failure text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RunFailure { + pub message: String, + pub output: RunOutput, +} + +impl RunFailure { + fn without_output(message: impl Into) -> Self { + Self { + message: message.into(), + output: RunOutput { + stdout: String::new(), + debug_events: Vec::new(), + frontend_debug_observations: Vec::new(), + }, + } + } + + fn with_frontend(message: impl Into, observations: &[DebugObservation]) -> Self { + let mut failure = Self::without_output(message); + failure.output.frontend_debug_observations = observations.to_vec(); + failure + } + + fn with_output(message: impl Into, output: RunOutput) -> Self { + Self { + message: message.into(), + output, + } + } + + fn into_message(self) -> String { + self.message + } +} + +impl std::fmt::Display for RunFailure { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.message) + } } +impl std::error::Error for RunFailure {} + /// A single definition visible through the namespace query surface. #[derive(Debug, Clone, PartialEq, Eq)] pub struct QueryDefinition { @@ -390,7 +662,9 @@ pub fn build_source(source: &str, file_path: &str) -> BuildResult { file_path: file_path.to_string(), reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, + debug_observations: Vec::new(), }; } @@ -405,59 +679,86 @@ pub fn build_source(source: &str, file_path: &str) -> BuildResult { file_path: file_path.to_string(), reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, + debug_observations: Vec::new(), }; } + let mut source_origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + source_origins.extend(support_modules.origins); prepend_support_modules(&mut parse_result.module, support_modules.modules); - - let resolve_result = resolve(&parse_result.module); + let primary_span = parse_result.module.span; + parse_result.errors = all_diagnostics; + let checked_program = match CheckedResourceProgram::prepare( + parse_result, + source_origins, + &[], + CheckOptions::default(), + ) { + Ok(program) => Arc::new(program), + Err(error) => { + let mut diagnostics = error.diagnostics().unwrap_or(&[]).to_vec(); + if matches!( + &error, + ResourceProgramError::ResolveMetadata { .. } + | ResourceProgramError::CheckMetadata { .. } + ) { + diagnostics.push(Diagnostic::error(0, error.to_string(), primary_span)); + } + return BuildResult { + has_errors: true, + diagnostics, + source: source.to_string(), + file_path: file_path.to_string(), + reflection_metadata: None, + checked_expression_types: None, + breakpoint_exclusions: None, + explicit_comptime_values: None, + debug_observations: Vec::new(), + }; + } + }; + let module = checked_program.module(); + let resolve_result = checked_program.resolved(); + let check_result = checked_program.checked(); + let mut all_diagnostics = checked_program.parse_diagnostics().to_vec(); all_diagnostics.extend(resolve_result.diagnostics.clone()); - - let has_resolve_errors = has_error_diagnostics(&all_diagnostics); - if has_resolve_errors { - return BuildResult { - has_errors: true, - diagnostics: all_diagnostics, - source: source.to_string(), - file_path: file_path.to_string(), - reflection_metadata: None, - checked_expression_types: None, - explicit_comptime_values: None, - }; - } - - // Phase 4: Type check - let check_result = check(&parse_result.module, &resolve_result); all_diagnostics.extend(check_result.diagnostics.clone()); - - let has_typecheck_errors = has_error_diagnostics(&all_diagnostics); - if has_typecheck_errors { - return BuildResult { - has_errors: true, - diagnostics: all_diagnostics, - source: source.to_string(), - file_path: file_path.to_string(), - reflection_metadata: None, - checked_expression_types: None, - explicit_comptime_values: None, - }; - } - - // Phase 5: Execute verify blocks at compile time let reflection_metadata = check_result.reflection_metadata.clone(); - let checked_expression_types = Arc::new(expression_type_names(&check_result)); - let (explicit_comptime_values, comptime_diagnostics) = evaluate_explicit_comptime_expressions( - &parse_result.module, + let mut type_names = expression_type_names(check_result, resolve_result); + type_names.resource_program = Some(checked_program.clone()); + let checked_expression_types = Arc::new(type_names); + let evaluation = evaluate_explicit_comptime_expressions_capture( + module, reflection_metadata.clone(), checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), ); - all_diagnostics.extend(comptime_diagnostics); - let verify_diagnostics = run_verify_blocks_with_metadata_and_expression_types( - &parse_result.module, - check_result.reflection_metadata, - checked_expression_types.clone(), + let mut debug_observations = Vec::new(); + append_debug_events( + &mut debug_observations, + DebugPhase::Comptime, + evaluation.debug_events, ); - all_diagnostics.extend(verify_diagnostics); + all_diagnostics.extend(evaluation.diagnostics); + let explicit_comptime_values = Arc::new(evaluation.values); + if !has_error_diagnostics(&all_diagnostics) { + let results = run_verify_blocks_detailed_with_checked_values( + module, + check_result.reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + explicit_comptime_values.clone(), + ); + all_diagnostics.extend(verify_results_to_diagnostics(&results)); + for result in results { + append_debug_events( + &mut debug_observations, + DebugPhase::FrontendVerify, + result.debug_events, + ); + } + } let has_errors = has_error_diagnostics(&all_diagnostics); @@ -468,7 +769,9 @@ pub fn build_source(source: &str, file_path: &str) -> BuildResult { file_path: file_path.to_string(), reflection_metadata: Some(reflection_metadata), checked_expression_types: Some(checked_expression_types), - explicit_comptime_values: Some(Arc::new(explicit_comptime_values)), + breakpoint_exclusions: Some(Arc::new(check_result.breakpoint_exclusions.clone())), + explicit_comptime_values: Some(explicit_comptime_values), + debug_observations, } } @@ -2511,13 +2814,349 @@ pub fn build_file_with_options(path: &Path, options: BuildOptions) -> BuildResul build_file_inner(path, true, options) } +/// Validate a file through the existing build pipeline, then lower its checked +/// program into backend-neutral HIR and MIR. +/// +/// This is a read-only staging entry point. It does not emit an artifact or +/// alter the interpreter-backed build and run paths. +pub fn lower_file_for_backend(path: &Path) -> Result { + lower_file_for_backend_with_options(path, BuildOptions::default()) +} + +/// Lower a valid file with mode-specific build policy. +pub fn lower_file_for_backend_with_options( + path: &Path, + options: BuildOptions, +) -> Result { + lower_file_for_backend_inner(path, options, BackendLoweringMode::Program) +} + +/// Lower checked `verify` and `property` bodies as native test functions. +/// Property inputs are explicit parameters for a future native test runner. +pub fn lower_file_for_native_tests( + path: &Path, +) -> Result { + lower_file_for_backend_inner( + path, + BuildOptions::default(), + BackendLoweringMode::TestBodies, + ) +} + +/// Lower the primary file's checked `verify` bodies and append a native entry +/// that calls them in declaration order. `native_verify_entry` is absent when +/// the primary file has no `verify` blocks. +pub fn lower_file_for_native_verify_suite( + path: &Path, +) -> Result { + lower_file_for_native_verify_suite_with_options(path, BuildOptions::default()) +} + +/// Lower the native verify suite with mode-specific source policy. +pub fn lower_file_for_native_verify_suite_with_options( + path: &Path, + options: BuildOptions, +) -> Result { + lower_file_for_backend_inner(path, options, BackendLoweringMode::VerifySuite) +} + +/// Lower checked property bodies with the interpreter's deterministic `given` +/// cases and append one native entry that executes those cases. +pub fn lower_file_for_native_property_suite( + path: &Path, +) -> Result { + lower_file_for_native_property_suite_with_options(path, BuildOptions::default()) +} + +/// Lower the native property suite with mode-specific source policy. +pub fn lower_file_for_native_property_suite_with_options( + path: &Path, + options: BuildOptions, +) -> Result { + lower_file_for_backend_inner(path, options, BackendLoweringMode::PropertySuite) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BackendLoweringMode { + Program, + TestBodies, + VerifySuite, + PropertySuite, +} + +fn lower_file_for_backend_inner( + path: &Path, + options: BuildOptions, + mode: BackendLoweringMode, +) -> Result { + let file_path = path.display().to_string(); + let source = fs::read_to_string(path).map_err(|error| { + BackendLoweringError::Build(BuildResult { + diagnostics: vec![Diagnostic::error( + 0, + format!("failed to read {}: {error}", path.display()), + Span::new(FileId::new(0), 0, 0), + )], + has_errors: true, + source: String::new(), + file_path: file_path.clone(), + reflection_metadata: None, + checked_expression_types: None, + breakpoint_exclusions: None, + explicit_comptime_values: None, + debug_observations: Vec::new(), + }) + })?; + let build_failure = + |diagnostics: Vec, + reflection_metadata: Option>, + checked_expression_types: Option>, + explicit_comptime_values: Option>, + debug_observations: Vec| { + BackendLoweringError::Build(BuildResult { + diagnostics, + has_errors: true, + source: source.clone(), + file_path: file_path.clone(), + reflection_metadata, + checked_expression_types, + breakpoint_exclusions: None, + explicit_comptime_values, + debug_observations, + }) + }; + + // Keep one coherent compiler session for all backend facts. Re-running a + // successful user-facing build would allocate new DefId and TypeId values + // and would make cross-phase identity depend on two coincidentally equal + // compilations. + let entry_file = FileId::new(0); + let mut parse_result = parse_source_with_query(&source, &file_path); + let source_program_entry = find_main_function(&parse_result.module).map(|(_, main)| main.span); + let mut diagnostics = parse_result.errors.clone(); + if has_error_diagnostics(&diagnostics) { + return Err(build_failure(diagnostics, None, None, None, Vec::new())); + } + + let mut support_modules = discover_stdlib_modules_with_diagnostics(); + support_modules.extend(discover_project_modules_with_diagnostics(path)); + diagnostics.extend(support_modules.diagnostics); + if has_error_diagnostics(&diagnostics) { + return Err(build_failure(diagnostics, None, None, None, Vec::new())); + } + + let mut source_origins = HashMap::from([(entry_file, SourceOrigin::Project)]); + source_origins.extend(support_modules.origins); + prepend_support_modules(&mut parse_result.module, support_modules.modules); + + let primary_span = parse_result.module.span; + parse_result.errors = diagnostics; + let checked_program = match CheckedResourceProgram::prepare( + parse_result, + source_origins, + &[], + CheckOptions { + release: options.release, + }, + ) { + Ok(program) => Arc::new(program), + Err(error) => { + let mut diagnostics = error.diagnostics().unwrap_or(&[]).to_vec(); + if matches!( + &error, + ResourceProgramError::ResolveMetadata { .. } + | ResourceProgramError::CheckMetadata { .. } + ) { + diagnostics.push(Diagnostic::error(0, error.to_string(), primary_span)); + } + return Err(build_failure(diagnostics, None, None, None, Vec::new())); + } + }; + let module = checked_program.module(); + let resolve_result = checked_program.resolved(); + let check_result = checked_program.checked(); + let mut diagnostics = checked_program + .parse_diagnostics() + .iter() + .chain(&resolve_result.diagnostics) + .chain(&check_result.diagnostics) + .cloned() + .collect::>(); + + let reflection_metadata = check_result.reflection_metadata.clone(); + let mut checked_type_names = expression_type_names(check_result, resolve_result); + checked_type_names.resource_program = Some(checked_program.clone()); + let checked_expression_types = Arc::new(checked_type_names); + let evaluation = evaluate_explicit_comptime_expressions_capture( + module, + reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + ); + let mut debug_observations = Vec::new(); + append_debug_events( + &mut debug_observations, + DebugPhase::Comptime, + evaluation.debug_events, + ); + diagnostics.extend(evaluation.diagnostics); + let explicit_comptime_values = Arc::new(evaluation.values); + if !has_error_diagnostics(&diagnostics) { + let results = run_verify_blocks_detailed_with_checked_values( + module, + reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + explicit_comptime_values.clone(), + ); + diagnostics.extend(verify_results_to_diagnostics(&results)); + for result in results { + append_debug_events( + &mut debug_observations, + DebugPhase::FrontendVerify, + result.debug_events, + ); + } + } + if has_error_diagnostics(&diagnostics) { + return Err(build_failure( + diagnostics, + Some(reflection_metadata), + Some(checked_expression_types), + Some(explicit_comptime_values), + debug_observations, + )); + } + + let mut hir = if mode != BackendLoweringMode::Program { + jett_hir::lower_checked_ordinary_program_with_test_bodies(&checked_program) + } else { + jett_hir::lower_checked_ordinary_program(&checked_program) + } + .map_err(|errors| { + BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) + })?; + native_constants::bake_values( + &mut hir, + &explicit_comptime_values, + &check_result.interner, + &check_result.reflection_metadata, + &check_result.method_value_definitions, + ) + .map_err(|errors| { + BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) + })?; + let native_verify_entry = if mode == BackendLoweringMode::VerifySuite { + append_native_verify_suite( + &mut hir, + module, + entry_file, + &check_result.interner, + &checked_program, + ) + .map_err(|error| error.with_debug_observations(&debug_observations))? + } else { + None + }; + let native_property_plan = if mode == BackendLoweringMode::PropertySuite { + if jett_hir::has_reflected_callable_plans(&hir) + || (hir.resource_manifest.kinds().is_empty() + && hir.resource_source.belongs_to(&checked_program)) + { + let cases = jett_comptime::verify::collect_checked_property_cases_with_checked_values( + checked_program.clone(), + explicit_comptime_values.clone(), + ) + .map_err(|errors| { + let mut failures = diagnostics.clone(); + failures.extend(errors); + build_failure( + failures, + Some(reflection_metadata.clone()), + Some(checked_expression_types.clone()), + Some(explicit_comptime_values.clone()), + debug_observations.clone(), + ) + })?; + native_property_cases::append_checked_property_suite( + &mut hir, + &checked_program, + entry_file, + &cases, + ) + .map_err(|errors| { + BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) + })? + } else { + let cases = collect_property_cases_with_checked_values( + module, + reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + explicit_comptime_values.clone(), + ); + native_property_cases::append_property_suite( + &mut hir, + module, + entry_file, + &cases, + &check_result.interner, + &check_result.reflection_metadata, + &check_result.method_value_definitions, + ) + .map_err(|errors| { + BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) + })? + } + } else { + None + }; + let native_property_entry = native_property_plan.as_ref().map(|plan| plan.entry); + let program_entry = lowered_program_entry(&hir, source_program_entry) + .map_err(|error| error.with_debug_observations(&debug_observations))?; + jett_hir::complete_value_conversions(&mut hir, &check_result.interner).map_err(|errors| { + BackendLoweringError::Hir(errors).with_debug_observations(&debug_observations) + })?; + let mir = jett_mir::lower(&hir, &check_result.interner).map_err(|errors| { + BackendLoweringError::Mir(errors).with_debug_observations(&debug_observations) + })?; + jett_mir::validate(&mir).map_err(|errors| { + BackendLoweringError::MirValidation(errors).with_debug_observations(&debug_observations) + })?; + + Ok(BackendLoweringResult { + diagnostics, + source, + hir, + mir, + program_entry, + native_verify_entry, + native_property_entry, + native_property_plan, + interner: BackendTypes(checked_program.clone()), + source_origins: checked_program.source_origins().clone(), + reflection_metadata, + checked_expression_types, + explicit_comptime_values, + debug_observations, + }) +} + fn build_file_inner(path: &Path, include_project: bool, options: BuildOptions) -> BuildResult { + prepare_reference_file(path, include_project, options).build +} + +fn prepare_reference_file( + path: &Path, + include_project: bool, + options: BuildOptions, +) -> PreparedReference { let file_path_str = path.display().to_string(); let source = match fs::read_to_string(path) { Ok(s) => s, Err(e) => { - return BuildResult { + return PreparedReference::rejected(BuildResult { diagnostics: vec![Diagnostic::error( 0, format!("failed to read {}: {}", path.display(), e), @@ -2528,8 +3167,10 @@ fn build_file_inner(path: &Path, include_project: bool, options: BuildOptions) - file_path: file_path_str, reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, - }; + debug_observations: Vec::new(), + }); } }; @@ -2538,19 +3179,26 @@ fn build_file_inner(path: &Path, include_project: bool, options: BuildOptions) - // Phase 1+2: Lex + Parse (parse internally calls tokenize) let mut parse_result = parse_source_with_query(&source, &file_path_str); all_diagnostics.extend(parse_result.errors.clone()); + let entry = + find_main_function(&parse_result.module).map(|(namespace, main)| PreparedReferenceEntry { + namespace, + span: main.span, + }); // If there are parse errors, stop here — resolve/typecheck won't produce useful results let has_parse_errors = has_error_diagnostics(&all_diagnostics); if has_parse_errors { - return BuildResult { + return PreparedReference::rejected(BuildResult { has_errors: true, diagnostics: all_diagnostics, source, file_path: file_path_str, reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, - }; + debug_observations: Vec::new(), + }); } // Multi-file: prepend stdlib and sibling project modules so @@ -2562,84 +3210,120 @@ fn build_file_inner(path: &Path, include_project: bool, options: BuildOptions) - } all_diagnostics.extend(support_modules.diagnostics); if has_error_diagnostics(&all_diagnostics) { - return BuildResult { + return PreparedReference::rejected(BuildResult { has_errors: true, diagnostics: all_diagnostics, source, file_path: file_path_str, reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, - }; + debug_observations: Vec::new(), + }); } prepend_support_modules(&mut parse_result.module, support_modules.modules); - // Phase 3: Resolve names - let resolve_result = resolve(&parse_result.module); - all_diagnostics.extend(resolve_result.diagnostics.clone()); - - let has_resolve_errors = has_error_diagnostics(&all_diagnostics); - if has_resolve_errors { - return BuildResult { - has_errors: true, - diagnostics: all_diagnostics, - source, - file_path: file_path_str, - reflection_metadata: None, - checked_expression_types: None, - explicit_comptime_values: None, - }; - } - - // Phase 4: Type check - let check_result = check_with_options( - &parse_result.module, - &resolve_result, + let primary_span = parse_result.module.span; + let mut source_origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + source_origins.extend(support_modules.origins); + // Discovery observations retain their position after the primary parse. + parse_result.errors = all_diagnostics; + let program = match CheckedResourceProgram::prepare( + parse_result, + source_origins, + &[], CheckOptions { release: options.release, }, - ); - all_diagnostics.extend(check_result.diagnostics.clone()); - - let has_typecheck_errors = has_error_diagnostics(&all_diagnostics); - if has_typecheck_errors { - return BuildResult { - has_errors: true, - diagnostics: all_diagnostics, - source, - file_path: file_path_str, - reflection_metadata: None, - checked_expression_types: None, - explicit_comptime_values: None, - }; - } + ) { + Ok(program) => Arc::new(program), + Err(error) => { + let mut diagnostics = error.diagnostics().unwrap_or(&[]).to_vec(); + if matches!( + &error, + ResourceProgramError::ResolveMetadata { .. } + | ResourceProgramError::CheckMetadata { .. } + ) { + diagnostics.push(Diagnostic::error(0, error.to_string(), primary_span)); + } + return PreparedReference::rejected(BuildResult { + has_errors: true, + diagnostics, + source, + file_path: file_path_str, + reflection_metadata: None, + checked_expression_types: None, + breakpoint_exclusions: None, + explicit_comptime_values: None, + debug_observations: Vec::new(), + }); + } + }; + let check_result = program.checked(); + let resolve_result = program.resolved(); + let mut all_diagnostics = program + .parse_diagnostics() + .iter() + .chain(&resolve_result.diagnostics) + .chain(&check_result.diagnostics) + .cloned() + .collect::>(); // Phase 5: Execute verify blocks at compile time let reflection_metadata = check_result.reflection_metadata.clone(); - let checked_expression_types = Arc::new(expression_type_names(&check_result)); - let (explicit_comptime_values, comptime_diagnostics) = evaluate_explicit_comptime_expressions( - &parse_result.module, + let mut checked_type_names = expression_type_names(check_result, resolve_result); + checked_type_names.resource_program = Some(program.clone()); + let checked_expression_types = Arc::new(checked_type_names); + let evaluation = evaluate_explicit_comptime_expressions_capture( + program.module(), reflection_metadata.clone(), checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), ); - all_diagnostics.extend(comptime_diagnostics); - let verify_diagnostics = run_verify_blocks_with_metadata_and_expression_types( - &parse_result.module, - check_result.reflection_metadata, - checked_expression_types.clone(), + let mut debug_observations = Vec::new(); + append_debug_events( + &mut debug_observations, + DebugPhase::Comptime, + evaluation.debug_events, ); - all_diagnostics.extend(verify_diagnostics); + all_diagnostics.extend(evaluation.diagnostics); + let explicit_comptime_values = Arc::new(evaluation.values); + if !has_error_diagnostics(&all_diagnostics) { + let results = run_verify_blocks_detailed_with_checked_values( + program.module(), + check_result.reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + explicit_comptime_values.clone(), + ); + all_diagnostics.extend(verify_results_to_diagnostics(&results)); + for result in results { + append_debug_events( + &mut debug_observations, + DebugPhase::FrontendVerify, + result.debug_events, + ); + } + } let has_errors = has_error_diagnostics(&all_diagnostics); - BuildResult { + let build = BuildResult { has_errors, diagnostics: all_diagnostics, source, file_path: file_path_str, reflection_metadata: Some(reflection_metadata), checked_expression_types: Some(checked_expression_types), - explicit_comptime_values: Some(Arc::new(explicit_comptime_values)), + breakpoint_exclusions: Some(Arc::new(check_result.breakpoint_exclusions.clone())), + explicit_comptime_values: Some(explicit_comptime_values), + debug_observations, + }; + PreparedReference { + build, + program: Some(program), + entry, } } @@ -2773,12 +3457,113 @@ fn query_diagnostic_sources( sources } -fn expression_type_names(check_result: &CheckResult) -> HashMap { - check_result - .type_map +fn expression_type_names( + check_result: &CheckResult, + resolve_result: &jett_resolve::ResolveResult, +) -> CheckedExpressionTypes { + let (mut names, bindings) = checked_body_type_names( + &check_result.type_map, + &check_result.comptime_type_bindings, + &check_result.interner, + ); + // Generic body facts belong only to their selected instance, even when + // the checker has seen just one instance so far. Compiler-owned facades + // can execute other interpreter source instances without checking them. + for instance in &check_result.generic_function_instantiations { + for span in instance.type_map.keys() { + names.remove(span); + } + remove_scoped_expression_names(&mut names, &instance.comptime_type_bindings); + } + let mut functions: HashMap>> = HashMap::new(); + for instance in &check_result.generic_function_instantiations { + let facts = &instance.specialization; + let (expressions, bindings) = checked_body_type_names( + &instance.type_map, + &instance.comptime_type_bindings, + &check_result.interner, + ); + functions + .entry(resolve_result.scope_table.def(instance.definition).span) + .or_default() + .push(Arc::new(CheckedFunctionTypes { + bindings, + type_arguments: instance + .concrete_args + .iter() + .map(|ty| check_result.interner.type_name(*ty)) + .collect(), + type_argument_reflections: facts.type_argument_reflections.clone(), + type_info_kinds: facts.type_info_kinds.clone(), + type_info_primitives: facts.type_info_primitives.clone(), + type_kind_values: facts.type_kind_values.clone(), + type_primitive_values: facts.type_primitive_values.clone(), + expressions: Arc::new(expressions), + })); + } + CheckedExpressionTypes { + resource_program: None, + bindings, + expressions: names, + functions, + } +} + +fn checked_body_type_names( + types: &HashMap, + bindings: &HashMap>, + interner: &jett_types::TypeInterner, +) -> (HashMap, CheckedScopedBindings) { + let mut expressions = types .iter() - .map(|(span, ty_id)| (*span, check_result.interner.type_name(*ty_id))) - .collect() + .map(|(span, ty)| (*span, interner.type_name(*ty))) + .collect(); + remove_scoped_expression_names(&mut expressions, bindings); + // Legacy maps also contain descendant entries. Export each binding only + // under its lexical parent, never as a sibling with the last body's facts. + let nested = bindings + .values() + .flatten() + .flat_map(|binding| binding.body.comptime_type_bindings.keys().copied()) + .collect::>(); + let scopes = bindings + .iter() + .filter(|(span, _)| !nested.contains(span)) + .map(|(span, candidates)| { + ( + *span, + candidates + .iter() + .map(|binding| { + let (expressions, bindings) = checked_body_type_names( + &binding.body.type_map, + &binding.body.comptime_type_bindings, + interner, + ); + Arc::new(CheckedScopedTypes { + bound_type: interner.type_name(binding.bound_type), + reflection: Some(binding.reflection.clone()), + expressions, + bindings, + }) + }) + .collect(), + ) + }) + .collect(); + (expressions, scopes) +} + +fn remove_scoped_expression_names( + expressions: &mut HashMap, + bindings: &HashMap>, +) { + for binding in bindings.values().flatten() { + for span in binding.body.type_map.keys() { + expressions.remove(span); + } + remove_scoped_expression_names(expressions, &binding.body.comptime_type_bindings); + } } fn update_current_namespace( @@ -2813,6 +3598,140 @@ fn find_main_function(module: &Module) -> Option<(Option, &FunctionDef)> None } +fn lowered_program_entry( + hir: &jett_hir::Program, + source_entry: Option, +) -> Result, BackendLoweringError> { + let Some(source_entry) = source_entry else { + return Ok(None); + }; + let mut matches = hir + .functions + .iter() + .filter(|function| function.span == source_entry); + let Some(entry) = matches.next() else { + return Err(BackendLoweringError::Hir(vec![jett_hir::LowerError { + span: source_entry, + message: "primary source program entry has no checked HIR function identity" + .to_string(), + }])); + }; + if matches.next().is_some() { + return Err(BackendLoweringError::Hir(vec![jett_hir::LowerError { + span: source_entry, + message: "primary source program entry has multiple checked HIR function identities" + .to_string(), + }])); + } + Ok(Some(entry.id)) +} + +fn append_native_verify_suite( + hir: &mut jett_hir::Program, + module: &Module, + entry_file: FileId, + types: &jett_types::TypeInterner, + checked: &Arc, +) -> Result, BackendLoweringError> { + if hir.resource_source.manifest().is_some() { + return jett_hir::append_checked_native_verify_suite(hir, checked, entry_file) + .map_err(BackendLoweringError::Hir); + } + let mut verifies = Vec::new(); + let mut suite_namespace = None; + for item in &module.items { + let Item::Verify(verify) = item else { + continue; + }; + if verify.span.file != entry_file { + continue; + } + let mut matches = hir.functions.iter().filter(|function| { + function.span == verify.span + && function.identity.declaration.kind == jett_hir::DeclarationKind::Verify + }); + let Some(function) = matches.next() else { + return Err(BackendLoweringError::Hir(vec![jett_hir::LowerError { + span: verify.span, + message: "checked verify body has no exact HIR function".to_owned(), + }])); + }; + if matches.next().is_some() { + return Err(BackendLoweringError::Hir(vec![jett_hir::LowerError { + span: verify.span, + message: "checked verify body has multiple HIR functions".to_owned(), + }])); + } + if suite_namespace.is_none() { + suite_namespace = Some(function.identity.declaration.namespace.clone()); + } + verifies.push((function.id, function.span)); + } + let Some(span) = verifies.first().map(|(_, span)| *span) else { + return Ok(None); + }; + let id = jett_hir::FunctionId::new(hir.functions.len() as u32); + let statements = verifies + .into_iter() + .map(|(function, span)| { + let ownership = jett_hir::CallOwnership::generated( + jett_hir::GeneratedOperation::NativeSuite { function }, + &[], + &[], + &[], + jett_types::TypeInterner::NOTHING, + &[], + types, + ) + .map_err(|message| { + BackendLoweringError::Hir(vec![jett_hir::LowerError { span, message }]) + })?; + Ok(jett_hir::Statement { + kind: jett_hir::StatementKind::Expression(jett_hir::Expression { + kind: jett_hir::ExpressionKind::Call { + function, + args: Vec::new(), + evaluation_order: Vec::new(), + ownership, + }, + ty: jett_types::TypeInterner::NOTHING, + span, + }), + span, + }) + }) + .collect::, BackendLoweringError>>()?; + hir.functions.push(jett_hir::Function { + id, + debug_kind: jett_hir::FunctionDebugKind::named( + suite_namespace.as_deref().unwrap_or_default(), + &format!("__native_verify_suite:{}", span.start), + ), + identity: jett_hir::FunctionIdentity { + declaration: jett_hir::DeclarationId { + origin: SourceOrigin::Project, + namespace: suite_namespace.unwrap_or_default(), + name: format!("__native_verify_suite:{}", span.start), + kind: jett_hir::DeclarationKind::Verify, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: Vec::new(), + capture_count: 0, + return_type: jett_types::TypeInterner::NOTHING, + locals: Vec::new(), + body: jett_hir::Block { statements, span }, + span, + }); + Ok(Some(id)) +} + fn prepend_support_modules(module: &mut Module, support_modules: Vec) { if support_modules.is_empty() { return; @@ -2832,22 +3751,33 @@ fn discover_stdlib_modules() -> Vec { } fn discover_stdlib_modules_with_diagnostics() -> DiscoveredModules { - discover_modules_in_dir(&stdlib_root(), None, STDLIB_FILE_ID_START, "stdlib") + discover_modules_in_dir( + &stdlib_root(), + None, + STDLIB_FILE_ID_START, + "stdlib", + SourceOrigin::Stdlib, + ) } fn stdlib_root() -> PathBuf { + if let Some(path) = std::env::var_os("JETT_STDLIB_DIR") { + return PathBuf::from(path); + } + if let Ok(executable) = std::env::current_exe() { + if let Some(prefix) = executable.parent().and_then(Path::parent) { + let installed = prefix.join("lib").join("jett").join("stdlib"); + if installed.is_dir() { + return installed; + } + } + } PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") .join("stdlib") } -/// Discover and parse all sibling .jett files in the project (if a jett.proj exists). -/// Returns parsed modules for files other than the entry file. -fn discover_project_modules(entry_path: &Path) -> Vec { - discover_project_modules_with_diagnostics(entry_path).modules -} - fn discover_project_modules_with_diagnostics(entry_path: &Path) -> DiscoveredModules { let canon = entry_path.canonicalize().ok(); let project_root = find_project_root(entry_path).ok(); @@ -2857,9 +3787,10 @@ fn discover_project_modules_with_diagnostics(entry_path: &Path) -> DiscoveredMod diagnostics: Vec::new(), files: HashMap::new(), sources: HashMap::new(), + origins: HashMap::new(), }; }; - discover_modules_in_dir(&root, canon.as_deref(), 1, "project") + discover_modules_in_dir(&root, canon.as_deref(), 1, "project", SourceOrigin::Project) } fn discover_query_project_modules_with_diagnostics(start_dir: &Path) -> DiscoveredModules { @@ -2869,9 +3800,10 @@ fn discover_query_project_modules_with_diagnostics(start_dir: &Path) -> Discover diagnostics: Vec::new(), files: HashMap::new(), sources: HashMap::new(), + origins: HashMap::new(), }; }; - discover_modules_in_dir(&root, None, 1, "project") + discover_modules_in_dir(&root, None, 1, "project", SourceOrigin::Project) } fn discover_modules_in_dir( @@ -2879,6 +3811,7 @@ fn discover_modules_in_dir( skip_canon: Option<&Path>, start_file_id: u32, module_kind: &str, + origin: SourceOrigin, ) -> DiscoveredModules { let mut files = Vec::new(); if let Err(err) = collect_jett_files(root, &mut files) { @@ -2894,6 +3827,7 @@ fn discover_modules_in_dir( )], files: HashMap::new(), sources: HashMap::new(), + origins: HashMap::new(), }; } if module_kind == "stdlib" { @@ -2916,6 +3850,7 @@ fn discover_modules_in_dir( let mut diagnostics = Vec::new(); let mut module_files = HashMap::new(); let mut module_sources = HashMap::new(); + let mut module_origins = HashMap::new(); for (idx, file_path) in files.iter().enumerate() { // Skip the entry file when parsing project siblings. let should_skip = skip_canon @@ -2944,6 +3879,7 @@ fn discover_modules_in_dir( .unwrap_or_else(|_| file_path.clone()); module_files.insert(file_id, display_path); module_sources.insert(file_id, source.clone()); + module_origins.insert(file_id, origin.clone()); let parsed = parse(&source, file_id); if has_error_diagnostics(&parsed.errors) { for mut diagnostic in parsed.errors { @@ -2965,6 +3901,7 @@ fn discover_modules_in_dir( diagnostics, files: module_files, sources: module_sources, + origins: module_origins, } } @@ -2986,8 +3923,8 @@ pub fn run_file(path: &Path) -> Result<(), String> { .map(|_| ()) } -/// Run a .jett file and capture runtime stdout produced by `Stdout.write`, -/// `print`, and `println`. +/// Run a .jett file and capture capability-backed `Stdout.write` output. +/// This legacy helper silently discards debug observations. pub fn run_file_capture_stdout(path: &Path) -> Result { run_file_with_options( path, @@ -3003,7 +3940,7 @@ pub fn run_file_capture_stdout(path: &Path) -> Result { .map(|output| output.stdout) } -/// Run a .jett file and capture stdout plus trace/breakpoint debug lines. +/// Run a .jett file and capture application stdout and typed debug observations. pub fn run_file_capture_output(path: &Path) -> Result { run_file_with_options( path, @@ -3018,6 +3955,80 @@ pub fn run_file_capture_output(path: &Path) -> Result { ) } +/// Run a file in the reference interpreter and retain partial captured output +/// when execution fails. +/// +/// Unlike the legacy run APIs, the error carries application stdout and debug +/// lines produced before the terminal failure. Its display text remains the +/// exact legacy error string. +pub fn run_file_capture_outcome(path: &Path) -> Result { + run_file_with_captured_options( + path, + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: None, + clock_test_samples: None, + environment_test_snapshot: None, + graphics_test_events: None, + }, + ) +} + +/// Capture a scripted Random run, retaining partial output on runtime failure. +pub fn run_file_capture_outcome_with_random_test_samples( + path: &Path, + samples: Vec, +) -> Result { + run_file_with_captured_options( + path, + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: Some(samples), + clock_test_samples: None, + environment_test_snapshot: None, + graphics_test_events: None, + }, + ) +} + +/// Capture an injected Environment launch snapshot with partial failure output. +pub fn run_file_capture_outcome_with_environment_test_snapshot( + path: &Path, + snapshot: EnvironmentTestSnapshot, +) -> Result { + run_file_with_captured_options( + path, + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: None, + clock_test_samples: None, + environment_test_snapshot: Some(snapshot), + graphics_test_events: None, + }, + ) +} + +/// Capture a scripted Clock run, retaining partial output on runtime failure. +pub fn run_file_capture_outcome_with_clock_test_samples( + path: &Path, + samples: Vec, +) -> Result { + run_file_with_captured_options( + path, + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: None, + clock_test_samples: Some(samples), + environment_test_snapshot: None, + graphics_test_events: None, + }, + ) +} + /// Run with a backend-neutral scripted Random provider for deterministic tests. pub fn run_file_with_random_test_samples( path: &Path, @@ -3133,6 +4144,24 @@ pub fn run_file_with_environment_test_snapshot( } /// Run with deterministic graphics input and real scene validation, without a window. +pub fn run_file_capture_outcome_with_graphics_test_events( + path: &Path, + events: Vec, +) -> Result { + run_file_with_captured_options( + path, + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: None, + clock_test_samples: None, + environment_test_snapshot: None, + graphics_test_events: Some(events), + }, + ) +} + +/// Run with deterministic graphics input and scene validation, without a window. pub fn run_file_capture_output_with_graphics_test_events( path: &Path, events: Vec, @@ -3151,6 +4180,13 @@ pub fn run_file_capture_output_with_graphics_test_events( } fn run_file_with_options(path: &Path, options: RunOptions) -> Result { + run_file_with_captured_options(path, options).map_err(RunFailure::into_message) +} + +fn run_file_with_captured_options( + path: &Path, + options: RunOptions, +) -> Result { if runtime_requires_caller_thread(path, &options, cfg!(target_os = "macos")) { // AppKit must create and pump windows on the process's main thread. // In particular, do not block that thread joining a graphics worker: @@ -3188,9 +4224,44 @@ fn runtime_requires_caller_thread(path: &Path, options: &RunOptions, macos: bool }) } -fn run_file_inner(path: &Path, options: RunOptions) -> Result { - let build = build_file(path); +fn run_file_inner(path: &Path, options: RunOptions) -> Result { + run_prepared_reference( + prepare_reference_file(path, true, BuildOptions::default()), + options, + ) +} +fn run_prepared_reference( + prepared: PreparedReference, + options: RunOptions, +) -> Result { + let PreparedReference { + build, + program, + entry, + } = prepared; + + if options.emit_runtime_debug { + use std::io::Write; + let mut stderr = std::io::stderr().lock(); + for observation in &build.debug_observations { + stderr + .write_all(observation.event.text.as_bytes()) + .map_err(|error| { + RunFailure::with_frontend( + format!("failed to write compiler debug output: {error}"), + &build.debug_observations, + ) + })?; + } + stderr.flush().map_err(|error| { + RunFailure::with_frontend( + format!("failed to write compiler debug output: {error}"), + &build.debug_observations, + ) + })?; + } + let frontend_failure = |message| RunFailure::with_frontend(message, &build.debug_observations); if build.has_errors { let errors: Vec = build .diagnostics @@ -3198,24 +4269,43 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result .filter(|d| d.severity == jett_diagnostics::Severity::Error) .map(|d| format!("{}: {}", d.code, d.message)) .collect(); - return Err(format!( + return Err(frontend_failure(format!( "cannot run — compilation errors:\n{}", errors.join("\n") - )); + ))); } - // Parse again to get the module for interpretation - let source = fs::read_to_string(path) - .map_err(|e| format!("failed to read {}: {}", path.display(), e))?; - let file_id = FileId::new(0); - let parse_result = parse(&source, file_id); - let module = parse_result.module; - - let Some((main_namespace, main_func)) = find_main_function(&module) else { - return Err("runtime error: no `main` function found".to_string()); - }; + let program = program.ok_or_else(|| { + frontend_failure( + "runtime error: successful compilation has no retained checked program".to_string(), + ) + })?; + let entry = entry + .ok_or_else(|| frontend_failure("runtime error: no `main` function found".to_string()))?; + let main_func = program + .module() + .items + .iter() + .find_map(|item| { + if let Item::Function(function) = item + && entry.span.file == FileId::new(0) + && function.span == entry.span + && function.name.name == "main" + { + Some(function) + } else { + None + } + }) + .ok_or_else(|| { + frontend_failure( + "runtime error: retained primary entry does not match the checked program" + .to_string(), + ) + })?; + let main_namespace = entry.namespace; - let main_args = default_runtime_args_for_main(main_func)?; + let main_args = default_runtime_args_for_main(main_func).map_err(&frontend_failure)?; use jett_comptime::interpreter::Interpreter; let mut interp = if options.emit_runtime_debug { @@ -3223,6 +4313,12 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result } else { Interpreter::new() }; + interp + .install_checked_resource_program( + program.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) + .map_err(&frontend_failure)?; if main_func .params .iter() @@ -3231,7 +4327,9 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result if let Some(samples) = options.random_test_samples.clone() { interp.set_random_test_samples(samples); } else { - interp.initialize_random_provider()?; + interp + .initialize_random_provider() + .map_err(&frontend_failure)?; } } if main_func @@ -3253,11 +4351,11 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result if let Some(snapshot) = options.environment_test_snapshot.clone() { interp .set_environment_test_snapshot(snapshot) - .map_err(|error| format!("runtime error: {error}"))?; + .map_err(|error| frontend_failure(format!("runtime error: {error}")))?; } else { interp .initialize_environment_provider() - .map_err(|error| format!("runtime error: {error}"))?; + .map_err(|error| frontend_failure(format!("runtime error: {error}")))?; } } if main_func @@ -3274,6 +4372,9 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result if let Some(metadata) = build.reflection_metadata.clone() { interp.set_reflection_metadata(metadata); } + if let Some(exclusions) = build.breakpoint_exclusions.clone() { + interp.set_breakpoint_exclusions(exclusions); + } if let Some(expression_types) = build.checked_expression_types.clone() { interp.set_checked_expression_types(expression_types); } @@ -3284,33 +4385,35 @@ fn run_file_inner(path: &Path, options: RunOptions) -> Result interp.enable_stdout_capture(); } - // Register compiler-shipped stdlib modules before project and entry files. - for module in discover_stdlib_modules() { - register_module_items(&mut interp, &module); - } - - // Register items from sibling project files first (so they're available to main file). - let sibling_modules = discover_project_modules(path); - for module in &sibling_modules { - register_module_items(&mut interp, module); - } + // The exact checked merged module preserves support -> project -> primary order. + register_module_items(&mut interp, program.module()); - // Register items from the entry file (may override sibling definitions). - register_module_items(&mut interp, &module); - - // Call main(). Scripted providers are exact expectations: a successful - // deterministic run must consume every supplied operation sample. - match interp.call_function_in_namespace(main_namespace.as_deref(), "main", main_args) { - Ok(_) => { - reject_unconsumed_test_samples("Random", interp.random_test_samples_remaining())?; - reject_unconsumed_test_samples("Clock", interp.clock_test_samples_remaining())?; - reject_unconsumed_test_samples("Graphics", interp.graphics_test_events_remaining())?; - Ok(RunOutput { - stdout: interp.take_stdout_output(), - debug_output: interp.take_debug_output(), + // The checked entry's arguments were granted by default_runtime_args_for_main; + // no arbitrary host aggregate enters this trusted source-body boundary. + // Scripted providers are exact expectations: a successful deterministic run + // must consume every supplied operation sample. + let terminal_result = match interp.call_checked_function_in_namespace( + main_namespace.as_deref(), + "main", + main_args, + ) { + Ok(_) => reject_unconsumed_test_samples("Random", interp.random_test_samples_remaining()) + .and_then(|()| { + reject_unconsumed_test_samples("Clock", interp.clock_test_samples_remaining()) }) - } - Err(e) => Err(format!("runtime error: {}", e)), + .and_then(|()| { + reject_unconsumed_test_samples("Graphics", interp.graphics_test_events_remaining()) + }), + Err(error) => Err(format!("runtime error: {error}")), + }; + let output = RunOutput { + stdout: interp.take_stdout_output(), + debug_events: interp.take_debug_events(), + frontend_debug_observations: build.debug_observations, + }; + match terminal_result { + Ok(()) => Ok(output), + Err(message) => Err(RunFailure::with_output(message, output)), } } @@ -3427,12 +4530,14 @@ pub fn format_file_in_place(path: &Path) -> Result<(), String> { // --------------------------------------------------------------------------- /// A single block result in a test run. +#[derive(Debug, Clone)] pub struct TestBlockResult { pub name: String, pub passed: bool, pub error: Option, pub is_property: bool, pub iterations: Option, + pub debug_events: Vec, pub line: u32, pub column: u32, pub end_line: u32, @@ -3440,6 +4545,7 @@ pub struct TestBlockResult { } /// Result of running `jett test` on a single file. +#[derive(Debug, Clone)] pub struct TestResult { pub total: usize, pub passed: usize, @@ -3448,8 +4554,43 @@ pub struct TestResult { pub file_path: String, /// Per-block results. pub blocks: Vec, + /// Explicit evaluation observations; block events live only in `blocks`. + pub debug_observations: Vec, +} + +/// A fatal test setup/evaluation failure with actual observations retained. +#[derive(Debug, Clone)] +pub struct TestFailure { + pub message: String, + pub file_path: String, + pub debug_observations: Vec, + /// Earlier files, including their block outcomes, in actual execution order. + pub completed_files: Vec, +} + +impl TestFailure { + fn new(path: &Path, message: impl Into) -> Self { + Self { + message: message.into(), + file_path: path.display().to_string(), + debug_observations: Vec::new(), + completed_files: Vec::new(), + } + } + + fn into_message(self) -> String { + self.message + } } +impl std::fmt::Display for TestFailure { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for TestFailure {} + /// Result of running `jett test` across an entire project. pub struct ProjectTestResult { pub total_files: usize, @@ -3472,6 +4613,7 @@ pub struct BundleResult { pub project_root: String, pub output_path: String, pub files: Vec, + pub debug_observations: Vec, } /// A bundle failure, optionally retaining validation or ordering diagnostics @@ -3479,6 +4621,7 @@ pub struct BundleResult { pub struct BundleError { message: String, details: Option, + debug_observations: Vec, } enum BundleErrorDetails { @@ -3487,6 +4630,20 @@ enum BundleErrorDetails { } impl BundleError { + pub fn debug_observations(&self) -> &[DebugObservation] { + match self.diagnostic_result() { + Some(result) => &result.debug_observations, + None => &self.debug_observations, + } + } + + fn with_debug_observations(mut self, observations: &[DebugObservation]) -> Self { + if self.debug_observations().is_empty() { + self.debug_observations = observations.to_vec(); + } + self + } + fn from_validation(validation: BuildResult) -> Option { if !validation.has_errors { return None; @@ -3495,6 +4652,7 @@ impl BundleError { Some(Self { message: format!("candidate bundle failed validation:\n{}", errors.join("\n")), details: Some(BundleErrorDetails::Validation(validation)), + debug_observations: Vec::new(), }) } @@ -3528,6 +4686,7 @@ impl From for BundleError { Self { message, details: None, + debug_observations: Vec::new(), } } } @@ -3548,8 +4707,13 @@ impl std::error::Error for BundleError {} /// Parse a .jett file and run all verify blocks, reporting per-block results. pub fn test_file(path: &Path) -> Result { + test_file_capture_outcome(path).map_err(TestFailure::into_message) +} + +pub fn test_file_capture_outcome(path: &Path) -> Result { + let fail = |message| TestFailure::new(path, message); let source = fs::read_to_string(path) - .map_err(|e| format!("failed to read {}: {}", path.display(), e))?; + .map_err(|e| fail(format!("failed to read {}: {}", path.display(), e)))?; let file_id = FileId::new(0); let mut parse_result = parse(&source, file_id); @@ -3566,7 +4730,7 @@ pub fn test_file(path: &Path) -> Result { .filter(|d| d.severity == jett_diagnostics::Severity::Error) .map(|d| format!("{}: {}", d.code, d.message)) .collect(); - return Err(format!("parse errors:\n{}", msgs.join("\n"))); + return Err(fail(format!("parse errors:\n{}", msgs.join("\n")))); } let mut support_modules = discover_stdlib_modules_with_diagnostics(); @@ -3576,33 +4740,75 @@ pub fn test_file(path: &Path) -> Result { &project_modules.diagnostics, )); if !support_errors.is_empty() { - return Err(format!( + return Err(fail(format!( "support parse errors:\n{}", support_errors.join("\n") - )); + ))); } + let mut source_origins = HashMap::from([(file_id, SourceOrigin::Project)]); + source_origins.extend(support_modules.origins); + source_origins.extend(project_modules.origins.clone()); strip_test_items_from_support_modules(&mut support_modules.modules); strip_test_items_from_support_modules(&mut project_modules.modules); - parse_result.module = assemble_test_project_module(path, parse_result.module, project_modules)?; + parse_result.module = + assemble_test_project_module(path, parse_result.module, project_modules).map_err(&fail)?; prepend_support_modules(&mut parse_result.module, support_modules.modules); - let resolve_result = resolve(&parse_result.module); - let resolve_errors = error_messages_from_diagnostics(&resolve_result.diagnostics); - if !resolve_errors.is_empty() { - return Err(format!("resolution errors:\n{}", resolve_errors.join("\n"))); - } - - let check_result = check(&parse_result.module, &resolve_result); - let type_errors = error_messages_from_diagnostics(&check_result.diagnostics); - if !type_errors.is_empty() { - return Err(format!("type errors:\n{}", type_errors.join("\n"))); - } - - let checked_expression_types = Arc::new(expression_type_names(&check_result)); - let results = run_verify_blocks_detailed_with_metadata_and_expression_types( - &parse_result.module, - Some(check_result.reflection_metadata), - Some(checked_expression_types), + let primary_span = parse_result.module.span; + let program = + CheckedResourceProgram::prepare(parse_result, source_origins, &[], CheckOptions::default()) + .map(Arc::new) + .map_err(|error| { + let phase = match &error { + ResourceProgramError::ParseDiagnostics(_) => "parse errors", + ResourceProgramError::ResolveDiagnostics(_) + | ResourceProgramError::ResolveMetadata { .. } => "resolution errors", + ResourceProgramError::CheckDiagnostics(_) + | ResourceProgramError::CheckMetadata { .. } => "type errors", + }; + let mut diagnostics = error.diagnostics().unwrap_or(&[]).to_vec(); + if matches!( + &error, + ResourceProgramError::ResolveMetadata { .. } + | ResourceProgramError::CheckMetadata { .. } + ) { + diagnostics.push(Diagnostic::error(0, error.to_string(), primary_span)); + } + fail(format!( + "{phase}:\n{}", + error_messages_from_diagnostics(&diagnostics).join("\n") + )) + })?; + let module = program.module(); + let resolve_result = program.resolved(); + let check_result = program.checked(); + let mut checked_type_names = expression_type_names(check_result, resolve_result); + checked_type_names.resource_program = Some(program.clone()); + let checked_expression_types = Arc::new(checked_type_names); + let evaluation = evaluate_explicit_comptime_expressions_capture( + module, + check_result.reflection_metadata.clone(), + checked_expression_types.clone(), + Arc::new(check_result.breakpoint_exclusions.clone()), + ); + let mut debug_observations = Vec::new(); + append_debug_events( + &mut debug_observations, + DebugPhase::Comptime, + evaluation.debug_events, + ); + let errors = error_messages_from_diagnostics(&evaluation.diagnostics); + if !errors.is_empty() { + let mut failure = fail(format!("comptime errors:\n{}", errors.join("\n"))); + failure.debug_observations = debug_observations; + return Err(failure); + } + let results = run_verify_blocks_detailed_with_checked_values( + module, + check_result.reflection_metadata.clone(), + checked_expression_types, + Arc::new(check_result.breakpoint_exclusions.clone()), + Arc::new(evaluation.values), ); let total = results.len(); @@ -3620,6 +4826,7 @@ pub fn test_file(path: &Path) -> Result { error: r.error, is_property: r.is_property, iterations: r.iterations, + debug_events: r.debug_events, line: line as u32, column: column as u32, end_line: end_line as u32, @@ -3634,6 +4841,7 @@ pub fn test_file(path: &Path) -> Result { failed, file_path: path.display().to_string(), blocks, + debug_observations, }) } @@ -3811,23 +5019,34 @@ fn existing_paths_share_file_identity(_left: &Path, _right: &Path) -> bool { /// to find `jett.proj`, then collects all `.jett` files in the project) and /// run verify blocks in each one. pub fn test_project(start_dir: &Path) -> Result { - let project_dir = find_project_root(start_dir)?; + test_project_capture_outcome(start_dir).map_err(TestFailure::into_message) +} + +pub fn test_project_capture_outcome(start_dir: &Path) -> Result { + let fail = |message| TestFailure::new(start_dir, message); + let project_dir = find_project_root(start_dir).map_err(&fail)?; let mut files = Vec::new(); collect_jett_files(&project_dir, &mut files) - .map_err(|e| format!("error scanning project: {e}"))?; + .map_err(|e| fail(format!("error scanning project: {e}")))?; if files.is_empty() { - return Err(format!( + return Err(fail(format!( "no .jett files found in project at {}", project_dir.display() - )); + ))); } files.sort(); let mut file_results = Vec::new(); for file_path in &files { - file_results.push(test_file(file_path)?); + match test_file_capture_outcome(file_path) { + Ok(result) => file_results.push(result), + Err(mut failure) => { + failure.completed_files = file_results; + return Err(failure); + } + } } let total_files = file_results.len(); @@ -3968,8 +5187,11 @@ fn bundle_ordering_error( file_path, reflection_metadata: None, checked_expression_types: None, + breakpoint_exclusions: None, explicit_comptime_values: None, + debug_observations: Vec::new(), })), + debug_observations: Vec::new(), } } @@ -4153,22 +5375,111 @@ pub fn bundle_project_detailed( if let Some(parent) = output_abs.parent() && !parent.as_os_str().is_empty() { - fs::create_dir_all(parent) - .map_err(|e| format!("failed to create {}: {}", parent.display(), e))?; - } - fs::write(&output_abs, bundled) - .map_err(|e| format!("failed to write {}: {}", output_abs.display(), e))?; + fs::create_dir_all(parent).map_err(|e| { + BundleError::from(format!("failed to create {}: {}", parent.display(), e)) + .with_debug_observations(&validation.debug_observations) + })?; + } + fs::write(&output_abs, bundled).map_err(|e| { + BundleError::from(format!("failed to write {}: {}", output_abs.display(), e)) + .with_debug_observations(&validation.debug_observations) + })?; Ok(BundleResult { project_root: project_dir.display().to_string(), output_path: output_abs.display().to_string(), files: bundled_files, + debug_observations: validation.debug_observations, }) } #[cfg(test)] mod tests { use super::*; + + #[test] + fn backend_native_verify_suite_preserves_checked_actor_fixture_tables() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass"); + for fixture in [ + "actor_named_arguments.jett", + "namespace_duplicate_leaf_actors.jett", + "namespace_qualified_actors.jett", + "numeric_literal_contexts.jett", + ] { + for release in [false, true] { + let lowered = lower_file_for_native_verify_suite_with_options( + &root.join(fixture), + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{fixture} release={release}: {error}")); + let entry = lowered + .native_verify_entry + .expect("original checked fixture verifies"); + let suite = &lowered.hir.functions[entry.index() as usize]; + assert!(suite.params.is_empty() && suite.locals.is_empty()); + assert_eq!(suite.capture_count, 0); + assert_eq!(suite.return_type, jett_types::TypeInterner::NOTHING); + let checked = lowered + .checked_expression_types + .resource_program + .as_ref() + .unwrap(); + assert!(lowered.hir.resource_source.belongs_to(checked)); + let expected = checked + .module() + .items + .iter() + .filter_map(|item| { + let Item::Verify(verify) = item else { + return None; + }; + (verify.span.file == FileId::new(0)).then_some(verify.span) + }) + .collect::>(); + let actual = suite + .body + .statements + .iter() + .map(|statement| { + let jett_hir::StatementKind::Expression(jett_hir::Expression { + kind: + jett_hir::ExpressionKind::Call { + function, + args, + evaluation_order, + .. + }, + ty, + span, + }) = &statement.kind + else { + panic!("canonical zero-argument verify call"); + }; + assert!(args.is_empty() && evaluation_order.is_empty()); + assert_eq!(*ty, jett_types::TypeInterner::NOTHING); + let target = &lowered.hir.functions[function.index() as usize]; + assert_eq!(target.span, *span); + assert_eq!( + target.identity.declaration.kind, + jett_hir::DeclarationKind::Verify + ); + assert_eq!(target.return_type, jett_types::TypeInterner::NOTHING); + *span + }) + .collect::>(); + assert_eq!(actual, expected, "{fixture} release={release}"); + jett_hir::validate(&lowered.hir).unwrap(); + jett_hir::validate_backend_types(&lowered.hir, &lowered.interner).unwrap(); + lowered + .hir + .resource_source + .validate_current(&lowered.hir, &lowered.interner) + .unwrap(); + jett_mir::validate(&lowered.mir).unwrap(); + } + } + } + use std::time::{SystemTime, UNIX_EPOCH}; fn temp_test_dir(name: &str) -> PathBuf { @@ -4179,6 +5490,534 @@ mod tests { std::env::temp_dir().join(format!("{name}_{nanos}")) } + #[test] + fn backend_checked_ordinary_constants_and_refinements_keep_their_unsealed_path() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + "namespace app\ntype Positive = int64 where value > 0\nfunction positive() returns Positive:\n return 5\nint64 cached = comptime 5\nfunction main(stdout: Stdout) returns nothing:\n Positive refined = comptime positive()\n int64 ordinary = comptime 7\n int64 total = cached + coarsen refined + ordinary\n Stdout.write(view stdout, \"{total}\\n\")\n return nothing\n", + ) + .unwrap(); + for release in [false, true] { + let lowered = lower_file_for_backend_with_options(&source, BuildOptions { release }) + .expect("ordinary constants and raw refinement lowering"); + assert!(!jett_hir::has_reflected_callable_plans(&lowered.hir)); + assert!(lowered.hir.resource_source.manifest().is_none()); + assert!(lowered.hir.resource_source.functions().is_empty()); + assert!(lowered.mir.resource_manifest.kinds().is_empty()); + jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + lowered.program_entry.expect("ordinary main"), + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .expect("ordinary constants and raw refinements still emit an object"); + } + } + + const DEBUG_PHASE_SOURCE: &str = r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("constant:") +function main(stdout: Stdout) returns nothing: + int64 baked = comptime observe("expression:") + Stdout.write(view stdout, "{cached + baked}\n") + print("trace public") + println("tail") +verify checking: + print("verify:") + assert true +"#; + + fn observed(phase: DebugPhase, kind: DebugEventKind, text: &str) -> DebugObservation { + DebugObservation { + phase, + event: DebugEvent { + kind, + text: text.into(), + }, + } + } + + fn frontend_phase_oracle() -> Vec { + vec![ + observed(DebugPhase::Comptime, DebugEventKind::Print, "constant:"), + observed(DebugPhase::Comptime, DebugEventKind::Print, "expression:"), + observed(DebugPhase::FrontendVerify, DebugEventKind::Print, "verify:"), + ] + } + + fn prepared_capture_options() -> RunOptions { + RunOptions { + capture_stdout: true, + emit_runtime_debug: false, + random_test_samples: None, + clock_test_samples: None, + environment_test_snapshot: None, + graphics_test_events: None, + } + } + + fn invalid_environment_options() -> RunOptions { + RunOptions { + environment_test_snapshot: Some(EnvironmentTestSnapshot { + arguments: vec![EnvironmentTestText::InvalidUnicode], + entries: Vec::new(), + }), + ..prepared_capture_options() + } + } + + #[test] + fn prepared_reference_uses_saved_primary_entry_and_support_after_source_deletion() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("jett.proj"), + "name: retained_reference\n", + ) + .unwrap(); + let support = directory.path().join("support.jett"); + let primary = directory.path().join("main.jett"); + fs::write( + &support, + r#"namespace support +export function echo[T](value: T) returns T: + return value +export function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "wrong entry\n") + return nothing +"#, + ) + .unwrap(); + fs::write( + &primary, + r#"function main(stdout: Stdout) returns nothing: + use support + Stdout.write(view stdout, "saved:{support.echo(7)}\n") + return nothing +"#, + ) + .unwrap(); + let prepared = prepare_reference_file(&primary, true, BuildOptions::default()); + assert!( + !prepared.build.has_errors, + "{:?}", + prepared.build.diagnostics + ); + let program = prepared.program.as_ref().unwrap(); + assert!(program.resolved().resource_kernels.is_empty()); + assert!(program.checked().resource_hooks.is_empty()); + assert!(!program.checked().generic_function_instantiations.is_empty()); + assert_eq!( + program.source_origins().get(&FileId::new(0)), + Some(&SourceOrigin::Project) + ); + assert!( + program + .source_origins() + .values() + .any(|origin| *origin == SourceOrigin::Stdlib) + ); + let selected = prepared.entry.as_ref().unwrap(); + assert_eq!(selected.span.file, FileId::new(0)); + assert_eq!(selected.namespace, None); + let (first_namespace, first_main) = find_main_function(program.module()).unwrap(); + assert_eq!(first_namespace.as_deref(), Some("support")); + assert_ne!( + first_main.span, selected.span, + "the merged first main is deliberately not the entry" + ); + // Any second read or discovery would fail or run different source now. + fs::write(&support, "this is no longer valid Jett\n").unwrap(); + fs::remove_file(&primary).unwrap(); + let output = run_prepared_reference(prepared, prepared_capture_options()).unwrap(); + assert_eq!(output.stdout, "saved:7\n"); + assert!(output.debug_events.is_empty()); + assert!(output.frontend_debug_observations.is_empty()); + } + + #[test] + fn prepared_reference_retains_comptime_verify_and_runtime_observations_once() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, DEBUG_PHASE_SOURCE).unwrap(); + let prepared = prepare_reference_file(&path, true, BuildOptions::default()); + assert!( + !prepared.build.has_errors, + "{:?}", + prepared.build.diagnostics + ); + assert_eq!(prepared.build.debug_observations, frontend_phase_oracle()); + fs::remove_file(&path).unwrap(); + let output = run_prepared_reference(prepared, prepared_capture_options()).unwrap(); + assert_eq!(output.stdout, "14\n"); + assert_eq!(output.frontend_debug_observations, frontend_phase_oracle()); + assert_eq!( + output.debug_events, + vec![ + DebugEvent { + kind: DebugEventKind::Print, + text: "trace public".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "tail\n".into() + }, + ] + ); + } + + #[test] + fn prepared_reference_refuses_frontend_failure_before_provider_initialization() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write( + &path, + r#"function reject() returns int64: + print("before-failure") + string impossible = string.repeat("ab", 9223372036854775807) + return string.char_count(impossible) +function main(env: Environment) returns nothing: + int64 failed = comptime reject() +verify unreachable: + println("must not execute") +"#, + ) + .unwrap(); + let prepared = prepare_reference_file(&path, true, BuildOptions::default()); + assert!(prepared.build.has_errors); + assert!( + prepared.program.is_some(), + "the checked session is retained after required-evaluation failure" + ); + let expected = vec![observed( + DebugPhase::Comptime, + DebugEventKind::Print, + "before-failure", + )]; + assert_eq!(prepared.build.debug_observations, expected); + fs::remove_file(&path).unwrap(); + let failure = run_prepared_reference(prepared, invalid_environment_options()).unwrap_err(); + assert!( + failure + .message + .contains("string.repeat: requested output is too large"), + "{failure}" + ); + assert!(!failure.message.contains("Environment"), "{failure}"); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + assert_eq!(failure.output.frontend_debug_observations, expected); + } + + #[test] + fn prepared_reference_refuses_missing_snapshot_before_provider_initialization() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write( + &path, + "function main(env: Environment) returns nothing:\n return nothing\n", + ) + .unwrap(); + let mut prepared = prepare_reference_file(&path, true, BuildOptions::default()); + assert!( + !prepared.build.has_errors, + "{:?}", + prepared.build.diagnostics + ); + prepared.program = None; + let failure = run_prepared_reference(prepared, invalid_environment_options()).unwrap_err(); + assert_eq!( + failure.message, + "runtime error: successful compilation has no retained checked program" + ); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + } + + #[test] + fn build_capture_keeps_comptime_and_actual_verification_once_in_phase_order() { + let build = build_source(DEBUG_PHASE_SOURCE, "phase.jett"); + assert!(!build.has_errors, "{:?}", build.diagnostics); + assert_eq!(build.debug_observations, frontend_phase_oracle()); + assert_eq!(DebugPhase::Comptime.as_str(), "comptime"); + assert_eq!(DebugPhase::FrontendVerify.as_str(), "frontend_verify"); + assert_eq!(DebugPhase::Runtime.as_str(), "runtime"); + } + + #[test] + fn captured_run_keeps_baked_reads_silent_and_application_output_separate() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, DEBUG_PHASE_SOURCE).unwrap(); + let output = run_file_capture_outcome(&path).unwrap(); + assert_eq!(output.stdout, "14\n"); + assert_eq!(output.frontend_debug_observations, frontend_phase_oracle()); + assert_eq!( + output.debug_events, + vec![ + DebugEvent { + kind: DebugEventKind::Print, + text: "trace public".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "tail\n".into() + }, + ] + ); + assert_eq!( + render_debug_events(&output.debug_events), + "trace publictail\n" + ); + assert_eq!(run_file_capture_stdout(&path).unwrap(), "14\n"); + } + + #[test] + fn failed_comptime_retains_events_in_build_run_and_fatal_test_results() { + let source = r#"namespace app +function reject() returns int64: + print("before-failure") + string impossible = string.repeat("ab", 9223372036854775807) + return string.char_count(impossible) +function main() returns nothing: + int64 failed = comptime reject() +verify unreachable: + println("must not execute") +"#; + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + let expected = vec![observed( + DebugPhase::Comptime, + DebugEventKind::Print, + "before-failure", + )]; + let build = build_source(source, "main.jett"); + assert!(build.has_errors); + assert_eq!(build.debug_observations, expected); + let failure = run_file_capture_outcome(&path).unwrap_err(); + assert!( + failure + .message + .contains("string.repeat: requested output is too large"), + "{failure}" + ); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + assert_eq!(failure.output.frontend_debug_observations, expected); + let failure = test_file_capture_outcome(&path).unwrap_err(); + assert!(failure.message.contains("comptime errors:"), "{failure}"); + assert_eq!(failure.file_path, path.display().to_string()); + assert_eq!(failure.debug_observations, expected); + assert!(failure.completed_files.is_empty()); + } + + #[test] + fn failed_frontend_verify_preserves_all_actual_block_events_before_run_refusal() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write( + &path, + r#"namespace app +function main() returns nothing: + println("must not run") +verify failing: + print("first") + assert false "verify failure" +verify following: + println("second") + assert true +"#, + ) + .unwrap(); + let failure = run_file_capture_outcome(&path).unwrap_err(); + assert!(failure.message.contains("verify failure"), "{failure}"); + assert!(failure.output.debug_events.is_empty()); + assert_eq!( + failure.output.frontend_debug_observations, + vec![ + observed(DebugPhase::FrontendVerify, DebugEventKind::Print, "first"), + observed( + DebugPhase::FrontendVerify, + DebugEventKind::Println, + "second\n" + ), + ] + ); + } + + #[test] + fn test_capture_attributes_failed_blocks_and_all_property_trials_without_duplicates() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("tests.jett"); + fs::write( + &path, + r#"namespace app +function observe() returns int64: + print("compile:") + return 7 +int64 cached = comptime observe() +verify failing: + println(cached) + assert false "block failure" +property complete: + given number: int64 + print("trial:") + assert number == number +"#, + ) + .unwrap(); + let result = test_file_capture_outcome(&path).unwrap(); + assert_eq!((result.total, result.passed, result.failed), (2, 1, 1)); + assert_eq!( + result.debug_observations, + vec![observed( + DebugPhase::Comptime, + DebugEventKind::Print, + "compile:" + )] + ); + assert_eq!(result.blocks[0].name, "failing"); + assert_eq!(render_debug_events(&result.blocks[0].debug_events), "7\n"); + assert_eq!(result.blocks[1].name, "complete"); + assert_eq!(result.blocks[1].iterations, Some(100)); + assert_eq!(result.blocks[1].debug_events.len(), 100); + assert_eq!( + render_debug_events(&result.blocks[1].debug_events), + "trial:".repeat(100) + ); + } + + #[test] + fn project_fatal_capture_preserves_prior_file_blocks_and_current_file_association() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("jett.proj"), + "name: debug_capture\nentry: 00_first.jett\n", + ) + .unwrap(); + let first = directory.path().join("00_first.jett"); + let second = directory.path().join("10_second.jett"); + fs::write( + &first, + "namespace first\nverify passed:\n print(\"first:\")\n assert true\n", + ) + .unwrap(); + fs::write( + &second, + r#"namespace second +function reject(label: string) returns int64: + println(view label) + string impossible = string.repeat("ab", 9223372036854775807) + return string.char_count(impossible) +verify stopped: + int64 value = comptime reject("second") + assert value == 0 +"#, + ) + .unwrap(); + let failure = match test_project_capture_outcome(directory.path()) { + Ok(_) => panic!("second file must fail required evaluation"), + Err(failure) => failure, + }; + assert_eq!(failure.file_path, second.display().to_string()); + assert_eq!(failure.completed_files.len(), 1); + let previous = &failure.completed_files[0]; + assert_eq!(previous.file_path, first.display().to_string()); + assert_eq!( + (previous.total, previous.passed, previous.failed), + (1, 1, 0) + ); + assert_eq!(previous.blocks.len(), 1); + assert!(previous.debug_observations.is_empty()); + assert_eq!( + render_debug_events(&previous.blocks[0].debug_events), + "first:" + ); + assert_eq!( + failure.debug_observations, + vec![observed( + DebugPhase::Comptime, + DebugEventKind::Println, + "second\n" + )] + ); + } + + #[test] + fn post_frontend_hir_failure_retains_capture_and_stable_diagnostic_text() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write( + &path, + r#"namespace app +function observe() returns int64: + print("compiled:") + return 7 +struct Packet: + data: bytes +function make_packet() returns Packet: + return Packet(data: bytes.new()) +function main() returns nothing: + int64 baked = comptime observe() + if baked == 7: + bytes borrowed = view make_packet().data +verify checking: + println("checked") + assert true +"#, + ) + .unwrap(); + let failure = match lower_file_for_backend(&path) { + Err(failure) => failure, + Ok(_) => panic!("nested temporary origin must fail after frontend observations"), + }; + assert_eq!( + failure.debug_observations(), + [ + observed(DebugPhase::Comptime, DebugEventKind::Print, "compiled:"), + observed( + DebugPhase::FrontendVerify, + DebugEventKind::Println, + "checked\n" + ), + ] + ); + assert!(failure.build_result().is_none()); + assert!(failure.to_string().starts_with("HIR lowering failed\n")); + assert!( + failure + .to_string() + .contains("native borrowed alias requires a stable local origin; temporary views remain unsupported") + ); + assert!(std::error::Error::source(&failure).is_some()); + } + + #[test] + fn bundle_validation_and_publication_failure_retain_actual_observations() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("jett.proj"), "name: bundle_capture\n").unwrap(); + fs::write(directory.path().join("main.jett"), DEBUG_PHASE_SOURCE).unwrap(); + let output = directory.path().join("target/program.jett"); + let bundle = bundle_project_detailed(directory.path(), &output).unwrap(); + assert_eq!(bundle.debug_observations, frontend_phase_oracle()); + let sentinel = directory.path().join("blocked"); + fs::write(&sentinel, b"preserved file").unwrap(); + let error = match bundle_project_detailed(directory.path(), &sentinel.join("program.jett")) + { + Ok(_) => panic!("output parent is a regular file"), + Err(error) => error, + }; + assert_eq!(error.debug_observations(), frontend_phase_oracle()); + assert!(error.diagnostic_result().is_none()); + assert!(error.kind_name().is_none()); + assert!(error.to_string().starts_with("failed to create ")); + assert_eq!(fs::read(&sentinel).unwrap(), b"preserved file"); + } + #[test] fn native_macos_graphics_stays_on_the_caller_thread() { let graphics = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -4244,7 +6083,13 @@ mod tests { fs::write(&broken, "namespace broken\nfunction nope(\n") .expect("broken support fixture should be written"); - let discovered = discover_modules_in_dir(&root, None, STDLIB_FILE_ID_START, "stdlib"); + let discovered = discover_modules_in_dir( + &root, + None, + STDLIB_FILE_ID_START, + "stdlib", + SourceOrigin::Stdlib, + ); let errors = error_messages_from_diagnostics(&discovered.diagnostics); fs::remove_dir_all(&root).expect("temp support dir should be removed"); diff --git a/crates/jett_driver/src/native.rs b/crates/jett_driver/src/native.rs new file mode 100644 index 00000000..1d5b564c --- /dev/null +++ b/crates/jett_driver/src/native.rs @@ -0,0 +1,2336 @@ +//! Host-native object emission and executable publication. +//! +//! Native compilation is deliberately split into an object stage and a link +//! stage. The object stage consumes the exact checked program-entry identity +//! published by [`crate::BackendLoweringResult`]. The Windows MSVC and Linux GNU +//! link stages accept explicit launcher metadata and invoke tools without a shell, +//! and publishes an executable only after a successful bounded link. + +#[path = "native_property_runner.rs"] +mod property_runner; +pub use property_runner::{ + NativePropertyFailure, NativePropertyOptions, NativePropertyRunError, + NativePropertySuiteResult, run_host_property_suite, +}; + +use std::ffi::{OsStr, OsString}; +use std::fmt; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +use jett_codegen_cranelift::{CodegenError, emit_host_object}; +use jett_hir::FunctionId; + +use crate::{ + BackendLoweringError, BackendLoweringResult, lower_file_for_native_property_suite, + lower_file_for_native_property_suite_with_options, lower_file_for_native_tests, + lower_file_for_native_verify_suite_with_options, +}; + +/// The sole target accepted by the version 1 native Windows linker. +pub const WINDOWS_MSVC_NATIVE_TARGET: &str = "x86_64-pc-windows-msvc"; + +/// Supported Linux GNU host; this is not a cross-linking contract. +pub const LINUX_GNU_NATIVE_TARGET: &str = "x86_64-unknown-linux-gnu"; + +const SUPPORTED_NATIVE_HOSTS: &str = "x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu"; + +/// System libraries required by the GNU dynamic-CRT launcher archive. +pub const LINUX_GNU_V1_NATIVE_LIBRARIES: &[&str] = &[ + "-lgcc_s", + "-lutil", + "-lrt", + "-lpthread", + "-lm", + "-ldl", + "-lc", +]; + +/// The runtime ABI expected by the version 1 native launcher. +pub const NATIVE_RUNTIME_ABI_VERSION_V1: u32 = 1; + +/// Maximum wall-clock time allowed for one native linker invocation. +pub const DEFAULT_NATIVE_LINK_TIMEOUT: Duration = Duration::from_secs(60); + +/// Canonical ordered native-link inputs required by the static-CRT launcher. +pub const WINDOWS_MSVC_STATIC_V1_NATIVE_LIBRARIES: &[&str] = &[ + "advapi32.lib", + "bcrypt.lib", + "gdi32.lib", + "kernel32.lib", + "msimg32.lib", + "opengl32.lib", + "user32.lib", + "winspool.lib", + "kernel32.lib", + "ntdll.lib", + "userenv.lib", + "ws2_32.lib", + "dbghelp.lib", + "/defaultlib:libcmt", +]; + +/// C runtime linkage used to build a launcher archive. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeCrtMode { + Static, + Dynamic, +} + +impl fmt::Display for NativeCrtMode { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Static => formatter.write_str("static"), + Self::Dynamic => formatter.write_str("dynamic"), + } + } +} + +/// Explicit metadata for one native launcher archive. +/// +/// Keeping this record separate from the archive path prevents the driver from +/// guessing ABI, CRT, target, or transitive native-library requirements. The +/// installed launcher manifest is validated before constructing this shape. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeLauncherBundle { + pub archive_path: PathBuf, + pub target: String, + pub runtime_abi_version: u32, + pub crt_mode: NativeCrtMode, + pub native_library_args: Vec, +} + +impl NativeLauncherBundle { + /// Load explicit installed runtime metadata; paths are relative to the manifest. + pub fn from_manifest(path: &Path, release: bool) -> Result { + let text = fs::read_to_string(path).map_err(|error| format!("cannot read native runtime bundle `{}`: {error}; install the matching runtime or use --runtime-bundle", path.display()))?; + let manifest: serde_json::Value = serde_json::from_str(&text).map_err(|error| { + format!( + "invalid native runtime manifest `{}`: {error}", + path.display() + ) + })?; + let field = |name: &str| { + manifest + .get(name) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| format!("native runtime manifest requires string `{name}`")) + }; + if manifest + .get("manifest_version") + .and_then(serde_json::Value::as_u64) + != Some(1) + { + return Err("unsupported native runtime manifest version (expected 1)".into()); + } + if field("compiler_version")? != env!("CARGO_PKG_VERSION") { + return Err(format!( + "native runtime compiler version must be {}", + env!("CARGO_PKG_VERSION") + )); + } + let profile = if release { "release" } else { "debug" }; + if field("profile")? != profile { + return Err(format!( + "native runtime profile mismatch: expected {profile}" + )); + } + let archive = Path::new(field("archive")?); + if archive.components().count() != 1 + || !matches!( + archive.components().next(), + Some(std::path::Component::Normal(_)) + ) + { + return Err("native runtime archive must be a filename beside its manifest".into()); + } + let abi = manifest + .get("runtime_abi_version") + .and_then(serde_json::Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or("native runtime manifest requires a runtime_abi_version integer")?; + let libraries = manifest + .get("native_library_args") + .and_then(serde_json::Value::as_array) + .ok_or("native runtime manifest requires native_library_args")? + .iter() + .map(|value| { + value + .as_str() + .map(OsString::from) + .ok_or("native library arguments must be strings") + }) + .collect::, _>>()?; + let bundle = Self { + archive_path: path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(archive), + target: field("target")?.to_owned(), + runtime_abi_version: abi, + crt_mode: match field("crt_mode")? { + "static" => NativeCrtMode::Static, + "dynamic" => NativeCrtMode::Dynamic, + _ => return Err("native runtime CRT mode must be static or dynamic".into()), + }, + native_library_args: libraries, + }; + validate_host_launcher(&bundle).map_err(|error| error.to_string())?; + validate_regular_file( + &bundle.archive_path, + NativePathRole::LauncherArchive, + Some(if bundle.target == LINUX_GNU_NATIVE_TARGET { + "a" + } else { + "lib" + }), + ) + .map_err(|error| error.to_string())?; + Ok(bundle) + } + + /// Describe a Linux GNU launcher archive built for the compiler host. + pub fn linux_gnu_v1(archive_path: impl Into) -> Self { + Self { + archive_path: archive_path.into(), + target: LINUX_GNU_NATIVE_TARGET.to_string(), + runtime_abi_version: NATIVE_RUNTIME_ABI_VERSION_V1, + crt_mode: NativeCrtMode::Dynamic, + native_library_args: LINUX_GNU_V1_NATIVE_LIBRARIES + .iter() + .map(OsString::from) + .collect(), + } + } + + /// Describe the canonical version 1 static-CRT launcher for Windows MSVC. + pub fn windows_msvc_static_v1(archive_path: impl Into) -> Self { + Self { + archive_path: archive_path.into(), + target: WINDOWS_MSVC_NATIVE_TARGET.to_string(), + runtime_abi_version: NATIVE_RUNTIME_ABI_VERSION_V1, + crt_mode: NativeCrtMode::Static, + native_library_args: WINDOWS_MSVC_STATIC_V1_NATIVE_LIBRARIES + .iter() + .map(OsString::from) + .collect(), + } + } +} + +/// A checked Jett program lowered through the production Cranelift backend. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeObjectArtifact { + target: String, + symbols: Vec, + bytes: Vec, + debug_observations: Vec, +} + +impl NativeObjectArtifact { + pub fn debug_observations(&self) -> &[crate::DebugObservation] { + &self.debug_observations + } + + pub fn target(&self) -> &str { + &self.target + } + + pub fn symbols(&self) -> &[String] { + &self.symbols + } + + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +/// Native object containing the exported launcher entry wrapper. +#[derive(Debug, Clone)] +pub struct NativeProgramObjectArtifact { + object: NativeObjectArtifact, + program_entry: FunctionId, + diagnostics: Vec, + source: String, +} + +impl NativeProgramObjectArtifact { + pub fn debug_observations(&self) -> &[crate::DebugObservation] { + self.object.debug_observations() + } + + pub fn diagnostics(&self) -> &[jett_diagnostics::Diagnostic] { + &self.diagnostics + } + + pub fn source(&self) -> &str { + &self.source + } + + pub fn target(&self) -> &str { + self.object.target() + } + + pub fn program_entry(&self) -> FunctionId { + self.program_entry + } + + pub fn symbols(&self) -> &[String] { + self.object.symbols() + } + + pub fn bytes(&self) -> &[u8] { + self.object.bytes() + } +} + +/// A successfully linked and atomically published native executable. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeExecutableArtifact { + pub path: PathBuf, + pub target: String, + pub program_entry: FunctionId, + pub symbols: Vec, + pub debug_observations: Vec, +} + +/// Filesystem role used in actionable native-build diagnostics. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativePathRole { + Source, + LauncherArchive, + Output, + OutputDirectory, + TemporaryBuildDirectory, + Object, + LinkedExecutable, +} + +impl fmt::Display for NativePathRole { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Source => formatter.write_str("source file"), + Self::LauncherArchive => formatter.write_str("launcher archive"), + Self::Output => formatter.write_str("output executable"), + Self::OutputDirectory => formatter.write_str("output directory"), + Self::TemporaryBuildDirectory => formatter.write_str("temporary build directory"), + Self::Object => formatter.write_str("generated object"), + Self::LinkedExecutable => formatter.write_str("linked executable"), + } + } +} + +/// Failure while emitting, linking, or publishing a native executable. +#[derive(Debug)] +pub enum NativeBuildError { + /// A later backend/link failure with the actual frontend observations. + Captured { + source: Box, + debug_observations: Vec, + }, + InspectPath { + role: NativePathRole, + path: PathBuf, + source: io::Error, + }, + ExpectedRegularFile { + role: NativePathRole, + path: PathBuf, + }, + ExpectedDirectory { + role: NativePathRole, + path: PathBuf, + }, + InvalidExtension { + role: NativePathRole, + path: PathBuf, + expected: &'static str, + }, + ResolveCurrentDirectory(io::Error), + Lowering { + source_path: PathBuf, + source: Box, + }, + MissingProgramEntry { + source_path: PathBuf, + }, + MissingVerifyBodies { + source_path: PathBuf, + }, + MissingPropertyBodies { + source_path: PathBuf, + }, + Codegen { + source_path: PathBuf, + source: CodegenError, + }, + EmptyObject, + UnsupportedHost { + actual: String, + supported: &'static str, + }, + ObjectTargetMismatch { + object_target: String, + host_target: String, + }, + LauncherTargetMismatch { + actual: String, + expected: &'static str, + }, + LauncherRuntimeAbiMismatch { + actual: u32, + expected: u32, + }, + LauncherCrtMismatch { + actual: NativeCrtMode, + expected: NativeCrtMode, + }, + LauncherNativeLibrariesMismatch { + actual: Vec, + }, + CreateTemporaryDirectory { + parent: PathBuf, + source: io::Error, + }, + WriteObject { + path: PathBuf, + source: io::Error, + }, + LinkerNotFound { + target: &'static str, + }, + WindowsSdkNotFound { + architecture: &'static str, + }, + SpawnLinker { + linker: PathBuf, + source: io::Error, + }, + PollLinker { + linker: PathBuf, + source: io::Error, + }, + TerminateLinker { + linker: PathBuf, + source: io::Error, + }, + CaptureLinkerOutput { + stream: &'static str, + source: io::Error, + }, + SpawnLinkerOutputThread { + stream: &'static str, + source: io::Error, + }, + LinkerOutputThreadPanicked { + stream: &'static str, + }, + LinkTimedOut { + linker: PathBuf, + timeout: Duration, + stdout: String, + stderr: String, + }, + LinkFailed { + linker: PathBuf, + status: ExitStatus, + stdout: String, + stderr: String, + }, + MissingLinkedExecutable { + path: PathBuf, + }, + PublishExecutable { + from: PathBuf, + to: PathBuf, + source: io::Error, + }, +} + +impl NativeBuildError { + pub fn debug_observations(&self) -> &[crate::DebugObservation] { + match self { + Self::Captured { + debug_observations, .. + } => debug_observations, + Self::Lowering { source, .. } => source.debug_observations(), + _ => &[], + } + } + + pub fn build_result(&self) -> Option<&crate::BuildResult> { + match self { + Self::Captured { source, .. } => source.build_result(), + Self::Lowering { source, .. } => source.build_result(), + _ => None, + } + } + + fn with_debug_observations(self, observations: &[crate::DebugObservation]) -> Self { + if observations.is_empty() || !self.debug_observations().is_empty() { + self + } else { + Self::Captured { + source: Box::new(self), + debug_observations: observations.to_vec(), + } + } + } +} + +impl fmt::Display for NativeBuildError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Captured { source, .. } => fmt::Display::fmt(source, formatter), + Self::InspectPath { role, path, source } => { + write!( + formatter, + "cannot inspect {role} `{}`: {source}", + path.display() + ) + } + Self::ExpectedRegularFile { role, path } => write!( + formatter, + "{role} `{}` must be an existing regular file", + path.display() + ), + Self::ExpectedDirectory { role, path } => write!( + formatter, + "{role} `{}` must be an existing directory", + path.display() + ), + Self::InvalidExtension { + role, + path, + expected, + } => write!( + formatter, + "{role} `{}` must use the `{expected}` extension", + path.display() + ), + Self::ResolveCurrentDirectory(source) => { + write!(formatter, "cannot resolve the current directory: {source}") + } + Self::Lowering { + source_path, + source, + } => write!( + formatter, + "cannot lower native source `{}`: {source}", + source_path.display() + ), + Self::MissingProgramEntry { source_path } => write!( + formatter, + "native source `{}` has no checked primary `main` program entry", + source_path.display() + ), + Self::MissingVerifyBodies { source_path } => write!( + formatter, + "native source `{}` has no checked `verify` bodies in the primary file", + source_path.display() + ), + Self::MissingPropertyBodies { source_path } => write!( + formatter, + "native source `{}` has no checked `property` bodies in the primary file", + source_path.display() + ), + Self::Codegen { + source_path, + source, + } => write!( + formatter, + "cannot emit native object for `{}`: {source}", + source_path.display() + ), + Self::EmptyObject => { + formatter.write_str("native code generation produced an empty object") + } + Self::UnsupportedHost { actual, supported } => write!( + formatter, + "native executable linking is unsupported on host `{actual}`; supported hosts: {supported}" + ), + Self::ObjectTargetMismatch { + object_target, + host_target, + } => write!( + formatter, + "native object target `{object_target}` does not match linker host `{host_target}`" + ), + Self::LauncherTargetMismatch { actual, expected } => write!( + formatter, + "launcher target `{actual}` does not match required target `{expected}`" + ), + Self::LauncherRuntimeAbiMismatch { actual, expected } => write!( + formatter, + "launcher runtime ABI version {actual} does not match required version {expected}" + ), + Self::LauncherCrtMismatch { actual, expected } => write!( + formatter, + "launcher CRT mode `{actual}` does not match required mode `{expected}`" + ), + Self::LauncherNativeLibrariesMismatch { actual } => write!( + formatter, + "launcher native-library arguments {:?} do not match the canonical target-specific version 1 contract", + actual + ), + Self::CreateTemporaryDirectory { parent, source } => write!( + formatter, + "cannot create a unique native build directory under `{}`: {source}", + parent.display() + ), + Self::WriteObject { path, source } => write!( + formatter, + "cannot write generated object `{}`: {source}", + path.display() + ), + Self::LinkerNotFound { target } => write!( + formatter, + "cannot find MSVC `link.exe` and its SDK environment for target `{target}`" + ), + Self::WindowsSdkNotFound { architecture } => write!( + formatter, + "cannot find a Windows SDK for MSVC architecture `{architecture}`" + ), + Self::SpawnLinker { linker, source } => write!( + formatter, + "cannot start native linker `{}`: {source}", + linker.display() + ), + Self::PollLinker { linker, source } => write!( + formatter, + "cannot wait for native linker `{}`: {source}", + linker.display() + ), + Self::TerminateLinker { linker, source } => write!( + formatter, + "cannot terminate timed-out native linker `{}`: {source}", + linker.display() + ), + Self::CaptureLinkerOutput { stream, source } => { + write!(formatter, "cannot capture native linker {stream}: {source}") + } + Self::SpawnLinkerOutputThread { stream, source } => { + write!( + formatter, + "cannot start native linker {stream} reader: {source}" + ) + } + Self::LinkerOutputThreadPanicked { stream } => { + write!(formatter, "native linker {stream} reader panicked") + } + Self::LinkTimedOut { + linker, + timeout, + stdout, + stderr, + } => write!( + formatter, + "native linker `{}` exceeded its {:?} deadline\nstdout:\n{}\nstderr:\n{}", + linker.display(), + timeout, + stdout, + stderr + ), + Self::LinkFailed { + linker, + status, + stdout, + stderr, + } => write!( + formatter, + "native linker `{}` failed with {status}\nstdout:\n{}\nstderr:\n{}", + linker.display(), + stdout, + stderr + ), + Self::MissingLinkedExecutable { path } => write!( + formatter, + "native linker reported success but did not create `{}`", + path.display() + ), + Self::PublishExecutable { from, to, source } => write!( + formatter, + "cannot atomically publish linked executable from `{}` to `{}`: {source}", + from.display(), + to.display() + ), + } + } +} + +impl std::error::Error for NativeBuildError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InspectPath { source, .. } + | Self::CreateTemporaryDirectory { source, .. } + | Self::WriteObject { source, .. } + | Self::SpawnLinker { source, .. } + | Self::PollLinker { source, .. } + | Self::TerminateLinker { source, .. } + | Self::CaptureLinkerOutput { source, .. } + | Self::SpawnLinkerOutputThread { source, .. } + | Self::PublishExecutable { source, .. } => Some(source), + Self::ResolveCurrentDirectory(source) => Some(source), + Self::Lowering { source, .. } => Some(source), + Self::Captured { source, .. } => Some(source.as_ref()), + Self::Codegen { source, .. } => Some(source), + _ => None, + } + } +} + +/// Return the exact Cranelift target used by host object emission. +pub fn host_target() -> String { + jett_codegen_cranelift::host_target().to_string() +} + +/// Lower a source file and emit an ordinary reachable host object, including +/// checked `verify` and `property` bodies as callable native test symbols. +/// +/// This stage does not require a source `main` and does not add the launcher +/// entry wrapper. Executable publication always uses the distinct program +/// object API below. +pub fn emit_host_object_for_file( + source_path: &Path, +) -> Result { + validate_regular_file(source_path, NativePathRole::Source, Some("jett"))?; + let lowered = + lower_file_for_native_tests(source_path).map_err(|source| NativeBuildError::Lowering { + source_path: source_path.to_path_buf(), + source: Box::new(source), + })?; + let object = emit_host_object(&lowered.mir, &lowered.interner).map_err(|source| { + NativeBuildError::Codegen { + source_path: source_path.to_path_buf(), + source, + } + .with_debug_observations(&lowered.debug_observations) + })?; + let mut object = native_object(object.target, object.symbols, object.bytes) + .map_err(|error| error.with_debug_observations(&lowered.debug_observations))?; + object.debug_observations = lowered.debug_observations; + Ok(object) +} + +/// Lower a source file and emit a host object using its exact checked entry ID. +pub fn emit_host_program_object_for_file( + source_path: &Path, +) -> Result { + emit_host_program_object_for_file_with_options(source_path, crate::BuildOptions::default()) +} + +pub fn emit_host_program_object_for_file_with_options( + source_path: &Path, + options: crate::BuildOptions, +) -> Result { + validate_regular_file(source_path, NativePathRole::Source, Some("jett"))?; + let lowered = + crate::lower_file_for_backend_with_options(source_path, options).map_err(|source| { + NativeBuildError::Lowering { + source_path: source_path.to_path_buf(), + source: Box::new(source), + } + })?; + let program_entry = lowered.program_entry.ok_or_else(|| { + NativeBuildError::MissingProgramEntry { + source_path: source_path.to_path_buf(), + } + .with_debug_observations(&lowered.debug_observations) + })?; + emit_program_object_from_lowering(source_path, lowered, program_entry, options.release) +} + +/// Emit one launcher-compatible object that calls every checked `verify` body +/// in the primary source file, in declaration order. +pub fn emit_host_verify_suite_object_for_file( + source_path: &Path, +) -> Result { + emit_host_verify_suite_object_for_file_with_options(source_path, crate::BuildOptions::default()) +} + +/// Apply source build policy and optimization to the native verify suite. +pub fn emit_host_verify_suite_object_for_file_with_options( + source_path: &Path, + options: crate::BuildOptions, +) -> Result { + validate_regular_file(source_path, NativePathRole::Source, Some("jett"))?; + let lowered = lower_file_for_native_verify_suite_with_options(source_path, options).map_err( + |source| NativeBuildError::Lowering { + source_path: source_path.to_path_buf(), + source: Box::new(source), + }, + )?; + let entry = lowered.native_verify_entry.ok_or_else(|| { + NativeBuildError::MissingVerifyBodies { + source_path: source_path.to_path_buf(), + } + .with_debug_observations(&lowered.debug_observations) + })?; + emit_program_object_from_lowering(source_path, lowered, entry, options.release) +} + +/// Emit one launcher-compatible object that calls each checked property body +/// with the deterministic inputs used by the interpreter's property runner. +pub fn emit_host_property_suite_object_for_file( + source_path: &Path, +) -> Result { + emit_host_property_suite_object_for_file_with_options( + source_path, + crate::BuildOptions::default(), + ) +} + +/// Apply source build policy and optimization to the native property suite. +pub fn emit_host_property_suite_object_for_file_with_options( + source_path: &Path, + options: crate::BuildOptions, +) -> Result { + validate_regular_file(source_path, NativePathRole::Source, Some("jett"))?; + let lowered = lower_file_for_native_property_suite_with_options(source_path, options).map_err( + |source| NativeBuildError::Lowering { + source_path: source_path.to_path_buf(), + source: Box::new(source), + }, + )?; + let entry = lowered.native_property_entry.ok_or_else(|| { + NativeBuildError::MissingPropertyBodies { + source_path: source_path.to_path_buf(), + } + .with_debug_observations(&lowered.debug_observations) + })?; + emit_program_object_from_lowering(source_path, lowered, entry, options.release) +} + +fn emit_program_object_from_lowering( + source_path: &Path, + lowered: BackendLoweringResult, + program_entry: FunctionId, + optimize: bool, +) -> Result { + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + program_entry, + jett_codegen_cranelift::CodegenOptions { optimize }, + ) + .map_err(|source| { + NativeBuildError::Codegen { + source_path: source_path.to_path_buf(), + source, + } + .with_debug_observations(&lowered.debug_observations) + })?; + let mut object = native_object(object.target, object.symbols, object.bytes) + .map_err(|error| error.with_debug_observations(&lowered.debug_observations))?; + object.debug_observations = lowered.debug_observations; + Ok(NativeProgramObjectArtifact { + object, + program_entry, + diagnostics: lowered.diagnostics, + source: lowered.source, + }) +} + +fn native_object( + target: String, + symbols: Vec, + bytes: Vec, +) -> Result { + if bytes.is_empty() { + return Err(NativeBuildError::EmptyObject); + } + Ok(NativeObjectArtifact { + target, + symbols, + bytes, + debug_observations: Vec::new(), + }) +} + +/// Compile, link, and atomically publish one native executable. +/// +/// The launcher archive is supplied by the caller; this function never starts +/// Cargo or attempts to discover build-tree artifacts. +pub fn build_host_executable( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, +) -> Result { + build_host_executable_with_options( + source_path, + launcher, + output_path, + crate::BuildOptions::default(), + ) +} + +/// Apply source release policy and backend optimization before atomic linking. +pub fn build_host_executable_with_options( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, + options: crate::BuildOptions, +) -> Result { + let object = emit_host_program_object_for_file_with_options(source_path, options)?; + link_host_object(&object, launcher, output_path) +} + +/// Compile and link the primary file's checked `verify` bodies as one native +/// executable. The launcher reports the first terminal assertion failure. +pub fn build_host_verify_suite_executable( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, +) -> Result { + build_host_verify_suite_executable_with_options( + source_path, + launcher, + output_path, + crate::BuildOptions::default(), + ) +} + +/// Apply source policy and optimization before linking the native verify suite. +pub fn build_host_verify_suite_executable_with_options( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, + options: crate::BuildOptions, +) -> Result { + let object = emit_host_verify_suite_object_for_file_with_options(source_path, options)?; + link_host_object(&object, launcher, output_path) +} + +/// Compile and link the checked property trials for one primary source file. +pub fn build_host_property_suite_executable( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, +) -> Result { + build_host_property_suite_executable_with_options( + source_path, + launcher, + output_path, + crate::BuildOptions::default(), + ) +} + +/// Apply source policy and optimization before linking the native property suite. +pub fn build_host_property_suite_executable_with_options( + source_path: &Path, + launcher: &NativeLauncherBundle, + output_path: &Path, + options: crate::BuildOptions, +) -> Result { + let object = emit_host_property_suite_object_for_file_with_options(source_path, options)?; + link_host_object(&object, launcher, output_path) +} + +/// Link a production object with an explicit launcher bundle and publish it. +pub fn link_host_object( + object: &NativeProgramObjectArtifact, + launcher: &NativeLauncherBundle, + output_path: &Path, +) -> Result { + link_host_object_with_timeout(object, launcher, output_path, DEFAULT_NATIVE_LINK_TIMEOUT) +} + +fn link_host_object_with_timeout( + object: &NativeProgramObjectArtifact, + launcher: &NativeLauncherBundle, + output_path: &Path, + timeout: Duration, +) -> Result { + link_host_object_inner(object, launcher, output_path, timeout) + .map_err(|error| error.with_debug_observations(object.debug_observations())) +} + +fn link_host_object_inner( + object: &NativeProgramObjectArtifact, + launcher: &NativeLauncherBundle, + output_path: &Path, + timeout: Duration, +) -> Result { + validate_link_host(object)?; + validate_host_launcher(launcher)?; + validate_regular_file( + &launcher.archive_path, + NativePathRole::LauncherArchive, + Some(if launcher.target == LINUX_GNU_NATIVE_TARGET { + "a" + } else { + "lib" + }), + )?; + // Linking runs in a temporary directory, so relative archive inputs must + // be resolved against the callers current directory first. + let mut launcher = launcher.clone(); + launcher.archive_path = absolute_output_path(&launcher.archive_path)?; + let output_path = absolute_output_path(output_path)?; + validate_output_path(&output_path)?; + + let output_parent = + output_path + .parent() + .ok_or_else(|| NativeBuildError::ExpectedDirectory { + role: NativePathRole::OutputDirectory, + path: output_path.clone(), + })?; + let build_directory = tempfile::Builder::new() + .prefix(".jett-native-") + .tempdir_in(output_parent) + .map_err(|source| NativeBuildError::CreateTemporaryDirectory { + parent: output_parent.to_path_buf(), + source, + })?; + let object_path = build_directory.path().join("program.obj"); + let linked_executable = build_directory.path().join("program.exe"); + let linked_pdb = build_directory.path().join("program.pdb"); + fs::write(&object_path, object.bytes()).map_err(|source| NativeBuildError::WriteObject { + path: object_path.clone(), + source, + })?; + + if launcher.target == LINUX_GNU_NATIVE_TARGET { + link_linux_gnu( + &object_path, + &launcher, + &linked_executable, + build_directory.path(), + timeout, + )?; + } else { + link_windows_msvc( + &object_path, + &launcher, + &linked_executable, + &linked_pdb, + build_directory.path(), + timeout, + )?; + } + let linked_metadata = fs::metadata(&linked_executable).map_err(|source| { + if source.kind() == io::ErrorKind::NotFound { + NativeBuildError::MissingLinkedExecutable { + path: linked_executable.clone(), + } + } else { + NativeBuildError::InspectPath { + role: NativePathRole::LinkedExecutable, + path: linked_executable.clone(), + source, + } + } + })?; + if !linked_metadata.is_file() { + return Err(NativeBuildError::ExpectedRegularFile { + role: NativePathRole::LinkedExecutable, + path: linked_executable, + }); + } + + publish_executable(&linked_executable, &output_path)?; + Ok(NativeExecutableArtifact { + path: output_path, + target: object.target().to_string(), + program_entry: object.program_entry, + symbols: object.symbols().to_vec(), + debug_observations: object.debug_observations().to_vec(), + }) +} + +fn validate_link_host(object: &NativeProgramObjectArtifact) -> Result<(), NativeBuildError> { + validate_link_host_for(object, jett_codegen_cranelift::host_target().to_string()) +} + +fn validate_link_host_for( + object: &NativeProgramObjectArtifact, + host_target: String, +) -> Result<(), NativeBuildError> { + let supported = (cfg!(all(target_os = "windows", target_env = "msvc")) + && host_target == WINDOWS_MSVC_NATIVE_TARGET) + || (cfg!(all(target_os = "linux", target_env = "gnu")) + && host_target == LINUX_GNU_NATIVE_TARGET); + if !supported { + return Err(NativeBuildError::UnsupportedHost { + actual: host_target, + supported: SUPPORTED_NATIVE_HOSTS, + }); + } + if object.target() != host_target { + return Err(NativeBuildError::ObjectTargetMismatch { + object_target: object.target().to_string(), + host_target, + }); + } + if object.bytes().is_empty() { + return Err(NativeBuildError::EmptyObject); + } + Ok(()) +} + +fn validate_host_launcher(launcher: &NativeLauncherBundle) -> Result<(), NativeBuildError> { + if cfg!(all( + target_os = "linux", + target_env = "gnu", + target_arch = "x86_64" + )) { + validate_launcher_contract( + launcher, + LINUX_GNU_NATIVE_TARGET, + NativeCrtMode::Dynamic, + LINUX_GNU_V1_NATIVE_LIBRARIES, + ) + } else { + validate_launcher(launcher) + } +} + +fn validate_launcher(launcher: &NativeLauncherBundle) -> Result<(), NativeBuildError> { + validate_launcher_contract( + launcher, + WINDOWS_MSVC_NATIVE_TARGET, + NativeCrtMode::Static, + WINDOWS_MSVC_STATIC_V1_NATIVE_LIBRARIES, + ) +} + +fn validate_launcher_contract( + launcher: &NativeLauncherBundle, + target: &'static str, + crt: NativeCrtMode, + libraries: &[&str], +) -> Result<(), NativeBuildError> { + if launcher.target != target { + return Err(NativeBuildError::LauncherTargetMismatch { + actual: launcher.target.clone(), + expected: target, + }); + } + if launcher.runtime_abi_version != NATIVE_RUNTIME_ABI_VERSION_V1 { + return Err(NativeBuildError::LauncherRuntimeAbiMismatch { + actual: launcher.runtime_abi_version, + expected: NATIVE_RUNTIME_ABI_VERSION_V1, + }); + } + if launcher.crt_mode != crt { + return Err(NativeBuildError::LauncherCrtMismatch { + actual: launcher.crt_mode, + expected: crt, + }); + } + let expected: Vec = libraries.iter().map(OsString::from).collect(); + if launcher.native_library_args != expected { + return Err(NativeBuildError::LauncherNativeLibrariesMismatch { + actual: launcher.native_library_args.clone(), + }); + } + Ok(()) +} + +fn validate_regular_file( + path: &Path, + role: NativePathRole, + extension: Option<&'static str>, +) -> Result<(), NativeBuildError> { + let metadata = fs::metadata(path).map_err(|source| NativeBuildError::InspectPath { + role, + path: path.to_path_buf(), + source, + })?; + if !metadata.is_file() { + return Err(NativeBuildError::ExpectedRegularFile { + role, + path: path.to_path_buf(), + }); + } + if let Some(expected) = extension + && !has_ascii_case_insensitive_extension(path, expected) + { + return Err(NativeBuildError::InvalidExtension { + role, + path: path.to_path_buf(), + expected, + }); + } + Ok(()) +} + +fn validate_output_path(path: &Path) -> Result<(), NativeBuildError> { + if cfg!(windows) && !has_ascii_case_insensitive_extension(path, "exe") { + return Err(NativeBuildError::InvalidExtension { + role: NativePathRole::Output, + path: path.to_path_buf(), + expected: "exe", + }); + } + let Some(parent) = path.parent() else { + return Err(NativeBuildError::ExpectedDirectory { + role: NativePathRole::OutputDirectory, + path: path.to_path_buf(), + }); + }; + let parent_metadata = fs::metadata(parent).map_err(|source| NativeBuildError::InspectPath { + role: NativePathRole::OutputDirectory, + path: parent.to_path_buf(), + source, + })?; + if !parent_metadata.is_dir() { + return Err(NativeBuildError::ExpectedDirectory { + role: NativePathRole::OutputDirectory, + path: parent.to_path_buf(), + }); + } + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.file_type().is_file() => Ok(()), + Ok(_) => Err(NativeBuildError::ExpectedRegularFile { + role: NativePathRole::Output, + path: path.to_path_buf(), + }), + Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(()), + Err(source) => Err(NativeBuildError::InspectPath { + role: NativePathRole::Output, + path: path.to_path_buf(), + source, + }), + } +} + +fn has_ascii_case_insensitive_extension(path: &Path, expected: &str) -> bool { + path.extension() + .and_then(OsStr::to_str) + .is_some_and(|extension| extension.eq_ignore_ascii_case(expected)) +} + +fn absolute_output_path(path: &Path) -> Result { + if path.is_absolute() { + return Ok(path.to_path_buf()); + } + std::env::current_dir() + .map(|current| current.join(path)) + .map_err(NativeBuildError::ResolveCurrentDirectory) +} + +fn link_linux_gnu( + object: &Path, + launcher: &NativeLauncherBundle, + executable: &Path, + working_directory: &Path, + timeout: Duration, +) -> Result<(), NativeBuildError> { + // One literal executable path, never shell text or space-split flags. + // Resolve Linux paths containing a separator against the caller's cwd, + // before the child moves into the build directory. Bare names use PATH. + let linker = + PathBuf::from(std::env::var_os("JETT_NATIVE_CC").unwrap_or_else(|| OsString::from("cc"))); + let linker = if linker.is_relative() && linker.as_os_str().as_encoded_bytes().contains(&b'/') { + absolute_output_path(&linker)? + } else { + linker + }; + let mut command = Command::new(&linker); + command + .current_dir(working_directory) + .arg("-no-pie") + .arg("-o") + .arg(executable) + .arg(object) + .arg(&launcher.archive_path) + .args(&launcher.native_library_args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let output = run_command_with_timeout(command, &linker, timeout)?; + if !output.status.success() { + return Err(NativeBuildError::LinkFailed { + linker, + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + Ok(()) +} + +#[cfg(windows)] +fn link_windows_msvc( + object_path: &Path, + launcher: &NativeLauncherBundle, + executable_path: &Path, + pdb_path: &Path, + working_directory: &Path, + timeout: Duration, +) -> Result<(), NativeBuildError> { + find_msvc_tools::find_windows_sdk("x86_64").ok_or(NativeBuildError::WindowsSdkNotFound { + architecture: "x86_64", + })?; + let tool = find_msvc_tools::find_tool(WINDOWS_MSVC_NATIVE_TARGET, "link.exe").ok_or( + NativeBuildError::LinkerNotFound { + target: WINDOWS_MSVC_NATIVE_TARGET, + }, + )?; + let linker_path = tool.path().to_path_buf(); + let mut command = tool.to_command(); + command + .current_dir(working_directory) + .args(linker_arguments( + object_path, + launcher, + executable_path, + pdb_path, + )) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let output = run_command_with_timeout(command, &linker_path, timeout)?; + if !output.status.success() { + return Err(NativeBuildError::LinkFailed { + linker: linker_path, + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + Ok(()) +} + +#[cfg(not(windows))] +fn link_windows_msvc( + _object_path: &Path, + _launcher: &NativeLauncherBundle, + _executable_path: &Path, + _pdb_path: &Path, + _working_directory: &Path, + _timeout: Duration, +) -> Result<(), NativeBuildError> { + Err(NativeBuildError::UnsupportedHost { + actual: jett_codegen_cranelift::host_target().to_string(), + supported: SUPPORTED_NATIVE_HOSTS, + }) +} + +#[cfg(any(windows, test))] +fn linker_arguments( + object_path: &Path, + launcher: &NativeLauncherBundle, + executable_path: &Path, + pdb_path: &Path, +) -> Vec { + let mut arguments = vec![ + OsString::from("/NOLOGO"), + OsString::from("/MACHINE:X64"), + OsString::from("/SUBSYSTEM:CONSOLE"), + OsString::from("/INCREMENTAL:NO"), + OsString::from("/MANIFEST:EMBED"), + prefixed_path_argument("/OUT:", executable_path), + prefixed_path_argument("/PDB:", pdb_path), + object_path.as_os_str().to_owned(), + launcher.archive_path.as_os_str().to_owned(), + ]; + arguments.extend(launcher.native_library_args.iter().cloned()); + arguments +} + +#[cfg(any(windows, test))] +fn prefixed_path_argument(prefix: &str, path: &Path) -> OsString { + let mut argument = OsString::from(prefix); + argument.push(path.as_os_str()); + argument +} + +#[derive(Debug)] +struct CommandOutput { + status: ExitStatus, + stdout: Vec, + stderr: Vec, +} + +fn run_command_with_timeout( + mut command: Command, + program: &Path, + timeout: Duration, +) -> Result { + // Retained files do not require EOF from inherited descendant handles. + // Duplicate append-only handles; positioned reads keep a bounded snapshot. + let stdout = capture_file("stdout")?; + let stderr = capture_file("stderr")?; + command + .stdout(capture_writer(stdout.as_file(), "stdout")?) + .stderr(capture_writer(stderr.as_file(), "stderr")?); + let child = command + .spawn() + .map_err(|source| NativeBuildError::SpawnLinker { + linker: program.to_path_buf(), + source, + })?; + // Every return path either observes a reaped child or terminates/reaps it. + let mut child = ManagedChild::new(child); + let start = Instant::now(); + + let (status, timed_out) = loop { + match child.try_wait() { + Ok(Some(status)) => break (status, false), + Ok(None) if start.elapsed() >= timeout => { + let status = child.terminate_and_wait(program)?; + break (status, true); + } + Ok(None) => { + let remaining = timeout.saturating_sub(start.elapsed()); + thread::sleep(remaining.min(Duration::from_millis(10))); + } + Err(source) => { + return Err(NativeBuildError::PollLinker { + linker: program.to_path_buf(), + source, + }); + } + } + }; + + let stdout = read_output_snapshot(stdout.as_file(), "stdout")?; + let stderr = read_output_snapshot(stderr.as_file(), "stderr")?; + if timed_out { + return Err(NativeBuildError::LinkTimedOut { + linker: program.to_path_buf(), + timeout, + stdout: String::from_utf8_lossy(&stdout).into_owned(), + stderr: String::from_utf8_lossy(&stderr).into_owned(), + }); + } + Ok(CommandOutput { + status, + stdout, + stderr, + }) +} + +struct ManagedChild { + child: Child, + reaped: bool, +} + +impl ManagedChild { + fn new(child: Child) -> Self { + Self { + child, + reaped: false, + } + } + + fn try_wait(&mut self) -> io::Result> { + let status = self.child.try_wait()?; + if status.is_some() { + self.reaped = true; + } + Ok(status) + } + + fn terminate_and_wait(&mut self, program: &Path) -> Result { + self.child + .kill() + .map_err(|source| NativeBuildError::TerminateLinker { + linker: program.to_path_buf(), + source, + })?; + let status = self + .child + .wait() + .map_err(|source| NativeBuildError::PollLinker { + linker: program.to_path_buf(), + source, + })?; + self.reaped = true; + Ok(status) + } +} + +impl Drop for ManagedChild { + fn drop(&mut self) { + if !self.reaped { + let _ = self.child.kill(); + let _ = self.child.wait(); + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CaptureStage { + Create, + Duplicate, + Snapshot, +} + +#[derive(Debug)] +struct CaptureIoError { + stage: CaptureStage, + source: io::Error, +} + +impl fmt::Display for CaptureIoError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let stage = match self.stage { + CaptureStage::Create => "create", + CaptureStage::Duplicate => "duplicate", + CaptureStage::Snapshot => "snapshot", + }; + write!(formatter, "{stage} capture: {}", self.source) + } +} + +impl std::error::Error for CaptureIoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +fn capture_error(stream: &'static str, stage: CaptureStage, source: io::Error) -> NativeBuildError { + NativeBuildError::CaptureLinkerOutput { + stream, + source: io::Error::new(source.kind(), CaptureIoError { stage, source }), + } +} + +fn capture_file(stream: &'static str) -> Result { + // Windows positioned reads update the shared cursor. Append-only writes + // still target EOF when a surviving descendant writes during a snapshot. + tempfile::Builder::new() + .append(true) + .tempfile() + .map_err(|source| capture_error(stream, CaptureStage::Create, source)) +} + +fn capture_writer(file: &fs::File, stream: &'static str) -> Result { + file.try_clone() + .map_err(|source| capture_error(stream, CaptureStage::Duplicate, source)) +} + +struct CaptureSnapshot<'a> { + file: &'a fs::File, + offset: u64, +} + +impl Read for CaptureSnapshot<'_> { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let count = read_capture_at(self.file, buffer, self.offset)?; + let advance = u64::try_from(count).map_err(io::Error::other)?; + self.offset = self.offset.checked_add(advance).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "capture snapshot offset overflow", + ) + })?; + Ok(count) + } +} + +#[cfg(unix)] +fn read_capture_at(file: &fs::File, buffer: &mut [u8], offset: u64) -> io::Result { + use std::os::unix::fs::FileExt; + file.read_at(buffer, offset) +} + +#[cfg(windows)] +fn read_capture_at(file: &fs::File, buffer: &mut [u8], offset: u64) -> io::Result { + use std::os::windows::fs::FileExt; + file.seek_read(buffer, offset) +} + +#[cfg(not(any(unix, windows)))] +fn read_capture_at(_file: &fs::File, _buffer: &mut [u8], _offset: u64) -> io::Result { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "positioned native capture is unavailable on this host", + )) +} + +fn read_output_snapshot( + file: &fs::File, + stream: &'static str, +) -> Result, NativeBuildError> { + let read = || -> io::Result> { + let length = file.metadata()?.len(); + let mut bytes = Vec::new(); + CaptureSnapshot { file, offset: 0 } + .take(length) + .read_to_end(&mut bytes)?; + Ok(bytes) + }; + read().map_err(|source| capture_error(stream, CaptureStage::Snapshot, source)) +} + +#[cfg(windows)] +fn publish_executable(from: &Path, to: &Path) -> Result<(), NativeBuildError> { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Storage::FileSystem::{ + MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW, + }; + + let from_wide: Vec = from.as_os_str().encode_wide().chain(Some(0)).collect(); + let to_wide: Vec = to.as_os_str().encode_wide().chain(Some(0)).collect(); + // SAFETY: both buffers are live, immutable, NUL-terminated UTF-16 paths + // for the duration of the call. The destination was validated as either + // absent or a regular file, and REPLACE_EXISTING preserves it if the move + // itself fails. + let published = unsafe { + MoveFileExW( + from_wide.as_ptr(), + to_wide.as_ptr(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, + ) + }; + if published == 0 { + return Err(NativeBuildError::PublishExecutable { + from: from.to_path_buf(), + to: to.to_path_buf(), + source: io::Error::last_os_error(), + }); + } + Ok(()) +} + +#[cfg(not(windows))] +fn publish_executable(from: &Path, to: &Path) -> Result<(), NativeBuildError> { + fs::rename(from, to).map_err(|source| NativeBuildError::PublishExecutable { + from: from.to_path_buf(), + to: to.to_path_buf(), + source, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn compile_observer_source(body: &str) -> String { + format!( + "namespace app\nfunction observe() returns int64:\n print(\"compile:\")\n return 7\n{body}\nverify checking:\n println(\"verify\")\n assert true\n" + ) + } + + fn compile_observer_oracle() -> Vec { + vec![ + crate::DebugObservation { + phase: crate::DebugPhase::Comptime, + event: crate::DebugEvent { + kind: crate::DebugEventKind::Print, + text: "compile:".into(), + }, + }, + crate::DebugObservation { + phase: crate::DebugPhase::FrontendVerify, + event: crate::DebugEvent { + kind: crate::DebugEventKind::Println, + text: "verify\n".into(), + }, + }, + ] + } + + #[test] + fn native_object_and_link_setup_failure_retain_completed_frontend_events() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + compile_observer_source( + "function main() returns nothing:\n int64 baked = comptime observe()", + ), + ) + .unwrap(); + let object = emit_host_program_object_for_file(&source).unwrap(); + assert_eq!(object.debug_observations(), compile_observer_oracle()); + let ordinary = emit_host_object_for_file(&source).unwrap(); + assert_eq!(ordinary.debug_observations(), compile_observer_oracle()); + let launcher = match host_target().as_str() { + WINDOWS_MSVC_NATIVE_TARGET => { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("missing.lib")) + } + LINUX_GNU_NATIVE_TARGET => { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("missing.a")) + } + _ => return, + }; + let output = directory.path().join("preserved.exe"); + fs::write(&output, b"existing publication").unwrap(); + let error = link_host_object(&object, &launcher, &output).unwrap_err(); + assert_eq!(error.debug_observations(), compile_observer_oracle()); + assert!(error.to_string().contains("missing."), "{error}"); + assert!(std::error::Error::source(&error).is_some()); + assert_eq!(fs::read(&output).unwrap(), b"existing publication"); + } + + #[test] + fn native_entry_refusal_keeps_events_and_sum_domain_rejection_prevents_them() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + compile_observer_source("int64 baked = comptime observe()"), + ) + .unwrap(); + let error = emit_host_program_object_for_file(&source).unwrap_err(); + assert_eq!(error.debug_observations(), compile_observer_oracle()); + let NativeBuildError::Captured { source: inner, .. } = error else { + panic!("entry refusal should retain captures"); + }; + assert!(matches!( + *inner, + NativeBuildError::MissingProgramEntry { .. } + )); + fs::write( + &source, + r#"namespace app +function observe() returns int64: + print("compile:") + return 7 +type Positive = int64 where value > 0 +function main() returns nothing: + int64 baked = comptime observe() + list[Positive] values = list() + Positive total = list.sum[Positive](view values) +verify checking: + println("verify") + assert true +"#, + ) + .unwrap(); + let error = emit_host_program_object_for_file(&source).unwrap_err(); + assert!(error.debug_observations().is_empty()); + let NativeBuildError::Lowering { source, .. } = error else { + panic!("sum domain must be rejected before frontend evaluation: {error:?}"); + }; + let built = source + .build_result() + .expect("sum-domain type checking failure"); + assert!(built.diagnostics.iter().any(|error| { + error.code.code() == 300 && error.message.contains("numeric primitive for list.sum") + })); + } + + #[test] + fn installed_launcher_manifest_validates_identity_profile_and_archive_location() { + let directory = tempfile::tempdir().unwrap(); + let host = host_target(); + let bundle = match host.as_str() { + LINUX_GNU_NATIVE_TARGET => { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("launcher.a")) + } + WINDOWS_MSVC_NATIVE_TARGET => { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("launcher.lib")) + } + _ => return, + }; + fs::write(&bundle.archive_path, b"test archive placeholder").unwrap(); + let manifest = directory.path().join("launcher.json"); + let value = serde_json::json!({ + "manifest_version": 1, + "compiler_version": env!("CARGO_PKG_VERSION"), + "target": bundle.target, + "runtime_abi_version": bundle.runtime_abi_version, + "crt_mode": bundle.crt_mode.to_string(), + "profile": "debug", + "archive": bundle.archive_path.file_name().unwrap().to_str().unwrap(), + "native_library_args": bundle.native_library_args.iter().map(|arg| arg.to_str().unwrap()).collect::>(), + }); + fs::write(&manifest, serde_json::to_vec(&value).unwrap()).unwrap(); + assert_eq!( + NativeLauncherBundle::from_manifest(&manifest, false).unwrap(), + bundle + ); + assert!( + NativeLauncherBundle::from_manifest(&manifest, true) + .unwrap_err() + .contains("profile mismatch") + ); + for (field, invalid) in [ + ("manifest_version", serde_json::json!(99)), + ("compiler_version", serde_json::json!("incompatible")), + ("target", serde_json::json!("wasm32-unknown-unknown")), + ("runtime_abi_version", serde_json::json!(99)), + ("crt_mode", serde_json::json!("unknown")), + ("native_library_args", serde_json::json!([])), + ("archive", serde_json::json!("../outside.a")), + ] { + let mut changed = value.clone(); + changed[field] = invalid; + fs::write(&manifest, serde_json::to_vec(&changed).unwrap()).unwrap(); + assert!( + NativeLauncherBundle::from_manifest(&manifest, false).is_err(), + "{field}" + ); + } + } + + #[test] + fn unsupported_host_diagnostic_names_both_native_hosts() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + "function main() returns nothing:\n return nothing\n", + ) + .unwrap(); + let object = emit_host_program_object_for_file(&source).unwrap(); + let error = validate_link_host_for(&object, "aarch64-unknown-linux-gnu".to_owned()) + .expect_err("unsupported hosts must not silently cross-link"); + assert!(matches!(error, NativeBuildError::UnsupportedHost { .. })); + let diagnostic = error.to_string(); + assert!( + diagnostic.contains("aarch64-unknown-linux-gnu"), + "{diagnostic}" + ); + assert!( + diagnostic.contains(WINDOWS_MSVC_NATIVE_TARGET), + "{diagnostic}" + ); + assert!(diagnostic.contains(LINUX_GNU_NATIVE_TARGET), "{diagnostic}"); + } + + #[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))] + #[test] + fn linux_cc_override_child() { + let Some(directory) = std::env::var_os("JETT_TEST_CC_DIRECTORY") else { + return; + }; + let directory = PathBuf::from(directory); + let result = link_linux_gnu( + &directory.join("main.o"), + &NativeLauncherBundle::linux_gnu_v1(directory.join("support.o")), + &directory.join("program"), + &directory.join("build directory"), + Duration::from_secs(10), + ); + if std::env::var_os("JETT_TEST_CC_MISSING").is_some() { + assert!( + matches!(result, Err(NativeBuildError::SpawnLinker { source, .. }) + if source.kind() == io::ErrorKind::NotFound) + ); + } else { + result.expect("literal compiler path must link from a different cwd"); + assert_eq!( + Command::new(directory.join("program")) + .status() + .unwrap() + .code(), + Some(42) + ); + } + } + + #[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))] + #[test] + fn linux_cc_overrides_are_literal_and_relative_to_the_caller() { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + fs::create_dir(root.join("tool chain")).unwrap(); + fs::create_dir(root.join("build directory")).unwrap(); + let cc = std::env::split_paths(&std::env::var_os("PATH").unwrap()) + .map(|path| path.join("cc")) + .find(|path| path.is_file()) + .expect("host C compiler"); + let cc = fs::canonicalize(cc).unwrap(); + let compiler = root.join("tool chain/cc literal"); + std::os::unix::fs::symlink(&cc, &compiler).unwrap(); + std::os::unix::fs::symlink(&cc, root.join("tool chain/cc")).unwrap(); + for (name, source) in [ + ( + "main", + "extern int answer(void); int main(void) { return answer(); }", + ), + ("support", "int answer(void) { return 42; }"), + ] { + fs::write(root.join(format!("{name}.c")), source).unwrap(); + assert!( + Command::new(&cc) + .arg("-c") + .arg(root.join(format!("{name}.c"))) + .arg("-o") + .arg(root.join(format!("{name}.o"))) + .status() + .unwrap() + .success() + ); + } + let path = std::env::join_paths( + std::iter::once(root.join("tool chain")) + .chain(std::env::split_paths(&std::env::var_os("PATH").unwrap())), + ) + .unwrap(); + for (compiler, missing) in [ + (compiler, false), + (PathBuf::from("cc"), false), + (PathBuf::from("./tool chain/missing compiler"), true), + (PathBuf::from("./tool chain/cc literal"), false), + ] { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args([ + "--exact", + "native::tests::linux_cc_override_child", + "--nocapture", + ]) + .current_dir(root) + .env("JETT_NATIVE_CC", &compiler) + .env("JETT_TEST_CC_DIRECTORY", root) + .env("PATH", &path) + .env_remove("JETT_TEST_CC_MISSING"); + if missing { + command.env("JETT_TEST_CC_MISSING", "1"); + } + let output = command.output().unwrap(); + assert!( + output.status.success(), + "compiler {}: {output:?}", + compiler.display() + ); + } + } + + #[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))] + #[test] + fn linux_launcher_contract_rejects_incompatible_metadata() { + let canonical = NativeLauncherBundle::linux_gnu_v1("launcher.a"); + assert!(validate_host_launcher(&canonical).is_ok()); + let mut wrong = canonical.clone(); + wrong.target = WINDOWS_MSVC_NATIVE_TARGET.to_owned(); + assert!(matches!( + validate_host_launcher(&wrong), + Err(NativeBuildError::LauncherTargetMismatch { .. }) + )); + wrong = canonical.clone(); + wrong.runtime_abi_version += 1; + assert!(matches!( + validate_host_launcher(&wrong), + Err(NativeBuildError::LauncherRuntimeAbiMismatch { .. }) + )); + wrong = canonical.clone(); + wrong.crt_mode = NativeCrtMode::Static; + assert!(matches!( + validate_host_launcher(&wrong), + Err(NativeBuildError::LauncherCrtMismatch { .. }) + )); + wrong = canonical; + wrong.native_library_args.pop(); + assert!(matches!( + validate_host_launcher(&wrong), + Err(NativeBuildError::LauncherNativeLibrariesMismatch { .. }) + )); + } + + #[cfg(any(unix, windows))] + #[test] + fn capture_snapshots_retain_handle_identity_and_append_after_cursor_changes() { + use std::io::{Seek, SeekFrom, Write}; + + let capture = capture_file("stdout").unwrap(); + let mut writer = capture_writer(capture.as_file(), "stdout").unwrap(); + assert!( + read_output_snapshot(capture.as_file(), "stdout") + .unwrap() + .is_empty() + ); + writer.write_all(b"prefix").unwrap(); + assert_eq!( + read_output_snapshot(capture.as_file(), "stdout").unwrap(), + b"prefix" + ); + // A duplicate writer must remain append-only after the parent moves + // their shared cursor. No reopen is used to restore an independent one. + capture.as_file().seek(SeekFrom::Start(1)).unwrap(); + writer.write_all(b"-suffix").unwrap(); + for _ in 0..2 { + assert_eq!( + read_output_snapshot(capture.as_file(), "stdout").unwrap(), + b"prefix-suffix" + ); + } + } + + #[cfg(any(unix, windows))] + #[test] + fn capture_snapshot_bounds_multichunk_reads_despite_interleaved_appends() { + use std::io::Write; + + let capture = capture_file("stderr").unwrap(); + let mut writer = capture_writer(capture.as_file(), "stderr").unwrap(); + let prefix = b"0123456789abcdef".repeat(8 * 1024); + let suffix = b"later-output".repeat(1024); + writer.write_all(&prefix).unwrap(); + let sampled_length = capture.as_file().metadata().unwrap().len(); + let mut snapshot = CaptureSnapshot { + file: capture.as_file(), + offset: 0, + } + .take(sampled_length); + let mut first = [0; 17]; + snapshot.read_exact(&mut first).unwrap(); + // On Windows this append moves the shared cursor to EOF. The next + // snapshot read must still use its own offset and the original bound. + writer.write_all(&suffix).unwrap(); + let mut captured = first.to_vec(); + snapshot.read_to_end(&mut captured).unwrap(); + assert_eq!(captured, prefix); + let mut all_output = prefix; + all_output.extend_from_slice(&suffix); + assert_eq!( + read_output_snapshot(capture.as_file(), "stderr").unwrap(), + all_output + ); + + // Short EOF remains valid if the file is shorter than a sampled bound. + let mut short = Vec::new(); + CaptureSnapshot { + file: capture.as_file(), + offset: 0, + } + .take(u64::try_from(all_output.len()).unwrap() + 1) + .read_to_end(&mut short) + .unwrap(); + assert_eq!(short, all_output); + } + + #[test] + fn capture_error_context_preserves_stream_kind_and_original_os_error() { + for stage in [ + CaptureStage::Create, + CaptureStage::Duplicate, + CaptureStage::Snapshot, + ] { + let original = io::Error::from_raw_os_error(5); + let kind = original.kind(); + let NativeBuildError::CaptureLinkerOutput { stream, source } = + capture_error("stderr", stage, original) + else { + panic!("capture context changed the public error variant"); + }; + assert_eq!(stream, "stderr"); + assert_eq!(source.kind(), kind); + let detail = source + .get_ref() + .unwrap() + .downcast_ref::() + .unwrap(); + assert_eq!(detail.stage, stage); + assert_eq!(detail.source.raw_os_error(), Some(5)); + let nested = std::error::Error::source(detail).unwrap(); + assert_eq!( + nested.downcast_ref::().unwrap().raw_os_error(), + Some(5) + ); + } + } + + #[cfg(windows)] + #[test] + fn command_runner_captures_windows_streams_and_nonzero_status() { + use std::os::windows::process::CommandExt; + + let mut command = Command::new("cmd.exe"); + command + .args(["/d", "/s", "/c"]) + .raw_arg(r#"&2&exit /b 7"#) + .stdin(Stdio::null()); + let output = + run_command_with_timeout(command, Path::new("cmd.exe"), Duration::from_secs(5)) + .unwrap(); + assert_eq!(output.status.code(), Some(7)); + assert_eq!(output.stdout, b"captured"); + assert_eq!(output.stderr, b"diagnostic"); + } + + #[cfg(windows)] + #[test] + fn command_windows_deadline_does_not_wait_for_inherited_capture_handles() { + use std::os::windows::process::CommandExt; + + let mut command = Command::new("cmd.exe"); + command + .args(["/d", "/s", "/c"]) + .raw_arg(r#"&2&start "" /b cmd.exe /d /c "ping.exe -n 6 127.0.0.1 >nul"&ping.exe -n 6 127.0.0.1 >nul"#) + .stdin(Stdio::null()); + let start = Instant::now(); + let error = + run_command_with_timeout(command, Path::new("cmd.exe"), Duration::from_millis(500)) + .expect_err("parent must time out while a descendant retains the captures"); + assert!( + start.elapsed() < Duration::from_secs(3), + "inherited handles defeated the deadline" + ); + match error { + NativeBuildError::LinkTimedOut { stdout, stderr, .. } => { + assert_eq!(stdout, "captured"); + assert_eq!(stderr, "diagnostic"); + } + error => panic!("unexpected error: {error}"), + } + } + + #[cfg(unix)] + #[test] + fn command_runner_captures_large_streams_and_failure_status() { + let stdout = "o".repeat(96 * 1024); + let stderr = "e".repeat(96 * 1024); + let mut command = Command::new("sh"); + command + .args([ + "-c", + "printf '%s' \"$1\"; printf '%s' \"$2\" >&2; exit 7", + "sh", + &stdout, + &stderr, + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let output = + run_command_with_timeout(command, Path::new("sh"), Duration::from_secs(3)).unwrap(); + assert_eq!(output.status.code(), Some(7)); + assert_eq!(output.stdout, stdout.as_bytes()); + assert_eq!(output.stderr, stderr.as_bytes()); + } + + #[cfg(unix)] + #[test] + fn command_deadline_does_not_wait_for_inherited_output_pipes() { + let mut command = Command::new("sh"); + command + .args([ + "-c", + "printf captured; printf diagnostic >&2; sleep 4 & wait", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let start = Instant::now(); + let error = run_command_with_timeout(command, Path::new("sh"), Duration::from_millis(100)) + .expect_err("shell must time out even when its child retains output handles"); + assert!( + start.elapsed() < Duration::from_secs(2), + "inherited pipes defeated the deadline" + ); + match error { + NativeBuildError::LinkTimedOut { stdout, stderr, .. } => { + assert_eq!(stdout, "captured"); + assert_eq!(stderr, "diagnostic"); + } + error => panic!("unexpected error: {error}"), + } + } + + #[cfg(unix)] + #[test] + fn command_runner_enforces_unix_deadline_and_reaps_child() { + let mut command = Command::new("sleep"); + command + .arg("10") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let start = Instant::now(); + assert!(matches!( + run_command_with_timeout(command, Path::new("sleep"), Duration::from_millis(25)), + Err(NativeBuildError::LinkTimedOut { .. }) + )); + assert!(start.elapsed() < Duration::from_secs(3)); + } + + #[test] + fn canonical_launcher_metadata_is_complete_and_static() { + let bundle = NativeLauncherBundle::windows_msvc_static_v1("launcher.lib"); + + assert_eq!(bundle.target, WINDOWS_MSVC_NATIVE_TARGET); + assert_eq!(bundle.runtime_abi_version, NATIVE_RUNTIME_ABI_VERSION_V1); + assert_eq!(bundle.crt_mode, NativeCrtMode::Static); + assert_eq!( + bundle.native_library_args, + WINDOWS_MSVC_STATIC_V1_NATIVE_LIBRARIES + .iter() + .map(OsString::from) + .collect::>() + ); + assert!(validate_launcher(&bundle).is_ok()); + } + + #[test] + fn launcher_contract_rejects_each_incompatible_dimension() { + let canonical = NativeLauncherBundle::windows_msvc_static_v1("launcher.lib"); + + let mut target = canonical.clone(); + target.target = "aarch64-pc-windows-msvc".to_string(); + assert!(matches!( + validate_launcher(&target), + Err(NativeBuildError::LauncherTargetMismatch { .. }) + )); + + let mut abi = canonical.clone(); + abi.runtime_abi_version += 1; + assert!(matches!( + validate_launcher(&abi), + Err(NativeBuildError::LauncherRuntimeAbiMismatch { .. }) + )); + + let mut crt = canonical.clone(); + crt.crt_mode = NativeCrtMode::Dynamic; + assert!(matches!( + validate_launcher(&crt), + Err(NativeBuildError::LauncherCrtMismatch { .. }) + )); + + let mut libraries = canonical; + libraries.native_library_args.pop(); + assert!(matches!( + validate_launcher(&libraries), + Err(NativeBuildError::LauncherNativeLibrariesMismatch { .. }) + )); + } + + #[test] + fn linker_paths_with_spaces_remain_single_arguments() { + let bundle = + NativeLauncherBundle::windows_msvc_static_v1(r"C:\launcher bundle\jett launcher.lib"); + let arguments = linker_arguments( + Path::new(r"C:\build area\program.obj"), + &bundle, + Path::new(r"C:\build area\program.exe"), + Path::new(r"C:\build area\program.pdb"), + ); + + assert!(arguments.contains(&OsString::from(r"C:\build area\program.obj"))); + assert!(arguments.contains(&OsString::from(r"C:\launcher bundle\jett launcher.lib"))); + assert!(arguments.contains(&OsString::from(r"/OUT:C:\build area\program.exe"))); + assert!(arguments.contains(&OsString::from(r"/PDB:C:\build area\program.pdb"))); + assert!(arguments.contains(&OsString::from("/MACHINE:X64"))); + assert!(!arguments.iter().any(|argument| argument == "cmd.exe")); + } + + #[test] + fn program_object_uses_the_checked_entry_and_exports_only_its_wrapper_variant() { + let directory = tempfile::tempdir().expect("temporary source directory"); + let source_path = directory.path().join("entry.jett"); + fs::write( + &source_path, + "function main() returns nothing:\n int64 answer = 42\n", + ) + .expect("write native source"); + + let program = emit_host_program_object_for_file(&source_path) + .expect("program object should lower and emit"); + assert_eq!( + program.symbols().last().map(String::as_str), + Some(jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + + let ordinary = + emit_host_object_for_file(&source_path).expect("ordinary object should emit"); + assert!( + !ordinary + .symbols() + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + } + + #[test] + fn program_object_does_not_invent_an_entry_for_a_verification_only_file() { + let source_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/simple.jett"); + + assert!(matches!( + emit_host_program_object_for_file(&source_path), + Err(NativeBuildError::MissingProgramEntry { .. }) + )); + } + + #[cfg(windows)] + #[test] + fn command_runner_enforces_its_deadline_and_reaps_the_process() { + let mut command = Command::new("ping.exe"); + command + .args(["-n", "6", "127.0.0.1"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let error = + run_command_with_timeout(command, Path::new("ping.exe"), Duration::from_millis(25)) + .expect_err("the long-running child must time out"); + assert!(matches!(error, NativeBuildError::LinkTimedOut { .. })); + } + + #[test] + fn publication_atomically_replaces_an_existing_executable() { + let directory = tempfile::tempdir().expect("temporary publication directory"); + let linked = directory.path().join("linked.exe"); + let output = directory.path().join("published.exe"); + fs::write(&linked, b"new executable").expect("write linked executable"); + fs::write(&output, b"old executable").expect("write existing executable"); + + publish_executable(&linked, &output).expect("publish executable"); + + assert_eq!( + fs::read(&output).expect("read published executable"), + b"new executable" + ); + assert!(!linked.exists()); + } + + #[test] + fn failed_publication_preserves_an_existing_executable() { + let directory = tempfile::tempdir().expect("temporary publication directory"); + let missing = directory.path().join("missing.exe"); + let output = directory.path().join("published.exe"); + fs::write(&output, b"old executable").expect("write existing executable"); + + assert!(matches!( + publish_executable(&missing, &output), + Err(NativeBuildError::PublishExecutable { .. }) + )); + assert_eq!( + fs::read(&output).expect("read preserved executable"), + b"old executable" + ); + } + + #[cfg(windows)] + #[test] + fn output_validation_rejects_a_non_executable_extension_before_linking() { + let path = std::env::current_dir() + .expect("current directory") + .join("native-output.txt"); + + assert!(matches!( + validate_output_path(&path), + Err(NativeBuildError::InvalidExtension { + role: NativePathRole::Output, + expected: "exe", + .. + }) + )); + } +} + +#[cfg(all( + test, + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +#[path = "native_property_tests.rs"] +mod property_tests; + +#[cfg(all( + test, + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +#[path = "native/actor_graph_hooks_tests.rs"] +mod actor_graph_hooks_tests; diff --git a/crates/jett_driver/src/native/actor_graph_hooks_tests.rs b/crates/jett_driver/src/native/actor_graph_hooks_tests.rs new file mode 100644 index 00000000..dab96f72 --- /dev/null +++ b/crates/jett_driver/src/native/actor_graph_hooks_tests.rs @@ -0,0 +1,332 @@ +//! The exact protected Support constructor feeds the public checked Driver bake. +//! Only this child test can wrap the resulting validated object for the existing +//! public normal linker; no production object/manifest authority is exposed. +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::{DefKind, ResourceKernelSpec}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, + time::{Duration, Instant}, +}; +const SUPPORT: &str = + include_str!("../../../jett_comptime/src/resource_execution/fixtures/resource_probe.jett"); +const INITIALIZER: &str = + include_str!("../../tests/actor_graph/fixtures/07_nested_hook_actor.jett"); +const CAPTURE: &str = + include_str!("../../tests/actor_graph/fixtures/08_nested_hook_capture_actor.jett"); +fn evidence() -> PathBuf { + std::env::var_os("JETT_ACTOR_GRAPH_EVIDENCE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/actor-graph-tests") + }) +} +fn profile() -> &'static str { + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } +} +struct Launcher { + bundle: NativeLauncherBundle, + _directory: tempfile::TempDir, +} + +fn launcher() -> &'static NativeLauncherBundle { + if let Some(manifest) = std::env::var_os("JETT_ACTOR_GRAPH_BUNDLE_MANIFEST") { + static BUNDLE: OnceLock = OnceLock::new(); + return BUNDLE.get_or_init(|| { + NativeLauncherBundle::from_manifest(Path::new(&manifest), !cfg!(debug_assertions)) + .expect("fresh ordinary runtime bundle matching this test profile") + }); + } + static LAUNCHER: OnceLock = OnceLock::new(); + &LAUNCHER + .get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + let cargo_profile = if profile == "debug" { "test" } else { profile }; + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let status = Command::new(env!("CARGO")) + .args(["build", "-q", "-p", "jett_native_launcher"]) + .args([ + "--target", + &host, + "--profile", + cargo_profile, + "--target-dir", + ]) + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build host- and profile-matched launcher"); + assert!(status.success(), "launcher build failed: {status}"); + let archive_name = if cfg!(windows) { + "jett_native_launcher.lib" + } else { + "libjett_native_launcher.a" + }; + let archive = target.join(&host).join(profile).join(archive_name); + let directory = tempfile::tempdir().expect("launcher bundle directory"); + let copied = directory.path().join(archive_name); + std::fs::copy(&archive, &copied).unwrap_or_else(|error| { + panic!( + "cannot copy launcher archive {}: {error}", + archive.display() + ) + }); + Launcher { + bundle: if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(copied) + } else { + NativeLauncherBundle::linux_gnu_v1(copied) + }, + _directory: directory, + } + }) + .bundle +} +fn checked( + source: &str, + release: bool, +) -> ( + Arc, + jett_common::Span, + jett_common::Span, +) { + let project = FileId::new(0); + let support = FileId::new(10_000); + let mut parsed = parse(SUPPORT, support); + let primary = parse(source, project); + assert!(parsed.errors.is_empty()); + assert!(primary.errors.is_empty()); + let entries = primary + .module + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) if function.name.name == "main" => { + Some((function.span, function.name.span)) + } + _ => None, + }) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("unique original main") + }; + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [resource] = resources.as_slice() else { + panic!("unique original protected Resource") + }; + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + let value = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (project, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap(); + (Arc::new(value), entry.0, entry.1) +} +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn run_bounded(binary: &Path, cwd: &Path) -> (std::process::Output, bool) { + let mut out = tempfile::NamedTempFile::new().unwrap(); + let mut err = tempfile::NamedTempFile::new().unwrap(); + let mut child = Command::new(binary) + .current_dir(cwd) + .env_clear() + .stdin(Stdio::null()) + .stdout(out.reopen().unwrap()) + .stderr(err.reopen().unwrap()) + .spawn() + .unwrap(); + let start = Instant::now(); + let mut timed_out = false; + let status = loop { + if let Some(status) = child.try_wait().unwrap() { + break status; + } + if start.elapsed() > Duration::from_secs(20) { + timed_out = true; + let _ = child.kill(); + break child.wait().unwrap(); + } + thread::sleep(Duration::from_millis(10)); + }; + let mut stdout = Vec::new(); + out.read_to_end(&mut stdout).unwrap(); + let mut stderr = Vec::new(); + err.read_to_end(&mut stderr).unwrap(); + ( + std::process::Output { + status, + stdout, + stderr, + }, + timed_out, + ) +} +#[test] +fn actor_graph_checked_hook_original_initializer_and_capture_native() { + let mut rows = Vec::new(); + for (name, source) in [ + ("original_initializer", INITIALIZER), + ("original_capture", CAPTURE), + ] { + for release in [false, true] { + let (program, span, name_span) = checked(source, release); + let required = crate::evaluate_checked_resource_required_values(&program); + assert!( + required.diagnostics.is_empty(), + "{name}: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + let definitions = program + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Function && definition.span == name_span + }) + .collect::>(); + let [definition] = definitions.as_slice() else { + panic!("exact checked entry DefId") + }; + let mut reference = jett_comptime::Interpreter::new(); + reference + .install_checked_resource_program( + program.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + reference.register_module(program.module()); + reference.set_explicit_comptime_values(Arc::new(required.values.clone())); + assert_eq!( + reference + .call_checked_program_entry(definition.id, Vec::new()) + .unwrap(), + jett_comptime::Value::Nothing + ); + let events = reference.take_debug_events(); + assert!(events.is_empty()); + let mut hir = jett_hir::lower_checked_resource_program(&program).unwrap(); + crate::bake_checked_resource_values(&mut hir, &program, &required.values).unwrap(); + jett_hir::complete_value_conversions(&mut hir, &program.checked().interner).unwrap(); + let entries = hir + .functions + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("exact HIR entry") + }; + let mir = jett_mir::lower(&hir, &program.checked().interner).unwrap(); + jett_mir::validate(&mir).unwrap(); + let emitted = jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &program.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .unwrap(); + let object = NativeProgramObjectArtifact { + object: native_object(emitted.target, emitted.symbols, emitted.bytes).unwrap(), + program_entry: *entry, + diagnostics: Vec::new(), + source: source.into(), + }; + let directory = evidence() + .join("native-hooks") + .join(profile()) + .join(format!( + "{name}-{}", + if release { + "release-source" + } else { + "debug-source" + } + )); + fs::create_dir_all(&directory).unwrap(); + let source_path = directory.join("main.jett"); + let support_path = directory.join("resource_probe.jett"); + fs::write(&source_path, source).unwrap(); + fs::write(&support_path, SUPPORT).unwrap(); + fs::write(directory.join("final.obj"), object.bytes()).unwrap(); + let binary = directory.join(if cfg!(windows) { + "graph-hook.exe" + } else { + "graph-hook" + }); + link_host_object(&object, launcher(), &binary).unwrap(); + fs::remove_file(source_path).unwrap(); + fs::remove_file(support_path).unwrap(); + let executable_bytes = fs::read(&binary).unwrap(); + let executable_sha256 = digest(&executable_bytes); + let executable_length = executable_bytes.len(); + drop(executable_bytes); + let (native, timed_out) = run_bounded(&binary, &directory); + fs::write(directory.join("native.stdout"), &native.stdout).unwrap(); + fs::write(directory.join("native.stderr"), &native.stderr).unwrap(); + rows.push(serde_json::json!({"name":name,"runtime_profile":profile(),"release_source":release,"required_events":0,"reference_events":format!("{events:?}"),"object_bytes":object.bytes().len(),"object_sha256":digest(object.bytes()),"executable_bytes":executable_length,"executable_sha256":executable_sha256,"source_removed":true,"support_removed":true,"native_exit":native.status.code(),"native_timed_out":timed_out,"native_terminal_reaped":true,"native_stdout":String::from_utf8_lossy(&native.stdout),"native_stderr":String::from_utf8_lossy(&native.stderr)})); + fs::write( + evidence().join(format!("native-hooks-{}.json", profile())), + serde_json::to_string_pretty(&rows).unwrap(), + ) + .unwrap(); + fs::remove_file(&binary).unwrap(); + assert!( + !binary.exists(), + "{name} case executable removed after reap" + ); + assert!(!timed_out, "{name} graph hook timeout"); + assert!(native.status.success(), "{name}: {native:?}"); + assert!(native.stdout.is_empty()); + assert!(native.stderr.is_empty()); + } + } +} diff --git a/crates/jett_driver/src/native_builder_constants.rs b/crates/jett_driver/src/native_builder_constants.rs new file mode 100644 index 00000000..cdcf8ae2 --- /dev/null +++ b/crates/jett_driver/src/native_builder_constants.rs @@ -0,0 +1,2 @@ +//! Reconstruction is shared with the sealed HIR actor graph factory. +pub(super) use jett_hir::evaluated_values::value_expression; diff --git a/crates/jett_driver/src/native_constants.rs b/crates/jett_driver/src/native_constants.rs new file mode 100644 index 00000000..8bd1f0e7 --- /dev/null +++ b/crates/jett_driver/src/native_constants.rs @@ -0,0 +1,2 @@ +//! All explicitly evaluated values share the sealed HIR reconstruction producer. +pub(crate) use jett_hir::bake_required_values as bake_values; diff --git a/crates/jett_driver/src/native_property_cases.rs b/crates/jett_driver/src/native_property_cases.rs new file mode 100644 index 00000000..c0b378fa --- /dev/null +++ b/crates/jett_driver/src/native_property_cases.rs @@ -0,0 +1,390 @@ +//! Translate deterministic property inputs into typed, compiler-owned HIR. +//! The interpreter chooses cases; only compiled Jett code executes their bodies. + +use jett_common::{FileId, Span}; +use jett_comptime::value::{ClosureTypeContext, Value}; +use jett_comptime::verify::{PROPERTY_DEFAULT_ITERATIONS, PropertyCase}; +use jett_hir::{ + Block, DeclarationId, DeclarationKind, Expression, ExpressionKind, Function, FunctionDebugKind, + FunctionId, FunctionIdentity, HandleKind, IntrinsicId, Local, LocalId, MapEntry, ParamMode, + StateId, Statement, StatementKind, VariantId, +}; +use jett_parser::ast::{Item, Module}; +use jett_types::{ReflectionMetadata, ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use std::collections::HashSet; + +pub(super) fn append_property_suite( + hir: &mut jett_hir::Program, + module: &Module, + entry_file: FileId, + cases: &[PropertyCase], + types: &TypeInterner, + reflection: &ReflectionMetadata, + method_value_definitions: &HashSet, +) -> Result, Vec> { + let function_values = function_value_candidates(&hir.functions, method_value_definitions); + let mut trials = Vec::new(); + let mut statements = Vec::new(); + let mut locals = Vec::new(); + let mut first_identity = None; + let mut first_span = None; + for item in &module.items { + let Item::Property(property) = item else { + continue; + }; + if property.span.file != entry_file { + continue; + } + let mut matches = hir.functions.iter().filter(|function| { + function.span == property.span + && function.identity.declaration.kind == DeclarationKind::Property + }); + let Some(function) = matches.next() else { + return Err(error( + property.span, + "checked property has no exact HIR function", + )); + }; + if matches.next().is_some() { + return Err(error( + property.span, + "checked property has multiple HIR functions", + )); + } + if first_identity.is_none() { + first_identity = Some(function.identity.declaration.clone()); + first_span = Some(property.span); + } + let mut count = 0; + for case in cases + .iter() + .filter(|case| case.property_span == property.span) + { + if case.iteration != count || case.arguments.len() != function.params.len() { + return Err(error( + property.span, + "generated property cases disagree with checked parameters or iteration order", + )); + } + let trial = NativePropertyTrial { + name: property.name.name.clone(), + span: property.name.span, + function: function.id, + given_names: property + .givens + .iter() + .map(|given| given.name.name.clone()) + .collect(), + case: case.clone(), + }; + append_trial( + &trial, + function, + types, + reflection, + &function_values, + None, + &mut locals, + &mut statements, + )?; + trials.push(trial); + count += 1; + } + if count != PROPERTY_DEFAULT_ITERATIONS { + return Err(error( + property.span, + format!( + "checked property has {count} generated cases; expected {PROPERTY_DEFAULT_ITERATIONS}" + ), + )); + } + } + let Some(span) = first_span else { + return Ok(None); + }; + let declaration = first_identity.expect("a suite span has a checked declaration"); + let id = FunctionId::new(hir.functions.len() as u32); + hir.functions.push(Function { + id, + debug_kind: jett_hir::FunctionDebugKind::named( + &declaration.namespace, + &format!("__native_property_suite:{}", span.start), + ), + identity: FunctionIdentity { + declaration: DeclarationId { + origin: declaration.origin, + namespace: declaration.namespace, + name: format!("__native_property_suite:{}", span.start), + kind: DeclarationKind::Property, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: Vec::new(), + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals, + body: Block { statements, span }, + span, + }); + Ok(Some(NativePropertyPlan { + entry: id, + trials, + function_values, + checked: None, + })) +} + +/// The opaque sampler and HIR factory own all typed values, locals and calls. +/// The ordinary fallback above keeps its existing public behavior. +pub(super) fn append_checked_property_suite( + hir: &mut jett_hir::Program, + checked: &std::sync::Arc, + entry_file: FileId, + cases: &jett_comptime::verify::CheckedPropertyCases, +) -> Result, Vec> { + let Some(plan) = + jett_hir::append_checked_native_property_suite(hir, checked, entry_file, cases)? + else { + return Ok(None); + }; + let trials = plan + .trials() + .iter() + .map(|trial| NativePropertyTrial { + name: trial.name().to_owned(), + span: trial.span(), + function: trial.function(), + given_names: trial.given_names().to_vec(), + case: trial.case().clone(), + }) + .collect(); + Ok(Some(NativePropertyPlan { + entry: plan.entry(), + trials, + function_values: Vec::new(), + checked: Some(plan), + })) +} + +/// Exact checked inputs and identities retained for native replay and shrinking. +/// Its contents are compiler-owned and cannot be supplied by source programs. +#[derive(Debug)] +pub struct NativePropertyPlan { + pub(crate) entry: FunctionId, + pub(crate) trials: Vec, + function_values: Vec, + checked: Option, +} + +#[derive(Clone, Debug)] +pub(crate) struct NativePropertyTrial { + pub(crate) name: String, + pub(crate) span: Span, + pub(crate) function: FunctionId, + pub(crate) given_names: Vec, + pub(crate) case: PropertyCase, +} + +impl NativePropertyPlan { + pub(crate) fn replay_mir( + &self, + lowered: &crate::BackendLoweringResult, + trials: &[NativePropertyTrial], + validate_inputs: bool, + ) -> Result { + if let Some(plan) = &self.checked { + // Public case values select only original trial identities. HIR + // rebuilds its private Source graph and validates changed inputs. + let mut cases = Vec::with_capacity(trials.len()); + for trial in trials { + let mut selected = plan.trials().iter().filter(|original| { + original.case().property_span == trial.case.property_span + && original.case().iteration == trial.case.iteration + }); + let original = selected.next().ok_or_else(|| { + crate::BackendLoweringError::Hir(error( + trial.span, + "native Property replay lost its original retained trial tuple", + )) + })?; + if selected.next().is_some() + || trial.name != original.name() + || trial.span != original.span() + || trial.function != original.function() + || trial.given_names != original.given_names() + { + return Err(crate::BackendLoweringError::Hir(error( + trial.span, + "native Property replay changed its retained name/span/function/given tuple", + ))); + } + cases.push(trial.case.clone()); + } + let mut hir = jett_hir::rebuild_checked_native_property_replay( + &lowered.hir, + plan, + &cases, + &lowered.interner, + ) + .map_err(crate::BackendLoweringError::Hir)?; + jett_hir::complete_value_conversions(&mut hir, &lowered.interner) + .map_err(crate::BackendLoweringError::Hir)?; + let mir = jett_mir::lower(&hir, &lowered.interner) + .map_err(crate::BackendLoweringError::Mir)?; + jett_mir::validate(&mir).map_err(crate::BackendLoweringError::MirValidation)?; + return Ok(mir); + } + let mut hir = lowered.hir.clone(); + let mut locals = Vec::new(); + let mut statements = Vec::new(); + for trial in trials { + let function = hir + .functions + .iter() + .find(|function| function.id == trial.function) + .ok_or_else(|| { + crate::BackendLoweringError::Hir(error( + trial.span, + "native property replay lost its checked function", + )) + })?; + append_trial( + trial, + function, + &lowered.interner, + &lowered.reflection_metadata, + &self.function_values, + validate_inputs.then_some(hir.functions.as_slice()), + &mut locals, + &mut statements, + ) + .map_err(crate::BackendLoweringError::Hir)?; + } + let suite = hir + .functions + .iter_mut() + .find(|function| function.id == self.entry) + .expect("checked property plan retains its suite entry"); + suite.locals = locals; + suite.body.statements = statements; + jett_hir::complete_value_conversions(&mut hir, &lowered.interner) + .map_err(crate::BackendLoweringError::Hir)?; + let mir = + jett_mir::lower(&hir, &lowered.interner).map_err(crate::BackendLoweringError::Mir)?; + jett_mir::validate(&mir).map_err(crate::BackendLoweringError::MirValidation)?; + Ok(mir) + } +} + +fn append_trial( + trial: &NativePropertyTrial, + function: &Function, + types: &TypeInterner, + reflection: &ReflectionMetadata, + functions: &[FunctionValueCandidate], + refinement_functions: Option<&[Function]>, + locals: &mut Vec, + statements: &mut Vec, +) -> Result<(), Vec> { + let span = trial.case.property_span; + if trial.case.arguments.len() != function.params.len() { + return Err(error( + span, + "native replay arguments disagree with checked parameters", + )); + } + statements.push(property_case_context( + Some(jett_hir::NativePropertyCase { + name: trial.name.clone(), + trial: trial.case.iteration as u64 + 1, + }), + span, + )); + let mut context = ValueContext { + actor_seeds: None, + types, + reflection, + functions, + refinement_functions, + locals, + bindings: statements, + }; + let args = trial + .case + .arguments + .iter() + .zip(&function.params) + .map(|(value, param)| value_expression(value, param.ty, span, &mut context)) + .collect::, _>>() + .map_err(|message| error(span, message))?; + let evaluation_order = (0..args.len()).collect::>(); + let parameter_types = function + .params + .iter() + .map(|parameter| parameter.ty) + .collect::>(); + let access = function + .params + .iter() + .map(|parameter| match parameter.mode { + ParamMode::Owned => jett_typecheck::CheckedCalleeAccess::Owned, + ParamMode::View => jett_typecheck::CheckedCalleeAccess::View, + }) + .collect::>(); + let ownership = jett_hir::CallOwnership::generated( + jett_hir::GeneratedOperation::NativeSuite { + function: function.id, + }, + &args, + ¶meter_types, + &access, + TypeInterner::NOTHING, + &evaluation_order, + types, + ) + .map_err(|message| error(span, message))?; + statements.push(Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::Call { + function: function.id, + evaluation_order, + ownership, + args, + }, + ty: TypeInterner::NOTHING, + span, + }), + span, + }); + statements.push(property_case_context(None, span)); + Ok(()) +} + +fn property_case_context(case: Option, span: Span) -> Statement { + Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::PropertyCaseContext(case), + ty: TypeInterner::NOTHING, + span, + }), + span, + } +} + +fn error(span: Span, message: impl Into) -> Vec { + vec![jett_hir::LowerError { + span, + message: message.into(), + }] +} + +pub(super) use jett_hir::evaluated_values::{ + FunctionValueCandidate, ValueContext, evaluated_intrinsic_expression, + function_value_candidates, value_expression, +}; diff --git a/crates/jett_driver/src/native_property_runner.rs b/crates/jett_driver/src/native_property_runner.rs new file mode 100644 index 00000000..c44769f4 --- /dev/null +++ b/crates/jett_driver/src/native_property_runner.rs @@ -0,0 +1,838 @@ +//! Native replay and shrinking against one already validated compiler session. +use super::*; +use crate::native_property_cases::NativePropertyTrial; + +#[derive(Clone, Copy, Debug)] +pub struct NativePropertyOptions { + pub optimize: bool, + pub attempt_timeout: Duration, +} + +impl Default for NativePropertyOptions { + fn default() -> Self { + Self { + optimize: false, + attempt_timeout: Duration::from_secs(60), + } + } +} + +#[derive(Debug)] +pub struct NativePropertyFailure { + pub name: String, + pub span: jett_common::Span, + /// One-based iteration within this property. + pub trial: usize, + pub counterexample: String, +} + +#[derive(Debug)] +pub struct NativePropertySuiteResult { + /// Trials executed before success or the first failing case. + pub trials: usize, + /// Output of the original suite; replay output is never mixed into it. + pub stdout: Vec, + pub stderr: Vec, + pub failure: Option, + pub frontend_debug_observations: Vec, +} + +#[derive(Debug)] +pub enum NativePropertyRunError { + Captured { + source: Box, + debug_observations: Vec, + }, + Build(NativeBuildError), + Lowering(BackendLoweringError), + TemporaryDirectory(io::Error), + Execution(NativeBuildError), + UnexpectedExit { + code: Option, + stdout: Vec, + stderr: Vec, + }, + ReplayPassed { + stdout: Vec, + stderr: Vec, + }, + /// A private replay failed after the original suite produced its output. + ReplayFailed { + source: Box, + /// Output belongs to the original suite, never the private replay. + stdout: Vec, + stderr: Vec, + }, +} + +impl NativePropertyRunError { + pub fn debug_observations(&self) -> &[crate::DebugObservation] { + match self { + Self::Captured { + debug_observations, .. + } => debug_observations, + Self::Build(error) | Self::Execution(error) => error.debug_observations(), + Self::Lowering(error) => error.debug_observations(), + Self::ReplayFailed { source, .. } => source.debug_observations(), + _ => &[], + } + } + + pub fn build_result(&self) -> Option<&crate::BuildResult> { + match self { + Self::Captured { source, .. } => source.build_result(), + Self::Build(error) | Self::Execution(error) => error.build_result(), + Self::Lowering(error) => error.build_result(), + Self::ReplayFailed { source, .. } => source.build_result(), + _ => None, + } + } + + /// Original suite streams when that process returned captured output. + /// An execution error before `CommandOutput` is returned has no such record. + pub fn original_suite_output(&self) -> Option<(&[u8], &[u8])> { + match self { + Self::Captured { source, .. } => source.original_suite_output(), + Self::UnexpectedExit { stdout, stderr, .. } + | Self::ReplayPassed { stdout, stderr } + | Self::ReplayFailed { stdout, stderr, .. } => Some((stdout, stderr)), + _ => None, + } + } + + fn with_original_suite_output(self, stdout: &[u8], stderr: &[u8]) -> Self { + Self::ReplayFailed { + source: Box::new(self.without_private_process_output()), + stdout: stdout.to_vec(), + stderr: stderr.to_vec(), + } + } + + fn without_private_process_output(self) -> Self { + match self { + Self::Captured { + source, + debug_observations, + } => Self::Captured { + source: Box::new(source.without_private_process_output()), + debug_observations, + }, + Self::Execution(error) => Self::Execution(without_private_execution_output(error)), + Self::UnexpectedExit { code, .. } => Self::UnexpectedExit { + code, + stdout: Vec::new(), + stderr: Vec::new(), + }, + Self::ReplayPassed { .. } => Self::ReplayPassed { + stdout: Vec::new(), + stderr: Vec::new(), + }, + Self::ReplayFailed { source, .. } => Self::ReplayFailed { + source: Box::new(source.without_private_process_output()), + stdout: Vec::new(), + stderr: Vec::new(), + }, + error => error, + } + } + + fn with_debug_observations(self, observations: &[crate::DebugObservation]) -> Self { + if observations.is_empty() || !self.debug_observations().is_empty() { + self + } else { + Self::Captured { + source: Box::new(self), + debug_observations: observations.to_vec(), + } + } + } +} + +fn without_private_execution_output(error: NativeBuildError) -> NativeBuildError { + match error { + NativeBuildError::Captured { + source, + debug_observations, + } => NativeBuildError::Captured { + source: Box::new(without_private_execution_output(*source)), + debug_observations, + }, + NativeBuildError::LinkTimedOut { + linker, timeout, .. + } => NativeBuildError::LinkTimedOut { + linker, + timeout, + stdout: String::new(), + stderr: String::new(), + }, + error => error, + } +} + +impl fmt::Display for NativePropertyRunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Captured { source, .. } => fmt::Display::fmt(source, f), + Self::Build(error) => write!(f, "native property build failed: {error}"), + Self::Lowering(error) => write!(f, "native property lowering failed: {error}"), + Self::TemporaryDirectory(error) => { + write!(f, "cannot create native property workspace: {error}") + } + Self::Execution(NativeBuildError::LinkTimedOut { timeout, .. }) => { + write!(f, "native property attempt timed out after {timeout:?}") + } + Self::Execution(error) => write!(f, "native property process failed: {error}"), + Self::UnexpectedExit { code, stderr, .. } => write!( + f, + "native property process did not complete with successful cleanup (exit {code:?}): {}", + String::from_utf8_lossy(stderr) + ), + Self::ReplayPassed { stderr, .. } => write!( + f, + "native property suite failure could not be reproduced in an isolated trial: {}", + String::from_utf8_lossy(stderr) + ), + Self::ReplayFailed { source, .. } => { + write!(f, "native property replay failed: {source}") + } + } + } +} + +impl std::error::Error for NativePropertyRunError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Build(error) | Self::Execution(error) => Some(error), + Self::Lowering(error) => Some(error), + Self::Captured { source, .. } => Some(source.as_ref()), + Self::ReplayFailed { source, .. } => Some(source.as_ref()), + Self::TemporaryDirectory(error) => Some(error), + _ => None, + } + } +} + +/// Validate, compile, and run the primary file's native property suite. If it +/// fails, replay and shrink its first failing case using only generated native +/// executables. Normal frontend verification is never bypassed. +pub fn run_host_property_suite( + source_path: &Path, + launcher: &NativeLauncherBundle, + options: NativePropertyOptions, +) -> Result { + validate_regular_file(source_path, NativePathRole::Source, Some("jett")) + .map_err(NativePropertyRunError::Build)?; + let lowered = lower_file_for_native_property_suite(source_path) + .map_err(NativePropertyRunError::Lowering)?; + run_lowered(source_path, &lowered, launcher, options) +} + +pub(super) fn run_lowered( + source_path: &Path, + lowered: &BackendLoweringResult, + launcher: &NativeLauncherBundle, + options: NativePropertyOptions, +) -> Result { + #[cfg(test)] + let result = run_lowered_inner(source_path, lowered, launcher, options, None); + #[cfg(not(test))] + let result = run_lowered_inner(source_path, lowered, launcher, options); + result.map_err(|error| error.with_debug_observations(&lowered.debug_observations)) +} + +/// Only unit tests can inject a backend fault after canonical checked lowering. +#[cfg(test)] +pub(super) fn run_lowered_with_test_fault( + source_path: &Path, + lowered: &BackendLoweringResult, + launcher: &NativeLauncherBundle, + fault: TestFault, + options: NativePropertyOptions, +) -> Result { + run_lowered_inner(source_path, lowered, launcher, options, Some(fault)) + .map_err(|error| error.with_debug_observations(&lowered.debug_observations)) +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub(super) enum TestFault { + IntRight { + original: i128, + replacement: i128, + }, + FloatRight { + original: f64, + replacement: f64, + }, + BoolAssertion { + original: bool, + replacement: bool, + }, + Compare { + original: jett_hir::BinaryOp, + replacement: jett_hir::BinaryOp, + }, + Loop, +} +#[cfg(test)] +impl TestFault { + fn apply(self, program: &mut jett_mir::Program) { + let mut changed = 0; + for function in &mut program.functions { + if function.identity.declaration.kind != jett_hir::DeclarationKind::Property + || function + .identity + .declaration + .name + .starts_with("__native_property_suite:") + { + continue; + } + if matches!(self, Self::Loop) { + let entry = function.entry; + let block = function + .blocks + .iter_mut() + .find(|block| block.id != entry) + .expect("timeout fixture retains a canonical internal block"); + block.terminator.kind = jett_mir::TerminatorKind::Goto(block.id); + changed += 1; + continue; + } + for block in &mut function.blocks { + for statement in &mut block.statements { + let jett_mir::StatementKind::Assert { condition, .. } = &mut statement.kind + else { + continue; + }; + match (self, &mut condition.kind) { + ( + Self::IntRight { + original, + replacement, + }, + jett_hir::ExpressionKind::Binary { right, .. }, + ) if right.kind == jett_hir::ExpressionKind::Int(original) => { + right.kind = jett_hir::ExpressionKind::Int(replacement); + changed += 1; + } + ( + Self::FloatRight { + original, + replacement, + }, + jett_hir::ExpressionKind::Binary { right, .. }, + ) if matches!(right.kind,jett_hir::ExpressionKind::Float(value) if value.to_bits()==original.to_bits()) => + { + right.kind = jett_hir::ExpressionKind::Float(replacement); + changed += 1; + } + ( + Self::BoolAssertion { + original, + replacement, + }, + jett_hir::ExpressionKind::Bool(value), + ) if *value == original => { + *value = replacement; + changed += 1; + } + ( + Self::Compare { + original, + replacement, + }, + jett_hir::ExpressionKind::Binary { op, .. }, + ) if *op == original => { + *op = replacement; + changed += 1; + } + _ => {} + } + } + } + } + assert_eq!( + changed, 1, + "the test fault must alter exactly the original scalar Property site" + ); + } +} + +fn run_lowered_inner( + source_path: &Path, + lowered: &BackendLoweringResult, + launcher: &NativeLauncherBundle, + options: NativePropertyOptions, + #[cfg(test)] test_fault: Option, +) -> Result { + let plan = lowered.native_property_plan.as_ref().ok_or_else(|| { + NativePropertyRunError::Build(NativeBuildError::MissingPropertyBodies { + source_path: source_path.to_owned(), + }) + })?; + let directory = tempfile::tempdir().map_err(NativePropertyRunError::TemporaryDirectory)?; + let runner = Runner { + source_path, + lowered, + launcher, + options, + executable: directory.path().join("property-attempt.exe"), + entry: plan.entry, + #[cfg(test)] + test_fault, + }; + let original = runner.execute(&lowered.mir)?; + if !failed(&original)? { + return Ok(NativePropertySuiteResult { + trials: plan.trials.len(), + stdout: original.stdout, + stderr: original.stderr, + failure: None, + frontend_debug_observations: lowered.debug_observations.clone(), + }); + } + + // Locate the first failure without interpreting human-readable output. + // Every prefix executes in a fresh process. The isolated replay below also + // rejects a failure that depends on earlier suite state. + let mut passing = 0; + let mut failing = plan.trials.len(); + while passing + 1 < failing { + let middle = passing + (failing - passing) / 2; + if runner + .replay(&plan.trials[..middle], false) + .map_err(|error| error.with_original_suite_output(&original.stdout, &original.stderr))? + { + failing = middle; + } else { + passing = middle; + } + } + let trial = &plan.trials[failing - 1]; + if !runner + .replay(std::slice::from_ref(trial), false) + .map_err(|error| error.with_original_suite_output(&original.stdout, &original.stderr))? + { + return Err(NativePropertyRunError::ReplayPassed { + stdout: original.stdout, + stderr: original.stderr, + }); + } + let shrunk = + jett_comptime::verify::shrink_property_inputs(trial.case.arguments.clone(), |arguments| { + let mut candidate = trial.clone(); + candidate.case.arguments = arguments.to_vec(); + runner.replay(&[candidate], true) + }) + .map_err(|error| error.with_original_suite_output(&original.stdout, &original.stderr))?; + let counterexample = trial + .given_names + .iter() + .zip(&shrunk) + .map(|(name, value)| format!("{name} = {value}")) + .collect::>() + .join(", "); + Ok(NativePropertySuiteResult { + trials: failing, + stdout: original.stdout, + stderr: original.stderr, + frontend_debug_observations: lowered.debug_observations.clone(), + failure: Some(NativePropertyFailure { + name: trial.name.clone(), + span: trial.span, + trial: trial.case.iteration + 1, + counterexample, + }), + }) +} + +fn failed(output: &CommandOutput) -> Result { + match output.status.code() { + Some(0) => Ok(false), + // The launcher returns 71 only if entry failed and cleanup succeeded. + Some(71) => Ok(true), + code => Err(NativePropertyRunError::UnexpectedExit { + code, + stdout: output.stdout.clone(), + stderr: output.stderr.clone(), + }), + } +} + +struct Runner<'a> { + source_path: &'a Path, + lowered: &'a BackendLoweringResult, + launcher: &'a NativeLauncherBundle, + options: NativePropertyOptions, + executable: PathBuf, + entry: FunctionId, + #[cfg(test)] + test_fault: Option, +} + +impl Runner<'_> { + fn replay( + &self, + trials: &[NativePropertyTrial], + validate_inputs: bool, + ) -> Result { + let mir = self + .lowered + .native_property_plan + .as_ref() + .unwrap() + .replay_mir(self.lowered, trials, validate_inputs) + .map_err(NativePropertyRunError::Lowering)?; + failed(&self.execute(&mir)?) + } + + fn execute(&self, mir: &jett_mir::Program) -> Result { + #[cfg(test)] + let injected = self.test_fault.map(|fault| { + let mut candidate = mir.clone(); + fault.apply(&mut candidate); + candidate + }); + #[cfg(test)] + let mir = injected.as_ref().unwrap_or(mir); + // The test-only fault is still subject to the normal typed MIR and + // Cranelift gates; no original/current proof is refreshed here. + #[cfg(test)] + if self.test_fault.is_some() { + jett_mir::validate(mir).map_err(|errors| { + NativePropertyRunError::Lowering(BackendLoweringError::MirValidation(errors)) + })?; + } + let emitted = jett_codegen_cranelift::emit_host_program_object_with_options( + mir, + &self.lowered.interner, + self.entry, + jett_codegen_cranelift::CodegenOptions { + optimize: self.options.optimize, + }, + ) + .map_err(|source| { + NativePropertyRunError::Build(NativeBuildError::Codegen { + source_path: self.source_path.to_owned(), + source, + }) + })?; + let object = NativeProgramObjectArtifact { + object: native_object(emitted.target, emitted.symbols, emitted.bytes) + .map_err(NativePropertyRunError::Build)?, + program_entry: self.entry, + diagnostics: self.lowered.diagnostics.clone(), + source: self.lowered.source.clone(), + }; + link_host_object(&object, self.launcher, &self.executable) + .map_err(NativePropertyRunError::Build)?; + let mut command = Command::new(&self.executable); + command.stdin(Stdio::null()); + run_command_with_timeout(command, &self.executable, self.options.attempt_timeout) + .map_err(NativePropertyRunError::Execution) + } +} + +#[cfg(test)] +mod debug_capture_tests { + use super::*; + + fn frontend_observation() -> crate::DebugObservation { + crate::DebugObservation { + phase: crate::DebugPhase::Comptime, + event: crate::DebugEvent { + kind: crate::DebugEventKind::Print, + text: "frontend:".into(), + }, + } + } + + fn assert_private_streams_empty(error: &NativePropertyRunError) { + match error { + NativePropertyRunError::Captured { source, .. } + | NativePropertyRunError::ReplayFailed { source, .. } => { + assert_private_streams_empty(source); + } + NativePropertyRunError::UnexpectedExit { + code, + stdout, + stderr, + } => { + assert_eq!(*code, Some(72)); + assert!(stdout.is_empty()); + assert!(stderr.is_empty()); + } + NativePropertyRunError::Execution(error) => { + let mut error = error; + while let NativeBuildError::Captured { source, .. } = error { + error = source; + } + let NativeBuildError::LinkTimedOut { + timeout, + stdout, + stderr, + .. + } = error + else { + panic!("expected private timeout cause: {error}"); + }; + assert_eq!(*timeout, Duration::from_millis(25)); + assert!(stdout.is_empty()); + assert!(stderr.is_empty()); + } + error => panic!("unexpected replay cause: {error}"), + } + } + + #[test] + fn replay_cleanup_and_timeout_errors_keep_original_streams_and_hide_private_streams() { + let errors = [ + NativePropertyRunError::UnexpectedExit { + code: Some(72), + stdout: b"private replay stdout".to_vec(), + stderr: b"private replay stderr".to_vec(), + }, + NativePropertyRunError::Captured { + source: Box::new(NativePropertyRunError::Execution( + NativeBuildError::Captured { + source: Box::new(NativeBuildError::LinkTimedOut { + linker: PathBuf::from("private-trial.exe"), + timeout: Duration::from_millis(25), + stdout: "private replay stdout".into(), + stderr: "private replay stderr".into(), + }), + debug_observations: Vec::new(), + }, + )), + debug_observations: Vec::new(), + }, + ]; + for error in errors { + let failure = error + .with_original_suite_output(b"original application", b"original debug\n") + .with_debug_observations(&[frontend_observation()]); + assert_eq!( + failure.original_suite_output(), + Some((&b"original application"[..], &b"original debug\n"[..])) + ); + assert_eq!(failure.debug_observations(), [frontend_observation()]); + assert_private_streams_empty(&failure); + let display = failure.to_string(); + assert!(display.starts_with("native property replay failed:")); + assert!(!display.contains("private replay"), "{display}"); + assert!(!format!("{failure:?}").contains("private replay")); + let mut cause = std::error::Error::source(&failure); + while let Some(error) = cause { + assert!(!error.to_string().contains("private replay")); + assert!(!format!("{error:?}").contains("private replay")); + cause = error.source(); + } + } + } + + #[test] + fn original_attempt_timeout_keeps_its_existing_typed_partial_capture() { + let failure = NativePropertyRunError::Execution(NativeBuildError::LinkTimedOut { + linker: PathBuf::from("original-suite.exe"), + timeout: Duration::from_millis(25), + stdout: "partial application".into(), + stderr: "partial debug".into(), + }); + assert!(failure.original_suite_output().is_none()); + assert_eq!( + failure.to_string(), + "native property attempt timed out after 25ms" + ); + let NativePropertyRunError::Execution(NativeBuildError::LinkTimedOut { + stdout, + stderr, + .. + }) = failure + else { + panic!("original attempt timeout retains its typed contract"); + }; + assert_eq!(stdout, "partial application"); + assert_eq!(stderr, "partial debug"); + } + + fn replay_test_launcher() -> NativeLauncherBundle { + // Match the existing property test harness so this control also runs + // independently, including a cold launcher archive. + let executable = std::env::current_exe().unwrap(); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + #[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) + ))] + if let Some(bundle) = super::super::property_tests::supplied_launcher(profile != "debug") { + return bundle; + } + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let status = Command::new(env!("CARGO")) + .args([ + "build", + "--locked", + "-q", + "-p", + "jett_native_launcher", + "--target", + &host, + ]) + .args([ + "--profile", + if profile == "debug" { "test" } else { profile }, + ]) + .arg("--target-dir") + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .unwrap(); + assert!(status.success(), "launcher build: {status}"); + let directory = target.join(host).join(profile); + if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.join("jett_native_launcher.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.join("libjett_native_launcher.a")) + } + } + + #[test] + fn post_original_replay_lowering_failure_preserves_original_output_and_frontend_capture() { + if !matches!( + host_target().as_str(), + WINDOWS_MSVC_NATIVE_TARGET | LINUX_GNU_NATIVE_TARGET + ) { + return; + } + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("property.jett"); + fs::write( + &source, + r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("frontend:") +property stable: + given number: int64 + print("original:") + assert number == number +"#, + ) + .unwrap(); + let mut lowered = lower_file_for_native_property_suite(&source).unwrap(); + let frontend = lowered.debug_observations.clone(); + assert_eq!(frontend.len(), 101); + let property_id = lowered.native_property_plan.as_ref().unwrap().trials[0].function; + let property = lowered + .mir + .functions + .iter_mut() + .find(|function| function.id == property_id) + .unwrap(); + let mut changed = 0; + for block in &mut property.blocks { + for statement in &mut block.statements { + if let jett_mir::StatementKind::Assert { condition, .. } = &mut statement.kind { + condition.kind = jett_hir::ExpressionKind::Bool(false); + changed += 1; + } + } + } + assert_eq!(changed, 1); + // The already emitted original MIR stays runnable. Checked replay must + // diagnose a lost source identity instead of discarding original output. + lowered + .hir + .functions + .retain(|function| function.id != property_id); + fs::remove_file(&source).unwrap(); + let failure = run_lowered( + &source, + &lowered, + &replay_test_launcher(), + NativePropertyOptions::default(), + ) + .unwrap_err(); + assert_eq!(failure.debug_observations(), frontend); + assert_eq!( + failure.original_suite_output(), + Some(( + &b""[..], + &b"original:runtime error: property 'stable' trial 1: assertion failed\n"[..], + )) + ); + let NativePropertyRunError::Captured { source, .. } = &failure else { + panic!("original frontend capture survives replay failure: {failure}"); + }; + let NativePropertyRunError::ReplayFailed { source, .. } = source.as_ref() else { + panic!("original runtime streams survive replay failure: {failure}"); + }; + let NativePropertyRunError::Lowering(BackendLoweringError::Hir(errors)) = source.as_ref() + else { + panic!("replay retains its actionable typed lowering cause: {failure}"); + }; + assert_eq!( + errors[0].message, + "native property replay lost its checked function" + ); + assert!( + failure + .to_string() + .contains("native property replay lost its checked function") + ); + } + + #[test] + fn property_link_failure_retains_original_frontend_phase_capture() { + let directory = tempfile::tempdir().unwrap(); + let launcher = match host_target().as_str() { + WINDOWS_MSVC_NATIVE_TARGET => { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("missing.lib")) + } + LINUX_GNU_NATIVE_TARGET => { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("missing.a")) + } + _ => return, + }; + let source = directory.path().join("property.jett"); + fs::write( + &source, + r#"namespace app +function observe(label: string) returns int64: + print(view label) + return 7 +int64 cached = comptime observe("compile:") +property complete: + given number: int64 + print("trial:") + assert number == number +"#, + ) + .unwrap(); + let error = run_host_property_suite(&source, &launcher, NativePropertyOptions::default()) + .unwrap_err(); + let observations = error.debug_observations(); + assert_eq!(observations.len(), 101); + assert_eq!(observations[0].phase, crate::DebugPhase::Comptime); + assert_eq!(observations[0].event.kind, crate::DebugEventKind::Print); + assert_eq!(observations[0].event.text, "compile:"); + assert!(observations[1..].iter().all(|observation| { + observation.phase == crate::DebugPhase::FrontendVerify + && observation.event.kind == crate::DebugEventKind::Print + && observation.event.text == "trial:" + })); + assert!(error.to_string().contains("missing."), "{error}"); + assert!(error.build_result().is_none()); + assert!(std::error::Error::source(&error).is_some()); + } +} diff --git a/crates/jett_driver/src/native_property_tests.rs b/crates/jett_driver/src/native_property_tests.rs new file mode 100644 index 00000000..9e5802d8 --- /dev/null +++ b/crates/jett_driver/src/native_property_tests.rs @@ -0,0 +1,580 @@ +//! Inject backend-only faults in test MIR after canonical frontend/HIR validation. +use super::*; + +#[test] +fn native_property_shrinking_preserves_generated_generic_interface_owners() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("generic-owners.jett"); + let text = include_str!("../../../tests/native/property_generic_owners.jett"); + fs::write(&source, text).unwrap(); + let lowered = lower_file_for_native_property_suite(&source).unwrap(); + let fault = property_runner::TestFault::IntRight { + original: 3, + replacement: 1, + }; + fs::write(&source, text.replace("<= 3", "<= 1")).unwrap(); + let oracle = crate::test_file(&source).unwrap(); + assert_eq!(oracle.failed, 1); + assert_eq!(oracle.blocks[1].iterations, Some(3)); + let expected = oracle.blocks[1].error.as_deref().unwrap(); + assert_eq!( + expected, + "length sentinel (counterexample: records = list(app.Boxed(value: 0), app.Boxed(value: 0)), texts = list())" + ); + fs::remove_file(&source).unwrap(); + let launcher = launcher(); + for optimize in [false, true] { + let result = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + fault, + NativePropertyOptions { + optimize, + ..NativePropertyOptions::default() + }, + ) + .unwrap(); + let failure = result.failure.unwrap(); + assert_eq!(result.trials, 3); + assert_eq!(failure.trial, 3); + assert_eq!( + format!( + "length sentinel (counterexample: {})", + failure.counterexample + ), + expected + ); + assert!(result.stdout.is_empty()); + assert_eq!( + result.stderr, + b"runtime error: property 'owners' trial 3: length sentinel\n" + ); + } +} + +#[test] +fn native_property_shrinking_reports_the_executed_float32_input() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("float32-input.jett"); + let text = "namespace app\nproperty domain:\n given number: float32\n assert number <= 4.0 \"float32 sentinel\"\n"; + fs::write(&source, text).unwrap(); + let lowered = lower_file_for_native_property_suite(&source).unwrap(); + let fault = property_runner::TestFault::FloatRight { + original: 4.0, + replacement: 3.0, + }; + fs::write(&source, text.replace("<= 4.0", "<= 3.0")).unwrap(); + let oracle = crate::test_file(&source).unwrap(); + assert_eq!(oracle.failed, 1); + assert_eq!(oracle.blocks[0].iterations, Some(4)); + let expected = oracle.blocks[0].error.as_deref().unwrap(); + assert_eq!( + expected, + "float32 sentinel (counterexample: number = 3.140000104904175)" + ); + fs::remove_file(&source).unwrap(); + let launcher = launcher(); + for optimize in [false, true] { + let result = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + fault, + NativePropertyOptions { + optimize, + ..NativePropertyOptions::default() + }, + ) + .unwrap(); + let failure = result.failure.unwrap(); + assert_eq!(result.trials, 4); + assert_eq!(failure.trial, 4); + assert_eq!( + format!( + "float32 sentinel (counterexample: {})", + failure.counterexample + ), + expected + ); + assert!(result.stdout.is_empty()); + assert_eq!( + result.stderr, + b"runtime error: property 'domain' trial 4: float32 sentinel\n" + ); + } +} + +#[test] +fn native_property_shrinking_checks_refinement_chains_and_nested_predicates() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("refined-inputs.jett"); + let text = r#"namespace constraints +function positive(value: int64) returns bool: + if value == 0: + string rejected = string.repeat("ab", 9223372036854775807) + return string.char_count(rejected) > 0 + return value > 0 +export type Positive = int64 where positive(value) +export type Large = Positive where value > 1 +namespace app +function positive(value: int64) returns bool: + return false +struct Boxed[T]: + value: T +type Nonempty = list[constraints.Large] where list.length[constraints.Large](value) > 0 +property refined: + given number: constraints.Large + given record: Boxed[constraints.Positive] + given items: Nonempty + assert true "property sentinel" +"#; + fs::write(&source, text).unwrap(); + let lowered = lower_file_for_native_property_suite(&source).unwrap(); + let fault = property_runner::TestFault::BoolAssertion { + original: true, + replacement: false, + }; + fs::write(&source, text.replace("assert true", "assert false")).unwrap(); + let oracle = crate::test_file(&source).unwrap(); + assert_eq!(oracle.failed, 1); + let expected = oracle.blocks[0].error.as_deref().unwrap(); + assert!(expected.contains("number = 2"), "{expected}"); + assert!(expected.contains("value: 1"), "{expected}"); + assert!(expected.contains("items = list(2)"), "{expected}"); + fs::remove_file(&source).unwrap(); + let launcher = launcher(); + for optimize in [false, true] { + let result = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + fault, + NativePropertyOptions { + optimize, + ..NativePropertyOptions::default() + }, + ) + .unwrap(); + let failure = result.failure.unwrap(); + assert_eq!(result.trials, 1); + assert_eq!(failure.trial, 1); + assert_eq!( + format!( + "property sentinel (counterexample: {})", + failure.counterexample + ), + expected + ); + assert!(result.stdout.is_empty()); + assert_eq!( + result.stderr, + b"runtime error: property 'refined' trial 1: property sentinel\n" + ); + } +} + +#[test] +fn native_property_shrinking_preserves_narrow_boundaries_and_nested_inputs() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("integer-boundary.jett"); + let text = r#"namespace app +struct Boxed[T]: + value: T +property boundary: + given number: int8 + given record: Boxed[int8] + given numbers: list[int8] + given lookup: map[int8, Boxed[int8]] + int8 minimum = -128 + assert number >= minimum "minimum sentinel" +"#; + fs::write(&source, text).unwrap(); + let lowered = lower_file_for_native_property_suite(&source).unwrap(); + let fault = property_runner::TestFault::Compare { + original: jett_hir::BinaryOp::GreaterEqual, + replacement: jett_hir::BinaryOp::NotEqual, + }; + fs::write(&source, text.replace(">= minimum", "!= minimum")).unwrap(); + let oracle = crate::test_file(&source).unwrap(); + assert_eq!(oracle.failed, 1); + assert_eq!(oracle.blocks[0].iterations, Some(8)); + let expected = oracle.blocks[0].error.as_deref().unwrap(); + assert!(expected.contains("number = -128"), "{expected}"); + assert!(!expected.contains("number = 128"), "{expected}"); + fs::remove_file(&source).unwrap(); + let launcher = launcher(); + for optimize in [false, true] { + let result = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + fault, + NativePropertyOptions { + optimize, + ..NativePropertyOptions::default() + }, + ) + .unwrap(); + let failure = result.failure.unwrap(); + assert_eq!(result.trials, 8); + assert_eq!(failure.trial, 8); + assert_eq!(failure.name, "boundary"); + assert_eq!( + format!( + "minimum sentinel (counterexample: {})", + failure.counterexample + ), + expected + ); + assert!(result.stdout.is_empty()); + assert_eq!( + result.stderr, + b"runtime error: property 'boundary' trial 8: minimum sentinel\n" + ); + } +} + +#[test] +fn native_property_runner_shrinks_native_failures_from_the_checked_session() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("shrink.jett"); + let text = "namespace app\nproperty first:\n given value: int8\n assert value == value\nproperty target:\n given number: int8\n given items: list[string]\n assert number <= 127 \"backend sentinel\"\n"; + fs::write(&source, text).unwrap(); + let lowered = lower_file_for_native_property_suite(&source).unwrap(); + let launcher = launcher(); + let success = + run_host_property_suite(&source, &launcher, NativePropertyOptions::default()).unwrap(); + assert!(success.failure.is_none()); + assert_eq!(success.trials, 200); + assert!(success.stdout.is_empty() && success.stderr.is_empty()); + + // Frontend/HIR validation stays mandatory. Inject the same typed MIR + // backend fault in the initial attempt and each native replay. + let fault = property_runner::TestFault::IntRight { + original: 127, + replacement: 2, + }; + + // The source oracle must reject the corresponding failing property, while + // a retained native session can replay even after its source is removed. + fs::write(&source, text.replace("<= 127", "<= 2")).unwrap(); + let oracle = crate::test_file(&source).unwrap(); + assert!(matches!( + run_host_property_suite(&source, &launcher, NativePropertyOptions::default()), + Err(NativePropertyRunError::Lowering( + BackendLoweringError::Build(_) + )) + )); + fs::remove_file(&source).unwrap(); + for optimize in [false, true] { + let result = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + fault, + NativePropertyOptions { + optimize, + ..NativePropertyOptions::default() + }, + ) + .unwrap(); + let failure = result.failure.expect("the native assertion must fail"); + assert_eq!(result.trials, 104); + assert_eq!(failure.name, "target"); + assert_eq!(failure.trial, 4); + assert_eq!(failure.counterexample, "number = 3, items = list()"); + assert_eq!(failure.span.start as usize, text.find("target:").unwrap()); + assert!(result.stdout.is_empty()); + assert_eq!( + result.stderr, + b"runtime error: property 'target' trial 4: backend sentinel\n" + ); + let expected = format!( + "backend sentinel (counterexample: {})", + failure.counterexample + ); + assert_eq!(oracle.failed, 1); + assert_eq!(oracle.blocks[1].error.as_deref(), Some(expected.as_str())); + assert_eq!(oracle.blocks[1].iterations, Some(failure.trial)); + } +} + +#[test] +fn native_property_runner_does_not_shrink_timeouts_or_unreproducible_failures() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("replay.jett"); + fs::write( + &source, + "property stable:\n if true:\n assert true\n", + ) + .unwrap(); + let launcher = launcher(); + let mut lowered = lower_file_for_native_property_suite(&source).unwrap(); + // A fault present only in the initial MIR cannot be reproduced by checked + // HIR replay. Do not return a passing result or fabricate a counterexample. + let property = lowered + .mir + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind == jett_hir::DeclarationKind::Property + && !function + .identity + .declaration + .name + .starts_with("__native_property_suite:") + }) + .unwrap(); + let mut changed = 0; + for block in &mut property.blocks { + for statement in &mut block.statements { + if let jett_mir::StatementKind::Assert { condition, .. } = &mut statement.kind { + condition.kind = jett_hir::ExpressionKind::Bool(false); + changed += 1; + } + } + } + assert_eq!(changed, 1); + let error = property_runner::run_lowered( + &source, + &lowered, + &launcher, + NativePropertyOptions::default(), + ) + .unwrap_err(); + assert!(matches!( + &error, + NativePropertyRunError::ReplayPassed { .. } + )); + assert!( + error + .to_string() + .contains("runtime error: property 'stable' trial 1: assertion failed") + ); + + // Restore canonical initial MIR after the unreproducible-only fault. + // The separate test fault loops each fresh MIR attempt without editing HIR. + lowered.mir = jett_mir::lower(&lowered.hir, &lowered.interner).unwrap(); + let error = property_runner::run_lowered_with_test_fault( + &source, + &lowered, + &launcher, + property_runner::TestFault::Loop, + NativePropertyOptions { + attempt_timeout: Duration::from_millis(25), + ..NativePropertyOptions::default() + }, + ) + .unwrap_err(); + assert!( + matches!( + error, + NativePropertyRunError::Execution(NativeBuildError::LinkTimedOut { .. }) + ), + "{error}" + ); +} + +fn launcher() -> NativeLauncherBundle { + let executable = std::env::current_exe().unwrap(); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + if let Some(bundle) = supplied_launcher(profile != "debug") { + return bundle; + } + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let status = Command::new(env!("CARGO")) + .args([ + "build", + "--locked", + "-q", + "-p", + "jett_native_launcher", + "--target", + &host, + ]) + .args([ + "--profile", + if profile == "debug" { "test" } else { profile }, + ]) + .arg("--target-dir") + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .unwrap(); + assert!(status.success(), "launcher build: {status}"); + let directory = target.join(host).join(profile); + if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.join("jett_native_launcher.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.join("libjett_native_launcher.a")) + } +} + +#[test] +fn native_property_failure_identifies_the_trial_and_cleans_owned_values() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("property.jett"); + fs::write( + &source, + r#"namespace app +property first: + given value: int8 + assert value == value +property target: + given number: int8 + given items: list[string] + assert number <= 127 "backend sentinel" +"#, + ) + .unwrap(); + let launcher = launcher(); + for (optimize, fail_in_trial) in [(false, true), (true, true), (false, false)] { + let mut lowered = lower_file_for_native_property_suite(&source) + .expect("the original property must pass frontend validation"); + let entry = lowered.native_property_entry.unwrap(); + let mut changed = 0; + for function in &mut lowered.mir.functions { + if !fail_in_trial + || function.identity.declaration.kind != jett_hir::DeclarationKind::Property + { + continue; + } + for block in &mut function.blocks { + for statement in &mut block.statements { + if let jett_mir::StatementKind::Assert { condition, .. } = &mut statement.kind { + if let jett_hir::ExpressionKind::Binary { right, .. } = &mut condition.kind + { + if right.kind == jett_hir::ExpressionKind::Int(127) { + // A backend divergence, after all source validation, makes + // the fourth deterministic int8 trial (42) fail. + right.kind = jett_hir::ExpressionKind::Int(2); + changed += 1; + } + } + } + } + } + } + if fail_in_trial { + assert_eq!(changed, 1, "alter exactly the checked target assertion"); + } else { + let suite = lowered + .mir + .functions + .iter_mut() + .find(|f| f.id == entry) + .unwrap(); + let exits = suite + .blocks + .iter_mut() + .filter(|block| { + matches!(block.terminator.kind, jett_mir::TerminatorKind::Return(_)) + }) + .collect::>(); + assert_eq!(exits.len(), 1); + for exit in exits { + exit.statements.push(jett_mir::Statement { + kind: jett_mir::StatementKind::Evaluate(jett_hir::Expression { + kind: jett_hir::ExpressionKind::RuntimeFailure("after suite".to_owned()), + ty: jett_types::TypeInterner::NOTHING, + span: suite.span, + }), + span: suite.span, + }); + } + } + let object = emit_program_object_from_lowering(&source, lowered, entry, optimize).unwrap(); + let binary = directory + .path() + .join(format!("property-{optimize}-{fail_in_trial}.exe")); + link_host_object(&object, &launcher, &binary).unwrap(); + let output = + run_command_with_timeout(Command::new(&binary), &binary, Duration::from_secs(60)) + .unwrap(); + assert_eq!(output.status.code(), Some(71), "{output:?}"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + if fail_in_trial { + "runtime error: property 'target' trial 4: backend sentinel\n" + } else { + "runtime error: after suite\n" + } + ); + } +} + +fn selected_launcher_manifest( + debug: Option, + release: Option, + use_release: bool, +) -> Result, String> { + match (debug,release) { + (None,None)=>Ok(None), + (Some(debug),Some(release))=>{ + if debug.is_empty() || release.is_empty() { return Err("supplied native test manifests must be nonempty".into()); } + let debug=PathBuf::from(debug);let release=PathBuf::from(release); + if !debug.is_absolute() || !release.is_absolute() { return Err("supplied native test manifests must be absolute".into()); } + Ok(Some(if use_release { release } else { debug })) + } + _=>Err("JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST and JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST must be supplied together".into()), + } +} +pub(super) fn supplied_launcher(use_release: bool) -> Option { + let path = selected_launcher_manifest( + std::env::var_os("JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST"), + std::env::var_os("JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST"), + use_release, + ) + .expect("explicit native Property test launcher selection")?; + Some( + NativeLauncherBundle::from_manifest(&path, use_release) + .expect("supplied normal native Property test bundle"), + ) +} +#[test] +fn native_property_launcher_requires_a_complete_absolute_manifest_pair() { + assert_eq!(selected_launcher_manifest(None, None, false).unwrap(), None); + assert!(selected_launcher_manifest(Some("debug.json".into()), None, false).is_err()); + assert!(selected_launcher_manifest(None, Some("release.json".into()), true).is_err()); + assert!( + selected_launcher_manifest( + Some("debug.json".into()), + Some("release.json".into()), + false + ) + .is_err() + ); + assert!( + selected_launcher_manifest( + Some(std::ffi::OsString::new()), + Some("release.json".into()), + false + ) + .is_err() + ); + let directory = tempfile::tempdir().unwrap(); + let debug = directory.path().join("debug.json"); + let release = directory.path().join("release.json"); + for use_release in [false, true] { + assert_eq!( + selected_launcher_manifest( + Some(debug.clone().into_os_string()), + Some(release.clone().into_os_string()), + use_release + ) + .unwrap(), + Some(if use_release { + release.clone() + } else { + debug.clone() + }) + ); + } +} diff --git a/crates/jett_driver/src/native_resource_constants.rs b/crates/jett_driver/src/native_resource_constants.rs new file mode 100644 index 00000000..be471a6f --- /dev/null +++ b/crates/jett_driver/src/native_resource_constants.rs @@ -0,0 +1,129 @@ +//! Required evaluation and native materialization share one checked Resource session. +use std::sync::Arc; + +use jett_comptime::{ExplicitComptimeEvaluation, ExplicitComptimeValues}; +use jett_hir::{LowerError, Program}; +use jett_typecheck::CheckedResourceProgram; + +/// Evaluate required values with the exact checked module and no runtime provider. +/// Observations belong to this phase; reading the cache never replays them. +pub fn evaluate_checked_resource_required_values( + checked: &Arc, +) -> ExplicitComptimeEvaluation { + let mut types = crate::expression_type_names(checked.checked(), checked.resolved()); + types.resource_program = Some(checked.clone()); + jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(types), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ) +} + +/// Bake required values without replacing the retained original Source archive. +/// Resource authority comes from the private checked cache, never its public mirror. +pub fn bake_checked_resource_values( + program: &mut Program, + checked: &Arc, + values: &ExplicitComptimeValues, +) -> Result<(), Vec> { + if !program.resource_source.belongs_to(checked) { + return Err(vec![LowerError { + span: checked.module().span, + message: "native required-value materialization received a foreign checked program" + .into(), + }]); + } + crate::native_constants::bake_values( + program, + values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + use jett_parser::{ast::Item, parse}; + use jett_resolve::ResourceKernelSpec; + use jett_typecheck::CheckOptions; + use jett_types::ResourceKernelRecipe; + use std::collections::HashMap; + + fn checked(release: bool) -> Arc { + let support = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = parse( + include_str!("../tests/native_conformance/resource/resource_probe.jett"), + support, + ); + let project = parse( + include_str!("../tests/native_conformance/resource/38_comptime_hook_unused.jett"), + primary, + ); + assert!(parsed.errors.is_empty()); + assert!(project.errors.is_empty()); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one original synthetic Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap(), + ) + } + + #[test] + fn native_required_materialization_rejects_foreign_checked_context() { + for release in [false, true] { + let original = checked(release); + let same_source_foreign = checked(release); + let required = evaluate_checked_resource_required_values(&original); + assert!(required.diagnostics.is_empty()); + let mut program = jett_hir::lower_checked_resource_program(&original).unwrap(); + let before = program.clone(); + let errors = + bake_checked_resource_values(&mut program, &same_source_foreign, &required.values) + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("foreign checked program")) + ); + assert_eq!(program, before); + bake_checked_resource_values(&mut program, &original, &required.values).unwrap(); + } + } +} diff --git a/crates/jett_driver/tests/actor_field_updates_reference.rs b/crates/jett_driver/tests/actor_field_updates_reference.rs new file mode 100644 index 00000000..73ad7ca4 --- /dev/null +++ b/crates/jett_driver/tests/actor_field_updates_reference.rs @@ -0,0 +1,169 @@ +//! Actor persistent field updates from checked Source. +use std::path::{Path, PathBuf}; + +use jett_driver::{build_file, lower_file_for_backend, run_file_capture_outcome}; + +fn fixture(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native") + .join(name) +} + +fn assert_main(name: &str, stdout: &str) { + let path = fixture(name); + let checked = build_file(&path); + assert!(!checked.has_errors, "{name}: {:?}", checked.diagnostics); + let outcome = run_file_capture_outcome(&path) + .unwrap_or_else(|failure| panic!("{name}: {}", failure.message)); + assert_eq!(outcome.stdout, stdout, "{name}"); + assert!(outcome.debug_events.is_empty(), "{name}: {outcome:?}"); + lower_file_for_backend(&path) + .unwrap_or_else(|error| panic!("{name}: checked actor lowering: {error}")); +} + +#[test] +fn original_actor_handler_field_update_executes_from_unchanged_source() { + assert_main( + "field_update_reference/24_actor_handler_future.jett", + "actor:1:9\n", + ); +} + +#[test] +fn actor_state_updates_keep_owning_values_instances_and_message_bindings_independent() { + for (name, stdout) in [ + ( + "01_nested_owners_independent_actors.jett", + "first:4:18:left!!:2|1:9:start:9|sender:2:7:left\nafter:4:18:left!!:2|2:7:right:9\n", + ), + ( + "10_nonshadowing_message_and_helper.jett", + "message:110|1:initial|9:helper|210|9:helper\n", + ), + ( + "03_early_respond_preserves_old_field.jett", + "early:4:4:9:9:9\n", + ), + ( + "16_nonresponding_return_preserves_state.jett", + "void-return:7:changed|7:changed|9:selected|1:initial\n", + ), + ] { + assert_main(&format!("actor_field_updates/{name}"), stdout); + } +} + +#[test] +fn actor_field_updates_execute_in_verify_and_property_bodies() { + let report = jett_driver::test_file(&fixture( + "actor_field_updates/03_early_respond_preserves_old_field.jett", + )) + .expect("actor field update verification bodies"); + assert_eq!(report.total, 2); + assert_eq!(report.passed, 2); + assert_eq!(report.failed, 0); + assert!( + report + .blocks + .iter() + .any(|block| block.is_property && block.iterations.is_some_and(|count| count > 0)) + ); +} + +#[test] +fn actor_field_updates_reject_consumed_readonly_and_borrowed_roots() { + for name in [ + "04_consumed_message_root_rejected.jett", + "05_persistent_state_not_restored_rejected.jett", + "06_view_message_root_rejected.jett", + "07_immutable_state_root_rejected.jett", + "08_live_whole_state_loan_rejected.jett", + "09_live_state_field_loan_rejected.jett", + "12_persistent_alias_owner_replacement_rejected.jett", + ] { + let checked = build_file(&fixture(&format!("actor_field_updates/{name}"))); + assert!( + checked.has_errors, + "{name}: invalid actor ownership admitted" + ); + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!( + errors + .iter() + .all(|diagnostic| (400..500).contains(&diagnostic.code.code())), + "{name}: expected ownership diagnostic, got {:?}", + checked.diagnostics + ); + } +} + +#[test] +fn actor_message_parameters_preserve_the_language_no_shadowing_rule() { + let checked = build_file(&fixture( + "actor_field_updates/02_shadowed_message_and_ordinary_helper.jett", + )); + assert!(checked.has_errors); + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.code.code() == 201), + "{:?}", + checked.diagnostics + ); +} + +#[test] +fn actor_persistent_views_keep_their_exact_backing_state_through_independent_updates() { + assert_main( + "actor_field_updates/11_persistent_aliases_and_independent_field.jett", + "before:2:5:2:5:2|2:21:2:21:10\nafter:2:5:2:5:19|2:21:2:21:32\n", + ); +} + +#[test] +fn actor_respond_is_a_terminal_handle_exit_without_an_unreachable_default_marker() { + assert_main( + "actor_field_updates/13_respond_is_terminal_handle_exit.jett", + "early:4:4:9:9:9\n", + ); +} + +#[test] +fn actor_projected_persistent_views_replay_their_checked_field_path() { + assert_main( + "actor_field_updates/14_projected_persistent_alias.jett", + concat!( + "before-first:envelope-2:102:2:inner-2:outer-2:1002|2:inner-2|2:inner-2|2\n", + "before-second:envelope-7:107:7:inner-7:outer-7:1007|7:inner-7|7:inner-7|7\n", + "after-first:envelope-2:102:2:inner-2:outer-2:1002|2:inner-2|2:inner-2|13\n", + "after-second:envelope-7:107:7:inner-7:outer-7:1007|7:inner-7|7:inner-7|15\n", + ), + ); +} + +#[test] +fn existing_actor_message_receiver_handlers_preserve_checked_evaluation_order() { + let path = fixture("nested_actor_message_handle.jett"); + let checked = build_file(&path); + assert!(!checked.has_errors, "{:?}", checked.diagnostics); + let outcome = run_file_capture_outcome(&path).expect("existing handled actor receiver"); + assert!(outcome.stdout.is_empty()); + lower_file_for_backend(&path).expect("existing handled actor receiver lowers"); +} + +#[test] +fn actor_owning_handle_reply_is_acquired_before_state_publication() { + assert_main( + "actor_field_updates/15_owning_handle_reply.jett", + "reply:2:5|1:9|3\n", + ); +} diff --git a/crates/jett_driver/tests/actor_graph/fixtures/01_original_escape.jett b/crates/jett_driver/tests/actor_graph/fixtures/01_original_escape.jett new file mode 100644 index 00000000..9f9e65ce --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/01_original_escape.jett @@ -0,0 +1,10 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = comptime spawn Counter() + int64 value = ask worker.read + println(value) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/02_shared_fresh.jett b/crates/jett_driver/tests/actor_graph/fixtures/02_shared_fresh.jett new file mode 100644 index 00000000..04546879 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/02_shared_fresh.jett @@ -0,0 +1,31 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +struct Pair: + left: Counter + right: Counter +function create() returns Pair: + Counter worker = spawn Counter() + send worker.add(4) + return Pair(left: clone worker, right: worker) +function baked() returns Pair: + return comptime create() +function repeated() returns nothing: + mutable int64 index = 0 + while index < 2: + Pair current = baked() + println(ask current.left.read) + index = index + 1 + return nothing +function main() returns nothing: + Pair first = baked() + Pair second = baked() + println(first.left == first.right, first.left == second.left) + send first.left.add(3) + println(ask first.right.read, ask second.left.read) + repeated() + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/03_nested_order.jett b/crates/jett_driver/tests/actor_graph/fixtures/03_nested_order.jett new file mode 100644 index 00000000..ff6b25eb --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/03_nested_order.jett @@ -0,0 +1,15 @@ +namespace app +actor Counter: + mutable int64 count = 1 +actor Holder: + app.Counter child = spawn app.Counter() +function baked() returns list[Holder]: + Holder first = spawn Holder() + Holder second = spawn Holder() + return list(clone first, first, second) +function roots() returns list[Holder]: + return comptime baked() +function main() returns nothing: + println(roots()) + println(roots()) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/04_primitive_callback.jett b/crates/jett_driver/tests/actor_graph/fixtures/04_primitive_callback.jett new file mode 100644 index 00000000..9fe3e1e8 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/04_primitive_callback.jett @@ -0,0 +1,13 @@ +namespace app +function create_callback() returns function() returns string: + string text = "baked" + return function() returns string: return text +actor Keeper: + mutable function() returns string callback = create_callback() + receive read responds string: + respond callback() +function main() returns nothing: + Keeper worker = comptime spawn Keeper() + string text = ask worker.read + println(text) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/05_pending_graph.jett b/crates/jett_driver/tests/actor_graph/fixtures/05_pending_graph.jett new file mode 100644 index 00000000..5b98a51a --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/05_pending_graph.jett @@ -0,0 +1,21 @@ +namespace app +actor Counter: + mutable int64 count = 1 +struct Pair: + ready: Counter + pending: Counter + twice: Counter +function create() returns Pair: + Counter ready = spawn Counter() + Counter pending = run clone ready + Counter twice = run clone pending + return Pair(ready: ready, pending: pending, twice: twice) +function main() returns nothing: + Pair pair = comptime create() + println(pair.ready, pair.pending, pair.twice) + Counter once = join clone pair.twice handle error: + return println(error) + Counter ready = join once handle error: + return println(error) + println(pair.ready, ready) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/06_callback_list.jett b/crates/jett_driver/tests/actor_graph/fixtures/06_callback_list.jett new file mode 100644 index 00000000..bcdd031d --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/06_callback_list.jett @@ -0,0 +1,23 @@ +namespace app +function left() returns function() returns string: + string text = "left" + return function() returns string: return text +function right() returns function() returns string: + string text = "right" + return function() returns string: return text +function callbacks() returns list[function() returns string]: + return list(left(), right()) +function read_callbacks(view items: list[function() returns string]) returns string: + function() returns string first = list.get[function() returns string](view items, 0) handle: + return "missing first" + function() returns string second = list.get[function() returns string](view items, 1) handle: + return "missing second" + return "{first()} {second()}" +actor Keeper: + mutable list[function() returns string] callbacks = callbacks() + receive read responds string: + respond read_callbacks(view callbacks) +function main() returns nothing: + Keeper worker = comptime spawn Keeper() + println(ask worker.read) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/07_nested_hook_actor.jett b/crates/jett_driver/tests/actor_graph/fixtures/07_nested_hook_actor.jett new file mode 100644 index 00000000..c6272c93 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/07_nested_hook_actor.jett @@ -0,0 +1,9 @@ +namespace app +actor Keeper: + mutable function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + receive alive responds bool: + respond true +function main() returns nothing: + Keeper keeper = comptime spawn Keeper() + bool okay = ask keeper.alive + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/08_nested_hook_capture_actor.jett b/crates/jett_driver/tests/actor_graph/fixtures/08_nested_hook_capture_actor.jett new file mode 100644 index 00000000..0482df75 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/08_nested_hook_capture_actor.jett @@ -0,0 +1,11 @@ +namespace app +actor Keeper(dispose: function(resource_probe.TestHandle) returns nothing): + receive alive responds bool: + respond true +function create() returns Keeper: + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close_descriptor() + return spawn Keeper(dispose: dispose) +function main() returns nothing: + Keeper keeper = comptime create() + bool okay = ask keeper.alive + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/09_enum_carrier.jett b/crates/jett_driver/tests/actor_graph/fixtures/09_enum_carrier.jett new file mode 100644 index 00000000..c3344a10 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/09_enum_carrier.jett @@ -0,0 +1,18 @@ +namespace app +actor Counter: + mutable int64 count = 7 + receive read responds int64: + respond count +enum Carrier: + held(worker: Counter) + empty +function create() returns Carrier: + return Carrier.held(spawn Counter()) +function main() returns nothing: + Carrier root = comptime create() + match root: + held(worker): + println(ask worker.read) + empty: + println(0) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/10_map_shared.jett b/crates/jett_driver/tests/actor_graph/fixtures/10_map_shared.jett new file mode 100644 index 00000000..3b01afa5 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/10_map_shared.jett @@ -0,0 +1,17 @@ +namespace app +actor Counter: + mutable int64 count = 3 + receive read responds int64: + respond count +function create() returns map[string, Counter]: + Counter worker = spawn Counter() + mutable map[string, Counter] entries = map.new[string, Counter]() + entries = map.set[string, Counter](entries, "first", clone worker) + entries = map.set[string, Counter](entries, "second", worker) + return entries +function main() returns nothing: + map[string, Counter] entries = comptime create() + list[Counter] workers = map.values[string, Counter](entries) + for worker in workers: + println(ask worker.read) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/11_optional_result.jett b/crates/jett_driver/tests/actor_graph/fixtures/11_optional_result.jett new file mode 100644 index 00000000..9fabcd47 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/11_optional_result.jett @@ -0,0 +1,20 @@ +namespace app +actor Counter: + mutable int64 count = 4 + receive read responds int64: + respond count +struct Envelope: + ready: optional[Counter] + completed: result[Counter, string] +function create() returns Envelope: + Counter worker = spawn Counter() + return Envelope(ready: some(clone worker), completed: ok(worker)) +function main() returns nothing: + Envelope root = comptime create() + Envelope copy = clone root + Counter first = copy.ready handle: + return nothing + Counter second = root.completed handle error: + return nothing + println(first == second, ask first.read) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/12_adapted_callback.jett b/crates/jett_driver/tests/actor_graph/fixtures/12_adapted_callback.jett new file mode 100644 index 00000000..f9d0e81d --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/12_adapted_callback.jett @@ -0,0 +1,26 @@ +namespace app +type Positive = int64 where value > 0 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +struct Holder: + callback: function() returns int64 +function adapted(view holder: Holder) returns function() returns Positive: + for field in type.fields[Holder](): + return type.field_value[Holder, function() returns Positive](view holder, view field) + return positive +actor Keeper(callback: function() returns Positive): + receive read responds Positive: + respond callback() +function create() returns Keeper: + Holder holder = Holder(callback: five) + return spawn Keeper(callback: adapted(view holder)) +function observe() returns int64: + Keeper worker = comptime create() + Positive observed = ask worker.read + return coarsen observed +function main() returns nothing: + println(observe()) + println(observe()) + return nothing diff --git a/crates/jett_driver/tests/actor_graph/fixtures/13_adapted_callback_production.jett b/crates/jett_driver/tests/actor_graph/fixtures/13_adapted_callback_production.jett new file mode 100644 index 00000000..50134975 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph/fixtures/13_adapted_callback_production.jett @@ -0,0 +1,25 @@ +namespace app +type Positive = int64 where value > 0 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +struct Holder: + callback: function() returns int64 +function adapted(view holder: Holder) returns function() returns Positive: + for field in type.fields[Holder](): + return type.field_value[Holder, function() returns Positive](view holder, view field) + return positive +actor Keeper(callback: function() returns Positive): + receive read responds Positive: + respond callback() +function create() returns Keeper: + Holder holder = Holder(callback: five) + return spawn Keeper(callback: adapted(view holder)) +function observe() returns int64: + Keeper worker = comptime create() + Positive observed = ask worker.read + return coarsen observed +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{observe()}\n{observe()}\n") + return nothing diff --git a/crates/jett_driver/tests/actor_graph_hook_observation.rs b/crates/jett_driver/tests/actor_graph_hook_observation.rs new file mode 100644 index 00000000..2a3fd1d5 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph_hook_observation.rs @@ -0,0 +1,180 @@ +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; +const SUPPORT: &str = + include_str!("../../jett_comptime/src/resource_execution/fixtures/resource_probe.jett"); +const SOURCE: &str = include_str!("actor_graph/fixtures/07_nested_hook_actor.jett"); +fn observed(source: &str, release: bool) -> serde_json::Value { + let project = FileId::new(0); + let support = FileId::new(10_000); + let mut parsed = parse(SUPPORT, support); + let primary = parse(source, project); + let entry = primary + .module + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "main" => Some(function.span), + _ => None, + }) + .expect("actual Source main"); + let resource = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .expect("unchanged actual Support resource"); + parsed.errors.extend(primary.errors); + parsed.module.items.extend(primary.module.items); + let mut out = serde_json::json!({"release_source":release,"desired_oracle":false,"parse_diagnostics":format!("{:?}",parsed.errors)}); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: resource, + member: member.into(), + recipe, + }) + .collect::>(); + let checked = match CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (project, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) { + Ok(value) => Arc::new(value), + Err(error) => { + out["checked_error"] = + serde_json::json!(format!("{error:?}; {:?}", error.diagnostics())); + return out; + } + }; + out["checked"] = serde_json::json!(true); + let fields=checked.module().items.iter().filter_map(|item|if let Item::Actor(actor)=item {Some(actor)} else {None}).flat_map(|actor|actor.state_fields.iter()).map(|field|{let callee=if let jett_parser::ast::Expr::Call(callee,_,_)=&field.value {Some(callee.as_ref())} else {None};serde_json::json!({"field":field.name.name,"span":format!("{:?}",field.value.span()),"expression_type":format!("{:?}",checked.checked().type_map.get(&field.value.span())),"ownership":format!("{:?}",checked.checked().call_ownership.get(&field.value.span())),"callee_type":format!("{:?}",callee.and_then(|callee|checked.checked().type_map.get(&callee.span()))),"callee_resolution":format!("{:?}",callee.and_then(|callee|checked.resolved().resolutions.get(&callee.span())))})}).collect::>(); + out["actor_initializer_facts"] = serde_json::json!(fields); + let evaluation = jett_driver::evaluate_checked_resource_required_values(&checked); + out["required_diagnostics"] = serde_json::json!(format!("{:?}", evaluation.diagnostics)); + out["required_event_count"] = serde_json::json!(evaluation.debug_events.len()); + if !evaluation.diagnostics.is_empty() { + return out; + } + out["required"] = serde_json::json!(true); + let mut interpreter = jett_comptime::Interpreter::new(); + match interpreter.install_checked_resource_program( + checked.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) { + Ok(()) => {} + Err(error) => { + out["reference_install_error"] = serde_json::json!(error); + return out; + } + } + interpreter.register_module(checked.module()); + interpreter.set_explicit_comptime_values(Arc::new(evaluation.values.clone())); + out["reference"] = serde_json::json!(format!( + "{:?}", + interpreter.call_function("app.main", vec![]) + )); + out["reference_events"] = serde_json::json!(format!("{:?}", interpreter.take_debug_events())); + let mut hir = match jett_hir::lower_checked_resource_program(&checked) { + Ok(value) => value, + Err(error) => { + out["hir_error"] = serde_json::json!(format!("{error:?}")); + return out; + } + }; + out["hir"] = serde_json::json!(true); + if let Err(error) = + jett_driver::bake_checked_resource_values(&mut hir, &checked, &evaluation.values) + { + out["bake_error"] = serde_json::json!(format!("{error:?}")); + return out; + } + out["bake"] = serde_json::json!(true); + if let Err(error) = jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner) + { + out["conversion_error"] = serde_json::json!(format!("{error:?}")); + return out; + } + out["conversion"] = serde_json::json!(true); + let entry = hir + .functions + .iter() + .filter(|function| { + function.span == entry + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entry.as_slice() else { + out["entry_error"] = serde_json::json!("not unique"); + return out; + }; + let mir = match jett_mir::lower(&hir, &checked.checked().interner) { + Ok(value) => value, + Err(error) => { + out["mir_error"] = serde_json::json!(format!("{error:?}")); + return out; + } + }; + out["mir"] = serde_json::json!(true); + if let Err(error) = jett_mir::validate(&mir) { + out["mir_validation_error"] = serde_json::json!(format!("{error:?}")); + return out; + } + match jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) { + Ok(object) => { + out["object_bytes"] = serde_json::json!(object.bytes.len()); + } + Err(error) => { + out["object_error"] = serde_json::json!(format!("{error:?}")); + } + } + out +} +#[test] +fn actor_graph_nested_hook_admission_observation() { + let rows = vec![observed(SOURCE, false), observed(SOURCE, true)]; + let text = serde_json::to_string_pretty(&rows).unwrap(); + println!("{text}"); + write_observation("nested-hook-admission.json", &text); + // This captures observations, including failures. Passing this collector + // does not assert source, reference, object or native acceptance. +} +#[test] +fn actor_graph_nested_hook_capture_admission_observation() { + let source = include_str!("actor_graph/fixtures/08_nested_hook_capture_actor.jett"); + let rows = vec![observed(source, false), observed(source, true)]; + let text = serde_json::to_string_pretty(&rows).unwrap(); + println!("{text}"); + write_observation("nested-hook-capture-admission.json", &text); +} + +fn write_observation(name: &str, text: &str) { + let directory = std::env::var_os("JETT_ACTOR_GRAPH_EVIDENCE_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/actor-graph-tests") + }); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join(name), text).unwrap(); +} diff --git a/crates/jett_driver/tests/actor_graph_native.rs b/crates/jett_driver/tests/actor_graph_native.rs new file mode 100644 index 00000000..52ee40c6 --- /dev/null +++ b/crates/jett_driver/tests/actor_graph_native.rs @@ -0,0 +1,327 @@ +//! Linked graph controls use ordinary runtime bundles and remove Source before execution. +#![cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +use jett_driver::native::{NativeLauncherBundle, host_target}; +use std::{ + fs, + io::Read, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::OnceLock, + time::{Duration, Instant}, +}; + +fn evidence() -> PathBuf { + std::env::var_os("JETT_ACTOR_GRAPH_EVIDENCE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/actor-graph-tests") + }) +} +fn profile() -> &'static str { + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } +} +struct Launcher { + bundle: NativeLauncherBundle, + _directory: tempfile::TempDir, +} + +fn launcher() -> &'static NativeLauncherBundle { + if let Some(manifest) = std::env::var_os("JETT_ACTOR_GRAPH_BUNDLE_MANIFEST") { + static BUNDLE: OnceLock = OnceLock::new(); + return BUNDLE.get_or_init(|| { + NativeLauncherBundle::from_manifest(Path::new(&manifest), !cfg!(debug_assertions)) + .expect("fresh ordinary runtime bundle matching this test profile") + }); + } + static LAUNCHER: OnceLock = OnceLock::new(); + &LAUNCHER + .get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + let cargo_profile = if profile == "debug" { "test" } else { profile }; + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let status = Command::new(env!("CARGO")) + .args(["build", "-q", "-p", "jett_native_launcher"]) + .args([ + "--target", + &host, + "--profile", + cargo_profile, + "--target-dir", + ]) + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build host- and profile-matched launcher"); + assert!(status.success(), "launcher build failed: {status}"); + let archive_name = if cfg!(windows) { + "jett_native_launcher.lib" + } else { + "libjett_native_launcher.a" + }; + let archive = target.join(&host).join(profile).join(archive_name); + let directory = tempfile::tempdir().expect("launcher bundle directory"); + let copied = directory.path().join(archive_name); + std::fs::copy(&archive, &copied).unwrap_or_else(|error| { + panic!( + "cannot copy launcher archive {}: {error}", + archive.display() + ) + }); + Launcher { + bundle: if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(copied) + } else { + NativeLauncherBundle::linux_gnu_v1(copied) + }, + _directory: directory, + } + }) + .bundle +} +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn run_bounded(binary: &Path, cwd: &Path) -> (std::process::Output, bool) { + let mut out = tempfile::NamedTempFile::new().unwrap(); + let mut err = tempfile::NamedTempFile::new().unwrap(); + let mut child = Command::new(binary) + .current_dir(cwd) + .env_clear() + .stdin(Stdio::null()) + .stdout(out.reopen().unwrap()) + .stderr(err.reopen().unwrap()) + .spawn() + .unwrap(); + let started = Instant::now(); + let mut timed_out = false; + let status = loop { + if let Some(status) = child.try_wait().unwrap() { + break status; + } + if started.elapsed() > Duration::from_secs(20) { + timed_out = true; + let _ = child.kill(); + break child.wait().unwrap(); + } + std::thread::sleep(Duration::from_millis(10)); + }; + let mut stdout = Vec::new(); + out.read_to_end(&mut stdout).unwrap(); + let mut stderr = Vec::new(); + err.read_to_end(&mut stderr).unwrap(); + ( + std::process::Output { + status, + stdout, + stderr, + }, + timed_out, + ) +} +#[test] +fn actor_graph_public_driver_reference_and_native_without_source() { + let mut rows = Vec::new(); + let selected = std::env::var("JETT_ACTOR_GRAPH_CASE").ok(); + for (name, text) in [ + ("01_original_escape", "1\n"), + ("02_shared_fresh", "true false\n8 5\n5\n5\n"), + ( + "03_nested_order", + "list(actor#1, actor#1, actor#3)\nlist(actor#5, actor#5, actor#7)\n", + ), + ("04_primitive_callback", "baked\n"), + ( + "05_pending_graph", + "actor#0 pending(actor#0) pending(pending(actor#0))\nactor#0 actor#0\n", + ), + ("06_callback_list", "left right\n"), + ("09_enum_carrier", "7\n"), + ("10_map_shared", "3\n3\n"), + ("11_optional_result", "true 4\n"), + ("12_adapted_callback", "5\n5\n"), + ("13_adapted_callback_production", "5\n5\n"), + ] { + if selected.as_deref().is_some_and(|selected| selected != name) { + continue; + } + let production_output = name == "13_adapted_callback_production"; + let source_policies: &[bool] = if production_output { + &[false, true] + } else { + &[false] + }; + for &release in source_policies { + let mut directory = evidence().join("native").join(profile()).join(name); + if production_output { + directory = directory.join(if release { + "release-source" + } else { + "debug-source" + }); + } + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("main.jett"); + fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/actor_graph/fixtures") + .join(format!("{name}.jett")), + &source, + ) + .unwrap(); + let reference = jett_driver::run_file_capture_output(&source) + .unwrap_or_else(|error| panic!("{name} reference: {error:?}")); + let printed: String = reference + .debug_events + .iter() + .filter(|e| { + matches!( + e.kind, + jett_driver::DebugEventKind::Print | jett_driver::DebugEventKind::Println + ) + }) + .map(|e| e.text.as_str()) + .collect(); + if production_output { + assert_eq!(reference.stdout, text, "{name} selected semantics"); + assert!(reference.debug_events.is_empty()); + } else { + assert_eq!(printed, text, "{name} selected semantics"); + assert!(reference.stdout.is_empty()); + } + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name} object: {error}")); + fs::write(directory.join("final.obj"), object.bytes()).unwrap(); + let binary = directory.join(if cfg!(windows) { "graph.exe" } else { "graph" }); + jett_driver::native::link_host_object(&object, launcher(), &binary) + .unwrap_or_else(|error| panic!("{name} link: {error}")); + fs::remove_file(&source).unwrap(); + let executable_bytes = fs::read(&binary).unwrap(); + let executable_sha256 = digest(&executable_bytes); + let executable_length = executable_bytes.len(); + drop(executable_bytes); + let (native, timed_out) = run_bounded(&binary, &directory); + fs::write(directory.join("native.stdout"), &native.stdout).unwrap(); + fs::write(directory.join("native.stderr"), &native.stderr).unwrap(); + let row = serde_json::json!({"name":name,"runtime_profile":profile(),"source_release_policy":release,"source_removed":!source.exists(),"reference_stdout":reference.stdout,"reference_prints":printed,"reference_events":format!("{:?}",reference.debug_events),"object_bytes":object.bytes().len(),"object_sha256":digest(object.bytes()),"executable_bytes":executable_length,"executable_sha256":executable_sha256,"native_exit":native.status.code(),"native_timed_out":timed_out,"native_terminal_reaped":true,"native_stdout":String::from_utf8_lossy(&native.stdout),"native_stderr":String::from_utf8_lossy(&native.stderr)}); + rows.push(row); + fs::write( + evidence().join(format!("native-{}.json", profile())), + serde_json::to_string_pretty(&rows).unwrap(), + ) + .unwrap(); + fs::remove_file(&binary).unwrap(); + assert!( + !binary.exists(), + "{name} case executable removed after reap" + ); + assert!(!timed_out, "{name} graph native timeout"); + assert!(native.status.success(), "{name} native: {native:?}"); + assert_eq!(native.stdout, reference.stdout.as_bytes(), "{name}"); + assert_eq!( + native.stderr, + jett_driver::render_debug_events(&reference.debug_events).as_bytes(), + "{name}" + ); + } + } + assert!( + !rows.is_empty(), + "selected graph control must exist in the unchanged source table" + ); +} + +#[test] +fn actor_graph_map_lowering_observation() { + let source = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/actor_graph/fixtures/10_map_shared.jett"); + let lowered = jett_driver::lower_file_for_backend(&source) + .expect("actual checked map control reaches backend lowering"); + let selected = |identity: &jett_hir::FunctionIdentity| { + identity.declaration.namespace == "app" + || (identity.declaration.namespace == "map" && identity.declaration.name == "values") + }; + let rows = lowered.hir.functions.iter().filter(|function| selected(&function.identity)).map(|function| { + let current = lowered.mir.functions.iter().find(|current| current.id == function.id).unwrap(); + let loops=current.blocks.iter().filter_map(|block|match &block.terminator.kind { + jett_mir::TerminatorKind::ForEach {key,value,by_view,iterable,body,exit}=>Some(serde_json::json!({"header":block.id.index(),"key":key.index(),"value":value.map(|value|value.index()),"by_view":by_view,"iterable":expression_fact(iterable),"body":body.index(),"exit":exit.index()})), + _=>None, + }).collect::>(); + serde_json::json!({"id":function.id.index(),"identity":format!("{:?}",function.identity),"hir_params":format!("{:#?}",function.params),"hir_locals":format!("{:#?}",function.locals),"hir_body":block_fact(&function.body),"mir_params":format!("{:#?}",current.params),"mir_locals":format!("{:#?}",current.locals),"mir_unprepared_loops":loops}) + }).collect::>(); + fs::create_dir_all(evidence()).unwrap(); + fs::write( + evidence().join("map-lowering-observation.json"), + serde_json::to_string_pretty(&rows).unwrap(), + ) + .unwrap(); + // This collector asserts lowering only; its retained ForEach is not an object/native acceptance. +} + +fn expression_fact(value: &jett_hir::Expression) -> serde_json::Value { + use jett_hir::ExpressionKind as E; + let detail = match &value.kind { + E::Local(local) => serde_json::json!({"local":local.index()}), + E::Call { + function, + args, + evaluation_order, + ownership, + } => { + let source = if let jett_hir::CallOwnership::Source(source) = ownership { + Some( + serde_json::json!({"bridge":format!("{:?}",source.bridge),"target":format!("{:?}",source.target),"arguments":source.arguments.iter().map(|argument|serde_json::json!({"parameter":argument.parameter_index,"actual_type":argument.actual_type.index(),"parameter_type":argument.parameter_type.index(),"staging":format!("{:?}",argument.staging)})).collect::>() }), + ) + } else { + None + }; + serde_json::json!({"function":function.index(),"args":args.iter().map(expression_fact).collect::>(),"evaluation_order":evaluation_order,"source":source}) + } + E::Intrinsic { + intrinsic, + args, + type_arguments, + evaluation_order, + .. + } => { + serde_json::json!({"intrinsic":format!("{intrinsic:?}"),"args":args.iter().map(expression_fact).collect::>(),"type_arguments":type_arguments.iter().map(|ty|ty.index()).collect::>(),"evaluation_order":evaluation_order}) + } + E::View(inner) | E::Clone(inner) => serde_json::json!({"inner":expression_fact(inner)}), + // Never recursively format the private graph origin or Source witnesses. + // The collector records bounded operand/occurrence facts, not new proof. + _ => serde_json::Value::Null, + }; + serde_json::json!({"kind":format!("{:?}",std::mem::discriminant(&value.kind)),"type":value.ty.index(),"span":format!("{:?}",value.span),"detail":detail}) +} +fn block_fact(block: &jett_hir::Block) -> serde_json::Value { + use jett_hir::StatementKind as H; + serde_json::json!(block.statements.iter().map(|statement|match &statement.kind { + H::Let {local,value}=>serde_json::json!({"kind":"Let","local":local.index(),"value":expression_fact(value)}), + H::Assign {target,value}=>serde_json::json!({"kind":"Assign","target":expression_fact(target),"value":expression_fact(value)}), + H::Return(value)=>serde_json::json!({"kind":"Return","value":value.as_ref().map(expression_fact)}), + H::For {key,value,by_view,iterable,body}=>serde_json::json!({"kind":"For","key":key.index(),"value":value.map(|value|value.index()),"by_view":by_view,"iterable":expression_fact(iterable),"body":block_fact(body)}), + H::Scope(body)=>serde_json::json!({"kind":"Scope","body":block_fact(body)}), + _=>serde_json::json!({"kind":format!("{:?}",std::mem::discriminant(&statement.kind))}), + }).collect::>()) +} diff --git a/crates/jett_driver/tests/backend_lowering.rs b/crates/jett_driver/tests/backend_lowering.rs new file mode 100644 index 00000000..4bb8e3db --- /dev/null +++ b/crates/jett_driver/tests/backend_lowering.rs @@ -0,0 +1,290 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use jett_common::SourceOrigin; +use jett_driver::{ + lower_file_for_backend, lower_file_for_native_property_suite, + lower_file_for_native_verify_suite, +}; +use jett_hir::{DeclarationKind, ExpressionKind, StatementKind}; + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass") +} + +fn run_pass_fixtures() -> Vec { + let mut fixtures = fs::read_dir(fixture_dir()) + .expect("run-pass fixture directory should be readable") + .map(|entry| entry.expect("fixture entry should be readable").path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "jett") + }) + .collect::>(); + fixtures.sort(); + fixtures +} + +fn fixture_name(path: &Path) -> String { + path.file_name() + .expect("fixture should have a file name") + .to_string_lossy() + .into_owned() +} + +#[test] +fn backend_lowering_retains_explicit_project_and_stdlib_origins() { + let fixture = fixture_dir().join("simple.jett"); + let lowered = lower_file_for_backend(&fixture).expect("simple fixture should lower"); + + assert_eq!( + lowered.source_origins.get(&jett_common::FileId::new(0)), + Some(&SourceOrigin::Project) + ); + assert!( + lowered + .source_origins + .values() + .any(|origin| *origin == SourceOrigin::Stdlib), + "lowering should retain an explicit stdlib source origin" + ); + assert!(!lowered.hir.functions.is_empty()); + assert_eq!(lowered.hir.functions.len(), lowered.mir.functions.len()); + assert_eq!( + lowered.program_entry, None, + "a verification-only source must not invent a native program entry" + ); +} + +#[test] +fn backend_lowering_publishes_the_exact_primary_program_entry() { + let fixture = fixture_dir().join("breakpoint_basic.jett"); + let lowered = lower_file_for_backend(&fixture).expect("main fixture should lower"); + let entry = lowered + .program_entry + .expect("the primary source main must have a checked function identity"); + + let hir_entry = lowered + .hir + .functions + .iter() + .find(|function| function.id == entry) + .expect("program entry must name one HIR function"); + let mir_entry = lowered + .mir + .functions + .iter() + .find(|function| function.id == entry) + .expect("program entry must name the corresponding MIR function"); + + assert_eq!(hir_entry.identity.declaration.name, "main"); + assert_eq!(hir_entry.identity.declaration.origin, SourceOrigin::Project); + assert_eq!(hir_entry.identity, mir_entry.identity); + assert_eq!(hir_entry.span, mir_entry.span); +} + +#[test] +fn native_verify_suite_calls_primary_file_bodies_in_declaration_order() { + let fixture = fixture_dir().join("verify_test.jett"); + let lowered = + lower_file_for_native_verify_suite(&fixture).expect("verify fixture should lower"); + assert_eq!(lowered.program_entry, None); + let entry = lowered + .native_verify_entry + .expect("verify fixture must have a suite entry"); + let suite = &lowered.hir.functions[entry.index() as usize]; + assert_eq!(suite.identity.declaration.namespace, "app"); + assert!( + suite + .identity + .declaration + .name + .starts_with("__native_verify_suite:") + ); + let called = suite + .body + .statements + .iter() + .map(|statement| match &statement.kind { + StatementKind::Expression(expression) => match &expression.kind { + ExpressionKind::Call { function, args, .. } if args.is_empty() => *function, + other => panic!("suite statement does not call a verify body: {other:?}"), + }, + other => panic!("suite has an unexpected statement: {other:?}"), + }) + .collect::>(); + assert_eq!(called.len(), 2); + let bodies = called + .iter() + .map(|id| &lowered.hir.functions[id.index() as usize]) + .collect::>(); + assert!(bodies.iter().all(|body| { + body.identity.declaration.kind == DeclarationKind::Verify + && body.span.file == suite.span.file + })); + assert!(bodies[0].identity.declaration.name.starts_with("add:")); + assert!(bodies[1].identity.declaration.name.starts_with("multiply:")); +} + +#[test] +fn native_property_suite_embeds_checked_generated_trials() { + let fixture = fixture_dir().join("namespace_runtime_verify_context.jett"); + let lowered = lower_file_for_native_property_suite(&fixture) + .expect("checked scalar property trials should lower"); + let entry = lowered + .native_property_entry + .expect("property fixture must have a native suite entry"); + let suite = &lowered.hir.functions[entry.index() as usize]; + let calls = suite + .body + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::Expression(expression) + if matches!(expression.kind, ExpressionKind::Call { .. }) => + { + Some(expression) + } + _ => None, + }) + .collect::>(); + // Diagnostic context statements bracket each trial but are not trials. + assert_eq!(calls.len(), 200); + assert_eq!(suite.identity.declaration.namespace, "alpha"); + let ExpressionKind::Call { function, args, .. } = &calls[0].kind else { + panic!("expected a direct property call"); + }; + assert_eq!( + lowered.hir.functions[function.index() as usize] + .identity + .declaration + .kind, + DeclarationKind::Property + ); + assert!(matches!(args[0].kind, ExpressionKind::Int(0))); +} + +#[test] +fn backend_lowering_handles_function_values_and_qualified_type_operations() { + let fixtures = [ + "generic_function_value_wrappers.jett", + "graphics_callback_runtime_error.jett", + "graphics_pipeline_scripted.jett", + "graphics_scene.jett", + "graphics_scripted.jett", + "json_namespace_duplicate_machine_envelope.jett", + "list_higher_order.jett", + "list_operations.jett", + "list_shape_helpers.jett", + "list_source_surface.jett", + "namespace_duplicate_leaf_types.jett", + "namespace_duplicate_leaf_interfaces.jett", + "namespace_exports_syntax.jett", + "namespace_machine_branch_narrowing.jett", + "namespace_qualified_interface_implement.jett", + "namespace_qualified_types.jett", + "namespace_use_alias.jett", + "reflection_type_id_duplicate_named_owners.jett", + ]; + + let failures = fixtures + .into_iter() + .filter_map(|name| { + lower_file_for_backend(&fixture_dir().join(name)) + .err() + .map(|error| (name, error.to_string())) + }) + .collect::>(); + assert!( + failures.is_empty(), + "checked function/type operations failed backend lowering:\n{failures:#?}" + ); +} + +#[test] +fn backend_lowering_handles_reflection_driven_generic_bodies() { + let fixtures = [ + "captured_local_function_name.jett", + "comptime_type_bind.jett", + "json_reflection_flat_decoder.jett", + "json_reflection_flat_serializer.jett", + "type_construction_builder.jett", + "type_construction_enum.jett", + "type_construction_machine.jett", + ]; + + let failures = fixtures + .into_iter() + .filter_map(|name| { + lower_file_for_backend(&fixture_dir().join(name)) + .err() + .map(|error| (name, error.to_string())) + }) + .collect::>(); + assert!( + failures.is_empty(), + "reflection generic bodies failed backend lowering:\n{failures:#?}" + ); +} + +#[test] +fn run_pass_backend_lowering_gaps_are_explicit_and_monotonic() { + let fixtures = run_pass_fixtures(); + assert_eq!(fixtures.len(), 182, "update the native parity denominator"); + + let worker_count = std::thread::available_parallelism() + .map_or(1, usize::from) + .min(8) + .min(fixtures.len()); + let chunk_size = fixtures.len().div_ceil(worker_count); + let failures = std::thread::scope(|scope| { + let workers = fixtures.chunks(chunk_size).map(|chunk| { + scope.spawn(|| { + chunk + .iter() + .filter_map(|fixture| { + lower_file_for_backend(fixture) + .err() + .map(|error| (fixture_name(fixture), error.to_string())) + }) + .collect::>() + }) + }); + workers + .flat_map(|worker| worker.join().expect("lowering worker should not panic")) + .collect::>() + }); + + assert!( + failures.is_empty(), + "run-pass fixtures still outside backend lowering:\n{failures:#?}" + ); +} + +#[test] +fn backend_lowering_bakes_explicit_comptime_but_preserves_runtime_calls() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("constants.jett"); + fs::write(&source, "function double(value: int64) returns int64:\n return value * 2\nfunction main() returns nothing:\n int64 baked = comptime double(21)\n int64 dynamic = double(20)\n return nothing\n").unwrap(); + let lowered = lower_file_for_backend(&source).expect("checked constants lower"); + let entry = lowered.program_entry.unwrap(); + let main = &lowered.mir.functions[entry.index() as usize]; + let jett_mir::StatementKind::Let { value: baked, .. } = &main.blocks[0].statements[0].kind + else { + panic!("baked let") + }; + assert!( + matches!(baked.kind, jett_hir::ExpressionKind::Int(42)), + "explicit comptime must become typed constant, got {:?}", + baked.kind + ); + let jett_mir::StatementKind::Let { value: dynamic, .. } = &main.blocks[0].statements[1].kind + else { + panic!("dynamic let") + }; + assert!( + matches!(dynamic.kind, jett_hir::ExpressionKind::Call { .. }), + "ordinary pure call must remain runtime MIR" + ); +} diff --git a/crates/jett_driver/tests/captured_run_failure.rs b/crates/jett_driver/tests/captured_run_failure.rs new file mode 100644 index 00000000..47278aa1 --- /dev/null +++ b/crates/jett_driver/tests/captured_run_failure.rs @@ -0,0 +1,78 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use jett_driver::{run_file_capture_outcome, run_file_capture_output}; + +static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct TemporarySource { + root: PathBuf, + path: PathBuf, +} + +impl TemporarySource { + fn new(source: &str) -> Self { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time should follow the Unix epoch") + .as_nanos(); + let sequence = FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "jett_captured_run_failure_{}_{}_{}", + std::process::id(), + timestamp, + sequence + )); + fs::create_dir(&root).expect("temporary source directory should be created"); + let path = root.join("main.jett"); + fs::write(&path, source).expect("temporary Jett source should be written"); + Self { root, path } + } + + fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for TemporarySource { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +#[test] +fn captured_run_failure_retains_stdout_and_typed_debug_events() { + let fixture = TemporarySource::new( + r#"namespace app +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before failure") + int64 marker = 42 + trace marker + string impossible = string.repeat("ab", 9223372036854775807) + Stdout.write(view stdout, impossible) +"#, + ); + + let failure = run_file_capture_outcome(fixture.path()) + .expect_err("the oversized repeat should fail after producing output"); + let expected_message = "runtime error: string.repeat: requested output is too large"; + + assert_eq!(failure.message, expected_message); + assert_eq!(failure.to_string(), expected_message); + assert_eq!(failure.output.stdout, "before failure"); + assert_eq!( + failure.output.debug_events, + [jett_driver::DebugEvent { + kind: jett_driver::DebugEventKind::Trace, + text: "trace marker: int64 = 42\n".into(), + }] + ); + + assert_eq!( + run_file_capture_output(fixture.path()) + .expect_err("the legacy capture API should preserve its String failure"), + expected_message + ); +} diff --git a/crates/jett_driver/tests/fixture_suite.rs b/crates/jett_driver/tests/fixture_suite.rs index c32441c3..e243511f 100644 --- a/crates/jett_driver/tests/fixture_suite.rs +++ b/crates/jett_driver/tests/fixture_suite.rs @@ -6,10 +6,11 @@ use jett_diagnostics::Severity; use jett_driver::{ BuildOptions, ClockTestSample, EnvironmentTestEntry, EnvironmentTestSnapshot, EnvironmentTestText, RandomTestSample, build_file, build_file_with_options, build_source, - completions, completions_at, hover_type, run_file, run_file_capture_output, - run_file_capture_stdout, run_file_capture_stdout_with_clock_test_samples, - run_file_capture_stdout_with_environment_test_snapshot, - run_file_capture_stdout_with_random_test_samples, run_file_with_clock_test_samples, + completions, completions_at, hover_type, run_file, + run_file_capture_outcome_with_clock_test_samples, + run_file_capture_outcome_with_environment_test_snapshot, + run_file_capture_outcome_with_random_test_samples, run_file_capture_output, + run_file_capture_stdout, run_file_with_clock_test_samples, run_file_with_environment_test_snapshot, run_file_with_random_test_samples, test_file, }; use jett_parser::ast::Item; @@ -330,8 +331,8 @@ compile_pass_fixture!( compile_pass_generic_reflection_local_fact_specialization, "generic_reflection_local_fact_specialization.jett" ); -compile_pass_fixture!( - compile_pass_generic_reflection_runtime_guard_deferral, +compile_fail_fixture!( + compile_fail_generic_reflection_runtime_guard_deferral, "generic_reflection_runtime_guard_deferral.jett" ); compile_pass_fixture!( @@ -392,6 +393,15 @@ compile_pass_fixture!( ); compile_fail_fixture!(compile_fail_type_mismatch, "type_mismatch.jett"); +compile_fail_fixture!(compile_fail_function_view_modes, "function_view_modes.jett"); +compile_fail_fixture!( + compile_fail_function_expression_ownership, + "function_expression_ownership.jett" +); +compile_fail_fixture!( + compile_fail_function_expression_policy, + "function_expression_policy.jett" +); compile_fail_fixture!(compile_fail_secret_stdout, "secret_stdout.jett"); compile_fail_fixture!(compile_fail_secret_print, "secret_print.jett"); @@ -434,6 +444,10 @@ compile_fail_fixture!( compile_fail_comptime_expression_requires_closed_value, "comptime_expression_requires_closed_value.jett" ); +compile_fail_fixture!( + compile_fail_comptime_generic_requires_closed_value, + "comptime_generic_requires_closed_value.jett" +); compile_fail_fixture!( compile_fail_comptime_expression_rejects_capability, "comptime_expression_rejects_capability.jett" @@ -458,6 +472,14 @@ compile_fail_fixture!( compile_fail_refinement_requires_handle, "refinement_requires_handle.jett" ); +compile_fail_fixture!( + compile_fail_refinement_sum_requires_handle, + "refinement_sum_requires_handle.jett" +); +compile_fail_fixture!( + compile_fail_refinement_sum_requires_handle_error, + "refinement_sum_requires_handle_error.jett" +); run_pass_fixture!(run_pass_simple, "simple.jett"); run_pass_fixture!(run_pass_fibonacci, "fibonacci.jett"); @@ -528,6 +550,16 @@ run_pass_fixture!( run_pass_explicit_struct_equality, "explicit_struct_equality.jett" ); +#[test] +fn interface_explicit_comparisons_preserve_concrete_and_domain_contracts() { + let path = fixture_path("native", "interface_explicit_comparison.jett"); + let output = run_file_capture_output(&path).expect("explicit comparison reference"); + assert_eq!(output.stdout, "true false\ntrue true\n7 7 8\n"); + assert!(output.debug_events.is_empty()); + let tested = test_file(&path).expect("explicit comparison verify block"); + assert_eq!((tested.total, tested.passed, tested.failed), (1, 1, 0)); +} + run_pass_fixture!( run_pass_primitive_collection_hash_types, "primitive_collection_hash_types.jett" @@ -652,11 +684,19 @@ fn run_file_capture_stdout_captures_json_runtime_output() { #[test] fn run_file_capture_output_captures_debug_lines() { let cases = [ - ("trace_basic.jett", "trace total: int64 = 42"), - ("breakpoint_basic.jett", "breakpoint hit: total: int64 = 42"), + ( + "trace_basic.jett", + jett_driver::DebugEventKind::Trace, + "trace total: int64 = 42\n", + ), + ( + "breakpoint_basic.jett", + jett_driver::DebugEventKind::Breakpoint, + "breakpoint hit: total: int64 = 42\n", + ), ]; - for (name, expected) in cases { + for (name, kind, expected) in cases { let path = fixture_path("run_pass", name); let output = run_file_capture_output(&path).unwrap_or_else(|err| { panic!( @@ -665,7 +705,13 @@ fn run_file_capture_output_captures_debug_lines() { ) }); assert_eq!(output.stdout, ""); - assert_eq!(output.debug_output, vec![expected]); + assert_eq!( + output.debug_events, + vec![jett_driver::DebugEvent { + kind, + text: expected.into() + }] + ); } } @@ -1184,6 +1230,18 @@ compile_fail_fixture!( compile_fail_ownership_branch_partial_move, "ownership_branch_partial_move.jett" ); +compile_fail_fixture!( + compile_fail_ownership_machine_state_widening_use_after_move, + "ownership_machine_state_widening_use_after_move.jett" +); +compile_fail_fixture!( + compile_fail_immutable_reassignment, + "immutable_reassignment.jett" +); +compile_fail_fixture!( + compile_fail_projected_readonly_roots, + "projected_readonly_roots.jett" +); compile_fail_fixture!( compile_fail_ownership_for_zero_iteration_rebind, "ownership_for_zero_iteration_rebind.jett" @@ -1200,6 +1258,10 @@ compile_fail_fixture!( compile_fail_ownership_collection_literals_move_elements, "ownership_collection_literals_move_elements.jett" ); +compile_fail_fixture!( + compile_fail_ownership_result_optional_wrappers_move_payloads, + "ownership_result_optional_wrappers_move_payloads.jett" +); compile_fail_fixture!( compile_fail_math_sum_consumes_list, "math_sum_consumes_list.jett" @@ -1571,7 +1633,7 @@ fn compile_fail_bytes_concat_consumes_both_inputs() { #[test] fn run_pass_random_scripted_contract() { let path = fixture_path("run_pass", "random_scripted.jett"); - let output = run_file_capture_stdout_with_random_test_samples( + let output = run_file_capture_outcome_with_random_test_samples( &path, vec![ RandomTestSample::Bounded(0), @@ -1588,8 +1650,10 @@ fn run_pass_random_scripted_contract() { ], ) .unwrap_or_else(|err| panic!("expected {} to run successfully: {err}", path.display())); + assert!(output.stdout.is_empty()); + assert!(output.frontend_debug_observations.is_empty()); assert_eq!( - output, + jett_driver::render_debug_events(&output.debug_events), concat!( "5:random.int64: lower bound must be less than upper bound:-1:1:0:9\n", "-9223372036854775808:9223372036854775806:0:0.9999999999999999:false:true\n", @@ -1637,7 +1701,7 @@ fn runtime_fail_random_invalid_test_sample() { #[test] fn run_pass_clock_scripted_contract() { let path = fixture_path("run_pass", "clock_scripted.jett"); - let output = run_file_capture_stdout_with_clock_test_samples( + let output = run_file_capture_outcome_with_clock_test_samples( &path, vec![ ClockTestSample::Wall { @@ -1663,7 +1727,12 @@ fn run_pass_clock_scripted_contract() { ], ) .unwrap_or_else(|err| panic!("expected {} to run successfully: {err}", path.display())); - assert_eq!(output, "0:0:-1:42123:40000\n"); + assert!(output.stdout.is_empty()); + assert!(output.frontend_debug_observations.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&output.debug_events), + "0:0:-1:42123:40000\n" + ); } #[test] @@ -1745,7 +1814,7 @@ fn runtime_fail_clock_invalid_test_sample() { fn run_pass_environment_injected_snapshot_contract() { let path = fixture_path("run_pass", "environment_snapshot.jett"); let text = |value: &str| EnvironmentTestText::Unicode(value.to_string()); - let output = run_file_capture_stdout_with_environment_test_snapshot( + let output = run_file_capture_outcome_with_environment_test_snapshot( &path, EnvironmentTestSnapshot { arguments: vec![text("first"), text(""), text("third")], @@ -1774,8 +1843,10 @@ fn run_pass_environment_injected_snapshot_contract() { }, ) .unwrap_or_else(|err| panic!("expected {} to run successfully: {err}", path.display())); + assert!(output.stdout.is_empty()); + assert!(output.frontend_debug_observations.is_empty()); assert_eq!( - output, + jett_driver::render_debug_events(&output.debug_events), concat!( "4:3:first:[]:third\n", "value:none:first:Environment.get: value is not valid Unicode:Environment.get: invalid variable name\n" @@ -2078,8 +2149,8 @@ compile_fail_fixture!( compile_fail_generic_reflection_guarded_type_arg_error, "generic_reflection_guarded_type_arg_error.jett" ); -compile_fail_fixture!( - compile_fail_generic_reflection_guarded_unknown_fact, +compile_pass_fixture!( + compile_pass_generic_reflection_guarded_unknown_fact, "generic_reflection_guarded_unknown_fact.jett" ); compile_fail_fixture!( @@ -2090,8 +2161,8 @@ compile_fail_fixture!( compile_fail_generic_reflection_match_reachable_arm_error, "generic_reflection_match_reachable_arm_error.jett" ); -compile_fail_fixture!( - compile_fail_generic_reflection_match_unknown_fact, +compile_pass_fixture!( + compile_pass_generic_reflection_match_unknown_fact, "generic_reflection_match_unknown_fact.jett" ); compile_fail_fixture!( @@ -2162,6 +2233,48 @@ compile_fail_fixture!( compile_fail_equatable_signature_is_exact, "equatable_signature_is_exact.jett" ); +compile_fail_fixture!(compile_fail_mutable_global, "mutable_global.jett"); +compile_fail_fixture!( + compile_fail_global_constant_initializer, + "global_constant_initializer.jett" +); +compile_fail_fixture!( + compile_fail_direct_collection_equality, + "direct_collection_equality.jett" +); +compile_fail_fixture!( + compile_fail_direct_collection_equality_wrappers, + "direct_collection_equality_wrappers.jett" +); +compile_fail_fixture!( + compile_fail_direct_collection_equality_generic, + "direct_collection_equality_generic.jett" +); +compile_fail_fixture!( + compile_fail_direct_collection_equality_comptime, + "direct_collection_equality_comptime.jett" +); +compile_fail_fixture!(compile_fail_interface_equality, "interface_equality.jett"); +compile_fail_fixture!( + compile_fail_interface_equality_generic, + "interface_equality_generic.jett" +); +compile_fail_fixture!( + compile_fail_interface_equality_comptime, + "interface_equality_comptime.jett" +); +compile_fail_fixture!( + compile_fail_interface_equality_enum_payload, + "interface_equality_enum_payload.jett" +); +compile_fail_fixture!( + compile_fail_enum_struct_equality_requires_equatable, + "enum_struct_equality_requires_equatable.jett" +); +compile_fail_fixture!( + compile_fail_graphics_enum_equality_effect, + "graphics_enum_equality_effect.jett" +); compile_fail_fixture!( compile_fail_collection_struct_hashing_is_unsupported, "collection_struct_hashing_is_unsupported.jett" @@ -2975,3 +3088,40 @@ fn stdlib_loaded_for_test_file() { .unwrap_or_else(|err| panic!("expected jett test path to load stdlib: {err}")); assert_eq!(result.failed, 0); } + +compile_fail_fixture!( + compile_fail_generic_method_owner, + "generic_method_owner.jett" +); + +compile_fail_fixture!( + compile_fail_generic_reflection_body_type_error, + "generic_reflection_body_type_error.jett" +); + +compile_fail_fixture!( + compile_fail_closure_reflection_boolean_boundaries, + "closure_reflection_boolean_boundaries.jett" +); + +compile_fail_fixture!( + compile_fail_generic_reflection_unknown_guard_invalid_binding, + "generic_reflection_unknown_guard_invalid_binding.jett" +); + +compile_fail_fixture!(compile_fail_view_owned_returns, "view_owned_returns.jett"); + +compile_fail_fixture!( + compile_fail_view_owned_arguments, + "view_owned_arguments.jett" +); + +compile_fail_fixture!( + compile_fail_view_explicit_owned_modes, + "view_explicit_owned_modes.jett" +); + +compile_fail_fixture!( + compile_fail_view_return_annotations, + "view_return_annotations.jett" +); diff --git a/crates/jett_driver/tests/global_constants.rs b/crates/jett_driver/tests/global_constants.rs new file mode 100644 index 00000000..6ff7e71c --- /dev/null +++ b/crates/jett_driver/tests/global_constants.rs @@ -0,0 +1,490 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use jett_diagnostics::Severity; +use jett_driver::{ + build_file, build_source, run_file_capture_outcome, run_file_capture_stdout, test_file, +}; +use tempfile::TempDir; + +struct SourceFixture { + directory: TempDir, + entry: PathBuf, +} + +impl SourceFixture { + fn new(source: &str) -> Self { + let directory = tempfile::tempdir().expect("create constant fixture directory"); + let entry = directory.path().join("main.jett"); + fs::write(&entry, source).expect("write constant fixture source"); + Self { directory, entry } + } + + fn project(source: &str) -> Self { + let directory = tempfile::tempdir().expect("create constant project directory"); + fs::create_dir(directory.path().join("src")).expect("create project source directory"); + fs::write( + directory.path().join("jett.proj"), + "name: global_constants\nversion: 0.1.0\nentry: src/main.jett\n", + ) + .expect("write constant project manifest"); + let entry = directory.path().join("src/main.jett"); + fs::write(&entry, source).expect("write constant project entry"); + Self { directory, entry } + } + + fn write(&self, path: &str, source: &str) { + fs::write(self.directory.path().join(path), source).expect("write project source"); + } + + fn entry(&self) -> &Path { + &self.entry + } +} + +fn assert_builds(path: &Path) { + let built = build_file(path); + assert!(!built.has_errors, "{:?}", built.diagnostics); +} + +fn assert_checks_pass(path: &Path, total: usize) { + let checks = test_file(path).expect("constant verify and property blocks should execute"); + let failures = checks + .blocks + .iter() + .filter(|block| !block.passed) + .map(|block| (&block.name, &block.error)) + .collect::>(); + assert_eq!(checks.total, total, "{failures:?}"); + assert_eq!(checks.passed, total, "{failures:?}"); + assert_eq!(checks.failed, 0, "{failures:?}"); + for property in checks.blocks.iter().filter(|block| block.is_property) { + assert!(property.iterations.is_some_and(|iterations| iterations > 0)); + } +} + +fn assert_constant_rejected_before_execution(path: &Path, expected_message: &str) { + let built = build_file(path); + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!( + built.has_errors, + "invalid constants must be rejected even when unused" + ); + assert!(!errors.is_empty(), "{:?}", built.diagnostics); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 9001), + "{:?}", + built.diagnostics + ); + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.message.contains(expected_message)), + "{:?}", + built.diagnostics + ); + let failure = run_file_capture_outcome(path) + .expect_err("invalid compile-time constants must prevent main execution"); + assert!(failure.message.contains("E9001"), "{}", failure.message); + assert!( + failure.message.contains(expected_message), + "{}", + failure.message + ); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + let failure = test_file(path) + .err() + .expect("test execution must reject invalid constants"); + assert!(failure.contains("E9001"), "{failure}"); + assert!(failure.contains(expected_message), "{failure}"); +} + +#[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +fn assert_native_constant_rejection_preserves_output(path: &Path, expected_message: &str) { + use jett_driver::native::{ + NativeBuildError, NativeLauncherBundle, build_host_executable_with_options, + build_host_property_suite_executable, build_host_verify_suite_executable, + }; + + let directory = tempfile::tempdir().expect("create native rejection output directory"); + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing native output"; + fs::write(&output, sentinel).expect("write existing native executable sentinel"); + // Frontend failure must happen before the launcher archive is opened. + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let check_failure = |error: NativeBuildError| { + let NativeBuildError::Lowering { source, .. } = error else { + panic!("expected checked constant rejection before native publication, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(checked) = *source else { + panic!("expected preserved frontend diagnostics before HIR"); + }; + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(!errors.is_empty(), "{:?}", checked.diagnostics); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 9001), + "{:?}", + checked.diagnostics + ); + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.message.contains(expected_message)), + "{:?}", + checked.diagnostics + ); + assert_eq!( + fs::read(&output).expect("read preserved executable"), + sentinel + ); + }; + for release in [false, true] { + check_failure( + build_host_executable_with_options( + path, + &launcher, + &output, + jett_driver::BuildOptions { release }, + ) + .expect_err("invalid constants prevent debug and release native publication"), + ); + } + check_failure( + build_host_verify_suite_executable(path, &launcher, &output) + .expect_err("invalid constants prevent native verify suite publication"), + ); + check_failure( + build_host_property_suite_executable(path, &launcher, &output) + .expect_err("invalid constants prevent native property suite publication"), + ); +} + +#[test] +fn build_source_evaluates_constants_before_verification() { + let built = build_source( + "namespace app\nint64 base = 42\nint64 answer = base\nverify constants:\n assert answer == 42\n", + "constants.jett", + ); + assert!(!built.has_errors, "{:?}", built.diagnostics); + let values = built + .explicit_comptime_values + .expect("checked constant table"); + assert!(!values.is_empty()); + assert_eq!(values.len(), 2); +} + +#[test] +fn literal_and_explicit_constants_match_reference_checks() { + let fixture = SourceFixture::new( + r#"namespace app +int64 count = 21 +int64 copied = count +int64 doubled = comptime (copied * 2) +int8 maximum = 127 +int8 wrapped = comptime (maximum + 1) +uint8 unsigned_maximum = 255 +uint8 unsigned_wrapped = comptime (unsigned_maximum + 1) +float32 scale = 1.25 +bool enabled = true +string label = comptime "{doubled}:{wrapped}:{unsigned_wrapped}:{scale}:{enabled}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{label}\n") +verify constants: + int8 minimum = -128 + assert doubled == 42 + assert wrapped == minimum + assert unsigned_wrapped == 0 + assert scale == 1.25 + assert enabled + assert label == "42:-128:0:1.25:true" +property constant_reads: + given value: int64 + assert value + doubled - doubled == value + assert label == "42:-128:0:1.25:true" +"#, + ); + let built = build_file(fixture.entry()); + assert!(!built.has_errors, "{:?}", built.diagnostics); + let values = built + .explicit_comptime_values + .expect("checked constant table"); + assert!( + !values.is_empty(), + "namespace declarations have compiler values" + ); + assert_eq!(values.len(), values.values().count()); + assert_eq!( + run_file_capture_stdout(fixture.entry()).expect("run literal constant program"), + "42:-128:0:1.25:true\n" + ); + assert_checks_pass(fixture.entry(), 2); +} + +#[test] +fn sibling_namespaces_keep_same_named_constants_distinct() { + let fixture = SourceFixture::project( + r#"namespace app +int64 answer = 80 +function combined() returns int64: + use alpha + use beta + return alpha.read() + beta.read() + answer +function main(stdout: Stdout) returns nothing: + use alpha + use beta + Stdout.write(view stdout, "{alpha.read()}:{beta.read()}:{answer}:{combined()}\n") +verify combined: + assert combined() == 120 +property namespace_constants: + given value: int64 + assert combined() == 120 +"#, + ); + fixture.write( + "src/00_alpha.jett", + r#"namespace alpha +int64 answer = 11 +export function read() returns int64: + return answer +verify alpha_constant: + assert read() == 11 +"#, + ); + fixture.write( + "src/10_beta.jett", + r#"namespace beta +int64 answer = 29 +export function read() returns int64: + return answer +verify beta_constant: + assert read() == 29 +"#, + ); + assert_builds(fixture.entry()); + assert_eq!( + run_file_capture_stdout(fixture.entry()).expect("run project constant program"), + "11:29:80:120\n" + ); + assert_checks_pass(fixture.entry(), 2); + assert_checks_pass(&fixture.directory.path().join("src/00_alpha.jett"), 1); + assert_checks_pass(&fixture.directory.path().join("src/10_beta.jett"), 1); +} + +#[test] +fn inline_callbacks_and_generic_bodies_read_namespace_constants() { + let fixture = SourceFixture::new( + r#"namespace app +int64 offset = 6 +string prefix = "baked" +function generic_offset[T](value: T) returns int64: + return offset +function make_callback[T](value: T) returns function(int64) returns string: + return function(number: int64) returns string: return "{prefix}:{number + offset}" +function callback_result() returns string: + function(int64) returns string callback = make_callback[bool](true) + return callback(4) +function inline_result() returns int64: + function(int64) returns int64 callback = function(number: int64) returns int64: return number + offset + return callback(3) +function main(stdout: Stdout) returns nothing: + int64 integer_offset = generic_offset[int64](10) + int64 string_offset = generic_offset[string]("ignored") + Stdout.write(view stdout, "{integer_offset}:{string_offset}:{callback_result()}:{inline_result()}\n") +verify callback_constants: + assert generic_offset[int64](10) == 6 + assert generic_offset[string]("ignored") == 6 + assert callback_result() == "baked:10" + assert inline_result() == 9 +property generic_constants: + given value: int64 + assert generic_offset[int64](value) == 6 + assert callback_result() == "baked:10" +"#, + ); + assert_builds(fixture.entry()); + assert_eq!( + run_file_capture_stdout(fixture.entry()).expect("run callback constant program"), + "6:6:baked:10:9\n" + ); + assert_checks_pass(fixture.entry(), 2); +} + +#[test] +fn ordinary_function_initializers_require_explicit_comptime() { + let fixture = SourceFixture::new( + r#"namespace app +function compute() returns int64: + return 42 +int64 answer = compute() +function main() returns int64: + return answer +"#, + ); + let built = build_file(fixture.entry()); + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(built.has_errors); + assert_eq!(errors.len(), 1, "{:?}", built.diagnostics); + assert_eq!(errors[0].code.code(), 378, "{:?}", built.diagnostics); + assert!(errors[0].message.contains("comptime expression")); +} + +#[test] +fn explicit_comptime_function_initializers_are_baked() { + let fixture = SourceFixture::new( + r#"namespace app +int64 base = 21 +function compute() returns int64: + return base * 2 +int64 answer = comptime compute() +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{answer}\n") +verify evaluated_constant: + assert answer == 42 +property evaluated_constant_reads: + given value: int64 + assert answer == 42 +"#, + ); + assert_builds(fixture.entry()); + assert_eq!( + run_file_capture_stdout(fixture.entry()).expect("run evaluated constant program"), + "42\n" + ); + assert_checks_pass(fixture.entry(), 2); +} + +#[test] +fn unused_constant_evaluation_failure_prevents_runtime_execution() { + let fixture = SourceFixture::new( + r#"namespace app +function impossible() returns string: + return string.repeat("ab", 9223372036854775807) +string unused = comptime impossible() +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "main must not run") +verify would_otherwise_pass: + assert true +"#, + ); + let built = build_file(fixture.entry()); + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!( + built.has_errors, + "failing constants must be evaluated even when unused" + ); + assert!(!errors.is_empty(), "{:?}", built.diagnostics); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 9001) + ); + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.message.contains("requested output is too large")), + "{:?}", + built.diagnostics + ); + let failure = run_file_capture_outcome(fixture.entry()) + .expect_err("failed compile-time constants must prevent main execution"); + assert!(failure.message.contains("E9001"), "{}", failure.message); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + let failure = test_file(fixture.entry()) + .err() + .expect("test execution must reject failed constants"); + assert!(failure.contains("E9001"), "{failure}"); +} + +#[test] +fn namespace_constants_cannot_be_rebound() { + let fixture = SourceFixture::new( + r#"namespace app +int64 answer = 42 +function main() returns nothing: + answer = 43 +"#, + ); + let built = build_file(fixture.entry()); + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(built.has_errors); + assert_eq!(errors.len(), 1, "{:?}", built.diagnostics); + assert_eq!(errors[0].code.code(), 404, "{:?}", built.diagnostics); + assert!(errors[0].message.contains("cannot reassign `answer`")); +} + +#[test] +fn unused_aggregate_constants_are_rejected_before_execution() { + for declaration in [ + "list[int64] unused = comptime list(1, 2)", + "Record unused = comptime Record(value: 42)", + ] { + let fixture = SourceFixture::new(&format!( + "namespace app\nstruct Record:\n value: int64\n{declaration}\nfunction main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"main must not run\")\nverify would_otherwise_pass:\n assert true\nproperty constant_check:\n given value: int64\n assert value == value\n" + )); + let expected = "implicitly copyable primitive type"; + assert_constant_rejected_before_execution(fixture.entry(), expected); + #[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) + ))] + assert_native_constant_rejection_preserves_output(fixture.entry(), expected); + } +} + +#[test] +fn hidden_pending_primitive_constants_are_rejected_before_execution() { + for expression in ["run(42)", "run(run(42))"] { + let fixture = SourceFixture::new(&format!( + "namespace app\nint64 pending_value = comptime {expression}\nfunction main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"main must not run\")\nverify would_otherwise_pass:\n assert true\nproperty constant_check:\n given value: int64\n assert value == value\n" + )); + let expected = "does not match its declared primitive type"; + assert_constant_rejected_before_execution(fixture.entry(), expected); + #[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) + ))] + assert_native_constant_rejection_preserves_output(fixture.entry(), expected); + } +} diff --git a/crates/jett_driver/tests/graphics_frontend.rs b/crates/jett_driver/tests/graphics_frontend.rs index f23b1565..78344881 100644 --- a/crates/jett_driver/tests/graphics_frontend.rs +++ b/crates/jett_driver/tests/graphics_frontend.rs @@ -12,6 +12,54 @@ fn fixture(kind: &str, name: &str) -> PathBuf { .join(name) } +// A valid capability-taking nested closure can occur outside a graphics callback +// graph. Inside it, the conservative audit rejects its signature, effect call, +// and capability use, even if that inner closure is not invoked. Keep the outer +// perform() calls so equality/method/helper dispatch still has to be followed. +fn assert_typed_callback_effect(source: &str) { + let outcome = build_source(source, "graphics-typed-callback-effect.jett"); + let errors = outcome + .diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .collect::>(); + assert_eq!( + errors.iter().map(|d| d.code.code()).collect::>(), + vec![372, 500, 372], + "{:?}", + outcome.diagnostics + ); + assert!(errors[0].message.contains("callback closures must be pure")); + assert!(errors[1].message.contains("")); + assert!(errors[1].message.contains("app.effect")); + assert!( + errors[2] + .message + .contains("callback closures cannot contain opaque capability values") + ); +} + +fn without_nested_effect(source: &str) -> String { + let line = source + .lines() + .find(|line| line.contains("emit = function(")) + .expect("typed nested effect probe must exist"); + let indent = &line[..line.len() - line.trim_start().len()]; + source.replace(line, &format!("{indent}function(int64) returns int64 emit = function(value: int64) returns int64: return value")) +} + +fn assert_graphics_codes(source: &str, expected: &[u16]) { + let outcome = build_source(source, "graphics-policy-variant.jett"); + let mut codes = outcome + .diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .map(|d| d.code.code()) + .collect::>(); + codes.sort_unstable(); + assert_eq!(codes, expected, "{:?}", outcome.diagnostics); +} + #[test] fn graphics_scene_and_callbacks_compile_without_opening_a_window() { let outcome = test_file(&fixture("run_pass", "graphics_scene.jett")) @@ -27,30 +75,48 @@ fn graphics_frontend_policy_failures() { ("graphics_capability_ownership.jett", vec![502, 503]), ("graphics_private_kernel.jett", vec![303, 313]), ("graphics_callback_types.jett", vec![300]), - ("graphics_callback_policy.jett", vec![372, 372, 372]), + ("graphics_callback_policy.jett", vec![300, 372, 372, 372]), ("graphics_capability_required.jett", vec![303, 500]), ("graphics_verify_forbidden.jett", vec![303, 501]), ("graphics_property_forbidden.jett", vec![303, 372]), ("graphics_namespace_collision.jett", vec![204, 205]), - ("graphics_fake_authority.jett", vec![372]), + ("graphics_fake_authority.jett", vec![300, 372]), ("graphics_comptime_forbidden.jett", vec![504, 504]), - ("graphics_inline_callback_effect.jett", vec![500]), + ("graphics_inline_callback_effect.jett", vec![372, 372, 500]), ("graphics_callback_capture_capability.jett", vec![402]), - ("graphics_opaque_data.jett", vec![372]), - ("graphics_named_callback_nested_effect.jett", vec![500]), - ("graphics_transitive_callback_effect.jett", vec![500]), - ("graphics_inline_transitive_effect.jett", vec![500]), - ("graphics_callback_capability_alias.jett", vec![372]), + ("graphics_opaque_data.jett", vec![300, 372]), + ( + "graphics_named_callback_nested_effect.jett", + vec![372, 372, 500], + ), + ( + "graphics_transitive_callback_effect.jett", + vec![372, 372, 500], + ), + ( + "graphics_inline_transitive_effect.jett", + vec![372, 372, 500], + ), + ("graphics_callback_capability_alias.jett", vec![300, 372]), ("graphics_private_kernel_values.jett", vec![303, 313, 313]), - ("graphics_state_callback_authority.jett", vec![372]), - ("graphics_pipeline_callback_effect.jett", vec![500]), - ("graphics_pipeline_inferred_callback_effect.jett", vec![500]), - ("graphics_pipeline_authority.jett", vec![372]), + ( + "graphics_state_callback_authority.jett", + vec![372, 402, 500], + ), + ( + "graphics_pipeline_callback_effect.jett", + vec![372, 372, 500], + ), + ( + "graphics_pipeline_inferred_callback_effect.jett", + vec![372, 372, 500], + ), + ("graphics_pipeline_authority.jett", vec![300, 372]), ("graphics_pipeline_state_authority.jett", vec![372]), - ("graphics_mutual_method_effect.jett", vec![500]), - ("graphics_generic_method_effect.jett", vec![500]), - ("graphics_inline_method_effect.jett", vec![500]), - ("graphics_equality_effect.jett", vec![500]), + ("graphics_mutual_method_effect.jett", vec![372, 372, 500]), + ("graphics_generic_method_effect.jett", vec![372, 372, 500]), + ("graphics_inline_method_effect.jett", vec![372, 372, 500]), + ("graphics_equality_effect.jett", vec![372, 372, 500]), ] { let outcome = build_file(&fixture("compile_fail", name)); let mut codes = outcome @@ -89,10 +155,12 @@ fn graphics_equality_operators_audit_implicit_methods_in_concrete_instantiations .collect::>(); assert_eq!( codes, - vec![500], + vec![372, 500, 372], "{operator}, generic={generic}: {:?}", outcome.diagnostics ); + assert_typed_callback_effect(&source); + assert_graphics_codes(&without_nested_effect(&source), &[]); } } } @@ -108,6 +176,18 @@ fn graphics_equality_audit_does_not_follow_unrelated_generic_instantiations() { ); let outcome = build_source(&source, "graphics-equality-isolation.jett"); assert!(!outcome.has_errors, "{operator}: {:?}", outcome.diagnostics); + let reachable_effect = source + .replace( + "Pure left = Pure(value: state)", + "Effectful left = Effectful(value: state)", + ) + .replace( + "Pure right = Pure(value: state)", + "Effectful right = Effectful(value: state)", + ) + .replace("compare[Pure]", "compare[Effectful]"); + assert_ne!(reachable_effect, source); + assert_typed_callback_effect(&reachable_effect); } } @@ -127,3 +207,116 @@ fn graphics_pipeline_and_mutual_callbacks_compile_without_opening_a_window() { assert!(!outcome.has_errors, "{name}: {:?}", outcome.diagnostics); } } + +#[test] +fn graphics_nested_effect_probes_have_well_typed_pure_controls() { + for name in [ + "graphics_inline_callback_effect.jett", + "graphics_named_callback_nested_effect.jett", + "graphics_transitive_callback_effect.jett", + "graphics_inline_transitive_effect.jett", + "graphics_pipeline_callback_effect.jett", + "graphics_pipeline_inferred_callback_effect.jett", + "graphics_mutual_method_effect.jett", + "graphics_generic_method_effect.jett", + "graphics_inline_method_effect.jett", + "graphics_equality_effect.jett", + ] { + let source = std::fs::read_to_string(fixture("compile_fail", name)).unwrap(); + assert_typed_callback_effect(&source); + assert_graphics_codes(&without_nested_effect(&source), &[]); + } +} + +#[test] +fn graphics_generic_method_isolation_rejects_reachable_effects() { + let source = include_str!("../../../tests/compile_pass/graphics_generic_method_isolation.jett"); + assert_graphics_codes(source, &[]); + let reachable_effect = source + .replace( + "Pure pure = Pure(value: state)", + "Effectful pure = Effectful(value: state)", + ) + .replace("evaluate[Pure]", "evaluate[Effectful]"); + assert_ne!(reachable_effect, source); + assert_typed_callback_effect(&reachable_effect); +} + +#[test] +fn graphics_callback_signature_and_policy_are_independent_obligations() { + let source = include_str!("../../../tests/compile_fail/graphics_callback_policy.jett"); + let pipeline = source.replace( + "graphics.run[int64](view display, ", + "display into view graphics.run[int64](", + ); + for source in [ + source.to_owned(), + source.replace("graphics.run[int64]", "graphics.run"), + pipeline.clone(), + pipeline.replace("graphics.run[int64]", "graphics.run"), + ] { + assert_graphics_codes(&source, &[300, 372, 372, 372]); + // A pure but wrong Key type owes E0300, not the impure-update E0372. + let wrong_key = source.replace( + "impure_update(state: int64, view display: Graphics)", + "impure_update(state: int64, key: string)", + ); + assert_ne!(wrong_key, source); + assert_graphics_codes(&wrong_key, &[300, 372, 372]); + // Correct types with a borrowed Key owe only the parameter-mode E0372. + let borrowed_key = source.replace( + "impure_update(state: int64, view display: Graphics)", + "impure_update(state: int64, view key: graphics.Key)", + ); + assert_graphics_codes(&borrowed_key, &[372, 372, 372]); + let valid_update = borrowed_key.replace("view key: graphics.Key", "key: graphics.Key"); + assert_graphics_codes(&valid_update, &[372, 372]); + } +} + +#[test] +fn graphics_authority_diagnostics_have_independent_controls() { + for (name, before, after, expected) in [ + ( + "graphics_callback_capability_alias.jett", + "type Display = Graphics", + "type Display = int64", + &[][..], + ), + ( + "graphics_opaque_data.jett", + "view display, display, 0", + "view display, 0, 0", + &[300][..], + ), + ( + "graphics_state_callback_authority.jett", + "return effect(view stdout)", + "return 0", + &[372][..], + ), + ( + "graphics_callback_capture_capability.jett", + "Graphics captured = display", + "int64 captured = state", + &[][..], + ), + ] { + let source = std::fs::read_to_string(fixture("compile_fail", name)).unwrap(); + let variant = source.replace(before, after); + assert_ne!(source, variant, "{name}"); + assert_graphics_codes(&variant, expected); + } + // Removing the forged type alone still leaves the explicit-view obligation. + let source = include_str!("../../../tests/compile_fail/graphics_fake_authority.jett"); + let typed = source.replace(r#""Graphics""#, "display"); + assert_ne!(typed, source); + assert_graphics_codes(&typed, &[372]); + assert_graphics_codes(&source.replace(r#""Graphics""#, "view display"), &[]); + + let source = include_str!("../../../tests/compile_fail/graphics_pipeline_authority.jett"); + let typed = source.replace(r#""fake authority""#, "display"); + assert_ne!(typed, source); + assert_graphics_codes(&typed, &[]); + assert_graphics_codes(&typed.replace("into view", "into"), &[372]); +} diff --git a/crates/jett_driver/tests/graphics_runtime.rs b/crates/jett_driver/tests/graphics_runtime.rs index fa6401e9..d5121379 100644 --- a/crates/jett_driver/tests/graphics_runtime.rs +++ b/crates/jett_driver/tests/graphics_runtime.rs @@ -21,7 +21,7 @@ fn graphics_main_receives_authority_and_runs_checked_callbacks_without_a_window( ) .expect("checked graphics callbacks should run through the scripted provider"); assert_eq!(output.stdout, "closed\n"); - assert!(output.debug_output.is_empty()); + assert!(output.debug_events.is_empty()); } #[test] @@ -57,7 +57,7 @@ fn graphics_pipelines_preserve_generic_calls_aliases_and_argument_order() { ) .expect("explicit and inferred graphics pipelines should run through the scripted provider"); assert_eq!(output.stdout, "closed three times\n"); - assert!(output.debug_output.is_empty()); + assert!(output.debug_events.is_empty()); } #[test] diff --git a/crates/jett_driver/tests/list_sum_domain.rs b/crates/jett_driver/tests/list_sum_domain.rs new file mode 100644 index 00000000..496b69c1 --- /dev/null +++ b/crates/jett_driver/tests/list_sum_domain.rs @@ -0,0 +1,148 @@ +use std::fs; +use std::path::Path; + +use jett_diagnostics::Severity; +use jett_driver::{BuildOptions, BuildResult, build_file_with_options, build_source, test_file}; + +const DOMAIN: &str = "numeric primitive for list.sum"; + +fn assert_rejected(built: &BuildResult) { + assert!(built.has_errors, "unsupported sum was accepted"); + let errors = built + .diagnostics + .iter() + .filter(|error| error.severity == Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{:?}", built.diagnostics); + assert_eq!(errors[0].code.code(), 300, "{errors:?}"); + assert!(errors[0].message.contains(DOMAIN), "{errors:?}"); + assert!(built.debug_observations.is_empty()); +} + +fn reject_source(source: &str) { + assert_rejected(&build_source(source, "list_sum_domain.jett")); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + for release in [false, true] { + assert_rejected(&build_file_with_options(&path, BuildOptions { release })); + } +} + +#[test] +fn list_sum_public_facade_rejects_unsupported_element_domains() { + let declarations = "namespace app\ntype Positive = int64 where value > 0\ntype PositiveAlias = Positive\ntype Hidden = secret[int64]\nstruct Record:\n value: int64\n"; + for element in [ + "bool", + "string", + "bytes", + "optional[int64]", + "result[int64, string]", + "list[int64]", + "Record", + "Positive", + "PositiveAlias", + "secret[int64]", + "Hidden", + "secret[Positive]", + ] { + reject_source(&format!( + "{declarations}function total(view values: list[{element}]) returns {element}:\n return list.sum[{element}](view values)\n" + )); + } +} + +#[test] +fn list_sum_generic_inference_pipelines_and_function_wrappers_keep_domain_policy() { + let generic = "namespace app\nfunction total[T](values: list[T]) returns T:\n return list.sum[T](view values)\n"; + for call in [ + "total[bool](values)", + "total(values)", + "values into total[bool]()", + "values into total()", + ] { + reject_source(&format!( + "{generic}function invalid(values: list[bool]) returns bool:\n return {call}\n" + )); + } + for call in [ + "list.sum(view values)", + "view values into list.sum[bool]()", + "view values into list.sum()", + ] { + reject_source(&format!( + "namespace app\nfunction invalid(view values: list[bool]) returns bool:\n return {call}\n" + )); + } + reject_source( + "namespace app\nfunction invalid(values: list[bool]) returns bool:\n return list.sum[bool](view values)\nfunction invoke() returns bool:\n function(list[bool]) returns bool callback = invalid\n return callback(list(true))\n", + ); +} + +#[test] +fn list_sum_invalid_domain_stops_required_values_and_test_bodies_before_evaluation() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + for body in [ + "function total() returns bool:\n list[bool] values = list()\n return list.sum[bool](view values)\nfunction main() returns nothing:\n bool baked = comptime total()\n", + "verify rejected_sum:\n list[bool] values = list()\n bool total = list.sum[bool](view values)\n assert total\n", + "property rejected_sum:\n given flag: bool\n list[bool] values = list(flag)\n bool total = list.sum[bool](view values)\n assert total\n", + ] { + let source = format!("namespace app\n{body}"); + reject_source(&source); + fs::write(&path, source).unwrap(); + let failure = test_file(&path).expect_err("unsupported sum prevents suite execution"); + assert!( + failure.contains("E0300") && failure.contains(DOMAIN), + "{failure}" + ); + } +} + +#[test] +fn list_sum_numeric_aliases_preserve_generic_and_function_value_routes() { + let source = r#"namespace app +type Small = int8 +type Alias = Small +function total[T](values: list[T]) returns T: + return list.sum[T](view values) +function wrapped(values: list[Alias]) returns Alias: + return total(values) +function exercise() returns bool: + list[Alias] direct_values = list(127, 1) + list[Alias] inferred_values = list(127, 1) + Alias direct = list.sum[Alias](view direct_values) + Alias inferred = list.sum(view inferred_values) + list[Alias] piped_values = list(127, 1) + Alias piped = piped_values into total[Alias]() + function(list[Alias]) returns Alias callback = wrapped + Alias indirect = callback(list(127, 1)) + Alias expected = -128 + Alias zero = 0 + list[Alias] empty = list() + Alias empty_total = list.sum[Alias](view empty) + return direct == expected and inferred == expected and piped == expected and indirect == expected and empty_total == zero +verify numeric_alias_routes: + assert exercise() + bool baked = comptime exercise() + assert baked +property numeric_alias_trials: + given repeat: bool + assert exercise() + if repeat: + assert exercise() +"#; + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + for release in [false, true] { + let built = build_file_with_options(&path, BuildOptions { release }); + assert!(!built.has_errors, "{:?}", built.diagnostics); + } + let outcome = test_file(&path).expect("numeric alias routes"); + assert_eq!((outcome.total, outcome.passed, outcome.failed), (2, 2, 0)); + let primitives = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/list_sum_primitives.jett"); + let outcome = test_file(&primitives).expect("all primitive widths remain supported"); + assert_eq!((outcome.total, outcome.passed, outcome.failed), (2, 2, 0)); +} diff --git a/crates/jett_driver/tests/native_conformance.rs b/crates/jett_driver/tests/native_conformance.rs new file mode 100644 index 00000000..1a338061 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance.rs @@ -0,0 +1,8981 @@ +//! Run the complete native conformance gates on each supported host. +#![cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] + +use jett_common::FileId; +use jett_driver::native::{ + NativeLauncherBundle, build_host_executable, build_host_property_suite_executable, + build_host_verify_suite_executable, host_target, +}; +use jett_parser::ast::Item; +use jett_runtime::{clock, environment, graphics, random}; +use std::fs; +use std::io::Read; +use std::path::Path; +use std::process::{Child, Command, Stdio}; +use std::sync::OnceLock; +use std::time::{Duration, Instant}; + +#[path = "support/native_inventory_execution.rs"] +mod inventory_execution; + +#[path = "native_conformance/call_ownership.rs"] +mod call_ownership; + +#[path = "native_conformance/resource_report.rs"] +mod resource_report; + +#[path = "native_conformance/resource_cases.rs"] +mod resource_cases; + +#[path = "native_conformance/resource_execution.rs"] +mod resource_execution; + +#[path = "native_conformance/scoped_call_views.rs"] +mod scoped_call_views; + +#[path = "native_conformance/local_view_aliases.rs"] +mod local_view_aliases; + +#[path = "native_conformance/erased_same_owner_arguments.rs"] +mod erased_same_owner_arguments; + +#[path = "native_conformance/pending_graphics.rs"] +mod pending_graphics; + +#[path = "native_conformance/graphics_authority.rs"] +mod graphics_authority; + +#[path = "native_conformance/display_results.rs"] +mod display_results; + +#[path = "native_conformance/equality_results.rs"] +mod equality_results; + +#[path = "native_conformance/actor_identity.rs"] +mod actor_identity; + +#[path = "native_conformance/interface_equality.rs"] +mod interface_equality; + +#[path = "native_conformance/secret_struct_constructors.rs"] +mod secret_struct_constructors; + +#[path = "native_conformance/secret_fields.rs"] +mod secret_fields; + +#[path = "native_conformance/secret_bitfield_machine_constructors.rs"] +mod secret_bitfield_machine_constructors; + +#[path = "native_conformance/secret_validating_struct_constructors.rs"] +mod secret_validating_struct_constructors; + +#[path = "native_conformance/qualified_result_tasks.rs"] +mod qualified_result_tasks; + +#[path = "native_conformance/refinement_reuse.rs"] +mod refinement_reuse; + +#[path = "native_conformance/refinement_calls.rs"] +mod refinement_calls; + +#[path = "native_conformance/refinement_returns.rs"] +mod refinement_returns; + +#[path = "native_conformance/inline_refinement_returns.rs"] +mod inline_refinement_returns; + +#[path = "native_conformance/refinement_sum.rs"] +mod refinement_sum; + +#[path = "native_conformance/reflected_producers.rs"] +mod reflected_producers; + +#[path = "native_conformance/reflected_callables.rs"] +mod reflected_callables; + +#[path = "native_conformance/measured_launcher.rs"] +mod measured_launcher; + +#[path = "native_conformance/suite_options.rs"] +mod suite_options; + +#[path = "native_conformance/reflected_nested_producers.rs"] +mod reflected_nested_producers; + +#[path = "native_conformance/debug_event_transport.rs"] +mod debug_event_transport; + +#[path = "native_conformance/hidden_secret_print.rs"] +mod hidden_secret_print; + +#[path = "native_conformance/pending_sum_diagnostics.rs"] +mod pending_sum_diagnostics; + +#[path = "native_conformance/json_qualified_machines.rs"] +mod json_qualified_machines; + +#[path = "native_conformance/json_inferred_slots.rs"] +mod json_inferred_slots; + +#[path = "native_conformance/json_inferred_parse_slots.rs"] +mod json_inferred_parse_slots; + +#[path = "native_conformance/bitfield_local_views.rs"] +mod bitfield_local_views; + +#[path = "native_conformance/secret_owner_local_views.rs"] +mod secret_owner_local_views; + +#[path = "native_conformance/uninhabited_callbacks.rs"] +mod uninhabited_callbacks; + +#[path = "native_conformance/latent_callbacks.rs"] +mod latent_callbacks; + +#[path = "native_conformance/mutable_borrowed_sums.rs"] +mod mutable_borrowed_sums; +#[path = "native_conformance/ordinary_borrowed_sums.rs"] +mod ordinary_borrowed_sums; + +#[path = "native_conformance/ordinary_borrowed_sums_additional.rs"] +mod ordinary_borrowed_sums_additional; + +#[path = "native_conformance/field_updates.rs"] +mod field_updates; + +struct Launcher { + bundle: NativeLauncherBundle, + _directory: tempfile::TempDir, +} + +// Literal fixture assertions retain their operation-specific bytes. Native +// stderr comparisons always use the complete ordered event stream below. +fn debug_print_text(events: &[jett_driver::DebugEvent]) -> String { + events + .iter() + .filter(|event| { + matches!( + event.kind, + jett_driver::DebugEventKind::Print | jett_driver::DebugEventKind::Println + ) + }) + .map(|event| event.text.as_str()) + .collect() +} + +fn debug_trace_lines(events: &[jett_driver::DebugEvent]) -> Vec { + events + .iter() + .filter(|event| { + matches!( + event.kind, + jett_driver::DebugEventKind::Trace | jett_driver::DebugEventKind::Breakpoint + ) + }) + .map(|event| { + event + .text + .strip_suffix('\n') + .unwrap_or(&event.text) + .to_owned() + }) + .collect() +} + +fn launcher() -> &'static NativeLauncherBundle { + if let Some(bundle) = measured_launcher::supplied(!cfg!(debug_assertions)) { + return bundle; + } + static LAUNCHER: OnceLock = OnceLock::new(); + &LAUNCHER + .get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + let cargo_profile = if profile == "debug" { "test" } else { profile }; + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let status = Command::new(env!("CARGO")) + .args(["build", "-q", "-p", "jett_native_launcher"]) + .args([ + "--target", + &host, + "--profile", + cargo_profile, + "--target-dir", + ]) + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build host- and profile-matched launcher"); + assert!(status.success(), "launcher build failed: {status}"); + let archive_name = if cfg!(windows) { + "jett_native_launcher.lib" + } else { + "libjett_native_launcher.a" + }; + let archive = target.join(&host).join(profile).join(archive_name); + let directory = tempfile::tempdir().expect("launcher bundle directory"); + let copied = directory.path().join(archive_name); + std::fs::copy(&archive, &copied).unwrap_or_else(|error| { + panic!( + "cannot copy launcher archive {}: {error}", + archive.display() + ) + }); + Launcher { + bundle: if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(copied) + } else { + NativeLauncherBundle::linux_gnu_v1(copied) + }, + _directory: directory, + } + }) + .bundle +} + +struct ExecutionChild(Child); + +impl Drop for ExecutionChild { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +fn run_bounded(executable: &Path, directory: &Path) -> std::process::Output { + run_bounded_with_env(executable, directory, None) +} + +fn run_bounded_with_env( + executable: &Path, + directory: &Path, + scripted_provider: Option<(&str, &str)>, +) -> std::process::Output { + run_bounded_with_environment( + executable, + directory, + scripted_provider.as_slice(), + scripted_provider, + ) +} + +fn run_bounded_with_environment( + executable: &Path, + directory: &Path, + environment: &[(&str, &str)], + scripted_provider: Option<(&str, &str)>, +) -> std::process::Output { + // File-backed output also bounds the wait if a child process inherits a handle. + let mut stdout = tempfile::NamedTempFile::new().unwrap(); + let mut stderr = tempfile::NamedTempFile::new().unwrap(); + let mut command = Command::new(executable); + command + .current_dir(directory) + .env_clear() + .stdin(Stdio::null()) + .stdout(stdout.reopen().unwrap()) + .stderr(stderr.reopen().unwrap()); + for (name, value) in environment { + command.env(name, value); + } + let mut child = ExecutionChild(command.spawn().expect("execute native artifact")); + let start = Instant::now(); + let status = loop { + if let Some(status) = child.0.try_wait().expect("poll native artifact") { + break status; + } + assert!( + start.elapsed() < Duration::from_secs(60), + "native executable exceeded 60 second deadline" + ); + std::thread::sleep(Duration::from_millis(10)); + }; + let snapshot = |file: &mut std::fs::File| { + let mut bytes = Vec::new(); + file.take(file.metadata().unwrap().len()) + .read_to_end(&mut bytes) + .unwrap(); + bytes + }; + let actual = std::process::Output { + status, + stdout: snapshot(stdout.as_file_mut()), + stderr: snapshot(stderr.as_file_mut()), + }; + retain_resource_native_evidence(executable, directory, scripted_provider, &actual); + actual +} + +fn retain_resource_native_evidence( + executable: &Path, + directory: &Path, + scripted_provider: Option<(&str, &str)>, + actual: &std::process::Output, +) { + let Some(root) = std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1") else { + return; + }; + let root = std::path::PathBuf::from(root); + assert!(root.is_absolute()); + fs::create_dir_all(&root).unwrap(); + let destination = root.join(directory.file_name().unwrap()); + fs::create_dir(&destination).unwrap(); + let mut files = Vec::new(); + for path in [ + executable.to_path_buf(), + directory.join("resource.obj"), + directory.join("resource-native-report-v1.bin"), + directory.join("resource-native-host-abi-control-v2.bin"), + ] { + if path.is_file() { + let metadata = path.metadata().unwrap(); + assert!(metadata.len() <= 256 * 1024 * 1024); + let saved = destination.join(path.file_name().unwrap()); + fs::copy(&path, &saved).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(&fs::read(&saved).unwrap()); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": metadata.len()})); + } + } + for (name, bytes) in [ + ("stdout.bin", &actual.stdout), + ("stderr.bin", &actual.stderr), + ] { + let saved = destination.join(name); + fs::write(&saved, bytes).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(bytes); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": bytes.len()})); + } + if let Some((name, script)) = scripted_provider { + let saved = destination.join("provider-script.txt"); + fs::write(&saved, script).unwrap(); + let hash = jett_runtime::crypto::sha256_digest(script.as_bytes()); + let hash = hash + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + files.push(serde_json::json!({"path": saved, "sha256": hash, "bytes": script.len(), "environment_key": name})); + } + let receipt = serde_json::json!({"exit_code": actual.status.code(), "files": files, "captured_before_assertions_and_teardown": true}); + fs::write( + destination.join("receipt.json"), + serde_json::to_vec_pretty(&receipt).unwrap(), + ) + .unwrap(); +} + +#[test] +fn native_comptime_reflection_callbacks_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_reflection_callbacks.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("reflection callback oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "list:int64\nalias:int64\ntext:int64\ninteger:int64\nint64:int64\nstring:int64\nlist[int64]:int64\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_reflection_callbacks.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native reflection callbacks"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_reflected_callbacks_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_reflected_callbacks.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("reflected loop callback oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + concat!( + "plain\nnumber:int64\nplain\ntext:string\nplain\nrepeated:int64\n", + "plain\nnumber:int64\nplain\ntext:string\nplain\nrepeated:int64\n", + "int64:bool\nstring:bool\nint64:bool\n", + ) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_reflected_callbacks.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native reflected callbacks"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_integer_wrapping_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/reflected_integer_wrapping.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("scoped integer wrapping oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + concat!( + "-128,128,-128\n-128,128,-128\n-128,128,-128\n-128,128,-128\n", + "0,0\n0,0\n-128\n128\n-128\n-128\n128\n-128\n0\n0\n0\n0\n", + ) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("reflected_integer_wrapping.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native scoped integer wrapping"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_alias_callbacks_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/reflected_alias_callbacks.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("reflected alias callback oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + concat!( + "alias:Label\nprimitive:string\nalias:Other\nalias:Label\nlist:list[Label]\nlist:list[string]\n", + "alias:Label\nprimitive:string\nalias:Other\nalias:Label\nlist:list[Label]\nlist:list[string]\n", + "alias:Label\nalias:Label\nalias:Label,primitive:string\nalias:Label,primitive:string\n", + "primitive:string\nprimitive:string\n", + "primitive:string,primitive:string,primitive:string,alias:Label,primitive:string\n", + "primitive:string,primitive:string,primitive:string,alias:Label,primitive:string\n", + "-128\n-128\n", + ) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("reflected_alias_callbacks.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native reflected alias callbacks"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_closure_reflection_guards_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/closure_reflection_guards.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("closure reflection guard oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "0\n2\n0\n3\ntype:int64:0\ntype:list[int64]:2\n2\n3\n4\n0\n0\n0\n2\n0\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("closure_reflection_guards.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native closure reflection guards"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_generic_reflection_expressions_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_reflection_expressions.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("generic reflection oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "type:int64\n\nlive:int64\nbaked:string\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("generic_reflection_expressions.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native generic reflection"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_generic_integer_wrapping_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_integer_wrapping.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("generic wrapping oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "-128\n128\n0\n0\n-128\n128\n-128\n128\n-128\n128\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("generic_integer_wrapping.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native generic wrapping"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_primitive_list_sums_match_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/list_sum_primitives.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("list_sum.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("primitive list sum oracle"); + let nonempty = "int8:-128;int16:-32768;int32:-2147483648;int64:-9223372036854775808;uint8:1;uint16:1;uint32:1;uint64:1;float32:0;float64:1;\n"; + let empty = + "int8:0;int16:0;int32:0;int64:0;uint8:0;uint16:0;uint32:0;uint64:0;float32:0;float64:0;\n"; + assert_eq!( + expected.stdout, + format!("{nonempty}{nonempty}{empty}{empty}") + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("list_sum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native primitive list sum program"); + binaries.push(binary); + } + let verify_binary = directory.path().join("list_sum_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native primitive list sum verify suite"); + let property_binary = directory.path().join("list_sum_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native primitive list sum property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_primitive_list_sums_reject_pending_in_both_profiles() { + for (ty, first, second) in [ + ("int8", "2", "3"), + ("uint64", "2", "3"), + ("float32", "2.0", "3.0"), + ("float64", "2.0", "3.0"), + ] { + for (name, initializer, message) in [ + ( + "first", + format!("list(run run {first}, {second})"), + "list.__sum: list elements must be int64 or float64", + ), + ( + "later", + format!("list({first}, run run {second})"), + "list.__sum: mixed types", + ), + ( + "outer", + format!("run run list({first}, {second})"), + "list.__sum: argument must be a list", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_list_sum.jett"); + fs::write( + &source, + format!( + "namespace app\nfunction main(stdout: Stdout) returns nothing:\n list[{ty}] values = {initializer}\n Stdout.write(view stdout, \"before\\n\")\n {ty} total = list.sum[{ty}](view values)\n trace total\n Stdout.write(view stdout, \"after\\n\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("pending primitive list sum oracle"); + assert_eq!(expected.output.stdout, "before\n", "{ty}/{name}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{ty}/{name}" + ); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{ty}/{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("pending_sum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native pending primitive list sum program"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{ty}/{name}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{ty}/{name}" + ); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{ty}/{name}: {actual:?}" + ); + } + } + } +} + +#[test] +fn native_list_sort_refinements_and_ordering_match_interpreter_in_both_profiles() { + let refined_stdout = concat!( + "narrow:-128,-128,0,127,|narrow:-128\n", + "unsigned:2,9223372036854775808,18446744073709551615,18446744073709551615,\n", + "decimal:-2.25,0,16777216,16777216,\n", + "integer:-9223372036854775808,-9223372036854775808,2,9223372036854775807,\n", + "word:apple,apple,eclair,zebra,|source:4\n", + "flag:false,false,true,true,\n", + "nested:-3,-3,2,|nested:-3\n", + "alias:-2,3,3,\n", + "empty:0:0;single:5:solo\n", + "refined:false:true\n", + "baked:narrow:-128:127:apple:zebra:false\n", + "pending:zebra:apple\nready:apple:zebra\n", + "plain-pending:zebra:apple\nplain-ready:apple:zebra;sorted:true:false:true\n", + ); + let ordering_stdout = concat!( + "int8:false:2,1,\nuint8:false:2,1,\nfloat32:false:2,1,\nuint64:false:2,1,\n", + "f32:nan:false:false:true:true;infinity:true:false;sort:true:true\n", + "f64:nan:false:false:true:true;infinity:true:false;sort:true:true\n", + "index-string:first:second:zebra\nindex-scalar:11:22:2\nindex-row:first:second:zebra\n", + ); + let refined_debug = [ + "trace head: app.Word = pending(pending(zebra))", + "trace once: app.Word = pending(zebra)", + "trace joined: app.Word = zebra", + "trace head: string = pending(zebra)", + "trace joined: string = zebra", + ]; + let ordering_debug = [ + "trace ordered: list[list[string]] = list(list(pending(pending(zebra)), first), list(apple, second))", + "trace ordered: list[list[int64]] = list(list(pending(2), 11), list(1, 22))", + "trace ordered: list[list[string]] = list(pending(pending(list(zebra, first))), list(apple, second))", + ]; + for (name, stdout, debug) in [ + ( + "list_sort_refinements", + refined_stdout, + refined_debug.as_slice(), + ), + ( + "list_ordering_controls", + ordering_stdout, + ordering_debug.as_slice(), + ), + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("list ordering oracle"); + assert_eq!(expected.stdout, stdout, "{name}"); + assert_eq!(debug_trace_lines(&expected.debug_events), debug, "{name}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("ordering_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("ordering_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native list ordering verify suite"); + let property_binary = directory.path().join("ordering_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native list ordering property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!( + actual.status.success(), + "{name}, release={release}: {actual:?}" + ); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{name}: {actual:?}"); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } + } +} + +#[test] +fn native_pending_refined_list_sort_rejects_outer_pending_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/list_sort_refinements.jett"); + let template = fs::read_to_string(fixture).unwrap(); + let (declarations, _) = template.rsplit_once("function main(").unwrap(); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_list_sort.jett"); + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n pending_outer_failure(view stdout)\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before\n"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + assert_eq!( + expected.message, + "runtime error: list.__sort expects a list argument" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("pending_sort_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native pending refined list sort"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_math_aggregate_primitives_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/math_aggregate_primitives.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("math_aggregate_primitives.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("primitive math oracle"); + let reports = [ + "int8:-0.5:-0.5", + "int16:-0.5:-0.5", + "int32:-0.5:-0.5", + "int64:0:0", + "uint8:127.5:127.5", + "uint16:32767.5:32767.5", + "uint32:2147483647.5:2147483647.5", + "uint64:13835058055282164000:13835058055282164000", + "float32:5592405.333333333:16777216", + "float64:5592405.666666667:16777216", + ]; + let mut stdout = String::new(); + for context in ["runtime", "comptime"] { + for report in reports { + stdout.push_str(&format!("{context}:{report}\n")); + } + } + stdout.push_str(concat!( + "ieee32:nan:true:true;infinity:inf:inf;opposite:true:true\n", + "ieee64:nan:true:true;infinity:inf:inf;opposite:true:true\n", + "maximum:true:true;residual:true\n", + "baked:ieee32:nan:true:true;infinity:inf:inf;opposite:true:true\n", + )); + assert_eq!(expected.stdout, stdout); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("math_aggregate_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native primitive math aggregate program"); + binaries.push(binary); + } + let verify_binary = directory.path().join("math_aggregate_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native primitive math aggregate verify suite"); + let property_binary = directory.path().join("math_aggregate_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native primitive math aggregate property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_math_aggregate_primitives_reject_empty_and_pending_in_both_profiles() { + let mut cases = Vec::new(); + for operation in ["average", "median"] { + for ty in [ + "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", + "float64", + ] { + cases.push(( + format!("{operation}_{ty}_empty"), + operation, + ty, + "list()".to_owned(), + format!("math.{operation}: list is empty"), + )); + } + } + for (name, operation, ty, initializer, message) in [ + ( + "average_first", + "average", + "int8", + "list(run run 2, 3)", + "math.average expects a list of numeric values", + ), + ( + "average_later", + "average", + "float32", + "list(2.0, run run 3.0)", + "math.average expects a list of numeric values", + ), + ( + "average_outer", + "average", + "uint64", + "run run list(2, 3)", + "math.__average expects a list of numbers", + ), + ( + "median_first", + "median", + "uint64", + "list(run run 2, 3)", + "math.median expects a list of numeric values", + ), + ( + "median_later", + "median", + "int16", + "list(2, run run 3)", + "math.median expects a list of numeric values", + ), + ( + "median_outer", + "median", + "float32", + "run run list(2.0, 3.0)", + "math.__median expects a list of numbers", + ), + ] { + cases.push(( + name.to_owned(), + operation, + ty, + initializer.to_owned(), + message.to_owned(), + )); + } + for (name, operation, ty, initializer, message) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("math_aggregate_failure.jett"); + fs::write( + &source, + format!( + "namespace app\nfunction aggregate[T](values: list[T]) returns float64:\n return math.{operation}[T](values)\nfunction main(stdout: Stdout) returns nothing:\n list[{ty}] values = {initializer}\n Stdout.write(view stdout, \"before\\n\")\n float64 value = aggregate[{ty}](values)\n trace value\n Stdout.write(view stdout, \"after\\n\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before\n", "{name}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{name}" + ); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("math_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}: {actual:?}" + ); + } + } +} + +#[test] +fn native_empty_collection_iteration_matches_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/empty_collection_iteration.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("empty_collection_iteration.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("empty iteration oracle"); + assert_eq!( + expected.stdout, + concat!( + "direct:done\nsource\nargument\neffects:done\n", + "generic:0:0:live:3\ndead:7:7\n", + "nested:2:0:borrowed:2:0\n", + "maps:0:context:0:0:0:0:0\nmapped:0:joined:0\n", + ) + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("empty_iteration_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native empty collection iteration program"); + binaries.push(binary); + } + let verify_binary = directory.path().join("empty_iteration_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native empty collection iteration verify suite"); + let property_binary = directory.path().join("empty_iteration_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native empty collection iteration property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_empty_collection_iteration_rejects_pending_in_both_profiles() { + for (collection, binding) in [("list()", "value"), ("map()", "key, value")] { + for comptime in [false, true] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_empty_iteration.jett"); + let initializer = if comptime { + format!("comptime run run {collection}") + } else { + format!("run run {collection}") + }; + fs::write( + &source, + format!( + "namespace app\nfunction main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n for {binding} in {initializer}:\n trace value\n Stdout.write(view stdout, \"unexpected body\\n\")\n Stdout.write(view stdout, \"after\\n\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before\n"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + assert_eq!( + expected.message, + "runtime error: for loop requires a list, string, map, or set value" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("pending_empty_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| { + panic!("{collection}, comptime={comptime}, release={release}: {error}") + }); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{actual:?}" + ); + } + } + } +} + +#[test] +fn empty_collection_iteration_still_rejects_invalid_dead_bodies() { + for (body, code, message) in [ + ( + "int64 copied = value + 1", + 301, + "cannot apply `+` to `` and `int64`", + ), + ( + "string rendered = \"{value}\"", + 332, + "type `` does not implement interface `Displayable`", + ), + ( + "missing_function(value)", + 200, + "undefined name: `missing_function`", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("invalid_dead_body.jett"); + let text = format!( + "namespace app\nfunction main() returns nothing:\n for value in list():\n {body}\n return nothing\n" + ); + fs::write(&source, &text).unwrap(); + for checked in [ + jett_driver::build_source(&text, "invalid_dead_body.jett"), + jett_driver::build_file(&source), + ] { + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), code); + assert_eq!(errors[0].message, message); + } + } +} + +#[test] +fn native_uninhabited_sum_arms_match_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/uninhabited_sum_arms.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("uninhabited_sum_arms.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("uninhabited sum oracle"); + assert_eq!( + expected.stdout, + concat!( + "handlers:0:0:0\nreuse:0:0:2:0:default:0:0\n", + "transforms:0:0:0:0:0\noptional source\neffect optional:0\n", + "result source\neffect failure:0\ncontext:0:0:0:callbacks:0:0:0\n", + "baked:0:0:0:0:0:2:0:0:0:0:0:0\npayloads:1:1\n", + "surviving:37:49\n", + "mixed-sums:7,:7,:8,:8,\n", + "mixed-nested:9,:9,:10,:10,\n", + "mixed-callbacks:7,:7,\n", + "mixed-rows:added:1:0;added:3:2;|added:3:2;added:1:0;\n", + "mixed-row-callbacks:added:1:0;added:3:2;|added:3:2;added:1:0;\n", + "boundary:local=7:0;view=7:0;call=7:0;nested=added:1:1:0|added:1:1:0;second=7,:|added:3:2;|added:1:0;|added:3:2;|added:1:0;\n", + "reflection:outer=list[int64]:list[int64];literal=list[int64]:list[int64];payload=app.Count;root=int64;aliases=app.Count:app.OtherCount:app.Count:app.OtherCount\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace error: string = bad", + "trace item: int64 = 8", + "trace error: string = reuse", + "trace error: string = reuse", + "trace item: int64 = 8", + "trace item: int64 = 8", + "trace item: int64 = 8", + "trace item: int64 = 8", + "trace error: string = default", + "trace error: string = absent", + "trace error: string = concrete", + "trace item: app.Count = 8", + "trace item: app.Named = 8", + "trace item: function() returns int64 = function(app.callback)", + "trace item: int64 = pending(pending(6))", + "trace once: int64 = pending(6)", + "trace ready: int64 = 6", + "trace item: int64 = pending(pending(8))", + "trace once: int64 = pending(8)", + "trace ready: int64 = 8", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("uninhabited_sum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native uninhabited sum program"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("uninhabited_sum_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native uninhabited sum verify suite"); + let property_binary = directory.path().join("uninhabited_sum_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native uninhabited sum property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + let suite_debug = "trace error: string = bad\ntrace item: int64 = 8\n"; + for (suite, repetitions) in [(verify_binary, 1), (property_binary, 100)] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, suite_debug.repeat(repetitions).as_bytes()); + } +} + +#[test] +fn native_uninhabited_sum_arms_and_reverse_reject_pending_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/uninhabited_sum_arms.jett"); + let template = fs::read_to_string(&fixture).unwrap(); + let declarations = template.split("function main(").next().unwrap(); + for (name, operation, value, message) in [ + ( + "optional", + "unwrap_optional", + "none", + "handle block requires a result or optional value, got pending(pending(none))", + ), + ( + "failure", + "unwrap_result", + "fail(\"bad\")", + "handle block requires a result or optional value, got pending(pending(fail(bad)))", + ), + ( + "success", + "collect_errors", + "ok(8)", + "handle block requires a result or optional value, got pending(pending(ok(8)))", + ), + ( + "reverse", + "list.reverse", + "list()", + "list.__length expects a list argument", + ), + ] { + for comptime in [false, true] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_uninhabited_sum.jett"); + let initializer = if comptime { + format!("comptime run run {value}") + } else { + format!("run run {value}") + }; + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n int64 size = list.length(view {operation}({initializer}))\n Stdout.write(view stdout, \"after:{{size}}\\n\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before\n", "{name}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{name}" + ); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("pending_sum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| { + panic!("{name}, comptime={comptime}, release={release}: {error}") + }); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}: {actual:?}" + ); + } + } + } +} + +#[test] +fn uninhabited_sum_handlers_still_reject_invalid_arms() { + for (declaration, argument) in [ + ( + "function invalid[T](candidate: optional[T]) returns list[T]:\n T item = candidate handle: return list()\n int64 copied = item + 1\n return list(item)\n", + "none", + ), + ( + "function invalid[T, E](candidate: result[T, E]) returns list[E]:\n T item = candidate handle error:\n int64 copied = error + 1\n return list(error)\n return list()\n", + "ok(8)", + ), + ] { + let text = format!( + "namespace app\n{declaration}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"{{list.length(view invalid({argument}))}}\")\n" + ); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("invalid_uninhabited_arm.jett"); + fs::write(&source, &text).unwrap(); + for checked in [ + jett_driver::build_source(&text, "invalid_uninhabited_arm.jett"), + jett_driver::build_file(&source), + ] { + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 301); + assert_eq!( + errors[0].message, + "cannot apply `+` to `` and `int64`" + ); + } + } +} + +#[test] +fn native_builds_reject_manufactured_never_values_before_publication() { + let default_forge = "function forge[T](values: list[T]) returns list[T]:\n T item = none handle: default 7\n return list(item)\n"; + for (name, declaration, namespace_value, invocation, code) in [ + ( + "default", + default_forge, + "", + "int64 size = list.length(view forge(list()))", + 300, + ), + ( + "local", + "function forge[T](values: list[T]) returns list[T]:\n T item = 7\n return list(item)\n", + "", + "int64 size = list.length(view forge(list()))", + 311, + ), + ( + "return", + "function forge[T](values: list[T]) returns T:\n return 7\nfunction invoke[T](values: list[T]) returns list[T]:\n T item = forge[T](values)\n return list(item)\n", + "", + "int64 size = list.length(view invoke(list()))", + 305, + ), + ( + "append", + "function forge[T](values: list[T]) returns list[T]:\n return list.append[T](values, 7)\n", + "", + "int64 size = list.length(view forge(list()))", + 300, + ), + ( + "list_return", + "function forge[T](values: list[T]) returns list[T]:\n return list(7)\n", + "", + "int64 size = list.length(view forge(list()))", + 300, + ), + ( + "namespace_comptime", + default_forge, + "int64 forged = comptime list.length(view forge(list()))\n", + "trace forged", + 300, + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("manufactured_never.jett"); + let function_name = format!("manufacture_{name}"); + let declaration = declaration.replace("forge[", &format!("{function_name}[")); + let namespace_value = namespace_value.replace("forge(", &format!("{function_name}(")); + let invocation = invocation.replace("forge(", &format!("{function_name}(")); + let text = format!( + "namespace app\n{declaration}{namespace_value}function main(stdout: Stdout) returns nothing:\n {invocation}\n Stdout.write(view stdout, \"must not execute\\n\")\nverify valid_check:\n assert true\nproperty valid_trials:\n given value: int64\n assert value == value\n" + ); + fs::write(&source, &text).unwrap(); + let check_diagnostics = |checked: jett_driver::BuildResult| { + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(checked.has_errors, "{name}"); + assert_eq!(errors.len(), 1, "{name}: {errors:?}"); + assert_eq!(errors[0].code.code(), code, "{name}: {errors:?}"); + assert!(errors[0].message.contains(""), "{name}: {errors:?}"); + assert!(errors[0].message.contains("int64"), "{name}: {errors:?}"); + }; + check_diagnostics(jett_driver::build_source(&text, "manufactured_never.jett")); + check_diagnostics(jett_driver::build_file(&source)); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("invalid Never values must prevent reference execution"); + assert!(expected.output.stdout.is_empty(), "{name}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{name}" + ); + assert!( + expected.message.contains(&format!("E{code:04}")), + "{name}: {}", + expected.message + ); + + let binary = directory.path().join("preserved.exe"); + let sentinel = b"existing native output"; + fs::write(&binary, sentinel).unwrap(); + // A checked source error must precede opening the launcher archive. + let unused_launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let check_native = |error: jett_driver::native::NativeBuildError| { + let jett_driver::native::NativeBuildError::Lowering { + source: lowering, .. + } = error + else { + panic!("{name}: expected frontend rejection, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(checked) = *lowering else { + panic!("{name}: expected preserved diagnostics before HIR"); + }; + check_diagnostics(checked); + assert_eq!(fs::read(&binary).unwrap(), sentinel, "{name}"); + }; + for release in [false, true] { + check_native( + jett_driver::native::build_host_executable_with_options( + &source, + &unused_launcher, + &binary, + jett_driver::BuildOptions { release }, + ) + .expect_err("invalid Never values must prevent native program publication"), + ); + } + check_native( + build_host_verify_suite_executable(&source, &unused_launcher, &binary) + .expect_err("invalid Never values must prevent native verify publication"), + ); + check_native( + build_host_property_suite_executable(&source, &unused_launcher, &binary) + .expect_err("invalid Never values must prevent native property publication"), + ); + } +} + +#[test] +fn native_generic_lexical_type_scope_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_lexical_type_scope.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("lexical type scope oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "T\nT\nT\nT\nT\nint64\nstring\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("generic_lexical_type_scope.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native lexical type scope"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_generic_interface_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_interface_identity.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("generic interface oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "integer:17\ntext:hello\nsecret\ninteger:23\nsecret\ninteger:31\ninteger:31\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("generic_interface_identity.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("native generic interface identity"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_interface_refined_interfaces_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_interfaces.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .unwrap_or_else(|error| panic!("refined interface oracle: {error:?}")); + assert_eq!( + debug_print_text(&expected.debug_events), + "selected:text:plain\nselected:small:7\nselected:record:kept\nagain:selected:text:plain\ntext:plain\nselected:text:plain\nselected:result-ok:9\nselected:result-fail:bad\nselected:optional-none\nselected:text:plain\nselected:small:7\nselected:record:kept\nagain:selected:text:plain\ntext:plain\nselected:text:plain\nselected:result-ok:9\nselected:result-fail:bad\nselected:optional-none\nselected:text:direct\nselected:text:direct\nselected:text:direct\nselected:text:direct\nselected:text:direct\nselected:text:direct\nselected:text:callback\nselected:text:callback\nselected:text:direct\nrefinement type constraint failed for 'app.Selected'\nrefinement type constraint failed for 'app.Selected'\nselected:text:fresh\nselected:text:fresh\nrefinement type constraint failed for 'app.Rejected'\n" + ); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(debug.contains("[redacted]")); + assert!(!debug.contains("hidden-refined-interface")); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_refined_interfaces.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native refined interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(actual.stderr, debug.as_bytes()); +} + +#[test] +fn native_interface_method_returns_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_method_returns.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("interface method return oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "number:7\nnumber:7\nnumber:8\nnumber:7\ntext:hello\ntext:hello\ntext:hello!\ntext:hello\ntiny:127\ntiny:127\ntiny:-128\ntiny:127\nnumber:13\ntext:baked\ntiny:127\nnumber:13\ntext:baked\ntiny:127\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_method_returns.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native interface method returns"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_interface_display_contexts_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_display_contexts.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("interface display context oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "erased:number:7\nconcrete:Ada\nerased:record:Ada\nconcrete:Ada\nerased:number:7\nerased:record:Ada\nfield:erased:record:Ada\ncall:erased:record:returned\nevaluated\nonce:erased:record:returned\nerased:record:returned\nerased:record:returned\nscoped:erased:record:returned\nnumber:7 Ada\nenabled\ndisabled\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_display_contexts.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native interface display"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_interface_display_failure_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_display_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("pending interface display must fail"); + assert_eq!(debug_print_text(&expected.output.debug_events), "prefix\n"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + assert_eq!( + expected.message, + "runtime error: undefined function 'app.Named.name'" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_display_failure.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native interface display failure"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_enum_struct_equality_matches_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/enum_struct_equality.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("enum_structs.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("explicit payload equality oracle"); + assert_eq!( + expected.stdout, + "true true true true true true true true true true\n" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("enum_structs_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native explicit payload methods"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_enum_struct_equality_failure_cleans_cursor_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/enum_struct_equality_failure.jett"); + let original = fs::read_to_string(fixture).unwrap().replace("\r\n", "\n"); + let pending = original.replace("string rejected = string.repeat(\"ab\", 9223372036854775807)\n return string.char_count(rejected) == 0", "return run true"); + assert_ne!(pending, original); + for (original, message) in [ + ( + &original, + "runtime error: string.repeat: requested output is too large", + ), + (&pending, "runtime error: Equatable.equals must return bool"), + ] { + for operator in ["==", "!="] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("enum_failure.jett"); + fs::write( + &source, + original.replace("\"left\\n\") ==", &format!("\"left\\n\") {operator}")), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "true\nleft\nright\n"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + assert_eq!(expected.message, message); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("enum_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native failing payload method"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); + } + } + } +} + +#[test] +fn native_interface_refined_small_values_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_small_values.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("small_values.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("refined small-value interface oracle"); + assert_eq!( + expected.stdout, + "nothing\nunit\nagain\nbool:false\nenabled:true\nbytes:6f6b\ndata:6f6b\nnothing\nunit\nagain\nbool:false\nenabled:true\nbytes:6f6b\ndata:6f6b\nunit\nunit\n6f6b:6f6b:7368:7368:7368:2:data:6f6b:2:2\n6f6b:6f6b:7368:7368:7368:2:data:6f6b:2:2\n" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("small_values_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native refined small-value interfaces"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_transparent_borrow_argument_failure_cleans_owners_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_small_values.jett"); + let original = fs::read_to_string(fixture).unwrap(); + let (declarations, _) = original.split_once("function main(").unwrap(); + for callee in ["sized", "callback"] { + for borrowed in ["view coarsen packet.data", "view declassify packet.hidden"] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("borrow_failure.jett"); + fs::write(&source, format!("{declarations}function failed() returns int64:\n string rejected = string.repeat(\"ab\", 9223372036854775807)\n return string.char_count(rejected)\nfunction main(stdout: Stdout) returns nothing:\n Packet packet = Packet(data: bytes.from_string(\"ok\"), hidden: bytes.from_string(\"sh\")) handle error:\n Stdout.write(view stdout, error)\n return nothing\n function(view bytes, int64) returns string callback = sized\n Stdout.write(view stdout, {callee}({borrowed}, failed()))\n")).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert!(expected.output.stdout.is_empty(), "{expected:?}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{expected:?}" + ); + assert_eq!( + expected.message, + "runtime error: string.repeat: requested output is too large" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("borrow_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native failure following a transparent borrow"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(71), + "{callee}({borrowed}): {actual:?}" + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); + } + } + } +} + +#[test] +fn native_interface_refined_actors_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_actors.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("refined actor interface oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "counter selected again\n3\n3\nselected 5\ncounter\nselected\nagain\nselected\n7\n7\nselected\nrefinement type constraint failed for 'app.Rejected'\n3 3\n" + ); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace copied: app.Holder = app.Holder(worker: pending(pending(actor#0)))", + "trace later: app.Named = pending(pending(actor#0))", + ] + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_refined_actors.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native refined actors"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(actual.stderr, debug.as_bytes()); +} + +#[test] +fn native_refinement_declaration_contexts_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refinement_declaration_contexts.jett"); + let expected = jett_driver::run_file_capture_output(&fixture) + .expect("refinement declaration context oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "selected:text:value\nselected:text:value\nagain:selected:text:value\ndecoy:alias\nstring list[string] 7 false\n7\nbigger:7:string\nrejected:int8:list[int8]:refinement type constraint failed for 'domain.Positive'\nfalse\nbool list[bool] 2 false\n2\nbigger-rejected:bool:list[bool]:refinement type constraint failed for 'domain.Bigger'\nfalse\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("refinement_declaration_contexts.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("native refinement declaration contexts"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_contextual_comptime_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/contextual_comptime_values.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("contextual comptime oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "int64:primitive\nstring:primitive\napp.Label:alias\nint8\napp.Label\nint8\nstring\napp.Label\n0\n0\nint8\nlist[int8]\napp.Label\nlist[app.Label]\nstring\nlist[string]\nint8\napp.Label\nint8\nstring\nint8\noff\napp.T\napp.T\napp.T\nint16\nstring:primitive\napp.Label:alias\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("contextual_comptime_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native contextual comptime"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_opaque_fields_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/reflected_opaque_fields.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("reflected opaque field oracle"); + assert_eq!(expected.stdout, "ready\nready\n"); + assert_eq!( + debug_print_text(&expected.debug_events), + "plain:5\npending:5\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("reflected_opaque_fields.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native reflected opaque fields"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_comptime_actor_computation_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_actor_computation.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("comptime actor computation oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "live:5\nbaked:5\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_actor_computation.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("native comptime actor computation"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_indirect_owned_results_match_interpreter_in_both_profiles() { + for (fixture_name, stdout, failure) in [ + ( + "indirect_owned_results", + "true true true\n4 4 4 4 4 4 4\n", + None, + ), + ( + "indirect_owned_result_failure", + "1;2;3;", + Some("runtime error: string.repeat: requested output is too large"), + ), + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../tests/native/{fixture_name}.jett")); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{fixture_name}.jett")); + fs::copy(&fixture, &source).unwrap(); + let interpreted = jett_driver::run_file_capture_outcome(&source); + if let Some(message) = failure { + let expected = interpreted.unwrap_err(); + assert_eq!(expected.message, message); + assert_eq!(expected.output.stdout, stdout); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + } else { + let expected = interpreted.unwrap(); + assert_eq!(expected.stdout, stdout); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + } + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("{fixture_name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native owned indirect results"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(if failure.is_some() { 71 } else { 0 }), + "{actual:?}" + ); + assert_eq!(actual.stdout, stdout.as_bytes()); + let stderr = failure + .map(|message| format!("{message}\n")) + .unwrap_or_default(); + assert_eq!(actual.stderr, stderr.as_bytes(), "{actual:?}"); + } + } +} + +#[test] +fn native_global_constants_match_interpreter_without_source_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/global_constants.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("constants.jett"); + fs::copy(fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("constant oracle"); + let line = "-128:18446744073709551615:3.140000104904175:hello:42:true:true:43\n"; + assert_eq!( + expected.stdout, + format!("{}hello:42:42:42\n", line.repeat(3)) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace minimum: app.Count = -128", + "trace label: string = hello:42", + "trace unit: nothing = nothing", + ] + ); + let debug = jett_driver::render_debug_events(&expected.debug_events); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("constants_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native constants"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("constants_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native constant verify suite"); + let property_binary = directory.path().join("constants_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native constant property suite"); + fs::remove_file(source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + if release { + b"".as_slice() + } else { + debug.as_bytes() + } + ); + } + let verified = run_bounded(&verify_binary, directory.path()); + assert!(verified.status.success(), "{verified:?}"); + assert!(verified.stdout.is_empty(), "{verified:?}"); + assert_eq!( + String::from_utf8_lossy(&verified.stderr), + "trace answer: int64 = 42\nbreakpoint hit\n" + ); + let property = run_bounded(&property_binary, directory.path()); + assert!(property.status.success(), "{property:?}"); + assert!(property.stdout.is_empty(), "{property:?}"); + assert_eq!( + String::from_utf8_lossy(&property.stderr), + "trace greeting: string = hello\n".repeat(100) + ); +} + +#[test] +fn native_sibling_namespace_constants_stay_distinct_without_source() { + let directory = tempfile::tempdir().unwrap(); + let sources = directory.path().join("src"); + fs::create_dir(&sources).unwrap(); + let manifest = directory.path().join("jett.proj"); + fs::write( + &manifest, + "name: sibling_constants\nversion: 0.1.0\nentry: src/main.jett\n", + ) + .unwrap(); + let alpha = sources.join("00_alpha.jett"); + let bravo = sources.join("10_bravo.jett"); + // Equal-width namespace and value spellings put the constant declarations + // at identical byte offsets. Their file identities must keep them distinct. + for (path, namespace, answer) in [(&alpha, "alpha", 11), (&bravo, "bravo", 29)] { + fs::write( + path, + format!( + "namespace {namespace}\nint64 answer = {answer}\nexport function read() returns int64:\n return answer\n" + ), + ) + .unwrap(); + } + let source = sources.join("main.jett"); + fs::write( + &source, + r#"namespace app +int64 answer = 80 +function combined() returns int64: + use alpha + use bravo + return alpha.read() + bravo.read() + answer +function main(stdout: Stdout) returns nothing: + use alpha + use bravo + Stdout.write(view stdout, "{alpha.read()}:{bravo.read()}:{answer}:{combined()}\n") +verify namespace_constants: + use alpha + use bravo + assert alpha.read() == 11 + assert bravo.read() == 29 + assert answer == 80 + assert combined() == 120 +property namespace_constant_reads: + given value: int64 + use alpha + use bravo + assert alpha.read() == 11 + assert bravo.read() == 29 + assert answer == 80 + assert value + combined() - combined() == value +"#, + ) + .unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("namespace constant oracle"); + assert_eq!(expected.stdout, "11:29:80:120\n"); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("namespace_constants_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native sibling namespace constants"); + binaries.push((binary, expected.stdout.as_bytes().to_vec())); + } + let verify_binary = directory.path().join("namespace_constants_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native sibling namespace constant verify suite"); + binaries.push((verify_binary, Vec::new())); + let property_binary = directory.path().join("namespace_constants_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native sibling namespace constant property suite"); + binaries.push((property_binary, Vec::new())); + for path in [&source, &alpha, &bravo, &manifest] { + fs::remove_file(path).unwrap(); + } + for (binary, stdout) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, stdout, "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_wrapped_enum_equality_matches_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/wrapped_enum_equality.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("wrapped_enum.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("wrapped equality oracle"); + assert_eq!( + expected.stdout, + "true true true true true true true true true true true true\ntrue true true true true true\n" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("wrapped_enum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native wrapped enum equality"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_pending_sum_handles_fail_before_extraction_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_sum_handle_failure.jett"); + let template = fs::read_to_string(fixture).unwrap(); + for (ty, value, payload, rendered, handle) in [ + ( + "optional[string]", + "some(\"payload\")", + "string", + "some(payload)", + "handle:", + ), + ("optional[string]", "none", "string", "none", "handle:"), + ( + "result[string, int64]", + "ok(\"payload\")", + "string", + "ok(payload)", + "handle error:", + ), + ( + "result[string, int64]", + "fail(17)", + "string", + "fail(17)", + "handle error:", + ), + ( + "optional[Record]", + "some(Record(label: \"payload\"))", + "Record", + "some(app.Record(label: payload))", + "handle:", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_sum.jett"); + let text = template + .replace("optional[string]", ty) + .replace("some(\"payload\")", value) + .replace("string found", &format!("{payload} found")) + .replace("handle:", handle); + fs::write(&source, text).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before;"); + assert_eq!( + expected.message, + format!( + "runtime error: handle block requires a result or optional value, got pending(pending({rendered}))" + ) + ); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("pending_sum_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native pending sum handle"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{actual:?}" + ); + } + } +} + +#[test] +fn native_pending_aggregate_access_fails_before_observation_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_aggregate_access.jett"); + let template = fs::read_to_string(&fixture).unwrap(); + let (declarations, _) = template.rsplit_once("function main(").unwrap(); + for (scenario, message) in [ + ( + "direct_record_failure", + "field access is not supported on pending(app.Record(label: Ada, items: list(7)))", + ), + ( + "nested_record_failure", + "field access is not supported on pending(pending(app.Record(label: Ada, items: list(7))))", + ), + ( + "cloned_record_failure", + "field access is not supported on pending(pending(app.Record(label: Ada, items: list(7))))", + ), + ( + "machine_field_failure", + "field access is not supported on pending(pending(app.Session@active(Ada, 7)))", + ), + ( + "machine_state_failure", + "'at' requires a machine value, got pending(pending(app.Session@active(Ada, 7)))", + ), + ( + "broad_machine_state_failure", + "'at' requires a machine value, got pending(pending(app.Session@active(Ada, 7)))", + ), + ( + "machine_transition_failure", + "app.Session.transition: first argument must be a machine value", + ), + ( + "enum_match_failure", + "match requires an enum value, got pending(pending(app.Choice.rows(list(7))))", + ), + ( + "enum_payload_failure", + "unsupported binary operation: pending(7) Add 1", + ), + ( + "projected_iteration_failure", + "field access is not supported on pending(pending(app.Record(label: Ada, items: list(7))))", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_aggregate.jett"); + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n {scenario}(view stdout)\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.output.stdout, "before\n", "{scenario}"); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{scenario}" + ); + let debug = if scenario == "enum_payload_failure" { + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + [ + "trace value: int64 = pending(pending(7))", + "trace joined: int64 = pending(7)", + ], + "{scenario}" + ); + jett_driver::render_debug_events(&expected.output.debug_events) + } else { + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{scenario}: {expected:?}" + ); + String::new() + }; + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("pending_aggregate_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{scenario}, release={release}: {error}")); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{scenario}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{scenario}" + ); + let debug = if release { "" } else { debug.as_str() }; + assert_eq!( + actual.stderr, + format!("{debug}{}\n", expected.message).as_bytes(), + "{scenario}, release={release}: {actual:?}" + ); + } + } +} + +#[test] +fn native_ready_and_joined_aggregate_access_preserves_owners_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_aggregate_access.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("ready_aggregate.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("ready aggregate oracle"); + assert_eq!( + expected.stdout, + "record:Ada:1\nrecord:Ada:1\nrecord:Ada:1\n7\ntrue:Ada:7\ntrue:Ada:7\n1:1\n1:1\n7\nint8:8\nfloat32:1.5\nbool:false\nnothing joined\n" + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace value: int8 = pending(pending(7))", + "trace once: int8 = pending(7)", + "trace ready: int8 = 7", + "trace value: float32 = pending(pending(1.25))", + "trace once: float32 = pending(1.25)", + "trace ready: float32 = 1.25", + "trace value: bool = pending(pending(true))", + "trace once: bool = pending(true)", + "trace ready: bool = true", + "trace value: nothing = pending(pending(nothing))", + "trace once: nothing = pending(nothing)", + "trace ready: nothing = nothing", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("ready_aggregate_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native ready and joined aggregate control"); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } +} + +#[test] +fn native_projected_reads_and_reconstructed_owners_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/projected_read_controls.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("projected_read_controls.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).expect("projected read oracle"); + assert_eq!( + expected.stdout, + "original:1\ncopied:2\noriginal:1\nloop:3\nmatch:2:2\ntemporary:1\nfields:7:1\n" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("projected_read_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native projected read control"); + binaries.push(binary); + } + let verify_binary = directory.path().join("projected_read_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native projected read verify suite"); + let property_binary = directory.path().join("projected_read_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native projected read property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_interface_refined_sums_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_sums.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("refined_sums.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("refined interface sums oracle"); + let values = "choice:small:7;again:choice:small:7;outcome:text;outcome-fail:kept;choice:small:7;outcome:text;\n"; + assert_eq!( + expected.stdout, + format!( + "{values}{values}choice rejected\nchoice rejected\noutcome rejected\noutcome rejected\nchoice:small:11;choice:small:11;\nchoice:small:11\nabsent rejected\nabsent rejected\npending rejected\nchoice:small:11\n17:true\n17:true\n" + ) + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("refined_sums_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native refined interface sums"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_interface_math_facade_contexts_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_math_facade_contexts.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("math_facades.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("math facade context oracle"); + let primitives = "integer:3;fraction:1.25;integer:1;fraction:1.25;integer:2;fraction:2.5;\n"; + let hidden = "hidden-integer;hidden-fraction;hidden-integer;hidden-fraction;hidden-integer;hidden-fraction;\n"; + let callbacks = "integer:3;fraction:1.25;hidden-integer;hidden-fraction;\n"; + assert_eq!( + expected.stdout, + format!("{primitives}{hidden}{callbacks}{primitives}{hidden}{callbacks}{callbacks}") + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("math_facades_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native math facade contexts"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_interface_facade_results_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_facade_results.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("facade result oracle"); + let typed = "small:ok\nsmall:fail:expected int8 in range -128..127\nwide:ok\nwide:fail:expected int64, got null\nfloat32:ok\nfloat32:fail:expected float64, got bool\npositive:ok\npositive:fail:refinement type constraint failed for 'app.Positive'\nrecord:ok\nrecord:fail:count: expected int8 in range -128..127\nrecord:fail:unknown field 'extra' for app.Record\n"; + let raw = "tree:fail:unterminated JSON array\nwide:fail:expected int64, got string\ntree:fail:missing array item 0\nmaybe-tree:ok\nmaybe-tree:fail:expected object, got array\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!("{typed}{typed}{raw}{raw}") + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-facade-result")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_facade_results.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native facade results"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_refined_containers_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_refined_containers.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("refined container oracle"); + let mixed = "mapping:1\nmembers:1\nsome:9\nnone\nok:11\nfail:problem\ncallback:5\ncallback:7\ncallback:7\nfunction:5\n"; + assert_eq!(debug_print_text(&expected.debug_events), mixed.repeat(2)); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_refined_containers.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native refined containers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_reflected_fields_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_reflected_fields.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("reflected interface oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "small:7\nsmall:7\nwide:9\nsecret\n".repeat(9) + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-reflected-interface")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_reflected_fields.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native reflected interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_opaque_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_opaque_identity.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("opaque interface oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "first\n5\nfirst\n5\nfirst\n7\n2\nfirst\nsecond\nfirst\nchecked-builder\nchecked-builder\nrecord:7\nrecord:9\nbuilder\nchecked-builder\nrecord:9\nchecked-builder\nrecord:9\nchecked-builder\nchecked-builder\nbuilder\nchecked-builder\nbuilder\noutput\noutput\noutput\noutput\nstdout\n" + ); + assert_eq!(expected.stdout, "ready"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_opaque_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native opaque interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_interface_facade_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_facade_identity.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("facade interface oracle"); + let mixed = "small:7\nwide:7\nfloat32:1.5\nfloat64:1.5\nsmall-list:2\nwide-list:2\npositive:9\nsmall-box:11\nsecret-box\nevent:13\nheader:15\nsession:17\nactive:19\n"; + let math = "wide:3\nfloat64:3.5\nwide:5\nfloat64:6\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!("{mixed}{mixed}small:21\nsmall:23\n{math}{math}") + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-json-interface")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_facade_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native facade interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_nominal_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_nominal_identity.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("nominal interface oracle"); + let mixed = "active:7\nsession:7\nempty-state\nrefined-session:7\nidle\nevent:normal\ntagged:chosen\nheader:2\nnonzero-header:3\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!( + "{mixed}{mixed}empty-state\nactive:9\nsession:9\nactive:9\nactive:7\nactive:7\nactive:7\nactive:7\n" + ) + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-nominal-secret")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_nominal_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native nominal interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_function_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_function_identity.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("function interface oracle"); + let mixed = "reader:7\nreader:11\nwords:hello\nitems:3\nborrowed\nowned\nnested:8\nsmall:17\nreader:19\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!( + "{mixed}{mixed}reader:11\n11\n11\nreader:11\nreader:11\nreader:11\nbroad:13\nnarrow:13\nnarrow:13\ncalled:13\ncalled:13\nnarrow:13\nlabel-factory:made\nnamed-factory:made\nnamed-factory:made\nnamed-factory:made\nmade\n" + ) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_function_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native function interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_collection_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_collection_identity.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("collection interface oracle"); + let mixed = "numbers:2\nwords:2\nsecrets:1\nnonempty:3\nnumber-map:1\nword-map:2\nnumber-set:1\nword-set:1\nmaybe-number:7\nmaybe-word:hello\nno-number\nno-word\nnumber-ok:9\nword-ok:yes\nnumber-fail:bad\nword-fail:11\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!( + "{mixed}{mixed}numbers:2\nnumbers:2\nnumbers:2\nnumbers:2\nnumbers:2\ninterfaces:3\nnumbers:1\nnumbers:2\nwords:1\ninterfaces:3\ncounted:4\ncounted:4\nnumbers:3\nnumbers:3\nnumbers:4\nnumbers:4\n6\n2\n9\n17\n" + ) + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-collection-secret")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_collection_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native collection interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_primitive_identity_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_primitive_identity.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("primitive interface oracle"); + let mixed = "int8:7\nint64:7\nuint8:9\nuint64:9\nfloat32\nfloat64\nint16:15\nint32:31\nuint16:16\nuint32:32\npositive:7\nnonempty:yes\nstring:yes\n"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!( + "{mixed}{mixed}int8:7\nint8:7\npositive:7\nint8:8\nint8:8\nint8:7\nint8:7\nint8:11\nint8:11\nint8:11\nint8:11\nint8:12\nint8:13\nint8:7\nint8:7\n" + ) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_primitive_identity.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native primitive interfaces"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/interface_values.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("interface interpreter oracle"); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-interface-token") + || line.contains("hidden-generic-interface") + || line.contains("reflection-failed")) + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("visible-generic-interface")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native interface values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_interface_pending_failure_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_pending_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("a pending receiver has no concrete dispatch target"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("interface_pending_failure.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native interface failure"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + ); +} + +#[test] +fn native_verify_suite_executes_all_checked_bodies() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/verify_test.jett"); + let directory = tempfile::tempdir().expect("verify executable directory"); + let executable = directory.path().join("verify_test.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &executable) + .expect("link native verify suite"); + assert_eq!(artifact.path, executable); + let output = run_bounded(&artifact.path, directory.path()); + assert!(output.status.success(), "native verify failed: {output:?}"); + assert!( + output.stdout.is_empty(), + "verify emitted stdout: {output:?}" + ); + assert!( + output.stderr.is_empty(), + "verify emitted stderr: {output:?}" + ); +} + +#[test] +fn native_suites_reject_failing_source_assertions() { + for (kind, given) in [("verify", ""), ("property", " given n: int64\n")] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("failure.jett"); + fs::write( + &source, + format!( + "{kind} first:\n{given} assert true\n\n{kind} second:\n{given} assert false \"native suite sentinel\"\n" + ), + ).unwrap(); + let oracle = jett_driver::test_file(&source).expect("checked failing suite"); + assert_eq!((oracle.passed, oracle.failed), (1, 1)); + let binary = directory.path().join("failure.exe"); + let build = if kind == "property" { + build_host_property_suite_executable + } else { + build_host_verify_suite_executable + }; + let error = + build(&source, launcher(), &binary).expect_err("reject failed source assertion"); + assert!( + error.to_string().contains("native suite sentinel"), + "{error}" + ); + assert!( + !binary.exists(), + "a failed suite must not publish an artifact" + ); + } +} + +#[test] +fn native_builds_reject_mutable_globals_before_publication() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compile_fail/mutable_global.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("mutable_global.jett"); + fs::copy(fixture, &source).unwrap(); + let reference = jett_driver::run_file_capture_output(&source).unwrap_err(); + assert!(reference.contains("E0377"), "{reference}"); + for release in [false, true] { + let binary = directory.path().join("preserved.exe"); + fs::write(&binary, b"existing output").unwrap(); + let error = jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_err(); + let jett_driver::native::NativeBuildError::Lowering { + source: lowering, .. + } = error + else { + panic!("expected frontend rejection, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(checked) = *lowering else { + panic!("expected diagnostics before HIR"); + }; + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 377) + ); + assert_eq!(fs::read(&binary).unwrap(), b"existing output"); + } +} + +#[test] +fn native_builds_reject_direct_collection_equality_before_publication() { + for ty in [ + "bytes", + "list[int64]", + "map[string, int64]", + "set[int64]", + "optional[int64]", + "result[int64, string]", + ] { + for op in ["==", "!="] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("comparison.jett"); + fs::write(&source, format!("namespace app\nfunction compare(view left: {ty}, view right: {ty}) returns bool:\n return left {op} right\nfunction main() returns nothing:\n return nothing\n")).unwrap(); + for release in [false, true] { + let binary = directory.path().join("preserved.exe"); + fs::write(&binary, b"existing output").unwrap(); + let error = jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_err(); + let jett_driver::native::NativeBuildError::Lowering { + source: lowering, .. + } = error + else { + panic!("{ty} {op}: expected frontend rejection, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(checked) = *lowering else { + panic!("{ty} {op}: expected diagnostics before HIR"); + }; + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{ty} {op}: {errors:?}"); + assert_eq!(errors[0].code.code(), 376); + assert_eq!( + errors[0].message, + format!( + "operator `{op}` is not defined for `{ty}`; compare its contents explicitly" + ) + ); + assert_eq!(fs::read(&binary).unwrap(), b"existing output"); + } + } + } +} + +#[test] +fn native_builds_preserve_shrunk_property_diagnostics_and_existing_artifacts() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("property_failure.jett"); + let text = r#"namespace app +function main() returns nothing: + return nothing +verify first: + assert true +property first_property: + given n: int8 + assert n == n +property shrunk_failure: + given number: int8 + given items: list[int64] + assert number < 2 "native property sentinel" +"#; + fs::write(&source, text).unwrap(); + let tested = jett_driver::test_file(&source).expect("checked property failure"); + assert_eq!((tested.total, tested.passed, tested.failed), (3, 2, 1)); + let failure = tested.blocks.iter().find(|block| !block.passed).unwrap(); + assert_eq!(failure.name, "shrunk_failure"); + assert!(failure.is_property); + assert_eq!(failure.iterations, Some(4)); + assert_eq!(failure.line, 9); + assert_eq!( + failure.error.as_deref(), + Some("native property sentinel (counterexample: number = 2, items = list())") + ); + + let checked = jett_driver::build_file(&source); + assert!(checked.has_errors); + let diagnostics = |result: &jett_driver::BuildResult| { + result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .map(|diagnostic| { + ( + diagnostic.code.code(), + diagnostic.span, + diagnostic.message.clone(), + ) + }) + .collect::>() + }; + let expected = diagnostics(&checked); + assert_eq!(expected.len(), 1); + assert_eq!(expected[0].0, 9000); + assert_eq!( + &text[expected[0].1.start as usize..expected[0].1.end as usize], + "shrunk_failure" + ); + assert_eq!( + expected[0].2, + format!( + "comptime verify failed in 'shrunk_failure': {}", + failure.error.as_ref().unwrap() + ) + ); + + for mode in ["program", "verify", "property"] { + let binary = directory.path().join(format!("{mode}.exe")); + fs::write(&binary, b"existing output sentinel").unwrap(); + let result = match mode { + "program" => build_host_executable(&source, launcher(), &binary), + "verify" => build_host_verify_suite_executable(&source, launcher(), &binary), + "property" => build_host_property_suite_executable(&source, launcher(), &binary), + _ => unreachable!(), + }; + let error = result.expect_err("a failed property must prevent native publication"); + let jett_driver::native::NativeBuildError::Lowering { + source: lowering, .. + } = error + else { + panic!("{mode}: expected frontend rejection, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(native) = *lowering else { + panic!("{mode}: expected property diagnostics before HIR"); + }; + assert_eq!(diagnostics(&native), expected, "{mode}"); + assert_eq!( + fs::read(&binary).unwrap(), + b"existing output sentinel", + "{mode}" + ); + } +} + +#[test] +fn native_opaque_capability_values_match_interpreter() { + for capability in ["Stderr", "Stdin", "Filesystem", "Network", "Process", "Log"] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("capability.jett"); + fs::write( + &source, + format!( + r#"namespace test +function observe(view value: {capability}) returns nothing: + trace value + return nothing +function main(authority: {capability}) returns nothing: + function(view {capability}) returns nothing callback = observe + callback(view authority) + {capability} moved = authority + {capability} pending = run moved + {capability} nested = run pending + trace nested + {capability} inner = join nested handle error: + return nothing + {capability} joined = join inner handle error: + return nothing + observe(view joined) + {capability} cancelled = join joined handle error: + println(error) + return nothing + trace cancelled + return nothing +"# + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert_eq!( + debug_print_text(&expected.debug_events), + "task was cancelled\n" + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + format!("trace value: {capability} = nothing"), + format!("trace nested: {capability} = pending(pending(nothing))"), + format!("trace value: {capability} = nothing"), + ] + ); + let binary = directory.path().join("capability.exe"); + build_host_executable(&source, launcher(), &binary) + .unwrap_or_else(|error| panic!("{capability}: {error}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{capability}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{capability}"); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events), + "{capability}" + ); + } +} + +#[test] +fn native_bitfield_constructor_checks_dynamic_widths() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/bitfield_roundtrip.jett"); + let directory = tempfile::tempdir().expect("bitfield verify directory"); + let executable = directory.path().join("bitfield_widths.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &executable) + .expect("link native bitfield verify suite"); + let output = run_bounded(&artifact.path, directory.path()); + assert!( + output.status.success(), + "native bitfield checks failed: {output:?}" + ); +} + +#[test] +fn native_property_suite_executes_deterministic_scalar_trials() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/namespace_runtime_verify_context.jett"); + let directory = tempfile::tempdir().expect("property executable directory"); + let executable = directory.path().join("scalar_property_suite.exe"); + let artifact = build_host_property_suite_executable(&fixture, launcher(), &executable) + .expect("link native scalar property suite"); + let output = run_bounded(&artifact.path, directory.path()); + assert!( + output.status.success(), + "native properties failed: {output:?}" + ); +} + +#[test] +fn native_property_suites_execute_for_all_run_pass_fixtures() { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass"); + let mut sources = fs::read_dir(&fixtures) + .expect("run-pass fixture directory") + .map(|entry| entry.expect("fixture entry").path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "jett") + }) + .collect::>(); + sources.sort(); + let mut attempted = 0; + let mut property_blocks = 0; + let mut passed = 0; + let mut failures = Vec::new(); + for source in sources { + let text = fs::read_to_string(&source).expect("fixture source"); + let parsed = jett_parser::parse(&text, FileId::new(0)); + let count = parsed + .module + .items + .iter() + .filter(|item| matches!(item, Item::Property(_))) + .count(); + if count == 0 { + continue; + } + attempted += 1; + property_blocks += count; + match jett_driver::native::run_host_property_suite( + &source, + launcher(), + jett_driver::native::NativePropertyOptions::default(), + ) { + Ok(result) => { + if result.failure.is_none() { + assert_eq!( + result.trials, + count * jett_comptime::verify::PROPERTY_DEFAULT_ITERATIONS + ); + passed += 1; + } else { + failures.push(format!("{}: {result:?}", source.display())); + } + } + Err(error) => failures.push(format!("{}: {error}", source.display())), + } + } + println!("native property suites: {passed}/{attempted}; {property_blocks} blocks"); + for failure in &failures { + println!("{failure}"); + } + assert_eq!(attempted, 3, "native property fixture denominator changed"); + assert_eq!( + property_blocks, 18, + "native property block denominator changed" + ); + assert_eq!(passed, attempted, "native property suite failures"); +} + +#[test] +fn native_verify_suite_keeps_inline_callbacks_inside_their_parent_body() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/json_public_secret_policy.jett"); + let directory = tempfile::tempdir().expect("verify executable directory"); + let executable = directory.path().join("json_public_secret_policy.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &executable) + .expect("link native verify suite with inline callbacks"); + let output = run_bounded(&artifact.path, directory.path()); + assert!(output.status.success(), "native verify failed: {output:?}"); +} + +#[test] +fn native_verify_suites_execute_for_all_run_pass_fixtures() { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass"); + let mut sources = fs::read_dir(&fixtures) + .expect("run-pass fixture directory") + .map(|entry| entry.expect("fixture entry").path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "jett") + }) + .collect::>(); + sources.sort(); + let directory = tempfile::tempdir().expect("native verify audit directory"); + let mut attempted = 0; + let mut passed = 0; + let mut failures = Vec::new(); + for source in sources { + let text = fs::read_to_string(&source).expect("fixture source"); + let parsed = jett_parser::parse(&text, FileId::new(0)); + if !parsed + .module + .items + .iter() + .any(|item| matches!(item, Item::Verify(_))) + { + continue; + } + attempted += 1; + let executable = directory.path().join(format!( + "{}.exe", + source.file_stem().expect("fixture stem").to_string_lossy() + )); + match build_host_verify_suite_executable(&source, launcher(), &executable) { + Ok(artifact) => { + let output = run_bounded(&artifact.path, directory.path()); + if output.status.success() { + passed += 1; + } else { + failures.push(format!("{}: {output:?}", source.display())); + } + fs::remove_file(&artifact.path).expect("remove finished verify executable"); + } + Err(error) => failures.push(format!("{}: {error}", source.display())), + } + } + println!("native verify suites: {passed}/{attempted}"); + for failure in &failures { + println!("{failure}"); + } + assert_eq!(attempted, 155, "native verify fixture denominator changed"); + assert_eq!(passed, attempted, "native verify suite failures"); +} + +#[test] +fn native_scripted_capabilities_match_interpreter() { + let clock_samples = vec![ + clock::ClockTestSample::Wall { + unix_seconds: 0, + subsecond_nanoseconds: 0, + }, + clock::ClockTestSample::Wall { + unix_seconds: 0, + subsecond_nanoseconds: 0, + }, + clock::ClockTestSample::Wall { + unix_seconds: -1, + subsecond_nanoseconds: 999_999_999, + }, + clock::ClockTestSample::Wall { + unix_seconds: 42, + subsecond_nanoseconds: 123_456_789, + }, + clock::ClockTestSample::Wall { + unix_seconds: 40, + subsecond_nanoseconds: 0, + }, + ]; + let clock_fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/clock_scripted.jett"); + let clock_expected = jett_driver::run_file_capture_outcome_with_clock_test_samples( + &clock_fixture, + clock_samples.clone(), + ) + .expect("scripted Clock interpreter oracle"); + let clock_script = clock::encode_test_script(&clock_samples); + assert_scripted_fixture( + &clock_fixture, + clock::TEST_SCRIPT_ENV, + &clock_script, + &clock_expected, + ); + + let random_samples = vec![ + random::RandomTestSample::Bounded(0), + random::RandomTestSample::Bounded(u64::MAX - 1), + random::RandomTestSample::Unit53(0), + random::RandomTestSample::Unit53((1_u64 << 53) - 1), + random::RandomTestSample::Boolean(false), + random::RandomTestSample::Boolean(true), + random::RandomTestSample::Bounded(0), + random::RandomTestSample::Bounded(2), + random::RandomTestSample::Bounded(0), + random::RandomTestSample::Bounded(1), + random::RandomTestSample::Bounded(0), + ]; + let random_fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/random_scripted.jett"); + let random_expected = jett_driver::run_file_capture_outcome_with_random_test_samples( + &random_fixture, + random_samples.clone(), + ) + .expect("scripted Random interpreter oracle"); + let random_script = random::encode_test_script(&random_samples); + assert_scripted_fixture( + &random_fixture, + random::TEST_SCRIPT_ENV, + &random_script, + &random_expected, + ); + + let text = |value: &str| environment::EnvironmentTestText::Unicode(value.to_owned()); + let environment_snapshot = environment::EnvironmentTestSnapshot { + arguments: vec![text("first"), text(""), text("third")], + entries: vec![ + environment::EnvironmentTestEntry { + name: text("PRESENT"), + value: text("value"), + }, + environment::EnvironmentTestEntry { + name: text("DUPLICATE"), + value: text("first"), + }, + environment::EnvironmentTestEntry { + name: text("DUPLICATE"), + value: text("second"), + }, + environment::EnvironmentTestEntry { + name: text("BROKEN"), + value: environment::EnvironmentTestText::InvalidUnicode, + }, + environment::EnvironmentTestEntry { + name: environment::EnvironmentTestText::InvalidUnicode, + value: text("ignored"), + }, + ], + }; + let environment_fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/environment_snapshot.jett"); + let environment_expected = + jett_driver::run_file_capture_outcome_with_environment_test_snapshot( + &environment_fixture, + environment_snapshot.clone(), + ) + .expect("injected Environment interpreter oracle"); + let environment_script = environment::encode_test_snapshot(&environment_snapshot); + assert_scripted_fixture( + &environment_fixture, + environment::TEST_SNAPSHOT_ENV, + &environment_script, + &environment_expected, + ); +} + +fn assert_scripted_fixture( + fixture: &Path, + env: &str, + script: &str, + expected: &jett_driver::RunOutput, +) { + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(fixture, launcher(), &binary).expect("compile scripted fixture"); + let actual = run_bounded_with_env(&binary, directory.path(), Some((env, script))); + assert!(actual.status.success(), "{}: {actual:?}", fixture.display()); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{}", + fixture.display() + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{}: {actual:?}", + fixture.display() + ); +} + +#[test] +fn native_success_rejects_unconsumed_scripted_provider_inputs() { + let directory = tempfile::tempdir().expect("isolated scripted provider fixture"); + let fixture = directory.path().join("unconsumed.jett"); + fs::write( + &fixture, + "namespace app\nfunction main(rng: Random, clock: Clock, display: Graphics) returns nothing:\n return nothing\n", + ) + .expect("write scripted provider fixture"); + let binary = directory.path().join("unconsumed.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile provider fixture"); + + let random_samples = vec![random::RandomTestSample::Boolean(true)]; + let clock_samples = vec![clock::ClockTestSample::Unavailable]; + let graphics_events = vec![graphics::TestEvent::Close]; + let cases = [ + ( + random::TEST_SCRIPT_ENV, + random::encode_test_script(&random_samples), + jett_driver::run_file_capture_outcome_with_random_test_samples( + &fixture, + random_samples, + ) + .expect_err("interpreter must reject unconsumed Random sample"), + ), + ( + clock::TEST_SCRIPT_ENV, + clock::encode_test_script(&clock_samples), + jett_driver::run_file_capture_outcome_with_clock_test_samples(&fixture, clock_samples) + .expect_err("interpreter must reject unconsumed Clock sample"), + ), + ( + graphics::TEST_SCRIPT_ENV, + graphics::encode_test_script(&graphics_events), + jett_driver::run_file_capture_outcome_with_graphics_test_events( + &fixture, + graphics_events, + ) + .expect_err("interpreter must reject unconsumed Graphics event"), + ), + ]; + for (environment, script, expected) in cases { + let actual = run_bounded_with_env(&binary, directory.path(), Some((environment, &script))); + assert_eq!(actual.status.code(), Some(71), "{environment}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{environment}" + ); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{environment}" + ); + } +} + +#[test] +fn native_scalar_stdout_and_owned_bytes_match_interpreter() { + for (name, relative_path) in [ + ( + "native_scalar_entry", + "../../tests/run_pass/native_scalar_entry.jett", + ), + ("hello_print", "../../tests/run_pass/hello_print.jett"), + ("bytes_ownership", "../../tests/native/bytes_ownership.jett"), + ("string_search", "../../tests/native/string_search.jett"), + ( + "string_intrinsics", + "../../tests/native/string_intrinsics.jett", + ), + ("unit_enum", "../../tests/native/unit_enum.jett"), + ("payload_enum", "../../tests/native/payload_enum.jett"), + ( + "payload_enum_equality", + "../../tests/native/payload_enum_equality.jett", + ), + ("bitfield_values", "../../tests/native/bitfield_values.jett"), + ( + "contextual_generic_empty_list", + "../../tests/native/contextual_generic_empty_list.jett", + ), + ("machine_values", "../../tests/native/machine_values.jett"), + ("uuid_values", "../../tests/native/uuid_values.jett"), + ("function_values", "../../tests/native/function_values.jett"), + ( + "captured_closure_values", + "../../tests/native/captured_closure_values.jett", + ), + ( + "function_descriptor_ownership", + "../../tests/native/function_descriptor_ownership.jett", + ), + ( + "inline_functions", + "../../tests/native/inline_functions.jett", + ), + ( + "list_source_values", + "../../tests/native/list_source_values.jett", + ), + ("list_sort", "../../tests/native/list_sort.jett"), + ( + "list_insert_remove", + "../../tests/native/list_insert_remove.jett", + ), + ("set_values", "../../tests/native/set_values.jett"), + ("map_values", "../../tests/native/map_values.jett"), + ( + "json_generic_raw_bridge", + "../../tests/native/json_generic_raw_bridge.jett", + ), + ( + "json_generic_primitives", + "../../tests/native/json_generic_primitives.jett", + ), + ( + "json_struct_serialize", + "../../tests/native/json_struct_serialize.jett", + ), + ( + "json_aggregate_parse", + "../../tests/native/json_aggregate_parse.jett", + ), + ( + "json_result_parse", + "../../tests/native/json_result_parse.jett", + ), + ("json_set_parse", "../../tests/native/json_set_parse.jett"), + ( + "json_struct_parse", + "../../tests/native/json_struct_parse.jett", + ), + ( + "json_machine_parse", + "../../tests/native/json_machine_parse.jett", + ), + ( + "json_machine_serialize", + "../../tests/native/json_machine_serialize.jett", + ), + ( + "json_secret_source", + "../../tests/native/json_secret_source.jett", + ), + ( + "json_enum_source", + "../../tests/native/json_enum_source.jett", + ), + ( + "json_enum_raw_source", + "../../tests/native/json_enum_raw_source.jett", + ), + ( + "json_pipeline_source", + "../../tests/native/json_pipeline_source.jett", + ), + ( + "json_refinement_source", + "../../tests/native/json_refinement_source.jett", + ), + ( + "json_refined_record_source", + "../../tests/native/json_refined_record_source.jett", + ), + ( + "json_bytes_raw_source", + "../../tests/native/json_bytes_raw_source.jett", + ), + ( + "json_bitfield_source", + "../../tests/native/json_bitfield_source.jett", + ), + ( + "json_generic_parse_primitives", + "../../tests/native/json_generic_parse_primitives.jett", + ), + ("encoding", "../../tests/native/encoding.jett"), + ("csv", "../../tests/native/csv.jett"), + ("secret_values", "../../tests/native/secret_values.jett"), + ( + "reflection_scalars", + "../../tests/native/reflection_scalars.jett", + ), + ("type_info", "../../tests/native/type_info.jett"), + ("type_fields", "../../tests/native/type_fields.jett"), + ( + "type_construction_struct", + "../../tests/native/type_construction_struct.jett", + ), + ( + "type_construction_bitfield", + "../../tests/native/type_construction_bitfield.jett", + ), + ( + "type_construction_enum", + "../../tests/native/type_construction_enum.jett", + ), + ( + "type_construction_machine", + "../../tests/native/type_construction_machine.jett", + ), + ("type_variants", "../../tests/native/type_variants.jett"), + ( + "reflected_type_dispatch", + "../../tests/native/reflected_type_dispatch.jett", + ), + ( + "projected_sequences", + "../../tests/native/projected_sequences.jett", + ), + ( + "bitfield_reflection", + "../../tests/native/bitfield_reflection.jett", + ), + ("crypto", "../../tests/native/crypto.jett"), + ("math_aggregate", "../../tests/native/math_aggregate.jett"), + ( + "clock_production", + "../../tests/native/clock_production.jett", + ), + ( + "string_scalar_iteration", + "../../tests/native/string_scalar_iteration.jett", + ), + ( + "nested_handle_view_call", + "../../tests/run_pass/uint64_checked_expression_runtime_main.jett", + ), + ( + "nested_handler_call", + "../../tests/native/nested_handler_call.jett", + ), + ( + "nested_handler_order", + "../../tests/native/nested_handler_order.jett", + ), + ( + "numeric_conversions", + "../../tests/native/numeric_conversions.jett", + ), + ( + "refinement_validation", + "../../tests/native/refinement_validation.jett", + ), + ( + "refinement_collections", + "../../tests/native/refinement_collections.jett", + ), + ( + "empty_never_list", + "../../tests/native/empty_never_list.jett", + ), + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path); + let expected = jett_driver::run_file_capture_output(&fixture) + .unwrap_or_else(|error| panic!("{name}: interpreter oracle: {error}")); + if name == "uuid_values" { + assert_eq!( + debug_print_text(&expected.debug_events), + "true true true true\n" + ); + } + if name == "function_values" { + assert_eq!(debug_print_text(&expected.debug_events), "8 14 4 3 z a b\n"); + } + if name == "function_descriptor_ownership" { + assert_eq!(debug_print_text(&expected.debug_events), "5 8\n10\n12\n"); + } + if name == "list_source_values" { + assert_eq!( + debug_print_text(&expected.debug_events), + "a true true true true 2 true true true\n" + ); + } + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .unwrap_or_else(|error| panic!("{name}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{name}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_list_invalid_indices_match_interpreter_errors() { + for name in ["list_insert_invalid_index", "list_remove_invalid_index"] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject an invalid list index"); + let directory = tempfile::tempdir().expect("isolated list failure directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile invalid list index fixture"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_actor_spawn_releases_state_with_context() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/actor_spawn.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "spawned\n"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile actor spawn"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_actor_messages_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/actor_messages.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "hits: 5\n"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile actor messages"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_graphics_authority_reaches_main() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/graphics_authority.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "graphics authority granted\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile Graphics entry"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_scripted_graphics_matches_interpreter() { + for (name, events) in [ + ( + "graphics_scripted.jett", + vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Key(graphics::Key::Left), + graphics::TestEvent::Close, + ], + ), + ( + "graphics_pipeline_scripted.jett", + vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + graphics::TestEvent::Key(graphics::Key::Left), + graphics::TestEvent::Close, + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ], + ), + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(name); + let expected = jett_driver::run_file_capture_output_with_graphics_test_events( + &fixture, + events.clone(), + ) + .expect("scripted Graphics interpreter oracle"); + let script = graphics::encode_test_script(&events); + assert_scripted_fixture(&fixture, graphics::TEST_SCRIPT_ENV, &script, &expected); + } +} + +#[test] +fn native_graphics_pending_scalar_state_matches_interpreter() { + for (name, events) in [ + ( + "graphics_pending_scalar_state.jett", + vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Key(graphics::Key::Left), + graphics::TestEvent::Close, + ], + ), + ( + "graphics_pending_scalar_state_types.jett", + vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + graphics::TestEvent::Key(graphics::Key::Left), + graphics::TestEvent::Close, + ], + ), + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native") + .join(name); + let expected = jett_driver::run_file_capture_output_with_graphics_test_events( + &fixture, + events.clone(), + ) + .expect("pending Graphics interpreter oracle"); + let script = graphics::encode_test_script(&events); + let directory = tempfile::tempdir().expect("isolated pending Graphics state directory"); + let binary = directory.path().join("graphics_pending_state.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending Graphics state"); + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((graphics::TEST_SCRIPT_ENV, &script)), + ); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_graphics_callback_runtime_error_is_terminal() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/graphics_callback_runtime_error.jett"); + // An unused event must not replace the callback's earlier runtime failure. + let events = vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ]; + let expected = + jett_driver::run_file_capture_outcome_with_graphics_test_events(&fixture, events.clone()) + .expect_err("interpreter callback must fail"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile Graphics callback fixture"); + let script = graphics::encode_test_script(&events); + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((graphics::TEST_SCRIPT_ENV, &script)), + ); + assert!(!actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + b"runtime error: string.repeat: requested output is too large\n" + ); +} + +#[test] +fn native_graphics_handled_failures_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/graphics_scripted.jett"); + let source = std::fs::read_to_string(&fixture).expect("read Graphics fixture"); + let cases = [ + ( + "config", + source.replacen("width: 32", "width: 0", 1), + vec![], + ), + ( + "scene", + source.replacen("red: state.moves", "red: 999", 1), + vec![], + ), + ( + "host", + source.clone(), + vec![graphics::TestEvent::HostError( + "injected graphics failure".into(), + )], + ), + ]; + for (name, source, events) in cases { + let directory = tempfile::tempdir().expect("isolated Graphics fixture"); + let path = directory.path().join(format!("{name}.jett")); + std::fs::write(&path, source).expect("write Graphics fixture"); + let expected = + jett_driver::run_file_capture_output_with_graphics_test_events(&path, events.clone()) + .expect("Graphics interpreter oracle"); + let script = graphics::encode_test_script(&events); + assert_scripted_fixture(&path, graphics::TEST_SCRIPT_ENV, &script, &expected); + } +} + +#[test] +fn native_actor_fixture_bodies_match_interpreter() { + for (fixture, main) in [ + ( + "actor_named_arguments.jett", + "function main() returns nothing:\n println(actor_results())\n", + ), + ( + "namespace_duplicate_leaf_actors.jett", + "function main() returns nothing:\n println(direct_duplicate_leaf_actors(), alias_duplicate_leaf_actors())\n", + ), + ( + "namespace_qualified_actors.jett", + "function main() returns nothing:\n println(direct_actor(), alias_actor())\n", + ), + ( + "numeric_literal_contexts.jett", + "function main() returns nothing:\n ByteCounter counter = spawn ByteCounter(seed: 4 + 5)\n send counter.add(10 + 11)\n println(ask counter.current)\n", + ), + ] { + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .expect("read actor fixture"); + let directory = tempfile::tempdir().expect("isolated actor execution directory"); + let source_path = directory.path().join(fixture); + std::fs::write(&source_path, format!("{source}\n{main}")) + .expect("write executable actor fixture"); + let expected = + jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{fixture}: {actual:?}" + ); + } +} + +#[test] +fn native_structured_concurrency_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/structured_concurrency.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "hello:ready:failed\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile structured concurrency"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_pending_nothing_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/structured_concurrency_nothing.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending nothing directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending nothing"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_string_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_string_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending string directory"); + let binary = directory.path().join("pending_string_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending string values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_bytes_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_bytes_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending bytes directory"); + let binary = directory.path().join("pending_bytes_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending bytes values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_list_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_list_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending list directory"); + let binary = directory.path().join("pending_list_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending list values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_list_intrinsics_match_interpreter_errors() { + for name in [ + "pending_list_length_container_failure", + "pending_list_append_container_failure", + "pending_list_get_container_failure", + "pending_list_get_index_failure", + "pending_list_insert_index_failure", + "pending_list_remove_index_failure", + "pending_list_sort_container_failure", + "pending_list_sort_index_failure", + "pending_list_is_sorted_container_failure", + "pending_list_swap_first_failure", + "pending_list_swap_second_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending list intrinsic operands"); + let directory = tempfile::tempdir().expect("isolated pending list intrinsic directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending list intrinsic"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_index_and_count_intrinsics_match_interpreter_errors() { + for name in [ + "pending_range_end_failure", + "pending_range_start_failure", + "pending_range_second_failure", + "pending_range_step_failure", + "pending_bytes_get_index_failure", + "pending_bytes_get_container_failure", + "pending_bytes_slice_start_failure", + "pending_bytes_slice_end_failure", + "pending_bytes_slice_container_failure", + "pending_string_slice_start_failure", + "pending_string_slice_end_failure", + "pending_string_slice_container_failure", + "pending_string_repeat_count_failure", + "pending_string_repeat_container_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending index or count"); + let directory = tempfile::tempdir().expect("isolated pending intrinsic directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending index or count intrinsic"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_math_intrinsics_match_interpreter_errors() { + for name in [ + "pending_math_abs_failure", + "pending_math_min_failure", + "pending_math_min_first_failure", + "pending_math_max_failure", + "pending_math_sqrt_failure", + "pending_math_floor_failure", + "pending_math_ceil_failure", + "pending_math_round_failure", + "pending_math_log_failure", + "pending_math_log2_failure", + "pending_math_log10_failure", + "pending_math_sin_failure", + "pending_math_cos_failure", + "pending_math_tan_failure", + "pending_math_pow_failure", + "pending_math_pow_exponent_failure", + "pending_math_clamp_failure", + "pending_math_clamp_upper_failure", + "pending_math_mod_failure", + "pending_math_gcd_failure", + "pending_math_lcm_failure", + "pending_math_factorial_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending math operands"); + let directory = tempfile::tempdir().expect("isolated pending math directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending math case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_conversion_intrinsics_match_interpreter_errors() { + for name in [ + "pending_float32_from_float64_failure", + "pending_float64_from_int64_failure", + "pending_int64_from_float64_failure", + "pending_string_from_int64_failure", + "pending_string_from_uint64_failure", + "pending_string_from_float64_failure", + "pending_string_from_bool_failure", + "pending_int64_from_string_failure", + "pending_uint64_from_string_failure", + "pending_float64_from_string_failure", + "pending_bytes_from_string_failure", + "pending_bytes_from_hex_failure", + "pending_bytes_to_string_failure", + "pending_bytes_to_hex_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending conversion operand"); + let directory = tempfile::tempdir().expect("isolated pending conversion directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending conversion case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_string_and_bytes_intrinsics_match_interpreter_errors() { + for name in [ + "pending_string_upper_failure", + "pending_string_split_delimiter_failure", + "pending_string_join_separator_failure", + "pending_string_join_element_failure", + "pending_string_join_list_failure", + "pending_string_replace_third_failure", + "pending_bytes_length_failure", + "pending_bytes_concat_first_failure", + "pending_bytes_concat_second_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending string or bytes operands"); + let directory = tempfile::tempdir().expect("isolated pending text/bytes directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending text/bytes case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_encoding_csv_and_crypto_intrinsics_match_interpreter() { + for name in [ + "pending_encoding_base64_encode_failure", + "pending_encoding_base64_decode_failure", + "pending_encoding_hex_decode_failure", + "pending_encoding_url_encode_failure", + "pending_encoding_url_decode_failure", + "pending_encoding_form_encode_failure", + "pending_encoding_form_decode_failure", + "pending_csv_parse_failure", + "pending_csv_parse_with_header_failure", + "pending_csv_stringify_list_failure", + "pending_csv_stringify_row_failure", + "pending_crypto_hmac_contextual_key_failure", + "pending_crypto_hmac_key_failure", + "pending_crypto_hmac_message_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending intrinsic operand"); + let directory = tempfile::tempdir().expect("isolated pending intrinsic directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending intrinsic case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } + + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_csv_stringify_field.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending CSV field directory"); + let binary = directory.path().join("pending_csv_stringify_field.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending CSV field case"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_secret_task_join_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_secret_task_join.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated secret task directory"); + let binary = directory.path().join("pending_secret_task_join.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile secret task case"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_pending_capability_intrinsics_match_interpreter() { + for name in [ + "pending_clock_now_failure", + "pending_random_float_failure", + "pending_random_bool_failure", + "pending_random_int_failure", + "pending_environment_args_failure", + "pending_environment_get_authority_failure", + "pending_environment_get_key_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending capability operand"); + let directory = tempfile::tempdir().expect("isolated pending capability directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending capability case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } + for name in ["pending_stdout_write", "pending_stdout_write_text"] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending stdout directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending stdout case"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_pending_secret_and_bitfield_intrinsics_match_interpreter() { + for name in [ + "pending_secret_compare_string_first_failure", + "pending_secret_compare_string_second_failure", + "pending_secret_compare_bytes_first_failure", + "pending_secret_compare_bytes_second_failure", + "pending_bitfield_to_bytes_failure", + "pending_bitfield_from_bytes_failure", + "pending_bitfield_payload_element_failure", + "pending_bitfield_payload_list_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending secret or bitfield operand"); + let directory = tempfile::tempdir().expect("isolated pending intrinsic directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending secret or bitfield case"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_secret_redact.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending secret directory"); + let binary = directory.path().join("pending_secret_redact.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending redaction case"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_pending_actor_scalar_state_matches_interpreter() { + for name in [ + "pending_actor_scalar_state", + "pending_actor_scalar_state_types", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending actor state directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending actor state"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_set_map_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_set_map_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending set and map directory"); + let binary = directory.path().join("pending_set_map_values.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending set and map values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_record_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_record_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending record directory"); + let binary = directory.path().join("pending_record_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending record values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_sum_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_sum_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending sum directory"); + let binary = directory.path().join("pending_sum_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending sum values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_function_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_function_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending function directory"); + let binary = directory.path().join("pending_function_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending function values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_function_call_matches_interpreter_error() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_function_call_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject a pending function call"); + let directory = tempfile::tempdir().expect("isolated pending function call directory"); + let binary = directory.path().join("pending_function_call_failure.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending function call failure"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_pending_type_construction_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_type_construction.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending builder directory"); + let binary = directory.path().join("pending_type_construction.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending builder"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_construction_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflected_construction.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated reflected construction directory"); + let binary = directory.path().join("pending_reflected_construction.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reflected construction"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_scalar_fields_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflected_scalar_fields.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated reflected field directory"); + let binary = directory.path().join("pending_reflected_scalar_fields.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reflected fields"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_owned_fields_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflected_owned_fields.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated reflected field directory"); + let binary = directory.path().join("pending_reflected_owned_fields.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reflected fields"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflected_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated reflected collection directory"); + let binary = directory.path().join("pending_reflected_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile reflected pending collections"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_sums_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflected_sums.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated reflected sum directory"); + let binary = directory.path().join("pending_reflected_sums.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reflected pending sums"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_contextual_empty_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_contextual_empty_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending empty collection directory"); + let binary = directory + .path() + .join("pending_contextual_empty_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending contextual empty collections"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflection_metadata_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_reflection_metadata.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending reflection directory"); + let binary = directory.path().join("pending_reflection_metadata.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending reflection metadata"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_owner_errors_match_interpreter() { + for name in [ + "pending_variant_value_failure", + "pending_machine_state_value_failure", + "pending_struct_field_owner_failure", + "pending_variant_field_owner_failure", + "pending_machine_field_owner_failure", + "pending_bitfield_field_owner_failure", + "pending_narrowed_machine_field_owner_failure", + "pending_variant_foreign_field_precedence", + "pending_machine_foreign_field_precedence", + "pending_variant_both_metadata_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending reflected owner"); + let directory = tempfile::tempdir().expect("isolated pending reflected owner directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending reflected owner"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_secret_debug_failure_messages_are_redacted() { + for name in [ + "secret_pending_struct_field_owner_failure", + "secret_pending_variant_value_failure", + "secret_pending_machine_state_value_failure", + "secret_pending_variant_field_owner_failure", + "secret_pending_machine_field_owner_failure", + "secret_pending_builder_put_failure", + "secret_pending_builder_finish_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("pending reflected value must fail"); + assert!( + expected.message.contains("[redacted]"), + "{name}: {}", + expected.message + ); + assert!( + !expected.message.contains("hidden-"), + "{name}: {}", + expected.message + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_json_nested_secrets_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_nested_secret_probe.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated nested secret JSON directory"); + let binary = directory.path().join("json_nested_secret_probe.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile nested secret JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_recursive_enum_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_recursive_enum_probe.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated recursive enum JSON directory"); + let binary = directory.path().join("json_recursive_enum_probe.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile recursive enum JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_json_inputs_match_interpreter_errors() { + for name in [ + "pending_json_parse_input_failure", + "pending_json_serialize_string_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending JSON input"); + let directory = tempfile::tempdir().expect("isolated pending JSON directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending JSON input"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_scalar_formatting_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_scalar_formatting.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending formatting directory"); + let binary = directory.path().join("pending_scalar_formatting.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending formatting"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_reflected_field_error_matches_interpreter() { + for name in [ + "pending_reflected_field_failure", + "pending_reflected_variant_field_failure", + "pending_reflected_machine_field_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending reflected field metadata"); + let directory = tempfile::tempdir().expect("isolated pending reflection failure directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending reflected field failure"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_builder_field_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_builder_field.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending builder field directory"); + let binary = directory.path().join("pending_builder_field.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending builder field"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_builder_member_metadata_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_builder_member_metadata.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending builder member directory"); + let binary = directory.path().join("pending_builder_member_metadata.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending builder member metadata"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_reflected_foreign_field_errors_match_interpreter() { + for name in [ + "reflected_struct_foreign_field_failure", + "reflected_struct_foreign_incompatible_failure", + "reflected_enum_foreign_field_failure", + "reflected_machine_foreign_field_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject foreign reflected field"); + let directory = tempfile::tempdir().expect("isolated reflected field directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile foreign reflected field"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_reflected_requested_type_errors_match_interpreter() { + for name in [ + "reflected_struct_requested_type_failure", + "reflected_enum_requested_type_failure", + "reflected_machine_requested_type_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject requested reflected field type"); + let directory = tempfile::tempdir().expect("isolated reflected type directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile requested reflected field type"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_reflected_missing_candidate_owner_errors_match_interpreter() { + for name in [ + "reflected_struct_missing_candidate_owner_failure", + "reflected_enum_missing_candidate_owner_failure", + "reflected_enum_empty_candidate_owner_failure", + "reflected_machine_missing_candidate_owner_failure", + "reflected_machine_empty_candidate_owner_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject foreign reflected field"); + let directory = tempfile::tempdir().expect("isolated missing field directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile missing reflected field candidate"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_type_arg_index_errors_match_interpreter() { + for name in [ + "pending_type_arg_failure", + "pending_type_arg_nested_failure", + "type_arg_negative_failure", + "type_arg_out_of_range_failure", + "type_arg_alias_out_of_range_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject invalid type.arg index"); + let directory = tempfile::tempdir().expect("isolated type.arg directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile type.arg failure"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_type_construction_invalid_use_matches_interpreter() { + for name in [ + "pending_type_construction_invalid_use", + "pending_type_construction_put_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending builder use"); + let directory = tempfile::tempdir().expect("isolated pending builder misuse directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending builder misuse"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_actor_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_actor_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending actor directory"); + let binary = directory.path().join("pending_actor_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending actor values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_actor_send_matches_interpreter_error() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_actor_send_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject a pending actor send"); + assert_eq!( + expected.message, + "runtime error: send/ask: expected actor value, got pending(actor#0)" + ); + let directory = tempfile::tempdir().expect("isolated pending actor send directory"); + let binary = directory.path().join("pending_actor_send_failure.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending actor send"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_pending_capability_values_match_interpreter() { + for name in [ + "pending_capability_values", + "pending_capability_cancelled", + "pending_environment_values", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending capability directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending capability values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + let expected_debug = jett_driver::render_debug_events(&expected.debug_events); + assert_eq!(actual.stderr, expected_debug.as_bytes(), "{name}"); + } +} + +#[test] +fn native_pending_scalar_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_scalar_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending scalar directory"); + let binary = directory.path().join("pending_scalar_values.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending scalar values"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_narrow_integers_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_narrow_integers.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending narrow integer directory"); + let binary = directory.path().join("pending_narrow_integers.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending narrow integers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_refined_narrow_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_refined_narrow.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending refinement directory"); + let binary = directory.path().join("pending_refined_narrow.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending refinement"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refinement_pending_predicate_result_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refinement_pending_predicate_result.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending predicate directory"); + let binary = directory + .path() + .join("refinement_pending_predicate_result.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending predicate"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refinement_pending_aggregate_builders_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refinement_pending_aggregate_builders.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let message = "refinement constraint for 'app.Positive' must return bool, got pending(true)"; + let evaluation_error = "error evaluating refinement constraint for 'app.StrictPositive': unsupported binary operation: pending(7) Gt 0"; + assert_eq!( + debug_print_text(&expected.debug_events), + format!("{message}\n{message}\n{evaluation_error}\n{message}\n{message}\n") + ); + let directory = tempfile::tempdir().expect("isolated aggregate refinement directory"); + let binary = directory + .path() + .join("refinement_pending_aggregate_builders.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile aggregate refinement builders"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_pending_scalar_aggregates_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_scalar_aggregates.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending scalar aggregate directory"); + let binary = directory.path().join("pending_scalar_aggregates.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending scalar aggregates"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_scalar_operations_match_interpreter_errors() { + for name in [ + "pending_scalar_add_failure", + "pending_scalar_nested_add_failure", + "pending_scalar_comparison_failure", + "pending_scalar_float_comparison_failure", + "pending_scalar_negation_failure", + "pending_scalar_not_failure", + "pending_scalar_and_failure", + "pending_scalar_and_right_failure", + "pending_scalar_or_failure", + "pending_scalar_or_right_failure", + "pending_scalar_condition_failure", + "pending_scalar_breakpoint_failure", + "pending_random_lower_bound_failure", + "pending_random_upper_bound_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending scalar operation"); + let directory = tempfile::tempdir().expect("isolated pending scalar operation directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending scalar operation"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_numeric_aggregates_match_interpreter_errors() { + for name in [ + "pending_list_sum_first_failure", + "pending_list_sum_later_failure", + "pending_list_sum_container_failure", + "pending_math_average_failure", + "pending_math_average_container_failure", + "pending_math_median_failure", + "pending_math_median_container_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending numeric list element"); + let directory = tempfile::tempdir().expect("isolated pending numeric directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending numeric aggregate"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_set_elements_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_set_elements.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending set directory"); + let binary = directory.path().join("pending_set_elements.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending set elements"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_map_entries_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/pending_map_entries.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending map directory"); + let binary = directory.path().join("pending_map_entries.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending map entries"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_map_intrinsics_match_interpreter_errors() { + for name in [ + "pending_map_length_failure", + "pending_map_has_failure", + "pending_map_get_failure", + "pending_map_insert_failure", + "pending_map_remove_failure", + "pending_map_from_lists_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending map or input list"); + let directory = tempfile::tempdir().expect("isolated pending map directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending map intrinsic"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_set_intrinsics_match_interpreter_errors() { + for name in [ + "pending_set_length_failure", + "pending_set_contains_failure", + "pending_set_add_failure", + "pending_set_remove_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending set container"); + let directory = tempfile::tempdir().expect("isolated pending set directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending set intrinsic"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_sequence_iteration_matches_interpreter_errors() { + for name in [ + "pending_list_iteration_failure", + "pending_string_iteration_failure", + "pending_map_iteration_failure", + "pending_set_iteration_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject pending iterable"); + let directory = tempfile::tempdir().expect("isolated pending iteration directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary).expect("compile pending iteration"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_scalar_short_circuit_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/pending_scalar_short_circuit.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated pending scalar directory"); + let binary = directory.path().join("pending_scalar_short_circuit.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending scalar short circuit"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_pending_enum_comparisons_match_interpreter_errors() { + for name in [ + "pending_enum_equality_failure", + "pending_enum_inequality_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject direct pending enum comparison"); + let directory = tempfile::tempdir().expect("isolated pending enum comparison directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending enum comparison"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_string_comparisons_match_interpreter_errors() { + for name in [ + "pending_string_equality_failure", + "pending_string_inequality_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject direct pending string comparison"); + let directory = tempfile::tempdir().expect("isolated pending string comparison directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending string comparison"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_pending_nothing_comparisons_match_interpreter_errors() { + for name in [ + "pending_nothing_equality_failure", + "pending_nothing_inequality_failure", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("interpreter must reject direct pending nothing comparison"); + let directory = tempfile::tempdir().expect("isolated pending comparison directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile pending nothing comparison"); + let actual = run_bounded(&binary, directory.path()); + assert!(!actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_captured_closures_match_interpreter() { + for (fixture, main) in [ + ( + "closures.jett", + "function main() returns nothing:\n println(test_make_adder(), test_linear(), test_count_above(), test_nested_closures())\n", + ), + ( + "closures_advanced.jett", + "function main() returns nothing:\n println(test_explicit_struct_parameter(), test_closure_as_argument(), test_conditional_closure_add(), test_conditional_closure_mul(), sum_with_closure(), accumulate_squares(), make_inc_and_dec(5), double_all_sum())\n", + ), + ("captured_local_function_name.jett", ""), + ] { + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .expect("read closure fixture"); + let directory = tempfile::tempdir().expect("isolated closure execution directory"); + let source_path = directory.path().join(fixture); + std::fs::write(&source_path, format!("{source}\n{main}")) + .expect("write closure fixture with an executable entry"); + let expected = + jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{fixture}: {actual:?}" + ); + } +} + +#[test] +fn native_nested_json_shapes_match_interpreter() { + for (fixture, main) in [ + ( + "namespace_use_alias.jett", + "function main() returns nothing:\n println(namespace_alias_summary())\n", + ), + ( + "json_shape_matrix.jett", + "function main() returns nothing:\n println(parse_matrix_summary(), parse_matrix_result_fail(), serialize_matrix_shape_checks(), parse_exact_accepts_matrix_shape(), parse_exact_rejects_nested_shape_extra())\n", + ), + ( + "json_tree_reflection_parse_wrapper.jett", + "function main() returns nothing:\n println(tree_reflected_parse_summary(), tree_reflected_missing_error(), tree_reflected_wrong_item_error(), tree_reflected_full_summary(), tree_reflected_result_failure(), tree_reflected_invalid_refinement_message(), tree_reflected_invalid_bitfield_message())\n", + ), + ] { + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .expect("read nested JSON fixture"); + let directory = tempfile::tempdir().expect("isolated nested JSON execution directory"); + let source_path = directory.path().join(fixture); + std::fs::write(&source_path, format!("{source}\n{main}")) + .expect("write nested JSON fixture with an executable entry"); + let expected = + jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{fixture}: {actual:?}" + ); + } +} + +#[test] +fn native_nested_json_public_serializer_omits_secret_collections() { + let fixture = "json_reflection_nested_serializer.jett"; + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .expect("read nested serializer fixture"); + let directory = tempfile::tempdir().expect("isolated nested serializer directory"); + let source_path = directory.path().join(fixture); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(reflected_user_json(), reflected_alias_refinement_json(), reflected_enum_json(), reflected_bitfield_json(), reflected_control_string_json(), reflected_all_control_string_json(), reflected_unicode_roundtrip_hex())\n" + ), + ) + .expect("write nested serializer executable"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{fixture}: {actual:?}" + ); +} + +#[test] +fn native_nested_json_decoder_matches_interpreter() { + let fixture = "json_reflection_nested_decoder.jett"; + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .expect("read nested decoder fixture"); + let directory = tempfile::tempdir().expect("isolated nested decoder directory"); + let source_path = directory.path().join(fixture); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(decoded_nested_summary(), decoded_result_failure(), invalid_refinement_message(), missing_nested_message(), wrong_struct_shape_message(), wrong_list_item_message(), invalid_bitfield_message(), invalid_enum_payload_message(), invalid_bitfield_enum_message(), decoded_top_level_refinement(), invalid_top_level_refinement_message(), decoded_refined_list_count(), invalid_refined_list_message(), decoded_top_level_float(), decoded_null_as_nothing(), invalid_nothing_message(), decoded_top_level_bytes_length(), decoded_blob_bytes_first(), invalid_bytes_message(), decoded_top_level_secret_redaction(), decoded_secret_field_summary(), invalid_secret_field_message(), missing_secret_field_message())\n" + ), + ) + .expect("write nested decoder executable"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{fixture}: {actual:?}" + ); +} + +#[test] +fn native_generic_struct_fields_match_interpreter() { + let source = include_str!("../../../tests/run_pass/generic_struct.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("generic_struct.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(pair_first_test(), pair_second_test(), box_value_test())\n" + ), + ) + .expect("write struct fixture with an executable entry"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .expect("compile generic struct fixture"); + std::fs::remove_file(&source_path).expect("source is unnecessary at runtime"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_alias_construction_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/alias_construction.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile alias construction"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refined_struct_builder_matches_interpreter() { + let source = include_str!("../../../tests/run_pass/type_construction_builder.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("refined_struct_builder.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(clone_user_by_reflection(), clone_generic_box_by_reflection(), clone_refined_user_by_reflection(), missing_field_message(), duplicate_field_message(), mismatched_metadata_message(), mismatched_finish_target_message())\n" + ), + ) + .expect("write reflected builder executable"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .expect("compile reflected builder executable"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_struct_base_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/refined_builder_base.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada\nrefinement type constraint failed for 'app.NonEmpty'\nrefinement type constraint failed for 'app.Long'\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile reflected base-value builder"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_enum_base_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refined_enum_builder_base.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "idle\nAda:Agent:ready\nrefinement type constraint failed for 'app.NonEmpty'\nrefinement type constraint failed for 'app.Long'\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile reflected enum base-value builder"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_machine_base_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refined_machine_builder_base.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada:Agent:ready\nrefinement type constraint failed for 'app.NonEmpty'\nrefinement type constraint failed for 'app.Long'\nNia\nrefinement type constraint failed for 'app.NonEmpty'\nfilled\nrefinement type constraint failed for 'app.Positive'\nempty\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile reflected machine base-value builder"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refined_struct_base_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refined_struct_base_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada:Agent\nrefinement type constraint failed for 'test.NonEmpty'\nrefinement type constraint failed for 'test.Long'\nAda\nrefinement type constraint failed for 'test.NonEmpty'\n7\nrefinement type constraint failed for 'test.Positive'\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile base-value refinement constructor"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refined_struct_derived_inputs_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refined_struct_derived_inputs.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Agent\nrefinement type constraint failed for 'test.Long'\naccepted\nrefinement type constraint failed for 'test.SecretPositive'\nAgent\nrefinement type constraint failed for 'test.Long'\naccepted\nrefinement type constraint failed for 'test.SecretPositive'\nAgent\nrefinement type constraint failed for 'test.SecretLong'\nrefinement type constraint failed for 'test.SecretNonEmpty'\n" + ); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile derived-input refinement constructor"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_json_narrow_integers_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_narrow_integers.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile narrow JSON decoders"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_json_float32_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_float32.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile float32 JSON decoder"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_json_set_serialize_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_set_serialize.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile set JSON serializer"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_json_extended_set_parse_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_set_parse_extended.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile extended set JSON parser"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_alias_json_parse_matches_interpreter() { + let source = include_str!("../../../tests/run_pass/reflection_type_id_duplicate_aliases.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("alias_json.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(alias_reflection_summary(), account_alias_json_summary(), audit_alias_json_summary())\n" + ), + ) + .expect("write alias JSON executable"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary).expect("compile alias JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_recursive_struct_json_matches_interpreter() { + let source = include_str!("../../../tests/run_pass/recursive_owned_values.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("recursive_json.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction recursive_json_summary() returns string:\n Node tail = Node(value: 1, next: none)\n Node original = Node(value: 2, next: some(tail))\n string encoded = json.serialize[Node](view original)\n Node decoded = json.parse_exact[Node](encoded) handle error:\n return error\n Node next = decoded.next handle:\n return \"missing next\"\n return \"{{decoded.value}}:{{next.value}}:{{encoded}}\"\nfunction main() returns nothing:\n println(recursive_json_summary())\n" + ), + ) + .expect("write recursive JSON executable"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "2:1:{\"value\":2,\"next\":{\"value\":1,\"next\":null}}\n" + ); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary).expect("compile recursive JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_uint64_checked_expression_dispatch_matches_interpreter() { + let source = + include_str!("../../../tests/run_pass/uint64_checked_expression_runtime_types.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("uint64_dispatch.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(constructed_list_uint64_dispatch(), appended_list_uint64_dispatch(), map_value_uint64_dispatch(), set_to_list_uint64_dispatch(), optional_uint64_dispatch(), result_uint64_dispatch())\n" + ), + ) + .expect("write uint64 dispatch fixture with an executable entry"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .expect("compile uint64 dispatch fixture"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_math_aggregates_preserve_interpreter_extremes() { + let source = include_str!("../../../tests/run_pass/math_extra.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let source_path = directory.path().join("math_extra.jett"); + std::fs::write( + &source_path, + format!( + "{source}\nfunction main() returns nothing:\n println(average_test(), average_finite_extremes_test(), average_preserves_small_residual_test(), median_odd_test(), median_even_test(), median_finite_extremes_test())\n" + ), + ) + .expect("write math aggregate fixture with an executable entry"); + let expected = jett_driver::run_file_capture_output(&source_path).expect("interpreter oracle"); + let binary = directory.path().join("program.exe"); + build_host_executable(&source_path, launcher(), &binary) + .expect("compile math aggregate fixture"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_debug_statements_match_interpreter_output() { + for relative_path in [ + "../../tests/run_pass/trace_basic.jett", + "../../tests/native/trace_int64.jett", + "../../tests/run_pass/breakpoint_basic.jett", + "../../tests/native/breakpoint_int64.jett", + "../../tests/native/debug_primitives.jett", + "../../tests/native/debug_aggregates.jett", + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let binary = directory.path().join("program.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile int64 trace fixture"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{relative_path}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{relative_path}"); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + debug, + "{relative_path}" + ); + } +} + +#[test] +fn native_breakpoints_preserve_lexical_frames_and_capture_types() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/breakpoint_lexical_frames.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert_eq!(debug_trace_lines(&expected.debug_events).len(), 13); + assert_eq!( + &debug_trace_lines(&expected.debug_events)[..3], + [ + "breakpoint hit: value: int64 = 1", + "breakpoint hit: nested: string = leaf, value: int64 = 1", + "breakpoint hit: value: int64 = 1", + ] + ); + assert_eq!( + debug_trace_lines(&expected.debug_events)[7], + "breakpoint hit: label: string = captured, seed: uint8 = 7, value: int64 = 3" + ); + assert_eq!( + debug_trace_lines(&expected.debug_events)[11], + debug_trace_lines(&expected.debug_events)[7] + ); + assert_eq!( + debug_trace_lines(&expected.debug_events) + .iter() + .filter(|line| line.contains("caller:")) + .count(), + 2 + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("breakpoint_frames.exe"); + build_host_executable(&fixture, launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_release_strips_debug_observations_and_their_conditions() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/release_debug_observations.jett"); + let directory = tempfile::tempdir().unwrap(); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + for release in [false, true] { + let binary = directory.path().join(format!("release_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &fixture, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + if release { + assert_eq!(actual.stdout, b"program output\n"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } else { + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); + } + } + let source = directory.path().join("debug_print.jett"); + std::fs::write( + &source, + "function main() returns nothing:\n println(7)\n", + ) + .unwrap(); + let output = directory.path().join("rejected.exe"); + let error = jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &output, + jett_driver::BuildOptions { release: true }, + ) + .unwrap_err(); + assert!(error.to_string().contains("E0362"), "{error}"); + assert!(!output.exists()); +} + +#[test] +fn native_breakpoints_omit_consumed_bindings() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/breakpoint_consumed_bindings.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(!debug.contains("initial:"), "{debug}"); + assert!(debug.contains("original_count: app.Count = 7"), "{debug}"); + assert!(debug.contains("copied_count: app.Count = 7"), "{debug}"); + assert!(debug.contains("breakpoint hit: moved: bytes"), "{debug}"); + assert!(debug.contains("breakpoint hit\n"), "{debug}"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("breakpoint_consumed.exe"); + build_host_executable(&fixture, launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(String::from_utf8_lossy(&actual.stderr), debug); +} + +#[test] +fn native_secret_debug_values_are_recursively_redacted() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/debug_secret_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(debug.contains("[redacted]"), "{debug}"); + for secret in ["hidden-", "4321", "7654"] { + assert!(!debug.contains(secret), "secret leaked: {debug}"); + } + assert!( + debug.contains("label: public-label, token: [redacted]"), + "{debug}" + ); + assert!(debug.contains("public-key: [redacted]"), "{debug}"); + assert!( + debug.contains("TypeConstruction[app.Vault](token: [redacted])"), + "{debug}" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("secret_debug.exe"); + build_host_executable(&fixture, launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(String::from_utf8_lossy(&actual.stderr), debug); +} + +#[test] +fn native_type_construction_debug_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/debug_type_construction.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada:7\n4\n3:4\nlogged_in\n" + ); + assert_eq!(debug_trace_lines(&expected.debug_events).len(), 9); + let directory = tempfile::tempdir().expect("isolated builder debug directory"); + let binary = directory.path().join("debug_type_construction.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile partial reflected builders with trace"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_collection_callbacks_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/collection_callbacks.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "44:14:true:true:2\nscore-2:score-4\n1:2\n" + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + ["trace value: int64 = 2"] + ); + let directory = tempfile::tempdir().expect("isolated collection callback directory"); + let binary = directory.path().join("collection_callbacks.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile higher-order collection helpers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_collection_shapes_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/collection_shapes.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "2:1:one:1:1:b:3\n2:2\n2:a:1\n" + ); + let directory = tempfile::tempdir().expect("isolated collection shape directory"); + let binary = directory.path().join("collection_shapes.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile collection shape conversions"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_json_refined_aggregates_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_refined_aggregates.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada\nactive.name: refinement type constraint failed for 'app.NonEmpty'\nLin\nnamed.name: refinement type constraint failed for 'app.NonEmpty'\nAda:ready\nLin:ready\nTao\nactive.profile: refinement type constraint failed for 'app.NamedProfile'\nMira\nNia\n" + ); + let directory = tempfile::tempdir().expect("isolated refined aggregate directory"); + let binary = directory.path().join("json_refined_aggregates.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile reflected enum and machine construction with validated fields"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_function_debug_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/debug_functions.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "3 5 9 5 5 9\n3 9 9 9\n4 6 10\n12 kept\n" + ); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(debug.contains("function(callback_library.increment)")); + assert!(debug.contains("function(left, right)")); + assert!(debug.contains("trace baked_inline: function(int64) returns int64 = function(source)")); + assert!(!debug.contains("DO_NOT_RENDER_CAPTURE")); + assert_eq!(debug.matches("breakpoint hit:").count(), 1); + let directory = tempfile::tempdir().expect("isolated function debug directory"); + let binary = directory.path().join("debug_functions.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile function values and nested callback debug layouts"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(String::from_utf8_lossy(&actual.stderr), debug); + + let verify_binary = directory.path().join("debug_functions_verify.exe"); + build_host_verify_suite_executable(&fixture, launcher(), &verify_binary) + .expect("compile callback traces in a native verify suite"); + let verified = run_bounded(&verify_binary, directory.path()); + assert!(verified.status.success(), "{verified:?}"); + assert!(verified.stdout.is_empty(), "{verified:?}"); + assert_eq!( + String::from_utf8_lossy(&verified.stderr), + "trace verify_named: function(int64) returns int64 = function(test.double)\ntrace verify_captured: function(int64) returns int64 = function(input)\n" + ); + + let property_binary = directory.path().join("debug_functions_property.exe"); + build_host_property_suite_executable(&fixture, launcher(), &property_binary) + .expect("compile callback traces across generated native property trials"); + let property = run_bounded(&property_binary, directory.path()); + assert!(property.status.success(), "{property:?}"); + assert!(property.stdout.is_empty(), "{property:?}"); + assert_eq!( + String::from_utf8_lossy(&property.stderr), + "trace property_callback: function(int64) returns int64 = function(test.double)\n" + .repeat(100) + ); +} + +#[test] +fn native_actor_debug_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/debug_actor_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert!(expected.stdout.is_empty()); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace first: test.Holder = actor#0", + "trace second: test.Holder = actor#1", + "trace active: list[test.Holder] = list(actor#2)", + "breakpoint hit: active: list[test.Holder] = list(actor#2), first: test.Holder = actor#0, second: test.Holder = actor#1", + ] + ); + let directory = tempfile::tempdir().expect("isolated actor debug directory"); + let binary = directory.path().join("debug_actor_values.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile actor traces and breakpoint without consuming handles"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_comptime_namespace_aliases_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_namespace_aliases.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "6 6\n105\n6\n"); + let directory = tempfile::tempdir().expect("isolated comptime namespace alias directory"); + let binary = directory.path().join("comptime_namespace_aliases.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile baked callbacks through scoped namespace aliases"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + + let verify_binary = directory + .path() + .join("comptime_namespace_aliases_verify.exe"); + build_host_verify_suite_executable(&fixture, launcher(), &verify_binary) + .expect("compile alias-aware baked callback in native verify body"); + let verified = run_bounded(&verify_binary, directory.path()); + assert!(verified.status.success(), "{verified:?}"); + assert!(verified.stdout.is_empty(), "{verified:?}"); + assert!(verified.stderr.is_empty(), "{verified:?}"); + + let property_binary = directory + .path() + .join("comptime_namespace_aliases_property.exe"); + build_host_property_suite_executable(&fixture, launcher(), &property_binary) + .expect("compile alias-aware baked callback across native property trials"); + let property = run_bounded(&property_binary, directory.path()); + assert!(property.status.success(), "{property:?}"); + assert!(property.stdout.is_empty(), "{property:?}"); + assert!(property.stderr.is_empty(), "{property:?}"); +} + +#[test] +fn native_method_function_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/method_function_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "10 24 105\n10 10 10 10\n10 10 10\n10 24 10 10\n10 10 12 13\n1010\n" + ); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(debug.contains("function(method_library.Point.amount)")); + assert!(debug.contains("function(method_library.Counter.amount)")); + assert!(debug.contains("function(alternate_library.Point.amount)")); + assert!(debug.contains("function(input)")); + let directory = tempfile::tempdir().expect("isolated method callback directory"); + let binary = directory.path().join("method_function_values.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile source method values and baked method callbacks"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(String::from_utf8_lossy(&actual.stderr), debug); + + let verify_binary = directory.path().join("method_function_values_verify.exe"); + build_host_verify_suite_executable(&fixture, launcher(), &verify_binary) + .expect("compile method callbacks in native verify bodies"); + let verified = run_bounded(&verify_binary, directory.path()); + assert!(verified.status.success(), "{verified:?}"); + assert!(verified.stdout.is_empty(), "{verified:?}"); + assert!(verified.stderr.is_empty(), "{verified:?}"); + + let property_binary = directory.path().join("method_function_values_property.exe"); + build_host_property_suite_executable(&fixture, launcher(), &property_binary) + .expect("compile method callbacks across native property trials"); + let property = run_bounded(&property_binary, directory.path()); + assert!(property.status.success(), "{property:?}"); + assert!(property.stdout.is_empty(), "{property:?}"); + assert!(property.stderr.is_empty(), "{property:?}"); +} + +#[test] +fn native_function_debug_values_clean_up_after_failure() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/debug_functions_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("failure after tracing owned function descriptors"); + let debug = jett_driver::render_debug_events(&expected.output.debug_events); + let directory = tempfile::tempdir().expect("isolated function debug failure directory"); + let binary = directory.path().join("debug_functions_failure.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile a terminal failure with live function debug metadata"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + format!("{debug}{}\n", expected.message) + ); +} + +#[test] +fn native_assertion_compiles_interpolated_failure_message() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/assert_messages.jett"); + let directory = tempfile::tempdir().expect("isolated verify directory"); + let binary = directory.path().join("assert_messages.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &binary) + .expect("compile native verify assertion"); + let actual = run_bounded(&artifact.path, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + let property_binary = directory.path().join("assert_property_messages.exe"); + let property = build_host_property_suite_executable(&fixture, launcher(), &property_binary) + .expect("compile native property assertion"); + let property_output = run_bounded(&property.path, directory.path()); + assert!(property_output.status.success(), "{property_output:?}"); + assert!(property_output.stdout.is_empty(), "{property_output:?}"); + assert!(property_output.stderr.is_empty(), "{property_output:?}"); +} + +#[test] +fn native_enum_struct_payload_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/enum_struct_payload.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated enum payload directory"); + let binary = directory.path().join("enum_struct_payload.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile enum struct payload"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_deep_interface_conversion_matches_interpreter_in_both_profiles() { + // Keep frontend recursion separate from the linked program's runtime stack. + std::thread::Builder::new().stack_size(8 * 1024 * 1024).spawn(|| { + let concrete = format!("{}User{}", "list[".repeat(130), "]".repeat(130)); + let element = format!("{}Named{}", "list[".repeat(129), "]".repeat(129)); + let source_text = format!("namespace app\ninterface Named:\n function name(view self: Named) returns string\nstruct User:\n value: string\nimplement Named for User:\n function name(view self: User) returns string:\n return self.value\ntype Concrete = {concrete}\ntype Element = {element}\ntype Erased = list[Element]\nfunction main(stdout: Stdout) returns nothing:\n Concrete values = list()\n Erased converted = values\n Stdout.write(view stdout, \"{{list.length[Element](view converted)}}\\n\")\n"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("deep_conversion.jett"); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert_eq!(expected.stdout, "0\n"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("conversion_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, launcher(), &binary, jett_driver::BuildOptions { release }, + ).unwrap(); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(actual.stderr, jett_driver::render_debug_events(&expected.debug_events).as_bytes(), "{actual:?}"); + } + }).unwrap().join().unwrap(); +} + +#[test] +fn native_deep_reflection_dispatch_matches_interpreter_in_both_profiles() { + // Give the frontend's recursive type walks enough stack for this source. + // Linked programs still run in separate processes with ordinary stacks. + std::thread::Builder::new() + .stack_size(8 * 1024 * 1024) + .spawn(|| { + let nested = format!("{}int64{}", "list[".repeat(70), "]".repeat(70)); + let source_text = format!( + "namespace app\ntype Deep = {nested}\nstruct Record:\n direct: {nested}\n aliased: Deep\nfunction main(stdout: Stdout) returns nothing:\n for field in type.fields[Record]():\n comptime type Field = field.type_info:\n Stdout.write(view stdout, \"{{type.kind[Field]()}}:{{type.name[Field]()}}\\n\")\n" + ); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("deep_reflection.jett"); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert_eq!(expected.stdout, format!("list:{nested}\nalias:app.Deep\n")); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("reflection_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(actual.stderr, jett_driver::render_debug_events(&expected.debug_events).as_bytes(), "{actual:?}"); + } + }) + .unwrap() + .join() + .unwrap(); +} + +#[test] +fn native_enum_deep_equality_matches_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/enum_deep_equality.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("deep_equality.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert_eq!(expected.stdout, "true true false false true false\n"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("equality_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_enum_aggregate_equality_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/enum_aggregate_equality.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated enum equality directory"); + let binary = directory.path().join("enum_aggregate_equality.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile aggregate enum equality"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_binary_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_handle_binary.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated nested handle directory"); + let binary = directory.path().join("nested_handle_binary.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile binary expression with a nested handle"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_field_receiver_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_field_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada\nfallback\nAda\nmissing\nLin\noptional fallback\nKai\nnested fallback\n" + ); + let directory = tempfile::tempdir().expect("isolated field receiver directory"); + let binary = directory.path().join("nested_field_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile field receiver with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_state_condition_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_state_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "active\nguest\n2\n" + ); + let directory = tempfile::tempdir().expect("isolated state condition directory"); + let binary = directory.path().join("nested_state_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile state test with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_for_iterable_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_for_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "source\nAda\nLin\nsource\nfallback\nsource\nmissing\n" + ); + let directory = tempfile::tempdir().expect("isolated for iterable directory"); + let binary = directory.path().join("nested_for_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile for iterable with nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_debug_conditions_nested_handle_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_debug_condition_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "after\n"); + assert_eq!( + debug_trace_lines(&expected.debug_events), + ["breakpoint hit"] + ); + let directory = tempfile::tempdir().expect("isolated debug condition directory"); + let binary = directory.path().join("nested_debug_condition_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile breakpoint condition with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); + + let verify_binary = directory.path().join("nested_debug_condition_verify.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &verify_binary) + .expect("compile assertion condition with nested handler"); + let verified = run_bounded(&artifact.path, directory.path()); + assert!(verified.status.success(), "{verified:?}"); + assert!(verified.stdout.is_empty(), "{verified:?}"); + assert!(verified.stderr.is_empty(), "{verified:?}"); +} + +#[test] +fn native_machine_transition_nested_handlers_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_transition_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "source\npayload\nBefore\nBefore:Ada\nsource\npayload\nChanged\nBefore:Fallback\n" + ); + let directory = tempfile::tempdir().expect("isolated machine transition directory"); + let binary = directory.path().join("nested_transition_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile machine transition with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_task_operands_nested_handlers_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_task_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Ada\nFallback\nsource\nLin\nsource\nFallback\njoined\njoined\n" + ); + let directory = tempfile::tempdir().expect("isolated task operand directory"); + let binary = directory.path().join("nested_task_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile run and join operands with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_actor_spawn_arguments_nested_handlers_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_actor_spawn_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "count\nseed\n7\nAda:3\ncount\nseed\n9\nFallback:7\n" + ); + let directory = tempfile::tempdir().expect("isolated actor spawn directory"); + let binary = directory.path().join("nested_actor_spawn_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile actor spawn arguments with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_actor_message_arguments_nested_handlers_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_actor_message_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "text\ntext\nBefore\nBefore:Ada:Ada\nAda\ntext\ntext\nChanged\nBefore:Fallback:Fallback\nFallback\nAda\nFallback\n" + ); + let directory = tempfile::tempdir().expect("isolated actor message directory"); + let binary = directory.path().join("nested_actor_message_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile actor message arguments with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_deep_debug_values_match_interpreter_without_a_fixed_depth_limit() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/debug_deep_values.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("deep.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert!(debug_print_text(&expected.debug_events).contains("app.Chain.end")); + assert!(debug.contains("[redacted]")); + assert!(!debug.contains("deep-secret-token")); + let binary = directory.path().join("deep.exe"); + build_host_executable(&source, launcher(), &binary).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!(actual.stderr, debug.as_bytes()); +} + +#[test] +fn native_debug_print_argument_failure_produces_no_partial_output() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("print_failure.jett"); + fs::write(&source, "namespace app\nfunction failed() returns int64:\n string rejected = string.repeat(\"ab\", 9223372036854775807)\n return string.char_count(rejected)\nfunction main() returns nothing:\n println(list(\"held\"), failed())\n").unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert!(expected.output.stdout.is_empty()); + assert!(expected.output.debug_events.is_empty()); + assert!(expected.message.contains("string.repeat"), "{expected:?}"); + let binary = directory.path().join("print_failure.exe"); + build_host_executable(&source, launcher(), &binary).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert!(actual.stdout.is_empty()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_debug_prints_aggregate_values_without_consuming_views() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native"); + for name in [ + "debug_print_values", + "debug_aggregates", + "debug_functions", + "debug_actor_values", + "debug_type_construction", + ] { + let original = fs::read_to_string(root.join(format!("{name}.jett"))).unwrap(); + let source_text = original + .lines() + .filter_map(|line| { + let content = line.trim_start(); + if content.starts_with("breakpoint ") { + return None; + } + Some(if let Some(value) = content.strip_prefix("trace ") { + format!( + "{}println(view {value})", + &line[..line.len() - content.len()] + ) + } else { + line.to_owned() + }) + }) + .collect::>() + .join("\n"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, format!("{source_text}\n")).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert!(expected.stdout.is_empty(), "{name}"); + assert!( + !debug_print_text(&expected.debug_events).is_empty(), + "{name}" + ); + assert!( + expected.debug_events.iter().all(|event| matches!( + event.kind, + jett_driver::DebugEventKind::Print | jett_driver::DebugEventKind::Println + )), + "{name}" + ); + assert!( + debug_trace_lines(&expected.debug_events).is_empty(), + "{name}" + ); + let binary = directory.path().join("print.exe"); + build_host_executable(&source, launcher(), &binary).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_float_remainder_matches_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/float_remainder.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert_eq!( + expected.stdout, + concat!( + "1.5 -1.5 1.5 -1.5\n1.5\n", + "1.5 -1.5 1.5 -1.5\n1.5\n", + "true true true true true true\n", + "true true true true true true\n", + "1.5 -1.5\ntrue\n", + ) + ); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("remainder.jett"); + fs::copy(&fixture, &source).unwrap(); + let mut executables = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("remainder_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + executables.push(binary); + } + fs::remove_file(&source).unwrap(); + for executable in executables { + let actual = run_bounded(&executable, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_float_remainder_rejects_pending_operands_and_cleans_owners() { + for width in ["float32", "float64"] { + for (left, right) in [("run run 5.5", "2.0"), ("5.5", "run run 2.0")] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_remainder.jett"); + fs::write(&source, format!( + "namespace app\nfunction main() returns nothing:\n list[string] owners = list(\"left\", \"right\")\n {width} left = {left}\n {width} right = {right}\n {width} remainder = left modulo right\n" + )).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert!( + expected.message.contains("unsupported binary operation:"), + "{expected:?}" + ); + assert!(expected.message.contains("Modulo"), "{expected:?}"); + for release in [false, true] { + let binary = directory.path().join(format!("pending_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); + } + } + } +} + +#[test] +fn native_arithmetic_width_matrix_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/arithmetic_matrix.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + concat!( + "i8 -128 127 -2 -128 0 true\n", + "i16 -32768 32767 -2 -32768 0 true\n", + "i32 -2147483648 2147483647 -2 -2147483648 0 true\n", + "i64 -9223372036854775808 9223372036854775807 -2 -9223372036854775808 0 true\n", + "u8 0 255 254 127 1 true\n", + "u16 0 65535 65534 32767 1 true\n", + "u32 0 4294967295 4294967294 2147483647 1 true\n", + "u64 0 18446744073709551615 18446744073709551614 9223372036854775807 1 true\n", + "f32 -1.25 3.75 -3.125 -0.5 true true false true true\n", + "f64 -1.25 3.75 -3.125 -0.5 true true false true true\n", + ) + ); + let directory = tempfile::tempdir().expect("isolated arithmetic matrix directory"); + let binary = directory.path().join("arithmetic_matrix.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile arithmetic matrix"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_json_enums_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_nested_enum.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "[\"north\",\"south\"]\n[{\"point\":[1,2]},\"empty\"]\n{\"a\":\"north\",\"b\":\"south\"}\n[{\"recorded\":[{\"name\":\"Ada\"}]},\"idle\"]\n" + ); + let directory = tempfile::tempdir().expect("isolated nested JSON enum directory"); + let binary = directory.path().join("json_nested_enum.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile nested JSON enums"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_json_bitfields_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_nested_bitfield.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "{\"title\":\"packet\",\"header\":{\"version\":4,\"flags\":2,\"protocol\":\"tcp\",\"payload\":[1,255]}}\nheader: protocol: expected enum string or object, got number\nheader: payload: 0: expected uint8 in range 0..255\n" + ); + let directory = tempfile::tempdir().expect("isolated nested bitfield directory"); + let binary = directory.path().join("json_nested_bitfield.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile nested bitfield JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_json_machines_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_nested_machine_parse.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated nested machine JSON directory"); + let binary = directory.path().join("json_nested_machine_parse.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile nested machine JSON"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_projected_machine_sequences_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/projected_machine_sequences.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "a 5\nb 7\n30 2 2 2\n30 30\n" + ); + let directory = tempfile::tempdir().expect("isolated machine projection directory"); + let binary = directory.path().join("projected_machine_sequences.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile iteration through machine fields"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_temporary_projected_views_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/temporary_projected_views.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("temporary view oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "make:list\nAda\nLin\nmake:set\ntag\nmake:map\nrow owned 7\nmake:string\n🚀\ne\n\u{301}\nmake:record\nAda\nLin\nrow owned\n11\nmake:direct\nmissing:direct\nAda:fallback:2\nmake:indirect\nmissing:indirect\nAda:fallback:2\nmake:early\nmissing:early\nreturned\nmake:break\nAda\nmake:continue\nLin\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("temporary_projected_views.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native temporary views"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_temporary_projected_view_failure_cleans_owners() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/temporary_projected_view_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("later argument fails after borrowing the temporary owner"); + assert_eq!( + debug_print_text(&expected.output.debug_events), + "owner\nargument\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory + .path() + .join("temporary_projected_view_failure.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native temporary view failure"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_projected_nested_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/projected_nested_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated nested projection directory"); + let binary = directory.path().join("projected_nested_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile nested collection projection"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_handle_projected_view_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_projected_view.jett"); + let directory = tempfile::tempdir().expect("isolated projected handler directory"); + let source = directory.path().join("nested_handle_projected_view.jett"); + fs::copy(&fixture, &source).expect("copy unchanged projected handler source"); + let expected = jett_driver::run_file_capture_output(&source).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "3 8\n10 13\n"); + let binary = directory.path().join("nested_handle_projected_view.exe"); + build_host_executable(&source, launcher(), &binary) + .expect("compile projected view before handler"); + fs::remove_file(&source).expect("erase projected handler source before native execution"); + assert!(!source.exists()); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reused_handle_locals_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/reused_handle_locals.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "1 2\n7 7\n5 5\n4\n2 2\n" + ); + let directory = tempfile::tempdir().expect("isolated reused handle directory"); + let binary = directory.path().join("reused_handle_locals.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reused local handles"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_projected_sum_handles_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/projected_sum_handles.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "18 18\n"); + let directory = tempfile::tempdir().expect("isolated projected sum directory"); + let binary = directory.path().join("projected_sum_handles.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile projected sum handles"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_join_local_reuse_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/join_local_reuse.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated join reuse directory"); + let binary = directory.path().join("join_local_reuse.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile reusable join local"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_run_local_aggregate_reuse_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/run_local_aggregate_reuse.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated run-local directory"); + let binary = directory.path().join("run_local_aggregate_reuse.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile local aggregate run"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_run_local_builder_reuse_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/run_local_builder_reuse.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated run-local builder directory"); + let binary = directory.path().join("run_local_builder_reuse.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile local builder run"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_transparent_local_reuse_matches_interpreter() { + for name in [ + "coarsen_local_refinement_reuse", + "declassify_local_secret_reuse", + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/native/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated transparent conversion directory"); + let binary = directory.path().join(format!("{name}.exe")); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile transparent local conversion"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert_eq!(actual.stderr, debug.as_bytes(), "{name}"); + } +} + +#[test] +fn native_projected_string_iteration_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/projected_string_iteration.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "4 4\n"); + let directory = tempfile::tempdir().expect("isolated projected string directory"); + let binary = directory.path().join("projected_string_iteration.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile projected string iteration"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_borrowed_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_borrowed_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert!( + debug_print_text(&expected.debug_events).ends_with("11 11\n2 2\n"), + "{expected:?}" + ); + let directory = tempfile::tempdir().expect("isolated nested collection directory"); + let binary = directory.path().join("nested_borrowed_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile nested borrowed collection iteration"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_nested_machine_borrowed_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_machine_borrowed_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert!( + debug_print_text(&expected.debug_events).ends_with("6 6\n"), + "{expected:?}" + ); + let directory = tempfile::tempdir().expect("isolated nested machine collection directory"); + let binary = directory + .path() + .join("nested_machine_borrowed_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile nested borrowed machine collection iteration"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_generic_empty_collections_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_empty_collections.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "0 0 0\n0 0\n1 1\nAda true\n" + ); + let directory = tempfile::tempdir().expect("isolated generic collection directory"); + let binary = directory.path().join("generic_empty_collections.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile generic empty collections"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_generic_map_aggregates_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/generic_map_aggregates.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "0 0\nAda true 2\n" + ); + let directory = tempfile::tempdir().expect("isolated generic map directory"); + let binary = directory.path().join("generic_map_aggregates.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile generic maps of aggregates"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_displayable_interpolation_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/displayable_interpolation.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "Hello user:Ada!\nResult user:Grace\n" + ); + let directory = tempfile::tempdir().expect("isolated displayable directory"); + let binary = directory.path().join("displayable_interpolation.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile displayable interpolation"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_handled_interpolation_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/handled_interpolation.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "value 3\nfirst\nfallback\nlast\nordered first 7 last\n" + ); + let directory = tempfile::tempdir().expect("isolated handled interpolation directory"); + let binary = directory.path().join("handled_interpolation.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile handled interpolation"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_match_scrutinee_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_match_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "hello\nempty\n"); + let directory = tempfile::tempdir().expect("isolated match scrutinee directory"); + let binary = directory.path().join("nested_match_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile match scrutinee with nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_clone_operand_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_clone_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "Ada\nfallback\n"); + let directory = tempfile::tempdir().expect("isolated clone operand directory"); + let binary = directory.path().join("nested_clone_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile clone operand with nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_coarsen_operand_nested_handle_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/nested_coarsen_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "Ada\nFallback\n"); + let directory = tempfile::tempdir().expect("isolated coarsen operand directory"); + let binary = directory.path().join("nested_coarsen_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile coarsen operand with nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_declassify_operand_nested_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_declassify_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "Ada\nFallback\n"); + let directory = tempfile::tempdir().expect("isolated declassify operand directory"); + let binary = directory.path().join("nested_declassify_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile declassify operand with nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_enum_binary_nested_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_enum_binary.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated enum binary directory"); + let binary = directory.path().join("nested_handle_enum_binary.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile enum equality with a nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refinement_local_source_reuse_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/refinement_local_source_reuse.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated refinement source directory"); + let binary = directory.path().join("refinement_local_source_reuse.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile refinement from reusable local"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_nested_refinement_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_refinement_handle.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated refinement handler directory"); + let binary = directory.path().join("nested_refinement_handle.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile refinement candidate with a nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_refined_binary_nested_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_refined_binary.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated refined binary directory"); + let binary = directory.path().join("nested_handle_refined_binary.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile refined equality with a nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_machine_values_can_be_evaluated_repeatedly() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_machine_reuse.jett"); + let expected = + jett_driver::run_file_capture_output(&fixture).expect("repeated comptime oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "active:7\n".repeat(12) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_machine_reuse.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("native repeated comptime machines"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_builders_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/comptime_builders.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("comptime builder oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + concat!( + "type.construct_finish: 'app.Record' is missing required field 'count'\n", + "record:5:runtime\n", + "type.construct_finish: 'app.Record' is missing required field 'label'\n", + "refinement type constraint failed for 'app.Positive'\n", + "record:8:runtime\nrecord:1:runtime\nrecord:2:runtime\nrecord:9:runtime\nbuilder\n", + "127\nalias\ndeclared-alias\nrecord:12:runtime\nrecord:14:runtime\n7\nheader:3\n", + "bitfield 'app.Header' field 'kind' is 8 bit(s) wide and cannot hold '256'\n", + ) + ); + assert!( + debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("[redacted]")) + ); + assert!( + !debug_trace_lines(&expected.debug_events) + .iter() + .any(|line| line.contains("hidden-baked-builder")) + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_builders.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native comptime builders"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + String::from_utf8_lossy(&actual.stderr), + jett_driver::render_debug_events(&expected.debug_events) + ); +} + +#[test] +fn native_comptime_composites_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/comptime_composites.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated comptime composite directory"); + let binary = directory.path().join("comptime_composites.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile a baked composite without running its source expression"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_generic_closures_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_generic_closures.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("generic closure oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "7:2\nname:3\nalias:4\n17\nkept\n29\nnested:5\n23:6\nint64\nLabel\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("comptime_generic_closures.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("native generic comptime closures"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_function_values_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_function_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated comptime function directory"); + let binary = directory.path().join("comptime_function_values.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile a baked function value without resolving its source expression"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_captured_functions_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_captured_functions.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated comptime closure directory"); + let binary = directory.path().join("comptime_captured_functions.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile a baked closure with an evaluated capture environment"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_comptime_function_values_execute_in_verify_suite() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/comptime_captured_functions.jett"); + let directory = tempfile::tempdir().expect("isolated comptime verify directory"); + let binary = directory.path().join("comptime_captured_verify.exe"); + let artifact = build_host_verify_suite_executable(&fixture, launcher(), &binary) + .expect("compile baked function values inside a verify body"); + let actual = run_bounded(&artifact.path, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); +} + +#[test] +fn native_view_function_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/view_function_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated view callback directory"); + let binary = directory.path().join("view_function_values.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile an indirect call through a view-parameter function value"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_source_function_named_like_builtin_prefix_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/source_function_builtin_prefix_name.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated source function directory"); + let binary = directory + .path() + .join("source_function_builtin_prefix_name.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile source function named like a builtin prefix"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_function_expression_calls_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/function_expression_calls.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated expression callback directory"); + let binary = directory.path().join("function_expression_calls.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile calls through function expressions"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_function_expression_callee_failure_cleans_owned_arguments() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/function_expression_callee_failure.jett"); + let expected = jett_driver::run_file_capture_outcome(&fixture) + .expect_err("callee selection must fail after evaluating owned arguments"); + let directory = tempfile::tempdir().expect("isolated callee failure directory"); + let binary = directory + .path() + .join("function_expression_callee_failure.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile failing callee"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes() + ); +} + +#[test] +fn native_named_enum_arguments_preserve_source_order() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/named_enum_arguments.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + assert_eq!( + debug_print_text(&expected.debug_events), + "amount\nlabel\nnumbers\nlabel 2 7\nfirst\nhandler\nafter\nafter 2 9\nallocated\nreturned 1 11\npiped\npiped amount\npiped numbers\npiped 2 12\nbaked 3 8\nmixed amount\nmixed label\nmixed numbers\nmixed label 2 13\nmixed piped\nmixed piped amount\nmixed piped numbers\nmixed piped 2 14\nmixed baked 1 15\nmixed function 2 16\nmixed function piped 1 17\n" + ); + let directory = tempfile::tempdir().expect("isolated named enum directory"); + let binary = directory.path().join("named_enum_arguments.exe"); + build_host_executable(&fixture, launcher(), &binary).expect("compile named enum payloads"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_constructor_nested_handles_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_constructors.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated constructor handle directory"); + let binary = directory.path().join("nested_handle_constructors.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile constructors with nested handlers"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = jett_driver::render_debug_events(&expected.debug_events); + assert_eq!(String::from_utf8_lossy(&actual.stderr), debug); +} + +#[test] +fn native_indirect_call_nested_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_indirect_call.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated indirect handle directory"); + let binary = directory.path().join("nested_handle_indirect_call.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile indirect call with a nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_indirect_view_argument_before_handler_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_indirect_view.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated indirect view directory"); + let binary = directory.path().join("nested_handle_indirect_view.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile indirect view argument before a handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_intrinsic_nested_handle_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_intrinsic.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated intrinsic handle directory"); + let binary = directory.path().join("nested_handle_intrinsic.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile intrinsic with a nested handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_indirect_aggregate_view_before_handler_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_indirect_aggregate_view.jett"); + let directory = tempfile::tempdir().expect("isolated aggregate view directory"); + let source = directory + .path() + .join("nested_handle_indirect_aggregate_view.jett"); + fs::copy(&fixture, &source).expect("copy unchanged indirect handler source"); + let expected = jett_driver::run_file_capture_output(&source).expect("interpreter oracle"); + assert_eq!(debug_print_text(&expected.debug_events), "5 8\n"); + let binary = directory + .path() + .join("nested_handle_indirect_aggregate_view.exe"); + build_host_executable(&source, launcher(), &binary) + .expect("compile indirect aggregate view before a handler"); + fs::remove_file(&source).expect("erase indirect handler source before native execution"); + assert!(!source.exists()); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_borrowed_stdlib_call_before_handler_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_borrowed_stdlib_call.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated borrowed stdlib directory"); + let binary = directory + .path() + .join("nested_handle_borrowed_stdlib_call.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile borrowed stdlib call before a handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_capability_view_before_handler_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_handle_capability_view.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("interpreter oracle"); + let directory = tempfile::tempdir().expect("isolated capability view directory"); + let binary = directory.path().join("nested_handle_capability_view.exe"); + build_host_executable(&fixture, launcher(), &binary) + .expect("compile capability view before a handler"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_reflected_interface_field_owners_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/reflected_interface_field_owners.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory + .path() + .join("reflected_interface_field_owners.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("reflected interface field owner oracle"); + assert_eq!( + expected.stdout, + concat!( + "direct:selected:small:7|nested:selected:small:7\n", + "record:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615\n", + "event:active:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7\n", + "mirror-event:mirror:selected:small:9|selected:small:9|small:9;nested:selected:small:9|selected:small:9|nested:selected:small:9|selected:small:9\n", + "state:active:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7\n", + "mirror-state:mirror:selected:small:9|selected:small:9|small:9;nested:selected:small:9|selected:small:9|nested:selected:small:9|selected:small:9\n", + "baked:\n", + "record:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615\n", + "event:active:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7\n", + "mirror-event:mirror:selected:small:9|selected:small:9|small:9;nested:selected:small:9|selected:small:9|nested:selected:small:9|selected:small:9\n", + "state:active:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7\n", + "mirror-state:mirror:selected:small:9|selected:small:9|small:9;nested:selected:small:9|selected:small:9|nested:selected:small:9|selected:small:9\n", + "reuse:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615=selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615=selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615\n", + "pending:selected:small:7|nested:selected:small:9|nested:selected:small:9\n", + "secret-direct:selected:small:7\n", + "secret-reflected:selected:small:7\n", + "secret-reuse:selected:small:7\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace copied: app.Named = pending(pending(7))", + "trace once: app.Named = pending(7)", + "trace copied: app.Named = pending(pending(9))", + "trace once: app.Named = pending(9)", + "trace copied: app.Named = pending(pending(9))", + "trace once: app.Named = pending(9)", + "trace ordinary: secret[app.Named] = [redacted]", + "trace reflected: secret[app.Named] = [redacted]", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("field_owners_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native reflected interface field owners"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("field_owners_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native reflected interface field verify suite"); + let property_binary = directory.path().join("field_owners_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native reflected interface field property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for suite in [verify_binary, property_binary] { + let actual = run_bounded(&suite, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_reflected_interface_field_failures_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/reflected_interface_field_owners.jett"); + let template = fs::read_to_string(&fixture).unwrap(); + let declarations = template.split("function main(").next().unwrap(); + for (operation, message) in [ + ( + "wrong_record_request", + "type.field_value: field 'item' has type 'app.Selected', requested 'int64'", + ), + ( + "wrong_event_member", + "type.variant_field_value: field metadata belongs to 'app.Event.active', expected 'app.Event.mirror'", + ), + ( + "pending_session_metadata", + concat!( + "type.machine_field_value: second argument must be TypeField, got pending(pending(", + "TypeField(index: 1, owner_type: app.Session, owner_member: some(active), name: item, ", + "type_name: app.Selected, kind: refinement, kind_tag: TypeKind.refinement_type, ", + "serialize_name: item, has_secret: false, ", + "type_info: TypeInfo(type_name: app.Selected, kind: refinement, ", + "kind_tag: TypeKind.refinement_type, primitive_tag: none, has_secret: false, ", + "args: list(TypeInfo(type_name: app.Named, kind: unknown, kind_tag: TypeKind.unknown_type, ", + "primitive_tag: none, has_secret: false, args: list()))))))", + ), + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("reflected_interface_failure.jett"); + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n string value = {operation}()\n Stdout.write(view stdout, value)\n return nothing\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("invalid reflected request or metadata must fail before dispatch"); + assert_eq!(expected.output.stdout, "before\n", "{operation}"); + assert!( + debug_trace_lines(&expected.output.debug_events).is_empty(), + "{operation}" + ); + assert_eq!(expected.message, format!("runtime error: {message}")); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("field_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{operation}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{operation}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{operation}: {actual:?}" + ); + } + } +} + +#[test] +fn native_interface_method_values_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_method_values.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("interface_method_values.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("interface method value oracle"); + assert_eq!( + expected.stdout, + concat!( + "owners:8:107:18:1008\n", + "slots:13:112:23:1013\n", + "coarsen:8\n", + "routes:8:8:8:8:8:8:8:8\n", + "adapted:18:18\n", + "alias:18\n", + "baked:8:8:8:8:8\n", + "receiver\nfactory\nordered:8\n", + "capability:9\n", + "reuse:9:8\n", + "collision:8:8:8\n", + ) + ); + assert_eq!(debug_trace_lines(&expected.debug_events).len(), 4); + for (event, method) in debug_trace_lines(&expected.debug_events).iter().zip([ + "callback_library.Reader.read", + "callback_library.Reader.offset", + "callback_library.Reader.read", + "callback_library.Absent.read", + ]) { + assert!(event.ends_with(&format!("= function({method})")), "{event}"); + } + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("method_values_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native interface method values"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("method_values_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native interface method value verify suite"); + let property_binary = directory.path().join("method_values_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native interface method value property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_interface_method_value_pending_receiver_matches_qualified_call() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_method_values.jett"); + let template = fs::read_to_string(&fixture).unwrap(); + let declarations = template.split("function main(").next().unwrap(); + for callee in ["callback", "library.Reader.read"] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("pending_method_receiver.jett"); + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n use callback_library as library\n library.Reader source = run 7\n function(view library.Reader) returns int64 callback = library.Reader.read\n Stdout.write(view stdout, \"before\\n\")\n int64 value = {callee}(view source)\n Stdout.write(view stdout, \"{{value}}\\n\")\n return nothing\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("pending receiver must fail before implementation dispatch"); + assert_eq!(expected.output.stdout, "before\n"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + assert_eq!( + expected.message, + "runtime error: undefined function 'callback_library.Reader.read'" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("pending_method_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native interface method pending receiver"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{callee}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{callee}: {actual:?}" + ); + } + } +} + +#[test] +fn native_borrowed_return_clone_controls_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/borrowed_return_clone_controls.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("borrowed_return_clone_controls.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("owned clone and implicit-copy return oracle"); + assert_eq!( + expected.stdout, + concat!( + "copies:3:4:3:3:before\n", + "owned:3:3:3:3:4:3\n", + "scalars:7:before\n", + "wrapped:2:2:2:2\n", + "baked:3:3:3:3:3:4:3:7:seed\n", + ) + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("borrowed_clones_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native owned clone and implicit-copy returns"); + binaries.push(binary); + } + let verify_binary = directory.path().join("borrowed_clones_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("native borrowed clone verify suite"); + let property_binary = directory.path().join("borrowed_clones_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("native borrowed clone property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_contextual_secret_constructor_failures_preserve_order_and_cleanup() { + let declarations = r#"namespace app +function first(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "first\n") + return list("owned") +function failing(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "failure\n") + return list.remove_at[string](list("other", "owner"), -1) +"#; + for (name, ty, constructor, output) in [ + ( + "list", + "secret[list[list[string]]]", + "list(first(view stdout), failing(view stdout))", + "before\nfirst\nfailure\n", + ), + ( + "map", + "secret[map[string, list[string]]]", + "map(\"first\": first(view stdout), \"second\": failing(view stdout))", + "before\nfirst\nfailure\n", + ), + ( + "some", + "secret[optional[list[string]]]", + "some(failing(view stdout))", + "before\nfailure\n", + ), + ( + "ok", + "secret[result[list[string], string]]", + "ok(failing(view stdout))", + "before\nfailure\n", + ), + ( + "fail", + "secret[result[int64, list[string]]]", + "fail(failing(view stdout))", + "before\nfailure\n", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + format!( + "{declarations}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n {ty} hidden = {constructor}\n Stdout.write(view stdout, \"must not run\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("payload fails before the secret constructor completes"); + assert_eq!(expected.output.stdout, output, "{name}: {expected:?}"); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native contextual secret constructor with terminal payload failure"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, output.as_bytes(), "{name}: {actual:?}"); + assert_eq!( + actual.stderr, + format!( + "{}{}\n", + jett_driver::render_debug_events(&expected.output.debug_events), + expected.message + ) + .as_bytes(), + "{name}: {actual:?}" + ); + } + } +} + +#[test] +fn native_contextual_secret_constructors_match_interpreter_in_both_profiles() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/contextual_secret_constructors.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("contextual secret constructor reference oracle"); + assert_eq!( + expected.stdout, + concat!( + "lists:list=3:-128:127:-1;empty=0\n", + "maps:map=2:127:-8;empty=0\n", + "sums:some:-128:none:ok:127:fail:-128\n", + "nested:twice=2:7:-8;children=7,-8,\n", + "owners:selected:small:7;record:8;|some:selected:small:9|ok:record:10|fail:selected:small:11|selected:small:12:record:13\n", + "children:refined=7,-8,;callbacks=-128:7\n", + "contexts:list=2:-8:7;empty=0;some:-128;fail:-8;map=-8;inline=some:127\n", + "baked:list=2:-128:127;some:-8;owners=selected:small:7;record:8;\n", + "observed:2:7:-8\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + ["trace observed: secret[list[int8]] = [redacted]"] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_values_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native contextual secret constructors"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("secret_values_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled secret constructor verify suite"); + let property_binary = directory.path().join("secret_values_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled secret constructor property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes()); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const WHOLE_MACHINE_REBINDING: &str = r#"namespace app +machine Session: + states: + empty + content(values: list[int64]) + transitions: + empty to content +function describe(view source: Session) returns string: + if source at content: + int64 count = list.length[int64](view source.values) + int64 first = list.get[int64](view source.values, 0) handle: + default -1 + return "content:{count}:{first}" + return "empty" +function report() returns string: + mutable Session source = Session(content, list(3, 5)) + string before = describe(view source) + source = Session(empty) + string blank = describe(view source) + Session at content precise = Session(content, list(7, 11, 13)) + source = clone precise + string copied = describe(view source) + string original = describe(view precise) + source = precise + string moved = describe(view source) + source = clone source + string cloned = describe(view source) + return "{before}|{blank}|{copied}|{original}|{moved}|{cloned}" +function guarded_rebind() returns string: + mutable Session source = Session(content, list(1)) + if source at content: + source = Session(content, list(19, 23)) + return describe(view source) + return "missing" +function cycle(count: int64) returns string: + mutable Session source = Session(empty) + mutable int64 cursor = 0 + while cursor < count: + source = Session(content, list(cursor, cursor + 1)) + source = Session(empty) + cursor = cursor + 1 + source = Session(content, list(31, 37)) + return describe(view source) +function pending_report() returns string: + mutable Session current = Session(content, list(1, 2)) + current = run run Session(content, list(41)) + trace current + Session once = join current handle error: + return error + trace once + Session ready = join once handle error: + return error + return describe(view ready) +function main(stdout: Stdout) returns nothing: + string baked = comptime report() + Stdout.write(view stdout, "moves:{report()}|{baked}\n") + Stdout.write(view stdout, "guard:{guarded_rebind()}\ncycle:{cycle(32)}\npending:{pending_report()}\n") +verify whole_machine_rebinding: + assert report() == comptime report() + assert guarded_rebind() == "content:2:19" + assert cycle(32) == "content:2:31" +property whole_machine_rebinding_trials: + given many: bool + mutable int64 count = 1 + if many: + count = 32 + assert cycle(count) == "content:2:31" +"#; + +#[test] +fn native_whole_machine_rebinding_preserves_owned_payloads_pending_depth_and_source_independence() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("machine_rebinding.jett"); + fs::write(&source, WHOLE_MACHINE_REBINDING).unwrap(); + let report = "content:2:3|empty|content:3:7|content:3:7|content:3:7|content:3:7"; + let stdout = format!( + "moves:{report}|{report}\nguard:content:2:19\ncycle:content:2:31\npending:content:1:41\n" + ); + let debug = [ + "trace current: app.Session = pending(pending(app.Session@content(list(41))))", + "trace once: app.Session = pending(app.Session@content(list(41)))", + ]; + let expected = + jett_driver::run_file_capture_outcome(&source).expect("selected whole-machine moves"); + assert_eq!(expected.stdout, stdout); + assert_eq!(debug_trace_lines(&expected.debug_events), debug); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("machine_rebinding_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native whole-machine moves"); + binaries.push((binary, release)); + } + let verify = directory.path().join("machine_rebinding_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join("machine_rebinding_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes()); + let stderr = if release { + String::new() + } else { + format!("{}\n", debug.join("\n")) + }; + assert_eq!(actual.stderr, stderr.as_bytes(), "{actual:?}"); + } + for binary in [verify, property] { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[path = "native_conformance/reflected_iteration_controls.rs"] +mod reflected_iteration_controls; + +#[path = "native_conformance/actor_field_updates.rs"] +mod actor_field_updates; + +#[path = "native_conformance/actor_required_values.rs"] +mod actor_required_values; + +#[path = "native_conformance/actor_direct_getter.rs"] +mod actor_direct_getter; + +#[path = "native_conformance/actor_temporary_owner_getter.rs"] +mod actor_temporary_owner_getter; diff --git a/crates/jett_driver/tests/native_conformance/actor_direct_getter.rs b/crates/jett_driver/tests/native_conformance/actor_direct_getter.rs new file mode 100644 index 00000000..5c1ad7a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_direct_getter.rs @@ -0,0 +1,142 @@ +//! The unchanged direct actor getter through ordinary measured launchers. +//! Output observations do not establish physical lifetime or reentry acceptance. +use super::*; + +const SOURCE: &[u8] = include_bytes!( + "../../../jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett" +); +const SOURCE_SHA256: &str = "a90ef2641c1a3937e02718368f699d4cf2c51d3f2b47ee5d15579a450aca4a92"; +const EVIDENCE_ENV: &str = "JETT_ACTOR_DIRECT_GETTER_NATIVE_EVIDENCE_V1"; + +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn artifact(path: &Path) -> serde_json::Value { + let bytes = fs::read(path).unwrap(); + serde_json::json!({ "path": path, "bytes": bytes.len(), "sha256": digest(&bytes) }) +} + +#[test] +#[ignore = "requires explicitly supplied measured Debug/Release normal runtime bundles"] +fn actor_handler_direct_getter_executes_unchanged_without_source_in_both_modes() { + assert_eq!(digest(SOURCE), SOURCE_SHA256); + assert!( + std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_none(), + "global executable/object retention must stay disabled" + ); + let evidence = std::path::PathBuf::from( + std::env::var_os(EVIDENCE_ENV).expect("fresh bounded actor direct getter evidence file"), + ); + assert!(evidence.is_absolute() && evidence.parent().unwrap().is_dir()); + assert!(!evidence.exists(), "never reuse a prior observation file"); + let mut rows = Vec::new(); + for release in [false, true] { + let profile = if release { "release" } else { "debug" }; + // A complete pair is mandatory. There is no Cargo or unmeasured archive + // fallback for this opt-in observation. + let launcher = measured_launcher::supplied(release) + .expect("explicitly supplied normal Debug/Release runtime bundle pair"); + let manifest_name = if release { + "JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST" + } else { + "JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST" + }; + let manifest = std::path::PathBuf::from(std::env::var_os(manifest_name).unwrap()); + let manifest_before = artifact(&manifest); + let archive_before = artifact(&launcher.archive_path); + let directory = tempfile::tempdir().unwrap(); + let directory_path = directory.path().to_path_buf(); + let source = directory.path().join("main.jett"); + fs::write(&source, SOURCE).unwrap(); + assert_eq!(fs::read(&source).unwrap(), SOURCE); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{profile} reference: {failure:?}")); + assert_eq!(reference.stdout, "observed:5\n"); + assert!(reference.debug_events.is_empty()); + assert!(reference.frontend_debug_observations.is_empty()); + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{profile} original object emission: {error}")); + assert_eq!(object.source().as_bytes(), SOURCE); + assert!(object.debug_observations().is_empty()); + let object_fact = serde_json::json!({ + "bytes": object.bytes().len(), "sha256": digest(object.bytes()), + "target": object.target(), "entry": object.program_entry().index(), + "symbols": object.symbols(), + }); + let binary = directory + .path() + .join(if cfg!(windows) { "actor.exe" } else { "actor" }); + let linked = jett_driver::native::link_host_object(&object, launcher, &binary) + .unwrap_or_else(|error| panic!("{profile} ordinary measured launcher link: {error}")); + assert!(linked.debug_observations.is_empty()); + assert_eq!(linked.target, launcher.target); + let executable_fact = artifact(&binary); + // The public linker owns and removes its temporary object/PDB directory. + // Keep only the linked executable in this isolated launch directory. + drop(object); + drop(linked); + drop(reference); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + let remaining = fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + assert_eq!(remaining, vec![binary.clone()]); + // The shared helper clears the environment, spools output to files, + // enforces its deadline and kills/waits in Drop even on panic. + let actual = run_bounded(&binary, directory.path()); + fs::remove_file(&binary).unwrap(); + let binary_removed = !binary.exists(); + directory.close().unwrap(); + let directory_removed = !directory_path.exists(); + assert_eq!(artifact(&manifest), manifest_before); + assert_eq!(artifact(&launcher.archive_path), archive_before); + rows.push(serde_json::json!({ + "runtime_profile": profile, "source_release_policy": release, + "source_sha256": SOURCE_SHA256, + "runtime_manifest": manifest_before, "runtime_archive": archive_before, + "runtime_abi_version": launcher.runtime_abi_version, + "runtime_target": launcher.target, "runtime_crt_mode": launcher.crt_mode.to_string(), + "object": object_fact, "executable": executable_fact, + "source_deleted_before_launch": true, + "isolated_directory_contained_only_executable_before_launch": true, + "object_host_value_dropped_before_launch": true, + "native_exit": actual.status.code(), "native_terminal_reaped": true, + "native_stdout_bytes": actual.stdout, "native_stderr_bytes": actual.stderr, + "executable_removed_after_reap": binary_removed, + "temporary_directory_removed": directory_removed, + "reference_policy": "debug/default interpreter; exact literal output also asserted", + "physical_lifetime_measured": false, "reentry_measured": false, + "full_native_acceptance": false, + })); + fs::write( + &evidence, + serde_json::to_vec_pretty(&serde_json::json!({ + "schema": "actor-direct-getter-native-execution-v1", "rows": rows, + "full_native_acceptance": false, + "scope": "unchanged whole Source paired ordinary native output observations only", + })) + .unwrap(), + ) + .unwrap(); + assert!(binary_removed && directory_removed); + assert_eq!(actual.status.code(), Some(0), "{profile}: {actual:?}"); + assert_eq!(actual.stdout, b"observed:5\n", "{profile}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{profile}: {actual:?}"); + eprintln!( + "Actor direct getter native observation: profile={profile}; source_deleted=true; exit=0; stdout=observed:5; stderr_bytes=0; terminal_reaped=true; executable_removed=true" + ); + } + assert_eq!(rows.len(), 2); + eprintln!( + "Actor direct getter native observations: 1 unchanged Source; 2 objects; 2 Source-deleted executions; profiles=debug,release; physical_lifetime_measured=false; reentry_measured=false; full_native_acceptance=false" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/actor_field_updates.rs b/crates/jett_driver/tests/native_conformance/actor_field_updates.rs new file mode 100644 index 00000000..a1173345 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_field_updates.rs @@ -0,0 +1,310 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use std::{fs, path::Path}; + +fn assert_main(name: &str, stdout: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native") + .join(name); + fs::copy(fixture, &source).unwrap(); + let checked = jett_driver::build_file(&source); + assert!(!checked.has_errors, "{name}: {:?}", checked.diagnostics); + let expected = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{name}: {}", failure.message)); + assert_eq!(expected.stdout, stdout, "{name}"); + assert!(expected.debug_events.is_empty(), "{name}: {expected:?}"); + assert!( + expected.frontend_debug_observations.is_empty(), + "{name}: {expected:?}" + ); + let binaries = [false, true].map(|release| { + let binary = directory.path().join(format!("actor-fields-{release}.exe")); + let artifact = jett_driver::native::build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + (binary, release) + }); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + stdout.as_bytes(), + "{name}, release={release}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_original_actor_handler_field_update_executes_without_source() { + assert_main( + "field_update_reference/24_actor_handler_future.jett", + "actor:1:9\n", + ); +} + +#[test] +fn native_actor_field_updates_preserve_owning_state_and_independent_instances_without_source() { + assert_main( + "actor_field_updates/01_nested_owners_independent_actors.jett", + "first:4:18:left!!:2|1:9:start:9|sender:2:7:left\nafter:4:18:left!!:2|2:7:right:9\n", + ); +} + +#[test] +fn native_actor_field_updates_preserve_message_and_helper_ownership_without_source() { + for (source, stdout) in [ + ( + "actor_field_updates/10_nonshadowing_message_and_helper.jett", + "message:110|1:initial|9:helper|210|9:helper\n", + ), + ( + "actor_field_updates/16_nonresponding_return_preserves_state.jett", + "void-return:7:changed|7:changed|9:selected|1:initial\n", + ), + ] { + assert_main(source, stdout); + } +} + +#[test] +fn native_actor_early_respond_abandons_field_commit_without_source() { + assert_main( + "actor_field_updates/03_early_respond_preserves_old_field.jett", + "early:4:4:9:9:9\n", + ); +} + +#[test] +fn native_actor_field_updates_execute_verify_and_property_suites_without_source() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/actor_field_updates/03_early_respond_preserves_old_field.jett"); + fs::copy(fixture, &source).unwrap(); + let reference = jett_driver::test_file(&source).expect("reference actor suites"); + assert_eq!( + (reference.total, reference.passed, reference.failed), + (2, 2, 0) + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let launcher = launcher_for_options(release); + let options = jett_driver::BuildOptions { release }; + let verify = directory.path().join(format!("actor-verify-{release}.exe")); + let property = directory + .path() + .join(format!("actor-property-{release}.exe")); + jett_driver::native::build_host_verify_suite_executable_with_options( + &source, launcher, &verify, options, + ) + .unwrap_or_else(|error| panic!("actor verify release={release}: {error}")); + jett_driver::native::build_host_property_suite_executable_with_options( + &source, launcher, &property, options, + ) + .unwrap_or_else(|error| panic!("actor property release={release}: {error}")); + binaries.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_actor_field_update_ownership_refusals_preserve_existing_publications() { + use jett_driver::native::{NativeBuildError, NativeLauncherBundle}; + for name in [ + "02_shadowed_message_and_ordinary_helper.jett", + "04_consumed_message_root_rejected.jett", + "05_persistent_state_not_restored_rejected.jett", + "06_view_message_root_rejected.jett", + "07_immutable_state_root_rejected.jett", + "08_live_whole_state_loan_rejected.jett", + "09_live_state_field_loan_rejected.jett", + "12_persistent_alias_owner_replacement_rejected.jett", + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/actor_field_updates") + .join(name), + &source, + ) + .unwrap(); + let output = directory.path().join("preserved.exe"); + let sentinel = b"previous checked actor publication"; + fs::write(&output, sentinel).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let assert_refusal = |error: NativeBuildError| { + let NativeBuildError::Lowering { source, .. } = error else { + panic!("{name}: expected Source rejection before archive lookup: {error}"); + }; + let jett_driver::BackendLoweringError::Build(built) = *source else { + panic!("{name}: expected checking rejection before HIR"); + }; + assert!(built.has_errors, "{name}"); + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(!errors.is_empty(), "{name}"); + if name == "02_shadowed_message_and_ordinary_helper.jett" { + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.code.code() == 201), + "{name}: {:?}", + built.diagnostics + ); + } else { + assert!( + errors + .iter() + .all(|diagnostic| (400..500).contains(&diagnostic.code.code())), + "{name}: {:?}", + built.diagnostics + ); + } + assert_eq!(fs::read(&output).unwrap(), sentinel, "{name}"); + }; + for release in [false, true] { + let options = jett_driver::BuildOptions { release }; + assert_refusal( + jett_driver::native::build_host_executable_with_options( + &source, &launcher, &output, options, + ) + .expect_err("invalid actor ownership prevents main publication"), + ); + assert_refusal( + jett_driver::native::build_host_verify_suite_executable_with_options( + &source, &launcher, &output, options, + ) + .expect_err("invalid actor ownership prevents verify publication"), + ); + assert_refusal( + jett_driver::native::build_host_property_suite_executable_with_options( + &source, &launcher, &output, options, + ) + .expect_err("invalid actor ownership prevents property publication"), + ); + } + } +} + +#[test] +fn native_actor_persistent_views_follow_exact_owned_state_without_source() { + assert_main( + "actor_field_updates/11_persistent_aliases_and_independent_field.jett", + "before:2:5:2:5:2|2:21:2:21:10\nafter:2:5:2:5:19|2:21:2:21:32\n", + ); +} + +#[test] +fn native_actor_respond_is_a_terminal_handle_exit_without_source() { + assert_main( + "actor_field_updates/13_respond_is_terminal_handle_exit.jett", + "early:4:4:9:9:9\n", + ); +} + +#[test] +fn native_actor_projected_persistent_views_replay_the_checked_path_without_source() { + assert_main( + "actor_field_updates/14_projected_persistent_alias.jett", + concat!( + "before-first:envelope-2:102:2:inner-2:outer-2:1002|2:inner-2|2:inner-2|2\n", + "before-second:envelope-7:107:7:inner-7:outer-7:1007|7:inner-7|7:inner-7|7\n", + "after-first:envelope-2:102:2:inner-2:outer-2:1002|2:inner-2|2:inner-2|13\n", + "after-second:envelope-7:107:7:inner-7:outer-7:1007|7:inner-7|7:inner-7|15\n", + ), + ); +} + +#[test] +fn native_existing_actor_receiver_handlers_keep_order_without_source_in_both_runtime_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/nested_actor_message_handle.jett"), + &source, + ) + .unwrap(); + let reference = + jett_driver::run_file_capture_outcome(&source).expect("existing handled actor receiver"); + assert!(reference.stdout.is_empty()); + let stderr = jett_driver::render_debug_events(&reference.debug_events); + assert_eq!( + super::debug_print_text(&reference.debug_events), + "text\ntext\nBefore\nBefore:Ada:Ada\nAda\ntext\ntext\nChanged\nBefore:Fallback:Fallback\nFallback\nAda\nFallback\n" + ); + let binaries = [false, true].map(|runtime_release| { + let binary = directory + .path() + .join(format!("actor-receiver-{runtime_release}.exe")); + // Both Sources keep Debug policy because the original program uses println. + jett_driver::native::build_host_executable( + &source, + launcher_for_options(runtime_release), + &binary, + ) + .unwrap_or_else(|error| { + panic!("existing actor receiver runtime_release={runtime_release}: {error}") + }); + (binary, runtime_release) + }); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, runtime_release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "runtime_release={runtime_release}: {actual:?}" + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "runtime_release={runtime_release}: {actual:?}" + ); + } +} + +#[test] +fn native_actor_owning_handle_reply_is_acquired_before_publication_without_source() { + assert_main( + "actor_field_updates/15_owning_handle_reply.jett", + "reply:2:5|1:9|3\n", + ); +} diff --git a/crates/jett_driver/tests/native_conformance/actor_identity.rs b/crates/jett_driver/tests/native_conformance/actor_identity.rs new file mode 100644 index 00000000..fe82edc6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_identity.rs @@ -0,0 +1,144 @@ +use super::*; + +fn fixture(name: &str) -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native") + .join(name) +} + +#[test] +fn native_actor_identity_original_and_nested_sources_match_current_runtime_profile_without_source() +{ + for (name, debug_text) in [ + ("actor_enum_equality.jett", "true false\n"), + ( + "actor_pending_alias_baseline.jett", + "actor#0 pending(actor#0)\nactor#0 actor#0\n", + ), + ( + "actor_pending_alias_identity.jett", + "true false\ntrue false false true false true\ntrue false 10 10\n", + ), + ( + "actor_direct_identity.jett", + "true false false true\ntrue false\n", + ), + ( + "actor_nested_identity.jett", + "true false false true\ntrue true\ntrue false\n", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture(name), &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("selected actor identity reference"); + assert!(expected.stdout.is_empty(), "{name}"); + assert_eq!( + debug_print_text(&expected.debug_events), + debug_text, + "{name}" + ); + assert!(debug_trace_lines(&expected.debug_events).is_empty()); + let binary = directory.path().join("actor.exe"); + // Original08 uses println, whose checked Source policy is debug. The + // Cargo test profile selects the matching normal runtime archive. + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes(), + "{name}" + ); + } +} + +#[test] +fn native_actor_identity_pending_direct_operations_keep_reference_errors_in_current_runtime_profile() + { + let template = fs::read_to_string(fixture("actor_pending_identity.jett")).unwrap(); + for (operator, label) in [("==", "Eq"), ("!=", "NotEq")] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + template.replace("pending == pending", &format!("pending {operator} pending")), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("direct pending actor equality remains unsupported"); + let message = format!( + "runtime error: unsupported binary operation: pending(actor#0) {label} pending(actor#0)" + ); + assert_eq!(expected.message, message); + assert!(expected.output.stdout.is_empty()); + assert!(debug_trace_lines(&expected.output.debug_events).is_empty()); + let binary = directory.path().join("actor_pending.exe"); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release: false }, + ) + .expect("native pending actor comparison"); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + format!("{message}\n").as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_actor_identity_production_output_matches_with_optimized_objects_without_source() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture("actor_optimized_identity.jett"), &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("production actor identity reference"); + assert_eq!( + expected.stdout, + concat!( + "true false false true\n", + "true false false true\n", + "true false false true false true\n", + "true false 10 10\n", + "true false\n", + ) + ); + assert!(expected.debug_events.is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("actor_identity_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native production actor identity"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/actor_required_values.rs b/crates/jett_driver/tests/native_conformance/actor_required_values.rs new file mode 100644 index 00000000..265ffdcb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_required_values.rs @@ -0,0 +1,142 @@ +//! The unchanged actor required callback through ordinary measured launchers. +//! Output observations do not establish physical lifetime or reentry acceptance. +use super::*; + +const SOURCE: &[u8] = include_bytes!( + "../../../jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett" +); +const SOURCE_SHA256: &str = "ce01b25e20a4955067e87fbc7389f1957b21fd10de6e76575e337f2f98111665"; +const EVIDENCE_ENV: &str = "JETT_ACTOR_REQUIRED_NATIVE_EVIDENCE_V1"; + +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn artifact(path: &Path) -> serde_json::Value { + let bytes = fs::read(path).unwrap(); + serde_json::json!({ "path": path, "bytes": bytes.len(), "sha256": digest(&bytes) }) +} + +#[test] +#[ignore = "requires explicitly supplied measured Debug/Release normal runtime bundles"] +fn actor_handler_required_callable_executes_unchanged_without_source_in_both_modes() { + assert_eq!(digest(SOURCE), SOURCE_SHA256); + assert!( + std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_none(), + "global executable/object retention must stay disabled" + ); + let evidence = std::path::PathBuf::from( + std::env::var_os(EVIDENCE_ENV).expect("fresh bounded actor required evidence file"), + ); + assert!(evidence.is_absolute() && evidence.parent().unwrap().is_dir()); + assert!(!evidence.exists(), "never reuse a prior observation file"); + let mut rows = Vec::new(); + for release in [false, true] { + let profile = if release { "release" } else { "debug" }; + // A complete pair is mandatory. There is no Cargo or unmeasured archive + // fallback for this opt-in observation. + let launcher = measured_launcher::supplied(release) + .expect("explicitly supplied normal Debug/Release runtime bundle pair"); + let manifest_name = if release { + "JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST" + } else { + "JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST" + }; + let manifest = std::path::PathBuf::from(std::env::var_os(manifest_name).unwrap()); + let manifest_before = artifact(&manifest); + let archive_before = artifact(&launcher.archive_path); + let directory = tempfile::tempdir().unwrap(); + let directory_path = directory.path().to_path_buf(); + let source = directory.path().join("main.jett"); + fs::write(&source, SOURCE).unwrap(); + assert_eq!(fs::read(&source).unwrap(), SOURCE); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{profile} reference: {failure:?}")); + assert_eq!(reference.stdout, "observed:5\n"); + assert!(reference.debug_events.is_empty()); + assert!(reference.frontend_debug_observations.is_empty()); + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{profile} original object emission: {error}")); + assert_eq!(object.source().as_bytes(), SOURCE); + assert!(object.debug_observations().is_empty()); + let object_fact = serde_json::json!({ + "bytes": object.bytes().len(), "sha256": digest(object.bytes()), + "target": object.target(), "entry": object.program_entry().index(), + "symbols": object.symbols(), + }); + let binary = directory + .path() + .join(if cfg!(windows) { "actor.exe" } else { "actor" }); + let linked = jett_driver::native::link_host_object(&object, launcher, &binary) + .unwrap_or_else(|error| panic!("{profile} ordinary measured launcher link: {error}")); + assert!(linked.debug_observations.is_empty()); + assert_eq!(linked.target, launcher.target); + let executable_fact = artifact(&binary); + // The public linker owns and removes its temporary object/PDB directory. + // Keep only the linked executable in this isolated launch directory. + drop(object); + drop(linked); + drop(reference); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + let remaining = fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + assert_eq!(remaining, vec![binary.clone()]); + // The shared helper clears the environment, spools output to files, + // enforces its deadline and kills/waits in Drop even on panic. + let actual = run_bounded(&binary, directory.path()); + fs::remove_file(&binary).unwrap(); + let binary_removed = !binary.exists(); + directory.close().unwrap(); + let directory_removed = !directory_path.exists(); + assert_eq!(artifact(&manifest), manifest_before); + assert_eq!(artifact(&launcher.archive_path), archive_before); + rows.push(serde_json::json!({ + "runtime_profile": profile, "source_release_policy": release, + "source_sha256": SOURCE_SHA256, + "runtime_manifest": manifest_before, "runtime_archive": archive_before, + "runtime_abi_version": launcher.runtime_abi_version, + "runtime_target": launcher.target, "runtime_crt_mode": launcher.crt_mode.to_string(), + "object": object_fact, "executable": executable_fact, + "source_deleted_before_launch": true, + "isolated_directory_contained_only_executable_before_launch": true, + "object_host_value_dropped_before_launch": true, + "native_exit": actual.status.code(), "native_terminal_reaped": true, + "native_stdout_bytes": actual.stdout, "native_stderr_bytes": actual.stderr, + "executable_removed_after_reap": binary_removed, + "temporary_directory_removed": directory_removed, + "reference_policy": "debug/default interpreter; exact literal output also asserted", + "physical_lifetime_measured": false, "reentry_measured": false, + "full_native_acceptance": false, + })); + fs::write( + &evidence, + serde_json::to_vec_pretty(&serde_json::json!({ + "schema": "actor-required-native-execution-v1", "rows": rows, + "full_native_acceptance": false, + "scope": "unchanged whole Source paired ordinary native output observations only", + })) + .unwrap(), + ) + .unwrap(); + assert!(binary_removed && directory_removed); + assert_eq!(actual.status.code(), Some(0), "{profile}: {actual:?}"); + assert_eq!(actual.stdout, b"observed:5\n", "{profile}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{profile}: {actual:?}"); + eprintln!( + "Actor required native observation: profile={profile}; source_deleted=true; exit=0; stdout=observed:5; stderr_bytes=0; terminal_reaped=true; executable_removed=true" + ); + } + assert_eq!(rows.len(), 2); + eprintln!( + "Actor required native observations: 1 unchanged Source; 2 objects; 2 Source-deleted executions; profiles=debug,release; physical_lifetime_measured=false; reentry_measured=false; full_native_acceptance=false" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/actor_temporary_owner_getter.rs b/crates/jett_driver/tests/native_conformance/actor_temporary_owner_getter.rs new file mode 100644 index 00000000..1094d3af --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/actor_temporary_owner_getter.rs @@ -0,0 +1,142 @@ +//! The unchanged actor getter with a retained Packet/list temporary owner. +//! Output observations do not establish physical lifetime or reentry acceptance. +use super::*; + +const SOURCE: &[u8] = include_bytes!( + "../../../jett_comptime/src/resource_execution/fixtures/actor_handler_temporary_owner_getter.jett" +); +const SOURCE_SHA256: &str = "a1f3fa32e14828daebb472f316608683b303187eba63b987edbab56d381a2f0f"; +const EVIDENCE_ENV: &str = "JETT_ACTOR_TEMPORARY_OWNER_NATIVE_EVIDENCE_V1"; + +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn artifact(path: &Path) -> serde_json::Value { + let bytes = fs::read(path).unwrap(); + serde_json::json!({ "path": path, "bytes": bytes.len(), "sha256": digest(&bytes) }) +} + +#[test] +#[ignore = "requires explicitly supplied measured Debug/Release normal runtime bundles"] +fn actor_handler_temporary_owner_getter_executes_unchanged_without_source_in_both_modes() { + assert_eq!(digest(SOURCE), SOURCE_SHA256); + assert!( + std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_none(), + "global executable/object retention must stay disabled" + ); + let evidence = std::path::PathBuf::from( + std::env::var_os(EVIDENCE_ENV).expect("fresh bounded actor temporary owner evidence file"), + ); + assert!(evidence.is_absolute() && evidence.parent().unwrap().is_dir()); + assert!(!evidence.exists(), "never reuse a prior observation file"); + let mut rows = Vec::new(); + for release in [false, true] { + let profile = if release { "release" } else { "debug" }; + // A complete pair is mandatory. There is no Cargo or unmeasured archive + // fallback for this opt-in observation. + let launcher = measured_launcher::supplied(release) + .expect("explicitly supplied normal Debug/Release runtime bundle pair"); + let manifest_name = if release { + "JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST" + } else { + "JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST" + }; + let manifest = std::path::PathBuf::from(std::env::var_os(manifest_name).unwrap()); + let manifest_before = artifact(&manifest); + let archive_before = artifact(&launcher.archive_path); + let directory = tempfile::tempdir().unwrap(); + let directory_path = directory.path().to_path_buf(); + let source = directory.path().join("main.jett"); + fs::write(&source, SOURCE).unwrap(); + assert_eq!(fs::read(&source).unwrap(), SOURCE); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{profile} reference: {failure:?}")); + assert_eq!(reference.stdout, "observed:5\n"); + assert!(reference.debug_events.is_empty()); + assert!(reference.frontend_debug_observations.is_empty()); + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{profile} original object emission: {error}")); + assert_eq!(object.source().as_bytes(), SOURCE); + assert!(object.debug_observations().is_empty()); + let object_fact = serde_json::json!({ + "bytes": object.bytes().len(), "sha256": digest(object.bytes()), + "target": object.target(), "entry": object.program_entry().index(), + "symbols": object.symbols(), + }); + let binary = directory + .path() + .join(if cfg!(windows) { "actor.exe" } else { "actor" }); + let linked = jett_driver::native::link_host_object(&object, launcher, &binary) + .unwrap_or_else(|error| panic!("{profile} ordinary measured launcher link: {error}")); + assert!(linked.debug_observations.is_empty()); + assert_eq!(linked.target, launcher.target); + let executable_fact = artifact(&binary); + // The public linker owns and removes its temporary object/PDB directory. + // Keep only the linked executable in this isolated launch directory. + drop(object); + drop(linked); + drop(reference); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + let remaining = fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + assert_eq!(remaining, vec![binary.clone()]); + // The shared helper clears the environment, spools output to files, + // enforces its deadline and kills/waits in Drop even on panic. + let actual = run_bounded(&binary, directory.path()); + fs::remove_file(&binary).unwrap(); + let binary_removed = !binary.exists(); + directory.close().unwrap(); + let directory_removed = !directory_path.exists(); + assert_eq!(artifact(&manifest), manifest_before); + assert_eq!(artifact(&launcher.archive_path), archive_before); + rows.push(serde_json::json!({ + "runtime_profile": profile, "source_release_policy": release, + "source_sha256": SOURCE_SHA256, + "runtime_manifest": manifest_before, "runtime_archive": archive_before, + "runtime_abi_version": launcher.runtime_abi_version, + "runtime_target": launcher.target, "runtime_crt_mode": launcher.crt_mode.to_string(), + "object": object_fact, "executable": executable_fact, + "source_deleted_before_launch": true, + "isolated_directory_contained_only_executable_before_launch": true, + "object_host_value_dropped_before_launch": true, + "native_exit": actual.status.code(), "native_terminal_reaped": true, + "native_stdout_bytes": actual.stdout, "native_stderr_bytes": actual.stderr, + "executable_removed_after_reap": binary_removed, + "temporary_directory_removed": directory_removed, + "reference_policy": "debug/default interpreter; exact literal output also asserted", + "physical_lifetime_measured": false, "reentry_measured": false, + "full_native_acceptance": false, + })); + fs::write( + &evidence, + serde_json::to_vec_pretty(&serde_json::json!({ + "schema": "actor-temporary-owner-native-execution-v1", "rows": rows, + "full_native_acceptance": false, + "scope": "unchanged whole Source paired ordinary native output observations only", + })) + .unwrap(), + ) + .unwrap(); + assert!(binary_removed && directory_removed); + assert_eq!(actual.status.code(), Some(0), "{profile}: {actual:?}"); + assert_eq!(actual.stdout, b"observed:5\n", "{profile}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{profile}: {actual:?}"); + eprintln!( + "Actor temporary owner getter native observation: profile={profile}; source_deleted=true; exit=0; stdout=observed:5; stderr_bytes=0; terminal_reaped=true; executable_removed=true" + ); + } + assert_eq!(rows.len(), 2); + eprintln!( + "Actor temporary owner getter native observations: 1 unchanged Source; 2 objects; 2 Source-deleted executions; profiles=debug,release; physical_lifetime_measured=false; reentry_measured=false; full_native_acceptance=false" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views.rs b/crates/jett_driver/tests/native_conformance/bitfield_local_views.rs new file mode 100644 index 00000000..cf66f098 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views.rs @@ -0,0 +1,183 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable_with_options; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +#[derive(Clone, Copy)] +enum BitfieldOutcome<'a> { + Success(&'a str), + RuntimeFailure { stdout: &'a str, message: &'a str }, +} + +fn run_case(name: &str, source_text: &str, expected_stdout: &str) { + run_case_with_traces( + name, + source_text, + BitfieldOutcome::Success(expected_stdout), + &[], + ); +} + +fn run_case_with_traces( + name: &str, + source_text: &str, + expected: BitfieldOutcome<'_>, + expected_traces: &[&str], +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, terminal_stderr) = match expected { + BitfieldOutcome::Success(stdout) => { + let output = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + BitfieldOutcome::RuntimeFailure { stdout, message } => { + let failure = run_file_capture_outcome(&source) + .expect_err("expected terminal bitfield receiver fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + let expected_events = expected_traces + .iter() + .map(|text| jett_driver::DebugEvent { + kind: jett_driver::DebugEventKind::Trace, + text: (*text).to_owned(), + }) + .collect::>(); + assert_eq!( + reference.debug_events, expected_events, + "{name}: {reference:?}" + ); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&reference.debug_events) + }; + let stderr = format!("{debug}{terminal_stderr}"); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_bitfield_local_views_bitfield_payload_local_view_matches_reference_without_source_in_both_profiles() + { + run_case( + "bitfield_payload_local_view", + include_str!("bitfield_local_views/bitfield_payload_local_view.jett"), + "2:3:2:4007ff", + ); +} + +#[test] +fn native_bitfield_local_views_bitfield_payload_owned_control_matches_reference_without_source_in_both_profiles() + { + run_case( + "bitfield_payload_owned_control", + include_str!("bitfield_local_views/bitfield_payload_owned_control.jett"), + "3:2:4007ff", + ); +} + +#[test] +fn native_bitfield_local_views_view_parameter_payload_keeps_original_owner_without_source_in_both_profiles() + { + run_case( + "view_parameter_payload", + include_str!("bitfield_local_views/01_view_parameter.jett"), + "view:2:2:3:2:4007ff\nowner:4:2\n", + ); +} + +#[test] +fn native_bitfield_local_views_envelope_path_keeps_alias_and_owner_reads_without_source_in_both_profiles() + { + run_case( + "envelope_path", + include_str!("bitfield_local_views/02_envelope_path.jett"), + "path:2:2:2:4:23:4007ff\nowner:4:2:23\n", + ); +} + +#[test] +fn native_bitfield_local_views_empty_payload_keeps_independent_clone_without_source_in_both_profiles() + { + run_case( + "empty_payload", + include_str!("bitfield_local_views/03_empty_payload.jett"), + "empty:0:0:1:0:00\nowner:0:0\n", + ); +} + +#[test] +fn native_bitfield_local_views_01_pending_whole_packet_matches_reference_without_source_in_both_profiles() + { + run_case_with_traces( + "01_pending_whole_packet", + include_str!("bitfield_local_views/01_pending_whole_packet.jett"), + BitfieldOutcome::RuntimeFailure { + stdout: "before:whole\n", + message: "runtime error: field access is not supported on pending(pending(app.Packet(version: 4, flags: 0, payload: list(7, 255))))", + }, + &[], + ); +} + +#[test] +fn native_bitfield_local_views_02_pending_payload_copies_matches_reference_without_source_in_both_profiles() + { + run_case_with_traces( + "02_pending_payload_copies", + include_str!("bitfield_local_views/02_pending_payload_copies.jett"), + BitfieldOutcome::Success("before:payload\nitems:3:2\n"), + &[ + "trace forwarded: list[uint8] = pending(pending(list(7, 255)))\n", + "trace once: list[uint8] = pending(list(7, 255))\n", + "trace forwarded: list[uint8] = pending(pending(list(7, 255)))\n", + "trace once: list[uint8] = pending(list(7, 255))\n", + "trace packet: app.Packet = app.Packet(version: 4, flags: 0, payload: pending(pending(list(7, 255))))\n", + ], + ); +} diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_pending_whole_packet.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_pending_whole_packet.jett new file mode 100644 index 00000000..aec2c1f3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_pending_whole_packet.jett @@ -0,0 +1,15 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + Packet packet = run run Packet(version: 4, flags: 0, payload: list(first, second)) + Stdout.write(view stdout, "before:whole\n") + list[uint8] borrowed = view packet.payload + Stdout.write(view stdout, "after:borrow\n") + list[uint8] forwarded = borrowed + Stdout.write(view stdout, "after:length:{list.length[uint8](view forwarded)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_view_parameter.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_view_parameter.jett new file mode 100644 index 00000000..526ffea6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/01_view_parameter.jett @@ -0,0 +1,21 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function describe(view source: Packet) returns string: + list[uint8] borrowed = view source.payload + list[uint8] forwarded = borrowed + mutable list[uint8] copied = clone forwarded + uint8 added = 9 + copied = list.append[uint8](copied, added) + bytes encoded = Packet.to_bytes(clone source) + return "{list.length(view borrowed)}:{list.length(view forwarded)}:{list.length(view copied)}:{list.length(view source.payload)}:{bytes.to_hex(view encoded)}" +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + Packet packet = Packet(version: 4, flags: 0, payload: list(first, second)) + string observed = describe(view packet) + Stdout.write(view stdout, "view:{observed}\n") + Stdout.write(view stdout, "owner:{packet.version}:{list.length(view packet.payload)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_envelope_path.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_envelope_path.jett new file mode 100644 index 00000000..39d4d057 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_envelope_path.jett @@ -0,0 +1,26 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +struct Envelope: + packet: Packet + stamp: int64 +function payload_count(view values: list[uint8]) returns int64: + return list.length(view values) +function describe(view source: Envelope) returns string: + Packet projected = view source.packet + list[uint8] borrowed = view projected.payload + list[uint8] forwarded = borrowed + list[uint8] mixed = view source.packet.payload + bytes encoded = Packet.to_bytes(clone projected) + return "{payload_count(view forwarded)}:{payload_count(view mixed)}:{list.length(view source.packet.payload)}:{projected.version}:{source.stamp}:{bytes.to_hex(view encoded)}" +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + Packet packet = Packet(version: 4, flags: 0, payload: list(first, second)) + Envelope envelope = Envelope(packet: packet, stamp: 23) + string observed = describe(view envelope) + Stdout.write(view stdout, "path:{observed}\n") + Stdout.write(view stdout, "owner:{envelope.packet.version}:{list.length(view envelope.packet.payload)}:{envelope.stamp}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_pending_payload_copies.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_pending_payload_copies.jett new file mode 100644 index 00000000..23aa70e7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/02_pending_payload_copies.jett @@ -0,0 +1,31 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function ready_payload(view packet: Packet) returns result[list[uint8], string]: + list[uint8] borrowed = view packet.payload + list[uint8] forwarded = borrowed + trace forwarded + list[uint8] once = join clone forwarded handle error: + return fail(error) + trace once + list[uint8] ready = join once handle error: + return fail(error) + return ok(ready) +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + uint8 added = 9 + Packet packet = Packet(version: 4, flags: 0, payload: run run list(first, second)) + Stdout.write(view stdout, "before:payload\n") + list[uint8] ready_copy = ready_payload(view packet) handle error: + Stdout.write(view stdout, "join-error:{error}\n") + return nothing + list[uint8] original_again = ready_payload(view packet) handle error: + Stdout.write(view stdout, "join-error:{error}\n") + return nothing + list[uint8] expanded = list.append[uint8](ready_copy, added) + trace packet + Stdout.write(view stdout, "items:{list.length[uint8](view expanded)}:{list.length[uint8](view original_again)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/03_empty_payload.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/03_empty_payload.jett new file mode 100644 index 00000000..0455eeb6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/03_empty_payload.jett @@ -0,0 +1,19 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function describe(view source: Packet) returns string: + list[uint8] borrowed = view source.payload + list[uint8] forwarded = borrowed + mutable list[uint8] copied = clone forwarded + uint8 added = 9 + copied = list.append[uint8](copied, added) + bytes encoded = Packet.to_bytes(clone source) + return "{list.length(view borrowed)}:{list.length(view forwarded)}:{list.length(view copied)}:{list.length(view source.payload)}:{bytes.to_hex(view encoded)}" +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(version: 0, flags: 0, payload: list.new[uint8]()) + string observed = describe(view packet) + Stdout.write(view stdout, "empty:{observed}\n") + Stdout.write(view stdout, "owner:{packet.version}:{list.length(view packet.payload)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_local_view.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_local_view.jett new file mode 100644 index 00000000..e3dc2a1f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_local_view.jett @@ -0,0 +1,17 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + uint8 added = 9 + Packet packet = Packet(version: 4, flags: 0, payload: list(first, second)) + list[uint8] borrowed = view packet.payload + list[uint8] forwarded = borrowed + mutable list[uint8] copied = clone forwarded + copied = list.append[uint8](copied, added) + bytes encoded = Packet.to_bytes(clone packet) + Stdout.write(view stdout, "{list.length(view forwarded)}:{list.length(view copied)}:{list.length(view packet.payload)}:{bytes.to_hex(view encoded)}") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_owned_control.jett b/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_owned_control.jett new file mode 100644 index 00000000..883e76c2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/bitfield_local_views/bitfield_payload_owned_control.jett @@ -0,0 +1,15 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + uint8 added = 9 + Packet packet = Packet(version: 4, flags: 0, payload: list(first, second)) + list[uint8] copied = clone packet.payload + list[uint8] expanded = list.append[uint8](copied, added) + bytes encoded = Packet.to_bytes(clone packet) + Stdout.write(view stdout, "{list.length(view expanded)}:{list.length(view packet.payload)}:{bytes.to_hex(view encoded)}") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership.rs b/crates/jett_driver/tests/native_conformance/call_ownership.rs new file mode 100644 index 00000000..4260cdb7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership.rs @@ -0,0 +1,291 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use std::fs; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +fn run_case(name: &str, source_text: &str, expected: ExpectedOutcome) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, terminal_stderr) = match expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(&source) + .expect_err("expected terminal caller-ownership fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = jett_driver::native::build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + terminal_stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_call_ownership_direct_bare_view_argument_relinquishes_the_owner() { + run_case( + "01_direct_last_use", + include_str!("call_ownership/01_direct_last_use.jett"), + ExpectedOutcome::Success("direct:2\n"), + ); +} + +#[test] +fn native_call_ownership_indirect_bare_view_argument_relinquishes_the_owner() { + run_case( + "02_indirect_last_use", + include_str!("call_ownership/02_indirect_last_use.jett"), + ExpectedOutcome::Success("indirect:2\n"), + ); +} + +#[test] +fn native_call_ownership_named_view_arguments_follow_the_checked_permutation() { + run_case( + "03_named_last_use", + include_str!("call_ownership/03_named_last_use.jett"), + ExpectedOutcome::Success("named:5\n"), + ); +} + +#[test] +fn native_call_ownership_pipeline_bare_view_argument_relinquishes_the_owner() { + run_case( + "04_pipeline_last_use", + include_str!("call_ownership/04_pipeline_last_use.jett"), + ExpectedOutcome::Success("pipeline:2\n"), + ); +} + +#[test] +fn native_call_ownership_written_views_and_explicit_clone_preserve_the_owner() { + run_case( + "05_explicit_retention", + include_str!("call_ownership/05_explicit_retention.jett"), + ExpectedOutcome::Success("retained:2:2:2:2:2:2\n"), + ); +} + +#[test] +fn native_call_ownership_source_first_owned_parameter_is_transferred() { + run_case( + "06_source_first_owned_transfer", + include_str!("call_ownership/06_source_first_owned_transfer.jett"), + ExpectedOutcome::Success("transfer:5:5:2\n"), + ); +} + +#[test] +fn native_call_ownership_relinquished_owner_survives_a_later_default_handler() { + run_case( + "07_later_handler_default", + include_str!("call_ownership/07_later_handler_default.jett"), + ExpectedOutcome::Success("before:default\nlater:default\nanswer:5\n"), + ); +} + +#[test] +fn native_call_ownership_aborted_call_cleans_owner_before_independent_owned_return() { + run_case( + "08_early_return_cleanup", + include_str!("call_ownership/08_early_return_cleanup.jett"), + ExpectedOutcome::Success("before:return\nlater:return\nreturned:3\n"), + ); +} + +#[test] +fn native_call_ownership_later_failure_cleans_staged_and_live_owners() { + run_case( + "09_terminal_failure_cleanup", + include_str!("call_ownership/09_terminal_failure_cleanup.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:cleanup\nfailure:cleanup\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_call_ownership_copyable_scalars_and_noncopyable_representations_are_distinct() { + run_case( + "10_scalar_refinement_function", + include_str!("call_ownership/10_scalar_refinement_function.jett"), + ExpectedOutcome::Success("typed:7:7:7:4\n"), + ); +} + +#[test] +fn native_call_ownership_named_owned_producers_are_evaluated_once_in_source_order() { + run_case( + "11_named_producer_order", + include_str!("call_ownership/11_named_producer_order.jett"), + ExpectedOutcome::Success("source:right\nsource:left\nordered:5\n"), + ); +} + +#[test] +fn native_call_ownership_consumed_binding_rereads_fail_before_native_publication() { + let cases = [ + ( + "negative_direct_reread", + include_str!("call_ownership/negative_direct_reread.jett"), + ), + ( + "negative_parenthesized_reread", + include_str!("call_ownership/negative_parenthesized_reread.jett"), + ), + ( + "negative_indirect_reread", + include_str!("call_ownership/negative_indirect_reread.jett"), + ), + ( + "negative_named_owned_reread", + include_str!("call_ownership/negative_named_owned_reread.jett"), + ), + ( + "negative_pipeline_reread", + include_str!("call_ownership/negative_pipeline_reread.jett"), + ), + ( + "negative_refinement_reread", + include_str!("call_ownership/negative_refinement_reread.jett"), + ), + ( + "negative_function_reread", + include_str!("call_ownership/negative_function_reread.jett"), + ), + ]; + for (name, source_text) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let launcher = if cfg!(windows) { + jett_driver::native::NativeLauncherBundle::windows_msvc_static_v1( + directory.path().join("unused.lib"), + ) + } else { + jett_driver::native::NativeLauncherBundle::linux_gnu_v1( + directory.path().join("unused.a"), + ) + }; + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing caller ownership publication"; + fs::write(&output, sentinel).unwrap(); + assert!(!launcher.archive_path.exists()); + for release in [false, true] { + let error = jett_driver::native::build_host_executable_with_options( + &source, + &launcher, + &output, + jett_driver::BuildOptions { release }, + ) + .expect_err("consumed binding must fail before runtime archive lookup"); + let build = error.build_result().expect("typed frontend failure"); + assert!(build.has_errors, "{name}, release={release}"); + let errors = build + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{name}, release={release}: {errors:?}"); + assert_eq!(errors[0].code.code(), 400, "{name}, release={release}"); + assert!(error.debug_observations().is_empty()); + assert!(build.debug_observations.is_empty()); + assert!(!launcher.archive_path.exists()); + assert_eq!(fs::read(&output).unwrap(), sentinel); + } + } +} + +#[test] +fn native_call_ownership_present_success_keeps_its_payload_after_pruning() { + run_case( + "12_inferred_present_success", + include_str!("call_ownership/12_inferred_present_success.jett"), + ExpectedOutcome::Success("before:success\nafter:success\ncount:2\n"), + ); +} + +#[test] +fn native_call_ownership_inhabited_error_control_keeps_the_original_branch() { + run_case( + "13_inhabited_error_control", + include_str!("call_ownership/13_inhabited_error_control.jett"), + ExpectedOutcome::Success("before:success\nafter:success\ncount:2\n"), + ); +} + +#[test] +fn native_call_ownership_present_success_still_rejects_pending_before_extraction() { + run_case( + "14_pending_present_success", + include_str!("call_ownership/14_pending_present_success.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:success\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(ok(list(1, 2))))", + }, + ); +} + +#[test] +fn native_call_ownership_converted_handled_view_reads_its_input_and_owns_the_box() { + run_case( + "15_converted_handled_view", + include_str!("call_ownership/15_converted_handled_view.jett"), + ExpectedOutcome::Success( + "source\ndefault:source\nlater\ndefault:later\nabsent:item:1:3\nsource\nlater\npresent:item:2:11\n", + ), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett new file mode 100644 index 00000000..b3882295 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/01_direct_last_use.jett @@ -0,0 +1,10 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 answer = call_probe.count(values) + Stdout.write(view stdout, "direct:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett new file mode 100644 index 00000000..18b5244b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/02_indirect_last_use.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 answer = callback(values) + Stdout.write(view stdout, "indirect:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett new file mode 100644 index 00000000..43458d97 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/03_named_last_use.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function pair(view left: list[int64], view right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1, 2) + list[int64] right = list(3, 4, 5) + int64 answer = call_probe.pair(right: right, left: left) + Stdout.write(view stdout, "named:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett b/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett new file mode 100644 index 00000000..01a4ccd7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/04_pipeline_last_use.jett @@ -0,0 +1,10 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 answer = values into call_probe.count() + Stdout.write(view stdout, "pipeline:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett b/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett new file mode 100644 index 00000000..aba23424 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/05_explicit_retention.jett @@ -0,0 +1,17 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + use call_probe as p + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 direct = call_probe.count(view values) + int64 aliased = p.count(view values) + int64 indirect = callback(view values) + int64 piped = values into view p.count() + int64 copied = call_probe.count(clone values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "retained:{direct}:{aliased}:{indirect}:{piped}:{copied}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett b/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett new file mode 100644 index 00000000..2f003e5a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/06_source_first_owned_transfer.jett @@ -0,0 +1,12 @@ +namespace call_probe +export function mix(view left: list[int64], right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](right) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1, 2) + list[int64] right = list(3, 4, 5) + int64 answer = call_probe.mix(right: right, left: view left) + int64 retained = call_probe.mix(right: list(6, 7, 8), left: view left) + Stdout.write(view stdout, "transfer:{answer}:{retained}:{list.length[int64](view left)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett b/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett new file mode 100644 index 00000000..52159e37 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/07_later_handler_default.jett @@ -0,0 +1,15 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later:default\n") + return 3 +function main(stdout: Stdout) returns nothing: + list[int64] values = list(1, 2) + optional[int64] absent = none + Stdout.write(view stdout, "before:default\n") + int64 answer = with_extra(values, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "answer:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett b/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett new file mode 100644 index 00000000..0fb21f2a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/08_early_return_cleanup.jett @@ -0,0 +1,14 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function choose(values: list[int64], backup: list[int64], incoming: optional[int64], view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "before:return\n") + int64 answer = with_extra(values, incoming handle: + Stdout.write(view stdout, "later:return\n") + return backup + ) + return list(answer) +function main(stdout: Stdout) returns nothing: + list[int64] selected = choose(list(1, 2), list(3, 4, 5), none, view stdout) + Stdout.write(view stdout, "returned:{list.length[int64](selected)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett b/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett new file mode 100644 index 00000000..9d4609dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/09_terminal_failure_cleanup.jett @@ -0,0 +1,18 @@ +namespace app +function with_extra(view values: list[int64], extra: int64) returns int64: + return list.length[int64](view values) + extra +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:cleanup\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes payload = bytes.from_string("cleanup") + list[string] retained = list("owner", "kept") + list[int64] values = list(1, 2) + optional[int64] absent = none + Stdout.write(view stdout, "before:cleanup\n") + int64 answer = with_extra(values, absent handle: + default failing(view stdout) + ) + Stdout.write(view stdout, "after:{answer}:{bytes.to_hex(view payload)}:{list.length[string](view retained)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett b/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett new file mode 100644 index 00000000..0d481fce --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/10_scalar_refinement_function.jett @@ -0,0 +1,21 @@ +namespace app +type Positive = int64 where value > 0 +function scalar(view value: int64) returns int64: + return value +function refined(view value: Positive) returns int64: + return coarsen clone value +function increment(value: int64) returns int64: + return value + 1 +function apply(view callback: function(int64) returns int64, value: int64) returns int64: + return callback(value) +function report(number: int64) returns string: + Positive established = number handle error: + return error + int64 nominal = refined(established) + function(int64) returns int64 callback = increment + int64 invoked = apply(callback, 3) + int64 copied = scalar(number) + return "{copied}:{number}:{nominal}:{invoked}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "typed:{report(7)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett b/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett new file mode 100644 index 00000000..c834df12 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/11_named_producer_order.jett @@ -0,0 +1,13 @@ +namespace app +function pair(view left: list[int64], view right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +function make_right(view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "source:right\n") + return list(3, 4, 5) +function make_left(view stdout: Stdout) returns list[int64]: + Stdout.write(view stdout, "source:left\n") + return list(1, 2) +function main(stdout: Stdout) returns nothing: + int64 answer = pair(right: make_right(view stdout), left: make_left(view stdout)) + Stdout.write(view stdout, "ordered:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett b/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett new file mode 100644 index 00000000..450f1c43 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/12_inferred_present_success.jett @@ -0,0 +1,15 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + int64 answer = measure(ok(list(1, 2)), view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett b/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett new file mode 100644 index 00000000..61f08d32 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/13_inhabited_error_control.jett @@ -0,0 +1,16 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + result[list[int64], string] outcome = ok(list(1, 2)) + int64 answer = measure(outcome, view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett b/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett new file mode 100644 index 00000000..12f8d854 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/14_pending_present_success.jett @@ -0,0 +1,15 @@ +namespace app +function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +function measure[E](outcome: result[list[int64], E], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "before:success\n") + int64 answer = count(outcome handle error: + Stdout.write(view stdout, "failure:success\n") + return 0 + ) + Stdout.write(view stdout, "after:success\n") + return answer +function main(stdout: Stdout) returns nothing: + int64 answer = measure(run run ok(list(1, 2)), view stdout) + Stdout.write(view stdout, "count:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett b/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett new file mode 100644 index 00000000..ac71ab24 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/15_converted_handled_view.jett @@ -0,0 +1,34 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + values: list[int64] +implement Named for Item: + function name(view self: Item) returns string: + return "item:{list.length[int64](view self.values)}" +function read(view value: Named, extra: int64) returns string: + return "{Named.name(view value)}:{extra}" +function make(view stdout: Stdout, present: bool) returns optional[Item]: + Stdout.write(view stdout, "source\n") + if present: + return some(Item(values: list(2, 3))) + return none +function later(view stdout: Stdout, present: bool) returns optional[int64]: + Stdout.write(view stdout, "later\n") + if present: + return some(11) + return none +function exercise(view stdout: Stdout, present: bool) returns string: + return read(view(make(view stdout, present) handle: + Stdout.write(view stdout, "default:source\n") + default Item(values: list(7)) + ), later(view stdout, present) handle: + Stdout.write(view stdout, "default:later\n") + default 3 + ) +function main(stdout: Stdout) returns nothing: + string absent = exercise(view stdout, false) + Stdout.write(view stdout, "absent:{absent}\n") + string present = exercise(view stdout, true) + Stdout.write(view stdout, "present:{present}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett new file mode 100644 index 00000000..3f2a4631 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_direct_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = call_probe.count(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "qualified:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett new file mode 100644 index 00000000..4cbd5d98 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_function_reread.jett @@ -0,0 +1,11 @@ +namespace app +function increment(value: int64) returns int64: + return value + 1 +function apply(view callback: function(int64) returns int64, value: int64) returns int64: + return callback(value) +function main(stdout: Stdout) returns nothing: + function(int64) returns int64 callback = increment + int64 before = apply(callback, 3) + int64 after = apply(view callback, 5) + Stdout.write(view stdout, "{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett new file mode 100644 index 00000000..28cb6ff4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_indirect_reread.jett @@ -0,0 +1,12 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + function(view list[int64]) returns int64 callback = call_probe.count + int64 before = callback(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "indirect:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett new file mode 100644 index 00000000..c35a36f2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_named_owned_reread.jett @@ -0,0 +1,14 @@ +namespace call_probe +export function mix(view left: list[int64], right: list[int64]) returns int64: + return list.length[int64](view left) + list.length[int64](view right) +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] left = list(1) + list[int64] right = list(2, 3) + int64 before = call_probe.mix(right: right, left: left) + int64 after = call_probe.count(view right) + Stdout.write(view stdout, "named-owned:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett new file mode 100644 index 00000000..5b287af7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_parenthesized_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = (call_probe.count)(values) + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "parenthesized:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett new file mode 100644 index 00000000..f7f1fdd3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_pipeline_reread.jett @@ -0,0 +1,11 @@ +namespace call_probe +export function count(view values: list[int64]) returns int64: + return list.length[int64](view values) +namespace app +function main(stdout: Stdout) returns nothing: + use call_probe + list[int64] values = list(1, 2) + int64 before = values into call_probe.count() + int64 after = call_probe.count(view values) + Stdout.write(view stdout, "pipeline-qualified:{before}:{after}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett b/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett new file mode 100644 index 00000000..bb992045 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/call_ownership/negative_refinement_reread.jett @@ -0,0 +1,13 @@ +namespace app +type Positive = int64 where value > 0 +function refined(view value: Positive) returns int64: + return coarsen clone value +function report(number: int64) returns string: + Positive established = number handle error: + return error + int64 before = refined(established) + int64 after = refined(view established) + return "{before}:{after}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report(7)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/debug_event_transport.rs b/crates/jett_driver/tests/native_conformance/debug_event_transport.rs new file mode 100644 index 00000000..4758b4bb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/debug_event_transport.rs @@ -0,0 +1,234 @@ +use super::{launcher, run_bounded}; +use jett_driver::native::{ + build_host_executable, build_host_executable_with_options, + build_host_property_suite_executable, build_host_verify_suite_executable, +}; +use jett_driver::{ + BuildOptions, DebugEvent, DebugEventKind, DebugObservation, DebugPhase, render_debug_events, + run_file_capture_outcome, +}; +use std::fs; + +fn event(kind: DebugEventKind, text: &str) -> DebugEvent { + DebugEvent { + kind, + text: text.into(), + } +} + +const MIXED_OBSERVATIONS: &str = r#"namespace app +function build_observe() returns int64: + print("compile:") + println("built") + return 7 +function observations() returns nothing: + int64 marker = 42 + print() + print("partial:") + trace marker + println() + println("trace fake\nbreakpoint hit\noutput,lambda:λ🙂\\\r") + breakpoint true + print("tail") +function main(stdout: Stdout) returns nothing: + int64 baked = comptime build_observe() + Stdout.write(view stdout, "application:{baked}") + observations() +verify checked: + println("frontend:") + assert true +"#; + +#[test] +fn native_debug_events_preserve_exact_boundaries_and_exclude_frontend_capture() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("mixed.jett"); + fs::write(&source, MIXED_OBSERVATIONS).unwrap(); + let reference = run_file_capture_outcome(&source).unwrap(); + assert_eq!(reference.stdout, "application:7"); + assert_eq!( + reference.frontend_debug_observations, + [ + DebugObservation { + phase: DebugPhase::Comptime, + event: event(DebugEventKind::Print, "compile:"), + }, + DebugObservation { + phase: DebugPhase::Comptime, + event: event(DebugEventKind::Println, "built\n"), + }, + DebugObservation { + phase: DebugPhase::FrontendVerify, + event: event(DebugEventKind::Println, "frontend:\n"), + }, + ], + ); + assert_eq!( + reference.debug_events, + [ + event(DebugEventKind::Print, ""), + event(DebugEventKind::Print, "partial:"), + event(DebugEventKind::Trace, "trace marker: int64 = 42\n"), + event(DebugEventKind::Println, "\n"), + event( + DebugEventKind::Println, + "trace fake\nbreakpoint hit\noutput,lambda:λ🙂\\\r\n", + ), + event( + DebugEventKind::Breakpoint, + "breakpoint hit: marker: int64 = 42\n" + ), + event(DebugEventKind::Print, "tail"), + ], + ); + let binary = directory.path().join("mixed.exe"); + let artifact = build_host_executable(&source, launcher(), &binary).unwrap(); + assert_eq!( + artifact.debug_observations, + reference.frontend_debug_observations + ); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, reference.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&reference.debug_events).as_bytes() + ); +} + +const ARGUMENT_FAILURE: &str = r#"namespace app +function argument() returns list[string]: + print("argument:") + return list("held") +function failed() returns int64: + println("failing") + string rejected = string.repeat("ab", 9223372036854775807) + return string.char_count(rejected) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "application-before") + print("before:") + println(argument(), failed()) + println("unreachable") +"#; + +#[test] +fn native_debug_argument_failure_keeps_prior_events_and_retires_outer_owners() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("arguments.jett"); + fs::write(&source, ARGUMENT_FAILURE).unwrap(); + let failure = run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(failure.output.stdout, "application-before"); + assert!(failure.output.frontend_debug_observations.is_empty()); + assert_eq!( + failure.output.debug_events, + [ + event(DebugEventKind::Print, "before:"), + event(DebugEventKind::Print, "argument:"), + event(DebugEventKind::Println, "failing\n"), + ], + ); + assert_eq!( + failure.message, + "runtime error: string.repeat: requested output is too large" + ); + let binary = directory.path().join("arguments.exe"); + build_host_executable(&source, launcher(), &binary).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + // A leaked outer argument owner would replace entry status 71 with 72. + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, failure.output.stdout.as_bytes()); + let expected = format!( + "{}{}\n", + render_debug_events(&failure.output.debug_events), + failure.message + ); + assert_eq!(actual.stderr, expected.as_bytes()); +} + +#[test] +fn native_partial_debug_text_is_adjacent_to_terminal_error_without_inserted_newline() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("partial_failure.jett"); + fs::write( + &source, + "namespace app\nfunction main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"application\")\n print(\"unterminated:\")\n list[string] retained = list(\"owner\")\n string rejected = string.repeat(\"ab\", 9223372036854775807)\n", + ).unwrap(); + let failure = run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(failure.output.stdout, "application"); + assert_eq!( + failure.output.debug_events, + [event(DebugEventKind::Print, "unterminated:")] + ); + let binary = directory.path().join("partial_failure.exe"); + build_host_executable(&source, launcher(), &binary).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, b"application"); + assert_eq!( + actual.stderr, + format!("unterminated:{}\n", failure.message).as_bytes() + ); +} + +#[test] +fn native_compiled_suites_emit_only_their_actual_debug_calls_after_source_deletion() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("suites.jett"); + fs::write( + &source, + "namespace app\nfunction main() returns nothing:\n return nothing\nverify diagnostic:\n print(\"verify:\")\n println(\"tail\")\n assert true\nproperty diagnostic_trials:\n given chosen: bool\n print(\"trial:\")\n println(chosen)\n assert chosen == chosen\n", + ).unwrap(); + let verify = directory.path().join("verify.exe"); + let property = directory.path().join("property.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + fs::remove_file(&source).unwrap(); + for (binary, diagnostic) in [ + (verify, "verify:tail\n".to_owned()), + (property, "trial:true\ntrial:false\n".repeat(50)), + ] { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, diagnostic.as_bytes()); + } +} + +#[test] +fn native_print_release_rejection_keeps_existing_publication_and_checks_arguments() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("release_print.jett"); + let binary = directory.path().join("preserved.exe"); + let sentinel = b"existing native publication"; + fs::write(&binary, sentinel).unwrap(); + fs::write( + &source, + "namespace app\nfunction main() returns nothing:\n println(1 + true)\n", + ) + .unwrap(); + let result = jett_driver::build_file_with_options(&source, BuildOptions { release: true }); + assert!(result.has_errors); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 362) + ); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 301) + ); + build_host_executable_with_options( + &source, + launcher(), + &binary, + BuildOptions { release: true }, + ) + .expect_err("release source and its arguments must be checked before publication"); + assert_eq!(fs::read(&binary).unwrap(), sentinel); +} diff --git a/crates/jett_driver/tests/native_conformance/display_results.rs b/crates/jett_driver/tests/native_conformance/display_results.rs new file mode 100644 index 00000000..d5aff2c9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/display_results.rs @@ -0,0 +1,297 @@ +use super::*; + +const MINIMAL_PENDING_RESULT: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Message: + text: string +implement Displayable for Message: + function display(view self: Message) returns string: + return run self.text +function main(stdout: Stdout) returns nothing: + Message value = Message(text: "shown") + Stdout.write(view stdout, "before\n") + Stdout.write(view stdout, "value:{value}\n") +"#; + +const ORDERED_FAILURE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +__DECLARATIONS__ +function prefix(view stdout: Stdout) returns string: + list[string] allocated = list("earlier", "owned") + Stdout.write(view stdout, "prefix\n") + return "allocated:{list.length(view allocated)}" +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later\n") + return none +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "handler\n") + return 7 +function main(stdout: Stdout) returns nothing: + bytes payload = bytes.from_string("retained-payload") + list[string] retained = list("kept", "owned") + __VALUE_DECLARATION__ + Stdout.write(view stdout, "before\n") + string message = "{prefix(view stdout)}:{value}:{later(view stdout) handle: default fallback(view stdout)}" + Stdout.write(view stdout, "value:{message}\n") + Stdout.write(view stdout, "after:{bytes.to_hex(view payload)}:{list.length(view retained)}\n") +"#; + +fn message_declarations(body: &str) -> String { + format!( + "struct Message:\n text: string\nimplement Displayable for Message:\n function display(view self: Message) returns string:\n{body}\n" + ) +} + +fn ordered_failure_source(declarations: &str, value: &str) -> String { + ORDERED_FAILURE + .replace("__DECLARATIONS__", declarations) + .replace("__VALUE_DECLARATION__", value) +} + +fn assert_display_failure_in_both_profiles( + name: &str, + text: &str, + stdout: &str, + message: &str, + debug: &[&str], +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, text).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("implicit display must fail before later interpolation effects"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert_eq!(expected.message, message, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + debug, + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("display_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&expected.output.debug_events) + }; + stderr.push_str(message); + stderr.push('\n'); + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_pending_display_result_fails_in_both_profiles() { + assert_display_failure_in_both_profiles( + "pending_result", + MINIMAL_PENDING_RESULT, + "before\n", + "runtime error: Displayable.display returned pending(shown) instead of string", + &[], + ); +} + +#[test] +fn native_display_result_failures_preserve_depth_order_and_cleanup() { + let cases = [ + ( + "double_unicode", + message_declarations( + " string shown = self.text\n trace shown\n return run run shown", + ), + "Message value = Message(text: \"shown-λ🙂\")", + "runtime error: Displayable.display returned pending(pending(shown-λ🙂)) instead of string", + vec!["trace shown: string = shown-λ🙂"], + ), + ( + "partial_join", + message_declarations( + " string nested = run run self.text\n return join nested handle error:\n return error", + ), + "Message value = Message(text: \"shown-λ🙂\")", + "runtime error: Displayable.display returned pending(shown-λ🙂) instead of string", + vec![], + ), + ( + "empty", + message_declarations(" return run self.text"), + "Message value = Message(text: \"\")", + "runtime error: Displayable.display returned pending() instead of string", + vec![], + ), + ( + "primitive_owner", + "implement Displayable for bool:\n function display(view self: bool) returns string:\n return run \"primitive-λ\"\n".to_owned(), + "bool value = true", + "runtime error: Displayable.display returned pending(primitive-λ) instead of string", + vec![], + ), + ( + "interface_owner", + "interface Named:\n function name(view self: Named) returns string\nstruct Message:\n text: string\nimplement Named for Message:\n function name(view self: Message) returns string:\n return self.text\nimplement Displayable for Named:\n function display(view self: Named) returns string:\n string text = Named.name(view self)\n return run text\n".to_owned(), + "Named value = Message(text: \"erased\")", + "runtime error: Displayable.display returned pending(erased) instead of string", + vec![], + ), + ( + "method_failure", + message_declarations( + " list[string] owned = list(\"method-owned\")\n list[string] removed = list.remove_at[string](owned, -1)\n return run self.text", + ), + "Message value = Message(text: \"unreached\")", + "runtime error: list.__remove_at: index -1 out of bounds", + vec![], + ), + ]; + for (name, declarations, value, message, debug) in cases { + assert_display_failure_in_both_profiles( + name, + &ordered_failure_source(&declarations, value), + "before\nprefix\n", + message, + &debug, + ); + } +} + +const READY_AND_EXPLICIT_CONTROLS: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace messages +export struct Pending: + text: string +implement Displayable for Pending: + function display(view self: Pending) returns string: + return run run self.text +export struct Joined: + text: string +implement Displayable for Joined: + function display(view self: Joined) returns string: + string nested = run run self.text + string once = join nested handle error: + return error + return join once handle error: + return error +export function format[T](view value: T) returns string: + return "{value}" +export function baked() returns string: + Joined value = Joined(text: "shown-λ🙂") + return "{value}" +namespace app +function main(stdout: Stdout) returns nothing: + use messages + messages.Pending pending_owner = messages.Pending(text: "shown-λ🙂") + string pending = messages.Pending.display(view pending_owner) + trace pending + Stdout.write(view stdout, "explicit-pending:{pending}\n") + string once = join pending handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + trace once + Stdout.write(view stdout, "explicit-partial:{once}\n") + string ready = join once handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + trace ready + Stdout.write(view stdout, "explicit:{ready}\n") + messages.Joined joined_owner = messages.Joined(text: "shown-λ🙂") + Stdout.write(view stdout, "implicit:{joined_owner}\n") + string generic = messages.format[messages.Joined](view joined_owner) + Stdout.write(view stdout, "generic:{generic}\n") + Stdout.write(view stdout, "owners:{pending_owner.text}:{joined_owner.text}\n") + string baked = comptime messages.baked() + Stdout.write(view stdout, "comptime:{baked}\n") +verify joined_display: + use messages + assert messages.baked() == "shown-λ🙂" + string baked = comptime messages.baked() + assert baked == "shown-λ🙂" +property joined_display_repeats: + given seed: int64 + use messages + assert messages.baked() == "shown-λ🙂" + assert seed == seed +"#; + +#[test] +fn native_joined_display_and_explicit_pending_calls_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, READY_AND_EXPLICIT_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("fully joined display and ordinary explicit calls remain valid"); + assert_eq!( + expected.stdout, + concat!( + "explicit-pending:pending(pending(shown-λ🙂))\n", + "explicit-partial:pending(shown-λ🙂)\n", + "explicit:shown-λ🙂\n", + "implicit:shown-λ🙂\n", + "generic:shown-λ🙂\n", + "owners:shown-λ🙂:shown-λ🙂\n", + "comptime:shown-λ🙂\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace pending: string = pending(pending(shown-λ🙂))", + "trace once: string = pending(shown-λ🙂)", + "trace ready: string = shown-λ🙂", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("joined_display_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native implicit joined display and explicit pending-string call"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("joined_display_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled joined display verify suite"); + let property_binary = directory.path().join("joined_display_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled joined display property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/equality_results.rs b/crates/jett_driver/tests/native_conformance/equality_results.rs new file mode 100644 index 00000000..0753bfa8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/equality_results.rs @@ -0,0 +1,390 @@ +use super::*; + +const EQUALITY_ERROR: &str = "runtime error: Equatable.equals must return bool"; + +const MINIMAL_PENDING_RESULT: &str = r#"namespace app +struct Item: + id: int64 +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return run true +function main(stdout: Stdout) returns nothing: + Item first = Item(id: 7) + Item second = Item(id: 7) + Stdout.write(view stdout, "before\n") + bool same = first __OPERATOR__ second + Stdout.write(view stdout, "value:{same}\n") +"#; + +const ORDERED_FAILURE: &str = r#"namespace app +struct Item: + answer: bool + label: string +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: +__METHOD_BODY__ +function operand(view stdout: Stdout, label: string, answer: bool) returns Item: + list[string] allocated = list(label, "owned") + Stdout.write(view stdout, "{label}\n") + return Item(answer: answer, label: "{label}:{list.length(view allocated)}") +function later(view stdout: Stdout) returns optional[bool]: + Stdout.write(view stdout, "later\n") + return none +function fallback(view stdout: Stdout) returns bool: + Stdout.write(view stdout, "handler\n") + return true +function short_effect(view stdout: Stdout) returns bool: + Stdout.write(view stdout, "short\n") + return true +function main(stdout: Stdout) returns nothing: + bytes payload = bytes.from_string("earlier-owned-payload") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") +__COMPARISON__ + Stdout.write(view stdout, "after:{bytes.to_hex(view payload)}:{list.length(view retained)}\n") +"#; + +fn ordered_failure_source(body: &str, comparison: &str) -> String { + ORDERED_FAILURE + .replace("__METHOD_BODY__", body) + .replace("__COMPARISON__", comparison) +} + +fn interpolation_comparison(operator: &str, answer: &str) -> String { + r#" string report = "{operand(view stdout, "left", __ANSWER__) __OPERATOR__ operand(view stdout, "right", true)}:{later(view stdout) handle: default fallback(view stdout)}" + Stdout.write(view stdout, "value:{report}\n")"# + .replace("__ANSWER__", answer) + .replace("__OPERATOR__", operator) +} + +fn assert_equality_failure_in_both_profiles( + name: &str, + text: &str, + stdout: &str, + message: &str, + debug: &[&str], +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, text).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).expect_err( + "implicit equality rejects a pending method result or preserves an earlier error", + ); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert_eq!(expected.message, message, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + debug, + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("equality_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&expected.output.debug_events) + }; + stderr.push_str(message); + stderr.push('\n'); + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_pending_equality_results_fail_for_both_operators_in_both_profiles() { + for operator in ["==", "!="] { + assert_equality_failure_in_both_profiles( + operator, + &MINIMAL_PENDING_RESULT.replace("__OPERATOR__", operator), + "before\n", + EQUALITY_ERROR, + &[], + ); + } +} + +#[test] +fn native_equality_result_failures_preserve_depth_order_and_cleanup() { + for operator in ["==", "!="] { + for (depth, body) in [ + ( + "one", + " bool answer = self.answer\n trace answer\n return run answer", + ), + ( + "two", + " bool answer = self.answer\n trace answer\n return run run answer", + ), + ] { + for answer in ["true", "false"] { + let name = format!("{operator}_{depth}_{answer}"); + let trace = if answer == "true" { + "trace answer: bool = true" + } else { + "trace answer: bool = false" + }; + assert_equality_failure_in_both_profiles( + &name, + &ordered_failure_source(body, &interpolation_comparison(operator, answer)), + "before\nleft\nright\n", + EQUALITY_ERROR, + &[trace], + ); + } + } + assert_equality_failure_in_both_profiles( + &format!("{operator}_partial_join"), + &ordered_failure_source( + " bool nested = run run self.answer\n return join nested handle error:\n return false", + &interpolation_comparison(operator, "true"), + ), + "before\nleft\nright\n", + EQUALITY_ERROR, + &[], + ); + assert_equality_failure_in_both_profiles( + &format!("{operator}_method_failure"), + &ordered_failure_source( + " list[string] owned = list(\"method-owned\")\n list[string] removed = list.remove_at[string](owned, -1)\n return run self.answer", + &interpolation_comparison(operator, "true"), + ), + "before\nleft\nright\n", + "runtime error: list.__remove_at: index -1 out of bounds", + &[], + ); + } + + for (operator, answer, short_operator) in [("==", "true", "and"), ("!=", "false", "or")] { + let comparison = r#" bool value = (operand(view stdout, "left", __ANSWER__) __OPERATOR__ operand(view stdout, "right", true)) __SHORT_OPERATOR__ short_effect(view stdout) + Stdout.write(view stdout, "value:{value}\n")"# + .replace("__ANSWER__", answer) + .replace("__OPERATOR__", operator) + .replace("__SHORT_OPERATOR__", short_operator); + assert_equality_failure_in_both_profiles( + &format!("{operator}_short_circuit"), + &ordered_failure_source(" return run self.answer", &comparison), + "before\nleft\nright\n", + EQUALITY_ERROR, + &[], + ); + } + + for (operator, answer) in [("==", "true"), ("!=", "false")] { + let comparison = r#" Item first_source = operand(view stdout, "left", true) + secret[Item] first = first_source + Item second_source = operand(view stdout, "right", true) + secret[Item] second = second_source + bool value = declassify(first __OPERATOR__ second) + Stdout.write(view stdout, "value:{value}:{later(view stdout) handle: default fallback(view stdout)}\n")"# + .replace("__OPERATOR__", operator); + let body = format!(" return run {answer}"); + assert_equality_failure_in_both_profiles( + &format!("{operator}_secret_operands"), + &ordered_failure_source(&body, &comparison), + "before\nleft\nright\n", + EQUALITY_ERROR, + &[], + ); + } +} + +const READY_AND_EXPLICIT_CONTROLS: &str = r#"namespace models +export struct Item: + id: int64 + label: string +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return run run(self.id == other.id) +export struct Ready: + id: int64 +implement Equatable for Ready: + function equals(view self: Ready, view other: Ready) returns bool: + return self.id == other.id +export struct Joined: + id: int64 +implement Equatable for Joined: + function equals(view self: Joined, view other: Joined) returns bool: + bool nested = run run(self.id == other.id) + bool once = join nested handle error: + return false + return join once handle error: + return false +export struct Holder: + item: Ready +export function equal[T](view first: T, view second: T) returns bool: + return first == second +export function reflected(view first: Holder, view second: Holder) returns bool: + mutable bool same = true + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field left = type.field_value[Holder, Field](view first, view field) + Field right = type.field_value[Holder, Field](view second, view field) + same = same and(left == right) + return same +export function ready_report() returns string: + Ready first = Ready(id: 7) + Ready same = Ready(id: 7) + Ready other = Ready(id: 9) + return "{first == same}:{first == other}:{first != other}:{first != same}" +export function joined_report() returns string: + Joined first = Joined(id: 7) + Joined same = Joined(id: 7) + Joined other = Joined(id: 9) + return "{first == same}:{first == other}:{first != other}:{first != same}" +export function generic_report() returns string: + Ready first = Ready(id: 7) + Ready same = Ready(id: 7) + Joined joined = Joined(id: 7) + Joined matching = Joined(id: 7) + return "{equal[Ready](view first, view same)}:{equal[Joined](view joined, view matching)}" +export function reflection_report() returns string: + Holder first = Holder(item: Ready(id: 7)) + Holder same = Holder(item: Ready(id: 7)) + Holder other = Holder(item: Ready(id: 9)) + return "{reflected(view first, view same)}:{reflected(view first, view other)}" +export function baked() returns bool: + Ready first = Ready(id: 7) + Ready same = Ready(id: 7) + Joined joined = Joined(id: 7) + Joined matching = Joined(id: 7) + return first == same and joined == matching +namespace app +function explicit_calls(view stdout: Stdout) returns nothing: + use models + models.Item first = models.Item(id: 7, label: "first") + models.Item same = models.Item(id: 7, label: "same") + models.Item other = models.Item(id: 9, label: "other") + bool pending_same = models.Item.equals(view first, view same) + bool pending_other = models.Item.equals(view first, view other) + trace pending_same + trace pending_other + Stdout.write(view stdout, "explicit-pending:{pending_same}:{pending_other}\n") + bool once_same = join pending_same handle error: + return nothing + bool once_other = join pending_other handle error: + return nothing + trace once_same + trace once_other + Stdout.write(view stdout, "explicit-partial:{once_same}:{once_other}\n") + bool ready_same = join once_same handle error: + return nothing + bool ready_other = join once_other handle error: + return nothing + trace ready_same + trace ready_other + Stdout.write(view stdout, "explicit-ready:{ready_same}:{ready_other}\n") + Stdout.write(view stdout, "owners:{first.id}:{same.id}:{other.id}:{first.label}:{same.label}:{other.label}\n") +function main(stdout: Stdout) returns nothing: + use models + explicit_calls(view stdout) + Stdout.write(view stdout, "ready:{models.ready_report()}\n") + Stdout.write(view stdout, "joined:{models.joined_report()}\n") + Stdout.write(view stdout, "generic:{models.generic_report()}\n") + Stdout.write(view stdout, "reflected:{models.reflection_report()}\n") + bool baked = comptime models.baked() + Stdout.write(view stdout, "comptime:{baked}\n") +verify equality_controls: + use models + assert models.ready_report() == "true:false:true:false" + assert models.joined_report() == "true:false:true:false" + assert models.generic_report() == "true:true" + assert models.reflection_report() == "true:false" + bool baked = comptime models.baked() + assert baked +property equality_controls_repeat: + given seed: int64 + use models + assert models.ready_report() == "true:false:true:false" + assert models.joined_report() == "true:false:true:false" + assert models.generic_report() == "true:true" + assert models.reflection_report() == "true:false" + assert models.baked() + assert seed == seed +"#; + +#[test] +fn native_ready_equality_and_explicit_pending_calls_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, READY_AND_EXPLICIT_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("ready implicit equality and ordinary explicit pending-bool calls remain valid"); + assert_eq!( + expected.stdout, + concat!( + "explicit-pending:pending(pending(true)):pending(pending(false))\n", + "explicit-partial:pending(true):pending(false)\n", + "explicit-ready:true:false\n", + "owners:7:7:9:first:same:other\n", + "ready:true:false:true:false\n", + "joined:true:false:true:false\n", + "generic:true:true\n", + "reflected:true:false\n", + "comptime:true\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace pending_same: bool = pending(pending(true))", + "trace pending_other: bool = pending(pending(false))", + "trace once_same: bool = pending(true)", + "trace once_other: bool = pending(false)", + "trace ready_same: bool = true", + "trace ready_other: bool = false", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("ready_equality_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native implicit ready equality and explicit pending-bool calls"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("equality_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled equality verify suite"); + let property_binary = directory.path().join("equality_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled equality property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs b/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs new file mode 100644 index 00000000..85298cbc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/erased_same_owner_arguments.rs @@ -0,0 +1,317 @@ +use super::*; + +const ORIGINAL_WRONG: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/05_erased_other_owner.jett" +)); +const ORIGINAL_MATCHING: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/erased_same_owner/12_erased_matching_owner.jett" +)); +const OWNER_FAILURE: &str = "runtime error: interface dispatch concrete argument owner mismatch"; + +fn matching_case(name: &str, source_text: &[u8], debug: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + debug + ); + let executable = directory.path().join(format!("{name}.exe")); + build_host_executable(&source, launcher(), &executable).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&executable, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty()); + assert_eq!(actual.stderr, debug.as_bytes()); +} + +fn mismatch_case(name: &str, source_text: &[u8], prefix: &str) { + failure_case(name, source_text, prefix, OWNER_FAILURE); +} + +fn failure_case(name: &str, source_text: &[u8], prefix: &str, message: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source).unwrap_err(); + assert_eq!(expected.message, message); + assert!(expected.output.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.output.debug_events), + prefix + ); + let executable = directory.path().join(format!("{name}.exe")); + build_host_executable(&source, launcher(), &executable).unwrap(); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&executable, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert!(actual.stdout.is_empty()); + assert_eq!(actual.stderr, format!("{prefix}{message}\n").as_bytes()); +} + +#[test] +fn native_erased_owner_original_wrong_source_rejects_before_implementation() { + mismatch_case("original_wrong_owner", ORIGINAL_WRONG, ""); +} + +#[test] +fn native_erased_owner_original_matching_source_preserves_view_dispatch() { + matching_case( + "original_matching_owner", + ORIGINAL_MATCHING, + "left:one:two\n", + ); +} + +#[test] +fn native_erased_owner_direct_and_callback_preserve_argument_order_before_failure() { + let original = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("\r\n", "\n"); + let source = original.replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ).replace( + "function main() returns nothing:", + "function receiver() returns Pairable:\n println(\"receiver\")\n return Left(name: \"one\")\nfunction other_value() returns Pairable:\n println(\"other\")\n return Right(name: \"two\")\nfunction main() returns nothing:", + ).replace( + "Pairable first = Left(name: \"one\")\n Pairable second = Right(name: \"two\")", + "Pairable first = receiver()\n Pairable second = other_value()", + ); + for callback in [false, true] { + let source = if callback { + source.replace( + "println(Pairable.combine(view first, view second))", + "function(view Pairable, view Pairable) returns string callback = Pairable.combine\n println(callback(view first, view second))", + ) + } else { + source.clone() + }; + mismatch_case( + &format!("ordered_callback_{callback}"), + source.as_bytes(), + "receiver\nother\n", + ); + } +} + +#[test] +fn native_erased_owner_owned_arguments_reject_before_body() { + let source = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("view self:", "self:") + .replace("view other:", "other:") + .replace( + "Pairable.combine(view first, view second)", + "Pairable.combine(first, second)", + ) + .replace( + "return \"left:{self.name}:{other.name}\"", + "println(\"entered-left-body\")\n return \"left:{self.name}:{other.name}\"", + ); + mismatch_case("owned_wrong_owner", source.as_bytes(), ""); +} + +const COLLECTION_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for list[optional[Left]]: + function combine(view self: list[optional[Left]], view other: list[optional[Left]]) returns string: + println("entered-left-body") + return "left" +implement Pairable for list[optional[Right]]: + function combine(view self: list[optional[Right]], view other: list[optional[Right]]) returns string: + return "right" +function main() returns nothing: + Pairable first = list(some(Left(name: "one"))) + Pairable second = list(some(OTHER_OWNER(name: "two"))) + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_nested_collection_identity_requires_exact_owner() { + mismatch_case( + "nested_wrong_owner", + COLLECTION_OWNER.replace("OTHER_OWNER", "Right").as_bytes(), + "", + ); + matching_case( + "nested_matching_owner", + COLLECTION_OWNER.replace("OTHER_OWNER", "Left").as_bytes(), + "entered-left-body\nleft\n", + ); +} + +#[test] +fn native_erased_owner_interface_children_keep_their_declared_erased_contract() { + let source = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view others: list[Pairable]) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for Left: + function combine(view self: Left, view others: list[Pairable]) returns string: + return self.name +implement Pairable for Right: + function combine(view self: Right, view others: list[Pairable]) returns string: + return self.name +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Right(name: "two") + list[Pairable] others = list(second) + println(Pairable.combine(view first, view others)) + return nothing +"#; + matching_case("retained_erased_children", source.as_bytes(), "one\n"); +} + +#[test] +fn native_erased_owner_prior_argument_failure_keeps_its_message_and_order() { + let original = std::str::from_utf8(ORIGINAL_WRONG) + .unwrap() + .replace("\r\n", "\n"); + let source = original.replace( + "function main() returns nothing:", + "function other_value() returns Pairable:\n println(\"other\")\n string rejected = string.repeat(\"ab\", 9223372036854775807)\n return Right(name: rejected)\nfunction main() returns nothing:", + ).replace("Pairable second = Right(name: \"two\")", "Pairable second = other_value()"); + failure_case( + "prior_argument_failure", + source.as_bytes(), + "other\n", + "runtime error: string.repeat: requested output is too large", + ); +} + +const PRIMITIVE_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +implement Pairable for int8: + function combine(view self: int8, view other: int8) returns string: + println("entered-int8-body") + return "int8" +implement Pairable for int64: + function combine(view self: int64, view other: int64) returns string: + return "int64" +function main() returns nothing: + int8 narrow = 7 + OTHER_TYPE second_value = 9 + Pairable first = narrow + Pairable second = second_value + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_equal_primitive_carriers_keep_checked_widths() { + mismatch_case( + "wrong_primitive_width", + PRIMITIVE_OWNER.replace("OTHER_TYPE", "int64").as_bytes(), + "", + ); + matching_case( + "matching_primitive_width", + PRIMITIVE_OWNER.replace("OTHER_TYPE", "int8").as_bytes(), + "entered-int8-body\nint8\n", + ); +} + +const GENERIC_OWNER: &str = r#"namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Box[T]: + item: T +implement Pairable for Box[int64]: + function combine(view self: Box[int64], view other: Box[int64]) returns string: + println("entered-int-box-body") + return "integer" +implement Pairable for Box[string]: + function combine(view self: Box[string], view other: Box[string]) returns string: + return "text" +function main() returns nothing: + Pairable first = Box[int64](item: 7) + Pairable second = OTHER_BOX + println(Pairable.combine(view first, view second)) + return nothing +"#; + +#[test] +fn native_erased_owner_nominal_generic_arguments_are_not_layout_casts() { + mismatch_case( + "wrong_generic_owner", + GENERIC_OWNER + .replace("OTHER_BOX", "Box[string](item: \"nine\")") + .as_bytes(), + "", + ); + matching_case( + "matching_generic_owner", + GENERIC_OWNER + .replace("OTHER_BOX", "Box[int64](item: 9)") + .as_bytes(), + "entered-int-box-body\ninteger\n", + ); +} + +#[test] +fn native_erased_owner_production_dispatch_matches_with_optimized_objects_without_source() { + for (name, original, status, output) in [ + ("matching", ORIGINAL_MATCHING, 0, b"left:one:two".as_slice()), + ("mismatched", ORIGINAL_WRONG, 71, b"".as_slice()), + ] { + let original = std::str::from_utf8(original).unwrap().replace("\r\n", "\n"); + assert!(original.contains("function main() returns nothing:")); + assert!(original.contains("println(Pairable.combine(view first, view second))")); + let production = original + .replace( + "function main() returns nothing:", + "function main(stdout: Stdout) returns nothing:", + ) + .replace( + "println(Pairable.combine(view first, view second))", + "Stdout.write(view stdout, Pairable.combine(view first, view second))", + ); + let directory = tempfile::tempdir().unwrap(); + let source = directory + .path() + .join(format!("production_owner_{name}.jett")); + fs::write(&source, production).unwrap(); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("production_owner_{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap(); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(status), "{actual:?}"); + assert_eq!(actual.stdout, output, "{actual:?}"); + let error = if status == 0 { + String::new() + } else { + format!("{OWNER_FAILURE}\n") + }; + assert_eq!(actual.stderr, error.as_bytes(), "{actual:?}"); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/field_updates.rs b/crates/jett_driver/tests/native_conformance/field_updates.rs new file mode 100644 index 00000000..910b0440 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/field_updates.rs @@ -0,0 +1,256 @@ +use super::*; + +fn fixture(name: &str) -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/field_update_reference") + .join(name) +} + +fn assert_main(name: &str, stdout: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture(name), &source).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!(expected.stdout, stdout, "{name}"); + assert!(expected.debug_events.is_empty(), "{name}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("fields_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + suite_options::launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_owned_field_updates_preserve_values_and_clone_independence_without_source() { + for (name, stdout) in [ + ( + "01_called_baselines.jett", + "original:1:before\nchanged:9:after\n", + ), + ("02_owning_clone_fields.jett", "2:1:before:3:3:after:true\n"), + ( + "03_rhs_order_and_early_return.jett", + "rhs\nchanged:9:before\nunchanged:1:before\n", + ), + ("04_exact_machine_state.jett", "machine:8:after:true\n"), + ( + "07_pending_children_transport.jett", + "children:17:5:again\n", + ), + ("09_generic_alias.jett", "1:9:number:new:text\n"), + ("10_refined_field_replacement.jett", "2:stable\n"), + ("21_guarded_field_control.jett", "guarded:4\n"), + ( + "32_borrowed_match_owner_preservation.jett", + "matches:6:6:6:6:6:6:6\n", + ), + ("22_branch_loop_lifetimes.jett", "5:first:5:second\n"), + ("23_required_comptime.jett", "baked:9\n"), + ( + "25_secret_owned_root.jett", + "secret:1:before:2|9:after:8|1:before:2\n", + ), + ("26_for_loop_update.jett", "loop:6:1:stable\n"), + ("27_nested_handler_update.jett", "handler:1:7:stable\n"), + ( + "28_metadata_holder_replacement.jett", + "metadata:string:stable\n", + ), + ( + "29_inline_owned_parameter.jett", + "inline:1:stable|7:changed\n", + ), + ( + "30_returned_inline_owned_parameter.jett", + "returned:1:8:stable\n", + ), + ( + "31_reflected_iteration_update.jett", + "reflected:6:int64|string|int64|:1:stable\n", + ), + ] { + assert_main(name, stdout); + } +} + +#[test] +fn native_field_update_refinement_and_bitfield_barriers_preserve_existing_publications() { + use jett_driver::native::{NativeBuildError, NativeLauncherBundle}; + + for (name, message) in [ + ( + "11_refined_root_rejected.jett", + "cannot assign through a nominal refinement; replace the complete validated value instead", + ), + ( + "12_refined_intermediate_rejected.jett", + "cannot assign through a nominal refinement; replace the complete validated value instead", + ), + ( + "13_bitfield_rejected.jett", + "cannot assign through a bitfield; construct and rebind the complete value instead", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture(name), &source).unwrap(); + let binary = directory.path().join("preserved.exe"); + let sentinel = b"existing field-update output"; + fs::write(&binary, sentinel).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let check = |error: NativeBuildError| { + let NativeBuildError::Lowering { source, .. } = error else { + panic!("{name}: expected frontend rejection before archive lookup: {error}"); + }; + let jett_driver::BackendLoweringError::Build(built) = *source else { + panic!("{name}: expected the selected field-write diagnostic before HIR"); + }; + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(built.has_errors, "{name}"); + assert_eq!(errors.len(), 1, "{name}: {:?}", built.diagnostics); + assert_eq!(errors[0].code.code(), 404, "{name}"); + assert_eq!(errors[0].message, message, "{name}"); + assert_eq!(fs::read(&binary).unwrap(), sentinel, "{name}"); + }; + for release in [false, true] { + let options = jett_driver::BuildOptions { release }; + check( + jett_driver::native::build_host_executable_with_options( + &source, &launcher, &binary, options, + ) + .expect_err("invalid field update prevents native main publication"), + ); + check( + jett_driver::native::build_host_verify_suite_executable_with_options( + &source, &launcher, &binary, options, + ) + .expect_err("invalid field update prevents native verify publication"), + ); + check( + jett_driver::native::build_host_property_suite_executable_with_options( + &source, &launcher, &binary, options, + ) + .expect_err("invalid field update prevents native property publication"), + ); + } + } +} + +#[test] +fn native_field_update_suites_execute_checked_verify_and_property_bodies_without_source() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture("01_called_baselines.jett"), &source).unwrap(); + let report = jett_driver::test_file(&source).expect("reference field update suites"); + assert_eq!(report.total, 2); + assert_eq!(report.passed, 2); + assert_eq!(report.failed, 0); + assert!(report.blocks.iter().any(|block| { + block.is_property && block.iterations.is_some_and(|iterations| iterations > 0) + })); + let mut binaries = Vec::new(); + for release in [false, true] { + let launcher = suite_options::launcher_for_options(release); + let options = jett_driver::BuildOptions { release }; + let verify = directory.path().join(format!("verify_{release}.exe")); + jett_driver::native::build_host_verify_suite_executable_with_options( + &source, launcher, &verify, options, + ) + .expect("native field update verify suite"); + let property = directory.path().join(format!("property_{release}.exe")); + jett_driver::native::build_host_property_suite_executable_with_options( + &source, launcher, &property, options, + ) + .expect("native field update property suite"); + binaries.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_field_update_preflight_precedes_rhs_and_preserves_pending_child_depth_without_source() { + for (name, stdout, diagnostic) in [ + ( + "05_pending_root_preflight.jett", + "before:root\n", + "field access is not supported on ", + ), + ( + "06_pending_intermediate_preflight.jett", + "before:inner\n", + "field access is not supported on ", + ), + ( + "08_stored_child_depth_refusal.jett", + "before:remaining-depth\n", + "unsupported binary operation: ", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(fixture(name), &source).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("pending field inspection must fail"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + assert!( + expected.message.starts_with("runtime error: "), + "{name}: {}", + expected.message + ); + assert!( + expected.message.contains(diagnostic), + "{name}: {}", + expected.message + ); + let stderr = format!("{}\n", expected.message); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("pending_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + suite_options::launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/graphics_authority.rs b/crates/jett_driver/tests/native_conformance/graphics_authority.rs new file mode 100644 index 00000000..672cb6e7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/graphics_authority.rs @@ -0,0 +1,288 @@ +use super::*; + +const PENDING_AUTHORITY_PROBE: &str = r#"namespace app +function update(state: int64, key: graphics.Key) returns int64: + return state +function render(view state: int64) returns graphics.Scene: + graphics.Color background = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: background, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function attempt(view display: Graphics, view stdout: Stdout) returns nothing: + graphics.Config config = graphics.Config(title: "authority", width: 0, height: 1) + Stdout.write(view stdout, "before\n") + graphics.run[int64](view display, config, 0, update, render) handle error: + Stdout.write(view stdout, "handled:{error}\n") + return nothing + Stdout.write(view stdout, "must not run\n") +function main(display: Graphics, stdout: Stdout) returns nothing: + attempt(view run display, view stdout) + Stdout.write(view stdout, "after\n") +"#; + +#[test] +fn native_pending_graphics_authority_fails_before_config_validation_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("authority.jett"); + fs::write(&source, PENDING_AUTHORITY_PROBE).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("pending Graphics authority must fail before the width-zero domain error"); + assert_eq!(expected.output.stdout, "before\n"); + assert!(expected.output.debug_events.is_empty()); + assert_eq!( + expected.message, + "runtime error: graphics.__run expects a Graphics capability" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("authority_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native forwarded pending Graphics authority probe"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, b"before\n"); + assert_eq!( + actual.stderr, + b"runtime error: graphics.__run expects a Graphics capability\n" + ); + } +} + +const CONFIG_BINDING: &str = + " graphics.Config config = graphics.Config(title: \"authority\", width: 0, height: 1)\n"; +const MAIN_CALL: &str = " attempt(view run display, view stdout)\n"; +const READY_CALL: &str = " attempt(view display, view stdout)\n"; +const TWICE_PENDING_CALL: &str = " attempt(view run (run display), view stdout)\n"; +const PARTIALLY_JOINED_CALL: &str = r#" Graphics nested = run (run display) + Graphics first = join nested handle error: + return nothing + attempt(view first, view stdout) +"#; +const FULLY_JOINED_CALL: &str = r#" Graphics nested = run (run display) + Graphics first = join nested handle error: + return nothing + Graphics ready = join first handle error: + return nothing + attempt(view ready, view stdout) +"#; +const FULLY_JOINED_REUSED_CALL: &str = r#" Graphics nested = run (run display) + Graphics first = join nested handle error: + return nothing + Graphics ready = join first handle error: + return nothing + attempt(view ready, view stdout) + attempt(view ready, view stdout) +"#; + +#[derive(Clone, Copy)] +enum AuthorityExpectation { + Terminal(&'static str), + Handled { attempts: usize }, +} + +fn authority_source(call: &str, config: &str) -> String { + assert!(PENDING_AUTHORITY_PROBE.contains(MAIN_CALL)); + assert!(PENDING_AUTHORITY_PROBE.contains(CONFIG_BINDING)); + PENDING_AUTHORITY_PROBE + .replacen(MAIN_CALL, call, 1) + .replacen(CONFIG_BINDING, config, 1) +} + +fn handled_output(attempts: usize) -> String { + let mut stdout = String::new(); + for _ in 0..attempts { + stdout.push_str("before\nhandled:graphics.run: window dimensions must be in 1..2048\n"); + } + stdout.push_str("after\n"); + stdout +} + +#[test] +fn native_graphics_authority_depth_and_ready_controls_match_reference_in_both_profiles() { + const AUTHORITY_ERROR: &str = "graphics.__run expects a Graphics capability"; + const PENDING_CONFIG: &str = " graphics.Config config = run graphics.Config(title: \"authority\", width: 0, height: 1)\n"; + const PENDING_TITLE: &str = " graphics.Config config = graphics.Config(title: run \"authority\", width: 0, height: 1)\n"; + const PENDING_WIDTH: &str = " graphics.Config config = graphics.Config(title: \"authority\", width: run 0, height: 1)\n"; + + let cases = [ + ( + "depth_two", + TWICE_PENDING_CALL, + CONFIG_BINDING, + AuthorityExpectation::Terminal(AUTHORITY_ERROR), + ), + ( + "depth_two_precedes_pending_config", + TWICE_PENDING_CALL, + PENDING_CONFIG, + AuthorityExpectation::Terminal(AUTHORITY_ERROR), + ), + ( + "partial_join", + PARTIALLY_JOINED_CALL, + CONFIG_BINDING, + AuthorityExpectation::Terminal(AUTHORITY_ERROR), + ), + ( + "partial_join_precedes_pending_title", + PARTIALLY_JOINED_CALL, + PENDING_TITLE, + AuthorityExpectation::Terminal(AUTHORITY_ERROR), + ), + ( + "partial_join_precedes_pending_width", + PARTIALLY_JOINED_CALL, + PENDING_WIDTH, + AuthorityExpectation::Terminal(AUTHORITY_ERROR), + ), + ( + "ready", + READY_CALL, + CONFIG_BINDING, + AuthorityExpectation::Handled { attempts: 1 }, + ), + ( + "joined", + FULLY_JOINED_CALL, + CONFIG_BINDING, + AuthorityExpectation::Handled { attempts: 1 }, + ), + ( + "joined_reused", + FULLY_JOINED_REUSED_CALL, + CONFIG_BINDING, + AuthorityExpectation::Handled { attempts: 2 }, + ), + ( + "ready_then_pending_title", + READY_CALL, + PENDING_TITLE, + AuthorityExpectation::Terminal("graphics: graphics.Config.title must be string"), + ), + ( + "joined_then_pending_config", + FULLY_JOINED_CALL, + PENDING_CONFIG, + AuthorityExpectation::Terminal("graphics: expected graphics.Config"), + ), + ( + "joined_then_pending_width", + FULLY_JOINED_CALL, + PENDING_WIDTH, + AuthorityExpectation::Terminal("graphics: graphics.Config.width must be int64"), + ), + ]; + // All cases fail or return handled domain data before opening a window. + // Source-level callbacks and Graphics capability policy stay unchanged. + for (name, call, config, expectation) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, authority_source(call, config)).unwrap(); + let outcome = jett_driver::run_file_capture_outcome(&source); + match expectation { + AuthorityExpectation::Terminal(message) => { + let expected = + outcome.expect_err(&format!("{name} must fail before graphics effects")); + assert_eq!(expected.output.stdout, "before\n", "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{name}" + ); + } + AuthorityExpectation::Handled { attempts } => { + let expected = outcome.unwrap_or_else(|error| panic!("{name}: {error:?}")); + assert_eq!(expected.stdout, handled_output(attempts), "{name}"); + assert!(expected.debug_events.is_empty(), "{name}"); + } + } + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("native {name}: {error:?}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + match expectation { + AuthorityExpectation::Terminal(message) => { + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, b"before\n", "{name}"); + assert_eq!( + actual.stderr, + format!("runtime error: {message}\n").as_bytes(), + "{name}" + ); + } + AuthorityExpectation::Handled { attempts } => { + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, handled_output(attempts).as_bytes(), "{name}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } + } + } + } +} + +#[test] +fn native_fully_joined_graphics_authority_reopens_scripted_sessions_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("joined_sessions.jett"); + let program = authority_source( + FULLY_JOINED_REUSED_CALL, + " graphics.Config config = graphics.Config(title: \"authority\", width: 16, height: 16)\n", + ) + .replacen( + " Stdout.write(view stdout, \"must not run\\n\")\n", + " Stdout.write(view stdout, \"closed\\n\")\n", + 1, + ); + fs::write(&source, program).unwrap(); + let events = vec![graphics::TestEvent::Close, graphics::TestEvent::Close]; + let expected = + jett_driver::run_file_capture_outcome_with_graphics_test_events(&source, events.clone()) + .expect("fully joined Graphics authority must open two successive scripted sessions"); + assert_eq!(expected.stdout, "before\nclosed\nbefore\nclosed\nafter\n"); + assert!(expected.debug_events.is_empty()); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("joined_sessions_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native reusable joined Graphics authority"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + let script = graphics::encode_test_script(&events); + for binary in binaries { + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((graphics::TEST_SCRIPT_ENV, &script)), + ); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, b"before\nclosed\nbefore\nclosed\nafter\n"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print.rs b/crates/jett_driver/tests/native_conformance/hidden_secret_print.rs new file mode 100644 index 00000000..99f34fdc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print.rs @@ -0,0 +1,297 @@ +//! Selected accepted-print redaction; terminal diagnostics remain independent. +use super::{launcher, run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::{build_host_executable, build_host_executable_with_options}; +use jett_driver::{BuildOptions, DebugEvent, DebugEventKind, run_file_capture_outcome}; +use std::fs; +use std::path::Path; + +struct Case { + name: &'static str, + source: &'static [u8], + events: &'static [&'static str], +} + +const CASES: &[Case] = &[ + Case { + name: "07_hidden_secret_debug_print", + source: include_bytes!("hidden_secret_print/07_hidden_secret_debug_print.jett"), + events: &["app.Hidden(token: [redacted])\n"], + }, + Case { + name: "15_public_erased_debug_print", + source: include_bytes!("hidden_secret_print/15_public_erased_debug_print.jett"), + events: &["app.Public(token: sentinel-public)\n"], + }, + Case { + name: "16_nested_generic", + source: include_bytes!("hidden_secret_print/16_nested_generic.jett"), + events: &[ + "app.Boxed(token: [redacted], value: 7)\n", + "list(app.Boxed(token: [redacted], value: 7)) app.Boxed(token: [redacted], value: 7)\n", + ], + }, + Case { + name: "17_nested_sums", + source: include_bytes!("hidden_secret_print/17_nested_sums.jett"), + events: &[ + "list(some(app.Child(token: [redacted], label: [redacted])), none) ok(app.Child(token: [redacted], label: [redacted])) fail([redacted])\n", + "app.Choice.item(app.Child(token: [redacted], label: [redacted])) app.Session@active([redacted], 4)\n", + ], + }, + Case { + name: "18_incomplete_builder", + source: include_bytes!("hidden_secret_print/18_incomplete_builder.jett"), + events: &[ + "TypeConstruction[app.Vault]()\n", + "TypeConstruction[app.Vault](token: [redacted])\n", + "TypeConstruction[app.Vault](token: [redacted], label: public)\n", + ], + }, + Case { + name: "19_pending_erased_collection", + source: include_bytes!("hidden_secret_print/19_pending_erased_collection.jett"), + events: &["pending(pending(list([redacted])))\n"], + }, + Case { + name: "21_pipeline_and_owned", + source: include_bytes!("hidden_secret_print/21_pipeline_and_owned.jett"), + events: &[ + "app.Hidden(token: [redacted])\n", + "app.Hidden(token: [redacted])\n", + ], + }, +]; + +fn assert_native_profiles(source: &Path, name: &str, status: i32, stderr: &[u8]) { + let directory = source.parent().unwrap(); + let binaries = [false, true].map(|release| { + let profile = if release { "release" } else { "debug" }; + let binary = directory.join(format!("{name}-{profile}.exe")); + let launcher = launcher_for_options(release); + eprintln!( + "JETT-HIDDEN-PRINT-NATIVE start case={name} profile={profile} source={} binary={} launcher={launcher:?}", + source.display(), + binary.display(), + ); + // Runtime archive profile is independent of the default debug Source policy. + build_host_executable(source, launcher, &binary) + .unwrap_or_else(|error| panic!("{name} ({profile}): {error:?}")); + (profile, binary) + }); + fs::remove_file(source).unwrap(); + assert!(!source.exists()); + for (profile, binary) in binaries { + let actual = run_bounded(&binary, directory); + assert_eq!( + actual.status.code(), + Some(status), + "{name} ({profile}): {actual:?}" + ); + assert!(actual.stdout.is_empty(), "{name} ({profile}): {actual:?}"); + assert_eq!(actual.stderr, stderr, "{name} ({profile})"); + eprintln!( + "JETT-HIDDEN-PRINT-NATIVE done case={name} profile={profile} source_deleted={} status={:?} stdout={:?} stderr={:?}", + !source.exists(), + actual.status.code(), + actual.stdout, + actual.stderr, + ); + } +} + +#[test] +fn native_hidden_secret_print_original_and_nested_sources_keep_selected_exact_events() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + let reference = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{}: {error:?}", case.name)); + assert!(reference.stdout.is_empty(), "{}", case.name); + assert!( + reference.frontend_debug_observations.is_empty(), + "{}", + case.name + ); + let expected: Vec<_> = case + .events + .iter() + .map(|text| DebugEvent { + kind: DebugEventKind::Println, + text: (*text).into(), + }) + .collect(); + assert_eq!(reference.debug_events, expected, "{}", case.name); + assert_native_profiles(&source, case.name, 0, case.events.join("").as_bytes()); + } +} + +#[test] +fn native_hidden_secret_print_argument_failure_emits_no_outer_event_and_retires_owned_argument() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("20_argument_failure.jett"); + fs::write( + &source, + include_bytes!("hidden_secret_print/20_argument_failure.jett"), + ) + .unwrap(); + let failure = run_file_capture_outcome(&source).unwrap_err(); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.frontend_debug_observations.is_empty()); + assert_eq!( + failure.output.debug_events, + [ + DebugEvent { + kind: DebugEventKind::Print, + text: "before:".into() + }, + DebugEvent { + kind: DebugEventKind::Print, + text: "argument:".into() + }, + DebugEvent { + kind: DebugEventKind::Println, + text: "failing\n".into() + }, + ] + ); + assert_eq!( + failure.message, + "runtime error: string.repeat: requested output is too large" + ); + // Status 72 would expose an unretired ordinary owner at entry completion. + assert_native_profiles( + &source, + "20_argument_failure", + 71, + b"before:argument:failing\nruntime error: string.repeat: requested output is too large\n", + ); +} + +#[test] +fn native_hidden_secret_print_source_release_rejection_preserves_publication() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + let binary = directory.path().join("preserved.exe"); + let sentinel = b"existing native publication"; + fs::write(&source, case.source).unwrap(); + fs::write(&binary, sentinel).unwrap(); + let result = jett_driver::build_file_with_options(&source, BuildOptions { release: true }); + assert!(result.has_errors, "{}", case.name); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 362), + "{}: {:?}", + case.name, + result.diagnostics + ); + build_host_executable_with_options( + &source, + launcher(), + &binary, + BuildOptions { release: true }, + ) + .expect_err("Source release Print/Println must fail before publication"); + assert_eq!(fs::read(&binary).unwrap(), sentinel, "{}", case.name); + } +} + +#[test] +fn native_hidden_secret_print_direct_and_refinement_qualified_source_restrictions_remain() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("direct.jett"); + fs::write( + &source, + r#"namespace app +type TokenAlias = secret[string] +type Token = TokenAlias where true +function directly(view token: secret[string]) returns nothing: + print(view token) +function refined(view token: Token) returns nothing: + println(view token) +function main() returns nothing: + return nothing +"#, + ) + .unwrap(); + let result = jett_driver::build_file(&source); + assert!(result.has_errors); + assert_eq!( + result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.code() == 600) + .count(), + 2, + "{:?}", + result.diagnostics + ); + let release = jett_driver::build_file_with_options(&source, BuildOptions { release: true }); + assert_eq!( + release + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.code() == 600) + .count(), + 2, + "{:?}", + release.diagnostics + ); + assert_eq!( + release + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.code() == 362) + .count(), + 2, + "{:?}", + release.diagnostics + ); +} + +#[test] +fn native_hidden_secret_print_preserves_once_order_without_invoking_getters_or_callbacks() { + let directory = tempfile::tempdir().unwrap(); + let source = directory + .path() + .join("22_once_order_and_noninvocation.jett"); + fs::write( + &source, + include_bytes!("hidden_secret_print/22_once_order_and_noninvocation.jett"), + ) + .unwrap(); + let expected = [ + (DebugEventKind::Print, "first:"), + (DebugEventKind::Print, "second:"), + ( + DebugEventKind::Println, + "app.Hidden(token: [redacted]) app.Hidden(token: [redacted])\n", + ), + (DebugEventKind::Println, "app.Hidden(token: [redacted])\n"), + (DebugEventKind::Print, "getter:"), + (DebugEventKind::Println, "public\n"), + (DebugEventKind::Println, "function(app.callback)\n"), + (DebugEventKind::Print, "callback:"), + (DebugEventKind::Print, "predicate:"), + (DebugEventKind::Println, "7\n"), + ] + .into_iter() + .map(|(kind, text)| DebugEvent { + kind, + text: text.into(), + }) + .collect::>(); + let reference = run_file_capture_outcome(&source).expect("authenticated print source"); + assert!(reference.stdout.is_empty()); + assert!(reference.frontend_debug_observations.is_empty()); + assert_eq!(reference.debug_events, expected); + assert_native_profiles( + &source, + "22_once_order_and_noninvocation", + 0, + jett_driver::render_debug_events(&expected).as_bytes(), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/07_hidden_secret_debug_print.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/07_hidden_secret_debug_print.jett new file mode 100644 index 00000000..6f2e7c5c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/07_hidden_secret_debug_print.jett @@ -0,0 +1,12 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Hidden: + token: secret[string] +implement Named for Hidden: + function name(view self: Hidden) returns string: + return "hidden" +function main() returns nothing: + Named erased = Hidden(token: "sentinel-secret") + println(view erased) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/15_public_erased_debug_print.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/15_public_erased_debug_print.jett new file mode 100644 index 00000000..9a7432a4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/15_public_erased_debug_print.jett @@ -0,0 +1,12 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Public: + token: string +implement Named for Public: + function name(view self: Public) returns string: + return "public" +function main() returns nothing: + Named erased = Public(token: "sentinel-public") + println(view erased) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/16_nested_generic.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/16_nested_generic.jett new file mode 100644 index 00000000..ff518184 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/16_nested_generic.jett @@ -0,0 +1,19 @@ +namespace app +type Token = secret[string] +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + token: Token + value: T +implement Named for Boxed[int64]: + function name(view self: Boxed[int64]) returns string: + return "generic" +function emit[T](view item: T) returns nothing: + println(view item) +function main() returns nothing: + Boxed[int64] original = Boxed[int64](token: "generic-secret", value: 7) + Named erased = clone original + emit[Named](view erased) + list[Named] items = list(erased) + println(view items, view original) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/17_nested_sums.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/17_nested_sums.jett new file mode 100644 index 00000000..5d8334fe --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/17_nested_sums.jett @@ -0,0 +1,23 @@ +namespace app +struct Child: + token: secret[string] + label: string +enum Choice: + item(value: Child) + empty +machine Session: + states: + idle + active(token: secret[string], count: int64) + transitions: + idle to active +function main() returns nothing: + Child child = Child(token: "child-secret", label: "[redacted]") + list[optional[Child]] children = list(some(clone child), none) + result[Child, string] success = ok(clone child) + result[int64, secret[string]] failed = fail("failure-secret") + println(view children, view success, view failed) + Choice selected = Choice.item(child) + Session active = Session(active, "machine-secret", 4) + println(view selected, view active) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/18_incomplete_builder.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/18_incomplete_builder.jett new file mode 100644 index 00000000..11f352a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/18_incomplete_builder.jett @@ -0,0 +1,15 @@ +namespace app +struct Vault: + token: secret[string] + label: string +function main() returns nothing: + Vault original = Vault(token: "builder-secret", label: "public") + mutable TypeConstruction builder = type.construct_start[Vault]() + println(view builder) + for field in type.fields[Vault](): + comptime type Field = field.type_info: + Field value = type.field_value[Vault, Field](view original, view field) + builder = type.construct_put[Vault, Field](builder, view field, value) handle error: + return nothing + println(view builder) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/19_pending_erased_collection.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/19_pending_erased_collection.jett new file mode 100644 index 00000000..c662cd1c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/19_pending_erased_collection.jett @@ -0,0 +1,12 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for list[secret[string]]: + function name(view self: list[secret[string]]) returns string: + return "hidden-list" +function main() returns nothing: + list[secret[string]] items = list("pending-secret") + Named erased = items + Named pending = run run erased + println(view pending) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/20_argument_failure.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/20_argument_failure.jett new file mode 100644 index 00000000..14227b87 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/20_argument_failure.jett @@ -0,0 +1,20 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Hidden: + token: secret[string] +implement Named for Hidden: + function name(view self: Hidden) returns string: + return "hidden" +function argument() returns Named: + print("argument:") + return Hidden(token: "argument-secret") +function failed() returns int64: + println("failing") + string rejected = string.repeat("ab", 9223372036854775807) + return string.char_count(rejected) +function main() returns nothing: + print("before:") + println(argument(), failed()) + println("unreachable") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/21_pipeline_and_owned.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/21_pipeline_and_owned.jett new file mode 100644 index 00000000..21adfa9c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/21_pipeline_and_owned.jett @@ -0,0 +1,16 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Hidden: + token: secret[string] +implement Named for Hidden: + function name(view self: Hidden) returns string: + return "hidden" +function emit(view value: Named) returns nothing: + println(view value) + return nothing +function main() returns nothing: + Named borrowed = Hidden(token: "borrowed-secret") + borrowed into view emit() + println(borrowed) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/hidden_secret_print/22_once_order_and_noninvocation.jett b/crates/jett_driver/tests/native_conformance/hidden_secret_print/22_once_order_and_noninvocation.jett new file mode 100644 index 00000000..ed7bda34 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/hidden_secret_print/22_once_order_and_noninvocation.jett @@ -0,0 +1,33 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Hidden: + token: secret[string] +implement Named for Hidden: + function name(view self: Hidden) returns string: + print("getter:") + return "public" +function argument(marker: string) returns Named: + print(marker) + return Hidden(token: "unpublished-argument") +function positive_check(value: int64) returns bool: + print("predicate:") + return value > 0 +type Positive = int64 where positive_check(value) +struct CallbackHolder: + callback: function() returns int64 +function callback() returns int64: + print("callback:") + return 7 +function main() returns nothing: + println(argument("first:"), argument("second:")) + Named retained = Hidden(token: "unpublished-retained") + println(view retained) + println(Named.name(view retained)) + CallbackHolder holder = CallbackHolder(callback: callback) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + println(view observed) + Positive returned = observed() + println(coarsen returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/inline_refinement_returns.rs b/crates/jett_driver/tests/native_conformance/inline_refinement_returns.rs new file mode 100644 index 00000000..b537b139 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/inline_refinement_returns.rs @@ -0,0 +1,218 @@ +use super::*; + +const INLINE_SUCCESS: &str = r#"namespace models +export type Positive = int64 where value > 0 +export type High = Positive where value > 5 +export type Text = string where string.char_count(value) > 0 +export type NonEmpty = list[int64] where list.length(view value) > 0 +export function make[T]() returns function(int64) returns T: + return function(value: int64) returns T: return value +export function reflected() returns function(int64) returns Positive: + comptime type Field = type.info[Positive](): + return function(value: int64) returns Field: return value +namespace shadow +export type Positive = int64 where value < 0 +namespace app +function report(number: int64) returns string: + use models + function(int64) returns models.Positive scalar = function(value: int64) returns models.Positive: + return value + models.Positive initial = scalar(number) + function(view models.Positive) returns models.High stronger = function(view value: models.Positive) returns models.High: + return clone value + models.High raised = stronger(view initial) + function(view models.High) returns models.High exact = function(view value: models.High) returns models.High: + return clone value + models.High copied = exact(view raised) + function(int64) returns models.Positive generic = models.make[models.Positive]() + models.Positive specialized = generic(number) + function(int64) returns models.Positive reflected = models.reflected() + models.Positive scoped = reflected(number) + function(string) returns models.Text text = function(value: string) returns models.Text: return value + models.Text label = text("Agent-λ🙂") + function(view list[int64]) returns models.NonEmpty values = function(view source: list[int64]) returns models.NonEmpty: + return clone source + list[int64] owned = list(7, 9) + models.NonEmpty kept = values(view owned) + function(view models.NonEmpty) returns models.NonEmpty echo = function(view source: models.NonEmpty) returns models.NonEmpty: + return clone source + models.NonEmpty copied_values = echo(view kept) + list[int64] public_values = coarsen copied_values + int64 first = coarsen initial + int64 second = coarsen copied + int64 third = coarsen specialized + int64 fourth = coarsen scoped + string public_text = coarsen label + return "{first}:{second}:{third}:{fourth}:{public_text}:{list.length(view public_values)}:{list.length(view owned)}" +function alias_report(number: int64) returns string: + use models as selected + use shadow as models + function(int64) returns selected.Positive callback = function(value: int64) returns selected.Positive: + return value + return "{coarsen callback(number)}" +function main(stdout: Stdout) returns nothing: + use models + string baked = comptime report(12) + function(int64) returns models.Positive callback = comptime models.make[models.Positive]() + models.Positive produced = callback(127) + int64 ready = coarsen produced + string alias_baked = comptime alias_report(7) + Stdout.write(view stdout, "{report(12)}|{baked}|{ready}|{alias_report(7)}:{alias_baked}\n") +verify inline_return_contracts: + assert report(12) == "12:12:12:12:Agent-λ🙂:2:2" + assert alias_report(7) == "7" +property inline_return_trials: + given high: bool + int64 number = 12 + if high: + assert report(number) == "12:12:12:12:Agent-λ🙂:2:2" + assert alias_report(number) == "12" + else: + assert report(127) == "127:127:127:127:Agent-λ🙂:2:2" + assert alias_report(127) == "127" +"#; + +const INLINE_FAILURE: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function echo(value: Positive) returns Positive: + return value +function operand(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "operand\n") + return -1 +function relay(view stdout: Stdout, callback: function(int64) returns Positive) returns Positive: + return callback(operand(view stdout)) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] kept = list("kept", "owned") + function(int64) returns Positive callback = function(value: int64) returns Positive: + list[string] closure_owned = list("closure-owned", "payload") + return value + Stdout.write(view stdout, "before\n") +__INVOCATION__ + Stdout.write(view stdout, "after:{bytes.to_hex(view earlier)}:{list.length(view kept)}\n") +"#; + +fn run_inline_refinement_case( + name: &str, + text: &str, + stdout: &str, + message: Option<&str>, + debug: &[&str], + suites: bool, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, text).unwrap(); + let runtime_debug_events = match (jett_driver::run_file_capture_outcome(&source), message) { + (Ok(expected), None) => { + assert_eq!(expected.stdout, stdout, "{name}"); + assert_eq!(debug_trace_lines(&expected.debug_events), debug, "{name}"); + expected.debug_events + } + (Err(expected), Some(message)) => { + assert_eq!(expected.message, message, "{name}"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + debug, + "{name}" + ); + expected.output.debug_events + } + (actual, message) => panic!("{name}: outcome {actual:?}, error {message:?}"), + }; + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("inline_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + } + let mut suite_binaries = Vec::new(); + if suites { + let verify = directory.path().join("inline_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join("inline_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + suite_binaries.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(if message.is_some() { 71 } else { 0 }), + "{name}: {actual:?}" + ); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&runtime_debug_events) + }; + if let Some(message) = message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + for binary in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_inline_refinement_returns_keep_captured_contracts_and_owned_values() { + run_inline_refinement_case( + "inline_return_contracts", + INLINE_SUCCESS, + "12:12:12:12:Agent-λ🙂:2:2|12:12:12:12:Agent-λ🙂:2:2|127|7:7\n", + None, + &[], + true, + ); +} + +#[test] +fn native_inline_refinement_return_failures_precede_checked_consumers_and_cleanup() { + for (name, invocation) in [ + ( + "local", + " Positive rejected = callback(operand(view stdout))", + ), + ( + "argument", + " Positive rejected = echo(callback(operand(view stdout)))", + ), + ( + "return", + " Positive rejected = relay(view stdout, callback)", + ), + ( + "higher_order_map", + " list[int64] inputs = list(operand(view stdout))\n list[Positive] rejected = list.map[int64, Positive](inputs, callback)", + ), + ] { + let source = INLINE_FAILURE.replace("__INVOCATION__", invocation); + run_inline_refinement_case( + name, + &source, + "before\noperand\n", + Some("runtime error: refinement type constraint failed for 'app.Positive'"), + &["trace marker: string = positive"], + false, + ); + } +} diff --git a/crates/jett_driver/tests/native_conformance/interface_equality.rs b/crates/jett_driver/tests/native_conformance/interface_equality.rs new file mode 100644 index 00000000..f23ac99b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/interface_equality.rs @@ -0,0 +1,86 @@ +use super::*; +use jett_driver::BuildOptions; + +#[test] +fn native_interface_equality_is_rejected_before_publication() { + for (name, count) in [ + ("interface_equality.jett", 7), + ("interface_equality_generic.jett", 3), + ("interface_equality_comptime.jett", 2), + ("interface_equality_enum_payload.jett", 4), + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/compile_fail") + .join(name); + let text = fs::read_to_string(fixture).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + format!("{text}function main() returns nothing:\n return nothing\n"), + ) + .unwrap(); + for release in [false, true] { + let binary = directory.path().join("preserved.exe"); + fs::write(&binary, b"existing output").unwrap(); + let error = jett_driver::native::build_host_executable_with_options( + &source, + super::suite_options::launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_err(); + let jett_driver::native::NativeBuildError::Lowering { + source: lowering, .. + } = error + else { + panic!("{name}: expected frontend rejection, got {error}"); + }; + let jett_driver::BackendLoweringError::Build(checked) = *lowering else { + panic!("{name}: expected diagnostics before HIR"); + }; + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), count, "{name}: {errors:?}"); + for diagnostic in errors { + assert_eq!(diagnostic.code.code(), 376, "{name}: {diagnostic:?}"); + assert!(diagnostic.message.contains("interface-typed values")); + assert!(diagnostic.message.contains("compare before erasure")); + } + assert_eq!(fs::read(&binary).unwrap(), b"existing output"); + } + } +} + +#[test] +fn native_explicit_interface_comparison_matches_both_profiles_without_source() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/interface_explicit_comparison.jett"); + let text = fs::read_to_string(fixture).unwrap(); + for release in [false, true] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, &text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("explicit domain comparison reference"); + assert_eq!(expected.stdout, "true false\ntrue true\n7 7 8\n"); + assert!(expected.debug_events.is_empty()); + let binary = directory.path().join("comparison.exe"); + jett_driver::native::build_host_executable_with_options( + &source, + super::suite_options::launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .expect("compile explicit domain comparisons"); + fs::remove_file(&source).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + fs::remove_file(&binary).unwrap(); + } +} diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots.rs b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots.rs new file mode 100644 index 00000000..1b703e17 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots.rs @@ -0,0 +1,244 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable_with_options; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +fn run_case(name: &str, source_text: &str, expected_stdout: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let reference = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(reference.stdout, expected_stdout, "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + expected_stdout.as_bytes(), + "{name}, release={release}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_json_inferred_parse_slots_parse_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_empty_list", + include_str!("json_inferred_parse_slots/parse_empty_list.jett"), + "valid:length:0\noccupied:decode-error:0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_absent_optional_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_absent_optional", + include_str!("json_inferred_parse_slots/parse_absent_optional.jett"), + "valid:none\noccupied:decode-error:unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_result_missing_error_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_result_missing_error", + include_str!("json_inferred_parse_slots/parse_result_missing_error.jett"), + "valid:success:int64\noccupied:decode-error:fail: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_result_missing_success_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_result_missing_success", + include_str!("json_inferred_parse_slots/parse_result_missing_success.jett"), + "valid:failure:string\noccupied:decode-error:ok: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_exact_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_exact_empty_list", + include_str!("json_inferred_parse_slots/parse_exact_empty_list.jett"), + "valid:length:0\noccupied:decode-error:0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_exact_absent_optional_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_exact_absent_optional", + include_str!("json_inferred_parse_slots/parse_exact_absent_optional.jett"), + "valid:none\noccupied:decode-error:unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_exact_result_missing_error_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_exact_result_missing_error", + include_str!("json_inferred_parse_slots/parse_exact_result_missing_error.jett"), + "valid:success:int64\noccupied:decode-error:fail: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_parse_exact_result_missing_success_matches_reference_without_source_in_both_profiles() + { + run_case( + "parse_exact_result_missing_success", + include_str!("json_inferred_parse_slots/parse_exact_result_missing_success.jett"), + "valid:failure:string\noccupied:decode-error:ok: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_empty_list", + include_str!("json_inferred_parse_slots/pipeline_parse_empty_list.jett"), + "valid:length:0\noccupied:decode-error:0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_absent_optional_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_absent_optional", + include_str!("json_inferred_parse_slots/pipeline_parse_absent_optional.jett"), + "valid:none\noccupied:decode-error:unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_result_missing_error_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_result_missing_error", + include_str!("json_inferred_parse_slots/pipeline_parse_result_missing_error.jett"), + "valid:success:int64\noccupied:decode-error:fail: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_result_missing_success_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_result_missing_success", + include_str!("json_inferred_parse_slots/pipeline_parse_result_missing_success.jett"), + "valid:failure:string\noccupied:decode-error:ok: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_exact_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_exact_empty_list", + include_str!("json_inferred_parse_slots/pipeline_parse_exact_empty_list.jett"), + "valid:length:0\noccupied:decode-error:0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_exact_absent_optional_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_exact_absent_optional", + include_str!("json_inferred_parse_slots/pipeline_parse_exact_absent_optional.jett"), + "valid:none\noccupied:decode-error:unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_exact_result_missing_error_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_exact_result_missing_error", + include_str!("json_inferred_parse_slots/pipeline_parse_exact_result_missing_error.jett"), + "valid:success:int64\noccupied:decode-error:fail: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_pipeline_parse_exact_result_missing_success_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_exact_result_missing_success", + include_str!("json_inferred_parse_slots/pipeline_parse_exact_result_missing_success.jett"), + "valid:failure:string\noccupied:decode-error:ok: unsupported reflected JSON type: \nmalformed:decode-error:expected result object with exactly one key\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_recursive_01_string_key_empty_map_matches_reference_without_source_in_both_profiles() + { + run_case( + "recursive_01_string_key_empty_map", + include_str!("json_inferred_parse_slots/recursive_01_string_key_empty_map.jett"), + "valid:length:0\noccupied:decode-error:x: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON object\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_recursive_02_generic_record_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "recursive_02_generic_record_empty_list", + include_str!("json_inferred_parse_slots/recursive_02_generic_record_empty_list.jett"), + "valid:length:0\noccupied:decode-error:items: 0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON object\n", + ); +} + +#[test] +fn native_json_inferred_parse_slots_recursive_03_nested_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "recursive_03_nested_empty_list", + include_str!("json_inferred_parse_slots/recursive_03_nested_empty_list.jett"), + "valid:outer:1:inner:0\noccupied:decode-error:0: 0: unsupported reflected JSON type: \nmalformed:decode-error:unterminated JSON array\n", + ); +} diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_absent_optional.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_absent_optional.jett new file mode 100644 index 00000000..4ce14bc2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_absent_optional.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[T](view shape: optional[T], raw: string) returns string: + use json + optional[T] parsed = json.parse[optional[T]](raw) handle error: + return "decode-error:{error}" + T occupied = parsed handle: + return "none" + return "some:{type.name[T]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view none, "null") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view none, "7") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view none, "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_empty_list.jett new file mode 100644 index 00000000..57a5dcd2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_empty_list.jett @@ -0,0 +1,14 @@ +namespace app +function inspect[T](view shape: list[T], raw: string) returns string: + use json + list[T] parsed = json.parse[list[T]](raw) handle error: + return "decode-error:{error}" + return "length:{list.length[T](view parsed)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(), "[]") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(), "[7]") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(), "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_absent_optional.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_absent_optional.jett new file mode 100644 index 00000000..1e743217 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_absent_optional.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[T](view shape: optional[T], raw: string) returns string: + use json + optional[T] parsed = json.parse_exact[optional[T]](raw) handle error: + return "decode-error:{error}" + T occupied = parsed handle: + return "none" + return "some:{type.name[T]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view none, "null") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view none, "7") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view none, "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_empty_list.jett new file mode 100644 index 00000000..de0eff22 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_empty_list.jett @@ -0,0 +1,14 @@ +namespace app +function inspect[T](view shape: list[T], raw: string) returns string: + use json + list[T] parsed = json.parse_exact[list[T]](raw) handle error: + return "decode-error:{error}" + return "length:{list.length[T](view parsed)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(), "[]") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(), "[7]") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(), "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_error.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_error.jett new file mode 100644 index 00000000..bd2f9a39 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_error.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = json.parse_exact[result[Value, Failure]](raw) handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view ok(7), "{{\"ok\":8}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view ok(7), "{{\"fail\":\"bad\"}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view ok(7), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_success.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_success.jett new file mode 100644 index 00000000..b66d9c89 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_exact_result_missing_success.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = json.parse_exact[result[Value, Failure]](raw) handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view fail("seed"), "{{\"fail\":\"next\"}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view fail("seed"), "{{\"ok\":8}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view fail("seed"), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_error.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_error.jett new file mode 100644 index 00000000..baa4cbac --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_error.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = json.parse[result[Value, Failure]](raw) handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view ok(7), "{{\"ok\":8}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view ok(7), "{{\"fail\":\"bad\"}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view ok(7), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_success.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_success.jett new file mode 100644 index 00000000..fc797d45 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/parse_result_missing_success.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = json.parse[result[Value, Failure]](raw) handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view fail("seed"), "{{\"fail\":\"next\"}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view fail("seed"), "{{\"ok\":8}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view fail("seed"), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_absent_optional.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_absent_optional.jett new file mode 100644 index 00000000..b9fa23bf --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_absent_optional.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[T](view shape: optional[T], raw: string) returns string: + use json + optional[T] parsed = raw into json.parse[optional[T]]() handle error: + return "decode-error:{error}" + T occupied = parsed handle: + return "none" + return "some:{type.name[T]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view none, "null") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view none, "7") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view none, "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_empty_list.jett new file mode 100644 index 00000000..1d1d6e61 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_empty_list.jett @@ -0,0 +1,14 @@ +namespace app +function inspect[T](view shape: list[T], raw: string) returns string: + use json + list[T] parsed = raw into json.parse[list[T]]() handle error: + return "decode-error:{error}" + return "length:{list.length[T](view parsed)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(), "[]") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(), "[7]") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(), "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_absent_optional.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_absent_optional.jett new file mode 100644 index 00000000..1c6b2cae --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_absent_optional.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[T](view shape: optional[T], raw: string) returns string: + use json + optional[T] parsed = raw into json.parse_exact[optional[T]]() handle error: + return "decode-error:{error}" + T occupied = parsed handle: + return "none" + return "some:{type.name[T]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view none, "null") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view none, "7") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view none, "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_empty_list.jett new file mode 100644 index 00000000..082194fa --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_empty_list.jett @@ -0,0 +1,14 @@ +namespace app +function inspect[T](view shape: list[T], raw: string) returns string: + use json + list[T] parsed = raw into json.parse_exact[list[T]]() handle error: + return "decode-error:{error}" + return "length:{list.length[T](view parsed)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(), "[]") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(), "[7]") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(), "[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_error.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_error.jett new file mode 100644 index 00000000..8b9b1b2a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_error.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = raw into json.parse_exact[result[Value, Failure]]() handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view ok(7), "{{\"ok\":8}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view ok(7), "{{\"fail\":\"bad\"}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view ok(7), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_success.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_success.jett new file mode 100644 index 00000000..0bc45c7b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_exact_result_missing_success.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = raw into json.parse_exact[result[Value, Failure]]() handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view fail("seed"), "{{\"fail\":\"next\"}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view fail("seed"), "{{\"ok\":8}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view fail("seed"), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_error.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_error.jett new file mode 100644 index 00000000..80920f48 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_error.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = raw into json.parse[result[Value, Failure]]() handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view ok(7), "{{\"ok\":8}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view ok(7), "{{\"fail\":\"bad\"}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view ok(7), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_success.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_success.jett new file mode 100644 index 00000000..eecac3f6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/pipeline_parse_result_missing_success.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[Value, Failure](view shape: result[Value, Failure], raw: string) returns string: + use json + result[Value, Failure] parsed = raw into json.parse[result[Value, Failure]]() handle error: + return "decode-error:{error}" + Value occupied = parsed handle error: + return "failure:{type.name[Failure]()}" + return "success:{type.name[Value]()}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view fail("seed"), "{{\"fail\":\"next\"}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view fail("seed"), "{{\"ok\":8}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view fail("seed"), "{{\"ok\":8,\"fail\":\"bad\"}}") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_01_string_key_empty_map.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_01_string_key_empty_map.jett new file mode 100644 index 00000000..61bfcf47 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_01_string_key_empty_map.jett @@ -0,0 +1,14 @@ +namespace app +function inspect[T](view shape: map[string, T], raw: string) returns string: + use json + map[string, T] parsed = raw into json.parse[map[string, T]]() handle error: + return "decode-error:{error}" + return "length:{map.length[string, T](view parsed)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view map(), "{{}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view map(), "{{\"x\":7}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view map(), "{{") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_02_generic_record_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_02_generic_record_empty_list.jett new file mode 100644 index 00000000..49e45e79 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_02_generic_record_empty_list.jett @@ -0,0 +1,16 @@ +namespace app +struct Envelope[T]: + items: list[T] +function inspect[T](view shape: list[T], raw: string) returns string: + use json + Envelope[T] parsed = json.parse_exact[Envelope[T]](raw) handle error: + return "decode-error:{error}" + return "length:{list.length[T](view parsed.items)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(), "{{\"items\":[]}}") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(), "{{\"items\":[7]}}") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(), "{{\"items\":[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_03_nested_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_03_nested_empty_list.jett new file mode 100644 index 00000000..20b9c65d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_parse_slots/recursive_03_nested_empty_list.jett @@ -0,0 +1,16 @@ +namespace app +function inspect[T](view shape: list[list[T]], raw: string) returns string: + use json + list[list[T]] parsed = raw into json.parse_exact[list[list[T]]]() handle error: + return "decode-error:{error}" + list[T] first = list.first[list[T]](view parsed) handle: + return "outer:0" + return "outer:{list.length[list[T]](view parsed)}:inner:{list.length[T](view first)}" +function main(stdout: Stdout) returns nothing: + string valid = inspect(view list(list()), "[[]]") + Stdout.write(view stdout, "valid:{valid}\n") + string occupied = inspect(view list(list()), "[[7]]") + Stdout.write(view stdout, "occupied:{occupied}\n") + string malformed = inspect(view list(list()), "[[") + Stdout.write(view stdout, "malformed:{malformed}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots.rs b/crates/jett_driver/tests/native_conformance/json_inferred_slots.rs new file mode 100644 index 00000000..2e7a04b3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots.rs @@ -0,0 +1,349 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::{ + NativeLauncherBundle, build_host_executable_with_options, + build_host_property_suite_executable_with_options, + build_host_verify_suite_executable_with_options, +}; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + PureSuites(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +fn run_case(name: &str, source_text: &str, expected: ExpectedOutcome) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, stdout, status, stderr) = match expected { + ExpectedOutcome::Success(stdout) | ExpectedOutcome::PureSuites(stdout) => { + let output = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, stdout, 0, String::new()) + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = run_file_capture_outcome(&source) + .expect_err("pending carrier must fail before producing JSON"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, stdout, 71, format!("{message}\n")) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + if matches!(expected, ExpectedOutcome::PureSuites(_)) { + let checked = jett_driver::test_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference suites: {error:?}")); + assert_eq!((checked.total, checked.passed, checked.failed), (2, 2, 0)); + assert_eq!(checked.blocks.len(), 2); + assert!(!checked.blocks[0].is_property); + assert!(checked.blocks[1].is_property); + assert_eq!(checked.blocks[1].iterations, Some(100)); + assert!(checked.debug_observations.is_empty(), "{name}: {checked:?}"); + assert!( + checked + .blocks + .iter() + .all(|block| block.debug_events.is_empty()), + "{name}: {checked:?}" + ); + } + let mut binaries = Vec::new(); + let mut suite_binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + if matches!(expected, ExpectedOutcome::PureSuites(_)) { + let verify_binary = directory + .path() + .join(format!("{name}_verify_{release}.exe")); + let verify = build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &verify_binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: verify build: {error}")); + assert!(verify.debug_observations.is_empty(), "{name}: {verify:?}"); + suite_binaries.push((verify_binary, release)); + let property_binary = directory + .path() + .join(format!("{name}_property_{release}.exe")); + let property = build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &property_binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: property build: {error}")); + assert!( + property.debug_observations.is_empty(), + "{name}: {property:?}" + ); + suite_binaries.push((property_binary, release)); + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, release={release}" + ); + } + for (binary, release) in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert!( + actual.stdout.is_empty(), + "{name}, release={release}: {actual:?}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_json_inferred_slots_empty_list_matches_reference_without_source_in_both_profiles() { + run_case( + "empty_list", + include_str!("json_inferred_slots/inferred_empty_json.jett"), + ExpectedOutcome::Success("[]\n"), + ); +} + +#[test] +fn native_json_inferred_slots_empty_list_public_matches_reference_without_source_in_both_profiles() +{ + run_case( + "empty_list_public", + include_str!("json_inferred_slots/inferred_empty_json_public.jett"), + ExpectedOutcome::Success("[]\n"), + ); +} + +#[test] +fn native_json_inferred_slots_typed_empty_list_control_matches_reference_without_source_in_both_profiles() + { + run_case( + "typed_empty_list_control", + include_str!("json_inferred_slots/typed_empty_json_control.jett"), + ExpectedOutcome::Success("[]\n"), + ); +} + +#[test] +fn native_json_inferred_slots_absent_optional_matches_reference_without_source_in_both_profiles() { + run_case( + "absent_optional", + include_str!("json_inferred_slots/01_absent_optional.jett"), + ExpectedOutcome::Success("null\n"), + ); +} + +#[test] +fn native_json_inferred_slots_result_missing_error_matches_reference_without_source_in_both_profiles() + { + run_case( + "result_missing_error", + include_str!("json_inferred_slots/02_result_missing_error.jett"), + ExpectedOutcome::Success("{\"ok\":7}\n"), + ); +} + +#[test] +fn native_json_inferred_slots_result_missing_success_matches_reference_without_source_in_both_profiles() + { + run_case( + "result_missing_success", + include_str!("json_inferred_slots/03_result_missing_success.jett"), + ExpectedOutcome::Success("{\"fail\":\"bad\"}\n"), + ); +} + +#[test] +fn native_json_inferred_slots_string_key_empty_map_matches_reference_without_source_in_both_profiles() + { + run_case( + "string_key_empty_map", + include_str!("json_inferred_slots/04_string_key_empty_map.jett"), + ExpectedOutcome::Success("{}\n"), + ); +} + +#[test] +fn native_json_inferred_slots_generic_record_empty_list_matches_reference_without_source_in_both_profiles() + { + run_case( + "generic_record_empty_list", + include_str!("json_inferred_slots/05_generic_record_empty_list.jett"), + ExpectedOutcome::Success("{\"items\":[]}\n"), + ); +} + +#[test] +fn native_json_inferred_slots_nested_empty_list_matches_reference_without_source_in_both_profiles() +{ + run_case( + "nested_empty_list", + include_str!("json_inferred_slots/06_nested_empty_list.jett"), + ExpectedOutcome::Success("[[]]\n"), + ); +} + +#[test] +fn native_json_inferred_slots_pending_empty_list_matches_reference_without_source_in_both_profiles() +{ + run_case( + "pending_empty_list", + include_str!("json_inferred_slots/07_pending_empty_list.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: for loop requires a list, string, map, or set value", + }, + ); +} + +#[test] +fn native_json_inferred_slots_pure_comptime_and_compiled_suites_match_reference() { + run_case( + "pure_facade_suites", + include_str!("json_inferred_slots/01_pure_facade_suites.jett"), + ExpectedOutcome::PureSuites("[]:[]:[]:[]\n"), + ); +} + +#[test] +fn native_json_inferred_slots_public_secret_field_projection_matches_reference() { + run_case( + "public_secret_field_projection", + include_str!("json_inferred_slots/05_public_secret_field_projection.jett"), + ExpectedOutcome::Success("{\"items\":[]}\n"), + ); +} + +#[test] +fn native_json_inferred_slots_policy_and_source_bodies_fail_before_publication() { + let cases = [ + ( + "inhabited_function_empty_list", + include_str!("json_inferred_slots/02_inhabited_function_empty_list.jett"), + 347, + ), + ( + "public_inhabited_function_empty_list", + include_str!("json_inferred_slots/03_public_inhabited_function_empty_list.jett"), + 347, + ), + ( + "public_empty_secret_slot", + include_str!("json_inferred_slots/04_public_empty_secret_slot.jett"), + 603, + ), + ( + "invalid_uninhabited_source_body", + include_str!("json_inferred_slots/06_invalid_uninhabited_source_body.jett"), + 311, + ), + ]; + for (name, source_text, expected_code) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + assert!(!launcher.archive_path.exists()); + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing inferred JSON publication"; + fs::write(&output, sentinel).unwrap(); + for release in [false, true] { + let error = build_host_executable_with_options( + &source, + &launcher, + &output, + BuildOptions { release }, + ) + .expect_err("JSON policy or invalid source body must fail before archive lookup"); + let build = error + .build_result() + .expect("typed frontend failure before publication"); + assert!(build.has_errors, "{name}, release={release}"); + let errors = build + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{name}, release={release}: {errors:?}"); + assert_eq!( + errors[0].code.code(), + expected_code, + "{name}, release={release}" + ); + assert!( + error.debug_observations().is_empty(), + "{name}, release={release}" + ); + assert!( + build.debug_observations.is_empty(), + "{name}, release={release}" + ); + assert!(!launcher.archive_path.exists()); + assert_eq!( + fs::read(&output).unwrap(), + sentinel, + "{name}, release={release}" + ); + } + } +} + +#[test] +fn native_json_inferred_slots_projected_alias_and_cloned_owner_remain_readable() { + run_case( + "owner_rereads", + include_str!("json_inferred_slots/owner_rereads.jett"), + ExpectedOutcome::Success("[]:[]:{\"items\":[]}:0:0:0\n"), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_absent_optional.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_absent_optional.jett new file mode 100644 index 00000000..07e8e392 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_absent_optional.jett @@ -0,0 +1,7 @@ +namespace app +function encode[T](view candidate: optional[T]) returns string: + use json + return json.serialize[optional[T]](view candidate) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view none)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_pure_facade_suites.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_pure_facade_suites.jett new file mode 100644 index 00000000..12e58a49 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/01_pure_facade_suites.jett @@ -0,0 +1,26 @@ +namespace app +function encode[T](view values: list[T]) returns string: + use json + return json.serialize[list[T]](view values) +function encode_public[T](view values: list[T]) returns string: + use json + return view values into json.serialize_public[list[T]]() +function selected_empty(public_mode: bool) returns string: + if public_mode: + return encode_public(view list()) + return encode(view list()) +function main(stdout: Stdout) returns nothing: + string baked = comptime selected_empty(false) + string public_baked = comptime selected_empty(true) + Stdout.write(view stdout, "{baked}:{public_baked}:{selected_empty(false)}:{selected_empty(true)}\n") + return nothing +verify pure_inferred_empty: + string baked = comptime selected_empty(false) + string public_baked = comptime selected_empty(true) + assert baked == "[]" + assert public_baked == "[]" + assert selected_empty(false) == baked + assert selected_empty(true) == public_baked +property inferred_empty_trials: + given public_mode: bool + assert selected_empty(public_mode) == "[]" diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_inhabited_function_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_inhabited_function_empty_list.jett new file mode 100644 index 00000000..9fd085e1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_inhabited_function_empty_list.jett @@ -0,0 +1,8 @@ +namespace app +function encode(view callbacks: list[function() returns int64]) returns string: + use json + return json.serialize[list[function() returns int64]](view callbacks) +function main(stdout: Stdout) returns nothing: + string encoded = encode(view list()) + Stdout.write(view stdout, "unexpected:{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_result_missing_error.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_result_missing_error.jett new file mode 100644 index 00000000..039bbcea --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/02_result_missing_error.jett @@ -0,0 +1,7 @@ +namespace app +function encode[Value, Failure](view candidate: result[Value, Failure]) returns string: + use json + return view candidate into json.serialize_public[result[Value, Failure]]() +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view ok(7))}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_public_inhabited_function_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_public_inhabited_function_empty_list.jett new file mode 100644 index 00000000..6a18ab58 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_public_inhabited_function_empty_list.jett @@ -0,0 +1,8 @@ +namespace app +function encode(view callbacks: list[function() returns int64]) returns string: + use json + return json.serialize_public[list[function() returns int64]](view callbacks) +function main(stdout: Stdout) returns nothing: + string encoded = encode(view list()) + Stdout.write(view stdout, "unexpected:{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_result_missing_success.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_result_missing_success.jett new file mode 100644 index 00000000..5e1ff557 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/03_result_missing_success.jett @@ -0,0 +1,8 @@ +namespace app +function encode[Value, Failure](view candidate: result[Value, Failure]) returns string: + use json + return json.serialize[result[Value, Failure]](view candidate) +function main(stdout: Stdout) returns nothing: + string encoded = encode(view fail("bad")) + Stdout.write(view stdout, "{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_public_empty_secret_slot.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_public_empty_secret_slot.jett new file mode 100644 index 00000000..de0414c1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_public_empty_secret_slot.jett @@ -0,0 +1,8 @@ +namespace app +function encode(view tokens: list[secret[string]]) returns string: + use json + return json.serialize_public[list[secret[string]]](view tokens) +function main(stdout: Stdout) returns nothing: + string encoded = encode(view list()) + Stdout.write(view stdout, "unexpected:{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_string_key_empty_map.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_string_key_empty_map.jett new file mode 100644 index 00000000..70742ad5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/04_string_key_empty_map.jett @@ -0,0 +1,7 @@ +namespace app +function encode[T](view entries: map[string, T]) returns string: + use json + return json.serialize_public[map[string, T]](view entries) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view map())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_generic_record_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_generic_record_empty_list.jett new file mode 100644 index 00000000..15529cf3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_generic_record_empty_list.jett @@ -0,0 +1,10 @@ +namespace app +struct Envelope[T]: + items: list[T] +function encode[T](view values: list[T]) returns string: + use json + Envelope[T] envelope = Envelope[T](items: clone values) + return json.serialize[Envelope[T]](view envelope) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_public_secret_field_projection.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_public_secret_field_projection.jett new file mode 100644 index 00000000..d07397c2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/05_public_secret_field_projection.jett @@ -0,0 +1,12 @@ +namespace app +struct Envelope[T]: + items: list[T] + token: secret[string] +function encode[T](view values: list[T]) returns string: + use json + secret[string] token = "private-token" + Envelope[T] envelope = Envelope[T](items: clone values, token: token) + return json.serialize_public[Envelope[T]](view envelope) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_invalid_uninhabited_source_body.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_invalid_uninhabited_source_body.jett new file mode 100644 index 00000000..69cfe5c7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_invalid_uninhabited_source_body.jett @@ -0,0 +1,10 @@ +namespace app +function encode[T](view values: list[T]) returns string: + use json + for item in view values: + T fabricated = 7 + return json.serialize[list[T]](view values) +function main(stdout: Stdout) returns nothing: + string encoded = encode(view list()) + Stdout.write(view stdout, "unexpected:{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_nested_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_nested_empty_list.jett new file mode 100644 index 00000000..ae2ac94e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/06_nested_empty_list.jett @@ -0,0 +1,7 @@ +namespace app +function encode[T](view values: list[list[T]]) returns string: + use json + return view values into json.serialize_public[list[list[T]]]() +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list(list()))}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/07_pending_empty_list.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/07_pending_empty_list.jett new file mode 100644 index 00000000..a46ffbbe --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/07_pending_empty_list.jett @@ -0,0 +1,10 @@ +namespace app +function encode[T](view values: list[T]) returns string: + use json + list[T] pending = run run clone values + return json.serialize[list[T]](view pending) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + string encoded = encode(view list()) + Stdout.write(view stdout, "after:{encoded}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json.jett new file mode 100644 index 00000000..77c3be03 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json.jett @@ -0,0 +1,7 @@ +namespace app +function encode[T](view values: list[T]) returns string: + use json + return json.serialize[list[T]](view values) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json_public.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json_public.jett new file mode 100644 index 00000000..be81fd95 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/inferred_empty_json_public.jett @@ -0,0 +1,7 @@ +namespace app +function encode[T](view values: list[T]) returns string: + use json + return json.serialize_public[list[T]](view values) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/owner_rereads.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/owner_rereads.jett new file mode 100644 index 00000000..032d2a41 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/owner_rereads.jett @@ -0,0 +1,18 @@ +namespace app +struct Envelope[T]: + items: list[T] +function encode[T](view values: list[T]) returns string: + use json + Envelope[T] owner = Envelope[T](items: clone values) + list[T] observed = view owner.items + string direct = json.serialize[list[T]](view observed) + string public_encoded = view observed into json.serialize_public[list[T]]() + Envelope[T] duplicate = clone owner + string copied = json.serialize[Envelope[T]](view duplicate) + int64 observed_count = list.length[T](view observed) + int64 owner_count = list.length[T](view owner.items) + int64 original_count = list.length[T](view values) + return "{direct}:{public_encoded}:{copied}:{observed_count}:{owner_count}:{original_count}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_inferred_slots/typed_empty_json_control.jett b/crates/jett_driver/tests/native_conformance/json_inferred_slots/typed_empty_json_control.jett new file mode 100644 index 00000000..cfd7631d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_inferred_slots/typed_empty_json_control.jett @@ -0,0 +1,7 @@ +namespace app +function encode(view values: list[int64]) returns string: + use json + return json.serialize[list[int64]](view values) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{encode(view list())}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines.rs b/crates/jett_driver/tests/native_conformance/json_qualified_machines.rs new file mode 100644 index 00000000..1bda062c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines.rs @@ -0,0 +1,501 @@ +//! Qualified JSON walks only the selected machine payload; bare requests keep policy checks. +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::native::{ + build_host_executable_with_options, build_host_property_suite_executable_with_options, + build_host_verify_suite_executable_with_options, +}; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +#[path = "json_qualified_machines/wrappers.rs"] +mod wrappers; + +#[derive(Clone, Copy)] +enum Oracle { + Exact(&'static str), + PureSuites(&'static str), + ParseErrors { exact: bool }, +} + +fn run_case(name: &str, source_text: &str, oracle: Oracle) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let reference = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + match oracle { + Oracle::Exact(expected) | Oracle::PureSuites(expected) => { + assert_eq!(reference.stdout, expected, "{name}"); + } + Oracle::ParseErrors { exact } => { + // Exact validation checks malformed field shape before the private + // key-domain refusal; lenient decoding reaches that refusal first. + let unsupported = "cached.entries: JSON object maps require string keys, got int64"; + let numeric = if exact { + "entries: expected object, got number" + } else { + unsupported + }; + let prefix = format!( + "wrong:error:{unsupported}\nnonempty:error:{unsupported}\nmissing:error:missing required field 'entries' for app.Session at ready.cached\nnumeric:error:{numeric}\n" + ); + assert!( + reference.stdout.starts_with(&prefix), + "{name}: {reference:?}" + ); + assert_eq!( + reference.stdout.lines().count(), + if exact { 8 } else { 7 }, + "{name}: {reference:?}" + ); + if exact { + assert!( + reference + .stdout + .contains("envelope:error:unknown field 'version'"), + "{name}: {reference:?}" + ); + assert!( + reference + .stdout + .contains("payload:error:unknown field 'extra'"), + "{name}: {reference:?}" + ); + } else { + assert!( + reference.stdout.contains("lenient:19\n"), + "{name}: {reference:?}" + ); + } + assert!( + reference.stdout.ends_with("after:23\nretained:1\n"), + "{name}: {reference:?}" + ); + } + } + if matches!(oracle, Oracle::PureSuites(_)) { + let checked = jett_driver::test_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference suites: {error:?}")); + assert_eq!((checked.total, checked.passed, checked.failed), (2, 2, 0)); + assert_eq!(checked.blocks.len(), 2); + assert!(!checked.blocks[0].is_property); + assert!(checked.blocks[1].is_property); + assert_eq!(checked.blocks[1].iterations, Some(100)); + assert!(checked.debug_observations.is_empty(), "{name}: {checked:?}"); + assert!( + checked + .blocks + .iter() + .all(|block| block.debug_events.is_empty()), + "{name}: {checked:?}" + ); + } + let mut binaries = Vec::new(); + let mut suite_binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + if matches!(oracle, Oracle::PureSuites(_)) { + let verify_binary = directory + .path() + .join(format!("{name}_verify_{release}.exe")); + let verify = build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &verify_binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: verify build: {error}")); + assert!(verify.debug_observations.is_empty(), "{name}: {verify:?}"); + suite_binaries.push((verify_binary, release)); + let property_binary = directory + .path() + .join(format!("{name}_property_{release}.exe")); + let property = build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &property_binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: property build: {error}")); + assert!( + property.debug_observations.is_empty(), + "{name}: {property:?}" + ); + suite_binaries.push((property_binary, release)); + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } + for (binary, release) in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert!( + actual.stdout.is_empty(), + "{name}, release={release}: {actual:?}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_qualified_machine_json_direct_serialize_matches_reference_without_source_in_both_profiles() + { + run_case( + "direct_serialize", + include_str!("json_qualified_machines/direct_serialize.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":7}}\nalive:7\n"), + ); +} + +#[test] +fn native_qualified_machine_json_direct_serialize_public_matches_reference_without_source_in_both_profiles() + { + run_case( + "direct_serialize_public", + include_str!("json_qualified_machines/direct_serialize_public.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":8}}\nalive:8\n"), + ); +} + +#[test] +fn native_qualified_machine_json_direct_parse_matches_reference_without_source_in_both_profiles() { + run_case( + "direct_parse", + include_str!("json_qualified_machines/direct_parse.jett"), + Oracle::Exact("9\n"), + ); +} + +#[test] +fn native_qualified_machine_json_direct_parse_exact_matches_reference_without_source_in_both_profiles() + { + run_case( + "direct_parse_exact", + include_str!("json_qualified_machines/direct_parse_exact.jett"), + Oracle::Exact("11\n"), + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_serialize_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_serialize", + include_str!("json_qualified_machines/pipeline_serialize.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":12}}\nalive:12\n"), + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_serialize_public_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_serialize_public", + include_str!("json_qualified_machines/pipeline_serialize_public.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":13}}\nalive:13\n"), + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_parse_matches_reference_without_source_in_both_profiles() +{ + run_case( + "pipeline_parse", + include_str!("json_qualified_machines/pipeline_parse.jett"), + Oracle::Exact("14\n"), + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_parse_exact_matches_reference_without_source_in_both_profiles() + { + run_case( + "pipeline_parse_exact", + include_str!("json_qualified_machines/pipeline_parse_exact.jett"), + Oracle::Exact("15\n"), + ); +} + +#[test] +fn native_qualified_machine_json_nested_serialize_alias_preserves_selected_payload() { + run_case( + "nested_serialize", + include_str!("json_qualified_machines/nested_serialize.jett"), + Oracle::Exact( + "[{\"state\":\"ready\",\"payload\":{\"count\":21}},{\"state\":\"ready\",\"payload\":{\"count\":22}}]\n21,22,\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_nested_serialize_public_alias_preserves_selected_payload() { + run_case( + "nested_serialize_public", + include_str!("json_qualified_machines/nested_serialize_public.jett"), + Oracle::Exact( + "[{\"state\":\"ready\",\"payload\":{\"count\":21}},{\"state\":\"ready\",\"payload\":{\"count\":22}}]\n21,22,\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_nested_parse_alias_preserves_selected_payload() { + run_case( + "nested_parse", + include_str!("json_qualified_machines/nested_parse.jett"), + Oracle::Exact("21,22,\n"), + ); +} + +#[test] +fn native_qualified_machine_json_nested_parse_exact_alias_preserves_selected_payload() { + run_case( + "nested_parse_exact", + include_str!("json_qualified_machines/nested_parse_exact.jett"), + Oracle::Exact("21,22,\n"), + ); +} + +#[test] +fn native_qualified_machine_json_direct_parse_errors_preserves_error_order_and_cleanup() { + run_case( + "direct_parse_errors", + include_str!("json_qualified_machines/direct_parse_errors.jett"), + Oracle::ParseErrors { exact: false }, + ); +} + +#[test] +fn native_qualified_machine_json_direct_parse_exact_errors_preserves_error_order_and_cleanup() { + run_case( + "direct_parse_exact_errors", + include_str!("json_qualified_machines/direct_parse_exact_errors.jett"), + Oracle::ParseErrors { exact: true }, + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_parse_errors_preserves_error_order_and_cleanup() { + run_case( + "pipeline_parse_errors", + include_str!("json_qualified_machines/pipeline_parse_errors.jett"), + Oracle::ParseErrors { exact: false }, + ); +} + +#[test] +fn native_qualified_machine_json_pipeline_parse_exact_errors_preserves_error_order_and_cleanup() { + run_case( + "pipeline_parse_exact_errors", + include_str!("json_qualified_machines/pipeline_parse_exact_errors.jett"), + Oracle::ParseErrors { exact: true }, + ); +} + +#[test] +fn native_qualified_machine_json_primitive_pipeline_parse_uses_the_source_decoder_independently() { + run_case( + "primitive_pipeline_parse", + include_str!("json_qualified_machines/primitive_pipeline_parse.jett"), + Oracle::Exact("14\nraw:14\n"), + ); +} + +#[test] +fn native_qualified_machine_json_primitive_pipeline_parse_exact_uses_the_source_decoder_independently() + { + run_case( + "primitive_pipeline_parse_exact", + include_str!("json_qualified_machines/primitive_pipeline_parse_exact.jett"), + Oracle::Exact("14\nraw:14\n"), + ); +} + +#[test] +fn native_qualified_machine_json_pure_comptime_and_compiled_suites_use_selected_payload() { + run_case( + "pure_selected_suites", + include_str!("json_qualified_machines/pure_selected_suites.jett"), + Oracle::PureSuites("{\"state\":\"ready\",\"payload\":{\"count\":27}}\n"), + ); +} + +#[test] +fn native_qualified_machine_json_bare_and_invalid_selected_payloads_keep_policy_rejection() { + let cases = [ + ( + "reject_bare_serialize", + include_str!("json_qualified_machines/reject_bare_serialize.jett"), + ), + ( + "reject_bare_serialize_public", + include_str!("json_qualified_machines/reject_bare_serialize_public.jett"), + ), + ( + "reject_bare_parse", + include_str!("json_qualified_machines/reject_bare_parse.jett"), + ), + ( + "reject_bare_parse_exact", + include_str!("json_qualified_machines/reject_bare_parse_exact.jett"), + ), + ( + "reject_cached_serialize", + include_str!("json_qualified_machines/reject_cached_serialize.jett"), + ), + ( + "reject_cached_serialize_public", + include_str!("json_qualified_machines/reject_cached_serialize_public.jett"), + ), + ( + "reject_cached_parse", + include_str!("json_qualified_machines/reject_cached_parse.jett"), + ), + ( + "reject_cached_parse_exact", + include_str!("json_qualified_machines/reject_cached_parse_exact.jett"), + ), + ( + "optional_bare_policy_negative", + include_str!("json_qualified_machines/wrappers/optional_bare_policy_negative.jett"), + ), + ( + "result_bare_policy_negative", + include_str!("json_qualified_machines/wrappers/result_bare_policy_negative.jett"), + ), + ( + "map_bare_policy_negative", + include_str!("json_qualified_machines/wrappers/map_bare_policy_negative.jett"), + ), + ]; + for (name, text) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, text).unwrap(); + for release in [false, true] { + let options = BuildOptions { release }; + let checked = jett_driver::build_file_with_options(&source, options); + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{name}, release={release}: {errors:?}"); + assert_eq!( + errors[0].code.code(), + 343, + "{name}, release={release}: {errors:?}" + ); + let binary = directory.path().join(format!("{name}_{release}.exe")); + let sentinel = b"existing qualified JSON publication"; + fs::write(&binary, sentinel).unwrap(); + let error = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + options, + ) + .expect_err("unsupported map keys must remain frontend policy errors"); + let build = error.build_result().expect("typed frontend failure"); + assert!( + build + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 343) + ); + assert_eq!(fs::read(&binary).unwrap(), sentinel); + } + } +} + +#[test] +fn native_qualified_machine_json_string_key_aliases_keep_exact_canonical_map_type() { + run_case( + "string_key_aliases", + include_str!("json_qualified_machines/string_key_aliases.jett"), + Oracle::Exact("41:41:{\"answer\":41}\n"), + ); +} + +#[test] +fn native_qualified_machine_json_all_state_metadata_loops_keep_alias_and_empty_dispatch() { + run_case( + "qualified_state_fields", + include_str!("json_qualified_machines/qualified_state_fields.jett"), + Oracle::Exact(concat!( + "ready:ready.count:app.Count;cached.entries:app.Entries;cached.backup:app.BackupCount;\n", + "empty:ready.count:app.Count;cached.entries:app.Entries;cached.backup:app.BackupCount;\n", + "selected:count:app.Count;\n", + "selected-empty:\n", + "retained:5\n", + )), + ); +} + +#[test] +fn native_qualified_machine_json_unselected_callable_interface_actor_payloads_skip_raw_decoder() { + let expected = concat!( + "{\"state\":\"ready\",\"payload\":{\"count\":31}}\n", + "{\"state\":\"ready\",\"payload\":{\"count\":31}}\n", + "32:33:31\n", + ); + let cases = [ + ( + "unselected_callable", + include_str!("json_qualified_machines/unselected_callable.jett"), + ), + ( + "unselected_interface", + include_str!("json_qualified_machines/unselected_interface.jett"), + ), + ( + "unselected_actor", + include_str!("json_qualified_machines/unselected_actor.jett"), + ), + ]; + for (name, text) in cases { + run_case(name, text, Oracle::Exact(expected)); + } +} diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse.jett new file mode 100644 index 00000000..5723e4cc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + string raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":9}}}}" + Session at ready value = json.parse[Session at ready](raw) handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_errors.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_errors.jett new file mode 100644 index 00000000..9c3b8d23 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_errors.jett @@ -0,0 +1,28 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function observed_count(raw: string) returns string: + Session at ready value = json.parse[Session at ready](raw) handle error: + return "error:{error}" + return "{value.count}" +function write_invalid_payloads(view stdout: Stdout) returns nothing: + string nonempty = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{\"x\":1}}}}}}") + Stdout.write(view stdout, "nonempty:{nonempty}\n") + string missing = observed_count("{{\"state\":\"cached\",\"payload\":{{}}}}") + Stdout.write(view stdout, "missing:{missing}\n") + string numeric = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":7}}}}") + Stdout.write(view stdout, "numeric:{numeric}\n") +function main(stdout: Stdout) returns nothing: + list[string] retained = list("kept") + string wrong = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{}}}}}}") + Stdout.write(view stdout, "wrong:{wrong}\n") + write_invalid_payloads(view stdout) + string lenient = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19,\"extra\":true}},\"version\":1}}") + Stdout.write(view stdout, "lenient:{lenient}\n") + string after = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":23}}}}") + Stdout.write(view stdout, "after:{after}\n") + Stdout.write(view stdout, "retained:{list.length[string](view retained)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact.jett new file mode 100644 index 00000000..ae88d815 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + string raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":11}}}}" + Session at ready value = json.parse_exact[Session at ready](raw) handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact_errors.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact_errors.jett new file mode 100644 index 00000000..fee8034e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_parse_exact_errors.jett @@ -0,0 +1,30 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function observed_count(raw: string) returns string: + Session at ready value = json.parse_exact[Session at ready](raw) handle error: + return "error:{error}" + return "{value.count}" +function write_invalid_payloads(view stdout: Stdout) returns nothing: + string nonempty = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{\"x\":1}}}}}}") + Stdout.write(view stdout, "nonempty:{nonempty}\n") + string missing = observed_count("{{\"state\":\"cached\",\"payload\":{{}}}}") + Stdout.write(view stdout, "missing:{missing}\n") + string numeric = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":7}}}}") + Stdout.write(view stdout, "numeric:{numeric}\n") +function main(stdout: Stdout) returns nothing: + list[string] retained = list("kept") + string wrong = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{}}}}}}") + Stdout.write(view stdout, "wrong:{wrong}\n") + write_invalid_payloads(view stdout) + string envelope_error = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19}},\"version\":1}}") + Stdout.write(view stdout, "envelope:{envelope_error}\n") + string payload_error = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19,\"extra\":true}}}}") + Stdout.write(view stdout, "payload:{payload_error}\n") + string after = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":23}}}}") + Stdout.write(view stdout, "after:{after}\n") + Stdout.write(view stdout, "retained:{list.length[string](view retained)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize.jett new file mode 100644 index 00000000..0f20e6cb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + Session at ready value = Session(ready, 7) + string encoded = json.serialize[Session at ready](view value) + Stdout.write(view stdout, "{encoded}\n") + Stdout.write(view stdout, "alive:{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize_public.jett new file mode 100644 index 00000000..299db48f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/direct_serialize_public.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + Session at ready value = Session(ready, 8) + string encoded = json.serialize_public[Session at ready](view value) + Stdout.write(view stdout, "{encoded}\n") + Stdout.write(view stdout, "alive:{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse.jett new file mode 100644 index 00000000..3b4cd1c0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + string raw = "[{{\"state\":\"ready\",\"payload\":{{\"count\":21}}}},{{\"state\":\"ready\",\"payload\":{{\"count\":22}}}}]" + list[Ready] values = json.parse[list[Ready]](raw) handle error: + return Stdout.write(view stdout, "error:{error}\n") + for value in view values: + Stdout.write(view stdout, "{value.count},") + Stdout.write(view stdout, "\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse_exact.jett new file mode 100644 index 00000000..dd73b4a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_parse_exact.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + string raw = "[{{\"state\":\"ready\",\"payload\":{{\"count\":21}}}},{{\"state\":\"ready\",\"payload\":{{\"count\":22}}}}]" + list[Ready] values = raw into json.parse_exact[list[Ready]]() handle error: + return Stdout.write(view stdout, "error:{error}\n") + for value in view values: + Stdout.write(view stdout, "{value.count},") + Stdout.write(view stdout, "\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize.jett new file mode 100644 index 00000000..ee35239e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + list[Ready] values = list(Session(ready, 21), Session(ready, 22)) + string encoded = json.serialize[list[Ready]](view values) + Stdout.write(view stdout, "{encoded}\n") + for value in view values: + Stdout.write(view stdout, "{value.count},") + Stdout.write(view stdout, "\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize_public.jett new file mode 100644 index 00000000..b30ed4be --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/nested_serialize_public.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + list[Ready] values = list(Session(ready, 21), Session(ready, 22)) + string encoded = values into view json.serialize_public[list[Ready]]() + Stdout.write(view stdout, "{encoded}\n") + for value in view values: + Stdout.write(view stdout, "{value.count},") + Stdout.write(view stdout, "\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse.jett new file mode 100644 index 00000000..71f862bd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + string raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":14}}}}" + Session at ready value = raw into json.parse[Session at ready]() handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_errors.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_errors.jett new file mode 100644 index 00000000..5cb85196 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_errors.jett @@ -0,0 +1,28 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function observed_count(raw: string) returns string: + Session at ready value = raw into json.parse[Session at ready]() handle error: + return "error:{error}" + return "{value.count}" +function write_invalid_payloads(view stdout: Stdout) returns nothing: + string nonempty = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{\"x\":1}}}}}}") + Stdout.write(view stdout, "nonempty:{nonempty}\n") + string missing = observed_count("{{\"state\":\"cached\",\"payload\":{{}}}}") + Stdout.write(view stdout, "missing:{missing}\n") + string numeric = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":7}}}}") + Stdout.write(view stdout, "numeric:{numeric}\n") +function main(stdout: Stdout) returns nothing: + list[string] retained = list("kept") + string wrong = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{}}}}}}") + Stdout.write(view stdout, "wrong:{wrong}\n") + write_invalid_payloads(view stdout) + string lenient = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19,\"extra\":true}},\"version\":1}}") + Stdout.write(view stdout, "lenient:{lenient}\n") + string after = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":23}}}}") + Stdout.write(view stdout, "after:{after}\n") + Stdout.write(view stdout, "retained:{list.length[string](view retained)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact.jett new file mode 100644 index 00000000..6be3b0db --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + string raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":15}}}}" + Session at ready value = raw into json.parse_exact[Session at ready]() handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact_errors.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact_errors.jett new file mode 100644 index 00000000..65bab5e2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_parse_exact_errors.jett @@ -0,0 +1,30 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function observed_count(raw: string) returns string: + Session at ready value = raw into json.parse_exact[Session at ready]() handle error: + return "error:{error}" + return "{value.count}" +function write_invalid_payloads(view stdout: Stdout) returns nothing: + string nonempty = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{\"x\":1}}}}}}") + Stdout.write(view stdout, "nonempty:{nonempty}\n") + string missing = observed_count("{{\"state\":\"cached\",\"payload\":{{}}}}") + Stdout.write(view stdout, "missing:{missing}\n") + string numeric = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":7}}}}") + Stdout.write(view stdout, "numeric:{numeric}\n") +function main(stdout: Stdout) returns nothing: + list[string] retained = list("kept") + string wrong = observed_count("{{\"state\":\"cached\",\"payload\":{{\"entries\":{{}}}}}}") + Stdout.write(view stdout, "wrong:{wrong}\n") + write_invalid_payloads(view stdout) + string envelope_error = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19}},\"version\":1}}") + Stdout.write(view stdout, "envelope:{envelope_error}\n") + string payload_error = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":19,\"extra\":true}}}}") + Stdout.write(view stdout, "payload:{payload_error}\n") + string after = observed_count("{{\"state\":\"ready\",\"payload\":{{\"count\":23}}}}") + Stdout.write(view stdout, "after:{after}\n") + Stdout.write(view stdout, "retained:{list.length[string](view retained)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize.jett new file mode 100644 index 00000000..a9d57dbc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + Session at ready value = Session(ready, 12) + string encoded = value into view json.serialize[Session at ready]() + Stdout.write(view stdout, "{encoded}\n") + Stdout.write(view stdout, "alive:{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize_public.jett new file mode 100644 index 00000000..63975f5d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pipeline_serialize_public.jett @@ -0,0 +1,12 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + Session at ready value = Session(ready, 13) + string encoded = value into view json.serialize_public[Session at ready]() + Stdout.write(view stdout, "{encoded}\n") + Stdout.write(view stdout, "alive:{value.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse.jett new file mode 100644 index 00000000..5a9272eb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse.jett @@ -0,0 +1,7 @@ +namespace app +function main(stdout: Stdout) returns nothing: + string raw = "14" + int64 value = clone raw into json.parse[int64]() handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value}\n") + Stdout.write(view stdout, "raw:{raw}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse_exact.jett new file mode 100644 index 00000000..f3b2a6d1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/primitive_pipeline_parse_exact.jett @@ -0,0 +1,7 @@ +namespace app +function main(stdout: Stdout) returns nothing: + string raw = "14" + int64 value = clone raw into json.parse_exact[int64]() handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "{value}\n") + Stdout.write(view stdout, "raw:{raw}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/pure_selected_suites.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pure_selected_suites.jett new file mode 100644 index 00000000..951ff04e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/pure_selected_suites.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function selected_json() returns string: + Session at ready value = Session(ready, 27) + return value into view json.serialize[Session at ready]() +function main(stdout: Stdout) returns nothing: + string baked = comptime selected_json() + Stdout.write(view stdout, "{baked}\n") +verify selected_ready: + string baked = comptime selected_json() + assert baked == "{{\"state\":\"ready\",\"payload\":{{\"count\":27}}}}" + assert selected_json() == baked +property selected_ready_trials: + given checked: bool + assert checked == checked + assert selected_json() == "{{\"state\":\"ready\",\"payload\":{{\"count\":27}}}}" diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/qualified_state_fields.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/qualified_state_fields.jett new file mode 100644 index 00000000..06a13e9a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/qualified_state_fields.jett @@ -0,0 +1,37 @@ +namespace app +type Count = int64 +type Entries = map[int64, int64] +type BackupCount = int64 +machine Session: + states: + empty + ready(count: Count) + cached(entries: Entries, backup: BackupCount) + transitions: + empty to ready + ready to cached +type Ready = Session at ready +type Empty = Session at empty +function declared_fields[T]() returns string: + mutable string names = "" + for state in type.machine_states[T](): + for field in state.fields: + comptime type Field = field.type_info: + string name = type.name[Field]() + names = "{names}{state.name}.{field.name}:{name};" + return names +function selected_fields[T](view value: T) returns string: + mutable string names = "" + for field in type.machine_state_value[T](view value).fields: + comptime type Field = field.type_info: + string name = type.name[Field]() + names = "{names}{field.name}:{name};" + return names +function main(stdout: Stdout) returns nothing: + Ready source = Session(ready, 5) + Empty empty_source = Session(empty) + Stdout.write(view stdout, "ready:{declared_fields[Session at ready]()}\n") + Stdout.write(view stdout, "empty:{declared_fields[Session at empty]()}\n") + Stdout.write(view stdout, "selected:{selected_fields[Session at ready](view source)}\n") + Stdout.write(view stdout, "selected-empty:{selected_fields[Session at empty](view empty_source)}\n") + Stdout.write(view stdout, "retained:{source.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse.jett new file mode 100644 index 00000000..01ca7f2e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse.jett @@ -0,0 +1,13 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(raw: string) returns string: + Session value = json.parse[Session](raw) handle error: + return error + return "unexpected" +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse_exact.jett new file mode 100644 index 00000000..391aeb34 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_parse_exact.jett @@ -0,0 +1,13 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(raw: string) returns string: + Session value = json.parse_exact[Session](raw) handle error: + return error + return "unexpected" +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize.jett new file mode 100644 index 00000000..3036585f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(view value: Session) returns string: + return json.serialize[Session](view value) +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize_public.jett new file mode 100644 index 00000000..233eecac --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_bare_serialize_public.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(view value: Session) returns string: + return json.serialize_public[Session](view value) +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse.jett new file mode 100644 index 00000000..0637abba --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse.jett @@ -0,0 +1,13 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(raw: string) returns string: + Session at cached value = json.parse[Session at cached](raw) handle error: + return error + return "unexpected" +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse_exact.jett new file mode 100644 index 00000000..1e710755 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_parse_exact.jett @@ -0,0 +1,13 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(raw: string) returns string: + Session at cached value = json.parse_exact[Session at cached](raw) handle error: + return error + return "unexpected" +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize.jett new file mode 100644 index 00000000..bca824bb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(view value: Session at cached) returns string: + return json.serialize[Session at cached](view value) +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize_public.jett new file mode 100644 index 00000000..9d1392de --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/reject_cached_serialize_public.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function rejected(view value: Session at cached) returns string: + return json.serialize_public[Session at cached](view value) +function main() returns nothing: + return nothing diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/string_key_aliases.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/string_key_aliases.jett new file mode 100644 index 00000000..52f3a661 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/string_key_aliases.jett @@ -0,0 +1,14 @@ +namespace app +type Label = string +type KeyLabel = Label +type Number = int64 +type Values = map[KeyLabel, Number] +function main(stdout: Stdout) returns nothing: + string raw = "{{\"answer\":41}}" + Values direct = json.parse[Values](raw) handle error: + return Stdout.write(view stdout, "direct error:{error}\n") + Values piped = clone raw into json.parse_exact[Values]() handle error: + return Stdout.write(view stdout, "pipeline error:{error}\n") + int64 first = map.get_or[KeyLabel, Number](clone direct, "answer", 0) + int64 second = map.get_or[KeyLabel, Number](clone piped, "answer", 0) + Stdout.write(view stdout, "{first}:{second}:{raw}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_actor.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_actor.jett new file mode 100644 index 00000000..726c4d9e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_actor.jett @@ -0,0 +1,21 @@ +namespace app +actor Counter: + mutable int64 total = 0 + receive read responds int64: + respond total +machine Session: + states: + ready(count: int64) + cached(owner: Counter) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + Ready original = Session(ready, 31) + string encoded = json.serialize[Ready](view original) + string public_encoded = json.serialize_public[Ready](view original) + Ready parsed = json.parse[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":32}}}}") handle error: + return Stdout.write(view stdout, "parse error:{error}\n") + Ready exact = json.parse_exact[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":33}}}}") handle error: + return Stdout.write(view stdout, "exact error:{error}\n") + Stdout.write(view stdout, "{encoded}\n{public_encoded}\n{parsed.count}:{exact.count}:{original.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_callable.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_callable.jett new file mode 100644 index 00000000..63e7f92f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_callable.jett @@ -0,0 +1,18 @@ +namespace app +type Callback = function(int64) returns int64 +machine Session: + states: + ready(count: int64) + cached(callback: Callback) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + Ready original = Session(ready, 31) + string encoded = json.serialize[Ready](view original) + string public_encoded = json.serialize_public[Ready](view original) + Ready parsed = json.parse[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":32}}}}") handle error: + return Stdout.write(view stdout, "parse error:{error}\n") + Ready exact = json.parse_exact[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":33}}}}") handle error: + return Stdout.write(view stdout, "exact error:{error}\n") + Stdout.write(view stdout, "{encoded}\n{public_encoded}\n{parsed.count}:{exact.count}:{original.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_interface.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_interface.jett new file mode 100644 index 00000000..df315ba8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/unselected_interface.jett @@ -0,0 +1,19 @@ +namespace app +interface Named: + function name(view self: Named) returns string +machine Session: + states: + ready(count: int64) + cached(owner: Named) + transitions: + ready to cached +type Ready = Session at ready +function main(stdout: Stdout) returns nothing: + Ready original = Session(ready, 31) + string encoded = json.serialize[Ready](view original) + string public_encoded = json.serialize_public[Ready](view original) + Ready parsed = json.parse[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":32}}}}") handle error: + return Stdout.write(view stdout, "parse error:{error}\n") + Ready exact = json.parse_exact[Ready]("{{\"state\":\"ready\",\"payload\":{{\"count\":33}}}}") handle error: + return Stdout.write(view stdout, "exact error:{error}\n") + Stdout.write(view stdout, "{encoded}\n{public_encoded}\n{parsed.count}:{exact.count}:{original.count}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers.rs b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers.rs new file mode 100644 index 00000000..daae73f1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers.rs @@ -0,0 +1,258 @@ +//! Selected machine payloads inside optional, result, and string-keyed map wrappers. +use super::{Oracle, run_case}; + +#[test] +fn native_qualified_machine_json_wrapper_optional_direct_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_direct_serialize", + include_str!("wrappers/optional_direct_serialize.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":41}}\nnull\nretained:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_pipeline_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_pipeline_serialize", + include_str!("wrappers/optional_pipeline_serialize.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":41}}\nnull\nretained:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_direct_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_direct_serialize_public", + include_str!("wrappers/optional_direct_serialize_public.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":41}}\nnull\nretained:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_pipeline_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_pipeline_serialize_public", + include_str!("wrappers/optional_pipeline_serialize_public.jett"), + Oracle::Exact("{\"state\":\"ready\",\"payload\":{\"count\":41}}\nnull\nretained:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_direct_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_direct_parse", + include_str!("wrappers/optional_direct_parse.jett"), + Oracle::Exact("parsed:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_pipeline_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_pipeline_parse", + include_str!("wrappers/optional_pipeline_parse.jett"), + Oracle::Exact("parsed:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_direct_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_direct_parse_exact", + include_str!("wrappers/optional_direct_parse_exact.jett"), + Oracle::Exact("parsed:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_optional_pipeline_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_optional_pipeline_parse_exact", + include_str!("wrappers/optional_pipeline_parse_exact.jett"), + Oracle::Exact("parsed:41:none\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_direct_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_direct_serialize", + include_str!("wrappers/result_direct_serialize.jett"), + Oracle::Exact( + "{\"ok\":{\"state\":\"ready\",\"payload\":{\"count\":51}}}\n{\"fail\":{\"state\":\"ready\",\"payload\":{\"count\":52}}}\nretained:ok:51:fail:52\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_pipeline_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_pipeline_serialize", + include_str!("wrappers/result_pipeline_serialize.jett"), + Oracle::Exact( + "{\"ok\":{\"state\":\"ready\",\"payload\":{\"count\":51}}}\n{\"fail\":{\"state\":\"ready\",\"payload\":{\"count\":52}}}\nretained:ok:51:fail:52\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_direct_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_direct_serialize_public", + include_str!("wrappers/result_direct_serialize_public.jett"), + Oracle::Exact( + "{\"ok\":{\"state\":\"ready\",\"payload\":{\"count\":51}}}\n{\"fail\":{\"state\":\"ready\",\"payload\":{\"count\":52}}}\nretained:ok:51:fail:52\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_pipeline_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_pipeline_serialize_public", + include_str!("wrappers/result_pipeline_serialize_public.jett"), + Oracle::Exact( + "{\"ok\":{\"state\":\"ready\",\"payload\":{\"count\":51}}}\n{\"fail\":{\"state\":\"ready\",\"payload\":{\"count\":52}}}\nretained:ok:51:fail:52\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_direct_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_direct_parse", + include_str!("wrappers/result_direct_parse.jett"), + Oracle::Exact("parsed:ok:51:fail:52\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_pipeline_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_pipeline_parse", + include_str!("wrappers/result_pipeline_parse.jett"), + Oracle::Exact("parsed:ok:51:fail:52\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_direct_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_direct_parse_exact", + include_str!("wrappers/result_direct_parse_exact.jett"), + Oracle::Exact("parsed:ok:51:fail:52\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_result_pipeline_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_result_pipeline_parse_exact", + include_str!("wrappers/result_pipeline_parse_exact.jett"), + Oracle::Exact("parsed:ok:51:fail:52\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_direct_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_direct_serialize", + include_str!("wrappers/map_direct_serialize.jett"), + Oracle::Exact( + "{\"entry\":{\"state\":\"ready\",\"payload\":{\"count\":61}}}\n{}\nretained:61:-1\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_pipeline_serialize_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_pipeline_serialize", + include_str!("wrappers/map_pipeline_serialize.jett"), + Oracle::Exact( + "{\"entry\":{\"state\":\"ready\",\"payload\":{\"count\":61}}}\n{}\nretained:61:-1\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_direct_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_direct_serialize_public", + include_str!("wrappers/map_direct_serialize_public.jett"), + Oracle::Exact( + "{\"entry\":{\"state\":\"ready\",\"payload\":{\"count\":61}}}\n{}\nretained:61:-1\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_pipeline_serialize_public_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_pipeline_serialize_public", + include_str!("wrappers/map_pipeline_serialize_public.jett"), + Oracle::Exact( + "{\"entry\":{\"state\":\"ready\",\"payload\":{\"count\":61}}}\n{}\nretained:61:-1\n", + ), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_direct_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_direct_parse", + include_str!("wrappers/map_direct_parse.jett"), + Oracle::Exact("parsed:61:-1\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_pipeline_parse_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_pipeline_parse", + include_str!("wrappers/map_pipeline_parse.jett"), + Oracle::Exact("parsed:61:-1\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_direct_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_direct_parse_exact", + include_str!("wrappers/map_direct_parse_exact.jett"), + Oracle::Exact("parsed:61:-1\n"), + ); +} + +#[test] +fn native_qualified_machine_json_wrapper_map_pipeline_parse_exact_matches_reference_in_both_profiles_without_source() + { + run_case( + "wrapper_map_pipeline_parse_exact", + include_str!("wrappers/map_pipeline_parse_exact.jett"), + Oracle::Exact("parsed:61:-1\n"), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_bare_policy_negative.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_bare_policy_negative.jett new file mode 100644 index 00000000..d985abbe --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_bare_policy_negative.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + map[string, Session] value = json.parse[map[string, Session]]("{{}}") handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "unexpected admission\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse.jett new file mode 100644 index 00000000..6d2a9185 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"entry\":{{\"state\":\"ready\",\"payload\":{{\"count\":61}}}}}}" + map[string, Session at ready] first = json.parse[map[string, Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{}}" + map[string, Session at ready] second = json.parse[map[string, Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse_exact.jett new file mode 100644 index 00000000..df824e6a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_parse_exact.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"entry\":{{\"state\":\"ready\",\"payload\":{{\"count\":61}}}}}}" + map[string, Session at ready] first = json.parse_exact[map[string, Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{}}" + map[string, Session at ready] second = json.parse_exact[map[string, Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize.jett new file mode 100644 index 00000000..845becf3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 61) + map[string, Session at ready] first = map("entry": seed) + map[string, Session at ready] second = map.new[string, Session at ready]() + string first_encoded = json.serialize[map[string, Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize[map[string, Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize_public.jett new file mode 100644 index 00000000..262c4ba4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_direct_serialize_public.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 61) + map[string, Session at ready] first = map("entry": seed) + map[string, Session at ready] second = map.new[string, Session at ready]() + string first_encoded = json.serialize_public[map[string, Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize_public[map[string, Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse.jett new file mode 100644 index 00000000..0b5e1c1d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"entry\":{{\"state\":\"ready\",\"payload\":{{\"count\":61}}}}}}" + map[string, Session at ready] first = first_raw into json.parse[map[string, Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{}}" + map[string, Session at ready] second = second_raw into json.parse[map[string, Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse_exact.jett new file mode 100644 index 00000000..32b394dd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_parse_exact.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"entry\":{{\"state\":\"ready\",\"payload\":{{\"count\":61}}}}}}" + map[string, Session at ready] first = first_raw into json.parse_exact[map[string, Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{}}" + map[string, Session at ready] second = second_raw into json.parse_exact[map[string, Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize.jett new file mode 100644 index 00000000..ba86b2f9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 61) + map[string, Session at ready] first = map("entry": seed) + map[string, Session at ready] second = map.new[string, Session at ready]() + string first_encoded = first into view json.serialize[map[string, Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize[map[string, Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize_public.jett new file mode 100644 index 00000000..4d7fd9b5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/map_pipeline_serialize_public.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function map_count(view values: map[string, Session at ready]) returns int64: + optional[Session at ready] maybe = map.get[string, Session at ready](clone values, "entry") + Session at ready item = maybe handle: + return -1 + return item.count +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 61) + map[string, Session at ready] first = map("entry": seed) + map[string, Session at ready] second = map.new[string, Session at ready]() + string first_encoded = first into view json.serialize_public[map[string, Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize_public[map[string, Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{map_count(view first)}:{map_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_bare_policy_negative.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_bare_policy_negative.jett new file mode 100644 index 00000000..0dba3134 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_bare_policy_negative.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + optional[Session] value = json.parse[optional[Session]]("null") handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "unexpected admission\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse.jett new file mode 100644 index 00000000..a0994ecb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":41}}}}" + optional[Session at ready] first = json.parse[optional[Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "null" + optional[Session at ready] second = json.parse[optional[Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse_exact.jett new file mode 100644 index 00000000..b23be23a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_parse_exact.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":41}}}}" + optional[Session at ready] first = json.parse_exact[optional[Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "null" + optional[Session at ready] second = json.parse_exact[optional[Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize.jett new file mode 100644 index 00000000..dab469ea --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 41) + optional[Session at ready] first = some(seed) + optional[Session at ready] second = none + string first_encoded = json.serialize[optional[Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize[optional[Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize_public.jett new file mode 100644 index 00000000..f52ea962 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_direct_serialize_public.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 41) + optional[Session at ready] first = some(seed) + optional[Session at ready] second = none + string first_encoded = json.serialize_public[optional[Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize_public[optional[Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse.jett new file mode 100644 index 00000000..fb440d84 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":41}}}}" + optional[Session at ready] first = first_raw into json.parse[optional[Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "null" + optional[Session at ready] second = second_raw into json.parse[optional[Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse_exact.jett new file mode 100644 index 00000000..719dcd7c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_parse_exact.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"state\":\"ready\",\"payload\":{{\"count\":41}}}}" + optional[Session at ready] first = first_raw into json.parse_exact[optional[Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "null" + optional[Session at ready] second = second_raw into json.parse_exact[optional[Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize.jett new file mode 100644 index 00000000..a0ea23e8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 41) + optional[Session at ready] first = some(seed) + optional[Session at ready] second = none + string first_encoded = first into view json.serialize[optional[Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize[optional[Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize_public.jett new file mode 100644 index 00000000..a89378e9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/optional_pipeline_serialize_public.jett @@ -0,0 +1,20 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function optional_count(view value: optional[Session at ready]) returns string: + Session at ready item = clone value handle: + return "none" + return "{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready seed = Session(ready, 41) + optional[Session at ready] first = some(seed) + optional[Session at ready] second = none + string first_encoded = first into view json.serialize_public[optional[Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize_public[optional[Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{optional_count(view first)}:{optional_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_bare_policy_negative.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_bare_policy_negative.jett new file mode 100644 index 00000000..bc0170a9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_bare_policy_negative.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function main(stdout: Stdout) returns nothing: + result[Session, string] value = json.parse[result[Session, string]]("{{\"fail\":\"inactive\"}}") handle error: + return Stdout.write(view stdout, "error:{error}\n") + Stdout.write(view stdout, "unexpected admission\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse.jett new file mode 100644 index 00000000..ad97bb44 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"ok\":{{\"state\":\"ready\",\"payload\":{{\"count\":51}}}}}}" + result[Session at ready, Session at ready] first = json.parse[result[Session at ready, Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{\"fail\":{{\"state\":\"ready\",\"payload\":{{\"count\":52}}}}}}" + result[Session at ready, Session at ready] second = json.parse[result[Session at ready, Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse_exact.jett new file mode 100644 index 00000000..0b9c90fb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_parse_exact.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"ok\":{{\"state\":\"ready\",\"payload\":{{\"count\":51}}}}}}" + result[Session at ready, Session at ready] first = json.parse_exact[result[Session at ready, Session at ready]](first_raw) handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{\"fail\":{{\"state\":\"ready\",\"payload\":{{\"count\":52}}}}}}" + result[Session at ready, Session at ready] second = json.parse_exact[result[Session at ready, Session at ready]](second_raw) handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize.jett new file mode 100644 index 00000000..94eaf3af --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready first_seed = Session(ready, 51) + Session at ready second_seed = Session(ready, 52) + result[Session at ready, Session at ready] first = ok(first_seed) + result[Session at ready, Session at ready] second = fail(second_seed) + string first_encoded = json.serialize[result[Session at ready, Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize[result[Session at ready, Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize_public.jett new file mode 100644 index 00000000..9c50a764 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_direct_serialize_public.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready first_seed = Session(ready, 51) + Session at ready second_seed = Session(ready, 52) + result[Session at ready, Session at ready] first = ok(first_seed) + result[Session at ready, Session at ready] second = fail(second_seed) + string first_encoded = json.serialize_public[result[Session at ready, Session at ready]](view first) + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = json.serialize_public[result[Session at ready, Session at ready]](view second) + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse.jett new file mode 100644 index 00000000..3c70a745 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"ok\":{{\"state\":\"ready\",\"payload\":{{\"count\":51}}}}}}" + result[Session at ready, Session at ready] first = first_raw into json.parse[result[Session at ready, Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{\"fail\":{{\"state\":\"ready\",\"payload\":{{\"count\":52}}}}}}" + result[Session at ready, Session at ready] second = second_raw into json.parse[result[Session at ready, Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse_exact.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse_exact.jett new file mode 100644 index 00000000..c9cc6110 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_parse_exact.jett @@ -0,0 +1,19 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + string first_raw = "{{\"ok\":{{\"state\":\"ready\",\"payload\":{{\"count\":51}}}}}}" + result[Session at ready, Session at ready] first = first_raw into json.parse_exact[result[Session at ready, Session at ready]]() handle error: + return Stdout.write(view stdout, "first error:{error}\n") + string second_raw = "{{\"fail\":{{\"state\":\"ready\",\"payload\":{{\"count\":52}}}}}}" + result[Session at ready, Session at ready] second = second_raw into json.parse_exact[result[Session at ready, Session at ready]]() handle error: + return Stdout.write(view stdout, "second error:{error}\n") + Stdout.write(view stdout, "parsed:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize.jett new file mode 100644 index 00000000..b5a026d8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready first_seed = Session(ready, 51) + Session at ready second_seed = Session(ready, 52) + result[Session at ready, Session at ready] first = ok(first_seed) + result[Session at ready, Session at ready] second = fail(second_seed) + string first_encoded = first into view json.serialize[result[Session at ready, Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize[result[Session at ready, Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize_public.jett b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize_public.jett new file mode 100644 index 00000000..8a7b800e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/json_qualified_machines/wrappers/result_pipeline_serialize_public.jett @@ -0,0 +1,21 @@ +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +function result_count(view value: result[Session at ready, Session at ready]) returns string: + Session at ready item = clone value handle error: + return "fail:{error.count}" + return "ok:{item.count}" +function main(stdout: Stdout) returns nothing: + Session at ready first_seed = Session(ready, 51) + Session at ready second_seed = Session(ready, 52) + result[Session at ready, Session at ready] first = ok(first_seed) + result[Session at ready, Session at ready] second = fail(second_seed) + string first_encoded = first into view json.serialize_public[result[Session at ready, Session at ready]]() + Stdout.write(view stdout, "{first_encoded}\n") + string second_encoded = second into view json.serialize_public[result[Session at ready, Session at ready]]() + Stdout.write(view stdout, "{second_encoded}\n") + Stdout.write(view stdout, "retained:{result_count(view first)}:{result_count(view second)}\n") diff --git a/crates/jett_driver/tests/native_conformance/latent_callbacks.rs b/crates/jett_driver/tests/native_conformance/latent_callbacks.rs new file mode 100644 index 00000000..0b52571c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/latent_callbacks.rs @@ -0,0 +1,82 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable_with_options; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +fn run_case(name: &str, source_text: &str, expected_stdout: &str) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let reference = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(reference.stdout, expected_stdout, "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(0), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + expected_stdout.as_bytes(), + "{name}, release={release}" + ); + assert!( + actual.stderr.is_empty(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_latent_callbacks_nested_zero_argument_callback_matches_reference_without_source_in_both_profiles() + { + run_case( + "01_nested_zero_argument_callback", + include_str!("latent_callbacks/01_nested_zero_argument_callback.jett"), + "nested:0:1:7\n", + ); +} + +#[test] +fn native_latent_callbacks_secret_result_descriptor_matches_reference_without_source_in_both_profiles() + { + run_case( + "02_secret_result_callback", + include_str!("latent_callbacks/02_secret_result_callback.jett"), + "secret:1:7:1\n", + ); +} + +#[test] +fn native_latent_callbacks_concrete_control_matches_reference_without_source_in_both_profiles() { + run_case( + "03_concrete_control", + include_str!("latent_callbacks/03_concrete_control.jett"), + "control:1:7:9\n", + ); +} diff --git a/crates/jett_driver/tests/native_conformance/latent_callbacks/01_nested_zero_argument_callback.jett b/crates/jett_driver/tests/native_conformance/latent_callbacks/01_nested_zero_argument_callback.jett new file mode 100644 index 00000000..c5501b33 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/latent_callbacks/01_nested_zero_argument_callback.jett @@ -0,0 +1,12 @@ +namespace app +function nested_map[T](values: list[T]) returns list[T]: + function(T) returns T callback = function(value: T) returns T: + function() returns T inner = function() returns T: return value + return inner() + return list.map[T, T](values, callback) +function main(stdout: Stdout) returns nothing: + int64 absent = list.length(view nested_map(list())) + list[int64] mapped = nested_map(list(7)) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + Stdout.write(view stdout, "nested:{absent}:{list.length[int64](view mapped)}:{first}\n") diff --git a/crates/jett_driver/tests/native_conformance/latent_callbacks/02_secret_result_callback.jett b/crates/jett_driver/tests/native_conformance/latent_callbacks/02_secret_result_callback.jett new file mode 100644 index 00000000..171a6c21 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/latent_callbacks/02_secret_result_callback.jett @@ -0,0 +1,14 @@ +namespace app +function make_secret[T](view values: list[T]) returns function(T) returns secret[T]: + return function(value: T) returns secret[T]: return value +function secret_descriptor_count[T](values: list[T]) returns int64: + function(T) returns secret[T] callback = make_secret[T](view values) + list[function(T) returns secret[T]] stored = list(clone callback) + return list.length[function(T) returns secret[T]](view stored) +function main(stdout: Stdout) returns nothing: + int64 stored = secret_descriptor_count(list()) + list[int64] values = list(7) + function(int64) returns secret[int64] callback = make_secret[int64](view values) + secret[int64] hidden = callback(7) + int64 shown = declassify hidden + Stdout.write(view stdout, "secret:{stored}:{shown}:{list.length[int64](view values)}\n") diff --git a/crates/jett_driver/tests/native_conformance/latent_callbacks/03_concrete_control.jett b/crates/jett_driver/tests/native_conformance/latent_callbacks/03_concrete_control.jett new file mode 100644 index 00000000..b0b4b63c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/latent_callbacks/03_concrete_control.jett @@ -0,0 +1,20 @@ +namespace app +function nested_map[T](values: list[T]) returns list[T]: + function(T) returns T callback = function(value: T) returns T: + function() returns T inner = function() returns T: return value + return inner() + return list.map[T, T](values, callback) +function make_secret[T](view values: list[T]) returns function(T) returns secret[T]: + return function(value: T) returns secret[T]: return value +function secret_descriptor_count[T](values: list[T]) returns int64: + function(T) returns secret[T] callback = make_secret[T](view values) + list[function(T) returns secret[T]] stored = list(clone callback) + return list.length[function(T) returns secret[T]](view stored) +function main(stdout: Stdout) returns nothing: + list[int64] mapped = nested_map(list(7)) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + function(int64) returns secret[int64] callback = make_secret[int64](view mapped) + secret[int64] hidden = callback(9) + int64 shown = declassify hidden + Stdout.write(view stdout, "control:{list.length[int64](view mapped)}:{first}:{shown}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases.rs b/crates/jett_driver/tests/native_conformance/local_view_aliases.rs new file mode 100644 index 00000000..5b482940 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases.rs @@ -0,0 +1,689 @@ +use super::*; + +use super::suite_options::launcher_for_options; + +#[derive(Clone, Copy)] +pub(super) enum ProjectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +pub(super) fn run_case(name: &str, source_text: &str, expected: ProjectedOutcome) { + run_case_with_traces(name, source_text, expected, &[]); +} + +fn run_case_with_traces( + name: &str, + source_text: &str, + expected: ProjectedOutcome, + expected_traces: &[&str], +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, terminal_stderr) = match expected { + ProjectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + ProjectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(&source) + .expect_err("expected terminal projected view fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + let expected_events = expected_traces + .iter() + .map(|text| jett_driver::DebugEvent { + kind: jett_driver::DebugEventKind::Trace, + text: (*text).to_owned(), + }) + .collect::>(); + assert_eq!( + reference.debug_events, expected_events, + "{name}: {reference:?}" + ); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = jett_driver::native::build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&reference.debug_events) + }; + let stderr = format!("{debug}{terminal_stderr}"); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_stable_projected_local_view_exact_packet_matches_reference_without_source() { + run_case( + "01_exact_packet", + include_str!("local_view_aliases/01_exact_packet.jett"), + ProjectedOutcome::Success(""), + ); +} + +#[test] +fn native_stable_projected_local_view_owned_bytes_and_forwarding_preserve_original_owner() { + run_case( + "02_owned_bytes_forwarded", + include_str!("local_view_aliases/02_owned_bytes_forwarded.jett"), + ProjectedOutcome::Success("4142:4142:414243:4142\n"), + ); +} + +#[test] +fn native_stable_projected_local_view_parameter_clone_preserves_callers_owner() { + run_case( + "03_view_parameter", + include_str!("local_view_aliases/03_view_parameter.jett"), + ProjectedOutcome::Success("4142:4142\n"), + ); +} + +#[test] +fn native_stable_projected_local_view_nested_field_path_preserves_owner() { + run_case( + "04_nested_local", + include_str!("local_view_aliases/04_nested_local.jett"), + ProjectedOutcome::Success("4344:4344:4344\n"), + ); +} + +#[test] +fn native_stable_projected_local_view_failure_preserves_argument_order_and_cleanup() { + run_case( + "05_projected_list_failure", + include_str!("local_view_aliases/05_projected_list_failure.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: "before\nborrowed:2\nfailure\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_stable_projected_local_view_direct_alias_control_remains_supported() { + run_case( + "06_direct_alias_control", + include_str!("local_view_aliases/06_direct_alias_control.jett"), + ProjectedOutcome::Success("4142:4142\n"), + ); +} + +#[test] +fn native_stable_projected_local_view_owned_field_copy_remains_independent() { + run_case( + "07_owned_field_copy_control", + include_str!("local_view_aliases/07_owned_field_copy_control.jett"), + ProjectedOutcome::Success("414243:4142\n"), + ); +} + +#[test] +fn native_stable_projected_local_view_struct_generic_and_qualified_endpoints_keep_checked_types() { + run_case( + "10_typed_endpoints", + include_str!("local_view_aliases/10_typed_endpoints.jett"), + ProjectedOutcome::Success( + "nested:4142:4142:4142\ngeneric:7:7:4344:434445:4344\nqualified:2:2:3:2:2:3\n", + ), + ); +} + +#[test] +fn native_temporary_projected_local_view_retains_the_whole_function_scope_owner() { + run_case( + "09_temporary_root", + include_str!("local_view_aliases/09_temporary_root_boundary.jett"), + ProjectedOutcome::Success(""), + ); +} + +#[test] +fn native_temporary_projected_local_view_produces_once_and_preserves_forwarded_reads() { + run_case( + "13_temporary_function_body", + include_str!("local_view_aliases/13_temporary_function_body.jett"), + ProjectedOutcome::Success(concat!( + "producer\n", + "between\n", + "borrowed:4142:4142\n", + "clone:414243:4142\n", + "after-last-read\n", + )), + ); +} + +#[test] +fn native_local_view_aliases_match_interpreter_in_both_profiles() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/local_view_aliases.jett"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::copy(&fixture, &source).unwrap(); + let expected = + jett_driver::run_file_capture_output(&source).expect("local view alias reference oracle"); + assert_eq!( + expected.stdout, + concat!( + "lists:10:17:3:10:10:3\n", + "bytes:4142:4142:2\n", + "strings:secret:secret:secret:refined:refined:refined\n", + "records:record:18:18:18\n", + "interfaces:named:named:named\n", + "functions:10:11:12:11:12\n", + "generics:7:7:5:5\n", + "sums:5:5:5\n", + "handlers:3:4:secret:3:secret:4:refined:5\n", + "collections:10:11:secret:owned:sum\n", + "scopes:27:5\n", + "reflection:values:app.Record;label:app.Record;reflection:7\n", + "pending:18:18\n", + "debug-root:10:10\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace source: list[int64] = list(2, 3, 5)", + "trace borrowed: list[int64] = list(2, 3, 5)", + "trace forwarded: list[int64] = list(2, 3, 5)", + "trace hidden_alias: secret[string] = [redacted]", + ] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("local_aliases_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native immutable local view aliases"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("local_aliases_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled local view alias verify suite"); + let property_binary = directory.path().join("local_aliases_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled local view alias property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +#[test] +fn native_local_view_alias_failure_preserves_order_and_cleanup() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + r#"namespace app +function first(view stdout: Stdout, view source: list[string]) returns int64: + list[string] borrowed = view source + list[string] forwarded = borrowed + Stdout.write(view stdout, "borrowed:{list.length(view forwarded)}\n") + return list.length(view source) +function failing(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "failure\n") + return list.remove_at[string](list("owned", "other"), -1) +function consume(amount: int64, values: list[string]) returns nothing: + return nothing +function main(stdout: Stdout) returns nothing: + list[string] source = list("keep", "source") + bytes payload = bytes.from_string("cleanup") + list[string] borrowed = view source + list[string] forwarded = borrowed + Stdout.write(view stdout, "before\n") + consume(first(view stdout, view forwarded), failing(view stdout)) + Stdout.write(view stdout, "must not run") +"#, + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("later owned argument fails after alias observation"); + assert_eq!(expected.output.stdout, "before\nborrowed:2\nfailure\n"); + assert!(expected.output.debug_events.is_empty()); + assert_eq!( + expected.message, + "runtime error: list.__remove_at: index -1 out of bounds" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("alias_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native alias borrow followed by terminal owned argument failure"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!("{}\n", expected.message).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_nonstring_qualified_local_aliases_preserve_owners_and_cleanup_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + r#"namespace app +type Numbers = list[int64] where true +function peek(view values: list[int64], index: int64) returns int64: + return list.get[int64](view values, index) handle: default -1 +function refined_copy(view source: Numbers) returns list[int64]: + list[int64] borrowed = coarsen view source + list[int64] forwarded = borrowed + return clone forwarded +function revealed_copy(view source: secret[list[int64]]) returns list[int64]: + list[int64] borrowed = declassify view source + list[int64] forwarded = borrowed + return clone forwarded +function main(stdout: Stdout) returns nothing: + list[string] earlier = list("owned", "cleanup") + Numbers refined = list(2, 3) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + mutable list[int64] refined_clone = refined_copy(view refined) + refined_clone = list.append[int64](refined_clone, 13) + list[int64] coarse = coarsen view refined + Stdout.write(view stdout, "coarsen:{list.length(view coarse)}:{list.length(view refined_clone)}:{peek(view coarse, 0)}:{peek(view coarse, 1)}:{peek(view refined_clone, 2)}\n") + list[int64] public_values = list(5, 7) + secret[list[int64]] hidden = public_values + mutable list[int64] hidden_clone = revealed_copy(view hidden) + hidden_clone = list.append[int64](hidden_clone, 17) + list[int64] revealed = declassify view hidden + Stdout.write(view stdout, "declassify:{list.length(view revealed)}:{list.length(view hidden_clone)}:{peek(view revealed, 0)}:{peek(view revealed, 1)}:{peek(view hidden_clone, 2)}\n") + string title = list.get[string](view earlier, 0) handle: default "missing" + Stdout.write(view stdout, "earlier:{title}:{list.length(view earlier)}\n") + list[string] unreachable = list.remove_at[string](earlier, -1) + Stdout.write(view stdout, "must not run") +"#, + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("terminal list failure follows qualified aliases and independent clones"); + assert_eq!( + expected.output.stdout, + "coarsen:2:3:2:3:13\ndeclassify:2:3:5:7:17\nearlier:owned:2\n" + ); + assert!(expected.output.debug_events.is_empty()); + assert_eq!( + expected.message, + "runtime error: list.__remove_at: index -1 out of bounds" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("qualified_alias_cleanup_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native nonstring coarsened and declassified local view aliases"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + format!("{}\n", expected.message).as_bytes(), + "{actual:?}" + ); + } +} + +#[test] +fn native_machine_projected_local_view_exact_packet_matches_reference_without_source() { + run_case( + "machine_01_exact_packet", + include_str!("local_view_aliases/machines/01_exact_packet.jett"), + ProjectedOutcome::Success(""), + ); +} + +#[test] +fn native_machine_projected_local_view_payloads_and_forwarding_preserve_original_owner() { + run_case( + "machine_02_owned_payloads_forwarded", + include_str!("local_view_aliases/machines/02_owned_payloads_forwarded.jett"), + ProjectedOutcome::Success("bytes:4142:4142:414243:4142\nitems:2:2:3:2\n"), + ); +} + +#[test] +fn native_machine_projected_local_view_parameter_clones_preserve_callers_owner() { + run_case( + "machine_03_view_parameter", + include_str!("local_view_aliases/machines/03_view_parameter.jett"), + ProjectedOutcome::Success("bytes:414243:4142\nitems:3:2\n"), + ); +} + +#[test] +fn native_machine_projected_local_view_struct_to_machine_path_preserves_owner() { + run_case( + "machine_04_struct_to_machine", + include_str!("local_view_aliases/machines/04_struct_to_machine.jett"), + ProjectedOutcome::Success("bytes:4344:4344:4344\nitems:2:2:2\n"), + ); +} + +#[test] +fn native_machine_projected_local_view_machine_to_struct_path_preserves_owner() { + run_case( + "machine_05_machine_to_struct", + include_str!("local_view_aliases/machines/05_machine_to_struct.jett"), + ProjectedOutcome::Success("bytes:4546:4546:4546\nitems:2:2:2\n"), + ); +} + +#[test] +fn native_machine_projected_local_view_failure_preserves_argument_order_and_cleanup() { + run_case( + "machine_06_later_argument_failure", + include_str!("local_view_aliases/machines/06_later_argument_failure.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: "before\nborrowed:2\nfailure\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_machine_projected_local_view_owned_field_copy_remains_independent() { + run_case( + "machine_07_owned_field_copy_control", + include_str!("local_view_aliases/machines/07_owned_field_copy_control.jett"), + ProjectedOutcome::Success("414243:4142\n"), + ); +} + +#[test] +fn native_machine_projected_local_view_owner_changes_reject_before_publication() { + use jett_driver::native::{NativeBuildError, build_host_executable_with_options}; + use jett_driver::{BuildOptions, build_file_with_options}; + + let cases = [ + ( + "machine_owner_consumption", + include_str!("local_view_aliases/machines/08_owner_consumption_boundary.jett"), + ), + ( + "machine_owner_transition", + include_str!("local_view_aliases/machines/09_owner_transition_boundary.jett"), + ), + ]; + for (name, source_text) in cases { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: admitted reference boundary: {error:?}")); + assert_eq!(reference.stdout, "4142\n", "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing native publication"; + fs::write(&output, sentinel).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + assert!(!launcher.archive_path.exists()); + + for release in [false, true] { + let options = BuildOptions { release }; + let checked = build_file_with_options(&source, options); + assert!( + !checked.has_errors, + "{name}: the source boundary stays frontend-admitted: {:?}", + checked.diagnostics + ); + let error = build_host_executable_with_options(&source, &launcher, &output, options) + .expect_err( + "persistent alias owner must fail before archive lookup and publication", + ); + assert!(error.debug_observations().is_empty(), "{name}: {error:?}"); + let NativeBuildError::Codegen { source, .. } = error else { + panic!("{name}: expected persistent alias ownership failure: {error:?}"); + }; + let jett_codegen_cranelift::CodegenError::InvalidMirContract { message, .. } = source + else { + panic!("{name}: expected persistent alias ownership contract: {source:?}"); + }; + let owner = message + .strip_prefix("native owner ") + .and_then(|rest| { + rest.strip_suffix( + " consumption or rebinding after creating a local view alias is not implemented", + ) + }) + .unwrap_or_else(|| panic!("{name}: unexpected ownership contract: {message}")); + owner + .parse::() + .unwrap_or_else(|error| panic!("{name}: invalid owner identity {owner}: {error}")); + assert_eq!(fs::read(&output).unwrap(), sentinel, "{name}"); + assert!(!launcher.archive_path.exists()); + } + } +} + +#[test] +fn native_machine_projected_local_view_pending_whole_owner_fails_before_field_observation() { + run_case_with_traces( + "machine_10_pending_whole_machine", + include_str!("local_view_aliases/machines/10_pending_whole_machine.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: "before:whole\n", + message: "runtime error: field access is not supported on pending(pending(app.Packet@ready(bytes(65, 66), list(2, 3))))", + }, + &[], + ); +} + +#[test] +fn native_machine_projected_local_view_pending_struct_to_machine_owner_preserves_depth() { + run_case_with_traces( + "machine_11_pending_struct_to_machine", + include_str!("local_view_aliases/machines/11_pending_struct_to_machine.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: "before:struct-machine\nintermediate\n", + message: "runtime error: field access is not supported on pending(pending(app.Packet@ready(bytes(67, 68), list(4, 5))))", + }, + &[ + "trace forwarded_machine: app.Packet at ready = pending(pending(app.Packet@ready(bytes(67, 68), list(4, 5))))\n", + ], + ); +} + +#[test] +fn native_machine_projected_local_view_pending_machine_to_struct_owner_preserves_depth() { + run_case_with_traces( + "machine_12_pending_machine_to_struct", + include_str!("local_view_aliases/machines/12_pending_machine_to_struct.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: "before:machine-struct\nintermediate\n", + message: "runtime error: field access is not supported on pending(pending(app.Payload(data: bytes(69, 70), items: list(6, 7))))", + }, + &[ + "trace forwarded_struct: app.Payload = pending(pending(app.Payload(data: bytes(69, 70), items: list(6, 7))))\n", + ], + ); +} + +#[test] +fn native_machine_projected_local_view_pending_endpoint_clones_join_without_changing_owner() { + run_case_with_traces( + "machine_13_pending_endpoint_copies", + include_str!("local_view_aliases/machines/13_pending_endpoint_copies.jett"), + ProjectedOutcome::Success("bytes:414243:4142\nitems:3:2\n"), + &[ + "trace forwarded: bytes = pending(pending(bytes(65, 66)))\n", + "trace once: bytes = pending(bytes(65, 66))\n", + "trace forwarded: bytes = pending(pending(bytes(65, 66)))\n", + "trace once: bytes = pending(bytes(65, 66))\n", + "trace forwarded: list[int64] = pending(pending(list(2, 3)))\n", + "trace once: list[int64] = pending(list(2, 3))\n", + "trace forwarded: list[int64] = pending(pending(list(2, 3)))\n", + "trace once: list[int64] = pending(list(2, 3))\n", + "trace packet: app.Packet at ready = app.Packet@ready(pending(pending(bytes(65, 66))), pending(pending(list(2, 3))))\n", + ], + ); +} + +#[test] +fn native_machine_projected_local_view_pending_whole_owner_clones_join_before_projection() { + run_case_with_traces( + "machine_14_pending_whole_owner_copies", + include_str!("local_view_aliases/machines/14_pending_whole_owner_copies.jett"), + ProjectedOutcome::Success("bytes:4142:414243:4142\nitems:2:3:2\n"), + &[ + "trace forwarded: app.Packet at ready = pending(pending(app.Packet@ready(bytes(65, 66), list(2, 3))))\n", + "trace once: app.Packet at ready = pending(app.Packet@ready(bytes(65, 66), list(2, 3)))\n", + "trace forwarded: app.Packet at ready = pending(pending(app.Packet@ready(bytes(65, 66), list(2, 3))))\n", + "trace once: app.Packet at ready = pending(app.Packet@ready(bytes(65, 66), list(2, 3)))\n", + "trace packet: app.Packet at ready = pending(pending(app.Packet@ready(bytes(65, 66), list(2, 3))))\n", + ], + ); +} + +#[test] +fn native_machine_projected_local_view_generic_and_qualified_endpoints_keep_exact_types() { + run_case( + "machine_15_typed_endpoints", + include_str!("local_view_aliases/machines/15_typed_endpoints.jett"), + ProjectedOutcome::Success(concat!( + "scalar:7:7:2:3:2:5:11:2:3:2:3:13\n", + "bytes:414243:4142:2:3:2:5:11:2:3:2:3:13:2:3:2:5:11:2:3:2:3:13\n", + "items:3:2:2:3:2:5:11:2:3:2:3:13\n", + )), + ); +} + +#[test] +fn native_mutable_owned_backing_keeps_original_unused_alias_fixture_executable() { + run_case( + "08_mutable_root_boundary", + include_str!("local_view_aliases/08_mutable_root_boundary.jett"), + ProjectedOutcome::Success(""), + ); +} + +#[test] +fn native_mutable_owned_backing_preserves_projected_forwarding_and_independent_copies() { + run_case( + "11_mutable_owned_backing", + include_str!("local_view_aliases/11_mutable_owned_backing.jett"), + ProjectedOutcome::Success("bytes:4142:4142:4142:414243:414244:4142\nitems:2:2:3:2\n"), + ); +} + +#[test] +fn native_reflected_mutable_owned_backing_preserves_selected_outer_alias_dependencies() { + run_case( + "12_reflected_mutable_owned_backing", + include_str!("local_view_aliases/12_reflected_mutable_owned_backing.jett"), + ProjectedOutcome::Success("direct:4142\nint64:4344:4344\nbool:4344:4344\n"), + ); +} + +#[test] +fn native_temporary_local_list_views_match_checked_reference_without_source() { + run_case( + "14_temporary_list_function_body", + include_str!("local_view_aliases/14_temporary_list_function_body.jett"), + ProjectedOutcome::Success(concat!( + "make:numbers\nnumbers:3:3:10:false\nfirst:2\nnegative:none\noutside:none\n", + "clone:17:10\nresult:10\nmake:names\nnames:2\nname:Ada\n", + )), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/01_exact_packet.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/01_exact_packet.jett new file mode 100644 index 00000000..7b60d551 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/01_exact_packet.jett @@ -0,0 +1,6 @@ +namespace app +struct Packet: + data: bytes +function main() returns nothing: + Packet item = Packet(data: bytes.new()) + bytes borrowed = view item.data diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/02_owned_bytes_forwarded.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/02_owned_bytes_forwarded.jett new file mode 100644 index 00000000..79754f31 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/02_owned_bytes_forwarded.jett @@ -0,0 +1,9 @@ +namespace app +struct Packet: + data: bytes +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(data: bytes.from_string("AB")) + bytes borrowed = view packet.data + bytes forwarded = borrowed + bytes grown = bytes.concat(clone forwarded, bytes.from_string("C")) + Stdout.write(view stdout, "{bytes.to_hex(view borrowed)}:{bytes.to_hex(view forwarded)}:{bytes.to_hex(view grown)}:{bytes.to_hex(view packet.data)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/03_view_parameter.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/03_view_parameter.jett new file mode 100644 index 00000000..af3abca6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/03_view_parameter.jett @@ -0,0 +1,11 @@ +namespace app +struct Packet: + data: bytes +function duplicate_data(view packet: Packet) returns bytes: + bytes borrowed = view packet.data + bytes forwarded = borrowed + return clone forwarded +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(data: bytes.from_string("AB")) + bytes duplicate = duplicate_data(view packet) + Stdout.write(view stdout, "{bytes.to_hex(view duplicate)}:{bytes.to_hex(view packet.data)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/04_nested_local.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/04_nested_local.jett new file mode 100644 index 00000000..bd95a050 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/04_nested_local.jett @@ -0,0 +1,10 @@ +namespace app +struct Packet: + data: bytes +struct Envelope: + packet: Packet +function main(stdout: Stdout) returns nothing: + Envelope envelope = Envelope(packet: Packet(data: bytes.from_string("CD"))) + bytes borrowed = view envelope.packet.data + bytes copied = clone borrowed + Stdout.write(view stdout, "{bytes.to_hex(view borrowed)}:{bytes.to_hex(view copied)}:{bytes.to_hex(view envelope.packet.data)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/05_projected_list_failure.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/05_projected_list_failure.jett new file mode 100644 index 00000000..c905c4ed --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/05_projected_list_failure.jett @@ -0,0 +1,19 @@ +namespace app +struct Packet: + items: list[string] +function first(view stdout: Stdout, view source: list[string]) returns int64: + Stdout.write(view stdout, "borrowed:{list.length(view source)}\n") + return list.length(view source) +function failing(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "failure\n") + return list.remove_at[string](list("owned", "other"), -1) +function consume(amount: int64, values: list[string]) returns nothing: + return nothing +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(items: list("keep", "source")) + bytes payload = bytes.from_string("cleanup") + list[string] borrowed = view packet.items + list[string] forwarded = borrowed + Stdout.write(view stdout, "before\n") + consume(first(view stdout, view forwarded), failing(view stdout)) + Stdout.write(view stdout, "must not run") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/06_direct_alias_control.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/06_direct_alias_control.jett new file mode 100644 index 00000000..fe6dc2d4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/06_direct_alias_control.jett @@ -0,0 +1,6 @@ +namespace app +function main(stdout: Stdout) returns nothing: + bytes source = bytes.from_string("AB") + bytes borrowed = view source + bytes forwarded = borrowed + Stdout.write(view stdout, "{bytes.to_hex(view forwarded)}:{bytes.to_hex(view source)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/07_owned_field_copy_control.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/07_owned_field_copy_control.jett new file mode 100644 index 00000000..1efda94e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/07_owned_field_copy_control.jett @@ -0,0 +1,8 @@ +namespace app +struct Packet: + data: bytes +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(data: bytes.from_string("AB")) + bytes copied = packet.data + bytes grown = bytes.concat(copied, bytes.from_string("C")) + Stdout.write(view stdout, "{bytes.to_hex(view grown)}:{bytes.to_hex(view packet.data)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/08_mutable_root_boundary.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/08_mutable_root_boundary.jett new file mode 100644 index 00000000..7c506528 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/08_mutable_root_boundary.jett @@ -0,0 +1,6 @@ +namespace app +struct Packet: + data: bytes +function main() returns nothing: + mutable Packet packet = Packet(data: bytes.from_string("AB")) + bytes borrowed = view packet.data diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/09_temporary_root_boundary.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/09_temporary_root_boundary.jett new file mode 100644 index 00000000..e0680e80 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/09_temporary_root_boundary.jett @@ -0,0 +1,7 @@ +namespace app +struct Packet: + data: bytes +function make_packet() returns Packet: + return Packet(data: bytes.from_string("AB")) +function main() returns nothing: + bytes borrowed = view make_packet().data diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/10_typed_endpoints.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/10_typed_endpoints.jett new file mode 100644 index 00000000..717a602c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/10_typed_endpoints.jett @@ -0,0 +1,44 @@ +namespace app +struct Packet: + data: bytes +struct Envelope: + packet: Packet +struct Carrier[T]: + value: T +type Numbers = list[int64] where true +struct Qualified: + hidden: secret[list[int64]] + refined: Numbers +function generic_copy[T](view source: Carrier[T]) returns T: + T borrowed = view source.value + T forwarded = borrowed + return clone forwarded +function qualified_report(view source: Qualified) returns string: + secret[list[int64]] hidden_alias = view source.hidden + list[int64] revealed = declassify view hidden_alias + Numbers refined_alias = view source.refined + list[int64] coarse = coarsen view refined_alias + mutable list[int64] grown = clone coarse + grown = list.append[int64](grown, 11) + return "{list.length(view revealed)}:{list.length(view coarse)}:{list.length(view grown)}" +function main(stdout: Stdout) returns nothing: + Envelope envelope = Envelope(packet: Packet(data: bytes.from_string("AB"))) + Packet projected = view envelope.packet + Packet forwarded = projected + bytes leaf = view forwarded.data + bytes leaf_copy = clone leaf + Stdout.write(view stdout, "nested:{bytes.to_hex(view leaf)}:{bytes.to_hex(view leaf_copy)}:{bytes.to_hex(view envelope.packet.data)}\n") + Carrier[int64] number = Carrier[int64](value: 7) + int64 scalar = generic_copy[int64](view number) + Carrier[bytes] payload = Carrier[bytes](value: bytes.from_string("CD")) + bytes copied = generic_copy[bytes](view payload) + bytes grown = bytes.concat(copied, bytes.from_string("E")) + Stdout.write(view stdout, "generic:{scalar}:{number.value}:{bytes.to_hex(view payload.value)}:{bytes.to_hex(view grown)}:{bytes.to_hex(view payload.value)}\n") + secret[list[int64]] hidden = list(2, 3) + Numbers refined = list(5, 7) handle error: + return Stdout.write(view stdout, "refinement error:{error}\n") + Qualified qualified = Qualified(hidden: hidden, refined: refined) handle error: + return Stdout.write(view stdout, "qualified error:{error}\n") + string first = qualified_report(view qualified) + string second = qualified_report(view qualified) + Stdout.write(view stdout, "qualified:{first}:{second}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/11_mutable_owned_backing.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/11_mutable_owned_backing.jett new file mode 100644 index 00000000..de3b7a29 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/11_mutable_owned_backing.jett @@ -0,0 +1,22 @@ +namespace app +struct Packet: + data: bytes + items: list[string] +struct Envelope: + packet: Packet +function inspect_bytes(view source: bytes) returns string: + bytes borrowed = view source + bytes forwarded = borrowed + return bytes.to_hex(view forwarded) +function main(stdout: Stdout) returns nothing: + mutable Envelope envelope = Envelope(packet: Packet(data: bytes.from_string("AB"), items: list("keep", "source"))) + bytes borrowed = view envelope.packet.data + bytes forwarded = borrowed + bytes grown = bytes.concat(clone forwarded, bytes.from_string("C")) + bytes copied = envelope.packet.data + bytes expanded = bytes.concat(copied, bytes.from_string("D")) + list[string] item_alias = view envelope.packet.items + list[string] forwarded_items = item_alias + list[string] grown_items = list.append[string](clone forwarded_items, "tail") + Stdout.write(view stdout, "bytes:{bytes.to_hex(view borrowed)}:{bytes.to_hex(view forwarded)}:{inspect_bytes(view forwarded)}:{bytes.to_hex(view grown)}:{bytes.to_hex(view expanded)}:{bytes.to_hex(view envelope.packet.data)}\n") + Stdout.write(view stdout, "items:{list.length(view item_alias)}:{list.length(view forwarded_items)}:{list.length(view grown_items)}:{list.length(view envelope.packet.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/12_reflected_mutable_owned_backing.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/12_reflected_mutable_owned_backing.jett new file mode 100644 index 00000000..b2b38053 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/12_reflected_mutable_owned_backing.jett @@ -0,0 +1,22 @@ +namespace app +struct Packet: + data: bytes +struct Metadata: + count: int64 + enabled: bool +function observe[T](view metadata: T, mutable packet: Packet, view stdout: Stdout) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + bytes alias = view packet.data + comptime type Inner = type.info[Field](): + bytes forwarded = alias + bytes copied = clone forwarded + Stdout.write(view stdout, "{type.name[Inner]()}:{bytes.to_hex(view forwarded)}:{bytes.to_hex(view copied)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + mutable Packet direct = Packet(data: bytes.from_string("AB")) + comptime type Bound = type.info[int64](): + bytes direct_alias = view direct.data + Stdout.write(view stdout, "direct:{bytes.to_hex(view direct_alias)}\n") + Metadata metadata = Metadata(count: 1, enabled: true) + observe[Metadata](view metadata, Packet(data: bytes.from_string("CD")), view stdout) diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/13_temporary_function_body.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/13_temporary_function_body.jett new file mode 100644 index 00000000..d27b58b6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/13_temporary_function_body.jett @@ -0,0 +1,15 @@ +namespace app +struct Packet: + data: bytes + sibling: bytes +function make_packet(view stdout: Stdout) returns Packet: + Stdout.write(view stdout, "producer\n") + return Packet(data: bytes.from_string("AB"), sibling: bytes.from_string("YZ")) +function main(stdout: Stdout) returns nothing: + bytes borrowed = view make_packet(view stdout).data + Stdout.write(view stdout, "between\n") + bytes forwarded = borrowed + Stdout.write(view stdout, "borrowed:{bytes.to_hex(view borrowed)}:{bytes.to_hex(view forwarded)}\n") + bytes grown = bytes.concat(clone forwarded, bytes.from_string("C")) + Stdout.write(view stdout, "clone:{bytes.to_hex(view grown)}:{bytes.to_hex(view borrowed)}\n") + Stdout.write(view stdout, "after-last-read\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/14_temporary_list_function_body.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/14_temporary_list_function_body.jett new file mode 100644 index 00000000..96c595c9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/14_temporary_list_function_body.jett @@ -0,0 +1,50 @@ +namespace app +struct Packet: + numbers: list[int64] + names: list[string] +function number_text(item: optional[int64]) returns string: + int64 selected = item handle: + return "none" + return string.from_int64(selected) +function name_text(item: optional[string]) returns string: + string selected = item handle: + return "none" + return selected +function make_packet(view stdout: Stdout, label: string) returns Packet: + Stdout.write(view stdout, "make:{label}\n") + return Packet(numbers: list(2, 3, 5), names: list("Ada", "Lin")) +mutual: + function count[T](view values: list[T]) returns int64 +function count[T](view values: list[T]) returns int64: + return list.length[T](view values) +function scenario(view stdout: Stdout) returns int64: + list[int64] numbers = view make_packet(view stdout, "numbers").numbers + list[int64] forwarded = numbers + int64 count_value = count[int64](view forwarded) + int64 inferred = count(view numbers) + int64 total = list.sum[int64](view numbers) + bool empty = list.is_empty[int64](view forwarded) + Stdout.write(view stdout, "numbers:{count_value}:{inferred}:{total}:{empty}\n") + optional[int64] first = list.get[int64](view numbers, 0) + optional[int64] negative = list.get[int64](view numbers, -1) + optional[int64] outside = list.get[int64](view numbers, 3) + Stdout.write(view stdout, "first:{number_text(first)}\n") + Stdout.write(view stdout, "negative:{number_text(negative)}\n") + Stdout.write(view stdout, "outside:{number_text(outside)}\n") + list[int64] copied = clone numbers + list[int64] extended = list.append[int64](copied, 7) + int64 independent = list.sum[int64](view extended) + Stdout.write(view stdout, "clone:{independent}:{list.sum[int64](view numbers)}\n") + return total +function names(view stdout: Stdout) returns nothing: + list[string] names = view make_packet(view stdout, "names").names + int64 count_value = count[string](view names) + optional[string] first = list.get[string](view names, 0) + Stdout.write(view stdout, "names:{count_value}\n") + Stdout.write(view stdout, "name:{name_text(first)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + int64 total = scenario(view stdout) + Stdout.write(view stdout, "result:{total}\n") + names(view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/01_exact_packet.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/01_exact_packet.jett new file mode 100644 index 00000000..cbbb429c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/01_exact_packet.jett @@ -0,0 +1,10 @@ +namespace app +machine Packet: + states: + ready(data: bytes) +function main() returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB")) + bytes borrowed = view packet.data + bytes forwarded = borrowed + bytes independent = clone forwarded + return nothing diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/02_owned_payloads_forwarded.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/02_owned_payloads_forwarded.jett new file mode 100644 index 00000000..8807eee4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/02_owned_payloads_forwarded.jett @@ -0,0 +1,15 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB"), list(2, 3)) + bytes borrowed_data = view packet.data + bytes forwarded_data = borrowed_data + bytes grown = bytes.concat(clone forwarded_data, bytes.from_string("C")) + list[int64] borrowed_items = view packet.items + list[int64] forwarded_items = borrowed_items + mutable list[int64] independent = clone forwarded_items + independent = list.append[int64](independent, 7) + Stdout.write(view stdout, "bytes:{bytes.to_hex(view borrowed_data)}:{bytes.to_hex(view forwarded_data)}:{bytes.to_hex(view grown)}:{bytes.to_hex(view packet.data)}\n") + Stdout.write(view stdout, "items:{list.length(view borrowed_items)}:{list.length(view forwarded_items)}:{list.length(view independent)}:{list.length(view packet.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/03_view_parameter.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/03_view_parameter.jett new file mode 100644 index 00000000..4d3dacc2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/03_view_parameter.jett @@ -0,0 +1,19 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +function duplicate_data(view packet: Packet at ready) returns bytes: + bytes borrowed = view packet.data + bytes forwarded = borrowed + return clone forwarded +function duplicate_items(view packet: Packet at ready) returns list[int64]: + list[int64] borrowed = view packet.items + list[int64] forwarded = borrowed + return clone forwarded +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB"), list(2, 3)) + bytes duplicate = bytes.concat(duplicate_data(view packet), bytes.from_string("C")) + mutable list[int64] items = duplicate_items(view packet) + items = list.append[int64](items, 7) + Stdout.write(view stdout, "bytes:{bytes.to_hex(view duplicate)}:{bytes.to_hex(view packet.data)}\n") + Stdout.write(view stdout, "items:{list.length(view items)}:{list.length(view packet.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/04_struct_to_machine.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/04_struct_to_machine.jett new file mode 100644 index 00000000..3fc28e57 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/04_struct_to_machine.jett @@ -0,0 +1,16 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +struct Envelope: + packet: Packet at ready +function main(stdout: Stdout) returns nothing: + Envelope envelope = Envelope(packet: Packet(ready, bytes.from_string("CD"), list(4, 5))) + Packet at ready borrowed_packet = view envelope.packet + Packet at ready forwarded_packet = borrowed_packet + bytes borrowed_data = view forwarded_packet.data + bytes duplicate = clone borrowed_data + list[int64] borrowed_items = view envelope.packet.items + list[int64] independent = clone borrowed_items + Stdout.write(view stdout, "bytes:{bytes.to_hex(view borrowed_data)}:{bytes.to_hex(view duplicate)}:{bytes.to_hex(view envelope.packet.data)}\n") + Stdout.write(view stdout, "items:{list.length(view borrowed_items)}:{list.length(view independent)}:{list.length(view envelope.packet.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/05_machine_to_struct.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/05_machine_to_struct.jett new file mode 100644 index 00000000..636e8322 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/05_machine_to_struct.jett @@ -0,0 +1,17 @@ +namespace app +struct Payload: + data: bytes + items: list[int64] +machine Envelope: + states: + ready(packet: Payload) +function main(stdout: Stdout) returns nothing: + Envelope at ready envelope = Envelope(ready, Payload(data: bytes.from_string("EF"), items: list(6, 7))) + Payload borrowed_packet = view envelope.packet + Payload forwarded_packet = borrowed_packet + bytes borrowed_data = view forwarded_packet.data + bytes duplicate = clone borrowed_data + list[int64] borrowed_items = view envelope.packet.items + list[int64] independent = clone borrowed_items + Stdout.write(view stdout, "bytes:{bytes.to_hex(view borrowed_data)}:{bytes.to_hex(view duplicate)}:{bytes.to_hex(view envelope.packet.data)}\n") + Stdout.write(view stdout, "items:{list.length(view borrowed_items)}:{list.length(view independent)}:{list.length(view envelope.packet.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/06_later_argument_failure.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/06_later_argument_failure.jett new file mode 100644 index 00000000..f63ebd98 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/06_later_argument_failure.jett @@ -0,0 +1,19 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[string]) +function first(view stdout: Stdout, view source: list[string]) returns int64: + Stdout.write(view stdout, "borrowed:{list.length(view source)}\n") + return list.length(view source) +function failing(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "failure\n") + return list.remove_at[string](list("owned", "other"), -1) +function consume(amount: int64, values: list[string]) returns nothing: + return nothing +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("cleanup"), list("keep", "source")) + list[string] borrowed = view packet.items + list[string] forwarded = borrowed + Stdout.write(view stdout, "before\n") + consume(first(view stdout, view forwarded), failing(view stdout)) + Stdout.write(view stdout, "must not run") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/07_owned_field_copy_control.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/07_owned_field_copy_control.jett new file mode 100644 index 00000000..e314597c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/07_owned_field_copy_control.jett @@ -0,0 +1,9 @@ +namespace app +machine Packet: + states: + ready(data: bytes) +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB")) + bytes copy = packet.data + bytes grown = bytes.concat(copy, bytes.from_string("C")) + Stdout.write(view stdout, "{bytes.to_hex(view grown)}:{bytes.to_hex(view packet.data)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/08_owner_consumption_boundary.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/08_owner_consumption_boundary.jett new file mode 100644 index 00000000..15dedb85 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/08_owner_consumption_boundary.jett @@ -0,0 +1,12 @@ +namespace app +machine Packet: + states: + ready(data: bytes) +function consume(packet: Packet at ready) returns nothing: + return nothing +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB")) + bytes borrowed = view packet.data + bytes forwarded = borrowed + Stdout.write(view stdout, "{bytes.to_hex(view forwarded)}\n") + consume(packet) diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/09_owner_transition_boundary.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/09_owner_transition_boundary.jett new file mode 100644 index 00000000..c1fa35f3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/09_owner_transition_boundary.jett @@ -0,0 +1,14 @@ +namespace app +machine Packet: + states: + ready(data: bytes) + empty + transitions: + ready to empty +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, bytes.from_string("AB")) + bytes borrowed = view packet.data + bytes forwarded = borrowed + Stdout.write(view stdout, "{bytes.to_hex(view forwarded)}\n") + Packet at empty finished = Packet.transition(packet, empty) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/10_pending_whole_machine.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/10_pending_whole_machine.jett new file mode 100644 index 00000000..81fe0389 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/10_pending_whole_machine.jett @@ -0,0 +1,10 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +function main(stdout: Stdout) returns nothing: + Packet at ready packet = run run Packet(ready, bytes.from_string("AB"), list(2, 3)) + Stdout.write(view stdout, "before:whole\n") + bytes borrowed = view packet.data + bytes forwarded = borrowed + Stdout.write(view stdout, "after:{bytes.to_hex(view forwarded)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/11_pending_struct_to_machine.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/11_pending_struct_to_machine.jett new file mode 100644 index 00000000..a2e15b6a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/11_pending_struct_to_machine.jett @@ -0,0 +1,16 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +struct Envelope: + packet: Packet at ready +function main(stdout: Stdout) returns nothing: + Envelope envelope = Envelope(packet: run run Packet(ready, bytes.from_string("CD"), list(4, 5))) + Stdout.write(view stdout, "before:struct-machine\n") + Packet at ready borrowed_machine = view envelope.packet + Packet at ready forwarded_machine = borrowed_machine + trace forwarded_machine + Stdout.write(view stdout, "intermediate\n") + bytes borrowed = view forwarded_machine.data + bytes forwarded = borrowed + Stdout.write(view stdout, "after:{bytes.to_hex(view forwarded)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/12_pending_machine_to_struct.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/12_pending_machine_to_struct.jett new file mode 100644 index 00000000..cfc24264 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/12_pending_machine_to_struct.jett @@ -0,0 +1,17 @@ +namespace app +struct Payload: + data: bytes + items: list[int64] +machine Envelope: + states: + ready(packet: Payload) +function main(stdout: Stdout) returns nothing: + Envelope at ready envelope = Envelope(ready, run run Payload(data: bytes.from_string("EF"), items: list(6, 7))) + Stdout.write(view stdout, "before:machine-struct\n") + Payload borrowed_struct = view envelope.packet + Payload forwarded_struct = borrowed_struct + trace forwarded_struct + Stdout.write(view stdout, "intermediate\n") + list[int64] borrowed = view forwarded_struct.items + list[int64] forwarded = borrowed + Stdout.write(view stdout, "after:{list.length(view forwarded)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/13_pending_endpoint_copies.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/13_pending_endpoint_copies.jett new file mode 100644 index 00000000..93e23ebb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/13_pending_endpoint_copies.jett @@ -0,0 +1,43 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +function ready_data(view packet: Packet at ready) returns result[bytes, string]: + bytes borrowed = view packet.data + bytes forwarded = borrowed + trace forwarded + bytes once = join clone forwarded handle error: + return fail(error) + trace once + bytes ready = join once handle error: + return fail(error) + return ok(ready) +function ready_items(view packet: Packet at ready) returns result[list[int64], string]: + list[int64] borrowed = view packet.items + list[int64] forwarded = borrowed + trace forwarded + list[int64] once = join clone forwarded handle error: + return fail(error) + trace once + list[int64] ready = join once handle error: + return fail(error) + return ok(ready) +function main(stdout: Stdout) returns nothing: + Packet at ready packet = Packet(ready, run run bytes.from_string("AB"), run run list(2, 3)) + bytes ready_data_copy = ready_data(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + bytes data_again = ready_data(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + bytes grown = bytes.concat(ready_data_copy, bytes.from_string("C")) + list[int64] ready_items_copy = ready_items(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + list[int64] items_again = ready_items(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + list[int64] extended = list.append[int64](ready_items_copy, 7) + trace packet + Stdout.write(view stdout, "bytes:{bytes.to_hex(view grown)}:{bytes.to_hex(view data_again)}\n") + Stdout.write(view stdout, "items:{list.length(view extended)}:{list.length(view items_again)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/14_pending_whole_owner_copies.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/14_pending_whole_owner_copies.jett new file mode 100644 index 00000000..529f396b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/14_pending_whole_owner_copies.jett @@ -0,0 +1,31 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) +function ready_copy(view packet: Packet at ready) returns result[Packet at ready, string]: + Packet at ready borrowed = view packet + Packet at ready forwarded = borrowed + trace forwarded + Packet at ready once = join clone forwarded handle error: + return fail(error) + trace once + Packet at ready ready = join once handle error: + return fail(error) + return ok(ready) +function main(stdout: Stdout) returns nothing: + Packet at ready packet = run run Packet(ready, bytes.from_string("AB"), list(2, 3)) + Packet at ready ready = ready_copy(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + bytes borrowed = view ready.data + bytes forwarded = borrowed + bytes grown = bytes.concat(clone forwarded, bytes.from_string("C")) + list[int64] borrowed_items = view ready.items + list[int64] forwarded_items = borrowed_items + list[int64] extended = list.append[int64](clone forwarded_items, 7) + Packet at ready again = ready_copy(view packet) handle error: + Stdout.write(view stdout, "unexpected:{error}\n") + return nothing + trace packet + Stdout.write(view stdout, "bytes:{bytes.to_hex(view borrowed)}:{bytes.to_hex(view grown)}:{bytes.to_hex(view again.data)}\n") + Stdout.write(view stdout, "items:{list.length(view borrowed_items)}:{list.length(view extended)}:{list.length(view again.items)}\n") diff --git a/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/15_typed_endpoints.jett b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/15_typed_endpoints.jett new file mode 100644 index 00000000..f20cbbfa --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/local_view_aliases/machines/15_typed_endpoints.jett @@ -0,0 +1,55 @@ +namespace app +type Numbers = list[int64] where true +type NarrowValues = Numbers where true +machine Packet: + states: + ready(refined: NarrowValues, hidden: secret[list[int64]]) +struct Envelope[T]: + packet: Packet at ready + extra: T +function peek(view values: list[int64], index: int64) returns int64: + return list.get[int64](view values, index) handle: + default -1 +function generic_copy[T](view source: Envelope[T]) returns T: + T borrowed = view source.extra + T forwarded = borrowed + return clone forwarded +function qualified_report[T](view source: Envelope[T]) returns string: + Packet at ready projected = view source.packet + Packet at ready forwarded = projected + NarrowValues refined_alias = view forwarded.refined + Numbers ancestor = coarsen view refined_alias + list[int64] coarse = coarsen view ancestor + mutable list[int64] grown = clone coarse + grown = list.append[int64](grown, 11) + secret[list[int64]] hidden_alias = view forwarded.hidden + list[int64] revealed = declassify view hidden_alias + mutable list[int64] hidden_grown = clone revealed + hidden_grown = list.append[int64](hidden_grown, 13) + NarrowValues original_refined = view source.packet.refined + Numbers original_ancestor = coarsen view original_refined + list[int64] original_coarse = coarsen view original_ancestor + list[int64] original_hidden = declassify view source.packet.hidden + return "{list.length(view coarse)}:{list.length(view grown)}:{list.length(view original_coarse)}:{peek(view original_coarse, 0)}:{peek(view grown, 2)}:{list.length(view revealed)}:{list.length(view hidden_grown)}:{list.length(view original_hidden)}:{peek(view original_hidden, 0)}:{peek(view hidden_grown, 2)}" +function main(stdout: Stdout) returns nothing: + NarrowValues refined = list(5, 7) handle error: + return Stdout.write(view stdout, "refinement error:{error}\n") + secret[list[int64]] hidden = list(3, 4) + Packet at ready packet = Packet(ready, refined, hidden) + Packet at ready byte_packet = clone packet + Packet at ready list_packet = clone packet + Envelope[int64] scalar = Envelope[int64](packet: packet, extra: 7) + Envelope[bytes] payload = Envelope[bytes](packet: byte_packet, extra: bytes.from_string("AB")) + Envelope[list[int64]] values = Envelope[list[int64]](packet: list_packet, extra: list(2, 3)) + int64 scalar_copy = generic_copy[int64](view scalar) + string scalar_report = qualified_report[int64](view scalar) + Stdout.write(view stdout, "scalar:{scalar_copy}:{scalar.extra}:{scalar_report}\n") + bytes payload_copy = generic_copy[bytes](view payload) + bytes payload_grown = bytes.concat(payload_copy, bytes.from_string("C")) + string byte_report = qualified_report[bytes](view payload) + string byte_again = qualified_report[bytes](view payload) + Stdout.write(view stdout, "bytes:{bytes.to_hex(view payload_grown)}:{bytes.to_hex(view payload.extra)}:{byte_report}:{byte_again}\n") + list[int64] values_copy = generic_copy[list[int64]](view values) + list[int64] values_grown = list.append[int64](values_copy, 17) + string list_report = qualified_report[list[int64]](view values) + Stdout.write(view stdout, "items:{list.length(view values_grown)}:{list.length(view values.extra)}:{list_report}\n") diff --git a/crates/jett_driver/tests/native_conformance/measured_launcher.rs b/crates/jett_driver/tests/native_conformance/measured_launcher.rs new file mode 100644 index 00000000..8fad1ab8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/measured_launcher.rs @@ -0,0 +1,117 @@ +//! Reuse explicitly supplied normal bundles in native integration tests. +use jett_driver::native::NativeLauncherBundle; +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::OnceLock; + +const DEBUG_MANIFEST: &str = "JETT_NATIVE_TEST_DEBUG_BUNDLE_MANIFEST"; +const RELEASE_MANIFEST: &str = "JETT_NATIVE_TEST_RELEASE_BUNDLE_MANIFEST"; + +fn selected_manifest( + debug: Option, + release: Option, + use_release: bool, +) -> Result, String> { + match (debug, release) { + (None, None) => Ok(None), + (Some(debug), Some(release)) => { + if debug.is_empty() || release.is_empty() { + return Err("supplied native test bundle manifests must be nonempty".into()); + } + let debug = PathBuf::from(debug); + let release = PathBuf::from(release); + if !debug.is_absolute() || !release.is_absolute() { + return Err("supplied native test bundle manifests must be absolute paths".into()); + } + Ok(Some(if use_release { release } else { debug })) + } + _ => Err(format!( + "{DEBUG_MANIFEST} and {RELEASE_MANIFEST} must be supplied together" + )), + } +} + +pub(super) fn supplied(use_release: bool) -> Option<&'static NativeLauncherBundle> { + static DEBUG: OnceLock<(PathBuf, NativeLauncherBundle)> = OnceLock::new(); + static RELEASE: OnceLock<(PathBuf, NativeLauncherBundle)> = OnceLock::new(); + let manifest = selected_manifest( + std::env::var_os(DEBUG_MANIFEST), + std::env::var_os(RELEASE_MANIFEST), + use_release, + ) + .expect("explicit native test bundle selection")?; + let slot = if use_release { &RELEASE } else { &DEBUG }; + let selected = slot.get_or_init(|| { + let bundle = NativeLauncherBundle::from_manifest(&manifest, use_release) + .expect("explicit native test bundle must pass normal manifest validation"); + (manifest.clone(), bundle) + }); + assert_eq!( + selected.0, manifest, + "explicit native test bundle changed after selection" + ); + Some(&selected.1) +} + +#[test] +fn explicit_manifest_selection_requires_a_complete_nonempty_pair() { + assert_eq!(selected_manifest(None, None, false).unwrap(), None); + assert!(selected_manifest(Some("debug.json".into()), None, false).is_err()); + assert!(selected_manifest(None, Some("release.json".into()), true).is_err()); + assert!(selected_manifest(Some(OsString::new()), Some("release.json".into()), false).is_err()); + assert!(selected_manifest(Some("debug.json".into()), Some(OsString::new()), true).is_err()); + assert!( + selected_manifest( + Some("debug.json".into()), + Some("release.json".into()), + false + ) + .is_err() + ); +} + +#[test] +fn explicit_manifest_selection_uses_the_requested_runtime_profile() { + let directory = tempfile::tempdir().unwrap(); + let debug = directory.path().join("debug.json"); + let release = directory.path().join("release.json"); + for use_release in [false, true] { + assert_eq!( + selected_manifest( + Some(debug.as_os_str().to_owned()), + Some(release.as_os_str().to_owned()), + use_release, + ) + .unwrap(), + Some(if use_release { + release.clone() + } else { + debug.clone() + }), + ); + } +} + +#[test] +fn explicit_manifest_validation_rejects_missing_invalid_and_wrong_profile_bundles() { + let directory = tempfile::tempdir().unwrap(); + let manifest = directory.path().join("runtime.json"); + assert!(NativeLauncherBundle::from_manifest(&manifest, false).is_err()); + std::fs::write(&manifest, b"invalid JSON").unwrap(); + assert!(NativeLauncherBundle::from_manifest(&manifest, false).is_err()); + std::fs::write( + &manifest, + serde_json::to_vec(&serde_json::json!({ + "manifest_version": 1, + "compiler_version": env!("CARGO_PKG_VERSION"), + "profile": "debug", + })) + .unwrap(), + ) + .unwrap(); + assert!( + NativeLauncherBundle::from_manifest(&manifest, true) + .unwrap_err() + .contains("profile mismatch") + ); +} diff --git a/crates/jett_driver/tests/native_conformance/mutable_borrowed_sums.rs b/crates/jett_driver/tests/native_conformance/mutable_borrowed_sums.rs new file mode 100644 index 00000000..3260e332 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/mutable_borrowed_sums.rs @@ -0,0 +1,62 @@ +use super::local_view_aliases::{ProjectedOutcome, run_case}; + +#[test] +fn native_mutable_optional_sum_preserves_named_argument_order_and_terminal_cleanup() { + run_case( + "mutable_sum_optional_failure", + include_str!( + "ordinary_borrowed_sums_mutable/13_mutable_optional_named_handle_failure.jett" + ), + ProjectedOutcome::RuntimeFailure { + stdout: concat!( + "before:optional:8\nprefix:optional:5\nlater:optional\n", + "default:optional\nfailure:optional\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} +#[test] +fn native_mutable_result_sum_preserves_named_argument_order_and_terminal_cleanup() { + run_case( + "mutable_sum_result_failure", + include_str!("ordinary_borrowed_sums_mutable/14_mutable_result_named_handle_failure.jett"), + ProjectedOutcome::RuntimeFailure { + stdout: concat!( + "before:result:8\nprefix:result:12\nlater:result\n", + "default:result\nfailure:result\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_mutable_sum_backing_keeps_original_scoped_forwarded_behaviour() { + run_case( + "mutable_sum_owned", + include_str!("ordinary_borrowed_sums_mutable/03_mutable_owned_scoped_forwarded.jett"), + ProjectedOutcome::Success(concat!( + "optional:nested:5\noptional:5:18:15:5\n", + "result:nested:12\nresult:12:29:36:12\ngeneric:1:1\n" + )), + ); +} +#[test] +fn native_mutable_sum_backing_keeps_original_copy_destinations_and_owning_unwrap() { + run_case( + "mutable_sum_copied", + include_str!("ordinary_borrowed_sums_mutable/15_mutable_backing_copied_payloads.jett"), + ProjectedOutcome::Success("integer:4:3\nstring:changed:kept\n"), + ); +} +#[test] +fn native_copied_payload_survives_mutable_sum_shell_replacement() { + run_case( + "mutable_sum_replaced", + include_str!( + "ordinary_borrowed_sums_mutable/16_copied_payload_survives_shell_replacement.jett" + ), + ProjectedOutcome::Success("integer:3:9\nstring:kept:new\n"), + ); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..c471c507 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs @@ -0,0 +1,262 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, emit_host_program_object_for_file_with_options, +}; +use std::fs; +use std::path::Path; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +struct SourceCase { + name: &'static str, + source: &'static str, + expected: ExpectedOutcome, +} + +const CASES: &[SourceCase] = &[ + SourceCase { + name: "01_optional_branches", + source: include_str!("ordinary_borrowed_sums/01_optional_branches.jett"), + expected: ExpectedOutcome::Success(concat!( + "before:written-some\nsome:written-some:2:5:5\n", + "before:written-none\nnone:written-none:7\n", + "before:bare-some\nsome:bare-some:2:12:12\n", + "before:bare-none\nnone:bare-none:7\n", + )), + }, + SourceCase { + name: "02_result_branches", + source: include_str!("ordinary_borrowed_sums/02_result_branches.jett"), + expected: ExpectedOutcome::Success(concat!( + "before:written-ok\nok:written-ok:2:5:5\n", + "before:written-fail\nfail:written-fail:written error\n", + "before:bare-ok\nok:bare-ok:2:12:12\n", + "before:bare-fail\nfail:bare-fail:bare error\n", + )), + }, + SourceCase { + name: "03_owned_scoped_forwarded", + source: include_str!("ordinary_borrowed_sums/03_owned_scoped_forwarded.jett"), + expected: ExpectedOutcome::Success(concat!( + "optional:nested:5\noptional:5:18:15:5\n", + "result:nested:12\nresult:12:29:36:12\n", + "generic:1:1\n", + )), + }, + SourceCase { + name: "04_named_prefix_return", + source: include_str!("ordinary_borrowed_sums/04_named_prefix_return.jett"), + expected: ExpectedOutcome::Success(concat!( + "prefix:optional:5\nlater:optional\nreturn:optional\n", + "prefix:result:12\nlater:result\nreturn:result\n", + "answers:7:11\n", + "prefix:optional:5\nlater:optional\nreturn:optional\n", + "prefix:result:12\nlater:result\nreturn:result\n", + "retained:7:11\n", + )), + }, + SourceCase { + name: "05_optional_terminal_prefix", + source: include_str!("ordinary_borrowed_sums/05_optional_terminal_prefix.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before:optional:8\nprefix:optional:5\nfailure:optional\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "06_result_terminal_prefix", + source: include_str!("ordinary_borrowed_sums/06_result_terminal_prefix.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before:result:8\nprefix:result:12\nfailure:result\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "07_outer_pending_some", + source: include_str!("ordinary_borrowed_sums/07_outer_pending_some.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(some(list(2, 3))))", + }, + }, + SourceCase { + name: "08_outer_pending_none", + source: include_str!("ordinary_borrowed_sums/08_outer_pending_none.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(none))", + }, + }, + SourceCase { + name: "09_outer_pending_ok", + source: include_str!("ordinary_borrowed_sums/09_outer_pending_ok.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(ok(list(2, 3))))", + }, + }, + SourceCase { + name: "10_outer_pending_fail", + source: include_str!("ordinary_borrowed_sums/10_outer_pending_fail.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "before\n", + message: "runtime error: handle block requires a result or optional value, got pending(pending(fail(empty)))", + }, + }, + SourceCase { + name: "11_pending_optional_payload", + source: include_str!("ordinary_borrowed_sums/11_pending_optional_payload.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "afteralias\n", + message: "runtime error: list.__sum: argument must be a list", + }, + }, + SourceCase { + name: "12_pending_result_payload", + source: include_str!("ordinary_borrowed_sums/12_pending_result_payload.jett"), + expected: ExpectedOutcome::RuntimeFailure { + stdout: "afteralias\n", + message: "runtime error: list.__sum: argument must be a list", + }, + }, +]; + +struct CheckedOutcome { + output: jett_driver::RunOutput, + status: i32, + stderr: String, +} + +fn reference(case: &SourceCase, source: &Path) -> CheckedOutcome { + let checked = jett_driver::build_file_with_options(source, BuildOptions::default()); + assert!( + !checked.has_errors, + "{}: Source must pass checking: {:?}", + case.name, checked.diagnostics + ); + let outcome = match case.expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(source) + .unwrap_or_else(|failure| panic!("{}: reference: {failure:?}", case.name)); + assert_eq!(output.stdout, stdout, "{}", case.name); + CheckedOutcome { + output, + status: 0, + stderr: String::new(), + } + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(source) + .expect_err("Source must reach its specified reference runtime failure"); + assert_eq!(failure.output.stdout, stdout, "{}", case.name); + assert_eq!(failure.message, message, "{}", case.name); + CheckedOutcome { + output: failure.output, + status: 71, + stderr: format!("{message}\n"), + } + } + }; + assert!(outcome.output.debug_events.is_empty(), "{}", case.name); + assert!( + outcome.output.frontend_debug_observations.is_empty(), + "{}", + case.name + ); + eprintln!( + "reference {}: status={} stdout={:?} stderr={:?}", + case.name, outcome.status, outcome.output.stdout, outcome.stderr + ); + outcome +} + +#[test] +fn ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes() { + assert_eq!(CASES.len(), 12); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } +} + +#[test] +fn ordinary_borrowed_sums_native_packet_matches_reference_without_source() { + let mut refusals = Vec::new(); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + let expected = reference(case, &source); + let mut binaries = Vec::new(); + for release in [false, true] { + let options = BuildOptions { release }; + // Reach native verification before building a runtime archive. A + // refused object is a real parity failure, never an expected pass. + if let Err(error) = emit_host_program_object_for_file_with_options(&source, options) { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("native refusal {refusal}"); + refusals.push(refusal); + continue; + } + let binary = directory + .path() + .join(format!("{}_{release}.exe", case.name)); + match build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + options, + ) { + Ok(artifact) => { + assert!(artifact.debug_observations.is_empty(), "{}", case.name); + binaries.push((binary, release)); + } + Err(error) => { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("native refusal {refusal}"); + refusals.push(refusal); + } + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists(), "{}: Source remains", case.name); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(expected.status), + "{}, release={release}: {actual:?}", + case.name + ); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{}, release={release}", + case.name + ); + assert_eq!( + actual.stderr, + expected.stderr.as_bytes(), + "{}, release={release}: {actual:?}", + case.name + ); + } + } + assert!( + refusals.is_empty(), + "ordinary borrowed-sum parity has {} native refusals:\n{}", + refusals.len(), + refusals.join("\n") + ); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett new file mode 100644 index 00000000..2bfd12a7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett @@ -0,0 +1,26 @@ +namespace app +function inspect(view source: optional[list[int64]], label: string, view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before:{label}\n") + list[int64] alias = view((view source) handle: + optional[int64] missing = none + int64 marker = missing handle: + default 7 + Stdout.write(view stdout, "none:{label}:{marker}\n") + return nothing + ) + list[int64] repeated = view((view source) handle: + Stdout.write(view stdout, "unexpected repeat:{label}\n") + return nothing + ) + Stdout.write(view stdout, "some:{label}:{list.length[int64](view alias)}:{list.sum[int64](view alias)}:{list.sum[int64](view repeated)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] written_some = some(list(2, 3)) + optional[list[int64]] written_none = none + optional[list[int64]] bare_some = some(list(5, 7)) + optional[list[int64]] bare_none = none + inspect(view written_some, "written-some", view stdout) + inspect(view written_none, "written-none", view stdout) + inspect(bare_some, "bare-some", view stdout) + inspect(bare_none, "bare-none", view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett new file mode 100644 index 00000000..b9b64458 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett @@ -0,0 +1,23 @@ +namespace app +function inspect(view source: result[list[int64], string], label: string, view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before:{label}\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "fail:{label}:{error}\n") + return nothing + ) + list[int64] repeated = view((view source) handle error: + Stdout.write(view stdout, "unexpected repeat:{label}:{error}\n") + return nothing + ) + Stdout.write(view stdout, "ok:{label}:{list.length[int64](view alias)}:{list.sum[int64](view alias)}:{list.sum[int64](view repeated)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] written_ok = ok(list(2, 3)) + result[list[int64], string] written_fail = fail("written error") + result[list[int64], string] bare_ok = ok(list(5, 7)) + result[list[int64], string] bare_fail = fail("bare error") + inspect(view written_ok, "written-ok", view stdout) + inspect(view written_fail, "written-fail", view stdout) + inspect(bare_ok, "bare-ok", view stdout) + inspect(bare_fail, "bare-fail", view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett new file mode 100644 index 00000000..0077160f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett @@ -0,0 +1,61 @@ +namespace app +function count[T](view source: optional[T]) returns int64: + T alias = view((view source) handle: + return 0 + ) + return 1 +function inspect_optional(view stdout: Stdout) returns nothing: + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 13) + if true: + list[int64] nested = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function inspect_result(view stdout: Stdout) returns nothing: + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 17) + if true: + list[int64] nested = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle error: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + inspect_optional(view stdout) + inspect_result(view stdout) + optional[int64] scalar = some(7) + optional[list[int64]] values = some(list(11)) + Stdout.write(view stdout, "generic:{count[int64](view scalar)}:{count[list[int64]](view values)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett new file mode 100644 index 00000000..788e8a61 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett @@ -0,0 +1,36 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "unexpected consume\n") + return extra + amount + list.sum[int64](view values) +function observe(view values: list[int64], label: string, view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:{label}:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(label: string, view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:{label}\n") + return none +function inspect_optional(view source: optional[list[int64]], view stdout: Stdout) returns int64: + list[int64] alias = view((view source) handle: + return -1 + ) + return consume(amount: observe(view alias, "optional", view stdout), values: view alias, stdout: view stdout, extra: later("optional", view stdout) handle: + Stdout.write(view stdout, "return:optional\n") + return 7 + ) +function inspect_result(view source: result[list[int64], string], view stdout: Stdout) returns int64: + list[int64] alias = view((view source) handle error: + return -1 + ) + return consume(amount: observe(view alias, "result", view stdout), values: view alias, stdout: view stdout, extra: later("result", view stdout) handle: + Stdout.write(view stdout, "return:result\n") + return 11 + ) +function main(stdout: Stdout) returns nothing: + optional[list[int64]] optional_source = some(list(2, 3)) + result[list[int64], string] result_source = ok(list(5, 7)) + int64 optional_answer = inspect_optional(view optional_source, view stdout) + int64 result_answer = inspect_result(view result_source, view stdout) + Stdout.write(view stdout, "answers:{optional_answer}:{result_answer}\n") + int64 optional_retained = inspect_optional(view optional_source, view stdout) + int64 result_retained = inspect_result(view result_source, view stdout) + Stdout.write(view stdout, "retained:{optional_retained}:{result_retained}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett new file mode 100644 index 00000000..8c40459c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett @@ -0,0 +1,22 @@ +namespace app +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:optional:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:optional\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function consume(amount: int64, extra: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:optional:{bytes.length(view earlier)}\n") + consume(amount: observe(view forwarded, view stdout), values: view forwarded, stdout: view stdout, extra: failing(view stdout)) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett new file mode 100644 index 00000000..8512bda6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett @@ -0,0 +1,22 @@ +namespace app +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:result:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:result\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function consume(amount: int64, extra: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:result:{bytes.length(view earlier)}\n") + consume(amount: observe(view forwarded, view stdout), values: view forwarded, stdout: view stdout, extra: failing(view stdout)) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett new file mode 100644 index 00000000..9f2c25a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = run run some(list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett new file mode 100644 index 00000000..72059abe --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = run run none + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett new file mode 100644 index 00000000..f0b989e7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = run run ok(list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett new file mode 100644 index 00000000..282e4554 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett @@ -0,0 +1,16 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + Stdout.write(view stdout, "before\n") + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = run run fail("empty") + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett new file mode 100644 index 00000000..1625ca6e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett @@ -0,0 +1,15 @@ +namespace app +function inspect(view stdout: Stdout, view source: optional[list[int64]]) returns nothing: + list[int64] alias = view((view source) handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + optional[list[int64]] source = some(run run list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett new file mode 100644 index 00000000..64ee128e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett @@ -0,0 +1,15 @@ +namespace app +function inspect(view stdout: Stdout, view source: result[list[int64], string]) returns nothing: + list[int64] alias = view((view source) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "afteralias\n") + int64 total = list.sum[int64](view forwarded) + Stdout.write(view stdout, "unexpected sum:{total}\n") + return nothing +function main(stdout: Stdout) returns nothing: + result[list[int64], string] source = ok(run run list(2, 3)) + inspect(view stdout, view source) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs new file mode 100644 index 00000000..d5566756 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional.rs @@ -0,0 +1,198 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, emit_host_program_object_for_file_with_options, +}; +use std::fs; +use std::path::Path; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +struct SourceCase { + name: &'static str, + source: &'static str, + expected: ExpectedOutcome, +} + +const CASES: &[SourceCase] = &[ + SourceCase { + name: "13_optional_named_handle_failure", + source: include_str!( + "ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett" + ), + expected: ExpectedOutcome::RuntimeFailure { + stdout: concat!( + "before:optional:8\nprefix:optional:5\nlater:optional\n", + "default:optional\nfailure:optional\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "14_result_named_handle_failure", + source: include_str!( + "ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett" + ), + expected: ExpectedOutcome::RuntimeFailure { + stdout: concat!( + "before:result:8\nprefix:result:12\nlater:result\n", + "default:result\nfailure:result\n", + ), + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + }, + SourceCase { + name: "15_mutable_copied_payloads", + source: include_str!("ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett"), + expected: ExpectedOutcome::Success("integer:4:3\nstring:changed:kept\n"), + }, +]; + +struct CheckedOutcome { + output: jett_driver::RunOutput, + status: i32, + stderr: String, +} + +fn reference(case: &SourceCase, source: &Path) -> CheckedOutcome { + let checked = jett_driver::build_file_with_options(source, BuildOptions::default()); + assert!( + !checked.has_errors, + "{}: Source must pass checking: {:?}", + case.name, checked.diagnostics + ); + let outcome = match case.expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(source) + .unwrap_or_else(|failure| panic!("{}: reference: {failure:?}", case.name)); + assert_eq!(output.stdout, stdout, "{}", case.name); + CheckedOutcome { + output, + status: 0, + stderr: String::new(), + } + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(source) + .expect_err("Source must reach its specified reference runtime failure"); + assert_eq!(failure.output.stdout, stdout, "{}", case.name); + assert_eq!(failure.message, message, "{}", case.name); + CheckedOutcome { + output: failure.output, + status: 71, + stderr: format!("{message}\n"), + } + } + }; + assert!(outcome.output.debug_events.is_empty(), "{}", case.name); + assert!( + outcome.output.frontend_debug_observations.is_empty(), + "{}", + case.name + ); + eprintln!( + "supplemental reference {}: status={} stdout={:?} stderr={:?}", + case.name, outcome.status, outcome.output.stdout, outcome.stderr + ); + outcome +} + +#[test] +fn ordinary_borrowed_sums_additional_reference_matches_three_source_outcomes() { + assert_eq!(CASES.len(), 3); + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } + eprintln!("supplemental reference: 3 Source cases matched exact outcomes"); +} + +#[test] +fn ordinary_borrowed_sums_additional_native_matches_six_executions_without_source() { + assert_eq!(CASES.len(), 3); + let mut refusals = Vec::new(); + let mut executions = 0; + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{}.jett", case.name)); + fs::write(&source, case.source).unwrap(); + let expected = reference(case, &source); + let mut binaries = Vec::new(); + for release in [false, true] { + let options = BuildOptions { release }; + // Refusals are parity failures. Attempt both profiles and remove + // Source before executing any available linked artifacts. + if let Err(error) = emit_host_program_object_for_file_with_options(&source, options) { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("supplemental native refusal {refusal}"); + refusals.push(refusal); + continue; + } + let binary = directory + .path() + .join(format!("{}_{release}.exe", case.name)); + match build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + options, + ) { + Ok(artifact) => { + assert!(artifact.debug_observations.is_empty(), "{}", case.name); + binaries.push((binary, release)); + } + Err(error) => { + let refusal = format!("{}, release={release}: {error:?}", case.name); + eprintln!("supplemental native refusal {refusal}"); + refusals.push(refusal); + } + } + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists(), "{}: Source remains", case.name); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(expected.status), + "{}, release={release}: {actual:?}", + case.name + ); + assert_eq!( + actual.stdout, + expected.output.stdout.as_bytes(), + "{}, release={release}", + case.name + ); + assert_eq!( + actual.stderr, + expected.stderr.as_bytes(), + "{}, release={release}: {actual:?}", + case.name + ); + executions += 1; + eprintln!( + "supplemental native {}: release={release} status={} Source absent", + case.name, expected.status + ); + } + } + assert!( + refusals.is_empty(), + "supplemental ordinary borrowed-sum parity has {} native refusals:\n{}", + refusals.len(), + refusals.join("\n") + ); + assert_eq!(executions, 6, "all 3 Sources require both native profiles"); + eprintln!("supplemental native: 3 Source cases / 6 executions matched exact outcomes"); +} diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett new file mode 100644 index 00000000..d77d9ae5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/13_optional_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:optional:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:optional\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:optional\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:optional:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:optional\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett new file mode 100644 index 00000000..d0d6ee65 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/14_result_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:result:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:result\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:result\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:result:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:result\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett new file mode 100644 index 00000000..30b22192 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_additional/15_mutable_copied_payloads.jett @@ -0,0 +1,25 @@ +namespace app +function copied_int(view stdout: Stdout) returns nothing: + optional[int64] source = some(3) + mutable int64 copied = view((view source) handle: + return nothing + ) + copied = copied + 1 + int64 original = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{original}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + result[string, string] source = ok("kept") + mutable string copied = view((view source) handle error: + return nothing + ) + copied = "changed" + string original = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{original}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_int(view stdout) + copied_string(view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/03_mutable_owned_scoped_forwarded.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/03_mutable_owned_scoped_forwarded.jett new file mode 100644 index 00000000..f14a1a8a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/03_mutable_owned_scoped_forwarded.jett @@ -0,0 +1,61 @@ +namespace app +function count[T](view source: optional[T]) returns int64: + T alias = view((view source) handle: + return 0 + ) + return 1 +function inspect_optional(view stdout: Stdout) returns nothing: + mutable optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 13) + if true: + list[int64] nested = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle: + return nothing + ) + Stdout.write(view stdout, "optional:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function inspect_result(view stdout: Stdout) returns nothing: + mutable result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 17) + if true: + list[int64] nested = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:nested:{list.sum[int64](view nested)}\n") + mutable int64 total = 0 + for number in view forwarded: + list[int64] loop_alias = view((view source) handle error: + return nothing + ) + total = total + number + list.sum[int64](view loop_alias) + list[int64] retained = view((view source) handle error: + return nothing + ) + Stdout.write(view stdout, "result:{list.sum[int64](view forwarded)}:{list.sum[int64](view copied)}:{total}:{list.sum[int64](view retained)}\n") + return nothing +function main(stdout: Stdout) returns nothing: + inspect_optional(view stdout) + inspect_result(view stdout) + optional[int64] scalar = some(7) + optional[list[int64]] values = some(list(11)) + Stdout.write(view stdout, "generic:{count[int64](view scalar)}:{count[list[int64]](view values)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/13_mutable_optional_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/13_mutable_optional_named_handle_failure.jett new file mode 100644 index 00000000..2b29a859 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/13_mutable_optional_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:optional:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:optional\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:optional\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + mutable optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:optional:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:optional\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/14_mutable_result_named_handle_failure.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/14_mutable_result_named_handle_failure.jett new file mode 100644 index 00000000..1ef17736 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/14_mutable_result_named_handle_failure.jett @@ -0,0 +1,28 @@ +namespace app +function consume(extra: int64, amount: int64, view values: list[int64], view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "unexpected consume\n") + return nothing +function observe(view values: list[int64], view stdout: Stdout) returns int64: + Stdout.write(view stdout, "prefix:result:{list.sum[int64](view values)}\n") + return list.length[int64](view values) +function later(view stdout: Stdout) returns optional[int64]: + Stdout.write(view stdout, "later:result\n") + return none +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:result\n") + list[string] broken = list.remove_at[string](list("owned", "other"), -1) + return list.length[string](broken) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + mutable result[list[int64], string] source = ok(list(5, 7)) + list[int64] alias = view((view source) handle error: + return nothing + ) + list[int64] forwarded = alias + Stdout.write(view stdout, "before:result:{bytes.length(view earlier)}\n") + consume(values: view forwarded, amount: observe(view forwarded, view stdout), stdout: view stdout, extra: later(view stdout) handle: + Stdout.write(view stdout, "default:result\n") + default failing(view stdout) + ) + Stdout.write(view stdout, "unexpected after\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/15_mutable_backing_copied_payloads.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/15_mutable_backing_copied_payloads.jett new file mode 100644 index 00000000..9e228d25 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/15_mutable_backing_copied_payloads.jett @@ -0,0 +1,25 @@ +namespace app +function copied_int(view stdout: Stdout) returns nothing: + mutable optional[int64] source = some(3) + mutable int64 copied = view((view source) handle: + return nothing + ) + copied = copied + 1 + int64 original = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{original}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + mutable result[string, string] source = ok("kept") + mutable string copied = view((view source) handle error: + return nothing + ) + copied = "changed" + string original = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{original}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_int(view stdout) + copied_string(view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/16_copied_payload_survives_shell_replacement.jett b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/16_copied_payload_survives_shell_replacement.jett new file mode 100644 index 00000000..433da1d6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/16_copied_payload_survives_shell_replacement.jett @@ -0,0 +1,25 @@ +namespace app +function copied_integer(view stdout: Stdout) returns nothing: + mutable optional[int64] source = some(3) + int64 copied = view((view source) handle: + return nothing + ) + source = some(9) + int64 current = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{current}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + mutable result[string, string] source = ok("kept") + string copied = view((view source) handle error: + return nothing + ) + source = ok("new") + string current = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{current}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_integer(view stdout) + copied_string(view stdout) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_graphics.rs b/crates/jett_driver/tests/native_conformance/pending_graphics.rs new file mode 100644 index 00000000..1066c79b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_graphics.rs @@ -0,0 +1,932 @@ +use super::*; + +const CONFIG_PROBE: &str = r#"namespace app +function update(state: int64, key: graphics.Key) returns int64: + return state +function render(view state: int64) returns graphics.Scene: + graphics.Color background = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: background, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function main(display: Graphics, stdout: Stdout) returns nothing: + graphics.Config config = graphics.Config(title: "pending", width: run 0, height: 1) + Stdout.write(view stdout, "before\n") + graphics.run[int64](view display, config, 0, update, render) handle error: + Stdout.write(view stdout, "handled:{error}\n") + return nothing + Stdout.write(view stdout, "after\n") +"#; + +#[test] +fn native_pending_graphics_config_matches_terminal_reference_failure() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, CONFIG_PROBE).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("pending width must fail decoding before opening a window"); + assert_eq!(expected.output.stdout, "before\n"); + assert_eq!( + expected.message, + "runtime error: graphics: graphics.Config.width must be int64" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("pending_config_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native pending Graphics Config probe"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, format!("{}\n", expected.message).as_bytes()); + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DecoderKind { + Config, + Scene, +} + +#[derive(Clone, Copy)] +enum DecoderExpectation { + Terminal(&'static str), + Handled(&'static str), +} + +struct DecoderCase { + name: String, + kind: DecoderKind, + body: String, + expectation: DecoderExpectation, +} + +const CONFIG_BODY: &str = r#" return graphics.Config(title: "pending", width: 16, height: 16) +"#; + +// An invalid ready background keeps old implementations headless when they +// accidentally ignore a pending value. Shape decoding must precede this domain +// failure, even when the pending value appears in a later rectangle or text. +const SCENE_BODY: &str = r#" graphics.Color background = graphics.Color(red: 999, green: 0, blue: 0) + graphics.Color ink = graphics.Color(red: 1, green: 2, blue: 3) + graphics.Rect rectangle = graphics.Rect(x: 0, y: 0, width: 1, height: 1, color: ink) + graphics.Color letter_ink = graphics.Color(red: 4, green: 5, blue: 6) + graphics.Text label = graphics.Text(x: 0, y: 0, text: "label", scale: 1, color: letter_ink) + list[graphics.Rect] rectangles = list(rectangle) + list[graphics.Text] texts = list(label) + return graphics.Scene(background: background, rectangles: rectangles, texts: texts) +"#; + +const DIMENSIONS_ERROR: &str = "graphics.run: window dimensions must be in 1..2048"; +const COLOR_ERROR: &str = "graphics.run: color channels must be in 0..255"; + +fn replace_fixture(source: &str, changes: &[(&str, &str)]) -> String { + changes + .iter() + .fold(source.to_owned(), |source, (old, new)| { + assert!(source.contains(old), "fixture replacement missing {old:?}"); + source.replacen(old, new, 1) + }) +} + +fn decoder_case( + name: &str, + kind: DecoderKind, + changes: &[(&str, &str)], + expectation: DecoderExpectation, +) -> DecoderCase { + DecoderCase { + name: name.to_owned(), + kind, + body: replace_fixture( + match kind { + DecoderKind::Config => CONFIG_BODY, + DecoderKind::Scene => SCENE_BODY, + }, + changes, + ), + expectation, + } +} + +fn config_cases() -> Vec { + use DecoderExpectation::{Handled, Terminal}; + let mut cases = vec![ + decoder_case( + "config_root", + DecoderKind::Config, + &[("return graphics.Config", "return run graphics.Config")], + Terminal("graphics: expected graphics.Config"), + ), + decoder_case( + "config_root_twice", + DecoderKind::Config, + &[ + ("return graphics.Config", "return run (run graphics.Config"), + ("height: 16)", "height: 16))"), + ], + Terminal("graphics: expected graphics.Config"), + ), + decoder_case( + "config_title", + DecoderKind::Config, + &[("title: \"pending\"", "title: run \"pending\"")], + Terminal("graphics: graphics.Config.title must be string"), + ), + decoder_case( + "config_title_twice", + DecoderKind::Config, + &[("title: \"pending\"", "title: run (run \"pending\")")], + Terminal("graphics: graphics.Config.title must be string"), + ), + decoder_case( + "config_width", + DecoderKind::Config, + &[("width: 16", "width: run 0")], + Terminal("graphics: graphics.Config.width must be int64"), + ), + decoder_case( + "config_height", + DecoderKind::Config, + &[("height: 16", "height: run 0")], + Terminal("graphics: graphics.Config.height must be int64"), + ), + decoder_case( + "config_title_precedes_width", + DecoderKind::Config, + &[ + ("title: \"pending\"", "title: run \"pending\""), + ("width: 16", "width: run 0"), + ], + Terminal("graphics: graphics.Config.title must be string"), + ), + decoder_case( + "config_width_precedes_height", + DecoderKind::Config, + &[ + ("width: 16", "width: run 0"), + ("height: 16", "height: run 0"), + ], + Terminal("graphics: graphics.Config.width must be int64"), + ), + decoder_case( + "config_decoding_precedes_dimensions", + DecoderKind::Config, + &[("width: 16", "width: 0"), ("height: 16", "height: run 0")], + Terminal("graphics: graphics.Config.height must be int64"), + ), + decoder_case( + "config_comptime_width", + DecoderKind::Config, + &[("width: 16", "width: comptime run 0")], + Terminal("graphics: graphics.Config.width must be int64"), + ), + decoder_case( + "config_ready_dimensions", + DecoderKind::Config, + &[("width: 16", "width: 0")], + Handled(DIMENSIONS_ERROR), + ), + ]; + cases.push(DecoderCase { + name: "config_partial_join".into(), + kind: DecoderKind::Config, + body: r#" graphics.Config pending = run (run graphics.Config(title: "pending", width: 0, height: 1)) + graphics.Config first = join pending handle error: + default graphics.Config(title: "fallback", width: 0, height: 1) + return first +"#.into(), + expectation: Terminal("graphics: expected graphics.Config"), + }); + cases.push(DecoderCase { + name: "config_partial_string_join".into(), + kind: DecoderKind::Config, + body: r#" string pending = run (run "pending") + string first = join pending handle error: + default "fallback" + return graphics.Config(title: first, width: 0, height: 1) +"# + .into(), + expectation: Terminal("graphics: graphics.Config.title must be string"), + }); + cases.push(DecoderCase { + name: "config_partial_integer_join".into(), + kind: DecoderKind::Config, + body: r#" int64 pending = run (run 0) + int64 first = join pending handle error: + default 0 + return graphics.Config(title: "pending", width: first, height: 1) +"# + .into(), + expectation: Terminal("graphics: graphics.Config.width must be int64"), + }); + cases.push(DecoderCase { + name: "config_joined_root_and_fields".into(), + kind: DecoderKind::Config, + body: r#" string title_task = run (run "joined") + string first_title = join title_task handle error: + default "fallback" + string title = join first_title handle error: + default "fallback" + int64 width_task = run (run 0) + int64 first_width = join width_task handle error: + default 0 + int64 width = join first_width handle error: + default 0 + graphics.Config task = run (run graphics.Config(title: title, width: width, height: 1)) + graphics.Config first = join task handle error: + default graphics.Config(title: "fallback", width: 0, height: 1) + return join first handle error: + default graphics.Config(title: "fallback", width: 0, height: 1) +"# + .into(), + expectation: Handled(DIMENSIONS_ERROR), + }); + cases +} + +fn scene_cases() -> Vec { + use DecoderExpectation::{Handled, Terminal}; + let mut cases = vec![ + decoder_case( + "scene_root", + DecoderKind::Scene, + &[("return graphics.Scene", "return run graphics.Scene")], + Terminal("graphics: expected graphics.Scene"), + ), + decoder_case( + "scene_root_twice", + DecoderKind::Scene, + &[ + ("return graphics.Scene", "return run (run graphics.Scene"), + ("texts: texts)", "texts: texts))"), + ], + Terminal("graphics: expected graphics.Scene"), + ), + decoder_case( + "scene_background", + DecoderKind::Scene, + &[( + "background = graphics.Color", + "background = run graphics.Color", + )], + Terminal("graphics: expected graphics.Color"), + ), + decoder_case( + "scene_background_red", + DecoderKind::Scene, + &[("red: 999", "red: run 999")], + Terminal("graphics: graphics.Color.red must be int64"), + ), + decoder_case( + "scene_background_green", + DecoderKind::Scene, + &[("green: 0", "green: run 0")], + Terminal("graphics: graphics.Color.green must be int64"), + ), + decoder_case( + "scene_background_blue", + DecoderKind::Scene, + &[("blue: 0", "blue: run 0")], + Terminal("graphics: graphics.Color.blue must be int64"), + ), + decoder_case( + "scene_rectangles", + DecoderKind::Scene, + &[( + "rectangles = list(rectangle)", + "rectangles = run list(rectangle)", + )], + Terminal("graphics: graphics.Scene.rectangles must be a list"), + ), + decoder_case( + "scene_rectangles_twice", + DecoderKind::Scene, + &[( + "rectangles = list(rectangle)", + "rectangles = run (run list(rectangle))", + )], + Terminal("graphics: graphics.Scene.rectangles must be a list"), + ), + decoder_case( + "scene_rectangle", + DecoderKind::Scene, + &[("rectangle = graphics.Rect", "rectangle = run graphics.Rect")], + Terminal("graphics: expected graphics.Rect"), + ), + decoder_case( + "scene_rectangle_twice", + DecoderKind::Scene, + &[( + "rectangle = graphics.Rect(x: 0, y: 0, width: 1, height: 1, color: ink)", + "rectangle = run (run graphics.Rect(x: 0, y: 0, width: 1, height: 1, color: ink))", + )], + Terminal("graphics: expected graphics.Rect"), + ), + decoder_case( + "scene_texts", + DecoderKind::Scene, + &[("texts = list(label)", "texts = run list(label)")], + Terminal("graphics: graphics.Scene.texts must be a list"), + ), + decoder_case( + "scene_texts_twice", + DecoderKind::Scene, + &[("texts = list(label)", "texts = run (run list(label))")], + Terminal("graphics: graphics.Scene.texts must be a list"), + ), + decoder_case( + "scene_text", + DecoderKind::Scene, + &[("label = graphics.Text", "label = run graphics.Text")], + Terminal("graphics: expected graphics.Text"), + ), + decoder_case( + "scene_text_twice", + DecoderKind::Scene, + &[( + "label = graphics.Text(x: 0, y: 0, text: \"label\", scale: 1, color: letter_ink)", + "label = run (run graphics.Text(x: 0, y: 0, text: \"label\", scale: 1, color: letter_ink))", + )], + Terminal("graphics: expected graphics.Text"), + ), + decoder_case( + "scene_rectangle_color", + DecoderKind::Scene, + &[("ink = graphics.Color", "ink = run graphics.Color")], + Terminal("graphics: expected graphics.Color"), + ), + decoder_case( + "scene_text_color", + DecoderKind::Scene, + &[( + "letter_ink = graphics.Color", + "letter_ink = run graphics.Color", + )], + Terminal("graphics: expected graphics.Color"), + ), + decoder_case( + "scene_background_precedes_rectangles", + DecoderKind::Scene, + &[ + ("green: 0", "green: run 0"), + ( + "rectangles = list(rectangle)", + "rectangles = run list(rectangle)", + ), + ], + Terminal("graphics: graphics.Color.green must be int64"), + ), + decoder_case( + "scene_rectangles_precede_texts", + DecoderKind::Scene, + &[ + ( + "rectangles = list(rectangle)", + "rectangles = run list(rectangle)", + ), + ("texts = list(label)", "texts = run list(label)"), + ], + Terminal("graphics: graphics.Scene.rectangles must be a list"), + ), + decoder_case( + "scene_rectangle_precedes_text", + DecoderKind::Scene, + &[ + ( + "rectangle = graphics.Rect(x: 0", + "rectangle = graphics.Rect(x: run 0", + ), + ( + "label = graphics.Text(x: 0", + "label = graphics.Text(x: run 0", + ), + ], + Terminal("graphics: graphics.Rect.x must be int64"), + ), + decoder_case( + "scene_ready_color", + DecoderKind::Scene, + &[], + Handled(COLOR_ERROR), + ), + ]; + for (field, value, error) in [ + ("x", "0", "graphics: graphics.Rect.x must be int64"), + ("y", "0", "graphics: graphics.Rect.y must be int64"), + ("width", "1", "graphics: graphics.Rect.width must be int64"), + ( + "height", + "1", + "graphics: graphics.Rect.height must be int64", + ), + ] { + let original = "rectangle = graphics.Rect(x: 0, y: 0, width: 1, height: 1, color: ink)"; + let changed = original.replacen( + &format!("{field}: {value}"), + &format!("{field}: run {value}"), + 1, + ); + cases.push(decoder_case( + &format!("scene_rectangle_{field}"), + DecoderKind::Scene, + &[(original, &changed)], + Terminal(error), + )); + } + for (field, value, error) in [ + ("x", "0", "graphics: graphics.Text.x must be int64"), + ("y", "0", "graphics: graphics.Text.y must be int64"), + ( + "text", + "\"label\"", + "graphics: graphics.Text.text must be string", + ), + ("scale", "1", "graphics: graphics.Text.scale must be int64"), + ] { + let original = + "label = graphics.Text(x: 0, y: 0, text: \"label\", scale: 1, color: letter_ink)"; + let changed = original.replacen( + &format!("{field}: {value}"), + &format!("{field}: run {value}"), + 1, + ); + cases.push(decoder_case( + &format!("scene_text_{field}"), + DecoderKind::Scene, + &[(original, &changed)], + Terminal(error), + )); + } + for (owner, values) in [("ink", ["1", "2", "3"]), ("letter_ink", ["4", "5", "6"])] { + for ((field, value), error) in ["red", "green", "blue"].into_iter().zip(values).zip([ + "graphics: graphics.Color.red must be int64", + "graphics: graphics.Color.green must be int64", + "graphics: graphics.Color.blue must be int64", + ]) { + let original = format!( + "{owner} = graphics.Color(red: {}, green: {}, blue: {})", + values[0], values[1], values[2] + ); + let changed = original.replacen( + &format!("{field}: {value}"), + &format!("{field}: run {value}"), + 1, + ); + cases.push(decoder_case( + &format!("scene_{owner}_{field}"), + DecoderKind::Scene, + &[(&original, &changed)], + Terminal(error), + )); + } + } + for (name, kind, binding, fallback, error) in [ + ( + "scene_partial_root_join", + "graphics.Scene", + "graphics.Scene(background: background, rectangles: rectangles, texts: texts)", + "fallback_scene()", + "graphics: expected graphics.Scene", + ), + ( + "scene_partial_rectangles_join", + "list[graphics.Rect]", + "list(rectangle)", + "list.new[graphics.Rect]()", + "graphics: graphics.Scene.rectangles must be a list", + ), + ( + "scene_partial_texts_join", + "list[graphics.Text]", + "list(label)", + "list.new[graphics.Text]()", + "graphics: graphics.Scene.texts must be a list", + ), + ] { + let original = match name { + "scene_partial_root_join" => format!(" return {binding}\n"), + "scene_partial_rectangles_join" => format!(" {kind} rectangles = {binding}\n"), + _ => format!(" {kind} texts = {binding}\n"), + }; + let target = match name { + "scene_partial_root_join" => " return first\n".to_owned(), + "scene_partial_rectangles_join" => format!(" {kind} rectangles = first\n"), + _ => format!(" {kind} texts = first\n"), + }; + let changed = format!( + " {kind} task = run (run {binding})\n {kind} first = join task handle error:\n default {fallback}\n{target}" + ); + cases.push(decoder_case( + name, + DecoderKind::Scene, + &[(&original, &changed)], + Terminal(error), + )); + } + cases.push(decoder_case( + "scene_comptime_text", + DecoderKind::Scene, + &[("text: \"label\"", "text: comptime run (run \"label\")")], + Terminal("graphics: graphics.Text.text must be string"), + )); + cases +} + +fn append_dispatch(source: &mut String, stem: &str, cases: &[&DecoderCase], result: &str) { + for (group, chunk) in cases.chunks(8).enumerate() { + source.push_str(&format!( + "function {stem}_group_{group}(name: string) returns {result}:\n" + )); + for case in chunk { + source.push_str(&format!( + " if name == \"{}\":\n return probe_{}()\n", + case.name, case.name + )); + } + source.push_str(&format!(" return fallback_{stem}()\n")); + } + source.push_str(&format!( + "function choose_{stem}(bucket: string, name: string) returns {result}:\n" + )); + for group in 0..cases.len().div_ceil(8) { + source.push_str(&format!( + " if bucket == \"{group}\":\n return {stem}_group_{group}(name)\n" + )); + } + source.push_str(&format!(" return fallback_{stem}()\n")); +} + +fn matrix_source(cases: &[DecoderCase]) -> String { + let mut source = r#"namespace app +struct Request: + bucket: string + name: string +function fallback_config() returns graphics.Config: + return graphics.Config(title: "ready", width: 16, height: 16) +function fallback_scene() returns graphics.Scene: + graphics.Color background = graphics.Color(red: 999, green: 0, blue: 0) + return graphics.Scene(background: background, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +"#.to_owned(); + for case in cases { + let result = match case.kind { + DecoderKind::Config => "graphics.Config", + DecoderKind::Scene => "graphics.Scene", + }; + source.push_str(&format!( + "function probe_{}() returns {result}:\n{}", + case.name, case.body + )); + } + for (kind, stem, result) in [ + (DecoderKind::Config, "config", "graphics.Config"), + (DecoderKind::Scene, "scene", "graphics.Scene"), + ] { + let selected = cases + .iter() + .filter(|case| case.kind == kind) + .collect::>(); + append_dispatch(&mut source, stem, &selected, result); + } + source.push_str( + r#"function update(state: Request, key: graphics.Key) returns Request: + return state +function render(view state: Request) returns graphics.Scene: + return choose_scene(state.bucket, state.name) +function request_text(view env: Environment, key: string) returns string: + optional[string] value = Environment.get(view env, key) handle error: + return error + return value handle: + default "missing" +function main(display: Graphics, stdout: Stdout, env: Environment) returns nothing: + string name = request_text(view env, "case") + string bucket = request_text(view env, "bucket") + graphics.Config config = choose_config(bucket, name) + Request request = Request(bucket: bucket, name: name) + Stdout.write(view stdout, "before\n") + graphics.run[Request](view display, config, request, update, render) handle error: + Stdout.write(view stdout, "handled:{error}\n") + return nothing + Stdout.write(view stdout, "after\n") +"#, + ); + source +} + +fn case_snapshot( + cases: &[DecoderCase], + case: &DecoderCase, +) -> environment::EnvironmentTestSnapshot { + let position = cases + .iter() + .filter(|candidate| candidate.kind == case.kind) + .position(|candidate| candidate.name == case.name) + .unwrap(); + environment::EnvironmentTestSnapshot { + arguments: Vec::new(), + entries: [ + ("case", case.name.clone()), + ("bucket", (position / 8).to_string()), + ] + .into_iter() + .map(|(name, value)| environment::EnvironmentTestEntry { + name: environment::EnvironmentTestText::Unicode(name.into()), + value: environment::EnvironmentTestText::Unicode(value), + }) + .collect(), + } +} + +fn build_profiles(source: &Path, directory: &Path, name: &str) -> Vec<(std::path::PathBuf, bool)> { + [false, true] + .into_iter() + .map(|release| { + let binary = directory.join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native pending Graphics decoder matrix"); + (binary, release) + }) + .collect() +} + +#[test] +fn native_pending_graphics_decoders_preserve_reference_errors_and_order_in_both_profiles() { + let mut cases = config_cases(); + cases.extend(scene_cases()); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("matrix.jett"); + fs::write(&source, matrix_source(&cases)).unwrap(); + // Pin the reference independently before emitting any native binaries. + for case in &cases { + let outcome = jett_driver::run_file_capture_outcome_with_environment_test_snapshot( + &source, + case_snapshot(&cases, case), + ); + match case.expectation { + DecoderExpectation::Terminal(message) => { + let expected = outcome.expect_err(&format!("{} must fail decoding", case.name)); + assert_eq!( + expected.message, + format!("runtime error: {message}"), + "{}", + case.name + ); + assert_eq!(expected.output.stdout, "before\n", "{}", case.name); + assert!(expected.output.debug_events.is_empty(), "{}", case.name); + } + DecoderExpectation::Handled(message) => { + let expected = outcome.unwrap_or_else(|error| panic!("{}: {error:?}", case.name)); + assert_eq!( + expected.stdout, + format!("before\nhandled:{message}\n"), + "{}", + case.name + ); + assert!(expected.debug_events.is_empty(), "{}", case.name); + } + } + } + let binaries = build_profiles(&source, directory.path(), "decoder_matrix"); + fs::remove_file(&source).unwrap(); + for (binary, _) in binaries { + for case in &cases { + let script = environment::encode_test_snapshot(&case_snapshot(&cases, case)); + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((environment::TEST_SNAPSHOT_ENV, &script)), + ); + match case.expectation { + DecoderExpectation::Terminal(message) => { + assert_eq!(actual.status.code(), Some(71), "{}: {actual:?}", case.name); + assert_eq!(actual.stdout, b"before\n", "{}", case.name); + assert_eq!( + actual.stderr, + format!("runtime error: {message}\n").as_bytes(), + "{}", + case.name + ); + } + DecoderExpectation::Handled(message) => { + assert!(actual.status.success(), "{}: {actual:?}", case.name); + assert_eq!( + actual.stdout, + format!("before\nhandled:{message}\n").as_bytes(), + "{}", + case.name + ); + assert!(actual.stderr.is_empty(), "{}: {actual:?}", case.name); + } + } + } + } +} + +const SCRIPTED_FAILURE: &str = r#"namespace app +function update(state: int64, key: graphics.Key) returns int64: + return 1 +function render(view state: int64) returns graphics.Scene: + if state == 1: + graphics.Color bad_background = graphics.Color(red: 999, green: 0, blue: 0) + graphics.Color ink = graphics.Color(red: 0, green: 0, blue: 0) + graphics.Text label = graphics.Text(x: run 0, y: 0, text: "pending", scale: 1, color: ink) + return graphics.Scene(background: bad_background, rectangles: list.new[graphics.Rect](), texts: list(label)) + graphics.Color background = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: background, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function main(display: Graphics, stdout: Stdout) returns nothing: + graphics.Config config = graphics.Config(title: "scripted", width: 16, height: 16) + Stdout.write(view stdout, "before\n") + graphics.run[int64](view display, config, INITIAL_STATE, update, render) handle error: + Stdout.write(view stdout, "handled:{error}\n") + return nothing + Stdout.write(view stdout, "after\n") +"#; + +#[test] +fn native_pending_graphics_render_failures_preserve_terminal_error_and_cleanup() { + for (name, initial, events) in [ + ( + "initial", + "1", + vec![graphics::TestEvent::HostError("must remain unused".into())], + ), + ( + "later", + "0", + vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + graphics::TestEvent::Key(graphics::Key::Left), + ], + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, SCRIPTED_FAILURE.replace("INITIAL_STATE", initial)).unwrap(); + let expected = jett_driver::run_file_capture_outcome_with_graphics_test_events( + &source, + events.clone(), + ) + .expect_err("scene decoding must fail before presentation or further provider input"); + assert_eq!(expected.output.stdout, "before\n", "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + assert_eq!( + expected.message, "runtime error: graphics: graphics.Text.x must be int64", + "{name}" + ); + let binaries = build_profiles(&source, directory.path(), name); + fs::remove_file(&source).unwrap(); + let script = graphics::encode_test_script(&events); + for (binary, _) in binaries { + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((graphics::TEST_SCRIPT_ENV, &script)), + ); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, b"before\n", "{name}"); + assert_eq!( + actual.stderr, b"runtime error: graphics: graphics.Text.x must be int64\n", + "{name}" + ); + } + } +} + +const JOINED_CONTROL: &str = r#"namespace app +function joined[T](value: T, fallback: T) returns T: + T task = run (run value) + T first = join task handle error: + default clone fallback + return join first handle error: + default fallback +function zero_color() returns graphics.Color: + return graphics.Color(red: 0, green: 0, blue: 0) +function plain_rectangle() returns graphics.Rect: + return graphics.Rect(x: 0, y: 0, width: 1, height: 1, color: zero_color()) +function plain_text() returns graphics.Text: + return graphics.Text(x: 0, y: 0, text: "fallback", scale: 1, color: zero_color()) +function plain_scene() returns graphics.Scene: + return graphics.Scene(background: zero_color(), rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function ready_rectangle() returns graphics.Rect: + graphics.Color ink = joined[graphics.Color](zero_color(), zero_color()) + graphics.Rect rectangle = graphics.Rect(x: joined[int64](0, 0), y: 0, width: 1, height: 1, color: ink) + return joined[graphics.Rect](rectangle, plain_rectangle()) +function ready_text() returns graphics.Text: + graphics.Color ink = joined[graphics.Color](zero_color(), zero_color()) + string text = joined[string]("joined", "fallback") + graphics.Text label = graphics.Text(x: 0, y: 0, text: text, scale: joined[int64](1, 1), color: ink) + return joined[graphics.Text](label, plain_text()) +function ready_scene() returns graphics.Scene: + graphics.Color background = joined[graphics.Color](zero_color(), zero_color()) + list[graphics.Rect] rectangles = joined[list[graphics.Rect]](list(ready_rectangle()), list.new[graphics.Rect]()) + list[graphics.Text] texts = joined[list[graphics.Text]](list(ready_text()), list.new[graphics.Text]()) + graphics.Scene scene = graphics.Scene(background: background, rectangles: rectangles, texts: texts) + return joined[graphics.Scene](scene, plain_scene()) +function ready_config() returns graphics.Config: + string title = joined[string]("joined", "fallback") + graphics.Config config = graphics.Config(title: title, width: joined[int64](16, 1), height: joined[int64](16, 1)) + return joined[graphics.Config](config, graphics.Config(title: "fallback", width: 1, height: 1)) +function update(state: int64, key: graphics.Key) returns int64: + trace state + return run state +function render(view state: int64) returns graphics.Scene: + trace state + return ready_scene() +function main(display: Graphics, stdout: Stdout) returns nothing: + graphics.Config config = comptime ready_config() + int64 initial = run 5 + Stdout.write(view stdout, "before\n") + graphics.run[int64](view display, config, initial, update, render) handle error: + Stdout.write(view stdout, "handled:{error}\n") + return nothing + Stdout.write(view stdout, "closed\n") +verify fully_joined_graphics_data: + graphics.Config config = ready_config() + assert config.title == "joined" + assert config.width == 16 + assert config.height == 16 + graphics.Scene scene = ready_scene() + assert scene.background.red == 0 + assert list.length(view scene.rectangles) == 1 + assert list.length(view scene.texts) == 1 + graphics.Rect rectangle = list.get[graphics.Rect](view scene.rectangles, 0) handle: + default plain_rectangle() + assert rectangle.width == 1 + graphics.Text label = list.get[graphics.Text](view scene.texts, 0) handle: + default plain_text() + assert label.text == "joined" +property joined_graphics_data_trials: + given seed: int64 + assert joined[int64](seed, 0) == seed + graphics.Scene scene = ready_scene() + assert scene.background.red == 0 + assert list.length(view scene.rectangles) == 1 + assert list.length(view scene.texts) == 1 +"#; + +#[test] +fn native_joined_graphics_data_and_pending_state_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("joined.jett"); + fs::write(&source, JOINED_CONTROL).unwrap(); + let events = vec![ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ]; + let expected = + jett_driver::run_file_capture_outcome_with_graphics_test_events(&source, events.clone()) + .expect("fully joined graphics data must accept a still-pending callback State"); + assert_eq!(expected.stdout, "before\nclosed\n"); + assert_eq!( + debug_trace_lines(&expected.debug_events), + [ + "trace state: int64 = pending(5)", + "trace state: int64 = pending(5)", + "trace state: int64 = pending(pending(5))", + ] + ); + let binaries = build_profiles(&source, directory.path(), "joined"); + let verify_binary = directory.path().join("joined_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled joined graphics data verify suite"); + let property_binary = directory.path().join("joined_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled joined graphics data property suite"); + fs::remove_file(&source).unwrap(); + let script = graphics::encode_test_script(&events); + for (binary, release) in binaries { + let actual = run_bounded_with_env( + &binary, + directory.path(), + Some((graphics::TEST_SCRIPT_ENV, &script)), + ); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, b"before\nclosed\n"); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics.rs b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics.rs new file mode 100644 index 00000000..67a667f8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics.rs @@ -0,0 +1,181 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable; +use jett_driver::{build_file, run_file_capture_outcome}; +use std::fs; + +fn run_case(name: &str, source_bytes: &[u8], stdout: &str, failure: Option<&str>) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_bytes).unwrap(); + let checked = build_file(&source); + assert!(!checked.has_errors, "{name}: {:?}", checked.diagnostics); + let (reference, status, stderr) = match failure { + Some(message) => { + let actual = run_file_capture_outcome(&source).expect_err("terminal receiver failure"); + assert_eq!(actual.message, message, "{name}"); + (actual.output, 71, format!("{message}\n")) + } + None => ( + run_file_capture_outcome(&source).expect("ready sum control"), + 0, + String::new(), + ), + }; + assert_eq!(reference.stdout, stdout, "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let binaries = [false, true].map(|release| { + let binary = directory.path().join(format!("{name}-{release}.exe")); + let artifact = build_host_executable(&source, launcher_for_options(release), &binary) + .unwrap_or_else(|error| panic!("{name}, runtime release={release}: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + (binary, release) + }); + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, runtime release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + stdout.as_bytes(), + "{name}, runtime release={release}" + ); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, runtime release={release}" + ); + } +} + +#[test] +fn native_pending_sum_diagnostics_redact_secret_result_error_without_joining() { + run_case( + "01_pending_secret_error", + include_bytes!("pending_sum_diagnostics/01_pending_secret_error.jett"), + "before\n", + Some( + "runtime error: handle block requires a result or optional value, got pending(pending(fail([redacted])))", + ), + ); +} + +#[test] +fn native_pending_sum_diagnostics_redact_nested_optional_field_without_joining() { + run_case( + "02_pending_optional_secret_field", + include_bytes!("pending_sum_diagnostics/02_pending_optional_secret_field.jett"), + "before\n", + Some( + "runtime error: handle block requires a result or optional value, got pending(pending(some(app.Hidden(token: [redacted]))))", + ), + ); +} + +#[test] +fn native_pending_sum_diagnostics_preserve_pipeline_result_secret_type() { + run_case( + "03_pipeline_pending_secret_error", + include_bytes!("pending_sum_diagnostics/03_pipeline_pending_secret_error.jett"), + "before\n", + Some( + "runtime error: handle block requires a result or optional value, got pending(pending(fail([redacted])))", + ), + ); +} + +#[test] +fn native_pending_sum_diagnostics_keep_ready_payloads_and_handler_error_types() { + run_case( + "ready", + br#"namespace app +function ready(value: int64) returns result[int64, secret[string]]: + return fail("ready-secret") +function main(stdout: Stdout) returns nothing: + result[secret[int64], string] success = ok(7) + secret[int64] direct = success handle error: + return nothing + int64 public_value = declassify direct + int64 fallback = 7 into ready handle error: + string message = declassify error + default string.char_count(message) + optional[secret[string]] candidate = some("ready-optional") + secret[string] selected = candidate handle: + return nothing + string public_text = declassify selected + Stdout.write(view stdout, "{public_value}:{fallback}:{string.char_count(public_text)}\n") + return nothing +"#, + "7:12:14\n", + None, + ); +} + +#[test] +fn native_pending_sum_diagnostics_preserve_prior_target_failure_and_once_order() { + const SOURCE: &str = r#"namespace app +function failed(view stdout: Stdout) returns result[int64, secret[string]]: + Stdout.write(view stdout, "producer\n") + string rejected = string.repeat("ab", 9223372036854775807) + return ok(string.char_count(rejected)) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + int64 unused = CANDIDATE handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing +"#; + for (name, candidate) in [ + ("prior_direct", "failed(view stdout)"), + ("prior_pipeline", "view stdout into failed"), + ] { + run_case( + name, + SOURCE.replace("CANDIDATE", candidate).as_bytes(), + "before\nproducer\n", + Some("runtime error: string.repeat: requested output is too large"), + ); + } +} + +#[test] +fn native_pending_sum_diagnostics_preserve_named_and_retained_view_pipeline_types() { + for (name, source) in [ + ( + "parenthesized_view_named", + include_bytes!("pending_sum_diagnostics/04_parenthesized_view_named_pipeline.jett") + .as_slice(), + ), + ( + "named_view_parameter", + include_bytes!("pending_sum_diagnostics/06_named_view_parameter_pipeline.jett") + .as_slice(), + ), + ( + "retained_view_parameter", + include_bytes!("pending_sum_diagnostics/07_retained_view_parameter_pipeline.jett") + .as_slice(), + ), + ] { + run_case( + name, + source, + "before\n", + Some( + "runtime error: handle block requires a result or optional value, got pending(pending(fail([redacted])))", + ), + ); + } +} diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/01_pending_secret_error.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/01_pending_secret_error.jett new file mode 100644 index 00000000..dfe8f3a2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/01_pending_secret_error.jett @@ -0,0 +1,9 @@ +namespace app +function main(stdout: Stdout) returns nothing: + result[int64, secret[string]] candidate = run run fail("pending-secret-error") + Stdout.write(view stdout, "before\n") + int64 unused = candidate handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/02_pending_optional_secret_field.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/02_pending_optional_secret_field.jett new file mode 100644 index 00000000..a7a342c1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/02_pending_optional_secret_field.jett @@ -0,0 +1,11 @@ +namespace app +struct Hidden: + token: secret[string] +function main(stdout: Stdout) returns nothing: + optional[Hidden] candidate = run run some(Hidden(token: "pending-secret-field")) + Stdout.write(view stdout, "before\n") + Hidden unused = candidate handle: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/03_pipeline_pending_secret_error.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/03_pipeline_pending_secret_error.jett new file mode 100644 index 00000000..73d956f2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/03_pipeline_pending_secret_error.jett @@ -0,0 +1,10 @@ +namespace app +function delayed(value: int64) returns result[int64, secret[string]]: + return run run fail("pipeline-secret-error") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + int64 unused = 7 into delayed handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/04_parenthesized_view_named_pipeline.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/04_parenthesized_view_named_pipeline.jett new file mode 100644 index 00000000..2d9b16d4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/04_parenthesized_view_named_pipeline.jett @@ -0,0 +1,10 @@ +namespace app +function delayed(value: int64) returns result[int64, secret[string]]: + return run run fail("pipeline-secret-error") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + int64 unused = 7 into (view delayed) handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/06_named_view_parameter_pipeline.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/06_named_view_parameter_pipeline.jett new file mode 100644 index 00000000..eaccafd4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/06_named_view_parameter_pipeline.jett @@ -0,0 +1,10 @@ +namespace app +function delayed(view value: int64) returns result[int64, secret[string]]: + return run run fail("pipeline-secret-error") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + int64 unused = 7 into view delayed handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/07_retained_view_parameter_pipeline.jett b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/07_retained_view_parameter_pipeline.jett new file mode 100644 index 00000000..55b5415d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/pending_sum_diagnostics/07_retained_view_parameter_pipeline.jett @@ -0,0 +1,11 @@ +namespace app +function delayed(view value: int64) returns result[int64, secret[string]]: + return run run fail("pipeline-secret-error") +function main(stdout: Stdout) returns nothing: + function(view int64) returns result[int64, secret[string]] callback = delayed + Stdout.write(view stdout, "before\n") + int64 unused = 7 into view callback handle error: + Stdout.write(view stdout, "unexpected handler\n") + return nothing + Stdout.write(view stdout, "unexpected success\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/qualified_result_tasks.rs b/crates/jett_driver/tests/native_conformance/qualified_result_tasks.rs new file mode 100644 index 00000000..7879222a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/qualified_result_tasks.rs @@ -0,0 +1,457 @@ +use super::*; + +const JOIN_CONTROLS: &str = r#"namespace app +type OutcomeAlias = result[list[string], string] +type HiddenAlias = secret[result[list[string], string]] +type RefinedOutcome = result[list[string], string] where true +function public_outcome(valid: bool) returns result[list[string], string]: + if valid: + return ok(list("Ada-λ🙂", "kept")) + return fail("missing-λ🙂") +function qualify[T](candidate: result[T, string]) returns secret[result[T, string]]: + return candidate +function join_hidden[T](candidate: secret[result[T, string]]) returns secret[result[T, string]]: + secret[result[T, string]] ready = join candidate handle error: + result[T, string] failed = fail("outer:{error}") + return qualify[T](failed) + return ready +function describe_public(view candidate: result[list[string], string]) returns string: + list[string] values = clone candidate handle error: + return "inner:{error}" + string first = list.get[string](view values, 0) handle: + return "missing" + return "ok:{list.length(view values)}:{first}" +function describe(view candidate: secret[result[list[string], string]]) returns string: + result[list[string], string] public = declassify clone candidate + return describe_public(view public) +function ready_report(valid: bool) returns string: + result[list[string], string] public = public_outcome(valid) + secret[result[list[string], string]] hidden = qualify[list[string]](public) + secret[result[list[string], string]] ready = join_hidden[list[string]](hidden) + return describe(view ready) +function once_report(valid: bool) returns string: + result[list[string], string] public = public_outcome(valid) + HiddenAlias pending = run qualify[list[string]](public) + HiddenAlias ready = join pending handle error: + return "outer:{error}" + return describe(view ready) +function twice_report(valid: bool) returns string: + result[list[string], string] public = public_outcome(valid) + secret[result[list[string], string]] pending = run run qualify[list[string]](public) + secret[result[list[string], string]] once = join_hidden[list[string]](pending) + secret[result[list[string], string]] ready = join_hidden[list[string]](once) + return describe(view ready) +function exact_report(valid: bool) returns string: + OutcomeAlias pending = run run public_outcome(valid) + OutcomeAlias once = join pending + OutcomeAlias ready = join once + return describe_public(view ready) +function nested_report(valid: bool) returns string: + result[list[string], string] public = public_outcome(valid) + secret[secret[result[list[string], string]]] hidden = public + secret[secret[result[list[string], string]]] pending = run run hidden + secret[secret[result[list[string], string]]] once = join pending handle error: + return "outer:{error}" + secret[secret[result[list[string], string]]] ready = join once handle error: + return "outer:{error}" + secret[result[list[string], string]] inner = declassify ready + return describe(view inner) +function refined_report(valid: bool) returns string: + result[list[string], string] public = public_outcome(valid) + RefinedOutcome checked = public handle error: + return "refinement:{error}" + RefinedOutcome pending = run run checked + RefinedOutcome once = join pending handle error: + return "outer:{error}" + RefinedOutcome ready = join once handle error: + return "outer:{error}" + result[list[string], string] base = coarsen ready + return describe_public(view base) +function scoped_report() returns string: + comptime type Bound = type.info[secret[result[list[string], string]]](): + result[list[string], string] public = public_outcome(true) + Bound pending = run run qualify[list[string]](public) + Bound once = join pending handle error: + return "outer:{error}" + Bound ready = join once handle error: + return "outer:{error}" + return describe(view ready) +function text_report(valid: bool) returns string: + mutable result[string, string] public = fail("text-error-λ🙂") + if valid: + public = ok("text-λ🙂") + secret[result[string, string]] pending = run run qualify[string](public) + secret[result[string, string]] once = join_hidden[string](pending) + secret[result[string, string]] ready = join_hidden[string](once) + result[string, string] exposed = declassify ready + string text = exposed handle error: + return "inner:{error}" + return "ok:{text}" +function ownership_report() returns string: + result[list[string], string] public = public_outcome(true) + secret[result[list[string], string]] pending = run qualify[list[string]](public) + secret[result[list[string], string]] first = join_hidden[list[string]](clone pending) + secret[result[list[string], string]] second = join_hidden[list[string]](clone pending) + result[list[string], string] exposed = declassify clone first + mutable list[string] independent = exposed handle error: + return error + independent = list.append[string](independent, "independent") + return "{list.length(view independent)}|{describe(view first)}|{describe(view second)}" +function baked_report() returns string: + string success = comptime twice_report(true) + string failure = comptime twice_report(false) + string nested = comptime nested_report(true) + return "{success}|{failure}|{nested}" +function matrix(valid: bool) returns string: + return "{ready_report(valid)}|{once_report(valid)}|{twice_report(valid)}|{exact_report(valid)}|{nested_report(valid)}|{refined_report(valid)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "success:{matrix(true)}\n") + Stdout.write(view stdout, "failure:{matrix(false)}\n") + Stdout.write(view stdout, "text:{text_report(true)}|{text_report(false)}\n") + Stdout.write(view stdout, "scoped:{scoped_report()}\n") + Stdout.write(view stdout, "ownership:{ownership_report()}\n") + Stdout.write(view stdout, "baked:{baked_report()}\n") + result[list[string], string] public = public_outcome(true) + secret[result[list[string], string]] pending = run run qualify[list[string]](public) + secret[result[list[string], string]] once = join_hidden[list[string]](pending) + result[list[string], string] partial = declassify clone once + trace partial + secret[result[list[string], string]] ready = join_hidden[list[string]](once) + Stdout.write(view stdout, "partial-then-ready:{describe(view ready)}\n") +verify qualified_result_joins: + assert matrix(true) == "ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂" + assert matrix(false) == "inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂" + assert text_report(true) == "ok:text-λ🙂" + assert text_report(false) == "inner:text-error-λ🙂" + assert scoped_report() == "ok:2:Ada-λ🙂" + assert ownership_report() == "3|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂" + assert baked_report() == "ok:2:Ada-λ🙂|inner:missing-λ🙂|ok:2:Ada-λ🙂" +property qualified_result_trials: + given valid: bool + mutable string expected = "inner:missing-λ🙂" + if valid: + expected = "ok:2:Ada-λ🙂" + assert ready_report(valid) == expected + assert once_report(valid) == expected + assert twice_report(valid) == expected + assert exact_report(valid) == expected + assert nested_report(valid) == expected + assert refined_report(valid) == expected + assert ownership_report() == "3|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂" +"#; + +#[test] +fn native_qualified_result_tasks_preserve_inner_results_and_pending_depth() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, JOIN_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("qualified join returns an outer result around the qualified inner result"); + assert_eq!( + expected.stdout, + concat!( + "success:ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂\n", + "failure:inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂|inner:missing-λ🙂\n", + "text:ok:text-λ🙂|inner:text-error-λ🙂\n", + "scoped:ok:2:Ada-λ🙂\n", + "ownership:3|ok:2:Ada-λ🙂|ok:2:Ada-λ🙂\n", + "baked:ok:2:Ada-λ🙂|inner:missing-λ🙂|ok:2:Ada-λ🙂\n", + "partial-then-ready:ok:2:Ada-λ🙂\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + ["trace partial: result[list[string], string] = pending(ok(list(Ada-λ🙂, kept)))"] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("qualified_result_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native qualified result tasks"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("qualified_result_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled qualified result verify suite"); + let property_binary = directory.path().join("qualified_result_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled qualified result property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const HANDLER_CONTROLS: &str = r#"namespace app +function public_failure(view stdout: Stdout) returns result[list[string], string]: + Stdout.write(view stdout, "operand\n") + return fail("inner-error") +function qualified_failure(view stdout: Stdout) returns secret[result[list[string], string]]: + result[list[string], string] public = public_failure(view stdout) + return public +function later_failure(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "handler\n") + list[string] owned = list("staged", "owner") + return list.remove_at[string](owned, -1) +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + list[string] retained = list("earlier", "owner") + Stdout.write(view stdout, "before\n") + secret[result[list[string], string]] pending = run qualified_failure(view stdout) + secret[result[list[string], string]] ready = join pending handle error: + Stdout.write(view stdout, "must not handle outer\n") + return nothing + result[list[string], string] public = declassify ready + list[string] values = public handle error: + Stdout.write(view stdout, "inner:{error}\n") + default later_failure(view stdout) + Stdout.write(view stdout, "must not run:{list.length(view values)}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") +"#; + +#[test] +fn native_qualified_result_join_keeps_inner_failure_for_its_handler_and_cleanup() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, HANDLER_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("only the inner handler runs and its later failure remains terminal"); + assert_eq!( + expected.message, + "runtime error: list.__remove_at: index -1 out of bounds" + ); + assert_eq!( + expected.output.stdout, + "before\noperand\ninner:inner-error\nhandler\n" + ); + assert!(expected.output.debug_events.is_empty(), "{expected:?}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("qualified_handler_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native qualified result inner handler"); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, format!("{}\n", expected.message).as_bytes()); + } +} + +const VIEW_AND_SCALAR_CONTROLS: &str = r#"namespace app +function ordinary() returns result[int8, int64]: + return ok(7) +function scalar_outcome(valid: bool) returns result[int8, int64]: + int8 number = 7 + int64 problem = 9 + if valid: + return ok(run run number) + return fail(run run problem) +function promote(candidate: result[int8, int64]) returns secret[result[int8, int64]]: + return candidate +function join_view(view candidate: secret[result[int8, int64]]) returns secret[result[int8, int64]]: + secret[result[int8, int64]] joined = join view candidate handle error: + result[int8, int64] failure = fail(-1) + return promote(failure) + trace candidate + return joined +function error_report(problem: int64) returns string: + int64 once = join problem handle error: + return "join-error:{error}" + int64 ready = join once handle error: + return "join-error:{error}" + return "fail:{ready}" +function scalar_report(candidate: result[int8, int64]) returns string: + int8 value = candidate handle error: + return error_report(error) + int8 once = join value handle error: + return "join-error:{error}" + int8 ready = join once handle error: + return "join-error:{error}" + return "ok:{ready}" +function qualified_report(valid: bool) returns string: + secret[result[int8, int64]] original = run run promote(scalar_outcome(valid)) + secret[result[int8, int64]] once = join original handle error: + return "outer:{error}" + secret[result[int8, int64]] ready = join once handle error: + return "outer:{error}" + result[int8, int64] exposed = declassify ready + return scalar_report(exposed) +function exact_report(valid: bool) returns string: + result[int8, int64] original = run run scalar_outcome(valid) + result[int8, int64] once = join original + result[int8, int64] ready = join once + return scalar_report(ready) +function ready_report(valid: bool) returns string: + secret[result[int8, int64]] original = promote(scalar_outcome(valid)) + secret[result[int8, int64]] ready = join original handle error: + return "outer:{error}" + result[int8, int64] exposed = declassify ready + return scalar_report(exposed) +function observe_error(view stdout: Stdout, problem: int64) returns nothing: + trace problem + int64 error_once = join problem handle error: + return nothing + trace error_once + int64 error_ready = join error_once handle error: + return nothing + trace error_ready + Stdout.write(view stdout, "payload-failure:{error_ready}\n") +function main(stdout: Stdout) returns nothing: + result[int8, int64] original = run run ordinary() + result[int8, int64] once = join view original + trace original + trace once + result[int8, int64] joined = join once + int8 explicit_value = joined handle error: + return nothing + Stdout.write(view stdout, "explicit:{explicit_value}\n") + trace original + secret[result[int8, int64]] hidden = run run promote(ordinary()) + secret[result[int8, int64]] hidden_once = join_view(view hidden) + secret[result[int8, int64]] hidden_ready = join_view(view hidden_once) + result[int8, int64] exposed = declassify hidden_ready + int8 borrowed_value = exposed handle error: + return nothing + Stdout.write(view stdout, "view-param:{borrowed_value}\n") + result[int8, int64] original_copy = declassify clone hidden + result[int8, int64] once_copy = declassify clone hidden_once + trace original_copy + trace once_copy + Stdout.write(view stdout, "qualified:{qualified_report(true)}|{qualified_report(false)}\n") + Stdout.write(view stdout, "exact:{exact_report(true)}|{exact_report(false)}\n") + Stdout.write(view stdout, "ready-qualified:{ready_report(true)}|{ready_report(false)}\n") + result[int8, int64] pending_payload = scalar_outcome(true) + int8 value = pending_payload handle error: + return nothing + trace value + int8 value_once = join value handle error: + return nothing + trace value_once + int8 value_ready = join value_once handle error: + return nothing + trace value_ready + Stdout.write(view stdout, "payload-success:{value_ready}\n") + result[int8, int64] failed_payload = scalar_outcome(false) + int8 unused = failed_payload handle error: + observe_error(view stdout, error) + default 0 +verify qualified_view_and_scalar_joins: + assert qualified_report(true) == "ok:7" + assert qualified_report(false) == "fail:9" + assert exact_report(true) == "ok:7" + assert exact_report(false) == "fail:9" + assert ready_report(true) == "ok:7" + assert ready_report(false) == "fail:9" +property qualified_scalar_join_trials: + given valid: bool + mutable string expected = "fail:9" + if valid: + expected = "ok:7" + assert qualified_report(valid) == expected + assert exact_report(valid) == expected + assert ready_report(valid) == expected +"#; + +#[test] +fn native_qualified_result_joins_preserve_views_and_scalar_payload_depth() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, VIEW_AND_SCALAR_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("explicit view joins preserve their owners and inner scalar metadata"); + let stdout = concat!( + "explicit:7\n", + "view-param:7\n", + "qualified:ok:7|fail:9\n", + "exact:ok:7|fail:9\n", + "ready-qualified:ok:7|fail:9\n", + "payload-success:7\n", + "payload-failure:9\n", + ); + let debug = [ + "trace original: result[int8, int64] = pending(pending(ok(7)))", + "trace once: result[int8, int64] = pending(ok(7))", + "trace original: result[int8, int64] = pending(pending(ok(7)))", + "trace candidate: secret[result[int8, int64]] = [redacted]", + "trace candidate: secret[result[int8, int64]] = [redacted]", + "trace original_copy: result[int8, int64] = pending(pending(ok(7)))", + "trace once_copy: result[int8, int64] = pending(ok(7))", + "trace value: int8 = pending(pending(7))", + "trace value_once: int8 = pending(7)", + "trace value_ready: int8 = 7", + "trace problem: int64 = pending(pending(9))", + "trace error_once: int64 = pending(9)", + "trace error_ready: int64 = 9", + ]; + assert_eq!(expected.stdout, stdout); + assert_eq!(debug_trace_lines(&expected.debug_events), debug); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("qualified_views_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native qualified result view and scalar controls"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("qualified_views_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled scalar join verify suite"); + let property_binary = directory.path().join("qualified_views_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled scalar join property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes()); + let stderr = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, stderr.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} diff --git a/crates/jett_driver/tests/native_conformance/refinement_calls.rs b/crates/jett_driver/tests/native_conformance/refinement_calls.rs new file mode 100644 index 00000000..90952ce0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/refinement_calls.rs @@ -0,0 +1,402 @@ +use super::*; + +const CALL_CONTROLS: &str = r#" +namespace app +function positive_check(value: int8) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int8 where positive_check(value) +function high_check(value: int8) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function nonempty_check(view values: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +function echo_scalar(view value: Positive) returns Positive: + return clone value +function echo_owned(value: Positive) returns Positive: + return value +function echo_text(view value: Text) returns Text: + return clone value +function echo_list(view value: NonEmpty) returns NonEmpty: + return clone value +function forward[T](view value: T) returns T: + return clone value +function shadow[Positive](view value: Positive) returns Positive: + return clone value +struct Reader: + version: int64 + function echo(view self: Reader, view value: NonEmpty) returns NonEmpty: + return clone value +function scoped(view value: NonEmpty) returns NonEmpty: + comptime type Bound = type.info[NonEmpty](): + function(view Bound) returns Bound callback = echo_list + return callback(view value) +function scalar_report() returns string: + int8 input = 7 + Positive source = input handle error: + return error + Positive direct = echo_scalar(view source) + function(view Positive) returns Positive callback = echo_scalar + Positive named = callback(view source) + Positive generic = forward[Positive](view source) + Positive piped = (clone source) into echo_owned + int8 first = coarsen direct + int8 second = coarsen named + int8 third = coarsen generic + int8 fourth = coarsen piped + int8 original = coarsen source + return "{first}:{second}:{third}:{fourth}:{original}" +function owned_report() returns string: + Text label = "Agent-λ🙂" handle error: + return error + NonEmpty values = list(7, 9) handle error: + return error + Text returned_text = echo_text(view label) + Text shadowed = shadow[Text](view label) + NonEmpty direct = echo_list(view values) + function(view NonEmpty) returns NonEmpty callback = echo_list + NonEmpty named = callback(view values) + NonEmpty generic = forward[NonEmpty](view values) + NonEmpty reflected = scoped(view values) + Reader reader = Reader(version: 1) + NonEmpty method = Reader.echo(view reader, view values) + function(view Reader, view NonEmpty) returns NonEmpty method_callback = Reader.echo + NonEmpty method_named = method_callback(view reader, view values) + mutable list[int64] independent = coarsen direct + independent = list.append[int64](independent, 11) + list[int64] original = coarsen clone values + list[int64] named_values = coarsen named + list[int64] generic_values = coarsen generic + list[int64] scoped_values = coarsen reflected + list[int64] method_values = coarsen method + list[int64] method_named_values = coarsen method_named + string first = coarsen returned_text + string second = coarsen shadowed + return "{first}:{second}:{list.length(view independent)}:{list.length(view original)}:{list.length(view named_values)}:{list.length(view generic_values)}:{list.length(view scoped_values)}:{list.length(view method_values)}:{list.length(view method_named_values)}" +function pending_report() returns string: + int8 input = 7 + Positive source = input handle error: + return error + Positive pending = run run clone source + Positive passed = echo_scalar(view pending) + Positive forwarded = forward[Positive](view passed) + trace forwarded + Positive once = join forwarded handle error: + return error + trace once + Positive joined = join once handle error: + return error + Positive returned = echo_scalar(view joined) + int8 ready = coarsen returned + int8 original = coarsen source + return "{ready}:{original}" +function new_child(value: int8) returns result[High, string]: + Positive ancestor = value handle error: + return fail(error) + High descendant = ancestor handle error: + return fail(error) + return ok(descendant) +function child_report(value: int8) returns string: + High ready = new_child(value) handle error: + return error + int8 number = coarsen ready + return "{number}" +function handled_return(view source: Positive) returns Positive: + optional[Positive] absent = none + return absent handle: + return clone source +function handled_default(view source: Positive, present: bool) returns Positive: + mutable optional[Positive] candidate = none + if present: + candidate = some(clone source) + return candidate handle: + default clone source +function handler_report() returns string: + int8 input = 7 + Positive source = input handle error: + return error + Positive returned = handled_return(view source) + Positive absent = handled_default(view source, false) + Positive present = handled_default(view source, true) + int8 first = coarsen returned + int8 second = coarsen absent + int8 third = coarsen present + return "{first}:{second}:{third}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "scalar:{scalar_report()}\n") + Stdout.write(view stdout, "owned:{owned_report()}\n") + Stdout.write(view stdout, "pending:{pending_report()}\n") + Stdout.write(view stdout, "child:{child_report(12)}|{child_report(7)}\n") + Stdout.write(view stdout, "handlers:{handler_report()}\n") + +"#; + +fn run_checked_call_case( + name: &str, + source_text: &str, + expected_stdout: &str, + expected_debug: &[&str], + compile_suites: bool, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!(expected.stdout, expected_stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.debug_events), + expected_debug, + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push((binary, release)); + } + let mut suites = Vec::new(); + if compile_suites { + let verify = directory.path().join(format!("{name}_verify.exe")); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join(format!("{name}_property.exe")); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + suites.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected_stdout.as_bytes(), "{name}"); + let debug = if release || expected_debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{name}: {actual:?}"); + } + for binary in suites { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_checked_refinement_calls_preserve_parameter_and_return_identity() { + run_checked_call_case( + "checked_refinement_calls", + CALL_CONTROLS, + concat!( + "scalar:7:7:7:7:7\n", + "owned:Agent-λ🙂:Agent-λ🙂:3:2:2:2:2:2:2\n", + "pending:7:7\n", + "child:12|refinement type constraint failed for 'app.High'\n", + "handlers:7:7:7\n", + ), + &[ + "trace marker: string = positive", + "trace marker: string = text", + "trace marker: string = nonempty", + "trace marker: string = positive", + "trace forwarded: app.Positive = pending(pending(7))", + "trace once: app.Positive = pending(7)", + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = positive", + ], + false, + ); +} + +#[test] +fn native_checked_refinement_call_proofs_follow_named_argument_order() { + run_checked_call_case( + "checked_refinement_argument_order", + ORDER_CONTROLS, + "before\nlabel\nnumber\nchosen:7:72657461696e6564\n", + &[ + "trace marker: string = text", + "trace marker: string = positive", + ], + false, + ); +} + +const PURE_CONTROLS: &str = r#"namespace app +type Positive = int64 where value > 0 +type Text = string where string.char_count(value) > 0 +type NonEmpty = list[int64] where list.length(view value) > 0 +function echo_num(view value: Positive) returns Positive: + return clone value +function echo_text(view value: Text) returns Text: + return clone value +function echo_list(view value: NonEmpty) returns NonEmpty: + return clone value +function forward[T](view value: T) returns T: + return clone value +function shadow[Positive](view value: Positive) returns Positive: + return clone value +function scoped(view value: NonEmpty) returns NonEmpty: + comptime type Bound = type.info[NonEmpty](): + function(view Bound) returns Bound callback = echo_list + return callback(view value) +function report(number: int64) returns string: + Positive established = number handle error: + return error + Text label = "Agent" handle error: + return error + NonEmpty values = list(7, 9) handle error: + return error + function(view Positive) returns Positive callback = echo_num + Positive returned = callback(view established) + Positive generic = forward[Positive](view returned) + Text text = shadow[Text](view label) + NonEmpty copied = scoped(view values) + mutable list[int64] independent = coarsen copied + independent = list.append[int64](independent, 11) + list[int64] original = coarsen clone values + int64 value = coarsen generic + string public_text = coarsen text + return "{value}:{public_text}:{list.length(view independent)}:{list.length(view original)}" +function baked_callback_report() returns string: + Positive established = 7 handle error: + return error + function(view Positive) returns Positive callback = comptime echo_num + Positive ready = callback(view established) + int64 value = coarsen ready + return "{value}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(7) + Stdout.write(view stdout, "pure:{report(7)}|{baked}|{baked_callback_report()}\n") +verify checked_refinement_call_values: + assert report(7) == "7:Agent:3:2" + assert report(0) == "refinement type constraint failed for 'app.Positive'" + assert baked_callback_report() == "7" +property checked_refinement_call_trials: + given high: bool + mutable int64 number = 7 + if high: + number = 127 + assert report(number) == "{number}:Agent:3:2" + assert report(0) == "refinement type constraint failed for 'app.Positive'" + assert baked_callback_report() == "7" +"#; + +#[test] +fn native_checked_refinement_calls_support_closed_baking_and_native_suites() { + run_checked_call_case( + "checked_refinement_pure_calls", + PURE_CONTROLS, + "pure:7:Agent:3:2|7:Agent:3:2|7\n", + &[], + true, + ); +} + +const BUILDER_CONTROLS: &str = r#"namespace app +function valid(value: int64) returns bool: + string marker = "positive" + trace marker + trace value + return value > 0 +type Positive = int64 where valid(value) +struct Item: + value: Positive +function main(stdout: Stdout) returns nothing: + Positive established = 7 handle error: + return nothing + mutable TypeConstruction checked = type.construct_start[Item]() + for field in type.fields[Item](): + checked = type.construct_put[Item, Positive](checked, view field, clone established) handle error: + return nothing + Item ready = type.construct_finish[Item](checked) handle error: + return nothing + int64 value = coarsen ready.value + Stdout.write(view stdout, "ready:{value}\n") + mutable TypeConstruction unvalidated = type.construct_start[Item]() + for field in type.fields[Item](): + unvalidated = type.construct_put[Item, int64](unvalidated, view field, 0) handle error: + return nothing + Item rejected = type.construct_finish[Item](unvalidated) handle error: + Stdout.write(view stdout, "rejected:{error}\n") + return nothing + Stdout.write(view stdout, "must not run\n") +"#; + +#[test] +fn native_checked_call_proofs_keep_reflected_builder_finish_validation_forced() { + run_checked_call_case( + "checked_calls_forced_builder_control", + BUILDER_CONTROLS, + "ready:7\nrejected:refinement type constraint failed for 'app.Positive'\n", + &[ + "trace marker: string = positive", + "trace value: int64 = 7", + "trace marker: string = positive", + "trace value: int64 = 7", + "trace marker: string = positive", + "trace value: int64 = 0", + ], + false, + ); +} + +// Deliberately outside this draft: new raw/ancestor values returned under a +// declared refinement are checker-admitted, but native return lowering lacks +// predicates. Ignored reference probes pin their terminal errors separately. +// Inline closure return annotations and callback lifetime policy are unchanged. + +const ORDER_CONTROLS: &str = r#" +namespace app +function positive_check(value: int8) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int8 where positive_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function number(view stdout: Stdout) returns Positive: + Stdout.write(view stdout, "number\n") + int8 input = 7 + Positive ready = input handle error: + return number(view stdout) + return ready +function label(view stdout: Stdout) returns Text: + Stdout.write(view stdout, "label\n") + Text ready = "Agent" handle error: + return label(view stdout) + return ready +function pick(first: Positive, label: Text) returns Positive: + return first +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + Stdout.write(view stdout, "before\n") + Positive chosen = pick(label: label(view stdout), first: number(view stdout)) + int8 value = coarsen chosen + Stdout.write(view stdout, "chosen:{value}:{bytes.to_hex(view earlier)}\n") + +"#; diff --git a/crates/jett_driver/tests/native_conformance/refinement_returns.rs b/crates/jett_driver/tests/native_conformance/refinement_returns.rs new file mode 100644 index 00000000..e7a7ff43 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/refinement_returns.rs @@ -0,0 +1,526 @@ +use super::*; + +const RETURN_CONTROLS: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function long_check(value: string) returns bool: + string marker = "long" + trace marker + return string.char_count(value) >= 3 +type Long = Text where long_check(value) +function nonempty_check(view values: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +function pair_check(view values: list[int64]) returns bool: + string marker = "pair" + trace marker + return list.length(view values) >= 2 +type Pair = NonEmpty where pair_check(view value) +function raw_scalar(value: int64) returns Positive: + return value +function elevate(view value: Positive) returns High: + return clone value +function echo_high(view value: High) returns High: + return clone value +function identity[T](view value: T) returns T: + return clone value +function create[T](value: int64) returns T: + return value +function raw_text(value: string) returns Text: + return value +function upgrade_text(view value: Text) returns Long: + return clone value +function raw_values(values: list[int64]) returns NonEmpty: + return values +function upgrade_values(view values: NonEmpty) returns Pair: + return clone values +struct Holder: + text: Long + values: Pair +function read_text(view holder: Holder) returns Long: + return holder.text +function read_values(view holder: Holder) returns Pair: + return holder.values +struct Maker: + version: int64 + function number(view self: Maker, value: int64) returns Positive: + return value +function shadow[Positive](view unused: Positive) returns app.Positive: + return 7 +function reflected(value: int64) returns Positive: + comptime type Bound = type.info[Positive](): + return value +function scalar_report() returns string: + Positive source = raw_scalar(12) + High stronger = elevate(view source) + High exact = echo_high(view stronger) + High generic = identity[High](view stronger) + function(view High) returns High callback = function(view value: High) returns High: + return clone value + High inline = callback(view stronger) + int64 first = coarsen exact + int64 second = coarsen generic + int64 third = coarsen inline + int64 original = coarsen source + return "{first}:{second}:{third}:{original}" +function pending_report() returns string: + Positive source = raw_scalar(12) + High stronger = elevate(view source) + High pending = run run clone stronger + High copied = echo_high(view pending) + trace copied + High once = join copied handle error: + return error + trace once + High ready = join once handle error: + return error + int64 value = coarsen ready + int64 original = coarsen stronger + return "{value}:{original}" +function owned_report() returns string: + string label = "Agent-λ🙂" + Text source_text = raw_text(label) + Long longer = upgrade_text(view source_text) + list[int64] owner = list(7, 9) + NonEmpty source_values = raw_values(clone owner) + Pair pair = upgrade_values(view source_values) + Holder holder = Holder(text: longer, values: pair) handle error: + return error + Long copied_text = read_text(view holder) + Pair copied_values = read_values(view holder) + mutable list[int64] independent = coarsen copied_values + independent = list.append[int64](independent, 11) + list[int64] refined_original = coarsen clone source_values + list[int64] field_original = coarsen holder.values + string public_text = coarsen copied_text + string original_text = coarsen source_text + return "{public_text}:{original_text}:{list.length(view independent)}:{list.length(view owner)}:{list.length(view refined_original)}:{list.length(view field_original)}" +function contexts_report() returns string: + function(int64) returns Positive callback = raw_scalar + Positive named = callback(7) + Maker maker = Maker(version: 1) + Positive method = Maker.number(view maker, 9) + Positive scoped = reflected(11) + Text label = "Agent" handle error: + return error + Positive selected = shadow[Text](view label) + Positive generated = create[Positive](13) + High generated_high = create[High](15) + int64 first = coarsen named + int64 second = coarsen method + int64 third = coarsen scoped + int64 fourth = coarsen selected + int64 fifth = coarsen generated + int64 sixth = coarsen generated_high + string text = coarsen label + return "{first}:{second}:{third}:{fourth}:{fifth}:{sixth}:{text}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "scalar:{scalar_report()}\n") + Stdout.write(view stdout, "pending:{pending_report()}\n") + Stdout.write(view stdout, "owned:{owned_report()}\n") + Stdout.write(view stdout, "contexts:{contexts_report()}\n") +"#; + +const RETURN_FAILURE: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function risky_check(value: int64) returns bool: + string marker = "risky" + trace marker + list[string] owned = list("predicate-owned") + list[string] removed = list.remove_at[string](owned, -1) + return value > 0 +type Risky = int64 where risky_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function nonempty_check(view values: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +function pair_check(view values: list[int64]) returns bool: + string marker = "pair" + trace marker + return list.length(view values) >= 2 +type Pair = NonEmpty where pair_check(view value) +function broken_value() returns int64: + list[string] owned = list("operand-owned") + list[string] removed = list.remove_at[string](owned, -1) + return 7 +function operand(view stdout: Stdout, label: string, value: int64) returns int64: + Stdout.write(view stdout, "{label}\n") + return value +function later(view stdout: Stdout) returns optional[string]: + Stdout.write(view stdout, "later\n") + return none +function fallback(view stdout: Stdout) returns string: + Stdout.write(view stdout, "handler\n") + return "fallback" +function attempt(view stdout: Stdout__PARAMETER__) returns __TARGET__: + list[string] owned = list("return-owned", "payload") + Stdout.write(view stdout, "operand\n") +__BODY__ +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") +__SETUP__ + Stdout.write(view stdout, "before\n") + __TARGET__ returned = attempt(view stdout__ARGUMENT__) + string message = "{later(view stdout) handle: default fallback(view stdout)}" + Stdout.write(view stdout, "after:{message}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") +"#; + +fn failure_source(target: &str, body: &str, ancestor: bool) -> String { + let source_type = if target == "Pair" { + "NonEmpty" + } else { + "Positive" + }; + let setup = if target == "Pair" { + " NonEmpty ancestor = list(7) handle error:\n return nothing" + } else { + " Positive ancestor = 7 handle error:\n return nothing" + }; + let parameter = format!(", view source: {source_type}"); + RETURN_FAILURE + .replace("__TARGET__", target) + .replace("__BODY__", body) + .replace("__PARAMETER__", if ancestor { ¶meter } else { "" }) + .replace( + "__ARGUMENT__", + if ancestor { ", view ancestor" } else { "" }, + ) + .replace("__SETUP__", if ancestor { setup } else { "" }) +} + +fn run_return_case( + name: &str, + text: &str, + stdout: &str, + message: Option<&str>, + debug: &[&str], + suites: bool, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, text).unwrap(); + let outcome = jett_driver::run_file_capture_outcome(&source); + let runtime_debug_events = match (outcome, message) { + (Ok(expected), None) => { + assert_eq!(expected.stdout, stdout, "{name}"); + assert_eq!(debug_trace_lines(&expected.debug_events), debug, "{name}"); + expected.debug_events + } + (Err(expected), Some(message)) => { + assert_eq!(expected.message, message, "{name}"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + debug, + "{name}" + ); + expected.output.debug_events + } + (actual, message) => panic!("{name}: outcome {actual:?}, error {message:?}"), + }; + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("return_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + } + let mut suite_binaries = Vec::new(); + if suites { + let verify = directory.path().join("return_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join("return_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + suite_binaries.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + let code = if message.is_some() { 71 } else { 0 }; + assert_eq!(actual.status.code(), Some(code), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&runtime_debug_events) + }; + if let Some(message) = message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + for binary in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_refined_returns_validate_new_suffixes_and_preserve_exact_owned_values() { + run_return_case( + "refined_return_values", + RETURN_CONTROLS, + concat!( + "scalar:12:12:12:12\n", + "pending:12:12\n", + "owned:Agent-λ🙂:Agent-λ🙂:3:2:2:2\n", + "contexts:7:9:11:7:13:15:Agent\n", + ), + None, + &[ + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = positive", + "trace marker: string = high", + "trace copied: app.High = pending(pending(12))", + "trace once: app.High = pending(12)", + "trace marker: string = text", + "trace marker: string = long", + "trace marker: string = nonempty", + "trace marker: string = pair", + "trace marker: string = positive", + "trace marker: string = positive", + "trace marker: string = positive", + "trace marker: string = text", + "trace marker: string = positive", + "trace marker: string = positive", + "trace marker: string = positive", + "trace marker: string = high", + ], + false, + ); +} + +#[test] +fn native_refined_return_failures_preserve_first_error_depth_order_and_cleanup() { + let cases: &[(&str, &str, &str, bool, &str, &str, &[&str])] = &[ + ( + "base_false", + "Positive", + " return operand(view stdout, \"left\", -1) + operand(view stdout, \"right\", 0)", + false, + "before\noperand\nleft\nright\n", + "runtime error: refinement type constraint failed for 'app.Positive'", + &["trace marker: string = positive"], + ), + ( + "ancestor_false", + "High", + " return clone source", + true, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.High'", + &[ + "trace marker: string = positive", + "trace marker: string = high", + ], + ), + ( + "generic_ancestor_false", + "High", + " return clone source", + true, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.High'", + &[ + "trace marker: string = positive", + "trace marker: string = high", + ], + ), + ( + "reflected_ancestor_false", + "High", + " comptime type Bound = type.info[High]():\n return clone source", + true, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.High'", + &[ + "trace marker: string = positive", + "trace marker: string = high", + ], + ), + ( + "predicate_error", + "Risky", + " return 7", + false, + "before\noperand\n", + "runtime error: error evaluating refinement constraint for 'app.Risky': list.__remove_at: index -1 out of bounds", + &["trace marker: string = risky"], + ), + ( + "pending_one", + "Positive", + " return run 7", + false, + "before\noperand\n", + "runtime error: error evaluating refinement constraint for 'app.Positive': unsupported binary operation: pending(7) Gt 0", + &["trace marker: string = positive"], + ), + ( + "pending_two", + "Positive", + " return run run 7", + false, + "before\noperand\n", + "runtime error: error evaluating refinement constraint for 'app.Positive': unsupported binary operation: pending(pending(7)) Gt 0", + &["trace marker: string = positive"], + ), + ( + "empty_string", + "Text", + " return \"\"", + false, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.Text'", + &["trace marker: string = text"], + ), + ( + "empty_list", + "NonEmpty", + " return list.new[int64]()", + false, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.NonEmpty'", + &["trace marker: string = nonempty"], + ), + ( + "owned_ancestor_false", + "Pair", + " return clone source", + true, + "before\noperand\n", + "runtime error: refinement type constraint failed for 'app.Pair'", + &[ + "trace marker: string = nonempty", + "trace marker: string = pair", + ], + ), + ( + "operand_error", + "Positive", + " return broken_value()", + false, + "before\noperand\n", + "runtime error: list.__remove_at: index -1 out of bounds", + &[], + ), + ]; + for (name, target, body, ancestor, stdout, message, debug) in cases { + let mut source = failure_source(target, body, *ancestor); + if name.starts_with("generic_") { + source = source + .replace("function attempt(", "function attempt[T](") + .replace(&format!(") returns {target}:"), ") returns T:") + .replace("= attempt(", &format!("= attempt[{target}](")); + } + run_return_case(name, &source, stdout, Some(message), debug, false); + } +} + +const PURE_RETURN_CONTROLS: &str = r#"namespace app +type Positive = int64 where value > 0 +type High = Positive where value > 10 +type Text = string where string.char_count(value) > 0 +type NonEmpty = list[int64] where list.length(view value) > 0 +function scalar(value: int64) returns Positive: + return value +function elevate(view value: Positive) returns High: + return clone value +function text(value: string) returns Text: + return value +function values(owned: list[int64]) returns NonEmpty: + return owned +function identity[T](view value: T) returns T: + return clone value +function create[T](value: int64) returns T: + return value +function reflected(value: int64) returns Positive: + comptime type Bound = type.info[Positive](): + return value +function report(number: int64) returns string: + Positive initial = scalar(number) + High stronger = elevate(view initial) + High copied = identity[High](view stronger) + High generated = create[High](number) + Text label = text("Agent") + NonEmpty nonempty = values(list(7, 9)) + Positive scoped = reflected(number) + int64 ready = coarsen copied + int64 generic = coarsen generated + int64 observed = coarsen scoped + string public_text = coarsen label + list[int64] public_values = coarsen nonempty + return "{ready}:{generic}:{observed}:{public_text}:{list.length(view public_values)}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(12) + function(int64) returns Positive callback = comptime scalar + Positive returned = callback(127) + int64 ready = coarsen returned + Stdout.write(view stdout, "pure:{report(12)}|{baked}|{ready}\n") +verify refined_return_values: + assert report(12) == "12:12:12:Agent:2" + assert report(127) == "127:127:127:Agent:2" +property refined_return_trials: + given high: bool + mutable int64 number = 12 + if high: + number = 127 + assert report(number) == "{number}:{number}:{number}:Agent:2" +"#; + +#[test] +fn native_refined_returns_support_closed_comptime_and_native_suites() { + run_return_case( + "refined_return_pure_values", + PURE_RETURN_CONTROLS, + "pure:12:12:12:Agent:2|12:12:12:Agent:2|127\n", + None, + &[], + true, + ); +} + +// Inline descriptors do not retain or validate their declared result annotation +// in the current interpreter. The success control uses only an already-checked +// exact refinement; invalid inline returns are a separate residual, not a parity +// claim. Public/raw interpreter APIs continue to force refinement validation. diff --git a/crates/jett_driver/tests/native_conformance/refinement_reuse.rs b/crates/jett_driver/tests/native_conformance/refinement_reuse.rs new file mode 100644 index 00000000..ab738b94 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/refinement_reuse.rs @@ -0,0 +1,457 @@ +use super::*; + +const REUSE_CONTROLS: &str = r#"namespace app +function positive_check(value: int8) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int8 where positive_check(value) +function high_check(value: int8) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function even_check(value: int8) returns bool: + string marker = "even" + trace marker + return value modulo 2 == 0 +type Even = High where even_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function long_check(value: string) returns bool: + string marker = "long" + trace marker + return string.char_count(value) >= 3 +type Long = Text where long_check(value) +function nonempty_check(view values: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +function pair_check(view values: list[int64]) returns bool: + string marker = "pair" + trace marker + return list.length(view values) >= 2 +type Pair = NonEmpty where pair_check(view value) +struct Item: + value: Positive +struct HighItem: + value: High +struct EvenItem: + value: Even +struct Exact: + values: NonEmpty + text: Text +struct Strong: + values: Pair + text: Long +function exact_numeric() returns string: + int8 input = 7 + Positive established = input handle error: + return error + Positive alias = clone established + Item ready = Item(value: clone alias) handle error: + return error + Positive copied = ready.value + int8 number = coarsen copied + return "{number}" +function owned_fields() returns string: + NonEmpty values = list(7, 9) handle error: + return error + Text text = "Agent" handle error: + return error + NonEmpty alias_values = clone values + Text alias_text = clone text + Exact exact = Exact(values: alias_values, text: alias_text) handle error: + return error + NonEmpty copied_values = exact.values + Text copied_text = exact.text + Strong strong = Strong(text: text, values: values) handle error: + return error + mutable list[int64] independent = coarsen clone copied_values + independent = list.append[int64](independent, 11) + list[int64] original = coarsen copied_values + list[int64] stronger = coarsen strong.values + string label = coarsen copied_text + return "{list.length(view independent)}:{list.length(view original)}:{list.length(view stronger)}:{label}" +function local_ancestor() returns string: + int8 input = 12 + Positive established = input handle error: + return error + Even leaf = clone established handle error: + return error + High middle = coarsen clone leaf + EvenItem ready = EvenItem(value: middle) handle error: + return error + Even copied = ready.value + int8 number = coarsen copied + return "{number}" +function field_ancestor() returns string: + int8 input = 12 + Positive established = input handle error: + return error + EvenItem ready = EvenItem(value: established) handle error: + return error + int8 number = coarsen ready.value + return "{number}" +function rejected_child() returns string: + int8 input = 7 + Positive established = input handle error: + return error + HighItem rejected = HighItem(value: established) handle error: + return error + return "must not succeed" +function pending_established() returns string: + int8 input = 7 + Positive established = input handle error: + return error + Positive pending = run run established + Item ready = Item(value: pending) handle error: + return error + Positive child = ready.value + trace child + Positive once = join child handle error: + return error + trace once + Positive joined = join once handle error: + return error + int8 number = coarsen joined + return "{number}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "exact:{exact_numeric()}\n") + Stdout.write(view stdout, "owned:{owned_fields()}\n") + Stdout.write(view stdout, "local-ancestor:{local_ancestor()}\n") + Stdout.write(view stdout, "field-ancestor:{field_ancestor()}\n") + Stdout.write(view stdout, "rejected:{rejected_child()}\n") + Stdout.write(view stdout, "pending:{pending_established()}\n") +"#; + +fn execute_reuse_case( + name: &str, + source_text: &str, + stdout: &str, + debug: &[&str], + compile_suites: bool, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, source_text).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!(expected.stdout, stdout, "{name}"); + assert_eq!(debug_trace_lines(&expected.debug_events), debug, "{name}"); + let mut programs = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + programs.push((binary, release)); + } + let mut suites = Vec::new(); + if compile_suites { + let verify = directory.path().join(format!("{name}_verify.exe")); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join(format!("{name}_property.exe")); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + suites.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in programs { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + for binary in suites { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_direct_refinement_reuse_skips_established_local_and_field_predicates() { + execute_reuse_case( + "direct_refinement_reuse", + REUSE_CONTROLS, + "exact:7\nowned:3:2:2:Agent\nlocal-ancestor:12\nfield-ancestor:12\nrejected:refinement type constraint failed for 'app.High'\npending:7\n", + &[ + "trace marker: string = positive", + "trace marker: string = nonempty", + "trace marker: string = text", + "trace marker: string = long", + "trace marker: string = pair", + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = even", + "trace marker: string = even", + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = even", + "trace marker: string = positive", + "trace marker: string = high", + "trace marker: string = positive", + "trace child: app.Positive = pending(pending(7))", + "trace once: app.Positive = pending(7)", + ], + false, + ); +} + +const ORDER_DECLARATIONS: &str = r#"namespace app +function positive_check(value: int8) returns bool: + string marker = "positive" + trace marker + trace value + return value > 0 +type Positive = int8 where positive_check(value) +function high_check(value: int8) returns bool: + string marker = "high" + trace marker + trace value + return value > 10 +type High = Positive where high_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +struct Mixed: + first: High + exact: Text + last: High + payload: list[string] +function input(view stdout: Stdout) returns int8: + Stdout.write(view stdout, "input\n") + return 12 +function payload(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "payload\n") + return list("staged", "owner") +function main(stdout: Stdout) returns nothing: + int8 first_input = __FIRST__ + Positive first = first_input handle error: + return nothing + Text label = "Agent" handle error: + return nothing + bytes earlier = bytes.from_string("retained") + Stdout.write(view stdout, "before\n") + Mixed ready = Mixed(last: input(view stdout), payload: payload(view stdout), first: first, exact: label) handle error: + Stdout.write(view stdout, "outcome:{error}\n") + Stdout.write(view stdout, "after:{bytes.to_hex(view earlier)}\n") + return nothing + int8 first_value = coarsen ready.first + int8 last_value = coarsen ready.last + Stdout.write(view stdout, "ready:{first_value}:{last_value}:{list.length(view ready.payload)}\n") + Stdout.write(view stdout, "after:{bytes.to_hex(view earlier)}\n") +"#; + +const PENDING_NEW: &str = r#"namespace app +function positive_check(value: int8) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int8 where positive_check(value) +function high_check(value: int8) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +struct Item: + value: High +function main(stdout: Stdout) returns nothing: + int8 input = 12 + Positive established = input handle error: + return nothing + Positive pending = run established + Item ready = Item(value: pending) handle error: + Stdout.write(view stdout, "field-error:{error}\n") + return nothing + Stdout.write(view stdout, "must not run\n") +"#; + +#[test] +fn native_direct_refinement_reuse_keeps_suffix_order_and_new_pending_rejection() { + for (name, first, outcome) in [ + ("reversed_ready", "12", "ready:12:12:2"), + ( + "reversed_rejected", + "7", + "outcome:refinement type constraint failed for 'app.High'", + ), + ] { + let source = ORDER_DECLARATIONS.replace("__FIRST__", first); + let stdout = format!("before\ninput\npayload\n{outcome}\nafter:72657461696e6564\n"); + let first_trace = format!("trace value: int8 = {first}"); + execute_reuse_case( + name, + &source, + &stdout, + &[ + "trace marker: string = positive", + &first_trace, + "trace marker: string = text", + "trace marker: string = positive", + "trace value: int8 = 12", + "trace marker: string = high", + "trace value: int8 = 12", + "trace marker: string = high", + &first_trace, + ], + false, + ); + } + // Pending data entering a NEW refinement is not an established proof. + execute_reuse_case( + "pending_new_descendant", + PENDING_NEW, + "field-error:error evaluating refinement constraint for 'app.High': unsupported binary operation: pending(12) Gt 10\n", + &[ + "trace marker: string = positive", + "trace marker: string = high", + ], + false, + ); +} + +const PURE_CONTROLS: &str = r#"namespace app +type Positive = int64 where value > 0 +type High = Positive where value > 10 +type NonEmpty = list[int64] where list.length(view value) > 0 +struct Item: + number: Positive + values: NonEmpty +struct Box[T]: + number: Positive + payload: T +function scalar_copy(view source: Positive) returns int64: + return coarsen clone source +function report(number: int64) returns string: + Positive established = number handle error: + return error + Positive alias = clone established + NonEmpty values = list(7, 9) handle error: + return error + NonEmpty copied = clone values + Item ready = Item(number: alias, values: copied) handle error: + return error + Positive field = ready.number + NonEmpty field_values = ready.values + list[int64] public = coarsen field_values + return "{scalar_copy(view field)}:{list.length(view public)}" +function generic_report[T](number: int64, payload: T) returns string: + Positive established = number handle error: + return error + Box[T] ready = Box[T](number: established, payload: payload) handle error: + return error + int64 value = coarsen ready.number + return "{value}" +function builder_report(number: int64) returns string: + mutable TypeConstruction builder = type.construct_start[Box[string]]() + for field in type.fields[Box[string]](): + if field.name == "number": + builder = type.construct_put[Box[string], int64](builder, view field, number) handle error: + return error + if field.name == "payload": + builder = type.construct_put[Box[string], string](builder, view field, "kept") handle error: + return error + Box[string] ready = type.construct_finish[Box[string]](builder) handle error: + return error + int64 value = coarsen ready.number + return "{value}:{ready.payload}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(12) + Stdout.write(view stdout, "pure:{report(12)}|{generic_report[list[string]](12, list("kept"))}|{baked}\n") + Stdout.write(view stdout, "builder:{builder_report(7)}|{builder_report(0)}\n") +verify established_refinement_reuse: + assert report(12) == "12:2" + assert generic_report[string](12, "kept") == "12" + assert builder_report(7) == "7:kept" + assert builder_report(0) == "refinement type constraint failed for 'app.Positive'" +property established_refinement_trials: + given high: bool + mutable int64 number = 12 + if high: + number = 127 + assert report(number) == "{number}:2" + assert generic_report[list[string]](number, list("kept")) == "{number}" + assert builder_report(0) == "refinement type constraint failed for 'app.Positive'" +"#; + +#[test] +fn native_direct_refinement_reuse_supports_pure_baking_and_suites_without_changing_builders() { + execute_reuse_case( + "pure_refinement_reuse", + PURE_CONTROLS, + "pure:12:2|12|12:2\nbuilder:7:kept|refinement type constraint failed for 'app.Positive'\n", + &[], + true, + ); +} + +const GENERIC_CONSTRUCTOR_CONTROLS: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Box[T]: + value: T +function make[T](view ignored: T) returns Box[bool]: + return Box[bool](value: true) +function refined_report(number: int64) returns string: + Positive established = number handle error: + return error + result[Box[Positive], string] candidate = Box[Positive](value: established) + Box[Positive] ready = candidate handle error: + return error + Positive copied = ready.value + int64 observed = coarsen copied + return "refined:{observed}" +function plain_report() returns string: + Positive established = 7 handle error: + return error + Box[bool] ready = make[Positive](view established) + return "plain:{ready.value}" +function main(stdout: Stdout) returns nothing: + string baked = comptime refined_report(7) + Stdout.write(view stdout, "generic:{refined_report(7)}|{plain_report()}|{baked}\n") +verify concrete_constructor_type_arguments: + assert refined_report(7) == "refined:7" + assert plain_report() == "plain:true" +property concrete_constructor_trials: + given high: bool + mutable int64 number = 7 + if high: + number = 127 + assert refined_report(number) == "refined:{number}" + assert plain_report() == "plain:true" +"#; + +#[test] +fn native_generic_constructor_refinement_shape_uses_the_selected_type_arguments() { + // Pure predicates isolate constructor type selection from the separately + // scoped reference parameter-validation event behavior. + execute_reuse_case( + "generic_constructor_refinement_shape", + GENERIC_CONSTRUCTOR_CONTROLS, + "generic:refined:7|plain:true|refined:7\n", + &[], + true, + ); +} + +// Scope note: traced refined parameters/returns and builder finish are excluded +// from direct reuse oracles. The unchanged pure parameter/builder controls above +// do not claim parity for their preexisting extra reference predicate events. +// Sibling/cross-namespace source comparisons are not manufactured here: checked +// nominal admission must be established before adding any such linked control. diff --git a/crates/jett_driver/tests/native_conformance/refinement_sum.rs b/crates/jett_driver/tests/native_conformance/refinement_sum.rs new file mode 100644 index 00000000..dce49883 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/refinement_sum.rs @@ -0,0 +1,80 @@ +use super::*; + +const EMPTY_REFINED_SUM: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function later(view stdout: Stdout) returns optional[string]: + Stdout.write(view stdout, "later\n") + return none +function fallback(view stdout: Stdout) returns string: + Stdout.write(view stdout, "handler\n") + return "fallback" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + list[Positive] values = list() + Stdout.write(view stdout, "before\n") + Positive total = list.sum[Positive](view values) + int64 public_total = coarsen total + string message = "{later(view stdout) handle: default fallback(view stdout)}" + Stdout.write(view stdout, "after:{public_total}:{message}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") +"#; + +// Keep the original refined-sum program: the selected numeric domain rejects +// it before its predicate, entry effects, or native publication can run. +#[test] +fn refined_sum_domain_rejects_before_reference_execution_and_native_publication() { + use jett_driver::native::{NativeBuildError, build_host_executable_with_options}; + use jett_driver::{BuildOptions, build_file_with_options}; + + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, EMPTY_REFINED_SUM).unwrap(); + let failure = jett_driver::run_file_capture_outcome(&source) + .expect_err("refined sums are rejected during checking"); + assert!(failure.message.contains("E0300"), "{}", failure.message); + assert!(failure.message.contains("numeric primitive for list.sum")); + assert!(failure.output.stdout.is_empty()); + assert!(failure.output.debug_events.is_empty()); + + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing native publication"; + fs::write(&output, sentinel).unwrap(); + let missing_launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + assert!(!missing_launcher.archive_path.exists()); + for release in [false, true] { + let options = BuildOptions { release }; + let checked = build_file_with_options(&source, options); + assert!(checked.has_errors, "refined sum must fail checking"); + assert!( + checked.diagnostics.iter().any(|error| { + error.code.code() == 300 && error.message.contains("numeric primitive for list.sum") + }), + "{:?}", + checked.diagnostics + ); + assert!(checked.debug_observations.is_empty()); + let error = + build_host_executable_with_options(&source, &missing_launcher, &output, options) + .expect_err( + "native refined summation must fail before archive lookup and publication", + ); + assert!(error.debug_observations().is_empty()); + let NativeBuildError::Lowering { source, .. } = error else { + panic!("expected frontend sum-domain rejection: {error:?}"); + }; + let built = source.build_result().expect("frontend type diagnostic"); + assert!(built.diagnostics.iter().any(|error| { + error.code.code() == 300 && error.message.contains("numeric primitive for list.sum") + })); + assert_eq!(fs::read(&output).unwrap(), sentinel); + assert!(!missing_launcher.archive_path.exists()); + } +} diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett new file mode 100644 index 00000000..139d0dfd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/01_original_wider.jett @@ -0,0 +1,13 @@ +namespace app +type Positive = int64 where value > 0 +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + Positive value = observed() + println(coarsen value) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett new file mode 100644 index 00000000..71d19b65 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/02_original_exact.jett @@ -0,0 +1,12 @@ +namespace app +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns int64 observed = type.field_value[CallbackHolder, function() returns int64](view holder, view field) + int64 value = observed() + println(value) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett new file mode 100644 index 00000000..42ff28dd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/03_read_without_invocation.jett @@ -0,0 +1,29 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett new file mode 100644 index 00000000..a6750ba2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/04_named_twice_source_unchanged.jett @@ -0,0 +1,32 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive first = observed() + Positive second = observed() + int64 original = holder.callback() + Stdout.write(view stdout, "{coarsen first}:{coarsen second}:{original}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett new file mode 100644 index 00000000..bba444e2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/05_captured_twice.jett @@ -0,0 +1,36 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + int64 offset = 7 + function() returns int64 captured = function() returns int64: + string marker = "source" + trace marker + return offset + Holder holder = Holder(callback: captured) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive first = observed() + Positive second = observed() + Stdout.write(view stdout, "{coarsen first}:{coarsen second}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett new file mode 100644 index 00000000..479d6a9d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/06_invalid_return.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_zero) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett new file mode 100644 index 00000000..93f40684 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/07_pending_result.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_pending) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett new file mode 100644 index 00000000..3fcaa27d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/08_pending_callable.jett @@ -0,0 +1,30 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: run source_five) + TypeField field = selected_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett new file mode 100644 index 00000000..d5269a56 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/09_wrong_selector_owner.jett @@ -0,0 +1,35 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +struct Other: + callback: function() returns int64 +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = other_field() + function() returns Positive observed = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive returned = observed() + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett new file mode 100644 index 00000000..132d0fb5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/10_optional_return.jett @@ -0,0 +1,40 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "predicate" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function() returns int64 +function source_five() returns int64: + string marker = "source" + trace marker + return 5 +function source_zero() returns int64: + string marker = "source" + trace marker + return 0 +function source_pending() returns int64: + string marker = "source" + trace marker + return run 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +struct OptionalHolder: + callback: function() returns optional[int64] +function some_five() returns optional[int64]: + string marker = "source" + trace marker + return some(5) +function optional_field() returns TypeField: + return list.get[TypeField](type.fields[OptionalHolder](), 0) handle: + return optional_field() +function main(stdout: Stdout) returns nothing: + OptionalHolder holder = OptionalHolder(callback: some_five) + TypeField field = optional_field() + function() returns optional[Positive] observed = type.field_value[OptionalHolder, function() returns optional[Positive]](view holder, view field) + Positive returned = observed() handle: + return nothing + Stdout.write(view stdout, "{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett new file mode 100644 index 00000000..4e638fee --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/11_ready_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: unit_source) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "ready\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett new file mode 100644 index 00000000..b9106665 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/12_pending_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: pending_unit_source) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett new file mode 100644 index 00000000..150696c4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/13_exact_pending_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: pending_unit_source) + TypeField field = unit_field() + function() returns nothing observed = type.field_value[UnitHolder, function() returns nothing](view holder, view field) + nothing returned = observed() + println(returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett new file mode 100644 index 00000000..a997dfef --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/14_exact_proved_pending_unit.jett @@ -0,0 +1,47 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct ProvenHolder: + callback: function() returns ReadyUnit +function proved_unit_source() returns ReadyUnit: + string marker = "source" + trace marker + ReadyUnit proved = nothing handle error: + return proved_unit_source() + return run run proved +function proved_field() returns TypeField: + return list.get[TypeField](type.fields[ProvenHolder](), 0) handle: + return proved_field() +function main(stdout: Stdout) returns nothing: + ProvenHolder holder = ProvenHolder(callback: proved_unit_source) + TypeField field = proved_field() + function() returns ReadyUnit observed = type.field_value[ProvenHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + println(coarsen returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett new file mode 100644 index 00000000..faadfe74 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/15_ancestor_proved_pending_unit.jett @@ -0,0 +1,47 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct HigherHolder: + callback: function() returns HigherUnit +function higher_unit_source() returns HigherUnit: + string marker = "source" + trace marker + HigherUnit proved = nothing handle error: + return higher_unit_source() + return run run proved +function higher_field() returns TypeField: + return list.get[TypeField](type.fields[HigherHolder](), 0) handle: + return higher_field() +function main(stdout: Stdout) returns nothing: + HigherHolder holder = HigherHolder(callback: higher_unit_source) + TypeField field = higher_field() + function() returns ReadyUnit observed = type.field_value[HigherHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + println(coarsen returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett new file mode 100644 index 00000000..6188975d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/16_captured_pending_unit.jett @@ -0,0 +1,41 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + nothing delayed = run run nothing + function() returns nothing captured = function() returns nothing: + string marker = "source" + trace marker + return delayed + UnitHolder holder = UnitHolder(callback: captured) + TypeField field = unit_field() + function() returns ReadyUnit observed = type.field_value[UnitHolder, function() returns ReadyUnit](view holder, view field) + ReadyUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett new file mode 100644 index 00000000..e0e46ae3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/17_optional_pending_unit.jett @@ -0,0 +1,46 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +struct OptionalUnitHolder: + callback: function() returns optional[nothing] +function optional_unit_source() returns optional[nothing]: + string marker = "source" + trace marker + return some(run run nothing) +function optional_unit_field() returns TypeField: + return list.get[TypeField](type.fields[OptionalUnitHolder](), 0) handle: + return optional_unit_field() +function main(stdout: Stdout) returns nothing: + OptionalUnitHolder holder = OptionalUnitHolder(callback: optional_unit_source) + TypeField field = optional_unit_field() + function() returns optional[ReadyUnit] observed = type.field_value[OptionalUnitHolder, function() returns optional[ReadyUnit]](view holder, view field) + ReadyUnit returned = observed() handle: + return nothing + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett new file mode 100644 index 00000000..014339a7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/18_invalid_refined_unit.jett @@ -0,0 +1,36 @@ +namespace app +function unit_check(value: nothing) returns bool: + string marker = "predicate" + trace marker + return true +type ReadyUnit = nothing where unit_check(value) +function higher_unit_check(value: nothing) returns bool: + string marker = "higher" + trace marker + return true +type HigherUnit = ReadyUnit where higher_unit_check(value) +function rejected_unit_check(value: nothing) returns bool: + string marker = "rejected" + trace marker + return false +type RejectedUnit = nothing where rejected_unit_check(value) +struct UnitHolder: + callback: function() returns nothing +function unit_source() returns nothing: + string marker = "source" + trace marker + return nothing +function pending_unit_source() returns nothing: + string marker = "source" + trace marker + return run run nothing +function unit_field() returns TypeField: + return list.get[TypeField](type.fields[UnitHolder](), 0) handle: + return unit_field() +function main(stdout: Stdout) returns nothing: + UnitHolder holder = UnitHolder(callback: unit_source) + TypeField field = unit_field() + function() returns RejectedUnit observed = type.field_value[UnitHolder, function() returns RejectedUnit](view holder, view field) + RejectedUnit returned = observed() + Stdout.write(view stdout, "consumer\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/19_generated_named_suites.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/19_generated_named_suites.jett new file mode 100644 index 00000000..c67713f4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/19_generated_named_suites.jett @@ -0,0 +1,31 @@ +namespace app +type Positive = int64 where value > 0 +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + Positive value = observed() + println(coarsen value) + return nothing + +function ready_five() returns Positive: + return 5 +verify lazy_callback_result: + Positive baked = comptime ready_five() + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + assert coarsen observed() == coarsen baked +property lazy_callback_trials: + given repeat: bool + Positive baked = comptime ready_five() + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + assert coarsen observed() == coarsen baked + if repeat: + assert coarsen observed() == 5 diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/20_generated_captured_suites.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/20_generated_captured_suites.jett new file mode 100644 index 00000000..7297cc2c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/20_generated_captured_suites.jett @@ -0,0 +1,29 @@ +namespace app +type Positive = int64 where value > 0 +struct CallbackHolder: + callback: function() returns int64 +function five() returns int64: + return 5 +function main() returns nothing: + CallbackHolder holder = CallbackHolder(callback: five) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + Positive value = observed() + println(coarsen value) + return nothing + +verify captured_callback_result: + int64 seed = 7 + CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + assert coarsen observed() == 7 +property captured_callback_trials: + given repeat: bool + int64 seed = 7 + CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed) + for field in type.fields[CallbackHolder](): + function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field) + assert coarsen observed() == 7 + if repeat: + assert coarsen observed() == 7 diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett new file mode 100644 index 00000000..4fa43642 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_invalid_return.jett @@ -0,0 +1,20 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_zero(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 0 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_zero) + TypeField field = selected_field() + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + Positive returned = observed(view stdout) + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett new file mode 100644 index 00000000..9779cbbf --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_named_lazy.jett @@ -0,0 +1,24 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_five(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 5 +function selected_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return selected_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = selected_field() + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Stdout.write(view stdout, "stored\n") + Positive first = observed(view stdout) + Stdout.write(view stdout, "first:{coarsen first}\n") + Positive second = observed(view stdout) + Stdout.write(view stdout, "second:{coarsen second}\n") + int64 original = holder.callback(view stdout) + Stdout.write(view stdout, "original:{original}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett new file mode 100644 index 00000000..a219c37e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_sources/production_wrong_selector_owner.jett @@ -0,0 +1,22 @@ +namespace app +function positive_check(value: int64) returns bool: + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + callback: function(view Stdout) returns int64 +function source_five(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "source\n") + return 5 +struct Other: + callback: function(view Stdout) returns int64 +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function main(stdout: Stdout) returns nothing: + Holder holder = Holder(callback: source_five) + TypeField field = other_field() + Stdout.write(view stdout, "before\n") + function(view Stdout) returns Positive observed = type.field_value[Holder, function(view Stdout) returns Positive](view holder, view field) + Positive returned = observed(view stdout) + Stdout.write(view stdout, "consumer:{coarsen returned}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/reflected_callable_suites.rs b/crates/jett_driver/tests/native_conformance/reflected_callable_suites.rs new file mode 100644 index 00000000..12c8cbad --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callable_suites.rs @@ -0,0 +1,240 @@ +//! Original callback Sources through reference and all three native purposes. +use super::*; +use std::path::PathBuf; + +#[derive(Clone, Copy, Debug)] +enum Purpose { + Program, + Verify, + Property, +} + +impl Purpose { + fn name(self) -> &'static str { + match self { + Self::Program => "program", + Self::Verify => "verify", + Self::Property => "property", + } + } + + fn emit( + self, + source: &Path, + ) -> Result< + jett_driver::native::NativeProgramObjectArtifact, + jett_driver::native::NativeBuildError, + > { + let options = jett_driver::BuildOptions { release: false }; + match self { + Self::Program => { + jett_driver::native::emit_host_program_object_for_file_with_options(source, options) + } + Self::Verify => { + jett_driver::native::emit_host_verify_suite_object_for_file_with_options( + source, options, + ) + } + Self::Property => { + jett_driver::native::emit_host_property_suite_object_for_file_with_options( + source, options, + ) + } + } + } +} + +struct Original { + name: &'static str, + source: &'static str, + verify: &'static str, + property: &'static str, +} + +const SOURCES: &[Original] = &[ + Original { + name: "named_required_suites", + source: include_str!("reflected_callable_sources/19_generated_named_suites.jett"), + verify: "lazy_callback_result", + property: "lazy_callback_trials", + }, + Original { + name: "captured_suites", + source: include_str!("reflected_callable_sources/20_generated_captured_suites.jett"), + verify: "captured_callback_result", + property: "captured_callback_trials", + }, +]; + +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +#[test] +fn original_sources_keep_both_reference_blocks_and_one_hundred_property_trials() { + for original in SOURCES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, original.source).unwrap(); + let frontend = jett_driver::build_file_with_options( + &source, + jett_driver::BuildOptions { release: false }, + ); + assert!( + !frontend.has_errors, + "{}: {:?}", + original.name, frontend.diagnostics + ); + let reference = jett_driver::test_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{}: {error:?}", original.name)); + assert_eq!( + (reference.total, reference.passed, reference.failed), + (2, 2, 0) + ); + assert_eq!(reference.blocks.len(), 2); + assert_eq!(reference.blocks[0].name, original.verify); + assert!(!reference.blocks[0].is_property); + assert!(reference.blocks[0].passed); + assert_eq!(reference.blocks[1].name, original.property); + assert!(reference.blocks[1].is_property && reference.blocks[1].passed); + assert_eq!(reference.blocks[1].iterations, Some(100)); + assert!( + reference + .blocks + .iter() + .all(|block| block.debug_events.is_empty()) + ); + let main = jett_driver::run_file_capture_output(&source) + .unwrap_or_else(|error| panic!("{} reference main: {error}", original.name)); + assert!(main.stdout.is_empty()); + assert_eq!(jett_driver::render_debug_events(&main.debug_events), "5\n"); + } +} + +#[test] +fn original_sources_emit_all_six_program_verify_property_objects() { + let mut objects = 0; + for original in SOURCES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, original.source).unwrap(); + for purpose in [Purpose::Program, Purpose::Verify, Purpose::Property] { + let object = purpose + .emit(&source) + .unwrap_or_else(|error| panic!("{} {}: {error}", original.name, purpose.name())); + assert!(!object.bytes().is_empty()); + objects += 1; + } + } + assert_eq!(objects, 6); +} + +#[test] +fn original_sources_delete_source_before_twelve_normal_profile_executions() { + let evidence = std::env::var_os("JETT_REFLECTED_CALLABLE_SUITE_EVIDENCE").map(PathBuf::from); + if let Some(root) = &evidence { + assert!(root.is_absolute()); + fs::create_dir(root).expect("fresh callback suite evidence directory"); + } + let mut object_count = 0; + let mut execution_count = 0; + for original in SOURCES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, original.source).unwrap(); + let mut objects = Vec::new(); + for purpose in [Purpose::Program, Purpose::Verify, Purpose::Property] { + let object = purpose + .emit(&source) + .unwrap_or_else(|error| panic!("{} {}: {error}", original.name, purpose.name())); + let case = evidence + .as_ref() + .map(|root| root.join(format!("{}-{}", original.name, purpose.name()))); + if let Some(case) = &case { + fs::create_dir(case).unwrap(); + fs::write(case.join("object.obj"), object.bytes()).unwrap(); + } + object_count += 1; + objects.push((purpose, object, case)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (purpose, object, case) in objects { + for release in [false, true] { + let profile = if release { "release" } else { "debug" }; + let executable = directory + .path() + .join(format!("{}-{profile}.exe", purpose.name())); + jett_driver::native::link_host_object( + &object, + suite_options::launcher_for_options(release), + &executable, + ) + .unwrap(); + let executable_sha256 = digest(&fs::read(&executable).unwrap()); + assert!(!source.exists()); + let output = run_bounded(&executable, directory.path()); + // The existing bounded runner reaps before returning. Retire + // only this one temporary executable, even on oracle failure. + fs::remove_file(&executable).unwrap(); + assert!(!executable.exists()); + if let Some(case) = &case { + fs::write(case.join(format!("{profile}-stdout.bin")), &output.stdout).unwrap(); + fs::write(case.join(format!("{profile}-stderr.bin")), &output.stderr).unwrap(); + let observation = serde_json::json!({ + "case": original.name, "purpose": purpose.name(), + "runtime_profile": profile, "source_build_release": false, + "source_sha256": digest(original.source.as_bytes()), + "source_deleted_before_launch": !source.exists(), + "object_sha256": digest(object.bytes()), + "object_entry": object.program_entry().index(), + "object_target": object.target(), + "executable_sha256": executable_sha256, + "executable_reaped_and_removed": true, + "status": output.status.code(), "stdout": output.stdout, + "stderr": output.stderr, + "public_ordinary_context_retirement_observation": null, + "retirement_observation_available": false + }); + fs::write( + case.join(format!("{profile}-observation.json")), + serde_json::to_vec_pretty(&observation).unwrap(), + ) + .unwrap(); + } + assert_eq!( + output.status.code(), + Some(0), + "{} {} {profile}: {output:?}", + original.name, + purpose.name() + ); + assert!( + output.stdout.is_empty(), + "{} {} {profile}: {output:?}", + original.name, + purpose.name() + ); + assert_eq!( + output.stderr, + if matches!(purpose, Purpose::Program) { + b"5\n".as_slice() + } else { + b"".as_slice() + }, + "{} {} {profile}", + original.name, + purpose.name() + ); + execution_count += 1; + } + } + } + assert_eq!((object_count, execution_count), (6, 12)); + println!( + "callback suites: 2 unchanged Sources, 6 objects, 12 Source-deleted normal profile executions" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/reflected_callables.rs b/crates/jett_driver/tests/native_conformance/reflected_callables.rs new file mode 100644 index 00000000..32e1fb27 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_callables.rs @@ -0,0 +1,490 @@ +//! Original Sources and additive callable-return controls. No Resource launcher. +use super::*; + +struct Case { + name: &'static str, + source: &'static str, + stdout: &'static str, + message: Option<&'static str>, + markers: &'static [&'static str], +} + +const CASES: &[Case] = &[ + Case { + name: "original_wider", + source: include_str!("reflected_callable_sources/01_original_wider.jett"), + stdout: "", + message: None, + markers: &[], + }, + Case { + name: "original_exact", + source: include_str!("reflected_callable_sources/02_original_exact.jett"), + stdout: "", + message: None, + markers: &[], + }, + Case { + name: "read_without_invocation", + source: include_str!("reflected_callable_sources/03_read_without_invocation.jett"), + stdout: "stored\n", + message: None, + markers: &[], + }, + Case { + name: "named_twice_source_unchanged", + source: include_str!("reflected_callable_sources/04_named_twice_source_unchanged.jett"), + stdout: "5:5:5\n", + message: None, + markers: &["source", "predicate", "source", "predicate", "source"], + }, + Case { + name: "captured_twice", + source: include_str!("reflected_callable_sources/05_captured_twice.jett"), + stdout: "7:7\n", + message: None, + markers: &["source", "predicate", "source", "predicate"], + }, + Case { + name: "invalid_return", + source: include_str!("reflected_callable_sources/06_invalid_return.jett"), + stdout: "", + message: Some("runtime error: refinement type constraint failed for 'app.Positive'"), + markers: &["source", "predicate"], + }, + Case { + name: "pending_result", + source: include_str!("reflected_callable_sources/07_pending_result.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "pending_callable", + source: include_str!("reflected_callable_sources/08_pending_callable.jett"), + stdout: "", + message: Some( + "runtime error: expected function value, got pending(function(app.source_five))", + ), + markers: &[], + }, + Case { + name: "wrong_selector_owner", + source: include_str!("reflected_callable_sources/09_wrong_selector_owner.jett"), + stdout: "", + message: Some( + "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Holder'", + ), + markers: &[], + }, + Case { + name: "optional_return", + source: include_str!("reflected_callable_sources/10_optional_return.jett"), + stdout: "5\n", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "ready_refined_unit", + source: include_str!("reflected_callable_sources/11_ready_refined_unit.jett"), + stdout: "ready\n", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "pending_refined_unit", + source: include_str!("reflected_callable_sources/12_pending_refined_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "exact_pending_unit", + source: include_str!("reflected_callable_sources/13_exact_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source"], + }, + Case { + name: "exact_proved_pending_unit", + source: include_str!("reflected_callable_sources/14_exact_proved_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source", "predicate"], + }, + Case { + name: "ancestor_proved_pending_unit", + source: include_str!("reflected_callable_sources/15_ancestor_proved_pending_unit.jett"), + stdout: "", + message: None, + markers: &["source", "predicate", "higher"], + }, + Case { + name: "captured_pending_unit", + source: include_str!("reflected_callable_sources/16_captured_pending_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "optional_pending_unit", + source: include_str!("reflected_callable_sources/17_optional_pending_unit.jett"), + stdout: "", + message: Some("runtime error: reflected callback return: expected a ready value"), + markers: &["source"], + }, + Case { + name: "invalid_refined_unit", + source: include_str!("reflected_callable_sources/18_invalid_refined_unit.jett"), + stdout: "", + message: Some("runtime error: refinement type constraint failed for 'app.RejectedUnit'"), + markers: &["source", "rejected"], + }, +]; + +fn reference(case: &Case, source: &Path) -> Vec { + let events = match (jett_driver::run_file_capture_outcome(source), case.message) { + (Ok(output), None) => { + assert_eq!(output.stdout, case.stdout, "{}", case.name); + output.debug_events + } + (Err(error), Some(message)) => { + assert_eq!(error.message, message, "{}", case.name); + assert_eq!(error.output.stdout, case.stdout, "{}", case.name); + error.output.debug_events + } + (actual, message) => panic!("{}: {actual:?}, expected {message:?}", case.name), + }; + let expected = case + .markers + .iter() + .map(|marker| format!("trace marker: string = {marker}")) + .collect::>(); + assert_eq!(debug_trace_lines(&events), expected, "{}", case.name); + if matches!( + case.name, + "exact_pending_unit" | "exact_proved_pending_unit" | "ancestor_proved_pending_unit" + ) { + assert_eq!( + debug_print_text(&events), + "pending(pending(nothing))\n", + "{} retains both existing pending layers without new validation", + case.name + ); + } + if matches!(case.name, "original_wider" | "original_exact") { + assert_eq!( + jett_driver::render_debug_events(&events), + "5\n", + "{}", + case.name + ); + } + events +} + +#[test] +fn reflected_callable_sources_reference_lazy_results_and_selector_precedence() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + reference(case, &source); + } +} + +#[test] +fn reflected_callable_sources_emit_objects_and_reject_changed_source_and_predicate_bodies() { + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let lowered = jett_driver::lower_file_for_backend_with_options( + &source, + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + let entry = lowered.program_entry.expect("checked ordinary main"); + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + assert!(!object.bytes.is_empty(), "{}", case.name); + if case.name == "original_wider" { + for predicate in [false, true] { + let mut forged = lowered.mir.clone(); + let function = forged + .functions + .iter_mut() + .find(|function| { + if predicate { + function.identity.declaration.kind + == jett_hir::DeclarationKind::RefinementPredicate + && function.identity.declaration.namespace == "app" + } else { + function.identity.declaration.name == "five" + && function.identity.declaration.namespace == "app" + } + }) + .expect("original source or exact predicate body"); + let returned = function + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + jett_mir::TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original typed return"); + returned.kind = if predicate { + jett_hir::ExpressionKind::Bool(false) + } else { + jett_hir::ExpressionKind::Int(6) + }; + let outcome = jett_codegen_cranelift::emit_host_program_object_with_options( + &forged, + &lowered.interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize: false }, + ); + let error = match outcome { + Err(error) => error, + Ok(object) => panic!( + "public MIR changed {} body but emitted {} object bytes", + if predicate { "predicate" } else { "source" }, + object.bytes.len() + ), + }; + assert!( + matches!(&error, jett_codegen_cranelift::CodegenError::InvalidMir(errors) + if errors.iter().any(|error| error.message.contains("reflected callable"))), + "{error:?}" + ); + } + } + } +} + +#[test] +fn native_reflected_callable_sources_delete_source_in_both_normal_runtime_profiles() { + let measured = + std::env::var_os("JETT_REFLECTED_CALLABLE_NORMAL_ARCHIVES").map(std::path::PathBuf::from); + let evidence = + std::env::var_os("JETT_REFLECTED_CALLABLE_NATIVE_EVIDENCE").map(std::path::PathBuf::from); + if let Some(directory) = &evidence { + fs::create_dir(directory).expect("new native evidence directory"); + } + for case in CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let events = reference(case, &source); + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + // Source/debug policy is independent of the runtime archive profile. + jett_driver::BuildOptions { release: false }, + ) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + let case_evidence = evidence.as_ref().map(|root| root.join(case.name)); + if let Some(path) = &case_evidence { + fs::create_dir(path).unwrap(); + fs::write(path.join("source.snapshot"), case.source).unwrap(); + fs::write(path.join("object.obj"), object.bytes()).unwrap(); + } + let mut executables = Vec::new(); + for runtime_release in [false, true] { + let profile = if runtime_release { "release" } else { "debug" }; + let executable = directory + .path() + .join(format!("callback_{runtime_release}.exe")); + let supplied_launcher = measured_launcher::supplied(runtime_release); + let launcher = measured + .as_ref() + .filter(|_| supplied_launcher.is_none()) + .map(|root| { + jett_driver::native::NativeLauncherBundle::from_manifest( + &root.join(format!("{profile}-bundle/launcher.json")), + runtime_release, + ) + .expect("exact measured normal runtime profile manifest") + }); + let launcher = supplied_launcher + .or(launcher.as_ref()) + .unwrap_or_else(|| suite_options::launcher_for_options(runtime_release)); + jett_driver::native::link_host_object(&object, launcher, &executable) + .unwrap_or_else(|error| panic!("{}: {error}", case.name)); + if let Some(path) = &case_evidence { + fs::copy(&executable, path.join(format!("{profile}.exe"))).unwrap(); + } + executables.push((executable, profile)); + } + fs::remove_file(&source).unwrap(); + assert!( + !source.exists(), + "Source must be absent before either launch" + ); + for (executable, profile) in executables { + let output = run_bounded(&executable, directory.path()); + if let Some(path) = &case_evidence { + fs::write( + path.join(format!("{profile}-observation.json")), + serde_json::to_vec_pretty(&serde_json::json!({ + "case": case.name, + "runtime_profile": profile, + "source_build_release": false, + "source_deleted_before_launch": !source.exists(), + "object_target": object.target(), + "object_entry": object.program_entry().index(), + "status": output.status.code(), + "stdout": output.stdout, + "stderr": output.stderr, + "public_ordinary_context_retirement_observation": null, + "retirement_observation_available": false, + })) + .unwrap(), + ) + .unwrap(); + } + assert_eq!( + output.status.code(), + Some(if case.message.is_some() { 71 } else { 0 }), + "{}: {output:?}", + case.name + ); + assert_eq!(output.stdout, case.stdout.as_bytes(), "{}", case.name); + let mut stderr = jett_driver::render_debug_events(&events); + if let Some(message) = case.message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!( + output.stderr, + stderr.as_bytes(), + "{}: {output:?}", + case.name + ); + } + } +} + +const PRODUCTION_CASES: &[Case] = &[ + Case { + name: "production_named_lazy", + source: include_str!("reflected_callable_sources/production_named_lazy.jett"), + stdout: "stored\nsource\nfirst:5\nsource\nsecond:5\nsource\noriginal:5\n", + message: None, + markers: &[], + }, + Case { + name: "production_invalid_return", + source: include_str!("reflected_callable_sources/production_invalid_return.jett"), + stdout: "stored\nsource\n", + message: Some("runtime error: refinement type constraint failed for 'app.Positive'"), + markers: &[], + }, + Case { + name: "production_wrong_selector_owner", + source: include_str!("reflected_callable_sources/production_wrong_selector_owner.jett"), + stdout: "before\n", + message: Some( + "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Holder'", + ), + markers: &[], + }, +]; + +#[test] +fn native_reflected_callable_production_output_matches_with_optimized_objects_without_source() { + let measured = + std::env::var_os("JETT_REFLECTED_CALLABLE_NORMAL_ARCHIVES").map(std::path::PathBuf::from); + let mut object_count = 0; + let mut execution_count = 0; + for case in PRODUCTION_CASES { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, case.source).unwrap(); + let events = reference(case, &source); + assert!(events.is_empty(), "{} uses canonical Stdout", case.name); + let mut executables = Vec::new(); + for optimize in [false, true] { + let object = jett_driver::native::emit_host_program_object_for_file_with_options( + &source, + // Production Source has no debug operations. This option reaches + // CodegenOptions.optimize independently of the runtime profile. + jett_driver::BuildOptions { release: optimize }, + ) + .unwrap_or_else(|error| panic!("{} optimize={optimize}: {error}", case.name)); + object_count += 1; + for runtime_release in [false, true] { + let profile = if runtime_release { "release" } else { "debug" }; + let executable = directory.path().join(format!( + "callback_production_{optimize}_{runtime_release}.exe" + )); + let supplied_launcher = measured_launcher::supplied(runtime_release); + let measured_launcher = measured + .as_ref() + .filter(|_| supplied_launcher.is_none()) + .map(|root| { + jett_driver::native::NativeLauncherBundle::from_manifest( + &root.join(format!("{profile}-bundle/launcher.json")), + runtime_release, + ) + .expect("exact measured normal runtime profile manifest") + }); + let launcher = supplied_launcher + .or(measured_launcher.as_ref()) + .unwrap_or_else(|| suite_options::launcher_for_options(runtime_release)); + jett_driver::native::link_host_object(&object, launcher, &executable) + .unwrap_or_else(|error| { + panic!( + "{} optimize={optimize} runtime={profile}: {error}", + case.name + ) + }); + executables.push((executable, optimize, profile)); + } + } + fs::remove_file(&source).unwrap(); + assert!( + !source.exists(), + "Source must be absent before every launch" + ); + for (executable, optimize, profile) in executables { + let output = run_bounded(&executable, directory.path()); + assert_eq!( + output.status.code(), + Some(if case.message.is_some() { 71 } else { 0 }), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + assert_eq!( + output.stdout, + case.stdout.as_bytes(), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + let stderr = case + .message + .map(|message| format!("{message}\n")) + .unwrap_or_default(); + assert_eq!( + output.stderr, + stderr.as_bytes(), + "{} optimize={optimize} runtime={profile}: {output:?}", + case.name + ); + execution_count += 1; + } + } + assert_eq!(object_count, PRODUCTION_CASES.len() * 2); + assert_eq!(execution_count, PRODUCTION_CASES.len() * 4); + println!( + "reflected callable production controls: {} Sources, {object_count} objects, {execution_count} Source-deleted executions", + PRODUCTION_CASES.len() + ); +} + +#[path = "reflected_callable_suites.rs"] +mod generated_suites; diff --git a/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs b/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs new file mode 100644 index 00000000..12262767 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_iteration_controls.rs @@ -0,0 +1,119 @@ +//! Ordinary reflected metadata controls for runtime, comptime, and native suites. +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, build_host_property_suite_executable_with_options, + build_host_verify_suite_executable_with_options, +}; +use std::fs; + +struct Case { + name: &'static str, + source: &'static str, + stdout: &'static str, +} + +macro_rules! case { + ($name:literal) => { + Case { + name: $name, + source: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_iteration_controls/", + $name, + ".jett" + )), + stdout: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_iteration_controls/", + $name, + ".stdout" + )), + } + }; +} + +fn run_form(name: &str, source_text: &str, expected_stdout: &str) { + let expected_stdout = expected_stdout.replace("\r\n", "\n"); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, source_text).unwrap(); + let reference = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|failure| panic!("{name}: {failure:?}")); + assert_eq!(reference.stdout, expected_stdout, "{name}"); + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + let mut programs = Vec::new(); + let mut suites = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("iteration_{release}.exe")); + build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + programs.push(binary); + let verify = directory + .path() + .join(format!("iteration_verify_{release}.exe")); + let property = directory + .path() + .join(format!("iteration_property_{release}.exe")); + build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &verify, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &property, + BuildOptions { release }, + ) + .unwrap_or_else(|failure| panic!("{name}: {failure}")); + suites.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for binary in programs { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert_eq!( + actual.stdout, + expected_stdout.as_bytes(), + "{name}: {actual:?}" + ); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } + for binary in suites { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_ordinary_equal_type_metadata_branches_and_pure_helpers_match_reference() { + for case in [ + case!("nominal"), + case!("members"), + case!("arguments_nested"), + ] { + run_form(case.name, case.source, case.stdout); + if matches!(case.name, "nominal" | "members") { + let mut piped = case.source.to_owned(); + for owner in ["Record", "T", "Header"] { + piped = piped.replace( + &format!("type.field_value[{owner}, Field](view source, view field)"), + &format!("source into view type.field_value[{owner}, Field](view field)"), + ); + } + assert_ne!(piped, case.source, "{}: getter pipeline control", case.name); + run_form(&format!("{}_pipeline", case.name), &piped, case.stdout); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/reflected_nested_producers.rs b/crates/jett_driver/tests/native_conformance/reflected_nested_producers.rs new file mode 100644 index 00000000..c02dc290 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_nested_producers.rs @@ -0,0 +1,600 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use jett_driver::BuildOptions; +use jett_driver::native::{ + build_host_executable_with_options, build_host_property_suite_executable_with_options, + build_host_verify_suite_executable_with_options, +}; +use std::fs; + +struct NestedCase { + name: &'static str, + source: &'static str, + stdout: &'static str, + debug: &'static str, + message: Option<&'static str>, +} + +macro_rules! case { + ($name:literal) => { + case!($name, None) + }; + ($name:literal, $message:expr) => { + NestedCase { + name: $name, + source: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_nested/", + $name, + ".jett" + )), + stdout: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_nested/", + $name, + ".stdout" + )), + debug: include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/native/reflected_nested/", + $name, + ".debug" + )), + message: $message, + } + }; +} + +const POSITIVE_FALSE: &str = "runtime error: refinement type constraint failed for 'app.Positive'"; +const TEXT_FALSE: &str = "runtime error: refinement type constraint failed for 'app.Text'"; +const READY_LIST: &str = "runtime error: reflected field refinement: expected a ready list value"; +const READY_SET: &str = "runtime error: reflected field refinement: expected a ready set value"; +const READY_MAP: &str = "runtime error: reflected field refinement: expected a ready map value"; +const READY_OPTIONAL: &str = + "runtime error: reflected field refinement: expected a ready optional value"; +const READY_RESULT: &str = + "runtime error: reflected field refinement: expected a ready result value"; + +fn piped_getters(source: &str) -> String { + let mut count = 0; + let mut output = String::new(); + for line in source.lines() { + if let Some(call) = line.strip_prefix(" return type.") { + if let Some(callee) = call.strip_suffix("(view source, view field)") { + if callee.starts_with("field_value[") + || callee.starts_with("variant_field_value[") + || callee.starts_with("machine_field_value[") + { + output.push_str(&format!( + " return source into view type.{callee}(view field)\n" + )); + count += 1; + continue; + } + } + } + output.push_str(line); + output.push('\n'); + } + for (owner, shape, getter) in [ + ("Holder", "record", "field_value"), + ("Event", "event", "variant_field_value"), + ("Session", "session", "machine_field_value"), + ] { + let direct = format!( + "type.{getter}[{owner}, list[Positive]](view {shape}_operand(view source), view field_operand(view field))" + ); + let pipeline = format!( + "{shape}_operand(view source) into view type.{getter}[{owner}, list[Positive]](view field_operand(view field))" + ); + if output.contains(&direct) { + output = output.replace(&direct, &pipeline); + count += 1; + } + } + assert!( + count >= 3, + "fixture did not exercise all three getter pipelines" + ); + output +} + +fn first_getter(source: &str, shape: &str) -> String { + let original = "string first = summarize(read_record(view record, view record_selector))"; + assert!( + source.contains(original), + "terminal fixture needs a selectable first getter" + ); + source.replace( + original, + &format!("string first = summarize(read_{shape}(view {shape}, view {shape}_selector))"), + ) +} + +fn later_handler(source: &str) -> String { + let helpers = r#"function fallback(view stdout: Stdout) returns string: + Stdout.write(view stdout, "handler\n") + return "fallback" +function later(view stdout: Stdout) returns result[string, string]: + Stdout.write(view stdout, "later\n") + return fail("later") +"#; + let with_helpers = source.replace("function main(", &format!("{helpers}function main(")); + assert_ne!(source, with_helpers); + let sentinel = " string unreachable = \"{later(view stdout) handle error: default fallback(view stdout)}\""; + let call = if source.contains(" string value = report()") { + " string value = report()" + } else { + assert!( + source.contains(" report(view stdout)"), + "terminal fixture needs its report boundary" + ); + " report(view stdout)" + }; + let guarded = with_helpers.replace(call, &format!("{call}\n{sentinel}")); + assert_ne!( + with_helpers, guarded, + "terminal report sentinel was not inserted" + ); + guarded +} + +fn run_form( + name: &str, + source_text: &str, + stdout: &str, + debug_text: &str, + message: Option<&str>, + suites: bool, +) { + let stdout = stdout.replace("\r\n", "\n"); + let stdout = stdout.as_str(); + let debug_text = debug_text.replace("\r\n", "\n"); + let debug_text = debug_text.as_str(); + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, source_text).unwrap(); + match (jett_driver::run_file_capture_outcome(&source), message) { + (Ok(actual), None) => { + assert_eq!(actual.stdout, stdout, "{name}"); + assert_eq!( + jett_driver::render_debug_events(&actual.debug_events), + debug_text, + "{name}" + ); + assert!( + actual + .debug_events + .iter() + .all(|event| event.kind == jett_driver::DebugEventKind::Trace), + "{name}" + ); + } + (Err(actual), Some(message)) => { + assert_eq!(actual.message, message, "{name}"); + assert_eq!(actual.output.stdout, stdout, "{name}"); + assert_eq!( + jett_driver::render_debug_events(&actual.output.debug_events), + debug_text, + "{name}" + ); + assert!( + actual + .output + .debug_events + .iter() + .all(|event| event.kind == jett_driver::DebugEventKind::Trace), + "{name}" + ); + } + (actual, expected) => panic!("{name}: {actual:?}; expected terminal message {expected:?}"), + } + let mut binaries = Vec::new(); + let mut suite_binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("nested_{release}.exe")); + build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + if suites { + let verify = directory + .path() + .join(format!("nested_verify_{release}.exe")); + let property = directory + .path() + .join(format!("nested_property_{release}.exe")); + build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &verify, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &property, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + suite_binaries.extend([verify, property]); + } + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + // 72 is the cleanup-override status; it must never replace terminal 71. + assert_eq!( + actual.status.code(), + Some(if message.is_some() { 71 } else { 0 }), + "{name}: {actual:?}" + ); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release { + String::new() + } else { + debug_text.to_owned() + }; + if let Some(message) = message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + for binary in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +fn run_case(case: NestedCase, all_getters: bool, suites: bool) { + let shapes: &[&str] = if all_getters { + &["record", "event", "session"] + } else { + &[""] + }; + for shape in shapes { + let source = if all_getters { + first_getter(case.source, shape) + } else { + case.source.to_owned() + }; + let source = if case.message.is_some() { + later_handler(&source) + } else { + source + }; + run_form( + &format!("{}_{shape}_direct", case.name), + &source, + case.stdout, + case.debug, + case.message, + suites, + ); + let piped = piped_getters(&source); + run_form( + &format!("{}_{shape}_pipeline", case.name), + &piped, + case.stdout, + case.debug, + case.message, + suites, + ); + } +} + +#[test] +fn native_nested_reflected_ready_and_empty_builtin_wrappers_match_reference() { + for case in [ + case!("list_ready_success"), + case!("list_empty_success"), + case!("set_ready_success"), + case!("set_empty_success"), + case!("map_ready_success"), + case!("map_empty_success"), + case!("optional_ready_success"), + case!("optional_none_success"), + case!("result_ok_success"), + case!("result_fail_success"), + case!("repeated_map_refinement_comparator_control"), + case!("repeated_result_refinement_comparator_control"), + case!("inactive_secret_arm_success"), + ] { + run_case(case, false, false); + } +} + +#[test] +fn native_nested_reflected_proof_prefixes_owned_unicode_and_operand_order() { + for case in [ + case!("proof_prefixes"), + case!("owned_proof_prefixes"), + case!("owned_unicode_independence"), + case!("recursive_wrappers"), + case!("operand_order"), + ] { + assert!( + case.source + .as_bytes() + .windows("Agent-λ🙂".len()) + .any(|slice| slice == "Agent-λ🙂".as_bytes()) + || matches!(case.name, "proof_prefixes" | "operand_order") + ); + run_case(case, false, false); + } +} + +#[test] +fn native_nested_reflected_preflight_precedes_all_predicates() { + for case in [ + case!("list_later_optional_pending", Some(READY_OPTIONAL)), + case!("map_later_optional_pending", Some(READY_OPTIONAL)), + case!("optional_inner_list_pending", Some(READY_LIST)), + case!("result_inner_map_pending", Some(READY_MAP)), + case!( + "later_occupied_secret_refusal", + Some( + "runtime error: reflected field refinement: cannot establish new invariants beneath secret" + ) + ), + ] { + run_case(case, true, false); + } +} + +#[test] +fn native_nested_reflected_predicates_stop_at_first_failure_in_value_order() { + for case in [ + case!("list_middle_false", Some(POSITIVE_FALSE)), + case!("list_owned_text_false", Some(TEXT_FALSE)), + case!("optional_present_false", Some(POSITIVE_FALSE)), + case!("result_occupied_false", Some(POSITIVE_FALSE)), + case!("map_key_value_order_false", Some(POSITIVE_FALSE)), + case!( + "list_child_pending", + Some( + "runtime error: error evaluating refinement constraint for 'app.Positive': unsupported binary operation: pending(9) Gt 0" + ) + ), + ] { + run_case(case, true, false); + } +} + +#[test] +fn native_nested_reflected_exact_pending_schemas_preserve_both_depths() { + for case in [ + case!("list_exact_pending_1"), + case!("list_exact_pending_2"), + case!("set_exact_pending_1"), + case!("set_exact_pending_2"), + case!("map_exact_pending_1"), + case!("map_exact_pending_2"), + case!("optional_exact_pending_1"), + case!("optional_exact_pending_2"), + case!("result_exact_pending_1"), + case!("result_exact_pending_2"), + case!("child_exact_pending_1"), + case!("child_exact_pending_2"), + case!("declared_root_ancestor_pending"), + case!("child_ancestor_ready"), + ] { + run_case(case, false, false); + } +} + +#[test] +fn native_nested_reflected_changed_pending_wrappers_do_not_inherit_child_exactness() { + for case in [ + case!("list_outer_pending_1", Some(READY_LIST)), + case!("list_outer_pending_2", Some(READY_LIST)), + case!("set_outer_pending_1", Some(READY_SET)), + case!("set_outer_pending_2", Some(READY_SET)), + case!("map_outer_pending_1", Some(READY_MAP)), + case!("map_outer_pending_2", Some(READY_MAP)), + case!("optional_outer_pending_1", Some(READY_OPTIONAL)), + case!("optional_outer_pending_2", Some(READY_OPTIONAL)), + case!("result_outer_pending_1", Some(READY_RESULT)), + case!("result_outer_pending_2", Some(READY_RESULT)), + case!("child_ancestor_pending_1", Some(READY_LIST)), + case!("child_ancestor_pending_2", Some(READY_LIST)), + ] { + run_case(case, true, false); + } +} + +#[test] +fn native_nested_reflected_original_selector_errors_precede_container_preflight() { + for case in [ + case!( + "selector_wrong_owner_record", + Some( + "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Holder'" + ) + ), + case!( + "selector_wrong_owner_event", + Some( + "runtime error: type.variant_field_value: field metadata belongs to 'app.Other', expected 'app.Event.content'" + ) + ), + case!( + "selector_wrong_owner_session", + Some( + "runtime error: type.machine_field_value: field metadata belongs to 'app.Other', expected 'app.Session.content'" + ) + ), + case!( + "selector_wrong_member_event", + Some( + "runtime error: type.variant_field_value: field metadata belongs to 'app.Event.mirror', expected 'app.Event.content'" + ) + ), + case!( + "selector_wrong_member_session", + Some( + "runtime error: type.machine_field_value: field metadata belongs to 'app.Session.mirror', expected 'app.Session.content'" + ) + ), + case!( + "selector_wrong_request_record", + Some( + "runtime error: type.field_value: field 'text' has type 'string', requested 'list[app.Positive]'" + ) + ), + case!( + "selector_wrong_request_event", + Some( + "runtime error: type.variant_field_value: field 'text' has type 'string', requested 'list[app.Positive]'" + ) + ), + case!( + "selector_wrong_request_session", + Some( + "runtime error: type.machine_field_value: field 'text' has type 'string', requested 'list[app.Positive]'" + ) + ), + ] { + run_case(case, false, false); + } +} + +#[test] +fn native_nested_reflected_closed_comptime_verify_and_property_match_in_both_profiles() { + for case in [ + case!("pure_proof_prefixes"), + case!("pure_owned_proof_prefixes"), + case!("pure_recursive_wrappers"), + ] { + run_case(case, false, true); + } +} + +fn run_root_case(case: NestedCase) { + let checked_constructor = " Record source = Record(values: seed()) handle error:\n Stdout.write(view stdout, error)\n return nothing"; + let constructor = if case.source.contains(checked_constructor) { + checked_constructor + } else { + " Record source = Record(values: seed())" + }; + assert!(case.source.contains(constructor)); + for shape in ["record", "event", "machine"] { + let mut source = case.source.to_owned(); + let mut debug = case.debug.to_owned(); + if shape != "record" { + let (owner, construct, selector) = if shape == "event" { + ( + "Event", + "Event.content(seed())", + "event_selector(view source)", + ) + } else { + ( + "Session", + "Session(content, seed())", + "machine_selector(view source)", + ) + }; + source = source + .replace(constructor, &format!(" {owner} source = {construct}")) + .replace( + "TypeField field = record_selector()", + &format!("TypeField field = {selector}"), + ) + .replace( + "extracted = record_direct(view source, view field)", + &format!("extracted = {shape}_direct(view source, view field)"), + ); + let value_prefix = if shape == "event" { + "app.Event.content(" + } else { + "app.Session@content(" + }; + debug = debug.replace( + "trace source: app.Record = app.Record(values: ", + &format!("trace source: app.{owner} = {value_prefix}"), + ); + } + if case.message.is_some() { + source = later_handler(&source); + } + for pipeline in [false, true] { + let text = if pipeline { + source.replace( + &format!("extracted = {shape}_direct(view source, view field)"), + &format!("extracted = {shape}_pipe(view source, view field)"), + ) + } else { + source.clone() + }; + run_form( + &format!("{}_{shape}_{pipeline}", case.name), + &text, + case.stdout, + &debug, + case.message, + false, + ); + } + } +} + +#[test] +fn native_nested_reflected_root_to_generic_base_checks_readiness_without_leaf_rechecks() { + for case in [ + case!("root_base_list_depth0"), + case!("root_base_list_depth1", Some(READY_LIST)), + case!("root_base_list_depth2", Some(READY_LIST)), + case!("root_base_set_depth0"), + case!("root_base_set_depth1", Some(READY_SET)), + case!("root_base_set_depth2", Some(READY_SET)), + case!("root_base_map_depth0"), + case!("root_base_map_depth1", Some(READY_MAP)), + case!("root_base_map_depth2", Some(READY_MAP)), + case!("root_base_optional_depth0"), + case!("root_base_optional_depth1", Some(READY_OPTIONAL)), + case!("root_base_optional_depth2", Some(READY_OPTIONAL)), + case!("root_base_result_depth0"), + case!("root_base_result_depth1", Some(READY_RESULT)), + case!("root_base_result_depth2", Some(READY_RESULT)), + case!("root_generic_exact_pending2"), + case!("root_named_exact_pending2"), + case!("root_to_plain_pending2"), + case!("root_new_sibling_pending2", Some(READY_LIST)), + case!("root_shared_named_ancestor_pending2"), + ] { + run_root_case(case); + } +} + +#[test] +fn native_nested_reflected_nominal_argument_facts_preserve_pending_leaf_proofs() { + for case in [ + case!("root_nominal_ordinary_element_ready"), + case!("root_nominal_holder_positive_ready"), + case!("root_nominal_unused_positive_ready"), + case!("root_nominal_unused_alias_ready"), + case!("root_nominal_unused_plain_ready"), + case!("root_nominal_ordinary_element_pending2"), + case!("root_nominal_holder_positive_pending2", Some(READY_LIST)), + case!("root_nominal_unused_positive_pending2", Some(READY_LIST)), + case!("root_nominal_unused_alias_pending2", Some(READY_LIST)), + case!("root_nominal_unused_plain_pending2"), + ] { + run_root_case(case); + } +} + +// TypeField index/name/type forgery cannot be expressed in admitted source. +// Existing HIR/MIR corrupted-selector tests cover those refusal boundaries. +// Changes beneath callable/nominal/erased interface types remain outside the +// ready builtin relation. Root refinement over an existing secret/nominal base +// is a separate unaudited contract; these fixtures do not claim its closure. diff --git a/crates/jett_driver/tests/native_conformance/reflected_producers.rs b/crates/jett_driver/tests/native_conformance/reflected_producers.rs new file mode 100644 index 00000000..591a4920 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/reflected_producers.rs @@ -0,0 +1,1103 @@ +use super::*; + +const REFLECTED_CONTROLS: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function sibling_check(value: int64) returns bool: + string marker = "sibling" + trace marker + return value >= 0 +type Sibling = int64 where sibling_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function long_check(value: string) returns bool: + string marker = "long" + trace marker + return string.char_count(value) > 2 +type Long = Text where long_check(value) +function nonempty_check(view value: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view value) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +struct Record: + raw: int64 + positive: Positive + high: High + sibling: Sibling +enum Event: + first(raw: int64, positive: Positive, high: High, sibling: Sibling) + mirror(sibling: Sibling, high: High, positive: Positive, raw: int64) +machine Session: + states: + first(raw: int64, positive: Positive, high: High, sibling: Sibling) + mirror(sibling: Sibling, high: High, positive: Positive, raw: int64) + transitions: + first to mirror +struct Parcel: + raw_text: string + text: Text + raw_values: list[int64] + values: NonEmpty +enum Packet: + content(values: list[int64]) + empty +machine Store: + states: + content(values: list[int64]) + empty + transitions: + content to empty +function positive_seed(value: int64) returns Positive: + Positive ready = value handle error: + return positive_seed(value) + return ready +function high_seed(view value: Positive) returns High: + High ready = clone value handle error: + return high_seed(view value) + return ready +function sibling_seed(value: int64) returns Sibling: + Sibling ready = value handle error: + return sibling_seed(value) + return ready +function record_seed(value: int64) returns Record: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + return Record(raw: value, positive: positive, high: high, sibling: sibling) handle error: + return record_seed(value) +function event_seed(value: int64, reversed: bool) returns Event: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Event.mirror(sibling, high, positive, value) + return Event.first(value, positive, high, sibling) +function session_seed(value: int64, reversed: bool) returns Session: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Session(mirror, sibling, high, positive, value) + return Session(first, value, positive, high, sibling) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Record](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function record_read(view source: Record, index: int64) returns High: + TypeField field = record_field(index) + return type.field_value[Record, High](view source, view field) +function event_read(view source: Event, index: int64) returns High: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, High](view source, view field) +function session_read(view source: Session, index: int64) returns High: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, High](view source, view field) +function observe(view value: Positive) returns int64: + return coarsen clone value +function record_weaker(view source: Record) returns Positive: + TypeField field = record_field(2) + return type.field_value[Record, Positive](view source, view field) +function event_weaker(view source: Event, reversed: bool) returns Positive: + mutable int64 index = 2 + if reversed: + index = 1 + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, Positive](view source, view field) +function session_weaker(view source: Session, reversed: bool) returns Positive: + mutable int64 index = 2 + if reversed: + index = 1 + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, Positive](view source, view field) +function record_report(value: int64) returns string: + Record source = record_seed(value) + int64 raw = coarsen record_read(view source, 0) + int64 ancestor = coarsen record_read(view source, 1) + int64 exact = coarsen record_read(view source, 2) + int64 sibling = coarsen record_read(view source, 3) + int64 original = coarsen source.high + Positive weaker = record_weaker(view source) + int64 reused = observe(view weaker) + return "{raw}:{ancestor}:{exact}:{sibling}:{original}:{reused}" +function event_report(value: int64, reversed: bool) returns string: + Event source = event_seed(value, reversed) + TypeVariant variant = type.variant_value[Event](view source) + int64 first = coarsen event_read(view source, 0) + int64 second = coarsen event_read(view source, 1) + int64 third = coarsen event_read(view source, 2) + int64 fourth = coarsen event_read(view source, 3) + Positive weaker = event_weaker(view source, reversed) + int64 reused = observe(view weaker) + return "{variant.name}:{first}:{second}:{third}:{fourth}:{reused}" +function session_report(value: int64, reversed: bool) returns string: + Session source = session_seed(value, reversed) + TypeMachineState state = type.machine_state_value[Session](view source) + int64 first = coarsen session_read(view source, 0) + int64 second = coarsen session_read(view source, 1) + int64 third = coarsen session_read(view source, 2) + int64 fourth = coarsen session_read(view source, 3) + Positive weaker = session_weaker(view source, reversed) + int64 reused = observe(view weaker) + return "{state.name}:{first}:{second}:{third}:{fourth}:{reused}" +function parcel_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Parcel](), index) handle: + return parcel_field(0) +function parcel_seed() returns Parcel: + Text text = "Agent-λ🙂" handle error: + return parcel_seed() + NonEmpty values = list(7, 9) handle error: + return parcel_seed() + return Parcel(raw_text: "Agent-λ🙂", text: text, raw_values: list(7, 9), values: values) handle error: + return parcel_seed() +function parcel_text(view source: Parcel, index: int64) returns Long: + TypeField field = parcel_field(index) + return type.field_value[Parcel, Long](view source, view field) +function parcel_values(view source: Parcel, index: int64) returns NonEmpty: + TypeField field = parcel_field(index) + return type.field_value[Parcel, NonEmpty](view source, view field) +function packet_values(view source: Packet) returns NonEmpty: + TypeVariant variant = type.variant_value[Packet](view source) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return packet_values(view source) + return type.variant_field_value[Packet, NonEmpty](view source, view field) +function store_values(view source: Store) returns NonEmpty: + TypeMachineState state = type.machine_state_value[Store](view source) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return store_values(view source) + return type.machine_field_value[Store, NonEmpty](view source, view field) +function packet_raw(view source: Packet) returns list[int64]: + TypeVariant variant = type.variant_value[Packet](view source) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return packet_raw(view source) + return type.variant_field_value[Packet, list[int64]](view source, view field) +function store_raw(view source: Store) returns list[int64]: + TypeMachineState state = type.machine_state_value[Store](view source) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return store_raw(view source) + return type.machine_field_value[Store, list[int64]](view source, view field) +function owned_report() returns string: + Parcel source = parcel_seed() + string first = coarsen parcel_text(view source, 0) + string second = coarsen parcel_text(view source, 1) + NonEmpty raw = parcel_values(view source, 2) + NonEmpty exact = parcel_values(view source, 3) + mutable list[int64] independent = coarsen raw + independent = list.append[int64](independent, 11) + list[int64] untouched = coarsen exact + Packet packet = Packet.content(list(7, 9)) + Store store = Store(content, list(7, 9)) + mutable list[int64] packet_copy = coarsen packet_values(view packet) + packet_copy = list.append[int64](packet_copy, 11) + list[int64] store_copy = coarsen store_values(view store) + list[int64] packet_original = packet_raw(view packet) + list[int64] store_original = store_raw(view store) + return "{first}:{second}:{list.length(view independent)}:{list.length(view untouched)}:{list.length(view source.raw_values)}:{list.length(view packet_copy)}:{list.length(view packet_original)}:{list.length(view store_copy)}:{list.length(view store_original)}" +function pending_report() returns string: + Positive positive = positive_seed(12) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(12) + Record source = Record(raw: 12, positive: positive, high: run run high, sibling: sibling) handle error: + return error + High copied = record_read(view source, 2) + trace copied + High once = join copied handle error: + return error + trace once + High ready = join once handle error: + return error + int64 value = coarsen ready + High original = source.high + trace original + return "{value}" +function event_pending_report() returns string: + Positive positive = positive_seed(12) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(12) + Event source = Event.first(12, positive, run high, sibling) + High copied_event = event_read(view source, 2) + trace copied_event + High ready = join copied_event handle error: + return error + int64 value = coarsen ready + High original_event = event_read(view source, 2) + trace original_event + return "{value}" +function session_pending_report() returns string: + Positive positive = positive_seed(12) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(12) + Session source = Session(first, 12, positive, run run high, sibling) + High copied_session = session_read(view source, 2) + trace copied_session + High once_session = join copied_session handle error: + return error + trace once_session + High ready = join once_session handle error: + return error + int64 value = coarsen ready + High original_session = session_read(view source, 2) + trace original_session + return "{value}" +function source_operand(view stdout: Stdout, view source: Record) returns Record: + Stdout.write(view stdout, "source\n") + return clone source +function field_operand(view stdout: Stdout) returns TypeField: + Stdout.write(view stdout, "field\n") + return record_field(0) +function operands_report(view stdout: Stdout) returns string: + Record source = record_seed(12) + High copied = type.field_value[Record, High](view source_operand(view stdout, view source), view field_operand(view stdout)) + int64 value = coarsen copied + int64 original = coarsen source.high + return "{value}:{original}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "record:{record_report(12)}\n") + Stdout.write(view stdout, "event:{event_report(12, false)}|{event_report(12, true)}\n") + Stdout.write(view stdout, "machine:{session_report(12, false)}|{session_report(12, true)}\n") + Stdout.write(view stdout, "owned:{owned_report()}\n") + Stdout.write(view stdout, "pending:{pending_report()}|{event_pending_report()}|{session_pending_report()}\n") + Stdout.write(view stdout, "operands\n") + string operand_report = operands_report(view stdout) + Stdout.write(view stdout, "operands:{operand_report}\n") +"#; + +const REFLECTED_FAILURE: &str = r#"namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function sibling_check(value: int64) returns bool: + string marker = "sibling" + trace marker + return value >= 0 +type Sibling = int64 where sibling_check(value) +function risky_check(value: int64) returns bool: + string marker = "risky" + trace marker + list[string] owned = list("predicate-owned") + list[string] removed = list.remove_at[string](owned, -1) + return value > 0 +type Risky = int64 where risky_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +function nonempty_check(view value: list[int64]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view value) > 0 +type NonEmpty = list[int64] where nonempty_check(view value) +struct Record: + raw: int64 + text: string + values: list[int64] +struct Other: + raw: int64 +enum Event: + first(value: Positive) + second(value: int64) +machine Session: + states: + first(value: Sibling) + second(value: int64) + transitions: + first to second +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Record](), index) handle: + return record_field(0) +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function later(view stdout: Stdout) returns optional[string]: + Stdout.write(view stdout, "later\n") + return none +function fallback(view stdout: Stdout) returns string: + Stdout.write(view stdout, "handler\n") + return "fallback" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") +__SETUP__ + Stdout.write(view stdout, "before\n") +__READ__ + string message = "{later(view stdout) handle: default fallback(view stdout)}" + Stdout.write(view stdout, "after:{message}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") +"#; + +const PURE_ENTRY: &str = r#"function report(number: int64, reversed: bool) returns string: + return "{record_report(number)};{event_report(number, reversed)};{session_report(number, reversed)};{owned_report()}" +function expected(number: int64, reversed: bool) returns string: + mutable string member = "first" + if reversed: + member = "mirror" + return "{number}:{number}:{number}:{number}:{number}:{number};{member}:{number}:{number}:{number}:{number}:{number};{member}:{number}:{number}:{number}:{number}:{number};Agent-λ🙂:Agent-λ🙂:3:2:2:3:2:2:2" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(12, true) + Stdout.write(view stdout, "pure:{report(12, false)}|{baked}\n") +verify reflected_producer_values: + assert report(12, false) == expected(12, false) + assert report(127, true) == expected(127, true) +property reflected_producer_trials: + given high: bool + given reversed: bool + mutable int64 number = 12 + if high: + number = 127 + assert report(number, reversed) == expected(number, reversed) +"#; + +fn pure_source() -> String { + let (prefix, _) = REFLECTED_CONTROLS + .split_once("function pending_report()") + .unwrap(); + let without_traces = prefix + .replace(" string marker = \"positive\"\n trace marker\n", "") + .replace(" string marker = \"high\"\n trace marker\n", "") + .replace(" string marker = \"sibling\"\n trace marker\n", "") + .replace(" string marker = \"text\"\n trace marker\n", "") + .replace(" string marker = \"long\"\n trace marker\n", "") + .replace(" string marker = \"nonempty\"\n trace marker\n", ""); + format!("{without_traces}{PURE_ENTRY}") +} + +fn marker_lines(markers: &[&str]) -> Vec { + markers + .iter() + .map(|marker| format!("trace marker: string = {marker}")) + .collect() +} + +fn control_debug() -> Vec { + let read_group = [ + "positive", "high", "sibling", "positive", "high", "high", "positive", "high", + ]; + let mut expected = Vec::new(); + // Record, both enum variants, and both machine states each seed three + // invariants, then read raw/full, ancestor/suffix, exact, and sibling/full. + for _ in 0..5 { + expected.extend(marker_lines(&read_group)); + } + expected.extend(marker_lines(&[ + "text", "nonempty", "text", "long", "long", "nonempty", "nonempty", "nonempty", + ])); + expected.extend(marker_lines(&["positive", "high", "sibling"])); + expected.extend([ + "trace copied: app.High = pending(pending(12))".into(), + "trace once: app.High = pending(12)".into(), + "trace original: app.High = pending(pending(12))".into(), + ]); + expected.extend(marker_lines(&["positive", "high", "sibling"])); + expected.extend([ + "trace copied_event: app.High = pending(12)".into(), + "trace original_event: app.High = pending(12)".into(), + ]); + expected.extend(marker_lines(&["positive", "high", "sibling"])); + expected.extend([ + "trace copied_session: app.High = pending(pending(12))".into(), + "trace once_session: app.High = pending(12)".into(), + "trace original_session: app.High = pending(pending(12))".into(), + ]); + expected.extend(marker_lines(&[ + "positive", "high", "sibling", "positive", "high", + ])); + expected +} + +fn run_reflected_case( + name: &str, + text: &str, + stdout: &str, + message: Option<&str>, + debug: &[String], + suites: bool, +) { + run_reflected_form(name, text, stdout, message, debug, suites); + let piped = piped_getters(text); + assert_ne!( + piped, text, + "{name}: pipeline fixture must exercise a getter" + ); + run_reflected_form( + &format!("{name}_pipeline"), + &piped, + stdout, + message, + debug, + suites, + ); +} + +fn piped_getters(text: &str) -> String { + let mut piped = text.to_owned(); + for getter in [ + "type.field_value[Record, High]", + "type.variant_field_value[Event, High]", + "type.machine_field_value[Session, High]", + "type.field_value[Record, Positive]", + "type.variant_field_value[Event, Positive]", + "type.machine_field_value[Session, Positive]", + "type.field_value[Parcel, Long]", + "type.field_value[Parcel, NonEmpty]", + "type.variant_field_value[Packet, NonEmpty]", + "type.machine_field_value[Store, NonEmpty]", + "type.variant_field_value[Packet, list[int64]]", + "type.machine_field_value[Store, list[int64]]", + "type.field_value[Record, Risky]", + "type.field_value[Record, Text]", + "type.field_value[Record, NonEmpty]", + ] { + piped = piped.replace( + &format!("{getter}(view source, view field)"), + &format!("view source into view {getter}(view field)"), + ); + } + piped.replace( + "type.field_value[Record, High](view source_operand(view stdout, view source), view field_operand(view stdout))", + "source_operand(view stdout, view source) into view type.field_value[Record, High](view field_operand(view stdout))", + ) +} + +fn run_reflected_form( + name: &str, + text: &str, + stdout: &str, + message: Option<&str>, + debug: &[String], + suites: bool, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, text).unwrap(); + let runtime_debug_events = match (jett_driver::run_file_capture_outcome(&source), message) { + (Ok(expected), None) => { + assert_eq!(expected.stdout, stdout, "{name}"); + assert_eq!(debug_trace_lines(&expected.debug_events), debug, "{name}"); + expected.debug_events + } + (Err(expected), Some(message)) => { + assert_eq!(expected.message, message, "{name}"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + debug, + "{name}" + ); + expected.output.debug_events + } + (actual, message) => panic!("{name}: outcome {actual:?}, error {message:?}"), + }; + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("reflected_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push((binary, release)); + } + let mut suite_binaries = Vec::new(); + if suites { + let verify = directory.path().join("reflected_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify).unwrap(); + let property = directory.path().join("reflected_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property).unwrap(); + suite_binaries.extend([verify, property]); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(if message.is_some() { 71 } else { 0 }), + "{name}: {actual:?}" + ); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + let mut stderr = if release || debug.is_empty() { + String::new() + } else { + jett_driver::render_debug_events(&runtime_debug_events) + }; + if let Some(message) = message { + stderr.push_str(message); + stderr.push('\n'); + } + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } + for binary in suite_binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{name}: {actual:?}"); + assert!(actual.stdout.is_empty(), "{name}: {actual:?}"); + assert!(actual.stderr.is_empty(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_reflected_root_refinements_validate_selected_suffixes_and_preserve_owned_sources() { + run_reflected_case( + "reflected_root_values", + REFLECTED_CONTROLS, + concat!( + "record:12:12:12:12:12:12\n", + "event:first:12:12:12:12:12|mirror:12:12:12:12:12\n", + "machine:first:12:12:12:12:12|mirror:12:12:12:12:12\n", + "owned:Agent-λ🙂:Agent-λ🙂:3:2:2:3:2:2:2\n", + "pending:12|12|12\n", + "operands\nsource\nfield\noperands:12:12\n", + ), + None, + &control_debug(), + false, + ); +} + +#[test] +fn native_reflected_root_refinements_support_closed_comptime_and_native_suites() { + run_reflected_case( + "reflected_root_pure_values", + &pure_source(), + concat!( + "pure:12:12:12:12:12:12;first:12:12:12:12:12;first:12:12:12:12:12;", + "Agent-λ🙂:Agent-λ🙂:3:2:2:3:2:2:2|", + "12:12:12:12:12:12;mirror:12:12:12:12:12;mirror:12:12:12:12:12;", + "Agent-λ🙂:Agent-λ🙂:3:2:2:3:2:2:2\n", + ), + None, + &[], + true, + ); +} + +struct FailureCase { + name: &'static str, + setup: &'static str, + read: &'static str, + message: &'static str, + markers: &'static [&'static str], +} + +#[test] +fn native_reflected_root_refinement_failures_preserve_selector_precedence_and_cleanup() { + let cases: &[FailureCase] = &[ + FailureCase { + name: "record_base", + setup: r#" Record source = Record(raw: -1, text: "", values: list.new[int64]()) + TypeField field = record_field(0)"#, + read: r#" High returned = type.field_value[Record, High](view source, view field)"#, + message: "runtime error: refinement type constraint failed for 'app.Positive'", + markers: &["positive"], + }, + FailureCase { + name: "enum_ancestor", + setup: r#" Positive positive = 7 handle error: + return nothing + Event source = Event.first(positive) + TypeField field = event_field(view source)"#, + read: r#" High returned = type.variant_field_value[Event, High](view source, view field)"#, + message: "runtime error: refinement type constraint failed for 'app.High'", + markers: &["positive", "high"], + }, + FailureCase { + name: "machine_sibling", + setup: r#" Sibling sibling = 7 handle error: + return nothing + Session source = Session(first, sibling) + TypeField field = session_field(view source)"#, + read: r#" High returned = type.machine_field_value[Session, High](view source, view field)"#, + message: "runtime error: refinement type constraint failed for 'app.High'", + markers: &["sibling", "positive", "high"], + }, + FailureCase { + name: "predicate_error", + setup: r#" Record source = Record(raw: -1, text: "", values: list.new[int64]()) + TypeField field = record_field(0)"#, + read: r#" Risky returned = type.field_value[Record, Risky](view source, view field)"#, + message: "runtime error: error evaluating refinement constraint for 'app.Risky': list.__remove_at: index -1 out of bounds", + markers: &["risky"], + }, + FailureCase { + name: "empty_text", + setup: r#" Record source = Record(raw: 7, text: "", values: list.new[int64]()) + TypeField field = record_field(1)"#, + read: r#" Text returned = type.field_value[Record, Text](view source, view field)"#, + message: "runtime error: refinement type constraint failed for 'app.Text'", + markers: &["text"], + }, + FailureCase { + name: "empty_list", + setup: r#" Record source = Record(raw: 7, text: "", values: list.new[int64]()) + TypeField field = record_field(2)"#, + read: r#" NonEmpty returned = type.field_value[Record, NonEmpty](view source, view field)"#, + message: "runtime error: refinement type constraint failed for 'app.NonEmpty'", + markers: &["nonempty"], + }, + FailureCase { + name: "pending_one", + setup: r#" Record source = Record(raw: run 7, text: "", values: list.new[int64]()) + TypeField field = record_field(0)"#, + read: r#" Positive returned = type.field_value[Record, Positive](view source, view field)"#, + message: "runtime error: error evaluating refinement constraint for 'app.Positive': unsupported binary operation: pending(7) Gt 0", + markers: &["positive"], + }, + FailureCase { + name: "pending_two", + setup: r#" Session source = Session(second, run run 7) + TypeField field = session_field(view source)"#, + read: r#" Positive returned = type.machine_field_value[Session, Positive](view source, view field)"#, + message: "runtime error: error evaluating refinement constraint for 'app.Positive': unsupported binary operation: pending(pending(7)) Gt 0", + markers: &["positive"], + }, + FailureCase { + name: "wrong_owner", + setup: r#" Record source = Record(raw: 7, text: "", values: list.new[int64]()) + TypeField field = other_field()"#, + read: r#" Positive returned = type.field_value[Record, Positive](view source, view field)"#, + message: "runtime error: type.field_value: field metadata belongs to 'app.Other', expected 'app.Record'", + markers: &[], + }, + FailureCase { + name: "wrong_request", + setup: r#" Record source = Record(raw: 7, text: "", values: list.new[int64]()) + TypeField field = record_field(1)"#, + read: r#" Positive returned = type.field_value[Record, Positive](view source, view field)"#, + message: "runtime error: type.field_value: field 'text' has type 'string', requested 'app.Positive'", + markers: &[], + }, + FailureCase { + name: "wrong_enum_member", + setup: r#" Positive positive = 7 handle error: + return nothing + Event prior = Event.first(positive) + Event source = Event.second(7) + TypeField field = event_field(view prior)"#, + read: r#" High returned = type.variant_field_value[Event, High](view source, view field)"#, + message: "runtime error: type.variant_field_value: field metadata belongs to 'app.Event.first', expected 'app.Event.second'", + markers: &["positive"], + }, + FailureCase { + name: "wrong_machine_member", + setup: r#" Sibling sibling = 7 handle error: + return nothing + Session prior = Session(first, sibling) + Session source = Session(second, 7) + TypeField field = session_field(view prior)"#, + read: r#" High returned = type.machine_field_value[Session, High](view source, view field)"#, + message: "runtime error: type.machine_field_value: field metadata belongs to 'app.Session.first', expected 'app.Session.second'", + markers: &["sibling"], + }, + FailureCase { + name: "pending_selector", + setup: r#" Session source = Session(second, 7) + TypeField field = run run session_field(view source)"#, + read: r#" High returned = type.machine_field_value[Session, High](view source, view field)"#, + message: "runtime error: type.machine_field_value: second argument must be TypeField, got pending(pending(TypeField(index: 0, owner_type: app.Session, owner_member: some(second), name: value, type_name: int64, kind: primitive, kind_tag: TypeKind.primitive_type, serialize_name: value, has_secret: false, type_info: TypeInfo(type_name: int64, kind: primitive, kind_tag: TypeKind.primitive_type, primitive_tag: some(TypePrimitive.int64_type), has_secret: false, args: list()))))", + markers: &[], + }, + ]; + for case in cases { + let source = REFLECTED_FAILURE + .replace("__SETUP__", case.setup) + .replace("__READ__", case.read); + run_reflected_case( + case.name, + &source, + "before\n", + Some(case.message), + &marker_lines(case.markers), + false, + ); + } +} + +// TypeField is compiler-owned: source cannot forge a metadata index, field +// name, or declared type. Corrupt-plan coverage belongs in HIR/MIR tests. +// These linked cases preserve the original compatibility admission domain; +// changed nested-container requests remain outside the native parity claim. + +const REFLECTED_VALUE_OBSERVERS: &str = r#"namespace app +struct Holder[T]: + value: T +type Positive = int64 where value > 0 +type NumberList = list[int64] +enum Event: + empty = 17 + content(text: string, values: list[optional[int64]]) +machine Session: + states: + empty + content(text: string, values: list[optional[int64]]) + transitions: + empty to content +function field_summary(view fields: list[TypeField]) returns string: + mutable list[string] parts = list.new[string]() + for field in view fields: + string member = clone field.owner_member handle: + default "none" + string part = "{field.index}:{field.owner_type}.{member}:{field.name}:{field.type_info.type_name}" + parts = list.append[string](parts, part) + return string.join(parts, ";") +function variant_summary(value: TypeVariant) returns string: + string fields = field_summary(view value.fields) + return "{value.index}:{value.owner_type}:{value.name}:{value.discriminant}:{value.has_secret}:[{fields}]" +function state_summary(value: TypeMachineState) returns string: + string fields = field_summary(view value.fields) + return "{value.index}:{value.owner_type}:{value.name}:{value.has_secret}:[{fields}]" +function info_summary(value: TypeInfo) returns string: + int64 count = list.length[TypeInfo](view value.args) + return "{value.type_name}:{value.kind}:{value.has_secret}:{count}" +function variant_report(view source: Event) returns string: + TypeVariant first = type.variant_value[Event](view source) + list[TypeField] consumed = first.fields + int64 count = list.length[TypeField](view consumed) + TypeVariant second = type.variant_value[Event](view source) + return "{count}|{variant_summary(second)}" +function state_report(view source: Session) returns string: + TypeMachineState first = type.machine_state_value[Session](view source) + list[TypeField] consumed = first.fields + int64 count = list.length[TypeField](view consumed) + TypeMachineState second = type.machine_state_value[Session](view source) + return "{count}|{state_summary(second)}" +function narrowed_report(view source: Session at content) returns string: + TypeMachineState selected = type.machine_state_value[Session at content](view source) + return state_summary(selected) +function map_report(index: int64) returns string: + TypeInfo selected = type.arg[map[string, list[optional[int64]]]](index) + return info_summary(selected) +function holder_report(index: int64) returns string: + TypeInfo selected = type.arg[Holder[list[optional[int64]]]](index) + return info_summary(selected) +function refinement_report(index: int64) returns string: + TypeInfo selected = type.arg[Positive](index) + return info_summary(selected) +function report_event(filled: bool) returns Event: + if filled: + return Event.content("owned", list(some(7), none)) + return Event.empty +function report_session(filled: bool) returns Session: + if filled: + return Session(content, "owned", list(some(7), none)) + return Session(empty) +function report(filled: bool) returns string: + Event event = report_event(filled) + Session session = report_session(filled) + string variant = variant_report(view event) + string state = state_report(view session) + return "{variant}|{state}|{map_report(0)}|{map_report(1)}|{holder_report(0)}|{refinement_report(0)}" +function narrowed_ready() returns string: + Session at content source = Session(content, "owned", list(some(7), none)) + return narrowed_report(view source) +function event_operand(view stdout: Stdout, view source: Event) returns Event: + Stdout.write(view stdout, "event operand\n") + return clone source +function state_operand(view stdout: Stdout, view source: Session at content) returns Session at content: + Stdout.write(view stdout, "state operand\n") + return clone source +function index_operand(view stdout: Stdout, index: int64) returns int64: + Stdout.write(view stdout, "index operand\n") + return index +function consume_variant(value: TypeVariant, view stdout: Stdout) returns string: + Stdout.write(view stdout, "variant consumer\n") + return value.name +function consume_state(value: TypeMachineState, view stdout: Stdout) returns string: + Stdout.write(view stdout, "state consumer\n") + return value.name +function consume_info(value: TypeInfo, view stdout: Stdout) returns string: + Stdout.write(view stdout, "info consumer\n") + return value.type_name +function operand_report(view stdout: Stdout) returns string: + Event source = Event.content("owned", list(some(7), none)) + Session at content session = Session(content, "owned", list(some(7), none)) + string variant = type.variant_value[Event](view event_operand(view stdout, view source)) into consume_variant(view stdout) + string state = type.machine_state_value[Session at content](view state_operand(view stdout, view session)) into consume_state(view stdout) + string info = type.arg[map[string, list[optional[int64]]]](index_operand(view stdout, 1)) into consume_info(view stdout) + return "{variant}:{state}:{info}|{variant_report(view source)}|{narrowed_report(view session)}" +"#; + +const VALUE_EMPTY: &str = concat!( + "0|0:app.Event:empty:17:false:[]|0|0:app.Session:empty:false:[]|", + "string:primitive:false:0|list[optional[int64]]:list:false:1|", + "list[optional[int64]]:list:false:1|int64:primitive:false:0" +); +const VALUE_FIELDS_EVENT: &str = concat!( + "0:app.Event.content:text:string;", + "1:app.Event.content:values:list[optional[int64]]" +); +const VALUE_FIELDS_STATE: &str = concat!( + "0:app.Session.content:text:string;", + "1:app.Session.content:values:list[optional[int64]]" +); + +fn value_content() -> String { + format!( + "2|1:app.Event:content:18:false:[{VALUE_FIELDS_EVENT}]|2|1:app.Session:content:false:[{VALUE_FIELDS_STATE}]|string:primitive:false:0|list[optional[int64]]:list:false:1|list[optional[int64]]:list:false:1|int64:primitive:false:0" + ) +} + +fn value_narrowed() -> String { + format!("1:app.Session:content:false:[{VALUE_FIELDS_STATE}]") +} + +fn piped_value_observers(text: &str) -> String { + let mut piped = text.to_owned(); + for (direct, pipeline) in [ + ( + "type.variant_value[Event](view source)", + "source into view type.variant_value[Event]()", + ), + ( + "type.machine_state_value[Session](view source)", + "source into view type.machine_state_value[Session]()", + ), + ( + "type.machine_state_value[Session at content](view source)", + "source into view type.machine_state_value[Session at content]()", + ), + ( + "type.arg[map[string, list[optional[int64]]]](index)", + "index into type.arg[map[string, list[optional[int64]]]]()", + ), + ( + "type.arg[Holder[list[optional[int64]]]](index)", + "index into type.arg[Holder[list[optional[int64]]]]()", + ), + ( + "type.arg[Positive](index)", + "index into type.arg[Positive]()", + ), + ( + "type.variant_value[Event](view event_operand(view stdout, view source))", + "event_operand(view stdout, view source) into view type.variant_value[Event]()", + ), + ( + "type.machine_state_value[Session at content](view state_operand(view stdout, view session))", + "state_operand(view stdout, view session) into view type.machine_state_value[Session at content]()", + ), + ( + "type.arg[map[string, list[optional[int64]]]](index_operand(view stdout, 1))", + "index_operand(view stdout, 1) into type.arg[map[string, list[optional[int64]]]]()", + ), + ( + "type.variant_value[Event](view paused)", + "paused into view type.variant_value[Event]()", + ), + ( + "type.machine_state_value[Session](view paused)", + "paused into view type.machine_state_value[Session]()", + ), + ( + "type.machine_state_value[Session at empty](view paused)", + "paused into view type.machine_state_value[Session at empty]()", + ), + ] { + piped = piped.replace(direct, pipeline); + } + for ty in ["list[int64]", "map[string, int64]", "NumberList", "int64"] { + piped = piped.replace( + &format!("type.arg[{ty}](index_operand(view stdout, index))"), + &format!("index_operand(view stdout, index) into type.arg[{ty}]()"), + ); + } + piped +} + +fn run_value_observer_case( + name: &str, + source: &str, + stdout: &str, + message: Option<&str>, + suites: bool, +) { + run_reflected_form(name, source, stdout, message, &[], suites); + let piped = piped_value_observers(source); + assert_ne!( + piped, source, + "{name}: fixture must exercise an observer pipeline" + ); + run_reflected_form( + &format!("{name}_pipeline"), + &piped, + stdout, + message, + &[], + suites, + ); +} + +#[test] +fn native_reflected_value_observer_pipelines_preserve_metadata_ownership_and_operand_order() { + let source = format!( + "{REFLECTED_VALUE_OBSERVERS}{}", + r#" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "empty:{report(false)}\ncontent:{report(true)}\nnarrowed:{narrowed_ready()}\n") + Stdout.write(view stdout, "operands\n") + string observed = operand_report(view stdout) + Stdout.write(view stdout, "observed:{observed}\n") +"# + ); + let stdout = format!( + "empty:{VALUE_EMPTY}\ncontent:{}\nnarrowed:{}\noperands\nevent operand\nvariant consumer\nstate operand\nstate consumer\nindex operand\ninfo consumer\nobserved:content:content:list[optional[int64]]|2|1:app.Event:content:18:false:[{VALUE_FIELDS_EVENT}]|{}\n", + value_content(), + value_narrowed(), + value_narrowed() + ); + run_value_observer_case("reflected_value_observers", &source, &stdout, None, false); +} + +#[test] +fn native_reflected_value_observer_pipelines_support_closed_comptime_and_native_suites() { + let (pure, _) = REFLECTED_VALUE_OBSERVERS + .split_once("function event_operand(") + .unwrap(); + let source = format!( + "{pure}{}", + r#" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(true) + Stdout.write(view stdout, "pure:{report(false)}|{baked}|{narrowed_ready()}\n") +verify reflected_value_metadata: + assert report(false) == comptime report(false) + assert report(true) == comptime report(true) + assert narrowed_ready() == comptime narrowed_ready() +property reflected_value_metadata_trials: + given filled: bool + string runtime_value = report(filled) + mutable string expected = comptime report(false) + if filled: + expected = comptime report(true) + assert runtime_value == expected +"# + ); + let stdout = format!( + "pure:{VALUE_EMPTY}|{}|{}\n", + value_content(), + value_narrowed() + ); + run_value_observer_case( + "reflected_value_observers_pure", + &source, + &stdout, + None, + true, + ); +} + +#[test] +fn native_reflected_value_observer_pipeline_errors_keep_pending_depth_and_fail_before_consumers() { + for (name, ty, index, message) in [ + ( + "negative", + "list[int64]", + "-1", + "runtime error: type.arg expects a non-negative int64 index, got -1", + ), + ( + "out_of_range", + "map[string, int64]", + "2", + "runtime error: type.arg index 2 is out of range for type 'map[string, int64]'", + ), + ( + "alias_range", + "NumberList", + "1", + "runtime error: type.arg index 1 is out of range for type 'app.NumberList'", + ), + ( + "zero_args", + "int64", + "0", + "runtime error: type.arg index 0 is out of range for type 'int64'", + ), + ( + "pending_index", + "list[int64]", + "run 0", + "runtime error: type.arg expects a non-negative int64 index, got pending(0)", + ), + ( + "nested_pending_index", + "map[string, int64]", + "run run 0", + "runtime error: type.arg expects a non-negative int64 index, got pending(pending(0))", + ), + ] { + let source = format!( + "{REFLECTED_VALUE_OBSERVERS}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n int64 index = {index}\n string observed = type.arg[{ty}](index_operand(view stdout, index)) into consume_info(view stdout)\n Stdout.write(view stdout, \"after:{{observed}}\\n\")\n" + ); + run_value_observer_case( + &format!("reflected_value_{name}"), + &source, + "before\nindex operand\n", + Some(message), + false, + ); + } + for (name, setup, read, message) in [ + ( + "pending_enum", + "Event paused = run Event.empty", + "type.variant_value[Event](view paused) into consume_variant(view stdout)", + "runtime error: type.variant_value: expected enum value for 'app.Event', got pending(app.Event.empty)", + ), + ( + "pending_machine", + "Session paused = run run Session(empty)", + "type.machine_state_value[Session](view paused) into consume_state(view stdout)", + "runtime error: type.machine_state_value: expected machine value for 'app.Session', got pending(pending(app.Session@empty))", + ), + ( + "pending_narrowed", + "Session at empty paused = run Session(empty)", + "type.machine_state_value[Session at empty](view paused) into consume_state(view stdout)", + "runtime error: type.machine_state_value: expected machine value for 'app.Session at empty', got pending(app.Session@empty)", + ), + ] { + let source = format!( + "{REFLECTED_VALUE_OBSERVERS}function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"before\\n\")\n {setup}\n string observed = {read}\n Stdout.write(view stdout, \"after:{{observed}}\\n\")\n" + ); + run_value_observer_case( + &format!("reflected_value_{name}"), + &source, + "before\n", + Some(message), + false, + ); + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett b/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/01_connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett b/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett new file mode 100644 index 00000000..d4318743 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/02_partial_actual_failure.jett @@ -0,0 +1,11 @@ +namespace app +function consume(token: resource_probe.TestHandle, label: int64) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + consume(token, resource_probe.terminal_label()) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett b/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett new file mode 100644 index 00000000..c4f7dffc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/03_implicit_drop.jett @@ -0,0 +1,6 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett b/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett new file mode 100644 index 00000000..b0c6ffdd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/04_live_replace.jett @@ -0,0 +1,11 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2401) handle error: + return nothing + token = resource_probe.create(view net, 2402) handle error: + return nothing + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett b/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett new file mode 100644 index 00000000..90c458f7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/05_self_rebind.jett @@ -0,0 +1,8 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2421) handle error: + return nothing + token = token + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett b/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett new file mode 100644 index 00000000..3a169c38 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/06_replace_closed.jett @@ -0,0 +1,10 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2411) handle error: + return nothing + resource_probe.close(token) + token = resource_probe.create(view net, 2412) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett b/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett new file mode 100644 index 00000000..701fd3c1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/07_rhs_domain_failure.jett @@ -0,0 +1,15 @@ +namespace app +function observe_previous(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2431) handle error: + return nothing + token = resource_probe.create(view net, 2432) handle error: + observe_previous(view net, view token) + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett b/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett new file mode 100644 index 00000000..49051902 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/08_rhs_ordinary_failure.jett @@ -0,0 +1,9 @@ +namespace app +function main(net: Network) returns nothing: + use resource_probe + mutable resource_probe.TestHandle token = resource_probe.create(view net, 2441) handle error: + return nothing + token = resource_probe.create(view net, resource_probe.terminal_label()) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett b/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett new file mode 100644 index 00000000..97660978 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 502) handle error: + return nothing + accept_staged(token: token, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett b/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett new file mode 100644 index 00000000..a56e1c27 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/10_later_handle_original.jett @@ -0,0 +1,24 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 501) handle error: + return nothing + accept_staged(token: stage_owned(view net, token), marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett b/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett new file mode 100644 index 00000000..0e951718 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/11_later_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 503) handle error: + return nothing + accept_staged(token: token, marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett b/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett new file mode 100644 index 00000000..b0e4afff --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett @@ -0,0 +1,22 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function observe_retained(view net: Network, view token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 retained = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 505) handle error: + return nothing + accept_staged(token: view token, marker: fail_marker() handle error: + observe_retained(view net, view token) + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett b/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett new file mode 100644 index 00000000..c7bd4031 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 506) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett b/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett new file mode 100644 index 00000000..fb4e94f8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/14_first_handle_success.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function successful_marker() returns result[int64, string]: + return ok(7) +export function main(net: Network) returns nothing: + use resource_probe + accept_staged(token: resource_probe.create(view net, 507) handle error: + return nothing + , marker: successful_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett b/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett new file mode 100644 index 00000000..3e8f5712 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/15_later_handle_two_owned_views.jett @@ -0,0 +1,20 @@ +namespace app +function accept_staged(marker: int64, view first: resource_probe.TestHandle, view second: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed_first = resource_probe.borrow(view net, view first) handle error: + return nothing + int64 observed_second = resource_probe.borrow(view net, view second) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first = resource_probe.create(view net, 511) handle error: + return nothing + resource_probe.TestHandle second = resource_probe.create(view net, 512) handle error: + return nothing + accept_staged(first: first, second: second, marker: fail_marker() handle error: + return nothing + , net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett b/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett new file mode 100644 index 00000000..e6e4de00 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/16_later_handle_return_other_owner.jett @@ -0,0 +1,27 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +function observe_return(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function return_retained(view net: Network, token: resource_probe.TestHandle, retained: resource_probe.TestHandle) returns resource_probe.TestHandle: + accept_staged(token: token, marker: fail_marker() handle error: + return observe_return(view net, retained) + , net: view net) + return retained +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 521) handle error: + return nothing + resource_probe.TestHandle retained = resource_probe.create(view net, 522) handle error: + return nothing + resource_probe.TestHandle returned = return_retained(view net, token, retained) + resource_probe.close(returned) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett b/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett new file mode 100644 index 00000000..bf16a7ea --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/17_later_handle_default.jett @@ -0,0 +1,16 @@ +namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + use resource_probe + if marker == 7: + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + return nothing + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 531) handle error: + return nothing + accept_staged(token: token, marker:(fail_marker() handle error: default 7), net: view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett b/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett new file mode 100644 index 00000000..b75d5af7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/18_indirect_named_close.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett b/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett new file mode 100644 index 00000000..eaa7e5a8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/19_indirect_named_terminal_failure.jett @@ -0,0 +1,16 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = wrong_target + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett b/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett new file mode 100644 index 00000000..c0aa00e0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/20_indirect_named_alias.jett @@ -0,0 +1,17 @@ +namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + use resource_probe + int64 marker = resource_probe.terminal_label() + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = close_selected + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett b/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett new file mode 100644 index 00000000..969f6727 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/21_indirect_namespace_close.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + resource_probe.TestHandle token = resource_probe.create(view net, 711) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett b/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett new file mode 100644 index 00000000..3745559a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/22_indirect_namespace_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.close + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 711) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett new file mode 100644 index 00000000..0b02ae6f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/23_returned_hook_close.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett b/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett new file mode 100644 index 00000000..bc1f3ae8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/24_returned_hook_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett b/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett new file mode 100644 index 00000000..41ac9d49 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/25_returned_hook_relay.jett @@ -0,0 +1,11 @@ +namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = relay() + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett b/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett new file mode 100644 index 00000000..a3f62a81 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/26_returned_hook_unused.jett @@ -0,0 +1,5 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett new file mode 100644 index 00000000..60fe237d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] create_selected = resource_probe.create_descriptor() + resource_probe.TestHandle token = create_selected(view net, 731) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett b/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett new file mode 100644 index 00000000..bb06d7a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett @@ -0,0 +1,12 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, view resource_probe.TestHandle) returns result[int64, string] borrow_selected = resource_probe.borrow_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 741) handle error: + return nothing + int64 observed = borrow_selected(view net, view token) handle error: + default 0 + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett new file mode 100644 index 00000000..2a15c925 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett @@ -0,0 +1,7 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 751) handle error: + return nothing + resource_probe.descriptor()(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett new file mode 100644 index 00000000..4f9ab427 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett @@ -0,0 +1,16 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett b/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett new file mode 100644 index 00000000..489613a5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett @@ -0,0 +1,16 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, true)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett b/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett new file mode 100644 index 00000000..5d8a51a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett @@ -0,0 +1,17 @@ +namespace app +function argument_label(view net: Network, view marker: resource_probe.TestHandle) returns int64: + use resource_probe + int64 label = resource_probe.borrow(view net, view marker) handle error: + default 0 + int64 ignored = resource_probe.terminal_label() + return label +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 742) handle error: + return nothing + resource_probe.TestHandle marker = resource_probe.create(view net, 744) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, argument_label(view net, view marker)) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett new file mode 100644 index 00000000..c16c8295 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + int64 label = comptime 761 + resource_probe.TestHandle token = resource_probe.create(view net, label) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett b/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett new file mode 100644 index 00000000..e8fe4b75 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] acquire = comptime resource_probe.create_descriptor() + resource_probe.TestHandle token = acquire(view net, 771) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett b/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett new file mode 100644 index 00000000..598eb712 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett @@ -0,0 +1,12 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(view Network, view resource_probe.TestHandle) returns result[int64, string] inspect = comptime resource_probe.borrow_descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 781) handle error: + return nothing + int64 observed = inspect(view net, view token) handle error: + default 0 + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett b/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett new file mode 100644 index 00000000..820e6f86 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett @@ -0,0 +1,9 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = dispose + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + alias(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett b/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett new file mode 100644 index 00000000..d78724a0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett @@ -0,0 +1,11 @@ +namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime relay() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett b/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett new file mode 100644 index 00000000..7a9016c7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett @@ -0,0 +1,5 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett b/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett new file mode 100644 index 00000000..e5e15a6c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett @@ -0,0 +1,6 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + return(comptime resource_probe.descriptor())(token) diff --git a/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett b/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett new file mode 100644 index 00000000..bf7c520e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett @@ -0,0 +1,8 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] empty = comptime resource_probe.empty() + resource_probe.TestHandle token = empty handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett b/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett new file mode 100644 index 00000000..8ad0264c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett @@ -0,0 +1,13 @@ +namespace app +function choose(flag: bool) returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + if flag: + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose(true) + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett b/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett new file mode 100644 index 00000000..42f9ee20 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett @@ -0,0 +1,14 @@ +namespace app +function choose[T](flag: bool) returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + string type_label = type.name[T]() + if flag and type_label == "int64": + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose[int64](true) + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett b/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett new file mode 100644 index 00000000..5435534b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett @@ -0,0 +1,13 @@ +namespace app +function choose() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + comptime type Inner = type.info[int64](): + return resource_probe.descriptor() + return resource_probe.descriptor() +export function main(net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime choose() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett b/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett new file mode 100644 index 00000000..7f2b97b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett @@ -0,0 +1,12 @@ +namespace app +function selector() returns bool: + return comptime false +export function main(net: Network) returns nothing: + use resource_probe + bool stop = selector() + if stop: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett b/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett new file mode 100644 index 00000000..8a7bb79a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett @@ -0,0 +1,18 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett b/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett new file mode 100644 index 00000000..91588893 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett @@ -0,0 +1,15 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett b/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett new file mode 100644 index 00000000..fcdb9eab --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett b/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett new file mode 100644 index 00000000..5eae8bd4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett b/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett new file mode 100644 index 00000000..21dac3ee --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett b/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett new file mode 100644 index 00000000..d155b91b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett b/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett new file mode 100644 index 00000000..cc4e815e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett @@ -0,0 +1,20 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett b/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett new file mode 100644 index 00000000..cabc7ca1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 831) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett b/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett new file mode 100644 index 00000000..b8015fb8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, filler: resource_probe.TestHandle, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + resource_probe.close(filler) + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + default 0 + resource_probe.close(filler) + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 851) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, outcome: view held, filler: resource_probe.create(view net, 852) handle error: + return nothing + ) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett b/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett new file mode 100644 index 00000000..3ae29ba7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, filler: resource_probe.TestHandle, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + resource_probe.close(filler) + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + default 0 + resource_probe.close(filler) + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 851) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, outcome: held, filler: resource_probe.create(view net, 852) handle error: + return nothing + ) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett new file mode 100644 index 00000000..9509c21a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 861) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett new file mode 100644 index 00000000..b37bfe80 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett @@ -0,0 +1,17 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + resource_probe.TestHandle token = view((view outcome) handle error: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 861) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett b/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett new file mode 100644 index 00000000..7ce1d945 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + return resource_probe.borrow(view net, view token) handle error: + return 0 + return 0 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett new file mode 100644 index 00000000..8c2ce807 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 disabled = observe_optional(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_optional(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett new file mode 100644 index 00000000..b22832f0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett @@ -0,0 +1,24 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 disabled = observe_optional(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_optional(view net, view held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett new file mode 100644 index 00000000..71713278 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle disabled_owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] disabled_held = some(disabled_owner) + int64 disabled = observe_optional(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle owner = resource_probe.create(view net, 872) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett new file mode 100644 index 00000000..4a36e386 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett @@ -0,0 +1,22 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] disabled_held = none + int64 disabled = observe_optional(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett new file mode 100644 index 00000000..1e4dfcc5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 disabled = observe_result(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_result(view net, view held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett new file mode 100644 index 00000000..0eb5ebd5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 disabled = observe_result(view net, view held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + int64 observed = observe_result(view net, view held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett new file mode 100644 index 00000000..34a46a77 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle disabled_owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] disabled_held = ok(disabled_owner) + int64 disabled = observe_result(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle owner = resource_probe.create(view net, 872) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held, true) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett new file mode 100644 index 00000000..73d50356 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett @@ -0,0 +1,24 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string], enabled: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] disabled_held = fail("empty") + int64 disabled = observe_result(view net, disabled_held, false) + if disabled != 0: + int64 ignored = resource_probe.terminal_label() + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held, true) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett new file mode 100644 index 00000000..49086a77 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett @@ -0,0 +1,23 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett new file mode 100644 index 00000000..11b412c3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett @@ -0,0 +1,21 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett new file mode 100644 index 00000000..d867e34b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett @@ -0,0 +1,20 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett new file mode 100644 index 00000000..519c73f2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett @@ -0,0 +1,18 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett new file mode 100644 index 00000000..3c2b76ba --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett @@ -0,0 +1,27 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett new file mode 100644 index 00000000..4be5b3df --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett new file mode 100644 index 00000000..0f5126af --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett @@ -0,0 +1,22 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett new file mode 100644 index 00000000..e4894503 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett @@ -0,0 +1,20 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett b/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett new file mode 100644 index 00000000..dda12c17 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett @@ -0,0 +1,26 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett b/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett new file mode 100644 index 00000000..6693b253 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett @@ -0,0 +1,24 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle: + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett b/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett new file mode 100644 index 00000000..82128d9e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett @@ -0,0 +1,23 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett b/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett new file mode 100644 index 00000000..edf4f740 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett @@ -0,0 +1,21 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + optional[resource_probe.TestHandle] held = none + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett b/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett new file mode 100644 index 00000000..5af3cd4f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett b/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett new file mode 100644 index 00000000..fb1634ca --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett @@ -0,0 +1,28 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = held handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + resource_probe.close(token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett b/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett new file mode 100644 index 00000000..3f019392 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett @@ -0,0 +1,25 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett b/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett new file mode 100644 index 00000000..fcadaaf2 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett @@ -0,0 +1,23 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 remaining = 2 + mutable int64 total = 0 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + result[resource_probe.TestHandle, string] held = fail("empty") + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett new file mode 100644 index 00000000..2ad34f70 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett @@ -0,0 +1,27 @@ +namespace app +function exercise_optional(view net: Network, label: int64, enabled: bool) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if enabled: + if observed != 17: + int64 ignored = resource_probe.terminal_label() + else: + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing +export function main(net: Network) returns nothing: + exercise_optional(view net, 871, false) + exercise_optional(view net, 872, true) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett new file mode 100644 index 00000000..26280998 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett @@ -0,0 +1,29 @@ +namespace app +function exercise_result(view net: Network, label: int64, enabled: bool) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, label) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 observed = 0 + if enabled: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if enabled: + if observed != 17: + int64 ignored = resource_probe.terminal_label() + else: + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing +export function main(net: Network) returns nothing: + exercise_result(view net, 871, false) + exercise_result(view net, 872, true) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett new file mode 100644 index 00000000..58a532b6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett @@ -0,0 +1,19 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett new file mode 100644 index 00000000..c2500a8b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett new file mode 100644 index 00000000..14c24282 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett @@ -0,0 +1,22 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett new file mode 100644 index 00000000..4ea52874 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett @@ -0,0 +1,24 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett b/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett new file mode 100644 index 00000000..255c37b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett @@ -0,0 +1,38 @@ +namespace app +function observe_optional(view net: Network, view first_outcome: optional[resource_probe.TestHandle], view second_outcome: optional[resource_probe.TestHandle], choose_first: bool) returns int64: + use resource_probe + mutable int64 observed = 0 + if choose_first: + resource_probe.TestHandle token = view((view first_outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + else: + resource_probe.TestHandle token = view((view second_outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first_owner = resource_probe.create(view net, 891) handle error: + return nothing + optional[resource_probe.TestHandle] first_held = some(first_owner) + resource_probe.TestHandle second_owner = resource_probe.create(view net, 892) handle error: + return nothing + optional[resource_probe.TestHandle] second_held = some(second_owner) + int64 first_observed = observe_optional(view net, view first_held, view second_held, true) + if first_observed != 17: + int64 ignored = resource_probe.terminal_label() + int64 second_observed = observe_optional(view net, view first_held, view second_held, false) + if second_observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle first_token = first_held handle: + return nothing + resource_probe.close(first_token) + resource_probe.TestHandle second_token = second_held handle: + return nothing + resource_probe.close(second_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett b/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett new file mode 100644 index 00000000..92a58600 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett @@ -0,0 +1,40 @@ +namespace app +function observe_result(view net: Network, view first_outcome: result[resource_probe.TestHandle, string], view second_outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Left = type.info[int64](): + resource_probe.TestHandle token = view((view first_outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + return 0 + comptime type Right = type.info[int64](): + resource_probe.TestHandle token = view((view second_outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 current = resource_probe.borrow(view net, view token) handle error: + return 0 + observed = observed + current + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle first_owner = resource_probe.create(view net, 891) handle error: + return nothing + result[resource_probe.TestHandle, string] first_held = ok(first_owner) + resource_probe.TestHandle second_owner = resource_probe.create(view net, 892) handle error: + return nothing + result[resource_probe.TestHandle, string] second_held = ok(second_owner) + int64 observed = observe_result(view net, view first_held, view second_held) + if observed != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle first_token = first_held handle error: + return nothing + resource_probe.close(first_token) + resource_probe.TestHandle second_token = second_held handle error: + return nothing + resource_probe.close(second_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett new file mode 100644 index 00000000..dbb2e832 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett new file mode 100644 index 00000000..68e6a392 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett @@ -0,0 +1,24 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 observed = 0 + comptime type Inner = type.info[int64](): + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + return observed +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett new file mode 100644 index 00000000..38337fcb --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett @@ -0,0 +1,25 @@ +namespace app +function observe_optional(view net: Network, view outcome: optional[resource_probe.TestHandle]) returns int64: + use resource_probe + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle: + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + int64 observed = observe_optional(view net, held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett new file mode 100644 index 00000000..aaa66729 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett @@ -0,0 +1,30 @@ +namespace app +function observe_result(view net: Network, view outcome: result[resource_probe.TestHandle, string]) returns int64: + use resource_probe + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view outcome) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return 0 + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + if error != "borrow sentinel": + int64 ignored = resource_probe.terminal_label() + default 0 + total = total + observed + remaining = remaining - 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + int64 observed = observe_result(view net, view held) + if observed != 0: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett b/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett new file mode 100644 index 00000000..f80726df --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett @@ -0,0 +1,22 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + remaining = remaining - 1 + break + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett b/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett new file mode 100644 index 00000000..6cf316dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett @@ -0,0 +1,25 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + remaining = remaining - 1 + continue + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett new file mode 100644 index 00000000..779604c5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett new file mode 100644 index 00000000..8cf6020a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett @@ -0,0 +1,21 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + break + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett new file mode 100644 index 00000000..853e8316 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett @@ -0,0 +1,24 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + result[resource_probe.TestHandle, string] held = ok(owner) + mutable int64 total = 0 + list[int64] phases = list(1, 2) + for phase in phases: + resource_probe.TestHandle token = view((view held) handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + continue + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle error: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett new file mode 100644 index 00000000..8092050a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett @@ -0,0 +1,30 @@ +namespace app +enum Route: + first + second +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + Route route = Route.first + match route: + first: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + second: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett new file mode 100644 index 00000000..23246401 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett @@ -0,0 +1,30 @@ +namespace app +enum Route: + first + second +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 observed = 0 + Route route = Route.second + match route: + first: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + second: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if observed != 17: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett new file mode 100644 index 00000000..95b3591e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett @@ -0,0 +1,29 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] first_phases = list(1, 2) + for phase in first_phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + list[int64] second_phases = list(3, 4) + for phase in second_phases: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 68: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett b/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett new file mode 100644 index 00000000..bf2d8684 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett @@ -0,0 +1,29 @@ +namespace app +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + list[int64] outer_phases = list(1, 2) + for outer_phase in outer_phases: + resource_probe.TestHandle outer_token = view((view held) handle: + return nothing + ) + int64 outer_observed = resource_probe.borrow(view net, view outer_token) handle error: + return nothing + total = total + outer_observed + list[int64] inner_phases = list(3, 4) + for inner_phase in inner_phases: + resource_probe.TestHandle inner_token = view((view held) handle: + return nothing + ) + int64 inner_observed = resource_probe.borrow(view net, view inner_token) handle error: + return nothing + total = total + inner_observed + if total != 102: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett new file mode 100644 index 00000000..51e154b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/18_pipeline_construct_move_close.jett @@ -0,0 +1,12 @@ +namespace app +function move_owned(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(1801) handle error: + return nothing + token into move_owned() into resource_probe.close() + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett new file mode 100644 index 00000000..ad31ad94 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_borrow_fail.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett new file mode 100644 index 00000000..049c2b67 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_construct_fail.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett new file mode 100644 index 00000000..ddd72270 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/19_pipeline_written_view_retains_owner.jett @@ -0,0 +1,18 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1901) handle error: + return -1 + int64 observed = token into view read_first(view net) handle error: + return -2 + resource_probe.close(token) + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 5: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett new file mode 100644 index 00000000..8e5ec656 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett new file mode 100644 index 00000000..fb437fff --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_construct_fail.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett new file mode 100644 index 00000000..ca1d1167 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/20_pipeline_bare_owner_view_operation.jett @@ -0,0 +1,17 @@ +namespace app +function read_first(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2001) handle error: + return -1 + int64 observed = token into read_first(view net) handle error: + return -2 + return observed +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 5: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett new file mode 100644 index 00000000..658660bd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/21_pipeline_named_actual_abort.jett @@ -0,0 +1,25 @@ +namespace app +function stage_owned(view net: Network, token: resource_probe.TestHandle) returns resource_probe.TestHandle: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return token + return token +function accept_staged(token: resource_probe.TestHandle, marker: int64, view net: Network) returns nothing: + use resource_probe + int64 observed = resource_probe.borrow(view net, view token) handle error: + return nothing + resource_probe.close(token) + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +export function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2101) handle error: + return nothing + stage_owned(view net, token) into accept_staged(net: view net, marker: fail_marker() handle error: + return nothing + ) + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett new file mode 100644 index 00000000..081c061d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett @@ -0,0 +1,13 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != -1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett new file mode 100644 index 00000000..3bed80e8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/22_pipeline_domain_success_close.jett @@ -0,0 +1,13 @@ +namespace app +export function scenario(view net: Network) returns int64: + use resource_probe + resource_probe.TestHandle token = net into view resource_probe.create(2201) handle error: + return -1 + token into resource_probe.close() + return 9 +export function main(net: Network) returns nothing: + use resource_probe + int64 observed = scenario(view net) + if observed != 9: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett b/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett new file mode 100644 index 00000000..da5ad406 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/pipeline/23_pipeline_indirect_close_descriptor.jett @@ -0,0 +1,11 @@ +namespace app +export function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing close_token = resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 2301) handle error: + return nothing + token into close_token + return nothing +export function main(net: Network) returns nothing: + scenario(view net) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/01_original.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett new file mode 100644 index 00000000..8a0c2fc3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/02_index_distinct.jett @@ -0,0 +1,27 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if field.index == 0: + total = total + observed + else: + total = total + observed + 1 + if total != 35: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett new file mode 100644 index 00000000..68ab8f1f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/03_name_distinct.jett @@ -0,0 +1,27 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + if field.name == "first": + total = total + observed + else: + total = total + observed + 1 + if total != 35: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett new file mode 100644 index 00000000..8e72a798 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/reflected_field_native/04_ordinary_helper.jett @@ -0,0 +1,36 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +function field_ordinal_total() returns int64: + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + Element observed = 17 + if field.index == 0: + total = total + observed + else: + total = total + observed + 1 + return total +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if field_ordinal_total() != 35: + int64 ignored = resource_probe.terminal_label() + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett b/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett new file mode 100644 index 00000000..77588e58 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource/resource_probe.jett @@ -0,0 +1,27 @@ +namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function empty() returns optional[TestHandle]: + return none +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function terminal_label() returns int64: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return 1 +export function create_descriptor() returns function(view Network, int64) returns result[TestHandle, string]: + return kernel_create +export function borrow_descriptor() returns function(view Network, view TestHandle) returns result[int64, string]: + return kernel_borrow +export function select_create(view net: Network, view anchor: TestHandle, fail_now: bool) returns function(view Network, int64) returns result[TestHandle, string]: + int64 observed = borrow(view net, view anchor) handle error: + default 0 + if fail_now: + int64 ignored = terminal_label() + return kernel_create diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett new file mode 100644 index 00000000..106c9e23 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/01_empty_tokens.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett new file mode 100644 index 00000000..fece130d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/02_absent_tokens.jett @@ -0,0 +1,96 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + if list.length[optional[resource_probe.TestHandle]](view values) != 2: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 2: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett new file mode 100644 index 00000000..01bca721 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/03_empty_map.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + map[string, resource_probe.TestHandle] values = empty_map() + if map.length[string, resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett new file mode 100644 index 00000000..805fd1be --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/04_absent_map.jett @@ -0,0 +1,98 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + if map.length[string, optional[resource_probe.TestHandle]](view values) != 1: + int64 ignored = resource_probe.terminal_label() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + int64 ignored = resource_probe.terminal_label() + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + if absent_count != 1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett new file mode 100644 index 00000000..83606a00 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/05_empty_result_tokens.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_result_tokens() handle error: + int64 ignored = resource_probe.terminal_label() + return nothing + if list.length[resource_probe.TestHandle](view values) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett new file mode 100644 index 00000000..caf33d0a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/06_failed_result_tokens.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = failed_result_tokens() handle error: + if error != "empty": + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett new file mode 100644 index 00000000..de8f7bf4 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/07_absent_envelope.jett @@ -0,0 +1,94 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenEnvelope value = absent_envelope() + if value.marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view value.tokens) != 0: + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett new file mode 100644 index 00000000..acb9f717 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/08_absent_box.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenBox[resource_probe.TestHandle] value = absent_box() + if list.length[resource_probe.TestHandle](view value.items) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett new file mode 100644 index 00000000..fcd6e511 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/09_empty_choice.jett @@ -0,0 +1,87 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenChoice value = empty_choice() + match value: + empty: + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett new file mode 100644 index 00000000..c829b3b7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/10_absent_choice.jett @@ -0,0 +1,91 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + if marker != 17: + int64 ignored = resource_probe.terminal_label() + if list.length[resource_probe.TestHandle](view tokens) != 0: + int64 ignored = resource_probe.terminal_label() + return nothing + other: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett new file mode 100644 index 00000000..f2ae060f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/11_absent_state.jett @@ -0,0 +1,95 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett new file mode 100644 index 00000000..bc73159a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/12_absent_exact_state.jett @@ -0,0 +1,95 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false + +export function main(net: Network) returns nothing: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + int64 ignored = resource_probe.terminal_label() + if !source17_absent_optional(view value.fallback): + int64 ignored = resource_probe.terminal_label() + return nothing + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett new file mode 100644 index 00000000..09cabeb8 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/13_wrong_empty_length_control.jett @@ -0,0 +1,84 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + list[resource_probe.TestHandle] values = empty_tokens() + if list.length[resource_probe.TestHandle](view values) != 1: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett new file mode 100644 index 00000000..359dc142 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/14_required_primitive_controls.jett @@ -0,0 +1,86 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker + +export function main(net: Network) returns nothing: + int64 answer = absent_required_controls() + if answer != 34: + int64 ignored = resource_probe.terminal_label() + if absent_namespace_value != 17: + int64 ignored = resource_probe.terminal_label() + return nothing diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett new file mode 100644 index 00000000..7dac2f29 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/list_length.jett @@ -0,0 +1,3 @@ +namespace list +export function length[T](view items: list[T]) returns int64: + return list.__length[T](view items) diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett new file mode 100644 index 00000000..d540cfab --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/map_length.jett @@ -0,0 +1,3 @@ +namespace map +export function length[K, V](view items: map[K, V]) returns int64: + return map.__length[K, V](view items) diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate/original.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs new file mode 100644 index 00000000..c9a3cdd3 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_absent_aggregate_baseline_inputs.rs @@ -0,0 +1,287 @@ +//! Finite Source-only baseline data shared by reference and object probes. +//! No native status, channel, message or exit assertion is admitted here. +#![allow(dead_code)] // Each independent harness consumes its own phases. + +use jett_common::{FileId, SourceOrigin, Span}; +use jett_parser::ast::Item; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram, ResourceProgramError}; +use jett_types::ResourceKernelRecipe; +use std::collections::HashMap; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; + +pub const SUPPORT: &str = include_str!("resource/resource_probe.jett"); +pub const ORIGINAL: &str = include_str!("resource_absent_aggregate/original.jett"); +pub const LIST_LENGTH: &str = include_str!("resource_absent_aggregate/list_length.jett"); +pub const MAP_LENGTH: &str = include_str!("resource_absent_aggregate/map_length.jett"); + +#[derive(Clone, Copy, Debug)] +pub enum ReferenceExpectation { + Nothing, + Error(&'static str), +} + +pub struct Input { + pub name: &'static str, + pub source: &'static str, + pub reference: ReferenceExpectation, +} + +macro_rules! clean { + ($name:literal, $path:literal) => { + Input { + name: $name, + source: include_str!($path), + reference: ReferenceExpectation::Nothing, + } + }; +} + +pub const INPUTS: &[Input] = &[ + clean!( + "01_empty_tokens", + "resource_absent_aggregate/01_empty_tokens.jett" + ), + clean!( + "02_absent_tokens", + "resource_absent_aggregate/02_absent_tokens.jett" + ), + clean!( + "03_empty_map", + "resource_absent_aggregate/03_empty_map.jett" + ), + clean!( + "04_absent_map", + "resource_absent_aggregate/04_absent_map.jett" + ), + clean!( + "05_empty_result_tokens", + "resource_absent_aggregate/05_empty_result_tokens.jett" + ), + clean!( + "06_failed_result_tokens", + "resource_absent_aggregate/06_failed_result_tokens.jett" + ), + clean!( + "07_absent_envelope", + "resource_absent_aggregate/07_absent_envelope.jett" + ), + clean!( + "08_absent_box", + "resource_absent_aggregate/08_absent_box.jett" + ), + clean!( + "09_empty_choice", + "resource_absent_aggregate/09_empty_choice.jett" + ), + clean!( + "10_absent_choice", + "resource_absent_aggregate/10_absent_choice.jett" + ), + clean!( + "11_absent_state", + "resource_absent_aggregate/11_absent_state.jett" + ), + clean!( + "12_absent_exact_state", + "resource_absent_aggregate/12_absent_exact_state.jett" + ), + Input { + name: "13_wrong_empty_length_control", + source: include_str!("resource_absent_aggregate/13_wrong_empty_length_control.jett"), + reference: ReferenceExpectation::Error("list.__remove_at: index -1 out of bounds"), + }, + clean!( + "14_required_primitive_controls", + "resource_absent_aggregate/14_required_primitive_controls.jett" + ), +]; + +#[derive(Clone, Copy, Debug)] +pub enum Phase { + Unstarted, + SourceIdentity, + Parsing, + Catalog, + CheckedPrepare, + Resolution, + Checking, + ResolveMetadata, + CheckMetadata, + RequiredCapture, + RequiredCache, + ReferenceSetup, + ReferenceEntryIdentity, + ReferenceCall, + ReferenceObservations, + WorkerEligibility, + OriginalConstructorCall, + VerifyPropertyWorkers, + HirLowering, + ValueBaking, + HirEntryIdentity, + ValueConversions, + MirLowering, + MirValidation, + ObjectEmission, + ObjectObservations, +} + +#[derive(Clone, Copy, Debug)] +pub enum FailureKind { + Returned, + UnexpectedPanic, +} + +#[derive(Debug)] +pub struct Failure { + pub phase: Phase, + pub kind: FailureKind, + pub detail: String, +} + +pub struct Attempt { + pub entered: Vec, + pub outcome: Result, +} + +pub fn enter(entered: &mut Vec, phase: Phase) { + entered.push(phase); +} + +pub fn failure(phase: Phase, detail: impl std::fmt::Debug) -> Failure { + Failure { + phase, + kind: FailureKind::Returned, + detail: format!("{detail:?}"), + } +} + +pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { + payload + .downcast_ref::() + .cloned() + .or_else(|| { + payload + .downcast_ref::<&str>() + .map(|text| (*text).to_string()) + }) + .unwrap_or_else(|| "non-string panic payload".to_string()) +} + +pub fn attempt(run: impl FnOnce(&mut Vec) -> Result) -> Attempt { + let mut entered = vec![Phase::Unstarted]; + let outcome = match catch_unwind(AssertUnwindSafe(|| run(&mut entered))) { + Ok(outcome) => outcome, + Err(payload) => Err(Failure { + phase: *entered.last().unwrap(), + kind: FailureKind::UnexpectedPanic, + detail: panic_message(payload.as_ref()), + }), + }; + Attempt { entered, outcome } +} + +fn checked_failure(error: ResourceProgramError) -> Failure { + let phase = match &error { + ResourceProgramError::ParseDiagnostics(_) => Phase::Parsing, + ResourceProgramError::ResolveDiagnostics(_) => Phase::Resolution, + ResourceProgramError::CheckDiagnostics(_) => Phase::Checking, + ResourceProgramError::ResolveMetadata { .. } => Phase::ResolveMetadata, + ResourceProgramError::CheckMetadata { .. } => Phase::CheckMetadata, + }; + // Display alone omits ordinary diagnostic messages; retain full spans/codes. + failure(phase, (&error, error.diagnostics())) +} + +pub fn prepare( + input: &Input, + release: bool, + entered: &mut Vec, +) -> Result<(Arc, Span), Failure> { + enter(entered, Phase::SourceIdentity); + if !input.source.starts_with(ORIGINAL) { + return Err(failure( + Phase::SourceIdentity, + "whole original Source17 prefix changed", + )); + } + let primary = FileId::new(0); + let support = FileId::new(10_000); + let list = FileId::new(10_001); + let map = FileId::new(10_002); + enter(entered, Phase::Parsing); + let mut parsed = jett_parser::parse(SUPPORT, support); + let resource_spans = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let mut entries = Vec::new(); + for (source, file) in [ + (LIST_LENGTH, list), + (MAP_LENGTH, map), + (input.source, primary), + ] { + let fragment = jett_parser::parse(source, file); + if file == primary { + entries.extend(fragment.module.items.iter().filter_map(|item| match item { + Item::Function(function) + if function.name.name == "main" && function.name.span.file == primary => + { + Some(function.span) + } + _ => None, + })); + } + parsed.module.items.extend(fragment.module.items); + parsed.errors.extend(fragment.errors); + } + if !parsed.errors.is_empty() { + return Err(failure(Phase::Parsing, &parsed.errors)); + } + enter(entered, Phase::Catalog); + let [resource] = resource_spans.as_slice() else { + return Err(failure( + Phase::Catalog, + "sole original Support Resource declaration missing", + )); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + let [entry] = entries.as_slice() else { + return Err(failure( + Phase::Catalog, + "primary original main declaration is not unique", + )); + }; + enter(entered, Phase::CheckedPrepare); + let checked = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (primary, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + (list, SourceOrigin::Stdlib), + (map, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .map_err(checked_failure)?; + Ok((Arc::new(checked), *entry)) +} diff --git a/crates/jett_driver/tests/native_conformance/resource_cases.rs b/crates/jett_driver/tests/native_conformance/resource_cases.rs new file mode 100644 index 00000000..44b846b0 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_cases.rs @@ -0,0 +1,3269 @@ +//! Identical original Source and finite script/oracle inputs for both test harnesses. +//! No record contains a runtime token, Resource key or installer authority. +#![allow(dead_code)] // Each harness consumes its own side of the shared observation oracle. +pub(crate) const SUPPORT: &str = include_str!("resource/resource_probe.jett"); +#[derive(Clone, Copy, Debug)] +pub(crate) enum Script { + Construct(i64), + ConstructFail(i64, &'static str), + FinalizerPanic(i64), + Borrow(i64, i64), + BorrowFail(i64, &'static str), + BorrowPanic(i64), +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Event { + Constructed(i64), + ConstructionFailed(i64), + Borrowed(i64), + BorrowFailed(i64), + Finalized(i64), +} +#[derive(Clone, Copy, Debug)] +pub(crate) enum ReferenceOutcome { + Clean, + Error(&'static str), + ProviderPanic(&'static str), + CleanupPanic(&'static str), +} +pub(crate) struct Case { + pub(crate) name: &'static str, + pub(crate) source: &'static str, + pub(crate) script: &'static [Script], + pub(crate) events: &'static [Event], + pub(crate) reference: ReferenceOutcome, + // Native status/channel literals are separate from the reference error representation. + pub(crate) body: u32, + pub(crate) cleanup: u32, + pub(crate) channel: u32, + pub(crate) ordinary_message: &'static [u8], + // Ordinary terminal errors have no Resource diagnostic without an independent protocol fault. + pub(crate) resource_message: &'static [u8], + pub(crate) exit: i32, +} +pub(crate) const CASES: &[Case] = &[ + Case { + name: "connected", + source: include_str!("resource/01_connected_entry.jett"), + script: &[Script::Construct(1), Script::Borrow(1, 7)], + events: &[ + Event::Constructed(1), + Event::Borrowed(1), + Event::Finalized(1), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "factory_fail", + source: include_str!("resource/01_connected_entry.jett"), + script: &[Script::ConstructFail(1, "declined")], + events: &[Event::ConstructionFailed(1)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_actual", + source: include_str!("resource/02_partial_actual_failure.jett"), + script: &[Script::Construct(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"", + exit: 71, + }, + Case { + name: "implicit_drop", + source: include_str!("resource/03_implicit_drop.jett"), + script: &[Script::Construct(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "finalizer_panic", + source: include_str!("resource/03_implicit_drop.jett"), + script: &[Script::FinalizerPanic(1)], + events: &[Event::Constructed(1), Event::Finalized(1)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "replace_live", + source: include_str!("resource/04_live_replace.jett"), + script: &[ + Script::Construct(2401), + Script::Construct(2402), + Script::Borrow(2402, 7), + ], + events: &[ + Event::Constructed(2401), + Event::Constructed(2402), + Event::Finalized(2401), + Event::Borrowed(2402), + Event::Finalized(2402), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rebind_self", + source: include_str!("resource/05_self_rebind.jett"), + script: &[Script::Construct(2421)], + events: &[Event::Constructed(2421), Event::Finalized(2421)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "replace_closed", + source: include_str!("resource/06_replace_closed.jett"), + script: &[Script::Construct(2411), Script::Construct(2412)], + events: &[ + Event::Constructed(2411), + Event::Finalized(2411), + Event::Constructed(2412), + Event::Finalized(2412), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rhs_domain_failure", + source: include_str!("resource/07_rhs_domain_failure.jett"), + script: &[ + Script::Construct(2431), + Script::ConstructFail(2432, "rhs sentinel"), + Script::Borrow(2431, 7), + ], + events: &[ + Event::Constructed(2431), + Event::ConstructionFailed(2432), + Event::Borrowed(2431), + Event::Finalized(2431), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "rhs_ordinary_failure", + source: include_str!("resource/08_rhs_ordinary_failure.jett"), + script: &[Script::Construct(2441)], + events: &[Event::Constructed(2441), Event::Finalized(2441)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"", + exit: 71, + }, + Case { + name: "replace_old_finalizer_panic", + source: include_str!("resource/04_live_replace.jett"), + script: &[Script::FinalizerPanic(2401), Script::Construct(2402)], + events: &[ + Event::Constructed(2401), + Event::Constructed(2402), + Event::Finalized(2401), + Event::Finalized(2402), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "later_handle_minimal", + source: include_str!("resource/09_later_handle_minimal.jett"), + script: &[Script::Construct(502)], + events: &[Event::Constructed(502), Event::Finalized(502)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_original", + source: include_str!("resource/10_later_handle_original.jett"), + script: &[Script::Construct(501), Script::Borrow(501, 7)], + events: &[ + Event::Constructed(501), + Event::Borrowed(501), + Event::Finalized(501), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_success", + source: include_str!("resource/11_later_handle_success.jett"), + script: &[Script::Construct(503), Script::Borrow(503, 7)], + events: &[ + Event::Constructed(503), + Event::Borrowed(503), + Event::Finalized(503), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_written_view", + source: include_str!("resource/12_later_handle_written_view.jett"), + script: &[Script::Construct(505), Script::Borrow(505, 7)], + events: &[ + Event::Constructed(505), + Event::Borrowed(505), + Event::Finalized(505), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "first_handle_failure", + source: include_str!("resource/13_first_handle_failure.jett"), + script: &[Script::ConstructFail(506, "first sentinel")], + events: &[Event::ConstructionFailed(506)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_cleanup_panic", + source: include_str!("resource/09_later_handle_minimal.jett"), + script: &[Script::FinalizerPanic(502)], + events: &[Event::Constructed(502), Event::Finalized(502)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "first_handle_success", + source: include_str!("resource/14_first_handle_success.jett"), + script: &[Script::Construct(507), Script::Borrow(507, 7)], + events: &[ + Event::Constructed(507), + Event::Borrowed(507), + Event::Finalized(507), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_two_owned_views", + source: include_str!("resource/15_later_handle_two_owned_views.jett"), + script: &[Script::Construct(511), Script::Construct(512)], + events: &[ + Event::Constructed(511), + Event::Constructed(512), + Event::Finalized(512), + Event::Finalized(511), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_return_other_owner", + source: include_str!("resource/16_later_handle_return_other_owner.jett"), + script: &[ + Script::Construct(521), + Script::Construct(522), + Script::Borrow(522, 7), + ], + events: &[ + Event::Constructed(521), + Event::Constructed(522), + Event::Finalized(521), + Event::Borrowed(522), + Event::Finalized(522), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_default", + source: include_str!("resource/17_later_handle_default.jett"), + script: &[Script::Construct(531), Script::Borrow(531, 7)], + events: &[ + Event::Constructed(531), + Event::Borrowed(531), + Event::Finalized(531), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "later_handle_return_cleanup_panic", + source: include_str!("resource/16_later_handle_return_other_owner.jett"), + script: &[Script::FinalizerPanic(521), Script::Construct(522)], + events: &[ + Event::Constructed(521), + Event::Constructed(522), + Event::Finalized(521), + Event::Finalized(522), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_close", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_named_close_factory_fail", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::ConstructFail(701, "indirect factory sentinel")], + events: &[Event::ConstructionFailed(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_named_terminal_failure", + source: include_str!("resource/19_indirect_named_terminal_failure.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"", + exit: 71, + }, + Case { + name: "indirect_named_close_cleanup_panic", + source: include_str!("resource/18_indirect_named_close.jett"), + script: &[Script::FinalizerPanic(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_terminal_cleanup_panic", + source: include_str!("resource/19_indirect_named_terminal_failure.jett"), + script: &[Script::FinalizerPanic(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_named_alias", + source: include_str!("resource/20_indirect_named_alias.jett"), + script: &[Script::Construct(701)], + events: &[Event::Constructed(701), Event::Finalized(701)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::Construct(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close_factory_fail", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::ConstructFail(711, "namespace factory sentinel")], + events: &[Event::ConstructionFailed(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "indirect_namespace_close_cleanup_panic", + source: include_str!("resource/21_indirect_namespace_close.jett"), + script: &[Script::FinalizerPanic(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "indirect_namespace_alias", + source: include_str!("resource/22_indirect_namespace_alias.jett"), + script: &[Script::Construct(711)], + events: &[Event::Constructed(711), Event::Finalized(711)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_close", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory_fail", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::ConstructFail(721, "returned hook factory sentinel")], + events: &[Event::ConstructionFailed(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_cleanup_panic", + source: include_str!("resource/23_returned_hook_close.jett"), + script: &[Script::FinalizerPanic(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "returned_hook_alias", + source: include_str!("resource/24_returned_hook_alias.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_relay", + source: include_str!("resource/25_returned_hook_relay.jett"), + script: &[Script::Construct(721)], + events: &[Event::Constructed(721), Event::Finalized(721)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_unused", + source: include_str!("resource/26_returned_hook_unused.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory", + source: include_str!("resource/27_returned_hook_factory.jett"), + script: &[Script::Construct(731)], + events: &[Event::Constructed(731), Event::Finalized(731)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_factory_domain_fail", + source: include_str!("resource/27_returned_hook_factory.jett"), + script: &[Script::ConstructFail(731, "returned factory sentinel")], + events: &[Event::ConstructionFailed(731)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "returned_hook_borrow", + source: include_str!("resource/28_returned_hook_borrow.jett"), + script: &[Script::Construct(741), Script::Borrow(741, 17)], + events: &[ + Event::Constructed(741), + Event::Borrowed(741), + Event::Finalized(741), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_close", + source: include_str!("resource/29_immediate_returned_hook_close.jett"), + script: &[Script::Construct(751)], + events: &[Event::Constructed(751), Event::Finalized(751)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_factory", + source: include_str!("resource/30_immediate_returned_hook_factory.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + Script::Construct(743), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::Constructed(743), + Event::Finalized(743), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_factory_domain_fail", + source: include_str!("resource/30_immediate_returned_hook_factory.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + Script::ConstructFail(743, "immediate returned factory sentinel"), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::ConstructionFailed(743), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_returned_hook_callee_failure", + source: include_str!("resource/31_immediate_returned_hook_callee_failure.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + Script::Borrow(742, 1), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Borrowed(742), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"", + exit: 71, + }, + Case { + name: "immediate_returned_hook_actual_failure", + source: include_str!("resource/32_immediate_returned_hook_actual_failure.jett"), + script: &[ + Script::Construct(742), + Script::Construct(744), + Script::Borrow(744, 743), + ], + events: &[ + Event::Constructed(742), + Event::Constructed(744), + Event::Borrowed(744), + Event::Finalized(744), + Event::Finalized(742), + ], + reference: ReferenceOutcome::Error("list.__remove_at: index -1 out of bounds"), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: b"list.__remove_at: index -1 out of bounds", + resource_message: b"", + exit: 71, + }, + Case { + name: "comptime_hook_close", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_close_factory_fail", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::ConstructFail( + 761, + "comptime close factory sentinel", + )], + events: &[Event::ConstructionFailed(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_close_cleanup_panic", + source: include_str!("resource/33_comptime_hook_close.jett"), + script: &[Script::FinalizerPanic(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "comptime_hook_factory", + source: include_str!("resource/34_comptime_hook_factory.jett"), + script: &[Script::Construct(771)], + events: &[Event::Constructed(771), Event::Finalized(771)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_factory_domain_fail", + source: include_str!("resource/34_comptime_hook_factory.jett"), + script: &[Script::ConstructFail(771, "comptime factory sentinel")], + events: &[Event::ConstructionFailed(771)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_borrow", + source: include_str!("resource/35_comptime_hook_borrow.jett"), + script: &[Script::Construct(781), Script::Borrow(781, 17)], + events: &[ + Event::Constructed(781), + Event::Borrowed(781), + Event::Finalized(781), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_alias", + source: include_str!("resource/36_comptime_hook_alias.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_relay", + source: include_str!("resource/37_comptime_hook_relay.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_unused", + source: include_str!("resource/38_comptime_hook_unused.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "immediate_comptime_hook_close", + source: include_str!("resource/39_immediate_comptime_hook_close.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_absence", + source: include_str!("resource/40_comptime_hook_absence.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_pure_choice", + source: include_str!("resource/41_comptime_hook_pure_choice.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_generic", + source: include_str!("resource/42_comptime_hook_generic.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_hook_scoped", + source: include_str!("resource/43_comptime_hook_scoped.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "comptime_scalar_helper", + source: include_str!("resource/44_comptime_scalar_helper.jett"), + script: &[Script::Construct(761)], + events: &[Event::Constructed(761), Event::Finalized(761)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_none_written", + source: include_str!("resource/45_view_optional_none_written.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_none_bare", + source: include_str!("resource/46_view_optional_none_bare.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_some_written", + source: include_str!("resource/47_view_optional_some_written.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_some_bare", + source: include_str!("resource/48_view_optional_some_bare.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_fail_written", + source: include_str!("resource/49_view_result_fail_written.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_fail_bare", + source: include_str!("resource/50_view_result_fail_bare.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_ok_written", + source: include_str!("resource/51_view_result_ok_written.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_ok_bare", + source: include_str!("resource/52_view_result_ok_bare.jett"), + script: &[Script::Construct(831), Script::Borrow(831, 17)], + events: &[ + Event::Constructed(831), + Event::Borrowed(831), + Event::Finalized(831), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_written_argument_abort", + source: include_str!("resource/53_view_optional_written_argument_abort.jett"), + script: &[ + Script::Construct(851), + Script::ConstructFail(852, "late sentinel"), + ], + events: &[ + Event::Constructed(851), + Event::ConstructionFailed(852), + Event::Finalized(851), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_bare_argument_abort", + source: include_str!("resource/54_view_result_bare_argument_abort.jett"), + script: &[ + Script::Construct(851), + Script::ConstructFail(852, "late sentinel"), + ], + events: &[ + Event::Constructed(851), + Event::ConstructionFailed(852), + Event::Finalized(851), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_optional_written_borrow_fail", + source: include_str!("resource/55_view_optional_written_borrow_fail.jett"), + script: &[ + Script::Construct(861), + Script::BorrowFail(861, "borrow sentinel"), + ], + events: &[ + Event::Constructed(861), + Event::BorrowFailed(861), + Event::Finalized(861), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "view_result_bare_borrow_fail", + source: include_str!("resource/56_view_result_bare_borrow_fail.jett"), + script: &[ + Script::Construct(861), + Script::BorrowFail(861, "borrow sentinel"), + ], + events: &[ + Event::Constructed(861), + Event::BorrowFailed(861), + Event::Finalized(861), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "nested_view_optional_some_written", + source: include_str!("resource/57_nested_view_optional_some_written.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_some", + source: include_str!("resource/58_if_optional_written_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_some_construct_fail", + source: include_str!("resource/58_if_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_written_none", + source: include_str!("resource/59_if_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_some", + source: include_str!("resource/60_if_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_some_construct_fail", + source: include_str!("resource/60_if_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_optional_bare_none", + source: include_str!("resource/61_if_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_ok", + source: include_str!("resource/62_if_result_written_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_ok_construct_fail", + source: include_str!("resource/62_if_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_written_fail", + source: include_str!("resource/63_if_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_ok", + source: include_str!("resource/64_if_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_ok_construct_fail", + source: include_str!("resource/64_if_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_result_bare_fail", + source: include_str!("resource/65_if_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_some", + source: include_str!("resource/66_scope_optional_written_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_some_construct_fail", + source: include_str!("resource/66_scope_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_none", + source: include_str!("resource/67_scope_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_some", + source: include_str!("resource/68_scope_optional_bare_some.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_some_construct_fail", + source: include_str!("resource/68_scope_optional_bare_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_bare_none", + source: include_str!("resource/69_scope_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_ok", + source: include_str!("resource/70_scope_result_written_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_ok_construct_fail", + source: include_str!("resource/70_scope_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_written_fail", + source: include_str!("resource/71_scope_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_ok", + source: include_str!("resource/72_scope_result_bare_ok.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_ok_construct_fail", + source: include_str!("resource/72_scope_result_bare_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_fail", + source: include_str!("resource/73_scope_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_some", + source: include_str!("resource/74_while_optional_written_some.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_some_construct_fail", + source: include_str!("resource/74_while_optional_written_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_written_none", + source: include_str!("resource/75_while_optional_written_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_some", + source: include_str!("resource/76_while_optional_bare_some.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_some_construct_fail", + source: include_str!("resource/76_while_optional_bare_some.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_none", + source: include_str!("resource/77_while_optional_bare_none.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_ok", + source: include_str!("resource/78_while_result_written_ok.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_ok_construct_fail", + source: include_str!("resource/78_while_result_written_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_fail", + source: include_str!("resource/79_while_result_written_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_ok", + source: include_str!("resource/80_while_result_bare_ok.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_ok_construct_fail", + source: include_str!("resource/80_while_result_bare_ok.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_bare_fail", + source: include_str!("resource/81_while_result_bare_fail.jett"), + script: &[], + events: &[], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_optional_reuse", + source: include_str!("resource/82_if_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_optional_reuse_construct_fail", + source: include_str!("resource/82_if_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_result_reuse", + source: include_str!("resource/83_if_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Construct(872), + Script::Borrow(872, 17), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::Constructed(872), + Event::Borrowed(872), + Event::Finalized(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_owned_result_reuse_construct_fail", + source: include_str!("resource/83_if_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::ConstructFail(872, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Finalized(871), + Event::ConstructionFailed(872), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_optional_reuse", + source: include_str!("resource/84_scope_owned_optional_reuse.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_optional_reuse_construct_fail", + source: include_str!("resource/84_scope_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_result_reuse", + source: include_str!("resource/85_scope_owned_result_reuse.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_owned_result_reuse_construct_fail", + source: include_str!("resource/85_scope_owned_result_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_reuse", + source: include_str!("resource/86_while_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_reuse_construct_fail", + source: include_str!("resource/86_while_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_reuse", + source: include_str!("resource/87_while_owned_result_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_reuse_construct_fail", + source: include_str!("resource/87_while_owned_result_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_sibling_optional_bindings", + source: include_str!("resource/88_if_sibling_optional_bindings.jett"), + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(892), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "if_sibling_optional_bindings_construct_fail", + source: include_str!("resource/88_if_sibling_optional_bindings.jett"), + script: &[ + Script::Construct(891), + Script::ConstructFail(892, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::ConstructionFailed(892), + Event::Finalized(891), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_sibling_result_bindings", + source: include_str!("resource/89_scope_sibling_result_bindings.jett"), + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(892), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_sibling_result_bindings_construct_fail", + source: include_str!("resource/89_scope_sibling_result_bindings.jett"), + script: &[ + Script::Construct(891), + Script::ConstructFail(892, "nested factory sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::ConstructionFailed(892), + Event::Finalized(891), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_borrow_fail", + source: include_str!("resource/90_scope_optional_written_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_optional_written_borrow_fail_construct_fail", + source: include_str!("resource/90_scope_optional_written_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_borrow_fail", + source: include_str!("resource/91_scope_result_bare_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "scope_result_bare_borrow_fail_construct_fail", + source: include_str!("resource/91_scope_result_bare_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_borrow_fail", + source: include_str!("resource/92_while_optional_bare_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_optional_bare_borrow_fail_construct_fail", + source: include_str!("resource/92_while_optional_bare_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_borrow_fail", + source: include_str!("resource/93_while_result_written_borrow_fail.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_result_written_borrow_fail_construct_fail", + source: include_str!("resource/93_while_result_written_borrow_fail.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_break", + source: include_str!("resource/94_while_owned_optional_break.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_optional_break_construct_fail", + source: include_str!("resource/94_while_owned_optional_break.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_continue", + source: include_str!("resource/95_while_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "while_owned_result_continue_construct_fail", + source: include_str!("resource/95_while_owned_result_continue.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Pipeline Source scenarios preserve operation outcomes and retirement order. + // Exact panic body/cleanup/channel reports are enforced by the native gate. + Case { + name: "pipeline_construct_move_close", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::Construct(1801)], + events: &[Event::Constructed(1801), Event::Finalized(1801)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_written_view_retains_owner", + source: include_str!("resource/pipeline/19_pipeline_written_view_retains_owner.jett"), + script: &[Script::Construct(1901), Script::Borrow(1901, 5)], + events: &[ + Event::Constructed(1901), + Event::Borrowed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_bare_owner_view_operation", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::Construct(2001), Script::Borrow(2001, 5)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Provider-panic body and cleanup status must be checked through real Source. + // Existing Reference fixture20 provider panic remains inside full native parity scope. + Case { + name: "pipeline_bare_owner_provider_panic", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::Construct(2001), Script::BorrowPanic(2001)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + // Provider-panic body and cleanup status must be checked through real Source. + // Cleanup precedence must be established through the same genuine fixture20 Source. + Case { + name: "pipeline_bare_owner_provider_panic_cleanup_wins", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::FinalizerPanic(2001), Script::BorrowPanic(2001)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_named_actual_abort", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::Construct(2101), Script::Borrow(2101, 1)], + events: &[ + Event::Constructed(2101), + Event::Borrowed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_domain_failure_stops_next_step", + source: include_str!("resource/pipeline/22_pipeline_domain_failure_stops_next_step.jett"), + script: &[Script::ConstructFail(2201, "denied")], + events: &[Event::ConstructionFailed(2201)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_written_view_borrow_fail", + source: include_str!("resource/pipeline/19_pipeline_written_view_borrow_fail.jett"), + script: &[ + Script::Construct(1901), + Script::BorrowFail(1901, "unavailable"), + ], + events: &[ + Event::Constructed(1901), + Event::BorrowFailed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_indirect_close_descriptor", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::Construct(2301)], + events: &[Event::Constructed(2301), Event::Finalized(2301)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_construct_factory_fail", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::ConstructFail(1801, "declined")], + events: &[Event::ConstructionFailed(1801)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_construct_close_cleanup_panic", + source: include_str!("resource/pipeline/18_pipeline_construct_move_close.jett"), + script: &[Script::FinalizerPanic(1801)], + events: &[Event::Constructed(1801), Event::Finalized(1801)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_written_view_construct_fail", + source: include_str!("resource/pipeline/19_pipeline_written_view_construct_fail.jett"), + script: &[Script::ConstructFail(1901, "declined")], + events: &[Event::ConstructionFailed(1901)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_written_view_close_cleanup_panic", + source: include_str!("resource/pipeline/19_pipeline_written_view_retains_owner.jett"), + script: &[Script::FinalizerPanic(1901), Script::Borrow(1901, 5)], + events: &[ + Event::Constructed(1901), + Event::Borrowed(1901), + Event::Finalized(1901), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_bare_owner_construct_fail", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_construct_fail.jett"), + script: &[Script::ConstructFail(2001, "declined")], + events: &[Event::ConstructionFailed(2001)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_bare_owner_borrow_fail", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_borrow_fail.jett"), + script: &[ + Script::Construct(2001), + Script::BorrowFail(2001, "unavailable"), + ], + events: &[ + Event::Constructed(2001), + Event::BorrowFailed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_bare_owner_operation_cleanup_panic", + source: include_str!("resource/pipeline/20_pipeline_bare_owner_view_operation.jett"), + script: &[Script::FinalizerPanic(2001), Script::Borrow(2001, 5)], + events: &[ + Event::Constructed(2001), + Event::Borrowed(2001), + Event::Finalized(2001), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_named_actual_construct_fail", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::ConstructFail(2101, "declined")], + events: &[Event::ConstructionFailed(2101)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "pipeline_named_actual_stage_borrow_fail", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[ + Script::Construct(2101), + Script::BorrowFail(2101, "stage unavailable"), + ], + events: &[ + Event::Constructed(2101), + Event::BorrowFailed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_named_actual_abort_cleanup_panic", + source: include_str!("resource/pipeline/21_pipeline_named_actual_abort.jett"), + script: &[Script::FinalizerPanic(2101), Script::Borrow(2101, 1)], + events: &[ + Event::Constructed(2101), + Event::Borrowed(2101), + Event::Finalized(2101), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_domain_success_close", + source: include_str!("resource/pipeline/22_pipeline_domain_success_close.jett"), + script: &[Script::Construct(2201)], + events: &[Event::Constructed(2201), Event::Finalized(2201)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_domain_success_close_cleanup_panic", + source: include_str!("resource/pipeline/22_pipeline_domain_success_close.jett"), + script: &[Script::FinalizerPanic(2201)], + events: &[Event::Constructed(2201), Event::Finalized(2201)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "pipeline_indirect_construct_fail", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::ConstructFail(2301, "declined")], + events: &[Event::ConstructionFailed(2301)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Cleanup panic must retain its exact native status and channel. + Case { + name: "pipeline_indirect_close_cleanup_panic", + source: include_str!("resource/pipeline/23_pipeline_indirect_close_descriptor.jett"), + script: &[Script::FinalizerPanic(2301)], + events: &[Event::Constructed(2301), Event::Finalized(2301)], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_optional_reuse", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_first_borrow_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_reuse_second_borrow_fail", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/01_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_optional_break", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_break_construct_fail", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_optional_break_first_borrow_fail", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_optional_break_cleanup_panic", + source: include_str!("resource/control_flow/02_for_owned_optional_break.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_for_owned_result_continue", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_construct_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_first_borrow_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_for_owned_result_continue_second_borrow_fail", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_for_owned_result_continue_cleanup_panic", + source: include_str!("resource/control_flow/03_for_owned_result_continue.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_match_owned_optional_first", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_first_construct_fail", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_first_first_borrow_fail", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_match_owned_optional_first_cleanup_panic", + source: include_str!("resource/control_flow/04_match_owned_optional_first.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "control_flow_match_owned_optional_second", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_second_construct_fail", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "control_flow_match_owned_optional_second_first_borrow_fail", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // INFERRED for this new enclosing Source: characterize native cleanup status before acceptance. + Case { + name: "control_flow_match_owned_optional_second_cleanup_panic", + source: include_str!("resource/control_flow/05_match_owned_optional_second.jett"), + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_third_borrow_fail", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_consecutive_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/07_consecutive_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_construct_fail", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_first_inner_borrow_fail", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + // Native outcomes are checked by the linked Source-deleted gate. + Case { + name: "control_flow_nested_for_owned_optional_reuse_cleanup_panic", + source: include_str!("resource/control_flow/08_nested_for_owned_optional_reuse.jett"), + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; + +pub(crate) const PIPELINE_CASE_START: usize = 138; +pub(crate) const PIPELINE_CASE_COUNT: usize = 23; +pub(crate) fn pipeline_cases() -> &'static [Case] { + let end = PIPELINE_CASE_START + PIPELINE_CASE_COUNT; + assert_eq!( + CASES.len(), + CONTROL_FLOW_CASE_START + CONTROL_FLOW_CASE_COUNT, + "full Source-only additive control-flow candidate corpus" + ); + assert!( + CASES[..PIPELINE_CASE_START] + .iter() + .all(|case| !case.name.starts_with("pipeline_")), + "original corpus must not contain the new pipeline family" + ); + let selected = &CASES[PIPELINE_CASE_START..end]; + assert!( + selected + .iter() + .all(|case| case.name.starts_with("pipeline_")), + "focused gate must select only the appended pipeline family" + ); + selected +} + +pub(crate) const CONTROL_FLOW_CASE_START: usize = 161; +pub(crate) const CONTROL_FLOW_CASE_COUNT: usize = 30; +pub(crate) fn control_flow_cases() -> &'static [Case] { + let end = CONTROL_FLOW_CASE_START + CONTROL_FLOW_CASE_COUNT; + assert_eq!( + CASES.len(), + end, + "full Source-only control-flow candidate corpus" + ); + assert!( + CASES[..CONTROL_FLOW_CASE_START] + .iter() + .all(|case| !case.name.starts_with("control_flow_")), + "original corpus must not contain the new control-flow family" + ); + let selected = &CASES[CONTROL_FLOW_CASE_START..end]; + assert!( + selected + .iter() + .all(|case| case.name.starts_with("control_flow_")), + "focused gate must select only the appended control-flow family" + ); + selected +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution.rs b/crates/jett_driver/tests/native_conformance/resource_execution.rs new file mode 100644 index 00000000..640d3f56 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution.rs @@ -0,0 +1,856 @@ +//! Explicit matched single-runtime tests. Production launcher metadata is never used. +use super::{resource_cases as cases, resource_report as report}; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{ + collections::HashMap, + ffi::OsString, + fs, + io::Read, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::Arc, + time::{Duration, Instant}, +}; + +const RECEIPT_ENV: &str = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_RECEIPT_V1"; +// CI supplies an independently measured digest while compiling this test binary. +// Runtime environment selects only the receipt path and cannot replace this pin. +const RECEIPT_SHA256: &str = + match option_env!("JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1") { + Some(measured) => measured, + None => "984f1f05f3ff09b66358cf5e213482c0e4d4b9ebfab8b0988f9a555246177753", + }; +struct Archive { + path: PathBuf, + archive_sha256: String, + target: String, + profile: String, + linker: PathBuf, + linker_sha256: Option, + libraries: Vec, + sdk_lib: Option, +} +fn digest(bytes: &[u8]) -> String { + jett_runtime::crypto::sha256_digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn field<'a>(value: &'a serde_json::Value, name: &str) -> &'a str { + value + .get(name) + .and_then(serde_json::Value::as_str) + .unwrap_or_else(|| panic!("archive receipt lacks string {name}")) +} +fn measured_file(value: &serde_json::Value, path: &str, hash: &str) -> PathBuf { + let path = PathBuf::from(field(value, path)); + assert!(path.is_absolute(), "receipt paths must be absolute"); + let metadata = fs::metadata(&path).unwrap(); + assert!( + metadata.is_file() && metadata.len() <= 256 * 1024 * 1024, + "{}", + path.display() + ); + let bytes = fs::read(&path).unwrap(); + assert_eq!( + digest(&bytes), + field(value, hash).to_ascii_lowercase(), + "{}", + path.display() + ); + path +} +fn archive(release: bool) -> Archive { + let path = PathBuf::from( + std::env::var_os(RECEIPT_ENV) + .expect("explicit Root-measured matched archive receipt is required"), + ); + let bytes = fs::read(path).unwrap(); + assert!(bytes.len() <= 1024 * 1024); + assert_eq!( + digest(&bytes), + RECEIPT_SHA256, + "exact independently measured archive receipt" + ); + let receipt: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!( + receipt.get("version").and_then(serde_json::Value::as_u64), + Some(1) + ); + let profile = if release { "release" } else { "debug" }; + let rows = receipt + .get("archives") + .and_then(serde_json::Value::as_array) + .unwrap(); + let matching = rows + .iter() + .filter(|row| field(row, "profile") == profile) + .collect::>(); + let [row] = matching.as_slice() else { + panic!("exact {profile} archive receipt must be unique"); + }; + let target = field(row, "target"); + assert_eq!(target, jett_driver::native::host_target()); + assert_eq!( + row.get("runtime_abi").and_then(serde_json::Value::as_u64), + Some(1) + ); + assert_eq!( + row.get("layout_wire").and_then(serde_json::Value::as_u64), + Some(3) + ); + assert_eq!( + row.get("private_cfgs"), + Some(&serde_json::json!([ + "test", + "jett_resource_native_test_archive" + ])) + ); + assert!(matches!(field(row, "crt_mode"), "static" | "dynamic")); + let path = measured_file(row, "archive_path", "archive_sha256"); + let linker = PathBuf::from(field(row, "linker_path")); + assert!(linker.is_absolute() && linker.is_file()); + for name in ["build_receipt", "native_static_libs_receipt", "crt_receipt"] { + let provenance = row.get(name).unwrap(); + let _ = measured_file(provenance, "path", "sha256"); + } + let libraries = row + .get("native_library_args") + .and_then(serde_json::Value::as_array) + .unwrap() + .iter() + .map(|item| { + let value = item.as_str().expect("exact native library argument"); + let lower = value.to_ascii_lowercase(); + assert!( + !lower.contains("jett_runtime") && !lower.contains("jett_native_launcher"), + "a second Rust runtime archive is forbidden" + ); + OsString::from(value) + }) + .collect::>(); + assert!( + !libraries.is_empty(), + "actual native-static-libs vector must be recorded" + ); + let library_receipt = measured_file( + row.get("native_static_libs_receipt").unwrap(), + "path", + "sha256", + ); + let observed: serde_json::Value = + serde_json::from_slice(&fs::read(library_receipt).unwrap()).unwrap(); + assert_eq!( + row.get("native_library_args"), + observed.get("arguments"), + "exact measured library order and duplicates" + ); + let crt_receipt = measured_file(row.get("crt_receipt").unwrap(), "path", "sha256"); + let crt: serde_json::Value = serde_json::from_slice(&fs::read(crt_receipt).unwrap()).unwrap(); + assert_eq!(field(row, "crt_mode"), field(&crt, "mode")); + assert_eq!(field(row, "linker_path"), field(&crt, "linker_path")); + let linker_sha256 = crt.get("linker_executable").map(|measurement| { + let measured = measured_file(measurement, "path", "sha256"); + assert_eq!( + fs::canonicalize(&measured).unwrap(), + fs::canonicalize(&linker).unwrap(), + "measured linker executable" + ); + field(measurement, "sha256").to_ascii_lowercase() + }); + if target == jett_driver::native::LINUX_GNU_NATIVE_TARGET { + assert_eq!(field(row, "crt_mode"), "dynamic"); + assert!(linker_sha256.is_some(), "GNU linker bytes must be measured"); + } + let sdk_lib = crt + .get("sdk_LIB") + .and_then(serde_json::Value::as_str) + .map(OsString::from); + Archive { + path, + archive_sha256: field(row, "archive_sha256").to_ascii_lowercase(), + target: target.into(), + profile: profile.into(), + linker, + linker_sha256, + libraries, + sdk_lib, + } +} +fn checked( + project: &Path, + stdlib: &Path, + release: bool, +) -> (Arc, jett_common::Span) { + let project_text = fs::read_to_string(project).unwrap(); + let support_text = fs::read_to_string(stdlib).unwrap(); + let primary = FileId::new(0); + let support = FileId::new(10_000); + let mut parsed = parse(&support_text, support); + let project = parse(&project_text, primary); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + let entries = project + .module + .items + .iter() + .filter_map(|item| match item { + Item::Function(function) + if function.name.name == "main" && function.name.span.file == primary => + { + Some(function.span) + } + _ => None, + }) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("actual primary main declaration must be unique"); + }; + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [resource] = resources.as_slice() else { + panic!("actual synthetic Stdlib Resource must be unique"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + let checked = CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (primary, SourceOrigin::Project), + (support, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "original checked Source: {error:?}; {:?}", + error.diagnostics() + ) + }); + (Arc::new(checked), *entry) +} +fn try_object( + project: &Path, + stdlib: &Path, + release: bool, +) -> Result { + let (checked, span) = checked(project, stdlib, release); + let required = jett_driver::evaluate_checked_resource_required_values(&checked); + assert!( + required.diagnostics.is_empty(), + "required Source evaluation: {:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required + .values + .checked_required_values(&checked) + .expect("required checked cache must not contain runtime Resource authority"); + let mut hir = jett_hir::lower_checked_resource_program(&checked).unwrap(); + jett_driver::bake_checked_resource_values(&mut hir, &checked, &required.values).unwrap(); + // Same original declaration span -> FunctionId join as the actual driver. + let entries = hir + .functions + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entries.as_slice() else { + panic!("original driver entry has no unique HIR identity"); + }; + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + jett_mir::validate(&mir).unwrap(); + jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) +} +fn object(project: &Path, stdlib: &Path, release: bool) -> jett_codegen_cranelift::ObjectArtifact { + try_object(project, stdlib, release).unwrap() +} + +fn prefix(prefix: &str, path: &Path) -> OsString { + let mut argument = OsString::from(prefix); + argument.push(path.as_os_str()); + argument +} +fn command(archive: &Archive, object: &Path, binary: &Path) -> Command { + assert_eq!( + digest(&fs::read(&archive.path).unwrap()), + archive.archive_sha256, + "measured runtime archive before each link" + ); + if let Some(expected) = &archive.linker_sha256 { + assert_eq!( + &digest(&fs::read(&archive.linker).unwrap()), + expected, + "measured linker bytes before each link" + ); + } + #[cfg(windows)] + let mut command = { + let tool = find_msvc_tools::find_tool(&archive.target, "link.exe") + .expect("same MSVC linker discovery used by driver"); + assert_eq!( + fs::canonicalize(tool.path()).unwrap(), + fs::canonicalize(&archive.linker).unwrap(), + "measured linker identity" + ); + let mut command = tool.to_command(); + let actual_lib = command + .get_envs() + .find_map(|(name, value)| { + name.to_str() + .filter(|name| name.eq_ignore_ascii_case("LIB")) + .and(value) + }) + .map(|value| value.to_os_string()) + .or_else(|| std::env::var_os("LIB")); + assert_eq!( + actual_lib, archive.sdk_lib, + "preserved measured MSVC SDK LIB environment" + ); + command.args([ + "/NOLOGO", + "/MACHINE:X64", + "/SUBSYSTEM:CONSOLE", + "/INCREMENTAL:NO", + "/MANIFEST:EMBED", + ]); + command + .arg(prefix("/OUT:", binary)) + .arg(prefix("/PDB:", &binary.with_extension("pdb"))); + command + }; + #[cfg(not(windows))] + let mut command = { + let mut command = Command::new(&archive.linker); + command.arg("-no-pie").arg("-o").arg(binary); + command + }; + command + .arg(object) + .arg(&archive.path) + .args(&archive.libraries); + command +} +fn link(archive: &Archive, object: &Path, binary: &Path, directory: &Path) { + // Match the driver native link timeout, file-backed capture and kill+reap contract. + let mut stdout = tempfile::NamedTempFile::new().unwrap(); + let mut stderr = tempfile::NamedTempFile::new().unwrap(); + let mut command = command(archive, object, binary); + command + .current_dir(directory) + .stdin(Stdio::null()) + .stdout(stdout.reopen().unwrap()) + .stderr(stderr.reopen().unwrap()); + let mut child = + super::ExecutionChild(command.spawn().expect("link sole matched private runtime")); + let start = Instant::now(); + let status = loop { + if let Some(status) = child.0.try_wait().unwrap() { + break status; + } + assert!( + start.elapsed() < jett_driver::native::DEFAULT_NATIVE_LINK_TIMEOUT, + "Resource native linker exceeded 60 seconds" + ); + std::thread::sleep(Duration::from_millis(10)); + }; + let read = |file: &mut fs::File| { + let mut bytes = Vec::new(); + file.take(file.metadata().unwrap().len()) + .read_to_end(&mut bytes) + .unwrap(); + bytes + }; + let out = read(stdout.as_file_mut()); + let err = read(stderr.as_file_mut()); + assert!( + status.success(), + "{status}; stdout={} stderr={}", + String::from_utf8_lossy(&out), + String::from_utf8_lossy(&err) + ); + assert!(binary.is_file()); +} +fn script(case: &cases::Case) -> String { + script_with_attempts(case, 1) +} + +fn script_with_attempts(case: &cases::Case, attempts: u32) -> String { + script_operations(case.script, attempts) +} + +fn script_operations(rows: &[cases::Script], attempts: u32) -> String { + let mut output = b"JTRST001".to_vec(); + output.extend(1u32.to_le_bytes()); + output.extend(0u32.to_le_bytes()); + output.extend(0u64.to_le_bytes()); + output.extend(attempts.to_le_bytes()); + output.extend((rows.len() as u32).to_le_bytes()); + for row in rows { + let (tag, label) = match *row { + cases::Script::Construct(label) => (1u32, label), + cases::Script::ConstructFail(label, _) => (2, label), + cases::Script::FinalizerPanic(label) => (3, label), + cases::Script::Borrow(label, _) => (4, label), + cases::Script::BorrowFail(label, _) => (5, label), + cases::Script::BorrowPanic(label) => (6, label), + }; + output.extend(tag.to_le_bytes()); + output.extend(label.to_le_bytes()); + match *row { + cases::Script::ConstructFail(_, error) | cases::Script::BorrowFail(_, error) => { + output.extend((error.len() as u32).to_le_bytes()); + output.extend(error.as_bytes()); + } + cases::Script::Borrow(_, value) => output.extend(value.to_le_bytes()), + _ => {} + } + } + let length = output.len() as u64; + output[16..24].copy_from_slice(&length.to_le_bytes()); + output.iter().map(|byte| format!("{byte:02x}")).collect() +} +fn event(expected: cases::Event) -> (i64, report::EventKind) { + match expected { + cases::Event::Constructed(label) => (label, report::EventKind::Constructed), + cases::Event::ConstructionFailed(label) => (label, report::EventKind::ConstructionFailed), + cases::Event::Borrowed(label) => (label, report::EventKind::Borrowed), + cases::Event::BorrowFailed(label) => (label, report::EventKind::BorrowFailed), + cases::Event::Finalized(label) => (label, report::EventKind::Finalized), + } +} +fn assert_report(case: &cases::Case, report: &report::Report) { + assert_eq!(report.attempts.len(), 1, "{}", case.name); + let attempt = &report.attempts[0]; + assert_eq!( + attempt.completion.body_status, case.body, + "{} original body; full report: {report:?}", + case.name + ); + assert_eq!( + attempt.completion.cleanup_status, case.cleanup, + "{} cleanup", + case.name + ); + let channel = match case.channel { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid oracle"), + }; + assert_eq!( + attempt.completion.selected_channel, channel, + "{} channel", + case.name + ); + assert_eq!( + attempt.ordinary_message, case.ordinary_message, + "{} ordinary message", + case.name + ); + assert_eq!( + attempt.resource_message, case.resource_message, + "{} resource message", + case.name + ); + assert!( + report.observations_ready_for_acceptance(), + "{} zero BEFORE-teardown obligations: {report:?}", + case.name + ); + assert_eq!(report.events.len(), case.events.len()); + for (index, (actual, expected)) in report.events.iter().zip(case.events).enumerate() { + let (label, kind) = event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + case.name + ); + } + assert_eq!(attempt.event_sequence_end, case.events.len() as u64); + assert_eq!(report.script_remaining, 0); + assert_eq!( + ( + report.created, + report.destroy_attempts, + report.destroy_status, + report.write_status + ), + (true, 1, 0, 0) + ); + assert_eq!( + report.all_attempts_succeeded(), + case.body == 0 && case.cleanup == 0 && case.channel == 0 + ); +} +#[test] +fn native_resource_receipt_pin_runtime_env_cannot_replace_compiled_pin() { + let directory = tempfile::tempdir().unwrap(); + let receipt = directory.path().join("unmeasured-receipt.json"); + let bytes = br#"{"version":1,"archives":[]}"#; + fs::write(&receipt, bytes).unwrap(); + let forged = digest(bytes); + assert_ne!(forged, RECEIPT_SHA256); + let output = Command::new(std::env::current_exe().unwrap()) + .args([ + "resource_execution::native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown", + "--ignored", + "--exact", + "--test-threads=1", + ]) + .env(RECEIPT_ENV, &receipt) + .env("JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1", forged) + .current_dir(directory.path()) + .output() + .unwrap(); + assert!(!output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stdout.contains("exact independently measured archive receipt") + || stderr.contains("exact independently measured archive receipt"), + "{output:?}" + ); +} + +#[derive(Eq, PartialEq)] +struct LinkedProgramKey { + project_bytes: Vec, + support_bytes: Vec, + release: bool, + archive_path: PathBuf, + archive_sha256: String, + target: String, + profile: String, +} +impl LinkedProgramKey { + fn new(source: &str, support: &str, release: bool, archive: &Archive) -> Self { + Self { + project_bytes: source.as_bytes().to_vec(), + support_bytes: support.as_bytes().to_vec(), + release, + archive_path: archive.path.clone(), + archive_sha256: archive.archive_sha256.clone(), + target: archive.target.clone(), + profile: archive.profile.clone(), + } + } +} + +fn run_native_cases(selected: &[cases::Case]) { + run_native_cases_with_support(selected, cases::SUPPORT); +} + +fn run_native_cases_with_support(selected: &[cases::Case], support_source: &str) { + run_native_cases_observing(selected, support_source, |_, _| {}); +} + +fn run_native_cases_observing( + selected: &[cases::Case], + support_source: &str, + mut observe: impl FnMut(&cases::Case, &report::Report), +) { + struct LinkedProgram { + key: LinkedProgramKey, + directory: tempfile::TempDir, + binary: PathBuf, + binary_sha256: String, + } + + for release in [false, true] { + let archive = archive(release); + // Retain only completed files under the same exact project/support bytes, + // checked profile, target and measured archive. FileIds/origins and kernel + // catalog stay fixed by checked(); witnesses and runtime state are never cached. + let mut previous: Option = None; + let mut builds = 0usize; + for case in selected { + let key = LinkedProgramKey::new(case.source, support_source, release, &archive); + let reuse = previous.as_ref().is_some_and(|program| program.key == key); + if !reuse { + // Retire the previous files before allocating the next build directory. + // At most one completed program is retained for the current profile. + drop(previous.take()); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, support_source).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + // Every actual link retains the measured receipt/archive/linker guards. + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!( + !entry.exists() && !support.exists() && !project.exists() && !stdlib.exists() + ); + let binary_sha256 = digest(&fs::read(&binary).unwrap()); + previous = Some(LinkedProgram { + key, + directory, + binary, + binary_sha256, + }); + builds += 1; + } + let program = previous.as_ref().unwrap(); + let compiled = program.directory.path(); + assert!( + !compiled.join("project/main.jett").exists() + && !compiled + .join("synthetic-stdlib/resource_probe.jett") + .exists() + && !compiled.join("project").exists() + && !compiled.join("synthetic-stdlib").exists() + ); + assert_eq!( + digest(&fs::read(&program.binary).unwrap()), + program.binary_sha256, + "completed native program bytes before every execution" + ); + // A cache hit still gets a cold directory, fresh child and its own report. + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + assert!(!root.join("resource-native-report-v1.bin").exists()); + assert!(!root.join("project").exists() && !root.join("synthetic-stdlib").exists()); + if std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_some() { + // Preserve the existing per-case object evidence without copying a + // linked executable into every transient execution directory. + fs::copy(compiled.join("resource.obj"), root.join("resource.obj")).unwrap(); + } + let mut lightweight = lightweight_observations::Capture::prepare( + "single", + case, + None, + support_source, + &archive, + &compiled.join("resource.obj"), + &program.binary, + root, + compiled, + builds, + !reuse, + ); + let encoded = script(case); + let actual = super::run_bounded_with_env( + &program.binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let bytes = fs::read(path).unwrap(); + if let Some(capture) = &mut lightweight { + capture.save_output(&bytes, &actual); + } + let report = report::decode(&bytes).unwrap(); + assert_eq!( + actual.status.code(), + Some(case.exit), + "{} {}: {actual:?}; {report:?}", + case.name, + archive.profile + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + assert_report(case, &report); + observe(case, &report); + if let Some(capture) = lightweight { + directory.close().unwrap(); + capture.finish(&report, &actual); + } + } + eprintln!( + "Resource native build reuse: profile={}; {} Source-deleted executions; {builds} emitted/linked programs", + archive.profile, + selected.len() + ); + } +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown() { + run_native_cases(cases::CASES); + eprintln!( + "Resource native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + cases::CASES.len(), + 2 * cases::CASES.len() + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_pipeline_original_source_lifecycle_matches_reference_and_retires_before_teardown() + { + let selected = cases::pipeline_cases(); + run_native_cases(selected); + eprintln!( + "Resource pipeline native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + selected.len(), + 2 * selected.len() + ); +} + +fn run_object_preflight(selected: &[cases::Case], family: &str) { + run_object_preflight_with_support(selected, family, cases::SUPPORT); +} + +fn run_object_preflight_with_support(selected: &[cases::Case], family: &str, support_source: &str) { + let expected = 2 * selected.len(); + let mut emitted = 0usize; + let mut refusals = Vec::new(); + for release in [false, true] { + for case in selected { + let directory = tempfile::tempdir().unwrap(); + let entry = directory.path().join("main.jett"); + let support = directory.path().join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, support_source).unwrap(); + // Source/HIR/MIR assertions remain fatal; only final CodegenError is collected. + let outcome = try_object(&entry, &support, release); + fs::remove_file(&entry).unwrap(); + fs::remove_file(&support).unwrap(); + assert!(!entry.exists() && !support.exists()); + match outcome { + Ok(artifact) => { + assert_eq!(artifact.target, jett_driver::native::host_target()); + assert!( + artifact.symbols.iter().any( + |symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1 + ) + ); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + emitted += 1; + } + Err(error) => { + let refusal = format!("case={} release={release}: {error:?}", case.name); + eprintln!("Resource {family} native object refusal: {refusal}"); + refusals.push(refusal); + } + } + } + } + assert_eq!(emitted + refusals.len(), expected); + assert!( + refusals.is_empty(), + "Resource {family} native object preflight: {}/{} attempts refused\n{}", + refusals.len(), + expected, + refusals.join("\n") + ); + assert_eq!(emitted, expected); + eprintln!( + "Resource {family} native object preflight: {} cases; {emitted} emitted objects; profiles=debug,release; no linked execution", + selected.len() + ); +} + +#[test] +fn native_resource_pipeline_object_preflight_checks_all_sources_in_both_profiles() { + run_object_preflight(cases::pipeline_cases(), "pipeline"); +} + +#[test] +fn native_resource_control_flow_object_preflight_checks_all_sources_in_both_profiles() { + run_object_preflight(cases::control_flow_cases(), "control-flow"); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_control_flow_original_source_lifecycle_matches_reference_and_retires_before_teardown() + { + let selected = cases::control_flow_cases(); + run_native_cases(selected); + eprintln!( + "Resource control-flow native acceptance: {} cases; {} Source-deleted executions; profiles=debug,release", + selected.len(), + 2 * selected.len() + ); +} + +#[path = "resource_reflected_field_harness.rs"] +mod reflected_field_harness; + +#[path = "resource_execution/absent_aggregate_baseline.rs"] +mod absent_aggregate_baseline; + +#[path = "resource_occupied_struct_harness.rs"] +mod occupied_struct_harness; + +#[path = "resource_execution/source17_suites.rs"] +mod source17_suites; + +#[path = "resource_execution/observation_controls.rs"] +mod observation_controls; + +#[path = "resource_execution/sum_replacement.rs"] +mod sum_replacement; + +#[path = "resource_execution/lightweight_observations.rs"] +mod lightweight_observations; diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs b/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs new file mode 100644 index 00000000..69d49731 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/absent_aggregate_baseline.rs @@ -0,0 +1,449 @@ +//! Whole Source17 through strict object emission and Source-deleted native execution. +use super::*; +#[path = "../resource_absent_aggregate_baseline_inputs.rs"] +mod inputs; + +fn emit( + input: &inputs::Input, + release: bool, + entered: &mut Vec, +) -> Result { + use inputs::{Phase, enter, failure}; + let (checked, span) = inputs::prepare(input, release, entered)?; + enter(entered, Phase::RequiredCapture); + let required = jett_driver::evaluate_checked_resource_required_values(&checked); + if !required.diagnostics.is_empty() || !required.debug_events.is_empty() { + return Err(failure( + Phase::RequiredCapture, + (&required.diagnostics, &required.debug_events), + )); + } + enter(entered, Phase::RequiredCache); + required + .values + .checked_required_values(&checked) + .map_err(|error| failure(Phase::RequiredCache, error))?; + if required.values.is_empty() + || !required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + { + return Err(failure( + Phase::RequiredCache, + "all original required values must be primitive 17", + )); + } + let constants = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(declaration) + if declaration.name.span.file == FileId::new(0) + && declaration.name.name == "absent_namespace_value" => + { + Some(declaration.name.span) + } + _ => None, + }) + .collect::>(); + let [constant] = constants.as_slice() else { + return Err(failure( + Phase::RequiredCache, + "unique original namespace constant required", + )); + }; + if required.values.constant(*constant) != Some(&jett_comptime::Value::Int64(17)) { + return Err(failure( + Phase::RequiredCache, + "original namespace value must be baked as 17", + )); + } + enter(entered, Phase::HirLowering); + let mut hir = jett_hir::lower_checked_resource_program(&checked) + .map_err(|error| failure(Phase::HirLowering, error))?; + enter(entered, Phase::ValueBaking); + jett_driver::bake_checked_resource_values(&mut hir, &checked, &required.values) + .map_err(|error| failure(Phase::ValueBaking, error))?; + for name in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + "refined_shape_contract", + "absent_required_controls", + ] { + if !hir.functions.iter().any(|function| { + function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) { + return Err(failure( + Phase::ValueBaking, + ("original declaration must remain", name), + )); + } + } + enter(entered, Phase::HirEntryIdentity); + let entries = hir + .functions + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.kind == jett_hir::DeclarationKind::Function + }) + .map(|function| function.id) + .collect::>(); + let [entry] = entries.as_slice() else { + return Err(failure( + Phase::HirEntryIdentity, + "unique original main -> HIR identity required", + )); + }; + enter(entered, Phase::ValueConversions); + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner) + .map_err(|error| failure(Phase::ValueConversions, error))?; + enter(entered, Phase::MirLowering); + let mir = jett_mir::lower(&hir, &checked.checked().interner) + .map_err(|error| failure(Phase::MirLowering, error))?; + enter(entered, Phase::MirValidation); + jett_mir::validate(&mir).map_err(|error| failure(Phase::MirValidation, error))?; + enter(entered, Phase::ObjectEmission); + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + *entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .map_err(|error| failure(Phase::ObjectEmission, error))?; + enter(entered, Phase::ObjectObservations); + if object.bytes.is_empty() + || object.target != jett_driver::native::host_target() + || !object + .symbols + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + || !object + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + { + return Err(failure( + Phase::ObjectObservations, + ( + &object.target, + &object.symbols, + "nonempty host object with entry and Resource manifest required", + ), + )); + } + Ok(object) +} + +#[test] +fn native_resource_absent_aggregate_object_baseline_records_all_first_rejecting_phases() { + assert_eq!(inputs::INPUTS.len(), 14); + let mut emitted = 0; + let mut failures = Vec::new(); + for release in [false, true] { + for input in inputs::INPUTS { + let attempt = inputs::attempt(|entered| emit(input, release, entered)); + match attempt.outcome { + Ok(object) => { + emitted += 1; + eprintln!( + "ABSENT_OBJECT name={:?} release={release} entered={:?} bytes={} target={:?}", + input.name, + attempt.entered, + object.bytes.len(), + object.target + ); + } + Err(failure) => { + eprintln!( + "ABSENT_OBJECT name={:?} release={release} entered={:?} failure={failure:?}", + input.name, attempt.entered + ); + failures.push((input.name, release, failure)); + } + } + } + } + assert_eq!(emitted + failures.len(), 28); + eprintln!( + "ABSENT_OBJECT_TOTAL attempts=28 emitted={emitted} failed={}", + failures.len() + ); + assert!( + failures.is_empty(), + "strict object gate failures: {failures:#?}" + ); + assert_eq!(emitted, 28); +} + +struct NativeExpectation { + body: u32, + channel: report::FailureChannel, + ordinary_message: &'static [u8], + resource_message: &'static [u8], + exit: i32, +} +fn expected(input: &inputs::Input) -> NativeExpectation { + match input.reference { + inputs::ReferenceExpectation::Nothing => NativeExpectation { + body: 0, + channel: report::FailureChannel::Clean, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + inputs::ReferenceExpectation::Error(error) => { + assert_eq!(input.name, "13_wrong_empty_length_control"); + assert_eq!(error, "list.__remove_at: index -1 out of bounds"); + NativeExpectation { + body: 1, + channel: report::FailureChannel::Body, + ordinary_message: error.as_bytes(), + // terminal_label fails in the ordinary list kernel; the Resource channel stays empty. + resource_message: b"", + exit: 71, + } + } + } +} + +fn empty_script(attempts: u32) -> String { + assert!(matches!(attempts, 1 | 2)); + let mut bytes = b"JTRST001".to_vec(); + bytes.extend(1u32.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + bytes.extend(32u64.to_le_bytes()); + bytes.extend(attempts.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + assert_eq!(bytes.len(), 32); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn assert_observed(input: &inputs::Input, observed: &report::Report, attempts: usize) { + let expected = expected(input); + assert_eq!( + observed.attempts.len(), + attempts, + "{}: {observed:?}", + input.name + ); + let mut previous = 0; + for (index, attempt) in observed.attempts.iter().enumerate() { + assert!( + attempt.completion.attempt > previous, + "{} attempt identity", + input.name + ); + previous = attempt.completion.attempt; + assert_eq!( + attempt.completion.body_status, expected.body, + "{} entry{index} original body: {observed:?}", + input.name + ); + assert_eq!( + attempt.completion.cleanup_status, 0, + "{} cleanup", + input.name + ); + assert_eq!( + attempt.completion.selected_channel, expected.channel, + "{} channel", + input.name + ); + assert_eq!( + attempt.ordinary_message, expected.ordinary_message, + "{} ordinary message", + input.name + ); + assert_eq!( + attempt.resource_message, expected.resource_message, + "{} resource message", + input.name + ); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} entry{index} obligations BEFORE destruction: {observed:?}", + input.name + ); + assert_eq!( + attempt.event_sequence_end, 0, + "{} event boundary", + input.name + ); + } + assert!(observed.events.is_empty(), "{}: {observed:?}", input.name); + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status + ), + (true, 1, 0, 0) + ); + assert!( + observed.observations_ready_for_acceptance(), + "{}: {observed:?}", + input.name + ); + assert_eq!(observed.all_attempts_succeeded(), expected.body == 0); +} + +fn execute(input: &inputs::Input, release: bool, archive: &Archive, attempts: u32) { + assert!(input.source.starts_with(inputs::ORIGINAL)); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let sources = [ + (project.join("main.jett"), input.source), + (stdlib.join("resource_probe.jett"), inputs::SUPPORT), + (stdlib.join("list_length.jett"), inputs::LIST_LENGTH), + (stdlib.join("map_length.jett"), inputs::MAP_LENGTH), + ]; + for (path, source) in &sources { + fs::write(path, source).unwrap(); + assert_eq!(fs::read(path).unwrap(), source.as_bytes()); + } + // The shared preparation authenticates these exact finite Source bytes, + // trusted fragments and original FileId/span/DefId joins. + let attempt = inputs::attempt(|entered| emit(input, release, entered)); + let artifact = attempt.outcome.unwrap_or_else(|failure| { + panic!( + "{} release={release} entered={:?}: {failure:?}", + input.name, attempt.entered + ) + }); + assert_eq!(artifact.target, archive.target); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!project.exists() && !stdlib.exists()); + assert!(sources.iter().all(|(path, _)| !path.exists())); + let encoded = empty_script(attempts); + let actual = super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let report_path = root.join("resource-native-report-v1.bin"); + assert!(report_path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(report_path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(expected(input).exit), + "{} {}: {actual:?}; {observed:?}", + input.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_observed(input, &observed, usize::try_from(attempts).unwrap()); + eprintln!( + "ABSENT_NATIVE name={:?} release={release} attempts={attempts} source_deleted=true report={observed:?}", + input.name + ); +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum NativeAttemptMode { + Single, + CleanReentry, + AllReentry, +} + +fn run_native(mode: NativeAttemptMode) { + let clean_reentry = mode == NativeAttemptMode::CleanReentry; + assert_eq!(inputs::INPUTS.len(), 14); + let mut passed = 0; + let mut failures = Vec::new(); + for release in [false, true] { + let archive = archive(release); + for input in inputs::INPUTS { + if clean_reentry && matches!(input.reference, inputs::ReferenceExpectation::Error(_)) { + continue; + } + let attempts = if mode == NativeAttemptMode::Single { + 1 + } else { + 2 + }; + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + execute(input, release, &archive, attempts) + })); + match outcome { + Ok(()) => passed += 1, + Err(payload) => { + failures.push((input.name, release, inputs::panic_message(payload.as_ref()))) + } + } + } + } + let required = if clean_reentry { 26 } else { 28 }; + assert_eq!(passed + failures.len(), required); + assert!( + failures.is_empty(), + "strict Source-deleted native gate failures: {failures:#?}" + ); + assert_eq!(passed, required); + let entries = required + * if mode == NativeAttemptMode::Single { + 1 + } else { + 2 + }; + eprintln!( + "ABSENT_NATIVE_TOTAL executables={passed} entries={entries} clean_reentry={clean_reentry}" + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_absent_aggregate_all_original_sources_retire_before_teardown() { + run_native(NativeAttemptMode::Single); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_absent_aggregate_thirteen_clean_sources_same_grant_reentry() { + // Source13 remains in the full single-entry gate above. Its original two-error + // reference behavior is a separate unresolved native failure-lifetime obligation. + run_native(NativeAttemptMode::CleanReentry); +} + +#[test] +#[ignore = "requires exact Root-measured ordinary-error-reentry Debug/Release archive receipt"] +fn native_resource_absent_aggregate_all_original_sources_reenter_with_same_provider_grant() { + // Every original row is selected, including Source13's signed bounds error + // twice on one retained context/provider/grant. assert_observed checks both + // original completions, exact diagnostics, zero pre-teardown obligations, + // continuous empty events and first process failure; execute deletes Source. + run_native(NativeAttemptMode::AllReentry); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/lightweight_observations.rs b/crates/jett_driver/tests/native_conformance/resource_execution/lightweight_observations.rs new file mode 100644 index 00000000..23f4d9dd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/lightweight_observations.rs @@ -0,0 +1,202 @@ +//! Optional small evidence rows; this never copies linked programs or objects. +//! A final observation.json appears only after the existing oracles and disposal. +use super::{Archive, cases, digest, report}; +use std::{ + fs::{self, OpenOptions}, + io::Write, + path::{Path, PathBuf}, + process::Output, +}; + +pub(super) const ENV: &str = "JETT_RESOURCE_SUM_NATIVE_OBSERVATIONS_DIR_V1"; + +fn selected_root() -> Option { + let root = PathBuf::from(std::env::var_os(ENV)?); + assert!( + root.is_absolute(), + "lightweight evidence root must be absolute" + ); + let metadata = fs::symlink_metadata(&root).unwrap(); + assert!(metadata.is_dir() && !metadata.file_type().is_symlink()); + Some(root) +} + +pub(super) fn assert_fresh_root() { + if let Some(root) = selected_root() { + assert!( + std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_none(), + "lightweight observations exclude copy-all evidence" + ); + assert!( + fs::read_dir(root).unwrap().next().is_none(), + "use an empty fresh evidence root" + ); + } +} + +fn absent(path: &Path) -> bool { + match fs::symlink_metadata(path) { + Ok(_) => false, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => true, + Err(error) => panic!("observe {}: {error}", path.display()), + } +} + +fn file_identity(path: &Path) -> serde_json::Value { + let metadata = fs::metadata(path).unwrap(); + assert!(metadata.is_file() && metadata.len() <= 256 * 1024 * 1024); + let bytes = fs::read(path).unwrap(); + serde_json::json!({"path": path, "bytes": bytes.len(), "sha256": digest(&bytes)}) +} + +fn write_new(path: &Path, bytes: &[u8]) { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .unwrap(); + file.write_all(bytes).unwrap(); +} + +pub(super) struct Capture { + destination: PathBuf, + execution_root: PathBuf, + compilation_root: PathBuf, + metadata: serde_json::Value, + payloads_saved: bool, +} + +impl Capture { + pub(super) fn prepare( + mode: &str, + case: &cases::Case, + ready: Option<&cases::Case>, + support: &str, + archive: &Archive, + object: &Path, + binary: &Path, + execution_root: &Path, + compilation_root: &Path, + build_ordinal: usize, + freshly_built: bool, + ) -> Option { + let root = selected_root()?; + assert!(matches!(mode, "single" | "reentry")); + assert!( + case.name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + ); + assert!(absent(&compilation_root.join("project"))); + assert!(absent(&compilation_root.join("synthetic-stdlib"))); + assert!(absent(&compilation_root.join("project/main.jett"))); + assert!(absent( + &compilation_root.join("synthetic-stdlib/resource_probe.jett") + )); + assert!(execution_root.is_dir()); + let destination = root.join(format!("{mode}-{}-{}", archive.profile, case.name)); + fs::create_dir(&destination).unwrap(); + let metadata = serde_json::json!({ + "schema": "resource-sum-lightweight-native-observation-v1", + "mode": mode, + "case": case.name, + "ready_case": ready.map(|case| case.name), + "source_sha256": digest(case.source.as_bytes()), + "support_sha256": digest(support.as_bytes()), + "ready_source_sha256": ready.map(|case| digest(case.source.as_bytes())), + "profile": archive.profile, + "checked_release": archive.profile == "release", + "target": archive.target, + "linked_program_build_ordinal": build_ordinal, + "build_ordinal_scope": if mode == "single" { "profile" } else { "child" }, + "freshly_built_for_case": freshly_built, + "runtime_abi": 1, + "layout_wire": 3, + "validated_archive_receipt_sha256": super::RECEIPT_SHA256, + "archive": {"path": archive.path, "sha256": archive.archive_sha256}, + "object_before_launch": file_identity(object), + "executable_before_launch": file_identity(binary), + "execution_cwd": execution_root, + "compilation_directory": compilation_root, + "source_and_support_absent_before_launch": true, + "copies_objects_or_executables": false, + "physical_storage_measured": false, + "checked_program_weak_release_measured": false, + "full_acceptance": false, + }); + Some(Self { + destination, + execution_root: execution_root.to_owned(), + compilation_root: compilation_root.to_owned(), + metadata, + payloads_saved: false, + }) + } + + // Retain raw process bytes before any report decode or case assertion can fail. + // Without the final metadata such a row remains incomplete, never accepted. + pub(super) fn save_output(&mut self, bytes: &[u8], actual: &Output) { + assert!(!self.payloads_saved); + for (name, payload) in [ + ("report.bin", bytes), + ("stdout.bin", actual.stdout.as_slice()), + ("stderr.bin", actual.stderr.as_slice()), + ] { + write_new(&self.destination.join(name), payload); + self.metadata[name] = serde_json::json!({ + "bytes": payload.len(), "sha256": digest(payload), + }); + } + self.payloads_saved = true; + } + + pub(super) fn finish(mut self, observed: &report::Report, actual: &Output) { + assert!(self.payloads_saved); + assert!( + absent(&self.execution_root), + "execution TempDir.close must succeed first" + ); + self.metadata["process"] = serde_json::json!({ + "bounded_runner_returned": true, + "exit_code": actual.status.code(), + "success": actual.status.success(), + "status_debug": format!("{:?}", actual.status), + }); + self.metadata["execution_tempdir_closed_and_absent"] = serde_json::json!(true); + self.metadata["compilation_directory_absent_after_run"] = + serde_json::json!(absent(&self.compilation_root)); + self.metadata["attempt_count"] = serde_json::json!(observed.attempts.len()); + self.metadata["attempts"] = serde_json::json!( + observed + .attempts + .iter() + .map(|attempt| { + serde_json::json!({ + "id": attempt.completion.attempt.to_string(), + "body_status": attempt.completion.body_status, + "cleanup_status": attempt.completion.cleanup_status, + "channel": format!("{:?}", attempt.completion.selected_channel), + "event_sequence_end": attempt.event_sequence_end.to_string(), + "resources_retired": attempt.counts.resources_retired(), + "ordinary_empty": attempt.counts.ordinary_empty, + }) + }) + .collect::>() + ); + self.metadata["observations"] = serde_json::json!({ + "flags": observed.observations.flags, + "counters": observed.observations.counters.values.map(|value| value.to_string()), + "attempts": observed.observations.attempts.iter().map(|attempt| { + serde_json::json!({ + "id": attempt.identity.id.to_string(), + "ordinal": attempt.identity.ordinal, + "begin_counters": attempt.begin.counters.values.map(|value| value.to_string()), + "end_counters": attempt.end.map(|end| end.counters.values.map(|value| value.to_string())), + }) + }).collect::>(), + }); + self.metadata["existing_oracles_and_observer_returned"] = serde_json::json!(true); + let bytes = serde_json::to_vec_pretty(&self.metadata).unwrap(); + write_new(&self.destination.join("observation.json"), &bytes); + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/observation_controls.rs b/crates/jett_driver/tests/native_conformance/resource_execution/observation_controls.rs new file mode 100644 index 00000000..3b98924b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/observation_controls.rs @@ -0,0 +1,1081 @@ +//! Private host ABI observations, never evidence of compiled Source execution. +use super::super::resource_report as report; +use jett_runtime::native_abi::JettRuntimeStatusV1; + +const MAX_NESTED: usize = 16 * 1024 * 1024; +const MAX_HOST: usize = MAX_NESTED + 256; +#[derive(Debug, PartialEq, Eq)] +pub(super) enum HostDecodeError { + TooLarge, + Truncated, + Magic, + Version(u32), + Reserved(u32), + Length, + Tag(&'static str, u32), + Presence, + Trailing, + Nested(report::DecodeError), +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ControlOrigin { + HostAbi, + PureRecorder, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ControlPurpose { + Runtime, + Property, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ControlResult { + Unavailable, + HostStatus(u32), + RecorderOk, + RecorderErr, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RecordedBody { + NoError, + WrongPurpose, + InvalidHandle, + WrongOperation, + WrongFrame, + OtherResource(u32), + HostPanic, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct OversizeProbe { + pub(super) before_length: u64, + pub(super) after_length: u64, + pub(super) bytes_preserved: bool, + pub(super) cleared: bool, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct HostControl { + pub(super) mode: u32, + pub(super) origin: ControlOrigin, + pub(super) failed: bool, + pub(super) write_status: u32, + pub(super) result: ControlResult, + pub(super) recorded_body: Option, + pub(super) oversize_probe: Option, + pub(super) acquire_operation: Option, + pub(super) attempt: Option, + pub(super) purpose: Option, + pub(super) created: bool, + pub(super) destroy_attempts: u32, + pub(super) destroy_status: Option, + pub(super) report: Option, +} +struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, +} +impl<'a> Cursor<'a> { + fn take(&mut self, length: usize) -> Result<&'a [u8], HostDecodeError> { + let end = self + .offset + .checked_add(length) + .ok_or(HostDecodeError::TooLarge)?; + let value = self + .bytes + .get(self.offset..end) + .ok_or(HostDecodeError::Truncated)?; + self.offset = end; + Ok(value) + } + fn word(&mut self) -> Result { + Ok(u32::from_le_bytes( + self.take(4)? + .try_into() + .map_err(|_| HostDecodeError::Truncated)?, + )) + } + fn wide(&mut self) -> Result { + Ok(u64::from_le_bytes( + self.take(8)? + .try_into() + .map_err(|_| HostDecodeError::Truncated)?, + )) + } + fn flag(&mut self, name: &'static str) -> Result { + match self.word()? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(HostDecodeError::Tag(name, value)), + } + } + fn optional_word(&mut self, name: &'static str) -> Result, HostDecodeError> { + if self.flag(name)? { + self.word().map(Some) + } else { + Ok(None) + } + } + fn optional_wide(&mut self, name: &'static str) -> Result, HostDecodeError> { + if self.flag(name)? { + self.wide().map(Some) + } else { + Ok(None) + } + } +} +pub(super) fn decode_host(bytes: &[u8]) -> Result { + if bytes.len() > MAX_HOST { + return Err(HostDecodeError::TooLarge); + } + let mut input = Cursor { bytes, offset: 0 }; + if input.take(8)? != b"JTRHC002" { + return Err(HostDecodeError::Magic); + } + let version = input.word()?; + if version != 2 { + return Err(HostDecodeError::Version(version)); + } + let reserved = input.word()?; + if reserved != 0 { + return Err(HostDecodeError::Reserved(reserved)); + } + if input.wide()? != bytes.len() as u64 { + return Err(HostDecodeError::Length); + } + let mode = input.word()?; + if !(1..=12).contains(&mode) { + return Err(HostDecodeError::Tag("mode", mode)); + } + let origin = match input.word()? { + 1 => ControlOrigin::HostAbi, + 2 => ControlOrigin::PureRecorder, + value => return Err(HostDecodeError::Tag("origin", value)), + }; + if (mode == 11) != (origin == ControlOrigin::PureRecorder) { + return Err(HostDecodeError::Presence); + } + let failed = input.flag("failed")?; + let write_status = input.word()?; + let result = match input.word()? { + 0 => ControlResult::Unavailable, + 1 => ControlResult::HostStatus(input.word()?), + 2 => ControlResult::RecorderOk, + 3 => ControlResult::RecorderErr, + value => return Err(HostDecodeError::Tag("result", value)), + }; + let recorded_body = match input.word()? { + 0 => None, + 1 => Some(RecordedBody::NoError), + 2 => Some(RecordedBody::WrongPurpose), + 3 => Some(RecordedBody::InvalidHandle), + 4 => Some(RecordedBody::WrongOperation), + 5 => Some(RecordedBody::WrongFrame), + 6 => { + let status = input.word()?; + if status == 0 { + return Err(HostDecodeError::Presence); + } + Some(RecordedBody::OtherResource(status)) + } + 7 => Some(RecordedBody::HostPanic), + value => return Err(HostDecodeError::Tag("reason", value)), + }; + let oversize_probe = if input.flag("probe")? { + Some(OversizeProbe { + before_length: input.wide()?, + after_length: input.wide()?, + bytes_preserved: input.flag("bytes_preserved")?, + cleared: input.flag("cleared")?, + }) + } else { + None + }; + let acquire_operation = input.optional_word("acquire")?; + let attempt = input.optional_wide("entry")?; + let purpose = if attempt.is_some() { + Some(match input.word()? { + 1 => ControlPurpose::Runtime, + 2 => ControlPurpose::Property, + value => return Err(HostDecodeError::Tag("purpose", value)), + }) + } else { + None + }; + let created = input.flag("created")?; + let destroy_attempts = input.word()?; + if destroy_attempts > 1 { + return Err(HostDecodeError::Presence); + } + let destroy_status = input.optional_word("destroy_status")?; + let nested = input.flag("nested")?; + let nested_report = if nested { + if origin != ControlOrigin::HostAbi { + return Err(HostDecodeError::Presence); + } + let length = usize::try_from(input.wide()?).map_err(|_| HostDecodeError::TooLarge)?; + if length > MAX_NESTED { + return Err(HostDecodeError::TooLarge); + } + Some(report::decode(input.take(length)?).map_err(HostDecodeError::Nested)?) + } else { + None + }; + if input.offset != bytes.len() { + return Err(HostDecodeError::Trailing); + } + let value = HostControl { + mode, + origin, + failed, + write_status, + result, + recorded_body, + oversize_probe, + acquire_operation, + attempt, + purpose, + created, + destroy_attempts, + destroy_status, + report: nested_report, + }; + value.validate_shape()?; + Ok(value) +} +impl HostControl { + fn validate_shape(&self) -> Result<(), HostDecodeError> { + let bad = HostDecodeError::Presence; + if self.origin == ControlOrigin::PureRecorder { + if matches!(self.result, ControlResult::HostStatus(_)) + || self.recorded_body.is_some() + || self.oversize_probe.is_some() + || self.acquire_operation.is_some() + || self.attempt.is_some() + || self.purpose.is_some() + || self.created + || self.destroy_attempts != 0 + || self.destroy_status.is_some() + || self.report.is_some() + { + return Err(bad); + } + if self.result != ControlResult::RecorderOk && !self.failed { + return Err(bad); + } + } else { + if matches!( + self.result, + ControlResult::RecorderOk | ControlResult::RecorderErr + ) || self.attempt == Some(0) + || self.purpose.is_some() != self.attempt.is_some() + || (self.oversize_probe.is_some() && self.mode != 12) + || (self.acquire_operation.is_some() && self.mode == 12) + || (self.destroy_status.is_some() != (self.destroy_attempts == 1)) + || (!self.created + && (self.destroy_attempts != 0 + || self.attempt.is_some() + || self.report.is_some())) + || self.purpose.is_some_and(|purpose| { + (purpose == ControlPurpose::Property) != (self.mode == 10) + }) + { + return Err(bad); + } + if !self.failed + && (self.result == ControlResult::Unavailable + || self.recorded_body.is_none() + || self.attempt.is_none() + || !self.created + || self.report.is_none() + || self.destroy_status.is_none() + || (self.mode == 12 && self.oversize_probe.is_none()) + || (self.mode != 12 && self.acquire_operation.is_none())) + { + return Err(bad); + } + if let Some(report) = &self.report { + let ControlResult::HostStatus(status) = self.result else { + return Err(bad); + }; + if report.attempts.len() != 1 + || Some(report.attempts[0].completion.attempt) != self.attempt + || report.attempts[0].completion.body_status != status + || report.created != self.created + || report.destroy_attempts != self.destroy_attempts + || Some(report.destroy_status) != self.destroy_status + || report.write_status != self.write_status + { + return Err(bad); + } + } + } + if self.write_status != 0 && !self.failed { + return Err(bad); + } + Ok(()) + } + pub(super) fn passed(&self) -> bool { + if self.failed || self.write_status != 0 || self.validate_shape().is_err() { + return false; + } + if self.origin == ControlOrigin::PureRecorder { + return self.mode == 11 && self.result == ControlResult::RecorderOk; + } + let oversized = self.mode == 12; + let expected_status = if oversized { + JettRuntimeStatusV1::OK.code() + } else { + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + }; + let expected_reason = match self.mode { + 3 => RecordedBody::WrongOperation, + 6 | 9 => RecordedBody::InvalidHandle, + 10 => RecordedBody::WrongPurpose, + _ => RecordedBody::NoError, + }; + let expected = match self.mode { + 1 | 2 | 10 => [1, 1, 0, 0, 0], + 3 => [1, 0, 0, 0, 0], + 4..=6 => [0, 0, 1, 0, 0], + 7..=9 => [0, 0, 0, 1, 0], + 12 => [0; 5], + _ => return false, + }; + let Some(report) = &self.report else { + return false; + }; + let Some(observation) = report.observations.attempts.first() else { + return false; + }; + let Some(end) = observation.end else { + return false; + }; + let expected_history = if oversized { 3 } else { 2 }; + let expected_channel = if oversized { + report::FailureChannel::Clean + } else { + report::FailureChannel::Body + }; + let expected_flags = if oversized { 4 } else { 0 }; + let context_ok = if oversized { + self.oversize_probe.is_some_and(|probe| { + probe.before_length == 65_537 + && probe.after_length == probe.before_length + && probe.bytes_preserved + && probe.cleared + }) + } else { + self.oversize_probe.is_none() + }; + self.result == ControlResult::HostStatus(expected_status) + && self.recorded_body == Some(expected_reason) + && self.created + && self.destroy_attempts == 1 + && self.destroy_status == Some(0) + && report.resources_retired() + && report.ordinary_storage_empty() + && report.cleanup_succeeded() + && report.teardown_succeeded() + && report.events.is_empty() + && report.events_all_assigned() + && report.attempts[0].completion.cleanup_status == 0 + && report.attempts[0].completion.selected_channel == expected_channel + && report.observations.flags == expected_flags + && report.observations.counters.values == expected + && context_ok + && report.observations.attempts.len() == 1 + && observation.identity.id == self.attempt.unwrap_or(0) + && observation.identity.ordinal == 0 + && observation.begin.counters.values == [0; 5] + && end.counters.values == expected + && observation.begin.sequence == Some(1) + && end.sequence == Some(expected_history as u64) + && report.observations.history.len() == expected_history + && report + .observations + .history + .iter() + .enumerate() + .all(|(index, event)| { + event.sequence == index as u64 + 1 + && event.channel == report::Channel::Attempt(observation.identity) + && event.context.is_none() + && if index == 0 { + matches!(event.kind, report::HistoryKind::Begin) + } else if index + 1 == expected_history { + matches!(event.kind, report::HistoryKind::End) + } else { + oversized && matches!(event.kind, report::HistoryKind::Clear) + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn word(bytes: &mut Vec, value: u32) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + fn wide(bytes: &mut Vec, value: u64) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + fn set_word(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + fn set_wide(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + fn length(bytes: &mut [u8]) { + let actual = bytes.len() as u64; + set_wide(bytes, 16, actual); + } + fn counts(mode: u32) -> [u64; 5] { + match mode { + 1 | 2 | 10 => [1, 1, 0, 0, 0], + 3 => [1, 0, 0, 0, 0], + 4..=6 => [0, 0, 1, 0, 0], + 7..=9 => [0, 0, 0, 1, 0], + 12 => [0; 5], + _ => panic!("host mode"), + } + } + fn nested(mode: u32) -> Vec { + let oversized = mode == 12; + let status = u32::from(!oversized); + let history = if oversized { 3 } else { 2 }; + let mut bytes = b"JTRRP002".to_vec(); + word(&mut bytes, 2); + word(&mut bytes, 0); + wide(&mut bytes, 0); + word(&mut bytes, 1); + word(&mut bytes, 0); + wide(&mut bytes, 2); // genuine unconsumed provider operations + wide(&mut bytes, 41); + for value in [status, 0, status, 0] { + word(&mut bytes, value); + } + for _ in 0..8 { + wide(&mut bytes, 0); + } + word(&mut bytes, 1); + word(&mut bytes, 0); + word(&mut bytes, 0); + word(&mut bytes, 0); + wide(&mut bytes, 0); + for value in counts(mode) { + wide(&mut bytes, value); + } + word(&mut bytes, if oversized { 4 } else { 0 }); + word(&mut bytes, 1); + word(&mut bytes, history); + wide(&mut bytes, 0); + wide(&mut bytes, history as u64); + wide(&mut bytes, 41); + word(&mut bytes, 0); + word(&mut bytes, 1); + wide(&mut bytes, 1); + for _ in 0..5 { + wide(&mut bytes, 0); + } + word(&mut bytes, 1); + word(&mut bytes, 1); + wide(&mut bytes, history as u64); + for value in counts(mode) { + wide(&mut bytes, value); + } + for index in 0..history { + wide(&mut bytes, u64::from(index) + 1); + word( + &mut bytes, + if index == 0 { + 1 + } else if index + 1 == history { + 6 + } else { + 5 + }, + ); + word(&mut bytes, 1); + wide(&mut bytes, 41); + word(&mut bytes, 0); + word(&mut bytes, 0); + } + for value in [0, 1, 1, 0] { + word(&mut bytes, value); + } + length(&mut bytes); + report::decode(&bytes).unwrap(); + bytes + } + fn fixture(mode: u32) -> Vec { + let pure = mode == 11; + let mut bytes = b"JTRHC002".to_vec(); + word(&mut bytes, 2); + word(&mut bytes, 0); + wide(&mut bytes, 0); + word(&mut bytes, mode); + word(&mut bytes, if pure { 2 } else { 1 }); + word(&mut bytes, 0); + word(&mut bytes, 0); + if pure { + word(&mut bytes, 2); + } else { + word(&mut bytes, 1); + word(&mut bytes, u32::from(mode != 12)); + } + word( + &mut bytes, + match mode { + 11 => 0, + 3 => 4, + 6 | 9 => 3, + 10 => 2, + _ => 1, + }, + ); + word(&mut bytes, u32::from(mode == 12)); + if mode == 12 { + wide(&mut bytes, 65_537); + wide(&mut bytes, 65_537); + word(&mut bytes, 1); + word(&mut bytes, 1); + } + word(&mut bytes, u32::from(!pure && mode != 12)); + if !pure && mode != 12 { + word(&mut bytes, 0); + } + word(&mut bytes, u32::from(!pure)); + if !pure { + wide(&mut bytes, 41); + word(&mut bytes, if mode == 10 { 2 } else { 1 }); + } + word(&mut bytes, u32::from(!pure)); + word(&mut bytes, u32::from(!pure)); + word(&mut bytes, u32::from(!pure)); + if !pure { + word(&mut bytes, 0); + } + word(&mut bytes, u32::from(!pure)); + if !pure { + let report = nested(mode); + wide(&mut bytes, report.len() as u64); + bytes.extend_from_slice(&report); + } + length(&mut bytes); + assert!( + decode_host(&bytes).unwrap().passed(), + "uncorrupted host baseline mode {mode}" + ); + bytes + } + #[test] + fn host_control_all_twelve_producer_shapes_preserve_host_origin_and_unconsumed_script() { + for mode in 1..=12 { + let bytes = fixture(mode); + let control = decode_host(&bytes).unwrap(); + assert!(control.passed()); + assert_eq!(control.mode, mode); + assert!(matches!( + report::decode(&bytes), + Err(report::DecodeError::Magic) + )); + if mode == 11 { + assert_eq!(control.result, ControlResult::RecorderOk); + assert!(control.report.is_none()); + } else { + assert_eq!(control.report.as_ref().unwrap().script_remaining, 2); + } + } + } + #[test] + fn host_control_rejects_truncation_trailing_and_closed_tag_corruptions() { + let baseline = fixture(10); + for end in 0..baseline.len() { + let mut changed = baseline[..end].to_vec(); + if end >= 24 { + length(&mut changed); + } + assert!(decode_host(&changed).is_err(), "truncation {end}"); + } + let mut trailing = baseline.clone(); + trailing.push(0); + length(&mut trailing); + assert_eq!(decode_host(&trailing), Err(HostDecodeError::Trailing)); + for (offset, value) in [ + (8, 1), + (12, 1), + (24, 0), + (24, 13), + (28, 3), + (32, 2), + (40, 4), + (48, 8), + (52, 2), + (56, 2), + (64, 2), + (76, 3), + (80, 2), + (84, 2), + (88, 2), + (96, 2), + ] { + let mut changed = baseline.clone(); + set_word(&mut changed, offset, value); + assert!(decode_host(&changed).is_err(), "closed field {offset}"); + } + let mut wrong_length = baseline; + set_wide(&mut wrong_length, 16, u64::MAX); + assert!(decode_host(&wrong_length).is_err()); + } + #[test] + fn host_control_rejects_missing_duplicate_reordered_fields_and_origin_substitution() { + let baseline = fixture(6); + let mut missing = baseline.clone(); + missing.drain(44..48); + length(&mut missing); + assert!(decode_host(&missing).is_err()); + let mut duplicate = baseline.clone(); + duplicate.splice(44..44, 1u32.to_le_bytes()); + length(&mut duplicate); + assert!(decode_host(&duplicate).is_err()); + let mut reordered = baseline.clone(); + let reason = reordered[48..52].to_vec(); + let purpose = reordered[76..80].to_vec(); + reordered[48..52].copy_from_slice(&purpose); + reordered[76..80].copy_from_slice(&reason); + assert!(decode_host(&reordered).is_err()); + let mut pure_origin = baseline.clone(); + set_word(&mut pure_origin, 28, 2); + assert!(decode_host(&pure_origin).is_err()); + let mut pure_mode = baseline; + set_word(&mut pure_mode, 24, 11); + assert!(decode_host(&pure_mode).is_err()); + let mut wrong_pure = fixture(11); + set_word(&mut wrong_pure, 40, 1); + assert!(decode_host(&wrong_pure).is_err()); + } + #[test] + fn host_control_unknown_differs_from_observed_no_error_and_recorder_success() { + let baseline = fixture(1); + assert_eq!( + decode_host(&baseline).unwrap().recorded_body, + Some(RecordedBody::NoError) + ); + let mut unknown = baseline; + set_word(&mut unknown, 32, 1); + set_word(&mut unknown, 48, 0); + let control = decode_host(&unknown).unwrap(); + assert_eq!(control.recorded_body, None); + assert!(!control.passed()); + let mut no_report = fixture(1); + set_word(&mut no_report, 32, 1); + set_word(&mut no_report, 96, 0); + no_report.truncate(100); + length(&mut no_report); + let control = decode_host(&no_report).unwrap(); + assert!(control.report.is_none()); + assert!(!control.passed()); + let mut pure_unknown = fixture(11); + set_word(&mut pure_unknown, 32, 1); + set_word(&mut pure_unknown, 40, 0); + let control = decode_host(&pure_unknown).unwrap(); + assert_eq!(control.result, ControlResult::Unavailable); + assert!(!control.passed()); + } + #[test] + fn host_control_distinguishes_wrong_purpose_from_wrong_frame_and_checks_teardown_probe_flags() { + let mut wrong_frame = fixture(10); + set_word(&mut wrong_frame, 48, 5); + let control = decode_host(&wrong_frame).unwrap(); + assert_eq!(control.recorded_body, Some(RecordedBody::WrongFrame)); + assert!(!control.passed()); + let mut failed_destroy = fixture(1); + set_word(&mut failed_destroy, 32, 1); + set_word(&mut failed_destroy, 92, 9); + assert!( + decode_host(&failed_destroy).is_err(), + "outer/inner teardown must agree" + ); + let baseline = fixture(12); + for (offset, value) in [(56, 65_536), (64, 65_536), (72, 0), (76, 0)] { + let mut changed = baseline.clone(); + set_word(&mut changed, offset, value); + assert!( + !decode_host(&changed).unwrap().passed(), + "oversize probe {offset}" + ); + } + for flags in [0, 1, 2, 8, 16, 32, 63] { + let mut changed = baseline.clone(); + set_word(&mut changed, 128 + 152 + 40, flags); + assert!( + !decode_host(&changed).unwrap().passed(), + "only BYTE_LIMIT is expected for oversize" + ); + } + let mut ordinary_flagged = fixture(1); + set_word(&mut ordinary_flagged, 108 + 152 + 40, 4); + assert!(!decode_host(&ordinary_flagged).unwrap().passed()); + } +} + +const OBSERVATION_EVIDENCE_ENV: &str = "JETT_RESOURCE_OBSERVATION_EVIDENCE_V2"; +const HOST_CONTROL_ENV: &str = "JETT_RESOURCE_NATIVE_TEST_CONTROL_V2"; +const HOST_CONTROL_FILE: &str = "resource-native-host-abi-control-v2.bin"; +const SOURCE_REPORT_FILE: &str = "resource-native-report-v1.bin"; + +struct ObservationProgram { + directory: tempfile::TempDir, + binary: std::path::PathBuf, + binary_sha256: String, + object_sha256: String, + source_sha256: String, +} +fn observation_evidence(name: &str, files: &[(&str, &[u8])], metadata: serde_json::Value) { + let Some(root) = std::env::var_os(OBSERVATION_EVIDENCE_ENV) else { + return; + }; + let root = std::path::PathBuf::from(root); + std::fs::create_dir_all(&root).unwrap(); + let directory = root.join(name); + std::fs::create_dir(&directory) + .expect("fresh observation evidence segment; preserve previous evidence"); + for (name, bytes) in files { + std::fs::write(directory.join(name), bytes).unwrap(); + } + std::fs::write( + directory.join("observations.json"), + serde_json::to_vec_pretty(&metadata).unwrap(), + ) + .unwrap(); +} +fn observation_program( + case: &super::cases::Case, + release: bool, + archive: &super::Archive, +) -> ObservationProgram { + let directory = tempfile::tempdir().unwrap(); + let project = directory.path().join("project"); + let stdlib = directory.path().join("synthetic-stdlib"); + std::fs::create_dir(&project).unwrap(); + std::fs::create_dir(&stdlib).unwrap(); + let source = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + std::fs::write(&source, case.source).unwrap(); + std::fs::write(&support, super::cases::SUPPORT).unwrap(); + let artifact = super::object(&source, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|symbol| symbol == "jett_aot_resource_v1_manifest") + ); + let object = directory.path().join("resource.obj"); + let binary = directory.path().join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + let object_sha256 = super::digest(&artifact.bytes); + std::fs::write(&object, &artifact.bytes).unwrap(); + super::link(archive, &object, &binary, directory.path()); + let binary_bytes = std::fs::read(&binary).unwrap(); + let binary_sha256 = super::digest(&binary_bytes); + let source_sha256 = super::digest(case.source.as_bytes()); + observation_evidence( + &format!("{}-{}-link", archive.profile, case.name), + &[ + ("resource.obj", &artifact.bytes), + ( + if cfg!(windows) { + "resource.exe" + } else { + "resource" + }, + &binary_bytes, + ), + ], + serde_json::json!({ + "kind":"linked-program", "source_sha256":source_sha256, "support_sha256":super::digest(super::cases::SUPPORT.as_bytes()), + "object_sha256":object_sha256, "binary_sha256":binary_sha256, "archive_sha256":archive.archive_sha256, + "target":archive.target, "profile":archive.profile, "source_optimization":release, + "linker_path":archive.linker, "linker_sha256":archive.linker_sha256, + "ordered_native_library_args":archive.libraries, "sdk_LIB":archive.sdk_lib, + "runtime_abi":1, "private_report_protocol":2, + }), + ); + std::fs::remove_dir_all(&project).unwrap(); + std::fs::remove_dir_all(&stdlib).unwrap(); + std::fs::remove_file(&object).unwrap(); + assert!( + !source.exists() + && !support.exists() + && !project.exists() + && !stdlib.exists() + && !object.exists() + ); + ObservationProgram { + directory, + binary, + binary_sha256, + object_sha256, + source_sha256, + } +} +fn observation_run( + program: &ObservationProgram, + archive: &super::Archive, + name: &str, + encoded_script: &str, + mode: Option<&str>, +) -> (std::process::Output, Vec) { + assert!(!program.directory.path().join("project").exists()); + assert!(!program.directory.path().join("synthetic-stdlib").exists()); + assert!(!program.directory.path().join("resource.obj").exists()); + assert_eq!( + super::digest(&std::fs::read(&program.binary).unwrap()), + program.binary_sha256 + ); + let directory = tempfile::tempdir().unwrap(); + let mut environment = vec![("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", encoded_script)]; + if let Some(mode) = mode { + environment.push((HOST_CONTROL_ENV, mode)); + } + let actual = super::super::run_bounded_with_environment( + &program.binary, + directory.path(), + &environment, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", encoded_script)), + ); + let file = if mode.is_some() { + HOST_CONTROL_FILE + } else { + SOURCE_REPORT_FILE + }; + let other = if mode.is_some() { + SOURCE_REPORT_FILE + } else { + HOST_CONTROL_FILE + }; + assert!( + !directory.path().join(other).exists(), + "separate report origins" + ); + let report_path = directory.path().join(file); + assert!(std::fs::metadata(&report_path).unwrap().len() <= 16 * 1024 * 1024 + 256); + let bytes = std::fs::read(&report_path).unwrap(); + observation_evidence( + &format!("{}-{name}", archive.profile), + &[ + (file, &bytes), + ("stdout.bin", &actual.stdout), + ("stderr.bin", &actual.stderr), + ], + serde_json::json!({ + "kind": if mode.is_some() { "private-host-control" } else { "compiled-original-source" }, + "mode":mode, "source_sha256":program.source_sha256, "object_sha256":program.object_sha256, + "binary_sha256":program.binary_sha256, "archive_sha256":archive.archive_sha256, + "profile":archive.profile, "exit":actual.status.code(), "script_hex":encoded_script, + "report_sha256":super::digest(&bytes), "stdout_sha256":super::digest(&actual.stdout), + "stderr_sha256":super::digest(&actual.stderr), "source_and_object_deleted_before_execution":true, + }), + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{name}: {actual:?}" + ); + (actual, bytes) +} +fn assert_acquisition_boundaries(observed: &super::report::Report, expected: &[[u64; 5]]) { + let snapshot = &observed.observations; + assert_eq!(snapshot.flags, 0); + assert_eq!(snapshot.attempts.len(), expected.len()); + assert_eq!(observed.attempts.len(), expected.len()); + assert_eq!( + snapshot.history.len(), + expected.len() * 2, + "Runtime entries have no Property-context call history" + ); + let mut previous = [0; 5]; + for (index, (attempt, expected)) in snapshot.attempts.iter().zip(expected).enumerate() { + assert_eq!( + attempt.identity.id, + observed.attempts[index].completion.attempt + ); + assert_eq!(attempt.identity.ordinal, index as u32); + assert_eq!(attempt.begin.counters.values, previous); + assert_eq!(attempt.end.as_ref().unwrap().counters.values, *expected); + let begin = &snapshot.history[index * 2]; + let end = &snapshot.history[index * 2 + 1]; + assert_eq!( + begin.channel, + super::report::Channel::Attempt(attempt.identity) + ); + assert_eq!(end.channel, begin.channel); + assert_eq!(begin.context, None); + assert_eq!(end.context, None); + assert!(matches!(begin.kind, super::report::HistoryKind::Begin)); + assert!(matches!(end.kind, super::report::HistoryKind::End)); + assert_eq!(attempt.begin.sequence, Some(begin.sequence)); + assert_eq!(attempt.end.as_ref().unwrap().sequence, Some(end.sequence)); + previous = *expected; + } + assert_eq!(snapshot.counters.values, previous); + assert_eq!(snapshot.context_bytes, 0); + assert_eq!(snapshot.final_sequence, (expected.len() * 2) as u64); +} +#[test] +#[ignore = "requires fresh pinned measured protocol2 private Debug/Release archives"] +fn native_resource_observation_v2_measures_acquisition_host_ingress_and_reentry() { + for release in [false, true] { + let archive = super::archive(release); + let connected = super::cases::CASES + .iter() + .find(|case| case.name == "connected") + .unwrap(); + let factory_fail = super::cases::CASES + .iter() + .find(|case| case.name == "factory_fail") + .unwrap(); + assert_eq!(connected.source, factory_fail.source); + let program = observation_program(connected, release, &archive); + for (case, counters) in [ + (connected, [3, 1, 1, 1, 1]), + (factory_fail, [1, 1, 1, 1, 1]), + ] { + let encoded = super::script(case); + let (actual, bytes) = observation_run(&program, &archive, case.name, &encoded, None); + let observed = super::report::decode(&bytes).unwrap(); + assert_eq!(actual.status.code(), Some(case.exit), "{observed:?}"); + super::assert_report(case, &observed); + assert_acquisition_boundaries(&observed, &[counters]); + } + let encoded = super::script_operations(&[super::cases::Script::Construct(2)], 1); + let (actual, bytes) = observation_run( + &program, + &archive, + "provider-label-mismatch", + &encoded, + None, + ); + let observed = super::report::decode(&bytes).unwrap(); + assert_eq!(actual.status.code(), Some(71)); + assert_eq!(observed.attempts.len(), 1); + let attempt = &observed.attempts[0]; + assert_eq!(attempt.completion.body_status, 1); + assert_eq!(attempt.completion.cleanup_status, 0); + assert_eq!( + attempt.completion.selected_channel, + super::report::FailureChannel::Body + ); + assert!(attempt.ordinary_message.is_empty()); + assert_eq!( + attempt.resource_message, + b"native Resource protocol refused" + ); + assert!(observed.events.is_empty()); + assert_eq!( + observed.script_remaining, 1, + "rejected dispatch does not consume provider script" + ); + assert!( + observed.resources_retired() + && observed.ordinary_storage_empty() + && observed.teardown_succeeded() + ); + assert!( + !observed.observations_ready_for_acceptance() && !observed.all_attempts_succeeded() + ); + assert_acquisition_boundaries(&observed, &[[1, 1, 1, 1, 1]]); + run_closed_host_controls(&program, &archive, connected); + drop(program); + let implicit = super::cases::CASES + .iter() + .find(|case| case.name == "implicit_drop") + .unwrap(); + let program = observation_program(implicit, release, &archive); + let encoded = super::script_operations( + &[ + super::cases::Script::Construct(1), + super::cases::Script::Construct(1), + ], + 2, + ); + let (actual, bytes) = + observation_run(&program, &archive, "two-entry-reentry", &encoded, None); + let observed = super::report::decode(&bytes).unwrap(); + assert_eq!(actual.status.code(), Some(0)); + assert_eq!(observed.attempts.len(), 2); + assert!(observed.observations_ready_for_acceptance() && observed.all_attempts_succeeded()); + assert_ne!( + observed.attempts[0].completion.attempt, + observed.attempts[1].completion.attempt + ); + assert!( + observed + .attempts + .iter() + .all(|attempt| attempt.ordinary_message.is_empty() + && attempt.resource_message.is_empty()) + ); + assert_eq!(observed.attempts[0].event_sequence_end, 2); + assert_eq!(observed.attempts[1].event_sequence_end, 4); + let expected = [ + super::cases::Event::Constructed(1), + super::cases::Event::Finalized(1), + super::cases::Event::Constructed(1), + super::cases::Event::Finalized(1), + ]; + assert_eq!(observed.events.len(), expected.len()); + for (index, (actual, expected)) in observed.events.iter().zip(expected).enumerate() { + let (label, kind) = super::event(expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind) + ); + } + assert_acquisition_boundaries(&observed, &[[1; 5], [2; 5]]); + } +} +fn run_closed_host_controls( + program: &ObservationProgram, + archive: &super::Archive, + connected: &super::cases::Case, +) { + const MODES: [&str; 12] = [ + "hook-null-output", + "hook-context-overlap-output", + "hook-unknown-operation", + "factory-prepare-null-output", + "factory-prepare-context-overlap-output", + "factory-prepare-zero-call", + "factory-commit-null-output", + "factory-commit-context-overlap-output", + "factory-commit-zero-prepared", + "property-acquire-hook", + "recorder-only", + "oversized-property-context", + ]; + let encoded = super::script(connected); + for (index, mode) in MODES.iter().enumerate() { + let (actual, bytes) = observation_run(program, archive, mode, &encoded, Some(mode)); + assert!( + super::report::decode(&bytes).is_err(), + "host envelope never certifies a Source report" + ); + let observed = decode_host(&bytes).unwrap(); + assert_eq!(observed.mode, index as u32 + 1); + assert_eq!(actual.status.code(), Some(0), "{observed:?}"); + assert!(observed.passed(), "{observed:?}"); + if let Some(report) = &observed.report { + assert_eq!(report.script_remaining, connected.script.len() as u64); + assert!(report.events.is_empty()); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/source17_suites.rs b/crates/jett_driver/tests/native_conformance/resource_execution/source17_suites.rs new file mode 100644 index 00000000..abbb1287 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/source17_suites.rs @@ -0,0 +1,1115 @@ +//! Whole unchanged Resource canonical suite fixtures; available observations only. +//! Deliberately does not award full acceptance without missing runtime telemetry. +use super::*; +use jett_comptime::verify::collect_checked_property_cases_with_checked_values; +use jett_hir::{ExpressionKind, StatementKind}; +use serde_json::{Value as Json, json}; + +const SOURCE17: &str = include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett" +); +const SUPPORT: &str = include_str!("../resource/resource_probe.jett"); +const SOURCE17_SHA256: &str = "c8e74848356a5b3d09ab6659318b1e1292718d34443f7f37222bafd6f3eed0ff"; +const NORMAL_RECEIPT_ENV: &str = "JETT_SOURCE17_NORMAL_ARCHIVE_RECEIPT_V1"; +const NORMAL_RECEIPT_SHA256: &str = + "be980ae18006737304ae69af4d254f78cad15c51fe8e6226c6a2b9ca9cf4b6b4"; +const EVIDENCE_ENV: &str = "JETT_SOURCE17_SUITE_EVIDENCE_V1"; +const UNMEASURED: &[&str] = &[ + "individual ordinary storage-kind live counts beyond aggregate ordinary_empty", + "individual private artifact-retainer counts beyond checked-program and caller input-Arc release", + "normal-launcher custody counts and same-context reentry", +]; + +const PURE_REPLACEMENTS: &str = include_str!( + "../../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/pure_replacement_suites.jett" +); +const PURE_REPLACEMENTS_SHA256: &str = + "0801c8b24add5deb9ea7071f3fc2ca5a4573dbe00a62df38d1a474fadc3774ab"; + +#[derive(Clone, Copy)] +enum Fixture { + Source17, + PureReplacements, +} +impl Fixture { + fn source(self) -> &'static str { + match self { + Self::Source17 => SOURCE17, + Self::PureReplacements => PURE_REPLACEMENTS, + } + } + fn sha256(self) -> &'static str { + match self { + Self::Source17 => SOURCE17_SHA256, + Self::PureReplacements => PURE_REPLACEMENTS_SHA256, + } + } + fn file_name(self) -> &'static str { + match self { + Self::Source17 => "source17.jett", + Self::PureReplacements => "pure_replacement_suites.jett", + } + } + fn evidence_env(self) -> &'static str { + match self { + Self::Source17 => EVIDENCE_ENV, + Self::PureReplacements => "JETT_PURE_REPLACEMENT_SUITE_EVIDENCE_V1", + } + } + fn evidence_schema(self) -> &'static str { + match self { + Self::Source17 => "jett-source17-suite-available-observations-v1", + Self::PureReplacements => "jett-pure-replacement-suite-available-observations-v1", + } + } + fn property_name(self) -> &'static str { + match self { + Self::Source17 => "absent_aggregate_property", + Self::PureReplacements => "pure_replacements_trials", + } + } + fn given_name(self) -> &'static str { + match self { + Self::Source17 => "marker", + Self::PureReplacements => "input", + } + } + fn main_count(self) -> usize { + match self { + Self::Source17 => 0, + Self::PureReplacements => 1, + } + } + fn retained_functions(self) -> &'static [&'static str] { + match self { + Self::Source17 => &[ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + "refined_shape_contract", + "absent_required_controls", + ], + Self::PureReplacements => { + &["optional_marker", "string_marker", "integer_marker", "main"] + } + } + } +} + +#[derive(Clone, Copy)] +enum Purpose { + Verify, + Property, +} +impl Purpose { + fn name(self) -> &'static str { + match self { + Self::Verify => "verify", + Self::Property => "property", + } + } +} +struct Prepared { + purpose: Purpose, + release: bool, + object: jett_codegen_cranelift::ObjectArtifact, + entry: jett_hir::FunctionId, + expected: Json, +} + +fn checked_suites( + fixture: Fixture, + project: &Path, + support: &Path, + release: bool, +) -> Arc { + let source = fs::read(project).unwrap(); + let support_source = fs::read_to_string(support).unwrap(); + assert_eq!( + source, + fixture.source().as_bytes(), + "whole unchanged original Source without a main wrapper" + ); + assert_eq!(digest(&source), fixture.sha256()); + assert_eq!(support_source, SUPPORT); + let primary = FileId::new(0); + let support_file = FileId::new(10_000); + let mut parsed = parse(&support_source, support_file); + let source = parse(fixture.source(), primary); + assert!(parsed.errors.is_empty() && source.errors.is_empty()); + assert_eq!( + source + .module + .items + .iter() + .filter(|item| { + matches!(item, Item::Function(function) if function.name.name == "main") + }) + .count(), + fixture.main_count(), + "retain the original Source main without using it as a suite wrapper" + ); + let resources = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [resource] = resources.as_slice() else { + panic!("unique original trusted Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *resource, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(source.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (primary, SourceOrigin::Project), + (support_file, SourceOrigin::Stdlib), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap(), + ) +} + +fn expected_contexts( + fixture: Fixture, + program: &jett_hir::Program, + plan: &jett_hir::CheckedNativePropertyPlan, +) -> Json { + let contexts = program.functions[plan.entry().index() as usize] + .body + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::Expression(expression) => match &expression.kind { + ExpressionKind::PropertyCaseContext(context) => Some(context.clone()), + _ => None, + }, + _ => None, + }) + .collect::>(); + assert_eq!(contexts.len(), plan.trials().len() * 2); + let mut expected = Vec::new(); + for (ordinal, (trial, pair)) in plan + .trials() + .iter() + .zip(contexts.chunks_exact(2)) + .enumerate() + { + let one_based = u64::try_from(trial.case().iteration).unwrap() + 1; + assert_eq!(trial.name(), fixture.property_name()); + assert_eq!(trial.case().iteration, ordinal); + assert_eq!(trial.given_names(), &[fixture.given_name().to_owned()]); + assert_eq!( + pair[0], + Some(jett_hir::NativePropertyCase { + name: trial.name().into(), + trial: one_based, + }) + ); + assert_eq!(pair[1], None); + expected.push(json!({ + "ordinal_zero_based": ordinal, + "function": trial.function().index(), + "property_span": format!("{:?}", trial.case().property_span), + "name_span": format!("{:?}", trial.span()), + "given_names": trial.given_names(), + "checked_arguments": format!("{:?}", trial.case().arguments), + "checked_argument_int64_bits": trial.case().arguments.iter().map(|value| match value { + jett_comptime::Value::Int64(value) => *value as u64, + _ => panic!("checked given value must retain its int64 domain"), + }).collect::>(), + "context_set_bytes_utf8": format!("property '{}' trial {}: ", trial.name(), one_based), + "context_clear_after_call": true, + })); + } + json!({ + "expected_only_not_runtime_observation": true, + "trial_count": plan.trials().len(), + "trials_in_checked_order": expected, + }) +} + +fn prepare(fixture: Fixture, project: &Path, support: &Path, release: bool) -> Vec { + let checked = checked_suites(fixture, project, support, release); + let checked_weak = Arc::downgrade(&checked); + let evaluation = jett_driver::evaluate_checked_resource_required_values(&checked); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + let values = Arc::new(evaluation.values); + let values_weak = Arc::downgrade(&values); + values.checked_required_values(&checked).unwrap(); + assert!(!values.is_empty()); + match fixture { + Fixture::Source17 => assert!( + values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + ), + Fixture::PureReplacements => { + // Same original-span required-value oracle as the existing object test. + assert_eq!(values.len(), 6); + let mut constants = 0; + for item in &checked.module().items { + let Item::VarDecl(declaration) = item else { + continue; + }; + if declaration.name.span.file != FileId::new(0) { + continue; + } + let expected = match declaration.name.name.as_str() { + "baked_optional" => jett_comptime::Value::Int64(7), + "baked_string" => jett_comptime::Value::String("after".into()), + "baked_integer" => jett_comptime::Value::Int64(23), + name => panic!("unexpected original Source constant: {name}"), + }; + let jett_parser::ast::Expr::Comptime(_, span) = &declaration.value else { + panic!("original required initializer"); + }; + assert_eq!( + values.get(*span, &jett_comptime::ComptimeContext::default()), + Some(&expected) + ); + constants += 1; + } + assert_eq!(constants, 3); + } + } + let mut hir = jett_hir::lower_checked_resource_program_with_test_bodies(&checked).unwrap(); + jett_driver::bake_checked_resource_values(&mut hir, &checked, &values).unwrap(); + for &name in fixture.retained_functions() { + assert!( + hir.functions.iter().any(|function| { + function.identity.declaration.origin == SourceOrigin::Project + && function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }), + "whole unused/exported Source declaration retained: {name}" + ); + } + let manifest = hir.resource_manifest.clone(); + let originals = hir + .functions + .iter() + .map(|function| (function.id, function.identity.clone(), function.span)) + .collect::>(); + let verify = jett_hir::append_checked_native_verify_suite(&mut hir, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + let cases = collect_checked_property_cases_with_checked_values(checked.clone(), values.clone()) + .unwrap(); + let plan = + jett_hir::append_checked_native_property_suite(&mut hir, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + assert!(values.same_checked_cache(cases.required_values())); + assert_eq!(plan.trials().len(), cases.cases().len()); + // Each unchanged fixture uses the checked default; never truncate a plan to 100. + assert_eq!( + plan.trials().len(), + jett_comptime::verify::PROPERTY_DEFAULT_ITERATIONS + ); + for (trial, original) in plan.trials().iter().zip(cases.cases()) { + assert_eq!(trial.case().property_span, original.property_span); + assert_eq!(trial.case().iteration, original.iteration); + assert_eq!(trial.case().arguments.len(), original.arguments.len()); + assert!( + trial + .case() + .arguments + .iter() + .zip(&original.arguments) + .all(|(left, right)| left.same_checked_snapshot(right)) + ); + } + let property_expected = expected_contexts(fixture, &hir, &plan); + let verify_targets = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Verify(verify) if verify.span.file == FileId::new(0) => Some(json!({ + "name": verify.name.name, + "span": format!("{:?}", verify.span), + })), + _ => None, + }) + .collect::>(); + assert_eq!(verify_targets.len(), 1); + let verify_expected = json!({ + "expected_only_not_runtime_observation": true, + "original_targets_in_checked_order": verify_targets, + }); + jett_hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + jett_hir::validate(&hir).unwrap(); + hir.resource_source + .validate_current(&hir, &checked.checked().interner) + .unwrap(); + assert_eq!(hir.resource_manifest, manifest); + drop(manifest); + assert_eq!(expected_contexts(fixture, &hir, &plan), property_expected); + for (id, identity, span) in originals { + let function = &hir.functions[id.index() as usize]; + assert_eq!(function.id, id); + assert_eq!(function.identity, identity); + assert_eq!(function.span, span); + } + let mir = jett_mir::lower(&hir, &checked.checked().interner).unwrap(); + jett_mir::validate(&mir).unwrap(); + let output = [ + (Purpose::Verify, verify, verify_expected), + (Purpose::Property, plan.entry(), property_expected), + ] + .into_iter() + .map(|(purpose, entry, expected)| { + let object = jett_codegen_cranelift::emit_host_program_object_with_options( + &mir, + &checked.checked().interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize: release }, + ) + .unwrap(); + assert!(!object.bytes.is_empty()); + assert_eq!(object.target, jett_driver::native::host_target()); + assert!( + object + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + assert!( + object + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + Prepared { + purpose, + release, + object, + entry, + expected, + } + }) + .collect::>(); + // Real lifetime observation for these roots, without exposing opaque plan internals. + // It does not enumerate all private artifact strong counts. + drop(mir); + drop(hir); + drop(plan); + drop(cases); + drop(values); + drop(checked); + assert!( + checked_weak.upgrade().is_none(), + "no retained checked-root cycle" + ); + assert!( + values_weak.upgrade().is_none(), + "required-value input Arc released" + ); + output +} + +fn normal_archive(release: bool, private: &Archive) -> Archive { + let path = PathBuf::from( + std::env::var_os(NORMAL_RECEIPT_ENV) + .expect("Root must supply the exact measured normal receipt; no runtime builds"), + ); + assert!(path.is_absolute()); + let bytes = fs::read(path).unwrap(); + assert!(bytes.len() <= 1024 * 1024); + assert_eq!(digest(&bytes), NORMAL_RECEIPT_SHA256); + let receipt: Json = serde_json::from_slice(&bytes).unwrap(); + let profile = if release { "release" } else { "debug" }; + let rows = receipt["archives"] + .as_array() + .unwrap() + .iter() + .filter(|row| field(row, "profile") == profile && field(row, "kind") == "launcher") + .collect::>(); + let [row] = rows.as_slice() else { + panic!("unique measured normal launcher"); + }; + assert_eq!(row["private_exports"].as_u64(), Some(0)); + assert_eq!(row["selected_package_private_cfgs"], json!([])); + assert_eq!(row["main_definitions"].as_u64(), Some(1)); + assert_eq!(field(row, "target"), private.target); + assert_eq!(field(row, "crt_mode"), "static"); + let measured = measured_file(&row["archive"], "path", "sha256"); + let manifest = measured_file(&row["manifest"], "path", "sha256"); + let supplied = super::super::measured_launcher::supplied(release) + .expect("both normal Debug/Release manifests must already be supplied"); + let selected = + jett_driver::native::NativeLauncherBundle::from_manifest(&manifest, release).unwrap(); + assert_eq!( + supplied, &selected, + "supplied manifest must select the pinned measured bundle" + ); + assert_eq!( + fs::canonicalize(&selected.archive_path).unwrap(), + fs::canonicalize(&measured).unwrap() + ); + assert_eq!(selected.runtime_abi_version, 1); + assert_eq!( + selected.crt_mode, + jett_driver::native::NativeCrtMode::Static + ); + let arguments = selected + .native_library_args + .iter() + .map(|arg| arg.to_str().unwrap()) + .collect::>(); + assert_eq!( + json!(arguments), + row["selected_manifest_native_library_args"] + ); + let build_environment = &row["build"]["environment_overrides"]; + assert_eq!( + fs::canonicalize(field( + build_environment, + "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER" + )) + .unwrap(), + fs::canonicalize(&private.linker).unwrap(), + ); + assert_eq!( + Some(OsString::from(field(build_environment, "LIB"))), + private.sdk_lib, + "same measured native linker environment", + ); + Archive { + path: measured, + archive_sha256: field(&row["archive"], "sha256").into(), + target: selected.target, + profile: profile.into(), + linker: private.linker.clone(), + linker_sha256: private.linker_sha256.clone(), + libraries: selected.native_library_args, + sdk_lib: private.sdk_lib.clone(), + } +} + +fn empty_two_entry_script() -> String { + let mut bytes = b"JTRST001".to_vec(); + bytes.extend(1u32.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + bytes.extend(32u64.to_le_bytes()); + bytes.extend(2u32.to_le_bytes()); + bytes.extend(0u32.to_le_bytes()); + assert_eq!(bytes.len(), 32); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn acquisition_counters(counts: &report::Counters) -> Json { + json!({ + "hook_prepare_ingress": counts.values[0], + "acquire_hook_prepare_ingress": counts.values[1], + "factory_prepare_ingress": counts.values[2], + "factory_commit_ingress": counts.values[3], + "provider_construct_dispatch": counts.values[4], + }) +} + +fn observe_context_history(observed: &report::Report, purpose: Purpose, expected: &Json) -> Json { + let telemetry = &observed.observations; + assert_eq!( + telemetry.flags, 0, + "incomplete private telemetry is never acceptance" + ); + assert_eq!( + telemetry.counters.values, [0; 5], + "measured dispatches, not provider-event inference" + ); + assert_eq!(telemetry.attempts.len(), observed.attempts.len()); + assert!( + telemetry + .history + .iter() + .all(|event| matches!(event.channel, report::Channel::Attempt(_))), + "these suites have no legacy/session context mutations" + ); + let mut attempts = Vec::new(); + for (ordinal, (boundary, attempt)) in telemetry + .attempts + .iter() + .zip(&observed.attempts) + .enumerate() + { + assert_eq!(boundary.identity.id, attempt.completion.attempt); + assert_eq!(boundary.identity.ordinal as usize, ordinal); + let end = boundary.end.as_ref().expect("known genuine sealed attempt"); + assert_eq!(boundary.begin.counters.values, [0; 5]); + assert_eq!(end.counters.values, [0; 5]); + let events = telemetry + .history + .iter() + .filter(|event| event.channel == report::Channel::Attempt(boundary.identity)) + .collect::>(); + assert_eq!( + events.first().map(|event| event.sequence), + boundary.begin.sequence + ); + assert_eq!(events.last().map(|event| event.sequence), end.sequence); + assert!(matches!( + events.first().unwrap().kind, + report::HistoryKind::Begin + )); + assert!(matches!( + events.last().unwrap().kind, + report::HistoryKind::End + )); + assert_eq!(events.first().unwrap().context, None); + assert_eq!(events.last().unwrap().context, None); + let mut trials = Vec::new(); + match purpose { + Purpose::Verify => assert_eq!( + events.len(), + 2, + "Verify must have no Property mutations/calls" + ), + Purpose::Property => { + let expected_trials = expected["trials_in_checked_order"].as_array().unwrap(); + assert_eq!( + expected_trials.len() as u64, + expected["trial_count"].as_u64().unwrap() + ); + let mut cursor = 1; + for trial in expected_trials { + let bytes = trial["context_set_bytes_utf8"].as_str().unwrap().as_bytes(); + let target = u32::try_from(trial["function"].as_u64().unwrap()).unwrap(); + let arguments = trial["checked_argument_int64_bits"].as_array().unwrap(); + let set = events + .get(cursor) + .expect("every checked trial has a runtime Set"); + assert!(matches!(set.kind, report::HistoryKind::Set)); + assert_eq!(set.context.as_deref(), Some(bytes)); + cursor += 1; + let entered = events.get(cursor).expect("real target prologue after Set"); + assert_eq!(entered.context.as_deref(), Some(bytes)); + let report::HistoryKind::CallEnter { call, parameters } = &entered.kind else { + panic!("expected actual target entry") + }; + assert_eq!(call.function, target); + assert_eq!(parameters.len(), arguments.len()); + for (ordinal, (parameter, expected_bits)) in + parameters.iter().zip(arguments).enumerate() + { + assert_eq!(parameter.parameter as usize, ordinal); + assert_eq!( + parameter.value, + report::ParameterValue::Int64 { + bits: expected_bits.as_u64().unwrap() + }, + "actual installed marker bits must match the checked generated trial" + ); + } + let target_call = *call; + let entered_sequence = entered.sequence; + cursor += 1; + let mut complete = None; + let mut nested = 0; + loop { + let event = events + .get(cursor) + .expect("target completion and Clear must be present"); + if matches!(event.kind, report::HistoryKind::Clear) { + break; + } + assert_eq!(event.context.as_deref(), Some(bytes)); + assert!( + complete.is_none(), + "no additional call occurs after target completion before Clear" + ); + match &event.kind { + report::HistoryKind::CallEnter { call, .. } => { + assert_ne!( + call.function, target, + "extra Property target occurrence" + ); + nested += 1; + } + report::HistoryKind::CallComplete { + call, + entered_sequence: actual_entry, + body_status, + failure_status, + } => { + assert_eq!(*body_status, 0); + assert_eq!(*failure_status, None); + if *call == target_call { + assert_eq!(*actual_entry, Some(entered_sequence)); + complete = Some(event.sequence); + } + } + _ => panic!("unexpected mutation/boundary within checked trial"), + } + cursor += 1; + } + let completed = complete.expect("genuine successful target completion"); + let cleared = events[cursor]; + assert_eq!(cleared.context, None); + trials.push(json!({ + "set_sequence": set.sequence, "entered_sequence": entered_sequence, + "completed_sequence": completed, "clear_sequence": cleared.sequence, + "function": target_call.function, "signature": target_call.signature, + "template": target_call.template, "operation": target_call.operation, + "call": target_call.call, "scope": target_call.scope, + "actual_installed_int64_bits": parameters.iter().map(|parameter| + match parameter.value { report::ParameterValue::Int64 { bits } => bits, + report::ParameterValue::Unobserved => panic!("checked given value is unobserved") }).collect::>(), + "actual_parameter_shapes": parameters.iter().map(|parameter| parameter.shape).collect::>(), + "exact_context_bytes": entered.context, + "nested_resource_calls_retained": nested, + })); + cursor += 1; + } + assert_eq!( + cursor + 1, + events.len(), + "no missing, duplicated or extra trials" + ); + } + } + attempts.push(json!({ + "attempt": boundary.identity.id, "ordinal": boundary.identity.ordinal, + "begin_sequence": boundary.begin.sequence, "end_sequence": end.sequence, + "begin_counters": acquisition_counters(&boundary.begin.counters), + "end_counters": acquisition_counters(&end.counters), + "trials": trials, + })); + } + json!({ + "scope": "Resource ABI validated prologue and actual first completion publication", + "ordinary_abi_calls_measured": false, + "non_int64_parameter_values_measured": false, + "final_counters": acquisition_counters(&telemetry.counters), + "incomplete_flags": telemetry.flags, + "history_records": telemetry.history.len(), + "history_context_bytes": telemetry.context_bytes, + "history_final_sequence": telemetry.final_sequence, + "attempts": attempts, + "raw_history": telemetry.history.iter().map(|event| json!({ + "sequence": event.sequence, "channel": format!("{:?}", event.channel), + "exact_context_bytes": event.context, "event": format!("{:?}", event.kind), + })).collect::>(), + }) +} + +fn observe_private(observed: &report::Report, purpose: Purpose, expected: &Json) -> Json { + assert_eq!( + observed.attempts.len(), + 2, + "same context/grant/provider, two entry calls" + ); + let context_history = observe_context_history(observed, purpose, expected); + let mut previous = 0; + let attempts = observed.attempts.iter().map(|attempt| { + assert!(attempt.completion.attempt > previous); + previous = attempt.completion.attempt; + assert_eq!(attempt.completion.body_status, 0); + assert_eq!(attempt.completion.cleanup_status, 0); + assert_eq!(attempt.completion.selected_channel, report::FailureChannel::Clean); + assert!(attempt.ordinary_message.is_empty() && attempt.resource_message.is_empty()); + assert!(attempt.counts.resources_retired() && attempt.counts.ordinary_empty); + assert_eq!(attempt.event_sequence_end, 0); + json!({ + "attempt": attempt.completion.attempt, + "body_status": attempt.completion.body_status, + "cleanup_status": attempt.completion.cleanup_status, + "channel": format!("{:?}", attempt.completion.selected_channel), + "resource_owners": attempt.counts.owners, "resource_loans": attempt.counts.loans, + "resource_frames": attempt.counts.frames, "registry_entries": attempt.counts.registry_entries, + "owner_handles": attempt.counts.owner_handles, "loan_handles": attempt.counts.loan_handles, + "frame_handles": attempt.counts.frame_handles, "provisional_returns": attempt.counts.provisional_returns, + "ordinary_storage_empty": attempt.counts.ordinary_empty, + "observed_before_context_destruction": true, "event_sequence_end": attempt.event_sequence_end, + }) + }).collect::>(); + assert!(observed.events.is_empty()); + assert_eq!(observed.script_remaining, 0); + assert!(observed.observations_ready_for_acceptance() && observed.all_attempts_succeeded()); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status + ), + (true, 1, 0, 0) + ); + json!({ + "attempts": attempts, "provider_events": [], "script_remaining": observed.script_remaining, + "created": observed.created, "destroy_attempts": observed.destroy_attempts, + "destroy_status": observed.destroy_status, "report_write_status": observed.write_status, + "same_context_reentry_observed_by_matched_launcher": true, + "zero_all_acquisition_dispatch_counters_measured": true, + "property_context_and_installed_int64_arguments": context_history, + }) +} + +fn run_whole_canonical_suites(fixture: Fixture) { + assert!( + cfg!(windows), + "candidate pins existing measured MSVC archives" + ); + assert_eq!(digest(fixture.source().as_bytes()), fixture.sha256()); + let evidence = PathBuf::from( + std::env::var_os(fixture.evidence_env()).expect("fresh absolute evidence directory"), + ); + assert!(evidence.is_absolute()); + fs::create_dir(&evidence).expect("evidence directory must not already exist"); + let directory = tempfile::tempdir().unwrap(); + let project_dir = directory.path().join("project"); + let support_dir = directory.path().join("synthetic-stdlib"); + fs::create_dir(&project_dir).unwrap(); + fs::create_dir(&support_dir).unwrap(); + let project = project_dir.join(fixture.file_name()); + let support = support_dir.join("resource_probe.jett"); + fs::write(&project, fixture.source()).unwrap(); + fs::write(&support, SUPPORT).unwrap(); + let mut prepared = Vec::new(); + for release in [false, true] { + prepared.extend(prepare(fixture, &project, &support, release)); + } + assert_eq!(prepared.len(), 4); + fs::remove_dir_all(&project_dir).unwrap(); + fs::remove_dir_all(&support_dir).unwrap(); + assert!(!project.exists() && !support.exists()); + assert!(!project_dir.exists() && !support_dir.exists()); + let mut launches = 0; + let mut private_attempts = 0; + for prepared in prepared { + let profile = if prepared.release { "release" } else { "debug" }; + let matched = archive(prepared.release); + let normal = normal_archive(prepared.release, &matched); + for (private, runtime) in [(false, &normal), (true, &matched)] { + let kind = if private { "private" } else { "normal" }; + let case = evidence.join(format!("{}-{profile}-{kind}", prepared.purpose.name())); + fs::create_dir(&case).unwrap(); + let execution = tempfile::tempdir().unwrap(); + let object = execution.path().join("suite.obj"); + let binary = execution.path().join("suite.exe"); + fs::write(&object, &prepared.object.bytes).unwrap(); + let object_sha256 = digest(&fs::read(&object).unwrap()); + assert_eq!(object_sha256, digest(&prepared.object.bytes)); + assert_eq!(prepared.object.target, runtime.target); + link(runtime, &object, &binary, execution.path()); + fs::remove_file(&object).unwrap(); + assert!(!object.exists()); + let executable_sha256 = digest(&fs::read(&binary).unwrap()); + assert!(!project_dir.exists() && !support_dir.exists()); + let script = empty_two_entry_script(); + let output = super::super::run_bounded_with_env( + &binary, + execution.path(), + if private { + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", script.as_str())) + } else { + None + }, + ); + // The existing bounded runner reaps the child before returning. + fs::remove_file(&binary).unwrap(); + assert!(!binary.exists()); + let pdb = binary.with_extension("pdb"); + if pdb.exists() { + fs::remove_file(&pdb).unwrap(); + } + let observation = if private { + let report_path = execution.path().join("resource-native-report-v1.bin"); + assert!(report_path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let bytes = fs::read(&report_path).unwrap(); + fs::write(case.join("resource-native-report-v1.bin"), &bytes).unwrap(); + let observed = report::decode(&bytes).unwrap(); + let available = observe_private(&observed, prepared.purpose, &prepared.expected); + private_attempts += observed.attempts.len(); + available + } else { + json!({ + "live_custody_observations": null, + "same_context_reentry": null, + "normal_launcher_observability_available": false, + }) + }; + fs::write(case.join("stdout.bin"), &output.stdout).unwrap(); + fs::write(case.join("stderr.bin"), &output.stderr).unwrap(); + fs::write( + case.join("observation.json"), + serde_json::to_vec_pretty(&json!({ + "schema": fixture.evidence_schema(), + "full_acceptance": false, + "remaining_instrumentation_obligations": UNMEASURED, + "whole_source_sha256": fixture.sha256(), + "source_deleted_before_launch": !project.exists() && !support.exists(), + "source_build_profile": profile, + "runtime_profile": runtime.profile, + "runtime_kind": kind, + "runtime_archive_sha256": runtime.archive_sha256, + "entry": prepared.entry.index(), + "purpose": prepared.purpose.name(), + "object_sha256": object_sha256, + "object_deleted_before_launch": !object.exists(), + "executable_sha256": executable_sha256, + "child_reaped_executable_and_pdb_removed": !binary.exists() && !pdb.exists(), + "compiler_checked_program_weak_root_released": true, + "required_value_input_arc_released": true, + "opaque_successful_cache_individual_weak_observation_available": false, + "expected_checked_plan": prepared.expected, + "runtime_observations": observation, + "status": output.status.code(), + })) + .unwrap(), + ) + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{profile} {kind}: {output:?}" + ); + assert!( + output.stdout.is_empty() && output.stderr.is_empty(), + "{output:?}" + ); + launches += 1; + } + } + assert_eq!(launches, 8); + assert_eq!(private_attempts, 8); + fs::write(evidence.join("summary.json"), serde_json::to_vec_pretty(&json!({ + "objects": 4, "processes": launches, "private_entry_attempts": private_attempts, + "full_acceptance": false, + "remaining_instrumentation_obligations": UNMEASURED, + "note": "Successful local test means available measurements passed; it cannot close full acceptance.", + })).unwrap()).unwrap(); +} + +#[test] +#[ignore = "requires pinned measured normal/private archives; records available observations only"] +fn source17_whole_canonical_suites_record_available_native_observations() { + run_whole_canonical_suites(Fixture::Source17); +} + +#[test] +fn pure_replacement_canonical_suite_preparation_preserves_original_required_cache() { + assert_eq!( + digest(PURE_REPLACEMENTS.as_bytes()), + PURE_REPLACEMENTS_SHA256 + ); + let directory = tempfile::tempdir().unwrap(); + let project = directory.path().join("pure_replacement_suites.jett"); + let support = directory.path().join("resource_probe.jett"); + fs::write(&project, PURE_REPLACEMENTS).unwrap(); + fs::write(&support, SUPPORT).unwrap(); + for release in [false, true] { + let prepared = prepare(Fixture::PureReplacements, &project, &support, release); + assert_eq!(prepared.len(), 2); + assert!(matches!(prepared[0].purpose, Purpose::Verify)); + assert!(matches!(prepared[1].purpose, Purpose::Property)); + assert_ne!(prepared[0].entry, prepared[1].entry); + } + directory.close().unwrap(); +} + +#[test] +#[ignore = "requires current measured normal/private archives; unchanged pure replacement suites"] +fn pure_replacement_whole_canonical_suites_record_available_native_observations() { + run_whole_canonical_suites(Fixture::PureReplacements); +} + +#[test] +fn source17_property_history_rejects_wrong_values_order_and_incomplete_calls() { + fn fixture() -> (report::Report, Json) { + let identity = report::AttemptIdentity { id: 41, ordinal: 0 }; + let mut history = vec![report::HistoryEvent { + sequence: 1, + channel: report::Channel::Attempt(identity), + context: None, + kind: report::HistoryKind::Begin, + }]; + let mut expected = Vec::new(); + for (ordinal, bits) in [u64::MAX, 23].into_iter().enumerate() { + let context = format!( + "property 'absent_aggregate_property' trial {}: ", + ordinal + 1 + ) + .into_bytes(); + let sequence = history.len() as u64 + 1; + let call = report::CallIdentity { + call: 51 + ordinal as u64 * 2, + scope: 52 + ordinal as u64 * 2, + function: 7, + signature: 8, + template: 9, + operation: 10, + }; + history.push(report::HistoryEvent { + sequence, + channel: report::Channel::Attempt(identity), + context: Some(context.clone()), + kind: report::HistoryKind::Set, + }); + history.push(report::HistoryEvent { + sequence: sequence + 1, + channel: report::Channel::Attempt(identity), + context: Some(context.clone()), + kind: report::HistoryKind::CallEnter { + call, + parameters: vec![report::Parameter { + parameter: 0, + shape: 3, + value: report::ParameterValue::Int64 { bits }, + }], + }, + }); + history.push(report::HistoryEvent { + sequence: sequence + 2, + channel: report::Channel::Attempt(identity), + context: Some(context.clone()), + kind: report::HistoryKind::CallComplete { + call, + entered_sequence: Some(sequence + 1), + body_status: 0, + failure_status: None, + }, + }); + history.push(report::HistoryEvent { + sequence: sequence + 3, + channel: report::Channel::Attempt(identity), + context: None, + kind: report::HistoryKind::Clear, + }); + expected.push( + json!({ "function": 7, "checked_argument_int64_bits": [bits], + "context_set_bytes_utf8": String::from_utf8(context).unwrap() }), + ); + } + let end = history.len() as u64 + 1; + history.push(report::HistoryEvent { + sequence: end, + channel: report::Channel::Attempt(identity), + context: None, + kind: report::HistoryKind::End, + }); + let context_bytes = history + .iter() + .map(|event| event.context.as_ref().map_or(0, Vec::len) as u64) + .sum(); + let counters = report::Counters { values: [0; 5] }; + let observed = report::Report { + attempts: vec![report::Attempt { + completion: report::Completion { + attempt: 41, + body_status: 0, + cleanup_status: 0, + selected_channel: report::FailureChannel::Clean, + }, + counts: report::Counts { + owners: 0, + loans: 0, + frames: 0, + registry_entries: 0, + owner_handles: 0, + loan_handles: 0, + frame_handles: 0, + provisional_returns: 0, + ordinary_empty: true, + }, + ordinary_message: Vec::new(), + resource_message: Vec::new(), + event_sequence_end: 0, + }], + events: Vec::new(), + script_remaining: 0, + destroy_status: 0, + created: true, + destroy_attempts: 1, + write_status: 0, + observations: report::Snapshot { + counters, + flags: 0, + context_bytes, + final_sequence: end, + attempts: vec![report::AttemptObservation { + identity, + begin: report::Boundary { + sequence: Some(1), + counters, + }, + end: Some(report::Boundary { + sequence: Some(end), + counters, + }), + }], + history, + }, + }; + ( + observed, + json!({"trial_count": 2, "trials_in_checked_order": expected}), + ) + } + // Authenticate the complete baseline before testing any corruption. + let (baseline, expected) = fixture(); + observe_context_history(&baseline, Purpose::Property, &expected); + for mutation in 0..5 { + let (mut observed, expected) = fixture(); + match mutation { + 0 => match &mut observed.observations.history[2].kind { + report::HistoryKind::CallEnter { parameters, .. } => { + parameters[0].value = report::ParameterValue::Int64 { bits: 0 } + } + _ => unreachable!(), + }, + 1 => observed.observations.history.swap(1, 5), + 2 => { + observed.observations.history.remove(3); + } + 3 => observed.observations.flags = 1, + 4 => match &mut observed.observations.history[2].kind { + report::HistoryKind::CallEnter { call, .. } => call.function = 8, + _ => unreachable!(), + }, + _ => unreachable!(), + } + assert!( + std::panic::catch_unwind(|| observe_context_history( + &observed, + Purpose::Property, + &expected + )) + .is_err(), + "corrupt telemetry mutation {mutation} must fail the checked-plan oracle" + ); + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_execution/sum_replacement.rs b/crates/jett_driver/tests/native_conformance/resource_execution/sum_replacement.rs new file mode 100644 index 00000000..37404d39 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_execution/sum_replacement.rs @@ -0,0 +1,156 @@ +//! Guarded future Source-deleted execution over the new exact shared cases. +use super::*; +#[path = "../resource_occupied_struct_reentry.rs"] +mod reentry; +#[path = "../resource_sum_replacement_cases.rs"] +mod sum_cases; + +fn explicit_inputs() { + lightweight_observations::assert_fresh_root(); + assert!( + std::env::var_os(RECEIPT_ENV).is_some(), + "supply the fresh measured private receipt" + ); + assert!( + option_env!("JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1").is_some(), + "compile this test image with that exact measured receipt pin" + ); + assert!( + std::env::var_os("JETT_RESOURCE_NATIVE_EVIDENCE_DIR_V1").is_none(), + "the new matrix must not copy every object/executable" + ); +} + +fn expected_counters(case: &cases::Case) -> [u64; 5] { + let acquisitions = u64::try_from( + case.script + .iter() + .filter(|operation| { + matches!( + operation, + cases::Script::Construct(_) + | cases::Script::ConstructFail(_, _) + | cases::Script::FinalizerPanic(_) + ) + }) + .count(), + ) + .unwrap(); + let borrows = u64::try_from( + case.script + .iter() + .filter(|operation| matches!(operation, cases::Script::Borrow(_, _))) + .count(), + ) + .unwrap(); + // These whole Sources close immediately after their one successful borrow. + // No scripted BorrowFail/Panic case is authored in this finite matrix. + assert!(case.script.iter().all(|operation| !matches!( + operation, + cases::Script::BorrowFail(_, _) | cases::Script::BorrowPanic(_) + ))); + assert!(borrows <= 1); + let closes = u64::from(borrows == 1); + [ + acquisitions + borrows + closes, + acquisitions, + acquisitions, + acquisitions, + acquisitions, + ] +} + +fn assert_counters(case: &cases::Case, actual: &report::Report) { + let expected = expected_counters(case); + assert_eq!( + actual.observations.counters.values, expected, + "{} actual cumulative ingress/dispatch", + case.name + ); + assert_eq!(actual.observations.attempts.len(), 1); + let attempt = &actual.observations.attempts[0]; + assert_eq!(attempt.begin.counters.values, [0; 5]); + assert_eq!(attempt.end.unwrap().counters.values, expected); +} + +fn assert_reentry_counters(first: &cases::Case, ready: &cases::Case, actual: &report::Report) { + let first_expected = expected_counters(first); + let ready_expected = expected_counters(ready); + let total: [u64; 5] = + std::array::from_fn(|index| first_expected[index] + ready_expected[index]); + assert_eq!(actual.observations.attempts.len(), 2); + assert_eq!( + actual.observations.counters.values, total, + "{} retained cumulative ingress/dispatch", + first.name + ); + let first_attempt = &actual.observations.attempts[0]; + let ready_attempt = &actual.observations.attempts[1]; + assert_eq!(first_attempt.begin.counters.values, [0; 5]); + assert_eq!(first_attempt.end.unwrap().counters.values, first_expected); + assert_eq!(ready_attempt.begin.counters.values, first_expected); + assert_eq!(ready_attempt.end.unwrap().counters.values, total); + assert_eq!( + first_attempt.identity.id, + actual.attempts[0].completion.attempt + ); + assert_eq!( + ready_attempt.identity.id, + actual.attempts[1].completion.attempt + ); + assert_eq!( + ( + first_attempt.identity.ordinal, + ready_attempt.identity.ordinal + ), + (0, 1) + ); +} + +#[test] +fn native_resource_direct_sum_replacement_sources_emit_with_original_endpoints() { + assert_eq!(sum_cases::CASES.len(), 19); + let ready = sum_cases::CASES + .iter() + .filter(|case| case.name.ends_with("_ready")) + .collect::>(); + assert_eq!(ready.len(), 6); + for case in ready { + run_object_preflight(std::slice::from_ref(case), case.name); + } +} + +#[test] +#[ignore = "requires fresh sum-capable protocol2 measured private Debug/Release archives and compiled pin"] +fn native_resource_direct_sum_replacements_retire_before_teardown_with_measured_attempts() { + explicit_inputs(); + assert_eq!(sum_cases::CASES.len(), 19); + run_native_cases_observing(sum_cases::CASES, cases::SUPPORT, assert_counters); + eprintln!( + "Resource direct sum replacement: 19 cases; 38 Source-deleted executions; paired Debug/Release; counters measured; per-kind shell telemetry unavailable" + ); +} + +#[test] +#[ignore = "requires fresh sum-capable protocol2 measured private Debug/Release archives and compiled pin"] +fn native_resource_direct_sum_replacements_reenter_with_the_same_provider_grant() { + explicit_inputs(); + assert_eq!(sum_cases::CASES.len(), 19); + for release in [false, true] { + for first in sum_cases::CASES { + let ready = sum_cases::ready_for(first); + reentry::run_original_then_ready_observing( + first, + ready, + release, + cases::SUPPORT, + |actual| { + assert_reentry_counters(first, ready, actual); + }, + ); + } + } + eprintln!( + "Resource direct sum reentry: 19 first/ready pairs per profile; exact cumulative acquisition/dispatch counters; retained actual attempt identities and original completion/error records" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs new file mode 100644 index 00000000..173e193b --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_cases.rs @@ -0,0 +1,149 @@ +//! Shared original Source observations. These rows grant no runtime authority. +use super::cases::{Case, Event, ReferenceOutcome, Script}; +const SINGLE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" +); +const PAIR: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" +); +const PREFIX: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" +); +const fn clean( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} +const fn panic( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + } +} +pub(super) const CASES: &[Case] = &[ + clean( + "occupied_struct_single_ready", + SINGLE, + &[Script::Construct(1001)], + &[Event::Constructed(1001), Event::Finalized(1001)], + ), + clean( + "occupied_struct_single_factory_fail", + SINGLE, + &[Script::ConstructFail(1001, "declined")], + &[Event::ConstructionFailed(1001)], + ), + panic( + "occupied_struct_single_cleanup_panic", + SINGLE, + &[Script::FinalizerPanic(1001)], + &[Event::Constructed(1001), Event::Finalized(1001)], + ), + clean( + "occupied_struct_pair_ready", + PAIR, + &[Script::Construct(1002), Script::Construct(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + clean( + "occupied_struct_pair_first_factory_fail", + PAIR, + &[Script::ConstructFail(1002, "declined")], + &[Event::ConstructionFailed(1002)], + ), + clean( + "occupied_struct_pair_second_factory_fail", + PAIR, + &[ + Script::Construct(1002), + Script::ConstructFail(1003, "declined"), + ], + &[ + Event::Constructed(1002), + Event::ConstructionFailed(1003), + Event::Finalized(1002), + ], + ), + panic( + "occupied_struct_pair_first_cleanup_panic", + PAIR, + &[Script::FinalizerPanic(1002), Script::Construct(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + panic( + "occupied_struct_pair_second_cleanup_panic", + PAIR, + &[Script::Construct(1002), Script::FinalizerPanic(1003)], + &[ + Event::Constructed(1002), + Event::Constructed(1003), + Event::Finalized(1002), + Event::Finalized(1003), + ], + ), + clean( + "occupied_struct_prefix_ready", + PREFIX, + &[Script::Construct(1004)], + &[Event::Constructed(1004), Event::Finalized(1004)], + ), + clean( + "occupied_struct_prefix_factory_fail", + PREFIX, + &[Script::ConstructFail(1004, "declined")], + &[Event::ConstructionFailed(1004)], + ), + panic( + "occupied_struct_prefix_cleanup_panic", + PREFIX, + &[Script::FinalizerPanic(1004)], + &[Event::Constructed(1004), Event::Finalized(1004)], + ), +]; +pub(super) fn ready_for(first: &Case) -> &'static Case { + CASES + .iter() + .find(|case| { + case.source.as_bytes() == first.source.as_bytes() && case.name.ends_with("_ready") + }) + .unwrap() +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs new file mode 100644 index 00000000..2cfa1596 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_harness.rs @@ -0,0 +1,77 @@ +//! Exact original occupied Sources; receipt guards and retirement oracle are shared. +use super::{ + archive, cases, fs, link, object, report, run_native_cases, run_native_cases_with_support, + run_object_preflight, run_object_preflight_with_support, script, +}; +#[path = "resource_occupied_struct_cases.rs"] +mod occupied_struct_cases; +#[path = "resource_primitive_companion_cases.rs"] +mod primitive_companion_cases; +#[path = "resource_occupied_struct_reentry.rs"] +mod reentry; + +#[test] +fn native_resource_occupied_struct_three_sources_emit_in_both_profiles() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_object_preflight(occupied_struct_cases::CASES, "occupied-struct"); +} +#[test] +#[ignore = "requires fresh independently measured occupied-capable Debug/Release single-runtime archives"] +fn native_resource_occupied_struct_scripts_retire_before_teardown() { + assert_eq!(occupied_struct_cases::CASES.len(), 11); + run_native_cases(occupied_struct_cases::CASES); +} +#[test] +#[ignore = "requires fresh independently measured occupied-capable Debug/Release single-runtime archives"] +fn native_resource_occupied_struct_scripts_reenter_with_same_provider_grant() { + for release in [false, true] { + for first in occupied_struct_cases::CASES { + reentry::run_original_then_ready( + first, + occupied_struct_cases::ready_for(first), + release, + ); + } + } +} + +#[test] +fn native_resource_primitive_companions_sources_emit_in_both_profiles() { + assert_eq!(primitive_companion_cases::CASES.len(), 23); + run_object_preflight_with_support( + primitive_companion_cases::CASES, + "primitive-companions", + &companion_support(), + ); +} + +#[test] +#[ignore = "requires fresh independently measured scalar-task-capable Debug/Release single-runtime archives"] +fn native_resource_primitive_companions_retire_before_teardown() { + run_native_cases_with_support(primitive_companion_cases::CASES, &companion_support()); +} + +#[test] +#[ignore = "requires fresh independently measured scalar-task-capable Debug/Release single-runtime archives"] +fn native_resource_primitive_companions_reenter_with_same_provider_grant() { + for release in [false, true] { + for first in primitive_companion_cases::CASES { + reentry::run_original_then_ready_with_support( + first, + primitive_companion_cases::ready_for(first), + release, + &companion_support(), + ); + } + } +} + +fn companion_support() -> String { + format!( + "{}\n{}", + cases::SUPPORT, + include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett" + ) + ) +} diff --git a/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs new file mode 100644 index 00000000..c89fc3a1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_occupied_struct_reentry.rs @@ -0,0 +1,221 @@ +//! Original Source06 first script followed by READY on one retained Session. +//! Every completion remains observable; exceptional reentry is never exit zero. +use super::super::lightweight_observations; +use super::{archive, cases, fs, link, object, report, script}; + +fn script_bytes(case: &cases::Case) -> Vec { + let encoded = script(case); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!( + u32::from_le_bytes(bytes[28..32].try_into().unwrap()) as usize, + case.script.len() + ); + assert_eq!( + u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize, + bytes.len() + ); + bytes +} + +fn original_then_ready_script(first: &cases::Case, ready: &cases::Case) -> String { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + assert_eq!( + (ready.body, ready.cleanup, ready.channel, ready.exit), + (0, 0, 0, 0) + ); + let mut bytes = script_bytes(first); + let ready_bytes = script_bytes(ready); + assert_eq!(&bytes[..16], &ready_bytes[..16]); + bytes.extend_from_slice(&ready_bytes[32..]); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + let operations = u32::try_from(first.script.len() + ready.script.len()).unwrap(); + bytes[28..32].copy_from_slice(&operations.to_le_bytes()); + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn channel(expected: u32) -> report::FailureChannel { + match expected { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid native completion oracle"), + } +} + +fn assert_completion(case: &cases::Case, attempt: &report::Attempt, end: usize) { + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel, + ), + (case.body, case.cleanup, channel(case.channel)), + "{} retained completion: {attempt:?}", + case.name + ); + assert_eq!(attempt.ordinary_message, case.ordinary_message); + assert_eq!(attempt.resource_message, case.resource_message); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} BEFORE teardown: {attempt:?}", + case.name + ); + assert_eq!(attempt.event_sequence_end, u64::try_from(end).unwrap()); +} + +fn assert_original_then_ready(first: &cases::Case, ready: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", first.name); + let first_id = observed.attempts[0].completion.attempt; + let second_id = observed.attempts[1].completion.attempt; + assert!(first_id != 0 && second_id > first_id); + assert_completion(first, &observed.attempts[0], first.events.len()); + assert_completion( + ready, + &observed.attempts[1], + first.events.len() + ready.events.len(), + ); + assert_eq!( + observed.events.len(), + first.events.len() + ready.events.len() + ); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(first.events.iter().chain(ready.events.iter())) + .enumerate() + { + let (label, kind) = super::super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + first.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status, + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert_eq!( + observed.all_attempts_succeeded(), + first.body == 0 && first.cleanup == 0 && first.channel == 0 + ); +} + +pub(super) fn run_original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + run_original_then_ready_with_support(first, ready, release, cases::SUPPORT); +} + +pub(super) fn run_original_then_ready_with_support( + first: &cases::Case, + ready: &cases::Case, + release: bool, + support_source: &str, +) { + run_original_then_ready_observing(first, ready, release, support_source, |_| {}); +} + +// The existing event/completion/custody checks remain mandatory before observation. +pub(super) fn run_original_then_ready_observing( + first: &cases::Case, + ready: &cases::Case, + release: bool, + support_source: &str, + observe: impl FnOnce(&report::Report), +) { + let archive = archive(release); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, first.source).unwrap(); + fs::write(&support, support_source).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let mut lightweight = lightweight_observations::Capture::prepare( + "reentry", + first, + Some(ready), + support_source, + &archive, + &object_path, + &binary, + root, + root, + 1, + true, + ); + let encoded = original_then_ready_script(first, ready); + let actual = super::super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let bytes = fs::read(path).unwrap(); + if let Some(capture) = &mut lightweight { + capture.save_output(&bytes, &actual); + } + let observed = report::decode(&bytes).unwrap(); + assert_eq!( + actual.status.code(), + Some(first.exit), + "{} {} retained first exit: {actual:?}; {observed:?}", + first.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_original_then_ready(first, ready, &observed); + observe(&observed); + if let Some(capture) = lightweight { + directory.close().unwrap(); + capture.finish(&observed, &actual); + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_primitive_companion_cases.rs b/crates/jett_driver/tests/native_conformance/resource_primitive_companion_cases.rs new file mode 100644 index 00000000..4898b968 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_primitive_companion_cases.rs @@ -0,0 +1,246 @@ +//! Additional exact Source controls; the original occupied corpus stays intact. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const PRIMITIVES: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett" +); +const ALIASES: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett" +); +const PENDING: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett" +); +const INSPECTION: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/39_owned_struct_pending_int64_inspection.jett" +); +const PENDING_PRIMITIVES: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett" +); +const PREFIX: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/41_owned_struct_pending_prior_failure.jett" +); +const MATRIX: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett" +); +const UNIT_INSPECTION: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/43_owned_struct_pending_nothing_inspection.jett" +); +const TEXT_INSPECTION: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/44_owned_struct_pending_string_inspection.jett" +); + +const fn clean( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} +const fn panic( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + } +} +const fn body_error( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], + message: &'static str, +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::Error(message), + body: 1, + cleanup: 0, + channel: 1, + ordinary_message: message.as_bytes(), + resource_message: b"", + exit: 71, + } +} + +pub(super) const CASES: &[Case] = &[ + body_error( + "pending_unit_inspection_error", + UNIT_INSPECTION, + &[Script::Construct(1107)], + &[Event::Constructed(1107), Event::Finalized(1107)], + "unsupported binary operation: pending(nothing) Eq nothing", + ), + clean( + "pending_unit_inspection_factory_failure_ready", + UNIT_INSPECTION, + &[Script::ConstructFail(1107, "declined")], + &[Event::ConstructionFailed(1107)], + ), + body_error( + "pending_text_inspection_error", + TEXT_INSPECTION, + &[Script::Construct(1108)], + &[Event::Constructed(1108), Event::Finalized(1108)], + "unsupported binary operation: pending(retained) Eq retained", + ), + clean( + "pending_text_inspection_factory_failure_ready", + TEXT_INSPECTION, + &[Script::ConstructFail(1108, "declined")], + &[Event::ConstructionFailed(1108)], + ), + clean( + "primitive_companions_ready", + PRIMITIVES, + &[Script::Construct(1101)], + &[Event::Constructed(1101), Event::Finalized(1101)], + ), + clean( + "primitive_companions_factory_fail", + PRIMITIVES, + &[Script::ConstructFail(1101, "declined")], + &[Event::ConstructionFailed(1101)], + ), + panic( + "primitive_companions_cleanup_panic", + PRIMITIVES, + &[Script::FinalizerPanic(1101)], + &[Event::Constructed(1101), Event::Finalized(1101)], + ), + clean( + "alias_companions_ready", + ALIASES, + &[Script::Construct(1102)], + &[Event::Constructed(1102), Event::Finalized(1102)], + ), + clean( + "alias_companions_factory_fail", + ALIASES, + &[Script::ConstructFail(1102, "declined")], + &[Event::ConstructionFailed(1102)], + ), + panic( + "alias_companions_cleanup_panic", + ALIASES, + &[Script::FinalizerPanic(1102)], + &[Event::Constructed(1102), Event::Finalized(1102)], + ), + clean( + "pending_int64_ready", + PENDING, + &[Script::Construct(1103)], + &[Event::Constructed(1103), Event::Finalized(1103)], + ), + clean( + "pending_int64_factory_fail", + PENDING, + &[Script::ConstructFail(1103, "declined")], + &[Event::ConstructionFailed(1103)], + ), + panic( + "pending_int64_cleanup_panic", + PENDING, + &[Script::FinalizerPanic(1103)], + &[Event::Constructed(1103), Event::Finalized(1103)], + ), + clean( + "pending_primitives_ready", + PENDING_PRIMITIVES, + &[Script::Construct(1104)], + &[Event::Constructed(1104), Event::Finalized(1104)], + ), + clean( + "pending_primitives_factory_fail", + PENDING_PRIMITIVES, + &[Script::ConstructFail(1104, "declined")], + &[Event::ConstructionFailed(1104)], + ), + panic( + "pending_primitives_cleanup_panic", + PENDING_PRIMITIVES, + &[Script::FinalizerPanic(1104)], + &[Event::Constructed(1104), Event::Finalized(1104)], + ), + clean( + "pending_matrix_ready", + MATRIX, + &[Script::Construct(1106)], + &[Event::Constructed(1106), Event::Finalized(1106)], + ), + clean( + "pending_matrix_factory_fail", + MATRIX, + &[Script::ConstructFail(1106, "declined")], + &[Event::ConstructionFailed(1106)], + ), + panic( + "pending_matrix_cleanup_panic", + MATRIX, + &[Script::FinalizerPanic(1106)], + &[Event::Constructed(1106), Event::Finalized(1106)], + ), + body_error( + "pending_inspection_error", + INSPECTION, + &[Script::Construct(1103)], + &[Event::Constructed(1103), Event::Finalized(1103)], + "unsupported binary operation: pending(17) Eq 17", + ), + clean( + "pending_inspection_factory_failure_ready", + INSPECTION, + &[Script::ConstructFail(1103, "declined")], + &[Event::ConstructionFailed(1103)], + ), + body_error( + "pending_prefix_error", + PREFIX, + &[Script::Construct(1105)], + &[Event::Constructed(1105), Event::Finalized(1105)], + "list.__remove_at: index -1 out of bounds", + ), + clean( + "pending_prefix_factory_failure_ready", + PREFIX, + &[Script::ConstructFail(1105, "declined")], + &[Event::ConstructionFailed(1105)], + ), +]; + +pub(super) fn ready_for(first: &Case) -> &'static Case { + CASES + .iter() + .find(|case| { + case.source.as_bytes() == first.source.as_bytes() && case.name.ends_with("_ready") + }) + .unwrap() +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs new file mode 100644 index 00000000..6e85efa7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_cases.rs @@ -0,0 +1,249 @@ +//! Shared exact derived Source/script observations; no execution or custody authority. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const BREAK_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/07_reflected_field_optional_break.jett" +); +const CONTINUE_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/08_reflected_field_optional_continue.jett" +); +const DEFAULT_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/09_reflected_field_optional_scalar_default.jett" +); + +const READY_SCRIPT: &[Script] = &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), +]; +const READY_EVENTS: &[Event] = &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), +]; + +// First ten rows preserve the existing reference DERIVED_INPUTS exactly. +pub(super) const CASES: &[Case] = &[ + Case { + name: "reflected_resource_break_ready", + source: BREAK_SOURCE, + script: &[Script::Construct(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_break_first_borrow_fail", + source: BREAK_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_break_cleanup_panic", + source: BREAK_SOURCE, + script: &[Script::FinalizerPanic(871), Script::Borrow(871, 17)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_resource_continue_ready", + source: CONTINUE_SOURCE, + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_first_borrow_fail", + source: CONTINUE_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_later_borrow_fail", + source: CONTINUE_SOURCE, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_continue_cleanup_panic", + source: CONTINUE_SOURCE, + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_resource_scalar_default_first_borrow_fail", + source: DEFAULT_SOURCE, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_scalar_default_later_borrow_fail", + source: DEFAULT_SOURCE, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_resource_scalar_default_cleanup_panic", + source: DEFAULT_SOURCE, + script: &[ + Script::FinalizerPanic(871), + Script::BorrowFail(871, "borrow sentinel"), + Script::Borrow(871, 17), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + // Additional same-Source successful tail for scalar Default reentry. + Case { + name: "reflected_resource_scalar_default_ready", + source: DEFAULT_SOURCE, + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, +]; + +pub(super) fn original_cases() -> &'static [Case] { + &CASES[..10] +} + +pub(super) fn ready_for(case: &Case) -> &'static Case { + if case.source == BREAK_SOURCE { + &CASES[0] + } else if case.source == CONTINUE_SOURCE { + &CASES[3] + } else { + assert_eq!(case.source, DEFAULT_SOURCE); + &CASES[10] + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs new file mode 100644 index 00000000..aa7b4304 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_derived_harness.rs @@ -0,0 +1,34 @@ +//! Exact derived reflected Sources through object, single-entry and reentry gates. +use super::{reflected_derived_cases, run_native_cases, run_object_preflight}; + +#[test] +fn native_resource_reflected_derived_controls_emit_in_both_profiles() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + run_object_preflight(reflected_derived_cases::CASES, "reflected-derived-controls"); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_derived_controls_retire_before_teardown() { + assert_eq!(reflected_derived_cases::CASES.len(), 11); + run_native_cases(reflected_derived_cases::CASES); + eprintln!( + "Resource reflected derived single-entry acceptance: 10 original scripts plus scalar-default READY baseline; 22 Source-deleted executions; profiles=debug,release" + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_derived_controls_reenter_with_same_provider_grant() { + assert_eq!(reflected_derived_cases::original_cases().len(), 10); + for release in [false, true] { + for first in reflected_derived_cases::original_cases() { + let ready = reflected_derived_cases::ready_for(first); + super::source06_reentry::run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource reflected derived same-grant reentry acceptance: 10 original scripts then their same-Source READY; 20 Source-deleted executions; 40 entries; profiles=debug,release; retained completions and first nonzero exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs new file mode 100644 index 00000000..8076234d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_cases.rs @@ -0,0 +1,219 @@ +//! Standalone reflected-field Sources; existing191case catalog remains unchanged. +//! Exact tuples are checked by single-entry and same-grant native execution. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const READY_SCRIPT: &[Script] = &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), +]; +const READY_EVENTS: &[Event] = &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Borrowed(871), + Event::Finalized(871), +]; +pub(crate) const CASES: &[Case] = &[ + Case { + name: "reflected_field_original", + source: include_str!("resource/reflected_field_native/01_original.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_index_distinct", + source: include_str!("resource/reflected_field_native/02_index_distinct.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_name_distinct", + source: include_str!("resource/reflected_field_native/03_name_distinct.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_ordinary_helper", + source: include_str!("resource/reflected_field_native/04_ordinary_helper.jett"), + script: READY_SCRIPT, + events: READY_EVENTS, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, +]; + +// Exact original Source06 scripts from its checked exceptional reference gate. +// Native statuses/messages use the existing matched-launcher completion contract; +// these new enclosing cases require linked execution before acceptance. +const SOURCE06: &str = include_str!("resource/reflected_field_native/01_original.jett"); +pub(crate) const SOURCE06_EXCEPTIONAL_CASES: &[Case] = &[ + Case { + name: "reflected_field_construct_fail", + source: SOURCE06, + script: &[Script::ConstructFail(871, "nested factory sentinel")], + events: &[Event::ConstructionFailed(871)], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_first_borrow_fail", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_later_borrow_fail", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowFail(871, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_field_ready_cleanup_panic", + source: SOURCE06, + script: &[ + Script::FinalizerPanic(871), + Script::Borrow(871, 17), + Script::Borrow(871, 17), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_field_first_borrow_provider_panic", + source: SOURCE06, + script: &[Script::Construct(871), Script::BorrowPanic(871)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + Case { + name: "reflected_field_later_borrow_provider_panic", + source: SOURCE06, + script: &[ + Script::Construct(871), + Script::Borrow(871, 17), + Script::BorrowPanic(871), + ], + events: READY_EVENTS, + reference: ReferenceOutcome::ProviderPanic("selected test borrow provider panic"), + body: 1, + cleanup: 0, + channel: 2, + ordinary_message: b"", + resource_message: b"runtime operation panicked", + exit: 73, + }, + Case { + name: "reflected_field_cleanup_wins_provider_panic", + source: SOURCE06, + script: &[Script::FinalizerPanic(871), Script::BorrowPanic(871)], + events: &[ + Event::Constructed(871), + Event::Borrowed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 1, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_field_cleanup_wins_domain_failure_return", + source: SOURCE06, + script: &[ + Script::FinalizerPanic(871), + Script::BorrowFail(871, "borrow sentinel"), + ], + events: &[ + Event::Constructed(871), + Event::BorrowFailed(871), + Event::Finalized(871), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs new file mode 100644 index 00000000..39b2730d --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_harness.rs @@ -0,0 +1,211 @@ +//! Four original checked Sources through strict final emit and matched native execution. +use super::{ + archive, cases, fs, link, object, report, run_native_cases, run_object_preflight, script, +}; + +#[path = "resource_reflected_field_cases.rs"] +mod reflected_field_cases; + +#[test] +fn native_resource_reflected_field_four_sources_emit_in_both_profiles() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_object_preflight(reflected_field_cases::CASES, "reflected-field"); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_four_sources_retire_before_teardown() { + assert_eq!(reflected_field_cases::CASES.len(), 4); + run_native_cases(reflected_field_cases::CASES); + eprintln!( + "Resource reflected-field native acceptance: 4 cases; 8 Source-deleted executions; profiles=debug,release" + ); +} + +fn two_clean_entries(case: &cases::Case) -> String { + assert_eq!( + (case.body, case.cleanup, case.channel, case.exit), + (0, 0, 0, 0) + ); + let encoded = script(case); + let mut bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!(case.script.len(), 3); + assert_eq!(bytes.len(), 84); + let operations = bytes[32..].to_vec(); + bytes.extend(operations); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + bytes[28..32].copy_from_slice(&6u32.to_le_bytes()); + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + assert_eq!(bytes.len(), 136); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn assert_clean_reentry(case: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", case.name); + let mut previous = 0; + for (index, attempt) in observed.attempts.iter().enumerate() { + assert!( + attempt.completion.attempt > previous, + "{} attempt identity", + case.name + ); + previous = attempt.completion.attempt; + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel + ), + (0, 0, report::FailureChannel::Clean), + "{} entry {index}: {observed:?}", + case.name + ); + assert!(attempt.ordinary_message.is_empty() && attempt.resource_message.is_empty()); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} entry {index} BEFORE teardown: {observed:?}", + case.name + ); + assert_eq!( + attempt.event_sequence_end, + ((index + 1) * case.events.len()) as u64 + ); + } + assert_eq!(observed.events.len(), 2 * case.events.len()); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(case.events.iter().cycle().take(2 * case.events.len())) + .enumerate() + { + let (label, kind) = super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + case.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert!(observed.all_attempts_succeeded()); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_four_sources_clean_same_grant_reentry() { + for release in [false, true] { + let archive = archive(release); + for case in reflected_field_cases::CASES { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, case.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let encoded = two_clean_entries(case); + let actual = super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(0), + "{} {}: {actual:?}; {observed:?}", + case.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_clean_reentry(case, &observed); + } + } + eprintln!( + "Resource reflected-field clean reentry acceptance: 4 cases; 8 Source-deleted executions; 16 entries on the same per-executable provider/grant; profiles=debug,release" + ); +} + +#[test] +fn native_resource_reflected_field_source06_exceptional_scripts_emit_in_both_profiles() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_object_preflight( + reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES, + "reflected-field-source06-exceptional", + ); +} + +#[test] +#[ignore = "requires exact Root-measured double-cfg Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_field_source06_exceptional_scripts_retire_before_teardown() { + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + run_native_cases(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES); + eprintln!( + "Resource reflected-field exceptional single-entry acceptance: 8 cases; 16 Source-deleted executions; profiles=debug,release; exceptional same-grant reentry remains separate" + ); +} + +#[path = "resource_reflected_field_reentry.rs"] +mod source06_reentry; + +#[path = "resource_reflected_derived_cases.rs"] +mod reflected_derived_cases; + +#[path = "resource_reflected_derived_harness.rs"] +mod derived_controls; + +#[path = "resource_reflected_return_cases.rs"] +mod reflected_return_cases; + +#[path = "resource_reflected_return_harness.rs"] +mod reflected_returns; diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs new file mode 100644 index 00000000..73b87b49 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_field_reentry.rs @@ -0,0 +1,200 @@ +//! Original Source06 first script followed by READY on one retained Session. +//! Every completion remains observable; exceptional reentry is never exit zero. +use super::{archive, cases, fs, link, object, reflected_field_cases, report, script}; + +fn original_cases() -> impl Iterator { + std::iter::once(&reflected_field_cases::CASES[0]) + .chain(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.iter()) +} + +fn script_bytes(case: &cases::Case) -> Vec { + let encoded = script(case); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect::>(); + assert_eq!(&bytes[..8], b"JTRST001"); + assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 1); + assert_eq!( + u32::from_le_bytes(bytes[28..32].try_into().unwrap()) as usize, + case.script.len() + ); + assert_eq!( + u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize, + bytes.len() + ); + bytes +} + +fn original_then_ready_script(first: &cases::Case, ready: &cases::Case) -> String { + assert_eq!(first.source.as_bytes(), ready.source.as_bytes()); + assert_eq!( + (ready.body, ready.cleanup, ready.channel, ready.exit), + (0, 0, 0, 0) + ); + let mut bytes = script_bytes(first); + let ready_bytes = script_bytes(ready); + assert_eq!(&bytes[..16], &ready_bytes[..16]); + bytes.extend_from_slice(&ready_bytes[32..]); + bytes[24..28].copy_from_slice(&2u32.to_le_bytes()); + let operations = u32::try_from(first.script.len() + ready.script.len()).unwrap(); + bytes[28..32].copy_from_slice(&operations.to_le_bytes()); + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn channel(expected: u32) -> report::FailureChannel { + match expected { + 0 => report::FailureChannel::Clean, + 1 => report::FailureChannel::Body, + 2 => report::FailureChannel::HostPanic, + 3 => report::FailureChannel::Cleanup, + _ => panic!("invalid native completion oracle"), + } +} + +fn assert_completion(case: &cases::Case, attempt: &report::Attempt, end: usize) { + assert_eq!( + ( + attempt.completion.body_status, + attempt.completion.cleanup_status, + attempt.completion.selected_channel, + ), + (case.body, case.cleanup, channel(case.channel)), + "{} retained completion: {attempt:?}", + case.name + ); + assert_eq!(attempt.ordinary_message, case.ordinary_message); + assert_eq!(attempt.resource_message, case.resource_message); + assert!( + attempt.counts.resources_retired() && attempt.counts.ordinary_empty, + "{} BEFORE teardown: {attempt:?}", + case.name + ); + assert_eq!(attempt.event_sequence_end, u64::try_from(end).unwrap()); +} + +fn assert_original_then_ready(first: &cases::Case, ready: &cases::Case, observed: &report::Report) { + assert_eq!(observed.attempts.len(), 2, "{}: {observed:?}", first.name); + let first_id = observed.attempts[0].completion.attempt; + let second_id = observed.attempts[1].completion.attempt; + assert!(first_id != 0 && second_id > first_id); + assert_completion(first, &observed.attempts[0], first.events.len()); + assert_completion( + ready, + &observed.attempts[1], + first.events.len() + ready.events.len(), + ); + assert_eq!( + observed.events.len(), + first.events.len() + ready.events.len() + ); + for (index, (actual, expected)) in observed + .events + .iter() + .zip(first.events.iter().chain(ready.events.iter())) + .enumerate() + { + let (label, kind) = super::super::event(*expected); + assert_eq!( + (actual.sequence, actual.label, actual.kind), + ((index + 1) as u64, label, kind), + "{} event {index}", + first.name + ); + } + assert_eq!(observed.script_remaining, 0); + assert_eq!( + ( + observed.created, + observed.destroy_attempts, + observed.destroy_status, + observed.write_status, + ), + (true, 1, 0, 0) + ); + assert!(observed.observations_ready_for_acceptance()); + assert_eq!( + observed.all_attempts_succeeded(), + first.body == 0 && first.cleanup == 0 && first.channel == 0 + ); +} + +pub(super) fn run_original_then_ready(first: &cases::Case, ready: &cases::Case, release: bool) { + let archive = archive(release); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let project = root.join("project"); + let stdlib = root.join("synthetic-stdlib"); + fs::create_dir(&project).unwrap(); + fs::create_dir(&stdlib).unwrap(); + let entry = project.join("main.jett"); + let support = stdlib.join("resource_probe.jett"); + fs::write(&entry, first.source).unwrap(); + fs::write(&support, cases::SUPPORT).unwrap(); + let artifact = object(&entry, &support, release); + assert_eq!(artifact.target, archive.target); + assert!( + artifact + .symbols + .iter() + .any(|name| name == jett_codegen_cranelift::JETT_AOT_ENTRY_SYMBOL_V1) + ); + assert!( + artifact + .symbols + .iter() + .any(|name| name == "jett_aot_resource_v1_manifest") + ); + let object_path = root.join("resource.obj"); + let binary = root.join(if cfg!(windows) { + "resource.exe" + } else { + "resource" + }); + fs::write(&object_path, artifact.bytes).unwrap(); + link(&archive, &object_path, &binary, root); + fs::remove_dir_all(&project).unwrap(); + fs::remove_dir_all(&stdlib).unwrap(); + assert!(!entry.exists() && !support.exists() && !project.exists() && !stdlib.exists()); + let encoded = original_then_ready_script(first, ready); + let actual = super::super::super::run_bounded_with_env( + &binary, + root, + Some(("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1", &encoded)), + ); + let path = root.join("resource-native-report-v1.bin"); + assert!(path.metadata().unwrap().len() <= 16 * 1024 * 1024); + let observed = report::decode(&fs::read(path).unwrap()).unwrap(); + assert_eq!( + actual.status.code(), + Some(first.exit), + "{} {} retained first exit: {actual:?}; {observed:?}", + first.name, + archive.profile + ); + assert!( + actual.stdout.is_empty() && actual.stderr.is_empty(), + "{actual:?}" + ); + assert_original_then_ready(first, ready, &observed); +} + +#[test] +#[ignore = "requires fresh independently measured reentry-capable Debug/Release single-runtime archives"] +fn native_resource_reflected_field_source06_all_scripts_reenter_with_same_provider_grant() { + let ready = &reflected_field_cases::CASES[0]; + assert_eq!(ready.name, "reflected_field_original"); + assert_eq!(reflected_field_cases::SOURCE06_EXCEPTIONAL_CASES.len(), 8); + assert_eq!(original_cases().count(), 9); + for release in [false, true] { + for first in original_cases() { + run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource reflected Source06 same-grant reentry acceptance: 9 original scripts then READY; 18 Source-deleted executions; 36 entries; profiles=debug,release; retained original completions and first nonzero exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs new file mode 100644 index 00000000..1f48e9dc --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_return_cases.rs @@ -0,0 +1,297 @@ +//! Shared proposed reflected body/owned Return observations; no custody authority. +//! All new Source/completion oracles require actual reference/native gates. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const BODY_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/10_reflected_field_successful_return.jett" +); +const OWNED_SOURCE: &str = include_str!( + "../../../jett_comptime/src/resource_execution/tests/native_source_oracles/source06_exceptional/11_reflected_field_owned_return.jett" +); + +pub(super) const CASES: &[Case] = &[ + Case { + name: "reflected_body_return_first_ready", + source: BODY_SOURCE, + script: &[Script::Construct(881), Script::Borrow(881, 17)], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_ready", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::Borrow(881, 11), + Script::Borrow(881, 17), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_first_borrow_fail", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::BorrowFail(881, "borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_borrow_fail", + source: BODY_SOURCE, + script: &[ + Script::Construct(881), + Script::Borrow(881, 11), + Script::BorrowFail(881, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_body_return_second_cleanup_panic", + source: BODY_SOURCE, + script: &[ + Script::FinalizerPanic(881), + Script::Borrow(881, 11), + Script::Borrow(881, 17), + ], + events: &[ + Event::Constructed(881), + Event::Borrowed(881), + Event::Borrowed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_body_return_handled_failure_cleanup_panic", + source: BODY_SOURCE, + script: &[ + Script::FinalizerPanic(881), + Script::BorrowFail(881, "borrow sentinel"), + ], + events: &[ + Event::Constructed(881), + Event::BorrowFailed(881), + Event::Finalized(881), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 0, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_owned_return_first_ready", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 17), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_second_ready", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::Borrow(891, 17), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Borrowed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_first_borrow_fail", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::BorrowFail(891, "borrow sentinel"), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_second_borrow_fail", + source: OWNED_SOURCE, + script: &[ + Script::Construct(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::BorrowFail(891, "later borrow sentinel"), + Script::Borrow(892, 29), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Borrowed(892), + Event::Finalized(892), + ], + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + }, + Case { + name: "reflected_owned_return_cleanup_suppresses_publication", + source: OWNED_SOURCE, + script: &[ + Script::FinalizerPanic(891), + Script::Construct(892), + Script::Borrow(891, 17), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, + Case { + name: "reflected_owned_return_handled_failure_cleanup_suppresses_publication", + source: OWNED_SOURCE, + script: &[ + Script::FinalizerPanic(891), + Script::Construct(892), + Script::Borrow(891, 11), + Script::BorrowFail(891, "later borrow sentinel"), + ], + events: &[ + Event::Constructed(891), + Event::Constructed(892), + Event::Borrowed(891), + Event::BorrowFailed(891), + Event::Finalized(891), + Event::Finalized(892), + ], + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + }, +]; + +pub(super) fn ready_for(case: &Case) -> &'static Case { + if case.source == BODY_SOURCE { + &CASES[0] + } else { + assert_eq!(case.source, OWNED_SOURCE); + &CASES[6] + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs b/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs new file mode 100644 index 00000000..fa650365 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_reflected_return_harness.rs @@ -0,0 +1,36 @@ +//! Dedicated reflected Return objects, Source-deleted singles and immutable reentry. +use super::{reflected_return_cases, run_native_cases, run_object_preflight}; + +#[test] +fn native_resource_reflected_body_and_owned_returns_emit_in_both_profiles() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_object_preflight( + reflected_return_cases::CASES, + "reflected-body-and-owned-returns", + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_body_and_owned_returns_retire_before_teardown() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + run_native_cases(reflected_return_cases::CASES); + eprintln!( + "Resource dedicated reflected Return single-entry acceptance: 12 cases; 24 Source-deleted executions; profiles=debug,release" + ); +} + +#[test] +#[ignore = "requires exact independently measured reentry-capable Debug/Release single-runtime archive receipt"] +fn native_resource_reflected_body_and_owned_returns_reenter_with_same_provider_grant() { + assert_eq!(reflected_return_cases::CASES.len(), 12); + for release in [false, true] { + for first in reflected_return_cases::CASES { + let ready = reflected_return_cases::ready_for(first); + super::source06_reentry::run_original_then_ready(first, ready, release); + } + } + eprintln!( + "Resource dedicated reflected Return same-grant reentry acceptance: 12 first scripts then same-Source READY; 24 Source-deleted executions; 48 entries; profiles=debug,release; retained original completions and first exits" + ); +} diff --git a/crates/jett_driver/tests/native_conformance/resource_report.rs b/crates/jett_driver/tests/native_conformance/resource_report.rs new file mode 100644 index 00000000..ad63c5e1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_report.rs @@ -0,0 +1,1607 @@ +//! Strict owned observations from the matched private Resource launcher. +//! This module grants no Source, provider, registry or custody authority. + +const MAX_REPORT: usize = 16 * 1024 * 1024; +const MAX_ATTEMPTS: u32 = 64; +const MAX_EVENTS: u32 = 8192; +const MAX_MESSAGE: u32 = 65_536; +const MAX_SCRIPT_REMAINING: u64 = 4096; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum DecodeError { + TooLarge, + Truncated { + offset: usize, + needed: usize, + }, + Magic, + Version(u32), + Length { + declared: u64, + actual: usize, + }, + Reserved { + field: &'static str, + value: u32, + }, + Limit { + field: &'static str, + value: u64, + maximum: u64, + }, + Flag { + field: &'static str, + value: u32, + }, + Channel(u32), + EventKind(u32), + AttemptOrder(usize), + EventOrder(usize), + EventEnd { + attempt: usize, + end: u64, + }, + Completion(usize), + Teardown, + Trailing(usize), + Allocation, + Observation, + ObservationFlags(u32), + HistoryKind(u32), + ParameterValue(u32), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum FailureChannel { + Clean, + Body, + HostPanic, + Cleanup, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum EventKind { + Constructed, + ConstructionFailed, + Borrowed, + BorrowFailed, + Finalized, +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Completion { + pub(super) attempt: u64, + pub(super) body_status: u32, + pub(super) cleanup_status: u32, + pub(super) selected_channel: FailureChannel, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Counts { + pub(super) owners: u64, + pub(super) loans: u64, + pub(super) frames: u64, + pub(super) registry_entries: u64, + pub(super) owner_handles: u64, + pub(super) loan_handles: u64, + pub(super) frame_handles: u64, + pub(super) provisional_returns: u64, + pub(super) ordinary_empty: bool, +} +impl Counts { + pub(super) fn resources_retired(&self) -> bool { + [ + self.owners, + self.loans, + self.frames, + self.registry_entries, + self.owner_handles, + self.loan_handles, + self.frame_handles, + self.provisional_returns, + ] + .into_iter() + .all(|count| count == 0) + } +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Attempt { + pub(super) completion: Completion, + pub(super) counts: Counts, + pub(super) ordinary_message: Vec, + pub(super) resource_message: Vec, + pub(super) event_sequence_end: u64, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Event { + pub(super) sequence: u64, + pub(super) label: i64, + pub(super) kind: EventKind, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Report { + pub(super) observations: Snapshot, + pub(super) attempts: Vec, + pub(super) events: Vec, + pub(super) script_remaining: u64, + pub(super) destroy_status: u32, + pub(super) created: bool, + pub(super) destroy_attempts: u32, + pub(super) write_status: u32, +} +impl Report { + /// Counts are the real observations copied BEFORE destruction. A cleanup + /// panic can still retire every owner, and remains visible in its channel. + pub(super) fn resources_retired(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.counts.resources_retired()) + } + pub(super) fn ordinary_storage_empty(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.counts.ordinary_empty) + } + pub(super) fn cleanup_succeeded(&self) -> bool { + self.attempts + .iter() + .all(|attempt| attempt.completion.cleanup_status == 0) + } + pub(super) fn script_consumed(&self) -> bool { + self.script_remaining == 0 + } + pub(super) fn teardown_succeeded(&self) -> bool { + self.created + && self.destroy_attempts == 1 + && self.destroy_status == 0 + && self.write_status == 0 + } + pub(super) fn events_all_assigned(&self) -> bool { + self.attempts + .last() + .map_or(0, |attempt| attempt.event_sequence_end) + == self.events.last().map_or(0, |event| event.sequence) + } + /// Expected Source body/cleanup outcomes, process exit, stdout and stderr + /// remain separate exact harness oracles. This never treats a panic as clean. + pub(super) fn observations_ready_for_acceptance(&self) -> bool { + !self.attempts.is_empty() + && self.observations.complete(self.attempts.len()) + && self.resources_retired() + && self.ordinary_storage_empty() + && self.script_consumed() + && self.teardown_succeeded() + && self.events_all_assigned() + } + pub(super) fn all_attempts_succeeded(&self) -> bool { + !self.attempts.is_empty() + && self.attempts.iter().all(|attempt| { + attempt.completion.selected_channel == FailureChannel::Clean + && attempt.completion.body_status == 0 + && attempt.completion.cleanup_status == 0 + }) + } +} + +const MAX_HISTORY: u32 = 8192; +const MAX_CONTEXT: u32 = 65_536; +const MAX_CONTEXT_BYTES: u64 = 1_048_576; +const MAX_PARAMETERS: u32 = 1024; +const KNOWN_OBSERVATION_FLAGS: u32 = 63; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct Counters { + pub(super) values: [u64; 5], +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct AttemptIdentity { + pub(super) id: u64, + pub(super) ordinal: u32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Channel { + Session, + Attempt(AttemptIdentity), +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct CallIdentity { + pub(super) call: u64, + pub(super) scope: u64, + pub(super) function: u32, + pub(super) signature: u32, + pub(super) template: u32, + pub(super) operation: u32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ParameterValue { + Int64 { bits: u64 }, + Unobserved, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct Parameter { + pub(super) parameter: u32, + pub(super) shape: u32, + pub(super) value: ParameterValue, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct Boundary { + pub(super) sequence: Option, + pub(super) counters: Counters, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct AttemptObservation { + pub(super) identity: AttemptIdentity, + pub(super) begin: Boundary, + pub(super) end: Option, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) enum HistoryKind { + Begin, + End, + Set, + Clear, + CallEnter { + call: CallIdentity, + parameters: Vec, + }, + CallComplete { + call: CallIdentity, + entered_sequence: Option, + body_status: u32, + failure_status: Option, + }, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct HistoryEvent { + pub(super) sequence: u64, + pub(super) channel: Channel, + pub(super) context: Option>, + pub(super) kind: HistoryKind, +} +#[derive(Debug, PartialEq, Eq)] +pub(super) struct Snapshot { + pub(super) counters: Counters, + pub(super) flags: u32, + pub(super) attempts: Vec, + pub(super) history: Vec, + pub(super) context_bytes: u64, + pub(super) final_sequence: u64, +} +impl Counters { + fn follows(self, previous: Self) -> bool { + self.values[1] <= self.values[0] + && self + .values + .iter() + .zip(previous.values) + .all(|(now, before)| *now >= before) + } +} +impl Snapshot { + fn complete(&self, attempts: usize) -> bool { + self.flags == 0 + && self.attempts.len() == attempts + && self.attempts.iter().all(|attempt| { + attempt.begin.sequence.is_some() + && attempt.end.is_some_and(|end| end.sequence.is_some()) + }) + && self.history.iter().all(|event| { + !matches!( + event.kind, + HistoryKind::CallComplete { + entered_sequence: None, + .. + } + ) + }) + } + fn validate(&self, attempts: &[Attempt]) -> Result<(), DecodeError> { + let bad = || DecodeError::Observation; + let zero = Counters { values: [0; 5] }; + let identity_known = |identity: AttemptIdentity| { + attempts + .get(identity.ordinal as usize) + .is_some_and(|attempt| attempt.completion.attempt == identity.id) + }; + let mut previous = zero; + let mut previous_ordinal = None; + let mut boundary_sequence = 0; + for attempt in &self.attempts { + if !identity_known(attempt.identity) + || previous_ordinal.is_some_and(|ordinal| attempt.identity.ordinal <= ordinal) + || !attempt.begin.counters.follows(previous) + { + return Err(bad()); + } + previous_ordinal = Some(attempt.identity.ordinal); + previous = attempt.begin.counters; + for (boundary, beginning) in [(Some(attempt.begin), true), (attempt.end, false)] { + let Some(boundary) = boundary else { continue }; + if !boundary.counters.follows(previous) { + return Err(bad()); + } + previous = boundary.counters; + if let Some(sequence) = boundary.sequence { + if sequence <= boundary_sequence { + return Err(bad()); + } + let event = self + .history + .get(usize::try_from(sequence - 1).map_err(|_| bad())?) + .ok_or_else(bad)?; + if event.sequence != sequence + || event.channel != Channel::Attempt(attempt.identity) + || if beginning { + !matches!(event.kind, HistoryKind::Begin) + } else { + !matches!(event.kind, HistoryKind::End) + } + { + return Err(bad()); + } + boundary_sequence = sequence; + } + } + } + if !self.counters.follows(previous) { + return Err(bad()); + } + let mut active = None; + let mut current_context: Option<&[u8]> = None; + let mut call_stack = Vec::new(); + call_stack + .try_reserve_exact(self.history.len()) + .map_err(|_| DecodeError::Allocation)?; + for (index, event) in self.history.iter().enumerate() { + if event.sequence != u64::try_from(index).map_err(|_| bad())? + 1 { + return Err(bad()); + } + if let Channel::Attempt(identity) = event.channel { + if !identity_known(identity) { + return Err(bad()); + } + if self.flags == 0 && active.is_some_and(|current| current != identity) { + return Err(bad()); + } + } + match &event.kind { + HistoryKind::Begin => { + let Channel::Attempt(identity) = event.channel else { + return Err(bad()); + }; + if (self.flags == 0 && active.is_some()) || event.context.is_some() { + return Err(bad()); + } + active = Some(identity); + current_context = None; + if self.flags == 0 + && !self.attempts.iter().any(|attempt| { + attempt.identity == identity + && attempt.begin.sequence == Some(event.sequence) + }) + { + return Err(bad()); + } + } + HistoryKind::End => { + let Channel::Attempt(identity) = event.channel else { + return Err(bad()); + }; + if self.flags == 0 && active != Some(identity) { + return Err(bad()); + } + if self.flags == 0 + && !self.attempts.iter().any(|attempt| { + attempt.identity == identity + && attempt + .end + .is_some_and(|end| end.sequence == Some(event.sequence)) + }) + { + return Err(bad()); + } + if self.flags == 0 && !call_stack.is_empty() { + return Err(bad()); + } + if self.flags == 0 && event.context.as_deref() != current_context { + return Err(bad()); + } + active = None; + current_context = None; + } + HistoryKind::Set if event.context.is_none() => return Err(bad()), + HistoryKind::Clear if event.context.is_some() => return Err(bad()), + HistoryKind::CallEnter { call, parameters } => { + if !matches!(event.channel, Channel::Attempt(_)) + || event.context.is_none() + || call.call == 0 + || call.scope == 0 + { + return Err(bad()); + } + if self.flags == 0 { + if event.context.as_deref() != current_context { + return Err(bad()); + } + call_stack.push(event.sequence); + } + if self.history[..index].iter().any(|prior| matches!(&prior.kind, HistoryKind::CallEnter { call: old, .. } if old.call == call.call || old.scope == call.scope)) { return Err(bad()); } + if parameters.iter().enumerate().any(|(index, parameter)| { + parameters[..index] + .iter() + .any(|prior| prior.parameter == parameter.parameter) + }) { + return Err(bad()); + } + } + HistoryKind::CallComplete { + call, + entered_sequence, + body_status, + failure_status, + } => { + if *failure_status == Some(0) || (failure_status.is_none() && *body_status != 0) + { + return Err(bad()); + } + if self.flags == 0 && event.context.as_deref() != current_context { + return Err(bad()); + } + if self.flags == 0 && call_stack.pop() != *entered_sequence { + return Err(bad()); + } + if let Some(sequence) = entered_sequence { + if *sequence == 0 || *sequence >= event.sequence { + return Err(bad()); + } + let entered = self + .history + .get(usize::try_from(sequence - 1).map_err(|_| bad())?) + .ok_or_else(bad)?; + if entered.channel != event.channel + || !matches!(&entered.kind, HistoryKind::CallEnter { call: original, .. } if original == call) + { + return Err(bad()); + } + if self.history[..index].iter().any(|prior| matches!(&prior.kind, HistoryKind::CallComplete { entered_sequence: Some(old), .. } if old == sequence)) { return Err(bad()); } + } else if self.flags == 0 { + return Err(bad()); + } + } + HistoryKind::Set | HistoryKind::Clear => { + if self.flags == 0 && event.channel == Channel::Session && active.is_some() { + return Err(bad()); + } + if matches!(event.channel, Channel::Attempt(_)) { + current_context = event.context.as_deref(); + } + } + } + if self.flags == 0 + && matches!(event.channel, Channel::Attempt(_)) + && active.is_none() + && !matches!(event.kind, HistoryKind::End) + { + return Err(bad()); + } + } + + if self.flags == 0 && self.history.iter().any(|event| matches!(event.kind, HistoryKind::CallEnter { .. }) + && !self.history.iter().any(|completed| matches!(&completed.kind, HistoryKind::CallComplete { entered_sequence: Some(sequence), .. } if *sequence == event.sequence))) { + return Err(bad()); + } + if self.flags == 0 && (active.is_some() || !self.complete(attempts.len())) { + return Err(bad()); + } + Ok(()) + } +} + +struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, +} +impl<'a> Cursor<'a> { + fn take(&mut self, length: usize) -> Result<&'a [u8], DecodeError> { + let end = self + .offset + .checked_add(length) + .ok_or(DecodeError::TooLarge)?; + let output = self + .bytes + .get(self.offset..end) + .ok_or(DecodeError::Truncated { + offset: self.offset, + needed: length, + })?; + self.offset = end; + Ok(output) + } + fn u32(&mut self) -> Result { + let data = self.take(4)?; + Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])) + } + fn u64(&mut self) -> Result { + let data = self.take(8)?; + Ok(u64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ])) + } + fn zero(&mut self, field: &'static str) -> Result<(), DecodeError> { + let value = self.u32()?; + if value != 0 { + return Err(DecodeError::Reserved { field, value }); + } + Ok(()) + } + fn flag(&mut self, field: &'static str) -> Result { + match self.u32()? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(DecodeError::Flag { field, value }), + } + } + fn bounded(&mut self, field: &'static str, maximum: u32) -> Result { + let value = self.u32()?; + if value > maximum { + return Err(DecodeError::Limit { + field, + value: u64::from(value), + maximum: u64::from(maximum), + }); + } + Ok(value) + } + fn counts(&mut self) -> Result { + let result = Counts { + owners: self.u64()?, + loans: self.u64()?, + frames: self.u64()?, + registry_entries: self.u64()?, + owner_handles: self.u64()?, + loan_handles: self.u64()?, + frame_handles: self.u64()?, + provisional_returns: self.u64()?, + ordinary_empty: self.flag("ordinary_empty")?, + }; + self.zero("counts")?; + Ok(result) + } + fn message(&mut self, length: u32) -> Result, DecodeError> { + let bytes = self.take(length as usize)?; + let mut result = Vec::new(); + result + .try_reserve_exact(bytes.len()) + .map_err(|_| DecodeError::Allocation)?; + result.extend_from_slice(bytes); + Ok(result) + } + fn attempt(&mut self, index: usize) -> Result { + let attempt = self.u64()?; + let body_status = self.u32()?; + let cleanup_status = self.u32()?; + let selected_channel = match self.u32()? { + 0 => FailureChannel::Clean, + 1 => FailureChannel::Body, + 2 => FailureChannel::HostPanic, + 3 => FailureChannel::Cleanup, + value => return Err(DecodeError::Channel(value)), + }; + self.zero("completion")?; + if attempt == 0 + || match selected_channel { + FailureChannel::Clean => body_status != 0 || cleanup_status != 0, + FailureChannel::Body | FailureChannel::HostPanic => cleanup_status != 0, + FailureChannel::Cleanup => cleanup_status == 0, + } + { + return Err(DecodeError::Completion(index)); + } + let counts = self.counts()?; + let ordinary_length = self.bounded("ordinary_message", MAX_MESSAGE)?; + let resource_length = self.bounded("resource_message", MAX_MESSAGE)?; + let event_sequence_end = self.u64()?; + let ordinary_message = self.message(ordinary_length)?; + let resource_message = self.message(resource_length)?; + Ok(Attempt { + completion: Completion { + attempt, + body_status, + cleanup_status, + selected_channel, + }, + counts, + ordinary_message, + resource_message, + event_sequence_end, + }) + } + fn event(&mut self) -> Result { + let sequence = self.u64()?; + let label = i64::from_le_bytes( + self.take(8)? + .try_into() + .map_err(|_| DecodeError::TooLarge)?, + ); + let kind = match self.u32()? { + 1 => EventKind::Constructed, + 2 => EventKind::ConstructionFailed, + 3 => EventKind::Borrowed, + 4 => EventKind::BorrowFailed, + 5 => EventKind::Finalized, + value => return Err(DecodeError::EventKind(value)), + }; + self.zero("event")?; + Ok(Event { + sequence, + label, + kind, + }) + } +} + +impl Cursor<'_> { + fn optional_u64(&mut self, field: &'static str) -> Result, DecodeError> { + if self.flag(field)? { + self.u64().map(Some) + } else { + Ok(None) + } + } + fn optional_u32(&mut self, field: &'static str) -> Result, DecodeError> { + if self.flag(field)? { + self.u32().map(Some) + } else { + Ok(None) + } + } + fn observation_counters(&mut self) -> Result { + Ok(Counters { + values: [ + self.u64()?, + self.u64()?, + self.u64()?, + self.u64()?, + self.u64()?, + ], + }) + } + fn observation_boundary(&mut self) -> Result { + Ok(Boundary { + sequence: self.optional_u64("boundary_sequence")?, + counters: self.observation_counters()?, + }) + } + fn observation_identity(&mut self) -> Result { + Ok(AttemptIdentity { + id: self.u64()?, + ordinal: self.u32()?, + }) + } + fn call_identity(&mut self) -> Result { + Ok(CallIdentity { + call: self.u64()?, + scope: self.u64()?, + function: self.u32()?, + signature: self.u32()?, + template: self.u32()?, + operation: self.u32()?, + }) + } + fn observations(&mut self, attempts: &[Attempt]) -> Result { + let counters = self.observation_counters()?; + let flags = self.u32()?; + if flags & !KNOWN_OBSERVATION_FLAGS != 0 { + return Err(DecodeError::ObservationFlags(flags)); + } + let count = self.bounded("observed_attempts", MAX_ATTEMPTS)?; + let history_count = self.bounded("history", MAX_HISTORY)?; + let context_bytes = self.u64()?; + if context_bytes > MAX_CONTEXT_BYTES { + return Err(DecodeError::Limit { + field: "context_bytes", + value: context_bytes, + maximum: MAX_CONTEXT_BYTES, + }); + } + let final_sequence = self.u64()?; + let minimum = (count as usize) + .checked_mul(60) + .and_then(|rows| { + (history_count as usize) + .checked_mul(20) + .and_then(|events| rows.checked_add(events)) + }) + .and_then(|value| value.checked_add(16)) + .ok_or(DecodeError::TooLarge)?; + if minimum > self.bytes.len() - self.offset { + return Err(DecodeError::Truncated { + offset: self.offset, + needed: minimum, + }); + } + let mut observed = Vec::new(); + let mut history = Vec::new(); + observed + .try_reserve_exact(count as usize) + .map_err(|_| DecodeError::Allocation)?; + history + .try_reserve_exact(history_count as usize) + .map_err(|_| DecodeError::Allocation)?; + for _ in 0..count { + let identity = self.observation_identity()?; + let begin = self.observation_boundary()?; + let end = if self.flag("end_present")? { + Some(self.observation_boundary()?) + } else { + None + }; + observed.push(AttemptObservation { + identity, + begin, + end, + }); + } + let mut copied_context = 0u64; + for _ in 0..history_count { + let sequence = self.u64()?; + let kind = self.u32()?; + let channel = if self.flag("attempt_present")? { + Channel::Attempt(self.observation_identity()?) + } else { + Channel::Session + }; + let context = if self.flag("context_present")? { + let length = self.bounded("context_length", MAX_CONTEXT)?; + copied_context = copied_context + .checked_add(u64::from(length)) + .ok_or(DecodeError::TooLarge)?; + if copied_context > context_bytes { + return Err(DecodeError::Observation); + } + Some(self.message(length)?) + } else { + None + }; + let kind = match kind { + 1 => HistoryKind::Begin, + 2 => HistoryKind::Set, + 3 => { + let call = self.call_identity()?; + let count = self.bounded("parameters", MAX_PARAMETERS)?; + let minimum = (count as usize) + .checked_mul(12) + .ok_or(DecodeError::TooLarge)?; + if minimum > self.bytes.len() - self.offset { + return Err(DecodeError::Truncated { + offset: self.offset, + needed: minimum, + }); + } + let mut parameters = Vec::new(); + parameters + .try_reserve_exact(count as usize) + .map_err(|_| DecodeError::Allocation)?; + for _ in 0..count { + let parameter = self.u32()?; + let shape = self.u32()?; + let tag = self.u32()?; + let value = match tag { + 0 => ParameterValue::Unobserved, + 1 => ParameterValue::Int64 { bits: self.u64()? }, + _ => return Err(DecodeError::ParameterValue(tag)), + }; + parameters.push(Parameter { + parameter, + shape, + value, + }); + } + HistoryKind::CallEnter { call, parameters } + } + 4 => HistoryKind::CallComplete { + call: self.call_identity()?, + entered_sequence: self.optional_u64("entered_sequence")?, + body_status: self.u32()?, + failure_status: self.optional_u32("failure_status")?, + }, + 5 => HistoryKind::Clear, + 6 => HistoryKind::End, + value => return Err(DecodeError::HistoryKind(value)), + }; + history.push(HistoryEvent { + sequence, + channel, + context, + kind, + }); + } + if copied_context != context_bytes || final_sequence != u64::from(history_count) { + return Err(DecodeError::Observation); + } + let result = Snapshot { + counters, + flags, + attempts: observed, + history, + context_bytes, + final_sequence, + }; + result.validate(attempts)?; + Ok(result) + } +} + +pub(super) fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_REPORT { + return Err(DecodeError::TooLarge); + } + let mut cursor = Cursor { bytes, offset: 0 }; + if cursor.take(8)? != b"JTRRP002" { + return Err(DecodeError::Magic); + } + let version = cursor.u32()?; + if version != 2 { + return Err(DecodeError::Version(version)); + } + cursor.zero("header")?; + let declared = cursor.u64()?; + if declared != bytes.len() as u64 { + return Err(DecodeError::Length { + declared, + actual: bytes.len(), + }); + } + let attempt_count = cursor.bounded("attempts", MAX_ATTEMPTS)?; + let event_count = cursor.bounded("events", MAX_EVENTS)?; + let script_remaining = cursor.u64()?; + if script_remaining > MAX_SCRIPT_REMAINING { + return Err(DecodeError::Limit { + field: "script_remaining", + value: script_remaining, + maximum: MAX_SCRIPT_REMAINING, + }); + } + let mut attempts = Vec::new(); + let mut events = Vec::new(); + attempts + .try_reserve_exact(attempt_count as usize) + .map_err(|_| DecodeError::Allocation)?; + events + .try_reserve_exact(event_count as usize) + .map_err(|_| DecodeError::Allocation)?; + let mut previous_attempt = 0; + let mut previous_end = 0; + for index in 0..attempt_count as usize { + let attempt = cursor.attempt(index)?; + if attempt.completion.attempt <= previous_attempt { + return Err(DecodeError::AttemptOrder(index)); + } + if attempt.event_sequence_end < previous_end { + return Err(DecodeError::EventEnd { + attempt: index, + end: attempt.event_sequence_end, + }); + } + previous_attempt = attempt.completion.attempt; + previous_end = attempt.event_sequence_end; + attempts.push(attempt); + } + let mut previous_sequence = 0; + for index in 0..event_count as usize { + let event = cursor.event()?; + if event.sequence <= previous_sequence { + return Err(DecodeError::EventOrder(index)); + } + previous_sequence = event.sequence; + events.push(event); + } + for (index, attempt) in attempts.iter().enumerate() { + let end = attempt.event_sequence_end; + if end != 0 + && events + .binary_search_by_key(&end, |event| event.sequence) + .is_err() + { + return Err(DecodeError::EventEnd { + attempt: index, + end, + }); + } + } + let observations = cursor.observations(&attempts)?; + let destroy_status = cursor.u32()?; + let created = cursor.flag("created")?; + let destroy_attempts = cursor.u32()?; + let write_status = cursor.u32()?; + if destroy_attempts != u32::from(created) + || (!created && (destroy_status != 0 || !attempts.is_empty() || !events.is_empty())) + { + return Err(DecodeError::Teardown); + } + if cursor.offset != bytes.len() { + return Err(DecodeError::Trailing(bytes.len() - cursor.offset)); + } + Ok(Report { + observations, + attempts, + events, + script_remaining, + destroy_status, + created, + destroy_attempts, + write_status, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + fn word(bytes: &mut Vec, value: u32) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + fn wide(bytes: &mut Vec, value: u64) { + bytes.extend_from_slice(&value.to_le_bytes()); + } + + const FIRST_PROVIDER_EVENT: usize = 40 + 112 + 10 + 112; + struct ObservationOffsets { + start: usize, + rows: [usize; 2], + history: [usize; 8], + parameter_tag: usize, + } + fn observation_fixture(bytes: &mut Vec) -> ObservationOffsets { + let start = bytes.len(); + for _ in 0..5 { + wide(bytes, 0); + } + word(bytes, 0); + word(bytes, 2); + word(bytes, 8); + wide(bytes, 6); + wide(bytes, 8); + let mut rows = [0; 2]; + for (index, (id, begin, end)) in [(41, 1, 6), (44, 7, 8)].into_iter().enumerate() { + rows[index] = bytes.len(); + wide(bytes, id); + word(bytes, index as u32); + word(bytes, 1); + wide(bytes, begin); + for _ in 0..5 { + wide(bytes, 0); + } + word(bytes, 1); + word(bytes, 1); + wide(bytes, end); + for _ in 0..5 { + wide(bytes, 0); + } + } + let mut history = [0; 8]; + let mut parameter_tag = 0; + for (index, kind) in [1, 2, 3, 4, 5, 6, 1, 6].into_iter().enumerate() { + history[index] = bytes.len(); + wide(bytes, index as u64 + 1); + word(bytes, kind); + word(bytes, 1); + wide(bytes, if index < 6 { 41 } else { 44 }); + word(bytes, u32::from(index >= 6)); + if (1..=3).contains(&index) { + word(bytes, 1); + word(bytes, 2); + bytes.extend_from_slice(&[0, 255]); + } else { + word(bytes, 0); + } + if kind == 3 || kind == 4 { + wide(bytes, 11); + wide(bytes, 12); + for value in [7, 8, 9, 10] { + word(bytes, value); + } + if kind == 3 { + word(bytes, 1); + word(bytes, 0); + word(bytes, 1); + parameter_tag = bytes.len(); + word(bytes, 1); + wide(bytes, i64::MIN as u64); + } else { + word(bytes, 1); + wide(bytes, 3); + word(bytes, 0); + word(bytes, 0); + } + } + } + ObservationOffsets { + start, + rows, + history, + parameter_tag, + } + } + fn observation_offsets() -> ObservationOffsets { + let mut bytes = vec![0; FIRST_PROVIDER_EVENT + 48]; + observation_fixture(&mut bytes) + } + + fn fixture() -> Vec { + let mut bytes = b"JTRRP002".to_vec(); + word(&mut bytes, 2); + word(&mut bytes, 0); + wide(&mut bytes, 0); + word(&mut bytes, 2); + word(&mut bytes, 2); + wide(&mut bytes, 3); + for (attempt, body, channel, end, ordinary, resource) in [ + (41, 71, 1, 1, &b"ordinary"[..], &[0xff, b'x'][..]), + (44, 0, 0, 4, &[][..], &[][..]), + ] { + wide(&mut bytes, attempt); + for value in [body, 0, channel, 0] { + word(&mut bytes, value); + } + for _ in 0..8 { + wide(&mut bytes, 0); + } + word(&mut bytes, 1); + word(&mut bytes, 0); + word(&mut bytes, ordinary.len() as u32); + word(&mut bytes, resource.len() as u32); + wide(&mut bytes, end); + bytes.extend_from_slice(ordinary); + bytes.extend_from_slice(resource); + } + for (sequence, label, kind) in [(1, -501i64, 3), (4, i64::MIN, 5)] { + wide(&mut bytes, sequence); + wide(&mut bytes, label as u64); + word(&mut bytes, kind); + word(&mut bytes, 0); + } + observation_fixture(&mut bytes); + for value in [0, 1, 1, 0] { + word(&mut bytes, value); + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + decode(&bytes).expect("valid uncorrupted v2 baseline before any mutation"); + bytes + } + fn set_word(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + fn set_wide(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + #[test] + fn resource_report_preserves_original_fields_and_keeps_acceptance_predicates_separate() { + let report = decode(&fixture()).unwrap(); + assert_eq!(report.attempts.len(), 2); + assert_eq!(report.attempts[0].completion.body_status, 71); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::Body + ); + assert_eq!(report.attempts[0].ordinary_message, b"ordinary"); + assert_eq!(report.attempts[0].resource_message, [0xff, b'x']); + assert_eq!(report.events[0].label, -501); + assert_eq!(report.events[1].label, i64::MIN); + assert_eq!(report.events[0].kind, EventKind::Borrowed); + assert!( + report.resources_retired() + && report.ordinary_storage_empty() + && report.cleanup_succeeded() + ); + assert!(report.teardown_succeeded() && report.events_all_assigned()); + assert!( + !report.script_consumed() + && !report.all_attempts_succeeded() + && !report.observations_ready_for_acceptance() + ); + } + #[test] + fn resource_report_rejects_every_truncation_trailing_bytes_and_declared_length_lies() { + let bytes = fixture(); + for end in 0..bytes.len() { + let mut prefix = bytes[..end].to_vec(); + if end >= 24 { + set_wide(&mut prefix, 16, end as u64); + } + assert!(decode(&prefix).is_err(), "truncated prefix {end}"); + } + let mut trailing = bytes.clone(); + trailing.push(0); + let length = trailing.len() as u64; + set_wide(&mut trailing, 16, length); + assert_eq!(decode(&trailing), Err(DecodeError::Trailing(1))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, 16, u64::MAX); + assert_eq!( + decode(&wrong), + Err(DecodeError::Length { + declared: u64::MAX, + actual: bytes.len() + }) + ); + } + #[test] + fn resource_report_rejects_oversized_storage_before_copying_or_allocating_messages() { + assert_eq!(decode(&vec![0; MAX_REPORT + 1]), Err(DecodeError::TooLarge)); + for (offset, value, field, maximum) in [ + (24, 65, "attempts", 64), + (28, 8193, "events", 8192), + (136, 65537, "ordinary_message", 65536), + (140, 65537, "resource_message", 65536), + ] { + let mut bytes = fixture(); + set_word(&mut bytes, offset, value); + assert_eq!( + decode(&bytes), + Err(DecodeError::Limit { + field, + value: u64::from(value), + maximum + }) + ); + } + let mut bytes = fixture(); + set_wide(&mut bytes, 32, 4097); + assert_eq!( + decode(&bytes), + Err(DecodeError::Limit { + field: "script_remaining", + value: 4097, + maximum: 4096 + }) + ); + } + #[test] + fn resource_report_rejects_reserved_unknown_kinds_and_nonboolean_flags() { + let bytes = fixture(); + let first_event = FIRST_PROVIDER_EVENT; + let mut wrong = bytes.clone(); + wrong[0] = b'X'; + assert_eq!(decode(&wrong), Err(DecodeError::Magic)); + let mut wrong = bytes.clone(); + set_word(&mut wrong, 8, 1); + assert_eq!(decode(&wrong), Err(DecodeError::Version(1))); + for (offset, field) in [ + (12, "header"), + (60, "completion"), + (132, "counts"), + (first_event + 20, "event"), + ] { + let mut wrong = bytes.clone(); + set_word(&mut wrong, offset, 9); + assert_eq!( + decode(&wrong), + Err(DecodeError::Reserved { field, value: 9 }) + ); + } + let mut wrong = bytes.clone(); + set_word(&mut wrong, 56, 4); + assert_eq!(decode(&wrong), Err(DecodeError::Channel(4))); + let mut wrong = bytes.clone(); + set_word(&mut wrong, first_event + 16, 6); + assert_eq!(decode(&wrong), Err(DecodeError::EventKind(6))); + let mut wrong = bytes.clone(); + set_word(&mut wrong, 128, 2); + assert_eq!( + decode(&wrong), + Err(DecodeError::Flag { + field: "ordinary_empty", + value: 2 + }) + ); + let mut wrong = bytes.clone(); + let created = wrong.len() - 12; + set_word(&mut wrong, created, 2); + assert_eq!( + decode(&wrong), + Err(DecodeError::Flag { + field: "created", + value: 2 + }) + ); + } + #[test] + fn resource_report_rejects_nonmonotonic_identities_and_impossible_event_prefixes() { + let bytes = fixture(); + let second_attempt = 162; + let first_event = FIRST_PROVIDER_EVENT; + let mut wrong = bytes.clone(); + set_wide(&mut wrong, second_attempt, 41); + assert_eq!(decode(&wrong), Err(DecodeError::AttemptOrder(1))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, first_event, 0); + assert_eq!(decode(&wrong), Err(DecodeError::EventOrder(0))); + let mut wrong = bytes.clone(); + set_wide(&mut wrong, first_event + 24, 1); + assert_eq!(decode(&wrong), Err(DecodeError::EventOrder(1))); + for end in [2, u64::MAX] { + let mut wrong = bytes.clone(); + set_wide(&mut wrong, 144, end); + assert!(matches!(decode(&wrong), Err(DecodeError::EventEnd { .. }))); + } + let mut wrong = bytes.clone(); + set_wide(&mut wrong, second_attempt + 104, 0); + assert_eq!( + decode(&wrong), + Err(DecodeError::EventEnd { attempt: 1, end: 0 }) + ); + } + #[test] + fn resource_report_never_masks_live_custody_cleanup_panic_or_teardown_failure() { + let mut bytes = fixture(); + set_wide(&mut bytes, 32, 0); + let mut report = decode(&bytes).unwrap(); + assert!(report.observations_ready_for_acceptance()); + assert!(!report.all_attempts_succeeded()); + report.attempts[0].counts.owners = 1; + assert!(!report.resources_retired() && !report.observations_ready_for_acceptance()); + for index in 0..8 { + let mut live = fixture(); + set_wide(&mut live, 64 + index * 8, 17); + let report = decode(&live).unwrap(); + assert!(!report.resources_retired(), "live count {index}"); + } + let mut unknown_end = fixture(); + set_wide(&mut unknown_end, 162 + 104, 1); + let report = decode(&unknown_end).unwrap(); + assert!(!report.events_all_assigned()); + let mut panic = fixture(); + set_word(&mut panic, 48, 0); + set_word(&mut panic, 56, 2); + let report = decode(&panic).unwrap(); + assert_eq!(report.attempts[0].completion.body_status, 0); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::HostPanic + ); + assert!(!report.all_attempts_succeeded()); + let mut bytes = fixture(); + set_word(&mut bytes, 52, 37); + set_word(&mut bytes, 56, 3); + let report = decode(&bytes).unwrap(); + assert_eq!(report.attempts[0].completion.cleanup_status, 37); + assert_eq!(report.attempts[0].completion.body_status, 71); + assert_eq!( + report.attempts[0].completion.selected_channel, + FailureChannel::Cleanup + ); + assert!(!report.cleanup_succeeded()); + assert!(report.resources_retired()); + let mut wrong = fixture(); + set_word(&mut wrong, 52, 37); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + set_word(&mut wrong, 56, 3); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + set_word(&mut wrong, 56, 0); + assert_eq!(decode(&wrong), Err(DecodeError::Completion(0))); + let mut wrong = fixture(); + let footer = wrong.len() - 16; + set_word(&mut wrong, footer + 8, 2); + assert_eq!(decode(&wrong), Err(DecodeError::Teardown)); + let mut failed = fixture(); + let footer = failed.len() - 16; + set_word(&mut failed, footer, 13); + set_word(&mut failed, footer + 12, 14); + let report = decode(&failed).unwrap(); + assert_eq!((report.destroy_status, report.write_status), (13, 14)); + assert!(!report.teardown_succeeded()); + } + #[test] + fn resource_report_v2_retains_real_boundaries_raw_context_and_parameter_bits() { + let mut bytes = fixture(); + set_wide(&mut bytes, 32, 0); + let report = decode(&bytes).unwrap(); + assert!(report.observations_ready_for_acceptance()); + assert_eq!(report.observations.final_sequence, 8); + assert_eq!(report.observations.context_bytes, 6); + assert_eq!( + report.observations.attempts[0].identity, + AttemptIdentity { id: 41, ordinal: 0 } + ); + assert_eq!( + report.observations.history[1].context.as_deref(), + Some(&[0, 255][..]) + ); + let HistoryKind::CallEnter { call, parameters } = &report.observations.history[2].kind + else { + panic!("actual entered call") + }; + assert_eq!((call.call, call.scope, call.function), (11, 12, 7)); + assert_eq!( + parameters[0].value, + ParameterValue::Int64 { + bits: i64::MIN as u64 + } + ); + assert_eq!(report.observations.history[4].context, None); + } + #[test] + fn resource_report_v2_rejects_closed_tag_limit_and_legacy_corruptions_after_valid_baseline() { + let baseline = fixture(); + decode(&baseline).unwrap(); + let at = observation_offsets(); + let mut legacy = baseline.clone(); + legacy[..8].copy_from_slice(b"JTRRP001"); + assert_eq!(decode(&legacy), Err(DecodeError::Magic)); + for (offset, value) in [ + (at.start + 40, 64), + (at.start + 44, 65), + (at.start + 48, 8193), + (at.rows[0] + 12, 2), + (at.history[0] + 8, 99), + (at.history[0] + 12, 2), + (at.history[0] + 28, 2), + (at.parameter_tag, 2), + ] { + let mut changed = baseline.clone(); + set_word(&mut changed, offset, value); + assert!(decode(&changed).is_err(), "closed field {offset}"); + } + let mut wrong_function = baseline.clone(); + set_word(&mut wrong_function, at.history[3] + 54, 99); + assert!(decode(&wrong_function).is_err()); + let mut context_limit = baseline.clone(); + set_wide(&mut context_limit, at.start + 52, MAX_CONTEXT_BYTES + 1); + assert!(decode(&context_limit).is_err()); + } + #[test] + fn resource_report_v2_rejects_counter_boundary_and_exact_call_join_corruption() { + let baseline = fixture(); + decode(&baseline).unwrap(); + let at = observation_offsets(); + for (offset, value) in [ + (at.start + 8, 1), // acquire subtype exceeds all-hook + (at.rows[0] + 24, 1), // begin counter decreases at End + (at.rows[0] + 72, 1), // duplicate Begin/End sequence + (at.rows[1], 41), // foreign/duplicate attempt identity + (at.history[1], 1), // noncontiguous sequence + (at.history[3] + 16, 44), // completion transported to another attempt + (at.history[3] + 38, 77), // completed call handle differs + (at.history[3] + 46, 77), // completed scope differs + (at.history[3] + 74, 4), // completion does not reference a prior CallEnter + ] { + let mut changed = baseline.clone(); + set_wide(&mut changed, offset, value); + assert!(decode(&changed).is_err(), "identity/counter {offset}"); + } + } + #[test] + fn resource_report_v2_preserves_incomplete_and_unobserved_without_inventing_zero() { + let mut baseline = fixture(); + set_wide(&mut baseline, 32, 0); + decode(&baseline).unwrap(); + let at = observation_offsets(); + for flag in [1, 2, 4, 8, 16, 32] { + let mut changed = baseline.clone(); + set_word(&mut changed, at.start + 40, flag); + let report = decode(&changed).unwrap(); + assert_eq!(report.observations.flags, flag); + assert!(!report.observations_ready_for_acceptance()); + } + // A known counter1 is retained as1, including an incomplete report. + let mut changed = baseline.clone(); + set_wide(&mut changed, at.start, 1); + set_word(&mut changed, at.start + 40, 8); + let report = decode(&changed).unwrap(); + assert_eq!(report.observations.counters.values[0], 1); + assert!(!report.observations_ready_for_acceptance()); + // Explicit missing sequence consumes no invented sequence word. + let mut unknown = baseline.clone(); + set_word(&mut unknown, at.start + 40, 8); + set_word(&mut unknown, at.rows[0] + 12, 0); + unknown.drain(at.rows[0] + 16..at.rows[0] + 24); + let length = unknown.len() as u64; + set_wide(&mut unknown, 16, length); + let report = decode(&unknown).unwrap(); + assert_eq!(report.observations.attempts[0].begin.sequence, None); + assert!(!report.observations_ready_for_acceptance()); + set_word(&mut unknown, at.start + 40, 0); + assert!( + decode(&unknown).is_err(), + "unknown boundary cannot claim complete telemetry" + ); + let mut unobserved = baseline.clone(); + set_word(&mut unobserved, at.parameter_tag, 0); + unobserved.drain(at.parameter_tag + 4..at.parameter_tag + 12); + let length = unobserved.len() as u64; + set_wide(&mut unobserved, 16, length); + let report = decode(&unobserved).unwrap(); + let HistoryKind::CallEnter { parameters, .. } = &report.observations.history[2].kind else { + panic!("call") + }; + assert_eq!(parameters[0].value, ParameterValue::Unobserved); + } + + #[test] + fn resource_report_v2_preserves_empty_context_and_refuses_missing_completion() { + let baseline = fixture(); + decode(&baseline).unwrap(); + let at = observation_offsets(); + let mut empty = baseline.clone(); + for index in [3, 2, 1] { + set_word(&mut empty, at.history[index] + 32, 0); + empty.drain(at.history[index] + 36..at.history[index] + 38); + } + set_wide(&mut empty, at.start + 52, 0); + let length = empty.len() as u64; + set_wide(&mut empty, 16, length); + let report = decode(&empty).unwrap(); + assert_eq!( + report.observations.history[1].context.as_deref(), + Some(&[][..]) + ); + assert_eq!(report.observations.history[4].context, None); + let mut report = decode(&baseline).unwrap(); + report.observations.history[3].kind = HistoryKind::Set; + assert!( + report.observations.validate(&report.attempts).is_err(), + "an entered call must actually complete before End" + ); + } + + fn nested_clean_report() -> Report { + let mut value = decode(&fixture()).unwrap(); + let identity = AttemptIdentity { id: 41, ordinal: 0 }; + let second = AttemptIdentity { id: 44, ordinal: 1 }; + let parent = CallIdentity { + call: 11, + scope: 12, + function: 7, + signature: 8, + template: 9, + operation: 10, + }; + let child = CallIdentity { + call: 21, + scope: 22, + function: 17, + signature: 18, + template: 19, + operation: 20, + }; + let kinds = [ + (identity, None, HistoryKind::Begin), + (identity, Some(b"trial".to_vec()), HistoryKind::Set), + ( + identity, + Some(b"trial".to_vec()), + HistoryKind::CallEnter { + call: parent, + parameters: vec![], + }, + ), + ( + identity, + Some(b"trial".to_vec()), + HistoryKind::CallEnter { + call: child, + parameters: vec![], + }, + ), + ( + identity, + Some(b"trial".to_vec()), + HistoryKind::CallComplete { + call: child, + entered_sequence: Some(4), + body_status: 0, + failure_status: None, + }, + ), + ( + identity, + Some(b"trial".to_vec()), + HistoryKind::CallComplete { + call: parent, + entered_sequence: Some(3), + body_status: 0, + failure_status: None, + }, + ), + (identity, None, HistoryKind::Clear), + (identity, None, HistoryKind::End), + (second, None, HistoryKind::Begin), + (second, None, HistoryKind::End), + ]; + value.observations.history = kinds + .into_iter() + .enumerate() + .map(|(index, (identity, context, kind))| HistoryEvent { + sequence: index as u64 + 1, + channel: Channel::Attempt(identity), + context, + kind, + }) + .collect(); + value.observations.context_bytes = 25; + value.observations.final_sequence = 10; + value.observations.attempts[0] + .end + .as_mut() + .unwrap() + .sequence = Some(8); + value.observations.attempts[1].begin.sequence = Some(9); + value.observations.attempts[1] + .end + .as_mut() + .unwrap() + .sequence = Some(10); + value.observations.validate(&value.attempts).unwrap(); + value + } + #[test] + fn resource_report_v2_actual_success_none_and_nested_context_lifo_are_checked() { + let baseline = nested_clean_report(); + assert!(matches!( + baseline.observations.history[4].kind, + HistoryKind::CallComplete { + failure_status: None, + body_status: 0, + .. + } + )); + for mutation in 0..8 { + let mut changed = nested_clean_report(); + match mutation { + 0 => { + let left = std::mem::replace( + &mut changed.observations.history[4].kind, + HistoryKind::Clear, + ); + let right = std::mem::replace(&mut changed.observations.history[5].kind, left); + changed.observations.history[4].kind = right; + } + 1 => { + changed.observations.history[1].kind = HistoryKind::Clear; + changed.observations.history[1].context = None; + } + 2 => changed.observations.history[2].context = Some(b"wrong".to_vec()), + 3 => changed.observations.history[1].channel = Channel::Session, + 4 => { + if let HistoryKind::CallComplete { failure_status, .. } = + &mut changed.observations.history[4].kind + { + *failure_status = Some(0); + } + } + 5 => { + if let HistoryKind::CallComplete { body_status, .. } = + &mut changed.observations.history[4].kind + { + *body_status = 71; + } + } + 6 => changed.observations.history[4].context = Some(b"wrong".to_vec()), + 7 => changed.observations.history[7].context = Some(vec![]), + _ => unreachable!(), + } + assert!( + changed.observations.validate(&changed.attempts).is_err(), + "actual completion/context mutation {mutation}" + ); + } + let mut cleared = nested_clean_report(); + let clear = cleared.observations.history.remove(6); + cleared.observations.history.insert(4, clear); + for (index, event) in cleared.observations.history.iter_mut().enumerate() { + event.sequence = index as u64 + 1; + } + cleared.observations.history[5].context = None; + cleared.observations.history[6].context = None; + cleared.observations.context_bytes = 15; + cleared.observations.validate(&cleared.attempts).unwrap(); + let mut failed = nested_clean_report(); + if let HistoryKind::CallComplete { failure_status, .. } = + &mut failed.observations.history[4].kind + { + *failure_status = Some(71); + } + failed.observations.validate(&failed.attempts).unwrap(); + } +} diff --git a/crates/jett_driver/tests/native_conformance/resource_sum_replacement_cases.rs b/crates/jett_driver/tests/native_conformance/resource_sum_replacement_cases.rs new file mode 100644 index 00000000..34c953a5 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/resource_sum_replacement_cases.rs @@ -0,0 +1,339 @@ +//! New original Sources/scripts; no row is runtime authority or accepted evidence. +use super::cases::{Case, Event, ReferenceOutcome, Script}; + +const ORIGINAL: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/original_optional_assignment.jett" +); +const OPTIONAL: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/optional_transitions.jett" +); +const RESULT: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/result_transitions.jett" +); +const PRIMITIVE: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/primitive_failure.jett" +); +const LOOP: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_optional.jett" +); +const CONDITIONAL: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/conditional_result.jett" +); + +const fn clean( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::Clean, + body: 0, + cleanup: 0, + channel: 0, + ordinary_message: b"", + resource_message: b"", + exit: 0, + } +} +const fn cleanup_panic( + name: &'static str, + source: &'static str, + script: &'static [Script], + events: &'static [Event], +) -> Case { + Case { + name, + source, + script, + events, + reference: ReferenceOutcome::CleanupPanic("selected test resource cleanup panic"), + body: 255, + cleanup: 255, + channel: 3, + ordinary_message: b"", + resource_message: b"native Resource cleanup failed", + exit: 71, + } +} +pub(super) const CASES: &[Case] = &[ + clean("sum_original_none_ready", ORIGINAL, &[], &[]), + clean( + "sum_optional_ready", + OPTIONAL, + &[ + Script::Construct(2501), + Script::Construct(2502), + Script::Construct(2503), + Script::Borrow(2503, 7), + ], + &[ + Event::Constructed(2501), + Event::Finalized(2501), + Event::Constructed(2502), + Event::Constructed(2503), + Event::Finalized(2502), + Event::Borrowed(2503), + Event::Finalized(2503), + ], + ), + clean( + "sum_optional_rhs_failure", + OPTIONAL, + &[ + Script::Construct(2501), + Script::Construct(2502), + Script::ConstructFail(2503, "rhs sentinel"), + ], + &[ + Event::Constructed(2501), + Event::Finalized(2501), + Event::Constructed(2502), + Event::ConstructionFailed(2503), + Event::Finalized(2502), + ], + ), + cleanup_panic( + "sum_optional_old_to_none_cleanup_failure", + OPTIONAL, + &[Script::FinalizerPanic(2501)], + &[Event::Constructed(2501), Event::Finalized(2501)], + ), + cleanup_panic( + "sum_optional_old_to_some_cleanup_failure", + OPTIONAL, + &[ + Script::Construct(2501), + Script::FinalizerPanic(2502), + Script::Construct(2503), + ], + &[ + Event::Constructed(2501), + Event::Finalized(2501), + Event::Constructed(2502), + Event::Constructed(2503), + Event::Finalized(2502), + Event::Finalized(2503), + ], + ), + cleanup_panic( + "sum_optional_both_cleanup_failure", + OPTIONAL, + &[ + Script::Construct(2501), + Script::FinalizerPanic(2502), + Script::FinalizerPanic(2503), + ], + &[ + Event::Constructed(2501), + Event::Finalized(2501), + Event::Constructed(2502), + Event::Constructed(2503), + Event::Finalized(2502), + Event::Finalized(2503), + ], + ), + cleanup_panic( + "sum_optional_incoming_late_cleanup_failure", + OPTIONAL, + &[ + Script::Construct(2501), + Script::Construct(2502), + Script::FinalizerPanic(2503), + Script::Borrow(2503, 7), + ], + &[ + Event::Constructed(2501), + Event::Finalized(2501), + Event::Constructed(2502), + Event::Constructed(2503), + Event::Finalized(2502), + Event::Borrowed(2503), + Event::Finalized(2503), + ], + ), + clean( + "sum_result_ready", + RESULT, + &[ + Script::Construct(2601), + Script::Construct(2602), + Script::Construct(2603), + Script::Borrow(2603, 7), + ], + &[ + Event::Constructed(2601), + Event::Finalized(2601), + Event::Constructed(2602), + Event::Constructed(2603), + Event::Finalized(2602), + Event::Borrowed(2603), + Event::Finalized(2603), + ], + ), + clean( + "sum_result_rhs_failure", + RESULT, + &[ + Script::Construct(2601), + Script::Construct(2602), + Script::ConstructFail(2603, "rhs sentinel"), + ], + &[ + Event::Constructed(2601), + Event::Finalized(2601), + Event::Constructed(2602), + Event::ConstructionFailed(2603), + Event::Finalized(2602), + ], + ), + cleanup_panic( + "sum_result_old_to_fail_cleanup_failure", + RESULT, + &[Script::FinalizerPanic(2601)], + &[Event::Constructed(2601), Event::Finalized(2601)], + ), + cleanup_panic( + "sum_result_old_to_ok_cleanup_failure", + RESULT, + &[ + Script::Construct(2601), + Script::FinalizerPanic(2602), + Script::Construct(2603), + ], + &[ + Event::Constructed(2601), + Event::Finalized(2601), + Event::Constructed(2602), + Event::Constructed(2603), + Event::Finalized(2602), + Event::Finalized(2603), + ], + ), + cleanup_panic( + "sum_result_both_cleanup_failure", + RESULT, + &[ + Script::Construct(2601), + Script::FinalizerPanic(2602), + Script::FinalizerPanic(2603), + ], + &[ + Event::Constructed(2601), + Event::Finalized(2601), + Event::Constructed(2602), + Event::Constructed(2603), + Event::Finalized(2602), + Event::Finalized(2603), + ], + ), + cleanup_panic( + "sum_result_incoming_late_cleanup_failure", + RESULT, + &[ + Script::Construct(2601), + Script::Construct(2602), + Script::FinalizerPanic(2603), + Script::Borrow(2603, 7), + ], + &[ + Event::Constructed(2601), + Event::Finalized(2601), + Event::Constructed(2602), + Event::Constructed(2603), + Event::Finalized(2602), + Event::Borrowed(2603), + Event::Finalized(2603), + ], + ), + clean( + "sum_primitive_ready", + PRIMITIVE, + &[Script::Construct(2701)], + &[Event::Constructed(2701), Event::Finalized(2701)], + ), + clean( + "sum_loop_ready", + LOOP, + &[ + Script::Construct(2801), + Script::Construct(2802), + Script::Borrow(2802, 7), + ], + &[ + Event::Constructed(2801), + Event::Finalized(2801), + Event::Constructed(2802), + Event::Borrowed(2802), + Event::Finalized(2802), + ], + ), + clean( + "sum_conditional_result_ready", + CONDITIONAL, + &[ + Script::Construct(2901), + Script::Construct(2902), + Script::Borrow(2902, 7), + ], + &[ + Event::Constructed(2901), + Event::Constructed(2902), + Event::Finalized(2901), + Event::Borrowed(2902), + Event::Finalized(2902), + ], + ), + clean( + "sum_conditional_result_fail_ok", + CONDITIONAL, + &[ + Script::ConstructFail(2901, "first"), + Script::Construct(2902), + Script::Borrow(2902, 7), + ], + &[ + Event::ConstructionFailed(2901), + Event::Constructed(2902), + Event::Borrowed(2902), + Event::Finalized(2902), + ], + ), + clean( + "sum_conditional_result_ok_fail", + CONDITIONAL, + &[ + Script::Construct(2901), + Script::ConstructFail(2902, "second"), + ], + &[ + Event::Constructed(2901), + Event::ConstructionFailed(2902), + Event::Finalized(2901), + ], + ), + clean( + "sum_conditional_result_fail_fail", + CONDITIONAL, + &[ + Script::ConstructFail(2901, "first"), + Script::ConstructFail(2902, "second"), + ], + &[ + Event::ConstructionFailed(2901), + Event::ConstructionFailed(2902), + ], + ), +]; +pub(super) fn ready_for(first: &Case) -> &'static Case { + CASES + .iter() + .find(|case| { + case.source.as_bytes() == first.source.as_bytes() && case.name.ends_with("_ready") + }) + .unwrap() +} diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views.rs b/crates/jett_driver/tests/native_conformance/scoped_call_views.rs new file mode 100644 index 00000000..4c24b127 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views.rs @@ -0,0 +1,207 @@ +use super::run_bounded; +use super::suite_options::launcher_for_options; +use std::fs; + +#[derive(Clone, Copy)] +enum ExpectedOutcome { + Success(&'static str), + RuntimeFailure { + stdout: &'static str, + message: &'static str, + }, +} + +fn run_case(name: &str, source_text: &str, expected: ExpectedOutcome) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, terminal_stderr) = match expected { + ExpectedOutcome::Success(stdout) => { + let output = jett_driver::run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + ExpectedOutcome::RuntimeFailure { stdout, message } => { + let failure = jett_driver::run_file_capture_outcome(&source) + .expect_err("expected terminal scoped-view fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = jett_driver::native::build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + terminal_stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_scoped_call_view_field_is_observed_before_the_later_handler() { + run_case( + "01_field_marker", + include_str!("scoped_call_views/01_field_marker.jett"), + ExpectedOutcome::Success("before:ready\nlater\nvalue:7:3\nowner:value:7\n"), + ); +} + +#[test] +fn native_scoped_call_view_pending_field_fails_before_the_later_marker() { + run_case( + "02_pending_field", + include_str!("scoped_call_views/02_pending_field.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:pending\n", + message: "runtime error: field access is not supported on pending(pending(app.Packet(member: 7)))", + }, + ); +} + +#[test] +fn native_scoped_call_view_declared_named_alias_control_preserves_owner() { + run_case( + "03_declared_alias", + include_str!("scoped_call_views/03_declared_alias.jett"), + ExpectedOutcome::Success("before:alias\nlater\nvalue:7:3\nowner:value:7\n"), + ); +} + +#[test] +fn native_scoped_call_view_ends_before_post_call_owner_consumption() { + run_case( + "04_post_call_consume", + include_str!("scoped_call_views/04_post_call_consume.jett"), + ExpectedOutcome::Success("before:consume\nlater\nvalue:7:3\nconsumed:value:7\n"), + ); +} + +#[test] +fn native_scoped_call_view_aborted_call_ends_before_owned_return_operand() { + run_case( + "05_early_return", + include_str!("scoped_call_views/05_early_return.jett"), + ExpectedOutcome::Success( + "before:return\nearly\nreturned:value:7\nbefore:return\nanswer:value:9:4\nreturned:value:9\n", + ), + ); +} + +#[test] +fn native_scoped_call_view_mutable_field_owner_can_rebind_after_call() { + run_case( + "06_mutable_field", + include_str!("scoped_call_views/06_mutable_field.jett"), + ExpectedOutcome::Success("before:mutable\nlater\nvalue:7:3\nowner:value:9\n"), + ); +} + +#[test] +fn native_scoped_call_view_owned_field_temporary_is_evaluated_once() { + run_case( + "07_owned_field_temporary", + include_str!("scoped_call_views/07_owned_field_temporary.jett"), + ExpectedOutcome::Success( + "before:temporary\nsource:temporary\nlater\nvalue:7:3\nafter:temporary\n", + ), + ); +} + +#[test] +fn native_scoped_call_view_direct_mutable_named_owner_can_rebind_after_call() { + run_case( + "08_mutable_named", + include_str!("scoped_call_views/08_mutable_named.jett"), + ExpectedOutcome::Success("before:mutable-value\nlater\nvalue:7:3\nowner:value:9\n"), + ); +} + +#[test] +fn native_scoped_call_view_direct_owned_named_temporary_is_evaluated_once() { + run_case( + "09_owned_named_temporary", + include_str!("scoped_call_views/09_owned_named_temporary.jett"), + ExpectedOutcome::Success( + "before:temporary-value\nsource:temporary-value\nlater\nvalue:7:3\nafter:temporary-value\n", + ), + ); +} + +#[test] +fn native_scoped_call_view_indirect_field_call_ends_before_owner_consumption() { + run_case( + "10_indirect_field_call", + include_str!("scoped_call_views/10_indirect_field_call.jett"), + ExpectedOutcome::Success("before:indirect\nlater:indirect\nvalue:7:3\nconsumed:value:7\n"), + ); +} + +#[test] +fn native_scoped_call_view_capability_formal_is_borrowed_until_operation_completes() { + run_case( + "11_capability_call", + include_str!("scoped_call_views/11_capability_call.jett"), + ExpectedOutcome::Success( + "before:capability\nlater:capability\nwritten:capability\nafter:capability\n", + ), + ); +} + +#[test] +fn native_scoped_call_view_reflected_read_completes_validation_before_owner_consumption() { + run_case( + "12_reflected_read", + include_str!("scoped_call_views/12_reflected_read.jett"), + ExpectedOutcome::Success( + "before:reflection\nselector:reflection\nvalue:12\nconsumed:value:7:12\n", + ), + ); +} + +#[test] +fn native_scoped_call_view_temporary_later_failure_preserves_first_error_and_cleanup() { + run_case( + "13_owned_temporary_later_failure", + include_str!("scoped_call_views/13_owned_temporary_later_failure.jett"), + ExpectedOutcome::RuntimeFailure { + stdout: "before:cleanup\nsource:cleanup\nfailure:cleanup\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/01_field_marker.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/01_field_marker.jett new file mode 100644 index 00000000..bcc06249 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/01_field_marker.jett @@ -0,0 +1,23 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(member: 7) + optional[int64] absent = none + Stdout.write(view stdout, "before:ready\n") + string answer = report(view packet.member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Stdout.write(view stdout, "owner:{Named.name(view packet.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/02_pending_field.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/02_pending_field.jett new file mode 100644 index 00000000..c4a9b145 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/02_pending_field.jett @@ -0,0 +1,22 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + Packet packet = run run Packet(member: 7) + optional[int64] absent = none + Stdout.write(view stdout, "before:pending\n") + string answer = report(view packet.member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "after:{answer}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/03_declared_alias.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/03_declared_alias.jett new file mode 100644 index 00000000..b920c7ea --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/03_declared_alias.jett @@ -0,0 +1,24 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(member: 7) + Named borrowed = view packet.member + optional[int64] absent = none + Stdout.write(view stdout, "before:alias\n") + string answer = report(view borrowed, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Stdout.write(view stdout, "owner:{Named.name(view packet.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/04_post_call_consume.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/04_post_call_consume.jett new file mode 100644 index 00000000..200ce3e6 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/04_post_call_consume.jett @@ -0,0 +1,24 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(member: 7) + optional[int64] absent = none + Stdout.write(view stdout, "before:consume\n") + string answer = report(view packet.member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Packet consumed = packet + Stdout.write(view stdout, "consumed:{Named.name(view consumed.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/05_early_return.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/05_early_return.jett new file mode 100644 index 00000000..7ddf8274 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/05_early_return.jett @@ -0,0 +1,24 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function observe(packet: Packet, incoming: optional[int64], view stdout: Stdout) returns Packet: + Stdout.write(view stdout, "before:return\n") + string answer = report(view packet.member, incoming handle: + Stdout.write(view stdout, "early\n") + return packet + ) + Stdout.write(view stdout, "answer:{answer}\n") + return packet +function main(stdout: Stdout) returns nothing: + Packet early = observe(Packet(member: 7), none, view stdout) + Stdout.write(view stdout, "returned:{Named.name(view early.member)}\n") + Packet ready = observe(Packet(member: 9), some(4), view stdout) + Stdout.write(view stdout, "returned:{Named.name(view ready.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/06_mutable_field.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/06_mutable_field.jett new file mode 100644 index 00000000..4aef9737 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/06_mutable_field.jett @@ -0,0 +1,24 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + mutable Packet packet = Packet(member: 7) + optional[int64] absent = none + Stdout.write(view stdout, "before:mutable\n") + string answer = report(view packet.member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + packet = Packet(member: 9) + Stdout.write(view stdout, "owner:{Named.name(view packet.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/07_owned_field_temporary.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/07_owned_field_temporary.jett new file mode 100644 index 00000000..270f4f6f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/07_owned_field_temporary.jett @@ -0,0 +1,25 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function make_packet(view stdout: Stdout) returns Packet: + Stdout.write(view stdout, "source:temporary\n") + return Packet(member: 7) +function main(stdout: Stdout) returns nothing: + optional[int64] absent = none + Stdout.write(view stdout, "before:temporary\n") + string answer = report(view make_packet(view stdout).member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Stdout.write(view stdout, "after:temporary\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/08_mutable_named.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/08_mutable_named.jett new file mode 100644 index 00000000..9d0c2257 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/08_mutable_named.jett @@ -0,0 +1,22 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function main(stdout: Stdout) returns nothing: + mutable Named member = 7 + optional[int64] absent = none + Stdout.write(view stdout, "before:mutable-value\n") + string answer = report(view member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + member = 9 + Stdout.write(view stdout, "owner:{Named.name(view member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/09_owned_named_temporary.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/09_owned_named_temporary.jett new file mode 100644 index 00000000..f9e7d497 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/09_owned_named_temporary.jett @@ -0,0 +1,23 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later\n") + return 3 +function make_named(view stdout: Stdout) returns Named: + Stdout.write(view stdout, "source:temporary-value\n") + return 7 +function main(stdout: Stdout) returns nothing: + optional[int64] absent = none + Stdout.write(view stdout, "before:temporary-value\n") + string answer = report(view make_named(view stdout), absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Stdout.write(view stdout, "after:temporary-value\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/10_indirect_field_call.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/10_indirect_field_call.jett new file mode 100644 index 00000000..d13e3477 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/10_indirect_field_call.jett @@ -0,0 +1,25 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return "{Named.name(view item)}:{extra}" +function fallback(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "later:indirect\n") + return 3 +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(member: 7) + function(view Named, int64) returns string callback = report + optional[int64] absent = none + Stdout.write(view stdout, "before:indirect\n") + string answer = callback(view packet.member, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "{answer}\n") + Packet consumed = packet + Stdout.write(view stdout, "consumed:{Named.name(view consumed.member)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/11_capability_call.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/11_capability_call.jett new file mode 100644 index 00000000..a9721caa --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/11_capability_call.jett @@ -0,0 +1,14 @@ +namespace app +function write(view stdout: Stdout, text: string) returns nothing: + Stdout.write(view stdout, text) +function fallback(view stdout: Stdout) returns string: + Stdout.write(view stdout, "later:capability\n") + return "written:capability\n" +function main(stdout: Stdout) returns nothing: + optional[string] absent = none + Stdout.write(view stdout, "before:capability\n") + write(view stdout, absent handle: + default fallback(view stdout) + ) + Stdout.write(view stdout, "after:capability\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/12_reflected_read.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/12_reflected_read.jett new file mode 100644 index 00000000..ae4a44b1 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/12_reflected_read.jett @@ -0,0 +1,26 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +type Positive = int64 where value > 0 +struct Packet: + member: Named + count: int64 +function selected_field(view stdout: Stdout) returns TypeField: + Stdout.write(view stdout, "selector:reflection\n") + return list.get[TypeField](type.fields[Packet](), 1) handle: + return selected_field(view stdout) +function main(stdout: Stdout) returns nothing: + Packet packet = Packet(member: 7, count: 12) + optional[TypeField] absent = none + Stdout.write(view stdout, "before:reflection\n") + Positive copied = type.field_value[Packet, Positive](view packet, view(absent handle: + default selected_field(view stdout) + )) + int64 observed = coarsen copied + Stdout.write(view stdout, "value:{observed}\n") + Packet consumed = packet + Stdout.write(view stdout, "consumed:{Named.name(view consumed.member)}:{consumed.count}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/scoped_call_views/13_owned_temporary_later_failure.jett b/crates/jett_driver/tests/native_conformance/scoped_call_views/13_owned_temporary_later_failure.jett new file mode 100644 index 00000000..dca2eb06 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/scoped_call_views/13_owned_temporary_later_failure.jett @@ -0,0 +1,28 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "value:{self}" +struct Packet: + member: Named +function report(view stdout: Stdout, view item: Named, extra: int64) returns string: + Stdout.write(view stdout, "callee:cleanup\n") + return "{Named.name(view item)}:{extra}" +function make_packet(view stdout: Stdout) returns Packet: + Stdout.write(view stdout, "source:cleanup\n") + return Packet(member: 7) +function failing(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure:cleanup\n") + list[string] values = list.remove_at[string](list("owned", "other"), -1) + return list.length(view values) +function main(stdout: Stdout) returns nothing: + bytes payload = bytes.from_string("cleanup") + list[string] retained = list("owner", "kept") + optional[int64] absent = none + Stdout.write(view stdout, "before:cleanup\n") + string answer = report(view stdout, view make_packet(view stdout).member, absent handle: + default failing(view stdout) + ) + Stdout.write(view stdout, "after:{answer}:{bytes.to_hex(view payload)}:{list.length(view retained)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_bitfield_machine_constructors.rs b/crates/jett_driver/tests/native_conformance/secret_bitfield_machine_constructors.rs new file mode 100644 index 00000000..b0c85cfa --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_bitfield_machine_constructors.rs @@ -0,0 +1,386 @@ +use super::*; + +const CONSTRUCTOR_CONTROLS: &str = r#"namespace wire +export bitfield Header: + version: 4 bits + length: 8 bits +export type Classified = secret[Header] +type HeaderAlias = Header +bitfield Wide: + sequence: 64 bits +enum Protocol: + tcp = 6 + udp = 17 +bitfield network NetworkHeader: + version: 4 bits + protocol: 8 bits as Protocol +bitfield Packet: + kind: 8 bits + payload: list[uint8] +interface Named: + function name(view self: Named) returns string +struct Counter: + value: int8 +implement Named for Counter: + function name(view self: Counter) returns string: + return "record:{self.value}" +type Callback = function(int8) returns int8 +function captured(offset: int8) returns Callback: + return function(value: int8) returns int8: return value + offset +machine Session: + states: + idle + active(label: string, numbers: list[int8], owner: Named, callback: Callback) + transitions: + idle to active + active to idle +export function shown(view source: secret[Header]) returns string: + int64 version = declassify source.version + int64 length = declassify source.length + return "{version}:{length}" +function public_header() returns Header: + return Header(version: 15, length: 255) +function classified() returns Classified: + return Header(version: 7, length: 128) +function generic_header[T](unused: T) returns secret[Header]: + return Header(version: 15, length: 255) +function nested_header() returns string: + secret[secret[Header]] nested = Header(version: 15, length: 255) + secret[Header] once = declassify nested + return shown(view once) +export function constructor_report() returns string: + secret[Header] zero = Header(version: 0, length: 0) + secret[HeaderAlias] high = Header(version: 15, length: 255) + Classified returned = classified() + secret[Header] generic = generic_header[uint8](7) + function() returns secret[Header] maker = function() returns secret[Header]: return Header(version: 7, length: 255) + secret[Header] inline = maker() + secret[Header] pending = run run (Header(version: 9, length: 255)) + secret[Header] once = join pending handle error: + return error + secret[Header] ready = join once handle error: + return error + secret[Header] baked = comptime Header(version: 15, length: 255) + Header original = public_header() + secret[Header] lifted = clone original + secret[Header] producer = public_header() + return "{shown(view zero)}|{shown(view high)}|{shown(view returned)}|{shown(view generic)}|{shown(view inline)}|{shown(view ready)}|{shown(view baked)}|{shown(view lifted)}|{shown(view producer)}|{nested_header()}" +function unwrap_attempt(candidate: secret[result[Header, string]]) returns string: + result[Header, string] exposed = declassify candidate + Header ready = exposed handle error: + return error + secret[Header] hidden = ready + return shown(view hidden) +export function checked(version: int64, length: int64) returns string: + secret[result[Header, string]] attempt = Header(version: version, length: length) + return unwrap_attempt(attempt) +function contextual_handle(version: int64) returns string: + secret[Header] hidden = Header(version: version, length: 255) handle error: + return error + return shown(view hidden) +function nested_attempt() returns string: + secret[secret[result[Header, string]]] nested = Header(version: (15), length: (255)) + secret[result[Header, string]] once = declassify nested + return unwrap_attempt(once) +function baked_attempt() returns string: + secret[result[Header, string]] attempt = comptime Header(version: (15), length: 255) + return unwrap_attempt(attempt) +export function validation_report() returns string: + return "{checked(0, 0)}|{checked(15, 255)}|{checked(16, 255)}|{checked(-1, 255)}|{checked(15, 256)}|{contextual_handle(15)}|{nested_attempt()}|{baked_attempt()}" +function wide_checked(value: uint64) returns string: + secret[result[Wide, string]] attempt = Wide(sequence: value) + result[Wide, string] exposed = declassify attempt + Wide ready = exposed handle error: + return error + return "{ready.sequence}" +export function wide_report() returns string: + secret[Wide] wide = Wide(sequence: 18446744073709551615) + uint64 sequence = declassify wide.sequence + secret[NetworkHeader] hidden_network = NetworkHeader(version: 15, protocol: Protocol.tcp) + Protocol protocol = declassify hidden_network.protocol + return "{sequence}:{wide_checked(18446744073709551615)}:{protocol == Protocol.tcp}" +function payload_copy() returns secret[list[uint8]]: + secret[Packet] packet = Packet(kind: 255, payload: list(0, 127, 255)) + return packet.payload +export function payload_report() returns string: + list[uint8] original = list(0, 127, 255) + secret[Packet] source = Packet(kind: 255, payload: clone original) + secret[Packet] duplicate = clone source + secret[list[uint8]] escaped = payload_copy() + mutable list[uint8] independent = declassify escaped + independent = list.append[uint8](independent, 9) + uint8 last = list.get[uint8](view independent, 2) handle: + return "missing item" + list[uint8] source_items = declassify source.payload + list[uint8] duplicate_items = declassify duplicate.payload + return "{list.length(view original)}:{list.length(view independent)}:{last}:{list.length(view source_items)}:{list.length(view duplicate_items)}" +function machine_summary(view source: secret[Session at active]) returns string: + string label = declassify source.label + list[int8] numbers = declassify source.numbers + Named owner = declassify source.owner + Callback callback = declassify source.callback + return "{label}:{list.length(view numbers)}:{Named.name(view owner)}:{callback(4)}" +function machine_return(value: int8) returns secret[Session at active]: + return Session(active, "Returned", list(-128, 127), Counter(value: value), captured(value)) +function generic_machine[T](unused: T) returns secret[Session at active]: + return Session(active, "Generic", list(-128, 127), Counter(value: 6), captured(6)) +function nested_machine() returns string: + secret[secret[Session at active]] nested = Session(active, "Nested", list(-128, 127), Counter(value: 7), captured(7)) + secret[Session at active] once = declassify nested + return machine_summary(view once) +function joined_machine() returns string: + secret[Session at active] pending = run run Session(active, "Joined", list(-128, 127), Counter(value: 10), captured(10)) + secret[Session at active] once = join pending handle error: + return error + secret[Session at active] ready = join once handle error: + return error + return machine_summary(view ready) +function idle_machine() returns bool: + secret[Session at idle] hidden = Session(idle) + Session at idle ready = declassify hidden + return ready at idle +export function machine_report() returns string: + secret[Session at active] direct = Session(active, "Ada-λ🙂", list(-128, 127), Counter(value: 7), captured(7)) + secret[Session at active] returned = machine_return(8) + function() returns secret[Session at active] maker = function() returns secret[Session at active]: + return Session(active, "Inline", list(-128, 127), Counter(value: 9), captured(9)) + secret[Session at active] inline = maker() + secret[Session at active] generic = generic_machine[bool](true) + return "{machine_summary(view direct)}|{machine_summary(view returned)}|{machine_summary(view inline)}|{machine_summary(view generic)}|{nested_machine()}|{joined_machine()}|{idle_machine()}" +export function baked_report() returns string: + secret[Header] header = comptime Header(version: 15, length: 255) + secret[Session at active] session = comptime Session(active, "Baked", list(-128, 127), Counter(value: 7), captured(7)) + return "{shown(view header)}|{baked_attempt()}|{machine_summary(view session)}" +namespace app +function alias_report() returns string: + use wire as w + secret[w.Header] hidden = w.Header(version: 7, length: 31) + return w.shown(view hidden) +function main(stdout: Stdout) returns nothing: + use wire + Stdout.write(view stdout, "constructors:{wire.constructor_report()}\n") + Stdout.write(view stdout, "validation:{wire.validation_report()}\n") + Stdout.write(view stdout, "wide:{wire.wide_report()}\n") + Stdout.write(view stdout, "payload:{wire.payload_report()}\n") + Stdout.write(view stdout, "machines:{wire.machine_report()}\n") + Stdout.write(view stdout, "alias:{alias_report()}\n") + Stdout.write(view stdout, "comptime:{wire.baked_report()}\n") +verify secret_constructors: + use wire + assert wire.constructor_report() == "0:0|15:255|7:128|15:255|7:255|9:255|15:255|15:255|15:255|15:255" + assert wire.validation_report() == "0:0|15:255|bitfield 'wire.Header' field 'version' is 4 bit(s) wide and cannot hold '16'|bitfield 'wire.Header' field 'version' is 4 bit(s) wide and cannot hold '-1'|bitfield 'wire.Header' field 'length' is 8 bit(s) wide and cannot hold '256'|15:255|15:255|15:255" + assert wire.wide_report() == "18446744073709551615:18446744073709551615:true" + assert wire.payload_report() == "3:4:255:3:3" + assert wire.machine_report() == "Ada-λ🙂:2:record:7:11|Returned:2:record:8:12|Inline:2:record:9:13|Generic:2:record:6:10|Nested:2:record:7:11|Joined:2:record:10:14|true" + assert alias_report() == "7:31" + assert wire.baked_report() == "15:255|15:255|Baked:2:record:7:11" +property secret_constructor_trials: + given high: bool + use wire + mutable int64 version = 0 + if high: + version = 15 + assert wire.checked(version, 255) == "{version}:255" + assert wire.payload_report() == "3:4:255:3:3" + assert wire.machine_report() == "Ada-λ🙂:2:record:7:11|Returned:2:record:8:12|Inline:2:record:9:13|Generic:2:record:6:10|Nested:2:record:7:11|Joined:2:record:10:14|true" +"#; + +#[test] +fn native_secret_bitfield_and_machine_constructors_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, CONSTRUCTOR_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("accepted secret bitfield values, validating results, and exact machine states"); + assert_eq!( + expected.stdout, + concat!( + "constructors:0:0|15:255|7:128|15:255|7:255|9:255|15:255|15:255|15:255|15:255\n", + "validation:0:0|15:255|bitfield 'wire.Header' field 'version' is 4 bit(s) wide and cannot hold '16'|bitfield 'wire.Header' field 'version' is 4 bit(s) wide and cannot hold '-1'|bitfield 'wire.Header' field 'length' is 8 bit(s) wide and cannot hold '256'|15:255|15:255|15:255\n", + "wide:18446744073709551615:18446744073709551615:true\n", + "payload:3:4:255:3:3\n", + "machines:Ada-λ🙂:2:record:7:11|Returned:2:record:8:12|Inline:2:record:9:13|Generic:2:record:6:10|Nested:2:record:7:11|Joined:2:record:10:14|true\n", + "alias:7:31\n", + "comptime:15:255|15:255|Baked:2:record:7:11\n", + ) + ); + assert!(expected.debug_events.is_empty(), "{expected:?}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_constructors_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native qualified nominal constructors"); + binaries.push(binary); + } + let verify_binary = directory.path().join("secret_constructors_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled secret nominal constructor verify suite"); + let property_binary = directory.path().join("secret_constructors_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled secret nominal constructor property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const EFFECT_DECLARATIONS: &str = r#"namespace app +bitfield Packet: + kind: 8 bits + payload: list[uint8] +machine Snapshot: + states: + active(label: string, payload: list[uint8], count: int64) + closed + transitions: + active to closed +function payload(view stdout: Stdout) returns list[uint8]: + Stdout.write(view stdout, "payload\n") + return list(0, 127, 255) +function width(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "width\n") + return 256 +function failure(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "failure\n") + list[int64] owned = list(1, 2) + list[int64] failed = list.remove_at[int64](owned, -1) + return list.length(view failed) +function failing_payload(view stdout: Stdout) returns list[uint8]: + Stdout.write(view stdout, "failure\n") + list[uint8] owned = list(1, 2) + return list.remove_at[uint8](owned, -1) +function label(view stdout: Stdout) returns string: + Stdout.write(view stdout, "label\n") + return "Ada" +function later(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "must not run\n") + return 7 +function handled_count(view stdout: Stdout, candidate: optional[int64]) returns int64: + return candidate handle: + Stdout.write(view stdout, "fallback\n") + default 7 +"#; + +fn execute_effect_case( + name: &str, + statements: &str, + expected_stdout: &str, + terminal_error: Option<&str>, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + format!( + "{EFFECT_DECLARATIONS}function main(stdout: Stdout) returns nothing:\n bytes earlier = bytes.from_string(\"retained\")\n list[string] retained = list(\"earlier\", \"owner\")\n Stdout.write(view stdout, \"before\\n\")\n{statements} Stdout.write(view stdout, \"after:{{bytes.to_hex(view earlier)}}:{{list.length(view retained)}}\\n\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source); + match (expected, terminal_error) { + (Ok(expected), None) => { + assert_eq!(expected.stdout, expected_stdout, "{name}"); + assert!(expected.debug_events.is_empty(), "{name}"); + } + (Err(expected), Some(message)) => { + assert_eq!(expected.message, message, "{name}"); + assert_eq!(expected.output.stdout, expected_stdout, "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + } + (expected, error) => panic!("{name}: expected error {error:?}, got {expected:?}"), + } + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(if terminal_error.is_some() { 71 } else { 0 }), + "{name}: {actual:?}" + ); + assert_eq!(actual.stdout, expected_stdout.as_bytes(), "{name}"); + let stderr = terminal_error.map_or(String::new(), |error| format!("{error}\n")); + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_secret_constructor_validation_observes_all_fields_in_lexical_order() { + for (name, constructor, ordering) in [ + ( + "payload_first", + "Packet(payload: payload(view stdout), kind: width(view stdout))", + "payload\nwidth\n", + ), + ( + "width_first", + "Packet(kind: width(view stdout), payload: payload(view stdout))", + "width\npayload\n", + ), + ] { + let statements = format!( + " secret[result[Packet, string]] attempt = {constructor}\n result[Packet, string] exposed = declassify attempt\n Packet checked = exposed handle error:\n Stdout.write(view stdout, \"handled:{{error}}\\n\")\n default Packet(kind: 255, payload: list(9))\n Stdout.write(view stdout, \"ready:{{checked.kind}}:{{list.length(view checked.payload)}}\\n\")\n" + ); + let stdout = format!( + "before\n{ordering}handled:bitfield 'app.Packet' field 'kind' is 8 bit(s) wide and cannot hold '256'\nready:255:1\nafter:72657461696e6564:2\n" + ); + execute_effect_case(name, &statements, &stdout, None); + } + execute_effect_case( + "machine_handled_payload", + " optional[int64] candidate = none\n secret[Snapshot at active] hidden = Snapshot(active, label(view stdout), payload(view stdout), handled_count(view stdout, candidate))\n string text = declassify hidden.label\n list[uint8] values = declassify hidden.payload\n int64 count = declassify hidden.count\n Stdout.write(view stdout, \"ready:{text}:{list.length(view values)}:{count}\\n\")\n", + "before\nlabel\npayload\nfallback\nready:Ada:3:7\nafter:72657461696e6564:2\n", + None, + ); +} + +#[test] +fn native_secret_constructor_field_failures_suppress_later_effects_and_clean_up() { + let message = "runtime error: list.__remove_at: index -1 out of bounds"; + for (name, statements, stdout) in [ + ( + "bitfield_field_failure", + " secret[result[Packet, string]] attempt = Packet(payload: payload(view stdout), kind: failure(view stdout))\n result[Packet, string] exposed = declassify attempt\n Packet checked = exposed handle error:\n Stdout.write(view stdout, \"must not handle\\n\")\n default Packet(kind: 0, payload: list())\n", + "before\npayload\nfailure\n", + ), + ( + "bitfield_first_field_failure", + " secret[result[Packet, string]] attempt = Packet(kind: failure(view stdout), payload: payload(view stdout))\n result[Packet, string] exposed = declassify attempt\n Packet checked = exposed handle error:\n Stdout.write(view stdout, \"must not handle\\n\")\n default Packet(kind: 0, payload: list())\n", + "before\nfailure\n", + ), + ( + "machine_field_failure", + " secret[Snapshot at active] hidden = Snapshot(active, label(view stdout), failing_payload(view stdout), later(view stdout))\n", + "before\nlabel\nfailure\n", + ), + ] { + execute_effect_case(name, statements, stdout, Some(message)); + } +} diff --git a/crates/jett_driver/tests/native_conformance/secret_fields.rs b/crates/jett_driver/tests/native_conformance/secret_fields.rs new file mode 100644 index 00000000..5c7ff1ee --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_fields.rs @@ -0,0 +1,356 @@ +use super::*; + +const FIELD_CONTROLS: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +type Selected = Named where true +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +function selected(value: int8) returns Selected: + Named base = value + Selected chosen = base handle error: + return selected(value) + return chosen +type Callback = function(int8) returns int8 +function captured(offset: int8) returns Callback: + return function(value: int8) returns int8: return value + offset +struct Item: + number: int8 + text: string + items: list[int8] + owner: Named + callback: Callback + token: secret[string] + marker: nothing +struct Box[T]: + value: T +type Positive = secret[int8] where value > 0 +struct Nominal: + amount: Positive +bitfield Header: + version: 4 bits + length: 8 bits +machine Session: + states: + active(label: string, number: int8) + closed + transitions: + active to closed +function seed() returns Item: + return Item(number: -128, text: "Ada-λ🙂", items: list(-128, 127), owner: selected(7), callback: captured(3), token: "token", marker: nothing) +function marker_name(value: nothing) returns string: + return "nothing" +function plain_summary(view source: Item) returns string: + list[int8] items = source.items + Named owner = source.owner + Callback callback = source.callback + string token = declassify source.token + nothing marker = source.marker + return "{source.number}:{source.text}:{list.length(view items)}:{Named.name(view owner)}:{callback(4)}:{token}:{marker_name(marker)}" +function secret_summary(view source: secret[Item]) returns string: + int8 number = declassify source.number + string text = declassify source.text + list[int8] items = declassify source.items + Named owner = declassify source.owner + Callback callback = declassify source.callback + string token = declassify source.token + nothing marker = source.marker + return "{number}:{text}:{list.length(view items)}:{Named.name(view owner)}:{callback(4)}:{token}:{marker_name(marker)}" +function direct_report() returns string: + Item original = seed() + secret[Item] hidden = clone original + secret[Item] copied = clone hidden + return "{plain_summary(view original)}|{secret_summary(view hidden)}|{secret_summary(view copied)}" +function generic_field[T](view source: secret[Box[T]]) returns secret[T]: + return source.value +function generic_report() returns string: + Box[int8] plain_number = Box[int8](value: -128) + secret[Box[int8]] number = plain_number + Box[string] plain_text = Box[string](value: "generic-λ") + secret[Box[string]] text = plain_text + int8 answer = declassify generic_field[int8](view number) + string label = declassify generic_field[string](view text) + return "{answer}:{label}" +function reflected_report() returns string: + Item source = seed() + TypeField field = list.get[TypeField](type.fields[Item](), 5) handle: + return "missing metadata" + secret[string] token = type.field_value[Item, secret[string]](view source, view field) + string visible = declassify token + secret[Item] hidden = clone source + string direct = declassify hidden.token + return "{visible}:{direct}:{plain_summary(view source)}" +function nominal_report() returns string: + secret[int8] positive = 7 + Nominal source = Nominal(amount: positive) handle error: + return error + Positive ordinary = source.amount + secret[Nominal] hidden = clone source + Positive copied = hidden.amount + secret[int8] first = coarsen ordinary + secret[int8] second = coarsen copied + int8 first_value = declassify first + int8 second_value = declassify second + return "{first_value}:{second_value}" +function other_owners_report() returns string: + Header header = Header(version: 5, length: 127) + secret[Header] hidden_header = clone header + int64 version = declassify hidden_header.version + int64 length = declassify hidden_header.length + Session at active session = Session(active, "session-λ", 7) + secret[Session at active] hidden_session = clone session + string label = declassify hidden_session.label + int8 number = declassify hidden_session.number + return "{header.version}:{header.length}:{version}:{length}|{session.label}:{session.number}:{label}:{number}" +function copy_items() returns secret[list[int8]]: + Item source = seed() + secret[Item] hidden = clone source + return hidden.items +function copy_owner() returns secret[Named]: + Item source = seed() + secret[Item] hidden = clone source + return hidden.owner +function copy_callback() returns secret[Callback]: + Item source = seed() + secret[Item] hidden = clone source + return hidden.callback +function clone_report() returns string: + Item source = seed() + secret[Item] hidden = clone source + secret[list[int8]] escaped = copy_items() + mutable list[int8] independent = declassify escaped + independent = list.append[int8](independent, 9) + Named owner = declassify copy_owner() + Callback callback = declassify copy_callback() + return "{list.length(view independent)}:{list.length(view source.items)}:{Named.name(view owner)}:{callback(4)}|{secret_summary(view hidden)}|{plain_summary(view source)}" +function double_joined[T](candidate: T) returns result[T, string]: + T once = join candidate handle error: + return fail(error) + T ready = join once handle error: + return fail(error) + return ok(ready) +function pending_report() returns string: + Item source = Item(number: run run 7, text: run run "λ🙂", items: run run list(-128, 127), owner: run run selected(7), callback: run run captured(3), token: run run "token", marker: nothing) + secret[Item] hidden = source + int8 pending_number = declassify hidden.number + int8 once_number = join pending_number handle error: + return error + int8 number = join once_number handle error: + return error + string pending_text = declassify hidden.text + string once_text = join pending_text handle error: + return error + string text = join once_text handle error: + return error + list[int8] pending_items = declassify hidden.items + list[int8] items = double_joined[list[int8]](pending_items) handle error: + return error + Named pending_owner = declassify hidden.owner + Named owner = double_joined[Named](pending_owner) handle error: + return error + Callback pending_callback = declassify hidden.callback + Callback callback = double_joined[Callback](pending_callback) handle error: + return error + string pending_token = declassify hidden.token + string token = double_joined[string](pending_token) handle error: + return error + return "{pending_number}:{once_number}:{number}|{pending_text}:{once_text}:{text}|{list.length(view items)}:{Named.name(view owner)}:{callback(4)}:{token}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "direct:{direct_report()}\n") + Stdout.write(view stdout, "generic:{generic_report()}\n") + Stdout.write(view stdout, "reflected:{reflected_report()}\n") + Stdout.write(view stdout, "nominal:{nominal_report()}\n") + Stdout.write(view stdout, "owners:{other_owners_report()}\n") + Stdout.write(view stdout, "clone:{clone_report()}\n") + Stdout.write(view stdout, "pending:{pending_report()}\n") + string baked = comptime direct_report() + Stdout.write(view stdout, "comptime:{baked}\n") +verify secret_field_values: + assert direct_report() == "-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing" + assert generic_report() == "-128:generic-λ" + assert reflected_report() == "token:token:-128:Ada-λ🙂:2:selected:small:7:7:token:nothing" + assert nominal_report() == "7:7" + assert other_owners_report() == "5:127:5:127|session-λ:7:session-λ:7" + assert pending_report() == "pending(pending(7)):pending(7):7|pending(pending(λ🙂)):pending(λ🙂):λ🙂|2:selected:small:7:7:token" + assert clone_report() == "3:2:selected:small:7:7|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing" + string baked = comptime direct_report() + assert baked == direct_report() +property secret_field_trials: + given value: int8 + Box[int8] source = Box[int8](value: value) + secret[Box[int8]] hidden = source + int8 copied = declassify generic_field[int8](view hidden) + assert copied == value + assert clone_report() == "3:2:selected:small:7:7|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing" +"#; + +#[test] +fn native_secret_field_reads_preserve_qualified_values_and_owners_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, FIELD_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("secret field reads and owned field copies remain accepted"); + assert_eq!( + expected.stdout, + concat!( + "direct:-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing\n", + "generic:-128:generic-λ\n", + "reflected:token:token:-128:Ada-λ🙂:2:selected:small:7:7:token:nothing\n", + "nominal:7:7\n", + "owners:5:127:5:127|session-λ:7:session-λ:7\n", + "clone:3:2:selected:small:7:7|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing\n", + "pending:pending(pending(7)):pending(7):7|pending(pending(λ🙂)):pending(λ🙂):λ🙂|2:selected:small:7:7:token\n", + "comptime:-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing|-128:Ada-λ🙂:2:selected:small:7:7:token:nothing\n", + ) + ); + assert!(expected.debug_events.is_empty(), "{expected:?}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_fields_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native exact field qualification"); + binaries.push(binary); + } + let verify_binary = directory.path().join("secret_fields_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled secret field verify suite"); + let property_binary = directory.path().join("secret_fields_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled secret field property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const FAILURE_DECLARATIONS: &str = r#"namespace app +struct Item: + value: int64 +function failing_owner(view stdout: Stdout) returns secret[Item]: + Stdout.write(view stdout, "receiver\n") + list[int64] owned = list(1, 2) + list[int64] failed = list.remove_at[int64](owned, -1) + return Item(value: list.length(view failed)) +"#; + +fn assert_failure( + name: &str, + body: &str, + stdout: &str, + reference_message: &str, + native_message: &str, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + format!( + "{FAILURE_DECLARATIONS}function main(stdout: Stdout) returns nothing:\n bytes earlier = bytes.from_string(\"retained\")\n list[string] retained = list(\"earlier\", \"owner\")\n Stdout.write(view stdout, \"before\\n\")\n{body} Stdout.write(view stdout, \"must not run:{{bytes.to_hex(view earlier)}}:{{list.length(view retained)}}\")\n" + ), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("secret field access must preserve the first runtime failure"); + assert_eq!(expected.message, reference_message, "{name}"); + assert_eq!(expected.output.stdout, stdout, "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!("{native_message}\n").as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_pending_secret_field_children_preserve_depth_and_first_error_in_both_profiles() { + for (name, pending, join_body, stdout, message) in [ + ( + "pending_child", + "run 7", + " Stdout.write(view stdout, \"copied:{copied}\\n\")\n int64 answer = copied + 1\n", + "before\ncopied:pending(7)\n", + "runtime error: unsupported binary operation: pending(7) Add 1", + ), + ( + "partial_child", + "run run 7", + " Stdout.write(view stdout, \"copied:{copied}\\n\")\n int64 once = join copied handle error:\n return nothing\n Stdout.write(view stdout, \"joined:{once}\\n\")\n int64 answer = once + 1\n", + "before\ncopied:pending(pending(7))\njoined:pending(7)\n", + "runtime error: unsupported binary operation: pending(7) Add 1", + ), + ] { + let body = format!( + " Item original = Item(value: {pending})\n secret[Item] hidden = original\n int64 copied = declassify hidden.value\n{join_body}" + ); + assert_failure(name, &body, stdout, message, message); + } + let message = "runtime error: list.__remove_at: index -1 out of bounds"; + assert_failure( + "receiver_failure", + " int64 copied = declassify failing_owner(view stdout).value\n", + "before\nreceiver\n", + message, + message, + ); +} + +#[test] +fn native_pending_secret_field_owners_keep_typed_redaction_in_both_profiles() { + // Secret-qualified receiver failures share one typed-redaction contract. + // Pending wrappers must not expose the owner payload in either path. + let message = "runtime error: field access is not supported on [redacted]"; + for (name, setup, pending) in [ + ("pending_owner", "", "run clone original"), + ("nested_pending_owner", "", "run run clone original"), + ( + "partial_pending_owner", + " secret[Item] twice = run run clone original\n", + "join twice handle error:\n return nothing", + ), + ] { + let body = format!( + " Item original = Item(value: 7)\n{setup} secret[Item] hidden = {pending}\n int64 copied = declassify hidden.value\n" + ); + assert_failure(name, &body, "before\n", message, message); + } +} diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views.rs b/crates/jett_driver/tests/native_conformance/secret_owner_local_views.rs new file mode 100644 index 00000000..ab6dc1a7 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views.rs @@ -0,0 +1,149 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable_with_options; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +enum SecretOutcome<'a> { + Success(&'a str), + RuntimeFailure { stdout: &'a str, message: &'a str }, +} + +fn run_case(name: &str, source_text: &str, expected_stdout: &str) { + run_case_with_outcome(name, source_text, SecretOutcome::Success(expected_stdout)); +} + +fn run_case_with_outcome(name: &str, source_text: &str, expected: SecretOutcome<'_>) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, stderr) = match expected { + SecretOutcome::Success(stdout) => { + let output = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + SecretOutcome::RuntimeFailure { stdout, message } => { + let failure = run_file_capture_outcome(&source) + .expect_err("expected terminal secret receiver fixture failure"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + let stderr = format!("{}\n", failure.message); + (failure.output, 71, stderr) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_secret_owner_local_views_01_secret_owner_direct_alias_matches_reference_without_source_in_both_profiles() + { + run_case( + "01_secret_owner_direct_alias", + include_str!("secret_owner_local_views/01_secret_owner_direct_alias.jett"), + "direct:4142:2:4142\n", + ); +} + +#[test] +fn native_secret_owner_local_views_02_secret_owner_nested_forwarded_alias_matches_reference_without_source_in_both_profiles() + { + run_case( + "02_secret_owner_nested_forwarded_alias", + include_str!("secret_owner_local_views/02_secret_owner_nested_forwarded_alias.jett"), + "nested:4344:2:4344\n", + ); +} + +#[test] +fn native_secret_owner_local_views_03_secret_owner_owned_control_matches_reference_without_source_in_both_profiles() + { + run_case( + "03_secret_owner_owned_control", + include_str!("secret_owner_local_views/03_secret_owner_owned_control.jett"), + "owned:4344:2:4344\n", + ); +} + +#[test] +fn native_secret_owner_local_views_04_secret_bitfield_payload_matches_reference_without_source_in_both_profiles() + { + run_case( + "04_secret_bitfield_payload", + include_str!("secret_owner_local_views/04_secret_bitfield_payload.jett"), + "bitfield:2:3:2:4007ff\n", + ); +} + +#[test] +fn native_secret_owner_local_views_05_secret_machine_owned_root_matches_reference_without_source_in_both_profiles() + { + run_case( + "05_secret_machine_owned_root", + include_str!("secret_owner_local_views/05_secret_machine_owned_root.jett"), + "owned:4142:3:4142:2\nowner:4142:2\n", + ); +} + +#[test] +fn native_secret_owner_local_views_06_secret_machine_view_parameter_matches_reference_without_source_in_both_profiles() + { + run_case( + "06_secret_machine_view_parameter", + include_str!("secret_owner_local_views/06_secret_machine_view_parameter.jett"), + "view:4344:3:4344:2\nagain:4344:3:4344:2\nowner:4344:2\n", + ); +} + +#[test] +fn native_secret_owner_local_views_07_pending_secret_struct_receiver_preserves_native_redaction_without_source_in_both_profiles() + { + run_case_with_outcome( + "07_pending_secret_struct_receiver", + include_str!("secret_owner_local_views/07_pending_secret_struct_receiver.jett"), + SecretOutcome::RuntimeFailure { + stdout: "before:secret\n", + message: "runtime error: field access is not supported on [redacted]", + }, + ); +} diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/01_secret_owner_direct_alias.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/01_secret_owner_direct_alias.jett new file mode 100644 index 00000000..8f70e081 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/01_secret_owner_direct_alias.jett @@ -0,0 +1,11 @@ +namespace app +struct Packet: + data: bytes +function main(stdout: Stdout) returns nothing: + secret[Packet] hidden = Packet(data: bytes.from_hex("4142") handle error: + return nothing + ) + secret[bytes] borrowed = view hidden.data + bytes copied = clone declassify borrowed + Stdout.write(view stdout, "direct:{bytes.to_hex(view copied)}:{bytes.length(view copied)}:{bytes.to_hex(view declassify hidden.data)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/02_secret_owner_nested_forwarded_alias.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/02_secret_owner_nested_forwarded_alias.jett new file mode 100644 index 00000000..77020daa --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/02_secret_owner_nested_forwarded_alias.jett @@ -0,0 +1,16 @@ +namespace app +struct Payload: + data: bytes +struct Packet: + payload: Payload +function main(stdout: Stdout) returns nothing: + bytes data = bytes.from_hex("4344") handle error: + return nothing + secret[Packet] hidden = Packet(payload: Payload(data: data)) + secret[Payload] aggregate = view hidden.payload + secret[Payload] forwarded_aggregate = view aggregate + secret[bytes] borrowed = view forwarded_aggregate.data + secret[bytes] forwarded = view borrowed + bytes copied = clone declassify forwarded + Stdout.write(view stdout, "nested:{bytes.to_hex(view copied)}:{bytes.length(view copied)}:{bytes.to_hex(view declassify hidden.payload.data)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/03_secret_owner_owned_control.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/03_secret_owner_owned_control.jett new file mode 100644 index 00000000..685892ef --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/03_secret_owner_owned_control.jett @@ -0,0 +1,12 @@ +namespace app +struct Payload: + data: bytes +struct Packet: + payload: Payload +function main(stdout: Stdout) returns nothing: + bytes data = bytes.from_hex("4344") handle error: + return nothing + secret[Packet] hidden = Packet(payload: Payload(data: data)) + bytes copied = clone declassify hidden.payload.data + Stdout.write(view stdout, "owned:{bytes.to_hex(view copied)}:{bytes.length(view copied)}:{bytes.to_hex(view declassify hidden.payload.data)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/04_secret_bitfield_payload.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/04_secret_bitfield_payload.jett new file mode 100644 index 00000000..9fb3462c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/04_secret_bitfield_payload.jett @@ -0,0 +1,17 @@ +namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function main(stdout: Stdout) returns nothing: + uint8 first = 7 + uint8 second = 255 + uint8 added = 9 + secret[Packet] hidden = Packet(version: 4, flags: 0, payload: list(first, second)) + secret[list[uint8]] borrowed = view hidden.payload + secret[list[uint8]] forwarded = view borrowed + mutable list[uint8] copied = clone declassify forwarded + copied = list.append[uint8](copied, added) + bytes encoded = Packet.to_bytes(clone declassify hidden) + Stdout.write(view stdout, "bitfield:{list.length(view declassify forwarded)}:{list.length(view copied)}:{list.length(view declassify hidden.payload)}:{bytes.to_hex(view encoded)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/05_secret_machine_owned_root.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/05_secret_machine_owned_root.jett new file mode 100644 index 00000000..9bbf0c98 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/05_secret_machine_owned_root.jett @@ -0,0 +1,17 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) + empty +function main(stdout: Stdout) returns nothing: + secret[Packet at ready] hidden = Packet(ready, bytes.from_string("AB"), list(2, 3)) + secret[bytes] borrowed_data = view hidden.data + secret[bytes] forwarded_data = view borrowed_data + secret[list[int64]] borrowed_items = view hidden.items + secret[list[int64]] forwarded_items = view borrowed_items + bytes copied_data = clone declassify forwarded_data + mutable list[int64] copied_items = clone declassify forwarded_items + copied_items = list.append[int64](copied_items, 5) + Stdout.write(view stdout, "owned:{bytes.to_hex(view copied_data)}:{list.length(view copied_items)}:{bytes.to_hex(view declassify forwarded_data)}:{list.length(view declassify forwarded_items)}\n") + Stdout.write(view stdout, "owner:{bytes.to_hex(view declassify hidden.data)}:{list.length(view declassify hidden.items)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/06_secret_machine_view_parameter.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/06_secret_machine_view_parameter.jett new file mode 100644 index 00000000..e15cd033 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/06_secret_machine_view_parameter.jett @@ -0,0 +1,20 @@ +namespace app +machine Packet: + states: + ready(data: bytes, items: list[int64]) + empty +function describe(view source: secret[Packet at ready]) returns string: + secret[bytes] borrowed_data = view source.data + secret[bytes] forwarded_data = view borrowed_data + secret[list[int64]] borrowed_items = view source.items + secret[list[int64]] forwarded_items = view borrowed_items + bytes copied_data = clone declassify forwarded_data + mutable list[int64] copied_items = clone declassify forwarded_items + copied_items = list.append[int64](copied_items, 5) + return "{bytes.to_hex(view copied_data)}:{list.length(view copied_items)}:{bytes.to_hex(view declassify source.data)}:{list.length(view declassify source.items)}" +function main(stdout: Stdout) returns nothing: + secret[Packet at ready] hidden = Packet(ready, bytes.from_string("CD"), list(7, 8)) + Stdout.write(view stdout, "view:{describe(view hidden)}\n") + Stdout.write(view stdout, "again:{describe(view hidden)}\n") + Stdout.write(view stdout, "owner:{bytes.to_hex(view declassify hidden.data)}:{list.length(view declassify hidden.items)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_owner_local_views/07_pending_secret_struct_receiver.jett b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/07_pending_secret_struct_receiver.jett new file mode 100644 index 00000000..9682415c --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_owner_local_views/07_pending_secret_struct_receiver.jett @@ -0,0 +1,15 @@ +namespace app +struct Payload: + data: bytes +struct Packet: + payload: Payload +function main(stdout: Stdout) returns nothing: + secret[Packet] hidden = run run Packet(payload: Payload(data: bytes.from_string("AB"))) + Stdout.write(view stdout, "before:secret\n") + secret[Payload] aggregate = view hidden.payload + Stdout.write(view stdout, "after:aggregate\n") + secret[bytes] borrowed = view aggregate.data + Stdout.write(view stdout, "after:data\n") + bytes copied = clone declassify borrowed + Stdout.write(view stdout, "after:length:{bytes.length(view copied)}\n") + return nothing diff --git a/crates/jett_driver/tests/native_conformance/secret_struct_constructors.rs b/crates/jett_driver/tests/native_conformance/secret_struct_constructors.rs new file mode 100644 index 00000000..0d20f3af --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_struct_constructors.rs @@ -0,0 +1,279 @@ +use super::*; + +const SUCCESS_CONTROLS: &str = r#"namespace models +export interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +export struct Counter: + value: int8 +implement Named for Counter: + function name(view self: Counter) returns string: + return "record:{self.value}" +export type Selected = Named where true +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +function selected(value: int8) returns Selected: + Named base = value + Selected chosen = base handle error: + return selected(value) + return chosen +type Callback = function(int8) returns secret[int8] +function classified(value: secret[int8]) returns secret[int8]: + return value +function captured(offset: int8) returns function(secret[int8]) returns secret[int8]: + return function(value: secret[int8]) returns secret[int8]: return value + offset +export struct Item: + count: int8 + labels: list[string] + owner: Named + callback: Callback +export struct Box[T]: + value: T +export function make[T](value: T) returns secret[Box[T]]: + return Box[T](value: value) +function label(view values: list[string]) returns string: + return list.get[string](view values, 0) handle: default "missing" +function describe(view source: Item) returns string: + Callback callback = source.callback + secret[int8] hidden = callback(4) + int8 answer = declassify hidden + return "{source.count}:{list.length(view source.labels)}:{label(view source.labels)}:{Named.name(view source.owner)}:{answer}" +export function read_hidden(view source: secret[Item]) returns string: + Item visible = declassify clone source + return describe(view visible) +function public_item(count: int8) returns Item: + return Item(count: count, labels: list("public", "kept"), owner: Counter(value: count), callback: classified) +function make_item(count: int8) returns secret[Item]: + return Item(count: count, labels: list("return"), owner: selected(count), callback: classified) +export function direct_report() returns string: + secret[Item] direct = Item(count: -128, labels: list("direct", "kept"), owner: selected(7), callback: classified) + secret[Item] parenthesized = (Item(count: 127, labels: list("paren"), owner: Counter(value: 8), callback: classified)) + secret[secret[Item]] nested = Item(count: -1, labels: list("nested"), owner: selected(9), callback: classified) + secret[Item] once = declassify clone nested + return "{read_hidden(view direct)}|{read_hidden(view parenthesized)}|{read_hidden(view once)}" +export function producer_report() returns string: + Item original = public_item(11) + secret[Item] lifted = clone original + secret[Item] from_call = public_item(12) + return "{describe(view original)}|{read_hidden(view lifted)}|{read_hidden(view from_call)}" +export function generic_report() returns string: + secret[Box[int8]] number = Box[int8](value: -128) + secret[Box[Named]] owner = Box[Named](value: selected(7)) + secret[Box[int8]] returned = make[int8](127) + secret[Box[Named]] tagged = make[Named](Counter(value: 8)) + Box[int8] plain_number = declassify clone number + Box[Named] plain_owner = declassify clone owner + Box[int8] plain_returned = declassify clone returned + Box[Named] plain_tagged = declassify clone tagged + return "{plain_number.value}:{Named.name(view plain_owner.value)}:{plain_returned.value}:{Named.name(view plain_tagged.value)}" +export function return_report() returns string: + secret[Item] returned = make_item(7) + int8 offset = 3 + function(int8) returns secret[Item] maker = function(value: int8) returns secret[Item]: + return Item(count: value, labels: list("inline"), owner: Counter(value: value), callback: captured(offset)) + secret[Item] inline = maker(8) + return "{read_hidden(view returned)}|{read_hidden(view inline)}" +export function pending_report() returns string: + secret[Item] pending = run run Item(count: 7, labels: list("pending"), owner: selected(7), callback: classified) + secret[Item] once = join pending handle error: + return error + secret[Item] ready = join once handle error: + return error + return read_hidden(view ready) +export function baked_report() returns string: + secret[Item] hidden = comptime Item(count: -8, labels: list("baked"), owner: selected(9), callback: classified) + secret[Item] returned = comptime make_item(10) + return "{read_hidden(view hidden)}|{read_hidden(view returned)}" +export function clone_report() returns string: + secret[Item] hidden = Item(count: 7, labels: list("clone", "kept"), owner: Counter(value: 8), callback: captured(3)) + Item visible = declassify clone hidden + mutable list[string] copied = visible.labels + copied = list.append[string](copied, "independent") + return "{list.length(view copied)}:{list.length(view visible.labels)}|{describe(view visible)}|{read_hidden(view hidden)}" +namespace app +function main(stdout: Stdout) returns nothing: + use models + Stdout.write(view stdout, "direct:{models.direct_report()}\n") + Stdout.write(view stdout, "producers:{models.producer_report()}\n") + Stdout.write(view stdout, "generic:{models.generic_report()}\n") + Stdout.write(view stdout, "returns:{models.return_report()}\n") + Stdout.write(view stdout, "pending:{models.pending_report()}\n") + Stdout.write(view stdout, "baked:{models.baked_report()}\n") + Stdout.write(view stdout, "clone:{models.clone_report()}\n") + secret[models.Item] observed = models.Item(count: 7, labels: list("observed"), owner: models.Counter(value: 8), callback: function(value: int8) returns secret[int8]: return value) + trace observed + Stdout.write(view stdout, "observed:{models.read_hidden(view observed)}\n") +verify secret_struct_values: + use models + assert models.direct_report() == "-128:2:direct:selected:small:7:4|127:1:paren:record:8:4|-1:1:nested:selected:small:9:4" + assert models.producer_report() == "11:2:public:record:11:4|11:2:public:record:11:4|12:2:public:record:12:4" + assert models.generic_report() == "-128:selected:small:7:127:record:8" + assert models.return_report() == "7:1:return:selected:small:7:4|8:1:inline:record:8:7" + assert models.pending_report() == "7:1:pending:selected:small:7:4" + assert models.baked_report() == "-8:1:baked:selected:small:9:4|10:1:return:selected:small:10:4" + assert models.clone_report() == "3:2|7:2:clone:record:8:7|7:2:clone:record:8:7" +property secret_struct_trials: + given seed: int8 + use models + secret[models.Box[int8]] hidden = models.Box[int8](value: seed) + models.Box[int8] visible = declassify clone hidden + assert visible.value == seed + secret[models.Box[int8]] returned = models.make[int8](seed) + models.Box[int8] plain = declassify clone returned + assert plain.value == seed + assert models.generic_report() == "-128:selected:small:7:127:record:8" + assert models.clone_report() == "3:2|7:2:clone:record:8:7|7:2:clone:record:8:7" +"#; + +#[test] +fn native_secret_struct_constructors_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, SUCCESS_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("accepted contextual secret struct constructors"); + assert_eq!( + expected.stdout, + concat!( + "direct:-128:2:direct:selected:small:7:4|127:1:paren:record:8:4|-1:1:nested:selected:small:9:4\n", + "producers:11:2:public:record:11:4|11:2:public:record:11:4|12:2:public:record:12:4\n", + "generic:-128:selected:small:7:127:record:8\n", + "returns:7:1:return:selected:small:7:4|8:1:inline:record:8:7\n", + "pending:7:1:pending:selected:small:7:4\n", + "baked:-8:1:baked:selected:small:9:4|10:1:return:selected:small:10:4\n", + "clone:3:2|7:2:clone:record:8:7|7:2:clone:record:8:7\n", + "observed:7:1:observed:record:8:4\n", + ) + ); + assert_eq!( + debug_trace_lines(&expected.debug_events), + ["trace observed: secret[models.Item] = [redacted]"] + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_struct_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native contextual secret struct constructors"); + binaries.push((binary, release)); + } + let verify_binary = directory.path().join("secret_struct_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled secret struct verify suite"); + let property_binary = directory.path().join("secret_struct_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled secret struct property suite"); + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + let debug = if release { + String::new() + } else { + jett_driver::render_debug_events(&expected.debug_events) + }; + assert_eq!(actual.stderr, debug.as_bytes(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const FIELD_FAILURE: &str = r#"namespace app +struct Packet: + first: list[string] + second: list[string] +function first(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "first\n") + return list("staged", "owned") +function failing(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "failure\n") + list[string] owned = list("failure", "owner") + return list.remove_at[string](owned, -1) +function make(view stdout: Stdout) returns __TYPE__: + return __CONSTRUCTOR__ +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("retained") + list[string] retained = list("earlier", "owner") + Stdout.write(view stdout, "before\n") + __TYPE__ hidden = make(view stdout) + Stdout.write(view stdout, "must not run:{bytes.to_hex(view earlier)}:{list.length(view retained)}") +"#; + +#[test] +fn native_secret_struct_field_failures_preserve_order_and_cleanup_in_both_profiles() { + for (name, ty, constructor) in [ + ( + "returned", + "secret[Packet]", + "Packet(second: first(view stdout), first: failing(view stdout))", + ), + ( + "parenthesized", + "secret[Packet]", + "(Packet(second: first(view stdout), first: failing(view stdout)))", + ), + ( + "nested", + "secret[secret[Packet]]", + "Packet(second: first(view stdout), first: failing(view stdout))", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + FIELD_FAILURE + .replace("__TYPE__", ty) + .replace("__CONSTRUCTOR__", constructor), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&source) + .expect_err("later field failure prevents secret record construction"); + assert_eq!(expected.output.stdout, "before\nfirst\nfailure\n", "{name}"); + assert!(expected.output.debug_events.is_empty(), "{name}"); + assert_eq!( + expected.message, "runtime error: list.__remove_at: index -1 out of bounds", + "{name}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("field_failure_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}: {error}")); + binaries.push(binary); + } + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + format!("{}\n", expected.message).as_bytes(), + "{name}: {actual:?}" + ); + } + } +} diff --git a/crates/jett_driver/tests/native_conformance/secret_validating_struct_constructors.rs b/crates/jett_driver/tests/native_conformance/secret_validating_struct_constructors.rs new file mode 100644 index 00000000..39202005 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/secret_validating_struct_constructors.rs @@ -0,0 +1,416 @@ +use super::*; + +const SUCCESS_CONTROLS: &str = r#"namespace models +type Positive = int8 where value > 0 +type AboveTen = Positive where value > 10 +type NonEmpty = string where string.char_count(value) > 0 +type Long = NonEmpty where string.char_count(value) >= 3 +type NonEmptyNumbers = list[int8] where list.length[int8](view value) > 0 +type PairNumbers = NonEmptyNumbers where list.length[int8](view value) >= 2 +struct Item: + number: AboveTen + label: Long + numbers: PairNumbers +struct Box[T]: + number: Positive + payload: T +function small(value: int8) returns int8: + return value +function pair() returns list[int8]: + return list(-128, 127) +function inspect(candidate: secret[result[Item, string]]) returns string: + result[Item, string] exposed = declassify candidate + Item ready = exposed handle error: + return error + int8 number = coarsen ready.number + string label = coarsen ready.label + list[int8] numbers = coarsen ready.numbers + int8 first = list.get[int8](view numbers, 0) handle: + return "missing" + return "{number}:{label}:{list.length(view numbers)}:{first}" +export function checked(number: int8, label: string, numbers: list[int8]) returns string: + secret[result[Item, string]] candidate = Item(number: number, label: label, numbers: numbers) + return inspect(candidate) +export function number_failures() returns string: + return "{checked(0, "Agent", list(-128, 127))}|{checked(7, "Agent", list(-128, 127))}" +export function owned_failures() returns string: + return "{checked(12, "", list(-128, 127))}|{checked(12, "Al", list(-128, 127))}|{checked(12, "Agent", list())}|{checked(12, "Agent", list(7))}" +function already_checked() returns string: + AboveTen number = small(12) handle error: + return error + Long label = "Agent" handle error: + return error + PairNumbers numbers = pair() handle error: + return error + secret[result[Item, string]] candidate = Item(number: number, label: label, numbers: numbers) + return inspect(candidate) +function public_attempt(number: int8) returns result[Item, string]: + return Item(number: number, label: "Agent", numbers: pair()) +function hidden_attempt(number: int8) returns secret[result[Item, string]]: + return Item(number: number, label: "Agent", numbers: pair()) +function nested_attempt(number: int8) returns string: + secret[secret[result[Item, string]]] nested = Item(number: number, label: "Agent", numbers: pair()) + secret[result[Item, string]] once = declassify nested + return inspect(once) +function joined_attempt(number: int8) returns string: + secret[result[Item, string]] pending = run run Item(number: number, label: "Agent", numbers: pair()) + secret[result[Item, string]] once = join pending handle error: + return error + secret[result[Item, string]] ready = join once handle error: + return error + return inspect(ready) +export function context_report() returns string: + secret[result[Item, string]] returned = hidden_attempt(12) + function(int8) returns secret[result[Item, string]] maker = function(number: int8) returns secret[result[Item, string]]: + return Item(number: number, label: "Agent", numbers: pair()) + secret[result[Item, string]] inline = maker(12) + secret[result[Item, string]] parenthesized = ((Item)(number: (small(12)), label: "Agent", numbers: pair())) + return "{already_checked()}|{inspect(returned)}|{inspect(inline)}|{inspect(parenthesized)}|{nested_attempt(12)}|{joined_attempt(12)}" +export function producer_report() returns string: + result[Item, string] original = public_attempt(12) + secret[result[Item, string]] promoted = clone original + secret[result[Item, string]] producer = public_attempt(12) + secret[result[Item, string]] original_copy = clone original + return "{inspect(promoted)}|{inspect(producer)}|{inspect(original_copy)}" +function generic_attempt[T](number: int8, payload: T) returns secret[result[Box[T], string]]: + return Box[T](number: number, payload: payload) +export function generic_report() returns string: + secret[result[Box[list[string]], string]] hidden = generic_attempt[list[string]](7, list("kept", "owner")) + result[Box[list[string]], string] exposed = declassify hidden + Box[list[string]] ready = exposed handle error: + return error + mutable list[string] copied = ready.payload + copied = list.append[string](copied, "independent") + int8 number = coarsen ready.number + string last = list.get[string](view copied, 2) handle: + return "missing" + secret[result[Box[string], string]] text_hidden = Box[string](number: small(7), payload: "generic") + result[Box[string], string] text_exposed = declassify text_hidden + Box[string] text_ready = text_exposed handle error: + return error + return "{number}:{list.length(view ready.payload)}:{list.length(view copied)}:{last}:{text_ready.payload}" +function escaped_numbers() returns list[int8]: + secret[result[Item, string]] hidden = Item(number: small(12), label: "Agent", numbers: pair()) + result[Item, string] exposed = declassify hidden + Item ready = exposed handle error: + return list() + return coarsen ready.numbers +export function ownership_report() returns string: + secret[result[Item, string]] hidden = Item(number: small(12), label: "Agent", numbers: pair()) + result[Item, string] exposed = declassify clone hidden + Item ready = exposed handle error: + return error + Item duplicate = clone ready + mutable list[int8] copied = coarsen ready.numbers + copied = list.append[int8](copied, 9) + mutable list[int8] escaped = escaped_numbers() + escaped = list.append[int8](escaped, 10) + int8 first = list.get[int8](view escaped, 0) handle: + return "missing" + list[int8] original_numbers = coarsen ready.numbers + list[int8] duplicate_numbers = coarsen duplicate.numbers + return "{list.length(view copied)}:{list.length(view escaped)}:{first}:{list.length(view original_numbers)}:{list.length(view duplicate_numbers)}|{inspect(hidden)}" +export function baked_report() returns string: + secret[result[Item, string]] direct = comptime Item(number: small(12), label: "Agent", numbers: pair()) + secret[result[Item, string]] returned = comptime hidden_attempt(12) + secret[result[Item, string]] failed = comptime Item(number: small(7), label: "Agent", numbers: pair()) + return "{inspect(direct)}|{inspect(returned)}|{inspect(failed)}" +namespace app +function main(stdout: Stdout) returns nothing: + use models + Stdout.write(view stdout, "valid:{models.checked(12, "Agent", list(-128, 127))}\n") + Stdout.write(view stdout, "number-errors:{models.number_failures()}\n") + Stdout.write(view stdout, "owned-errors:{models.owned_failures()}\n") + Stdout.write(view stdout, "contexts:{models.context_report()}\n") + Stdout.write(view stdout, "producers:{models.producer_report()}\n") + Stdout.write(view stdout, "generic:{models.generic_report()}\n") + Stdout.write(view stdout, "ownership:{models.ownership_report()}\n") + Stdout.write(view stdout, "baked:{models.baked_report()}\n") +verify secret_validating_structs: + use models + assert models.checked(12, "Agent", list(-128, 127)) == "12:Agent:2:-128" + assert models.number_failures() == "refinement type constraint failed for 'models.Positive'|refinement type constraint failed for 'models.AboveTen'" + assert models.owned_failures() == "refinement type constraint failed for 'models.NonEmpty'|refinement type constraint failed for 'models.Long'|refinement type constraint failed for 'models.NonEmptyNumbers'|refinement type constraint failed for 'models.PairNumbers'" + assert models.context_report() == "12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128" + assert models.producer_report() == "12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128" + assert models.generic_report() == "7:2:3:independent:generic" + assert models.ownership_report() == "3:3:-128:2:2|12:Agent:2:-128" + assert models.baked_report() == "12:Agent:2:-128|12:Agent:2:-128|refinement type constraint failed for 'models.AboveTen'" +property secret_validating_struct_trials: + given high: bool + use models + mutable int8 number = 12 + if high: + number = 127 + assert models.checked(number, "Agent", list(-128, 127)) == "{number}:Agent:2:-128" + assert models.generic_report() == "7:2:3:independent:generic" + assert models.ownership_report() == "3:3:-128:2:2|12:Agent:2:-128" +"#; + +#[test] +fn native_secret_validating_struct_results_match_reference_in_both_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, SUCCESS_CONTROLS).unwrap(); + let expected = jett_driver::run_file_capture_output(&source) + .expect("accepted secret validating struct constructor results"); + assert_eq!( + expected.stdout, + concat!( + "valid:12:Agent:2:-128\n", + "number-errors:refinement type constraint failed for 'models.Positive'|refinement type constraint failed for 'models.AboveTen'\n", + "owned-errors:refinement type constraint failed for 'models.NonEmpty'|refinement type constraint failed for 'models.Long'|refinement type constraint failed for 'models.NonEmptyNumbers'|refinement type constraint failed for 'models.PairNumbers'\n", + "contexts:12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128\n", + "producers:12:Agent:2:-128|12:Agent:2:-128|12:Agent:2:-128\n", + "generic:7:2:3:independent:generic\n", + "ownership:3:3:-128:2:2|12:Agent:2:-128\n", + "baked:12:Agent:2:-128|12:Agent:2:-128|refinement type constraint failed for 'models.AboveTen'\n", + ) + ); + assert!(expected.debug_events.is_empty(), "{expected:?}"); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory + .path() + .join(format!("secret_validating_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .expect("native secret validating struct results"); + binaries.push(binary); + } + let verify_binary = directory.path().join("secret_validating_verify.exe"); + build_host_verify_suite_executable(&source, launcher(), &verify_binary) + .expect("compiled secret validating struct verify suite"); + let property_binary = directory.path().join("secret_validating_property.exe"); + build_host_property_suite_executable(&source, launcher(), &property_binary) + .expect("compiled secret validating struct property suite"); + fs::remove_file(&source).unwrap(); + for binary in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } + for binary in [verify_binary, property_binary] { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.is_empty(), "{actual:?}"); + } +} + +const EFFECT_DECLARATIONS: &str = r#"namespace app +function first_check(value: int8) returns bool: + string marker = "first-predicate" + trace marker + return value > 0 +type First = int8 where first_check(value) +function second_check(value: int8) returns bool: + string marker = "second-predicate" + trace marker + return value > 0 +type Second = int8 where second_check(value) +struct Pair: + first: First + second: Second + payload: list[string] +function throwing_check(value: int8) returns bool: + string marker = "throwing-predicate" + trace marker + list[int8] owned = list(1, 2) + list[int8] failed = list.remove_at[int8](owned, -1) + return list.length(view failed) > 0 +type Throwing = int8 where throwing_check(value) +function last_check(value: int8) returns bool: + string marker = "last-predicate" + trace marker + return value > 0 +type Last = int8 where last_check(value) +struct Broken: + first: Throwing + last: Last + payload: list[string] +function payload(view stdout: Stdout) returns list[string]: + Stdout.write(view stdout, "payload\n") + return list("staged", "owned") +function first_input(view stdout: Stdout, value: int8) returns int8: + Stdout.write(view stdout, "first-input\n") + return value +function second_input(view stdout: Stdout, value: int8) returns int8: + Stdout.write(view stdout, "second-input\n") + return value +function failing_input(view stdout: Stdout) returns int8: + Stdout.write(view stdout, "failure\n") + list[int8] owned = list(1, 2) + list[int8] failed = list.remove_at[int8](owned, -1) + return list.get[int8](view failed, 0) handle: + default 0 +function inspect_pair(candidate: secret[result[Pair, string]]) returns string: + result[Pair, string] exposed = declassify candidate + Pair ready = exposed handle error: + return error + int8 first = coarsen ready.first + int8 second = coarsen ready.second + return "{first}:{second}:{list.length(view ready.payload)}" +function inspect_broken(candidate: secret[result[Broken, string]]) returns string: + result[Broken, string] exposed = declassify candidate + Broken ready = exposed handle error: + return error + return "must not succeed" +"#; + +fn execute_effect_case( + name: &str, + statements: &str, + expected_stdout: &str, + expected_debug: &[&str], + terminal_error: Option<&str>, +) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write( + &source, + format!( + "{EFFECT_DECLARATIONS}function main(stdout: Stdout) returns nothing:\n bytes earlier = bytes.from_string(\"retained\")\n list[string] retained = list(\"earlier\", \"owner\")\n Stdout.write(view stdout, \"before\\n\")\n{statements} Stdout.write(view stdout, \"after:{{bytes.to_hex(view earlier)}}:{{list.length(view retained)}}\\n\")\n" + ), + ) + .unwrap(); + let runtime_debug_events = match ( + jett_driver::run_file_capture_outcome(&source), + terminal_error, + ) { + (Ok(expected), None) => { + assert_eq!(expected.stdout, expected_stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.debug_events), + expected_debug, + "{name}" + ); + expected.debug_events + } + (Err(expected), Some(message)) => { + assert_eq!(expected.message, message, "{name}"); + assert_eq!(expected.output.stdout, expected_stdout, "{name}"); + assert_eq!( + debug_trace_lines(&expected.output.debug_events), + expected_debug, + "{name}" + ); + expected.output.debug_events + } + (expected, error) => panic!("{name}: expected error {error:?}, got {expected:?}"), + }; + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + jett_driver::native::build_host_executable_with_options( + &source, + launcher(), + &binary, + jett_driver::BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: {error}")); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(if terminal_error.is_some() { 71 } else { 0 }), + "{name}: {actual:?}" + ); + assert_eq!(actual.stdout, expected_stdout.as_bytes(), "{name}"); + let mut stderr = if release { + String::new() + } else { + jett_driver::render_debug_events(&runtime_debug_events) + }; + if let Some(error) = terminal_error { + stderr.push_str(error); + stderr.push('\n'); + } + assert_eq!(actual.stderr, stderr.as_bytes(), "{name}: {actual:?}"); + } +} + +#[test] +fn native_secret_struct_validation_preserves_field_then_predicate_order_and_result_data() { + for (name, first, second, outcome, debug) in [ + ( + "reverse_fields_ready", + 7, + 7, + "7:7:2", + &[ + "trace marker: string = second-predicate", + "trace marker: string = first-predicate", + ][..], + ), + ( + "second_predicate_fails", + 7, + 0, + "refinement type constraint failed for 'app.Second'", + &["trace marker: string = second-predicate"][..], + ), + ( + "first_predicate_fails", + 0, + 7, + "refinement type constraint failed for 'app.First'", + &[ + "trace marker: string = second-predicate", + "trace marker: string = first-predicate", + ][..], + ), + ] { + let statements = format!( + " secret[result[Pair, string]] candidate = Pair(payload: payload(view stdout), second: second_input(view stdout, {second}), first: first_input(view stdout, {first}))\n Stdout.write(view stdout, \"outcome:{{inspect_pair(candidate)}}\\n\")\n" + ); + let stdout = format!( + "before\npayload\nsecond-input\nfirst-input\noutcome:{outcome}\nafter:72657461696e6564:2\n" + ); + execute_effect_case(name, &statements, &stdout, debug, None); + } + // A predicate's terminal operation is captured as validation failure data. + // All field expressions run before it; the following predicate is skipped. + execute_effect_case( + "predicate_operation_becomes_result_data", + " secret[result[Broken, string]] candidate = Broken(payload: payload(view stdout), first: first_input(view stdout, 7), last: second_input(view stdout, 7))\n Stdout.write(view stdout, \"outcome:{inspect_broken(candidate)}\\n\")\n", + "before\npayload\nfirst-input\nsecond-input\noutcome:error evaluating refinement constraint for 'app.Throwing': list.__remove_at: index -1 out of bounds\nafter:72657461696e6564:2\n", + &["trace marker: string = throwing-predicate"], + None, + ); +} + +#[test] +fn native_secret_validating_struct_field_failures_precede_predicates_and_handlers() { + for (name, constructor, stdout) in [ + ( + "later_field_failure", + "Pair(payload: payload(view stdout), first: failing_input(view stdout), second: second_input(view stdout, 7))", + "before\npayload\nfailure\n", + ), + ( + "first_field_failure", + "Pair(first: failing_input(view stdout), payload: payload(view stdout), second: second_input(view stdout, 7))", + "before\nfailure\n", + ), + ] { + let statements = format!( + " secret[result[Pair, string]] candidate = {constructor}\n result[Pair, string] exposed = declassify candidate\n Pair ready = exposed handle error:\n Stdout.write(view stdout, \"must not handle\\n\")\n return nothing\n Stdout.write(view stdout, \"must not run:{{list.length(view ready.payload)}}\\n\")\n" + ); + execute_effect_case( + name, + &statements, + stdout, + &[], + Some("runtime error: list.__remove_at: index -1 out of bounds"), + ); + } +} diff --git a/crates/jett_driver/tests/native_conformance/suite_options.rs b/crates/jett_driver/tests/native_conformance/suite_options.rs new file mode 100644 index 00000000..ae1d073f --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/suite_options.rs @@ -0,0 +1,330 @@ +use super::run_bounded; +use jett_driver::native::*; +use jett_driver::{BackendLoweringError, BuildOptions}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +const SUITE: &str = r#"namespace app +function checked(value: bool) returns bool: + string marker = "suite" + trace marker + breakpoint false + return value == value +function main() returns nothing: + return nothing +verify checked_values: + assert checked(true) +property checked_trials: + given chosen: bool + assert checked(chosen) +"#; + +const POLICY: &str = r#"namespace app +function debug_helper() returns nothing: + println("debug") +function main() returns nothing: + return nothing +verify debug_source: + assert true +property debug_trials: + given chosen: bool + assert chosen == chosen +"#; + +const OPTIMIZATION: &str = r#"namespace app +function identity(value: int64) returns int64: + mutable int64 total = value + for offset in list(0, 1, 2, 3, 4, 5, 6, 7): + total = total + offset + return total - 28 +function main() returns nothing: + return nothing +verify arithmetic: + assert identity(12) == 12 +property arithmetic_trials: + given input: int64 + assert identity(input) == input +"#; + +struct ProfileLauncher { + bundle: NativeLauncherBundle, + _directory: tempfile::TempDir, +} + +/// Suite options select Jett source/optimization policy. The caller separately +/// supplies an archive compiled for that runtime profile, as the public API requires. +pub(super) fn launcher_for_options(release: bool) -> &'static NativeLauncherBundle { + if let Some(bundle) = super::measured_launcher::supplied(release) { + return bundle; + } + static DEBUG: OnceLock = OnceLock::new(); + static RELEASE: OnceLock = OnceLock::new(); + let slot = if release { &RELEASE } else { &DEBUG }; + &slot + .get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let target = executable + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("native-values-launcher"); + let host = host_target(); + let profile = if release { "release" } else { "debug" }; + let cargo_profile = if release { "release" } else { "test" }; + let status = Command::new(env!("CARGO")) + .args([ + "build", + "-q", + "-p", + "jett_native_launcher", + "--target", + &host, + "--profile", + cargo_profile, + "--target-dir", + ]) + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build explicit suite runtime profile"); + assert!(status.success(), "suite runtime build failed: {status}"); + let archive_name = if cfg!(windows) { + "jett_native_launcher.lib" + } else { + "libjett_native_launcher.a" + }; + let archive = target.join(&host).join(profile).join(archive_name); + let directory = tempfile::tempdir().unwrap(); + fs::copy(&archive, directory.path().join(archive_name)).unwrap(); + let template = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join(archive_name)) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join(archive_name)) + }; + let manifest = directory.path().join("runtime.json"); + fs::write( + &manifest, + serde_json::to_vec(&serde_json::json!({ + "manifest_version": 1, + "compiler_version": env!("CARGO_PKG_VERSION"), + "profile": profile, + "archive": archive_name, + "runtime_abi_version": template.runtime_abi_version, + "crt_mode": template.crt_mode.to_string(), + "target": template.target, + "native_library_args": template.native_library_args.iter() + .map(|arg| arg.to_str().unwrap()).collect::>(), + })) + .unwrap(), + ) + .unwrap(); + let bundle = NativeLauncherBundle::from_manifest(&manifest, release).unwrap(); + assert!( + NativeLauncherBundle::from_manifest(&manifest, !release) + .unwrap_err() + .contains("profile mismatch") + ); + ProfileLauncher { + bundle, + _directory: directory, + } + }) + .bundle +} + +#[test] +fn native_suite_options_preserve_defaults_and_select_source_and_runtime_profiles() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, SUITE).unwrap(); + let mut binaries: Vec<(PathBuf, bool, usize)> = Vec::new(); + for property in [false, true] { + let default = if property { + emit_host_property_suite_object_for_file(&source).unwrap() + } else { + emit_host_verify_suite_object_for_file(&source).unwrap() + }; + let explicit = if property { + emit_host_property_suite_object_for_file_with_options(&source, BuildOptions::default()) + .unwrap() + } else { + emit_host_verify_suite_object_for_file_with_options(&source, BuildOptions::default()) + .unwrap() + }; + assert_eq!( + default.bytes(), + explicit.bytes(), + "default suite object changed" + ); + let default_binary = directory.path().join(format!("default_{property}.exe")); + if property { + build_host_property_suite_executable( + &source, + launcher_for_options(false), + &default_binary, + ) + .unwrap(); + } else { + build_host_verify_suite_executable( + &source, + launcher_for_options(false), + &default_binary, + ) + .unwrap(); + } + binaries.push((default_binary, false, if property { 100 } else { 1 })); + for release in [false, true] { + let binary = directory + .path() + .join(format!("suite_{property}_{release}.exe")); + if property { + build_host_property_suite_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap(); + } else { + build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap(); + } + binaries.push((binary, release, if property { 100 } else { 1 })); + } + } + fs::remove_file(&source).unwrap(); + for (binary, release, trials) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert!(actual.stdout.is_empty(), "{actual:?}"); + let expected = if release { + String::new() + } else { + "trace marker: string = suite\n".repeat(trials) + }; + assert_eq!(actual.stderr, expected.as_bytes(), "{actual:?}"); + } +} + +#[test] +fn native_suite_release_policy_rejects_debug_print_before_publication() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, POLICY).unwrap(); + for property in [false, true] { + let default = if property { + jett_driver::lower_file_for_native_property_suite(&source) + } else { + jett_driver::lower_file_for_native_verify_suite(&source) + }; + default.expect("default debug suite still admits debug printing"); + let rejected = if property { + jett_driver::lower_file_for_native_property_suite_with_options( + &source, + BuildOptions { release: true }, + ) + } else { + jett_driver::lower_file_for_native_verify_suite_with_options( + &source, + BuildOptions { release: true }, + ) + }; + let Err(BackendLoweringError::Build(result)) = rejected else { + panic!("release policy did not reject debug print") + }; + assert!( + result + .diagnostics + .iter() + .any(|d| u32::from(d.code.code()) == 362) + ); + let output = directory.path().join(format!("preserved_{property}.exe")); + fs::write(&output, b"existing output").unwrap(); + let error = if property { + build_host_property_suite_executable_with_options( + &source, + launcher_for_options(true), + &output, + BuildOptions { release: true }, + ) + } else { + build_host_verify_suite_executable_with_options( + &source, + launcher_for_options(true), + &output, + BuildOptions { release: true }, + ) + } + .unwrap_err(); + assert!(matches!(error, NativeBuildError::Lowering { .. })); + assert_eq!(fs::read(&output).unwrap(), b"existing output"); + } +} + +#[test] +fn native_suite_release_objects_use_optimized_codegen() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("main.jett"); + fs::write(&source, OPTIMIZATION).unwrap(); + for property in [false, true] { + let lowered = if property { + jett_driver::lower_file_for_native_property_suite_with_options( + &source, + BuildOptions { release: true }, + ) + .unwrap() + } else { + jett_driver::lower_file_for_native_verify_suite_with_options( + &source, + BuildOptions { release: true }, + ) + .unwrap() + }; + let entry = if property { + lowered.native_property_entry + } else { + lowered.native_verify_entry + } + .unwrap(); + let emit = |optimize| { + jett_codegen_cranelift::emit_host_program_object_with_options( + &lowered.mir, + &lowered.interner, + entry, + jett_codegen_cranelift::CodegenOptions { optimize }, + ) + .unwrap() + .bytes + }; + let optimized = emit(true); + assert_ne!( + optimized, + emit(false), + "fixture must distinguish optimization" + ); + let actual = if property { + emit_host_property_suite_object_for_file_with_options( + &source, + BuildOptions { release: true }, + ) + .unwrap() + } else { + emit_host_verify_suite_object_for_file_with_options( + &source, + BuildOptions { release: true }, + ) + .unwrap() + }; + assert_eq!(actual.bytes(), optimized); + } +} diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks.rs b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks.rs new file mode 100644 index 00000000..acb0ca14 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks.rs @@ -0,0 +1,164 @@ +use super::{run_bounded, suite_options::launcher_for_options}; +use jett_driver::native::build_host_executable_with_options; +use jett_driver::{BuildOptions, run_file_capture_outcome}; +use std::fs; + +enum CallbackOutcome<'a> { + Success(&'a str), + RuntimeFailure { stdout: &'a str, message: &'a str }, +} + +fn run_case(name: &str, source_text: &str, expected_stdout: &str) { + run_case_with_outcome(name, source_text, CallbackOutcome::Success(expected_stdout)); +} + +fn run_case_with_outcome(name: &str, source_text: &str, expected: CallbackOutcome<'_>) { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join(format!("{name}.jett")); + fs::write(&source, source_text).unwrap(); + let (reference, status, stderr) = match expected { + CallbackOutcome::Success(stdout) => { + let output = run_file_capture_outcome(&source) + .unwrap_or_else(|error| panic!("{name}: checked reference fixture: {error:?}")); + assert_eq!(output.stdout, stdout, "{name}"); + (output, 0, String::new()) + } + CallbackOutcome::RuntimeFailure { stdout, message } => { + let failure = run_file_capture_outcome(&source) + .expect_err("independent owned-list operation must fail after descriptor creation"); + assert_eq!(failure.output.stdout, stdout, "{name}"); + assert_eq!(failure.message, message, "{name}"); + (failure.output, 71, format!("{message}\n")) + } + }; + assert!(reference.debug_events.is_empty(), "{name}: {reference:?}"); + assert!( + reference.frontend_debug_observations.is_empty(), + "{name}: {reference:?}" + ); + let mut binaries = Vec::new(); + for release in [false, true] { + let binary = directory.path().join(format!("{name}_{release}.exe")); + let artifact = build_host_executable_with_options( + &source, + launcher_for_options(release), + &binary, + BuildOptions { release }, + ) + .unwrap_or_else(|error| panic!("{name}, release={release}: native build: {error}")); + assert!( + artifact.debug_observations.is_empty(), + "{name}: {artifact:?}" + ); + binaries.push((binary, release)); + } + fs::remove_file(&source).unwrap(); + assert!(!source.exists()); + for (binary, release) in binaries { + let actual = run_bounded(&binary, directory.path()); + assert_eq!( + actual.status.code(), + Some(status), + "{name}, release={release}: {actual:?}" + ); + assert_eq!( + actual.stdout, + reference.stdout.as_bytes(), + "{name}, release={release}" + ); + assert_eq!( + actual.stderr, + stderr.as_bytes(), + "{name}, release={release}: {actual:?}" + ); + } +} + +#[test] +fn native_uninhabited_callbacks_direct_empty_matches_reference_without_source_in_both_profiles() { + run_case( + "01_inferred_empty_inline_callback", + include_str!("uninhabited_callbacks/01_inferred_empty_inline_callback.jett"), + "direct:0:-1\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_captured_empty_matches_reference_without_source_in_both_profiles() { + run_case( + "02_inferred_empty_captured_callback", + include_str!("uninhabited_callbacks/02_inferred_empty_captured_callback.jett"), + "captured:0:-1\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_concrete_callbacks_matches_reference_without_source_in_both_profiles() + { + run_case( + "03_concrete_callback_controls", + include_str!("uninhabited_callbacks/03_concrete_callback_controls.jett"), + "direct:2:7:captured:3:3\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_04_descriptor_ownership_matches_reference_without_source_in_both_profiles() + { + run_case( + "04_descriptor_ownership", + include_str!("uninhabited_callbacks/04_descriptor_ownership.jett"), + "owners:0:1:1\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_05_pending_descriptor_empty_map_matches_reference_without_source_in_both_profiles() + { + run_case( + "05_pending_descriptor_empty_map", + include_str!("uninhabited_callbacks/05_pending_descriptor_empty_map.jett"), + "pending:0:0\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_06_terminal_cleanup_preserves_failure_and_cleanup_without_source_in_both_profiles() + { + run_case_with_outcome( + "06_terminal_cleanup", + include_str!("uninhabited_callbacks/06_terminal_cleanup.jett"), + CallbackOutcome::RuntimeFailure { + stdout: "before:0:1:5\n", + message: "runtime error: list.__remove_at: index -1 out of bounds", + }, + ); +} + +#[test] +fn native_uninhabited_callbacks_07_identity_matches_reference_without_source_in_both_profiles() { + run_case( + "07_uninhabited_identity_callback", + include_str!("uninhabited_callbacks/07_uninhabited_identity_callback.jett"), + "identity:0\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_08_loop_body_matches_reference_without_source_in_both_profiles() { + run_case( + "08_loop_body_callback", + include_str!("uninhabited_callbacks/08_loop_body_callback.jett"), + "loop:0:-1:1:3\n", + ); +} + +#[test] +fn native_uninhabited_callbacks_09_equality_body_matches_reference_without_source_in_both_profiles() +{ + run_case( + "09_equality_body_callback", + include_str!("uninhabited_callbacks/09_equality_body_callback.jett"), + "equality:0:false:1:true\n", + ); +} diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/01_inferred_empty_inline_callback.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/01_inferred_empty_inline_callback.jett new file mode 100644 index 00000000..ae19f309 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/01_inferred_empty_inline_callback.jett @@ -0,0 +1,9 @@ +namespace app +function mapped_report[T](values: list[T]) returns string: + function(T) returns int64 mapper = function(ignored: T) returns int64: return 7 + list[int64] mapped = list.map[T, int64](values, mapper) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + return "{list.length[int64](view mapped)}:{first}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "direct:{mapped_report(list())}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/02_inferred_empty_captured_callback.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/02_inferred_empty_captured_callback.jett new file mode 100644 index 00000000..7358e4a9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/02_inferred_empty_captured_callback.jett @@ -0,0 +1,12 @@ +namespace app +function make_size[T](view values: list[T]) returns function(T) returns int64: + int64 size = list.length[T](view values) + return function(ignored: T) returns int64: return size +function captured_report[T](values: list[T]) returns string: + function(T) returns int64 mapper = make_size[T](view values) + list[int64] mapped = list.map[T, int64](values, mapper) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + return "{list.length[int64](view mapped)}:{first}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "captured:{captured_report(list())}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/03_concrete_callback_controls.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/03_concrete_callback_controls.jett new file mode 100644 index 00000000..26fe0e4a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/03_concrete_callback_controls.jett @@ -0,0 +1,18 @@ +namespace app +function mapped_report[T](values: list[T]) returns string: + function(T) returns int64 mapper = function(ignored: T) returns int64: return 7 + list[int64] mapped = list.map[T, int64](values, mapper) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + return "{list.length[int64](view mapped)}:{first}" +function make_size[T](view values: list[T]) returns function(T) returns int64: + int64 size = list.length[T](view values) + return function(ignored: T) returns int64: return size +function captured_report[T](values: list[T]) returns string: + function(T) returns int64 mapper = make_size[T](view values) + list[int64] mapped = list.map[T, int64](values, mapper) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + return "{list.length[int64](view mapped)}:{first}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "direct:{mapped_report(list(1, 2))}:captured:{captured_report(list(3, 4, 5))}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/04_descriptor_ownership.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/04_descriptor_ownership.jett new file mode 100644 index 00000000..425c6b37 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/04_descriptor_ownership.jett @@ -0,0 +1,14 @@ +namespace app +function make_size[T](view values: list[T]) returns function(T) returns int64: + int64 size = list.length[T](view values) + return function(ignored: T) returns int64: return size +function ownership_report[T](values: list[T]) returns string: + function(T) returns int64 callback = make_size[T](view values) + list[function(T) returns int64] originals = list(clone callback) + list[function(T) returns int64] copies = clone originals + function(T) returns int64 selected = list.get[function(T) returns int64](view copies, 0) handle: + return "missing" + list[int64] mapped = list.map[T, int64](values, selected) + return "{list.length[int64](view mapped)}:{list.length[function(T) returns int64](view originals)}:{list.length[function(T) returns int64](view copies)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "owners:{ownership_report(list())}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/05_pending_descriptor_empty_map.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/05_pending_descriptor_empty_map.jett new file mode 100644 index 00000000..3d8f14b9 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/05_pending_descriptor_empty_map.jett @@ -0,0 +1,16 @@ +namespace app +function make_size[T](view values: list[T]) returns function(T) returns int64: + int64 size = list.length[T](view values) + return function(ignored: T) returns int64: return size +function pending_report[T](values: list[T]) returns string: + function(T) returns int64 callback = make_size[T](view values) + function(T) returns int64 pending = run run clone callback + list[int64] mapped = list.map[T, int64](values, clone pending) + function(T) returns int64 once = join pending handle error: + return "first:{error}" + function(T) returns int64 ready = join once handle error: + return "second:{error}" + list[int64] joined = list.map[T, int64](list.new[T](), ready) + return "{list.length[int64](view mapped)}:{list.length[int64](view joined)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "pending:{pending_report(list())}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/06_terminal_cleanup.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/06_terminal_cleanup.jett new file mode 100644 index 00000000..3b91fb04 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/06_terminal_cleanup.jett @@ -0,0 +1,14 @@ +namespace app +function make_size[T](view values: list[T]) returns function(T) returns int64: + int64 size = list.length[T](view values) + return function(ignored: T) returns int64: return size +function terminal[T](values: list[T], view stdout: Stdout) returns nothing: + function(T) returns int64 callback = make_size[T](view values) + list[function(T) returns int64] callbacks = list(clone callback) + bytes owner = bytes.from_string("owned") + Stdout.write(view stdout, "before:{list.length[T](view values)}:{list.length[function(T) returns int64](view callbacks)}:{bytes.length(view owner)}\n") + list[string] removed = list.remove_at[string](list("owned", "other"), -1) + list[int64] mapped = list.map[T, int64](values, callback) + Stdout.write(view stdout, "unreachable:{list.length[int64](view mapped)}:{list.length[string](view removed)}:{list.length[function(T) returns int64](view callbacks)}:{bytes.length(view owner)}\n") +function main(stdout: Stdout) returns nothing: + terminal(list(), view stdout) diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/07_uninhabited_identity_callback.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/07_uninhabited_identity_callback.jett new file mode 100644 index 00000000..d674575a --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/07_uninhabited_identity_callback.jett @@ -0,0 +1,7 @@ +namespace app +function identity_report[T](values: list[T]) returns int64: + function(T) returns T callback = function(value: T) returns T: return value + list[T] mapped = list.map[T, T](values, callback) + return list.length[T](view mapped) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "identity:{identity_report(list())}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/08_loop_body_callback.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/08_loop_body_callback.jett new file mode 100644 index 00000000..d53102dd --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/08_loop_body_callback.jett @@ -0,0 +1,13 @@ +namespace app +function loop_report[T](values: list[T]) returns string: + function(T) returns int64 callback = function(ignored: T) returns int64: + mutable int64 total = 0 + for item in list(1, 2): + total = total + item + return total + list[int64] mapped = list.map[T, int64](values, callback) + int64 first = list.get[int64](view mapped, 0) handle: + default -1 + return "{list.length[int64](view mapped)}:{first}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "loop:{loop_report(list())}:{loop_report(list(9))}\n") diff --git a/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/09_equality_body_callback.jett b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/09_equality_body_callback.jett new file mode 100644 index 00000000..652c3f22 --- /dev/null +++ b/crates/jett_driver/tests/native_conformance/uninhabited_callbacks/09_equality_body_callback.jett @@ -0,0 +1,10 @@ +namespace app +function equality_report[T](values: list[T]) returns string: + function(T) returns bool callback = function(value: T) returns bool: + return value == value + list[bool] mapped = list.map[T, bool](values, callback) + bool first = list.get[bool](view mapped, 0) handle: + default false + return "{list.length[bool](view mapped)}:{first}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "equality:{equality_report(list())}:{equality_report(list(9))}\n") diff --git a/crates/jett_driver/tests/native_execution.rs b/crates/jett_driver/tests/native_execution.rs new file mode 100644 index 00000000..25e89150 --- /dev/null +++ b/crates/jett_driver/tests/native_execution.rs @@ -0,0 +1,512 @@ +//! Production native execution, without the source tree as runtime cwd. +#![cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))] + +use jett_driver::native::{NativeLauncherBundle, build_host_executable}; +use std::path::Path; +use std::process::Command; +use std::sync::OnceLock; + +struct Launcher { + bundle: NativeLauncherBundle, + _directory: tempfile::TempDir, +} + +impl std::ops::Deref for Launcher { + type Target = NativeLauncherBundle; + + fn deref(&self) -> &Self::Target { + &self.bundle + } +} + +fn launcher() -> Launcher { + // Cache bytes, not a path into a stale build tree or a leaked TempDir. + static ARCHIVE: OnceLock> = OnceLock::new(); + let archive = ARCHIVE.get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let profile_directory = executable.parent().unwrap().parent().unwrap(); + let profile = profile_directory.file_name().unwrap().to_str().unwrap(); + // Cargo's built-in test profile shares the debug output directory. + let cargo_profile = if profile == "debug" { "test" } else { profile }; + let host = jett_driver::native::host_target(); + // Share one nested cache, outside the outer Cargo build lock. + // Explicit host and profile still select the correct archive. + let target = profile_directory + .parent() + .unwrap() + .join("native-values-launcher"); + let status = Command::new(env!("CARGO")) + .args(["build", "-q", "-p", "jett_native_launcher"]) + .args([ + "--target", + &host, + "--profile", + cargo_profile, + "--target-dir", + ]) + .arg(&target) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build host- and profile-matched launcher"); + assert!(status.success(), "launcher build failed: {status}"); + let archive = target + .join(&host) + .join(profile) + .join("libjett_native_launcher.a"); + std::fs::read(&archive) + .unwrap_or_else(|error| panic!("missing archive {}: {error}", archive.display())) + }); + let directory = tempfile::tempdir().expect("launcher bundle directory"); + let path = directory.path().join("libjett_native_launcher.a"); + std::fs::write(&path, archive).expect("materialize launcher archive"); + Launcher { + bundle: NativeLauncherBundle::linux_gnu_v1(path), + _directory: directory, + } +} + +#[test] +fn native_scalar_entry_links_and_executes_without_source_tree() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/native_scalar_entry.jett"); + let directory = tempfile::tempdir().expect("isolated execution directory"); + let artifact = build_host_executable( + &fixture, + &launcher(), + &directory.path().join("native program"), + ) + .expect("compile and link genuine Cranelift object with runtime launcher"); + let output = run_bounded(&artifact.path, directory.path()); + assert!( + output.status.success(), + "native status: {}; stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout, b""); + assert_eq!(output.stderr, b""); +} + +#[test] +fn native_actor_spawn_releases_state_with_context() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/actor_spawn.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + "spawned\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("actor_spawn"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_actor_messages_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/actor_messages.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + "hits: 5\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("actor_messages"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_graphics_authority_reaches_main() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/graphics_authority.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + "graphics authority granted\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("graphics_authority"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_actor_fixture_bodies_match_interpreter() { + for (fixture, main) in [ + ( + "actor_named_arguments.jett", + "function main() returns nothing:\n println(actor_results())\n", + ), + ( + "namespace_duplicate_leaf_actors.jett", + "function main() returns nothing:\n println(direct_duplicate_leaf_actors(), alias_duplicate_leaf_actors())\n", + ), + ( + "namespace_qualified_actors.jett", + "function main() returns nothing:\n println(direct_actor(), alias_actor())\n", + ), + ( + "numeric_literal_contexts.jett", + "function main() returns nothing:\n ByteCounter counter = spawn ByteCounter(seed: 4 + 5)\n send counter.add(10 + 11)\n println(ask counter.current)\n", + ), + ] { + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass") + .join(fixture), + ) + .unwrap(); + let directory = tempfile::tempdir().unwrap(); + let source_path = directory.path().join(fixture); + std::fs::write(&source_path, format!("{source}\n{main}")).unwrap(); + let expected = jett_driver::run_file_capture_output(&source_path).unwrap(); + assert!(expected.stdout.is_empty()); + let binary = directory.path().join("program"); + build_host_executable(&source_path, &launcher(), &binary) + .unwrap_or_else(|error| panic!("{fixture}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{fixture}: {actual:?}"); + assert_eq!( + actual.stdout, + expected.stdout.as_bytes(), + "{fixture}: {actual:?}" + ); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); + } +} + +#[test] +fn native_structured_concurrency_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/structured_concurrency.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + assert_eq!( + jett_driver::render_debug_events(&expected.debug_events), + "hello:ready:failed\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("structured_concurrency"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_named_function_callbacks_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/function_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("function_values"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_refinement_source_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_refinement_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_refinement_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_bytes_raw_source_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_bytes_raw_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_bytes_raw_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_bitfield_source_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_bitfield_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_bitfield_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_enum_raw_source_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_enum_raw_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_enum_raw_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_pipeline_source_matches_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/json_pipeline_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_pipeline_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_json_refined_record_source_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/json_refined_record_source.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("json_refined_record_source"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +#[test] +fn native_list_source_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/list_source_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("list_source_values"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + jett_driver::render_debug_events(&expected.debug_events).as_bytes() + ); +} + +struct ExecutionChild(std::process::Child); + +impl Drop for ExecutionChild { + fn drop(&mut self) { + // Also runs on poll errors and timeout panics. No reader threads outlive + // the child, and an already reaped Child will not signal a reused PID. + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +fn run_bounded(executable: &Path, directory: &Path) -> std::process::Output { + run_bounded_with_timeout(executable, directory, std::time::Duration::from_secs(10)) +} + +fn run_bounded_with_timeout( + executable: &Path, + directory: &Path, + timeout: std::time::Duration, +) -> std::process::Output { + use std::io::Read; + use std::process::Stdio; + use std::time::{Duration, Instant}; + // A descendant retaining stdout/stderr cannot keep a pipe reader alive. + let mut stdout = tempfile::NamedTempFile::new().unwrap(); + let mut stderr = tempfile::NamedTempFile::new().unwrap(); + let mut child = ExecutionChild( + Command::new(executable) + .current_dir(directory) + .env_clear() + .stdin(Stdio::null()) + .stdout(stdout.reopen().unwrap()) + .stderr(stderr.reopen().unwrap()) + .spawn() + .expect("execute native artifact"), + ); + let start = Instant::now(); + let status = loop { + if let Some(status) = child.0.try_wait().expect("poll native artifact") { + break status; + } + assert!( + start.elapsed() < timeout, + "native executable exceeded {timeout:?} deadline" + ); + std::thread::sleep(Duration::from_millis(10)); + }; + let snapshot = |file: &mut std::fs::File| { + let length = file.metadata().unwrap().len(); + let mut bytes = Vec::new(); + file.take(length).read_to_end(&mut bytes).unwrap(); + bytes + }; + std::process::Output { + status, + stdout: snapshot(stdout.as_file_mut()), + stderr: snapshot(stderr.as_file_mut()), + } +} + +#[test] +fn native_explicit_comptime_is_baked_before_runtime_codegen() { + let directory = tempfile::tempdir().unwrap(); + let source = directory.path().join("constant.jett"); + std::fs::write(&source, "function main() returns nothing:\n int64 baked = comptime math.factorial(5)\n while baked != 120:\n int64 unexpected = 0\n return nothing\n").unwrap(); + let binary = directory.path().join("constant"); + build_host_executable(&source, &launcher(), &binary) + .expect("bake pure stdlib computation and link"); + std::fs::remove_file(&source).unwrap(); + let output = run_bounded(&binary, directory.path()); + assert!(output.status.success(), "{output:?}"); + assert!(output.stdout.is_empty()); + assert!(output.stderr.is_empty()); +} + +#[test] +fn native_uint32_wrapping_contract_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/runtime_fail/uint32_multiplication_overflow.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("wrapping"); + build_host_executable(&fixture, &launcher(), &binary).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success()); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert!(actual.stderr.is_empty()); + assert!(expected.debug_events.is_empty()); + + // The original fixture has no observable arithmetic result. Supplement it + // with a runtime branch so a non-wrapping result cannot pass merely by exit. + let source = directory.path().join("observe.jett"); + std::fs::write(&source, "function main() returns nothing:\n uint32 maximum = 4294967295\n uint32 wrapped = maximum * maximum\n while wrapped != 1:\n int64 unexpected = 0\n return nothing\n").unwrap(); + build_host_executable(&source, &launcher(), &binary).unwrap(); + std::fs::remove_file(source).unwrap(); + assert!(run_bounded(&binary, directory.path()).status.success()); +} + +#[test] +fn native_harness_does_not_wait_for_inherited_output_pipes() { + use std::os::unix::fs::PermissionsExt; + let directory = tempfile::tempdir().unwrap(); + let executable = directory.path().join("detached-output"); + std::fs::write( + &executable, + "#!/bin/sh\n/bin/sleep 4 &\nprintf captured\nprintf diagnostic >&2\n", + ) + .unwrap(); + std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap(); + let start = std::time::Instant::now(); + let output = run_bounded(&executable, directory.path()); + assert!( + start.elapsed() < std::time::Duration::from_secs(2), + "inherited pipes defeated the deadline" + ); + assert!(output.status.success()); + assert_eq!(output.stdout, b"captured"); + assert_eq!(output.stderr, b"diagnostic"); +} + +#[test] +fn native_harness_timeout_reaps_the_direct_child() { + use std::os::unix::fs::PermissionsExt; + use std::time::{Duration, Instant}; + let directory = tempfile::tempdir().unwrap(); + let executable = directory.path().join("timeout"); + std::fs::write( + &executable, + "#!/bin/sh\nprintf '%s' \"$$\" > child.pid\nexec /bin/sleep 10\n", + ) + .unwrap(); + std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap(); + let start = Instant::now(); + let result = std::panic::catch_unwind(|| { + run_bounded_with_timeout(&executable, directory.path(), Duration::from_millis(100)) + }); + assert!(result.is_err(), "stuck native child must time out"); + assert!(start.elapsed() < Duration::from_secs(2)); + let pid = std::fs::read_to_string(directory.path().join("child.pid")).unwrap(); + assert!( + !Path::new("/proc").join(pid.trim()).exists(), + "child must be reaped" + ); +} diff --git a/crates/jett_driver/tests/native_parity_manifest.rs b/crates/jett_driver/tests/native_parity_manifest.rs new file mode 100644 index 00000000..4e012f21 --- /dev/null +++ b/crates/jett_driver/tests/native_parity_manifest.rs @@ -0,0 +1,614 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use jett_common::FileId; +use jett_parser::ast::Item; +use jett_runtime::{environment, graphics}; +use serde_json::{Map, Value}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Obligation { + Lower, + ObjectEmit, + MainExecute, + RuntimeContract, +} + +impl Obligation { + fn parse(value: &str) -> Result { + match value { + "lower" => Ok(Self::Lower), + "object_emit" => Ok(Self::ObjectEmit), + "main_execute" => Ok(Self::MainExecute), + "runtime_contract" => Ok(Self::RuntimeContract), + unknown => Err(format!("unknown obligation `{unknown}`")), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ExpectedOutcome { + LowerOnly, + Success, + WrappingSuccess, + ExpectedFailure, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FixtureCategory { + RunPass, + RuntimeFail, +} + +impl ExpectedOutcome { + fn parse(value: &str) -> Result { + match value { + "lower_only" => Ok(Self::LowerOnly), + "success" => Ok(Self::Success), + "wrapping_success" => Ok(Self::WrappingSuccess), + "expected_failure" => Ok(Self::ExpectedFailure), + unknown => Err(format!("unknown expected outcome `{unknown}`")), + } + } +} + +#[derive(Debug)] +struct Fixture { + obligations: BTreeSet, + expected_outcome: ExpectedOutcome, +} + +#[derive(Debug)] +struct Manifest { + fixtures: BTreeMap, +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("workspace root should resolve") +} + +fn require_exact_fields( + object: &Map, + expected: &[&str], + context: &str, +) -> Result<(), String> { + let expected = expected.iter().copied().collect::>(); + let actual = object.keys().map(String::as_str).collect::>(); + let missing = expected.difference(&actual).copied().collect::>(); + let unknown = actual.difference(&expected).copied().collect::>(); + if missing.is_empty() && unknown.is_empty() { + Ok(()) + } else { + Err(format!( + "{context} has invalid fields; missing={missing:?}, unknown={unknown:?}" + )) + } +} + +fn required_string<'a>( + object: &'a Map, + field: &str, + context: &str, +) -> Result<&'a str, String> { + object[field] + .as_str() + .ok_or_else(|| format!("{context}.{field} must be a string")) +} + +fn validate_fixture_path(path: &str, context: &str) -> Result { + if path.contains('\\') || Path::new(path).is_absolute() { + return Err(format!( + "{context}.path must be a relative forward-slash path" + )); + } + let segments = path.split('/').collect::>(); + let ["tests", category, file_name] = segments.as_slice() else { + return Err(format!( + "{context}.path must be `tests/run_pass/*.jett` or `tests/runtime_fail/*.jett`" + )); + }; + if file_name.is_empty() || !file_name.ends_with(".jett") { + return Err(format!("{context}.path is not a supported fixture path")); + } + match *category { + "run_pass" => Ok(FixtureCategory::RunPass), + "runtime_fail" => Ok(FixtureCategory::RuntimeFail), + _ => Err(format!("{context}.path is not a supported fixture path")), + } +} + +fn parse_fixture(value: &Value, index: usize) -> Result<(String, Fixture), String> { + let context = format!("fixtures[{index}]"); + let object = value + .as_object() + .ok_or_else(|| format!("{context} must be an object"))?; + let mut fields = vec!["path", "obligations", "expected_outcome"]; + if object.contains_key("clock_test_samples") { + fields.push("clock_test_samples"); + } + if object.contains_key("random_test_samples") { + fields.push("random_test_samples"); + } + if object.contains_key("environment_test_snapshot") { + fields.push("environment_test_snapshot"); + } + if object.contains_key("graphics_test_events") { + fields.push("graphics_test_events"); + } + require_exact_fields(object, &fields, &context)?; + let scripted_count = [ + "clock_test_samples", + "random_test_samples", + "environment_test_snapshot", + "graphics_test_events", + ] + .into_iter() + .filter(|field| object.contains_key(*field)) + .count(); + if scripted_count > 1 { + return Err(format!( + "{context} cannot script multiple capabilities together" + )); + } + if let Some(snapshot) = object.get("environment_test_snapshot") { + environment::decode_test_snapshot(&snapshot.to_string()) + .map_err(|error| format!("{context}.environment_test_snapshot: {error}"))?; + } + if let Some(events) = object.get("graphics_test_events") { + graphics::decode_test_script(&events.to_string()) + .map_err(|error| format!("{context}.graphics_test_events: {error}"))?; + } + + if let Some(samples) = object.get("clock_test_samples") { + let samples = samples + .as_array() + .ok_or_else(|| format!("{context}.clock_test_samples must be an array"))?; + for (sample_index, sample) in samples.iter().enumerate() { + let sample_context = format!("{context}.clock_test_samples[{sample_index}]"); + if sample == "unavailable" { + continue; + } + let sample = sample.as_object().ok_or_else(|| { + format!("{sample_context} must be a wall sample or `unavailable`") + })?; + require_exact_fields(sample, &["wall"], &sample_context)?; + let wall = sample["wall"] + .as_object() + .ok_or_else(|| format!("{sample_context}.wall must be an object"))?; + require_exact_fields(wall, &["unix_seconds", "nanoseconds"], &sample_context)?; + wall["unix_seconds"] + .as_str() + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| { + format!("{sample_context}.wall.unix_seconds must be an i128 decimal string") + })?; + wall["nanoseconds"] + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| { + format!("{sample_context}.wall.nanoseconds must be a u32 integer") + })?; + } + } + if let Some(samples) = object.get("random_test_samples") { + let samples = samples + .as_array() + .ok_or_else(|| format!("{context}.random_test_samples must be an array"))?; + for (sample_index, sample) in samples.iter().enumerate() { + let sample_context = format!("{context}.random_test_samples[{sample_index}]"); + let sample = sample + .as_object() + .ok_or_else(|| format!("{sample_context} must be a Random sample object"))?; + if sample.contains_key("bounded") { + require_exact_fields(sample, &["bounded"], &sample_context)?; + sample["bounded"] + .as_str() + .and_then(|v| v.parse::().ok()) + .ok_or_else(|| { + format!("{sample_context}.bounded must be a u64 decimal string") + })?; + } else if sample.contains_key("unit53") { + require_exact_fields(sample, &["unit53"], &sample_context)?; + sample["unit53"] + .as_str() + .and_then(|v| v.parse::().ok()) + .ok_or_else(|| { + format!("{sample_context}.unit53 must be a u64 decimal string") + })?; + } else { + require_exact_fields(sample, &["boolean"], &sample_context)?; + sample["boolean"] + .as_bool() + .ok_or_else(|| format!("{sample_context}.boolean must be a bool"))?; + } + } + } + + let path = required_string(object, "path", &context)?.to_owned(); + let category = validate_fixture_path(&path, &context)?; + let obligation_values = object["obligations"] + .as_array() + .ok_or_else(|| format!("{context}.obligations must be an array"))?; + if obligation_values.is_empty() { + return Err(format!("{context}.obligations must not be empty")); + } + let mut obligations = BTreeSet::new(); + for (obligation_index, value) in obligation_values.iter().enumerate() { + let value = value + .as_str() + .ok_or_else(|| format!("{context}.obligations[{obligation_index}] must be a string"))?; + let obligation = Obligation::parse(value)?; + if !obligations.insert(obligation) { + return Err(format!( + "{context}.obligations contains duplicate `{value}`" + )); + } + } + let expected_outcome = + ExpectedOutcome::parse(required_string(object, "expected_outcome", &context)?)?; + + let classification_obligations = obligations + .iter() + .copied() + .filter(|obligation| *obligation != Obligation::ObjectEmit) + .collect::>(); + let object_emit_is_valid = !obligations.contains(&Obligation::ObjectEmit) + || (category == FixtureCategory::RunPass && obligations.contains(&Obligation::Lower)); + let lower_only = BTreeSet::from([Obligation::Lower]); + let main_execute = BTreeSet::from([Obligation::Lower, Obligation::MainExecute]); + let runtime_contract = BTreeSet::from([Obligation::RuntimeContract]); + let classification_is_valid = if classification_obligations == lower_only { + category == FixtureCategory::RunPass && expected_outcome == ExpectedOutcome::LowerOnly + } else if classification_obligations == main_execute { + category == FixtureCategory::RunPass + && matches!( + expected_outcome, + ExpectedOutcome::Success | ExpectedOutcome::ExpectedFailure + ) + } else if classification_obligations == runtime_contract { + category == FixtureCategory::RuntimeFail + && matches!( + expected_outcome, + ExpectedOutcome::WrappingSuccess | ExpectedOutcome::ExpectedFailure + ) + } else { + false + }; + if !object_emit_is_valid || !classification_is_valid { + return Err(format!( + "{context} has an invalid path/obligation/outcome combination" + )); + } + + Ok(( + path, + Fixture { + obligations, + expected_outcome, + }, + )) +} + +fn parse_manifest(source: &str) -> Result { + let value: Value = serde_json::from_str(source) + .map_err(|error| format!("manifest is not valid JSON: {error}"))?; + let object = value + .as_object() + .ok_or_else(|| "manifest root must be an object".to_owned())?; + require_exact_fields(object, &["version", "fixtures"], "manifest")?; + if object["version"].as_u64() != Some(1) { + return Err("manifest.version must be the integer 1".to_owned()); + } + let fixture_values = object["fixtures"] + .as_array() + .ok_or_else(|| "manifest.fixtures must be an array".to_owned())?; + let mut fixtures = BTreeMap::new(); + for (index, value) in fixture_values.iter().enumerate() { + let (path, fixture) = parse_fixture(value, index)?; + if fixtures.insert(path.clone(), fixture).is_some() { + return Err(format!("manifest contains duplicate fixture path `{path}`")); + } + } + Ok(Manifest { fixtures }) +} + +fn load_manifest() -> Manifest { + let source = fs::read_to_string(workspace_root().join("tests/native_parity.json")) + .expect("native parity manifest should be readable"); + parse_manifest(&source).expect("native parity manifest should be valid") +} + +fn discovered_fixture_paths(kind: &str) -> BTreeSet { + let directory = workspace_root().join("tests").join(kind); + fs::read_dir(&directory) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display())) + .map(|entry| entry.expect("fixture directory entry should be readable")) + .filter(|entry| { + entry + .path() + .extension() + .is_some_and(|extension| extension == "jett") + }) + .map(|entry| { + format!( + "tests/{kind}/{}", + entry + .file_name() + .to_str() + .expect("fixture file names should be UTF-8") + ) + }) + .collect() +} + +fn manifest_paths_with_obligation(manifest: &Manifest, obligation: Obligation) -> BTreeSet { + manifest + .fixtures + .iter() + .filter(|(_, fixture)| fixture.obligations.contains(&obligation)) + .map(|(path, _)| path.clone()) + .collect() +} + +fn ast_main_paths(run_pass_paths: &BTreeSet) -> BTreeSet { + let root = workspace_root(); + run_pass_paths + .iter() + .filter_map(|path| { + let source = fs::read_to_string(root.join(path)) + .unwrap_or_else(|error| panic!("failed to read {path}: {error}")); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!( + parsed.errors.is_empty(), + "fixture {path} has parse errors: {:?}", + parsed.errors + ); + parsed + .module + .items + .iter() + .any( + |item| matches!(item, Item::Function(function) if function.name.name == "main"), + ) + .then(|| path.clone()) + }) + .collect() +} + +fn paths_with_outcome( + manifest: &Manifest, + obligation: Obligation, + outcome: ExpectedOutcome, +) -> BTreeSet { + manifest + .fixtures + .iter() + .filter(|(_, fixture)| { + fixture.obligations.contains(&obligation) && fixture.expected_outcome == outcome + }) + .map(|(path, _)| path.clone()) + .collect() +} + +#[test] +fn native_parity_manifest_matches_fixture_inventory() { + let manifest = load_manifest(); + + let discovered_run_pass = discovered_fixture_paths("run_pass"); + let manifested_lower = manifest_paths_with_obligation(&manifest, Obligation::Lower); + assert_eq!( + manifested_lower, discovered_run_pass, + "lowering manifest must exactly match tests/run_pass" + ); + assert_eq!(manifested_lower.len(), 182, "lowering denominator changed"); + + let manifested_object_emit = manifest_paths_with_obligation(&manifest, Obligation::ObjectEmit); + assert_eq!( + manifested_object_emit, discovered_run_pass, + "every run-pass fixture must emit a native object with reachable code" + ); + assert_eq!( + manifested_object_emit.len(), + 182, + "native object-emission coverage changed" + ); + + let discovered_mains = ast_main_paths(&discovered_run_pass); + let manifested_mains = manifest_paths_with_obligation(&manifest, Obligation::MainExecute); + assert_eq!( + manifested_mains, discovered_mains, + "main-execution manifest must exactly match AST-discovered top-level main functions" + ); + assert_eq!(manifested_mains.len(), 30, "main denominator changed"); + + let discovered_runtime = discovered_fixture_paths("runtime_fail"); + let manifested_runtime = manifest_paths_with_obligation(&manifest, Obligation::RuntimeContract); + assert_eq!( + manifested_runtime, discovered_runtime, + "runtime-contract manifest must exactly match tests/runtime_fail" + ); + assert_eq!( + manifested_runtime.len(), + 25, + "runtime-contract denominator changed" + ); + + let expected_runtime_failures = [ + "clock_provider_failure.jett", + "math_clamp_nan_bound.jett", + "math_clamp_nan_upper_bound.jett", + "math_clamp_reversed_float_bounds.jett", + "random_invalid_test_sample.jett", + "random_provider_exhausted.jett", + "range_capacity_overflow.jett", + "string_repeat_capacity_overflow.jett", + ] + .map(|name| format!("tests/runtime_fail/{name}")) + .into_iter() + .collect::>(); + let manifested_runtime_failures = paths_with_outcome( + &manifest, + Obligation::RuntimeContract, + ExpectedOutcome::ExpectedFailure, + ); + assert_eq!(manifested_runtime_failures, expected_runtime_failures); + assert_eq!(manifested_runtime_failures.len(), 8); + + let expected_wrapping_success = discovered_runtime + .difference(&expected_runtime_failures) + .cloned() + .collect::>(); + let manifested_wrapping_success = paths_with_outcome( + &manifest, + Obligation::RuntimeContract, + ExpectedOutcome::WrappingSuccess, + ); + assert_eq!(manifested_wrapping_success, expected_wrapping_success); + assert_eq!(manifested_wrapping_success.len(), 17); + + let expected_main_failures = + BTreeSet::from(["tests/run_pass/graphics_callback_runtime_error.jett".to_owned()]); + let manifested_main_failures = paths_with_outcome( + &manifest, + Obligation::MainExecute, + ExpectedOutcome::ExpectedFailure, + ); + assert_eq!(manifested_main_failures, expected_main_failures); + assert_eq!( + paths_with_outcome(&manifest, Obligation::MainExecute, ExpectedOutcome::Success).len(), + 29 + ); +} + +#[test] +fn native_parity_object_emit_obligations_emit_host_objects() { + const SIMPLE_APP_ADD_SYMBOL: &str = + "jett_v0_a9076f8b4f10f051ac43be7e160564e56e68c5278c8dd064137a42f152d4bf4f"; + + let root = workspace_root(); + let manifest = load_manifest(); + let fixture_paths = manifest_paths_with_obligation(&manifest, Obligation::ObjectEmit); + assert!( + !fixture_paths.is_empty(), + "object-emission coverage must not pass vacuously" + ); + + for path in fixture_paths { + let fixture = root.join(&path); + let first = jett_driver::native::emit_host_object_for_file(&fixture) + .unwrap_or_else(|error| panic!("failed to emit native object for {path}: {error}")); + assert_eq!(first.target(), jett_driver::native::host_target()); + assert!(!first.bytes().is_empty(), "{path} emitted an empty object"); + assert!( + !first.symbols().is_empty(), + "{path} emitted no reachable symbols" + ); + if path == "tests/run_pass/simple.jett" { + assert!( + first + .symbols() + .iter() + .any(|symbol| symbol == SIMPLE_APP_ADD_SYMBOL), + "{path} must emit the stable symbol derived from `app.add`" + ); + } + if matches!( + path.as_str(), + "tests/run_pass/simple.jett" + | "tests/run_pass/graphics_scripted.jett" + | "tests/run_pass/type_info_reflection.jett" + ) { + let second = + jett_driver::native::emit_host_object_for_file(&fixture).unwrap_or_else(|error| { + panic!("failed to repeat native object emission for {path}: {error}") + }); + assert_eq!(first.bytes(), second.bytes(), "{path} object bytes changed"); + assert_eq!(first.symbols(), second.symbols(), "{path} symbols changed"); + } + } +} + +#[test] +fn native_test_object_emits_checked_verify_and_property_bodies() { + let directory = tempfile::tempdir().expect("test source directory"); + let source = directory.path().join("test_bodies.jett"); + fs::write( + &source, + "namespace app\nverify equal_literals:\n assert 1 == 1\nproperty identity:\n given value: int64\n assert value == value\n", + ) + .expect("write checked test bodies"); + let object = + jett_driver::native::emit_host_object_for_file(&source).expect("emit native test bodies"); + assert_eq!(object.symbols().len(), 2); + assert!(!object.bytes().is_empty()); +} + +#[test] +fn native_parity_manifest_parser_rejects_malformed_entries() { + let invalid_manifests = [ + ( + "unknown root field", + r#"{"version":1,"fixtures":[],"extra":true}"#, + ), + ( + "unknown fixture field", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only","extra":true}]}"#, + ), + ( + "malformed Clock sample", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only","clock_test_samples":[{"wall":{"unix_seconds":"0","nanoseconds":-1}}]}]}"#, + ), + ( + "unknown Clock sample field", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only","clock_test_samples":[{"wall":{"unix_seconds":"0","nanoseconds":0,"extra":true}}]}]}"#, + ), + ( + "malformed Random sample", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only","random_test_samples":[{"bounded":"-1"}]}]}"#, + ), + ( + "ambiguous Random sample", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only","random_test_samples":[{"bounded":"0","boolean":true}]}]}"#, + ), + ( + "unknown obligation", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["compile"],"expected_outcome":"lower_only"}]}"#, + ), + ( + "duplicate obligation", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower","lower"],"expected_outcome":"lower_only"}]}"#, + ), + ( + "duplicate fixture", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only"},{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"lower_only"}]}"#, + ), + ( + "unknown outcome", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["lower"],"expected_outcome":"maybe"}]}"#, + ), + ( + "invalid obligation combination", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["main_execute"],"expected_outcome":"success"}]}"#, + ), + ( + "object emission without lowering", + r#"{"version":1,"fixtures":[{"path":"tests/run_pass/a.jett","obligations":["object_emit"],"expected_outcome":"lower_only"}]}"#, + ), + ( + "invalid category", + r#"{"version":1,"fixtures":[{"path":"tests/runtime_fail/a.jett","obligations":["lower"],"expected_outcome":"lower_only"}]}"#, + ), + ("invalid version", r#"{"version":2,"fixtures":[]}"#), + ]; + + for (name, source) in invalid_manifests { + assert!( + parse_manifest(source).is_err(), + "invalid manifest case `{name}` was accepted" + ); + } +} diff --git a/crates/jett_driver/tests/native_values.rs b/crates/jett_driver/tests/native_values.rs new file mode 100644 index 00000000..62ac615a --- /dev/null +++ b/crates/jett_driver/tests/native_values.rs @@ -0,0 +1,1599 @@ +//! Production native execution, without the source tree as runtime cwd. +#![cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))] + +use jett_driver::native::{NativeLauncherBundle, build_host_executable}; +use jett_driver::{RunFailure, render_debug_events}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +fn launcher() -> NativeLauncherBundle { + static ARCHIVE: OnceLock = OnceLock::new(); + let archive = ARCHIVE.get_or_init(|| { + let executable = std::env::current_exe().expect("test executable"); + let profile = executable.parent().unwrap().parent().unwrap(); + let target = profile.parent().unwrap().join("native-values-launcher"); + let host = jett_driver::native::host_target(); + let profile_name = profile.file_name().unwrap().to_str().unwrap(); + let cargo_profile = if profile_name == "debug" { + "test" + } else { + profile_name + }; + let debug = target.join(&host).join(profile_name); + let status = Command::new(env!("CARGO")) + .args(["build", "-q", "-p", "jett_native_launcher", "--target-dir"]) + .arg(&target) + .args(["--target", &host, "--profile", cargo_profile]) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .status() + .expect("build target-matched launcher"); + assert!(status.success(), "launcher build failed: {status}"); + let archive = debug.join("libjett_native_launcher.a"); + assert!(archive.is_file(), "missing archive: {}", archive.display()); + archive + }); + NativeLauncherBundle::linux_gnu_v1(archive.clone()) +} + +fn run_bounded(executable: &Path, directory: &Path) -> std::process::Output { + run_bounded_input(executable, directory, None) +} + +fn run_bounded_input( + executable: &Path, + directory: &Path, + input: Option<&str>, +) -> std::process::Output { + use std::process::Stdio; + use std::time::{Duration, Instant}; + let mut child = Command::new(executable) + .current_dir(directory) + .env_clear() + .envs(input.map(|value| ("JETT_NATIVE_TEST_INPUT", value))) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("execute native artifact"); + // Drain concurrently so output larger than a pipe does not deadlock. + use std::io::Read; + let mut stdout = child.stdout.take().unwrap(); + let mut stderr = child.stderr.take().unwrap(); + let out = std::thread::spawn(move || { + let mut b = Vec::new(); + stdout.read_to_end(&mut b).unwrap(); + b + }); + let err = std::thread::spawn(move || { + let mut b = Vec::new(); + stderr.read_to_end(&mut b).unwrap(); + b + }); + let start = Instant::now(); + let status = loop { + if let Some(status) = child.try_wait().expect("poll native artifact") { + break status; + } + if start.elapsed() > Duration::from_secs(10) { + child.kill().expect("terminate stuck native artifact"); + child.wait().expect("reap stuck native artifact"); + panic!("native executable exceeded 10 second deadline"); + } + std::thread::sleep(Duration::from_millis(10)); + }; + std::process::Output { + status, + stdout: out.join().unwrap(), + stderr: err.join().unwrap(), + } +} + +fn terminal_stderr(failure: &RunFailure) -> String { + let mut text = render_debug_events(&failure.output.debug_events); + text.push_str(&failure.message); + text.push('\n'); + text +} + +#[test] +fn native_string_fixtures_match_interpreter_output() { + for name in ["hello_print", "escape_sequences"] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/run_pass/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + assert!(!expected.stdout.is_empty()); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary) + .unwrap_or_else(|error| panic!("{name}: {error:?}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_string_search_respects_grapheme_boundaries() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/string_search.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("string search oracle"); + assert!(expected.stdout.is_empty()); + assert_eq!( + render_debug_events(&expected.debug_events), + "1 2\n-1 0\n0 0\n0 2\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native string search"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_remaining_string_intrinsics_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/string_intrinsics.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("string intrinsic oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary) + .expect("compile native string intrinsics"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_string_scalar_iteration_matches_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/string_scalar_iteration.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("string loop oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native string loop"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_clock_and_stdout_capabilities_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/clock_production.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("clock oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native clock"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_unit_enums_match_interpreter() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/unit_enum.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("enum oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native enum"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_payload_enums_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/payload_enum.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("enum payload oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native enum payloads"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_bitfield_values_match_interpreter() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/bitfield_values.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("bitfield oracle"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("compile native bitfield"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +fn run_source(source: &str) -> std::process::Output { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("values.jett"); + std::fs::write(&path, source).unwrap(); + let expected = jett_driver::run_file_capture_output(&path).expect("interpreter oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("native compilation"); + std::fs::remove_file(path).unwrap(); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + actual +} + +#[test] +fn native_strings_follow_computed_control_flow_and_release_aliases() { + let output = run_source( + r#" +function label(value: int64) returns string: + string prefix = "item" + if value == 2: + return "{prefix}:{value * 3}" + return "{prefix}{value}" +function main(stdout: Stdout) returns nothing: + mutable string current = "start" + mutable int64 index = 0 + while index < 4: + string alias = current + current = label(index) + index = index + 1 + if index == 2: + continue + Stdout.write(view stdout, "{alias}->{current};") + if current == "item3" && label(2) != "wrong": + println(current, index * 7, true, 18446744073709551615) + println(1.25, false, nothing) +"#, + ); + assert_eq!(output.stdout, b"start->item0;item1->item:6;item:6->item3;"); + assert_eq!( + output.stderr, + b"item3 28 true 18446744073709551615\n1.25 false nothing\n" + ); +} + +#[test] +fn native_print_preserves_lexical_argument_effects_and_short_circuiting() { + let output = run_source( + r#" +function marked(value: int64) returns string: + print(value) + return "v{value}" +function unused() returns bool: + println("must not execute") + return true +function main() returns nothing: + bool first = false && unused() + bool second = true || unused() + println(marked(1), marked(2), first, second) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"12v1 v2 false true\n"); +} + +#[test] +fn native_terminal_failure_unwinds_compiled_string_frames() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/runtime_fail/string_repeat_capacity_overflow.jett"); + let directory = tempfile::tempdir().unwrap(); + let nested = directory.path().join("nested.jett"); + std::fs::write( + &nested, + r#" +function explode(value: string) returns string: + string alias = value + print("before:", alias) + return "left{string.repeat(value, 9223372036854775807)}right" +function relay(value: string) returns string: + return explode(value) +function main() returns nothing: + string keep = "keep" + println("left", relay("ab"), keep) + println("must not execute") +"#, + ) + .unwrap(); + for path in [&fixture, &nested] { + let expected = + jett_driver::run_file_capture_outcome(path).expect_err("terminal failure oracle"); + let binary = directory.path().join("program"); + build_host_executable(path, &launcher(), &binary).expect("compile terminal failure"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!( + String::from_utf8(actual.stderr).unwrap(), + terminal_stderr(&expected) + ); + } +} + +#[test] +fn native_unicode_leaf_operations_preserve_graphemes() { + let output = run_source( + r#" +function main() returns nothing: + string text = "é🇷🇴x" + println(string.char_count(text), string.slice(text, 0, 1), string.slice(text, 1, 2)) + println(string.slice(text, -10, 20), string.slice(text, 2, 1)) + println(string.upper("Straße"), string.lower("İ"), string.trim(" hi ")) + println(string.trim_start(" a "), string.trim_end(" b ")) + println(string.repeat("é", 3), string.repeat("x", -1), string.repeat("", 9223372036854775807)) + println(string.is_alpha("é"), string.is_numeric("12"), string.is_numeric("123")) + println(string.center("hi", 7), string.zfill("-42", 6)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .starts_with("3 é 🇷🇴\n") + ); +} + +#[test] +fn native_proven_fixture_outputs_remain_monotonic() { + for name in [ + "tests/run_pass/escape_sequences.jett", + "tests/run_pass/explicit_comptime_expression.jett", + "tests/run_pass/hello_print.jett", + "tests/run_pass/named_argument_runtime_order.jett", + "tests/run_pass/namespace_runtime_main_context.jett", + "tests/run_pass/native_scalar_entry.jett", + "tests/run_pass/stdlib_loading.jett", + "tests/run_pass/string_interpolation.jett", + "tests/runtime_fail/int16_return_overflow.jett", + "tests/runtime_fail/int32_assignment_underflow.jett", + "tests/runtime_fail/int8_expression_underflow.jett", + "tests/runtime_fail/uint16_parameter_overflow.jett", + "tests/runtime_fail/uint32_multiplication_overflow.jett", + "tests/runtime_fail/uint32_nested_expression_overflow.jett", + "tests/runtime_fail/uint8_expression_overflow.jett", + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(name); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary) + .unwrap_or_else(|error| panic!("{name}: {error}")); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "{name}"); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{name}: {actual:?}" + ); + } +} + +#[test] +fn native_numeric_intrinsics_match_wrapping_and_ieee_contracts() { + let output = run_source( + r#" +function main() returns nothing: + int64 minimum = -9223372036854775807 - 1 + println(math.abs(minimum), math.gcd(minimum, 0), math.lcm(minimum, 1)) + println(math.factorial(21), math.mod(minimum, -1), math.lcm(3037000500, 3037000501)) + println(math.min(4, -3), math.max(-4, 3), math.abs(-2.5)) + println(math.sqrt(9.0), math.pow(2.0, 5.0), math.round(-1.5), math.floor(1.9), math.ceil(1.1)) + println(math.log(1.0), math.log2(8.0), math.log10(100.0), math.clamp(3.0, 0.0, 2.0)) + println(math.sin(0.0), math.cos(0.0), math.tan(0.0), math.pi(), math.e()) + float64 nan = math.sqrt(-1.0) + println(math.min(nan, 1.0), math.max(1.0, nan), nan == nan) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .contains("-9223372036854775808 -9223372036854775808 -9223372036854775808\n") + ); +} + +#[test] +fn native_math_runtime_contract_fixtures() { + for name in [ + "math_factorial_overflow", + "math_mod_overflow", + "math_lcm_overflow", + "math_clamp_nan_bound", + "math_clamp_nan_upper_bound", + "math_clamp_reversed_float_bounds", + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../tests/runtime_fail/{name}.jett")); + let expected = jett_driver::run_file_capture_outcome(&fixture); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary) + .unwrap_or_else(|e| panic!("{name}: {e}")); + let actual = run_bounded(&binary, directory.path()); + match expected { + Ok(output) => { + assert!(actual.status.success(), "{name}: {actual:?}"); + assert_eq!(actual.stdout, output.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&output.debug_events).as_bytes() + ); + } + Err(failure) => { + assert_eq!(actual.status.code(), Some(71), "{name}: {actual:?}"); + assert_eq!(actual.stdout, failure.output.stdout.as_bytes()); + assert_eq!( + String::from_utf8(actual.stderr).unwrap(), + terminal_stderr(&failure) + ); + } + } + } +} + +#[test] +fn native_adjacent_scalar_interpolation_has_structural_capacity() { + let output = run_source( + r#" +function render(value: int64) returns string: + return "{value}{value}{value}{value}{value}{value}" +function main() returns nothing: + println(render(7)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"777777\n"); +} + +#[test] +fn native_long_interpolation_calls_print_and_cleanup() { + let scalars = "{value}".repeat(64); + let strings = "{text}".repeat(64); + let source = format!( + r#" +function render(value: int64, text: string) returns string: + return "{scalars}{strings}" +function nested(value: int64) returns string: + return "[{{"{{render(value, "x")}}"}}]" +function main() returns nothing: + mutable int64 index = 0 + while index < 3: + string text = nested(index) + string alias = text + if text == alias && text != "": + print(text) + index = index + 1 + println(nested(7), 1, 2, 3, 4, 5, 6, true, nothing) +"# + ); + let output = run_source(&source); + let mut expected = String::new(); + for value in [0, 1, 2, 7] { + expected.push_str(&format!( + "[{}{}]", + value.to_string().repeat(64), + "x".repeat(64) + )); + } + expected.push_str(" 1 2 3 4 5 6 true nothing\n"); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, expected.as_bytes()); +} + +#[test] +fn native_float32_formatting_matches_typed_oracle() { + let output = run_source( + r#" +function compute(value: float32) returns float32: + return (value + 1.0) - 1.0 +function literal() returns float32: + return 0.1 +function main() returns nothing: + mutable float32 value = 0.1 + println(value, "{value}") + value = 0.2 + println(value, compute(0.1), literal()) + float32 baked = comptime compute(0.1) + float32 computed = compute(0.1) + string text = "{computed}" + if text == "{baked}" && text != "0.1": + println("rounded", text) + else: + println("wrong precision") + float64 wide = 0.1 + println(wide) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"0.10000000149011612 0.10000000149011612\n0.20000000298023224 0.10000002384185791 0.10000000149011612\nrounded 0.10000002384185791\n0.1\n"); +} + +#[test] +fn native_float32_arithmetic_and_ieee_edges_match_oracle() { + let output = run_source( + r#" +function arithmetic(value: float32) returns float32: + return (value * value + value) / 0.3 +function cancellation(value: float32) returns float32: + return (value + 1.0) - value +function main() returns nothing: + println(arithmetic(0.1), cancellation(16777216.0)) + float32 rounded_literal = 16777217.0 + println(rounded_literal - 16777216.0) + float32 huge = 340282346638528859811704183484516925440.0 + float32 tiny = 0.0000000000000000000000000000000000000000000014 + float32 zero = 0.0 + float32 negative = -zero + float32 infinity = huge * 2.0 + float32 nan = zero / zero + println(infinity, -infinity, tiny / 2.0, negative, nan == nan) + string text = "{infinity} {negative}" + if text == "inf -0": + println("ieee") +"#, + ); + let value = 0.1_f32; + let arithmetic = ((value * value + value) / 0.3_f32) as f64; + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!( + output.stderr, + format!("{arithmetic} 0\n0\ninf -inf 0 -0 false\nieee\n").as_bytes() + ); +} + +#[test] +fn native_interpolation_ownership_bound_counts_scalar_segments() { + let output = run_source( + r#" +function compose(value: int64) returns string: + return "{value}{value + 1}{value + 2}{value + 3}{value + 4}{value + 5}{value + 6}{value + 7}{value + 8}{value + 9}{value + 10}{value + 11}" +function main() returns nothing: + println(compose(1)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"123456789101112\n"); +} + +#[test] +fn native_owned_bytes_move_borrow_clone_and_loop() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/native/bytes_ownership.jett"); + let expected = jett_driver::run_file_capture_output(&fixture).expect("bytes oracle"); + assert!(expected.stdout.is_empty()); + assert_eq!( + render_debug_events(&expected.debug_events).as_bytes(), + b"2 486921\n787878 2\n" + ); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("bytes native compilation"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); +} + +#[test] +fn native_bytes_cleanup_across_early_returns_and_short_circuit() { + let output = run_source( + r#" +function select(value: bytes, stop: bool) returns bytes: + bytes unused = bytes.from_string("discard") + if stop: + return value + return bytes.concat(value, bytes.from_string("!")) +function consume(value: bytes) returns bool: + println(bytes.to_hex(view value)) + return true +function main() returns nothing: + bytes a = select(bytes.from_string("a"), true) + bytes b = select(bytes.from_string("b"), false) + bool no = false && consume(bytes.from_string("no")) + bool yes = true || consume(bytes.from_string("no")) + println(bytes.to_hex(a), bytes.to_hex(b), no, yes) + mutable bytes current = bytes.new() + mutable int64 index = 0 + while index < 4: + current = bytes.from_string("{index}") + index = index + 1 + if index == 2: + continue + if index == 3: + break + println(bytes.to_hex(current)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"61 6221 false true\n32\n"); +} + +#[test] +fn native_terminal_failure_drops_bytes_in_nested_frames_and_arguments() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("failure.jett"); + std::fs::write( + &path, + r#" +function explode(value: bytes) returns string: + bytes other = clone value + print(bytes.to_hex(view value)) + return string.repeat("ab", 9223372036854775807) +function relay(value: bytes) returns string: + bytes local = bytes.from_string("held") + return explode(value) +function later(first: bytes, second: string) returns nothing: + println("unreachable") +function main() returns nothing: + bytes owner = bytes.from_string("x") + later(bytes.from_string("temporary"), relay(owner)) +"#, + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("compile byte cleanup"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_bytes_result_optional_fixture_functions_execute() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/bytes_operations.jett"); + let mut source = std::fs::read_to_string(fixture).unwrap(); + source.push_str( + r#" +function main() returns nothing: + println(new_bytes_test(), from_string_length_test(), to_string_test()) + println(get_valid_test(), get_oob_test(), get_negative_test()) + println(slice_test(), slice_content_test(), concat_test(), concat_length_test()) + println(slice_clamp_test(), roundtrip_test(), to_hex_test()) + println(from_hex_length_test(), from_hex_prefix_test()) + println(from_hex_invalid_message(), from_hex_odd_message(), concat_empty_test()) + println(observer_views_preserve_owner(), explicit_clone_preserves_original()) +"#, + ); + let actual = run_source(&source); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.starts_with(b"0 5 hello\n65 -1 -1\n")); +} + +#[test] +fn native_nested_owned_sum_payloads_and_handler_exits() { + let actual = run_source( + r#" +function wrap(value: bytes) returns result[optional[bytes], string]: + return ok(some(value)) +function unwrap(value: result[optional[bytes], string]) returns bytes: + optional[bytes] inner = value handle error: + return bytes.from_string(error) + bytes output = inner handle: + default bytes.from_string("none") + return output +function main() returns nothing: + result[optional[bytes], string] nested = wrap(bytes.from_string("yes")) + result[optional[bytes], string] duplicate = clone nested + bytes first = unwrap(nested) + bytes second = unwrap(duplicate) + bytes failed = unwrap(fail("bad")) + bytes absent = unwrap(ok(none)) + println(bytes.to_hex(first), bytes.to_hex(second), bytes.to_hex(failed), bytes.to_hex(absent)) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"796573 796573 626164 6e6f6e65\n"); +} + +#[test] +fn native_handler_backedges_defaults_and_binary_byte_errors() { + let actual = run_source( + r#" +function classify(raw: string) returns string: + bytes data = bytes.from_hex(raw) handle error: + return error + string decoded = bytes.to_string(data) handle error: + return error + return decoded +function main() returns nothing: + println(classify("0X41"), classify("+1"), classify("ff"), classify("00")) + mutable int64 index = 0 + mutable optional[bytes] held = some(bytes.from_string("old")) + while index < 4: + index = index + 1 + held = some(bytes.from_string("{index}")) + bytes data = bytes.from_hex("zz") handle error: + if index == 2: + continue + if index == 3: + break + default bytes.from_string(error) + println(bytes.length(data)) + bytes last = held handle: + default bytes.new() + println(bytes.to_hex(last)) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert!(actual.stderr.ends_with(b"35\n33\n")); +} + +#[test] +fn native_numeric_result_parsing_and_original_minimum_contracts() { + for name in [ + "math_abs_int64_min", + "math_gcd_int64_min", + "math_lcm_int64_min", + ] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../tests/runtime_fail/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("numeric result fixture"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes() + ); + } + run_source( + r#" +function number(raw: string) returns string: + int64 parsed = int64.from_string(raw) handle error: + return error + return "{parsed}" +function main() returns nothing: + println(number("-9223372036854775808"), number("wrong")) + uint64 large = uint64.from_string("18446744073709551615") handle error: + default 0 + float64 real = float64.from_string("-0") handle error: + default 1.0 + println(large, real) +"#, + ); +} + +#[test] +fn native_terminal_failure_bypasses_result_handler_with_live_nested_owners() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("failure.jett"); + std::fs::write( + &path, + r#" +function explode(value: bytes) returns result[bytes, string]: + string doomed = string.repeat("ab", 9223372036854775807) + return ok(bytes.concat(value, bytes.from_string(doomed))) +function relay(value: bytes) returns result[bytes, string]: + optional[bytes] held = some(bytes.from_string("held")) + result[bytes, string] outcome = explode(value) + bytes after = held handle: + default bytes.new() + print(bytes.length(after)) + return outcome +function main() returns nothing: + result[optional[bytes], string] held = ok(some(bytes.from_string("outer"))) + bytes output = relay(bytes.from_string("input")) handle error: + println("must not catch terminal failure") + default bytes.from_string(error) + optional[bytes] nested = held handle error: + return nothing + print(bytes.length(output)) +"#, + ) + .unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("sum cleanup compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_linear_argument_order_and_conditional_moves() { + let actual = run_source( + r#" +function observed(view value: bytes) returns int64: + print("observe:", bytes.to_hex(view value), ";") + return bytes.length(view value) +function marked(value: bytes) returns bytes: + print("mark:", bytes.to_hex(view value), ";") + return value +function pair(first: int64, second: bytes) returns nothing: + println(first, bytes.to_hex(view second)) +function shared(view first: bytes, second: bytes) returns nothing: + println(bytes.to_hex(view first), bytes.to_hex(view second)) +function consumed(value: bytes) returns bool: + println("consume", bytes.to_hex(view value)) + return true +function branch(flag: bool) returns nothing: + bytes owner = bytes.from_string("x") + bool used = flag && consumed(owner) + println(used) +function main() returns nothing: + bytes owner = bytes.from_string("ab") + shared(second: marked(clone owner), first: view owner) + pair(first: observed(view owner), second: owner) + bytes a = bytes.from_string("a") + bytes b = bytes.from_string("b") + pair(second: marked(b), first: observed(view a)) + branch(false) + branch(true) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"mark: 6162 ;6162 6162\nobserve: 6162 ;2 6162\nmark: 62 ;observe: 61 ;1 62\nfalse\nconsume 78\ntrue\n"); +} + +#[test] +fn native_list_access_fixture_executes_compiled_stdlib_control_flow() { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/run_pass/list_access_source.jett"); + let mut source = std::fs::read_to_string(fixture).unwrap(); + source.push_str(r#" +function main() returns nothing: + println(inferred_first_reuses_list(), inferred_first_view_parameter(), inferred_pipeline_first_reuses_list()) + println(explicit_last_reuses_list(), empty_first_and_last(), is_empty_outcomes_reuse_lists()) +"#); + let actual = run_source(&source); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"13 42 62\n33 true true\n"); +} + +#[test] +fn native_nested_lists_clone_owned_elements_and_run_stdlib_loops() { + let actual = run_source( + r#" +function main() returns nothing: + bytes data = bytes.from_string("x") + list[bytes] originals = list(data) + list[bytes] copies = clone originals + list[bytes] extended = list.append[bytes](copies, bytes.from_string("y")) + list[bytes] reversed = list.reverse[bytes](extended) + bytes first = list.first[bytes](view reversed) handle: + default bytes.new() + bytes original = list.first[bytes](view originals) handle: + default bytes.new() + println(bytes.to_hex(first), bytes.to_hex(original), list.length[bytes](view reversed)) + list[list[bytes]] nested = list(originals, reversed) + list[list[bytes]] duplicate = clone nested + list[bytes] inner = list.last[list[bytes]](view duplicate) handle: + default list.new[bytes]() + bytes last = list.last[bytes](view inner) handle: + default bytes.new() + println(bytes.to_hex(last), list.length[list[bytes]](view nested)) + list[string] repeated = list.repeat[string]("held", 3) + string text = list.last[string](view repeated) handle: + default "empty" + float64 number = list.first[float64](list(-0.0, 2.5)) handle: + default 1.0 + println(text, list.length[string](view repeated), number) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"79 78 2\n78 2\nheld 3 -0\n"); +} +#[test] +fn native_partial_list_construction_and_nested_owner_failure_cleanup() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("failure.jett"); + std::fs::write(&path, r#" +function explode(view held: list[optional[bytes]]) returns list[bytes]: + return list(bytes.from_string("first"), bytes.from_string(string.repeat("ab", 9223372036854775807)), bytes.from_string("{list.length[optional[bytes]](view held)}")) +function main() returns nothing: + list[optional[bytes]] held = list(some(bytes.from_string("held"))) + list[bytes] output = explode(view held) + println(list.length[bytes](view output), list.length[optional[bytes]](view held)) +"#).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("partial list compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_list_sum_wrapping_contracts_and_observable_results() { + for name in ["list_sum_overflow", "math_sum_overflow"] { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../tests/runtime_fail/{name}.jett")); + let expected = jett_driver::run_file_capture_output(&fixture).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("sum fixture compilation"); + let actual = run_bounded(&binary, directory.path()); + assert!(actual.status.success(), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes()); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes() + ); + } + let actual = run_source( + r#" +function main() returns nothing: + list[int64] items = list(9223372036854775807, 1) + println(list.sum[int64](view items), list.length[int64](view items)) + println(math.sum(list(9223372036854775807, 2))) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + b"-9223372036854775808 2\n-9223372036854775807\n" + ); +} + +#[test] +fn native_list_iteration_evaluates_once_and_bounds_nested_loans() { + let actual = run_source( + r#" +function produced() returns list[string]: + print("once;") + return list("a", "b", "c") +function main() returns nothing: + for value in produced(): + if value == "b": + continue + print(value) + println(";") + list[int64] original = list(1, 2, 3) + mutable int64 total = 0 + for item in view original: + for inner in view original: + total = total + item * inner + if inner == 2: + break + list[int64] moved = original + println(total, list.length[int64](view moved)) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"once;ac;\n18 3\n"); +} + +#[test] +fn native_consuming_iteration_moves_nested_elements_and_drops_remainders() { + let actual = run_source( + r#" +function first(items: list[bytes]) returns bytes: + for item in items: + return item + return bytes.new() +function main() returns nothing: + list[list[bytes]] nested = list(list(bytes.from_string("a"), bytes.from_string("b")), list(bytes.from_string("c"))) + list[bytes] flat = list.flatten[bytes](nested) + list[bytes] copied = clone flat + println(bytes.to_hex(first(flat))) + mutable int64 index = 0 + for item in copied: + index = index + 1 + if index == 1: + continue + println(bytes.to_hex(item)) + break + list[list[int64]] chunks = list.chunk[int64](list(1, 2, 3, 4, 5), 2) + for inner in chunks: + println(list.sum[int64](view inner)) +"#, + ); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!(actual.stderr, b"61\n62\n3\n7\n5\n"); +} + +#[test] +fn native_consuming_iteration_failure_drops_yield_and_unvisited_elements() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("failure.jett"); + std::fs::write(&path, r#" +function explode(value: bytes) returns string: + print(bytes.to_hex(view value)) + return string.repeat("ab", 9223372036854775807) +function main() returns nothing: + list[result[bytes, string]] items = list(ok(bytes.from_string("first")), ok(bytes.from_string("unvisited")), fail("last")) + for item in items: + bytes payload = item handle error: + return nothing + println(explode(payload)) +"#).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("consuming loop cleanup compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_iteration_reentry_empty_handlers_and_payload_widths() { + run_source( + r#" +function values(index: int64) returns list[string]: + print("build", index, ";") + if index == 0: + return list.new[string]() + return list("a", "b", "c") +function reentry() returns nothing: + mutable int64 count = 0 + while count < 3: + for item in values(count): + if item == "b": + continue + print(item) + optional[string] missing = none + string value = missing handle: + if count >= 0: + break + default "unused" + count = count + 1 + println("done") +function payloads() returns nothing: + list[int8] narrow = list(127, -128) + for value in view narrow: + println(value + 1) + list[uint8] unsigned = list(255, 0) + for value in unsigned: + println(value + 1) + list[float32] floats = list(1.25, -0.0) + for value in view floats: + println(value) +function main() returns nothing: + reentry() + payloads() + list[string] words = list("x", "y", "z") + for outer in view words: + for inner in view words: + if inner == "x": + continue + print(outer, inner) + break + if outer == "y": + break + println(list.length[string](view words)) +"#, + ); +} + +#[test] +fn native_range_boundaries_and_terminal_capacity_contract() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/run_pass/range_step_boundaries.jett"); + let mut source = std::fs::read_to_string(fixture).unwrap(); + source.push_str( + r#" +function main() returns nothing: + println(positive_step_boundary(), negative_step_boundary()) + for item in range(5): + print(item) + for item in range(5, -1, -2): + print(item) + println(list.length[int64](range(4, 2)), list.sum[int64](range(2, 6))) +"#, + ); + let actual = run_source(&source); + assert!(actual.stdout.is_empty(), "{actual:?}"); + assert_eq!( + actual.stderr, + b"9223372036854775806 -9223372036854775807\n012345310 14\n" + ); + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/runtime_fail/range_capacity_overflow.jett"); + let expected = + jett_driver::run_file_capture_outcome(&fixture).expect_err("terminal range contract"); + let directory = tempfile::tempdir().unwrap(); + let binary = directory.path().join("program"); + build_host_executable(&fixture, &launcher(), &binary).expect("range fixture compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_range_failure_cleans_nested_live_owners_without_entering_handler() { + for step in ["0", "1"] { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("range_failure.jett"); + let source = r#" +function explode(value: list[bytes], step: int64) returns result[list[int64], string]: + print(list.length[bytes](view value)) + return ok(range(0, 9223372036854775807, step)) +function main() returns nothing: + list[result[bytes, string]] outer = list(ok(bytes.from_string("held"))) + list[int64] output = explode(list(bytes.from_string("argument")), STEP) handle error: + println("not a result failure") + return nothing + println(list.length[int64](view output), list.length[result[bytes, string]](view outer)) +"# + .replace("STEP", step); + std::fs::write(&path, source).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("range cleanup compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); + } +} + +#[test] +fn native_string_list_kernels_run_real_split_and_line_fixtures() { + for (name, call) in [ + ( + "string_split_grapheme_boundaries", + "split_ignores_partial_grapheme_delimiters()", + ), + ("string_lines_mixed_endings", "lines_mixed_endings_test()"), + ( + "string_split_max", + "limited_split_preserves_remainder(), one_part_keeps_original_value(), limited_split_ignores_partial_grapheme_delimiter()", + ), + ] { + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../tests/run_pass/{name}.jett")); + let mut source = std::fs::read_to_string(fixture).unwrap(); + source.push_str(&format!( + "\nfunction main() returns nothing:\n println({call})\n" + )); + run_source(&source); + } + run_source( + r#" +function main() returns nothing: + bytes data = bytes.from_hex("65cc81f09f91a8e2808df09f91a9e2808df09f91a7e2808df09f91a6f09f87baf09f87b8") handle error: + return nothing + string text = bytes.to_string(data) handle error: + return nothing + println(string.reverse(text), string.join(string.chars(text), "|")) + println(string.join(string.words(" one\t two\nthree "), ",")) + println(string.join(string.split("ab", ""), "|")) + println(list.length[string](string.lines("")), string.join(string.lines("\r\n\nend\r"), "|")) +"#, + ); +} + +#[test] +fn native_user_struct_generic_fixture() { + let source = include_str!("../../../tests/run_pass/generic_struct.jett"); + let output = run_source(&format!( + "{source}\nfunction main() returns nothing:\n println(pair_first_test(), pair_second_test(), box_value_test())\n" + )); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"42 hello 100\n"); +} + +#[test] +fn native_handler_default_ends_exited_iteration_loan() { + let output = run_source( + r#" +function main() returns nothing: + list[string] items = list("one", "two") + optional[int64] missing = none + int64 chosen = missing handle: + for item in view items: + print(item) + default 7 + default 0 + list[string] moved = items + println(chosen, list.length[string](view moved)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"one7 2\n"); +} + +#[test] +fn native_user_struct_explicit_equality_fixture() { + let source = include_str!("../../../tests/run_pass/explicit_struct_equality.jett"); + let output = run_source(&format!( + "{source}\nfunction main() returns nothing:\n println(equality_uses_the_explicit_contract())\n" + )); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"true\n"); +} + +#[test] +fn native_user_struct_nested_owners_moves_views_clones_and_overwrite() { + let output = run_source( + r#" +struct Packet: + data: bytes + label: string + number: int64 +struct Envelope: + packet: Packet + extras: list[optional[bytes]] +function mark(value: int64) returns int64: + print(value, ";") + return value * 3 +function make(value: int64) returns Packet: + return Packet(number: mark(value), label: "p{value}", data: bytes.from_string("{value}")) +function wrap(value: int64) returns Envelope: + return Envelope(packet: make(value), extras: list(some(bytes.from_string("extra")))) +function read(view value: Envelope) returns nothing: + println(value.packet.label, value.packet.number, bytes.to_hex(view value.packet.data)) +function relay(value: Envelope, early: bool) returns Envelope: + Envelope held = wrap(9) + if early: + return value + return clone value +function main() returns nothing: + mutable Envelope current = wrap(1) + Envelope copied = clone current + current = relay(wrap(2), true) + read(view copied) + read(view current) + bytes extracted = clone current.packet.data + println(bytes.to_hex(view extracted), list.length[optional[bytes]](view current.extras)) + Envelope other = relay(current, false) + read(view other) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!( + output.stderr, + b"1 ;2 ;9 ;p1 3 31\np2 6 32\n32 1\n9 ;p2 6 32\n" + ); +} + +#[test] +fn native_user_struct_terminal_failure_cleans_partial_fields_and_callers() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("failure.jett"); + std::fs::write(&path, r#" +struct Packet: + data: bytes + text: string +struct Envelope: + packet: Packet + extra: optional[bytes] +function explode(value: Packet) returns Packet: + return Packet(data: bytes.from_string("partial"), text: string.repeat("ab", 9223372036854775807)) +function relay(value: Packet) returns Packet: + Envelope held = Envelope(packet: Packet(data: bytes.from_string("held"), text: "outer"), extra: some(bytes.new())) + return explode(value) +function main() returns nothing: + mutable Packet value = Packet(data: bytes.from_string("before"), text: "before") + println(value.text) + value = relay(clone value) + println("unreachable") +"#).unwrap(); + let expected = jett_driver::run_file_capture_outcome(&path).expect_err("terminal oracle"); + let binary = directory.path().join("program"); + build_host_executable(&path, &launcher(), &binary).expect("struct cleanup compilation"); + let actual = run_bounded(&binary, directory.path()); + assert_eq!(actual.status.code(), Some(71), "{actual:?}"); + assert_eq!(actual.stdout, expected.output.stdout.as_bytes()); + assert!(actual.stdout.is_empty()); + assert_eq!( + render_debug_events(&expected.output.debug_events), + "before\n" + ); + assert_eq!(actual.stderr, terminal_stderr(&expected).as_bytes()); +} + +#[test] +fn native_handler_default_nested_regions_and_reentry() { + let output = run_source( + r#" +function inner_exit() returns nothing: + list[string] outer = list("a", "b") + for item in view outer: + list[string] inner = list("x", "y") + optional[int64] missing = none + int64 selected = missing handle: + for nested in view inner: + print(item, nested) + default 7 + default 0 + list[string] moved = inner + println(selected, list.length[string](view moved)) + println(list.length[string](outer)) +function multiple_exit() returns nothing: + list[string] outer = list("a", "b") + list[string] inner = list("x", "y") + mutable int64 count = 0 + while count < 2: + optional[int64] missing = none + int64 selected = missing handle: + for item in view outer: + for nested in view inner: + print(item, nested) + default 8 + default 0 + println(selected) + count = count + 1 + println(list.length[string](outer), list.length[string](inner)) +function main() returns nothing: + inner_exit() + multiple_exit() +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"a x7 2\nb x7 2\n2\na x8\na x8\n2 2\n"); +} + +#[test] +fn native_handler_default_retains_outer_iteration_loan() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("invalid_move.jett"); + std::fs::write( + &path, + r#" +function main() returns nothing: + list[string] items = list("a", "b") + for item in view items: + optional[int64] missing = none + int64 selected = missing handle: + for nested in view items: + default 7 + default 0 + list[string] moved = items + println(selected, list.length[string](view moved)) + break +"#, + ) + .unwrap(); + let error = + build_host_executable(&path, &launcher(), &directory.path().join("program")).unwrap_err(); + assert!(error.to_string().contains("while borrowed"), "{error}"); +} + +#[test] +fn native_owned_field_read_copies_before_parent_is_consumed() { + let output = run_source( + r#" +struct Packet: + data: bytes +function consume(value: Packet) returns nothing: + println(bytes.to_hex(view value.data)) +function main() returns nothing: + Packet item = Packet(data: bytes.from_string("ab")) + bytes copied = item.data + consume(item) + bytes combined = bytes.concat(copied, bytes.from_string("c")) + println(bytes.to_hex(view combined)) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"6162\n616263\n"); +} + +#[test] +fn native_user_struct_projection_loans_cannot_escape_or_conflict() { + for (body, expected) in [ + ( + "bytes borrowed = view item.data\n consume(view borrowed, item)", + "after creating a local view alias is not implemented", + ), + ("consume(view item.data, item)", "while borrowed"), + ("Packet moved = item\n println(item.label)", "moved"), + ] { + let source = format!( + r#" +struct Packet: + data: bytes + label: string +function consume(view data: bytes, owner: Packet) returns nothing: + println(bytes.length(view data), owner.label) +function main() returns nothing: + Packet item = Packet(data: bytes.new(), label: "p") + {body} +"# + ); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("loan.jett"); + std::fs::write(&path, source).unwrap(); + let error = build_host_executable(&path, &launcher(), &directory.path().join("program")) + .unwrap_err(); + assert!(error.to_string().contains(expected), "{expected}: {error}"); + } +} + +#[test] +fn native_user_struct_payload_bits_and_temporary_projection() { + let output = run_source( + r#" +struct Numbers: + narrow: int8 + unsigned: uint8 + wide: uint64 + real: float32 + double: float64 + flag: bool + empty: nothing +function make(value: float32) returns Numbers: + return Numbers(narrow: -128, unsigned: 255, wide: 18446744073709551615, real: value, double: -0.0, flag: true, empty: nothing) +function main() returns nothing: + Numbers numbers = make(0.1) + println(numbers.narrow, numbers.unsigned, numbers.wide) + println(numbers.real, numbers.double, numbers.flag, numbers.empty) + println(make(1.25).real) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!( + output.stderr, + b"-128 255 18446744073709551615\n0.10000000149011612 -0 true nothing\n1.25\n" + ); +} + +#[test] +fn native_user_struct_same_object_observes_dynamic_process_input() { + let source = r#" +struct Packet: + label: string + data: bytes + number: int64 +implement Equatable for Packet: + function equals(view self: Packet, view other: Packet) returns bool: + return self.number == other.number +function make(seed: int64) returns Packet: + return Packet(label: "item{seed}", data: bytes.from_string("{seed}"), number: seed * 3) +function probe(seed: int64) returns nothing: + Packet first = make(seed) + Packet other = Packet(label: "different", data: bytes.new(), number: seed * 3) + Packet copied = clone first + println(first.label, bytes.to_hex(view copied.data), first.number) + println(first == other, first != other, Equatable.equals(view first, view other)) +"#; + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("dynamic.jett"); + std::fs::write(&path, source).unwrap(); + let lowered = jett_driver::lower_file_for_backend(&path).unwrap(); + let probe = lowered + .mir + .functions + .iter() + .find(|f| f.identity.declaration.name == "probe") + .expect("probe identity"); + let symbol = jett_codegen_cranelift::symbol_name(&probe.identity, &lowered.interner).unwrap(); + let object = jett_codegen_cranelift::emit_host_object(&lowered.mir, &lowered.interner).unwrap(); + let object_path = directory.path().join("dynamic.o"); + std::fs::write(&object_path, object.bytes).unwrap(); + // Test-only adapter exposes a generated local symbol; production ABI and + // entry restrictions are unchanged. No code or data bytes are rewritten. + // Generated functions receive an environment handle before source arguments, + // and scalar parameters include a pending-depth lane. Both are zero here. + let expose = Command::new("objcopy") + .arg(format!("--globalize-symbol={symbol}")) + .arg(&object_path) + .output() + .unwrap(); + assert!(expose.status.success(), "{expose:?}"); + let shim = directory.path().join("input.c"); + std::fs::write( + &shim, + format!( + r#" +#include +#include +extern void {symbol}(void *, uint64_t, int64_t, uint64_t); +extern uint32_t jett_rt_v1_value_status(void *); +uint32_t jett_aot_v1_entry(void *context) {{ + const char *input = getenv("JETT_NATIVE_TEST_INPUT"); + if (!input) return 1; + {symbol}(context, 0, strtoll(input, 0, 10), 0); + return jett_rt_v1_value_status(context); +}} +"# + ), + ) + .unwrap(); + let bundle = launcher(); + let binary = directory.path().join("dynamic"); + let linked = Command::new("cc") + .arg("-no-pie") + .arg(&shim) + .arg(&object_path) + .arg(&bundle.archive_path) + .args(&bundle.native_library_args) + .arg("-o") + .arg(&binary) + .output() + .unwrap(); + assert!(linked.status.success(), "{linked:?}"); + // Compile exactly once, then vary real process inputs. Only the oracle + // source changes between runs; it is never passed to native execution. + let mut outputs = std::collections::BTreeSet::new(); + for input in ["-7", "0", "11", "937"] { + std::fs::write( + &path, + format!("{source}\nfunction main() returns nothing:\n probe({input})\n"), + ) + .unwrap(); + let expected = jett_driver::run_file_capture_output(&path).unwrap(); + let actual = run_bounded_input(&binary, directory.path(), Some(input)); + assert_eq!(actual.status.code(), Some(0), "{actual:?}"); + assert_eq!(actual.stdout, expected.stdout.as_bytes(), "input {input}"); + assert_eq!( + actual.stderr, + render_debug_events(&expected.debug_events).as_bytes(), + "{actual:?}" + ); + outputs.insert(actual.stderr); + } + assert_eq!(outputs.len(), 4); +} + +#[test] +fn native_user_struct_consuming_iteration_has_owner_slot_without_allocating_body() { + let output = run_source( + r#" +struct Packet: + number: int64 + data: bytes +function first(items: list[Packet]) returns int64: + for item in items: + return item.number + return -1 +function main() returns nothing: + println(first(list(Packet(number: 7, data: bytes.new()), Packet(number: 9, data: bytes.new())))) +"#, + ); + assert!(output.stdout.is_empty(), "{output:?}"); + assert_eq!(output.stderr, b"7\n"); +} diff --git a/crates/jett_driver/tests/project_test_loading.rs b/crates/jett_driver/tests/project_test_loading.rs index 862dd765..bfa235d0 100644 --- a/crates/jett_driver/tests/project_test_loading.rs +++ b/crates/jett_driver/tests/project_test_loading.rs @@ -182,8 +182,12 @@ fn project_tests_preserve_logical_source_symlink_order() { std::os::unix::fs::symlink("../target/z_core.jett", &linked_source) .expect("create in-root source symlink"); #[cfg(windows)] - if let Err(error) = std::os::windows::fs::symlink_file("../target/z_core.jett", &linked_source) - { + if let Err(error) = std::os::windows::fs::symlink_file( + std::path::Path::new("..") + .join("target") + .join("z_core.jett"), + &linked_source, + ) { if error.kind() == std::io::ErrorKind::PermissionDenied || error.raw_os_error() == Some(1314) { diff --git a/crates/jett_driver/tests/projected_assignment.rs b/crates/jett_driver/tests/projected_assignment.rs new file mode 100644 index 00000000..8cd1acdc --- /dev/null +++ b/crates/jett_driver/tests/projected_assignment.rs @@ -0,0 +1,275 @@ +use std::fs; +use std::path::Path; + +use jett_diagnostics::Severity; +use jett_driver::{BuildResult, build_file, build_source, run_file_capture_outcome, test_file}; + +const DECLARATIONS: &str = "namespace app\nstruct Inner:\n value: int64\nstruct Record:\n inner: Inner\n label: string\nenum Choice:\n wrapped(record: Record)\nfunction make() returns Record:\n return Record(inner: Inner(value: 1), label: \"before\")\n"; +const ENTRIES: &str = "function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"must not run\")\nverify valid_check:\n assert true\nproperty valid_trials:\n given number: int64\n assert number == number\n"; + +fn rejected_cases() -> Vec<(&'static str, String, u16, &'static str)> { + [ + ( + "immutable_local", + "function invalid() returns nothing:\n Record record = make()\n (record).inner.value = 9\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "owned_parameter", + "function invalid(record: Record) returns nothing:\n record.inner.value = 9\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "mutable_view_parameter", + "function invalid(view mutable record: Record) returns nothing:\n record.inner.value = 9\n", + 401, + "cannot assign through a view because views are read-only", + ), + ( + "owned_rebind_from_view", + "function invalid(view source: Record) returns nothing:\n mutable Record alias = make()\n alias = view source\n alias.inner.value = 9\n", + 401, + "cannot rebind an owned value from a view; clone the value instead", + ), + ( + "parenthesized_owned_rebind_from_view", + "function invalid(view source: Record) returns nothing:\n mutable Record alias = make()\n ((alias)) = view source\n alias.inner.value = 9\n", + 401, + "cannot rebind an owned value from a view; clone the value instead", + ), + ( + "explicit_view", + "function invalid() returns nothing:\n mutable Record record = make()\n (view record).inner.value = 9\n", + 401, + "cannot assign through a view because views are read-only", + ), + ( + "temporary_call", + "function invalid() returns nothing:\n make().inner.value = 9\n", + 404, + "projected assignment requires an owned mutable binding", + ), + ( + "temporary_clone", + "function invalid() returns nothing:\n Record record = make()\n (clone record).inner.value = 9\n", + 404, + "projected assignment requires an owned mutable binding", + ), + ( + "borrowed_loop", + "function invalid() returns nothing:\n mutable list[Record] records = list(make())\n for record in view records:\n record.inner.value = 9\n", + 401, + "cannot assign through a view because views are read-only", + ), + ( + "borrowed_match", + "function invalid(view choice: Choice) returns nothing:\n match choice:\n wrapped(record):\n record.inner.value = 9\n", + 401, + "cannot assign through a view because views are read-only", + ), + ( + "expired_binding", + "function invalid() returns nothing:\n if false:\n mutable Record record = make()\n trace record\n Record record = make()\n record.inner.value = 9\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "actor_state", + "actor Store:\n app.Record record = make()\n receive replace:\n record.inner.value = 9\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "actor_message_view", + "actor Store:\n receive replace(view mutable record: Record):\n record.inner.value = 9\n", + 401, + "cannot assign through a view because views are read-only", + ), + ( + "actor_owned_rebind_from_view", + "actor Store:\n receive replace(view source: Record):\n mutable Record alias = make()\n alias = view source\n alias.inner.value = 9\n", + 401, + "cannot rebind an owned value from a view; clone the value instead", + ), + ( + "explicit_comptime", + "function invalid() returns int64:\n Record record = make()\n record.inner.value = 9\n return record.inner.value\nfunction requested() returns int64:\n return comptime invalid()\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "verify_local", + "verify invalid:\n Record record = make()\n record.inner.value = 9\n assert true\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ( + "property_given", + "property invalid:\n given record: Record\n record.inner.value = 9\n assert true\n", + 404, + "cannot reassign `record` because it is not mutable", + ), + ] + .into_iter() + .map(|(name, body, code, message)| (name, format!("{DECLARATIONS}{body}{ENTRIES}"), code, message)) + .collect() +} + +fn assert_rejected(built: BuildResult, name: &str, code: u16, message: &str) { + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(built.has_errors, "{name}: invalid projected write accepted"); + assert_eq!(errors.len(), 1, "{name}: {:?}", built.diagnostics); + assert_eq!( + errors[0].code.code(), + code, + "{name}: {:?}", + built.diagnostics + ); + assert_eq!(errors[0].message, message, "{name}"); +} + +#[test] +fn readonly_projected_writes_fail_before_driver_evaluation() { + for (name, source, code, message) in rejected_cases() { + assert_rejected(build_source(&source, name), name, code, message); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + assert_rejected(build_file(&path), name, code, message); + let failure = run_file_capture_outcome(&path).expect_err("readonly write prevents main"); + assert!( + failure.message.contains(&format!("E{code:04}")), + "{name}: {}", + failure.message + ); + assert!( + failure.message.contains(message), + "{name}: {}", + failure.message + ); + assert!(failure.output.stdout.is_empty(), "{name}"); + assert!(failure.output.debug_events.is_empty(), "{name}"); + let failure = test_file(&path) + .err() + .expect("readonly write prevents suites"); + assert!( + failure.contains(&format!("E{code:04}")), + "{name}: {failure}" + ); + assert!(failure.contains(message), "{name}: {failure}"); + } +} + +#[test] +fn owned_mutable_projected_writes_pass_build_only_checks() { + let source = format!( + "{DECLARATIONS}function local() returns nothing:\n mutable Record record = make()\n (record).inner.value = 9\nfunction parameter(mutable record: Record) returns nothing:\n record.label = \"after\"\n" + ); + let built = build_source(&source, "owned_mutable.jett"); + assert!(!built.has_errors, "{:?}", built.diagnostics); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + let built = build_file(&path); + assert!(!built.has_errors, "{:?}", built.diagnostics); +} + +#[test] +fn projected_reads_and_reconstructed_owners_pass_reference_suites() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/projected_read_controls.jett"); + let built = build_file(&path); + assert!(!built.has_errors, "{:?}", built.diagnostics); + let output = jett_driver::run_file_capture_output(&path).expect("projected read controls"); + assert_eq!( + output.stdout, + "original:1\ncopied:2\noriginal:1\nloop:3\nmatch:2:2\ntemporary:1\nfields:7:1\n" + ); + assert!(output.debug_events.is_empty()); + let checks = test_file(&path).expect("projected read suites"); + assert_eq!(checks.total, 2); + assert_eq!(checks.passed, 2); + assert_eq!(checks.failed, 0); + assert!( + checks + .blocks + .iter() + .any(|block| block.is_property && block.iterations.is_some_and(|count| count > 0)) + ); +} + +#[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +#[test] +fn readonly_projected_writes_preserve_existing_native_publications() { + use jett_driver::native::{ + NativeBuildError, NativeLauncherBundle, build_host_executable_with_options, + build_host_property_suite_executable, build_host_verify_suite_executable, + }; + + for (name, source, code, message) in rejected_cases().into_iter().filter(|(name, _, _, _)| { + matches!( + *name, + "mutable_view_parameter" + | "owned_rebind_from_view" + | "parenthesized_owned_rebind_from_view" + | "actor_owned_rebind_from_view" + | "temporary_call" + | "explicit_comptime" + | "verify_local" + | "property_given" + ) + }) { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing native output"; + fs::write(&path, source).unwrap(); + fs::write(&output, sentinel).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let check = |error: NativeBuildError| { + let NativeBuildError::Lowering { source, .. } = error else { + panic!("{name}: expected frontend rejection before archive lookup: {error}"); + }; + let jett_driver::BackendLoweringError::Build(built) = *source else { + panic!("{name}: expected preserved frontend diagnostics before HIR"); + }; + assert_rejected(built, name, code, message); + assert_eq!(fs::read(&output).unwrap(), sentinel, "{name}"); + }; + for release in [false, true] { + check( + build_host_executable_with_options( + &path, + &launcher, + &output, + jett_driver::BuildOptions { release }, + ) + .expect_err("readonly write prevents native publication"), + ); + } + check( + build_host_verify_suite_executable(&path, &launcher, &output) + .expect_err("readonly write prevents native verify publication"), + ); + check( + build_host_property_suite_executable(&path, &launcher, &output) + .expect_err("readonly write prevents native property publication"), + ); + } +} diff --git a/crates/jett_driver/tests/projected_field_updates_reference.rs b/crates/jett_driver/tests/projected_field_updates_reference.rs new file mode 100644 index 00000000..64ba6241 --- /dev/null +++ b/crates/jett_driver/tests/projected_field_updates_reference.rs @@ -0,0 +1,162 @@ +//! Source regressions for owned field replacement and its checked boundaries. +use std::path::PathBuf; + +use jett_driver::{build_file, run_file_capture_outcome, test_file}; + +fn fixture(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/native/field_update_reference") + .join(name) +} + +fn require_admitted(name: &str) -> PathBuf { + let path = fixture(name); + let built = build_file(&path); + assert!(!built.has_errors, "{name}: {:?}", built.diagnostics); + path +} + +#[test] +fn ordinary_field_updates_execute_from_genuine_source() { + for (name, expected) in [ + ( + "01_called_baselines.jett", + "original:1:before\nchanged:9:after\n", + ), + ("02_owning_clone_fields.jett", "2:1:before:3:3:after:true\n"), + ( + "03_rhs_order_and_early_return.jett", + "rhs\nchanged:9:before\nunchanged:1:before\n", + ), + ("04_exact_machine_state.jett", "machine:8:after:true\n"), + ( + "07_pending_children_transport.jett", + "children:17:5:again\n", + ), + ("09_generic_alias.jett", "1:9:number:new:text\n"), + ("10_refined_field_replacement.jett", "2:stable\n"), + ("22_branch_loop_lifetimes.jett", "5:first:5:second\n"), + ( + "25_secret_owned_root.jett", + "secret:1:before:2|9:after:8|1:before:2\n", + ), + ("26_for_loop_update.jett", "loop:6:1:stable\n"), + ("27_nested_handler_update.jett", "handler:1:7:stable\n"), + ( + "28_metadata_holder_replacement.jett", + "metadata:string:stable\n", + ), + ( + "29_inline_owned_parameter.jett", + "inline:1:stable|7:changed\n", + ), + ( + "30_returned_inline_owned_parameter.jett", + "returned:1:8:stable\n", + ), + ( + "31_reflected_iteration_update.jett", + "reflected:6:int64|string|int64|:1:stable\n", + ), + ] { + let path = require_admitted(name); + let outcome = run_file_capture_outcome(&path) + .unwrap_or_else(|failure| panic!("{name}: {}", failure.message)); + assert_eq!(outcome.stdout, expected, "{name}"); + assert!(outcome.debug_events.is_empty(), "{name}"); + } +} + +#[test] +fn field_update_verify_and_property_bodies_share_the_contract() { + for (name, count) in [ + ("01_called_baselines.jett", 2), + ("02_owning_clone_fields.jett", 1), + ("03_rhs_order_and_early_return.jett", 1), + ("04_exact_machine_state.jett", 1), + ("09_generic_alias.jett", 1), + ("10_refined_field_replacement.jett", 1), + ("22_branch_loop_lifetimes.jett", 1), + ("25_secret_owned_root.jett", 1), + ("26_for_loop_update.jett", 1), + ("27_nested_handler_update.jett", 1), + ("28_metadata_holder_replacement.jett", 1), + ("29_inline_owned_parameter.jett", 1), + ("30_returned_inline_owned_parameter.jett", 1), + ("31_reflected_iteration_update.jett", 1), + ] { + let path = require_admitted(name); + let report = test_file(&path).unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!(report.total, count, "{name}"); + assert_eq!(report.passed, count, "{name}"); + assert_eq!(report.failed, 0, "{name}"); + if name == "01_called_baselines.jett" { + assert!(report.blocks.iter().any(|block| { + block.is_property && block.iterations.is_some_and(|iterations| iterations > 0) + })); + } + } +} + +#[test] +fn field_update_required_comptime_has_an_independent_build_and_execution_gate() { + let path = require_admitted("23_required_comptime.jett"); + let outcome = run_file_capture_outcome(&path).expect("required comptime field update"); + assert_eq!(outcome.stdout, "baked:9\n"); + assert!(outcome.debug_events.is_empty()); + let report = test_file(&path).expect("required comptime verify body"); + assert_eq!(report.total, 1); + assert_eq!(report.passed, 1); + assert_eq!(report.failed, 0); +} + +#[test] +fn pending_ancestors_refuse_before_rhs_and_stored_child_depth_is_preserved() { + for (name, expected_stdout) in [ + ("05_pending_root_preflight.jett", "before:root\n"), + ("06_pending_intermediate_preflight.jett", "before:inner\n"), + ( + "08_stored_child_depth_refusal.jett", + "before:remaining-depth\n", + ), + ] { + let path = require_admitted(name); + let failure = run_file_capture_outcome(&path) + .expect_err("pending inspection must not complete successfully"); + assert_eq!(failure.output.stdout, expected_stdout, "{name}"); + assert!(failure.output.debug_events.is_empty(), "{name}"); + assert!(!failure.message.is_empty(), "{name}"); + } +} + +#[test] +fn selected_field_write_barriers_reject_at_the_source_boundary() { + for name in [ + "11_refined_root_rejected.jett", + "12_refined_intermediate_rejected.jett", + "13_bitfield_rejected.jett", + "14_consumed_root_rejected.jett", + "15_whole_loan_rejected.jett", + "16_field_loan_rejected.jett", + "17_rhs_root_consumption_rejected.jett", + "19_view_alias_rejected.jett", + "20_unknown_machine_state_rejected.jett", + ] { + let path = fixture(name); + let built = build_file(&path); + assert!(built.has_errors, "{name}: checked invalid field update"); + } +} + +#[test] +fn changing_a_field_invalidates_an_existing_nonzero_fact() { + // Establish the positive guard before crediting the stale-fact rejection. + let path = require_admitted("21_guarded_field_control.jett"); + let outcome = run_file_capture_outcome(&path).expect("guarded field control"); + assert_eq!(outcome.stdout, "guarded:4\n"); + let built = build_file(&fixture("18_stale_nonzero_rejected.jett")); + assert!( + built.has_errors, + "stale field nonzero proof survived replacement" + ); +} diff --git a/crates/jett_driver/tests/support/native_inventory_execution.rs b/crates/jett_driver/tests/support/native_inventory_execution.rs new file mode 100644 index 00000000..87f6aecc --- /dev/null +++ b/crates/jett_driver/tests/support/native_inventory_execution.rs @@ -0,0 +1,157 @@ +//! Manifest execution gates run in the portable native conformance suite. +use super::*; +use serde_json::Value; +use std::collections::BTreeSet; + +#[path = "native_manifest_inputs.rs"] +mod inputs; + +fn execute_fixture(row: &Value, source: &Path) -> Result<(), String> { + let clock_samples = inputs::clock_samples(row)?; + let random_samples = inputs::random_samples(row)?; + let environment_snapshot = inputs::environment_snapshot(row)?; + let graphics_events = inputs::graphics_events(row)?; + let providers = [ + clock_samples.is_some(), + random_samples.is_some(), + environment_snapshot.is_some(), + graphics_events.is_some(), + ]; + if providers.into_iter().filter(|present| *present).count() > 1 { + return Err("multiple scripted providers in one inventory row".into()); + } + + let (oracle, script) = if let Some(samples) = clock_samples { + let script = clock::encode_test_script(&samples); + ( + jett_driver::run_file_capture_outcome_with_clock_test_samples(source, samples), + Some((clock::TEST_SCRIPT_ENV, script)), + ) + } else if let Some(samples) = random_samples { + let script = random::encode_test_script(&samples); + ( + jett_driver::run_file_capture_outcome_with_random_test_samples(source, samples), + Some((random::TEST_SCRIPT_ENV, script)), + ) + } else if let Some(snapshot) = environment_snapshot { + let script = environment::encode_test_snapshot(&snapshot); + ( + jett_driver::run_file_capture_outcome_with_environment_test_snapshot(source, snapshot), + Some((environment::TEST_SNAPSHOT_ENV, script)), + ) + } else if let Some(events) = graphics_events { + let script = graphics::encode_test_script(&events); + ( + jett_driver::run_file_capture_outcome_with_graphics_test_events(source, events), + Some((graphics::TEST_SCRIPT_ENV, script)), + ) + } else { + (jett_driver::run_file_capture_outcome(source), None) + }; + let expected_failure = row["expected_outcome"] == "expected_failure"; + if oracle.is_err() != expected_failure { + return Err(format!( + "interpreter outcome contradicts inventory: {oracle:?}" + )); + } + let (output, failure) = match oracle { + Ok(output) => (output, None), + Err(failure) => (failure.output, Some(failure.message)), + }; + // Native execution observes runtime events only, in exact diagnostic order. + // Compile-phase captures are retained separately by the frontend. + let mut stderr = jett_driver::render_debug_events(&output.debug_events); + if let Some(message) = failure { + stderr.push_str(&message); + stderr.push('\n'); + } + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + let binary = directory.path().join("inventory.exe"); + let isolated_source = directory.path().join("main.jett"); + fs::copy(source, &isolated_source).map_err(|error| error.to_string())?; + build_host_executable(&isolated_source, launcher(), &binary) + .map_err(|error| error.to_string())?; + fs::remove_file(&isolated_source).map_err(|error| error.to_string())?; + let actual = run_bounded_with_env( + &binary, + directory.path(), + script.as_ref().map(|(name, value)| (*name, value.as_str())), + ); + // Entry failure 71 requires successful context destruction. Leaked owned + // values override it with cleanup failure 72; both are tested by the launcher. + let status_matches = if expected_failure { + actual.status.code() == Some(71) + } else { + actual.status.success() + }; + if !status_matches + || actual.stdout != output.stdout.as_bytes() + || actual.stderr != stderr.as_bytes() + { + return Err(format!( + "native/interpreter mismatch: native={actual:?}, expected stdout={:?}, stderr={stderr:?}, failure={expected_failure}", + output.stdout + )); + } + Ok(()) +} + +fn check_inventory(obligation: &str, success_kind: &str, successes: usize, failures: usize) { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest: Value = serde_json::from_slice( + &fs::read(root.join("tests/native_parity.json")).expect("native inventory"), + ) + .expect("native inventory JSON"); + assert_eq!(manifest["version"], 1); + let rows = manifest["fixtures"].as_array().expect("inventory rows"); + let rows = rows + .iter() + .filter(|row| { + row["obligations"] + .as_array() + .expect("fixture obligations") + .iter() + .any(|value| value == obligation) + }) + .collect::>(); + assert_eq!( + rows.len(), + successes + failures, + "{obligation} denominator changed" + ); + let mut paths = BTreeSet::new(); + let mut counts = [0, 0]; + let mut errors = Vec::new(); + for row in rows { + let path = row["path"].as_str().expect("fixture path"); + assert!(paths.insert(path), "duplicate inventory path {path}"); + match row["expected_outcome"].as_str() { + Some("expected_failure") => counts[1] += 1, + Some(kind) if kind == success_kind => counts[0] += 1, + other => panic!("{path}: invalid {obligation} outcome {other:?}"), + } + if let Err(error) = execute_fixture(row, &root.join(path)) { + errors.push(format!("{path}: {error}")); + } + } + assert_eq!( + counts, + [successes, failures], + "{obligation} outcomes changed" + ); + assert!( + errors.is_empty(), + "{obligation} execution failures:\n{}", + errors.join("\n") + ); +} + +#[test] +fn native_inventory_main_execution_matches_interpreter() { + check_inventory("main_execute", "success", 29, 1); +} + +#[test] +fn native_inventory_runtime_contracts_match_interpreter() { + check_inventory("runtime_contract", "wrapping_success", 17, 8); +} diff --git a/crates/jett_driver/tests/support/native_manifest_inputs.rs b/crates/jett_driver/tests/support/native_manifest_inputs.rs new file mode 100644 index 00000000..6b63614a --- /dev/null +++ b/crates/jett_driver/tests/support/native_manifest_inputs.rs @@ -0,0 +1,96 @@ +//! Scripted provider inputs shared by inventory execution tests and the report tool. +use jett_runtime::clock::ClockTestSample; +use jett_runtime::environment::{self, EnvironmentTestSnapshot}; +use jett_runtime::graphics::{self, TestEvent as GraphicsTestEvent}; +use jett_runtime::random::RandomTestSample; +use serde_json::Value; + +pub(crate) fn clock_samples(fixture: &Value) -> Result>, String> { + let Some(samples) = fixture.get("clock_test_samples") else { + return Ok(None); + }; + let samples = samples + .as_array() + .ok_or("clock_test_samples must be an array")?; + samples + .iter() + .map(|sample| { + if sample == "unavailable" { + return Ok(ClockTestSample::Unavailable); + } + let wall = sample.get("wall").ok_or("invalid Clock sample")?; + let unix_seconds = wall["unix_seconds"] + .as_str() + .ok_or("Clock seconds must be a decimal string")? + .parse::() + .map_err(|_| "invalid Clock seconds")?; + let subsecond_nanoseconds = wall["nanoseconds"] + .as_u64() + .ok_or("Clock nanoseconds must be an unsigned integer")? + .try_into() + .map_err(|_| "Clock nanoseconds exceed u32")?; + Ok(ClockTestSample::Wall { + unix_seconds, + subsecond_nanoseconds, + }) + }) + .collect::, &str>>() + .map(Some) + .map_err(str::to_owned) +} + +pub(crate) fn random_samples(fixture: &Value) -> Result>, String> { + let Some(samples) = fixture.get("random_test_samples") else { + return Ok(None); + }; + let samples = samples + .as_array() + .ok_or("random_test_samples must be an array")?; + samples + .iter() + .map(|sample| { + let object = sample.as_object().ok_or("invalid Random sample")?; + if let Some(value) = object.get("bounded") { + return value + .as_str() + .ok_or("bounded Random sample must be a decimal string")? + .parse::() + .map(RandomTestSample::Bounded) + .map_err(|_| "invalid bounded Random sample"); + } + if let Some(value) = object.get("unit53") { + return value + .as_str() + .ok_or("unit53 Random sample must be a decimal string")? + .parse::() + .map(RandomTestSample::Unit53) + .map_err(|_| "invalid unit53 Random sample"); + } + if let Some(value) = object.get("boolean") { + return value + .as_bool() + .map(RandomTestSample::Boolean) + .ok_or("invalid boolean Random sample"); + } + Err("invalid Random sample") + }) + .collect::, &str>>() + .map(Some) + .map_err(str::to_owned) +} + +pub(crate) fn environment_snapshot( + fixture: &Value, +) -> Result, String> { + fixture + .get("environment_test_snapshot") + .map(|value| environment::decode_test_snapshot(&value.to_string()).map_err(str::to_owned)) + .transpose() +} + +pub(crate) fn graphics_events(fixture: &Value) -> Result>, String> { + fixture + .get("graphics_test_events") + .map(|events| graphics::decode_test_script(&events.to_string()).map(Vec::from)) + .transpose() +} diff --git a/crates/jett_driver/tests/view_escapes.rs b/crates/jett_driver/tests/view_escapes.rs new file mode 100644 index 00000000..296b9099 --- /dev/null +++ b/crates/jett_driver/tests/view_escapes.rs @@ -0,0 +1,204 @@ +use std::fs; + +use jett_diagnostics::Severity; +use jett_driver::{BuildResult, build_source, run_file_capture_outcome, test_file}; + +const DECLARATIONS: &str = "namespace app\nfunction consume(values: list[int64]) returns int64:\n return list.length(view values)\nfunction generic_consume[T](values: list[T]) returns int64:\n return list.length[T](view values)\n"; +const ENTRIES: &str = "function main(stdout: Stdout) returns nothing:\n Stdout.write(view stdout, \"must not run\")\nverify valid_check:\n assert true\nproperty valid_trials:\n given input: int64\n assert input == input\n"; + +fn rejected_cases() -> Vec<(&'static str, String)> { + [ + ( + "borrowed_return", + "function invalid(view values: list[int64]) returns list[int64]:\n return values\n", + ), + ( + "forwarded_return", + "function invalid(view values: list[int64]) returns list[int64]:\n list[int64] first = view values\n list[int64] second = first\n return (second)\n", + ), + ( + "owned_call_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return consume(borrowed)\n", + ), + ( + "generic_call_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return generic_consume[int64](borrowed)\n", + ), + ( + "indirect_call_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n function(list[int64]) returns int64 callback = consume\n return callback(borrowed)\n", + ), + ( + "pipeline_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return borrowed into consume\n", + ), + ( + "coarsened_return", + "type Data = list[int64] where true\nfunction invalid(view values: Data) returns list[int64]:\n return coarsen values\n", + ), + ( + "declassified_return", + "function invalid(view values: secret[list[int64]]) returns list[int64]:\n return declassify values\n", + ), + ( + "return_annotation", + "function invalid(view values: list[int64]) returns view list[int64]:\n return clone values\n", + ), + ( + "unused_generic_return_annotation", + "function invalid[T](view value: T) returns view T:\n return clone value\n", + ), + ( + "before_explicit_comptime", + "function invalid(view values: list[int64]) returns list[int64]:\n return values\nfunction baking() returns string:\n return comptime string.repeat(\"a\", 9223372036854775807)\n", + ), + ( + "verify_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return consume(borrowed)\nverify invalid_check:\n list[int64] values = list(7)\n int64 observed = invalid(view values)\n assert observed == 1\n", + ), + ( + "property_alias", + "function invalid(view values: list[int64]) returns int64:\n list[int64] borrowed = view values\n return consume(borrowed)\nproperty invalid_trials:\n given input: int64\n list[int64] values = list(input)\n int64 observed = invalid(view values)\n assert observed == 1\n", + ), + ] + .into_iter() + .map(|(name, body)| (name, format!("{DECLARATIONS}{body}{ENTRIES}"))) + .collect() +} + +fn assert_rejected(built: BuildResult, name: &str) { + let errors = built + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(built.has_errors, "{name}"); + assert_eq!(errors.len(), 1, "{name}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{name}: {errors:?}"); +} + +#[test] +fn borrowed_values_cannot_escape_before_program_or_suite_execution() { + for (name, source) in rejected_cases() { + assert_rejected(build_source(&source, name), name); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + let failure = + run_file_capture_outcome(&path).expect_err("view escape prevents main from executing"); + assert!(failure.message.contains("E0401"), "{name}: {failure:?}"); + assert!(failure.output.stdout.is_empty(), "{name}: {failure:?}"); + assert!( + failure.output.debug_events.is_empty(), + "{name}: {failure:?}" + ); + let failure = test_file(&path) + .err() + .expect("view escape prevents suite execution"); + assert!(failure.contains("E0401"), "{name}: {failure}"); + } +} + +#[test] +fn lexical_verify_and_property_observations_preserve_borrowed_aliases() { + let cases = [ + ( + "verify_alias", + "verify observed_check:\n list[int64] values = list(7)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n assert list.length(view borrowed) == 1\n assert list.length(view values) == 1\n", + "observed_check", + false, + ), + ( + "property_alias", + "property observed_trials:\n given input: int64\n list[int64] values = list(input)\n list[int64] borrowed = view values\n int64 observed = consume(borrowed)\n assert observed == 1\n assert list.length(view borrowed) == 1\n assert list.length(view values) == 1\n", + "observed_trials", + true, + ), + ]; + for (name, body, block_name, is_property) in cases { + let source = format!("{DECLARATIONS}{body}"); + let built = build_source(&source, name); + assert!(!built.has_errors, "{name}: {:?}", built.diagnostics); + assert!(built.debug_observations.is_empty(), "{name}"); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + fs::write(&path, source).unwrap(); + let outcome = test_file(&path).expect("lexical observation suites should execute"); + assert_eq!( + (outcome.total, outcome.passed, outcome.failed), + (1, 1, 0), + "{name}" + ); + assert!(outcome.debug_observations.is_empty(), "{name}"); + assert_eq!(outcome.blocks.len(), 1, "{name}"); + let block = &outcome.blocks[0]; + assert_eq!(block.name, block_name, "{name}"); + assert!(block.passed, "{name}: {:?}", block.error); + assert!(block.error.is_none(), "{name}"); + assert_eq!(block.is_property, is_property, "{name}"); + assert_eq!( + block.iterations, + if is_property { Some(100) } else { None }, + "{name}" + ); + assert!(block.debug_events.is_empty(), "{name}"); + } +} + +#[cfg(all( + target_arch = "x86_64", + any( + all(target_os = "windows", target_env = "msvc"), + all(target_os = "linux", target_env = "gnu") + ) +))] +#[test] +fn borrowed_value_escapes_preserve_existing_native_publications() { + use jett_driver::native::{ + NativeBuildError, NativeLauncherBundle, build_host_executable_with_options, + build_host_property_suite_executable, build_host_verify_suite_executable, + }; + + for (name, source) in rejected_cases() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("main.jett"); + let output = directory.path().join("preserved.exe"); + let sentinel = b"existing native publication"; + fs::write(&path, source).unwrap(); + fs::write(&output, sentinel).unwrap(); + let launcher = if cfg!(windows) { + NativeLauncherBundle::windows_msvc_static_v1(directory.path().join("unused.lib")) + } else { + NativeLauncherBundle::linux_gnu_v1(directory.path().join("unused.a")) + }; + let check = |error: NativeBuildError| { + let NativeBuildError::Lowering { source, .. } = error else { + panic!("{name}: expected frontend rejection before archive lookup: {error}"); + }; + let jett_driver::BackendLoweringError::Build(built) = *source else { + panic!("{name}: expected frontend diagnostics before HIR"); + }; + assert_rejected(built, name); + assert_eq!(fs::read(&output).unwrap(), sentinel, "{name}"); + }; + for release in [false, true] { + check( + build_host_executable_with_options( + &path, + &launcher, + &output, + jett_driver::BuildOptions { release }, + ) + .expect_err("view escape prevents native publication"), + ); + } + check( + build_host_verify_suite_executable(&path, &launcher, &output) + .expect_err("view escape prevents native verify publication"), + ); + check( + build_host_property_suite_executable(&path, &launcher, &output) + .expect_err("view escape prevents native property publication"), + ); + } +} diff --git a/crates/jett_hir/Cargo.toml b/crates/jett_hir/Cargo.toml index 6f3a7823..e947f069 100644 --- a/crates/jett_hir/Cargo.toml +++ b/crates/jett_hir/Cargo.toml @@ -7,6 +7,8 @@ license.workspace = true [dependencies] jett_common.workspace = true +jett_comptime.workspace = true +jett_intrinsics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true jett_typecheck.workspace = true diff --git a/crates/jett_hir/src/actor_callable_sealing.rs b/crates/jett_hir/src/actor_callable_sealing.rs new file mode 100644 index 00000000..f0328458 --- /dev/null +++ b/crates/jett_hir/src/actor_callable_sealing.rs @@ -0,0 +1,361 @@ +//! Initial actor snapshots composed with the sole actual callable-sealer receipt. +//! Raw predecessor and pre-attachment output tables never retain their own edge. +use super::*; +use crate::reflected_callable::CheckedCallableSealing; + +#[derive(Debug)] +pub(super) struct Epoch { + checked: Arc, + sources: Arc>, + after: Program, + affected: Vec, +} + +/// An actor transition derived from the real shared token output, not a second +/// token or peer producer. Only the actual original Lowerer may finish it. +pub(crate) struct InitialCallableRenewal { + checked: Arc, + sources: Arc>, + output: Program, + affected: Vec, + temporary: Option, +} + +/// Complete peer checkpoint derived only inside the actual original finish: +/// exact retained actor stage plus the real initial sum producer. +pub(crate) struct InitialPeerCheckpoint { + checked: Arc, + sources: Arc>, + output: Program, +} +impl InitialPeerCheckpoint { + pub(crate) fn authenticate( + &self, + program: &Program, + types: &TypeInterner, + sources: &Arc>, + checked: &Arc, + ) -> Result<(), String> { + if !Arc::ptr_eq(checked, &self.checked) + || !std::ptr::eq(types, &self.checked.checked().interner) + || !Arc::ptr_eq(sources, &self.sources) + || !same_program(program, &self.output) + { + return Err( + "temporary original finalization changed its complete actual actor/sum checkpoint" + .into(), + ); + } + Ok(()) + } +} + +fn metadata_equal(left: &Program, right: &Program) -> bool { + left.resource_source == right.resource_source + && left.resource_manifest == right.resource_manifest + && left.equality_methods == right.equality_methods +} +fn same_program(left: &Program, right: &Program) -> bool { + metadata_equal(left, right) + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} +fn epoch(function: &Function) -> Option<&Arc> { + function + .actor_handler + .as_ref() + .and_then(|entry| entry.callable_epoch.as_ref()) + .or_else(|| { + function + .actor_state_views + .iter() + .find_map(|view| view.callable_epoch.as_ref()) + }) +} +fn affected(before: &Program, output: &Program) -> Result, String> { + if before.functions.len() != output.functions.len() { + return Err("actor callable sealing changed function inventory".into()); + } + let mut changed = Vec::new(); + for (original, produced) in before.functions.iter().zip(&output.functions) { + if original.id != produced.id + || original.actor_handler != produced.actor_handler + || original.actor_state_views != produced.actor_state_views + { + return Err( + "actor callable sealing changed original actor entry/view inventory".into(), + ); + } + if Snapshot::of(original) != Snapshot::of(produced) + && (original.actor_handler.is_some() || !original.actor_state_views.is_empty()) + { + changed.push(original.id); + } + } + Ok(changed) +} +fn renew_snapshots(protected: &mut Program, changed: &[FunctionId]) -> Result<(), String> { + for id in changed { + let function = protected + .functions + .get_mut(id.index() as usize) + .filter(|f| f.id == *id) + .ok_or("actor callable replay lost the protected original function")?; + let snapshot = Snapshot::of(function); + if let Some(entry) = &mut function.actor_handler { + entry.snapshot = Some(Box::new(snapshot.clone())); + } + for view in &mut function.actor_state_views { + view.constructor = Some(Box::new(snapshot.clone())); + } + } + super::validate_program(protected) + .map_err(|errors| format!("actor callable produced snapshots: {errors:?}")) +} + +pub(crate) fn renew( + program: &mut Program, + before: &Program, + types: &TypeInterner, + receipt: &CheckedCallableSealing<'_>, + original: Option<&Arc>, + construction: &mut Option, +) -> Result, String> { + receipt.authenticate_output(before, program, types)?; + super::validate_program(before) + .map_err(|errors| format!("actor callable original snapshots: {errors:?}"))?; + let mut protected = receipt.replay(before, types)?; + let changed = affected(before, &protected)?; + if changed.is_empty() { + return Ok(None); + } + let checked = original.ok_or( + "actor-changing callable sealing has no represented original-Lowerer epoch; later actor sealing remains unsupported" + )?; + if !receipt + .checked() + .is_some_and(|owner| Arc::ptr_eq(owner, checked)) + || !std::ptr::eq(types, &checked.checked().interner) + || before.resource_source != ResourceSourceArchive::empty() + || before + .functions + .iter() + .any(|function| epoch(function).is_some()) + { + return Err( + "actor callable sealing lost its exact original checked context, arena or archive" + .into(), + ); + } + let temporary = if crate::temporary_local_views::has_markers(before) { + let fresh = construction.take().ok_or( + "initial actor callable sealing with temporary-owner finalization remains unsupported without its fresh original construction" + )?; + Some(crate::temporary_local_views::prepare_original_finalization( + before, program, types, receipt, checked, fresh, + )?) + } else { + None + }; + renew_snapshots(&mut protected, &changed)?; + *program = protected.clone(); + Ok(Some(InitialCallableRenewal { + checked: checked.clone(), + sources: receipt.source_epoch().clone(), + output: protected, + affected: changed, + temporary, + })) +} + +fn preflight_attachment(program: &Program, affected: &[FunctionId]) -> Result<(), String> { + for (index, id) in affected.iter().enumerate() { + let function = program + .functions + .get(id.index() as usize) + .filter(|f| f.id == *id) + .ok_or("actor callable epoch lost an affected function")?; + if affected[..index].contains(id) + || (function.actor_handler.is_none() && function.actor_state_views.is_empty()) + || function + .actor_handler + .as_ref() + .is_some_and(|entry| entry.callable_epoch.is_some()) + || function + .actor_state_views + .iter() + .any(|view| view.callable_epoch.is_some()) + { + return Err( + "actor callable attachment has repeated or foreign original epoch inventory".into(), + ); + } + } + Ok(()) +} + +fn attach(program: &mut Program, edge: &Arc) -> Result<(), String> { + preflight_attachment(program, &edge.affected)?; + for id in &edge.affected { + let function = program + .functions + .get_mut(id.index() as usize) + .filter(|f| f.id == *id) + .ok_or("actor callable epoch lost an affected function")?; + if let Some(entry) = &mut function.actor_handler { + if entry.callable_epoch.is_some() { + return Err("repeated actor callable epochs are not represented".into()); + } + entry.callable_epoch = Some(edge.clone()); + } + for view in &mut function.actor_state_views { + if view.callable_epoch.is_some() { + return Err( + "repeated actor constructor callable epochs are not represented".into(), + ); + } + view.callable_epoch = Some(edge.clone()); + } + } + Ok(()) +} + +/// Fixed initial finalization, after existing peers checked the same before +/// archive. Authenticate the complete retained actor stage, then run the genuine +/// initial sum producer once on it. No caller supplies an expected final table. +pub(crate) fn finish_original( + program: &mut Program, + types: &TypeInterner, + renewal: InitialCallableRenewal, +) -> Result, String> { + if !std::ptr::eq(types, &renewal.checked.checked().interner) + || renewal.output.resource_source != ResourceSourceArchive::empty() + || !same_program(program, &renewal.output) + { + return Err( + "initial actor callable peers changed their protected complete checkpoint".into(), + ); + } + let mut completed = renewal.output.clone(); + crate::mutable_sum_backings::seal_program(&mut completed, types) + .map_err(|errors| format!("initial actor callable sum sealing: {errors:?}"))?; + super::validate_program(&completed) + .map_err(|errors| format!("initial actor callable snapshots: {errors:?}"))?; + crate::mutable_sum_backings::validate_program(&completed) + .map_err(|errors| format!("initial actor callable sum receipts: {errors:?}"))?; + preflight_attachment(&completed, &renewal.affected)?; + // Finalize only immutable BEFORE payloads, after complete peer preflight + // and before the retained after table exists. Ordinary APIs have no capability. + let checkpoint = InitialPeerCheckpoint { + checked: renewal.checked.clone(), + sources: renewal.sources.clone(), + output: completed.clone(), + }; + let temporary = match renewal.temporary { + Some(prepared) => { + Some(prepared.commit(&completed, types, &renewal.sources, &checkpoint)?) + } + None => None, + }; + let edge = Arc::new(Epoch { + checked: renewal.checked.clone(), + sources: renewal.sources.clone(), + after: completed.clone(), + affected: renewal.affected.clone(), + }); + attach(&mut completed, &edge)?; + super::validate_program(&completed) + .map_err(|errors| format!("initial actor callable attached snapshots: {errors:?}"))?; + *program = completed; + Ok(temporary) +} + +/// Non-executable actor metadata at one exact private Source checkpoint. +/// Existing protected restoration has already selected the represented epoch; +/// this function never searches arbitrary ancestors or copies historical bodies. +fn source_view( + sources: &Arc>, + program: &Program, + restored: &[Function], + checked: &Arc, +) -> Result, &'static str> { + super::validate_program(program).map_err(|_| "actor callable current snapshots changed")?; + crate::mutable_sum_backings::validate_program(program) + .map_err(|_| "actor callable current sum receipts changed")?; + if program.resource_source.checked_program().is_some() { + if !program.resource_source.belongs_to(checked) { + return Err("actor callable Source epoch belongs to another checked program"); + } + program + .resource_source + .validate_current(program, &checked.checked().interner) + .map_err( + |_| "actor callable current table has no exact authenticated peer checkpoint", + )?; + } else if program.resource_source != ResourceSourceArchive::empty() { + return Err("actor callable initial Source epoch acquired foreign archive views"); + } + let mut matching = Vec::>::new(); + for function in restored { + if let Some(edge) = epoch(function) { + if Arc::ptr_eq(&edge.sources, sources) + && !matching.iter().any(|old| Arc::ptr_eq(old, edge)) + { + matching.push(edge.clone()); + } + } + } + let [edge] = matching.as_slice() else { + return Err("actor callable Source comparison has no exact represented epoch"); + }; + if !Arc::ptr_eq(&edge.checked, checked) + || edge.after.resource_manifest != program.resource_manifest + || edge.after.equality_methods != program.equality_methods + { + return Err("actor callable epoch changed original session, manifest or equality table"); + } + let mut expected = edge.after.clone(); + attach(&mut expected, edge) + .map_err(|_| "actor callable epoch contains its own edge or repeated history")?; + if !crate::resource_materialization::functions_equal(&expected.functions, restored) { + return Err("actor callable Source epoch has no exact canonical/bake restoration path"); + } + let mut view = restored.to_vec(); + for id in &edge.affected { + let historical = edge + .sources + .get(id.index() as usize) + .filter(|f| f.id == *id) + .ok_or("actor callable Source epoch lost historical actor metadata")?; + let current = view + .get_mut(id.index() as usize) + .filter(|f| f.id == *id) + .ok_or("actor callable Source epoch lost restored actor metadata")?; + current.actor_handler = historical.actor_handler.clone(); + current.actor_state_views = historical.actor_state_views.clone(); + } + Ok(view) +} + +/// The historical table never leaves this private actor module as executable HIR. +pub(crate) fn source_matches( + sources: &Arc>, + program: &Program, + restored: &[Function], + original: &[Function], + checked: &Arc, +) -> Result<(), &'static str> { + let historical = source_view(sources, program, restored, checked)?; + if historical.len() != original.len() { + return Err("actor callable Source epoch changed the complete function inventory"); + } + if !crate::reflected_callable::source_functions_equal(&historical, original) { + return Err( + "actor callable Source comparison changed outside exact historical metadata replay", + ); + } + Ok(()) +} + +#[cfg(test)] +#[path = "actor_callable_sealing_edge_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/actor_callable_sealing_edge_tests.rs b/crates/jett_hir/src/actor_callable_sealing_edge_tests.rs new file mode 100644 index 00000000..63d9aff2 --- /dev/null +++ b/crates/jett_hir/src/actor_callable_sealing_edge_tests.rs @@ -0,0 +1,402 @@ +//! Focused controls for actual initial actor callable sealing epochs. +//! Every positive epoch comes from the unchanged checked Source and real lowerer. +use super::*; +use crate::{DeclarationKind, ExpressionKind, FunctionId}; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett" +); + +// Separate complete Source control for the exact original temporary/actor +// composition. Both original Source witnesses remain byte-identical. +const TEMPORARY_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +struct Packet: + data: list[int64] + sibling: list[int64] +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +function make_packet() returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + list[int64] borrowed = view make_packet().data + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing +"#; + +fn setup(release: bool) -> (Program, Arc, Arc) { + let file = FileId::new(0); + let checked = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "unchanged direct-handler Source; release={release}: {error:?}; {:?}", + error.diagnostics() + ) + }), + ); + let program = crate::lower_checked_ordinary_program(&checked) + .unwrap_or_else(|errors| panic!("genuine initial sealing; release={release}: {errors:?}")); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + let handler = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::ActorHandler + && function.identity.declaration.name == "Worker.read" + }) + .expect("actual checked Worker.read"); + let edge = epoch(handler) + .expect("actual initial token edits require an actor epoch") + .clone(); + assert!(Arc::ptr_eq(&edge.checked, &checked)); + assert!(edge.affected.contains(&handler.id)); + (program, checked, edge) +} + +fn same_program(left: &Program, right: &Program) -> bool { + left.resource_source == right.resource_source + && left.resource_manifest == right.resource_manifest + && left.equality_methods == right.equality_methods + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} + +#[test] +fn actual_initial_epoch_restores_only_its_exact_historical_actor_metadata() { + for release in [false, true] { + let (program, checked, edge) = setup(release); + // These retained tables must precede attachment of their own edge. + assert!( + edge.sources + .iter() + .all(|function| epoch(function).is_none()) + ); + assert!( + edge.after + .functions + .iter() + .all(|function| epoch(function).is_none()) + ); + assert_eq!(edge.after.resource_source, ResourceSourceArchive::empty()); + let view = source_view(&edge.sources, &program, &program.functions, &checked).unwrap(); + assert!(crate::reflected_callable::source_functions_equal( + &view, + &edge.sources + )); + assert!(!crate::resource_materialization::functions_equal( + &view, + &program.functions + )); + for id in &edge.affected { + let index = id.index() as usize; + assert_eq!(view[index].actor_handler, edge.sources[index].actor_handler); + assert_eq!( + view[index].actor_state_views, + edge.sources[index].actor_state_views + ); + let mut preserve_actual = program.functions[index].clone(); + preserve_actual.actor_handler = view[index].actor_handler.clone(); + preserve_actual.actor_state_views = view[index].actor_state_views.clone(); + assert!(crate::resource_materialization::functions_equal( + &[preserve_actual], + &[view[index].clone()] + )); + } + } +} + +#[test] +fn equal_source_table_copies_cannot_impersonate_the_private_source_epoch() { + for release in [false, true] { + let (program, checked, edge) = setup(release); + let copied = Arc::new(edge.sources.as_ref().clone()); + assert!(!Arc::ptr_eq(&copied, &edge.sources)); + assert!(crate::resource_materialization::functions_equal( + &copied, + &edge.sources + )); + assert!(source_view(&copied, &program, &program.functions, &checked).is_err()); + source_view(&edge.sources, &program, &program.functions, &checked).unwrap(); + } +} + +#[test] +fn actor_epoch_clones_share_identity_and_all_private_strong_edges_expire() { + for release in [false, true] { + let (weak_edge, weak_sources, weak_checked) = { + let (program, checked, edge) = setup(release); + let cloned = program.clone(); + let handler = cloned + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::ActorHandler + && function.identity.declaration.name == "Worker.read" + }) + .unwrap(); + assert!(Arc::ptr_eq(epoch(handler).unwrap(), &edge)); + let weak_edge = Arc::downgrade(&edge); + let weak_sources = Arc::downgrade(&edge.sources); + let weak_checked = Arc::downgrade(&checked); + drop(program); + drop(edge); + assert!( + weak_edge.upgrade().is_some(), + "the actual Program clone keeps its original epoch" + ); + { + let held = weak_edge.upgrade().unwrap(); + source_view(&held.sources, &cloned, &cloned.functions, &checked).unwrap(); + } + drop(cloned); + assert!( + weak_edge.upgrade().is_none(), + "the completed epoch must not own itself" + ); + assert!( + weak_sources.upgrade().is_none(), + "raw Source tables must not cycle through sealed plans" + ); + drop(checked); + (weak_edge, weak_sources, weak_checked) + }; + assert!(weak_edge.upgrade().is_none()); + assert!(weak_sources.upgrade().is_none()); + assert!( + weak_checked.upgrade().is_none(), + "the complete dropped HIR graph must release its checked Source" + ); + } +} + +fn corrupt(functions: &mut Vec, edit: usize) { + let sibling = functions + .iter() + .position(|function| { + function.identity.declaration.kind == DeclarationKind::Function + && function.identity.declaration.name == "five" + }) + .unwrap(); + let handler = functions + .iter() + .position(|function| function.actor_handler.is_some()) + .unwrap(); + match edit { + 0 => { + let mut changed = false; + crate::resource_materialization::walk_block( + &mut functions[sibling].body, + &mut |expression| { + if let ExpressionKind::Int(value) = &mut expression.kind { + if !changed { + *value += 1; + changed = true; + } + } + Ok(()) + }, + ) + .unwrap(); + assert!(changed, "real five body must contain its literal"); + } + 1 => functions[sibling].capture_count += 1, + 2 => functions[handler].body.span.end += 1, + 3 => functions[handler].body.statements.reverse(), + 4 => { + let mut extra = functions[sibling].clone(); + extra.id = FunctionId::new(functions.len() as u32); + extra.identity.declaration.name = "$unsupported.appended".into(); + functions.push(extra); + } + _ => unreachable!(), + } +} + +#[test] +fn source_epoch_refuses_changed_siblings_headers_bodies_and_appended_functions() { + for release in [false, true] { + let (program, checked, edge) = setup(release); + for edit in 0..5 { + // A private epoch view is exact for the complete restored table, + // rather than only checking the protected actor or a Source prefix. + let mut restored = program.functions.clone(); + corrupt(&mut restored, edit); + assert!( + source_view(&edge.sources, &program, &restored, &checked).is_err(), + "restored edit {edit}" + ); + // Current HIR must independently remain at an authenticated checkpoint. + let mut changed = program.clone(); + corrupt(&mut changed.functions, edit); + let before = changed.clone(); + assert!( + source_view(&edge.sources, &changed, &program.functions, &checked).is_err(), + "current edit {edit}" + ); + assert!(same_program(&changed, &before)); + } + source_view(&edge.sources, &program, &program.functions, &checked).unwrap(); + } +} + +#[test] +fn source_epoch_refuses_foreign_checked_programs_and_foreign_epoch_attachments() { + for release in [false, true] { + let (program, checked, edge) = setup(release); + let (foreign, foreign_checked, foreign_edge) = setup(release); + assert!(!Arc::ptr_eq(&checked, &foreign_checked)); + assert!(!Arc::ptr_eq(&edge, &foreign_edge)); + assert!( + source_view( + &edge.sources, + &program, + &program.functions, + &foreign_checked + ) + .is_err() + ); + assert!( + source_view( + &foreign_edge.sources, + &program, + &program.functions, + &checked + ) + .is_err() + ); + assert!( + source_view( + &edge.sources, + &foreign, + &foreign.functions, + &foreign_checked + ) + .is_err() + ); + let mut restored = program.functions.clone(); + let handler = restored + .iter_mut() + .find(|function| function.actor_handler.is_some()) + .unwrap(); + handler.actor_handler.as_mut().unwrap().callable_epoch = Some(foreign_edge.clone()); + assert!(source_view(&edge.sources, &program, &restored, &checked).is_err()); + source_view(&edge.sources, &program, &program.functions, &checked).unwrap(); + } +} + +#[test] +fn initial_actor_sealing_refuses_foreign_context_arena_and_later_entry_transactionally() { + for release in [false, true] { + let (program, checked, edge) = setup(release); + let (_, foreign, _) = setup(release); + // Retain the genuine producer's raw historical input. No Source syntax, + // plan, actor entry, receipt or positive epoch is constructed manually. + let mut before = edge.after.clone(); + before.functions = edge.sources.as_ref().clone(); + let types = &checked.checked().interner; + assert!(!Arc::ptr_eq(&checked, &foreign)); + assert!(!std::ptr::eq(types, &foreign.checked().interner)); + let mut wrong_context = before.clone(); + assert!( + crate::reflected_callable::seal_original_program( + &mut wrong_context, + Some(&foreign), + types, + ) + .is_err() + ); + assert!(same_program(&wrong_context, &before)); + let mut wrong_arena = before.clone(); + assert!( + crate::reflected_callable::seal_original_program( + &mut wrong_arena, + Some(&checked), + &foreign.checked().interner, + ) + .is_err() + ); + assert!(same_program(&wrong_arena, &before)); + let mut later = before.clone(); + let errors = crate::reflected_callable::seal_program(&mut later, types).unwrap_err(); + assert!(errors.iter().any(|error| { + error + .message + .contains("later actor sealing remains unsupported") + })); + assert!(same_program(&later, &before)); + let file = FileId::new(0); + let temporary_checked = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(TEMPORARY_SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("separate temporary composition must check: {error:?}")), + ); + let mut temporary_program = crate::lower_checked_ordinary_program(&temporary_checked) + .unwrap_or_else(|errors| panic!("unchanged complete Packet composition: {errors:?}")); + let temporary_types = &temporary_checked.checked().interner; + crate::validate(&temporary_program).unwrap(); + crate::validate_backend_types(&temporary_program, temporary_types).unwrap(); + temporary_program + .resource_source + .validate_current(&temporary_program, temporary_types) + .unwrap(); + let temporary_handler = temporary_program + .functions + .iter() + .find(|f| f.actor_handler.is_some()) + .unwrap(); + let temporary_edge = epoch(temporary_handler) + .expect("real combined actor epoch") + .clone(); + source_view( + &temporary_edge.sources, + &temporary_program, + &temporary_program.functions, + &temporary_checked, + ) + .unwrap(); + let weak_edge = Arc::downgrade(&temporary_edge); + let weak_source = Arc::downgrade(&temporary_edge.sources); + crate::complete_value_conversions(&mut temporary_program, temporary_types).unwrap(); + crate::validate_backend_types(&temporary_program, temporary_types).unwrap(); + let retained_archive = temporary_program.resource_source.clone(); + drop(temporary_edge); + drop(temporary_program); + assert!(weak_edge.upgrade().is_some()); + drop(retained_archive); + assert!(weak_edge.upgrade().is_none()); + assert!(weak_source.upgrade().is_none()); + // Refusals cannot alter the actual original program or its private edge. + source_view(&edge.sources, &program, &program.functions, &checked).unwrap(); + } +} diff --git a/crates/jett_hir/src/actor_callable_sealing_tests.rs b/crates/jett_hir/src/actor_callable_sealing_tests.rs new file mode 100644 index 00000000..27708faa --- /dev/null +++ b/crates/jett_hir/src/actor_callable_sealing_tests.rs @@ -0,0 +1,226 @@ +//! Genuine Source regression for the initial actor/callable sealing boundary. +//! This module creates no actor, reflected callable or sealing proof manually. +use crate::{ + CallOwnership, DeclarationKind, Expression, ExpressionKind, Function, HandleKind, LocalId, + Program, ReflectedCallablePlan, ReflectedFieldAction, ReflectedFieldValidation, StatementKind, +}; +use jett_common::{FileId, SourceOrigin}; +use jett_resolve::DefKind; +use jett_typecheck::{CheckOptions, CheckedActorHandlerTarget, CheckedResourceProgram}; +use jett_types::Type; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/actor_handler_direct_getter.jett" +); + +fn checked(release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| { + panic!( + "direct-handler Source must check before lowering; release={release}: {error:?}; {:?}", + error.diagnostics() + ) + }), + ) +} + +fn same_program(left: &Program, right: &Program) -> bool { + left.resource_source == right.resource_source + && left.resource_manifest == right.resource_manifest + && left.equality_methods == right.equality_methods + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} + +fn assert_actual_handler_getter_and_adapter( + program: &Program, + checked: &Arc, +) -> ReflectedCallablePlan { + let types = &checked.checked().interner; + crate::validate(program).unwrap(); + crate::validate_backend_types(program, types).unwrap(); + assert!(program.resource_source.belongs_to(checked)); + program + .resource_source + .validate_current(program, types) + .unwrap(); + + let actors = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| definition.kind == DefKind::Actor && definition.name == "app.Worker") + .collect::>(); + let [actor] = actors.as_slice() else { + panic!("one exact original Worker declaration"); + }; + let actor_type = checked.checked().definition_types[&actor.id]; + let Type::Actor(actor_id) = types.resolve(actor_type) else { + panic!("original Worker must retain its checked actor type"); + }; + let target = CheckedActorHandlerTarget { + actor_definition: actor.id, + actor: *actor_id, + handler_index: 0, + }; + let handlers = program + .functions + .iter() + .filter(|function| { + function.identity.declaration.kind == DeclarationKind::ActorHandler + && function.identity.declaration.name == "Worker.read" + }) + .collect::>(); + let [handler] = handlers.as_slice() else { + panic!("one genuine Worker.read native entry"); + }; + let handler: &Function = handler; + let entry = handler + .actor_handler + .as_ref() + .expect("original actor entry receipt"); + assert_eq!(entry.target(), target); + entry.validate(handler).unwrap(); + assert!( + checked + .checked() + .actor_handler(target) + .unwrap() + .ownership_validated() + ); + assert!(entry.roles().is_empty()); + assert!(handler.params.is_empty()); + assert_eq!(handler.capture_count, 0); + assert!(handler.source_definition.is_none()); + + let mut reads = Vec::new(); + let mut body = handler.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(fields)), + .. + } = &expression.kind + { + for (ordinal, field) in fields.iter().enumerate() { + if let ReflectedFieldAction::CallableReturn(plan) = &field.action { + reads.push((ordinal, expression.clone(), plan.clone())); + } + } + } + Ok(()) + }) + .unwrap(); + let [(ordinal, getter, plan)] = reads.as_slice() else { + panic!("the sole Holder field must create one real handler-owned reflected getter"); + }; + assert_eq!(*ordinal, 0); + assert!(plan.is_sealed()); + assert!(!plan.is_materialized(), "this probe has no comptime bake"); + assert!(plan.validate_original()); + plan.validate_types(types).unwrap(); + plan.validate_current(program, handler, getter, Some(*ordinal)) + .unwrap(); + assert_eq!(plan.original_owner(), Some(handler.id)); + let original_getter = plan.original_getter().expect("actual getter seal"); + assert_eq!(original_getter.span, getter.span); + assert_eq!(original_getter.ty, getter.ty); + assert_ne!(plan.source_type(), plan.requested_type()); + + let adapter = &program.functions[plan.function().index() as usize]; + assert_eq!(adapter.id, plan.function()); + assert_ne!(adapter.id, handler.id); + assert_eq!(plan.canonical_adapter().unwrap().id, adapter.id); + assert!(adapter.source_definition.is_none()); + assert!(adapter.actor_handler.is_none()); + assert_eq!(adapter.capture_count, 1); + assert_eq!(adapter.params.len(), 1); + assert_eq!(adapter.params[0].local, LocalId::new(0)); + assert_eq!(adapter.params[0].ty, plan.source_type()); + assert_eq!(adapter.return_type, plan.checked_plan().requested_return()); + let [statement] = adapter.body.statements.as_slice() else { + panic!("one canonical adapter return"); + }; + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + target, + kind: + HandleKind::ReflectedReturn { + plan: returned, + callable: Some(stored), + }, + .. + }, + .. + })) = &statement.kind + else { + panic!("actual reflected adapter must retain its sealed return plan"); + }; + assert_eq!( + stored, plan, + "getter and adapter must retain the same actual token" + ); + assert_eq!(returned.as_ref(), plan.return_plan()); + let ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + ownership: CallOwnership::Generated(generated), + } = &target.kind + else { + panic!("canonical adapter must invoke its original source descriptor"); + }; + assert_eq!(target.ty, plan.checked_plan().source_return()); + assert!(matches!(callee.kind, ExpressionKind::Local(local) if local == LocalId::new(0))); + assert!(args.is_empty()); + assert!(evaluation_order.is_empty()); + assert!(matches!( + &generated.operation, + crate::GeneratedOperation::ReflectedCallableAdapter { callee, plan: admission } + if *callee == LocalId::new(0) && admission == plan.checked_plan() + )); + plan.clone() +} + +#[test] +fn direct_handler_reflected_getter_survives_initial_sealing_and_idempotent_completion() { + for release in [false, true] { + let checked = checked(release); + // Do not bypass the genuine initial lowering or anticipate an error string. + // The first real failing phase establishes the RED result. + let mut program = + crate::lower_checked_ordinary_program(&checked).unwrap_or_else(|errors| { + panic!( + "genuine direct-handler initial lowering failed; release={release}: {errors:?}" + ) + }); + let plan = assert_actual_handler_getter_and_adapter(&program, &checked); + let original_source = program.resource_source.functions().to_vec(); + let initial = program.clone(); + let types = &checked.checked().interner; + for _ in 0..2 { + crate::complete_value_conversions(&mut program, types).unwrap(); + let current = assert_actual_handler_getter_and_adapter(&program, &checked); + assert_eq!( + current, plan, + "completion must preserve the actual sealed token" + ); + assert!( + same_program(&program, &initial), + "completion must be idempotent" + ); + assert!(crate::resource_materialization::functions_equal( + &original_source, + program.resource_source.functions(), + )); + } + } +} diff --git a/crates/jett_hir/src/actor_graph.rs b/crates/jett_hir/src/actor_graph.rs new file mode 100644 index 00000000..ce1e78e3 --- /dev/null +++ b/crates/jett_hir/src/actor_graph.rs @@ -0,0 +1,1375 @@ +//! Private required-value graph plans. Raw actor IDs never enter execution HIR. +use crate::evaluated_values::{ValueContext, function_value_candidates, value_expression}; +use crate::resource_source::{Materialized, RequiredMaterialization}; +use crate::{ + Block, Expression, ExpressionKind as E, Function, FunctionId, FunctionIdentity, Local, LocalId, + LowerError, Program, Statement, StatementKind as S, +}; +use jett_common::Span; +use jett_comptime::{CheckedActorGraphHook, CheckedRequiredValue, ExplicitComptimeValues, Value}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{ReflectionMetadata, Type, TypeId, TypeInterner}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, OnceLock, Weak}, +}; + +#[derive(Debug, Clone)] +pub struct ActorGraphSeeds { + ready: HashMap, + pending: HashMap<(u64, u64), LocalId>, + hooks: HashMap, + origin: Arc, +} +impl ActorGraphSeeds { + pub(crate) fn bind_reflected_source( + &self, + admitted: &jett_comptime::value::ReflectedCallableValue, + expression: &mut Expression, + ) -> Result<(), String> { + let leaf = std::ptr::from_ref(admitted).addr(); + if !self.origin.callback_leaves.contains(&leaf) { + return Err("graph callback has no exact retained checked leaf".into()); + } + let E::InterfaceCoerce { value, adapters } = &mut expression.kind else { + return Err("graph callback lost its staged checked conversion".into()); + }; + let [adapter] = adapters.as_mut_slice() else { + return Err("graph callback has no unique checked conversion".into()); + }; + let plan = adapter + .reflected + .as_mut() + .ok_or("graph callback lost checked admission")?; + plan.bind_graph_source(CallbackSource { + origin: Arc::downgrade(&self.origin), + leaf, + source: value.as_ref().clone(), + }); + Ok(()) + } + pub(crate) fn hook(&self, value: &Value, ty: TypeId) -> Result { + self.hooks + .get(&std::ptr::from_ref(value).addr()) + .filter(|expression| expression.ty == ty) + .cloned() + .ok_or_else(|| "graph hook lost its exact protected leaf/signature".into()) + } + pub fn actor_type(&self, seed: u64) -> Option { + self.ready.get(&seed).map(|entry| entry.0) + } + pub(crate) fn ready(&self, seed: u64, ty: TypeId) -> Result { + let (expected, local) = self.ready.get(&seed).ok_or("actor graph seed is absent")?; + if *expected != ty { + return Err("actor graph reference changed its exact nominal actor type".into()); + } + Ok(*local) + } + pub(crate) fn pending(&self, value: &Value, ty: TypeId) -> Result { + let (seed, depth) = + pending_actor(value).ok_or("pending actor graph carrier has no exact base seed")?; + self.ready(seed, ty)?; + self.pending + .get(&(seed, depth)) + .copied() + .ok_or_else(|| "pending actor graph reference was not reserved".into()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ActorGraphOperation { + Begin, + ReserveBase { + ty: TypeId, + fields: usize, + }, + ReservePending { + ty: TypeId, + depth: u64, + }, + Fill { + ty: TypeId, + field: usize, + node: usize, + }, + HookBox { + hook: crate::ResourceHookRef, + projection: usize, + }, + RootFill { + ty: TypeId, + }, + RootPublish { + ty: TypeId, + }, + Commit, +} +#[derive(Debug)] +struct Origin { + checked: Arc, + proof: CheckedRequiredValue, + function: FunctionId, + identity: FunctionIdentity, + original: Expression, + hooks: Vec<(CheckedActorGraphHook, crate::ResourceHookRef)>, + // All declarations/types are retained independently of caller-owned HIR. + functions: OnceLock>, + archive: crate::ResourceSourceArchive, + callback_leaves: HashSet, + callbacks: OnceLock>, +} +#[derive(Debug)] +struct Instruction { + origin: Arc, + index: usize, + operation: ActorGraphOperation, + result: TypeId, + operands: Vec<(LocalId, TypeId)>, +} +#[derive(Debug, Clone)] +pub struct ActorGraphInstruction(Arc); +impl PartialEq for ActorGraphInstruction { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for ActorGraphInstruction {} +impl ActorGraphInstruction { + /// Select only the sealed adapter associated with this exact frozen Graph leaf. + pub fn reflected_callable( + &self, + value: &jett_comptime::value::ReflectedCallableValue, + ) -> Option<&crate::ReflectedCallablePlan> { + if !Arc::ptr_eq(value.checked_program(), &self.0.origin.checked) { + return None; + } + let leaf = std::ptr::from_ref(value).addr(); + self.0 + .origin + .callbacks + .get()? + .iter() + .find_map(|(expected, plan)| { + (*expected == leaf + && plan.is_sealed() + && plan.validate_original() + && plan.checked_plan() == value.checked_plan()) + .then_some(plan) + }) + } + pub fn projected_hooks(&self) -> &[(CheckedActorGraphHook, crate::ResourceHookRef)] { + &self.0.origin.hooks + } + pub fn required_proof(&self) -> &CheckedRequiredValue { + &self.0.origin.proof + } + pub fn projected_hook(&self) -> Option<&CheckedActorGraphHook> { + let ActorGraphOperation::HookBox { projection, .. } = self.operation() else { + return None; + }; + self.0.origin.hooks.get(*projection).map(|(proof, _)| proof) + } + pub fn operation(&self) -> &ActorGraphOperation { + &self.0.operation + } + pub fn index(&self) -> usize { + self.0.index + } + pub fn same_import(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0.origin, &other.0.origin) + } + pub fn function(&self) -> FunctionId { + self.0.origin.function + } + pub fn identity(&self) -> &FunctionIdentity { + &self.0.origin.identity + } + pub fn original(&self) -> &Expression { + &self.0.origin.original + } + pub fn functions(&self) -> &[Function] { + self.0 + .origin + .functions + .get() + .expect("private graph declarations were sealed before publication") + } + pub fn checked(&self) -> &Arc { + &self.0.origin.checked + } + pub fn actor_seed_type(&self, seed: u64) -> Option { + self.0 + .origin + .proof + .actor_graph()? + .nodes() + .iter() + .find(|node| node.seed() == seed) + .map(|node| node.ty()) + } + pub fn field_value(&self) -> Result<&Value, String> { + let graph = self + .0 + .origin + .proof + .actor_graph() + .ok_or("actor graph proof lost its template")?; + if matches!(self.operation(), ActorGraphOperation::RootFill { .. }) { + return Ok(graph.root()); + } + let ActorGraphOperation::Fill { field, node, .. } = self.operation() else { + return Err("actor graph instruction has no field value".into()); + }; + let graph = self + .0 + .origin + .proof + .actor_graph() + .ok_or("actor graph proof lost its template")?; + graph + .nodes() + .get(*node) + .and_then(|node| node.captures().iter().chain(node.state()).nth(*field)) + .map(|field| field.value()) + .ok_or_else(|| "actor graph field proof is absent".into()) + } + pub fn validate( + &self, + function: FunctionId, + identity: &FunctionIdentity, + args: &[Expression], + result: TypeId, + types: &TypeInterner, + ) -> Result<(), String> { + if self.function() != function + || self.identity() != identity + || result != self.0.result + || args.len() != self.0.operands.len() + { + return Err("actor graph instruction changed its checked owner or signature".into()); + } + let origin = &self.0.origin; + origin.archive.validate_types(types)?; + if !origin.proof.belongs_to(&origin.checked) + || origin + .proof + .actor_graph() + .is_none_or(|graph| !graph.belongs_to(&origin.checked)) + { + return Err("actor graph instruction has foreign required proof".into()); + } + if let ActorGraphOperation::HookBox { hook, projection } = self.operation() { + let (proof, original_hook) = origin + .hooks + .get(*projection) + .ok_or("graph hook projection is absent")?; + let checked = proof.checked_hook(&origin.checked)?; + if hook != original_hook + || hook.function_type() != result + || hook.definition() != checked.definition + { + return Err( + "graph hook changed its protected required path or original producer".into(), + ); + } + } + for (arg, (local, ty)) in args.iter().zip(&self.0.operands) { + if arg.ty != *ty + || !matches!(arg.kind, E::Local(actual) if actual == *local) + || ty.index() as usize >= types.len() + || types.resolve(*ty) != origin.checked.checked().interner.resolve(*ty) + { + return Err( + "actor graph instruction changed its exact original local operand or type" + .into(), + ); + } + } + Ok(()) + } +} +#[derive(Debug)] +struct Plan { + origin: Arc, + body: Block, + output: LocalId, + output_type: TypeId, + instructions: Vec, + locals: Vec, + original_local_count: usize, +} +#[derive(Debug, Clone)] +pub struct ActorGraphPlan(Arc); +impl PartialEq for ActorGraphPlan { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for ActorGraphPlan {} +impl ActorGraphPlan { + pub fn required_proof(&self) -> &CheckedRequiredValue { + &self.0.origin.proof + } + pub fn factory_metadata_local(&self, local: &Local, _types: &TypeInterner) -> bool { + local.id.index() as usize >= self.0.original_local_count + && self.0.locals.get(local.id.index() as usize) == Some(local) + } + pub fn body(&self) -> &Block { + &self.0.body + } + pub fn function(&self) -> FunctionId { + self.0.origin.function + } + pub fn output(&self) -> LocalId { + self.0.output + } + pub fn output_type(&self) -> TypeId { + self.0.output_type + } + pub fn instructions(&self) -> &[ActorGraphInstruction] { + &self.0.instructions + } + pub fn validate(&self, function: &Function, types: &TypeInterner) -> Result<(), String> { + if function.id != self.0.origin.function || function.identity != self.0.origin.identity { + return Err("actor graph import changed required owner".into()); + } + self.0.origin.archive.validate_types(types)?; + let graph = self + .0 + .origin + .proof + .actor_graph() + .ok_or("actor graph plan lost template")?; + if !graph.belongs_to(&self.0.origin.checked) || graph.root_type() != self.output_type() { + return Err("actor graph plan changed checked root type".into()); + } + // Preserve the exact independently captured local headers. A checked + // machine-state value may initialize its widened machine local; value + // type compatibility remains the common backend validator's obligation. + if function.locals.get(..self.0.locals.len()) != Some(self.0.locals.as_slice()) { + return Err("actor graph plan changed its original or generated local headers".into()); + } + for instruction in &self.0.instructions { + if instruction.function() != function.id || instruction.identity() != &function.identity + { + return Err("actor graph instruction escaped its owning occurrence".into()); + } + } + if graph.root_type().index() as usize >= types.len() + || types.resolve(graph.root_type()) + != self + .0 + .origin + .checked + .checked() + .interner + .resolve(graph.root_type()) + { + return Err("actor graph root has foreign native type meaning".into()); + } + Ok(()) + } +} + +/// The common producer requires original immutable HIR and private cache proof. +/// Its only inputs are checked values and retained compiler metadata; no caller +/// can supply a graph body, positive instruction proof or actor seed map. +pub fn materialize_actor_graphs( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, + reflection: &ReflectionMetadata, + methods: &HashSet, +) -> Result<(), Vec> { + let archive = program.resource_source.clone(); + let result = (|| -> Result<(), String> { + let Some(checked) = archive.checked_program() else { + if values.has_actor_graphs() { + return Err( + "actor graph materialization requires the original checked program".into(), + ); + } + return Ok(()); + }; + let proofs = values.checked_required_values(checked)?; + if !proofs.iter().any(|proof| proof.actor_graph().is_some()) { + return Ok(()); + } + archive.validate_current(program, types)?; + let original_functions = program.functions.clone(); + let candidates = function_value_candidates(&original_functions, methods); + let mut functions = original_functions.clone(); + let mut rows = archive.required_materializations().to_vec(); + for function_index in 0..original_functions.len() { + let owner = functions[function_index].clone(); + let mut occurrences = Vec::new(); + crate::resource_materialization::walk_block( + &mut functions[function_index].body, + &mut |expression| { + if !matches!(expression.kind, E::Comptime { .. }) { + return Ok(()); + } + let selected = proofs + .iter() + .filter(|proof| { + crate::resource_materialization::proof_matches( + proof, &owner, expression, checked, &archive, + ) + }) + .collect::>(); + if let [proof] = selected.as_slice() + && proof.actor_graph().is_some() + { + occurrences.push((expression.clone(), (*proof).clone())); + } + Ok(()) + }, + )?; + for (original, proof) in occurrences { + let mut locals = functions[function_index].locals.clone(); + let plan = derive( + &owner, + &original, + &proof, + checked, + &archive, + types, + reflection, + &candidates, + &mut functions, + &mut locals, + )?; + functions[function_index].locals = locals; + let mut protected_body = plan.body().clone(); + crate::resource_materialization::walk_block( + &mut protected_body, + &mut |expression| { + if let E::ActorGraphStep { instruction, .. } = &expression.kind + && let ActorGraphOperation::HookBox { hook, .. } = + instruction.operation() + { + rows.push(RequiredMaterialization { + function: owner.id, + identity: owner.identity.clone(), + original: original.clone(), + current: expression.clone(), + hook: Some(hook.clone()), + proof: proof.clone(), + projection: instruction.projected_hook().cloned(), + }); + } + Ok(()) + }, + )?; + let mut installed = 0usize; + crate::resource_materialization::walk_block( + &mut functions[function_index].body, + &mut |expression| { + if crate::resource_materialization::expression_equal(expression, &original) + { + expression.kind = E::ActorGraphImport(plan.clone()); + rows.push(RequiredMaterialization { + function: owner.id, + identity: owner.identity.clone(), + original: original.clone(), + current: expression.clone(), + hook: None, + proof: proof.clone(), + projection: None, + }); + installed += 1; + } + Ok(()) + }, + )?; + if installed != 1 { + return Err( + "actor graph required occurrence is not unique in its checked function" + .into(), + ); + } + } + } + let required_only = + crate::resource_materialization::required_only_functions(&archive, &functions, &rows)?; + let view = Materialized { + functions, + values: rows, + required_only, + }; + program.functions = view.functions.clone(); + program.resource_source.graph_values = Some(Arc::new(Transition { + checked: checked.clone(), + original: original_functions, + current: program.functions.clone(), + proofs: rows_for_transition(&view.values), + })); + program.resource_source.install_materialized(view); + Ok(()) + })(); + result.map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |f| f.span), + message, + }] + }) +} + +#[allow(clippy::too_many_arguments)] +fn derive( + owner: &Function, + original: &Expression, + proof: &CheckedRequiredValue, + checked: &Arc, + archive: &crate::ResourceSourceArchive, + types: &TypeInterner, + reflection: &ReflectionMetadata, + candidates: &[crate::evaluated_values::FunctionValueCandidate], + functions: &mut Vec, + locals: &mut Vec, +) -> Result { + let graph = proof + .actor_graph() + .ok_or("required actor graph has no opaque template")?; + if !proof.belongs_to(checked) || !graph.belongs_to(checked) || graph.root_type() != original.ty + { + return Err("required actor graph changed checked nominal root".into()); + } + let span = original.span; + let mut hooks = Vec::new(); + let manifest = archive + .manifest() + .ok_or("actor graph has no original hook manifest")?; + for projection in proof.actor_graph_hooks()? { + let original_hook = projection.checked_hook(checked)?; + let hook = manifest + .hook_for_definition(original_hook.definition) + .ok_or("projected graph hook is absent from the original Resource manifest")?; + if hook.function_type() != projection.signature() + || hook.function_type() != original_hook.function_type + || hook.resource_type() != original_hook.resource_type + || hook.resource_kind().definition() != original_hook.resource_definition + { + return Err( + "projected graph hook changed exact nominal signature or Resource family".into(), + ); + } + hook.validate(types)?; + hooks.push((projection, hook)); + } + let origin = Arc::new(Origin { + checked: checked.clone(), + proof: proof.clone(), + function: owner.id, + identity: owner.identity.clone(), + original: original.clone(), + hooks, + functions: OnceLock::new(), + archive: archive.clone(), + callback_leaves: graph_callback_leaves(proof)?, + callbacks: OnceLock::new(), + }); + let mut seeds = ActorGraphSeeds { + ready: HashMap::new(), + pending: HashMap::new(), + hooks: HashMap::new(), + origin: origin.clone(), + }; + let mut body = Block { + statements: Vec::new(), + span, + }; + let mut instructions = Vec::new(); + let ticket = local(locals, TypeInterner::UINT64, span)?; + push_step( + &origin, + ActorGraphOperation::Begin, + TypeInterner::UINT64, + &[], + Some(ticket), + span, + &mut body, + &mut instructions, + ); + for node in graph.nodes() { + let Type::Actor(id) = types.resolve(node.ty()) else { + return Err("actor graph seed has non-actor type".into()); + }; + let declaration = types.resolve_actor(*id); + let fields = declaration + .capability_params + .iter() + .chain(&declaration.state_fields) + .collect::>(); + let frozen = node + .captures() + .iter() + .chain(node.state()) + .collect::>(); + if fields.len() != frozen.len() + || fields + .iter() + .zip(&frozen) + .any(|((name, ty), field)| *name != field.name() || *ty != field.ty()) + { + return Err("actor graph changed exact constructor-owned field layout".into()); + } + let actor = local(locals, node.ty(), span)?; + if seeds + .ready + .insert(node.seed(), (node.ty(), actor)) + .is_some() + { + return Err("actor graph repeats a seed node".into()); + } + push_step( + &origin, + ActorGraphOperation::ReserveBase { + ty: node.ty(), + fields: frozen.len(), + }, + node.ty(), + &[(ticket, TypeInterner::UINT64)], + Some(actor), + span, + &mut body, + &mut instructions, + ); + } + let mut pending = Vec::new(); + for value in std::iter::once(graph.root()).chain(graph.nodes().iter().flat_map(|node| { + node.captures() + .iter() + .chain(node.state()) + .map(|field| field.value()) + })) { + collect_pending(value, &mut pending); + } + pending.sort_unstable(); + pending.dedup(); + for (seed, depth) in pending { + let (ty, base) = *seeds + .ready + .get(&seed) + .ok_or("pending actor graph seed is absent")?; + let alias = local(locals, ty, span)?; + seeds.pending.insert((seed, depth), alias); + push_step( + &origin, + ActorGraphOperation::ReservePending { ty, depth }, + ty, + &[(ticket, TypeInterner::UINT64), (base, ty)], + Some(alias), + span, + &mut body, + &mut instructions, + ); + } + for (index, (projection, hook)) in origin.hooks.iter().enumerate() { + let instruction = ActorGraphInstruction(Arc::new(Instruction { + origin: origin.clone(), + index: instructions.len(), + operation: ActorGraphOperation::HookBox { + hook: hook.clone(), + projection: index, + }, + result: projection.signature(), + operands: vec![(ticket, TypeInterner::UINT64)], + })); + let expression = Expression { + kind: E::ActorGraphStep { + instruction: instruction.clone(), + args: vec![Expression { + kind: E::Local(ticket), + ty: TypeInterner::UINT64, + span, + }], + }, + ty: projection.signature(), + span, + }; + if seeds + .hooks + .insert(std::ptr::from_ref(projection.value()?).addr(), expression) + .is_some() + { + return Err("graph hook repeats an authenticated leaf projection".into()); + } + instructions.push(instruction); + } + for (node_index, node) in graph.nodes().iter().enumerate() { + let base = seeds.ready(node.seed(), node.ty())?; + for (index, field) in node.captures().iter().chain(node.state()).enumerate() { + let mut bindings = Vec::new(); + let value = value_expression( + field.value(), + field.ty(), + span, + &mut ValueContext { + actor_seeds: Some(&seeds), + types, + reflection, + functions: candidates, + refinement_functions: None, + locals, + bindings: &mut bindings, + }, + )?; + body.statements.extend(bindings); + let stored = local(locals, field.ty(), span)?; + body.statements.push(Statement { + kind: S::Let { + local: stored, + value, + }, + span, + }); + push_step( + &origin, + ActorGraphOperation::Fill { + ty: field.ty(), + field: index, + node: node_index, + }, + TypeInterner::NOTHING, + &[ + (ticket, TypeInterner::UINT64), + (base, node.ty()), + (stored, field.ty()), + ], + None, + span, + &mut body, + &mut instructions, + ); + } + } + let mut bindings = Vec::new(); + let root = value_expression( + graph.root(), + graph.root_type(), + span, + &mut ValueContext { + actor_seeds: Some(&seeds), + types, + reflection, + functions: candidates, + refinement_functions: None, + locals, + bindings: &mut bindings, + }, + )?; + body.statements.extend(bindings); + let output = local(locals, graph.root_type(), span)?; + body.statements.push(Statement { + kind: S::Let { + local: output, + value: root, + }, + span, + }); + let root_bits = local(locals, TypeInterner::UINT64, span)?; + push_step( + &origin, + ActorGraphOperation::RootFill { + ty: graph.root_type(), + }, + TypeInterner::UINT64, + &[(ticket, TypeInterner::UINT64), (output, graph.root_type())], + Some(root_bits), + span, + &mut body, + &mut instructions, + ); + push_step( + &origin, + ActorGraphOperation::Commit, + TypeInterner::NOTHING, + &[(ticket, TypeInterner::UINT64)], + None, + span, + &mut body, + &mut instructions, + ); + let published_output = local(locals, graph.root_type(), span)?; + push_step( + &origin, + ActorGraphOperation::RootPublish { + ty: graph.root_type(), + }, + graph.root_type(), + &[ + (ticket, TypeInterner::UINT64), + (root_bits, TypeInterner::UINT64), + ], + Some(published_output), + span, + &mut body, + &mut instructions, + ); + let output = published_output; + // Complete exact interface/callback conversions inside this private body before + // freezing it; the later public program visitor cannot rewrite sealed data. + let mut converted = functions[owner.id.index() as usize].clone(); + converted.locals = locals.clone(); + // Keep previous Graph and ordinary reflected construction sites visible + // to the full seal validator while converting this private recipe. + converted.body.statements.push(Statement { + kind: S::Scope(body), + span, + }); + let mut temporary = Program { + resource_manifest: manifest.clone(), + resource_source: archive.clone(), + functions: functions.to_vec(), + equality_methods: archive + .equality_methods() + .cloned() + .ok_or("graph factory lost original equality metadata")?, + }; + temporary.functions[owner.id.index() as usize] = converted; + temporary.resource_source.graph_values = Some(Arc::new(Transition { + checked: checked.clone(), + original: conversion_sources_from_archive(archive), + current: temporary.functions.clone(), + proofs: vec![proof.clone()], + })); + // The private factory has authenticated the complete input table and built + // this exact recipe Scope from its checked proof. Its staged execution view + // must include that Scope before peer conversion validates the whole table. + // Original Source functions, required rows and required-only IDs stay intact. + temporary + .resource_source + .install_materialized(Materialized { + functions: temporary.functions.clone(), + values: archive.required_materializations().to_vec(), + required_only: archive.required_only_function_ids().to_vec(), + }); + crate::interface_values::complete_graph_value_conversions(&mut temporary, types).map_err( + |errors| { + errors + .into_iter() + .map(|e| e.message) + .collect::>() + .join("; ") + }, + )?; + let mut converted = temporary.functions[owner.id.index() as usize].clone(); + let Some(Statement { + kind: S::Scope(converted_body), + .. + }) = converted.body.statements.pop() + else { + return Err("graph conversion lost its exact private recipe scope".into()); + }; + let mut callbacks = Vec::new(); + for function in &temporary.functions { + for plan in crate::reflected_callable_plans(function) { + if let Some(source) = plan.graph_source() { + if source + .origin + .upgrade() + .is_some_and(|source| Arc::ptr_eq(&source, &origin)) + { + if !plan.is_sealed() || !plan.validate_original() { + return Err( + "graph callback adapter was not authenticated and sealed".into() + ); + } + callbacks.push((source.leaf, plan)); + } + } + } + } + if callbacks.len() != origin.callback_leaves.len() + || origin.callback_leaves.iter().any(|leaf| { + callbacks + .iter() + .filter(|(expected, _)| expected == leaf) + .count() + != 1 + }) + { + return Err("graph callback leaf has no unique sealed checked adapter".into()); + } + origin + .callbacks + .set(callbacks) + .map_err(|_| "graph callbacks were sealed twice")?; + // Conversion can introduce callable adapters. Retain their dense IDs and + // checked generated bodies in the common program before freezing this plan. + let original_count = functions.len(); + functions.extend(temporary.functions.into_iter().skip(original_count)); + origin + .functions + .set(functions.clone()) + .map_err(|_| "actor graph declarations were sealed twice")?; + *locals = converted.locals; + Ok(ActorGraphPlan(Arc::new(Plan { + origin, + body: converted_body, + output, + output_type: graph.root_type(), + instructions, + locals: locals.clone(), + original_local_count: owner.locals.len(), + }))) +} +fn local(locals: &mut Vec, ty: TypeId, span: Span) -> Result { + let id = LocalId::new( + u32::try_from(locals.len()).map_err(|_| "actor graph native local table exhausted")?, + ); + locals.push(Local { + id, + name: format!("__checked_actor_graph_{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }); + Ok(id) +} +#[allow(clippy::too_many_arguments)] +fn push_step( + origin: &Arc, + operation: ActorGraphOperation, + result: TypeId, + operands: &[(LocalId, TypeId)], + output: Option, + span: Span, + body: &mut Block, + instructions: &mut Vec, +) { + let instruction = ActorGraphInstruction(Arc::new(Instruction { + origin: origin.clone(), + index: instructions.len(), + operation, + result, + operands: operands.to_vec(), + })); + let args = operands + .iter() + .map(|(local, ty)| Expression { + kind: E::Local(*local), + ty: *ty, + span, + }) + .collect(); + let value = Expression { + kind: E::ActorGraphStep { + instruction: instruction.clone(), + args, + }, + ty: result, + span, + }; + body.statements.push(Statement { + kind: match output { + Some(local) => S::Let { local, value }, + None => S::Expression(value), + }, + span, + }); + instructions.push(instruction); +} +fn pending_actor(mut value: &Value) -> Option<(u64, u64)> { + let mut depth = 0u64; + loop { + match value { + Value::Pending(inner) => { + depth = depth.checked_add(1)?; + value = inner; + } + Value::Typed { value: inner, .. } => value = inner, + Value::Actor(seed) => return Some((*seed, depth)), + _ => return None, + } + } +} +fn collect_pending(value: &Value, output: &mut Vec<(u64, u64)>) { + let mut work = vec![value]; + while let Some(value) = work.pop() { + if let Some((seed, depth)) = pending_actor(value) + && depth != 0 + { + output.push((seed, depth)); + continue; + } + match value { + Value::Pending(inner) + | Value::Typed { value: inner, .. } + | Value::OptionalSome(inner) + | Value::ResultOk(inner) + | Value::ResultFail(inner) => work.push(inner), + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => work.extend(values), + Value::Struct { fields, .. } => work.extend(fields.iter().map(|(_, value)| value)), + Value::Map(entries) => { + work.extend(entries.iter().flat_map(|(key, value)| [key, value])) + } + Value::Function { captures, .. } => work.extend(captures.values()), + Value::TypeConstruction { fields, .. } => { + work.extend(fields.iter().map(|(_, _, _, value)| value)) + } + _ => {} + } + } +} + +pub fn visit_actor_graph_factory_values( + body: &mut Block, + mut visit: impl FnMut(&Expression), +) -> Result<(), String> { + crate::resource_materialization::walk_block(body, &mut |value| { + visit(value); + Ok(()) + }) +} + +/// Readonly occurrence traversal; a caller cannot mutate a sealed body. +pub fn visit_actor_graph_plans( + body: &mut Block, + mut visit: impl FnMut(&ActorGraphPlan), +) -> Result<(), String> { + crate::resource_materialization::walk_block(body, &mut |expression| { + if let E::ActorGraphImport(plan) = &expression.kind { + visit(plan); + } + Ok(()) + }) +} + +pub fn validate_actor_graph_execution( + program: &Program, + types: &TypeInterner, +) -> Result<(), String> { + let mut found = false; + for function in &program.functions { + let mut body = function.body.clone(); + visit_actor_graph_plans(&mut body, |plan| { + found = true; + let _ = plan; + })?; + } + if found { + program.resource_source.validate_current(program, types)?; + } + Ok(()) +} + +mod capture_bindings; +pub use capture_bindings::ActorGraphCaptureBinding; +#[cfg(test)] +mod tests; + +/// Exact checked Graph transformations; only the private producer constructs rows. +#[derive(Debug)] +pub(crate) struct Transition { + checked: Arc, + original: Vec, + current: Vec, + proofs: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct CallbackSource { + // The origin retains sealed plans; a weak back-reference avoids a proof cycle. + origin: Weak, + leaf: usize, + source: Expression, +} + +impl CallbackSource { + pub(crate) fn same_source(&self, other: &Self) -> bool { + let Self { + origin, + leaf, + source, + } = self; + let Self { + origin: other_origin, + leaf: other_leaf, + source: other_source, + } = other; + Weak::ptr_eq(origin, other_origin) + && leaf == other_leaf + && crate::resource_materialization::expression_equal(source, other_source) + } + + pub(crate) fn validate_site( + &self, + reference: &jett_comptime::value::ReflectedCallableValue, + site: &Expression, + owner: &Function, + archive: &crate::ResourceSourceArchive, + ) -> Result<(), String> { + let E::FunctionAdapter { value, .. } = &site.kind else { + return Err("graph callback lost its canonical adapter site".into()); + }; + let origin = self + .origin + .upgrade() + .ok_or("graph callback lost its retained origin")?; + let transition = archive + .graph_values + .as_ref() + .ok_or("graph callback lacks protected owner transition")?; + if !archive.belongs_to(&origin.checked) + || !Arc::ptr_eq(reference.checked_program(), &origin.checked) + || !Arc::ptr_eq(&transition.checked, &origin.checked) + || !origin.proof.belongs_to(&origin.checked) + || !origin.callback_leaves.contains(&self.leaf) + || owner.id != origin.function + || owner.identity != origin.identity + || !transition + .current + .get(owner.id.index() as usize) + .is_some_and(|current| { + crate::reflected_callable::source_functions_equal( + std::slice::from_ref(owner), + std::slice::from_ref(current), + ) + }) + || !crate::resource_materialization::expression_equal(value, &self.source) + || site.ty != reference.requested_type() + || value.ty != reference.source_type() + { + return Err( + "graph callback changed its exact retained leaf, descriptor or protected owner" + .into(), + ); + } + Ok(()) + } +} + +fn rows_for_transition(rows: &[RequiredMaterialization]) -> Vec { + rows.iter() + .filter(|row| row.proof.actor_graph().is_some()) + .map(|row| row.proof.clone()) + .collect() +} + +fn conversion_sources_from_archive(archive: &crate::ResourceSourceArchive) -> Vec { + archive.graph_values.as_ref().map_or_else( + || archive.functions().to_vec(), + |transition| transition.original.clone(), + ) +} + +pub(crate) fn conversion_sources(program: &Program) -> Result, String> { + validate_conversion_input(program).map_err(|errors| { + errors + .into_iter() + .map(|e| e.message) + .collect::>() + .join("; ") + })?; + let transition = program + .resource_source + .graph_values + .as_ref() + .ok_or("graph callback has no protected Source transition")?; + let mut source = transition.original.clone(); + let original_count = source.len(); + source.extend(program.functions.iter().skip(original_count).cloned()); + Ok(source) +} + +pub(crate) fn original_function( + archive: &crate::ResourceSourceArchive, + function: &Function, + checked: &Arc, +) -> Result, String> { + let Some(transition) = &archive.graph_values else { + return Ok(None); + }; + if !archive.belongs_to(checked) + || !Arc::ptr_eq(&transition.checked, checked) + || transition.proofs.is_empty() + || transition + .proofs + .iter() + .any(|proof| !proof.belongs_to(checked) || proof.actor_graph().is_none()) + { + return Err("Graph Source restoration belongs to another checked proof".into()); + } + let Some(current) = transition.current.get(function.id.index() as usize) else { + return Ok(None); + }; + if !crate::reflected_callable::source_functions_equal( + std::slice::from_ref(function), + std::slice::from_ref(current), + ) { + return Ok(None); + } + Ok(transition + .original + .get(function.id.index() as usize) + .cloned()) +} + +pub(crate) fn validate_conversion_input(program: &Program) -> Result<(), Vec> { + let Some(transition) = &program.resource_source.graph_values else { + return Ok(()); + }; + if !program.resource_source.belongs_to(&transition.checked) + || program.resource_source.manifest() != Some(&program.resource_manifest) + || program.resource_source.equality_methods() != Some(&program.equality_methods) + || transition.proofs.is_empty() + || transition + .proofs + .iter() + .any(|proof| !proof.belongs_to(&transition.checked) || proof.actor_graph().is_none()) + || !crate::reflected_callable::source_functions_equal( + &program.functions, + &transition.current, + ) + || program + .functions + .iter() + .enumerate() + .any(|(index, function)| { + function.id.index() as usize != index + || function + .locals + .iter() + .enumerate() + .any(|(index, local)| local.id.index() as usize != index) + }) + { + return Err(vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |f| f.span), + message: "Graph conversion changed its protected checked whole-program input".into(), + }]); + } + // The complete table, including appended adapters and every recipe site, + // remains authenticated; callers cannot hide edited generated declarations + // behind an original-Source prefix comparison. + let checked_types = &transition.checked.checked().interner; + let result = (|| -> Result<(), String> { + program.resource_source.validate_types(checked_types)?; + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + match &expression.kind { + E::ActorGraphImport(plan) => plan.validate(function, checked_types)?, + E::ActorGraphStep { instruction, args } => { + instruction.validate( + function.id, + &function.identity, + args, + expression.ty, + checked_types, + )?; + } + _ => {} + } + Ok(()) + })?; + } + Ok(()) + })(); + result.map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |f| f.span), + message, + }] + })?; + Ok(()) +} + +pub(crate) fn complete_conversion( + program: &mut Program, + before: &Program, +) -> Result<(), Vec> { + validate_conversion_input(before)?; + let Some(transition) = &before.resource_source.graph_values else { + return Ok(()); + }; + if program.resource_source != before.resource_source { + return Err(vec![LowerError { + span: before.functions[0].span, + message: "Graph conversion replaced its protected original archive".into(), + }]); + } + program.resource_source.graph_values = Some(Arc::new(Transition { + checked: transition.checked.clone(), + original: transition.original.clone(), + current: program.functions.clone(), + proofs: transition.proofs.clone(), + })); + Ok(()) +} + +fn graph_callback_leaves(proof: &CheckedRequiredValue) -> Result, String> { + fn walk(value: &Value, output: &mut HashSet) { + match value { + Value::ReflectedCallable(reference) => { + output.insert(std::ptr::from_ref(reference.as_ref()).addr()); + walk(reference.source(), output); + } + Value::Typed { value, .. } + | Value::Pending(value) + | Value::OptionalSome(value) + | Value::ResultOk(value) + | Value::ResultFail(value) => walk(value, output), + Value::List(values) | Value::Set(values) => { + for value in values { + walk(value, output); + } + } + Value::Map(entries) => { + for (key, value) in entries { + walk(key, output); + walk(value, output); + } + } + Value::Struct { fields, .. } => { + for (_, value) in fields { + walk(value, output); + } + } + Value::Enum { fields, .. } | Value::Machine { fields, .. } => { + for value in fields { + walk(value, output); + } + } + Value::Function { captures, .. } => { + for value in captures.values() { + walk(value, output); + } + } + Value::TypeConstruction { fields, .. } => { + for (_, _, _, value) in fields { + walk(value, output); + } + } + _ => {} + } + } + let graph = proof + .actor_graph() + .ok_or("graph callback association lacks checked template")?; + let mut output = HashSet::new(); + walk(graph.root(), &mut output); + for node in graph.nodes() { + for field in node.captures().iter().chain(node.state()) { + walk(field.value(), &mut output); + } + } + Ok(output) +} diff --git a/crates/jett_hir/src/actor_graph/capture_bindings.rs b/crates/jett_hir/src/actor_graph/capture_bindings.rs new file mode 100644 index 00000000..d718ba84 --- /dev/null +++ b/crates/jett_hir/src/actor_graph/capture_bindings.rs @@ -0,0 +1,185 @@ +//! Capture headers derived from a private graph seed and original actor Source. +use super::*; +use crate::{DeclarationKind, Param}; +use jett_parser::ast::Item; +use jett_resolve::DefKind; + +#[derive(Debug, Clone, PartialEq)] +pub struct ActorGraphCaptureBinding { + handler: FunctionId, + node: usize, + field: usize, + parameter: Param, + actor_type: TypeId, +} +impl ActorGraphCaptureBinding { + pub fn handler(&self) -> FunctionId { + self.handler + } + pub fn node(&self) -> usize { + self.node + } + pub fn field(&self) -> usize { + self.field + } + pub fn parameter(&self) -> &Param { + &self.parameter + } + pub fn actor_type(&self) -> TypeId { + self.actor_type + } + pub fn field_type(&self) -> TypeId { + self.parameter.ty + } + pub fn mutable(&self) -> bool { + self.parameter.mutable + } +} +impl ActorGraphPlan { + /// No caller-supplied actor name, TypeId or formal can mint this relation. + pub fn actor_capture_bindings(&self) -> Result, String> { + let origin = &self.0.origin; + let graph = origin + .proof + .actor_graph() + .ok_or("capture graph has no retained template")?; + if !origin.proof.belongs_to(&origin.checked) || !graph.belongs_to(&origin.checked) { + return Err("capture graph belongs to a foreign checked program".into()); + } + let types = &origin.checked.checked().interner; + origin.archive.validate_types(types)?; + let functions = origin + .functions + .get() + .ok_or("graph capture declarations were not sealed")?; + let mut rows = Vec::new(); + for (node_index, node) in graph.nodes().iter().enumerate() { + let Type::Actor(actor_id) = types.resolve(node.ty()) else { + return Err("graph seed lost nominal Actor type".into()); + }; + let shape = types.resolve_actor(*actor_id); + let definitions = origin + .checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Actor + && origin + .checked + .checked() + .definition_types + .get(&definition.id) + == Some(&node.ty()) + }) + .collect::>(); + let [definition] = definitions.as_slice() else { + return Err("graph seed has no unique original actor DefId/type".into()); + }; + let actors = origin + .checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Actor(actor) if actor.name.span == definition.span => Some(actor), + _ => None, + }) + .collect::>(); + let [actor] = actors.as_slice() else { + return Err("graph actor has no unique original AST declaration".into()); + }; + if shape.name != definition.name + || actor.capability_params.len() != shape.capability_params.len() + || actor.state_fields.len() != shape.state_fields.len() + || actor.handlers.len() != shape.messages.len() + || node.captures().len() != shape.capability_params.len() + || node.state().len() != shape.state_fields.len() + { + return Err("graph actor capture/state cardinality changed".into()); + } + for (message_index, handler_source) in actor.handlers.iter().enumerate() { + let handlers = functions + .iter() + .filter(|function| { + function.identity.declaration.kind == DeclarationKind::ActorHandler + && function.span == handler_source.span + }) + .collect::>(); + let [handler] = handlers.as_slice() else { + return Err("graph field has no unique original handler header".into()); + }; + let original = origin + .archive + .execution_functions() + .get(handler.id.index() as usize) + .filter(|function| { + function.id == handler.id + && function.identity == handler.identity + && function.span == handler.span + && function.capture_count == handler.capture_count + && function.params == handler.params + && function.locals == handler.locals + && function.return_type == handler.return_type + && function.debug_kind == handler.debug_kind + }) + .ok_or("graph handler differs from whole original Source archive")?; + let message = &shape.messages[message_index]; + let captured = shape.capability_params.len() + shape.state_fields.len(); + if original.capture_count != captured + || original.params.len() != captured + message.params.len() + || original.return_type != message.responds + || handler_source.params.len() != message.params.len() + { + return Err("graph handler formal/return layout changed".into()); + } + for (field_index, ((field_name, field_ty), frozen)) in shape + .capability_params + .iter() + .chain(&shape.state_fields) + .zip(node.captures().iter().chain(node.state())) + .enumerate() + { + let parameter = &original.params[field_index]; + let (name, span, mutable) = if field_index < actor.capability_params.len() { + let source = &actor.capability_params[field_index]; + (&source.name.name, source.span, source.mutable) + } else { + let source = + &actor.state_fields[field_index - actor.capability_params.len()]; + (&source.name.name, source.span, source.mutable) + }; + if frozen.name() != field_name + || frozen.ty() != *field_ty + || name != field_name + || parameter.name != *field_name + || parameter.ty != *field_ty + || parameter.span != span + || parameter.mutable != mutable + || original + .locals + .get(parameter.local.index() as usize) + .is_none_or(|local| { + local.id != parameter.local + || local.name != parameter.name + || local.ty != parameter.ty + || local.span != parameter.span + || local.mutable != parameter.mutable + }) + { + return Err("graph field changed exact typed Source capture header".into()); + } + rows.push(ActorGraphCaptureBinding { + handler: original.id, + node: node_index, + field: field_index, + parameter: parameter.clone(), + actor_type: node.ty(), + }); + } + } + } + Ok(rows) + } +} diff --git a/crates/jett_hir/src/actor_graph/tests.rs b/crates/jett_hir/src/actor_graph/tests.rs new file mode 100644 index 00000000..5a8a2198 --- /dev/null +++ b/crates/jett_hir/src/actor_graph/tests.rs @@ -0,0 +1,637 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::{collections::HashMap, sync::Arc}; +const SOURCE: &str = r#"namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function make() returns Counter: + return comptime spawn Counter() +function main() returns nothing: + Counter worker = make() + int64 observed = ask worker.read + println(observed) + return nothing +"#; +fn prepared() -> Arc { + prepared_source(SOURCE) +} +fn prepared_source(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .expect("checked graph Source"), + ) +} +fn values( + checked: &Arc, +) -> jett_comptime::ExplicitComptimeValues { + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + evaluation.values +} +fn baked( + checked: &Arc, + values: &jett_comptime::ExplicitComptimeValues, +) -> crate::Program { + let mut hir = crate::lower_checked_ordinary_program(checked).unwrap(); + crate::bake_required_values( + &mut hir, + values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + crate::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + hir +} +fn plans(program: &crate::Program) -> Vec { + let mut plans = Vec::new(); + for function in &program.functions { + let mut body = function.body.clone(); + visit_actor_graph_plans(&mut body, |plan| plans.push(plan.clone())).unwrap(); + } + plans +} +#[test] +fn exact_private_graph_plan_is_at_required_occurrence_and_keeps_constructor_proof() { + let checked = prepared(); + let values = values(&checked); + let program = baked(&checked, &values); + let plans = plans(&program); + assert_eq!(plans.len(), 1); + let plan = &plans[0]; + let owner = &program.functions[plan.function().index() as usize]; + plan.validate(owner, &checked.checked().interner).unwrap(); + let captures = plan.actor_capture_bindings().unwrap(); + assert_eq!(captures.len(), 1); + let captured = &captures[0]; + assert_eq!((captured.node(), captured.field()), (0, 0)); + assert!(captured.mutable()); + let handler = &program.functions[captured.handler().index() as usize]; + assert_eq!(handler.params[0], *captured.parameter()); + assert_eq!(captured.field_type(), TypeInterner::INT64); + assert!(matches!( + plan.instructions()[0].operation(), + ActorGraphOperation::Begin + )); + assert!(matches!( + plan.instructions()[plan.instructions().len() - 2].operation(), + ActorGraphOperation::Commit + )); + assert_eq!( + plan.instructions() + .iter() + .filter(|step| matches!(step.operation(), ActorGraphOperation::ReserveBase { .. })) + .count(), + 1 + ); + assert_eq!( + plan.instructions() + .iter() + .filter(|step| matches!(step.operation(), ActorGraphOperation::Fill { .. })) + .count(), + 1 + ); + validate_actor_graph_execution(&program, &checked.checked().interner).unwrap(); + assert!( + program + .functions + .iter() + .any(|function| function.identity.declaration.kind + == crate::DeclarationKind::ActorConstructor), + "normal checked constructor remains available" + ); +} +#[test] +fn public_span_mirror_cannot_replace_private_graph_snapshot() { + let checked = prepared(); + let mut values = values(&checked); + let proof = values.checked_required_values(&checked).unwrap().remove(0); + values.insert( + proof.source_span(), + jett_comptime::ComptimeContext::default(), + jett_comptime::Value::Actor(777), + ); + let program = baked(&checked, &values); + let plans = plans(&program); + let step = plans[0] + .instructions() + .iter() + .find(|step| matches!(step.operation(), ActorGraphOperation::Fill { .. })) + .unwrap(); + assert_eq!(step.field_value().unwrap(), &jett_comptime::Value::Int64(1)); +} +#[test] +fn raw_public_actor_value_cannot_mint_checked_graph_import() { + let checked = prepared(); + let actual = values(&checked); + let proof = actual.checked_required_values(&checked).unwrap().remove(0); + let mut forged = jett_comptime::ExplicitComptimeValues::default(); + forged.insert( + proof.source_span(), + jett_comptime::ComptimeContext::default(), + jett_comptime::Value::Actor(0), + ); + let mut program = crate::lower_checked_ordinary_program(&checked).unwrap(); + assert!( + crate::bake_required_values( + &mut program, + &forged, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions + ) + .is_err() + ); +} +#[test] +fn changed_current_hir_or_missing_nominal_table_refuses_before_mir() { + let checked = prepared(); + let values = values(&checked); + let program = baked(&checked, &values); + let plan = plans(&program).remove(0); + let mut changed = program.clone(); + let owner = &mut changed.functions[plan.function().index() as usize]; + owner.locals[plan.output().index() as usize].ty = TypeInterner::BOOL; + assert!(validate_actor_graph_execution(&changed, &checked.checked().interner).is_err()); + let mut missing = TypeInterner::new(); + for ty in checked.checked().interner.type_ids() { + assert_eq!( + missing.intern(checked.checked().interner.resolve(ty).clone()), + ty + ); + } + assert!( + plan.validate( + &program.functions[plan.function().index() as usize], + &missing + ) + .is_err() + ); +} +#[test] +fn instruction_copy_retains_exact_owner_operand_and_type_guard() { + let checked = prepared(); + let values = values(&checked); + let program = baked(&checked, &values); + let plan = plans(&program).remove(0); + let owner = &program.functions[plan.function().index() as usize]; + let step = plan + .instructions() + .iter() + .find(|step| matches!(step.operation(), ActorGraphOperation::Fill { .. })) + .unwrap(); + let original = plan + .body() + .statements + .iter() + .find_map(|statement| match &statement.kind { + crate::StatementKind::Expression(Expression { + kind: E::ActorGraphStep { instruction, args }, + ty, + .. + }) if instruction == step => Some((args.clone(), *ty)), + _ => None, + }) + .unwrap(); + step.validate( + owner.id, + &owner.identity, + &original.0, + original.1, + &checked.checked().interner, + ) + .unwrap(); + let mut changed = original.0; + changed[0].kind = E::Int(0); + assert!( + step.validate( + owner.id, + &owner.identity, + &changed, + original.1, + &checked.checked().interner + ) + .is_err() + ); +} + +const INDEPENDENT_GRAPH_AND_REFLECTED: &str = r#"namespace app +type Positive = int64 where value > 0 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +struct Holder: + callback: function() returns int64 +function adapted(view holder: Holder) returns function() returns Positive: + for field in type.fields[Holder](): + return type.field_value[Holder, function() returns Positive](view holder, view field) + return positive +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function observe() returns int64: + Counter worker = comptime spawn Counter() + Holder holder = Holder(callback: five) + for field in type.fields[Holder](): + function() returns Positive callback = type.field_value[Holder, function() returns Positive](view holder, view field) + Positive validated = callback() + int64 count = ask worker.read + return count + coarsen validated + return 0 +"#; + +const ADAPTED_CALLBACK_GRAPH_FIELD: &str = r#"namespace app +type Positive = int64 where value > 0 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +struct Holder: + callback: function() returns int64 +function adapted(view holder: Holder) returns function() returns Positive: + for field in type.fields[Holder](): + return type.field_value[Holder, function() returns Positive](view holder, view field) + return positive +actor Keeper(callback: function() returns Positive): + receive read responds Positive: + respond callback() +function create() returns Keeper: + Holder holder = Holder(callback: five) + return spawn Keeper(callback: adapted(view holder)) +function observe() returns int64: + Keeper worker = comptime create() + Positive observed = ask worker.read + return coarsen observed +"#; + +const TWO_GRAPH_SITES: &str = r#"namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +function observe() returns int64: + Counter first = comptime spawn Counter() + Counter second = comptime spawn Counter() + send first.add(4) + int64 left = ask first.read + int64 right = ask second.read + return left * 10 + right +"#; + +const GRAPH_AND_ORDINARY_REFINEMENT: &str = r#"namespace app +type Positive = int64 where value > 0 +function positive() returns Positive: + return 5 +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function observe() returns int64: + Counter worker = comptime spawn Counter() + Positive cached = comptime positive() + int64 ordinary = comptime 7 + int64 observed = ask worker.read + return observed + coarsen cached + ordinary +"#; + +const SAME_SIGNATURE_FOREIGN_GRAPH_LEAF: &str = r#"namespace app +type Positive = int64 where value > 0 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +struct Holder: + callback: function() returns int64 +function adapted(view holder: Holder) returns function() returns Positive: + for field in type.fields[Holder](): + return type.field_value[Holder, function() returns Positive](view holder, view field) + return positive +actor Keeper(callback: function() returns Positive): + receive read responds Positive: + respond callback() +function create() returns Keeper: + Holder holder = Holder(callback: five) + return spawn Keeper(callback: adapted(view holder)) +function observe() returns int64: + Keeper first = comptime create() + Keeper second = comptime create() + Positive left = ask first.read + Positive right = ask second.read + return coarsen left + coarsen right +"#; + +fn reference_observations( + checked: &Arc, + values: &jett_comptime::ExplicitComptimeValues, + expected: i64, +) { + let mut runtime = jett_comptime::Interpreter::new(); + runtime + .install_checked_resource_program( + checked.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + runtime.set_reflection_metadata(checked.checked().reflection_metadata.clone()); + runtime.set_checked_expression_types(Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + })); + runtime.register_module(checked.module()); + runtime.set_explicit_comptime_values(Arc::new(values.clone())); + for _ in 0..2 { + assert_eq!( + runtime.call_function("app.observe", Vec::new()).unwrap(), + Value::Int64(expected), + ); + assert!(runtime.take_debug_events().is_empty()); + } +} + +fn validate_interop( + program: &crate::Program, + checked: &Arc, +) { + crate::validate(program).unwrap(); + crate::validate_backend_types(program, &checked.checked().interner).unwrap(); + validate_actor_graph_execution(program, &checked.checked().interner).unwrap(); +} + +fn reflected_graph_field( + plan: &ActorGraphPlan, +) -> ( + &ActorGraphInstruction, + &jett_comptime::value::ReflectedCallableValue, +) { + plan.instructions() + .iter() + .find_map(|instruction| { + if !matches!(instruction.operation(), ActorGraphOperation::Fill { .. }) { + return None; + } + let Value::ReflectedCallable(reference) = instruction.field_value().ok()?.payload() + else { + return None; + }; + Some((instruction, reference.as_ref())) + }) + .expect("actual frozen Actor field contains the admitted reflected callable") +} + +#[test] +fn independent_graph_and_reflected_getter_preserve_one_source_owner() { + let checked = prepared_source(INDEPENDENT_GRAPH_AND_REFLECTED); + let values = values(&checked); + let program = baked(&checked, &values); + assert_eq!(plans(&program).len(), 1); + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "observe") + .unwrap(); + let ordinary = crate::reflected_callable_plans(owner); + assert!( + !ordinary.is_empty(), + "ordinary checked getter stays in the Graph owner" + ); + assert!( + ordinary + .iter() + .all(|plan| plan.is_sealed() && plan.validate_original()) + ); + validate_interop(&program, &checked); + reference_observations(&checked, &values, 6); +} + +#[test] +fn adapted_callback_in_actor_field_selects_its_sealed_graph_leaf() { + let checked = prepared_source(ADAPTED_CALLBACK_GRAPH_FIELD); + let values = values(&checked); + let program = baked(&checked, &values); + let plans = plans(&program); + assert_eq!(plans.len(), 1); + let (instruction, reference) = reflected_graph_field(&plans[0]); + let selected = instruction + .reflected_callable(reference) + .expect("exact frozen leaf selects its protected HIR plan"); + assert!(selected.is_sealed() && selected.validate_original()); + assert_eq!(selected.checked_plan(), reference.checked_plan()); + assert_eq!(selected.checked_plan().source_return(), TypeInterner::INT64); + assert_ne!( + selected.checked_plan().requested_return(), + TypeInterner::INT64 + ); + assert!(selected.canonical_adapter().is_some()); + validate_interop(&program, &checked); + reference_observations(&checked, &values, 5); +} + +#[test] +fn multiple_graph_sites_in_one_owner_reserve_independent_activations() { + let checked = prepared_source(TWO_GRAPH_SITES); + let values = values(&checked); + let program = baked(&checked, &values); + let plans = plans(&program); + assert_eq!(plans.len(), 2); + assert_eq!(plans[0].function(), plans[1].function()); + assert_ne!( + plans[0].required_proof().source_span(), + plans[1].required_proof().source_span() + ); + assert!(!plans[0].instructions()[0].same_import(&plans[1].instructions()[0])); + for plan in &plans { + let field = plan + .instructions() + .iter() + .find(|instruction| matches!(instruction.operation(), ActorGraphOperation::Fill { .. })) + .unwrap(); + assert_eq!(field.field_value().unwrap(), &Value::Int64(1)); + } + validate_interop(&program, &checked); + // 51 proves mutation of the first actor does not reach the second. Repeating + // the call proves each source activation starts from the same frozen value. + reference_observations(&checked, &values, 51); +} + +#[test] +fn graph_and_ordinary_required_refinement_share_authenticated_materialization() { + let checked = prepared_source(GRAPH_AND_ORDINARY_REFINEMENT); + let values = values(&checked); + let proofs = values.checked_required_values(&checked).unwrap(); + assert_eq!(proofs.len(), 3); + assert_eq!( + proofs + .iter() + .filter(|proof| proof.actor_graph().is_some()) + .count(), + 1 + ); + let program = baked(&checked, &values); + assert!(!crate::has_reflected_callable_plans(&program)); + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + assert!(!matches!( + expression.kind, + E::Constant { .. } | E::Comptime { .. } + )); + Ok(()) + }) + .unwrap(); + } + validate_interop(&program, &checked); + reference_observations(&checked, &values, 13); +} + +#[test] +fn same_signature_foreign_graph_leaf_and_return_plan_cannot_substitute() { + let checked = prepared_source(SAME_SIGNATURE_FOREIGN_GRAPH_LEAF); + let values = values(&checked); + let program = baked(&checked, &values); + let plans = plans(&program); + assert_eq!(plans.len(), 2); + let (first_instruction, first_reference) = reflected_graph_field(&plans[0]); + let (second_instruction, second_reference) = reflected_graph_field(&plans[1]); + assert_eq!( + first_reference.source_type(), + second_reference.source_type() + ); + assert_eq!( + first_reference.requested_type(), + second_reference.requested_type() + ); + assert_eq!( + first_reference.checked_plan(), + second_reference.checked_plan() + ); + let first = first_instruction + .reflected_callable(first_reference) + .unwrap(); + let second = second_instruction + .reflected_callable(second_reference) + .unwrap(); + assert_ne!(first, second); + assert!( + first_instruction + .reflected_callable(second_reference) + .is_none() + ); + assert!( + second_instruction + .reflected_callable(first_reference) + .is_none() + ); + validate_interop(&program, &checked); + reference_observations(&checked, &values, 10); + + let mut changed = program.clone(); + let adapter = &mut changed.functions[first.function().index() as usize]; + let crate::StatementKind::Return(Some(Expression { + kind: + E::Handle { + kind: crate::HandleKind::ReflectedReturn { callable, .. }, + .. + }, + .. + })) = &mut adapter.body.statements[0].kind + else { + panic!("canonical adapted return root"); + }; + *callable = Some(second.clone()); + assert!( + crate::validate(&changed).is_err(), + "same signature does not transfer the sealed return authority" + ); + assert!(crate::complete_value_conversions(&mut changed, &checked.checked().interner).is_err()); +} + +#[test] +fn ordinary_required_value_keeps_the_retained_actor_archive_and_refuses_edits() { + const ORDINARY: &str = "namespace app\nactor Counter:\n mutable int64 count = 1\nfunction main() returns nothing:\n int64 answer = comptime 7\n println(answer)\n return nothing\n"; + let checked = prepared_source(ORDINARY); + let required = values(&checked); + assert!(!required.has_actor_graphs()); + let types = &checked.checked().interner; + let original = crate::lower_checked_ordinary_program(&checked).unwrap(); + assert!(!crate::has_reflected_callable_plans(&original)); + assert!(original.resource_source.checked_program().is_some()); + let program = baked(&checked, &required); + program + .resource_source + .validate_current(&program, types) + .unwrap(); + assert!(plans(&program).is_empty()); + let mut converted = program.clone(); + crate::complete_value_conversions(&mut converted, types).unwrap(); + converted + .resource_source + .validate_current(&converted, types) + .unwrap(); + let mut before_forgery = original.clone(); + before_forgery.functions[0].span.end += 1; + assert!( + crate::bake_required_values( + &mut before_forgery, + &required, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions + ) + .is_err() + ); + let foreign = prepared_source(ORDINARY); + let foreign_required = values(&foreign); + let mut foreign_forgery = original; + assert!( + crate::bake_required_values( + &mut foreign_forgery, + &foreign_required, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions + ) + .is_err() + ); + let mut current_forgery = program; + current_forgery.functions[0].span.end += 1; + assert!(crate::complete_value_conversions(&mut current_forgery, types).is_err()); +} diff --git a/crates/jett_hir/src/actor_handlers.rs b/crates/jett_hir/src/actor_handlers.rs new file mode 100644 index 00000000..07d35e5c --- /dev/null +++ b/crates/jett_hir/src/actor_handlers.rs @@ -0,0 +1,819 @@ +//! Original checked actor roots, separate from callable function declarations. +use crate::*; +use jett_typecheck::{CheckedActorBindingRole, CheckedActorHandler, CheckedActorHandlerTarget}; + +#[path = "actor_callable_sealing.rs"] +mod callable_sealing; +pub(crate) use callable_sealing::{ + InitialCallableRenewal, InitialPeerCheckpoint, + finish_original as finish_original_callable_sealing, renew as renew_callable_snapshots, + source_matches as callable_source_matches, +}; + +fn same_callable_epoch( + left: &Option>, + right: &Option>, +) -> bool { + match (left, right) { + (None, None) => true, + (Some(left), Some(right)) => Arc::ptr_eq(left, right), + _ => false, + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ActorHandlerRole { + role: CheckedActorBindingRole, + fact: CheckedBindingFact, + local: LocalId, + view: Option, +} +impl ActorHandlerRole { + pub(super) fn new( + role: CheckedActorBindingRole, + fact: CheckedBindingFact, + local: LocalId, + ) -> Self { + Self { + role, + fact, + local, + view: None, + } + } + pub fn role(&self) -> CheckedActorBindingRole { + self.role + } + pub fn fact(&self) -> CheckedBindingFact { + self.fact + } + pub fn local(&self) -> LocalId { + self.local + } + pub(super) fn set_view(&mut self, view: ActorStateView) { + self.view = Some(view); + } + pub fn state_view(&self) -> Option<&ActorStateView> { + self.view.as_ref() + } +} + +#[derive(Debug, Clone)] +pub struct ActorStateView { + receipt: Arc, + fact: CheckedBindingFact, + local: LocalId, + root: LocalId, + initializer: Box, + constructor: Option>, + callable_epoch: Option>, +} +impl PartialEq for ActorStateView { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.receipt, &other.receipt) + && self.fact == other.fact + && self.local == other.local + && self.root == other.root + && self.initializer == other.initializer + && self.constructor == other.constructor + && same_callable_epoch(&self.callable_epoch, &other.callable_epoch) + } +} +impl ActorStateView { + pub(super) fn new( + receipt: CheckedActorHandler, + fact: CheckedBindingFact, + local: LocalId, + root: LocalId, + initializer: Expression, + ) -> Self { + Self { + receipt: Arc::new(receipt), + fact, + local, + root, + initializer: Box::new(initializer), + constructor: None, + callable_epoch: None, + } + } + pub fn local(&self) -> LocalId { + self.local + } + pub fn root(&self) -> LocalId { + self.root + } + pub fn field(&self) -> Option { + self.receipt + .bindings() + .iter() + .find_map(|binding| match binding.role { + CheckedActorBindingRole::State { field } if binding.fact == self.fact => { + Some(field) + } + _ => None, + }) + } + + pub fn initializer(&self) -> &Expression { + &self.initializer + } + pub fn matches_constructor(&self, function: &Function) -> bool { + function.identity.declaration.kind == DeclarationKind::ActorConstructor + && function.source_definition.is_none() + && self.constructor.as_deref() == Some(&Snapshot::of(function)) + && self.matches_metadata(&function.locals) + } + pub fn matches_metadata(&self, locals: &[Local]) -> bool { + let Some(alias) = locals + .get(self.local.index() as usize) + .filter(|local| local.id == self.local) + else { + return false; + }; + let Some(root) = locals + .get(self.root.index() as usize) + .filter(|local| local.id == self.root) + else { + return false; + }; + self.receipt.ownership_validated() + && self.receipt.bindings().iter().any(|role| { + matches!(role.role, CheckedActorBindingRole::State { .. }) && role.fact == self.fact + }) + && matches!( + self.fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(_) + } + ) + && alias.view_source == Some(root.id) + && !alias.mutable + && alias.ty == self.fact.ty + && alias.span.file == self.fact.declaration_span.file + && alias.span.start <= self.fact.declaration_span.start + && alias.span.end >= self.fact.declaration_span.end + } + pub fn validate(&self, locals: &[Local], types: &TypeInterner) -> Result<(), String> { + if !self.matches_metadata(locals) { + return Err("actor persistent view changed its original checked state binding".into()); + } + super::local_views::validate_local_view_initializer( + &self.initializer, + self.root, + locals[self.root.index() as usize].ty, + self.fact.ty, + types, + ) + .map_err(str::to_owned) + } + pub(super) fn remap(&self, map: &impl Fn(LocalId) -> Option) -> Result { + fn expression( + value: &mut Expression, + map: &impl Fn(LocalId) -> Option, + ) -> Result<(), String> { + match &mut value.kind { + ExpressionKind::Local(local) => { + *local = map(*local) + .ok_or("actor view initializer lost original declaration/local join")? + } + ExpressionKind::Field { base, .. } + | ExpressionKind::View(base) + | ExpressionKind::Coarsen(base) + | ExpressionKind::Declassify(base) => expression(base, map)?, + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + expression(value, map)? + } + _ => { + return Err( + "actor state view initializer is not a finite stable typed projection" + .into(), + ); + } + } + Ok(()) + } + let mut result = self.clone(); + result.local = map(self.local).ok_or("actor view lost alias declaration")?; + result.root = map(self.root).ok_or("actor view lost root declaration")?; + expression(&mut result.initializer, map)?; + Ok(result) + } +} + +#[derive(Debug, Clone)] +struct Snapshot { + id: FunctionId, + identity: FunctionIdentity, + params: Vec, + captures: usize, + result: TypeId, + locals: Vec, + body: Block, + span: Span, +} +impl PartialEq for Snapshot { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.identity == other.identity + && self.params == other.params + && self.captures == other.captures + && self.result == other.result + && self.locals == other.locals + && self.span == other.span + && crate::resource_materialization::hir_blocks_equal(&self.body, &other.body) + } +} +impl Snapshot { + fn of(function: &Function) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + params: function.params.clone(), + captures: function.capture_count, + result: function.return_type, + locals: function.locals.clone(), + body: function.body.clone(), + span: function.span, + } + } +} + +/// Minted only while lowering the actual original ActorDef handler ordinal. +/// Source ownership remains in the checker receipt; this is its native projection. +#[derive(Debug, Clone)] +pub struct ActorHandlerEntry { + target: CheckedActorHandlerTarget, + receipt: Arc, + roles: Vec, + snapshot: Option>, + callable_epoch: Option>, +} +impl PartialEq for ActorHandlerEntry { + fn eq(&self, other: &Self) -> bool { + self.target == other.target + && Arc::ptr_eq(&self.receipt, &other.receipt) + && self.roles == other.roles + && self.snapshot == other.snapshot + && same_callable_epoch(&self.callable_epoch, &other.callable_epoch) + } +} +impl ActorHandlerEntry { + pub(super) fn new( + target: CheckedActorHandlerTarget, + receipt: CheckedActorHandler, + roles: Vec, + ) -> Self { + Self { + target, + receipt: Arc::new(receipt), + roles, + snapshot: None, + callable_epoch: None, + } + } + pub fn target(&self) -> CheckedActorHandlerTarget { + self.target + } + pub fn roles(&self) -> &[ActorHandlerRole] { + &self.roles + } + pub fn allows_state_view(&self, function: &Function, local: LocalId) -> bool { + if self.validate(function).is_err() { + return false; + } + self.roles.iter().any(|role| { + role.local == local + && matches!(role.role, CheckedActorBindingRole::State { .. }) + && matches!( + role.fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(_) + } + ) + }) + } + pub fn validate(&self, function: &Function) -> Result<(), String> { + if !self.receipt.ownership_validated() + || self.receipt.target() != self.target + || self.receipt.body_span() != function.body.span + || self.receipt.response_type() != function.return_type + || function.source_definition.is_some() + || function.identity.declaration.kind != DeclarationKind::ActorHandler + { + return Err("actor entry changed its original checked handler root".into()); + } + if self.roles.len() != self.receipt.bindings().len() + || self.roles.len() != function.params.len() + { + return Err("actor entry role cardinality changed".into()); + } + let mut states = 0; + let mut capabilities = 0; + let mut messages = 0; + for (index, (role, checked)) in self.roles.iter().zip(self.receipt.bindings()).enumerate() { + let parameter = &function.params[index]; + let local = function + .locals + .get(role.local.index() as usize) + .filter(|local| local.id == role.local) + .ok_or("actor role local is absent")?; + if role.local.index() as usize != index + || role.role != checked.role + || role.fact != checked.fact + || parameter.local != role.local + || parameter.ty != role.fact.ty + || parameter.mutable != role.fact.mutable + || local.ty != role.fact.ty + || local.mutable != role.fact.mutable + || local.span.file != role.fact.declaration_span.file + || local.span.start > role.fact.declaration_span.start + || local.span.end < role.fact.declaration_span.end + { + return Err("actor role changed its exact checked declaration".into()); + } + let mode = match role.fact.mode { + CheckedBindingMode::Owned => ParamMode::Owned, + CheckedBindingMode::View { .. } => ParamMode::View, + }; + if parameter.mode != mode { + return Err("actor role changed checked ownership mode".into()); + } + match role.role { + CheckedActorBindingRole::Capability { parameter } + if parameter == capabilities && states == 0 && messages == 0 => + { + capabilities += 1 + } + CheckedActorBindingRole::State { field } if field == states && messages == 0 => { + states += 1 + } + CheckedActorBindingRole::Message { parameter } if parameter == messages => { + messages += 1 + } + _ => return Err("actor role ordinal/order changed".into()), + } + if let CheckedBindingMode::View { + source: CheckedViewSource::Binding(definition), + } = role.fact.mode + { + let owner = self.roles[..index] + .iter() + .find(|owner| owner.fact.definition == definition) + .ok_or("actor state view has no earlier original backing role")?; + if role.view.as_ref().is_none_or(|view| { + !view.matches_metadata(&function.locals) + || view.root() != owner.local + || view.local() != local.id + }) { + return Err( + "actor state view lost original typed initializer projection".into(), + ); + } + if local.view_source != Some(owner.local) { + return Err("actor state view changed original dependency".into()); + } + } else if local.view_source.is_some() || role.view.is_some() { + return Err("actor role acquired an invented alias origin".into()); + } + } + if function.capture_count != capabilities + states { + return Err("actor capture projection changed".into()); + } + if self.snapshot.as_deref() != Some(&Snapshot::of(function)) { + return Err("actor handler changed its sealed original native body".into()); + } + Ok(()) + } +} + +pub(super) fn seal_program(program: &mut Program) -> Result<(), Vec> { + for function in &mut program.functions { + let snapshot = Snapshot::of(function); + for view in &mut function.actor_state_views { + if view.constructor.is_none() { + view.constructor = Some(Box::new(snapshot.clone())); + } + } + if let Some(mut entry) = function.actor_handler.take() { + if entry.snapshot.is_some() { + function.actor_handler = Some(entry); + continue; + } + entry.snapshot = Some(Box::new(Snapshot::of(function))); + entry.validate(function).map_err(|message| { + vec![LowerError { + span: function.span, + message, + }] + })?; + function.actor_handler = Some(entry); + } + } + Ok(()) +} +pub(super) fn validate_program(program: &Program) -> Result<(), Vec> { + for function in &program.functions { + for view in &function.actor_state_views { + if !view.matches_constructor(function) { + return Err(vec![ValidationError { + span: function.span, + message: "actor state view is outside its original constructor body".into(), + }]); + } + } + if function.identity.declaration.kind == DeclarationKind::ActorHandler { + let result = function + .actor_handler + .as_ref() + .ok_or_else(|| "actor handler lacks original entry receipt".to_owned()) + .and_then(|entry| entry.validate(function)); + result.map_err(|message| { + vec![ValidationError { + span: function.span, + message, + }] + })?; + } else if function.actor_handler.is_some() { + return Err(vec![ValidationError { + span: function.span, + message: "actor entry attached to foreign function kind".into(), + }]); + } + } + Ok(()) +} + +/// Advance complete actor snapshots only after the actual ordinary required +/// producer has authenticated every replacement and generated binding. +pub(super) fn required_bake( + program: &mut Program, + before: &Program, + values: &jett_comptime::ExplicitComptimeValues, + types: &TypeInterner, + bake: &crate::reflected_callable::baked::CheckedRequiredBake<'_>, +) -> Result<(), String> { + bake.authenticate_output(before, program, types)?; + validate_program(before) + .map_err(|errors| format!("invalid actor required-bake preimage: {errors:?}"))?; + let mut changed = false; + for (original, current) in before.functions.iter().zip(&program.functions) { + if current.actor_handler != original.actor_handler + || current.actor_state_views != original.actor_state_views + { + return Err("actor required bake replaced original receipt or view inventory".into()); + } + if (original.actor_handler.is_some() || !original.actor_state_views.is_empty()) + && Snapshot::of(original) != Snapshot::of(current) + { + changed = true; + } + } + if !changed { + // An empty bake grants no new snapshot authority and retains the + // preexisting no-value behavior for an otherwise unchanged actor. + return validate_program(program) + .map_err(|errors| format!("invalid unchanged actor bake: {errors:?}")); + } + let replay = bake.replay(before, values, types)?; + for (function, expected) in program.functions.iter_mut().zip(&replay.functions) { + let snapshot = Snapshot::of(expected); + for view in &mut function.actor_state_views { + view.constructor = Some(Box::new(snapshot.clone())); + } + if let Some(entry) = function.actor_handler.as_mut() { + entry.snapshot = Some(Box::new(snapshot)); + } + for view in &function.actor_state_views { + view.validate(&function.locals, types)?; + } + } + validate_program(program) + .map_err(|errors| format!("invalid renewed actor required bake: {errors:?}")) +} + +pub(super) fn conversion( + program: &mut Program, + before: &Program, + types: &TypeInterner, + conversion: &interface_values::CanonicalValueConversion<'_>, +) -> Result<(), Vec> { + validate_program(before).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + let expected = conversion.replay(before, types)?; + for function in &mut program.functions { + if !function.actor_state_views.is_empty() { + let original = before + .functions + .iter() + .find(|old| old.id == function.id) + .ok_or_else(|| { + vec![LowerError { + span: function.span, + message: "actor constructor conversion lost original".into(), + }] + })?; + let replay = expected + .functions + .iter() + .find(|old| old.id == function.id) + .ok_or_else(|| { + vec![LowerError { + span: function.span, + message: "actor constructor conversion lost replay".into(), + }] + })?; + if function.actor_state_views != original.actor_state_views + || Snapshot::of(function) != Snapshot::of(replay) + { + return Err(vec![LowerError { + span: function.span, + message: + "actor constructor conversion changed original view proof or operations" + .into(), + }]); + } + let snapshot = Snapshot::of(replay); + for view in &mut function.actor_state_views { + view.constructor = Some(Box::new(snapshot.clone())); + } + } + let Some(entry) = function.actor_handler.as_mut() else { + continue; + }; + let original = before + .functions + .iter() + .find(|old| old.id == function.id) + .and_then(|old| old.actor_handler.as_ref()) + .ok_or_else(|| { + vec![LowerError { + span: function.span, + message: "actor conversion lost original receipt".into(), + }] + })?; + let replay = expected + .functions + .iter() + .find(|old| old.id == function.id) + .ok_or_else(|| { + vec![LowerError { + span: function.span, + message: "actor conversion lost replay function".into(), + }] + })?; + if entry != original { + return Err(vec![LowerError { + span: function.span, + message: "actor conversion replaced original receipt".into(), + }]); + } + let snapshot = Snapshot::of(replay); + // Borrow the full function only after releasing its mutable receipt. + entry.snapshot = Some(Box::new(snapshot)); + } + for function in &program.functions { + if let Some(entry) = &function.actor_handler { + entry.validate(function).map_err(|message| { + vec![LowerError { + span: function.span, + message, + }] + })?; + } + } + Ok(()) +} + +/// One exact original Send/Ask occurrence with its checked incoming acquisitions. +#[derive(Debug, Clone, PartialEq)] +pub struct ActorMessageProof { + checked: jett_typecheck::CheckedActorMessageCall, + handler: FunctionId, + receiver: Box, + receiver_shape: ActorReceiverProof, + arguments: Vec, +} +impl ActorMessageProof { + pub fn target(&self) -> CheckedActorHandlerTarget { + self.checked.target + } + pub fn original_receiver(&self) -> &Expression { + &self.receiver + } + pub fn receiver_proof(&self) -> &ActorReceiverProof { + &self.receiver_shape + } + pub fn response_type(&self) -> TypeId { + self.checked.response_type + } + pub fn argument(&self, index: usize) -> Option<&ArgumentOwnership> { + self.arguments.get(index) + } + pub fn validate( + &self, + functions: &[Function], + expression: &Expression, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, + context: jett_typecheck::CheckedOwnershipContext, + initial: bool, + ) -> Result<(), String> { + let ExpressionKind::ActorMessage { + actor, + message, + handler, + args, + evaluation_order, + kind, + .. + } = &expression.kind + else { + return Err("actor packet attached to non-message".into()); + }; + if initial && actor.as_ref() != self.receiver.as_ref() { + return Err("actor message receiver changed its original expression".into()); + } + let expected_kind = match kind { + ActorMessageKind::Send => jett_typecheck::CheckedActorMessageKind::Send, + ActorMessageKind::Ask => jett_typecheck::CheckedActorMessageKind::Ask, + }; + let result = if *kind == ActorMessageKind::Send { + TypeInterner::NOTHING + } else { + self.checked.response_type + }; + if *handler != self.handler + || self.checked.kind != expected_kind + || self.checked.context != context + || actor.ty != self.checked.receiver_type + || actor.span != self.checked.receiver_span + || expression.ty != result + || expression.span.file != self.checked.source_span.file + || expression.span.start > self.checked.source_span.start + || expression.span.end < self.checked.source_span.end + || !matches!(types.resolve(actor.ty),Type::Actor(id) if *id==self.checked.target.actor) + { + return Err( + "actor message changed its original checked target, kind or source occurrence" + .into(), + ); + } + let definition = types.resolve_actor(self.checked.target.actor); + let original = definition + .messages + .get(self.checked.target.handler_index) + .ok_or("actor message ordinal is absent")?; + if original.name != *message + || original.responds != self.checked.response_type + || original.params.len() != args.len() + { + return Err("actor message changed original canonical message shape".into()); + } + let function = functions + .get(handler.index() as usize) + .filter(|function| function.id == *handler) + .ok_or("actor message handler function absent")?; + let entry = function + .actor_handler + .as_ref() + .ok_or("actor message target has no original handler receipt")?; + if entry.target() != self.checked.target + || function.return_type != self.checked.response_type + || function.params.len() != function.capture_count + args.len() + { + return Err("actor message selected a foreign original handler".into()); + } + for (index, argument) in self.arguments.iter().enumerate() { + let param = &function.params[function.capture_count + index]; + let access = if param.mode == ParamMode::View { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + }; + if original.params[index].1 != argument.parameter_type + || param.ty != argument.parameter_type + || access != argument.callee_access + { + return Err("actor message changed checked formal type or mode".into()); + } + } + super::call_ownership::validate_actor_arguments( + &self.arguments, + args, + evaluation_order, + context, + locals, + types, + initial, + ) + } +} +pub(super) fn message_proof( + body: &BodyLowerer<'_, '_>, + inner: &Expr, + span: Span, + actor: &Expression, + message: &str, + handler: FunctionId, + args: &[Expression], + order: &[usize], + kind: ActorMessageKind, +) -> Result { + let checked = body + .actor_message_calls + .get(&span) + .ok_or("actor message has no exact checked Source packet")?; + let scopes = body + .required_scopes + .iter() + .map(|scope| (scope.owner, scope.index)) + .collect::>(); + if !body.parent.check.validates_actor_message_at( + checked, + body.actor_message_root, + &scopes, + span, + ) { + return Err("actor message lacks its original checked session/body occurrence".into()); + } + let original = match inner { + Expr::Call(callee, _, _) => callee.as_ref(), + _ => inner, + }; + let Expr::FieldAccess(receiver, name, _) = original else { + return Err("actor Source message receiver changed".into()); + }; + let expected = match kind { + ActorMessageKind::Send => jett_typecheck::CheckedActorMessageKind::Send, + ActorMessageKind::Ask => jett_typecheck::CheckedActorMessageKind::Ask, + }; + if checked.source_span != span + || checked.receiver_span != receiver.span() + || actor.span != receiver.span() + || actor.ty != checked.receiver_type + || checked.kind != expected + || checked.context != body.ownership_context + || name.name != message + || !matches!(body.parent.check.interner.resolve(actor.ty),Type::Actor(id) if *id==checked.target.actor) + { + return Err("actor message does not join its full original Send/Ask source".into()); + } + let receipt = body + .parent + .check + .actor_handler(checked.target) + .ok_or("actor message original handler is not authenticated")?; + if !receipt.ownership_validated() { + return Err("actor message original handler ownership is incomplete".into()); + } + let definition = body + .parent + .check + .interner + .resolve_actor(checked.target.actor); + if definition + .messages + .get(checked.target.handler_index) + .is_none_or(|original| { + original.name != message || original.responds != checked.response_type + }) + || body + .parent + .actor_handler_ids + .get(&(actor.ty, message.to_owned())) + != Some(&handler) + { + return Err("actor message changed original handler ordinal association".into()); + } + let arguments = + body.build_source_arguments(span, args, order, &checked.arguments, checked.context)?; + Ok(ActorMessageProof { + checked: checked.clone(), + handler, + receiver: Box::new(actor.clone()), + receiver_shape: ActorReceiverProof::original(actor, &body.parent.check.interner), + arguments, + }) +} + +/// Dependency inventory only; this grants no actor or Source authority. +pub fn has_actor_messages(block: &Block) -> bool { + let mut body = block.clone(); + let mut found = false; + let result = super::resource_materialization::walk_block(&mut body, &mut |value| { + found |= matches!(value.kind, ExpressionKind::ActorMessage { .. }); + Ok(()) + }); + result.is_ok() && found +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_hir/src/actor_handlers/tests.rs b/crates/jett_hir/src/actor_handlers/tests.rs new file mode 100644 index 00000000..c92248b9 --- /dev/null +++ b/crates/jett_hir/src/actor_handlers/tests.rs @@ -0,0 +1,192 @@ +use super::*; +use jett_diagnostics::Severity; +const SOURCE: &str = r#"namespace app +struct State: + value: int64 + items: list[int64] +actor Counter(seed: int64): + mutable State state = State(value: seed, items: list(seed)) + receive replace(values: list[int64]): + state.items = values + state.value = state.value + 1 + receive read responds int64: + respond state.value +function main() returns int64: + Counter first = spawn Counter(seed: 1) + Counter other = spawn Counter(seed: 2) + list[int64] supplied = list(3) + list[int64] wrong = list(4) + send first.replace(supplied) + return ask first.read +"#; +fn checked( + source: &str, +) -> ( + ast::Module, + jett_resolve::ResolveResult, + CheckResult, + HashMap, +) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{:?}", + resolved.diagnostics + ); + let check = jett_typecheck::check(&parsed.module, &resolved); + assert!( + check + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{:?}", + check.diagnostics + ); + ( + parsed.module, + resolved, + check, + HashMap::from([(file, SourceOrigin::Project)]), + ) +} +fn program() -> (Program, CheckResult) { + let (module, resolved, check, origins) = checked(SOURCE); + let program = lower(&module, &resolved, &check, &origins).expect("genuine actor Source lowers"); + (program, check) +} +#[test] +fn original_actor_entry_and_message_operands_reject_corruption() { + let (original, check) = program(); + validate(&original).unwrap(); + validate_backend_types(&original, &check.interner).unwrap(); + let main = original + .functions + .iter() + .position(|function| function.identity.declaration.name == "main") + .unwrap(); + let mut wrong_receiver = original.clone(); + let other = wrong_receiver.functions[main] + .locals + .iter() + .find(|local| local.name == "other") + .unwrap() + .id; + let StatementKind::Expression(Expression { + kind: ExpressionKind::ActorMessage { actor, .. }, + .. + }) = &mut wrong_receiver.functions[main].body.statements[4].kind + else { + panic!("original send"); + }; + actor.kind = ExpressionKind::Local(other); + assert!(validate_backend_types(&wrong_receiver, &check.interner).is_err()); + let mut wrong_argument = original.clone(); + let wrong = wrong_argument.functions[main] + .locals + .iter() + .find(|local| local.name == "wrong") + .unwrap() + .id; + let StatementKind::Expression(Expression { + kind: ExpressionKind::ActorMessage { args, .. }, + .. + }) = &mut wrong_argument.functions[main].body.statements[4].kind + else { + panic!("original send"); + }; + args[0].kind = ExpressionKind::Local(wrong); + assert!(validate_backend_types(&wrong_argument, &check.interner).is_err()); + let mut wrong_role = original.clone(); + let handler = wrong_role + .functions + .iter_mut() + .find(|function| function.actor_handler.is_some()) + .unwrap(); + handler.params[1].mutable = false; + assert!(validate(&wrong_role).is_err()); +} +#[test] +fn same_span_changed_original_actor_cannot_mint_native_receipt() { + let (mut module, resolved, check, origins) = checked(SOURCE); + let actor = module + .items + .iter_mut() + .find_map(|item| { + if let Item::Actor(actor) = item { + Some(actor) + } else { + None + } + }) + .unwrap(); + actor.handlers[0].body.stmts.swap(0, 1); + let errors = lower(&module, &resolved, &check, &origins) + .expect_err("retained original actor syntax is immutable"); + assert!( + errors + .iter() + .any(|error| error.message.contains("original checked ownership receipt")), + "{errors:?}" + ); +} +#[test] +fn projected_persistent_state_view_retains_original_initializer_and_constructor_scope() { + let source = r#"namespace app +struct Inner: + value: int64 +struct Outer: + inner: Inner +actor Counter(seed: int64): + mutable Outer owned = Outer(inner: Inner(value: seed)) + Inner projected = view owned.inner + Inner alias = view projected + mutable Inner independent = Inner(value: seed) + receive advance(amount: int64): + independent.value = independent.value + amount + receive read responds int64: + respond projected.value + alias.value + independent.value +function main() returns int64: + Counter first = spawn Counter(seed: 1) + send first.advance(2) + return ask first.read +"#; + let (module, resolved, check, origins) = checked(source); + let original = lower(&module, &resolved, &check, &origins).unwrap(); + validate_backend_types(&original, &check.interner).unwrap(); + let constructor = original + .functions + .iter() + .find(|function| !function.actor_state_views.is_empty()) + .unwrap(); + assert_eq!(constructor.actor_state_views.len(), 2); + let view = constructor.actor_state_views[0].clone(); + assert!(view.matches_constructor(constructor)); + let mut foreign = original.clone(); + let main = foreign + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + main.actor_state_views.push(view); + assert!(validate(&foreign).is_err()); + let handler = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "Counter.read") + .unwrap(); + let entry = handler.actor_handler.as_ref().unwrap(); + assert_eq!( + entry + .roles() + .iter() + .filter(|role| role.state_view().is_some()) + .count(), + 2 + ); +} diff --git a/crates/jett_hir/src/actor_required_bake_tests.rs b/crates/jett_hir/src/actor_required_bake_tests.rs new file mode 100644 index 00000000..d3dcbdde --- /dev/null +++ b/crates/jett_hir/src/actor_required_bake_tests.rs @@ -0,0 +1,608 @@ +use super::*; +use crate::evaluated_values::{ValueContext, function_value_candidates, value_expression}; +use jett_common::SourceOrigin; +use jett_typecheck::CheckedResourceProgram; + +const WITNESS: &str = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett" +); +const SCALAR: &str = "namespace app +function five() returns int64: + return 5 +actor Worker: + mutable int64 count = 0 + receive first responds int64: + int64 baked = comptime five() + respond baked + receive second responds int64: + int64 baked = comptime five() + respond baked +function main() returns int64: + Worker worker = spawn Worker() + int64 first = ask worker.first + int64 second = ask worker.second + return first + second +"; +const VIEWS: &str = "namespace app +struct Inner: + value: int64 +struct Outer: + inner: Inner +function five() returns int64: + return 5 +actor Counter: + mutable Outer owned = Outer(inner: Inner(value: comptime five())) + Inner projected = view owned.inner + Inner alias = view projected + receive read responds int64: + int64 baked = comptime five() + respond projected.value + alias.value + baked +function main() returns int64: + Counter counter = spawn Counter() + return ask counter.read +"; + +fn checked(source: &str, release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +fn required(checked: &Arc) -> ExplicitComptimeValues { + let captured = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new( + jett_comptime::checked_types::CheckedExpressionTypes::from_checked_program(checked), + ), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert!(captured.debug_events.is_empty()); + captured.values +} +fn setup( + source: &str, + release: bool, +) -> (Program, Arc, ExplicitComptimeValues) { + let checked = checked(source, release); + let values = required(&checked); + let program = crate::lower_checked_ordinary_program(&checked).unwrap(); + (program, checked, values) +} +fn same_program(left: &Program, right: &Program) -> bool { + left.resource_source == right.resource_source + && left.resource_manifest == right.resource_manifest + && left.equality_methods == right.equality_methods + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} +fn actual_bake<'types>( + before: &Program, + checked: &Arc, + values: &ExplicitComptimeValues, + types: &'types TypeInterner, +) -> CheckedRequiredBake<'types> { + let functions = function_value_candidates( + &before.functions, + &checked.checked().method_value_definitions, + ); + required_bake_receipt::produce( + before, + values, + types, + |value, ty, span, locals, bindings| { + value_expression( + value, + ty, + span, + &mut ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals, + bindings, + }, + ) + }, + ) + .unwrap() +} +fn bake( + program: &mut Program, + checked: &Arc, + values: &ExplicitComptimeValues, +) -> Result<(), Vec> { + crate::bake_required_values( + program, + values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) +} +fn materialized_plans(program: &Program) -> Vec { + let mut result = Vec::new(); + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::InterfaceCoerce { adapters, .. } = &expression.kind { + result.extend(adapters.iter().filter_map(|adapter| { + adapter + .reflected + .as_ref() + .filter(|plan| plan.is_materialized() && !plan.is_sealed()) + .cloned() + })); + } + Ok(()) + }) + .unwrap(); + } + result +} + +#[test] +fn whole_original_actor_witness_bakes_with_exact_owner_cache_and_full_snapshot() { + for release in [false, true] { + let (mut program, checked, values) = setup(WITNESS, release); + let source = program.resource_source.functions().to_vec(); + let before = program.clone(); + let proof = values.checked_required_values(&checked).unwrap().remove(0); + let mut selected = 0; + for owner in &before.functions { + let mut body = owner.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if matches!(expression.kind, ExpressionKind::Comptime { .. }) { + assert!(crate::resource_materialization::proof_matches( + &proof, + owner, + expression, + &checked, + &before.resource_source + )); + selected += 1; + } + Ok(()) + }) + .unwrap(); + } + assert_eq!(selected, 1); + bake(&mut program, &checked, &values).unwrap(); + assert!(crate::resource_materialization::functions_equal( + &source, + program.resource_source.functions() + )); + assert!(same_required_cache(&program, &values)); + validate_conversion_input(&program).unwrap(); + crate::actor_handlers::validate_program(&program).unwrap(); + let before_handler = before + .functions + .iter() + .find(|function| function.actor_handler.is_some()) + .unwrap(); + let handler = &program.functions[before_handler.id.index() as usize]; + let entry = handler.actor_handler.as_ref().unwrap(); + assert_eq!( + entry.target(), + before_handler.actor_handler.as_ref().unwrap().target() + ); + assert_eq!( + entry.roles(), + before_handler.actor_handler.as_ref().unwrap().roles() + ); + assert!(entry.validate(handler).is_ok()); + assert!( + before_handler + .actor_handler + .as_ref() + .unwrap() + .validate(handler) + .is_err() + ); + assert_eq!(handler.params, before_handler.params); + assert_eq!(handler.capture_count, before_handler.capture_count); + assert_eq!( + &handler.locals[..before_handler.locals.len()], + before_handler.locals.as_slice() + ); + assert_eq!(materialized_plans(&program).len(), 1); + let mut required_left = 0; + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + required_left += + usize::from(matches!(expression.kind, ExpressionKind::Comptime { .. })); + Ok(()) + }) + .unwrap(); + } + assert_eq!(required_left, 0); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + assert!( + crate::resource_materialization::functions_equal( + program.resource_source.execution_functions(), + &program.functions, + ), + "the actual sealed table must remain the exact authenticated MIR input" + ); + // Runtime execution remains a separate genuine native gate. + } +} + +#[test] +fn equal_scalar_actor_occurrences_keep_distinct_exact_handlers_through_completion() { + for release in [false, true] { + let (mut program, checked, values) = setup(SCALAR, release); + let proofs = values.checked_required_values(&checked).unwrap(); + assert_eq!(proofs.len(), 2); + assert_ne!(proofs[0].owner(), proofs[1].owner()); + let source = program.resource_source.functions().to_vec(); + for owner in &program.functions { + let mut body = owner.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if matches!(expression.kind, ExpressionKind::Comptime { .. }) { + assert_eq!( + proofs + .iter() + .filter(|proof| crate::resource_materialization::proof_matches( + proof, + owner, + expression, + &checked, + &program.resource_source + )) + .count(), + 1 + ); + } + Ok(()) + }) + .unwrap(); + } + bake(&mut program, &checked, &values).unwrap(); + crate::actor_handlers::validate_program(&program).unwrap(); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + assert!(crate::resource_materialization::functions_equal( + &source, + program.resource_source.functions() + )); + } +} + +#[test] +fn required_constructor_and_handler_renew_snapshots_without_rebuilding_persistent_views() { + for release in [false, true] { + let (mut program, checked, values) = setup(VIEWS, release); + let before = program.clone(); + let constructor = before + .functions + .iter() + .find(|function| !function.actor_state_views.is_empty()) + .unwrap(); + assert_eq!(constructor.actor_state_views.len(), 2); + let handler = before + .functions + .iter() + .find(|function| function.actor_handler.is_some()) + .unwrap(); + let roles = handler.actor_handler.as_ref().unwrap().roles().to_vec(); + bake(&mut program, &checked, &values).unwrap(); + let after = &program.functions[constructor.id.index() as usize]; + for (old, new) in constructor + .actor_state_views + .iter() + .zip(&after.actor_state_views) + { + assert!(old.matches_constructor(constructor)); + assert!(!old.matches_constructor(after)); + assert!(new.matches_constructor(after)); + assert_eq!(old.local(), new.local()); + assert_eq!(old.root(), new.root()); + assert_eq!(old.field(), new.field()); + assert_eq!(old.initializer(), new.initializer()); + new.validate(&after.locals, &checked.checked().interner) + .unwrap(); + } + let after_handler = &program.functions[handler.id.index() as usize]; + assert_eq!( + after_handler.actor_handler.as_ref().unwrap().roles(), + roles.as_slice() + ); + crate::actor_handlers::validate_program(&program).unwrap(); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + } +} + +#[test] +fn actual_required_bake_receipt_retains_tokens_cache_program_and_arena() { + for release in [false, true] { + let (before, checked, values) = setup(WITNESS, release); + let types = &checked.checked().interner; + let receipt = actual_bake(&before, &checked, &values, types); + let output = receipt.candidate(); + let replay = receipt + .replay(&before.clone(), &values.clone(), types) + .unwrap(); + assert!(same_program(&output, &replay)); + assert_eq!(materialized_plans(&output), materialized_plans(&replay)); + assert!(!materialized_plans(&output).is_empty()); + let other = actual_bake(&before, &checked, &values, types).candidate(); + assert_ne!(materialized_plans(&output), materialized_plans(&other)); + assert!(receipt.authenticate_output(&before, &other, types).is_err()); + let other_cache = required(&checked); + assert!(!other_cache.same_checked_cache(&values)); + assert!(receipt.replay(&before, &other_cache, types).is_err()); + let mut wrong_cache_output = output.clone(); + assert!( + crate::actor_handlers::required_bake( + &mut wrong_cache_output, + &before, + &other_cache, + types, + &receipt + ) + .is_err() + ); + assert!(same_program(&wrong_cache_output, &output)); + let (foreign, foreign_checked, _) = setup(WITNESS, release); + let foreign_types = &foreign_checked.checked().interner; + assert!(!std::ptr::eq(types, foreign_types)); + assert!(receipt.replay(&before, &values, foreign_types).is_err()); + assert!(receipt.replay(&foreign, &values, types).is_err()); + let proof = values.checked_required_values(&checked).unwrap().remove(0); + let mut public_mirror = values.clone(); + public_mirror.insert( + proof.source_span(), + jett_comptime::ComptimeContext::default(), + Value::Int64(99), + ); + assert!(same_program( + &receipt.replay(&before, &public_mirror, types).unwrap(), + &output + )); + } +} + +#[test] +fn actor_required_snapshot_seam_refuses_complete_input_and_output_edits_transactionally() { + for release in [false, true] { + let (before, checked, values) = setup(WITNESS, release); + let types = &checked.checked().interner; + let receipt = actual_bake(&before, &checked, &values, types); + let output = receipt.candidate(); + for corruption in 0..8 { + let mut changed = output.clone(); + let handler = changed + .functions + .iter() + .position(|function| function.actor_handler.is_some()) + .unwrap(); + match corruption { + 0 => changed.functions[handler].body.span.end += 1, + 1 => changed.functions[handler].capture_count += 1, + 2 => { + changed.functions[handler].locals[0].mutable = + !changed.functions[handler].locals[0].mutable + } + 3 => changed.functions.swap(0, 1), + 4 => changed.functions[handler].actor_handler = None, + 5 => { + changed + .equality_methods + .insert(TypeInterner::INT64, FunctionId::new(0)); + } + 6 => { + let mut extra = changed.functions[0].clone(); + extra.id = FunctionId::new(changed.functions.len() as u32); + changed.functions.push(extra); + } + 7 => { + let (foreign, _, _) = setup(WITNESS, release); + changed.functions[handler].actor_handler = foreign + .functions + .iter() + .find(|function| function.actor_handler.is_some()) + .unwrap() + .actor_handler + .clone(); + } + _ => unreachable!(), + } + let changed_before = changed.clone(); + assert!( + crate::actor_handlers::required_bake( + &mut changed, + &before, + &values, + types, + &receipt + ) + .is_err() + ); + assert!(same_program(&changed, &changed_before)); + } + let mut changed_before = before.clone(); + changed_before.functions[0].span.end += 1; + let mut candidate = output.clone(); + assert!( + crate::actor_handlers::required_bake( + &mut candidate, + &changed_before, + &values, + types, + &receipt + ) + .is_err() + ); + assert!(same_program(&candidate, &output)); + let mut forged = ExplicitComptimeValues::default(); + let proof = values.checked_required_values(&checked).unwrap().remove(0); + forged.insert( + proof.source_span(), + jett_comptime::ComptimeContext::default(), + proof.value().clone(), + ); + let mut unchanged = before.clone(); + assert!(bake(&mut unchanged, &checked, &forged).is_err()); + assert!(same_program(&unchanged, &before)); + } +} + +#[test] +fn actor_required_materializer_rejects_forged_values_locals_and_late_failure_without_publication() { + for release in [false, true] { + let (before, checked, values) = setup(SCALAR, release); + let types = &checked.checked().interner; + let functions = function_value_candidates( + &before.functions, + &checked.checked().method_value_definitions, + ); + for corruption in 0..5 { + let mut program = before.clone(); + let mut calls = 0; + let result = materialize_checked_reflected_values( + &mut program, + &values, + types, + |value, ty, span, locals, bindings| { + calls += 1; + if corruption == 4 && calls == 2 { + return Err("second checked replacement refused".into()); + } + let mut output = value_expression( + value, + ty, + span, + &mut ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals, + bindings, + }, + )?; + match corruption { + 0 => output.kind = ExpressionKind::Int(6), + 1 => locals[0].mutable = !locals[0].mutable, + 2 => { + let mut hidden = locals[0].clone(); + hidden.id = LocalId::new(locals.len() as u32); + hidden.mutable = false; + hidden.view_source = None; + hidden.ty = ty; + hidden.debug_ty = ty; + let id = hidden.id; + locals.push(hidden); + bindings.push(Statement { + kind: StatementKind::Let { + local: id, + value: output.clone(), + }, + span, + }); + } + 3 => output.span.end += 1, + 4 => {} + _ => unreachable!(), + } + Ok(output) + }, + ); + assert!(result.is_err(), "corruption {corruption}"); + assert!(same_program(&program, &before)); + } + } +} + +#[test] +fn required_float_actor_snapshots_preserve_nan_payloads_and_signed_zero_bits() { + const SOURCE: &str = "namespace app +function make_nan() returns float64: + return 0.0 / 0.0 +actor Floating: + receive nan_value responds float64: + float64 baked = comptime make_nan() + respond baked + receive positive responds float64: + float64 baked = comptime 0.0 + respond baked + receive negative responds float64: + float64 baked = comptime -0.0 + respond baked +function main() returns nothing: + Floating worker = spawn Floating() + float64 first = ask worker.nan_value + float64 second = ask worker.positive + float64 third = ask worker.negative + return nothing +"; + for release in [false, true] { + let (mut program, checked, values) = setup(SOURCE, release); + bake(&mut program, &checked, &values).unwrap(); + crate::actor_handlers::validate_program(&program).unwrap(); + for (name, expected) in [ + ("Floating.nan_value", None), + ("Floating.positive", Some(0.0f64.to_bits())), + ("Floating.negative", Some((-0.0f64).to_bits())), + ] { + let handler = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let entry = handler.actor_handler.as_ref().unwrap(); + let mut body = handler.body.clone(); + let mut bits = Vec::new(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Float(value) = expression.kind { + bits.push(value.to_bits()); + } + Ok(()) + }) + .unwrap(); + assert_eq!(bits.len(), 1); + if let Some(expected) = expected { + assert_eq!(bits[0], expected); + } else { + assert!(f64::from_bits(bits[0]).is_nan()); + } + assert!(entry.validate(handler).is_ok()); + let mut changed = handler.clone(); + crate::resource_materialization::walk_block(&mut changed.body, &mut |expression| { + if let ExpressionKind::Float(value) = &mut expression.kind { + let mask = if value.is_nan() { 1 } else { 1u64 << 63 }; + *value = f64::from_bits(value.to_bits() ^ mask); + } + Ok(()) + }) + .unwrap(); + assert!( + entry.validate(&changed).is_err(), + "exact {name} float bits must remain authenticated" + ); + } + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + } +} diff --git a/crates/jett_hir/src/borrowed_match_tests.rs b/crates/jett_hir/src/borrowed_match_tests.rs new file mode 100644 index 00000000..97052702 --- /dev/null +++ b/crates/jett_hir/src/borrowed_match_tests.rs @@ -0,0 +1,186 @@ +//! Source-derived borrowed match payload transport and corruption controls. +use super::*; +use jett_diagnostics::Severity; + +const SOURCE: &str = r#"namespace app +struct Payload: + values: list[int64] +enum Choice: + first(data: Payload) + second(data: Payload) + empty +enum OtherChoice: + foreign(data: Payload) +function observe(view values: list[int64]) returns int64: + return list.length[int64](view values) +function observe_payload(view data: Payload) returns int64: + return observe(view data.values) +function take(data: Payload) returns int64: + return observe(view data.values) +function inspect(view choice: Choice) returns int64: + match choice: + first(data): + Payload alias = view data + return observe_payload(view data) + observe_payload(view alias) + observe(view data.values) + second(data): + return observe_payload(view data) + empty: + return 0 +function owned(choice: Choice) returns int64: + match choice: + first(data): + return take(data) + second(data): + return take(data) + empty: + return 0 +"#; + +fn checked_source(source: &str) -> (Program, CheckResult) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let stdlib_file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let stdlib_source = + std::fs::read_to_string(root.join("stdlib/list.jett")).expect("actual shipped list facade"); + let mut stdlib = jett_parser::parse(&stdlib_source, stdlib_file); + assert!(stdlib.errors.is_empty(), "{:?}", stdlib.errors); + stdlib.module.items.extend(parsed.module.items); + let module = Module { + items: stdlib.module.items, + span: parsed.module.span, + }; + let resolved = jett_resolve::resolve(&module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (file, SourceOrigin::Project), + (stdlib_file, SourceOrigin::Stdlib), + ]); + let program = lower(&module, &resolved, &checked, &origins).expect("checked match lowers"); + validate_backend_types(&program, &checked.interner).expect("genuine match transport"); + (program, checked) +} + +fn inspect_index(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == "inspect") + .expect("source inspect") +} + +fn matched(function: &mut Function) -> (&mut Expression, &mut Vec) { + match &mut function.body.statements[0].kind { + StatementKind::Match { scrutinee, arms } => (scrutinee, arms), + _ => panic!("source match"), + } +} + +#[test] +fn borrowed_match_payload_calls_keep_direct_projected_and_alias_origins() { + checked_source(SOURCE); + checked_source( + "namespace app\nenum Number:\n number(value: int64)\nfunction identity(value: int64) returns int64:\n return value\nfunction inspect(view number: Number) returns int64:\n match number:\n number(value):\n return identity(value)\n", + ); +} + +#[test] +fn borrowed_match_transport_preserves_transparent_scrutinee_views() { + for (declaration, input, scrutinee) in [ + ("", "Choice", "(view choice)"), + ("", "secret[Choice]", "declassify(view choice)"), + ( + "type RefinedChoice = Choice where true\n", + "RefinedChoice", + "coarsen(view choice)", + ), + ] { + let source = format!( + "namespace app\nstruct Payload:\n values: list[int64]\nenum Choice:\n item(data: Payload)\n{declaration}function observe(view data: Payload) returns int64:\n return list.length[int64](view data.values)\nfunction inspect(view choice: {input}) returns int64:\n match {scrutinee}:\n item(data):\n return observe(view data)\n" + ); + checked_source(&source); + } +} + +#[test] +fn changed_match_tag_slot_owner_or_scope_cannot_reuse_source_call_authority() { + let (program, checked) = checked_source(SOURCE); + let index = inspect_index(&program); + let first = match &program.functions[index].body.statements[0].kind { + StatementKind::Match { arms, .. } => arms[0].bindings[0], + _ => panic!("source match"), + }; + let foreign_enum = checked + .interner + .type_ids() + .find(|ty| { + matches!(checked.interner.resolve(*ty), Type::Enum(id) + if checked.interner.resolve_enum(*id).name.ends_with("OtherChoice")) + }) + .expect("different nominal enum"); + for case in 0..8 { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + match case { + 0 => { + let (_, arms) = matched(function); + arms[0].variant = arms[1].variant; + } + 1 => { + let (_, arms) = matched(function); + arms[0].bindings.clear(); + } + 2 => { + let (_, arms) = matched(function); + arms[0].bindings[0] = arms[1].bindings[0]; + } + 3 => { + let (_, arms) = matched(function); + arms[0].bindings.push(first); + } + 4 => { + let (scrutinee, _) = matched(function); + scrutinee.ty = foreign_enum; + } + 5 => { + function.locals[first.index() as usize].ty = TypeInterner::INT64; + } + 6 => { + let (_, arms) = matched(function); + let returned = arms[0].body.statements.pop().expect("source call"); + function.body.statements.push(returned); + } + 7 => { + let (_, arms) = matched(function); + let returned = arms[0].body.statements.pop().expect("source call"); + arms[1].body.statements.push(returned); + } + _ => unreachable!(), + } + let errors = validate_backend_types(&changed, &checked.interner) + .expect_err("changed match binding provenance"); + assert!( + errors.iter().any(|error| error.message.contains("match") + || error.message.contains("borrowed binding") + || error.message.contains("binding metadata")), + "{case}: {errors:?}" + ); + } +} diff --git a/crates/jett_hir/src/borrowed_sum_view_tests.rs b/crates/jett_hir/src/borrowed_sum_view_tests.rs new file mode 100644 index 00000000..b4bdd454 --- /dev/null +++ b/crates/jett_hir/src/borrowed_sum_view_tests.rs @@ -0,0 +1,380 @@ +use super::*; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; + +const SUPPORT: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const OCCUPIED: &str = include_str!( + "../../jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett" +); + +fn checked_resource_source(source: &str, release: bool) -> Arc { + let support = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = jett_parser::parse(SUPPORT, support); + let primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(primary.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}: {:?}", error.diagnostics())), + ) +} + +fn observer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + ["observe_optional", "observe_result"] + .contains(&function.identity.declaration.name.as_str()) + }) + .expect("genuine observer function") +} + +fn alias_initializer(function: &Function) -> (&Local, &Expression) { + let alias = function + .locals + .iter() + .find(|local| local.name == "token") + .expect("borrowed payload local"); + let value = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == alias.id => Some(value), + _ => None, + }) + .expect("original payload initializer"); + (alias, value) +} + +#[test] +fn borrowed_sum_views_lower_shared_checked_sources() { + let sources = [ + include_str!( + "../../jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett" + ), + OCCUPIED, + include_str!( + "../../jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked_resource_source(source, release); + let program = lower_checked_resource_program(&checked).unwrap(); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + assert!(program.resource_source.belongs_to(&checked)); + assert_eq!(program.resource_source.functions(), program.functions); + let function = observer(&program); + let (alias, initializer) = alias_initializer(function); + let backing = function + .locals + .iter() + .find(|local| local.name == "outcome") + .unwrap(); + assert_eq!(alias.view_source, Some(backing.id)); + assert_ne!(alias.ty, backing.ty); + assert!(function.params.iter().any(|parameter| { + parameter.local == backing.id && parameter.mode == ParamMode::View + })); + let handle = borrowed_sum_view_initializer( + initializer, + backing.id, + backing.ty, + alias.ty, + &checked.checked().interner, + ) + .unwrap() + .unwrap(); + assert!(matches!(handle.kind, ExpressionKind::Handle { .. })); + } + } +} + +#[test] +fn borrowed_sum_views_reject_changed_hir_projection() { + let checked = checked_resource_source(OCCUPIED, false); + let program = lower_checked_resource_program(&checked).unwrap(); + let function = observer(&program); + let (alias, _) = alias_initializer(function); + let alias_id = alias.id; + let backing_id = alias.view_source.unwrap(); + let foreign_id = function.params[0].local; + for change in [ + "kind", + "source", + "source type", + "endpoint", + "inner view", + "outer view", + "error binding", + "default", + "conditional default", + "orphan", + ] { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "observe_optional") + .unwrap(); + if change == "orphan" { + function.body.statements.retain(|statement| { + !matches!(statement.kind, StatementKind::Let { local, .. } if local == alias_id) + }); + } else { + let source_type = function.locals[backing_id.index() as usize].ty; + if change == "endpoint" { + function.locals[alias_id.index() as usize].ty = source_type; + } + let value = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { local, value } if *local == alias_id => Some(value), + _ => None, + }) + .unwrap(); + if change == "outer view" { + let ExpressionKind::View(inner) = &value.kind else { + panic!("original outer view"); + }; + *value = inner.as_ref().clone(); + } else { + if change == "endpoint" { + value.ty = source_type; + } + let ExpressionKind::View(handle) = &mut value.kind else { + panic!("original outer view"); + }; + if change == "endpoint" { + handle.ty = source_type; + } + let ExpressionKind::Handle { + target, + kind, + error_local, + failure, + } = &mut handle.kind + else { + panic!("original sum Handle"); + }; + match change { + "kind" => *kind = HandleKind::Result, + "error binding" => *error_local = Some(foreign_id), + "inner view" => { + let ExpressionKind::View(inner) = &target.kind else { + panic!("original inner view"); + }; + *target = inner.clone(); + } + "source" | "source type" => { + let ExpressionKind::View(backing) = &mut target.kind else { + panic!("original inner view"); + }; + if change == "source" { + backing.kind = ExpressionKind::Local(foreign_id); + } else { + backing.ty = alias.ty; + } + } + "default" | "conditional default" => { + let alternate = Statement { + kind: StatementKind::HandleDefault(Expression { + kind: ExpressionKind::Local(foreign_id), + ty: alias.ty, + span: handle.span, + }), + span: handle.span, + }; + let alternate = if change == "conditional default" { + Statement { + kind: StatementKind::If { + condition: Expression { + kind: ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span: handle.span, + }, + then_block: Block { + statements: vec![alternate], + span: handle.span, + }, + else_block: None, + }, + span: handle.span, + } + } else { + alternate + }; + failure.statements.insert(0, alternate); + } + "endpoint" => {} + _ => panic!("bounded mutation"), + } + } + } + let errors = validate_backend_types(&changed, &checked.checked().interner).unwrap_err(); + assert!( + errors.iter().any(|error| { + error.message.contains("borrowed Handle") + || error.message.contains("borrowed alias") + || error.message.contains("no validated initializer") + }), + "{change}: {errors:?}" + ); + } +} + +#[test] +fn borrowed_sum_views_refuse_temporaries_and_outer_handler_defaults() { + for initializer in [ + "view((view temporary()) handle:\n return nothing\n )", + "view((view source) handle:\n default list(9)\n )", + "view((view source) handle:\n if flag:\n default list(9)\n return nothing\n )", + "view(source handle:\n return nothing\n )", + ] { + let source = format!( + "namespace app\nfunction temporary() returns optional[list[int64]]:\n return some(list(1))\nfunction inspect(view source: optional[list[int64]], flag: bool) returns nothing:\n list[int64] alias = {initializer}\n return nothing\n" + ); + let errors = crate::local_view_tests::checked_source(&source, false).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("native borrowed alias")), + "{initializer}: {errors:?}" + ); + } +} + +#[test] +fn borrowed_sum_views_keep_nested_handle_defaults_and_generic_payload_identity() { + let source = r#"namespace app +function inspect[T](view source: optional[T], view result_source: result[T, string]) returns nothing: + T alias = view((view source) handle: + optional[int64] missing = none + int64 marker = missing handle: + default 7 + return nothing + ) + T result_alias = view((view result_source) handle error: + return nothing + ) + return nothing +function main() returns nothing: + inspect[list[int64]](view some(list(1)), view ok(list(2))) + inspect[int64](view some(3), view ok(4)) + return nothing +"#; + let (program, checked) = crate::local_view_tests::checked_source(source, false).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let instances = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(instances.len(), 2); + for function in instances { + for (name, source_name) in [("alias", "source"), ("result_alias", "result_source")] { + let alias = function + .locals + .iter() + .find(|local| local.name == name) + .unwrap(); + let backing = function + .locals + .iter() + .find(|local| local.name == source_name) + .unwrap(); + if alias.ty == TypeInterner::INT64 { + assert_eq!(alias.view_source, None, "implicitly copyable payload"); + } else { + assert_eq!(alias.view_source, Some(backing.id)); + let value = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == alias.id => Some(value), + _ => None, + }) + .unwrap(); + assert!( + borrowed_sum_view_initializer( + value, + backing.id, + backing.ty, + alias.ty, + &checked.interner + ) + .unwrap() + .is_some() + ); + } + } + } +} diff --git a/crates/jett_hir/src/call_observation.rs b/crates/jett_hir/src/call_observation.rs new file mode 100644 index 00000000..55e43388 --- /dev/null +++ b/crates/jett_hir/src/call_observation.rs @@ -0,0 +1,433 @@ +//! Constructor-owned descriptor storage snapshots in original checked tests. +//! This is an exact reflected-read proof, never a type-only observation policy. + +use super::*; +use crate::call_ownership::{ + ArgumentOwnership, CallerBindingFact, CallerBindingMode, CallerOrigin, ObservationProof, + OwnershipLocalInfo, SourceCallOwnership, +}; +use jett_typecheck::{CheckedCallerEffect, CheckedCallerSyntax, CheckedOwnershipContext}; + +#[derive(Debug, Clone)] +pub(crate) struct OriginalBinding { + original: CheckedBindingFact, + current: CallerBindingFact, + value: Expression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Binding { + original: CheckedBindingFact, + current: CallerBindingFact, +} + +#[derive(Clone)] +pub(crate) struct PurposeProof { + checked: Arc, + call_span: Span, + source_span: Span, + context: CheckedOwnershipContext, + root: CallerBindingFact, + requested: TypeId, + bindings: Vec, + constructed_types: Vec, + admissions: Vec, +} + +impl std::fmt::Debug for PurposeProof { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PurposeObservationProof") + .field("call_span", &self.call_span) + .field("source_span", &self.source_span) + .field("context", &self.context) + .field("root", &self.root) + .field("bindings", &self.bindings) + .finish_non_exhaustive() + } +} + +impl PartialEq for PurposeProof { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.checked, &other.checked) + && self.call_span == other.call_span + && self.source_span == other.source_span + && self.context == other.context + && self.root == other.root + && self.requested == other.requested + && self.bindings == other.bindings + && self.constructed_types == other.constructed_types + && self.admissions == other.admissions + } +} +impl Eq for PurposeProof {} + +fn test_context(context: CheckedOwnershipContext) -> bool { + matches!( + context, + CheckedOwnershipContext::Verify | CheckedOwnershipContext::Property + ) +} + +/// Called only after an original VarDecl's checked definition and local exist. +pub(crate) fn record( + body: &mut BodyLowerer<'_, '_>, + declaration: &ast::VarDecl, + definition: DefId, + local: LocalId, + value: &Expression, +) { + let Some(checked) = &body.parent.reflected_checked_program else { + return; + }; + if !test_context(body.ownership_context) + || !std::ptr::eq(body.parent.module, checked.module()) + || !std::ptr::eq(body.parent.check, checked.checked()) + || declaration.mutable + { + return; + } + let Some(original) = body.binding_facts.get(&declaration.name.span).copied() else { + return; + }; + let Some(stored) = body.locals.get(local.index() as usize) else { + return; + }; + if original.definition != definition + || original.mode != CheckedBindingMode::Owned + || original.mutable + || original.ty != stored.ty + || value.ty != stored.ty + || stored.id != local + || stored.view_source.is_some() + || checked + .checked() + .binding_facts + .get(&original.declaration_span) + != Some(&original) + || !matches!( + body.parent.check.interner.resolve(stored.ty), + Type::Struct(_) | Type::Function { .. } + ) + || crate::call_ownership::observation_data_type(&body.parent.check.interner, stored.ty) + { + return; + } + body.observation_initializers.insert( + local, + OriginalBinding { + original, + current: CallerBindingFact { + local, + declaration_span: original.declaration_span, + ty: original.ty, + mode: CallerBindingMode::Owned, + mutable: false, + }, + value: value.clone(), + }, + ); +} + +/// No supplied mutable expression can create this record: its backing rows +/// came exclusively from the original checked BodyLowerer above. +pub(crate) fn construct( + body: &BodyLowerer<'_, '_>, + call_span: Span, + parameter_index: usize, + source_span: Span, + origin: &CallerOrigin, + expression: &Expression, + actual_type: TypeId, +) -> Result, String> { + let types = &body.parent.check.interner; + if parameter_index != 0 + || !test_context(body.ownership_context) + || !matches!(types.resolve(actual_type), Type::Struct(_)) + || body.intrinsic_ids.get(&call_span) != Some(&IntrinsicId::TypeFieldValue) + { + return Ok(None); + } + let Some(checked) = &body.parent.reflected_checked_program else { + return Ok(None); + }; + if !std::ptr::eq(body.parent.module, checked.module()) + || !std::ptr::eq(body.parent.check, checked.checked()) + || checked.checked().call_ownership.get(&call_span) != body.call_ownership.get(&call_span) + { + return Err("observing reflected holder lost its original checked purpose".into()); + } + let Some(arguments) = body.intrinsic_type_arguments.get(&call_span) else { + return Ok(None); + }; + let [owner, requested] = arguments.as_slice() else { + return Ok(None); + }; + if *owner != actual_type || !matches!(types.resolve(*requested), Type::Function { .. }) { + return Ok(None); + } + let Some(layout) = + crate::reflected_fields::reflected_field_layout(types, IntrinsicId::TypeFieldValue, *owner) + else { + return Ok(None); + }; + let admissions = layout + .into_iter() + .filter_map(|(_, _, source)| { + (source != *requested) + .then(|| { + checked + .checked() + .reflected_callable_plan(source, *requested) + }) + .flatten() + }) + .collect::>(); + if admissions.is_empty() { + return Ok(None); + } + let CallerOrigin::Binding(root) = origin else { + return Err("observing reflected holder has no original owning local".into()); + }; + let mut value = expression; + while let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + if !matches!(value.kind, ExpressionKind::Local(local) if local == root.local) + || value.ty != actual_type + || root.ty != actual_type + || root.mode != CallerBindingMode::Owned + || root.mutable + { + return Err("observing reflected holder differs from its immutable checked root".into()); + } + let mut proof = PurposeProof { + checked: checked.clone(), + call_span, + source_span, + context: body.ownership_context, + root: *root, + requested: *requested, + bindings: Vec::new(), + constructed_types: Vec::new(), + admissions, + }; + let mut captures = Vec::new(); + reconstruct(body, value, &mut proof, &mut captures, &mut HashSet::new())?; + captures.sort_unstable_by_key(|(id, _)| id.index()); + captures.dedup(); + Ok(Some(ObservationProof::purpose( + actual_type, + captures, + proof, + ))) +} + +fn reconstruct( + body: &BodyLowerer<'_, '_>, + value: &Expression, + proof: &mut PurposeProof, + captures: &mut Vec<(LocalId, TypeId)>, + active: &mut HashSet, +) -> Result<(), String> { + let types = &body.parent.check.interner; + if !proof.constructed_types.contains(&value.ty) { + proof.constructed_types.push(value.ty); + } + if crate::call_ownership::observation_data_type(types, value.ty) { + return Ok(()); + } + if let ExpressionKind::Local(local) = &value.kind { + if !active.insert(*local) { + return Err("observing reflected holder has cyclic local backing".into()); + } + let row = body + .observation_initializers + .get(local) + .ok_or("observing reflected holder has no original immutable constructor row")?; + if row.current.ty != value.ty || row.value.ty != value.ty { + return Err("observing reflected holder backing has another checked type".into()); + } + if !proof + .bindings + .iter() + .any(|binding| binding.current.local == *local) + { + proof.bindings.push(Binding { + original: row.original, + current: row.current, + }); + } + reconstruct(body, &row.value, proof, captures, active)?; + active.remove(local); + return Ok(()); + } + if matches!(types.resolve(value.ty), Type::Function { .. }) { + let descriptor = body.observation_proof(value, value.ty)?; + captures.extend_from_slice(descriptor.captures()); + return Ok(()); + } + let ( + Type::Struct(id), + ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + .. + }, + ) = (types.resolve(value.ty), &value.kind) + else { + return Err( + "observing reflected holder has no supported original constructor proof".into(), + ); + }; + let declared = &types.resolve_struct(*id).fields; + let mut order = evaluation_order.clone(); + order.sort_unstable(); + if *struct_type != value.ty + || fields.len() != declared.len() + || order != (0..fields.len()).collect::>() + || types + .nominal_type_arguments(value.ty) + .iter() + .any(|ty| !crate::call_ownership::observation_data_type(types, *ty)) + { + return Err("observing reflected holder constructor changed its checked layout".into()); + } + for (field, (_, expected)) in fields.iter().zip(declared) { + if field.ty != *expected { + return Err("observing reflected holder has another declared field type".into()); + } + reconstruct(body, field, proof, captures, active)?; + } + Ok(()) +} + +impl PurposeProof { + pub(crate) fn metadata_types(&self, mut visit: impl FnMut(TypeId)) { + for &ty in &self.constructed_types { + visit(ty); + } + visit(self.requested); + } + pub(crate) fn metadata_locals(&self, mut visit: impl FnMut(LocalId)) { + visit(self.root.local); + for binding in &self.bindings { + visit(binding.current.local); + } + } + pub(crate) fn remap_locals( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + self.root.local = map(self.root.local)?; + for binding in &mut self.bindings { + binding.current.local = map(binding.current.local)?; + } + Ok(()) + } + pub(crate) fn validate( + &self, + argument: &ArgumentOwnership, + source: &SourceCallOwnership, + types: &TypeInterner, + local: &impl Fn(LocalId) -> Option, + ) -> Result<(), String> { + if !test_context(self.context) + || source.context != self.context + || source.target + != crate::call_ownership::CallTarget::Intrinsic(IntrinsicId::TypeFieldValue) + || argument.effect != CheckedCallerEffect::ObserveData + || argument.syntax != CheckedCallerSyntax::WrittenView + || argument.parameter_index != 0 + || argument.source_index != 0 + || argument.source_span != self.source_span + || argument.actual_type != self.root.ty + || argument.origin != CallerOrigin::Binding(self.root) + || self + .bindings + .first() + .is_none_or(|binding| binding.current != self.root) + || self.admissions.is_empty() + || self.admissions.iter().any(|admission| { + admission.requested_type() != self.requested + || !admission.validate(self.checked.checked()) + }) + { + return Err( + "call ownership reflected observation lost its exact checked read/root/purpose" + .into(), + ); + } + let original_types = &self.checked.checked().interner; + for &ty in &self.constructed_types { + if ty.index() as usize >= types.len() + || ty.index() as usize >= original_types.len() + || types.resolve(ty) != original_types.resolve(ty) + || types.nominal_type_arguments(ty) != original_types.nominal_type_arguments(ty) + || matches!(original_types.resolve(ty), Type::Struct(id) + if types.resolve_struct(*id) != original_types.resolve_struct(*id)) + { + return Err( + "call ownership reflected observation changed an original constructor type" + .into(), + ); + } + } + for binding in &self.bindings { + let fact = binding.current; + let Some(stored) = local(fact.local) else { + return Err( + "call ownership reflected observation binding is outside its owner".into(), + ); + }; + if self + .checked + .checked() + .binding_facts + .get(&binding.original.declaration_span) + != Some(&binding.original) + || binding.original.mode != CheckedBindingMode::Owned + || binding.original.mutable + || binding.original.ty != fact.ty + || binding.original.declaration_span != fact.declaration_span + || fact.mode != CallerBindingMode::Owned + || fact.mutable + || stored.mutable + || stored.ty != fact.ty + || stored.view_source.is_some() + || stored.is_view_parameter + || stored.view_iteration.is_some() + || stored.span.file != fact.declaration_span.file + || fact.declaration_span.start < stored.span.start + || fact.declaration_span.end > stored.span.end + { + return Err("call ownership reflected observation lost original immutable binding ownership".into()); + } + } + Ok(()) + } + + /// This join relies on the separately mandatory whole-source/current graph + /// seal; keeping a signature or cloning a public packet cannot replace it. + pub(crate) fn validate_read(&self, expression: &Expression) -> Result<(), String> { + let ExpressionKind::Intrinsic { + intrinsic: IntrinsicId::TypeFieldValue, + type_arguments, + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &expression.kind + else { + return Err("call ownership reflected observation has no sealed callable read".into()); + }; + if expression.span != self.call_span || type_arguments.as_slice() != [self.root.ty, self.requested] + || !plans.iter().any(|plan| matches!(&plan.action, ReflectedFieldAction::CallableReturn(callable) + if callable.authenticates_observation_read(&self.checked, self.call_span, self.context) + && self.admissions.contains(callable.checked_plan()))) + { return Err("call ownership reflected observation lost its original sealed callable read".into()); } + Ok(()) + } +} + +#[cfg(test)] +#[path = "call_observation_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/call_observation_tests.rs b/crates/jett_hir/src/call_observation_tests.rs new file mode 100644 index 00000000..a856a9b1 --- /dev/null +++ b/crates/jett_hir/src/call_observation_tests.rs @@ -0,0 +1,339 @@ +use super::*; + +// Exact original Source19/20 bytes; no Driver fixture path dependency. +const NAMED: &str = "namespace app\ntype Positive = int64 where value > 0\nstruct CallbackHolder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction main() returns nothing:\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n Positive value = observed()\n println(coarsen value)\n return nothing\n\nfunction ready_five() returns Positive:\n return 5\nverify lazy_callback_result:\n Positive baked = comptime ready_five()\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == coarsen baked\nproperty lazy_callback_trials:\n given repeat: bool\n Positive baked = comptime ready_five()\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == coarsen baked\n if repeat:\n assert coarsen observed() == 5\n"; +const CAPTURED: &str = "namespace app\ntype Positive = int64 where value > 0\nstruct CallbackHolder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction main() returns nothing:\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n Positive value = observed()\n println(coarsen value)\n return nothing\n\nverify captured_callback_result:\n int64 seed = 7\n CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == 7\nproperty captured_callback_trials:\n given repeat: bool\n int64 seed = 7\n CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == 7\n if repeat:\n assert coarsen observed() == 7\n"; + +fn checked(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .unwrap(), + ) +} + +fn fixture(source: &str) -> (Program, Arc) { + let checked = checked(source); + let program = lower_checked_ordinary_program_with_test_bodies(&checked) + .expect("original checked suite holders lower"); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + (program, checked) +} + +fn rejects_current_graph(program: &Program, checked: &jett_typecheck::CheckedResourceProgram) { + // This is the complete mandatory input gate used by MIR::lower, including + // the immutable reflected source graph before structural type validation. + let errors = validate(program) + .and_then(|()| validate_backend_types(program, &checked.checked().interner)) + .expect_err("changed current descriptor graph must not acquire snapshot authority"); + assert!( + errors + .iter() + .any(|error| error.message.contains("reflected callable") + || error.message.contains("call ownership")), + "{errors:?}" + ); +} + +fn reads(function: &Function) -> Vec { + let mut body = function.body.clone(); + let mut values = Vec::new(); + crate::resource_materialization::walk_block(&mut body, &mut |value| { + if matches!( + value.kind, + ExpressionKind::Intrinsic { + intrinsic: IntrinsicId::TypeFieldValue, + .. + } + ) { + values.push(value.clone()); + } + Ok(()) + }) + .unwrap(); + values +} + +#[test] +fn checked_named_and_captured_suite_holders_have_exact_storage_proofs() { + for (source, captured) in [(NAMED, false), (CAPTURED, true)] { + let (program, checked) = fixture(source); + assert!(program.resource_source.belongs_to(&checked)); + for kind in [DeclarationKind::Verify, DeclarationKind::Property] { + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == kind) + .unwrap(); + let values = reads(owner); + assert_eq!(values.len(), 1); + let ExpressionKind::Intrinsic { + ownership: CallOwnership::Source(call), + .. + } = &values[0].kind + else { + panic!("original reflected call") + }; + let observation = call.arguments[0].observation_proof().unwrap(); + let proof = observation + .purpose_proof() + .expect("constructor-owned purpose proof"); + proof.validate_read(&values[0]).unwrap(); + assert!(!crate::call_ownership::observation_data_type( + &checked.checked().interner, + call.arguments[0].actual_type + )); + assert_eq!(observation.captures().is_empty(), !captured); + assert_eq!(proof.root.mode, CallerBindingMode::Owned); + assert!(!proof.root.mutable); + assert_eq!(proof.bindings.len(), 1); + let mut metadata = Vec::new(); + CallOwnership::Source(call.clone()).metadata_local_ids(|id| metadata.push(id)); + assert!(metadata.contains(&proof.root.local)); + for &(capture, _) in observation.captures() { + assert!(metadata.contains(&capture)); + } + } + } +} + +#[test] +fn checked_holder_observation_refuses_same_signature_descriptor_and_body_substitution() { + let source = format!("{NAMED}\nfunction zero() returns int64:\n return 0\n"); + let (program, checked) = fixture(&source); + let zero = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "zero") + .unwrap() + .id; + let verify = program + .functions + .iter() + .position(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .unwrap(); + let mut substituted = program.clone(); + let StatementKind::Let { + value: + Expression { + kind: ExpressionKind::StructConstruct { fields, .. }, + .. + }, + .. + } = &mut substituted.functions[verify].body.statements[1].kind + else { + panic!("original holder initializer") + }; + assert!(matches!(fields[0].kind, ExpressionKind::FunctionRef(_))); + fields[0].kind = ExpressionKind::FunctionRef(zero); + rejects_current_graph(&substituted, &checked); + let mut body = program.clone(); + let five = body + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap(); + let StatementKind::Return(Some(value)) = &mut five.body.statements[0].kind else { + panic!("source callback return") + }; + value.kind = ExpressionKind::Int(0); + rejects_current_graph(&body, &checked); +} + +#[test] +fn checked_captured_holder_refuses_capture_producer_and_owner_changes() { + let (program, checked) = fixture(CAPTURED); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .unwrap(); + for mutation in 0..4 { + let mut changed = program.clone(); + let owner = &mut changed.functions[index]; + match mutation { + 0 => { + let StatementKind::Let { value, .. } = &mut owner.body.statements[0].kind else { + panic!("original capture producer") + }; + value.kind = ExpressionKind::Int(8); + } + 1 => owner.identity.declaration.kind = DeclarationKind::Function, + 2 => { + let holder = owner + .locals + .iter_mut() + .find(|local| local.name == "holder") + .unwrap(); + holder.mutable = true; + } + 3 => { + let seed = owner + .locals + .iter() + .find(|local| local.name == "seed") + .unwrap() + .id; + let holder = owner + .locals + .iter_mut() + .find(|local| local.name == "holder") + .unwrap(); + holder.view_source = Some(seed); + } + _ => unreachable!(), + } + rejects_current_graph(&changed, &checked); + } +} + +#[test] +fn purpose_observation_requires_same_checked_arc_and_sealed_callable_read() { + let (program, _) = fixture(NAMED); + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .unwrap(); + let value = reads(owner).remove(0); + let ExpressionKind::Intrinsic { + ownership: CallOwnership::Source(call), + .. + } = &value.kind + else { + panic!("checked read") + }; + let proof = call.arguments[0] + .observation_proof() + .unwrap() + .purpose_proof() + .unwrap(); + let mut foreign = proof.clone(); + foreign.checked = checked(NAMED); + assert!(foreign.validate_read(&value).is_err()); + let mut missing = value.clone(); + let ExpressionKind::Intrinsic { + field_validation, .. + } = &mut missing.kind + else { + unreachable!() + }; + *field_validation = Some(ReflectedFieldValidation::Read); + assert!(proof.validate_read(&missing).is_err()); + let property = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Property) + .unwrap(); + let property_read = reads(property).remove(0); + let ExpressionKind::Intrinsic { + field_validation: Some(validation), + .. + } = property_read.kind + else { + panic!("another genuine checked plan") + }; + let mut transplanted = value.clone(); + let ExpressionKind::Intrinsic { + field_validation, .. + } = &mut transplanted.kind + else { + unreachable!() + }; + *field_validation = Some(validation); + assert!(proof.validate_read(&transplanted).is_err()); + let mut moved = value.clone(); + moved.span.start += 1; + assert!(proof.validate_read(&moved).is_err()); +} + +#[test] +fn checked_immutable_holder_alias_retains_both_original_constructor_headers() { + let source = NAMED.replace( + " CallbackHolder holder = CallbackHolder(callback: five)\n", + " CallbackHolder holder = CallbackHolder(callback: five)\n CallbackHolder alias = holder\n", + ).replace("(view holder, view field)", "(view alias, view field)"); + let (program, _) = fixture(&source); + for kind in [DeclarationKind::Verify, DeclarationKind::Property] { + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == kind) + .unwrap(); + let value = reads(owner).remove(0); + let ExpressionKind::Intrinsic { + ownership: CallOwnership::Source(call), + .. + } = &value.kind + else { + panic!("checked alias read") + }; + let proof = call.arguments[0] + .observation_proof() + .unwrap() + .purpose_proof() + .unwrap(); + assert_eq!(proof.bindings.len(), 2); + assert_eq!( + proof.root.local, + owner + .locals + .iter() + .find(|local| local.name == "alias") + .unwrap() + .id + ); + assert_eq!( + proof.bindings[1].current.local, + owner + .locals + .iter() + .find(|local| local.name == "holder") + .unwrap() + .id + ); + proof.validate_read(&value).unwrap(); + } +} + +#[test] +fn purpose_observation_metadata_remap_preserves_original_definition_identity() { + let (program, _) = fixture(CAPTURED); + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Property) + .unwrap(); + let value = reads(owner).remove(0); + let ExpressionKind::Intrinsic { + ownership: CallOwnership::Source(call), + .. + } = &value.kind + else { + panic!("checked read") + }; + let original = call.arguments[0] + .observation_proof() + .unwrap() + .purpose_proof() + .unwrap(); + let mut remapped = original.clone(); + remapped + .remap_locals(&mut |id| Ok::<_, ()>(LocalId(id.index() + 100))) + .unwrap(); + assert_eq!( + remapped.root.local.index(), + original.root.local.index() + 100 + ); + assert_eq!(remapped.bindings[0].original, original.bindings[0].original); + assert_eq!(remapped.bindings[0].current.local, remapped.root.local); + let mut metadata = Vec::new(); + remapped.metadata_locals(|id| metadata.push(id)); + assert!(metadata.iter().all(|id| id.index() >= 100)); +} diff --git a/crates/jett_hir/src/call_ownership.rs b/crates/jett_hir/src/call_ownership.rs new file mode 100644 index 00000000..326789e4 --- /dev/null +++ b/crates/jett_hir/src/call_ownership.rs @@ -0,0 +1,5749 @@ +//! Caller authority retained separately from physical invocation operands. +use jett_common::Span; +use jett_typecheck::{ + CheckedCalleeAccess, CheckedCallerEffect, CheckedCallerSyntax, CheckedInvocationShape, + CheckedOwnershipContext, +}; +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::{Expression, ExpressionKind, FunctionId, FunctionIdentity, IntrinsicId, LocalId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallerViewSource { + Local(LocalId), + Other, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallerBindingMode { + Owned, + View { source: CallerViewSource }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallerBindingFact { + pub local: LocalId, + pub declaration_span: Span, + pub ty: TypeId, + pub mode: CallerBindingMode, + pub mutable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallerOrigin { + Binding(CallerBindingFact), + BorrowedProjection { source: CallerViewSource }, + OwnedFieldCopy { parent: CallerViewSource }, + OwnedExpression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArgumentOwnership { + pub source_span: Span, + pub source_index: usize, + pub parameter_index: usize, + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub syntax: CheckedCallerSyntax, + pub origin: CallerOrigin, + pub callee_access: CheckedCalleeAccess, + /// Actual implementation access; a validated helper bridge may differ. + pub physical_access: CheckedCalleeAccess, + pub effect: CheckedCallerEffect, + pub staging: ArgumentStaging, + /// Immutable source authority. MIR may stage an operand, never rewrite this witness. + witness: SourceArgumentWitness, + retained_snapshot: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceArgumentWitness { + source_span: Span, + source_index: usize, + parameter_index: usize, + actual_type: TypeId, + occurrence_type: TypeId, + parameter_type: TypeId, + syntax: CheckedCallerSyntax, + origin: CallerOrigin, + callee_access: CheckedCalleeAccess, + effect: CheckedCallerEffect, + context: CheckedOwnershipContext, + observation: Option, + handled_result: Option, + projection_root: Option, + match_origin: Option<(LocalId, crate::MatchBinding)>, + temporary_origin: Option>, +} + +/// Original checked flow occurrence, separate from the owner's stored type. +/// Only the Source lowerer can mint this; it grants no alias or loan authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceProjectionRoot { + local: LocalId, + span: Span, + occurrence_type: TypeId, + storage_type: TypeId, +} + +fn retained_snapshot_origin(origin: &CallerOrigin) -> bool { + matches!( + origin, + CallerOrigin::Binding(_) + | CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(_) + } + ) +} + +/// Checked ordinary-data endpoint permission, independent of caller consumption. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RetainedSnapshotProof { + actual_type: TypeId, + occurrence_type: TypeId, + /// Exact initial HIR header; pipeline View and raw source spans differ. + physical_span: Span, +} + +/// Exact original Handle backing; it grants no CFG acquisition authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceHandledResult { + ty: TypeId, + span: Span, +} + +/// Checked capture ownership, separate from a function's parameter/result types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObservationProof { + actual_type: TypeId, + captures: Vec<(LocalId, TypeId)>, + descriptor_checked: bool, + purpose: Option, +} + +impl ObservationProof { + pub(super) fn purpose( + actual_type: TypeId, + captures: Vec<(LocalId, TypeId)>, + purpose: crate::call_observation::PurposeProof, + ) -> Self { + Self { + actual_type, + captures, + descriptor_checked: false, + purpose: Some(purpose), + } + } + + pub(super) fn captures(&self) -> &[(LocalId, TypeId)] { + &self.captures + } + + #[cfg(test)] + pub(super) fn purpose_proof(&self) -> Option<&crate::call_observation::PurposeProof> { + self.purpose.as_ref() + } +} + +impl ArgumentOwnership { + pub fn source_witness(&self) -> &SourceArgumentWitness { + &self.witness + } + pub fn observation_proof(&self) -> Option<&ObservationProof> { + self.witness.observation.as_ref() + } + pub fn retained_snapshot_type(&self) -> Option { + self.retained_snapshot.map(|proof| proof.occurrence_type) + } + pub fn retained_snapshot_span(&self) -> Option { + self.retained_snapshot.map(|proof| proof.physical_span) + } +} + +impl SourceArgumentWitness { + /// Read-only proof closure; never execution liveness, a new loan, or a setter. + pub(crate) fn temporary_local_origin( + &self, + ) -> Option<&crate::temporary_local_views::TemporaryLocalArgument> { + self.temporary_origin.as_deref() + } + + pub fn match_origin(&self) -> Option<(LocalId, crate::MatchBinding)> { + self.match_origin + } + pub fn projection_root(&self) -> Option<(LocalId, Span, TypeId, TypeId)> { + self.projection_root.map(|root| { + ( + root.local, + root.span, + root.occurrence_type, + root.storage_type, + ) + }) + } + pub fn occurrence_type(&self) -> TypeId { + self.occurrence_type + } + pub fn syntax(&self) -> CheckedCallerSyntax { + self.syntax + } + pub fn context(&self) -> CheckedOwnershipContext { + self.context + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArgumentStaging { + Original, + Transferred { owner: LocalId }, + Copied { value: LocalId }, + Borrowed { loan: LocalId }, + Relinquished { owner: LocalId, loan: LocalId }, + RetainedSnapshot { owner: LocalId, loan: LocalId }, + Observed { owner: LocalId }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallTarget { + ResourceHook(crate::ResourceHookRef), + Function(FunctionId), + /// Proven source facade identity; its implementation body need not survive pruning. + Declaration { + identity: FunctionIdentity, + signature_type: TypeId, + }, + Interface { + function: FunctionId, + interface_type: TypeId, + method_index: usize, + }, + Indirect { + signature_type: TypeId, + }, + Intrinsic(IntrinsicId), +} + +/// An implementation helper does not change the original caller disposition. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallBridge { + ResourceHook { + hook: crate::ResourceHookRef, + }, + Direct, + TrustedIntrinsic { + intrinsic: IntrinsicId, + }, + JsonSource { + intrinsic: IntrinsicId, + function: FunctionId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceCallOwnership { + pub target: CallTarget, + pub shape: CheckedInvocationShape, + pub context: CheckedOwnershipContext, + /// Formal order; source_index retains original lexical order. + pub arguments: Vec, + pub bridge: CallBridge, + /// HIR-added metadata tail. It is never counted as a source actual. + pub generated_operands: Vec, + certificate: Box, + temporary_local_backing: Option, +} + +impl SourceCallOwnership { + pub(crate) fn temporary_local_backing(&self) -> Option<&crate::TemporaryLocalBacking> { + self.temporary_local_backing.as_ref() + } + pub(crate) fn set_temporary_local_backing(&mut self, proof: crate::TemporaryLocalBacking) { + self.temporary_local_backing = Some(proof); + } + pub(crate) fn clear_temporary_local_backing(&mut self) { + self.temporary_local_backing = None; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct SourceCallCertificate { + result_type: TypeId, + generated_operands: Vec, + target: CallTarget, + shape: CheckedInvocationShape, + context: CheckedOwnershipContext, + bridge: CallBridge, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GeneratedAcquisition { + Copy, + OwnedExpression, + Borrow { source: CallerViewSource }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedArgumentOwnership { + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub callee_access: CheckedCalleeAccess, + pub acquisition: GeneratedAcquisition, + pub staging: GeneratedArgumentStaging, + staging_certificate: GeneratedArgumentStaging, + witness: GeneratedArgumentWitness, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratedArgumentStaging { + Existing { loan: Option }, + OwnedProducer { owner: LocalId, loan: LocalId }, + OrdinarySnapshot { owner: LocalId, loan: LocalId }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedSnapshotProof { + actual_type: TypeId, +} + +impl GeneratedSnapshotProof { + pub fn actual_type(&self) -> TypeId { + self.actual_type + } +} + +/// Private original node identity. The exact discriminant is recorded even for +/// ordinary data leaves; a typed leaf is never interchangeable with a Clone. +#[derive(Debug, Clone, PartialEq, Eq)] +struct GeneratedProducerShape { + kind: std::mem::Discriminant, + ty: TypeId, + span: Span, + identity: GeneratedProducerIdentity, + backing: Vec, + ordinary_handle_source_snapshot: bool, + ordinary_local_snapshot: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum GeneratedProducerIdentity { + ActorGraphImport(crate::ActorGraphPlan), + ActorGraphStep(crate::ActorGraphInstruction), + ResourceHook(crate::ResourceHookRef), + ResourceInvoke { + hook: crate::ResourceHookRef, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Data, + Transparent(GeneratedTransparentShape), + Local(LocalId), + Constant(Span), + Function(FunctionId), + Closure { + function: FunctionId, + captures: Vec, + }, + Call { + function: FunctionId, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Intrinsic { + intrinsic: IntrinsicId, + type_arguments: Vec, + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Indirect { + operands: Vec<(TypeId, Span)>, + order: Vec, + }, + Struct { + owner: TypeId, + order: Vec, + validates_refinements: bool, + }, + Bitfield { + owner: TypeId, + order: Vec, + validates_widths: bool, + }, + Enum { + owner: TypeId, + variant: crate::VariantId, + order: Vec, + }, + Machine { + owner: TypeId, + state: crate::StateId, + }, + Transition { + owner: TypeId, + state: crate::StateId, + }, + Field { + owner: TypeId, + field: crate::FieldId, + }, + InterfaceCoerce(Vec), + FunctionAdapter(FunctionId), + Handle { + kind: crate::HandleKind, + error_local: Option, + failure_span: Span, + }, + Inline { + params: Vec, + view_params: Vec, + local_floor: u32, + body_span: Span, + }, + ActorSpawn { + constructor: Option, + actor_type: String, + order: Vec, + }, + ActorMessage { + handler: FunctionId, + message: String, + order: Vec, + }, + State(crate::StateId), +} + +/// Created only at the canonical extractor's checked Inline-to-descriptor join. +#[derive(Debug, Clone)] +pub(crate) struct GeneratedInlineExtraction { + original: GeneratedProducerShape, + replacement: GeneratedProducerShape, +} + +pub(crate) fn generated_inline_extraction( + original: &Expression, + replacement: &Expression, +) -> Option { + if !matches!(original.kind, ExpressionKind::InlineFunction { .. }) + || !matches!( + replacement.kind, + ExpressionKind::FunctionRef(_) | ExpressionKind::ClosureRef { .. } + ) + || original.ty != replacement.ty + || original.span != replacement.span + { + return None; + } + Some(GeneratedInlineExtraction { + original: GeneratedProducerShape::from_expression(original), + replacement: GeneratedProducerShape::from_expression(replacement), + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GeneratedTransparentShape { + View, + Coarsen, + Declassify, + RefinementValidated, +} + +/// Immutable original node, usable only after the consumer authenticates a +/// canonical owning storage definition. It cannot mint or replace a witness. +#[derive(Debug, Clone, Copy)] +pub struct GeneratedOriginalShape<'a> { + shape: &'a GeneratedProducerShape, +} + +impl<'a> GeneratedOriginalShape<'a> { + pub fn ty(&self) -> TypeId { + self.shape.ty + } + pub fn span(&self) -> Span { + self.shape.span + } + pub fn handle(&self) -> Option> { + matches!( + self.shape.identity, + GeneratedProducerIdentity::Handle { .. } + ) + .then_some(GeneratedHandleShape { shape: self.shape }) + } + pub fn validate_reconstructed( + &self, + value: &Expression, + mut validate_storage: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + self.shape.validate_lowered(value, &mut validate_storage) + } + pub fn validate_existing_local_snapshot( + &self, + value: &Expression, + mut validate_storage: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + if !self.shape.ordinary_local_snapshot { + return Err( + "call ownership generated node has no sealed existing Local snapshot permission" + .into(), + ); + } + let restored = self + .shape + .restore_local_snapshot(value) + .ok_or("call ownership generated node is not its exact existing Local snapshot")?; + self.shape + .validate_lowered(&restored, &mut validate_storage) + } +} + +/// Read-only original Handle node supplied only during independently +/// authenticated MIR reconstruction. It cannot mint or replace a certificate. +#[derive(Debug, Clone, Copy)] +pub struct GeneratedHandleShape<'a> { + shape: &'a GeneratedProducerShape, +} + +impl<'a> GeneratedHandleShape<'a> { + pub fn kind(&self) -> &'a crate::HandleKind { + let GeneratedProducerIdentity::Handle { kind, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + kind + } + pub fn result_type(&self) -> TypeId { + self.shape.ty + } + pub fn source_type(&self) -> TypeId { + self.shape.backing[0].ty + } + pub fn span(&self) -> Span { + self.shape.span + } + pub fn source_span(&self) -> Span { + self.shape.backing[0].span + } + pub fn error_local(&self) -> Option { + let GeneratedProducerIdentity::Handle { error_local, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + *error_local + } + pub fn failure_span(&self) -> Span { + let GeneratedProducerIdentity::Handle { failure_span, .. } = &self.shape.identity else { + unreachable!("private Handle shape"); + }; + *failure_span + } + pub fn validate_source_shape(&self, value: &Expression) -> Result<(), String> { + let mut reject = |_: &Expression, _: GeneratedOriginalShape<'a>| { + Err("call ownership generated Handle source requires its original tree".into()) + }; + self.validate_lowered_source_shape(value, &mut reject) + } + pub fn validate_lowered_source_shape( + &self, + value: &Expression, + mut validate_handle: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + let original = &self.shape.backing[0]; + match original.validate_lowered(value, &mut validate_handle) { + Ok(()) => Ok(()), + Err(error) => { + if !self.shape.ordinary_handle_source_snapshot { + return Err(error); + } + let Some(restored) = original.restore_local_snapshot(value) else { + return Err(error); + }; + original.validate_lowered(&restored, &mut validate_handle) + } + } + } +} + +fn generated_backing_values(value: &Expression) -> Vec<&Expression> { + use ExpressionKind as E; + match &value.kind { + // The exact opaque plan identity seals its immutable body separately. + E::ActorGraphImport(_) => Vec::new(), + E::IndirectCall { callee, .. } => vec![callee], + E::Field { base, .. } => vec![base], + E::Handle { target, .. } => vec![target], + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + fields.iter().collect() + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + payloads.iter().collect() + } + E::MachineTransition { + source, payloads, .. + } => std::iter::once(source.as_ref()).chain(payloads).collect(), + E::Binary { left, right, .. } => vec![left, right], + E::Unary { value, .. } + | E::View(value) + | E::Clone(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => vec![value], + E::ListConstruct { elements } => elements.iter().collect(), + E::MapConstruct { entries } => entries + .iter() + .flat_map(|entry| [&entry.key, &entry.value]) + .collect(), + E::StringInterpolation(parts) => parts + .iter() + .filter_map(|part| match part { + crate::StringSegment::Value(value) => Some(value), + _ => None, + }) + .collect(), + E::ActorSpawn { args, .. } | E::ActorGraphStep { args, .. } => args.iter().collect(), + E::ActorMessage { actor, args, .. } => { + std::iter::once(actor.as_ref()).chain(args).collect() + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ResourceInvoke { .. } + | E::ClosureRef { .. } + | E::Call { .. } + | E::Intrinsic { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) + | E::InlineFunction { .. } => Vec::new(), + } +} + +impl GeneratedProducerShape { + fn from_expression(value: &Expression) -> Self { + use ExpressionKind as E; + let occurrence = + |values: &[Expression]| values.iter().map(|value| (value.ty, value.span)).collect(); + let mut backing = Vec::new(); + let mut add = |value: &Expression| backing.push(Self::from_expression(value)); + let identity = match &value.kind { + E::ActorGraphImport(plan) => GeneratedProducerIdentity::ActorGraphImport(plan.clone()), + E::ActorGraphStep { instruction, args } => { + for value in args { + add(value); + } + GeneratedProducerIdentity::ActorGraphStep(instruction.clone()) + } + E::Local(local) => GeneratedProducerIdentity::Local(*local), + E::Constant { declaration } => GeneratedProducerIdentity::Constant(*declaration), + E::FunctionRef(function) => GeneratedProducerIdentity::Function(*function), + E::ResourceHookValue { hook } => GeneratedProducerIdentity::ResourceHook(hook.clone()), + E::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::ResourceInvoke { + hook: hook.clone(), + operands: occurrence(args), + order: evaluation_order.clone(), + }, + E::ClosureRef { function, captures } => GeneratedProducerIdentity::Closure { + function: *function, + captures: captures.clone(), + }, + // Invocation arguments have their own independently sealed packets. + // Retain their exact typed occurrences, not rewritten storage trees. + E::Call { + function, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::Call { + function: *function, + operands: occurrence(args), + order: evaluation_order.clone(), + }, + E::Intrinsic { + intrinsic, + type_arguments, + args, + evaluation_order, + .. + } => GeneratedProducerIdentity::Intrinsic { + intrinsic: *intrinsic, + type_arguments: type_arguments.clone(), + operands: occurrence(args), + order: evaluation_order.clone(), + }, + E::IndirectCall { + callee, + args, + evaluation_order, + .. + } => { + add(callee); + GeneratedProducerIdentity::Indirect { + operands: occurrence(args), + order: evaluation_order.clone(), + } + } + E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + .. + } => { + for value in fields { + add(value); + } + GeneratedProducerIdentity::Struct { + owner: *struct_type, + order: evaluation_order.clone(), + validates_refinements: *validates_refinements, + } + } + E::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + } => { + for value in fields { + add(value); + } + GeneratedProducerIdentity::Bitfield { + owner: *bitfield_type, + order: evaluation_order.clone(), + validates_widths: *validates_widths, + } + } + E::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } => { + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Enum { + owner: *enum_type, + variant: *variant, + order: evaluation_order.clone(), + } + } + E::MachineConstruct { + state_type, + state, + payloads, + } => { + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Machine { + owner: *state_type, + state: *state, + } + } + E::MachineTransition { + source, + state_type, + target, + payloads, + } => { + add(source); + for value in payloads { + add(value); + } + GeneratedProducerIdentity::Transition { + owner: *state_type, + state: *target, + } + } + E::Field { + base, + owner_type, + field, + } => { + add(base); + GeneratedProducerIdentity::Field { + owner: *owner_type, + field: *field, + } + } + E::InterfaceCoerce { value, adapters } => { + add(value); + GeneratedProducerIdentity::InterfaceCoerce(adapters.clone()) + } + E::FunctionAdapter { value, function } => { + add(value); + GeneratedProducerIdentity::FunctionAdapter(*function) + } + E::Handle { + target, + kind, + error_local, + failure, + } => { + add(target); + GeneratedProducerIdentity::Handle { + kind: kind.clone(), + error_local: *error_local, + failure_span: failure.span, + } + } + E::InlineFunction { + params, + view_params, + local_floor, + body, + .. + } => GeneratedProducerIdentity::Inline { + params: params.clone(), + view_params: view_params.clone(), + local_floor: *local_floor, + body_span: body.span, + }, + E::ActorSpawn { + actor_type, + args, + evaluation_order, + constructor, + } => { + for value in args { + add(value); + } + GeneratedProducerIdentity::ActorSpawn { + constructor: *constructor, + actor_type: actor_type.clone(), + order: evaluation_order.clone(), + } + } + E::ActorMessage { + actor, + message, + handler, + args, + evaluation_order, + .. + } => { + add(actor); + for value in args { + add(value); + } + GeneratedProducerIdentity::ActorMessage { + handler: *handler, + message: message.clone(), + order: evaluation_order.clone(), + } + } + E::StateIs { value, state } => { + add(value); + GeneratedProducerIdentity::State(*state) + } + E::Binary { left, right, .. } => { + add(left); + add(right); + GeneratedProducerIdentity::Data + } + E::View(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::View) + } + E::Coarsen(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::Coarsen) + } + E::Declassify(value) => { + add(value); + GeneratedProducerIdentity::Transparent(GeneratedTransparentShape::Declassify) + } + E::RefinementValidated(value) => { + add(value); + GeneratedProducerIdentity::Transparent( + GeneratedTransparentShape::RefinementValidated, + ) + } + E::Unary { value, .. } + | E::Clone(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::Comptime { value, .. } => { + add(value); + GeneratedProducerIdentity::Data + } + E::ListConstruct { elements } => { + for value in elements { + add(value); + } + GeneratedProducerIdentity::Data + } + E::MapConstruct { entries } => { + for entry in entries { + add(&entry.key); + add(&entry.value); + } + GeneratedProducerIdentity::Data + } + E::StringInterpolation(parts) => { + for part in parts { + if let crate::StringSegment::Value(value) = part { + add(value); + } + } + GeneratedProducerIdentity::Data + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => GeneratedProducerIdentity::Data, + }; + Self { + kind: std::mem::discriminant(&value.kind), + ty: value.ty, + span: value.span, + identity, + backing, + ordinary_handle_source_snapshot: false, + ordinary_local_snapshot: false, + } + } + + fn apply_inline_extractions(&mut self, records: &[GeneratedInlineExtraction]) { + if matches!(self.identity, GeneratedProducerIdentity::Inline { .. }) + && let Some(record) = records.iter().find(|record| record.original == *self) + { + *self = record.replacement.clone(); + return; + } + for value in &mut self.backing { + value.apply_inline_extractions(records); + } + } + + fn certify_handle_sources(&mut self, types: &TypeInterner) { + self.ordinary_local_snapshot = + self.local_snapshot_backing() && observation_data_type(types, self.ty); + if matches!(self.identity, GeneratedProducerIdentity::Handle { .. }) { + let target = &self.backing[0]; + self.ordinary_handle_source_snapshot = + target.local_snapshot_backing() && observation_data_type(types, target.ty); + } + for value in &mut self.backing { + value.certify_handle_sources(types); + } + } + + fn local_snapshot_backing(&self) -> bool { + match self.identity { + GeneratedProducerIdentity::Local(_) => true, + GeneratedProducerIdentity::InterfaceCoerce(_) => { + self.backing[0].local_snapshot_backing() + } + _ => false, + } + } + + fn restore_local_snapshot(&self, value: &Expression) -> Option { + match (&self.identity, &value.kind) { + (GeneratedProducerIdentity::Local(_), ExpressionKind::Clone(inner)) + if value.ty == self.ty + && value.span == self.span + && inner.ty == self.ty + && inner.span == self.span => + { + Some(inner.as_ref().clone()) + } + ( + GeneratedProducerIdentity::InterfaceCoerce(expected), + ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + }, + ) if adapters == expected && value.ty == self.ty && value.span == self.span => { + let mut restored = value.clone(); + let ExpressionKind::InterfaceCoerce { value, .. } = &mut restored.kind else { + unreachable!() + }; + *value = Box::new(self.backing[0].restore_local_snapshot(inner)?); + Some(restored) + } + _ => None, + } + } + + fn validate_lowered<'a>( + &'a self, + value: &Expression, + validate_handle: &mut impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + let actual = Self::from_expression(value); + if self.kind != actual.kind + || self.ty != actual.ty + || self.span != actual.span + || self.identity != actual.identity + || self.backing.len() != actual.backing.len() + { + if matches!(value.kind, ExpressionKind::Local(_)) + && value.ty == self.ty + && value.span == self.span + { + return validate_handle(value, GeneratedOriginalShape { shape: self }); + } + return Err( + "call ownership generated operand changes its sealed original producer shape" + .into(), + ); + } + for (expected, value) in self.backing.iter().zip(generated_backing_values(value)) { + expected.validate_lowered(value, validate_handle)?; + } + Ok(()) + } + + fn metadata_types(&self, visit: &mut impl FnMut(TypeId)) { + visit(self.ty); + match &self.identity { + GeneratedProducerIdentity::Call { operands, .. } + | GeneratedProducerIdentity::Indirect { operands, .. } + | GeneratedProducerIdentity::Intrinsic { operands, .. } + | GeneratedProducerIdentity::ResourceInvoke { operands, .. } => { + for &(ty, _) in operands { + visit(ty); + } + } + _ => {} + } + match &self.identity { + GeneratedProducerIdentity::ResourceHook(hook) + | GeneratedProducerIdentity::ResourceInvoke { hook, .. } => { + hook.metadata_types(&mut *visit) + } + GeneratedProducerIdentity::Intrinsic { type_arguments, .. } => { + for &ty in type_arguments { + visit(ty); + } + } + GeneratedProducerIdentity::Struct { owner, .. } + | GeneratedProducerIdentity::Bitfield { owner, .. } + | GeneratedProducerIdentity::Enum { owner, .. } + | GeneratedProducerIdentity::Machine { owner, .. } + | GeneratedProducerIdentity::Transition { owner, .. } + | GeneratedProducerIdentity::Field { owner, .. } => visit(*owner), + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + visit(adapter.source); + visit(adapter.target); + } + } + GeneratedProducerIdentity::Handle { + kind: + crate::HandleKind::Refinement { + refined_type, + predicates, + }, + .. + } => { + visit(*refined_type); + for predicate in predicates { + visit(predicate.refined_type); + visit(predicate.base_type); + visit(predicate.input_type); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_types(visit); + } + } + + fn metadata_locals(&self, visit: &mut impl FnMut(LocalId)) { + match &self.identity { + GeneratedProducerIdentity::Local(local) => visit(*local), + GeneratedProducerIdentity::Closure { captures, .. } => { + for &local in captures { + visit(local); + } + } + GeneratedProducerIdentity::Handle { + error_local: Some(local), + .. + } => visit(*local), + GeneratedProducerIdentity::Inline { + params, + view_params, + .. + } => { + for &local in params.iter().chain(view_params) { + visit(local); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_locals(visit); + } + } + + fn remap_locals( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + match &mut self.identity { + GeneratedProducerIdentity::Local(local) => *local = map(*local)?, + GeneratedProducerIdentity::Closure { captures, .. } => { + for local in captures { + *local = map(*local)?; + } + } + GeneratedProducerIdentity::Handle { + error_local: Some(local), + .. + } => *local = map(*local)?, + GeneratedProducerIdentity::Inline { + params, + view_params, + .. + } => { + for local in params.iter_mut().chain(view_params) { + *local = map(*local)?; + } + } + _ => {} + } + for value in &mut self.backing { + value.remap_locals(map)?; + } + Ok(()) + } + + fn metadata_functions(&self, visit: &mut impl FnMut(FunctionId)) { + match &self.identity { + GeneratedProducerIdentity::Function(function) + | GeneratedProducerIdentity::FunctionAdapter(function) + | GeneratedProducerIdentity::Closure { function, .. } + | GeneratedProducerIdentity::Call { function, .. } + | GeneratedProducerIdentity::ActorMessage { + handler: function, .. + } => visit(*function), + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + visit(adapter.function); + } + } + GeneratedProducerIdentity::ActorSpawn { + constructor: Some(function), + .. + } => visit(*function), + GeneratedProducerIdentity::Handle { + kind: crate::HandleKind::Refinement { predicates, .. }, + .. + } => { + for predicate in predicates { + visit(predicate.function); + } + } + _ => {} + } + for value in &self.backing { + value.metadata_functions(visit); + } + } + + fn remap_functions( + &mut self, + map: &mut impl FnMut(FunctionId) -> Result, + ) -> Result<(), E> { + match &mut self.identity { + GeneratedProducerIdentity::Function(function) + | GeneratedProducerIdentity::FunctionAdapter(function) + | GeneratedProducerIdentity::Closure { function, .. } + | GeneratedProducerIdentity::Call { function, .. } + | GeneratedProducerIdentity::ActorMessage { + handler: function, .. + } => *function = map(*function)?, + GeneratedProducerIdentity::InterfaceCoerce(adapters) => { + for adapter in adapters { + adapter.function = map(adapter.function)?; + } + } + GeneratedProducerIdentity::ActorSpawn { + constructor: Some(function), + .. + } => *function = map(*function)?, + GeneratedProducerIdentity::Handle { + kind: crate::HandleKind::Refinement { predicates, .. }, + .. + } => { + for predicate in predicates { + predicate.function = map(predicate.function)?; + } + } + _ => {} + } + for value in &mut self.backing { + value.remap_functions(map)?; + } + Ok(()) + } +} + +/// Original actor receiver shape only. This grants no call-parameter acquisition, +/// borrowed storage, Resource custody or runtime instance authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ActorReceiverProof { + shape: GeneratedProducerShape, +} +impl ActorReceiverProof { + pub(super) fn original(value: &Expression, types: &TypeInterner) -> Self { + let mut shape = GeneratedProducerShape::from_expression(value); + shape.certify_handle_sources(types); + Self { shape } + } + pub fn validate_lowered<'a>( + &'a self, + value: &Expression, + mut storage: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + self.shape.validate_lowered(value, &mut storage) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedArgumentWitness { + actual_type: TypeId, + parameter_type: TypeId, + callee_access: CheckedCalleeAccess, + acquisition: GeneratedAcquisition, + origin: CallerViewSource, + source_span: Span, + written_view: bool, + owned_producer: bool, + producer_shape: GeneratedProducerShape, + ordinary_snapshot: Option, +} + +impl GeneratedArgumentWitness { + pub fn actual_type(&self) -> TypeId { + self.actual_type + } + pub fn parameter_type(&self) -> TypeId { + self.parameter_type + } + pub fn callee_access(&self) -> CheckedCalleeAccess { + self.callee_access + } + pub fn acquisition(&self) -> &GeneratedAcquisition { + &self.acquisition + } + pub fn origin(&self) -> CallerViewSource { + self.origin + } + pub fn source_span(&self) -> Span { + self.source_span + } + pub fn written_view(&self) -> bool { + self.written_view + } + pub fn owned_producer(&self) -> bool { + self.owned_producer + } + /// Compare an authenticated restored operand with the immutable original + /// operation/backing shape; matching a type or producer flag is insufficient. + pub fn validate_producer_shape(&self, value: &Expression) -> Result<(), String> { + self.producer_shape.validate_lowered(value, &mut |_, _| { + Err("call ownership original generated operand cannot be a lowered storage slot".into()) + }) + } + /// A storage substitution needs an independently authenticated unique + /// owning Let; original Handles additionally require their exact CFG proof. + pub fn validate_lowered_producer_shape<'a>( + &'a self, + value: &Expression, + mut validate_handle: impl FnMut(&Expression, GeneratedOriginalShape<'a>) -> Result<(), String>, + ) -> Result<(), String> { + self.producer_shape + .validate_lowered(value, &mut validate_handle) + } + pub fn ordinary_snapshot_proof(&self) -> Option<&GeneratedSnapshotProof> { + self.ordinary_snapshot.as_ref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct GeneratedCallCertificate { + operation: GeneratedOperation, + result_type: TypeId, + evaluation_order: Vec, + arguments: Vec, +} + +fn generated_owned_producer(mut value: &Expression) -> bool { + loop { + match &value.kind { + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) + | ExpressionKind::RefinementValidated(inner) => value = inner, + ExpressionKind::Call { .. } + | ExpressionKind::IndirectCall { .. } + | ExpressionKind::Intrinsic { .. } + | ExpressionKind::StructConstruct { .. } + | ExpressionKind::BitfieldConstruct { .. } + | ExpressionKind::MachineConstruct { .. } + | ExpressionKind::MachineTransition { .. } + | ExpressionKind::Clone(_) + | ExpressionKind::Run(_) + | ExpressionKind::Join(_) => return true, + _ => return false, + } + } +} + +fn generated_witness( + value: &Expression, + parameter_type: TypeId, + callee_access: CheckedCalleeAccess, + acquisition: GeneratedAcquisition, + types: &TypeInterner, +) -> GeneratedArgumentWitness { + let mut producer_shape = GeneratedProducerShape::from_expression(value); + producer_shape.certify_handle_sources(types); + GeneratedArgumentWitness { + actual_type: value.ty, + parameter_type, + callee_access, + acquisition, + origin: immediate_source(value), + source_span: value.span, + written_view: original_view(value), + owned_producer: generated_owned_producer(value), + producer_shape, + ordinary_snapshot: observation_data_type(types, value.ty).then_some( + GeneratedSnapshotProof { + actual_type: value.ty, + }, + ), + } +} + +fn generated_tuple_matches(argument: &GeneratedArgumentOwnership) -> bool { + let witness = &argument.witness; + argument.actual_type == witness.actual_type + && argument.parameter_type == witness.parameter_type + && argument.callee_access == witness.callee_access + && argument.acquisition == witness.acquisition +} + +fn generated_operation_function(operation: &GeneratedOperation) -> Option { + match operation { + GeneratedOperation::Display { method } | GeneratedOperation::Equality { method } => { + Some(*method) + } + GeneratedOperation::InterfaceDispatch { function, .. } + | GeneratedOperation::NativeSuite { function } + | GeneratedOperation::RefinementPredicate { function } => Some(*function), + _ => None, + } +} + +fn remap_generated_operation( + operation: &mut GeneratedOperation, + map: &mut impl FnMut(FunctionId) -> Result, +) -> Result<(), E> { + match operation { + GeneratedOperation::Display { method } | GeneratedOperation::Equality { method } => { + *method = map(*method)? + } + GeneratedOperation::InterfaceDispatch { function, .. } + | GeneratedOperation::NativeSuite { function } + | GeneratedOperation::RefinementPredicate { function } => *function = map(*function)?, + _ => {} + } + Ok(()) +} + +fn remap_generated_operation_locals( + operation: &mut GeneratedOperation, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + if let GeneratedOperation::FunctionAdapter { callee, .. } + | GeneratedOperation::ReflectedCallableAdapter { callee, .. } = operation + { + *callee = map(*callee)?; + } + Ok(()) +} + +fn validate_generated_stage( + argument: &GeneratedArgumentOwnership, + argument_type: TypeId, + local: &impl Fn(LocalId) -> Option, + initial_hir: bool, +) -> Result<(), String> { + if !generated_tuple_matches(argument) { + return Err("generated ownership differs from its sealed original acquisition".into()); + } + if argument.staging != argument.staging_certificate { + return Err("generated stage differs from its sealed storage association".into()); + } + if initial_hir && argument.staging != (GeneratedArgumentStaging::Existing { loan: None }) { + return Err("initial HIR generated ownership cannot carry MIR staging authority".into()); + } + let (owner, loan) = match argument.staging { + GeneratedArgumentStaging::Existing { loan: None } => return Ok(()), + GeneratedArgumentStaging::Existing { loan: Some(loan) } => (None, loan), + GeneratedArgumentStaging::OwnedProducer { owner, loan } => { + if !argument.witness.owned_producer { + return Err("generated stage has no sealed owning producer proof".into()); + } + (Some(owner), loan) + } + GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + if argument + .witness + .ordinary_snapshot + .as_ref() + .map(|proof| proof.actual_type) + != Some(argument.actual_type) + { + return Err("generated stage has no sealed ordinary snapshot proof".into()); + } + (Some(owner), loan) + } + }; + if argument.callee_access != CheckedCalleeAccess::View + || !matches!( + argument.acquisition, + GeneratedAcquisition::Borrow { .. } | GeneratedAcquisition::Copy + ) + { + return Err("generated loan stage has no original physically borrowed acquisition".into()); + } + if require_local(loan, local)?.ty != argument_type || argument_type != argument.actual_type { + return Err("generated loan stage changed its sealed operand type".into()); + } + if let Some(owner) = owner { + let owner = require_local(owner, local)?; + if owner.ty != argument_type + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("generated backing stage cannot acquire a borrowed slot".into()); + } + } + Ok(()) +} + +fn generated_stage_local_ids(staging: GeneratedArgumentStaging, visit: &mut impl FnMut(LocalId)) { + match staging { + GeneratedArgumentStaging::Existing { loan: Some(loan) } => visit(loan), + GeneratedArgumentStaging::OwnedProducer { owner, loan } + | GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + visit(owner); + visit(loan); + } + GeneratedArgumentStaging::Existing { loan: None } => {} + } +} + +fn remap_generated_staging( + staging: &mut GeneratedArgumentStaging, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + match staging { + GeneratedArgumentStaging::Existing { loan: Some(loan) } => *loan = map(*loan)?, + GeneratedArgumentStaging::OwnedProducer { owner, loan } + | GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + *owner = map(*owner)?; + *loan = map(*loan)?; + } + GeneratedArgumentStaging::Existing { loan: None } => {} + } + Ok(()) +} + +/// Each operation is validated against its own existing compiler contract. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GeneratedOperation { + Display { + method: FunctionId, + }, + Equality { + method: FunctionId, + }, + InterfaceDispatch { + function: FunctionId, + interface_type: TypeId, + method_index: usize, + }, + FunctionAdapter { + callee: LocalId, + source_type: TypeId, + target_type: TypeId, + }, + ReflectedCallableAdapter { + callee: LocalId, + plan: jett_typecheck::CheckedReflectedCallablePlan, + }, + JsonBridge { + intrinsic: IntrinsicId, + }, + NativeSuite { + function: FunctionId, + }, + RefinementPredicate { + function: FunctionId, + }, + FailureFormatting, + PropertyContext, + ReflectionMetadata { + intrinsic: IntrinsicId, + }, + EvaluatedValue { + intrinsic: IntrinsicId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedCallOwnership { + pub operation: GeneratedOperation, + pub arguments: Vec, + certificate: GeneratedCallCertificate, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallOwnership { + Source(SourceCallOwnership), + Generated(GeneratedCallOwnership), +} + +impl CallOwnership { + /// Read-only refusal guard; presence supplies no minting or storage authority. + pub fn has_temporary_local_receipts(&self) -> bool { + match self { + Self::Source(source) => { + source.temporary_local_backing.is_some() + || source + .arguments + .iter() + .any(|argument| argument.witness.temporary_origin.is_some()) + } + Self::Generated(_) => false, + } + } + pub fn metadata_types(&self, mut visit: impl FnMut(TypeId)) { + match self { + Self::Source(source) => { + visit(source.certificate.result_type); + match &source.shape { + CheckedInvocationShape::Function { signature_type } => visit(*signature_type), + CheckedInvocationShape::Intrinsic { + result_type, + operands, + .. + } => { + visit(*result_type); + for role in operands { + visit(role.ty()); + } + } + } + match &source.target { + CallTarget::Declaration { + identity, + signature_type, + } => { + visit(*signature_type); + for &ty in &identity.type_arguments { + visit(ty); + } + for binding in &identity.scoped_type_bindings { + visit(binding.ty); + } + } + CallTarget::ResourceHook(hook) => hook.metadata_types(&mut visit), + CallTarget::Interface { interface_type, .. } => visit(*interface_type), + CallTarget::Indirect { signature_type } => visit(*signature_type), + _ => {} + } + for argument in &source.arguments { + visit(argument.actual_type); + visit(argument.witness.occurrence_type); + if let Some(result) = argument.witness.handled_result { + visit(result.ty); + } + if let Some(root) = argument.witness.projection_root { + visit(root.occurrence_type); + visit(root.storage_type); + } + if let Some((_, proof)) = argument.witness.match_origin { + visit(proof.enum_type()); + visit(proof.binder_type()); + } + if let Some(proof) = argument.retained_snapshot { + visit(proof.actual_type); + visit(proof.occurrence_type); + } + visit(argument.parameter_type); + if let CallerOrigin::Binding(fact) = &argument.origin { + visit(fact.ty); + } + if let Some(proof) = argument.observation_proof() { + visit(proof.actual_type); + if let Some(purpose) = &proof.purpose { + purpose.metadata_types(&mut visit); + } + for &(_, ty) in &proof.captures { + visit(ty); + } + } + } + for argument in &source.generated_operands { + visit(argument.actual_type); + visit(argument.parameter_type); + argument.witness.producer_shape.metadata_types(&mut visit); + } + } + Self::Generated(generated) => { + visit(generated.certificate.result_type); + match generated.operation { + GeneratedOperation::InterfaceDispatch { interface_type, .. } => { + visit(interface_type) + } + GeneratedOperation::FunctionAdapter { + source_type, + target_type, + .. + } => { + visit(source_type); + visit(target_type); + } + GeneratedOperation::ReflectedCallableAdapter { ref plan, .. } => { + visit(plan.source_type()); + visit(plan.requested_type()); + } + _ => {} + } + for argument in &generated.arguments { + visit(argument.actual_type); + visit(argument.parameter_type); + argument.witness.producer_shape.metadata_types(&mut visit); + } + } + } + } + pub fn source_parameter_effect(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.effect), + Self::Generated(_) => None, + } + } + pub fn source_parameter_access(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.callee_access), + Self::Generated(_) => None, + } + } + pub fn source_parameter_syntax(&self, index: usize) -> Option { + match self { + Self::Source(source) => source.arguments.get(index).map(|arg| arg.syntax), + Self::Generated(_) => None, + } + } + pub fn parameter_count(&self) -> usize { + match self { + Self::Source(source) => source.arguments.len() + source.generated_operands.len(), + Self::Generated(generated) => generated.arguments.len(), + } + } + + pub fn parameter_physical_access(&self, index: usize) -> Option { + match self { + Self::Source(source) => source + .arguments + .get(index) + .map(|arg| arg.physical_access) + .or_else(|| { + index.checked_sub(source.arguments.len()).and_then(|tail| { + source + .generated_operands + .get(tail) + .map(|arg| arg.callee_access) + }) + }), + Self::Generated(generated) => { + generated.arguments.get(index).map(|arg| arg.callee_access) + } + } + } + + pub fn stage_parameter( + &mut self, + index: usize, + owner: Option, + loan: LocalId, + ) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source staging authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or_else(|| "source staging parameter is out of bounds".to_string())?; + if argument.staging != ArgumentStaging::Original + || argument.physical_access != CheckedCalleeAccess::View + { + return Err("source staging requires an original physically borrowed operand".into()); + } + argument.staging = match owner { + Some(owner) + if argument.effect == CheckedCallerEffect::RelinquishOwned + || (argument.effect == CheckedCallerEffect::TransferOwned + && source.bridge != CallBridge::Direct) => + { + ArgumentStaging::Relinquished { owner, loan } + } + None if matches!( + argument.effect, + CheckedCallerEffect::RetainBorrow | CheckedCallerEffect::ObserveData + ) => + { + ArgumentStaging::Borrowed { loan } + } + _ => { + return Err( + "source staging does not match the checked caller effect and bridge".into(), + ); + } + }; + Ok(()) + } + + pub fn stage_retained_snapshot( + &mut self, + index: usize, + owner: LocalId, + loan: LocalId, + ) -> Result<(), String> { + let Self::Source(source) = self else { + return Err( + "generated operand cannot acquire retained source snapshot authority".into(), + ); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or("retained snapshot parameter is out of bounds")?; + if argument.staging != ArgumentStaging::Original + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != CheckedCalleeAccess::View + || argument.retained_snapshot.is_none() + || owner == loan + { + return Err("retained snapshot requires an original checked ordinary-data view".into()); + } + argument.staging = ArgumentStaging::RetainedSnapshot { owner, loan }; + Ok(()) + } + + pub fn stage_observation(&mut self, index: usize, owner: LocalId) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source observation authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or_else(|| "source observation parameter is out of bounds".to_string())?; + if argument.staging != ArgumentStaging::Original + || argument.effect != CheckedCallerEffect::ObserveData + || argument.physical_access != CheckedCalleeAccess::Owned + { + return Err( + "observation staging requires original ObserveData at a physical owned boundary" + .into(), + ); + } + argument.staging = ArgumentStaging::Observed { owner }; + Ok(()) + } + + pub fn stage_acquisition(&mut self, index: usize, local: LocalId) -> Result<(), String> { + let Self::Source(source) = self else { + return Err("generated operand cannot acquire source staging authority".into()); + }; + let argument = source + .arguments + .get_mut(index) + .ok_or("source acquisition parameter is out of bounds")?; + if argument.staging != ArgumentStaging::Original { + return Err("source acquisition is already staged".into()); + } + argument.staging = match (argument.effect, argument.physical_access) { + (CheckedCallerEffect::TransferOwned, CheckedCalleeAccess::Owned) => { + ArgumentStaging::Transferred { owner: local } + } + (CheckedCallerEffect::Copy, _) => ArgumentStaging::Copied { value: local }, + _ => { + return Err( + "source acquisition does not match checked copy or physical ownership".into(), + ); + } + }; + Ok(()) + } + + pub fn stage_generated_parameter( + &mut self, + index: usize, + staging: GeneratedArgumentStaging, + ) -> Result<(), String> { + let Self::Generated(generated) = self else { + return Err("source packet cannot acquire generated staging authority".into()); + }; + if generated.operation != generated.certificate.operation { + return Err("generated stage operation differs from its certificate".into()); + } + if generated.arguments.len() != generated.certificate.arguments.len() { + return Err("generated stage argument count differs from its certificate".into()); + } + let witness = generated + .certificate + .arguments + .get(index) + .ok_or("generated stage certificate parameter is out of bounds")?; + let argument = generated + .arguments + .get_mut(index) + .ok_or("generated stage parameter is out of bounds")?; + if !generated_tuple_matches(argument) + || argument.witness != *witness + || argument.staging != argument.staging_certificate + || argument.staging != (GeneratedArgumentStaging::Existing { loan: None }) + { + return Err( + "generated stage cannot rewrite its original acquisition certificate".into(), + ); + } + if argument.callee_access != CheckedCalleeAccess::View + || !matches!( + argument.acquisition, + GeneratedAcquisition::Borrow { .. } | GeneratedAcquisition::Copy + ) + { + return Err("generated stage requires a physically borrowed original operand".into()); + } + match staging { + GeneratedArgumentStaging::Existing { loan: Some(_) } => {} + GeneratedArgumentStaging::OwnedProducer { .. } if argument.witness.owned_producer => {} + GeneratedArgumentStaging::OrdinarySnapshot { .. } + if argument.witness.ordinary_snapshot.is_some() => {} + _ => return Err("generated stage has no sealed original backing permission".into()), + } + argument.staging = staging; + argument.staging_certificate = staging; + Ok(()) + } + + /// Structural metadata only: this must not feed execution liveness or loans. + pub fn metadata_local_ids(&self, mut visit: impl FnMut(LocalId)) { + match self { + Self::Source(source) => { + for argument in &source.arguments { + if let Some(proof) = &argument.witness.temporary_origin { + proof.metadata_locals(&mut visit); + } + if let Some(root) = argument.witness.projection_root { + visit(root.local); + } + if let Some((root, _)) = argument.witness.match_origin { + visit(root); + } + if let Some(proof) = argument.observation_proof() { + if let Some(purpose) = &proof.purpose { + purpose.metadata_locals(&mut visit); + } + for &(capture, _) in &proof.captures { + visit(capture); + } + } + match argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => visit(owner), + ArgumentStaging::Copied { value } => visit(value), + ArgumentStaging::Borrowed { loan } => visit(loan), + ArgumentStaging::Relinquished { owner, loan } + | ArgumentStaging::RetainedSnapshot { owner, loan } => { + visit(owner); + visit(loan); + } + ArgumentStaging::Observed { owner } => visit(owner), + } + match argument.origin { + CallerOrigin::Binding(fact) => { + visit(fact.local); + if let CallerBindingMode::View { + source: CallerViewSource::Local(id), + } = fact.mode + { + visit(id); + } + } + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(id), + } + | CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(id), + } => visit(id), + _ => {} + } + } + for operand in &source.generated_operands { + operand.metadata_local_ids(&mut visit); + } + } + Self::Generated(generated) => { + if let GeneratedOperation::FunctionAdapter { callee, .. } + | GeneratedOperation::ReflectedCallableAdapter { callee, .. } = + generated.operation + { + visit(callee); + } + for operand in &generated.arguments { + operand.metadata_local_ids(&mut visit); + } + } + } + } + + pub fn remap_metadata_locals( + &mut self, + mut map: impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + match self { + Self::Source(source) => { + for argument in &mut source.arguments { + if let Some(proof) = &mut argument.witness.temporary_origin { + proof.remap(&mut map)?; + } + if let Some(root) = &mut argument.witness.projection_root { + root.local = map(root.local)?; + } + if let Some((root, _)) = &mut argument.witness.match_origin { + *root = map(*root)?; + } + if let Some(proof) = &mut argument.witness.observation { + if let Some(purpose) = &mut proof.purpose { + purpose.remap_locals(&mut map)?; + } + for (capture, _) in &mut proof.captures { + *capture = map(*capture)?; + } + } + remap_origin(&mut argument.witness.origin, &mut map)?; + match &mut argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => *owner = map(*owner)?, + ArgumentStaging::Copied { value } => *value = map(*value)?, + ArgumentStaging::Borrowed { loan } => *loan = map(*loan)?, + ArgumentStaging::Relinquished { owner, loan } + | ArgumentStaging::RetainedSnapshot { owner, loan } => { + *owner = map(*owner)?; + *loan = map(*loan)?; + } + ArgumentStaging::Observed { owner } => *owner = map(*owner)?, + } + match &mut argument.origin { + CallerOrigin::Binding(fact) => { + fact.local = map(fact.local)?; + if let CallerBindingMode::View { source } = &mut fact.mode { + remap_source(source, &mut map)?; + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => { + remap_source(source, &mut map)? + } + CallerOrigin::OwnedExpression => {} + } + } + for operand in &mut source.generated_operands { + operand.remap_metadata_locals(&mut map)?; + } + for operand in &mut source.certificate.generated_operands { + operand.remap_metadata_locals(&mut map)?; + } + } + Self::Generated(generated) => { + for operand in &mut generated.arguments { + operand.remap_metadata_locals(&mut map)?; + } + remap_generated_operation_locals(&mut generated.operation, &mut map)?; + remap_generated_operation_locals(&mut generated.certificate.operation, &mut map)?; + for witness in &mut generated.certificate.arguments { + if let GeneratedAcquisition::Borrow { source } = &mut witness.acquisition { + remap_source(source, &mut map)?; + } + remap_source(&mut witness.origin, &mut map)?; + witness.producer_shape.remap_locals(&mut map)?; + } + } + } + Ok(()) + } + + /// Structural identities only; declaration provenance is not an executable root. + pub fn metadata_function_ids(&self, mut visit: impl FnMut(FunctionId)) { + match self { + Self::Source(source) => { + match source.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + visit(function) + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = source.bridge { + visit(function); + } + for argument in &source.generated_operands { + argument + .witness + .producer_shape + .metadata_functions(&mut visit); + } + } + Self::Generated(generated) => { + if let Some(function) = generated_operation_function(&generated.operation) { + visit(function); + } + for argument in &generated.arguments { + argument + .witness + .producer_shape + .metadata_functions(&mut visit); + } + } + } + } + + pub fn remap_metadata_functions( + &mut self, + mut map: impl FnMut(FunctionId) -> Result, + ) -> Result<(), E> { + match self { + Self::Source(source) => { + match &mut source.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + *function = map(*function)? + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = &mut source.bridge { + *function = map(*function)?; + } + match &mut source.certificate.target { + CallTarget::Function(function) | CallTarget::Interface { function, .. } => { + *function = map(*function)? + } + _ => {} + } + if let CallBridge::JsonSource { function, .. } = &mut source.certificate.bridge { + *function = map(*function)?; + } + for argument in &mut source.generated_operands { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + for argument in &mut source.certificate.generated_operands { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + } + Self::Generated(generated) => { + remap_generated_operation(&mut generated.operation, &mut map)?; + remap_generated_operation(&mut generated.certificate.operation, &mut map)?; + for argument in &mut generated.arguments { + argument.witness.producer_shape.remap_functions(&mut map)?; + } + for witness in &mut generated.certificate.arguments { + witness.producer_shape.remap_functions(&mut map)?; + } + } + } + Ok(()) + } + + pub(crate) fn apply_generated_inline_extractions( + &mut self, + records: &[GeneratedInlineExtraction], + ) { + match self { + Self::Source(source) => { + for argument in &mut source.generated_operands { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + for argument in &mut source.certificate.generated_operands { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + } + Self::Generated(generated) => { + for argument in &mut generated.arguments { + argument + .witness + .producer_shape + .apply_inline_extractions(records); + } + for witness in &mut generated.certificate.arguments { + witness.producer_shape.apply_inline_extractions(records); + } + } + } + } + + pub fn generated( + operation: GeneratedOperation, + args: &[Expression], + parameter_types: &[TypeId], + access: &[CheckedCalleeAccess], + result_type: TypeId, + evaluation_order: &[usize], + types: &TypeInterner, + ) -> Result { + if !valid_type(types, result_type) { + return Err("generated result type is outside its interner".into()); + } + if evaluation_order.len() != args.len() { + return Err("generated evaluation order has the wrong operand count".into()); + } + let mut seen = vec![false; args.len()]; + for &index in evaluation_order { + let slot = seen + .get_mut(index) + .ok_or("generated evaluation order is out of bounds")?; + if *slot { + return Err("generated evaluation order repeats an operand".into()); + } + *slot = true; + } + if args.len() != parameter_types.len() || args.len() != access.len() { + return Err("generated ownership parameter count is invalid".into()); + } + let mut arguments = Vec::with_capacity(args.len()); + for ((argument, ¶meter_type), &callee_access) in + args.iter().zip(parameter_types).zip(access) + { + if !valid_type(types, argument.ty) || !valid_type(types, parameter_type) { + return Err("generated ownership type is outside its interner".into()); + } + let acquisition = + if jett_typecheck::ownership::is_implicitly_copyable(types, argument.ty) { + GeneratedAcquisition::Copy + } else if callee_access == CheckedCalleeAccess::View { + GeneratedAcquisition::Borrow { + source: immediate_source(argument), + } + } else { + GeneratedAcquisition::OwnedExpression + }; + let witness = generated_witness( + argument, + parameter_type, + callee_access, + acquisition.clone(), + types, + ); + arguments.push(GeneratedArgumentOwnership { + actual_type: argument.ty, + parameter_type, + callee_access, + acquisition, + staging: GeneratedArgumentStaging::Existing { loan: None }, + staging_certificate: GeneratedArgumentStaging::Existing { loan: None }, + witness, + }); + } + let certificate = GeneratedCallCertificate { + operation: operation.clone(), + result_type, + evaluation_order: evaluation_order.to_vec(), + arguments: arguments + .iter() + .map(|argument| argument.witness.clone()) + .collect(), + }; + Ok(Self::Generated(GeneratedCallOwnership { + operation, + arguments, + certificate, + })) + } +} + +fn valid_type(types: &TypeInterner, ty: TypeId) -> bool { + (ty.index() as usize) < types.len() +} + +pub(crate) fn immediate_source(mut value: &Expression) -> CallerViewSource { + loop { + match &value.kind { + ExpressionKind::Local(local) => return CallerViewSource::Local(*local), + ExpressionKind::Field { base, .. } + | ExpressionKind::View(base) + | ExpressionKind::Coarsen(base) + | ExpressionKind::Declassify(base) + | ExpressionKind::RefinementValidated(base) => value = base, + ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + } if adapters.is_empty() => value = inner, + _ => return CallerViewSource::Other, + } + } +} + +fn remap_source( + source: &mut CallerViewSource, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + if let CallerViewSource::Local(id) = source { + *id = map(*id)?; + } + Ok(()) +} + +fn remap_origin( + origin: &mut CallerOrigin, + map: &mut impl FnMut(LocalId) -> Result, +) -> Result<(), E> { + match origin { + CallerOrigin::Binding(fact) => { + fact.local = map(fact.local)?; + if let CallerBindingMode::View { source } = &mut fact.mode { + remap_source(source, map)?; + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => remap_source(source, map)?, + CallerOrigin::OwnedExpression => {} + } + Ok(()) +} + +impl GeneratedArgumentOwnership { + pub fn original_witness(&self) -> &GeneratedArgumentWitness { + &self.witness + } + fn metadata_local_ids(&self, visit: &mut impl FnMut(LocalId)) { + if let GeneratedAcquisition::Borrow { + source: CallerViewSource::Local(id), + } = self.acquisition + { + visit(id); + } + if let CallerViewSource::Local(id) = self.witness.origin { + visit(id); + } + self.witness.producer_shape.metadata_locals(visit); + generated_stage_local_ids(self.staging, visit); + generated_stage_local_ids(self.staging_certificate, visit); + } + fn remap_metadata_locals( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + if let GeneratedAcquisition::Borrow { source } = &mut self.acquisition { + remap_source(source, map)?; + } + if let GeneratedAcquisition::Borrow { source } = &mut self.witness.acquisition { + remap_source(source, map)?; + } + remap_source(&mut self.witness.origin, map)?; + self.witness.producer_shape.remap_locals(map)?; + remap_generated_staging(&mut self.staging, map)?; + remap_generated_staging(&mut self.staging_certificate, map)?; + Ok(()) + } +} + +/// Primitive local facts usable by HIR and MIR without sharing their bodies. +#[derive(Debug, Clone, Copy)] +pub struct OwnershipLocalInfo { + pub ty: TypeId, + pub mutable: bool, + pub span: Span, + pub view_source: Option, + pub is_view_parameter: bool, + /// Present only while an exact original/prepared viewed loop proves this binding. + pub view_iteration: Option, + /// Canonical payload origin, active only inside its exact match arm. + /// Source invocation mode separately decides whether the binding is borrowed. + pub match_binding: Option, +} + +/// Validate structural authority before considering the current operand tree. +/// The caller separately proves its typed conversion/staging/target relation. +pub fn validate_operand_ownership( + ownership: &CallOwnership, + argument_types: &[TypeId], + evaluation_order: &[usize], + types: &TypeInterner, + local: impl Fn(LocalId) -> Option, + initial_hir: bool, +) -> Result<(), String> { + if ownership.parameter_count() != argument_types.len() + || evaluation_order.len() != argument_types.len() + { + return Err("call ownership operand count is invalid".into()); + } + let mut seen = vec![false; argument_types.len()]; + for ¶meter in evaluation_order { + let Some(slot) = seen.get_mut(parameter) else { + return Err("call ownership evaluation permutation is out of bounds".into()); + }; + if *slot { + return Err("call ownership evaluation permutation repeats an operand".into()); + } + *slot = true; + } + for &ty in argument_types { + if !valid_type(types, ty) { + return Err("call ownership operand type is outside its interner".into()); + } + } + let mut valid_metadata = true; + ownership.metadata_types(|ty| valid_metadata &= valid_type(types, ty)); + if !valid_metadata { + return Err("call ownership metadata type is outside its interner".into()); + } + match ownership { + CallOwnership::Source(source) => { + if source.target != source.certificate.target + || source.shape != source.certificate.shape + || source.context != source.certificate.context + || source.bridge != source.certificate.bridge + || source.generated_operands != source.certificate.generated_operands + { + return Err( + "call ownership invocation differs from its checked source certificate".into(), + ); + } + let source_count = source.arguments.len(); + let mut source_seen = vec![false; source_count]; + match &source.shape { + CheckedInvocationShape::Function { signature_type } => { + if !valid_type(types, *signature_type) { + return Err("call ownership signature type is outside its interner".into()); + } + let Type::Function { + params, + view_params, + .. + } = types.resolve(*signature_type) + else { + return Err("call ownership signature is not a function".into()); + }; + if params.len() != source_count || view_params.len() != source_count { + return Err("call ownership signature arity is invalid".into()); + } + for (index, argument) in source.arguments.iter().enumerate() { + let access = if view_params[index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.parameter_type != params[index] + || argument.callee_access != access + { + return Err( + "call ownership signature parameter type or access is invalid" + .into(), + ); + } + } + } + CheckedInvocationShape::Intrinsic { + intrinsic, + result_type, + operands, + } => { + if !valid_type(types, *result_type) || operands.len() != source_count { + return Err("call ownership intrinsic source shape is invalid".into()); + } + for (index, (argument, role)) in + source.arguments.iter().zip(operands).enumerate() + { + if !valid_type(types, role.ty()) || role.ty() != argument.parameter_type { + return Err("call ownership intrinsic operand type is invalid".into()); + } + let permitted = jett_typecheck::intrinsic_operand_access( + *intrinsic, + index, + source_count, + ) + .ok_or_else(|| { + "call ownership intrinsic source operand has no closed role".to_string() + })?; + let print = matches!( + role, + jett_typecheck::CheckedIntrinsicOperandRole::PrintArgument { .. } + ); + if print { + if !matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + return Err( + "call ownership print role belongs to another intrinsic".into(), + ); + } + let expected = if argument.syntax == CheckedCallerSyntax::WrittenView { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.callee_access != expected { + return Err("call ownership print access is invalid".into()); + } + } else if permitted != argument.callee_access { + return Err( + "call ownership intrinsic access differs from its closed role" + .into(), + ); + } + if !print { + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, role.ty()); + let valid = match role { + jett_typecheck::CheckedIntrinsicOperandRole::Copy { .. } => { + copyable + } + jett_typecheck::CheckedIntrinsicOperandRole::Owned { .. } => { + !copyable && permitted == CheckedCalleeAccess::Owned + } + jett_typecheck::CheckedIntrinsicOperandRole::View { .. } => { + !copyable && permitted == CheckedCalleeAccess::View + } + jett_typecheck::CheckedIntrinsicOperandRole::PrintArgument { + .. + } => false, + }; + if !valid { + return Err("call ownership intrinsic role does not match its checked type and access".into()); + } + } + } + } + } + for (parameter_index, argument) in source.arguments.iter().enumerate() { + let witness = &argument.witness; + if argument.source_span != witness.source_span + || argument.source_index != witness.source_index + || argument.parameter_index != witness.parameter_index + || argument.actual_type != witness.actual_type + || argument.parameter_type != witness.parameter_type + || argument.syntax != witness.syntax + || argument.origin != witness.origin + || argument.callee_access != witness.callee_access + || argument.effect != witness.effect + || source.context != witness.context + { + return Err("call ownership disagrees with its original source witness".into()); + } + if argument.parameter_index != parameter_index + || argument.source_index >= source_count + || source_seen[argument.source_index] + { + return Err("call ownership source/formal permutation is invalid".into()); + } + source_seen[argument.source_index] = true; + if evaluation_order.get(argument.source_index) != Some(¶meter_index) { + return Err("call ownership lexical evaluation order disagrees with its source permutation".into()); + } + if !valid_type(types, argument.actual_type) + || !valid_type(types, witness.occurrence_type) + || witness + .handled_result + .is_some_and(|result| !valid_type(types, result.ty)) + || !valid_type(types, argument.parameter_type) + { + return Err("call ownership source type is outside its interner".into()); + } + if witness.occurrence_type != argument.actual_type + && !(has_outer_secret(types, argument.parameter_type) + && secret_taint_matches( + types, + witness.occurrence_type, + argument.actual_type, + )) + { + return Err("call ownership sealed occurrence has no exact expected secret qualification".into()); + } + validate_origin( + &argument.origin, + witness.temporary_origin.as_deref(), + &local, + types, + )?; + if source_match_origin(&argument.origin, &local)? != witness.match_origin { + return Err( + "call ownership match payload differs from its original scoped origin" + .into(), + ); + } + validate_effect(argument, source.context, types)?; + if argument.effect == CheckedCallerEffect::ObserveData { + let proof = argument + .observation_proof() + .ok_or("observing call has no checked data acquisition proof")?; + if proof.actual_type != argument.actual_type { + return Err("observing call acquisition has another original type".into()); + } + if let Some(purpose) = &proof.purpose { + purpose.validate(argument, source, types, &local)?; + } else if !(observation_data_type(types, proof.actual_type) + || (proof.descriptor_checked + && matches!(types.resolve(proof.actual_type), Type::Function { .. }))) + { + return Err( + "observing call acquisition contains unsupported authority".into() + ); + } + for &(capture, ty) in &proof.captures { + let value = require_local(capture, &local)?; + if value.ty != ty + || value.view_source.is_some() + || value.is_view_parameter + || value.view_iteration.is_some() + || !observation_data_type(types, ty) + { + return Err( + "observing descriptor capture has no owning ordinary-data proof" + .into(), + ); + } + } + } + if let Some(proof) = argument.retained_snapshot { + if argument.effect != CheckedCallerEffect::RetainBorrow + || !retained_snapshot_origin(&argument.origin) + || proof.actual_type != argument.actual_type + || proof.occurrence_type != witness.occurrence_type + || !observation_data_type(types, proof.actual_type) + || !observation_data_type(types, proof.occurrence_type) + { + return Err( + "retained snapshot lost its checked ordinary-data endpoint proof" + .into(), + ); + } + } + if let Some(root) = witness.projection_root { + let stored = require_local(root.local, &local)?; + if stored.ty != root.storage_type + || !flow_projection_root_matches( + types, + root.occurrence_type, + root.storage_type, + ) + || !matches!(argument.origin, + CallerOrigin::BorrowedProjection { source: CallerViewSource::Local(id) } + | CallerOrigin::OwnedFieldCopy { parent: CallerViewSource::Local(id) } + if id == root.local) + || root.span.file != argument.source_span.file + || root.span.start < argument.source_span.start + || root.span.end > argument.source_span.end + || root.span.start >= root.span.end + { + return Err( + "call ownership flow projection lost its exact checked root".into() + ); + } + } + if initial_hir && argument.staging != ArgumentStaging::Original { + return Err( + "initial HIR call ownership cannot carry MIR staging authority".into(), + ); + } + match argument.staging { + ArgumentStaging::Original => {} + ArgumentStaging::Transferred { owner } => { + let owner = require_local(owner, &local)?; + if argument.effect != CheckedCallerEffect::TransferOwned + || argument.physical_access != CheckedCalleeAccess::Owned + || owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err( + "transferred call acquisition disagrees with checked ownership" + .into(), + ); + } + } + ArgumentStaging::Copied { value } => { + if argument.effect != CheckedCallerEffect::Copy + || require_local(value, &local)?.ty != argument_types[parameter_index] + { + return Err( + "copied call staging disagrees with checked copy or operand type" + .into(), + ); + } + } + ArgumentStaging::Borrowed { loan } => { + if argument.physical_access != CheckedCalleeAccess::View + || !matches!( + argument.effect, + CheckedCallerEffect::RetainBorrow + | CheckedCallerEffect::ObserveData + ) + { + return Err( + "borrowed call staging disagrees with the caller effect".into() + ); + } + if require_local(loan, &local)?.ty != argument_types[parameter_index] { + return Err("borrowed call staging has the wrong operand type".into()); + } + } + ArgumentStaging::Relinquished { owner, loan } => { + if argument.physical_access != CheckedCalleeAccess::View + || !(argument.effect == CheckedCallerEffect::RelinquishOwned + || (argument.effect == CheckedCallerEffect::TransferOwned + && source.bridge != CallBridge::Direct)) + { + return Err( + "owning call staging disagrees with the caller effect or bridge" + .into(), + ); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("relinquished call owner cannot be a borrowed slot".into()); + } + if require_local(loan, &local)?.ty != argument_types[parameter_index] { + return Err("relinquished call loan has the wrong operand type".into()); + } + } + ArgumentStaging::RetainedSnapshot { owner, loan } => { + if argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != CheckedCalleeAccess::View + || argument.retained_snapshot_type() + != Some(argument_types[parameter_index]) + || owner == loan + { + return Err("retained snapshot disagrees with checked endpoint or caller retention".into()); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + || require_local(loan, &local)?.ty != owner.ty + { + return Err( + "retained snapshot requires an owning endpoint and exact loan type" + .into(), + ); + } + } + ArgumentStaging::Observed { owner } => { + if argument.effect != CheckedCallerEffect::ObserveData + || argument.physical_access != CheckedCalleeAccess::Owned + { + return Err("observation owner disagrees with the caller effect".into()); + } + let owner = require_local(owner, &local)?; + if owner.ty != argument_types[parameter_index] + || owner.view_source.is_some() + || owner.is_view_parameter + || owner.view_iteration.is_some() + { + return Err("observation snapshot cannot be a borrowed slot or change the operand type".into()); + } + } + } + if source.bridge == CallBridge::Direct + && argument.physical_access != argument.callee_access + { + return Err("direct source call changed its physical parameter access".into()); + } + } + for (tail, argument) in source.generated_operands.iter().enumerate() { + if !metadata_root_matches(source, argument.actual_type, types) + || argument.actual_type != argument.parameter_type + || argument.callee_access != CheckedCalleeAccess::Owned + || argument.acquisition != GeneratedAcquisition::OwnedExpression + { + return Err( + "call ownership metadata tail has no closed compiler-owned schema".into(), + ); + } + if evaluation_order.get(source_count + tail) != Some(&(source_count + tail)) { + return Err( + "generated metadata must follow all original source operands".into(), + ); + } + validate_generated_operand(argument, &local, types)?; + } + } + CallOwnership::Generated(generated) => { + if generated.operation != generated.certificate.operation + || evaluation_order != generated.certificate.evaluation_order + || generated.arguments.len() != generated.certificate.arguments.len() + { + return Err( + "generated invocation differs from its original operation/order certificate" + .into(), + ); + } + for (index, argument) in generated.arguments.iter().enumerate() { + if argument.witness != generated.certificate.arguments[index] { + return Err( + "generated argument witness differs from its invocation certificate".into(), + ); + } + validate_generated_operand(argument, &local, types)?; + validate_generated_stage(argument, argument_types[index], &local, initial_hir)?; + } + if let GeneratedOperation::FunctionAdapter { + callee, + source_type, + .. + } = generated.operation + { + if require_local(callee, &local)?.ty != source_type { + return Err("generated callable adapter capture differs from its sealed source signature".into()); + } + } + if let GeneratedOperation::ReflectedCallableAdapter { callee, ref plan } = + generated.operation + { + if !plan.validate_types(types) { + return Err("reflected callable checked signature meaning changed".into()); + } + if require_local(callee, &local)?.ty != plan.source_type() { + return Err( + "reflected callable capture differs from its checked signature".into(), + ); + } + } + } + } + Ok(()) +} + +fn require_local( + id: LocalId, + local: &impl Fn(LocalId) -> Option, +) -> Result { + local(id).ok_or_else(|| "call ownership origin is outside its function".into()) +} + +fn validate_source( + source: CallerViewSource, + local: &impl Fn(LocalId) -> Option, +) -> Result<(), String> { + if let CallerViewSource::Local(id) = source { + require_local(id, local)?; + } + Ok(()) +} + +fn source_match_origin( + origin: &CallerOrigin, + local: &impl Fn(LocalId) -> Option, +) -> Result, String> { + let mut id = match origin { + CallerOrigin::Binding(fact) => fact.local, + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(id), + } + | CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(id), + } => *id, + _ => return Ok(None), + }; + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(id) { + return Err("call ownership match payload alias origin is cyclic".into()); + } + let metadata = require_local(id, local)?; + if let Some(proof) = metadata.match_binding { + if !proof.matches_binding(metadata.ty, metadata.span) { + return Err("call ownership match payload changed its exact declaration".into()); + } + return Ok(Some((id, proof))); + } + match metadata.view_source { + Some(source) => id = source, + None => return Ok(None), + } + } +} + +fn validate_origin( + origin: &CallerOrigin, + temporary: Option<&crate::temporary_local_views::TemporaryLocalArgument>, + local: &impl Fn(LocalId) -> Option, + types: &TypeInterner, +) -> Result<(), String> { + match origin { + CallerOrigin::Binding(fact) => { + let value = require_local(fact.local, local)?; + if !valid_type(types, fact.ty) + || value.ty != fact.ty + || value.mutable != fact.mutable + || value.span.file != fact.declaration_span.file + || fact.declaration_span.start < value.span.start + || fact.declaration_span.end > value.span.end + { + return Err( + "call ownership binding metadata differs from its declared local".into(), + ); + } + match fact.mode { + CallerBindingMode::Owned + if !jett_typecheck::ownership::is_implicitly_copyable(types, fact.ty) + && (value.view_source.is_some() + || value.is_view_parameter + || value.view_iteration.is_some()) => + { + return Err("call ownership falsely claims an owned borrowed binding".into()); + } + CallerBindingMode::View { source } => { + validate_source(source, local)?; + let expected = match source { + CallerViewSource::Local(id) => Some(id), + CallerViewSource::Other => None, + }; + let iteration = value + .view_iteration + .is_some_and(|proof| proof.matches_binding(fact.ty, fact.declaration_span)); + let matched = value + .match_binding + .is_some_and(|proof| proof.matches_binding(fact.ty, fact.declaration_span)); + let temporary = temporary.is_some_and(|proof| proof.matches(fact, value)); + if (!temporary && value.view_source != expected) + || (expected.is_none() + && !value.is_view_parameter + && !iteration + && !matched + && !temporary) + { + return Err( + "call ownership borrowed binding lost its immediate source".into() + ); + } + } + CallerBindingMode::Owned => {} + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => validate_source(*source, local)?, + CallerOrigin::OwnedExpression => {} + } + Ok(()) +} + +pub(super) fn validate_actor_arguments( + arguments: &[ArgumentOwnership], + args: &[Expression], + order: &[usize], + context: CheckedOwnershipContext, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, + initial: bool, +) -> Result<(), String> { + if arguments.len() != args.len() || order.len() != args.len() { + return Err("actor argument cardinality changed".into()); + } + let lookup = |id: LocalId| locals.get(id.index() as usize).copied(); + let mut seen = vec![false; args.len()]; + for (index, argument) in arguments.iter().enumerate() { + let witness = &argument.witness; + if argument.source_span != witness.source_span + || argument.source_index != witness.source_index + || argument.parameter_index != index + || witness.parameter_index != index + || argument.actual_type != witness.actual_type + || argument.parameter_type != witness.parameter_type + || argument.syntax != witness.syntax + || argument.origin != witness.origin + || argument.callee_access != witness.callee_access + || argument.physical_access != witness.callee_access + || argument.effect != witness.effect + || context != witness.context + || argument.staging != ArgumentStaging::Original + || argument.source_index >= args.len() + || seen[argument.source_index] + || order.get(argument.source_index) != Some(&index) + { + return Err("actor argument changed its original checked occurrence".into()); + } + seen[argument.source_index] = true; + validate_origin( + &argument.origin, + witness.temporary_origin.as_deref(), + &lookup, + types, + )?; + if source_match_origin(&argument.origin, &lookup)? != witness.match_origin { + return Err("actor argument changed scoped matched payload origin".into()); + } + validate_effect(argument, context, types)?; + if argument.syntax != CheckedCallerSyntax::Bare + || !matches!( + argument.effect, + CheckedCallerEffect::Copy + | CheckedCallerEffect::TransferOwned + | CheckedCallerEffect::RelinquishOwned + ) + { + return Err("actor message requires its own incoming value".into()); + } + if initial { + validate_source_operand(&args[index], argument, &CallBridge::Direct, locals, types)?; + } else if !source_parameter_matches( + argument, + args[index].ty, + argument.parameter_type, + types, + ) { + return Err("actor argument changed checked operand type".into()); + } + } + Ok(()) +} + +fn validate_effect( + argument: &ArgumentOwnership, + context: CheckedOwnershipContext, + types: &TypeInterner, +) -> Result<(), String> { + if argument.syntax == CheckedCallerSyntax::WrittenView + && argument.callee_access == CheckedCalleeAccess::Owned + { + return Err("call ownership written view cannot satisfy a source-owned parameter".into()); + } + let copyable = jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type); + if argument.effect == CheckedCallerEffect::ObserveData { + if context == CheckedOwnershipContext::Ordinary { + return Err("ordinary call has no observation retention authority".into()); + } + return Ok(()); // Snapshot/capture acquisition is proved independently by its consumer. + } + let expected = if copyable { + CheckedCallerEffect::Copy + } else if argument.syntax == CheckedCallerSyntax::WrittenView { + CheckedCallerEffect::RetainBorrow + } else { + if matches!( + argument.origin, + CallerOrigin::Binding(CallerBindingFact { + mode: CallerBindingMode::View { .. }, + .. + }) | CallerOrigin::BorrowedProjection { .. } + ) { + return Err("bare move-only borrowed call input cannot acquire ownership".into()); + } + match argument.callee_access { + CheckedCalleeAccess::Owned => CheckedCallerEffect::TransferOwned, + CheckedCalleeAccess::View => CheckedCallerEffect::RelinquishOwned, + } + }; + if argument.effect != expected { + return Err("call ownership effect does not follow its checked tuple".into()); + } + Ok(()) +} + +fn validate_generated_operand( + argument: &GeneratedArgumentOwnership, + local: &impl Fn(LocalId) -> Option, + types: &TypeInterner, +) -> Result<(), String> { + if !valid_type(types, argument.actual_type) || !valid_type(types, argument.parameter_type) { + return Err("generated call ownership type is outside its interner".into()); + } + match argument.acquisition { + GeneratedAcquisition::Copy + if !jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type) => + { + return Err("generated operand cannot copy a move-only value".into()); + } + GeneratedAcquisition::Borrow { source } => { + if argument.callee_access != CheckedCalleeAccess::View { + return Err("generated borrow cannot supply physical ownership".into()); + } + validate_source(source, local)?; + } + GeneratedAcquisition::OwnedExpression + if argument.callee_access != CheckedCalleeAccess::Owned => + { + return Err("generated owner has no physical ownership destination".into()); + } + _ => {} + } + Ok(()) +} + +/// Ordinary snapshot domain. Function capture authority is proved from the +/// descriptor's checked construction separately, never from its signature. +pub fn observation_data_type(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if !valid_type(types, ty) { + return false; + } + if !seen.insert(ty) { + return true; + } + if !types + .nominal_type_arguments(ty) + .iter() + .all(|&argument| visit(types, argument, seen)) + { + return false; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Capability(_) + | Type::Actor(_) + | Type::Interface(_) + | Type::Function { .. } + | Type::Error + | Type::TypeConstruction => false, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) && visit(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| visit(types, field.ty, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)) + }), + Type::Machine(id) => types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)) + }), + Type::MachineState { machine, state } => types + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)) + }), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Never => true, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +impl crate::BodyLowerer<'_, '_> { + pub(crate) fn generated_ownership( + &mut self, + operation: GeneratedOperation, + args: &[Expression], + parameter_types: &[TypeId], + access: &[CheckedCalleeAccess], + result_type: TypeId, + evaluation_order: &[usize], + span: Span, + ) -> Option { + match CallOwnership::generated( + operation, + args, + parameter_types, + access, + result_type, + evaluation_order, + &self.parent.check.interner, + ) { + Ok(ownership) => Some(ownership), + Err(message) => { + self.parent.error(span, message); + None + } + } + } + pub(crate) fn finish_call_ownership( + &mut self, + mut ownership: CallOwnership, + function: Option, + intrinsic: Option, + args: &[Expression], + span: Span, + ) -> Option { + let result = self.join_physical_ownership(&mut ownership, function, intrinsic, args); + if let Err(message) = result { + self.parent.error(span, message); + return None; + } + Some(ownership) + } + + pub(crate) fn finish_resource_call_ownership( + &mut self, + mut ownership: CallOwnership, + hook: &crate::ResourceHookRef, + args: &[Expression], + span: Span, + ) -> Option { + let proof = (|| -> Result<(), String> { + hook.validate(&self.parent.check.interner)?; + if !self.parent.resource_manifest.contains_hook(hook) { + return Err("Resource hook is from another checked program".into()); + } + let CallOwnership::Source(source) = &mut ownership else { + return Err("Resource invocation requires an original Source certificate".into()); + }; + if source.target != CallTarget::ResourceHook(hook.clone()) + || source.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type(), + }) + || source.arguments.len() != args.len() + || !source.generated_operands.is_empty() + { + return Err( + "Resource invocation differs from its exact checked hook target".into(), + ); + } + source.bridge = CallBridge::ResourceHook { hook: hook.clone() }; + source.certificate.bridge = source.bridge.clone(); + Ok(()) + })(); + if let Err(message) = proof { + self.parent.error(span, message); + return None; + } + Some(ownership) + } + + fn join_physical_ownership( + &self, + ownership: &mut CallOwnership, + function: Option, + intrinsic: Option, + args: &[Expression], + ) -> Result<(), String> { + let CallOwnership::Source(source) = ownership else { + return Err("source handoff requires a source packet".into()); + }; + let source_count = source.arguments.len(); + if source_count > args.len() { + return Err("source handoff removed an original operand".into()); + } + if let Some(function) = function { + let (params, access) = self.ownership_function_parameters(function)?; + if params.len() != args.len() { + return Err("source handoff physical function arity changed".into()); + } + let direct = matches!(source.target, CallTarget::Function(target) | CallTarget::Interface { function: target, .. } if target == function); + if !direct { + let CallTarget::Intrinsic(intrinsic) = source.target else { + return Err( + "source call cannot redirect to an unrelated implementation function" + .into(), + ); + }; + if !json_intrinsic(intrinsic) || !self.json_ownership_helper(function, intrinsic) { + return Err("source JSON handoff has no closed trusted helper identity".into()); + } + source.bridge = CallBridge::JsonSource { + intrinsic, + function, + }; + } + for (index, argument) in source.arguments.iter_mut().enumerate() { + argument.physical_access = access[index]; + } + } else if let Some(intrinsic) = intrinsic { + match source.target.clone() { + CallTarget::Intrinsic(original) if original == intrinsic => {} + CallTarget::Function(function) => { + let key = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + .ok_or("intrinsic facade has no exact source declaration key")?; + let identity = self.ownership_declaration(key)?; + let CheckedInvocationShape::Function { signature_type } = source.shape else { + return Err("intrinsic facade has no checked signature".into()); + }; + if !trusted_intrinsic_declaration(&identity, intrinsic) { + return Err("intrinsic handoff is not a closed compiler facade".into()); + } + source.target = CallTarget::Declaration { + identity, + signature_type, + }; + source.bridge = CallBridge::TrustedIntrinsic { intrinsic }; + } + CallTarget::Declaration { ref identity, .. } + if trusted_intrinsic_declaration(identity, intrinsic) => + { + source.bridge = CallBridge::TrustedIntrinsic { intrinsic }; + } + _ => return Err("source intrinsic handoff changed its checked target".into()), + } + for (index, argument) in source.arguments.iter_mut().enumerate() { + argument.physical_access = + if matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + argument.callee_access + } else { + jett_typecheck::intrinsic_operand_access(intrinsic, index, source_count) + .ok_or("source intrinsic physical operand has no closed access")? + }; + } + } + for argument in &args[source_count..] { + validate_compiler_metadata_operand(argument, source, &self.parent.check.interner)?; + source.generated_operands.push(GeneratedArgumentOwnership { + actual_type: argument.ty, + parameter_type: argument.ty, + callee_access: CheckedCalleeAccess::Owned, + acquisition: GeneratedAcquisition::OwnedExpression, + staging: GeneratedArgumentStaging::Existing { loan: None }, + staging_certificate: GeneratedArgumentStaging::Existing { loan: None }, + witness: generated_witness( + argument, + argument.ty, + CheckedCalleeAccess::Owned, + GeneratedAcquisition::OwnedExpression, + &self.parent.check.interner, + ), + }); + } + source.certificate.generated_operands = source.generated_operands.clone(); + source.certificate.target = source.target.clone(); + source.certificate.bridge = source.bridge.clone(); + Ok(()) + } + + pub(crate) fn ownership_function_parameters( + &self, + function: FunctionId, + ) -> Result<(Vec, Vec), String> { + let key = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + .ok_or("physical source helper has no exact HIR key")?; + let (params, views) = match key { + crate::FunctionKey::Definition { + definition, + concrete_args, + specialization, + } if !concrete_args.is_empty() => { + let instance = self + .parent + .check + .generic_function_instantiations + .iter() + .find(|instance| { + instance.definition == *definition + && instance.concrete_args == *concrete_args + && instance.specialization == *specialization + }) + .ok_or("physical source helper has no exact checked generic body")?; + let source = self + .parent + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Function(source) + if self.parent.definition_at( + source.name.span, + jett_resolve::DefKind::Function, + ) == Some(*definition) => + { + Some(source) + } + _ => None, + }) + .ok_or("physical helper has no source parameter modes")?; + ( + instance.parameter_types.clone(), + source.params.iter().map(|param| param.view).collect(), + ) + } + crate::FunctionKey::Definition { definition, .. } => { + let ty = *self + .parent + .check + .definition_types + .get(definition) + .ok_or("physical source helper has no checked function type")?; + let Type::Function { + params, + view_params, + .. + } = self.parent.check.interner.resolve(ty) + else { + return Err("physical source helper type is not a function".into()); + }; + (params.clone(), view_params.clone()) + } + crate::FunctionKey::Method { source_span } => { + let method = self + .parent + .check + .method_definitions + .iter() + .find(|method| method.source_span == *source_span) + .ok_or("physical method has no checked declaration")?; + let source = self + .parent + .method_at(*source_span) + .ok_or("physical method has no source parameter modes")?; + ( + method.parameter_types.clone(), + source.params.iter().map(|param| param.view).collect(), + ) + } + crate::FunctionKey::Interface { owner, method } => { + let Type::Interface(id) = self.parent.check.interner.resolve(*owner) else { + return Err("physical dispatcher is not an interface".into()); + }; + let signature = self + .parent + .check + .interner + .resolve_interface(*id) + .methods + .get(*method) + .ok_or("physical interface slot is out of bounds")?; + ( + signature.params.iter().map(|(_, ty, _)| *ty).collect(), + signature.params.iter().map(|(_, _, view)| *view).collect(), + ) + } + }; + Ok(( + params, + views + .into_iter() + .map(|view| { + if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) + .collect(), + )) + } + + fn json_ownership_helper(&self, function: FunctionId, intrinsic: IntrinsicId) -> bool { + let Some(key) = self + .function_ids + .iter() + .find_map(|(key, &id)| (id == function).then_some(key)) + else { + return false; + }; + self.ownership_declaration(key) + .is_ok_and(|identity| json_helper_declaration(&identity, intrinsic)) + } + + pub(crate) fn source_ownership( + &mut self, + span: Span, + args: &[Expression], + evaluation_order: &[usize], + ) -> Option { + let result = self.build_source_ownership(span, args, evaluation_order); + match result { + Ok(value) => Some(value), + Err(message) => { + self.parent.error(span, message); + None + } + } + } + + fn build_source_ownership( + &self, + span: Span, + args: &[Expression], + evaluation_order: &[usize], + ) -> Result { + let checked = self + .call_ownership + .get(&span) + .ok_or("source invocation has no checked caller ownership")?; + if checked.context != self.ownership_context { + return Err("source invocation ownership context is not its lexical context".into()); + } + if checked.arguments.len() != args.len() || evaluation_order.len() != args.len() { + return Err("checked caller ownership source operand count changed".into()); + } + let target = self.checked_ownership_target(&checked.target, &checked.shape)?; + let arguments = self.build_source_arguments( + span, + args, + evaluation_order, + &checked.arguments, + checked.context, + )?; + let shape = checked.shape.clone(); + let context = checked.context; + let result_type = self + .pipeline_step_call_types + .get(&span) + .or_else(|| self.expression_types.get(&span)) + .copied() + .ok_or("source invocation has no exact checked result type")?; + let ownership = CallOwnership::Source(SourceCallOwnership { + target: target.clone(), + shape: shape.clone(), + context, + arguments, + bridge: CallBridge::Direct, + generated_operands: Vec::new(), + temporary_local_backing: None, + certificate: Box::new(SourceCallCertificate { + target, + shape, + context, + bridge: CallBridge::Direct, + result_type, + generated_operands: Vec::new(), + }), + }); + validate_operand_ownership( + &ownership, + &args.iter().map(|arg| arg.ty).collect::>(), + evaluation_order, + &self.parent.check.interner, + |id| self.ownership_local_info(id), + true, + )?; + Ok(ownership) + } + + pub(super) fn build_source_arguments( + &self, + span: Span, + args: &[Expression], + evaluation_order: &[usize], + checked_arguments: &[jett_typecheck::CheckedArgumentOwnership], + context: CheckedOwnershipContext, + ) -> Result, String> { + if context != self.ownership_context + || checked_arguments.len() != args.len() + || evaluation_order.len() != args.len() + { + return Err("actor/source argument context or arity changed".into()); + } + let mut arguments = Vec::with_capacity(args.len()); + for parameter_index in 0..args.len() { + let fact = checked_arguments + .iter() + .find(|fact| fact.parameter_index == parameter_index) + .ok_or("checked caller ownership has no exact formal occurrence")?; + if checked_arguments.get(fact.source_index) != Some(fact) + || evaluation_order.get(fact.source_index) != Some(¶meter_index) + { + return Err("checked caller ownership source/formal join changed".into()); + } + if self.source_expression_types.get(&fact.source_span) != Some(&fact.actual_type) { + return Err( + "call ownership raw source occurrence differs from its exact checked body" + .into(), + ); + } + // A pipeline input and its field/callable target can share a span. + // The dedicated ledger retains input authority without overwriting + // either the target expression or the prior raw source output. + let occurrence_type = + if fact.source_index == 0 && self.pipeline_step_call_types.contains_key(&span) { + *self + .pipeline_step_input_types + .get(&span) + .ok_or("pipeline ownership input has no exact checked physical type")? + } else { + *self + .expression_types + .get(&fact.source_span) + .ok_or("call ownership occurrence has no exact checked physical type")? + }; + if occurrence_type != fact.actual_type + && !(has_outer_secret(&self.parent.check.interner, fact.parameter_type) + && secret_taint_matches( + &self.parent.check.interner, + occurrence_type, + fact.actual_type, + )) + { + return Err( + "call ownership occurrence has no checked outer-secret qualification".into(), + ); + } + let origin = self.checked_caller_origin(&fact.origin)?; + let observation = if fact.effect == CheckedCallerEffect::ObserveData { + Some( + match self.observation_proof(&args[parameter_index], fact.actual_type) { + Ok(proof) => proof, + Err(original) => crate::call_observation::construct( + self, + span, + parameter_index, + fact.source_span, + &origin, + &args[parameter_index], + fact.actual_type, + )? + .ok_or(original)?, + }, + ) + } else { + None + }; + let witness = SourceArgumentWitness { + source_span: fact.source_span, + source_index: fact.source_index, + parameter_index, + actual_type: fact.actual_type, + occurrence_type, + parameter_type: fact.parameter_type, + syntax: fact.syntax, + origin: origin.clone(), + callee_access: fact.callee_access, + effect: fact.effect, + context, + observation, + handled_result: None, + projection_root: self.checked_flow_projection_root(fact.source_span, &origin)?, + match_origin: source_match_origin(&origin, &|id| self.ownership_local_info(id))?, + temporary_origin: self.temporary_argument_origin(&origin), + }; + let mut argument = ArgumentOwnership { + source_span: fact.source_span, + source_index: fact.source_index, + parameter_index, + actual_type: fact.actual_type, + parameter_type: fact.parameter_type, + syntax: fact.syntax, + origin: origin.clone(), + callee_access: fact.callee_access, + physical_access: fact.callee_access, + effect: fact.effect, + staging: ArgumentStaging::Original, + witness, + retained_snapshot: (fact.effect == CheckedCallerEffect::RetainBorrow + && retained_snapshot_origin(&origin) + && observation_data_type(&self.parent.check.interner, fact.actual_type) + && observation_data_type(&self.parent.check.interner, occurrence_type)) + .then_some(RetainedSnapshotProof { + actual_type: fact.actual_type, + occurrence_type, + physical_span: args[parameter_index].span, + }), + }; + if argument.origin == CallerOrigin::OwnedExpression { + let original = source_operand( + &args[parameter_index], + &argument, + &CallBridge::Direct, + &self.parent.check.interner, + )?; + let backing = source_view_backing(original); + if matches!(backing.kind, ExpressionKind::Handle { .. }) { + argument.witness.handled_result = Some(SourceHandledResult { + ty: backing.ty, + span: backing.span, + }); + } + } + arguments.push(argument); + } + Ok(arguments) + } + + fn checked_flow_projection_root( + &self, + source_span: Span, + origin: &CallerOrigin, + ) -> Result, String> { + let local = match origin { + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(id), + } + | CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(id), + } => *id, + _ => return Ok(None), + }; + let stored = self + .locals + .get(local.index() as usize) + .filter(|stored| stored.id == local) + .ok_or("call ownership projection root has no exact stored local")?; + if !valid_type(&self.parent.check.interner, stored.ty) { + return Err("call ownership projection root has invalid stored metadata".into()); + } + if !matches!( + self.parent.check.interner.resolve(stored.ty), + Type::Machine(_) + ) { + return Ok(None); + } + let root = self + .source_projection_roots + .get(&source_span) + .ok_or("call ownership projection has no original source AST root")?; + let occurrence_type = *self + .source_expression_types + .get(&root.span) + .ok_or("call ownership projection root has no exact checked body type")?; + if !flow_projection_root_matches(&self.parent.check.interner, occurrence_type, stored.ty) { + return Ok(None); + } + let definition = self + .parent + .resolve + .resolutions + .get(&root.span) + .ok_or("call ownership projection root has no exact source definition")?; + if self.local_ids.get(definition) != Some(&local) + || self.expression_types.get(&root.span) != Some(&occurrence_type) + || root.span.file != source_span.file + || root.span.start < source_span.start + || root.span.end > source_span.end + || root.span.start >= root.span.end + { + return Err( + "call ownership flow projection differs from its original source root".into(), + ); + } + Ok(Some(SourceProjectionRoot { + local, + span: root.span, + occurrence_type, + storage_type: stored.ty, + })) + } + + fn checked_ownership_target( + &self, + target: &jett_typecheck::CheckedInvocationTarget, + shape: &CheckedInvocationShape, + ) -> Result { + use jett_typecheck::CheckedInvocationTarget as Target; + if let Target::Resolved(definition) = target + && let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(*definition) + { + return Ok(CallTarget::ResourceHook(hook)); + } + // A resolver kernel without the original envelope cannot become an ordinary function. + if let Target::Resolved(definition) = target + && self + .parent + .resolve + .resource_kernels + .contains_definition(*definition) + { + return Err( + "Resource hook requires lower_checked_resource_program and its original manifest" + .into(), + ); + } + let key = match target { + Target::Resolved(definition) => crate::FunctionKey::Definition { + definition: *definition, + concrete_args: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + Target::Generic(generic) => crate::FunctionKey::Definition { + definition: generic.definition, + concrete_args: generic.concrete_args.clone(), + specialization: generic.specialization.clone(), + }, + Target::Method(method) => crate::FunctionKey::Method { + source_span: method.source_span, + }, + Target::Interface(interface) => { + let key = crate::FunctionKey::Interface { + owner: interface.interface_type, + method: interface.method_index, + }; + let function = *self + .function_ids + .get(&key) + .ok_or("source interface ownership target has no HIR dispatcher")?; + return Ok(CallTarget::Interface { + function, + interface_type: interface.interface_type, + method_index: interface.method_index, + }); + } + Target::Indirect(signature_type) => { + return Ok(CallTarget::Indirect { + signature_type: *signature_type, + }); + } + Target::Intrinsic(intrinsic) => return Ok(CallTarget::Intrinsic(*intrinsic)), + }; + if let Some(&function) = self.function_ids.get(&key) { + return Ok(CallTarget::Function(function)); + } + let CheckedInvocationShape::Function { signature_type } = shape else { + return Err("source declaration ownership target has no function signature".into()); + }; + let identity = self.ownership_declaration(&key)?; + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib { + return Err("source ownership declaration has no executable HIR target".into()); + } + Ok(CallTarget::Declaration { + identity, + signature_type: *signature_type, + }) + } + + fn ownership_declaration(&self, key: &crate::FunctionKey) -> Result { + let crate::FunctionKey::Definition { + definition, + concrete_args, + specialization, + } = key + else { + return Err("ownership declaration is not an ordinary concrete source function".into()); + }; + let info = self + .parent + .resolve + .scope_table + .definitions + .get(definition.index() as usize) + .filter(|info| info.id == *definition) + .ok_or("ownership declaration definition is outside its resolution session")?; + if info.kind != jett_resolve::DefKind::Function { + return Err("ownership declaration is not a function definition".into()); + } + let origin = self + .parent + .origins + .get(&info.span.file) + .cloned() + .ok_or("ownership declaration has no compiler source origin")?; + Ok(FunctionIdentity { + declaration: crate::DeclarationId { + origin, + namespace: info.namespace.clone().unwrap_or_default(), + name: info + .name + .rsplit('.') + .next() + .unwrap_or(&info.name) + .to_string(), + kind: crate::DeclarationKind::Function, + }, + scoped_type_bindings: Vec::new(), + type_arguments: concrete_args.clone(), + specialization: specialization.clone(), + }) + } + + fn checked_view_source( + &self, + source: jett_typecheck::CheckedViewSource, + ) -> Result { + match source { + jett_typecheck::CheckedViewSource::Other => Ok(CallerViewSource::Other), + jett_typecheck::CheckedViewSource::Binding(definition) => self + .local_ids + .get(&definition) + .copied() + .map(CallerViewSource::Local) + .ok_or_else(|| "caller origin has no local in its exact checked body".into()), + } + } + + pub(super) fn ownership_local_info(&self, id: LocalId) -> Option { + let local = self + .locals + .get(id.index() as usize) + .filter(|local| local.id == id)?; + let is_view_parameter = self.view_parameter_locals.contains(&id); + Some(OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter, + view_iteration: self.view_iteration_bindings.get(&id).copied(), + match_binding: self.match_bindings.get(&id).copied(), + }) + } + + pub(crate) fn check_pipeline_actuals( + &mut self, + span: Span, + piped: &Expression, + written_view: bool, + extras: &[jett_parser::ast::CallArg], + ) -> Option<()> { + let valid = self.call_ownership.get(&span).is_some_and(|checked| { + checked.arguments.len() == extras.len() + 1 + && checked.arguments.first().is_some_and(|first| { + self.source_expression_types.get(&first.source_span) == Some(&first.actual_type) + && self.pipeline_step_input_types.get(&span) == Some(&piped.ty) + && first.syntax + == if written_view || original_view(piped) { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + } + }) + && checked.arguments[1..] + .iter() + .zip(extras) + .all(|(fact, extra)| { + fact.source_span == extra.value.span() + && fact.syntax == source_syntax(&extra.value) + && self.source_expression_types.get(&extra.value.span()) + == Some(&fact.actual_type) + }) + }); + if !valid { + self.parent.error( + span, + "pipeline ownership differs from its original virtual source occurrences", + ); + return None; + } + Some(()) + } + + fn checked_caller_origin( + &self, + origin: &jett_typecheck::CheckedCallerOrigin, + ) -> Result { + use jett_typecheck::CheckedCallerOrigin as Origin; + match origin { + Origin::Binding(fact) => { + if self.binding_facts.get(&fact.declaration_span) != Some(fact) { + return Err("caller binding fact is not owned by its exact checked body".into()); + } + let local = *self + .local_ids + .get(&fact.definition) + .ok_or("caller binding has no exact HIR local")?; + let mode = match fact.mode { + jett_typecheck::CheckedBindingMode::Owned => CallerBindingMode::Owned, + jett_typecheck::CheckedBindingMode::View { source } => { + CallerBindingMode::View { + source: self.checked_view_source(source)?, + } + } + }; + Ok(CallerOrigin::Binding(CallerBindingFact { + local, + declaration_span: fact.declaration_span, + ty: fact.ty, + mode, + mutable: fact.mutable, + })) + } + Origin::BorrowedProjection { source } => Ok(CallerOrigin::BorrowedProjection { + source: self.checked_view_source(*source)?, + }), + Origin::OwnedFieldCopy { parent } => Ok(CallerOrigin::OwnedFieldCopy { + parent: self.checked_view_source(*parent)?, + }), + Origin::OwnedExpression => Ok(CallerOrigin::OwnedExpression), + } + } + + pub(crate) fn check_source_actuals( + &mut self, + span: Span, + actuals: &[jett_parser::ast::CallArg], + ) -> Option<()> { + let valid = self.call_ownership.get(&span).is_some_and(|checked| { + checked.arguments.len() == actuals.len() + && checked.arguments.iter().zip(actuals).all(|(fact, actual)| { + fact.source_span == actual.value.span() + && fact.syntax == source_syntax(&actual.value) + && self.source_expression_types.get(&actual.value.span()) + == Some(&fact.actual_type) + }) + }); + if !valid { + self.parent.error( + span, + "caller ownership differs from its original source argument syntax or type", + ); + return None; + } + Some(()) + } + + pub(super) fn observation_proof( + &self, + expression: &Expression, + actual_type: TypeId, + ) -> Result { + let mut value = expression; + while let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + let types = &self.parent.check.interner; + if observation_data_type(types, actual_type) { + return Ok(ObservationProof { + actual_type, + captures: Vec::new(), + descriptor_checked: false, + purpose: None, + }); + } + if !valid_type(types, actual_type) + || !matches!(types.resolve(actual_type), Type::Function { .. }) + { + return Err( + "observing caller input has no supported ordinary-data snapshot proof".into(), + ); + } + let captures = match &value.kind { + ExpressionKind::FunctionRef(_) => Vec::new(), + ExpressionKind::InlineFunction { + local_floor, body, .. + } => self + .locals + .iter() + .filter(|local| { + local.id.index() < *local_floor + && crate::inline_functions::body_uses_local(body, local.id) + }) + .map(|local| (local.id, local.ty)) + .collect(), + _ => { + return Err( + "observing function descriptor has no checked capture construction proof" + .into(), + ); + } + }; + for &(id, ty) in &captures { + let local = self + .ownership_local_info(id) + .ok_or("observing descriptor capture is outside its function")?; + if local.view_source.is_some() + || local.is_view_parameter + || local.view_iteration.is_some() + || !observation_data_type(types, ty) + { + return Err( + "observing descriptor capture cannot acquire authority or a borrowed owner" + .into(), + ); + } + } + Ok(ObservationProof { + actual_type, + captures, + descriptor_checked: true, + purpose: None, + }) + } +} + +fn source_syntax(mut expression: &jett_parser::ast::Expr) -> CheckedCallerSyntax { + loop { + match expression { + jett_parser::ast::Expr::Paren(inner, _) + | jett_parser::ast::Expr::Coarsen(inner, _) + | jett_parser::ast::Expr::Declassify(inner, _) => expression = inner, + jett_parser::ast::Expr::View(_, _) => return CheckedCallerSyntax::WrittenView, + _ => return CheckedCallerSyntax::Bare, + } + } +} + +/// Syntactic place provenance only, recorded while lowering the original AST. +pub(super) fn source_projection_ast_root( + mut expression: &jett_parser::ast::Expr, +) -> Option<&jett_parser::ast::Ident> { + use jett_parser::ast::Expr; + let mut projected = false; + loop { + expression = match expression { + Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) => inner, + Expr::FieldAccess(base, _, _) => { + projected = true; + base + } + Expr::Ident(root) if projected => return Some(root), + _ => return None, + }; + } +} + +fn original_view(mut expression: &Expression) -> bool { + loop { + match &expression.kind { + ExpressionKind::Coarsen(inner) | ExpressionKind::Declassify(inner) => { + expression = inner + } + ExpressionKind::View(_) => return true, + _ => return false, + } + } +} + +fn json_intrinsic(intrinsic: IntrinsicId) -> bool { + matches!( + intrinsic, + IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + ) +} + +fn trusted_intrinsic_declaration(identity: &FunctionIdentity, intrinsic: IntrinsicId) -> bool { + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib + || identity.declaration.kind != crate::DeclarationKind::Function + { + return false; + } + match ( + identity.declaration.namespace.as_str(), + identity.declaration.name.as_str(), + intrinsic, + ) { + ("math", "average", IntrinsicId::MathAverage) + | ("math", "median", IntrinsicId::MathMedian) => true, + ("json", "serialize", IntrinsicId::JsonSerialize) + | ("json", "serialize_public", IntrinsicId::JsonSerializePublic) + | ("json", "parse", IntrinsicId::JsonParse) + | ("json", "parse_exact", IntrinsicId::JsonParseExact) => true, + _ => false, + } +} + +fn json_helper_declaration(identity: &FunctionIdentity, intrinsic: IntrinsicId) -> bool { + if identity.declaration.origin != jett_common::SourceOrigin::Stdlib + || identity.declaration.namespace != "json" + || identity.declaration.kind != crate::DeclarationKind::Function + { + return false; + } + match intrinsic { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => matches!( + identity.declaration.name.as_str(), + "serialize" + | "serialize_public" + | "serialize_raw" + | "json_serialize_native_enum" + | "json_serialize_native_bitfield" + | "json_serialize_native_bytes" + | "json_serialize_native_nothing" + ), + IntrinsicId::JsonParse => matches!( + identity.declaration.name.as_str(), + "parse" + | "parse_raw" + | "json_parse_native_enum" + | "json_parse_native_bitfield" + | "json_parse_native_secret_tree" + | "json_parse_native_string" + | "json_parse_native_bool" + | "json_parse_native_int64" + | "json_parse_native_uint8" + | "json_parse_native_uint64" + | "json_parse_native_float64" + | "json_parse_native_bytes" + | "json_parse_native_nothing" + ), + IntrinsicId::JsonParseExact => matches!( + identity.declaration.name.as_str(), + "parse_exact" + | "parse_raw" + | "json_parse_exact_native_enum" + | "json_parse_exact_native_bitfield" + | "json_parse_native_secret_tree" + | "json_parse_native_string" + | "json_parse_native_bool" + | "json_parse_native_int64" + | "json_parse_native_uint8" + | "json_parse_native_uint64" + | "json_parse_native_float64" + | "json_parse_native_bytes" + | "json_parse_native_nothing" + ), + _ => false, + } +} + +/// Full typed HIR gate, run on every retained or unused invocation before MIR. +pub fn validate_program_call_ownership( + program: &crate::Program, + types: &TypeInterner, +) -> Result<(), Vec> { + crate::validate_backend_types(program, types) +} + +pub fn validate_hir_invocation( + functions: &[crate::Function], + locals: &[OwnershipLocalInfo], + expression: &Expression, + types: &TypeInterner, + context: CheckedOwnershipContext, +) -> Result<(), String> { + if let ExpressionKind::ActorMessage { proof, .. } = &expression.kind { + return proof.validate(functions, expression, locals, types, context, true); + } + let (ownership, args, order) = match &expression.kind { + ExpressionKind::Call { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::IndirectCall { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::ResourceInvoke { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + validate_operand_ownership( + ownership, + &args.iter().map(|arg| arg.ty).collect::>(), + order, + types, + |id| locals.get(id.index() as usize).copied(), + true, + )?; + validate_invocation_target(functions, expression, types, context)?; + match ownership { + CallOwnership::Source(source) => { + for (index, argument) in source.arguments.iter().enumerate() { + if let Some(purpose) = argument + .observation_proof() + .and_then(|proof| proof.purpose.as_ref()) + { + purpose.validate_read(expression)?; + } + if argument + .retained_snapshot_span() + .is_some_and(|span| span != args[index].span) + { + return Err( + "call ownership retained snapshot physical occurrence changed".into(), + ); + } + validate_source_operand(&args[index], argument, &source.bridge, locals, types)?; + } + for (tail, argument) in source.generated_operands.iter().enumerate() { + validate_generated_operand_tree( + &args[source.arguments.len() + tail], + argument, + locals, + )?; + validate_compiler_metadata_operand( + &args[source.arguments.len() + tail], + source, + types, + )?; + } + } + CallOwnership::Generated(generated) => { + for (value, argument) in args.iter().zip(&generated.arguments) { + validate_generated_operand_tree(value, argument, locals)?; + } + } + } + Ok(()) +} + +/// Validate invocation identity without granting authority to a rewritten operand. +/// MIR separately proves each generated result, owning Let and BeginCallView. +pub fn validate_invocation_target( + functions: &[crate::Function], + expression: &Expression, + types: &TypeInterner, + context: CheckedOwnershipContext, +) -> Result<(), String> { + let (ownership, args, _order) = match &expression.kind { + ExpressionKind::Call { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::IndirectCall { + ownership, + args, + evaluation_order, + .. + } + | ExpressionKind::ResourceInvoke { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + if ownership.parameter_count() != args.len() { + return Err("call ownership target operand count is invalid".into()); + } + let mut valid_metadata = true; + ownership.metadata_types(|ty| valid_metadata &= valid_type(types, ty)); + if !valid_metadata { + return Err("call ownership target metadata type is outside its interner".into()); + } + if let CallOwnership::Source(source) = ownership { + if source.target != source.certificate.target + || source.shape != source.certificate.shape + || source.context != source.certificate.context + || source.bridge != source.certificate.bridge + || source.generated_operands != source.certificate.generated_operands + { + return Err( + "call ownership invocation differs from its checked source certificate".into(), + ); + } + } + if args.iter().any(|argument| !valid_type(types, argument.ty)) { + return Err("call ownership physical operand type is outside its interner".into()); + } + let target = match &expression.kind { + ExpressionKind::Call { function, .. } => Some(function_lookup(functions, *function)?), + _ => None, + }; + match ownership { + CallOwnership::Source(source) => { + if expression.ty != source.certificate.result_type { + return Err( + "call ownership result differs from its checked source certificate".into(), + ); + } + if source.context != context { + return Err("call ownership source context is not its lexical owner".into()); + } + match (&source.target, &source.bridge, &expression.kind) { + ( + CallTarget::ResourceHook(target_hook), + CallBridge::ResourceHook { hook: bridge_hook }, + ExpressionKind::ResourceInvoke { hook, .. }, + ) if target_hook == hook && bridge_hook == hook => { + hook.validate(types)?; + if source.shape + != (CheckedInvocationShape::Function { + signature_type: hook.function_type(), + }) + || !source.generated_operands.is_empty() + { + return Err("Resource invocation changed its original hook signature or added Generated operands".into()); + } + let Type::Function { + params, + view_params, + .. + } = types.resolve(hook.function_type()) + else { + return Err("Resource hook has no exact function signature".into()); + }; + if params.len() != args.len() || view_params.len() != args.len() { + return Err("Resource invocation hook arity changed".into()); + } + for (index, argument) in source.arguments.iter().enumerate() { + let expected_access = if view_params[index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + if argument.physical_access != expected_access + || argument.callee_access != expected_access + || argument.parameter_type != params[index] + || !source_parameter_matches( + argument, + args[index].ty, + params[index], + types, + ) + { + return Err("Resource invocation differs from its exact source formal type or mode".into()); + } + } + } + + ( + CallTarget::Function(source_target), + CallBridge::Direct, + ExpressionKind::Call { function, .. }, + ) if source_target == function => { + validate_function_shape( + &source.shape, + target.ok_or("source call has no physical function")?, + types, + )?; + } + ( + CallTarget::Interface { + function: source_target, + interface_type, + method_index, + }, + CallBridge::Direct, + ExpressionKind::Call { function, .. }, + ) if source_target == function => { + let target = + target.ok_or("source interface call has no physical dispatcher")?; + validate_function_shape(&source.shape, target, types)?; + let Type::Interface(id) = types.resolve(*interface_type) else { + return Err("source interface ownership owner is not an interface".into()); + }; + let method = types + .resolve_interface(*id) + .methods + .get(*method_index) + .ok_or("source interface ownership slot is out of bounds")?; + if target.source_definition.is_some() + || target.params.len() != method.params.len() + || target + .params + .iter() + .zip(&method.params) + .any(|(param, (_, ty, view))| { + param.ty != *ty || (param.mode == crate::ParamMode::View) != *view + }) + || target.return_type != method.return_type + { + return Err( + "source interface ownership dispatcher differs from the checked slot" + .into(), + ); + } + } + ( + CallTarget::Indirect { signature_type }, + CallBridge::Direct, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if callee.ty != *signature_type + || source.shape + != (CheckedInvocationShape::Function { + signature_type: *signature_type, + }) + { + return Err( + "indirect call ownership signature differs from its actual callee" + .into(), + ); + } + } + ( + CallTarget::Intrinsic(source_id), + CallBridge::Direct, + ExpressionKind::Intrinsic { intrinsic, .. }, + ) if source_id == intrinsic => { + if !matches!(source.shape, CheckedInvocationShape::Intrinsic { intrinsic: id, .. } if id == *intrinsic) + { + return Err("source intrinsic ownership shape has another operation".into()); + } + } + ( + CallTarget::Declaration { + identity, + signature_type, + }, + CallBridge::TrustedIntrinsic { intrinsic: bridge }, + ExpressionKind::Intrinsic { intrinsic, .. }, + ) if bridge == intrinsic + && trusted_intrinsic_declaration(identity, *intrinsic) + && source.shape + == (CheckedInvocationShape::Function { + signature_type: *signature_type, + }) => {} + ( + CallTarget::Intrinsic(source_id), + CallBridge::JsonSource { + intrinsic: bridge, + function: helper, + }, + ExpressionKind::Call { function, .. }, + ) if source_id == bridge && helper == function && json_intrinsic(*bridge) => { + let target = target.ok_or("source JSON ownership helper is absent")?; + if !json_helper_declaration(&target.identity, *bridge) + || target.capture_count != 0 + { + return Err( + "source JSON ownership helper has no trusted closed identity".into(), + ); + } + let CheckedInvocationShape::Intrinsic { result_type, .. } = source.shape else { + return Err("source JSON helper has no original intrinsic shape".into()); + }; + if target.return_type != result_type { + return Err("source JSON helper changed the checked result type".into()); + } + } + _ => { + return Err( + "call ownership physical target has no exact checked source join".into(), + ); + } + } + for (index, argument) in source.arguments.iter().enumerate() { + if let Some(target) = target { + let parameter = target + .params + .get(index) + .ok_or("ownership physical function parameter is missing")?; + if argument.physical_access != access(parameter.mode) { + return Err("call ownership physical function access changed".into()); + } + if !source_parameter_matches(argument, args[index].ty, parameter.ty, types) { + return Err( + "call ownership converted operand differs from its physical parameter" + .into(), + ); + } + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + let Type::Function { params, .. } = types.resolve(callee.ty) else { + return Err("call ownership indirect callee is not a function".into()); + }; + let parameter_type = params + .get(index) + .copied() + .ok_or("call ownership indirect parameter is missing")?; + if !source_parameter_matches(argument, args[index].ty, parameter_type, types) { + return Err("call ownership indirect converted operand differs from its physical parameter".into()); + } + } + } + } + CallOwnership::Generated(generated) => { + if generated.operation != generated.certificate.operation + || _order != &generated.certificate.evaluation_order + || expression.ty != generated.certificate.result_type + { + return Err( + "generated invocation differs from its sealed operation/result/order".into(), + ); + } + if target.is_some_and(|target| target.params.len() != args.len()) { + return Err( + "generated invocation differs from its exact physical target arity".into(), + ); + } + match (&generated.operation, &expression.kind) { + (GeneratedOperation::Display { method }, ExpressionKind::Call { function, .. }) + if method == function => + { + let target = target.ok_or("display ownership method is absent")?; + if target.params.len() != 1 + || target.params[0].mode != crate::ParamMode::View + || target.return_type != TypeInterner::STRING + || target.identity.declaration.kind != crate::DeclarationKind::Method + { + return Err( + "generated display ownership has an invalid method shape".into() + ); + } + } + ( + GeneratedOperation::Equality { method }, + ExpressionKind::Call { function, .. }, + ) if method == function => { + let target = target.ok_or("equality ownership method is absent")?; + if target.params.len() != 2 + || target.params[0].ty != target.params[1].ty + || target + .params + .iter() + .any(|param| param.mode != crate::ParamMode::View) + || target.return_type != TypeInterner::BOOL + || target.identity.declaration.kind != crate::DeclarationKind::Method + { + return Err( + "generated equality ownership has an invalid method shape".into() + ); + } + } + ( + GeneratedOperation::InterfaceDispatch { + function: expected, + interface_type, + method_index, + }, + ExpressionKind::Call { function, .. }, + ) if expected == function => { + if !valid_type(types, *interface_type) { + return Err("generated dispatch owner is outside its interner".into()); + } + let Type::Interface(id) = types.resolve(*interface_type) else { + return Err("generated dispatch owner is not an interface".into()); + }; + let slot = types + .resolve_interface(*id) + .methods + .get(*method_index) + .ok_or("generated dispatch slot is out of bounds")?; + let target = target.ok_or("generated dispatch implementation is absent")?; + if target.identity.declaration.kind != crate::DeclarationKind::Method + || slot.params.len() != target.params.len() + || target.return_type != slot.return_type + || slot + .params + .iter() + .zip(&target.params) + .any(|((_, _, view), param)| { + *view != (param.mode == crate::ParamMode::View) + }) + { + return Err( + "generated dispatch implementation differs from its interface slot" + .into(), + ); + } + } + ( + GeneratedOperation::FunctionAdapter { + callee: expected, + source_type, + target_type, + }, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if !matches!(callee.kind, ExpressionKind::Local(local) if local == *expected) + || callee.ty != *source_type + || !valid_type(types, *source_type) + || !valid_type(types, *target_type) + || !matches!(types.resolve(*target_type), Type::Function { .. }) + { + return Err("generated callable adapter ownership types are invalid".into()); + } + } + ( + GeneratedOperation::ReflectedCallableAdapter { + callee: expected, + plan, + }, + ExpressionKind::IndirectCall { callee, .. }, + ) => { + if !plan.validate_types(types) { + return Err("reflected callable checked signature meaning changed".into()); + } + if !matches!(callee.kind, ExpressionKind::Local(local) if local == *expected) + || callee.ty != plan.source_type() + || expression.ty != plan.source_return() + { + return Err("reflected callable invocation differs from its actual checked signature".into()); + } + } + ( + GeneratedOperation::RefinementPredicate { + function: predicate, + }, + ExpressionKind::Call { function, .. }, + ) if predicate == function => { + let target = target.ok_or("generated refinement predicate is absent")?; + if target.params.len() != 1 + || target.params[0].mode != crate::ParamMode::Owned + || target.capture_count != 0 + || target.return_type != TypeInterner::BOOL + || target.identity.declaration.kind + != crate::DeclarationKind::RefinementPredicate + { + return Err( + "generated refinement ownership has an invalid predicate shape".into(), + ); + } + } + ( + GeneratedOperation::NativeSuite { function: expected }, + ExpressionKind::Call { function, .. }, + ) if expected == function => { + let target = target.ok_or("generated native suite target is absent")?; + if !matches!( + target.identity.declaration.kind, + crate::DeclarationKind::Verify | crate::DeclarationKind::Property + ) || target.return_type != TypeInterner::NOTHING + || target.capture_count != 0 + { + return Err( + "generated native suite has no exact checked verify/property target" + .into(), + ); + } + } + ( + GeneratedOperation::EvaluatedValue { + intrinsic: expected, + }, + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + .. + }, + ) if expected == intrinsic => { + validate_evaluated_value_shape( + *intrinsic, + type_arguments, + args, + expression.ty, + &generated.arguments, + types, + )?; + } + ( + GeneratedOperation::ReflectionMetadata { + intrinsic: expected, + }, + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + args, + .. + }, + ) if *expected == IntrinsicId::TypeName + && expected == intrinsic + && args.is_empty() + && type_arguments.len() == 1 + && valid_type(types, type_arguments[0]) + && expression.ty == TypeInterner::STRING => {} + _ => { + return Err( + "generated call ownership has no closed operation/target contract".into(), + ); + } + } + for (index, argument) in generated.arguments.iter().enumerate() { + if let Some(target) = target { + let parameter = target + .params + .get(index) + .ok_or("generated physical function parameter is missing")?; + if argument.parameter_type != parameter.ty + || argument.callee_access != access(parameter.mode) + { + return Err( + "generated ownership differs from its physical function signature" + .into(), + ); + } + if args[index].ty != parameter.ty + && !(matches!( + generated.operation, + GeneratedOperation::Display { .. } + | GeneratedOperation::Equality { .. } + ) && has_outer_secret(types, argument.actual_type) + && secret_taint_matches(types, args[index].ty, parameter.ty)) + { + return Err( + "generated converted operand differs from its physical parameter" + .into(), + ); + } + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + let Type::Function { + params, + view_params, + .. + } = types.resolve(callee.ty) + else { + return Err("generated indirect ownership callee is not a function".into()); + }; + if params.get(index) != Some(&argument.parameter_type) + || params.get(index) != Some(&args[index].ty) + || view_params.get(index).copied().map(|view| { + if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) != Some(argument.callee_access) + { + return Err( + "generated indirect ownership differs from its descriptor signature" + .into(), + ); + } + } + } + } + } + let declared_result = if let ExpressionKind::ResourceInvoke { hook, .. } = &expression.kind { + match types.resolve(hook.function_type()) { + Type::Function { return_type, .. } => Some(*return_type), + _ => None, + } + } else if let Some(target) = target { + Some(target.return_type) + } else if let ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + match types.resolve(callee.ty) { + Type::Function { return_type, .. } => Some(*return_type), + _ => None, + } + } else { + None + }; + if let Some(declared) = declared_result { + let taint = match ownership { + CallOwnership::Source(source) => expression.ty == source.certificate.result_type, + CallOwnership::Generated(generated) => { + matches!( + generated.operation, + GeneratedOperation::Display { .. } | GeneratedOperation::Equality { .. } + ) && generated + .arguments + .iter() + .any(|argument| has_outer_secret(types, argument.actual_type)) + } + }; + if expression.ty != declared + && !(taint && secret_taint_matches(types, expression.ty, declared)) + { + return Err( + "call ownership physical result has no exact checked return/secret join".into(), + ); + } + } + Ok(()) +} + +fn access(mode: crate::ParamMode) -> CheckedCalleeAccess { + if mode == crate::ParamMode::View { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } +} + +fn function_lookup( + functions: &[crate::Function], + id: FunctionId, +) -> Result<&crate::Function, String> { + functions + .get(id.index() as usize) + .filter(|function| function.id == id) + .ok_or_else(|| "ownership physical function is outside its program".into()) +} + +fn validate_function_shape( + shape: &CheckedInvocationShape, + target: &crate::Function, + types: &TypeInterner, +) -> Result<(), String> { + let CheckedInvocationShape::Function { signature_type } = shape else { + return Err("source function ownership has an intrinsic shape".into()); + }; + let Type::Function { + params, + view_params, + return_type, + } = types.resolve(*signature_type) + else { + return Err("source function ownership signature is invalid".into()); + }; + if target.capture_count != 0 + || params.len() != target.params.len() + || *return_type != target.return_type + || target + .params + .iter() + .zip(params) + .zip(view_params) + .any(|((parameter, ty), view)| { + parameter.ty != *ty || (parameter.mode == crate::ParamMode::View) != *view + }) + { + return Err( + "source function ownership signature differs from the exact HIR declaration".into(), + ); + } + Ok(()) +} + +fn source_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + types: &TypeInterner, +) -> Result<&'a Expression, String> { + if expression.ty != argument.actual_type + && matches!( + bridge, + CallBridge::JsonSource { + intrinsic: IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic, + .. + } + ) + && jett_typecheck::ownership::is_implicitly_copyable(types, argument.actual_type) + { + return json_scalar_original(expression, argument, types); + } + let mut value = expression; + while value.span != argument.source_span { + value = match &value.kind { + ExpressionKind::View(inner) => inner, + _ => return Err("call ownership actual lost its original typed occurrence".into()), + }; + } + while value.ty != argument.actual_type { + if value.ty == argument.witness.occurrence_type + && has_outer_secret(types, argument.parameter_type) + && secret_taint_matches(types, value.ty, argument.actual_type) + { + break; + } + value = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => { + return Err("call ownership current operand has no typed conversion from the checked actual".into()); + } + }; + } + if value.span != argument.source_span { + return Err("call ownership actual lost its original typed occurrence".into()); + } + Ok(value) +} + +fn json_scalar_original<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + types: &TypeInterner, +) -> Result<&'a Expression, String> { + let ExpressionKind::View(tree) = &expression.kind else { + return Err("source JSON scalar bridge requires a borrowed raw tree".into()); + }; + let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &tree.kind + else { + return Err("source JSON scalar bridge lost its raw tree construction".into()); + }; + let Type::Enum(id) = types.resolve(*enum_type) else { + return Err("source JSON scalar bridge tree is not an enum".into()); + }; + let definition = types.resolve_enum(*id); + let variant = definition + .variants + .get(variant.index() as usize) + .ok_or("source JSON scalar bridge variant is invalid")?; + if definition.name != "json.JsonTree" + || tree.ty != *enum_type + || expression.ty != *enum_type + || payloads.len() != 1 + || evaluation_order != &[0] + || payloads[0].ty != TypeInterner::STRING + { + return Err("source JSON scalar bridge raw shape is invalid".into()); + } + let original = match (&payloads[0].kind, types.resolve(argument.actual_type)) { + (ExpressionKind::Clone(original), Type::String) if variant.name == "string_value" => { + original.as_ref() + } + ( + ExpressionKind::StringInterpolation(parts), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64, + ) if variant.name == "number_value" => { + let [crate::StringSegment::Value(original)] = parts.as_slice() else { + return Err( + "source JSON numeric bridge must evaluate exactly one source value".into(), + ); + }; + original + } + _ => return Err("source JSON scalar bridge does not match the original primitive".into()), + }; + if original.ty != argument.actual_type || original.span != argument.source_span { + return Err("source JSON scalar bridge lost its exact original occurrence".into()); + } + Ok(original) +} + +/// Prove the original checked expression, its finite conversion and source place. +/// This accepts an original expression, never a lowered result slot by itself. +pub fn validate_source_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, +) -> Result<&'a Expression, String> { + let original = source_operand(expression, argument, bridge, types)?; + if let Some(backing) = validate_source_handled_operand(expression, argument, bridge, types)? + && !matches!(backing.kind, ExpressionKind::Handle { .. }) + { + return Err("call ownership original handled occurrence is no longer a Handle".into()); + } + validate_operand_origin(original, argument, locals, types)?; + Ok(original) +} + +fn source_view_backing(mut value: &Expression) -> &Expression { + while let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + value +} + +/// Rejoin a privately retained Handle occurrence through its original finite +/// conversion. MIR must independently prove a lowered Local's complete CFG; +/// this occurrence check never grants an owner or manufactures a Binding. +pub fn validate_source_handled_operand<'a>( + expression: &'a Expression, + argument: &ArgumentOwnership, + bridge: &CallBridge, + types: &TypeInterner, +) -> Result, String> { + let Some(result) = argument.witness.handled_result else { + return Ok(None); + }; + if argument.origin != CallerOrigin::OwnedExpression { + return Err("call ownership handled occurrence is not an original source producer".into()); + } + let original = source_operand(expression, argument, bridge, types)?; + let backing = source_view_backing(original); + if backing.ty != result.ty || backing.span != result.span { + return Err( + "call ownership handled backing differs from its original typed occurrence".into(), + ); + } + Ok(Some(backing)) +} + +fn validate_operand_origin( + value: &Expression, + argument: &ArgumentOwnership, + locals: &[OwnershipLocalInfo], + types: &TypeInterner, +) -> Result<(), String> { + // This is only the exact checked SOURCE projection proof. The runtime tree + // and any nested call's sealed promoted result certificate stay unchanged. + let raw_occurrence = if value.ty != argument.actual_type + && value.ty == argument.witness.occurrence_type + && has_outer_secret(types, argument.parameter_type) + && secret_taint_matches(types, value.ty, argument.actual_type) + { + let mut raw = value.clone(); + raw.ty = argument.actual_type; + let mut place = &mut raw; + while let ExpressionKind::View(inner) = &mut place.kind { + if inner.ty == argument.witness.occurrence_type { + inner.ty = argument.actual_type; + } + place = inner; + } + Some(raw) + } else { + None + }; + let value = raw_occurrence.as_ref().unwrap_or(value); + let mut leaf = value; + loop { + leaf = match &leaf.kind { + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) + | ExpressionKind::RefinementValidated(inner) + | ExpressionKind::InterfaceCoerce { value: inner, .. } + | ExpressionKind::FunctionAdapter { value: inner, .. } => inner, + _ => break, + }; + } + match &argument.origin { + CallerOrigin::Binding(fact) => { + let checked_state_narrowing = value.ty == leaf.ty + && leaf.ty == argument.actual_type + && leaf.ty == argument.witness.occurrence_type + && machine_state_parent_matches(types, leaf.ty, fact.ty); + if !matches!(leaf.kind, ExpressionKind::Local(local) if local == fact.local) + || (leaf.ty != fact.ty && !checked_state_narrowing) + { + return Err( + "call ownership binding does not match its actual typed operand".into(), + ); + } + } + CallerOrigin::BorrowedProjection { source } + | CallerOrigin::OwnedFieldCopy { parent: source } => { + if !matches!(leaf.kind, ExpressionKind::Field { .. }) + || immediate_source(leaf) != *source + { + return Err( + "call ownership projection does not match its immediate checked source".into(), + ); + } + if let CallerViewSource::Local(id) = source { + let parent = locals + .get(id.index() as usize) + .ok_or("call projection source is outside its function")?; + if let Some(root) = argument.witness.projection_root { + validate_flow_projection_operand(value, *id, parent.ty, root, types)?; + } else { + crate::validate_local_view_initializer(value, *id, parent.ty, value.ty, types) + .map_err(str::to_string)?; + } + } else { + let mut field = leaf; + while let ExpressionKind::Field { base, .. } = &field.kind { + crate::local_views::validate_field_projection(field, types) + .map_err(str::to_string)?; + field = base; + } + } + } + CallerOrigin::OwnedExpression => { + if matches!( + leaf.kind, + ExpressionKind::Local(_) | ExpressionKind::Field { .. } + ) { + return Err("call ownership cannot manufacture a producer acquisition from a binding or field".into()); + } + } + } + Ok(()) +} + +fn validate_flow_projection_operand( + value: &Expression, + source: LocalId, + storage_type: TypeId, + root: SourceProjectionRoot, + types: &TypeInterner, +) -> Result<(), String> { + if root.local != source + || root.storage_type != storage_type + || !flow_projection_root_matches(types, root.occurrence_type, storage_type) + { + return Err("call ownership flow projection changed its declared root storage".into()); + } + // Every existing field/wrapper/endpoint check remains strict. Only this + // privately certified source occurrence supplies its exact flowed root type. + crate::validate_local_view_initializer(value, source, root.occurrence_type, value.ty, types) + .map_err(str::to_string)?; + let mut terminal = value; + loop { + terminal = match &terminal.kind { + ExpressionKind::Field { base, .. } => base, + ExpressionKind::View(inner) + | ExpressionKind::Coarsen(inner) + | ExpressionKind::Declassify(inner) => inner, + ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => value, + ExpressionKind::Local(id) + if *id == root.local + && terminal.span == root.span + && terminal.ty == root.occurrence_type => + { + return Ok(()); + } + _ => { + return Err( + "call ownership flow projection changed its exact source occurrence".into(), + ); + } + }; + } +} + +fn flow_projection_root_matches(types: &TypeInterner, occurrence: TypeId, storage: TypeId) -> bool { + if !valid_type(types, occurrence) || !valid_type(types, storage) { + return false; + } + let (Type::MachineState { machine, state }, Type::Machine(parent)) = + (types.resolve(occurrence), types.resolve(storage)) + else { + return false; + }; + machine == parent && types.resolve_machine(*machine).state(*state).is_some() +} + +pub fn validate_generated_operand_tree( + value: &Expression, + argument: &GeneratedArgumentOwnership, + locals: &[OwnershipLocalInfo], +) -> Result<(), String> { + if !generated_tuple_matches(argument) { + return Err("generated operand differs from its sealed acquisition witness".into()); + } + let mut original = value; + // Physical View wrapping is separate from the original generated operand. + if !argument.witness.written_view + && argument.callee_access == CheckedCalleeAccess::View + && let ExpressionKind::View(inner) = &original.kind + && original.span == inner.span + && original.ty == inner.ty + { + original = inner; + } + // A canonical conversion keeps the physical occurrence header, but may + // allocate an adapter around the sealed raw parameter. Rejoin its backing + // only after the existing exact-actual-type conversion walk below. + if original.span != argument.witness.source_span { + return Err( + "generated operand differs from its original typed occurrence or backing".into(), + ); + } + while original.ty != argument.actual_type { + original = match &original.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => return Err("generated operand has no checked actual/conversion type".into()), + }; + } + if original.span != argument.witness.source_span + || original_view(original) != argument.witness.written_view + || immediate_source(original) != argument.witness.origin + { + return Err( + "generated operand differs from its original typed occurrence or backing".into(), + ); + } + argument.witness.validate_producer_shape(original)?; + match argument.acquisition { + GeneratedAcquisition::Borrow { source } if immediate_source(original) != source => { + return Err("generated borrow origin differs from its operand".into()); + } + GeneratedAcquisition::OwnedExpression => { + if original_view(original) { + return Err("generated owner cannot acquire an explicit borrowed operand".into()); + } + if let CallerViewSource::Local(id) = immediate_source(original) { + let local = locals + .get(id.index() as usize) + .ok_or("generated operand source is outside its function")?; + if local.view_source.is_some() + || local.is_view_parameter + || local.view_iteration.is_some() + { + return Err("generated owner cannot acquire a borrowed binding".into()); + } + } + } + _ => {} + } + Ok(()) +} + +fn metadata_operation(source: &SourceCallOwnership) -> Option { + match (&source.target, &source.bridge) { + (CallTarget::Intrinsic(id), CallBridge::Direct) + if matches!( + id, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeArg + ) => + { + Some(*id) + } + _ => None, + } +} + +fn canonical_metadata_enum(types: &TypeInterner, ty: TypeId) -> bool { + if !valid_type(types, ty) { + return false; + } + let Type::Enum(id) = types.resolve(ty) else { + return false; + }; + let definition = types.resolve_enum(*id); + let names: &[&str] = match definition.name.as_str() { + "TypeKind" => &[ + "primitive_type", + "alias_type", + "refinement_type", + "struct_type", + "bitfield_type", + "enum_type", + "list_type", + "set_type", + "map_type", + "optional_type", + "result_type", + "secret_type", + "function_type", + "machine_type", + "machine_state_type", + "unknown_type", + "resource_type", + ], + "TypePrimitive" => &[ + "int8_type", + "int16_type", + "int32_type", + "int64_type", + "uint8_type", + "uint16_type", + "uint32_type", + "uint64_type", + "float32_type", + "float64_type", + "string_type", + "bool_type", + "bytes_type", + "nothing_type", + "type_construction_type", + "unknown_type", + ], + _ => return false, + }; + definition.variants.len() == names.len() + && definition + .variants + .iter() + .zip(names) + .enumerate() + .all(|(index, (variant, name))| { + variant.name == *name + && variant.fields.is_empty() + && variant.discriminant == index as i64 + }) +} + +fn canonical_metadata_type(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if !valid_type(types, ty) || !types.nominal_type_arguments(ty).is_empty() { + return false; + } + if !seen.insert(ty) { + return true; + } + match types.resolve(ty) { + Type::Int64 | Type::String | Type::Bool => true, + Type::Optional(inner) | Type::List(inner) => visit(types, *inner, seen), + Type::Enum(_) => canonical_metadata_enum(types, ty), + Type::Struct(id) => { + let definition = types.resolve_struct(*id); + if !definition.methods.is_empty() { + return false; + } + let fields = &definition.fields; + let names: &[&str] = match definition.name.as_str() { + "TypeInfo" => &[ + "type_name", + "kind", + "kind_tag", + "primitive_tag", + "has_secret", + "args", + ], + "TypeField" => &[ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ], + "TypeVariant" => &[ + "index", + "owner_type", + "name", + "discriminant", + "has_secret", + "fields", + ], + "TypeMachineState" => &["index", "owner_type", "name", "has_secret", "fields"], + _ => return false, + }; + if !fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(names.iter().copied()) + { + return false; + } + let field = |index: usize| fields[index].1; + let named = |candidate: TypeId, name: &str| { + valid_type(types, candidate) + && match types.resolve(candidate) { + Type::Struct(id) => types.resolve_struct(*id).name == name, + Type::Enum(id) => types.resolve_enum(*id).name == name, + _ => false, + } + }; + let optional = |candidate: TypeId, payload: TypeId| { + valid_type(types, candidate) + && matches!(types.resolve(candidate), Type::Optional(inner) if *inner == payload) + }; + let list_named = |candidate: TypeId, name: &str| { + valid_type(types, candidate) + && matches!(types.resolve(candidate), Type::List(inner) if named(*inner, name)) + }; + let shape = match definition.name.as_str() { + "TypeInfo" => { + field(0) == TypeInterner::STRING + && field(1) == TypeInterner::STRING + && named(field(2), "TypeKind") + && valid_type(types, field(3)) + && matches!(types.resolve(field(3)), Type::Optional(inner) if named(*inner, "TypePrimitive")) + && field(4) == TypeInterner::BOOL + && valid_type(types, field(5)) + && matches!(types.resolve(field(5)), Type::List(inner) if *inner == ty) + } + "TypeField" => { + field(0) == TypeInterner::INT64 + && [1, 3, 4, 5, 7] + .into_iter() + .all(|index| field(index) == TypeInterner::STRING) + && optional(field(2), TypeInterner::STRING) + && named(field(6), "TypeKind") + && field(8) == TypeInterner::BOOL + && named(field(9), "TypeInfo") + } + "TypeVariant" => { + field(0) == TypeInterner::INT64 + && field(1) == TypeInterner::STRING + && field(2) == TypeInterner::STRING + && field(3) == TypeInterner::INT64 + && field(4) == TypeInterner::BOOL + && list_named(field(5), "TypeField") + } + "TypeMachineState" => { + field(0) == TypeInterner::INT64 + && field(1) == TypeInterner::STRING + && field(2) == TypeInterner::STRING + && field(3) == TypeInterner::BOOL + && list_named(field(4), "TypeField") + } + _ => false, + }; + shape && fields.iter().all(|(_, child)| visit(types, *child, seen)) + } + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +fn metadata_root_matches(source: &SourceCallOwnership, ty: TypeId, types: &TypeInterner) -> bool { + let Some(operation) = metadata_operation(source) else { + return false; + }; + if !canonical_metadata_type(types, ty) { + return false; + } + let Type::Struct(id) = types.resolve(ty) else { + return false; + }; + let expected = match operation { + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue => "TypeField", + IntrinsicId::TypeVariantValue => "TypeVariant", + IntrinsicId::TypeMachineStateValue => "TypeMachineState", + IntrinsicId::TypeArg => "TypeInfo", + _ => return false, + }; + types.resolve_struct(*id).name == expected + && match (&source.shape, operation) { + ( + CheckedInvocationShape::Intrinsic { operands, .. }, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue, + ) => operands.get(1).is_some_and(|role| role.ty() == ty), + (CheckedInvocationShape::Intrinsic { result_type, .. }, _) => *result_type == ty, + _ => false, + } +} + +/// Compiler metadata is a closed constant construction, never a borrowed or +/// cloned source payload. MIR must reconstruct a lowered producer before use. +pub fn validate_compiler_metadata_operand( + value: &Expression, + source: &SourceCallOwnership, + types: &TypeInterner, +) -> Result<(), String> { + fn constant(value: &Expression, types: &TypeInterner) -> bool { + if !canonical_metadata_type(types, value.ty) { + return false; + } + match (&value.kind, types.resolve(value.ty)) { + (ExpressionKind::Int(_), Type::Int64) + | (ExpressionKind::String(_), Type::String) + | (ExpressionKind::Bool(_), Type::Bool) => true, + (ExpressionKind::OptionalNone, Type::Optional(_)) => true, + (ExpressionKind::OptionalSome(value), Type::Optional(inner)) => { + value.ty == *inner && constant(value, types) + } + (ExpressionKind::ListConstruct { elements }, Type::List(inner)) => elements + .iter() + .all(|value| value.ty == *inner && constant(value, types)), + ( + ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + }, + Type::Enum(id), + ) => { + *enum_type == value.ty + && payloads.is_empty() + && evaluation_order.is_empty() + && types + .resolve_enum(*id) + .variants + .get(variant.index() as usize) + .is_some_and(|variant| variant.fields.is_empty()) + } + ( + ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + }, + Type::Struct(id), + ) => { + let definition = types.resolve_struct(*id); + *struct_type == value.ty + && !*validates_refinements + && refinement_predicates.is_empty() + && evaluation_order.iter().copied().eq(0..fields.len()) + && fields.len() == definition.fields.len() + && fields + .iter() + .zip(&definition.fields) + .all(|(value, (_, ty))| value.ty == *ty && constant(value, types)) + } + _ => false, + } + } + if metadata_root_matches(source, value.ty, types) && constant(value, types) { + Ok(()) + } else { + Err( + "call ownership compiler metadata tail has no canonical owned construction proof" + .into(), + ) + } +} + +fn secret_taint_matches(types: &TypeInterner, mut actual: TypeId, declared: TypeId) -> bool { + if !valid_type(types, actual) || !valid_type(types, declared) { + return false; + } + for _ in 0..types.len() { + if actual == declared { + return true; + } + let Type::Secret(inner) = types.resolve(actual) else { + return false; + }; + actual = *inner; + } + false +} + +fn has_outer_secret(types: &TypeInterner, ty: TypeId) -> bool { + valid_type(types, ty) && matches!(types.resolve(ty), Type::Secret(_)) +} + +/// Locate only the first Secret boundary behind nominal refinement bases. +/// This is metadata for an exact checked actual, not refinement coarsening. +fn nominal_secret_payload(types: &TypeInterner, mut actual: TypeId) -> Option { + if !valid_type(types, actual) || !matches!(types.resolve(actual), Type::Refinement { .. }) { + return None; + } + for _ in 0..types.len() { + if !valid_type(types, actual) { + return None; + } + match types.resolve(actual) { + Type::Refinement { base, .. } => actual = *base, + Type::Secret(inner) => return valid_type(types, *inner).then_some(*inner), + _ => return None, + } + } + None +} + +// The checker admits only this one-way nominal state erasure. No state is +// recovered or substituted, and the checked actual occurrence stays exact. +fn machine_state_parent_matches(types: &TypeInterner, actual: TypeId, declared: TypeId) -> bool { + if !valid_type(types, actual) || !valid_type(types, declared) { + return false; + } + matches!( + (types.resolve(actual), types.resolve(declared)), + (Type::MachineState { machine, .. }, Type::Machine(parent)) if machine == parent + ) +} + +fn source_parameter_matches( + argument: &ArgumentOwnership, + physical: TypeId, + declared: TypeId, + types: &TypeInterner, +) -> bool { + physical == declared + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && machine_state_parent_matches(types, physical, declared)) + || (has_outer_secret(types, argument.actual_type) + && secret_taint_matches(types, physical, declared)) + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && nominal_secret_payload(types, physical) == Some(declared)) + || (physical == argument.actual_type + && physical == argument.witness.occurrence_type + && declared == argument.parameter_type + && has_outer_secret(types, declared) + && secret_taint_matches(types, declared, physical)) +} + +fn evaluated_key_type(types: &TypeInterner, mut ty: TypeId) -> bool { + let mut seen = std::collections::HashSet::new(); + while valid_type(types, ty) && seen.insert(ty) { + match types.resolve(ty) { + Type::Refinement { base, .. } => ty = *base, + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::String + | Type::Bool => return true, + _ => return false, + } + } + false +} + +fn validate_evaluated_value_shape( + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + args: &[Expression], + result: TypeId, + arguments: &[GeneratedArgumentOwnership], + types: &TypeInterner, +) -> Result<(), String> { + if type_arguments.iter().any(|&ty| !valid_type(types, ty)) || !valid_type(types, result) { + return Err("generated evaluated value has an unknown type argument or result".into()); + } + let construction_result = || { + matches!(types.resolve(result), Type::Result(ok, error) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *error == TypeInterner::STRING) + }; + let metadata = |ty: TypeId, name: &str| { + valid_type(types, ty) + && canonical_metadata_type(types, ty) + && matches!(types.resolve(ty), Type::Struct(id) if types.resolve_struct(*id).name == name) + }; + let construction_owner = |ty: TypeId| { + matches!( + types.resolve(ty), + Type::Struct(_) + | Type::Bitfield(_) + | Type::Enum(_) + | Type::Machine(_) + | Type::MachineState { .. } + ) + }; + let shape = match intrinsic { + IntrinsicId::SetNew => { + type_arguments.len() == 1 + && args.is_empty() + && evaluated_key_type(types, type_arguments[0]) + && matches!(types.resolve(result), Type::Set(inner) if *inner == type_arguments[0]) + } + IntrinsicId::SetAdd => { + type_arguments.len() == 1 + && args.len() == 2 + && evaluated_key_type(types, type_arguments[0]) + && args[0].ty == result + && args[1].ty == type_arguments[0] + && matches!(types.resolve(result), Type::Set(inner) if *inner == type_arguments[0]) + } + IntrinsicId::BytesNew => { + type_arguments.is_empty() && args.is_empty() && result == TypeInterner::BYTES + } + IntrinsicId::BytesFromHex => { + type_arguments.is_empty() + && args.len() == 1 + && args[0].ty == TypeInterner::STRING + && matches!(types.resolve(result), Type::Result(ok, error) if *ok == TypeInterner::BYTES && *error == TypeInterner::STRING) + } + IntrinsicId::TypeConstructStart => { + type_arguments.len() == 1 + && args.is_empty() + && construction_owner(type_arguments[0]) + && result == TypeInterner::TYPE_CONSTRUCTION + } + IntrinsicId::TypeConstructVariantStart => { + type_arguments.len() == 1 + && args.len() == 1 + && matches!(types.resolve(type_arguments[0]), Type::Enum(_)) + && metadata(args[0].ty, "TypeVariant") + && construction_result() + } + IntrinsicId::TypeConstructMachineStart => { + type_arguments.len() == 1 + && args.len() == 1 + && matches!( + types.resolve(type_arguments[0]), + Type::Machine(_) | Type::MachineState { .. } + ) + && metadata(args[0].ty, "TypeMachineState") + && construction_result() + } + IntrinsicId::TypeConstructPut => { + type_arguments.len() == 2 + && args.len() == 3 + && construction_owner(type_arguments[0]) + && args[0].ty == TypeInterner::TYPE_CONSTRUCTION + && metadata(args[1].ty, "TypeField") + && args[2].ty == type_arguments[1] + && construction_result() + } + _ => false, + }; + if !shape { + return Err("generated evaluated value has no closed intrinsic/type/result shape".into()); + } + for (index, argument) in arguments.iter().enumerate() { + let access = jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()) + .ok_or("generated evaluated value operand has no closed access")?; + if argument.callee_access != access || argument.parameter_type != args[index].ty { + return Err( + "generated evaluated value ownership differs from its closed physical operand" + .into(), + ); + } + } + Ok(()) +} diff --git a/crates/jett_hir/src/call_ownership_tests.rs b/crates/jett_hir/src/call_ownership_tests.rs new file mode 100644 index 00000000..b135d714 --- /dev/null +++ b/crates/jett_hir/src/call_ownership_tests.rs @@ -0,0 +1,3371 @@ +//! Source-derived caller-packet corruption controls. Not executed in this draft. +use super::*; +use jett_diagnostics::Severity; + +const NAMED: &str = r#"function observe(view first: list[int64], view second: list[int64]) returns int64: + return 7 +function alternate(view first: list[int64], view second: list[int64]) returns int64: + return 9 +function exercise(left: list[int64], right: list[int64], unrelated: list[string]) returns int64: + return observe(second: right, first: left) +"#; + +fn checked_source(source: &str) -> (Program, CheckResult) { + checked_source_with_suites(source, false) +} + +fn checked_source_with_suites(source: &str, include_suites: bool) -> (Program, CheckResult) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve: {:?}", + resolved.diagnostics, + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "check: {:?}", + checked.diagnostics, + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let lowered = if include_suites { + lower_with_test_bodies(&parsed.module, &resolved, &checked, &origins) + } else { + lower(&parsed.module, &resolved, &checked, &origins) + }; + let program = lowered.expect("source lowers"); + validate_program_call_ownership(&program, &checked.interner) + .expect("original packet validates"); + (program, checked) +} + +fn exercise_index(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == "exercise") + .expect("source exercise") +} + +fn returned_call(function: &Function) -> &Expression { + function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("returned source call") +} + +fn returned_call_mut(function: &mut Function) -> &mut Expression { + function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("returned source call") +} + +fn source_packet_mut(expression: &mut Expression) -> &mut SourceCallOwnership { + let packet = match &mut expression.kind { + ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } => ownership, + _ => panic!("source invocation"), + }; + let CallOwnership::Source(source) = packet else { + panic!("checked source packet"); + }; + source +} + +fn local_id(function: &Function, name: &str) -> LocalId { + function + .locals + .iter() + .find(|local| local.name == name) + .expect("source local") + .id +} + +fn rejects(program: &Program, checked: &CheckResult, case: &str) { + let errors = validate_program_call_ownership(program, &checked.interner) + .expect_err("corrupted checked invocation must be refused"); + assert!( + errors.iter().any(|error| error.message.contains("call ownership") + || error.message.contains("caller") || error.message.contains("invocation") + || (case == "source-backed tail" && matches!(error.message.as_str(), + "generated owner cannot acquire a borrowed binding" + | "generated operand differs from its original typed occurrence or backing"))), + "{case}: {errors:?}", + ); +} + +#[test] +fn caller_packets_preserve_named_order_and_borrowed_source_authority() { + let (program, checked) = checked_source(NAMED); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + evaluation_order, + ownership, + .. + } = &returned_call(function).kind + else { + panic!("direct named invocation"); + }; + let CallOwnership::Source(source) = ownership else { + panic!("source packet"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!( + source + .arguments + .iter() + .map(|argument| argument.source_index) + .collect::>(), + [1, 0] + ); + assert!(source.arguments.iter().all(|argument| { + argument.effect == jett_typecheck::CheckedCallerEffect::RelinquishOwned + && argument.syntax == jett_typecheck::CheckedCallerSyntax::Bare + && argument.staging == ArgumentStaging::Original + })); + assert!( + matches!(args[0].kind, ExpressionKind::Local(local) if local == local_id(function, "left")) + ); + assert!( + matches!(args[1].kind, ExpressionKind::Local(local) if local == local_id(function, "right")) + ); + assert!(matches!( + checked.interner.resolve(source.arguments[0].actual_type), + Type::List(_) + )); + + let source = "function observe(view value: list[int64]) returns int64:\n return 7\nfunction exercise(view value: list[int64]) returns int64:\n return observe(view value)\n"; + let (program, _) = checked_source(source); + let expression = returned_call(&program.functions[exercise_index(&program)]); + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &expression.kind + else { + panic!("retained source invocation"); + }; + let argument = &source.arguments[0]; + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert!(matches!( + argument.origin, + CallerOrigin::Binding(CallerBindingFact { + mode: CallerBindingMode::View { + source: CallerViewSource::Other + }, + .. + }) + )); +} + +#[test] +fn caller_packets_reject_count_order_and_source_fact_corruption() { + for case in [ + "count", + "order", + "source index", + "effect", + "syntax", + "context", + "origin", + ] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "order" { + let ExpressionKind::Call { + evaluation_order, .. + } = &mut expression.kind + else { + unreachable!(); + }; + *evaluation_order = vec![0, 1]; + } else { + let packet = source_packet_mut(expression); + match case { + "count" => { + packet.arguments.pop(); + } + "source index" => packet.arguments[0].source_index = 0, + "effect" => { + packet.arguments[0].effect = jett_typecheck::CheckedCallerEffect::RetainBorrow + } + "syntax" => { + packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::WrittenView + } + "context" => packet.context = jett_typecheck::CheckedOwnershipContext::Verify, + "origin" => packet.arguments[0].origin = CallerOrigin::OwnedExpression, + _ => unreachable!(), + } + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_bind_the_executed_target_and_complete_signature() { + for case in ["physical target", "packet target", "signature"] { + let (mut program, mut checked) = checked_source(NAMED); + let alternate = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .unwrap() + .id; + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "physical target" { + let ExpressionKind::Call { function, .. } = &mut expression.kind else { + unreachable!(); + }; + *function = alternate; + } else { + let packet = source_packet_mut(expression); + if case == "packet target" { + packet.target = CallTarget::Function(alternate); + } else { + let ty = packet.arguments[0].parameter_type; + packet.shape = jett_typecheck::CheckedInvocationShape::Function { + signature_type: checked.interner.intern(Type::Function { + params: vec![ty, ty], + view_params: vec![true, true], + return_type: TypeInterner::BOOL, + }), + }; + } + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_require_the_original_operand_and_exact_local_metadata() { + for case in [ + "different same-type owner", + "different type", + "local metadata", + "foreign local", + ] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let function = &mut program.functions[index]; + let left = local_id(function, "left"); + let right = local_id(function, "right"); + let unrelated = local_id(function, "unrelated"); + if case == "local metadata" { + let replacement = function.locals[unrelated.index() as usize].ty; + function.locals[left.index() as usize].ty = replacement; + } else { + let expression = returned_call_mut(function); + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + unreachable!(); + }; + args[0].kind = ExpressionKind::Local(match case { + "different same-type owner" => right, + "different type" => unrelated, + "foreign local" => LocalId::new(u32::MAX), + _ => unreachable!(), + }); + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_cannot_import_mir_loan_staging_into_original_hir() { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let owner = local_id(&program.functions[index], "left"); + let loan = local_id(&program.functions[index], "right"); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { ownership, .. } = &mut expression.kind else { + unreachable!(); + }; + ownership + .stage_parameter(0, Some(owner), loan) + .expect("typed MIR stage mutation"); + rejects(&program, &checked, "MIR stage in original HIR"); +} + +#[test] +fn caller_packets_preserve_safe_secret_promotion_but_reject_forged_operands() { + let source = "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(value)\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("source promoted operand"); + }; + assert_ne!( + packet.arguments[0].actual_type, + packet.arguments[0].parameter_type + ); + assert!(matches!( + checked.interner.resolve(packet.arguments[0].parameter_type), + Type::Secret(_) + )); + args[0].ty = TypeInterner::INT64; + rejects(&program, &checked, "invented promoted operand type"); +} + +const REFLECTED_METADATA_SOURCE: &str = r#"namespace app +struct Record: + value: int64 +function exercise(view value: Record, view field: TypeField) returns int64: + return type.field_value[Record, int64](view value, view field) +"#; + +#[test] +fn caller_packets_preserve_canonical_owned_metadata_and_reject_tail_corruption() { + let (program, checked) = checked_source(REFLECTED_METADATA_SOURCE); + let expression = returned_call(&program.functions[exercise_index(&program)]); + let ExpressionKind::Intrinsic { + args, + ownership: CallOwnership::Source(source), + .. + } = &expression.kind + else { + panic!("reflected source intrinsic"); + }; + assert_eq!(source.arguments.len(), 2); + assert_eq!(source.generated_operands.len(), 1); + assert_eq!(args.len(), 3); + assert_eq!( + source.generated_operands[0].acquisition, + GeneratedAcquisition::OwnedExpression + ); + assert!(!jett_typecheck::ownership::is_implicitly_copyable( + &checked.interner, + args[2].ty + )); + validate_compiler_metadata_operand(&args[2], source, &checked.interner) + .expect("canonical compiler metadata"); + + for case in [ + "tail acquisition", + "source-backed tail", + "metadata schema", + "tail evaluation order", + ] { + let (mut program, mut checked) = checked_source(REFLECTED_METADATA_SOURCE); + let index = exercise_index(&program); + let field = local_id(&program.functions[index], "field"); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Intrinsic { + args, + ownership: CallOwnership::Source(source), + evaluation_order, + .. + } = &mut expression.kind + else { + panic!("reflected source intrinsic"); + }; + match case { + "tail acquisition" => { + source.generated_operands[0].acquisition = GeneratedAcquisition::Copy + } + "source-backed tail" => args[2].kind = ExpressionKind::Local(field), + "metadata schema" => { + let Type::Struct(id) = *checked.interner.resolve(args[2].ty) else { + panic!("metadata struct"); + }; + let mut definition = checked.interner.resolve_struct(id).clone(); + definition.fields[0].1 = TypeInterner::STRING; + checked.interner.update_struct(id, definition); + } + "tail evaluation order" => *evaluation_order = vec![2, 0, 1], + _ => unreachable!(), + } + rejects(&program, &checked, case); + } +} + +#[test] +fn caller_packets_seal_checked_results_and_exact_physical_operands() { + for case in ["source result", "converted operand"] { + let (mut program, checked) = checked_source(NAMED); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + if case == "source result" { + expression.ty = TypeInterner::BOOL; + } else { + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + panic!("source call"); + }; + args[0].ty = TypeInterner::STRING; + } + rejects(&program, &checked, case); + } + + let source = "function exercise(callback: function(view list[int64]) returns int64, value: list[int64]) returns int64:\n return callback(value)\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::IndirectCall { args, .. } = &mut expression.kind else { + panic!("source indirect call"); + }; + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "indirect converted operand"); +} + +#[test] +fn caller_packets_preserve_checked_contextual_secret_producer_results() { + let source = "function produce() returns list[int64]:\n return list(1)\nfunction exercise() returns secret[list[int64]]:\n return produce()\n"; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + assert!( + matches!(checked.interner.resolve(expression.ty), Type::Secret(inner) + if matches!(checked.interner.resolve(*inner), Type::List(_))) + ); + expression.ty = TypeInterner::STRING; + rejects(&program, &checked, "forged contextual producer result"); +} + +#[test] +fn caller_packets_separate_raw_and_qualified_source_occurrences() { + let sources = [ + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(value)\n", + "struct Packet:\n items: list[int64]\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: Packet) returns int64:\n return observe(value.items)\n", + "struct Packet:\n items: list[int64]\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: Packet) returns int64:\n return observe(view value.items)\n", + "function produce() returns list[int64]:\n return list(1)\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise() returns int64:\n return observe(produce())\n", + "function produce() returns list[int64]:\n return list(1)\nfunction observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise() returns int64:\n return observe(view produce())\n", + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return value into observe()\n", + "function observe(view value: secret[list[int64]]) returns int64:\n return 7\nfunction exercise(value: list[int64]) returns int64:\n return observe(view value)\n", + ]; + for source in sources { + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("qualified source call"); + }; + let argument = &packet.arguments[0]; + assert!(matches!( + checked.interner.resolve(argument.actual_type), + Type::List(_) + )); + assert!( + matches!(checked.interner.resolve(argument.parameter_type), Type::Secret(inner) + if *inner == argument.actual_type) + ); + assert_eq!( + checked.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!( + argument.source_witness().occurrence_type() == argument.actual_type + || matches!(checked.interner.resolve(argument.source_witness().occurrence_type()), Type::Secret(inner) + if *inner == argument.actual_type) + ); + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "forged qualified occurrence"); + } +} + +#[test] +fn caller_packets_keep_pipeline_input_separate_from_field_callee_occurrences() { + let sources = [ + "function increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4 into holder.callback\n", + "function increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4 into holder.callback into holder.callback\n", + "function recover(value: int64) returns result[int64, string]:\n return ok(value)\nfunction increment(value: int64) returns int64:\n return value + 1\nstruct Holder:\n callback: function(int64) returns int64\nfunction exercise() returns int64:\n Holder holder = Holder(callback: increment)\n return 4\n into recover() handle error:\n default 0\n into holder.callback\n", + ]; + for source in sources { + let (mut program, checked) = checked_source(source); + assert!(!checked.pipeline_step_input_types.is_empty()); + assert!( + checked + .pipeline_step_input_types + .values() + .all(|ty| *ty == TypeInterner::INT64) + ); + assert!( + checked.pipeline_step_input_types.keys().any(|span| { + checked.type_map.get(span).is_some_and(|ty| { + matches!(checked.interner.resolve(*ty), Type::Function { .. }) + }) + }), + "field target shares a step span with its virtual input" + ); + let index = exercise_index(&program); + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("field pipeline remains an indirect invocation"); + }; + assert_eq!(packet.arguments[0].actual_type, TypeInterner::INT64); + assert_eq!( + packet.arguments[0].source_witness().occurrence_type(), + TypeInterner::INT64 + ); + args[0].ty = TypeInterner::STRING; + rejects(&program, &checked, "forged pipeline input operand"); + } +} + +#[test] +fn generated_native_suites_seal_target_result_and_zero_argument_order() { + let source = "function ordinary() returns nothing:\n return nothing\nverify accepted:\n assert true\n"; + let (program, checked) = checked_source_with_suites(source, true); + let suite = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .expect("checked verify"); + let ordinary = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Function) + .expect("ordinary function"); + let ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { function: suite.id }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &checked.interner, + ) + .expect("compiler suite packet"); + let original = Expression { + kind: ExpressionKind::Call { + ownership, + function: suite.id, + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::NOTHING, + span: suite.span, + }; + validate_hir_invocation( + &program.functions, + &[], + &original, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact checked suite"); + for case in ["target", "operation", "result"] { + let mut changed = original.clone(); + let ExpressionKind::Call { + ownership: CallOwnership::Generated(packet), + function, + .. + } = &mut changed.kind + else { + unreachable!() + }; + match case { + "target" => *function = ordinary.id, + "operation" => { + packet.operation = GeneratedOperation::NativeSuite { + function: ordinary.id, + } + } + "result" => changed.ty = TypeInterner::BOOL, + _ => unreachable!(), + } + assert!( + validate_hir_invocation( + &program.functions, + &[], + &changed, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "{case}" + ); + } + let mut ordinary_call = original.clone(); + let ExpressionKind::Call { + ownership, + function, + .. + } = &mut ordinary_call.kind + else { + unreachable!() + }; + *function = ordinary.id; + *ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: ordinary.id, + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &checked.interner, + ) + .unwrap(); + assert!( + validate_hir_invocation( + &program.functions, + &[], + &ordinary_call, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); +} + +#[test] +fn generated_evaluated_values_validate_closed_intrinsic_shapes() { + let mut types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let set = types.intern(Type::Set(TypeInterner::INT64)); + let bytes_result = types.intern(Type::Result(TypeInterner::BYTES, TypeInterner::STRING)); + let make = |intrinsic, + type_arguments, + args: Vec, + result, + access: Vec| { + let order = (0..args.len()).collect::>(); + let parameters = args.iter().map(|value| value.ty).collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meters, + &access, + result, + &order, + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args, + evaluation_order: order, + }, + ty: result, + span, + } + }; + let empty_set = make( + IntrinsicId::SetNew, + vec![TypeInterner::INT64], + vec![], + set, + vec![], + ); + let inserted = make( + IntrinsicId::SetAdd, + vec![TypeInterner::INT64], + vec![ + empty_set.clone(), + Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }, + ], + set, + vec![jett_typecheck::CheckedCalleeAccess::Owned; 2], + ); + let empty_bytes = make( + IntrinsicId::BytesNew, + vec![], + vec![], + TypeInterner::BYTES, + vec![], + ); + let parsed_bytes = make( + IntrinsicId::BytesFromHex, + vec![], + vec![Expression { + kind: ExpressionKind::String("00".into()), + ty: TypeInterner::STRING, + span, + }], + bytes_result, + vec![jett_typecheck::CheckedCalleeAccess::Owned], + ); + for original in [empty_set, inserted, empty_bytes, parsed_bytes] { + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("closed evaluated value"); + let mut changed = original.clone(); + let ExpressionKind::Intrinsic { type_arguments, .. } = &mut changed.kind else { + unreachable!() + }; + type_arguments.push(TypeInterner::STRING); + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + } +} + +#[test] +fn generated_stages_keep_private_acquisition_and_snapshot_authority() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let span = Span::new(FileId::new(0), 0, 1); + let borrowed = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: list, + span, + })), + ty: list, + span, + }; + let mut packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[borrowed], + &[list], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + assert!( + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OwnedProducer { + owner: LocalId::new(1), + loan: LocalId::new(2) + } + ) + .is_err() + ); + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OrdinarySnapshot { + owner: LocalId::new(1), + loan: LocalId::new(2), + }, + ) + .expect("ordinary data snapshot"); + let local = |id: LocalId| { + Some(OwnershipLocalInfo { + ty: list, + mutable: false, + span, + view_source: (id == LocalId::new(2)).then_some(LocalId::new(1)), + is_view_parameter: false, + view_iteration: None, + match_binding: None, + }) + }; + validate_operand_ownership(&packet, &[list], &[0], &types, local, false) + .expect("typed staged shape"); + assert!(validate_operand_ownership(&packet, &[list], &[0], &types, local, true).is_err()); + assert!( + packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::Existing { + loan: Some(LocalId::new(2)) + } + ) + .is_err(), + "a second transition cannot replace a sealed storage association" + ); + let CallOwnership::Generated(generated) = &mut packet else { + unreachable!() + }; + generated.arguments[0].staging = GeneratedArgumentStaging::OwnedProducer { + owner: LocalId::new(1), + loan: LocalId::new(2), + }; + assert!( + validate_operand_ownership(&packet, &[list], &[0], &types, local, false).is_err(), + "public stage cannot relabel a snapshot as a producer" + ); + let mut appended = packet.clone(); + let CallOwnership::Generated(generated) = &mut appended else { + unreachable!() + }; + generated.arguments.push(generated.arguments[0].clone()); + assert!( + appended + .stage_generated_parameter( + 1, + GeneratedArgumentStaging::Existing { + loan: Some(LocalId::new(2)) + } + ) + .is_err(), + "public extra argument is bounded before certificate lookup" + ); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::NOTHING, + }); + let descriptor = Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: signature, + span, + }; + let mut descriptor_packet = CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(0), + source_type: signature, + target_type: signature, + }, + &[descriptor], + &[signature], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::NOTHING, + &[0], + &types, + ) + .unwrap(); + assert!( + descriptor_packet + .stage_generated_parameter( + 0, + GeneratedArgumentStaging::OrdinarySnapshot { + owner: LocalId::new(1), + loan: LocalId::new(2) + } + ) + .is_err(), + "signature cannot prove captures" + ); +} + +fn generated_metadata_constant(types: &TypeInterner, ty: TypeId, span: Span) -> Expression { + let kind = match types.resolve(ty) { + Type::Int64 => ExpressionKind::Int(0), + Type::String => ExpressionKind::String(String::new()), + Type::Bool => ExpressionKind::Bool(false), + Type::Optional(_) => ExpressionKind::OptionalNone, + Type::List(_) => ExpressionKind::ListConstruct { elements: vec![] }, + Type::Enum(_) => ExpressionKind::EnumConstruct { + enum_type: ty, + variant: VariantId::new(0), + payloads: vec![], + evaluation_order: vec![], + }, + Type::Struct(id) => { + let fields = types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| generated_metadata_constant(types, *ty, span)) + .collect::>(); + ExpressionKind::StructConstruct { + struct_type: ty, + evaluation_order: (0..fields.len()).collect(), + fields, + validates_refinements: false, + refinement_predicates: vec![], + } + } + other => panic!("unexpected compiler metadata field: {other:?}"), + }; + Expression { kind, ty, span } +} + +#[test] +fn generated_evaluated_builders_keep_canonical_metadata_and_payload_shapes() { + let source = "struct Record:\n value: int64\nenum Choice:\n chosen(value: int64)\nmachine Session:\n states:\n ready(value: int64)\n empty\n transitions:\n ready to empty\nfunction exercise() returns nothing:\n return nothing\n"; + let (_, checked) = checked_source(source); + let mut types = checked.interner; + let named = |types: &TypeInterner, name: &str| { + types + .type_ids() + .find(|&ty| types.type_name(ty) == name) + .expect("checked named type") + }; + let record = named(&types, "Record"); + let choice = named(&types, "Choice"); + let session = named(&types, "Session"); + let field = named(&types, "TypeField"); + let variant = named(&types, "TypeVariant"); + let state = named(&types, "TypeMachineState"); + let result = types.intern(Type::Result( + TypeInterner::TYPE_CONSTRUCTION, + TypeInterner::STRING, + )); + let span = Span::new(FileId::new(0), 0, 1); + let make = |intrinsic, type_arguments, args: Vec, result| { + let parameters = args.iter().map(|value| value.ty).collect::>(); + let order = (0..args.len()).collect::>(); + let access = (0..args.len()) + .map(|index| { + jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()).unwrap() + }) + .collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meters, + &access, + result, + &order, + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args, + evaluation_order: order, + }, + ty: result, + span, + } + }; + let started = make( + IntrinsicId::TypeConstructStart, + vec![record], + vec![], + TypeInterner::TYPE_CONSTRUCTION, + ); + let variant_started = make( + IntrinsicId::TypeConstructVariantStart, + vec![choice], + vec![generated_metadata_constant(&types, variant, span)], + result, + ); + let machine_started = make( + IntrinsicId::TypeConstructMachineStart, + vec![session], + vec![generated_metadata_constant(&types, state, span)], + result, + ); + let put = make( + IntrinsicId::TypeConstructPut, + vec![record, TypeInterner::INT64], + vec![ + started.clone(), + generated_metadata_constant(&types, field, span), + Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }, + ], + result, + ); + for original in [started, variant_started, machine_started, put] { + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("closed evaluated builder"); + let mut changed = original.clone(); + let ExpressionKind::Intrinsic { type_arguments, .. } = &mut changed.kind else { + unreachable!() + }; + type_arguments.clear(); + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + } + let malformed = make( + IntrinsicId::TypeConstructVariantStart, + vec![choice], + vec![generated_metadata_constant(&types, field, span)], + result, + ); + assert!( + validate_hir_invocation( + &[], + &[], + &malformed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "field metadata cannot stand in for selected variant metadata" + ); + let malformed_owner = make( + IntrinsicId::TypeConstructStart, + vec![TypeInterner::INT64], + vec![], + TypeInterner::TYPE_CONSTRUCTION, + ); + assert!( + validate_hir_invocation( + &[], + &[], + &malformed_owner, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "a primitive cannot be substituted for the checked construction owner" + ); +} + +#[test] +fn generated_zero_argument_adapters_bind_and_remap_the_actual_capture() { + let mut types = TypeInterner::new(); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + let span = Span::new(FileId::new(0), 0, 1); + let mut ownership = CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(1), + source_type: signature, + target_type: signature, + }, + &[], + &[], + &[], + TypeInterner::INT64, + &[], + &types, + ) + .unwrap(); + let local = |id: LocalId| { + Some(OwnershipLocalInfo { + ty: if id == LocalId::new(1) { + signature + } else { + TypeInterner::INT64 + }, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: None, + match_binding: None, + }) + }; + validate_operand_ownership(&ownership, &[], &[], &types, local, true) + .expect("capture type joined even without arguments"); + assert!(validate_operand_ownership(&ownership, &[], &[], &types, |_| None, true).is_err()); + assert!( + validate_operand_ownership( + &ownership, + &[], + &[], + &types, + |id| { + let mut fact = local(id).unwrap(); + fact.ty = TypeInterner::INT64; + Some(fact) + }, + true + ) + .is_err() + ); + let mut original = Expression { + kind: ExpressionKind::IndirectCall { + ownership: ownership.clone(), + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::INT64, + span, + }; + validate_invocation_target( + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact captured target"); + let ExpressionKind::IndirectCall { callee, .. } = &mut original.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(0)); + assert!( + validate_invocation_target( + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err() + ); + let mut ids = vec![]; + ownership.metadata_local_ids(|id| ids.push(id)); + assert!(ids.contains(&LocalId::new(1))); + ownership + .remap_metadata_locals(|id| Ok::<_, String>(LocalId::new(id.index() + 2))) + .unwrap(); + validate_operand_ownership( + &ownership, + &[], + &[], + &types, + |id| { + (id == LocalId::new(3)).then_some(OwnershipLocalInfo { + ty: signature, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: None, + match_binding: None, + }) + }, + true, + ) + .expect("public and private target identities remap together"); + let remapped = Expression { + kind: ExpressionKind::IndirectCall { + ownership, + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(3)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + }, + ty: TypeInterner::INT64, + span, + }; + validate_invocation_target( + &[], + &remapped, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .unwrap(); +} + +#[test] +fn generated_metadata_return_comes_from_the_exact_intrinsic_schema() { + let types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let make = |intrinsic, type_arguments, result| { + let ownership = CallOwnership::generated( + GeneratedOperation::ReflectionMetadata { intrinsic }, + &[], + &[], + &[], + result, + &[], + &types, + ) + .unwrap(); + Expression { + kind: ExpressionKind::Intrinsic { + ownership, + intrinsic, + type_arguments, + reflection_arguments: vec![], + refinement_predicates: vec![], + field_validation: None, + args: vec![], + evaluation_order: vec![], + }, + ty: result, + span, + } + }; + let original = make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64], + TypeInterner::STRING, + ); + validate_hir_invocation( + &[], + &[], + &original, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect("exact generated TypeName"); + for changed in [ + make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64], + TypeInterner::BOOL, + ), + make(IntrinsicId::TypeName, vec![], TypeInterner::STRING), + make( + IntrinsicId::TypeName, + vec![TypeInterner::INT64, TypeInterner::STRING], + TypeInterner::STRING, + ), + make( + IntrinsicId::TypeHasSecret, + vec![TypeInterner::INT64], + TypeInterner::BOOL, + ), + ] { + assert!( + validate_hir_invocation( + &[], + &[], + &changed, + &types, + jett_typecheck::CheckedOwnershipContext::Ordinary + ) + .is_err(), + "a freshly minted certificate cannot manufacture a metadata return or operation schema" + ); + } +} + +#[test] +fn generated_owned_operand_cannot_acquire_a_logical_viewed_iteration_binding() { + let mut types = TypeInterner::new(); + let item = types.intern(Type::List(TypeInterner::INT64)); + let iterable = types.intern(Type::List(item)); + let span = Span::new(FileId::new(0), 0, 10); + let value = Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: item, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[value.clone()], + &[item], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::NOTHING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + assert_eq!( + generated.arguments[0].acquisition, + GeneratedAcquisition::OwnedExpression, + "the canonical owned formal must exercise the generated-owner gate" + ); + let proof = crate::checked_view_iteration_binding( + &types, + span, + iterable, + item, + crate::IterationPart::Element, + ) + .unwrap(); + let local = OwnershipLocalInfo { + ty: item, + mutable: false, + span, + view_source: None, + is_view_parameter: false, + view_iteration: Some(proof), + match_binding: None, + }; + let error = validate_generated_operand_tree(&value, &generated.arguments[0], &[local]) + .expect_err("generated acquisition cannot relabel logical source view"); + assert_eq!(error, "generated owner cannot acquire a borrowed binding"); +} + +const GENERATED_PRODUCER_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function make() returns Item: + return Item(value: 7) +function alternate() returns Item: + return Item(value: 9) +"#; + +fn displayed_call_mut(function: &mut Function) -> &mut Expression { + let ExpressionKind::StringInterpolation(parts) = &mut returned_call_mut(function).kind else { + panic!("source interpolation"); + }; + let call = parts + .iter_mut() + .find_map(|part| match part { + StringSegment::Value(Expression { + kind: ExpressionKind::DisplayResult(call), + .. + }) => Some(call.as_mut()), + _ => None, + }) + .expect("exact generated display invocation"); + assert!(matches!( + call.kind, + ExpressionKind::Call { + ownership: CallOwnership::Generated(_), + .. + } + )); + call +} + +fn generated_original_argument( + call: &mut Expression, +) -> (&mut Expression, &GeneratedArgumentOwnership) { + let ExpressionKind::Call { + args, + ownership: CallOwnership::Generated(packet), + .. + } = &mut call.kind + else { + panic!("generated invocation"); + }; + (&mut args[0], &packet.arguments[0]) +} + +fn generated_endpoint_mut(value: &mut Expression) -> &mut Expression { + match value { + Expression { + kind: ExpressionKind::View(inner), + .. + } => inner, + other => other, + } +} + +#[test] +fn generated_producer_shapes_keep_original_clone_call_and_constructor_distinct() { + for endpoint in ["make()", "Item(value: 7)", "clone make()"] { + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise() returns string:\n return \"{{{endpoint}}}\"\n" + ); + let (program, checked) = checked_source(&source); + let index = exercise_index(&program); + let mut changed = program.clone(); + let (value, argument) = + generated_original_argument(displayed_call_mut(&mut changed.functions[index])); + assert!(argument.original_witness().owned_producer()); + let endpoint = generated_endpoint_mut(value); + let original = endpoint.clone(); + endpoint.kind = ExpressionKind::Clone(Box::new(original)); + rejects(&changed, &checked, "added generated endpoint clone"); + } + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise() returns string:\n return \"{{make()}}\"\n" + ); + let (mut program, checked) = checked_source(&source); + let alternate = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .unwrap() + .id; + let index = exercise_index(&program); + let (value, _) = generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let ExpressionKind::Call { function, .. } = &mut generated_endpoint_mut(value).kind else { + panic!("original producer call"); + }; + *function = alternate; + rejects( + &program, + &checked, + "same-result generated target substitution", + ); +} + +#[test] +fn generated_producer_backing_follows_local_and_function_metadata_remaps() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let signature = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: list, + }); + let span = Span::new(FileId::new(0), 0, 1); + let source = Expression { + kind: ExpressionKind::IndirectCall { + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty: signature, + span, + }), + args: vec![], + evaluation_order: vec![], + ownership: CallOwnership::generated( + GeneratedOperation::FunctionAdapter { + callee: LocalId::new(1), + source_type: signature, + target_type: signature, + }, + &[], + &[], + &[], + list, + &[], + &types, + ) + .unwrap(), + }, + ty: list, + span, + }; + let mut packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(2), + }, + &[source.clone()], + &[list], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + generated.arguments[0] + .original_witness() + .validate_producer_shape(&source) + .unwrap(); + let mut substituted = source.clone(); + let ExpressionKind::IndirectCall { callee, .. } = &mut substituted.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(0)); + assert!( + generated.arguments[0] + .original_witness() + .validate_producer_shape(&substituted) + .is_err() + ); + let mut locals = vec![]; + packet.metadata_local_ids(|id| locals.push(id)); + assert!(locals.contains(&LocalId::new(1))); + packet + .remap_metadata_locals(|id| Ok::<_, String>(LocalId::new(id.index() + 3))) + .unwrap(); + packet + .remap_metadata_functions(|id| Ok::<_, String>(FunctionId::new(id.index() + 5))) + .unwrap(); + let mut functions = vec![]; + packet.metadata_function_ids(|id| functions.push(id)); + assert!(functions.contains(&FunctionId::new(7))); + let mut remapped = source; + let ExpressionKind::IndirectCall { callee, .. } = &mut remapped.kind else { + unreachable!() + }; + callee.kind = ExpressionKind::Local(LocalId::new(4)); + let CallOwnership::Generated(generated) = &packet else { + unreachable!() + }; + generated.arguments[0] + .original_witness() + .validate_producer_shape(&remapped) + .unwrap(); + validate_operand_ownership(&packet, &[list], &[0], &types, |_| None, true).unwrap(); +} + +#[test] +fn generated_constructor_shapes_keep_authenticated_inline_descriptors() { + let source = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + callback: function(int64) returns int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function exercise(offset: int64) returns string: + return "{Item(callback: function(value: int64) returns int64: return value + offset)}" +"#; + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let (value, _) = generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let ExpressionKind::StructConstruct { fields, .. } = &mut generated_endpoint_mut(value).kind + else { + panic!("owned constructor"); + }; + let ExpressionKind::ClosureRef { captures, .. } = &mut fields[0].kind else { + panic!("canonical captured inline descriptor"); + }; + assert_eq!(captures.len(), 1); + captures.clear(); + rejects(&program, &checked, "descriptor backing substitution"); +} + +#[test] +fn generated_handle_shape_restoration_keeps_source_snapshot_and_cfg_authority_separate() { + let source = format!( + "{GENERATED_PRODUCER_SOURCE}function exercise(incoming: optional[int64]) returns string:\n return \"{{Item(value: incoming handle: default 7)}}\"\n" + ); + let (mut program, _checked) = checked_source(&source); + let index = exercise_index(&program); + let (value, argument) = + generated_original_argument(displayed_call_mut(&mut program.functions[index])); + let original = value.clone(); + let ExpressionKind::StructConstruct { fields, .. } = &mut generated_endpoint_mut(value).kind + else { + panic!("owned constructor"); + }; + let ExpressionKind::Handle { target, .. } = &fields[0].kind else { + panic!("original handled field"); + }; + let original_target = target.as_ref().clone(); + let handle_span = fields[0].span; + fields[0].kind = ExpressionKind::Local(LocalId::new(100)); + assert!( + argument + .original_witness() + .validate_producer_shape(value) + .is_err(), + "original HIR cannot carry lowered result authority" + ); + let mut reached = 0; + argument + .original_witness() + .validate_lowered_producer_shape(value, |produced, original| { + let handle = original.handle().expect("exact original Handle node"); + assert_eq!(handle.kind(), &HandleKind::Optional); + assert_eq!(produced.ty, handle.result_type()); + assert_eq!(produced.span, handle_span); + handle.validate_source_shape(&original_target)?; + let snapshot = Expression { + kind: ExpressionKind::Clone(Box::new(original_target.clone())), + ty: original_target.ty, + span: original_target.span, + }; + handle.validate_source_shape(&snapshot)?; + let double = Expression { + kind: ExpressionKind::Clone(Box::new(snapshot)), + ty: original_target.ty, + span: original_target.span, + }; + assert!(handle.validate_source_shape(&double).is_err()); + reached += 1; + Ok(()) + }) + .expect("callback only authenticates this original Handle node"); + assert_eq!(reached, 1); + let mut foreign = original.clone(); + let ExpressionKind::StructConstruct { fields, .. } = + &mut generated_endpoint_mut(&mut foreign).kind + else { + unreachable!() + }; + fields[0].kind = ExpressionKind::Int(7); + assert!( + argument + .original_witness() + .validate_lowered_producer_shape(&foreign, |_, _| { + panic!("a non-Local replacement never receives Handle authority") + }) + .is_err() + ); +} + +#[test] +fn generated_storage_callbacks_keep_exact_non_handle_nodes_and_copy_permissions() { + let mut types = TypeInterner::new(); + let span = Span::new(FileId::new(0), 0, 1); + let literal = Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[literal.clone()], + &[literal.ty], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(packet) = packet else { + unreachable!() + }; + let witness = packet.arguments[0].original_witness(); + let storage = Expression { + kind: ExpressionKind::Local(LocalId::new(100)), + ..literal.clone() + }; + assert!(witness.validate_producer_shape(&storage).is_err()); + witness + .validate_lowered_producer_shape(&storage, |value, original| { + assert_eq!((value.ty, value.span), (original.ty(), original.span())); + assert!(original.handle().is_none()); + original + .validate_reconstructed(&literal, |_, _| Err("unexpected nested storage".into()))?; + let inserted_clone = Expression { + kind: ExpressionKind::Clone(Box::new(literal.clone())), + ..literal.clone() + }; + assert!( + original + .validate_reconstructed(&inserted_clone, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + assert!( + original + .validate_existing_local_snapshot(&inserted_clone, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + Ok(()) + }) + .expect("read-only callback recurses against the same original primitive node"); + + let list = types.intern(Type::List(TypeInterner::INT64)); + let descriptor = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + for (ty, ordinary_copy) in [(list, true), (descriptor, false)] { + let original = Expression { + kind: ExpressionKind::Local(LocalId::new(1)), + ty, + span, + }; + let packet = CallOwnership::generated( + GeneratedOperation::Display { + method: FunctionId::new(0), + }, + &[original.clone()], + &[ty], + &[jett_typecheck::CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + &types, + ) + .unwrap(); + let CallOwnership::Generated(packet) = packet else { + unreachable!() + }; + let witness = packet.arguments[0].original_witness(); + let storage = Expression { + kind: ExpressionKind::Local(LocalId::new(100)), + ty, + span, + }; + let snapshot = Expression { + kind: ExpressionKind::Clone(Box::new(original.clone())), + ty, + span, + }; + witness + .validate_lowered_producer_shape(&storage, |_, node| { + assert!( + node.validate_reconstructed(&snapshot, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + assert_eq!( + node.validate_existing_local_snapshot(&snapshot, |_, _| Err( + "unexpected nested storage".into() + )) + .is_ok(), + ordinary_copy + ); + let double = Expression { + kind: ExpressionKind::Clone(Box::new(snapshot.clone())), + ty, + span, + }; + assert!( + node.validate_existing_local_snapshot(&double, |_, _| Err( + "unexpected nested storage".into() + )) + .is_err() + ); + let wrong_backing = Expression { + kind: ExpressionKind::Clone(Box::new(storage.clone())), + ty, + span, + }; + assert!( + node.validate_existing_local_snapshot(&wrong_backing, |_, _| Err( + "foreign storage cannot replace original backing".into() + )) + .is_err() + ); + Ok(()) + }) + .expect("storage restoration does not infer descriptor copy authority"); + } +} + +#[test] +fn caller_packets_preserve_exact_nominal_secret_argument_joins() { + for callee in ["callback", "factory()"] { + for (parameter, argument, result) in [ + ("int64", "Classified", "secret[int64]"), + ("int64", "DeepClassified", "secret[int64]"), + ("Classified", "secret[Classified]", "Classified"), + ("DeepClassified", "secret[DeepClassified]", "DeepClassified"), + ] { + let source = format!( + "type Classified = secret[int64] where true\n\ + type DeepClassified = Classified where true\n\ + type Alternative = secret[int64] where true\n\ + type PublicValue = int64 where true\n\ + function callback(value: {parameter}) returns {parameter}:\n return value\n\ + function factory() returns function({parameter}) returns {parameter}:\n return callback\n\ + function exercise(value: {argument}, unrelated: Alternative, public_value: PublicValue) returns {result}:\n return {callee}(value)\n" + ); + let (program, checked) = checked_source(&source); + let exercise = &program.functions[exercise_index(&program)]; + let expression = returned_call(exercise); + let (args, source) = match &expression.kind { + ExpressionKind::Call { + args, + ownership: CallOwnership::Source(source), + .. + } + | ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(source), + .. + } => (args, source), + _ => panic!("checked source call"), + }; + let argument = &source.arguments[0]; + assert_eq!( + argument.actual_type, + argument.source_witness().occurrence_type() + ); + let CallerOrigin::Binding(binding) = &argument.origin else { + panic!("exact nominal source binding"); + }; + assert_eq!(binding.ty, argument.actual_type); + match checked.interner.resolve(argument.actual_type) { + Type::Refinement { .. } => assert_eq!(args[0].ty, argument.actual_type), + Type::Secret(inner) => assert_eq!(*inner, argument.parameter_type), + _ => panic!("nominal-secret or original explicit-secret control"), + } + assert_eq!(source.arguments.len(), 1); + } + } +} + +#[test] +fn caller_packets_reject_replaced_nominal_secret_operands_and_untainted_results() { + for callee in ["callback", "factory()"] { + let source = format!( + "type Classified = secret[int64] where true\n\ + type Alternative = secret[int64] where true\n\ + type PublicValue = int64 where true\n\ + function callback(value: int64) returns int64:\n return value\n\ + function factory() returns function(int64) returns int64:\n return callback\n\ + function exercise(value: Classified, unrelated: Alternative, public_value: PublicValue) returns secret[int64]:\n return {callee}(value)\n" + ); + let (program, checked) = checked_source(&source); + let index = exercise_index(&program); + for name in ["unrelated", "public_value"] { + let mut corrupted = program.clone(); + let replacement = corrupted.functions[index] + .locals + .iter() + .find(|local| local.name == name) + .expect("independent nominal control") + .ty; + let expression = returned_call_mut(&mut corrupted.functions[index]); + let args = match &mut expression.kind { + ExpressionKind::Call { args, .. } | ExpressionKind::IndirectCall { args, .. } => { + args + } + _ => panic!("checked source call"), + }; + assert_ne!(replacement, args[0].ty); + args[0].ty = replacement; + let expression = returned_call(&corrupted.functions[index]); + let error = validate_invocation_target( + &corrupted.functions, + expression, + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect_err("another nominal type cannot replace the immutable actual"); + assert!( + error.contains("converted operand differs from its physical parameter"), + "{callee}/{name}: {error}" + ); + rejects(&corrupted, &checked, "replaced nominal Secret operand"); + } + let mut corrupted = program.clone(); + returned_call_mut(&mut corrupted.functions[index]).ty = TypeInterner::INT64; + let error = validate_invocation_target( + &corrupted.functions, + returned_call(&corrupted.functions[index]), + &checked.interner, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ) + .expect_err("Secret lifting retains its checked result"); + assert_eq!( + error, + "call ownership result differs from its checked source certificate" + ); + rejects(&corrupted, &checked, "untainted nominal Secret result"); + } +} + +#[test] +fn caller_packets_accept_exact_qualified_descriptors_but_not_value_substitutions() { + let source = r#"namespace app +struct Holder: + callback: function(int64) returns int64 +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64, marker: int64) returns int64: + return marker +function exercise(holder: Holder, stored: function(int64) returns int64) returns int64: + return inspect(marker: 1, callback: app.identity) +"#; + for replacement in ["local", "field"] { + let (mut program, checked) = checked_source(source); + let index = exercise_index(&program); + let holder = local_id(&program.functions[index], "holder"); + let stored = local_id(&program.functions[index], "stored"); + let holder_type = program.functions[index].locals[holder.index() as usize].ty; + let expression = returned_call_mut(&mut program.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + evaluation_order, + .. + } = &mut expression.kind + else { + panic!("qualified descriptor argument"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + assert!(matches!(args[0].kind, ExpressionKind::FunctionRef(_))); + args[0].kind = if replacement == "local" { + ExpressionKind::Local(stored) + } else { + ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(holder), + ty: holder_type, + span: args[0].span, + }), + owner_type: holder_type, + field: FieldId::new(0), + } + }; + let errors = validate_program_call_ownership(&program, &checked.interner) + .expect_err("a descriptor producer cannot be replaced by a value origin"); + assert!(errors.iter().any(|error| error.message == + "call ownership cannot manufacture a producer acquisition from a binding or field"), + "{replacement}: {errors:?}"); + } +} + +#[test] +fn caller_packets_keep_method_producers_and_real_callback_field_projections_distinct() { + let source = r#"namespace models +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +namespace app +struct Holder: + callback: function(view models.Point) returns int64 +function inspect(view callback: function(view models.Point) returns int64) returns int64: + return 7 +function method_call() returns int64: + use models as m + return inspect(view m.Point.amount) +function exercise(view holder: Holder) returns int64: + return inspect(view holder.callback) +"#; + let (program, checked) = checked_source(source); + let method = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "method_call") + .expect("method source caller"); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(method).kind + else { + panic!("method descriptor call"); + }; + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + let ExpressionKind::View(inner) = &args[0].kind else { + panic!("written descriptor view"); + }; + assert!(matches!(inner.kind, ExpressionKind::FunctionRef(_))); + let field = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(field).kind + else { + panic!("borrowed value field call"); + }; + assert!( + matches!(packet.arguments[0].origin, CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local) + } if local == local_id(field, "holder")) + ); + let ExpressionKind::View(inner) = &args[0].kind else { + panic!("written field view"); + }; + assert!(matches!(inner.kind, ExpressionKind::Field { .. })); + validate_program_call_ownership(&program, &checked.interner) + .expect("both exact origins validate"); +} + +#[test] +fn caller_packets_generic_mutual_source_calls_have_exact_syntax_type_and_target_proofs() { + let source = r#"namespace sample +mutual: + function leaf[T](view value: T) returns int64 + function alternate[T](value: T) returns int64 + function forward[T](view value: T) returns int64 +function leaf[T](view value: T) returns int64: + return 7 +function alternate[T](value: T) returns int64: + return 9 +function forward[T](view value: T) returns int64: + return leaf[T](view value) +function exercise(values: list[int64]) returns int64: + return forward[list[int64]](view values) +function instantiate_alternate(values: list[int64]) returns int64: + return alternate[list[int64]](values) +"#; + let (program, checked) = checked_source(source); + let forward = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "forward") + .expect("concrete mutual forwarding"); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(forward).kind + else { + panic!("source forward call"); + }; + assert_eq!( + packet.arguments[0].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(packet.arguments[0].callee_access, CheckedCalleeAccess::View); + validate_program_call_ownership(&program, &checked.interner).expect("every exact packet"); + for mutation in ["syntax", "type", "target"] { + let mut corrupted = program.clone(); + let alternate = corrupted + .functions + .iter() + .find(|function| function.identity.declaration.name == "alternate") + .expect("owned alternative") + .id; + let index = corrupted + .functions + .iter() + .position(|function| function.identity.declaration.name == "forward") + .unwrap(); + let expression = returned_call_mut(&mut corrupted.functions[index]); + let ExpressionKind::Call { + function, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("real mutual packet"); + }; + match mutation { + "syntax" => packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::Bare, + "type" => packet.arguments[0].actual_type = TypeInterner::INT64, + "target" => *function = alternate, + _ => unreachable!(), + } + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("exact original mutual proof cannot be changed"); + assert!(!errors.is_empty(), "{mutation}"); + } +} + +const MACHINE_STATE_SOURCE_JOINS: &str = r#"machine Session: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +machine OtherSession: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +function precise(session: Session at logged_in) returns string: + return session.user_id +function guest_only(session: Session at guest) returns string: + return "guest" +function other_only(view session: OtherSession at logged_in) returns int64: + return 2 +function wide(view session: Session) returns int64: + return 7 +function owned_wide(session: Session) returns int64: + return 9 +function guarded(session: Session) returns string: + if session at logged_in: + return precise(session) + return "guest" +function exercise_direct(view session: Session at logged_in) returns int64: + return wide(view session) +function exercise_indirect(view session: Session at logged_in, callback: function(view Session) returns int64) returns int64: + return callback(view session) +"#; + +fn machine_state_caller_index(program: &Program, name: &str) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == name) + .expect("exact source function") +} + +fn guarded_machine_call(function: &Function) -> &Expression { + let StatementKind::If { then_block, .. } = &function.body.statements[0].kind else { + panic!("checked source state guard"); + }; + let StatementKind::Return(Some(expression)) = &then_block.statements[0].kind else { + panic!("call inside exact state guard"); + }; + expression +} + +#[test] +fn caller_packets_machine_guard_keeps_exact_occurrence_and_declared_binding() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + let caller = &program.functions[machine_state_caller_index(&program, "guarded")]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &guarded_machine_call(caller).kind + else { + panic!("checked narrowed call"); + }; + let argument = &packet.arguments[0]; + let CallerOrigin::Binding(fact) = &argument.origin else { + panic!("original binding"); + }; + let Type::Machine(parent) = checked.interner.resolve(fact.ty) else { + panic!("declaration remains bare machine"); + }; + let Type::MachineState { machine, .. } = checked.interner.resolve(argument.actual_type) else { + panic!("checked occurrence retains exact state"); + }; + assert_eq!(parent, machine); + assert_ne!(fact.ty, argument.actual_type); + assert_eq!(caller.locals[fact.local.index() as usize].ty, fact.ty); + assert_eq!(args[0].ty, argument.actual_type); + assert_eq!( + argument.source_witness().occurrence_type(), + argument.actual_type + ); + assert!(matches!(args[0].kind, ExpressionKind::Local(local) if local == fact.local)); + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::TransferOwned + ); + assert_eq!(argument.callee_access, CheckedCalleeAccess::Owned); + validate_program_call_ownership(&program, &checked.interner).expect("exact state proof"); +} + +#[test] +fn caller_packets_machine_state_views_erase_only_to_exact_parent_parameters() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + for name in ["exercise_direct", "exercise_indirect"] { + let caller = &program.functions[machine_state_caller_index(&program, name)]; + let (args, packet) = match &returned_call(caller).kind { + ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } + | ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Source(packet), + .. + } => (args, packet), + _ => panic!("checked direct or indirect source call"), + }; + let argument = &packet.arguments[0]; + let Type::MachineState { machine, .. } = checked.interner.resolve(argument.actual_type) + else { + panic!("physical argument remains exact state"); + }; + let Type::Machine(parent) = checked.interner.resolve(argument.parameter_type) else { + panic!("parameter is bare parent machine"); + }; + assert_eq!(machine, parent); + assert_ne!(argument.actual_type, argument.parameter_type); + assert_eq!(args[0].ty, argument.actual_type); + assert_eq!( + argument.source_witness().occurrence_type(), + argument.actual_type + ); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + } + validate_program_call_ownership(&program, &checked.interner) + .expect("one-way exact parent joins"); +} + +#[test] +fn caller_packets_machine_state_joins_preserve_private_type_syntax_and_mode_proofs() { + let (program, checked) = checked_source(MACHINE_STATE_SOURCE_JOINS); + let direct = machine_state_caller_index(&program, "exercise_direct"); + let guest = program.functions[machine_state_caller_index(&program, "guest_only")].params[0].ty; + let other = program.functions[machine_state_caller_index(&program, "other_only")].params[0].ty; + let owned = program.functions[machine_state_caller_index(&program, "owned_wide")].id; + for mutation in ["wrong state", "wrong machine", "syntax", "owned target"] { + let mut corrupted = program.clone(); + let ExpressionKind::Call { + function, + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut returned_call_mut(&mut corrupted.functions[direct]).kind + else { + panic!("real source View packet"); + }; + match mutation { + "wrong state" | "wrong machine" => { + let replacement = if mutation == "wrong state" { + guest + } else { + other + }; + assert_ne!(args[0].ty, replacement); + args[0].ty = replacement; + let ExpressionKind::View(inner) = &mut args[0].kind else { + panic!("written View"); + }; + inner.ty = replacement; + } + "syntax" => packet.arguments[0].syntax = jett_typecheck::CheckedCallerSyntax::Bare, + "owned target" => *function = owned, + _ => unreachable!(), + } + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("private exact state/source/mode witness cannot be changed"); + assert!(!errors.is_empty(), "{mutation}"); + } + let mut corrupted = program.clone(); + let guarded = machine_state_caller_index(&corrupted, "guarded"); + let StatementKind::If { then_block, .. } = + &mut corrupted.functions[guarded].body.statements[0].kind + else { + panic!("original guard"); + }; + let StatementKind::Return(Some(expression)) = &mut then_block.statements[0].kind else { + panic!("guarded call"); + }; + let packet = source_packet_mut(expression); + let CallerOrigin::Binding(fact) = &mut packet.arguments[0].origin else { + panic!("binding fact"); + }; + fact.ty = other; + assert!( + validate_program_call_ownership(&corrupted, &checked.interner).is_err(), + "the declared binding fact stays exact" + ); +} + +#[test] +fn caller_packets_machine_state_join_does_not_admit_wrong_nominal_or_state_sources() { + for source in [ + MACHINE_STATE_SOURCE_JOINS.replace( + "return wide(view session)", + "return other_only(view session)", + ), + MACHINE_STATE_SOURCE_JOINS.replace("return precise(session)", "return guest_only(session)"), + ] { + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error + && diagnostic.code.code() == 304), + "wrong machine/state remains a checked argument error: {:?}", + checked.diagnostics + ); + } +} + +const CONVERTED_SOURCE_HANDLE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function read(view value: Show) returns int64: + return 7 +function exercise(incoming: optional[uint64], spare: uint64) returns int64: + uint64 fallback = 0 + return read(view(incoming handle: + default fallback + )) +"#; + +#[test] +fn caller_packets_converted_handle_keeps_inner_occurrence_separate_from_written_view() { + let (program, checked) = checked_source(CONVERTED_SOURCE_HANDLE); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(function).kind + else { + panic!("source read"); + }; + let argument = &packet.arguments[0]; + let backing = call_ownership::validate_source_handled_operand( + &args[0], + argument, + &packet.bridge, + &checked.interner, + ) + .expect("exact finite conversion") + .expect("original Handle"); + assert!(matches!(backing.kind, ExpressionKind::Handle { .. })); + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_eq!(argument.actual_type, TypeInterner::UINT64); + assert!(matches!( + checked.interner.resolve(argument.parameter_type), + Type::Interface(_) + )); + assert_ne!(backing.span, argument.source_span); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.origin, CallerOrigin::OwnedExpression); +} + +#[test] +fn caller_packets_converted_handle_rejects_lost_inner_occurrence_and_original_node() { + let (program, checked) = checked_source(CONVERTED_SOURCE_HANDLE); + for mutation in ["span", "type", "literal replacement"] { + let mut corrupted = program.clone(); + let function = &mut corrupted.functions[exercise_index(&program)]; + let ExpressionKind::Call { args, .. } = &mut returned_call_mut(function).kind else { + panic!("source read"); + }; + let mut backing = &mut args[0]; + while matches!( + backing.kind, + ExpressionKind::View(_) + | ExpressionKind::InterfaceCoerce { .. } + | ExpressionKind::FunctionAdapter { .. } + ) { + backing = match &mut backing.kind { + ExpressionKind::View(inner) + | ExpressionKind::InterfaceCoerce { value: inner, .. } + | ExpressionKind::FunctionAdapter { value: inner, .. } => inner, + _ => unreachable!("transparent conversion checked before borrowing"), + }; + } + assert!(matches!(backing.kind, ExpressionKind::Handle { .. })); + match mutation { + "span" => backing.span.start += 1, + "type" => backing.ty = TypeInterner::INT64, + "literal replacement" => backing.kind = ExpressionKind::Int(0), + _ => unreachable!(), + } + rejects(&corrupted, &checked, mutation); + } +} + +const QUALIFIED_UNIT_ENUM_SOURCE: &str = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function exercise(holder: Holder, stored: models.Choice) returns int64: + use models as m + return inspect(marker: 1, choice: m.Choice.empty) +"#; + +#[test] +fn caller_packets_qualified_unit_enums_are_producers_not_same_typed_value_origins() { + let (program, checked) = checked_source(QUALIFIED_UNIT_ENUM_SOURCE); + let index = exercise_index(&program); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + evaluation_order, + .. + } = &returned_call(&program.functions[index]).kind + else { + panic!("qualified enum Source invocation"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(packet.arguments[0].origin, CallerOrigin::OwnedExpression); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &args[0].kind + else { + panic!("exact static unit construction"); + }; + assert_eq!(*enum_type, packet.arguments[0].actual_type); + assert_eq!(args[0].ty, *enum_type); + assert!(payloads.is_empty() && evaluation_order.is_empty()); + let Type::Enum(id) = checked.interner.resolve(*enum_type) else { + panic!("nominal Enum identity"); + }; + let declaration = checked.interner.resolve_enum(*id); + assert_eq!(declaration.name, "models.Choice"); + assert_eq!(declaration.variants[variant.index() as usize].name, "empty"); + assert!( + declaration.variants[variant.index() as usize] + .fields + .is_empty() + ); + for replacement in ["local", "field", "public projection"] { + let mut corrupted = program.clone(); + let holder = local_id(&corrupted.functions[index], "holder"); + let stored = local_id(&corrupted.functions[index], "stored"); + let holder_type = corrupted.functions[index].locals[holder.index() as usize].ty; + let expression = returned_call_mut(&mut corrupted.functions[index]); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &mut expression.kind + else { + panic!("checked enum invocation"); + }; + match replacement { + "local" => args[0].kind = ExpressionKind::Local(stored), + "field" => { + args[0].kind = ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(holder), + ty: holder_type, + span: args[0].span, + }), + owner_type: holder_type, + field: FieldId::new(0), + } + } + "public projection" => { + packet.arguments[0].origin = CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Other, + } + } + _ => unreachable!(), + } + let expected = if replacement == "public projection" { + "call ownership disagrees with its original source witness" + } else { + "call ownership cannot manufacture a producer acquisition from a binding or field" + }; + let errors = validate_program_call_ownership(&corrupted, &checked.interner) + .expect_err("static producer authority cannot become a real value origin"); + assert!( + errors.iter().any(|error| error.message == expected), + "{replacement}: {errors:?}" + ); + } +} + +#[test] +fn caller_packets_enum_valued_bindings_fields_and_temporary_fields_keep_exact_roots() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function from_field(view holder: Holder) returns int64: + return inspect(holder.choice, 1) +function from_view(view holder: Holder) returns int64: + return inspect(view holder.choice, 2) +function exercise(view stored: models.Choice) returns int64: + return inspect(view stored, 3) +function from_temporary() returns int64: + use models + return inspect(Holder(choice: models.Choice.empty).choice, 4) +"#; + let (program, checked) = checked_source(source); + for name in ["from_field", "from_view", "exercise", "from_temporary"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("checked runtime origin control"); + let ExpressionKind::Call { + args, + ownership: CallOwnership::Source(packet), + .. + } = &returned_call(function).kind + else { + panic!("ordinary enum call"); + }; + let argument = &packet.arguments[0]; + assert!(matches!( + checked.interner.resolve(argument.actual_type), + Type::Enum(_) + )); + let mut value = &args[0]; + if let ExpressionKind::View(inner) = &value.kind { + value = inner; + } + match name { + "from_field" => { + assert_eq!( + argument.origin, + CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Local(local_id(function, "holder")), + } + ); + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + } + "from_view" => { + assert_eq!( + argument.origin, + CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local_id(function, "holder")), + } + ); + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + } + "exercise" => { + assert!(matches!(argument.origin, CallerOrigin::Binding(fact) + if fact.local == local_id(function, "stored"))); + assert!(matches!(value.kind, ExpressionKind::Local(_))); + } + "from_temporary" => { + assert_eq!( + argument.origin, + CallerOrigin::OwnedFieldCopy { + parent: CallerViewSource::Other, + } + ); + let ExpressionKind::Field { base, .. } = &value.kind else { + panic!("a real temporary field is not a static enum producer"); + }; + assert!(matches!(base.kind, ExpressionKind::StructConstruct { .. })); + } + _ => unreachable!(), + } + } + validate_program_call_ownership(&program, &checked.interner) + .expect("enum TypeId alone does not change source roots or copying rules"); +} + +#[test] +fn caller_packets_retained_pipeline_seals_physical_header_separately_from_raw_source() { + let source = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(view values: list[int64]) returns int64: + return values into view read(2) +"#; + let (program, checked) = checked_source(source); + let index = exercise_index(&program); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &returned_call(&program.functions[index]).kind + else { + panic!("source pipeline"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.retained_snapshot_span(), Some(args[0].span)); + assert_ne!(args[0].span, argument.source_span); + assert_eq!(argument.source_span, { + let ExpressionKind::View(raw) = &args[0].kind else { + panic!("written pipeline View"); + }; + raw.span + }); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.staging, ArgumentStaging::Original); + + let mut changed = program.clone(); + let ExpressionKind::Call { args, .. } = + &mut returned_call_mut(&mut changed.functions[index]).kind + else { + panic!("original pipeline"); + }; + args[0].span.start += 1; + let errors = validate_program_call_ownership(&changed, &checked.interner) + .expect_err("a new physical header cannot replace the private initial occurrence"); + assert!( + errors + .iter() + .any(|error| error.message + == "call ownership retained snapshot physical occurrence changed"), + "{errors:?}" + ); +} + +const GENERATED_CONVERTED_CALLABLE_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function show(view item: Named) returns string: + return Named.name(view item) +function invoke_reader(reader: function(view User) returns string) returns string: + User input = User(label: "higher-order") + return reader(view input) +function exercise() returns string: + function(function(view Named) returns string) returns string higher = invoke_reader + return higher(show) +"#; +const GENERATED_CONVERTED_CONTAINER_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function make_reader(prefix: string) returns function(view Named) returns string: + return function(view item: Named) returns string: return "{prefix}:{Named.name(view item)}" +function first_reader(items: list[function(view User) returns string]) returns string: + User input = User(label: "higher-container") + for callback in items: + return callback(view input) + return "empty" +function exercise() returns string: + function(list[function(view Named) returns string]) returns string adapted = first_reader + return adapted(list(make_reader("higher"))) +"#; +const GENERATED_CONVERTED_TWO_READERS: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +function show(view item: Named) returns string: + return Named.name(view item) +function invoke_reader(first: function(view User) returns string, second: function(view User) returns string) returns string: + User input = User(label: "higher-order") + return "{first(view input)}:{second(view input)}" +function exercise() returns string: + function(function(view Named) returns string, function(view Named) returns string) returns string higher = invoke_reader + return higher(show, show) +"#; + +fn checked_generated_conversion_source(source: &str, release: bool) -> (Program, CheckResult) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve: {:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "check: {:?}", + checked.diagnostics + ); + let program = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("canonical generated adapter keeps its sealed raw actual"); + validate_program_call_ownership(&program, &checked.interner) + .expect("complete generated ownership"); + (program, checked) +} + +fn generated_converted_adapter_index(program: &Program, container: bool) -> usize { + program + .functions + .iter() + .position(|function| { + function.body.statements.iter().any(|statement| { + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(packet), + args, + .. + }, + .. + })) = &statement.kind + else { + return false; + }; + matches!(packet.operation, GeneratedOperation::FunctionAdapter { .. }) + && args.iter().zip(&packet.arguments).any(|(value, argument)| { + value.ty != argument.actual_type + && match &value.kind { + ExpressionKind::InterfaceCoerce { adapters, .. } => { + container && !adapters.is_empty() + } + ExpressionKind::FunctionAdapter { .. } => !container, + _ => false, + } + }) + }) + }) + .expect("source-derived generated conversion body") +} + +fn generated_converted_argument(function: &Function) -> (&Expression, &GeneratedArgumentOwnership) { + let ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(packet), + args, + .. + } = &returned_call(function).kind + else { + panic!("generated indirect adapter call"); + }; + args.iter() + .zip(&packet.arguments) + .find(|(value, argument)| value.ty != argument.actual_type) + .expect("original actual differs from converted physical formal") +} + +fn generated_converted_endpoint(mut value: &Expression, actual: TypeId) -> &Expression { + while value.ty != actual { + value = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => panic!("canonical finite conversion"), + }; + } + value +} + +fn generated_converted_endpoint_mut(mut value: &mut Expression, actual: TypeId) -> &mut Expression { + while value.ty != actual { + value = match &mut value.kind { + ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } => value, + _ => panic!("canonical finite conversion"), + }; + } + value +} + +fn generated_conversion_locals(function: &Function) -> Vec { + function + .locals + .iter() + .map(|local| OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter: function + .params + .iter() + .any(|param| param.local == local.id && param.mode == ParamMode::View), + view_iteration: None, + match_binding: None, + }) + .collect() +} + +#[test] +fn generated_converted_callable_rejoins_original_target_parameter_without_place_peeling() { + for release in [false, true] { + let (mut program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_CALLABLE_SOURCE, release); + let index = generated_converted_adapter_index(&program, false); + let function = &program.functions[index]; + let (value, argument) = generated_converted_argument(function); + assert!(matches!(value.kind, ExpressionKind::FunctionAdapter { .. })); + assert_eq!( + call_ownership::immediate_source(value), + CallerViewSource::Other, + "a callable adapter remains outside general place provenance" + ); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("sealed raw target parameter"); + }; + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(local) + ); + assert_eq!(raw.span, argument.original_witness().source_span()); + assert_eq!(value.span, raw.span); + assert_eq!(raw.ty, function.locals[local.index() as usize].ty); + assert_eq!(argument.acquisition, GeneratedAcquisition::OwnedExpression); + validate_generated_operand_tree(value, argument, &generated_conversion_locals(function)) + .expect("strict raw backing after canonical callable conversion"); + let count = program.functions.len(); + complete_value_conversions(&mut program, &checked.interner) + .expect("existing adapter completion is repeatable"); + assert_eq!(program.functions.len(), count, "no new duplicate adapters"); + } +} + +#[test] +fn generated_converted_container_rejoins_raw_local_without_erasing_callback_table() { + for release in [false, true] { + let (program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_CONTAINER_SOURCE, release); + let function = &program.functions[generated_converted_adapter_index(&program, true)]; + let (value, argument) = generated_converted_argument(function); + let ExpressionKind::InterfaceCoerce { adapters, .. } = &value.kind else { + panic!("container conversion"); + }; + assert!(!adapters.is_empty()); + assert_eq!( + call_ownership::immediate_source(value), + CallerViewSource::Other + ); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw list-of-callables target parameter"); + }; + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(local) + ); + assert_eq!(argument.acquisition, GeneratedAcquisition::OwnedExpression); + for adapter in adapters { + let target = &program.functions[adapter.function.index() as usize]; + assert_eq!(target.id, adapter.function); + assert_eq!(target.capture_count, 1); + assert_eq!(target.params[0].ty, adapter.source); + let Type::Function { + params, + view_params, + return_type, + } = checked.interner.resolve(adapter.target) + else { + panic!("checked callback target signature"); + }; + assert_eq!( + target.params[1..] + .iter() + .map(|param| param.ty) + .collect::>(), + *params + ); + assert_eq!( + target.params[1..] + .iter() + .map(|param| param.mode == ParamMode::View) + .collect::>(), + *view_params + ); + assert_eq!(target.return_type, *return_type); + } + validate_generated_operand_tree(value, argument, &generated_conversion_locals(function)) + .expect("raw origin does not replace independent callback table validation"); + } +} + +#[test] +fn generated_converted_raw_occurrence_refuses_same_typed_backing_and_arbitrary_wrappers() { + let (program, checked) = + checked_generated_conversion_source(GENERATED_CONVERTED_TWO_READERS, false); + let index = generated_converted_adapter_index(&program, false); + let function = &program.functions[index]; + let (value, argument) = generated_converted_argument(function); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw target parameter"); + }; + let spare = function + .params + .iter() + .find(|param| param.local != local && param.ty == raw.ty) + .expect("independent same-typed source parameter") + .local; + for mutation in [ + "physical span", + "raw span", + "foreign local", + "same type clone", + "same type adapter", + "nonconversion", + ] { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let ExpressionKind::IndirectCall { + args, + ownership: CallOwnership::Generated(packet), + .. + } = &mut returned_call_mut(function).kind + else { + panic!("generated converted call"); + }; + let actual = packet.arguments[0].actual_type; + if mutation == "physical span" { + args[0].span.start += 1; + } else if mutation == "nonconversion" { + let original = args[0].clone(); + args[0].kind = ExpressionKind::Clone(Box::new(original)); + } else { + let raw = generated_converted_endpoint_mut(&mut args[0], actual); + match mutation { + "raw span" => raw.span.start += 1, + "foreign local" => raw.kind = ExpressionKind::Local(spare), + "same type clone" => raw.kind = ExpressionKind::Clone(Box::new(raw.clone())), + "same type adapter" => { + raw.kind = ExpressionKind::FunctionAdapter { + value: Box::new(raw.clone()), + function: program.functions[index].id, + } + } + _ => unreachable!(), + } + } + let errors = validate_program_call_ownership(&changed, &checked.interner).expect_err( + "conversion cannot replace original raw occurrence/backing or mint same-type peel", + ); + assert!( + errors.iter().any(|error| matches!( + error.message.as_str(), + "generated operand differs from its original typed occurrence or backing" + | "generated operand has no checked actual/conversion type" + )), + "{mutation}: {errors:?}" + ); + } +} + +#[test] +fn generated_converted_raw_owner_keeps_borrowed_slot_gate_and_metadata_remap_identity() { + let (program, _) = + checked_generated_conversion_source(GENERATED_CONVERTED_CALLABLE_SOURCE, false); + let function = &program.functions[generated_converted_adapter_index(&program, false)]; + let (value, argument) = generated_converted_argument(function); + let raw = generated_converted_endpoint(value, argument.actual_type); + let ExpressionKind::Local(local) = raw.kind else { + panic!("raw original Local"); + }; + let mut locals = generated_conversion_locals(function); + locals[local.index() as usize].is_view_parameter = true; + assert_eq!( + validate_generated_operand_tree(value, argument, &locals).unwrap_err(), + "generated owner cannot acquire a borrowed binding" + ); + + let ExpressionKind::IndirectCall { ownership, .. } = &returned_call(function).kind else { + panic!("adapter call"); + }; + let mut remapped = ownership.clone(); + remapped + .remap_metadata_locals(|id| Ok::<_, ()>(LocalId::new(id.index() + 3))) + .expect("structural remap"); + let CallOwnership::Generated(packet) = remapped else { + panic!("same generated packet"); + }; + let argument = &packet.arguments[0]; + let mapped = LocalId::new(local.index() + 3); + assert_eq!( + argument.original_witness().origin(), + CallerViewSource::Local(mapped) + ); + let mut raw = raw.clone(); + raw.kind = ExpressionKind::Local(mapped); + argument + .original_witness() + .validate_producer_shape(&raw) + .expect("raw shape remaps with the origin"); + let mut stale = raw; + stale.kind = ExpressionKind::Local(local); + assert!( + argument + .original_witness() + .validate_producer_shape(&stale) + .is_err(), + "old numeric Local cannot replace remapped original backing" + ); +} + +const FLOW_PROJECTED_MACHINE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + label: string +implement Named for Item: + function name(view self: Item) returns string: + return self.label +machine Session: + states: + active(item: Named) + cached(item: Named) + empty +machine Other: + states: + active(item: Named) +function exercise(source: Session, other: Session, foreign: Other) returns string: + if source at active: + return Named.name(view source.item) + return "empty" +machine TaskState: + states: + ready(value: nothing) + cached(value: nothing) + empty +function pending_depth(value: nothing) returns int64: + return 7 +function copied(state: TaskState) returns int64: + if state at ready: + return pending_depth(state.value) + return -1 +"#; + +fn flow_projected_call(function: &Function) -> &Expression { + let StatementKind::If { then_block, .. } = &function.body.statements[0].kind else { + panic!("source state guard"); + }; + let StatementKind::Return(Some(value)) = &then_block.statements[0].kind else { + panic!("guarded source invocation"); + }; + value +} + +fn flow_projected_call_mut(function: &mut Function) -> &mut Expression { + let StatementKind::If { then_block, .. } = &mut function.body.statements[0].kind else { + panic!("source state guard"); + }; + let StatementKind::Return(Some(value)) = &mut then_block.statements[0].kind else { + panic!("guarded source invocation"); + }; + value +} + +fn flow_projected_field_mut(expression: &mut Expression) -> &mut Expression { + let ExpressionKind::Call { args, .. } = &mut expression.kind else { + panic!("source invocation"); + }; + let value = &mut args[0]; + fn field(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, ExpressionKind::View(_)) { + let ExpressionKind::View(inner) = &mut value.kind else { + unreachable!() + }; + return field(inner); + } + assert!(matches!(value.kind, ExpressionKind::Field { .. })); + value + } + field(value) +} + +#[test] +fn caller_packets_flow_projected_machine_calls_preserve_view_and_nothing_copy() { + use jett_typecheck::CheckedCallerEffect; + let (program, checked) = checked_source(FLOW_PROJECTED_MACHINE); + for (name, effect) in [ + ("exercise", CheckedCallerEffect::RetainBorrow), + ("copied", CheckedCallerEffect::Copy), + ] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &flow_projected_call(function).kind + else { + panic!("original source packet"); + }; + let argument = &packet.arguments[0]; + let (local, span, occurrence, storage) = argument + .source_witness() + .projection_root() + .expect("exact checked flow root"); + assert_eq!(argument.effect, effect); + assert!(matches!( + checked.interner.resolve(storage), + Type::Machine(_) + )); + assert!(matches!( + checked.interner.resolve(occurrence), + Type::MachineState { .. } + )); + assert_eq!(function.locals[local.index() as usize].ty, storage); + assert!( + function + .locals + .iter() + .all(|local| local.view_source.is_none()) + ); + let value = if let ExpressionKind::View(inner) = &args[0].kind { + inner + } else { + &args[0] + }; + let ExpressionKind::Field { + base, owner_type, .. + } = &value.kind + else { + panic!("source field"); + }; + assert_eq!(*owner_type, occurrence); + assert_eq!(base.span, span); + assert!(matches!(base.kind, ExpressionKind::Local(id) if id == local)); + assert_eq!(base.ty, occurrence); + assert!( + validate_local_view_initializer( + &args[0], + local, + storage, + args[0].ty, + &checked.interner + ) + .is_err(), + "Source certificate cannot weaken the persistent alias helper" + ); + if name == "copied" { + assert_eq!(argument.actual_type, TypeInterner::NOTHING); + } + } +} + +#[test] +fn caller_packets_flow_projected_machine_rejects_sibling_state_and_root_corruption() { + let (program, mut checked) = checked_source(FLOW_PROJECTED_MACHINE); + let index = exercise_index(&program); + let function = &program.functions[index]; + let source = local_id(function, "source"); + let other = local_id(function, "other"); + let foreign = local_id(function, "foreign"); + let Type::Machine(machine) = *checked + .interner + .resolve(function.locals[source.index() as usize].ty) + else { + panic!("bare stored owner"); + }; + let Type::Machine(foreign_machine) = *checked + .interner + .resolve(function.locals[foreign.index() as usize].ty) + else { + panic!("foreign nominal owner"); + }; + let cached = checked + .interner + .resolve_machine(machine) + .state_id("cached") + .unwrap(); + let foreign_active = checked + .interner + .resolve_machine(foreign_machine) + .state_id("active") + .unwrap(); + let cached_type = checked.interner.intern(Type::MachineState { + machine, + state: cached, + }); + let foreign_type = checked.interner.intern(Type::MachineState { + machine: foreign_machine, + state: foreign_active, + }); + let stale = checked.interner.intern(Type::MachineState { + machine, + state: jett_types::MachineStateId::new(u32::MAX), + }); + for mutation in [ + "sibling", + "foreign", + "stale", + "root local", + "root span", + "field", + "endpoint", + "public origin", + ] { + let mut changed = program.clone(); + let invocation = flow_projected_call_mut(&mut changed.functions[index]); + if mutation == "public origin" { + source_packet_mut(invocation).arguments[0].origin = CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(other), + }; + } else { + let field = flow_projected_field_mut(invocation); + if mutation == "endpoint" { + field.ty = TypeInterner::BOOL; + } else { + let ExpressionKind::Field { + base, + owner_type, + field, + .. + } = &mut field.kind + else { + panic!("exact field"); + }; + match mutation { + "sibling" | "foreign" | "stale" => { + let ty = match mutation { + "sibling" => cached_type, + "foreign" => foreign_type, + _ => stale, + }; + *owner_type = ty; + base.ty = ty; + } + "root local" => base.kind = ExpressionKind::Local(other), + "root span" => base.span.start += 1, + "field" => *field = FieldId::new(1), + _ => unreachable!(), + } + } + } + assert!( + validate_program_call_ownership(&changed, &checked.interner).is_err(), + "{mutation}" + ); + } +} + +#[test] +fn caller_packets_flow_projection_metadata_visits_and_remaps_private_root() { + let (program, checked) = checked_source(FLOW_PROJECTED_MACHINE); + let function = &program.functions[exercise_index(&program)]; + let ExpressionKind::Call { ownership, .. } = &flow_projected_call(function).kind else { + panic!("source call"); + }; + let CallOwnership::Source(packet) = ownership else { + panic!("source packet"); + }; + let original = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + let mut visited = Vec::new(); + ownership.metadata_types(|ty| visited.push(ty)); + assert!(visited.contains(&original.2) && visited.contains(&original.3)); + let mut locals = Vec::new(); + ownership.metadata_local_ids(|local| locals.push(local)); + assert!(locals.contains(&original.0)); + let mut remapped = ownership.clone(); + remapped + .remap_metadata_locals(|local| Ok::<_, ()>(LocalId::new(local.index() + 1))) + .unwrap(); + let CallOwnership::Source(packet) = remapped else { + panic!("remapped packet"); + }; + let rewritten = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + assert_eq!( + rewritten, + ( + LocalId::new(original.0.index() + 1), + original.1, + original.2, + original.3 + ) + ); + assert!( + matches!(packet.arguments[0].origin, CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local) } if local == rewritten.0) + ); + validate_program_call_ownership(&program, &checked.interner).unwrap(); +} + +const ORDINARY_PROJECTED_ROOTS: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Holder: + item: Named +machine Session: + states: + active(item: Named) + empty +function ordinary(view source: Holder) returns string: + return Named.name(view source.item) +function qualified(view source: Session at active) returns string: + return Named.name(view source.item) +"#; + +#[test] +fn caller_packets_ordinary_and_declared_state_projection_roots_keep_existing_helper() { + let (program, checked) = checked_source(ORDINARY_PROJECTED_ROOTS); + for name in ["ordinary", "qualified"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let ExpressionKind::Call { + ownership: CallOwnership::Source(packet), + args, + .. + } = &returned_call(function).kind + else { + panic!("original source packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.source_witness().projection_root(), None); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + let CallerOrigin::BorrowedProjection { + source: CallerViewSource::Local(local), + } = argument.origin + else { + panic!("exact original source root"); + }; + let stored = &function.locals[local.index() as usize]; + assert!(matches!( + checked.interner.resolve(stored.ty), + Type::Struct(_) | Type::MachineState { .. } + )); + validate_local_view_initializer(&args[0], local, stored.ty, args[0].ty, &checked.interner) + .expect("ordinary stored root keeps the original stable-backing proof"); + } +} diff --git a/crates/jett_hir/src/callable_sealing_execution_tests.rs b/crates/jett_hir/src/callable_sealing_execution_tests.rs new file mode 100644 index 00000000..8be9720d --- /dev/null +++ b/crates/jett_hir/src/callable_sealing_execution_tests.rs @@ -0,0 +1,300 @@ +use super::*; +use crate::evaluated_values::{ValueContext, function_value_candidates, value_expression}; +use jett_common::SourceOrigin; +use jett_typecheck::CheckedResourceProgram; + +const WITNESS: &str = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett" +); +fn checked(release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(WITNESS, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ) +} +fn required(checked: &Arc) -> jett_comptime::ExplicitComptimeValues { + let result = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new( + jett_comptime::checked_types::CheckedExpressionTypes::from_checked_program(checked), + ), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + result.values +} +fn bound( + original: &Program, + checked: &Arc, + values: &jett_comptime::ExplicitComptimeValues, +) -> Program { + let types = &checked.checked().interner; + let mut program = original.clone(); + let functions = function_value_candidates( + &program.functions, + &checked.checked().method_value_definitions, + ); + crate::materialize_checked_reflected_values( + &mut program, + values, + types, + |value, ty, span, locals, bindings| { + value_expression( + value, + ty, + span, + &mut ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals, + bindings, + }, + ) + }, + ) + .unwrap(); + let before = program.clone(); + let conversion = + crate::interface_values::recorded_coercion_for_test(&mut program, &before, types).unwrap(); + crate::actor_handlers::conversion(&mut program, &before, types, &conversion).unwrap(); + crate::mutable_sum_backings::conversion(&mut program, &before, types, &conversion).unwrap(); + assert!( + crate::temporary_local_views::conversion(&program, &before, types, &conversion) + .unwrap() + .is_none() + ); + super::super::baked::complete_conversion(&mut program, &before).unwrap(); + assert!(crate::resource_materialization::functions_equal( + program.resource_source.execution_functions(), + &program.functions, + )); + program +} +fn baseline( + release: bool, +) -> ( + Program, + Arc, + jett_comptime::ExplicitComptimeValues, +) { + let checked = checked(release); + let values = required(&checked); + let original = crate::lower_checked_ordinary_program(&checked).unwrap(); + (bound(&original, &checked, &values), checked, values) +} +fn validated_sealed(program: &Program, checked: &Arc) { + crate::validate(program).unwrap(); + crate::validate_backend_types(program, &checked.checked().interner).unwrap(); + crate::actor_handlers::validate_program(program).unwrap(); + assert!(crate::resource_materialization::functions_equal( + program.resource_source.execution_functions(), + &program.functions, + )); +} + +#[test] +fn actual_sealing_receipt_advances_full_actor_required_execution_table_and_is_idempotent() { + for release in [false, true] { + let (mut program, checked, values) = baseline(release); + let types = &checked.checked().interner; + let original = program.resource_source.functions().to_vec(); + let before = program.clone(); + let receipt = super::super::seal_program_with_receipt(&mut program, types).unwrap(); + assert!(!receipt.0.edits.is_empty()); + assert!(receipt.0.edits.iter().all(|edit| edit.previous.is_some())); + assert!(Arc::ptr_eq(receipt.checked().unwrap(), &checked)); + assert!(same_program( + &receipt.replay(&before, types).unwrap(), + receipt.output() + )); + receipt + .authenticate_output(&before, receipt.output(), types) + .unwrap(); + let renewed = + super::super::baked::callable_sealing(receipt.output(), &before, types, &receipt) + .unwrap() + .unwrap(); + assert!(crate::resource_materialization::functions_equal( + renewed.current_functions(), + &program.functions + )); + validated_sealed(&program, &checked); + assert!(super::super::baked::same_required_cache(&program, &values)); + assert!(crate::resource_materialization::functions_equal( + &original, + program.resource_source.functions() + )); + let sealed = program.functions.clone(); + crate::complete_value_conversions(&mut program, types).unwrap(); + validated_sealed(&program, &checked); + assert!(crate::resource_materialization::functions_equal( + &sealed, + &program.functions + )); + assert!(super::super::baked::same_required_cache(&program, &values)); + } +} + +#[test] +fn actual_sealing_receipt_rejects_foreign_program_cache_arena_and_archive_identity() { + for release in [false, true] { + let (mut program, checked, values) = baseline(release); + let types = &checked.checked().interner; + let before = program.clone(); + let receipt = super::super::seal_program_with_receipt(&mut program, types).unwrap(); + validated_sealed(&program, &checked); + let (foreign, foreign_checked, _) = baseline(release); + let foreign_types = &foreign_checked.checked().interner; + assert!(!std::ptr::eq(types, foreign_types)); + assert!(receipt.replay(&before, foreign_types).is_err()); + assert!(receipt.replay(&foreign, types).is_err()); + let second_values = required(&checked); + assert!(!values.same_checked_cache(&second_values)); + let original = crate::lower_checked_ordinary_program(&checked).unwrap(); + let second = bound(&original, &checked, &second_values); + let mut different_cache = before.clone(); + different_cache.resource_source.reflected_values = second.resource_source.reflected_values; + assert!(receipt.replay(&different_cache, types).is_err()); + let mut different_required_cache = before.clone(); + different_required_cache.resource_source.required_cache = Some(Arc::new(second_values)); + assert!(receipt.replay(&different_required_cache, types).is_err()); + let mut different_execution = before.clone(); + different_execution.resource_source.install_materialized( + crate::resource_source::Materialized { + functions: before.functions.clone(), + values: Vec::new(), + required_only: Vec::new(), + }, + ); + assert!(receipt.replay(&different_execution, types).is_err()); + } +} + +#[test] +fn actual_sealing_receipt_refuses_previous_current_and_occurrence_token_splices() { + for release in [false, true] { + let (mut program, checked, _) = baseline(release); + let types = &checked.checked().interner; + let before = program.clone(); + let receipt = super::super::seal_program_with_receipt(&mut program, types).unwrap(); + validated_sealed(&program, &checked); + let mut independently_sealed = before.clone(); + let other = + super::super::seal_program_with_receipt(&mut independently_sealed, types).unwrap(); + assert_ne!(receipt.0.edits[0].current, other.0.edits[0].current); + for corruption in 0..7 { + let mut edits = receipt.0.edits.clone(); + match corruption { + 0 => edits[0].previous = None, + 1 => edits[0].current = other.0.edits[0].current.clone(), + 2 => edits[0].expression += 1, + 3 => edits[0].owner = FunctionId::new(u32::MAX), + 4 => edits[0].slot = Slot::Getter { field: 0 }, + 5 => edits.push(edits[0].clone()), + 6 => { + edits.remove(0); + } + _ => unreachable!(), + } + assert!( + produce( + &before, + receipt.output(), + types, + receipt.source_epoch().clone(), + edits + ) + .is_err(), + "corruption {corruption}" + ); + } + let mut changed_reference = receipt.0.edits.clone(); + assert!(changed_reference[0].current.data.reference.is_some()); + Arc::make_mut(&mut changed_reference[0].current.data).reference = None; + let replayed_reference = replay_edits(&before, &changed_reference).unwrap(); + assert!( + produce( + &before, + &replayed_reference, + types, + receipt.source_epoch().clone(), + changed_reference, + ) + .is_err() + ); + let wrong_epoch = Arc::new(program.functions.clone()); + assert!( + produce( + &before, + receipt.output(), + types, + wrong_epoch, + receipt.0.edits.clone() + ) + .is_err() + ); + } +} + +#[test] +fn actual_sealing_receipt_refuses_complete_unrelated_output_edits_without_publication() { + for release in [false, true] { + let (mut program, checked, _) = baseline(release); + let types = &checked.checked().interner; + let before = program.clone(); + let receipt = super::super::seal_program_with_receipt(&mut program, types).unwrap(); + validated_sealed(&program, &checked); + for corruption in 0..8 { + let mut changed = receipt.output().clone(); + let handler = changed + .functions + .iter() + .position(|function| function.actor_handler.is_some()) + .unwrap(); + match corruption { + 0 => changed.functions[0].body.span.end += 1, + 1 => changed.functions[0].capture_count += 1, + 2 => { + changed.functions[handler].locals[0].mutable = + !changed.functions[handler].locals[0].mutable + } + 3 => changed.functions.swap(0, 1), + 4 => changed.functions[handler].actor_handler = None, + 5 => { + changed + .equality_methods + .insert(TypeInterner::INT64, FunctionId::new(0)); + } + 6 => { + let mut extra = changed.functions[0].clone(); + extra.id = FunctionId::new(changed.functions.len() as u32); + changed.functions.push(extra); + } + 7 => { + changed.functions[handler].return_type = TypeInterner::NOTHING; + } + _ => unreachable!(), + } + let saved = changed.clone(); + assert!( + receipt + .authenticate_output(&before, &changed, types) + .is_err() + ); + assert!( + super::super::baked::callable_sealing(&changed, &before, types, &receipt).is_err() + ); + assert!(same_program(&changed, &saved)); + } + } +} diff --git a/crates/jett_hir/src/evaluated_builders.rs b/crates/jett_hir/src/evaluated_builders.rs new file mode 100644 index 00000000..038bcb55 --- /dev/null +++ b/crates/jett_hir/src/evaluated_builders.rs @@ -0,0 +1,247 @@ +//! Recreate evaluated construction builders using checked layouts and typed +//! runtime operations. Source computations are never re-executed here. + +use crate::{Block, Expression, ExpressionKind, HandleKind, IntrinsicId, Statement, StatementKind}; +use jett_common::Span; +use jett_comptime::{Interpreter, Value}; +use jett_types::{ReflectionFieldInfo, ReflectionTypeInfo, Type, TypeId, TypeInterner}; + +use crate::evaluated_values::{ValueContext, evaluated_intrinsic_expression, value_expression}; + +pub(crate) fn builder_expression( + value: &Value, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let Value::TypeConstruction { + type_name, + variant, + state, + fields, + } = value + else { + return Err("expected evaluated construction builder".into()); + }; + let types = context.types; + let reflection = context.reflection; + let owner = checked_type(type_name, context)?; + let info = reflection + .get_source_type_info(type_name) + .ok_or_else(|| format!("checked builder owner metadata `{type_name}` is absent"))?; + let mut arguments = vec![info.clone()]; + let (intrinsic, metadata, selected_fields, member): (_, _, Vec, _) = + match (variant, state) { + (Some(name), None) => { + let variants = reflection + .get_type_variants(type_name) + .or_else(|| reflection.get_type_variants_for_id(owner)) + .ok_or("checked builder variant metadata is absent")?; + let selected = variants + .iter() + .find(|variant| variant.name == *name) + .ok_or("evaluated builder variant is absent from checked metadata")?; + arguments.extend(variants.iter().flat_map(|variant| { + variant.fields.iter().map(|field| field.type_info.clone()) + })); + ( + IntrinsicId::TypeConstructVariantStart, + Some(Interpreter::reflection_variant_info_value( + type_name, selected, + )), + selected.fields.clone(), + Some(name.as_str()), + ) + } + (None, Some(name)) => { + let owner_name = machine_owner(owner, types)?; + let machine = reflection + .get_machine(type_name) + .or_else(|| reflection.get_machine(owner_name)) + .ok_or("checked builder machine metadata is absent")?; + let selected = machine + .states + .iter() + .find(|state| state.name == *name) + .ok_or("evaluated builder state is absent from checked metadata")?; + arguments.extend( + machine + .states + .iter() + .flat_map(|state| state.fields.iter().map(|field| field.type_info.clone())), + ); + ( + IntrinsicId::TypeConstructMachineStart, + Some(Interpreter::reflection_machine_state_info_value( + owner_name, selected, + )), + selected.fields.clone(), + Some(name.as_str()), + ) + } + (None, None) => { + let selected = if matches!(info.kind.as_str(), "struct" | "bitfield") { + reflection + .get_type_fields(type_name) + .or_else(|| reflection.get_type_fields_for_id(owner)) + .ok_or("checked builder field metadata is absent")? + .to_vec() + } else { + Vec::new() + }; + arguments.extend(selected.iter().map(|field| field.type_info.clone())); + (IntrinsicId::TypeConstructStart, None, selected, None) + } + _ => return Err("evaluated builder selects both a variant and a state".into()), + }; + let mut args = Vec::new(); + if let Some(metadata) = metadata { + let name = if state.is_some() { + "TypeMachineState" + } else { + "TypeVariant" + }; + args.push(value_expression( + &metadata, + checked_type(name, context)?, + span, + context, + )?); + } + let mut current = evaluated_intrinsic_expression( + intrinsic, + vec![owner], + arguments, + args, + if member.is_some() { + builder_result_type(types)? + } else { + TypeInterner::TYPE_CONSTRUCTION + }, + span, + types, + )?; + if member.is_some() { + current = unwrap_builder(current, span); + } + for (index, name, stored_type, payload) in fields { + let declared_type = checked_type(stored_type, context)?; + let field = selected_fields + .get(*index) + .filter(|field| { + field.index == *index && field.name == *name + && checked_type(&field.type_name, context).ok() == Some(declared_type) + }) + .ok_or_else(|| format!("evaluated builder field `{type_name}.{name}` at {index} disagrees with checked type `{stored_type}`"))?; + let mut payload_type = declared_type; + // Record, enum, and machine builders can contain unvalidated base + // values. Finishing the builder, not baking it, validates refinements. + if !matches!(types.resolve(owner), Type::Bitfield(_)) { + while let Type::Refinement { base, .. } = types.resolve(payload_type) { + payload_type = *base; + } + } + let payload_info = if declared_type == payload_type { + field.type_info.clone() + } else { + reflection + .get_source_type_info(&types.type_name(payload_type)) + .ok_or("checked builder storage metadata is absent")? + .clone() + }; + let metadata_owner = if state.is_some() { + machine_owner(owner, types)? + } else { + type_name + }; + let metadata = Interpreter::reflection_field_info_value(metadata_owner, member, field); + let metadata = value_expression( + &metadata, + checked_type("TypeField", context)?, + span, + context, + )?; + let payload = value_expression(payload, payload_type, span, context)?; + current = unwrap_builder( + evaluated_intrinsic_expression( + IntrinsicId::TypeConstructPut, + vec![owner, payload_type], + vec![info.clone(), payload_info], + vec![current, metadata, payload], + builder_result_type(types)?, + span, + types, + )?, + span, + ); + } + Ok(current) +} + +fn checked_type(name: &str, context: &ValueContext<'_>) -> Result { + if let Some(ty) = context.reflection.source_type_id_for_name(name) { + return Ok(ty); + } + let identity = context + .reflection + .get_source_type_info(name) + .map(ReflectionTypeInfo::canonical_identity); + let mut candidates = context.types.type_ids().filter(|ty| { + context.types.type_name(*ty) == name + || identity.as_ref().is_some_and(|identity| { + context + .reflection + .get_type_info_for_id(*ty) + .is_some_and(|info| info.canonical_identity() == *identity) + }) + }); + match (candidates.next(), candidates.next()) { + (Some(ty), None) => Ok(ty), + _ => Err(format!( + "builder type `{name}` has no unambiguous checked identity" + )), + } +} + +fn machine_owner(owner: TypeId, types: &TypeInterner) -> Result<&str, String> { + match types.resolve(owner) { + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + Ok(&types.resolve_machine(*id).name) + } + _ => Err("evaluated machine builder has no checked machine owner".into()), + } +} + +fn builder_result_type(types: &TypeInterner) -> Result { + types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Result(ok, error) + if *ok == TypeInterner::TYPE_CONSTRUCTION && *error == TypeInterner::STRING) + }) + .ok_or("checked builder result type is absent".into()) +} + +fn unwrap_builder(target: Expression, span: Span) -> Expression { + Expression { + kind: ExpressionKind::Handle { + target: Box::new(target), + kind: HandleKind::Result, + error_local: None, + failure: Block { + statements: vec![Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::RuntimeFailure( + "cannot reconstruct evaluated construction builder".into(), + ), + ty: TypeInterner::NOTHING, + span, + }), + span, + }], + span, + }, + }, + ty: TypeInterner::TYPE_CONSTRUCTION, + span, + } +} diff --git a/crates/jett_hir/src/evaluated_values.rs b/crates/jett_hir/src/evaluated_values.rs new file mode 100644 index 00000000..e19d61ef --- /dev/null +++ b/crates/jett_hir/src/evaluated_values.rs @@ -0,0 +1,909 @@ +//! Shared typed reconstruction for evaluated values. Actor seeds require a private checked graph plan. +use crate::{ + Block, DeclarationKind, Expression, ExpressionKind, Function, FunctionDebugKind, FunctionId, + HandleKind, IntrinsicId, Local, LocalId, MapEntry, ParamMode, StateId, Statement, + StatementKind, VariantId, +}; +use jett_common::Span; +use jett_comptime::value::{ClosureTypeContext, Value}; +use jett_types::{ReflectionMetadata, ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use std::collections::HashSet; + +#[derive(Debug)] +pub struct FunctionValueCandidate { + scoped_type_bindings: Vec, + source_name: Option, + kind: DeclarationKind, + body_span: Span, + params: Vec, + view_params: Vec, + captures: Vec<(String, TypeId)>, + return_type: TypeId, + id: FunctionId, + type_arguments: Vec, + specialization: jett_typecheck::CheckedGenericSpecialization, +} + +impl FunctionValueCandidate { + pub fn id(&self) -> FunctionId { + self.id + } + pub fn captures(&self) -> &[(String, TypeId)] { + &self.captures + } + fn matches_type_context(&self, context: &ClosureTypeContext, types: &TypeInterner) -> bool { + self.scoped_type_bindings.len() == context.scoped_type_bindings.len() + && self + .scoped_type_bindings + .iter() + .zip(&context.scoped_type_bindings) + .all(|(expected, actual)| { + expected.name == actual.name + && types.type_name(expected.ty) == actual.canonical_name + && actual.reflection.as_ref() == Some(&expected.reflection) + }) + && self.type_arguments.len() == context.arguments.len() + && self + .type_arguments + .iter() + .zip(&context.arguments) + .all(|(ty, actual)| types.type_name(*ty) == actual.canonical_name) + && (self.specialization.type_argument_reflections.is_empty() + || (self.specialization.type_argument_reflections.len() == context.arguments.len() + && self + .specialization + .type_argument_reflections + .iter() + .zip(&context.arguments) + .all(|(expected, actual)| actual.reflection.as_ref() == Some(expected)))) + && self.matches_reflection_parameters(context) + } + + fn matches_reflection_parameters(&self, context: &ClosureTypeContext) -> bool { + let expected = &self.specialization; + match &context.checked_function { + Some(actual) => { + expected.type_info_kinds == actual.type_info_kinds + && expected.type_info_primitives == actual.type_info_primitives + && expected.type_kind_values == actual.type_kind_values + && expected.type_primitive_values == actual.type_primitive_values + } + None => { + expected.type_info_kinds.is_empty() + && expected.type_info_primitives.is_empty() + && expected.type_kind_values.is_empty() + && expected.type_primitive_values.is_empty() + } + } + } +} + +pub fn function_value_candidates( + functions: &[Function], + method_value_definitions: &HashSet, +) -> Vec { + let dispatch_names = functions + .iter() + .filter(|function| is_interface_dispatcher(function)) + .filter_map(|function| match &function.debug_kind { + FunctionDebugKind::Named(name) => Some(name.as_str()), + FunctionDebugKind::Inline => None, + }) + .collect::>(); + functions + .iter() + .filter(|function| { + matches!( + function.identity.declaration.kind, + DeclarationKind::Function + | DeclarationKind::Method + | DeclarationKind::Verify + | DeclarationKind::Property + ) && (function.identity.declaration.kind != DeclarationKind::Method + // Generated interface dispatchers have no concrete source body. + // Their reserved identity and canonical debug name identify a + // named callback returned by explicit compile-time evaluation. + || is_interface_dispatcher(function) + // An implementation for an interface owner can share its debug + // name with a declared slot; the qualified spelling selects the slot. + || (method_value_definitions.contains(&function.span) + && !matches!( + &function.debug_kind, + FunctionDebugKind::Named(name) if dispatch_names.contains(name.as_str()) + ))) + }) + .map(|function| { + let declaration = &function.identity.declaration; + let source_name = match &function.debug_kind { + FunctionDebugKind::Named(name) => Some(name.clone()), + FunctionDebugKind::Inline => None, + }; + FunctionValueCandidate { + scoped_type_bindings: function.identity.scoped_type_bindings.clone(), + type_arguments: function.identity.type_arguments.clone(), + specialization: function.identity.specialization.clone(), + source_name, + kind: declaration.kind, + body_span: function.body.span, + params: function + .params + .iter() + .skip(function.capture_count) + .map(|param| param.ty) + .collect(), + view_params: function + .params + .iter() + .skip(function.capture_count) + .map(|param| param.mode == ParamMode::View) + .collect(), + captures: function + .params + .iter() + .take(function.capture_count) + .map(|param| (param.name.clone(), param.ty)) + .collect(), + return_type: function.return_type, + id: function.id, + } + }) + .collect() +} + +fn is_interface_dispatcher(function: &Function) -> bool { + function.identity.declaration.kind == DeclarationKind::Method + && function + .identity + .declaration + .name + .starts_with("$interface.dispatch.") +} + +pub struct ValueContext<'a> { + pub actor_seeds: Option<&'a crate::actor_graph::ActorGraphSeeds>, + pub types: &'a TypeInterner, + pub reflection: &'a ReflectionMetadata, + pub functions: &'a [FunctionValueCandidate], + /// Present only when constructing unvalidated property shrink candidates. + pub refinement_functions: Option<&'a [Function]>, + pub locals: &'a mut Vec, + pub bindings: &'a mut Vec, +} + +fn erased_value_type( + value: &Value, + types: &TypeInterner, + seeds: Option<&crate::actor_graph::ActorGraphSeeds>, +) -> Option { + if let Value::Actor(seed) = value { + return seeds?.actor_type(*seed); + } + if let Value::Typed { type_name, .. } = value { + let mut candidates = types + .type_ids() + .filter(|ty| types.type_name(*ty) == *type_name); + let candidate = candidates.next()?; + return candidates.next().is_none().then_some(candidate); + } + let primitive = match value { + Value::Int64(_) => Some(TypeInterner::INT64), + Value::Uint64(_) => Some(TypeInterner::UINT64), + Value::Float64(_) => Some(TypeInterner::FLOAT64), + Value::Bool(_) => Some(TypeInterner::BOOL), + Value::String(_) => Some(TypeInterner::STRING), + Value::Bytes(_) => Some(TypeInterner::BYTES), + Value::Nothing => Some(TypeInterner::NOTHING), + Value::TypeConstruction { .. } => Some(TypeInterner::TYPE_CONSTRUCTION), + _ => None, + }; + if primitive.is_some() { + return primitive; + } + let mut candidates = types + .type_ids() + .filter(|ty| match (types.resolve(*ty), value) { + ( + Type::Struct(id), + Value::Struct { + type_name, + concrete_type, + .. + }, + ) => types.resolve_struct(*id).name == concrete_type.as_deref().unwrap_or(type_name), + (Type::Bitfield(id), Value::Struct { type_name, .. }) => { + types.resolve_bitfield(*id).name == *type_name + } + (Type::Enum(id), Value::Enum { type_name, .. }) => { + types.resolve_enum(*id).name == *type_name + } + (Type::Machine(id), Value::Machine { type_name, .. }) => { + types.resolve_machine(*id).name == *type_name + } + _ => false, + }); + let candidate = candidates.next()?; + candidates.next().is_none().then_some(candidate) +} + +fn checked_refinement_candidate( + mut value: &Value, + ty: TypeId, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let types = context.types; + let mut base_type = ty; + while let Type::Refinement { base, .. } = types.resolve(base_type) { + base_type = *base; + } + let input_type = match types.resolve(base_type) { + Type::Secret(inner) => *inner, + _ => base_type, + }; + let mut current = ty; + let mut predicates = Vec::new(); + while let Type::Refinement { name, base } = types.resolve(current) { + let mut functions = context + .refinement_functions + .unwrap() + .iter() + .filter(|function| { + let declaration = &function.identity.declaration; + let canonical = if declaration.namespace.is_empty() { + declaration.name.clone() + } else { + format!("{}.{}", declaration.namespace, declaration.name) + }; + declaration.kind == DeclarationKind::RefinementPredicate + && canonical == *name + && function.params.len() == 1 + && function.params[0].ty == input_type + }); + let function = functions.next().ok_or_else(|| { + format!("refinement '{name}' has no exact checked predicate for native replay") + })?; + if functions.next().is_some() { + return Err(format!( + "refinement '{name}' has multiple native replay predicates" + )); + } + predicates.push(crate::RefinementPredicate { + refined_type: current, + type_name: name.clone(), + function: function.id, + base_type, + input_type, + }); + if let Value::Typed { + type_name, + value: inner, + } = value + { + if type_name == name { + value = inner; + } + } + current = *base; + } + predicates.reverse(); + let target = value_expression(value, base_type, span, context)?; + Ok(Expression { + kind: ExpressionKind::Handle { + target: Box::new(target), + kind: HandleKind::Refinement { + refined_type: ty, + predicates, + }, + error_local: None, + failure: Block { + statements: vec![ + Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::PropertyCaseContext(None), + ty: TypeInterner::NOTHING, + span, + }), + span, + }, + Statement { + kind: StatementKind::Return(None), + span, + }, + ], + span, + }, + }, + ty, + span, + }) +} + +pub fn value_expression( + value: &Value, + ty: TypeId, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let types = context.types; + if context.refinement_functions.is_some() + && matches!(types.resolve(ty), Type::Refinement { .. }) + { + return checked_refinement_candidate(value, ty, span, context); + } + let kind = match (types.resolve(ty), value) { + (Type::Actor(_), Value::Actor(seed)) => { + let seeds = context + .actor_seeds + .ok_or("actor value has no private checked graph seed proof")?; + let local = seeds.ready(*seed, ty)?; + ExpressionKind::Local(local) + } + (Type::Actor(_), Value::Pending(_)) => { + let seeds = context + .actor_seeds + .ok_or("pending actor has no private checked graph seed proof")?; + ExpressionKind::Local(seeds.pending(value, ty)?) + } + (_, Value::Pending(inner)) => { + ExpressionKind::Run(Box::new(value_expression(inner, ty, span, context)?)) + } + (Type::Interface(_), _) => { + let concrete = erased_value_type(value, types, context.actor_seeds) + .ok_or("erased compile-time value has no unambiguous checked concrete type")?; + ExpressionKind::interface_coerce(Box::new(value_expression( + value, concrete, span, context, + )?)) + } + (Type::Refinement { base, .. }, Value::Typed { type_name, value }) + if types.type_name(ty) == *type_name => + { + ExpressionKind::RefinementValidated(Box::new(value_expression( + value, *base, span, context, + )?)) + } + (_, Value::Typed { value, .. }) => return value_expression(value, ty, span, context), + (Type::TypeConstruction, Value::TypeConstruction { .. }) => { + return crate::evaluated_builders::builder_expression(value, span, context); + } + ( + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64, + Value::Int64(number), + ) => ExpressionKind::Int(i128::from(*number)), + (Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Uint64(number)) => { + ExpressionKind::Int(i128::from(*number)) + } + (Type::Float32 | Type::Float64, Value::Float64(number)) => ExpressionKind::Float(*number), + (Type::String, Value::String(text)) => ExpressionKind::String(text.clone()), + (Type::Bool, Value::Bool(flag)) => ExpressionKind::Bool(*flag), + (Type::Nothing, Value::Nothing) => ExpressionKind::Nothing, + (Type::Function { .. }, Value::ResourceHook(_)) => { + return context + .actor_seeds + .ok_or("hook value has no private graph reconstructor")? + .hook(value, ty); + } + (Type::Function { .. }, Value::ReflectedCallable(adapter)) => { + if ty != adapter.requested_type() + || !std::ptr::eq(types, &adapter.checked_program().checked().interner) + || !adapter + .checked_plan() + .validate(adapter.checked_program().checked()) + { + return Err( + "generated reflected callback belongs to another checked type context".into(), + ); + } + let source = value_expression(adapter.source(), adapter.source_type(), span, context)?; + let mut expression = crate::lower_reflected_callable_value(source, adapter, span)?; + if let Some(seeds) = context.actor_seeds { + seeds.bind_reflected_source(adapter, &mut expression)?; + } + return Ok(expression); + } + (Type::Function { .. }, Value::NamedFunction(_) | Value::Function { .. }) => { + return function_value_expression(value, ty, span, context); + } + (Type::Bytes, Value::Bytes(bytes)) => return bytes_expression(bytes, span, types), + (Type::List(element), Value::List(values)) => ExpressionKind::ListConstruct { + elements: values + .iter() + .map(|value| value_expression(value, *element, span, context)) + .collect::>()?, + }, + (Type::Map(key, mapped), Value::Map(entries)) => ExpressionKind::MapConstruct { + entries: entries + .iter() + .map(|(key_value, mapped_value)| { + Ok(MapEntry { + key: value_expression(key_value, *key, span, context)?, + value: value_expression(mapped_value, *mapped, span, context)?, + }) + }) + .collect::>()?, + }, + (Type::Set(element), Value::Set(values)) => { + return set_expression(values, *element, ty, span, context); + } + (Type::Optional(_), Value::OptionalNone) => ExpressionKind::OptionalNone, + (Type::Optional(inner), Value::OptionalSome(value)) => { + ExpressionKind::OptionalSome(Box::new(value_expression(value, *inner, span, context)?)) + } + (Type::Result(ok, _), Value::ResultOk(value)) => { + ExpressionKind::ResultOk(Box::new(value_expression(value, *ok, span, context)?)) + } + (Type::Result(_, failure), Value::ResultFail(value)) => { + ExpressionKind::ResultFail(Box::new(value_expression(value, *failure, span, context)?)) + } + (Type::Secret(inner), _) => ExpressionKind::interface_coerce(Box::new(value_expression( + value, *inner, span, context, + )?)), + (Type::Refinement { base, .. }, _) => ExpressionKind::RefinementValidated(Box::new( + value_expression(value, *base, span, context)?, + )), + (Type::Struct(id), Value::Struct { fields, .. }) => { + let definition = types.resolve_struct(*id); + if fields.len() != definition.fields.len() { + return Err(format!( + "generated struct has {} fields; checked type expects {}", + fields.len(), + definition.fields.len() + )); + } + ExpressionKind::StructConstruct { + struct_type: ty, + fields: definition + .fields + .iter() + .map(|(name, field_type)| { + let (_, value) = fields + .iter() + .find(|(candidate, _)| candidate == name) + .ok_or_else(|| format!("generated struct field `{name}` is absent"))?; + value_expression(value, *field_type, span, context) + }) + .collect::>()?, + evaluation_order: (0..definition.fields.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + } + } + (Type::Bitfield(id), Value::Struct { fields, .. }) => { + let definition = types.resolve_bitfield(*id); + if fields.len() != definition.fields.len() { + return Err(format!( + "generated bitfield has {} fields; checked type expects {}", + fields.len(), + definition.fields.len() + )); + } + ExpressionKind::BitfieldConstruct { + bitfield_type: ty, + fields: definition + .fields + .iter() + .map(|field| { + let (_, value) = fields + .iter() + .find(|(name, _)| name == &field.name) + .ok_or_else(|| { + format!("generated bitfield field `{}` is absent", field.name) + })?; + value_expression(value, field.ty, span, context) + }) + .collect::>()?, + evaluation_order: (0..definition.fields.len()).collect(), + // The property generator already bounds every bitfield field. + validates_widths: false, + } + } + ( + Type::Enum(id), + Value::Enum { + variant, fields, .. + }, + ) => { + let definition = types.resolve_enum(*id); + let (index, variant_def) = definition + .variants + .iter() + .enumerate() + .find(|(_, candidate)| candidate.name == *variant) + .ok_or_else(|| format!("generated enum variant `{variant}` is absent"))?; + if fields.len() != variant_def.fields.len() { + return Err(format!( + "generated enum variant `{variant}` has wrong arity" + )); + } + let index = u32::try_from(index) + .map_err(|_| "generated enum variant index exceeds u32".to_owned())?; + ExpressionKind::EnumConstruct { + enum_type: ty, + variant: VariantId::new(index), + evaluation_order: (0..fields.len()).collect(), + payloads: fields + .iter() + .zip(&variant_def.fields) + .map(|(value, (_, field_type))| { + value_expression(value, *field_type, span, context) + }) + .collect::>()?, + } + } + ( + Type::Machine(machine) | Type::MachineState { machine, .. }, + Value::Machine { state, fields, .. }, + ) => { + let definition = types.resolve_machine(*machine); + let (index, state_def) = definition + .states + .iter() + .enumerate() + .find(|(_, candidate)| candidate.name == *state) + .ok_or_else(|| format!("generated machine state `{state}` is absent"))?; + let index = u32::try_from(index) + .map_err(|_| "generated machine state index exceeds u32".to_owned())?; + if let Type::MachineState { + state: expected, .. + } = types.resolve(ty) + && expected.index() != index + { + return Err(format!( + "generated machine state `{state}` disagrees with checked state" + )); + } + if fields.len() != state_def.fields.len() { + return Err(format!("generated machine state `{state}` has wrong arity")); + } + let state_type = if matches!(types.resolve(ty), Type::MachineState { .. }) { + ty + } else { + types + .type_ids() + .find(|candidate| { + matches!( + types.resolve(*candidate), + Type::MachineState { machine: owner, state: state_id } + if owner == machine && state_id.index() == index + ) + }) + .ok_or("checked machine state type is absent")? + }; + let constructed = Expression { + kind: ExpressionKind::MachineConstruct { + state_type, + state: StateId::new(index), + payloads: fields + .iter() + .zip(&state_def.fields) + .map(|(value, (_, field_type))| { + value_expression(value, *field_type, span, context) + }) + .collect::>()?, + }, + ty: state_type, + span, + }; + if state_type == ty { + return Ok(constructed); + } + // Construct the exact state, then apply the checked widening at a + // binding boundary. Erasure and refinement validation must see the + // requested bare machine type, not its constructor's state type. + let local = LocalId::new(context.locals.len() as u32); + context.locals.push(Local { + id: local, + name: format!("__native_baked_machine_{}", local.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }); + context.bindings.push(Statement { + kind: StatementKind::Let { + local, + value: constructed, + }, + span, + }); + return Ok(Expression { + kind: ExpressionKind::Clone(Box::new(Expression { + kind: ExpressionKind::Local(local), + ty, + span, + })), + ty, + span, + }); + } + (expected, actual) => { + return Err(format!( + "cannot materialize generated `{actual:?}` as checked `{expected:?}`" + )); + } + }; + Ok(Expression { kind, ty, span }) +} + +pub fn selected_function<'a>( + value: &Value, + expected: TypeId, + types: &TypeInterner, + functions: &'a [FunctionValueCandidate], +) -> Result<&'a FunctionValueCandidate, String> { + let candidates = functions + .iter() + .filter(|function| match value { + Value::NamedFunction(name) => { + matches!( + function.kind, + DeclarationKind::Function | DeclarationKind::Method + ) && function.source_name.as_ref() == Some(name) + && function.captures.is_empty() + } + Value::Function { + body, + captures, + type_context, + .. + } => { + function.source_name.is_none() + && function.body_span == body.span + && function.matches_type_context(type_context, types) + && function + .captures + .iter() + .all(|(name, _)| captures.contains_key(name)) + } + _ => false, + }) + .collect::>(); + let signature = |function: &FunctionValueCandidate| Type::Function { + params: function.params.clone(), + view_params: function.view_params.clone(), + return_type: function.return_type, + }; + let exact = candidates + .iter() + .copied() + .filter(|function| types.resolve(expected) == &signature(function)) + .collect::>(); + // Generic values may have several checked specializations. Preserve exact + // selection; otherwise use only an unambiguous evaluated source identity. + let matches = if exact.is_empty() { + &candidates + } else { + &exact + }; + let label = match value { + Value::NamedFunction(name) => format!("function value `{name}`"), + _ => "inline function value".into(), + }; + let function = match matches.as_slice() { + [] => return Err(format!("checked {label} is absent")), + [function] => *function, + _ => return Err(format!("checked {label} is ambiguous")), + }; + Ok(function) +} + +fn function_value_expression( + value: &Value, + expected: TypeId, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let function = selected_function(value, expected, context.types, context.functions)?; + let signature = |function: &FunctionValueCandidate| Type::Function { + params: function.params.clone(), + view_params: function.view_params.clone(), + return_type: function.return_type, + }; + let label = match value { + Value::NamedFunction(name) => format!("function value `{name}`"), + _ => "inline function value".into(), + }; + let source_signature = signature(function); + let source_type = context + .types + .type_ids() + .find(|ty| context.types.resolve(*ty) == &source_signature) + .ok_or_else(|| format!("checked {label} signature is absent"))?; + let function_id = function.id; + let capture_types = function.captures.clone(); + let mut capture_locals = Vec::with_capacity(capture_types.len()); + for (name, capture_type) in capture_types { + let Value::Function { captures, .. } = value else { + return Err("named function unexpectedly requires captures".into()); + }; + let captured = captures + .get(&name) + .ok_or_else(|| format!("checked closure capture `{name}` is absent"))?; + let expression = value_expression(captured, capture_type, span, context)?; + let id = LocalId::new(context.locals.len() as u32); + context.locals.push(Local { + id, + name: format!("__native_baked_capture_{}", id.index()), + ty: capture_type, + debug_ty: capture_type, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }); + context.bindings.push(Statement { + kind: StatementKind::Let { + local: id, + value: expression, + }, + span, + }); + capture_locals.push(id); + } + let kind = if capture_locals.is_empty() { + ExpressionKind::FunctionRef(function_id) + } else { + ExpressionKind::ClosureRef { + function: function_id, + captures: capture_locals, + } + }; + let source = Expression { + kind, + ty: source_type, + span, + }; + if source_type == expected { + Ok(source) + } else { + Ok(Expression { + kind: ExpressionKind::interface_coerce(Box::new(source)), + ty: expected, + span, + }) + } +} + +pub fn evaluated_intrinsic_expression( + intrinsic: IntrinsicId, + type_arguments: Vec, + reflection_arguments: Vec, + args: Vec, + ty: TypeId, + span: Span, + types: &TypeInterner, +) -> Result { + let parameter_types = args.iter().map(|argument| argument.ty).collect::>(); + let access = (0..args.len()) + .map(|index| { + jett_typecheck::intrinsic_operand_access(intrinsic, index, args.len()) + .ok_or_else(|| "evaluated intrinsic operand has no checked access role".to_string()) + }) + .collect::, _>>()?; + let evaluation_order = (0..args.len()).collect::>(); + let ownership = crate::CallOwnership::generated( + crate::GeneratedOperation::EvaluatedValue { intrinsic }, + &args, + ¶meter_types, + &access, + ty, + &evaluation_order, + types, + )?; + Ok(Expression { + kind: ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + refinement_predicates: Vec::new(), + field_validation: None, + args, + evaluation_order, + ownership, + }, + ty, + span, + }) +} + +fn set_expression( + values: &[Value], + element: TypeId, + ty: TypeId, + span: Span, + context: &mut ValueContext<'_>, +) -> Result { + let mut current = evaluated_intrinsic_expression( + IntrinsicId::SetNew, + vec![element], + Vec::new(), + Vec::new(), + ty, + span, + context.types, + )?; + for value in values { + let element_value = value_expression(value, element, span, context)?; + current = evaluated_intrinsic_expression( + IntrinsicId::SetAdd, + vec![element], + Vec::new(), + vec![current, element_value], + ty, + span, + context.types, + )?; + } + Ok(current) +} + +fn bytes_expression(bytes: &[u8], span: Span, types: &TypeInterner) -> Result { + let empty = evaluated_intrinsic_expression( + IntrinsicId::BytesNew, + Vec::new(), + Vec::new(), + Vec::new(), + TypeInterner::BYTES, + span, + types, + )?; + if bytes.is_empty() { + return Ok(empty); + } + let result_type = types + .type_ids() + .find(|id| { + matches!(types.resolve(*id), Type::Result(ok, failure) if *ok == TypeInterner::BYTES && *failure == TypeInterner::STRING) + }) + .ok_or("checked `result[bytes, string]` type is absent")?; + let hex = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + Ok(Expression { + kind: ExpressionKind::Handle { + target: Box::new(evaluated_intrinsic_expression( + IntrinsicId::BytesFromHex, + Vec::new(), + Vec::new(), + vec![Expression { + kind: ExpressionKind::String(hex), + ty: TypeInterner::STRING, + span, + }], + result_type, + span, + types, + )?), + kind: HandleKind::Result, + error_local: None, + failure: Block { + statements: vec![ + Statement { + kind: StatementKind::Assert { + condition: Expression { + kind: ExpressionKind::Bool(false), + ty: TypeInterner::BOOL, + span, + }, + message: None, + }, + span, + }, + Statement { + kind: StatementKind::HandleDefault(empty), + span, + }, + ], + span, + }, + }, + ty: TypeInterner::BYTES, + span, + }) +} diff --git a/crates/jett_hir/src/field_updates.rs b/crates/jett_hir/src/field_updates.rs new file mode 100644 index 00000000..10d39536 --- /dev/null +++ b/crates/jett_hir/src/field_updates.rs @@ -0,0 +1,486 @@ +//! A field-write place is minted only from the exact checked Source body. +//! The sealed shape is independent of MIR live-owner/loan validation. + +use crate::{BodyLowerer, Expression, ExpressionKind, Local, LocalId}; +use jett_parser::ast::{AssignStmt, Expr}; +use jett_typecheck::{ + CheckedAssignmentPlace, CheckedFieldUpdateResourceRequirement, validate_assignment_place_shape, +}; +use jett_types::{Type, TypeInterner}; + +/// Constructor-only identity of one actually lowered checked Source occurrence. +/// A repeated span, outer root and path do not identify a reflected body arm. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FieldUpdateOccurrence(u64); + +#[derive(Debug, Clone, PartialEq)] +pub struct FieldUpdatePlace { + occurrence: FieldUpdateOccurrence, + required_scopes: Vec, + scoped_type_bindings: Vec, + checked: CheckedAssignmentPlace, + original_span: jett_common::Span, + target: Box, + root: LocalId, +} + +impl FieldUpdatePlace { + pub fn occurrence(&self) -> FieldUpdateOccurrence { + self.occurrence + } + pub fn required_scopes(&self) -> &[crate::RequiredScopeIdentity] { + &self.required_scopes + } + pub fn scoped_type_bindings(&self) -> &[crate::ScopedTypeBinding] { + &self.scoped_type_bindings + } + pub fn root(&self) -> LocalId { + self.root + } + pub fn target(&self) -> &Expression { + self.target.as_ref() + } + pub fn span(&self) -> jett_common::Span { + self.checked.assignment_span + } + pub fn root_type(&self) -> jett_types::TypeId { + self.checked.root_type + } + pub fn projections(&self) -> &[crate::FieldUpdateProjection] { + &self.checked.projections + } + + /// Canonical shape validation grants no live-owner or loan authority. + pub fn validate( + &self, + target: &Expression, + locals: &[Local], + types: &TypeInterner, + ) -> Result<(), String> { + validate_assignment_place_shape(&self.checked, types) + .map_err(|error| format!("invalid checked field update shape: {error:?}"))?; + if self.checked.resource != CheckedFieldUpdateResourceRequirement::Ordinary { + return Err("pending Resource projected assignment transport".into()); + } + if self.original_span != self.span() || target != self.target.as_ref() { + return Err("field update changed its exact Source target".into()); + } + let root = locals + .get(self.root.index() as usize) + .filter(|local| local.id == self.root) + .ok_or("field update root is absent from its local table")?; + let declared = self.checked.root.declaration_span; + if !root.mutable + || root.view_source.is_some() + || root.ty != self.checked.root.ty + || root.span.file != declared.file + || root.span.start > declared.start + || root.span.end < declared.end + { + return Err("field update root changed its checked binding".into()); + } + let mut expression = target; + for projection in self.checked.projections.iter().rev() { + let ExpressionKind::Field { + base, + owner_type, + field, + } = &expression.kind + else { + return Err("field update path is not its exact canonical projection".into()); + }; + let owner = match types.resolve(projection.owner_type) { + Type::Secret(inner) => *inner, + _ => projection.owner_type, + }; + if base.ty != projection.owner_type + || *owner_type != owner + || field.index() as usize != projection.field_index + || expression.ty != projection.field_type + { + return Err("field update changed owner, field ordinal or qualification".into()); + } + expression = base; + } + if !matches!(expression.kind, ExpressionKind::Local(root) if root == self.root) + || expression.ty != self.root_type() + { + return Err("field update changed its checked root use".into()); + } + Ok(()) + } +} + +fn source_root(expression: &Expr) -> Option<&jett_parser::ast::Ident> { + match expression { + Expr::Ident(ident) => Some(ident), + Expr::FieldAccess(object, _, _) => source_root(object), + Expr::Paren(inner, _) => source_root(inner), + _ => None, + } +} + +pub(super) fn authenticate( + body: &mut BodyLowerer<'_, '_>, + original: &AssignStmt, + target: &Expression, +) -> Option { + let checked = body.assignment_places.get(&original.span).cloned(); + let result = (|| { + let checked = checked.ok_or("field update has no exact checked-body place")?; + if !checked.ownership_validated() { + return Err("field update lacks completed Source ownership validation"); + } + let root = + source_root(&original.target).ok_or("field update Source root is not a binding")?; + if root.span != checked.root_use_span + || body.parent.resolve.resolutions.get(&root.span) != Some(&checked.root.definition) + || body.binding_facts.get(&checked.root.declaration_span) != Some(&checked.root) + { + return Err("field update Source binding association changed"); + } + let local = body + .local_ids + .get(&checked.root.definition) + .copied() + .ok_or("field update binding has no owning HIR local")?; + let occurrence = FieldUpdateOccurrence(body.next_field_update_occurrence); + body.next_field_update_occurrence = body + .next_field_update_occurrence + .checked_add(1) + .ok_or("field update occurrence inventory exhausted")?; + let place = FieldUpdatePlace { + occurrence, + required_scopes: body.required_scopes.clone(), + scoped_type_bindings: body.scoped_type_bindings.clone(), + checked, + original_span: original.span, + target: Box::new(target.clone()), + root: local, + }; + place + .validate(target, &body.locals, &body.parent.check.interner) + .map_err(|_| "field update failed canonical checked place validation")?; + if body.view_parameter_locals.contains(&local) { + return Err("field update cannot target a view parameter"); + } + Some(place).ok_or("field update place is absent") + })(); + match result { + Ok(place) => Some(place), + Err(message) => { + body.parent.error(original.span, message); + None + } + } +} + +/// Exhaustive dependency walk of physically owned current function bodies. +/// Extracted closures and sealed graph factories retain their own identity. +pub fn field_update_places(block: &crate::Block) -> Vec<&FieldUpdatePlace> { + fn expression<'a>(value: &'a Expression, output: &mut Vec<&'a FieldUpdatePlace>) { + use crate::ExpressionKind as E; + match &value.kind { + E::Binary { left, right, .. } => { + expression(left, output); + expression(right, output); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } + | E::Field { base: value, .. } => expression(value, output), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + expression(value, output); + } + } + E::IndirectCall { callee, args, .. } => { + expression(callee, output); + for value in args { + expression(value, output); + } + } + E::ActorMessage { actor, args, .. } => { + expression(actor, output); + for value in args { + expression(value, output); + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + expression(value, output); + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + expression(value, output); + } + } + E::MachineTransition { + source, payloads, .. + } => { + expression(source, output); + for value in payloads { + expression(value, output); + } + } + E::ListConstruct { elements } => { + for value in elements { + expression(value, output); + } + } + E::MapConstruct { entries } => { + for entry in entries { + expression(&entry.key, output); + expression(&entry.value, output); + } + } + E::Handle { + target, failure, .. + } => { + expression(target, output); + walk(failure, output); + } + E::StringInterpolation(parts) => { + for part in parts { + if let crate::StringSegment::Value(value) = part { + expression(value, output); + } + } + } + E::ActorGraphImport(_) + | E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + } + fn walk<'a>(block: &'a crate::Block, output: &mut Vec<&'a FieldUpdatePlace>) { + use crate::StatementKind as S; + for statement in &block.statements { + match &statement.kind { + S::UpdateField { + target, + value, + place, + } => { + output.push(place); + expression(target, output); + expression(value, output); + } + S::Assign { target, value } => { + expression(target, output); + expression(value, output); + } + S::Let { value, .. } + | S::HandleDefault(value) + | S::Expression(value) + | S::Respond(value) => expression(value, output), + S::Return(value) => { + if let Some(value) = value { + expression(value, output); + } + } + S::If { + condition, + then_block, + else_block, + } => { + expression(condition, output); + walk(then_block, output); + if let Some(block) = else_block { + walk(block, output); + } + } + S::While { condition, body } => { + expression(condition, output); + walk(body, output); + } + S::For { iterable, body, .. } => { + expression(iterable, output); + walk(body, output); + } + S::Match { scrutinee, arms } => { + expression(scrutinee, output); + for arm in arms { + walk(&arm.body, output); + } + } + S::Assert { condition, message } => { + expression(condition, output); + if let Some(value) = message { + expression(value, output); + } + } + S::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, output); + } + } + S::Scope(body) => walk(body, output), + S::ReflectedTypeDispatch { type_info, arms } => { + expression(type_info, output); + for arm in arms { + walk(&arm.body, output); + } + } + S::Break | S::Continue | S::Trace(_) => {} + } + } + } + let mut output = Vec::new(); + walk(block, &mut output); + output +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + use std::collections::HashMap; + + const SOURCE: &str = "struct Record:\n value: int64\n sibling: int64\nfunction next_value(view record: Record) returns int64:\n return record.value + 1\nfunction update(mutable record: Record) returns Record:\n record.value = next_value(view record)\n return record\n"; + fn checked( + source: &str, + ) -> ( + jett_parser::ast::Module, + jett_resolve::ResolveResult, + jett_typecheck::CheckResult, + ) { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + (parsed.module, resolved, checked) + } + #[test] + fn source_field_place_retains_exact_same_type_sibling_ordinal() { + let (module, resolved, mut checked) = checked(SOURCE); + let origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + let high = + crate::lower(&module, &resolved, &checked, &origins).expect("checked field update HIR"); + let update = high + .functions + .iter() + .find(|f| f.identity.declaration.name == "update") + .unwrap(); + let places = field_update_places(&update.body); + assert_eq!(places.len(), 1); + assert_eq!(places[0].projections()[0].field_index, 0); + let fact = checked.assignment_places.values_mut().next().unwrap(); + fact.projections[0].field_index = 1; + assert!(!fact.ownership_validated()); + assert!(crate::lower(&module, &resolved, &checked, &origins).is_err()); + } + #[test] + fn source_field_place_walk_includes_nested_handler_bodies() { + let source = "struct Record:\n value: int64\n sibling: int64\nfunction update(mutable record: Record, incoming: optional[int64]) returns Record:\n int64 selected = incoming handle:\n mutable Record independent = Record(value: 0, sibling: 0)\n independent.value = 7\n default independent.value\n record.sibling = selected\n return record\n"; + let (module, resolved, checked) = checked(source); + let high = crate::lower( + &module, + &resolved, + &checked, + &HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + ) + .expect("nested handler field update HIR"); + let update = high + .functions + .iter() + .find(|f| f.identity.declaration.name == "update") + .unwrap(); + assert_eq!(field_update_places(&update.body).len(), 2); + } +} + +pub(super) fn checked_function_has_updates( + check: &jett_typecheck::CheckResult, + function: jett_common::Span, +) -> bool { + fn inside(child: jett_common::Span, owner: jett_common::Span) -> bool { + child.file == owner.file && child.start >= owner.start && child.end <= owner.end + } + fn body_has_updates(body: &jett_typecheck::CheckedBodyFacts) -> bool { + body.assignment_places + .values() + .any(|place| place.ownership_validated()) + || body + .comptime_type_bindings + .values() + .flatten() + .any(|row| body_has_updates(&row.body)) + } + check + .assignment_places + .iter() + .any(|(span, place)| inside(*span, function) && place.ownership_validated()) + || check.comptime_type_bindings.iter().any(|(span, rows)| { + inside(*span, function) && rows.iter().any(|row| body_has_updates(&row.body)) + }) +} + +pub(super) fn requires_original_reflected_archive(program: &crate::Program) -> bool { + program.functions.iter().any(|function| { + function.identity.declaration.kind == crate::DeclarationKind::Function + && function.source_definition.is_some() + && function.capture_count == 0 + && function.identity.type_arguments.is_empty() + && function.identity.scoped_type_bindings.is_empty() + && function.identity.specialization + == jett_typecheck::CheckedGenericSpecialization::default() + && field_update_places(&function.body).iter().any(|place| { + place.required_scopes().iter().any(|scope| { + matches!( + scope.selection, + jett_typecheck::CheckedComptimeTypeSelection::ReflectedIteration(_) + ) + }) + }) + }) +} diff --git a/crates/jett_hir/src/inline_functions.rs b/crates/jett_hir/src/inline_functions.rs new file mode 100644 index 00000000..ccef1a99 --- /dev/null +++ b/crates/jett_hir/src/inline_functions.rs @@ -0,0 +1,665 @@ +//! Extract inline functions into ordinary checked HIR functions. Captured +//! locals become leading synthetic parameters supplied by a native environment. + +use crate::{ + Block, DeclarationId, DeclarationKind, Expression, ExpressionKind, Function, FunctionDebugKind, + FunctionId, FunctionIdentity, Local, LocalId, Param, ParamMode, StatementKind, StringSegment, +}; +use jett_types::{Type, TypeInterner}; +use std::collections::HashSet; + +/// These records are created only while lowering physical original AST nodes. +/// Neither a generated name nor a same-span source copy can supply a node token. +pub(crate) struct LoweredSourceInline { + pub(crate) source_node: usize, + pub(crate) root: Option, + pub(crate) original: Expression, +} + +#[derive(Debug)] +pub(crate) struct InlineSource { + pub(crate) source_node: usize, + pub(crate) root: FunctionId, + pub(crate) function: FunctionId, +} + +#[cfg(test)] +pub(super) fn extract_inline_functions(functions: &mut Vec, types: &TypeInterner) { + extract_inline_functions_with_sources(functions, types, &[]); +} + +pub(super) fn extract_inline_functions_with_sources( + functions: &mut Vec, + types: &TypeInterner, + source_nodes: &[LoweredSourceInline], +) -> Vec { + let mut extractor = Extractor { + types, + source_nodes, + sources: Vec::new(), + next_id: functions.len() as u32, + pending: Vec::new(), + detached_aliases: HashSet::new(), + generated_shape_rewrites: Vec::new(), + }; + let mut index = 0; + while index < functions.len() { + let function = &mut functions[index]; + let root = extractor + .sources + .iter() + .find(|source| source.function == function.id) + .map_or(function.id, |source| source.root); + let parent = ParentInfo { + root, + identity: &function.identity, + locals: &function.locals, + mutable_sum_backings: &function.mutable_sum_backings, + }; + extractor.detached_aliases.clear(); + extractor.generated_shape_rewrites.clear(); + extractor.block(&mut function.body, &parent); + // Only bodies actually transferred into extracted functions can leave + // stale origins here. Arbitrary orphan metadata must remain an error. + for &alias in &extractor.detached_aliases { + if !block_mentions_local(&function.body, alias.index(), true) { + function.locals[alias.index() as usize].view_source = None; + } + } + function + .mutable_sum_backings + .retain(|proof| block_mentions_local(&function.body, proof.alias.index(), true)); + functions.append(&mut extractor.pending); + index += 1; + } + extractor.sources +} + +struct ParentInfo<'a> { + root: FunctionId, + identity: &'a FunctionIdentity, + locals: &'a [Local], + mutable_sum_backings: &'a [crate::MutableSumBacking], +} + +struct Extractor<'a> { + types: &'a TypeInterner, + source_nodes: &'a [LoweredSourceInline], + sources: Vec, + next_id: u32, + pending: Vec, + detached_aliases: HashSet, + generated_shape_rewrites: Vec, +} + +impl Extractor<'_> { + fn block(&mut self, block: &mut Block, parent: &ParentInfo<'_>) { + for statement in &mut block.statements { + match &mut statement.kind { + StatementKind::Let { value, .. } + | StatementKind::HandleDefault(value) + | StatementKind::Expression(value) + | StatementKind::Respond(value) => self.expression(value, parent), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + self.expression(target, parent); + self.expression(value, parent); + } + StatementKind::Return(value) => { + if let Some(value) = value { + self.expression(value, parent); + } + } + StatementKind::If { + condition, + then_block, + else_block, + } => { + self.expression(condition, parent); + self.block(then_block, parent); + if let Some(else_block) = else_block { + self.block(else_block, parent); + } + } + StatementKind::While { condition, body } => { + self.expression(condition, parent); + self.block(body, parent); + } + StatementKind::For { iterable, body, .. } => { + self.expression(iterable, parent); + self.block(body, parent); + } + StatementKind::Match { scrutinee, arms } => { + self.expression(scrutinee, parent); + for arm in arms { + self.block(&mut arm.body, parent); + } + } + StatementKind::Assert { condition, message } => { + self.expression(condition, parent); + if let Some(message) = message { + self.expression(message, parent); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + self.expression(condition, parent); + } + } + StatementKind::Scope(body) => self.block(body, parent), + StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info, parent); + for arm in arms { + self.block(&mut arm.body, parent); + } + } + StatementKind::Break | StatementKind::Continue | StatementKind::Trace(_) => {} + } + } + } + + fn expression(&mut self, expression: &mut Expression, parent: &ParentInfo<'_>) { + if let ExpressionKind::InlineFunction { + scoped_type_bindings, + params, + view_params, + local_floor, + body, + } = &expression.kind + { + let captures = (0..*local_floor) + .filter(|id| block_uses_local(body, *id)) + .map(LocalId::new) + .collect::>(); + let Type::Function { + params: expected, + view_params: expected_views, + return_type, + } = self.types.resolve(expression.ty) + else { + return; + }; + if params.len() != expected.len() + || params.len() != expected_views.len() + || params + .iter() + .zip(expected_views) + .any(|(id, view)| view_params.contains(id) != *view) + { + return; + } + let parameters = params + .iter() + .zip(expected) + .map(|(id, expected_ty)| { + let local = parent.locals.get(id.index() as usize)?; + (local.id == *id && local.ty == *expected_ty).then(|| Param { + local: *id, + name: local.name.clone(), + ty: local.ty, + mode: if view_params.contains(id) { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: local.mutable, + span: local.span, + }) + }) + .collect::>>(); + let Some(parameters) = parameters else { + return; + }; + let capture_parameters = captures + .iter() + .map(|id| { + let local = parent.locals.get(id.index() as usize)?; + (local.id == *id).then(|| Param { + local: *id, + name: local.name.clone(), + ty: local.ty, + mode: ParamMode::Owned, + mutable: local.mutable, + span: local.span, + }) + }) + .collect::>>(); + let Some(mut capture_parameters) = capture_parameters else { + return; + }; + let original_shape = expression.clone(); + let capture_count = capture_parameters.len(); + capture_parameters.extend(parameters); + let id = FunctionId(self.next_id); + self.next_id += 1; + let sources = self + .source_nodes + .iter() + .filter(|source| { + source.root == Some(parent.root) + && crate::resource_materialization::expression_equal( + &source.original, + &original_shape, + ) + }) + .collect::>(); + if let [source] = sources.as_slice() { + self.sources.push(InlineSource { + source_node: source.source_node, + root: parent.root, + function: id, + }); + } + let name = format!( + "{}$inline{}_{}", + parent.identity.declaration.name, expression.span.start, expression.span.end + ); + let mut locals = parent.locals.to_vec(); + self.detached_aliases + .extend(parent.locals.iter().filter_map(|local| { + (local.view_source.is_some() + && local.id.index() >= *local_floor + && block_mentions_local(body, local.id.index(), true)) + .then_some(local.id) + })); + // Captures are independent owned environment entries. Their local + // metadata must not retain an origin in the enclosing function. + for capture in &captures { + locals[capture.index() as usize].view_source = None; + } + // The copied dense table includes parent and sibling bodies. Their + // origins do not belong to this newly created function context. + for local in &mut locals { + if local.view_source.is_some() + && !block_mentions_local(body, local.id.index(), true) + { + local.view_source = None; + } + } + self.pending.push(Function { + id, + identity: FunctionIdentity { + declaration: DeclarationId { + origin: parent.identity.declaration.origin.clone(), + namespace: parent.identity.declaration.namespace.clone(), + name, + kind: match parent.identity.declaration.kind { + DeclarationKind::Verify | DeclarationKind::Property => { + parent.identity.declaration.kind + } + _ => DeclarationKind::Function, + }, + }, + scoped_type_bindings: scoped_type_bindings.clone(), + type_arguments: parent.identity.type_arguments.clone(), + specialization: parent.identity.specialization.clone(), + }, + debug_kind: FunctionDebugKind::Inline, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: parent + .mutable_sum_backings + .iter() + .filter(|proof| block_mentions_local(body, proof.alias.index(), true)) + .cloned() + .collect(), + params: capture_parameters, + capture_count, + return_type: *return_type, + locals, + body: body.clone(), + span: expression.span, + }); + expression.kind = if captures.is_empty() { + ExpressionKind::FunctionRef(id) + } else { + ExpressionKind::ClosureRef { + function: id, + captures, + } + }; + if let Some(record) = + crate::call_ownership::generated_inline_extraction(&original_shape, expression) + { + self.generated_shape_rewrites.push(record); + } + return; + } + match &mut expression.kind { + // A sealed import is rewritten only by its dedicated constructor. + ExpressionKind::ActorGraphImport(_) => {} + ExpressionKind::Binary { left, right, .. } => { + self.expression(left, parent); + self.expression(right, parent); + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::StateIs { value, .. } + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::Field { base: value, .. } + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => self.expression(value, parent), + ExpressionKind::Call { + args, ownership, .. + } + | ExpressionKind::Intrinsic { + args, ownership, .. + } + | ExpressionKind::ResourceInvoke { + args, ownership, .. + } => { + for argument in args { + self.expression(argument, parent); + } + ownership.apply_generated_inline_extractions(&self.generated_shape_rewrites); + } + ExpressionKind::ActorSpawn { args, .. } + | ExpressionKind::ActorGraphStep { args, .. } => { + for argument in args { + self.expression(argument, parent); + } + } + ExpressionKind::IndirectCall { + callee, + args, + ownership, + .. + } => { + self.expression(callee, parent); + for argument in args { + self.expression(argument, parent); + } + ownership.apply_generated_inline_extractions(&self.generated_shape_rewrites); + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::BitfieldConstruct { fields, .. } => { + for field in fields { + self.expression(field, parent); + } + } + ExpressionKind::MachineConstruct { payloads, .. } + | ExpressionKind::EnumConstruct { payloads, .. } => { + for payload in payloads { + self.expression(payload, parent); + } + } + ExpressionKind::MachineTransition { + source, payloads, .. + } => { + self.expression(source, parent); + for payload in payloads { + self.expression(payload, parent); + } + } + ExpressionKind::ListConstruct { elements } => { + for element in elements { + self.expression(element, parent); + } + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expression(&mut entry.key, parent); + self.expression(&mut entry.value, parent); + } + } + ExpressionKind::Handle { + target, failure, .. + } => { + self.expression(target, parent); + self.block(failure, parent); + } + ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let StringSegment::Value(value) = part { + self.expression(value, parent); + } + } + } + ExpressionKind::ActorMessage { actor, args, .. } => { + self.expression(actor, parent); + for argument in args { + self.expression(argument, parent); + } + } + ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::Local(_) + | ExpressionKind::ResourceHookValue { .. } + | ExpressionKind::FunctionRef(_) + | ExpressionKind::ClosureRef { .. } + | ExpressionKind::OptionalNone + | ExpressionKind::InlineFunction { .. } => {} + } + } +} + +pub(crate) fn body_uses_local(block: &Block, target: LocalId) -> bool { + block_uses_local(block, target.index()) +} + +fn block_uses_local(block: &Block, target: u32) -> bool { + block_mentions_local(block, target, false) +} + +fn block_mentions_local(block: &Block, target: u32, include_bindings: bool) -> bool { + block + .statements + .iter() + .any(|statement| match &statement.kind { + StatementKind::Let { local, value } => { + (include_bindings && local.index() == target) + || expression_mentions_local(value, target, include_bindings) + } + StatementKind::HandleDefault(value) + | StatementKind::Expression(value) + | StatementKind::Respond(value) => { + expression_mentions_local(value, target, include_bindings) + } + StatementKind::Assign { + target: place, + value, + } + | StatementKind::UpdateField { + target: place, + value, + .. + } => { + expression_mentions_local(place, target, include_bindings) + || expression_mentions_local(value, target, include_bindings) + } + StatementKind::Return(value) => value + .as_ref() + .is_some_and(|value| expression_mentions_local(value, target, include_bindings)), + StatementKind::If { + condition, + then_block, + else_block, + } => { + expression_mentions_local(condition, target, include_bindings) + || block_mentions_local(then_block, target, include_bindings) + || else_block + .as_ref() + .is_some_and(|block| block_mentions_local(block, target, include_bindings)) + } + StatementKind::While { condition, body } => { + expression_mentions_local(condition, target, include_bindings) + || block_mentions_local(body, target, include_bindings) + } + StatementKind::For { iterable, body, .. } => { + expression_mentions_local(iterable, target, include_bindings) + || block_mentions_local(body, target, include_bindings) + } + StatementKind::Match { scrutinee, arms } => { + expression_mentions_local(scrutinee, target, include_bindings) + || arms + .iter() + .any(|arm| block_mentions_local(&arm.body, target, include_bindings)) + } + StatementKind::Assert { condition, message } => { + expression_mentions_local(condition, target, include_bindings) + || message.as_ref().is_some_and(|message| { + expression_mentions_local(message, target, include_bindings) + }) + } + StatementKind::Trace(local) => local.index() == target, + StatementKind::Breakpoint { + condition, + bindings, + } => { + condition.as_ref().is_some_and(|condition| { + expression_mentions_local(condition, target, include_bindings) + }) || bindings.iter().any(|binding| binding.index() == target) + } + StatementKind::Scope(body) => block_mentions_local(body, target, include_bindings), + StatementKind::ReflectedTypeDispatch { type_info, arms } => { + expression_mentions_local(type_info, target, include_bindings) + || arms + .iter() + .any(|arm| block_mentions_local(&arm.body, target, include_bindings)) + } + StatementKind::Break | StatementKind::Continue => false, + }) +} + +fn expression_mentions_local(expression: &Expression, target: u32, include_bindings: bool) -> bool { + if include_bindings { + if let ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } = &expression.kind + { + let mut mentioned = false; + ownership.metadata_local_ids(|local| mentioned |= local.index() == target); + if mentioned { + return true; + } + } + } + match &expression.kind { + ExpressionKind::ActorGraphImport(plan) => { + block_mentions_local(plan.body(), target, include_bindings) + } + ExpressionKind::Local(local) => local.index() == target, + ExpressionKind::Binary { left, right, .. } => { + expression_mentions_local(left, target, include_bindings) + || expression_mentions_local(right, target, include_bindings) + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::StateIs { value, .. } + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::Field { base: value, .. } + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => { + expression_mentions_local(value, target, include_bindings) + } + ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } + | ExpressionKind::Intrinsic { args, .. } + | ExpressionKind::ActorSpawn { args, .. } + | ExpressionKind::ActorGraphStep { args, .. } => args + .iter() + .any(|arg| expression_mentions_local(arg, target, include_bindings)), + ExpressionKind::IndirectCall { callee, args, .. } => { + expression_mentions_local(callee, target, include_bindings) + || args + .iter() + .any(|arg| expression_mentions_local(arg, target, include_bindings)) + } + ExpressionKind::StructConstruct { fields, .. } + | ExpressionKind::BitfieldConstruct { fields, .. } => fields + .iter() + .any(|field| expression_mentions_local(field, target, include_bindings)), + ExpressionKind::MachineConstruct { payloads, .. } + | ExpressionKind::EnumConstruct { payloads, .. } => payloads + .iter() + .any(|payload| expression_mentions_local(payload, target, include_bindings)), + ExpressionKind::MachineTransition { + source, payloads, .. + } => { + expression_mentions_local(source, target, include_bindings) + || payloads + .iter() + .any(|payload| expression_mentions_local(payload, target, include_bindings)) + } + ExpressionKind::ListConstruct { elements } => elements + .iter() + .any(|element| expression_mentions_local(element, target, include_bindings)), + ExpressionKind::MapConstruct { entries } => entries.iter().any(|entry| { + expression_mentions_local(&entry.key, target, include_bindings) + || expression_mentions_local(&entry.value, target, include_bindings) + }), + ExpressionKind::Handle { + target: handled, + failure, + .. + } => { + expression_mentions_local(handled, target, include_bindings) + || block_mentions_local(failure, target, include_bindings) + } + ExpressionKind::StringInterpolation(parts) => parts.iter().any(|part| match part { + StringSegment::Text(_) => false, + StringSegment::Value(value) => { + expression_mentions_local(value, target, include_bindings) + } + }), + ExpressionKind::InlineFunction { body, .. } => { + block_mentions_local(body, target, include_bindings) + } + ExpressionKind::ClosureRef { captures, .. } => { + captures.iter().any(|local| local.index() == target) + } + ExpressionKind::ActorMessage { actor, args, .. } => { + expression_mentions_local(actor, target, include_bindings) + || args + .iter() + .any(|arg| expression_mentions_local(arg, target, include_bindings)) + } + ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::ResourceHookValue { .. } + | ExpressionKind::FunctionRef(_) + | ExpressionKind::OptionalNone => false, + } +} diff --git a/crates/jett_hir/src/interface_values.rs b/crates/jett_hir/src/interface_values.rs new file mode 100644 index 00000000..fa428286 --- /dev/null +++ b/crates/jett_hir/src/interface_values.rs @@ -0,0 +1,1152 @@ +//! Preserve checked interface boundaries before ownership and CFG lowering. +use super::*; + +#[path = "value_conversion_receipt.rs"] +mod conversion_receipt; +pub(super) use conversion_receipt::CanonicalValueConversion; + +impl Lowerer<'_> { + pub(super) fn collect_interface_dispatches(&mut self) -> Vec { + let mut slots = Vec::new(); + fn calls(slots: &mut Vec<(TypeId, usize)>, facts: &HashMap) { + slots.extend( + facts + .values() + .map(|call| (call.interface_type, call.method_index)), + ); + } + fn values(slots: &mut Vec<(TypeId, usize)>, facts: &HashMap) { + slots.extend(facts.values().filter_map(|value| match value { + CheckedMethodValue::Source { .. } => None, + CheckedMethodValue::Interface { + interface_type, + method_index, + } => Some((*interface_type, *method_index)), + })); + } + fn bindings( + slots: &mut Vec<(TypeId, usize)>, + facts: &HashMap>, + ) { + for binding in facts.values().flatten() { + calls(slots, &binding.body.interface_calls); + values(slots, &binding.body.method_values); + bindings(slots, &binding.body.comptime_type_bindings); + } + } + calls(&mut slots, &self.check.interface_calls); + values(&mut slots, &self.check.method_values); + bindings(&mut slots, &self.check.comptime_type_bindings); + for body in &self.check.generic_function_instantiations { + calls(&mut slots, &body.interface_calls); + values(&mut slots, &body.method_values); + bindings(&mut slots, &body.comptime_type_bindings); + } + let first = self.functions.len() + + self.actor_constructors.len() + + self.actor_handlers.len() + + self.refinement_sources.len(); + let mut functions = Vec::new(); + slots.sort_by_key(|(owner, method)| (owner.index(), *method)); + slots.dedup(); + for (owner, method_index) in slots { + let Type::Interface(interface) = self.check.interner.resolve(owner) else { + self.error( + self.module.span, + "checked interface slot has a non-interface owner", + ); + continue; + }; + let interface = self.check.interner.resolve_interface(*interface); + let Some((&definition, _)) = + self.check.definition_types.iter().find(|(definition, ty)| { + **ty == owner + && self.resolve.scope_table.def(**definition).kind == DefKind::Interface + }) + else { + self.error( + self.module.span, + "checked interface slot has no resolved declaration", + ); + continue; + }; + let declaration = self.resolve.scope_table.def(definition); + let span = declaration.span; + let Some(method) = interface.methods.get(method_index) else { + self.error(span, "checked interface method slot is out of range"); + continue; + }; + let Some(origin) = self.origins.get(&span.file).cloned() else { + self.error(span, "checked interface declaration has no source origin"); + continue; + }; + let id = FunctionId((first + functions.len()) as u32); + self.function_ids.insert( + FunctionKey::Interface { + owner, + method: method_index, + }, + id, + ); + let params = method + .params + .iter() + .enumerate() + .map(|(index, (name, ty, view))| Param { + local: LocalId(index as u32), + name: name.clone(), + ty: *ty, + mode: if *view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: false, + span, + }) + .collect::>(); + let locals = params + .iter() + .map(|param| Local { + id: param.local, + name: param.name.clone(), + ty: param.ty, + debug_ty: param.ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }) + .collect(); + let mut statements = Vec::new(); + for implementation in &self.check.method_definitions { + if implementation.interface_type != Some(owner) + || implementation.method_name != method.name + { + continue; + } + let Some(&target) = self.function_ids.get(&FunctionKey::Method { + source_span: implementation.source_span, + }) else { + self.error( + implementation.source_span, + "checked interface implementation has no native function", + ); + continue; + }; + let receiver = Expression { + kind: ExpressionKind::Local(LocalId(0)), + ty: owner, + span, + }; + let condition = Expression { + kind: ExpressionKind::Binary { + left: Box::new(Expression { + kind: ExpressionKind::InterfaceType(Box::new(receiver)), + ty: TypeInterner::UINT64, + span, + }), + op: BinaryOp::Equal, + right: Box::new(Expression { + kind: ExpressionKind::Int(implementation.owner_type.index() as i128), + ty: TypeInterner::UINT64, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }; + let args = params + .iter() + .zip(&implementation.parameter_types) + .map(|(param, ty)| { + let mut value = Expression { + kind: ExpressionKind::Local(param.local), + ty: param.ty, + span, + }; + coerce(&mut value, *ty, &self.check.interner); + value + }) + .collect::>(); + let access = params + .iter() + .map(|param| { + if param.mode == ParamMode::View { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let ownership = match crate::CallOwnership::generated( + crate::GeneratedOperation::InterfaceDispatch { + function: target, + interface_type: owner, + method_index, + }, + &args, + &implementation.parameter_types, + &access, + implementation.return_type, + &(0..args.len()).collect::>(), + &self.check.interner, + ) { + Ok(ownership) => ownership, + Err(message) => { + self.error(span, message); + continue; + } + }; + let value = Expression { + kind: ExpressionKind::Call { + ownership, + function: target, + evaluation_order: (0..args.len()).collect(), + args, + }, + ty: implementation.return_type, + span, + }; + statements.push(Statement { + kind: StatementKind::If { + condition, + then_block: Block { + statements: vec![Statement { + kind: StatementKind::Return(Some(value)), + span, + }], + span, + }, + else_block: None, + }, + span, + }); + } + let name = format!("{}.{}", interface.name, method.name); + statements.push(Statement { + kind: StatementKind::Return(Some(Expression { + kind: ExpressionKind::RuntimeFailure(format!("undefined function '{name}'")), + ty: method.return_type, + span, + })), + span, + }); + functions.push(Function { + id, + identity: FunctionIdentity { + declaration: DeclarationId { + origin, + namespace: declaration.namespace.clone().unwrap_or_default(), + name: format!("$interface.dispatch.{name}"), + kind: DeclarationKind::Method, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }, + debug_kind: FunctionDebugKind::Named(name), + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params, + capture_count: 0, + return_type: method.return_type, + locals, + body: Block { statements, span }, + span, + }); + } + functions + } +} + +pub(super) fn contains_erased_boundary(types: &TypeInterner, ty: TypeId) -> bool { + match types.resolve(ty) { + Type::Interface(_) => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => contains_erased_boundary(types, *inner), + Type::Map(a, b) | Type::Result(a, b) => { + contains_erased_boundary(types, *a) || contains_erased_boundary(types, *b) + } + Type::Function { .. } => true, + _ => false, + } +} + +pub(super) fn representation_type(types: &TypeInterner, mut ty: TypeId) -> TypeId { + while let Type::Secret(inner) | Type::Refinement { base: inner, .. } = types.resolve(ty) { + ty = *inner; + } + ty +} + +fn is_secret(types: &TypeInterner, mut ty: TypeId) -> bool { + loop { + match types.resolve(ty) { + Type::Secret(_) => return true, + Type::Refinement { base, .. } => ty = *base, + _ => return false, + } + } +} + +/// A contextual empty/absent value can fill only bottom slots. Rebuilding the +/// container is required even without interfaces: its element ownership flags +/// may change, and a nested callable still needs its checked adapter. +fn never_slots_compatible(types: &TypeInterner, source: TypeId, target: TypeId) -> bool { + if source == target || source == TypeInterner::NEVER { + return true; + } + match (types.resolve(source), types.resolve(target)) { + (Type::List(source), Type::List(target)) + | (Type::Optional(source), Type::Optional(target)) + | (Type::Secret(source), Type::Secret(target)) => { + never_slots_compatible(types, *source, *target) + } + (Type::Map(source_key, source_value), Type::Map(target_key, target_value)) + | (Type::Result(source_key, source_value), Type::Result(target_key, target_value)) => { + never_slots_compatible(types, *source_key, *target_key) + && never_slots_compatible(types, *source_value, *target_value) + } + (_, Type::Secret(target)) => never_slots_compatible(types, source, *target), + _ => false, + } +} + +pub(crate) fn coerce(value: &mut Expression, expected: TypeId, types: &TypeInterner) { + if value.ty == expected + || value.ty == TypeInterner::NEVER + // Pure calls retain secret arguments so their result stays tainted. + || (is_secret(types, value.ty) && !is_secret(types, expected) + && !matches!(types.resolve(expected), Type::Interface(_))) + || !(contains_erased_boundary(types, expected) + || contains_erased_boundary(types, value.ty) + || never_slots_compatible(types, value.ty, expected) + || representation_type(types, expected) == representation_type(types, value.ty)) + { + return; + } + let placeholder = Expression { + kind: ExpressionKind::Nothing, + ty: expected, + span: value.span, + }; + let inner = std::mem::replace(value, placeholder); + value.kind = ExpressionKind::interface_coerce(Box::new(inner)); +} + +pub(super) fn coerce_program(program: &mut Program, types: &TypeInterner) { + let mut signatures = program + .functions + .iter() + .map(|function| function.params.iter().map(|param| param.ty).collect()) + .collect::>>(); + let adapters = std::cell::RefCell::new(Adapters { + next: program.functions.len() as u32, + ids: program + .functions + .iter() + .filter_map(|function| { + if !function + .identity + .declaration + .name + .starts_with("$interface.adapter.") + || function.capture_count != 1 + || function.params.is_empty() + { + return None; + } + let signature = Type::Function { + params: function.params[1..].iter().map(|param| param.ty).collect(), + view_params: function.params[1..] + .iter() + .map(|param| param.mode == ParamMode::View) + .collect(), + return_type: function.return_type, + }; + let target = types + .type_ids() + .find(|ty| types.resolve(*ty) == &signature)?; + Some(((function.params[0].ty, target), function.id)) + }) + .collect(), + pending: Vec::new(), + }); + let mut index = 0; + while index < program.functions.len() { + let original_functions = program.functions.clone(); + let function = &mut program.functions[index]; + let locals = function + .locals + .iter() + .map(|local| local.ty) + .collect::>(); + let pass = Coercions { + types, + signatures: &signatures, + locals: &locals, + return_type: function.return_type, + identity: &function.identity, + adapters: &adapters, + functions: &original_functions, + }; + pass.block(&mut function.body, None); + let pending = std::mem::take(&mut adapters.borrow_mut().pending); + signatures.extend(pending.iter().map(|function| { + function + .params + .iter() + .map(|param| param.ty) + .collect::>() + })); + program.functions.extend(pending); + index += 1; + } +} + +/// Complete checked conversions introduced by generated values, such as explicit +/// comptime results and property inputs. Existing adapters are reused, and every +/// new adapter body passes through the same conversion and validation pipeline. +pub fn complete_value_conversions( + program: &mut Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut candidate = program.clone(); + complete_value_conversions_inner(&mut candidate, types)?; + *program = candidate; + Ok(()) +} + +fn complete_value_conversions_inner( + program: &mut Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let before = program.clone(); + super::reflected_callable::baked::validate_conversion_input(&before)?; + super::actor_graph::validate_conversion_input(&before)?; + super::native_suite_transition::validate_conversion_input(&before).map_err(|message| { + vec![LowerError { + span: before.functions[0].span, + message, + }] + })?; + let archive = before.resource_source.clone(); + let graphs = archive + .required_materializations() + .iter() + .any(|row| row.proof.actor_graph().is_some()); + if graphs { + archive + .validate_current(program, types) + .map_err(|message| { + vec![LowerError { + span: program.functions[0].span, + message, + }] + })?; + } + let conversion = conversion_receipt::coerce(program, &before, types)?; + super::actor_handlers::conversion(program, &before, types, &conversion)?; + super::mutable_sum_backings::conversion(program, &before, types, &conversion)?; + let temporary_execution = + super::temporary_local_views::conversion(program, &before, types, &conversion)?; + let native_suites = + super::native_suite_transition::converted(program, &before).map_err(|message| { + vec![LowerError { + span: before.functions[0].span, + message, + }] + })?; + // Each private transition authenticates the same unchanged archive and + // exact post-conversion functions before their renewed views are joined. + let mut graph_converted = program.clone(); + super::actor_graph::complete_conversion(&mut graph_converted, &before)?; + super::reflected_callable::baked::complete_conversion(program, &before)?; + program.resource_source.graph_values = graph_converted.resource_source.graph_values; + if let Some(view) = temporary_execution { + program.resource_source.install_materialized(view); + } + if native_suites.is_some() { + program.resource_source.native_suites = native_suites; + } + super::reflected_callable::seal_program(program, types)?; + if graphs { + program + .resource_source + .install_materialized(crate::resource_source::Materialized { + functions: program.functions.clone(), + values: archive.required_materializations().to_vec(), + required_only: archive.required_only_function_ids().to_vec(), + }); + } + validate(program) + .and_then(|()| validate_backend_types(program, types)) + .map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect() + }) +} + +#[cfg(test)] +pub(super) fn recorded_coercion_for_test<'types>( + program: &mut Program, + before: &Program, + types: &'types TypeInterner, +) -> Result, Vec> { + conversion_receipt::coerce(program, before, types) +} + +// The private Graph factory binds its exact checked recipe transition before +// conversion. Empty archives supply no callback construction authority. +pub(crate) fn complete_graph_value_conversions( + program: &mut Program, + types: &TypeInterner, +) -> Result<(), Vec> { + complete_value_conversions(program, types) +} + +struct Adapters { + next: u32, + ids: HashMap<(TypeId, TypeId), FunctionId>, + pending: Vec, +} + +impl Adapters { + fn function( + &mut self, + source: TypeId, + target: TypeId, + types: &TypeInterner, + identity: &FunctionIdentity, + span: Span, + ) -> Option { + if let Some(id) = self.ids.get(&(source, target)) { + return Some(*id); + } + let Type::Function { + return_type: source_return, + params: source_params, + view_params: source_views, + } = types.resolve(source) + else { + return None; + }; + let Type::Function { + params: target_params, + view_params, + return_type, + } = types.resolve(target) + else { + return None; + }; + let id = FunctionId(self.next); + self.next += 1; + let mut params = vec![Param { + local: LocalId(0), + name: "$source".into(), + ty: source, + mode: ParamMode::Owned, + mutable: false, + span, + }]; + params.extend(target_params.iter().zip(view_params).enumerate().map( + |(index, (ty, view))| Param { + local: LocalId(index as u32 + 1), + name: format!("$argument{index}"), + ty: *ty, + mode: if *view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: false, + span, + }, + )); + let locals = params + .iter() + .map(|param| Local { + id: param.local, + name: param.name.clone(), + ty: param.ty, + debug_ty: param.ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }) + .collect(); + let args = params[1..] + .iter() + .map(|param| Expression { + kind: ExpressionKind::Local(param.local), + ty: param.ty, + span, + }) + .collect::>(); + let access = source_views + .iter() + .map(|view| { + if *view { + jett_typecheck::CheckedCalleeAccess::View + } else { + jett_typecheck::CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let ownership = crate::CallOwnership::generated( + crate::GeneratedOperation::FunctionAdapter { + callee: params[0].local, + source_type: source, + target_type: target, + }, + &args, + source_params, + &access, + *source_return, + &(0..args.len()).collect::>(), + types, + ) + .ok()?; + let value = Expression { + kind: ExpressionKind::IndirectCall { + ownership, + callee: Box::new(Expression { + kind: ExpressionKind::Local(params[0].local), + ty: source, + span, + }), + evaluation_order: (0..args.len()).collect(), + args, + }, + ty: *source_return, + span, + }; + let mut identity = identity.clone(); + identity.declaration.name = + format!("$interface.adapter.{}.{}", source.index(), target.index()); + identity.declaration.kind = DeclarationKind::Function; + self.pending.push(Function { + id, + identity, + debug_kind: FunctionDebugKind::Inline, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params, + capture_count: 1, + return_type: *return_type, + locals, + body: Block { + statements: vec![Statement { + kind: StatementKind::Return(Some(value)), + span, + }], + span, + }, + span, + }); + self.ids.insert((source, target), id); + Some(id) + } +} + +struct Coercions<'a> { + types: &'a TypeInterner, + signatures: &'a [Vec], + locals: &'a [TypeId], + return_type: TypeId, + identity: &'a FunctionIdentity, + adapters: &'a std::cell::RefCell, + functions: &'a [Function], +} + +impl Coercions<'_> { + fn expected(&self, value: &mut Expression, expected: TypeId, handled: Option) { + self.expression(value, handled); + coerce(value, expected, self.types); + self.adapt(value); + } + fn adapt(&self, expression: &mut Expression) { + if let ExpressionKind::InterfaceCoerce { value, adapters } = &mut expression.kind { + if let Some(callable) = adapters + .iter_mut() + .find_map(|entry| entry.reflected.as_mut()) + { + if callable.resolve_return(self.functions).is_err() { + return; + } + let next = FunctionId(self.adapters.borrow().next); + if let Some(mut function) = + super::reflected_callable::adapter(callable, next, self.identity, self.types) + { + let id = function.id; + callable.bind(id, self.identity); + if let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: stored, .. + }, + .. + }, + .. + })) = &mut function.body.statements[0].kind + { + *stored = Some(callable.clone()); + } + let mut pending = self.adapters.borrow_mut(); + pending.next += 1; + pending.pending.push(function); + expression.kind = ExpressionKind::FunctionAdapter { + value: value.clone(), + function: id, + }; + return; + } + return; + } + } + let ExpressionKind::InterfaceCoerce { value, .. } = &expression.kind else { + return; + }; + let direct = self.adapters.borrow_mut().function( + value.ty, + expression.ty, + self.types, + self.identity, + expression.span, + ); + if let Some(function) = direct { + expression.kind = ExpressionKind::FunctionAdapter { + value: value.clone(), + function, + }; + } else if let ExpressionKind::InterfaceCoerce { value, adapters } = &mut expression.kind { + self.container_adapters(value.ty, expression.ty, expression.span, adapters); + } + } + fn container_adapters( + &self, + source: TypeId, + target: TypeId, + span: Span, + output: &mut Vec, + ) { + let source = representation_type(self.types, source); + let target = representation_type(self.types, target); + if source == target || source == TypeInterner::NEVER { + return; + } + match (self.types.resolve(source), self.types.resolve(target)) { + (Type::Function { .. }, Type::Function { .. }) => { + if output + .iter() + .any(|entry| entry.source == source && entry.target == target) + { + return; + } + if let Some(function) = self.adapters.borrow_mut().function( + source, + target, + self.types, + self.identity, + span, + ) { + output.push(InterfaceFunctionAdapter { + source, + target, + function, + reflected: None, + }); + } + } + (Type::List(a), Type::List(b)) | (Type::Optional(a), Type::Optional(b)) => { + self.container_adapters(*a, *b, span, output); + } + (Type::Map(_, a), Type::Map(_, b)) => { + self.container_adapters(*a, *b, span, output); + } + (Type::Result(a, b), Type::Result(c, d)) => { + self.container_adapters(*a, *c, span, output); + self.container_adapters(*b, *d, span, output); + } + _ => {} + } + } + fn block(&self, block: &mut Block, handled: Option) { + for statement in &mut block.statements { + match &mut statement.kind { + StatementKind::Let { local, value } => { + self.expected(value, self.locals[local.index() as usize], handled) + } + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + self.expression(target, handled); + self.expected(value, target.ty, handled); + } + StatementKind::Return(Some(value)) => { + self.expected(value, self.return_type, handled) + } + StatementKind::HandleDefault(value) => { + self.expected(value, handled.unwrap_or(value.ty), handled) + } + StatementKind::Expression(value) => self.expression(value, handled), + StatementKind::If { + condition, + then_block, + else_block, + } => { + self.expression(condition, handled); + self.block(then_block, handled); + if let Some(block) = else_block { + self.block(block, handled); + } + } + StatementKind::While { condition, body } => { + self.expression(condition, handled); + self.block(body, handled); + } + StatementKind::For { iterable, body, .. } => { + self.expression(iterable, handled); + self.block(body, handled); + } + StatementKind::Match { scrutinee, arms } => { + self.expression(scrutinee, handled); + for arm in arms { + self.block(&mut arm.body, handled); + } + } + StatementKind::Assert { condition, message } => { + self.expression(condition, handled); + if let Some(value) = message { + self.expression(value, handled); + } + } + StatementKind::Breakpoint { + condition: Some(condition), + .. + } => self.expression(condition, handled), + StatementKind::Scope(block) => self.block(block, handled), + StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info, handled); + for arm in arms { + self.block(&mut arm.body, handled); + } + } + _ => {} + } + } + } + fn machine_payloads( + &self, + ty: TypeId, + state: StateId, + values: &mut [Expression], + handled: Option, + ) { + let machine = match self.types.resolve(ty) { + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + self.types.resolve_machine(*id) + } + _ => return, + }; + if let Some(state) = machine.states.get(state.index() as usize) { + for (value, (_, expected)) in values.iter_mut().zip(&state.fields) { + self.expected(value, *expected, handled); + } + } + } + fn expression(&self, expression: &mut Expression, handled: Option) { + let ty = expression.ty; + match &mut expression.kind { + ExpressionKind::ResourceInvoke { hook, args, .. } => { + if let Type::Function { params, .. } = self.types.resolve(hook.function_type()) { + for (argument, expected) in args.iter_mut().zip(params) { + self.expected(argument, *expected, handled); + } + } + } + ExpressionKind::Call { function, args, .. } => { + for (argument, expected) in args + .iter_mut() + .zip(&self.signatures[function.index() as usize]) + { + self.expected(argument, *expected, handled); + } + } + ExpressionKind::IndirectCall { callee, args, .. } => { + self.expression(callee, handled); + if let Type::Function { params, .. } = self.types.resolve(callee.ty) { + for (argument, expected) in args.iter_mut().zip(params) { + self.expected(argument, *expected, handled); + } + } + } + ExpressionKind::StructConstruct { + struct_type, + fields, + refinement_predicates, + .. + } => { + if let Type::Struct(id) = self.types.resolve(*struct_type) { + for (index, (value, (_, expected))) in fields + .iter_mut() + .zip(&self.types.resolve_struct(*id).fields) + .enumerate() + { + if refinement_predicates + .get(index) + .is_some_and(|chain| !chain.is_empty()) + { + // MIR validates this ancestor before giving the + // field its refined type. It is not an erased box + // from which that refinement may be extracted. + self.expression(value, handled); + } else { + self.expected(value, *expected, handled); + } + } + } + } + ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + .. + } => { + if let Type::Enum(id) = self.types.resolve(*enum_type) { + for (value, (_, expected)) in payloads.iter_mut().zip( + &self.types.resolve_enum(*id).variants[variant.index() as usize].fields, + ) { + self.expected(value, *expected, handled); + } + } + } + ExpressionKind::ListConstruct { elements } => { + if let Type::List(inner) = self.types.resolve(ty) { + for value in elements { + self.expected(value, *inner, handled); + } + } + } + ExpressionKind::MapConstruct { entries } => { + if let Type::Map(key, value) = self.types.resolve(ty) { + for entry in entries { + self.expected(&mut entry.key, *key, handled); + self.expected(&mut entry.value, *value, handled); + } + } + } + ExpressionKind::ResultOk(value) => { + if let Type::Result(inner, _) = self.types.resolve(ty) { + self.expected(value, *inner, handled); + } + } + ExpressionKind::ResultFail(value) => { + if let Type::Result(_, inner) = self.types.resolve(ty) { + self.expected(value, *inner, handled); + } + } + ExpressionKind::OptionalSome(value) => { + if let Type::Optional(inner) = self.types.resolve(ty) { + self.expected(value, *inner, handled); + } + } + ExpressionKind::Handle { + target, + kind, + failure, + .. + } => { + // The checker contextualizes the success shape, while the + // original Local/Call keeps its exact producer signature. + // Bare bottom success has no payload and stays visible to MIR; + // inhabited containers need an explicit checked conversion. + let contextual = match (kind, self.types.resolve(target.ty)) { + (HandleKind::Optional, Type::Optional(success)) + if *success != ty && *success != TypeInterner::NEVER => + { + Some(Type::Optional(ty)) + } + (HandleKind::Result, Type::Result(success, error)) + if *success != ty && *success != TypeInterner::NEVER => + { + Some(Type::Result(ty, *error)) + } + _ => None, + } + .and_then(|contextual| { + self.types + .type_ids() + .find(|ty| self.types.resolve(*ty) == &contextual) + }); + if let Some(contextual) = contextual { + self.expected(target, contextual, handled); + } else { + self.expression(target, handled); + } + self.block(failure, Some(ty)); + } + ExpressionKind::Binary { left, right, .. } => { + self.expression(left, handled); + self.expression(right, handled); + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::Field { base: value, .. } + | ExpressionKind::StateIs { value, .. } + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => self.expression(value, handled), + ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let StringSegment::Value(value) = part { + self.expression(value, handled); + } + } + } + ExpressionKind::Intrinsic { args, .. } + | ExpressionKind::BitfieldConstruct { fields: args, .. } => { + for value in args { + self.expression(value, handled); + } + } + ExpressionKind::MachineConstruct { + state_type, + state, + payloads, + } => { + self.machine_payloads(*state_type, *state, payloads, handled); + } + ExpressionKind::MachineTransition { + source, + state_type, + target, + payloads, + } => { + self.expression(source, handled); + self.machine_payloads(*state_type, *target, payloads, handled); + } + ExpressionKind::ActorSpawn { + args, constructor, .. + } => { + if let Some(constructor) = constructor { + for (value, expected) in args + .iter_mut() + .zip(&self.signatures[constructor.index() as usize]) + { + self.expected(value, *expected, handled); + } + } else if let Type::Actor(id) = self.types.resolve(ty) { + let actor = self.types.resolve_actor(*id); + for (value, (_, expected)) in args + .iter_mut() + .zip(actor.capability_params.iter().chain(&actor.state_fields)) + { + self.expected(value, *expected, handled); + } + } + } + ExpressionKind::ActorMessage { + actor, + handler, + args, + .. + } => { + self.expression(actor, handled); + let signature = &self.signatures[handler.index() as usize]; + let offset = signature.len().saturating_sub(args.len()); + for (value, expected) in args.iter_mut().zip(&signature[offset..]) { + self.expected(value, *expected, handled); + } + } + _ => {} + } + self.adapt(expression); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut expression.kind + { + for plan in plans { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + if callable.canonical_adapter().is_none() { + let mut adapters = self.adapters.borrow_mut(); + let id = FunctionId(adapters.next); + if let Some(function) = super::reflected_callable::adapter( + callable, + id, + self.identity, + self.types, + ) { + adapters.next += 1; + adapters.pending.push(function); + callable.bind(id, self.identity); + } + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn never_container_conversion_is_directional_and_preserves_known_leaves() { + let mut types = TypeInterner::new(); + let empty = types.intern(Type::List(TypeInterner::NEVER)); + let strings = types.intern(Type::List(TypeInterner::STRING)); + let optional_empty = types.intern(Type::Optional(empty)); + let optional_strings = types.intern(Type::Optional(strings)); + assert!(never_slots_compatible(&types, empty, strings)); + assert!(never_slots_compatible( + &types, + optional_empty, + optional_strings + )); + assert!(!never_slots_compatible(&types, strings, empty)); + assert!(!never_slots_compatible( + &types, + TypeInterner::INT8, + TypeInterner::INT64 + )); + let empty_map = types.intern(Type::Map(TypeInterner::NEVER, TypeInterner::NEVER)); + let string_map = types.intern(Type::Map(TypeInterner::STRING, TypeInterner::STRING)); + assert!(never_slots_compatible(&types, empty_map, string_map)); + assert!(!never_slots_compatible(&types, string_map, empty_map)); + let secret_strings = types.intern(Type::Secret(strings)); + assert!(never_slots_compatible(&types, empty, secret_strings)); + assert!(!never_slots_compatible(&types, secret_strings, empty)); + let set_empty = types.intern(Type::Set(TypeInterner::NEVER)); + let set_strings = types.intern(Type::Set(TypeInterner::STRING)); + assert!(!never_slots_compatible(&types, set_empty, set_strings)); + } +} diff --git a/crates/jett_hir/src/iteration_bindings.rs b/crates/jett_hir/src/iteration_bindings.rs new file mode 100644 index 00000000..8ee00eb3 --- /dev/null +++ b/crates/jett_hir/src/iteration_bindings.rs @@ -0,0 +1,74 @@ +//! Exact source-level backing for viewed iteration bindings. +//! These are logical views, separate from ABI parameters and backend storage. +use jett_common::Span; +use jett_types::{Type, TypeId, TypeInterner}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IterationPart { + Element, + Key, + Value, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ViewIterationBinding { + loop_span: Span, + iterable_type: TypeId, + binder_type: TypeId, + part: IterationPart, +} + +impl ViewIterationBinding { + pub fn loop_span(self) -> Span { + self.loop_span + } + pub fn iterable_type(self) -> TypeId { + self.iterable_type + } + pub fn binder_type(self) -> TypeId { + self.binder_type + } + pub fn part(self) -> IterationPart { + self.part + } + pub(crate) fn matches_binding(self, ty: TypeId, declaration: Span) -> bool { + self.binder_type == ty + && declaration.file == self.loop_span.file + && declaration.start >= self.loop_span.start + && declaration.end <= self.loop_span.end + } +} + +/// Prove one endpoint of the actual checked loop. The caller separately proves +/// its exact binder LocalId, original For/ForEach association and body scope. +pub fn checked_view_iteration_binding( + types: &TypeInterner, + loop_span: Span, + iterable_type: TypeId, + binder_type: TypeId, + part: IterationPart, +) -> Result { + if iterable_type == TypeInterner::ERROR + || binder_type == TypeInterner::ERROR + || iterable_type.index() as usize >= types.len() + || binder_type.index() as usize >= types.len() + { + return Err("viewed iteration references an invalid endpoint type".into()); + } + let endpoint = match (types.resolve(iterable_type), part) { + (Type::List(element) | Type::Set(element), IterationPart::Element) => *element, + (Type::String, IterationPart::Element) => TypeInterner::STRING, + (Type::Map(key, _), IterationPart::Key) => *key, + (Type::Map(_, value), IterationPart::Value) => *value, + _ => return Err("viewed iteration binding role differs from its exact iterable".into()), + }; + if endpoint != binder_type { + return Err("viewed iteration binding type differs from its exact endpoint".into()); + } + Ok(ViewIterationBinding { + loop_span, + iterable_type, + binder_type, + part, + }) +} diff --git a/crates/jett_hir/src/lib.rs b/crates/jett_hir/src/lib.rs index 06d81b5b..3295e7ea 100644 --- a/crates/jett_hir/src/lib.rs +++ b/crates/jett_hir/src/lib.rs @@ -4,21 +4,131 @@ //! structured control flow. Unsupported source constructs fail explicitly; //! they never survive as embedded AST nodes. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use jett_common::{FileId, SourceOrigin, Span}; +pub use jett_intrinsics::IntrinsicId; use jett_parser::ast::{self, Expr, Item, Module, Stmt}; use jett_resolve::{DefId, DefKind, ResolveResult}; use jett_typecheck::{ - CheckResult, CheckedCallArgumentOrder, CheckedGenericCall, CheckedGenericFunctionInstantiation, - CheckedMethodCall, CheckedMethodDefinition, CheckedStructConstruction, + CheckResult, CheckedAssignmentPlace, CheckedBindingFact, CheckedBindingMode, CheckedBodyFacts, + CheckedCallArgumentOrder, CheckedCallOwnership, CheckedCalleeAccess, + CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, + CheckedGenericFunctionInstantiation, CheckedGenericSpecialization, CheckedInterfaceCall, + CheckedMethodCall, CheckedMethodDefinition, CheckedMethodValue, CheckedStaticSelection, + CheckedStructConstruction, CheckedViewSource, }; -use jett_types::{Type, TypeId}; +use jett_types::{ + ReflectionBitfieldFieldInfo, ReflectionBitfieldInfo, ReflectionFieldInfo, + ReflectionMachineInfo, ReflectionMachineStateInfo, ReflectionMachineTransitionInfo, + ReflectionTypeInfo, ReflectionVariantInfo, Type, TypeId, TypeInterner, +}; + +mod actor_handlers; +pub use actor_handlers::{ + ActorHandlerEntry, ActorHandlerRole, ActorMessageProof, ActorStateView, has_actor_messages, +}; +mod field_updates; +pub use field_updates::{FieldUpdateOccurrence, FieldUpdatePlace, field_update_places}; +pub use jett_typecheck::CheckedComptimeTypeSelection as FieldUpdateScopeSelection; +pub use jett_typecheck::{ + CheckedFieldOwner as FieldUpdateOwner, CheckedFieldProjection as FieldUpdateProjection, +}; +mod ordinary_required_values; +pub use ordinary_required_values::bake_required_values; +mod native_suite_transition; +pub use native_suite_transition::CheckedNativeSuiteCall; +mod native_verify_suite; +pub use native_verify_suite::append_checked_native_verify_suite; +mod native_property_suite; +pub use native_property_suite::{ + CheckedNativePropertyPlan, CheckedNativePropertyTrial, append_checked_native_property_suite, + rebuild_checked_native_property_replay, +}; +mod actor_graph; +pub use actor_graph::{ + ActorGraphCaptureBinding, ActorGraphInstruction, ActorGraphOperation, ActorGraphPlan, + ActorGraphSeeds, materialize_actor_graphs, validate_actor_graph_execution, + visit_actor_graph_factory_values, visit_actor_graph_plans, +}; +mod call_observation; +mod call_ownership; +mod evaluated_builders; +pub mod evaluated_values; +mod reflected_field_sources; +mod resource_manifest; +mod resource_materialization; +mod resource_source; +pub use reflected_field_sources::ReflectedFieldDispatchProof; +pub use resource_manifest::{ + ResourceHookRef, ResourceKind, ResourceKindId, ResourceKindRef, ResourceManifest, +}; +pub use resource_materialization::materialize_checked_required_values; +pub use resource_source::{RequiredMaterialization, ResourceSourceArchive}; +mod iteration_bindings; +mod match_bindings; +pub use match_bindings::{MatchBinding, checked_match_binding}; +#[cfg(test)] +mod borrowed_match_tests; +pub use iteration_bindings::{IterationPart, ViewIterationBinding, checked_view_iteration_binding}; +#[cfg(test)] +mod borrowed_sum_view_tests; +#[cfg(test)] +mod call_ownership_tests; +mod inline_functions; +mod interface_values; +#[cfg(test)] +mod local_view_tests; +mod local_views; +mod mutable_sum_backings; +pub use mutable_sum_backings::MutableSumBacking; +mod temporary_local_views; +pub use temporary_local_views::{ + TemporaryLocalBacking, TemporaryLocalScope, temporary_local_backings, +}; +mod reflected_callable; +mod reflected_fields; +pub use reflected_callable::{ + ReflectedCallablePlan, has_reflected_callable_plans, lower_reflected_callable_value, + materialize_checked_reflected_values, reflected_callable_plans, +}; +mod type_validation; + +pub use call_ownership::{ + ActorReceiverProof, ArgumentOwnership, ArgumentStaging, CallBridge, CallOwnership, CallTarget, + CallerBindingFact, CallerBindingMode, CallerOrigin, CallerViewSource, GeneratedAcquisition, + GeneratedArgumentOwnership, GeneratedArgumentStaging, GeneratedArgumentWitness, + GeneratedCallOwnership, GeneratedHandleShape, GeneratedOperation, GeneratedOriginalShape, + GeneratedSnapshotProof, ObservationProof, OwnershipLocalInfo, SourceArgumentWitness, + SourceCallOwnership, observation_data_type, validate_compiler_metadata_operand, + validate_generated_operand_tree, validate_hir_invocation, validate_invocation_target, + validate_operand_ownership, validate_program_call_ownership, validate_source_handled_operand, + validate_source_operand, +}; +pub use interface_values::complete_value_conversions; +pub use local_views::{ + borrowed_sum_view_initializer, is_borrowed_local, local_view_root, + validate_local_view_initializer, validate_local_views, +}; +pub use reflected_fields::{ + ReflectedFieldAction, ReflectedFieldPlan, ReflectedFieldRequirement, + ReflectedFieldRequirementPlan, ReflectedFieldUnsupported, ReflectedFieldValidation, + is_reflected_field_intrinsic, refinement_predicate_declaration_matches, reflected_field_layout, + reflected_field_requirement, valid_reflected_field_metadata_type, + validate_reflected_field_plans, +}; +pub use type_validation::validate_backend_types; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FunctionId(u32); impl FunctionId { + /// Allocate a compiler-managed function after checked HIR lowering. + pub fn new(index: u32) -> Self { + Self(index) + } + pub fn index(self) -> u32 { self.0 } @@ -28,6 +138,11 @@ impl FunctionId { pub struct LocalId(u32); impl LocalId { + /// Allocate a canonical local identity during backend-neutral lowering. + pub fn new(index: u32) -> Self { + Self(index) + } + pub fn index(self) -> u32 { self.0 } @@ -37,6 +152,10 @@ impl LocalId { pub struct FieldId(u32); impl FieldId { + pub fn new(index: u32) -> Self { + Self(index) + } + pub fn index(self) -> u32 { self.0 } @@ -46,6 +165,11 @@ impl FieldId { pub struct VariantId(u32); impl VariantId { + /// Select a checked enum variant when building compiler-owned HIR. + pub fn new(index: u32) -> Self { + Self(index) + } + pub fn index(self) -> u32 { self.0 } @@ -55,6 +179,11 @@ impl VariantId { pub struct StateId(u32); impl StateId { + /// Select a checked machine state when building compiler-owned HIR. + pub fn new(index: u32) -> Self { + Self(index) + } + pub fn index(self) -> u32 { self.0 } @@ -73,37 +202,88 @@ pub struct DeclarationId { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DeclarationKind { Function, + Verify, + Property, Method, + ActorConstructor, ActorHandler, + RefinementPredicate, } -/// One concrete in-memory function identity. Type arguments are empty for an -/// ordinary function and form part of identity after monomorphization. Raw -/// `TypeId`s are session-local; persistent artifacts must encode their -/// canonical structural type identities instead. +/// One concrete in-memory function identity. Type arguments, checked +/// reflection-visible specialization facts, and lexical comptime type bindings +/// form identity after monomorphization. All are empty for an ordinary named +/// function. Raw `TypeId`s +/// are session-local; persistent artifacts must encode their canonical +/// structural type identities and this specialization discriminator instead. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FunctionIdentity { + /// Lexical comptime type bindings, in enclosing-to-inner order. + pub scoped_type_bindings: Vec, pub declaration: DeclarationId, pub type_arguments: Vec, + pub specialization: CheckedGenericSpecialization, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ScopedTypeBinding { + pub name: String, + pub ty: TypeId, + pub reflection: jett_types::ReflectionTypeInfo, +} + +/// Exact checker-selected lexical scope of a required expression. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequiredScopeIdentity { + pub owner: Span, + pub index: usize, + pub selection: CheckedComptimeTypeSelection, } #[derive(Debug, Clone, PartialEq)] pub struct Program { + pub resource_manifest: ResourceManifest, + pub resource_source: ResourceSourceArchive, pub functions: Vec, + pub equality_methods: HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct Function { pub id: FunctionId, pub identity: FunctionIdentity, + pub debug_kind: FunctionDebugKind, pub source_definition: Option, + pub actor_handler: Option, + pub actor_state_views: Vec, + pub mutable_sum_backings: Vec, pub params: Vec, + /// Leading parameters supplied by a closure environment or actor state snapshot. + pub capture_count: usize, pub return_type: TypeId, pub locals: Vec, pub body: Block, pub span: Span, } +/// Source identity used when formatting a function value. Extraction can turn +/// either source form into a FunctionRef, so its origin must remain explicit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionDebugKind { + Named(String), + Inline, +} + +impl FunctionDebugKind { + pub fn named(namespace: &str, name: &str) -> Self { + Self::Named(if namespace.is_empty() { + name.to_owned() + } else { + format!("{namespace}.{name}") + }) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct Param { pub local: LocalId, @@ -125,7 +305,13 @@ pub struct Local { pub id: LocalId, pub name: String, pub ty: TypeId, + pub debug_ty: TypeId, + pub debug_type_name: Option, pub mutable: bool, + /// Immediate stable backing local for a checked borrowed alias. Parameters + /// use `ParamMode`; owned locals and generated snapshots have no source. + /// Projected aliases keep their canonical typed path in the initializer. + pub view_source: Option, pub span: Span, } @@ -151,6 +337,12 @@ pub enum StatementKind { target: Expression, value: Expression, }, + /// Exact checked Source place; no expression-shaped write inference. + UpdateField { + target: Expression, + value: Expression, + place: FieldUpdatePlace, + }, Return(Option), /// Yield a fallback value to the nearest enclosing handle expression. HandleDefault(Expression), @@ -182,9 +374,34 @@ pub enum StatementKind { message: Option, }, Trace(LocalId), - Breakpoint(Option), + Breakpoint { + condition: Option, + bindings: Vec, + }, Respond(Expression), Scope(Block), + /// Execute the checker-specialized body whose concrete bound type matches + /// the runtime `TypeInfo` value produced by a trusted reflection loop. + /// + /// This is compiler-owned control flow. Source cannot construct it, and a + /// backend must compare source-visible reflection identity rather than + /// executing every arm for every loop element. + ReflectedTypeDispatch { + type_info: Expression, + arms: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ReflectedTypeArm { + /// Canonical position exported by the checker. It is retained for stable + /// diagnostics and for backends that can dispatch on loop ordinals. + pub iteration_index: usize, + pub bound_type: TypeId, + /// Source-visible reflection identity of the checker-selected bound type, + /// including aliases inside nested type arguments. + pub reflection_identity: String, + pub body: Block, } #[derive(Debug, Clone, PartialEq)] @@ -194,14 +411,51 @@ pub struct Expression { pub span: Span, } +/// Compiler-generated identity of a deterministic native property trial. +#[derive(Debug, Clone, PartialEq)] +pub struct NativePropertyCase { + pub name: String, + /// One-based trial number. + pub trial: u64, +} + #[derive(Debug, Clone, PartialEq)] pub enum ExpressionKind { + /// Sealed required-value graph, expanded at this expression activation. + ActorGraphImport(ActorGraphPlan), + /// Private import instruction derived from the exact checked graph plan. + ActorGraphStep { + instruction: ActorGraphInstruction, + args: Vec, + }, Int(i128), Float(f64), String(String), Bool(bool), Nothing, Local(LocalId), + /// A checked namespace constant materialized before MIR lowering. + Constant { + declaration: Span, + }, + /// A checked, concrete source function used as a first-class value. + FunctionRef(FunctionId), + /// Compiler-owned descriptor; no opaque runtime representation is implied. + ResourceHookValue { + hook: ResourceHookRef, + }, + /// Original Source invocation; ordinary owner/drop plans cannot execute it. + ResourceInvoke { + hook: ResourceHookRef, + args: Vec, + evaluation_order: Vec, + ownership: CallOwnership, + }, + /// A checked inline function with an environment copied from caller locals. + ClosureRef { + function: FunctionId, + captures: Vec, + }, Binary { left: Box, op: BinaryOp, @@ -216,16 +470,32 @@ pub enum ExpressionKind { args: Vec, /// Parameter indexes in lexical source evaluation order. evaluation_order: Vec, + ownership: CallOwnership, }, Intrinsic { - canonical_name: String, + intrinsic: IntrinsicId, + /// Concrete checked generic operands in source order. An intrinsic + /// with no type operands carries an empty vector. + type_arguments: Vec, + /// Checker-owned source identity for reflection operands, including + /// aliases that share a canonical type ID. Construction start also + /// carries the checked source type of each struct field in layout order. + reflection_arguments: Vec, + /// Field predicate chains for reflected struct fields or flattened + /// enum variant payloads, or machine state payloads. Empty for other + /// intrinsics and targets without refinement fields. + refinement_predicates: Vec>, + /// Per-declared-field proof plans, consumed into CFG after selector checks. + field_validation: Option, args: Vec, evaluation_order: Vec, + ownership: CallOwnership, }, IndirectCall { callee: Box, args: Vec, evaluation_order: Vec, + ownership: CallOwnership, }, StructConstruct { struct_type: TypeId, @@ -233,6 +503,8 @@ pub enum ExpressionKind { /// Field indexes in lexical source evaluation order. evaluation_order: Vec, validates_refinements: bool, + /// Predicate chains by canonical field index for base-value inputs. + refinement_predicates: Vec>, }, BitfieldConstruct { bitfield_type: TypeId, @@ -270,12 +542,49 @@ pub enum ExpressionKind { EnumConstruct { enum_type: TypeId, variant: VariantId, + /// Variant payloads in declaration order. payloads: Vec, + /// Payload indices in lexical source evaluation order. + evaluation_order: Vec, }, StringInterpolation(Vec), - Comptime(Box), + /// Require the string returned by an implicitly selected display method to + /// be ready before interpolation evaluates its next segment. + DisplayResult(Box), + /// Require the bool returned by implicit struct equality to be ready before + /// using or negating it, while retaining its checked secret qualification. + EquatableResult(Box), + Comptime { + value: Box, + bindings: Vec, + scopes: Vec, + /// Stable lookup identity, even when parentheses widen the expression span. + source_span: Span, + }, Declassify(Box), Coarsen(Box), + /// Compiler-owned conversion after every required refinement predicate passed. + RefinementValidated(Box), + /// Explicit conversion at an interface-compatible typed boundary. + InterfaceCoerce { + value: Box, + adapters: Vec, + }, + /// A checked signature adapter retaining its source function descriptor. + FunctionAdapter { + value: Box, + function: FunctionId, + }, + /// Concrete implementation identity of an erased interface value. + InterfaceType(Box), + /// Compiler-owned terminal runtime failure with an already checked result type. + RuntimeFailure(String), + /// Compiler-owned terminal failure borrowing an exact string error message. + /// This has no source spelling and evaluates to nothing. + RuntimeFailureMessage(Box), + /// Set or clear diagnostic context around a generated property call. + /// This has no source-level spelling and evaluates to nothing. + PropertyCaseContext(Option), StateIs { value: Box, state: StateId, @@ -284,21 +593,30 @@ pub enum ExpressionKind { Join(Box), Cancel(Box), InlineFunction { + scoped_type_bindings: Vec, params: Vec, + view_params: Vec, + /// First local allocated inside this closure. Earlier locals are captures. + local_floor: u32, body: Block, }, ActorSpawn { actor_type: String, args: Vec, evaluation_order: Vec, + /// Source spawns invoke the checked constructor. `None` is the + /// compiler-generated allocation at the end of that constructor. + constructor: Option, }, ActorMessage { actor: Box, message: String, + handler: FunctionId, args: Vec, /// Parameter indexes in lexical source evaluation order. evaluation_order: Vec, kind: ActorMessageKind, + proof: ActorMessageProof, }, Field { base: Box, @@ -309,11 +627,47 @@ pub enum ExpressionKind { Clone(Box), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Generated callback conversion required inside a container conversion. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InterfaceFunctionAdapter { + pub source: TypeId, + pub target: TypeId, + pub function: FunctionId, + /// Present only while completing a checked reflected callable value. + pub reflected: Option, +} + +impl ExpressionKind { + pub fn interface_coerce(value: Box) -> Self { + Self::InterfaceCoerce { + value, + adapters: Vec::new(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum HandleKind { Result, Optional, - Refinement { refined_type: TypeId }, + Refinement { + refined_type: TypeId, + predicates: Vec, + }, + /// Private callable adapter boundary; never admitted as a source handler. + ReflectedReturn { + plan: Box, + callable: Option, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RefinementPredicate { + pub refined_type: TypeId, + pub type_name: String, + pub function: FunctionId, + pub base_type: TypeId, + pub input_type: TypeId, } #[derive(Debug, Clone, PartialEq)] @@ -373,10 +727,14 @@ pub struct ValidationError { /// Validate backend-facing HIR invariants after lowering and before MIR. pub fn validate(program: &Program) -> Result<(), Vec> { + reflected_callable::validate_program(program)?; + actor_handlers::validate_program(program)?; + mutable_sum_backings::validate_program(program)?; let mut validator = Validator { program, errors: Vec::new(), local_count: 0, + local_types: Vec::new(), loop_depth: 0, handle_depth: 0, }; @@ -385,11 +743,15 @@ pub fn validate(program: &Program) -> Result<(), Vec> { validator.error(function.span, "function IDs must be dense and ordered"); } validator.local_count = function.locals.len(); + validator.local_types = function.locals.iter().map(|local| local.ty).collect(); for (local_index, local) in function.locals.iter().enumerate() { if local.id.index() as usize != local_index { validator.error(local.span, "local IDs must be dense and ordered"); } } + validator + .errors + .extend(local_views::validate_structure(function)); for param in &function.params { validator.check_local(param.local, param.span); } @@ -406,6 +768,7 @@ struct Validator<'a> { program: &'a Program, errors: Vec, local_count: usize, + local_types: Vec, loop_depth: usize, handle_depth: usize, } @@ -450,7 +813,8 @@ impl Validator<'_> { self.check_local(*local, statement.span); self.expression(value); } - StatementKind::Assign { target, value } => { + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { self.expression(target); self.expression(value); } @@ -528,19 +892,83 @@ impl Validator<'_> { } } StatementKind::Trace(local) => self.check_local(*local, statement.span), - StatementKind::Breakpoint(condition) => { + StatementKind::Breakpoint { + condition, + bindings, + } => { if let Some(condition) = condition { self.expression(condition); } + for binding in bindings { + self.check_local(*binding, statement.span); + } } StatementKind::Respond(value) => self.expression(value), StatementKind::Scope(block) => self.block(block), + StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info); + let mut iteration_indexes = std::collections::HashSet::new(); + for arm in arms { + if !iteration_indexes.insert(arm.iteration_index) { + self.error( + statement.span, + "reflected type dispatch contains a duplicate iteration index", + ); + } + self.block(&arm.body); + } + if arms.is_empty() { + self.error(statement.span, "reflected type dispatch has no arms"); + } + } } } fn expression(&mut self, expression: &Expression) { match &expression.kind { + ExpressionKind::ActorGraphImport(plan) => self.block(plan.body()), + ExpressionKind::ActorGraphStep { args, .. } => { + for arg in args { + self.expression(arg); + } + } + ExpressionKind::Local(local) => self.check_local(*local, expression.span), + ExpressionKind::FunctionRef(function) => { + if function.index() as usize >= self.program.functions.len() { + self.error( + expression.span, + "function value references an unknown HIR function", + ); + } + } + ExpressionKind::ClosureRef { function, captures } => { + let Some(target) = self.program.functions.get(function.index() as usize) else { + self.error( + expression.span, + "closure references an unknown HIR function", + ); + return; + }; + if target.capture_count != captures.len() + || target + .params + .iter() + .take(target.capture_count) + .zip(captures) + .any(|(parameter, capture)| { + self.local_types.get(capture.index() as usize) != Some(¶meter.ty) + }) + { + self.error( + expression.span, + "closure capture list disagrees with its function", + ); + } + for capture in captures { + self.check_local(*capture, expression.span); + } + } ExpressionKind::Binary { left, right, .. } => { self.expression(left); self.expression(right); @@ -551,16 +979,67 @@ impl Validator<'_> { | ExpressionKind::OptionalSome(value) | ExpressionKind::View(value) | ExpressionKind::Clone(value) - | ExpressionKind::Comptime(value) + | ExpressionKind::Comptime { value, .. } | ExpressionKind::Declassify(value) | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::InterfaceType(value) | ExpressionKind::Run(value) | ExpressionKind::Join(value) | ExpressionKind::Cancel(value) => self.expression(value), + ExpressionKind::FunctionAdapter { value, function } => { + if function.index() as usize >= self.program.functions.len() { + self.error( + expression.span, + "adapter references an unknown HIR function", + ); + } + self.expression(value); + } + ExpressionKind::InterfaceCoerce { value, adapters } => { + for entry in adapters { + if entry.function.index() as usize >= self.program.functions.len() { + self.error( + expression.span, + "container adapter references an unknown HIR function", + ); + } + } + self.expression(value); + } + ExpressionKind::ResourceHookValue { hook } => { + if !self.program.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource descriptor has no original program manifest", + ); + } + } + ExpressionKind::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => { + if !self.program.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource invocation has no original program manifest", + ); + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } ExpressionKind::Call { function, args, evaluation_order, + .. } => { if function.index() as usize >= self.program.functions.len() { self.error(expression.span, "call references an unknown HIR function"); @@ -571,10 +1050,47 @@ impl Validator<'_> { } } ExpressionKind::Intrinsic { + intrinsic, + field_validation, args, evaluation_order, .. } => { + match (is_reflected_field_intrinsic(*intrinsic), field_validation) { + (true, Some(ReflectedFieldValidation::Validate(plans))) => { + for plan in plans { + for predicate in plan.predicates() { + let valid = self + .program + .functions + .get(predicate.function.index() as usize) + .is_some_and(|function| { + function.id == predicate.function + && refinement_predicate_declaration_matches( + &function.identity.declaration, + &predicate.type_name, + ) + && function.params.len() == 1 + && function.capture_count == 0 + && function.params[0].mode == ParamMode::Owned + && function.params[0].ty == predicate.input_type + && function.return_type == TypeInterner::BOOL + }); + if !valid { + self.error( + expression.span, + "reflected validation predicate function is invalid", + ); + } + } + } + } + (false, None) => {} + _ => self.error( + expression.span, + "reflected read requires checked source validation plans", + ), + } self.check_evaluation_order(evaluation_order, args.len(), expression.span); for argument in args { self.expression(argument); @@ -584,6 +1100,7 @@ impl Validator<'_> { callee, args, evaluation_order, + .. } => { self.expression(callee); self.check_evaluation_order(evaluation_order, args.len(), expression.span); @@ -594,9 +1111,34 @@ impl Validator<'_> { ExpressionKind::StructConstruct { fields, evaluation_order, + validates_refinements, + refinement_predicates, .. + } => { + self.check_evaluation_order(evaluation_order, fields.len(), expression.span); + if (*validates_refinements && refinement_predicates.len() != fields.len()) + || (!*validates_refinements && !refinement_predicates.is_empty()) + { + self.error( + expression.span, + "struct refinement predicate layout is invalid", + ); + } + for chain in refinement_predicates { + for predicate in chain { + if predicate.function.index() as usize >= self.program.functions.len() { + self.error( + expression.span, + "struct refinement predicate is outside HIR function table", + ); + } + } + } + for field in fields { + self.expression(field); + } } - | ExpressionKind::BitfieldConstruct { + ExpressionKind::BitfieldConstruct { fields, evaluation_order, .. @@ -632,11 +1174,22 @@ impl Validator<'_> { } ExpressionKind::Handle { target, + kind, error_local, failure, .. } => { self.expression(target); + if let HandleKind::Refinement { predicates, .. } = kind { + for predicate in predicates { + if predicate.function.index() as usize >= self.program.functions.len() { + self.error( + expression.span, + "refinement predicate is outside HIR function table", + ); + } + } + } if let Some(local) = error_local { self.check_local(*local, expression.span); } @@ -644,7 +1197,12 @@ impl Validator<'_> { self.block(failure); self.handle_depth -= 1; } - ExpressionKind::EnumConstruct { payloads, .. } => { + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + } => { + self.check_evaluation_order(evaluation_order, payloads.len(), expression.span); for payload in payloads { self.expression(payload); } @@ -657,28 +1215,74 @@ impl Validator<'_> { } } ExpressionKind::StateIs { value, .. } => self.expression(value), - ExpressionKind::InlineFunction { params, body } => { + ExpressionKind::InlineFunction { + params, + view_params, + local_floor, + body, + .. + } => { + if *local_floor as usize > self.local_count { + self.error( + expression.span, + "inline function local floor is out of range", + ); + } for param in params { self.check_local(*param, expression.span); + if param.index() < *local_floor { + self.error( + expression.span, + "inline function parameter precedes its local floor", + ); + } + } + for view_param in view_params { + if !params.contains(view_param) { + self.error( + expression.span, + "inline function view parameter is not a parameter", + ); + } } self.block(body); } ExpressionKind::ActorSpawn { args, evaluation_order, + constructor, .. } => { self.check_evaluation_order(evaluation_order, args.len(), expression.span); + if let Some(constructor) = constructor { + if self + .program + .functions + .get(constructor.index() as usize) + .is_none() + { + self.error(expression.span, "actor constructor target is absent"); + } + } for argument in args { self.expression(argument); } } ExpressionKind::ActorMessage { actor, + handler, args, evaluation_order, .. } => { + if self + .program + .functions + .get(handler.index() as usize) + .is_none() + { + self.error(expression.span, "actor handler target is absent"); + } self.expression(actor); self.check_evaluation_order(evaluation_order, args.len(), expression.span); for argument in args { @@ -687,10 +1291,13 @@ impl Validator<'_> { } ExpressionKind::Field { base, .. } => self.expression(base), ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } | ExpressionKind::Float(_) | ExpressionKind::String(_) | ExpressionKind::Bool(_) | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) | ExpressionKind::OptionalNone => {} } } @@ -712,7 +1319,82 @@ pub fn lower( check: &CheckResult, origins: &HashMap, ) -> Result> { - Lowerer::new(module, resolve, check, origins).lower() + Lowerer::new(module, resolve, check, origins, false).lower() +} + +/// Lower checked verification and property bodies as callable test predicates. +/// Property `given` bindings become typed function parameters; generation stays +/// with the test runner. Production program lowering continues to use `lower`. +pub fn lower_with_test_bodies( + module: &Module, + resolve: &ResolveResult, + check: &CheckResult, + origins: &HashMap, +) -> Result> { + Lowerer::new(module, resolve, check, origins, true).lower() +} + +/// Preserve ordinary lowering policy while retaining one immutable checked +/// session for genuine reflected callable constructors and required values. +pub fn lower_checked_ordinary_program( + original: &Arc, +) -> Result> { + lower_ordinary_program(original, false) +} + +pub fn lower_checked_ordinary_program_with_test_bodies( + original: &Arc, +) -> Result> { + lower_ordinary_program(original, true) +} + +fn lower_ordinary_program( + original: &Arc, + tests: bool, +) -> Result> { + let mut lowerer = Lowerer::new( + original.module(), + original.resolved(), + original.checked(), + original.source_origins(), + tests, + ); + lowerer.reflected_checked_program = Some(original.clone()); + lowerer.lower() +} + +/// Lower the original immutable checked Resource envelope, including unused hooks. +pub fn lower_checked_resource_program( + original: &Arc, +) -> Result> { + lower_resource_program(original, false) +} +pub fn lower_checked_resource_program_with_test_bodies( + original: &Arc, +) -> Result> { + lower_resource_program(original, true) +} +fn lower_resource_program( + original: &Arc, + tests: bool, +) -> Result> { + let manifest = ResourceManifest::checked(original).map_err(|message| { + vec![LowerError { + span: Span::new(FileId::new(0), 0, 0), + message, + }] + })?; + let mut lowerer = Lowerer::new( + original.module(), + original.resolved(), + original.checked(), + original.source_origins(), + tests, + ); + lowerer.resource_manifest = manifest; + lowerer.authenticated_resource_lowering = true; + lowerer.reflected_checked_program = Some(original.clone()); + lowerer.lower() } struct FunctionSource<'a> { @@ -731,11 +1413,30 @@ struct ActorHandlerSource<'a> { message_index: usize, } +struct ActorConstructorSource<'a> { + id: FunctionId, + actor_definition: DefId, + actor: &'a ast::ActorDef, + ty: TypeId, +} + +struct RefinementSource<'a> { + id: FunctionId, + definition: DefId, + alias: &'a ast::TypeAlias, + ty: TypeId, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum FunctionKey { + Interface { + owner: TypeId, + method: usize, + }, Definition { definition: DefId, concrete_args: Vec, + specialization: CheckedGenericSpecialization, }, Method { source_span: Span, @@ -743,14 +1444,28 @@ enum FunctionKey { } struct Lowerer<'a> { + temporary_construction: Option, + resource_manifest: ResourceManifest, + authenticated_resource_lowering: bool, + reflected_checked_program: Option>, module: &'a Module, resolve: &'a ResolveResult, check: &'a CheckResult, origins: &'a HashMap, functions: Vec>, + actor_constructors: Vec>, + actor_constructor_ids: HashMap, + actor_handler_ids: HashMap<(TypeId, String), FunctionId>, actor_handlers: Vec>, + actor_state_view_initializers: + HashMap<(DefId, usize), (ActorStateView, HashMap)>, + refinement_sources: Vec>, + refinement_function_ids: HashMap, function_ids: HashMap, + constant_definitions: HashMap, errors: Vec, + include_test_bodies: bool, + source_inline_nodes: Vec, } impl<'a> Lowerer<'a> { @@ -759,36 +1474,177 @@ impl<'a> Lowerer<'a> { resolve: &'a ResolveResult, check: &'a CheckResult, origins: &'a HashMap, + include_test_bodies: bool, ) -> Self { + let constant_definitions = module + .items + .iter() + .filter_map(|item| { + let Item::VarDecl(decl) = item else { + return None; + }; + if decl.mutable { + return None; + } + resolve + .scope_table + .definitions + .iter() + .find(|definition| definition.span == decl.name.span) + .map(|definition| (definition.id, decl.name.span)) + }) + .collect(); Self { + temporary_construction: Some(temporary_local_views::Construction::new(module, check)), + resource_manifest: ResourceManifest::empty(), + authenticated_resource_lowering: false, + reflected_checked_program: None, module, resolve, check, origins, functions: Vec::new(), + actor_constructors: Vec::new(), + actor_constructor_ids: HashMap::new(), + actor_handler_ids: HashMap::new(), actor_handlers: Vec::new(), + actor_state_view_initializers: HashMap::new(), + refinement_sources: Vec::new(), + refinement_function_ids: HashMap::new(), function_ids: HashMap::new(), + constant_definitions, errors: Vec::new(), + include_test_bodies, + source_inline_nodes: Vec::new(), } } fn lower(mut self) -> Result> { self.collect_functions(); + let interface_dispatches = self.collect_interface_dispatches(); let sources = std::mem::take(&mut self.functions); let mut functions = Vec::with_capacity(sources.len()); for source in sources { + let first_inline = self.source_inline_nodes.len(); if let Some(function) = self.lower_function(source) { + self.assign_inline_root(first_inline, function.id); + functions.push(function); + } + } + let actor_constructors = std::mem::take(&mut self.actor_constructors); + for source in actor_constructors { + let first_inline = self.source_inline_nodes.len(); + if let Some(function) = self.lower_actor_constructor(source) { + self.assign_inline_root(first_inline, function.id); functions.push(function); } } let actor_handlers = std::mem::take(&mut self.actor_handlers); for source in actor_handlers { + let first_inline = self.source_inline_nodes.len(); if let Some(function) = self.lower_actor_handler(source) { + self.assign_inline_root(first_inline, function.id); + functions.push(function); + } + } + let refinement_sources = std::mem::take(&mut self.refinement_sources); + for source in refinement_sources { + let first_inline = self.source_inline_nodes.len(); + if let Some(function) = self.lower_refinement_predicate(source) { + self.assign_inline_root(first_inline, function.id); functions.push(function); } } + functions.extend(interface_dispatches); + if self.include_test_bodies { + let mut namespaces = HashMap::new(); + for item in &self.module.items { + if let Item::Namespace(namespace) = item { + namespaces.insert(namespace.span.file, namespace.name.name.clone()); + continue; + } + let test = match item { + Item::Verify(test) => Some(( + DeclarationKind::Verify, + &test.name, + &[][..], + &test.body, + test.span, + )), + Item::Property(test) => Some(( + DeclarationKind::Property, + &test.name, + test.givens.as_slice(), + &test.body, + test.span, + )), + _ => None, + }; + if let Some((kind, name, givens, body, span)) = test { + let namespace = namespaces.get(&span.file).map_or("", String::as_str); + let first_inline = self.source_inline_nodes.len(); + if let Some(function) = self.lower_test_body( + FunctionId(functions.len() as u32), + kind, + namespace, + name, + givens, + body, + span, + ) { + self.assign_inline_root(first_inline, function.id); + functions.push(function); + } + } + } + } if self.errors.is_empty() { - let program = Program { functions }; + let inline_sources = inline_functions::extract_inline_functions_with_sources( + &mut functions, + &self.check.interner, + &self.source_inline_nodes, + ); + let equality_methods = self + .check + .equality_methods + .iter() + .map(|(&owner, method)| { + self.function_ids + .get(&FunctionKey::Method { + source_span: method.source_span, + }) + .copied() + .map(|target| (owner, target)) + .ok_or_else(|| { + vec![LowerError { + span: method.source_span, + message: + "checked equality method is absent from HIR function identities" + .into(), + }] + }) + }) + .collect::, _>>()?; + let mut program = Program { + resource_manifest: self.resource_manifest.clone(), + resource_source: ResourceSourceArchive::empty(), + functions, + equality_methods, + }; + let construction = self.temporary_construction.take().ok_or_else(|| { + vec![LowerError { + span: Span::new(FileId::new(0), 0, 0), + message: "original lowering lost its unpublished temporary construction".into(), + }] + })?; + interface_values::coerce_program(&mut program, &self.check.interner); + actor_handlers::seal_program(&mut program)?; + program = reflected_callable::seal_fresh_original_program( + program, + self.reflected_checked_program.as_ref(), + &self.check.interner, + construction, + )?; validate(&program).map_err(|errors| { errors .into_iter() @@ -798,12 +1654,57 @@ impl<'a> Lowerer<'a> { }) .collect::>() })?; + type_validation::validate_original_backend_types(&program, &self.check.interner) + .map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + if let Some(original) = &self.reflected_checked_program + && (self.authenticated_resource_lowering + || has_reflected_callable_plans(&program) + || temporary_local_views::has_markers(&program) + || field_updates::requires_original_reflected_archive(&program) + || self + .check + .interner + .type_ids() + .any(|ty| matches!(self.check.interner.resolve(ty), Type::Actor(_)))) + { + program.resource_source = ResourceSourceArchive::checked( + &program, + original, + &self.check.interner, + inline_sources, + ); + } + temporary_local_views::validate_program(&program, &self.check.interner).map_err( + |errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + }, + )?; Ok(program) } else { Err(self.errors) } } + fn assign_inline_root(&mut self, first: usize, root: FunctionId) { + for source in &mut self.source_inline_nodes[first..] { + source.root = Some(root); + } + } + fn collect_functions(&mut self) { let mut generic_templates = HashMap::new(); for item in &self.module.items { @@ -812,7 +1713,13 @@ impl<'a> Lowerer<'a> { let Some(definition) = self.definition_at(function.name.span, DefKind::Function) else { - self.error(function.name.span, "function has no resolved definition"); + self.error( + function.name.span, + format!( + "function `{}` has no resolved definition", + function.name.name + ), + ); continue; }; let id = FunctionId(self.functions.len() as u32); @@ -820,6 +1727,7 @@ impl<'a> Lowerer<'a> { FunctionKey::Definition { definition, concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), }, id, ); @@ -856,6 +1764,7 @@ impl<'a> Lowerer<'a> { FunctionKey::Definition { definition: instantiation.definition, concrete_args: instantiation.concrete_args.clone(), + specialization: instantiation.specialization.clone(), }, id, ); @@ -897,17 +1806,86 @@ impl<'a> Lowerer<'a> { self.error(actor.name.span, "actor has no resolved definition"); continue; }; - for (message_index, handler) in actor.handlers.iter().enumerate() { - let id = FunctionId((self.functions.len() + self.actor_handlers.len()) as u32); - self.actor_handlers.push(ActorHandlerSource { - id, - actor_definition, - actor, + let Some(&ty) = self.check.definition_types.get(&actor_definition) else { + self.error(actor.name.span, "actor has no checked type"); + continue; + }; + let id = FunctionId((self.functions.len() + self.actor_constructors.len()) as u32); + self.actor_constructor_ids.insert(ty, id); + self.actor_constructors.push(ActorConstructorSource { + id, + actor_definition, + actor, + ty, + }); + } + for item in &self.module.items { + let Item::Actor(actor) = item else { + continue; + }; + let Some(actor_definition) = self.definition_at(actor.name.span, DefKind::Actor) else { + self.error(actor.name.span, "actor has no resolved definition"); + continue; + }; + let Some(&actor_type) = self.check.definition_types.get(&actor_definition) else { + self.error(actor.name.span, "actor handler has no checked actor type"); + continue; + }; + for (message_index, handler) in actor.handlers.iter().enumerate() { + let id = FunctionId( + (self.functions.len() + + self.actor_constructors.len() + + self.actor_handlers.len()) as u32, + ); + self.actor_handler_ids + .insert((actor_type, handler.name.name.clone()), id); + self.actor_handlers.push(ActorHandlerSource { + id, + actor_definition, + actor, handler, message_index, }); } } + for item in &self.module.items { + let Item::TypeAlias(alias) = item else { + continue; + }; + if alias.constraint.is_none() { + continue; + } + let Some(definition) = self.definition_at(alias.name.span, DefKind::Type) else { + self.error(alias.name.span, "refinement has no resolved definition"); + continue; + }; + let declared = self.resolve.scope_table.def(definition); + let canonical = match &declared.namespace { + Some(namespace) if !namespace.is_empty() => { + format!("{namespace}.{}", alias.name.name) + } + _ => alias.name.name.clone(), + }; + let Some(ty) = self.check.interner.type_ids().find(|id| { + matches!(self.check.interner.resolve(*id), Type::Refinement { name, .. } if name == &canonical) + }) else { + self.error(alias.name.span, "checked refinement type is missing"); + continue; + }; + let id = FunctionId( + (self.functions.len() + + self.actor_constructors.len() + + self.actor_handlers.len() + + self.refinement_sources.len()) as u32, + ); + self.refinement_function_ids.insert(ty, id); + self.refinement_sources.push(RefinementSource { + id, + definition, + alias, + ty, + }); + } } fn method_at(&self, span: Span) -> Option<&'a ast::FunctionDef> { @@ -920,6 +1898,115 @@ impl<'a> Lowerer<'a> { }) } + fn lower_test_body( + &mut self, + id: FunctionId, + kind: DeclarationKind, + namespace: &str, + name: &ast::Ident, + givens: &[ast::GivenDecl], + source_body: &ast::Block, + span: Span, + ) -> Option { + let Some(origin) = self.origins.get(&span.file).cloned() else { + self.error(span, "test body has no source origin"); + return None; + }; + let function_ids = self.function_ids.clone(); + let mut body_lowerer = BodyLowerer::new( + self, + &function_ids, + self.check.type_map.clone(), + self.check.source_type_map.clone(), + self.check.debug_type_names.clone(), + self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), + self.check.assignment_places.clone(), + self.check.generic_calls.clone(), + self.check.intrinsic_ids.clone(), + self.check.intrinsic_type_arguments.clone(), + self.check.intrinsic_reflection_arguments.clone(), + self.check.call_argument_orders.clone(), + self.check.method_calls.clone(), + self.check.interface_calls.clone(), + self.check.method_values.clone(), + self.check.struct_constructions.clone(), + self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), + HashMap::new(), + facts_in_span(&self.check.comptime_type_bindings, span), + ); + body_lowerer.ownership_context = match kind { + DeclarationKind::Verify => jett_typecheck::CheckedOwnershipContext::Verify, + DeclarationKind::Property => jett_typecheck::CheckedOwnershipContext::Property, + _ => { + body_lowerer + .parent + .error(span, "test ownership context has no test declaration"); + return None; + } + }; + let mut params = Vec::with_capacity(givens.len()); + for given in givens { + let Some(definition) = body_lowerer + .parent + .definition_at(given.name.span, DefKind::Variable) + else { + body_lowerer + .parent + .error(given.span, "property input has no checked definition"); + return None; + }; + let Some(&ty) = body_lowerer.parent.check.definition_types.get(&definition) else { + body_lowerer + .parent + .error(given.span, "property input has no checked type"); + return None; + }; + let local = + body_lowerer.allocate_local(definition, &given.name.name, ty, false, given.span); + params.push(Param { + local, + name: given.name.name.clone(), + ty, + mode: ParamMode::Owned, + mutable: false, + span: given.span, + }); + } + let body = body_lowerer.lower_block(source_body); + body_lowerer.reject_unconsumed_static_selections(); + body_lowerer.reject_unconsumed_comptime_type_bindings(); + let mutable_sum_backings = body_lowerer.mutable_sum_backings; + let locals = body_lowerer.locals; + Some(Function { + id, + identity: FunctionIdentity { + declaration: DeclarationId { + origin, + namespace: namespace.to_owned(), + name: format!("{}:{}", name.name, span.start), + kind, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }, + debug_kind: FunctionDebugKind::named(namespace, &name.name), + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings, + params, + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals, + body, + span, + }) + } + fn lower_function(&mut self, source: FunctionSource<'a>) -> Option { let origin = match self.origins.get(&source.function.span.file) { Some(origin) => origin.clone(), @@ -935,35 +2022,80 @@ impl<'a> Lowerer<'a> { parameter_types, return_type, expression_types, + source_expression_types, + debug_type_names, + binding_modes, + call_ownership, + binding_facts, + assignment_places, generic_calls, + intrinsic_ids, + intrinsic_type_arguments, + intrinsic_reflection_arguments, call_argument_orders, method_calls, + interface_calls, + method_values, struct_constructions, pipeline_step_call_types, + pipeline_step_input_types, + static_selections, + comptime_type_bindings, concrete_args, + specialization, ) = if let Some(instantiation) = &source.instantiation { ( instantiation.parameter_types.clone(), instantiation.return_type, instantiation.type_map.clone(), + instantiation.source_type_map.clone(), + instantiation.debug_type_names.clone(), + instantiation.binding_modes.clone(), + instantiation.call_ownership.clone(), + instantiation.binding_facts.clone(), + instantiation.assignment_places.clone(), instantiation.generic_calls.clone(), + instantiation.intrinsic_ids.clone(), + instantiation.intrinsic_type_arguments.clone(), + instantiation.intrinsic_reflection_arguments.clone(), instantiation.call_argument_orders.clone(), instantiation.method_calls.clone(), + instantiation.interface_calls.clone(), + instantiation.method_values.clone(), instantiation.struct_constructions.clone(), instantiation.pipeline_step_call_types.clone(), + instantiation.pipeline_step_input_types.clone(), + instantiation.static_selections.clone(), + instantiation.comptime_type_bindings.clone(), instantiation.concrete_args.clone(), + instantiation.specialization.clone(), ) } else if let Some(method) = &source.method { ( method.parameter_types.clone(), method.return_type, self.check.type_map.clone(), + self.check.source_type_map.clone(), + self.check.debug_type_names.clone(), + self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), + self.check.assignment_places.clone(), self.check.generic_calls.clone(), + self.check.intrinsic_ids.clone(), + self.check.intrinsic_type_arguments.clone(), + self.check.intrinsic_reflection_arguments.clone(), self.check.call_argument_orders.clone(), self.check.method_calls.clone(), + self.check.interface_calls.clone(), + self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), + HashMap::new(), + facts_in_span(&self.check.comptime_type_bindings, source.function.span), Vec::new(), + CheckedGenericSpecialization::default(), ) } else { let Some(definition) = source.definition else { @@ -984,6 +2116,7 @@ impl<'a> Lowerer<'a> { Type::Function { params, return_type, + .. } => (params.clone(), *return_type), _ => { self.error( @@ -997,12 +2130,27 @@ impl<'a> Lowerer<'a> { parameter_types, return_type, self.check.type_map.clone(), + self.check.source_type_map.clone(), + self.check.debug_type_names.clone(), + self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), + self.check.assignment_places.clone(), self.check.generic_calls.clone(), + self.check.intrinsic_ids.clone(), + self.check.intrinsic_type_arguments.clone(), + self.check.intrinsic_reflection_arguments.clone(), self.check.call_argument_orders.clone(), self.check.method_calls.clone(), + self.check.interface_calls.clone(), + self.check.method_values.clone(), self.check.struct_constructions.clone(), self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), + HashMap::new(), + facts_in_span(&self.check.comptime_type_bindings, source.function.span), Vec::new(), + CheckedGenericSpecialization::default(), ) }; if parameter_types.len() != source.function.params.len() { @@ -1017,13 +2165,84 @@ impl<'a> Lowerer<'a> { let mut body_lowerer = BodyLowerer::new( self, &function_ids, - &expression_types, - &generic_calls, - &call_argument_orders, - &method_calls, - &struct_constructions, - &pipeline_step_call_types, + expression_types, + source_expression_types, + debug_type_names, + binding_modes, + call_ownership, + binding_facts, + assignment_places, + generic_calls, + intrinsic_ids, + intrinsic_type_arguments, + intrinsic_reflection_arguments, + call_argument_orders, + method_calls, + interface_calls, + method_values, + struct_constructions, + pipeline_step_call_types, + pipeline_step_input_types, + static_selections, + comptime_type_bindings, ); + if let Some(instantiation) = &source.instantiation { + let checked = body_lowerer.parent.check; + let index = checked + .generic_function_instantiations + .iter() + .position(|candidate| { + candidate.definition == instantiation.definition + && candidate.concrete_args == instantiation.concrete_args + && candidate.specialization == instantiation.specialization + }); + let Some(index) = index else { + body_lowerer.parent.error( + source.function.span, + "generic actor message root has no original checked context", + ); + return None; + }; + body_lowerer.actor_message_root = + jett_typecheck::CheckedActorMessageRoot::Generic(index); + body_lowerer.actor_message_calls = instantiation.actor_message_calls.clone(); + } + if (body_lowerer.parent.authenticated_resource_lowering + || (body_lowerer.parent.reflected_checked_program.is_some() + && field_updates::checked_function_has_updates( + body_lowerer.parent.check, + source.function.span, + ))) + && source.instantiation.is_none() + && source.method.is_none() + && source.function.type_params.is_empty() + && source.definition.is_some_and(|definition| { + reflected_field_sources::original_root_function( + body_lowerer.parent.module, + body_lowerer.parent.resolve, + source.function, + definition, + ) + }) + { + body_lowerer.reflected_field_root = Some(&source.function.body); + } + if body_lowerer.parent.reflected_checked_program.is_some() + && source.instantiation.is_none() + && source.method.is_none() + && source.function.type_params.is_empty() + && source.definition.is_some_and(|definition| { + reflected_field_sources::original_root_function( + body_lowerer.parent.module, + body_lowerer.parent.resolve, + source.function, + definition, + ) + }) + { + body_lowerer.temporary_root = Some(&source.function.body); + } + body_lowerer.return_type = Some(return_type); let mut params = Vec::with_capacity(source.function.params.len()); for (param, ty) in source.function.params.iter().zip(parameter_types) { let Some(definition) = body_lowerer @@ -1055,7 +2274,16 @@ impl<'a> Lowerer<'a> { span: param.span, }); } + body_lowerer.view_parameter_locals.extend( + params + .iter() + .filter(|parameter| parameter.mode == ParamMode::View) + .map(|parameter| parameter.local), + ); let body = body_lowerer.lower_block(&source.function.body); + body_lowerer.reject_unconsumed_static_selections(); + body_lowerer.reject_unconsumed_comptime_type_bindings(); + let mutable_sum_backings = body_lowerer.mutable_sum_backings; let locals = body_lowerer.locals; let (namespace, name, kind) = if let Some(method) = &source.method { if let Some(interface) = &method.interface_name { @@ -1092,6 +2320,12 @@ impl<'a> Lowerer<'a> { ) }; + let debug_kind = match &source.method { + Some(method) => { + FunctionDebugKind::named(&method.owner_name, &source.function.name.name) + } + None => FunctionDebugKind::named(&namespace, &source.function.name.name), + }; Some(Function { id: source.id, identity: FunctionIdentity { @@ -1101,10 +2335,17 @@ impl<'a> Lowerer<'a> { name, kind, }, + scoped_type_bindings: Vec::new(), type_arguments: concrete_args, + specialization, }, + debug_kind, source_definition: source.definition, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings, params, + capture_count: 0, return_type, locals, body, @@ -1112,103 +2353,149 @@ impl<'a> Lowerer<'a> { }) } - fn lower_actor_handler(&mut self, source: ActorHandlerSource<'a>) -> Option { + fn lower_actor_constructor(&mut self, source: ActorConstructorSource<'a>) -> Option { let origin = match self.origins.get(&source.actor.span.file) { Some(origin) => origin.clone(), None => { self.error( source.actor.span, - "source origin is missing for actor handler", - ); - return None; - } - }; - let actor_type = match self.check.definition_types.get(&source.actor_definition) { - Some(ty) => *ty, - None => { - self.error(source.actor.name.span, "actor has no checked type"); - return None; - } - }; - let actor_definition = match self.check.interner.resolve(actor_type) { - Type::Actor(id) => self.check.interner.resolve_actor(*id).clone(), - _ => { - self.error( - source.actor.name.span, - "checked actor definition is not an actor", - ); - return None; - } - }; - let message = match actor_definition.messages.get(source.message_index) { - Some(message) => message.clone(), - None => { - self.error( - source.handler.span, - "actor handler has no checked message definition", + "source origin is missing for actor constructor", ); return None; } }; - if source.actor.capability_params.len() != actor_definition.capability_params.len() - || source.actor.state_fields.len() != actor_definition.state_fields.len() - || source.handler.params.len() != message.params.len() - { + let Type::Actor(actor_id) = self.check.interner.resolve(source.ty) else { self.error( - source.handler.span, - "checked actor shape changed during HIR lowering", + source.actor.name.span, + "checked actor constructor target is not an actor", ); return None; + }; + let actor_id = *actor_id; + let definition = self.check.interner.resolve_actor(actor_id).clone(); + if source.actor.capability_params.len() != definition.capability_params.len() + || source.actor.state_fields.len() != definition.state_fields.len() + { + self.error(source.actor.span, "checked actor constructor shape changed"); + return None; } + let namespace = self + .resolve + .scope_table + .def(source.actor_definition) + .namespace + .clone() + .unwrap_or_default(); + let constructor_bindings = source + .actor + .state_fields + .iter() + .flat_map(|field| facts_in_span(&self.check.comptime_type_bindings, field.value.span())) + .collect(); let function_ids = self.function_ids.clone(); - let expression_types = self.check.type_map.clone(); - let generic_calls = self.check.generic_calls.clone(); - let call_argument_orders = self.check.call_argument_orders.clone(); - let method_calls = self.check.method_calls.clone(); - let struct_constructions = self.check.struct_constructions.clone(); - let pipeline_step_call_types = self.check.pipeline_step_call_types.clone(); let mut body_lowerer = BodyLowerer::new( self, &function_ids, - &expression_types, - &generic_calls, - &call_argument_orders, - &method_calls, - &struct_constructions, - &pipeline_step_call_types, + self.check.type_map.clone(), + self.check.source_type_map.clone(), + self.check.debug_type_names.clone(), + self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), + self.check.assignment_places.clone(), + self.check.generic_calls.clone(), + self.check.intrinsic_ids.clone(), + self.check.intrinsic_type_arguments.clone(), + self.check.intrinsic_reflection_arguments.clone(), + self.check.call_argument_orders.clone(), + self.check.method_calls.clone(), + self.check.interface_calls.clone(), + self.check.method_values.clone(), + self.check.struct_constructions.clone(), + self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), + HashMap::new(), + constructor_bindings, ); - + let mut params = Vec::with_capacity(definition.capability_params.len()); + let mut fields = + Vec::with_capacity(definition.capability_params.len() + definition.state_fields.len()); for (param, (_, ty)) in source .actor .capability_params .iter() - .zip(actor_definition.capability_params.iter()) + .zip(&definition.capability_params) { - let Some(definition) = body_lowerer + let Some(definition_id) = body_lowerer .parent .definition_at(param.name.span, DefKind::Param) else { body_lowerer .parent - .error(param.name.span, "actor capability parameter is unresolved"); + .error(param.name.span, "actor constructor parameter is unresolved"); return None; }; - body_lowerer.allocate_local( - definition, + let local = body_lowerer.allocate_local( + definition_id, ¶m.name.name, *ty, param.mutable, param.span, ); + params.push(Param { + local, + name: param.name.name.clone(), + ty: *ty, + mode: if param.view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: param.mutable, + span: param.span, + }); + fields.push((local, *ty, param.span)); + } + let mut statements = Vec::with_capacity(fields.len() + source.actor.state_fields.len() + 1); + // The interpreter retains constructor arguments in the actor and gives + // state initializers a separate working environment. Preserve that + // capture before any initializer consumes its parameter binding. + for (local, ty, span) in &mut fields { + let original = *local; + let captured = LocalId(body_lowerer.locals.len() as u32); + let mut metadata = body_lowerer.locals[original.index() as usize].clone(); + metadata.id = captured; + metadata.name = format!("$actor.capture.{}", original.index()); + metadata.view_source = None; + body_lowerer.locals.push(metadata); + statements.push(Statement { + kind: StatementKind::Let { + local: captured, + value: Expression { + kind: ExpressionKind::Clone(Box::new(Expression { + kind: ExpressionKind::Local(original), + ty: *ty, + span: *span, + })), + ty: *ty, + span: *span, + }, + }, + span: *span, + }); + *local = captured; } - for (field, (_, ty)) in source + let mut actor_state_views = Vec::new(); + for (field_index, (field, (_, ty))) in source .actor .state_fields .iter() - .zip(actor_definition.state_fields.iter()) + .zip(&definition.state_fields) + .enumerate() { - let Some(definition) = body_lowerer + let value = body_lowerer.lower_expression(&field.value)?; + let Some(definition_id) = body_lowerer .parent .definition_at(field.name.span, DefKind::Variable) else { @@ -1217,217 +2504,1003 @@ impl<'a> Lowerer<'a> { .error(field.name.span, "actor state field is unresolved"); return None; }; - body_lowerer.allocate_local( - definition, + let local = body_lowerer.allocate_local( + definition_id, &field.name.name, *ty, field.mutable, field.span, ); + if let Some(fact) = body_lowerer.binding_facts.get(&field.name.span).copied() { + if let CheckedBindingMode::View { + source: CheckedViewSource::Binding(backing), + } = fact.mode + { + let Some(root) = body_lowerer.local_ids.get(&backing).copied() else { + body_lowerer.parent.error( + field.span, + "actor state view initializer has no original backing", + ); + return None; + }; + body_lowerer.locals[local.index() as usize].view_source = Some(root); + let target = jett_typecheck::CheckedActorHandlerTarget { + actor_definition: source.actor_definition, + actor: actor_id, + handler_index: 0, + }; + let Some(receipt) = body_lowerer + .parent + .check + .actor_handler(target) + .cloned() + .filter(|receipt| receipt.matches_source_actor(source.actor)) + else { + body_lowerer.parent.error( + field.span, + "actor state view lacks its original actor receipt", + ); + return None; + }; + let view = ActorStateView::new(receipt, fact, local, root, value.clone()); + let definitions = body_lowerer + .local_ids + .iter() + .map(|(definition, local)| (*local, *definition)) + .collect(); + body_lowerer.parent.actor_state_view_initializers.insert( + (source.actor_definition, field_index), + (view.clone(), definitions), + ); + actor_state_views.push(view); + } + } + statements.push(Statement { + kind: StatementKind::Let { local, value }, + span: field.span, + }); + fields.push((local, *ty, field.span)); } - let mut params = Vec::with_capacity(source.handler.params.len()); - for (param, (_, ty)) in source.handler.params.iter().zip(message.params.iter()) { - let Some(definition) = body_lowerer - .parent - .definition_at(param.name.span, DefKind::Param) - else { - body_lowerer - .parent - .error(param.name.span, "actor message parameter is unresolved"); - return None; - }; - let local = body_lowerer.allocate_local( - definition, - ¶m.name.name, - *ty, - param.mutable, - param.span, - ); - params.push(Param { - local, - name: param.name.name.clone(), - ty: *ty, - mode: if param.view { - ParamMode::View + body_lowerer.reject_unconsumed_static_selections(); + body_lowerer.reject_unconsumed_comptime_type_bindings(); + let args = fields + .into_iter() + .map(|(local, ty, span)| { + let value = Expression { + kind: ExpressionKind::Local(local), + ty, + span, + }; + if body_lowerer.locals[local.index() as usize] + .view_source + .is_some() + { + Expression { + kind: ExpressionKind::View(Box::new(value)), + ty, + span, + } } else { - ParamMode::Owned + value + } + }) + .collect::>(); + let evaluation_order = (0..args.len()).collect(); + statements.push(Statement { + kind: StatementKind::Return(Some(Expression { + kind: ExpressionKind::ActorSpawn { + actor_type: definition.name, + args, + evaluation_order, + constructor: None, }, - mutable: param.mutable, - span: param.span, - }); - } - let body = body_lowerer.lower_block(&source.handler.body); - let locals = body_lowerer.locals; - let actor_def = self.resolve.scope_table.def(source.actor_definition); - + ty: source.ty, + span: source.actor.span, + })), + span: source.actor.span, + }); + let debug_kind = FunctionDebugKind::named(&namespace, &source.actor.name.name); Some(Function { id: source.id, identity: FunctionIdentity { declaration: DeclarationId { origin, - namespace: actor_def.namespace.clone().unwrap_or_default(), - name: format!("{}.{}", source.actor.name.name, source.handler.name.name), - kind: DeclarationKind::ActorHandler, + namespace, + name: source.actor.name.name.clone(), + kind: DeclarationKind::ActorConstructor, }, + scoped_type_bindings: Vec::new(), type_arguments: Vec::new(), + specialization: CheckedGenericSpecialization::default(), }, + debug_kind, source_definition: None, + actor_handler: None, + actor_state_views, + mutable_sum_backings: body_lowerer.mutable_sum_backings, params, - return_type: message.responds, - locals, - body, - span: source.handler.span, + capture_count: 0, + return_type: source.ty, + locals: body_lowerer.locals, + body: Block { + statements, + span: source.actor.span, + }, + span: source.actor.span, }) } - fn definition_at(&self, span: Span, kind: DefKind) -> Option { - self.resolve - .scope_table - .definitions - .iter() - .find(|definition| definition.span == span && definition.kind == kind) - .map(|definition| definition.id) - } - - fn error(&mut self, span: Span, message: impl Into) { - self.errors.push(LowerError { - span, - message: message.into(), - }); - } -} - -struct BodyLowerer<'lowerer, 'program> { - parent: &'lowerer mut Lowerer<'program>, - function_ids: &'lowerer HashMap, - expression_types: &'lowerer HashMap, - generic_calls: &'lowerer HashMap, - call_argument_orders: &'lowerer HashMap, - method_calls: &'lowerer HashMap, - struct_constructions: &'lowerer HashMap, - pipeline_step_call_types: &'lowerer HashMap, - local_ids: HashMap, - locals: Vec, -} + fn lower_actor_handler(&mut self, source: ActorHandlerSource<'a>) -> Option { + let origin = match self.origins.get(&source.actor.span.file) { + Some(origin) => origin.clone(), + None => { + self.error( + source.actor.span, + "source origin is missing for actor handler", + ); + return None; + } + }; + let actor_type = match self.check.definition_types.get(&source.actor_definition) { + Some(ty) => *ty, + None => { + self.error(source.actor.name.span, "actor has no checked type"); + return None; + } + }; + let actor_definition = match self.check.interner.resolve(actor_type) { + Type::Actor(id) => self.check.interner.resolve_actor(*id).clone(), + _ => { + self.error( + source.actor.name.span, + "checked actor definition is not an actor", + ); + return None; + } + }; + let actor_id = match self.check.interner.resolve(actor_type) { + Type::Actor(id) => *id, + _ => return None, + }; + let target = jett_typecheck::CheckedActorHandlerTarget { + actor_definition: source.actor_definition, + actor: actor_id, + handler_index: source.message_index, + }; + let receipt = match self.check.actor_handler(target).cloned() { + Some(receipt) + if receipt.ownership_validated() + && receipt.body_span() == source.handler.body.span + && receipt.matches_source_actor(source.actor) => + { + receipt + } + _ => { + self.error( + source.handler.span, + "actor handler lacks its original checked ownership receipt", + ); + return None; + } + }; + let facts = receipt.body(); + let message = match actor_definition.messages.get(source.message_index) { + Some(message) => message.clone(), + None => { + self.error( + source.handler.span, + "actor handler has no checked message definition", + ); + return None; + } + }; + if source.actor.capability_params.len() != actor_definition.capability_params.len() + || source.actor.state_fields.len() != actor_definition.state_fields.len() + || source.handler.params.len() != message.params.len() + { + self.error( + source.handler.span, + "checked actor shape changed during HIR lowering", + ); + return None; + } -impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { - fn new( - parent: &'lowerer mut Lowerer<'program>, - function_ids: &'lowerer HashMap, - expression_types: &'lowerer HashMap, - generic_calls: &'lowerer HashMap, - call_argument_orders: &'lowerer HashMap, - method_calls: &'lowerer HashMap, - struct_constructions: &'lowerer HashMap, - pipeline_step_call_types: &'lowerer HashMap, - ) -> Self { - Self { - parent, - function_ids, + let function_ids = self.function_ids.clone(); + let expression_types = facts.type_map.clone(); + let source_expression_types = facts.source_type_map.clone(); + let debug_type_names = facts.debug_type_names.clone(); + let binding_modes = facts.binding_modes.clone(); + let call_ownership = facts.call_ownership.clone(); + let binding_facts = facts.binding_facts.clone(); + let assignment_places = facts.assignment_places.clone(); + let generic_calls = facts.generic_calls.clone(); + let intrinsic_ids = facts.intrinsic_ids.clone(); + let intrinsic_type_arguments = facts.intrinsic_type_arguments.clone(); + let intrinsic_reflection_arguments = facts.intrinsic_reflection_arguments.clone(); + let call_argument_orders = facts.call_argument_orders.clone(); + let method_calls = facts.method_calls.clone(); + let interface_calls = facts.interface_calls.clone(); + let method_values = facts.method_values.clone(); + let struct_constructions = facts.struct_constructions.clone(); + let pipeline_step_call_types = facts.pipeline_step_call_types.clone(); + let pipeline_step_input_types = facts.pipeline_step_input_types.clone(); + let static_selections = facts.static_selections.clone(); + let comptime_type_bindings = facts.comptime_type_bindings.clone(); + let mut body_lowerer = BodyLowerer::new( + self, + &function_ids, expression_types, + source_expression_types, + debug_type_names, + binding_modes, + call_ownership, + binding_facts, + assignment_places, generic_calls, + intrinsic_ids, + intrinsic_type_arguments, + intrinsic_reflection_arguments, call_argument_orders, method_calls, + interface_calls, + method_values, struct_constructions, pipeline_step_call_types, - local_ids: HashMap::new(), - locals: Vec::new(), - } - } + pipeline_step_input_types, + static_selections, + comptime_type_bindings, + ); - fn allocate_local( - &mut self, - definition: DefId, - name: &str, - ty: TypeId, - mutable: bool, - span: Span, - ) -> LocalId { - let id = LocalId(self.locals.len() as u32); - self.local_ids.insert(definition, id); - self.locals.push(Local { - id, - name: name.to_string(), - ty, - mutable, - span, - }); - id - } + body_lowerer.actor_message_calls = facts.actor_message_calls.clone(); + body_lowerer.actor_message_root = + jett_typecheck::CheckedActorMessageRoot::ActorHandler(target); + // Handler state is an input snapshot, not an uninitialized local. + // Captured parameters are supplied through the actor environment; + // message parameters follow them in the ordinary call signature. + let mut params = Vec::with_capacity( + source.actor.capability_params.len() + + source.actor.state_fields.len() + + source.handler.params.len(), + ); - fn lower_block(&mut self, block: &ast::Block) -> Block { - let mut statements = Vec::new(); - for statement in &block.stmts { - if let Some(statement) = self.lower_statement(statement) { - statements.push(statement); - } + let mut roles = Vec::new(); + for (parameter, (param, (_, ty))) in source + .actor + .capability_params + .iter() + .zip(actor_definition.capability_params.iter()) + .enumerate() + { + let Some(definition) = body_lowerer + .parent + .definition_at(param.name.span, DefKind::Param) + else { + body_lowerer + .parent + .error(param.name.span, "actor capability parameter is unresolved"); + return None; + }; + let local = body_lowerer.allocate_local( + definition, + ¶m.name.name, + *ty, + param.mutable, + param.span, + ); + let role = jett_typecheck::CheckedActorBindingRole::Capability { parameter }; + let fact = receipt + .bindings() + .iter() + .find(|binding| binding.role == role) + .filter(|binding| binding.fact.definition == definition && binding.fact.ty == *ty) + .map(|binding| binding.fact); + let Some(fact) = fact else { + body_lowerer.parent.error( + source.handler.span, + "actor handler role changed its exact checked binding", + ); + return None; + }; + roles.push(ActorHandlerRole::new(role, fact, local)); + params.push(Param { + local, + name: param.name.name.clone(), + ty: *ty, + mode: if param.view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: param.mutable, + span: param.span, + }); } - Block { - statements, - span: block.span, + for (field_index, (field, (_, ty))) in source + .actor + .state_fields + .iter() + .zip(actor_definition.state_fields.iter()) + .enumerate() + { + let Some(definition) = body_lowerer + .parent + .definition_at(field.name.span, DefKind::Variable) + else { + body_lowerer + .parent + .error(field.name.span, "actor state field is unresolved"); + return None; + }; + let local = body_lowerer.allocate_local( + definition, + &field.name.name, + *ty, + field.mutable, + field.span, + ); + let role = jett_typecheck::CheckedActorBindingRole::State { field: field_index }; + let fact = receipt + .bindings() + .iter() + .find(|binding| binding.role == role) + .filter(|binding| binding.fact.definition == definition && binding.fact.ty == *ty) + .map(|binding| binding.fact); + let Some(fact) = fact else { + body_lowerer.parent.error( + source.handler.span, + "actor handler role changed its exact checked binding", + ); + return None; + }; + roles.push(ActorHandlerRole::new(role, fact, local)); + params.push(Param { + local, + name: field.name.name.clone(), + ty: *ty, + mode: match fact.mode { + CheckedBindingMode::Owned => ParamMode::Owned, + CheckedBindingMode::View { .. } => ParamMode::View, + }, + mutable: field.mutable, + span: field.span, + }); } - } - - fn lower_statement(&mut self, statement: &Stmt) -> Option { - let (kind, span) = match statement { - Stmt::VarDecl(decl) => { - let Some(definition) = self.parent.definition_at(decl.name.span, DefKind::Variable) - else { - self.parent - .error(decl.name.span, "local has no resolved definition"); + for role in &mut roles { + if let CheckedBindingMode::View { + source: CheckedViewSource::Binding(source_definition), + } = role.fact().mode + { + let Some(backing) = body_lowerer.local_ids.get(&source_definition).copied() else { + body_lowerer.parent.error( + source.handler.span, + "actor state view has no earlier captured backing", + ); return None; }; - let Some(ty) = self - .expression_types - .get(&decl.value.span()) - .copied() - .or_else(|| self.parent.check.definition_types.get(&definition).copied()) - else { - self.parent - .error(decl.name.span, "local has no checked type"); + body_lowerer.locals[role.local().index() as usize].view_source = Some(backing); + let jett_typecheck::CheckedActorBindingRole::State { field } = role.role() else { + body_lowerer.parent.error( + source.handler.span, + "actor capability view cannot invent state dependency", + ); return None; }; - let local = - self.allocate_local(definition, &decl.name.name, ty, decl.mutable, decl.span); - let value = self.lower_expression(&decl.value)?; - (StatementKind::Let { local, value }, decl.span) - } - Stmt::Assign(assign) => ( - StatementKind::Assign { - target: self.lower_expression(&assign.target)?, - value: self.lower_expression(&assign.value)?, - }, - assign.span, - ), - Stmt::Return(ret) => { - let value = match &ret.value { - Some(value) => Some(self.lower_expression(value)?), - None => None, + let Some((original, definitions)) = body_lowerer + .parent + .actor_state_view_initializers + .get(&(source.actor_definition, field)) + else { + body_lowerer.parent.error( + source.handler.span, + "actor state view has no original typed initializer", + ); + return None; }; - (StatementKind::Return(value), ret.span) + match original.remap(&|id| { + definitions + .get(&id) + .and_then(|definition| body_lowerer.local_ids.get(definition)) + .copied() + }) { + Ok(view) => role.set_view(view), + Err(message) => { + body_lowerer.parent.error(source.handler.span, message); + return None; + } + } } - Stmt::Expr(expr) => { - let kind = if let Expr::Default(value, _) = &expr.expr { - StatementKind::HandleDefault(self.lower_expression(value)?) + } + let capture_count = params.len(); + for (parameter, (param, (_, ty))) in source + .handler + .params + .iter() + .zip(message.params.iter()) + .enumerate() + { + let Some(definition) = body_lowerer + .parent + .definition_at(param.name.span, DefKind::Param) + else { + body_lowerer + .parent + .error(param.name.span, "actor message parameter is unresolved"); + return None; + }; + let local = body_lowerer.allocate_local( + definition, + ¶m.name.name, + *ty, + param.mutable, + param.span, + ); + let role = jett_typecheck::CheckedActorBindingRole::Message { parameter }; + let fact = receipt + .bindings() + .iter() + .find(|binding| binding.role == role) + .filter(|binding| binding.fact.definition == definition && binding.fact.ty == *ty) + .map(|binding| binding.fact); + let Some(fact) = fact else { + body_lowerer.parent.error( + source.handler.span, + "actor handler role changed its exact checked binding", + ); + return None; + }; + roles.push(ActorHandlerRole::new(role, fact, local)); + params.push(Param { + local, + name: param.name.name.clone(), + ty: *ty, + mode: if param.view { + ParamMode::View } else { - StatementKind::Expression(self.lower_expression(&expr.expr)?) - }; - (kind, expr.span) - } - Stmt::If(branch) => { - let condition = self.lower_expression(&branch.condition)?; - let then_block = self.lower_block(&branch.then_block); - let else_block = - self.lower_else_chain(&branch.else_ifs, branch.else_block.as_ref()); - ( - StatementKind::If { - condition, - then_block, - else_block, - }, - branch.span, - ) + ParamMode::Owned + }, + mutable: param.mutable, + span: param.span, + }); + } + body_lowerer.view_parameter_locals.extend( + params + .iter() + .filter(|parameter| parameter.mode == ParamMode::View) + .map(|parameter| parameter.local), + ); + let body = body_lowerer.lower_block(&source.handler.body); + body_lowerer.reject_unconsumed_static_selections(); + body_lowerer.reject_unconsumed_comptime_type_bindings(); + let mutable_sum_backings = body_lowerer.mutable_sum_backings; + let locals = body_lowerer.locals; + let actor_def = self.resolve.scope_table.def(source.actor_definition); + + Some(Function { + id: source.id, + identity: FunctionIdentity { + declaration: DeclarationId { + origin, + namespace: actor_def.namespace.clone().unwrap_or_default(), + name: format!("{}.{}", source.actor.name.name, source.handler.name.name), + kind: DeclarationKind::ActorHandler, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }, + debug_kind: FunctionDebugKind::named(&actor_def.name, &source.handler.name.name), + source_definition: None, + actor_handler: Some(ActorHandlerEntry::new(target, receipt, roles)), + actor_state_views: Vec::new(), + mutable_sum_backings, + params, + capture_count, + return_type: message.responds, + locals, + body, + span: source.handler.span, + }) + } + + fn lower_refinement_predicate(&mut self, source: RefinementSource<'a>) -> Option { + let constraint = source.alias.constraint.as_ref()?; + let origin = match self.origins.get(&source.alias.span.file) { + Some(origin) => origin.clone(), + None => { + self.error(source.alias.span, "source origin is missing for refinement"); + return None; + } + }; + let Some(value_definition) = self.definition_at(constraint.span(), DefKind::Variable) + else { + self.error( + constraint.span(), + "refinement value has no resolved definition", + ); + return None; + }; + let mut input_type = source.ty; + while let Type::Refinement { base, .. } = self.check.interner.resolve(input_type) { + input_type = *base; + } + if let Type::Secret(inner) = self.check.interner.resolve(input_type) { + input_type = *inner; + } + let function_ids = self.function_ids.clone(); + let mut lowerer = BodyLowerer::new( + self, + &function_ids, + self.check.type_map.clone(), + self.check.source_type_map.clone(), + self.check.debug_type_names.clone(), + self.check.binding_modes.clone(), + self.check.call_ownership.clone(), + self.check.binding_facts.clone(), + self.check.assignment_places.clone(), + self.check.generic_calls.clone(), + self.check.intrinsic_ids.clone(), + self.check.intrinsic_type_arguments.clone(), + self.check.intrinsic_reflection_arguments.clone(), + self.check.call_argument_orders.clone(), + self.check.method_calls.clone(), + self.check.interface_calls.clone(), + self.check.method_values.clone(), + self.check.struct_constructions.clone(), + self.check.pipeline_step_call_types.clone(), + self.check.pipeline_step_input_types.clone(), + HashMap::new(), + facts_in_span(&self.check.comptime_type_bindings, source.alias.span), + ); + let local = lowerer.allocate_local( + value_definition, + "value", + input_type, + false, + constraint.span(), + ); + let result = lowerer.lower_expression(constraint)?; + lowerer.reject_unconsumed_comptime_type_bindings(); + let mutable_sum_backings = lowerer.mutable_sum_backings; + let locals = lowerer.locals; + let declared = self.resolve.scope_table.def(source.definition); + Some(Function { + id: source.id, + identity: FunctionIdentity { + declaration: DeclarationId { + origin, + namespace: declared.namespace.clone().unwrap_or_default(), + name: source.alias.name.name.clone(), + kind: DeclarationKind::RefinementPredicate, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }, + debug_kind: FunctionDebugKind::named( + declared.namespace.as_deref().unwrap_or_default(), + &source.alias.name.name, + ), + source_definition: Some(source.definition), + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings, + params: vec![Param { + local, + name: "value".to_string(), + ty: input_type, + mode: ParamMode::Owned, + mutable: false, + span: constraint.span(), + }], + capture_count: 0, + return_type: TypeInterner::BOOL, + locals, + body: Block { + statements: vec![Statement { + kind: StatementKind::Return(Some(result)), + span: constraint.span(), + }], + span: source.alias.span, + }, + span: source.alias.span, + }) + } + + fn definition_at(&self, span: Span, kind: DefKind) -> Option { + self.resolve + .resolutions + .get(&span) + .copied() + .filter(|definition| self.resolve.scope_table.def(*definition).kind == kind) + .or_else(|| { + self.resolve + .scope_table + .definitions + .iter() + .find(|definition| definition.span == span && definition.kind == kind) + .map(|definition| definition.id) + }) + } + + fn error(&mut self, span: Span, message: impl Into) { + self.errors.push(LowerError { + span, + message: message.into(), + }); + } +} + +struct BodyLowerer<'lowerer, 'program> { + parent: &'lowerer mut Lowerer<'program>, + function_ids: &'lowerer HashMap, + expression_types: HashMap, + source_expression_types: HashMap, + source_projection_roots: HashMap, + debug_type_names: HashMap, + binding_modes: HashMap, + call_ownership: HashMap, + actor_message_calls: HashMap, + actor_message_root: jett_typecheck::CheckedActorMessageRoot, + binding_facts: HashMap, + assignment_places: HashMap, + next_field_update_occurrence: u64, + mutable_sum_backings: Vec, + temporary_root: Option<&'program ast::Block>, + temporary_scope: Option<(Span, usize)>, + temporary_prefix: Vec, + temporary_origins: HashMap, + generic_calls: HashMap, + intrinsic_ids: HashMap, + intrinsic_type_arguments: HashMap>, + intrinsic_reflection_arguments: HashMap>, + call_argument_orders: HashMap, + method_calls: HashMap, + interface_calls: HashMap, + method_values: HashMap, + struct_constructions: HashMap, + pipeline_step_call_types: HashMap, + pipeline_step_input_types: HashMap, + static_selections: HashMap, + comptime_type_bindings: HashMap>, + consumed_static_selections: HashSet, + consumed_comptime_type_bindings: HashSet, + local_ids: HashMap, + locals: Vec, + visible_bindings: Vec>, + scoped_type_bindings: Vec, + required_scopes: Vec, + reflected_field_root: Option<&'program ast::Block>, + retained_reflected_bindings: HashSet, + observation_initializers: HashMap, + return_type: Option, + ownership_context: jett_typecheck::CheckedOwnershipContext, + view_parameter_locals: HashSet, + view_iteration_bindings: HashMap, + match_bindings: HashMap, +} + +impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { + fn new( + parent: &'lowerer mut Lowerer<'program>, + function_ids: &'lowerer HashMap, + expression_types: HashMap, + source_expression_types: HashMap, + debug_type_names: HashMap, + binding_modes: HashMap, + call_ownership: HashMap, + binding_facts: HashMap, + assignment_places: HashMap, + generic_calls: HashMap, + intrinsic_ids: HashMap, + intrinsic_type_arguments: HashMap>, + intrinsic_reflection_arguments: HashMap>, + call_argument_orders: HashMap, + method_calls: HashMap, + interface_calls: HashMap, + method_values: HashMap, + struct_constructions: HashMap, + pipeline_step_call_types: HashMap, + pipeline_step_input_types: HashMap, + static_selections: HashMap, + comptime_type_bindings: HashMap>, + ) -> Self { + let actor_message_calls = parent.check.actor_message_calls.clone(); + Self { + parent, + function_ids, + expression_types, + source_expression_types, + source_projection_roots: HashMap::new(), + debug_type_names, + binding_modes, + call_ownership, + actor_message_calls, + actor_message_root: jett_typecheck::CheckedActorMessageRoot::Ordinary, + binding_facts, + assignment_places, + next_field_update_occurrence: 0, + mutable_sum_backings: Vec::new(), + temporary_root: None, + temporary_scope: None, + temporary_prefix: Vec::new(), + temporary_origins: HashMap::new(), + generic_calls, + intrinsic_ids, + intrinsic_type_arguments, + intrinsic_reflection_arguments, + call_argument_orders, + method_calls, + interface_calls, + method_values, + struct_constructions, + pipeline_step_call_types, + pipeline_step_input_types, + static_selections, + comptime_type_bindings, + consumed_static_selections: HashSet::new(), + consumed_comptime_type_bindings: HashSet::new(), + local_ids: HashMap::new(), + locals: Vec::new(), + visible_bindings: vec![HashMap::new()], + scoped_type_bindings: Vec::new(), + required_scopes: Vec::new(), + reflected_field_root: None, + retained_reflected_bindings: HashSet::new(), + observation_initializers: HashMap::new(), + return_type: None, + ownership_context: jett_typecheck::CheckedOwnershipContext::Ordinary, + view_parameter_locals: HashSet::new(), + view_iteration_bindings: HashMap::new(), + match_bindings: HashMap::new(), + } + } + + fn static_selection(&mut self, span: Span) -> Option { + let selection = self.static_selections.get(&span).copied()?; + self.consumed_static_selections.insert(span); + Some(selection) + } + + fn reject_unconsumed_static_selections(&mut self) { + let mut spans = self + .static_selections + .keys() + .copied() + .filter(|span| !self.consumed_static_selections.contains(span)) + .collect::>(); + spans.sort_by_key(|span| (span.file.index(), span.start, span.end)); + for span in spans { + self.parent.error( + span, + "checked static selection does not match a lowered statement", + ); + } + } + + fn reject_unconsumed_comptime_type_bindings(&mut self) { + let mut spans = self + .comptime_type_bindings + .keys() + .copied() + .filter(|span| !self.consumed_comptime_type_bindings.contains(span)) + .collect::>(); + spans.sort_by_key(|span| (span.file.index(), span.start, span.end)); + for span in spans { + self.parent.error( + span, + "checked comptime type binding does not match a lowered statement", + ); + } + } + + fn consume_omitted_body_facts(&mut self, body_span: Span) { + let in_body = |span: &Span| { + span.file == body_span.file + && span.start >= body_span.start + && span.end <= body_span.end + }; + self.consumed_static_selections + .extend(self.static_selections.keys().copied().filter(in_body)); + self.consumed_comptime_type_bindings + .extend(self.comptime_type_bindings.keys().copied().filter(in_body)); + } + + fn allocate_local( + &mut self, + definition: DefId, + name: &str, + ty: TypeId, + mutable: bool, + span: Span, + ) -> LocalId { + let id = LocalId(self.locals.len() as u32); + self.local_ids.insert(definition, id); + if let Some(scope) = self.visible_bindings.last_mut() { + scope.insert(name.to_string(), id); + } + self.locals.push(Local { + id, + name: name.to_string(), + ty, + debug_ty: self + .parent + .check + .definition_types + .get(&definition) + .copied() + .unwrap_or(ty), + debug_type_name: self + .debug_type_names + .get(&self.parent.resolve.scope_table.def(definition).span) + .cloned(), + mutable, + view_source: None, + span, + }); + id + } + + fn mutable_alias_backing_is_owned(&self, root: LocalId) -> bool { + let Some(local) = self + .locals + .get(root.index() as usize) + .filter(|local| local.id == root) + else { + return false; + }; + if self.view_parameter_locals.contains(&root) + || self.view_iteration_bindings.contains_key(&root) + || self.match_bindings.contains_key(&root) + { + return false; + } + let mut definitions = self + .local_ids + .iter() + .filter_map(|(definition, local)| (*local == root).then_some(*definition)); + let Some(definition) = definitions.next() else { + return false; + }; + if definitions.next().is_some() { + return false; + } + let Some(declaration) = self + .parent + .resolve + .scope_table + .definitions + .get(definition.index() as usize) + .filter(|declaration| declaration.id == definition) + else { + return false; + }; + let Some(fact) = self.binding_facts.get(&declaration.span) else { + return false; + }; + fact.definition == definition + && fact.declaration_span == declaration.span + && fact.ty == local.ty + && fact.mutable == local.mutable + && fact.mode == CheckedBindingMode::Owned + && local.span.file == declaration.span.file + && local.span.start <= declaration.span.start + && local.span.end >= declaration.span.end + } + + fn local_view_source( + &mut self, + declaration: &ast::VarDecl, + value: &Expression, + ty: TypeId, + ) -> Option> { + let Some(mode) = self.binding_modes.get(&declaration.name.span).copied() else { + self.parent.error( + declaration.name.span, + "local declaration has no checked ownership mode", + ); + return None; + }; + let CheckedBindingMode::View { source } = mode else { + return Some(None); + }; + let CheckedViewSource::Binding(definition) = source else { + self.parent.error(declaration.value.span(), "native borrowed alias requires a stable local origin; temporary views remain unsupported"); + return None; + }; + let Some(source) = self.local_ids.get(&definition).copied() else { + self.parent.error( + declaration.value.span(), + "native borrowed alias source is not a local binding", + ); + return None; + }; + let stable_root = + local_views::immutable_alias_root(&self.locals, source).is_some_and(|root| { + !self.locals[root.index() as usize].mutable + || self.mutable_alias_backing_is_owned(root) + }); + if declaration.mutable || !stable_root { + let message = if declaration.mutable { + "native borrowed alias requires immutable bindings along its stable origin" + } else { + "native borrowed alias requires immutable aliases and a stable owned backing" + }; + self.parent.error(declaration.span, message); + return None; + } + if let Err(message) = local_views::validate_local_view_initializer( + value, + source, + self.locals[source.index() as usize].ty, + ty, + &self.parent.check.interner, + ) { + self.parent.error(declaration.value.span(), message); + return None; + } + Some(Some(source)) + } + + fn lower_block(&mut self, block: &ast::Block) -> Block { + self.visible_bindings.push(HashMap::new()); + let mut statements = Vec::new(); + let previous_temporary_scope = self.temporary_scope; + for (index, statement) in block.stmts.iter().enumerate() { + self.temporary_scope = self + .temporary_root + .filter(|root| std::ptr::eq(*root, block)) + .map(|_| (block.span, index)); + let lowered = self.lower_statement(statement); + statements.append(&mut self.temporary_prefix); + if let Some(statement) = lowered { + statements.push(statement); + } + } + self.temporary_scope = previous_temporary_scope; + self.visible_bindings.pop(); + Block { + statements, + span: block.span, + } + } + + fn lower_statement(&mut self, statement: &Stmt) -> Option { + let (kind, span) = match statement { + Stmt::VarDecl(decl) => self.lower_local_declaration(decl)?, + Stmt::Assign(assign) => { + let target = self.lower_expression(&assign.target)?; + let value = self.lower_expression(&assign.value)?; + let kind = if matches!(target.kind, ExpressionKind::Local(_)) { + StatementKind::Assign { target, value } + } else { + let place = field_updates::authenticate(self, assign, &target)?; + StatementKind::UpdateField { + target, + value, + place, + } + }; + (kind, assign.span) + } + Stmt::Return(ret) => { + let value = match &ret.value { + Some(value) => { + let value = self.lower_expression(value)?; + Some(self.refine_return_value(value, ret.span)?) + } + None => None, + }; + (StatementKind::Return(value), ret.span) + } + Stmt::Expr(expr) => { + let kind = if let Expr::Default(value, _) = &expr.expr { + StatementKind::HandleDefault(self.lower_expression(value)?) + } else { + StatementKind::Expression(self.lower_expression(&expr.expr)?) + }; + (kind, expr.span) } + Stmt::If(branch) => self.lower_if_statement(branch)?, Stmt::While(loop_stmt) => ( StatementKind::While { condition: self.lower_expression(&loop_stmt.condition)?, @@ -1435,24 +3508,8 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { }, loop_stmt.span, ), - Stmt::For(loop_stmt) => { - let key = self.allocate_declared_local(&loop_stmt.variable, false)?; - let value = match &loop_stmt.value_variable { - Some(binding) => Some(self.allocate_declared_local(binding, false)?), - None => None, - }; - ( - StatementKind::For { - key, - value, - by_view: loop_stmt.view, - iterable: self.lower_expression(&loop_stmt.iterable)?, - body: self.lower_block(&loop_stmt.body), - }, - loop_stmt.span, - ) - } - Stmt::Match(match_stmt) => (self.lower_match(match_stmt)?, match_stmt.span), + Stmt::For(loop_stmt) => self.lower_for_statement(loop_stmt)?, + Stmt::Match(match_stmt) => self.lower_match_statement(match_stmt)?, Stmt::Break(span) => (StatementKind::Break, *span), Stmt::Continue(span) => (StatementKind::Continue, *span), Stmt::Assert(assertion) => ( @@ -1470,39 +3527,666 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { self.parent.error(trace.span, "trace target is unresolved"); return None; }; + if let Some(declaration) = self.parent.constant_definitions.get(definition).copied() + { + if self.parent.check.release { + return None; + } + return self.lower_constant_trace(trace, *definition, declaration); + } let Some(local) = self.local_ids.get(definition).copied() else { self.parent.error(trace.span, "trace target is not a local"); return None; }; (StatementKind::Trace(local), trace.span) } - Stmt::Breakpoint(point) => ( - StatementKind::Breakpoint(match &point.condition { - Some(value) => Some(self.lower_expression(value)?), - None => None, - }), - point.span, - ), + Stmt::Breakpoint(point) => { + let enclosing_context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::BreakpointExpression, + ); + let condition = match &point.condition { + Some(value) => self.lower_expression(value).map(Some), + None => Some(None), + }; + self.ownership_context = enclosing_context; + let condition = condition?; + let mut visible = std::collections::BTreeMap::new(); + for scope in &self.visible_bindings { + visible.extend(scope.iter().map(|(name, local)| (name, *local))); + } + if let Some(excluded) = self.parent.check.breakpoint_exclusions.get(&point.span) { + visible.retain(|name, _| !excluded.contains(*name)); + } + ( + StatementKind::Breakpoint { + condition, + bindings: visible.into_values().collect(), + }, + point.span, + ) + } Stmt::Respond(response) => ( StatementKind::Respond(self.lower_expression(&response.value)?), response.span, ), - Stmt::ComptimeTypeBind(binding) => ( - StatementKind::Scope(self.lower_block(&binding.body)), - binding.span, - ), + Stmt::ComptimeTypeBind(binding) => { + (self.lower_comptime_type_bind(binding)?, binding.span) + } Stmt::Use(_) => return None, }; + if self.parent.check.release + && matches!( + kind, + StatementKind::Trace(_) | StatementKind::Breakpoint { .. } + ) + { + // Lower first to consume checked nested-body facts, then discard + // the entire observation before MIR can expand its condition. + return None; + } Some(Statement { kind, span }) } + #[inline(never)] + fn lower_local_declaration(&mut self, decl: &ast::VarDecl) -> Option<(StatementKind, Span)> { + Some({ + let Some(definition) = self.parent.definition_at(decl.name.span, DefKind::Variable) + else { + self.parent + .error(decl.name.span, "local has no resolved definition"); + return None; + }; + let Some(ty) = self + .expression_types + .get(&decl.name.span) + .copied() + .filter(|ty| { + // Bare machine annotations erase precise state in + // the binding while its producer keeps that state. + // Secret qualification also preserves both sides. + matches!( + self.parent.check.interner.resolve(*ty), + Type::Machine(_) | Type::Secret(_) + ) || interface_values::contains_erased_boundary( + &self.parent.check.interner, + *ty, + ) || self + .expression_types + .get(&decl.value.span()) + .is_some_and(|actual| { + interface_values::contains_erased_boundary( + &self.parent.check.interner, + *actual, + ) + }) + }) + .or_else(|| self.expression_types.get(&decl.value.span()).copied()) + .or_else(|| self.parent.check.definition_types.get(&definition).copied()) + else { + self.parent + .error(decl.name.span, "local has no checked type"); + return None; + }; + // The new binding becomes visible only after its initializer, + // including any handled failure and nested breakpoint. + let value = self.lower_expression(&decl.value)?; + if self.binding_modes.get(&decl.name.span) + == Some(&CheckedBindingMode::View { + source: CheckedViewSource::Other, + }) + && let Some((local, rewritten)) = + self.lower_temporary_local_declaration(decl, definition, &value, ty) + { + call_observation::record(self, decl, definition, local, &rewritten); + return Some(( + StatementKind::Let { + local, + value: rewritten, + }, + decl.span, + )); + } + let view_source = self.local_view_source(decl, &value, ty)?; + let local = + self.allocate_local(definition, &decl.name.name, ty, decl.mutable, decl.span); + self.locals[local.index() as usize].view_source = view_source; + if let Err(message) = self.record_mutable_sum_backing(decl, definition, local, &value) { + self.parent.error(decl.span, message); + return None; + } + call_observation::record(self, decl, definition, local, &value); + (StatementKind::Let { local, value }, decl.span) + }) + } + + #[inline(never)] + fn lower_if_statement(&mut self, branch: &ast::IfStmt) -> Option<(StatementKind, Span)> { + Some({ + if let Some(selection) = self.static_selection(branch.span) { + let selected = match selection { + CheckedStaticSelection::IfThen => Some(&branch.then_block), + CheckedStaticSelection::IfElseIf(index) => match branch.else_ifs.get(index) { + Some((_, block)) => Some(block), + None => { + self.parent.error( + branch.span, + "checked static else-if selection is out of range", + ); + return None; + } + }, + CheckedStaticSelection::IfElse => match branch.else_block.as_ref() { + Some(block) => Some(block), + None => { + self.parent.error( + branch.span, + "checked static else selection has no source branch", + ); + return None; + } + }, + CheckedStaticSelection::IfNoBranch => { + if branch.else_block.is_some() { + self.parent.error( + branch.span, + "checked static no-branch selection disagrees with source else", + ); + return None; + } + None + } + CheckedStaticSelection::MatchArm(_) => { + self.parent.error( + branch.span, + "checked static match selection attached to an if statement", + ); + return None; + } + }; + let Some(selected) = selected else { + return None; + }; + ( + StatementKind::Scope(self.lower_block(selected)), + branch.span, + ) + } else { + let condition = self.lower_expression(&branch.condition)?; + let then_block = self.lower_block(&branch.then_block); + let else_block = + self.lower_else_chain(&branch.else_ifs, branch.else_block.as_ref()); + ( + StatementKind::If { + condition, + then_block, + else_block, + }, + branch.span, + ) + } + }) + } + + #[inline(never)] + fn lower_for_statement(&mut self, loop_stmt: &ast::ForStmt) -> Option<(StatementKind, Span)> { + Some({ + let iterable = self.lower_expression(&loop_stmt.iterable)?; + self.visible_bindings.push(HashMap::new()); + let key = self.allocate_declared_local(&loop_stmt.variable, false)?; + let value = match &loop_stmt.value_variable { + Some(binding) => Some(self.allocate_declared_local(binding, false)?), + None => None, + }; + let saved_iteration_bindings = self.view_iteration_bindings.clone(); + if loop_stmt.view { + let part = if matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::Map(..) + ) { + IterationPart::Key + } else { + IterationPart::Element + }; + let bindings = [(key, part)] + .into_iter() + .chain(value.map(|value| (value, IterationPart::Value))); + for (local, part) in bindings { + let metadata = &self.locals[local.index() as usize]; + if metadata.mutable + || metadata.view_source.is_some() + || self.view_parameter_locals.contains(&local) + || self.view_iteration_bindings.contains_key(&local) + { + self.parent.error( + loop_stmt.span, + "viewed iteration binder is not a fresh immutable local", + ); + return None; + } + match checked_view_iteration_binding( + &self.parent.check.interner, + loop_stmt.span, + iterable.ty, + metadata.ty, + part, + ) { + Ok(proof) + if !jett_typecheck::ownership::is_implicitly_copyable( + &self.parent.check.interner, + proof.binder_type(), + ) => + { + self.view_iteration_bindings.insert(local, proof); + } + Ok(_) => {} + Err(message) => { + self.parent.error(loop_stmt.span, message); + return None; + } + } + } + } + let saved_reflected_bindings = std::mem::take(&mut self.retained_reflected_bindings); + if self.required_scopes.is_empty() + && self.reflected_field_root.is_some_and(|root| { + root.stmts.iter().any(|statement| { + matches!(statement, Stmt::For(original) if std::ptr::eq(original, loop_stmt)) + }) + }) + { + self.retained_reflected_bindings = reflected_field_sources::original_loop_bindings( + self.parent.resolve, + self.parent.check, + loop_stmt, + ).into_iter().collect(); + } + let body = if matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::List(element) if *element == TypeInterner::NEVER + ) || matches!( + self.parent.check.interner.resolve(iterable.ty), + Type::Map(key, value) + if *key == TypeInterner::NEVER || *value == TypeInterner::NEVER + ) { + // The frontend still checks this body. An uninhabited + // element type proves that no iteration can enter it; + // retain the iterable so MIR validates pending values. + self.consume_omitted_body_facts(loop_stmt.body.span); + Block { + statements: Vec::new(), + span: loop_stmt.body.span, + } + } else { + self.lower_block(&loop_stmt.body) + }; + self.retained_reflected_bindings = saved_reflected_bindings; + self.view_iteration_bindings = saved_iteration_bindings; + self.visible_bindings.pop(); + ( + StatementKind::For { + key, + value, + by_view: loop_stmt.view, + iterable, + body, + }, + loop_stmt.span, + ) + }) + } + + #[inline(never)] + fn lower_match_statement( + &mut self, + match_stmt: &ast::MatchStmt, + ) -> Option<(StatementKind, Span)> { + Some({ + if let Some(selection) = self.static_selection(match_stmt.span) { + let CheckedStaticSelection::MatchArm(index) = selection else { + self.parent.error( + match_stmt.span, + "checked static if selection attached to a match statement", + ); + return None; + }; + let Some(arm) = match_stmt.arms.get(index) else { + self.parent.error( + match_stmt.span, + "checked static match-arm selection is out of range", + ); + return None; + }; + if matches!(&arm.pattern, ast::Pattern::Variant(_, bindings) if !bindings.is_empty()) + { + self.parent.error( + arm.span, + "checked static match arm unexpectedly binds runtime payloads", + ); + return None; + } + ( + StatementKind::Scope(self.lower_block(&arm.body)), + match_stmt.span, + ) + } else { + (self.lower_match(match_stmt)?, match_stmt.span) + } + }) + } + + fn lower_constant_trace( + &mut self, + trace: &ast::TraceStmt, + definition: DefId, + declaration: Span, + ) -> Option { + let Some(ty) = self.parent.check.definition_types.get(&definition).copied() else { + self.parent + .error(trace.span, "constant trace target has no checked type"); + return None; + }; + // Materialize the baked value only for this observation. The temporary + // is not a lexical binding and must not appear in later breakpoints. + let local = LocalId(self.locals.len() as u32); + self.locals.push(Local { + id: local, + name: trace.name.name.clone(), + ty, + debug_ty: ty, + debug_type_name: self + .parent + .check + .debug_type_names + .get(&declaration) + .cloned(), + mutable: false, + view_source: None, + span: trace.span, + }); + Some(Statement { + kind: StatementKind::Scope(Block { + statements: vec![ + Statement { + kind: StatementKind::Let { + local, + value: Expression { + kind: ExpressionKind::Constant { declaration }, + ty, + span: trace.name.span, + }, + }, + span: trace.span, + }, + Statement { + kind: StatementKind::Trace(local), + span: trace.span, + }, + ], + span: trace.span, + }), + span: trace.span, + }) + } + + fn lower_comptime_type_bind( + &mut self, + binding: &ast::ComptimeTypeBindStmt, + ) -> Option { + let Some(bindings) = self.comptime_type_bindings.get(&binding.span).cloned() else { + self.parent.error( + binding.span, + "comptime type statement has no checked concrete binding", + ); + return None; + }; + self.consumed_comptime_type_bindings.insert(binding.span); + // The checker retains the last recursively checked expansion in its + // legacy flat maps for the interpreter. Those descendant entries are + // owned by `CheckedBodyFacts` and are validated while lowering each + // specialized body, not as siblings in the enclosing function. + self.consumed_static_selections + .extend(self.static_selections.keys().copied().filter(|span| { + span.file == binding.body.span.file + && span.start >= binding.body.span.start + && span.end <= binding.body.span.end + })); + self.consumed_comptime_type_bindings.extend( + self.comptime_type_bindings.keys().copied().filter(|span| { + span.file == binding.body.span.file + && span.start >= binding.body.span.start + && span.end <= binding.body.span.end + }), + ); + + // A trusted reflection loop over no fields has no concrete body to + // dispatch. The runtime loop has no iterations either. + if bindings.is_empty() { + return Some(StatementKind::Scope(Block { + statements: Vec::new(), + span: binding.body.span, + })); + } + + if bindings.len() == 1 + && bindings[0].selection == CheckedComptimeTypeSelection::Unconditional + { + let checked = bindings + .into_iter() + .next() + .expect("single checked binding exists"); + return Some(StatementKind::Scope( + self.lower_bound_type_body(binding, 0, checked), + )); + } + + if bindings.iter().any(|checked| { + !matches!( + checked.selection, + CheckedComptimeTypeSelection::ReflectedIteration(_) + ) + }) { + self.parent.error( + binding.span, + "checked comptime type binding has inconsistent selection semantics", + ); + return None; + } + + // The initializer is a trusted `TypeInfo` value belonging to the + // current reflection-loop element. It is evaluated once, then used to + // select the one checker-specialized body with matching canonical + // reflected type identity. + let type_info = self.lower_expression(&binding.value)?; + let is_type_info = match self.parent.check.interner.resolve(type_info.ty) { + Type::Struct(id) => self.parent.check.interner.resolve_struct(*id).name == "TypeInfo", + _ => false, + }; + if !is_type_info { + self.parent.error( + binding.value.span(), + "reflected comptime type dispatch selector is not TypeInfo", + ); + return None; + } + let retain_ordinals = self.retained_reflected_bindings.contains(&binding.span); + let mut arms = Vec::with_capacity(bindings.len()); + let mut bound_types = HashSet::new(); + for (index, checked) in bindings.into_iter().enumerate() { + let CheckedComptimeTypeSelection::ReflectedIteration(iteration_index) = + checked.selection + else { + unreachable!("selection kind checked above"); + }; + // Several reflected elements may have the same concrete type. + // Share an arm only when both canonical type and source-visible + // reflection agree; aliases can specialize the body differently. + let reflection_identity = checked.reflection.reflection_identity(); + if !retain_ordinals + && !bound_types.insert((checked.bound_type, reflection_identity.clone())) + { + continue; + } + let bound_type = checked.bound_type; + let body = self.lower_bound_type_body(binding, index, checked); + let body = if retain_ordinals { + // An ordinal owns an exact lexical Source body even when another + // field has the same canonical type and reflection identity. + Block { + statements: vec![Statement { + kind: StatementKind::Scope(body), + span: binding.span, + }], + span: binding.body.span, + } + } else { + body + }; + arms.push(ReflectedTypeArm { + iteration_index, + bound_type, + reflection_identity, + body, + }); + } + Some(StatementKind::ReflectedTypeDispatch { type_info, arms }) + } + + fn lower_bound_type_body( + &mut self, + binding: &ast::ComptimeTypeBindStmt, + index: usize, + checked: CheckedComptimeTypeBinding, + ) -> Block { + self.scoped_type_bindings.push(ScopedTypeBinding { + name: binding.name.name.clone(), + ty: checked.bound_type, + reflection: checked.reflection.clone(), + }); + self.required_scopes.push(RequiredScopeIdentity { + owner: binding.span, + index, + selection: checked.selection, + }); + let body = self.lower_block_with_checked_facts(&binding.body, checked.body); + self.required_scopes.pop(); + self.scoped_type_bindings.pop(); + body + } + + fn lower_block_with_checked_facts( + &mut self, + block: &ast::Block, + facts: CheckedBodyFacts, + ) -> Block { + let CheckedBodyFacts { + type_map, + source_type_map, + debug_type_names, + binding_modes, + call_ownership, + binding_facts, + assignment_places, + actor_message_calls, + generic_calls, + intrinsic_ids, + intrinsic_type_arguments, + intrinsic_reflection_arguments, + call_argument_orders, + method_calls, + interface_calls, + method_values, + struct_constructions, + pipeline_step_call_types, + pipeline_step_input_types, + static_selections, + comptime_type_bindings, + } = facts; + + let saved_expression_types = std::mem::replace(&mut self.expression_types, type_map); + let saved_source_expression_types = + std::mem::replace(&mut self.source_expression_types, source_type_map); + let saved_debug_type_names = + std::mem::replace(&mut self.debug_type_names, debug_type_names); + let saved_binding_modes = std::mem::replace(&mut self.binding_modes, binding_modes); + let saved_call_ownership = std::mem::replace(&mut self.call_ownership, call_ownership); + let saved_actor_message_calls = + std::mem::replace(&mut self.actor_message_calls, actor_message_calls); + let saved_binding_facts = std::mem::replace(&mut self.binding_facts, binding_facts); + let saved_assignment_places = + std::mem::replace(&mut self.assignment_places, assignment_places); + let saved_generic_calls = std::mem::replace(&mut self.generic_calls, generic_calls); + let saved_intrinsic_ids = std::mem::replace(&mut self.intrinsic_ids, intrinsic_ids); + let saved_intrinsic_type_arguments = + std::mem::replace(&mut self.intrinsic_type_arguments, intrinsic_type_arguments); + let saved_intrinsic_reflection_arguments = std::mem::replace( + &mut self.intrinsic_reflection_arguments, + intrinsic_reflection_arguments, + ); + let saved_call_argument_orders = + std::mem::replace(&mut self.call_argument_orders, call_argument_orders); + let saved_method_calls = std::mem::replace(&mut self.method_calls, method_calls); + let saved_interface_calls = std::mem::replace(&mut self.interface_calls, interface_calls); + let saved_method_values = std::mem::replace(&mut self.method_values, method_values); + let saved_struct_constructions = + std::mem::replace(&mut self.struct_constructions, struct_constructions); + let saved_pipeline_step_call_types = + std::mem::replace(&mut self.pipeline_step_call_types, pipeline_step_call_types); + let saved_pipeline_step_input_types = std::mem::replace( + &mut self.pipeline_step_input_types, + pipeline_step_input_types, + ); + let saved_static_selections = + std::mem::replace(&mut self.static_selections, static_selections); + let saved_comptime_type_bindings = + std::mem::replace(&mut self.comptime_type_bindings, comptime_type_bindings); + let saved_consumed_static = std::mem::take(&mut self.consumed_static_selections); + let saved_consumed_comptime = std::mem::take(&mut self.consumed_comptime_type_bindings); + let saved_local_ids = self.local_ids.clone(); + + let lowered = self.lower_block(block); + self.reject_unconsumed_static_selections(); + self.reject_unconsumed_comptime_type_bindings(); + + self.local_ids = saved_local_ids; + self.expression_types = saved_expression_types; + self.source_expression_types = saved_source_expression_types; + self.debug_type_names = saved_debug_type_names; + self.binding_modes = saved_binding_modes; + self.call_ownership = saved_call_ownership; + self.binding_facts = saved_binding_facts; + self.actor_message_calls = saved_actor_message_calls; + self.assignment_places = saved_assignment_places; + self.generic_calls = saved_generic_calls; + self.intrinsic_ids = saved_intrinsic_ids; + self.intrinsic_type_arguments = saved_intrinsic_type_arguments; + self.intrinsic_reflection_arguments = saved_intrinsic_reflection_arguments; + self.call_argument_orders = saved_call_argument_orders; + self.method_calls = saved_method_calls; + self.interface_calls = saved_interface_calls; + self.method_values = saved_method_values; + self.struct_constructions = saved_struct_constructions; + self.pipeline_step_call_types = saved_pipeline_step_call_types; + self.pipeline_step_input_types = saved_pipeline_step_input_types; + self.static_selections = saved_static_selections; + self.comptime_type_bindings = saved_comptime_type_bindings; + self.consumed_static_selections = saved_consumed_static; + self.consumed_comptime_type_bindings = saved_consumed_comptime; + lowered + } + fn allocate_declared_local(&mut self, name: &ast::Ident, mutable: bool) -> Option { let Some(definition) = self.parent.definition_at(name.span, DefKind::Variable) else { self.parent .error(name.span, "binding has no resolved definition"); return None; }; - let Some(ty) = self.parent.check.definition_types.get(&definition).copied() else { + let Some(ty) = self + .expression_types + .get(&name.span) + .copied() + .or_else(|| self.parent.check.definition_types.get(&definition).copied()) + else { self.parent.error(name.span, "binding has no checked type"); return None; }; @@ -1534,9 +4218,95 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { tail } + fn lower_interpolation_value(&mut self, expression: &Expr) -> Option { + let value = self.lower_expression(expression)?; + let method_span = self + .parent + .check + .method_definitions + .iter() + .find(|method| { + method.owner_type == value.ty + && method.interface_name.as_deref() == Some("Displayable") + && method.method_name == "display" + && method.parameter_types == [value.ty] + && method.return_type == TypeInterner::STRING + }) + .map(|method| method.source_span); + let Some(method_span) = method_span else { + if matches!( + self.parent.check.interner.resolve(value.ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Nothing + ) { + return Some(value); + } + self.parent.error( + expression.span(), + "displayable interpolation has no checked display method", + ); + return None; + }; + let Some(function) = self + .function_ids + .get(&FunctionKey::Method { + source_span: method_span, + }) + .copied() + else { + self.parent.error( + expression.span(), + "displayable interpolation has no concrete HIR method", + ); + return None; + }; + let (parameter_types, _) = self + .ownership_function_parameters(function) + .map_err(|message| self.parent.error(expression.span(), message)) + .ok()?; + let args = vec![value]; + let ownership = self.generated_ownership( + GeneratedOperation::Display { method: function }, + &args, + ¶meter_types, + &[CheckedCalleeAccess::View], + TypeInterner::STRING, + &[0], + expression.span(), + )?; + Some(Expression { + kind: ExpressionKind::DisplayResult(Box::new(Expression { + kind: ExpressionKind::Call { + ownership, + function, + args, + evaluation_order: vec![0], + }, + ty: TypeInterner::STRING, + span: expression.span(), + })), + ty: TypeInterner::STRING, + span: expression.span(), + }) + } + fn lower_expression(&mut self, expression: &Expr) -> Option { let span = expression.span(); let ty = self.expression_type(expression)?; + if let Some(root) = call_ownership::source_projection_ast_root(expression) { + self.source_projection_roots.insert(span, root.clone()); + } let kind = match expression { Expr::IntLiteral(value, _) => ExpressionKind::Int(*value), Expr::FloatLiteral(value, _) => ExpressionKind::Float(*value), @@ -1550,33 +4320,86 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { .error(ident.span, "identifier has no resolved definition"); return None; }; - let Some(local) = self.local_ids.get(&definition).copied() else { + if let Some(local) = self.local_ids.get(&definition).copied() { + ExpressionKind::Local(local) + } else if let Some(declaration) = self.parent.constant_definitions.get(&definition) + { + ExpressionKind::Constant { + declaration: *declaration, + } + } else if let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(definition) + { + ExpressionKind::ResourceHookValue { hook } + } else if self.parent.resolve.scope_table.def(definition).kind == DefKind::Function + { + ExpressionKind::FunctionRef(self.resolve_function_value_target(expression)?) + } else { self.parent.error( span, - "identifier is not a local value in the initial HIR subset", + "identifier is neither a local nor a checked concrete function value", ); return None; - }; - ExpressionKind::Local(local) + } + } + Expr::Binary(left, op @ (ast::BinOp::Eq | ast::BinOp::NotEq), right, _) + if self.method_calls.contains_key(&span) => + { + self.lower_equality_expression(left, *op, right, ty, span)? } Expr::Binary(left, op, right, _) => ExpressionKind::Binary { left: Box::new(self.lower_expression(left)?), op: lower_binary_op(*op), right: Box::new(self.lower_expression(right)?), }, + Expr::Unary(ast::UnaryOp::Neg, value, _) => match value.as_ref() { + Expr::IntLiteral(value, _) => { + let Some(value) = value.checked_neg() else { + self.parent + .error(span, "negated integer literal exceeds the HIR value range"); + return None; + }; + ExpressionKind::Int(value) + } + Expr::FloatLiteral(value, _) => ExpressionKind::Float(-value), + _ => ExpressionKind::Unary { + op: UnaryOp::Negate, + value: Box::new(self.lower_expression(value)?), + }, + }, Expr::Unary(op, value, _) => ExpressionKind::Unary { op: lower_unary_op(*op), value: Box::new(self.lower_expression(value)?), }, Expr::FieldAccess(base, field, _) => { - if self.enum_variant_index(ty, field).is_some() { + if let Some(definition) = self.parent.resolve.resolutions.get(&span) + && let Some(hook) = self + .parent + .resource_manifest + .hook_for_definition(*definition) + { + return Some(Expression { + kind: ExpressionKind::ResourceHookValue { hook }, + ty, + span, + }); + } + + if self.method_values.contains_key(&span) + || self.resolved_expression_kind(expression) == Some(DefKind::Function) + { + ExpressionKind::FunctionRef(self.resolve_function_value_target(expression)?) + } else if self.enum_constructor_variant(expression, ty).is_some() { self.lower_enum_construct(ty, field, &[], span)? } else { self.lower_field(base, field)? } } - Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { - self.lower_call(callee, args, span)? + Expr::Call(callee, args, _) => self.lower_call(callee, args, span, false)?, + Expr::GenericCall(callee, type_args, args, _) => { + self.lower_call(callee, args, span, !type_args.is_empty())? } Expr::ListConstruct(elements, _) => ExpressionKind::ListConstruct { elements: elements @@ -1603,17 +4426,18 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ExpressionKind::OptionalSome(Box::new(self.lower_expression(value)?)) } Expr::None(_) => ExpressionKind::OptionalNone, - Expr::StringInterpolation(parts, _) => ExpressionKind::StringInterpolation( - parts - .iter() - .map(|part| match part { - ast::StringPart::Literal(text) => Some(StringSegment::Text(text.clone())), + Expr::StringInterpolation(parts, _) => { + let mut segments = Vec::with_capacity(parts.len()); + for part in parts { + segments.push(match part { + ast::StringPart::Literal(text) => StringSegment::Text(text.clone()), ast::StringPart::Expr(value) => { - Some(StringSegment::Value(self.lower_expression(value)?)) + StringSegment::Value(self.lower_interpolation_value(value)?) } - }) - .collect::>>()?, - ), + }); + } + ExpressionKind::StringInterpolation(segments) + } Expr::EnumVariant(_, variant, _) => { self.lower_enum_construct(ty, variant, &[], span)? } @@ -1625,15 +4449,29 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return self.lower_pipeline(initial, steps, ty, span); } Expr::Paren(inner, _) => { + let first_inline = self.parent.source_inline_nodes.len(); return self.lower_expression(inner).map(|mut lowered| { lowered.span = span; lowered.ty = ty; + // Parentheses widen HIR spans without replacing the + // physical inline source node retained by its proof. + if matches!(lowered.kind, ExpressionKind::InlineFunction { .. }) { + let node = std::ptr::from_ref(Self::unparenthesized(inner)).addr(); + for source in &mut self.parent.source_inline_nodes[first_inline..] { + if source.source_node == node { + source.original = lowered.clone(); + } + } + } lowered }); } - Expr::Comptime(value, _) => { - ExpressionKind::Comptime(Box::new(self.lower_expression(value)?)) - } + Expr::Comptime(value, source_span) => ExpressionKind::Comptime { + value: Box::new(self.lower_expression(value)?), + bindings: self.scoped_type_bindings.clone(), + scopes: self.required_scopes.clone(), + source_span: *source_span, + }, Expr::Declassify(value, _) => { ExpressionKind::Declassify(Box::new(self.lower_expression(value)?)) } @@ -1672,38 +4510,15 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { ExpressionKind::Cancel(Box::new(self.lower_expression(value)?)) } Expr::InlineFn(params, _, body, _) => { - let mut lowered_params = Vec::with_capacity(params.len()); - for param in params { - let Some(definition) = - self.parent.definition_at(param.name.span, DefKind::Param) - else { - self.parent - .error(param.name.span, "closure parameter is unresolved"); - return None; - }; - let Some(param_type) = - self.parent.check.definition_types.get(&definition).copied() - else { - self.parent - .error(param.name.span, "closure parameter has no checked type"); - return None; - }; - lowered_params.push(self.allocate_local( - definition, - ¶m.name.name, - param_type, - param.mutable, - param.span, - )); - } - ExpressionKind::InlineFunction { - params: lowered_params, - body: self.lower_block(body), - } + self.lower_inline_function(params, body, ty, span)? + } + Expr::Spawn(inner, _) => self.lower_actor_spawn(inner, ty)?, + Expr::Send(inner, span) => { + self.lower_actor_message(inner, *span, ActorMessageKind::Send)? + } + Expr::Ask(inner, span) => { + self.lower_actor_message(inner, *span, ActorMessageKind::Ask)? } - Expr::Spawn(inner, _) => self.lower_actor_spawn(inner)?, - Expr::Send(inner, _) => self.lower_actor_message(inner, ActorMessageKind::Send)?, - Expr::Ask(inner, _) => self.lower_actor_message(inner, ActorMessageKind::Ask)?, Expr::View(value, _) => ExpressionKind::View(Box::new(self.lower_expression(value)?)), Expr::Clone(value, _) => ExpressionKind::Clone(Box::new(self.lower_expression(value)?)), unsupported => { @@ -1714,26 +4529,206 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; } }; - Some(Expression { kind, ty, span }) - } - - fn lower_actor_spawn(&mut self, inner: &Expr) -> Option { - let (callee, args, call_span) = match inner { - Expr::Call(callee, args, span) => (callee.as_ref(), args.as_slice(), *span), - _ => (inner, &[][..], inner.span()), - }; - let actor_type = self.canonical_call_name(callee)?; - let (args, evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; - Some(ExpressionKind::ActorSpawn { + let lowered = Expression { kind, ty, span }; + if self.parent.reflected_checked_program.is_some() + && matches!(expression, Expr::InlineFn(..)) + { + self.parent + .source_inline_nodes + .push(inline_functions::LoweredSourceInline { + source_node: std::ptr::from_ref(expression).addr(), + root: None, + original: lowered.clone(), + }); + } + let checked_constructor_call = match expression { + Expr::Call(callee, ..) | Expr::GenericCall(callee, ..) => { + let callee = Self::unparenthesized(callee); + self.struct_constructions.contains_key(&span) + || self.is_declaration_reference(callee, DefKind::Bitfield) + || self.is_declaration_reference(callee, DefKind::Machine) + } + _ => false, + }; + Some( + if matches!( + expression, + Expr::ListConstruct(..) + | Expr::MapConstruct(..) + | Expr::Some(..) + | Expr::None(..) + | Expr::Ok(..) + | Expr::Fail(..) + ) || checked_constructor_call + { + normalize_secret_constructor(lowered, &self.parent.check.interner) + } else { + lowered + }, + ) + } + + #[inline(never)] + fn lower_equality_expression( + &mut self, + left: &Expr, + op: ast::BinOp, + right: &Expr, + ty: TypeId, + span: Span, + ) -> Option { + Some({ + let function = self.resolve_user_call_target(left, span)?; + let args = vec![self.lower_expression(left)?, self.lower_expression(right)?]; + let (parameter_types, _) = self + .ownership_function_parameters(function) + .map_err(|message| self.parent.error(span, message)) + .ok()?; + let ownership = self.generated_ownership( + GeneratedOperation::Equality { method: function }, + &args, + ¶meter_types, + &[CheckedCalleeAccess::View, CheckedCalleeAccess::View], + ty, + &[0, 1], + span, + )?; + let call = ExpressionKind::EquatableResult(Box::new(Expression { + kind: ExpressionKind::Call { + ownership, + function, + args, + evaluation_order: vec![0, 1], + }, + ty, + span, + })); + if op == ast::BinOp::NotEq { + ExpressionKind::Unary { + op: UnaryOp::Not, + value: Box::new(Expression { + kind: call, + ty, + span, + }), + } + } else { + call + } + }) + } + + #[inline(never)] + fn lower_inline_function( + &mut self, + params: &[ast::Param], + body: &ast::Block, + ty: TypeId, + span: Span, + ) -> Option { + Some({ + let Type::Function { + params: parameter_types, + view_params: parameter_views, + return_type, + .. + } = self.parent.check.interner.resolve(ty).clone() + else { + self.parent + .error(span, "closure has no checked function signature"); + return None; + }; + if params.len() != parameter_types.len() + || params.len() != parameter_views.len() + || params + .iter() + .zip(¶meter_views) + .any(|(param, view)| param.view != *view) + { + self.parent.error( + span, + "closure parameters disagree with its checked signature", + ); + return None; + } + let local_floor = self.locals.len() as u32; + self.visible_bindings.push(HashMap::new()); + let mut lowered_params = Vec::with_capacity(params.len()); + let mut view_params = Vec::new(); + for (param, param_type) in params.iter().zip(parameter_types) { + let Some(definition) = self.parent.definition_at(param.name.span, DefKind::Param) + else { + self.parent + .error(param.name.span, "closure parameter is unresolved"); + return None; + }; + // A generic instantiation's expression signature contains + // its concrete parameter types; global definition metadata + // shares source DefIds across specializations. + let local = self.allocate_local( + definition, + ¶m.name.name, + param_type, + param.mutable, + param.span, + ); + lowered_params.push(local); + if param.view { + view_params.push(local); + } + } + let enclosing_return_type = self.return_type.replace(return_type); + let enclosing_context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ); + let enclosing_view_parameters = std::mem::replace( + &mut self.view_parameter_locals, + view_params.iter().copied().collect(), + ); + let enclosing_iteration_bindings = std::mem::take(&mut self.view_iteration_bindings); + let enclosing_match_bindings = std::mem::take(&mut self.match_bindings); + let body = self.lower_block(body); + self.view_parameter_locals = enclosing_view_parameters; + self.view_iteration_bindings = enclosing_iteration_bindings; + self.match_bindings = enclosing_match_bindings; + self.ownership_context = enclosing_context; + self.return_type = enclosing_return_type; + self.visible_bindings.pop(); + ExpressionKind::InlineFunction { + scoped_type_bindings: self.scoped_type_bindings.clone(), + params: lowered_params, + view_params, + local_floor, + body, + } + }) + } + + fn lower_actor_spawn(&mut self, inner: &Expr, ty: TypeId) -> Option { + let (callee, args, call_span) = match inner { + Expr::Call(callee, args, span) => (callee.as_ref(), args.as_slice(), *span), + _ => (inner, &[][..], inner.span()), + }; + let actor_type = self.dotted_expression_name(callee)?; + let (args, evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; + let Some(&constructor) = self.parent.actor_constructor_ids.get(&ty) else { + self.parent + .error(call_span, "actor spawn has no checked constructor"); + return None; + }; + Some(ExpressionKind::ActorSpawn { actor_type, args, evaluation_order, + constructor: Some(constructor), }) } fn lower_actor_message( &mut self, inner: &Expr, + source_span: Span, kind: ActorMessageKind, ) -> Option { let (callee, args, call_span) = match inner { @@ -1746,16 +4741,214 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; }; let actor = Box::new(self.lower_expression(actor)?); + let Some(&handler) = self + .parent + .actor_handler_ids + .get(&(actor.ty, message.name.clone())) + else { + self.parent + .error(message.span, "actor message has no checked handler"); + return None; + }; let (args, evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; + let proof = match actor_handlers::message_proof( + self, + inner, + source_span, + &actor, + &message.name, + handler, + &args, + &evaluation_order, + kind, + ) { + Ok(proof) => proof, + Err(message) => { + self.parent.error(source_span, message); + return None; + } + }; Some(ExpressionKind::ActorMessage { actor, message: message.name.clone(), + handler, args, evaluation_order, kind, + proof, + }) + } + + fn refine_return_value(&mut self, value: Expression, span: Span) -> Option { + let Some(return_type) = self.return_type else { + return Some(value); + }; + if value.ty == return_type + || value.ty == TypeInterner::NEVER + || !matches!( + self.parent.check.interner.resolve(return_type), + Type::Refinement { .. } + ) + { + return Some(value); + } + let mut predicates = self.checked_refinement_predicates(return_type, span)?; + if let Some(index) = predicates + .iter() + .position(|predicate| predicate.refined_type == value.ty) + { + predicates.drain(..=index); + } + let error_local = LocalId(self.locals.len() as u32); + self.locals.push(Local { + id: error_local, + name: "$return.error".to_string(), + ty: TypeInterner::STRING, + debug_ty: TypeInterner::STRING, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }); + let failure = Block { + statements: vec![Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::RuntimeFailureMessage(Box::new(Expression { + kind: ExpressionKind::Local(error_local), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::NOTHING, + span, + }), + span, + }], + span, + }; + Some(Expression { + kind: ExpressionKind::Handle { + target: Box::new(value), + kind: HandleKind::Refinement { + refined_type: return_type, + predicates, + }, + error_local: Some(error_local), + failure, + }, + ty: return_type, + span, }) } + fn checked_refinement_predicates( + &mut self, + refined_type: TypeId, + span: Span, + ) -> Option> { + let mut predicates = Vec::new(); + let mut base_type = refined_type; + while let Type::Refinement { base, .. } = self.parent.check.interner.resolve(base_type) { + base_type = *base; + } + let input_type = if let Type::Secret(inner) = self.parent.check.interner.resolve(base_type) + { + *inner + } else { + base_type + }; + let mut current = refined_type; + while let Type::Refinement { name, base } = self.parent.check.interner.resolve(current) { + let Some(function) = self.parent.refinement_function_ids.get(¤t).copied() else { + self.parent + .error(span, "refinement has no checked predicate function"); + return None; + }; + predicates.push(RefinementPredicate { + refined_type: current, + type_name: name.clone(), + function, + base_type, + input_type, + }); + current = *base; + } + predicates.reverse(); + Some(predicates) + } + + fn reflected_finish_predicates( + &mut self, + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + span: Span, + ) -> Option>> { + if intrinsic != IntrinsicId::TypeConstructFinish || type_arguments.len() != 1 { + return Some(Vec::new()); + } + let field_types = match self.parent.check.interner.resolve(type_arguments[0]) { + Type::Struct(id) => self + .parent + .check + .interner + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(), + Type::Enum(id) => self + .parent + .check + .interner + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|(_, ty)| *ty)) + .collect::>(), + Type::Machine(id) => self + .parent + .check + .interner + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|(_, ty)| *ty)) + .collect::>(), + Type::MachineState { machine, state } => self + .parent + .check + .interner + .resolve_machine(*machine) + .state(*state) + .expect("checked machine state") + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(), + _ => return Some(Vec::new()), + }; + if !field_types.iter().any(|ty| { + matches!( + self.parent.check.interner.resolve(*ty), + Type::Refinement { .. } + ) + }) { + return Some(Vec::new()); + } + field_types + .into_iter() + .map(|ty| { + if matches!( + self.parent.check.interner.resolve(ty), + Type::Refinement { .. } + ) { + self.checked_refinement_predicates(ty, span) + } else { + Some(Vec::new()) + } + }) + .collect() + } + fn lower_handle( &mut self, target: Expression, @@ -1764,19 +4957,38 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { output_type: TypeId, span: Span, ) -> Option { - let kind = match self.parent.check.interner.resolve(target.ty) { - Type::Result(_, _) => HandleKind::Result, - Type::Optional(_) => HandleKind::Optional, - _ if matches!( - self.parent.check.interner.resolve(output_type), - Type::Refinement { .. } - ) => - { + let refinement_predicates = if matches!( + self.parent.check.interner.resolve(output_type), + Type::Refinement { .. } + ) { + Some(self.checked_refinement_predicates(output_type, span)?) + } else { + None + }; + let refines_whole_sum = refinement_predicates.as_ref().is_some_and(|predicates| { + predicates.first().is_some_and(|predicate| { + target.ty == predicate.base_type || target.ty == predicate.input_type + }) + }); + let kind = match ( + self.parent.check.interner.resolve(target.ty), + refinement_predicates, + ) { + (Type::Result(_, _), _) if !refines_whole_sum => HandleKind::Result, + (Type::Optional(_), _) if !refines_whole_sum => HandleKind::Optional, + (_, Some(mut predicates)) => { + if let Some(index) = predicates + .iter() + .position(|predicate| predicate.refined_type == target.ty) + { + predicates.drain(..=index); + } HandleKind::Refinement { refined_type: output_type, + predicates, } } - _ => { + (_, None) => { self.parent.error( span, "checked handle target is neither result, optional, nor refinement", @@ -1784,13 +4996,19 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { return None; } }; + self.visible_bindings.push(HashMap::new()); let error_local = if let Some(name) = error_name { let Some(definition) = self.parent.definition_at(name.span, DefKind::Variable) else { self.parent .error(name.span, "handle error binding has no resolved definition"); return None; }; - let Some(ty) = self.parent.check.definition_types.get(&definition).copied() else { + let ty = match self.parent.check.interner.resolve(target.ty) { + _ if matches!(kind, HandleKind::Refinement { .. }) => Some(TypeInterner::STRING), + Type::Result(_, error) => Some(*error), + _ => self.parent.check.definition_types.get(&definition).copied(), + }; + let Some(ty) = ty else { self.parent .error(name.span, "handle error binding has no checked type"); return None; @@ -1800,6 +5018,7 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { None }; let failure = self.lower_block(failure); + self.visible_bindings.pop(); Some(Expression { kind: ExpressionKind::Handle { target: Box::new(target), @@ -1817,19 +5036,39 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { callee: &Expr, args: &[ast::CallArg], call_span: Span, + has_explicit_type_arguments: bool, ) -> Option { - let enum_variant = match callee { - Expr::EnumVariant(_, variant, _) => Some(variant), - Expr::FieldAccess(_, field, _) - if self - .expression_types - .get(&call_span) - .is_some_and(|ty| self.enum_variant_index(*ty, field).is_some()) => - { - Some(field) - } - _ => None, - }; + if let Some(call) = self.interface_calls.get(&call_span) { + let key = FunctionKey::Interface { + owner: call.interface_type, + method: call.method_index, + }; + let function = self.function_ids.get(&key).copied()?; + self.check_source_actuals(call_span, args)?; + let (args, evaluation_order) = + self.lower_arguments_in_parameter_order(args, call_span)?; + let ownership = self.source_ownership(call_span, &args, &evaluation_order)?; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &args, + call_span, + )?, + function, + args, + evaluation_order, + }); + } + // Call checking treats parentheses as transparent when selecting a + // declaration, intrinsic, or function-value signature. Its checked + // callee type and definition therefore belong to the inner expression. + let callee = Self::unparenthesized(callee); + let enum_variant = self + .expression_types + .get(&call_span) + .and_then(|ty| self.enum_constructor_variant(callee, *ty)); if let Some(variant) = enum_variant { let ty = self.expression_types.get(&call_span).copied().or_else(|| { self.parent @@ -1838,1195 +5077,5400 @@ impl<'lowerer, 'program> BodyLowerer<'lowerer, 'program> { })?; return self.lower_enum_construct(ty, variant, args, call_span); } - if let Expr::Ident(name) = callee - && self.resolved_kind(name) == Some(DefKind::Bitfield) - { - return self.lower_bitfield_construct(args, call_span); - } - if let Expr::Ident(name) = callee - && self.resolved_kind(name) == Some(DefKind::Machine) - { - return self.lower_machine_construct(args, call_span); + if self.is_declaration_reference(callee, DefKind::Bitfield) { + return self.lower_bitfield_construct(callee, args, call_span); } if let Expr::FieldAccess(base, member, _) = callee && member.name == "transition" - && matches!(base.as_ref(), Expr::Ident(name) if self.resolved_kind(name) == Some(DefKind::Machine)) + && (self.resolved_expression_kind(base) == Some(DefKind::Machine) + || self.resolved_expression_kind(callee) == Some(DefKind::Machine)) { return self.lower_machine_transition(args, call_span); } - if let Some(construction) = self.struct_constructions.get(&call_span) { + if self.is_declaration_reference(callee, DefKind::Machine) { + return self.lower_machine_construct(callee, args, call_span); + } + if let Some(construction) = self.struct_constructions.get(&call_span).cloned() { let (fields, evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; + let refinement_predicates = if construction.validates_refinements { + let Type::Struct(id) = self.parent.check.interner.resolve(construction.struct_type) + else { + self.parent.error( + call_span, + "checked struct constructor target is not a struct", + ); + return None; + }; + let field_types = self + .parent + .check + .interner + .resolve_struct(*id) + .fields + .clone(); + if field_types.len() != fields.len() { + self.parent + .error(call_span, "checked struct constructor field count changed"); + return None; + } + let mut chains = Vec::with_capacity(fields.len()); + for (field, (_, expected)) in fields.iter().zip(field_types) { + if field.ty != expected + && matches!( + self.parent.check.interner.resolve(expected), + Type::Refinement { .. } + ) + { + let mut predicates = + self.checked_refinement_predicates(expected, call_span)?; + if let Some(index) = predicates + .iter() + .position(|predicate| predicate.refined_type == field.ty) + { + predicates.drain(..=index); + } + chains.push(predicates); + } else { + chains.push(Vec::new()); + } + } + chains + } else { + Vec::new() + }; return Some(ExpressionKind::StructConstruct { struct_type: construction.struct_type, fields, evaluation_order, validates_refinements: construction.validates_refinements, + refinement_predicates, }); } - let (lowered_args, evaluation_order) = + let (mut lowered_args, mut evaluation_order) = self.lower_arguments_in_parameter_order(args, call_span)?; - if matches!( - callee, - Expr::Ident(ident) - if matches!(self.resolved_kind(ident), Some(DefKind::Variable | DefKind::Param)) - ) { + self.check_source_actuals(call_span, args)?; + let ownership = self.source_ownership(call_span, &lowered_args, &evaluation_order)?; + if let CallOwnership::Source(source) = &ownership + && let CallTarget::ResourceHook(hook) = &source.target + { + let hook = hook.clone(); + let ownership = + self.finish_resource_call_ownership(ownership, &hook, &lowered_args, call_span)?; + return Some(ExpressionKind::ResourceInvoke { + hook, + ownership, + args: lowered_args, + evaluation_order, + }); + } + let source_call = self.is_source_call(callee, call_span); + // A callee can be any checked function value, including a projected + // field or another call's result. Declaration and intrinsic identities + // still select their existing direct paths. + if !source_call + && !self.intrinsic_ids.contains_key(&call_span) + && self.is_checked_function_value(callee) + { return Some(ExpressionKind::IndirectCall { + ownership: self.finish_call_ownership( + ownership, + None, + None, + &lowered_args, + call_span, + )?, callee: Box::new(self.lower_expression(callee)?), args: lowered_args, evaluation_order, }); } - if self.is_source_call(callee, call_span) { + if source_call { + let function = self.resolve_user_call_target(callee, call_span)?; Some(ExpressionKind::Call { - function: self.resolve_user_call_target(callee, call_span)?, + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, args: lowered_args, evaluation_order, }) } else { - Some(ExpressionKind::Intrinsic { - canonical_name: self.canonical_call_name(callee)?, - args: lowered_args, - evaluation_order, - }) - } - } - - fn lower_enum_construct( - &mut self, - enum_type: TypeId, - variant: &ast::Ident, - args: &[ast::CallArg], - span: Span, - ) -> Option { - let Some(index) = self.enum_variant_index(enum_type, variant) else { - self.parent - .error(span, "checked enum construction has no matching variant"); - return None; - }; - let payloads = args - .iter() - .map(|arg| self.lower_expression(&arg.value)) - .collect::>>()?; - Some(ExpressionKind::EnumConstruct { - enum_type, - variant: VariantId(index as u32), - payloads, - }) - } - - fn enum_variant_index(&self, enum_type: TypeId, variant: &ast::Ident) -> Option { - let Type::Enum(enum_id) = *self.parent.check.interner.resolve(enum_type) else { - return None; - }; - self.parent - .check - .interner - .resolve_enum(enum_id) - .variants - .iter() - .position(|candidate| candidate.name == variant.name) - } - - fn resolved_kind(&self, ident: &ast::Ident) -> Option { - self.parent - .resolve - .resolutions - .get(&ident.span) - .map(|definition| self.parent.resolve.scope_table.def(*definition).kind) + let intrinsic = self.checked_intrinsic_id(call_span)?; + let type_arguments = + self.checked_intrinsic_type_arguments(call_span, has_explicit_type_arguments)?; + let mut reflection_arguments = self + .intrinsic_reflection_arguments + .get(&call_span) + .cloned() + .unwrap_or_default(); + let raw_json_source = match intrinsic { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => { + Some("serialize_raw") + } + IntrinsicId::JsonParse | IntrinsicId::JsonParseExact => Some("parse_raw"), + _ => None, + }; + if let Some(name) = raw_json_source + && type_arguments.len() == 1 + && lowered_args.len() == 1 + && self.is_raw_json_tree(type_arguments[0]) + { + let Some(function) = self.trusted_stdlib_function("json", name) else { + self.parent + .error(call_span, "trusted raw JSON source is missing"); + return None; + }; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + let enum_json_source = match intrinsic { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => { + Some("json_serialize_native_enum") + } + IntrinsicId::JsonParse => Some("json_parse_native_enum"), + IntrinsicId::JsonParseExact => Some("json_parse_exact_native_enum"), + _ => None, + }; + if let Some(name) = enum_json_source + && type_arguments.len() == 1 + && lowered_args.len() == 1 + && self.is_non_raw_json_enum(type_arguments[0]) + && let Some(function) = + self.trusted_stdlib_generic_function("json", name, call_span) + { + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + let bitfield_json_source = match intrinsic { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => { + Some("json_serialize_native_bitfield") + } + IntrinsicId::JsonParse => Some("json_parse_native_bitfield"), + IntrinsicId::JsonParseExact => Some("json_parse_exact_native_bitfield"), + _ => None, + }; + if let Some(name) = bitfield_json_source + && type_arguments.len() == 1 + && lowered_args.len() == 1 + && matches!( + self.parent.check.interner.resolve(type_arguments[0]), + Type::Bitfield(_) + ) + && let Some(function) = + self.trusted_stdlib_generic_function("json", name, call_span) + { + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + if matches!( + intrinsic, + IntrinsicId::JsonParse | IntrinsicId::JsonParseExact + ) && type_arguments.len() == 1 + && lowered_args.len() == 1 + && self.is_secret_raw_json_tree(type_arguments[0]) + { + let Some(function) = + self.trusted_stdlib_function("json", "json_parse_native_secret_tree") + else { + self.parent + .error(call_span, "trusted secret raw JSON source is missing"); + return None; + }; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + if matches!( + intrinsic, + IntrinsicId::JsonParse | IntrinsicId::JsonParseExact + ) && type_arguments.len() == 1 + && lowered_args.len() == 1 + && let Some(name) = self.native_json_primitive_parser(type_arguments[0]) + { + let Some(function) = self.trusted_stdlib_function("json", name) else { + self.parent + .error(call_span, "trusted primitive JSON parser is missing"); + return None; + }; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + if matches!( + intrinsic, + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic + ) && type_arguments.len() == 1 + && lowered_args.len() == 1 + && matches!( + self.parent.check.interner.resolve(type_arguments[0]), + Type::Bytes + ) + { + let Some(function) = + self.trusted_stdlib_function("json", "json_serialize_native_bytes") + else { + self.parent + .error(call_span, "trusted bytes JSON serializer is missing"); + return None; + }; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &lowered_args, + call_span, + )?, + function, + args: lowered_args, + evaluation_order, + }); + } + if matches!( + intrinsic, + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic + ) && type_arguments.len() == 1 + && lowered_args.len() == 1 + && let Some(kind) = self.lower_primitive_json_serialization( + type_arguments[0], + &lowered_args[0], + &ownership, + call_span, + ) + { + return Some(kind); + } + if matches!( + intrinsic, + IntrinsicId::TypeInfo + | IntrinsicId::TypeKindTag + | IntrinsicId::TypePrimitiveTag + | IntrinsicId::TypeFields + | IntrinsicId::TypeBitfieldFields + | IntrinsicId::TypeBitfieldLayout + | IntrinsicId::TypeMachineLayout + | IntrinsicId::TypeMachineStates + | IntrinsicId::TypeMachineTransitions + | IntrinsicId::TypeVariants + ) { + if !lowered_args.is_empty() + || type_arguments.len() != 1 + || reflection_arguments.len() != 1 + { + self.parent.error( + call_span, + "reflection intrinsic has no checked type operand", + ); + return None; + } + let ty = self.expression_types.get(&call_span).copied()?; + let info = &reflection_arguments[0]; + return match intrinsic { + IntrinsicId::TypeInfo => self.lower_reflection_type_info(info, ty, call_span), + IntrinsicId::TypeKindTag => self + .reflected_enum_value( + ty, + ReflectionTypeInfo::kind_tag_variant(&info.kind), + call_span, + ) + .map(|value| value.kind), + IntrinsicId::TypePrimitiveTag => self.lower_reflection_primitive_tag( + info.primitive_tag.as_deref(), + ty, + call_span, + ), + IntrinsicId::TypeFields => self.lower_reflection_type_fields( + type_arguments[0], + &info.type_name, + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeBitfieldFields => self.lower_reflection_bitfield_fields( + type_arguments[0], + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeBitfieldLayout => self.lower_reflection_bitfield_layout( + type_arguments[0], + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeVariants => self.lower_reflection_type_variants( + type_arguments[0], + &info.type_name, + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeMachineLayout => self.lower_reflection_machine_layout( + type_arguments[0], + &info.type_name, + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeMachineStates => self.lower_reflection_machine_states( + type_arguments[0], + &info.type_name, + &info.kind, + ty, + call_span, + ), + IntrinsicId::TypeMachineTransitions => self + .lower_reflection_machine_transitions( + type_arguments[0], + &info.kind, + ty, + call_span, + ), + _ => unreachable!(), + }; + } + if intrinsic == IntrinsicId::TypeConstructStart + && type_arguments.len() == 1 + && reflection_arguments + .first() + .is_some_and(|info| matches!(info.kind.as_str(), "struct" | "bitfield")) + { + let Some(fields) = self + .parent + .check + .reflection_metadata + .get_type_fields_for_id(type_arguments[0]) + else { + self.parent.error( + call_span, + "type.construct_start has no checked field metadata", + ); + return None; + }; + reflection_arguments.extend(fields.iter().map(|field| field.type_info.clone())); + } + if intrinsic == IntrinsicId::TypeConstructVariantStart + && type_arguments.len() == 1 + && reflection_arguments + .first() + .is_some_and(|info| info.kind == "enum") + { + let Some(variants) = self + .parent + .check + .reflection_metadata + .get_type_variants_for_id(type_arguments[0]) + else { + self.parent.error( + call_span, + "type.construct_variant_start has no checked variant metadata", + ); + return None; + }; + reflection_arguments.extend(variants.iter().flat_map(|variant| { + variant.fields.iter().map(|field| field.type_info.clone()) + })); + } + if intrinsic == IntrinsicId::TypeConstructMachineStart + && type_arguments.len() == 1 + && reflection_arguments + .first() + .is_some_and(|info| matches!(info.kind.as_str(), "machine" | "machine_state")) + { + let machine = self.checked_reflection_machine(type_arguments[0], call_span)?; + reflection_arguments.extend( + machine + .states + .iter() + .flat_map(|state| state.fields.iter().map(|field| field.type_info.clone())), + ); + } + self.append_reflected_value_arguments( + intrinsic, + &type_arguments, + &reflection_arguments, + &mut lowered_args, + &mut evaluation_order, + self.expression_types.get(&call_span).copied(), + call_span, + )?; + self.append_reflected_read_arguments( + intrinsic, + &type_arguments, + &reflection_arguments, + &mut lowered_args, + &mut evaluation_order, + call_span, + )?; + let refinement_predicates = + self.reflected_finish_predicates(intrinsic, &type_arguments, call_span)?; + let field_validation = + self.reflected_read_validation(intrinsic, &type_arguments, call_span)?; + Some(ExpressionKind::Intrinsic { + ownership: self.finish_call_ownership( + ownership, + None, + Some(intrinsic), + &lowered_args, + call_span, + )?, + intrinsic, + type_arguments, + reflection_arguments, + refinement_predicates, + field_validation, + args: lowered_args, + evaluation_order, + }) + } } - fn is_source_call(&self, callee: &Expr, span: Span) -> bool { - if self.method_calls.contains_key(&span) { - return true; - } - let Expr::Ident(ident) = callee else { - return false; + fn reflected_enum_value( + &mut self, + ty: TypeId, + variant: &str, + span: Span, + ) -> Option { + let Type::Enum(enum_id) = self.parent.check.interner.resolve(ty) else { + self.parent + .error(span, "reflected metadata tag is not an enum"); + return None; + }; + let Some(index) = self + .parent + .check + .interner + .resolve_enum(*enum_id) + .variants + .iter() + .position(|candidate| candidate.name == variant && candidate.fields.is_empty()) + else { + self.parent + .error(span, "reflected metadata tag has no checked variant"); + return None; }; - self.resolved_kind(ident) == Some(DefKind::Function) + Some(Expression { + kind: ExpressionKind::EnumConstruct { + enum_type: ty, + variant: VariantId(index as u32), + payloads: Vec::new(), + evaluation_order: Vec::new(), + }, + ty, + span, + }) } - fn canonical_call_name(&mut self, callee: &Expr) -> Option { - fn dotted(expression: &Expr) -> Option { - match expression { - Expr::Ident(ident) => Some(ident.name.clone()), - Expr::FieldAccess(base, field, _) => { - Some(format!("{}.{}", dotted(base)?, field.name)) - } - _ => None, - } + fn lower_reflection_primitive_tag( + &mut self, + variant: Option<&str>, + optional_ty: TypeId, + span: Span, + ) -> Option { + let Type::Optional(primitive_ty) = self.parent.check.interner.resolve(optional_ty) else { + self.parent + .error(span, "reflected primitive tag is not optional"); + return None; + }; + let primitive_ty = *primitive_ty; + match variant { + Some(variant) => Some(ExpressionKind::OptionalSome(Box::new( + self.reflected_enum_value(primitive_ty, variant, span)?, + ))), + None => Some(ExpressionKind::OptionalNone), } - dotted(callee).or_else(|| { - self.parent.error( - callee.span(), - "checked intrinsic has no canonical call name", - ); - None - }) } - fn lower_bitfield_construct( + fn lower_reflection_type_info( &mut self, - args: &[ast::CallArg], + info: &ReflectionTypeInfo, + ty: TypeId, span: Span, ) -> Option { - let checked_type = self.expression_types.get(&span).copied()?; - let (bitfield_type, validates_widths) = - match self.parent.check.interner.resolve(checked_type) { - Type::Bitfield(_) => (checked_type, false), - Type::Result(ok, _) - if matches!(self.parent.check.interner.resolve(*ok), Type::Bitfield(_)) => - { - (*ok, true) - } - _ => { - self.parent - .error(span, "checked bitfield construction has an invalid type"); - return None; - } - }; - let Type::Bitfield(bitfield_id) = *self.parent.check.interner.resolve(bitfield_type) else { - unreachable!() + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { + self.parent.error(span, "type.info result is not TypeInfo"); + return None; }; - let definition = self.parent.check.interner.resolve_bitfield(bitfield_id); - let mut source_indices = vec![usize::MAX; definition.fields.len()]; - for (source_index, arg) in args.iter().enumerate() { - let field_index = if let Some(name) = &arg.name { - definition - .fields - .iter() - .position(|field| field.name == name.name)? - } else { - source_indices - .iter() - .position(|index| *index == usize::MAX)? - }; - source_indices[field_index] = source_index; + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = [ + "type_name", + "kind", + "kind_tag", + "primitive_tag", + "has_secret", + "args", + ]; + if definition.name != "TypeInfo" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent + .error(span, "type.info has no checked TypeInfo layout"); + return None; } - let evaluation_order = (0..args.len()) - .map(|source| { - source_indices - .iter() - .position(|candidate| *candidate == source) - .unwrap() + let field_types = definition + .fields + .iter() + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let Type::List(arg_type) = self.parent.check.interner.resolve(field_types[5]) else { + self.parent.error(span, "TypeInfo arguments are not a list"); + return None; + }; + let arg_type = *arg_type; + if arg_type != ty { + self.parent + .error(span, "TypeInfo argument type is inconsistent"); + return None; + } + let string_field = |value: &str| Expression { + kind: ExpressionKind::String(value.to_string()), + ty: field_types[0], + span, + }; + let kind_tag = self.reflected_enum_value( + field_types[2], + ReflectionTypeInfo::kind_tag_variant(&info.kind), + span, + )?; + let primitive_tag = self.lower_reflection_primitive_tag( + info.primitive_tag.as_deref(), + field_types[3], + span, + )?; + let args = info + .args + .iter() + .map(|arg| { + Some(Expression { + kind: self.lower_reflection_type_info(arg, arg_type, span)?, + ty: arg_type, + span, + }) }) - .collect(); - let fields = source_indices - .into_iter() - .map(|index| self.lower_expression(&args[index].value)) .collect::>>()?; - Some(ExpressionKind::BitfieldConstruct { - bitfield_type, + let fields = vec![ + string_field(&info.type_name), + Expression { + kind: ExpressionKind::String(info.kind.clone()), + ty: field_types[1], + span, + }, + kind_tag, + Expression { + kind: primitive_tag, + ty: field_types[3], + span, + }, + Expression { + kind: ExpressionKind::Bool(info.has_secret), + ty: field_types[4], + span, + }, + Expression { + kind: ExpressionKind::ListConstruct { elements: args }, + ty: field_types[5], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, fields, - evaluation_order, - validates_widths, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), }) } - fn lower_machine_construct( + fn lower_reflection_type_fields( &mut self, - args: &[ast::CallArg], + owner_ty: TypeId, + owner_name: &str, + owner_kind: &str, + list_ty: TypeId, span: Span, ) -> Option { - let state_type = self.expression_types.get(&span).copied()?; - let Type::MachineState { state, .. } = self.parent.check.interner.resolve(state_type) - else { - self.parent - .error(span, "machine construction lacks a checked state type"); + let Type::List(field_ty) = self.parent.check.interner.resolve(list_ty) else { + self.parent.error(span, "type.fields result is not a list"); return None; }; - let payloads = args[1..] + let field_ty = *field_ty; + if !matches!(owner_kind, "struct" | "bitfield") { + return Some(ExpressionKind::ListConstruct { + elements: Vec::new(), + }); + } + let fields = self + .parent + .check + .reflection_metadata + .get_type_fields_for_id(owner_ty) + .map(<[_]>::to_vec); + let fields = match fields { + Some(fields) => fields, + None if matches!( + self.parent.check.interner.resolve(owner_ty), + Type::Struct(_) | Type::Bitfield(_) + ) => + { + self.parent + .error(span, "type.fields has no checked field metadata"); + return None; + } + None => Vec::new(), + }; + let elements = fields .iter() - .map(|arg| self.lower_expression(&arg.value)) + .map(|field| { + Some(Expression { + kind: self + .lower_reflection_type_field(field, owner_name, None, field_ty, span)?, + ty: field_ty, + span, + }) + }) .collect::>>()?; - Some(ExpressionKind::MachineConstruct { - state_type, - state: StateId(state.index()), - payloads, - }) + Some(ExpressionKind::ListConstruct { elements }) } - fn lower_machine_transition( + fn lower_reflection_type_field( &mut self, - args: &[ast::CallArg], + field: &ReflectionFieldInfo, + owner_name: &str, + owner_member: Option<&str>, + ty: TypeId, span: Span, ) -> Option { - let state_type = self.expression_types.get(&span).copied()?; - let Type::MachineState { state, .. } = self.parent.check.interner.resolve(state_type) - else { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { self.parent - .error(span, "machine transition lacks a checked target state"); + .error(span, "type.fields element is not TypeField"); return None; }; - let source = Box::new(self.lower_expression(&args[0].value)?); - let payloads = args[2..] + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = [ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ]; + if definition.name != "TypeField" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent + .error(span, "type.fields has no checked TypeField layout"); + return None; + } + let field_types = definition + .fields .iter() - .map(|arg| self.lower_expression(&arg.value)) - .collect::>>()?; - Some(ExpressionKind::MachineTransition { - source, - state_type, - target: StateId(state.index()), - payloads, - }) - } - - fn lower_match(&mut self, match_stmt: &ast::MatchStmt) -> Option { - let scrutinee = self.lower_expression(&match_stmt.expr)?; - let Type::Enum(enum_id) = *self.parent.check.interner.resolve(scrutinee.ty) else { - self.parent.error( - match_stmt.expr.span(), - "checked match scrutinee is not an enum", - ); + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let Ok(index) = i128::try_from(field.index) else { + self.parent + .error(span, "reflected field index is too large"); return None; }; - let enum_definition = self.parent.check.interner.resolve_enum(enum_id).clone(); - let mut arms = Vec::with_capacity(match_stmt.arms.len()); - for arm in &match_stmt.arms { - let (variant, source_bindings, field_types) = match &arm.pattern { - ast::Pattern::Ident(name) => { - let Some(index) = enum_definition - .variants - .iter() - .position(|candidate| candidate.name == name.name) - else { - self.parent - .error(name.span, "checked match variant is missing"); - return None; - }; - (Some(VariantId(index as u32)), &[][..], &[][..]) - } - ast::Pattern::Variant(name, bindings) => { - let Some(index) = enum_definition - .variants - .iter() - .position(|candidate| candidate.name == name.name) - else { - self.parent - .error(name.span, "checked match variant is missing"); - return None; - }; - let fields = &enum_definition.variants[index].fields; - ( - Some(VariantId(index as u32)), - bindings.as_slice(), - fields.as_slice(), - ) - } - ast::Pattern::Other(_) => (None, &[][..], &[][..]), - }; - let mut bindings = Vec::with_capacity(source_bindings.len()); - for (binding, (_, field_type)) in source_bindings.iter().zip(field_types) { - let Some(definition) = self.parent.definition_at(binding.span, DefKind::Variable) + let string_field = |value: &str, ty| Expression { + kind: ExpressionKind::String(value.to_string()), + ty, + span, + }; + let kind_tag = self.reflected_enum_value( + field_types[6], + ReflectionTypeInfo::kind_tag_variant(&field.kind), + span, + )?; + let type_info = self.lower_reflection_type_info(&field.type_info, field_types[9], span)?; + let owner_member = match owner_member { + Some(member) => { + let Type::Optional(string_ty) = self.parent.check.interner.resolve(field_types[2]) else { self.parent - .error(binding.span, "match binding has no resolved definition"); + .error(span, "reflected owner member is not optional string"); return None; }; - bindings.push(self.allocate_local( - definition, - &binding.name, - *field_type, - false, - binding.span, - )); + ExpressionKind::OptionalSome(Box::new(string_field(member, *string_ty))) } - let body = self.lower_block(&arm.body); - arms.push(MatchArm { - variant, - bindings, - body, - span: arm.span, - }); - } - Some(StatementKind::Match { scrutinee, arms }) + None => ExpressionKind::OptionalNone, + }; + let fields = vec![ + Expression { + kind: ExpressionKind::Int(index), + ty: field_types[0], + span, + }, + string_field(owner_name, field_types[1]), + Expression { + kind: owner_member, + ty: field_types[2], + span, + }, + string_field(&field.name, field_types[3]), + string_field(&field.type_name, field_types[4]), + string_field(&field.kind, field_types[5]), + kind_tag, + string_field(&field.serialize_name, field_types[7]), + Expression { + kind: ExpressionKind::Bool(field.has_secret), + ty: field_types[8], + span, + }, + Expression { + kind: type_info, + ty: field_types[9], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) } - fn resolve_user_call_target(&mut self, callee: &Expr, call_span: Span) -> Option { - if let Some(method) = self.method_calls.get(&call_span) { - let key = FunctionKey::Method { - source_span: method.source_span, - }; - let Some(function) = self.function_ids.get(&key).copied() else { - self.parent.error( - call_span, - "method call target has no checked concrete HIR function", - ); - return None; - }; - return Some(function); + fn lower_reflection_type_variants( + &mut self, + owner_ty: TypeId, + owner_name: &str, + owner_kind: &str, + list_ty: TypeId, + span: Span, + ) -> Option { + let Type::List(variant_ty) = self.parent.check.interner.resolve(list_ty) else { + self.parent + .error(span, "type.variants result is not a list"); + return None; + }; + let variant_ty = *variant_ty; + if owner_kind != "enum" { + return Some(ExpressionKind::ListConstruct { + elements: Vec::new(), + }); } - - let Expr::Ident(ident) = callee else { - self.parent.error( - callee.span(), - "only direct user-function calls are in the current HIR subset", - ); + let Some(variants) = self + .parent + .check + .reflection_metadata + .get_type_variants_for_id(owner_ty) + .map(<[_]>::to_vec) + else { + self.parent + .error(span, "type.variants has no checked variant metadata"); return None; }; - let Some(definition) = self.parent.resolve.resolutions.get(&ident.span).copied() else { + let elements = variants + .iter() + .map(|variant| { + Some(Expression { + kind: self + .lower_reflection_type_variant(variant, owner_name, variant_ty, span)?, + ty: variant_ty, + span, + }) + }) + .collect::>>()?; + Some(ExpressionKind::ListConstruct { elements }) + } + + fn lower_reflection_type_variant( + &mut self, + variant: &ReflectionVariantInfo, + owner_name: &str, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { self.parent - .error(ident.span, "call target has no resolved definition"); + .error(span, "type.variants element is not TypeVariant"); return None; }; - let key = if let Some(generic) = self.generic_calls.get(&call_span) { - if generic.definition != definition { - self.parent.error( - call_span, - "checked generic call target disagrees with name resolution", - ); - return None; - } - FunctionKey::Definition { - definition, - concrete_args: generic.concrete_args.clone(), - } - } else { - FunctionKey::Definition { - definition, - concrete_args: Vec::new(), - } + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = [ + "index", + "owner_type", + "name", + "discriminant", + "has_secret", + "fields", + ]; + if definition.name != "TypeVariant" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent + .error(span, "type.variants has no checked TypeVariant layout"); + return None; + } + let field_types = definition + .fields + .iter() + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let Type::List(field_ty) = self.parent.check.interner.resolve(field_types[5]) else { + self.parent.error(span, "TypeVariant fields are not a list"); + return None; }; - let Some(function) = self.function_ids.get(&key).copied() else { - self.parent.error( - ident.span, - "call target has no checked concrete HIR function", - ); + let field_ty = *field_ty; + let Ok(index) = i128::try_from(variant.index) else { + self.parent + .error(span, "reflected variant index is too large"); return None; }; - Some(function) + let field_values = variant + .fields + .iter() + .map(|field| { + Some(Expression { + kind: self.lower_reflection_type_field( + field, + owner_name, + Some(&variant.name), + field_ty, + span, + )?, + ty: field_ty, + span, + }) + }) + .collect::>>()?; + let fields = vec![ + Expression { + kind: ExpressionKind::Int(index), + ty: field_types[0], + span, + }, + Expression { + kind: ExpressionKind::String(owner_name.to_string()), + ty: field_types[1], + span, + }, + Expression { + kind: ExpressionKind::String(variant.name.clone()), + ty: field_types[2], + span, + }, + Expression { + kind: ExpressionKind::Int(variant.discriminant.into()), + ty: field_types[3], + span, + }, + Expression { + kind: ExpressionKind::Bool(variant.has_secret), + ty: field_types[4], + span, + }, + Expression { + kind: ExpressionKind::ListConstruct { + elements: field_values, + }, + ty: field_types[5], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) } - fn lower_pipeline( + fn checked_reflection_machine( &mut self, - initial: &Expr, - steps: &[ast::PipelineStep], - pipeline_type: TypeId, - pipeline_span: Span, - ) -> Option { - let mut current = self.lower_expression(initial)?; - for (index, step) in steps.iter().enumerate() { - let output_type = if let Some(next_step) = steps.get(index + 1) { - let Some(ty) = self.expression_types.get(&next_step.span).copied() else { - self.parent - .error(next_step.span, "pipeline step has no checked input type"); - return None; - }; - ty - } else { - pipeline_type - }; - let Some(call_type) = self.pipeline_step_call_types.get(&step.span).copied() else { + owner_ty: TypeId, + span: Span, + ) -> Option { + let machine_ty = match self.parent.check.interner.resolve(owner_ty) { + Type::Machine(_) => Some(owner_ty), + Type::MachineState { machine, .. } => self + .parent + .check + .interner + .type_ids() + .find(|ty| matches!(self.parent.check.interner.resolve(*ty), Type::Machine(id) if id == machine)), + _ => None, + }; + machine_ty + .and_then(|machine_ty| { self.parent - .error(step.span, "pipeline step has no checked call-result type"); - return None; - }; - current = self.lower_pipeline_step(current, step, call_type)?; - if let Some(handle) = &step.handle { - current = self.lower_handle( - current, - handle.error_name.as_ref(), - &handle.body, - output_type, - handle.span, - )?; - } - } - current.ty = pipeline_type; - current.span = pipeline_span; - Some(current) + .check + .reflection_metadata + .get_machine_for_id(machine_ty) + .cloned() + }) + .or_else(|| { + self.parent + .error(span, "machine reflection has no checked metadata"); + None + }) } - fn lower_pipeline_step( + fn reflected_machine_info( &mut self, - piped: Expression, - step: &ast::PipelineStep, - output_type: TypeId, - ) -> Option { - let (callee, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); - let piped = if piped_as_view { - Expression { - ty: piped.ty, - span: step.function.span(), - kind: ExpressionKind::View(Box::new(piped)), - } + owner_ty: TypeId, + owner_kind: &str, + span: Span, + ) -> Option { + if matches!(owner_kind, "machine" | "machine_state") { + self.checked_reflection_machine(owner_ty, span) } else { - piped - }; - let (args, evaluation_order) = - self.lower_pipeline_arguments(piped, extra_args, step.span)?; - let kind = if self.is_source_call(callee, step.span) { - ExpressionKind::Call { - function: self.resolve_user_call_target(callee, step.span)?, - args, - evaluation_order, - } - } else { - ExpressionKind::Intrinsic { - canonical_name: self.canonical_call_name(callee)?, - args, - evaluation_order, - } + Some(ReflectionMachineInfo::new(Vec::new(), Vec::new())) + } + } + + fn lower_reflection_machine_layout( + &mut self, + owner_ty: TypeId, + owner_name: &str, + owner_kind: &str, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { + self.parent + .error(span, "type.machine_layout result is not TypeMachine"); + return None; }; - Some(Expression { - kind, - ty: output_type, - span: step.span, + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = ["states", "edges"]; + if definition.name != "TypeMachine" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent.error( + span, + "type.machine_layout has no checked TypeMachine layout", + ); + return None; + } + let states_ty = definition.fields[0].1; + let edges_ty = definition.fields[1].1; + let fields = vec![ + Expression { + kind: self.lower_reflection_machine_states( + owner_ty, owner_name, owner_kind, states_ty, span, + )?, + ty: states_ty, + span, + }, + Expression { + kind: self + .lower_reflection_machine_transitions(owner_ty, owner_kind, edges_ty, span)?, + ty: edges_ty, + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: vec![0, 1], + validates_refinements: false, + refinement_predicates: Vec::new(), }) } - fn pipeline_step_call_parts(step: &ast::PipelineStep) -> (&Expr, &[ast::CallArg], bool) { - let (function, piped_as_view) = match &step.function { - Expr::View(inner, _) => (inner.as_ref(), true), - _ => (&step.function, false), + fn lower_reflection_machine_states( + &mut self, + owner_ty: TypeId, + owner_name: &str, + owner_kind: &str, + list_ty: TypeId, + span: Span, + ) -> Option { + let Type::List(state_ty) = self.parent.check.interner.resolve(list_ty) else { + self.parent + .error(span, "type.machine_states result is not a list"); + return None; }; - match function { - Expr::Call(callee, args, _) => (callee, args, piped_as_view), - Expr::GenericCall(callee, _, args, _) => (callee, args, piped_as_view), - _ => (function, &step.extra_args, piped_as_view), - } + let state_ty = *state_ty; + let machine = self.reflected_machine_info(owner_ty, owner_kind, span)?; + let owner_name = owner_name + .split_once(" at ") + .map_or(owner_name, |(base, _)| base); + let elements = machine + .states + .iter() + .map(|state| { + Some(Expression { + kind: self.lower_reflection_machine_state(state, owner_name, state_ty, span)?, + ty: state_ty, + span, + }) + }) + .collect::>>()?; + Some(ExpressionKind::ListConstruct { elements }) } - fn lower_pipeline_arguments( + fn lower_reflection_machine_transitions( &mut self, - piped: Expression, - extra_args: &[ast::CallArg], - step_span: Span, - ) -> Option<(Vec, Vec)> { - let argument_count = extra_args.len() + 1; - let source_indices = if let Some(order) = self.call_argument_orders.get(&step_span) { - order.source_indices.clone() - } else if extra_args.iter().all(|arg| arg.name.is_none()) { - (0..argument_count).collect() - } else { + owner_ty: TypeId, + owner_kind: &str, + list_ty: TypeId, + span: Span, + ) -> Option { + let Type::List(edge_ty) = self.parent.check.interner.resolve(list_ty) else { + self.parent + .error(span, "type.machine_transitions result is not a list"); + return None; + }; + let edge_ty = *edge_ty; + let machine = self.reflected_machine_info(owner_ty, owner_kind, span)?; + let elements = machine + .edges + .iter() + .map(|edge| { + Some(Expression { + kind: self.lower_reflection_machine_transition(edge, edge_ty, span)?, + ty: edge_ty, + span, + }) + }) + .collect::>>()?; + Some(ExpressionKind::ListConstruct { elements }) + } + + fn lower_reflection_machine_transition( + &mut self, + edge: &ReflectionMachineTransitionInfo, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { self.parent.error( - step_span, - "named pipeline arguments have no checked parameter order", + span, + "type.machine_transitions element is not TypeMachineTransition", ); return None; }; - let mut seen = vec![false; argument_count]; - let invalid_permutation = source_indices.iter().any(|&index| { - if index >= argument_count || seen[index] { - true - } else { - seen[index] = true; - false - } - }); - if source_indices.len() != argument_count || invalid_permutation { - self.parent - .error(step_span, "checked pipeline argument order is invalid"); + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = ["index", "source_index", "source", "target_index", "target"]; + if definition.name != "TypeMachineTransition" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent.error( + span, + "type.machine_transitions has no checked TypeMachineTransition layout", + ); return None; } - let evaluation_order = (0..argument_count) - .map(|source_index| { - source_indices - .iter() - .position(|&candidate| candidate == source_index) - .expect("validated pipeline order must be a permutation") - }) - .collect(); - - let mut piped = Some(piped); - let args = source_indices - .into_iter() - .map(|source_index| { - if source_index == 0 { - piped.take() - } else { - self.lower_expression(&extra_args[source_index - 1].value) - } - }) - .collect::>>()?; - Some((args, evaluation_order)) + let mut fields = Vec::with_capacity(5); + for (position, value) in [ + (0, edge.index), + (1, edge.source_index), + (3, edge.target_index), + ] { + let Ok(value) = i128::try_from(value) else { + self.parent + .error(span, "reflected machine transition index is too large"); + return None; + }; + fields.push(( + position, + Expression { + kind: ExpressionKind::Int(value), + ty: definition.fields[position].1, + span, + }, + )); + } + for (position, value) in [(2, &edge.source), (4, &edge.target)] { + fields.push(( + position, + Expression { + kind: ExpressionKind::String(value.clone()), + ty: definition.fields[position].1, + span, + }, + )); + } + fields.sort_by_key(|(position, _)| *position); + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields: fields.into_iter().map(|(_, field)| field).collect(), + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) } - fn lower_arguments_in_parameter_order( + fn lower_reflection_machine_state( &mut self, - args: &[ast::CallArg], - call_span: Span, - ) -> Option<(Vec, Vec)> { - let source_indices = if let Some(order) = self.call_argument_orders.get(&call_span) { - order.source_indices.clone() - } else if args.iter().all(|arg| arg.name.is_none()) { - (0..args.len()).collect() - } else { + state: &ReflectionMachineStateInfo, + owner_name: &str, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { self.parent.error( - call_span, - "named call arguments have no checked parameter order", + span, + "type.machine_state_value result is not TypeMachineState", ); return None; }; - let mut seen = vec![false; args.len()]; - let invalid_permutation = source_indices.iter().any(|&index| { - if index >= args.len() || seen[index] { - true - } else { - seen[index] = true; - false - } - }); - if source_indices.len() != args.len() || invalid_permutation { + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = ["index", "owner_type", "name", "has_secret", "fields"]; + if definition.name != "TypeMachineState" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent.error( + span, + "type.machine_state_value has no checked TypeMachineState layout", + ); + return None; + } + let field_types = definition + .fields + .iter() + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let Type::List(field_ty) = self.parent.check.interner.resolve(field_types[4]) else { self.parent - .error(call_span, "checked call argument order is invalid"); + .error(span, "TypeMachineState fields are not a list"); return None; + }; + let field_ty = *field_ty; + let Ok(index) = i128::try_from(state.index) else { + self.parent + .error(span, "reflected machine state index is too large"); + return None; + }; + let field_values = state + .fields + .iter() + .map(|field| { + Some(Expression { + kind: self.lower_reflection_type_field( + field, + owner_name, + Some(&state.name), + field_ty, + span, + )?, + ty: field_ty, + span, + }) + }) + .collect::>>()?; + let fields = vec![ + Expression { + kind: ExpressionKind::Int(index), + ty: field_types[0], + span, + }, + Expression { + kind: ExpressionKind::String(owner_name.to_string()), + ty: field_types[1], + span, + }, + Expression { + kind: ExpressionKind::String(state.name.clone()), + ty: field_types[2], + span, + }, + Expression { + kind: ExpressionKind::Bool(state.has_secret), + ty: field_types[3], + span, + }, + Expression { + kind: ExpressionKind::ListConstruct { + elements: field_values, + }, + ty: field_types[4], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) + } + + fn checked_reflection_bitfield( + &mut self, + owner_ty: TypeId, + owner_kind: &str, + span: Span, + ) -> Option { + if owner_kind != "bitfield" { + return Some(ReflectionBitfieldInfo::new(false, Vec::new())); } - let evaluation_order = (0..args.len()) - .map(|source_index| { - source_indices - .iter() - .position(|&candidate| candidate == source_index) - .expect("validated call order must be a permutation") + self.parent + .check + .reflection_metadata + .get_bitfield_for_id(owner_ty) + .cloned() + .or_else(|| { + self.parent + .error(span, "bitfield reflection has no checked layout metadata"); + None + }) + } + + fn lower_reflection_bitfield_fields( + &mut self, + owner_ty: TypeId, + owner_kind: &str, + list_ty: TypeId, + span: Span, + ) -> Option { + let Type::List(field_ty) = self.parent.check.interner.resolve(list_ty) else { + self.parent + .error(span, "type.bitfield_fields result is not a list"); + return None; + }; + let field_ty = *field_ty; + let bitfield = self.checked_reflection_bitfield(owner_ty, owner_kind, span)?; + let elements = bitfield + .fields + .iter() + .map(|field| { + Some(Expression { + kind: self.lower_reflection_bitfield_field(field, field_ty, span)?, + ty: field_ty, + span, + }) }) - .collect(); - let lowered = source_indices - .into_iter() - .map(|index| self.lower_expression(&args[index].value)) .collect::>>()?; - Some((lowered, evaluation_order)) + Some(ExpressionKind::ListConstruct { elements }) } - fn lower_field(&mut self, base: &Expr, field: &ast::Ident) -> Option { - let base = self.lower_expression(base)?; - let owner_type = match self.parent.check.interner.resolve(base.ty) { - Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } => base.ty, - Type::Secret(inner) - if matches!( - self.parent.check.interner.resolve(*inner), - Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } - ) => - { - *inner - } - _ => { - self.parent.error( - field.span, - "checked field owner has no HIR aggregate layout", - ); - return None; - } + fn lower_reflection_bitfield_layout( + &mut self, + owner_ty: TypeId, + owner_kind: &str, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { + self.parent + .error(span, "type.bitfield_layout result is not TypeBitfield"); + return None; }; - let field_index = match *self.parent.check.interner.resolve(owner_type) { - Type::Struct(struct_id) => self - .parent - .check - .interner - .resolve_struct(struct_id) + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = ["network_order", "fields"]; + if definition.name != "TypeBitfield" + || !definition .fields .iter() - .position(|(name, _)| name == &field.name), - Type::Bitfield(bitfield_id) => self - .parent - .check - .interner - .resolve_bitfield(bitfield_id) + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent.error( + span, + "type.bitfield_layout has no checked TypeBitfield layout", + ); + return None; + } + let field_types = definition + .fields + .iter() + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let bitfield = self.checked_reflection_bitfield(owner_ty, owner_kind, span)?; + let fields = vec![ + Expression { + kind: ExpressionKind::Bool(bitfield.network_order), + ty: field_types[0], + span, + }, + Expression { + kind: self.lower_reflection_bitfield_fields( + owner_ty, + owner_kind, + field_types[1], + span, + )?, + ty: field_types[1], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) + } + + fn lower_reflection_bitfield_field( + &mut self, + field: &ReflectionBitfieldFieldInfo, + ty: TypeId, + span: Span, + ) -> Option { + let Type::Struct(struct_id) = self.parent.check.interner.resolve(ty) else { + self.parent + .error(span, "bitfield reflection element is not TypeBitfieldField"); + return None; + }; + let definition = self.parent.check.interner.resolve_struct(*struct_id); + let expected = [ + "index", + "name", + "shape", + "shape_tag", + "width", + "type_info", + "enum_type", + ]; + if definition.name != "TypeBitfieldField" + || !definition .fields .iter() - .position(|candidate| candidate.name == field.name), - Type::MachineState { machine, state } => self - .parent - .check - .interner - .resolve_machine(machine) - .state(state) - .and_then(|state| { - state - .fields - .iter() - .position(|(name, _)| name == &field.name) - }), - _ => unreachable!(), + .map(|(name, _)| name.as_str()) + .eq(expected) + { + self.parent.error( + span, + "bitfield reflection has no checked TypeBitfieldField layout", + ); + return None; + } + let field_types = definition + .fields + .iter() + .map(|(_, field_ty)| *field_ty) + .collect::>(); + let Ok(index) = i128::try_from(field.index) else { + self.parent + .error(span, "reflected bitfield field index is too large"); + return None; }; - let Some(field_index) = field_index else { + let shape_variant = if field.shape == "payload" { + "payload_field" + } else { + "bits_field" + }; + let shape_tag = self.reflected_enum_value(field_types[3], shape_variant, span)?; + let type_info = self.lower_reflection_type_info(&field.type_info, field_types[5], span)?; + let enum_type = match &field.enum_type { + Some(info) => { + let Type::Optional(info_ty) = self.parent.check.interner.resolve(field_types[6]) + else { + self.parent + .error(span, "bitfield enum metadata is not optional TypeInfo"); + return None; + }; + let info_ty = *info_ty; + let value = Expression { + kind: self.lower_reflection_type_info(info, info_ty, span)?, + ty: info_ty, + span, + }; + ExpressionKind::OptionalSome(Box::new(value)) + } + None => ExpressionKind::OptionalNone, + }; + let fields = vec![ + Expression { + kind: ExpressionKind::Int(index), + ty: field_types[0], + span, + }, + Expression { + kind: ExpressionKind::String(field.name.clone()), + ty: field_types[1], + span, + }, + Expression { + kind: ExpressionKind::String(field.shape.clone()), + ty: field_types[2], + span, + }, + shape_tag, + Expression { + kind: ExpressionKind::Int(field.width.into()), + ty: field_types[4], + span, + }, + Expression { + kind: type_info, + ty: field_types[5], + span, + }, + Expression { + kind: enum_type, + ty: field_types[6], + span, + }, + ]; + Some(ExpressionKind::StructConstruct { + struct_type: ty, + fields, + evaluation_order: (0..expected.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }) + } + + fn checked_intrinsic_type_arguments( + &mut self, + span: Span, + required: bool, + ) -> Option> { + if let Some(arguments) = self.intrinsic_type_arguments.get(&span) { + return Some(arguments.clone()); + } + if required { + self.parent.error( + span, + "generic compiler intrinsic has no checked concrete type operands", + ); + None + } else { + Some(Vec::new()) + } + } + + fn checked_intrinsic_id(&mut self, span: Span) -> Option { + self.intrinsic_ids.get(&span).copied().or_else(|| { + self.parent.error( + span, + "checked compiler intrinsic has no closed intrinsic identity", + ); + None + }) + } + + fn dotted_expression_name(&mut self, expression: &Expr) -> Option { + fn dotted(expression: &Expr) -> Option { + match expression { + Expr::Ident(ident) => Some(ident.name.clone()), + Expr::FieldAccess(base, field, _) => { + Some(format!("{}.{}", dotted(base)?, field.name)) + } + _ => None, + } + } + dotted(expression).or_else(|| { self.parent - .error(field.span, "checked aggregate field has no canonical index"); + .error(expression.span(), "expression has no canonical dotted name"); + None + }) + } + + fn lower_enum_construct( + &mut self, + enum_type: TypeId, + variant: &ast::Ident, + args: &[ast::CallArg], + span: Span, + ) -> Option { + let enum_type = + interface_values::representation_type(&self.parent.check.interner, enum_type); + let Some(index) = self.enum_variant_index(enum_type, variant) else { + self.parent + .error(span, "checked enum construction has no matching variant"); + return None; + }; + let (payloads, evaluation_order) = self.lower_arguments_in_parameter_order(args, span)?; + Some(ExpressionKind::EnumConstruct { + enum_type, + variant: VariantId(index as u32), + payloads, + evaluation_order, + }) + } + + fn enum_variant_index(&self, enum_type: TypeId, variant: &ast::Ident) -> Option { + let enum_type = + interface_values::representation_type(&self.parent.check.interner, enum_type); + let Type::Enum(enum_id) = *self.parent.check.interner.resolve(enum_type) else { + return None; + }; + self.parent + .check + .interner + .resolve_enum(enum_id) + .variants + .iter() + .position(|candidate| candidate.name == variant.name) + } + + fn enum_constructor_variant<'b>( + &self, + callee: &'b Expr, + output_type: TypeId, + ) -> Option<&'b ast::Ident> { + // Compiler-predefined enum names resolve as constants and have no + // checked value expression. Actual fields keep a checked base. + match callee { + Expr::EnumVariant(_, variant, _) => Some(variant), + Expr::FieldAccess(base, variant, _) + if (matches!( + self.resolved_expression_kind(base), + Some(DefKind::Enum | DefKind::Type) + ) || matches!( + self.resolved_expression_kind(callee), + Some(DefKind::Enum | DefKind::Type) + ) || (matches!( + self.resolved_expression_kind(base), + None | Some(DefKind::Constant) + ) && !self.expression_types.contains_key(&base.span()))) + && self.enum_variant_index(output_type, variant).is_some() => + { + Some(variant) + } + _ => None, + } + } + + fn is_checked_function_value(&self, expression: &Expr) -> bool { + self.expression_types + .get(&expression.span()) + .is_some_and(|ty| { + matches!( + self.parent.check.interner.resolve(*ty), + Type::Function { .. } + ) + }) + } + + fn unparenthesized(mut expression: &Expr) -> &Expr { + while let Expr::Paren(inner, _) = expression { + expression = inner; + } + expression + } + + fn resolved_definition(&self, expression: &Expr) -> Option { + self.parent + .resolve + .resolutions + .get(&expression.span()) + .copied() + } + + fn resolved_expression_kind(&self, expression: &Expr) -> Option { + self.resolved_definition(expression) + .map(|definition| self.parent.resolve.scope_table.def(definition).kind) + } + + fn is_declaration_reference(&self, expression: &Expr, kind: DefKind) -> bool { + let Some(definition) = self.resolved_definition(expression) else { + return false; + }; + let info = self.parent.resolve.scope_table.def(definition); + if info.kind != kind { + return false; + } + match expression { + Expr::Ident(_) => true, + Expr::FieldAccess(_, member, _) => info + .name + .rsplit('.') + .next() + .is_some_and(|name| name == member.name), + _ => false, + } + } + + fn is_source_call(&self, callee: &Expr, span: Span) -> bool { + if self.method_calls.contains_key(&span) { + return true; + } + let Some(definition) = self.resolved_definition(callee) else { + return false; + }; + if self.parent.resolve.scope_table.def(definition).kind != DefKind::Function { + return false; + } + if self + .generic_calls + .get(&span) + .is_some_and(|generic| generic.definition != definition) + { + let redirected_json = self.is_trusted_stdlib_intrinsic(definition, span) + && matches!( + self.intrinsic_ids.get(&span).copied(), + Some( + IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + | IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + ) + ); + return !redirected_json; + } + let key = self.generic_calls.get(&span).map_or_else( + || FunctionKey::Definition { + definition, + concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }, + |generic| FunctionKey::Definition { + definition, + concrete_args: generic.concrete_args.clone(), + specialization: generic.specialization.clone(), + }, + ); + self.function_ids.contains_key(&key) || !self.is_trusted_stdlib_intrinsic(definition, span) + } + + fn is_trusted_stdlib_intrinsic(&self, definition: DefId, span: Span) -> bool { + let info = self.parent.resolve.scope_table.def(definition); + self.parent.origins.get(&info.span.file) == Some(&SourceOrigin::Stdlib) + && self.intrinsic_ids.contains_key(&span) + } + + fn is_raw_json_tree(&self, ty: TypeId) -> bool { + matches!(self.parent.check.interner.resolve(ty), Type::Enum(id) if self.parent.check.interner.resolve_enum(*id).name == "json.JsonTree") + } + + fn is_secret_raw_json_tree(&self, ty: TypeId) -> bool { + matches!(self.parent.check.interner.resolve(ty), Type::Secret(inner) if self.is_raw_json_tree(*inner)) + } + + fn is_non_raw_json_enum(&self, ty: TypeId) -> bool { + matches!(self.parent.check.interner.resolve(ty), Type::Enum(_)) + && !self.is_raw_json_tree(ty) + } + + fn native_json_primitive_parser(&self, ty: TypeId) -> Option<&'static str> { + match self.parent.check.interner.resolve(ty) { + Type::String => Some("json_parse_native_string"), + Type::Bool => Some("json_parse_native_bool"), + Type::Int64 => Some("json_parse_native_int64"), + Type::Uint8 => Some("json_parse_native_uint8"), + Type::Uint64 => Some("json_parse_native_uint64"), + Type::Float64 => Some("json_parse_native_float64"), + Type::Bytes => Some("json_parse_native_bytes"), + Type::Nothing => Some("json_parse_native_nothing"), + _ => None, + } + } + + fn lower_primitive_json_serialization( + &mut self, + ty: TypeId, + value: &Expression, + ownership: &CallOwnership, + span: Span, + ) -> Option { + if matches!(self.parent.check.interner.resolve(ty), Type::Nothing) { + let function = self.trusted_stdlib_function("json", "json_serialize_native_nothing")?; + let args = vec![value.clone()]; + return Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership.clone(), + Some(function), + None, + &args, + span, + )?, + function, + args, + evaluation_order: vec![0], + }); + } + if matches!(self.parent.check.interner.resolve(ty), Type::Bool) { + return Some(ExpressionKind::StringInterpolation(vec![ + StringSegment::Value(value.clone()), + ])); + } + let (variant_name, payload) = match self.parent.check.interner.resolve(ty) { + Type::String => ( + "string_value", + Some(Expression { + kind: ExpressionKind::Clone(Box::new(value.clone())), + ty, + span, + }), + ), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 => ( + "number_value", + Some(Expression { + kind: ExpressionKind::StringInterpolation(vec![StringSegment::Value( + value.clone(), + )]), + ty: TypeInterner::STRING, + span, + }), + ), + _ => return None, + }; + let tree_type = self + .parent + .check + .interner + .type_ids() + .find(|candidate| self.is_raw_json_tree(*candidate))?; + let Type::Enum(enum_id) = self.parent.check.interner.resolve(tree_type) else { return None; }; - Some(ExpressionKind::Field { - base: Box::new(base), - owner_type, - field: FieldId(field_index as u32), - }) + let variant = self + .parent + .check + .interner + .resolve_enum(*enum_id) + .variants + .iter() + .position(|candidate| candidate.name == variant_name)?; + let function = self.trusted_stdlib_function("json", "serialize_raw")?; + let payloads = payload.into_iter().collect::>(); + let tree = Expression { + kind: ExpressionKind::EnumConstruct { + enum_type: tree_type, + variant: VariantId(variant as u32), + evaluation_order: (0..payloads.len()).collect(), + payloads, + }, + ty: tree_type, + span, + }; + let args = vec![Expression { + kind: ExpressionKind::View(Box::new(tree)), + ty: tree_type, + span, + }]; + Some(ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership.clone(), + Some(function), + None, + &args, + span, + )?, + function, + args, + evaluation_order: vec![0], + }) + } + + fn trusted_stdlib_function(&self, namespace: &str, name: &str) -> Option { + self.parent.module.items.iter().find_map(|item| { + let Item::Function(function) = item else { + return None; + }; + if function.name.name != name || !function.type_params.is_empty() { + return None; + } + let definition = self + .parent + .definition_at(function.name.span, DefKind::Function)?; + let declared = self.parent.resolve.scope_table.def(definition); + if declared.namespace.as_deref() != Some(namespace) + || self.parent.origins.get(&function.name.span.file) != Some(&SourceOrigin::Stdlib) + { + return None; + } + self.function_ids + .get(&FunctionKey::Definition { + definition, + concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }) + .copied() + }) + } + + fn trusted_stdlib_generic_function( + &self, + namespace: &str, + name: &str, + span: Span, + ) -> Option { + let checked = self.generic_calls.get(&span)?; + self.parent.module.items.iter().find_map(|item| { + let Item::Function(function) = item else { + return None; + }; + if function.name.name != name || function.type_params.is_empty() { + return None; + } + let definition = self + .parent + .definition_at(function.name.span, DefKind::Function)?; + if definition != checked.definition { + return None; + } + let declared = self.parent.resolve.scope_table.def(definition); + if declared.namespace.as_deref() != Some(namespace) + || self.parent.origins.get(&function.name.span.file) != Some(&SourceOrigin::Stdlib) + { + return None; + } + self.function_ids + .get(&FunctionKey::Definition { + definition, + concrete_args: checked.concrete_args.clone(), + specialization: checked.specialization.clone(), + }) + .copied() + }) + } + + fn lower_bitfield_construct( + &mut self, + callee: &Expr, + args: &[ast::CallArg], + span: Span, + ) -> Option { + let checked_type = self.expression_types.get(&span).copied()?; + let output_type = + constructor_type_without_secret(&self.parent.check.interner, checked_type); + let (bitfield_type, validates_widths) = + match self.parent.check.interner.resolve(output_type) { + Type::Bitfield(_) => (output_type, false), + Type::Result(ok, error) + if *error == TypeInterner::STRING + && matches!(self.parent.check.interner.resolve(*ok), Type::Bitfield(_)) => + { + (*ok, true) + } + _ => { + self.parent + .error(span, "checked bitfield construction has an invalid type"); + return None; + } + }; + let declared_type = self + .resolved_definition(callee) + .and_then(|definition| self.parent.check.definition_types.get(&definition).copied()); + if declared_type != Some(bitfield_type) { + self.parent.error( + span, + "checked bitfield construction target differs from its declaration", + ); + return None; + } + let Type::Bitfield(bitfield_id) = *self.parent.check.interner.resolve(bitfield_type) else { + unreachable!() + }; + let definition = self.parent.check.interner.resolve_bitfield(bitfield_id); + let mut source_indices = vec![usize::MAX; definition.fields.len()]; + for (source_index, arg) in args.iter().enumerate() { + let field_index = if let Some(name) = &arg.name { + definition + .fields + .iter() + .position(|field| field.name == name.name)? + } else { + source_indices + .iter() + .position(|index| *index == usize::MAX)? + }; + source_indices[field_index] = source_index; + } + let evaluation_order = (0..args.len()) + .map(|source| { + source_indices + .iter() + .position(|candidate| *candidate == source) + .unwrap() + }) + .collect(); + let fields = source_indices + .into_iter() + .map(|index| self.lower_expression(&args[index].value)) + .collect::>>()?; + Some(ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + }) + } + + fn lower_machine_construct( + &mut self, + callee: &Expr, + args: &[ast::CallArg], + span: Span, + ) -> Option { + let checked_type = self.expression_types.get(&span).copied()?; + let state_type = constructor_type_without_secret(&self.parent.check.interner, checked_type); + let Type::MachineState { machine, state } = *self.parent.check.interner.resolve(state_type) + else { + self.parent + .error(span, "machine construction lacks a checked state type"); + return None; + }; + let declared_type = self + .resolved_definition(callee) + .and_then(|definition| self.parent.check.definition_types.get(&definition).copied()); + if !matches!(declared_type.map(|ty| self.parent.check.interner.resolve(ty)), Some(Type::Machine(selected)) if *selected == machine) + { + self.parent.error( + span, + "checked machine construction target differs from its declaration", + ); + return None; + } + let source_state = args.first().and_then(|arg| match &arg.value { + Expr::Ident(name) if arg.name.is_none() => self + .parent + .check + .interner + .resolve_machine(machine) + .state_id(&name.name), + _ => None, + }); + if source_state != Some(state) { + self.parent.error( + span, + "checked machine construction state differs from its source target", + ); + return None; + } + let payloads = args[1..] + .iter() + .map(|arg| self.lower_expression(&arg.value)) + .collect::>>()?; + Some(ExpressionKind::MachineConstruct { + state_type, + state: StateId(state.index()), + payloads, + }) + } + + fn lower_machine_transition( + &mut self, + args: &[ast::CallArg], + span: Span, + ) -> Option { + let state_type = self.expression_types.get(&span).copied()?; + let Type::MachineState { state, .. } = self.parent.check.interner.resolve(state_type) + else { + self.parent + .error(span, "machine transition lacks a checked target state"); + return None; + }; + let source = Box::new(self.lower_expression(&args[0].value)?); + let payloads = args[2..] + .iter() + .map(|arg| self.lower_expression(&arg.value)) + .collect::>>()?; + Some(ExpressionKind::MachineTransition { + source, + state_type, + target: StateId(state.index()), + payloads, + }) + } + + fn lower_match(&mut self, match_stmt: &ast::MatchStmt) -> Option { + let scrutinee = self.lower_expression(&match_stmt.expr)?; + let Type::Enum(enum_id) = *self.parent.check.interner.resolve(scrutinee.ty) else { + self.parent.error( + match_stmt.expr.span(), + "checked match scrutinee is not an enum", + ); + return None; + }; + let enum_definition = self.parent.check.interner.resolve_enum(enum_id).clone(); + let mut arms = Vec::with_capacity(match_stmt.arms.len()); + for arm in &match_stmt.arms { + self.visible_bindings.push(HashMap::new()); + let saved_match_bindings = self.match_bindings.clone(); + let (variant, source_bindings, field_types) = match &arm.pattern { + ast::Pattern::Ident(name) => { + let Some(index) = enum_definition + .variants + .iter() + .position(|candidate| candidate.name == name.name) + else { + self.parent + .error(name.span, "checked match variant is missing"); + return None; + }; + (Some(VariantId(index as u32)), &[][..], &[][..]) + } + ast::Pattern::Variant(name, bindings) => { + let Some(index) = enum_definition + .variants + .iter() + .position(|candidate| candidate.name == name.name) + else { + self.parent + .error(name.span, "checked match variant is missing"); + return None; + }; + let fields = &enum_definition.variants[index].fields; + ( + Some(VariantId(index as u32)), + bindings.as_slice(), + fields.as_slice(), + ) + } + ast::Pattern::Other(_) => (None, &[][..], &[][..]), + }; + let mut bindings = Vec::with_capacity(source_bindings.len()); + for (binding, (_, field_type)) in source_bindings.iter().zip(field_types) { + let Some(definition) = self.parent.definition_at(binding.span, DefKind::Variable) + else { + self.parent + .error(binding.span, "match binding has no resolved definition"); + return None; + }; + bindings.push(self.allocate_local( + definition, + &binding.name, + *field_type, + false, + binding.span, + )); + } + let proofs = match match_bindings::checked_match_arm_bindings( + &self.parent.check.interner, + match_stmt.span, + scrutinee.ty, + variant, + arm.span, + &bindings, + |id| self.ownership_local_info(id), + ) { + Ok(proofs) => proofs, + Err(message) => { + self.parent.error(arm.span, message); + return None; + } + }; + self.match_bindings.extend(proofs); + let body = self.lower_block(&arm.body); + self.match_bindings = saved_match_bindings; + self.visible_bindings.pop(); + arms.push(MatchArm { + variant, + bindings, + body, + span: arm.span, + }); + } + Some(StatementKind::Match { scrutinee, arms }) + } + + fn resolve_function_value_target(&mut self, expression: &Expr) -> Option { + if let Some(method) = self.method_values.get(&expression.span()) { + let key = match method { + CheckedMethodValue::Source { source_span } => FunctionKey::Method { + source_span: *source_span, + }, + CheckedMethodValue::Interface { + interface_type, + method_index, + } => FunctionKey::Interface { + owner: *interface_type, + method: *method_index, + }, + }; + return self.function_ids.get(&key).copied().or_else(|| { + self.parent.error( + expression.span(), + "method value target has no checked HIR function", + ); + None + }); + } + let Some(definition) = self.resolved_definition(expression) else { + self.parent.error( + expression.span(), + "function value has no resolved definition", + ); + return None; + }; + let key = FunctionKey::Definition { + definition, + concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }; + self.function_ids.get(&key).copied().or_else(|| { + self.parent.error( + expression.span(), + "function value has no checked concrete HIR function", + ); + None + }) + } + + fn resolve_user_call_target(&mut self, callee: &Expr, call_span: Span) -> Option { + if let Some(method) = self.method_calls.get(&call_span) { + let key = FunctionKey::Method { + source_span: method.source_span, + }; + let Some(function) = self.function_ids.get(&key).copied() else { + self.parent.error( + call_span, + "method call target has no checked concrete HIR function", + ); + return None; + }; + return Some(function); + } + + let Some(definition) = self.resolved_definition(callee) else { + self.parent + .error(callee.span(), "call target has no resolved definition"); + return None; + }; + let key = if let Some(generic) = self.generic_calls.get(&call_span) { + if generic.definition != definition { + self.parent.error( + call_span, + "checked generic call target disagrees with name resolution", + ); + return None; + } + FunctionKey::Definition { + definition, + concrete_args: generic.concrete_args.clone(), + specialization: generic.specialization.clone(), + } + } else { + FunctionKey::Definition { + definition, + concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + } + }; + let Some(function) = self.function_ids.get(&key).copied() else { + self.parent.error( + callee.span(), + "call target has no checked concrete HIR function", + ); + return None; + }; + Some(function) + } + + fn lower_pipeline( + &mut self, + initial: &Expr, + steps: &[ast::PipelineStep], + pipeline_type: TypeId, + pipeline_span: Span, + ) -> Option { + let mut current = self.lower_expression(initial)?; + for (index, step) in steps.iter().enumerate() { + let output_type = if let Some(next_step) = steps.get(index + 1) { + let Some(ty) = self.pipeline_step_input_types.get(&next_step.span).copied() else { + self.parent + .error(next_step.span, "pipeline step has no checked input type"); + return None; + }; + ty + } else { + pipeline_type + }; + let Some(call_type) = self.pipeline_step_call_types.get(&step.span).copied() else { + self.parent + .error(step.span, "pipeline step has no checked call-result type"); + return None; + }; + current = self.lower_pipeline_step(current, step, call_type)?; + if let Some(handle) = &step.handle { + current = self.lower_handle( + current, + handle.error_name.as_ref(), + &handle.body, + output_type, + handle.span, + )?; + } + } + current.ty = pipeline_type; + current.span = pipeline_span; + Some(current) + } + + fn lower_pipeline_step( + &mut self, + piped: Expression, + step: &ast::PipelineStep, + output_type: TypeId, + ) -> Option { + let (callee, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + if self.enum_constructor_variant(callee, output_type).is_none() { + self.check_pipeline_actuals(step.span, &piped, piped_as_view, extra_args)?; + } + let piped = if piped_as_view { + Expression { + ty: piped.ty, + span: step.function.span(), + kind: ExpressionKind::View(Box::new(piped)), + } + } else { + piped + }; + let (mut args, mut evaluation_order) = + self.lower_pipeline_arguments(piped, extra_args, step.span)?; + let source_call = self.is_source_call(callee, step.span); + let kind = if let Some(variant) = self.enum_constructor_variant(callee, output_type) { + ExpressionKind::EnumConstruct { + enum_type: output_type, + variant: VariantId(self.enum_variant_index(output_type, variant)? as u32), + payloads: args, + evaluation_order, + } + } else if self.call_ownership.get(&step.span).is_some_and(|packet| { + matches!(&packet.target, jett_typecheck::CheckedInvocationTarget::Resolved(definition) if self.parent.resource_manifest.hook_for_definition(*definition).is_some()) + }) { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; + let CallOwnership::Source(source) = &ownership else { return None; }; + let CallTarget::ResourceHook(hook) = &source.target else { return None; }; + let hook = hook.clone(); + ExpressionKind::ResourceInvoke { ownership: self.finish_resource_call_ownership(ownership, &hook, &args, step.span)?, hook, args, evaluation_order } + } else if !source_call + && !self.intrinsic_ids.contains_key(&step.span) + && self.is_checked_function_value(callee) + { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; + ExpressionKind::IndirectCall { + ownership: self.finish_call_ownership(ownership, None, None, &args, step.span)?, + callee: Box::new(self.lower_expression(callee)?), + args, + evaluation_order, + } + } else if source_call { + let function = self.resolve_user_call_target(callee, step.span)?; + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; + ExpressionKind::Call { + ownership: self.finish_call_ownership( + ownership, + Some(function), + None, + &args, + step.span, + )?, + function, + args, + evaluation_order, + } + } else { + let ownership = self.source_ownership(step.span, &args, &evaluation_order)?; + let has_explicit_type_arguments = Self::has_explicit_type_arguments(&step.function); + let intrinsic = self.checked_intrinsic_id(step.span)?; + let type_arguments = + self.checked_intrinsic_type_arguments(step.span, has_explicit_type_arguments)?; + let reflection_arguments = self + .intrinsic_reflection_arguments + .get(&step.span) + .cloned() + .unwrap_or_default(); + self.append_reflected_value_arguments( + intrinsic, + &type_arguments, + &reflection_arguments, + &mut args, + &mut evaluation_order, + Some(output_type), + step.span, + )?; + self.append_reflected_read_arguments( + intrinsic, + &type_arguments, + &reflection_arguments, + &mut args, + &mut evaluation_order, + step.span, + )?; + let refinement_predicates = + self.reflected_finish_predicates(intrinsic, &type_arguments, step.span)?; + let field_validation = + self.reflected_read_validation(intrinsic, &type_arguments, step.span)?; + ExpressionKind::Intrinsic { + ownership: self.finish_call_ownership( + ownership, + None, + Some(intrinsic), + &args, + step.span, + )?, + intrinsic, + type_arguments, + reflection_arguments, + refinement_predicates, + field_validation, + args, + evaluation_order, + } + }; + Some(Expression { + kind, + ty: output_type, + span: step.span, + }) + } + + fn pipeline_step_call_parts(step: &ast::PipelineStep) -> (&Expr, &[ast::CallArg], bool) { + let (function, piped_as_view) = match &step.function { + Expr::View(inner, _) => (inner.as_ref(), true), + _ => (&step.function, false), + }; + match function { + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + (Self::unparenthesized(callee), args, piped_as_view) + } + _ => ( + Self::unparenthesized(function), + &step.extra_args, + piped_as_view, + ), + } + } + + fn has_explicit_type_arguments(expression: &Expr) -> bool { + match expression { + Expr::GenericCall(_, arguments, _, _) => !arguments.is_empty(), + Expr::View(inner, _) | Expr::Paren(inner, _) => { + Self::has_explicit_type_arguments(inner) + } + _ => false, + } + } + + fn lower_pipeline_arguments( + &mut self, + piped: Expression, + extra_args: &[ast::CallArg], + step_span: Span, + ) -> Option<(Vec, Vec)> { + let argument_count = extra_args.len() + 1; + let source_indices = if let Some(order) = self.call_argument_orders.get(&step_span) { + order.source_indices.clone() + } else if extra_args.iter().all(|arg| arg.name.is_none()) { + (0..argument_count).collect() + } else { + self.parent.error( + step_span, + "named pipeline arguments have no checked parameter order", + ); + return None; + }; + let mut seen = vec![false; argument_count]; + let invalid_permutation = source_indices.iter().any(|&index| { + if index >= argument_count || seen[index] { + true + } else { + seen[index] = true; + false + } + }); + if source_indices.len() != argument_count || invalid_permutation { + self.parent + .error(step_span, "checked pipeline argument order is invalid"); + return None; + } + let evaluation_order = (0..argument_count) + .map(|source_index| { + source_indices + .iter() + .position(|&candidate| candidate == source_index) + .expect("validated pipeline order must be a permutation") + }) + .collect(); + + let mut piped = Some(piped); + let args = source_indices + .into_iter() + .map(|source_index| { + if source_index == 0 { + piped.take() + } else { + self.lower_expression(&extra_args[source_index - 1].value) + } + }) + .collect::>>()?; + Some((args, evaluation_order)) + } + + fn lower_arguments_in_parameter_order( + &mut self, + args: &[ast::CallArg], + call_span: Span, + ) -> Option<(Vec, Vec)> { + let source_indices = if let Some(order) = self.call_argument_orders.get(&call_span) { + order.source_indices.clone() + } else if args.iter().all(|arg| arg.name.is_none()) { + (0..args.len()).collect() + } else { + self.parent.error( + call_span, + "named call arguments have no checked parameter order", + ); + return None; + }; + let mut seen = vec![false; args.len()]; + let invalid_permutation = source_indices.iter().any(|&index| { + if index >= args.len() || seen[index] { + true + } else { + seen[index] = true; + false + } + }); + if source_indices.len() != args.len() || invalid_permutation { + self.parent + .error(call_span, "checked call argument order is invalid"); + return None; + } + let evaluation_order = (0..args.len()) + .map(|source_index| { + source_indices + .iter() + .position(|&candidate| candidate == source_index) + .expect("validated call order must be a permutation") + }) + .collect(); + let lowered = source_indices + .into_iter() + .map(|index| self.lower_expression(&args[index].value)) + .collect::>>()?; + Some((lowered, evaluation_order)) + } + + fn lower_field(&mut self, base: &Expr, field: &ast::Ident) -> Option { + let base = self.lower_expression(base)?; + let owner_type = match self.parent.check.interner.resolve(base.ty) { + Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } => base.ty, + Type::Secret(inner) + if matches!( + self.parent.check.interner.resolve(*inner), + Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } + ) => + { + *inner + } + _ => { + self.parent.error( + field.span, + "checked field owner has no HIR aggregate layout", + ); + return None; + } + }; + let field_index = match *self.parent.check.interner.resolve(owner_type) { + Type::Struct(struct_id) => self + .parent + .check + .interner + .resolve_struct(struct_id) + .fields + .iter() + .position(|(name, _)| name == &field.name), + Type::Bitfield(bitfield_id) => self + .parent + .check + .interner + .resolve_bitfield(bitfield_id) + .fields + .iter() + .position(|candidate| candidate.name == field.name), + Type::MachineState { machine, state } => self + .parent + .check + .interner + .resolve_machine(machine) + .state(state) + .and_then(|state| { + state + .fields + .iter() + .position(|(name, _)| name == &field.name) + }), + _ => unreachable!(), + }; + let Some(field_index) = field_index else { + self.parent + .error(field.span, "checked aggregate field has no canonical index"); + return None; + }; + Some(ExpressionKind::Field { + base: Box::new(base), + owner_type, + field: FieldId(field_index as u32), + }) + } + + fn expression_type(&mut self, expression: &Expr) -> Option { + if let Some(ty) = self.expression_types.get(&expression.span()).copied() { + return Some(ty); + } + if let Expr::Ident(ident) = expression + && let Some(definition) = self.parent.resolve.resolutions.get(&ident.span) + && let Some(ty) = self.parent.check.definition_types.get(definition) + { + return Some(*ty); + } + self.parent + .error(expression.span(), "expression has no checked type"); + None + } +} + +fn constructor_type_without_secret(types: &TypeInterner, mut ty: TypeId) -> TypeId { + while let Type::Secret(inner) = types.resolve(ty) { + ty = *inner; + } + ty +} + +fn normalize_secret_constructor(mut expression: Expression, types: &TypeInterner) -> Expression { + let checked_type = expression.ty; + let constructor_type = constructor_type_without_secret(types, checked_type); + let exact_constructor = match (&expression.kind, types.resolve(constructor_type)) { + (ExpressionKind::ListConstruct { .. }, Type::List(_)) + | (ExpressionKind::MapConstruct { .. }, Type::Map(..)) + | (ExpressionKind::OptionalSome(_) | ExpressionKind::OptionalNone, Type::Optional(_)) + | (ExpressionKind::ResultOk(_) | ExpressionKind::ResultFail(_), Type::Result(..)) => true, + ( + ExpressionKind::StructConstruct { + struct_type, + validates_refinements: false, + .. + }, + Type::Struct(_), + ) => *struct_type == constructor_type, + ( + ExpressionKind::StructConstruct { + struct_type, + validates_refinements: true, + .. + }, + Type::Result(ok, error), + ) => { + *ok == *struct_type + && *error == TypeInterner::STRING + && matches!(types.resolve(*struct_type), Type::Struct(_)) + } + ( + ExpressionKind::BitfieldConstruct { + bitfield_type, + validates_widths: false, + .. + }, + Type::Bitfield(_), + ) => *bitfield_type == constructor_type, + ( + ExpressionKind::BitfieldConstruct { + bitfield_type, + validates_widths: true, + .. + }, + Type::Result(ok, error), + ) => { + *ok == *bitfield_type + && *error == TypeInterner::STRING + && matches!(types.resolve(*bitfield_type), Type::Bitfield(_)) + } + ( + ExpressionKind::MachineConstruct { + state_type, state, .. + }, + Type::MachineState { + state: expected, .. + }, + ) => *state_type == constructor_type && state.index() == expected.index(), + _ => false, + }; + if constructor_type == checked_type || !exact_constructor { + return expression; + } + + // Contextual checking keeps the secret qualification on the constructor. + // Preserve its exact nominal or container type so payload conversions run + // before qualification. Never peel a nominal refinement or change a + // producer's checked type to make its representation fit a constructor. + let span = expression.span; + expression.ty = constructor_type; + Expression { + kind: ExpressionKind::interface_coerce(Box::new(expression)), + ty: checked_type, + span, + } +} + +fn lower_binary_op(op: ast::BinOp) -> BinaryOp { + match op { + ast::BinOp::Add => BinaryOp::Add, + ast::BinOp::Sub => BinaryOp::Subtract, + ast::BinOp::Mul => BinaryOp::Multiply, + ast::BinOp::Div => BinaryOp::Divide, + ast::BinOp::Modulo => BinaryOp::Modulo, + ast::BinOp::Eq => BinaryOp::Equal, + ast::BinOp::NotEq => BinaryOp::NotEqual, + ast::BinOp::Lt => BinaryOp::Less, + ast::BinOp::Gt => BinaryOp::Greater, + ast::BinOp::LtEq => BinaryOp::LessEqual, + ast::BinOp::GtEq => BinaryOp::GreaterEqual, + ast::BinOp::And => BinaryOp::And, + ast::BinOp::Or => BinaryOp::Or, + } +} + +fn lower_unary_op(op: ast::UnaryOp) -> UnaryOp { + match op { + ast::UnaryOp::Not => UnaryOp::Not, + ast::UnaryOp::Neg => UnaryOp::Negate, + } +} + +fn facts_in_span(facts: &HashMap, owner: Span) -> HashMap { + facts + .iter() + .filter(|(span, _)| { + span.file == owner.file && span.start >= owner.start && span.end <= owner.end + }) + .map(|(span, value)| (*span, value.clone())) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::FileId; + use jett_diagnostics::Severity; + use jett_types::{CapabilityKind, TypeInterner}; + + fn lower_source(source: &str) -> Program { + lower_source_mode(source, false) + } + + fn lower_source_mode(source: &str, include_test_bodies: bool) -> Program { + lower_source_with_check(source, include_test_bodies).0 + } + + fn lower_source_with_check( + source: &str, + include_test_bodies: bool, + ) -> (Program, jett_typecheck::CheckResult) { + lower_source_with_prelude(source, include_test_bodies, false) + } + + fn lower_source_with_prelude( + source: &str, + include_test_bodies: bool, + include_equatable: bool, + ) -> (Program, jett_typecheck::CheckResult) { + let file = FileId::new(0); + let mut parsed = jett_parser::parse(source, file); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let mut origins = HashMap::from([(file, SourceOrigin::Project)]); + if include_equatable { + let prelude_file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut prelude = jett_parser::parse( + "namespace stdlib\nexport interface Equatable:\n function equals(view self: Equatable, view other: Equatable) returns bool\n", + prelude_file, + ); + assert!(prelude.errors.is_empty()); + prelude.module.items.append(&mut parsed.module.items); + parsed.module.items = prelude.module.items; + origins.insert(prelude_file, SourceOrigin::Stdlib); + } + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != Severity::Error), + "resolve errors: {:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != Severity::Error), + "type errors: {:?}", + checked.diagnostics + ); + let program = if include_test_bodies { + lower_with_test_bodies(&parsed.module, &resolved, &checked, &origins) + .expect("test HIR lowering failed") + } else { + lower(&parsed.module, &resolved, &checked, &origins).expect("HIR lowering failed") + }; + (program, checked) + } + + #[test] + fn completing_generated_value_conversions_reuses_existing_adapters() { + let file = FileId::new(0); + let source = "type Source = function(secret[int64]) returns secret[int64]\ntype Target = function(int64) returns secret[int64]\nfunction secret_identity(value: secret[int64]) returns secret[int64]:\n return value\nfunction callback() returns Target:\n return secret_identity\nfunction convert(items: list[Source]) returns list[Target]:\n return items\n"; + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + !checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "{:?}", + checked.diagnostics + ); + let mut program = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + let initial = program.clone(); + assert_eq!( + program + .functions + .iter() + .filter(|function| function + .identity + .declaration + .name + .starts_with("$interface.adapter.")) + .count(), + 1 + ); + complete_value_conversions(&mut program, &checked.interner).unwrap(); + complete_value_conversions(&mut program, &checked.interner).unwrap(); + assert_eq!(program, initial); + } + + #[test] + fn lowers_checked_verify_and_property_bodies_as_native_test_functions() { + let program = lower_source_mode( + r#"namespace app +verify equal_literals: + assert 1 == 1 +property identity: + given value: int64 + assert value == value +"#, + true, + ); + assert_eq!(program.functions.len(), 2); + assert_eq!(program.functions[0].identity.declaration.namespace, "app"); + assert_eq!(program.functions[1].identity.declaration.namespace, "app"); + assert_eq!(program.functions[0].params.len(), 0); + assert_eq!(program.functions[1].params.len(), 1); + assert_eq!(program.functions[1].params[0].ty, TypeInterner::INT64); + assert!(matches!( + program.functions[0].body.statements[0].kind, + StatementKind::Assert { .. } + )); + assert!(matches!( + program.functions[1].body.statements[0].kind, + StatementKind::Assert { .. } + )); + } + + #[test] + fn lowers_capability_parameters_with_nominal_types() { + let program = lower_source( + r#"namespace app +function main(stdout: Stdout, stderr: Stderr, stdin: Stdin, filesystem: Filesystem, network: Network, clock: Clock, random: Random, process: Process, environment: Environment, log: Log, graphics: Graphics) returns nothing: + return nothing +"#, + ); + let actual = program.functions[0] + .params + .iter() + .map(|param| param.ty) + .collect::>(); + let expected = CapabilityKind::ALL.map(TypeInterner::capability).to_vec(); + + assert_eq!(actual, expected); + assert!(actual.iter().all(|type_id| *type_id != TypeInterner::ERROR)); + } + + #[test] + fn lowers_typed_functions_calls_and_control_flow_deterministically() { + let source = r#"namespace app +function add(a: int64, b: int64) returns int64: + return a + b +function choose(flag: bool, a: int64, b: int64) returns int64: + if flag: + return add(a, b) + else: + return b +"#; + let first = lower_source(source); + let second = lower_source(source); + assert_eq!(first, second); + assert_eq!(first.functions.len(), 2); + + let add = &first.functions[0]; + assert_eq!(add.id.index(), 0); + assert_eq!(add.identity.declaration.namespace, "app"); + assert_eq!(add.identity.declaration.name, "add"); + assert_eq!(add.identity.declaration.origin, SourceOrigin::Project); + assert_eq!(add.params.len(), 2); + assert_eq!(add.locals.len(), 2); + + let choose = &first.functions[1]; + assert_eq!(choose.id.index(), 1); + assert_eq!(choose.params.len(), 3); + let StatementKind::If { then_block, .. } = &choose.body.statements[0].kind else { + panic!("expected lowered if statement"); + }; + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + })) = &then_block.statements[0].kind + else { + panic!("expected lowered direct call"); + }; + assert_eq!(*function, add.id); + } + + #[test] + fn requires_explicit_source_origin() { + let source = "function main() returns nothing:\n return\n"; + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + let errors = lower(&parsed.module, &resolved, &checked, &HashMap::new()) + .expect_err("missing authority must fail"); + assert!( + errors + .iter() + .any(|error| error.message.contains("source origin")) + ); + } + + #[test] + fn lowers_mutual_function_bodies_through_declaration_resolutions() { + let program = lower_source( + r#"namespace app +mutual: + function is_even(value: int64) returns bool + function is_odd(value: int64) returns bool +function is_even(value: int64) returns bool: + if value == 0: + return true + return is_odd(value - 1) +function is_odd(value: int64) returns bool: + if value == 0: + return false + return is_even(value - 1) +"#, + ); + + assert_eq!(program.functions.len(), 2); + assert!( + program + .functions + .iter() + .all(|function| function.source_definition.is_some()) + ); + } + + #[test] + fn canonicalizes_contextual_negated_literals_at_signed_minimum() { + let program = lower_source("function minimum() returns int8:\n return -128\n"); + let StatementKind::Return(Some(value)) = &program.functions[0].body.statements[0].kind + else { + panic!("expected a returned literal"); + }; + assert_eq!(value.ty, jett_types::TypeInterner::INT8); + assert_eq!(value.kind, ExpressionKind::Int(-128)); + } + + #[test] + fn snapshots_core_hir() { + let program = lower_source( + r#"namespace app +function add(left: int64, right: int64) returns int64: + return left + right +function main() returns int64: + int64 value = add(right: 2, left: 1) + if value > 0: + return value + return 0 +"#, + ); + insta::assert_debug_snapshot!("hir_core", program); + } + + #[test] + fn snapshots_enum_and_handle_hir() { + let program = lower_source( + r#"namespace app +enum Value: + number(value: int64) + missing +function parse(raw: string) returns result[Value, string]: + return ok(Value.number(1)) +function main() returns int64: + Value value = parse("x") handle error: + default Value.missing + match value: + number(number): + return number + missing: + return 0 +"#, + ); + insta::assert_debug_snapshot!("hir_enum_handle", program); + } + + #[test] + fn validator_rejects_unknown_function_targets() { + let mut program = lower_source( + r#"namespace app +function identity(value: int64) returns int64: + return value +function main() returns int64: + return identity(1) +"#, + ); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + })) = &mut program.functions[1].body.statements[0].kind + else { + panic!("expected call"); + }; + *function = FunctionId(99); + let errors = validate(&program).expect_err("invalid target must fail validation"); + assert!( + errors + .iter() + .any(|error| error.message.contains("unknown HIR function")) + ); + } + + #[test] + fn validator_rejects_invalid_argument_evaluation_orders() { + let mut program = lower_source( + r#"namespace app +function difference(left: int64, right: int64) returns int64: + return left - right +function main() returns int64: + return difference(right: 2, left: 7) +"#, + ); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { + evaluation_order, .. + }, + .. + })) = &mut program.functions[1].body.statements[0].kind + else { + panic!("expected call"); + }; + *evaluation_order = vec![0, 0]; + + let errors = validate(&program).expect_err("invalid evaluation order must fail validation"); + assert!(errors.iter().any(|error| { + error + .message + .contains("argument evaluation order must be a permutation") + })); + } + + #[test] + fn validator_rejects_invalid_enum_payload_evaluation_orders() { + let original = lower_source( + r#"namespace app +enum Pair: + values(left: int64, right: int64) +function make() returns Pair: + return Pair.values(right: 2, left: 7) +"#, + ); + for invalid_order in [vec![0, 0], vec![1], vec![0, 2]] { + let mut program = original.clone(); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::EnumConstruct { + evaluation_order, .. + }, + .. + })) = &mut program.functions[0].body.statements[0].kind + else { + panic!("expected enum constructor"); + }; + *evaluation_order = invalid_order; + let errors = validate(&program).expect_err("invalid enum payload order"); + assert!(errors.iter().any(|error| { + error + .message + .contains("argument evaluation order must be a permutation") + })); + } + } + + #[test] + fn lowers_mutable_locals_assignments_and_loops() { + let source = r#"namespace app +function count_to(limit: int64) returns int64: + mutable int64 current = 0 + while current < limit: + current = current + 1 + return current +"#; + let program = lower_source(source); + let function = &program.functions[0]; + assert_eq!(function.locals.len(), 2); + assert!(function.locals[1].mutable); + assert!(matches!( + function.body.statements[0].kind, + StatementKind::Let { .. } + )); + let StatementKind::While { body, .. } = &function.body.statements[1].kind else { + panic!("expected lowered while statement"); + }; + assert!(matches!( + body.statements[0].kind, + StatementKind::Assign { .. } | StatementKind::UpdateField { .. } + )); + } + + #[test] + fn lowers_for_debug_comptime_and_interpolation_forms() { + let source = r#"namespace app +function render(view values: list[int64]) returns string: + mutable string output = comptime "values" + for value in view values: + breakpoint value > 10 + trace value + output = "{output}:{value}" + return output +"#; + let program = lower_source(source); + let function = &program.functions[0]; + assert!(matches!( + function.body.statements[0].kind, + StatementKind::Let { + value: Expression { + kind: ExpressionKind::Comptime { .. }, + .. + }, + .. + } + )); + let StatementKind::For { body, .. } = &function.body.statements[1].kind else { + panic!("expected explicit for loop"); + }; + assert!(matches!( + body.statements[0].kind, + StatementKind::Breakpoint { + condition: Some(_), + .. + } + )); + assert!(matches!(body.statements[1].kind, StatementKind::Trace(_))); + let StatementKind::Assign { value, .. } = &body.statements[2].kind else { + panic!("expected interpolation assignment"); + }; + assert!(matches!(value.kind, ExpressionKind::StringInterpolation(_))); + } + + #[test] + fn implicit_struct_equality_checks_results_before_negation_and_preserves_secret_types() { + fn initializer<'a>(function: &'a Function, name: &str) -> &'a Expression { + let local = function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() + .id; + function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { + local: target, + value, + } if *target == local => Some(value), + _ => None, + }) + .unwrap() + } + let (program, checked) = lower_source_with_prelude( + r#"namespace app +struct Item: + id: int64 +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return run true +enum Choice: + item(value: Item) +function inspect(view left: Item, view right: Item) returns bool: + bool direct = Item.equals(view left, view right) + bool equal = left == right + bool different = left != right + bool primitive = true == false + trace direct + trace equal + trace primitive + return different +function hidden(view left: secret[Item], view right: Item) returns secret[bool]: + secret[bool] equal = left == right + secret[bool] different = left != right + trace equal + return different +function nested(view left: Choice, view right: Choice) returns bool: + return left == right +"#, + false, + true, + ); + let inspect = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let ExpressionKind::Call { + function: target, .. + } = initializer(inspect, "direct").kind + else { + panic!("explicit equality method call must remain unwrapped"); + }; + assert!(matches!( + initializer(inspect, "primitive").kind, + ExpressionKind::Binary { .. } + )); + for name in ["inspect", "hidden"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + for binding in ["equal", "different"] { + let value = initializer(function, binding); + let boundary = if binding == "different" { + let ExpressionKind::Unary { + op: UnaryOp::Not, + value: inner, + } = &value.kind + else { + panic!("inequality must negate the checked result"); + }; + assert_eq!(inner.ty, value.ty); + inner.as_ref() + } else { + value + }; + let ExpressionKind::EquatableResult(call) = &boundary.kind else { + panic!("implicit equality must check its method result"); + }; + assert_eq!(call.ty, boundary.ty); + assert_eq!(call.span, boundary.span); + assert!( + matches!(call.kind, ExpressionKind::Call { function, .. } if function == target) + ); + if name == "hidden" { + assert!( + matches!(checked.interner.resolve(boundary.ty), Type::Secret(inner) + if *inner == TypeInterner::BOOL) + ); + let ExpressionKind::Call { args, .. } = &call.kind else { + unreachable!() + }; + assert!(matches!( + checked.interner.resolve(args[0].ty), + Type::Secret(_) + )); + } else { + assert_eq!(boundary.ty, TypeInterner::BOOL); + } + } + } + let nested = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "nested") + .unwrap(); + assert!(matches!( + &nested.body.statements[0].kind, + StatementKind::Return(Some(Expression { + kind: ExpressionKind::Binary { .. }, + .. + })) + )); + } + + #[test] + fn interpolation_checks_only_implicitly_selected_display_results() { + let program = lower_source( + r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return run "shown" +function inspect(view item: Item) returns string: + string direct = Item.display(view item) + string pending = run "plain" + return "{item}:{direct}:{pending}:{7}" +"#, + ); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let StatementKind::Let { value: direct, .. } = &function.body.statements[0].kind else { + panic!("expected direct method call binding"); + }; + let ExpressionKind::Call { + function: target, .. + } = &direct.kind + else { + panic!("direct method call must remain unwrapped"); + }; + let StatementKind::Return(Some(interpolation)) = &function.body.statements[2].kind else { + panic!("expected interpolation return"); + }; + let ExpressionKind::StringInterpolation(segments) = &interpolation.kind else { + panic!("expected interpolation"); + }; + let values = segments + .iter() + .filter_map(|segment| match segment { + StringSegment::Value(value) => Some(value), + StringSegment::Text(_) => None, + }) + .collect::>(); + assert_eq!(values.len(), 4); + let ExpressionKind::DisplayResult(selected) = &values[0].kind else { + panic!("implicit display call must retain its result check"); + }; + assert_eq!(values[0].ty, TypeInterner::STRING); + assert_eq!(selected.ty, TypeInterner::STRING); + assert_eq!(selected.span, values[0].span); + assert!( + matches!(selected.kind, ExpressionKind::Call { function, .. } + if function == *target) + ); + assert!(matches!(values[1].kind, ExpressionKind::Local(_))); + assert!(matches!(values[2].kind, ExpressionKind::Local(_))); + assert!(matches!(values[3].kind, ExpressionKind::Int(7))); + } + + #[test] + fn omits_uninhabited_loop_bodies_without_extracting_inline_functions() { + let program = lower_source( + r#"namespace app +function main() returns nothing: + for item in run list(): + function(int64) returns int64 callback = function(value: int64) returns int64: + return value + int64 copied = callback(item) + trace copied + for key, value in run map(): + trace key + trace value + return nothing +"#, + ); + assert_eq!(program.functions.len(), 1); + let function = &program.functions[0]; + assert_eq!(function.locals.len(), 3); + for statement in &function.body.statements[..2] { + let StatementKind::For { + key, + value, + iterable, + body, + .. + } = &statement.kind + else { + panic!("expected a retained typed loop"); + }; + assert_eq!( + function.locals[key.index() as usize].ty, + TypeInterner::NEVER + ); + if let Some(value) = value { + assert_eq!( + function.locals[value.index() as usize].ty, + TypeInterner::NEVER + ); + } + assert!(matches!(iterable.kind, ExpressionKind::Run(_))); + assert!(body.statements.is_empty()); + } + } + + #[test] + fn uninhabited_generic_loops_consume_dead_body_facts_and_keep_reserved_identities() { + let program = lower_source( + r#"namespace app +function identity[T](value: T) returns T: + return value +function count[T](view values: list[T]) returns int64: + mutable int64 total = 0 + for value in view values: + T copied = identity(value) + if type.kind_tag[T]() == TypeKind.primitive_type: + trace copied + else: + trace value + total = total + 1 + return total +function main() returns int64: + return count(list()) +"#, + ); + let count = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "count") + .unwrap(); + assert_eq!(count.identity.type_arguments, [TypeInterner::NEVER]); + assert!(!count.locals.iter().any(|local| local.name == "copied")); + let StatementKind::For { by_view, body, .. } = &count.body.statements[1].kind else { + panic!("expected an empty generic iteration body"); + }; + assert!(*by_view); + assert!(body.statements.is_empty()); + // Checked generic identities remain eagerly reserved. Native + // reachability, rather than HIR body lowering, excludes dead targets. + let identity = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "identity") + .unwrap(); + assert_eq!(identity.identity.type_arguments, [TypeInterner::NEVER]); + assert_eq!(identity.params[0].ty, TypeInterner::NEVER); + assert_eq!(identity.return_type, TypeInterner::NEVER); + } + + #[test] + fn retains_bodies_for_empty_collections_with_inhabited_element_types() { + let program = lower_source( + r#"namespace app +function main() returns nothing: + list[int64] numbers = list() + for number in view numbers: + trace number + map[int64, string] names = map() + for key, value in view names: + trace key + trace value + return nothing +"#, + ); + let function = &program.functions[0]; + for (statement_index, expected_statements) in [(1, 1), (3, 2)] { + let StatementKind::For { body, .. } = &function.body.statements[statement_index].kind + else { + panic!("expected an inhabited collection loop"); + }; + assert_eq!(body.statements.len(), expected_statements); + assert!( + body.statements + .iter() + .all(|statement| matches!(statement.kind, StatementKind::Trace(_))) + ); + } + } + + #[test] + fn lowers_actor_receive_handlers_as_backend_functions() { + let program = lower_source( + r#"namespace app +actor Counter: + mutable int64 count = 0 + receive add(amount: int64) responds int64: + count = count + amount + respond count +"#, + ); + + assert_eq!(program.functions.len(), 2); + let constructor = &program.functions[0]; + assert_eq!( + constructor.identity.declaration.kind, + DeclarationKind::ActorConstructor + ); + assert!(matches!( + constructor.body.statements.as_slice(), + [ + Statement { + kind: StatementKind::Let { .. }, + .. + }, + Statement { + kind: StatementKind::Return(Some(Expression { + kind: ExpressionKind::ActorSpawn { + constructor: None, + .. + }, + .. + })), + .. + } + ] + )); + let handler = &program.functions[1]; + assert_eq!(handler.identity.declaration.namespace, "app"); + assert_eq!(handler.identity.declaration.name, "Counter.add"); + assert_eq!(handler.capture_count, 1); + assert_eq!(handler.params.len(), 2); + assert_eq!(handler.params[0].name, "count"); + assert_eq!(handler.params[1].name, "amount"); + assert!(matches!( + handler.body.statements.as_slice(), + [ + Statement { + kind: StatementKind::Assign { .. } | StatementKind::UpdateField { .. }, + .. + }, + Statement { + kind: StatementKind::Respond(_), + .. + } + ] + )); + } + + #[test] + fn lowers_nested_generic_instantiations_and_calls() { + let source = r#"namespace app +function inner[T](value: T) returns T: + return value +function outer[T](value: T) returns T: + return inner[T](value) +function main() returns int64: + return outer[int64](42) +"#; + let program = lower_source(source); + assert_eq!(program.functions.len(), 3); + + let main = &program.functions[0]; + let outer = &program.functions[1]; + let inner = &program.functions[2]; + assert!(main.identity.type_arguments.is_empty()); + assert_eq!(outer.identity.type_arguments.len(), 1); + assert_eq!(inner.identity.type_arguments.len(), 1); + assert_eq!(outer.params[0].ty, outer.identity.type_arguments[0]); + assert_eq!(inner.params[0].ty, inner.identity.type_arguments[0]); + + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + })) = &main.body.statements[0].kind + else { + panic!("expected main to call outer[int64]"); + }; + assert_eq!(*function, outer.id); + + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + })) = &outer.body.statements[0].kind + else { + panic!("expected outer[int64] to call inner[int64]"); + }; + assert_eq!(*function, inner.id); + } + + #[test] + fn prunes_checker_selected_generic_if_and_match_control_flow() { + let program = lower_source( + r#"namespace app +function list_length_or_zero[T](value: T) returns int64: + if type.kind_tag[T]() == TypeKind.list_type: + list[int64] items = value + return 1 + return 0 +function primitive_string_or_other[T](value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + match primitive: + string_type: + string text = value + return text + other: + return "other" +function main() returns string: + list[int64] values = list(1, 2) + int64 present = list_length_or_zero[list[int64]](values) + int64 absent = list_length_or_zero[string]("Ada") + string text = primitive_string_or_other[string]("Ada") + string other = primitive_string_or_other[int64](7) + return "{present}:{absent}:{text}:{other}" +"#, + ); + + let list_instantiations = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "list_length_or_zero") + .collect::>(); + assert_eq!(list_instantiations.len(), 2); + assert!(list_instantiations.iter().any(|function| { + matches!( + function.body.statements.as_slice(), + [ + Statement { + kind: StatementKind::Scope(_), + .. + }, + Statement { + kind: StatementKind::Return(_), + .. + } + ] + ) + })); + assert!(list_instantiations.iter().any(|function| { + matches!( + function.body.statements.as_slice(), + [Statement { + kind: StatementKind::Return(_), + .. + }] + ) + })); + + let match_instantiations = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "primitive_string_or_other") + .collect::>(); + assert_eq!(match_instantiations.len(), 2); + assert!(match_instantiations.iter().all(|function| { + function + .body + .statements + .iter() + .all(|statement| !matches!(statement.kind, StatementKind::Match { .. })) + && function + .body + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::Scope(_))) + })); + } + + #[test] + fn alias_and_underlying_type_lower_to_distinct_branch_and_match_functions() { + let program = lower_source( + r#"namespace app +type Names = list[string] +function classify_branch[T]() returns string: + if type.kind_tag[T]() == TypeKind.alias_type: + return "alias" + else: + return "list" +function classify_match[T]() returns string: + match type.kind_tag[T](): + alias_type: + return "alias" + other: + return "list" +function main() returns nothing: + string branch_alias = classify_branch[Names]() + string branch_list = classify_branch[list[string]]() + string match_alias = classify_match[Names]() + string match_list = classify_match[list[string]]() +"#, + ); + + for name in ["classify_branch", "classify_match"] { + let instantiations = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == name) + .collect::>(); + assert_eq!(instantiations.len(), 2); + assert_eq!( + instantiations[0].identity.type_arguments, + instantiations[1].identity.type_arguments + ); + assert_ne!( + instantiations[0].identity.specialization, + instantiations[1].identity.specialization + ); + assert!(instantiations.iter().all(|function| { + function.body.statements.iter().all(|statement| { + !matches!( + statement.kind, + StatementKind::If { .. } | StatementKind::Match { .. } + ) + }) + })); + } + + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("main should lower"); + let call_targets = main + .body + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::Let { + value: + Expression { + kind: ExpressionKind::Call { function, .. }, + .. + }, + .. + } => Some(*function), + _ => None, + }) + .collect::>(); + assert_eq!(call_targets.len(), 4); + } + + #[test] + fn rejects_static_selection_attached_to_the_wrong_statement_kind() { + let source = r#"function choose[T]() returns int64: + if type.kind_tag[T]() == TypeKind.list_type: + return 1 + return 0 +function main() returns int64: + return choose[list[int64]]() +"#; + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + let resolved = jett_resolve::resolve(&parsed.module); + let mut checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "type errors: {:?}", + checked.diagnostics + ); + let instantiation = checked + .generic_function_instantiations + .first_mut() + .expect("choose[list[int64]] should be instantiated"); + let span = *instantiation + .static_selections + .keys() + .next() + .expect("static if selection should be exported"); + instantiation + .static_selections + .insert(span, CheckedStaticSelection::MatchArm(0)); + + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let errors = lower(&parsed.module, &resolved, &checked, &origins) + .expect_err("mismatched static selection must fail lowering"); + assert!(errors.iter().any(|error| { + error + .message + .contains("static match selection attached to an if") + })); + } + + #[test] + fn keeps_inferred_instantiation_type_facts_separate() { + let source = r#"namespace app +function identity[T](value: T) returns T: + return value +function main() returns nothing: + int64 number = identity(1) + string text = identity("jett") +"#; + let program = lower_source(source); + assert_eq!(program.functions.len(), 3); + let main = &program.functions[0]; + let integer_identity = &program.functions[1]; + let string_identity = &program.functions[2]; + + assert_ne!( + integer_identity.identity.type_arguments, + string_identity.identity.type_arguments + ); + assert_eq!(integer_identity.params[0].ty, integer_identity.return_type); + assert_eq!(string_identity.params[0].ty, string_identity.return_type); + + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected first inferred generic call"); + }; + let ExpressionKind::Call { function, .. } = value.kind else { + panic!("expected first inferred generic call target"); + }; + assert_eq!(function, integer_identity.id); + + let StatementKind::Let { value, .. } = &main.body.statements[1].kind else { + panic!("expected second inferred generic call"); + }; + let ExpressionKind::Call { function, .. } = value.kind else { + panic!("expected second inferred generic call target"); + }; + assert_eq!(function, string_identity.id); + } + + #[test] + fn keeps_generic_loop_binding_types_separate() { + let program = lower_source( + r#"namespace app +function count[T](items: list[T]) returns int64: + mutable int64 total = 0 + for item in view items: + total = total + 1 + return total +function main() returns nothing: + int64 numbers = count[int64](list(1, 2)) + int64 words = count[string](list("one", "two")) +"#, + ); + let mut binding_types = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "count") + .map(|function| { + let item = function + .locals + .iter() + .find(|local| local.name == "item") + .expect("generic loop binding should lower"); + (function.identity.type_arguments[0], item.ty) + }) + .collect::>(); + binding_types.sort_by_key(|(argument, _)| argument.index()); + assert_eq!( + binding_types, + [ + (TypeInterner::INT64, TypeInterner::INT64), + (TypeInterner::STRING, TypeInterner::STRING), + ] + ); + } + + #[test] + fn lowers_recursive_generic_calls_to_the_reserved_identity() { + let source = r#"namespace app +function repeat[T](value: T, remaining: int64) returns T: + if remaining == 0: + return value + return repeat[T](value, remaining - 1) +function main() returns int64: + return repeat[int64](7, 2) +"#; + let program = lower_source(source); + assert_eq!(program.functions.len(), 2); + let repeat = &program.functions[1]; + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + })) = &repeat.body.statements[1].kind + else { + panic!("expected recursive concrete call"); + }; + assert_eq!(*function, repeat.id); } - fn expression_type(&mut self, expression: &Expr) -> Option { - if let Some(ty) = self.expression_types.get(&expression.span()).copied() { - return Some(ty); - } - if let Expr::Ident(ident) = expression - && let Some(definition) = self.parent.resolve.resolutions.get(&ident.span) - && let Some(ty) = self.parent.check.definition_types.get(definition) - { - return Some(*ty); - } - self.parent - .error(expression.span(), "expression has no checked type"); - None - } -} + #[test] + fn lowers_struct_construction_fields_and_named_calls_to_canonical_order() { + let source = r#"namespace app +struct Point: + x: int64 + y: int64 +function difference(left: int64, right: int64) returns int64: + return left - right +function main() returns int64: + Point point = Point(y: 2, x: 7) + return difference(right: point.y, left: point.x) +"#; + let program = lower_source(source); + let difference = &program.functions[0]; + let main = &program.functions[1]; -fn lower_binary_op(op: ast::BinOp) -> BinaryOp { - match op { - ast::BinOp::Add => BinaryOp::Add, - ast::BinOp::Sub => BinaryOp::Subtract, - ast::BinOp::Mul => BinaryOp::Multiply, - ast::BinOp::Div => BinaryOp::Divide, - ast::BinOp::Modulo => BinaryOp::Modulo, - ast::BinOp::Eq => BinaryOp::Equal, - ast::BinOp::NotEq => BinaryOp::NotEqual, - ast::BinOp::Lt => BinaryOp::Less, - ast::BinOp::Gt => BinaryOp::Greater, - ast::BinOp::LtEq => BinaryOp::LessEqual, - ast::BinOp::GtEq => BinaryOp::GreaterEqual, - ast::BinOp::And => BinaryOp::And, - ast::BinOp::Or => BinaryOp::Or, - } -} + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected point construction"); + }; + let ExpressionKind::StructConstruct { fields, .. } = &value.kind else { + panic!("expected canonical struct construction"); + }; + assert!(matches!(fields[0].kind, ExpressionKind::Int(7))); + assert!(matches!(fields[1].kind, ExpressionKind::Int(2))); -fn lower_unary_op(op: ast::UnaryOp) -> UnaryOp { - match op { - ast::UnaryOp::Not => UnaryOp::Not, - ast::UnaryOp::Neg => UnaryOp::Negate, + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Call { + function, + args, + evaluation_order, + .. + }, + .. + })) = &main.body.statements[1].kind + else { + panic!("expected named function call"); + }; + assert_eq!(*function, difference.id); + assert_eq!(evaluation_order, &[1, 0]); + let ExpressionKind::Field { field, .. } = args[0].kind else { + panic!("expected left field argument"); + }; + assert_eq!(field.index(), 0); + let ExpressionKind::Field { field, .. } = args[1].kind else { + panic!("expected right field argument"); + }; + assert_eq!(field.index(), 1); } -} - -#[cfg(test)] -mod tests { - use super::*; - use jett_common::FileId; - use jett_diagnostics::Severity; - fn lower_source(source: &str) -> Program { - let file = FileId::new(0); - let parsed = jett_parser::parse(source, file); - assert!( - parsed.errors.is_empty(), - "parse errors: {:?}", - parsed.errors - ); - let resolved = jett_resolve::resolve(&parsed.module); - assert!( - resolved - .diagnostics - .iter() - .all(|d| d.severity != Severity::Error), - "resolve errors: {:?}", - resolved.diagnostics + #[test] + fn lowers_interface_calls_to_the_concrete_method_body() { + let source = r#"namespace app +interface Scored: + function score(view self: Scored, bonus: int64) returns int64 +struct User: + points: int64 +implement Scored for User: + function score(view self: User, bonus: int64) returns int64: + return self.points + bonus +function main() returns int64: + User user = User(points: 7) + return Scored.score(bonus: 5, self: view user) +"#; + let program = lower_source(source); + assert_eq!(program.functions.len(), 2); + let main = &program.functions[0]; + let method = &program.functions[1]; + assert_eq!(method.identity.declaration.kind, DeclarationKind::Method); + assert_eq!( + method.identity.declaration.name, + "app.User as app.Scored.score" ); - let checked = jett_typecheck::check(&parsed.module, &resolved); - assert!( - checked - .diagnostics - .iter() - .all(|d| d.severity != Severity::Error), - "type errors: {:?}", - checked.diagnostics + assert_eq!( + method.debug_kind, + FunctionDebugKind::Named("app.User.score".into()) ); - let origins = HashMap::from([(file, SourceOrigin::Project)]); - lower(&parsed.module, &resolved, &checked, &origins).expect("HIR lowering failed") + + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Call { + function, + args, + evaluation_order, + .. + }, + .. + })) = &main.body.statements[1].kind + else { + panic!("expected concrete method call"); + }; + assert_eq!(*function, method.id); + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert!(matches!(args[1].kind, ExpressionKind::Int(5))); } #[test] - fn lowers_typed_functions_calls_and_control_flow_deterministically() { - let source = r#"namespace app -function add(a: int64, b: int64) returns int64: - return a + b -function choose(flag: bool, a: int64, b: int64) returns int64: - if flag: - return add(a, b) - else: - return b -"#; - let first = lower_source(source); - let second = lower_source(source); - assert_eq!(first, second); - assert_eq!(first.functions.len(), 2); + fn lowers_aliased_concrete_method_values_to_source_targets() { + let program = lower_source( + r#"namespace models +export interface Reader: + function read(view self: Reader) returns int64 +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +implement Reader for Point: + function read(view self: Point) returns int64: + return self.value +namespace app +struct Callbacks: + amount: function(view models.Point) returns int64 + read: function(view models.Point) returns int64 +function reader() returns function(view models.Point) returns int64: + use models as m + return (m.Point.read) +function main() returns int64: + use models as m + function(view m.Point) returns int64 callback = (m.Point.amount) + Callbacks callbacks = Callbacks(amount: m.Point.amount, read: m.Point.read) + m.Point point = m.Point(value: 7) + return (callback)(view point) +"#, + ); + let method = |name: &str| { + program + .functions + .iter() + .find(|function| function.debug_kind == FunctionDebugKind::Named(name.into())) + .expect("concrete source method") + }; + let amount = method("models.Point.amount"); + let read = method("models.Point.read"); + assert_eq!(amount.identity.declaration.kind, DeclarationKind::Method); + assert_eq!(read.identity.declaration.kind, DeclarationKind::Method); + assert_eq!(amount.params[0].mode, ParamMode::View); + assert_eq!(read.params[0].mode, ParamMode::View); + assert_ne!(amount.id, read.id); + assert_eq!( + read.identity.declaration.name, + "models.Point as models.Reader.read" + ); - let add = &first.functions[0]; - assert_eq!(add.id.index(), 0); - assert_eq!(add.identity.declaration.namespace, "app"); - assert_eq!(add.identity.declaration.name, "add"); - assert_eq!(add.identity.declaration.origin, SourceOrigin::Project); - assert_eq!(add.params.len(), 2); - assert_eq!(add.locals.len(), 2); + let reader = method("app.reader"); + let StatementKind::Return(Some(value)) = &reader.body.statements[0].kind else { + panic!("expected returned method value"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(read.id)); - let choose = &first.functions[1]; - assert_eq!(choose.id.index(), 1); - assert_eq!(choose.params.len(), 3); - let StatementKind::If { then_block, .. } = &choose.body.statements[0].kind else { - panic!("expected lowered if statement"); + let main = method("app.main"); + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected method local"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(amount.id)); + let StatementKind::Let { + value: + Expression { + kind: ExpressionKind::StructConstruct { fields, .. }, + .. + }, + .. + } = &main.body.statements[1].kind + else { + panic!("expected aggregate containing method values"); }; + assert_eq!(fields[0].kind, ExpressionKind::FunctionRef(amount.id)); + assert_eq!(fields[1].kind, ExpressionKind::FunctionRef(read.id)); let StatementKind::Return(Some(Expression { - kind: ExpressionKind::Call { function, .. }, + kind: ExpressionKind::IndirectCall { args, .. }, .. - })) = &then_block.statements[0].kind + })) = &main.body.statements[3].kind else { - panic!("expected lowered direct call"); + panic!("expected indirect method call"); }; - assert_eq!(*function, add.id); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); } #[test] - fn requires_explicit_source_origin() { - let source = "function main() returns nothing:\n return\n"; - let file = FileId::new(0); - let parsed = jett_parser::parse(source, file); - let resolved = jett_resolve::resolve(&parsed.module); - let checked = jett_typecheck::check(&parsed.module, &resolved); - let errors = lower(&parsed.module, &resolved, &checked, &HashMap::new()) - .expect_err("missing authority must fail"); + fn lowers_callback_only_interface_slots_with_canonical_names_and_view_modes() { + let program = lower_source( + r#"namespace models +export interface Reader: + function read(view self: Reader) returns int64 + function write(view self: Reader, view stdout: Stdout) returns nothing +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 + function write(view self: int64, view stdout: Stdout) returns nothing: + return nothing +namespace app +function reader() returns function(view models.Reader) returns int64: + use models as m + return (m.Reader.read) +function writer() returns function(view models.Reader, view Stdout) returns nothing: + use models + return models.Reader.write +function main() returns int64: + use models as m + function(view m.Reader) returns int64 callback = m.Reader.read + m.Reader value = 7 + return callback(view value) +"#, + ); + let named = |name: &str| { + program + .functions + .iter() + .find(|function| function.debug_kind == FunctionDebugKind::Named(name.into())) + .expect("named HIR function") + }; + let read = named("models.Reader.read"); + let write = named("models.Reader.write"); + assert_eq!(read.identity.declaration.namespace, "models"); + assert_eq!(read.identity.declaration.kind, DeclarationKind::Method); + assert_eq!(read.capture_count, 0); + assert_eq!(write.capture_count, 0); + assert_eq!(read.params.len(), 1); + assert_eq!(write.params.len(), 2); + assert_eq!(read.params[0].mode, ParamMode::View); assert!( - errors + write + .params .iter() - .any(|error| error.message.contains("source origin")) + .all(|param| param.mode == ParamMode::View) + ); + assert_eq!(read.params[0].ty, write.params[0].ty); + assert_eq!(read.return_type, TypeInterner::INT64); + assert_eq!(write.return_type, TypeInterner::NOTHING); + assert_eq!( + read.body.statements.len(), + 2, + "dispatch and failure fallback" + ); + assert_eq!(write.body.statements.len(), 2); + for (factory, target) in [("app.reader", read.id), ("app.writer", write.id)] { + let StatementKind::Return(Some(value)) = &named(factory).body.statements[0].kind else { + panic!("expected a returned interface method value"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(target)); + } + let main = named("app.main"); + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected interface callback binding"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(read.id)); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::IndirectCall { args, .. }, + .. + })) = &main.body.statements[2].kind + else { + panic!("expected indirect interface callback invocation"); + }; + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.debug_kind == read.debug_kind) + .count(), + 1, + "repeated method values must share one dispatcher", ); } #[test] - fn snapshots_core_hir() { + fn collects_interface_method_values_found_only_in_generic_body_facts() { let program = lower_source( r#"namespace app -function add(left: int64, right: int64) returns int64: - return left + right +interface Reader: + function read(view self: Reader) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self +function factory[T](seed: T) returns function(view Reader) returns int64: + return Reader.read function main() returns int64: - int64 value = add(right: 2, left: 1) - if value > 0: - return value - return 0 + function(view Reader) returns int64 first = factory[int64](1) + function(view Reader) returns int64 second = factory[string]("two") + Reader value = 7 + return first(view value) + second(view value) "#, ); - insta::assert_debug_snapshot!("hir_core", program); + let dispatcher = program + .functions + .iter() + .find(|function| { + function.debug_kind == FunctionDebugKind::Named("app.Reader.read".into()) + }) + .expect("callback-only generic interface dispatcher"); + let factories = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "factory") + .collect::>(); + assert_eq!(factories.len(), 2); + for factory in factories { + let StatementKind::Return(Some(value)) = &factory.body.statements[0].kind else { + panic!("expected specialized interface callback return"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(dispatcher.id)); + } } #[test] - fn snapshots_enum_and_handle_hir() { + fn collects_interface_method_values_in_nested_reflected_body_facts() { let program = lower_source( r#"namespace app -enum Value: - number(value: int64) - missing -function parse(raw: string) returns result[Value, string]: - return ok(Value.number(1)) +interface Reader: + function read(view self: Reader) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self +struct Inner: + count: int64 +struct Outer: + item: Inner +function reflected[T](view value: T, view reader: Reader) returns int64: + mutable int64 output = 0 + for outer in type.fields[T](): + comptime type Field = outer.type_info: + for inner in type.fields[Field](): + comptime type Item = inner.type_info: + function(view Reader) returns int64 callback = Reader.read + output = callback(view reader) + return output function main() returns int64: - Value value = parse("x") handle error: - default Value.missing - match value: - number(number): - return number - missing: - return 0 + Outer value = Outer(item: Inner(count: 1)) + Reader reader = 7 + return reflected[Outer](view value, view reader) "#, ); - insta::assert_debug_snapshot!("hir_enum_handle", program); + let dispatcher = program + .functions + .iter() + .find(|function| { + function.debug_kind == FunctionDebugKind::Named("app.Reader.read".into()) + }) + .expect("nested reflected interface dispatcher"); + let reflected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "reflected") + .expect("specialized reflected function"); + let StatementKind::For { body, .. } = &reflected.body.statements[1].kind else { + panic!("expected outer reflected loop"); + }; + let StatementKind::ReflectedTypeDispatch { arms, .. } = &body.statements[0].kind else { + panic!("expected outer type binding dispatch"); + }; + assert_eq!(arms.len(), 1); + let StatementKind::For { body, .. } = &arms[0].body.statements[0].kind else { + panic!("expected inner reflected loop"); + }; + let StatementKind::ReflectedTypeDispatch { arms, .. } = &body.statements[0].kind else { + panic!("expected inner type binding dispatch"); + }; + assert_eq!(arms.len(), 1); + let StatementKind::Let { value, .. } = &arms[0].body.statements[0].kind else { + panic!("expected interface callback in nested binding body"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(dispatcher.id)); } #[test] - fn validator_rejects_unknown_function_targets() { - let mut program = lower_source( + fn keeps_concrete_method_values_in_generic_and_inline_fact_contexts() { + let program = lower_source( r#"namespace app -function identity(value: int64) returns int64: - return value +struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +function factory[T](seed: T) returns function(view Point) returns int64: + return Point.amount +function inline_factory() returns function() returns function(view Point) returns int64: + return function() returns function(view Point) returns int64: return Point.amount function main() returns int64: - return identity(1) + function(view Point) returns int64 first = factory[int64](1) + function(view Point) returns int64 second = factory[string]("two") + Point point = Point(value: 7) + return first(view point) "#, ); - let StatementKind::Return(Some(Expression { - kind: ExpressionKind::Call { function, .. }, - .. - })) = &mut program.functions[1].body.statements[0].kind - else { - panic!("expected call"); - }; - *function = FunctionId(99); - let errors = validate(&program).expect_err("invalid target must fail validation"); - assert!( - errors - .iter() - .any(|error| error.message.contains("unknown HIR function")) - ); + let method = program + .functions + .iter() + .find(|function| { + function.debug_kind == FunctionDebugKind::Named("app.Point.amount".into()) + }) + .expect("concrete method target"); + let mut factory_count = 0; + let mut inline_count = 0; + for function in &program.functions { + if function.identity.declaration.name != "factory" + && function.debug_kind != FunctionDebugKind::Inline + { + continue; + } + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("expected returned concrete method value"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(method.id)); + if function.debug_kind == FunctionDebugKind::Inline { + inline_count += 1; + } else { + factory_count += 1; + } + } + assert_eq!(factory_count, 2); + assert_eq!(inline_count, 1); } #[test] - fn validator_rejects_invalid_argument_evaluation_orders() { - let mut program = lower_source( + fn keeps_method_values_when_switching_reflected_body_facts() { + let program = lower_source( r#"namespace app -function difference(left: int64, right: int64) returns int64: - return left - right +struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +struct Mixed: + label: string + count: int64 +function reflected[T](view value: T, view point: Point) returns int64: + mutable int64 output = 0 + for field in type.fields[T](): + comptime type Field = field.type_info: + function(view Point) returns int64 callback = Point.amount + output = callback(view point) + function(view Point) returns int64 after = Point.amount + return after(view point) + output function main() returns int64: - return difference(right: 2, left: 7) + Mixed value = Mixed(label: "one", count: 1) + Point point = Point(value: 7) + return reflected[Mixed](view value, view point) "#, ); - let StatementKind::Return(Some(Expression { - kind: ExpressionKind::Call { - evaluation_order, .. - }, - .. - })) = &mut program.functions[1].body.statements[0].kind - else { - panic!("expected call"); + let method = program + .functions + .iter() + .find(|function| { + function.debug_kind == FunctionDebugKind::Named("app.Point.amount".into()) + }) + .expect("concrete method target"); + let reflected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "reflected") + .expect("concrete reflected function"); + let StatementKind::For { body, .. } = &reflected.body.statements[1].kind else { + panic!("expected reflected loop"); }; - *evaluation_order = vec![0, 0]; - - let errors = validate(&program).expect_err("invalid evaluation order must fail validation"); - assert!(errors.iter().any(|error| { - error - .message - .contains("argument evaluation order must be a permutation") - })); + let StatementKind::ReflectedTypeDispatch { arms, .. } = &body.statements[0].kind else { + panic!("expected reflected type dispatch"); + }; + assert_eq!(arms.len(), 2); + for arm in arms { + let StatementKind::Let { value, .. } = &arm.body.statements[0].kind else { + panic!("expected method local inside specialized body"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(method.id)); + } + let StatementKind::Let { value, .. } = &reflected.body.statements[2].kind else { + panic!("expected method local after specialized body"); + }; + assert_eq!(value.kind, ExpressionKind::FunctionRef(method.id)); } #[test] - fn lowers_mutable_locals_assignments_and_loops() { + fn keeps_named_argument_orders_inside_generic_instantiations() { let source = r#"namespace app -function count_to(limit: int64) returns int64: - mutable int64 current = 0 - while current < limit: - current = current + 1 - return current +function inner[T](first: T, second: T) returns T: + return first +function outer[T](first: T, second: T) returns T: + return inner[T](second: second, first: first) +function main() returns int64: + return outer[int64](second: 2, first: 7) "#; let program = lower_source(source); - let function = &program.functions[0]; - assert_eq!(function.locals.len(), 2); - assert!(function.locals[1].mutable); - assert!(matches!( - function.body.statements[0].kind, - StatementKind::Let { .. } - )); - let StatementKind::While { body, .. } = &function.body.statements[1].kind else { - panic!("expected lowered while statement"); - }; - assert!(matches!( - body.statements[0].kind, - StatementKind::Assign { .. } - )); + let main = &program.functions[0]; + let outer = &program.functions[1]; + + for function in [main, outer] { + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Call { + args, + evaluation_order, + .. + }, + .. + })) = &function.body.statements[0].kind + else { + panic!("expected named generic call"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!( + args[0].kind, + ExpressionKind::Int(7) | ExpressionKind::Local(_) + )); + } } #[test] - fn lowers_for_debug_comptime_and_interpolation_forms() { + fn lowers_list_and_map_construction_in_lexical_order() { let source = r#"namespace app -function render(view values: list[int64]) returns string: - mutable string output = comptime "values" - for value in view values: - breakpoint value > 10 - trace value - output = "{output}:{value}" - return output +function main() returns map[string, list[int64]]: + return map("odds": list(1, 3), "evens": list(2, 4)) "#; let program = lower_source(source); - let function = &program.functions[0]; - assert!(matches!( - function.body.statements[0].kind, - StatementKind::Let { - value: Expression { - kind: ExpressionKind::Comptime(_), - .. - }, - .. - } - )); - let StatementKind::For { body, .. } = &function.body.statements[1].kind else { - panic!("expected explicit for loop"); + let main = &program.functions[0]; + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::MapConstruct { entries }, + .. + })) = &main.body.statements[0].kind + else { + panic!("expected map construction"); }; - assert!(matches!( - body.statements[0].kind, - StatementKind::Breakpoint(Some(_)) - )); - assert!(matches!(body.statements[1].kind, StatementKind::Trace(_))); - let StatementKind::Assign { value, .. } = &body.statements[2].kind else { - panic!("expected interpolation assignment"); + assert_eq!(entries.len(), 2); + assert!( + matches!(entries[0].key.kind, ExpressionKind::String(ref value) if value == "odds") + ); + let ExpressionKind::ListConstruct { elements } = &entries[0].value.kind else { + panic!("expected nested list construction"); }; - assert!(matches!(value.kind, ExpressionKind::StringInterpolation(_))); + assert!(matches!(elements[0].kind, ExpressionKind::Int(1))); + assert!(matches!(elements[1].kind, ExpressionKind::Int(3))); } #[test] - fn lowers_actor_receive_handlers_as_backend_functions() { - let program = lower_source( - r#"namespace app -actor Counter: - mutable int64 count = 0 - receive add(amount: int64) responds int64: - count = count + amount - respond count + fn lowers_contextual_secret_collection_and_sum_constructors() { + let (program, checked) = lower_source_with_check( + r#"function main() returns nothing: + secret[list[int64]] numbers = list(5, 7) + secret[map[string, int64]] lookup = map("answer": 7) + secret[optional[int64]] present = some(7) + secret[optional[int64]] absent = none + secret[result[int64, string]] success = ok(7) + secret[result[int64, string]] failure = fail("reason") + secret[list[string]] empty_list = list() + secret[map[string, list[string]]] empty_map = map() + return nothing "#, + false, ); - - assert_eq!(program.functions.len(), 1); - let handler = &program.functions[0]; - assert_eq!(handler.identity.declaration.namespace, "app"); - assert_eq!(handler.identity.declaration.name, "Counter.add"); - assert_eq!(handler.params.len(), 1); - assert_eq!(handler.params[0].name, "amount"); - assert!(handler.locals.iter().any(|local| local.name == "count")); - assert!(matches!( - handler.body.statements.as_slice(), - [ - Statement { - kind: StatementKind::Assign { .. }, - .. - }, - Statement { - kind: StatementKind::Respond(_), - .. + let main = &program.functions[0]; + for (index, statement) in main.body.statements[..8].iter().enumerate() { + let StatementKind::Let { local, value } = &statement.kind else { + panic!("expected constructor local"); + }; + assert_eq!(value.ty, main.locals[local.index() as usize].ty); + assert_eq!(checked.type_map[&value.span], value.ty); + let Type::Secret(inner_type) = checked.interner.resolve(value.ty) else { + panic!("expected retained checked secret type"); + }; + let ExpressionKind::InterfaceCoerce { + value: constructor, + adapters, + } = &value.kind + else { + panic!("expected explicit secret qualification"); + }; + assert!(adapters.is_empty()); + assert_eq!(constructor.ty, *inner_type); + assert_eq!(constructor.span, value.span); + match (&constructor.kind, checked.interner.resolve(*inner_type)) { + (ExpressionKind::ListConstruct { elements }, Type::List(element_type)) => { + assert!(elements.iter().all(|element| element.ty == *element_type)); + if index == 0 { + assert!(matches!(elements[0].kind, ExpressionKind::Int(5))); + assert!(matches!(elements[1].kind, ExpressionKind::Int(7))); + } else { + assert!(elements.is_empty()); + assert_eq!(*element_type, TypeInterner::STRING); + } } - ] - )); + (ExpressionKind::MapConstruct { entries }, Type::Map(key, value)) => { + assert!(entries.iter().all(|entry| entry.key.ty == *key)); + assert!(entries.iter().all(|entry| entry.value.ty == *value)); + if index == 7 { + assert!(entries.is_empty()); + assert!(matches!(checked.interner.resolve(*value), Type::List(_))); + } + } + (ExpressionKind::OptionalSome(payload), Type::Optional(expected)) + | (ExpressionKind::ResultOk(payload), Type::Result(expected, _)) + | (ExpressionKind::ResultFail(payload), Type::Result(_, expected)) => { + assert_eq!(payload.ty, *expected); + } + (ExpressionKind::OptionalNone, Type::Optional(expected)) => { + assert_eq!(*expected, TypeInterner::INT64); + } + _ => panic!("constructor did not retain its exact container shape"), + } + } } #[test] - fn lowers_nested_generic_instantiations_and_calls() { - let source = r#"namespace app -function inner[T](value: T) returns T: - return value -function outer[T](value: T) returns T: - return inner[T](value) + fn secret_local_metadata_preserves_declared_qualification_and_producer_signatures() { + let (program, checked) = lower_source_with_check( + r#"function make(value: int8) returns list[int8]: + return list(value) +function read(view values: list[int8]) returns int64: + return 0 function main() returns int64: - return outer[int64](42) -"#; - let program = lower_source(source); - assert_eq!(program.functions.len(), 3); - - let main = &program.functions[0]; - let outer = &program.functions[1]; - let inner = &program.functions[2]; - assert!(main.identity.type_arguments.is_empty()); - assert_eq!(outer.identity.type_arguments.len(), 1); - assert_eq!(inner.identity.type_arguments.len(), 1); - assert_eq!(outer.params[0].ty, outer.identity.type_arguments[0]); - assert_eq!(inner.params[0].ty, inner.identity.type_arguments[0]); - - let StatementKind::Return(Some(Expression { - kind: ExpressionKind::Call { function, .. }, - .. - })) = &main.body.statements[0].kind - else { - panic!("expected main to call outer[int64]"); + int8 seed = 7 + secret[secret[list[int8]]] twice = list(seed) + secret[list[int8]] once = declassify clone twice + list[int8] public = declassify clone once + secret[list[int8]] from_call = make(seed) + secret[secret[list[int8]]] twice_from_call = make(seed) + return read(view public) +"#, + false, + ); + let make = &program.functions[0]; + assert!(matches!( + checked.interner.resolve(make.return_type), + Type::List(element) if *element == TypeInterner::INT8 + )); + let main = &program.functions[2]; + let binding = |name: &str| { + main.body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } + if main.locals[local.index() as usize].name == name => + { + Some((&main.locals[local.index() as usize], value)) + } + _ => None, + }) + .expect("source binding") }; - assert_eq!(*function, outer.id); + let (twice, constructed) = binding("twice"); + let (once, declassified) = binding("once"); + let (public, _) = binding("public"); + assert_eq!(checked.interner.resolve(twice.ty), &Type::Secret(once.ty)); + assert_eq!(checked.interner.resolve(once.ty), &Type::Secret(public.ty)); + assert_eq!(public.ty, make.return_type); + let ExpressionKind::InterfaceCoerce { value: literal, .. } = &constructed.kind else { + panic!("expected qualification of the exact checked literal"); + }; + assert_eq!(constructed.ty, twice.ty); + assert_eq!(literal.ty, make.return_type); + assert_eq!(checked.type_map[&literal.span], literal.ty); + assert!(matches!(literal.kind, ExpressionKind::ListConstruct { .. })); + let ExpressionKind::Declassify(cloned) = &declassified.kind else { + panic!("expected one-layer declassification"); + }; + let ExpressionKind::Clone(source) = &cloned.kind else { + panic!("expected explicit clone of the qualified local"); + }; + assert_eq!(source.kind, ExpressionKind::Local(twice.id)); + assert_eq!(source.ty, twice.ty); + assert_eq!(declassified.ty, once.ty); + + for name in ["from_call", "twice_from_call"] { + let (local, initializer) = binding(name); + assert_eq!(local.ty, initializer.ty); + let producer = match &initializer.kind { + ExpressionKind::InterfaceCoerce { value, .. } => value.as_ref(), + _ => initializer, + }; + let ExpressionKind::Call { function, .. } = producer.kind else { + panic!("expected original checked public producer"); + }; + assert_eq!(function, make.id); + assert_eq!(checked.type_map[&producer.span], producer.ty); + if name == "twice_from_call" { + assert_eq!(local.ty, twice.ty); + assert_eq!(producer.ty, make.return_type); + } else { + assert_eq!(local.ty, once.ty); + } + } let StatementKind::Return(Some(Expression { kind: ExpressionKind::Call { function, .. }, + ty, .. - })) = &outer.body.statements[0].kind + })) = &main.body.statements.last().expect("return").kind else { - panic!("expected outer[int64] to call inner[int64]"); + panic!("expected ordinary public reader call"); }; - assert_eq!(*function, inner.id); + assert_eq!(*function, program.functions[1].id); + assert_eq!(*ty, TypeInterner::INT64); } #[test] - fn keeps_inferred_instantiation_type_facts_separate() { - let source = r#"namespace app -function identity[T](value: T) returns T: - return value -function main() returns nothing: - int64 number = identity(1) - string text = identity("jett") -"#; - let program = lower_source(source); - assert_eq!(program.functions.len(), 3); - let main = &program.functions[0]; - let integer_identity = &program.functions[1]; - let string_identity = &program.functions[2]; - - assert_ne!( - integer_identity.identity.type_arguments, - string_identity.identity.type_arguments + fn secret_constructor_payloads_keep_nested_qualification_and_nominal_types() { + let (program, checked) = lower_source_with_check( + r#"type Count = int64 where true +function nested(value: Count) returns secret[list[secret[optional[Count]]]]: + return list(some(value)) +function pending() returns secret[list[int64]]: + return run (list(7)) +function fixed() returns secret[list[int64]]: + return comptime (list(9)) +"#, + false, ); - assert_eq!(integer_identity.params[0].ty, integer_identity.return_type); - assert_eq!(string_identity.params[0].ty, string_identity.return_type); - - let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { - panic!("expected first inferred generic call"); + let nested = &program.functions[0]; + let StatementKind::Return(Some(outer)) = &nested.body.statements[0].kind else { + panic!("expected nested constructor return"); }; - let ExpressionKind::Call { function, .. } = value.kind else { - panic!("expected first inferred generic call target"); + let ExpressionKind::InterfaceCoerce { value: list, .. } = &outer.kind else { + panic!("expected secret list qualification"); }; - assert_eq!(function, integer_identity.id); - - let StatementKind::Let { value, .. } = &main.body.statements[1].kind else { - panic!("expected second inferred generic call"); + let ExpressionKind::ListConstruct { elements } = &list.kind else { + panic!("expected list constructor"); }; - let ExpressionKind::Call { function, .. } = value.kind else { - panic!("expected second inferred generic call target"); + let ExpressionKind::InterfaceCoerce { + value: optional, .. + } = &elements[0].kind + else { + panic!("expected independently qualified optional payload"); }; - assert_eq!(function, string_identity.id); + let ExpressionKind::OptionalSome(payload) = &optional.kind else { + panic!("expected optional constructor"); + }; + assert_eq!(payload.ty, nested.params[0].ty); + assert!(matches!( + checked.interner.resolve(payload.ty), + Type::Refinement { .. } + )); + assert!(matches!(payload.kind, ExpressionKind::Local(_))); + + for function in &program.functions[1..3] { + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("expected wrapped constructor return"); + }; + let constructor = match &value.kind { + ExpressionKind::Run(inner) | ExpressionKind::Comptime { value: inner, .. } => inner, + _ => panic!("normalization must retain the source execution wrapper"), + }; + assert_eq!(constructor.ty, value.ty); + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &constructor.kind else { + panic!("expected normalized parenthesized literal"); + }; + assert!(matches!(inner.kind, ExpressionKind::ListConstruct { .. })); + assert_eq!(value.ty, function.return_type); + } } #[test] - fn lowers_recursive_generic_calls_to_the_reserved_identity() { - let source = r#"namespace app -function repeat[T](value: T, remaining: int64) returns T: - if remaining == 0: - return value - return repeat[T](value, remaining - 1) -function main() returns int64: - return repeat[int64](7, 2) -"#; - let program = lower_source(source); - assert_eq!(program.functions.len(), 2); - let repeat = &program.functions[1]; - let StatementKind::Return(Some(Expression { - kind: ExpressionKind::Call { function, .. }, - .. - })) = &repeat.body.statements[1].kind - else { - panic!("expected recursive concrete call"); - }; - assert_eq!(*function, repeat.id); + fn secret_constructor_normalization_enables_existing_payload_conversions() { + let (program, checked) = lower_source_with_check( + r#"interface Named: + function name(view self: Named) returns string +struct Item: + label: string +implement Named for Item: + function name(view self: Item) returns string: + return self.label +type Callback = function(int64) returns secret[int64] +function classified(value: secret[int64]) returns secret[int64]: + return value +function names(item: Item) returns secret[list[Named]]: + return list(item) +function callbacks() returns secret[list[Callback]]: + return list(classified) +"#, + false, + ); + for name in ["names", "callbacks"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("source function"); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::InterfaceCoerce { value: list, .. }, + .. + })) = &function.body.statements[0].kind + else { + panic!("expected secret qualification"); + }; + let ExpressionKind::ListConstruct { elements } = &list.kind else { + panic!("expected list constructor"); + }; + let Type::List(expected_element) = checked.interner.resolve(list.ty) else { + panic!("expected exact inner list type"); + }; + assert_eq!(elements[0].ty, *expected_element); + if name == "names" { + let ExpressionKind::InterfaceCoerce { value: source, .. } = &elements[0].kind + else { + panic!("expected nominal-to-interface payload conversion"); + }; + assert_eq!(source.ty, function.params[0].ty); + assert!(matches!(source.kind, ExpressionKind::Local(_))); + } else { + let ExpressionKind::FunctionAdapter { + value: source, + function: adapter, + } = &elements[0].kind + else { + panic!("expected existing checked function adapter"); + }; + assert!(matches!(source.kind, ExpressionKind::FunctionRef(_))); + assert_ne!(source.ty, elements[0].ty); + assert_eq!(program.functions[adapter.index() as usize].capture_count, 1); + } + } } #[test] - fn lowers_struct_construction_fields_and_named_calls_to_canonical_order() { - let source = r#"namespace app -struct Point: - x: int64 - y: int64 -function difference(left: int64, right: int64) returns int64: - return left - right -function main() returns int64: - Point point = Point(y: 2, x: 7) - return difference(right: point.y, left: point.x) -"#; - let program = lower_source(source); - let difference = &program.functions[0]; - let main = &program.functions[1]; - - let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { - panic!("expected point construction"); + fn secret_constructor_normalization_is_exact_and_does_not_peel_refinements() { + let mut types = TypeInterner::new(); + let list = types.intern(Type::List(TypeInterner::INT64)); + let optional = types.intern(Type::Optional(TypeInterner::INT64)); + let refined = types.intern(Type::Refinement { + name: "Numbers".into(), + base: list, + }); + let secret_list = types.intern(Type::Secret(list)); + let twice_secret = types.intern(Type::Secret(secret_list)); + let secret_optional = types.intern(Type::Secret(optional)); + let secret_refined = types.intern(Type::Secret(refined)); + let span = Span::new(FileId::new(0), 3, 9); + for ty in [list, secret_optional, secret_refined] { + let original = Expression { + kind: ExpressionKind::ListConstruct { elements: vec![] }, + ty, + span, + }; + assert_eq!( + normalize_secret_constructor(original.clone(), &types), + original + ); + } + for kind in [ + ExpressionKind::Local(LocalId::new(0)), + ExpressionKind::Call { + ownership: CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &types, + ) + .unwrap(), + function: FunctionId::new(0), + args: vec![], + evaluation_order: vec![], + }, + ] { + let original = Expression { + kind, + ty: secret_list, + span, + }; + assert_eq!( + normalize_secret_constructor(original.clone(), &types), + original + ); + } + let original = Expression { + kind: ExpressionKind::ListConstruct { elements: vec![] }, + ty: twice_secret, + span, }; - let ExpressionKind::StructConstruct { fields, .. } = &value.kind else { - panic!("expected canonical struct construction"); + let normalized = normalize_secret_constructor(original, &types); + assert_eq!(normalized.ty, twice_secret); + let ExpressionKind::InterfaceCoerce { value, .. } = &normalized.kind else { + panic!("expected outer qualification"); }; - assert!(matches!(fields[0].kind, ExpressionKind::Int(7))); - assert!(matches!(fields[1].kind, ExpressionKind::Int(2))); + assert_eq!(value.ty, list); + assert_eq!(value.span, span); + assert_eq!( + normalize_secret_constructor(normalized.clone(), &types), + normalized + ); + } - let StatementKind::Return(Some(Expression { - kind: - ExpressionKind::Call { - function, - args, - evaluation_order, - }, - .. - })) = &main.body.statements[1].kind - else { - panic!("expected named function call"); + #[test] + fn contextual_secret_structs_preserve_checked_nominal_constructors() { + let source = r#"struct Item: + left: int64 + right: int64 +struct Box[T]: + value: T +function direct() returns secret[Item]: + return Item(right: 2, left: 1) +function nested() returns secret[secret[Item]]: + return (Item(left: 3, right: 4)) +function pending() returns secret[Item]: + return run (Item(left: 5, right: 6)) +function generic[T](value: T) returns secret[Box[T]]: + return Box[T](value: value) +function public_item() returns Item: + return Item(left: 7, right: 8) +function producer() returns secret[Item]: + return public_item() +function main() returns nothing: + secret[Item] local = Item(left: 9, right: 10) + secret[Box[int64]] explicit = Box[int64](value: 11) + secret[Box[int64]] specialized = generic[int64](12) + return nothing +"#; + let (program, checked) = lower_source_with_check(source, false); + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty()); + let mut source_calls = HashMap::new(); + for item in &parsed.module.items { + let Item::Function(function) = item else { + continue; + }; + for statement in &function.body.stmts { + let (name, mut value) = match statement { + Stmt::Return(ast::ReturnStmt { + value: Some(value), .. + }) => (&function.name.name, value), + Stmt::VarDecl(declaration) => (&declaration.name.name, &declaration.value), + _ => continue, + }; + while let Expr::Paren(inner, _) | Expr::Run(inner, _) = value { + value = inner; + } + if matches!(value, Expr::Call(..) | Expr::GenericCall(..)) { + source_calls.insert(name.as_str(), value.span()); + } + } + } + let check_constructor = |name: &str, value: &Expression| { + let mut nominal = value.ty; + let mut depth = 0; + while let Type::Secret(inner) = checked.interner.resolve(nominal) { + nominal = *inner; + depth += 1; + } + assert!(depth > 0); + let ExpressionKind::InterfaceCoerce { + value: constructor, + adapters, + } = &value.kind + else { + panic!("expected secret qualification around the constructor"); + }; + assert!(adapters.is_empty()); + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + } = &constructor.kind + else { + panic!("expected exact nominal struct constructor"); + }; + assert_eq!(constructor.ty, nominal); + assert_eq!(*struct_type, nominal); + assert!(matches!(checked.interner.resolve(nominal), Type::Struct(_))); + assert!(!validates_refinements); + assert!(refinement_predicates.is_empty()); + assert_eq!(fields.len(), evaluation_order.len()); + // Parentheses may widen the HIR constructor span before the return + // coercion adds qualification. Facts retain the original call span. + let source_span = source_calls[name]; + assert_eq!(constructor.span.file, source_span.file, "{name}"); + assert!( + constructor.span.start <= source_span.start + && constructor.span.end >= source_span.end, + "{name}: HIR span {:?} does not contain source call {source_span:?}", + constructor.span, + ); + let fact = checked + .struct_constructions + .get(&source_span) + .or_else(|| { + checked + .generic_function_instantiations + .iter() + .find_map(|body| body.struct_constructions.get(&source_span)) + }) + .unwrap_or_else(|| panic!( + "{name}: source call {source_span:?} must have a checked construction fact (HIR span {:?})", + constructor.span, + )); + assert_eq!(fact.struct_type, nominal); + depth }; - assert_eq!(*function, difference.id); - assert_eq!(evaluation_order, &[1, 0]); - let ExpressionKind::Field { field, .. } = args[0].kind else { - panic!("expected left field argument"); + for name in ["direct", "nested", "pending", "generic"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("source or specialized function"); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("expected constructor return"); + }; + assert_eq!(value.ty, function.return_type); + let constructor = if name == "pending" { + let ExpressionKind::Run(inner) = &value.kind else { + panic!("pending constructor must retain its source run wrapper"); + }; + assert_eq!(inner.ty, value.ty); + inner.as_ref() + } else { + value + }; + assert_eq!( + check_constructor(name, constructor), + if name == "nested" { 2 } else { 1 } + ); + if name == "direct" { + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &value.kind else { + unreachable!(); + }; + let ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } = &inner.kind + else { + unreachable!(); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!(fields[0].kind, ExpressionKind::Int(1))); + assert!(matches!(fields[1].kind, ExpressionKind::Int(2))); + assert_eq!(inner.span, value.span); + } + } + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + for statement in &main.body.statements[..2] { + let StatementKind::Let { local, value } = &statement.kind else { + panic!("expected qualified constructor local"); + }; + assert_eq!(value.ty, main.locals[local.index() as usize].ty); + assert_eq!( + check_constructor(&main.locals[local.index() as usize].name, value), + 1 + ); + } + let producer = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "producer") + .unwrap(); + let StatementKind::Return(Some(value)) = &producer.body.statements[0].kind else { + panic!("expected producer call"); }; - assert_eq!(field.index(), 0); - let ExpressionKind::Field { field, .. } = args[1].kind else { - panic!("expected right field argument"); + let original = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } => value.as_ref(), + _ => value, }; - assert_eq!(field.index(), 1); + assert!(matches!(original.kind, ExpressionKind::Call { .. })); + assert_eq!(original.ty, checked.type_map[&original.span]); + assert!(!checked.struct_constructions.contains_key(&original.span)); } #[test] - fn lowers_interface_calls_to_the_concrete_method_body() { - let source = r#"namespace app -interface Scored: - function score(view self: Scored, bonus: int64) returns int64 -struct User: - points: int64 -implement Scored for User: - function score(view self: User, bonus: int64) returns int64: - return self.points + bonus -function main() returns int64: - User user = User(points: 7) - return Scored.score(bonus: 5, self: view user) -"#; - let program = lower_source(source); - assert_eq!(program.functions.len(), 2); - let main = &program.functions[0]; - let method = &program.functions[1]; - assert_eq!(method.identity.declaration.kind, DeclarationKind::Method); - assert_eq!( - method.identity.declaration.name, - "app.User as app.Scored.score" + fn secret_struct_payload_conversions_preserve_source_evaluation_order() { + let (program, checked) = lower_source_with_check( + r#"interface Named: + function name(view self: Named) returns string +struct Item: + label: string +implement Named for Item: + function name(view self: Item) returns string: + return self.label +type Count = int64 where true +type Callback = function(int64) returns secret[int64] +struct Bundle: + owner: Named + callback: Callback + count: list[Count] + fallback: int64 +struct Validated: + count: Count +function classified(value: secret[int64]) returns secret[int64]: + return value +function bundle(item: Item, count: Count, maybe: optional[int64]) returns secret[Bundle]: + return Bundle(fallback: maybe handle: + default 9 + , count: list(count), callback: classified, owner: item) +function validated(count: Count) returns result[Validated, string]: + return Validated(count: count) +"#, + false, ); - + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "bundle") + .unwrap(); let StatementKind::Return(Some(Expression { kind: - ExpressionKind::Call { - function, - args, - evaluation_order, + ExpressionKind::InterfaceCoerce { + value: constructor, .. }, + ty, .. - })) = &main.body.statements[1].kind + })) = &function.body.statements[0].kind else { - panic!("expected concrete method call"); + panic!("expected qualified bundle construction"); }; - assert_eq!(*function, method.id); - assert_eq!(evaluation_order, &[1, 0]); - assert!(matches!(args[0].kind, ExpressionKind::View(_))); - assert!(matches!(args[1].kind, ExpressionKind::Int(5))); + assert_eq!(*ty, function.return_type); + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + } = &constructor.kind + else { + panic!("expected exact bundle constructor"); + }; + assert_eq!(constructor.ty, *struct_type); + assert_eq!(evaluation_order, &[3, 2, 1, 0]); + assert!(!validates_refinements); + assert!(refinement_predicates.is_empty()); + let Type::Struct(id) = checked.interner.resolve(*struct_type) else { + unreachable!(); + }; + let declared = &checked.interner.resolve_struct(*id).fields; + assert!( + fields + .iter() + .zip(declared) + .all(|(field, (_, ty))| field.ty == *ty) + ); + let ExpressionKind::InterfaceCoerce { value: owner, .. } = &fields[0].kind else { + panic!("expected the existing nominal-to-interface field conversion"); + }; + assert_eq!(owner.ty, function.params[0].ty); + assert!(matches!(owner.kind, ExpressionKind::Local(_))); + let ExpressionKind::FunctionAdapter { + value: callback, + function: adapter, + } = &fields[1].kind + else { + panic!("expected the existing callback field adapter"); + }; + assert!(matches!(callback.kind, ExpressionKind::FunctionRef(_))); + assert_eq!(program.functions[adapter.index() as usize].capture_count, 1); + let ExpressionKind::ListConstruct { elements } = &fields[2].kind else { + panic!("expected an independently checked nominal payload container"); + }; + assert_eq!(elements[0].ty, function.params[1].ty); + assert!(matches!( + checked.interner.resolve(elements[0].ty), + Type::Refinement { .. } + )); + assert!(matches!(elements[0].kind, ExpressionKind::Local(_))); + assert!(matches!(fields[3].kind, ExpressionKind::Handle { .. })); + + let validated = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "validated") + .unwrap(); + let StatementKind::Return(Some(value)) = &validated.body.statements[0].kind else { + panic!("expected validating constructor return"); + }; + let ExpressionKind::StructConstruct { + struct_type, + validates_refinements, + refinement_predicates, + .. + } = &value.kind + else { + panic!("a refinement-field constructor must retain its result boundary"); + }; + assert!(*validates_refinements); + assert_eq!(refinement_predicates, &[Vec::new()]); + assert_eq!( + checked.interner.resolve(value.ty), + &Type::Result(*struct_type, TypeInterner::STRING) + ); + assert_eq!(value.ty, validated.return_type); } #[test] - fn keeps_named_argument_orders_inside_generic_instantiations() { - let source = r#"namespace app -function inner[T](first: T, second: T) returns T: - return first -function outer[T](first: T, second: T) returns T: - return inner[T](second: second, first: first) -function main() returns int64: - return outer[int64](second: 2, first: 7) + fn secret_validating_structs_preserve_result_types_predicates_and_source_order() { + let source = r#"type Positive = int8 where value > 0 +struct Item: + value: Positive + label: string +struct Box[T]: + value: Positive + payload: T +function direct(input: int8) returns secret[result[Item, string]]: + return Item(label: "direct", value: input) +function exact(input: Positive) returns secret[result[Item, string]]: + return Item(label: "exact", value: input) +function nested(input: int8) returns secret[secret[result[Item, string]]]: + return ((Item)(label: "nested", value: input)) +function pending(input: int8) returns secret[result[Item, string]]: + return run (Item(label: "pending", value: input)) +function handled(input: optional[int8], fallback: int8) returns secret[result[Item, string]]: + return Item(label: "handled", value: input handle: + default fallback + ) +function generic[T](input: int8, payload: T) returns secret[result[Box[T], string]]: + return Box[T](payload: payload, value: input) +function ordinary(input: int8) returns result[Item, string]: + return Item(label: "ordinary", value: input) +function producer(input: int8) returns secret[result[Item, string]]: + return ordinary(input) +function main() returns nothing: + int8 seed = 7 + secret[result[Item, string]] local = Item(label: "local", value: seed) + secret[result[Box[int64], string]] specialized = generic[int64](seed, 11) + return nothing "#; - let program = lower_source(source); - let main = &program.functions[0]; - let outer = &program.functions[1]; - - for function in [main, outer] { - let StatementKind::Return(Some(Expression { - kind: - ExpressionKind::Call { - args, - evaluation_order, - .. - }, - .. - })) = &function.body.statements[0].kind + let (program, checked) = lower_source_with_check(source, false); + let parsed = jett_parser::parse(source, FileId::new(0)); + let source_calls = parsed + .module + .items + .iter() + .filter_map(|item| { + let Item::Function(function) = item else { + return None; + }; + let Stmt::Return(ast::ReturnStmt { + value: Some(mut_value), + .. + }) = function.body.stmts.first()? + else { + return None; + }; + let mut value = mut_value; + while let Expr::Run(inner, _) | Expr::Paren(inner, _) = value { + value = inner; + } + Some((function.name.name.as_str(), value.span())) + }) + .collect::>(); + for name in ["direct", "exact", "nested", "pending", "handled", "generic"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("{name}: constructor return"); + }; + assert_eq!(value.ty, function.return_type, "{name}"); + let value = if name == "pending" { + let ExpressionKind::Run(inner) = &value.kind else { + panic!("pending constructor keeps its source run"); + }; + assert_eq!(inner.ty, value.ty); + inner.as_ref() + } else { + value + }; + let mut result_type = value.ty; + let mut depth = 0; + while let Type::Secret(inner) = checked.interner.resolve(result_type) { + result_type = *inner; + depth += 1; + } + assert_eq!(depth, if name == "nested" { 2 } else { 1 }, "{name}"); + let ExpressionKind::InterfaceCoerce { + value: constructor, + adapters, + } = &value.kind else { - panic!("expected named generic call"); + panic!("{name}: exact result qualification"); + }; + assert!(adapters.is_empty()); + assert_eq!(constructor.ty, result_type); + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + } = &constructor.kind + else { + panic!("{name}: validating constructor"); }; + assert!(*validates_refinements); + assert_eq!( + checked.interner.resolve(result_type), + &Type::Result(*struct_type, TypeInterner::STRING) + ); assert_eq!(evaluation_order, &[1, 0]); - assert!(matches!( - args[0].kind, - ExpressionKind::Int(7) | ExpressionKind::Local(_) - )); + assert_eq!(fields.len(), 2); + assert_eq!(refinement_predicates.len(), fields.len()); + assert!(refinement_predicates[1].is_empty()); + let Type::Struct(id) = checked.interner.resolve(*struct_type) else { + panic!("exact nominal struct"); + }; + let definition = checked.interner.resolve_struct(*id); + assert_eq!(fields[1].ty, definition.fields[1].1); + if name == "exact" { + assert!(refinement_predicates[0].is_empty()); + assert_eq!(fields[0].ty, definition.fields[0].1); + } else { + let [predicate] = refinement_predicates[0].as_slice() else { + panic!("{name}: preserved refinement predicate"); + }; + assert_eq!(predicate.refined_type, definition.fields[0].1); + assert_eq!(fields[0].ty, TypeInterner::INT8); + assert_eq!( + program.functions[predicate.function.index() as usize].return_type, + TypeInterner::BOOL + ); + } + if name == "handled" { + assert!(matches!(fields[0].kind, ExpressionKind::Handle { .. })); + } + let source_span = source_calls[name]; + assert_eq!(constructor.span.file, source_span.file); + assert!( + constructor.span.start <= source_span.start + && constructor.span.end >= source_span.end + ); + let fact = checked + .struct_constructions + .get(&source_span) + .or_else(|| { + checked + .generic_function_instantiations + .iter() + .find_map(|body| body.struct_constructions.get(&source_span)) + }) + .expect("checked source construction fact"); + assert_eq!(fact.struct_type, *struct_type); + assert!(fact.validates_refinements); } + let ordinary = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "ordinary") + .unwrap(); + let StatementKind::Return(Some(value)) = &ordinary.body.statements[0].kind else { + panic!("ordinary constructor return"); + }; + assert!(matches!( + value.kind, + ExpressionKind::StructConstruct { + validates_refinements: true, + .. + } + )); + assert_eq!(value.ty, checked.type_map[&value.span]); + let producer = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "producer") + .unwrap(); + let StatementKind::Return(Some(value)) = &producer.body.statements[0].kind else { + panic!("producer return"); + }; + let call = match &value.kind { + ExpressionKind::InterfaceCoerce { value, .. } => value.as_ref(), + _ => value, + }; + let ExpressionKind::Call { function, .. } = &call.kind else { + panic!("ordinary producer must remain a call"); + }; + let target = &program.functions[function.index() as usize]; + assert_eq!(target.identity, ordinary.identity); + assert_eq!(target.return_type, ordinary.return_type); + assert!(matches!( + checked.interner.resolve(target.return_type), + Type::Result(_, TypeInterner::STRING) + )); + assert_eq!(call.span, source_calls["producer"]); + assert_eq!(call.ty, checked.type_map[&call.span]); + assert!(!checked.struct_constructions.contains_key(&call.span)); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let StatementKind::Let { local, value } = &main.body.statements[1].kind else { + panic!("qualified constructor local"); + }; + assert_eq!(value.ty, main.locals[local.index() as usize].ty); + let ExpressionKind::InterfaceCoerce { + value: constructor, .. + } = &value.kind + else { + panic!("local result qualification"); + }; + assert_eq!(constructor.span, value.span); + assert!(matches!( + constructor.kind, + ExpressionKind::StructConstruct { + validates_refinements: true, + .. + } + )); } #[test] - fn lowers_list_and_map_construction_in_lexical_order() { - let source = r#"namespace app -function main() returns map[string, list[int64]]: - return map("odds": list(1, 3), "evens": list(2, 4)) -"#; - let program = lower_source(source); - let main = &program.functions[0]; - let StatementKind::Return(Some(Expression { - kind: ExpressionKind::MapConstruct { entries }, - .. - })) = &main.body.statements[0].kind + fn secret_struct_normalization_keeps_nominal_and_validation_boundaries_exact() { + let mut types = TypeInterner::new(); + let first_id = types.add_struct(jett_types::StructDef { + name: "First".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: vec![], + }); + let first = types.intern(Type::Struct(first_id)); + let second_id = types.add_struct(jett_types::StructDef { + name: "Second".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: vec![], + }); + let second = types.intern(Type::Struct(second_id)); + let refined = types.intern(Type::Refinement { + name: "Selected".into(), + base: first, + }); + let result = types.intern(Type::Result(first, TypeInterner::STRING)); + let wrong_ok = types.intern(Type::Result(second, TypeInterner::STRING)); + let wrong_error = types.intern(Type::Result(first, TypeInterner::INT64)); + let refined_ok = types.intern(Type::Result(refined, TypeInterner::STRING)); + let refined_result = types.intern(Type::Refinement { + name: "CheckedResult".into(), + base: result, + }); + let secret_first = types.intern(Type::Secret(first)); + let secret_second = types.intern(Type::Secret(second)); + let secret_refined = types.intern(Type::Secret(refined)); + let secret_result = types.intern(Type::Secret(result)); + let secret_wrong_ok = types.intern(Type::Secret(wrong_ok)); + let secret_wrong_error = types.intern(Type::Secret(wrong_error)); + let secret_refined_ok = types.intern(Type::Secret(refined_ok)); + let secret_refined_result = types.intern(Type::Secret(refined_result)); + let span = Span::new(FileId::new(0), 3, 9); + let make = |ty, validates_refinements| Expression { + kind: ExpressionKind::StructConstruct { + struct_type: first, + fields: vec![Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }], + evaluation_order: vec![0], + validates_refinements, + refinement_predicates: if validates_refinements { + vec![vec![]] + } else { + vec![] + }, + }, + ty, + span, + }; + for (ty, validating) in [ + (first, false), + (secret_second, false), + (secret_refined, false), + (result, true), + (secret_result, false), + (secret_wrong_ok, true), + (secret_wrong_error, true), + (secret_refined_ok, true), + (secret_refined_result, true), + (secret_first, true), + ] { + let original = make(ty, validating); + assert_eq!( + normalize_secret_constructor(original.clone(), &types), + original + ); + } + for (qualified, validating, output) in + [(secret_first, false, first), (secret_result, true, result)] + { + let nested = types.intern(Type::Secret(qualified)); + for ty in [qualified, nested] { + let original = make(ty, validating); + let normalized = normalize_secret_constructor(original.clone(), &types); + assert_eq!(normalized.ty, ty); + assert_eq!(normalized.span, span); + let ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + } = &normalized.kind + else { + panic!("expected exact struct qualification"); + }; + assert_eq!(inner.ty, output); + assert_eq!(inner.span, span); + assert_eq!(inner.kind, original.kind); + assert!(adapters.is_empty()); + assert_eq!( + normalize_secret_constructor(normalized.clone(), &types), + normalized + ); + } + } + // Even a matching result success TypeId must denote the exact struct, + // rather than a refinement whose representation happens to be one. + let mut refined_constructor = make(secret_refined_ok, true); + let ExpressionKind::StructConstruct { struct_type, .. } = &mut refined_constructor.kind else { - panic!("expected map construction"); + unreachable!(); }; - assert_eq!(entries.len(), 2); - assert!( - matches!(entries[0].key.kind, ExpressionKind::String(ref value) if value == "odds") + *struct_type = refined; + assert_eq!( + normalize_secret_constructor(refined_constructor.clone(), &types), + refined_constructor ); - let ExpressionKind::ListConstruct { elements } = &entries[0].value.kind else { - panic!("expected nested list construction"); - }; - assert!(matches!(elements[0].kind, ExpressionKind::Int(1))); - assert!(matches!(elements[1].kind, ExpressionKind::Int(3))); } #[test] @@ -3119,47 +10563,356 @@ function main() returns int64: } #[test] - fn lowers_enum_construction_and_exhaustive_match() { - let source = r#"namespace app -enum Shape: - point - circle(radius: int64) - rect(width: int64, height: int64) -function area(shape: Shape) returns int64: - match shape: - point: - return 0 - circle(radius): - return radius * radius - rect(width, height): - return width * height -function main() returns int64: - Shape shape = Shape.circle(3) - return area(shape) -"#; - let program = lower_source(source); - let area = &program.functions[0]; - let main = &program.functions[1]; - let StatementKind::Match { arms, .. } = &area.body.statements[0].kind else { - panic!("expected lowered match"); - }; - assert_eq!(arms.len(), 3); - assert_eq!(arms[0].variant.expect("point variant").index(), 0); - assert_eq!(arms[1].variant.expect("circle variant").index(), 1); - assert_eq!(arms[1].bindings.len(), 1); - assert_eq!(arms[2].bindings.len(), 2); - - let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { - panic!("expected enum local"); + fn lowers_enum_construction_and_exhaustive_match() { + let source = r#"namespace app +enum Shape: + point + circle(radius: int64) + rect(width: int64, height: int64) +function area(shape: Shape) returns int64: + match shape: + point: + return 0 + circle(radius): + return radius * radius + rect(width, height): + return width * height +function main() returns int64: + Shape shape = Shape.circle(3) + return area(shape) +"#; + let program = lower_source(source); + let area = &program.functions[0]; + let main = &program.functions[1]; + let StatementKind::Match { arms, .. } = &area.body.statements[0].kind else { + panic!("expected lowered match"); + }; + assert_eq!(arms.len(), 3); + assert_eq!(arms[0].variant.expect("point variant").index(), 0); + assert_eq!(arms[1].variant.expect("circle variant").index(), 1); + assert_eq!(arms[1].bindings.len(), 1); + assert_eq!(arms[2].bindings.len(), 2); + + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected enum local"); + }; + let ExpressionKind::EnumConstruct { + variant, payloads, .. + } = &value.kind + else { + panic!("expected enum construction"); + }; + assert_eq!(variant.index(), 1); + assert!(matches!(payloads[0].kind, ExpressionKind::Int(3))); + } + + #[test] + fn lowers_secret_enum_constructors_with_nominal_targets() { + let program = lower_source( + "namespace app\nenum Choice:\n empty\n number(value: int8)\nfunction main() returns nothing:\n secret[Choice] empty = Choice.empty\n secret[Choice] number = Choice.number(-128)\n return nothing\n", + ); + let main = &program.functions[0]; + let mut target = None; + for (index, statement) in main.body.statements[..2].iter().enumerate() { + let StatementKind::Let { value, .. } = &statement.kind else { + panic!("expected secret enum local"); + }; + let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + .. + } = &value.kind + else { + panic!("expected nominal enum construction"); + }; + assert_ne!(value.ty, *enum_type, "secrecy belongs to the expression"); + assert_eq!(*target.get_or_insert(*enum_type), *enum_type); + assert_eq!(variant.index(), index as u32); + assert_eq!(payloads.len(), index); + } + } + + fn return_refinement_guard( + function: &Function, + ) -> (&Expression, &[RefinementPredicate], LocalId, &Block) { + let StatementKind::Return(Some(value)) = &function.body.statements.last().unwrap().kind + else { + panic!( + "expected source return in {}", + function.identity.declaration.name + ); + }; + let ExpressionKind::Handle { + target, + kind: + HandleKind::Refinement { + refined_type, + predicates, + }, + error_local: Some(error), + failure, + } = &value.kind + else { + panic!("expected generated return refinement guard: {value:?}"); + }; + assert_eq!(*refined_type, function.return_type); + assert_eq!(value.ty, function.return_type); + let StatementKind::Expression(Expression { + kind: ExpressionKind::RuntimeFailureMessage(message), + ty, + .. + }) = &failure.statements[0].kind + else { + panic!("expected exact dynamic predicate rejection"); + }; + assert_eq!(*ty, TypeInterner::NOTHING); + assert_eq!(message.ty, TypeInterner::STRING); + assert_eq!(message.kind, ExpressionKind::Local(*error)); + let metadata = &function.locals[error.index() as usize]; + assert_eq!(metadata.ty, TypeInterner::STRING); + assert!(metadata.view_source.is_none()); + (target, predicates, *error, failure) + } + + #[test] + fn native_return_refinement_checks_only_missing_predicate_suffixes() { + let (program, checked) = lower_source_with_check( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +function from_base(raw: int64) returns Higher: + return raw +function from_ancestor(ready: Positive) returns Higher: + return ready +function exact(ready: Higher) returns Higher: + return ready +function pending(raw: int64) returns Positive: + return run raw +"#, + false, + ); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + for (name, names) in [ + ("from_base", vec!["app.Positive", "app.Higher"]), + ("from_ancestor", vec!["app.Higher"]), + ("pending", vec!["app.Positive"]), + ] { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let (source, predicates, _, _) = return_refinement_guard(function); + assert_eq!( + predicates + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + names + ); + let parameter = function.params[0].local; + if name == "pending" { + assert!(matches!(&source.kind, ExpressionKind::Run(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == parameter))); + } else { + assert_eq!(source.kind, ExpressionKind::Local(parameter)); + } + assert_eq!( + source.ty, function.params[0].ty, + "source signature must stay exact" + ); + } + let exact = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exact") + .unwrap(); + let StatementKind::Return(Some(value)) = &exact.body.statements[0].kind else { + panic!("exact return"); + }; + assert_eq!(value.kind, ExpressionKind::Local(exact.params[0].local)); + assert!( + !exact + .locals + .iter() + .any(|local| local.name == "$return.error") + ); + } + + #[test] + fn native_return_refinement_uses_concrete_generic_and_method_targets() { + let (program, checked) = lower_source_with_check( + r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + raw: int64 + function promoted(view self: Holder) returns Positive: + return self.raw +function generic[T](raw: int64, witness: T) returns T: + return raw +function main() returns nothing: + Positive seed = 7 handle error: return nothing + Positive found = generic[Positive](raw: 8, witness: seed) + int64 plain = generic[int64](raw: 9, witness: 0) + trace found + trace plain + return nothing +"#, + false, + ); + validate_backend_types(&program, &checked.interner).unwrap(); + let method = program + .functions + .iter() + .find(|f| f.debug_kind == FunctionDebugKind::Named("app.Holder.promoted".into())) + .unwrap(); + let (source, predicates, _, _) = return_refinement_guard(method); + assert!(matches!(source.kind, ExpressionKind::Field { .. })); + assert_eq!(predicates.len(), 1); + let generic = program + .functions + .iter() + .filter(|f| f.identity.declaration.name == "generic") + .collect::>(); + assert_eq!(generic.len(), 2); + for function in generic { + if function.return_type == TypeInterner::INT64 { + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("plain generic"); + }; + assert_eq!(value.kind, ExpressionKind::Local(function.params[0].local)); + assert!( + !function + .locals + .iter() + .any(|local| local.name == "$return.error") + ); + } else { + let (source, predicates, _, _) = return_refinement_guard(function); + assert_eq!(source.ty, TypeInterner::INT64); + assert_eq!(predicates.len(), 1); + assert_eq!( + predicates[0].refined_type, + function.identity.type_arguments[0] + ); + } + } + } + + #[test] + fn native_return_refinement_inline_targets_restore_enclosing_scope() { + let (program, checked) = lower_source_with_check( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +function outer(raw: int64) returns Higher: + function() returns int64 plain = function() returns int64: + return raw + function() returns Positive narrowed = function() returns Positive: + function() returns int64 nested = function() returns int64: + return raw + return raw + return raw +"#, + false, + ); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let outer = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "outer") + .unwrap(); + let (_, predicates, _, _) = return_refinement_guard(outer); + assert_eq!( + predicates + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.Positive", "app.Higher"] + ); + let callbacks = program + .functions + .iter() + .filter(|f| f.debug_kind == FunctionDebugKind::Inline) + .collect::>(); + assert_eq!(callbacks.len(), 3); + for callback in callbacks { + if callback.return_type == TypeInterner::INT64 { + let StatementKind::Return(Some(value)) = &callback.body.statements[0].kind else { + panic!("plain inline return"); + }; + assert!(matches!(value.kind, ExpressionKind::Local(_))); + } else { + let (_, predicates, _, _) = return_refinement_guard(callback); + assert_eq!( + predicates + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.Positive"] + ); + } + } + } + + #[test] + fn native_return_refinement_survives_reflected_body_fact_switches() { + let program = lower_source( + r#"namespace app +type Positive = int64 where value > 0 +struct Mixed: + label: string + count: int64 +function reflected[T](view model: T, raw: int64) returns Positive: + for field in type.fields[T](): + comptime type Field = field.type_info: + return raw + return raw +function main() returns nothing: + Mixed model = Mixed(label: "one", count: 1) + Positive found = reflected[Mixed](view model, 7) + trace found + return nothing +"#, + ); + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "reflected") + .unwrap(); + let StatementKind::For { body, .. } = &function.body.statements[0].kind else { + panic!("reflected loop"); }; - let ExpressionKind::EnumConstruct { - variant, payloads, .. - } = &value.kind - else { - panic!("expected enum construction"); + let StatementKind::ReflectedTypeDispatch { arms, .. } = &body.statements[0].kind else { + panic!("bound types"); }; - assert_eq!(variant.index(), 1); - assert!(matches!(payloads[0].kind, ExpressionKind::Int(3))); + assert_eq!(arms.len(), 2); + for arm in arms { + let StatementKind::Return(Some(value)) = &arm.body.statements[0].kind else { + panic!("bound return"); + }; + let ExpressionKind::Handle { + target, + kind: + HandleKind::Refinement { + refined_type, + predicates, + }, + .. + } = &value.kind + else { + panic!("bound return guard"); + }; + assert_eq!(*refined_type, function.return_type); + assert_eq!(target.ty, TypeInterner::INT64); + assert_eq!(target.kind, ExpressionKind::Local(function.params[1].local)); + assert_eq!(predicates[0].type_name, "app.Positive"); + } + let (_, predicates, _, _) = return_refinement_guard(function); + assert_eq!(predicates[0].type_name, "app.Positive"); } #[test] @@ -3177,7 +10930,11 @@ function positive_or_one(raw: int64, fallback: Positive) returns Positive: panic!("expected refinement local"); }; let ExpressionKind::Handle { - kind: HandleKind::Refinement { refined_type }, + kind: + HandleKind::Refinement { + refined_type, + ref predicates, + }, error_local: Some(error_local), .. } = value.kind @@ -3185,7 +10942,68 @@ function positive_or_one(raw: int64, fallback: Positive) returns Positive: panic!("expected refinement boundary handle"); }; assert_eq!(refined_type, value.ty); + assert_eq!(predicates.len(), 1); + assert_eq!(predicates[0].type_name, "app.Positive"); assert_eq!(function.locals[error_local.index() as usize].name, "error"); + assert_eq!( + program.functions[1].identity.declaration.kind, + DeclarationKind::RefinementPredicate + ); + } + + #[test] + fn lowers_whole_sum_refinements_before_sum_extraction() { + let program = lower_source( + r#"namespace app +type Choice = optional[int64] where true +type Outcome = result[int64, int64] where true +function check(maybe: optional[int64], outcome: result[int64, int64], wrapped: result[Outcome, int64], optional_wrapped: optional[Choice]) returns nothing: + Choice first = maybe handle error: return nothing + Outcome second = outcome handle error: return nothing + Outcome third = wrapped handle error: return nothing + Choice fourth = optional_wrapped handle: return nothing + return nothing +"#, + ); + let function = &program.functions[0]; + for (index, expected) in [ + TypeInterner::STRING, + TypeInterner::STRING, + TypeInterner::INT64, + ] + .into_iter() + .enumerate() + { + let StatementKind::Let { value, .. } = &function.body.statements[index].kind else { + panic!("expected a checked binding"); + }; + let ExpressionKind::Handle { + kind, + error_local: Some(error), + .. + } = &value.kind + else { + panic!("expected a checked handle"); + }; + assert_eq!(function.locals[error.index() as usize].ty, expected); + if index < 2 { + assert!( + matches!(kind, HandleKind::Refinement { predicates, .. } if predicates.len() == 1) + ); + } else { + assert!(matches!(kind, HandleKind::Result)); + } + } + let StatementKind::Let { value, .. } = &function.body.statements[3].kind else { + panic!("expected optional extraction"); + }; + assert!(matches!( + value.kind, + ExpressionKind::Handle { + kind: HandleKind::Optional, + .. + } + )); } #[test] @@ -3250,26 +11068,728 @@ function guest() returns Session at guest: } #[test] - fn lowers_checked_compiler_calls_as_intrinsics() { + fn secret_bitfield_constructors_preserve_plain_and_validating_outputs() { + let (program, checked) = lower_source_with_check( + r#"bitfield network Header: + first: 4 bits + last: 4 bits + payload: list[uint8] +type HeaderAlias = Header +function fixed(payload: list[uint8]) returns secret[HeaderAlias]: + return ((Header))(payload: payload, last: 15, first: 0) +function dynamic(width: int64, payload: list[uint8]) returns secret[result[Header, string]]: + return Header(payload: payload, last: width, first: 0) +function nested(width: int64, payload: list[uint8]) returns secret[secret[result[Header, string]]]: + return (Header(first: 0, last: width, payload: payload)) +function nested_plain(payload: list[uint8]) returns secret[secret[Header]]: + return (Header(first: 0, last: 1, payload: payload)) +function pending(payload: list[uint8]) returns secret[Header]: + return run (Header(first: 1, last: 2, payload: payload)) +function parenthesized_number(payload: list[uint8]) returns secret[result[Header, string]]: + return Header(first: 0, last: (7), payload: payload) +function handled(maybe: optional[list[uint8]]) returns secret[Header]: + return Header(first: 1, last: 2, payload: maybe handle: + default list() + ) +function generic[T](unused: T, payload: list[uint8]) returns secret[Header]: + return Header(first: 1, last: 2, payload: payload) +function ordinary(width: int64, payload: list[uint8]) returns result[Header, string]: + return Header(first: 0, last: width, payload: payload) +function producer(payload: list[uint8]) returns secret[Header]: + return fixed(payload) +function main() returns nothing: + secret[Header] local = Header(first: 1, last: 2, payload: list()) + secret[result[Header, string]] validation = Header(first: 1, last: (2), payload: list()) + secret[Header] specialized = generic[int64](3, list()) + return nothing +"#, + false, + ); + for (name, validating, depth, order) in [ + ("fixed", false, 1, vec![2, 1, 0]), + ("dynamic", true, 1, vec![2, 1, 0]), + ("nested", true, 2, vec![0, 1, 2]), + ("nested_plain", false, 2, vec![0, 1, 2]), + ("pending", false, 1, vec![0, 1, 2]), + ("parenthesized_number", true, 1, vec![0, 1, 2]), + ("handled", false, 1, vec![0, 1, 2]), + ("generic", false, 1, vec![0, 1, 2]), + ] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("{name}: expected returned constructor"); + }; + assert_eq!(value.ty, function.return_type, "{name}"); + let value = if name == "pending" { + let ExpressionKind::Run(inner) = &value.kind else { + panic!("pending construction must retain run"); + }; + assert_eq!(inner.ty, value.ty); + inner.as_ref() + } else { + value + }; + let mut expected = value.ty; + for _ in 0..depth { + let Type::Secret(inner) = checked.interner.resolve(expected) else { + panic!("{name}: missing checked secret layer"); + }; + expected = *inner; + } + let ExpressionKind::InterfaceCoerce { + value: constructor, + adapters, + } = &value.kind + else { + panic!("{name}: expected qualification boundary"); + }; + assert!(adapters.is_empty()); + assert_eq!(constructor.ty, expected, "{name}"); + let ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + } = &constructor.kind + else { + panic!("{name}: expected bitfield constructor"); + }; + assert_eq!(*validates_widths, validating, "{name}"); + assert_eq!(evaluation_order, &order, "{name}"); + let Type::Bitfield(id) = checked.interner.resolve(*bitfield_type) else { + panic!("exact nominal bitfield"); + }; + let definition = checked.interner.resolve_bitfield(*id); + assert!(definition.network_order); + assert!( + fields + .iter() + .zip(&definition.fields) + .all(|(value, field)| value.ty == field.ty) + ); + if validating { + assert_eq!( + checked.interner.resolve(constructor.ty), + &Type::Result(*bitfield_type, TypeInterner::STRING), + "{name}" + ); + } else { + assert_eq!(constructor.ty, *bitfield_type, "{name}"); + } + if name == "handled" { + assert!(matches!(fields[2].kind, ExpressionKind::Handle { .. })); + } + } + for name in ["ordinary", "producer"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("control return"); + }; + if name == "ordinary" { + assert!(matches!( + value.kind, + ExpressionKind::BitfieldConstruct { + validates_widths: true, + .. + } + )); + } else { + assert!(matches!(value.kind, ExpressionKind::Call { .. })); + } + assert_eq!(value.ty, checked.type_map[&value.span]); + } + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + for (statement, validating) in main.body.statements[..2].iter().zip([false, true]) { + let StatementKind::Let { local, value } = &statement.kind else { + panic!("constructor local"); + }; + assert_eq!(value.ty, main.locals[local.index() as usize].ty); + let ExpressionKind::InterfaceCoerce { value: inner, .. } = &value.kind else { + panic!("local qualification"); + }; + assert_eq!(inner.span, value.span); + assert!( + matches!(inner.kind, ExpressionKind::BitfieldConstruct { validates_widths, .. } if validates_widths == validating) + ); + } + } + + #[test] + fn secret_machine_constructors_preserve_state_payload_conversions_and_wrappers() { + let (program, checked) = lower_source_with_check( + r#"namespace models +export interface Named: + function name(view self: Named) returns string +export struct Item: + label: string +implement Named for Item: + function name(view self: Item) returns string: + return self.label +export type Callback = function(int64) returns secret[int64] +export machine Session: + states: + active(label: string, owner: Named, callback: Callback, count: int64) + idle + transitions: + active to idle +export bitfield Header: + value: 4 bits +namespace app +function classified(value: secret[int64]) returns secret[int64]: + return value +function active(item: models.Item, maybe: optional[int64]) returns secret[models.Session at active]: + use models + use models as m + return (m.Session)(active, "Ada", item, classified, maybe handle: + default 7 + ) +function nested() returns secret[secret[models.Session at idle]]: + use models + use models as m + return (m.Session(idle)) +function pending() returns secret[models.Session at idle]: + use models + use models as m + return run (m.Session(idle)) +function generic[T](unused: T) returns secret[models.Session at idle]: + use models + use models as m + return m.Session(idle) +function header() returns secret[models.Header]: + use models + use models as m + return (m.Header)(value: 7) +function main() returns nothing: + use models as m + use models + secret[models.Session at idle] local = m.Session(idle) + secret[models.Session at idle] specialized = generic[int64](3) + return nothing +"#, + false, + ); + for (name, depth) in [("active", 1), ("nested", 2), ("pending", 1), ("generic", 1)] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("{name}: state constructor return"); + }; + assert_eq!(value.ty, function.return_type); + let value = if name == "pending" { + let ExpressionKind::Run(inner) = &value.kind else { + panic!("preserved run"); + }; + assert_eq!(inner.ty, value.ty); + inner.as_ref() + } else { + value + }; + let mut state_type = value.ty; + for _ in 0..depth { + let Type::Secret(inner) = checked.interner.resolve(state_type) else { + panic!("retained secret layer"); + }; + state_type = *inner; + } + let ExpressionKind::InterfaceCoerce { + value: constructor, + adapters, + } = &value.kind + else { + panic!("{name}: state qualification"); + }; + assert!(adapters.is_empty()); + assert_eq!(constructor.ty, state_type); + let ExpressionKind::MachineConstruct { + state_type: actual, + state, + payloads, + } = &constructor.kind + else { + panic!("{name}: exact machine constructor"); + }; + assert_eq!(*actual, state_type); + let Type::MachineState { + machine, + state: expected, + } = checked.interner.resolve(state_type) + else { + panic!("exact nominal state"); + }; + assert_eq!(state.index(), expected.index()); + let definition = checked + .interner + .resolve_machine(*machine) + .state(*expected) + .unwrap(); + assert_eq!(payloads.len(), definition.fields.len()); + assert!( + payloads + .iter() + .zip(&definition.fields) + .all(|(value, (_, ty))| value.ty == *ty) + ); + if name == "active" { + assert_eq!(definition.name, "active"); + assert!( + matches!(payloads[0].kind, ExpressionKind::String(ref label) if label == "Ada") + ); + assert!(matches!( + payloads[1].kind, + ExpressionKind::InterfaceCoerce { .. } + )); + assert!(matches!( + payloads[2].kind, + ExpressionKind::FunctionAdapter { .. } + )); + assert!(matches!(payloads[3].kind, ExpressionKind::Handle { .. })); + } else { + assert_eq!(definition.name, "idle"); + assert!(payloads.is_empty()); + } + } + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let StatementKind::Let { local, value } = &main.body.statements[0].kind else { + panic!("qualified local state"); + }; + assert_eq!(value.ty, main.locals[local.index() as usize].ty); + let ExpressionKind::InterfaceCoerce { + value: constructor, .. + } = &value.kind + else { + panic!("local state qualification"); + }; + assert!(matches!( + constructor.kind, + ExpressionKind::MachineConstruct { .. } + )); + let header = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "header") + .unwrap(); + assert!( + matches!(&header.body.statements[0].kind, StatementKind::Return(Some(Expression { kind: ExpressionKind::InterfaceCoerce { value, .. }, .. })) if matches!(value.kind, ExpressionKind::BitfieldConstruct { validates_widths: false, .. })) + ); + } + + #[test] + fn secret_bitfield_and_machine_normalization_requires_exact_targets() { + let mut types = TypeInterner::new(); + let first_id = types.add_bitfield(jett_types::BitfieldDef { + name: "First".into(), + network_order: false, + fields: vec![], + }); + let first = types.intern(Type::Bitfield(first_id)); + let other_id = types.add_bitfield(jett_types::BitfieldDef { + name: "Other".into(), + network_order: false, + fields: vec![], + }); + let other = types.intern(Type::Bitfield(other_id)); + let result = types.intern(Type::Result(first, TypeInterner::STRING)); + let wrong_ok = types.intern(Type::Result(other, TypeInterner::STRING)); + let wrong_error = types.intern(Type::Result(first, TypeInterner::INT64)); + let refined = types.intern(Type::Refinement { + name: "Selected".into(), + base: first, + }); + let machine_id = types.add_machine(jett_types::MachineDef { + name: "Session".into(), + states: vec![ + jett_types::MachineStateDef { + name: "active".into(), + fields: vec![], + }, + jett_types::MachineStateDef { + name: "idle".into(), + fields: vec![], + }, + ], + transitions: vec![], + }); + let machine = types.intern(Type::Machine(machine_id)); + let active = types.intern(Type::MachineState { + machine: machine_id, + state: jett_types::MachineStateId::new(0), + }); + let idle = types.intern(Type::MachineState { + machine: machine_id, + state: jett_types::MachineStateId::new(1), + }); + let span = Span::new(FileId::new(0), 3, 9); + let bitfield = |validates_widths| ExpressionKind::BitfieldConstruct { + bitfield_type: first, + fields: vec![], + evaluation_order: vec![], + validates_widths, + }; + let state = |state_type, index| ExpressionKind::MachineConstruct { + state_type, + state: StateId(index), + payloads: vec![], + }; + for (output, kind, accepted) in [ + (first, bitfield(false), true), + (result, bitfield(true), true), + (first, bitfield(true), false), + (result, bitfield(false), false), + (other, bitfield(false), false), + (wrong_ok, bitfield(true), false), + (wrong_error, bitfield(true), false), + (refined, bitfield(false), false), + (active, state(active, 0), true), + (active, state(active, 1), false), + (active, state(idle, 0), false), + (machine, state(active, 0), false), + ( + active, + ExpressionKind::Call { + ownership: CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: FunctionId::new(0), + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + &types, + ) + .unwrap(), + function: FunctionId::new(0), + args: vec![], + evaluation_order: vec![], + }, + false, + ), + ] { + let secret = types.intern(Type::Secret(output)); + let twice = types.intern(Type::Secret(secret)); + for qualified in [secret, twice] { + let original = Expression { + kind: kind.clone(), + ty: qualified, + span, + }; + let normalized = normalize_secret_constructor(original.clone(), &types); + if accepted { + assert_eq!(normalized.ty, qualified); + assert_eq!(normalized.span, span); + let ExpressionKind::InterfaceCoerce { value, adapters } = &normalized.kind + else { + panic!("exact qualified constructor"); + }; + assert_eq!(value.ty, output); + assert_eq!(value.span, span); + assert_eq!(value.kind, original.kind); + assert!(adapters.is_empty()); + assert_eq!( + normalize_secret_constructor(normalized.clone(), &types), + normalized + ); + } else { + assert_eq!(normalized, original); + } + } + } + } + + #[test] + fn lowers_checked_compiler_calls_as_intrinsics() { + let source = r#"namespace app +function absolute(value: int64) returns int64: + return math.abs(value) +"#; + let program = lower_source(source); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Intrinsic { + intrinsic, args, .. + }, + .. + })) = &program.functions[0].body.statements[0].kind + else { + panic!("expected compiler intrinsic"); + }; + assert_eq!(*intrinsic, IntrinsicId::MathAbs); + assert_eq!(args.len(), 1); + } + + #[test] + fn direct_user_functions_remain_distinct_from_intrinsics() { + let program = lower_source( + r#"namespace app +function identity(value: int64) returns int64: + return value +function main() returns int64: + return identity(7) +"#, + ); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("main should lower"); + assert!(matches!( + main.body.statements[0].kind, + StatementKind::Return(Some(Expression { + kind: ExpressionKind::Call { .. }, + .. + })) + )); + } + + #[test] + fn lowering_rejects_an_intrinsic_without_a_checked_closed_identity() { let source = r#"namespace app function absolute(value: int64) returns int64: return math.abs(value) "#; - let program = lower_source(source); + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + let resolved = jett_resolve::resolve(&parsed.module); + let mut checked = jett_typecheck::check(&parsed.module, &resolved); + assert_eq!( + checked.intrinsic_ids.values().copied().collect::>(), + [IntrinsicId::MathAbs] + ); + checked.intrinsic_ids.clear(); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + + let errors = lower(&parsed.module, &resolved, &checked, &origins) + .expect_err("unclassified intrinsic must not enter HIR"); + + assert!( + errors + .iter() + .any(|error| { error.message.contains("has no closed intrinsic identity") }) + ); + } + + #[test] + fn preserves_checked_intrinsic_type_operands() { + let program = lower_source( + r#"namespace app +function describe[T]() returns string: + return type.name[T]() +function main() returns string: + return describe[int64]() +"#, + ); + let describe = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "describe") + .expect("concrete describe function should lower"); let StatementKind::Return(Some(Expression { kind: ExpressionKind::Intrinsic { - canonical_name, - args, + intrinsic, + type_arguments, .. }, .. - })) = &program.functions[0].body.statements[0].kind + })) = &describe.body.statements[0].kind else { - panic!("expected compiler intrinsic"); + panic!("expected reflected compiler intrinsic"); }; - assert_eq!(canonical_name, "math.abs"); - assert_eq!(args.len(), 1); + assert_eq!(*intrinsic, IntrinsicId::TypeName); + assert_eq!(type_arguments, describe.identity.type_arguments.as_slice()); + } + + #[test] + fn preserves_stdlib_kernel_type_operands_in_inferred_generic_instantiations() { + let source = r#"namespace list +export function length[T](view items: list[T]) returns int64: + return list.__length[T](view items) +function main() returns int64: + list[int64] items = list() + return list.length(view items) +"#; + let file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let parsed = jett_parser::parse(source, file); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve errors: {:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "type errors: {:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Stdlib)]); + let program = lower(&parsed.module, &resolved, &checked, &origins) + .expect("stdlib generic kernel call should lower"); + + let length = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "length") + .expect("concrete length function should lower"); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + .. + }, + .. + })) = &length.body.statements[0].kind + else { + panic!("expected stdlib kernel intrinsic"); + }; + assert_eq!(*intrinsic, IntrinsicId::ListLength); + assert_eq!(type_arguments, length.identity.type_arguments.as_slice()); + } + + #[test] + fn lowers_list_stdlib_specializations_with_checked_kernel_operands() { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let stdlib_file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let project_file = FileId::new(0); + let stdlib_source = std::fs::read_to_string(root.join("stdlib/list.jett")) + .expect("list stdlib source should be readable"); + let project_source = + std::fs::read_to_string(root.join("tests/run_pass/list_operations.jett")) + .expect("list fixture should be readable"); + let stdlib = jett_parser::parse(&stdlib_source, stdlib_file); + let project = jett_parser::parse(&project_source, project_file); + assert!( + stdlib.errors.is_empty(), + "stdlib parse errors: {:?}", + stdlib.errors + ); + assert!( + project.errors.is_empty(), + "project parse errors: {:?}", + project.errors + ); + let mut items = stdlib.module.items; + items.extend(project.module.items); + let module = Module { + items, + span: project.module.span, + }; + let resolved = jett_resolve::resolve(&module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "resolve errors: {:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "type errors: {:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (stdlib_file, SourceOrigin::Stdlib), + (project_file, SourceOrigin::Project), + ]); + lower(&module, &resolved, &checked, &origins) + .expect("list stdlib specializations should lower"); + } + + #[test] + fn lowers_reflected_comptime_type_bodies_as_exclusive_dispatch() { + let program = lower_source( + r#"namespace app +struct User: + name: string + age: int64 + city: string +function reflected_names[T](view value: T) returns string: + mutable string output = "" + for field in type.fields[T](): + comptime type Field = field.type_info: + output = type.name[Field]() + return output +function main() returns string: + User user = User(name: "Ada", age: 37, city: "London") + return reflected_names[User](view user) +"#, + ); + let reflected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "reflected_names") + .expect("concrete reflected function should lower"); + let StatementKind::For { body, .. } = &reflected.body.statements[1].kind else { + panic!("expected reflected field loop"); + }; + let StatementKind::ReflectedTypeDispatch { type_info, arms } = &body.statements[0].kind + else { + panic!("expected compiler-owned reflected type dispatch"); + }; + assert!(matches!(type_info.kind, ExpressionKind::Field { .. })); + assert_eq!(arms.len(), 2); + assert_eq!( + arms.iter() + .map(|arm| arm.iteration_index) + .collect::>(), + [0, 1] + ); + assert_ne!(arms[0].bound_type, arms[1].bound_type); + for arm in arms { + let StatementKind::Assign { + value: + Expression { + kind: + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + .. + }, + .. + }, + .. + } = &arm.body.statements[0].kind + else { + panic!("expected specialized reflected body"); + }; + assert_eq!(*intrinsic, IntrinsicId::TypeName); + assert_eq!(type_arguments, &[arm.bound_type]); + } } #[test] @@ -3343,6 +11863,30 @@ function main() returns nothing: assert!(matches!(args[0].kind, ExpressionKind::Int(1))); assert!(matches!(args[1].kind, ExpressionKind::Int(2))); assert_eq!(evaluation_order, &[1, 0]); + let constructor = &program.functions[1]; + assert_eq!( + constructor.identity.declaration.kind, + DeclarationKind::ActorConstructor + ); + assert_eq!(constructor.params[0].name, "seed"); + assert_eq!(constructor.params[1].name, "step"); + assert!(matches!( + constructor.body.statements[constructor.params.len()].kind, + StatementKind::Let { + value: Expression { + kind: ExpressionKind::Local(_), + .. + }, + .. + } + )); + assert!(matches!( + value.kind, + ExpressionKind::ActorSpawn { + constructor: Some(id), + .. + } if id == constructor.id + )); let StatementKind::Expression(Expression { kind: @@ -3396,25 +11940,367 @@ function main() returns nothing: } #[test] - fn lowers_inline_functions_and_indirect_calls() { + fn extracts_capture_free_inline_functions_and_indirect_calls() { + let source = r#"namespace app +function main() returns int64: + function(int64) returns int64 double = function(value: int64) returns int64: return value * 2 + return double(4) +"#; + let program = lower_source(source); + let main = &program.functions[0]; + let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { + panic!("expected closure local"); + }; + assert!(matches!( + value.kind, + ExpressionKind::FunctionRef(FunctionId(1)) + )); + assert_eq!(program.functions[1].params.len(), 1); + assert_eq!(main.debug_kind, FunctionDebugKind::Named("app.main".into())); + assert_eq!(program.functions[1].debug_kind, FunctionDebugKind::Inline); + assert_eq!(program.functions[1].params[0].name, "value"); + assert!(matches!( + main.body.statements[1].kind, + StatementKind::Return(Some(Expression { + kind: ExpressionKind::IndirectCall { .. }, + .. + })) + )); + } + + #[test] + fn lowers_checked_function_expression_callees_without_losing_direct_calls() { + let source = r#"namespace app +function increment(value: int64) returns int64: + return value + 1 +function factory(delta: int64) returns function(int64) returns int64: + return function(value: int64) returns int64: return value + delta +struct Wrapper: + invoke: function(int64) returns int64 +function parenthesized() returns int64: + function(int64) returns int64 callback = increment + return (callback)(4) +function returned() returns int64: + return factory(3)(4) +function projected() returns int64: + Wrapper wrapper = Wrapper(invoke: increment) + return wrapper.invoke(4) +function inline_call() returns int64: + return (function(value: int64) returns int64: return value * 2)(4) +function captured_call() returns int64: + int64 delta = 3 + return (function(value: int64) returns int64: return value + delta)(4) +function direct() returns int64: + return increment(4) +"#; + let program = lower_source(source); + let returned_value = |name: &str| { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("checked caller"); + let StatementKind::Return(Some(value)) = &function + .body + .statements + .last() + .expect("return statement") + .kind + else { + panic!("expected returned value in {name}"); + }; + value + }; + for name in [ + "parenthesized", + "returned", + "projected", + "inline_call", + "captured_call", + ] { + let ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + } = &returned_value(name).kind + else { + panic!("expected indirect call in {name}"); + }; + assert_eq!(evaluation_order, &[0]); + assert!(matches!(args[0].kind, ExpressionKind::Int(4))); + assert!(match name { + "parenthesized" => matches!(callee.kind, ExpressionKind::Local(_)), + "returned" => matches!(callee.kind, ExpressionKind::Call { .. }), + "projected" => matches!(callee.kind, ExpressionKind::Field { .. }), + "inline_call" => matches!(callee.kind, ExpressionKind::FunctionRef(_)), + "captured_call" => matches!(callee.kind, ExpressionKind::ClosureRef { .. }), + _ => unreachable!(), + }); + } + assert!(matches!( + returned_value("direct").kind, + ExpressionKind::Call { .. } + )); + let projected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "projected") + .expect("projected caller"); + assert!(matches!( + projected.body.statements[0].kind, + StatementKind::Let { + value: Expression { + kind: ExpressionKind::StructConstruct { .. }, + .. + }, + .. + } + )); + } + + #[test] + fn expression_callees_retain_view_arguments_and_argument_handlers() { + let source = r#"namespace app +function choose(view first: int64, second: int64) returns int64: + return first + second +function main() returns int64: + function(view int64, int64) returns int64 callback = choose + optional[int64] fallback = none + return (callback)(view 3, fallback handle: + default 4 + ) +"#; + let program = lower_source(source); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + }, + .. + })) = &program.functions[1].body.statements[2].kind + else { + panic!("expected handled expression-callee call"); + }; + assert!(matches!(callee.kind, ExpressionKind::Local(_))); + assert_eq!(evaluation_order, &[0, 1]); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert!(matches!(args[1].kind, ExpressionKind::Handle { .. })); + assert_eq!(program.functions[0].params[0].mode, ParamMode::View); + } + + #[test] + fn parenthesized_callees_preserve_checked_declaration_and_intrinsic_identity() { + let source = r#"namespace app +function subtract(left: int64, right: int64) returns int64: + return left - right +function identity[T](value: T) returns T: + return value +struct Pair: + left: int64 + right: int64 +enum Choice: + item(left: int64, right: int64) +function main() returns int64: + int64 named = ((subtract))(right: 3, left: 10) + int64 qualified = (app.subtract)(right: 2, left: 9) + int64 explicit = (identity)[int64](value: 8) + int64 inferred = (identity)(value: 7) + Pair pair = (Pair)(right: 5, left: 6) + Choice choice = (Choice.item)(right: 4, left: 9) + (println)(named) + return qualified +"#; + let program = lower_source(source); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("main function"); + for (index, first, second) in [(0, 10, 3), (1, 9, 2)] { + let StatementKind::Let { + value: + Expression { + kind: + ExpressionKind::Call { + args, + evaluation_order, + .. + }, + .. + }, + .. + } = &main.body.statements[index].kind + else { + panic!("expected direct named call"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!(args[0].kind, ExpressionKind::Int(value) if value == first)); + assert!(matches!(args[1].kind, ExpressionKind::Int(value) if value == second)); + } + for index in [2, 3] { + assert!(matches!( + main.body.statements[index].kind, + StatementKind::Let { + value: Expression { + kind: ExpressionKind::Call { .. }, + .. + }, + .. + } + )); + } + let StatementKind::Let { + value: + Expression { + kind: + ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + }, + .. + }, + .. + } = &main.body.statements[4].kind + else { + panic!("expected parenthesized struct constructor"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!(fields[0].kind, ExpressionKind::Int(6))); + assert!(matches!(fields[1].kind, ExpressionKind::Int(5))); + let StatementKind::Let { + value: + Expression { + kind: + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + }, + .. + }, + .. + } = &main.body.statements[5].kind + else { + panic!("expected parenthesized named enum constructor"); + }; + assert_eq!(evaluation_order, &[1, 0]); + assert!(matches!(payloads[0].kind, ExpressionKind::Int(9))); + assert!(matches!(payloads[1].kind, ExpressionKind::Int(4))); + assert!(matches!( + main.body.statements[6].kind, + StatementKind::Expression(Expression { + kind: ExpressionKind::Intrinsic { + intrinsic: IntrinsicId::Println, + .. + }, + .. + }) + )); + } + + #[test] + fn extracts_captured_inline_functions_with_environment_parameters() { + let source = r#"namespace app +function make(seed: int64) returns function(int64) returns int64: + return function(value: int64) returns int64: return value + seed +"#; + let program = lower_source(source); + assert_eq!(program.functions.len(), 2); + assert!(matches!( + program.functions[0].body.statements[0].kind, + StatementKind::Return(Some(Expression { + kind: ExpressionKind::ClosureRef { + function: FunctionId(1), + ref captures, + }, + .. + })) if captures == &[LocalId::new(0)] + )); + assert_eq!(program.functions[1].capture_count, 1); + assert_eq!(program.functions[1].params.len(), 2); + assert_eq!(program.functions[1].debug_kind, FunctionDebugKind::Inline); + assert_eq!(program.functions[1].params[0].name, "seed"); + assert_eq!(program.functions[1].params[1].name, "value"); + } + + #[test] + fn generic_inline_parameters_retain_each_concrete_signature_and_source_identity() { + let program = lower_source( + r#"namespace app +function factory[T](seed: T) returns function(view T) returns T: + return function(view input: T) returns T: return seed +function main() returns int64: + function(view int64) returns int64 integer = factory[int64](1) + function(view string) returns string text = factory[string]("value") + return integer(view 2) +"#, + ); + let mut types = Vec::new(); + for function in &program.functions { + if function.debug_kind != FunctionDebugKind::Inline { + continue; + } + let concrete = function.identity.type_arguments[0]; + types.push(concrete); + assert_eq!(function.capture_count, 1); + assert_eq!(function.params[0].name, "seed"); + assert_eq!(function.params[0].ty, concrete); + assert_eq!(function.params[1].name, "input"); + assert_eq!(function.params[1].ty, concrete); + assert_eq!(function.params[1].mode, ParamMode::View); + assert_eq!(function.return_type, concrete); + } + types.sort_by_key(|ty| ty.index()); + assert_eq!(types, vec![TypeInterner::INT64, TypeInterner::STRING]); + } + + #[test] + fn validates_closure_captures_by_type_across_local_id_spaces() { let source = r#"namespace app -function main() returns int64: - function(int64) returns int64 double = function(value: int64) returns int64: return value * 2 - return double(4) +function make(seed: int64) returns function(int64) returns int64: + int64 alternate = 2 + return function(value: int64) returns int64: return value + seed "#; - let program = lower_source(source); - let main = &program.functions[0]; - let StatementKind::Let { value, .. } = &main.body.statements[0].kind else { - panic!("expected closure local"); + let mut program = lower_source(source); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::ClosureRef { captures, .. }, + .. + })) = &mut program.functions[0].body.statements[1].kind + else { + panic!("expected captured closure return"); }; - assert!(matches!(value.kind, ExpressionKind::InlineFunction { .. })); + captures[0] = LocalId::new(1); + validate(&program).expect("caller capture IDs need only match target parameter types"); + + program.functions[0].locals[1].ty = TypeInterner::STRING; + assert!( + validate(&program).is_err(), + "capture type mismatch must fail" + ); + } + + #[test] + fn extracts_inline_function_view_parameter() { + let program = lower_source( + r#"namespace app +function make() returns function(view int64) returns int64: + return function(view value: int64) returns int64: return value +"#, + ); + assert_eq!(program.functions.len(), 2); assert!(matches!( - main.body.statements[1].kind, + program.functions[0].body.statements[0].kind, StatementKind::Return(Some(Expression { - kind: ExpressionKind::IndirectCall { .. }, + kind: ExpressionKind::FunctionRef(FunctionId(1)), .. })) )); + assert_eq!(program.functions[1].params[0].mode, ParamMode::View); } #[test] @@ -3462,6 +12348,7 @@ function main() returns int64: function, args, evaluation_order, + .. } = &value.kind else { panic!("expected lowered generic pipeline step"); @@ -3478,6 +12365,7 @@ function main() returns int64: function, args, evaluation_order, + .. }, .. })) = &main.body.statements[2].kind @@ -3490,6 +12378,7 @@ function main() returns int64: function, args, evaluation_order, + .. } = &args[0].kind else { panic!("expected concrete method pipeline step"); @@ -3499,6 +12388,184 @@ function main() returns int64: assert!(matches!(args[0].kind, ExpressionKind::View(_))); } + #[test] + fn lowers_function_value_pipeline_steps_with_checked_views_and_arguments() { + let source = r#"namespace app +function choose(view first: int64, second: int64) returns int64: + return first + second +function apply(callback: function(view int64, int64) returns int64) returns int64: + return 1 into view callback(2) +function main() returns int64: + function(view int64, int64) returns int64 callback = choose + return 3 into view callback(4) +"#; + let program = lower_source(source); + let choose = &program.functions[0]; + assert_eq!(choose.params[0].mode, ParamMode::View); + for (function_index, statement_index, first, second) in [(1, 0, 1, 2), (2, 1, 3, 4)] { + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + }, + .. + })) = &program.functions[function_index].body.statements[statement_index].kind + else { + panic!("expected function-value pipeline call"); + }; + assert!(matches!(callee.kind, ExpressionKind::Local(LocalId(0)))); + assert_eq!(evaluation_order, &[0, 1]); + let ExpressionKind::View(input) = &args[0].kind else { + panic!("expected viewed pipeline input"); + }; + assert!(matches!(input.kind, ExpressionKind::Int(value) if value == first)); + assert!(matches!(args[1].kind, ExpressionKind::Int(value) if value == second)); + } + } + + #[test] + fn lowers_expression_pipeline_targets_without_unwrapping_grouped_calls() { + let source = r#"namespace app +function choose(view first: int64, second: int64) returns int64: + return first + second +function factory() returns function(view int64, int64) returns int64: + return choose +struct Wrapper: + invoke: function(view int64, int64) returns int64 +function projected() returns int64: + Wrapper wrapper = Wrapper(invoke: choose) + return 3 into view wrapper.invoke(4) +function grouped() returns int64: + return 3 into view (factory())(4) +function parenthesized() returns int64: + function(view int64, int64) returns int64 callback = choose + return 3 into view (callback)(4) +function unary_factory() returns function(int64) returns int64: + return function(value: int64) returns int64: return value + 1 +function grouped_without_arguments() returns int64: + return 3 into (unary_factory()) +"#; + let program = lower_source(source); + for name in [ + "projected", + "grouped", + "parenthesized", + "grouped_without_arguments", + ] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("pipeline caller"); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + }, + .. + })) = &function + .body + .statements + .last() + .expect("return statement") + .kind + else { + panic!("expected expression pipeline call in {name}"); + }; + assert!(match name { + "projected" => matches!(callee.kind, ExpressionKind::Field { .. }), + "grouped" | "grouped_without_arguments" => { + matches!(&callee.kind, ExpressionKind::Call { args, .. } if args.is_empty()) + } + "parenthesized" => matches!(callee.kind, ExpressionKind::Local(_)), + _ => unreachable!(), + }); + if name == "grouped_without_arguments" { + assert_eq!(evaluation_order, &[0]); + assert!(matches!(args[0].kind, ExpressionKind::Int(3))); + } else { + assert_eq!(evaluation_order, &[0, 1]); + assert!(matches!(args[0].kind, ExpressionKind::View(_))); + assert!(matches!(args[1].kind, ExpressionKind::Int(4))); + } + } + } + + #[test] + fn named_enum_calls_and_pipeline_payloads_use_checked_order_through_aliases() { + let source = r#"namespace models +export enum Choice: + item(first: int64, second: int64, third: int64) +namespace app +function direct() returns models.Choice: + use models as m + return (m.Choice.item)(third: 3, first: 1, second: 2) +function piped() returns models.Choice: + use models as m + return 1 into (m.Choice.item)(third: 3, second: 2) +"#; + let program = lower_source(source); + for (index, expected_order) in [(0, vec![2, 0, 1]), (1, vec![0, 2, 1])] { + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + }, + .. + })) = &program.functions[index] + .body + .statements + .last() + .expect("return statement") + .kind + else { + panic!("expected named enum construction"); + }; + assert_eq!(evaluation_order, &expected_order); + assert!(matches!(payloads[0].kind, ExpressionKind::Int(1))); + assert!(matches!(payloads[1].kind, ExpressionKind::Int(2))); + assert!(matches!(payloads[2].kind, ExpressionKind::Int(3))); + } + } + + #[test] + fn function_fields_returning_enums_are_not_variant_constructors() { + let source = r#"namespace app +enum Choice: + item(value: int64) + other(value: int64) +function other(value: int64) returns Choice: + return Choice.other(value) +struct Holder: + item: function(int64) returns Choice +function direct() returns Choice: + Holder holder = Holder(item: other) + return holder.item(4) +function piped() returns Choice: + Holder holder = Holder(item: other) + return 4 into holder.item +"#; + let program = lower_source(source); + for function in &program.functions[1..] { + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::IndirectCall { callee, .. }, + .. + })) = &function.body.statements[1].kind + else { + panic!("expected function field call"); + }; + assert!(matches!(callee.kind, ExpressionKind::Field { .. })); + } + } + #[test] fn lowers_handled_pipeline_steps_and_continues_with_the_unwrapped_value() { let source = r#"namespace app @@ -3581,4 +12648,454 @@ function main() returns int64: Some(true) )); } + + #[test] + fn contextual_absent_handle_preserves_the_local_producer_signature() { + let program = lower_source( + r#"function defaulted[T](value: optional[T]) returns int64: + return value handle: default 7 +function main() returns int64: + return defaulted(none) +"#, + ); + let defaulted = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "defaulted") + .unwrap(); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Handle { target, .. }, + ty, + .. + })) = &defaulted.body.statements[0].kind + else { + panic!("expected contextual handle"); + }; + assert_eq!(*ty, TypeInterner::INT64); + assert_eq!(target.ty, defaulted.params[0].ty); + assert!(matches!(target.kind, ExpressionKind::Local(_))); + assert_eq!(defaulted.identity.type_arguments, [TypeInterner::NEVER]); + } + + #[test] + fn contextual_inhabited_handle_converts_the_sum_before_payload_extraction() { + let program = lower_source( + r#"function defaulted[T](value: optional[list[T]]) returns list[string]: + return value handle: default list("fallback") +function main() returns nothing: + list[string] values = defaulted(some(list())) + trace values + return nothing +"#, + ); + let defaulted = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "defaulted") + .unwrap(); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::Handle { target, .. }, + .. + })) = &defaulted.body.statements[0].kind + else { + panic!("expected contextual handle"); + }; + let ExpressionKind::InterfaceCoerce { value, adapters } = &target.kind else { + panic!("expected explicit contextual sum conversion"); + }; + assert_eq!(value.ty, defaulted.params[0].ty); + assert!(matches!(value.kind, ExpressionKind::Local(_))); + assert_ne!(target.ty, value.ty); + assert!(adapters.is_empty()); + assert_eq!(defaulted.identity.type_arguments, [TypeInterner::NEVER]); + } + + #[test] + fn mixed_empty_literals_rebuild_the_first_nested_list_ownership_shape() { + let program = lower_source( + r#"function main() returns nothing: + for values in list(list(), list("ready")): + trace values + return nothing +"#, + ); + let StatementKind::For { iterable, .. } = &program.functions[0].body.statements[0].kind + else { + panic!("expected inferred iteration"); + }; + let ExpressionKind::ListConstruct { elements } = &iterable.kind else { + panic!("expected mixed literal"); + }; + let ExpressionKind::InterfaceCoerce { value, .. } = &elements[0].kind else { + panic!("expected empty list conversion"); + }; + assert_ne!(value.ty, elements[0].ty); + assert_eq!(elements[0].ty, elements[1].ty); + assert!( + matches!(&value.kind, ExpressionKind::ListConstruct { elements } if elements.is_empty()) + ); + } + + #[test] + fn mixed_callable_empty_returns_keep_source_signature_and_convert_adapter_return() { + let program = lower_source( + r#"function make_empty[T](view items: list[T]) returns function() returns list[T]: + return function() returns list[T]: return list() +function ready() returns list[string]: + return list("ready") +function main() returns nothing: + for callback in list(make_empty(list()), ready): + list[string] values = callback() + trace values + return nothing +"#, + ); + let adapter = program + .functions + .iter() + .find(|function| { + function + .identity + .declaration + .name + .starts_with("$interface.adapter.") + }) + .expect("source signature adapter"); + let StatementKind::Return(Some(Expression { + kind: ExpressionKind::InterfaceCoerce { value, adapters }, + ty, + .. + })) = &adapter.body.statements[0].kind + else { + panic!("expected converted adapter return"); + }; + assert!(matches!(value.kind, ExpressionKind::IndirectCall { .. })); + assert_eq!(*ty, adapter.return_type); + assert_ne!(*ty, value.ty); + assert!(adapters.is_empty()); + } + + #[test] + fn machine_bare_local_annotations_keep_storage_type_and_exact_producer_provenance() { + let (program, checked) = lower_source_with_check( + r#"namespace app +machine Session: + states: + empty + content(values: list[int64]) + transitions: + empty to content +type SessionAlias = Session +function replace() returns Session: + mutable Session source = Session(empty) + source = Session(content, list(7)) + return source +function immutable() returns Session: + Session source = Session(content, list(7)) + return source +function alias() returns Session: + SessionAlias source = Session(empty) + return source +function copy(view source: Session at content) returns Session: + Session erased = clone source + return erased +function pending() returns Session: + mutable Session source = run Session(empty) + source = run run Session(content, list(7)) + return source +function precise() returns Session at empty: + Session at empty source = Session(empty) + return source +"#, + false, + ); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + for name in ["replace", "immutable", "alias", "copy", "pending"] { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let StatementKind::Let { local, value } = &function.body.statements[0].kind else { + panic!("{name}: machine binding"); + }; + let local = &function.locals[local.index() as usize]; + let Type::Machine(owner) = checked.interner.resolve(local.ty) else { + panic!("{name}: declaration must erase initializer state in storage"); + }; + assert_eq!(local.ty, function.return_type); + assert_eq!(local.debug_ty, local.ty); + assert_eq!(checked.type_map.get(&value.span), Some(&value.ty)); + let Type::MachineState { machine, .. } = checked.interner.resolve(value.ty) else { + panic!("{name}: original producer state must remain precise"); + }; + assert_eq!(machine, owner); + if name == "copy" { + assert!(matches!(value.kind, ExpressionKind::Clone(_))); + } else { + let mut producer = value; + while let ExpressionKind::Run(inner) = &producer.kind { + producer = inner; + } + let ExpressionKind::MachineConstruct { + state_type, state, .. + } = &producer.kind + else { + panic!("{name}: exact constructor must remain intact"); + }; + assert_eq!(*state_type, producer.ty); + let Type::MachineState { + state: expected, .. + } = checked.interner.resolve(*state_type) + else { + panic!("checked constructor state"); + }; + assert_eq!(state.index(), expected.index()); + } + if matches!(name, "replace" | "pending") { + let StatementKind::Assign { target, value } = &function.body.statements[1].kind + else { + panic!("{name}: ordinary rebinding"); + }; + assert_eq!(target.ty, local.ty); + assert_eq!(checked.type_map.get(&value.span), Some(&value.ty)); + assert!( + matches!(checked.interner.resolve(value.ty), Type::MachineState { machine, .. } if machine == owner) + ); + } + } + let precise = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "precise") + .unwrap(); + let StatementKind::Let { local, value } = &precise.body.statements[0].kind else { + panic!("precise binding"); + }; + assert_eq!(precise.locals[local.index() as usize].ty, value.ty); + assert!(matches!( + checked.interner.resolve(value.ty), + Type::MachineState { .. } + )); + } + + #[test] + fn machine_rebinding_does_not_widen_explicit_or_guarded_state_targets() { + for body in [ + " mutable Session at empty source = Session(empty)\n source = Session(content, list(7))\n return source\n", + " mutable Session source = Session(empty)\n if source at empty:\n source = Session(content, list(7))\n return source\n", + ] { + let source = format!( + "namespace app\nmachine Session:\n states:\n empty\n content(values: list[int64])\n transitions:\n empty to content\nfunction invalid() returns Session:\n{body}" + ); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .any(|d| d.severity == jett_diagnostics::Severity::Error), + "precise assignment must remain rejected: {:?}", + checked.diagnostics + ); + } + } + + const VIEWED_ITERATION_CALL_SOURCE: &str = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function text_size(view text: string) returns int64: + return 1 +function list_items(view items: list[Item]) returns int64: + mutable int64 total = 0 + for item in view items: + total = total + inspect(view item) + return total +function map_items(view items: map[string, Item]) returns int64: + mutable int64 total = 0 + for key, item in view items: + total = total + text_size(view key) + inspect(view item) + return total +function strings(view text: string) returns int64: + mutable int64 total = 0 + for item in view text: + total = total + text_size(view item) + return total +function set_items(view items: set[string]) returns int64: + mutable int64 total = 0 + for item in view items: + total = total + text_size(view item) + return total +function owned_items(items: list[Item]) returns int64: + mutable int64 total = 0 + for item in items: + total = total + inspect(item) + return total +"#; + + #[test] + fn viewed_loop_caller_origins_use_typed_iteration_backing_not_abi_parameters() { + let (program, checked) = lower_source_with_check(VIEWED_ITERATION_CALL_SOURCE, false); + validate_backend_types(&program, &checked.interner).expect("exact viewed iteration source"); + for name in ["list_items", "map_items", "strings", "set_items"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let StatementKind::For { + key, + value, + by_view, + .. + } = &function.body.statements[1].kind + else { + panic!("source For") + }; + assert!(*by_view); + assert!(!function.params.iter().any(|param| param.local == *key)); + assert!( + !function + .params + .iter() + .any(|param| Some(param.local) == *value) + ); + assert!(function.locals[key.index() as usize].view_source.is_none()); + } + let owned = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "owned_items") + .unwrap(); + assert!(matches!( + owned.body.statements[1].kind, + StatementKind::For { by_view: false, .. } + )); + } + + #[test] + fn viewed_loop_proof_stays_in_exact_body_and_rejects_forged_binders() { + let (program, checked) = lower_source_with_check(VIEWED_ITERATION_CALL_SOURCE, false); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "list_items") + .unwrap(); + let original_loop = program.functions[index].body.statements[1].clone(); + for case in 0..6 { + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let StatementKind::For { + key, by_view, body, .. + } = &mut function.body.statements[1].kind + else { + unreachable!() + }; + match case { + 0 => *by_view = false, + 1 => { + let escaped = body.statements[0].clone(); + function.body.statements.insert(2, escaped); + } + 2 => *key = function.params[0].local, + 3 => function.locals[key.index() as usize].ty = TypeInterner::STRING, + 4 => { + let escaped = body.statements[0].clone(); + function.body.statements[1] = escaped; + } + 5 => function.body.statements.insert(2, original_loop.clone()), + _ => unreachable!(), + } + let errors = validate_backend_types(&changed, &checked.interner) + .expect_err("forged viewed loop backing"); + assert!( + errors.iter().any(|error| error + .message + .contains("borrowed binding lost its immediate source") + || error.message.contains("viewed iteration binder") + || error.message.contains("viewed iteration binding type") + || error + .message + .contains("iteration binding is declared more than once")), + "{case}: {errors:?}" + ); + } + assert!(matches!( + original_loop.kind, + StatementKind::For { by_view: true, .. } + )); + } + + #[test] + fn nested_viewed_iteration_preserves_each_immediate_binder_and_forwarded_alias() { + let (program, checked) = lower_source_with_check( + r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function nested(view groups: list[list[Item]]) returns int64: + mutable int64 total = 0 + for items in view groups: + for item in view items: + Item alias = view item + total = total + inspect(view alias) + return total +"#, + false, + ); + validate_backend_types(&program, &checked.interner) + .expect("nested and forwarded view binding"); + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "nested") + .unwrap(); + let StatementKind::For { + key: outer, body, .. + } = &mut function.body.statements[1].kind + else { + unreachable!() + }; + let StatementKind::For { key: inner, .. } = &mut body.statements[0].kind else { + unreachable!() + }; + *inner = *outer; + let errors = validate_backend_types(&changed, &checked.interner) + .expect_err("nested loop cannot reuse its parent binder"); + assert!( + errors + .iter() + .any(|error| error.message.contains("fresh immutable local")), + "{errors:?}" + ); + } + + #[test] + fn copyable_viewed_loop_binding_can_be_captured_as_an_ordinary_copy() { + let (program, checked) = lower_source_with_check( + r#"namespace app +function copy_capture(view items: list[int64]) returns int64: + mutable int64 total = 0 + for item in view items: + function() returns int64 callback = function() returns int64: + return item + total = total + callback() + return total +"#, + false, + ); + validate_backend_types(&program, &checked.interner) + .expect("copyable viewed iteration capture"); + } } + +#[cfg(test)] +#[path = "actor_callable_sealing_tests.rs"] +mod actor_callable_sealing_tests; diff --git a/crates/jett_hir/src/local_view_tests.rs b/crates/jett_hir/src/local_view_tests.rs new file mode 100644 index 00000000..d65d8073 --- /dev/null +++ b/crates/jett_hir/src/local_view_tests.rs @@ -0,0 +1,1599 @@ +use super::*; +use jett_diagnostics::Severity; + +pub(super) fn checked_source( + source: &str, + tests: bool, +) -> Result<(Program, CheckResult), Vec> { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + !resolved + .diagnostics + .iter() + .any(|error| error.severity == Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + !checked + .diagnostics + .iter() + .any(|error| error.severity == Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let program = if tests { + lower_with_test_bodies(&parsed.module, &resolved, &checked, &origins) + } else { + lower(&parsed.module, &resolved, &checked, &origins) + }?; + Ok((program, checked)) +} + +fn local<'a>(function: &'a Function, name: &str) -> &'a Local { + function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() +} + +#[test] +fn local_views_preserve_immediate_origins_and_owned_copies() { + let (program, _) = checked_source( + r#"namespace app +struct Record: + values: list[int64] + number: int64 + text: string +function inspect(view source: Record) returns nothing: + Record direct = view source + Record forwarded = direct + Record wrapped = (view forwarded) + Record owned = clone wrapped + list[int64] field_copy = source.values + list[int64] parent_view_field_copy = (view source).values + int64 number_copy = view source.number + string text_copy = view source.text + trace wrapped + return nothing +"#, + false, + ) + .unwrap(); + let function = &program.functions[0]; + let source = local(function, "source"); + let direct = local(function, "direct"); + let forwarded = local(function, "forwarded"); + assert_eq!(direct.view_source, Some(source.id)); + assert_eq!(forwarded.view_source, Some(direct.id)); + assert_eq!(local(function, "wrapped").view_source, Some(forwarded.id)); + assert_eq!( + local_view_root(&function.locals, local(function, "wrapped").id), + Some(source.id) + ); + assert!(is_borrowed_local( + &function.locals, + &function.params, + source.id + )); + for name in [ + "owned", + "field_copy", + "parent_view_field_copy", + "number_copy", + "text_copy", + ] { + assert_eq!(local(function, name).view_source, None, "{name}"); + } +} + +#[test] +fn local_views_preserve_secret_wrapper_origins_and_reject_allocating_coercions() { + let (program, mut checked) = checked_source( + "namespace app\nfunction inspect(view source: secret[list[int64]]) returns nothing:\n list[int64] alias = declassify view source\n trace alias\n return nothing\n", + false, + ).unwrap(); + let function = &program.functions[0]; + let source = local(function, "source").id; + let alias = local(function, "alias"); + assert_eq!(alias.view_source, Some(source)); + let StatementKind::Let { value, .. } = &function.body.statements[0].kind else { + panic!("alias initializer"); + }; + let source_type = function.locals[source.index() as usize].ty; + validate_local_view_initializer(value, source, source_type, alias.ty, &checked.interner) + .unwrap(); + let changed = checked.interner.intern(Type::List(TypeInterner::STRING)); + let conversion = Expression { + kind: ExpressionKind::interface_coerce(Box::new(value.clone())), + ty: changed, + span: value.span, + }; + assert!( + validate_local_view_initializer( + &conversion, + source, + source_type, + changed, + &checked.interner + ) + .unwrap_err() + .contains("allocate") + ); +} + +#[test] +fn local_views_keep_same_named_sibling_bindings_distinct() { + let (program, _) = checked_source( + r#"namespace app +function inspect() returns nothing: + if true: + list[int64] source = list(1) + list[int64] alias = view source + trace alias + else: + list[int64] source = list(2) + list[int64] alias = view source + trace alias + return nothing +"#, + false, + ) + .unwrap(); + let function = &program.functions[0]; + let sources = function + .locals + .iter() + .filter(|local| local.name == "source") + .collect::>(); + assert_eq!(sources.len(), 2); + let aliases = function + .locals + .iter() + .filter(|local| local.name == "alias") + .collect::>(); + assert_eq!(aliases.len(), 2); + assert_eq!(aliases[0].view_source, Some(sources[0].id)); + assert_eq!(aliases[1].view_source, Some(sources[1].id)); + assert_ne!(sources[0].id, sources[1].id); +} + +#[test] +fn local_views_transport_generic_inline_and_test_body_facts() { + let (program, _) = checked_source(r#"namespace app +function inspect[T](view source: list[T]) returns nothing: + list[T] alias = view source + trace alias + return nothing +function main() returns nothing: + function(view list[int64]) returns nothing callback = function(view source: list[int64]) returns nothing: + list[int64] inline_alias = view source + trace inline_alias + return nothing + inspect[int64](view list(1)) + callback(view list(2)) +verify local_aliases: + list[int64] source = list(3) + list[int64] test_alias = view source + trace test_alias + assert true +property generated_aliases: + given number: int64 + list[int64] source = list(number) + list[int64] test_alias = view source + trace test_alias + assert true +"#, true).unwrap(); + let mut contexts = Vec::new(); + for function in &program.functions { + if let Some(alias) = function.locals.iter().find(|local| { + ["alias", "inline_alias", "test_alias"].contains(&local.name.as_str()) + && local.view_source.is_some() + && (function.debug_kind == FunctionDebugKind::Inline + || local.name != "inline_alias") + }) { + assert!(local_view_root(&function.locals, alias.id).is_some()); + contexts.push(function.identity.declaration.kind); + } + } + assert_eq!(contexts.len(), 4); + assert!(contexts.contains(&DeclarationKind::Verify)); + assert!(contexts.contains(&DeclarationKind::Property)); +} + +#[test] +fn local_views_transport_reflected_body_facts() { + let (program, _) = checked_source( + r#"namespace app +struct Record: + number: int64 +function inspect[T](view value: T, view source: list[int64]) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + list[int64] reflected_alias = view source + trace reflected_alias + list[int64] after_alias = view source + trace after_alias + return nothing +function main() returns nothing: + inspect[Record](view Record(number: 1), view list(2)) +"#, + false, + ) + .unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source").id; + assert_eq!(local(function, "reflected_alias").view_source, Some(source)); + assert_eq!(local(function, "after_alias").view_source, Some(source)); +} + +#[test] +fn projected_local_views_keep_typed_endpoints_and_immediate_owner_dependencies() { + let (program, checked) = checked_source( + r#"namespace app +struct Packet: + data: bytes + items: list[int64] + hidden: secret[list[int64]] + number: int64 +struct Envelope: + packet: Packet +function inspect(view source: Envelope) returns nothing: + Packet projected_record = view source.packet + bytes projected_bytes = view source.packet.data + list[int64] projected_items = view projected_record.items + list[int64] forwarded = projected_items + list[int64] revealed = declassify view source.packet.hidden + list[int64] copied = source.packet.items + list[int64] cloned = clone forwarded + int64 scalar = view source.packet.number + trace forwarded + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = &program.functions[0]; + let source = local(function, "source"); + for name in ["projected_record", "projected_bytes", "revealed"] { + let alias = local(function, name); + assert_eq!(alias.view_source, Some(source.id), "{name}"); + assert_ne!( + alias.ty, source.ty, + "field endpoint is distinct from its owner" + ); + assert_eq!(local_view_root(&function.locals, alias.id), Some(source.id)); + } + assert_eq!( + local(function, "projected_items").view_source, + Some(local(function, "projected_record").id) + ); + assert_eq!( + local(function, "forwarded").view_source, + Some(local(function, "projected_items").id) + ); + for name in ["copied", "cloned", "scalar"] { + assert_eq!(local(function, name).view_source, None, "{name}"); + } +} + +#[test] +fn projected_local_views_transport_generic_and_reflected_binding_contexts() { + let (program, checked) = checked_source( + r#"namespace app +struct Holder[T]: + value: T +struct Metadata: + number: int64 +function inspect[T](view source: Holder[T]) returns nothing: + T alias = view source.value + trace alias + return nothing +function reflected[T](view metadata: T, view source: Holder[list[int64]]) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + list[int64] reflected_alias = view source.value + trace reflected_alias + list[int64] after_alias = view source.value + trace after_alias + return nothing +function main() returns nothing: + inspect[int64](view Holder[int64](value: 7)) + inspect[list[int64]](view Holder[list[int64]](value: list(2, 3))) + reflected[Metadata](view Metadata(number: 1), view Holder[list[int64]](value: list(5))) +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let inspect = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(inspect.len(), 2); + for function in inspect { + let alias = local(function, "alias"); + if alias.ty == TypeInterner::INT64 { + assert_eq!(alias.view_source, None); + } else { + assert_eq!(alias.view_source, Some(local(function, "source").id)); + } + } + let reflected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "reflected") + .unwrap(); + for name in ["reflected_alias", "after_alias"] { + assert_eq!( + local(reflected, name).view_source, + Some(local(reflected, "source").id) + ); + } +} + +#[test] +fn projected_local_views_require_initializer_proofs_even_when_unused() { + let (program, checked) = checked_source( + "namespace app\nstruct Packet:\n data: bytes\nfunction inspect(view source: Packet) returns nothing:\n bytes unused_alias = view source.data\n return nothing\n", + false, + ).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let mut orphan = program.clone(); + orphan.functions[0].body.statements.remove(0); + assert!( + validate_backend_types(&orphan, &checked.interner) + .unwrap_err() + .iter() + .any(|error| error.message.contains("no validated initializer")) + ); + inline_functions::extract_inline_functions(&mut orphan.functions, &checked.interner); + assert!( + complete_value_conversions(&mut orphan, &checked.interner) + .unwrap_err() + .iter() + .any(|error| error.message.contains("no validated initializer")) + ); + let mut forged = program; + let StatementKind::Let { value, .. } = &mut forged.functions[0].body.statements[0].kind else { + panic!("unused projected initializer"); + }; + let ExpressionKind::View(field) = &mut value.kind else { + panic!("explicit projected view"); + }; + let ExpressionKind::Field { field, .. } = &mut field.kind else { + panic!("typed projection"); + }; + *field = FieldId(u32::MAX); + assert!( + validate_backend_types(&forged, &checked.interner) + .unwrap_err() + .iter() + .any(|error| error.message.contains("invalid field index")) + ); +} + +#[test] +fn projected_local_views_keep_unused_inline_bindings_and_clear_copied_origins() { + let (program, checked) = checked_source( + r#"namespace app +struct Packet: + data: bytes +function inspect(view source: Packet) returns nothing: + bytes outer_alias = view source.data + function(view Packet) returns nothing callback = function(view packet: Packet) returns nothing: + bytes inner_alias = view packet.data + return nothing + bytes later_alias = view source.data + callback(view source) + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let outer = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert!(local(outer, "outer_alias").view_source.is_some()); + assert!(local(outer, "later_alias").view_source.is_some()); + assert!(local(outer, "inner_alias").view_source.is_none()); + let inline = program + .functions + .iter() + .find(|function| function.debug_kind == FunctionDebugKind::Inline) + .unwrap(); + assert!(local(inline, "inner_alias").view_source.is_some()); + assert!(local(inline, "outer_alias").view_source.is_none()); + assert!(local(inline, "later_alias").view_source.is_none()); +} + +#[test] +fn local_views_reject_unstable_native_origins_without_changing_checking() { + for body in [ + "list[int64] alias = view list(1)", + "list[int64] source = list(1)\n mutable list[int64] alias = view source", + ] { + let source = format!( + "namespace app\nstruct Record:\n values: list[int64]\nfunction inspect() returns nothing:\n {body}\n trace alias\n return nothing\n" + ); + let errors = + checked_source(&source, false).expect_err("unsupported native alias must fail HIR"); + assert!( + errors + .iter() + .any(|error| error.message.contains("native borrowed alias")), + "{body}: {errors:?}" + ); + } +} + +#[test] +fn projected_local_views_preserve_existing_refinement_ancestors_and_secret_promotions() { + let (program, checked) = checked_source( + r#"namespace app +type Values = list[int64] where true +type NarrowValues = Values where true +struct Packet: + plain: list[int64] + refined: NarrowValues + hidden: secret[NarrowValues] +function inspect(view source: Packet, view values: list[int64]) returns nothing: + NarrowValues direct = view source.refined + Values ancestor = coarsen view source.refined + list[int64] base = coarsen view source.refined + NarrowValues revealed = declassify view source.hidden + secret[list[int64]] promoted_field = view source.plain + secret[list[int64]] promoted_local = view values + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source"); + for name in ["direct", "ancestor", "base", "revealed", "promoted_field"] { + assert_eq!(local(function, name).view_source, Some(source.id), "{name}"); + } + assert_eq!( + local(function, "promoted_local").view_source, + Some(local(function, "values").id) + ); + let direct = local(function, "direct").ty; + let ancestor = local(function, "ancestor").ty; + let base = local(function, "base").ty; + assert!( + matches!(checked.interner.resolve(direct), Type::Refinement { base: inner, .. } if *inner == ancestor) + ); + assert!( + matches!(checked.interner.resolve(ancestor), Type::Refinement { base: inner, .. } if *inner == base) + ); + assert_eq!(local(function, "revealed").ty, direct); + for name in ["promoted_field", "promoted_local"] { + assert!( + matches!(checked.interner.resolve(local(function, name).ty), Type::Secret(inner) if *inner == base), + "{name}" + ); + } +} + +#[test] +fn unused_projected_local_views_cannot_invent_or_discard_nominal_refinement_proofs() { + let source = r#"namespace app +type Proven = list[int64] where true +type Rejected = list[int64] where false +struct Packet: + items: Proven +function inspect(view source: Packet) returns nothing: + Proven borrowed = view source.items + return nothing +"#; + for invalid in [ + "target", + "discard target", + "view", + "conversion", + "discard conversion", + "coarsen", + "declassify", + ] { + let (mut program, checked) = checked_source(source, false).unwrap(); + let rejected = checked.interner.type_ids().find(|ty| { + matches!(checked.interner.resolve(*ty), Type::Refinement { name, .. } if name == "app.Rejected") + }).expect("declared Rejected refinement is interned"); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let borrowed = local(function, "borrowed").id; + let proven = function.locals[borrowed.index() as usize].ty; + let Type::Refinement { base, .. } = checked.interner.resolve(proven) else { + panic!("declared predicate-bearing endpoint"); + }; + let target = if invalid.starts_with("discard") { + *base + } else { + rejected + }; + function.locals[borrowed.index() as usize].ty = target; + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!("unused projected alias still has an initializer"); + }; + if !matches!(invalid, "target" | "discard target") { + let inner = Box::new(value.clone()); + value.kind = match invalid { + "view" => ExpressionKind::View(inner), + "conversion" | "discard conversion" => ExpressionKind::interface_coerce(inner), + "coarsen" => ExpressionKind::Coarsen(inner), + "declassify" => ExpressionKind::Declassify(inner), + _ => unreachable!(), + }; + value.ty = target; + } + // The owning Packet, field index/type and source identity remain valid. + // Only the alleged nominal predicate proof was changed. + validate(&program).unwrap(); + let errors = validate_backend_types(&program, &checked.interner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("native borrowed alias")), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn local_views_validate_origins_types_and_conversion_rebuilding() { + let (mut program, checked) = checked_source("namespace app\nfunction inspect(view source: list[int64]) returns nothing:\n list[int64] alias = view source\n trace alias\n return nothing\n", false).unwrap(); + let original = program.clone(); + program.functions[0].locals[1].view_source = Some(LocalId::new(1)); + assert!( + validate(&program) + .unwrap_err() + .iter() + .any(|error| error.message.contains("cycle")) + ); + program = original.clone(); + program.functions[0].locals[1].view_source = Some(LocalId::new(90)); + assert!(validate(&program).is_err()); + program = original.clone(); + program.functions[0].locals[0].mutable = true; + assert!(validate_local_views(&program, &checked.interner).is_err()); + program = original.clone(); + program.functions[0].locals[1].ty = TypeInterner::INT64; + assert!(validate_local_views(&program, &checked.interner).is_err()); + program = original; + let StatementKind::Let { value, .. } = &mut program.functions[0].body.statements[0].kind else { + panic!("alias initializer"); + }; + let original_value = value.clone(); + value.kind = ExpressionKind::Clone(Box::new(original_value)); + let errors = complete_value_conversions(&mut program, &checked.interner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("stable backing local")) + ); +} + +#[test] +fn bitfield_local_views_compose_with_struct_fields_and_keep_owned_controls() { + let (program, checked) = checked_source( + r#"namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +struct Envelope: + packet: Packet +function inspect(view source: Packet, view envelope: Envelope) returns nothing: + list[uint8] direct = view source.payload + list[uint8] forwarded = direct + Packet projected_packet = view envelope.packet + list[uint8] projected_payload = view projected_packet.payload + list[uint8] mixed = view envelope.packet.payload + list[uint8] copied = source.payload + list[uint8] cloned = clone forwarded + int64 scalar = view source.version + return nothing +function owned() returns nothing: + uint8 first = 7 + uint8 second = 255 + Packet packet = Packet(version: 4, flags: 0, payload: list(first, second)) + list[uint8] borrowed = view packet.payload + list[uint8] forwarded = borrowed + list[uint8] independent = clone forwarded + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let inspect = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(inspect, "source"); + let envelope = local(inspect, "envelope"); + assert!(matches!( + checked.interner.resolve(source.ty), + Type::Bitfield(_) + )); + assert_eq!(local(inspect, "direct").view_source, Some(source.id)); + assert_eq!( + local(inspect, "forwarded").view_source, + Some(local(inspect, "direct").id) + ); + for name in ["projected_packet", "mixed"] { + assert_eq!( + local(inspect, name).view_source, + Some(envelope.id), + "{name}" + ); + } + assert_eq!(local(inspect, "projected_packet").ty, source.ty); + assert_eq!( + local(inspect, "projected_payload").view_source, + Some(local(inspect, "projected_packet").id) + ); + assert_eq!( + local_view_root(&inspect.locals, local(inspect, "projected_payload").id), + Some(envelope.id) + ); + for name in ["copied", "cloned", "scalar"] { + assert_eq!(local(inspect, name).view_source, None, "{name}"); + } + let owned = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "owned") + .unwrap(); + assert_eq!( + local(owned, "borrowed").view_source, + Some(local(owned, "packet").id) + ); + assert_eq!( + local(owned, "forwarded").view_source, + Some(local(owned, "borrowed").id) + ); + assert_eq!(local(owned, "independent").view_source, None); +} + +#[test] +fn unused_bitfield_local_views_require_exact_field_and_root_proofs() { + let (program, mut checked) = checked_source( + r#"namespace app +bitfield network Packet: + version: 4 bits + flags: 4 bits + payload: list[uint8] +bitfield network Other: + version: 4 bits + flags: 4 bits + payload: list[uint8] +function inspect(view source: Packet, view other: Packet, view foreign: Other) returns nothing: + list[uint8] unused_alias = view source.payload + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source").id; + let other = local(function, "other").id; + let foreign_type = local(function, "foreign").ty; + let alias = local(function, "unused_alias").id; + let different_items = checked.interner.intern(Type::List(TypeInterner::STRING)); + for (invalid, expected) in [ + ("field index", "invalid field index"), + ("numeric field", "invalid field endpoint type"), + ("field endpoint", "invalid field endpoint type"), + ("field owner", "invalid field owner"), + ("source identity", "stable backing local"), + ("source type", "stable backing local"), + ("foreign bitfield", "stable backing local"), + ("orphan", "no validated initializer"), + ] { + let mut forged = program.clone(); + let function = forged + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + if invalid == "orphan" { + function.body.statements.remove(0); + } else { + if invalid == "source type" { + function.locals[source.index() as usize].ty = foreign_type; + } + if invalid == "field endpoint" { + function.locals[alias.index() as usize].ty = different_items; + } + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!("unused bitfield alias initializer"); + }; + if invalid == "field endpoint" { + value.ty = different_items; + } + let ExpressionKind::View(projection) = &mut value.kind else { + panic!("explicit field view"); + }; + if invalid == "field endpoint" { + projection.ty = different_items; + } + let ExpressionKind::Field { + base, + owner_type, + field, + } = &mut projection.kind + else { + panic!("checked bitfield field projection"); + }; + if invalid == "field owner" { + *owner_type = foreign_type; + } + if invalid == "foreign bitfield" { + *owner_type = foreign_type; + base.ty = foreign_type; + } + if invalid == "field index" { + *field = FieldId(u32::MAX); + } + if invalid == "numeric field" { + *field = FieldId(0); + } + if invalid == "source identity" { + base.kind = ExpressionKind::Local(other); + } + } + let errors = validate_backend_types(&forged, &checked.interner).unwrap_err(); + assert!( + errors.iter().any(|error| error.message.contains(expected)), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn qualified_machine_local_views_compose_with_struct_fields_and_exact_qualifications() { + let (program, checked) = checked_source( + r#"namespace app +type Values = list[int64] where true +type NarrowValues = Values where true +struct Payload: + items: list[int64] + refined: NarrowValues +machine Session: + states: + ready(payload: Payload, hidden: secret[NarrowValues], number: int64) + empty +struct Envelope: + session: Session at ready +function inspect(view source: Session at ready, view envelope: Envelope) returns nothing: + Payload projected_record = view source.payload + list[int64] machine_struct = view source.payload.items + list[int64] record_items = view projected_record.items + list[int64] forwarded = record_items + Session at ready projected_machine = view envelope.session + list[int64] struct_machine = view projected_machine.payload.items + list[int64] mixed = view envelope.session.payload.items + NarrowValues direct = view source.payload.refined + Values ancestor = coarsen view source.payload.refined + NarrowValues revealed = declassify view source.hidden + secret[list[int64]] promoted = view source.payload.items + list[int64] copied = source.payload.items + list[int64] cloned = clone forwarded + int64 scalar = view source.number + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source"); + let envelope = local(function, "envelope"); + for name in [ + "projected_record", + "machine_struct", + "direct", + "ancestor", + "revealed", + "promoted", + ] { + assert_eq!(local(function, name).view_source, Some(source.id), "{name}"); + assert_eq!( + local_view_root(&function.locals, local(function, name).id), + Some(source.id) + ); + } + for name in ["projected_machine", "mixed"] { + assert_eq!( + local(function, name).view_source, + Some(envelope.id), + "{name}" + ); + } + assert_eq!( + local(function, "record_items").view_source, + Some(local(function, "projected_record").id) + ); + assert_eq!( + local(function, "forwarded").view_source, + Some(local(function, "record_items").id) + ); + assert_eq!( + local(function, "struct_machine").view_source, + Some(local(function, "projected_machine").id) + ); + assert!(matches!( + checked.interner.resolve(source.ty), + Type::MachineState { .. } + )); + assert_eq!(local(function, "projected_machine").ty, source.ty); + assert_ne!(local(function, "machine_struct").ty, source.ty); + assert_eq!(local(function, "direct").ty, local(function, "revealed").ty); + for name in ["copied", "cloned", "scalar"] { + assert_eq!(local(function, name).view_source, None, "{name}"); + } +} + +#[test] +fn qualified_machine_local_views_transport_generic_binding_contexts() { + let (program, checked) = checked_source( + r#"namespace app +struct Payload: + items: list[int64] +machine Session: + states: + ready(payload: Payload) + empty +struct Envelope[T]: + session: Session at ready + extra: T +function inspect[T](view source: Envelope[T]) returns nothing: + list[int64] alias = view source.session.payload.items + T generic_alias = view source.extra + list[int64] forwarded = alias + trace forwarded + return nothing +function main() returns nothing: + inspect[int64](view Envelope[int64](session: Session(ready, Payload(items: list(1))), extra: 7)) + inspect[list[int64]](view Envelope[list[int64]](session: Session(ready, Payload(items: list(2))), extra: list(3))) +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let inspect = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(inspect.len(), 2); + for function in inspect { + let source = local(function, "source"); + let alias = local(function, "alias"); + assert_eq!(alias.view_source, Some(source.id)); + assert_eq!(local(function, "forwarded").view_source, Some(alias.id)); + let generic_alias = local(function, "generic_alias"); + if generic_alias.ty == TypeInterner::INT64 { + assert_eq!(generic_alias.view_source, None); + } else { + assert_eq!(generic_alias.ty, alias.ty); + assert_eq!(generic_alias.view_source, Some(source.id)); + } + } +} + +#[test] +fn unused_qualified_machine_local_views_require_exact_state_field_and_root_proofs() { + let (program, mut checked) = checked_source( + r#"namespace app +machine Session: + states: + ready(items: list[int64]) + cached(items: list[string]) + empty +machine Other: + states: + ready(items: list[int64]) +function inspect(view source: Session at ready, view other: Session at ready, view foreign: Other at ready) returns nothing: + list[int64] unused_alias = view source.items + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source").id; + let other = local(function, "other").id; + let foreign_type = local(function, "foreign").ty; + let alias = local(function, "unused_alias").id; + let source_type = local(function, "source").ty; + let Type::MachineState { machine, .. } = *checked.interner.resolve(source_type) else { + panic!("statically qualified root"); + }; + let cached = checked + .interner + .resolve_machine(machine) + .state_id("cached") + .unwrap(); + let empty = checked + .interner + .resolve_machine(machine) + .state_id("empty") + .unwrap(); + let cached_type = checked.interner.intern(Type::MachineState { + machine, + state: cached, + }); + let empty_type = checked.interner.intern(Type::MachineState { + machine, + state: empty, + }); + let stale_type = checked.interner.intern(Type::MachineState { + machine, + state: jett_types::MachineStateId::new(u32::MAX), + }); + let bare_type = checked.interner.intern(Type::Machine(machine)); + let different_items = checked.interner.intern(Type::List(TypeInterner::STRING)); + for (invalid, expected) in [ + ("stale state", "invalid machine state"), + ("different state", "invalid field endpoint type"), + ("empty state", "invalid field index"), + ("field index", "invalid field index"), + ("field endpoint", "invalid field endpoint type"), + ("field owner", "invalid field owner"), + ("source identity", "stable backing local"), + ("source type", "stable backing local"), + ("bare owner", "state-qualified machine fields"), + ("foreign machine", "stable backing local"), + ("orphan", "no validated initializer"), + ] { + let mut forged = program.clone(); + let function = forged + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + if invalid == "orphan" { + function.body.statements.remove(0); + } else { + if invalid == "source type" { + function.locals[source.index() as usize].ty = bare_type; + } + if invalid == "field endpoint" { + function.locals[alias.index() as usize].ty = different_items; + } + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!("unused machine field alias initializer"); + }; + if invalid == "field endpoint" { + value.ty = different_items; + } + let ExpressionKind::View(projection) = &mut value.kind else { + panic!("explicit field view"); + }; + if invalid == "field endpoint" { + projection.ty = different_items; + } + let ExpressionKind::Field { + base, + owner_type, + field, + } = &mut projection.kind + else { + panic!("checked machine field projection"); + }; + let changed_owner = match invalid { + "stale state" => Some(stale_type), + "different state" => Some(cached_type), + "empty state" => Some(empty_type), + "bare owner" => Some(bare_type), + "foreign machine" => Some(foreign_type), + _ => None, + }; + if let Some(changed_owner) = changed_owner { + *owner_type = changed_owner; + base.ty = changed_owner; + } + if invalid == "field owner" { + *owner_type = cached_type; + } + if invalid == "field index" { + *field = FieldId(u32::MAX); + } + if invalid == "source identity" { + base.kind = ExpressionKind::Local(other); + } + } + let errors = validate_backend_types(&forged, &checked.interner).unwrap_err(); + assert!( + errors.iter().any(|error| error.message.contains(expected)), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn secret_owner_local_views_preserve_exact_nested_field_qualification() { + let (program, checked) = checked_source( + r#"namespace app +type Values = list[int64] where true +type Hidden = secret[Values] where true +struct Payload: + items: Values + hidden: secret[Values] + nominal_hidden: Hidden + unit: nothing +struct Envelope: + payload: Payload +bitfield network Packet: + width: 8 bits + payload: list[uint8] +machine Session: + states: + ready(payload: Payload) + empty +function inspect(view source: secret[Envelope], view packet: secret[Packet], view session: secret[Session at ready]) returns nothing: + secret[Payload] aggregate = view source.payload + secret[Payload] forwarded_aggregate = view aggregate + secret[Values] refined = view forwarded_aggregate.items + Values revealed = declassify view forwarded_aggregate.items + list[int64] coarsened = coarsen declassify view forwarded_aggregate.items + secret[Values] already_hidden = view forwarded_aggregate.hidden + Hidden nominal_hidden = view forwarded_aggregate.nominal_hidden + nothing unit = view source.payload.unit + secret[list[uint8]] payload = view packet.payload + secret[Payload] machine_record = view session.payload + secret[Values] machine_items = view session.payload.items + Values owned = clone declassify refined + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source"); + let aggregate = local(function, "aggregate"); + let forwarded = local(function, "forwarded_aggregate"); + assert_eq!(aggregate.view_source, Some(source.id)); + assert_eq!(forwarded.view_source, Some(aggregate.id)); + for name in [ + "refined", + "revealed", + "coarsened", + "already_hidden", + "nominal_hidden", + ] { + assert_eq!( + local(function, name).view_source, + Some(forwarded.id), + "{name}" + ); + assert_eq!( + local_view_root(&function.locals, local(function, name).id), + Some(source.id) + ); + } + assert_eq!( + local(function, "payload").view_source, + Some(local(function, "packet").id) + ); + for name in ["machine_record", "machine_items"] { + assert_eq!( + local(function, name).view_source, + Some(local(function, "session").id), + "{name}" + ); + } + let values = local(function, "revealed").ty; + assert!( + matches!(checked.interner.resolve(local(function, "refined").ty), Type::Secret(inner) if *inner == values) + ); + assert_eq!( + local(function, "already_hidden").ty, + local(function, "refined").ty + ); + assert!( + matches!(checked.interner.resolve(local(function, "nominal_hidden").ty), Type::Refinement { name, base } if name == "app.Hidden" && *base == local(function, "refined").ty) + ); + assert_eq!(local(function, "unit").ty, TypeInterner::NOTHING); + assert_eq!(local(function, "unit").view_source, None); + assert_eq!(local(function, "owned").view_source, None); + let unit = local(function, "unit").id; + let value = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == unit => Some(value), + _ => None, + }) + .unwrap(); + validate_local_view_initializer( + value, + source.id, + source.ty, + TypeInterner::NOTHING, + &checked.interner, + ) + .unwrap(); +} + +#[test] +fn secret_owner_local_views_keep_generic_scalar_and_linear_binding_facts() { + let (program, checked) = checked_source( + r#"namespace app +struct Box[T]: + item: T +function inspect[T](view source: secret[Box[T]]) returns nothing: + secret[T] borrowed = view source.item + T revealed = declassify view source.item + return nothing +function main() returns nothing: + secret[Box[int64]] scalar = Box[int64](item: 7) + secret[Box[list[int64]]] aggregate = Box[list[int64]](item: list(1)) + secret[list[int64]] local_alias = view aggregate.item + list[int64] owned = clone declassify local_alias + inspect[int64](view scalar) + inspect[list[int64]](view aggregate) + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let functions = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(functions.len(), 2); + for function in functions { + let source = local(function, "source"); + let borrowed = local(function, "borrowed"); + let revealed = local(function, "revealed"); + assert!( + matches!(checked.interner.resolve(borrowed.ty), Type::Secret(inner) if *inner == revealed.ty) + ); + // Secret-qualified scalars retain the existing noncopyable source mode; + // only an explicitly declassified primitive has implicit copy facts. + assert_eq!(borrowed.view_source, Some(source.id)); + let expected = if revealed.ty == TypeInterner::INT64 { + None + } else { + Some(source.id) + }; + assert_eq!(revealed.view_source, expected); + } + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + assert_eq!( + local(main, "local_alias").view_source, + Some(local(main, "aggregate").id) + ); + assert_eq!(local(main, "owned").view_source, None); +} + +#[test] +fn unused_secret_owner_local_views_reject_forged_qualification_and_origins() { + let (program, mut checked) = checked_source( + r#"namespace app +type Proven = list[int64] where true +type Sibling = list[int64] where false +type Hidden = secret[Proven] where true +struct Payload: + items: Proven + hidden: secret[Proven] + nominal_hidden: Hidden + unit: nothing +struct Foreign: + items: Proven +function inspect(view source: secret[Payload], view other: secret[Payload], view foreign: secret[Foreign]) returns nothing: + secret[Proven] unused_alias = view source.items + return nothing +"#, + false, + ) + .unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = local(function, "source"); + let other = local(function, "other").id; + let foreign = local(function, "foreign").ty; + let alias = local(function, "unused_alias").id; + let secret_proven = local(function, "unused_alias").ty; + let Type::Secret(proven) = *checked.interner.resolve(secret_proven) else { + panic!("secret nominal endpoint") + }; + let Type::Secret(payload) = *checked.interner.resolve(source.ty) else { + panic!("direct secret owner") + }; + let Type::Secret(foreign_payload) = *checked.interner.resolve(foreign) else { + panic!("foreign direct secret owner") + }; + let Type::Refinement { + base: plain_list, .. + } = *checked.interner.resolve(proven) + else { + panic!("nominal field") + }; + let sibling = checked.interner.type_ids().find(|ty| matches!(checked.interner.resolve(*ty), Type::Refinement { name, .. } if name == "app.Sibling")).unwrap(); + let hidden = checked.interner.type_ids().find(|ty| matches!(checked.interner.resolve(*ty), Type::Refinement { name, .. } if name == "app.Hidden")).unwrap(); + let double_secret_endpoint = checked.interner.intern(Type::Secret(secret_proven)); + let secret_sibling = checked.interner.intern(Type::Secret(sibling)); + let secret_base = checked.interner.intern(Type::Secret(plain_list)); + let extra_nominal_secret = checked.interner.intern(Type::Secret(hidden)); + let secret_unit = checked.interner.intern(Type::Secret(TypeInterner::NOTHING)); + let double_secret_owner = checked.interner.intern(Type::Secret(source.ty)); + let source_id = source.id; + for (invalid, expected) in [ + ("drop secrecy", "invalid field endpoint type"), + ("extra endpoint secrecy", "invalid field endpoint type"), + ("already-secret double wrap", "invalid field endpoint type"), + ("nominal-secret extra wrap", "invalid field endpoint type"), + ("unit secret wrap", "invalid field endpoint type"), + ("sibling refinement", "invalid field endpoint type"), + ("discard refinement", "invalid field endpoint type"), + ("multiple owner secrets", "invalid field owner"), + ("unqualified owner", "invalid field endpoint type"), + ("field owner", "invalid field owner"), + ("field index", "invalid field index"), + ("source identity", "stable backing local"), + ("source type", "stable backing local"), + ("foreign owner", "stable backing local"), + ("orphan", "no validated initializer"), + ] { + let mut forged = program.clone(); + let function = forged + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + if invalid == "orphan" { + function.body.statements.remove(0); + } else { + if invalid == "source type" { + function.locals[source_id.index() as usize].ty = foreign; + } + let endpoint = match invalid { + "drop secrecy" => proven, + "extra endpoint secrecy" | "already-secret double wrap" => double_secret_endpoint, + "nominal-secret extra wrap" => extra_nominal_secret, + "unit secret wrap" => secret_unit, + "sibling refinement" => secret_sibling, + "discard refinement" => secret_base, + _ => secret_proven, + }; + function.locals[alias.index() as usize].ty = endpoint; + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!("unused field alias initializer") + }; + value.ty = endpoint; + let ExpressionKind::View(projection) = &mut value.kind else { + panic!("explicit secret field view") + }; + projection.ty = endpoint; + let ExpressionKind::Field { + base, + owner_type, + field, + } = &mut projection.kind + else { + panic!("exact field") + }; + match invalid { + "already-secret double wrap" => *field = FieldId(1), + "nominal-secret extra wrap" => *field = FieldId(2), + "unit secret wrap" => *field = FieldId(3), + "multiple owner secrets" => base.ty = double_secret_owner, + "unqualified owner" => base.ty = payload, + "field owner" => *owner_type = foreign_payload, + "field index" => *field = FieldId(u32::MAX), + "source identity" => base.kind = ExpressionKind::Local(other), + "foreign owner" => { + base.ty = foreign; + *owner_type = foreign_payload; + } + _ => {} + } + } + let errors = validate_backend_types(&forged, &checked.interner).unwrap_err(); + assert!( + errors.iter().any(|error| error.message.contains(expected)), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn local_views_accept_mutable_owned_backing_without_mutable_aliases() { + for (parameters, body) in [ + ("", "mutable Record source = Record(values: list(1))"), + ("mutable source: Record", ""), + ] { + let body = if body.is_empty() { + String::new() + } else { + format!(" {body}\n") + }; + let source = format!( + "namespace app\nstruct Record:\n values: list[int64]\nfunction inspect({parameters}) returns list[int64]:\n{body} list[int64] alias = view source.values\n list[int64] forwarded = alias\n return clone forwarded\n" + ); + let (program, checked) = checked_source(&source, false).unwrap(); + validate(&program).unwrap(); + validate_local_views(&program, &checked.interner).unwrap(); + let function = &program.functions[0]; + let root = local(function, "source"); + assert!(root.mutable); + assert!(root.view_source.is_none()); + assert_eq!(local(function, "alias").view_source, Some(root.id)); + assert_eq!( + local(function, "forwarded").view_source, + Some(local(function, "alias").id) + ); + for name in ["alias", "forwarded"] { + let mut changed = program.clone(); + let alias = local(&changed.functions[0], name).id; + changed.functions[0].locals[alias.index() as usize].mutable = true; + assert!( + validate_local_views(&changed, &checked.interner).is_err(), + "{name}" + ); + } + } +} + +#[test] +fn local_views_reject_consistently_mutable_view_parameter_backing() { + let (mut program, checked) = checked_source("namespace app\nfunction inspect(view source: list[int64]) returns nothing:\n list[int64] alias = view source\n trace alias\n return nothing\n", false).unwrap(); + let root = local(&program.functions[0], "source").id; + program.functions[0].locals[root.index() as usize].mutable = true; + program.functions[0].params[0].mutable = true; + let errors = validate_local_views(&program, &checked.interner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("immutable view-parameter backing")), + "{errors:?}" + ); +} + +#[test] +fn mutable_local_view_backing_requires_its_exact_selected_owned_fact() { + let source = "namespace app\nstruct Record:\n values: list[int64]\nfunction inspect() returns list[int64]:\n mutable Record source = Record(values: list(1))\n Record other = clone source\n list[int64] alias = view source.values\n return clone alias\n"; + for invalid in ["missing", "definition", "view", "type", "mutable"] { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let mut checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{:?}", + checked.diagnostics + ); + let root = *checked + .binding_facts + .iter() + .find(|(_, fact)| fact.mutable) + .unwrap() + .0; + let other = checked + .binding_facts + .values() + .find(|fact| fact.declaration_span != root && fact.mode == CheckedBindingMode::Owned) + .unwrap() + .definition; + if invalid == "missing" { + checked.binding_facts.remove(&root); + } else { + let fact = checked.binding_facts.get_mut(&root).unwrap(); + match invalid { + "definition" => fact.definition = other, + "view" => { + fact.mode = CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } + "type" => fact.ty = TypeInterner::STRING, + "mutable" => fact.mutable = false, + _ => unreachable!(), + } + } + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let errors = lower(&parsed.module, &resolved, &checked, &origins).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("stable owned backing")), + "{invalid}: {errors:?}" + ); + } +} + +const SCOPED_MUTABLE_ALIAS_SOURCE: &str = r#"namespace app +struct Holder[T]: + value: T +function inspect[T](mutable source: Holder[T]) returns nothing: + comptime type Bound = type.info[T](): + T alias = view source.value + T forwarded = alias + trace forwarded + return nothing +function main() returns nothing: + inspect[int64](Holder[int64](value: 7)) + inspect[list[int64]](Holder[list[int64]](value: list(2, 3))) +"#; + +#[test] +fn reflected_mutable_aliases_use_only_their_concrete_scoped_root_facts() { + let (program, checked) = checked_source(SCOPED_MUTABLE_ALIAS_SOURCE, false).unwrap(); + validate_backend_types(&program, &checked.interner).unwrap(); + let functions = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "inspect") + .collect::>(); + assert_eq!(functions.len(), 2); + for function in functions { + let root = local(function, "source"); + let alias = local(function, "alias"); + let forwarded = local(function, "forwarded"); + assert!(root.mutable); + if alias.ty == TypeInterner::INT64 { + assert!(alias.view_source.is_none()); + assert!(forwarded.view_source.is_none()); + } else { + assert_eq!(alias.view_source, Some(root.id)); + assert_eq!(forwarded.view_source, Some(alias.id)); + } + } +} + +#[test] +fn reflected_mutable_aliases_never_rescue_missing_or_foreign_selected_root_facts() { + for invalid in [ + "missing", + "definition", + "declaration", + "type", + "mutable", + "mode", + "cross instance", + ] { + let file = FileId::new(0); + let parsed = jett_parser::parse(SCOPED_MUTABLE_ALIAS_SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let mut checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{:?}", + checked.diagnostics + ); + let scalar = checked + .generic_function_instantiations + .iter() + .find(|body| body.concrete_args == [TypeInterner::INT64]) + .unwrap(); + let foreign = *scalar + .binding_facts + .values() + .find(|fact| fact.mutable) + .unwrap(); + let target = checked + .generic_function_instantiations + .iter_mut() + .find(|body| body.concrete_args != [TypeInterner::INT64]) + .unwrap(); + let original = *target + .binding_facts + .values() + .find(|fact| fact.mutable) + .unwrap(); + assert_eq!(original.definition, foreign.definition); + assert_eq!(original.declaration_span, foreign.declaration_span); + assert_ne!(original.ty, foreign.ty); + let selected = &mut target.comptime_type_bindings.values_mut().next().unwrap()[0].body; + assert_eq!( + selected.binding_facts.get(&original.declaration_span), + Some(&original) + ); + let alias = *selected + .binding_facts + .values() + .find(|fact| !fact.mutable) + .unwrap(); + if invalid == "missing" { + selected.binding_facts.remove(&original.declaration_span); + } else { + let fact = selected + .binding_facts + .get_mut(&original.declaration_span) + .unwrap(); + match invalid { + "definition" => fact.definition = alias.definition, + "declaration" => fact.declaration_span = alias.declaration_span, + "type" => fact.ty = TypeInterner::STRING, + "mutable" => fact.mutable = false, + "mode" => { + fact.mode = CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } + "cross instance" => *fact = foreign, + _ => unreachable!(), + } + } + // The valid enclosing concrete map must never rescue the edited child. + assert_eq!( + target.binding_facts.get(&original.declaration_span), + Some(&original) + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let errors = lower(&parsed.module, &resolved, &checked, &origins).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("stable owned backing")), + "{invalid}: {errors:?}" + ); + } +} diff --git a/crates/jett_hir/src/local_views.rs b/crates/jett_hir/src/local_views.rs new file mode 100644 index 00000000..05048f76 --- /dev/null +++ b/crates/jett_hir/src/local_views.rs @@ -0,0 +1,457 @@ +//! Checked local borrow origins. Immutable aliases preserve their backing value. +//! Mutable terminal owners remain subject to persistent loan validation. + +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::{ + Block, Expression, ExpressionKind, Function, HandleKind, Local, LocalId, Param, ParamMode, + Program, StatementKind, ValidationError, +}; + +/// Resolve an alias to its backing local. Invalid IDs and cycles have no root. +pub fn local_view_root(locals: &[Local], mut id: LocalId) -> Option { + for _ in 0..locals.len() { + let local = locals + .get(id.index() as usize) + .filter(|local| local.id == id)?; + match local.view_source { + Some(source) => id = source, + None => return Some(id), + } + } + None +} + +pub fn is_borrowed_local(locals: &[Local], params: &[Param], id: LocalId) -> bool { + locals + .get(id.index() as usize) + .is_some_and(|local| local.id == id && local.view_source.is_some()) + || params + .iter() + .any(|param| param.local == id && param.mode == ParamMode::View) +} + +/// Resolve a finite chain whose alias slots are immutable. The terminal +/// binding's ownership and readiness are checked separately. +pub(super) fn immutable_alias_root(locals: &[Local], mut id: LocalId) -> Option { + for _ in 0..locals.len() { + let local = locals + .get(id.index() as usize) + .filter(|local| local.id == id)?; + match local.view_source { + Some(source) if !local.mutable => id = source, + Some(_) => return None, + None => return Some(id), + } + } + None +} + +pub(super) fn validate_structure(function: &Function) -> Vec { + let mut errors = Vec::new(); + for local in &function.locals { + if local.view_source.is_none() { + continue; + } + let message = if function + .actor_state_views + .iter() + .any(|view| view.local() == local.id && view.matches_constructor(function)) + || function + .actor_handler + .as_ref() + .is_some_and(|entry| entry.allows_state_view(function, local.id)) + { + None + } else if function.params.iter().any(|param| param.local == local.id) { + Some("parameter ownership must use ParamMode, not a local alias origin") + } else if local_view_root(&function.locals, local.id).is_none() { + Some("borrowed local origin is outside its function or contains a cycle") + } else if let Some(root) = immutable_alias_root(&function.locals, local.id) { + function + .params + .iter() + .any(|parameter| { + parameter.local == root + && parameter.mode == ParamMode::View + && function.locals[root.index() as usize].mutable + }) + .then_some("native borrowed alias requires immutable view-parameter backing") + } else { + Some("native borrowed alias requires immutable alias bindings") + }; + if let Some(message) = message { + errors.push(ValidationError { + span: local.span, + message: message.into(), + }); + } + } + errors +} + +/// Validate dependency metadata after transformations or LocalId remapping. +/// Full backend validation proves each alias's typed initializer and rejects +/// borrowed metadata without an initializer. A projected endpoint can differ +/// from the type of its whole backing local. +pub fn validate_local_views( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + for function in &program.functions { + errors.extend(validate_structure(function)); + for local in &function.locals { + if local.view_source.is_some() + && (local.ty.index() as usize) < types.len() + && jett_typecheck::ownership::is_implicitly_copyable(types, local.ty) + { + errors.push(ValidationError { + span: local.span, + message: "implicitly copyable locals cannot carry a borrowed alias origin" + .into(), + }); + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +fn backing_type(types: &TypeInterner, mut ty: TypeId) -> Option { + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Secret(inner) | Type::Refinement { base: inner, .. } => ty = *inner, + _ => return Some(ty), + } + } + None +} + +fn erased_interface(types: &TypeInterner, mut ty: TypeId) -> Option { + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Secret(inner) => ty = *inner, + Type::Interface(_) => return Some(true), + _ => return Some(false), + } + } + None +} + +fn same_backing_type(types: &TypeInterner, left: TypeId, right: TypeId) -> bool { + let Some(left_backing) = backing_type(types, left) else { + return false; + }; + backing_type(types, right) == Some(left_backing) + && erased_interface(types, left) == erased_interface(types, right) +} + +/// Secrecy can be added without acquiring a new nominal predicate proof. +fn secret_promotion(types: &TypeInterner, source: TypeId, mut target: TypeId) -> bool { + if source.index() as usize >= types.len() { + return false; + } + for _ in 0..types.len() { + if target.index() as usize >= types.len() { + return false; + } + if source == target { + return true; + } + let Type::Secret(inner) = types.resolve(target) else { + return false; + }; + target = *inner; + } + false +} + +fn refinement_ancestor(types: &TypeInterner, mut source: TypeId, target: TypeId) -> bool { + for _ in 0..types.len() { + if source.index() as usize >= types.len() { + return false; + } + let Type::Refinement { base, .. } = types.resolve(source) else { + return false; + }; + source = *base; + if source == target { + return true; + } + } + false +} + +/// Match the checker's already-secret exception without discarding a field's +/// nominal refinement identity or peeling the aggregate receiver's wrappers. +fn declared_field_is_secret(types: &TypeInterner, mut ty: TypeId) -> Option { + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Secret(_) => return Some(true), + Type::Refinement { base, .. } => ty = *base, + _ => return Some(false), + } + } + None +} + +pub(crate) fn validate_field_projection( + value: &Expression, + types: &TypeInterner, +) -> Result<(), &'static str> { + let ExpressionKind::Field { + base, + owner_type, + field, + } = &value.kind + else { + return Err("borrowed projection metadata does not describe a field"); + }; + if owner_type.index() as usize >= types.len() || base.ty.index() as usize >= types.len() { + return Err("native borrowed projection has an invalid field owner"); + } + let secret_owner = matches!( + types.resolve(base.ty), + Type::Secret(inner) + if *inner == *owner_type + && matches!( + types.resolve(*inner), + Type::Struct(_) | Type::Bitfield(_) | Type::MachineState { .. } + ) + ); + if base.ty != *owner_type && !secret_owner { + return Err("native borrowed projection has an invalid field owner"); + } + let field_type = match types.resolve(*owner_type) { + Type::Struct(struct_id) => types + .resolve_struct(*struct_id) + .fields + .get(field.index() as usize) + .map(|(_, ty)| *ty), + Type::Bitfield(bitfield_id) => types + .resolve_bitfield(*bitfield_id) + .fields + .get(field.index() as usize) + .map(|field| field.ty), + Type::MachineState { machine, state } => { + let Some(state) = types.resolve_machine(*machine).state(*state) else { + return Err("native borrowed projection has an invalid machine state"); + }; + state.fields.get(field.index() as usize).map(|(_, ty)| *ty) + } + _ => { + return Err( + "native borrowed projection requires ordinary struct, bitfield or state-qualified machine fields", + ); + } + }; + let Some(field_type) = field_type else { + return Err("native borrowed projection has an invalid field index"); + }; + let Some(already_secret) = declared_field_is_secret(types, field_type) else { + return Err("native borrowed projection has an invalid field endpoint type"); + }; + let valid_result = if secret_owner && field_type != TypeInterner::NOTHING && !already_secret { + matches!(types.resolve(value.ty), Type::Secret(inner) if *inner == field_type) + } else { + value.ty == field_type + }; + if !valid_result { + return Err("native borrowed projection has an invalid field endpoint type"); + } + Ok(()) +} + +pub fn validate_local_view_initializer( + value: &Expression, + source: LocalId, + source_type: TypeId, + target: TypeId, + types: &TypeInterner, +) -> Result<(), &'static str> { + if borrowed_sum_view_initializer(value, source, source_type, target, types)?.is_some() { + return Ok(()); + } + if !same_backing_type(types, value.ty, target) { + return Err("native borrowed alias cannot change its backing representation"); + } + if !secret_promotion(types, value.ty, target) { + return Err("native borrowed alias cannot introduce or discard a nominal refinement"); + } + let mut value = value; + loop { + match &value.kind { + ExpressionKind::Local(id) + if *id == source && secret_promotion(types, source_type, value.ty) => + { + return Ok(()); + } + ExpressionKind::Field { base, .. } => { + validate_field_projection(value, types)?; + value = base; + } + ExpressionKind::View(inner) => { + if !same_backing_type(types, value.ty, inner.ty) { + return Err("native borrowed alias cannot change its backing representation"); + } + if !secret_promotion(types, inner.ty, value.ty) { + return Err( + "native borrowed alias cannot introduce or discard a nominal refinement", + ); + } + value = inner; + } + ExpressionKind::Coarsen(inner) => { + if !same_backing_type(types, value.ty, inner.ty) + || !refinement_ancestor(types, inner.ty, value.ty) + { + return Err( + "native borrowed alias coarsen requires an existing refinement ancestor", + ); + } + value = inner; + } + ExpressionKind::Declassify(inner) => { + if !same_backing_type(types, value.ty, inner.ty) + || inner.ty.index() as usize >= types.len() + || !matches!(types.resolve(inner.ty), Type::Secret(payload) if *payload == value.ty) + { + return Err( + "native borrowed alias declassification requires the exact secret inner type", + ); + } + value = inner; + } + ExpressionKind::InterfaceCoerce { + value: inner, + adapters, + } if adapters.is_empty() => { + if !same_backing_type(types, value.ty, inner.ty) { + return Err( + "native borrowed alias cannot allocate an interface or container conversion", + ); + } + if !secret_promotion(types, inner.ty, value.ty) { + return Err( + "native borrowed alias conversion cannot introduce or discard a nominal refinement", + ); + } + value = inner; + } + _ => { + return Err( + "native borrowed alias initializer must preserve its stable backing local", + ); + } + } + } +} + +/// Read the exact typed borrowed Handle projection. This structural check does +/// not grant custody authority: Resource consumers must additionally join the +/// privately archived original occurrence and their constructor-owned CFG. +pub fn borrowed_sum_view_initializer<'a>( + value: &'a Expression, + source: LocalId, + source_type: TypeId, + endpoint: TypeId, + types: &TypeInterner, +) -> Result, &'static str> { + let ExpressionKind::View(handle) = &value.kind else { + return Ok(None); + }; + let ExpressionKind::Handle { + target, + kind, + error_local, + failure, + } = &handle.kind + else { + return Ok(None); + }; + if source_type.index() as usize >= types.len() + || endpoint.index() as usize >= types.len() + || value.ty != endpoint + || handle.ty != endpoint + { + return Err("native borrowed Handle changed its exact payload endpoint"); + } + let payload = match (types.resolve(source_type), kind, error_local) { + (Type::Optional(payload), HandleKind::Optional, None) + | (Type::Result(payload, _), HandleKind::Result, Some(_)) => *payload, + _ => return Err("native borrowed Handle kind differs from its backing sum"), + }; + if payload != endpoint { + return Err("native borrowed Handle changed its exact payload endpoint"); + } + let ExpressionKind::View(backing) = &target.kind else { + return Err("native borrowed Handle requires its original written target view"); + }; + if target.ty != source_type + || backing.ty != source_type + || !matches!(backing.kind, ExpressionKind::Local(id) if id == source) + { + return Err("native borrowed Handle lost its exact stable backing local"); + } + if handle_body_has_default(failure) { + return Err("native borrowed Handle cannot yield an alternate default payload"); + } + if !matches!( + failure.statements.last().map(|statement| &statement.kind), + Some(StatementKind::Return(_)) + ) { + return Err("native borrowed Handle failure must exit without yielding a payload"); + } + Ok(Some(handle)) +} + +/// Only lexical blocks share this Handle's continuation. Expression-level +/// nested Handles and inline function bodies have their own nearest handler. +fn handle_body_has_default(body: &Block) -> bool { + body.statements + .iter() + .any(|statement| match &statement.kind { + StatementKind::HandleDefault(_) => true, + StatementKind::If { + then_block, + else_block, + .. + } => { + handle_body_has_default(then_block) + || else_block.as_ref().is_some_and(handle_body_has_default) + } + StatementKind::While { body, .. } + | StatementKind::For { body, .. } + | StatementKind::Scope(body) => handle_body_has_default(body), + StatementKind::Match { arms, .. } => { + arms.iter().any(|arm| handle_body_has_default(&arm.body)) + } + StatementKind::ReflectedTypeDispatch { arms, .. } => { + arms.iter().any(|arm| handle_body_has_default(&arm.body)) + } + StatementKind::Let { .. } + | StatementKind::Assign { .. } + | StatementKind::UpdateField { .. } + | StatementKind::Return(_) + | StatementKind::Expression(_) + | StatementKind::Respond(_) + | StatementKind::Break + | StatementKind::Continue + | StatementKind::Assert { .. } + | StatementKind::Trace(_) + | StatementKind::Breakpoint { .. } => false, + }) +} diff --git a/crates/jett_hir/src/match_bindings.rs b/crates/jett_hir/src/match_bindings.rs new file mode 100644 index 00000000..845554ea --- /dev/null +++ b/crates/jett_hir/src/match_bindings.rs @@ -0,0 +1,144 @@ +//! Canonical enum payload binding origins, scoped to one actual match arm. +//! A match binding is not an ABI parameter or an ownership permission. +use jett_common::Span; +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::{LocalId, OwnershipLocalInfo, VariantId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MatchBinding { + match_span: Span, + enum_type: TypeId, + variant: VariantId, + field_index: usize, + binder_type: TypeId, + declaration_span: Span, +} + +impl MatchBinding { + pub fn match_span(self) -> Span { + self.match_span + } + pub fn enum_type(self) -> TypeId { + self.enum_type + } + pub fn variant(self) -> VariantId { + self.variant + } + pub fn field_index(self) -> usize { + self.field_index + } + pub fn binder_type(self) -> TypeId { + self.binder_type + } + pub fn declaration_span(self) -> Span { + self.declaration_span + } + pub(crate) fn matches_binding(self, ty: TypeId, declaration: Span) -> bool { + self.binder_type == ty && self.declaration_span == declaration + } +} + +fn inside(child: Span, parent: Span) -> bool { + child.file == parent.file && child.start >= parent.start && child.end <= parent.end +} + +/// Prove the canonical enum/tag/field relation. Consumers separately establish +/// the exact LocalId, original/current match association and arm body scope. +pub fn checked_match_binding( + types: &TypeInterner, + match_span: Span, + enum_type: TypeId, + variant: VariantId, + field_index: usize, + binder_type: TypeId, + declaration_span: Span, +) -> Result { + if enum_type == TypeInterner::ERROR + || binder_type == TypeInterner::ERROR + || enum_type.index() as usize >= types.len() + || binder_type.index() as usize >= types.len() + || !inside(declaration_span, match_span) + { + return Err("match binding has an invalid exact type or Source association".into()); + } + let Type::Enum(id) = types.resolve(enum_type) else { + return Err("match binding owner is not an exact enum".into()); + }; + let field = types + .resolve_enum(*id) + .variants + .get(variant.index() as usize) + .and_then(|variant| variant.fields.get(field_index)) + .ok_or("match binding tag or field slot is outside its canonical enum")?; + if field.1 != binder_type { + return Err("match binding type differs from its canonical payload field".into()); + } + Ok(MatchBinding { + match_span, + enum_type, + variant, + field_index, + binder_type, + declaration_span, + }) +} + +pub(crate) fn checked_match_arm_bindings( + types: &TypeInterner, + match_span: Span, + enum_type: TypeId, + variant: Option, + arm_span: Span, + bindings: &[LocalId], + local: impl Fn(LocalId) -> Option, +) -> Result, String> { + if bindings.is_empty() { + return Ok(Vec::new()); + } + let variant = variant.ok_or("catch-all match arm cannot declare enum payload bindings")?; + if enum_type == TypeInterner::ERROR || enum_type.index() as usize >= types.len() { + return Err("match binding has an invalid exact enum type".into()); + } + let Type::Enum(id) = types.resolve(enum_type) else { + return Err("match binding owner is not an exact enum".into()); + }; + let fields = &types + .resolve_enum(*id) + .variants + .get(variant.index() as usize) + .ok_or("match binding tag is outside its exact enum")? + .fields; + if bindings.len() != fields.len() || !inside(arm_span, match_span) { + return Err("match bindings differ from their exact canonical arm".into()); + } + let mut result = Vec::new(); + for (field_index, &id) in bindings.iter().enumerate() { + if bindings[..field_index].contains(&id) { + return Err("match payload reuses another binding slot".into()); + } + let metadata = local(id).ok_or("match binding is outside its exact function")?; + if metadata.mutable + || metadata.view_source.is_some() + || metadata.is_view_parameter + || metadata.view_iteration.is_some() + || metadata.match_binding.is_some() + || !inside(metadata.span, arm_span) + { + return Err("match payload is not a fresh immutable binding in its exact arm".into()); + } + let proof = checked_match_binding( + types, + match_span, + enum_type, + variant, + field_index, + metadata.ty, + metadata.span, + )?; + if !jett_typecheck::ownership::is_implicitly_copyable(types, metadata.ty) { + result.push((id, proof)); + } + } + Ok(result) +} diff --git a/crates/jett_hir/src/mutable_sum_backings.rs b/crates/jett_hir/src/mutable_sum_backings.rs new file mode 100644 index 00000000..ffd0ec63 --- /dev/null +++ b/crates/jett_hir/src/mutable_sum_backings.rs @@ -0,0 +1,656 @@ +//! Original checked mutable owners for canonical borrowed Handle declarations. +//! This proof supplies no loan expiry, Resource custody or alternate Default. +use super::*; +use std::sync::Arc; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub struct MutableSumBacking { + pub(crate) alias: LocalId, + backing: LocalId, + alias_fact: CheckedBindingFact, + backing_fact: CheckedBindingFact, + source_initializer: Expression, + initializer: Expression, + required_scopes: Vec, + scoped_types: Vec, + producer: Arc<()>, + original: Option>, +} +impl PartialEq for MutableSumBacking { + fn eq(&self, other: &Self) -> bool { + self.alias == other.alias + && self.backing == other.backing + && self.alias_fact == other.alias_fact + && self.backing_fact == other.backing_fact + && self.required_scopes == other.required_scopes + && self.scoped_types == other.scoped_types + && Arc::ptr_eq(&self.producer, &other.producer) + && resource_materialization::expression_equal( + &self.source_initializer, + &other.source_initializer, + ) + && resource_materialization::expression_equal(&self.initializer, &other.initializer) + && match (&self.original, &other.original) { + (None, None) => true, + (Some(a), Some(b)) => resource_materialization::functions_equal( + std::slice::from_ref(a.as_ref()), + std::slice::from_ref(b.as_ref()), + ), + _ => false, + } + } +} +fn snapshot(function: &Function) -> Function { + let mut result = function.clone(); + result.mutable_sum_backings.clear(); + // These independent receipts renew at their own authenticated transitions. + // This snapshot pins the real function identity/header/locals/operations. + result.actor_handler = None; + result.actor_state_views.clear(); + result +} +impl MutableSumBacking { + fn current(&self, function: &Function) -> Result<(), String> { + let original = self + .original + .as_ref() + .ok_or("mutable sum backing has no original function")?; + if !resource_materialization::functions_equal( + std::slice::from_ref(original.as_ref()), + &[snapshot(function)], + ) { + return Err("mutable sum backing changed its original function context or body".into()); + } + let alias = function + .locals + .get(self.alias.index() as usize) + .filter(|local| local.id == self.alias) + .ok_or("mutable sum alias is missing")?; + let backing = function + .locals + .get(self.backing.index() as usize) + .filter(|local| local.id == self.backing) + .ok_or("mutable sum owner is missing")?; + if !backing.mutable + || backing.view_source.is_some() + || self.backing_fact.mode != CheckedBindingMode::Owned + || !self.backing_fact.mutable + || backing.ty != self.backing_fact.ty + || backing.span.file != self.backing_fact.declaration_span.file + || backing.span.start > self.backing_fact.declaration_span.start + || backing.span.end < self.backing_fact.declaration_span.end + || function + .params + .iter() + .any(|param| param.local == self.backing && param.mode != ParamMode::Owned) + || alias.ty != self.alias_fact.ty + || alias.mutable != self.alias_fact.mutable + || alias.span.file != self.alias_fact.declaration_span.file + || alias.span.start > self.alias_fact.declaration_span.start + || alias.span.end < self.alias_fact.declaration_span.end + { + return Err("mutable sum backing lost its exact checked owned binding".into()); + } + let expected_source = match self.alias_fact.mode { + CheckedBindingMode::Owned => None, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(source), + } if source == self.backing_fact.definition && !alias.mutable => Some(self.backing), + _ => return Err("mutable sum alias changed its checked acquisition".into()), + }; + if alias.view_source != expected_source { + return Err("mutable sum alias changed its checked acquisition".into()); + } + Ok(()) + } +} + +impl Function { + /// Consumed only with the actual original canonical Let initializer. + pub fn mutable_sum_backing( + &self, + alias: LocalId, + initializer: &Expression, + backing: LocalId, + ) -> Result, String> { + validate_function(self)?; + let mut matches = self + .mutable_sum_backings + .iter() + .filter(|proof| proof.alias == alias); + let Some(proof) = matches.next() else { + return Ok(None); + }; + if matches.next().is_some() + || proof.backing != backing + || !resource_materialization::expression_equal(&proof.initializer, initializer) + { + return Err("mutable sum backing differs from its exact original declaration".into()); + } + Ok(Some(proof)) + } +} + +impl BodyLowerer<'_, '_> { + /// Run for both final Owned copies and true View aliases. Neither the + /// absence of view_source nor a mutable header establishes ownership. + pub(super) fn record_mutable_sum_backing( + &mut self, + declaration: &ast::VarDecl, + definition: DefId, + alias: LocalId, + value: &Expression, + ) -> Result<(), String> { + let ExpressionKind::View(handle) = &value.kind else { + return Ok(()); + }; + let ExpressionKind::Handle { target, .. } = &handle.kind else { + return Ok(()); + }; + let ExpressionKind::View(source) = &target.kind else { + return Ok(()); + }; + let ExpressionKind::Local(backing) = source.kind else { + return Ok(()); + }; + let header = self + .locals + .get(backing.index() as usize) + .filter(|local| local.id == backing) + .ok_or("mutable sum backing is missing")?; + if !header.mutable { + return Ok(()); + } + if !matches!( + self.parent.check.interner.resolve(header.ty), + Type::Optional(_) | Type::Result(_, _) + ) { + return Ok(()); + } + local_views::borrowed_sum_view_initializer( + value, + backing, + header.ty, + value.ty, + &self.parent.check.interner, + )? + .ok_or("mutable sum backing lost its canonical Handle")?; + if !self.mutable_alias_backing_is_owned(backing) { + return Err("mutable borrowed Handle lacks its exact selected owned backing".into()); + } + let root_definition = self + .local_ids + .iter() + .find_map(|(definition, local)| (*local == backing).then_some(*definition)) + .ok_or("mutable sum root has no original definition")?; + let root_span = self.parent.resolve.scope_table.def(root_definition).span; + let backing_fact = *self + .binding_facts + .get(&root_span) + .ok_or("mutable sum root has no selected fact")?; + let alias_fact = *self + .binding_facts + .get(&declaration.name.span) + .filter(|fact| { + fact.definition == definition + && fact.declaration_span == declaration.name.span + && fact.ty == value.ty + && fact.mutable == declaration.mutable + }) + .ok_or("mutable sum alias has no exact selected binding fact")?; + let copyable = jett_typecheck::ownership::is_implicitly_copyable( + &self.parent.check.interner, + value.ty, + ); + let mode = if copyable { + CheckedBindingMode::Owned + } else { + CheckedBindingMode::View { + source: CheckedViewSource::Binding(root_definition), + } + }; + if alias_fact.mode != mode || (!copyable && declaration.mutable) { + return Err("mutable sum alias lost its checked acquisition mode".into()); + } + self.mutable_sum_backings.push(MutableSumBacking { + alias, + backing, + alias_fact, + backing_fact, + source_initializer: value.clone(), + initializer: value.clone(), + required_scopes: self.required_scopes.clone(), + scoped_types: self.scoped_type_bindings.clone(), + producer: Arc::new(()), + original: None, + }); + Ok(()) + } +} + +fn validate_function(function: &Function) -> Result<(), String> { + for (index, proof) in function.mutable_sum_backings.iter().enumerate() { + if function.mutable_sum_backings[..index] + .iter() + .any(|old| old.alias == proof.alias) + { + return Err("mutable sum backing duplicated an original declaration".into()); + } + proof.current(function)?; + } + Ok(()) +} +pub(super) fn validate_program(program: &Program) -> Result<(), Vec> { + for function in &program.functions { + validate_function(function).map_err(|message| { + vec![ValidationError { + span: function.span, + message, + }] + })?; + } + Ok(()) +} +fn prepared_initializer( + function: &Function, + proof: &MutableSumBacking, + types: &TypeInterner, +) -> Result { + let value = physical_initializer(function, proof)?; + local_views::borrowed_sum_view_initializer( + &value, + proof.backing, + proof.backing_fact.ty, + proof.alias_fact.ty, + types, + )? + .ok_or("mutable sum backing lost its canonical prepared initializer")?; + Ok(value) +} +fn physical_initializer( + function: &Function, + proof: &MutableSumBacking, +) -> Result { + let matches = declarations(&function.body) + .into_iter() + .filter(|(local, _)| *local == proof.alias) + .collect::>(); + let [(.., value)] = matches.as_slice() else { + return Err("mutable sum backing lost its unique physical declaration".into()); + }; + Ok((**value).clone()) +} +pub(super) fn seal_program( + program: &mut Program, + types: &TypeInterner, +) -> Result<(), Vec> { + for function in &mut program.functions { + let prepared = function + .mutable_sum_backings + .iter() + .map(|proof| prepared_initializer(function, proof, types)) + .collect::, _>>() + .map_err(|message| { + vec![LowerError { + span: function.span, + message, + }] + })?; + let original = Arc::new(snapshot(function)); + for (proof, value) in function.mutable_sum_backings.iter_mut().zip(prepared) { + if proof.original.is_none() { + proof.initializer = value; + proof.original = Some(original.clone()); + } + } + validate_function(function).map_err(|message| { + vec![LowerError { + span: function.span, + message, + }] + })?; + } + Ok(()) +} +pub(super) fn conversion( + program: &mut Program, + before: &Program, + types: &TypeInterner, + conversion: &interface_values::CanonicalValueConversion<'_>, +) -> Result<(), Vec> { + validate_program(before).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + let expected = conversion.replay(before, types)?; + for original in &before.functions { + if original.mutable_sum_backings.is_empty() { + continue; + } + let fail = || { + vec![LowerError { + span: original.span, + message: "mutable sum backing conversion changed its original proof or operations" + .into(), + }] + }; + let replay = expected + .functions + .iter() + .find(|function| function.id == original.id) + .ok_or_else(fail)?; + let current = program + .functions + .iter_mut() + .find(|function| function.id == original.id) + .ok_or_else(fail)?; + if current.mutable_sum_backings != original.mutable_sum_backings + || !resource_materialization::functions_equal(&[snapshot(current)], &[snapshot(replay)]) + { + return Err(fail()); + } + let updated = Arc::new(snapshot(replay)); + let prepared = current + .mutable_sum_backings + .iter() + .map(|proof| prepared_initializer(replay, proof, types)) + .collect::, _>>() + .map_err(|_| fail())?; + for (proof, value) in current.mutable_sum_backings.iter_mut().zip(prepared) { + proof.initializer = value; + proof.original = Some(updated.clone()); + } + } + validate_program(program).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect() + }) +} + +/// Only reflected_callable::seal_program supplies these exact old/new tokens. +/// Replay the successful private sealing operation; public current metadata is +/// never used as replacement authority, and callable identity remains exact. +pub(super) fn callable_sealing( + program: &mut Program, + before: &Program, + edits: &[(ReflectedCallablePlan, ReflectedCallablePlan)], +) -> Result<(), Vec> { + fn replace(value: &mut Expression, edits: &[(ReflectedCallablePlan, ReflectedCallablePlan)]) { + let replace_plan = |plan: &mut ReflectedCallablePlan| { + if let Some((_, sealed)) = edits.iter().find(|(previous, _)| previous == &*plan) { + *plan = sealed.clone(); + } + }; + match &mut value.kind { + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } => { + for plan in plans { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + replace_plan(callable); + } + } + } + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(callable), + .. + }, + .. + } => replace_plan(callable), + _ => {} + } + } + for original in &before.functions { + if original.mutable_sum_backings.is_empty() + || original + .mutable_sum_backings + .iter() + .all(|proof| proof.original.is_none()) + { + // The initial genuine lowering is sealed after this pass finishes. + continue; + } + let fail = || { + vec![LowerError { + span: original.span, + message: "mutable sum backing changed outside original callable sealing".into(), + }] + }; + validate_function(original).map_err(|_| fail())?; + let current = program + .functions + .iter_mut() + .find(|function| function.id == original.id) + .ok_or_else(fail)?; + if current.mutable_sum_backings != original.mutable_sum_backings { + return Err(fail()); + } + let mut expected = snapshot(original); + resource_materialization::walk_block(&mut expected.body, &mut |value| { + replace(value, edits); + Ok(()) + }) + .map_err(|_| fail())?; + if !resource_materialization::functions_equal(&[expected], &[snapshot(current)]) { + return Err(fail()); + } + let renewed = Arc::new(snapshot(current)); + let prepared = current + .mutable_sum_backings + .iter() + .map(|proof| physical_initializer(current, proof)) + .collect::, _>>() + .map_err(|_| fail())?; + for (proof, value) in current.mutable_sum_backings.iter_mut().zip(prepared) { + proof.initializer = value; + proof.original = Some(renewed.clone()); + } + validate_function(current).map_err(|_| fail())?; + } + Ok(()) +} + +fn declarations(block: &crate::Block) -> Vec<(LocalId, &Expression)> { + fn expression<'a>(value: &'a Expression, output: &mut Vec<(LocalId, &'a Expression)>) { + use crate::ExpressionKind as E; + match &value.kind { + E::Binary { left, right, .. } => { + expression(left, output); + expression(right, output); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } + | E::Field { base: value, .. } => expression(value, output), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + expression(value, output); + } + } + E::IndirectCall { callee, args, .. } => { + expression(callee, output); + for value in args { + expression(value, output); + } + } + E::ActorMessage { actor, args, .. } => { + expression(actor, output); + for value in args { + expression(value, output); + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + expression(value, output); + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + expression(value, output); + } + } + E::MachineTransition { + source, payloads, .. + } => { + expression(source, output); + for value in payloads { + expression(value, output); + } + } + E::ListConstruct { elements } => { + for value in elements { + expression(value, output); + } + } + E::MapConstruct { entries } => { + for entry in entries { + expression(&entry.key, output); + expression(&entry.value, output); + } + } + E::Handle { + target, failure, .. + } => { + expression(target, output); + walk(failure, output); + } + E::StringInterpolation(parts) => { + for part in parts { + if let crate::StringSegment::Value(value) = part { + expression(value, output); + } + } + } + E::ActorGraphImport(_) + | E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + } + fn walk<'a>(block: &'a crate::Block, output: &mut Vec<(LocalId, &'a Expression)>) { + use crate::StatementKind as S; + for statement in &block.statements { + match &statement.kind { + S::UpdateField { + target, + value, + place: _, + } => { + expression(target, output); + expression(value, output); + } + S::Assign { target, value } => { + expression(target, output); + expression(value, output); + } + S::Let { local, value } => { + output.push((*local, value)); + expression(value, output); + } + S::HandleDefault(value) | S::Expression(value) | S::Respond(value) => { + expression(value, output) + } + S::Return(value) => { + if let Some(value) = value { + expression(value, output); + } + } + S::If { + condition, + then_block, + else_block, + } => { + expression(condition, output); + walk(then_block, output); + if let Some(block) = else_block { + walk(block, output); + } + } + S::While { condition, body } => { + expression(condition, output); + walk(body, output); + } + S::For { iterable, body, .. } => { + expression(iterable, output); + walk(body, output); + } + S::Match { scrutinee, arms } => { + expression(scrutinee, output); + for arm in arms { + walk(&arm.body, output); + } + } + S::Assert { condition, message } => { + expression(condition, output); + if let Some(value) = message { + expression(value, output); + } + } + S::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, output); + } + } + S::Scope(body) => walk(body, output), + S::ReflectedTypeDispatch { type_info, arms } => { + expression(type_info, output); + for arm in arms { + walk(&arm.body, output); + } + } + S::Break | S::Continue | S::Trace(_) => {} + } + } + } + let mut output = Vec::new(); + walk(block, &mut output); + output +} diff --git a/crates/jett_hir/src/mutable_sum_backings/tests.rs b/crates/jett_hir/src/mutable_sum_backings/tests.rs new file mode 100644 index 00000000..6d9308e9 --- /dev/null +++ b/crates/jett_hir/src/mutable_sum_backings/tests.rs @@ -0,0 +1,226 @@ +use super::*; +use jett_diagnostics::Severity; + +const SOURCE: &str = r#"namespace app +function borrowed() returns list[int64]: + mutable optional[list[int64]] source = some(list(2, 3)) + list[int64] alias = view((view source) handle: return list()) + return clone alias +function copied() returns string: + mutable result[string, string] source = ok("kept") + string alias = view((view source) handle error: return error) + source = ok("new") + return alias +"#; + +fn checked( + source: &str, +) -> ( + jett_parser::ParseResult, + jett_resolve::ResolveResult, + CheckResult, +) { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + resolved + .diagnostics + .iter() + .chain(&checked.diagnostics) + .all(|error| error.severity != Severity::Error), + "{:?} {:?}", + resolved.diagnostics, + checked.diagnostics + ); + (parsed, resolved, checked) +} +fn high(source: &str) -> (Program, CheckResult) { + let (parsed, resolved, checked) = checked(source); + let high = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + ) + .unwrap(); + (high, checked) +} + +#[test] +fn canonical_copy_and_view_declarations_both_retain_exact_owned_backing() { + let (high, checked) = high(SOURCE); + validate(&high).unwrap(); + validate_backend_types(&high, &checked.interner).unwrap(); + assert_eq!(high.functions.len(), 2); + for function in &high.functions { + assert_eq!(function.mutable_sum_backings.len(), 1); + let proof = &function.mutable_sum_backings[0]; + assert!(proof.backing_fact.mutable); + assert_eq!(proof.backing_fact.mode, CheckedBindingMode::Owned); + let alias = &function.locals[proof.alias.index() as usize]; + assert_eq!( + alias.view_source.is_none(), + function.identity.declaration.name == "copied" + ); + assert_eq!( + function + .mutable_sum_backing(proof.alias, &proof.initializer, proof.backing) + .unwrap(), + Some(proof) + ); + } +} + +#[test] +fn copied_destination_cannot_bypass_selected_owner_fact_authentication() { + for invalid in ["missing", "mode", "mutable", "type", "definition"] { + let (parsed, resolved, mut checked) = checked(SOURCE); + let key = *checked + .binding_facts + .iter() + .find(|(_, fact)| { + fact.mutable && matches!(checked.interner.resolve(fact.ty), Type::Result(_, _)) + }) + .unwrap() + .0; + let other = checked + .binding_facts + .values() + .find(|fact| fact.declaration_span != key) + .unwrap() + .definition; + if invalid == "missing" { + checked.binding_facts.remove(&key); + } else { + let fact = checked.binding_facts.get_mut(&key).unwrap(); + match invalid { + "mode" => { + fact.mode = CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } + "mutable" => fact.mutable = false, + "type" => fact.ty = TypeInterner::STRING, + "definition" => fact.definition = other, + _ => unreachable!(), + } + } + let errors = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + ) + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("selected owned backing")), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn receipts_refuse_foreign_function_changed_initializer_and_alias_modes() { + let (original, checked) = high(SOURCE); + for invalid in ["foreign", "owner", "alias", "body", "duplicate", "reseal"] { + let mut changed = original.clone(); + let proof = changed.functions[0].mutable_sum_backings[0].clone(); + match invalid { + "foreign" => changed.functions[1].mutable_sum_backings = vec![proof], + "owner" => changed.functions[0].params.push(Param { + local: proof.backing, + name: "foreign".into(), + ty: proof.backing_fact.ty, + mode: ParamMode::View, + mutable: true, + span: proof.backing_fact.declaration_span, + }), + "alias" => changed.functions[0].locals[proof.alias.index() as usize].mutable = true, + "body" | "reseal" => changed.functions[0].body.statements.reverse(), + "duplicate" => changed.functions[0].mutable_sum_backings.push(proof), + _ => unreachable!(), + } + assert!(validate(&changed).is_err(), "{invalid}"); + if invalid == "reseal" { + assert!(seal_program(&mut changed, &checked.interner).is_err()); + } + } +} + +#[test] +fn canonical_conversion_refreshes_only_the_authenticated_original_body() { + let (mut high, checked) = high(SOURCE); + interface_values::complete_value_conversions(&mut high, &checked.interner).unwrap(); + validate(&high).unwrap(); + let mut changed = high.clone(); + changed.functions[0].body.statements.reverse(); + assert!(interface_values::complete_value_conversions(&mut changed, &checked.interner).is_err()); +} + +#[test] +fn first_sum_snapshot_follows_original_reflected_callable_sealing() { + let source = r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +function inspect(view holder: Holder, view field: TypeField) returns nothing: + mutable optional[int64] source = some(1) + int64 copied = view((view source) handle: return nothing) + function() returns Positive callback = type.field_value[Holder, function() returns Positive](view holder, view field) + return nothing +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(FileId::new(0), SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let mut high = lower_checked_ordinary_program(&checked).unwrap(); + let owner = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert_eq!(owner.mutable_sum_backings.len(), 1); + let plans = reflected_callable_plans(owner); + assert_eq!(plans.len(), 1); + assert_eq!( + reflected_callable_plans(owner.mutable_sum_backings[0].original.as_ref().unwrap()), + plans, + "sum snapshot pins the genuine sealed callable tokens", + ); + validate(&high).unwrap(); + validate_backend_types(&high, &checked.checked().interner).unwrap(); + for _ in 0..2 { + interface_values::complete_value_conversions(&mut high, &checked.checked().interner) + .unwrap(); + validate(&high).unwrap(); + validate_backend_types(&high, &checked.checked().interner).unwrap(); + let owner = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert_eq!(reflected_callable_plans(owner), plans); + } + let before = high.clone(); + let mut changed = before.clone(); + changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap() + .body + .statements + .reverse(); + assert!(callable_sealing(&mut changed, &before, &[]).is_err()); +} diff --git a/crates/jett_hir/src/native_property_suite.rs b/crates/jett_hir/src/native_property_suite.rs new file mode 100644 index 00000000..8a078c48 --- /dev/null +++ b/crates/jett_hir/src/native_property_suite.rs @@ -0,0 +1,746 @@ +//! Canonical Property wrappers over an immutable checked case/cache graph. +use crate::evaluated_values::{ValueContext, function_value_candidates, value_expression}; +use crate::{ + Block, CallOwnership, DeclarationId, DeclarationKind, Expression, ExpressionKind, Function, + FunctionDebugKind, FunctionId, FunctionIdentity, GeneratedOperation, Local, LowerError, + ParamMode, Program, Statement, StatementKind, +}; +use jett_common::{FileId, Span}; +use jett_comptime::verify::{CheckedPropertyCases, PROPERTY_DEFAULT_ITERATIONS, PropertyCase}; +use jett_comptime::{ExplicitComptimeValues, Value}; +use jett_parser::ast::Item; +use jett_typecheck::{CheckedCalleeAccess, CheckedResourceProgram}; +use jett_types::{Type, TypeId, TypeInterner}; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct CheckedNativePropertyTrial { + name: String, + span: Span, + function: FunctionId, + given_names: Vec, + case: PropertyCase, +} +impl CheckedNativePropertyTrial { + pub fn name(&self) -> &str { + &self.name + } + pub fn span(&self) -> Span { + self.span + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn given_names(&self) -> &[String] { + &self.given_names + } + pub fn case(&self) -> &PropertyCase { + &self.case + } +} + +/// No public constructor: only checked sampling and canonical HIR construction +/// produce the original body, capture, input and purpose association. +#[derive(Debug, Clone)] +pub struct CheckedNativePropertyPlan { + data: Arc, +} +#[derive(Debug)] +struct PropertyData { + base: Program, + cases: CheckedPropertyCases, + checked: Arc, + entry_file: FileId, + entry: FunctionId, + trials: Vec, + canonical: Function, +} +impl CheckedNativePropertyPlan { + pub fn entry(&self) -> FunctionId { + self.data.entry + } + pub fn trials(&self) -> &[CheckedNativePropertyTrial] { + &self.data.trials + } + pub(crate) fn same_plan(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} + +pub fn append_checked_native_property_suite( + program: &mut Program, + checked: &Arc, + entry_file: FileId, + cases: &CheckedPropertyCases, +) -> Result, Vec> { + input(program, checked)?; + if !cases.belongs_to(checked) + || !crate::native_suite_transition::same_required_cache(program, cases.required_values()) + || crate::native_suite_transition::has_property_suite(program) + { + return Err(error( + fallback_span(program), + "native Property cases changed their checked program/cache/purpose", + )); + } + let mut trials = Vec::new(); + let mut locals = Vec::new(); + let mut statements = Vec::new(); + let mut first = None; + for item in &checked.module().items { + let Item::Property(property) = item else { + continue; + }; + if property.span.file != entry_file { + continue; + } + let function = original_target(program, checked, property)?; + if first.is_none() { + first = Some(function.clone()); + } + let selected = cases + .cases() + .iter() + .filter(|case| case.property_span == property.span) + .collect::>(); + if selected.len() != PROPERTY_DEFAULT_ITERATIONS { + return Err(error( + property.span, + format!( + "checked property has {} generated cases; expected {PROPERTY_DEFAULT_ITERATIONS}", + selected.len() + ), + )); + } + for (iteration, case) in selected.into_iter().enumerate() { + if case.iteration != iteration || case.arguments.len() != function.params.len() { + return Err(error( + property.span, + "native Property sampling changed its exact parameter/iteration tuple", + )); + } + let trial = CheckedNativePropertyTrial { + name: property.name.name.clone(), + span: property.name.span, + function: function.id, + given_names: property + .givens + .iter() + .map(|given| given.name.name.clone()) + .collect(), + case: case.clone(), + }; + append_trial(program, checked, &trial, None, &mut locals, &mut statements)?; + trials.push(trial); + } + } + let Some(first) = first else { + return Ok(None); + }; + let entry = FunctionId::new(u32::try_from(program.functions.len()).map_err(|_| { + error( + first.span, + "native Property function table exceeds its ID range", + ) + })?); + let canonical = suite_function( + entry, + &first.identity.declaration, + first.span, + locals, + statements, + ); + let plan = CheckedNativePropertyPlan { + data: Arc::new(PropertyData { + base: program.clone(), + cases: cases.clone(), + checked: checked.clone(), + entry_file, + entry, + trials, + canonical: canonical.clone(), + }), + }; + install(program, canonical, &plan)?; + Ok(Some(plan)) +} + +/// Rebuild from the private pre-suite graph; a public edited HIR body never +/// supplies Source or callback authority. Changed shrink inputs are validated. +pub fn rebuild_checked_native_property_replay( + program: &Program, + plan: &CheckedNativePropertyPlan, + cases: &[PropertyCase], + types: &TypeInterner, +) -> Result> { + let checked = &plan.data.checked; + input(program, checked)?; + program + .resource_source + .validate_types(types) + .map_err(|message| error(fallback_span(program), message))?; + if !crate::native_suite_transition::owns_property_suite(program, plan) + || !plan.data.cases.belongs_to(checked) + || !crate::native_suite_transition::same_required_cache( + program, + plan.data.cases.required_values(), + ) + { + return Err(error( + fallback_span(program), + "native Property replay does not own this exact checked suite", + )); + } + let mut candidate = plan.data.base.clone(); + input(&candidate, checked)?; + let mut previous = None; + let mut locals = Vec::new(); + let mut statements = Vec::new(); + for case in cases { + if case.property_span.file != plan.data.entry_file { + return Err(error( + case.property_span, + "native Property replay changed its entry-file purpose", + )); + } + let mut matches = plan.data.trials.iter().enumerate().filter(|(_, trial)| { + trial.case.property_span == case.property_span && trial.case.iteration == case.iteration + }); + let Some((ordinal, original)) = matches.next() else { + return Err(error( + case.property_span, + "native Property replay changed its original property/iteration identity", + )); + }; + if matches.next().is_some() || previous.is_some_and(|prior| ordinal <= prior) { + return Err(error( + case.property_span, + "native Property replay duplicated or reordered an original trial", + )); + } + previous = Some(ordinal); + let mut trial = original.clone(); + trial.case = case.clone(); + append_trial( + &candidate, + checked, + &trial, + Some(&original.case.arguments), + &mut locals, + &mut statements, + )?; + } + let original = &plan.data.canonical; + let canonical = suite_function( + plan.data.entry, + &original.identity.declaration, + original.span, + locals, + statements, + ); + install(&mut candidate, canonical, plan)?; + Ok(candidate) +} + +fn input(program: &Program, checked: &Arc) -> Result<(), Vec> { + let span = fallback_span(program); + if !program.resource_source.belongs_to(checked) + || !program + .resource_source + .checked_program() + .is_some_and(|retained| Arc::ptr_eq(retained, checked)) + { + return Err(error( + span, + "native Property transition has no exact protected ordinary Source archive", + )); + } + program + .resource_source + .validate_current(program, &checked.checked().interner) + .map_err(|message| error(span, message))?; + crate::reflected_callable::baked::validate_conversion_input(program)?; + crate::actor_graph::validate_conversion_input(program)?; + Ok(()) +} + +fn original_target<'a>( + program: &'a Program, + checked: &Arc, + property: &jett_parser::ast::PropertyBlock, +) -> Result<&'a Function, Vec> { + let span = property.span; + let mut namespace = ""; + for item in &checked.module().items { + if let Item::Namespace(item) = item { + if item.span.file == span.file { + namespace = &item.name.name; + } + } + } + let origin = checked + .source_origins() + .get(&span.file) + .ok_or_else(|| error(span, "checked Property has no original Source origin"))?; + let name = format!("{}:{}", property.name.name, span.start); + let mut matches = program + .resource_source + .functions() + .iter() + .filter(|function| { + function.span == span + && function.identity.declaration.kind == DeclarationKind::Property + && function.identity.declaration.namespace == namespace + && function.identity.declaration.name == name + && &function.identity.declaration.origin == origin + }); + let original = matches + .next() + .ok_or_else(|| error(span, "checked Property has no exact original HIR body"))?; + if matches.next().is_some() { + return Err(error( + span, + "checked Property has multiple original HIR bodies", + )); + } + let current = program + .functions + .get(original.id.index() as usize) + .ok_or_else(|| error(span, "checked Property lost its protected current body"))?; + if current.id != original.id + || current.identity != original.identity + || current.debug_kind != original.debug_kind + || current.source_definition != original.source_definition + || current.span != original.span + || current.body.span != original.body.span + || current.params != original.params + || current.capture_count != 0 + || current.return_type != TypeInterner::NOTHING + || current.params.len() != property.givens.len() + || current + .params + .iter() + .zip(&property.givens) + .any(|(param, given)| param.name != given.name.name) + || !current.identity.scoped_type_bindings.is_empty() + || !current.identity.type_arguments.is_empty() + || current.identity.specialization + != jett_typecheck::CheckedGenericSpecialization::default() + { + return Err(error( + span, + "checked Property changed its original formal/header tuple", + )); + } + Ok(current) +} + +fn append_trial( + program: &Program, + checked: &Arc, + trial: &CheckedNativePropertyTrial, + sampled: Option<&[Value]>, + locals: &mut Vec, + statements: &mut Vec, +) -> Result<(), Vec> { + let span = trial.case.property_span; + let function = program + .functions + .get(trial.function.index() as usize) + .ok_or_else(|| error(span, "native Property trial lost its checked function"))?; + if function.id != trial.function + || function.span != span + || function.identity.declaration.kind != DeclarationKind::Property + || trial.case.arguments.len() != function.params.len() + { + return Err(error( + span, + "native Property trial changed its original callable tuple", + )); + } + let types = &checked.checked().interner; + let functions = function_value_candidates( + &program.functions, + &checked.checked().method_value_definitions, + ); + statements.push(case_context( + Some(crate::NativePropertyCase { + name: trial.name.clone(), + trial: u64::try_from(trial.case.iteration) + .map_err(|_| error(span, "native Property trial ordinal exceeds its range"))? + + 1, + }), + span, + )); + let mut args = Vec::new(); + for (index, (value, param)) in trial + .case + .arguments + .iter() + .zip(&function.params) + .enumerate() + { + authenticate_callbacks(program, value, param.ty, checked) + .map_err(|message| error(span, message))?; + let unchanged = sampled + .and_then(|values| values.get(index)) + .is_some_and(|original| original.same_checked_snapshot(value)); + let mut context = ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: sampled + .filter(|_| !unchanged) + .map(|_| program.functions.as_slice()), + locals, + bindings: statements, + }; + args.push( + value_expression(value, param.ty, span, &mut context) + .map_err(|message| error(span, message))?, + ); + } + let evaluation_order = (0..args.len()).collect::>(); + let parameters = function + .params + .iter() + .map(|param| param.ty) + .collect::>(); + let access = function + .params + .iter() + .map(|param| match param.mode { + ParamMode::Owned => CheckedCalleeAccess::Owned, + ParamMode::View => CheckedCalleeAccess::View, + }) + .collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: function.id, + }, + &args, + ¶meters, + &access, + TypeInterner::NOTHING, + &evaluation_order, + types, + ) + .map_err(|message| error(span, message))?; + let expression = Expression { + kind: ExpressionKind::Call { + function: function.id, + args, + evaluation_order, + ownership, + }, + ty: TypeInterner::NOTHING, + span, + }; + let local_facts = locals + .iter() + .map(|local| crate::OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter: false, + view_iteration: None, + match_binding: None, + }) + .collect::>(); + crate::validate_hir_invocation( + &program.functions, + &local_facts, + &expression, + types, + jett_typecheck::CheckedOwnershipContext::Property, + ) + .map_err(|message| error(span, message))?; + statements.push(Statement { + kind: StatementKind::Expression(expression), + span, + }); + statements.push(case_context(None, span)); + Ok(()) +} + +// Authenticate callback descriptors before the general value materializer can +// select a body by its typed shape. Values are inspected as stored metadata; +// pending callbacks are never joined or invoked here. +fn authenticate_callbacks( + program: &Program, + value: &Value, + ty: TypeId, + checked: &Arc, +) -> Result<(), String> { + let types = &checked.checked().interner; + match (types.resolve(ty), value) { + (_, Value::Pending(inner) | Value::Typed { value: inner, .. }) => { + authenticate_callbacks(program, inner, ty, checked) + } + (Type::Refinement { base, .. } | Type::Secret(base), _) => { + authenticate_callbacks(program, value, *base, checked) + } + (Type::Function { .. }, _) => { + value.validate_checked_callable_source(checked, ty)?; + match value { + Value::Function { captures, .. } => { + let functions = function_value_candidates( + &program.functions, + &checked.checked().method_value_definitions, + ); + let selected = + crate::evaluated_values::selected_function(value, ty, types, &functions)?; + for (name, capture_type) in selected.captures() { + let captured = captures + .get(name) + .ok_or("native Property callback lost an original capture")?; + authenticate_callbacks(program, captured, *capture_type, checked)?; + } + Ok(()) + } + Value::ReflectedCallable(adapter) => authenticate_callbacks( + program, + adapter.source(), + adapter.source_type(), + checked, + ), + _ => Ok(()), + } + } + (Type::List(element) | Type::Set(element), Value::List(values) | Value::Set(values)) => { + for value in values { + authenticate_callbacks(program, value, *element, checked)?; + } + Ok(()) + } + (Type::Map(key, mapped), Value::Map(entries)) => { + for (k, v) in entries { + authenticate_callbacks(program, k, *key, checked)?; + authenticate_callbacks(program, v, *mapped, checked)?; + } + Ok(()) + } + (Type::Optional(inner), Value::OptionalSome(value)) + | (Type::Result(inner, _), Value::ResultOk(value)) + | (Type::Result(_, inner), Value::ResultFail(value)) => { + authenticate_callbacks(program, value, *inner, checked) + } + (Type::Struct(id), Value::Struct { fields, .. }) => { + for (name, ty) in &types.resolve_struct(*id).fields { + let value = fields + .iter() + .find(|(field, _)| field == name) + .ok_or("native Property struct field is absent")?; + authenticate_callbacks(program, &value.1, *ty, checked)?; + } + Ok(()) + } + ( + Type::Enum(id), + Value::Enum { + variant, fields, .. + }, + ) => { + let variant = types + .resolve_enum(*id) + .variants + .iter() + .find(|candidate| candidate.name == *variant) + .ok_or("native Property enum variant is absent")?; + if fields.len() != variant.fields.len() { + return Err("native Property enum payload arity changed".into()); + } + for (value, (_, ty)) in fields.iter().zip(&variant.fields) { + authenticate_callbacks(program, value, *ty, checked)?; + } + Ok(()) + } + ( + Type::Machine(id) | Type::MachineState { machine: id, .. }, + Value::Machine { state, fields, .. }, + ) => { + let state = types + .resolve_machine(*id) + .states + .iter() + .find(|candidate| candidate.name == *state) + .ok_or("native Property machine state is absent")?; + if fields.len() != state.fields.len() { + return Err("native Property machine payload arity changed".into()); + } + for (value, (_, ty)) in fields.iter().zip(&state.fields) { + authenticate_callbacks(program, value, *ty, checked)?; + } + Ok(()) + } + (Type::TypeConstruction, Value::TypeConstruction { fields, .. }) => { + for (_, _, stored, value) in fields { + let declared = if let Some(ty) = checked + .checked() + .reflection_metadata + .source_type_id_for_name(stored) + { + ty + } else { + let mut candidates = types + .type_ids() + .filter(|ty| types.type_name(*ty) == *stored); + let selected = candidates + .next() + .ok_or("native Property builder field has no checked type identity")?; + if candidates.next().is_some() { + return Err( + "native Property builder field has ambiguous checked type identity" + .into(), + ); + } + selected + }; + // The shared builder materializer separately joins this stored + // field tuple to the original checked builder layout. + authenticate_callbacks(program, value, declared, checked)?; + } + Ok(()) + } + (Type::Interface(_), _) => { + // General reconstruction selects the same concrete identity; an + // opaque erased callable cannot bypass exact Source authentication. + if contains_callback(value) { + return Err( + "native Property erased callable has no exact checked concrete Source identity" + .into(), + ); + } + Ok(()) + } + _ => Ok(()), + } +} + +fn contains_callback(value: &Value) -> bool { + match value { + Value::NamedFunction(_) | Value::Function { .. } | Value::ReflectedCallable(_) => true, + Value::Typed { value, .. } + | Value::Pending(value) + | Value::OptionalSome(value) + | Value::ResultOk(value) + | Value::ResultFail(value) => contains_callback(value), + Value::List(values) + | Value::Set(values) + | Value::Enum { fields: values, .. } + | Value::Machine { fields: values, .. } => values.iter().any(contains_callback), + Value::Map(entries) => entries + .iter() + .any(|(key, value)| contains_callback(key) || contains_callback(value)), + Value::Struct { fields, .. } => fields.iter().any(|(_, value)| contains_callback(value)), + Value::TypeConstruction { fields, .. } => fields + .iter() + .any(|(_, _, _, value)| contains_callback(value)), + _ => false, + } +} + +fn suite_function( + id: FunctionId, + declaration: &DeclarationId, + span: Span, + locals: Vec, + statements: Vec, +) -> Function { + let name = format!("__native_property_suite:{}", span.start); + Function { + id, + debug_kind: FunctionDebugKind::named(&declaration.namespace, &name), + identity: FunctionIdentity { + declaration: DeclarationId { + origin: declaration.origin.clone(), + namespace: declaration.namespace.clone(), + name, + kind: DeclarationKind::Property, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: Vec::new(), + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals, + body: Block { statements, span }, + span, + } +} +fn install( + program: &mut Program, + canonical: Function, + plan: &CheckedNativePropertyPlan, +) -> Result<(), Vec> { + let before = program.clone(); + input(&before, &plan.data.checked)?; + if canonical.id != plan.data.entry + || canonical.id.index() as usize != before.functions.len() + || canonical.span != plan.data.canonical.span + || canonical.identity != plan.data.canonical.identity + || !canonical.params.is_empty() + || canonical.capture_count != 0 + || canonical.return_type != TypeInterner::NOTHING + { + return Err(error( + canonical.span, + "private Property constructor changed its canonical purpose/header", + )); + } + let mut candidate = before.clone(); + candidate.functions.push(canonical.clone()); + let mut graph_candidate = candidate.clone(); + crate::actor_graph::complete_conversion(&mut graph_candidate, &before)?; + crate::reflected_callable::baked::complete_conversion(&mut candidate, &before)?; + candidate.resource_source.graph_values = graph_candidate.resource_source.graph_values; + if candidate.resource_source.reflected_values.is_some() { + crate::reflected_callable::baked::install_property_suite(&mut candidate, plan.clone())?; + } + crate::native_suite_transition::append_property( + &mut candidate, + &before, + &plan.data.checked, + plan.data.entry_file, + &canonical, + plan, + ) + .map_err(|message| error(canonical.span, message))?; + input(&candidate, &plan.data.checked)?; + *program = candidate; + Ok(()) +} +fn case_context(case: Option, span: Span) -> Statement { + Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::PropertyCaseContext(case), + ty: TypeInterner::NOTHING, + span, + }), + span, + } +} +fn fallback_span(program: &Program) -> Span { + program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span) +} +fn error(span: Span, message: impl Into) -> Vec { + vec![LowerError { + span, + message: message.into(), + }] +} + +#[cfg(test)] +#[path = "native_property_suite_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/native_property_suite_tests.rs b/crates/jett_hir/src/native_property_suite_tests.rs new file mode 100644 index 00000000..76c3c6c2 --- /dev/null +++ b/crates/jett_hir/src/native_property_suite_tests.rs @@ -0,0 +1,571 @@ +use super::*; +use jett_common::SourceOrigin; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::collections::HashMap; +const NAMED: &str = "namespace app\ntype Positive = int64 where value > 0\nstruct CallbackHolder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction main() returns nothing:\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n Positive value = observed()\n println(coarsen value)\n return nothing\n\nfunction ready_five() returns Positive:\n return 5\nverify lazy_callback_result:\n Positive baked = comptime ready_five()\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == coarsen baked\nproperty lazy_callback_trials:\n given repeat: bool\n Positive baked = comptime ready_five()\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == coarsen baked\n if repeat:\n assert coarsen observed() == 5\n"; +const CAPTURED: &str = "namespace app\ntype Positive = int64 where value > 0\nstruct CallbackHolder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction main() returns nothing:\n CallbackHolder holder = CallbackHolder(callback: five)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n Positive value = observed()\n println(coarsen value)\n return nothing\n\nverify captured_callback_result:\n int64 seed = 7\n CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == 7\nproperty captured_callback_trials:\n given repeat: bool\n int64 seed = 7\n CallbackHolder holder = CallbackHolder(callback: function() returns int64: return seed)\n for field in type.fields[CallbackHolder]():\n function() returns Positive observed = type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\n assert coarsen observed() == 7\n if repeat:\n assert coarsen observed() == 7\n"; + +fn fixture( + source: &str, +) -> ( + Program, + Arc, + Arc, + CheckedPropertyCases, +) { + let file = FileId::new(0); + let checked = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .unwrap(), + ); + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes::from_checked_program(&checked)), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + let values = Arc::new(evaluation.values); + let mut program = crate::lower_checked_ordinary_program_with_test_bodies(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + let cases = jett_comptime::verify::collect_checked_property_cases_with_checked_values( + checked.clone(), + values.clone(), + ) + .unwrap(); + (program, checked, values, cases) +} +fn convert(program: &mut Program, checked: &Arc) { + crate::complete_value_conversions(program, &checked.checked().interner).unwrap(); + crate::validate(program).unwrap(); + crate::validate_backend_types(program, &checked.checked().interner).unwrap(); +} +fn calls(function: &Function) -> Vec { + function + .body + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::Expression(expression) + if matches!(expression.kind, ExpressionKind::Call { .. }) => + { + Some(expression.clone()) + } + _ => None, + }) + .collect() +} +#[test] +fn checked_property_suite_original_named_and_captured_sources_keep_exact_100_trials() { + for source in [NAMED, CAPTURED] { + let (mut program, checked, _, cases) = fixture(source); + let before = program.clone(); + let plan = + append_checked_native_property_suite(&mut program, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + assert_eq!(plan.trials().len(), PROPERTY_DEFAULT_ITERATIONS); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &program.functions[..before.functions.len()] + )); + assert_eq!(program.resource_manifest, before.resource_manifest); + assert_eq!(program.equality_methods, before.equality_methods); + let suite = &program.functions[plan.entry().index() as usize]; + assert!(suite.params.is_empty()); + assert_eq!(suite.capture_count, 0); + assert_eq!(suite.return_type, TypeInterner::NOTHING); + assert_eq!(calls(suite).len(), PROPERTY_DEFAULT_ITERATIONS); + for (ordinal, (call, trial)) in calls(suite).iter().zip(plan.trials()).enumerate() { + assert_eq!(trial.case().iteration, ordinal); + let ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } = &call.kind + else { + unreachable!() + }; + assert_eq!(*function, trial.function()); + assert_eq!(args.len(), 1); + assert_eq!(evaluation_order, &[0]); + } + convert(&mut program, &checked); + let replay = plan.trials()[7..9] + .iter() + .map(|trial| trial.case().clone()) + .collect::>(); + let mut replayed = rebuild_checked_native_property_replay( + &program, + &plan, + &replay, + &checked.checked().interner, + ) + .unwrap(); + assert_eq!( + calls(&replayed.functions[plan.entry().index() as usize]).len(), + 2 + ); + convert(&mut replayed, &checked); + } +} +#[test] +fn checked_property_suite_refuses_foreign_cache_and_original_body_or_header_forgery() { + let (program, checked, _, cases) = fixture(NAMED); + let (_, _, _, foreign_cases) = fixture(NAMED); + let mut foreign = program.clone(); + assert!( + append_checked_native_property_suite( + &mut foreign, + &checked, + FileId::new(0), + &foreign_cases + ) + .is_err() + ); + for case in 0..5 { + let mut changed = program.clone(); + match case { + 0 => changed.resource_source = crate::ResourceSourceArchive::empty(), + 1 => changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap() + .body + .statements + .clear(), + 2 => { + changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::Property + }) + .unwrap() + .params[0] + .ty = TypeInterner::STRING + } + 3 => { + changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::Property + }) + .unwrap() + .identity + .declaration + .namespace = "other".into() + } + 4 => { + let mut function = changed.functions[0].clone(); + function.id = FunctionId::new(u32::try_from(changed.functions.len()).unwrap()); + changed.functions.push(function); + } + _ => unreachable!(), + } + let before = changed.clone(); + assert!( + append_checked_native_property_suite(&mut changed, &checked, FileId::new(0), &cases) + .is_err(), + "case {case}" + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &changed.functions + )); + assert_eq!(before.resource_source, changed.resource_source); + } +} +#[test] +fn checked_property_suite_refuses_public_wrapper_mutation_and_foreign_plan_replay() { + let (mut program, checked, _, cases) = fixture(CAPTURED); + let plan = append_checked_native_property_suite(&mut program, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + convert(&mut program, &checked); + let selected = vec![plan.trials()[0].case().clone()]; + for case in 0..5 { + let mut changed = program.clone(); + let suite = &mut changed.functions[plan.entry().index() as usize]; + match case { + 0 => suite.body.statements.swap(0, 1), + 1 => suite.body.statements.push(suite.body.statements[0].clone()), + 2 => suite.return_type = TypeInterner::BOOL, + 3 => suite.identity.declaration.namespace = "other".into(), + 4 => { + let StatementKind::Expression(expression) = &mut suite.body.statements[0].kind + else { + unreachable!() + }; + expression.kind = ExpressionKind::PropertyCaseContext(None); + } + _ => unreachable!(), + } + assert!( + rebuild_checked_native_property_replay( + &changed, + &plan, + &selected, + &checked.checked().interner + ) + .is_err(), + "mutation {case}" + ); + assert!( + crate::complete_value_conversions(&mut changed, &checked.checked().interner).is_err() + ); + } + let (mut foreign, foreign_checked, _, foreign_cases) = fixture(CAPTURED); + let foreign_plan = append_checked_native_property_suite( + &mut foreign, + &foreign_checked, + FileId::new(0), + &foreign_cases, + ) + .unwrap() + .unwrap(); + assert!( + rebuild_checked_native_property_replay( + &program, + &foreign_plan, + &selected, + &checked.checked().interner + ) + .is_err() + ); +} +#[test] +fn checked_property_replay_refuses_forged_order_purpose_shape_and_validates_changed_bool_input() { + let (mut program, checked, _, cases) = fixture(NAMED); + let plan = append_checked_native_property_suite(&mut program, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + convert(&mut program, &checked); + let original = plan.trials()[0].case().clone(); + let mut changed = original.clone(); + changed.arguments[0] = Value::Bool(!matches!(original.arguments[0], Value::Bool(true))); + let mut replay = rebuild_checked_native_property_replay( + &program, + &plan, + &[changed], + &checked.checked().interner, + ) + .unwrap(); + convert(&mut replay, &checked); + let mut wrong_type = original.clone(); + wrong_type.arguments[0] = Value::String("forged".into()); + let mut wrong_purpose = original.clone(); + wrong_purpose.property_span.file = FileId::new(77); + let mut wrong_arity = original.clone(); + wrong_arity.arguments.clear(); + for forged in [ + vec![wrong_type], + vec![wrong_purpose], + vec![wrong_arity], + vec![original.clone(), original.clone()], + vec![plan.trials()[1].case().clone(), original], + ] { + assert!( + rebuild_checked_native_property_replay( + &program, + &plan, + &forged, + &checked.checked().interner + ) + .is_err() + ); + } +} +#[test] +fn checked_property_replay_revalidates_same_payload_without_original_refinement_snapshot() { + let source = NAMED + .replace("given repeat: bool", "given repeat: Positive") + .replace("if repeat:", "if coarsen repeat > 0:"); + let (mut program, checked, _, cases) = fixture(&source); + let plan = append_checked_native_property_suite(&mut program, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + convert(&mut program, &checked); + let mut changed = plan.trials()[0].case().clone(); + let original = changed.arguments[0].clone(); + changed.arguments[0] = Value::Int64(-1); + assert!(!original.same_checked_snapshot(&changed.arguments[0])); + let replay = rebuild_checked_native_property_replay( + &program, + &plan, + &[changed], + &checked.checked().interner, + ) + .unwrap(); + let mut body = replay.functions[plan.entry().index() as usize].body.clone(); + let mut validation = 0; + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if matches!( + expression.kind, + ExpressionKind::Handle { + kind: crate::HandleKind::Refinement { .. }, + .. + } + ) { + validation += 1; + } + Ok(()) + }) + .unwrap(); + assert!( + validation > 0, + "changed input must execute the existing checked refinement validation before the property consumer" + ); +} + +#[test] +fn checked_property_authenticates_every_emitted_nested_callback_capture() { + let source = format!( + "{NAMED}\nfunction original_inner(seed: int64) returns function() returns int64:\n return function() returns int64: return seed\n" + ); + let (program, checked, _, _) = fixture(&source); + let mut interpreter = jett_comptime::Interpreter::from_checked_resource_program( + checked.clone(), + jett_comptime::ExecutionPurpose::ReferenceRuntime, + ) + .unwrap(); + interpreter.set_reflection_metadata(checked.checked().reflection_metadata.clone()); + interpreter.set_checked_expression_types(Arc::new( + CheckedExpressionTypes::from_checked_program(&checked), + )); + interpreter + .set_breakpoint_exclusions(Arc::new(checked.checked().breakpoint_exclusions.clone())); + let inner = interpreter + .call_function("app.original_inner", vec![Value::Int64(5)]) + .unwrap(); + let types = &checked.checked().interner; + let definition = checked + .resolved() + .scope_table + .definitions + .iter() + .find(|definition| definition.name == "app.original_inner") + .unwrap(); + let Type::Function { + return_type: callback_ty, + .. + } = types.resolve(checked.checked().definition_types[&definition.id]) + else { + unreachable!() + }; + let holder_ty = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.CallbackHolder") + .unwrap(); + let holder = Value::Struct { + type_name: "app.CallbackHolder".into(), + concrete_type: None, + fields: vec![("callback".into(), inner.clone())], + }; + // Source captures only the ordinary int64 seed. Moving the resulting + // callback into its declared holder does not capture a move-only value. + authenticate_callbacks(&program, &holder, holder_ty, &checked).unwrap(); + let functions = function_value_candidates( + &program.functions, + &checked.checked().method_value_definitions, + ); + let mut locals = Vec::new(); + let mut bindings = Vec::new(); + let emitted = value_expression( + &holder, + holder_ty, + checked.module().span, + &mut ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals: &mut locals, + bindings: &mut bindings, + }, + ) + .unwrap(); + let ExpressionKind::StructConstruct { fields, .. } = &emitted.kind else { + panic!("typed holder") + }; + let ExpressionKind::ClosureRef { captures, .. } = &fields[0].kind else { + panic!("genuine callback descriptor") + }; + assert_eq!(captures.len(), 1); + assert_eq!(locals.len(), 1); + assert_eq!(locals[0].ty, TypeInterner::INT64); + assert!(matches!( + &bindings[0].kind, + StatementKind::Let { + value: Expression { + kind: ExpressionKind::Int(5), + .. + }, + .. + } + )); + let mut forged = inner; + let Value::Function { body, .. } = &mut forged else { + panic!("source inline callback") + }; + let jett_parser::ast::Stmt::Return(statement) = &mut body.stmts[0] else { + panic!("original return") + }; + let value = statement.value.as_mut().unwrap(); + *value = jett_parser::ast::Expr::IntLiteral(0, value.span()); + // The ordinary span/context selector can still find its original HIR + // candidate. Recursive Source authentication must refuse the changed body. + assert!( + crate::evaluated_values::selected_function(&forged, *callback_ty, types, &functions) + .is_ok() + ); + let forged_holder = Value::Struct { + type_name: "app.CallbackHolder".into(), + concrete_type: None, + fields: vec![("callback".into(), forged)], + }; + assert!(authenticate_callbacks(&program, &forged_holder, holder_ty, &checked).is_err()); +} + +const ACTOR_WITHOUT_CALLBACKS: &str = r#"namespace app +struct Record: + value: int64 +actor Store: + mutable Record item = Record(value: 0) + receive set(amount: int64): + item.value = amount + receive read responds int64: + respond item.value +property actor_values: + given value: int64 + Store store = spawn Store() + send store.set(value) + int64 actual = ask store.read + assert actual == value +"#; + +#[test] +fn checked_property_actor_inputs_keep_the_protected_archive_without_callable_plans() { + let (mut program, checked, values, cases) = fixture(ACTOR_WITHOUT_CALLBACKS); + assert!(!crate::has_reflected_callable_plans(&program)); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_source.belongs_to(&checked)); + assert!( + program + .resource_source + .checked_program() + .is_some_and(|retained| { Arc::ptr_eq(retained, &checked) }) + ); + assert!(crate::reflected_callable::baked::same_required_cache( + &program, &values + )); + let before = program.clone(); + let plan = append_checked_native_property_suite(&mut program, &checked, FileId::new(0), &cases) + .unwrap() + .unwrap(); + assert_eq!(plan.trials().len(), PROPERTY_DEFAULT_ITERATIONS); + assert_eq!(program.functions.len(), before.functions.len() + 1); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &program.functions[..before.functions.len()] + )); + let suite = &program.functions[plan.entry().index() as usize]; + assert_eq!(calls(suite).len(), PROPERTY_DEFAULT_ITERATIONS); + for (iteration, trial) in plan.trials().iter().enumerate() { + assert_eq!(trial.case().iteration, iteration); + assert!(matches!( + trial.case().arguments.as_slice(), + [Value::Int64(_)] + )); + } + convert(&mut program, &checked); + let selected = plan.trials()[7..9] + .iter() + .map(|trial| trial.case().clone()) + .collect::>(); + let mut replay = rebuild_checked_native_property_replay( + &program, + &plan, + &selected, + &checked.checked().interner, + ) + .unwrap(); + assert_eq!( + calls(&replay.functions[plan.entry().index() as usize]).len(), + 2 + ); + convert(&mut replay, &checked); +} + +#[test] +fn checked_property_without_callable_plans_refuses_table_and_case_forgery_atomically() { + let (program, checked, _, cases) = fixture(ACTOR_WITHOUT_CALLBACKS); + let (_, _, _, foreign_cases) = fixture(ACTOR_WITHOUT_CALLBACKS); + assert!(!crate::has_reflected_callable_plans(&program)); + let property = program + .functions + .iter() + .position(|function| function.identity.declaration.kind == DeclarationKind::Property) + .unwrap(); + for mutation in 0..3 { + let mut changed = program.clone(); + match mutation { + 0 => { + let mut extra = changed.functions[property].clone(); + extra.id = FunctionId::new(u32::try_from(changed.functions.len()).unwrap()); + changed.functions.push(extra); + } + 1 => changed.functions[property].body.statements.clear(), + 2 => {} + _ => unreachable!(), + } + let before = changed.clone(); + let selected = if mutation == 2 { + &foreign_cases + } else { + &cases + }; + assert!( + append_checked_native_property_suite(&mut changed, &checked, FileId::new(0), selected,) + .is_err(), + "mutation {mutation}" + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &changed.functions, + )); + assert_eq!(before.resource_source, changed.resource_source); + if mutation != 2 { + assert!( + crate::complete_value_conversions(&mut changed, &checked.checked().interner,) + .is_err(), + "edited protected table {mutation}" + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &changed.functions, + )); + assert_eq!(before.resource_source, changed.resource_source); + } + } +} diff --git a/crates/jett_hir/src/native_suite_transition.rs b/crates/jett_hir/src/native_suite_transition.rs new file mode 100644 index 00000000..fa198c5c --- /dev/null +++ b/crates/jett_hir/src/native_suite_transition.rs @@ -0,0 +1,443 @@ +//! Constructor-only native suite occurrences over a complete checked table. +use crate::{Expression, ExpressionKind, Function, FunctionId, Program, ResourceSourceArchive}; +use jett_common::FileId; +use jett_comptime::ExplicitComptimeValues; +use jett_typecheck::CheckedResourceProgram; +use std::sync::Arc; + +#[derive(Debug)] +enum Purpose { + Verify { targets: Vec }, + Property(crate::CheckedNativePropertyPlan), +} + +#[derive(Debug)] +struct Origin { + checked: Arc, + before: Program, + entry_file: FileId, + canonical: Function, + purpose: Purpose, +} + +#[derive(Debug)] +struct Call { + origin: Arc, + ordinal: usize, + expression: Expression, +} + +/// An exact constructor-owned wrapper occurrence, never Source call authority. +#[derive(Clone)] +pub struct CheckedNativeSuiteCall(Arc); + +impl std::fmt::Debug for CheckedNativeSuiteCall { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("CheckedNativeSuiteCall") + .field("owner", &self.owner()) + .field("target", &self.target()) + .field("ordinal", &self.expression_ordinal()) + .finish() + } +} + +impl PartialEq for CheckedNativeSuiteCall { + fn eq(&self, other: &Self) -> bool { + self.same_receipt(other) + } +} +impl Eq for CheckedNativeSuiteCall {} + +impl CheckedNativeSuiteCall { + pub fn owner(&self) -> FunctionId { + self.0.origin.canonical.id + } + pub fn target(&self) -> FunctionId { + let ExpressionKind::Call { function, .. } = &self.0.expression.kind else { + unreachable!("private suite call constructor") + }; + *function + } + pub fn expression_ordinal(&self) -> usize { + self.0.ordinal + } + pub fn arguments(&self) -> &[Expression] { + let ExpressionKind::Call { args, .. } = &self.0.expression.kind else { + unreachable!("private suite call constructor") + }; + args + } + pub fn evaluation_order(&self) -> &[usize] { + let ExpressionKind::Call { + evaluation_order, .. + } = &self.0.expression.kind + else { + unreachable!("private suite call constructor") + }; + evaluation_order + } + pub fn same_receipt(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } + pub fn matches(&self, owner: FunctionId, expression: &Expression, ordinal: usize) -> bool { + self.owner() == owner + && self.0.ordinal == ordinal + && crate::resource_materialization::expression_equal(&self.0.expression, expression) + } + pub fn belongs_to_archive(&self, archive: &ResourceSourceArchive) -> bool { + archive.belongs_to(&self.0.origin.checked) + && archive.native_suites.as_ref().is_some_and(|transition| { + transition.calls.iter().any(|call| self.same_receipt(call)) + }) + } +} + +#[derive(Debug)] +pub(crate) struct Transition { + origins: Vec>, + current: Vec, + calls: Vec, +} + +impl Transition { + pub(crate) fn functions(&self) -> &[Function] { + &self.current + } + pub(crate) fn function(&self, id: FunctionId) -> Option<&Function> { + self.origins + .iter() + .find(|origin| origin.canonical.id == id)?; + self.current + .get(id.index() as usize) + .filter(|function| function.id == id) + } + pub(crate) fn call( + &self, + owner: FunctionId, + expression: &Expression, + ordinal: usize, + ) -> Option<&CheckedNativeSuiteCall> { + self.calls + .iter() + .find(|call| call.matches(owner, expression, ordinal)) + } +} + +pub(crate) fn same_required_cache(program: &Program, values: &ExplicitComptimeValues) -> bool { + program + .resource_source + .required_cache + .as_ref() + .is_some_and(|retained| retained.same_checked_cache(values)) + || crate::reflected_callable::baked::same_required_cache(program, values) +} + +pub(crate) fn has_property_suite(program: &Program) -> bool { + program + .resource_source + .native_suites + .as_ref() + .is_some_and(|transition| { + transition + .origins + .iter() + .any(|origin| matches!(origin.purpose, Purpose::Property(_))) + }) + || crate::reflected_callable::baked::has_property_suite(program) +} + +pub(crate) fn owns_property_suite( + program: &Program, + plan: &crate::CheckedNativePropertyPlan, +) -> bool { + program + .resource_source + .native_suites + .as_ref() + .is_some_and(|transition| { + transition.origins.iter().any(|origin| { + matches!(&origin.purpose, Purpose::Property(current) if current.same_plan(plan)) + }) + }) +} + +pub(crate) fn append_verify( + candidate: &mut Program, + before: &Program, + checked: &Arc, + entry_file: FileId, + canonical: &Function, + targets: Vec, +) -> Result<(), String> { + append( + candidate, + before, + checked, + entry_file, + canonical, + Purpose::Verify { targets }, + ) +} + +pub(crate) fn append_property( + candidate: &mut Program, + before: &Program, + checked: &Arc, + entry_file: FileId, + canonical: &Function, + plan: &crate::CheckedNativePropertyPlan, +) -> Result<(), String> { + append( + candidate, + before, + checked, + entry_file, + canonical, + Purpose::Property(plan.clone()), + ) +} + +fn append( + candidate: &mut Program, + before: &Program, + checked: &Arc, + entry_file: FileId, + canonical: &Function, + purpose: Purpose, +) -> Result<(), String> { + before + .resource_source + .validate_current(before, &checked.checked().interner)?; + if !before.resource_source.belongs_to(checked) + || candidate.resource_manifest != before.resource_manifest + || candidate.equality_methods != before.equality_methods + || candidate.functions.len() != before.functions.len() + 1 + || canonical.id.index() as usize != before.functions.len() + || !crate::resource_materialization::functions_equal( + &candidate.functions[..before.functions.len()], + &before.functions, + ) + || !crate::resource_materialization::functions_equal( + &candidate.functions[before.functions.len()..], + std::slice::from_ref(canonical), + ) + { + return Err("native suite append changed its complete checked before table".into()); + } + let mut origins = before + .resource_source + .native_suites + .as_ref() + .map_or_else(Vec::new, |transition| transition.origins.clone()); + origins.push(Arc::new(Origin { + checked: checked.clone(), + before: before.clone(), + entry_file, + canonical: canonical.clone(), + purpose, + })); + let transition = transition(origins, &candidate.functions)?; + candidate.resource_source.native_suites = Some(Arc::new(transition)); + candidate + .resource_source + .validate_current(candidate, &checked.checked().interner) +} + +fn transition(origins: Vec>, current: &[Function]) -> Result { + let mut calls = Vec::new(); + for origin in &origins { + if !origin.before.resource_source.belongs_to(&origin.checked) + || origin.canonical.span.file != origin.entry_file + { + return Err("native suite origin lost its checked entry-file purpose".into()); + } + let function = current + .get(origin.canonical.id.index() as usize) + .filter(|function| function.id == origin.canonical.id) + .ok_or("native suite conversion lost its canonical owner")?; + let mut header = function.clone(); + header.body = origin.canonical.body.clone(); + if !crate::resource_materialization::functions_equal( + std::slice::from_ref(&header), + std::slice::from_ref(&origin.canonical), + ) { + return Err("native suite conversion changed its canonical header or locals".into()); + } + let mut body = function.body.clone(); + let mut ordinal = 0; + let mut targets = Vec::new(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + ordinal += 1; + let ExpressionKind::Call { + function, + ownership: crate::CallOwnership::Generated(packet), + .. + } = &expression.kind + else { + return Ok(()); + }; + if !matches!(&packet.operation, crate::GeneratedOperation::NativeSuite { function: target } if target == function) + { + return Ok(()); + } + targets.push(*function); + calls.push(CheckedNativeSuiteCall(Arc::new(Call { + origin: origin.clone(), + ordinal, + expression: expression.clone(), + }))); + Ok(()) + })?; + match &origin.purpose { + Purpose::Verify { targets: expected } if &targets != expected => { + return Err("native verify suite changed its complete ordered targets".into()); + } + Purpose::Property(plan) => { + let mut canonical_targets = Vec::new(); + let mut canonical = origin.canonical.body.clone(); + crate::resource_materialization::walk_block(&mut canonical, &mut |expression| { + if let ExpressionKind::Call { + function, + ownership: crate::CallOwnership::Generated(packet), + .. + } = &expression.kind + && matches!(&packet.operation, crate::GeneratedOperation::NativeSuite { function: target } if target == function) + { + canonical_targets.push(*function); + } + Ok(()) + })?; + if plan.entry() != origin.canonical.id || targets != canonical_targets { + return Err( + "native Property suite changed its complete active case order".into(), + ); + } + } + _ => {} + } + } + Ok(Transition { + origins, + current: current.to_vec(), + calls, + }) +} + +pub(crate) fn validate_conversion_input(program: &Program) -> Result<(), String> { + let Some(transition) = &program.resource_source.native_suites else { + return Ok(()); + }; + for origin in &transition.origins { + if !program.resource_source.belongs_to(&origin.checked) { + return Err("native suite conversion belongs to another checked program".into()); + } + program + .resource_source + .validate_current(program, &origin.checked.checked().interner)?; + } + Ok(()) +} + +/// Only the canonical conversion pass calls this after authenticating its input. +/// Peer transitions still receive the exact unchanged-before archive. +pub(crate) fn converted( + candidate: &Program, + before: &Program, +) -> Result>, String> { + validate_conversion_input(before)?; + let Some(previous) = &before.resource_source.native_suites else { + return Ok(None); + }; + if candidate.resource_source != before.resource_source { + return Err("native suite conversion replaced its unchanged-before archive".into()); + } + Ok(Some(Arc::new(transition( + previous.origins.clone(), + &candidate.functions, + )?))) +} + +pub(crate) fn callable_sealing( + program: &Program, + before: &Program, + edits: &[(crate::ReflectedCallablePlan, crate::ReflectedCallablePlan)], +) -> Result>, Vec> { + let result = (|| -> Result>, String> { + validate_conversion_input(before)?; + if before.resource_source.native_suites.is_none() { + return Ok(None); + } + for checked in [before, program] { + crate::mutable_sum_backings::validate_program(checked) + .and_then(|()| crate::actor_handlers::validate_program(checked)) + .map_err(|errors| format!("native suite independent receipts: {errors:?}"))?; + } + let mut replay = before.clone(); + for owner in &mut replay.functions { + crate::resource_materialization::walk_block(&mut owner.body, &mut |expression| { + let replace = |plan: &mut crate::ReflectedCallablePlan| { + if let Some((_, current)) = edits.iter().find(|(previous, _)| previous == plan) + { + *plan = current.clone(); + } + }; + match &mut expression.kind { + ExpressionKind::Intrinsic { + field_validation: Some(crate::ReflectedFieldValidation::Validate(fields)), + .. + } => { + for field in fields { + if let crate::ReflectedFieldAction::CallableReturn(plan) = + &mut field.action + { + replace(plan); + } + } + } + ExpressionKind::Handle { + kind: + crate::HandleKind::ReflectedReturn { + callable: Some(plan), + .. + }, + .. + } => replace(plan), + _ => {} + } + Ok(()) + })?; + } + let semantic = |program: &Program| { + let mut functions = program.functions.clone(); + for function in &mut functions { + // These three peer inventories passed their own complete check + // above. Every actual operation and temporary marker stays exact. + function.mutable_sum_backings.clear(); + function.actor_handler = None; + function.actor_state_views.clear(); + } + functions + }; + if !crate::resource_materialization::functions_equal(&semantic(program), &semantic(&replay)) + { + return Err("native suite callable sealing changed outside exact token replay".into()); + } + converted(program, before) + })(); + result.map_err(|message| { + vec![crate::LowerError { + span: before + .functions + .first() + .map_or(jett_common::Span::new(FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + }) +} + +#[cfg(test)] +#[path = "native_suite_transition_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/native_suite_transition_tests.rs b/crates/jett_hir/src/native_suite_transition_tests.rs new file mode 100644 index 00000000..796a1a7b --- /dev/null +++ b/crates/jett_hir/src/native_suite_transition_tests.rs @@ -0,0 +1,512 @@ +use super::*; +use crate::{CallOwnership, DeclarationKind}; +use jett_common::SourceOrigin; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_comptime::verify::{ + CheckedPropertyCases, collect_checked_property_cases_with_checked_values, +}; +use jett_resolve::ResourceKernelSpec; +use jett_types::ResourceKernelRecipe; +use std::collections::HashMap; + +const SOURCE17: &str = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett" +); +const SUPPORT: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + +fn checked(release: bool) -> Arc { + let support = FileId::new(10_000); + let entry = FileId::new(0); + let mut parsed = jett_parser::parse(SUPPORT, support); + let source = jett_parser::parse(SOURCE17, entry); + assert!(parsed.errors.is_empty() && source.errors.is_empty()); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + jett_parser::ast::Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one original Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(source.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (entry, SourceOrigin::Project), + ]), + &catalog, + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ) +} + +fn capture(checked: &Arc) -> Arc { + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes::from_checked_program(checked)), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + Arc::new(evaluation.values) +} + +fn prepared( + release: bool, +) -> ( + Program, + Arc, + Arc, +) { + let checked = checked(release); + let values = capture(&checked); + let mut program = crate::lower_checked_resource_program_with_test_bodies(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + (program, checked, values) +} + +fn cases( + checked: &Arc, + values: &Arc, +) -> CheckedPropertyCases { + collect_checked_property_cases_with_checked_values(checked.clone(), values.clone()).unwrap() +} + +fn receipts(program: &Program, entry: FunctionId) -> Vec { + let mut body = program.functions[entry.index() as usize].body.clone(); + let mut ordinal = 0; + let mut output = Vec::new(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + ordinal += 1; + if let ExpressionKind::Call { + ownership: CallOwnership::Generated(packet), + .. + } = &expression.kind + && matches!( + packet.operation, + crate::GeneratedOperation::NativeSuite { .. } + ) + { + output.push( + program + .resource_source + .native_suite_call(entry, expression, ordinal) + .unwrap() + .clone(), + ); + } + Ok(()) + }) + .unwrap(); + output +} + +#[test] +fn source17_native_suite_transition_retains_whole_source_and_distinct_trial_occurrences() { + for release in [false, true] { + let (mut program, checked, values) = prepared(release); + let original = program.clone(); + assert!(!program.resource_manifest.kinds().is_empty()); + assert!( + program + .functions + .iter() + .any(|function| function.identity.declaration.name == "refined_shape_contract") + ); + let verify = + crate::append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + let cases = cases(&checked, &values); + let plan = crate::append_checked_native_property_suite( + &mut program, + &checked, + FileId::new(0), + &cases, + ) + .unwrap() + .unwrap(); + assert_eq!(plan.trials().len(), cases.cases().len()); + assert_eq!( + plan.trials().len(), + jett_comptime::verify::PROPERTY_DEFAULT_ITERATIONS + ); + assert_eq!(program.resource_manifest, original.resource_manifest); + assert_eq!(program.equality_methods, original.equality_methods); + assert!(crate::resource_materialization::functions_equal( + program.resource_source.functions(), + original.resource_source.functions() + )); + assert!(crate::resource_materialization::functions_equal( + &program.functions[..original.functions.len()], + &original.functions + )); + assert_eq!(receipts(&program, verify).len(), 1); + let calls = receipts(&program, plan.entry()); + assert_eq!(calls.len(), plan.trials().len()); + for (index, call) in calls.iter().enumerate() { + assert!(call.belongs_to_archive(&program.resource_source)); + assert!(calls[..index].iter().all(|other| !call.same_receipt(other))); + if let Some(previous) = index.checked_sub(1) { + assert!(call.expression_ordinal() > calls[previous].expression_ordinal()); + } + } + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + assert!(!calls[0].belongs_to_archive(&program.resource_source)); + assert_eq!(receipts(&program, plan.entry()).len(), plan.trials().len()); + } +} + +#[test] +fn source17_native_suite_append_refuses_changed_originals_and_manufactured_wrapper() { + let (original, checked, _) = prepared(false); + let (foreign, _, _) = prepared(false); + for case in 0..5 { + let mut changed = original.clone(); + match case { + 0 => changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "empty_tokens") + .unwrap() + .body + .statements + .clear(), + 1 => changed.resource_source = foreign.resource_source.clone(), + 2 => changed.resource_source = ResourceSourceArchive::empty(), + 3 => changed.resource_manifest = crate::ResourceManifest::default(), + 4 => { + let mut fake = changed + .functions + .iter() + .find(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .unwrap() + .clone(); + fake.id = FunctionId::new(u32::try_from(changed.functions.len()).unwrap()); + fake.identity.declaration.name = "__native_verify_suite:forged".into(); + changed.functions.push(fake); + } + _ => unreachable!(), + } + let before = changed.clone(); + assert!( + crate::append_checked_native_verify_suite(&mut changed, &checked, FileId::new(0)) + .is_err(), + "case {case}" + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &changed.functions + )); + assert_eq!(before.resource_source, changed.resource_source); + } +} + +#[test] +fn source17_native_property_requires_exact_cache_and_replays_original_case_identities() { + let (mut program, checked, values) = prepared(false); + let fresh = capture(&checked); + let fresh_cases = cases(&checked, &fresh); + assert!( + crate::append_checked_native_property_suite( + &mut program, + &checked, + FileId::new(0), + &fresh_cases + ) + .is_err() + ); + let original_cases = cases(&checked, &values); + let plan = crate::append_checked_native_property_suite( + &mut program, + &checked, + FileId::new(0), + &original_cases, + ) + .unwrap() + .unwrap(); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + let selected = plan.trials()[7..9] + .iter() + .map(|trial| trial.case().clone()) + .collect::>(); + let replay = crate::rebuild_checked_native_property_replay( + &program, + &plan, + &selected, + &checked.checked().interner, + ) + .unwrap(); + assert_eq!(receipts(&replay, plan.entry()).len(), selected.len()); + let reordered = [selected[1].clone(), selected[0].clone()]; + assert!( + crate::rebuild_checked_native_property_replay( + &program, + &plan, + &reordered, + &checked.checked().interner + ) + .is_err() + ); + let (mut foreign, foreign_checked, foreign_values) = prepared(false); + let foreign_cases = cases(&foreign_checked, &foreign_values); + let foreign_plan = crate::append_checked_native_property_suite( + &mut foreign, + &foreign_checked, + FileId::new(0), + &foreign_cases, + ) + .unwrap() + .unwrap(); + assert!( + crate::rebuild_checked_native_property_replay( + &program, + &foreign_plan, + &selected, + &checked.checked().interner + ) + .is_err() + ); +} + +#[test] +fn source17_native_suite_current_table_cannot_renew_mutated_trial_or_missing_receipts() { + let (mut original, checked, values) = prepared(false); + let sampled = cases(&checked, &values); + let plan = crate::append_checked_native_property_suite( + &mut original, + &checked, + FileId::new(0), + &sampled, + ) + .unwrap() + .unwrap(); + for case in 0..4 { + let mut changed = original.clone(); + let wrapper = &mut changed.functions[plan.entry().index() as usize]; + match case { + 0 => { + wrapper.body.statements.remove(0); + } + 1 => { + wrapper.body.statements.swap(0, 3); + } + 2 => { + wrapper + .body + .statements + .push(wrapper.body.statements[0].clone()); + } + 3 => { + wrapper.identity.declaration.namespace = "foreign".into(); + } + _ => unreachable!(), + }; + assert!( + crate::complete_value_conversions(&mut changed, &checked.checked().interner).is_err(), + "case {case}" + ); + } + let mut missing = original.clone(); + missing.resource_source.native_suites = None; + assert!( + missing + .resource_source + .native_suite_function(plan.entry()) + .is_none() + ); + assert!( + missing + .resource_source + .validate_current(&missing, &checked.checked().interner) + .is_err() + ); + assert!( + crate::append_checked_native_verify_suite(&mut missing, &checked, FileId::new(0)).is_err() + ); +} + +const TEMPORARY: &str = "namespace app\nstruct Packet:\n data: list[int64]\n sibling: list[int64]\nfunction make_packet() returns Packet:\n return Packet(data: list(1, 2), sibling: list(3, 4))\nfunction inspect(view data: list[int64]) returns nothing:\n return nothing\nfunction main() returns nothing:\n list[int64] borrowed = view make_packet().data\n inspect(view borrowed)\n list[int64] forwarded = borrowed\n inspect(view forwarded)\n list[int64] independent = clone forwarded\nverify independent:\n assert true\nproperty independent_trials:\n given marker: int64\n assert marker == marker\n"; + +#[test] +fn temporary_only_archive_appends_verify_and_property_without_graph_or_resource_authority() { + for release in [false, true] { + let file = FileId::new(0); + let checked = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(TEMPORARY, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ); + let values = capture(&checked); + let mut program = crate::lower_checked_ordinary_program_with_test_bodies(&checked).unwrap(); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() + .id; + let original_markers = crate::temporary_local_backings( + &program, + &program.functions[main.index() as usize], + &checked.checked().interner, + ) + .unwrap(); + assert_eq!(original_markers.len(), 1); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_source.graph_values.is_none()); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + crate::append_checked_native_verify_suite(&mut program, &checked, file) + .unwrap() + .unwrap(); + let sampled = cases(&checked, &values); + let plan = + crate::append_checked_native_property_suite(&mut program, &checked, file, &sampled) + .unwrap() + .unwrap(); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + assert_eq!( + crate::temporary_local_backings( + &program, + &program.functions[main.index() as usize], + &checked.checked().interner, + ) + .unwrap(), + original_markers + ); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_source.graph_values.is_none()); + assert_eq!( + receipts(&program, plan.entry()).len(), + sampled.cases().len() + ); + } +} + +#[test] +fn suite_and_temporary_tables_replay_fresh_required_callable_sealing() { + let reflected = "namespace app\ntype Positive = int64 where value > 0\nstruct Holder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction positive() returns Positive:\n return 5\nfunction stored() returns function() returns Positive:\n Holder holder = Holder(callback: five)\n mutable function() returns Positive selected = positive\n for field in type.fields[Holder]():\n selected = type.field_value[Holder, function() returns Positive](view holder, view field)\n return selected\n"; + let source = TEMPORARY.replace("namespace app\n", reflected).replace( + "function main() returns nothing:\n", + "function main() returns nothing:\n function() returns Positive baked = comptime stored()\n int64 observed = coarsen baked()\n", + ); + let file = FileId::new(0); + let checked = Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(&source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .unwrap(), + ); + let values = capture(&checked); + let mut program = crate::lower_checked_ordinary_program_with_test_bodies(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + let mut staged = false; + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::InterfaceCoerce { adapters, .. } = &expression.kind { + staged |= adapters.iter().any(|adapter| { + adapter + .reflected + .as_ref() + .is_some_and(|plan| plan.is_materialized() && !plan.is_sealed()) + }); + } + Ok(()) + }) + .unwrap(); + } + assert!( + staged, + "required callable remains at its exact pre-conversion adapter site" + ); + crate::append_checked_native_verify_suite(&mut program, &checked, file) + .unwrap() + .unwrap(); + let sampled = cases(&checked, &values); + let plan = crate::append_checked_native_property_suite(&mut program, &checked, file, &sampled) + .unwrap() + .unwrap(); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + assert!( + program + .functions + .iter() + .flat_map(crate::reflected_callable_plans) + .all(|plan| plan.is_sealed()) + ); + assert_eq!( + receipts(&program, plan.entry()).len(), + sampled.cases().len() + ); + // A subsequent pass must validate the fully sealed table, not the earlier + // pre-sealing snapshot or a callable-metadata-erased approximation. + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); +} diff --git a/crates/jett_hir/src/native_verify_suite.rs b/crates/jett_hir/src/native_verify_suite.rs new file mode 100644 index 00000000..6f806803 --- /dev/null +++ b/crates/jett_hir/src/native_verify_suite.rs @@ -0,0 +1,239 @@ +//! Canonical suite construction over an exact retained checked ordinary table. +use crate::{ + Block, CallOwnership, DeclarationId, DeclarationKind, Expression, ExpressionKind, Function, + FunctionDebugKind, FunctionId, FunctionIdentity, GeneratedOperation, LowerError, Program, + Statement, StatementKind, +}; +use jett_common::{FileId, Span}; +use jett_parser::ast::Item; +use jett_types::TypeInterner; +use std::sync::Arc; + +/// Append only the original checked entry file's zero-argument verify bodies. +/// The caller supplies neither a body nor a list of target IDs. Empty archives +/// cannot renew protected required-value or Graph custody through this boundary. +pub fn append_checked_native_verify_suite( + program: &mut Program, + checked: &Arc, + entry_file: FileId, +) -> Result, Vec> { + let span = program + .functions + .first() + .map_or(Span::new(entry_file, 0, 0), |function| function.span); + let result = append(program, checked, entry_file); + result.map_err(|message| vec![LowerError { span, message }]) +} + +fn append( + program: &mut Program, + checked: &Arc, + entry_file: FileId, +) -> Result, String> { + let retained = program + .resource_source + .checked_program() + .ok_or("native verify transition has no retained checked ordinary Source")? + .clone(); + if !Arc::ptr_eq(&retained, checked) || !program.resource_source.belongs_to(checked) { + return Err("native verify transition belongs to another checked Source".into()); + } + let types = &checked.checked().interner; + program.resource_source.validate_current(program, types)?; + crate::reflected_callable::baked::validate_conversion_input(program).map_err(messages)?; + crate::actor_graph::validate_conversion_input(program).map_err(messages)?; + let targets = original_targets(program, entry_file)?; + let Some(first) = targets.first() else { + return Ok(None); + }; + let namespace = first.identity.declaration.namespace.clone(); + let span = first.span; + let name = format!("__native_verify_suite:{}", span.start); + if program.functions.iter().any(|function| { + function.span == span + && function.identity.declaration.name == name + && function.identity.declaration.kind == DeclarationKind::Verify + }) { + return Err("native verify transition already has its canonical suite".into()); + } + let id = FunctionId::new( + u32::try_from(program.functions.len()) + .map_err(|_| "native verify suite function table exceeds its ID range")?, + ); + let statements = targets + .iter() + .map(|target| suite_call(target, &program.functions, types)) + .collect::, _>>()?; + let suite = Function { + id, + debug_kind: FunctionDebugKind::named(&namespace, &name), + identity: FunctionIdentity { + declaration: DeclarationId { + origin: first.identity.declaration.origin.clone(), + namespace, + name, + kind: DeclarationKind::Verify, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: jett_typecheck::CheckedGenericSpecialization::default(), + }, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: Vec::new(), + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals: Vec::new(), + body: Block { statements, span }, + span, + }; + let mut candidate = program.clone(); + candidate.functions.push(suite.clone()); + // The candidate differs by exactly the private factory's canonical suite. + // Each existing transition checks the unchanged archive and exact old table + // before retaining the new complete table, including this appended body. + let mut graph_candidate = candidate.clone(); + crate::actor_graph::complete_conversion(&mut graph_candidate, program).map_err(messages)?; + crate::reflected_callable::baked::complete_conversion(&mut candidate, program) + .map_err(messages)?; + candidate.resource_source.graph_values = graph_candidate.resource_source.graph_values; + crate::native_suite_transition::append_verify( + &mut candidate, + program, + checked, + entry_file, + &suite, + targets.iter().map(|target| target.id).collect(), + )?; + crate::reflected_callable::baked::validate_conversion_input(&candidate).map_err(messages)?; + crate::actor_graph::validate_conversion_input(&candidate).map_err(messages)?; + candidate + .resource_source + .validate_current(&candidate, types)?; + *program = candidate; + Ok(Some(id)) +} + +fn original_targets(program: &Program, entry_file: FileId) -> Result, String> { + let checked = program + .resource_source + .checked_program() + .ok_or("native verify targets lost their checked Source")?; + let mut namespaces = std::collections::HashMap::new(); + let mut targets = Vec::new(); + for item in &checked.module().items { + if let Item::Namespace(namespace) = item { + namespaces.insert(namespace.span.file, namespace.name.name.as_str()); + continue; + } + let Item::Verify(verify) = item else { + continue; + }; + if verify.span.file != entry_file { + continue; + } + let namespace = namespaces + .get(&verify.span.file) + .copied() + .unwrap_or_default(); + let origin = checked + .source_origins() + .get(&verify.span.file) + .ok_or("native verify target has no checked Source origin")?; + let name = format!("{}:{}", verify.name.name, verify.span.start); + let mut originals = program + .resource_source + .functions() + .iter() + .filter(|function| { + function.span == verify.span + && function.identity.declaration.kind == DeclarationKind::Verify + && function.identity.declaration.namespace == namespace + && function.identity.declaration.name == name + && &function.identity.declaration.origin == origin + }); + let original = originals + .next() + .ok_or("native verify target has no exact original checked HIR body")?; + if originals.next().is_some() { + return Err("native verify target has multiple original checked HIR bodies".into()); + } + let current = program + .functions + .get(original.id.index() as usize) + .ok_or("native verify target disappeared from its protected current table")?; + if current.id != original.id + || current.identity != original.identity + || current.debug_kind != original.debug_kind + || current.source_definition != original.source_definition + || current.span != original.span + || current.body.span != original.body.span + || !current.params.is_empty() + || current.capture_count != 0 + || current.return_type != TypeInterner::NOTHING + || !current.identity.scoped_type_bindings.is_empty() + || !current.identity.type_arguments.is_empty() + || current.identity.specialization + != jett_typecheck::CheckedGenericSpecialization::default() + { + return Err( + "native verify target changed its exact checked zero-argument header".into(), + ); + } + targets.push(current.clone()); + } + Ok(targets) +} + +fn suite_call( + target: &Function, + functions: &[Function], + types: &TypeInterner, +) -> Result { + let ownership = CallOwnership::generated( + GeneratedOperation::NativeSuite { + function: target.id, + }, + &[], + &[], + &[], + TypeInterner::NOTHING, + &[], + types, + )?; + let expression = Expression { + kind: ExpressionKind::Call { + function: target.id, + args: Vec::new(), + evaluation_order: Vec::new(), + ownership, + }, + ty: TypeInterner::NOTHING, + span: target.span, + }; + crate::validate_hir_invocation( + functions, + &[], + &expression, + types, + jett_typecheck::CheckedOwnershipContext::Verify, + )?; + Ok(Statement { + kind: StatementKind::Expression(expression), + span: target.span, + }) +} + +fn messages(errors: Vec) -> String { + errors + .into_iter() + .map(|error| error.message) + .collect::>() + .join("; ") +} + +#[cfg(test)] +#[path = "native_verify_suite_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/native_verify_suite_tests.rs b/crates/jett_hir/src/native_verify_suite_tests.rs new file mode 100644 index 00000000..dc6ed6ff --- /dev/null +++ b/crates/jett_hir/src/native_verify_suite_tests.rs @@ -0,0 +1,248 @@ +use super::*; +use jett_common::SourceOrigin; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = "namespace app\nactor Counter:\n mutable int64 count = 1\n receive read responds int64:\n respond count\nfunction make() returns Counter:\n return spawn Counter()\nverify first:\n Counter counter = make()\n int64 observed = ask counter.read\n assert observed == 1\nverify second:\n assert true\n"; + +fn prepared(source: &str) -> (Program, Arc) { + let file = FileId::new(0); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + let mut program = crate::lower_checked_ordinary_program_with_test_bodies(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &evaluation.values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + (program, checked) +} + +#[test] +fn checked_native_verify_suite_preserves_actor_and_graph_tables_and_original_order() { + for source in [ + SOURCE.to_owned(), + SOURCE.replace("return spawn", "return comptime spawn"), + ] { + let (mut program, checked) = prepared(&source); + let before = program.clone(); + let expected = before + .resource_source + .functions() + .iter() + .filter(|function| function.identity.declaration.kind == DeclarationKind::Verify) + .map(|function| (function.id, function.span)) + .collect::>(); + let entry = append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &program.functions[..before.functions.len()] + )); + assert_eq!(program.resource_manifest, before.resource_manifest); + assert_eq!(program.equality_methods, before.equality_methods); + let suite = &program.functions[entry.index() as usize]; + assert!(suite.params.is_empty() && suite.locals.is_empty()); + assert_eq!(suite.capture_count, 0); + assert_eq!(suite.return_type, TypeInterner::NOTHING); + let actual = suite + .body + .statements + .iter() + .map(|statement| { + let StatementKind::Expression(Expression { + kind: + ExpressionKind::Call { + function, + args, + evaluation_order, + .. + }, + ty, + span, + }) = &statement.kind + else { + panic!("canonical suite call"); + }; + assert!(args.is_empty() && evaluation_order.is_empty()); + assert_eq!(*ty, TypeInterner::NOTHING); + (*function, *span) + }) + .collect::>(); + assert_eq!(actual, expected); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + assert!( + append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)).is_err() + ); + } +} + +#[test] +fn checked_native_verify_suite_refuses_changed_original_body_and_foreign_or_empty_archive() { + for source in [ + SOURCE.to_owned(), + SOURCE.replace("return spawn", "return comptime spawn"), + ] { + let (original, checked) = prepared(&source); + let (foreign, _) = prepared(&source); + for case in 0..7 { + let mut program = original.clone(); + match case { + 0 => { + let owner = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "make") + .unwrap(); + owner.body.statements.clear(); + } + 1 => program.resource_source = foreign.resource_source.clone(), + 2 => program.resource_source = crate::ResourceSourceArchive::empty(), + 3 => { + let verify = program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::Verify + }) + .unwrap(); + verify.return_type = TypeInterner::BOOL; + } + 4 => { + let verify = program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind == DeclarationKind::Verify + }) + .unwrap(); + verify.body.statements.clear(); + } + 5 => { + let mut forged = program.functions.last().unwrap().clone(); + forged.id = FunctionId::new(u32::try_from(program.functions.len()).unwrap()); + forged.identity.declaration.name = "__native_verify_suite:forged".into(); + program.functions.push(forged); + } + 6 => { + program + .equality_methods + .insert(TypeInterner::INT64, FunctionId::new(0)); + } + _ => unreachable!(), + } + let before = program.clone(); + assert!( + append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)).is_err(), + "case {case}" + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &program.functions + )); + assert_eq!(program.resource_source, before.resource_source); + } + } +} + +#[test] +fn checked_native_verify_suite_rejects_extra_reordered_mutated_calls_and_headers() { + for source in [ + SOURCE.to_owned(), + SOURCE.replace("return spawn", "return comptime spawn"), + ] { + let (mut original, checked) = prepared(&source); + let entry = append_checked_native_verify_suite(&mut original, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + for case in 0..6 { + let mut program = original.clone(); + let suite = &mut program.functions[entry.index() as usize]; + match case { + 0 => suite.body.statements.push(suite.body.statements[0].clone()), + 1 => suite.body.statements.swap(0, 1), + 2 => { + let StatementKind::Expression(expression) = &mut suite.body.statements[0].kind + else { + unreachable!(); + }; + expression.ty = TypeInterner::BOOL; + } + 3 => suite.identity.declaration.namespace = "other".into(), + 4 => suite.return_type = TypeInterner::BOOL, + 5 => { + let StatementKind::Expression(Expression { + kind: ExpressionKind::Call { function, .. }, + .. + }) = &mut suite.body.statements[0].kind + else { + unreachable!(); + }; + *function = entry; + } + _ => unreachable!(), + } + assert!( + crate::complete_value_conversions(&mut program, &checked.checked().interner) + .is_err(), + "mutated canonical suite {case}" + ); + assert!( + append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)).is_err(), + "mutated suite cannot renew custody {case}" + ); + } + } +} + +#[test] +fn checked_native_verify_suite_without_entry_file_verifies_keeps_the_exact_archive() { + let (mut program, checked) = prepared(SOURCE); + let before = program.clone(); + assert_eq!( + append_checked_native_verify_suite(&mut program, &checked, FileId::new(1)).unwrap(), + None + ); + assert!(crate::resource_materialization::functions_equal( + &before.functions, + &program.functions + )); + assert_eq!(program.resource_source, before.resource_source); +} diff --git a/crates/jett_hir/src/ordinary_required_values.rs b/crates/jett_hir/src/ordinary_required_values.rs new file mode 100644 index 00000000..ae1c7637 --- /dev/null +++ b/crates/jett_hir/src/ordinary_required_values.rs @@ -0,0 +1,370 @@ +//! Materialize explicitly evaluated values as typed, compiler-owned HIR. +//! +//! Ordinary expressions are never evaluated here. A failed or absent baked +//! value cannot fall back to running the original computation at runtime. + +use crate::{ + Block, Expression, ExpressionKind as E, Local, LowerError, Program, Statement, + StatementKind as S, StringSegment, +}; +use jett_common::Span; +use jett_comptime::{ComptimeContext, ExplicitComptimeValues}; +use jett_types::{ReflectionMetadata, TypeInterner}; +use std::collections::HashSet; + +use crate::evaluated_values::{ + FunctionValueCandidate, ValueContext, function_value_candidates, value_expression, +}; + +pub fn bake_required_values( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, + reflection: &ReflectionMetadata, + method_value_definitions: &HashSet, +) -> Result<(), Vec> { + crate::materialize_actor_graphs(program, values, types, reflection, method_value_definitions)?; + if !program.resource_manifest.kinds().is_empty() { + return crate::materialize_checked_required_values(program, values, types); + } + let archive = program.resource_source.clone(); + let sealed_graphs = + !archive.required_materializations().is_empty() && values.has_actor_graphs(); + // Any retained checked ordinary archive must carry replacements through + // the private required-value transition; raw baking cannot advance it. + let checked_archive = archive.checked_program().is_some(); + if checked_archive { + archive + .validate_current(program, types) + .map_err(|message| { + vec![LowerError { + span: program.functions[0].span, + message, + }] + })?; + } + let function_values = function_value_candidates(&program.functions, method_value_definitions); + if crate::has_reflected_callable_plans(program) || checked_archive { + return crate::materialize_checked_reflected_values( + program, + values, + types, + |value, ty, span, locals, bindings| { + value_expression( + value, + ty, + span, + &mut ValueContext { + actor_seeds: None, + types, + reflection, + functions: &function_values, + refinement_functions: None, + locals, + bindings, + }, + ) + }, + ); + } + let mut errors = Vec::new(); + for function in &mut program.functions { + let specialization = &function.identity.specialization; + let mut baker = Baker { + context: ComptimeContext { + type_arguments: function + .identity + .type_arguments + .iter() + .map(|ty| types.type_name(*ty)) + .collect(), + type_argument_reflections: specialization.type_argument_reflections.clone(), + type_info_kinds: specialization.type_info_kinds.clone(), + type_info_primitives: specialization.type_info_primitives.clone(), + type_kind_values: specialization.type_kind_values.clone(), + type_primitive_values: specialization.type_primitive_values.clone(), + scoped_type_bindings: scoped_bindings( + &function.identity.scoped_type_bindings, + types, + ), + }, + values, + types, + reflection, + function_values: &function_values, + locals: &mut function.locals, + bindings: Vec::new(), + errors: Vec::new(), + }; + baker.block(&mut function.body); + function.body.statements.splice(0..0, baker.bindings); + errors.extend(baker.errors); + } + if errors.is_empty() { + if sealed_graphs { + program + .resource_source + .install_materialized(crate::resource_source::Materialized { + functions: program.functions.clone(), + values: archive.required_materializations().to_vec(), + required_only: archive.required_only_function_ids().to_vec(), + }); + } + Ok(()) + } else { + Err(errors) + } +} + +fn scoped_bindings( + bindings: &[crate::ScopedTypeBinding], + types: &TypeInterner, +) -> Vec { + bindings + .iter() + .map(|binding| jett_comptime::value::ClosureScopedTypeBinding { + name: binding.name.clone(), + canonical_name: types.type_name(binding.ty), + reflection: Some(binding.reflection.clone()), + }) + .collect() +} + +struct Baker<'a> { + context: ComptimeContext, + values: &'a ExplicitComptimeValues, + types: &'a TypeInterner, + reflection: &'a ReflectionMetadata, + function_values: &'a [FunctionValueCandidate], + locals: &'a mut Vec, + bindings: Vec, + errors: Vec, +} + +impl Baker<'_> { + fn block(&mut self, body: &mut Block) { + for statement in &mut body.statements { + match &mut statement.kind { + S::Let { value, .. } + | S::Expression(value) + | S::HandleDefault(value) + | S::Respond(value) => self.expression(value), + S::Assign { target, value } | S::UpdateField { target, value, .. } => { + self.expression(target); + self.expression(value); + } + S::Return(value) + | S::Breakpoint { + condition: value, .. + } => { + if let Some(value) = value { + self.expression(value); + } + } + S::If { + condition, + then_block, + else_block, + } => { + self.expression(condition); + self.block(then_block); + if let Some(body) = else_block { + self.block(body); + } + } + S::While { condition, body } => { + self.expression(condition); + self.block(body); + } + S::For { iterable, body, .. } => { + self.expression(iterable); + self.block(body); + } + S::Match { scrutinee, arms } => { + self.expression(scrutinee); + for arm in arms { + self.block(&mut arm.body); + } + } + S::Assert { condition, message } => { + self.expression(condition); + if let Some(message) = message { + self.expression(message); + } + } + S::Scope(body) => self.block(body), + S::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info); + for arm in arms { + self.block(&mut arm.body); + } + } + S::Break | S::Continue | S::Trace(_) => {} + } + } + } + + fn expression(&mut self, expr: &mut Expression) { + if let E::Constant { declaration } = expr.kind { + match self.values.constant(declaration) { + Some(value) => match value_expression( + value, + expr.ty, + expr.span, + &mut ValueContext { + actor_seeds: None, + types: self.types, + reflection: self.reflection, + functions: self.function_values, + refinement_functions: None, + locals: &mut *self.locals, + bindings: &mut self.bindings, + }, + ) { + Ok(replacement) => *expr = replacement, + Err(message) => self.errors.push(LowerError { + span: expr.span, + message: format!("cannot materialize constant: {message}"), + }), + }, + None => self.errors.push(LowerError { + span: expr.span, + message: "namespace constant has no checked compile-time value".into(), + }), + } + return; + } + if let E::Comptime { + bindings, + source_span, + .. + } = &expr.kind + { + let mut context = self.context.clone(); + if !bindings.is_empty() { + context.scoped_type_bindings = scoped_bindings(bindings, self.types); + } + match self.values.get(*source_span, &context) { + Some(value) => match value_expression( + value, + expr.ty, + expr.span, + &mut ValueContext { + actor_seeds: None, + types: self.types, + reflection: self.reflection, + functions: self.function_values, + refinement_functions: None, + locals: &mut *self.locals, + bindings: &mut self.bindings, + }, + ) { + Ok(replacement) => *expr = replacement, + Err(message) => self.errors.push(LowerError { + span: expr.span, + message: format!("cannot materialize compile-time value: {message}"), + }), + }, + None => self.errors.push(LowerError { + span: expr.span, + message: "explicit comptime expression has no evaluated value".into(), + }), + } + return; + } + match &mut expr.kind { + // Its immutable body was materialized by the dedicated graph producer. + E::ActorGraphImport(_) => {} + E::ResourceInvoke { .. } | E::ResourceHookValue { .. } => self.errors.push(LowerError { + span: expr.span, + message: "pending ResourceOwnershipPlan: native constant materialization cannot represent a Resource operation or descriptor".into(), + }), + E::Binary { left, right, .. } => { + self.expression(left); + self.expression(right); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::RuntimeFailureMessage(value) + | E::FunctionAdapter { value, .. } + | E::InterfaceCoerce { value, .. } + | E::InterfaceType(value) + | E::StateIs { value, .. } + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Field { base: value, .. } => self.expression(value), + E::Call { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } + | E::StructConstruct { fields: args, .. } + | E::BitfieldConstruct { fields: args, .. } + | E::MachineConstruct { payloads: args, .. } + | E::EnumConstruct { payloads: args, .. } + | E::ListConstruct { elements: args } => self.expressions(args), + E::IndirectCall { callee, args, .. } => { + self.expression(callee); + self.expressions(args); + } + E::MachineTransition { + source, payloads, .. + } => { + self.expression(source); + self.expressions(payloads); + } + E::ActorMessage { actor, args, .. } => { + self.expression(actor); + self.expressions(args); + } + E::MapConstruct { entries } => { + for entry in entries { + self.expression(&mut entry.key); + self.expression(&mut entry.value); + } + } + E::Handle { + target, failure, .. + } => { + self.expression(target); + self.block(failure); + } + E::StringInterpolation(parts) => { + for part in parts { + if let StringSegment::Value(value) = part { + self.expression(value); + } + } + } + E::InlineFunction { body, .. } => self.block(body), + E::Int(_) + | E::Constant { .. } + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::PropertyCaseContext(_) + | E::RuntimeFailure(_) + | E::OptionalNone + | E::Local(_) + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::Comptime { .. } => {} + } + } + + fn expressions(&mut self, expressions: &mut [Expression]) { + for expr in expressions { + self.expression(expr); + } + } +} diff --git a/crates/jett_hir/src/reflected_callable.rs b/crates/jett_hir/src/reflected_callable.rs new file mode 100644 index 00000000..9ba357d1 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable.rs @@ -0,0 +1,1209 @@ +//! Constructor-only reflected callable evidence. Labels are diagnostic data; +//! custody comes from the immutable checked session and original typed tuple. + +use super::*; + +#[path = "reflected_callable_baked.rs"] +pub(crate) mod baked; +#[path = "reflected_callable_materialized.rs"] +mod materialized_source; +pub use baked::{has_reflected_callable_plans, materialize_checked_reflected_values}; + +#[path = "reflected_callable_sealing_receipt.rs"] +mod sealing; +pub(crate) use sealing::CheckedCallableSealing; + +#[derive(Debug, Clone)] +pub struct ReflectedCallablePlan { + data: Arc, +} + +#[derive(Debug, Clone)] +struct Plan { + checked: Arc, + admission: jett_typecheck::CheckedReflectedCallablePlan, + returned: ReflectedFieldPlan, + span: Span, + function: Option, + context: Option, + seal: Option>, + materialized: bool, + ordinal: Option, + reference: Option>, + graph_source: Option, +} + +#[derive(Debug)] +struct Seal { + owner: FunctionId, + getter: Expression, + adapter: Function, + predicates: Vec, + sources: Arc>, +} + +impl PartialEq for ReflectedCallablePlan { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ReflectedCallablePlan {} + +impl ReflectedCallablePlan { + pub(crate) fn new( + checked: Arc, + admission: jett_typecheck::CheckedReflectedCallablePlan, + returned: ReflectedFieldPlan, + span: Span, + ordinal: Option, + ) -> Self { + Self { + data: Arc::new(Plan { + checked, + admission, + returned, + span, + function: None, + context: None, + seal: None, + materialized: false, + ordinal, + reference: None, + graph_source: None, + }), + } + } + + pub(crate) fn bind_graph_source(&mut self, source: super::actor_graph::CallbackSource) { + Arc::make_mut(&mut self.data).graph_source = Some(source); + } + + pub(crate) fn graph_source(&self) -> Option<&super::actor_graph::CallbackSource> { + self.data.graph_source.as_ref() + } + + pub fn source_type(&self) -> TypeId { + self.data.admission.source_type() + } + pub fn requested_type(&self) -> TypeId { + self.data.admission.requested_type() + } + pub fn checked_plan(&self) -> &jett_typecheck::CheckedReflectedCallablePlan { + &self.data.admission + } + pub fn return_plan(&self) -> &ReflectedFieldPlan { + &self.data.returned + } + pub fn function(&self) -> FunctionId { + self.data.function.unwrap_or(FunctionId(u32::MAX)) + } + pub fn canonical_adapter(&self) -> Option<&Function> { + self.data.seal.as_ref().map(|seal| &seal.adapter) + } + pub fn original_owner(&self) -> Option { + self.data.seal.as_ref().map(|seal| seal.owner) + } + pub fn original_getter(&self) -> Option<&Expression> { + self.data.seal.as_ref().map(|seal| &seal.getter) + } + pub fn original_predicates(&self) -> &[Function] { + self.data + .seal + .as_ref() + .map_or(&[], |seal| seal.predicates.as_slice()) + } + pub fn is_materialized(&self) -> bool { + self.data.materialized + } + pub fn is_sealed(&self) -> bool { + self.data.seal.is_some() + } + + /// Readonly join for original checked test observations. The full current + /// getter/source graph is validated separately by validate_program. + pub(crate) fn authenticates_observation_read( + &self, + checked: &Arc, + span: Span, + context: jett_typecheck::CheckedOwnershipContext, + ) -> bool { + Arc::ptr_eq(&self.data.checked, checked) + && self.data.span == span + && self.validate_original() + && !self.data.materialized + && self.data.context.as_ref().is_some_and(|owner| { + matches!( + (context, owner.declaration.kind), + ( + jett_typecheck::CheckedOwnershipContext::Verify, + DeclarationKind::Verify + ) | ( + jett_typecheck::CheckedOwnershipContext::Property, + DeclarationKind::Property + ) + ) + }) + } + + pub(crate) fn resolve_return(&mut self, functions: &[Function]) -> Result<(), String> { + if self.data.materialized && self.data.function.is_none() { + let returned = returned_plan( + &self.data.checked, + functions, + ReflectedFieldRequirementPlan { + source_type: self.data.admission.source_return(), + requested_type: self.data.admission.requested_return(), + requirement: self.data.admission.return_requirement().clone(), + }, + )?; + Arc::make_mut(&mut self.data).returned = returned; + } + Ok(()) + } + + pub(crate) fn bind(&mut self, function: FunctionId, context: &FunctionIdentity) { + let data = Arc::make_mut(&mut self.data); + data.function = Some(function); + data.context = Some(context.clone()); + } + + pub fn validate_types(&self, types: &TypeInterner) -> Result<(), &'static str> { + let original = self.data.checked.checked(); + if types.len() < original.interner.len() { + return Err("reflected callable lost its original checked type table"); + } + for ty in original.interner.type_ids() { + let expected = original.interner.resolve(ty); + if types.resolve(ty) != expected + || types.nominal_type_arguments(ty) != original.interner.nominal_type_arguments(ty) + || !types.contains_nominal_definition(expected) + { + return Err("reflected callable original type meaning changed"); + } + let same = match expected { + Type::Struct(id) => { + types.resolve_struct(*id) == original.interner.resolve_struct(*id) + } + Type::Enum(id) => types.resolve_enum(*id) == original.interner.resolve_enum(*id), + Type::Bitfield(id) => { + types.resolve_bitfield(*id) == original.interner.resolve_bitfield(*id) + } + Type::Interface(id) => { + types.resolve_interface(*id) == original.interner.resolve_interface(*id) + } + Type::Actor(id) => types.resolve_actor(*id) == original.interner.resolve_actor(*id), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id) == original.interner.resolve_machine(*id) + } + _ => true, + }; + if !same { + return Err("reflected callable original nominal definition changed"); + } + } + if !self.data.admission.validate(original) + || self.source_type().index() as usize >= types.len() + || self.requested_type().index() as usize >= types.len() + || types.resolve(self.source_type()) != original.interner.resolve(self.source_type()) + || types.resolve(self.requested_type()) + != original.interner.resolve(self.requested_type()) + || self.data.returned.source_type != self.data.admission.source_return() + || self.data.returned.requested_type != self.data.admission.requested_return() + { + return Err("reflected callable admission differs from its checked session"); + } + let expected = ReflectedFieldRequirementPlan { + source_type: self.data.returned.source_type, + requested_type: self.data.returned.requested_type, + requirement: self.data.admission.return_requirement().clone(), + }; + super::reflected_fields::validate_requirement_node(types, &expected, &self.data.returned) + } + + pub fn validate_original(&self) -> bool { + let Some(seal) = &self.data.seal else { + return false; + }; + self.validate_types(&self.data.checked.checked().interner) + .is_ok() + && self.data.function == Some(seal.adapter.id) + && self.data.context.as_ref().is_some_and(|context| { + seal.adapter.identity.scoped_type_bindings == context.scoped_type_bindings + && seal.adapter.identity.type_arguments == context.type_arguments + && seal.adapter.identity.specialization == context.specialization + && seal.adapter.identity.declaration.origin == context.declaration.origin + && seal.adapter.identity.declaration.namespace == context.declaration.namespace + }) + } + + pub(crate) fn validate_current( + &self, + program: &Program, + owner: &Function, + getter: &Expression, + ordinal: Option, + ) -> Result<(), &'static str> { + let seal = self + .data + .seal + .as_ref() + .ok_or("reflected callable has no original constructor seal")?; + if !self.validate_original() + || seal.owner != owner.id + || self.data.ordinal != ordinal + || self.data.context.as_ref() != Some(&owner.identity) + || getter.span != self.data.span + || !super::resource_materialization::expression_equal( + &erase_callable(getter), + &seal.getter, + ) + || !program + .functions + .get(self.function().index() as usize) + .is_some_and(|function| { + super::resource_materialization::functions_equal( + std::slice::from_ref(&erase_adapter(function)), + std::slice::from_ref(&seal.adapter), + ) + }) + || seal.predicates.iter().any(|expected| { + !program + .functions + .get(expected.id.index() as usize) + .is_some_and(|current| { + let Ok(current) = original_function( + current, + &program.resource_source, + &self.data.checked, + ) else { + return false; + }; + let Ok(expected) = original_function( + expected, + &program.resource_source, + &self.data.checked, + ) else { + return false; + }; + super::resource_materialization::functions_equal( + std::slice::from_ref(¤t), + std::slice::from_ref(&expected), + ) + }) + }) + { + return Err("reflected callable getter, adapter, predicate or context changed"); + } + if program.resource_source.checked_program().is_some() + && !program.resource_source.belongs_to(&self.data.checked) + { + return Err("reflected callable belongs to another original checked session"); + } + let context = self + .data + .context + .as_ref() + .ok_or("reflected callable context is missing")?; + let expected = adapter( + self, + self.function(), + context, + &self.data.checked.checked().interner, + ) + .ok_or("reflected callable adapter cannot be reconstructed")?; + let mut reconstruction = program.clone(); + reconstruction.functions[self.function().index() as usize] = expected; + super::interface_values::coerce_program( + &mut reconstruction, + &self.data.checked.checked().interner, + ); + if !super::resource_materialization::functions_equal( + std::slice::from_ref(&erase_adapter( + &reconstruction.functions[self.function().index() as usize], + )), + std::slice::from_ref(&seal.adapter), + ) { + return Err("reflected callable canonical body, tuple or return conversion changed"); + } + sources_match(&seal.sources, program, &self.data.checked)?; + Ok(()) + } +} + +fn erase_callable(expression: &Expression) -> Expression { + let mut erased = expression.clone(); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut erased.kind + { + for plan in plans { + if matches!(plan.action, ReflectedFieldAction::CallableReturn(_)) { + plan.action = ReflectedFieldAction::Exact; + } + } + } + erased +} + +pub(crate) fn adapter( + plan: &ReflectedCallablePlan, + id: FunctionId, + context: &FunctionIdentity, + types: &TypeInterner, +) -> Option { + plan.validate_types(types).ok()?; + let Type::Function { + params: source_params, + view_params: source_views, + return_type: source_return, + } = types.resolve(plan.source_type()) + else { + return None; + }; + let Type::Function { + params: target_params, + view_params: target_views, + return_type: target_return, + } = types.resolve(plan.requested_type()) + else { + return None; + }; + let span = plan.data.span; + let mut params = vec![Param { + local: LocalId(0), + name: "$source".into(), + ty: plan.source_type(), + mode: ParamMode::Owned, + mutable: false, + span, + }]; + params.extend( + target_params + .iter() + .zip(target_views) + .enumerate() + .map(|(index, (ty, view))| Param { + local: LocalId(index as u32 + 1), + name: format!("$argument{index}"), + ty: *ty, + mode: if *view { + ParamMode::View + } else { + ParamMode::Owned + }, + mutable: false, + span, + }), + ); + let locals = params + .iter() + .map(|param| Local { + id: param.local, + name: param.name.clone(), + ty: param.ty, + debug_ty: param.ty, + debug_type_name: None, + mutable: false, + view_source: None, + span, + }) + .collect(); + let mut args = params[1..] + .iter() + .map(|param| Expression { + kind: ExpressionKind::Local(param.local), + ty: param.ty, + span, + }) + .collect::>(); + for (argument, expected) in args.iter_mut().zip(source_params) { + super::interface_values::coerce(argument, *expected, types); + if argument.ty != *expected { + return None; + } + } + let access = source_views + .iter() + .map(|view| { + if *view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + } + }) + .collect::>(); + let order = (0..args.len()).collect::>(); + let ownership = CallOwnership::generated( + GeneratedOperation::ReflectedCallableAdapter { + callee: LocalId(0), + plan: plan.checked_plan().clone(), + }, + &args, + source_params, + &access, + *source_return, + &order, + types, + ) + .ok()?; + let invoked = Expression { + kind: ExpressionKind::IndirectCall { + ownership, + callee: Box::new(Expression { + kind: ExpressionKind::Local(LocalId(0)), + ty: plan.source_type(), + span, + }), + args, + evaluation_order: order, + }, + ty: *source_return, + span, + }; + let returned = Expression { + kind: ExpressionKind::Handle { + target: Box::new(invoked), + kind: HandleKind::ReflectedReturn { + plan: Box::new(plan.return_plan().clone()), + callable: None, + }, + error_local: None, + failure: Block { + statements: Vec::new(), + span, + }, + }, + ty: *target_return, + span, + }; + let mut identity = context.clone(); + identity.declaration.name = format!("$reflected.callable.{}", id.index()); + identity.declaration.kind = DeclarationKind::Function; + Some(Function { + id, + identity, + debug_kind: FunctionDebugKind::Inline, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params, + capture_count: 1, + return_type: *target_return, + locals, + body: Block { + statements: vec![Statement { + kind: StatementKind::Return(Some(returned)), + span, + }], + span, + }, + span, + }) +} + +pub(crate) fn seal_program( + program: &mut Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut candidate = program.clone(); + seal_program_with_receipt(&mut candidate, types)?; + *program = candidate; + Ok(()) +} + +/// Actual original lowering supplies the checked context; the token producer +/// remains the same sole sealer used by ordinary completion. +pub(super) fn seal_original_program( + program: &mut Program, + original: Option<&Arc>, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut candidate = program.clone(); + let (_, actor, _) = seal_program_with_actor_context(&mut candidate, types, original, None)?; + finish_initial_sealing(&mut candidate, types, actor)?; + *program = candidate; + Ok(()) +} + +/// Own the only unpublished construction. Neither the registry nor pending +/// cells can be returned through a public API on success or failure. +pub(super) fn seal_fresh_original_program( + mut program: Program, + original: Option<&Arc>, + types: &TypeInterner, + construction: super::temporary_local_views::Construction, +) -> Result> { + let (_, actor, construction) = + seal_program_with_actor_context(&mut program, types, original, Some(construction))?; + let completed = finish_initial_sealing(&mut program, types, actor)?; + super::temporary_local_views::finish_fresh_original(&program, types, construction, completed) + .map_err(|message| initial_error(&program, message))?; + Ok(program) +} +fn initial_error(program: &Program, message: String) -> Vec { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |f| f.span), + message, + }] +} +fn finish_initial_sealing( + program: &mut Program, + types: &TypeInterner, + actor: Option, +) -> Result, Vec> { + if let Some(actor) = actor { + super::actor_handlers::finish_original_callable_sealing(program, types, actor) + .map_err(|message| initial_error(program, message)) + } else { + super::mutable_sum_backings::seal_program(program, types)?; + Ok(None) + } +} + +fn seal_program_with_receipt<'types>( + program: &mut Program, + types: &'types TypeInterner, +) -> Result, Vec> { + let (receipt, _, _) = seal_program_with_actor_context(program, types, None, None)?; + Ok(receipt) +} + +fn seal_program_with_actor_context<'types>( + program: &mut Program, + types: &'types TypeInterner, + original: Option<&Arc>, + mut construction: Option, +) -> Result< + ( + CheckedCallableSealing<'types>, + Option, + Option, + ), + Vec, +> { + let sum_before = program.clone(); + let mut sealing_edits = Vec::new(); + let originals = Arc::new(program.functions.clone()); + // Keep the actual raw epoch; comparison canonicalizes outer callable metadata. + let sources = originals.clone(); + let mut attached = Vec::new(); + for owner in &mut program.functions { + let identity = owner.identity.clone(); + let owner_id = owner.id; + let mut expression_ordinal = 0usize; + super::resource_materialization::walk_block(&mut owner.body, &mut |expression| { + let occurrence = expression_ordinal; + expression_ordinal += 1; + let getter = erase_callable(expression); + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut expression.kind + { + for (ordinal, plan) in plans.iter_mut().enumerate() { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + if callable.is_sealed() { + continue; + } + if callable.data.ordinal != Some(ordinal) { + return Err("reflected callable selected field ordinal changed".into()); + } + let adapter = originals + .get(callable.function().index() as usize) + .ok_or("reflected callable adapter is missing")? + .clone(); + let predicates = callable + .return_plan() + .predicates() + .into_iter() + .map(|predicate| { + originals + .get(predicate.function.index() as usize) + .cloned() + .ok_or("reflected callable predicate is missing") + }) + .collect::, _>>()?; + let previous = callable.clone(); + let data = Arc::make_mut(&mut callable.data); + if data.context.as_ref() != Some(&identity) { + return Err("reflected callable constructor context changed".into()); + } + data.seal = Some(Arc::new(Seal { + owner: owner_id, + getter: getter.clone(), + adapter, + predicates, + sources: sources.clone(), + })); + sealing_edits.push(sealing::Edit { + owner: owner_id, + expression: occurrence, + slot: sealing::Slot::Getter { field: ordinal }, + previous: Some(previous), + current: callable.clone(), + }); + attached.push((callable.function(), callable.clone())); + } + } + } + Ok(()) + }) + .map_err(|message| { + vec![LowerError { + span: owner.span, + message, + }] + })?; + } + for function in originals.iter() { + let mut body = function.body.clone(); + super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Handle { kind: HandleKind::ReflectedReturn { callable: Some(callable), .. }, .. } = &expression.kind { + if !callable.is_materialized() || callable.is_sealed() { return Ok(()); } + let mut callable = callable.clone(); + let context = callable.data.context.clone().ok_or("materialized reflected callable lacks context")?; + let mut sites = Vec::new(); + for owner in originals.iter() { + let mut owner_body = owner.body.clone(); + super::resource_materialization::walk_block(&mut owner_body, &mut |value| { + if matches!(value.kind, ExpressionKind::FunctionAdapter { function: id, .. } if id == callable.function()) { + sites.push((owner.id, owner.identity.clone(), value.clone())); + } + Ok(()) + })?; + } + if sites.len() != 1 || sites[0].1 != context { return Err("materialized reflected callable has no exact construction site".into()); } + let predicates = callable.return_plan().predicates().into_iter().map(|predicate| originals.get(predicate.function.index() as usize).cloned().ok_or("materialized reflected predicate is missing")).collect::, _>>()?; + let reference = callable.data.reference.as_ref().ok_or("materialized reflected callable lost read provenance")?; + if !program.resource_source.belongs_to(reference.checked_program()) { return Err("materialized reflected callable lacks its original checked HIR archive".into()); } + let owner = &originals[sites[0].0.index() as usize]; + if !baked::owns_materialized_owner(&program.resource_source, owner) + && !callable.data.graph_source.as_ref().is_some_and(|source| { + source.validate_site(reference, &sites[0].2, owner, &program.resource_source).is_ok() + }) + { + return Err("materialized reflected callable has no protected checked required-value construction site".into()); + } + for expected in &predicates { + let original = program.resource_source.functions().get(expected.id.index() as usize).ok_or("materialized predicate is absent from original archive")?; + let expected = original_function(expected,&program.resource_source,reference.checked_program())?; + if !super::resource_materialization::functions_equal(std::slice::from_ref(original), std::slice::from_ref(&expected)) { + return Err("materialized reflected predicate body differs from original checked Source".into()); + } + } + materialized_source::validate(reference, &sites[0].2, &originals[sites[0].0.index() as usize], &originals, &program.resource_source)?; + let protected_sources = if callable.data.graph_source.is_some() { + Arc::new(super::actor_graph::conversion_sources(program)? + .iter().map(erase_function).collect::>()) + } else { + sources.clone() + }; + Arc::make_mut(&mut callable.data).seal = Some(Arc::new(Seal { owner: sites[0].0, getter: sites[0].2.clone(), adapter: erase_adapter(function), predicates, sources: protected_sources })); + attached.push((function.id, callable)); + } + Ok(()) + }).map_err(|message| vec![LowerError { span: function.span, message }])?; + } + for (id, callable) in attached { + let function = &mut program.functions[id.index() as usize]; + if let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: stored, .. + }, + .. + }, + .. + })) = &mut function.body.statements[0].kind + { + sealing_edits.push(sealing::Edit { + owner: id, + expression: 0, + slot: sealing::Slot::Adapter, + previous: stored.clone(), + current: callable.clone(), + }); + *stored = Some(callable); + } + } + let receipt = sealing::produce(&sum_before, program, types, originals, sealing_edits).map_err( + |message| { + vec![LowerError { + span: sum_before + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span), + message, + }] + }, + )?; + let actor = super::actor_handlers::renew_callable_snapshots( + program, + &sum_before, + types, + &receipt, + original, + &mut construction, + ) + .map_err(|message| { + vec![LowerError { + span: sum_before + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |f| f.span), + message, + }] + })?; + let sum_edits = receipt.token_pairs(); + super::mutable_sum_backings::callable_sealing(program, &sum_before, &sum_edits)?; + let baked = baked::callable_sealing(program, &sum_before, types, &receipt)?; + let native_suites = + super::native_suite_transition::callable_sealing(program, &sum_before, &sum_edits)?; + super::temporary_local_views::callable_sealing(program, &sum_before, &sum_edits)?; + if baked.is_some() { + program.resource_source.reflected_values = baked; + } + if native_suites.is_some() { + program.resource_source.native_suites = native_suites; + } + Ok((receipt, actor, construction)) +} + +fn erase_adapter(function: &Function) -> Function { + let mut function = function.clone(); + let _ = super::resource_materialization::walk_block(&mut function.body, &mut |expression| { + if let ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { callable, .. }, + .. + } = &mut expression.kind + { + *callable = None; + } + Ok(()) + }); + function +} + +fn erase_function(function: &Function) -> Function { + let mut function = erase_adapter(function); + // Independent sum receipts validate their own complete owner/body snapshot. + // Exclude only that inventory to avoid cyclic seals; preserve every actual + // operation, binding header and checked callable identity in its own gate. + function.mutable_sum_backings.clear(); + let _ = super::resource_materialization::walk_block(&mut function.body, &mut |expression| { + *expression = erase_callable(expression); + Ok(()) + }); + function +} + +pub(crate) fn validate_program(program: &Program) -> Result<(), Vec> { + let mut errors = Vec::new(); + let mut authorized = HashMap::new(); + for owner in &program.functions { + let mut body = owner.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |getter| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &getter.kind + { + for (ordinal, plan) in plans.iter().enumerate() { + if let ReflectedFieldAction::CallableReturn(callable) = &plan.action { + if let Err(message) = + callable.validate_current(program, owner, getter, Some(ordinal)) + { + errors.push(ValidationError { + span: getter.span, + message: message.into(), + }); + } else { + authorized.insert(callable.function(), callable.clone()); + } + } + } + } + if let ExpressionKind::FunctionAdapter { function, .. } = &getter.kind { + if let Some(adapter) = program.functions.get(function.index() as usize) { + if let Some(Statement { + kind: + StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(callable), + .. + }, + .. + }, + .. + })), + .. + }) = adapter.body.statements.first() + { + if callable.is_materialized() { + if let Err(message) = + callable.validate_current(program, owner, getter, None) + { + errors.push(ValidationError { + span: getter.span, + message: message.into(), + }); + } else { + authorized.insert(*function, callable.clone()); + } + } + } + } + } + Ok(()) + }); + } + for function in &program.functions { + let mut body = function.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { plan, callable }, + .. + } = &expression.kind + { + if !callable.as_ref().is_some_and(|callable| { + authorized.get(&function.id) == Some(callable) + && callable.function() == function.id + && callable.return_plan() == plan.as_ref() + }) { + errors.push(ValidationError { + span: expression.span, + message: "reflected callable return has no authenticated original getter" + .into(), + }); + } + } + if let ExpressionKind::IndirectCall { + ownership: CallOwnership::Generated(generated), + .. + } = &expression.kind + { + if let GeneratedOperation::ReflectedCallableAdapter { plan, .. } = + &generated.operation + { + if !authorized + .get(&function.id) + .is_some_and(|callable| callable.checked_plan() == plan) + { + errors.push(ValidationError { + span: expression.span, + message: + "reflected callable source invocation has no authenticated adapter" + .into(), + }); + } + } + } + Ok(()) + }); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Stage an already admitted, owned reflected callable for ordinary conversion +/// completion. This constructs no callback result and runs no predicate. +pub fn lower_reflected_callable_value( + value: Expression, + admitted: &jett_comptime::value::ReflectedCallableValue, + span: Span, +) -> Result { + let checked = admitted.checked_program().clone(); + let admission = admitted.checked_plan().clone(); + if value.ty != admission.source_type() || !admission.validate(checked.checked()) { + return Err("materialized reflected callable has no exact checked admission".into()); + } + let source = admission.source_type(); + let target = admission.requested_type(); + let placeholder = ReflectedFieldPlan { + source_type: admission.source_return(), + requested_type: admission.requested_return(), + action: ReflectedFieldAction::Exact, + }; + let mut callable = ReflectedCallablePlan::new(checked, admission, placeholder, span, None); + Arc::make_mut(&mut callable.data).materialized = true; + Arc::make_mut(&mut callable.data).reference = Some(Arc::new(admitted.clone())); + Ok(Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(value), + adapters: vec![InterfaceFunctionAdapter { + source, + target, + function: FunctionId(u32::MAX), + reflected: Some(callable), + }], + }, + ty: target, + span, + }) +} + +fn sources_match( + expected: &Arc>, + program: &Program, + checked: &Arc, +) -> Result<(), &'static str> { + let restore = |functions: &[Function]| { + functions + .iter() + .map(|function| original_function(function, &program.resource_source, checked)) + .collect::, _>>() + }; + let current = + restore(&program.functions).map_err(|_| "reflected current Source restoration failed")?; + let original = restore(expected).map_err(|_| "reflected expected Source restoration failed")?; + // Existing ordinary matching retains full actor metadata. A changed actor + // Snapshot therefore requires the exact epoch view below rather than erasure. + if original.iter().all(|expected| { + current + .get(expected.id.index() as usize) + .is_some_and(|current| { + source_functions_equal( + std::slice::from_ref(current), + std::slice::from_ref(expected), + ) + }) + }) { + return Ok(()); + } + super::actor_handlers::callable_source_matches(expected, program, ¤t, &original, checked) +} + +/// Undo only protected checked replacements. Public edited HIR cannot create a +/// row or renew the seal, and each row must still occupy its exact owner/site. +fn original_function( + function: &Function, + archive: &ResourceSourceArchive, + checked: &Arc, +) -> Result { + if let Some(original) = baked::original_function(function, archive, checked)? { + return Ok(original); + } + if let Some(original) = super::actor_graph::original_function(archive, function, checked)? { + return Ok(original); + } + let mut original = function.clone(); + for row in archive + .required_materializations() + .iter() + .filter(|row| row.function() == function.id) + { + if !archive.belongs_to(checked) + || !row.proof.belongs_to(checked) + || row.identity() != &function.identity + { + return Err( + "reflected callable required replacement has no original checked owner".into(), + ); + } + let mut matches = 0usize; + super::resource_materialization::walk_block(&mut original.body, &mut |expression| { + if super::resource_materialization::expression_equal(expression, row.current()) { + matches += 1; + *expression = row.original().clone(); + } else if super::resource_materialization::expression_equal(expression, row.original()) + { + matches += 1; + } + Ok(()) + })?; + if matches != 1 { + return Err("reflected callable required replacement moved or was duplicated".into()); + } + } + Ok(original) +} + +fn returned_plan( + checked: &Arc, + functions: &[Function], + node: ReflectedFieldRequirementPlan, +) -> Result { + use ReflectedFieldAction as A; + use ReflectedFieldRequirement as R; + let predicates = |ids: Vec| -> Result, String> { + ids.into_iter() + .map(|refined_type| { + let types = &checked.checked().interner; + let Type::Refinement { name, .. } = types.resolve(refined_type) else { + return Err("materialized return predicate is not a refinement".into()); + }; + let (base_type, _) = super::reflected_fields::refinement_chain(types, refined_type) + .ok_or("invalid materialized return chain")?; + let input_type = match types.resolve(base_type) { + Type::Secret(inner) => *inner, + _ => base_type, + }; + let (definition, declaration_span, constraint_span, origin) = + checked_refinement_declaration(checked, name)?; + let mut matches = functions.iter().filter(|function| { + function.source_definition == Some(definition) + && function.span == declaration_span + && function.body.span == declaration_span + && &function.identity.declaration.origin == origin + && refinement_predicate_declaration_matches( + &function.identity.declaration, + name, + ) + && function.params.len() == 1 + && function.params[0].span == constraint_span + && function.params[0].ty == input_type + && function.params[0].mode == ParamMode::Owned + && !function.params[0].mutable + && function.capture_count == 0 + && function.return_type == TypeInterner::BOOL + }); + let function = matches + .next() + .ok_or("materialized return has no checked predicate declaration")?; + if matches.next().is_some() { + return Err("materialized return predicate context is ambiguous".into()); + } + Ok(RefinementPredicate { + refined_type, + type_name: name.clone(), + function: function.id, + base_type, + input_type, + }) + }) + .collect() + }; + let action = match node.requirement { + R::Exact => A::Exact, + R::Predicates(ids) => A::Predicates(predicates(ids)?), + R::Unsupported(reason) => A::Unsupported(reason), + R::Refine { + base, + predicates: ids, + } => A::Refine { + base: Box::new(returned_plan(checked, functions, *base)?), + predicates: predicates(ids)?, + }, + R::List(child) => A::List(Box::new(returned_plan(checked, functions, *child)?)), + R::Set(child) => A::Set(Box::new(returned_plan(checked, functions, *child)?)), + R::Optional(child) => A::Optional(Box::new(returned_plan(checked, functions, *child)?)), + R::Map { key, value } => A::Map { + key: Box::new(returned_plan(checked, functions, *key)?), + value: Box::new(returned_plan(checked, functions, *value)?), + }, + R::Result { ok, error } => A::Result { + ok: Box::new(returned_plan(checked, functions, *ok)?), + error: Box::new(returned_plan(checked, functions, *error)?), + }, + }; + Ok(ReflectedFieldPlan { + source_type: node.source_type, + requested_type: node.requested_type, + action, + }) +} + +// Refinement aliases are named checked types, not entries in the value/function +// definition environment. Bind the predicate to its exact immutable Source alias. +fn checked_refinement_declaration<'a>( + checked: &'a jett_typecheck::CheckedResourceProgram, + type_name: &str, +) -> Result<(DefId, Span, Span, &'a SourceOrigin), String> { + let (namespace, name) = type_name.rsplit_once('.').unwrap_or(("", type_name)); + let mut definitions = checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Type + && definition.name == type_name + && definition.namespace.as_deref().unwrap_or_default() == namespace + }); + let definition = definitions + .next() + .ok_or("materialized return has no checked refinement declaration")?; + if definitions.next().is_some() { + return Err("materialized return checked refinement declaration is ambiguous".into()); + } + let mut aliases = checked.module().items.iter().filter_map(|item| { + let jett_parser::ast::Item::TypeAlias(alias) = item else { + return None; + }; + (alias.name.span == definition.span + && alias.name.name == name + && alias.constraint.is_some()) + .then_some(alias) + }); + let alias = aliases + .next() + .ok_or("materialized return has no original constrained type declaration")?; + if aliases.next().is_some() { + return Err("materialized return original constrained declaration is ambiguous".into()); + } + let constraint = alias + .constraint + .as_ref() + .ok_or("materialized return lost original constraint")?; + let origin = checked + .source_origins() + .get(&alias.span.file) + .ok_or("materialized return refinement has no checked Source origin")?; + Ok((definition.id, alias.span, constraint.span(), origin)) +} + +pub fn reflected_callable_plans(function: &Function) -> Vec { + let mut plans = Vec::new(); + let mut body = function.body.clone(); + let _ = super::resource_materialization::walk_block(&mut body, &mut |expression| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(fields)), + .. + } = &expression.kind + { + for field in fields { + if let ReflectedFieldAction::CallableReturn(plan) = &field.action { + plans.push(plan.clone()); + } + } + } + if let ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(plan), + .. + }, + .. + } = &expression.kind + { + plans.push(plan.clone()); + } + Ok(()) + }); + plans +} + +#[cfg(test)] +#[path = "reflected_callable_tests.rs"] +mod tests; + +// Metadata erased here is independently authenticated by validate_program. +// Graph body, instruction identity, local headers and Source structure remain exact. +pub(crate) fn source_functions_equal(left: &[Function], right: &[Function]) -> bool { + let left = left.iter().map(erase_function).collect::>(); + let right = right.iter().map(erase_function).collect::>(); + super::resource_materialization::functions_equal(&left, &right) +} diff --git a/crates/jett_hir/src/reflected_callable_baked.rs b/crates/jett_hir/src/reflected_callable_baked.rs new file mode 100644 index 00000000..11927917 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_baked.rs @@ -0,0 +1,308 @@ +//! Private checked transition for ordinary required values in an adapter owner. +use super::*; +use jett_comptime::{ExplicitComptimeValues, Value}; + +#[path = "required_bake_receipt.rs"] +mod required_bake_receipt; +pub(crate) use required_bake_receipt::CheckedRequiredBake; + +#[derive(Debug)] +pub(crate) struct Transition { + checked: Arc, + original: Vec, + cache: ExplicitComptimeValues, + property: Option, + current: Vec, +} + +impl Transition { + pub(crate) fn current_functions(&self) -> &[Function] { + &self.current + } +} + +fn equal(left: &[Function], right: &[Function]) -> bool { + let left = left.iter().map(erase_function).collect::>(); + let right = right.iter().map(erase_function).collect::>(); + super::super::resource_materialization::functions_equal(&left, &right) +} + +pub fn has_reflected_callable_plans(program: &Program) -> bool { + program + .functions + .iter() + .any(|function| !reflected_callable_plans(function).is_empty()) +} + +/// The callback constructs typed HIR only. Every result, added local and binding +/// is checked against the private required cache before becoming a transition. +pub fn materialize_checked_reflected_values( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, + mut materialize: impl FnMut( + &Value, + TypeId, + Span, + &mut Vec, + &mut Vec, + ) -> Result, +) -> Result<(), Vec> { + let span = program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span); + let result = (|| -> Result { + let bake = required_bake_receipt::produce(program, values, types, &mut materialize)?; + let mut candidate = bake.candidate(); + crate::actor_handlers::required_bake(&mut candidate, program, values, types, &bake)?; + let checked = bake.checked().clone(); + let original = bake.original_functions().to_vec(); + // Every ordinary replacement and added local has passed the existing + // private value matcher above. Renew Graph custody at this fixed boundary + // before installing the corresponding reflected transition. + super::super::actor_graph::complete_conversion(&mut candidate, program) + .map_err(|errors| format!("invalid combined Graph transition: {errors:?}"))?; + candidate.resource_source.reflected_values = Some(Arc::new(Transition { + checked, + original, + cache: values.clone(), + property: None, + current: candidate.functions.clone(), + })); + if crate::temporary_local_views::has_markers(program) + || program + .resource_source + .required_materializations() + .iter() + .any(|row| row.proof.actor_graph().is_some()) + { + candidate + .resource_source + .install_materialized(crate::resource_source::Materialized { + functions: candidate.functions.clone(), + values: program.resource_source.required_materializations().to_vec(), + required_only: program + .resource_source + .required_only_function_ids() + .to_vec(), + }); + } + Ok(candidate) + })(); + match result { + Ok(candidate) => { + *program = candidate; + Ok(()) + } + Err(message) => Err(vec![LowerError { span, message }]), + } +} + +pub(super) fn original_function( + function: &Function, + archive: &ResourceSourceArchive, + checked: &Arc, +) -> Result, String> { + let Some(transition) = &archive.reflected_values else { + return Ok(None); + }; + if !archive.belongs_to(checked) || !Arc::ptr_eq(&transition.checked, checked) { + return Err("ordinary reflected materialization belongs to another checked Source".into()); + } + let Some(current) = transition.current.get(function.id.index() as usize) else { + return Ok(None); + }; + if !equal( + std::slice::from_ref(function), + std::slice::from_ref(current), + ) { + return Ok(None); + } + Ok(transition + .original + .get(function.id.index() as usize) + .cloned()) +} + +pub(crate) fn validate_conversion_input(program: &Program) -> Result<(), Vec> { + let Some(transition) = &program.resource_source.reflected_values else { + return Ok(()); + }; + if !program.resource_source.belongs_to(&transition.checked) + || !equal(&program.functions, &transition.current) + { + return Err(vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span), + message: "reflected conversion changed its protected required-value input".into(), + }]); + } + Ok(()) +} + +/// Called only around the ordinary canonical conversion pass after its gate. +pub(crate) fn complete_conversion( + program: &mut Program, + before: &Program, +) -> Result<(), Vec> { + validate_conversion_input(before)?; + let Some(transition) = &before.resource_source.reflected_values else { + return Ok(()); + }; + if program.resource_source != before.resource_source { + return Err(vec![LowerError { + span: before.functions[0].span, + message: "reflected conversion replaced original archive".into(), + }]); + } + program.resource_source.reflected_values = Some(Arc::new(Transition { + checked: transition.checked.clone(), + original: transition.original.clone(), + cache: transition.cache.clone(), + property: transition.property.clone(), + current: program.functions.clone(), + })); + Ok(()) +} + +/// Renew this exact checked cache view from the retained actual sealer output. +/// Full function equality includes actor metadata; no erased view can grant it. +pub(super) fn callable_sealing( + program: &Program, + before: &Program, + types: &TypeInterner, + receipt: &super::CheckedCallableSealing<'_>, +) -> Result>, Vec> { + let result = (|| -> Result>, String> { + let Some(transition) = &before.resource_source.reflected_values else { + return Ok(None); + }; + let mut replay = receipt.replay(before, types)?; + if program.resource_source != before.resource_source + || program.resource_manifest != before.resource_manifest + || program.equality_methods != before.equality_methods + || !before.resource_source.belongs_to(&transition.checked) + || !receipt + .checked() + .is_some_and(|checked| Arc::ptr_eq(checked, &transition.checked)) + || !crate::resource_materialization::functions_equal( + &before.functions, + &transition.current, + ) + { + return Err( + "baked callable sealing changed its exact checked cache or full input table".into(), + ); + } + // This peer's real renewal changes private snapshots. Replay that same + // producer on retained actual sealer output, preserving every actor field. + crate::mutable_sum_backings::callable_sealing(&mut replay, before, &receipt.token_pairs()) + .map_err(|errors| format!("baked callable sealing sum replay: {errors:?}"))?; + for checked_program in [before, program] { + crate::actor_handlers::validate_program(checked_program) + .map_err(|errors| format!("baked callable sealing actor snapshot: {errors:?}"))?; + } + if !crate::resource_materialization::functions_equal(&replay.functions, &program.functions) + { + return Err("baked callable sealing differs from its retained complete output".into()); + } + Ok(Some(Arc::new(Transition { + checked: transition.checked.clone(), + original: transition.original.clone(), + cache: transition.cache.clone(), + property: transition.property.clone(), + current: replay.functions, + }))) + })(); + result.map_err(|message| { + vec![LowerError { + span: before + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span), + message, + }] + }) +} + +pub(super) fn owns_materialized_owner(archive: &ResourceSourceArchive, owner: &Function) -> bool { + archive.reflected_values.as_ref().is_some_and(|transition| { + archive.belongs_to(&transition.checked) + && transition + .current + .get(owner.id.index() as usize) + .is_some_and(|current| { + equal(std::slice::from_ref(current), std::slice::from_ref(owner)) + }) + }) +} + +/// Readonly association with the private successful required-cache snapshot. +pub(crate) fn same_required_cache(program: &Program, values: &ExplicitComptimeValues) -> bool { + program + .resource_source + .reflected_values + .as_ref() + .is_some_and(|transition| { + program.resource_source.belongs_to(&transition.checked) + && transition.cache.same_checked_cache(values) + }) +} +pub(crate) fn has_property_suite(program: &Program) -> bool { + program + .resource_source + .reflected_values + .as_ref() + .is_some_and(|transition| transition.property.is_some()) +} +pub(crate) fn owns_property_suite( + program: &Program, + plan: &crate::CheckedNativePropertyPlan, +) -> bool { + program + .resource_source + .reflected_values + .as_ref() + .and_then(|transition| transition.property.as_ref()) + .is_some_and(|current| current.same_plan(plan)) +} +/// Only the private canonical Property constructor calls this after reconstructing +/// its complete wrapper. Public mutable HIR has no append/reseal entry point. +pub(crate) fn install_property_suite( + program: &mut Program, + plan: crate::CheckedNativePropertyPlan, +) -> Result<(), Vec> { + validate_conversion_input(program)?; + let transition = program + .resource_source + .reflected_values + .as_ref() + .ok_or_else(|| { + vec![LowerError { + span: program.functions[0].span, + message: "native Property transition lost its protected required-value graph" + .into(), + }] + })?; + if transition.property.is_some() { + return Err(vec![LowerError { + span: program.functions[0].span, + message: "native Property transition already owns a generated suite".into(), + }]); + } + program.resource_source.reflected_values = Some(Arc::new(Transition { + checked: transition.checked.clone(), + original: transition.original.clone(), + cache: transition.cache.clone(), + property: Some(plan), + current: transition.current.clone(), + })); + Ok(()) +} + +#[cfg(test)] +#[path = "actor_required_bake_tests.rs"] +mod actor_required_bake_tests; diff --git a/crates/jett_hir/src/reflected_callable_materialized.rs b/crates/jett_hir/src/reflected_callable_materialized.rs new file mode 100644 index 00000000..794bf7f7 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_materialized.rs @@ -0,0 +1,595 @@ +//! Match materialized descriptors and capture values without invoking Source. +use super::*; +use jett_comptime::Value; +use std::cell::RefCell; + +pub(super) fn validate( + reference: &jett_comptime::value::ReflectedCallableValue, + site: &Expression, + owner: &Function, + functions: &[Function], + archive: &ResourceSourceArchive, +) -> Result<(), String> { + let ExpressionKind::FunctionAdapter { value, .. } = &site.kind else { + return Err("materialized reflected callable has no source descriptor".into()); + }; + if !archive.belongs_to(reference.checked_program()) || value.ty != reference.source_type() { + return Err( + "materialized reflected callable source belongs to another checked context".into(), + ); + } + let context = Context { + owner, + functions, + archive, + checked: reference.checked_program(), + used: RefCell::new(HashSet::new()), + }; + if !context.matches( + reference.source(), + value, + context.checked.checked().interner.len(), + ) { + return Err("materialized source callback, captures or pending layers differ from the admitted read".into()); + } + let mut descriptor = value.as_ref(); + while let ExpressionKind::Run(inner) = &descriptor.kind { + descriptor = inner; + } + let id = match &descriptor.kind { + ExpressionKind::FunctionRef(id) | ExpressionKind::ClosureRef { function: id, .. } => { + Some(*id) + } + ExpressionKind::FunctionAdapter { .. } => None, + _ => return Err("materialized callback lost its original descriptor".into()), + }; + if let Some(id) = id { + let original = archive + .functions() + .get(id.index() as usize) + .ok_or("materialized callback target is absent")?; + if (reference.source_definition().is_some() + && original.source_definition != reference.source_definition()) + || reference + .source_body() + .is_some_and(|body| original.body.span != body.span) + { + return Err("materialized callback selects another original source declaration".into()); + } + } + Ok(()) +} + +struct Context<'a> { + owner: &'a Function, + functions: &'a [Function], + archive: &'a ResourceSourceArchive, + checked: &'a Arc, + used: RefCell>, +} + +pub(super) fn validate_required_values( + checked: &Arc, + archive: &ResourceSourceArchive, + owner: &Function, + functions: &[Function], + replacements: &[(jett_comptime::CheckedRequiredValue, Expression)], +) -> Result, String> { + let context = Context { + owner, + functions, + archive, + checked, + used: RefCell::new(HashSet::new()), + }; + for (proof, current) in replacements { + if !proof.belongs_to(checked) + || !context.matches(proof.value(), current, checked.checked().interner.len()) + { + return Err( + "ordinary required replacement differs from its opaque checked value".into(), + ); + } + } + Ok(context.used.into_inner()) +} + +impl Context<'_> { + fn target(&self, id: FunctionId, ty: TypeId) -> Option<&Function> { + let current = self.functions.get(id.index() as usize)?; + let original = self.archive.functions().get(id.index() as usize)?; + let current = original_function(current, self.archive, self.checked).ok()?; + if !super::resource_materialization::functions_equal( + std::slice::from_ref(¤t), + std::slice::from_ref(original), + ) { + return None; + } + let signature = Type::Function { + params: original.params[original.capture_count..] + .iter() + .map(|param| param.ty) + .collect(), + view_params: original.params[original.capture_count..] + .iter() + .map(|param| param.mode == ParamMode::View) + .collect(), + return_type: original.return_type, + }; + (self.checked.checked().interner.resolve(ty) == &signature).then_some(original) + } + + fn binding(&self, local: LocalId) -> Option { + let metadata = self.owner.locals.get(local.index() as usize)?; + if metadata.id != local || metadata.mutable || metadata.view_source.is_some() { + return None; + } + let mut values = Vec::new(); + bindings(&self.owner.body, local, &mut values); + let [value] = values.as_slice() else { + return None; + }; + self.used.borrow_mut().insert(local); + (value.ty == metadata.ty).then(|| value.clone()) + } + + fn matches(&self, value: &Value, expression: &Expression, budget: usize) -> bool { + if budget == 0 || expression.ty.index() as usize >= self.checked.checked().interner.len() { + return false; + } + let child = + |value: &Value, expression: &Expression| self.matches(value, expression, budget - 1); + let types = &self.checked.checked().interner; + use ExpressionKind as E; + if let E::Clone(inner) = &expression.kind { + return child(value, inner); + } + if let E::Local(local) = &expression.kind { + return self + .binding(*local) + .is_some_and(|binding| child(value, &binding)); + } + if let Value::Typed { type_name, value } = value { + if let E::RefinementValidated(inner) = &expression.kind { + return types.type_name(expression.ty) == *type_name && child(value, inner); + } + return child(value, expression); + } + if let Value::Pending(value) = value { + return matches!(&expression.kind, E::Run(inner) if inner.ty == expression.ty && child(value, inner)); + } + if let E::Run(_) = &expression.kind { + return false; + } + if let E::RefinementValidated(inner) = &expression.kind { + return matches!(types.resolve(expression.ty), Type::Refinement { base, .. } if *base == inner.ty) + && child(value, inner); + } + if let E::InterfaceCoerce { + value: inner, + adapters, + } = &expression.kind + { + if let Value::ReflectedCallable(admitted) = value { + return adapters.len() == 1 + && adapters[0].reflected.as_ref().is_some_and(|plan| { + plan.is_materialized() + && plan.checked_plan() == admitted.checked_plan() + && Arc::ptr_eq(&plan.data.checked, admitted.checked_program()) + && plan.data.reference.is_some() + && expression.ty == admitted.requested_type() + && inner.ty == admitted.source_type() + && child(admitted.source(), inner) + }); + } + return adapters.is_empty() + && child(value, inner) + && self + .checked + .checked() + .ordinary_types_compatible(expression.ty, inner.ty); + } + // Builders are reconstructed by the compiler's sole evaluated-builder + // producer. Compare its complete typed transcript, including schema, + // metadata, field order, pending layers and failure operations. + if matches!(value, Value::TypeConstruction { .. }) { + let functions = crate::evaluated_values::function_value_candidates( + self.functions, + &self.checked.checked().method_value_definitions, + ); + let mut locals = Vec::new(); + let mut bindings = Vec::new(); + let mut context = crate::evaluated_values::ValueContext { + actor_seeds: None, + types, + reflection: &self.checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals: &mut locals, + bindings: &mut bindings, + }; + return crate::evaluated_builders::builder_expression( + value, + expression.span, + &mut context, + ) + .is_ok_and(|expected| { + locals.is_empty() + && bindings.is_empty() + && crate::resource_materialization::expression_equal(&expected, expression) + }); + } + match (value, &expression.kind) { + (Value::Int64(a), E::Int(b)) => i128::from(*a) == *b, + (Value::Uint64(a), E::Int(b)) => i128::from(*a) == *b, + (Value::Float64(a), E::Float(b)) => a.to_bits() == b.to_bits(), + (Value::String(a), E::String(b)) => a == b, + (Value::Bool(a), E::Bool(b)) => a == b, + (Value::Nothing, E::Nothing) | (Value::OptionalNone, E::OptionalNone) => true, + (Value::OptionalSome(a), E::OptionalSome(b)) + | (Value::ResultOk(a), E::ResultOk(b)) + | (Value::ResultFail(a), E::ResultFail(b)) => child(a, b), + (Value::List(a), E::ListConstruct { elements: b }) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| child(a, b)) + } + (Value::Map(a), E::MapConstruct { entries: b }) => { + a.len() == b.len() + && a.iter() + .zip(b) + .all(|((ak, av), b)| child(ak, &b.key) && child(av, &b.value)) + } + ( + Value::Struct { + type_name, + concrete_type, + fields, + }, + E::StructConstruct { + struct_type, + fields: current, + evaluation_order, + validates_refinements, + refinement_predicates, + }, + ) => { + let Type::Struct(id) = types.resolve(*struct_type) else { + return false; + }; + let definition = types.resolve_struct(*id); + *struct_type == expression.ty + && !validates_refinements + && refinement_predicates.is_empty() + && concrete_type.as_ref().unwrap_or(type_name) == &types.type_name(*struct_type) + && *evaluation_order == (0..current.len()).collect::>() + && fields.len() == current.len() + && definition.fields.len() == current.len() + && definition + .fields + .iter() + .zip(current) + .all(|((name, ty), current)| { + current.ty == *ty + && fields + .iter() + .find(|(field, _)| field == name) + .is_some_and(|(_, value)| child(value, current)) + }) + } + ( + Value::Enum { + type_name, + variant, + fields, + }, + E::EnumConstruct { + enum_type, + variant: current, + payloads, + evaluation_order, + }, + ) => { + let Type::Enum(id) = types.resolve(*enum_type) else { + return false; + }; + let Some(definition) = types + .resolve_enum(*id) + .variants + .get(current.index() as usize) + else { + return false; + }; + *enum_type == expression.ty + && *type_name == types.type_name(*enum_type) + && definition.name == *variant + && fields.len() == payloads.len() + && definition.fields.len() == payloads.len() + && *evaluation_order == (0..payloads.len()).collect::>() + && fields + .iter() + .zip(payloads) + .zip(&definition.fields) + .all(|((a, b), (_, ty))| b.ty == *ty && child(a, b)) + } + (Value::NamedFunction(name), E::FunctionRef(id)) => { + let Some(original) = self.target(*id, expression.ty) else { + return false; + }; + let Some(definition) = original.source_definition else { + return false; + }; + let info = self.checked.resolved().scope_table.def(definition); + original.capture_count == 0 + && info.kind == jett_resolve::DefKind::Function + && info.name == *name + } + (Value::Function { body, captures, .. }, E::FunctionRef(id)) + | (Value::Function { body, captures, .. }, E::ClosureRef { function: id, .. }) => { + let Some(original) = self.target(*id, expression.ty) else { + return false; + }; + let locals: &[LocalId] = match &expression.kind { + E::ClosureRef { captures, .. } => captures, + _ => &[], + }; + original.source_definition.is_none() + && original.body.span == body.span + && original.capture_count == locals.len() + && original.params[..original.capture_count] + .iter() + .zip(locals) + .all(|(param, local)| { + self.binding(*local).is_some_and(|current| { + current.ty == param.ty + && captures + .get(¶m.name) + .is_some_and(|value| child(value, ¤t)) + }) + }) + } + (Value::ReflectedCallable(admitted), E::FunctionAdapter { function, .. }) => { + let Some(adapter) = self.functions.get(function.index() as usize) else { + return false; + }; + let Some(Statement { + kind: + StatementKind::Return(Some(Expression { + kind: + E::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(plan), + .. + }, + .. + }, + .. + })), + .. + }) = adapter.body.statements.first() + else { + return false; + }; + plan.is_materialized() + && plan.checked_plan() == admitted.checked_plan() + && Arc::ptr_eq(&plan.data.checked, admitted.checked_program()) + && expression.ty == admitted.requested_type() + && validate( + admitted, + expression, + self.owner, + self.functions, + self.archive, + ) + .is_ok() + } + _ => false, + } + } +} + +fn bindings(block: &Block, local: LocalId, values: &mut Vec) { + for statement in &block.statements { + match &statement.kind { + StatementKind::Let { + local: current, + value, + } if *current == local => values.push(value.clone()), + StatementKind::Assign { + target: + Expression { + kind: ExpressionKind::Local(current), + .. + }, + value, + } if *current == local => values.push(value.clone()), + StatementKind::If { + then_block, + else_block, + .. + } => { + bindings(then_block, local, values); + if let Some(block) = else_block { + bindings(block, local, values); + } + } + StatementKind::For { body, .. } + | StatementKind::While { body, .. } + | StatementKind::Scope(body) => bindings(body, local, values), + StatementKind::Match { arms, .. } => { + for arm in arms { + bindings(&arm.body, local, values); + } + } + StatementKind::ReflectedTypeDispatch { arms, .. } => { + for arm in arms { + bindings(&arm.body, local, values); + } + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn materialized_builder_requires_its_complete_canonical_transcript() { + let file = FileId::new(0); + let checked = Arc::new(jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse("namespace app\nstruct First:\n raw: int64\nstruct Second:\n raw: int64\nfunction context() returns result[TypeConstruction, string]:\n return fail(\"unused\")\n", file), + HashMap::from([(file, SourceOrigin::Project)]), &[], + jett_typecheck::CheckOptions { release: false }).unwrap()); + let program = lower_checked_resource_program(&checked).unwrap(); + let owner = &program.functions[0]; + let types = &checked.checked().interner; + let value = Value::TypeConstruction { + type_name: "app.First".into(), + variant: None, + state: None, + fields: vec![(0, "raw".into(), "int64".into(), Value::Int64(7))], + }; + let functions = crate::evaluated_values::function_value_candidates( + &program.functions, + &checked.checked().method_value_definitions, + ); + let mut locals = Vec::new(); + let mut bindings = Vec::new(); + let expression = crate::evaluated_builders::builder_expression( + &value, + owner.span, + &mut crate::evaluated_values::ValueContext { + actor_seeds: None, + types, + reflection: &checked.checked().reflection_metadata, + functions: &functions, + refinement_functions: None, + locals: &mut locals, + bindings: &mut bindings, + }, + ) + .unwrap(); + assert!(locals.is_empty() && bindings.is_empty()); + let context = Context { + owner, + functions: &program.functions, + archive: &program.resource_source, + checked: &checked, + used: RefCell::new(HashSet::new()), + }; + assert!(context.matches(&value, &expression, types.len())); + for forgery in 0..4 { + let mut changed = expression.clone(); + let ExpressionKind::Handle { + target, failure, .. + } = &mut changed.kind + else { + panic!("builder result") + }; + let ExpressionKind::Intrinsic { + args, + type_arguments, + .. + } = &mut target.kind + else { + panic!("builder put") + }; + match forgery { + 0 => args[2].kind = ExpressionKind::Int(8), + 1 => { + type_arguments[0] = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.Second") + .unwrap() + } + 2 => args.swap(1, 2), + 3 => failure.statements.clear(), + _ => unreachable!(), + } + assert!( + !context.matches(&value, &changed, types.len()), + "accepted builder forgery {forgery}" + ); + } + } + + #[test] + fn materialized_values_preserve_nominal_identity_and_every_pending_layer() { + let file = FileId::new(0); + let parsed = jett_parser::parse( + "namespace app\nstruct First:\n raw: int64\nstruct Second:\n raw: int64\nfunction context() returns nothing:\n return nothing\n", + file, + ); + assert!(parsed.errors.is_empty()); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let program = lower_checked_resource_program(&checked).unwrap(); + let owner = &program.functions[0]; + let context = Context { + owner, + functions: &program.functions, + archive: &program.resource_source, + checked: &checked, + used: RefCell::new(HashSet::new()), + }; + let types = &checked.checked().interner; + let first = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.First") + .unwrap(); + let second = types + .type_ids() + .find(|ty| types.type_name(*ty) == "app.Second") + .unwrap(); + let span = owner.span; + let primitive = Expression { + kind: ExpressionKind::Int(5), + ty: TypeInterner::INT64, + span, + }; + let construct = |ty| Expression { + kind: ExpressionKind::StructConstruct { + struct_type: ty, + fields: vec![primitive.clone()], + evaluation_order: vec![0], + validates_refinements: false, + refinement_predicates: Vec::new(), + }, + ty, + span, + }; + let retained = Value::Struct { + type_name: "app.First".into(), + concrete_type: None, + fields: vec![("raw".into(), Value::Int64(5))], + }; + assert!(context.matches(&retained, &construct(first), types.len())); + assert!( + !context.matches(&retained, &construct(second), types.len()), + "same-layout nominal substitution" + ); + let run = |value| Expression { + kind: ExpressionKind::Run(Box::new(value)), + ty: TypeInterner::INT64, + span, + }; + let pending = Value::Pending(Box::new(Value::Pending(Box::new(Value::Int64(5))))); + assert!(context.matches(&pending, &run(run(primitive.clone())), types.len())); + assert!( + !context.matches(&pending, &run(primitive.clone()), types.len()), + "pending layer removed" + ); + assert!( + !context.matches(&pending, &run(run(run(primitive))), types.len()), + "pending layer added" + ); + } +} diff --git a/crates/jett_hir/src/reflected_callable_sealing_receipt.rs b/crates/jett_hir/src/reflected_callable_sealing_receipt.rs new file mode 100644 index 00000000..bb07c6c7 --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_sealing_receipt.rs @@ -0,0 +1,319 @@ +//! Receipt of the sole actual callable sealer, including optional adapter slots. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Slot { + Getter { field: usize }, + Adapter, +} + +#[derive(Clone)] +pub(super) struct Edit { + pub(super) owner: FunctionId, + pub(super) expression: usize, + pub(super) slot: Slot, + pub(super) previous: Option, + pub(super) current: ReflectedCallablePlan, +} + +struct Sealing<'types> { + types: &'types TypeInterner, + before: Program, + output: Program, + checked: Option>, + source_epoch: Arc>, + edits: Vec, +} + +/// No public constructor or stored archive back-reference. This receipt lives +/// only across the real producer's exact peer-renewal boundary. +pub(crate) struct CheckedCallableSealing<'types>(Box>); + +fn metadata_equal(left: &Program, right: &Program) -> bool { + let Program { + resource_manifest, + resource_source, + equality_methods, + functions: _, + } = left; + let Program { + resource_manifest: other_manifest, + resource_source: other_source, + equality_methods: other_equality, + functions: _, + } = right; + resource_manifest == other_manifest + && resource_source == other_source + && equality_methods == other_equality +} +fn same_program(left: &Program, right: &Program) -> bool { + metadata_equal(left, right) + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} + +fn replacement( + edits: &[Edit], + used: &mut [bool], + owner: FunctionId, + expression: usize, + slot: Slot, + previous: Option<&ReflectedCallablePlan>, +) -> Result, String> { + let selected = edits + .iter() + .enumerate() + .filter(|(_, edit)| { + edit.owner == owner && edit.expression == expression && edit.slot == slot + }) + .collect::>(); + let [(index, edit)] = selected.as_slice() else { + return if selected.is_empty() { + Ok(None) + } else { + Err("callable sealing repeated an exact original token slot".into()) + }; + }; + if used[*index] || edit.previous.as_ref() != previous { + return Err("callable sealing changed an exact previous token".into()); + } + used[*index] = true; + Ok(Some(edit.current.clone())) +} + +fn replay_edits(before: &Program, edits: &[Edit]) -> Result { + let mut replay = before.clone(); + let mut used = vec![false; edits.len()]; + for function in &mut replay.functions { + let owner = function.id; + let mut ordinal = 0usize; + crate::resource_materialization::walk_block(&mut function.body, &mut |value| { + let expression = ordinal; + ordinal += 1; + match &mut value.kind { + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(fields)), + .. + } => { + for (field, plan) in fields.iter_mut().enumerate() { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + if let Some(current) = replacement( + edits, + &mut used, + owner, + expression, + Slot::Getter { field }, + Some(callable), + )? { + *callable = current; + } + } + } + } + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { callable, .. }, + .. + } => { + if let Some(current) = replacement( + edits, + &mut used, + owner, + expression, + Slot::Adapter, + callable.as_ref(), + )? { + *callable = Some(current); + } + } + _ => {} + } + Ok(()) + })?; + } + if used.iter().any(|used| !used) { + return Err("callable sealing lost its exact original getter or adapter occurrence".into()); + } + Ok(replay) +} + +impl CheckedCallableSealing<'_> { + pub(crate) fn authenticate_output( + &self, + before: &Program, + output: &Program, + types: &TypeInterner, + ) -> Result<(), String> { + if !std::ptr::eq(types, self.0.types) + || !same_program(before, &self.0.before) + || !same_program(output, &self.0.output) + || !crate::resource_materialization::functions_equal( + &self.0.source_epoch, + &before.functions, + ) + || self.0.checked.as_ref().is_some_and(|checked| { + before + .resource_source + .checked_program() + .is_some_and(|original| !Arc::ptr_eq(original, checked)) + }) + { + return Err("callable sealing receipt has another complete input, output, archive or type arena".into()); + } + Ok(()) + } + pub(crate) fn replay(&self, before: &Program, types: &TypeInterner) -> Result { + self.authenticate_output(before, &self.0.output, types)?; + Ok(self.0.output.clone()) + } + pub(crate) fn before(&self) -> &Program { + &self.0.before + } + pub(crate) fn output(&self) -> &Program { + &self.0.output + } + pub(crate) fn source_epoch(&self) -> &Arc> { + &self.0.source_epoch + } + pub(crate) fn checked(&self) -> Option<&Arc> { + self.0.checked.as_ref() + } + pub(super) fn token_pairs(&self) -> Vec<(ReflectedCallablePlan, ReflectedCallablePlan)> { + self.0 + .edits + .iter() + .filter_map(|edit| { + edit.previous + .as_ref() + .map(|previous| (previous.clone(), edit.current.clone())) + }) + .collect() + } +} + +fn bound_plan_relation(previous: &ReflectedCallablePlan, current: &ReflectedCallablePlan) -> bool { + let Plan { + checked, + admission, + returned, + span, + function, + context, + seal, + materialized, + ordinal, + reference, + graph_source, + } = previous.data.as_ref(); + let Plan { + checked: other_checked, + admission: other_admission, + returned: other_returned, + span: other_span, + function: other_function, + context: other_context, + seal: other_seal, + materialized: other_materialized, + ordinal: other_ordinal, + reference: other_reference, + graph_source: other_graph_source, + } = current.data.as_ref(); + let same_reference = match (reference, other_reference) { + (Some(left), Some(right)) => Arc::ptr_eq(left, right), + (None, None) => true, + _ => false, + }; + let same_graph_source = match (graph_source, other_graph_source) { + (Some(left), Some(right)) => left.same_source(right), + (None, None) => true, + _ => false, + }; + seal.is_none() + && other_seal.is_some() + && Arc::ptr_eq(checked, other_checked) + && admission == other_admission + && returned == other_returned + && span == other_span + && function == other_function + && context == other_context + && materialized == other_materialized + && ordinal == other_ordinal + && same_reference + && same_graph_source +} + +/// Called only inside the real sealer after its actual token insertions and +/// before any archive peer changes. A consumer cannot nominate a replacement table. +pub(super) fn produce<'types>( + before: &Program, + output: &Program, + types: &'types TypeInterner, + source_epoch: Arc>, + edits: Vec, +) -> Result, String> { + if !metadata_equal(before, output) + || !crate::resource_materialization::functions_equal(&source_epoch, &before.functions) + || !same_program(&replay_edits(before, &edits)?, output) + { + return Err( + "actual callable sealing changed outside its exact recorded token edits".into(), + ); + } + let mut checked = before.resource_source.checked_program().cloned(); + for edit in &edits { + let current = &edit.current; + if !current.is_sealed() + || !current.validate_original() + || current.validate_types(types).is_err() + { + return Err("actual callable sealing produced an invalid current token".into()); + } + let selected = checked.get_or_insert_with(|| current.data.checked.clone()); + if !Arc::ptr_eq(selected, ¤t.data.checked) { + return Err("actual callable sealing mixed checked Source sessions".into()); + } + if let Some(previous) = &edit.previous { + if !bound_plan_relation(previous, current) { + return Err("actual callable sealing changed its bound token relation".into()); + } + } else if edit.slot != Slot::Adapter + || current.function() != edit.owner + || edits + .iter() + .filter(|getter| { + matches!(getter.slot, Slot::Getter { .. }) && getter.current == *current + }) + .count() + != 1 + { + return Err( + "initial callable adapter insertion lacks its actual paired getter token".into(), + ); + } + } + if let Some(checked) = &checked { + if before.resource_source.checked_program().is_some() { + // The execution view can still be a protected pre-seal peer (for + // example Graph). Its renewal remains owned by that producer. This + // receipt binds the exact real input/output tables and type meanings; + // each consuming peer authenticates its own current view. + before.resource_source.validate_types(types)?; + } + if edits + .iter() + .any(|edit| !Arc::ptr_eq(checked, &edit.current.data.checked)) + { + return Err("actual callable sealing lost its exact checked program".into()); + } + } + Ok(CheckedCallableSealing(Box::new(Sealing { + types, + before: before.clone(), + output: output.clone(), + checked, + source_epoch, + edits, + }))) +} + +#[cfg(test)] +#[path = "callable_sealing_execution_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/reflected_callable_tests.rs b/crates/jett_hir/src/reflected_callable_tests.rs new file mode 100644 index 00000000..53b3778f --- /dev/null +++ b/crates/jett_hir/src/reflected_callable_tests.rs @@ -0,0 +1,300 @@ +use super::*; + +const SOURCE: &str = "namespace app\ntype Positive = int64 where value > 0\nfunction five() returns int64:\n return 5\nfunction zero() returns int64:\n return 0\nstruct Holder:\n first: function() returns int64\n second: function() returns int64\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns Positive:\n return type.field_value[Holder, function() returns Positive](view holder, view field)\n"; + +fn checked(source: &str) -> Arc { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ) +} + +fn fixture() -> (Program, Arc) { + let checked = checked(SOURCE); + let hir = + lower_checked_ordinary_program(&checked).expect("ordinary checked reflected lowering"); + (hir, checked) +} + +#[test] +fn ordinary_checked_constants_and_raw_refinements_have_no_reflected_source_archive() { + let checked = checked( + "namespace app\ntype Positive = int64 where value > 0\nfunction positive() returns Positive:\n return 5\nPositive cached = comptime positive()\nfunction main() returns nothing:\n int64 ordinary = comptime 7\n println(coarsen cached + ordinary)\n return nothing\n", + ); + let program = lower_checked_ordinary_program(&checked).expect("ordinary checked constants"); + assert!(!has_reflected_callable_plans(&program)); + assert!(program.resource_source.manifest().is_none()); + assert!(program.resource_source.functions().is_empty()); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_manifest.hooks().next().is_none()); + let unsealed = lower( + checked.module(), + checked.resolved(), + checked.checked(), + checked.source_origins(), + ) + .expect("existing ordinary lowering"); + assert!(super::super::resource_materialization::functions_equal( + &program.functions, + &unsealed.functions, + )); +} + +#[test] +fn checked_read_is_lazy_and_each_ordinal_owns_an_original_adapter() { + let (program, checked) = fixture(); + assert!(program.resource_source.belongs_to(&checked)); + assert!(program.resource_manifest.kinds().is_empty()); + assert!(program.resource_manifest.hooks().next().is_none()); + validate(&program).unwrap(); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + let owner = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let plans = reflected_callable_plans(owner); + assert_eq!(plans.len(), 2); + assert_ne!(plans[0].function(), plans[1].function()); + for plan in plans { + assert!(plan.validate_original()); + assert_eq!(plan.original_owner(), Some(owner.id)); + let adapter = &program.functions[plan.function().index() as usize]; + assert_eq!(adapter.capture_count, 1); + assert_eq!(adapter.params[0].ty, plan.source_type()); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + target, + kind: + HandleKind::ReflectedReturn { + callable: Some(root), + .. + }, + .. + }, + .. + })) = &adapter.body.statements[0].kind + else { + panic!("canonical return root") + }; + assert_eq!(root, &plan); + let ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + ownership: CallOwnership::Generated(generated), + } = &target.kind + else { + panic!("one original source invocation") + }; + assert_eq!(target.ty, plan.checked_plan().source_return()); + assert!(matches!(callee.kind, ExpressionKind::Local(LocalId(0)))); + assert!(args.is_empty() && evaluation_order.is_empty()); + assert!( + matches!(&generated.operation,GeneratedOperation::ReflectedCallableAdapter {plan:admission,..} if admission == plan.checked_plan()) + ); + } + let mut owner = owner.clone(); + let mut read_calls = 0; + super::super::resource_materialization::walk_block(&mut owner.body, &mut |value| { + if matches!( + value.kind, + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(_)), + .. + } + ) { + read_calls += 1; + } + assert!(!matches!( + value.kind, + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { .. }, + .. + } + )); + Ok(()) + }) + .unwrap(); + assert_eq!( + read_calls, 1, + "read constructs descriptors; invocation owns validation" + ); +} + +#[test] +fn same_signature_source_body_and_ordinal_and_root_substitution_are_rejected() { + let (original, checked) = fixture(); + for mutation in 0..5 { + let mut program = original.clone(); + let owner_index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let plans = reflected_callable_plans(&program.functions[owner_index]); + match mutation { + 0 => { + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap(); + let StatementKind::Return(Some(value)) = &mut function.body.statements[0].kind + else { + panic!("source return") + }; + value.kind = ExpressionKind::Int(0); + } + 1 => { + super::super::resource_materialization::walk_block( + &mut program.functions[owner_index].body, + &mut |value| { + if let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut value.kind + { + plans.swap(0, 1); + } + Ok(()) + }, + ) + .unwrap(); + } + 2 => { + let adapter = &mut program.functions[plans[0].function().index() as usize]; + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { callable, .. }, + .. + }, + .. + })) = &mut adapter.body.statements[0].kind + else { + panic!("return root") + }; + *callable = Some(plans[1].clone()); + } + 3 => { + let predicate = plans[0].original_predicates()[0].id; + let StatementKind::Return(Some(value)) = &mut program.functions + [predicate.index() as usize] + .body + .statements[0] + .kind + else { + panic!("predicate return") + }; + value.kind = ExpressionKind::Bool(true); + } + _ => { + program.functions[owner_index] + .identity + .scoped_type_bindings + .clear(); + program.functions[owner_index] + .identity + .declaration + .namespace = "other".into(); + } + } + assert!(validate(&program).is_err(), "forgery {mutation}"); + assert!( + complete_value_conversions(&mut program, &checked.checked().interner).is_err(), + "forgery cannot reseal {mutation}" + ); + } +} + +#[test] +fn checked_plan_rejects_view_parameter_and_carrier_strengthening() { + let checked = checked( + "namespace app\ntype Positive = int64 where value > 0\ntype Text = string where value != \"\"\nstruct Holder:\n callback: function(int64) returns int64\n validated: function(int64) returns Positive\n refined_parameter: function(Positive) returns Positive\n view_parameter: function(view int64) returns Positive\n foreign_return: function(int64) returns Text\nfunction inspect(view holder: Holder, view field: TypeField) returns function(int64) returns Positive:\n return type.field_value[Holder, function(int64) returns Positive](view holder, view field)\nfunction unrelated(value: Text) returns nothing:\n return nothing\n", + ); + let types = &checked.checked().interner; + let positive = types + .type_ids() + .find( + |ty| matches!(types.resolve(*ty), Type::Refinement {name,..} if name == "app.Positive"), + ) + .unwrap(); + let text = types + .type_ids() + .find(|ty| matches!(types.resolve(*ty), Type::Refinement {name,..} if name == "app.Text")) + .unwrap(); + let function_type = |parameter, view, returned| { + types + .type_ids() + .find(|ty| { + matches!(types.resolve(*ty), Type::Function {params,view_params,return_type} + if params == &[parameter] && view_params == &[view] && *return_type == returned) + }) + .unwrap() + }; + let actual = function_type(TypeInterner::INT64, false, TypeInterner::INT64); + let requested = function_type(TypeInterner::INT64, false, positive); + let refined_parameter = function_type(positive, false, positive); + let view_parameter = function_type(TypeInterner::INT64, true, positive); + let foreign_return = function_type(TypeInterner::INT64, false, text); + assert!( + checked + .checked() + .reflected_callable_plan(actual, requested) + .is_some() + ); + assert!( + checked + .checked() + .reflected_callable_plan(requested, requested) + .is_some(), + "the exact callable contract remains admitted" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, refined_parameter) + .is_none(), + "a refined caller parameter is not implicitly coarsened to int64" + ); + assert!( + checked + .checked() + .reflected_callable_plan(refined_parameter, actual) + .is_none(), + "an int64 caller parameter cannot implicitly establish Positive" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, view_parameter) + .is_none(), + "owned and view parameter contracts must match" + ); + assert!( + checked + .checked() + .reflected_callable_plan(view_parameter, requested) + .is_none(), + "view mismatch is refused in both directions" + ); + assert!( + checked + .checked() + .reflected_callable_plan(actual, foreign_return) + .is_none(), + "an int64 result cannot establish a string-backed refinement" + ); + let (program, _) = fixture(); + assert!(has_reflected_callable_plans(&program)); +} diff --git a/crates/jett_hir/src/reflected_field_sources.rs b/crates/jett_hir/src/reflected_field_sources.rs new file mode 100644 index 00000000..a5b2053d --- /dev/null +++ b/crates/jett_hir/src/reflected_field_sources.rs @@ -0,0 +1,928 @@ +//! Constructor-only correspondence for direct original nominal struct field loops. +//! Metadata is runtime data; ordinal-specific checked bodies remain Source authority. +use crate::{ + Block, DeclarationKind, Expression, ExpressionKind as E, FieldId, Function, FunctionId, + LocalId, Program, ReflectedTypeArm, Statement, StatementKind as S, +}; +use jett_common::Span; +use jett_intrinsics::IntrinsicId; +use jett_parser::ast::{self, Expr, Item, Stmt, TypeExpr}; +use jett_resolve::{DefId, DefKind, ResolveResult}; +use jett_typecheck::{ + CheckResult, CheckedBodyFacts, CheckedComptimeTypeSelection, CheckedGenericSpecialization, + CheckedResourceProgram, +}; +use jett_types::{ReflectionFieldInfo, ReflectionTypeInfo, Type, TypeId, TypeInterner}; +use std::sync::Arc; + +#[derive(Debug)] +struct SelectedBody { + checked_index: usize, + address: usize, + bound_type: TypeId, + reflection: ReflectionTypeInfo, + // The constructor retains the full original checked expansion. No public + // constructor, setter, or caller-supplied fact map can substitute this body. + _facts: CheckedBodyFacts, +} + +#[derive(Debug)] +struct Dispatch { + checked: Arc, + function: FunctionId, + declaration: DefId, + function_span: Span, + loop_address: usize, + binding_address: usize, + loop_span: Span, + binding_span: Span, + binder: DefId, + owner_definition: DefId, + owner: TypeId, + owner_reflection: ReflectionTypeInfo, + fields: Vec<(TypeId, ReflectionFieldInfo)>, + selected: Vec, + key: LocalId, + field_index: FieldId, + type_info_field: FieldId, + loop_path: Vec, + dispatch_path: Vec, + containers: Vec>, + scopes: Vec>, + type_info: Expression, + arms: Vec, +} + +/// Immutable original Source authority; only successful checked HIR lowering +/// can create it. Clones retain the same checked session and exact body record. +#[derive(Debug, Clone)] +pub struct ReflectedFieldDispatchProof { + data: Arc, +} +impl PartialEq for ReflectedFieldDispatchProof { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ReflectedFieldDispatchProof {} + +impl ReflectedFieldDispatchProof { + /// Read-only membership in one exact original checked ordinal body. This + /// observes no runtime metadata and supplies no Resource custody/provider. + pub fn matches_field_update_scope( + &self, + scope: &crate::RequiredScopeIdentity, + binding: &crate::ScopedTypeBinding, + ) -> bool { + let CheckedComptimeTypeSelection::ReflectedIteration(ordinal) = scope.selection else { + return false; + }; + self.data.binding_span == scope.owner + && self.validate_original() + && self.data.selected.get(ordinal).is_some_and(|selected| { + selected.checked_index == scope.index + && selected.bound_type == binding.ty + && selected.reflection == binding.reflection + }) + } + pub fn function(&self) -> FunctionId { + self.data.function + } + pub fn key(&self) -> LocalId { + self.data.key + } + pub fn owner(&self) -> TypeId { + self.data.owner + } + pub fn owner_reflection(&self) -> &ReflectionTypeInfo { + &self.data.owner_reflection + } + pub fn fields(&self) -> &[(TypeId, ReflectionFieldInfo)] { + &self.data.fields + } + pub fn type_info(&self) -> &Expression { + &self.data.type_info + } + pub fn arms(&self) -> &[ReflectedTypeArm] { + &self.data.arms + } + pub fn field_index(&self) -> FieldId { + self.data.field_index + } + pub fn type_info_field(&self) -> FieldId { + self.data.type_info_field + } + pub fn loop_path(&self) -> &[usize] { + &self.data.loop_path + } + pub fn dispatch_path(&self) -> &[usize] { + &self.data.dispatch_path + } + pub fn arm_container_path(&self, index: usize) -> Option<&[usize]> { + self.data.containers.get(index).map(Vec::as_slice) + } + /// The explicit Scope body path in the original lexical inventory. + pub fn arm_path(&self, index: usize) -> Option<&[usize]> { + self.data.scopes.get(index).map(Vec::as_slice) + } + pub fn iteration_index(&self, index: usize) -> Option { + self.data.arms.get(index).map(|arm| arm.iteration_index) + } + pub(crate) fn belongs_to(&self, checked: &Arc) -> bool { + Arc::ptr_eq(&self.data.checked, checked) + } + pub(crate) fn matches( + &self, + functions: &[Function], + type_info: &Expression, + arms: &[ReflectedTypeArm], + ) -> bool { + if !self.validate_original() { + return false; + } + let Some(original) = functions + .iter() + .find(|function| function.id == self.function()) + else { + return false; + }; + let mut candidate = original.clone(); + let Some(statement) = root_dispatch_mut(&mut candidate.body, &self.data.dispatch_path) + else { + return false; + }; + statement.kind = S::ReflectedTypeDispatch { + type_info: type_info.clone(), + arms: arms.to_vec(), + }; + // The existing archive comparison preserves float bits, including NaN. + crate::resource_materialization::functions_equal( + std::slice::from_ref(original), + std::slice::from_ref(&candidate), + ) + } + pub(crate) fn validate_original(&self) -> bool { + let data = &self.data; + let Some(source) = original_function(&data.checked, data.declaration, data.function_span) + else { + return false; + }; + let loops = source + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::For(source) if source.span == data.loop_span => Some(source), + _ => None, + }) + .collect::>(); + let [source_loop] = loops.as_slice() else { + return false; + }; + if std::ptr::from_ref(*source_loop).addr() != data.loop_address { + return false; + } + let bindings = source_loop + .body + .stmts + .iter() + .filter_map(|statement| match statement { + Stmt::ComptimeTypeBind(source) if source.span == data.binding_span => Some(source), + _ => None, + }) + .collect::>(); + let [binding] = bindings.as_slice() else { + return false; + }; + if std::ptr::from_ref(*binding).addr() != data.binding_address + || direct_owner(&data.checked, source_loop) + != Some(( + data.owner_definition, + data.owner, + data.owner_reflection.clone(), + )) + || binder(&data.checked, source_loop) != Some(data.binder) + || checked_fields(&data.checked, data.owner).as_ref() != Some(&data.fields) + || !binding_selects_binder(&data.checked, binding, data.binder) + { + return false; + } + let Some(rows) = data + .checked + .checked() + .comptime_type_bindings + .get(&binding.span) + else { + return false; + }; + if rows.len() != data.selected.len() { + return false; + } + data.selected.iter().enumerate().all(|(ordinal, selected)| { + let Some(row) = rows.get(selected.checked_index) else { + return false; + }; + std::ptr::from_ref(&row.body).addr() == selected.address + && row.selection == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + && row.bound_type == selected.bound_type + && row.reflection == selected.reflection + }) + } +} + +pub(crate) fn capture( + program: &Program, + checked: &Arc, +) -> Vec { + let mut proofs = Vec::new(); + for function in &program.functions { + if function.identity.declaration.kind != DeclarationKind::Function + || !function.identity.type_arguments.is_empty() + || !function.identity.scoped_type_bindings.is_empty() + || function.identity.specialization != CheckedGenericSpecialization::default() + || function.capture_count != 0 + { + continue; + } + let Some(declaration) = function.source_definition else { + continue; + }; + let Some(source) = original_function(checked, declaration, function.span) else { + continue; + }; + let Some(definition) = checked + .resolved() + .scope_table + .definitions + .get(declaration.index() as usize) + else { + continue; + }; + let namespace = definition.namespace.as_deref().unwrap_or(""); + let canonical_name = if namespace.is_empty() { + source.name.name.clone() + } else { + format!("{namespace}.{}", source.name.name) + }; + if function.identity.declaration.name != source.name.name + || definition.name != canonical_name + || function.identity.declaration.namespace + != definition.namespace.as_deref().unwrap_or("") + || checked.source_origins().get(&source.span.file) + != Some(&function.identity.declaration.origin) + { + continue; + } + // Root body facts are the only admitted producer context. Nested or + // generic reflected expansions need their own checked scope cursor. + for statement in &source.body.stmts { + let Stmt::For(source_loop) = statement else { + continue; + }; + let Some((owner_definition, owner, owner_reflection)) = + direct_owner(checked, source_loop) + else { + continue; + }; + let Some(fields) = checked_fields(checked, owner) else { + continue; + }; + let Some(binder) = binder(checked, source_loop) else { + continue; + }; + let loops = function + .body + .statements + .iter() + .enumerate() + .filter(|(_, statement)| { + statement.span == source_loop.span && matches!(statement.kind, S::For { .. }) + }) + .collect::>(); + let [(loop_index, loop_statement)] = loops.as_slice() else { + continue; + }; + let S::For { + key, + value: None, + by_view: false, + iterable, + body, + } = &loop_statement.kind + else { + continue; + }; + let Some(local) = function.locals.get(key.index() as usize) else { + continue; + }; + if local.id != *key + || local.span != source_loop.variable.span + || local.mutable + || local.name != source_loop.variable.name + || local.view_source.is_some() + || checked.checked().definition_types.get(&binder) != Some(&local.ty) + || !metadata_list_matches( + iterable, + local.ty, + owner_reflection.type_name.as_str(), + &fields, + checked, + ) + { + continue; + } + for statement in &source_loop.body.stmts { + let Stmt::ComptimeTypeBind(binding) = statement else { + continue; + }; + if !binding_selects_binder(checked, binding, binder) { + continue; + } + if let Some(proof) = capture_dispatch( + checked, + function, + declaration, + source_loop, + binding, + *loop_index, + body, + *key, + binder, + owner_definition, + owner, + &owner_reflection, + &fields, + ) { + proofs.push(proof); + } + } + } + } + proofs +} + +#[allow(clippy::too_many_arguments)] +fn capture_dispatch( + checked: &Arc, + function: &Function, + declaration: DefId, + source_loop: &ast::ForStmt, + binding: &ast::ComptimeTypeBindStmt, + loop_index: usize, + body: &Block, + key: LocalId, + binder: DefId, + owner_definition: DefId, + owner: TypeId, + owner_reflection: &ReflectionTypeInfo, + fields: &[(TypeId, ReflectionFieldInfo)], +) -> Option { + let dispatches = body + .statements + .iter() + .enumerate() + .filter(|(_, statement)| { + statement.span == binding.span + && matches!(statement.kind, S::ReflectedTypeDispatch { .. }) + }) + .collect::>(); + let [(dispatch_index, statement)] = dispatches.as_slice() else { + return None; + }; + let S::ReflectedTypeDispatch { type_info, arms } = &statement.kind else { + return None; + }; + let local = function.locals.get(key.index() as usize)?; + let E::Field { + base, + owner_type, + field, + } = &type_info.kind + else { + return None; + }; + let Type::Struct(field_struct) = checked.checked().interner.resolve(local.ty) else { + return None; + }; + let field_header = checked.checked().interner.resolve_struct(*field_struct); + if *field != FieldId::new(9) + || *owner_type != local.ty + || field_header.fields.get(9).map(|(_, ty)| *ty) != Some(type_info.ty) + || !matches!(base.kind, E::Local(found) if found == key) + || base.ty != local.ty + || type_info.span != binding.value.span() + { + return None; + } + let rows = checked + .checked() + .comptime_type_bindings + .get(&binding.span)?; + if arms.len() != fields.len() || rows.len() != fields.len() || fields.is_empty() { + return None; + } + let mut selected = Vec::with_capacity(fields.len()); + let mut containers = Vec::with_capacity(fields.len()); + let mut scopes = Vec::with_capacity(fields.len()); + let loop_path = vec![0, loop_index]; + let dispatch_path = vec![0, loop_index, 2, 0, 0, *dispatch_index]; + for (ordinal, ((bound_type, metadata), arm)) in fields.iter().zip(arms).enumerate() { + let candidates = rows + .iter() + .enumerate() + .filter(|(_, row)| { + row.selection == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + }) + .collect::>(); + let [(checked_index, row)] = candidates.as_slice() else { + return None; + }; + let [wrapper] = arm.body.statements.as_slice() else { + return None; + }; + let S::Scope(original_body) = &wrapper.kind else { + return None; + }; + if row.bound_type != *bound_type + || row.reflection != metadata.type_info + || arm.iteration_index != ordinal + || arm.bound_type != *bound_type + || arm.reflection_identity != metadata.type_info.reflection_identity() + || wrapper.span != binding.span + || original_body.span != binding.body.span + || arm.body.span != binding.body.span + { + return None; + } + selected.push(SelectedBody { + checked_index: *checked_index, + address: std::ptr::from_ref(&row.body).addr(), + bound_type: row.bound_type, + reflection: row.reflection.clone(), + _facts: row.body.clone(), + }); + let mut container = dispatch_path.clone(); + container.extend([2, ordinal]); + let mut scope = container.clone(); + scope.extend([0, 0, 2, 0]); + containers.push(container); + scopes.push(scope); + } + Some(ReflectedFieldDispatchProof { + data: Arc::new(Dispatch { + checked: checked.clone(), + function: function.id, + declaration, + function_span: function.span, + loop_address: std::ptr::from_ref(source_loop).addr(), + binding_address: std::ptr::from_ref(binding).addr(), + loop_span: source_loop.span, + binding_span: binding.span, + binder, + owner_definition, + owner, + owner_reflection: owner_reflection.clone(), + fields: fields.to_vec(), + selected, + key, + field_index: FieldId::new(0), + type_info_field: FieldId::new(9), + loop_path, + dispatch_path, + containers, + scopes, + type_info: type_info.clone(), + arms: arms.clone(), + }), + }) +} + +fn original_function( + checked: &CheckedResourceProgram, + declaration: DefId, + span: Span, +) -> Option<&ast::FunctionDef> { + original_function_parts(checked.module(), checked.resolved(), declaration, span) +} + +fn original_function_parts<'a>( + module: &'a ast::Module, + resolve: &ResolveResult, + declaration: DefId, + span: Span, +) -> Option<&'a ast::FunctionDef> { + let definition = resolve + .scope_table + .definitions + .get(declaration.index() as usize)?; + if definition.id != declaration || definition.kind != DefKind::Function { + return None; + } + let candidates = module + .items + .iter() + .filter_map(|item| match item { + Item::Function(source) + if source.name.span == definition.span + && source.span == span + && source.type_params.is_empty() => + { + Some(source) + } + _ => None, + }) + .collect::>(); + let [source] = candidates.as_slice() else { + return None; + }; + Some(*source) +} + +pub(crate) fn original_root_function( + module: &ast::Module, + resolve: &ResolveResult, + source: &ast::FunctionDef, + declaration: DefId, +) -> bool { + original_function_parts(module, resolve, declaration, source.span) + .is_some_and(|original| std::ptr::eq(original, source)) +} + +fn direct_owner( + checked: &CheckedResourceProgram, + source: &ast::ForStmt, +) -> Option<(DefId, TypeId, ReflectionTypeInfo)> { + direct_owner_parts(checked.resolved(), checked.checked(), source) +} + +fn direct_owner_parts( + resolve: &ResolveResult, + check: &CheckResult, + source: &ast::ForStmt, +) -> Option<(DefId, TypeId, ReflectionTypeInfo)> { + let Expr::GenericCall(_, operands, arguments, occurrence) = &source.iterable else { + return None; + }; + if source.view + || source.value_variable.is_some() + || !arguments.is_empty() + || check.intrinsic_ids.get(occurrence) != Some(&IntrinsicId::TypeFields) + { + return None; + } + let [TypeExpr::Named(operand)] = operands.as_slice() else { + return None; + }; + let definition = *resolve.resolutions.get(&operand.span)?; + let header = resolve + .scope_table + .definitions + .get(definition.index() as usize)?; + if header.id != definition || header.kind != DefKind::Struct { + return None; + } + let [owner] = check.intrinsic_type_arguments.get(occurrence)?.as_slice() else { + return None; + }; + let [reflection] = check + .intrinsic_reflection_arguments + .get(occurrence)? + .as_slice() + else { + return None; + }; + let types = &check.interner; + if check.definition_types.get(&definition) != Some(owner) + || !matches!(types.resolve(*owner), Type::Struct(_)) + || !types.nominal_type_arguments(*owner).is_empty() + || reflection.kind != "struct" + || !reflection.args.is_empty() + || check.reflection_metadata.get_type_info_for_id(*owner) != Some(reflection) + { + return None; + } + Some((definition, *owner, reflection.clone())) +} + +fn binder(checked: &CheckedResourceProgram, source: &ast::ForStmt) -> Option { + binder_parts(checked.resolved(), source) +} + +fn binder_parts(resolve: &ResolveResult, source: &ast::ForStmt) -> Option { + let candidates = resolve + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Variable + && definition.span == source.variable.span + && definition.name == source.variable.name + }) + .collect::>(); + let [definition] = candidates.as_slice() else { + return None; + }; + Some(definition.id) +} + +fn binding_selects_binder( + checked: &CheckedResourceProgram, + binding: &ast::ComptimeTypeBindStmt, + binder: DefId, +) -> bool { + binding_selects_binder_parts(checked.resolved(), binding, binder) +} + +fn binding_selects_binder_parts( + resolve: &ResolveResult, + binding: &ast::ComptimeTypeBindStmt, + binder: DefId, +) -> bool { + let Expr::FieldAccess(base, member, _) = &binding.value else { + return false; + }; + let Expr::Ident(variable) = base.as_ref() else { + return false; + }; + member.name == "type_info" && resolve.resolutions.get(&variable.span) == Some(&binder) +} + +fn checked_fields( + checked: &CheckedResourceProgram, + owner: TypeId, +) -> Option> { + checked_fields_parts(checked.checked(), owner) +} + +fn checked_fields_parts( + check: &CheckResult, + owner: TypeId, +) -> Option> { + let types = &check.interner; + let Type::Struct(id) = types.resolve(owner) else { + return None; + }; + let fields = &types.resolve_struct(*id).fields; + let metadata = check.reflection_metadata.get_type_fields_for_id(owner)?; + if metadata.len() != fields.len() + || metadata + .iter() + .zip(fields) + .enumerate() + .any(|(index, (metadata, (name, ty)))| { + metadata.index != index + || metadata.name != *name + || ty.index() as usize >= types.len() + }) + { + return None; + } + Some( + fields + .iter() + .map(|(_, ty)| *ty) + .zip(metadata.iter().cloned()) + .collect(), + ) +} + +/// Called while lowering the exact original root For AST occurrence. This is +/// only a lowering choice; Source authority is minted later in archive capture. +pub(crate) fn original_loop_bindings( + resolve: &ResolveResult, + check: &CheckResult, + source: &ast::ForStmt, +) -> Vec { + if source.view || source.value_variable.is_some() { + return Vec::new(); + } + let Some((_, owner, _)) = direct_owner_parts(resolve, check, source) else { + return Vec::new(); + }; + let Some(fields) = checked_fields_parts(check, owner) else { + return Vec::new(); + }; + let Some(binder) = binder_parts(resolve, source) else { + return Vec::new(); + }; + source + .body + .stmts + .iter() + .filter_map(|statement| { + let Stmt::ComptimeTypeBind(binding) = statement else { + return None; + }; + if !binding_selects_binder_parts(resolve, binding, binder) { + return None; + } + let rows = check.comptime_type_bindings.get(&binding.span)?; + if rows.len() != fields.len() || fields.is_empty() { + return None; + } + fields + .iter() + .enumerate() + .all(|(ordinal, (ty, field))| { + let candidates = rows + .iter() + .filter(|row| { + row.selection + == CheckedComptimeTypeSelection::ReflectedIteration(ordinal) + }) + .collect::>(); + let [row] = candidates.as_slice() else { + return false; + }; + row.bound_type == *ty && row.reflection == field.type_info + }) + .then_some(binding.span) + }) + .collect() +} + +fn metadata_list_matches( + iterable: &Expression, + element_type: TypeId, + owner_name: &str, + fields: &[(TypeId, ReflectionFieldInfo)], + checked: &CheckedResourceProgram, +) -> bool { + let types = &checked.checked().interner; + let Type::Struct(id) = types.resolve(element_type) else { + return false; + }; + let definition = types.resolve_struct(*id); + let expected = [ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ]; + if definition.name != "TypeField" + || !definition + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(expected) + || definition.fields[0].1 != TypeInterner::INT64 + || !matches!(types.resolve(iterable.ty), Type::List(element) if *element == element_type) + { + return false; + } + let E::ListConstruct { elements } = &iterable.kind else { + return false; + }; + elements.len() == fields.len() + && elements.iter().zip(fields).all(|(element, (_, metadata))| { + let E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + } = &element.kind + else { + return false; + }; + *struct_type == element_type + && element.ty == element_type + && fields.len() == expected.len() + && evaluation_order.iter().copied().eq(0..expected.len()) + && refinement_predicates.is_empty() + && fields + .iter() + .zip(&definition.fields) + .all(|(field, (_, ty))| field.ty == *ty) + && i128::try_from(metadata.index) + .ok() + .is_some_and(|index| matches!(fields[0].kind, E::Int(value) if value == index)) + && matches!(&fields[1].kind, E::String(value) if value == owner_name) + && matches!(fields[2].kind, E::OptionalNone) + && matches!(&fields[3].kind, E::String(value) if value == &metadata.name) + && matches!(&fields[4].kind, E::String(value) if value == &metadata.type_name) + && matches!(&fields[5].kind, E::String(value) if value == &metadata.kind) + && metadata_tag_matches( + &fields[6], + ReflectionTypeInfo::kind_tag_variant(&metadata.kind), + types, + ) + && matches!(&fields[7].kind, E::String(value) if value == &metadata.serialize_name) + && matches!(fields[8].kind, E::Bool(value) if value == metadata.has_secret) + && metadata_type_info_matches(&fields[9], &metadata.type_info, types) + }) +} + +fn metadata_tag_matches(value: &Expression, expected: &str, types: &TypeInterner) -> bool { + let E::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &value.kind + else { + return false; + }; + let Type::Enum(id) = types.resolve(*enum_type) else { + return false; + }; + *enum_type == value.ty + && payloads.is_empty() + && evaluation_order.is_empty() + && types + .resolve_enum(*id) + .variants + .get(variant.index() as usize) + .is_some_and(|variant| variant.name == expected && variant.fields.is_empty()) +} + +fn metadata_type_info_matches( + value: &Expression, + expected: &ReflectionTypeInfo, + types: &TypeInterner, +) -> bool { + let E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + } = &value.kind + else { + return false; + }; + let Type::Struct(id) = types.resolve(*struct_type) else { + return false; + }; + let header = types.resolve_struct(*id); + let members = [ + "type_name", + "kind", + "kind_tag", + "primitive_tag", + "has_secret", + "args", + ]; + if *struct_type != value.ty + || header.name != "TypeInfo" + || !header + .fields + .iter() + .map(|(name, _)| name.as_str()) + .eq(members) + || fields.len() != members.len() + || !evaluation_order.iter().copied().eq(0..members.len()) + || !refinement_predicates.is_empty() + || !fields + .iter() + .zip(&header.fields) + .all(|(field, (_, ty))| field.ty == *ty) + || !matches!(&fields[0].kind, E::String(name) if name == &expected.type_name) + || !matches!(&fields[1].kind, E::String(kind) if kind == &expected.kind) + || !metadata_tag_matches( + &fields[2], + ReflectionTypeInfo::kind_tag_variant(&expected.kind), + types, + ) + || !matches!(fields[4].kind, E::Bool(secret) if secret == expected.has_secret) + { + return false; + } + let primitive_matches = match (&fields[3].kind, expected.primitive_tag.as_deref()) { + (E::OptionalNone, None) => true, + (E::OptionalSome(tag), Some(expected)) => { + matches!(types.resolve(fields[3].ty), Type::Optional(inner) if *inner == tag.ty) + && metadata_tag_matches(tag, expected, types) + } + _ => false, + }; + let E::ListConstruct { elements } = &fields[5].kind else { + return false; + }; + primitive_matches + && matches!(types.resolve(fields[5].ty), Type::List(inner) if *inner == value.ty) + && elements.len() == expected.args.len() + && elements + .iter() + .zip(&expected.args) + .all(|(argument, expected)| { + argument.ty == value.ty && metadata_type_info_matches(argument, expected, types) + }) +} + +fn root_dispatch_mut<'a>(body: &'a mut Block, path: &[usize]) -> Option<&'a mut Statement> { + let [0, loop_index, 2, 0, 0, dispatch_index] = path else { + return None; + }; + let S::For { body, .. } = &mut body.statements.get_mut(*loop_index)?.kind else { + return None; + }; + let statement = body.statements.get_mut(*dispatch_index)?; + matches!(statement.kind, S::ReflectedTypeDispatch { .. }).then_some(statement) +} diff --git a/crates/jett_hir/src/reflected_fields.rs b/crates/jett_hir/src/reflected_fields.rs new file mode 100644 index 00000000..e4afb77f --- /dev/null +++ b/crates/jett_hir/src/reflected_fields.rs @@ -0,0 +1,2447 @@ +//! Checked proof plans for reflected reads. Selector admission remains in the +//! original intrinsic; these plans execute only after that read succeeds. +use super::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReflectedFieldValidation { + Validate(Vec), + /// Internal MIR stage: execute only the original selector checks and read. + Read, + /// Internal MIR stage: strict original read followed by a lazy adapter. + ReadCallable(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReflectedFieldPlan { + pub source_type: TypeId, + pub requested_type: TypeId, + pub action: ReflectedFieldAction, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReflectedFieldAction { + Exact, + Predicates(Vec), + Unsupported(ReflectedFieldUnsupported), + CallableReturn(ReflectedCallablePlan), + Refine { + base: Box, + predicates: Vec, + }, + List(Box), + Set(Box), + Map { + key: Box, + value: Box, + }, + Optional(Box), + Result { + ok: Box, + error: Box, + }, +} + +impl ReflectedFieldPlan { + /// Canonical execution order, also used by function reachability visitors. + pub fn predicates(&self) -> Vec<&RefinementPredicate> { + let mut result = Vec::new(); + self.collect_predicates(&mut result); + result + } + + fn collect_predicates<'a>(&'a self, result: &mut Vec<&'a RefinementPredicate>) { + match &self.action { + ReflectedFieldAction::Exact | ReflectedFieldAction::Unsupported(_) => {} + ReflectedFieldAction::Predicates(predicates) => result.extend(predicates), + ReflectedFieldAction::CallableReturn(plan) => { + plan.return_plan().collect_predicates(result) + } + ReflectedFieldAction::Refine { base, predicates } => { + base.collect_predicates(result); + result.extend(predicates); + } + ReflectedFieldAction::List(child) + | ReflectedFieldAction::Set(child) + | ReflectedFieldAction::Optional(child) => child.collect_predicates(result), + ReflectedFieldAction::Map { key, value } => { + key.collect_predicates(result); + value.collect_predicates(result); + } + ReflectedFieldAction::Result { ok, error } => { + ok.collect_predicates(result); + error.collect_predicates(result); + } + } + } +} + +pub use jett_typecheck::{ + ProducedRefinementRequirement as ReflectedFieldRequirement, + ProducedRefinementRequirementPlan as ReflectedFieldRequirementPlan, + ProducedRefinementUnsupported as ReflectedFieldUnsupported, +}; + +pub fn is_reflected_field_intrinsic(id: IntrinsicId) -> bool { + matches!( + id, + IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue + ) +} + +/// A proof must call the declaration of that exact nominal refinement, not a +/// different predicate with the same carrier and bool signature. +pub fn refinement_predicate_declaration_matches( + declaration: &DeclarationId, + type_name: &str, +) -> bool { + let (namespace, name) = type_name.rsplit_once('.').unwrap_or(("", type_name)); + declaration.kind == DeclarationKind::RefinementPredicate + && declaration.namespace == namespace + && declaration.name == name +} + +pub fn valid_reflected_field_metadata_type(types: &TypeInterner, ty: TypeId) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + let Type::Struct(id) = types.resolve(ty) else { + return false; + }; + let fields = &types.resolve_struct(*id).fields; + let names = [ + "index", + "owner_type", + "owner_member", + "name", + "type_name", + "kind", + "kind_tag", + "serialize_name", + "has_secret", + "type_info", + ]; + types.resolve_struct(*id).name == "TypeField" + && fields.iter().map(|(name, _)| name.as_str()).eq(names) + && fields[0].1 == TypeInterner::INT64 + && [1, 3, 4, 5, 7] + .into_iter() + .all(|index| fields[index].1 == TypeInterner::STRING) + && (fields[2].1.index() as usize) < types.len() + && matches!(types.resolve(fields[2].1), Type::Optional(inner) if *inner == TypeInterner::STRING) + && fields[8].1 == TypeInterner::BOOL +} + +/// Flattened in the same order as checked hidden TypeField operands. Member +/// names are validated against the active enum/machine tag by the raw getter. +pub fn reflected_field_layout( + types: &TypeInterner, + id: IntrinsicId, + owner: TypeId, +) -> Option, usize, TypeId)>> { + if owner.index() as usize >= types.len() { + return None; + } + let fields = match (id, types.resolve(owner)) { + (IntrinsicId::TypeFieldValue, Type::Struct(id)) => types + .resolve_struct(*id) + .fields + .iter() + .enumerate() + .map(|(index, (_, ty))| (None, index, *ty)) + .collect(), + (IntrinsicId::TypeFieldValue, Type::Bitfield(id)) => types + .resolve_bitfield(*id) + .fields + .iter() + .enumerate() + .map(|(index, field)| (None, index, field.ty)) + .collect(), + (IntrinsicId::TypeVariantFieldValue, Type::Enum(id)) => types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| { + variant + .fields + .iter() + .enumerate() + .map(|(index, (_, ty))| (Some(variant.name.clone()), index, *ty)) + }) + .collect(), + ( + IntrinsicId::TypeMachineFieldValue, + Type::Machine(id) | Type::MachineState { machine: id, .. }, + ) => types + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| { + state + .fields + .iter() + .enumerate() + .map(|(index, (_, ty))| (Some(state.name.clone()), index, *ty)) + }) + .collect(), + _ => return None, + }; + Some(fields) +} + +fn contains_refinement(types: &TypeInterner, ty: TypeId, seen: &mut HashSet) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + let children = match types.resolve(ty) { + Type::Refinement { .. } => return true, + Type::Secret(inner) | Type::List(inner) | Type::Set(inner) | Type::Optional(inner) => { + vec![*inner] + } + Type::Result(a, b) | Type::Map(a, b) => vec![*a, *b], + Type::Function { + params, + return_type, + .. + } => params + .iter() + .copied() + .chain(std::iter::once(*return_type)) + .collect(), + // Named payload fields never make a requested nominal type contain a + // new producer invariant. Only its declared generic arguments do. + Type::Struct(_) + | Type::Enum(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Bitfield(_) + | Type::Interface(_) + | Type::Actor(_) => types.nominal_type_arguments(ty).to_vec(), + _ => Vec::new(), + }; + children + .into_iter() + .any(|child| contains_refinement(types, child, seen)) +} + +pub(crate) fn refinement_chain( + types: &TypeInterner, + mut ty: TypeId, +) -> Option<(TypeId, Vec)> { + let mut chain = Vec::new(); + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Refinement { base, .. } => { + chain.push(ty); + ty = *base; + } + _ => { + chain.reverse(); + return Some((ty, chain)); + } + } + } + None +} + +pub fn reflected_field_requirement( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, +) -> Option { + jett_typecheck::produced_refinement_requirement(types, actual, requested) +} + +fn requirement_node( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + if budget == 0 + || actual.index() as usize >= types.len() + || requested.index() as usize >= types.len() + { + return None; + } + let requirement = requirement_action(types, actual, requested, budget - 1)?; + Some(ReflectedFieldRequirementPlan { + source_type: actual, + requested_type: requested, + requirement, + }) +} + +fn requirement_action( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + use ReflectedFieldRequirement as R; + if actual == requested || !contains_refinement(types, requested, &mut HashSet::new()) { + return Some(R::Exact); + } + let (actual_base, actual_chain) = refinement_chain(types, actual)?; + // Only the declared root chain establishes an ancestor proof. A changed + // wrapper must still be ready even if every occupied child is already exact. + if actual_chain.contains(&requested) { + return Some(R::Exact); + } + // Keep the established non-builtin root-base contracts separate. A bare + // ready builtin request with refined children has a structural preflight + // even when the actual root declaration proves every child exactly. + if actual_base == requested + && !matches!( + types.resolve(requested), + Type::List(_) | Type::Set(_) | Type::Map(..) | Type::Optional(_) | Type::Result(..) + ) + { + return Some(R::Exact); + } + let (base, mut chain) = refinement_chain(types, requested)?; + if !chain.is_empty() { + // Reuse the nearest declared requested ancestor, including a common + // ancestor of sibling root branches. Its proof already establishes the + // complete base, so a remaining root suffix needs no wrapper preflight. + if let Some(index) = chain.iter().rposition(|ty| actual_chain.contains(ty)) { + chain.drain(..=index); + return Some(R::Predicates(chain)); + } + // A new root over a generic base must keep the original declared source + // spelling. Erasing its root here would turn changed-wrapper readiness + // into a false whole-schema Exact before any outer predicate executes. + let child = requirement_node(types, actual, base, budget)?; + return Some(match child.requirement { + R::Exact => R::Predicates(chain), + R::Unsupported(reason) => R::Unsupported(reason), + _ => R::Refine { + base: Box::new(child), + predicates: chain, + }, + }); + } + let child = |source, target| requirement_node(types, source, target, budget).map(Box::new); + Some( + match (types.resolve(actual_base), types.resolve(requested)) { + (Type::List(a), Type::List(b)) => R::List(child(*a, *b)?), + (Type::Set(a), Type::Set(b)) => R::Set(child(*a, *b)?), + (Type::Map(ak, av), Type::Map(bk, bv)) => R::Map { + key: child(*ak, *bk)?, + value: child(*av, *bv)?, + }, + (Type::Optional(a), Type::Optional(b)) => R::Optional(child(*a, *b)?), + (Type::Result(a, b), Type::Result(c, d)) => R::Result { + ok: child(*a, *c)?, + error: child(*b, *d)?, + }, + (_, Type::Secret(_)) => R::Unsupported(ReflectedFieldUnsupported::Secret), + _ => R::Unsupported(ReflectedFieldUnsupported::CallableOrNominal), + }, + ) +} + +pub fn validate_reflected_field_plans( + types: &TypeInterner, + intrinsic: IntrinsicId, + owner: TypeId, + requested: TypeId, + plans: &[ReflectedFieldPlan], +) -> Result<(), &'static str> { + let layout = reflected_field_layout(types, intrinsic, owner) + .ok_or("invalid reflected validation owner")?; + if plans.len() != layout.len() { + return Err("reflected validation plan count disagrees with field layout"); + } + for ((_, _, actual), plan) in layout.iter().zip(plans) { + if plan.source_type != *actual || plan.requested_type != requested { + return Err("reflected validation types disagree with declared field and request"); + } + if let ReflectedFieldAction::CallableReturn(callable) = &plan.action { + if callable.source_type() != plan.source_type + || callable.requested_type() != plan.requested_type + { + return Err("reflected callable action differs from its selected field types"); + } + callable.validate_types(types)?; + continue; + } + let requirement = requirement_node(types, *actual, requested, types.len()) + .ok_or("invalid reflected validation type")?; + validate_requirement_node(types, &requirement, plan)?; + } + Ok(()) +} + +pub(crate) fn validate_requirement_node( + types: &TypeInterner, + expected: &ReflectedFieldRequirementPlan, + plan: &ReflectedFieldPlan, +) -> Result<(), &'static str> { + use ReflectedFieldAction as A; + use ReflectedFieldRequirement as R; + if plan.source_type != expected.source_type || plan.requested_type != expected.requested_type { + return Err("reflected validation child types disagree with checked schema"); + } + match (&expected.requirement, &plan.action) { + (R::Exact, A::Exact) => Ok(()), + (R::Unsupported(a), A::Unsupported(b)) if a == b => Ok(()), + (R::Predicates(expected), A::Predicates(predicates)) => { + validate_plan_predicates(types, plan.requested_type, expected, predicates) + } + ( + R::Refine { + base: expected, + predicates: suffix, + }, + A::Refine { base, predicates }, + ) => { + validate_requirement_node(types, expected, base)?; + validate_plan_predicates(types, plan.requested_type, suffix, predicates) + } + (R::List(a), A::List(b)) | (R::Set(a), A::Set(b)) | (R::Optional(a), A::Optional(b)) => { + validate_requirement_node(types, a, b) + } + (R::Map { key: a, value: b }, A::Map { key: c, value: d }) + | (R::Result { ok: a, error: b }, A::Result { ok: c, error: d }) => { + validate_requirement_node(types, a, c)?; + validate_requirement_node(types, b, d) + } + _ => Err("reflected validation action disagrees with checked source"), + } +} + +fn validate_plan_predicates( + types: &TypeInterner, + requested: TypeId, + expected: &[TypeId], + predicates: &[RefinementPredicate], +) -> Result<(), &'static str> { + if predicates + .iter() + .map(|p| p.refined_type) + .ne(expected.iter().copied()) + { + return Err("reflected validation predicate suffix disagrees with checked source"); + } + let (base_type, _) = + refinement_chain(types, requested).ok_or("invalid reflected refinement chain")?; + let input_type = match types.resolve(base_type) { + Type::Secret(inner) => *inner, + _ => base_type, + }; + if input_type.index() as usize >= types.len() { + return Err("invalid reflected predicate input type"); + } + for predicate in predicates { + // Expected ids came from the checked chain, so a forged id can never + // reach resolve without first failing the exact suffix comparison. + let Type::Refinement { name, .. } = types.resolve(predicate.refined_type) else { + return Err("reflected predicate is not a refinement"); + }; + if predicate.type_name != *name + || predicate.base_type != base_type + || predicate.input_type != input_type + { + return Err("reflected predicate metadata disagrees with declaration"); + } + } + Ok(()) +} + +impl BodyLowerer<'_, '_> { + /// Reflected value observers use compiler metadata after their one source + /// operand. A pipeline result type comes from its checked step signature; + /// the step span itself describes the input, so never infer this from it. + pub(super) fn append_reflected_value_arguments( + &mut self, + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + reflection_arguments: &[ReflectionTypeInfo], + args: &mut Vec, + evaluation_order: &mut Vec, + result_type: Option, + span: Span, + ) -> Option<()> { + if type_arguments.len() != 1 || args.len() != 1 { + return Some(()); + } + let Some(info) = reflection_arguments.first() else { + return Some(()); + }; + let selected = match intrinsic { + IntrinsicId::TypeVariantValue => info.kind == "enum", + IntrinsicId::TypeMachineStateValue => { + matches!(info.kind.as_str(), "machine" | "machine_state") + } + IntrinsicId::TypeArg => reflection_arguments.len() == 1, + _ => false, + }; + if !selected { + return Some(()); + } + let result_type = result_type?; + let values: Vec = match intrinsic { + IntrinsicId::TypeVariantValue => { + let Some(variants) = self + .parent + .check + .reflection_metadata + .get_type_variants_for_id(type_arguments[0]) + .map(<[_]>::to_vec) + else { + self.parent + .error(span, "type.variant_value has no checked variant metadata"); + return None; + }; + variants + .iter() + .map(|variant| { + self.lower_reflection_type_variant( + variant, + &info.type_name, + result_type, + span, + ) + }) + .collect::>()? + } + IntrinsicId::TypeMachineStateValue => { + let machine = self.checked_reflection_machine(type_arguments[0], span)?; + let owner_name = info + .type_name + .split_once(" at ") + .map_or(info.type_name.as_str(), |(base, _)| base); + machine + .states + .iter() + .map(|state| { + self.lower_reflection_machine_state(state, owner_name, result_type, span) + }) + .collect::>()? + } + IntrinsicId::TypeArg => info + .args + .iter() + .map(|arg| self.lower_reflection_type_info(arg, result_type, span)) + .collect::>()?, + _ => return Some(()), + }; + for kind in values { + evaluation_order.push(args.len()); + args.push(Expression { + kind, + ty: result_type, + span, + }); + } + Some(()) + } + + /// Direct and piped getters share the same compiler-owned metadata tail. + /// User operands keep their checked source order and run before this tail. + pub(super) fn append_reflected_read_arguments( + &mut self, + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + reflection_arguments: &[ReflectionTypeInfo], + args: &mut Vec, + evaluation_order: &mut Vec, + span: Span, + ) -> Option<()> { + if type_arguments.len() != 2 || args.len() != 2 { + return Some(()); + } + let Some(info) = reflection_arguments.first() else { + return Some(()); + }; + let fields: Vec<(Option, ReflectionFieldInfo)> = match intrinsic { + IntrinsicId::TypeFieldValue if matches!(info.kind.as_str(), "struct" | "bitfield") => { + let Some(fields) = self + .parent + .check + .reflection_metadata + .get_type_fields_for_id(type_arguments[0]) + else { + self.parent + .error(span, "type.field_value has no checked field metadata"); + return None; + }; + fields.iter().map(|field| (None, field.clone())).collect() + } + IntrinsicId::TypeVariantFieldValue if info.kind == "enum" => { + let Some(variants) = self + .parent + .check + .reflection_metadata + .get_type_variants_for_id(type_arguments[0]) + else { + self.parent.error( + span, + "type.variant_field_value has no checked variant metadata", + ); + return None; + }; + variants + .iter() + .flat_map(|variant| { + variant + .fields + .iter() + .map(|field| (Some(variant.name.clone()), field.clone())) + }) + .collect() + } + IntrinsicId::TypeMachineFieldValue + if matches!(info.kind.as_str(), "machine" | "machine_state") => + { + let machine = self.checked_reflection_machine(type_arguments[0], span)?; + machine + .states + .iter() + .flat_map(|state| { + state + .fields + .iter() + .map(|field| (Some(state.name.clone()), field.clone())) + }) + .collect() + } + _ => return Some(()), + }; + let owner_name = if intrinsic == IntrinsicId::TypeMachineFieldValue { + info.type_name + .split_once(" at ") + .map_or(info.type_name.as_str(), |(base, _)| base) + } else { + &info.type_name + }; + let field_type = args[1].ty; + for (member, field) in fields { + let kind = self.lower_reflection_type_field( + &field, + owner_name, + member.as_deref(), + field_type, + span, + )?; + evaluation_order.push(args.len()); + args.push(Expression { + kind, + ty: field_type, + span, + }); + } + Some(()) + } + + pub(super) fn reflected_read_validation( + &mut self, + intrinsic: IntrinsicId, + type_arguments: &[TypeId], + span: Span, + ) -> Option> { + if !is_reflected_field_intrinsic(intrinsic) { + return Some(None); + } + let [owner, requested] = type_arguments else { + self.parent + .error(span, "reflected read has no exact checked type operands"); + return None; + }; + let Some(layout) = reflected_field_layout(&self.parent.check.interner, intrinsic, *owner) + else { + self.parent + .error(span, "reflected read has no checked field layout"); + return None; + }; + let mut plans = Vec::with_capacity(layout.len()); + for (ordinal, (_, _, source_type)) in layout.into_iter().enumerate() { + if source_type != *requested { + if let Some(checked) = self.parent.reflected_checked_program.clone() { + if let Some(callable) = checked + .checked() + .reflected_callable_plan(source_type, *requested) + { + let child = ReflectedFieldRequirementPlan { + source_type: callable.source_return(), + requested_type: callable.requested_return(), + requirement: callable.return_requirement().clone(), + }; + let return_plan = self.lower_reflected_requirement(child, span)?; + plans.push(ReflectedFieldPlan { + source_type, + requested_type: *requested, + action: ReflectedFieldAction::CallableReturn( + ReflectedCallablePlan::new( + checked, + callable, + return_plan, + span, + Some(ordinal), + ), + ), + }); + continue; + } + } + } + let Some(requirement) = requirement_node( + &self.parent.check.interner, + source_type, + *requested, + self.parent.check.interner.len(), + ) else { + self.parent + .error(span, "reflected read has invalid proof types"); + return None; + }; + plans.push(self.lower_reflected_requirement(requirement, span)?); + } + Some(Some(ReflectedFieldValidation::Validate(plans))) + } + + fn lower_reflected_requirement( + &mut self, + node: ReflectedFieldRequirementPlan, + span: Span, + ) -> Option { + use ReflectedFieldAction as A; + use ReflectedFieldRequirement as R; + let action = match node.requirement { + R::Exact => A::Exact, + R::Unsupported(reason) => A::Unsupported(reason), + R::Predicates(expected) => A::Predicates(self.reflected_predicate_suffix( + node.requested_type, + &expected, + span, + )?), + R::Refine { base, predicates } => A::Refine { + base: Box::new(self.lower_reflected_requirement(*base, span)?), + predicates: self.reflected_predicate_suffix( + node.requested_type, + &predicates, + span, + )?, + }, + R::List(child) => A::List(Box::new(self.lower_reflected_requirement(*child, span)?)), + R::Set(child) => A::Set(Box::new(self.lower_reflected_requirement(*child, span)?)), + R::Optional(child) => { + A::Optional(Box::new(self.lower_reflected_requirement(*child, span)?)) + } + R::Map { key, value } => A::Map { + key: Box::new(self.lower_reflected_requirement(*key, span)?), + value: Box::new(self.lower_reflected_requirement(*value, span)?), + }, + R::Result { ok, error } => A::Result { + ok: Box::new(self.lower_reflected_requirement(*ok, span)?), + error: Box::new(self.lower_reflected_requirement(*error, span)?), + }, + }; + Some(ReflectedFieldPlan { + source_type: node.source_type, + requested_type: node.requested_type, + action, + }) + } + + fn reflected_predicate_suffix( + &mut self, + requested: TypeId, + expected: &[TypeId], + span: Span, + ) -> Option> { + let mut predicates = self.checked_refinement_predicates(requested, span)?; + predicates.retain(|predicate| expected.contains(&predicate.refined_type)); + if predicates + .iter() + .map(|p| p.refined_type) + .ne(expected.iter().copied()) + { + self.parent + .error(span, "reflected read has no canonical predicate suffix"); + return None; + } + Some(predicates) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_diagnostics::Severity; + + const SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +type Sibling = int64 where value >= 0 +struct Record: + raw: int64 + positive: Positive + higher: Higher + sibling: Sibling +enum Event: + first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling) + second(higher: Higher, raw: int64) +machine Session: + states: + first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling) + second(higher: Higher, raw: int64) + transitions: + first to second +function record_read(view value: Record, view field: TypeField) returns Higher: + return type.field_value[Record, Higher](view value, view field) +function event_read(view value: Event, view field: TypeField) returns Higher: + return type.variant_field_value[Event, Higher](view value, view field) +function session_read(view value: Session, view field: TypeField) returns Higher: + return type.machine_field_value[Session, Higher](view value, view field) +"#; + + fn lower_source() -> (Program, TypeInterner) { + lower_source_text(SOURCE) + } + + fn lower_source_text(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let program = lower(&parsed.module, &resolved, &checked, &origins).expect("reflected HIR"); + (program, checked.interner) + } + + fn read_mut(program: &mut Program) -> &mut Expression { + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "record_read") + .unwrap(); + let StatementKind::Return(Some(value)) = &mut function.body.statements[0].kind else { + panic!("reflected return"); + }; + value + } + + #[test] + fn native_reflected_root_plans_use_declared_slots_and_only_new_suffixes() { + let (program, types) = lower_source(); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for name in ["record_read", "event_read", "session_read"] { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + }, + .. + })) = &function.body.statements[0].kind + else { + panic!("checked reflected plans"); + }; + let layout = reflected_field_layout(&types, *intrinsic, type_arguments[0]).unwrap(); + assert_eq!( + plans.iter().map(|p| p.source_type).collect::>(), + layout.iter().map(|(_, _, ty)| *ty).collect::>() + ); + let expected = if name == "record_read" { + vec![ + vec!["app.Positive", "app.Higher"], + vec!["app.Higher"], + vec![], + vec!["app.Positive", "app.Higher"], + ] + } else { + vec![ + vec!["app.Positive", "app.Higher"], + vec!["app.Higher"], + vec![], + vec!["app.Positive", "app.Higher"], + vec![], + vec!["app.Positive", "app.Higher"], + ] + }; + let actual = plans + .iter() + .map(|p| match &p.action { + ReflectedFieldAction::Exact => Vec::new(), + ReflectedFieldAction::Predicates(chain) => { + chain.iter().map(|p| p.type_name.as_str()).collect() + } + _ => panic!("root scalar is supported"), + }) + .collect::>>(); + assert_eq!(actual, expected, "{name}"); + } + } + + #[test] + fn native_reflected_root_hir_rejects_incomplete_or_forged_proof_plans() { + let (original, types) = lower_source(); + let sibling = original + .functions + .iter() + .find(|f| { + f.identity.declaration.kind == DeclarationKind::RefinementPredicate + && f.identity.declaration.name == "Sibling" + }) + .unwrap() + .id; + for mutation in 0..12 { + let mut program = original.clone(); + let value = read_mut(&mut program); + let ExpressionKind::Intrinsic { + type_arguments, + field_validation, + .. + } = &mut value.kind + else { + panic!("reflected read"); + }; + let Some(ReflectedFieldValidation::Validate(plans)) = field_validation else { + panic!("proof plans"); + }; + match mutation { + 0 => { + plans.pop(); + } + 1 => plans[0].source_type = TypeInterner::BOOL, + 2 => plans[0].action = ReflectedFieldAction::Exact, + 3 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p[0].type_name = "Positive".into(); + } + 4 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p[0].input_type = TypeInterner::STRING; + } + 5 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p[0].function = sibling; + } + 6 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p[0].function = FunctionId::new(u32::MAX); + } + 7 => *field_validation = None, + 8 => type_arguments[1] = TypeInterner::INT64, + 9 => *field_validation = Some(ReflectedFieldValidation::Read), + 10 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p[0].base_type = TypeInterner::BOOL; + } + 11 => { + let ReflectedFieldAction::Predicates(p) = &mut plans[0].action else { + unreachable!(); + }; + p.pop(); + } + _ => unreachable!(), + } + let structural = validate(&program).is_err(); + let typed = validate_backend_types(&program, &types).is_err(); + assert!( + structural || typed, + "forged plan {mutation} escaped validation" + ); + if mutation == 5 || mutation == 6 { + assert!(structural, "wrong predicate identity is structural"); + } + } + } + + #[test] + fn native_reflected_root_metadata_requires_canonical_names_and_used_slot_types() { + for mutation in 0..6 { + let (program, mut types) = lower_source(); + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "record_read") + .unwrap(); + let metadata_type = function.params[1].ty; + assert!(valid_reflected_field_metadata_type(&types, metadata_type)); + let Type::Struct(id) = types.resolve(metadata_type) else { + panic!("TypeField descriptor"); + }; + let id = *id; + let mut definition = types.resolve_struct(id).clone(); + match mutation { + 0 => definition.fields[1].0 = "owner".into(), + 1 => definition.fields[1].1 = TypeInterner::INT64, + 2 => definition.fields[2].1 = types.intern(Type::Optional(TypeInterner::INT64)), + 3 => definition.fields[3].1 = TypeInterner::BOOL, + 4 => { + definition.fields.pop(); + } + 5 => definition.fields[0].1 = TypeInterner::BOOL, + _ => unreachable!(), + } + types.update_struct(id, definition); + assert!( + !valid_reflected_field_metadata_type(&types, metadata_type), + "metadata mutation {mutation}" + ); + } + } + + #[test] + fn producer_nominal_argument_detection_never_infers_arguments_from_payload_fields() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "models.Positive".into(), + base: TypeInterner::INT64, + }); + let element = types.add_struct(jett_types::StructDef { + name: "models.Element".into(), + fields: vec![("value".into(), positive)], + methods: Vec::new(), + }); + let element = types.intern(Type::Struct(element)); + let event = types.add_enum(jett_types::EnumDef { + name: "models.Event".into(), + variants: vec![jett_types::VariantDef { + name: "content".into(), + fields: vec![("value".into(), positive)], + discriminant: 0, + }], + }); + let event = types.intern(Type::Enum(event)); + let machine = types.add_machine(jett_types::MachineDef { + name: "models.Session".into(), + states: vec![jett_types::MachineStateDef { + name: "content".into(), + fields: vec![("value".into(), positive)], + }], + transitions: Vec::new(), + }); + let state = types.intern(Type::MachineState { + machine, + state: jett_types::MachineStateId::new(0), + }); + let machine = types.intern(Type::Machine(machine)); + let bitfield = types.add_bitfield(jett_types::BitfieldDef { + name: "models.Packet".into(), + network_order: false, + fields: vec![jett_types::BitfieldFieldDef { + name: "value".into(), + ty: positive, + kind: jett_types::BitfieldFieldKind::Payload, + }], + }); + let bitfield = types.intern(Type::Bitfield(bitfield)); + for leaf in [element, event, machine, state, bitfield] { + assert!(types.nominal_type_arguments(leaf).is_empty()); + assert!(!contains_refinement(&types, leaf, &mut HashSet::new())); + } + let marker = types.add_struct(jett_types::StructDef { + name: "models.Marker[models.Positive]".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: Vec::new(), + }); + let marker = types.intern(Type::Struct(marker)); + types + .register_nominal_type_arguments(marker, vec![positive]) + .unwrap(); + let plain_marker = types.add_struct(jett_types::StructDef { + name: "models.Marker[int64]".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: Vec::new(), + }); + let plain_marker = types.intern(Type::Struct(plain_marker)); + types + .register_nominal_type_arguments(plain_marker, vec![TypeInterner::INT64]) + .unwrap(); + for (leaf, requires_ready) in [(element, false), (marker, true), (plain_marker, false)] { + let list = types.intern(Type::List(leaf)); + let root = types.intern(Type::Refinement { + name: format!("models.Values{}", leaf.index()), + base: list, + }); + let requirement = reflected_field_requirement(&types, root, list).unwrap(); + if requires_ready { + assert_eq!( + requirement, + ReflectedFieldRequirement::List(Box::new(ReflectedFieldRequirementPlan { + source_type: leaf, + requested_type: leaf, + requirement: ReflectedFieldRequirement::Exact, + })) + ); + } else { + assert_eq!(requirement, ReflectedFieldRequirement::Exact); + } + } + assert_eq!( + reflected_field_requirement(&types, plain_marker, marker), + Some(ReflectedFieldRequirement::Unsupported( + ReflectedFieldUnsupported::CallableOrNominal + )) + ); + } + + fn nominal_leaf_source(leaf: &str) -> String { + format!( + r#" +namespace app +type Positive = int64 where value > 0 +type Alias = Positive +struct Element: + value: Positive +struct Holder[T]: + value: T +struct Unused[T]: + value: int64 +type Values = list[{leaf}] where true +struct Record: + values: Values +enum Event: + content(values: Values) +machine Session: + states: + content(values: Values) + empty + transitions: + content to empty +function record_read(view source: Record, view field: TypeField) returns list[{leaf}]: + return type.field_value[Record, list[{leaf}]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[{leaf}]: + return source into view type.field_value[Record, list[{leaf}]](view field) +function event_read(view source: Event, view field: TypeField) returns list[{leaf}]: + return type.variant_field_value[Event, list[{leaf}]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[{leaf}]: + return source into view type.variant_field_value[Event, list[{leaf}]](view field) +function machine_read(view source: Session, view field: TypeField) returns list[{leaf}]: + return type.machine_field_value[Session, list[{leaf}]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[{leaf}]: + return source into view type.machine_field_value[Session, list[{leaf}]](view field) +"# + ) + } + + #[test] + fn checked_nominal_leaf_plans_preserve_ordinary_proof_and_unused_generic_invariants() { + for (leaf, requires_ready) in [ + ("Element", false), + ("Holder[Positive]", true), + ("Unused[Alias]", true), + ("Unused[int64]", false), + ("Holder[Element]", false), + ] { + let (mut program, types) = lower_source_text(&nominal_leaf_source(leaf)); + validate_backend_types(&program, &types).unwrap(); + for name in [ + "record_read", + "record_pipe", + "event_read", + "event_pipe", + "machine_read", + "machine_pipe", + ] { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + }, + .. + })) = &function.body.statements[0].kind + else { + panic!("{name} nominal leaf plan"); + }; + assert_eq!(plans.len(), 1); + assert!( + plans[0].predicates().is_empty(), + "no proven predicates rerun" + ); + if requires_ready { + let ReflectedFieldAction::List(child) = &plans[0].action else { + panic!("{name}: {leaf} requires readiness"); + }; + assert_eq!(child.action, ReflectedFieldAction::Exact); + assert_eq!(child.source_type, child.requested_type); + } else { + assert_eq!( + plans[0].action, + ReflectedFieldAction::Exact, + "{name}: {leaf} stays exact" + ); + } + } + // Closed IR must reject both adding readiness to a plain nominal + // leaf and deleting it when a declared generic argument is refined. + let read = read_mut(&mut program); + let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut read.kind + else { + panic!("record plan"); + }; + if requires_ready { + plans[0].action = ReflectedFieldAction::Exact; + } else { + let Type::List(child) = types.resolve(plans[0].requested_type) else { + panic!("requested list"); + }; + plans[0].action = ReflectedFieldAction::List(Box::new(ReflectedFieldPlan { + source_type: *child, + requested_type: *child, + action: ReflectedFieldAction::Exact, + })); + } + let errors = + validate_backend_types(&program, &types).expect_err("forged readiness plan"); + assert!( + errors.iter().any(|error| error + .message + .contains("reflected validation action disagrees")), + "{leaf}: {errors:?}" + ); + } + } + + #[test] + fn native_reflected_root_schema_reuses_ancestors_and_keeps_nested_base_proofs() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "app.Positive".into(), + base: TypeInterner::INT64, + }); + let higher = types.intern(Type::Refinement { + name: "app.Higher".into(), + base: positive, + }); + assert_eq!( + reflected_field_requirement(&types, higher, positive), + Some(ReflectedFieldRequirement::Exact) + ); + assert_eq!( + reflected_field_requirement(&types, higher, TypeInterner::INT64), + Some(ReflectedFieldRequirement::Exact) + ); + let sibling_high = types.intern(Type::Refinement { + name: "app.SiblingHigh".into(), + base: positive, + }); + assert_eq!( + reflected_field_requirement(&types, higher, sibling_high), + Some(ReflectedFieldRequirement::Predicates(vec![sibling_high])) + ); + let list = types.intern(Type::List(positive)); + let nonempty = types.intern(Type::Refinement { + name: "app.Nonempty".into(), + base: list, + }); + assert_eq!( + reflected_field_requirement(&types, list, nonempty), + Some(ReflectedFieldRequirement::Predicates(vec![nonempty])) + ); + assert_eq!( + reflected_field_requirement(&types, nonempty, list), + Some(ReflectedFieldRequirement::List(Box::new( + ReflectedFieldRequirementPlan { + source_type: positive, + requested_type: positive, + requirement: ReflectedFieldRequirement::Exact, + } + ))) + ); + let plain_list = types.intern(Type::List(TypeInterner::INT64)); + assert_eq!( + reflected_field_requirement(&types, plain_list, nonempty), + Some(ReflectedFieldRequirement::Refine { + base: Box::new(ReflectedFieldRequirementPlan { + source_type: plain_list, + requested_type: list, + requirement: ReflectedFieldRequirement::List(Box::new( + ReflectedFieldRequirementPlan { + source_type: TypeInterner::INT64, + requested_type: positive, + requirement: ReflectedFieldRequirement::Predicates(vec![positive]), + } + )), + }), + predicates: vec![nonempty], + }) + ); + let secret = types.intern(Type::Secret(positive)); + let hidden = types.intern(Type::Refinement { + name: "app.Hidden".into(), + base: secret, + }); + assert_eq!( + reflected_field_requirement(&types, secret, hidden), + Some(ReflectedFieldRequirement::Predicates(vec![hidden])) + ); + assert_eq!( + reflected_field_requirement(&types, hidden, secret), + Some(ReflectedFieldRequirement::Exact) + ); + let plain_secret = types.intern(Type::Secret(TypeInterner::INT64)); + assert_eq!( + reflected_field_requirement(&types, plain_secret, hidden), + Some(ReflectedFieldRequirement::Unsupported( + ReflectedFieldUnsupported::Secret + )) + ); + let callback = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: positive, + }); + let plain_callback = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + assert_eq!( + reflected_field_requirement(&types, plain_callback, callback), + Some(ReflectedFieldRequirement::Unsupported( + ReflectedFieldUnsupported::CallableOrNominal + )) + ); + } + + fn requirement_children( + requirement: &ReflectedFieldRequirement, + ) -> Vec<&ReflectedFieldRequirementPlan> { + match requirement { + ReflectedFieldRequirement::List(child) + | ReflectedFieldRequirement::Set(child) + | ReflectedFieldRequirement::Optional(child) => vec![child], + ReflectedFieldRequirement::Map { key, value } => vec![key, value], + ReflectedFieldRequirement::Result { ok, error } => vec![ok, error], + _ => panic!("changed builtin wrapper"), + } + } + + fn requirement_wrapper(types: &mut TypeInterner, kind: usize, child: TypeId) -> TypeId { + types.intern(match kind { + 0 => Type::List(child), + 1 => Type::Set(child), + 2 => Type::Map(child, child), + 3 => Type::Optional(child), + 4 => Type::Result(child, child), + _ => panic!("test wrapper kind"), + }) + } + + #[test] + fn native_reflected_recursive_requirements_keep_wrapper_readiness_and_leaf_provenance() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "app.Positive".into(), + base: TypeInterner::INT64, + }); + let higher = types.intern(Type::Refinement { + name: "app.Higher".into(), + base: positive, + }); + let sibling = types.intern(Type::Refinement { + name: "app.Sibling".into(), + base: TypeInterner::INT64, + }); + for kind in 0..5 { + let requested = requirement_wrapper(&mut types, kind, positive); + assert_eq!( + reflected_field_requirement(&types, requested, requested), + Some(ReflectedFieldRequirement::Exact) + ); + for (source, expected) in [ + (higher, ReflectedFieldRequirement::Exact), + ( + sibling, + ReflectedFieldRequirement::Predicates(vec![positive]), + ), + ( + TypeInterner::INT64, + ReflectedFieldRequirement::Predicates(vec![positive]), + ), + ] { + let actual = requirement_wrapper(&mut types, kind, source); + let requirement = reflected_field_requirement(&types, actual, requested).unwrap(); + for child in requirement_children(&requirement) { + assert_eq!(child.source_type, source); + assert_eq!(child.requested_type, positive); + assert_eq!(child.requirement, expected); + } + } + } + let plain_secret = types.intern(Type::Secret(TypeInterner::INT64)); + let refined_secret = types.intern(Type::Secret(positive)); + let actual = types.intern(Type::Optional(plain_secret)); + let requested = types.intern(Type::Optional(refined_secret)); + let requirement = reflected_field_requirement(&types, actual, requested).unwrap(); + assert_eq!( + requirement_children(&requirement)[0].requirement, + ReflectedFieldRequirement::Unsupported(ReflectedFieldUnsupported::Secret) + ); + let plain_callable = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: TypeInterner::INT64, + }); + let refined_callable = types.intern(Type::Function { + params: vec![], + view_params: vec![], + return_type: positive, + }); + let actual = types.intern(Type::List(plain_callable)); + let requested = types.intern(Type::List(refined_callable)); + let requirement = reflected_field_requirement(&types, actual, requested).unwrap(); + assert_eq!( + requirement_children(&requirement)[0].requirement, + ReflectedFieldRequirement::Unsupported(ReflectedFieldUnsupported::CallableOrNominal) + ); + } + + #[test] + fn native_reflected_root_to_builtin_base_checks_readiness_without_repeating_child_proofs() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "app.Positive".into(), + base: TypeInterner::INT64, + }); + for kind in 0..5 { + let base = requirement_wrapper(&mut types, kind, positive); + let named = types.intern(Type::Refinement { + name: format!("app.Wrapper{kind}"), + base, + }); + let higher = types.intern(Type::Refinement { + name: format!("app.HigherWrapper{kind}"), + base: named, + }); + let sibling = types.intern(Type::Refinement { + name: format!("app.SiblingWrapper{kind}"), + base: named, + }); + let independent = types.intern(Type::Refinement { + name: format!("app.IndependentWrapper{kind}"), + base, + }); + let plain = requirement_wrapper(&mut types, kind, TypeInterner::INT64); + // Plan shape is independent of runtime pending depth. Exact named + // and generic requests and no-refinement requests skip preflight, + // retaining the original pending owner rather than joining it. + for (actual, requested) in [ + (named, named), + (higher, named), + (base, base), + (named, plain), + ] { + assert_eq!( + reflected_field_requirement(&types, actual, requested), + Some(ReflectedFieldRequirement::Exact) + ); + } + assert_eq!( + reflected_field_requirement(&types, named, higher), + Some(ReflectedFieldRequirement::Predicates(vec![higher])) + ); + assert_eq!( + reflected_field_requirement(&types, higher, sibling), + Some(ReflectedFieldRequirement::Predicates(vec![sibling])) + ); + for actual in [named, higher] { + let ReflectedFieldRequirement::Refine { + base: ready, + predicates, + } = reflected_field_requirement(&types, actual, independent).unwrap() + else { + panic!("new root must preflight generic base"); + }; + assert_eq!(ready.source_type, actual); + assert_eq!(ready.requested_type, base); + assert_eq!(predicates, [independent]); + for child in requirement_children(&ready.requirement) { + assert_eq!(child.requirement, ReflectedFieldRequirement::Exact); + } + let requirement = reflected_field_requirement(&types, actual, base).unwrap(); + for child in requirement_children(&requirement) { + assert_eq!(child.source_type, positive); + assert_eq!(child.requested_type, positive); + assert_eq!(child.requirement, ReflectedFieldRequirement::Exact); + } + } + } + } + + const RECURSIVE_SOURCE: &str = r#"namespace app + type Positive = int64 where value > 0 + type Higher = Positive where value > 10 + type Sibling = int64 where value >= 0 + type Text = string where value != "" + function is_nonempty(view values: list[Positive]) returns bool: + for item in view values: + return true + return false + type Nonempty = list[Positive] where is_nonempty(view value) + type Larger = Nonempty where true + type AlternateLarger = Nonempty where true + type OtherValues = list[Positive] where true + struct RecursiveRecord: + raw: list[optional[result[map[int64, list[int64]], set[int64]]]] + known: list[optional[result[map[Higher, list[Higher]], set[Higher]]]] + enum RecursiveEvent: + content(raw: list[optional[result[map[int64, list[int64]], set[int64]]]]) + known(value: list[optional[result[map[Higher, list[Higher]], set[Higher]]]]) + machine RecursiveSession: + states: + content(raw: list[optional[result[map[int64, list[int64]], set[int64]]]]) + known(value: list[optional[result[map[Higher, list[Higher]], set[Higher]]]]) + transitions: + content to known + struct TextRecord: + raw: optional[string] + struct RootRecord: + raw: list[int64] + struct ProvenRootRecord: + established: Nonempty + struct HigherRootRecord: + established: Larger + function record_read(view value: RecursiveRecord, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return type.field_value[RecursiveRecord, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view value, view field) + function record_pipe(view value: RecursiveRecord, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return value into view type.field_value[RecursiveRecord, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) + function event_read(view value: RecursiveEvent, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return type.variant_field_value[RecursiveEvent, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view value, view field) + function event_pipe(view value: RecursiveEvent, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return value into view type.variant_field_value[RecursiveEvent, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) + function session_read(view value: RecursiveSession, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return type.machine_field_value[RecursiveSession, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view value, view field) + function session_pipe(view value: RecursiveSession, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return value into view type.machine_field_value[RecursiveSession, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) + function text_read(view value: TextRecord, view field: TypeField) returns optional[Text]: + return type.field_value[TextRecord, optional[Text]](view value, view field) + function root_read(view value: RootRecord, view field: TypeField) returns Nonempty: + return type.field_value[RootRecord, Nonempty](view value, view field) + function named_root_read(view value: ProvenRootRecord, view field: TypeField) returns Nonempty: + return type.field_value[ProvenRootRecord, Nonempty](view value, view field) + function generic_root_base_read(view value: ProvenRootRecord, view field: TypeField) returns list[Positive]: + return type.field_value[ProvenRootRecord, list[Positive]](view value, view field) + function plain_root_base_read(view value: ProvenRootRecord, view field: TypeField) returns list[int64]: + return type.field_value[ProvenRootRecord, list[int64]](view value, view field) + function promoted_root_read(view value: ProvenRootRecord, view field: TypeField) returns Larger: + return type.field_value[ProvenRootRecord, Larger](view value, view field) + function independent_root_read(view value: ProvenRootRecord, view field: TypeField) returns OtherValues: + return type.field_value[ProvenRootRecord, OtherValues](view value, view field) + function shared_root_read(view value: HigherRootRecord, view field: TypeField) returns AlternateLarger: + return type.field_value[HigherRootRecord, AlternateLarger](view value, view field) +"#; + + fn recursive_source() -> String { + // Keep this long type matrix readable without changing Jett indentation. + RECURSIVE_SOURCE + .lines() + .map(|line| line.strip_prefix(' ').unwrap_or(line)) + .collect::>() + .join("\n") + } + + fn plans(value: &Expression) -> &[ReflectedFieldPlan] { + let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &value.kind + else { + panic!("reflected plans"); + }; + plans + } + + #[test] + fn native_reflected_recursive_plans_preserve_builtin_shapes_declared_proofs_and_call_forms() { + let (program, types) = lower_source_text(&recursive_source()); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for prefix in ["record", "event", "session"] { + let direct = plans(returned_read(&program, &format!("{prefix}_read"))); + let piped = plans(returned_read(&program, &format!("{prefix}_pipe"))); + assert_eq!(direct, piped); + let ReflectedFieldAction::List(optional) = &direct[0].action else { + panic!("list"); + }; + let ReflectedFieldAction::Optional(result) = &optional.action else { + panic!("optional"); + }; + let ReflectedFieldAction::Result { ok, error } = &result.action else { + panic!("result"); + }; + let ReflectedFieldAction::Map { key, value } = &ok.action else { + panic!("map"); + }; + assert!(matches!(key.action, ReflectedFieldAction::Predicates(_))); + assert!(matches!(value.action, ReflectedFieldAction::List(_))); + assert!(matches!(error.action, ReflectedFieldAction::Set(_))); + assert_eq!( + direct[0] + .predicates() + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.Positive", "app.Positive", "app.Positive"] + ); + // A changed whole wrapper retains readiness even when all leaves + // are declared Higher values read through established Positive. + assert!(matches!(direct[1].action, ReflectedFieldAction::List(_))); + assert!(direct[1].predicates().is_empty()); + } + let named = &plans(returned_read(&program, "named_root_read"))[0]; + let generic = &plans(returned_read(&program, "generic_root_base_read"))[0]; + let plain = &plans(returned_read(&program, "plain_root_base_read"))[0]; + assert!(matches!(named.action, ReflectedFieldAction::Exact)); + assert!(matches!(plain.action, ReflectedFieldAction::Exact)); + let ReflectedFieldAction::List(child) = &generic.action else { + panic!("generic base readiness"); + }; + assert!(matches!(child.action, ReflectedFieldAction::Exact)); + assert!(generic.predicates().is_empty()); + let promoted = &plans(returned_read(&program, "promoted_root_read"))[0]; + let shared = &plans(returned_read(&program, "shared_root_read"))[0]; + assert!(matches!( + promoted.action, + ReflectedFieldAction::Predicates(_) + )); + assert_eq!( + promoted + .predicates() + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.Larger"] + ); + assert!(matches!(shared.action, ReflectedFieldAction::Predicates(_))); + assert_eq!( + shared + .predicates() + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.AlternateLarger"] + ); + let independent = &plans(returned_read(&program, "independent_root_read"))[0]; + let ReflectedFieldAction::Refine { base, predicates } = &independent.action else { + panic!("sibling base readiness"); + }; + assert_eq!(base.source_type, named.source_type); + let ReflectedFieldAction::List(child) = &base.action else { + panic!("base list"); + }; + assert!(matches!(child.action, ReflectedFieldAction::Exact)); + assert_eq!(predicates[0].type_name, "app.OtherValues"); + assert_eq!(independent.predicates().len(), 1); + let text = &plans(returned_read(&program, "text_read"))[0]; + assert!(matches!(text.action, ReflectedFieldAction::Optional(_))); + assert_eq!(text.predicates()[0].input_type, TypeInterner::STRING); + let root = &plans(returned_read(&program, "root_read"))[0]; + let ReflectedFieldAction::Refine { base, predicates } = &root.action else { + panic!("base-first root"); + }; + assert!(matches!(base.action, ReflectedFieldAction::List(_))); + assert_eq!(predicates[0].type_name, "app.Nonempty"); + assert_eq!( + root.predicates() + .iter() + .map(|p| p.type_name.as_str()) + .collect::>(), + ["app.Positive", "app.Nonempty"] + ); + } + + fn recursive_key_mut(plan: &mut ReflectedFieldPlan) -> &mut ReflectedFieldPlan { + let ReflectedFieldAction::List(optional) = &mut plan.action else { + panic!("list"); + }; + let ReflectedFieldAction::Optional(result) = &mut optional.action else { + panic!("optional"); + }; + let ReflectedFieldAction::Result { ok, .. } = &mut result.action else { + panic!("result"); + }; + let ReflectedFieldAction::Map { key, .. } = &mut ok.action else { + panic!("map"); + }; + key + } + + #[test] + fn native_reflected_recursive_hir_rejects_forged_child_schemas_and_canonical_predicates() { + let (original, types) = lower_source_text(&recursive_source()); + let sibling = original + .functions + .iter() + .find(|f| { + f.identity.declaration.kind == DeclarationKind::RefinementPredicate + && f.identity.declaration.name == "Sibling" + }) + .unwrap() + .id; + let mut foreign = TypeInterner::new(); + let mut invalid = TypeInterner::INT64; + for index in 0..=types.len() { + invalid = foreign.intern(Type::Refinement { + name: format!("app.UncheckedForeignType{index}"), + base: TypeInterner::INT64, + }); + } + assert!(invalid.index() as usize >= types.len()); + for mutation in 0..12 { + let mut program = original.clone(); + let value = read_mut(&mut program); + let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut value.kind + else { + panic!("plans"); + }; + if mutation == 0 { + plans[0].requested_type = invalid; + } else if mutation == 1 { + let ReflectedFieldAction::List(child) = &plans[0].action else { + panic!("list"); + }; + plans[0].action = ReflectedFieldAction::Optional(child.clone()); + } else { + let key = recursive_key_mut(&mut plans[0]); + match mutation { + 2 => key.source_type = TypeInterner::STRING, + 3 => key.requested_type = invalid, + 4 => key.action = ReflectedFieldAction::Exact, + _ => { + let ReflectedFieldAction::Predicates(predicates) = &mut key.action else { + panic!("leaf"); + }; + match mutation { + 5 => predicates[0].function = sibling, + 6 => predicates[0].function = FunctionId::new(u32::MAX), + 7 => predicates[0].refined_type = invalid, + 8 => predicates[0].base_type = TypeInterner::STRING, + 9 => predicates[0].input_type = TypeInterner::STRING, + 10 => predicates.clear(), + 11 => predicates.push(predicates[0].clone()), + _ => unreachable!(), + } + } + } + } + let structural = validate(&program).is_err(); + let typed = validate_backend_types(&program, &types).is_err(); + assert!(structural || typed, "accepted child mutation {mutation}"); + if matches!(mutation, 5 | 6) { + assert!(structural, "recursive function identity is checked"); + } else { + assert!(typed, "recursive checked schema is authoritative"); + } + } + } + + #[test] + fn native_reflected_refine_base_plan_retains_declared_source_identity() { + let (mut program, types) = lower_source_text(&recursive_source()); + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "independent_root_read") + .unwrap(); + let StatementKind::Return(Some(value)) = &mut function.body.statements[0].kind else { + panic!("root return"); + }; + let ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } = &mut value.kind + else { + panic!("reflected root plan"); + }; + let ReflectedFieldAction::Refine { base, .. } = &mut plans[0].action else { + panic!("independent root base"); + }; + assert_ne!(base.source_type, base.requested_type); + // Forged erasure must not transform structural readiness into exact + // whole-schema proof or discard the original declared producer owner. + base.source_type = base.requested_type; + assert!(validate_backend_types(&program, &types).is_err()); + } + + fn returned_read<'a>(program: &'a Program, name: &str) -> &'a Expression { + let function = program + .functions + .iter() + .find(|f| f.identity.declaration.name == name) + .unwrap(); + let StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("reflected return for {name}"); + }; + value + } + + fn metadata_summary( + args: &[Expression], + ) -> Vec<(i128, String, Option, String, String)> { + args[2..] + .iter() + .map(|value| { + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + } = &value.kind + else { + panic!("canonical hidden TypeField"); + }; + assert_eq!(*struct_type, args[1].ty); + assert_eq!(value.ty, args[1].ty); + assert_eq!(fields.len(), 10); + assert_eq!(*evaluation_order, (0..10).collect::>()); + assert!(refinement_predicates.is_empty()); + let ExpressionKind::Int(index) = fields[0].kind else { + panic!("checked field index"); + }; + let string = |field: &Expression| { + let ExpressionKind::String(value) = &field.kind else { + panic!("checked metadata string"); + }; + value.clone() + }; + let member = match &fields[2].kind { + ExpressionKind::OptionalNone => None, + ExpressionKind::OptionalSome(value) => Some(string(value)), + _ => panic!("checked owner member"), + }; + ( + index, + string(&fields[1]), + member, + string(&fields[3]), + string(&fields[4]), + ) + }) + .collect() + } + + #[test] + fn native_reflected_getter_pipelines_preserve_metadata_and_root_plans() { + let source = format!( + "{SOURCE}{}", + r#" +function record_pipe(view value: Record, view field: TypeField) returns Higher: + return value into view type.field_value[Record, Higher](view field) +function event_pipe(view value: Event, view field: TypeField) returns Higher: + return value into view type.variant_field_value[Event, Higher](view field) +function session_pipe(view value: Session, view field: TypeField) returns Higher: + return value into view type.machine_field_value[Session, Higher](view field) +"# + ); + let (program, types) = lower_source_text(&source); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for (direct, pipe, owner, members) in [ + ("record_read", "record_pipe", "app.Record", vec![None]), + ( + "event_read", + "event_pipe", + "app.Event", + vec![Some("first"), Some("second")], + ), + ( + "session_read", + "session_pipe", + "app.Session", + vec![Some("first"), Some("second")], + ), + ] { + let ExpressionKind::Intrinsic { + intrinsic: direct_id, + type_arguments: direct_types, + reflection_arguments: direct_reflection, + field_validation: direct_validation, + args: direct_args, + .. + } = &returned_read(&program, direct).kind + else { + panic!("direct read"); + }; + let ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + field_validation, + args, + evaluation_order, + .. + } = &returned_read(&program, pipe).kind + else { + panic!("piped read"); + }; + assert_eq!(intrinsic, direct_id); + assert_eq!(type_arguments, direct_types); + assert_eq!(reflection_arguments, direct_reflection); + assert_eq!(field_validation, direct_validation); + assert_eq!(*evaluation_order, (0..args.len()).collect::>()); + for (index, arg) in args[..2].iter().enumerate() { + let ExpressionKind::View(value) = &arg.kind else { + panic!("view operand {index}"); + }; + assert!( + matches!(value.kind, ExpressionKind::Local(local) if local == LocalId::new(index as u32)) + ); + } + let expected = members + .into_iter() + .flat_map(|member| { + let fields = if member == Some("second") { + vec![(0, "higher", "app.Higher"), (1, "raw", "int64")] + } else { + vec![ + (0, "raw", "int64"), + (1, "positive", "app.Positive"), + (2, "higher", "app.Higher"), + (3, "sibling", "app.Sibling"), + ] + }; + fields.into_iter().map(move |(index, name, ty)| { + ( + index, + owner.to_string(), + member.map(str::to_string), + name.to_string(), + ty.to_string(), + ) + }) + }) + .collect::>(); + assert_eq!(metadata_summary(args), expected, "{pipe}"); + assert_eq!(metadata_summary(direct_args), expected, "{direct}"); + } + } + + #[test] + fn native_reflected_getter_pipelines_keep_bitfield_and_narrowed_machine_owners() { + let source = format!( + "{SOURCE}{}", + r#" +bitfield Header: + first: 4 bits + second: 8 bits +function header_read(view value: Header, view field: TypeField) returns int64: + return type.field_value[Header, int64](view value, view field) +function header_pipe(view value: Header, view field: TypeField) returns int64: + return value into view type.field_value[Header, int64](view field) +function narrowed_read(view value: Session at first, view field: TypeField) returns Higher: + return type.machine_field_value[Session at first, Higher](view value, view field) +function narrowed_pipe(view value: Session at first, view field: TypeField) returns Higher: + return value into view type.machine_field_value[Session at first, Higher](view field) +"# + ); + let (program, types) = lower_source_text(&source); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for (direct, pipe, owner, count) in [ + ("header_read", "header_pipe", "app.Header", 2), + ("narrowed_read", "narrowed_pipe", "app.Session", 6), + ] { + let ExpressionKind::Intrinsic { + type_arguments: direct_types, + field_validation: direct_validation, + args: direct_args, + .. + } = &returned_read(&program, direct).kind + else { + panic!("direct read"); + }; + let ExpressionKind::Intrinsic { + type_arguments, + field_validation, + args, + evaluation_order, + .. + } = &returned_read(&program, pipe).kind + else { + panic!("piped read"); + }; + assert_eq!(type_arguments, direct_types); + assert_eq!(field_validation, direct_validation); + assert_eq!(*evaluation_order, (0..args.len()).collect::>()); + let summary = metadata_summary(args); + assert_eq!(summary.len(), count); + assert!(summary.iter().all(|(_, actual, _, _, _)| actual == owner)); + assert_eq!(summary, metadata_summary(direct_args)); + if pipe == "header_pipe" { + assert!( + summary + .iter() + .all(|(_, _, member, _, ty)| member.is_none() && ty == "int64") + ); + let Some(ReflectedFieldValidation::Validate(plans)) = field_validation else { + panic!("primitive plan"); + }; + assert!( + plans + .iter() + .all(|plan| plan.action == ReflectedFieldAction::Exact) + ); + } else { + assert_eq!(summary[0].2.as_deref(), Some("first")); + assert_eq!(summary[4].2.as_deref(), Some("second")); + } + } + } + + #[test] + fn native_reflected_getter_pipeline_stages_source_then_selector_and_feeds_next_call() { + let source = format!( + "{SOURCE}{}", + r#" +function source_operand(view value: Record) returns Record: + return clone value +function field_operand(view field: TypeField) returns TypeField: + return clone field +function observe(value: Higher) returns int64: + return coarsen value +function pipeline_operands(view value: Record, view field: TypeField) returns int64: + return source_operand(view value) into view type.field_value[Record, Higher](view field_operand(view field)) into observe +"# + ); + let (program, types) = lower_source_text(&source); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + let ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } = &returned_read(&program, "pipeline_operands").kind + else { + panic!("next source call"); + }; + assert_eq!( + program + .functions + .iter() + .find(|f| f.id == *function) + .unwrap() + .identity + .declaration + .name, + "observe" + ); + assert_eq!(evaluation_order, &[0]); + let ExpressionKind::Intrinsic { + type_arguments, + args, + evaluation_order, + field_validation: Some(ReflectedFieldValidation::Validate(_)), + .. + } = &args[0].kind + else { + panic!("piped getter input"); + }; + assert_eq!(args.len(), 6); + assert_eq!(*evaluation_order, (0..6).collect::>()); + for (arg, expected) in args[..2].iter().zip(["source_operand", "field_operand"]) { + let ExpressionKind::View(inner) = &arg.kind else { + panic!("borrowed temporary"); + }; + let ExpressionKind::Call { function, .. } = &inner.kind else { + panic!("operand expression"); + }; + assert_eq!( + program + .functions + .iter() + .find(|f| f.id == *function) + .unwrap() + .identity + .declaration + .name, + expected + ); + } + let Type::Refinement { name, .. } = types.resolve(type_arguments[1]) else { + panic!("requested checked result"); + }; + assert_eq!(name, "app.Higher"); + } + + const VALUE_OBSERVERS: &str = r#"namespace app +struct Holder[T]: + value: T +type Positive = int64 where value > 0 +enum Event: + empty = 17 + content(text: string, values: list[optional[int64]]) +machine Session: + states: + empty + content(text: string, values: list[optional[int64]]) + transitions: + empty to content +function variant_direct(view source: Event) returns TypeVariant: + return type.variant_value[Event](view source) +function variant_pipe(view source: Event) returns TypeVariant: + return source into view type.variant_value[Event]() +function state_direct(view source: Session) returns TypeMachineState: + return type.machine_state_value[Session](view source) +function state_pipe(view source: Session) returns TypeMachineState: + return source into view type.machine_state_value[Session]() +function narrowed_direct(view source: Session at content) returns TypeMachineState: + return type.machine_state_value[Session at content](view source) +function narrowed_pipe(view source: Session at content) returns TypeMachineState: + return source into view type.machine_state_value[Session at content]() +function map_direct(index: int64) returns TypeInfo: + return type.arg[map[string, list[optional[int64]]]](index) +function map_pipe(index: int64) returns TypeInfo: + return index into type.arg[map[string, list[optional[int64]]]]() +function holder_direct(index: int64) returns TypeInfo: + return type.arg[Holder[list[optional[int64]]]](index) +function holder_pipe(index: int64) returns TypeInfo: + return index into type.arg[Holder[list[optional[int64]]]]() +function refinement_direct(index: int64) returns TypeInfo: + return type.arg[Positive](index) +function refinement_pipe(index: int64) returns TypeInfo: + return index into type.arg[Positive]() +function primitive_direct(index: int64) returns TypeInfo: + return type.arg[int64](index) +function primitive_pipe(index: int64) returns TypeInfo: + return index into type.arg[int64]() +"#; + + #[derive(Debug, PartialEq, Eq)] + enum MetadataKind { + Int(i128), + String(String), + Bool(bool), + Struct(TypeId, Vec), + Enum(TypeId, VariantId, Vec), + List(Vec), + Some(Box), + None, + } + + #[derive(Debug, PartialEq, Eq)] + struct MetadataValue { + ty: TypeId, + kind: MetadataKind, + } + + // Compare complete recursive metadata without source spans. In particular, + // this catches an input type being substituted for a pipeline result type. + fn metadata_value(value: &Expression) -> MetadataValue { + let kind = match &value.kind { + ExpressionKind::Int(value) => MetadataKind::Int(*value), + ExpressionKind::String(value) => MetadataKind::String(value.clone()), + ExpressionKind::Bool(value) => MetadataKind::Bool(*value), + ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + } => { + assert!(!validates_refinements); + assert!(refinement_predicates.is_empty()); + assert_eq!(*evaluation_order, (0..fields.len()).collect::>()); + MetadataKind::Struct(*struct_type, fields.iter().map(metadata_value).collect()) + } + ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } => { + assert_eq!(*evaluation_order, (0..payloads.len()).collect::>()); + MetadataKind::Enum( + *enum_type, + *variant, + payloads.iter().map(metadata_value).collect(), + ) + } + ExpressionKind::ListConstruct { elements } => { + MetadataKind::List(elements.iter().map(metadata_value).collect()) + } + ExpressionKind::OptionalSome(value) => { + MetadataKind::Some(Box::new(metadata_value(value))) + } + ExpressionKind::OptionalNone => MetadataKind::None, + other => panic!("non-metadata expression: {other:?}"), + }; + MetadataValue { ty: value.ty, kind } + } + + fn metadata_fields(value: &Expression) -> &[Expression] { + let ExpressionKind::StructConstruct { fields, .. } = &value.kind else { + panic!("checked metadata struct"); + }; + fields + } + + fn metadata_text(value: &Expression) -> &str { + let ExpressionKind::String(value) = &value.kind else { + panic!("checked metadata text"); + }; + value + } + + fn assert_value_metadata_parity(program: &Program, direct: &str, pipe: &str, count: usize) { + let direct = returned_read(program, direct); + let pipe = returned_read(program, pipe); + let ExpressionKind::Intrinsic { + intrinsic: direct_id, + type_arguments: direct_types, + reflection_arguments: direct_reflection, + args: direct_args, + field_validation: None, + .. + } = &direct.kind + else { + panic!("direct value observer"); + }; + let ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + args, + evaluation_order, + field_validation: None, + .. + } = &pipe.kind + else { + panic!("pipeline value observer"); + }; + assert_eq!(pipe.ty, direct.ty); + assert_eq!(intrinsic, direct_id); + assert_eq!(type_arguments, direct_types); + assert_eq!(reflection_arguments, direct_reflection); + assert_eq!(args[0].ty, direct_args[0].ty); + assert_eq!(args.len(), count + 1); + assert_eq!(*evaluation_order, (0..args.len()).collect::>()); + assert!(args[1..].iter().all(|arg| arg.ty == pipe.ty)); + assert_eq!( + args[1..].iter().map(metadata_value).collect::>(), + direct_args[1..] + .iter() + .map(metadata_value) + .collect::>() + ); + } + + #[test] + fn native_reflected_value_pipelines_match_complete_direct_metadata() { + let (program, types) = lower_source_text(VALUE_OBSERVERS); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for (direct, pipe, count) in [ + ("variant_direct", "variant_pipe", 2), + ("state_direct", "state_pipe", 2), + ("narrowed_direct", "narrowed_pipe", 2), + ("map_direct", "map_pipe", 2), + ("holder_direct", "holder_pipe", 1), + ("refinement_direct", "refinement_pipe", 1), + ("primitive_direct", "primitive_pipe", 0), + ] { + assert_value_metadata_parity(&program, direct, pipe, count); + } + for (name, expected) in [ + ("map_pipe", vec!["string", "list[optional[int64]]"]), + ("holder_pipe", vec!["list[optional[int64]]"]), + ("refinement_pipe", vec!["int64"]), + ] { + let ExpressionKind::Intrinsic { args, .. } = &returned_read(&program, name).kind else { + panic!("type.arg"); + }; + assert_eq!( + args[1..] + .iter() + .map(|arg| metadata_text(&metadata_fields(arg)[0])) + .collect::>(), + expected + ); + } + } + + #[test] + fn native_reflected_value_pipeline_metadata_keeps_owner_members_and_all_narrowed_states() { + let (program, types) = lower_source_text(VALUE_OBSERVERS); + for (name, owner, fields_index) in [ + ("variant_pipe", "app.Event", 5), + ("state_pipe", "app.Session", 4), + ("narrowed_pipe", "app.Session", 4), + ] { + let ExpressionKind::Intrinsic { + type_arguments, + args, + .. + } = &returned_read(&program, name).kind + else { + panic!("value observer"); + }; + if name == "narrowed_pipe" { + assert!(matches!( + types.resolve(type_arguments[0]), + Type::MachineState { .. } + )); + assert_eq!(args[0].ty, type_arguments[0]); + } + for (index, (metadata, member)) in + args[1..].iter().zip(["empty", "content"]).enumerate() + { + let fields = metadata_fields(metadata); + assert!( + matches!(fields[0].kind, ExpressionKind::Int(actual) if actual == index as i128) + ); + assert_eq!(metadata_text(&fields[1]), owner); + assert_eq!(metadata_text(&fields[2]), member); + if name == "variant_pipe" { + assert!( + matches!(fields[3].kind, ExpressionKind::Int(actual) if actual == 17 + index as i128) + ); + } + let ExpressionKind::ListConstruct { elements } = &fields[fields_index].kind else { + panic!("payload TypeField list"); + }; + assert_eq!(elements.len(), index * 2); + for (field_index, (field, field_name)) in + elements.iter().zip(["text", "values"]).enumerate() + { + let slots = metadata_fields(field); + assert!( + matches!(slots[0].kind, ExpressionKind::Int(actual) if actual == field_index as i128) + ); + assert_eq!(metadata_text(&slots[1]), owner); + let ExpressionKind::OptionalSome(actual_member) = &slots[2].kind else { + panic!("declared owner member"); + }; + assert_eq!(metadata_text(actual_member), member); + assert_eq!(metadata_text(&slots[3]), field_name); + } + } + } + } + + #[test] + fn native_reflected_value_pipeline_stages_one_operand_and_feeds_the_next_call() { + let source = format!( + "{VALUE_OBSERVERS}{}", + r#" +function source_operand(view source: Event) returns Event: + return clone source +function state_operand(view source: Session at content) returns Session at content: + return clone source +function index_operand(index: int64) returns int64: + return index +function consume_variant(value: TypeVariant) returns string: + return value.name +function consume_state(value: TypeMachineState) returns string: + return value.name +function consume_info(value: TypeInfo) returns string: + return value.type_name +function variant_order(view source: Event) returns string: + return source_operand(view source) into view type.variant_value[Event]() into consume_variant +function state_order(view source: Session at content) returns string: + return state_operand(view source) into view type.machine_state_value[Session at content]() into consume_state +function arg_order(index: int64) returns string: + return index_operand(index) into type.arg[map[string, list[optional[int64]]]]() into consume_info +"# + ); + let (program, types) = lower_source_text(&source); + validate(&program).unwrap(); + validate_backend_types(&program, &types).unwrap(); + for (name, producer, consumer, borrowed) in [ + ("variant_order", "source_operand", "consume_variant", true), + ("state_order", "state_operand", "consume_state", true), + ("arg_order", "index_operand", "consume_info", false), + ] { + let ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } = &returned_read(&program, name).kind + else { + panic!("following call"); + }; + assert_eq!( + program.functions[function.index() as usize] + .identity + .declaration + .name, + consumer + ); + assert_eq!(evaluation_order, &[0]); + let ExpressionKind::Intrinsic { + args, + evaluation_order, + .. + } = &args[0].kind + else { + panic!("observed pipeline input"); + }; + assert_eq!(*evaluation_order, (0..args.len()).collect::>()); + let input = if borrowed { + let ExpressionKind::View(value) = &args[0].kind else { + panic!("borrowed source temporary"); + }; + value.as_ref() + } else { + &args[0] + }; + let ExpressionKind::Call { function, .. } = &input.kind else { + panic!("one source operand call"); + }; + assert_eq!( + program.functions[function.index() as usize] + .identity + .declaration + .name, + producer + ); + for metadata in &args[1..] { + metadata_value(metadata); + } + } + } +} diff --git a/crates/jett_hir/src/required_actor_owners.rs b/crates/jett_hir/src/required_actor_owners.rs new file mode 100644 index 00000000..9081e2db --- /dev/null +++ b/crates/jett_hir/src/required_actor_owners.rs @@ -0,0 +1,122 @@ +//! Exact original ActorDef/ordinal joins for checked required-value metadata. +use super::*; +use jett_parser::ast::ActorDef; +use jett_resolve::{DefId, DefInfo, DefKind}; +use jett_types::ActorId; + +fn original_actor<'a>( + checked: &'a CheckedResourceProgram, + definition: DefId, +) -> Option<(&'a ActorDef, &'a DefInfo, TypeId, ActorId)> { + let declared = checked + .resolved() + .scope_table + .definitions + .get(definition.index() as usize)?; + if declared.id != definition || declared.kind != DefKind::Actor { + return None; + } + let ty = *checked.checked().definition_types.get(&definition)?; + let Type::Actor(actor_id) = checked.checked().interner.resolve(ty) else { + return None; + }; + let originals = checked + .module() + .items + .iter() + .filter_map(|item| match item { + Item::Actor(actor) if actor.name.span == declared.span => Some(actor), + _ => None, + }) + .collect::>(); + let [actor] = originals.as_slice() else { + return None; + }; + if checked.checked().interner.resolve_actor(*actor_id).name != declared.name { + return None; + } + Some((actor, declared, ty, *actor_id)) +} + +fn common( + function: &Function, + actor: &ActorDef, + declared: &DefInfo, + checked: &CheckedResourceProgram, +) -> bool { + function.source_definition.is_none() + && function.identity.declaration.namespace + == declared.namespace.as_deref().unwrap_or_default() + && checked.source_origins().get(&actor.span.file) + == Some(&function.identity.declaration.origin) +} + +pub(super) fn matches( + owner: CheckedRequiredOwner, + function: &Function, + checked: &Arc, +) -> bool { + match owner { + CheckedRequiredOwner::ActorInitializer { + definition, + declaration, + field, + } => { + let Some((actor, declared, ty, actor_id)) = original_actor(checked, definition) else { + return false; + }; + let Some(initializer) = actor.state_fields.get(field) else { + return false; + }; + let Some((name, _)) = checked + .checked() + .interner + .resolve_actor(actor_id) + .state_fields + .get(field) + else { + return false; + }; + common(function, actor, declared, checked) + && function.identity.declaration.kind == DeclarationKind::ActorConstructor + && function.identity.declaration.name == actor.name.name + && function.actor_handler.is_none() + && function.span == actor.span + && function.body.span == actor.span + && function.return_type == ty + && initializer.name.span == declaration + && initializer.name.name == *name + } + CheckedRequiredOwner::ActorHandler { + target, + declaration, + } => { + let Some((actor, declared, _, actor_id)) = + original_actor(checked, target.actor_definition) + else { + return false; + }; + let Some(handler) = actor.handlers.get(target.handler_index) else { + return false; + }; + let Some(receipt) = checked.checked().actor_handler(target) else { + return false; + }; + common(function, actor, declared, checked) + && target.actor == actor_id + && function.identity.declaration.kind == DeclarationKind::ActorHandler + && function.identity.declaration.name + == format!("{}.{}", actor.name.name, handler.name.name) + && function.span == handler.span + && function.body.span == handler.body.span + && handler.name.span == declaration + && receipt.matches_source_actor(actor) + && receipt.body_span() == handler.body.span + && receipt.response_type() == function.return_type + && function.actor_handler.as_ref().is_some_and(|entry| { + entry.target() == target && entry.validate(function).is_ok() + }) + } + _ => false, + } +} diff --git a/crates/jett_hir/src/required_bake_receipt.rs b/crates/jett_hir/src/required_bake_receipt.rs new file mode 100644 index 00000000..2f36a19e --- /dev/null +++ b/crates/jett_hir/src/required_bake_receipt.rs @@ -0,0 +1,218 @@ +//! Ephemeral receipt of the actual checked ordinary required-value bake. +use super::*; + +struct Bake<'types> { + types: &'types TypeInterner, + before: Program, + after: Program, + checked: Arc, + original: Vec, + cache: ExplicitComptimeValues, +} + +/// Minted only after the private producer authenticates every replacement and +/// generated binding. No archive stores this temporary receipt. +pub(crate) struct CheckedRequiredBake<'types>(Box>); + +fn metadata_equal(left: &Program, right: &Program) -> bool { + let Program { + resource_manifest, + resource_source, + functions: _, + equality_methods, + } = left; + let Program { + resource_manifest: other_manifest, + resource_source: other_source, + functions: _, + equality_methods: other_equality, + } = right; + resource_manifest == other_manifest + && resource_source == other_source + && equality_methods == other_equality +} + +impl CheckedRequiredBake<'_> { + pub(super) fn candidate(&self) -> Program { + self.0.after.clone() + } + pub(super) fn original_functions(&self) -> &[Function] { + &self.0.original + } + pub(super) fn checked(&self) -> &Arc { + &self.0.checked + } + + pub(crate) fn authenticate_output( + &self, + before: &Program, + after: &Program, + types: &TypeInterner, + ) -> Result<(), String> { + if !std::ptr::eq(types, self.0.types) + || !metadata_equal(before, &self.0.before) + || !crate::resource_materialization::functions_equal( + &before.functions, + &self.0.before.functions, + ) + || !metadata_equal(after, &self.0.after) + || !crate::resource_materialization::functions_equal( + &after.functions, + &self.0.after.functions, + ) + { + return Err( + "required bake receipt has another complete input, output or type arena".into(), + ); + } + Ok(()) + } + + pub(crate) fn replay( + &self, + before: &Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, + ) -> Result { + self.authenticate_output(before, &self.0.after, types)?; + if !self.0.cache.same_checked_cache(values) + || !self.0.before.resource_source.belongs_to(&self.0.checked) + { + return Err( + "required bake receipt has another successful cache or checked program".into(), + ); + } + Ok(self.0.after.clone()) + } +} + +/// This is the sole production constructor. A caller supplies a typed value +/// constructor, never a positive transition proof or an output function table. +pub(super) fn produce<'types>( + program: &Program, + values: &ExplicitComptimeValues, + types: &'types TypeInterner, + mut materialize: impl FnMut( + &Value, + TypeId, + Span, + &mut Vec, + &mut Vec, + ) -> Result, +) -> Result, String> { + if program.resource_source.checked_program().is_none() + || !program.resource_manifest.kinds().is_empty() + { + return Err("ordinary required transition has no retained checked ordinary archive".into()); + } + crate::actor_graph::validate_conversion_input(program) + .map_err(|errors| format!("invalid protected Graph input: {errors:?}"))?; + crate::reflected_callable::validate_program(program) + .map_err(|errors| format!("invalid original reflected owner: {errors:?}"))?; + program.resource_source.validate_current(program, types)?; + crate::actor_handlers::validate_program(program) + .map_err(|errors| format!("invalid original actor snapshot: {errors:?}"))?; + let checked = program + .resource_source + .checked_program() + .ok_or("reflected required transition lacks original checked Source")? + .clone(); + // The private Graph witness authenticates the entire derived table, + // including appended adapters. Original Source still compares exactly. + let source_count = if program.resource_source.graph_values.is_some() { + program.resource_source.functions().len() + } else { + program.functions.len() + }; + let original = program + .functions + .iter() + .take(source_count) + .map(|function| { + crate::reflected_callable::original_function( + function, + &program.resource_source, + &checked, + ) + }) + .collect::, _>>()?; + if !super::equal(&original, program.resource_source.functions()) { + return Err( + "reflected required transition changed original Source before construction".into(), + ); + } + let proofs = values.checked_required_values(&checked)?; + let mut candidate = program.clone(); + let functions = program.functions.clone(); + for function in &mut candidate.functions { + let owner = function.clone(); + let mut bindings = Vec::new(); + let original_local_count = function.locals.len(); + let mut replacements = Vec::new(); + crate::resource_materialization::walk_block(&mut function.body, &mut |expression| { + if !matches!( + expression.kind, + ExpressionKind::Constant { .. } | ExpressionKind::Comptime { .. } + ) { + return Ok(()); + } + let selected = proofs + .iter() + .filter(|proof| { + crate::resource_materialization::proof_matches( + proof, + &owner, + expression, + &checked, + &program.resource_source, + ) + }) + .collect::>(); + let [proof] = selected.as_slice() else { + return Err("required value lacks a unique original checked owner/context".into()); + }; + let replacement = materialize( + proof.value(), + expression.ty, + expression.span, + &mut function.locals, + &mut bindings, + )?; + if replacement.ty != expression.ty || replacement.span != expression.span { + return Err("required replacement changed its original typed site".into()); + } + replacements.push(((*proof).clone(), replacement.clone())); + *expression = replacement; + Ok(()) + })?; + if function.locals.get(..original_local_count) != Some(owner.locals.as_slice()) { + return Err("required materializer changed an original local".into()); + } + function.body.statements.splice(0..0, bindings.clone()); + let used = crate::reflected_callable::materialized_source::validate_required_values( + &checked, + &candidate.resource_source, + function, + &functions, + &replacements, + )?; + let added = &function.locals[original_local_count..]; + if added.len() != bindings.len() || used.len() != added.len() + || added.iter().enumerate().any(|(offset,local)| { + local.id.index() as usize != original_local_count + offset + || local.mutable || local.view_source.is_some() || local.debug_ty != local.ty + || local.debug_type_name.is_some() || !used.contains(&local.id) + }) + || bindings.iter().any(|statement| !matches!(statement.kind, StatementKind::Let { local, .. } if used.contains(&local))) + { return Err("required materializer added an unauthenticated local or statement".into()); } + } + + Ok(CheckedRequiredBake(Box::new(Bake { + types, + before: program.clone(), + after: candidate, + checked, + original, + cache: values.clone(), + }))) +} diff --git a/crates/jett_hir/src/required_owner_materialization_tests.rs b/crates/jett_hir/src/required_owner_materialization_tests.rs new file mode 100644 index 00000000..faec3601 --- /dev/null +++ b/crates/jett_hir/src/required_owner_materialization_tests.rs @@ -0,0 +1,330 @@ +//! Required inline and refinement values retain their exact original owner. +use super::*; + +const INLINE_SOURCE: &str = "namespace app\nfunction callback[T]() returns function() returns string:\n return function() returns string: return comptime type.name[T]()\nfunction scoped[T]() returns function() returns string:\n comptime type Bound = type.info[T]():\n return function() returns string: return comptime type.name[Bound]()\nfunction nested() returns function() returns function() returns int64:\n return function() returns function() returns int64:\n return function() returns int64: return comptime 7\nfunction main() returns nothing:\n function() returns string small = callback[int8]()\n function() returns string text = callback[string]()\n function() returns string bound = scoped[int16]()\n function() returns function() returns int64 factory = nested()\n function() returns int64 inner = factory()\n return nothing\n"; + +#[test] +fn required_inline_parentheses_preserve_the_physical_source_association() { + let source = "namespace app\nfunction callback() returns function() returns int64:\n return ((function() returns int64: return comptime 7))\nfunction main() returns nothing:\n function() returns int64 selected = callback()\n return nothing\n"; + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let [row] = program.resource_source.required_materializations() else { + panic!("one required parenthesized inline value") + }; + assert!(matches!(row.current().kind, E::Int(7))); + let function = program + .resource_source + .functions() + .iter() + .find(|f| f.id == row.function()) + .unwrap(); + let inline = row.proof.original_inline_function().unwrap().unwrap(); + assert_ne!( + function.span, + inline.span(), + "the extracted span retains both parentheses" + ); + assert!( + program + .resource_source + .original_inline_root(function, row.original(), inline, &checked) + .is_some() + ); + } +} + +#[test] +fn parenthesized_required_inline_expansions_keep_each_scoped_selection() { + let source = "namespace app\nstruct Pair:\n first: int8\n second: int16\nfunction labels() returns nothing:\n for field in type.fields[Pair]():\n comptime type Field = field.type_info:\n function() returns string label = (function() returns string: return comptime type.name[Field]())\n return nothing\nfunction main() returns nothing:\n labels()\n return nothing\n"; + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let rows = program.resource_source.required_materializations(); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].proof.source_span(), rows[1].proof.source_span()); + assert_ne!( + rows[0].proof.scoped_bindings(), + rows[1].proof.scoped_bindings() + ); + for row in rows { + let function = program + .resource_source + .functions() + .iter() + .find(|f| f.id == row.function()) + .unwrap(); + assert!(proof_matches( + &row.proof, + function, + row.original(), + &checked, + &program.resource_source + )); + let sibling = rows + .iter() + .find(|other| other.function() != row.function()) + .unwrap(); + assert!(!proof_matches( + &sibling.proof, + function, + row.original(), + &checked, + &program.resource_source + )); + } + } +} + +#[test] +fn required_inline_values_match_exact_extraction_root_and_complete_context() { + for release in [false, true] { + let checked = checked(INLINE_SOURCE, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + let original = program.resource_source.functions().to_vec(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + assert!(functions_equal( + program.resource_source.functions(), + &original + )); + let rows = program.resource_source.required_materializations(); + assert_eq!(rows.len(), 4); + assert_eq!( + rows.iter() + .filter(|row| row.proof.generic_index().is_some()) + .count(), + 3 + ); + assert!( + rows.iter() + .any(|row| row.proof.scoped_bindings().len() == 1) + ); + assert!( + rows.iter() + .any(|row| matches!(&row.current().kind, E::Int(7))) + ); + for row in rows { + let function = original.iter().find(|f| f.id == row.function()).unwrap(); + assert_eq!(function.debug_kind, crate::FunctionDebugKind::Inline); + assert!(function.source_definition.is_none()); + assert!(proof_matches( + &row.proof, + function, + row.original(), + &checked, + &program.resource_source + )); + let inline = row.proof.original_inline_function().unwrap().unwrap(); + assert!( + program + .resource_source + .original_inline_root(function, row.original(), inline, &checked) + .is_some() + ); + let copied = inline.clone(); + assert!( + program + .resource_source + .original_inline_root(function, row.original(), &copied, &checked) + .is_none() + ); + assert!(!proof_matches( + &row.proof, + function, + row.original(), + &checked, + &crate::ResourceSourceArchive::empty() + )); + for field in 0..5 { + let mut changed = function.clone(); + match field { + 0 => changed.identity.declaration.namespace = "forged".into(), + 1 => changed.return_type = TypeInterner::BOOL, + 2 => changed + .identity + .specialization + .type_info_kinds + .push((0, "forged".into())), + 3 => changed.span.start += 1, + _ => changed.identity.scoped_type_bindings.clear(), + } + if changed != *function { + assert!(!proof_matches( + &row.proof, + &changed, + row.original(), + &checked, + &program.resource_source + )); + } + } + let mut expression = row.original().clone(); + let E::Comptime { value, .. } = &mut expression.kind else { + panic!("required inline node") + }; + value.kind = E::Bool(false); + assert!(!proof_matches( + &row.proof, + function, + &expression, + &checked, + &program.resource_source + )); + let sibling = original + .iter() + .find(|f| f.id != function.id && f.debug_kind == crate::FunctionDebugKind::Inline) + .unwrap(); + let mut copied_owner = sibling.clone(); + copied_owner.span = function.span; + assert!(!proof_matches( + &row.proof, + &copied_owner, + row.original(), + &checked, + &program.resource_source + )); + } + let foreign = self::checked(INLINE_SOURCE, release); + let foreign_values = required(&foreign); + let foreign_proofs = foreign_values.checked_required_values(&foreign).unwrap(); + for row in rows { + let function = original.iter().find(|f| f.id == row.function()).unwrap(); + let foreign_proof = foreign_proofs + .iter() + .find(|proof| { + proof.source_span() == row.proof.source_span() + && proof.type_arguments() == row.proof.type_arguments() + }) + .unwrap(); + assert!(!proof_matches( + foreign_proof, + function, + row.original(), + &checked, + &program.resource_source + )); + } + } +} + +const REFINEMENT_SOURCE: &str = "namespace domain\ntype Positive = int64 where value > 0 and (comptime 3) > 0\ntype Other = int64 where value > 0 and (comptime 4) > 0\nnamespace app\nfunction main() returns nothing:\n return nothing\n"; + +#[test] +fn required_refinement_values_match_original_predicate_and_checked_declaration() { + for release in [false, true] { + let checked = checked(REFINEMENT_SOURCE, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let rows = program.resource_source.required_materializations(); + assert_eq!(rows.len(), 2); + for row in rows { + let function = program + .resource_source + .functions() + .iter() + .find(|f| f.id == row.function()) + .unwrap(); + assert_eq!( + function.identity.declaration.kind, + DeclarationKind::RefinementPredicate + ); + assert!(matches!( + row.proof.owner(), + CheckedRequiredOwner::RefinementPredicate { .. } + )); + assert!(row.proof.original_inline_function().unwrap().is_none()); + assert!(proof_matches( + &row.proof, + function, + row.original(), + &checked, + &program.resource_source + )); + for field in 0..6 { + let mut changed = function.clone(); + match field { + 0 => changed.source_definition = None, + 1 => changed.identity.declaration.namespace = "app".into(), + 2 => changed.identity.declaration.kind = DeclarationKind::Function, + 3 => changed.params.clear(), + 4 => { + changed.identity.declaration.name = + if function.identity.declaration.name == "Positive" { + "Other".into() + } else { + "Positive".into() + } + } + _ => { + changed.identity.declaration.name = format!( + "{}.{}", + function.identity.declaration.namespace, + function.identity.declaration.name, + ) + } + } + assert!(!proof_matches( + &row.proof, + &changed, + row.original(), + &checked, + &program.resource_source + )); + } + let sibling = rows + .iter() + .find(|other| other.function() != row.function()) + .unwrap(); + assert!(!proof_matches( + &sibling.proof, + function, + row.original(), + &checked, + &program.resource_source + )); + let mut changed = row.original().clone(); + let E::Comptime { value, .. } = &mut changed.kind else { + panic!("required predicate node") + }; + value.kind = E::Int(9); + assert!(!proof_matches( + &row.proof, + function, + &changed, + &checked, + &program.resource_source + )); + } + let foreign = self::checked(REFINEMENT_SOURCE, release); + let foreign_values = required(&foreign); + for foreign_proof in foreign_values.checked_required_values(&foreign).unwrap() { + let row = rows + .iter() + .find(|row| row.proof.source_span() == foreign_proof.source_span()) + .unwrap(); + let function = program + .resource_source + .functions() + .iter() + .find(|f| f.id == row.function()) + .unwrap(); + assert!(!proof_matches( + &foreign_proof, + function, + row.original(), + &checked, + &program.resource_source + )); + } + } +} diff --git a/crates/jett_hir/src/resource_manifest.rs b/crates/jett_hir/src/resource_manifest.rs new file mode 100644 index 00000000..93af0465 --- /dev/null +++ b/crates/jett_hir/src/resource_manifest.rs @@ -0,0 +1,431 @@ +//! Original checked Resource declarations and hooks, including unused entries. +//! Public references cannot be constructed from a type, name or raw lowering input. +use std::sync::Arc; + +use jett_common::{SourceOrigin, Span}; +use jett_parser::ast::Item; +use jett_resolve::{DefId, DefKind}; +use jett_typecheck::{CheckedResourceHook, CheckedResourceProgram, validate_resource_hooks}; +use jett_types::{ResourceKernelRecipe, Type, TypeId, TypeInterner}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceKindId(u32); +impl ResourceKindId { + pub fn index(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceKind { + definition: DefId, + ty: TypeId, + declaration: Span, + name: String, + namespace: Option, +} +impl ResourceKind { + pub fn definition(&self) -> DefId { + self.definition + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn declaration(&self) -> Span { + self.declaration + } + pub fn name(&self) -> &str { + &self.name + } + pub fn namespace(&self) -> Option<&str> { + self.namespace.as_deref() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Hook { + checked: CheckedResourceHook, + kind: ResourceKindId, + recipe: ResourceKernelRecipe, +} + +#[derive(Debug)] +struct ManifestData { + original: Arc, + kinds: Vec, + hooks: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct ResourceManifest { + data: Option>, +} +impl PartialEq for ResourceManifest { + fn eq(&self, other: &Self) -> bool { + match (&self.data, &other.data) { + (None, None) => true, + (Some(left), Some(right)) => Arc::ptr_eq(left, right), + _ => false, + } + } +} +impl Eq for ResourceManifest {} + +/// Exact nominal provenance, separate from the dense index used for table lookup. +#[derive(Debug, Clone)] +pub struct ResourceKindRef { + data: Arc, + index: usize, +} +impl PartialEq for ResourceKindRef { + fn eq(&self, other: &Self) -> bool { + self.index == other.index && Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ResourceKindRef {} +impl ResourceKindRef { + pub fn id(&self) -> ResourceKindId { + ResourceKindId(self.index as u32) + } + pub fn ty(&self) -> TypeId { + self.data.kinds[self.index].ty + } + pub fn declaration(&self) -> Span { + self.data.kinds[self.index].declaration + } + pub fn definition(&self) -> DefId { + self.data.kinds[self.index].definition + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + ResourceManifest { + data: Some(self.data.clone()), + } + .validate(types)?; + self.data + .kinds + .get(self.index) + .ok_or("Resource kind reference is outside its original manifest")?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct ResourceHookRef { + data: Arc, + index: usize, +} +impl PartialEq for ResourceHookRef { + fn eq(&self, other: &Self) -> bool { + self.index == other.index && Arc::ptr_eq(&self.data, &other.data) + } +} +impl Eq for ResourceHookRef {} +impl ResourceHookRef { + pub fn definition(&self) -> DefId { + self.data.hooks[self.index].checked.definition + } + pub fn resource_type(&self) -> TypeId { + self.data.hooks[self.index].checked.resource_type + } + pub fn function_type(&self) -> TypeId { + self.data.hooks[self.index].checked.function_type + } + pub fn kind(&self) -> ResourceKindId { + self.data.hooks[self.index].kind + } + pub fn resource_kind(&self) -> ResourceKindRef { + ResourceKindRef { + data: self.data.clone(), + index: self.kind().index() as usize, + } + } + + pub fn recipe(&self) -> ResourceKernelRecipe { + self.data.hooks[self.index].recipe + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + ResourceManifest { + data: Some(self.data.clone()), + } + .validate(types)?; + self.data + .hooks + .get(self.index) + .ok_or("Resource hook reference is outside its original manifest")?; + Ok(()) + } + pub fn metadata_types(&self, mut visit: impl FnMut(TypeId)) { + visit(self.resource_type()); + visit(self.function_type()); + if let Type::Function { + params, + return_type, + .. + } = self + .data + .original + .checked() + .interner + .resolve(self.function_type()) + { + for &ty in params { + visit(ty); + } + visit(*return_type); + } + } +} + +impl ResourceManifest { + pub fn empty() -> Self { + Self::default() + } + pub fn kinds(&self) -> &[ResourceKind] { + self.data.as_ref().map_or(&[], |data| data.kinds.as_slice()) + } + pub fn hooks(&self) -> impl Iterator + '_ { + self.data.iter().flat_map(|data| { + (0..data.hooks.len()).map(move |index| ResourceHookRef { + data: data.clone(), + index, + }) + }) + } + pub fn contains_type(&self, ty: TypeId) -> bool { + self.kinds().iter().any(|kind| kind.ty == ty) + } + pub fn kind_for_type(&self, ty: TypeId) -> Option { + let data = self.data.as_ref()?; + let index = data.kinds.iter().position(|kind| kind.ty == ty)?; + Some(ResourceKindRef { + data: data.clone(), + index, + }) + } + pub fn contains_kind(&self, kind: &ResourceKindRef) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(data, &kind.data) && kind.index < data.kinds.len()) + } + + pub fn contains_hook(&self, hook: &ResourceHookRef) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(data, &hook.data) && hook.index < data.hooks.len()) + } + pub(crate) fn hook_for_definition(&self, definition: DefId) -> Option { + self.hooks().find(|hook| hook.definition() == definition) + } + pub(crate) fn checked(original: &Arc) -> Result { + let (kinds, hooks) = records(original)?; + let manifest = Self { + data: Some(Arc::new(ManifestData { + original: original.clone(), + kinds, + hooks, + })), + }; + manifest.validate(&original.checked().interner)?; + Ok(manifest) + } + /// Reject unknown Resource kinds anywhere in a checked layout or signature. + /// This validates provenance only; it does not grant aggregate custody. + pub fn validate_type(&self, types: &TypeInterner, ty: TypeId) -> Result<(), String> { + let mut pending = vec![ty]; + let mut seen = std::collections::HashSet::new(); + while let Some(ty) = pending.pop() { + if ty.index() as usize >= types.len() { + return Err("Resource manifest type visitor is outside its interner".into()); + } + if !seen.insert(ty) { + continue; + } + pending.extend_from_slice(types.nominal_type_arguments(ty)); + match types.resolve(ty) { + Type::Resource(_) if !self.contains_type(ty) => { + return Err( + "Resource type has no original checked nominal manifest entry".into(), + ); + } + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => pending.push(*inner), + Type::Map(key, value) | Type::Result(key, value) => { + pending.push(*key); + pending.push(*value); + } + Type::Function { + params, + return_type, + .. + } => { + pending.extend_from_slice(params); + pending.push(*return_type); + } + Type::Struct(id) => { + let definition = types.resolve_struct(*id); + pending.extend(definition.fields.iter().map(|(_, ty)| *ty)); + for method in &definition.methods { + pending.extend(method.params.iter().map(|(_, ty, _)| *ty)); + pending.push(method.return_type); + } + } + Type::Enum(id) => { + for variant in &types.resolve_enum(*id).variants { + pending.extend(variant.fields.iter().map(|(_, ty)| *ty)); + } + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + for state in &types.resolve_machine(*id).states { + pending.extend(state.fields.iter().map(|(_, ty)| *ty)); + } + } + Type::Interface(id) => { + for method in &types.resolve_interface(*id).methods { + pending.extend(method.params.iter().map(|(_, ty, _)| *ty)); + pending.push(method.return_type); + } + } + Type::Actor(id) => { + let actor = types.resolve_actor(*id); + pending.extend( + actor + .capability_params + .iter() + .chain(&actor.state_fields) + .map(|(_, ty)| *ty), + ); + for message in &actor.messages { + pending.extend(message.params.iter().map(|(_, ty)| *ty)); + pending.push(message.responds); + } + } + _ => {} + } + } + Ok(()) + } + pub fn validate(&self, types: &TypeInterner) -> Result<(), String> { + let Some(data) = &self.data else { + return Ok(()); + }; + let (kinds, hooks) = records(&data.original)?; + if kinds != data.kinds || hooks != data.hooks { + return Err( + "Resource manifest differs from its original checked declarations or unused hooks" + .into(), + ); + } + for kind in &data.kinds { + if kind.ty.index() as usize >= types.len() + || types.resolve(kind.ty) != data.original.checked().interner.resolve(kind.ty) + || !matches!(types.resolve(kind.ty), Type::Resource(name) if name == &kind.name) + { + return Err( + "Resource manifest nominal type differs from its original declaration".into(), + ); + } + } + for hook in &data.hooks { + if !hook.recipe.matches_signature( + types, + hook.checked.resource_type, + hook.checked.function_type, + ) || types.resolve(hook.checked.function_type) + != data + .original + .checked() + .interner + .resolve(hook.checked.function_type) + { + return Err( + "Resource manifest hook signature differs from its exact checked recipe".into(), + ); + } + } + Ok(()) + } +} + +fn records( + original: &Arc, +) -> Result<(Vec, Vec), String> { + validate_resource_hooks(original.module(), original.resolved(), original.checked()) + .map_err(|error| error.to_string())?; + let mut kinds = Vec::new(); + for item in &original.module().items { + let Item::Resource(resource) = item else { + continue; + }; + let declarations = original + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == DefKind::Resource && definition.span == resource.name.span + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + return Err( + "Resource manifest requires one exact original Resource declaration".into(), + ); + }; + if original.source_origins().get(&resource.name.span.file) != Some(&SourceOrigin::Stdlib) { + return Err( + "Resource manifest declaration has no compiler-shipped source origin".into(), + ); + } + let ty = *original + .checked() + .definition_types + .get(&declaration.id) + .ok_or("Resource manifest declaration has no checked nominal type")?; + if ty.index() as usize >= original.checked().interner.len() + || !matches!(original.checked().interner.resolve(ty), Type::Resource(name) if name == &declaration.name) + { + return Err("Resource manifest declaration has another nominal type".into()); + } + if kinds + .iter() + .any(|kind: &ResourceKind| kind.definition == declaration.id || kind.ty == ty) + { + return Err("Resource manifest duplicates a nominal declaration".into()); + } + kinds.push(ResourceKind { + definition: declaration.id, + ty, + declaration: resource.name.span, + name: declaration.name.clone(), + namespace: declaration.namespace.clone(), + }); + } + let mut hooks = Vec::new(); + for kernel in original.resolved().resource_kernels.iter() { + let checked = original + .checked() + .resource_hooks + .get(&kernel.definition) + .ok_or("Resource manifest kernel has no checked hook")? + .clone(); + let kind = kinds + .iter() + .position(|kind| { + kind.definition == kernel.resource_definition && kind.ty == checked.resource_type + }) + .ok_or("Resource manifest hook has no original nominal declaration")?; + hooks.push(Hook { + checked, + kind: ResourceKindId(kind as u32), + recipe: kernel.recipe, + }); + } + hooks.sort_by_key(|hook| hook.checked.definition.index()); + Ok((kinds, hooks)) +} + +#[cfg(test)] +#[path = "resource_manifest_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/resource_manifest_tests.rs b/crates/jett_hir/src/resource_manifest_tests.rs new file mode 100644 index 00000000..13910786 --- /dev/null +++ b/crates/jett_hir/src/resource_manifest_tests.rs @@ -0,0 +1,258 @@ +use super::*; +use crate::{ + CallBridge, CallOwnership, CallTarget, Expression, ExpressionKind as E, StatementKind as S, +}; +use jett_common::{FileId, SourceOrigin}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedCallerEffect, CheckedCallerSyntax}; +use std::collections::HashMap; + +const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +"#; +const SOURCE: &str = r#"namespace app +function factory(view net: Network, label: int64) returns result[resource_probe.TestHandle, string]: + use resource_probe + return resource_probe.create(view net, label) +function descriptor() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + return resource_probe.descriptor() +"#; + +fn checked(release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let source = parse(SOURCE, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(source.errors.is_empty(), "{:?}", source.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource.name.span) + } else { + None + } + }) + .unwrap(); + parsed.module.items.extend(source.module.items); + parsed.errors.extend(source.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +fn invoke(program: &crate::Program) -> &Expression { + program + .functions + .iter() + .flat_map(|function| &function.body.statements) + .find_map(|statement| match &statement.kind { + S::Return(Some(value)) | S::Expression(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("exact Source hook invocation") +} +fn invoke_mut(program: &mut crate::Program) -> &mut Expression { + program + .functions + .iter_mut() + .flat_map(|function| &mut function.body.statements) + .find_map(|statement| match &mut statement.kind { + S::Return(Some(value)) | S::Expression(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + .unwrap() +} + +#[test] +fn resource_manifest_keeps_source_hooks_descriptors_and_lexical_argument_order() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let types = &original.checked().interner; + crate::validate_backend_types(&program, types).unwrap(); + assert_eq!(program.resource_manifest.kinds().len(), 1); + assert_eq!(program.resource_manifest.hooks().count(), 3); // Borrow remains unused in Source. + let E::ResourceInvoke { + hook, + args, + evaluation_order, + ownership: CallOwnership::Source(source), + } = &invoke(&program).kind + else { + panic!("Source hook node"); + }; + assert_eq!(hook.recipe(), ResourceKernelRecipe::NetworkFactory); + let kind = hook.resource_kind(); + kind.validate(types).unwrap(); + assert!(program.resource_manifest.contains_kind(&kind)); + assert_eq!(kind.ty(), hook.resource_type()); + assert_eq!( + program.resource_manifest.kind_for_type(kind.ty()), + Some(kind) + ); + assert_eq!(evaluation_order, &[1, 0]); + assert_eq!(args.len(), 2); + assert_eq!(source.target, CallTarget::ResourceHook(hook.clone())); + assert_eq!( + source.bridge, + CallBridge::ResourceHook { hook: hook.clone() } + ); + assert_eq!(source.arguments[0].source_index, 1); + assert_eq!(source.arguments[0].syntax, CheckedCallerSyntax::WrittenView); + assert_eq!( + source.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!(source.arguments[1].effect, CheckedCallerEffect::Copy); + assert!(program.functions.iter().flat_map(|function| &function.body.statements).any(|statement| matches!(&statement.kind, S::Return(Some(Expression { kind: E::ResourceHookValue { hook }, ty, .. })) if hook.recipe() == ResourceKernelRecipe::Finalize && *ty == hook.function_type()))); + assert!( + crate::lower( + original.module(), + original.resolved(), + original.checked(), + original.source_origins() + ) + .is_err() + ); + crate::lower_checked_resource_program_with_test_bodies(&original).unwrap(); + } +} + +#[test] +fn resource_manifest_validates_unused_hooks_and_nominal_kind_records() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let data = program.resource_manifest.data.as_ref().unwrap(); + for mutation in 0..4 { + let mut kinds = data.kinds.clone(); + let mut hooks = data.hooks.clone(); + match mutation { + 0 => { + hooks.retain(|hook| hook.recipe != ResourceKernelRecipe::NetworkBorrow); + } + 1 => { + hooks + .iter_mut() + .find(|hook| hook.recipe == ResourceKernelRecipe::NetworkBorrow) + .unwrap() + .recipe = ResourceKernelRecipe::Finalize; + } + 2 => { + kinds[0].ty = TypeInterner::STRING; + } + _ => { + kinds[0].declaration.end += 1; + } + } + let mut changed = program.clone(); + changed.resource_manifest = ResourceManifest { + data: Some(Arc::new(ManifestData { + original: original.clone(), + kinds, + hooks, + })), + }; + let errors = + crate::validate_backend_types(&changed, &original.checked().interner).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("Resource manifest differs")), + "{errors:?}" + ); + } + } +} + +#[test] +fn resource_manifest_refuses_foreign_refs_empty_manifest_and_source_packet_changes() { + for release in [false, true] { + let original = checked(release); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let foreign = crate::lower_checked_resource_program(&checked(release)).unwrap(); + for mutation in 0..6 { + let mut changed = program.clone(); + if mutation == 0 { + changed.resource_manifest = ResourceManifest::empty(); + } else { + let call = invoke_mut(&mut changed); + let E::ResourceInvoke { + hook, + evaluation_order, + args, + ownership: CallOwnership::Source(source), + } = &mut call.kind + else { + unreachable!() + }; + match mutation { + 1 => { + *hook = foreign + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkFactory) + .unwrap(); + } + 2 => { + *evaluation_order = vec![0, 1]; + } + 3 => { + source.bridge = CallBridge::Direct; + } + 4 => { + args[1].ty = TypeInterner::STRING; + } + _ => { + call.ty = TypeInterner::STRING; + } + } + } + assert!( + crate::validate_backend_types(&changed, &original.checked().interner).is_err(), + "mutation {mutation}" + ); + } + } +} diff --git a/crates/jett_hir/src/resource_materialization.rs b/crates/jett_hir/src/resource_materialization.rs new file mode 100644 index 00000000..51f31dae --- /dev/null +++ b/crates/jett_hir/src/resource_materialization.rs @@ -0,0 +1,711 @@ +//! Required values become execution data only through the original checked cache. +use crate::resource_source::{Materialized, RequiredMaterialization}; +use crate::{ + Block, DeclarationKind, Expression, ExpressionKind as E, Function, FunctionId, LowerError, + Program, ResourceHookRef, StatementKind as S, StringSegment, +}; +use jett_common::{FileId, Span}; +use jett_comptime::{CheckedRequiredOwner, CheckedRequiredValue, ExplicitComptimeValues, Value}; +use jett_parser::ast::{Expr, Item}; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId, TypeInterner}; + +#[path = "required_actor_owners.rs"] +mod required_actor_owners; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +/// Derive required replacements from successful private checked entries. This +/// does not evaluate Source, install a provider, or accept a caller-baked view. +pub fn materialize_checked_required_values( + program: &mut Program, + values: &ExplicitComptimeValues, + types: &TypeInterner, +) -> Result<(), Vec> { + let before = program.clone(); + crate::actor_graph::validate_conversion_input(&before)?; + let archive = program.resource_source.clone(); + let span = program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |f| f.span); + let result = (|| -> Result { + archive.validate_types(types)?; + if archive.manifest() != Some(&program.resource_manifest) + || archive.equality_methods() != Some(&program.equality_methods) + || !functions_equal(archive.execution_functions(), &program.functions) + { + return Err( + "required materialization changed original checked HIR before authentication" + .into(), + ); + } + let checked = archive + .checked_program() + .ok_or("required materialization has no original checked program")?; + let proofs = values.checked_required_values(checked)?; + let mut functions = program.functions.clone(); + let mut rows = archive.required_materializations().to_vec(); + for function in &mut functions { + let owner = function.clone(); + walk_block(&mut function.body, &mut |expression| { + if !matches!(expression.kind, E::Comptime { .. } | E::Constant { .. }) { + return Ok(()); + } + let original = expression.clone(); + let selected = proofs + .iter() + .filter(|proof| proof_matches(proof, &owner, expression, checked, &archive)) + .collect::>(); + let [proof] = selected.as_slice() else { + return Err("required expression has no unique original checked value and complete context".into()); + }; + let (kind, hook) = derive_value( + proof, + expression.ty, + &program.resource_manifest, + checked, + types, + )?; + expression.kind = kind; + rows.push(RequiredMaterialization { + function: owner.id, + identity: owner.identity.clone(), + original, + current: expression.clone(), + hook, + proof: (*proof).clone(), + projection: None, + }); + Ok(()) + })?; + } + let required_only = required_only_functions(&archive, &functions, &rows)?; + Ok(Materialized { + functions, + values: rows, + required_only, + }) + })(); + match result { + Ok(view) => { + let mut candidate = program.clone(); + candidate.functions = view.functions.clone(); + crate::actor_graph::complete_conversion(&mut candidate, &before)?; + candidate.resource_source.install_materialized(view); + // Preserve the opaque successful cache association for canonical + // Property sampling; public value mirrors grant no new authority. + candidate.resource_source.required_cache = Some(Arc::new(values.clone())); + *program = candidate; + Ok(()) + } + Err(message) => Err(vec![LowerError { span, message }]), + } +} + +/// Shared by scalar and graph required-value producers. Candidates originate +/// only in successfully replaced checked operands; current runtime edges, +/// exports and actor handlers keep their complete original callable bodies. +pub(crate) fn required_only_functions( + archive: &crate::ResourceSourceArchive, + functions: &[Function], + rows: &[RequiredMaterialization], +) -> Result, String> { + let checked = archive + .checked_program() + .ok_or("required closure has no checked program")?; + let mut candidates = HashSet::new(); + for row in rows { + if !row.proof.belongs_to(checked) { + return Err("required execution view has foreign proof".into()); + } + if let E::Comptime { value, .. } = &row.original.kind { + references(value, &mut candidates); + } + } + closure(&mut candidates, &call_graph(archive.functions())); + let exports = archive + .exported_function_ids() + .iter() + .copied() + .collect::>(); + let mut live = functions + .iter() + .filter(|function| { + !candidates.contains(&function.id) + || exports.contains(&function.id) + || function.identity.declaration.name == "main" + || !matches!( + function.identity.declaration.kind, + DeclarationKind::Function | DeclarationKind::ActorConstructor + ) + }) + .map(|function| function.id) + .collect::>(); + closure(&mut live, &call_graph(functions)); + let mut required_only = candidates.difference(&live).copied().collect::>(); + required_only.sort_by_key(|id| id.index()); + Ok(required_only) +} + +pub(crate) fn proof_matches( + proof: &CheckedRequiredValue, + function: &Function, + expression: &Expression, + checked: &Arc, + archive: &crate::ResourceSourceArchive, +) -> bool { + if !proof.belongs_to(checked) { + return false; + } + if let E::Constant { declaration } = expression.kind { + return proof.owner() == CheckedRequiredOwner::NamespaceConstant { declaration } + && proof.generic_index().is_none() + && proof.scoped_bindings().is_empty(); + } + let E::Comptime { + source_span, + bindings, + scopes, + .. + } = &expression.kind + else { + return false; + }; + if proof.source_span() != *source_span || !proof.is_explicit_comptime() { + return false; + } + let Ok(original) = proof.original_comptime() else { + return false; + }; + if !matches!(original, Expr::Comptime(_, span) if span == source_span) + || !proof.matches_original_comptime(original) + { + return false; + } + let Ok(inline) = proof.original_inline_function() else { + return false; + }; + let owner = if let Some(inline) = inline { + let Some(root) = archive.original_inline_root(function, expression, inline, checked) else { + return false; + }; + if function.debug_kind != crate::FunctionDebugKind::Inline + || function.source_definition.is_some() + || function.identity.type_arguments != root.identity.type_arguments + || function.identity.specialization != root.identity.specialization + || function.identity.scoped_type_bindings.len() > bindings.len() + || !function + .identity + .scoped_type_bindings + .iter() + .zip(bindings) + .all(|(original, current)| original == current) + { + return false; + } + root + } else { + if matches!( + proof.owner(), + CheckedRequiredOwner::RefinementPredicate { .. } + | CheckedRequiredOwner::ActorInitializer { .. } + | CheckedRequiredOwner::ActorHandler { .. } + ) && archive + .original_required_function(function, expression, checked) + .is_none() + { + return false; + } + function + }; + let owner_matches = required_owner_matches(proof.owner(), owner, checked); + if !owner_matches + || proof.type_arguments() != function.identity.type_arguments + || proof.specialization() != &function.identity.specialization + { + return false; + } + let generic = if owner.identity.type_arguments.is_empty() { + None + } else { + let matches = checked + .checked() + .generic_function_instantiations + .iter() + .enumerate() + .filter(|(_, instance)| { + Some(instance.definition) == owner.source_definition + && instance.concrete_args == owner.identity.type_arguments + && instance.specialization == owner.identity.specialization + }) + .map(|(index, _)| index) + .collect::>(); + let [index] = matches.as_slice() else { + return false; + }; + Some(*index) + }; + proof.generic_index() == generic + && proof.scoped_bindings().len() == bindings.len() + && scopes.len() == bindings.len() + && proof + .scoped_bindings() + .iter() + .zip(bindings) + .zip(scopes) + .all(|((proof, binding), scope)| { + proof.name() == binding.name + && proof.bound_type() == binding.ty + && proof.reflection() == &binding.reflection + && proof.owner() == scope.owner + && proof.index() == scope.index + && proof.selection() == scope.selection + }) +} + +fn required_owner_matches( + owner: CheckedRequiredOwner, + function: &Function, + checked: &Arc, +) -> bool { + match owner { + CheckedRequiredOwner::Function { definition, declaration } => { + function.source_definition == Some(definition) + && checked.module().items.iter().any(|item| { + matches!(item, Item::Function(value) + if value.name.span == declaration && value.span == function.span + && (checked.resolved().scope_table.def(definition).span == value.name.span + || checked.resolved().resolutions.get(&value.name.span) == Some(&definition))) + }) + && function.identity.declaration.kind == DeclarationKind::Function + } + CheckedRequiredOwner::Verify { declaration } => function.identity.declaration.kind == DeclarationKind::Verify + && checked.module().items.iter().any(|item| matches!(item, Item::Verify(v) if v.name.span == declaration && v.span == function.span)), + CheckedRequiredOwner::Property { declaration } => function.identity.declaration.kind == DeclarationKind::Property + && checked.module().items.iter().any(|item| matches!(item, Item::Property(v) if v.name.span == declaration && v.span == function.span)), + CheckedRequiredOwner::RefinementPredicate { definition, declaration } => { + let Some(declared) = checked.resolved().scope_table.definitions.get(definition.index() as usize) else { + return false; + }; + let declaration_name = if function.identity.declaration.namespace.is_empty() { + function.identity.declaration.name.clone() + } else { + format!("{}.{}", function.identity.declaration.namespace, function.identity.declaration.name) + }; + function.source_definition == Some(definition) + && function.identity.declaration.kind == DeclarationKind::RefinementPredicate + && declared.id == definition + && declared.kind == jett_resolve::DefKind::Type + && declared.span == declaration + && function.identity.declaration.namespace == declared.namespace.as_deref().unwrap_or_default() + && declaration_name == declared.name + && checked.source_origins().get(&function.span.file) == Some(&function.identity.declaration.origin) + && checked.module().items.iter().any(|item| matches!(item, Item::TypeAlias(value) + if value.name.span == declaration && value.span == function.span + && value.name.name == function.identity.declaration.name + && value.constraint.is_some())) + } + actor @ (CheckedRequiredOwner::ActorInitializer { .. } + | CheckedRequiredOwner::ActorHandler { .. }) => + required_actor_owners::matches(actor, function, checked), + CheckedRequiredOwner::NamespaceConstant { .. } => false, + } +} + +fn derive_value( + proof: &CheckedRequiredValue, + ty: TypeId, + manifest: &crate::ResourceManifest, + checked: &Arc, + types: &TypeInterner, +) -> Result<(E, Option), String> { + if let Some(original) = proof.checked_hook(checked)? { + let hook = manifest + .hook_for_definition(original.definition) + .ok_or("required hook is absent from the original Resource manifest")?; + if hook.function_type() != ty + || hook.function_type() != original.function_type + || hook.resource_type() != original.resource_type + || hook.resource_kind().definition() != original.resource_definition + { + return Err( + "required hook changed its exact checked signature or nominal Resource identity" + .into(), + ); + } + hook.validate(types)?; + return Ok((E::ResourceHookValue { hook: hook.clone() }, Some(hook))); + } + if matches!(types.resolve(ty), Type::Secret(_) | Type::Refinement { .. }) { + return Err("checked required value is evaluated, but qualified native materialized value transport is pending".into()); + } + let value = proof.value().payload(); + let kind = match (types.resolve(ty), value) { + (Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 + | Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Int64(value)) => + E::Int(*value as i128), + (Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 + | Type::Uint8 | Type::Uint16 | Type::Uint32 | Type::Uint64, Value::Uint64(value)) => + E::Int(*value as i128), + (Type::Float32 | Type::Float64, Value::Float64(value)) => E::Float(*value), + (Type::String, Value::String(value)) => E::String(value.clone()), + (Type::Bool, Value::Bool(value)) => E::Bool(*value), + (Type::Nothing, Value::Nothing) => E::Nothing, + (Type::Optional(_), Value::OptionalNone) => E::OptionalNone, + _ => return Err("checked required value is evaluated, but its native materialized value transport is pending".into()), + }; + Ok((kind, None)) +} + +pub(crate) fn exported_functions( + program: &Program, + checked: &CheckedResourceProgram, +) -> Vec { + let mut spans = HashSet::new(); + for item in &checked.module().items { + match item { + Item::Function(value) if value.exported => { + spans.insert(value.name.span); + } + Item::Mutual(value) => { + for function in &value.declarations { + if function.exported { + spans.insert(function.name.span); + } + } + } + _ => {} + } + } + program + .functions + .iter() + .filter(|f| { + f.source_definition.is_some_and(|definition| { + spans.contains(&checked.resolved().scope_table.def(definition).span) + }) + }) + .map(|f| f.id) + .collect() +} +fn references(expression: &Expression, output: &mut HashSet) { + let mut copy = expression.clone(); + let _ = walk_expression(&mut copy, &mut |expression| { + match &expression.kind { + E::Call { function, .. } + | E::FunctionRef(function) + | E::ClosureRef { function, .. } + | E::FunctionAdapter { function, .. } => { + output.insert(*function); + } + E::ActorSpawn { + constructor: Some(function), + .. + } => { + output.insert(*function); + } + E::ActorMessage { handler, .. } => { + output.insert(*handler); + } + E::InterfaceCoerce { adapters, .. } => { + output.extend(adapters.iter().map(|a| a.function)) + } + _ => {} + } + Ok(()) + }); +} +fn call_graph(functions: &[Function]) -> HashMap> { + functions + .iter() + .map(|function| { + let mut body = function.body.clone(); + let mut targets = HashSet::new(); + let _ = walk_block(&mut body, &mut |expression| { + // This visitor already recurses; collecting one node avoids repeated traversal. + match &expression.kind { + E::Call { function, .. } + | E::FunctionRef(function) + | E::ClosureRef { function, .. } + | E::FunctionAdapter { function, .. } => { + targets.insert(*function); + } + E::ActorSpawn { + constructor: Some(function), + .. + } => { + targets.insert(*function); + } + E::ActorMessage { handler, .. } => { + targets.insert(*handler); + } + E::InterfaceCoerce { adapters, .. } => { + targets.extend(adapters.iter().map(|a| a.function)) + } + _ => {} + } + Ok(()) + }); + (function.id, targets) + }) + .collect() +} +fn closure(ids: &mut HashSet, graph: &HashMap>) { + let mut pending = ids.iter().copied().collect::>(); + while let Some(id) = pending.pop() { + if let Some(edges) = graph.get(&id) { + for edge in edges { + if ids.insert(*edge) { + pending.push(*edge); + } + } + } + } +} + +// Float equality authenticates exact bits, including NaN payloads and signed zero. +pub(crate) fn functions_equal(left: &[Function], right: &[Function]) -> bool { + if left.len() != right.len() { + return false; + } + left.iter().zip(right).all(|(left, right)| { + let mut left = left.clone(); + let mut right = right.clone(); + let a = clear_floats(&mut left.body); + let b = clear_floats(&mut right.body); + a == b && left == right + }) +} +pub(crate) fn expression_equal(left: &Expression, right: &Expression) -> bool { + let mut left = left.clone(); + let mut right = right.clone(); + let mut a = Vec::new(); + let mut b = Vec::new(); + let _ = walk_expression(&mut left, &mut |e| { + clear_float(e, &mut a); + Ok(()) + }); + let _ = walk_expression(&mut right, &mut |e| { + clear_float(e, &mut b); + Ok(()) + }); + a == b && left == right +} +fn clear_float(expression: &mut Expression, bits: &mut Vec) { + if let E::Float(value) = &mut expression.kind { + bits.push(value.to_bits()); + *value = 0.0; + } +} +fn clear_floats(block: &mut Block) -> Vec { + let mut bits = Vec::new(); + let _ = walk_block(block, &mut |expression| { + clear_float(expression, &mut bits); + Ok(()) + }); + bits +} + +pub(crate) fn walk_block( + block: &mut Block, + visit: &mut impl FnMut(&mut Expression) -> Result<(), String>, +) -> Result<(), String> { + for statement in &mut block.statements { + match &mut statement.kind { + S::Let { value, .. } + | S::Expression(value) + | S::HandleDefault(value) + | S::Respond(value) => walk_expression(value, visit)?, + S::Assign { target, value } | S::UpdateField { target, value, .. } => { + walk_expression(target, visit)?; + walk_expression(value, visit)?; + } + S::Return(value) => { + if let Some(value) = value { + walk_expression(value, visit)?; + } + } + S::If { + condition, + then_block, + else_block, + } => { + walk_expression(condition, visit)?; + walk_block(then_block, visit)?; + if let Some(block) = else_block { + walk_block(block, visit)?; + } + } + S::While { condition, body } => { + walk_expression(condition, visit)?; + walk_block(body, visit)?; + } + S::For { iterable, body, .. } => { + walk_expression(iterable, visit)?; + walk_block(body, visit)?; + } + S::Match { scrutinee, arms } => { + walk_expression(scrutinee, visit)?; + for arm in arms { + walk_block(&mut arm.body, visit)?; + } + } + S::Assert { condition, message } => { + walk_expression(condition, visit)?; + if let Some(value) = message { + walk_expression(value, visit)?; + } + } + S::Breakpoint { condition, .. } => { + if let Some(value) = condition { + walk_expression(value, visit)?; + } + } + S::Scope(block) => walk_block(block, visit)?, + S::ReflectedTypeDispatch { type_info, arms } => { + walk_expression(type_info, visit)?; + for arm in arms { + walk_block(&mut arm.body, visit)?; + } + } + S::Break | S::Continue | S::Trace(_) => {} + } + } + Ok(()) +} +fn walk_expression( + expression: &mut Expression, + visit: &mut impl FnMut(&mut Expression) -> Result<(), String>, +) -> Result<(), String> { + visit(expression)?; + match &mut expression.kind { + E::ActorGraphImport(plan) => { + let mut body = plan.body().clone(); + walk_block(&mut body, visit)?; + if !hir_blocks_equal(&body, plan.body()) { + return Err( + "sealed actor graph body was rewritten outside its checked producer".into(), + ); + } + } + + E::Binary { left, right, .. } => { + walk_expression(left, visit)?; + walk_expression(right, visit)?; + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Comptime { value, .. } + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::StateIs { value, .. } + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Field { base: value, .. } + | E::View(value) + | E::Clone(value) => walk_expression(value, visit)?, + E::Call { args, .. } + | E::Intrinsic { args, .. } + | E::ResourceInvoke { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + walk_expression(value, visit)?; + } + } + E::IndirectCall { callee, args, .. } => { + for value in args { + walk_expression(value, visit)?; + } + walk_expression(callee, visit)?; + } + E::ActorMessage { actor, args, .. } => { + walk_expression(actor, visit)?; + for value in args { + walk_expression(value, visit)?; + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + walk_expression(value, visit)?; + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + walk_expression(value, visit)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + walk_expression(source, visit)?; + for value in payloads { + walk_expression(value, visit)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + walk_expression(value, visit)?; + } + } + E::MapConstruct { entries } => { + for entry in entries { + walk_expression(&mut entry.key, visit)?; + walk_expression(&mut entry.value, visit)?; + } + } + E::Handle { + target, failure, .. + } => { + walk_expression(target, visit)?; + walk_block(failure, visit)?; + } + E::StringInterpolation(parts) => { + for part in parts { + if let StringSegment::Value(value) = part { + walk_expression(value, visit)?; + } + } + } + E::InlineFunction { body, .. } => walk_block(body, visit)?, + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) +} + +#[cfg(test)] +#[path = "resource_materialization_tests.rs"] +mod tests; + +pub(crate) fn hir_blocks_equal(left: &Block, right: &Block) -> bool { + let mut left = left.clone(); + let mut right = right.clone(); + let l = clear_floats(&mut left); + let r = clear_floats(&mut right); + l == r && left == right +} diff --git a/crates/jett_hir/src/resource_materialization_tests.rs b/crates/jett_hir/src/resource_materialization_tests.rs new file mode 100644 index 00000000..dc615b18 --- /dev/null +++ b/crates/jett_hir/src/resource_materialization_tests.rs @@ -0,0 +1,604 @@ +use super::*; +#[path = "required_owner_materialization_tests.rs"] +mod required_owner; +use jett_common::SourceOrigin; +use jett_comptime::ComptimeContext; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckOptions; +use jett_types::{ReflectionMetadata, ResourceKernelRecipe}; +const SUPPORT: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const CLOSE: &str = + include_str!("../../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett"); +fn checked(source: &str, release: bool) -> Arc { + checked_support(source, SUPPORT, release) +} + +fn checked_companion(source: &str, release: bool) -> Arc { + let support = format!( + "{SUPPORT}\n{}", + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett" + ) + ); + checked_support(source, &support, release) +} + +fn checked_support( + source: &str, + support_source: &str, + release: bool, +) -> Arc { + let support = FileId::new(10_000); + let primary = FileId::new(0); + let mut parsed = jett_parser::parse(support_source, support); + let project = jett_parser::parse(source, primary); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + Item::Resource(value) => Some(value.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("one Resource declaration"); + }; + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: *declaration, + member: member.into(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(project.module.items); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (support, SourceOrigin::Stdlib), + (primary, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}: {:?}", error.diagnostics())), + ) +} +fn required(checked: &Arc) -> ExplicitComptimeValues { + let types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let result = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(ReflectionMetadata::new()), + types, + Arc::new(HashMap::new()), + ); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + assert!(result.debug_events.is_empty()); + result.values +} + +#[test] +fn required_source_hooks_and_absence_keep_original_archive_and_derive_exact_view() { + let sources = [ + CLOSE, + include_str!( + "../../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + let original = program.resource_source.functions().to_vec(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + assert!(functions_equal( + program.resource_source.functions(), + &original + )); + assert!(functions_equal( + program.resource_source.execution_functions(), + &program.functions + )); + let count = program.resource_source.required_materializations().len(); + assert!(count > 0); + if source == CLOSE { + assert!( + program + .resource_source + .required_materializations() + .iter() + .any(|row| matches!(row.current().kind, E::Int(761))) + ); + } + for row in program.resource_source.required_materializations() { + assert!(matches!( + row.original().kind, + E::Comptime { .. } | E::Constant { .. } + )); + assert_eq!( + program.resource_source.materialized_hook( + row.function(), + row.original(), + row.current() + ), + row.hook() + ); + if let Some(hook) = row.hook() { + assert!( + matches!(&row.current().kind, E::ResourceHookValue { hook: current } if current == hook) + ); + assert_eq!(row.current().ty, hook.function_type()); + assert_eq!(row.current().span, row.original().span); + } + } + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + assert_eq!( + program.resource_source.required_materializations().len(), + count + ); + } + } +} + +#[test] +fn foreign_missing_and_public_mirror_values_never_materialize_resource_authority() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let values = required(&checked); + let foreign = self::checked(CLOSE, release); + let mut program = crate::lower_checked_resource_program(&foreign).unwrap(); + assert!(program.resource_source.belongs_to(&foreign)); + assert!(!program.resource_source.belongs_to(&checked)); + let before = program.clone(); + assert!( + materialize_checked_required_values(&mut program, &values, &foreign.checked().interner) + .is_err() + ); + assert_eq!(program, before); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + let proof = values + .checked_required_values(&checked) + .unwrap() + .into_iter() + .find(|v| v.checked_hook(&checked).unwrap().is_some()) + .unwrap(); + let mut mirror = ExplicitComptimeValues::default(); + mirror.insert( + proof.source_span(), + ComptimeContext::default(), + proof.value().clone(), + ); + assert!( + materialize_checked_required_values(&mut program, &mirror, &checked.checked().interner) + .is_err() + ); + assert!( + materialize_checked_required_values( + &mut program, + &ExplicitComptimeValues::default(), + &checked.checked().interner + ) + .is_err() + ); + let mut overwritten = values.clone(); + overwritten.insert( + proof.source_span(), + ComptimeContext::default(), + Value::Int64(99), + ); + materialize_checked_required_values( + &mut program, + &overwritten, + &checked.checked().interner, + ) + .unwrap(); + assert!( + program + .resource_source + .required_materializations() + .iter() + .any(|row| row.hook().is_some()) + ); + } +} + +#[test] +fn occupied_struct_primitive_companions_hir_keeps_exact_checked_schema_and_order() { + let sources = [ + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett" + ), + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett" + ), + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett" + ), + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett" + ), + include_str!( + "../../jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked_companion(source, release); + let types = &checked.checked().interner; + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + crate::validate_backend_types(&program, types).unwrap(); + let mut constructions = Vec::new(); + for function in &mut program.functions { + walk_block(&mut function.body, &mut |expression| { + if let E::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + .. + } = &expression.kind + { + constructions.push(( + *struct_type, + fields.iter().map(|field| field.ty).collect::>(), + evaluation_order.clone(), + *validates_refinements, + )); + } + Ok(()) + }) + .unwrap(); + } + let [(owner, fields, order, validates)] = constructions.as_slice() else { + panic!("one original occupied constructor") + }; + let Type::Struct(id) = types.resolve(*owner) else { + panic!("exact nominal struct") + }; + assert_eq!( + fields, + &types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>() + ); + assert!(!validates); + assert_eq!( + order + .iter() + .copied() + .collect::>(), + (0..fields.len()).collect() + ); + if fields.len() == 14 { + assert_eq!(&order[..3], &[8, 9, 0]); + } + assert!(program.resource_source.belongs_to(&checked)); + crate::validate_backend_types(&program, types).unwrap(); + } + } +} + +#[test] +fn altered_current_bodies_headers_and_materialized_hooks_refuse_atomically() { + for release in [false, true] { + let checked = checked(CLOSE, release); + let values = required(&checked); + let original = crate::lower_checked_resource_program(&checked).unwrap(); + let main = original + .functions + .iter() + .position(|f| f.identity.declaration.name == "main") + .unwrap(); + let mut changed = original.clone(); + changed.functions[main] + .identity + .declaration + .name + .push_str("_forged"); + let before = changed.clone(); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + assert_eq!(changed, before); + let mut changed = original.clone(); + changed.functions[main].body.statements.pop(); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + let mut changed = original.clone(); + let mut replaced = false; + walk_block(&mut changed.functions[main].body, &mut |expression| { + if matches!(expression.kind, E::Comptime { .. }) && !replaced { + expression.kind = E::ResourceHookValue { + hook: original + .resource_manifest + .hooks() + .find(|h| h.recipe() == ResourceKernelRecipe::Finalize) + .unwrap(), + }; + replaced = true; + } + Ok(()) + }) + .unwrap(); + assert!(replaced); + assert!( + materialize_checked_required_values(&mut changed, &values, &checked.checked().interner) + .is_err() + ); + let mut baked = original.clone(); + materialize_checked_required_values(&mut baked, &values, &checked.checked().interner) + .unwrap(); + walk_block(&mut baked.functions[main].body, &mut |expression| { + if matches!(expression.kind, E::ResourceHookValue { .. }) { + expression.ty = TypeInterner::BOOL; + } + Ok(()) + }) + .unwrap(); + assert!( + materialize_checked_required_values(&mut baked, &values, &checked.checked().interner) + .is_err() + ); + } +} + +#[test] +fn required_only_generic_and_scoped_helpers_keep_original_body_and_exports() { + let sources = [ + include_str!( + "../../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + ), + include_str!( + "../../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let choose = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "choose") + .unwrap(); + assert!( + program + .resource_source + .required_only_function_ids() + .contains(&choose.id) + ); + assert!( + program + .resource_source + .functions() + .iter() + .any(|f| f.id == choose.id && f.body == choose.body) + ); + assert!( + program + .resource_source + .required_only_function_ids() + .iter() + .all(|id| !program.resource_source.exported_function_ids().contains(id)) + ); + let exported = source.replacen("function choose", "export function choose", 1); + let checked = self::checked(&exported, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let choose = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "choose") + .unwrap(); + assert!( + program + .resource_source + .exported_function_ids() + .contains(&choose.id) + ); + assert!( + !program + .resource_source + .required_only_function_ids() + .contains(&choose.id) + ); + } + } +} + +#[test] +fn materialized_float_authentication_retains_nan_payload_and_signed_zero_bits() { + let span = Span::new(FileId::new(0), 1, 2); + let a = Expression { + kind: E::Float(f64::from_bits(0x7ff8_0000_0000_0001)), + ty: TypeInterner::FLOAT64, + span, + }; + let mut b = a.clone(); + assert!(expression_equal(&a, &b)); + b.kind = E::Float(f64::from_bits(0x7ff8_0000_0000_0002)); + assert!(!expression_equal(&a, &b)); + b.kind = E::Float(0.0); + let mut c = b.clone(); + c.kind = E::Float(-0.0); + assert!(!expression_equal(&b, &c)); +} + +#[test] +fn actual_generic_and_nested_scope_required_nodes_join_full_context() { + let source = include_str!( + "../../jett_comptime/src/resource_execution/fixtures/27_original_generic_scoped_required.jett" + ); + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let rows = program.resource_source.required_materializations(); + let generic = rows + .iter() + .find(|row| row.proof.generic_index().is_some()) + .expect("actual generic body required value"); + let scoped = rows + .iter() + .find(|row| row.proof.scoped_bindings().len() == 2) + .expect("actual two-scope required value"); + for row in [generic, scoped] { + assert!(matches!(&row.current().kind, E::String(value) if value == "int64")); + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == row.function()) + .unwrap(); + assert!(proof_matches( + &row.proof, + function, + row.original(), + &checked, + &program.resource_source, + )); + let mut copied = row.original().clone(); + if let E::Comptime { source_span, .. } = &mut copied.kind { + source_span.start += 1; + } + assert!(!proof_matches( + &row.proof, + function, + &copied, + &checked, + &program.resource_source + )); + } + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == generic.function()) + .unwrap(); + let mut changed = function.clone(); + changed + .identity + .specialization + .type_info_kinds + .push((0, "forged".into())); + assert!(!proof_matches( + &generic.proof, + &changed, + generic.original(), + &checked, + &program.resource_source, + )); + let function = program + .resource_source + .functions() + .iter() + .find(|function| function.id == scoped.function()) + .unwrap(); + for field in [0, 1, 2] { + let mut changed = scoped.original().clone(); + let E::Comptime { scopes, .. } = &mut changed.kind else { + panic!("retained required node"); + }; + match field { + 0 => scopes[0].index += 1, + 1 => scopes[0].owner.start += 1, + _ => { + scopes[0].selection = + jett_typecheck::CheckedComptimeTypeSelection::ReflectedIteration(99) + } + } + assert!(!proof_matches( + &scoped.proof, + function, + &changed, + &checked, + &program.resource_source + )); + } + } +} + +#[test] +fn mutual_required_body_uses_retained_executable_declaration() { + let source = "namespace app\nmutual:\n function first(flag: bool) returns int64\n function second() returns int64\nfunction first(flag: bool) returns int64:\n int64 literal = comptime 7\n if flag:\n return second()\n return literal\nfunction second() returns int64:\n return first(false)\nexport function main(net: Network) returns nothing:\n int64 selected = first(true)\n return nothing\n"; + for release in [false, true] { + let checked = checked(source, release); + let values = required(&checked); + let proofs = values.checked_required_values(&checked).unwrap(); + let [proof] = proofs.as_slice() else { + panic!("one closed required literal in the mutual body"); + }; + let CheckedRequiredOwner::Function { + definition, + declaration, + } = proof.owner() + else { + panic!("actual required function owner"); + }; + assert_ne!( + checked.resolved().scope_table.def(definition).span, + declaration, + "resolver keeps the earlier mutual signature; proof retains the executable body" + ); + let mut program = crate::lower_checked_resource_program(&checked).unwrap(); + materialize_checked_required_values(&mut program, &values, &checked.checked().interner) + .unwrap(); + let [row] = program.resource_source.required_materializations() else { + panic!("one materialized required literal"); + }; + assert!(matches!(row.current().kind, E::Int(7))); + assert_eq!( + program + .functions + .iter() + .find(|function| function.id == row.function()) + .unwrap() + .source_definition, + Some(definition) + ); + } +} diff --git a/crates/jett_hir/src/resource_source.rs b/crates/jett_hir/src/resource_source.rs new file mode 100644 index 00000000..0785b405 --- /dev/null +++ b/crates/jett_hir/src/resource_source.rs @@ -0,0 +1,532 @@ +//! Original checked HIR and type meanings; raw lowering cannot mint this archive. +use crate::{ + Expression, Function, FunctionId, FunctionIdentity, Program, ReflectedFieldDispatchProof, + ReflectedTypeArm, ResourceHookRef, ResourceManifest, +}; +use jett_comptime::CheckedRequiredValue; +use jett_typecheck::CheckedResourceProgram; +use jett_types::{Type, TypeId, TypeInterner}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug)] +enum Nominal { + Struct(jett_types::StructId, jett_types::StructDef), + Bitfield(jett_types::BitfieldId, jett_types::BitfieldDef), + Enum(jett_types::EnumId, jett_types::EnumDef), + Interface(jett_types::InterfaceId, jett_types::InterfaceDef), + Actor(jett_types::ActorId, jett_types::ActorDef), + Machine(jett_types::MachineId, jett_types::MachineDef), +} +impl Nominal { + fn capture(types: &TypeInterner, ty: &Type) -> Option { + Some(match ty { + Type::Struct(id) => Self::Struct(*id, types.resolve_struct(*id).clone()), + Type::Bitfield(id) => Self::Bitfield(*id, types.resolve_bitfield(*id).clone()), + Type::Enum(id) => Self::Enum(*id, types.resolve_enum(*id).clone()), + Type::Interface(id) => Self::Interface(*id, types.resolve_interface(*id).clone()), + Type::Actor(id) => Self::Actor(*id, types.resolve_actor(*id).clone()), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + Self::Machine(*id, types.resolve_machine(*id).clone()) + } + _ => return None, + }) + } + fn unchanged(&self, types: &TypeInterner) -> bool { + match self { + Self::Struct(id, original) => types.resolve_struct(*id) == original, + Self::Bitfield(id, original) => types.resolve_bitfield(*id) == original, + Self::Enum(id, original) => types.resolve_enum(*id) == original, + Self::Interface(id, original) => types.resolve_interface(*id) == original, + Self::Actor(id, original) => types.resolve_actor(*id) == original, + Self::Machine(id, original) => types.resolve_machine(*id) == original, + } + } +} +#[derive(Debug)] +struct TypeRow { + ty: TypeId, + original: Type, + arguments: Vec, + nominal: Option, +} +#[derive(Debug)] +struct Original { + checked: Arc, + manifest: ResourceManifest, + functions: Vec, + equality_methods: HashMap, + types: Vec, + exported: Vec, + reflected_fields: Vec, + inline_sources: Vec, +} + +/// A checked required occurrence and the expression derived from its opaque value. +#[derive(Clone)] +pub struct RequiredMaterialization { + pub(crate) function: FunctionId, + pub(crate) identity: FunctionIdentity, + pub(crate) original: Expression, + pub(crate) current: Expression, + pub(crate) hook: Option, + pub(crate) proof: CheckedRequiredValue, + pub(crate) projection: Option, +} +impl std::fmt::Debug for RequiredMaterialization { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RequiredMaterialization") + .field("function", &self.function) + .field("original", &self.original) + .field("current", &self.current) + .finish() + } +} +impl RequiredMaterialization { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn current(&self) -> &Expression { + &self.current + } + pub fn hook(&self) -> Option<&ResourceHookRef> { + if let Some(projection) = &self.projection { + let hook = self.hook.as_ref()?; + let original = projection + .checked_hook(&projection.required().checked_program()) + .ok()?; + if hook.definition() != original.definition + || hook.function_type() != projection.signature() + { + return None; + } + } + self.hook.as_ref() + } +} +#[derive(Debug)] +pub(crate) struct Materialized { + pub(crate) functions: Vec, + pub(crate) values: Vec, + pub(crate) required_only: Vec, +} +/// Clone shares immutable records; empty/default supplies no Source authority. +#[derive(Clone, Default)] +pub struct ResourceSourceArchive { + data: Option>, + execution: Option>, + pub(crate) reflected_values: Option>, + pub(crate) graph_values: Option>, + pub(crate) native_suites: Option>, + pub(crate) required_cache: Option>, +} +impl std::fmt::Debug for ResourceSourceArchive { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResourceSourceArchive") + .field("authenticated", &self.data.is_some()) + .finish() + } +} +fn same_optional_arc(left: &Option>, right: &Option>) -> bool { + match (left, right) { + (None, None) => true, + (Some(left), Some(right)) => Arc::ptr_eq(left, right), + _ => false, + } +} +impl PartialEq for ResourceSourceArchive { + fn eq(&self, other: &Self) -> bool { + // Exhaustive patterns make new archive fields require an identity audit. + // Hidden views on an empty archive still participate in the comparison. + let Self { + data, + execution, + reflected_values, + graph_values, + native_suites, + required_cache, + } = self; + let Self { + data: other_data, + execution: other_execution, + reflected_values: other_reflected_values, + graph_values: other_graph_values, + native_suites: other_native_suites, + required_cache: other_required_cache, + } = other; + same_optional_arc(data, other_data) + && same_optional_arc(execution, other_execution) + && same_optional_arc(reflected_values, other_reflected_values) + && same_optional_arc(graph_values, other_graph_values) + && same_optional_arc(native_suites, other_native_suites) + && same_optional_arc(required_cache, other_required_cache) + } +} +impl Eq for ResourceSourceArchive {} +impl ResourceSourceArchive { + pub fn empty() -> Self { + Self::default() + } + // Called only at successful completion of the original checked lowering. + pub(crate) fn checked( + program: &Program, + checked: &Arc, + types: &TypeInterner, + inline_sources: Vec, + ) -> Self { + let rows = types + .type_ids() + .map(|ty| TypeRow { + ty, + original: types.resolve(ty).clone(), + arguments: types.nominal_type_arguments(ty).to_vec(), + nominal: Nominal::capture(types, types.resolve(ty)), + }) + .collect(); + Self { + data: Some(Arc::new(Original { + checked: checked.clone(), + manifest: program.resource_manifest.clone(), + functions: program.functions.clone(), + equality_methods: program.equality_methods.clone(), + types: rows, + exported: super::resource_materialization::exported_functions(program, checked), + reflected_fields: super::reflected_field_sources::capture(program, checked), + inline_sources, + })), + execution: None, + reflected_values: None, + graph_values: None, + native_suites: None, + required_cache: None, + } + } + /// Original checked functions stay immutable; this view contains only checked replacements. + pub fn execution_functions(&self) -> &[Function] { + if let Some(suites) = &self.native_suites { + return suites.functions(); + } + if let Some(view) = &self.execution { + return view.functions.as_slice(); + } + self.reflected_values.as_ref().map_or_else( + || self.functions(), + |transition| transition.current_functions(), + ) + } + /// Exact checked session identity; equal source text cannot replace it. + pub fn belongs_to(&self, checked: &Arc) -> bool { + self.data + .as_ref() + .is_some_and(|data| Arc::ptr_eq(&data.checked, checked)) + } + pub fn required_materializations(&self) -> &[RequiredMaterialization] { + self.execution + .as_ref() + .map_or(&[], |view| view.values.as_slice()) + } + /// Graph metadata handlers are selected through the retained constructor's + /// actor declaration and handler spans, never a current callable type/name. + pub fn graph_metadata_actor_handlers(&self) -> Vec { + let Some(data) = &self.data else { + return Vec::new(); + }; + let mut spans = Vec::new(); + for row in self.required_materializations() { + let Some(graph) = row.proof.actor_graph() else { + continue; + }; + if !row.proof.belongs_to(&data.checked) || !graph.belongs_to(&data.checked) { + continue; + } + for node in graph.nodes() { + let name = data.checked.checked().interner.type_name(node.ty()); + for item in &data.checked.module().items { + let jett_parser::ast::Item::Actor(actor) = item else { + continue; + }; + let declarations = data + .checked + .resolved() + .scope_table + .definitions + .iter() + .filter(|definition| { + definition.kind == jett_resolve::DefKind::Actor + && definition.span == actor.name.span + && definition.name == name + }) + .count(); + if declarations == 1 { + spans.extend(actor.handlers.iter().map(|handler| handler.span)); + } + } + } + } + self.execution_functions() + .iter() + .filter(|function| { + function.identity.declaration.kind == crate::DeclarationKind::ActorHandler + && spans.contains(&function.span) + }) + .map(|function| function.id) + .collect() + } + pub fn required_only_function_ids(&self) -> &[FunctionId] { + self.execution + .as_ref() + .map_or(&[], |view| view.required_only.as_slice()) + } + pub fn exported_function_ids(&self) -> &[FunctionId] { + self.data + .as_ref() + .map_or(&[], |data| data.exported.as_slice()) + } + pub fn materialized_hook( + &self, + function: FunctionId, + original: &Expression, + current: &Expression, + ) -> Option<&ResourceHookRef> { + self.required_materializations() + .iter() + .find(|row| { + row.function == function + && super::resource_materialization::expression_equal(&row.original, original) + && super::resource_materialization::expression_equal(&row.current, current) + })? + .hook() + } + pub(crate) fn checked_program(&self) -> Option<&Arc> { + self.data.as_ref().map(|data| &data.checked) + } + /// Bind an extracted function to its physical checked inline node and + /// retained enclosing callable. All records are minted during original lowering. + pub(crate) fn original_inline_root( + &self, + function: &Function, + expression: &Expression, + source: &jett_parser::ast::Expr, + checked: &Arc, + ) -> Option<&Function> { + let data = self.data.as_ref()?; + if !Arc::ptr_eq(&data.checked, checked) { + return None; + } + let mut matches = data.inline_sources.iter().filter(|row| { + row.function == function.id && row.source_node == std::ptr::from_ref(source).addr() + }); + let row = matches.next()?; + if matches.next().is_some() { + return None; + } + self.original_required_function(function, expression, checked)?; + data.functions.iter().find(|f| f.id == row.root) + } + + pub(crate) fn original_required_function( + &self, + function: &Function, + expression: &Expression, + checked: &Arc, + ) -> Option<&Function> { + let data = self.data.as_ref()?; + if !Arc::ptr_eq(&data.checked, checked) { + return None; + } + let original = data.functions.iter().find(|f| f.id == function.id)?; + // The caller separately authenticates the complete table transition. + // This join additionally checks the entire extracted header and exact + // original operand; modified bodies cannot forge extraction provenance. + let mut header = function.clone(); + header.body = original.body.clone(); + if !super::resource_materialization::functions_equal( + std::slice::from_ref(&header), + std::slice::from_ref(original), + ) { + return None; + } + let mut body = original.body.clone(); + let mut occurrences = 0; + super::resource_materialization::walk_block(&mut body, &mut |candidate| { + if super::resource_materialization::expression_equal(candidate, expression) { + occurrences += 1; + } + Ok(()) + }) + .ok()?; + if occurrences != 1 { + return None; + } + Some(original) + } + pub(crate) fn install_materialized(&mut self, view: Materialized) { + self.execution = Some(Arc::new(view)); + } + /// A constructor-authenticated generated suite, distinct from original Source. + pub fn native_suite_function(&self, id: FunctionId) -> Option<&Function> { + self.native_suites.as_ref()?.function(id) + } + /// Exact whole-wrapper preorder occurrence; duplicate trial values stay distinct. + pub fn native_suite_call( + &self, + owner: FunctionId, + expression: &Expression, + ordinal: usize, + ) -> Option<&crate::CheckedNativeSuiteCall> { + self.native_suites + .as_ref()? + .call(owner, expression, ordinal) + } + pub fn functions(&self) -> &[Function] { + self.data + .as_ref() + .map_or(&[], |data| data.functions.as_slice()) + } + pub fn manifest(&self) -> Option<&ResourceManifest> { + self.data.as_ref().map(|data| &data.manifest) + } + pub fn equality_methods(&self) -> Option<&HashMap> { + self.data.as_ref().map(|data| &data.equality_methods) + } + /// Readonly eligibility for an original reflected-field proof-only witness. + /// This does not admit operands or grant a Resource execution family or scope. + pub fn has_reflected_field_dispatch(&self, function: FunctionId) -> bool { + self.data.as_ref().is_some_and(|data| { + data.reflected_fields.iter().any(|proof| { + proof.function() == function + && proof.belongs_to(&data.checked) + && proof.validate_original() + }) + }) + } + + /// Exact original dispatch operands select a constructor-only Source proof. + /// Runtime TypeInfo values, field names, and caller-made arms grant no authority. + pub fn get_reflected_field_dispatch( + &self, + function: FunctionId, + type_info: &Expression, + arms: &[ReflectedTypeArm], + ) -> Option<&ReflectedFieldDispatchProof> { + let data = self.data.as_ref()?; + let mut matches = data.reflected_fields.iter().filter(|proof| { + proof.function() == function + && proof.belongs_to(&data.checked) + && proof.matches(data.functions.as_slice(), type_info, arms) + }); + let proof = matches.next()?; + matches.next().is_none().then_some(proof) + } + + /// Native normalization starts only from the unchanged checked HIR view. + pub fn validate_current(&self, program: &Program, types: &TypeInterner) -> Result<(), String> { + self.validate_types(types)?; + let data = self + .data + .as_ref() + .ok_or("Resource ownership has no original checked HIR archive")?; + if self != &program.resource_source + || self.manifest() != Some(&program.resource_manifest) + || self.equality_methods() != Some(&program.equality_methods) + || !super::resource_materialization::functions_equal( + self.execution_functions(), + &program.functions, + ) + || data + .reflected_fields + .iter() + .any(|proof| !proof.belongs_to(&data.checked) || !proof.validate_original()) + { + return Err( + "Resource original checked HIR or reflected-field Source proof changed".into(), + ); + } + Ok(()) + } + + /// Additional canonical IDs cannot alter any original type's meaning. + /// Structural/backend type validation remains mandatory before this check. + pub fn validate_types(&self, types: &TypeInterner) -> Result<(), String> { + let data = self + .data + .as_ref() + .ok_or("Resource ownership has no original checked HIR archive")?; + if types.len() < data.types.len() { + return Err("Resource original checked type archive lost an original TypeId".into()); + } + for row in &data.types { + if types.resolve(row.ty) != &row.original + || types.nominal_type_arguments(row.ty) != row.arguments.as_slice() + { + return Err( + "Resource original checked type meaning or nominal metadata changed".into(), + ); + } + if !types.contains_nominal_definition(types.resolve(row.ty)) { + return Err( + "Resource original checked type archive has a missing nominal definition" + .into(), + ); + } + if row + .nominal + .as_ref() + .is_some_and(|nominal| !nominal.unchanged(types)) + { + return Err( + "Resource original checked type meaning or nominal metadata changed".into(), + ); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + #[test] + fn original_unused_nominal_row_with_missing_table_returns_error() { + for release in [false, true] { + let file = FileId::new(0); + let original = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse( + "function main() returns nothing:\n return nothing\n", + file, + ), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ); + let program = crate::lower_checked_resource_program(&original).unwrap(); + let types = &original.checked().interner; + assert!( + types + .type_ids() + .any(|ty| matches!(types.resolve(ty), Type::Enum(_))), + "original unused reflection enum row" + ); + let mut missing = TypeInterner::new(); + for ty in types.type_ids() { + assert_eq!(missing.intern(types.resolve(ty).clone()), ty); + } + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + program.resource_source.validate_types(&missing) + })); + assert_eq!( + result.unwrap().unwrap_err(), + "Resource original checked type archive has a missing nominal definition" + ); + } + } +} diff --git a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap index 625c6a53..f7725902 100644 --- a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap +++ b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_core.snap @@ -1,14 +1,22 @@ --- source: crates/jett_hir/src/lib.rs +assertion_line: 8071 expression: program --- Program { + resource_manifest: ResourceManifest { + data: None, + }, + resource_source: ResourceSourceArchive { + authenticated: false, + }, functions: [ Function { id: FunctionId( 0, ), identity: FunctionIdentity { + scoped_type_bindings: [], declaration: DeclarationId { origin: Project, namespace: "app", @@ -16,12 +24,26 @@ Program { kind: Function, }, type_arguments: [], + specialization: CheckedGenericSpecialization { + type_argument_kinds: [], + type_argument_reflections: [], + type_info_kinds: [], + type_info_primitives: [], + type_kind_values: [], + type_primitive_values: [], + }, }, + debug_kind: Named( + "app.add", + ), source_definition: Some( DefId( 51, ), ), + actor_handler: None, + actor_state_views: [], + mutable_sum_backings: [], params: [ Param { local: LocalId( @@ -60,6 +82,7 @@ Program { }, }, ], + capture_count: 0, return_type: TypeId( 3, ), @@ -72,7 +95,14 @@ Program { ty: TypeId( 3, ), + debug_ty: TypeId( + 3, + ), + debug_type_name: Some( + "int64", + ), mutable: false, + view_source: None, span: Span { file: FileId( 0, @@ -89,7 +119,14 @@ Program { ty: TypeId( 3, ), + debug_ty: TypeId( + 3, + ), + debug_type_name: Some( + "int64", + ), mutable: false, + view_source: None, span: Span { file: FileId( 0, @@ -185,6 +222,7 @@ Program { 1, ), identity: FunctionIdentity { + scoped_type_bindings: [], declaration: DeclarationId { origin: Project, namespace: "app", @@ -192,13 +230,28 @@ Program { kind: Function, }, type_arguments: [], + specialization: CheckedGenericSpecialization { + type_argument_kinds: [], + type_argument_reflections: [], + type_info_kinds: [], + type_info_primitives: [], + type_kind_values: [], + type_primitive_values: [], + }, }, + debug_kind: Named( + "app.main", + ), source_definition: Some( DefId( 52, ), ), + actor_handler: None, + actor_state_views: [], + mutable_sum_backings: [], params: [], + capture_count: 0, return_type: TypeId( 3, ), @@ -211,7 +264,14 @@ Program { ty: TypeId( 3, ), + debug_ty: TypeId( + 3, + ), + debug_type_name: Some( + "int64", + ), mutable: false, + view_source: None, span: Span { file: FileId( 0, @@ -269,6 +329,152 @@ Program { 1, 0, ], + ownership: Source( + SourceCallOwnership { + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 47, + ), + }, + context: Ordinary, + arguments: [ + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 162, + end: 163, + }, + source_index: 1, + parameter_index: 0, + actual_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 162, + end: 163, + }, + source_index: 1, + parameter_index: 0, + actual_type: TypeId( + 3, + ), + occurrence_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + match_origin: None, + temporary_origin: None, + }, + retained_snapshot: None, + }, + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 153, + end: 154, + }, + source_index: 0, + parameter_index: 1, + actual_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 153, + end: 154, + }, + source_index: 0, + parameter_index: 1, + actual_type: TypeId( + 3, + ), + occurrence_type: TypeId( + 3, + ), + parameter_type: TypeId( + 3, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + match_origin: None, + temporary_origin: None, + }, + retained_snapshot: None, + }, + ], + bridge: Direct, + generated_operands: [], + certificate: SourceCallCertificate { + result_type: TypeId( + 3, + ), + generated_operands: [], + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 47, + ), + }, + context: Ordinary, + bridge: Direct, + }, + temporary_local_backing: None, + }, + ), }, ty: TypeId( 3, @@ -436,4 +642,5 @@ Program { }, }, ], + equality_methods: {}, } diff --git a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap index fde53088..86264d3f 100644 --- a/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap +++ b/crates/jett_hir/src/snapshots/jett_hir__tests__hir_enum_handle.snap @@ -1,14 +1,22 @@ --- source: crates/jett_hir/src/lib.rs +assertion_line: 8093 expression: program --- Program { + resource_manifest: ResourceManifest { + data: None, + }, + resource_source: ResourceSourceArchive { + authenticated: false, + }, functions: [ Function { id: FunctionId( 0, ), identity: FunctionIdentity { + scoped_type_bindings: [], declaration: DeclarationId { origin: Project, namespace: "app", @@ -16,12 +24,26 @@ Program { kind: Function, }, type_arguments: [], + specialization: CheckedGenericSpecialization { + type_argument_kinds: [], + type_argument_reflections: [], + type_info_kinds: [], + type_info_primitives: [], + type_kind_values: [], + type_primitive_values: [], + }, }, + debug_kind: Named( + "app.parse", + ), source_definition: Some( DefId( 52, ), ), + actor_handler: None, + actor_state_views: [], + mutable_sum_backings: [], params: [ Param { local: LocalId( @@ -42,8 +64,9 @@ Program { }, }, ], + capture_count: 0, return_type: TypeId( - 36, + 48, ), locals: [ Local { @@ -54,7 +77,14 @@ Program { ty: TypeId( 10, ), + debug_ty: TypeId( + 10, + ), + debug_type_name: Some( + "string", + ), mutable: false, + view_source: None, span: Span { file: FileId( 0, @@ -74,7 +104,7 @@ Program { Expression { kind: EnumConstruct { enum_type: TypeId( - 35, + 47, ), variant: VariantId( 0, @@ -96,9 +126,12 @@ Program { }, }, ], + evaluation_order: [ + 0, + ], }, ty: TypeId( - 35, + 47, ), span: Span { file: FileId( @@ -110,7 +143,7 @@ Program { }, ), ty: TypeId( - 36, + 48, ), span: Span { file: FileId( @@ -152,6 +185,7 @@ Program { 1, ), identity: FunctionIdentity { + scoped_type_bindings: [], declaration: DeclarationId { origin: Project, namespace: "app", @@ -159,13 +193,28 @@ Program { kind: Function, }, type_arguments: [], + specialization: CheckedGenericSpecialization { + type_argument_kinds: [], + type_argument_reflections: [], + type_info_kinds: [], + type_info_primitives: [], + type_kind_values: [], + type_primitive_values: [], + }, }, + debug_kind: Named( + "app.main", + ), source_definition: Some( DefId( 53, ), ), + actor_handler: None, + actor_state_views: [], + mutable_sum_backings: [], params: [], + capture_count: 0, return_type: TypeId( 3, ), @@ -174,34 +223,46 @@ Program { id: LocalId( 0, ), - name: "value", + name: "error", ty: TypeId( - 35, + 10, ), + debug_ty: TypeId( + 10, + ), + debug_type_name: None, mutable: false, + view_source: None, span: Span { file: FileId( 0, ), - start: 188, - end: 261, + start: 220, + end: 225, }, }, Local { id: LocalId( 1, ), - name: "error", + name: "value", ty: TypeId( - 10, + 47, + ), + debug_ty: TypeId( + 47, + ), + debug_type_name: Some( + "app.Value", ), mutable: false, + view_source: None, span: Span { file: FileId( 0, ), - start: 220, - end: 225, + start: 188, + end: 261, }, }, Local { @@ -212,7 +273,12 @@ Program { ty: TypeId( 3, ), + debug_ty: TypeId( + 3, + ), + debug_type_name: None, mutable: false, + view_source: None, span: Span { file: FileId( 0, @@ -227,7 +293,7 @@ Program { Statement { kind: Let { local: LocalId( - 0, + 1, ), value: Expression { kind: Handle { @@ -256,9 +322,101 @@ Program { evaluation_order: [ 0, ], + ownership: Source( + SourceCallOwnership { + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 49, + ), + }, + context: Ordinary, + arguments: [ + ArgumentOwnership { + source_span: Span { + file: FileId( + 0, + ), + start: 208, + end: 211, + }, + source_index: 0, + parameter_index: 0, + actual_type: TypeId( + 10, + ), + parameter_type: TypeId( + 10, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + physical_access: Owned, + effect: Copy, + staging: Original, + witness: SourceArgumentWitness { + source_span: Span { + file: FileId( + 0, + ), + start: 208, + end: 211, + }, + source_index: 0, + parameter_index: 0, + actual_type: TypeId( + 10, + ), + occurrence_type: TypeId( + 10, + ), + parameter_type: TypeId( + 10, + ), + syntax: Bare, + origin: OwnedExpression, + callee_access: Owned, + effect: Copy, + context: Ordinary, + observation: None, + handled_result: None, + projection_root: None, + match_origin: None, + temporary_origin: None, + }, + retained_snapshot: None, + }, + ], + bridge: Direct, + generated_operands: [], + certificate: SourceCallCertificate { + result_type: TypeId( + 48, + ), + generated_operands: [], + target: Function( + FunctionId( + 0, + ), + ), + shape: Function { + signature_type: TypeId( + 49, + ), + }, + context: Ordinary, + bridge: Direct, + }, + temporary_local_backing: None, + }, + ), }, ty: TypeId( - 36, + 48, ), span: Span { file: FileId( @@ -271,7 +429,7 @@ Program { kind: Result, error_local: Some( LocalId( - 1, + 0, ), ), failure: Block { @@ -281,15 +439,16 @@ Program { Expression { kind: EnumConstruct { enum_type: TypeId( - 35, + 47, ), variant: VariantId( 1, ), payloads: [], + evaluation_order: [], }, ty: TypeId( - 35, + 47, ), span: Span { file: FileId( @@ -319,7 +478,7 @@ Program { }, }, ty: TypeId( - 35, + 47, ), span: Span { file: FileId( @@ -343,11 +502,11 @@ Program { scrutinee: Expression { kind: Local( LocalId( - 0, + 1, ), ), ty: TypeId( - 35, + 47, ), span: Span { file: FileId( @@ -500,4 +659,5 @@ Program { }, }, ], + equality_methods: {}, } diff --git a/crates/jett_hir/src/temporary_initial_finalization.rs b/crates/jett_hir/src/temporary_initial_finalization.rs new file mode 100644 index 00000000..e3fe91c4 --- /dev/null +++ b/crates/jett_hir/src/temporary_initial_finalization.rs @@ -0,0 +1,408 @@ +//! Original-only finalization of unchanged retained temporary producers. +//! The callable receipt proves table edits; this producer proves cell states. +use super::*; +use crate::reflected_callable::CheckedCallableSealing; + +/// Mint-time inventory of one unpublished Lowerer construction. Never cloned, +/// reconstructed from a Program, or returned by a public lowering entry point. +pub(crate) struct Construction { + module_address: usize, + check_address: usize, + records: Vec, +} +impl Construction { + pub(crate) fn new(module: &Module, check: &CheckResult) -> Self { + Self { + module_address: std::ptr::from_ref(module).addr(), + check_address: std::ptr::from_ref(check).addr(), + records: Vec::new(), + } + } + pub(super) fn register(&mut self, proof: TemporaryLocalBacking) { + self.records.push(proof); + } + fn context( + &self, + checked: &Arc, + types: &TypeInterner, + ) -> Result<(), String> { + if self.module_address != std::ptr::from_ref(checked.module()).addr() + || self.check_address != std::ptr::from_ref(checked.checked()).addr() + || !std::ptr::eq(types, &checked.checked().interner) + { + return Err("temporary original finalization has another fresh construction, checked context or arena".into()); + } + Ok(()) + } +} + +#[derive(Clone)] +struct Row { + function: FunctionId, + proof: TemporaryLocalBacking, + canonical: Expression, +} +pub(crate) struct Prepared { + construction: Construction, + checked: Arc, + sources: Arc>, + manifest: ResourceManifest, + equality: HashMap, + rows: Vec, +} +pub(crate) struct Completed { + construction: Construction, + checked: Arc, + rows: Vec, +} + +fn inventory( + program: &Program, + construction: &Construction, + checked: &Arc, + types: &TypeInterner, +) -> Result, String> { + construction.context(checked, types)?; + let mut result: Vec = Vec::new(); + for function in &program.functions { + for proof in markers(function) { + if !Arc::ptr_eq(&proof.0.checked, checked) + || construction + .records + .iter() + .filter(|record| *record == &proof) + .count() + != 1 + || result.iter().any(|row| { + row.proof == proof + || (row.function == function.id + && [row.proof.owner(), row.proof.alias()] + .iter() + .any(|id| *id == proof.owner() || *id == proof.alias())) + }) + { + return Err( + "temporary original finalization changed its exact minted marker inventory" + .into(), + ); + } + let canonical = canonical_production(function, &proof, types)?; + result.push(Row { + function: function.id, + proof, + canonical, + }); + } + } + if result.len() != construction.records.len() + || construction + .records + .iter() + .any(|proof| result.iter().filter(|row| &row.proof == proof).count() != 1) + { + return Err("temporary original finalization lost a minted declaration".into()); + } + Ok(result) +} +fn same_rows(left: &[Row], right: &[Row]) -> bool { + left.len() == right.len() + && left.iter().zip(right).all(|(a, b)| { + a.function == b.function + && a.proof == b.proof + && resource_materialization::expression_equal(&a.canonical, &b.canonical) + }) +} +fn open(rows: &[Row]) -> Result<(), String> { + if rows + .iter() + .any(|row| row.proof.0.canonical_producer.get().is_some()) + { + return Err("temporary original finalization requires every exact cell to be open".into()); + } + Ok(()) +} +fn bound_callable(plan: &ReflectedCallablePlan) -> Result<(), String> { + if plan.is_sealed() || plan.is_materialized() || plan.graph_source().is_some() { + return Err( + "temporary original canonical payload contains an unrepresented callable epoch".into(), + ); + } + bound_field(plan.return_plan()) +} +fn bound_field(plan: &ReflectedFieldPlan) -> Result<(), String> { + match &plan.action { + ReflectedFieldAction::Exact + | ReflectedFieldAction::Predicates(_) + | ReflectedFieldAction::Unsupported(_) => Ok(()), + ReflectedFieldAction::CallableReturn(plan) => bound_callable(plan), + ReflectedFieldAction::Refine { base, .. } + | ReflectedFieldAction::List(base) + | ReflectedFieldAction::Set(base) + | ReflectedFieldAction::Optional(base) => bound_field(base), + ReflectedFieldAction::Map { key, value } => { + bound_field(key)?; + bound_field(value) + } + ReflectedFieldAction::Result { ok, error } => { + bound_field(ok)?; + bound_field(error) + } + } +} +fn argument_proof(argument: &TemporaryLocalArgument) -> Result { + if argument.owner != argument.proof.owner() || argument.alias != argument.proof.alias() { + return Err("temporary original argument proof changed owner or alias identity".into()); + } + Ok(argument.proof.clone()) +} +fn payload_dependencies(value: &Expression) -> Result, String> { + let mut dependencies = Vec::new(); + let mut block = Block { + statements: vec![Statement { + kind: StatementKind::Expression(value.clone()), + span: value.span, + }], + span: value.span, + }; + resource_materialization::walk_block(&mut block, &mut |value| { + if let Some(proof) = marker(value) { + dependencies.push(proof.clone()); + } + let ownership = match &value.kind { + ExpressionKind::Call { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(CallOwnership::Source(source)) = ownership { + for argument in &source.arguments { + if let Some(argument) = argument.source_witness().temporary_local_origin() { + dependencies.push(argument_proof(argument)?); + } + } + } + match &value.kind { + ExpressionKind::ActorGraphImport(_) | ExpressionKind::ActorGraphStep { .. } => { + return Err( + "temporary original canonical payload contains an unrepresented Graph epoch" + .into(), + ); + } + ExpressionKind::Intrinsic { + field_validation: Some(validation), + .. + } => match validation { + ReflectedFieldValidation::Validate(plans) => { + for plan in plans { + bound_field(plan)?; + } + } + _ => { + return Err( + "temporary original canonical payload acquired a MIR selector".into(), + ); + } + }, + ExpressionKind::Handle { + kind: HandleKind::ReflectedReturn { plan, callable }, + .. + } => { + bound_field(plan)?; + if let Some(callable) = callable { + bound_callable(callable)?; + } + } + ExpressionKind::InterfaceCoerce { adapters, .. } => { + for adapter in adapters { + if let Some(plan) = &adapter.reflected { + bound_callable(plan)?; + } + } + } + _ => {} + } + Ok(()) + })?; + let mut unique = Vec::new(); + for proof in dependencies { + if !unique.contains(&proof) { + unique.push(proof); + } + } + Ok(unique) +} +fn dependency(row: &Row, predecessor: &TemporaryLocalBacking, rows: &[Row]) -> Result<(), String> { + let selected = rows + .iter() + .filter(|candidate| candidate.proof == *predecessor) + .collect::>(); + let [earlier] = selected.as_slice() else { + return Err( + "temporary original proof depends on an absent or foreign minted declaration".into(), + ); + }; + if earlier.function != row.function + || !Arc::ptr_eq(&earlier.proof.0.checked, &row.proof.0.checked) + || earlier.proof.0.scope.body != row.proof.0.scope.body + || earlier.proof.0.scope.statement >= row.proof.0.scope.statement + { + return Err( + "temporary original proof is not a strictly earlier same-body declaration".into(), + ); + } + Ok(()) +} +fn closed(rows: &[Row]) -> Result<(), String> { + for row in rows { + for payload in [ + &row.proof.0.initializer, + &row.proof.0.producer, + &row.proof.0.rewritten, + &row.canonical, + ] { + for predecessor in payload_dependencies(payload)? { + dependency(row, &predecessor, rows)?; + } + } + } + Ok(()) +} + +/// No writes: retain only the genuine receipt's BEFORE canonical values. +pub(crate) fn prepare( + before: &Program, + output: &Program, + types: &TypeInterner, + receipt: &CheckedCallableSealing<'_>, + checked: &Arc, + construction: Construction, +) -> Result { + receipt.authenticate_output(before, output, types)?; + if before.resource_source != ResourceSourceArchive::empty() + || !receipt + .checked() + .is_some_and(|owner| Arc::ptr_eq(owner, checked)) + { + return Err("temporary original finalization has no exact initial callable receipt".into()); + } + let rows = inventory(before, &construction, checked, types)?; + let produced = inventory(output, &construction, checked, types)?; + open(&rows)?; + if rows.is_empty() || !same_rows(&rows, &produced) { + return Err("temporary original producer changed during callable sealing; current-canonical renewal remains unsupported".into()); + } + closed(&rows)?; + let prepared = Prepared { + construction, + checked: checked.clone(), + sources: receipt.source_epoch().clone(), + manifest: before.resource_manifest.clone(), + equality: before.equality_methods.clone(), + rows, + }; + #[cfg(test)] + tests::observe(&prepared, before, output, types, receipt); + Ok(prepared) +} +impl Prepared { + /// The caller is the actual initial actor finish, after its exact full peer + /// checkpoint and real sum producer have passed. No current body is stored. + fn preflight_commit( + &self, + program: &Program, + types: &TypeInterner, + sources: &Arc>, + checkpoint: &crate::actor_handlers::InitialPeerCheckpoint, + ) -> Result<(), String> { + checkpoint.authenticate(program, types, sources, &self.checked)?; + if !Arc::ptr_eq(sources, &self.sources) + || program.resource_source != ResourceSourceArchive::empty() + || program.resource_manifest != self.manifest + || program.equality_methods != self.equality + { + return Err("temporary original finalization lost its exact initial peer epoch".into()); + } + let current = inventory(program, &self.construction, &self.checked, types)?; + if !same_rows(&self.rows, ¤t) { + return Err( + "temporary original finalization changed its retained canonical production".into(), + ); + } + open(&self.rows)?; + closed(&self.rows)?; + Ok(()) + } + pub(crate) fn commit( + self, + program: &Program, + types: &TypeInterner, + sources: &Arc>, + checkpoint: &crate::actor_handlers::InitialPeerCheckpoint, + ) -> Result { + self.preflight_commit(program, types, sources, checkpoint)?; + #[cfg(test)] + tests::observe_commit(&self, program, types, sources, checkpoint); + // The owned mint-time capability has no public escape or other setter. + // All cells and payloads were preflighted before this first write. + for row in &self.rows { + row.proof + .0 + .canonical_producer + .set(row.canonical.clone()) + .map_err(|_| { + "unpublished temporary original finalization lost its sole setter".to_owned() + })?; + } + Ok(Completed { + construction: self.construction, + checked: self.checked, + rows: self.rows, + }) + } +} + +/// Consume the original-only capability exactly once. Ordinary/no-delta paths +/// retain the existing late canonical producer; only a genuine completed cell +/// transition can skip it. No supplied bool or prefilled-cell fallback exists. +pub(crate) fn finish_fresh( + program: &Program, + types: &TypeInterner, + construction: Option, + completed: Option, +) -> Result<(), String> { + match (construction, completed) { + (Some(construction), None) => { + if let Some(proof) = construction.records.first() { + let rows = inventory(program, &construction, &proof.0.checked, types)?; + open(&rows)?; + } else if has_markers(program) { + return Err( + "temporary original finalization imported an unregistered marker".into(), + ); + } + seal_original_program(program, types) + .map_err(|errors| format!("temporary original finalization: {errors:?}")) + } + (None, Some(completed)) => { + let current = inventory(program, &completed.construction, &completed.checked, types)?; + if !same_rows(&completed.rows, ¤t) + || completed.rows.iter().any(|row| { + !row.proof.0.canonical_producer.get().is_some_and(|value| { + resource_materialization::expression_equal(value, &row.canonical) + }) + }) + { + return Err("temporary original completion lost its exact filled cells or canonical payloads".into()); + } + Ok(()) + } + _ => Err( + "temporary original finalization has no single consumed construction capability".into(), + ), + } +} + +#[cfg(test)] +#[path = "temporary_initial_finalization_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/temporary_initial_finalization_tests.rs b/crates/jett_hir/src/temporary_initial_finalization_tests.rs new file mode 100644 index 00000000..dd0fd07e --- /dev/null +++ b/crates/jett_hir/src/temporary_initial_finalization_tests.rs @@ -0,0 +1,543 @@ +//! Controls observe the real shared sealer and fresh Lowerer preparation cut. +//! No positive receipt, open marker, or construction is supplied by a test. +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; + +const SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +struct Packet: + data: list[int64] + sibling: list[int64] +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +function make_packet() returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + list[int64] borrowed = view make_packet().data + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing +"#; +const CHAIN_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +struct Packet: + data: list[int64] + sibling: list[int64] +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +function make_packet() returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +function next_packet(view first: list[int64], view again: list[int64]) returns Packet: + return Packet(data: clone first, sibling: clone again) +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + list[int64] borrowed = view make_packet().data + list[int64] second = view next_packet(view borrowed, view borrowed).data + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing +"#; + +const INTERFACE_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "number" +struct Holder: + callback: function() returns int64 +struct Packet: + data: list[int64] + sibling: list[int64] +function five() returns int64: + return 5 +function fallback() returns Positive: + return 7 +function make_packet(view input: Named) returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +actor Worker: + receive read responds int64: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = fallback + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + int64 value = coarsen selected() + respond value +function main(stdout: Stdout) returns nothing: + int64 number = 7 + list[int64] borrowed = view make_packet(view number).data + Worker worker = spawn Worker() + int64 observed = ask worker.read + Stdout.write(view stdout, "observed:{observed}\n") + return nothing +"#; + +#[derive(Clone, Copy)] +enum Case { + State, + Context, + Dependencies, + Cells, + Payload, + Coercion, +} +std::thread_local! { + static CASE: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static HITS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static COMMITS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} +fn observed(case: Case, source: &str, release: bool) -> (Program, Arc) { + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + CASE.with(|v| v.set(None)); + } + } + CASE.with(|v| assert!(v.replace(Some(case)).is_none())); + HITS.with(|v| v.set(0)); + COMMITS.with(|v| v.set(0)); + let reset = Reset; + let result = high(source, release); + assert_eq!( + HITS.with(|v| v.get()), + 1, + "real original preparation observed exactly once" + ); + assert_eq!( + COMMITS.with(|v| v.get()), + 1, + "real original commit observed exactly once" + ); + drop(reset); + result +} +fn checked(source: &str, release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("complete genuine Source must check: {error:?}")), + ) +} +fn high(source: &str, release: bool) -> (Program, Arc) { + let checked = checked(source, release); + let program = crate::lower_checked_ordinary_program(&checked) + .unwrap_or_else(|errors| panic!("real initial lowering; release={release}: {errors:?}")); + crate::validate(&program).unwrap(); + crate::validate_backend_types(&program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + (program, checked) +} +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "main") + .unwrap() +} +fn unsealed_copy(proof: &TemporaryLocalBacking) -> TemporaryLocalBacking { + let d = &proof.0; + TemporaryLocalBacking(Arc::new(Original { + checked: d.checked.clone(), + declaration: d.declaration, + declaration_address: d.declaration_address, + owner: d.owner.clone(), + alias: d.alias.clone(), + scope: TemporaryLocalScope { + body: d.scope.body, + statement: d.scope.statement, + }, + initializer: d.initializer.clone(), + producer: d.producer.clone(), + canonical_producer: OnceLock::new(), + rewritten: d.rewritten.clone(), + required_scopes: d.required_scopes.clone(), + scoped_types: d.scoped_types.clone(), + })) +} + +/// This test-only observer has no production constructor or setter. It borrows +/// only a genuinely prepared real receipt before the actual producer commits. +pub(super) fn observe( + prepared: &Prepared, + before: &Program, + output: &Program, + types: &TypeInterner, + receipt: &CheckedCallableSealing<'_>, +) { + let Some(case) = CASE.with(|v| v.take()) else { + return; + }; + open(&prepared.rows).unwrap(); + assert!(Arc::ptr_eq(&prepared.sources, receipt.source_epoch())); + let current = inventory(output, &prepared.construction, &prepared.checked, types).unwrap(); + assert!(same_rows(&prepared.rows, ¤t)); + match case { + Case::State => { + assert_eq!(prepared.rows.len(), 1); + assert!(marker(&prepared.rows[0].canonical).is_none()); + assert!( + payload_dependencies(&prepared.rows[0].canonical) + .unwrap() + .is_empty() + ); + assert_eq!(prepared.construction.records.len(), 1); + closed(&prepared.rows).unwrap(); + } + Case::Context => { + let foreign = checked(SOURCE, false); + assert!(!Arc::ptr_eq(&foreign, &prepared.checked)); + assert!(!std::ptr::eq(types, &foreign.checked().interner)); + assert!( + inventory( + before, + &prepared.construction, + &foreign, + &foreign.checked().interner + ) + .is_err() + ); + assert!( + receipt + .authenticate_output(before, output, &foreign.checked().interner) + .is_err() + ); + let empty = Construction::new(prepared.checked.module(), prepared.checked.checked()); + assert!(prepare(before, output, types, receipt, &prepared.checked, empty).is_err()); + for input in [true, false] { + let mut changed = if input { + before.clone() + } else { + output.clone() + }; + changed.functions[0].span.end += 1; + assert!( + receipt + .authenticate_output( + if input { &changed } else { before }, + if input { output } else { &changed }, + types, + ) + .is_err() + ); + } + let mut missing = before.clone(); + let owner = missing + .functions + .iter_mut() + .find(|f| f.id == prepared.rows[0].function) + .unwrap(); + owner.body.statements.remove(0); + assert!(inventory(&missing, &prepared.construction, &prepared.checked, types).is_err()); + let mut appended = before.clone(); + appended.functions.push(before.functions[0].clone()); + assert!( + receipt + .authenticate_output(&appended, output, types) + .is_err() + ); + } + Case::Dependencies => { + assert_eq!(prepared.rows.len(), 2); + let first = &prepared.rows[0]; + let second = &prepared.rows[1]; + let deps = payload_dependencies(&second.canonical).unwrap(); + assert_eq!( + deps, + vec![first.proof.clone()], + "two genuine argument uses deduplicate to one earlier proof" + ); + closed(&prepared.rows).unwrap(); + dependency(second, &first.proof, &prepared.rows).unwrap(); + dependency(second, &first.proof, &prepared.rows).unwrap(); + assert!(dependency(first, &second.proof, &prepared.rows).is_err()); + assert!(dependency(second, &second.proof, &prepared.rows).is_err()); + assert!(closed(&prepared.rows[1..]).is_err()); + let mut wrong_body = second.clone(); + wrong_body.function = FunctionId(u32::MAX); + assert!(dependency(&wrong_body, &first.proof, &prepared.rows).is_err()); + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &second.canonical.kind + else { + panic!(); + }; + let argument = source + .arguments + .iter() + .find_map(|arg| arg.source_witness().temporary_local_origin()) + .unwrap(); + let mut wrong = argument.clone(); + wrong.alias = LocalId(u32::MAX); + assert!(argument_proof(&wrong).is_err()); + let foreign = unsealed_copy(&first.proof); + assert!(dependency(second, &foreign, &prepared.rows).is_err()); + } + Case::Cells => { + let mut fake_row = prepared.rows[0].clone(); + fake_row.proof = unsealed_copy(&fake_row.proof); + fake_row + .proof + .0 + .canonical_producer + .set(fake_row.canonical.clone()) + .unwrap(); + assert!(open(&[fake_row]).is_err()); + } + Case::Coercion => { + assert_eq!(prepared.rows.len(), 1); + let row = &prepared.rows[0]; + assert!(!resource_materialization::expression_equal( + &row.canonical, + &row.proof.0.producer + )); + let ExpressionKind::Call { args, .. } = &row.canonical.kind else { + panic!(); + }; + assert!(matches!( + args[0].kind, + ExpressionKind::InterfaceCoerce { .. } + )); + assert!(same_rows(&prepared.rows, ¤t)); + } + Case::Payload => { + let mut sealed_getter = None; + for function in &output.functions { + let mut body = function.body.clone(); + resource_materialization::walk_block(&mut body, &mut |value| { + if let ExpressionKind::Intrinsic { field_validation: Some(ReflectedFieldValidation::Validate(plans)), .. } = &value.kind { + if plans.iter().any(|p| matches!(&p.action, ReflectedFieldAction::CallableReturn(plan) if plan.is_sealed())) { + sealed_getter = Some(value.clone()); + } + } + Ok(()) + }).unwrap(); + } + let sealed_getter = sealed_getter.expect("real output has a genuinely sealed getter"); + assert!(payload_dependencies(&sealed_getter).is_err()); + let mut changed = prepared.rows.clone(); + changed[0].canonical = sealed_getter; + assert!(!same_rows(&prepared.rows, &changed)); + } + } + // Negative operations above never commit or prefill any genuine cell. + open(&prepared.rows).unwrap(); + HITS.with(|v| v.set(v.get() + 1)); + CASE.with(|v| v.set(Some(case))); +} + +/// The checkpoint below also comes only from the real original actor finish. +pub(super) fn observe_commit( + prepared: &Prepared, + program: &Program, + types: &TypeInterner, + sources: &Arc>, + checkpoint: &crate::actor_handlers::InitialPeerCheckpoint, +) { + let Some(case) = CASE.with(|v| v.take()) else { + return; + }; + open(&prepared.rows).unwrap(); + prepared + .preflight_commit(program, types, sources, checkpoint) + .unwrap(); + if matches!(case, Case::Cells) { + let copied = Arc::new(sources.as_ref().clone()); + assert!(!Arc::ptr_eq(&copied, sources)); + assert!( + prepared + .preflight_commit(program, types, &copied, checkpoint) + .is_err() + ); + let mut changed = program.clone(); + let owner = changed + .functions + .iter_mut() + .find(|f| f.id == prepared.rows[0].function) + .unwrap(); + let StatementKind::Let { value, .. } = &mut owner.body.statements[0].kind else { + panic!(); + }; + value.span.end += 1; + assert!( + prepared + .preflight_commit(&changed, types, sources, checkpoint) + .is_err() + ); + let mut sibling = program.clone(); + let unrelated = sibling + .functions + .iter_mut() + .find(|f| f.id != prepared.rows[0].function) + .unwrap(); + unrelated.span.end += 1; + assert!( + prepared + .preflight_commit(&sibling, types, sources, checkpoint) + .is_err(), + "whole sibling header is part of the actual checkpoint" + ); + let mut appended = program.clone(); + appended.functions.push(program.functions[0].clone()); + assert!( + prepared + .preflight_commit(&appended, types, sources, checkpoint) + .is_err() + ); + } + open(&prepared.rows).unwrap(); + COMMITS.with(|v| v.set(v.get() + 1)); + CASE.with(|v| v.set(Some(case))); +} + +#[test] +fn packet_original_finalization_uses_genuine_before_values_and_is_idempotent() { + for release in [false, true] { + let (mut program, checked) = observed(Case::State, SOURCE, release); + let types = &checked.checked().interner; + let proofs = temporary_local_backings(&program, main(&program), types).unwrap(); + assert_eq!(proofs.len(), 1); + let canonical = proofs[0].owner_initializer().clone(); + assert!(proofs[0].0.canonical_producer.get().is_some()); + crate::complete_value_conversions(&mut program, types).unwrap(); + let first = program.clone(); + crate::complete_value_conversions(&mut program, types).unwrap(); + assert!(resource_materialization::functions_equal( + &first.functions, + &program.functions + )); + crate::validate_backend_types(&program, types).unwrap(); + program + .resource_source + .validate_current(&program, types) + .unwrap(); + assert!(resource_materialization::expression_equal( + &canonical, + proofs[0].owner_initializer() + )); + assert!(seal_original_program(&program, types).is_err()); + assert!(resource_materialization::expression_equal( + &canonical, + proofs[0].owner_initializer() + )); + } +} +#[test] +fn actual_preflight_refuses_foreign_context_inventory_and_table_without_cell_writes() { + for release in [false, true] { + observed(Case::Context, SOURCE, release); + } +} +#[test] +fn hidden_earlier_argument_closure_allows_repeated_uses_and_rejects_forward_edges() { + for release in [false, true] { + observed(Case::Dependencies, CHAIN_SOURCE, release); + } +} +#[test] +fn actual_finalization_preflight_checks_open_cells_exact_source_arc_and_current_producer() { + for release in [false, true] { + observed(Case::Cells, SOURCE, release); + } +} +#[test] +fn sealed_current_payload_cannot_replace_the_genuine_before_canonical() { + for release in [false, true] { + observed(Case::Payload, SOURCE, release); + } +} +#[test] +fn genuine_actor_and_earlier_temporary_graphs_expire_after_all_archive_owners_drop() { + for release in [false, true] { + let (program, checked) = high(CHAIN_SOURCE, release); + let proofs = + temporary_local_backings(&program, main(&program), &checked.checked().interner) + .unwrap(); + assert_eq!(proofs.len(), 2); + let weak = proofs + .iter() + .map(|p| Arc::downgrade(&p.0)) + .collect::>(); + let archive = program.resource_source.clone(); + let clone = program.clone(); + drop(proofs); + drop(program); + drop(checked); + assert!(weak.iter().all(|w| w.upgrade().is_some())); + drop(clone); + assert!( + weak.iter().all(|w| w.upgrade().is_some()), + "real archive retains the proofs" + ); + drop(archive); + assert!( + weak.iter().all(|w| w.upgrade().is_none()), + "frozen earlier proofs and actor edge have no ownership cycle" + ); + } +} + +#[test] +fn actual_original_finalization_retains_preseal_interface_coercion() { + for release in [false, true] { + let (mut program, checked) = observed(Case::Coercion, INTERFACE_SOURCE, release); + let types = &checked.checked().interner; + let proofs = temporary_local_backings(&program, main(&program), types).unwrap(); + let [proof] = proofs.as_slice() else { + panic!(); + }; + assert!(proof.0.canonical_producer.get().is_some()); + let ExpressionKind::Call { args, .. } = &proof.owner_initializer().kind else { + panic!(); + }; + assert!(matches!( + args[0].kind, + ExpressionKind::InterfaceCoerce { .. } + )); + assert!(!resource_materialization::expression_equal( + proof.owner_initializer(), + &proof.0.producer + )); + crate::complete_value_conversions(&mut program, types).unwrap(); + crate::validate_backend_types(&program, types).unwrap(); + program + .resource_source + .validate_current(&program, types) + .unwrap(); + } +} diff --git a/crates/jett_hir/src/temporary_local_views.rs b/crates/jett_hir/src/temporary_local_views.rs new file mode 100644 index 00000000..15ff258e --- /dev/null +++ b/crates/jett_hir/src/temporary_local_views.rs @@ -0,0 +1,880 @@ +//! Ordinary temporary owners retained by their original function-body scope. +//! SourceCall carries an opaque marker; the immutable archive is the inventory. +use super::*; +use std::sync::{Arc, OnceLock}; + +#[cfg(test)] +mod tests; + +#[path = "temporary_initial_finalization.rs"] +mod initial_finalization; +pub(crate) use initial_finalization::{ + Completed as CompletedOriginalFinalization, Construction, + Prepared as PreparedOriginalFinalization, finish_fresh as finish_fresh_original, + prepare as prepare_original_finalization, +}; + +struct Original { + checked: Arc, + declaration: CheckedBindingFact, + declaration_address: usize, + owner: Local, + alias: Local, + scope: TemporaryLocalScope, + initializer: Expression, + producer: Expression, + canonical_producer: OnceLock, + rewritten: Expression, + required_scopes: Vec, + scoped_types: Vec, +} + +/// No lexical scope, checked occurrence or owner can be supplied by a caller. +#[derive(Clone)] +pub struct TemporaryLocalBacking(Arc); +impl std::fmt::Debug for TemporaryLocalBacking { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TemporaryLocalBacking") + .field("owner", &self.owner()) + .field("alias", &self.alias()) + .field("scope", &self.0.scope) + .finish() + } +} +impl PartialEq for TemporaryLocalBacking { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for TemporaryLocalBacking {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TemporaryLocalArgument { + proof: TemporaryLocalBacking, + alias: LocalId, + owner: LocalId, +} +impl TemporaryLocalArgument { + pub(crate) fn metadata_locals(&self, mut visit: impl FnMut(LocalId)) { + visit(self.alias); + visit(self.owner); + } + pub(crate) fn remap( + &mut self, + map: &mut impl FnMut(LocalId) -> Result, + ) -> Result<(), E> { + self.alias = map(self.alias)?; + self.owner = map(self.owner)?; + Ok(()) + } + pub(crate) fn matches(&self, fact: &CallerBindingFact, value: OwnershipLocalInfo) -> bool { + let original = &self.proof.0; + self.alias == fact.local + && fact.ty == original.alias.ty + && fact.declaration_span == original.declaration.declaration_span + && fact.mode + == (CallerBindingMode::View { + source: CallerViewSource::Other, + }) + && !fact.mutable + && value.view_source == Some(self.owner) + && !value.mutable + && !value.is_view_parameter + && value.view_iteration.is_none() + && value.match_binding.is_none() + } +} + +#[derive(Debug)] +pub struct TemporaryLocalScope { + body: Span, + statement: usize, +} +impl TemporaryLocalScope { + pub fn body_span(&self) -> Span { + self.body + } + pub fn statement_index(&self) -> usize { + self.statement + } +} +impl TemporaryLocalBacking { + pub fn owner(&self) -> LocalId { + self.0.owner.id + } + pub fn alias(&self) -> LocalId { + self.0.alias.id + } + pub fn original_scope(&self) -> &TemporaryLocalScope { + &self.0.scope + } + pub fn original_initializer(&self) -> &Expression { + &self.0.initializer + } + pub fn owner_initializer(&self) -> &Expression { + self.0.canonical_producer.get().unwrap_or(&self.0.producer) + } + pub fn rewritten_initializer(&self) -> &Expression { + &self.0.rewritten + } + + /// Ignore only this exact original marker, never checked argument/call data. + pub fn matches_owner_initializer(&self, value: &Expression) -> bool { + let mut value = value.clone(); + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &mut value.kind + else { + return false; + }; + if source.temporary_local_backing() != Some(self) { + return false; + } + source.clear_temporary_local_backing(); + self.0 + .canonical_producer + .get() + .is_some_and(|original| resource_materialization::expression_equal(&value, original)) + } +} + +fn marker(value: &Expression) -> Option<&TemporaryLocalBacking> { + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &value.kind + else { + return None; + }; + source.temporary_local_backing() +} +fn markers(function: &Function) -> Vec { + let mut body = function.body.clone(); + let mut result = Vec::new(); + let _ = resource_materialization::walk_block(&mut body, &mut |value| { + if let Some(proof) = marker(value) { + result.push(proof.clone()); + } + Ok(()) + }); + result +} +pub(super) fn has_markers(program: &Program) -> bool { + program + .functions + .iter() + .any(|function| !markers(function).is_empty()) +} +fn expected(program: &Program) -> bool { + program + .resource_source + .functions() + .iter() + .any(|function| !markers(function).is_empty()) +} + +/// The immutable original, rather than an optional current marker, determines +/// which declarations owe storage. Whole-archive erasure remains the preexisting +/// public Program boundary; this API claims no protection beyond that boundary. +pub fn temporary_local_backings( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result, String> { + if !expected(program) && !has_markers(program) { + return Ok(Vec::new()); + } + program.resource_source.validate_current(program, types)?; + let current = program + .functions + .get(function.id.index() as usize) + .filter(|current| current.id == function.id) + .ok_or("temporary backing function is absent")?; + if !resource_materialization::functions_equal( + std::slice::from_ref(current), + std::slice::from_ref(function), + ) { + return Err("temporary backing consumer supplied a different function".into()); + } + let Some(original) = program + .resource_source + .functions() + .iter() + .find(|original| original.id == function.id) + else { + // Canonical conversion may append adapters. The complete current table + // was authenticated above; an appended function cannot import a Source + // temporary occurrence of its own. + return if markers(current).is_empty() { + Ok(Vec::new()) + } else { + Err("temporary backing has no original checked function".into()) + }; + }; + let originals = markers(original); + let actual = markers(current); + if originals != actual { + return Err("temporary backing lost its exact original inventory".into()); + } + for (index, proof) in originals.iter().enumerate() { + if originals[..index] + .iter() + .any(|previous| previous.alias() == proof.alias() || previous.owner() == proof.owner()) + { + return Err("temporary backing duplicated an original declaration".into()); + } + validate_record(program, current, proof, types)?; + } + Ok(originals) +} + +pub(super) fn validate_program( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + if expected(program) || has_markers(program) { + let span = program + .resource_source + .functions() + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span); + program + .resource_source + .validate_current(program, types) + .map_err(|message| vec![ValidationError { span, message }])?; + } + for function in &program.functions { + temporary_local_backings(program, function, types).map_err(|message| { + vec![ValidationError { + span: function.span, + message, + }] + })?; + } + Ok(()) +} + +fn validate_record( + program: &Program, + function: &Function, + proof: &TemporaryLocalBacking, + types: &TypeInterner, +) -> Result<(), String> { + let checked = program + .resource_source + .checked_program() + .ok_or("temporary backing has no checked Source")?; + if !Arc::ptr_eq(checked, &proof.0.checked) { + return Err("temporary backing changed its checked Source session".into()); + } + let owner = validate_source_record(function, proof, types)?; + if !proof.matches_owner_initializer(owner) { + return Err("temporary backing changed its canonical original production".into()); + } + Ok(()) +} + +fn validate_source_record<'a>( + function: &'a Function, + proof: &TemporaryLocalBacking, + types: &TypeInterner, +) -> Result<&'a Expression, String> { + let data = &proof.0; + if function.identity.declaration.kind != DeclarationKind::Function + || !function.identity.type_arguments.is_empty() + || !function.identity.scoped_type_bindings.is_empty() + || function.body.span != data.scope.body + || !data.required_scopes.is_empty() + || !data.scoped_types.is_empty() + || function.locals.get(data.owner.id.index() as usize) != Some(&data.owner) + || function.locals.get(data.alias.id.index() as usize) != Some(&data.alias) + || function + .params + .iter() + .any(|param| param.local == data.owner.id || param.local == data.alias.id) + || !matches!( + data.declaration.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ) + { + return Err("temporary backing changed its checked occurrence, scope or headers".into()); + } + let source = original_source_body(&data.checked, function) + .ok_or("temporary backing lost its original named function")?; + let Some(Stmt::VarDecl(declaration)) = source.stmts.get(data.scope.statement) else { + return Err("temporary backing original declaration is absent".into()); + }; + if std::ptr::from_ref(declaration).addr() != data.declaration_address + || declaration.name.span != data.declaration.declaration_span + || declaration.mutable + || !matches!(declaration.value, Expr::View(..)) + { + return Err("temporary backing original declaration changed".into()); + } + let mut owner_sites = Vec::new(); + let mut alias_sites = Vec::new(); + // Only original root-body declarations are supported in this first slice. + for (index, statement) in function.body.statements.iter().enumerate() { + if let StatementKind::Let { local, value } = &statement.kind { + if *local == data.owner.id { + owner_sites.push((index, value)); + } + if *local == data.alias.id { + alias_sites.push((index, value)); + } + } + } + let ([(owner_index, owner)], [(alias_index, alias)]) = + (owner_sites.as_slice(), alias_sites.as_slice()) + else { + return Err("temporary backing lost unique original owner and alias initialization".into()); + }; + if *alias_index != *owner_index + 1 + || !resource_materialization::expression_equal(alias, &data.rewritten) + { + return Err( + "temporary backing changed production, projection or initialization order".into(), + ); + } + local_views::validate_local_view_initializer( + alias, + data.owner.id, + data.owner.ty, + data.alias.ty, + types, + )?; + Ok(owner) +} + +/// Normalize only the exact own outer marker; preserve full canonical operands. +fn canonical_production( + function: &Function, + proof: &TemporaryLocalBacking, + types: &TypeInterner, +) -> Result { + let mut canonical = validate_source_record(function, proof, types)?.clone(); + let ExpressionKind::Call { + function: current_target, + args: current_args, + evaluation_order: current_order, + ownership: CallOwnership::Source(current_ownership), + } = &mut canonical.kind + else { + return Err("temporary original owner lost its checked direct invocation".into()); + }; + let ExpressionKind::Call { + function: original_target, + args: original_args, + evaluation_order: original_order, + ownership: CallOwnership::Source(original_ownership), + } = &proof.0.producer.kind + else { + return Err("temporary original producer is not its checked direct invocation".into()); + }; + if current_ownership.temporary_local_backing() != Some(proof) { + return Err("temporary original owner changed its occurrence marker".into()); + } + current_ownership.clear_temporary_local_backing(); + // Preserve canonical operands and every checked call-certificate field. + if *current_target != *original_target + || current_args.len() != original_args.len() + || current_order.as_slice() != original_order.as_slice() + || &*current_ownership != original_ownership + || canonical.ty != proof.0.producer.ty + || canonical.span != proof.0.producer.span + { + return Err("temporary canonical production changed its checked invocation headers".into()); + } + Ok(canonical) +} + +/// This is called only from the original Lowerer, after its canonical passes +/// and before its immutable archive is captured. It is deliberately single use; +/// public conversion entry points can never fill or replace this record. +pub(super) fn seal_original_program( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut pending = Vec::new(); + for function in &program.functions { + for proof in markers(function) { + let fail = |message: String| { + vec![LowerError { + span: function.span, + message, + }] + }; + if proof.0.canonical_producer.get().is_some() + || pending + .iter() + .any(|(previous, _): &(TemporaryLocalBacking, Expression)| previous == &proof) + { + return Err(fail( + "temporary original production was already captured".into(), + )); + } + let canonical = canonical_production(function, &proof, types).map_err(fail)?; + pending.push((proof, canonical)); + } + } + for (proof, canonical) in pending { + proof.0.canonical_producer.set(canonical).map_err(|_| { + vec![LowerError { + span: proof.0.declaration.declaration_span, + message: "temporary original production was already captured".into(), + }] + })?; + } + Ok(()) +} + +fn original_source_body<'a>( + checked: &'a jett_typecheck::CheckedResourceProgram, + function: &Function, +) -> Option<&'a ast::Block> { + let definition = function.source_definition?; + checked.module().items.iter().find_map(|item| match item { + Item::Function(source) + if source.span == function.span + && reflected_field_sources::original_root_function( + checked.module(), + checked.resolved(), + source, + definition, + ) => + { + Some(&source.body) + } + _ => None, + }) +} + +fn finite_ordinary(types: &TypeInterner, ty: TypeId, visiting: &mut HashSet) -> bool { + if ty.index() as usize >= types.len() || !visiting.insert(ty) { + return false; + } + let valid = match types.resolve(ty) { + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing => true, + Type::List(inner) => finite_ordinary(types, *inner, visiting), + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field)| finite_ordinary(types, *field, visiting)), + _ => false, + }; + visiting.remove(&ty); + valid +} +fn producer<'a>(value: &'a Expression, types: &TypeInterner) -> Option<&'a Expression> { + let ExpressionKind::View(current) = &value.kind else { + return None; + }; + let mut current = current.as_ref(); + let mut fields = 0; + while let ExpressionKind::Field { + base, owner_type, .. + } = ¤t.kind + { + if !matches!(types.resolve(*owner_type), Type::Struct(_)) { + return None; + } + local_views::validate_field_projection(current, types).ok()?; + fields += 1; + current = base; + } + if fields == 0 + || !matches!(types.resolve(value.ty), Type::Bytes | Type::List(_)) + || !matches!(types.resolve(current.ty), Type::Struct(_)) + || !finite_ordinary(types, current.ty, &mut HashSet::new()) + { + return None; + } + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = ¤t.kind + else { + return None; + }; + (source.bridge == CallBridge::Direct + && source.context == jett_typecheck::CheckedOwnershipContext::Ordinary + && source.temporary_local_backing().is_none()) + .then_some(current) +} +fn replace_producer(value: &mut Expression, owner: LocalId) { + match &mut value.kind { + ExpressionKind::View(inner) => replace_producer(inner, owner), + ExpressionKind::Field { base, .. } => replace_producer(base, owner), + _ => value.kind = ExpressionKind::Local(owner), + } +} + +impl BodyLowerer<'_, '_> { + /// Called only for genuine Other facts; unsuccessful recognition preserves + /// the existing conservative backend refusal without changing Source rules. + pub(super) fn lower_temporary_local_declaration( + &mut self, + declaration: &ast::VarDecl, + definition: DefId, + value: &Expression, + ty: TypeId, + ) -> Option<(LocalId, Expression)> { + let root = self.temporary_root?; + let (scope, index) = self.temporary_scope?; + let checked = self.parent.reflected_checked_program.clone()?; + let fact = *self.binding_facts.get(&declaration.name.span)?; + if scope != root.span + || self.required_scopes.len() != 0 + || self.scoped_type_bindings.len() != 0 + || !matches!(root.stmts.get(index), Some(Stmt::VarDecl(original)) if std::ptr::eq(original, declaration)) + || declaration.mutable + || !matches!(declaration.value, Expr::View(..)) + || fact.definition != definition + || fact.declaration_span != declaration.name.span + || fact.ty != ty + || fact.mutable + || fact.mode + != (CheckedBindingMode::View { + source: CheckedViewSource::Other, + }) + || self.binding_modes.get(&declaration.name.span) != Some(&fact.mode) + { + return None; + } + let production = producer(value, &self.parent.check.interner)?.clone(); + let ExpressionKind::Call { + function: callee, + ownership, + args, + evaluation_order, + } = &production.kind + else { + return None; + }; + let CallOwnership::Source(source) = ownership else { + return None; + }; + if source.target != CallTarget::Function(*callee) + || call_ownership::validate_operand_ownership( + ownership, + &args.iter().map(|argument| argument.ty).collect::>(), + evaluation_order, + &self.parent.check.interner, + |local| self.ownership_local_info(local), + true, + ) + .is_err() + { + return None; + }; + // Lowerer::lower drains its scheduling list before lowering bodies. + // Join the immutable original declaration to the retained exact ID map, + // rather than looking for authority in that now-empty scheduling list. + let ordinary_target = checked.module().items.iter().any(|item| { + let Item::Function(source) = item else { + return false; + }; + let Some(definition) = self + .parent + .definition_at(source.name.span, DefKind::Function) + else { + return false; + }; + source.type_params.is_empty() + && reflected_field_sources::original_root_function( + self.parent.module, + self.parent.resolve, + source, + definition, + ) + && self.function_ids.get(&FunctionKey::Definition { + definition, + concrete_args: Vec::new(), + specialization: CheckedGenericSpecialization::default(), + }) == Some(callee) + }); + if !ordinary_target { + return None; + } + let owner = LocalId(u32::try_from(self.locals.len()).ok()?); + let owner_header = Local { + id: owner, + name: String::new(), + ty: production.ty, + debug_ty: production.ty, + debug_type_name: None, + mutable: false, + view_source: None, + span: declaration.span, + }; + self.locals.push(owner_header.clone()); + let alias = self.allocate_local( + definition, + &declaration.name.name, + ty, + false, + declaration.span, + ); + self.locals[alias.index() as usize].view_source = Some(owner); + let mut rewritten = value.clone(); + replace_producer(&mut rewritten, owner); + let proof = TemporaryLocalBacking(Arc::new(Original { + checked, + declaration: fact, + declaration_address: std::ptr::from_ref(declaration).addr(), + owner: owner_header, + alias: self.locals[alias.index() as usize].clone(), + scope: TemporaryLocalScope { + body: scope, + statement: index, + }, + initializer: value.clone(), + producer: production.clone(), + canonical_producer: OnceLock::new(), + rewritten: rewritten.clone(), + required_scopes: self.required_scopes.clone(), + scoped_types: self.scoped_type_bindings.clone(), + })); + // Register at mint, before the marker or earlier-argument clones escape + // into this still-unpublished Lowerer construction. + self.parent + .temporary_construction + .as_mut()? + .register(proof.clone()); + let mut production = production; + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &mut production.kind + else { + return None; + }; + source.set_temporary_local_backing(proof.clone()); + self.temporary_origins.insert(alias, proof); + self.temporary_prefix.push(Statement { + kind: StatementKind::Let { + local: owner, + value: production, + }, + span: declaration.span, + }); + Some((alias, rewritten)) + } + pub(super) fn temporary_argument_origin( + &self, + origin: &CallerOrigin, + ) -> Option> { + let CallerOrigin::Binding(fact) = origin else { + return None; + }; + let proof = self.temporary_origins.get(&fact.local)?.clone(); + Some(Box::new(TemporaryLocalArgument { + owner: proof.owner(), + alias: proof.alias(), + proof, + })) + } +} + +// These transitions update the existing execution-functions view only after +// exact authenticated replay. Reusing this archive grants no Resource custody, +// execution family, provider or manifest scope. +fn semantic_functions(program: &Program) -> Vec { + let mut functions = program.functions.clone(); + for function in &mut functions { + function.mutable_sum_backings.clear(); + function.actor_handler = None; + function.actor_state_views.clear(); + } + functions +} +fn independently_validated(program: &Program) -> Result<(), Vec> { + mutable_sum_backings::validate_program(program) + .and_then(|()| actor_handlers::validate_program(program)) + .map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect() + }) +} +fn finish_replay( + program: &Program, + before: &Program, + replay: &Program, +) -> Result> { + independently_validated(before)?; + independently_validated(program)?; + let span = before + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |f| f.span); + if before.resource_source != program.resource_source + || before.resource_manifest != program.resource_manifest + || replay.equality_methods != program.equality_methods + || !resource_materialization::functions_equal( + &semantic_functions(program), + &semantic_functions(replay), + ) + { + return Err(vec![LowerError { + span, + message: "temporary backing changed outside its exact authenticated transition".into(), + }]); + } + let archive = before.resource_source.clone(); + let view = resource_source::Materialized { + functions: program.functions.clone(), + values: archive.required_materializations().to_vec(), + required_only: archive.required_only_function_ids().to_vec(), + }; + let mut candidate = program.clone(); + let native_suites = native_suite_transition::converted(program, before) + .map_err(|message| vec![LowerError { span, message }])?; + if native_suites.is_some() { + candidate.resource_source.native_suites = native_suites; + } + candidate + .resource_source + .install_materialized(resource_source::Materialized { + functions: view.functions.clone(), + values: view.values.clone(), + required_only: view.required_only.clone(), + }); + validate_program( + &candidate, + &archive + .checked_program() + .ok_or_else(|| { + vec![LowerError { + span, + message: "temporary transition lost checked Source".into(), + }] + })? + .checked() + .interner, + ) + .map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + Ok(view) +} +pub(super) fn conversion( + program: &Program, + before: &Program, + types: &TypeInterner, + conversion: &interface_values::CanonicalValueConversion<'_>, +) -> Result, Vec> { + if !expected(before) && !has_markers(before) { + return Ok(None); + } + validate_program(before, types).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + let replay = conversion.replay(before, types)?; + // The caller joins only this execution view after its peer Graph/baked + // transitions authenticate the same unchanged archive independently. + finish_replay(program, before, &replay).map(Some) +} +pub(super) fn callable_sealing( + program: &mut Program, + before: &Program, + edits: &[(ReflectedCallablePlan, ReflectedCallablePlan)], +) -> Result<(), Vec> { + if !expected(before) { + return Ok(()); + } // Initial genuine sealing precedes archive capture. + let types = &before + .resource_source + .checked_program() + .ok_or_else(|| { + vec![LowerError { + span: before.functions[0].span, + message: "temporary callable replay lost original Source".into(), + }] + })? + .checked() + .interner; + validate_program(before, types).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + let mut replay = before.clone(); + for function in &mut replay.functions { + resource_materialization::walk_block(&mut function.body, &mut |value| { + let replace = |plan: &mut ReflectedCallablePlan| { + if let Some((_, current)) = edits.iter().find(|(previous, _)| previous == plan) { + *plan = current.clone(); + } + }; + match &mut value.kind { + ExpressionKind::Intrinsic { + field_validation: Some(ReflectedFieldValidation::Validate(plans)), + .. + } => { + for plan in plans { + if let ReflectedFieldAction::CallableReturn(callable) = &mut plan.action { + replace(callable); + } + } + } + ExpressionKind::Handle { + kind: + HandleKind::ReflectedReturn { + callable: Some(callable), + .. + }, + .. + } => replace(callable), + _ => {} + } + Ok(()) + }) + .map_err(|message| { + vec![LowerError { + span: function.span, + message, + }] + })?; + } + let view = finish_replay(program, before, &replay)?; + program.resource_source.install_materialized(view); + Ok(()) +} diff --git a/crates/jett_hir/src/temporary_local_views/tests.rs b/crates/jett_hir/src/temporary_local_views/tests.rs new file mode 100644 index 00000000..9f053a5f --- /dev/null +++ b/crates/jett_hir/src/temporary_local_views/tests.rs @@ -0,0 +1,464 @@ +use super::*; + +const SOURCE: &str = "namespace app\nstruct Packet:\n data: list[int64]\n sibling: list[int64]\nfunction make_packet() returns Packet:\n return Packet(data: list(1, 2), sibling: list(3, 4))\nfunction inspect(view data: list[int64]) returns nothing:\n return nothing\nfunction main() returns nothing:\n list[int64] borrowed = view make_packet().data\n inspect(view borrowed)\n list[int64] forwarded = borrowed\n inspect(view forwarded)\n list[int64] independent = clone forwarded\n"; + +fn high(source: &str, release: bool) -> (Program, Arc) { + let file = FileId::new(0); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ); + let program = lower_checked_ordinary_program(&checked).unwrap(); + (program, checked) +} +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() +} +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap() +} + +#[test] +fn whole_owner_and_other_fact_are_retained_for_unused_and_read_aliases() { + let unused = SOURCE.replace(" inspect(view borrowed)\n list[int64] forwarded = borrowed\n inspect(view forwarded)\n list[int64] independent = clone forwarded\n", ""); + for release in [false, true] { + for source in [SOURCE, unused.as_str()] { + let (program, checked) = high(source, release); + let function = main(&program); + let receipts = + temporary_local_backings(&program, function, &checked.checked().interner).unwrap(); + let [receipt] = receipts.as_slice() else { + panic!("one original temporary occurrence"); + }; + assert!(matches!( + receipt.0.declaration.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + )); + assert_eq!(receipt.original_scope().statement_index(), 0); + assert_eq!(receipt.original_scope().body_span(), function.body.span); + assert_eq!( + function.locals[receipt.alias().index() as usize].view_source, + Some(receipt.owner()) + ); + assert!( + function.locals[receipt.owner().index() as usize] + .name + .is_empty() + ); + assert_ne!( + receipt.owner_initializer().ty, + receipt.rewritten_initializer().ty + ); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + } + } +} + +#[test] +fn retained_archive_rejects_erased_foreign_and_duplicated_markers_before_pruning() { + let (original, checked) = high(SOURCE, false); + let (foreign, _) = high(SOURCE, false); + let foreign_proof = markers(main(&foreign))[0].clone(); + for alteration in ["erase", "foreign", "duplicate", "owner", "alias", "scope"] { + let mut changed = original.clone(); + let function = main_mut(&mut changed); + match alteration { + "erase" | "foreign" => { + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!(); + }; + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + .. + } = &mut value.kind + else { + panic!(); + }; + source.clear_temporary_local_backing(); + if alteration == "foreign" { + source.set_temporary_local_backing(foreign_proof.clone()); + } + } + "duplicate" => { + let statement = function.body.statements[0].clone(); + function.body.statements.push(statement); + } + "owner" => { + function.body.statements.remove(0); + } + "alias" => { + function.body.statements.remove(1); + } + "scope" => function.body.span.end += 1, + _ => unreachable!(), + } + assert!( + temporary_local_backings(&changed, main(&changed), &checked.checked().interner) + .is_err(), + "{alteration}" + ); + assert!( + validate_backend_types(&changed, &checked.checked().interner).is_err(), + "{alteration}" + ); + } + let mut no_archive = original.clone(); + no_archive.resource_source = ResourceSourceArchive::empty(); + assert!(validate_backend_types(&no_archive, &checked.checked().interner).is_err()); + let mut erased_functions = original.clone(); + erased_functions.functions.clear(); + assert!(validate_backend_types(&erased_functions, &checked.checked().interner).is_err()); +} + +#[test] +fn genuine_unmarked_same_shape_source_call_does_not_replace_expected_receipt() { + let source = SOURCE.replace( + "function main()", + "function spare() returns nothing:\n Packet owned = make_packet()\nfunction main()", + ); + let (mut program, checked) = high(&source, false); + let spare = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "spare") + .unwrap(); + let StatementKind::Let { + value: spare_value, .. + } = &spare.body.statements[0].kind + else { + panic!(); + }; + let ExpressionKind::Call { ownership, .. } = &spare_value.kind else { + panic!(); + }; + let unmarked = ownership.clone(); + let StatementKind::Let { value, .. } = &mut main_mut(&mut program).body.statements[0].kind + else { + panic!(); + }; + let ExpressionKind::Call { ownership, .. } = &mut value.kind else { + panic!(); + }; + *ownership = unmarked; + assert!( + temporary_local_backings(&program, main(&program), &checked.checked().interner).is_err() + ); +} + +#[test] +fn raw_lowering_cannot_capture_the_archive_or_stamp_a_temporary_owner() { + let file = FileId::new(0); + let parsed = jett_parser::parse(SOURCE, file); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + let errors = lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("temporary views remain unsupported")) + ); +} + +#[test] +fn no_edit_replay_does_not_authenticate_a_changed_body() { + let (before, _) = high(SOURCE, false); + let mut changed = before.clone(); + main_mut(&mut changed).body.statements.pop(); + assert!(callable_sealing(&mut changed, &before, &[]).is_err()); +} + +#[test] +fn original_canonical_capture_retains_coerced_direct_producer_and_is_single_use() { + let source = "namespace app\ninterface Named:\n function name(view self: Named) returns string\nimplement Named for int64:\n function name(view self: int64) returns string:\n return \"number\"\nstruct Packet:\n data: list[int64]\n sibling: list[int64]\nfunction make_packet(view input: Named) returns Packet:\n return Packet(data: list(1, 2), sibling: list(3, 4))\nfunction main() returns nothing:\n int64 number = 7\n list[int64] borrowed = view make_packet(view number).data\n"; + for release in [false, true] { + let (program, checked) = high(source, release); + let receipts = + temporary_local_backings(&program, main(&program), &checked.checked().interner) + .unwrap(); + let [receipt] = receipts.as_slice() else { + panic!(); + }; + assert!(receipt.0.canonical_producer.get().is_some()); + assert!(!resource_materialization::expression_equal( + receipt.owner_initializer(), + &receipt.0.producer + )); + let ExpressionKind::Call { args, .. } = &receipt.owner_initializer().kind else { + panic!(); + }; + assert!(matches!( + args[0].kind, + ExpressionKind::InterfaceCoerce { .. } + )); + let archived = program.resource_source.clone(); + assert!(seal_original_program(&program, &checked.checked().interner).is_err()); + assert_eq!(program.resource_source, archived); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + } +} + +#[test] +fn canonical_snapshot_keeps_foreign_argument_receipts_without_an_arc_cycle() { + let source = "namespace app\nstruct Packet:\n data: list[int64]\n sibling: list[int64]\nfunction first_packet() returns Packet:\n return Packet(data: list(1, 2), sibling: list(3, 4))\nfunction next_packet(view input: list[int64]) returns Packet:\n return Packet(data: clone input, sibling: list(5, 6))\nfunction main() returns nothing:\n list[int64] first = view first_packet().data\n list[int64] second = view next_packet(view first).data\n"; + let (program, checked) = high(source, false); + let receipts = + temporary_local_backings(&program, main(&program), &checked.checked().interner).unwrap(); + let [first, second] = receipts.as_slice() else { + panic!("two exact temporary occurrences"); + }; + assert!(marker(first.owner_initializer()).is_none()); + assert!(marker(second.owner_initializer()).is_none()); + let ExpressionKind::Call { ownership, .. } = &second.owner_initializer().kind else { + panic!(); + }; + assert!( + ownership.has_temporary_local_receipts(), + "stripping the producer marker must keep the first alias's argument receipt" + ); + let ExpressionKind::Call { + ownership: original_ownership, + .. + } = &second.0.producer.kind + else { + panic!(); + }; + assert_eq!( + ownership, original_ownership, + "the exact foreign argument receipt and full Source certificate survive capture" + ); + validate_backend_types(&program, &checked.checked().interner).unwrap(); + let first_weak = Arc::downgrade(&first.0); + let second_weak = Arc::downgrade(&second.0); + drop(receipts); + drop(program); + drop(checked); + assert!( + second_weak.upgrade().is_none(), + "the canonical producer must not own its own marker" + ); + assert!( + first_weak.upgrade().is_none(), + "foreign argument receipts form a forward-free dependency chain" + ); +} + +fn unsealed_copy(proof: &TemporaryLocalBacking) -> TemporaryLocalBacking { + let data = &proof.0; + TemporaryLocalBacking(Arc::new(Original { + checked: data.checked.clone(), + declaration: data.declaration, + declaration_address: data.declaration_address, + owner: data.owner.clone(), + alias: data.alias.clone(), + scope: TemporaryLocalScope { + body: data.scope.body, + statement: data.scope.statement, + }, + initializer: data.initializer.clone(), + producer: data.producer.clone(), + canonical_producer: OnceLock::new(), + rewritten: data.rewritten.clone(), + required_scopes: data.required_scopes.clone(), + scoped_types: data.scoped_types.clone(), + })) +} + +#[test] +fn unsealed_and_tampered_original_capture_do_not_gain_storage_authority() { + let (original, checked) = high(SOURCE, false); + let proof = markers(main(&original))[0].clone(); + let StatementKind::Let { value: owner, .. } = &main(&original).body.statements[0].kind else { + panic!(); + }; + let unsealed = unsealed_copy(&proof); + assert!(!unsealed.matches_owner_initializer(owner)); + for alteration in ["target", "span", "alias", "scope", "marker"] { + // Test-only synthetic preimage of the private original-finalization + // gate. It is not a second compiler-generated Source occurrence. + let candidate_proof = unsealed_copy(&proof); + let mut candidate = original.clone(); + candidate.resource_source = ResourceSourceArchive::empty(); + let function = main_mut(&mut candidate); + let StatementKind::Let { value, .. } = &mut function.body.statements[0].kind else { + panic!(); + }; + let ExpressionKind::Call { + ownership: CallOwnership::Source(source), + function: target, + .. + } = &mut value.kind + else { + panic!(); + }; + source.set_temporary_local_backing(candidate_proof.clone()); + match alteration { + "target" => *target = FunctionId(target.index() + 1), + "span" => value.span.end += 1, + "alias" => function.locals[candidate_proof.alias().index() as usize].mutable = true, + "scope" => function.body.span.end += 1, + "marker" => source.set_temporary_local_backing(proof.clone()), + _ => unreachable!(), + } + assert!( + seal_original_program(&candidate, &checked.checked().interner).is_err(), + "{alteration}" + ); + assert!( + candidate_proof.0.canonical_producer.get().is_none(), + "{alteration}" + ); + } +} + +fn required_values( + checked: &Arc, +) -> jett_comptime::ExplicitComptimeValues { + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new( + jett_comptime::checked_types::CheckedExpressionTypes::from_checked_program(checked), + ), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + evaluation.values +} + +#[test] +fn temporary_execution_joins_genuine_graph_and_baked_peer_transitions() { + let prefix = "namespace app\ntype Positive = int64 where value > 0\nstruct CallbackHolder:\n callback: function() returns int64\nfunction five() returns int64:\n return 5\nfunction reflected(view holder: CallbackHolder, view field: TypeField) returns function() returns Positive:\n return type.field_value[CallbackHolder, function() returns Positive](view holder, view field)\nactor Counter:\n mutable int64 count = 1\n receive read responds int64:\n respond count\nfunction make_counter() returns Counter:\n return comptime spawn Counter()\n"; + let source = format!( + "{prefix}{}", + SOURCE.strip_prefix("namespace app\n").unwrap() + ); + for release in [false, true] { + let (mut program, checked) = high(&source, release); + let values = required_values(&checked); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + assert!(program.resource_source.graph_values.is_some()); + assert!(program.resource_source.reflected_values.is_some()); + let before = program.clone(); + let mut replay = before.clone(); + let receipt = interface_values::recorded_coercion_for_test( + &mut replay, + &before, + &checked.checked().interner, + ) + .unwrap(); + let view = conversion(&replay, &before, &checked.checked().interner, &receipt) + .unwrap() + .unwrap(); + assert_eq!( + replay.resource_source, before.resource_source, + "peer transitions need the unchanged input archive" + ); + assert_eq!( + view.required_only, + before.resource_source.required_only_function_ids() + ); + assert_eq!( + view.values.len(), + before.resource_source.required_materializations().len() + ); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + program + .resource_source + .validate_current(&program, &checked.checked().interner) + .unwrap(); + assert_eq!( + temporary_local_backings(&program, main(&program), &checked.checked().interner) + .unwrap() + .len(), + 1 + ); + let mut changed = before.clone(); + main_mut(&mut changed).body.statements.pop(); + assert!( + crate::complete_value_conversions(&mut changed, &checked.checked().interner).is_err() + ); + } +} + +#[test] +fn genuine_appended_verify_entry_has_no_original_temporary_inventory() { + let graph = "namespace app\nactor Counter:\n mutable int64 count = 1\n receive read responds int64:\n respond count\nfunction make_counter() returns Counter:\n return comptime spawn Counter()\n"; + let source = format!( + "{graph}{}\nverify independent:\n assert true\n", + SOURCE.strip_prefix("namespace app\n").unwrap() + ); + let file = FileId::new(0); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(&source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .unwrap(), + ); + let values = required_values(&checked); + let mut program = crate::lower_checked_ordinary_program_with_test_bodies(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + assert!(program.resource_source.graph_values.is_some()); + let entry = crate::append_checked_native_verify_suite(&mut program, &checked, file) + .unwrap() + .unwrap(); + assert!( + program + .resource_source + .functions() + .iter() + .all(|function| function.id != entry) + ); + crate::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + assert!( + temporary_local_backings( + &program, + &program.functions[entry.index() as usize], + &checked.checked().interner + ) + .unwrap() + .is_empty() + ); + validate_backend_types(&program, &checked.checked().interner).unwrap(); +} diff --git a/crates/jett_hir/src/type_validation.rs b/crates/jett_hir/src/type_validation.rs new file mode 100644 index 00000000..0d5ae115 --- /dev/null +++ b/crates/jett_hir/src/type_validation.rs @@ -0,0 +1,1546 @@ +use std::collections::HashSet; + +use jett_common::Span; +use jett_types::{ + ActorId, BitfieldId, EnumId, FunctionSig, InterfaceId, MachineId, StructId, Type, TypeId, + TypeInterner, +}; + +use crate::{ + Block, Expression, ExpressionKind, Function, HandleKind, Program, Statement, StatementKind, + StringSegment, ValidationError, +}; + +/// Reject recovery-only types before a completed HIR program can reach MIR. +/// +/// Only types reachable from HIR are inspected. `TypeInterner::ERROR` is +/// always registered so checking every interned type would reject every valid +/// compilation. Named definitions are followed transitively because their +/// field and signature types are part of the backend-visible representation. +pub fn validate_backend_types( + program: &Program, + interner: &TypeInterner, +) -> Result<(), Vec> { + validate_backend_types_inner(program, interner, true) +} +pub(super) fn validate_original_backend_types( + program: &Program, + interner: &TypeInterner, +) -> Result<(), Vec> { + validate_backend_types_inner(program, interner, false) +} +fn validate_backend_types_inner( + program: &Program, + interner: &TypeInterner, + archived: bool, +) -> Result<(), Vec> { + let mut validator = BackendTypeValidator { + resource_manifest: &program.resource_manifest, + interner, + functions: &program.functions, + ownership_locals: Vec::new(), + ownership_context: jett_typecheck::CheckedOwnershipContext::Ordinary, + visited_types: HashSet::new(), + visited_definitions: HashSet::new(), + local_views: Vec::new(), + local_types: Vec::new(), + local_view_initializers: HashSet::new(), + borrowed_parameters: HashSet::new(), + iteration_binders: HashSet::new(), + match_binders: HashSet::new(), + errors: Vec::new(), + }; + if let Err(message) = program.resource_manifest.validate(interner) { + validator.error(Span::new(jett_common::FileId::new(0), 0, 0), message); + } + if program.resource_source.checked_program().is_some() + && program + .functions + .iter() + .any(|function| !crate::reflected_callable_plans(function).is_empty()) + { + if let Err(message) = program.resource_source.validate_types(interner) { + validator.error(Span::new(jett_common::FileId::new(0), 0, 0), message); + } + } + if archived { + if let Err(errors) = crate::temporary_local_views::validate_program(program, interner) { + validator.errors.extend(errors); + } + } + for function in &program.functions { + validator.function(function); + } + if let Err(errors) = crate::validate_local_views(program, interner) { + validator.errors.extend(errors); + } + for (&owner, &method) in &program.equality_methods { + let target = program + .functions + .get(method.index() as usize) + .filter(|function| function.id == method); + if let Some(target) = target { + if owner.index() as usize >= interner.len() + || !matches!(interner.resolve(owner), Type::Struct(_)) + || target.capture_count != 0 + || target.params.len() != 2 + || target.return_type != TypeInterner::BOOL + || target.params.iter().any(|parameter| { + parameter.ty != owner || parameter.mode != crate::ParamMode::View + }) + { + validator.error( + target.span, + "equality target must have two exact struct views and return bool", + ); + } + } else { + validator.error( + Span::new(jett_common::FileId::new(0), 0, 0), + "equality target is absent from HIR", + ); + } + } + if validator.errors.is_empty() { + Ok(()) + } else { + Err(validator.errors) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum DefinitionKey { + Struct(StructId), + Bitfield(BitfieldId), + Enum(EnumId), + Interface(InterfaceId), + Actor(ActorId), + Machine(MachineId), +} + +struct BackendTypeValidator<'a> { + resource_manifest: &'a crate::ResourceManifest, + interner: &'a TypeInterner, + functions: &'a [Function], + ownership_locals: Vec, + ownership_context: jett_typecheck::CheckedOwnershipContext, + visited_types: HashSet, + visited_definitions: HashSet, + local_views: Vec>, + local_types: Vec, + local_view_initializers: HashSet, + borrowed_parameters: HashSet, + iteration_binders: HashSet, + match_binders: HashSet, + errors: Vec, +} + +impl BackendTypeValidator<'_> { + fn error(&mut self, span: Span, message: impl Into) { + self.errors.push(ValidationError { + span, + message: message.into(), + }); + } + + fn function(&mut self, function: &Function) { + self.iteration_binders.clear(); + self.match_binders.clear(); + self.ownership_locals = function + .locals + .iter() + .map(|local| crate::OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + is_view_parameter: function + .params + .iter() + .any(|param| param.local == local.id && param.mode == crate::ParamMode::View), + view_iteration: None, + match_binding: None, + }) + .collect(); + self.ownership_context = if function.debug_kind == crate::FunctionDebugKind::Inline { + jett_typecheck::CheckedOwnershipContext::Ordinary + } else { + match function.identity.declaration.kind { + crate::DeclarationKind::Verify => jett_typecheck::CheckedOwnershipContext::Verify, + crate::DeclarationKind::Property => { + jett_typecheck::CheckedOwnershipContext::Property + } + _ => jett_typecheck::CheckedOwnershipContext::Ordinary, + } + }; + self.local_views = function + .locals + .iter() + .map(|local| local.view_source) + .collect(); + self.local_types = function.locals.iter().map(|local| local.ty).collect(); + self.local_view_initializers.clear(); + for view in &function.actor_state_views { + if let Err(message) = view.validate(&function.locals, self.interner) { + self.error(function.span, message); + } + } + if let Some(entry) = &function.actor_handler { + for parameter in &function.params { + if entry.allows_state_view(function, parameter.local) { + self.local_view_initializers.insert(parameter.local); + } + } + } + self.borrowed_parameters = function + .params + .iter() + .filter(|param| param.mode == crate::ParamMode::View) + .map(|param| param.local) + .collect(); + let function_name = &function.identity.declaration.name; + for (index, type_argument) in function.identity.type_arguments.iter().enumerate() { + self.type_id( + *type_argument, + function.span, + format!("function `{function_name}` type argument {index}"), + ); + } + for binding in &function.identity.scoped_type_bindings { + self.type_id( + binding.ty, + function.span, + format!("function `{function_name}` scoped type `{}`", binding.name), + ); + } + for parameter in &function.params { + self.type_id( + parameter.ty, + parameter.span, + format!("function `{function_name}` parameter `{}`", parameter.name), + ); + } + self.type_id( + function.return_type, + function.span, + format!("function `{function_name}` return type"), + ); + for local in &function.locals { + self.type_id( + local.ty, + local.span, + format!("function `{function_name}` local `{}`", local.name), + ); + } + self.block(&function.body, function_name); + for view in &function.actor_state_views { + let matches=function.body.statements.iter().filter(|statement|matches!(&statement.kind,StatementKind::Let{local,value} if *local==view.local() && value==view.initializer())).count(); + if matches != 1 { + self.error( + function.span, + "actor constructor view lost its original initializer", + ); + } + } + for local in &function.locals { + if local.view_source.is_some() && !self.local_view_initializers.contains(&local.id) { + self.error(local.span, "borrowed local has no validated initializer"); + } + } + } + + fn block(&mut self, block: &Block, function_name: &str) { + for statement in &block.statements { + self.statement(statement, function_name); + } + } + + fn statement(&mut self, statement: &Statement, function_name: &str) { + match &statement.kind { + StatementKind::Let { local, value } => { + if let Some(Some(source)) = self.local_views.get(local.index() as usize) + && let Some(&ty) = self.local_types.get(local.index() as usize) + { + let source = *source; + let validation = self + .local_types + .get(source.index() as usize) + .ok_or("borrowed local origin is outside its function") + .and_then(|&source_type| { + crate::local_views::validate_local_view_initializer( + value, + source, + source_type, + ty, + self.interner, + ) + }); + match validation { + Ok(()) => { + self.local_view_initializers.insert(*local); + } + Err(message) => self.error(value.span, message), + } + } + self.expression(value, function_name); + } + StatementKind::HandleDefault(value) + | StatementKind::Expression(value) + | StatementKind::Respond(value) => self.expression(value, function_name), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + self.expression(target, function_name); + self.expression(value, function_name); + } + StatementKind::Return(value) => { + if let Some(value) = value { + self.expression(value, function_name); + } + } + StatementKind::If { + condition, + then_block, + else_block, + } => { + self.expression(condition, function_name); + self.block(then_block, function_name); + if let Some(else_block) = else_block { + self.block(else_block, function_name); + } + } + StatementKind::While { condition, body } => { + self.expression(condition, function_name); + self.block(body, function_name); + } + StatementKind::For { + key, + value, + by_view, + iterable, + body, + } => { + self.expression(iterable, function_name); + let saved_locals = self.ownership_locals.clone(); + for id in [*key].into_iter().chain(*value) { + if !self.iteration_binders.insert(id) || self.match_binders.contains(&id) { + self.error( + statement.span, + "iteration binding is declared more than once in its exact function", + ); + } + } + if *by_view { + let part = if (iterable.ty.index() as usize) < self.interner.len() + && matches!(self.interner.resolve(iterable.ty), Type::Map(..)) + { + crate::IterationPart::Key + } else { + crate::IterationPart::Element + }; + let bindings = [(*key, part)] + .into_iter() + .chain(value.map(|value| (value, crate::IterationPart::Value))); + for (id, part) in bindings { + let result = self.ownership_locals.get(id.index() as usize).copied().ok_or_else(|| "viewed iteration binder is outside its function".to_string()).and_then(|local| { + if local.mutable || local.view_source.is_some() || local.is_view_parameter || local.view_iteration.is_some() || local.match_binding.is_some() + || local.span.file != statement.span.file || local.span.start < statement.span.start || local.span.end > statement.span.end { + return Err("viewed iteration binder is not a fresh immutable local in its exact For".into()); + } + crate::checked_view_iteration_binding(self.interner, statement.span, iterable.ty, local.ty, part) + }); + match result { + Ok(proof) + if !jett_typecheck::ownership::is_implicitly_copyable( + self.interner, + proof.binder_type(), + ) => + { + self.ownership_locals[id.index() as usize].view_iteration = + Some(proof) + } + Ok(_) => {} + Err(message) => self.error(statement.span, message), + } + } + } + self.block(body, function_name); + self.ownership_locals = saved_locals; + } + StatementKind::Match { scrutinee, arms } => { + self.expression(scrutinee, function_name); + let saved_locals = self.ownership_locals.clone(); + for arm in arms { + self.ownership_locals = saved_locals.clone(); + for &id in &arm.bindings { + if !self.match_binders.insert(id) || self.iteration_binders.contains(&id) { + self.error( + arm.span, + "match payload binding is declared more than once", + ); + } + } + match crate::match_bindings::checked_match_arm_bindings( + self.interner, + statement.span, + scrutinee.ty, + arm.variant, + arm.span, + &arm.bindings, + |id| self.ownership_locals.get(id.index() as usize).copied(), + ) { + Ok(proofs) => { + for (id, proof) in proofs { + self.ownership_locals[id.index() as usize].match_binding = + Some(proof); + } + } + Err(message) => self.error(arm.span, message), + } + self.block(&arm.body, function_name); + } + self.ownership_locals = saved_locals; + } + StatementKind::Assert { condition, message } => { + self.expression(condition, function_name); + if let Some(message) = message { + self.expression(message, function_name); + } + } + StatementKind::Breakpoint { condition, .. } => { + let context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::BreakpointExpression, + ); + if let Some(condition) = condition { + self.expression(condition, function_name); + } + self.ownership_context = context; + } + StatementKind::Scope(block) => self.block(block, function_name), + StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info, function_name); + for arm in arms { + self.type_id( + arm.bound_type, + statement.span, + format!( + "function `{function_name}` reflected type arm {}", + arm.iteration_index + ), + ); + self.block(&arm.body, function_name); + } + } + StatementKind::Break | StatementKind::Continue | StatementKind::Trace(_) => {} + } + } + + fn expression(&mut self, expression: &Expression, function_name: &str) { + if let ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } = &expression.kind + { + let mut metadata_types = Vec::new(); + ownership.metadata_types(|ty| metadata_types.push(ty)); + for ty in metadata_types { + self.type_id(ty, expression.span, "call ownership metadata type".into()); + } + // Source and generated authority is checked before dead bodies/locals can be pruned. + if let Err(message) = crate::call_ownership::validate_hir_invocation( + self.functions, + &self.ownership_locals, + expression, + self.interner, + self.ownership_context, + ) { + self.error(expression.span, message); + } + } + self.type_id( + expression.ty, + expression.span, + format!("function `{function_name}` expression result"), + ); + match &expression.kind { + ExpressionKind::ActorGraphImport(plan) => { + for statement in &plan.body().statements { + self.statement(statement, function_name); + } + } + ExpressionKind::Comptime { + value, bindings, .. + } => { + for binding in bindings { + self.type_id( + binding.ty, + expression.span, + format!("comptime binding `{}`", binding.name), + ); + } + self.expression(value, function_name); + } + ExpressionKind::Binary { left, right, .. } => { + self.expression(left, function_name); + self.expression(right, function_name); + } + ExpressionKind::RuntimeFailureMessage(value) => { + self.expression(value, function_name); + if value.ty != TypeInterner::STRING || expression.ty != TypeInterner::NOTHING { + self.error( + expression.span, + "dynamic runtime failure requires exact string input and nothing output", + ); + } + } + ExpressionKind::DisplayResult(value) => { + self.expression(value, function_name); + if expression.ty != TypeInterner::STRING || value.ty != TypeInterner::STRING { + self.error( + expression.span, + "display result boundary requires exact string input and output", + ); + } + } + ExpressionKind::EquatableResult(value) => { + self.expression(value, function_name); + if expression.ty != value.ty || !qualified_bool(self.interner, value.ty) { + self.error( + expression.span, + "equality result boundary requires the same bool type with only outer secret qualification", + ); + } + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => self.expression(value, function_name), + ExpressionKind::InterfaceCoerce { value, adapters } => { + self.expression(value, function_name); + for entry in adapters { + self.type_id( + entry.source, + expression.span, + "container callback source".into(), + ); + self.type_id( + entry.target, + expression.span, + "container callback target".into(), + ); + } + } + ExpressionKind::ResourceHookValue { hook } => { + if !self.resource_manifest.contains_hook(hook) + || expression.ty != hook.function_type() + { + self.error( + expression.span, + "Resource descriptor differs from its original manifest or signature", + ); + } + } + ExpressionKind::ResourceInvoke { hook, args, .. } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "Resource invocation belongs to another original manifest", + ); + } + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::Call { args, .. } => { + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + field_validation, + args, + .. + } => { + if crate::is_reflected_field_intrinsic(*intrinsic) { + match (type_arguments.as_slice(), field_validation) { + ( + [owner, requested], + Some(crate::ReflectedFieldValidation::Validate(plans)), + ) if *requested == expression.ty => { + if let Err(message) = crate::validate_reflected_field_plans( + self.interner, + *intrinsic, + *owner, + *requested, + plans, + ) { + self.error(expression.span, message); + } + } + _ => self.error( + expression.span, + "reflected read has no complete checked validation plan", + ), + } + } else if field_validation.is_some() { + self.error( + expression.span, + "non-selector intrinsic carries reflected validation plans", + ); + } + for (index, type_argument) in type_arguments.iter().enumerate() { + self.type_id( + *type_argument, + expression.span, + format!("function `{function_name}` intrinsic type argument {index}"), + ); + } + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::IndirectCall { callee, args, .. } => { + self.expression(callee, function_name); + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::StructConstruct { + struct_type, + fields, + .. + } => { + self.type_id( + *struct_type, + expression.span, + format!("function `{function_name}` struct construction target"), + ); + for field in fields { + self.expression(field, function_name); + } + } + ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + .. + } => { + self.type_id( + *bitfield_type, + expression.span, + format!("function `{function_name}` bitfield construction target"), + ); + for field in fields { + self.expression(field, function_name); + } + } + ExpressionKind::MachineConstruct { + state_type, + payloads, + .. + } => { + self.type_id( + *state_type, + expression.span, + format!("function `{function_name}` machine construction state"), + ); + for payload in payloads { + self.expression(payload, function_name); + } + } + ExpressionKind::MachineTransition { + source, + state_type, + payloads, + .. + } => { + self.expression(source, function_name); + self.type_id( + *state_type, + expression.span, + format!("function `{function_name}` machine transition state"), + ); + for payload in payloads { + self.expression(payload, function_name); + } + } + ExpressionKind::ListConstruct { elements } => { + for element in elements { + self.expression(element, function_name); + } + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expression(&entry.key, function_name); + self.expression(&entry.value, function_name); + } + } + ExpressionKind::Handle { + target, + kind, + failure, + .. + } => { + self.expression(target, function_name); + match kind { + HandleKind::Refinement { + refined_type, + predicates, + } => { + self.type_id( + *refined_type, + expression.span, + format!("function `{function_name}` refinement handle target"), + ); + for predicate in predicates { + self.type_id( + predicate.refined_type, + expression.span, + format!("function `{function_name}` refinement predicate type"), + ); + } + } + HandleKind::Result | HandleKind::Optional => {} + HandleKind::ReflectedReturn { plan, .. } => { + self.type_id( + plan.source_type, + expression.span, + "reflected callable source return".into(), + ); + self.type_id( + plan.requested_type, + expression.span, + "reflected callable requested return".into(), + ); + if plan.source_type != target.ty || plan.requested_type != expression.ty { + self.error(expression.span, "reflected callable return types changed"); + } + } + } + self.block(failure, function_name); + } + ExpressionKind::EnumConstruct { + enum_type, + payloads, + .. + } => { + self.type_id( + *enum_type, + expression.span, + format!("function `{function_name}` enum construction target"), + ); + for payload in payloads { + self.expression(payload, function_name); + } + } + ExpressionKind::StringInterpolation(segments) => { + for segment in segments { + if let StringSegment::Value(value) = segment { + self.expression(value, function_name); + } + } + } + ExpressionKind::StateIs { value, .. } => self.expression(value, function_name), + ExpressionKind::InlineFunction { + scoped_type_bindings, + params, + view_params, + body, + .. + } => { + for binding in scoped_type_bindings { + self.type_id( + binding.ty, + expression.span, + format!( + "function `{function_name}` inline scoped type `{}`", + binding.name + ), + ); + } + let saved_locals = self.ownership_locals.clone(); + for local in &mut self.ownership_locals { + local.view_iteration = None; + local.match_binding = None; + local.is_view_parameter = false; + } + for parameter in params { + if let Some(local) = self.ownership_locals.get_mut(parameter.index() as usize) { + local.is_view_parameter = view_params.contains(parameter); + } + } + let context = std::mem::replace( + &mut self.ownership_context, + jett_typecheck::CheckedOwnershipContext::Ordinary, + ); + let enclosing_iteration_binders = std::mem::take(&mut self.iteration_binders); + let enclosing_match_binders = std::mem::take(&mut self.match_binders); + self.block(body, function_name); + self.iteration_binders = enclosing_iteration_binders; + self.match_binders = enclosing_match_binders; + self.ownership_context = context; + self.ownership_locals = saved_locals; + } + ExpressionKind::ActorSpawn { args, .. } + | ExpressionKind::ActorGraphStep { args, .. } => { + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::ActorMessage { actor, args, .. } => { + if let Err(message) = crate::validate_hir_invocation( + self.functions, + &self.ownership_locals, + expression, + self.interner, + self.ownership_context, + ) { + self.error(expression.span, message); + } + self.expression(actor, function_name); + for argument in args { + self.expression(argument, function_name); + } + } + ExpressionKind::Field { + base, owner_type, .. + } => { + self.expression(base, function_name); + self.type_id( + *owner_type, + expression.span, + format!("function `{function_name}` field owner"), + ); + } + ExpressionKind::ClosureRef { captures, .. } => { + if captures.iter().any(|id| { + (self + .ownership_locals + .get(id.index() as usize) + .is_some_and(|local| local.view_iteration.is_some()) + || self + .local_views + .get(id.index() as usize) + .is_some_and(Option::is_some) + || self.borrowed_parameters.contains(id)) + && self.local_types.get(id.index() as usize).is_some_and(|ty| { + (ty.index() as usize) < self.interner.len() + && !jett_typecheck::ownership::is_implicitly_copyable( + self.interner, + *ty, + ) + }) + }) { + self.error( + expression.span, + "native callbacks cannot capture a borrowed local alias", + ); + } + } + ExpressionKind::Int(_) + | ExpressionKind::Constant { .. } + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::Local(_) + | ExpressionKind::FunctionRef(_) + | ExpressionKind::OptionalNone => {} + } + } + + fn type_id(&mut self, type_id: TypeId, span: Span, context: String) { + if type_id.index() as usize >= self.interner.len() { + self.error( + span, + format!( + "backend HIR references unknown type ID {} in {context}", + type_id.index() + ), + ); + return; + } + if !self.visited_types.insert(type_id) { + return; + } + + // An unused nominal type parameter remains part of checked identity. + // It must be closed even when no payload field refers to its type. + let nominal_arguments = self.interner.nominal_type_arguments(type_id).to_vec(); + for (index, argument) in nominal_arguments.into_iter().enumerate() { + self.type_id( + argument, + span, + format!("{context} nominal type argument {index}"), + ); + } + + match self.interner.resolve(type_id).clone() { + Type::List(element) | Type::Set(element) | Type::Optional(element) => { + self.type_id(element, span, format!("{context} element type")); + } + Type::Secret(inner) => { + self.type_id(inner, span, format!("{context} secret value type")); + } + Type::Map(key, value) => { + self.type_id(key, span, format!("{context} map key type")); + self.type_id(value, span, format!("{context} map value type")); + } + Type::Result(value, error) => { + self.type_id(value, span, format!("{context} result value type")); + self.type_id(error, span, format!("{context} result error type")); + } + Type::Function { + params, + view_params, + return_type, + } => { + if params.len() != view_params.len() { + self.error( + span, + format!("{context} function view parameter count mismatch"), + ); + } + for (index, parameter) in params.into_iter().enumerate() { + self.type_id( + parameter, + span, + format!("{context} function parameter {index}"), + ); + } + self.type_id(return_type, span, format!("{context} function return type")); + } + Type::Refinement { name, base } => { + self.type_id(base, span, format!("{context} refinement `{name}` base")); + } + Type::Struct(id) => self.struct_definition(id, span, &context), + Type::Bitfield(id) => self.bitfield_definition(id, span, &context), + Type::Enum(id) => self.enum_definition(id, span, &context), + Type::Interface(id) => self.interface_definition(id, span, &context), + Type::Actor(id) => self.actor_definition(id, span, &context), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + self.machine_definition(id, span, &context); + } + Type::Error => self.error( + span, + format!("backend HIR contains unresolved `` type in {context}"), + ), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::TypeConstruction + | Type::Never + | Type::Capability(_) => {} + Type::Resource(_) => { + if !self.resource_manifest.contains_type(type_id) { + self.error(span, "Resource type requires the original checked manifest; raw HIR lowering cannot authorize its layout"); + } + } + } + } + + fn struct_definition(&mut self, id: StructId, span: Span, context: &str) { + if !self.visited_definitions.insert(DefinitionKey::Struct(id)) { + return; + } + let definition = self.interner.resolve_struct(id).clone(); + for (field_name, field_type) in definition.fields { + self.type_id( + field_type, + span, + format!( + "{context} struct `{}` field `{field_name}`", + definition.name + ), + ); + } + for signature in definition.methods { + self.function_signature( + signature, + span, + format!("{context} struct `{}`", definition.name), + ); + } + } + + fn bitfield_definition(&mut self, id: BitfieldId, span: Span, context: &str) { + if !self.visited_definitions.insert(DefinitionKey::Bitfield(id)) { + return; + } + let definition = self.interner.resolve_bitfield(id).clone(); + for field in definition.fields { + self.type_id( + field.ty, + span, + format!( + "{context} bitfield `{}` field `{}`", + definition.name, field.name + ), + ); + } + } + + fn enum_definition(&mut self, id: EnumId, span: Span, context: &str) { + if !self.visited_definitions.insert(DefinitionKey::Enum(id)) { + return; + } + let definition = self.interner.resolve_enum(id).clone(); + for variant in definition.variants { + for (field_name, field_type) in variant.fields { + self.type_id( + field_type, + span, + format!( + "{context} enum `{}` variant `{}` field `{field_name}`", + definition.name, variant.name + ), + ); + } + } + } + + fn interface_definition(&mut self, id: InterfaceId, span: Span, context: &str) { + if !self + .visited_definitions + .insert(DefinitionKey::Interface(id)) + { + return; + } + let definition = self.interner.resolve_interface(id).clone(); + for signature in definition.methods { + self.function_signature( + signature, + span, + format!("{context} interface `{}`", definition.name), + ); + } + } + + fn actor_definition(&mut self, id: ActorId, span: Span, context: &str) { + if !self.visited_definitions.insert(DefinitionKey::Actor(id)) { + return; + } + let definition = self.interner.resolve_actor(id).clone(); + for (name, type_id) in definition.capability_params { + self.type_id( + type_id, + span, + format!("{context} actor `{}` capability `{name}`", definition.name), + ); + } + for (name, type_id) in definition.state_fields { + self.type_id( + type_id, + span, + format!("{context} actor `{}` state `{name}`", definition.name), + ); + } + for message in definition.messages { + for (name, type_id) in message.params { + self.type_id( + type_id, + span, + format!( + "{context} actor `{}` message `{}` parameter `{name}`", + definition.name, message.name + ), + ); + } + self.type_id( + message.responds, + span, + format!( + "{context} actor `{}` message `{}` response", + definition.name, message.name + ), + ); + } + } + + fn machine_definition(&mut self, id: MachineId, span: Span, context: &str) { + if !self.visited_definitions.insert(DefinitionKey::Machine(id)) { + return; + } + let definition = self.interner.resolve_machine(id).clone(); + for state in definition.states { + for (field_name, field_type) in state.fields { + self.type_id( + field_type, + span, + format!( + "{context} machine `{}` state `{}` field `{field_name}`", + definition.name, state.name + ), + ); + } + } + } + + fn function_signature(&mut self, signature: FunctionSig, span: Span, context: String) { + for (parameter_name, parameter_type, _) in signature.params { + self.type_id( + parameter_type, + span, + format!( + "{context} method `{}` parameter `{parameter_name}`", + signature.name + ), + ); + } + self.type_id( + signature.return_type, + span, + format!("{context} method `{}` return type", signature.name), + ); + } +} + +fn qualified_bool(types: &TypeInterner, mut ty: TypeId) -> bool { + for _ in 0..types.len() { + if ty == TypeInterner::BOOL { + return true; + } + if ty.index() as usize >= types.len() { + return false; + } + match types.resolve(ty) { + Type::Secret(inner) => ty = *inner, + _ => return false, + } + } + false +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use jett_common::{FileId, SourceOrigin}; + use jett_types::{CapabilityKind, StructDef}; + + use super::*; + use crate::{ + Block, DeclarationId, DeclarationKind, Expression, ExpressionKind, Function, FunctionId, + FunctionIdentity, IntrinsicId, Program, Statement, + }; + + fn test_span() -> Span { + Span::new(FileId::new(0), 0, 1) + } + + fn program_with(return_type: TypeId, statements: Vec) -> Program { + let span = test_span(); + Program { + resource_manifest: crate::ResourceManifest::empty(), + resource_source: crate::ResourceSourceArchive::empty(), + equality_methods: Default::default(), + functions: vec![Function { + id: FunctionId(0), + identity: FunctionIdentity { + declaration: DeclarationId { + origin: SourceOrigin::Project, + namespace: "test".to_string(), + name: "main".to_string(), + kind: DeclarationKind::Function, + }, + scoped_type_bindings: Vec::new(), + type_arguments: Vec::new(), + specialization: Default::default(), + }, + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + debug_kind: crate::FunctionDebugKind::named("test", "main"), + params: Vec::new(), + capture_count: 0, + return_type, + locals: Vec::new(), + body: Block { statements, span }, + span, + }], + } + } + + #[test] + fn equatable_result_preserves_exact_bool_qualification_without_refinement_peeling() { + let mut interner = TypeInterner::new(); + let secret = interner.intern(Type::Secret(TypeInterner::BOOL)); + let nested_secret = interner.intern(Type::Secret(secret)); + let refined = interner.intern(Type::Refinement { + name: "test.Truth".into(), + base: TypeInterner::BOOL, + }); + let secret_refined = interner.intern(Type::Secret(refined)); + for (input, output, valid) in [ + (TypeInterner::BOOL, TypeInterner::BOOL, true), + (secret, secret, true), + (nested_secret, nested_secret, true), + (TypeInterner::BOOL, secret, false), + (secret, TypeInterner::BOOL, false), + (secret, nested_secret, false), + (refined, refined, false), + (secret_refined, secret_refined, false), + (TypeInterner::INT64, TypeInterner::INT64, false), + (TypeInterner::STRING, TypeInterner::BOOL, false), + ] { + let expression = Expression { + kind: ExpressionKind::EquatableResult(Box::new(Expression { + kind: ExpressionKind::Bool(true), + ty: input, + span: test_span(), + })), + ty: output, + span: test_span(), + }; + let program = program_with( + output, + vec![Statement { + kind: StatementKind::Return(Some(expression)), + span: test_span(), + }], + ); + let result = validate_backend_types(&program, &interner); + if valid { + result.expect("unchanged qualified bool boundary"); + } else { + let errors = result.expect_err("invalid equality boundary type"); + assert!(errors.iter().any(|error| error.message == + "equality result boundary requires the same bool type with only outer secret qualification")); + } + } + } + + #[test] + fn native_return_dynamic_failure_requires_exact_string_and_nothing() { + let mut interner = TypeInterner::new(); + let secret_string = interner.intern(Type::Secret(TypeInterner::STRING)); + for (input, output, valid) in [ + (TypeInterner::STRING, TypeInterner::NOTHING, true), + (TypeInterner::INT64, TypeInterner::NOTHING, false), + (secret_string, TypeInterner::NOTHING, false), + (TypeInterner::STRING, TypeInterner::STRING, false), + ] { + let boundary = Expression { + kind: ExpressionKind::RuntimeFailureMessage(Box::new(Expression { + kind: if input == TypeInterner::INT64 { + ExpressionKind::Int(7) + } else { + ExpressionKind::String("error".into()) + }, + ty: input, + span: test_span(), + })), + ty: output, + span: test_span(), + }; + let program = program_with( + TypeInterner::NOTHING, + vec![Statement { + kind: StatementKind::Expression(boundary), + span: test_span(), + }], + ); + let result = validate_backend_types(&program, &interner); + if valid { + result.expect("exact borrowed error boundary"); + } else { + assert!(result.expect_err("malformed dynamic error").iter().any(|error| + error.message == "dynamic runtime failure requires exact string input and nothing output")); + } + } + } + + #[test] + fn display_result_requires_exact_string_input_and_output() { + let mut interner = TypeInterner::new(); + let secret = interner.intern(Type::Secret(TypeInterner::STRING)); + for (input, output, valid) in [ + (TypeInterner::STRING, TypeInterner::STRING, true), + (TypeInterner::INT64, TypeInterner::STRING, false), + (TypeInterner::STRING, TypeInterner::INT64, false), + (secret, secret, false), + ] { + let expression = Expression { + kind: ExpressionKind::DisplayResult(Box::new(Expression { + kind: if input == TypeInterner::INT64 { + ExpressionKind::Int(1) + } else { + ExpressionKind::String("shown".into()) + }, + ty: input, + span: test_span(), + })), + ty: output, + span: test_span(), + }; + let program = program_with( + output, + vec![Statement { + kind: StatementKind::Return(Some(expression)), + span: test_span(), + }], + ); + let result = validate_backend_types(&program, &interner); + if valid { + result.expect("exact string boundary"); + } else { + let errors = result.expect_err("invalid display boundary types"); + assert!(errors.iter().any(|error| error.message + == "display result boundary requires exact string input and output")); + } + } + } + + #[test] + fn accepts_never_capabilities_and_recursive_nominal_types() { + let mut interner = TypeInterner::new(); + let recursive_id = interner.add_struct(StructDef { + name: "Recursive".to_string(), + fields: Vec::new(), + methods: Vec::new(), + }); + let recursive_type = interner.intern(Type::Struct(recursive_id)); + let optional_recursive = interner.intern(Type::Optional(recursive_type)); + let mut fields = vec![ + ("next".to_string(), optional_recursive), + ("bottom".to_string(), TypeInterner::NEVER), + ]; + fields.extend( + CapabilityKind::ALL + .into_iter() + .map(|kind| (kind.name().to_string(), TypeInterner::capability(kind))), + ); + interner.update_struct( + recursive_id, + StructDef { + name: "Recursive".to_string(), + fields, + methods: Vec::new(), + }, + ); + + validate_backend_types(&program_with(recursive_type, Vec::new()), &interner) + .expect("accepted backend types should validate"); + } + + #[test] + fn rejects_error_types_in_comptime_binding_contexts() { + let span = test_span(); + let statement = Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::Comptime { + scopes: Vec::new(), + source_span: span, + value: Box::new(Expression { + kind: ExpressionKind::Int(7), + ty: TypeInterner::INT64, + span, + }), + bindings: vec![crate::ScopedTypeBinding { + name: "Bound".into(), + ty: TypeInterner::ERROR, + reflection: jett_types::ReflectionTypeInfo { + type_name: "int64".into(), + kind: "primitive".into(), + primitive_tag: Some("int64".into()), + has_secret: false, + args: Vec::new(), + }, + }], + }, + ty: TypeInterner::INT64, + span, + }), + span, + }; + let errors = validate_backend_types( + &program_with(TypeInterner::NOTHING, vec![statement]), + &TypeInterner::new(), + ) + .expect_err("comptime type bindings are backend-visible"); + assert!( + errors + .iter() + .any(|error| error.message.contains("comptime binding `Bound`") + && error.message.contains("unresolved `` type")) + ); + } + + #[test] + fn backend_type_gate_visits_unused_nominal_argument_edges() { + for invalid in [false, true] { + let mut interner = TypeInterner::new(); + let marker = interner.add_struct(StructDef { + name: "test.Marker".into(), + fields: Vec::new(), + methods: Vec::new(), + }); + let marker = interner.intern(Type::Struct(marker)); + let argument = if invalid { + interner.intern(Type::Optional(TypeInterner::ERROR)) + } else { + TypeInterner::INT64 + }; + interner + .register_nominal_type_arguments(marker, vec![argument]) + .unwrap(); + let result = validate_backend_types(&program_with(marker, Vec::new()), &interner); + if invalid { + let errors = result.expect_err("unused nominal arguments must be closed"); + assert!( + errors + .iter() + .any(|error| error.message.contains("nominal type argument 0") + && error.message.contains("unresolved `` type")) + ); + } else { + result.expect("closed unused nominal argument"); + } + } + } + + #[test] + fn rejects_a_direct_error_type() { + let interner = TypeInterner::new(); + let errors = + validate_backend_types(&program_with(TypeInterner::ERROR, Vec::new()), &interner) + .expect_err("the error sentinel must not reach a backend"); + + assert!(errors.iter().any(|error| { + error.message.contains("unresolved `` type") + && error.message.contains("return type") + })); + } + + #[test] + fn rejects_an_error_nested_in_a_recursive_definition() { + let mut interner = TypeInterner::new(); + let recursive_id = interner.add_struct(StructDef { + name: "Recursive".to_string(), + fields: Vec::new(), + methods: Vec::new(), + }); + let recursive_type = interner.intern(Type::Struct(recursive_id)); + let optional_recursive = interner.intern(Type::Optional(recursive_type)); + let error_list = interner.intern(Type::List(TypeInterner::ERROR)); + let nested_error = interner.intern(Type::Map(TypeInterner::STRING, error_list)); + interner.update_struct( + recursive_id, + StructDef { + name: "Recursive".to_string(), + fields: vec![ + ("next".to_string(), optional_recursive), + ("broken".to_string(), nested_error), + ], + methods: Vec::new(), + }, + ); + + let errors = validate_backend_types(&program_with(recursive_type, Vec::new()), &interner) + .expect_err("a nested error sentinel must not reach a backend"); + + assert!(errors.iter().any(|error| { + error.message.contains("unresolved `` type") + && error.message.contains("field `broken`") + && error.message.contains("map value type") + })); + } + + #[test] + fn rejects_error_types_in_hir_intrinsic_operands() { + let mut interner = TypeInterner::new(); + let nested_error = interner.intern(Type::Optional(TypeInterner::ERROR)); + let span = test_span(); + let statement = Statement { + kind: StatementKind::Expression(Expression { + kind: ExpressionKind::Intrinsic { + ownership: crate::CallOwnership::generated( + crate::GeneratedOperation::ReflectionMetadata { + intrinsic: IntrinsicId::TypeName, + }, + &[], + &[], + &[], + TypeInterner::STRING, + &[], + &interner, + ) + .unwrap(), + intrinsic: IntrinsicId::TypeName, + type_arguments: vec![nested_error], + reflection_arguments: Vec::new(), + refinement_predicates: Vec::new(), + field_validation: None, + args: Vec::new(), + evaluation_order: Vec::new(), + }, + ty: TypeInterner::STRING, + span, + }), + span, + }; + + let errors = validate_backend_types( + &program_with(TypeInterner::NOTHING, vec![statement]), + &interner, + ) + .expect_err("intrinsic type operands are backend-visible"); + + assert!(errors.iter().any(|error| { + error.message.contains("unresolved `` type") + && error.message.contains("intrinsic type argument 0") + })); + } + + #[test] + fn hir_lowering_runs_the_backend_type_gate() { + let source = "function main() returns int64:\n return 1\n"; + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + let resolved = jett_resolve::resolve(&parsed.module); + let mut checked = jett_typecheck::check(&parsed.module, &resolved); + let mut replaced = 0; + for type_id in checked.type_map.values_mut() { + if *type_id == TypeInterner::INT64 { + *type_id = TypeInterner::ERROR; + replaced += 1; + } + } + assert!(replaced > 0, "the literal should have a checked type"); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + + let errors = crate::lower(&parsed.module, &resolved, &checked, &origins) + .expect_err("HIR lowering must reject backend-ineligible types"); + + assert!( + errors + .iter() + .any(|error| error.message.contains("unresolved `` type")) + ); + } +} diff --git a/crates/jett_hir/src/value_conversion_receipt.rs b/crates/jett_hir/src/value_conversion_receipt.rs new file mode 100644 index 00000000..b6ee5215 --- /dev/null +++ b/crates/jett_hir/src/value_conversion_receipt.rs @@ -0,0 +1,87 @@ +//! Ephemeral proof of one private canonical coercer execution, not a reseal API. +use crate::{LowerError, Program}; +use jett_common::{FileId, Span}; +use jett_types::TypeInterner; + +struct Conversion<'types> { + types: &'types TypeInterner, + before: Program, + after: Program, +} + +/// No HIR node or archive stores this temporary producer receipt. +/// The private boxed snapshots cannot refer back to the newly created receipt. +pub(crate) struct CanonicalValueConversion<'types>(Box>); + +fn metadata_equal(left: &Program, right: &Program) -> bool { + left.resource_source == right.resource_source + && left.resource_manifest == right.resource_manifest + && left.equality_methods == right.equality_methods +} + +fn error(program: &Program, message: &str) -> Vec { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(FileId::new(0), 0, 0), |function| function.span), + message: message.into(), + }] +} + +/// Visible only to interface_values, whose completion path is the sole +/// production caller. No caller supplies an output table or conversion callback. +pub(super) fn coerce<'types>( + program: &mut Program, + before: &Program, + types: &'types TypeInterner, +) -> Result, Vec> { + if !metadata_equal(program, before) + || !crate::resource_materialization::functions_equal(&program.functions, &before.functions) + { + return Err(error( + before, + "canonical conversion changed its complete input before execution", + )); + } + super::coerce_program(program, types); + if !metadata_equal(program, before) { + return Err(error( + before, + "canonical coercer changed protected non-function metadata", + )); + } + Ok(CanonicalValueConversion(Box::new(Conversion { + types, + before: before.clone(), + after: program.clone(), + }))) +} + +impl CanonicalValueConversion<'_> { + /// Authenticate the complete producer preimage and retain every actual + /// occurrence-specific callable token, including appended adapter bodies. + pub(crate) fn replay( + &self, + before: &Program, + types: &TypeInterner, + ) -> Result> { + if !std::ptr::eq(types, self.0.types) + || !metadata_equal(before, &self.0.before) + || !crate::resource_materialization::functions_equal( + &before.functions, + &self.0.before.functions, + ) + { + return Err(error( + before, + "canonical conversion receipt has another complete input or type arena", + )); + } + Ok(self.0.after.clone()) + } +} + +#[cfg(test)] +#[path = "value_conversion_receipt_tests.rs"] +mod tests; diff --git a/crates/jett_hir/src/value_conversion_receipt_tests.rs b/crates/jett_hir/src/value_conversion_receipt_tests.rs new file mode 100644 index 00000000..14f56079 --- /dev/null +++ b/crates/jett_hir/src/value_conversion_receipt_tests.rs @@ -0,0 +1,613 @@ +use super::*; +use crate::{ + ExpressionKind, Function, ReflectedCallablePlan, ResourceSourceArchive, StatementKind, +}; +use jett_common::SourceOrigin; +use jett_typecheck::CheckedResourceProgram; +use std::collections::HashMap; +use std::sync::Arc; + +// Both required occurrences come from the real closed reflected read. The +// ordinary temporary owner shares their immutable checked archive. +const SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +struct Holder: + callback: function() returns int64 +function five() returns int64: + return 5 +function positive() returns Positive: + return 5 +function stored() returns function() returns Positive: + Holder holder = Holder(callback: five) + mutable function() returns Positive selected = positive + for field in type.fields[Holder](): + selected = type.field_value[Holder, function() returns Positive](view holder, view field) + return selected +struct Packet: + data: list[int64] + sibling: list[int64] +function make_packet() returns Packet: + return Packet(data: list(1, 2), sibling: list(3, 4)) +function inspect(view data: list[int64]) returns nothing: + return nothing +function main() returns nothing: + function() returns Positive first = comptime stored() + function() returns Positive second = comptime stored() + int64 first_value = coarsen first() + int64 second_value = coarsen second() + list[int64] borrowed = view make_packet().data + inspect(view borrowed) + list[int64] independent = clone borrowed +"#; + +fn checked(source: &str, release: bool) -> Arc { + let file = FileId::new(0); + Arc::new( + CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release }, + ) + .unwrap(), + ) +} + +fn staged_source(source: &str, release: bool) -> (Program, Arc) { + let checked = checked(source, release); + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new( + jett_comptime::checked_types::CheckedExpressionTypes::from_checked_program(&checked), + ), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + let mut program = crate::lower_checked_ordinary_program(&checked).unwrap(); + crate::bake_required_values( + &mut program, + &evaluation.values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + (program, checked) +} + +fn staged(release: bool) -> (Program, Arc) { + let (program, checked) = staged_source(SOURCE, release); + assert_eq!(staged_plans(&program).len(), 2); + (program, checked) +} + +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() +} + +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap() +} + +fn staged_plans(program: &Program) -> Vec { + let mut plans = Vec::new(); + for function in &program.functions { + let mut body = function.body.clone(); + crate::resource_materialization::walk_block(&mut body, &mut |value| { + if let ExpressionKind::InterfaceCoerce { adapters, .. } = &value.kind { + plans.extend(adapters.iter().filter_map(|entry| { + entry + .reflected + .as_ref() + .filter(|plan| plan.is_materialized() && !plan.is_sealed()) + .cloned() + })); + } + Ok(()) + }) + .unwrap(); + } + plans +} + +fn materialized_plans(program: &Program) -> Vec { + program + .functions + .iter() + .flat_map(crate::reflected_callable_plans) + .filter(ReflectedCallablePlan::is_materialized) + .collect() +} + +fn required_statements(program: &Program) -> Vec { + main(program) + .body + .statements + .iter() + .enumerate() + .filter_map(|(index, statement)| { + let StatementKind::Let { value, .. } = &statement.kind else { + return None; + }; + let ExpressionKind::InterfaceCoerce { adapters, .. } = &value.kind else { + return None; + }; + adapters + .iter() + .any(|entry| { + entry + .reflected + .as_ref() + .is_some_and(|plan| plan.is_materialized()) + }) + .then_some(index) + }) + .collect() +} + +fn replace_first_staged_plan(program: &mut Program, plan: ReflectedCallablePlan) { + let mut replacement = Some(plan); + for function in &mut program.functions { + crate::resource_materialization::walk_block(&mut function.body, &mut |value| { + if replacement.is_some() { + if let ExpressionKind::InterfaceCoerce { adapters, .. } = &mut value.kind { + if let Some(entry) = adapters.iter_mut().find(|entry| { + entry + .reflected + .as_ref() + .is_some_and(|plan| plan.is_materialized()) + }) { + entry.reflected = replacement.take(); + } + } + } + Ok(()) + }) + .unwrap(); + } + assert!(replacement.is_none()); +} + +fn same_program(left: &Program, right: &Program) -> bool { + metadata_equal(left, right) + && crate::resource_materialization::functions_equal(&left.functions, &right.functions) +} + +#[test] +fn canonical_conversion_composes_fresh_required_callables_with_a_genuine_temporary() { + for release in [false, true] { + let (before, checked) = staged(release); + let original_source = before.resource_source.functions().to_vec(); + let types = &checked.checked().interner; + let mut program = before.clone(); + crate::complete_value_conversions(&mut program, types).unwrap(); + program + .resource_source + .validate_current(&program, types) + .unwrap(); + let plans = materialized_plans(&program); + assert_eq!(plans.len(), 2); + assert!( + plans + .iter() + .all(|plan| plan.is_sealed() && plan.validate_original()) + ); + assert_ne!( + plans[0], plans[1], + "equal sampled values keep separate occurrences" + ); + assert_eq!( + crate::temporary_local_backings(&program, main(&program), types) + .unwrap() + .len(), + 1 + ); + assert!(crate::resource_materialization::functions_equal( + program.resource_source.functions(), + &original_source, + )); + let converted_functions = program.functions.clone(); + crate::complete_value_conversions(&mut program, types).unwrap(); + program + .resource_source + .validate_current(&program, types) + .unwrap(); + assert!(crate::resource_materialization::functions_equal( + &converted_functions, + &program.functions, + )); + assert_eq!(plans, materialized_plans(&program)); + } +} + +#[test] +fn canonical_conversion_receipt_replays_every_actual_bound_occurrence_without_reminting() { + let (before, checked) = staged(false); + let types = &checked.checked().interner; + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + assert_eq!(actual.resource_source, before.resource_source); + let actual_plans = materialized_plans(&actual); + assert_eq!(actual_plans.len(), 2); + assert_ne!(actual_plans[0], actual_plans[1]); + assert!(actual_plans.iter().all(|plan| !plan.is_sealed())); + assert!(actual.functions.len() > before.functions.len()); + for _ in 0..3 { + let replay = receipt.replay(&before, types).unwrap(); + assert!(same_program(&actual, &replay)); + assert_eq!(actual_plans, materialized_plans(&replay)); + } + crate::temporary_local_views::conversion(&actual, &before, types, &receipt) + .unwrap() + .unwrap(); +} + +#[test] +fn canonical_conversion_receipt_refuses_another_execution_of_the_same_checked_input() { + let (before, checked) = staged(false); + let types = &checked.checked().interner; + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + let mut independent = before.clone(); + let independent_receipt = coerce(&mut independent, &before, types).unwrap(); + assert!(!crate::resource_materialization::functions_equal( + &actual.functions, + &independent.functions, + )); + assert!( + crate::temporary_local_views::conversion(&independent, &before, types, &receipt).is_err() + ); + crate::temporary_local_views::conversion(&independent, &before, types, &independent_receipt) + .unwrap() + .unwrap(); +} + +#[derive(Clone, Copy, Debug)] +enum InputEdit { + ForeignPlan, + ForeignArchive, + BodySpan, + LocalType, + FunctionHeader, + EqualityMethods, + DroppedOccurrence, + DuplicatedOccurrence, + ReorderedOccurrences, + DroppedFunction, + DuplicatedFunction, + ReorderedFunctions, +} + +fn edit_input(program: &mut Program, foreign: &Program, edit: InputEdit) { + match edit { + InputEdit::ForeignPlan => { + replace_first_staged_plan(program, staged_plans(foreign)[0].clone()) + } + InputEdit::ForeignArchive => program.resource_source = foreign.resource_source.clone(), + InputEdit::BodySpan => main_mut(program).body.span.end += 1, + InputEdit::LocalType => { + main_mut(program) + .locals + .iter_mut() + .find(|local| local.name == "borrowed") + .unwrap() + .ty = TypeInterner::BOOL; + } + InputEdit::FunctionHeader => main_mut(program).capture_count += 1, + InputEdit::EqualityMethods => { + let owner = main(program).id; + program.equality_methods.insert(TypeInterner::BOOL, owner); + } + InputEdit::DroppedOccurrence => { + let indices = required_statements(program); + main_mut(program).body.statements.remove(indices[0]); + } + InputEdit::DuplicatedOccurrence => { + let indices = required_statements(program); + let statement = main(program).body.statements[indices[0]].clone(); + main_mut(program) + .body + .statements + .insert(indices[0], statement); + } + InputEdit::ReorderedOccurrences => { + let indices = required_statements(program); + main_mut(program) + .body + .statements + .swap(indices[0], indices[1]); + } + InputEdit::DroppedFunction => { + program.functions.pop(); + } + InputEdit::DuplicatedFunction => { + let function = program.functions.last().unwrap().clone(); + program.functions.push(function); + } + InputEdit::ReorderedFunctions => program.functions.swap(0, 1), + } +} + +#[test] +fn canonical_conversion_receipt_refuses_foreign_or_edited_complete_preimages() { + let (before, checked) = staged(false); + let (foreign, foreign_checked) = staged(false); + let types = &checked.checked().interner; + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + assert!( + receipt + .replay(&before, &foreign_checked.checked().interner) + .is_err() + ); + for edit in [ + InputEdit::ForeignPlan, + InputEdit::ForeignArchive, + InputEdit::BodySpan, + InputEdit::LocalType, + InputEdit::FunctionHeader, + InputEdit::EqualityMethods, + InputEdit::DroppedOccurrence, + InputEdit::DuplicatedOccurrence, + InputEdit::ReorderedOccurrences, + InputEdit::DroppedFunction, + InputEdit::DuplicatedFunction, + InputEdit::ReorderedFunctions, + ] { + let mut altered = before.clone(); + edit_input(&mut altered, &foreign, edit); + assert!(receipt.replay(&altered, types).is_err(), "{edit:?}"); + let altered_before = altered.clone(); + assert!(coerce(&mut altered, &before, types).is_err(), "{edit:?}"); + assert!(same_program(&altered, &altered_before), "{edit:?}"); + } +} + +#[derive(Clone, Copy, Debug)] +enum OutputEdit { + AdapterBody, + PredicateBody, + DroppedOccurrence, + DuplicatedOccurrence, + ReorderedOccurrences, + DroppedAdapter, + DuplicatedAdapter, + ReorderedAdapters, +} + +fn converted_required_statements(program: &Program, original_count: usize) -> Vec { + main(program) + .body + .statements + .iter() + .enumerate() + .filter_map(|(index, statement)| { + let StatementKind::Let { value, .. } = &statement.kind else { + return None; + }; + let ExpressionKind::FunctionAdapter { function, .. } = &value.kind else { + return None; + }; + (function.index() as usize >= original_count).then_some(index) + }) + .collect() +} + +fn edit_output(program: &mut Program, original_count: usize, edit: OutputEdit) { + match edit { + OutputEdit::AdapterBody => { + let mut edited = false; + crate::resource_materialization::walk_block( + &mut program.functions[original_count].body, + &mut |expression| { + if let ExpressionKind::IndirectCall { callee, .. } = &mut expression.kind { + callee.kind = ExpressionKind::Nothing; + edited = true; + } + Ok(()) + }, + ) + .unwrap(); + assert!(edited); + } + OutputEdit::PredicateBody => { + let plans = materialized_plans(program); + let predicate = plans[0].return_plan().predicates()[0].function; + let mut edited = false; + crate::resource_materialization::walk_block( + &mut program.functions[predicate.index() as usize].body, + &mut |expression| { + if let ExpressionKind::Int(value) = &mut expression.kind { + *value += 1; + edited = true; + } + Ok(()) + }, + ) + .unwrap(); + assert!(edited); + } + OutputEdit::DroppedOccurrence => { + let indices = converted_required_statements(program, original_count); + main_mut(program).body.statements.remove(indices[0]); + } + OutputEdit::DuplicatedOccurrence => { + let indices = converted_required_statements(program, original_count); + let statement = main(program).body.statements[indices[0]].clone(); + main_mut(program) + .body + .statements + .insert(indices[0], statement); + } + OutputEdit::ReorderedOccurrences => { + let indices = converted_required_statements(program, original_count); + main_mut(program) + .body + .statements + .swap(indices[0], indices[1]); + } + OutputEdit::DroppedAdapter => { + program.functions.pop(); + } + OutputEdit::DuplicatedAdapter => { + let function = program.functions.last().unwrap().clone(); + program.functions.push(function); + } + OutputEdit::ReorderedAdapters => program.functions.swap(original_count, original_count + 1), + } +} + +#[test] +fn canonical_conversion_receipt_does_not_authorize_edited_or_reordered_output_occurrences() { + let (before, checked) = staged(false); + let types = &checked.checked().interner; + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + assert_eq!(actual.functions.len(), before.functions.len() + 2); + assert_eq!( + converted_required_statements(&actual, before.functions.len()).len(), + 2 + ); + for edit in [ + OutputEdit::AdapterBody, + OutputEdit::PredicateBody, + OutputEdit::DroppedOccurrence, + OutputEdit::DuplicatedOccurrence, + OutputEdit::ReorderedOccurrences, + OutputEdit::DroppedAdapter, + OutputEdit::DuplicatedAdapter, + OutputEdit::ReorderedAdapters, + ] { + let mut altered = actual.clone(); + edit_output(&mut altered, before.functions.len(), edit); + assert!( + crate::temporary_local_views::conversion(&altered, &before, types, &receipt).is_err(), + "{edit:?}" + ); + } +} + +#[test] +fn canonical_conversion_failure_keeps_the_complete_input_unpublished() { + let (mut invalid, checked) = staged(false); + let types = &checked.checked().interner; + // A private negative control removes Source authority while retaining its + // real staged occurrences. The early baked/Graph gates are empty; canonical + // coercion changes the candidate before the temporary gate rejects it. + invalid.resource_source = ResourceSourceArchive::empty(); + crate::reflected_callable::baked::validate_conversion_input(&invalid).unwrap(); + crate::actor_graph::validate_conversion_input(&invalid).unwrap(); + let before = invalid.clone(); + let mut preview = before.clone(); + let receipt = coerce(&mut preview, &before, types).unwrap(); + assert!(!same_program(&preview, &before)); + drop(receipt); + assert!(crate::complete_value_conversions(&mut invalid, types).is_err()); + assert!(same_program(&invalid, &before)); + assert_eq!(staged_plans(&invalid).len(), 2); +} + +#[test] +fn canonical_conversion_receipt_checks_hidden_arc_identities_even_without_original_data() { + let (genuine, checked) = staged(false); + let types = &checked.checked().interner; + let mut before = genuine.clone(); + before.resource_source = ResourceSourceArchive::empty(); + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + let mut reflected = ResourceSourceArchive::empty(); + reflected.reflected_values = genuine.resource_source.reflected_values.clone(); + assert!(reflected.reflected_values.is_some()); + let mut execution = ResourceSourceArchive::empty(); + execution.install_materialized(crate::resource_source::Materialized { + functions: genuine.functions.clone(), + values: genuine.resource_source.required_materializations().to_vec(), + required_only: genuine + .resource_source + .required_only_function_ids() + .to_vec(), + }); + let graph_source = "namespace app\nactor Counter:\n mutable int64 count = 1\n receive read responds int64:\n respond count\nfunction make_counter() returns Counter:\n return comptime spawn Counter()\nfunction main() returns nothing:\n return nothing\n"; + let (graph_program, _) = staged_source(graph_source, false); + let mut graph = ResourceSourceArchive::empty(); + graph.graph_values = graph_program.resource_source.graph_values.clone(); + assert!(graph.graph_values.is_some()); + assert_eq!(before.resource_source, before.resource_source.clone()); + for hidden in [reflected, execution, graph] { + let mut altered = before.clone(); + altered.resource_source = hidden; + assert_ne!(before.resource_source, altered.resource_source); + assert!(receipt.replay(&altered, types).is_err()); + } +} + +fn set_float(program: &mut Program, value: f64) { + let mut count = 0; + for function in &mut program.functions { + crate::resource_materialization::walk_block(&mut function.body, &mut |expression| { + if let ExpressionKind::Float(literal) = &mut expression.kind { + *literal = value; + count += 1; + } + Ok(()) + }) + .unwrap(); + } + assert_eq!(count, 1); +} + +#[test] +fn canonical_conversion_receipt_preserves_nan_payloads_and_rejects_float_bit_edits() { + let source = "namespace app\nfunction floating() returns float64:\n return 0.0\nfunction main() returns nothing:\n return nothing\n"; + let checked = checked(source, false); + let types = &checked.checked().interner; + let original = crate::lower_checked_ordinary_program(&checked).unwrap(); + // NaNs are synthetic private equality controls, not Source admission claims. + for (accepted, changed) in [ + (0.0, -0.0), + ( + f64::from_bits(0x7ff8_0000_0000_0001), + f64::from_bits(0x7ff8_0000_0000_0002), + ), + ] { + let mut before = original.clone(); + set_float(&mut before, accepted); + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, types).unwrap(); + let replay = receipt.replay(&before, types).unwrap(); + assert!(same_program(&actual, &replay)); + let mut altered = before.clone(); + set_float(&mut altered, changed); + assert!(receipt.replay(&altered, types).is_err()); + } +} + +#[test] +fn canonical_conversion_receipt_has_no_retained_arc_cycle() { + let (before, checked) = staged(false); + let weak = Arc::downgrade(&checked); + let mut actual = before.clone(); + let receipt = coerce(&mut actual, &before, &checked.checked().interner).unwrap(); + let replay = receipt + .replay(&before, &checked.checked().interner) + .unwrap(); + drop(replay); + drop(receipt); + drop(actual); + drop(before); + drop(checked); + assert!(weak.upgrade().is_none()); +} diff --git a/crates/jett_intrinsics/Cargo.toml b/crates/jett_intrinsics/Cargo.toml new file mode 100644 index 00000000..81246f31 --- /dev/null +++ b/crates/jett_intrinsics/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jett_intrinsics" +description = "Closed compiler intrinsic identities shared across Jett phases" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] diff --git a/crates/jett_intrinsics/src/lib.rs b/crates/jett_intrinsics/src/lib.rs new file mode 100644 index 00000000..04c0d118 --- /dev/null +++ b/crates/jett_intrinsics/src/lib.rs @@ -0,0 +1,257 @@ +//! Closed identities for compiler-owned Jett operations. +//! +//! Source and stdlib functions remain ordinary functions. An operation enters +//! this set only when the compiler supplies its implementation. + +macro_rules! define_intrinsics { + ($( $variant:ident => $name:literal, )+) => { + /// One compiler-owned operation with a stable canonical identity. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum IntrinsicId { + $( $variant, )+ + } + + impl IntrinsicId { + /// Every intrinsic in canonical-name order. + pub const ALL: &'static [Self] = &[ + $( Self::$variant, )+ + ]; + + /// The stable spelling used in diagnostics and durable symbols. + pub const fn canonical_name(self) -> &'static str { + match self { + $( Self::$variant => $name, )+ + } + } + + /// Classify an exact canonical spelling. + pub fn from_canonical_name(name: &str) -> Option { + match name { + $( $name => Some(Self::$variant), )+ + _ => None, + } + } + } + }; +} + +define_intrinsics! { + BitfieldFromBytes => "bitfield.from_bytes", + BitfieldToBytes => "bitfield.to_bytes", + BytesConcat => "bytes.__concat", + BytesFromHex => "bytes.__from_hex", + BytesFromString => "bytes.__from_string", + BytesGet => "bytes.__get", + BytesLength => "bytes.__length", + BytesNew => "bytes.__new", + BytesSlice => "bytes.__slice", + BytesToHex => "bytes.__to_hex", + BytesToString => "bytes.__to_string", + ClockNow => "Clock.__now", + CryptoHmacSha256 => "crypto.__hmac_sha256", + CryptoMd5 => "crypto.__md5", + CryptoSha256 => "crypto.__sha256", + CryptoSha512 => "crypto.__sha512", + CsvParse => "csv.__parse", + CsvParseWithHeader => "csv.__parse_with_header", + CsvStringify => "csv.__stringify", + EncodingBase64Decode => "encoding.__base64_decode", + EncodingBase64Encode => "encoding.__base64_encode", + EncodingFormDecode => "encoding.__form_decode", + EncodingFormEncode => "encoding.__form_encode", + EncodingHexDecode => "encoding.__hex_decode", + EncodingUrlDecode => "encoding.__url_decode", + EncodingUrlEncode => "encoding.__url_encode", + EnvironmentArgs => "Environment.__args", + EnvironmentGet => "Environment.__get", + FilesystemReadFile => "Filesystem.read_file", + FilesystemWriteFile => "Filesystem.write_file", + Float32FromFloat64 => "float32.from_float64", + Float64FromInt64 => "float64.from_int64", + Float64FromString => "float64.from_string", + GraphicsRun => "graphics.__run", + Int64FromFloat64 => "int64.from_float64", + Int64FromString => "int64.from_string", + JsonParse => "json.parse", + JsonParseExact => "json.parse_exact", + JsonSerialize => "json.serialize", + JsonSerializePublic => "json.serialize_public", + ListAppend => "list.__append", + ListGetClone => "list.__get_clone", + ListGroupBy => "list.__group_by", + ListInsertAt => "list.__insert_at", + ListIsSorted => "list.__is_sorted", + ListLength => "list.__length", + ListNew => "list.__new", + ListRemoveAt => "list.__remove_at", + ListSort => "list.__sort", + ListSortBy => "list.__sort_by", + ListSortByIndex => "list.__sort_by_index", + ListSum => "list.__sum", + ListSwap => "list.__swap", + LogEmit => "log.__emit", + MapFromLists => "map.__from_lists", + MapGet => "map.__get", + MapHas => "map.__has", + MapInsert => "map.__insert", + MapLength => "map.__length", + MapNew => "map.__new", + MapRemove => "map.__remove", + MathAbs => "math.abs", + MathAverage => "math.__average", + MathCeil => "math.__ceil", + MathClamp => "math.__clamp", + MathCos => "math.__cos", + MathE => "math.__e", + MathFactorial => "math.__factorial", + MathFloor => "math.__floor", + MathGcd => "math.__gcd", + MathKernelAbs => "math.__abs", + MathKernelMax => "math.__max", + MathKernelMin => "math.__min", + MathLcm => "math.__lcm", + MathLog => "math.__log", + MathLog10 => "math.__log10", + MathLog2 => "math.__log2", + MathMax => "math.max", + MathMedian => "math.__median", + MathMin => "math.min", + MathMod => "math.__mod", + MathPi => "math.__pi", + MathPow => "math.__pow", + MathRound => "math.__round", + MathSin => "math.__sin", + MathSqrt => "math.__sqrt", + MathTan => "math.__tan", + Print => "print", + Println => "println", + RandomBool => "random.__bool", + RandomBounded => "random.__bounded", + RandomUnitFloat64 => "random.__unit_float64", + Range => "range", + SecretCompare => "secret.compare", + SecretRedact => "secret.redact", + SetAdd => "set.__add", + SetContains => "set.__contains", + SetLength => "set.__length", + SetNew => "set.__new", + SetRemove => "set.__remove", + StdoutWrite => "Stdout.write", + StringCharCount => "string.__char_count", + StringChars => "string.__chars", + StringCount => "string.__count", + StringFromBool => "string.__from_bool", + StringFromFloat64 => "string.__from_float64", + StringFromInt64 => "string.__from_int64", + StringFromUint64 => "string.__from_uint64", + StringIndexOf => "string.__index_of", + StringIsAlpha => "string.__is_alpha", + StringIsNumeric => "string.__is_numeric", + StringJoin => "string.__join", + StringLines => "string.__lines", + StringLower => "string.__lower", + StringRepeat => "string.__repeat", + StringReplace => "string.__replace", + StringSlice => "string.__slice", + StringSlugify => "string.__slugify", + StringSplit => "string.__split", + StringToLowerFirst => "string.__to_lower_first", + StringToUpperFirst => "string.__to_upper_first", + StringTrim => "string.__trim", + StringTrimEnd => "string.__trim_end", + StringTrimStart => "string.__trim_start", + StringUpper => "string.__upper", + StringWords => "string.__words", + TestMockClock => "test.mock.__clock", + TestMockEnvironment => "test.mock.__environment", + TestMockRandom => "test.mock.__random", + TypeArg => "type.arg", + TypeBitfieldFields => "type.bitfield_fields", + TypeBitfieldLayout => "type.bitfield_layout", + TypeConstructFinish => "type.construct_finish", + TypeConstructMachineStart => "type.construct_machine_start", + TypeConstructPut => "type.construct_put", + TypeConstructStart => "type.construct_start", + TypeConstructVariantStart => "type.construct_variant_start", + TypeFieldValue => "type.field_value", + TypeFields => "type.fields", + TypeHasSecret => "type.has_secret", + TypeInfo => "type.info", + TypeKind => "type.kind", + TypeKindTag => "type.kind_tag", + TypeMachineFieldValue => "type.machine_field_value", + TypeMachineLayout => "type.machine_layout", + TypeMachineStateValue => "type.machine_state_value", + TypeMachineStates => "type.machine_states", + TypeMachineTransitions => "type.machine_transitions", + TypeName => "type.name", + TypePrimitiveTag => "type.primitive_tag", + TypeVariantFieldValue => "type.variant_field_value", + TypeVariantValue => "type.variant_value", + TypeVariants => "type.variants", + Uint64FromString => "uint64.from_string", + UuidNew => "uuid.new", +} + +impl IntrinsicId { + /// Classify a checked call spelling. + /// + /// Bitfield conversion methods carry a nominal owner in source. Their + /// closed operation identity is independent of that owner; the HIR value + /// and result types retain the concrete bitfield type. + pub fn from_callable_name(name: &str) -> Option { + Self::from_canonical_name(name).or_else(|| { + let (_, method) = name.rsplit_once('.')?; + match method { + "from_bytes" => Some(Self::BitfieldFromBytes), + "to_bytes" => Some(Self::BitfieldToBytes), + _ => None, + } + }) + } +} + +impl std::fmt::Display for IntrinsicId { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.canonical_name()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn canonical_names_are_unique_and_round_trip() { + let mut names = HashSet::new(); + for &intrinsic in IntrinsicId::ALL { + let name = intrinsic.canonical_name(); + assert!(names.insert(name), "duplicate intrinsic name `{name}`"); + assert_eq!(IntrinsicId::from_canonical_name(name), Some(intrinsic)); + assert_eq!(IntrinsicId::from_callable_name(name), Some(intrinsic)); + } + } + + #[test] + fn checked_bitfield_method_names_have_closed_identities() { + assert_eq!( + IntrinsicId::from_callable_name("packet.Header.to_bytes"), + Some(IntrinsicId::BitfieldToBytes) + ); + assert_eq!( + IntrinsicId::from_callable_name("packet.Header.from_bytes"), + Some(IntrinsicId::BitfieldFromBytes) + ); + } + + #[test] + fn unknown_names_are_not_intrinsics() { + assert_eq!(IntrinsicId::from_callable_name("app.user_function"), None); + assert_eq!( + IntrinsicId::from_callable_name("type.future_operation"), + None + ); + } +} diff --git a/crates/jett_mir/Cargo.toml b/crates/jett_mir/Cargo.toml index 22a6e530..861df99c 100644 --- a/crates/jett_mir/Cargo.toml +++ b/crates/jett_mir/Cargo.toml @@ -8,10 +8,11 @@ license.workspace = true [dependencies] jett_common.workspace = true jett_hir.workspace = true +jett_typecheck.workspace = true jett_types.workspace = true [dev-dependencies] +jett_comptime.workspace = true jett_diagnostics.workspace = true jett_parser.workspace = true jett_resolve.workspace = true -jett_typecheck.workspace = true diff --git a/crates/jett_mir/src/actor_graph.rs b/crates/jett_mir/src/actor_graph.rs new file mode 100644 index 00000000..e6473520 --- /dev/null +++ b/crates/jett_mir/src/actor_graph.rs @@ -0,0 +1,239 @@ +//! Independent original import and current CFG seals. Public edits cannot mint them. +use super::*; +use std::sync::Arc; +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Witness { + original: Vec, + current: Arc, +} +#[derive(Debug, Clone)] +struct Snapshot { + id: FunctionId, + debug_kind: hir::FunctionDebugKind, + capture_count: usize, + span: Span, + identity: FunctionIdentity, + params: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, + return_type: TypeId, +} +impl PartialEq for Snapshot { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.debug_kind == other.debug_kind + && self.capture_count == other.capture_count + && self.span == other.span + && self.identity == other.identity + && self.params == other.params + && self.locals == other.locals + && self.entry == other.entry + && self.return_type == other.return_type + && breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} +fn snapshot(function: &Function) -> Snapshot { + Snapshot { + id: function.id, + debug_kind: function.debug_kind.clone(), + capture_count: function.capture_count, + span: function.span, + identity: function.identity.clone(), + params: function.params.clone(), + locals: function.locals.clone(), + blocks: function.blocks.clone(), + entry: function.entry, + return_type: function.return_type, + } +} +pub(super) fn has_records(function: &Function) -> bool { + function.actor_graph.is_some() +} +pub(super) fn seal( + original: &hir::Function, + lowered: &Function, + types: &TypeInterner, +) -> Result, String> { + let mut body = original.body.clone(); + let mut plans = Vec::new(); + hir::visit_actor_graph_plans(&mut body, |plan| plans.push(plan.clone()))?; + if plans.is_empty() { + let mut forged = false; + for block in &lowered.blocks { + crate::resource_ownership::visit_block(block, &mut |value| { + forged |= matches!(value.kind, hir::ExpressionKind::ActorGraphStep { .. }) + }); + } + if forged { + return Err( + "raw MIR import instruction has no original checked graph occurrence".into(), + ); + } + return Ok(None); + } + for plan in &plans { + plan.validate(original, types)?; + } + let mut actual = Vec::new(); + for block in &lowered.blocks { + crate::resource_ownership::visit_block(block, &mut |value| { + if let hir::ExpressionKind::ActorGraphStep { instruction, args } = &value.kind { + actual.push((instruction.clone(), args.clone(), value.ty)); + } + }); + } + let expected = plans + .iter() + .flat_map(|plan| plan.instructions()) + .collect::>(); + if actual.len() != expected.len() { + return Err("actor graph lowering changed instruction cardinality".into()); + } + // Blocks are allocated in lexical lowering order; each private instruction + // retains its required occurrence, exact local operands and operation index. + let mut seen = Vec::new(); + for (instruction, args, result) in &actual { + if !expected.iter().any(|expected| instruction == *expected) + || seen.iter().any(|other| instruction == other) + { + return Err( + "actor graph lowering changed or duplicated an original instruction".into(), + ); + } + instruction.validate(lowered.id, &lowered.identity, args, *result, types)?; + seen.push(instruction.clone()); + } + Ok(Some(Witness { + original: plans, + current: Arc::new(snapshot(lowered)), + })) +} +pub(super) fn validate(function: &Function) -> Result<(), String> { + if let Some(witness) = &function.actor_graph { + if *witness.current != snapshot(function) { + return Err( + "actor graph current CFG/local seal changed outside canonical preparation".into(), + ); + } + } else { + let mut forged = false; + for block in &function.blocks { + crate::resource_ownership::visit_block(block, &mut |value| { + forged |= matches!( + value.kind, + hir::ExpressionKind::ActorGraphStep { .. } + | hir::ExpressionKind::ActorGraphImport(_) + ) + }); + } + if forged { + return Err("actor graph execution has no original/current private seal".into()); + } + } + Ok(()) +} +pub(super) fn refresh(function: &mut Function) { + let current = Arc::new(snapshot(function)); + if let Some(witness) = &mut function.actor_graph { + witness.current = current; + } +} +pub fn validate_native_actor_graphs(program: &Program) -> Result<(), String> { + for function in &program.functions { + validate(function)?; + } + Ok(()) +} + +/// A function signature is metadata, not an occupied Resource leaf. This +/// classification is usable only for an exact factory expression/header retained +/// by the private graph plan and its independently validated current CFG seal. +pub(super) fn metadata_type(types: &TypeInterner, ty: TypeId) -> bool { + fn occupied( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) => true, + Type::Function { .. } => false, + Type::List(child) + | Type::Set(child) + | Type::Optional(child) + | Type::Secret(child) + | Type::Refinement { base: child, .. } => occupied(types, *child, seen), + Type::Map(a, b) | Type::Result(a, b) => { + occupied(types, *a, seen) || occupied(types, *b, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, child)| occupied(types, *child, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().any(|variant| { + variant + .fields + .iter() + .any(|(_, child)| occupied(types, *child, seen)) + }), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id).states.iter().any(|state| { + state + .fields + .iter() + .any(|(_, child)| occupied(types, *child, seen)) + }) + } + _ => false, + } + } + resource_type_pending(types, ty) && !occupied(types, ty, &mut std::collections::HashSet::new()) +} + +pub fn native_graph_metadata_expression( + function: &Function, + types: &TypeInterner, + value: &Expression, +) -> bool { + let Some(witness) = &function.actor_graph else { + return false; + }; + if validate(function).is_err() || !metadata_type(types, value.ty) { + return false; + } + witness.original.iter().any(|plan| { + let mut found = false; + let mut body = plan.body().clone(); + let result = hir::visit_actor_graph_factory_values(&mut body, |original| { + found |= breakpoint_regions::expressions_equal(original, value); + }); + result.is_ok() && found + }) +} + +pub fn native_graph_metadata_local( + function: &Function, + types: &TypeInterner, + local: &Local, +) -> bool { + let Some(witness) = &function.actor_graph else { + return false; + }; + if validate(function).is_err() || !metadata_type(types, local.ty) { + return false; + } + witness + .original + .iter() + .any(|plan| plan.factory_metadata_local(local, types)) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/actor_graph/tests.rs b/crates/jett_mir/src/actor_graph/tests.rs new file mode 100644 index 00000000..78a6f54a --- /dev/null +++ b/crates/jett_mir/src/actor_graph/tests.rs @@ -0,0 +1,333 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::{collections::HashMap, sync::Arc}; +const SOURCE: &str = r#"namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function make() returns Counter: + return comptime spawn Counter() +function main() returns nothing: + Counter worker = make() + int64 observed = ask worker.read + println(observed) + return nothing +"#; +fn prepared_source(source: &str) -> Arc { + let file = FileId::new(0); + Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + jett_parser::parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .expect("checked graph Source"), + ) +} +fn prepared() -> Arc { + prepared_source(SOURCE) +} +fn values( + checked: &Arc, +) -> jett_comptime::ExplicitComptimeValues { + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + assert!(evaluation.debug_events.is_empty()); + evaluation.values +} +fn graph() -> (Program, Arc) { + let checked = prepared(); + let values = values(&checked); + let mut hir = hir::lower_checked_ordinary_program(&checked).unwrap(); + hir::bake_required_values( + &mut hir, + &values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + hir::complete_value_conversions(&mut hir, &checked.checked().interner).unwrap(); + ( + crate::lower(&hir, &checked.checked().interner).unwrap(), + checked, + ) +} +#[test] +fn lowering_installs_independent_original_and_current_graph_seals() { + let (program, _) = graph(); + validate_native_actor_graphs(&program).unwrap(); + assert_eq!( + program + .functions + .iter() + .filter(|function| has_records(function)) + .count(), + 1 + ); + crate::validate(&program).unwrap(); +} +#[test] +fn raw_cfg_header_and_operand_edits_refuse() { + let (program, _) = graph(); + let index = program.functions.iter().position(has_records).unwrap(); + let mut changed = program.clone(); + changed.functions[index].capture_count += 1; + assert!(validate_native_actor_graphs(&changed).is_err()); + let mut changed = program.clone(); + changed.functions[index].blocks[0].statements.clear(); + assert!(validate_native_actor_graphs(&changed).is_err()); + let mut changed = program; + changed.functions[index].actor_graph = None; + assert!(validate_native_actor_graphs(&changed).is_err()); +} +#[test] +fn canonical_native_preparation_keeps_the_import_activation_seal() { + let (mut program, checked) = graph(); + let types = &checked.checked().interner; + crate::prepare_native_generated_functions(&mut program, types); + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + validate_native_actor_graphs(&program).unwrap(); + crate::validate(&program).unwrap(); +} + +#[test] +fn ordinary_shared_graph_factory_calls_keep_ordinary_storage_authority() { + let checked = prepared_source(include_str!( + "../../../jett_driver/tests/actor_graph/fixtures/02_shared_fresh.jett" + )); + let values = values(&checked); + let types = &checked.checked().interner; + let mut hir = hir::lower_checked_ordinary_program(&checked).unwrap(); + hir::bake_required_values( + &mut hir, + &values, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + hir::complete_value_conversions(&mut hir, types).unwrap(); + let mut program = crate::lower(&hir, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + let ownership = crate::validate_resource_ownership(&program, types).unwrap(); + for function in &program.functions { + if ownership + .required_only_function_ids() + .contains(&function.id) + { + continue; + } + let storage = if ownership.function(function.id).is_some() + || crate::native_graph_storage_has_records(function, types) + { + crate::ResourceCompanionPlan::analyze(&ownership, function.id) + .map(|plan| plan.storage().owned_locals.clone()) + } else { + crate::move_values::MoveValuePlan::analyze(&program, function, types) + .map(|plan| plan.owned_locals) + }; + assert!( + storage.is_ok(), + "{}: {:?}; graph rows: {}", + function.identity.declaration.name, + storage, + crate::native_graph_storage_has_records(function, types) + ); + } +} + +#[test] +fn map_shared_graph_iteration_prepares_ordinary_owned_element_storage() { + let source = include_str!("../../../jett_driver/tests/actor_graph/fixtures/10_map_shared.jett"); + let mut parsed = jett_parser::parse(source, FileId::new(0)); + let mut origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + for (index, support) in [ + include_str!("../../../../stdlib/map.jett"), + include_str!("../../../../stdlib/list.jett"), + ] + .into_iter() + .enumerate() + { + let file = FileId::new(10_000 + index as u32); + let support = jett_parser::parse(support, file); + assert!(support.errors.is_empty()); + let mut items = support.module.items; + items.append(&mut parsed.module.items); + parsed.module.items = items; + origins.insert(file, SourceOrigin::Stdlib); + } + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + origins, + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let values = values(&checked); + let types = &checked.checked().interner; + let mut hir = hir::lower_checked_ordinary_program(&checked).unwrap(); + hir::bake_required_values( + &mut hir, + &values, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + hir::complete_value_conversions(&mut hir, types).unwrap(); + let mut program = crate::lower(&hir, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + let main = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + assert!( + main.blocks + .iter() + .all(|block| !matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + validate_native_actor_graphs(&program).unwrap(); + let ownership = crate::validate_resource_ownership(&program, types).unwrap(); + let storage = crate::ResourceCompanionPlan::analyze(&ownership, main.id).unwrap(); + assert!(storage.storage().owned_locals.iter().any(|local| { + main.locals + .get(*local) + .is_some_and(|header| header.name.starts_with("$iterator")) + })); +} + +#[test] +fn protected_graph_list_iteration_transports_only_its_exact_generated_owner() { + let checked = prepared_source( + "namespace app\nactor Counter:\n mutable int64 count = 3\n receive read responds int64:\n respond count\nfunction main() returns nothing:\n list[Counter] workers = comptime list(spawn Counter())\n for worker in workers:\n println(ask worker.read)\n return nothing\n", + ); + let evaluated = values(&checked); + let types = &checked.checked().interner; + let mut hir = hir::lower_checked_ordinary_program(&checked).unwrap(); + hir::bake_required_values( + &mut hir, + &evaluated, + types, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + hir::complete_value_conversions(&mut hir, types).unwrap(); + let mut program = crate::lower(&hir, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + let main = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + assert!( + main.blocks + .iter() + .all(|block| !matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let ownership = crate::validate_resource_ownership(&program, types).unwrap(); + crate::ResourceCompanionPlan::analyze(&ownership, main.id).unwrap(); + let main_id = main.id; + let source = main + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::SequenceLength { + source: SequenceSource::Local(local), + .. + } => Some(*local), + _ => None, + }) + .unwrap(); + for forgery in 0..5 { + let mut forged = program.clone(); + let function = &mut forged.functions[main_id.index() as usize]; + match forgery { + 0 => function.locals[source.index() as usize].span.end += 1, + 1 => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements) + .find(|statement| matches!(&statement.kind, StatementKind::Let { local, .. } if *local == source)).unwrap(); + let StatementKind::Let { value, .. } = &mut statement.kind else { + unreachable!() + }; + value.span.end += 1; + } + 2 => { + let statement = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, StatementKind::SequenceGet { .. })) + .unwrap(); + let StatementKind::SequenceGet { consume, .. } = &mut statement.kind else { + unreachable!() + }; + *consume = false; + } + 3 => { + let statement = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, StatementKind::SequenceGet { .. })) + .unwrap(); + let StatementKind::SequenceGet { part, .. } = &mut statement.kind else { + unreachable!() + }; + *part = SequencePart::Value; + } + 4 => { + let block = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::SequenceGet { .. }) + }) + }) + .unwrap(); + block.statements.swap(0, 1); + } + _ => unreachable!(), + } + assert!( + crate::validate_resource_ownership(&forged, types).is_err(), + "accepted forgery {forgery}" + ); + } +} diff --git a/crates/jett_mir/src/actor_handlers.rs b/crates/jett_mir/src/actor_handlers.rs new file mode 100644 index 00000000..18c22364 --- /dev/null +++ b/crates/jett_mir/src/actor_handlers.rs @@ -0,0 +1,331 @@ +//! Private projection of a genuine HIR actor entry through canonical MIR edits. +use super::*; +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Witness { + source: Option, + constructor_views: Vec, + id: FunctionId, + identity: FunctionIdentity, + params: Vec, + captures: usize, + result: TypeId, + locals: Vec, + entry: BlockId, + blocks: Vec, + span: Span, +} +impl Witness { + fn current(&self, function: &Function) -> Result<(), String> { + if self.id != function.id + || self.identity != function.identity + || self.params != function.params + || self.captures != function.capture_count + || self.result != function.return_type + || self.locals != function.locals + || self.entry != function.entry + || self.blocks != function.blocks + || self.span != function.span + || self.constructor_views != function.actor_state_views + { + return Err( + "actor handler changed its original lowered body or role projection".into(), + ); + } + Ok(()) + } + fn refresh(&mut self, function: &Function) { + self.locals = function.locals.clone(); + self.entry = function.entry; + self.blocks = function.blocks.clone(); + } +} +pub(super) fn seal(source: &hir::Function, function: &Function) -> Result, String> { + if let Some(entry) = &source.actor_handler { + entry.validate(source)?; + } else if function.identity.declaration.kind == hir::DeclarationKind::ActorHandler { + return Err("actor handler has no original HIR entry".into()); + } else if source.actor_state_views.is_empty() { + return Ok(None); + } else if source + .actor_state_views + .iter() + .any(|view| !view.matches_constructor(source)) + { + return Err("actor constructor views lack their original Source function".into()); + } + if source.params != function.params + || source.capture_count != function.capture_count + || !function.locals.starts_with(&source.locals) + || source.id != function.id + || source.identity != function.identity + { + return Err("actor lowering changed its original checked role header".into()); + } + Ok(Some(Witness { + source: source.actor_handler.clone(), + constructor_views: source.actor_state_views.clone(), + id: function.id, + identity: function.identity.clone(), + params: function.params.clone(), + captures: function.capture_count, + result: function.return_type, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + span: function.span, + })) +} +pub(crate) fn has_records(function: &Function) -> bool { + function.actor_handler.is_some() || !function.actor_state_views.is_empty() +} +pub(super) fn validate(function: &Function) -> Result<(), String> { + match (&function.actor_handler, function.identity.declaration.kind) { + (Some(witness), hir::DeclarationKind::ActorHandler) if witness.source.is_some() => { + witness.current(function) + } + (Some(witness), hir::DeclarationKind::ActorConstructor) + if witness.source.is_none() && !witness.constructor_views.is_empty() => + { + witness.current(function) + } + (None, hir::DeclarationKind::ActorHandler) => { + Err("actor handler lacks original native entry receipt".into()) + } + (Some(_), _) => Err("actor handler receipt attached to foreign function".into()), + (None, _) if !function.actor_state_views.is_empty() => { + Err("actor state views lack original constructor witness".into()) + } + (None, _) => Ok(()), + } +} +impl Function { + pub fn actor_handler_entry(&self) -> Result, String> { + validate(self)?; + Ok(self + .actor_handler + .as_ref() + .and_then(|witness| witness.source.as_ref())) + } + pub fn actor_state_view(&self, local: LocalId) -> Result { + validate(self)?; + if self + .actor_state_views + .iter() + .any(|view| view.local() == local && view.matches_metadata(&self.locals)) + { + return Ok(true); + } + let Some(entry) = self.actor_handler_entry()? else { + return Ok(false); + }; + Ok(entry.roles().iter().any(|role| { + role.local() == local + && matches!( + role.role(), + jett_typecheck::CheckedActorBindingRole::State { .. } + ) + && matches!( + role.fact().mode, + jett_typecheck::CheckedBindingMode::View { + source: jett_typecheck::CheckedViewSource::Binding(_) + } + ) + })) + } +} +impl Function { + /// The final generated allocation transfers one closed set of ready locals. + /// A private original constructor receipt authenticates both owner and view + /// destinations; this never expires an ordinary local or call loan. + pub(crate) fn actor_constructor_view_transfers( + &self, + expression: &Expression, + types: &TypeInterner, + ) -> Result>, String> { + if self.actor_state_views.is_empty() { + return Ok(None); + } + validate(self)?; + if self.identity.declaration.kind != hir::DeclarationKind::ActorConstructor { + return Err("actor view transport is outside its original constructor".into()); + } + let hir::ExpressionKind::ActorSpawn { + actor_type, + args, + evaluation_order, + constructor: None, + } = &expression.kind + else { + return Ok(None); + }; + if !self.blocks.iter().any(|block|matches!(&block.terminator.kind,TerminatorKind::Return(Some(value)) if std::ptr::eq(value,expression))) {return Err("actor view transport is not its terminal original allocation".into());} + let jett_types::Type::Actor(actor) = types.resolve(self.return_type) else { + return Err("actor constructor lost canonical actor result".into()); + }; + let actor = types.resolve_actor(*actor); + let count = actor.capability_params.len() + actor.state_fields.len(); + if actor_type != &actor.name + || expression.ty != self.return_type + || args.len() != count + || evaluation_order.iter().copied().ne(0..count) + { + return Err("actor view allocation changed original complete field order".into()); + } + let mut owners = std::collections::BTreeSet::new(); + let mut aliases = Vec::new(); + for (index, (argument, (_, expected))) in args + .iter() + .zip(actor.capability_params.iter().chain(&actor.state_fields)) + .enumerate() + { + if argument.ty != *expected { + return Err("actor view allocation changed canonical field type".into()); + } + if let hir::ExpressionKind::Local(local) = argument.kind { + if self.is_view_local(local) || !owners.insert(local.index() as usize) { + return Err( + "actor allocation requires unique original owned field destinations".into(), + ); + } + continue; + } + let hir::ExpressionKind::View(value) = &argument.kind else { + return Err("actor allocation view fields must be frozen original locals".into()); + }; + let hir::ExpressionKind::Local(local) = value.kind else { + return Err("actor allocation view endpoint is not original local".into()); + }; + let view = self + .actor_state_views + .iter() + .find(|view| { + view.local() == local + && view.matches_metadata(&self.locals) + && view + .field() + .is_some_and(|field| actor.capability_params.len() + field == index) + }) + .ok_or("actor allocation view has no exact original state ordinal")?; + if value.ty != argument.ty + || value.ty != self.local(local).ok_or("actor alias local absent")?.ty + || aliases.contains(&view.local()) + { + return Err("actor allocation alias endpoint changed or repeated".into()); + } + aliases.push(view.local()); + } + if aliases.len() != self.actor_state_views.len() { + return Err("actor allocation lost a persistent dependent view".into()); + } + for alias in &aliases { + let root = self + .view_root(*alias) + .ok_or("actor allocation alias has invalid original backing")?; + if !owners.contains(&(root.index() as usize)) { + return Err( + "actor allocation alias backing is not among original owned destinations" + .into(), + ); + } + } + Ok(Some(aliases)) + } +} + +fn refresh(function: &mut Function, before: &Function) -> Result<(), String> { + let Some(original) = &before.actor_handler else { + return Ok(()); + }; + if function.id != before.id + || function.identity != before.identity + || function.params != before.params + || function.capture_count != before.capture_count + || function.return_type != before.return_type + || !function.locals.starts_with(&before.locals) + { + return Err("actor preparation changed original checked role identity".into()); + } + let mut next = original.clone(); + next.refresh(function); + function.actor_handler = Some(next); + validate(function) +} +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before)?; + let mut replay = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut replay, before, edit.clone(), types)?; + refresh(function, before) +} +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before)?; + let mut replay = function.clone(); + crate::breakpoint_regions::sum_transition(&mut replay, before, types)?; + refresh(function, before) +} +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before)?; + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let reachable = cfg + .reverse_postorder() + .iter() + .copied() + .collect::>(); + if map.len() != before.blocks.len() + || before + .blocks + .iter() + .any(|block| map[block.id.index() as usize].is_some() != reachable.contains(&block.id)) + { + return Err("actor block map is not exact reachable compaction".into()); + } + let get = |id: BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("actor live block has no exact compaction map") + }; + let mut expected = before + .blocks + .iter() + .filter(|block| reachable.contains(&block.id)) + .cloned() + .collect::>(); + for block in &mut expected { + block.id = get(block.id)?; + crate::sequences::prune::block_targets(&mut block.terminator.kind, &|target| { + *target = map[target.index() as usize].expect("reachable target has map") + }); + } + if expected != function.blocks + || function.entry != get(before.entry)? + || function.locals != before.locals + { + return Err("actor block compaction changed surviving operations".into()); + } + refresh(function, before) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/actor_handlers/tests.rs b/crates/jett_mir/src/actor_handlers/tests.rs new file mode 100644 index 00000000..8063b707 --- /dev/null +++ b/crates/jett_mir/src/actor_handlers/tests.rs @@ -0,0 +1,319 @@ +use super::*; +use hir::ExpressionKind; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; +const SOURCE: &str = r#"namespace app +struct State: + value: int64 + items: list[int64] +actor Counter(seed: int64): + mutable State state = State(value: seed, items: list(seed)) + receive replace(values: list[int64]): + state.items = values + state.value = state.value + 1 + receive read responds int64: + respond state.value +function main() returns int64: + Counter first = spawn Counter(seed: 1) + Counter other = spawn Counter(seed: 2) + list[int64] supplied = list(3) + list[int64] wrong = list(4) + send first.replace(supplied) + return ask first.read +"#; +fn program(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + resolved + .diagnostics + .iter() + .chain(&checked.diagnostics) + .all(|error| error.severity != jett_diagnostics::Severity::Error), + "{:?} {:?}", + resolved.diagnostics, + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + ( + crate::lower(&high, &checked.interner).unwrap(), + checked.interner, + ) +} +#[test] +fn actor_handler_cfg_and_roles_remain_bound_to_original_entry() { + let (program, types) = program(SOURCE); + crate::validate(&program).unwrap(); + crate::validate_call_ownership(&program, &types).unwrap(); + let index = program + .functions + .iter() + .position(|function| { + function.identity.declaration.kind == hir::DeclarationKind::ActorHandler + }) + .unwrap(); + let mut changed = program.clone(); + changed.functions[index].params[1].mutable = false; + assert!(crate::validate(&changed).is_err()); + let mut removed = program.clone(); + removed.functions[index].actor_handler = None; + assert!(crate::validate(&removed).is_err()); + let mut moved = program.clone(); + moved.functions[index].identity.declaration.kind = hir::DeclarationKind::Function; + assert!(crate::validate(&moved).is_err()); +} +#[test] +fn ordinary_message_caller_rejects_same_typed_receiver_and_argument_substitution() { + let (program, types) = program(SOURCE); + let main = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "main") + .unwrap(); + for receiver in [true, false] { + let mut changed = program.clone(); + let function = &mut changed.functions[main]; + let replacement = function + .locals + .iter() + .find(|local| local.name == if receiver { "other" } else { "wrong" }) + .unwrap() + .id; + let send = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Evaluate(Expression { + kind: ExpressionKind::ActorMessage { actor, args, .. }, + .. + }) => Some((actor, args)), + _ => None, + }) + .unwrap(); + if receiver { + send.0.kind = ExpressionKind::Local(replacement); + } else { + send.1[0].kind = ExpressionKind::Local(replacement); + } + assert!(crate::validate_call_ownership(&changed, &types).is_err()); + } +} +#[test] +fn genuine_actor_sequence_preparation_preserves_entry_receipt() { + let source = SOURCE.replace( + " state.value = state.value + 1", + " for amount in list(1, 2):\n state.value = state.value + amount", + ); + let (mut program, types) = program(&source); + crate::prepare_native_sequences(&mut program, &types); + crate::validate(&program).unwrap(); + crate::validate_call_ownership(&program, &types).unwrap(); + for function in &program.functions { + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } +} + +#[test] +fn handled_actor_receiver_rejoins_exact_original_sum_cfg() { + let source = SOURCE.replace( + "function main() returns int64:", + r#"function maybe_counter(valid: bool) returns result[Counter, string]: + if valid: + return ok(spawn Counter(seed: 1)) + return fail("missing") +function target_value(valid: bool) returns int64: + return ask(maybe_counter(valid) handle error: + default spawn Counter(seed: 2) + ).read +function main() returns int64:"#, + ); + let (mut program, types) = program(&source); + crate::validate_call_ownership(&program, &types) + .expect("genuine handled actor receiver before preparation"); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("genuine handled actor receiver after preparation"); + for function in &program.functions { + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } +} + +#[test] +fn owning_respond_handle_is_lowered_before_actor_publication() { + let source = SOURCE + .replace( + "actor Counter(seed: int64):", + r#"function maybe_values(valid: bool, value: int64) returns result[list[int64], string]: + if valid: + return ok(list(value)) + return fail("missing") +actor Counter(seed: int64):"#, + ) + .replace( + " receive read responds int64:", + r#" receive answer(valid: bool) responds list[int64]: + state.value = state.value + 1 + respond maybe_values(valid, state.value) handle error: + default list(9) + receive read responds int64:"#, + ); + let (program, types) = program(&source); + let answer = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "Counter.answer") + .unwrap(); + assert!(answer.blocks.iter().any(|block|matches!(&block.terminator.kind,TerminatorKind::Respond(value) if matches!(value.kind,ExpressionKind::Local(_))))); + assert!( + answer + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + ); + crate::validate(&program).unwrap(); + crate::validate_call_ownership(&program, &types).unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, answer, &types).unwrap(); +} + +#[test] +fn persistent_actor_views_transport_with_their_exact_owned_constructor_backing() { + let source = r#"namespace app +struct Inner: + value: int64 +struct Outer: + prefix: int64 + inner: Inner +actor Counter(seed: int64): + mutable Outer owned = Outer(prefix: 9, inner: Inner(value: seed)) + Inner projected = view owned.inner + Inner alias = view projected + mutable Inner independent = Inner(value: seed) + receive advance(amount: int64): + independent.value = independent.value + amount + alias.value + receive read responds int64: + respond projected.value + alias.value + independent.value +function main() returns int64: + Counter first = spawn Counter(seed: 1) + send first.advance(2) + return ask first.read +"#; + let (mut original, types) = program(source); + crate::validate_call_ownership(&original, &types).unwrap(); + for function in &original.functions { + crate::move_values::MoveValuePlan::analyze(&original, function, &types).unwrap(); + } + crate::prepare_native_sequences(&mut original, &types); + for function in &original.functions { + crate::move_values::MoveValuePlan::analyze(&original, function, &types).unwrap(); + } + let constructor = original + .functions + .iter() + .position(|function| !function.actor_state_views.is_empty()) + .unwrap(); + let mut missing = original.clone(); + missing.functions[constructor].actor_handler = None; + assert!(crate::validate(&missing).is_err()); + let mut foreign = original.clone(); + foreign.functions[constructor].identity.declaration.kind = hir::DeclarationKind::Function; + assert!(crate::validate(&foreign).is_err()); + let mut reordered = original.clone(); + let allocation = reordered.functions[constructor] + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(Expression { + kind: ExpressionKind::ActorSpawn { args, .. }, + .. + })) => Some(args), + _ => None, + }) + .unwrap(); + allocation.swap(1, 2); + assert!(crate::validate(&reordered).is_err()); +} + +#[test] +fn nonresponding_return_nothing_keeps_owned_state_for_publication() { + let source = SOURCE.replace( + " state.value = state.value + 1", + " state.value = state.value + 1\n return nothing", + ); + let (mut program, types) = program(&source); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + } + crate::validate(&program).unwrap(); + crate::validate_call_ownership(&program, &types).unwrap(); + let handler_index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "Counter.replace") + .unwrap(); + let handler = &program.functions[handler_index]; + let state = handler + .actor_handler_entry() + .unwrap() + .unwrap() + .roles() + .iter() + .find(|role| { + matches!( + role.role(), + jett_typecheck::CheckedActorBindingRole::State { .. } + ) + }) + .unwrap() + .local() + .index() as usize; + assert_eq!( + handler + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, StatementKind::UpdateField { .. })) + .count(), + 2, + ); + let return_block = handler + .blocks + .iter() + .position(|block| { + matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(value)) + if value.ty == TypeInterner::NOTHING + && matches!(value.kind, ExpressionKind::Nothing) + ) + }) + .expect("the genuine no-response exit stays Return(nothing)"); + let plan = crate::move_values::MoveValuePlan::analyze(&program, handler, &types).unwrap(); + assert!(plan.owned_locals.contains(&state)); + assert!(plan.live_in[return_block].contains(&state)); + assert!(plan.live_out[return_block].contains(&state)); + assert!( + plan.live_after_statement[return_block] + .iter() + .all(|live| live.contains(&state)) + ); + + let mut changed = program.clone(); + let terminator = &mut changed.functions[handler_index].blocks[return_block].terminator; + let TerminatorKind::Return(Some(value)) = &terminator.kind else { + unreachable!() + }; + terminator.kind = TerminatorKind::Respond(value.clone()); + assert!(crate::validate(&changed).is_err()); + } +} diff --git a/crates/jett_mir/src/analysis.rs b/crates/jett_mir/src/analysis.rs index d71b5210..36f5fb09 100644 --- a/crates/jett_mir/src/analysis.rs +++ b/crates/jett_mir/src/analysis.rs @@ -117,6 +117,13 @@ fn terminator_successors(terminator: &TerminatorKind) -> Vec { .chain(*otherwise) .collect(), TerminatorKind::ForEach { body, exit, .. } => vec![*body, *exit], + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), } } diff --git a/crates/jett_mir/src/breakpoint_regions.rs b/crates/jett_mir/src/breakpoint_regions.rs new file mode 100644 index 00000000..06300357 --- /dev/null +++ b/crates/jett_mir/src/breakpoint_regions.rs @@ -0,0 +1,1306 @@ +//! Constructor-owned lexical context for extracted breakpoint conditions. +//! These snapshots are metadata, never execution roots or caller authority. +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(super) struct Site { + block: BlockId, + index: usize, +} + +#[derive(Debug, Clone, PartialEq)] +struct ArchivedCondition(hir::Statement); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Edge { + source: BlockId, + arm: usize, + target: BlockId, +} + +#[derive(Debug, Clone, PartialEq)] +enum Endpoint { + Live(Site), + // Minted only by the exact typed Never-side selection below. The old + // endpoint is archival after compaction; active selected edges are remapped. + CanonicallyAborted { + selections: Vec, + loops: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq)] +struct SelectedEdge { + block: BlockId, + source: LocalId, + tag: LocalId, + source_type: TypeId, + selected: BlockId, + span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +struct SelectedLoop { + block: BlockId, + preheader: BlockId, + source: SequenceSource, + source_type: TypeId, + length: LocalId, + selected: BlockId, + span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct BreakpointRegion { + id: usize, + parent: Option, + owner: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + entry: BlockId, + origin: ArchivedCondition, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + edges: Vec, + endpoint: Endpoint, + selections: Vec, + loops: Vec, + abort_allowed: bool, +} + +#[derive(Clone, Default)] +pub(super) struct Capture { + drafts: Vec, + stack: Vec, +} + +#[derive(Clone)] +struct Draft { + parent: Option, + origin: ArchivedCondition, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + endpoint: Option, +} + +impl Capture { + pub(super) fn begin(&mut self, original: &hir::Statement, block: BlockId, index: usize) { + let id = self.drafts.len(); + self.drafts.push(Draft { + parent: self.stack.last().copied(), + origin: ArchivedCondition(original.clone()), + start: Site { block, index }, + statements: BTreeMap::new(), + terminators: BTreeMap::new(), + blocks: BTreeSet::from([block]), + endpoint: None, + }); + self.stack.push(id); + } + + pub(super) fn statement(&mut self, block: BlockId, index: usize, value: &Statement) { + if let Some(id) = self.stack.last().copied() { + let draft = &mut self.drafts[id]; + draft.blocks.insert(block); + draft + .statements + .insert(Site { block, index }, value.clone()); + } + } + + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + if let Some(id) = self.stack.last().copied() { + let draft = &mut self.drafts[id]; + draft.blocks.insert(block); + draft.terminators.insert(block, value.clone()); + } + } + + pub(super) fn block(&mut self, block: BlockId) { + if let Some(id) = self.stack.last().copied() { + self.drafts[id].blocks.insert(block); + } + } + + pub(super) fn end(&mut self, block: BlockId, index: usize) { + if let Some(id) = self.stack.pop() { + self.drafts[id].endpoint = Some(Site { block, index }); + } + } + + pub(super) fn seal(self, function: &Function) -> Result, String> { + if !self.stack.is_empty() { + return Err("breakpoint constructor has an unfinished condition capture".into()); + } + let mut records = Vec::new(); + for (id, draft) in self.drafts.into_iter().enumerate() { + let endpoint = draft + .endpoint + .ok_or("breakpoint constructor lacks an exact final endpoint")?; + records.push(BreakpointRegion { + id, + parent: draft.parent, + owner: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + entry: function.entry, + origin: draft.origin, + start: draft.start, + statements: draft.statements, + terminators: draft.terminators, + blocks: draft.blocks, + edges: Vec::new(), + endpoint: Endpoint::Live(endpoint), + selections: Vec::new(), + loops: Vec::new(), + abort_allowed: false, + }); + } + for id in (0..records.len()).rev() { + if let Some(parent) = records[id].parent { + let child_blocks = records[id].blocks.clone(); + records[parent].blocks.extend(child_blocks); + } + } + let all = edges(&function.blocks); + for record in &mut records { + record.edges = touching(&all, &record.blocks); + } + Ok(records) + } +} + +pub(super) struct VerifiedSites { + statements: BTreeSet, + terminators: BTreeSet, +} + +impl VerifiedSites { + pub(super) fn statement(&self, block: BlockId, index: usize) -> bool { + self.statements.contains(&Site { block, index }) + } + pub(super) fn terminator(&self, block: BlockId) -> bool { + self.terminators.contains(&block) + } +} + +pub(super) fn validate(function: &Function, types: &TypeInterner) -> Result { + let mut verified = VerifiedSites { + statements: BTreeSet::new(), + terminators: BTreeSet::new(), + }; + if function.breakpoint_regions.is_empty() { + if function.blocks.iter().any(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::Breakpoint { + condition: Some(_), + .. + } + ) + }) + }) { + return Err("breakpoint condition has no exact constructor-owned endpoint".into()); + } + return Ok(verified); + } + let mut ids = BTreeSet::new(); + let mut endpoints = BTreeSet::new(); + let all = edges(&function.blocks); + for record in &function.breakpoint_regions { + if !ids.insert(record.id) + || record.owner != function.id + || record.identity != function.identity + || record.debug_kind != function.debug_kind + || record.entry != function.entry + { + return Err("breakpoint extraction has duplicate or foreign function ownership".into()); + } + if let Some(parent) = record.parent + && (parent >= record.id || !function.breakpoint_regions.iter().any(|r| r.id == parent)) + { + return Err("breakpoint extraction has an orphan lexical parent".into()); + } + let hir::StatementKind::Breakpoint { + condition: Some(condition), + .. + } = &record.origin.0.kind + else { + return Err("breakpoint extraction lacks its archived checked condition".into()); + }; + if condition.ty != TypeInterner::BOOL + || record.blocks.is_empty() + || !record.blocks.contains(&record.start.block) + || function + .blocks + .get(record.start.block.index() as usize) + .is_none_or(|block| record.start.index > block.statements.len()) + || touching(&all, &record.blocks) != record.edges + { + return Err( + "breakpoint extraction changes its condition or exact CFG boundaries".into(), + ); + } + for (&site, expected) in &record.statements { + let actual = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.index)); + if !record.blocks.contains(&site.block) + || !verified.statements.insert(site) + || actual.is_none_or(|actual| !statement_equal(actual, expected)) + { + return Err( + "breakpoint extraction changes or duplicates an exact statement occurrence" + .into(), + ); + } + } + for (&block, expected) in &record.terminators { + let actual = function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block); + if !record.blocks.contains(&block) + || !verified.terminators.insert(block) + || actual.is_none_or(|value| !terminator_equal(&value.terminator, expected)) + { + return Err( + "breakpoint extraction changes or duplicates an exact terminator occurrence" + .into(), + ); + } + } + match &record.endpoint { + Endpoint::Live(site) => { + if !endpoints.insert(*site) { + return Err("breakpoint extraction duplicates its endpoint".into()); + } + if !matches!(record.statements.get(site).map(|s| &s.kind), Some(StatementKind::Breakpoint { condition: Some(value), .. }) if value.ty == TypeInterner::BOOL) + { + return Err("breakpoint extraction loses its exact final endpoint".into()); + } + } + Endpoint::CanonicallyAborted { selections, loops } => { + if !record.abort_allowed + || (selections.is_empty() && loops.is_empty()) + || selections != &record.selections + || loops != &record.loops + { + return Err( + "breakpoint extraction has no canonical aborted-edge evidence".into(), + ); + } + } + } + for selected in &record.selections { + validate_selection(function, selected, types)?; + } + for selected in &record.loops { + validate_loop_selection(function, selected, types)?; + } + } + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!( + statement.kind, + StatementKind::Breakpoint { + condition: Some(_), + .. + } + ) && !endpoints.contains(&Site { + block: block.id, + index, + }) { + return Err("breakpoint condition has no exact constructor-owned endpoint".into()); + } + } + } + Ok(verified) +} + +fn edges(blocks: &[BasicBlock]) -> Vec { + let mut result = Vec::new(); + for block in blocks { + let mut targets = Vec::new(); + match &block.terminator.kind { + TerminatorKind::Goto(target) => targets.push(*target), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => targets.extend([*then_block, *else_block]), + TerminatorKind::Switch { + variants, + otherwise, + .. + } => { + targets.extend(variants.iter().map(|(_, target, _)| *target)); + targets.extend(*otherwise); + } + TerminatorKind::ForEach { body, exit, .. } => targets.extend([*body, *exit]), + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + targets.extend(arms.iter().map(|arm| arm.target)); + targets.push(*otherwise); + } + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => {} + } + result.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + result +} + +fn touching(all: &[Edge], blocks: &BTreeSet) -> Vec { + all.iter() + .copied() + .filter(|edge| blocks.contains(&edge.source) || blocks.contains(&edge.target)) + .collect() +} + +// Only the canonical sequence pass constructs this finite edit description. +// Before is already authenticated; additions come from that exact ForEach, +// never from current call context, source spans or a post-mutation search. +#[derive(Clone)] +pub(super) struct SequenceEdit { + pub header: BlockId, + pub before: Terminator, + pub after: Terminator, + pub append: Vec<(BlockId, Vec)>, + pub prefix: Option<(BlockId, Vec)>, + pub redirects: Vec<(BlockId, Terminator, Terminator)>, + pub blocks: Vec, +} + +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + validate(before, types)?; + let Some(header) = before + .blocks + .get(edit.header.index() as usize) + .filter(|block| block.id == edit.header) + else { + return Err("breakpoint sequence transition has no exact header".into()); + }; + let existing = |id: BlockId| { + before + .blocks + .get(id.index() as usize) + .is_some_and(|block| block.id == id) + }; + if edit.append.iter().any(|(block, _)| !existing(*block)) + || edit + .prefix + .as_ref() + .is_some_and(|(block, _)| !existing(*block)) + || edit.redirects.iter().any(|(block, _, _)| !existing(*block)) + || edit + .blocks + .iter() + .enumerate() + .any(|(index, block)| block.id.index() as usize != before.blocks.len() + index) + { + return Err("breakpoint sequence edit has a foreign or nondense occurrence".into()); + } + if !terminator_equal(&header.terminator, &edit.before) + || !matches!(edit.before.kind, TerminatorKind::ForEach { .. }) + { + return Err("breakpoint sequence transition differs from its original ForEach".into()); + } + let mut expected = before.clone(); + expected.blocks[edit.header.index() as usize].terminator = edit.after.clone(); + for (block, values) in &edit.append { + expected.blocks[block.index() as usize] + .statements + .extend(values.clone()); + } + if let Some((block, prefix)) = &edit.prefix { + let statements = &mut expected.blocks[block.index() as usize].statements; + let mut values = prefix.clone(); + values.append(statements); + *statements = values; + } + for (block, old, new) in &edit.redirects { + if !terminator_equal(&expected.blocks[block.index() as usize].terminator, old) { + return Err("breakpoint sequence redirect has a foreign before-edge".into()); + } + expected.blocks[block.index() as usize].terminator = new.clone(); + } + expected.blocks.extend(edit.blocks.clone()); + if !blocks_equal(&expected.blocks, &function.blocks) { + return Err("breakpoint sequence transition has changes outside its canonical edit".into()); + } + let owners = before + .breakpoint_regions + .iter() + .filter(|record| record.terminators.contains_key(&edit.header)) + .map(|record| record.id) + .collect::>(); + let mut records = before.breakpoint_regions.clone(); + for record in &mut records { + if let Some((block, prefix)) = &edit.prefix { + shift(record, *block, 0, prefix.len()); + } + if record.terminators.contains_key(&edit.header) { + record.terminators.insert(edit.header, edit.after.clone()); + } + for (block, _, new) in &edit.redirects { + if record.terminators.contains_key(block) { + record.terminators.insert(*block, new.clone()); + } + } + if owners.contains(&record.id) { + if let ( + TerminatorKind::ForEach { iterable, exit, .. }, + TerminatorKind::Goto(selected), + ) = (&edit.before.kind, &edit.after.kind) + { + let Some((preheader, source, length)) = + edit.append.iter().find_map(|(block, values)| { + values.iter().find_map(|statement| { + if let StatementKind::SequenceLength { source, target } = + &statement.kind + { + Some((*block, source.clone(), *target)) + } else { + None + } + }) + }) + else { + return Err( + "breakpoint empty sequence lacks canonical length validation".into(), + ); + }; + if selected != exit || !empty_sequence(types, iterable.ty) { + return Err("breakpoint sequence selected an inhabited body".into()); + } + record.loops.push(SelectedLoop { + block: edit.header, + preheader, + source, + source_type: iterable.ty, + length, + selected: *selected, + span: edit.after.span, + }); + } + for (block, values) in &edit.append { + let start = before.blocks[block.index() as usize].statements.len(); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + index: start + index, + }, + value.clone(), + ); + } + record.blocks.insert(*block); + } + if let Some((block, values)) = &edit.prefix { + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + index, + }, + value.clone(), + ); + } + record.blocks.insert(*block); + } + for block in &edit.blocks { + record.blocks.insert(block.id); + for (index, value) in block.statements.iter().enumerate() { + record.statements.insert( + Site { + block: block.id, + index, + }, + value.clone(), + ); + } + record + .terminators + .insert(block.id, block.terminator.clone()); + } + } + } + // Ancestors include child CFG boundaries without claiming child sites. + for id in (0..records.len()).rev() { + if let Some(parent) = records[id].parent { + let blocks = records[id].blocks.clone(); + let parent_record = records + .iter_mut() + .find(|record| record.id == parent) + .ok_or("breakpoint preparation loses a parent")?; + parent_record.blocks.extend(blocks); + } + } + for record in &mut records { + record.edges = touching(&edges(&expected.blocks), &record.blocks); + if let Endpoint::Live(endpoint) = &record.endpoint { + record.abort_allowed |= reachable(&before.blocks, before.entry, endpoint.block) + && !reachable(&expected.blocks, expected.entry, endpoint.block) + && (!record.selections.is_empty() || !record.loops.is_empty()); + } + } + function.breakpoint_regions = records; + Ok(()) +} + +fn shift(record: &mut BreakpointRegion, block: BlockId, start: usize, count: usize) { + record.statements = std::mem::take(&mut record.statements) + .into_iter() + .map(|(mut site, statement)| { + if site.block == block && site.index >= start { + site.index += count; + } + (site, statement) + }) + .collect(); + if record.start.block == block && record.start.index >= start { + record.start.index += count; + } + if let Endpoint::Live(site) = &mut record.endpoint + && site.block == block + && site.index >= start + { + site.index += count; + } +} + +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + validate(before, types)?; + let mut expected = before.clone(); + let mut changed = Vec::new(); + for block in &mut expected.blocks { + let Some(Statement { + kind: StatementKind::SumTag { source, target }, + .. + }) = block.statements.last() + else { + continue; + }; + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + continue; + }; + let Some(source_local) = before.local(*source) else { + continue; + }; + if condition.ty != TypeInterner::BOOL + || !matches!(condition.kind, hir::ExpressionKind::Local(local) if local == *target) + || before + .local(*target) + .is_none_or(|local| local.ty != TypeInterner::BOOL) + || source_local.ty.index() as usize >= types.len() + { + continue; + } + let selected = match types.resolve(source_local.ty) { + jett_types::Type::Optional(inner) if *inner == TypeInterner::NEVER => *else_block, + jett_types::Type::Result(ok, error) + if *ok == TypeInterner::NEVER + && *error != TypeInterner::NEVER + && (error.index() as usize) < types.len() => + { + *else_block + } + jett_types::Type::Result(ok, error) + if *ok != TypeInterner::NEVER + && *error == TypeInterner::NEVER + && (ok.index() as usize) < types.len() => + { + *then_block + } + _ => continue, + }; + changed.push(SelectedEdge { + block: block.id, + source: *source, + tag: *target, + source_type: source_local.ty, + selected, + span: block.terminator.span, + }); + block.terminator.kind = TerminatorKind::Goto(selected); + } + if !blocks_equal(&expected.blocks, &function.blocks) { + return Err("breakpoint sum transition changes more than an exact Never-side edge".into()); + } + for record in &mut function.breakpoint_regions { + for selected in &changed { + if record.blocks.contains(&selected.block) { + if record.terminators.contains_key(&selected.block) { + record.terminators.insert( + selected.block, + expected.blocks[selected.block.index() as usize] + .terminator + .clone(), + ); + } + record.selections.push(selected.clone()); + } + } + record.edges = touching(&edges(&expected.blocks), &record.blocks); + if let Endpoint::Live(endpoint) = &record.endpoint { + record.abort_allowed |= reachable(&before.blocks, before.entry, endpoint.block) + && !reachable(&expected.blocks, expected.entry, endpoint.block) + && !record.selections.is_empty(); + } + } + Ok(()) +} + +fn validate_selection( + function: &Function, + selected: &SelectedEdge, + types: &TypeInterner, +) -> Result<(), String> { + let Some(block) = function + .blocks + .get(selected.block.index() as usize) + .filter(|block| block.id == selected.block) + else { + return Err("breakpoint selected edge has no retained block".into()); + }; + let exact_never = (selected.source_type.index() as usize) < types.len() + && match types.resolve(selected.source_type) { + jett_types::Type::Optional(inner) => *inner == TypeInterner::NEVER, + jett_types::Type::Result(ok, error) => { + (*ok == TypeInterner::NEVER) != (*error == TypeInterner::NEVER) + } + _ => false, + }; + if !exact_never + || function + .local(selected.source) + .is_none_or(|local| local.ty != selected.source_type) + || function + .local(selected.tag) + .is_none_or(|local| local.ty != TypeInterner::BOOL) + || !matches!(block.terminator.kind, TerminatorKind::Goto(target) if target == selected.selected) + || block.terminator.span != selected.span + || !matches!(block.statements.last().map(|s| &s.kind), Some(StatementKind::SumTag { source, target }) if *source == selected.source && *target == selected.tag) + { + return Err("breakpoint selected edge loses its exact runtime sum/tag proof".into()); + } + Ok(()) +} + +pub(super) fn remap_blocks(function: &mut Function, map: &[Option]) -> Result<(), String> { + let get = |id: BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or_else(|| "breakpoint surviving CFG reference has no block remap".to_string()) + }; + let old = std::mem::take(&mut function.breakpoint_regions); + let mut records = Vec::new(); + for mut record in old.clone() { + let surviving = old.iter().any(|child| { + (child.id == record.id || descendant(child, record.id, &old)) + && (child.statements.keys().any(|site| get(site.block).is_ok()) + || child.terminators.keys().any(|block| get(*block).is_ok())) + }); + if !surviving { + continue; + } + if let Endpoint::Live(site) = record.endpoint.clone() { + if get(site.block).is_err() { + if !record.abort_allowed + || (record.selections.is_empty() && record.loops.is_empty()) + { + return Err( + "breakpoint endpoint removal lacks canonical selected-edge proof".into(), + ); + } + record.endpoint = Endpoint::CanonicallyAborted { + selections: record.selections.clone(), + loops: record.loops.clone(), + }; + } + } + record.entry = get(record.entry)?; + // The entry prefix can itself be unreachable only when the whole region + // is removed. A surviving fragment never invents a replacement entry. + record.start.block = get(record.start.block)?; + record.statements = record + .statements + .into_iter() + .filter_map(|(mut site, value)| { + let block = get(site.block).ok()?; + site.block = block; + Some((site, value)) + }) + .collect(); + let mut terms = BTreeMap::new(); + for (block, mut value) in record.terminators { + let Ok(block) = get(block) else { + continue; + }; + let missing = std::cell::Cell::new(false); + sequences::prune::block_targets(&mut value.kind, &|id| match get(*id) { + Ok(new) => *id = new, + Err(_) => missing.set(true), + }); + if missing.get() { + return Err("breakpoint retained terminator targets a removed block".into()); + } + terms.insert(block, value); + } + record.terminators = terms; + record.blocks = record + .blocks + .into_iter() + .filter_map(|id| get(id).ok()) + .collect(); + record.edges = record + .edges + .into_iter() + .filter_map(|mut edge| { + edge.source = get(edge.source).ok()?; + edge.target = get(edge.target).ok()?; + Some(edge) + }) + .collect(); + record.selections.retain_mut(|selected| { + let (Ok(block), Ok(target)) = (get(selected.block), get(selected.selected)) else { + return false; + }; + selected.block = block; + selected.selected = target; + true + }); + record.loops.retain_mut(|selected| { + let (Ok(block), Ok(preheader), Ok(target)) = ( + get(selected.block), + get(selected.preheader), + get(selected.selected), + ) else { + return false; + }; + selected.block = block; + selected.preheader = preheader; + selected.selected = target; + true + }); + match &mut record.endpoint { + Endpoint::Live(site) => site.block = get(site.block)?, + Endpoint::CanonicallyAborted { selections, loops } => { + *selections = record.selections.clone(); + *loops = record.loops.clone(); + } + } + records.push(record); + } + function.breakpoint_regions = records; + Ok(()) +} + +pub(super) fn remap_locals(function: &mut Function, map: &[Option]) -> Result<(), String> { + for record in &mut function.breakpoint_regions { + for value in record.statements.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![value.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + remap_snapshot(&mut block, map)?; + *value = block.statements.remove(0); + } + for value in record.terminators.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: value.clone(), + }; + remap_snapshot(&mut block, map)?; + *value = block.terminator; + } + for selected in &mut record.selections { + selected.source = map + .get(selected.source.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint selected source has no local remap")?; + selected.tag = map + .get(selected.tag.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint selected tag has no local remap")?; + } + for selected in &mut record.loops { + selected.length = map + .get(selected.length.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint sequence length has no local remap")?; + let root = match &mut selected.source { + SequenceSource::Local(root) | SequenceSource::Projected { owner: root, .. } => root, + }; + *root = map + .get(root.index() as usize) + .copied() + .flatten() + .ok_or("breakpoint sequence source has no local remap")?; + } + if let Endpoint::CanonicallyAborted { selections, loops } = &mut record.endpoint { + *selections = record.selections.clone(); + *loops = record.loops.clone(); + } + } + Ok(()) +} + +fn descendant(child: &BreakpointRegion, ancestor: usize, records: &[BreakpointRegion]) -> bool { + let mut parent = child.parent; + for _ in 0..records.len() { + let Some(id) = parent else { + return false; + }; + if id == ancestor { + return true; + } + parent = records + .iter() + .find(|record| record.id == id) + .and_then(|record| record.parent); + } + false +} + +fn reachable(blocks: &[BasicBlock], entry: BlockId, target: BlockId) -> bool { + let all = edges(blocks); + let mut seen = BTreeSet::new(); + let mut pending = vec![entry]; + while let Some(block) = pending.pop() { + if !seen.insert(block) { + continue; + } + if block == target { + return true; + } + pending.extend( + all.iter() + .filter(|edge| edge.source == block) + .map(|edge| edge.target), + ); + } + false +} + +fn empty_sequence(types: &TypeInterner, ty: TypeId) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + match types.resolve(ty) { + jett_types::Type::List(element) | jett_types::Type::Set(element) => { + *element == TypeInterner::NEVER + } + jett_types::Type::Map(key, value) => { + *key == TypeInterner::NEVER || *value == TypeInterner::NEVER + } + _ => false, + } +} + +fn validate_loop_selection( + function: &Function, + selected: &SelectedLoop, + types: &TypeInterner, +) -> Result<(), String> { + if !empty_sequence(types, selected.source_type) + || selected.source.ty(function) != Some(selected.source_type) + || function.local(selected.length).is_none_or(|local| local.ty != TypeInterner::INT64) + || !function.blocks.get(selected.preheader.index() as usize).is_some_and(|block| block.statements.iter().any(|statement| matches!(&statement.kind, StatementKind::SequenceLength { source, target } if source == &selected.source && *target == selected.length))) + || !function.blocks.get(selected.block.index() as usize).is_some_and(|block| block.terminator.span == selected.span && matches!(block.terminator.kind, TerminatorKind::Goto(target) if target == selected.selected)) { + return Err("breakpoint empty sequence loses its exact validation or selected edge".into()); + } + Ok(()) +} + +pub(super) fn remap_snapshot( + block: &mut BasicBlock, + map: &[Option], +) -> Result<(), String> { + let mut missing = false; + sequences::prune::block_locals( + block, + &mut |id| { + if let Some(new) = map.get(id.index() as usize).copied().flatten() { + *id = new; + } else { + missing = true; + } + }, + &mut |floor| { + if (*floor as usize) <= map.len() { + *floor = map[..*floor as usize] + .iter() + .filter(|id| id.is_some()) + .count() as u32; + } + }, + ); + if missing { + Err("breakpoint surviving snapshot has no local remap".into()) + } else { + Ok(()) + } +} + +// Typed normalization changes only f64 payloads for equality. Their exact bits +// are compared separately, so NaN and signed zero never weaken a snapshot. +pub(super) fn statement_equal(left: &Statement, right: &Statement) -> bool { + let span = left.span; + let left = BasicBlock { + id: BlockId(0), + statements: vec![left.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }; + let right = BasicBlock { + id: BlockId(0), + statements: vec![right.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }; + block_equal(&left, &right) +} +pub(super) fn terminator_equal(left: &Terminator, right: &Terminator) -> bool { + block_equal( + &BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: left.clone(), + }, + &BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: right.clone(), + }, + ) +} +pub(super) fn blocks_equal(left: &[BasicBlock], right: &[BasicBlock]) -> bool { + left.len() == right.len() + && left + .iter() + .zip(right) + .all(|(left, right)| block_equal(left, right)) +} +pub(super) fn expressions_equal(left: &Expression, right: &Expression) -> bool { + let (mut left, mut right) = (left.clone(), right.clone()); + let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); + float_expression(&mut left, &mut left_bits); + float_expression(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} +pub(super) fn hir_blocks_equal(left: &hir::Block, right: &hir::Block) -> bool { + let (mut left, mut right) = (left.clone(), right.clone()); + let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); + float_hir_block(&mut left, &mut left_bits); + float_hir_block(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} +fn block_equal(left: &BasicBlock, right: &BasicBlock) -> bool { + let (mut left, mut right) = (left.clone(), right.clone()); + let (mut left_bits, mut right_bits) = (Vec::new(), Vec::new()); + float_block(&mut left, &mut left_bits); + float_block(&mut right, &mut right_bits); + left == right && left_bits == right_bits +} + +fn float_block(block: &mut BasicBlock, bits: &mut Vec) { + for statement in &mut block.statements { + match &mut statement.kind { + StatementKind::ResourceConstructor(crate::ResourceConstructorNode::Stage { + value, + .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => float_expression(value, bits), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + float_expression(target, bits); + float_expression(value, bits); + } + StatementKind::Assert { condition, message } => { + float_expression(condition, bits); + if let Some(value) = message { + float_expression(value, bits); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + float_expression(value, bits); + } + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + crate::ResourceConstructorNode::Begin { .. } + | crate::ResourceConstructorNode::Commit { .. } + | crate::ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } + match &mut block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + float_expression(value, bits); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => float_expression(value, bits), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } +} + +fn float_expression(value: &mut Expression, bits: &mut Vec) { + use hir::ExpressionKind as E; + match &mut value.kind { + // Arc identity already compares the immutable sealed plan exactly. + E::ActorGraphImport(_) => {} + E::Float(value) => { + bits.push(value.to_bits()); + *value = 0.0; + } + E::Binary { left, right, .. } => { + float_expression(left, bits); + float_expression(right, bits); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::RuntimeFailureMessage(value) + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceType(value) + | E::Comptime { value, .. } + | E::StateIs { value, .. } + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Field { base: value, .. } + | E::View(value) + | E::Clone(value) => float_expression(value, bits), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } + | E::StructConstruct { fields: args, .. } + | E::BitfieldConstruct { fields: args, .. } + | E::MachineConstruct { payloads: args, .. } + | E::EnumConstruct { payloads: args, .. } + | E::ListConstruct { elements: args } => { + for arg in args { + float_expression(arg, bits); + } + } + E::IndirectCall { callee, args, .. } + | E::ActorMessage { + actor: callee, + args, + .. + } + | E::MachineTransition { + source: callee, + payloads: args, + .. + } => { + float_expression(callee, bits); + for arg in args { + float_expression(arg, bits); + } + } + E::MapConstruct { entries } => { + for entry in entries { + float_expression(&mut entry.key, bits); + float_expression(&mut entry.value, bits); + } + } + E::Handle { + target, failure, .. + } => { + float_expression(target, bits); + float_hir_block(failure, bits); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + float_expression(value, bits); + } + } + } + E::InlineFunction { body, .. } => float_hir_block(body, bits), + E::Int(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::ResourceHookValue { .. } + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } +} + +fn float_hir_block(block: &mut hir::Block, bits: &mut Vec) { + use hir::StatementKind as H; + for statement in &mut block.statements { + match &mut statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => float_expression(value, bits), + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + float_expression(target, bits); + float_expression(value, bits); + } + H::Return(value) => { + if let Some(value) = value { + float_expression(value, bits); + } + } + H::If { + condition, + then_block, + else_block, + } => { + float_expression(condition, bits); + float_hir_block(then_block, bits); + if let Some(block) = else_block { + float_hir_block(block, bits); + } + } + H::While { condition, body } => { + float_expression(condition, bits); + float_hir_block(body, bits); + } + H::For { iterable, body, .. } => { + float_expression(iterable, bits); + float_hir_block(body, bits); + } + H::Match { scrutinee, arms } => { + float_expression(scrutinee, bits); + for arm in arms { + float_hir_block(&mut arm.body, bits); + } + } + H::Assert { condition, message } => { + float_expression(condition, bits); + if let Some(message) = message { + float_expression(message, bits); + } + } + H::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + float_expression(condition, bits); + } + } + H::Scope(block) => float_hir_block(block, bits), + H::ReflectedTypeDispatch { type_info, arms } => { + float_expression(type_info, bits); + for arm in arms { + float_hir_block(&mut arm.body, bits); + } + } + H::Trace(_) | H::Break | H::Continue => {} + } + } +} + +#[cfg(test)] +mod tests; + +// Read-only bit-exact expression comparison, sharing the constructor snapshot rule. +pub(super) fn expression_equal(left: &Expression, right: &Expression) -> bool { + statement_equal( + &Statement { + kind: StatementKind::Evaluate(left.clone()), + span: left.span, + }, + &Statement { + kind: StatementKind::Evaluate(right.clone()), + span: right.span, + }, + ) +} diff --git a/crates/jett_mir/src/breakpoint_regions/tests.rs b/crates/jett_mir/src/breakpoint_regions/tests.rs new file mode 100644 index 00000000..509a662d --- /dev/null +++ b/crates/jett_mir/src/breakpoint_regions/tests.rs @@ -0,0 +1,485 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const HANDLED: &str = r#"namespace app +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise() returns bool: + bool before = true + breakpoint(maybe_flag(true) handle error: + default false + ) + breakpoint(maybe_flag(false) handle error: + default false + ) + return before +"#; + +fn source_program(source: &str, release: bool) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("exact checked HIR"); + ( + crate::lower(&high, &checked.interner).expect("exact source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn rejects(program: &Program, types: &TypeInterner) { + let errors = crate::validate_call_ownership(program, types) + .expect_err("corrupted extraction is refused"); + assert!( + errors + .iter() + .any(|e| e.message.contains("breakpoint") || e.message.contains("source context")), + "{errors:?}" + ); +} + +#[test] +fn breakpoint_extraction_rejoins_real_nested_handler_conditions_and_release_elision() { + let (mut program, types) = source_program(HANDLED, false); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 2); + let sites = validate(function, &types).expect("original exact sites"); + let first = &function.blocks[function.entry.index() as usize].statements[0]; + assert!(matches!(first.kind, StatementKind::Let { .. })); + assert!( + !sites.statement(function.entry, 0), + "ordinary shared prefix remains ordinary" + ); + assert!( + !sites.terminator(function.blocks.last().expect("return block").id), + "ordinary return remains ordinary" + ); + for pass in [ + crate::prepare_native_sequences, + crate::prepare_native_uninhabited_sums, + crate::prepare_native_generated_functions, + ] { + pass(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("every canonical boundary preserves exact sites"); + } + let (released, types) = source_program(HANDLED, true); + assert!(exercise(&released).breakpoint_regions.is_empty()); + assert!(exercise(&released).blocks.iter().all(|b| { + b.statements + .iter() + .all(|s| !matches!(s.kind, StatementKind::Breakpoint { .. })) + })); + crate::validate_call_ownership(&released, &types) + .expect("release never evaluates debug condition"); +} + +#[test] +fn breakpoint_extraction_does_not_authorize_copied_moved_or_new_marker_calls() { + let (program, types) = source_program(HANDLED, false); + let function = exercise(&program); + let (site, original) = function.breakpoint_regions[0].statements.iter().find(|(_, statement)| matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(value.kind, hir::ExpressionKind::Call { .. }))).map(|(site, value)| (*site, value.clone())).expect("extracted real call"); + for mutation in ["copy", "move", "marker", "endpoint", "condition"] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "copy" => { + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: vec![original.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: original.span, + }, + }); + } + "move" => { + function.blocks[site.block.index() as usize] + .statements + .remove(site.index); + } + "marker" => { + let Endpoint::Live(endpoint) = &function.breakpoint_regions[0].endpoint else { + panic!("live endpoint"); + }; + let value = function.blocks[endpoint.block.index() as usize].statements + [endpoint.index] + .clone(); + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: vec![value], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: original.span, + }, + }); + } + "endpoint" | "condition" => { + let Endpoint::Live(endpoint) = &function.breakpoint_regions[0].endpoint else { + panic!("live endpoint"); + }; + let endpoint = *endpoint; + let target = &mut function.blocks[endpoint.block.index() as usize].statements + [endpoint.index]; + if mutation == "endpoint" { + target.kind = StatementKind::Evaluate(Expression { + kind: hir::ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span: target.span, + }); + } else { + let StatementKind::Breakpoint { condition, .. } = &mut target.kind else { + panic!("endpoint"); + }; + condition.as_mut().expect("condition").kind = hir::ExpressionKind::Bool(true); + } + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn breakpoint_extraction_rejects_orphan_duplicate_foreign_and_alternate_entry_records() { + let (program, types) = source_program(HANDLED, false); + for mutation in ["duplicate", "orphan", "foreign", "index", "edge"] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "duplicate" => function + .breakpoint_regions + .push(function.breakpoint_regions[0].clone()), + "orphan" => function.breakpoint_regions[0].parent = Some(usize::MAX), + "foreign" => function.breakpoint_regions[0].owner = program.functions[0].id, + "index" => { + let record = &mut function.breakpoint_regions[0]; + let (site, value) = record + .statements + .first_key_value() + .map(|(site, value)| (*site, value.clone())) + .expect("site"); + record.statements.remove(&site); + record.statements.insert( + Site { + index: site.index + 1, + ..site + }, + value, + ); + } + "edge" => { + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Goto(function.breakpoint_regions[0].start.block), + span: function.span, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +const NESTED: &str = r#"namespace app +function inspect(view values: list[int64]) returns bool: + return true +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise(values: list[int64]) returns bool: + breakpoint(false or (maybe_flag(false) handle error: + for item in view values: + breakpoint(inspect(view values)) + default true + )) + return inspect(view values) +"#; + +#[test] +fn breakpoint_extraction_nested_lazy_loop_sites_survive_sequence_prefixes_edges_and_remaps() { + let (mut program, types) = source_program(NESTED, false); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 2); + assert_eq!( + function.breakpoint_regions[1].parent, + Some(function.breakpoint_regions[0].id) + ); + let parent_sites = function.breakpoint_regions[0] + .statements + .keys() + .copied() + .collect::>(); + assert!( + function.breakpoint_regions[1] + .statements + .keys() + .all(|site| !parent_sites.contains(site)) + ); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("exact sequence prefixes and split edges"); + assert!( + exercise(&program) + .blocks + .iter() + .all(|b| !matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("dense prepared nested sites"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let prefix = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find(|s| matches!(s.kind, StatementKind::SequenceGet { .. })) + .expect("canonical getter"); + prefix.span.start += 1; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_exact_never_side_selection_retains_aborted_evaluation_context() { + let source = r#"namespace app +function accept[T](value: T) returns bool: + return true +function exercise[T](outcome: optional[T]) returns bool: + breakpoint(accept(outcome handle: + return false + )) + return true +function main() returns nothing: + bool observed = exercise(none) + trace observed + return nothing +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let function = exercise(&program); + assert_eq!(function.breakpoint_regions.len(), 1); + assert!(matches!( + function.breakpoint_regions[0].endpoint, + Endpoint::CanonicallyAborted { .. } + )); + assert!(!function.blocks.iter().any(|b| { + b.statements + .iter() + .any(|s| matches!(s.kind, StatementKind::Breakpoint { .. })) + })); + crate::validate_call_ownership(&program, &types) + .expect("absent call executes before the exact canonical abort"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + function.breakpoint_regions[0].selections[0].selected = function.entry; + rejects(&changed, &types); + let mut changed = program.clone(); + exercise_mut(&mut changed).breakpoint_regions[0].abort_allowed = false; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_observation_stays_lexical_across_generic_and_inline_bodies() { + let source = r#"namespace app +function consume[T](value: T) returns bool: + return true +function exercise(values: list[int64]) returns bool: + function(list[int64]) returns bool callback = function(item: list[int64]) returns bool: return consume(item) + breakpoint(consume(values) and callback(clone values)) + return consume(values) +"#; + let (program, types) = source_program(source, false); + crate::validate_call_ownership(&program, &types) + .expect("ordinary callee bodies do not inherit observation"); + assert!( + program + .functions + .iter() + .filter(|f| f.debug_kind == hir::FunctionDebugKind::Inline) + .all(|f| f.breakpoint_regions.is_empty()) + ); + let function = exercise(&program); + let sites = validate(function, &types).expect("lexical observed call sites"); + assert!(!sites.terminator(function.blocks.last().expect("return").id)); + let ordinary = source.replace( + " breakpoint(consume(values) and callback(clone values))", + " bool used = consume(values)", + ); + let parsed = jett_parser::parse(&ordinary, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked.diagnostics.iter().any(|d| d.code.code() == 400), + "{:?}", + checked.diagnostics + ); +} + +#[test] +fn breakpoint_extraction_float_snapshot_equality_is_bitwise_including_nan_and_signed_zero() { + let span = Span::new(FileId::new(0), 1, 2); + let statement = |bits| Statement { + kind: StatementKind::Evaluate(Expression { + kind: hir::ExpressionKind::Float(f64::from_bits(bits)), + ty: TypeInterner::FLOAT64, + span, + }), + span, + }; + let nan = statement(0x7ff8_0000_0000_0001); + assert!(statement_equal(&nan, &nan)); + assert!(!statement_equal(&nan, &statement(0x7ff8_0000_0000_0002))); + assert!(!statement_equal(&statement(0), &statement(1_u64 << 63))); +} + +#[test] +fn breakpoint_extraction_invalid_original_never_disappears_in_a_preparation() { + let (program, types) = source_program(HANDLED, false); + for pass in [ + crate::prepare_native_sequences, + crate::prepare_native_uninhabited_sums, + crate::prepare_native_generated_functions, + ] { + let mut changed = program.clone(); + let record = &mut exercise_mut(&mut changed).breakpoint_regions[0]; + record.edges.clear(); + let before = changed.clone(); + pass(&mut changed, &types); + assert_eq!( + changed, before, + "failed original proof keeps all runtime and private records" + ); + rejects(&changed, &types); + } +} + +#[test] +fn breakpoint_extraction_present_success_selection_keeps_the_live_endpoint_and_tag() { + let source = r#"namespace app +function exercise[T, E](outcome: result[T, E]) returns bool: + breakpoint(outcome handle error: + return false + ) + return true +function main() returns nothing: + bool observed = exercise(ok(true)) + trace observed + return nothing +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let function = exercise(&program); + assert!(matches!( + function.breakpoint_regions[0].endpoint, + Endpoint::Live(_) + )); + assert_eq!(function.breakpoint_regions[0].selections.len(), 1); + assert!(function.blocks.iter().any(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + })); + crate::validate_call_ownership(&program, &types) + .expect("selected true edge retains runtime tag and exact live endpoint"); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let selecting = function.breakpoint_regions[0].selections[0].block; + function.blocks[selecting.index() as usize] + .terminator + .span + .start += 1; + rejects(&changed, &types); +} + +#[test] +fn breakpoint_extraction_ordinary_loop_prefixes_shift_sites_without_gaining_observation() { + let source = r#"namespace app +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function exercise(view values: list[int64]) returns bool: + for item in view values: + breakpoint(maybe_flag(true) handle error: + default false + ) + return true +"#; + let (mut program, types) = source_program(source, false); + crate::prepare_native_sequences(&mut program, &types); + let function = exercise(&program); + let sites = + validate(function, &types).expect("ordinary loop boundary and dense breakpoint sites"); + let mut getters = 0; + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if matches!(statement.kind, StatementKind::SequenceGet { .. }) { + getters += 1; + assert!( + !sites.statement(block.id, index), + "outside ForEach prefix stays ordinary" + ); + } + } + } + assert_eq!(getters, 1); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("shared body prefix index remaps are exact"); +} diff --git a/crates/jett_mir/src/call_owner_generations.rs b/crates/jett_mir/src/call_owner_generations.rs new file mode 100644 index 00000000..6859c7e8 --- /dev/null +++ b/crates/jett_mir/src/call_owner_generations.rs @@ -0,0 +1,1339 @@ +//! Unique ownership of a captured mutable TypeConstruction generation. +//! Escrows are internal native storage, never Jett value locals or Source roots. +use super::*; +use jett_typecheck::{CheckedCalleeAccess, CheckedCallerEffect, CheckedCallerSyntax}; +use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct CallOwnerGenerationId(usize); +impl CallOwnerGenerationId { + pub fn index(self) -> usize { + self.0 + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct CallOwnerEscrowId(usize); +impl CallOwnerEscrowId { + pub fn index(self) -> usize { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallGenerationSlot { + generation: CallOwnerGenerationId, + escrow: CallOwnerEscrowId, + root: LocalId, + ty: TypeId, +} +impl CallGenerationSlot { + pub fn generation(&self) -> CallOwnerGenerationId { + self.generation + } + pub fn escrow(&self) -> CallOwnerEscrowId { + self.escrow + } + pub fn root(&self) -> LocalId { + self.root + } + pub fn ty(&self) -> TypeId { + self.ty + } +} + +#[derive(Debug, Clone)] +pub struct CallGenerationStoragePlan { + slots: Vec, + loans: BTreeMap>, +} +impl CallGenerationStoragePlan { + pub(crate) fn empty() -> Self { + Self { + slots: Vec::new(), + loans: BTreeMap::new(), + } + } + pub fn slots(&self) -> &[CallGenerationSlot] { + &self.slots + } + pub fn slot(&self, generation: CallOwnerGenerationId) -> Option<&CallGenerationSlot> { + self.slots + .get(generation.index()) + .filter(|slot| slot.generation == generation) + } + pub(crate) fn covers_loan(&self, generation: CallOwnerGenerationId, loan: LocalId) -> bool { + self.loans + .get(&generation) + .is_some_and(|loans| loans.contains(&loan)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Site { + block: BlockId, + statement: usize, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Edge { + source: BlockId, + arm: usize, + target: BlockId, +} +#[derive(Debug, Clone, PartialEq)] +struct CaptureSite { + loan: LocalId, + parameter: usize, + original_argument: hir::ArgumentOwnership, + begin: Site, +} +#[derive(Debug, Clone, PartialEq)] +struct ReplacementSite { + original: hir::Statement, + rhs: LocalId, + acquired: Site, + replacement: Site, +} + +/// The original invocation/assignment archive is immutable in original IDs. +/// Only live root/loan/RHS/sites and exact current snapshots are remapped. +#[derive(Debug, Clone, PartialEq)] +pub(super) struct CallOwnerGeneration { + id: CallOwnerGenerationId, + slot: CallOwnerEscrowId, + owner: FunctionId, + identity: FunctionIdentity, + entry: BlockId, + original_call: Expression, + original_root: hir::CallerBindingFact, + // Exact original HIR local header. Its full declaration span differs + // from the immutable Source fact's identifier span and never remaps. + original_root_header: Local, + current_source: hir::SourceCallOwnership, + consumer: Option, + root: LocalId, + captures: Vec, + replacements: Vec, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, + edges: Vec, +} + +#[derive(Clone)] +struct Scope { + original: Option, + generations: Vec, +} +#[derive(Clone)] +struct Draft { + original_call: Expression, + original_root: hir::CallerBindingFact, + // Exact original HIR local header. Its full declaration span differs + // from the immutable Source fact's identifier span and never remaps. + original_root_header: Local, + root: LocalId, + captures: Vec, + replacements: Vec, + start: Site, + statements: BTreeMap, + terminators: BTreeMap, + blocks: BTreeSet, +} +#[derive(Clone, Default)] +pub(super) struct Capture { + scopes: Vec, + drafts: Vec, + recording: BTreeSet, +} +impl Capture { + pub(super) fn begin_scope(&mut self, original: &Expression, types: &TypeInterner) { + let selected = source_packet(original).is_some_and(|source| source.arguments.iter().any(|argument| + argument.syntax == CheckedCallerSyntax::WrittenView + && argument.effect == CheckedCallerEffect::RetainBorrow + && argument.callee_access == CheckedCalleeAccess::View + && argument.physical_access == CheckedCalleeAccess::View + && matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if fact.mode == hir::CallerBindingMode::Owned && fact.mutable && (fact.ty.index() as usize) < types.len() && matches!(types.resolve(fact.ty), jett_types::Type::TypeConstruction)))); + self.scopes.push(Scope { + original: selected.then(|| original.clone()), + generations: Vec::new(), + }); + } + pub(super) fn end_scope(&mut self) -> Vec { + self.scopes + .pop() + .map(|scope| scope.generations) + .unwrap_or_default() + } + pub(super) fn abandoned(&self, depth: usize) -> Vec { + self.scopes + .iter() + .skip(depth) + .rev() + .flat_map(|scope| scope.generations.iter().rev().copied()) + .collect() + } + pub(super) fn stop(&mut self, id: CallOwnerGenerationId) { + self.recording.remove(&id); + } + pub(super) fn suspend(&mut self) -> Vec<(Option, Vec)> { + std::mem::take(&mut self.scopes) + .into_iter() + .map(|scope| (scope.original, scope.generations)) + .collect() + } + pub(super) fn restore( + &mut self, + scopes: Vec<(Option, Vec)>, + ) { + self.scopes = scopes + .into_iter() + .map(|(original, generations)| Scope { + original, + generations, + }) + .collect(); + } + pub(super) fn protected(&self, root: LocalId) -> Option { + self.scopes + .iter() + .rev() + .flat_map(|scope| scope.generations.iter().rev()) + .find(|id| self.drafts[id.index()].root == root) + .copied() + } + pub(super) fn candidate( + &mut self, + parameter: usize, + root: LocalId, + projection: &Expression, + block: BlockId, + statement: usize, + locals: &[Local], + types: &TypeInterner, + ) -> Result, String> { + let Some(scope) = self.scopes.last() else { + return Ok(None); + }; + let Some(original) = &scope.original else { + return Ok(None); + }; + let Some(source) = source_packet(original) else { + return Ok(None); + }; + let Some(argument) = source.arguments.get(parameter) else { + return Ok(None); + }; + let hir::CallerOrigin::Binding(fact) = &argument.origin else { + return Ok(None); + }; + let fact = *fact; + if argument.syntax != CheckedCallerSyntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.callee_access != CheckedCalleeAccess::View + || argument.physical_access != CheckedCalleeAccess::View + || fact.mode != hir::CallerBindingMode::Owned + || !fact.mutable + || fact.local != root + || fact.ty.index() as usize >= types.len() + || !matches!(types.resolve(fact.ty), jett_types::Type::TypeConstruction) + { + return Ok(None); + } + let Some(local) = locals + .get(root.index() as usize) + .filter(|local| local.id == root) + else { + return Err("call owner generation source root is outside its function".into()); + }; + let E::View(raw) = &projection.kind else { + return Err("call owner generation needs its original explicit view".into()); + }; + if !matches!(raw.kind, E::Local(id) if id == root) + || local.ty != fact.ty + || local.debug_ty != fact.ty + || !local.mutable + || local.view_source.is_some() + || local.span.file != fact.declaration_span.file + || fact.declaration_span.start < local.span.start + || fact.declaration_span.end > local.span.end + || argument.actual_type != fact.ty + || argument.parameter_type != fact.ty + || argument.source_witness().occurrence_type() != fact.ty + || raw.ty != fact.ty + || projection.ty != fact.ty + || source_operands(original) + .and_then(|args| args.get(parameter)) + .is_none_or(|original| { + !crate::breakpoint_regions::expression_equal(original, projection) + }) + { + return Err("call owner generation changes its exact plain source binding".into()); + } + if let Some(id) = self.protected(root) { + if !self + .scopes + .last() + .is_some_and(|scope| scope.generations.contains(&id)) + || !self.drafts[id.index()].replacements.is_empty() + { + return Err( + "call owner generation needs a separate overlapping cohort proof".into(), + ); + } + return Ok(Some((id, false))); + } + let id = CallOwnerGenerationId(self.drafts.len()); + let draft = Draft { + original_call: original.clone(), + original_root: fact, + original_root_header: local.clone(), + root, + captures: Vec::new(), + replacements: Vec::new(), + start: Site { block, statement }, + statements: BTreeMap::new(), + terminators: BTreeMap::new(), + blocks: BTreeSet::from([block]), + }; + self.drafts.push(draft); + self.scopes + .last_mut() + .ok_or("call owner generation has no current scope")? + .generations + .push(id); + self.recording.insert(id); + Ok(Some((id, true))) + } + pub(super) fn capture_loan( + &mut self, + id: CallOwnerGenerationId, + loan: LocalId, + parameter: usize, + block: BlockId, + statement: usize, + ) -> Result<(), String> { + let draft = self + .drafts + .get_mut(id.index()) + .ok_or("call owner generation capture is absent")?; + let argument = source_packet(&draft.original_call) + .and_then(|source| source.arguments.get(parameter)) + .ok_or("call owner generation original source argument is absent")? + .clone(); + if draft + .captures + .iter() + .any(|capture| capture.loan == loan || capture.parameter == parameter) + { + return Err("call owner generation duplicates a capture".into()); + } + draft.captures.push(CaptureSite { + loan, + parameter, + original_argument: argument, + begin: Site { block, statement }, + }); + Ok(()) + } + pub(super) fn replacement( + &mut self, + id: CallOwnerGenerationId, + original: &hir::Statement, + rhs: LocalId, + acquired: (BlockId, usize), + replacement: (BlockId, usize), + ) -> Result<(), String> { + let draft = self + .drafts + .get_mut(id.index()) + .ok_or("call owner generation replacement is absent")?; + let hir::StatementKind::Assign { target, value } = &original.kind else { + return Err("call owner generation replacement is not its source Assign".into()); + }; + if !matches!(target.kind, E::Local(root) if root == draft.root) + || target.ty != draft.original_root.ty + || value.ty != target.ty + || rhs == draft.root + { + return Err( + "call owner generation replacement changes its root or acquired type".into(), + ); + } + draft.replacements.push(ReplacementSite { + original: original.clone(), + rhs, + acquired: Site { + block: acquired.0, + statement: acquired.1, + }, + replacement: Site { + block: replacement.0, + statement: replacement.1, + }, + }); + Ok(()) + } + pub(super) fn statement(&mut self, block: BlockId, statement: usize, value: &Statement) { + for id in &self.recording { + let draft = &mut self.drafts[id.index()]; + draft.blocks.insert(block); + draft + .statements + .insert(Site { block, statement }, value.clone()); + } + } + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + for id in &self.recording { + let draft = &mut self.drafts[id.index()]; + draft.blocks.insert(block); + draft.terminators.insert(block, value.clone()); + } + } + pub(super) fn block(&mut self, block: BlockId) { + for id in &self.recording { + self.drafts[id.index()].blocks.insert(block); + } + } + pub(super) fn seal(self, function: &Function) -> Result, String> { + if !self.scopes.is_empty() || !self.recording.is_empty() { + return Err("call owner generation constructor left an open source scope".into()); + } + let edges = all_edges(function); + let records = self.drafts.into_iter().enumerate().map(|(index, draft)| CallOwnerGeneration { + id: CallOwnerGenerationId(index), slot: CallOwnerEscrowId(index), owner: function.id, identity: function.identity.clone(), entry: function.entry, + current_source: draft.statements.values().find_map(|statement| { + statement_source(statement).filter(|source| draft.captures.iter().all(|capture| + source.arguments.get(capture.parameter).is_some_and(|argument| + matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan)))) + .cloned() + }).expect("constructor-selected consuming Source invocation"), + consumer: draft.statements.iter().find_map(|(site, statement)| { + statement_source(statement).filter(|source| draft.captures.iter().all(|capture| + source.arguments.get(capture.parameter).is_some_and(|argument| + matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan)))) + .map(|_| *site) + }), + original_call: draft.original_call, original_root: draft.original_root, + original_root_header: draft.original_root_header, root: draft.root, + captures: draft.captures, replacements: draft.replacements, start: draft.start, + statements: draft.statements, terminators: draft.terminators, + edges: touching(&edges, &draft.blocks), blocks: draft.blocks, + }).collect::>(); + if records.iter().any(|record| record.consumer.is_none()) { + return Err("call owner generation has no constructor-owned Source consumer".into()); + } + Ok(records) + } +} + +fn statement_source(statement: &Statement) -> Option<&hir::SourceCallOwnership> { + match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => source_packet(value), + _ => None, + } +} + +pub(super) fn has_records(function: &Function) -> bool { + !function.call_owner_generations.is_empty() +} + +fn source_operands(value: &Expression) -> Option<&[Expression]> { + match &value.kind { + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + Some(args) + } + _ => None, + } +} + +fn source_packet(value: &Expression) -> Option<&hir::SourceCallOwnership> { + match &value.kind { + E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } + | E::IndirectCall { + ownership: hir::CallOwnership::Source(source), + .. + } + | E::Intrinsic { + ownership: hir::CallOwnership::Source(source), + .. + } => Some(source), + _ => None, + } +} +use hir::ExpressionKind as E; +fn all_edges(function: &Function) -> Vec { + let mut result = Vec::new(); + for block in &function.blocks { + let targets = match &block.terminator.kind { + TerminatorKind::Goto(target) => vec![*target], + TerminatorKind::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + TerminatorKind::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(*otherwise) + .collect(), + TerminatorKind::ForEach { body, exit, .. } => vec![*body, *exit], + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => Vec::new(), + }; + result.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + result +} +fn touching(edges: &[Edge], blocks: &BTreeSet) -> Vec { + edges + .iter() + .copied() + .filter(|edge| blocks.contains(&edge.source) || blocks.contains(&edge.target)) + .collect() +} +fn operation(statement: &StatementKind) -> Option { + match statement { + StatementKind::OpenCallOwnerGeneration { generation, .. } + | StatementKind::ReplaceCallOwnerGeneration { generation, .. } + | StatementKind::CloseCallOwnerGeneration { generation } => Some(*generation), + _ => None, + } +} + +pub(super) fn validate( + function: &Function, + types: &TypeInterner, +) -> Result { + let records = &function.call_owner_generations; + if records.is_empty() { + return if function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| operation(&statement.kind).is_some()) + { + Err("call owner generation operation has no constructor-owned record".into()) + } else { + Ok(CallGenerationStoragePlan::empty()) + }; + } + let mut plan = CallGenerationStoragePlan::empty(); + let mut occurrences = BTreeMap::new(); + let edges = all_edges(function); + for (index, record) in records.iter().enumerate() { + if record.id.index() != index + || record.slot.index() != index + || record.owner != function.id + || record.identity != function.identity + || record.entry != function.entry + { + return Err("call owner generation has duplicate or foreign ownership".into()); + } + let root = function + .local(record.root) + .ok_or("call owner generation root is absent")?; + let header = &record.original_root_header; + if header.id != record.original_root.local + || header.ty != record.original_root.ty + || header.debug_ty != header.ty + || !header.mutable + || header.view_source.is_some() + || header.span.file != record.original_root.declaration_span.file + || record.original_root.declaration_span.start < header.span.start + || record.original_root.declaration_span.end > header.span.end + { + return Err( + "call owner generation changes its sealed original source or local header".into(), + ); + } + if root.name != header.name + || root.ty != header.ty + || root.debug_ty != header.debug_ty + || root.debug_type_name != header.debug_type_name + || root.mutable != header.mutable + || root.view_source != header.view_source + || root.span != header.span + { + return Err("call owner generation changes its sealed local declaration header".into()); + } + if root.ty != record.original_root.ty + || root.debug_ty != root.ty + || !root.mutable + || root.view_source.is_some() + || function.parameter_for_local(root.id).is_some() + || root.ty.index() as usize >= types.len() + || !matches!(types.resolve(root.ty), jett_types::Type::TypeConstruction) + || record.original_root.mode != hir::CallerBindingMode::Owned + || !record.original_root.mutable + || record.captures.is_empty() + || !record.blocks.contains(&record.start.block) + || touching(&edges, &record.blocks) != record.edges + { + return Err( + "call owner generation loses its owning mutable plain source root or CFG".into(), + ); + } + for (&site, snapshot) in &record.statements { + let actual = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.statement)); + if !record.blocks.contains(&site.block) + || actual.is_none_or(|actual| { + !crate::breakpoint_regions::statement_equal(actual, snapshot) + }) + { + return Err( + "call owner generation changes an exact constructor-owned statement".into(), + ); + } + if operation(&snapshot.kind) == Some(record.id) + && occurrences.insert(site, record.id).is_some() + { + return Err("call owner generation duplicates its operation occurrence".into()); + } + } + for (&block, snapshot) in &record.terminators { + if function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block) + .is_none_or(|value| { + !crate::breakpoint_regions::terminator_equal(&value.terminator, snapshot) + }) + { + return Err( + "call owner generation changes an exact constructor-owned terminator".into(), + ); + } + } + if !matches!(record.statements.get(&record.start).map(|s| &s.kind), Some(StatementKind::OpenCallOwnerGeneration { generation, root }) if *generation == record.id && *root == record.root) + { + return Err("call owner generation loses its unique original Open".into()); + } + let mut loans = HashSet::new(); + for capture in &record.captures { + let loan = function + .local(capture.loan) + .ok_or("call owner generation loan is absent")?; + if !loans.insert(capture.loan) + || loan.ty != root.ty + || loan.view_source != Some(root.id) + || function.parameter_for_local(loan.id).is_some() + { + return Err( + "call owner generation duplicates or changes its nonowning capture".into(), + ); + } + let Some(Statement { + kind: StatementKind::BeginCallView { local, value }, + .. + }) = record.statements.get(&capture.begin) + else { + return Err("call owner generation capture loses its exact Begin".into()); + }; + let E::View(raw) = &value.kind else { + return Err( + "call owner generation capture needs its original explicit view".into(), + ); + }; + let argument = &capture.original_argument; + if source_packet(&record.original_call) + .and_then(|source| source.arguments.get(capture.parameter)) + != Some(argument) + || !matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if *fact == record.original_root) + { + return Err( + "call owner generation loses its immutable original source witness".into(), + ); + } + if *local != loan.id + || raw.ty != root.ty + || value.ty != root.ty + || !matches!(raw.kind, E::Local(id) if id == root.id) + || source_operands(&record.original_call) + .and_then(|args| args.get(capture.parameter)) + .is_none_or(|original| original.span != value.span) + || argument.actual_type != root.ty + || argument.parameter_type != root.ty + || argument.source_witness().occurrence_type() != root.ty + || argument.syntax != CheckedCallerSyntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.callee_access != CheckedCalleeAccess::View + || argument.physical_access != CheckedCalleeAccess::View + { + return Err( + "call owner generation capture changes its complete source contract".into(), + ); + } + } + for replacement in &record.replacements { + let rhs = function + .local(replacement.rhs) + .ok_or("call owner generation acquired RHS is absent")?; + if rhs.id == root.id + || rhs.ty != root.ty + || rhs.view_source.is_some() + || function.parameter_for_local(rhs.id).is_some() + || !matches!(record.statements.get(&replacement.acquired).map(|s| &s.kind), Some(StatementKind::Let { local, value }) if *local == rhs.id && value.ty == root.ty) + || !matches!(record.statements.get(&replacement.replacement).map(|s| &s.kind), Some(StatementKind::ReplaceCallOwnerGeneration { generation, root: actual, rhs_owner }) if *generation == record.id && *actual == root.id && *rhs_owner == rhs.id) + { + return Err( + "call owner generation replacement loses its prior independent owning RHS" + .into(), + ); + } + dominates(function, replacement.acquired, replacement.replacement)?; + if function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(statement.kind, StatementKind::Let { local, .. } | StatementKind::BeginCallView { local, .. } if local == rhs.id)).count() != 1 { + return Err("call owner generation RHS does not have a unique acquisition".into()); + } + } + if let Some(consumer) = record.consumer { + let source = record + .statements + .get(&consumer) + .and_then(statement_source) + .ok_or("call owner generation loses its exact Source consumer")?; + for capture in &record.captures { + let argument = source + .arguments + .get(capture.parameter) + .ok_or("call owner generation consumer loses its formal")?; + if !matches!(argument.staging, hir::ArgumentStaging::Borrowed { loan } if loan == capture.loan) + { + return Err("call owner generation consumer changes its nonowning loan".into()); + } + dominates(function, capture.begin, consumer)?; + } + if *source != record.current_source { + return Err( + "call owner generation consumer changes its original closed Source witness" + .into(), + ); + } + } + plan.slots.push(CallGenerationSlot { + generation: record.id, + escrow: record.slot, + root: root.id, + ty: root.ty, + }); + plan.loans.insert(record.id, loans); + } + for block in &function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + if let Some(generation) = operation(&value.kind) { + if occurrences.get(&Site { + block: block.id, + statement, + }) != Some(&generation) + { + return Err( + "call owner generation operation has no exact constructor-owned occurrence" + .into(), + ); + } + } + } + } + if !records.is_empty() { + validate_protocol(function, &plan)?; + } + Ok(plan) +} + +fn dominates(function: &Function, acquired: Site, used: Site) -> Result<(), String> { + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + let mut queue = vec![used]; + let mut seen = BTreeSet::new(); + let mut reached = false; + while let Some(site) = queue.pop() { + if site.block == acquired.block && acquired.statement < site.statement { + reached = true; + continue; + } + if !seen.insert(site.block) { + continue; + } + if site.block == function.entry || cfg.predecessors(site.block).is_empty() { + return Err("call owner generation replacement precedes RHS acquisition".into()); + } + for &block in cfg.predecessors(site.block) { + queue.push(Site { + block, + statement: function.blocks[block.index() as usize].statements.len(), + }); + } + } + if !reached { + return Err("call owner generation RHS has an uninitialized cycle".into()); + } + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum StorageState { + Closed, + Attached, + Retired, + MaybeRetired, +} +#[derive(Clone, PartialEq, Eq)] +struct ProtocolState { + storage: StorageState, + loans: HashSet, +} +impl ProtocolState { + fn closed() -> Self { + Self { + storage: StorageState::Closed, + loans: HashSet::new(), + } + } +} +fn merge(left: &ProtocolState, right: &ProtocolState) -> Result { + let storage = match (left.storage, right.storage) { + (StorageState::Closed, StorageState::Closed) => StorageState::Closed, + (StorageState::Closed, _) | (_, StorageState::Closed) => { + return Err("call owner generation has a path bypassing its active scope".into()); + } + (left, right) if left == right => left, + _ => StorageState::MaybeRetired, + }; + if left.loans != right.loans { + return Err("call owner generation joins different active capture cohorts".into()); + } + Ok(ProtocolState { + storage, + loans: left.loans.clone(), + }) +} +fn validate_protocol(function: &Function, plan: &CallGenerationStoragePlan) -> Result<(), String> { + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + let initial = vec![ProtocolState::closed(); plan.slots.len()]; + let mut outgoing: Vec>> = vec![None; function.blocks.len()]; + let mut queue = VecDeque::from([function.entry]); + let mut pending = BTreeSet::from([function.entry]); + while let Some(block) = queue.pop_front() { + pending.remove(&block); + let mut incoming: Option> = + (block == function.entry).then(|| initial.clone()); + for predecessor in cfg.predecessors(block) { + if let Some(states) = &outgoing[predecessor.index() as usize] { + if let Some(current) = &mut incoming { + for (current, state) in current.iter_mut().zip(states) { + *current = merge(current, state)?; + } + } else { + incoming = Some(states.clone()); + } + } + } + let Some(mut states) = incoming else { + continue; + }; + transfer(function, block, plan, &mut states)?; + if outgoing[block.index() as usize].as_ref() != Some(&states) { + outgoing[block.index() as usize] = Some(states); + for &successor in cfg.successors(block) { + if pending.insert(successor) { + queue.push_back(successor); + } + } + } + } + // Disconnected blocks remain an integrity obligation, never a permission. + for block in &function.blocks { + if outgoing[block.id.index() as usize].is_none() { + let mut states = initial.clone(); + transfer(function, block.id, plan, &mut states)?; + } + } + Ok(()) +} +fn transfer( + function: &Function, + block: BlockId, + plan: &CallGenerationStoragePlan, + states: &mut [ProtocolState], +) -> Result<(), String> { + for statement in &function.blocks[block.index() as usize].statements { + match &statement.kind { + StatementKind::OpenCallOwnerGeneration { generation, root } => { + let slot = plan + .slot(*generation) + .ok_or("call owner generation Open has no storage")?; + let state = &mut states[generation.index()]; + if *root != slot.root + || state.storage != StorageState::Closed + || !state.loans.is_empty() + { + return Err("call owner generation opens an active or foreign escrow".into()); + } + state.storage = StorageState::Attached; + } + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => { + let slot = plan + .slot(*generation) + .ok_or("call owner generation Replace has no storage")?; + let state = &mut states[generation.index()]; + if *root != slot.root + || *rhs_owner == *root + || state.storage == StorageState::Closed + || state.loans.is_empty() + { + return Err( + "call owner generation replacement has no captured active owner".into(), + ); + } + state.storage = StorageState::Retired; + } + StatementKind::CloseCallOwnerGeneration { generation } => { + let state = states + .get_mut(generation.index()) + .ok_or("call owner generation Close has no storage")?; + if state.storage == StorageState::Closed || !state.loans.is_empty() { + return Err( + "call owner generation closes an active loan or already closed escrow" + .into(), + ); + } + state.storage = StorageState::Closed; + } + StatementKind::BeginCallView { local, .. } => { + for (id, loans) in &plan.loans { + if loans.contains(local) { + let state = &mut states[id.index()]; + if state.storage != StorageState::Attached || !state.loans.insert(*local) { + return Err( + "call owner generation recaptures a retired or active cohort" + .into(), + ); + } + } + } + } + StatementKind::EndCallView { local } => { + for (id, loans) in &plan.loans { + if loans.contains(local) && !states[id.index()].loans.remove(local) { + return Err("call owner generation End has no active capture".into()); + } + } + } + StatementKind::Assign { target, .. } => { + if let E::Local(root) = target.kind { + if plan.slots.iter().any(|slot| { + slot.root == root + && states[slot.generation.index()].storage != StorageState::Closed + }) { + return Err( + "call owner generation protected root still uses ordinary Assign" + .into(), + ); + } + } + } + _ => {} + } + } + if matches!( + function.blocks[block.index() as usize].terminator.kind, + TerminatorKind::Return(_) | TerminatorKind::Respond(_) + ) && states + .iter() + .any(|state| state.storage != StorageState::Closed || !state.loans.is_empty()) + { + return Err("call owner generation exits before exact loan and escrow cleanup".into()); + } + Ok(()) +} + +// Only finite canonical preparation calls these transitions. Original archives +// never change and are never marked as runtime or liveness roots. +fn shift(record: &mut CallOwnerGeneration, block: BlockId, amount: usize) { + let shift_site = |site: &mut Site| { + if site.block == block { + site.statement += amount; + } + }; + shift_site(&mut record.start); + if let Some(consumer) = &mut record.consumer { + shift_site(consumer); + } + for capture in &mut record.captures { + shift_site(&mut capture.begin); + } + for replacement in &mut record.replacements { + shift_site(&mut replacement.acquired); + shift_site(&mut replacement.replacement); + } + record.statements = std::mem::take(&mut record.statements) + .into_iter() + .map(|(mut site, value)| { + shift_site(&mut site); + (site, value) + }) + .collect(); +} +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types)?; + // Reuse the identical complete finite before/after edit validation. This + // shadow cannot mint a generation or change the actual breakpoint records. + let mut shadow = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + let mut records = before.call_owner_generations.clone(); + for record in &mut records { + if let Some((block, prefix)) = &edit.prefix { + shift(record, *block, prefix.len()); + } + let owns_header = record.terminators.contains_key(&edit.header); + if owns_header { + record.terminators.insert(edit.header, edit.after.clone()); + } + for (block, _, new) in &edit.redirects { + if record.terminators.contains_key(block) { + record.terminators.insert(*block, new.clone()); + } + } + if owns_header { + for (block, values) in &edit.append { + record.blocks.insert(*block); + let offset = before.blocks[block.index() as usize].statements.len(); + let offset = offset + + edit + .prefix + .as_ref() + .filter(|(id, _)| id == block) + .map_or(0, |(_, values)| values.len()); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + statement: offset + index, + }, + value.clone(), + ); + } + } + if let Some((block, values)) = &edit.prefix { + record.blocks.insert(*block); + for (index, value) in values.iter().enumerate() { + record.statements.insert( + Site { + block: *block, + statement: index, + }, + value.clone(), + ); + } + } + for block in &edit.blocks { + record.blocks.insert(block.id); + record + .terminators + .insert(block.id, block.terminator.clone()); + for (index, value) in block.statements.iter().enumerate() { + record.statements.insert( + Site { + block: block.id, + statement: index, + }, + value.clone(), + ); + } + } + } + record.edges = touching(&all_edges(function), &record.blocks); + } + function.call_owner_generations = records; + Ok(()) +} +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types)?; + let mut shadow = function.clone(); + crate::breakpoint_regions::sum_transition(&mut shadow, before, types)?; + for record in &mut function.call_owner_generations { + for (block, snapshot) in &mut record.terminators { + let actual = &function.blocks[block.index() as usize].terminator; + if !crate::breakpoint_regions::terminator_equal(snapshot, actual) { + *snapshot = actual.clone(); + } + } + record.edges = touching(&all_edges_from_blocks(&function.blocks), &record.blocks); + } + Ok(()) +} +fn all_edges_from_blocks(blocks: &[BasicBlock]) -> Vec { + let mut edges = Vec::new(); + for block in blocks { + let targets = match &block.terminator.kind { + TerminatorKind::Goto(target) => vec![*target], + TerminatorKind::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + TerminatorKind::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(*otherwise) + .collect(), + TerminatorKind::ForEach { body, exit, .. } => vec![*body, *exit], + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + TerminatorKind::Return(_) + | TerminatorKind::Respond(_) + | TerminatorKind::Unreachable => Vec::new(), + }; + edges.extend(targets.into_iter().enumerate().map(|(arm, target)| Edge { + source: block.id, + arm, + target, + })); + } + edges +} +fn remap_statement(value: &mut Statement, map: &[Option]) -> Result<(), String> { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![value.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::breakpoint_regions::remap_snapshot(&mut block, map)?; + *value = block.statements.remove(0); + Ok(()) +} +pub(super) fn remap_locals(function: &mut Function, map: &[Option]) -> Result<(), String> { + let get = |id: LocalId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("generation surviving operand has no local remap") + }; + for record in &mut function.call_owner_generations { + record.root = get(record.root)?; + for capture in &mut record.captures { + capture.loan = get(capture.loan)?; + } + for replacement in &mut record.replacements { + replacement.rhs = get(replacement.rhs)?; + } + for value in record.statements.values_mut() { + remap_statement(value, map)?; + } + for value in record.terminators.values_mut() { + let mut block = BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: value.clone(), + }; + crate::breakpoint_regions::remap_snapshot(&mut block, map)?; + *value = block.terminator; + } + if record.consumer.is_some() { + let mut ownership = hir::CallOwnership::Source(record.current_source.clone()); + ownership.remap_metadata_locals(|id| get(id).map_err(str::to_owned))?; + let hir::CallOwnership::Source(source) = ownership else { + unreachable!() + }; + record.current_source = source; + } + } + Ok(()) +} +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + // Exactly the reachable compaction, never a caller-supplied subset/range. + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let reachable = cfg + .reverse_postorder() + .iter() + .copied() + .collect::>(); + if map.len() != before.blocks.len() + || before + .blocks + .iter() + .any(|block| map[block.id.index() as usize].is_some() != reachable.contains(&block.id)) + { + return Err("generation block remap is not exact reachable compaction".into()); + } + let get = |id: BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("generation surviving site has no block remap") + }; + let mut expected = before + .blocks + .iter() + .filter(|block| reachable.contains(&block.id)) + .cloned() + .collect::>(); + for block in &mut expected { + block.id = get(block.id)?; + crate::sequences::prune::block_targets(&mut block.terminator.kind, &|target| { + // Reachability includes every actual target, so this cannot be absent. + *target = map[target.index() as usize].expect("reachable edge target remains"); + }); + } + if !crate::breakpoint_regions::blocks_equal(&expected, &function.blocks) + || function.entry != get(before.entry)? + { + return Err("generation compaction changes surviving original blocks".into()); + } + let mut records = Vec::new(); + let mut ids = vec![None; before.call_owner_generations.len()]; + for mut record in before.call_owner_generations.clone() { + if get(record.start.block).is_err() { + if record.statements.keys().any(|site| get(site.block).is_ok()) { + return Err("generation surviving operations lose their Open".into()); + } + continue; + } + let new_id = CallOwnerGenerationId(records.len()); + ids[record.id.index()] = Some(new_id); + record.id = new_id; + record.slot = CallOwnerEscrowId(new_id.index()); + record.entry = get(record.entry)?; + record.start.block = get(record.start.block)?; + record.consumer = record.consumer.and_then(|mut site| { + site.block = get(site.block).ok()?; + Some(site) + }); + record.captures.retain_mut(|capture| { + if let Ok(block) = get(capture.begin.block) { + capture.begin.block = block; + true + } else { + false + } + }); + record.replacements.retain_mut(|replacement| { + if let (Ok(acquired), Ok(replaced)) = ( + get(replacement.acquired.block), + get(replacement.replacement.block), + ) { + replacement.acquired.block = acquired; + replacement.replacement.block = replaced; + true + } else { + false + } + }); + record.statements = record + .statements + .into_iter() + .filter_map(|(mut site, value)| { + site.block = get(site.block).ok()?; + Some((site, value)) + }) + .collect(); + record.terminators = record + .terminators + .into_iter() + .filter_map(|(block, mut value)| { + let block = get(block).ok()?; + crate::sequences::prune::block_targets(&mut value.kind, &|target| { + *target = map[target.index() as usize].expect("reachable snapshot edge") + }); + Some((block, value)) + }) + .collect(); + record.blocks = record + .blocks + .into_iter() + .filter_map(|block| get(block).ok()) + .collect(); + records.push(record); + } + let remap_operation = |statement: &mut Statement| -> Result<(), String> { + match &mut statement.kind { + StatementKind::OpenCallOwnerGeneration { generation, .. } + | StatementKind::ReplaceCallOwnerGeneration { generation, .. } + | StatementKind::CloseCallOwnerGeneration { generation } => { + *generation = ids + .get(generation.index()) + .copied() + .flatten() + .ok_or("generation operation survives its removed record")?; + } + _ => {} + } + Ok(()) + }; + for block in &mut function.blocks { + for statement in &mut block.statements { + remap_operation(statement)?; + } + } + for record in &mut records { + for value in record.statements.values_mut() { + remap_operation(value)?; + } + record.edges = touching(&all_edges(function), &record.blocks); + } + function.call_owner_generations = records; + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/call_owner_generations/tests.rs b/crates/jett_mir/src/call_owner_generations/tests.rs new file mode 100644 index 00000000..d81c9451 --- /dev/null +++ b/crates/jett_mir/src/call_owner_generations/tests.rs @@ -0,0 +1,500 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::HashMap; + +const CONDITIONAL: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + trace builder + return extra +function exercise(incoming: optional[int64]) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, incoming handle: + trace switchable + switchable = type.construct_start[Alternate]() + trace switchable + default 7 + ) + trace switchable + return selected +function main() returns nothing: + int64 absent = exercise(none) + trace absent + int64 present = exercise(some(11)) + trace present + return nothing +"#; + +const ABORT: &str = r#"namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + trace builder + return extra +function exercise(incoming: optional[int64]) returns int64: + mutable TypeConstruction switchable = type.construct_start[User]() + int64 selected = inspect_builder(view switchable, incoming handle: + switchable = type.construct_start[Alternate]() + trace switchable + return 13 + ) + trace switchable + return selected +function main() returns nothing: + int64 aborted = exercise(none) + trace aborted + return nothing +"#; + +fn source_program(source: &str, release: bool) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("checked Source HIR"); + let program = crate::lower(&high, &checked.interner).expect("constructor-owned generation MIR"); + crate::validate_call_ownership(&program, &checked.interner) + .expect("all original Source and generation proofs"); + (program, checked.interner) +} +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .expect("exercise") +} +fn prepared(program: &mut Program, types: &TypeInterner) { + crate::prepare_native_sequences(program, types); + crate::prepare_native_uninhabited_sums(program, types); + crate::prepare_native_generated_functions(program, types); + crate::validate_call_ownership(program, types) + .expect("fresh prepared Source and generation proofs"); +} +fn rejects(program: &Program, types: &TypeInterner, expected: &str) { + let message = validate(exercise(program), types) + .expect_err("corrupted generation must fail independently"); + assert!(message.contains(expected), "{message}"); + assert!( + crate::validate_call_ownership(program, types).is_err(), + "outer complete gate also refuses" + ); +} + +#[test] +fn generation_conditional_replacement_preserves_current_reads_and_captured_owner() { + for release in [false, true] { + let (mut program, types) = source_program(CONDITIONAL, release); + let function = exercise(&program); + assert_eq!(function.call_owner_generations.len(), 1); + let record = &function.call_owner_generations[0]; + assert_eq!(record.captures.len(), 1); + assert_eq!(record.replacements.len(), 1); + let loan = record.captures[0].loan; + let root = record.root; + // The actual capture is exactly a View(Local), never a new owner clone. + let capture = &function.blocks[record.captures[0].begin.block.index() as usize].statements + [record.captures[0].begin.statement]; + assert!( + matches!(&capture.kind, StatementKind::BeginCallView { value, .. } if matches!(&value.kind, E::View(raw) if matches!(raw.kind, E::Local(local) if local == root))) + ); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("scoped old owner survives replacement without cloning"); + let slot = plan.generation_storage().slots()[0]; + assert_eq!(slot.root(), root); + assert_eq!(slot.generation().index(), 0); + assert_eq!(slot.escrow().index(), 0); + assert!( + plan.generation_storage() + .covers_loan(slot.generation(), loan) + ); + prepared(&mut program, &types); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("fresh prepared generation and liveness"); + } +} + +#[test] +fn generation_abort_ends_loans_before_cleanup_and_never_requires_callee_entry() { + for release in [false, true] { + let (mut program, types) = source_program(ABORT, release); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + let abort = function.blocks.iter().find(|block| matches!(&block.terminator.kind, TerminatorKind::Return(Some(value)) if matches!(value.kind, E::Int(13)))).expect("source handler return13"); + let close = abort.statements.iter().position(|s| matches!(s.kind, StatementKind::CloseCallOwnerGeneration { generation } if generation == record.id)).expect("abort closes escrow"); + assert!( + close > 0 + && matches!(abort.statements[close - 1].kind, StatementKind::EndCallView { local } if local == record.captures[0].loan) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("abort cleanup has no retained owning capture"); + prepared(&mut program, &types); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("prepared abort cleanup"); + } +} + +#[test] +fn generation_repeated_replacements_keep_one_captured_escrow() { + let source = CONDITIONAL.replace(" trace switchable\n default 7", " trace switchable\n switchable = type.construct_start[User]()\n trace switchable\n default 7"); + for release in [false, true] { + let (program, types) = source_program(&source, release); + let function = exercise(&program); + assert_eq!(function.call_owner_generations.len(), 1); + assert_eq!(function.call_owner_generations[0].replacements.len(), 2); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("two replacements preserve the first captured generation"); + assert_eq!(plan.generation_storage().slots().len(), 1); + } +} + +#[test] +fn generation_operations_need_exact_private_sites_identity_and_independent_rhs() { + let (program, types) = source_program(CONDITIONAL, false); + for mutation in 0..6 { + let mut altered = program.clone(); + let function = exercise_mut(&mut altered); + let record = function.call_owner_generations[0].clone(); + match mutation { + 0 => { + function.blocks[record.start.block.index() as usize] + .statements + .remove(record.start.statement); + } + 1 => { + let value = function.blocks[record.start.block.index() as usize].statements + [record.start.statement] + .clone(); + function.blocks[record.start.block.index() as usize] + .statements + .push(value); + } + 2 => { + let site = record.replacements[0].replacement; + let StatementKind::ReplaceCallOwnerGeneration { rhs_owner, .. } = + &mut function.blocks[site.block.index() as usize].statements[site.statement] + .kind + else { + panic!("Replace") + }; + *rhs_owner = record.root; + } + 3 => { + let site = record.replacements[0].acquired; + let StatementKind::Let { value, .. } = + &mut function.blocks[site.block.index() as usize].statements[site.statement] + .kind + else { + panic!("acquired Let") + }; + value.kind = E::Local(record.captures[0].loan); + } + 4 => { + let last = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|s| { + matches!( + s.kind, + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + ) + }) + }) + .expect("close block"); + let value = last + .statements + .iter() + .find(|s| { + matches!( + s.kind, + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + ) + }) + .unwrap() + .clone(); + last.statements.push(value); + } + 5 => { + function.call_owner_generations[0].owner = program + .functions + .iter() + .find(|f| f.id != function.id) + .unwrap() + .id; + } + _ => unreachable!(), + } + rejects( + &altered, + &types, + if mutation == 5 { + "foreign" + } else { + "constructor-owned" + }, + ); + } +} + +#[test] +fn generation_disconnected_copy_and_alternate_scope_entry_are_not_authority() { + let (program, types) = source_program(CONDITIONAL, false); + let mut copy = program.clone(); + let function = exercise_mut(&mut copy); + let record = function.call_owner_generations[0].clone(); + let close = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .find(|s| { + matches!( + s.kind, + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + ) + }) + .unwrap() + .clone(); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: vec![close], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + rejects(©, &types, "constructor-owned"); + + let mut entry = program.clone(); + let function = exercise_mut(&mut entry); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Goto(record.replacements[0].replacement.block), + span: function.span, + }, + }); + rejects(&entry, &types, "CFG"); +} + +#[test] +fn generation_canonical_loop_and_dense_compaction_preserve_archives_without_roots() { + let source = CONDITIONAL.replace(" trace switchable\n default 7", " trace switchable\n for item in view values:\n trace switchable\n default 7").replace(" int64 selected =", " list[int64] values = list(1, 2)\n int64 selected ="); + for release in [false, true] { + let (mut program, types) = source_program(&source, release); + let original = exercise(&program).call_owner_generations[0].clone(); + let function = exercise_mut(&mut program); + let extra = LocalId::new(function.locals.len() as u32); + function.locals.push(Local { + id: extra, + name: "unused_generation_control".to_string(), + debug_type_name: None, + ty: TypeInterner::INT64, + debug_ty: TypeInterner::INT64, + mutable: false, + span: function.span, + view_source: None, + }); + assert!( + function + .local(extra) + .is_some_and(|local| local.name == "unused_generation_control") + ); + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + crate::validate_call_ownership(&program, &types) + .expect("unused metadata creates no execution authority"); + prepared(&mut program, &types); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + assert!( + crate::breakpoint_regions::expression_equal( + &original.original_call, + &record.original_call + ), + "original call archive is immutable" + ); + assert_eq!( + record.original_root, original.original_root, + "Source binding archive is immutable" + ); + assert_eq!( + record.original_root_header, original.original_root_header, + "full local header archive is immutable" + ); + assert_eq!( + record + .replacements + .iter() + .map(|replacement| &replacement.original) + .collect::>(), + original + .replacements + .iter() + .map(|replacement| &replacement.original) + .collect::>(), + "original source assignment archives are immutable" + ); + // Sequence lowering adds live cursor/length locals, so total count + // cannot prove whether this particular unused seed was removed. + assert!( + function + .locals + .iter() + .all(|local| local.name != "unused_generation_control") + ); + assert!( + function + .locals + .iter() + .enumerate() + .all(|(index, local)| local.id == LocalId::new(index as u32)), + "all surviving current local IDs remain dense" + ); + assert!( + function + .blocks + .iter() + .all(|b| !matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("canonical sequence addition and compaction rejoin every generation site"); + } +} + +#[test] +fn generation_source_name_and_full_local_headers_remain_independent_and_exact() { + for release in [false, true] { + let (mut program, types) = source_program(CONDITIONAL, release); + let function = exercise(&program); + let record = &function.call_owner_generations[0]; + let source_header = record.original_root; + let local_header = record.original_root_header.clone(); + assert_eq!(function.local(record.root), Some(&local_header)); + assert_eq!(source_header.local, local_header.id); + assert_eq!(source_header.declaration_span.file, local_header.span.file); + assert!(local_header.span.start < source_header.declaration_span.start); + assert!(source_header.declaration_span.end < local_header.span.end); + assert_eq!( + &CONDITIONAL[source_header.declaration_span.start as usize + ..source_header.declaration_span.end as usize], + "switchable" + ); + assert!( + CONDITIONAL[local_header.span.start as usize..local_header.span.end as usize] + .starts_with("mutable TypeConstruction switchable =") + ); + validate(function, &types) + .expect("each original checked header has its own exact coordinate role"); + prepared(&mut program, &types); + let record = &exercise(&program).call_owner_generations[0]; + assert_eq!( + record.original_root, source_header, + "Source identifier fact stays original" + ); + assert_eq!( + record.original_root_header, local_header, + "whole declaration archive never remaps" + ); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .expect("fresh prepared storage retains both independent headers"); + + for mutation in 0..6 { + let mut altered = program.clone(); + let function = exercise_mut(&mut altered); + let root = function.call_owner_generations[0].root; + match mutation { + 0 => { + // This span still contains the original name; containment + // alone must not replace exact sealed-header validation. + function.locals[root.index() as usize].span.start += 1; + } + 1 => function.locals[root.index() as usize] + .name + .push_str("_foreign"), + 2 => { + function.locals[root.index() as usize].debug_type_name = + Some("ForeignHeader".into()) + } + 3 => { + function.call_owner_generations[0] + .original_root + .declaration_span + .start += 1 + } + 4 => { + let header = &mut function.call_owner_generations[0].original_root_header; + header.id = LocalId::new(header.id.index() + 1); + } + 5 => { + function.call_owner_generations[0] + .original_root_header + .span + .start += 1 + } + _ => unreachable!(), + } + rejects( + &altered, + &types, + match mutation { + 3 => "immutable original source witness", + 4 => "sealed original source or local header", + _ => "sealed local declaration header", + }, + ); + } + } +} diff --git a/crates/jett_mir/src/call_ownership.rs b/crates/jett_mir/src/call_ownership.rs new file mode 100644 index 00000000..9442f905 --- /dev/null +++ b/crates/jett_mir/src/call_ownership.rs @@ -0,0 +1,3376 @@ +//! Typed source acquisitions, separate from physical invocation operands. +//! Every original block is checked before reachability or local compaction. +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; + +use jett_common::Span; +use jett_hir::{self as hir, Expression, ExpressionKind as E, LocalId}; +use jett_typecheck::{CheckedCallerEffect, CheckedOwnershipContext}; +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::{ + BlockId, Function, Program, ResourceCallNode, ResourceConstructorNode, StatementKind as S, + TerminatorKind as T, +}; + +#[cfg(test)] +mod tests; + +/// A validated source transfer. `binding` is the original caller's binding, +/// not the generated physical slot or a backing parent of a field copy. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceAcquisition { + pub binding: Option, + pub effect: CheckedCallerEffect, + pub actual_type: TypeId, + pub storage_type: TypeId, + pub source_span: Span, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ArgumentAcquisition { + pub parameter_index: usize, + pub source_index: usize, + pub source: SourceAcquisition, +} + +/// References identify already-validated occurrences in this immutable MIR. +/// Pointer equality is only a cache lookup, never ownership authority. +#[derive(Debug)] +pub struct CallerAcquisitions<'a> { + pub(crate) resource_pending: bool, + owners: BTreeMap, + calls: Vec<(&'a Expression, Vec)>, + operands: Vec<(&'a Expression, LocalId)>, + // Exact input reads authenticated by a complete converted Source Handle. + handled_conversions: Vec<(&'a Expression, &'a Expression)>, + pub(super) absent_successes: Vec, + pub(super) present_successes: Vec, + generation_storage: crate::CallGenerationStoragePlan, +} + +impl Default for CallerAcquisitions<'_> { + fn default() -> Self { + Self { + resource_pending: false, + owners: BTreeMap::new(), + calls: Vec::new(), + operands: Vec::new(), + handled_conversions: Vec::new(), + absent_successes: Vec::new(), + present_successes: Vec::new(), + generation_storage: crate::CallGenerationStoragePlan::empty(), + } + } +} +impl CallerAcquisitions<'_> { + pub(crate) fn generation_storage(&self) -> &crate::CallGenerationStoragePlan { + &self.generation_storage + } + pub fn owner_initializer(&self, owner: LocalId) -> Option<&SourceAcquisition> { + self.owners.get(&owner.index()) + } + + pub fn arguments(&self, expression: &Expression) -> Option<&[ArgumentAcquisition]> { + self.calls.iter().find_map(|(call, arguments)| { + std::ptr::eq(*call, expression).then_some(arguments.as_slice()) + }) + } + + pub fn owner_initializers(&self) -> impl Iterator { + self.owners + .iter() + .map(|(&local, source)| (LocalId::new(local), source)) + } + + /// Readonly occurrence lookup after full Source/CFG/Begin validation. + /// This does not make an unassociated View initializer owning or copyable. + pub(crate) fn handled_conversion_input(&self, conversion: &Expression) -> Option<&Expression> { + self.handled_conversions + .iter() + .find_map(|(proved, input)| std::ptr::eq(*proved, conversion).then_some(*input)) + } + + pub fn argument_binding(&self, expression: &Expression) -> Option { + self.operands + .iter() + .find_map(|(operand, binding)| std::ptr::eq(*operand, expression).then_some(*binding)) + } +} + +/// Internal original proof; only canonical sum preparation turns it into a +/// retained certificate. It is not part of the public caller-acquisition API. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct AbsentSuccessPlan { + pub(super) function: hir::FunctionId, + pub(super) identity: hir::FunctionIdentity, + pub(super) source: LocalId, + pub(super) tag: LocalId, + pub(super) output: LocalId, + pub(super) source_type: TypeId, + pub(super) output_type: TypeId, + pub(super) span: Span, + pub(super) selecting: BlockId, + pub(super) failure: BlockId, +} + +/// Only a complete original success-edge proof can be minted by canonical +/// Result(T,Never) preparation. No public packet or Goto creates this authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PresentSuccessPlan { + pub(super) function: hir::FunctionId, + pub(super) identity: hir::FunctionIdentity, + pub(super) source: LocalId, + pub(super) tag: LocalId, + pub(super) output: LocalId, + pub(super) source_type: TypeId, + pub(super) output_type: TypeId, + pub(super) span: Span, + pub(super) selecting: BlockId, + pub(super) success: BlockId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Site { + block: BlockId, + statement: usize, +} + +#[derive(Clone, Copy)] +enum Definition<'a> { + Let(&'a Expression), + Begin(&'a Expression), + SumTake { source: LocalId, success: bool }, + Other, +} + +#[derive(Clone, Copy)] +struct Defined<'a> { + site: Site, + span: Span, + kind: Definition<'a>, +} + +fn restore_converted_observation_snapshot( + initializer: &Expression, + argument: &hir::ArgumentOwnership, + types: &TypeInterner, +) -> Result { + if !matches!(initializer.kind, E::InterfaceCoerce { .. }) + || !hir::observation_data_type(types, argument.actual_type) + || !crate::handlers::can_snapshot_view(types, argument.actual_type) + { + return Err( + "call ownership observation requires its exact ordinary raw snapshot initializer" + .into(), + ); + } + fn restore( + value: &Expression, + argument: &hir::ArgumentOwnership, + ) -> Result { + match &value.kind { + E::InterfaceCoerce { + value: raw, + adapters, + } => Ok(Expression { + kind: E::InterfaceCoerce { + value: Box::new(restore(raw, argument)?), + adapters: adapters.clone(), + }, + ty: value.ty, + span: value.span, + }), + E::Clone(raw) + if value.ty == argument.actual_type + && raw.ty == argument.actual_type + && value.span == argument.source_span + && raw.span == argument.source_span => + { + Ok(raw.as_ref().clone()) + } + _ => Err("call ownership observation lost its exact raw snapshot occurrence".into()), + } + } + restore(initializer, argument) +} + +/// The integration gate runs this for every original function, including +/// descriptor-only bodies and disconnected blocks. HIR signature projections +/// retain exact IDs/identities/modes; no source names supply target authority. +pub fn validate_function<'a>( + program: &Program, + function: &'a Function, + types: &TypeInterner, +) -> Result, String> { + program.resource_manifest.validate(types)?; + ProgramValidation::new(program).validate_function(function, types) +} + +/// One immutable signature projection for one ownership validation pass. +/// The program borrow prevents reuse across mutation or canonical preparation. +pub(crate) struct ProgramValidation<'p> { + _program: &'p Program, + signatures: Vec, +} + +impl<'p> ProgramValidation<'p> { + pub(crate) fn new(program: &'p Program) -> Self { + let signatures = program + .functions + .iter() + .map(|function| hir::Function { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + // MIR has already replaced resolver-session IDs with declaration identity. + // The source certificate and exact dispatcher shape remain authoritative. + source_definition: None, + actor_handler: function.actor_handler_entry().ok().flatten().cloned(), + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + locals: function.locals.clone(), + body: hir::Block { + statements: Vec::new(), + span: function.span, + }, + span: function.span, + }) + .collect::>(); + Self { + _program: program, + signatures, + } + } + + pub(crate) fn validate_function<'a>( + &self, + function: &'a Function, + types: &TypeInterner, + ) -> Result, String> { + validate_function_with_signatures( + function, + types, + &self.signatures, + &self._program.resource_manifest, + ) + } +} + +fn validate_function_with_signatures<'a>( + function: &'a Function, + types: &TypeInterner, + signatures: &[hir::Function], + manifest: &hir::ResourceManifest, +) -> Result, String> { + // Includes disconnected blocks and unused original declarations. This gate + // is shared by public preparation and standalone MoveValuePlan acquisition. + crate::temporary_local_views::validate(function, types)?; + // Fresh exact site proof precedes lexical context selection. + let breakpoint_sites = crate::breakpoint_regions::validate(function, types)?; + let mut validator = Validator::new(function, types, signatures, manifest)?; + let context = if function.debug_kind == hir::FunctionDebugKind::Inline { + CheckedOwnershipContext::Ordinary + } else { + match function.identity.declaration.kind { + hir::DeclarationKind::Verify => CheckedOwnershipContext::Verify, + hir::DeclarationKind::Property => CheckedOwnershipContext::Property, + _ => CheckedOwnershipContext::Ordinary, + } + }; + for block in &function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + let site = Site { + block: block.id, + statement, + }; + let context = if breakpoint_sites.statement(block.id, statement) { + CheckedOwnershipContext::BreakpointExpression + } else { + context + }; + validator.statement(&value.kind, site, context)?; + } + let site = Site { + block: block.id, + statement: block.statements.len(), + }; + let context = if breakpoint_sites.terminator(block.id) { + CheckedOwnershipContext::BreakpointExpression + } else { + context + }; + validator.terminator(&block.terminator.kind, site, context)?; + } + validator.validate_prepared_absences()?; + validator.validate_prepared_presences()?; + validator.acquisitions.absent_successes = validator.absent_successes.into_inner(); + validator.acquisitions.present_successes = validator.present_successes.into_inner(); + validator.acquisitions.generation_storage = + crate::call_owner_generations::validate(function, types)?; + Ok(validator.acquisitions) +} + +struct Validator<'a, 't, 's> { + function: &'a Function, + types: &'t TypeInterner, + signatures: &'s [hir::Function], + manifest: &'s hir::ResourceManifest, + locals: Vec, + definitions: BTreeMap>>, + predecessors: Vec>, + stages: BTreeMap, + claimed_slots: BTreeSet, + acquisitions: CallerAcquisitions<'a>, + absent_successes: RefCell>, + present_successes: RefCell>, + iteration_scopes: Vec, + match_scopes: Vec, + iteration_scope_enabled: bool, +} + +impl<'a, 't, 's> Validator<'a, 't, 's> { + fn new( + function: &'a Function, + types: &'t TypeInterner, + signatures: &'s [hir::Function], + manifest: &'s hir::ResourceManifest, + ) -> Result { + manifest.validate_type(types, function.return_type)?; + for parameter in &function.params { + manifest.validate_type(types, parameter.ty)?; + } + for local in &function.locals { + manifest.validate_type(types, local.ty)?; + manifest.validate_type(types, local.debug_ty)?; + } + crate::ordinary_borrowed_sums::validate(function, types)?; + let iteration_scopes = crate::iteration_views::scopes(function, types)?; + let match_scopes = crate::match_bindings::scopes(function, types)?; + let locals = function + .locals + .iter() + .map(|local| hir::OwnershipLocalInfo { + ty: local.ty, + mutable: local.mutable, + span: local.span, + view_source: local.view_source, + view_iteration: None, + match_binding: None, + is_view_parameter: function + .parameter_for_local(local.id) + .is_some_and(|parameter| parameter.mode == hir::ParamMode::View), + }) + .collect::>(); + let stages = crate::call_views::validate(function, types)?; + let mut definitions = BTreeMap::>>::new(); + let mut predecessors = vec![Vec::new(); function.blocks.len()]; + let mut define = |local: LocalId, site, span, kind| { + definitions + .entry(local.index()) + .or_default() + .push(Defined { site, span, kind }); + }; + for block in &function.blocks { + if block.id.index() as usize >= function.blocks.len() + || function.blocks[block.id.index() as usize].id != block.id + { + return Err("call ownership block IDs are not dense".into()); + } + for (index, statement) in block.statements.iter().enumerate() { + if let S::ResourceConstructor(node) = &statement.kind { + function.resource_constructor(node.constructor())?; + } + let site = Site { + block: block.id, + statement: index, + }; + match &statement.kind { + S::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | S::Let { local, value } => { + define(*local, site, statement.span, Definition::Let(value)) + } + S::BeginCallView { local, value } => { + define(*local, site, statement.span, Definition::Begin(value)) + } + S::SumTake { + source, + target, + success, + } => { + let ordinary = + function.ordinary_borrowed_sum_projection(block.id, index)?; + let borrowed = function.resource_borrowed_sum_projection( + block.id, + crate::ResourcePosition::Statement(index), + )?; + let definition = if let Some(row) = ordinary { + if row.source() != *source + || (*success && row.output() != *target) + || (!*success && row.error() != Some(*target)) + { + return Err("call ownership ordinary borrowed Handle changed its extraction headers".into()); + } + Definition::Other + } else if let Some(row) = borrowed { + if row.source() != *source || (*success && row.output() != *target) { + return Err("call ownership borrowed Handle changed its current extraction headers".into()); + } + // A guarded view projection is never an owning producer. + Definition::Other + } else { + Definition::SumTake { + source: *source, + success: *success, + } + }; + define(*target, site, statement.span, definition); + } + S::ResourceCall(ResourceCallNode::Invoke { output: local, .. }) + | S::ResourceConstructor(ResourceConstructorNode::Commit { + output: local, + .. + }) + | S::CheckRefinement { local, .. } + | S::SequenceLength { target: local, .. } + | S::SequenceGet { target: local, .. } + | S::SumTag { target: local, .. } => { + define(*local, site, statement.span, Definition::Other) + } + S::ReplaceCallOwnerGeneration { root, .. } => { + define(*root, site, statement.span, Definition::Other) + } + S::Assign { target, .. } => { + if let Some(local) = address_root(target) { + define(local, site, statement.span, Definition::Other); + } + } + _ => {} + } + } + let site = Site { + block: block.id, + statement: block.statements.len(), + }; + match &block.terminator.kind { + T::Switch { variants, .. } => { + for (_, target, bindings) in variants { + for &local in bindings { + define( + local, + Site { + block: *target, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + } + } + } + T::ForEach { + key, value, body, .. + } => { + define( + *key, + Site { + block: *body, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + if let Some(value) = value { + define( + *value, + Site { + block: *body, + statement: 0, + }, + block.terminator.span, + Definition::Other, + ); + } + } + _ => { + let _ = site; + } + } + for target in successors(&block.terminator.kind) { + let incoming = predecessors + .get_mut(target.index() as usize) + .ok_or("call ownership CFG target is outside its function")?; + incoming.push(block.id); + } + } + for (id, local) in function.locals.iter().enumerate() { + if local.id.index() as usize != id + || !valid_type(types, local.ty) + || !valid_type(types, local.debug_ty) + { + return Err( + "call ownership local metadata is outside its function or interner".into(), + ); + } + } + for &local in definitions.keys() { + if function.local(LocalId::new(local)).is_none() { + return Err("call ownership definition is outside its local table".into()); + } + } + Ok(Self { + function, + types, + signatures, + manifest, + locals, + definitions, + predecessors, + stages, + claimed_slots: BTreeSet::new(), + acquisitions: CallerAcquisitions::default(), + absent_successes: RefCell::new(Vec::new()), + present_successes: RefCell::new(Vec::new()), + iteration_scopes, + match_scopes, + iteration_scope_enabled: true, + }) + } + + fn invocation( + &mut self, + expression: &'a Expression, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + if let E::ResourceInvoke { + hook, + ownership, + args, + evaluation_order, + .. + } = &expression.kind + { + if !self.manifest.contains_hook(hook) { + return Err( + "Resource invocation belongs to another original checked manifest".into(), + ); + } + let locals = self.locals_at(site); + hir::validate_hir_invocation( + self.signatures, + &locals, + expression, + self.types, + context, + )?; + let hir::CallOwnership::Source(source) = ownership else { + return Err("Resource invocation cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || evaluation_order.len() != args.len() { + return Err("Resource invocation source order or operand count changed".into()); + } + self.acquisitions.resource_pending = true; + return Ok(()); + } + let (ownership, args, order) = match &expression.kind { + E::Call { + ownership, + args, + evaluation_order, + .. + } + | E::Intrinsic { + ownership, + args, + evaluation_order, + .. + } + | E::IndirectCall { + ownership, + args, + evaluation_order, + .. + } => (ownership, args, evaluation_order), + _ => return Ok(()), + }; + if self.iteration_scope_enabled { + let ensure = |source: hir::CallerViewSource| { + if let hir::CallerViewSource::Local(local) = source { + crate::iteration_views::ensure_scope( + self.function, + &self.iteration_scopes, + local, + site.block, + )?; + crate::match_bindings::ensure_scope( + self.function, + &self.match_scopes, + local, + site.block, + )?; + } + Ok::<_, String>(()) + }; + match ownership { + hir::CallOwnership::Source(source) => { + for argument in &source.arguments { + match argument.origin { + hir::CallerOrigin::Binding(ref fact) => { + ensure(hir::CallerViewSource::Local(fact.local))? + } + hir::CallerOrigin::BorrowedProjection { source } + | hir::CallerOrigin::OwnedFieldCopy { parent: source } => { + ensure(source)? + } + hir::CallerOrigin::OwnedExpression => {} + } + } + for argument in &source.generated_operands { + ensure(argument.original_witness().origin())?; + } + } + hir::CallOwnership::Generated(generated) => { + for argument in &generated.arguments { + ensure(argument.original_witness().origin())?; + } + } + } + } + let local_info = self.locals_at(site); + hir::validate_operand_ownership( + ownership, + &args.iter().map(|value| value.ty).collect::>(), + order, + self.types, + |local| local_info.get(local.index() as usize).copied(), + false, + )?; + hir::validate_invocation_target(self.signatures, expression, self.types, context)?; + let resource_original = crate::resource_ownership::original_call_at( + self.function, + self.manifest, + self.types, + expression, + site.block, + site.statement, + )?; + if resource_original { + self.acquisitions.resource_pending = true; + } + match ownership { + hir::CallOwnership::Source(source) => { + let mut original = Vec::new(); + for (index, argument) in source.arguments.iter().enumerate() { + let value = &args[index]; + let acquisition = self.source_acquisition(argument, value.ty); + match argument.staging { + hir::ArgumentStaging::Original => { + let resource_actual = resource_original + && crate::resource_ownership::original_resource_actual( + self.manifest, + self.types, + argument.actual_type, + ); + if (argument.effect == CheckedCallerEffect::RelinquishOwned + && !resource_actual) + || (argument.effect == CheckedCallerEffect::ObserveData + && argument.physical_access + == jett_typecheck::CheckedCalleeAccess::Owned) + { + return Err("call ownership requires explicit owning staging at this physical boundary".into()); + } + self.original(value, argument, &source.bridge, site)?; + if consumes(argument.effect) && !resource_actual { + if let Some(binding) = acquisition.binding { + self.acquisitions.operands.push((value, binding)); + } + original.push(ArgumentAcquisition { + parameter_index: index, + source_index: argument.source_index, + source: acquisition, + }); + } + } + hir::ArgumentStaging::Transferred { owner } => { + self.slot_operand(value, owner, false)?; + let (initializer, defined) = self.owning_let(owner, value.ty, site)?; + self.original(initializer, argument, &source.bridge, defined.site)?; + self.claim_owner(owner, acquisition)?; + if !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) { + self.acquisitions.operands.push((value, owner)); + } + } + hir::ArgumentStaging::Copied { value: copied } => { + self.slot_operand(value, copied, matches!(value.kind, E::View(_)))?; + let (initializer, defined) = + self.storage_let(copied, value.ty, site, true)?; + if let Err(error) = + self.original(initializer, argument, &source.bridge, defined.site) + { + let E::Clone(inner) = &initializer.kind else { + return Err(error); + }; + if inner.ty != initializer.ty + || !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) + { + return Err(error); + } + self.original(inner, argument, &source.bridge, defined.site)?; + } + self.claim_slot(copied)?; + } + hir::ArgumentStaging::Observed { owner } => { + self.slot_operand(value, owner, false)?; + let (initializer, defined) = self.owning_let(owner, value.ty, site)?; + if argument.observation_proof().is_none() { + return Err("call ownership observation lost its checked data/capture proof".into()); + } + if let E::Clone(observed) = &initializer.kind { + if observed.ty != initializer.ty + || !crate::handlers::can_snapshot_view( + self.types, + initializer.ty, + ) + { + return Err("call ownership observation cannot snapshot an erased or unsupported value".into()); + } + self.original(observed, argument, &source.bridge, defined.site)?; + } else { + let restored = restore_converted_observation_snapshot( + initializer, + argument, + self.types, + )?; + self.original(&restored, argument, &source.bridge, defined.site)?; + } + self.claim_slot(owner)?; + } + hir::ArgumentStaging::Relinquished { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err( + "call ownership loan does not borrow the acquired owner".into(), + ); + } + let (initializer, defined) = + self.owning_let(owner, value.ty, begun.site)?; + // The scoped loan proves physical View access separately. + // Rejoin the complete owning initializer without inventing + // a wrapper around its original typed conversion spine. + self.original(initializer, argument, &source.bridge, defined.site)?; + self.claim_slot(loan)?; + self.claim_owner(owner, acquisition)?; + } + hir::ArgumentStaging::RetainedSnapshot { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err( + "retained snapshot loan does not borrow its exact owner".into(), + ); + } + let (initializer, defined) = + self.owning_let(owner, value.ty, begun.site)?; + let E::Clone(endpoint) = &initializer.kind else { + return Err( + "retained snapshot requires a full-endpoint Clone initializer" + .into(), + ); + }; + let physical_span = argument + .retained_snapshot_span() + .ok_or("retained snapshot has no checked physical occurrence")?; + // The initial physical View may have a pipeline-step span. + // Its child still rejoins the unchanged exact raw Source span. + let E::View(physical_loan) = &value.kind else { + unreachable!("slot_operand proved View"); + }; + let E::View(physical_owner) = &projection.kind else { + unreachable!("slot_operand proved View"); + }; + if argument.retained_snapshot_type() != Some(initializer.ty) + || endpoint.ty != initializer.ty + || initializer.span != physical_span + || endpoint.span != physical_span + || value.span != physical_span + || physical_loan.span != physical_span + || projection.span != physical_span + || physical_owner.span != physical_span + || !crate::handlers::can_snapshot_view(self.types, initializer.ty) + { + return Err("retained snapshot changes its checked endpoint type or occurrence".into()); + } + self.original(endpoint, argument, &source.bridge, defined.site)?; + self.retained_snapshot_order(source, argument, defined.site)?; + self.claim_slot(owner)?; + self.claim_slot(loan)?; + } + hir::ArgumentStaging::Borrowed { loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, site)?; + let restored = self + .restore_temporary_projection(projection, argument, begun.site)?; + if let Err(error) = + self.original(&restored, argument, &source.bridge, begun.site) + { + if !self.original_handled_view_initializer( + value, + projection, + argument, + &source.bridge, + begun.site, + )? { + return Err(error); + } + } else if hir::validate_source_handled_operand( + &restored, + argument, + &source.bridge, + self.types, + )? + .is_some() + { + self.handled_view_storage_headers( + value, projection, argument, begun.site, + )?; + } + self.claim_slot(loan)?; + } + } + } + self.acquisitions.calls.push((expression, original)); + for (tail, argument) in source.generated_operands.iter().enumerate() { + let value = &args[source.arguments.len() + tail]; + let (metadata, at) = match value.kind { + E::Local(local) => { + let (initializer, defined) = self.owning_let(local, value.ty, site)?; + self.claim_slot(local)?; + (initializer, defined.site) + } + _ => (value, site), + }; + self.generated_original(metadata, argument, at)?; + hir::validate_compiler_metadata_operand(metadata, source, self.types)?; + } + } + hir::CallOwnership::Generated(generated) => { + for (value, argument) in args.iter().zip(&generated.arguments) { + self.generated_operand(value, argument, site)?; + } + } + } + Ok(()) + } + + fn retained_snapshot_order( + &self, + source: &hir::SourceCallOwnership, + argument: &hir::ArgumentOwnership, + defined: Site, + ) -> Result<(), String> { + for later in &source.arguments { + if later.source_index <= argument.source_index { + continue; + } + let within = |span: Span| { + span.file == later.source_span.file + && span.start < span.end + && later.source_span.start <= span.start + && span.end <= later.source_span.end + }; + for block in &self.function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + if within(value.span) { + self.dominates( + defined, + Site { + block: block.id, + statement, + }, + )?; + } + } + if within(block.terminator.span) { + self.dominates( + defined, + Site { + block: block.id, + statement: block.statements.len(), + }, + )?; + } + } + } + Ok(()) + } + + fn locals_at(&self, site: Site) -> Vec { + let mut locals = self.locals.clone(); + if self.iteration_scope_enabled { + crate::iteration_views::apply( + &self.iteration_scopes, + site.block, + &mut locals, + self.types, + ); + crate::match_bindings::apply(&self.match_scopes, site.block, &mut locals); + } + locals + } + + fn source_acquisition( + &self, + argument: &hir::ArgumentOwnership, + storage_type: TypeId, + ) -> SourceAcquisition { + let binding = match &argument.origin { + hir::CallerOrigin::Binding(fact) + if matches!(fact.mode, hir::CallerBindingMode::Owned) + && !jett_typecheck::ownership::is_implicitly_copyable(self.types, fact.ty) => + { + Some(fact.local) + } + _ => None, + }; + SourceAcquisition { + binding, + effect: argument.effect, + actual_type: argument.actual_type, + storage_type, + source_span: argument.source_span, + } + } + + fn claim_slot(&mut self, local: LocalId) -> Result<(), String> { + if !self.claimed_slots.insert(local.index()) { + return Err( + "call ownership staging slot belongs to more than one source acquisition".into(), + ); + } + Ok(()) + } + + fn claim_owner(&mut self, owner: LocalId, source: SourceAcquisition) -> Result<(), String> { + self.claim_slot(owner)?; + if source.binding == Some(owner) { + return Err( + "call ownership acquired owner cannot replace its original binding in place".into(), + ); + } + self.acquisitions.owners.insert(owner.index(), source); + Ok(()) + } + + fn slot_operand( + &self, + expression: &Expression, + local: LocalId, + borrowed: bool, + ) -> Result<(), String> { + let value = if borrowed { + let E::View(value) = &expression.kind else { + return Err("call ownership staged loan requires physical View(Local)".into()); + }; + if value.ty != expression.ty { + return Err("call ownership staged view changes its slot type".into()); + } + value.as_ref() + } else { + expression + }; + if !matches!(value.kind, E::Local(id) if id == local) + || self + .function + .local(local) + .is_none_or(|metadata| metadata.ty != value.ty) + { + return Err( + "call ownership staged operand does not identify its exact storage slot".into(), + ); + } + Ok(()) + } + + fn owning_let( + &self, + local: LocalId, + ty: TypeId, + used: Site, + ) -> Result<(&'a Expression, Defined<'a>), String> { + self.storage_let(local, ty, used, false) + } + + fn storage_let( + &self, + local: LocalId, + ty: TypeId, + used: Site, + copy_view: bool, + ) -> Result<(&'a Expression, Defined<'a>), String> { + let metadata = self + .function + .local(local) + .ok_or("call ownership owner slot is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err("call ownership acquisition requires an exact new owning slot".into()); + } + let [defined] = self + .definitions + .get(&local.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership acquired slot requires one unique initializer".into()); + }; + let Definition::Let(value) = defined.kind else { + return Err("call ownership acquired slot is not an owning Let".into()); + }; + if value.ty != ty + || metadata.span != value.span + || (!copy_view && matches!(value.kind, E::View(_))) + { + return Err( + "call ownership owner initializer changes type, occurrence or ownership".into(), + ); + } + self.dominates(defined.site, used)?; + Ok((value, *defined)) + } + + fn loan( + &self, + loan: LocalId, + ty: TypeId, + used: Site, + ) -> Result<(&'a Expression, Defined<'a>), String> { + if !self.stages.contains_key(&(loan.index() as usize)) { + return Err("call ownership staged borrow has no typed Begin/End authority".into()); + } + let [defined] = self + .definitions + .get(&loan.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership loan requires one unique Begin".into()); + }; + let Definition::Begin(value) = defined.kind else { + return Err("call ownership loan initializer is not BeginCallView".into()); + }; + if value.ty != ty { + return Err("call ownership loan initializer changes its endpoint type".into()); + } + self.dominates(defined.site, used)?; + Ok((value, *defined)) + } + + fn actor_receiver( + &self, + current: &Expression, + proof: &hir::ActorReceiverProof, + site: Site, + ) -> Result<(), String> { + proof.validate_lowered(current, |value, shape| { + self.generated_storage(value, shape, site) + }) + } + + fn original( + &self, + value: &Expression, + argument: &hir::ArgumentOwnership, + bridge: &hir::CallBridge, + site: Site, + ) -> Result<(), String> { + let local_info = self.locals_at(site); + match hir::validate_source_operand(value, argument, bridge, &local_info, self.types) { + Ok(_) => Ok(()), + Err(error) => { + // A CFG result is not a source binding and never gets a fabricated + // Binding witness. Only a closed original producer may use this path. + if argument.origin != hir::CallerOrigin::OwnedExpression { + return Err(error); + } + let original = match hir::validate_source_handled_operand( + value, argument, bridge, self.types, + )? { + Some(backing) => backing, + None => { + let mut original = value; + while let E::View(inner) = &original.kind { + original = inner; + } + let occurrence_type = argument.source_witness().occurrence_type(); + if original.ty != occurrence_type || original.span != argument.source_span { + return Err(error); + } + original + } + }; + let E::Local(local) = original.kind else { + return Err(error); + }; + // The sealed physical occurrence may carry an expected Secret + // layer. Raw actual_type still controls caller acquisition. + self.produced_result(local, original.ty, original.span, site) + } + } + } + + fn produced_result( + &self, + local: LocalId, + ty: TypeId, + span: Span, + used: Site, + ) -> Result<(), String> { + let metadata = self + .function + .local(local) + .ok_or("call ownership produced result is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.span != span + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err("call ownership producer result has no exact owned metadata".into()); + } + let definitions = self + .definitions + .get(&local.index()) + .ok_or("call ownership producer result has no definitions")?; + let sum_anchors = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::SumTake { + source, + success: true, + } if defined.span == span => Some(source), + _ => None, + }) + .collect::>(); + let prepared = self + .function + .prepared_absent_successes + .iter() + .find(|record| record.plan().output == local); + if let Some(record) = prepared { + if record.plan().span != span || record.plan().output_type != ty { + return Err( + "call ownership prepared absence differs from its exact output occurrence" + .into(), + ); + } + self.prepared_absence(record.plan())?; + } + let default_source = match sum_anchors.as_slice() { + [source] => Some(*source), + [] => prepared.map(|record| record.plan().source), + _ => None, + }; + let present = self + .function + .prepared_present_successes + .iter() + .find(|record| record.plan().output == local); + if let Some(record) = present { + if record.plan().span != span || record.plan().output_type != ty { + return Err( + "call ownership prepared success differs from its exact output occurrence" + .into(), + ); + } + self.prepared_presence(record.plan())?; + } + let mut absent_plan = None; + let mut present_plan = None; + let mut producer = prepared.is_some(); + for defined in definitions { + match defined.kind { + Definition::SumTake { + source, + success: true, + } => { + if defined.span != span { + return Err("call ownership producer result has definitions from another source occurrence".into()); + } + let source_ty = self + .function + .local(source) + .ok_or("call ownership sum source is absent")? + .ty; + let payload = match self.types.resolve(source_ty) { + Type::Optional(inner) | Type::Result(inner, _) => *inner, + _ => { + return Err( + "call ownership produced result is not a typed sum payload".into(), + ); + } + }; + if payload == TypeInterner::NEVER { + absent_plan = Some(self.original_absence( + source, + local, + source_ty, + ty, + span, + defined.site.block, + )?); + } else if payload != ty + && !matches!(self.types.resolve(ty), Type::Secret(inner) if *inner == payload) + { + return Err( + "call ownership produced result differs from its sum payload".into(), + ); + } + self.producer_source(source, source_ty, span, defined.site)?; + self.sum_success_edge(source, defined.site.block)?; + if present.is_none() && self.present_source_payload(source_ty).is_some() { + present_plan = Some(self.original_presence( + source, + local, + source_ty, + ty, + span, + defined.site.block, + )?); + } + producer = true; + } + Definition::Let(value) if value.ty == ty && !matches!(value.kind, E::View(_)) => { + if let Some(source) = default_source { + if !contains(span, defined.span) || !contains(defined.span, value.span) { + return Err("call ownership handled default is outside its exact source occurrence".into()); + } + self.sum_failure_region(source, defined.site.block, span)?; + if self.handled_default_is_borrowed(value, defined.site) { + return Err( + "call ownership handled default cannot acquire a borrowed owner" + .into(), + ); + } + } else if defined.span != span { + return Err("call ownership producer result has definitions from another source occurrence".into()); + } + if let E::RefinementValidated(inner) = &value.kind { + let E::Local(source) = inner.kind else { + return Err( + "call ownership validated result has no owned candidate".into() + ); + }; + self.producer_source(source, inner.ty, span, defined.site)?; + if !matches!(self.types.resolve(ty), Type::Refinement { .. }) { + return Err( + "call ownership validated result has no nominal refinement target" + .into(), + ); + } + producer = true; + } else if !matches!(value.kind, E::Local(_) | E::Field { .. }) { + // Typed default/constructor definitions do not take a + // borrowed owner. The normal value/CFG verifier still + // checks their complete body and initialization edges. + producer = true; + } else if let E::Local(source) = value.kind { + if self.function.is_view_local(source) { + return Err( + "call ownership producer default cannot acquire a borrowed local" + .into(), + ); + } + } + } + _ => { + return Err( + "call ownership producer result contains an unsupported definition".into(), + ); + } + } + } + if !producer { + return Err( + "call ownership result is only a renamed binding, not a proved producer".into(), + ); + } + self.definitions_reach(definitions, used)?; + if let Some(plan) = absent_plan { + let mut plans = self.absent_successes.borrow_mut(); + if let Some(existing) = plans.iter().find(|existing| existing.output == plan.output) { + if existing != &plan { + return Err( + "call ownership absent output has contradictory original proofs".into(), + ); + } + } else { + plans.push(plan); + } + } + if let Some(plan) = present_plan { + let mut plans = self.present_successes.borrow_mut(); + if let Some(existing) = plans.iter().find(|existing| existing.output == plan.output) { + if existing != &plan { + return Err( + "call ownership present output has contradictory original proofs".into(), + ); + } + } else { + plans.push(plan); + } + } + Ok(()) + } + + fn restore_temporary_projection( + &self, + projection: &Expression, + argument: &hir::ArgumentOwnership, + used: Site, + ) -> Result { + let temporary = matches!( + argument.origin, + hir::CallerOrigin::BorrowedProjection { + source: hir::CallerViewSource::Other + } + ) || (argument.origin == hir::CallerOrigin::OwnedExpression + && argument.source_witness().syntax() + == jett_typecheck::CheckedCallerSyntax::WrittenView); + if !temporary { + return Ok(projection.clone()); + } + let root = address_root(projection) + .ok_or("call ownership temporary projection has no materialized root")?; + let ty = self + .function + .local(root) + .ok_or("call ownership temporary projection root is absent")? + .ty; + let (initializer, _) = self.owning_let(root, ty, used)?; + crate::call_views::replace_borrowed_root(projection, initializer.clone()) + .ok_or_else(|| "call ownership temporary projection lost its exact root type".into()) + } + + /// Rejoin only the exact converted original Handle after its owning storage + /// and Begin have been proved. The added physical View is not a source cast. + fn original_handled_view_initializer( + &mut self, + value: &Expression, + projection: &Expression, + argument: &hir::ArgumentOwnership, + bridge: &hir::CallBridge, + used: Site, + ) -> Result { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerSyntax as Syntax}; + if argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != Access::View + { + return Ok(false); + } + let E::View(_physical_loan) = &value.kind else { + return Ok(false); + }; + let E::View(physical_owner) = &projection.kind else { + return Ok(false); + }; + let E::Local(owner) = physical_owner.kind else { + return Ok(false); + }; + let (initializer, defined) = self.owning_let(owner, projection.ty, used)?; + if !matches!( + initializer.kind, + E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + return Ok(false); + } + let Some(backing) = + hir::validate_source_handled_operand(initializer, argument, bridge, self.types)? + else { + return Ok(false); + }; + if !matches!(backing.kind, E::Local(_)) { + return Ok(false); + } + self.handled_view_storage_headers(value, projection, argument, used)?; + self.original(initializer, argument, bridge, defined.site)?; + // The existing boxing operation reads its concrete payload borrowed and + // returns an independent owned box. Preserve the original written View; + // only this exact authenticated conversion/input pair models that read. + if let E::InterfaceCoerce { value: input, .. } = &initializer.kind + && matches!(self.types.resolve(initializer.ty), Type::Interface(_)) + && !crate::move_values::is_erased_interface(self.types, input.ty) + && matches!(&input.kind, E::View(raw) if std::ptr::eq(raw.as_ref(), backing)) + { + self.acquisitions + .handled_conversions + .push((initializer, input.as_ref())); + } + Ok(true) + } + + fn handled_view_storage_headers( + &self, + value: &Expression, + projection: &Expression, + argument: &hir::ArgumentOwnership, + used: Site, + ) -> Result<(), String> { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerSyntax as Syntax}; + if argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != CheckedCallerEffect::RetainBorrow + || argument.physical_access != Access::View + { + return Ok(()); + } + let E::View(physical_loan) = &value.kind else { + return Err("call ownership handled view lost its physical loan".into()); + }; + let E::View(physical_owner) = &projection.kind else { + return Err("call ownership handled view lost its physical owner".into()); + }; + let E::Local(owner) = physical_owner.kind else { + return Err("call ownership handled view has no materialized owner".into()); + }; + let (initializer, _) = self.owning_let(owner, projection.ty, used)?; + if value.span != argument.source_span + || physical_loan.span != argument.source_span + || projection.span != argument.source_span + || physical_owner.span != initializer.span + { + return Err("call ownership handled view changes its exact physical occurrence".into()); + } + Ok(()) + } + + fn producer_source( + &self, + source: LocalId, + ty: TypeId, + span: Span, + used: Site, + ) -> Result<(), String> { + let metadata = self + .function + .local(source) + .ok_or("call ownership producer source is absent")?; + if metadata.ty != ty + || metadata.debug_ty != ty + || metadata.span != span + || metadata.view_source.is_some() + || self.function.parameter_for_local(source).is_some() + { + return Err("call ownership producer source has no exact new owned slot".into()); + } + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership producer source requires one owning initializer".into()); + }; + let Definition::Let(value) = defined.kind else { + return Err("call ownership producer source is not an owning Let".into()); + }; + if value.ty != ty + || defined.span != span + || !contains(span, value.span) + || matches!(value.kind, E::View(_)) + { + return Err( + "call ownership producer source changes its type, source occurrence or ownership" + .into(), + ); + } + self.dominates(defined.site, used) + } + + fn sum_success_edge(&self, source: LocalId, success: BlockId) -> Result<(), String> { + let incoming = self + .predecessors + .get(success.index() as usize) + .ok_or("call ownership sum success block is absent")?; + if incoming.is_empty() { + return Err("call ownership sum payload has no selecting branch".into()); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership sum selecting block is absent")?; + if let Some(record) = self + .function + .prepared_present_successes + .iter() + .find(|record| { + let plan = record.plan(); + plan.source == source && plan.success == success && plan.selecting == previous + }) + { + self.prepared_presence(record.plan())?; + continue; + } + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership sum payload is not selected by its tag".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership sum selector is not a typed tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + { + return Err( + "call ownership sum success edge does not preserve tag ordering".into(), + ); + } + let [defined] = self + .definitions + .get(&tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership sum selector requires a unique tag definition".into()); + }; + let statement = self + .function + .blocks + .get(defined.site.block.index() as usize) + .and_then(|block| block.statements.get(defined.site.statement)) + .ok_or("call ownership sum tag definition is absent")?; + if !matches!(statement.kind, S::SumTag { source: owner, target } if owner == source && target == tag) + { + return Err("call ownership sum selector belongs to another owner".into()); + } + self.dominates( + defined.site, + Site { + block: previous, + statement: block.statements.len(), + }, + )?; + } + Ok(()) + } + + fn present_source_payload(&self, ty: TypeId) -> Option { + if !valid_type(self.types, ty) { + return None; + } + match self.types.resolve(ty) { + Type::Result(ok, error) + if *ok != TypeInterner::NEVER + && *ok != TypeInterner::ERROR + && *error == TypeInterner::NEVER + && valid_type(self.types, *ok) => + { + Some(*ok) + } + _ => None, + } + } + + fn original_presence( + &self, + source: LocalId, + output: LocalId, + source_type: TypeId, + output_type: TypeId, + span: Span, + success: BlockId, + ) -> Result { + if self.present_source_payload(source_type).is_none() { + return Err( + "call ownership present success requires an exact Result(T,Never) source".into(), + ); + } + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership present output is undefined")?; + if definitions + .iter() + .filter(|defined| { + matches!(defined.kind, + Definition::SumTake { source: owner, success: true } if owner == source) + && defined.span == span + && defined.site.block == success + }) + .count() + != 1 + { + return Err("call ownership present output requires one exact success anchor".into()); + } + let [selecting] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership present success requires one original selecting branch".into(), + ); + }; + let block = &self.function.blocks[selecting.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership present success has no original branch".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership present selector is not a tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + || condition.span != span + || block.terminator.span != span + { + return Err("call ownership present selector differs from its exact Handle".into()); + } + let plan = PresentSuccessPlan { + function: self.function.id, + identity: self.function.identity.clone(), + source, + tag, + output, + source_type, + output_type, + span, + selecting: *selecting, + success, + }; + self.presence_tag(&plan)?; + Ok(plan) + } + + fn presence_tag(&self, plan: &PresentSuccessPlan) -> Result<(), String> { + let payload = self + .present_source_payload(plan.source_type) + .ok_or("call ownership prepared success has no exact Result(T,Never) type")?; + if plan.function != self.function.id + || plan.identity != self.function.identity + || !valid_type(self.types, plan.output_type) + || (plan.output_type != payload + && !matches!(self.types.resolve(plan.output_type), Type::Secret(inner) if *inner == payload)) + { + return Err( + "call ownership prepared success has foreign function or payload type authority" + .into(), + ); + } + let source = self + .function + .local(plan.source) + .ok_or("call ownership present source is outside its function")?; + let output = self + .function + .local(plan.output) + .ok_or("call ownership present output is outside its function")?; + let tag = self + .function + .local(plan.tag) + .ok_or("call ownership present tag is outside its function")?; + if source.ty != plan.source_type + || source.debug_ty != plan.source_type + || output.ty != plan.output_type + || output.debug_ty != plan.output_type + || output.span != plan.span + || output.view_source.is_some() + || self.function.parameter_for_local(plan.output).is_some() + || tag.ty != TypeInterner::BOOL + || tag.debug_ty != TypeInterner::BOOL + || tag.span != plan.span + || tag.view_source.is_some() + || self.function.parameter_for_local(plan.tag).is_some() + { + return Err( + "call ownership present certificate changed its exact local metadata".into(), + ); + } + let block = self + .function + .blocks + .get(plan.selecting.index() as usize) + .ok_or("call ownership present selecting block is outside its function")?; + if self + .function + .blocks + .get(plan.success.index() as usize) + .is_none() + || plan.selecting == plan.success + || block.terminator.span != plan.span + { + return Err("call ownership present success edge is outside its exact Handle".into()); + } + let Some(statement) = block.statements.last() else { + return Err("call ownership present edge lost its runtime tag".into()); + }; + if statement.span != plan.span + || !matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + { + return Err("call ownership present edge lost its exact runtime SumTag".into()); + } + let [definition] = self + .definitions + .get(&plan.tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership present edge has an ambiguous tag definition".into()); + }; + if definition.site + != (Site { + block: plan.selecting, + statement: block.statements.len() - 1, + }) + { + return Err("call ownership present tag belongs to another selecting block".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, definition.site) + } + + fn prepared_presence(&self, plan: &PresentSuccessPlan) -> Result<(), String> { + self.presence_tag(plan)?; + if !matches!(self.function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.success) + { + return Err( + "call ownership prepared success changed its canonical selected edge".into(), + ); + } + let [selecting] = self + .predecessors + .get(plan.success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership prepared success has a foreign incoming edge".into()); + }; + if *selecting != plan.selecting { + return Err("call ownership prepared success lost its selecting predecessor".into()); + } + let [defined] = self + .definitions + .get(&plan.output.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership prepared success output requires one retained extraction".into(), + ); + }; + if defined.span != plan.span + || defined.site.block != plan.success + || !matches!(defined.kind, Definition::SumTake { source, success: true } if source == plan.source) + { + return Err("call ownership prepared success lost its exact successful SumTake".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, defined.site) + } + + fn validate_prepared_presences(&self) -> Result<(), String> { + let mut outputs = BTreeSet::new(); + for record in &self.function.prepared_present_successes { + if !outputs.insert(record.plan().output.index()) { + return Err( + "call ownership prepared success has duplicate output authority".into(), + ); + } + self.prepared_presence(record.plan())?; + } + Ok(()) + } + + fn absent_source_type(&self, ty: TypeId) -> bool { + if !valid_type(self.types, ty) { + return false; + } + match self.types.resolve(ty) { + Type::Optional(inner) => *inner == TypeInterner::NEVER, + Type::Result(ok, error) => { + *ok == TypeInterner::NEVER + && *error != TypeInterner::NEVER + && valid_type(self.types, *error) + } + _ => false, + } + } + + fn original_absence( + &self, + source: LocalId, + output: LocalId, + source_type: TypeId, + output_type: TypeId, + span: Span, + success: BlockId, + ) -> Result { + if !self.absent_source_type(source_type) { + return Err( + "call ownership absent success requires an exact uninhabited sum source".into(), + ); + } + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership absent output is undefined")?; + if definitions + .iter() + .filter(|defined| { + matches!(defined.kind, + Definition::SumTake { source: owner, success: true } if owner == source) + && defined.span == span + }) + .count() + != 1 + { + return Err("call ownership absent output requires one exact success anchor".into()); + } + let [selecting] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership absent success requires one selecting branch".into()); + }; + let block = &self.function.blocks[selecting.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err("call ownership absent success has no original branch".into()); + }; + let E::Local(tag) = condition.kind else { + return Err("call ownership absent selector is not a tag local".into()); + }; + if *then_block != success + || *else_block == success + || condition.ty != TypeInterner::BOOL + || condition.span != span + || block.terminator.span != span + { + return Err("call ownership absent selector differs from its exact Handle".into()); + } + let plan = AbsentSuccessPlan { + function: self.function.id, + identity: self.function.identity.clone(), + source, + tag, + output, + source_type, + output_type, + span, + selecting: *selecting, + failure: *else_block, + }; + self.absence_tag(&plan)?; + Ok(plan) + } + + fn absence_tag(&self, plan: &AbsentSuccessPlan) -> Result<(), String> { + if plan.function != self.function.id + || plan.identity != self.function.identity + || !self.absent_source_type(plan.source_type) + || !valid_type(self.types, plan.output_type) + { + return Err( + "call ownership prepared absence has foreign function or type authority".into(), + ); + } + let source = self + .function + .local(plan.source) + .ok_or("call ownership absent source is outside its function")?; + let output = self + .function + .local(plan.output) + .ok_or("call ownership absent output is outside its function")?; + let tag = self + .function + .local(plan.tag) + .ok_or("call ownership absent tag is outside its function")?; + if source.ty != plan.source_type + || source.debug_ty != plan.source_type + || output.ty != plan.output_type + || output.debug_ty != plan.output_type + || output.span != plan.span + || output.view_source.is_some() + || self.function.parameter_for_local(plan.output).is_some() + || tag.ty != TypeInterner::BOOL + || tag.debug_ty != TypeInterner::BOOL + || tag.span != plan.span + { + return Err( + "call ownership absent certificate changed its exact local metadata".into(), + ); + } + let block = self + .function + .blocks + .get(plan.selecting.index() as usize) + .ok_or("call ownership absent selecting block is outside its function")?; + if self + .function + .blocks + .get(plan.failure.index() as usize) + .is_none() + || plan.selecting == plan.failure + || block.terminator.span != plan.span + { + return Err("call ownership absent failure edge is outside its exact Handle".into()); + } + let Some(statement) = block.statements.last() else { + return Err("call ownership absent edge lost its runtime tag".into()); + }; + if statement.span != plan.span + || !matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + { + return Err("call ownership absent edge lost its exact runtime SumTag".into()); + } + let [definition] = self + .definitions + .get(&plan.tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err("call ownership absent edge has an ambiguous tag definition".into()); + }; + if definition.site + != (Site { + block: plan.selecting, + statement: block.statements.len() - 1, + }) + { + return Err("call ownership absent tag belongs to another selecting block".into()); + } + self.producer_source(plan.source, plan.source_type, plan.span, definition.site) + } + + fn prepared_absence(&self, plan: &AbsentSuccessPlan) -> Result<(), String> { + self.absence_tag(plan)?; + if !matches!(self.function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.failure) + { + return Err( + "call ownership prepared absence changed its canonical selected edge".into(), + ); + } + let definitions = self + .definitions + .get(&plan.output.index()) + .ok_or("call ownership prepared absent output has no defaults")?; + for defined in definitions { + let Definition::Let(value) = defined.kind else { + return Err( + "call ownership prepared absent output has a foreign definition".into(), + ); + }; + if value.ty != plan.output_type + || matches!(value.kind, E::View(_)) + || !contains(plan.span, defined.span) + || !contains(defined.span, value.span) + { + return Err( + "call ownership prepared absent default changes type or occurrence".into(), + ); + } + if let E::Local(local) = value.kind { + if self.function.is_view_local(local) { + return Err( + "call ownership prepared absent default acquires a borrowed owner".into(), + ); + } + } + self.sum_failure_region(plan.source, defined.site.block, plan.span)?; + } + Ok(()) + } + + fn validate_prepared_absences(&self) -> Result<(), String> { + let mut outputs = BTreeSet::new(); + for record in &self.function.prepared_absent_successes { + if !outputs.insert(record.plan().output.index()) { + return Err( + "call ownership prepared absence has duplicate output authority".into(), + ); + } + self.prepared_absence(record.plan())?; + } + Ok(()) + } + + /// A default definition belongs to the failed arm of this exact Handle, + /// not merely to a source span somewhere inside the same function. + fn sum_failure_region( + &self, + source: LocalId, + start: BlockId, + span: Span, + ) -> Result<(), String> { + let mut queue = vec![start]; + let mut seen = BTreeSet::new(); + let mut anchored = false; + while let Some(current) = queue.pop() { + if !seen.insert(current.index()) { + continue; + } + let incoming = self + .predecessors + .get(current.index() as usize) + .ok_or("call ownership handled default block is absent")?; + if current == self.function.entry || incoming.is_empty() { + return Err( + "call ownership handled default has no exact failure-edge anchor".into(), + ); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership handled default predecessor is absent")?; + let mut selected = false; + if let Some(record) = + self.function + .prepared_absent_successes + .iter() + .find(|record| { + let plan = record.plan(); + plan.source == source + && plan.span == span + && plan.selecting == previous + && plan.failure == current + }) + { + self.absence_tag(record.plan())?; + if !matches!(block.terminator.kind, T::Goto(target) if target == current) { + return Err( + "call ownership prepared default changed its authenticated false edge" + .into(), + ); + } + selected = true; + anchored = true; + } + if let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + { + if let E::Local(tag) = condition.kind { + if let [defined] = self + .definitions + .get(&tag.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + { + let statement = self + .function + .blocks + .get(defined.site.block.index() as usize) + .and_then(|block| block.statements.get(defined.site.statement)); + if statement.is_some_and(|statement| matches!(statement.kind, + S::SumTag { source: owner, target } if owner == source && target == tag)) { + if condition.ty != TypeInterner::BOOL + || self.function.local(tag).is_none_or(|local| local.ty != TypeInterner::BOOL) + || block.terminator.span != span || defined.span != span + || *else_block != current || *then_block == current { + return Err("call ownership handled default is not selected by its exact false-tag edge".into()); + } + self.dominates(defined.site, Site { block: previous, statement: block.statements.len() })?; + selected = true; + anchored = true; + } + } + } + } + if !selected { + queue.push(previous); + } + } + } + if !anchored { + return Err( + "call ownership handled default cycle has no exact failure-edge anchor".into(), + ); + } + Ok(()) + } + + fn generated_operand( + &mut self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + used: Site, + ) -> Result<(), String> { + let local_info = self.locals_at(used); + match argument.staging { + hir::GeneratedArgumentStaging::Existing { loan: None } => { + if hir::validate_generated_operand_tree(value, argument, &local_info).is_ok() { + return Ok(()); + } + // A physical owned operand can have been materialized by the + // ordinary ordered-value path. Its unique Let must restore the + // sealed original tree, including primitive Copy acquisitions. + if argument.callee_access != jett_typecheck::CheckedCalleeAccess::Owned { + return Err( + "generated borrowed operand lost its explicit storage association".into(), + ); + } + let E::Local(local) = value.kind else { + return self.generated_original(value, argument, used); + }; + self.slot_operand(value, local, false)?; + let (initializer, defined) = self.owning_let(local, value.ty, used)?; + if let Err(error) = self.generated_original(initializer, argument, defined.site) { + let E::Clone(original) = &initializer.kind else { + return Err(error); + }; + if argument.acquisition != hir::GeneratedAcquisition::Copy + || original.ty != initializer.ty + || !jett_typecheck::ownership::is_implicitly_copyable( + self.types, + argument.actual_type, + ) + { + return Err(error); + } + self.generated_original(original, argument, defined.site)?; + } + self.claim_slot(local) + } + hir::GeneratedArgumentStaging::Existing { loan: Some(loan) } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, used)?; + self.generated_original(projection, argument, begun.site)?; + self.claim_slot(loan) + } + hir::GeneratedArgumentStaging::OwnedProducer { owner, loan } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => { + self.slot_operand(value, loan, true)?; + let (projection, begun) = self.loan(loan, value.ty, used)?; + self.slot_operand(projection, owner, true)?; + if self + .function + .local(loan) + .and_then(|local| local.view_source) + != Some(owner) + { + return Err("generated loan does not borrow its exact acquired owner".into()); + } + let (initializer, defined) = self.owning_let(owner, value.ty, begun.site)?; + match argument.staging { + hir::GeneratedArgumentStaging::OwnedProducer { .. } => { + if !argument.original_witness().owned_producer() + || matches!(initializer.kind, E::View(_)) + { + return Err("generated owner lost its sealed full producer".into()); + } + let restored = crate::call_views::replace_borrowed_root( + projection, + initializer.clone(), + ) + .ok_or("generated owner changed its original endpoint type")?; + self.generated_original(&restored, argument, defined.site)?; + } + hir::GeneratedArgumentStaging::OrdinarySnapshot { .. } => { + if argument + .original_witness() + .ordinary_snapshot_proof() + .map(|proof| proof.actual_type()) + != Some(argument.actual_type) + { + return Err( + "generated snapshot lost its closed ordinary-data proof".into() + ); + } + let E::Clone(observed) = &initializer.kind else { + return Err( + "generated snapshot requires its exact explicit Clone initializer" + .into(), + ); + }; + if observed.ty != initializer.ty || initializer.ty != argument.actual_type { + return Err( + "generated snapshot changed its sealed endpoint type".into() + ); + } + self.generated_original(observed, argument, defined.site)?; + } + _ => unreachable!("matched generated owner stage"), + } + self.claim_slot(owner)?; + self.claim_slot(loan) + } + } + } + + fn generated_original( + &self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + used: Site, + ) -> Result<(), String> { + let restored = self.restore_generated_internal_copy(value, argument); + let value = restored.as_ref().unwrap_or(value); + let local_info = self.locals_at(used); + let error = match hir::validate_generated_operand_tree(value, argument, &local_info) { + Ok(()) => return Ok(()), + Err(error) => error, + }; + let witness = argument.original_witness(); + if witness.origin() != hir::CallerViewSource::Other + || value.ty != witness.actual_type() + || value.span != witness.source_span() + { + return Err(error); + } + let mut original = value; + if !witness.written_view() + && let E::View(inner) = &original.kind + && original.ty == inner.ty + && original.span == inner.span + { + original = inner; + } + // A private original node can be restored through unique owning storage; + // only its sealed Handle variant authorizes the selected CFG result proof. + // Every reconstructed root/target/wrapper/backing remains exact, including + // original explicit Clone versus an introduced Clone. + witness.validate_lowered_producer_shape(original, |value, shape| { + self.generated_storage(value, shape, used) + }) + } + + fn generated_storage( + &self, + value: &Expression, + shape: hir::GeneratedOriginalShape<'_>, + used: Site, + ) -> Result<(), String> { + self.generated_storage_inner(value, shape, used, &mut BTreeSet::new()) + } + + fn generated_storage_inner( + &self, + value: &Expression, + shape: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + if value.ty != shape.ty() || value.span != shape.span() { + return Err( + "call ownership generated storage changes its sealed original occurrence".into(), + ); + } + let E::Local(local) = value.kind else { + return Err("call ownership generated storage has no exact materialized slot".into()); + }; + if !aliases.insert(local.index()) { + return Err("call ownership generated storage contains a cycle".into()); + } + let result = (|| { + if let Some(handle) = shape.handle() { + return self.generated_handled_result(value, handle, shape, used, aliases); + } + let (initializer, defined) = self.owning_let(local, shape.ty(), used)?; + match shape.validate_reconstructed(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) { + Ok(()) => Ok(()), + Err(error) => shape + .validate_existing_local_snapshot(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + .map_err(|_| error), + } + })(); + aliases.remove(&local.index()); + result + } + + fn generated_handled_result( + &self, + value: &Expression, + shape: hir::GeneratedHandleShape<'_>, + original_node: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + if matches!(shape.kind(), hir::HandleKind::Refinement { .. }) { + return self.generated_refinement_result(value, shape, original_node, used, aliases); + } + if !matches!( + shape.kind(), + hir::HandleKind::Optional | hir::HandleKind::Result + ) || value.ty != shape.result_type() + || value.span != shape.span() + { + return Err( + "call ownership generated Handle has no exact selected sum occurrence".into(), + ); + } + let E::Local(local) = value.kind else { + return Err( + "call ownership generated Handle is not an authenticated CFG result".into(), + ); + }; + let definitions = self + .definitions + .get(&local.index()) + .ok_or("call ownership generated Handle result is undefined")?; + let prepared = self + .function + .prepared_absent_successes + .iter() + .find(|record| record.plan().output == local); + if prepared.is_none() + && let [defined] = definitions.as_slice() + && let Definition::Let(initializer) = defined.kind + && matches!(initializer.kind, E::Local(_)) + { + // Ordered constructor fields can add a unique owning slot around + // an already lowered result. Rejoin only the same exact occurrence; + // no binding, View, Clone or arbitrary producer becomes a Handle. + let (initializer, defined) = self.owning_let(local, shape.result_type(), used)?; + if initializer.span != shape.span() { + return Err( + "call ownership generated Handle storage changes its occurrence".into(), + ); + } + return self.generated_storage_inner(initializer, original_node, defined.site, aliases); + } + self.produced_result(local, shape.result_type(), shape.span(), used)?; + let sources = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::SumTake { + source, + success: true, + } if defined.span == shape.span() => Some(source), + _ => None, + }) + .collect::>(); + let source = match (sources.as_slice(), prepared) { + ([source], None) => *source, + ([], Some(record)) => record.plan().source, + _ => return Err("call ownership generated Handle lost its exact source anchor".into()), + }; + let source_type = self + .function + .local(source) + .ok_or("call ownership generated Handle source is absent")? + .ty; + if source_type != shape.source_type() + || !valid_type(self.types, source_type) + || !matches!( + (shape.kind(), self.types.resolve(source_type)), + (hir::HandleKind::Optional, Type::Optional(_)) + | (hir::HandleKind::Result, Type::Result(_, _)) + ) + { + return Err( + "call ownership generated Handle source differs from its sealed sum type".into(), + ); + } + self.producer_source(source, source_type, shape.span(), used)?; + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated Handle requires one exact source initializer".into(), + ); + }; + let Definition::Let(initializer) = defined.kind else { + return Err( + "call ownership generated Handle source is not its owning initializer".into(), + ); + }; + shape.validate_lowered_source_shape(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + } + + fn generated_refinement_result( + &self, + value: &Expression, + shape: hir::GeneratedHandleShape<'_>, + original_node: hir::GeneratedOriginalShape<'_>, + used: Site, + aliases: &mut BTreeSet, + ) -> Result<(), String> { + let hir::HandleKind::Refinement { + refined_type, + predicates, + } = shape.kind() + else { + return Err("call ownership generated refinement lost its immutable kind".into()); + }; + if value.ty != shape.result_type() + || value.span != shape.span() + || *refined_type != shape.result_type() + || predicates.is_empty() + || predicates + .last() + .is_none_or(|predicate| predicate.refined_type != *refined_type) + || !valid_type(self.types, *refined_type) + || !matches!(self.types.resolve(*refined_type), Type::Refinement { .. }) + { + return Err( + "call ownership generated refinement differs from its exact nominal occurrence" + .into(), + ); + } + let E::Local(output) = value.kind else { + return Err( + "call ownership generated refinement has no authenticated output slot".into(), + ); + }; + let definitions = self + .definitions + .get(&output.index()) + .ok_or("call ownership generated refinement output is undefined")?; + if let [defined] = definitions.as_slice() + && let Definition::Let(initializer) = defined.kind + && matches!(initializer.kind, E::Local(_)) + { + let (initializer, defined) = self.owning_let(output, value.ty, used)?; + if initializer.span != shape.span() { + return Err( + "call ownership generated refinement storage changes its occurrence".into(), + ); + } + return self.generated_storage_inner(initializer, original_node, defined.site, aliases); + } + let metadata = self + .function + .local(output) + .ok_or("call ownership generated refinement output is absent")?; + if metadata.ty != value.ty + || metadata.debug_ty != value.ty + || metadata.span != shape.span() + || metadata.view_source.is_some() + || self.function.parameter_for_local(output).is_some() + { + return Err( + "call ownership generated refinement output has no exact owning metadata".into(), + ); + } + let successes = definitions + .iter() + .filter_map(|defined| match defined.kind { + Definition::Let(Expression { + kind: E::RefinementValidated(inner), + ty, + span, + }) if *ty == value.ty && *span == shape.span() && defined.span == shape.span() => { + Some((*defined, inner.as_ref())) + } + _ => None, + }) + .collect::>(); + let [(success, candidate)] = successes.as_slice() else { + return Err( + "call ownership generated refinement requires one exact validated output".into(), + ); + }; + let E::Local(source) = candidate.kind else { + return Err( + "call ownership generated refinement validation has no exact candidate".into(), + ); + }; + if candidate.ty != shape.source_type() || candidate.span != shape.span() { + return Err( + "call ownership generated refinement candidate differs from its immutable source" + .into(), + ); + } + self.producer_source(source, candidate.ty, shape.span(), success.site)?; + let success_block = &self.function.blocks[success.site.block.index() as usize]; + if success.site.statement != 0 + || success_block.statements.len() != 1 + || success_block.terminator.span != shape.span() + || !matches!(success_block.terminator.kind, T::Goto(_)) + { + return Err( + "call ownership generated refinement changed its canonical success block".into(), + ); + } + let mut next = success.site.block; + let mut rejected = BTreeSet::new(); + let mut selecting = BTreeSet::new(); + let mut failed = None; + for predicate in predicates.iter().rev() { + let (previous, refusal, failure) = + self.refinement_selection(next, source, shape, predicate)?; + if !selecting.insert(previous.index()) + || !rejected.insert(refusal.index()) + || failed.is_some_and(|expected| expected != failure) + { + return Err("call ownership generated refinement changed its ordered checks or shared failure".into()); + } + failed = Some(failure); + next = previous; + } + let failed = failed.ok_or("call ownership generated refinement has no selected failure")?; + let failure_block = self + .function + .blocks + .get(failed.index() as usize) + .ok_or("call ownership generated refinement failure block is absent")?; + let incoming = self + .predecessors + .get(failed.index() as usize) + .ok_or("call ownership generated refinement failure predecessors are absent")?; + if incoming + .iter() + .map(|block| block.index()) + .collect::>() + != rejected + || incoming.len() != rejected.len() + || failed == success.site.block + || failure_block + .statements + .iter() + .any(|statement| !contains(shape.failure_span(), statement.span)) + || (!contains(shape.failure_span(), failure_block.terminator.span) + && failure_block.terminator.span != shape.span()) + { + return Err( + "call ownership generated refinement failure has a foreign edge or occurrence" + .into(), + ); + } + for defined in definitions { + if defined.site == success.site { + continue; + } + let Definition::Let(default) = defined.kind else { + return Err( + "call ownership generated refinement output has a foreign definition".into(), + ); + }; + if default.ty != value.ty + || !contains(shape.failure_span(), defined.span) + || !contains(defined.span, default.span) + || self.handled_default_is_borrowed(default, defined.site) + { + return Err("call ownership generated refinement default lost its exact owned failure occurrence".into()); + } + self.refinement_failure_region(failed, defined.site.block)?; + } + self.definitions_reach(definitions, used)?; + let [defined] = self + .definitions + .get(&source.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement requires one owning source initializer".into(), + ); + }; + let Definition::Let(initializer) = defined.kind else { + return Err( + "call ownership generated refinement source is not its owning initializer".into(), + ); + }; + shape.validate_lowered_source_shape(initializer, |value, nested| { + self.generated_storage_inner(value, nested, defined.site, aliases) + }) + } + + fn refinement_selection( + &self, + success: BlockId, + source: LocalId, + shape: hir::GeneratedHandleShape<'_>, + predicate: &hir::RefinementPredicate, + ) -> Result<(BlockId, BlockId, BlockId), String> { + let [previous] = self + .predecessors + .get(success.index() as usize) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement success has a bypass or foreign predecessor" + .into(), + ); + }; + let block = &self.function.blocks[previous.index() as usize]; + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + return Err( + "call ownership generated refinement requires its original predicate Branch".into(), + ); + }; + if *then_block != success + || *else_block == success + || block.terminator.span != shape.span() + || condition.ty != TypeInterner::BOOL + || condition.span != shape.span() + { + return Err("call ownership generated refinement changed its selecting edge".into()); + } + let E::Local(passed) = condition.kind else { + return Err( + "call ownership generated refinement requires its exact passed local".into(), + ); + }; + let (test, defined) = self.owning_let( + passed, + TypeInterner::BOOL, + Site { + block: *previous, + statement: block.statements.len(), + }, + )?; + if defined.site.block != *previous + || defined.site.statement + 1 != block.statements.len() + || defined.site.statement == 0 + { + return Err( + "call ownership generated refinement passed test is not after its exact check" + .into(), + ); + } + let S::CheckRefinement { + local: error, + call, + type_name, + } = &block.statements[defined.site.statement - 1].kind + else { + return Err( + "call ownership generated refinement lost its exact predicate check".into(), + ); + }; + let checked_site = Site { + block: *previous, + statement: defined.site.statement - 1, + }; + let checked = &block.statements[checked_site.statement]; + let [error_definition] = self + .definitions + .get(&error.index()) + .map(Vec::as_slice) + .unwrap_or(&[]) + else { + return Err( + "call ownership generated refinement error has a foreign definition".into(), + ); + }; + let error_metadata = self + .function + .local(*error) + .ok_or("call ownership generated refinement error is absent")?; + if error_definition.site != checked_site + || error_definition.span != shape.span() + || error_metadata.ty != TypeInterner::STRING + || error_metadata.debug_ty != TypeInterner::STRING + || error_metadata.span != shape.span() + || error_metadata.view_source.is_some() + || self.function.parameter_for_local(*error).is_some() + || checked.span != shape.span() + || type_name != &predicate.type_name + || call.ty != TypeInterner::BOOL + || call.span != shape.span() + { + return Err( + "call ownership generated refinement changed its exact predicate occurrence".into(), + ); + } + let E::Call { + function, + args, + evaluation_order, + ownership: hir::CallOwnership::Generated(packet), + } = &call.kind + else { + return Err( + "call ownership generated refinement check has no exact generated target".into(), + ); + }; + if *function != predicate.function + || args.len() != 1 + || evaluation_order != &[0] + || packet.operation + != (hir::GeneratedOperation::RefinementPredicate { + function: predicate.function, + }) + { + return Err( + "call ownership generated refinement check differs from its immutable predicate" + .into(), + ); + } + let span = shape.span(); + let mut input = Expression { + kind: E::Clone(Box::new(Expression { + kind: E::Local(source), + ty: shape.source_type(), + span, + })), + ty: shape.source_type(), + span, + }; + if input.ty != predicate.base_type && input.ty != predicate.input_type { + input = Expression { + kind: E::Coarsen(Box::new(input)), + ty: predicate.base_type, + span, + }; + } + if input.ty != predicate.input_type { + input = Expression { + kind: E::Declassify(Box::new(input)), + ty: predicate.input_type, + span, + }; + } + if args[0] != input + || test.ty != TypeInterner::BOOL + || test.span != span + || defined.span != span + { + return Err( + "call ownership generated refinement changed its exact candidate input".into(), + ); + } + let expected = Expression { + kind: E::Binary { + left: Box::new(Expression { + kind: E::View(Box::new(Expression { + kind: E::Local(*error), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::STRING, + span, + }), + op: hir::BinaryOp::Equal, + right: Box::new(Expression { + kind: E::String(String::new()), + ty: TypeInterner::STRING, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }; + if *test != expected { + return Err( + "call ownership generated refinement changed its exact empty-error test".into(), + ); + } + self.producer_source(source, shape.source_type(), span, checked_site)?; + let refusal = &self.function.blocks[else_block.index() as usize]; + let T::Goto(failed) = refusal.terminator.kind else { + return Err( + "call ownership generated refinement rejection does not enter its failure body" + .into(), + ); + }; + if refusal.terminator.span != span + || self.predecessors[else_block.index() as usize].as_slice() != [*previous] + { + return Err("call ownership generated refinement rejection has a foreign edge".into()); + } + if let Some(local) = shape.error_local() { + let metadata = self + .function + .local(local) + .ok_or("call ownership generated refinement error binding is absent")?; + if metadata.ty != TypeInterner::STRING + || metadata.debug_ty != TypeInterner::STRING + || metadata.view_source.is_some() + || self.function.parameter_for_local(local).is_some() + { + return Err( + "call ownership generated refinement error binding has invalid owning metadata" + .into(), + ); + } + } + match (shape.error_local(), refusal.statements.as_slice()) { + (None, []) => {} + (Some(local), [statement]) + if statement.span == span + && matches!(&statement.kind, S::Let { local: target, value } if *target == local + && value.ty == TypeInterner::STRING && value.span == span + && matches!(value.kind, E::Local(source) if source == *error)) => {} + _ => { + return Err( + "call ownership generated refinement lost its exact error binding".into(), + ); + } + } + Ok((*previous, *else_block, failed)) + } + + fn handled_default_is_borrowed(&self, value: &Expression, site: Site) -> bool { + let endpoint = value.ty; + let mut backing = value; + loop { + backing = match &backing.kind { + E::Coarsen(inner) | E::Declassify(inner) => inner, + E::View(_) | E::RefinementValidated(_) => return true, + E::Local(local) => { + if jett_typecheck::ownership::is_implicitly_copyable(self.types, endpoint) { + return false; + } + return self.locals_at(site).get(local.index() as usize).is_none_or( + |metadata| { + metadata.view_source.is_some() + || metadata.is_view_parameter + || metadata.view_iteration.is_some() + }, + ); + } + _ => return false, + }; + } + } + + fn refinement_failure_region(&self, failure: BlockId, start: BlockId) -> Result<(), String> { + let mut queue = vec![start]; + let mut seen = BTreeSet::new(); + let mut anchored = false; + while let Some(current) = queue.pop() { + if current == failure { + anchored = true; + continue; + } + if !seen.insert(current.index()) { + continue; + } + let incoming = self + .predecessors + .get(current.index() as usize) + .ok_or("call ownership generated refinement default block is absent")?; + if current == self.function.entry || incoming.is_empty() { + return Err( + "call ownership generated refinement default has a failure-region bypass" + .into(), + ); + } + queue.extend(incoming.iter().copied()); + } + if !anchored { + return Err( + "call ownership generated refinement default has no exact failure anchor".into(), + ); + } + Ok(()) + } + + fn restore_generated_internal_copy( + &self, + value: &Expression, + argument: &hir::GeneratedArgumentOwnership, + ) -> Option { + let hir::CallerViewSource::Local(source) = argument.original_witness().origin() else { + return None; + }; + argument.original_witness().ordinary_snapshot_proof()?; + // Existing Coarsen/Declassify lowering can snapshot a source Local. + // Restore only that exact transparent path for metadata validation; + // the emitted Clone and qualified runtime tree remain unchanged. + fn restore(value: &mut Expression, source: LocalId) -> bool { + let replacement = match &value.kind { + E::Clone(inner) + if value.ty == inner.ty + && value.span == inner.span + && matches!(inner.kind, E::Local(local) if local == source) => + { + Some(inner.as_ref().clone()) + } + _ => None, + }; + if let Some(replacement) = replacement { + *value = replacement; + return true; + } + match &mut value.kind { + E::View(inner) | E::Coarsen(inner) | E::Declassify(inner) => restore(inner, source), + _ => false, + } + } + let mut original = value.clone(); + if !restore(&mut original, source) { + return None; + } + let root = self.function.local(source)?; + let proof = Expression { + kind: E::View(Box::new(original.clone())), + ty: original.ty, + span: original.span, + }; + hir::validate_local_view_initializer(&proof, source, root.ty, original.ty, self.types) + .ok()?; + Some(original) + } + fn dominates(&self, definition: Site, used: Site) -> Result<(), String> { + self.definitions_reach( + &[Defined { + site: definition, + span: self.function.span, + kind: Definition::Other, + }], + used, + ) + } + + /// Check every backward path, retaining disconnected-block obligations. + /// A cycle with no defining predecessor cannot manufacture initialization. + fn definitions_reach(&self, definitions: &[Defined<'_>], used: Site) -> Result<(), String> { + let mut queue = vec![used]; + let mut seen = BTreeSet::new(); + let mut reached = false; + while let Some(site) = queue.pop() { + if definitions.iter().any(|definition| { + definition.site.block == site.block && definition.site.statement < site.statement + }) { + reached = true; + continue; + } + if !seen.insert(site.block.index()) { + continue; + } + let incoming = self + .predecessors + .get(site.block.index() as usize) + .ok_or("call ownership use block is absent")?; + if site.block == self.function.entry || incoming.is_empty() { + return Err( + "call ownership storage use is not preceded by its initializer on every path" + .into(), + ); + } + for &previous in incoming { + let block = self + .function + .blocks + .get(previous.index() as usize) + .ok_or("call ownership predecessor is absent")?; + queue.push(Site { + block: previous, + statement: block.statements.len(), + }); + } + } + if !reached { + return Err("call ownership storage cycle has no initialized producer".into()); + } + Ok(()) + } + + fn statement( + &mut self, + statement: &'a S, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + match statement { + S::ResourceCall(ResourceCallNode::Stage { value, .. }) + | S::ResourceConstructor(ResourceConstructorNode::Stage { value, .. }) + | S::Let { value, .. } + | S::BeginCallView { value, .. } + | S::Evaluate(value) + | S::HandleDefault(value) => self.expression(value, site, context)?, + S::CheckRefinement { call, .. } => self.expression(call, site, context)?, + S::Assign { target, value } | S::UpdateField { target, value, .. } => { + self.expression(target, site, context)?; + self.expression(value, site, context)?; + } + S::Assert { condition, message } => { + self.expression(condition, site, context)?; + if let Some(message) = message { + self.expression(message, site, context)?; + } + } + S::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + self.expression( + condition, + site, + CheckedOwnershipContext::BreakpointExpression, + )?; + } + } + _ => {} + } + Ok(()) + } + + fn terminator( + &mut self, + terminator: &'a T, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + match terminator { + T::Return(Some(value)) | T::Respond(value) => self.expression(value, site, context)?, + T::Branch { condition, .. } => self.expression(condition, site, context)?, + T::Switch { scrutinee, .. } => self.expression(scrutinee, site, context)?, + T::ForEach { iterable, .. } => self.expression(iterable, site, context)?, + T::ReflectedTypeDispatch { type_info, .. } => { + self.expression(type_info, site, context)? + } + _ => {} + } + Ok(()) + } + + fn expression( + &mut self, + expression: &'a Expression, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + self.manifest.validate_type(self.types, expression.ty)?; + if let E::ResourceHookValue { hook } = &expression.kind { + if !self.manifest.contains_hook(hook) || expression.ty != hook.function_type() { + return Err( + "Resource descriptor differs from its original checked manifest or signature" + .into(), + ); + } + } + self.invocation(expression, site, context)?; + match &expression.kind { + E::ActorGraphImport(_) => { + return Err( + "actor graph import was not expanded before MIR call ownership validation" + .into(), + ); + } + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + self.expression(value, site, context)?; + } + } + E::IndirectCall { callee, args, .. } => { + self.expression(callee, site, context)?; + for value in args { + self.expression(value, site, context)?; + } + } + E::Binary { left, right, .. } => { + self.expression(left, site, context)?; + self.expression(right, site, context)?; + } + E::Unary { value, .. } + | E::View(value) + | E::Clone(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::RuntimeFailureMessage(value) + | E::StateIs { value, .. } + | E::Comptime { value, .. } + | E::FunctionAdapter { value, .. } + | E::InterfaceCoerce { value, .. } => self.expression(value, site, context)?, + E::Field { base, .. } => self.expression(base, site, context)?, + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + self.expression(value, site, context)?; + } + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + for value in payloads { + self.expression(value, site, context)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + self.expression(source, site, context)?; + for value in payloads { + self.expression(value, site, context)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + self.expression(value, site, context)?; + } + } + E::MapConstruct { entries } => { + for value in entries { + self.expression(&value.key, site, context)?; + self.expression(&value.value, site, context)?; + } + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.expression(value, site, context)?; + } + } + } + E::Handle { + target, failure, .. + } => { + self.expression(target, site, context)?; + self.hir_block(failure, site, context)?; + } + E::InlineFunction { body, .. } => { + let saved = std::mem::replace(&mut self.iteration_scope_enabled, false); + let result = self.hir_block(body, site, CheckedOwnershipContext::Ordinary); + self.iteration_scope_enabled = saved; + result?; + } + E::ActorMessage { + actor, args, proof, .. + } => { + proof.validate( + self.signatures, + expression, + &self.locals_at(site), + self.types, + context, + false, + )?; + self.actor_receiver(actor, proof.receiver_proof(), site)?; + for (index, value) in args.iter().enumerate() { + let argument = proof + .argument(index) + .ok_or("actor message lost checked argument")?; + if self + .original(value, argument, &hir::CallBridge::Direct, site) + .is_err() + { + let E::Local(local) = value.kind else { + return Err("actor message operand changed its exact original Source association".into()); + }; + let (initializer, defined) = + self.storage_let(local, value.ty, site, true)?; + self.original( + initializer, + argument, + &hir::CallBridge::Direct, + defined.site, + )?; + } + } + self.expression(actor, site, context)?; + for value in args { + self.expression(value, site, context)?; + } + } + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::ResourceHookValue { .. } + | E::FunctionRef(_) + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) + } + + fn hir_block( + &mut self, + block: &'a hir::Block, + site: Site, + context: CheckedOwnershipContext, + ) -> Result<(), String> { + use hir::StatementKind as H; + for statement in &block.statements { + match &statement.kind { + H::Let { value, .. } + | H::Expression(value) + | H::HandleDefault(value) + | H::Respond(value) => self.expression(value, site, context)?, + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + self.expression(target, site, context)?; + self.expression(value, site, context)?; + } + H::Return(Some(value)) => self.expression(value, site, context)?, + H::If { + condition, + then_block, + else_block, + } => { + self.expression(condition, site, context)?; + self.hir_block(then_block, site, context)?; + if let Some(block) = else_block { + self.hir_block(block, site, context)?; + } + } + H::While { condition, body } => { + self.expression(condition, site, context)?; + self.hir_block(body, site, context)?; + } + H::For { iterable, body, .. } => { + self.expression(iterable, site, context)?; + self.hir_block(body, site, context)?; + } + H::Match { scrutinee, arms } => { + self.expression(scrutinee, site, context)?; + for arm in arms { + self.hir_block(&arm.body, site, context)?; + } + } + H::Assert { condition, message } => { + self.expression(condition, site, context)?; + if let Some(value) = message { + self.expression(value, site, context)?; + } + } + H::Breakpoint { condition, .. } => { + if let Some(value) = condition { + self.expression( + value, + site, + CheckedOwnershipContext::BreakpointExpression, + )?; + } + } + H::Scope(block) => self.hir_block(block, site, context)?, + H::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info, site, context)?; + for arm in arms { + self.hir_block(&arm.body, site, context)?; + } + } + H::Return(None) | H::Break | H::Continue | H::Trace(_) => {} + } + } + Ok(()) + } +} + +fn consumes(effect: CheckedCallerEffect) -> bool { + matches!( + effect, + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned + ) +} + +fn valid_type(types: &TypeInterner, ty: TypeId) -> bool { + (ty.index() as usize) < types.len() && ty != TypeInterner::ERROR +} + +fn contains(outer: Span, inner: Span) -> bool { + outer.file == inner.file && outer.start <= inner.start && inner.end <= outer.end +} + +fn address_root(mut expression: &Expression) -> Option { + loop { + expression = match &expression.kind { + E::Local(local) => return Some(*local), + E::Field { base, .. } => base, + E::View(value) + | E::Coarsen(value) + | E::Declassify(value) + | E::RefinementValidated(value) => value, + E::InterfaceCoerce { value, adapters } if adapters.is_empty() => value, + _ => return None, + }; + } +} + +fn successors(terminator: &T) -> Vec { + match terminator { + T::Goto(target) => vec![*target], + T::Branch { + then_block, + else_block, + .. + } => vec![*then_block, *else_block], + T::Switch { + variants, + otherwise, + .. + } => variants + .iter() + .map(|(_, target, _)| *target) + .chain(otherwise.iter().copied()) + .collect(), + T::ForEach { body, exit, .. } => vec![*body, *exit], + T::ReflectedTypeDispatch { + arms, otherwise, .. + } => arms + .iter() + .map(|arm| arm.target) + .chain(std::iter::once(*otherwise)) + .collect(), + T::Return(_) | T::Respond(_) | T::Unreachable => Vec::new(), + } +} diff --git a/crates/jett_mir/src/call_ownership/tests.rs b/crates/jett_mir/src/call_ownership/tests.rs new file mode 100644 index 00000000..54b7eb79 --- /dev/null +++ b/crates/jett_mir/src/call_ownership/tests.rs @@ -0,0 +1,5817 @@ +use super::*; +use std::collections::HashMap; + +const SIGNATURE_REUSE_SOURCE: &str = r#"namespace app +function accept(value: int64) returns int64: + return 7 +function first() returns int64: + return accept(1) +function second() returns int64: + return accept(2) +"#; + +#[test] +fn whole_ownership_pass_rejoins_fresh_signatures_and_preserves_error_order() { + let (mut program, types) = source_program(SIGNATURE_REUSE_SOURCE); + crate::validate_call_ownership(&program, &types).expect("original complete ownership pass"); + for function in &program.functions { + validate_function(&program, function, &types).expect("public per-function ownership pass"); + } + + // Keep structural parameter/local agreement while invalidating the exact + // callee signature retained by both already-checked source call packets. + let callee = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "accept") + .expect("declared accept"); + let parameter = &mut callee.params[0]; + parameter.ty = TypeInterner::INT32; + let local = &mut callee.locals[parameter.local.index() as usize]; + local.ty = TypeInterner::INT32; + local.debug_ty = TypeInterner::INT32; + crate::validate(&program).expect("signature mutation remains structurally well formed"); + + let expected = program + .functions + .iter() + .filter_map(|function| { + validate_function(&program, function, &types) + .err() + .map(|message| crate::ValidationError { + span: function.span, + message, + }) + }) + .collect::>(); + assert_eq!( + expected.len(), + 2, + "both unused caller functions retain exact signature obligations" + ); + assert!( + expected.iter().all(|error| error.message + == "source function ownership signature differs from the exact HIR declaration"), + "{expected:?}" + ); + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("a new whole pass cannot reuse the preceding callee signature"); + assert_eq!( + errors, expected, + "complete pass retains per-function diagnostics and declaration order" + ); +} + +#[test] +fn whole_ownership_pass_checks_a_late_disconnected_invocation() { + let (mut program, types) = source_program(SIGNATURE_REUSE_SOURCE); + crate::validate_call_ownership(&program, &types).expect("original complete ownership pass"); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "second") + .expect("last caller"); + let mut value = read_call(function).clone(); + value.ty = TypeInterner::BOOL; + let id = BlockId(function.blocks.len() as u32); + function.blocks.push(crate::BasicBlock { + id, + statements: vec![crate::Statement { + span: value.span, + kind: S::Evaluate(value), + }], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + let span = function.span; + crate::validate(&program) + .expect("disconnected expression metadata is structurally well formed"); + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("table reuse cannot skip the unused caller's disconnected block"); + assert_eq!( + errors, + vec![crate::ValidationError { + span, + message: "call ownership result differs from its checked source certificate".into(), + }] + ); +} + +const SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64], other: list[int64], incoming: optional[int64]) returns int64: + return read(values, incoming handle: + default 3 + ) +"#; + +fn source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("source HIR"); + ( + crate::lower(&high, &checked.interner).expect("source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "exercise") + .expect("exercise") +} + +fn read_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + .or_else(|| { + function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::Return(Some(value)) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .expect("read invocation") +} + +fn read_call_mut(function: &mut Function) -> &mut Expression { + // The handled source fixture materializes its actual call as an owning Let. + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::Call { .. } | E::IndirectCall { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("materialized read invocation") +} + +fn owning_stage(function: &Function) -> (LocalId, LocalId) { + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("source read"); + }; + let hir::ArgumentStaging::Relinquished { owner, loan } = source.arguments[0].staging else { + panic!("relinquished stage"); + }; + (owner, loan) +} + +fn rejects(program: &Program, types: &TypeInterner) { + let error = validate_function(program, exercise(program), types) + .expect_err("malformed acquisition must fail"); + assert!( + error.contains("call ownership") || error.contains("call view"), + "{error}" + ); +} + +#[test] +fn caller_acquisition_uses_original_owner_and_exact_handled_copy_result() { + for retained in [false, true] { + let source = if retained { + SOURCE.replace("return read(values,", "return read(view values,") + } else { + SOURCE.to_owned() + }; + let (program, types) = source_program(&source); + let function = exercise(&program); + let acquisitions = + validate_function(&program, function, &types).expect("valid call acquisition"); + let call = read_call(function); + let arguments = acquisitions + .arguments(call) + .expect("validated call occurrence"); + assert!( + arguments.is_empty(), + "staged owner must be consumed at its initializer" + ); + if retained { + assert_eq!(acquisitions.owner_initializers().count(), 0); + } else { + let (owner, loan) = owning_stage(function); + let acquired = acquisitions + .owner_initializer(owner) + .expect("owning initializer"); + assert_eq!(acquired.binding, Some(function.params[0].local)); + assert_ne!(acquired.binding, Some(owner)); + assert_ne!(acquired.binding, Some(loan)); + assert_eq!(acquired.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!(acquired.actual_type, function.params[0].ty); + } + } +} + +#[test] +fn caller_acquisition_original_owned_input_keeps_formal_and_source_indices() { + let source = r#"namespace app +function take(values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64]) returns int64: + return take(extra: 3, values: values) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let call = read_call(function); + let acquisitions = validate_function(&program, function, &types).expect("original owned call"); + let [argument] = acquisitions.arguments(call).expect("typed acquisition") else { + panic!("one owner"); + }; + assert_eq!(argument.parameter_index, 0); + assert_eq!(argument.source_index, 1); + assert_eq!(argument.source.binding, Some(function.params[0].local)); + assert_eq!(argument.source.effect, CheckedCallerEffect::TransferOwned); + let E::Call { args, .. } = &call.kind else { + panic!("owned call"); + }; + assert_eq!( + acquisitions.argument_binding(&args[0]), + Some(function.params[0].local) + ); + assert_eq!(acquisitions.argument_binding(&args[1]), None); + assert_eq!( + acquisitions.argument_binding(&args[0].clone()), + None, + "a clone is not a validated occurrence" + ); + assert_eq!(acquisitions.owner_initializers().count(), 0); +} + +#[test] +fn caller_acquisition_rejects_original_laundering_and_wrong_current_slot() { + let (program, types) = source_program(SOURCE); + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut read_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("call"); + }; + source.arguments[0].staging = hir::ArgumentStaging::Original; + rejects(&changed, &types); + + let mut changed = program.clone(); + let other = exercise(&changed).params[1].local; + let E::Call { args, .. } = &mut read_call_mut(exercise_mut(&mut changed)).kind else { + panic!("call"); + }; + let E::View(value) = &mut args[0].kind else { + panic!("physical view"); + }; + value.kind = E::Local(other); + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_rejects_same_type_owner_substitution_and_duplicate_initializer() { + let (program, types) = source_program(SOURCE); + let (owner, _) = owning_stage(exercise(&program)); + let other = exercise(&program).params[1].local; + let mut changed = program.clone(); + let initializer = exercise_mut(&mut changed) + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("owner initializer"); + initializer.kind = E::Local(other); + rejects(&changed, &types); + + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, statement) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + .map(|statement| (block, statement)) + }) + .expect("owner statement"); + let duplicate = function.blocks[block].statements[statement].clone(); + function.blocks[block] + .statements + .insert(statement, duplicate); + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_rejects_wrong_sum_payload_selection_and_missing_end() { + let (program, types) = source_program(SOURCE); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let output = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::SumTake { success, .. } if *success => Some(success), + _ => None, + }) + .expect("handled success"); + *output = false; + rejects(&changed, &types); + + let mut changed = program.clone(); + for block in &mut exercise_mut(&mut changed).blocks { + block + .statements + .retain(|statement| !matches!(statement.kind, S::EndCallView { .. })); + } + rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_does_not_skip_disconnected_corrupt_call_packets() { + let (mut program, types) = source_program(SOURCE); + let function = exercise_mut(&mut program); + let mut value = read_call(function).clone(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut value.kind + else { + panic!("call"); + }; + source.arguments[0].source_index = source.arguments[1].source_index; + let block = BlockId(function.blocks.len() as u32); + function.blocks.push(crate::BasicBlock { + id: block, + statements: vec![crate::Statement { + span: value.span, + kind: S::Evaluate(value), + }], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + rejects(&program, &types); +} + +#[test] +fn caller_acquisition_defaults_require_contained_spans_and_the_exact_failed_arm() { + let (program, types) = source_program(SOURCE); + validate_function(&program, exercise(&program), &types) + .expect("valid contained handled default"); + for malformed in [ + "statement span", + "value span", + "success arm", + "missing anchor", + "disconnected default", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let output = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .expect("handled success output"); + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, value)| + value.statements.iter().position(|statement| matches!(statement.kind, S::Let { local, .. } if local == output)) + .map(|index| (block, index))).expect("contained default definition"); + match malformed { + "statement span" => { + function.blocks[block].statements[index].span.file = + jett_common::FileId::new(u32::MAX); + } + "value span" => { + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + panic!("default Let"); + }; + value.span.file = jett_common::FileId::new(u32::MAX); + } + "success arm" => { + let default = function.blocks[block].statements.remove(index); + let success = function.blocks.iter_mut().find(|block| block.statements.iter().any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output))).expect("success arm"); + success.statements.push(default); + } + "missing anchor" => { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!(statement.kind, + S::SumTake { target, success: true, .. } if target == output) + }); + } + } + "disconnected default" => { + let default = function.blocks[block].statements[index].clone(); + function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![default], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_written_direct_owned_producer_restores_exact_backing() { + let source = r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "member" +function produce() returns Named: + return 7 +function read(view item: Named, extra: int64) returns int64: + return extra +function exercise(spare: Named, incoming: optional[int64]) returns int64: + return read(view produce(), incoming handle: + default 3 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("written View of original owned producer"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "backing is not a source Binding acquisition" + ); + let loan = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::BeginCallView { local, .. } => Some(local), + _ => None, + }) + .expect("direct produced view stage"); + let owner = function + .local(loan) + .unwrap() + .view_source + .expect("exact backing owner"); + assert!(function.local(owner).unwrap().view_source.is_none()); + let produce = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "produce") + .unwrap() + .id; + assert_eq!(function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(&statement.kind, + S::Let { local, value } if *local == owner && matches!(value.kind, E::Call { function, .. } if function == produce))).count(), 1); + for malformed in [ + "renamed binding", + "missing initializer", + "duplicate initializer", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, value)| + value.statements.iter().position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .map(|index| (block, index))).expect("owning producer initializer"); + match malformed { + "renamed binding" => { + let spare = function.params[0].local; + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + panic!("owner Let"); + }; + value.kind = E::Local(spare); + } + "missing initializer" => { + function.blocks[block].statements.remove(index); + } + "duplicate initializer" => { + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_handled_storage_keeps_sealed_occurrence_separate_from_actual() { + let source = r#"namespace app +function read(view values: secret[list[int64]], extra: int64) returns int64: + return extra +function exercise(incoming: optional[list[int64]]) returns int64: + return read((incoming handle: + default list(7) + ), 3) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let acquisitions = + validate_function(&program, function, &types).expect("handled Secret-qualified occurrence"); + let (owner, _) = owning_stage(function); + let source = acquisitions + .owner_initializer(owner) + .expect("produced owning acquisition"); + assert!(matches!(types.resolve(source.actual_type), Type::List(_))); + assert!( + matches!(types.resolve(source.storage_type), Type::Secret(inner) if *inner == source.actual_type) + ); + assert_eq!(source.binding, None); + assert_eq!(source.effect, CheckedCallerEffect::RelinquishOwned); + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("qualified source read"); + }; + packet.arguments[0].actual_type = source.storage_type; + rejects(&changed, &types); +} + +const ABSENT_SUCCESS_SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(values: list[int64]) returns int64: + return read(values, (none handle: + default 3 + )) +"#; + +fn absent_original_plan(program: &Program, types: &TypeInterner) -> AbsentSuccessPlan { + let proof = validate_function(program, exercise(program), types) + .expect("typed original absent success"); + let [plan] = proof.absent_successes.as_slice() else { + panic!("one exact absent-success plan"); + }; + plan.clone() +} + +#[test] +fn caller_acquisition_absent_success_is_original_metadata_then_private_prepared_proof() { + for source in [ + ABSENT_SUCCESS_SOURCE.to_owned(), + ABSENT_SUCCESS_SOURCE.replace("none handle:", "fail(\"missing\") handle error:"), + ] { + let (mut program, types) = source_program(&source); + let plan = absent_original_plan(&program, &types); + assert!( + matches!(types.resolve(plan.source_type), Type::Optional(inner) if *inner == TypeInterner::NEVER) + || matches!(types.resolve(plan.source_type), Type::Result(ok, error) if *ok == TypeInterner::NEVER && *error == TypeInterner::STRING) + ); + assert_eq!(plan.output_type, TypeInterner::INT64); + assert!(exercise(&program).prepared_absent_successes.is_empty()); + crate::prepare_native_sequences(&mut program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("all prepared caller proofs"); + let function = exercise(&program); + let [record] = function.prepared_absent_successes.as_slice() else { + panic!("one private preparation certificate"); + }; + let prepared = record.plan(); + assert_eq!(prepared.function, function.id); + assert_eq!(prepared.identity, function.identity); + assert_eq!(prepared.source_type, plan.source_type); + assert_eq!(prepared.output_type, plan.output_type); + assert_eq!(prepared.span, plan.span); + assert!( + matches!(function.blocks[prepared.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == prepared.failure) + ); + assert!(!function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == prepared.output))); + assert!( + matches!(function.blocks[prepared.selecting.index() as usize].statements.last().unwrap().kind, + S::SumTag { source, target } if source == prepared.source && target == prepared.tag) + ); + let original = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!( + program, original, + "a second preparation never duplicates authority" + ); + } +} + +#[test] +fn caller_acquisition_pending_sum_receiver_and_nested_absent_defaults_keep_order() { + let pending = ABSENT_SUCCESS_SOURCE.replace( + " return read(values, (none handle:", + " optional[int64] incoming = run run none\n return read(values, (incoming handle:", + ); + let (mut program, types) = source_program(&pending); + let proof = validate_function(&program, exercise(&program), &types) + .expect("typed inhabited pending receiver"); + assert!( + proof.absent_successes.is_empty(), + "an inhabited Optional[int64] never receives absent-success authority" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("pending receiver remains an ordinary typed sum"); + let function = exercise(&program); + assert!(function.prepared_absent_successes.is_empty()); + let (pending_block, pending_position, incoming) = function.blocks.iter().find_map(|block| { + block.statements.iter().enumerate().find_map(|(position, statement)| match &statement.kind { + S::Let { local, value } if matches!(&value.kind, E::Run(first) if matches!(first.kind, E::Run(_))) => Some((block.id, position, *local)), + _ => None, + }) + }).expect("both original pending layers remain executable"); + assert!( + matches!(types.resolve(function.local(incoming).unwrap().ty), Type::Optional(inner) if *inner == TypeInterner::INT64) + ); + let (block, source, tag, tag_position) = + function + .blocks + .iter() + .find_map(|block| { + block.statements.iter().enumerate().find_map( + |(position, statement)| match statement.kind { + S::SumTag { source, target } => Some((block, source, target, position)), + _ => None, + }, + ) + }) + .expect("ordinary runtime tag remains"); + let source_position = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == source)) + .expect("owned sum source remains"); + assert_eq!(block.id, pending_block); + assert!( + pending_position < source_position && source_position < tag_position, + "original pending creation and ordinary snapshot precede the runtime tag" + ); + let S::Let { value, .. } = &block.statements[source_position].kind else { + unreachable!() + }; + assert!( + matches!(&value.kind, E::Clone(inner) if matches!(inner.kind, E::Local(local) if local == incoming)), + "the existing handle snapshot preserves the pending receiver" + ); + let T::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + panic!("inhabited sum retains its branch"); + }; + assert!(matches!(condition.kind, E::Local(local) if local == tag)); + assert_ne!(then_block, else_block); + assert!(function.blocks[then_block.index() as usize].statements.iter().any(|statement| + matches!(statement.kind, S::SumTake { source: owner, success: true, .. } if owner == source)), + "inhabited success extraction remains"); + + let nested = ABSENT_SUCCESS_SOURCE.replace("default 3", "optional[int64] fallback = none\n default (fallback handle:\n default 4\n )"); + let (mut program, types) = source_program(&nested); + absent_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("nested prepared original occurrence"); + let function = exercise(&program); + let [record] = function.prepared_absent_successes.as_slice() else { + panic!("one exact nested absent-success certificate"); + }; + let plan = record.plan(); + let block = &function.blocks[plan.selecting.index() as usize]; + let source_position = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == plan.source), + ) + .expect("owning sum source remains executable"); + let tag_position = block.statements.iter().position(|statement| + matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)).unwrap(); + assert!( + source_position < tag_position, + "source evaluation precedes the runtime sum guard" + ); + assert_ne!( + plan.selecting, plan.failure, + "nested defaults remain after the exact false edge" + ); + let inner = function.blocks.iter().find(|block| + block.id != plan.selecting && matches!(&block.terminator.kind, T::Branch { .. }) + && block.statements.iter().any(|statement| matches!(statement.kind, + S::SumTag { source, .. } if matches!(types.resolve(function.local(source).unwrap().ty), + Type::Optional(payload) if *payload == TypeInterner::INT64)))) + .expect("typed nested fallback retains its inhabited branch"); + let T::Branch { then_block, .. } = inner.terminator.kind else { + unreachable!() + }; + assert!( + function.blocks[then_block.index() as usize] + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTake { success: true, .. })), + "inner inhabited success extraction remains after outer absence preparation" + ); +} + +#[test] +fn caller_acquisition_absent_success_rejects_original_forged_tag_default_and_manual_goto() { + let (program, types) = source_program(ABSENT_SUCCESS_SOURCE); + let plan = absent_original_plan(&program, &types); + for malformed in [ + "tag type", + "same branches", + "false extraction", + "foreign default", + "manual goto", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "tag type" => function.locals[plan.tag.index() as usize].ty = TypeInterner::INT64, + "same branches" => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[plan.selecting.index() as usize] + .terminator + .kind + else { + panic!("original branch"); + }; + *then_block = *else_block; + } + "false extraction" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == plan.output)).unwrap(); + let S::SumTake { success, .. } = &mut statement.kind else { + panic!("anchor"); + }; + *success = false; + } + "foreign default" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap(); + statement.span.file = jett_common::FileId::new(u32::MAX); + } + "manual goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.failure); + crate::sequences::prune::unreachable(function); + assert!(function.prepared_absent_successes.is_empty()); + } + _ => unreachable!(), + } + rejects(&changed, &types); + let before = changed.clone(); + crate::prepare_native_uninhabited_sums(&mut changed, &types); + assert_eq!( + changed, before, + "bad original graph is never rescued by pruning" + ); + } +} + +#[test] +fn caller_acquisition_absent_success_rejoins_every_private_prepared_record() { + let (mut program, types) = source_program(ABSENT_SUCCESS_SOURCE); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + let plan = exercise(&program).prepared_absent_successes[0] + .plan() + .clone(); + for malformed in [ + "missing record", + "duplicate record", + "changed goto", + "missing tag", + "wrong output type", + "foreign default", + "disconnected default", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "missing record" => function.prepared_absent_successes.clear(), + "duplicate record" => function + .prepared_absent_successes + .push(function.prepared_absent_successes[0].clone()), + "changed goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.selecting) + } + "missing tag" => { + function.blocks[plan.selecting.index() as usize].statements.retain(|statement| + !matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)); + } + "wrong output type" => { + function.locals[plan.output.index() as usize].debug_ty = TypeInterner::BOOL + } + "foreign default" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap(); + statement.span.file = jett_common::FileId::new(u32::MAX); + } + "disconnected default" => { + let statement = function.blocks.iter().flat_map(|block| &block.statements).find(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)).unwrap().clone(); + function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: crate::Terminator { + span: function.span, + kind: T::Unreachable, + }, + }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_absent_success_certificates_follow_real_dense_remapping() { + let source = ABSENT_SUCCESS_SOURCE.replace(" return read(values,", " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return read(values,"); + let (mut program, types) = source_program(&source); + let original = absent_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let after_sum = exercise(&program).prepared_absent_successes[0] + .plan() + .clone(); + assert_ne!( + after_sum.failure, original.failure, + "removed success block changes the failure ID" + ); + assert_ne!( + after_sum.source, original.source, + "detached inline formal changes the owner ID" + ); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("remapped certificate joins actual storage and graph"); + let function = exercise(&program); + let plan = function.prepared_absent_successes[0].plan(); + assert_eq!(function.local(plan.source).unwrap().ty, plan.source_type); + assert_eq!(function.local(plan.output).unwrap().ty, plan.output_type); + assert_eq!(function.local(plan.tag).unwrap().ty, TypeInterner::BOOL); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, T::Goto(target) if target == plan.failure) + ); +} + +const GENERATED_SNAPSHOT_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function exercise(view item: optional[Item], view spare: Item) returns string: + return "{item handle: default Item(value: 7)}" +"#; + +const GENERATED_PRODUCER_SOURCE: &str = r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function make(value: int64) returns Item: + return Item(value: value) +function other(value: int64) returns Item: + return Item(value: value) +function exercise(incoming: optional[int64]) returns string: + return "{make(incoming handle: default 7)}" +"#; + +fn generated_in_expression(value: &Expression) -> Option<&Expression> { + if matches!( + &value.kind, + E::Call { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::IndirectCall { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::Intrinsic { + ownership: hir::CallOwnership::Generated(_), + .. + } + ) { + return Some(value); + } + match &value.kind { + E::View(inner) + | E::Clone(inner) + | E::Coarsen(inner) + | E::Declassify(inner) + | E::Run(inner) + | E::Join(inner) + | E::RefinementValidated(inner) + | E::DisplayResult(inner) + | E::EquatableResult(inner) => generated_in_expression(inner), + E::StringInterpolation(parts) => parts.iter().find_map(|part| match part { + hir::StringSegment::Value(value) => generated_in_expression(value), + _ => None, + }), + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + args.iter().find_map(generated_in_expression) + } + _ => None, + } +} + +fn generated_in_expression_mut(value: &mut Expression) -> Option<&mut Expression> { + if matches!( + &value.kind, + E::Call { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::IndirectCall { + ownership: hir::CallOwnership::Generated(_), + .. + } | E::Intrinsic { + ownership: hir::CallOwnership::Generated(_), + .. + } + ) { + return Some(value); + } + match &mut value.kind { + E::View(inner) + | E::Clone(inner) + | E::Coarsen(inner) + | E::Declassify(inner) + | E::Run(inner) + | E::Join(inner) + | E::RefinementValidated(inner) + | E::DisplayResult(inner) + | E::EquatableResult(inner) => generated_in_expression_mut(inner), + E::StringInterpolation(parts) => parts.iter_mut().find_map(|part| match part { + hir::StringSegment::Value(value) => generated_in_expression_mut(value), + _ => None, + }), + E::Call { args, .. } | E::IndirectCall { args, .. } | E::Intrinsic { args, .. } => { + args.iter_mut().find_map(generated_in_expression_mut) + } + _ => None, + } +} + +fn generated_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) | S::HandleDefault(value) => { + generated_in_expression(value) + } + S::CheckRefinement { call, .. } => generated_in_expression(call), + _ => None, + }) + .or_else(|| match &block.terminator.kind { + T::Return(Some(value)) => generated_in_expression(value), + _ => None, + }) + }) + .expect("source-derived generated invocation") +} + +fn generated_call_mut(function: &mut Function) -> &mut Expression { + for block in &mut function.blocks { + for statement in &mut block.statements { + let found = match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) | S::HandleDefault(value) => { + generated_in_expression_mut(value) + } + S::CheckRefinement { call, .. } => generated_in_expression_mut(call), + _ => None, + }; + if let Some(found) = found { + return found; + } + } + if let T::Return(Some(value)) = &mut block.terminator.kind + && let Some(found) = generated_in_expression_mut(value) + { + return found; + } + } + panic!("source-derived generated invocation"); +} + +fn generated_argument(function: &Function) -> &hir::GeneratedArgumentOwnership { + let E::Call { + ownership: hir::CallOwnership::Generated(call), + .. + } = &generated_call(function).kind + else { + panic!("generated direct call"); + }; + &call.arguments[0] +} + +fn generated_owner_stage(function: &Function) -> (LocalId, LocalId) { + match generated_argument(function).staging { + hir::GeneratedArgumentStaging::OwnedProducer { owner, loan } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan } => (owner, loan), + other => panic!("generated owning stage: {other:?}"), + } +} + +fn generated_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("generated owning initializer") +} + +fn generated_rejects(program: &Program, types: &TypeInterner) -> String { + let error = validate_function(program, exercise(program), types) + .expect_err("malformed generated storage must fail"); + assert!( + error.contains("call ownership") + || error.contains("call view") + || error.contains("generated") + || error.contains("sealed"), + "{error}" + ); + error +} + +#[test] +fn caller_acquisition_generated_snapshot_keeps_handled_origin_and_empty_source_acquisitions() { + let (mut program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let function = exercise(&program); + let argument = generated_argument(function); + assert!( + argument + .original_witness() + .ordinary_snapshot_proof() + .is_some() + ); + assert!(!argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OrdinarySnapshot { .. } + )); + let (owner, loan) = generated_owner_stage(function); + let initializer = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("snapshot initializer"); + assert!(matches!(initializer.kind, E::Clone(_))); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquisitions = + validate_function(&program, function, &types).expect("generated handled snapshot"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "generated copy must not consume a source binding" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + validate_function(&program, exercise(&program), &types).expect("prepared generated snapshot"); +} + +#[test] +fn caller_acquisition_generated_snapshot_rejects_lost_clone_and_same_type_borrow_substitution() { + let (program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let (owner, _) = generated_owner_stage(exercise(&program)); + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(inner) = &initializer.kind else { + panic!("snapshot clone"); + }; + *initializer = inner.as_ref().clone(); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let spare = exercise(&changed).params[1].local; + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(observed) = &mut initializer.kind else { + panic!("snapshot clone"); + }; + let E::View(value) = &mut observed.kind else { + panic!("physical observed view"); + }; + value.kind = E::Local(spare); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let mut replacement = exercise(&changed) + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } if matches!(value.kind, E::StructConstruct { .. }) => { + Some(value.clone()) + } + _ => None, + }) + .expect("typed default constructor"); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Clone(observed) = &mut initializer.kind else { + panic!("snapshot clone"); + }; + let E::View(value) = &mut observed.kind else { + panic!("physical observed view"); + }; + replacement.span = value.span; + assert_eq!(replacement.ty, value.ty); + **value = replacement; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_storage_rejects_duplicate_owner_missing_end_and_public_relabel() { + let (program, types) = source_program(GENERATED_SNAPSHOT_SOURCE); + let (owner, loan) = generated_owner_stage(exercise(&program)); + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + .map(|index| (block, index)) + }) + .expect("owner definition"); + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + for block in &mut exercise_mut(&mut changed).blocks { + block.statements.retain( + |statement| !matches!(statement.kind, S::EndCallView { local } if local == loan), + ); + } + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let E::Call { + ownership: hir::CallOwnership::Generated(call), + .. + } = &mut generated_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("generated call"); + }; + call.arguments[0].staging = hir::GeneratedArgumentStaging::OwnedProducer { owner, loan }; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_owned_producer_rejects_added_clone_and_wrong_exact_target() { + let (program, types) = source_program(GENERATED_PRODUCER_SOURCE); + let function = exercise(&program); + let argument = generated_argument(function); + assert!(argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OwnedProducer { .. } + )); + let (owner, _) = generated_owner_stage(function); + validate_function(&program, function, &types).expect("complete generated producer"); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let original = initializer.clone(); + initializer.kind = E::Clone(Box::new(original)); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let other = changed + .functions + .iter() + .find(|function| function.identity.declaration.name == "other") + .expect("same-signature control") + .id; + let initializer = generated_initializer_mut(exercise_mut(&mut changed), owner); + let E::Call { function, .. } = &mut initializer.kind else { + panic!("full endpoint producer"); + }; + *function = other; + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_explicit_clone_and_constructor_producers_remain_distinct() { + for source in [ + GENERATED_PRODUCER_SOURCE.replace("{make(incoming", "{clone make(incoming"), + GENERATED_PRODUCER_SOURCE.replace( + "{make(incoming handle: default 7)}", + "{Item(value: incoming handle: default 7)}", + ), + ] { + let (program, types) = source_program(&source); + let function = exercise(&program); + let argument = generated_argument(function); + assert!(argument.original_witness().owned_producer()); + assert!(matches!( + argument.staging, + hir::GeneratedArgumentStaging::OwnedProducer { .. } + )); + validate_function(&program, function, &types) + .expect("original explicit Clone or exact constructor producer"); + } +} + +#[test] +fn caller_acquisition_generated_constructor_restores_each_exact_materialized_field() { + let source = GENERATED_PRODUCER_SOURCE + .replace( + " value: int64\n", + " value: int64\n marker: int64\n", + ) + .replace("Item(value: value)", "Item(value: value, marker: 1)") + .replace( + "{make(incoming handle: default 7)}", + "{Item(value: incoming handle: default 7, marker: 2)}", + ); + let (program, types) = source_program(&source); + let function = exercise(&program); + let (owner, _) = generated_owner_stage(function); + validate_function(&program, function, &types) + .expect("handled and primitive fields preserve their original nodes"); + let field = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { + local, + value: + Expression { + kind: E::StructConstruct { fields, .. }, + .. + }, + } if *local == owner => { + assert_eq!(fields.len(), 2); + let E::Local(field) = fields[1].kind else { + panic!("second field materialized at lexical position"); + }; + Some(field) + } + _ => None, + }) + .expect("complete constructor initializer"); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), field); + let original = initializer.clone(); + initializer.kind = E::Clone(Box::new(original)); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + let initializer = generated_initializer_mut(exercise_mut(&mut changed), field); + initializer.kind = E::Local(field); + generated_rejects(&changed, &types); +} + +#[test] +fn caller_acquisition_generated_storage_and_private_association_follow_dense_local_remapping() { + let source = GENERATED_SNAPSHOT_SOURCE.replace(" return \"{item handle:", + " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return \"{item handle:"); + let (mut program, types) = source_program(&source); + let before = generated_owner_stage(exercise(&program)); + crate::prepare_native_generated_functions(&mut program, &types); + let function = exercise(&program); + let after = generated_owner_stage(function); + assert_ne!( + after, before, + "detached inline formal must force real stage ID remapping" + ); + assert_eq!( + function.local(after.1).expect("remapped loan").view_source, + Some(after.0) + ); + validate_function(&program, function, &types) + .expect("all original and private Generated stage IDs remapped"); +} + +#[test] +fn caller_acquisition_generated_owned_copy_storage_requires_one_exact_original_initializer() { + let source = r#"namespace app +type Positive = int64 where value > 0 +function exercise(raw: int64) returns Positive: + return raw +"#; + let (mut program, types) = source_program(source); + let function = exercise_mut(&mut program); + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, value)| { + value + .statements + .iter() + .position(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + .map(|index| (block, index)) + }) + .expect("refinement check"); + let (original, span) = { + let S::CheckRefinement { call, .. } = &function.blocks[block].statements[index].kind else { + panic!("predicate"); + }; + let E::Call { + args, + ownership: hir::CallOwnership::Generated(packet), + .. + } = &call.kind + else { + panic!("generated predicate"); + }; + assert_eq!( + packet.arguments[0].acquisition, + hir::GeneratedAcquisition::Copy + ); + assert_eq!( + packet.arguments[0].callee_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + assert_eq!( + packet.arguments[0].staging, + hir::GeneratedArgumentStaging::Existing { loan: None } + ); + (args[0].clone(), args[0].span) + }; + let owner = LocalId::new(function.locals.len() as u32); + let mut metadata = function.locals[function.params[0].local.index() as usize].clone(); + metadata.id = owner; + metadata.name = "isolated_materialized_operand".into(); + metadata.ty = original.ty; + metadata.debug_ty = original.ty; + metadata.mutable = true; + metadata.view_source = None; + metadata.span = span; + function.locals.push(metadata); + let S::CheckRefinement { call, .. } = &mut function.blocks[block].statements[index].kind else { + panic!("predicate"); + }; + let E::Call { args, .. } = &mut call.kind else { + panic!("call"); + }; + args[0] = Expression { + kind: E::Local(owner), + ty: original.ty, + span, + }; + function.blocks[block].statements.insert( + index, + crate::Statement { + span, + kind: S::Let { + local: owner, + value: original, + }, + }, + ); + let acquisitions = validate_function(&program, exercise(&program), &types) + .expect("finite owned Copy materialization"); + assert_eq!(acquisitions.owner_initializers().count(), 0); + + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let duplicate = function.blocks[block].statements[index].clone(); + function.blocks[block].statements.insert(index, duplicate); + generated_rejects(&changed, &types); + + let mut changed = program.clone(); + exercise_mut(&mut changed).locals[owner.index() as usize].debug_ty = TypeInterner::BOOL; + generated_rejects(&changed, &types); +} + +const PRESENT_SUCCESS_SOURCE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise[E](incoming: result[list[int64], E]) returns int64: + return read((incoming handle error: + default list(7) + ), 3) +function main() returns nothing: + int64 answer = exercise(ok(list(1, 2))) + return nothing +"#; + +fn present_original_plan(program: &Program, types: &TypeInterner) -> PresentSuccessPlan { + let proof = validate_function(program, exercise(program), types) + .expect("typed original inhabited-success edge"); + let [plan] = proof.present_successes.as_slice() else { + panic!("one exact present-success plan"); + }; + plan.clone() +} + +#[test] +fn caller_acquisition_present_success_preserves_tag_and_owned_payload_without_default_authority() { + for source in [ + PRESENT_SUCCESS_SOURCE.to_owned(), + PRESENT_SUCCESS_SOURCE.replace( + "read(view values: list[int64]", + "read(view values: secret[list[int64]]", + ), + ] { + let (mut program, types) = source_program(&source); + let original = present_original_plan(&program, &types); + assert!( + matches!(types.resolve(original.source_type), Type::Result(ok, error) + if *ok != TypeInterner::NEVER && *error == TypeInterner::NEVER) + ); + assert!(exercise(&program).prepared_present_successes.is_empty()); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("canonical prepared success retains acquisition authority"); + let function = exercise(&program); + let [record] = function.prepared_present_successes.as_slice() else { + panic!("one private present-success record"); + }; + let plan = record.plan(); + assert_eq!( + ( + plan.function, + &plan.identity, + plan.source_type, + plan.output_type, + plan.span + ), + ( + function.id, + &function.identity, + original.source_type, + original.output_type, + original.span + ) + ); + assert!(function.prepared_absent_successes.is_empty()); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, + T::Goto(target) if target == plan.success) + ); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].statements.last().unwrap().kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + ); + let extractions = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!(statement.kind, S::SumTake { source, target, success: true } + if source == plan.source && target == plan.output) + }) + .count(); + assert_eq!( + extractions, 1, + "inhabited successful payload extraction stays executable" + ); + assert!(!function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == plan.output)), "erased default gains no prepared authority"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("successful payload/source ownership and cleanup remain exact"); + let prepared = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!( + program, prepared, + "repeated preparation cannot mint a second record from Goto" + ); + } + + let control = PRESENT_SUCCESS_SOURCE.replace("int64 answer = exercise(ok(list(1, 2)))", + "result[list[int64], string] incoming = ok(list(1, 2))\n int64 answer = exercise(incoming)"); + let (mut program, types) = source_program(&control); + assert!( + validate_function(&program, exercise(&program), &types) + .unwrap() + .present_successes + .is_empty() + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + assert!(exercise(&program).prepared_present_successes.is_empty()); + assert!( + exercise(&program) + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, T::Branch { .. })), + "an inhabited error arm keeps its ordinary selecting Branch" + ); +} + +#[test] +fn caller_acquisition_present_pending_receiver_keeps_original_snapshot_before_tag_and_take() { + let source = PRESENT_SUCCESS_SOURCE.replace( + "exercise(ok(list(1, 2)))", + "exercise(run run ok(list(1, 2)))", + ); + let (mut program, types) = source_program(&source); + present_original_plan(&program, &types); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + fn double_pending(value: &Expression) -> bool { + match &value.kind { + E::Run(first) if matches!(first.kind, E::Run(_)) => true, + E::Call { args, .. } | E::Intrinsic { args, .. } => args.iter().any(double_pending), + E::View(value) | E::Clone(value) => double_pending(value), + _ => false, + } + } + assert!( + main.blocks + .iter() + .any(|block| block + .statements + .iter() + .any(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) => double_pending(value), + _ => false, + })), + "both pending layers remain in the checked caller" + ); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("pending exact result keeps prepared source proof"); + let function = exercise(&program); + let plan = function.prepared_present_successes[0].plan(); + let block = &function.blocks[plan.selecting.index() as usize]; + let owning = block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, + S::Let { local, .. } if local == plan.source) + }) + .expect("original source owning snapshot"); + let tag = block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, + S::SumTag { source, target } if source == plan.source && target == plan.tag) + }) + .unwrap(); + assert!(owning < tag); + assert_eq!(tag + 1, block.statements.len()); + assert!(matches!(block.terminator.kind, T::Goto(target) if target == plan.success)); + assert!( + function.blocks[plan.success.index() as usize] + .statements + .iter() + .any( + |statement| matches!(statement.kind, S::SumTake { source, target, success: true } + if source == plan.source && target == plan.output) + ), + "pending guard remains before extraction" + ); +} + +#[test] +fn caller_acquisition_present_success_refuses_original_manual_or_foreign_edges_before_pruning() { + let (program, types) = source_program(PRESENT_SUCCESS_SOURCE); + let plan = present_original_plan(&program, &types); + for malformed in [ + "manual goto", + "wrong tag type", + "wrong tag owner", + "same branches", + "wrong extraction", + "foreign incoming", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "manual goto" => { + function.blocks[plan.selecting.index() as usize] + .terminator + .kind = T::Goto(plan.success); + crate::sequences::prune::unreachable(function); + assert!(function.prepared_present_successes.is_empty()); + } + "wrong tag type" => function.locals[plan.tag.index() as usize].ty = TypeInterner::INT64, + "wrong tag owner" => { + let foreign = function.params[0].local; + let S::SumTag { source, .. } = &mut function.blocks + [plan.selecting.index() as usize] + .statements + .last_mut() + .unwrap() + .kind + else { + unreachable!() + }; + *source = foreign; + } + "same branches" => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[plan.selecting.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + *else_block = *then_block; + } + "wrong extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter_mut().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap(); + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!() + }; + *success = false; + } + "foreign incoming" => function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![], + terminator: crate::Terminator { + span: plan.span, + kind: T::Goto(plan.success), + }, + }), + _ => unreachable!(), + } + rejects(&changed, &types); + let before = changed.clone(); + crate::prepare_native_uninhabited_sums(&mut changed, &types); + assert_eq!( + changed, before, + "canonical preparation never rescues an unproved original edge" + ); + } +} + +#[test] +fn caller_acquisition_present_success_rejoins_all_private_records_and_current_extraction() { + let (mut program, types) = source_program(PRESENT_SUCCESS_SOURCE); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::validate_call_ownership(&program, &types).unwrap(); + let plan = exercise(&program).prepared_present_successes[0] + .plan() + .clone(); + for malformed in [ + "missing record", + "duplicate record", + "wrong goto", + "missing tag", + "wrong output type", + "foreign identity", + "wrong extraction", + "foreign incoming", + "disconnected extraction", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match malformed { + "missing record" => function.prepared_present_successes.clear(), + "duplicate record" => { + let duplicate = function.prepared_present_successes[0].clone(); + function.prepared_present_successes.push(duplicate); + } + "wrong goto" => function.blocks[plan.selecting.index() as usize].terminator.kind = T::Goto(plan.selecting), + "missing tag" => function.blocks[plan.selecting.index() as usize].statements.retain(|statement| + !matches!(statement.kind, S::SumTag { source, target } if source == plan.source && target == plan.tag)), + "wrong output type" => function.locals[plan.output.index() as usize].debug_ty = TypeInterner::BOOL, + "foreign identity" => function.identity.declaration.namespace.push_str("_foreign"), + "wrong extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter_mut().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap(); + let S::SumTake { source, .. } = &mut statement.kind else { unreachable!() }; + *source = function.params[0].local; + } + "foreign incoming" => function.blocks.push(crate::BasicBlock { + id: BlockId(function.blocks.len() as u32), statements: vec![], + terminator: crate::Terminator { span: plan.span, kind: T::Goto(plan.success) }, + }), + "disconnected extraction" => { + let statement = function.blocks[plan.success.index() as usize].statements.iter().find(|statement| + matches!(statement.kind, S::SumTake { target, .. } if target == plan.output)).unwrap().clone(); + function.blocks.push(crate::BasicBlock { id: BlockId(function.blocks.len() as u32), statements: vec![statement], + terminator: crate::Terminator { span: plan.span, kind: T::Unreachable } }); + } + _ => unreachable!(), + } + rejects(&changed, &types); + } +} + +#[test] +fn caller_acquisition_present_success_private_ids_follow_real_dense_remapping() { + let source = PRESENT_SUCCESS_SOURCE.replace(" return read((incoming", + " function(int64) returns int64 callback = function(ignored: int64) returns int64:\n return ignored\n int64 first = callback(0)\n return read((incoming"); + let (mut program, types) = source_program(&source); + let original = present_original_plan(&program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let after_sum = exercise(&program).prepared_present_successes[0] + .plan() + .clone(); + assert_ne!( + after_sum.source, original.source, + "unused extracted inline formal causes actual dense local remapping" + ); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("every private ID rejoins remapped actual graph and storage"); + let function = exercise(&program); + let plan = function.prepared_present_successes[0].plan(); + assert_eq!(function.local(plan.source).unwrap().ty, plan.source_type); + assert_eq!(function.local(plan.output).unwrap().ty, plan.output_type); + assert_eq!(function.local(plan.tag).unwrap().ty, TypeInterner::BOOL); + assert!( + matches!(function.blocks[plan.selecting.index() as usize].terminator.kind, T::Goto(target) if target == plan.success) + ); +} + +const CONVERTED_OBSERVATION_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +implement Named for Boxed[string]: + function name(view self: Boxed[string]) returns string: + return "text" +function read(values: list[Named], expected: string) returns bool: + return true +function count(view values: list[Boxed[string]]) returns int64: + return 0 +property observed: + given values: list[Boxed[string]] + given other: list[Boxed[string]] + assert read(values, "text") + assert read(clone values, "text") + assert count(view values) == 0 +"#; + +fn observation_source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("property HIR"); + ( + crate::lower(&high, &checked.interner).expect("property MIR"), + checked.interner, + ) +} + +fn observed_property(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::Property + && function.identity.declaration.name.starts_with("observed:") + }) + .expect("checked property") +} + +fn observed_read_calls(function: &Function) -> Vec<&Expression> { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| { + let S::Assert { condition, .. } = &statement.kind else { + return None; + }; + matches!(condition.kind, E::Call { .. }).then_some(condition) + }) + .collect() +} + +fn observation_owner(call: &Expression) -> (LocalId, &hir::ArgumentOwnership) { + let E::Call { + ownership: hir::CallOwnership::Source(source), + args, + .. + } = &call.kind + else { + panic!("checked observed invocation"); + }; + let argument = &source.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::ObserveData); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + let hir::ArgumentStaging::Observed { owner } = argument.staging else { + panic!("observed owning slot"); + }; + assert!(matches!(args[0].kind, E::Local(local) if local == owner)); + (owner, argument) +} + +fn observation_initializer(function: &Function, owner: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("observed owning initializer") +} + +#[test] +fn source_observation_snapshots_raw_generic_data_before_interface_conversion() { + let (program, types) = observation_source_program(CONVERTED_OBSERVATION_SOURCE); + let function = observed_property(&program); + let calls = observed_read_calls(function); + assert_eq!(calls.len(), 2); + validate_function(&program, function, &types).expect("converted observed acquisition"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("original concrete owner stays available"); + for (index, call) in calls.iter().enumerate() { + let (owner, argument) = observation_owner(call); + let initializer = observation_initializer(function, owner); + let E::InterfaceCoerce { + value: snapshot, .. + } = &initializer.kind + else { + panic!("converted snapshot retains its checked contextual conversion"); + }; + assert_ne!(initializer.ty, argument.actual_type); + assert!(hir::observation_data_type(&types, argument.actual_type)); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let E::Clone(original) = &snapshot.kind else { + panic!("raw snapshot"); + }; + assert_eq!(snapshot.ty, argument.actual_type); + assert_eq!(original.ty, argument.actual_type); + assert_eq!(snapshot.span, argument.source_span); + assert_eq!(original.span, argument.source_span); + if index == 0 { + assert!(matches!(original.kind, E::Local(local) if local == function.params[0].local)); + } else { + assert!( + matches!(&original.kind, E::Clone(inner) + if matches!(inner.kind, E::Local(local) if local == function.params[0].local)), + "the explicit source Clone remains inside the compiler snapshot" + ); + } + } +} + +#[test] +fn source_observation_without_conversion_keeps_its_direct_snapshot_shape() { + let source = CONVERTED_OBSERVATION_SOURCE.replace( + "function read(values: list[Named]", + "function read(values: list[Boxed[string]]", + ); + let (program, types) = observation_source_program(&source); + let function = observed_property(&program); + let calls = observed_read_calls(function); + assert_eq!(calls.len(), 2); + for call in calls { + let (owner, argument) = observation_owner(call); + let initializer = observation_initializer(function, owner); + let E::Clone(original) = &initializer.kind else { + panic!("existing direct snapshot"); + }; + assert_eq!(initializer.ty, argument.actual_type); + assert_eq!(original.ty, initializer.ty); + assert!(crate::handlers::can_snapshot_view(&types, initializer.ty)); + } + validate_function(&program, function, &types).expect("direct observed acquisition"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("direct observation retains its owner"); +} + +#[test] +fn source_observation_conversion_rejects_missing_erased_and_foreign_snapshots() { + for mutation in 0..5 { + let (mut program, types) = observation_source_program(CONVERTED_OBSERVATION_SOURCE); + let function_id = observed_property(&program).id; + let (owner, _) = observation_owner(observed_read_calls(observed_property(&program))[0]); + let function = program + .functions + .iter_mut() + .find(|function| function.id == function_id) + .unwrap(); + if mutation == 4 { + let call = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::Assert { condition, .. } = &mut statement.kind else { + return None; + }; + matches!(condition.kind, E::Call { .. }).then_some(condition) + }) + .unwrap(); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut call.kind + else { + unreachable!(); + }; + source.arguments[0].staging = hir::ArgumentStaging::Original; + } else { + let other = function.params[1].local; + let initializer = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .unwrap(); + let E::InterfaceCoerce { + value: snapshot, .. + } = &mut initializer.kind + else { + panic!("checked conversion"); + }; + let E::Clone(original) = &mut snapshot.kind else { + panic!("raw snapshot"); + }; + match mutation { + 0 => *snapshot = Box::new(original.as_ref().clone()), + 1 => { + *snapshot = Box::new(original.as_ref().clone()); + let original_conversion = initializer.clone(); + initializer.kind = E::Clone(Box::new(original_conversion)); + } + 2 => snapshot.ty = TypeInterner::INT64, + 3 => original.kind = E::Local(other), + _ => unreachable!(), + } + } + let error = validate_function(&program, observed_property(&program), &types) + .expect_err("malformed observation snapshot must fail before preparation"); + let expected = match mutation { + 0 | 2 => "observation lost its exact raw snapshot occurrence", + 1 => "observation cannot snapshot an erased or unsupported value", + 3 => "binding does not match its actual typed operand", + 4 => "requires explicit owning staging", + _ => unreachable!(), + }; + assert!(error.contains(expected), "{mutation}: {error}"); + } +} + +// Checked predicate identities and nominal types are taken from real source. +// The extra function mirrors native_property_cases' compiler-owned replay call. +const GENERATED_REFINEMENT_REPLAY_SOURCE: &str = r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 1 +type Nonempty = list[int64] where true +property refined: + given number: Higher + given items: Nonempty + assert true +function number() returns Higher: + return 2 +function items() returns Nonempty: + return list(2) +"#; + +fn generated_refinement_program(default: bool) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(GENERATED_REFINEMENT_REPLAY_SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let mut high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("checked refinement HIR"); + let property_declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Property(property) if property.name.name == "refined" => { + Some(property) + } + _ => None, + }) + .expect("exact parsed property"); + let targets = high + .functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::Property + && function.span == property_declaration.span + && function.identity.declaration.name + == format!( + "{}:{}", + property_declaration.name.name, property_declaration.span.start + ) + }) + .collect::>(); + let [property] = targets.as_slice() else { + panic!("one exact checked property target"); + }; + let property = (*property).clone(); + let args = ["number", "items"] + .into_iter() + .map(|name| { + let function = high + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .expect("checked candidate"); + let hir::StatementKind::Return(Some(mut candidate)) = + function.body.statements[0].kind.clone() + else { + panic!("checked return guard"); + }; + let E::Handle { + error_local, + failure, + .. + } = &mut candidate.kind + else { + panic!("checked nominal refinement handle"); + }; + *error_local = None; + let failure_span = failure.span; + let body = if default { + let mut fallback = candidate.clone(); + fn occurrence(value: &mut Expression, span: Span) { + value.span = span; + match &mut value.kind { + E::ListConstruct { elements } => { + for value in elements { + occurrence(value, span); + } + } + E::Handle { + target, + error_local, + failure, + .. + } => { + occurrence(target, span); + *error_local = None; + *failure = hir::Block { + statements: vec![hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }], + span, + }; + } + _ => {} + } + } + occurrence(&mut fallback, failure_span); + hir::StatementKind::HandleDefault(fallback) + } else { + hir::StatementKind::Return(None) + }; + let E::Handle { failure, .. } = &mut candidate.kind else { + unreachable!(); + }; + *failure = hir::Block { + statements: vec![hir::Statement { + kind: body, + span: failure_span, + }], + span: failure_span, + }; + candidate + }) + .collect::>(); + let parameters = property + .params + .iter() + .map(|parameter| parameter.ty) + .collect::>(); + let order = vec![0, 1]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::NativeSuite { + function: property.id, + }, + &args, + ¶meters, + &[jett_typecheck::CheckedCalleeAccess::Owned; 2], + TypeInterner::NOTHING, + &order, + &checked.interner, + ) + .expect("exact replay factory"); + let span = property.span; + let call = Expression { + kind: E::Call { + function: property.id, + args, + evaluation_order: order, + ownership, + }, + ty: TypeInterner::NOTHING, + span, + }; + let mut identity = property.identity.clone(); + identity.declaration.name = "exercise".into(); + let id = hir::FunctionId::new(high.functions.len() as u32); + high.functions.push(hir::Function { + id, + identity, + debug_kind: hir::FunctionDebugKind::named("app", "exercise"), + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: Vec::new(), + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals: Vec::new(), + body: hir::Block { + statements: vec![ + hir::Statement { + kind: hir::StatementKind::Expression(call), + span, + }, + hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }, + ], + span, + }, + span, + }); + hir::validate_backend_types(&high, &checked.interner) + .expect("original immutable replay candidates"); + let program = crate::lower(&high, &checked.interner).expect("canonical refinement replay MIR"); + (program, checked.interner) +} + +#[test] +fn caller_acquisition_generated_refinement_replay_keeps_exact_chain_and_terminal_failure() { + let (program, types) = generated_refinement_program(false); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("ordered checked refinement candidates"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "generated candidates do not consume source bindings" + ); + let mut checks = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + S::CheckRefinement { type_name, .. } => Some(type_name.as_str()), + _ => None, + }) + .collect::>(); + checks.sort_unstable(); + assert_eq!(checks, ["app.Higher", "app.Nonempty", "app.Positive"]); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!( + statement.kind, + S::Let { + value: Expression { + kind: E::RefinementValidated(_), + .. + }, + .. + } + )) + .count(), + 2 + ); + assert!( + function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, T::Return(None))) + ); +} + +#[test] +fn caller_acquisition_generated_refinement_defaults_and_preparation_preserve_owned_candidates() { + let (mut program, types) = generated_refinement_program(true); + validate_function(&program, exercise(&program), &types) + .expect("defaults remain inside the exact failure body"); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::prepare_native_sequences(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("refinement source and predicate identities survive canonical preparation"); +} + +#[test] +fn caller_acquisition_generated_refinement_rejects_changed_predicates_candidate_and_edges() { + let (original, types) = generated_refinement_program(false); + for mutation in 0..8 { + let mut program = original.clone(); + let other_predicate = program + .functions + .iter() + .find(|function| { + function.identity.declaration.name == "Higher" + && function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + }) + .expect("distinct checked predicate") + .id; + let function = exercise_mut(&mut program); + let (block_index, check_index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block_index, block)| { + block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + .map(|index| (block_index, index)) + }) + .expect("first exact check"); + match mutation { + 0 => { + let S::CheckRefinement { type_name, .. } = + &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + *type_name = "app.Higher".into(); + } + 1 => { + let S::CheckRefinement { + call: + Expression { + kind: E::Call { args, .. }, + .. + }, + .. + } = &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + let E::Clone(inner) = &mut args[0].kind else { + panic!("canonical input clone"); + }; + inner.kind = E::Int(9); + } + 2 => { + let S::Let { value, .. } = + &mut function.blocks[block_index].statements[check_index + 1].kind + else { + panic!("passed local"); + }; + value.kind = E::Bool(true); + } + 3 => { + let T::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + std::mem::swap(then_block, else_block); + } + 4 => { + let T::Branch { else_block, .. } = function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + function.blocks[else_block.index() as usize].terminator.kind = T::Return(None); + } + 5 => { + let T::Branch { then_block, .. } = function.blocks[block_index].terminator.kind + else { + panic!("exact branch"); + }; + function.blocks[block_index].terminator.kind = T::Goto(then_block); + } + 6 => { + let success = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + S::Let { + value: Expression { + kind: E::RefinementValidated(_), + .. + }, + .. + } + ) + }) + }) + .expect("validated output"); + success.statements.push(success.statements[0].clone()); + } + _ => { + let S::CheckRefinement { + call: + Expression { + kind: E::Call { function, .. }, + .. + }, + .. + } = &mut function.blocks[block_index].statements[check_index].kind + else { + unreachable!(); + }; + *function = other_predicate; + } + } + generated_rejects(&program, &types); + } +} + +#[test] +fn caller_acquisition_generated_refinement_rejects_default_failure_bypass_and_borrowed_output() { + let (original, types) = generated_refinement_program(true); + let (output, success) = exercise(&original) + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match statement.kind { + S::Let { + local, + value: + Expression { + kind: E::RefinementValidated(_), + .. + }, + } => Some((local, block.id)), + _ => None, + }) + }) + .expect("first validated output"); + for borrowed in [false, true] { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let default = function + .blocks + .iter() + .find(|block| { + block.id != success && block.statements.iter().any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == output)) + }) + .expect("exact default block") + .id; + if borrowed { + let S::Let { value, .. } = function.blocks[default.index() as usize] + .statements + .iter_mut() + .find(|statement| matches!(statement.kind, S::Let { local, .. } if local == output)) + .map(|statement| &mut statement.kind) + .expect("default owning Let") + else { + unreachable!(); + }; + *value = Expression { + kind: E::View(Box::new(value.clone())), + ty: value.ty, + span: value.span, + }; + } else { + let block = function + .blocks + .iter_mut() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::CheckRefinement { .. })) + }) + .expect("selecting predicate"); + let T::Branch { else_block, .. } = &mut block.terminator.kind else { + panic!("canonical selecting Branch"); + }; + *else_block = default; + } + generated_rejects(&program, &types); + } +} + +const GENERATED_REFINEMENT_LOOP_DEFAULT_SOURCE: &str = r#"namespace app +type Nonempty = list[int64] where true +property refined: + given items: Nonempty + assert true +function candidate() returns Nonempty: + return list(2) +function loop_default(view values: list[secret[Nonempty]]) returns Nonempty: + for item in view values: + return clone declassify item + return list(2) +"#; + +fn generated_refinement_loop_default_program() -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(GENERATED_REFINEMENT_LOOP_DEFAULT_SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let mut high = hir::lower_with_test_bodies( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("checked viewed-loop HIR"); + let property_span = parsed + .module + .items + .iter() + .find_map(|item| match item { + jett_parser::ast::Item::Property(property) if property.name.name == "refined" => { + Some(property.span) + } + _ => None, + }) + .expect("exact parsed property"); + let targets = high + .functions + .iter() + .filter(|function| { + function.span == property_span + && function.identity.declaration.kind == hir::DeclarationKind::Property + && function.identity.declaration.name == format!("refined:{}", property_span.start) + }) + .collect::>(); + let [property] = targets.as_slice() else { + panic!("one exact checked property"); + }; + let property = (*property).clone(); + let fallback = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "loop_default") + .expect("source checked loop") + .clone(); + let original = high + .functions + .iter() + .find(|function| function.identity.declaration.name == "candidate") + .expect("checked refinement candidate"); + let hir::StatementKind::Return(Some(mut candidate)) = original.body.statements[0].kind.clone() + else { + panic!("refinement guard"); + }; + let mut iteration = fallback.body.statements[0].clone(); + let hir::StatementKind::For { body, .. } = &mut iteration.kind else { + panic!("source viewed For"); + }; + let hir::StatementKind::Return(Some(value)) = body.statements[0].kind.clone() else { + panic!("source checked explicit Clone"); + }; + body.statements[0].kind = hir::StatementKind::HandleDefault(value); + let failure_span = iteration.span; + let E::Handle { + error_local, + failure, + .. + } = &mut candidate.kind + else { + panic!("exact candidate Handle"); + }; + *error_local = None; + *failure = hir::Block { + statements: vec![ + iteration, + hir::Statement { + kind: hir::StatementKind::Return(None), + span: failure_span, + }, + ], + span: failure_span, + }; + let args = vec![candidate]; + let order = vec![0]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::NativeSuite { + function: property.id, + }, + &args, + &[property.params[0].ty], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::NOTHING, + &order, + &checked.interner, + ) + .expect("exact replay factory"); + let span = property.span; + let call = Expression { + kind: E::Call { + function: property.id, + args, + evaluation_order: order, + ownership, + }, + ty: TypeInterner::NOTHING, + span, + }; + let mut identity = property.identity.clone(); + identity.declaration.name = "exercise".into(); + high.functions.push(hir::Function { + id: hir::FunctionId::new(high.functions.len() as u32), + identity, + debug_kind: hir::FunctionDebugKind::named("app", "exercise"), + source_definition: None, + actor_handler: None, + actor_state_views: Vec::new(), + mutable_sum_backings: Vec::new(), + params: fallback.params, + capture_count: 0, + return_type: TypeInterner::NOTHING, + locals: fallback.locals, + body: hir::Block { + statements: vec![ + hir::Statement { + kind: hir::StatementKind::Expression(call), + span, + }, + hir::Statement { + kind: hir::StatementKind::Return(None), + span, + }, + ], + span, + }, + span, + }); + hir::validate_backend_types(&high, &checked.interner) + .expect("checked explicit Clone default preserves loop view"); + ( + crate::lower(&high, &checked.interner).expect("canonical replay with viewed-loop default"), + checked.interner, + ) +} + +#[test] +fn caller_acquisition_generated_refinement_defaults_distinguish_logical_loop_borrows_from_clone() { + let (original, types) = generated_refinement_loop_default_program(); + validate_function(&original, exercise(&original), &types) + .expect("explicit ordinary-data Clone acquires the default"); + let function = exercise(&original); + let (binding, body) = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::ForEach { + key, + value: None, + body, + .. + } => Some((*key, *body)), + _ => None, + }) + .expect("original viewed loop binding"); + assert!( + !function.is_view_local(binding), + "loop backing is distinct from ABI/persistent local views" + ); + let target_type = match types.resolve(function.local(binding).unwrap().ty) { + Type::Secret(inner) => *inner, + _ => panic!("exact Secret loop endpoint"), + }; + for wrapped in [false, true] { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let block = &mut function.blocks[body.index() as usize]; + let value = block + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } + if value.ty == target_type && matches!(value.kind, E::Clone(_)) => + { + Some(value) + } + _ => None, + }) + .expect("source explicit Clone default"); + let mut borrowed = Expression { + kind: E::Local(binding), + ty: function.locals[binding.index() as usize].ty, + span: value.span, + }; + if wrapped { + borrowed = Expression { + kind: E::View(Box::new(borrowed)), + ty: function.locals[binding.index() as usize].ty, + span: value.span, + }; + } + *value = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: target_type, + span: value.span, + }; + let error = generated_rejects(&program, &types); + assert!( + error.contains("default lost its exact owned failure occurrence"), + "{error}" + ); + } +} + +fn source_sum_loop_default(result: bool, qualified: bool) -> String { + let mut source = String::from("namespace app\n"); + if qualified { + source.push_str("type Nonempty = list[int64] where true\n"); + } + source.push_str("function count(view values: list[int64]) returns int64:\n return 1\n"); + if result { + source.push_str("function candidate() returns result[list[int64], string]:\n return fail(\"missing\")\n"); + } else { + source.push_str("function candidate() returns optional[list[int64]]:\n return none\n"); + } + let container = if qualified { + "list[secret[Nonempty]]" + } else { + "list[list[int64]]" + }; + let handler = if result { "handle error" } else { "handle" }; + let default = if qualified { + "clone coarsen declassify item" + } else { + "clone item" + }; + source.push_str(&format!("function exercise(view values: {container}) returns int64:\n for item in view values:\n return count(candidate() {handler}:\n default {default}\n )\n return 0\n")); + source +} + +fn source_sum_loop_default_ids(function: &Function) -> (LocalId, LocalId) { + let binding = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + T::ForEach { + key, value: None, .. + } => Some(key), + _ => None, + }) + .expect("one original viewed loop binder"); + let outputs = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .collect::>(); + let [output] = outputs.as_slice() else { + panic!("one exact inhabited sum success output"); + }; + (binding, *output) +} + +fn source_sum_loop_default_value(function: &mut Function, output: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == output => Some(value), + _ => None, + }) + .expect("exact handled default definition") +} + +#[test] +fn caller_acquisition_source_sum_defaults_keep_explicit_clone_and_exact_sum_proofs() { + for result in [false, true] { + for qualified in [false, true] { + let (original, types) = source_program(&source_sum_loop_default(result, qualified)); + let function = exercise(&original); + let (binding, output) = source_sum_loop_default_ids(function); + assert!( + !function.is_view_local(binding), + "logical loop backing is not a persistent/ABI view slot" + ); + let target = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "count") + .expect("checked count target") + .id; + let call = function.blocks.iter().flat_map(|block| &block.statements).find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) if matches!(value.kind, E::Call { function, .. } if function == target) => Some(value), _ => None, + }).expect("exact staged count invocation"); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &call.kind + else { + panic!("real Source packet"); + }; + let hir::ArgumentStaging::Relinquished { owner, .. } = source.arguments[0].staging + else { + panic!("owned handled result to view formal"); + }; + let acquisitions = validate_function(&original, function, &types) + .expect("explicit Clone owns the sum default"); + assert_eq!( + acquisitions + .owner_initializer(owner) + .expect("validated handled owner") + .binding, + None + ); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(&statement.kind, S::Let { local, value } if *local == output && matches!(value.kind, E::Clone(_))))); + let mut prepared = original.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + validate_function(&prepared, exercise(&prepared), &types) + .expect("canonical preparation preserves explicit owned defaults"); + assert!( + exercise(&prepared).prepared_absent_successes.is_empty(), + "inhabited sums gain no absence authority" + ); + } + } +} + +#[test] +fn caller_acquisition_source_sum_defaults_reject_logical_borrows_under_transparent_wrappers() { + for result in [false, true] { + for qualified in [false, true] { + let (original, types) = source_program(&source_sum_loop_default(result, qualified)); + let (binding, output) = source_sum_loop_default_ids(exercise(&original)); + let binding_ty = exercise(&original).local(binding).expect("loop local").ty; + let nominal = if qualified { + let Type::Secret(inner) = types.resolve(binding_ty) else { + panic!("exact Secret loop endpoint"); + }; + Some(*inner) + } else { + None + }; + for hidden_view in [false, true] { + let mut changed = original.clone(); + let value = source_sum_loop_default_value(exercise_mut(&mut changed), output); + let mut borrowed = Expression { + kind: E::Local(binding), + ty: binding_ty, + span: value.span, + }; + if hidden_view { + borrowed = Expression { + kind: E::View(Box::new(borrowed)), + ty: binding_ty, + span: value.span, + }; + } + if let Some(nominal) = nominal { + borrowed = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: nominal, + span: value.span, + }; + borrowed = Expression { + kind: E::Coarsen(Box::new(borrowed)), + ty: value.ty, + span: value.span, + }; + } + *value = borrowed; + let error = validate_function(&changed, exercise(&changed), &types) + .err() + .expect("borrowed default must fail"); + let expected = if hidden_view && !qualified { + "call ownership producer result contains an unsupported definition" + } else { + "call ownership handled default cannot acquire a borrowed owner" + }; + assert_eq!(error, expected); + if !hidden_view { + let mut prepared = original.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + let function = exercise_mut(&mut prepared); + let prepared_binding = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SequenceGet { target, .. } + if function + .local(target) + .is_some_and(|local| local.ty == binding_ty) => + { + Some(target) + } + _ => None, + }) + .expect("canonical viewed element storage"); + let prepared_output = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match statement.kind { + S::SumTake { + target, + success: true, + .. + } => Some(target), + _ => None, + }) + .expect("retained inhabited success output"); + let value = source_sum_loop_default_value(function, prepared_output); + let mut borrowed = Expression { + kind: E::Local(prepared_binding), + ty: binding_ty, + span: value.span, + }; + if let Some(nominal) = nominal { + borrowed = Expression { + kind: E::Declassify(Box::new(borrowed)), + ty: nominal, + span: value.span, + }; + borrowed = Expression { + kind: E::Coarsen(Box::new(borrowed)), + ty: value.ty, + span: value.span, + }; + } + *value = borrowed; + let error = validate_function(&prepared, exercise(&prepared), &types) + .err() + .expect("prepared logical borrow must fail"); + assert_eq!( + error, + "call ownership handled default cannot acquire a borrowed owner" + ); + } + } + } + } +} + +const CONVERTED_HANDLED_SOURCE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function exercise(incoming: optional[uint64], spare: uint64) returns string: + uint64 fallback = 0 + return Show.show(view(incoming handle: + default fallback + )) +"#; + +const OWNED_CONVERTED_HANDLED_SOURCE: &str = r#"namespace app +interface Show: + function show(view self: Show) returns string +implement Show for uint64: + function show(view self: uint64) returns string: + return "uint64" +function read(value: Show) returns string: + return "received" +function exercise(incoming: optional[uint64], spare: uint64) returns string: + uint64 fallback = 0 + return read((incoming handle: + default fallback + )) +"#; + +fn converted_handle_stage(function: &Function) -> Option { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("checked converted source call"); + }; + match packet.arguments[0].staging { + hir::ArgumentStaging::Original => None, + hir::ArgumentStaging::Borrowed { loan } => Some(loan), + hir::ArgumentStaging::Transferred { owner } + | hir::ArgumentStaging::Relinquished { owner, .. } + | hir::ArgumentStaging::RetainedSnapshot { owner, .. } + | hir::ArgumentStaging::Observed { owner } => Some(owner), + hir::ArgumentStaging::Copied { value } => Some(value), + } +} + +fn converted_handle_operand(function: &Function) -> &Expression { + if let Some(stage) = converted_handle_stage(function) { + return function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } | S::BeginCallView { local, value } if *local == stage => { + Some(value) + } + _ => None, + }) + .expect("exact source staging initializer"); + } + let E::Call { args, .. } = &read_call(function).kind else { + panic!("source read"); + }; + &args[0] +} + +fn converted_handle_operand_mut(function: &mut Function) -> &mut Expression { + if let Some(stage) = converted_handle_stage(function) { + return function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } | S::BeginCallView { local, value } if *local == stage => { + Some(value) + } + _ => None, + }) + .expect("exact source staging initializer"); + } + for block in &mut function.blocks { + for statement in &mut block.statements { + if let S::Let { value, .. } | S::Evaluate(value) = &mut statement.kind + && let E::Call { args, .. } = &mut value.kind + { + return &mut args[0]; + } + } + if let T::Return(Some(value)) = &mut block.terminator.kind + && let E::Call { args, .. } = &mut value.kind + { + return &mut args[0]; + } + } + panic!("source read invocation"); +} + +fn converted_handle_leaf_mut(mut value: &mut Expression) -> &mut Expression { + while matches!( + value.kind, + E::View(_) | E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + value = match &mut value.kind { + E::View(inner) + | E::InterfaceCoerce { value: inner, .. } + | E::FunctionAdapter { value: inner, .. } => inner, + _ => unreachable!("transparent conversion checked before borrowing"), + }; + } + value +} + +#[test] +fn caller_acquisition_converted_handle_joins_exact_inner_result_and_full_cfg() { + let (program, types) = source_program(CONVERTED_HANDLED_SOURCE); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("source read"); + }; + let argument = &packet.arguments[0]; + let backing = hir::validate_source_handled_operand( + converted_handle_operand(function), + argument, + &packet.bridge, + &types, + ) + .expect("exact conversion") + .expect("private original Handle backing"); + let E::Local(local) = backing.kind else { + panic!("lowered Handle result"); + }; + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_ne!(backing.span, argument.source_span); + assert_ne!(local, function.params[1].local); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(statement.kind, S::SumTake { target, success: true, .. } if target == local))); + validate_function(&program, function, &types).expect("original result and complete CFG proofs"); + // A written View of a statically selected method retains the inner source + // span. A separate owning interface formal exercises actual conversion. + let (converted, converted_types) = source_program(OWNED_CONVERTED_HANDLED_SOURCE); + let converted_function = exercise(&converted); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(converted_function).kind + else { + panic!("owning interface source call"); + }; + let operand = converted_handle_operand(converted_function); + assert!(matches!(operand.kind, E::InterfaceCoerce { .. })); + let backing = hir::validate_source_handled_operand( + operand, + &packet.arguments[0], + &packet.bridge, + &converted_types, + ) + .expect("actual finite conversion") + .expect("private original Handle"); + assert!(matches!(backing.kind, E::Local(_))); + assert_eq!(backing.ty, TypeInterner::UINT64); + assert_ne!(operand.ty, backing.ty); + validate_function(&converted, converted_function, &converted_types) + .expect("converted result CFG"); +} + +#[test] +fn caller_acquisition_converted_handle_refuses_foreign_occurrences_and_unproved_slots() { + let (program, types) = source_program(CONVERTED_HANDLED_SOURCE); + for mutation in [ + "inner span", + "inner type", + "parameter substitution", + "outer source span", + "sum occurrence", + "failed tag", + ] { + let mut corrupted = program.clone(); + let function = exercise_mut(&mut corrupted); + let spare = function.params[1].local; + let backing = converted_handle_leaf_mut(converted_handle_operand_mut(function)); + let E::Local(output) = backing.kind else { + panic!("original CFG result"); + }; + match mutation { + "inner span" => backing.span.start += 1, + "inner type" => backing.ty = TypeInterner::INT64, + "parameter substitution" => backing.kind = E::Local(spare), + "outer source span" => converted_handle_operand_mut(function).span.start += 1, + "sum occurrence" | "failed tag" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output)) + .expect("exact handled success"); + if mutation == "sum occurrence" { + statement.span.start += 1; + } else { + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!() + }; + *success = false; + } + } + _ => unreachable!(), + } + rejects(&corrupted, &types); + } + let (mut converted, converted_types) = source_program(OWNED_CONVERTED_HANDLED_SOURCE); + let original = converted_handle_operand_mut(exercise_mut(&mut converted)); + let E::InterfaceCoerce { value, .. } = &original.kind else { + panic!("actual conversion"); + }; + original.kind = value.kind.clone(); + rejects(&converted, &converted_types); +} + +const CONVERTED_VIEW_OWNER_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +implement Named for Boxed[int8]: + function name(view self: Boxed[int8]) returns string: + return "narrow" +function read(view value: Named) returns int64: + return 7 +function exercise() returns int64: + return read(Boxed[int8](value: 5)) +"#; + +#[test] +fn caller_acquisition_converted_view_producer_owns_the_complete_endpoint_once() { + let (program, types) = source_program(CONVERTED_VIEW_OWNER_SOURCE); + let function = exercise(&program); + let call = read_call(function); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + args, + .. + } = &call.kind + else { + panic!("checked Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + argument.callee_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert!(matches!( + argument.origin, + hir::CallerOrigin::OwnedExpression + )); + assert!(matches!( + types.resolve(argument.actual_type), + Type::Struct(_) + )); + let (owner, loan) = owning_stage(function); + assert_ne!(owner, loan); + let initializer = observation_initializer(function, owner); + let E::InterfaceCoerce { value: raw, .. } = &initializer.kind else { + panic!("owning storage contains the full checked conversion"); + }; + assert!(matches!(raw.kind, E::StructConstruct { .. })); + assert_eq!(raw.ty, argument.actual_type); + assert_eq!(raw.span, argument.source_span); + assert_ne!(initializer.ty, raw.ty); + assert!(matches!(types.resolve(initializer.ty), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let original_view = Expression { + kind: E::View(Box::new(initializer.clone())), + ty: initializer.ty, + span: initializer.span, + }; + assert!( + crate::call_views::temporary_root(&types, &function.locals, &original_view).is_none(), + "the converted endpoint does not acquire raw-temporary borrowing permission" + ); + assert!( + matches!(&args[0].kind, E::View(value) if matches!(value.kind, E::Local(id) if id == loan)) + ); + let owner_metadata = function.local(owner).expect("independent owning slot"); + assert_eq!(owner_metadata.ty, initializer.ty); + assert_eq!(owner_metadata.debug_ty, initializer.ty); + assert_eq!(owner_metadata.view_source, None); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + let block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))) + }) + .expect("materialized consuming operation"); + let owner_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("one full owner"); + let begin_at = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan), + ) + .expect("loan begins"); + let call_at = block.statements.iter().position(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))).unwrap(); + let end_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan)) + .expect("loan ends"); + assert!(owner_at < begin_at && begin_at < call_at && call_at < end_at); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .count(), + 1 + ); + let acquisitions = + validate_function(&program, function, &types).expect("exact owning conversion and loan"); + assert_eq!(acquisitions.owner_initializers().count(), 1); + let acquired = acquisitions.owner_initializer(owner).unwrap(); + assert_eq!(acquired.binding, None); + assert_eq!(acquired.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!(acquired.actual_type, raw.ty); + assert_eq!(acquired.storage_type, initializer.ty); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("ordinary owning storage and bounded View scope"); +} + +#[test] +fn caller_acquisition_converted_owner_preflight_preserves_written_borrow_paths() { + let written_producer = CONVERTED_VIEW_OWNER_SOURCE.replace( + "return read(Boxed[int8](value: 5))", + "return read(view Boxed[int8](value: 5))", + ); + let (program, types) = source_program(&written_producer); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("written producer Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(argument.staging, hir::ArgumentStaging::Original); + assert_eq!( + validate_function(&program, function, &types) + .expect("original written producer") + .owner_initializers() + .count(), + 0 + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::BeginCallView { .. })), + "no owning-stage permission from written View" + ); + + let source = r#"namespace app +interface Named: + function name(view self: Named) returns string +function read(view value: Named, extra: int64) returns int64: + return extra +function exercise(view value: Named, incoming: optional[int64]) returns int64: + return read(view value, incoming handle: + default 3 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("written parameter Source call"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("retained ABI parameter stages as a borrow"); + }; + assert_eq!( + function.local(loan).unwrap().view_source, + Some(function.params[0].local) + ); + assert_eq!( + function + .local(function.params[0].local) + .unwrap() + .view_source, + None + ); + assert_eq!( + validate_function(&program, function, &types) + .expect("existing written parameter borrow") + .owner_initializers() + .count(), + 0 + ); +} + +#[test] +fn caller_acquisition_converted_view_owner_keeps_original_and_owned_slot_guards() { + let (program, types) = source_program(CONVERTED_VIEW_OWNER_SOURCE); + let (owner, loan) = owning_stage(exercise(&program)); + for mutation in [ + "changed effect", + "missing stage", + "borrowed owner", + "borrowed initializer", + "missing initializer", + "missing conversion", + "foreign loan owner", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "changed effect" | "missing stage" | "borrowed owner" => { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(function).kind + else { + panic!("Source converted call"); + }; + match mutation { + "changed effect" => { + packet.arguments[0].effect = CheckedCallerEffect::RetainBorrow + } + "missing stage" => packet.arguments[0].staging = hir::ArgumentStaging::Original, + "borrowed owner" => { + packet.arguments[0].staging = + hir::ArgumentStaging::Relinquished { owner: loan, loan } + } + _ => unreachable!(), + } + } + "borrowed initializer" | "missing initializer" | "missing conversion" => { + let (block, index) = function.blocks.iter().enumerate().find_map(|(block, body)| + body.statements.iter().position(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)).map(|index| (block, index))) + .expect("original full owning initializer"); + if mutation == "missing initializer" { + function.blocks[block].statements.remove(index); + } else { + let S::Let { value, .. } = &mut function.blocks[block].statements[index].kind + else { + unreachable!(); + }; + if mutation == "missing conversion" { + let E::InterfaceCoerce { value: raw, .. } = &value.kind else { + panic!("original full owning conversion"); + }; + value.kind = raw.kind.clone(); + } else { + value.kind = E::View(Box::new(value.clone())); + } + } + } + "foreign loan owner" => function.locals[loan.index() as usize].view_source = None, + _ => unreachable!(), + } + let expected = match mutation { + "changed effect" => "call ownership disagrees with its original source witness", + "missing stage" => { + "call ownership requires explicit owning staging at this physical boundary" + } + "borrowed owner" => "relinquished call owner cannot be a borrowed slot", + "borrowed initializer" => { + "call ownership owner initializer changes type, occurrence or ownership" + } + "missing initializer" => "call ownership acquired slot requires one unique initializer", + "missing conversion" => { + "call ownership current operand has no typed conversion from the checked actual" + } + "foreign loan owner" => "call view has no borrowed origin", + _ => unreachable!(), + }; + assert_eq!( + validate_function(&changed, exercise(&changed), &types) + .expect_err("original witness and owning storage remain mandatory"), + expected, + "{mutation}" + ); + } +} + +const RETAINED_SNAPSHOT_DIRECT: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable list[int64] values = list(3) + list[int64] spare = list(9) + int64 answer = read(view values, incoming handle: + values = list(8) + default 2 + ) + int64 after = read(view values, 0) + return answer + after +"#; + +const RETAINED_SNAPSHOT_PROJECTED: &str = r#"namespace app +struct Store: + items: list[int64] +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable Store store = Store(items: list(3)) + int64 answer = read(view store.items, incoming handle: + store = Store(items: list(8)) + default 2 + ) + int64 after = read(view store.items, 0) + return answer + after +"#; + +fn retained_snapshot_stage(function: &Function) -> (LocalId, LocalId) { + let ownership = match &read_call(function).kind { + E::Call { ownership, .. } | E::IndirectCall { ownership, .. } => ownership, + _ => panic!("checked source invocation"), + }; + let hir::CallOwnership::Source(source) = ownership else { + panic!("Source packet"); + }; + let argument = &source.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("independent retained snapshot"); + }; + assert_ne!(owner, loan); + (owner, loan) +} + +fn retained_snapshot_initializer(function: &Function, owner: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique retained owner initializer") +} + +fn retained_snapshot_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique retained owner initializer") +} + +fn retained_snapshot_error(program: &Program, types: &TypeInterner) -> String { + validate_function(program, exercise(program), types).expect_err("corrupted retained snapshot") +} + +#[test] +fn caller_acquisition_retained_snapshots_capture_direct_and_projected_endpoint_before_rebinding() { + for (source, projected) in [ + (RETAINED_SNAPSHOT_DIRECT, false), + (RETAINED_SNAPSHOT_PROJECTED, true), + ] { + let (program, types) = source_program(source); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let acquisitions = + validate_function(&program, function, &types).expect("retained snapshot proof"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "caller ownership is retained" + ); + assert!( + acquisitions + .arguments(read_call(function)) + .expect("exact call") + .is_empty() + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + assert!( + function + .local(owner) + .expect("snapshot") + .view_source + .is_none() + ); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint snapshot"); + }; + let E::View(endpoint) = &endpoint.kind else { + panic!("original written view"); + }; + assert_eq!(matches!(endpoint.kind, E::Field { .. }), projected); + assert_eq!(matches!(endpoint.kind, E::Local(_)), !projected); + let owner_block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + }) + .expect("snapshot block"); + let tag_block = function + .blocks + .iter() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTag { .. })) + }) + .expect("later handled argument"); + assert_eq!(owner_block.id, tag_block.id); + let owner_index = owner_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("owner position"); + let begin_index = owner_block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).expect("loan position"); + let tag_index = owner_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("tag position"); + assert!(owner_index < begin_index && begin_index < tag_index); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + ) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("rebind does not free the independent snapshot loan"); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_indirect_and_abort_keep_exact_scope_cleanup() { + let indirect = RETAINED_SNAPSHOT_DIRECT.replace( + " int64 answer = read(view values,", + " function(view list[int64], int64) returns int64 callback = read\n int64 answer = callback(view values,", + ); + let abort = RETAINED_SNAPSHOT_DIRECT.replace(" default 2", " return 11"); + for (source, indirect) in [(&indirect, true), (&abort, false)] { + let (program, types) = source_program(source); + let function = exercise(&program); + let (_, loan) = retained_snapshot_stage(function); + assert_eq!( + matches!(read_call(function).kind, E::IndirectCall { .. }), + indirect + ); + validate_function(&program, function, &types).expect("call and aborted-call scopes"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("retained owner and independent snapshot cleanup"); + if !indirect { + let returned = function.blocks.iter().find(|block| + matches!(&block.terminator.kind, T::Return(Some(value)) if matches!(value.kind, E::Int(11)))) + .expect("original early handler return"); + assert!(returned.statements.iter().any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + )); + } + } +} + +#[test] +fn caller_acquisition_retained_snapshot_rejects_removed_clone_and_changed_source_occurrence() { + let (program, types) = source_program(RETAINED_SNAPSHOT_DIRECT); + let (owner, _) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "remove clone", + "span", + "endpoint type", + "different root", + "effect", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + if mutation == "effect" { + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &mut read_call_mut(function).kind + else { + panic!("source call"); + }; + source.arguments[0].effect = CheckedCallerEffect::RelinquishOwned; + } else { + let other = function + .locals + .iter() + .find(|local| local.name == "spare") + .expect("same-type unused owner") + .id; + let initializer = retained_snapshot_initializer_mut(function, owner); + match mutation { + "remove clone" => { + let E::Clone(endpoint) = &initializer.kind else { + panic!("snapshot"); + }; + *initializer = endpoint.as_ref().clone(); + } + "span" => initializer.span.start += 1, + "endpoint type" => { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + endpoint.ty = TypeInterner::BOOL; + } + "different root" => { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + let E::View(endpoint) = &mut endpoint.kind else { + panic!("written view"); + }; + endpoint.kind = E::Local(other); + } + _ => unreachable!(), + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("snapshot") || error.contains("call ownership"), + "{mutation}: {error}" + ); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_rejects_loan_substitution_missing_end_and_late_capture() { + let (program, types) = source_program(RETAINED_SNAPSHOT_DIRECT); + let (owner, loan) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "loan source", + "missing end", + "late capture", + "aliased owner", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + match mutation { + "loan source" => { + function.locals[loan.index() as usize].view_source = Some(function.params[0].local) + } + "aliased owner" => { + function.locals[owner.index() as usize].view_source = Some(function.params[0].local) + } + "missing end" => { + for block in &mut function.blocks { + block.statements.retain(|statement| !matches!(statement.kind, S::EndCallView { local } if local == loan)); + } + } + "late capture" => { + let block = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)) + }) + .expect("capture block"); + let at = block.statements.iter().position(|statement| + matches!(statement.kind, S::Let { local, .. } if local == owner)).expect("capture index"); + let capture = block.statements.remove(at); + let begin = block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).expect("loan"); + let begin = block.statements.remove(begin); + // Keep the loan valid on both handler arms, but move capture + // after the later source's SumTag evaluation. Lexical snapshot + // order must be proved independently from call dominance. + assert!( + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::SumTag { .. })) + ); + block.statements.push(capture); + block.statements.push(begin); + } + _ => unreachable!(), + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("snapshot") + || error.contains("call ownership") + || error.contains("call view"), + "{mutation}: {error}" + ); + } +} + +#[test] +fn caller_acquisition_retained_snapshot_metadata_remaps_both_slots_without_source_reads() { + let (program, types) = source_program(RETAINED_SNAPSHOT_PROJECTED); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let E::Call { ownership, .. } = &read_call(function).kind else { + panic!("source call"); + }; + let mut changed = ownership.clone(); + let mut ids = Vec::new(); + changed.metadata_local_ids(|id| ids.push(id)); + assert!(ids.contains(&owner) && ids.contains(&loan)); + changed + .remap_metadata_locals(|id| Ok::<_, ()>(LocalId::new(id.index() + 1))) + .expect("structural remap"); + let hir::CallOwnership::Source(source) = changed else { + panic!("Source"); + }; + assert_eq!( + source.arguments[0].staging, + hir::ArgumentStaging::RetainedSnapshot { + owner: LocalId::new(owner.index() + 1), + loan: LocalId::new(loan.index() + 1), + } + ); + assert_eq!( + source.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + source.arguments[0].retained_snapshot_type(), + Some(source.arguments[0].source_witness().occurrence_type()) + ); + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("actual canonical remaps keep snapshot proof"); + crate::move_values::MoveValuePlan::analyze(&prepared, exercise(&prepared), &types) + .expect("metadata does not reread the rebound source at the call"); +} + +#[test] +fn caller_acquisition_retained_snapshot_does_not_grant_descriptor_signature_clone_authority() { + let source = r#"namespace app +function value() returns int64: + return 7 +function read(view callback: function() returns int64, extra: int64) returns int64: + return callback() + extra +function exercise(incoming: optional[int64]) returns int64: + function() returns int64 callback = value + return read(view callback, incoming handle: + default 2 + ) +"#; + let (program, types) = source_program(source); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("descriptor source call"); + }; + assert!(source.arguments[0].retained_snapshot_type().is_none()); + assert!(matches!( + source.arguments[0].staging, + hir::ArgumentStaging::Borrowed { .. } + )); + let mut ownership = hir::CallOwnership::Source(source.clone()); + assert!( + ownership + .stage_retained_snapshot(0, LocalId::new(100), LocalId::new(101)) + .is_err() + ); + validate_function(&program, function, &types) + .expect("existing descriptor raw loan, no clone authority"); + + let produced = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + return extra +function exercise(incoming: optional[int64]) returns int64: + return read(view list(3), incoming handle: + default 2 + ) +"#; + let (program, types) = source_program(produced); + let function = exercise(&program); + let E::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &read_call(function).kind + else { + panic!("written producer Source call"); + }; + assert_eq!( + source.arguments[0].origin, + hir::CallerOrigin::OwnedExpression + ); + assert!(source.arguments[0].retained_snapshot_type().is_none()); + assert_eq!(source.arguments[0].staging, hir::ArgumentStaging::Original); + let mut ownership = hir::CallOwnership::Source(source.clone()); + assert!( + ownership + .stage_retained_snapshot(0, LocalId::new(100), LocalId::new(101)) + .is_err() + ); + validate_function(&program, function, &types) + .expect("existing fresh producer backing, no snapshot permission from type/span"); +} + +fn transparent_retained_source(secret: bool, projected: bool) -> String { + let declaration = if secret { + "" + } else { + "type Numbers = list[int64] where true\n" + }; + let ty = if secret { + "secret[list[int64]]" + } else { + "Numbers" + }; + let wrapper = if secret { "declassify" } else { "coarsen" }; + let (holder, input_ty, setup, endpoint, rebind) = if projected { + ( + format!("struct Store:\n items: {ty}\n"), + "Store", + " mutable Store store = input\n", + "store.items", + "store = replacement", + ) + } else { + ( + String::new(), + ty, + " mutable ", + "values", + "values = replacement", + ) + }; + let setup = if projected { + setup.to_string() + } else { + format!("{setup}{ty} values = input\n") + }; + format!( + "namespace app\n{declaration}{holder}function read(view values: list[int64], extra: int64) returns int64:\n mutable int64 total = extra\n for value in view values:\n total = total + value\n return total\nfunction exercise(input: {input_ty}, replacement: {input_ty}, spare: {input_ty}, incoming: optional[int64]) returns int64:\n{setup} int64 answer = read(view({wrapper} {endpoint}), incoming handle:\n {rebind}\n default 2\n )\n int64 after = read(view({wrapper} {endpoint}), 0)\n return answer + after\n" + ) +} + +fn transparent_retained_program(source: &str, release: bool) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("exact checked transparent-view Source HIR"); + let program = crate::lower(&high, &checked.interner) + .expect("retained transparent place keeps its original Source backing"); + (program, checked.interner) +} + +fn transparent_retained_leaf(mut value: &Expression, secret: bool, projected: bool) -> LocalId { + let E::Clone(endpoint) = &value.kind else { + panic!("one full-endpoint snapshot Clone"); + }; + value = endpoint; + let E::View(endpoint) = &value.kind else { + panic!("original written View"); + }; + value = endpoint; + value = match (&value.kind, secret) { + (E::Declassify(inner), true) | (E::Coarsen(inner), false) => inner, + _ => panic!("original checked qualifier conversion"), + }; + if projected { + let E::Field { base, .. } = &value.kind else { + panic!("original projected endpoint"); + }; + value = base; + } + let E::Local(local) = value.kind else { + panic!("exact original Local backing, no introduced inner Clone"); + }; + local +} + +fn transparent_retained_leaf_mut(mut value: &mut Expression) -> &mut Expression { + while !matches!(value.kind, E::Local(_)) { + value = match &mut value.kind { + E::Clone(inner) | E::View(inner) | E::Coarsen(inner) | E::Declassify(inner) => inner, + E::Field { base, .. } => base, + _ => panic!("checked finite place spine"), + }; + } + value +} + +fn assert_transparent_retained_scope( + program: &Program, + types: &TypeInterner, + secret: bool, + projected: bool, +) { + let function = exercise(program); + let (owner, loan) = retained_snapshot_stage(function); + let original = transparent_retained_leaf( + retained_snapshot_initializer(function, owner), + secret, + projected, + ); + let source_name = if projected { "store" } else { "values" }; + assert_eq!( + function.local(original).expect("original binding").name, + source_name + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquisitions = + validate_function(program, function, types).expect("unchanged Source place proof"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "caller retains its owner" + ); + assert!( + acquisitions + .arguments(read_call(function)) + .expect("exact source call") + .is_empty() + ); + let block = function + .blocks + .iter() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + }) + .expect("capture block"); + let capture = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .expect("capture"); + let begin = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan), + ) + .expect("loan begins"); + let tag = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("later handled selection"); + assert!( + capture < begin && begin < tag, + "snapshot before later handler work" + ); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan)) + ); + crate::move_values::MoveValuePlan::analyze(program, function, types) + .expect("independent snapshot survives original owner rebind or abort"); +} + +#[test] +fn caller_acquisition_retained_transparent_direct_places_survive_later_rebind() { + for secret in [false, true] { + let source = transparent_retained_source(secret, false); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + assert_transparent_retained_scope(&program, &types, secret, false); + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_projected_places_snapshot_only_the_endpoint() { + for secret in [false, true] { + let source = transparent_retained_source(secret, true); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + assert_transparent_retained_scope(&program, &types, secret, true); + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_indirect_and_return_keep_exact_loan_scope() { + for secret in [false, true] { + let direct = transparent_retained_source(secret, false); + let indirect = direct.replace(" int64 answer = read(view(", + " function(view list[int64], int64) returns int64 callback = read\n int64 answer = callback(view("); + let aborted = direct.replace(" default 2", " return 11"); + for (source, is_indirect) in [(&indirect, true), (&aborted, false)] { + for release in [false, true] { + let (program, types) = transparent_retained_program(source, release); + assert_transparent_retained_scope(&program, &types, secret, false); + assert_eq!( + matches!(read_call(exercise(&program)).kind, E::IndirectCall { .. }), + is_indirect + ); + if !is_indirect { + let (_, loan) = retained_snapshot_stage(exercise(&program)); + let returned = exercise(&program).blocks.iter().find(|block| + matches!(&block.terminator.kind, T::Return(Some(value)) if matches!(value.kind, E::Int(11)))) + .expect("original handler early return"); + assert!(returned.statements.iter().any(|statement| + matches!(statement.kind, S::EndCallView { local } if local == loan))); + } + } + } + } +} + +#[test] +fn caller_acquisition_retained_transparent_spines_reject_added_clone_and_foreign_backing() { + for (secret, projected) in [(false, false), (true, false), (false, true), (true, true)] { + let source = transparent_retained_source(secret, projected); + let (program, types) = transparent_retained_program(&source, false); + let (owner, _) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "inner clone", + "foreign backing", + "endpoint type", + "endpoint span", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let spare = function.params[2].local; + let initializer = retained_snapshot_initializer_mut(function, owner); + if matches!(mutation, "endpoint type" | "endpoint span") { + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + assert!(matches!(endpoint.kind, E::View(_)), "written View"); + if mutation == "endpoint type" { + endpoint.ty = TypeInterner::BOOL; + } else { + endpoint.span.start += 1; + } + } else { + let leaf = transparent_retained_leaf_mut(initializer); + if mutation == "inner clone" { + leaf.kind = E::Clone(Box::new(leaf.clone())); + } else { + leaf.kind = E::Local(spare); + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("call ownership") || error.contains("snapshot"), + "{mutation}: {error}" + ); + } + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("exact transparent place survives canonical remaps"); + } +} + +#[test] +fn caller_acquisition_retained_transparent_places_keep_known_borrow_and_owned_formal_policy() { + for (ty, declaration, wrapper) in [ + ( + "Numbers", + "type Numbers = list[int64] where true\n", + "coarsen", + ), + ("secret[list[int64]]", "", "declassify"), + ] { + for (owned_formal, expected) in [(false, 401), (true, 375)] { + let formal = if owned_formal { "" } else { "view " }; + let argument = if owned_formal { + format!("view({wrapper} values)") + } else { + format!("{wrapper} values") + }; + let source = format!( + "namespace app\n{declaration}function read({formal}values: list[int64], extra: int64) returns int64:\n return extra\nfunction rejected(view values: {ty}, incoming: optional[int64]) returns int64:\n return read({argument}, incoming handle:\n default 2\n )\n" + ); + for release in [false, true] { + let parsed = jett_parser::parse(&source, jett_common::FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + let errors = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{source}: {errors:?}"); + assert_eq!(errors[0].code.code(), expected, "{source}: {errors:?}"); + } + } + } +} + +const RETAINED_PHYSICAL_PIPELINE: &str = r#"namespace app +function read(view values: list[int64], extra: int64) returns int64: + mutable int64 total = extra + for value in view values: + total = total + value + return total +function exercise(incoming: optional[int64]) returns int64: + mutable list[int64] values = list(3) + list[int64] spare = list(9) + int64 answer = values into view read(incoming handle: + values = list(8) + default 2 + ) + return answer + read(view values, 0) +"#; + +#[test] +fn caller_acquisition_retained_physical_pipeline_span_preserves_raw_source_and_order() { + for release in [false, true] { + let (program, types) = transparent_retained_program(RETAINED_PHYSICAL_PIPELINE, release); + let function = exercise(&program); + let (owner, loan) = retained_snapshot_stage(function); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + args, + evaluation_order, + .. + } = &read_call(function).kind + else { + panic!("real source pipeline"); + }; + let argument = &packet.arguments[0]; + let physical = argument + .retained_snapshot_span() + .expect("private initial physical occurrence"); + assert_ne!(physical, argument.source_span); + assert_eq!(evaluation_order, &[0, 1]); + assert_eq!(argument.source_index, 0); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(args[0].span, physical); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("complete snapshot"); + }; + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + let E::View(raw) = &endpoint.kind else { + panic!("initial physical View"); + }; + assert_eq!(raw.span, argument.source_span); + let E::Local(original) = raw.kind else { + panic!("raw original binding"); + }; + assert_eq!( + function.local(original).expect("source binding").name, + "values" + ); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + let acquired = + validate_function(&program, function, &types).expect("physical and raw joins"); + assert_eq!(acquired.owner_initializers().count(), 0); + assert!( + acquired + .arguments(read_call(function)) + .expect("call") + .is_empty() + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("snapshot before later handler rebind"); + let mut prepared = program.clone(); + crate::prepare_native_sequences(&mut prepared, &types); + crate::prepare_native_uninhabited_sums(&mut prepared, &types); + crate::prepare_native_generated_functions(&mut prepared, &types); + crate::validate_call_ownership(&prepared, &types) + .expect("no span rewrite in structural remaps"); + } +} + +#[test] +fn caller_acquisition_retained_physical_reflected_getter_keeps_initial_view_and_raw_binding() { + let original_source = r#"namespace app +struct Record: + values: list[int64] +function exercise(view source: Record, view field: TypeField) returns list[int64]: + return source into view type.field_value[Record, list[int64]](view field) +"#; + // Match the existing recursive object regression: non-Exact field plans + // enter ordered call staging before the raw read and refinement checks. + let ordered_source = r#"namespace app +type Positive = int64 where value > 0 +struct Record: + values: list[optional[result[map[int64, list[int64]], set[int64]]]] +function exercise(view source: Record, view field: TypeField) returns list[optional[result[map[Positive, list[Positive]], set[Positive]]]]: + return source into view type.field_value[Record, list[optional[result[map[Positive, list[Positive]], set[Positive]]]]](view field) +"#; + for release in [false, true] { + for (source, ordered) in [(original_source, false), (ordered_source, true)] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let call = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) => + { + Some(value) + } + _ => None, + }) + .or_else(|| { + function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + T::Return(Some(value)) + if matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) => + { + Some(value) + } + _ => None, + }) + }) + .expect("real reflected getter invocation"); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + args, + .. + } = &call.kind + else { + panic!("unchanged reflected Source packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + if !ordered { + // Exact field plans use the existing unstaged raw getter path. + assert_eq!(argument.staging, hir::ArgumentStaging::Original); + let E::View(raw) = &args[0].kind else { + panic!("initial written physical View"); + }; + assert!(matches!(raw.kind, E::Local(local) if local == function.params[0].local)); + validate_function(&program, function, &types) + .expect("exact field getter stays Original"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("existing unstaged reflected read"); + continue; + } + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("ordinary Record endpoint snapshot"); + }; + let physical = argument + .retained_snapshot_span() + .expect("initial pipeline occurrence"); + assert_ne!(physical, argument.source_span); + assert_eq!(args[0].span, physical); + let initializer = retained_snapshot_initializer(function, owner); + let E::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint snapshot"); + }; + let E::View(raw) = &endpoint.kind else { + panic!("original written physical View"); + }; + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + assert_eq!(raw.span, argument.source_span); + assert!(matches!(raw.kind, E::Local(local) if local == function.params[0].local)); + assert_eq!(function.local(loan).expect("loan").view_source, Some(owner)); + validate_function(&program, function, &types) + .expect("reflected certificate and exact raw source stay active"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("existing reflected owning-result/read analysis"); + } + } +} + +#[test] +fn caller_acquisition_retained_physical_span_does_not_replace_raw_or_backing_authority() { + let (program, types) = transparent_retained_program(RETAINED_PHYSICAL_PIPELINE, false); + let (owner, loan) = retained_snapshot_stage(exercise(&program)); + for mutation in [ + "physical occurrence", + "raw occurrence", + "foreign binding", + "physical loan", + "public raw witness", + ] { + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + if mutation == "public raw witness" { + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut read_call_mut(function).kind + else { + panic!("source pipeline"); + }; + packet.arguments[0].source_span.start += 1; + } else if mutation == "physical loan" { + let E::Call { args, .. } = &mut read_call_mut(function).kind else { + panic!("call"); + }; + args[0].span.start += 1; + } else if mutation == "physical occurrence" { + let span = { + let initializer = retained_snapshot_initializer_mut(function, owner); + initializer.span.start += 1; + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + endpoint.span = initializer.span; + initializer.span + }; + // Preserve owner/initializer agreement so only the sealed original + // physical occurrence can reject this coordinated replacement. + function.locals[owner.index() as usize].span = span; + } else { + let spare = function + .locals + .iter() + .find(|local| local.name == "spare") + .expect("same-type source") + .id; + let initializer = retained_snapshot_initializer_mut(function, owner); + let E::Clone(endpoint) = &mut initializer.kind else { + panic!("snapshot"); + }; + let E::View(raw) = &mut endpoint.kind else { + panic!("physical pipeline View"); + }; + if mutation == "raw occurrence" { + raw.span.start += 1; + } else { + raw.kind = E::Local(spare); + } + } + let error = retained_snapshot_error(&changed, &types); + assert!( + error.contains("retained snapshot") || error.contains("call ownership"), + "{mutation}: {error}" + ); + } + assert_eq!( + exercise(&program) + .local(loan) + .expect("original loan") + .view_source, + Some(owner) + ); +} +const FLOW_PROJECTED_SOURCE: &str = r#"namespace app +machine Session: + states: + active(items: list[int64]) + cached(items: list[int64]) + empty +function read(view values: list[int64]) returns int64: + return 7 +function exercise(source: Session, other: Session) returns int64: + if source at active: + return read(view source.items) + return 0 +machine TaskState: + states: + ready(value: nothing) + cached(value: nothing) + empty +function depth(value: nothing) returns int64: + return 3 +function copied(source: TaskState) returns int64: + if source at ready: + return depth(source.value) + return 0 +"#; + +fn flow_projected_mir_field(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, E::View(_)) { + let E::View(inner) = &mut value.kind else { + unreachable!() + }; + return flow_projected_mir_field(inner); + } + assert!(matches!(value.kind, E::Field { .. })); + value +} + +fn flow_projected_mir_call_mut(function: &mut Function) -> &mut Expression { + for block in &mut function.blocks { + if let T::Return(Some(value)) = &mut block.terminator.kind { + if matches!(value.kind, E::Call { .. }) { + return value; + } + } + } + panic!("guarded original source call") +} + +#[test] +fn caller_acquisition_flow_projection_keeps_bare_storage_and_exact_original_state() { + let (mut program, types) = source_program(FLOW_PROJECTED_SOURCE); + for name in ["exercise", "copied"] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap(); + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("guarded source packet"); + }; + let root = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + assert!(matches!( + types.resolve(root.2), + jett_types::Type::MachineState { .. } + )); + assert!(matches!( + types.resolve(root.3), + jett_types::Type::Machine(_) + )); + assert_eq!(function.local(root.0).unwrap().ty, root.3); + assert_eq!( + packet.arguments[0].effect, + if name == "exercise" { + CheckedCallerEffect::RetainBorrow + } else { + CheckedCallerEffect::Copy + } + ); + assert!(matches!( + packet.arguments[0].staging, + hir::ArgumentStaging::Original + )); + validate_function(&program, function, &types) + .expect("original flow projection Source proof"); + } + crate::prepare_native_sequences(&mut program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types) + .expect("dense remaps preserve private root without a new loan"); +} + +#[test] +fn caller_acquisition_flow_projection_rejects_changed_state_root_span_and_endpoint() { + let (program, mut types) = source_program(FLOW_PROJECTED_SOURCE); + let function = exercise(&program); + let other = function + .locals + .iter() + .find(|local| local.name == "other") + .unwrap() + .id; + let E::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &read_call(function).kind + else { + panic!("source packet"); + }; + let root = packet.arguments[0] + .source_witness() + .projection_root() + .unwrap(); + let jett_types::Type::Machine(machine) = *types.resolve(root.3) else { + panic!("bare owner"); + }; + let cached = types.resolve_machine(machine).state_id("cached").unwrap(); + let sibling = types.intern(jett_types::Type::MachineState { + machine, + state: cached, + }); + for mutation in ["state", "span", "local", "field", "endpoint"] { + let mut changed = program.clone(); + let E::Call { args, .. } = + &mut flow_projected_mir_call_mut(exercise_mut(&mut changed)).kind + else { + panic!("source call"); + }; + let value = flow_projected_mir_field(&mut args[0]); + if mutation == "endpoint" { + value.ty = TypeInterner::BOOL; + } else { + let E::Field { + base, + owner_type, + field, + } = &mut value.kind + else { + panic!("field"); + }; + match mutation { + "state" => { + *owner_type = sibling; + base.ty = sibling; + } + "span" => base.span.start += 1, + "local" => base.kind = E::Local(other), + "field" => *field = hir::FieldId::new(1), + _ => unreachable!(), + } + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "{mutation}" + ); + } +} + +const MAP_GET_PUBLIC_DECLARATION: &str = "export function get[K, V](items: map[K, V], view key: K) returns optional[V]:\n return map.__get[K, V](items, view key)\n"; + +fn intrinsic_owning_map_program(release: bool) -> (Program, TypeInterner) { + assert!(include_str!("../../../../stdlib/map.jett").contains(MAP_GET_PUBLIC_DECLARATION)); + let stdlib_file = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let project_file = jett_common::FileId::new(0); + let stdlib_source = format!( + "namespace map\nexport interface Named:\n function name(view self: Named) returns string\n{MAP_GET_PUBLIC_DECLARATION}function retained(view items: map[string, Named], incoming: optional[string]) returns optional[Named]:\n return map.__get[string, Named](view items, incoming handle:\n default \"key\"\n )\n" + ); + let project_source = "namespace app\nfunction exercise(items: map[string, map.Named], view key: string) returns optional[map.Named]:\n use map\n return map.get[string, map.Named](items, view key)\n"; + let mut stdlib = jett_parser::parse(&stdlib_source, stdlib_file); + let mut project = jett_parser::parse(project_source, project_file); + assert!(stdlib.errors.is_empty(), "{:?}", stdlib.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + stdlib.module.items.append(&mut project.module.items); + let resolved = jett_resolve::resolve(&stdlib.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &stdlib.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (stdlib_file, jett_common::SourceOrigin::Stdlib), + (project_file, jett_common::SourceOrigin::Project), + ]); + let high = hir::lower(&stdlib.module, &resolved, &checked, &origins) + .expect("original compiler-origin facade and ordinary source caller"); + ( + crate::lower(&high, &checked.interner).expect("closed intrinsic owning staging"), + checked.interner, + ) +} + +fn map_get_function(program: &Program) -> &Function { + let mut matches = program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "get"); + let function = matches.next().expect("concrete public map.get body"); + assert!( + matches.next().is_none(), + "one selected public instantiation" + ); + function +} + +fn map_get_function_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "get") + .expect("concrete public map.get body") +} + +fn map_get_intrinsic(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::MapGet, + .. + } + ) + .then_some(value) + }) + .expect("materialized exact MapGet operation") +} + +fn map_get_intrinsic_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &mut statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::MapGet, + .. + } + ) + .then_some(value) + }) + .expect("materialized exact MapGet operation") +} + +#[test] +fn caller_acquisition_intrinsic_map_get_transfers_opaque_containing_owner_without_clone() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let function = map_get_function(&program); + let call = map_get_intrinsic(function); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + args, + evaluation_order, + .. + } = &call.kind + else { + panic!("exact checked intrinsic packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!(argument.syntax, jett_typecheck::CheckedCallerSyntax::Bare); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + argument.callee_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!(evaluation_order.as_slice(), &[0, 1]); + let hir::ArgumentStaging::Relinquished { owner, loan } = argument.staging else { + panic!("source owning transfer is staged"); + }; + assert_ne!(owner, loan); + let initializer = observation_initializer(function, owner); + assert!(matches!(initializer.kind, E::Local(local) if local == function.params[0].local)); + assert_eq!(initializer.ty, argument.actual_type); + assert_eq!(initializer.span, argument.source_span); + let Type::Map(key, value) = types.resolve(initializer.ty) else { + panic!("exact map owner"); + }; + assert_eq!(*key, TypeInterner::STRING); + assert!(matches!(types.resolve(*value), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + assert_eq!(function.local(owner).unwrap().view_source, None); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + assert!( + matches!(&args[0].kind, E::View(value) if matches!(value.kind, E::Local(id) if id == loan)) + ); + assert_eq!( + packet.arguments[1].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + let hir::ArgumentStaging::Copied { value: key_owner } = packet.arguments[1].staging else { + panic!("the implicitly copyable string key keeps its separate copied slot"); + }; + let key_initializer = observation_initializer(function, key_owner); + assert_eq!(key_initializer.ty, TypeInterner::STRING); + assert!(matches!(key_initializer.kind, E::Clone(_))); + let block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))) + }) + .expect("one consuming operation owns both loans"); + let owner_at = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .unwrap(); + let begin_at = block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).unwrap(); + let call_at = block.statements.iter().position(|statement| + matches!(&statement.kind, S::Let { value, .. } if std::ptr::eq(value, call))).unwrap(); + let end_at = block + .statements + .iter() + .position( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan), + ) + .unwrap(); + assert!(owner_at < begin_at && begin_at < call_at && call_at < end_at); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner) + ) + .count(), + 1 + ); + let acquisitions = validate_function(&program, function, &types) + .expect("original map move and exact loan"); + assert_eq!(acquisitions.owner_initializers().count(), 1); + let acquisition = acquisitions.owner_initializer(owner).unwrap(); + assert_eq!(acquisition.binding, Some(function.params[0].local)); + assert_eq!(acquisition.effect, CheckedCallerEffect::RelinquishOwned); + let E::Call { + ownership: hir::CallOwnership::Source(public), + .. + } = &read_call(exercise(&program)).kind + else { + panic!("ordinary caller preserves public source ownership"); + }; + assert_eq!( + public.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + assert_eq!( + public.arguments[0].callee_access, + jett_typecheck::CheckedCalleeAccess::Owned + ); + crate::validate_call_ownership(&program, &types) + .expect("all source and intrinsic packets remain exact"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("map owner consumed once and released after its scoped borrow"); + } +} + +#[test] +fn caller_acquisition_intrinsic_map_get_keeps_retained_maps_borrowed_across_later_handler() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "retained") + .expect("written view control"); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + .. + } = &map_get_intrinsic(function).kind + else { + panic!("exact intrinsic packet"); + }; + let argument = &packet.arguments[0]; + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("retained map gains only a scoped loan"); + }; + assert_eq!( + function.local(loan).unwrap().view_source, + Some(function.params[0].local) + ); + assert!(!crate::handlers::can_snapshot_view( + &types, + argument.actual_type + )); + let acquisitions = + validate_function(&program, function, &types).expect("retained map and handled key"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "neither the retained map nor the copied key transfers a source owner" + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + let hir::ArgumentStaging::Copied { value: copied } = packet.arguments[1].staging else { + panic!("the handled string key uses its own copied slot"); + }; + assert_eq!( + observation_initializer(function, copied).ty, + TypeInterner::STRING + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("later handler does not consume the retained map"); + } +} + +#[test] +fn caller_acquisition_intrinsic_map_get_refuses_missing_or_forged_owner_staging() { + for release in [false, true] { + let (program, types) = intrinsic_owning_map_program(release); + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + .. + } = &map_get_intrinsic(map_get_function(&program)).kind + else { + panic!("source intrinsic packet"); + }; + let hir::ArgumentStaging::Relinquished { owner, loan } = packet.arguments[0].staging else { + panic!("original owning stage"); + }; + for mutation in [ + "stage", + "effect", + "aliased owner", + "added clone", + "missing owner", + "missing end", + "loan backing", + "order", + ] { + let mut changed = program.clone(); + let function = map_get_function_mut(&mut changed); + match mutation { + "stage" | "effect" | "aliased owner" | "order" => { + let E::Intrinsic { + ownership: hir::CallOwnership::Source(packet), + evaluation_order, + .. + } = &mut map_get_intrinsic_mut(function).kind + else { + panic!("source intrinsic packet"); + }; + match mutation { + "stage" => packet.arguments[0].staging = hir::ArgumentStaging::Original, + "effect" => packet.arguments[0].effect = CheckedCallerEffect::RetainBorrow, + "aliased owner" => { + packet.arguments[0].staging = + hir::ArgumentStaging::Relinquished { owner: loan, loan } + } + "order" => *evaluation_order = vec![0, 0], + _ => unreachable!(), + } + } + "added clone" => { + let initializer = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == owner => Some(value), + _ => None, + }) + .expect("unique original owner initializer"); + initializer.kind = E::Clone(Box::new(initializer.clone())); + } + "missing owner" | "missing end" => { + let (block, index) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, body)| { + body.statements + .iter() + .position(|statement| match statement.kind { + S::Let { local, .. } if mutation == "missing owner" => { + local == owner + } + S::EndCallView { local } if mutation == "missing end" => { + local == loan + } + _ => false, + }) + .map(|index| (block, index)) + }) + .expect("original owner or scope endpoint"); + function.blocks[block].statements.remove(index); + } + "loan backing" => { + function.locals[loan.index() as usize].view_source = + Some(function.params[0].local) + } + _ => unreachable!(), + } + assert!( + validate_function(&changed, map_get_function(&changed), &types).is_err(), + "{mutation}" + ); + } + } +} + +const HANDLED_VIEW_DIRECT_SOURCE: &str = r#"namespace app +struct Item: + values: list[int64] +function read(view value: Item, extra: int64) returns int64: + return extra +function exercise(incoming: optional[Item], later: optional[int64]) returns int64: + return read(view(incoming handle: + default Item(values: list(7)) + ), later handle: + default 3 + ) +"#; + +const HANDLED_VIEW_CONVERTED_SOURCE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +struct Item: + values: list[int64] +implement Named for Item: + function name(view self: Item) returns string: + return "item" +function read(view value: Named, extra: int64) returns int64: + return extra +function exercise(incoming: optional[Item], later: optional[int64]) returns int64: + return read(view(incoming handle: + default Item(values: list(7)) + ), later handle: + default 3 + ) +"#; + +fn handled_view_read_call(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!(&value.kind, E::Call { ownership: hir::CallOwnership::Source(packet), .. } + if packet.arguments.len() == 2) + .then_some(value) + }) + .expect("the source read invocation, not its zero-argument producer") +} + +fn handled_view_read_call_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &mut statement.kind else { + return None; + }; + matches!(&value.kind, E::Call { ownership: hir::CallOwnership::Source(packet), .. } + if packet.arguments.len() == 2) + .then_some(value) + }) + .expect("the materialized source read invocation") +} + +fn handled_view_packet(expression: &Expression) -> &hir::SourceCallOwnership { + let ownership = match &expression.kind { + E::Call { ownership, .. } + | E::IndirectCall { ownership, .. } + | E::Intrinsic { ownership, .. } => ownership, + _ => panic!("source invocation"), + }; + let hir::CallOwnership::Source(packet) = ownership else { + panic!("original checked Source packet"); + }; + packet +} + +fn handled_view_storage( + function: &Function, + call: &Expression, + parameter: usize, +) -> (LocalId, LocalId) { + let packet = handled_view_packet(call); + let argument = &packet.arguments[parameter]; + assert_eq!(argument.origin, hir::CallerOrigin::OwnedExpression); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + argument.physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!(argument.retained_snapshot_type(), None); + let hir::ArgumentStaging::Borrowed { loan } = argument.staging else { + panic!("handled source producer borrows its own storage"); + }; + let owner = function + .local(loan) + .expect("loan") + .view_source + .expect("exact owner"); + assert_ne!(owner, loan); + assert_eq!(function.local(owner).expect("owner").view_source, None); + let initializer = observation_initializer(function, owner); + assert!(!matches!(initializer.kind, E::Clone(_) | E::View(_))); + assert_eq!(function.local(owner).unwrap().span, initializer.span); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .count(), + 1 + ); + (owner, loan) +} + +fn handled_view_projection(function: &Function, loan: LocalId) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let S::BeginCallView { local, value } = &statement.kind else { + return None; + }; + (*local == loan).then_some(value) + }) + .expect("unique Begin") +} + +fn handled_view_projection_mut(function: &mut Function, loan: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::BeginCallView { local, value } = &mut statement.kind else { + return None; + }; + (*local == loan).then_some(value) + }) + .expect("unique Begin") +} + +fn handled_view_initializer_mut(function: &mut Function, owner: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let S::Let { local, value } = &mut statement.kind else { + return None; + }; + (*local == owner).then_some(value) + }) + .expect("unique owner Let") +} + +fn handled_view_raw_result<'a>( + function: &'a Function, + call: &Expression, + parameter: usize, + types: &TypeInterner, +) -> &'a Expression { + let (owner, loan) = handled_view_storage(function, call, parameter); + let packet = handled_view_packet(call); + let argument = &packet.arguments[parameter]; + let initializer = observation_initializer(function, owner); + // Authenticate the original converted initializer directly. The ordinary + // direct path instead restores the original physical View over its result. + if matches!( + initializer.kind, + E::InterfaceCoerce { .. } | E::FunctionAdapter { .. } + ) { + return hir::validate_source_handled_operand(initializer, argument, &packet.bridge, types) + .expect("original conversion") + .expect("private Handle occurrence"); + } + let projection = handled_view_projection(function, loan); + let original = crate::call_views::replace_borrowed_root(projection, initializer.clone()) + .expect("exact original temporary root"); + let backing = hir::validate_source_handled_operand(&original, argument, &packet.bridge, types) + .expect("original written View") + .expect("private Handle occurrence"); + assert_eq!(backing.ty, initializer.ty); + assert_eq!(backing.span, initializer.span); + assert_eq!(backing.kind, initializer.kind); + initializer +} + +#[test] +fn caller_acquisition_handled_written_view_owns_the_exact_optional_and_result_endpoint() { + let outcome = HANDLED_VIEW_DIRECT_SOURCE + .replace("optional[Item]", "result[Item, string]") + .replace("view(incoming handle:", "view(incoming handle error:"); + for release in [false, true] { + for source in [HANDLED_VIEW_DIRECT_SOURCE, outcome.as_str()] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let call = handled_view_read_call(function); + let (owner, loan) = handled_view_storage(function, call, 0); + let backing = handled_view_raw_result(function, call, 0, &types); + let E::Local(output) = backing.kind else { + panic!("handled CFG output"); + }; + assert_ne!(output, owner); + assert_ne!(output, loan); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, + S::SumTake { target, success: true, .. } if target == output)) + ); + let begin_block = function.blocks.iter().find(|block| block.statements.iter() + .any(|statement| matches!(statement.kind, S::BeginCallView { local, .. } if local == loan))) + .expect("begin block"); + let begin = begin_block.statements.iter().position(|statement| + matches!(statement.kind, S::BeginCallView { local, .. } if local == loan)).unwrap(); + let later_tag = begin_block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::SumTag { .. })) + .expect("later source handler selection"); + assert!( + begin < later_tag, + "endpoint captured before later source work" + ); + assert!(function.blocks.iter().flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, S::EndCallView { local } if local == loan))); + let acquisitions = validate_function(&program, function, &types) + .expect("exact Handle CFG and storage"); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "written View retains caller policy" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("owned endpoint and scoped loan"); + } + } +} + +#[test] +fn caller_acquisition_handled_written_view_keeps_the_full_original_interface_conversion() { + for release in [false, true] { + let (program, types) = transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise(&program); + let call = handled_view_read_call(function); + let (owner, _) = handled_view_storage(function, call, 0); + let initializer = observation_initializer(function, owner); + assert!(matches!(initializer.kind, E::InterfaceCoerce { .. })); + assert!(matches!(types.resolve(initializer.ty), Type::Interface(_))); + assert!(!crate::handlers::can_snapshot_view(&types, initializer.ty)); + let raw = handled_view_raw_result(function, call, 0, &types); + assert!(matches!(raw.kind, E::Local(_))); + assert!(matches!(types.resolve(raw.ty), Type::Struct(_))); + assert_ne!(raw.ty, initializer.ty); + assert_ne!(raw.span, initializer.span); + validate_function(&program, function, &types) + .expect("private raw occurrence and full conversion"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("unsnapshotable converted owner is borrowed, never cloned"); + } +} + +const HANDLED_VIEW_LIST_GET: &str = "export function get[T](view items: list[T], index: int64) returns optional[T]:\n return list.__get_clone[T](view items, index)\n"; + +fn handled_view_reflected_program(release: bool) -> (Program, TypeInterner) { + assert!(include_str!("../../../../stdlib/list.jett").contains(HANDLED_VIEW_LIST_GET)); + let stdlib_file = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let project_file = jett_common::FileId::new(0); + let mut stdlib = jett_parser::parse( + &format!("namespace list\n{HANDLED_VIEW_LIST_GET}"), + stdlib_file, + ); + let mut project = jett_parser::parse( + include_str!( + "../../../jett_driver/tests/native_conformance/scoped_call_views/12_reflected_read.jett" + ), + project_file, + ); + assert!(stdlib.errors.is_empty(), "{:?}", stdlib.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + stdlib.module.items.append(&mut project.module.items); + let resolved = jett_resolve::resolve(&stdlib.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &stdlib.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([ + (stdlib_file, jett_common::SourceOrigin::Stdlib), + (project_file, jett_common::SourceOrigin::Project), + ]); + let high = + hir::lower(&stdlib.module, &resolved, &checked, &origins).expect("exact real source HIR"); + let program = + crate::lower(&high, &checked.interner).expect("exact real reflected handled View MIR"); + (program, checked.interner) +} + +#[test] +fn caller_acquisition_handled_written_view_rejoins_the_real_reflected_field_selector() { + for release in [false, true] { + let (program, types) = handled_view_reflected_program(release); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .expect("exact real main"); + let call = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + let (S::Let { value, .. } | S::Evaluate(value)) = &statement.kind else { + return None; + }; + matches!( + value.kind, + E::Intrinsic { + intrinsic: hir::IntrinsicId::TypeFieldValue, + .. + } + ) + .then_some(value) + }) + .expect("real reflected getter"); + let (_, loan) = handled_view_storage(function, call, 1); + let raw = handled_view_raw_result(function, call, 1, &types); + assert!(matches!(raw.kind, E::Local(_))); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any( + |statement| matches!(statement.kind, S::EndCallView { local } if local == loan) + ) + ); + validate_function(&program, function, &types) + .expect("original reflected bridge and handled CFG"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("caller packet remains usable after the reflected copy"); + } +} + +#[test] +fn caller_acquisition_handled_written_view_ends_on_early_return_and_keeps_nonhandle_controls() { + let source = HANDLED_VIEW_DIRECT_SOURCE.replace("default 3", "return 9"); + for release in [false, true] { + let (program, types) = transparent_retained_program(&source, release); + let function = exercise(&program); + let (_, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + assert!( + function.blocks.iter().any(|block| { + matches!(&block.terminator.kind, + T::Return(Some(value)) if matches!(value.kind, E::Int(9))) + && block.statements.iter().any(|statement| + matches!(statement.kind, S::EndCallView { local } if local == loan)) + }), + "later failure ends the live producer loan before returning" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("abort cleanup"); + let control = HANDLED_VIEW_DIRECT_SOURCE.replace( + "function exercise(incoming: optional[Item], later: optional[int64]) returns int64:\n return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:", + "function make() returns Item:\n return Item(values: list(7))\nfunction exercise(later: optional[int64]) returns int64:\n return read(view make(), later handle:"); + let (ordinary, ordinary_types) = transparent_retained_program(&control, release); + let ordinary_function = exercise(&ordinary); + let ordinary_call = handled_view_read_call(ordinary_function); + let packet = handled_view_packet(ordinary_call); + let hir::ArgumentStaging::Borrowed { loan } = packet.arguments[0].staging else { + panic!("existing call temporary path"); + }; + let owner = ordinary_function.local(loan).unwrap().view_source.unwrap(); + let initializer = observation_initializer(ordinary_function, owner); + assert!(matches!(initializer.kind, E::Call { .. })); + assert!( + hir::validate_source_handled_operand( + initializer, + &packet.arguments[0], + &packet.bridge, + &ordinary_types + ) + .expect("nonhandled source control") + .is_none() + ); + validate_function(&ordinary, ordinary_function, &ordinary_types) + .expect("unchanged original call temporary"); + } +} + +#[test] +fn caller_acquisition_handled_written_view_refuses_storage_occurrence_conversion_and_cfg_corruption() + { + for source in [HANDLED_VIEW_DIRECT_SOURCE, HANDLED_VIEW_CONVERTED_SOURCE] { + let (program, types) = transparent_retained_program(source, false); + for mutation in [ + "added clone", + "owner span", + "loan outer span", + "loan local span", + "begin span", + "inner span", + "inner type", + "foreign output", + "sum occurrence", + "wrong arm", + "missing initializer", + "missing begin", + "borrowed default", + "missing conversion", + ] { + if mutation == "missing conversion" && source == HANDLED_VIEW_DIRECT_SOURCE { + continue; + } + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + let (owner, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + let E::Local(output) = + handled_view_raw_result(function, handled_view_read_call(function), 0, &types).kind + else { + panic!("original handled output"); + }; + match mutation { + "added clone" => { + let value = handled_view_initializer_mut(function, owner); + value.kind = E::Clone(Box::new(value.clone())); + } + "owner span" => { + let E::View(inner) = &mut handled_view_projection_mut(function, loan).kind + else { + panic!("projection"); + }; + inner.span.start += 1; + } + "loan outer span" | "loan local span" => { + let E::Call { args, .. } = &mut handled_view_read_call_mut(function).kind + else { + panic!("source call"); + }; + if mutation == "loan outer span" { + args[0].span.start += 1; + } else { + let E::View(inner) = &mut args[0].kind else { + panic!("loan"); + }; + inner.span.start += 1; + } + } + "begin span" => handled_view_projection_mut(function, loan).span.start += 1, + "inner span" | "inner type" | "foreign output" => { + let spare = function.params[0].local; + let raw = + converted_handle_leaf_mut(handled_view_initializer_mut(function, owner)); + match mutation { + "inner span" => raw.span.start += 1, + "inner type" => raw.ty = TypeInterner::INT64, + "foreign output" => raw.kind = E::Local(spare), + _ => unreachable!(), + } + } + "sum occurrence" | "wrong arm" => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, S::SumTake { target, success: true, .. } if target == output)) + .expect("original success definition"); + if mutation == "sum occurrence" { + statement.span.start += 1; + } else { + let S::SumTake { success, .. } = &mut statement.kind else { + unreachable!(); + }; + *success = false; + } + } + "missing initializer" | "missing begin" => { + for block in &mut function.blocks { + block.statements.retain(|statement| if mutation == "missing initializer" { + !matches!(statement.kind, S::Let { local, .. } if local == owner) + } else { !matches!(statement.kind, S::BeginCallView { local, .. } if local == loan) }); + } + } + "borrowed default" => { + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + S::Let { local, value } if *local == output => Some(value), + _ => None, + }) + .expect("handled default definition"); + value.kind = E::View(Box::new(value.clone())); + } + "missing conversion" => { + let value = handled_view_initializer_mut(function, owner); + let E::InterfaceCoerce { value: raw, .. } = &value.kind else { + panic!("conversion"); + }; + value.kind = raw.kind.clone(); + } + _ => unreachable!(), + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "{mutation}" + ); + } + } +} + +#[test] +fn caller_acquisition_converted_handled_view_input_is_exact_and_reconstructed_after_preparation() { + let outcome = HANDLED_VIEW_CONVERTED_SOURCE + .replace("optional[Item]", "result[Item, string]") + .replace("view(incoming handle:", "view(incoming handle error:"); + for release in [false, true] { + for source in [HANDLED_VIEW_CONVERTED_SOURCE, outcome.as_str()] { + let (mut program, types) = transparent_retained_program(source, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + } + let function = exercise(&program); + let (owner, _) = + handled_view_storage(function, handled_view_read_call(function), 0); + let conversion = observation_initializer(function, owner); + let E::InterfaceCoerce { value: input, .. } = &conversion.kind else { + panic!("original converted owner"); + }; + let E::View(raw) = &input.kind else { + panic!("original written View input"); + }; + assert!(matches!(raw.kind, E::Local(_))); + let acquisitions = validate_function(&program, function, &types) + .expect("complete original Source, CFG, owner and Begin proof"); + let proved = acquisitions + .handled_conversion_input(conversion) + .expect("only this exact conversion has its authenticated input read"); + assert!(std::ptr::eq(proved, input.as_ref())); + assert_eq!( + acquisitions.owner_initializers().count(), + 0, + "input read never acquires a source binding" + ); + let independent = conversion.clone(); + assert!( + acquisitions + .handled_conversion_input(&independent) + .is_none(), + "same type/span/tree in another occurrence is no authority" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("borrowed conversion input and independent owned box"); + } + } + } +} + +#[test] +fn caller_acquisition_unassociated_identical_boxing_view_keeps_the_owning_escape_refusal() { + for release in [false, true] { + let (mut program, types) = + transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise_mut(&mut program); + let (owner, _) = handled_view_storage(function, handled_view_read_call(function), 0); + let initializer = observation_initializer(function, owner).clone(); + let mut local = function + .local(owner) + .expect("converted owner metadata") + .clone(); + local.id = LocalId::new(function.locals.len() as u32); + let extra = local.id; + function.locals.push(local); + let block = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any( + |statement| matches!(statement.kind, S::Let { local, .. } if local == owner), + ) + }) + .expect("original converted initializer block"); + let index = block + .statements + .iter() + .position(|statement| matches!(statement.kind, S::Let { local, .. } if local == owner)) + .unwrap(); + block.statements.insert( + index, + crate::Statement { + span: initializer.span, + kind: S::Let { + local: extra, + value: initializer, + }, + }, + ); + let function = exercise(&program); + let acquisitions = validate_function(&program, function, &types) + .expect("existing Source still authenticates only its original conversion"); + let extra_initializer = observation_initializer(function, extra); + assert!( + acquisitions + .handled_conversion_input(extra_initializer) + .is_none() + ); + let error = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect_err("no blanket InterfaceCoerce or View-to-owner waiver"); + assert!( + error.contains("native view cannot escape into an owning value"), + "{error}" + ); + } +} + +#[test] +fn caller_acquisition_converted_handled_view_refuses_same_typed_borrowed_backing_and_lost_end() { + let parameter_source = HANDLED_VIEW_CONVERTED_SOURCE.replace( + "incoming: optional[Item], later: optional[int64])", + "incoming: optional[Item], later: optional[int64], view spare: Item)", + ); + let body = " return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:\n default 3\n )\n"; + let loop_body = " for item in view items:\n return read(view(incoming handle:\n default Item(values: list(7))\n ), later handle:\n default 3\n )\n return 0\n"; + assert!(HANDLED_VIEW_CONVERTED_SOURCE.contains(body)); + let loop_source = HANDLED_VIEW_CONVERTED_SOURCE + .replace( + "incoming: optional[Item], later: optional[int64])", + "incoming: optional[Item], later: optional[int64], items: list[Item])", + ) + .replace(body, loop_body); + for release in [false, true] { + for (source, iteration) in [ + (parameter_source.as_str(), false), + (loop_source.as_str(), true), + ] { + let (program, types) = transparent_retained_program(source, release); + let function = exercise(&program); + let (owner, _) = handled_view_storage(function, handled_view_read_call(function), 0); + let raw = + handled_view_raw_result(function, handled_view_read_call(function), 0, &types); + let replacement = if iteration { + function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + T::ForEach { + key, + value: None, + by_view: true, + .. + } => Some(key), + _ => None, + }) + .expect("logical viewed-loop endpoint") + } else { + function.params[2].local + }; + assert_eq!(function.local(replacement).unwrap().ty, raw.ty); + if iteration { + assert!( + !function.is_view_local(replacement), + "logical iteration borrowing is not an ABI/persistent view flag" + ); + } else { + assert!(function.is_view_local(replacement)); + } + let mut changed = program.clone(); + let function = exercise_mut(&mut changed); + converted_handle_leaf_mut(handled_view_initializer_mut(function, owner)).kind = + E::Local(replacement); + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "same-typed borrowed backing cannot mint the converted input association" + ); + } + let (mut changed, types) = + transparent_retained_program(HANDLED_VIEW_CONVERTED_SOURCE, release); + let function = exercise_mut(&mut changed); + let (_, loan) = handled_view_storage(function, handled_view_read_call(function), 0); + for block in &mut function.blocks { + block.statements.retain( + |statement| !matches!(statement.kind, S::EndCallView { local } if local == loan), + ); + } + assert!( + validate_function(&changed, exercise(&changed), &types).is_err(), + "whole exact loan lifetime is required before input-read authority" + ); + } +} diff --git a/crates/jett_mir/src/call_views.rs b/crates/jett_mir/src/call_views.rs new file mode 100644 index 00000000..726acfb0 --- /dev/null +++ b/crates/jett_mir/src/call_views.rs @@ -0,0 +1,353 @@ +//! Exact eager value/field borrows whose lifetime belongs to a consuming call. +//! Source local aliases remain persistent; names never authorize scope expiry. +use super::*; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +#[cfg(test)] +mod tests; + +pub(crate) fn borrowed_source( + types: &TypeInterner, + locals: &[Local], + expression: &Expression, +) -> Option { + let hir::ExpressionKind::Local(source) = borrowed_root(expression)?.kind else { + return None; + }; + if !call_origin(locals, source) { + return None; + } + let origin = locals.get(source.index() as usize)?; + hir::validate_local_view_initializer(expression, source, origin.ty, expression.ty, types) + .ok()?; + Some(source) +} + +/// Persistent intermediate aliases retain their immutable-chain restriction. +/// The terminal owner may be mutable: the active Begin/End loan protects it. +fn call_origin(locals: &[Local], source: LocalId) -> bool { + let mut current = source; + for _ in 0..locals.len() { + let Some(local) = locals.get(current.index() as usize) else { + return false; + }; + if local.id != current { + return false; + } + match local.view_source { + Some(parent) if !local.mutable && parent.index() < current.index() => current = parent, + Some(_) => return false, + None => return true, + } + } + false +} + +fn borrowed_root(expression: &Expression) -> Option<&Expression> { + let hir::ExpressionKind::View(value) = &expression.kind else { + return None; + }; + let mut current = value.as_ref(); + loop { + match ¤t.kind { + hir::ExpressionKind::Field { base, .. } => { + current = base; + } + hir::ExpressionKind::View(value) + | hir::ExpressionKind::Coarsen(value) + | hir::ExpressionKind::Declassify(value) => current = value, + hir::ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + current = value; + } + _ => return Some(current), + } + } +} + +/// Source calls return owned values; constructors and explicit owning operators +/// also produce ordinary storage. A borrowed local, field, allocating conversion +/// or unknown control expression cannot acquire ownership via this predicate. +fn owned_temporary(root: &Expression) -> bool { + matches!( + root.kind, + hir::ExpressionKind::Call { .. } + | hir::ExpressionKind::IndirectCall { .. } + | hir::ExpressionKind::Intrinsic { .. } + | hir::ExpressionKind::StructConstruct { .. } + | hir::ExpressionKind::BitfieldConstruct { .. } + | hir::ExpressionKind::MachineConstruct { .. } + | hir::ExpressionKind::MachineTransition { .. } + | hir::ExpressionKind::Clone(_) + | hir::ExpressionKind::Run(_) + | hir::ExpressionKind::Join(_) + ) +} + +pub(crate) fn replace_borrowed_root( + expression: &Expression, + replacement: Expression, +) -> Option { + let root = borrowed_root(expression)?; + if root.ty != replacement.ty { + return None; + } + let mut rewritten = expression.clone(); + replace_root_node(&mut rewritten, replacement); + Some(rewritten) +} + +fn replace_root_node(expression: &mut Expression, replacement: Expression) { + match &mut expression.kind { + hir::ExpressionKind::Field { base, .. } => replace_root_node(base, replacement), + hir::ExpressionKind::View(value) + | hir::ExpressionKind::Coarsen(value) + | hir::ExpressionKind::Declassify(value) => replace_root_node(value, replacement), + hir::ExpressionKind::InterfaceCoerce { value, adapters } if adapters.is_empty() => { + replace_root_node(value, replacement); + } + _ => *expression = replacement, + } +} + +/// Prove the exact path against a hypothetical ordinary owning local before +/// operand lowering mutates CFG/locals. The original root is materialized once. +pub(crate) fn temporary_root( + types: &TypeInterner, + locals: &[Local], + expression: &Expression, +) -> Option { + let root = borrowed_root(expression)?; + if !owned_temporary(root) { + return None; + } + let source = LocalId::new(u32::try_from(locals.len()).ok()?); + let rewritten = replace_borrowed_root( + expression, + Expression { + kind: hir::ExpressionKind::Local(source), + ty: root.ty, + span: root.span, + }, + )?; + hir::validate_local_view_initializer(&rewritten, source, root.ty, expression.ty, types).ok()?; + Some(root.clone()) +} + +/// Validate unused and unreachable initializers and scope paths before pruning. +/// The returned map records only typed Begin authority, not synthesized names. +pub(crate) fn validate( + function: &Function, + types: &TypeInterner, +) -> Result, String> { + let mut stages = BTreeMap::new(); + let mut starts = BTreeMap::new(); + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::BeginCallView { local, value } = &statement.kind { + let definition = function.local(*local).ok_or("call view target is absent")?; + let source = definition + .view_source + .ok_or("call view has no borrowed origin")?; + if definition.id != *local + || definition.mutable + || definition.ty != value.ty + || definition.debug_ty != value.ty + || source.index() >= local.index() + || function.params.iter().any(|param| param.local == *local) + || borrowed_source(types, &function.locals, value) != Some(source) + { + return Err( + "call view requires its exact borrowed initializer and stable origin" + .into(), + ); + } + let root = function + .view_root(source) + .ok_or("call view origin is cyclic")?; + if stages + .insert(local.index() as usize, root.index() as usize) + .is_some() + { + return Err("call view has more than one initializer".into()); + } + starts.insert(local.index() as usize, block.id); + } + } + } + // An internal stage cannot escape by becoming another local's origin. Its + // only uses belong to the operation guarded by its own typed Begin/End. + if function.locals.iter().any(|local| { + local + .view_source + .is_some_and(|source| stages.contains_key(&(source.index() as usize))) + }) { + return Err("call view cannot escape into another local alias origin".into()); + } + let mut ends = BTreeSet::new(); + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + let target = match &statement.kind { + StatementKind::EndCallView { local } => { + if !stages.contains_key(&(local.index() as usize)) { + return Err("call view end does not identify an internal stage".into()); + } + ends.insert(local.index() as usize); + continue; + } + StatementKind::Let { local, .. } + | StatementKind::CheckRefinement { local, .. } + | StatementKind::SequenceLength { target: local, .. } + | StatementKind::SequenceGet { target: local, .. } + | StatementKind::SumTag { target: local, .. } + | StatementKind::SumTake { target: local, .. } => Some(*local), + StatementKind::Assign { target, .. } => match target.kind { + hir::ExpressionKind::Local(local) => Some(local), + _ => None, + }, + _ => None, + }; + if target.is_some_and(|local| stages.contains_key(&(local.index() as usize))) { + return Err("call view cannot acquire a source or owning initializer".into()); + } + } + if stages.keys().any(|stage| !ends.contains(stage)) { + return Err("call view has no typed end".into()); + } + if !stages.is_empty() { + let cfg = ControlFlowGraph::analyze(function).map_err(|error| format!("{error:?}"))?; + let facts = scope_facts(function); + for (&stage, &start) in &starts { + validate_stage(function, &cfg, &facts, stage, start)?; + } + } + Ok(stages) +} + +struct ScopeFacts { + statements: Vec>, + terminator: BTreeSet, +} + +fn referenced_locals(block: &BasicBlock) -> BTreeSet { + let mut block = block.clone(); + let mut locals = BTreeSet::new(); + crate::sequences::prune::block_runtime_locals( + &mut block, + &mut |local| { + locals.insert(local.index() as usize); + }, + &mut |_| {}, + ); + locals +} + +fn scope_facts(function: &Function) -> Vec { + function + .blocks + .iter() + .map(|block| { + let statements = block + .statements + .iter() + .map(|statement| { + referenced_locals(&BasicBlock { + id: block.id, + statements: vec![statement.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: statement.span, + }, + }) + }) + .collect(); + let terminator = referenced_locals(&BasicBlock { + id: block.id, + statements: Vec::new(), + terminator: block.terminator.clone(), + }); + ScopeFacts { + statements, + terminator, + } + }) + .collect() +} + +fn validate_stage( + function: &Function, + cfg: &ControlFlowGraph, + facts: &[ScopeFacts], + stage: usize, + start: BlockId, +) -> Result<(), String> { + let mut seen = vec![[false; 2]; function.blocks.len()]; + walk_stage(function, cfg, facts, stage, function.entry, &mut seen)?; + // Unreachable compiler metadata is still a proof obligation. Start at its + // initializer before examining any disconnected end/use component. + if !seen[start.index() as usize].iter().any(|seen| *seen) { + walk_stage(function, cfg, facts, stage, start, &mut seen)?; + } + for block in &function.blocks { + let id = block.id.index() as usize; + if !seen[id].iter().any(|seen| *seen) + && (facts[id] + .statements + .iter() + .any(|reads| reads.contains(&stage)) + || facts[id].terminator.contains(&stage)) + { + walk_stage(function, cfg, facts, stage, block.id, &mut seen)?; + } + } + Ok(()) +} + +fn walk_stage( + function: &Function, + cfg: &ControlFlowGraph, + facts: &[ScopeFacts], + stage: usize, + start: BlockId, + seen: &mut [[bool; 2]], +) -> Result<(), String> { + let mut pending = VecDeque::from([(start, false)]); + while let Some((id, mut active)) = pending.pop_front() { + let index = id.index() as usize; + if std::mem::replace(&mut seen[index][usize::from(active)], true) { + continue; + } + let block = &function.blocks[index]; + for (statement, reads) in block.statements.iter().zip(&facts[index].statements) { + match &statement.kind { + StatementKind::BeginCallView { local, .. } if local.index() as usize == stage => { + if active { + return Err("call view begins while its previous scope is active".into()); + } + active = true; + } + StatementKind::EndCallView { local } if local.index() as usize == stage => { + if !active { + return Err("call view ends without an active scope".into()); + } + active = false; + } + _ if !active && reads.contains(&stage) => { + return Err("call view is read outside its internal scope".into()); + } + _ => {} + } + } + if !active && facts[index].terminator.contains(&stage) { + return Err("call view is read outside its internal scope".into()); + } + if active + && matches!( + block.terminator.kind, + TerminatorKind::Return(_) | TerminatorKind::Respond(_) + ) + { + return Err("call view remains active at a successful exit".into()); + } + pending.extend(cfg.successors(id).iter().map(|next| (*next, active))); + } + Ok(()) +} diff --git a/crates/jett_mir/src/call_views/tests.rs b/crates/jett_mir/src/call_views/tests.rs new file mode 100644 index 00000000..cebbbd39 --- /dev/null +++ b/crates/jett_mir/src/call_views/tests.rs @@ -0,0 +1,680 @@ +use super::*; +use jett_hir::ExpressionKind; +use std::collections::HashMap; + +const PRELUDE: &str = r#"namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "member" +struct Packet: + member: Named +function report(view item: Named, extra: int64) returns string: + return Named.name(view item) +"#; + +fn source_program(body: &str) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let source = format!("{PRELUDE}{body}"); + let parsed = jett_parser::parse(&source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|item| item.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let hir = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("checked HIR"); + ( + lower(&hir, &checked.interner).expect("checked MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap() +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap() +} + +fn stages(function: &Function) -> Vec { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + StatementKind::BeginCallView { local, .. } => Some(local), + _ => None, + }) + .collect() +} + +const POST_CALL: &str = r#"function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + string answer = report(view packet.member, incoming handle: + default 3 + ) + return packet +"#; + +#[test] +fn call_views_end_after_actual_direct_and_indirect_calls_before_owner_moves() { + for indirect in [false, true] { + let body = if indirect { + POST_CALL.replace(" string answer = report", " function(view Named, int64) returns string callback = report\n string answer = callback") + } else { + POST_CALL.to_owned() + }; + let (program, types) = source_program(&body); + let function = exercise(&program); + let scope = stages(function); + assert_eq!(scope.len(), 1); + let stage = scope[0]; + let owner = function.params[0].local; + assert_eq!(function.local(stage).unwrap().view_source, Some(owner)); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); + assert!(plan.owned_locals.contains(&(owner.index() as usize))); + let entry = &function.blocks[function.entry.index() as usize]; + let begin = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::BeginCallView { .. })) + .unwrap(); + let sum = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .unwrap(); + assert!(begin < sum); + assert!( + plan.live_after_statement[function.entry.index() as usize][begin] + .contains(&(owner.index() as usize)) + ); + let end_block = function.blocks.iter().find(|block| block.statements.iter().any(|statement| matches!(statement.kind, StatementKind::EndCallView { local } if local == stage))).unwrap(); + let end = end_block + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::EndCallView { .. })) + .unwrap(); + assert!( + matches!(&end_block.statements[end - 1].kind, StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. })) + ); + let mut renamed = program.clone(); + exercise_mut(&mut renamed).locals[stage.index() as usize].name = "ordinary_name".into(); + validate(exercise(&renamed), &types).unwrap(); + } +} + +#[test] +fn aborted_call_ends_internal_view_before_the_owned_return_operand() { + let body = POST_CALL.replace(" default 3", " return packet"); + let (program, types) = source_program(&body); + let function = exercise(&program); + let stage = stages(function)[0]; + let owner = function.params[0].local; + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let returned = function.blocks.iter().filter(|block| matches!(block.terminator.kind, + TerminatorKind::Return(Some(Expression { kind: ExpressionKind::Local(local), .. })) if local == owner)).collect::>(); + assert_eq!(returned.len(), 2); + for block in returned { + assert!(block.statements.iter().any(|statement| matches!(statement.kind, StatementKind::EndCallView { local } if local == stage))); + } +} + +#[test] +fn call_view_keeps_pending_owned_and_forwarded_parameter_origins_exact() { + for borrowed in [false, true] { + let body = if borrowed { + "struct Envelope:\n packet: Packet\nfunction exercise(view source: Envelope, incoming: optional[int64]) returns string:\n Packet selected = view source.packet\n Packet forwarded = selected\n return report(view forwarded.member, incoming handle:\n default 3\n )\n" + } else { + "function exercise(incoming: optional[int64]) returns Packet:\n Packet packet = run run Packet(member: 7)\n string answer = report(view packet.member, incoming handle:\n default 3\n )\n return packet\n" + }; + let (program, types) = source_program(body); + let function = exercise(&program); + let stage = stages(function)[0]; + let source = function + .locals + .iter() + .find(|local| local.name == if borrowed { "forwarded" } else { "packet" }) + .unwrap() + .id; + assert_eq!(function.local(stage).unwrap().view_source, Some(source)); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); + assert_eq!( + plan.owned_locals.contains(&(source.index() as usize)), + !borrowed + ); + let entry = &function.blocks[function.entry.index() as usize]; + let begin = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::BeginCallView { .. })) + .unwrap(); + assert!( + plan.live_after_statement[function.entry.index() as usize][begin] + .contains(&(source.index() as usize)) + ); + assert!( + matches!(&entry.statements[begin].kind, StatementKind::BeginCallView { value: Expression { kind: ExpressionKind::View(value), .. }, .. } + if matches!(value.kind, ExpressionKind::Field { base: ref owner, .. } if matches!(owner.kind, ExpressionKind::Local(local) if local == source))) + ); + } +} + +#[test] +fn call_view_scope_respects_loop_exit_depth_and_preserves_source_alias_loans() { + for body in [ + r#"function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + mutable int64 count = 0 + while count < 2: + count = count + 1 + string answer = report(view packet.member, incoming handle: + if count == 1: + continue + default 3 + ) + return packet +"#, + r#"function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + string answer = report(view packet.member, incoming handle: + mutable int64 count = 0 + while count < 1: + count = count + 1 + continue + default 3 + ) + return packet +"#, + ] { + let (program, types) = source_program(body); + assert_eq!(stages(exercise(&program)).len(), 1); + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types).unwrap(); + } + let body = POST_CALL.replace( + " string answer = report(view packet.member,", + " Named borrowed = view packet.member\n string answer = report(view borrowed,", + ); + let (program, types) = source_program(&body); + let function = exercise(&program); + let active_stages = stages(function); + assert_eq!(active_stages.len(), 1); + let borrowed = function + .locals + .iter() + .find(|local| local.name == "borrowed") + .unwrap(); + assert_eq!( + function.local(active_stages[0]).unwrap().view_source, + Some(borrowed.id) + ); + assert!( + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .unwrap_err() + .contains("after creating a local view alias") + ); +} + +#[test] +fn call_view_preflight_rejects_malformed_unused_metadata_and_scope_paths() { + let (original, types) = source_program(POST_CALL); + let stage = stages(exercise(&original))[0]; + for fault in [ + "field", + "endpoint", + "mutable", + "origin", + "clone", + "missing_begin", + "missing_end", + "early_end", + "duplicate_end", + "source_let", + ] { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + match fault { + "mutable" => function.locals[stage.index() as usize].mutable = true, + "origin" => function.locals[stage.index() as usize].view_source = Some(stage), + "missing_begin" => { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!(statement.kind, StatementKind::BeginCallView { .. }) + }); + } + } + "missing_end" => { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!(statement.kind, StatementKind::EndCallView { .. }) + }); + } + } + "early_end" | "duplicate_end" => { + if fault == "early_end" { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!(statement.kind, StatementKind::EndCallView { .. }) + }); + } + } + let entry = &mut function.blocks[function.entry.index() as usize]; + let begin = entry + .statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::BeginCallView { .. }) + }) + .unwrap(); + entry.statements.insert( + begin + 1, + Statement { + kind: StatementKind::EndCallView { local: stage }, + span: function.span, + }, + ); + } + _ => { + let statement = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| matches!(statement.kind, StatementKind::BeginCallView { .. })) + .unwrap(); + let StatementKind::BeginCallView { local, value } = &mut statement.kind else { + unreachable!() + }; + if fault == "source_let" { + statement.kind = StatementKind::Let { + local: *local, + value: value.clone(), + }; + } else if fault == "clone" { + value.kind = ExpressionKind::Clone(Box::new(value.clone())); + } else { + let ExpressionKind::View(inner) = &mut value.kind else { + unreachable!() + }; + if fault == "endpoint" { + inner.ty = TypeInterner::INT64; + } else { + let ExpressionKind::Field { field, .. } = &mut inner.kind else { + unreachable!() + }; + *field = FieldId::new(99); + // Invalid unused field metadata must fail before pruning. + function.blocks[function.entry.index() as usize] + .terminator + .kind = TerminatorKind::Unreachable; + } + } + } + } + assert!( + crate::move_values::validate_local_view_initializers(function, &types).is_err(), + "{fault}" + ); + } +} + +#[test] +fn call_views_never_authorize_an_ordinary_alias_end_or_erased_owner_overlap() { + let source = POST_CALL.replace( + " string answer = report(view packet.member,", + " Named borrowed = view packet.member\n string answer = report(view borrowed,", + ); + let (mut program, types) = source_program(&source); + let function = exercise_mut(&mut program); + let declared = function + .locals + .iter() + .find(|local| local.name == "borrowed") + .unwrap() + .id; + function.locals[declared.index() as usize].name = "$callView999".into(); + function.blocks[function.entry.index() as usize] + .statements + .push(Statement { + kind: StatementKind::EndCallView { local: declared }, + span: function.span, + }); + assert!( + validate(function, &types) + .unwrap_err() + .contains("does not identify an internal stage") + ); + + let body = r#"function consume(packet: Packet) returns int64: + return 3 +function exercise(packet: Packet, incoming: optional[int64]) returns string: + return report(view packet.member, incoming handle: + default consume(packet) + ) +"#; + let (program, types) = source_program(body); + assert_eq!(stages(exercise(&program)).len(), 1); + assert!( + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .unwrap_err() + .contains("while borrowed") + ); +} + +#[test] +fn scoped_view_origin_and_end_survive_native_local_compaction() { + let body = r#"function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + function(int64) returns int64 unrelated = function(value: int64) returns int64: return value + Packet current = packet + string answer = report(view current.member, incoming handle: + default 3 + ) + return current +"#; + let (mut program, types) = source_program(&body); + let original = exercise(&program); + let stage_before = stages(original)[0]; + let owner_before = original.local(stage_before).unwrap().view_source.unwrap(); + let count_before = original.locals.len(); + crate::prepare_native_generated_functions(&mut program, &types); + let function = exercise(&program); + validate(function, &types).unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert_eq!(stages(function).len(), 1); + let stage = stages(function)[0]; + let owner = function.local(stage).unwrap().view_source.unwrap(); + assert!(function.locals.len() < count_before); + assert!(stage.index() < stage_before.index()); + assert!(owner.index() < owner_before.index()); + assert_eq!(function.local(owner).unwrap().name, "current"); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == stage) + }) + ); +} + +#[test] +fn scoped_call_view_borrows_a_mutable_terminal_owner_until_post_call_rebinding() { + let body = r#"function exercise(packet: Packet, incoming: optional[int64]) returns Packet: + mutable Packet current = packet + string answer = report(view current.member, incoming handle: + default 3 + ) + current = Packet(member: 9) + return current +"#; + let (program, types) = source_program(body); + let function = exercise(&program); + let stage = stages(function)[0]; + let owner = function + .locals + .iter() + .find(|local| local.name == "current") + .unwrap(); + assert!(owner.mutable && owner.view_source.is_none()); + assert_eq!(function.local(stage).unwrap().view_source, Some(owner.id)); + crate::validate(&program).unwrap(); + validate(function, &types).unwrap(); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(owner.id.index() as usize))); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); + let block = function + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(&statement.kind, StatementKind::Assign { target, .. } + if matches!(target.kind, ExpressionKind::Local(local) if local == owner.id)) + }) + }) + .unwrap(); + let end = block.statements.iter().position(|statement| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == stage) + }).unwrap(); + let assignment = block + .statements + .iter() + .position(|statement| { + matches!(&statement.kind, StatementKind::Assign { target, .. } + if matches!(target.kind, ExpressionKind::Local(local) if local == owner.id)) + }) + .unwrap(); + assert!(end < assignment); +} + +#[test] +fn scoped_call_view_materializes_the_original_owned_temporary_once_before_the_handler() { + for initializer in ["Packet(member: 7)", "run run Packet(member: 7)"] { + let body = format!( + r#"function make() returns Packet: + return {initializer} +function exercise(incoming: optional[int64]) returns string: + return report(view make().member, incoming handle: + default 3 + ) +"# + ); + let (program, types) = source_program(&body); + let function = exercise(&program); + let stage = stages(function)[0]; + let owner = function.local(stage).unwrap().view_source.unwrap(); + assert!(function.local(owner).unwrap().view_source.is_none()); + let make = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "make") + .unwrap() + .id; + let initializers = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == owner && matches!(value.kind, ExpressionKind::Call { function, .. } if function == make)) + }).count(); + assert_eq!(initializers, 1); + let entry = &function.blocks[function.entry.index() as usize]; + let owned = entry.statements.iter().position(|statement| { + matches!(statement.kind, StatementKind::Let { local, .. } if local == owner) + }).unwrap(); + let begin = entry.statements.iter().position(|statement| { + matches!(statement.kind, StatementKind::BeginCallView { local, .. } if local == stage) + }).unwrap(); + let sum = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .unwrap(); + assert!(owned < begin && begin < sum); + let StatementKind::BeginCallView { value, .. } = &entry.statements[begin].kind else { + unreachable!() + }; + assert!( + matches!(borrowed_root(value).unwrap().kind, ExpressionKind::Local(local) if local == owner) + ); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert_eq!( + plan.owned_locals + .iter() + .filter(|local| **local == owner.index() as usize) + .count(), + 1 + ); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); + assert!( + plan.live_after_statement[function.entry.index() as usize][begin] + .contains(&(owner.index() as usize)) + ); + + let mut missing = program.clone(); + let invalid = exercise_mut(&mut missing); + for block in &mut invalid.blocks { + block.statements.retain(|statement| !matches!(statement.kind, StatementKind::Let { local, .. } if local == owner)); + } + assert!( + crate::move_values::MoveValuePlan::analyze(&missing, exercise(&missing), &types) + .is_err() + ); + } +} + +#[test] +fn scoped_mutable_root_keeps_active_overlap_and_persistent_alias_safety() { + let body = r#"function exercise(packet: Packet, incoming: optional[int64]) returns string: + mutable Packet current = packet + return report(view current.member, incoming handle: + current = Packet(member: 9) + default 3 + ) +"#; + let (program, types) = source_program(body); + assert_eq!(stages(exercise(&program)).len(), 1); + assert!( + crate::move_values::MoveValuePlan::analyze(&program, exercise(&program), &types) + .unwrap_err() + .contains("while borrowed") + ); + + let forwarded = "struct Envelope:\n packet: Packet\nfunction exercise(view source: Envelope, incoming: optional[int64]) returns string:\n Packet selected = view source.packet\n return report(view selected.member, incoming handle:\n default 3\n )\n"; + let (mut program, types) = source_program(forwarded); + let function = exercise_mut(&mut program); + let selected = function + .locals + .iter() + .find(|local| local.name == "selected") + .unwrap() + .id; + function.locals[selected.index() as usize].mutable = true; + assert!(validate(function, &types).is_err()); + + let (mut program, types) = source_program(POST_CALL); + let function = exercise_mut(&mut program); + let stage = stages(function)[0]; + let mut escaped = function.local(stage).unwrap().clone(); + escaped.id = LocalId::new(function.locals.len() as u32); + escaped.name = "unused_alias".into(); + escaped.view_source = Some(stage); + function.locals.push(escaped); + assert!(validate(function, &types).unwrap_err().contains("escape")); +} + +#[test] +fn scoped_direct_named_view_keeps_mutable_storage_borrowed_until_post_call_rebinding() { + let body = r#"function exercise(incoming: optional[int64]) returns Named: + mutable Named current = 7 + string answer = report(view current, incoming handle: + default 3 + ) + current = 9 + return current +"#; + let (program, types) = source_program(body); + let function = exercise(&program); + let stage = stages(function)[0]; + let owner = function + .locals + .iter() + .find(|local| local.name == "current") + .unwrap(); + assert!(owner.mutable && owner.view_source.is_none()); + assert_eq!(function.local(stage).unwrap().view_source, Some(owner.id)); + let value = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::BeginCallView { value, .. } => Some(value), + _ => None, + }) + .unwrap(); + assert!(matches!(&value.kind, ExpressionKind::View(inner) + if matches!(inner.kind, ExpressionKind::Local(local) if local == owner.id))); + assert_eq!(value.ty, owner.ty); + crate::validate(&program).unwrap(); + validate(function, &types).unwrap(); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(owner.id.index() as usize))); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); +} + +#[test] +fn scoped_direct_owned_named_return_is_observed_once_without_a_field_or_readiness_guard() { + for pending in [false, true] { + let source = if pending { + " Named original = run run make_named()\n return report(view original," + } else { + " return report(view make_named()," + }; + let body = format!( + r#"function make_named() returns Named: + return 7 +function exercise(incoming: optional[int64]) returns string: +{source} incoming handle: + default 3 + ) +"# + ); + let (program, types) = source_program(&body); + let function = exercise(&program); + let stage = stages(function)[0]; + let owner = function.local(stage).unwrap().view_source.unwrap(); + let entry = &function.blocks[function.entry.index() as usize]; + let begin = entry.statements.iter().position(|statement| { + matches!(statement.kind, StatementKind::BeginCallView { local, .. } if local == stage) + }).unwrap(); + let StatementKind::BeginCallView { value, .. } = &entry.statements[begin].kind else { + unreachable!() + }; + assert!(matches!(&value.kind, ExpressionKind::View(inner) + if matches!(inner.kind, ExpressionKind::Local(local) if local == owner))); + assert_eq!(value.ty, function.local(owner).unwrap().ty); + assert!(!entry.statements[..=begin].iter().any(|statement| { + matches!( + statement.kind, + StatementKind::ReflectedContainerReady { .. } + ) + })); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(owner.index() as usize))); + assert!(!plan.owned_locals.contains(&(stage.index() as usize))); + assert!( + plan.live_after_statement[function.entry.index() as usize][begin] + .contains(&(owner.index() as usize)) + ); + if !pending { + let make = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "make_named") + .unwrap() + .id; + assert_eq!(function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == owner && matches!(value.kind, ExpressionKind::Call { function, .. } if function == make)) + }).count(), 1); + } + } +} diff --git a/crates/jett_mir/src/copy_values.rs b/crates/jett_mir/src/copy_values.rs new file mode 100644 index 00000000..ca81d02f --- /dev/null +++ b/crates/jett_mir/src/copy_values.rs @@ -0,0 +1,1242 @@ +//! Ownership planning for the copyable scalar/string MIR subset. +//! This deliberately rejects unextracted handlers rather than walking hidden +//! control flow. Resources and other move-only values need a distinct +//! move/borrow/drop analysis before their backend support can be enabled. +use crate::{ControlFlowGraph, Function, ResourceCallNode, StatementKind, TerminatorKind}; +use jett_hir::{Expression, ExpressionKind, IntrinsicId, StringSegment}; +use jett_types::{Type, TypeId, TypeInterner}; +use std::collections::BTreeSet; +type Set = BTreeSet; + +#[derive(Clone, Copy)] +struct PlanningContext<'a> { + program: Option<&'a crate::Program>, + companion: Option<&'a crate::resource_ownership::CompanionContext<'a>>, +} +impl<'a> PlanningContext<'a> { + fn is_some(self) -> bool { + self.program.is_some() + } + fn is_none(self) -> bool { + self.program.is_none() + } + fn is_some_and(self, f: impl FnOnce(&'a crate::Program) -> bool) -> bool { + self.program.is_some_and(f) + } + fn and_then(self, f: impl FnOnce(&'a crate::Program) -> Option) -> Option { + self.program.and_then(f) + } + fn carrier_type(self, ty: TypeId) -> bool { + self.companion + .is_some_and(|context| context.carrier_type(ty)) + } + fn resource_type(self, ty: TypeId) -> bool { + self.companion + .is_some_and(|context| context.resource_type(ty)) + } + fn graph_return(self) -> bool { + self.companion.is_some_and(|context| context.graph_return()) + } + fn graph_local(self, local: &jett_hir::Local) -> bool { + self.companion + .is_some_and(|context| context.graph_local(local)) + } + fn graph_expression(self, value: &Expression) -> bool { + self.companion + .is_some_and(|context| context.graph_expression(value)) + } + fn resource_expression(self, value: &Expression) -> bool { + self.companion + .is_some_and(|context| context.resource_expression(value)) + } + fn resource_local(self, local: &jett_hir::Local) -> bool { + self.companion + .is_some_and(|context| context.resource_local(local)) + } +} + +#[derive(Debug)] +pub struct CopyValuePlan { + pub owned_locals: Vec, + pub live_in: Vec, + pub live_out: Vec, + pub live_after_statement: Vec>, + /// Bound on owned intermediates in one full expression, including formatting. + pub temporary_slots: usize, + generation_storage: crate::CallGenerationStoragePlan, +} +impl CopyValuePlan { + pub fn generation_storage(&self) -> &crate::CallGenerationStoragePlan { + &self.generation_storage + } + pub fn analyze(function: &Function, types: &TypeInterner) -> Result { + Self::analyze_storage(function, types, None) + } + pub(crate) fn analyze_storage( + function: &Function, + types: &TypeInterner, + program: Option<&crate::Program>, + ) -> Result { + Self::analyze_with_context( + function, + types, + PlanningContext { + program, + companion: None, + }, + ) + } + pub(crate) fn analyze_companion( + context: &crate::resource_ownership::CompanionContext<'_>, + ) -> Result { + Self::analyze_with_context( + context.function, + context.types, + PlanningContext { + program: Some(context.program), + companion: Some(context), + }, + ) + } + fn analyze_with_context( + function: &Function, + types: &TypeInterner, + program: PlanningContext<'_>, + ) -> Result { + crate::temporary_local_views::validate(function, types)?; + if crate::temporary_local_views::has_records(function) && program.is_none() { + return Err( + "temporary local view storage requires complete Source call ownership validation" + .into(), + ); + } + crate::field_updates::validate_function(function, types)?; + crate::ordinary_borrowed_sums::validate(function, types)?; + if program.companion.is_none() + && (crate::resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| crate::resource_type_pending(types, local.ty))) + { + return Err("pending ResourceOwnershipPlan: ordinary CopyValuePlan cannot plan Resource carriers or descriptors".into()); + } + let generation_storage = crate::call_owner_generations::validate(function, types)?; + if program.is_none() && !generation_storage.slots().is_empty() { + return Err("generation storage requires complete program ownership validation".into()); + } + // A protected endpoint supplies its own typed storage proof. The + // type-only fallback stays strict for every other endpoint of that type. + if !program.graph_return() { + plan_type(types, function.return_type, program)?; + } + for local in &function.locals { + if !program.graph_local(local) { + plan_type(types, local.ty, program)?; + } + if function.view_root(local.id).is_none() { + return Err("borrowed local origin is invalid or cyclic".into()); + } + } + let cfg = ControlFlowGraph::analyze(function).map_err(|e| format!("{e:?}"))?; + let n = function.blocks.len(); + let mut facts = Vec::new(); + let mut max_temporaries = 0; + for block in &function.blocks { + let mut statements = Vec::new(); + for (index, statement) in block.statements.iter().enumerate() { + let mut reads = Set::new(); + let mut temporaries = 0; + let mut killed = None; + let definition = match &statement.kind { + StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::CloseCallOwnerGeneration { .. } => None, + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + reads.insert(root.index() as usize); + reads.insert(rhs_owner.index() as usize); + killed = Some(rhs_owner.index() as usize); + Some(root.index() as usize) + } + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { + reads.insert(source.index() as usize); + None + } + StatementKind::IterationBorrow { source, .. } => { + reads.insert(source.root().index() as usize); + None + } + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + reads.insert(source.root().index() as usize); + if let StatementKind::SequenceGet { index, .. } = statement.kind { + reads.insert(index.index() as usize); + temporaries += usize::from( + crate::move_values::is_copy_owned( + types, + function.locals[target.index() as usize].ty, + ) || crate::move_values::is_linear( + types, + function.locals[target.index() as usize].ty, + ), + ); + } + Some(target.index() as usize) + } + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } + if program.is_some() => + { + reads.insert(source.index() as usize); + if matches!(statement.kind, StatementKind::SumTake { .. }) { + let ty = function.locals[target.index() as usize].ty; + let borrowed = matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) && function + .ordinary_borrowed_sum_projection(block.id, index)? + .is_some(); + if !borrowed { + temporaries += usize::from( + crate::move_values::is_copy_owned(types, ty) + || crate::move_values::is_linear(types, ty), + ); + } + } + Some(target.index() as usize) + } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | StatementKind::Let { local, value } + | StatementKind::BeginCallView { local, value } => { + let borrowed = function + .local(*local) + .ok_or("local definition is outside its function")? + .view_source + .is_some(); + visit( + value, + &mut reads, + &mut temporaries, + types, + program, + borrowed, + )?; + Some(local.index() as usize) + } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: None, + .. + }) => { + if program.is_none() { + return Err( + "Resource call stage requires its fresh companion plan".into() + ); + } + visit(value, &mut reads, &mut temporaries, types, program, false)?; + None + } + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }) => { + let record = function + .resource_call_region(*region) + .ok_or("Resource call output lacks its sealed region")?; + for actual in record.actuals() { + if let Some(local) = actual.ordinary() { + reads.insert(local.index() as usize); + } + } + let ty = function.locals[output.index() as usize].ty; + temporaries += usize::from( + crate::move_values::is_copy_owned(types, ty) + || crate::move_values::is_linear(types, ty), + ); + Some(output.index() as usize) + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. }, + ) => { + if program.is_none() { + return Err( + "Resource call region requires its fresh companion plan".into() + ); + } + None + } + StatementKind::ResourceConstructor(node) => { + if program.is_none() { + return Err( + "Resource constructor requires its fresh companion plan".into() + ); + } + function.resource_constructor(node.constructor())?; + match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + visit(value, &mut reads, &mut temporaries, types, program, false)?; + None + } + crate::ResourceConstructorNode::Commit { output, .. } => { + Some(output.index() as usize) + } + _ => None, + } + } + StatementKind::ResourceLexicalExit(_) => { + if program.is_none() { + return Err( + "Resource lexical exit requires its fresh companion plan".into() + ); + } + None + } + StatementKind::EndCallView { local } => { + reads.insert(local.index() as usize); + killed = Some(local.index() as usize); + None + } + StatementKind::CheckRefinement { local, call, .. } => { + visit(call, &mut reads, &mut temporaries, types, program, false)?; + Some(local.index() as usize) + } + StatementKind::PrepareFieldUpdate { place, begin: true } => { + reads.insert(place.root().index() as usize); + None + } + StatementKind::PrepareFieldUpdate { begin: false, .. } => None, + StatementKind::UpdateField { value, place, .. } => { + reads.insert(place.root().index() as usize); + visit(value, &mut reads, &mut temporaries, types, program, false)?; + // Independent staged acquisition plus displaced-owner slot. + temporaries += 2; + None + } + StatementKind::Assign { target, value } => { + let ExpressionKind::Local(local) = target.kind else { + return Err("nonlocal assignment needs place ownership".into()); + }; + visit(value, &mut reads, &mut temporaries, types, program, false)?; + Some(local.index() as usize) + } + StatementKind::Evaluate(value) => { + visit(value, &mut reads, &mut temporaries, types, program, false)?; + None + } + StatementKind::Assert { condition, message } => { + visit( + condition, + &mut reads, + &mut temporaries, + types, + program, + false, + )?; + if let Some(message) = message { + visit(message, &mut reads, &mut temporaries, types, program, false)?; + } + None + } + StatementKind::Trace(local) => { + reads.insert(local.index() as usize); + temporaries += 1; + None + } + StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + visit( + condition, + &mut reads, + &mut temporaries, + types, + program, + false, + )?; + } + reads.extend(bindings.iter().map(|local| local.index() as usize)); + temporaries += usize::from(!bindings.is_empty()); + None + } + _ => return Err("statement needs explicit ownership lowering".into()), + }; + expand_view_reads(function, &mut reads)?; + max_temporaries = max_temporaries.max(temporaries); + statements.push((reads, definition, killed)); + } + let mut reads = Set::new(); + let mut temporaries = 0; + match &block.terminator.kind { + TerminatorKind::Return(Some(v)) + | TerminatorKind::Respond(v) + | TerminatorKind::Branch { condition: v, .. } => { + visit(v, &mut reads, &mut temporaries, types, program, false)? + } + TerminatorKind::ReflectedTypeDispatch { type_info, .. } if program.is_some() => { + visit( + type_info, + &mut reads, + &mut temporaries, + types, + program, + true, + )?; + } + TerminatorKind::Switch { + scrutinee, + variants, + .. + } if program.is_some() => { + visit( + scrutinee, + &mut reads, + &mut temporaries, + types, + program, + false, + )?; + temporaries += variants + .iter() + .map(|(_, _, bindings)| { + bindings + .iter() + .filter(|binding| { + let ty = function.locals[binding.index() as usize].ty; + crate::move_values::is_copy_owned(types, ty) + || crate::move_values::is_linear(types, ty) + }) + .count() + }) + .max() + .unwrap_or(0); + } + TerminatorKind::Return(None) + | TerminatorKind::Goto(_) + | TerminatorKind::Unreachable => {} + _ => return Err("terminator needs explicit ownership lowering".into()), + } + expand_view_reads(function, &mut reads)?; + max_temporaries = max_temporaries.max(temporaries); + facts.push((statements, reads)); + } + // Definite initialization is an intersection fixed point, including + // backedges. Reading a zero-initialized native slot is not a substitute. + let all: Set = (0..function.locals.len()).collect(); + let params: Set = function + .params + .iter() + .map(|p| p.local.index() as usize) + .collect(); + let mut initialized_in = vec![all.clone(); n]; + let mut initialized_out = vec![all; n]; + loop { + let mut changed = false; + for &id in cfg.reverse_postorder() { + let i = id.index() as usize; + let incoming = if id == function.entry { + params.clone() + } else { + let mut incoming = (0..function.locals.len()).collect::(); + for pred in cfg + .predecessors(id) + .iter() + .filter(|p| cfg.reverse_postorder().contains(p)) + { + let edge = switch_bindings_on_edge(function, *pred, id); + let produced = initialized_out[pred.index() as usize] + .union(&edge) + .copied() + .collect::(); + incoming = incoming.intersection(&produced).copied().collect(); + } + incoming + }; + let mut outgoing = incoming.clone(); + for (_, definition, killed) in &facts[i].0 { + if let Some(local) = killed { + outgoing.remove(local); + } + if let Some(d) = definition { + outgoing.insert(*d); + } + } + changed |= incoming != initialized_in[i] || outgoing != initialized_out[i]; + initialized_in[i] = incoming; + initialized_out[i] = outgoing; + } + if !changed { + break; + } + } + for &id in cfg.reverse_postorder() { + let i = id.index() as usize; + let mut initialized = initialized_in[i].clone(); + for (reads, definition, killed) in &facts[i].0 { + if !reads.is_subset(&initialized) { + return Err(format!("read before definite initialization in block {i}")); + } + if let Some(local) = killed { + initialized.remove(local); + } + if let Some(d) = definition { + initialized.insert(*d); + } + } + if !facts[i].1.is_subset(&initialized) { + return Err(format!( + "terminator read before definite initialization in block {i}" + )); + } + } + let mut live_in = vec![Set::new(); n]; + let mut live_out = live_in.clone(); + let mut after = facts + .iter() + .map(|(ss, _)| vec![Set::new(); ss.len()]) + .collect::>(); + loop { + let mut changed = false; + for &id in cfg.reverse_postorder().iter().rev() { + let i = id.index() as usize; + let out: Set = cfg + .successors(id) + .iter() + .flat_map(|s| { + let edge = switch_bindings_on_edge(function, id, *s); + live_in[s.index() as usize] + .difference(&edge) + .copied() + .collect::() + }) + .collect(); + let mut live = out.union(&facts[i].1).copied().collect::(); + for (j, (reads, definition, killed)) in facts[i].0.iter().enumerate().rev() { + after[i][j] = live.clone(); + if let Some(local) = killed { + live.remove(local); + } + if let Some(d) = definition { + live.remove(d); + } + live.extend(reads); + } + changed |= live != live_in[i] || out != live_out[i]; + live_in[i] = live; + live_out[i] = out; + } + if !changed { + break; + } + } + crate::temporary_local_views::pin_scope( + function, + types, + &cfg, + &mut live_in, + &mut live_out, + &mut after, + )?; + Ok(Self { + owned_locals: function + .locals + .iter() + .filter(|l| { + l.view_source.is_none() + && !program.resource_local(l) + && (crate::move_values::is_copy_owned(types, l.ty) + || (program.is_some() + && crate::move_values::is_linear(types, l.ty) + && !function.is_view_local(l.id))) + }) + .map(|l| l.id.index() as usize) + .collect(), + live_in, + live_out, + live_after_statement: after, + temporary_slots: max_temporaries, + generation_storage, + }) + } +} + +/// Reading a view also observes every alias and owner backing its value. Expand +/// before both initialization and liveness so cleanup cannot drop that owner. +fn expand_view_reads(function: &Function, reads: &mut Set) -> Result<(), String> { + for index in reads.clone() { + let mut local = function + .locals + .get(index) + .ok_or("native read is outside its local table")?; + if function.view_root(local.id).is_none() { + return Err("borrowed local origin is invalid or cyclic".into()); + } + while let Some(source) = local.view_source { + reads.insert(source.index() as usize); + local = function + .local(source) + .ok_or("borrowed local origin is outside its function")?; + } + } + Ok(()) +} + +pub(crate) fn switch_bindings_on_edge( + function: &Function, + source: crate::BlockId, + target: crate::BlockId, +) -> Set { + let TerminatorKind::Switch { + variants, + otherwise, + .. + } = &function.blocks[source.index() as usize].terminator.kind + else { + return Set::new(); + }; + let mut candidates = variants + .iter() + .filter(|(_, block, _)| *block == target) + .map(|(_, _, bindings)| { + bindings + .iter() + .map(|local| local.index() as usize) + .collect::() + }) + .collect::>(); + if *otherwise == Some(target) { + candidates.push(Set::new()); + } + let Some(mut common) = candidates.pop() else { + return Set::new(); + }; + for candidate in candidates { + common = common.intersection(&candidate).copied().collect(); + } + common +} +fn visit( + value: &Expression, + reads: &mut Set, + temporaries: &mut usize, + types: &TypeInterner, + program: PlanningContext<'_>, + borrowed: bool, +) -> Result<(), String> { + if let Some(context) = program.companion { + if !context.contains(value) { + return Err( + "Resource companion expression is outside its exact current function".into(), + ); + } + } + if !program.graph_expression(value) { + plan_type(types, value.ty, program)?; + } + if program.is_some() + && !program.resource_expression(value) + && crate::move_values::is_linear(types, value.ty) + { + *temporaries += usize::from(match &value.kind { + ExpressionKind::Local(_) => !borrowed, + ExpressionKind::Call { .. } + | ExpressionKind::IndirectCall { .. } + | ExpressionKind::Intrinsic { .. } + | ExpressionKind::Clone(_) + | ExpressionKind::ResultOk(_) + | ExpressionKind::ResultFail(_) + | ExpressionKind::OptionalSome(_) + | ExpressionKind::OptionalNone + | ExpressionKind::Join(_) + | ExpressionKind::ListConstruct { .. } + | ExpressionKind::MapConstruct { .. } + | ExpressionKind::StructConstruct { .. } + | ExpressionKind::EnumConstruct { .. } => true, + ExpressionKind::Field { .. } => !borrowed, + ExpressionKind::MachineConstruct { .. } | ExpressionKind::MachineTransition { .. } => { + true + } + ExpressionKind::BitfieldConstruct { .. } => true, + ExpressionKind::Run(inner) => { + matches!( + types.resolve(crate::move_values::representation_type(types, inner.ty)), + Type::Bytes + | Type::List(_) + | Type::Set(_) + | Type::Map(..) + | Type::Optional(_) + | Type::Result(..) + | Type::Struct(_) + | Type::Enum(_) + | Type::Bitfield(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Function { .. } + | Type::Interface(_) + | Type::TypeConstruction + ) + } + _ => false, + }); + } + // Count owning emitter operations, not copy-owned AST nodes. Children + // accumulate until full-expression cleanup; even short-circuit alternatives + // receive distinct slots during emission. View/clone add no ownership. + match &value.kind { + ExpressionKind::FunctionAdapter { .. } | ExpressionKind::InterfaceCoerce { .. } => { + *temporaries += 3; + } + ExpressionKind::RuntimeFailure(_) | ExpressionKind::PropertyCaseContext(Some(_)) => { + *temporaries += 1; + } + ExpressionKind::String(_) + | ExpressionKind::Local(_) + | ExpressionKind::Call { .. } + | ExpressionKind::IndirectCall { .. } + | ExpressionKind::Field { .. } + if !program.resource_expression(value) + && crate::move_values::is_copy_owned(types, value.ty) => + { + *temporaries += 1 + } + ExpressionKind::Intrinsic { + intrinsic: id, + args, + .. + } => { + if *id == IntrinsicId::GraphicsRun { + // The generated session loop stages callback results, scene and + // domain-result sums across its branches and backedge. + *temporaries += 16; + } else if matches!(id, IntrinsicId::Print | IntrinsicId::Println) { + // Empty output, argument concatenations, inter-argument spaces + // (literal + concat), and the optional newline (literal + concat). + *temporaries += 1 + args.len() + 2 * args.len().saturating_sub(1); + *temporaries += usize::from(*id == IntrinsicId::Println) * 2; + *temporaries += args.len(); + } else if crate::move_values::is_copy_owned(types, value.ty) { + // Copy-owned leaves and scalar conversion each own once. + *temporaries += 1; + } + } + ExpressionKind::StringInterpolation(segments) => { + // Initial empty literal plus one concatenation per segment. + *temporaries += 1 + segments.len(); + } + ExpressionKind::Run(value) + if matches!( + types.resolve(crate::move_values::representation_type(types, value.ty)), + Type::String + ) => + { + *temporaries += 1; + } + ExpressionKind::Join(value) + if matches!( + types.resolve(crate::move_values::representation_type(types, value.ty)), + Type::String + | Type::Bytes + | Type::List(_) + | Type::Set(_) + | Type::Map(..) + | Type::Optional(_) + | Type::Result(..) + | Type::Struct(_) + | Type::Enum(_) + | Type::Bitfield(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Function { .. } + | Type::Interface(_) + | Type::TypeConstruction + ) => + { + // Joining a pending owned value owns its extracted value before the + // result sum takes it. + *temporaries += 1; + } + ExpressionKind::FunctionRef(_) => { + // The descriptor and its source display label are owned temporaries. + *temporaries += 2; + } + ExpressionKind::ClosureRef { function, captures } => { + // The descriptor, environment, and source label are owned temporaries. + *temporaries += 3; + let closure = program + .and_then(|program| program.functions.get(function.index() as usize)) + .ok_or("closure capture needs its checked function")?; + if closure.capture_count != captures.len() { + return Err("closure capture count disagrees with its function".into()); + } + for param in closure.params.iter().take(closure.capture_count) { + *temporaries += usize::from(crate::move_values::is_copy_owned(types, param.ty)); + } + } + _ => {} + } + match &value.kind { + ExpressionKind::ResourceHookValue { .. } if program.companion.is_some() => {}, + ExpressionKind::ResourceInvoke { hook, args, evaluation_order, .. } if program.companion.is_some() => { + let Type::Function { view_params, .. } = types.resolve(hook.function_type()) else { return Err("Resource hook has no exact function signature".into()); }; + for ¶meter in evaluation_order { + visit(&args[parameter], reads, temporaries, types, program, view_params[parameter])?; + } + // Ordinary hook results still own normal runtime temporaries. + *temporaries += usize::from(!program.resource_expression(value) && (crate::move_values::is_copy_owned(types, value.ty) || crate::move_values::is_linear(types, value.ty))); + } + ExpressionKind::ResourceInvoke { .. } | ExpressionKind::ResourceHookValue { .. } => return Err("pending ResourceOwnershipPlan: ordinary expression liveness cannot own Resource operation or descriptor temporaries".into()), + ExpressionKind::Local(l) => { + reads.insert(l.index() as usize); + if program.is_some() && !program.resource_expression(value) && !borrowed && crate::move_values::is_linear(types, value.ty) { + // Test predicates clone local owners on each ordinary read; + // reserving this slot for all native functions is conservative. + *temporaries += 1; + } + } + ExpressionKind::ClosureRef { captures, .. } => { + reads.extend(captures.iter().map(|local| local.index() as usize)); + } + ExpressionKind::Int(_) + | ExpressionKind::Float(_) + | ExpressionKind::Bool(_) + | ExpressionKind::String(_) + | ExpressionKind::FunctionRef(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) => {} + ExpressionKind::Binary { left, op, right } => { + let enum_equality = + matches!(op, jett_hir::BinaryOp::Equal | jett_hir::BinaryOp::NotEqual) + && matches!( + types.resolve(crate::move_values::representation_type(types, left.ty)), + Type::Enum(_) + ); + visit(left, reads, temporaries, types, program, enum_equality)?; + visit(right, reads, temporaries, types, program, enum_equality)?; + // A custom payload method needs one owned traversal cursor. This + // is a conservative bound; pure primitive graphs need no cursor. + *temporaries += usize::from( + enum_equality + && program.is_some_and(|program| !program.equality_methods.is_empty()), + ); + } + ExpressionKind::OptionalNone if program.is_some() => {} + ExpressionKind::StructConstruct { + fields, + validates_refinements, + .. + } if program.is_some() => { + // A validated construction owns the record before wrapping it in + // a separately owned success sum. The expression type counts the + // sum above; reserve one more slot for the intermediate record. + *temporaries += usize::from(*validates_refinements); + for field in fields { + visit(field, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + } if program.is_some() => { + for &index in evaluation_order { + visit(&payloads[index], reads, temporaries, types, program, false)?; + } + } + ExpressionKind::MachineConstruct { payloads, .. } if program.is_some() => { + for payload in payloads { + visit(payload, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::MachineTransition { + source, payloads, .. + } if program.is_some() => { + visit(source, reads, temporaries, types, program, false)?; + for payload in payloads { + visit(payload, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::StateIs { value, .. } if program.is_some() => { + visit(value, reads, temporaries, types, program, true)?; + } + ExpressionKind::BitfieldConstruct { fields, .. } if program.is_some() => { + for field in fields { + visit(field, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::Field { base, .. } if program.is_some() => { + visit(base, reads, temporaries, types, program, true)?; + } + ExpressionKind::ListConstruct { elements } if program.is_some() => { + for element in elements { + visit(element, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::MapConstruct { entries } if program.is_some() => { + for entry in entries { + visit(&entry.key, reads, temporaries, types, program, false)?; + visit(&entry.value, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + if program.is_some() => + { + visit(value, reads, temporaries, types, program, false)? + } + ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) => { + visit(value, reads, temporaries, types, program, borrowed)? + } + ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::Unary { value, .. } => { + visit(value, reads, temporaries, types, program, false)? + } + ExpressionKind::View(value) + | ExpressionKind::Clone(value) + | ExpressionKind::RuntimeFailureMessage(value) => { + visit(value, reads, temporaries, types, program, true)? + } + ExpressionKind::Call { function, args, .. } => { + for (index, v) in args.iter().enumerate() { + let borrowed = program.is_some_and(|p| { + p.functions[function.index() as usize].params[index].mode + == crate::ParamMode::View + }); + visit(v, reads, temporaries, types, program, borrowed)?; + if !borrowed + && matches!(v.kind, ExpressionKind::View(_)) + && crate::move_values::is_linear(types, v.ty) + { + *temporaries += 1; + } + } + } + ExpressionKind::ActorGraphImport(_) => return Err("unexpanded actor graph import has no ordinary ownership plan".into()), + ExpressionKind::ActorGraphStep { instruction, args } => { + if program.graph_expression(value) + && matches!( + instruction.operation(), + jett_hir::ActorGraphOperation::HookBox { .. } + | jett_hir::ActorGraphOperation::RootPublish { .. } + ) + && (crate::move_values::is_copy_owned(types, value.ty) + || crate::move_values::is_linear(types, value.ty)) + { + *temporaries += 1; + } + for arg in args { + visit(arg, reads, temporaries, types, program, false)?; + } + } + ExpressionKind::ActorSpawn { + args, + evaluation_order, + constructor, + .. + } => { + if constructor.is_none() && program.is_some() { + // Allocation owns a record before transferring it to the actor registry. + *temporaries += 1; + } + for &index in evaluation_order { + let argument = &args[index]; + let borrowed = constructor.is_some_and(|id| { + program.is_some_and(|program| { + program.functions[id.index() as usize].params[index].mode + == crate::ParamMode::View + }) + }); + visit(argument, reads, temporaries, types, program, borrowed)?; + if !borrowed + && matches!(argument.kind, ExpressionKind::View(_)) + && crate::move_values::is_linear(types, argument.ty) + { + *temporaries += 1; + } + } + } + ExpressionKind::ActorMessage { + actor, + handler, + args, + evaluation_order, + .. + } => { + visit(actor, reads, temporaries, types, program, false)?; + let target = program + .and_then(|program| program.functions.get(handler.index() as usize)) + .ok_or("actor message needs its checked handler")?; + if crate::move_values::is_copy_owned(types, target.return_type) + || crate::move_values::is_linear(types, target.return_type) + { + // A send discards a response only after the call has owned it. + *temporaries += 1; + } + for &index in evaluation_order { + let argument = &args[index]; + let parameter = target + .params + .get(target.capture_count + index) + .ok_or("actor message argument is absent from its handler")?; + let _ = parameter; + let borrowed = false; // Actor boundary transfers its private incoming owner. + visit(argument, reads, temporaries, types, program, borrowed)?; + if !borrowed + && matches!(argument.kind, ExpressionKind::View(_)) + && crate::move_values::is_linear(types, argument.ty) + { + *temporaries += 1; + } + } + } + ExpressionKind::IndirectCall { callee, args, .. } => { + visit(callee, reads, temporaries, types, program, true)?; + let view_params = match types + .resolve(crate::move_values::representation_type(types, callee.ty)) + { + Type::Function { view_params, .. } if view_params.len() == args.len() => { + view_params + } + _ => return Err("indirect call has no matching function parameter modes".into()), + }; + for (argument, borrowed) in args.iter().zip(view_params) { + visit(argument, reads, temporaries, types, program, *borrowed)?; + if !*borrowed + && matches!(argument.kind, ExpressionKind::View(_)) + && crate::move_values::is_linear(types, argument.ty) + { + *temporaries += 1; + } + } + } + ExpressionKind::Intrinsic { + intrinsic, args, .. + } => { + for (index, v) in args.iter().enumerate() { + visit( + v, + reads, + temporaries, + types, + program, + crate::move_values::intrinsic_borrows(*intrinsic, index, v), + )?; + } + } + ExpressionKind::StringInterpolation(segments) => { + for s in segments { + match s { + StringSegment::Value(v) => { + visit(v, reads, temporaries, types, program, false)?; + // Formatting owns a string even when it retains an + // ordinary string or renders a pending one. + *temporaries += 1; + } + StringSegment::Text(_) => *temporaries += 1, + } + } + } + _ => return Err("expression needs explicit ownership lowering".into()), + } + Ok(()) +} + +fn copy_plan_type(types: &TypeInterner, ty: TypeId) -> Result<(), String> { + if ty.index() as usize >= types.len() { + return Err("invalid type in native ownership plan".into()); + } + if ty == TypeInterner::NEVER { + // An uninhabited sum arm or empty collection contributes no value to + // move, borrow, or drop. + return Ok(()); + } + if let Type::Secret(inner) | Type::Refinement { base: inner, .. } = types.resolve(ty) { + return copy_plan_type(types, *inner); + } + if let Type::Function { + params, + return_type, + .. + } = types.resolve(ty) + { + for param in params { + copy_plan_type(types, *param)?; + } + return copy_plan_type(types, *return_type); + } + if matches!( + types.resolve(ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::Bool + | Type::Nothing + | Type::String + | Type::Capability(_) + | Type::Actor(_) + | Type::Interface(_) + ) { + return Ok(()); + } + Err(format!( + "type {} requires separate move/borrow/drop analysis", + types.type_name(ty) + )) +} + +fn plan_type(types: &TypeInterner, ty: TypeId, program: PlanningContext<'_>) -> Result<(), String> { + plan_type_inner(types, ty, program, &mut BTreeSet::new()) +} +fn plan_type_inner( + types: &TypeInterner, + ty: TypeId, + program: PlanningContext<'_>, + seen: &mut BTreeSet, +) -> Result<(), String> { + if ty.index() as usize >= types.len() { + return Err("invalid native type".into()); + } + if !seen.insert(ty.index()) { + return Ok(()); + } + if program.carrier_type(ty) { + // The exact carrier graph owns these endpoints. Ordinary payload type + // checks remain recursive; no ordinary move/copy/drop permission is issued. + let mut children = Vec::new(); + match types.resolve(ty) { + Type::List(inner) | Type::Refinement { base: inner, .. } | Type::Optional(inner) => { + children.push(*inner) + } + Type::Map(key, value) | Type::Result(key, value) => children.extend([*key, *value]), + Type::Struct(id) => { + children.extend(types.resolve_struct(*id).fields.iter().map(|(_, ty)| *ty)) + } + Type::Enum(id) => children.extend( + types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter().map(|(_, ty)| *ty)), + ), + Type::Machine(id) | Type::MachineState { machine: id, .. } => children.extend( + types + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter().map(|(_, ty)| *ty)), + ), + _ => return Err("carrier companion type has no exact admitted graph family".into()), + } + for child in children { + plan_type_inner(types, child, program, seen)?; + } + return Ok(()); + } + if program.resource_type(ty) { + // The fresh custody proof owns only the carrier. Its ordinary companion + // and signature children keep their existing backend type checks. + match types.resolve(ty) { + Type::Resource(_) => return Ok(()), + Type::Optional(inner) => return plan_type_inner(types, *inner, program, seen), + Type::Result(ok, failure) => { + plan_type_inner(types, *ok, program, seen)?; + return plan_type_inner(types, *failure, program, seen); + } + Type::Function { + params, + return_type, + .. + } => { + for parameter in params { + plan_type_inner(types, *parameter, program, seen)?; + } + return plan_type_inner(types, *return_type, program, seen); + } + _ => return Err( + "pending Resource companion: qualified or aggregate custody type is unsupported" + .into(), + ), + } + } + if program.is_some() { + match types.resolve(ty) { + Type::Secret(inner) | Type::Refinement { base: inner, .. } => { + return plan_type_inner(types, *inner, program, seen); + } + Type::Function { + params, + return_type, + .. + } => { + for param in params { + plan_type_inner(types, *param, program, seen)?; + } + return plan_type_inner(types, *return_type, program, seen); + } + Type::Bytes | Type::TypeConstruction => return Ok(()), + Type::Struct(id) => { + for (_, field) in &types.resolve_struct(*id).fields { + plan_type_inner(types, *field, program, seen)?; + } + return Ok(()); + } + Type::Enum(id) => { + for variant in &types.resolve_enum(*id).variants { + for (_, field) in &variant.fields { + plan_type_inner(types, *field, program, seen)?; + } + } + return Ok(()); + } + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + for state in &types.resolve_machine(*id).states { + for (_, field) in &state.fields { + plan_type_inner(types, *field, program, seen)?; + } + } + return Ok(()); + } + Type::Bitfield(id) => { + for field in &types.resolve_bitfield(*id).fields { + plan_type_inner(types, field.ty, program, seen)?; + } + return Ok(()); + } + Type::List(inner) if *inner == TypeInterner::NEVER => return Ok(()), + Type::Optional(inner) | Type::List(inner) | Type::Set(inner) => { + return plan_type_inner(types, *inner, program, seen); + } + Type::Map(key, value) => { + plan_type_inner(types, *key, program, seen)?; + return plan_type_inner(types, *value, program, seen); + } + Type::Result(ok, error) => { + plan_type_inner(types, *ok, program, seen)?; + return plan_type_inner(types, *error, program, seen); + } + _ => {} + } + } + copy_plan_type(types, ty) +} diff --git a/crates/jett_mir/src/field_updates.rs b/crates/jett_mir/src/field_updates.rs new file mode 100644 index 00000000..17347e9f --- /dev/null +++ b/crates/jett_mir/src/field_updates.rs @@ -0,0 +1,661 @@ +//! Original checked field-write places and their preflight/commit pairing. +use crate::{ControlFlowGraph, Function, Program, StatementKind}; +use jett_hir::{self as hir, ExpressionKind, FieldUpdatePlace}; +use jett_types::TypeInterner; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Witness { + identity: hir::FunctionIdentity, + entry: crate::BlockId, + locals: Vec, + params: Vec, + blocks: Vec, + sites: Vec, +} +#[derive(Debug, Clone, PartialEq)] +struct Site { + place: FieldUpdatePlace, + target: hir::Expression, + value: hir::Expression, +} + +pub(crate) fn seal( + original: &hir::Function, + function: &Function, + types: &TypeInterner, +) -> Result, String> { + let mut sources = hir::field_update_places(&original.body) + .into_iter() + .cloned() + .collect::>(); + let mut sites = Vec::new(); + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::UpdateField { + target, + value, + place, + } = &statement.kind + { + let index = sources + .iter() + .position(|source| source == place) + .ok_or("field update has no exact original checked HIR place")?; + sources.remove(index); + place.validate(target, &function.locals, types)?; + sites.push(Site { + place: place.clone(), + target: target.clone(), + value: value.clone(), + }); + } + } + if !sources.is_empty() { + return Err("checked HIR field update was lost during MIR lowering".into()); + } + if sites.is_empty() { + return Ok(None); + } + let witness = Witness { + identity: original.identity.clone(), + entry: function.entry, + locals: function.locals.clone(), + params: function.params.clone(), + blocks: function.blocks.clone(), + sites, + }; + validate(function, types, Some(&witness))?; + Ok(Some(witness)) +} + +fn key(place: &FieldUpdatePlace) -> (u32, u32, u32, hir::FieldUpdateOccurrence) { + let span = place.span(); + (span.file.index(), span.start, span.end, place.occurrence()) +} + +fn validate( + function: &Function, + types: &TypeInterner, + witness: Option<&Witness>, +) -> Result<(), String> { + let mut preparations = BTreeMap::new(); + let mut commits = Vec::new(); + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + match &statement.kind { + StatementKind::PrepareFieldUpdate { place, begin: true } => { + place.validate(place.target(), &function.locals, types)?; + if place.required_scopes().len() != place.scoped_type_bindings().len() { + return Err( + "field update changed its checked scope/type-binding inventory".into(), + ); + } + for (scope, binding) in place + .required_scopes() + .iter() + .zip(place.scoped_type_bindings()) + { + if matches!( + scope.selection, + hir::FieldUpdateScopeSelection::ReflectedIteration(_) + ) { + crate::resource_ownership::validate_field_update_scope( + function, scope, binding, + )?; + } + } + if statement.span != place.span() + || preparations.insert(key(place), place).is_some() + { + return Err( + "field update preflight is duplicated or has changed Source association" + .into(), + ); + } + } + StatementKind::UpdateField { + target, + value, + place, + } => { + place.validate(target, &function.locals, types)?; + if function.is_view_local(place.root()) + || statement.span != place.span() + || value.ty != target.ty + { + return Err( + "field update commit changed ownership, Source association or field type" + .into(), + ); + } + commits.push(Site { + place: place.clone(), + target: target.clone(), + value: value.clone(), + }); + } + _ => {} + } + } + if commits.is_empty() && preparations.is_empty() { + return if witness.is_none() { + Ok(()) + } else { + Err("field update witness lost its current operations".into()) + }; + } + let witness = + witness.ok_or("field update has no constructor-authenticated original/current witness")?; + if witness.identity != function.identity + || witness.entry != function.entry + || witness.locals != function.locals + || witness.params != function.params + || !crate::breakpoint_regions::blocks_equal(&witness.blocks, &function.blocks) + || witness.sites != commits + || preparations.len() != commits.len() + || commits + .iter() + .any(|site| preparations.get(&key(&site.place)).copied() != Some(&site.place)) + { + return Err("field update changed its exact original/current operations".into()); + } + let cfg = ControlFlowGraph::analyze(function).map_err(|error| format!("{error:?}"))?; + let mut outgoing = vec![BTreeSet::new(); function.blocks.len()]; + loop { + let mut changed = false; + for block in cfg.reverse_postorder() { + let mut active = BTreeSet::new(); + for predecessor in cfg.predecessors(*block) { + active.extend(outgoing[predecessor.index() as usize].iter().copied()); + } + for statement in &function.blocks[block.index() as usize].statements { + match &statement.kind { + StatementKind::PrepareFieldUpdate { place, begin } => { + if *begin { + active.insert(key(place)); + } else { + active.remove(&key(place)); + } + } + StatementKind::UpdateField { place, .. } => { + active.remove(&key(place)); + } + _ => {} + } + } + if outgoing[block.index() as usize] != active { + outgoing[block.index() as usize] = active; + changed = true; + } + } + if !changed { + break; + } + } + let reachable = cfg + .reverse_postorder() + .iter() + .copied() + .collect::>(); + for block in cfg.reverse_postorder() { + let predecessors = cfg + .predecessors(*block) + .iter() + .filter(|predecessor| reachable.contains(*predecessor)) + .collect::>(); + let mut active = if *block == function.entry { + BTreeSet::new() + } else { + predecessors + .first() + .map_or_else(BTreeSet::new, |predecessor| { + outgoing[predecessor.index() as usize].clone() + }) + }; + if predecessors + .iter() + .any(|predecessor| outgoing[predecessor.index() as usize] != active) + { + return Err("field update reservations disagree across a reachable CFG merge".into()); + } + for statement in &function.blocks[block.index() as usize].statements { + match &statement.kind { + StatementKind::PrepareFieldUpdate { place, begin: true } => { + if !active.insert(key(place)) + || active + .iter() + .filter_map(|key| preparations.get(key)) + .filter(|other| other.root() == place.root()) + .count() + != 1 + { + return Err( + "field update root is already reserved by an enclosing RHS".into() + ); + } + } + StatementKind::PrepareFieldUpdate { + place, + begin: false, + } => { + if !active.remove(&key(place)) { + return Err("field update abort has no active preflight".into()); + } + } + StatementKind::UpdateField { place, .. } => { + if !active.remove(&key(place)) { + return Err( + "field update commit has no dominating ancestor preflight".into() + ); + } + } + StatementKind::Assign { target, .. } + if matches!(target.kind, ExpressionKind::Local(_)) => + { + let ExpressionKind::Local(local) = target.kind else { + unreachable!() + }; + if active + .iter() + .filter_map(|key| preparations.get(key)) + .any(|place| place.root() == local) + { + return Err("field update RHS overwrites its reserved root".into()); + } + } + StatementKind::Let { local, .. } + | StatementKind::ReplaceCallOwnerGeneration { root: local, .. } => { + if active + .iter() + .filter_map(|key| preparations.get(key)) + .any(|place| place.root() == *local) + { + return Err("field update RHS changes its reserved root generation".into()); + } + } + _ => {} + } + } + } + Ok(()) +} + +pub(crate) fn has_records(function: &Function) -> bool { + function.field_updates.is_some() +} + +fn refresh_canonical( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + let Some(original) = &before.field_updates else { + return Ok(()); + }; + if function.identity != before.identity + || function.entry != before.entry + || function.params != before.params + || !function.locals.starts_with(&before.locals) + { + return Err( + "field update canonical preparation changed original binding identities".into(), + ); + } + let sites = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| { + if let StatementKind::UpdateField { + target, + value, + place, + } = &statement.kind + { + Some(Site { + place: place.clone(), + target: target.clone(), + value: value.clone(), + }) + } else { + None + } + }) + .collect::>(); + if sites != original.sites { + return Err("field update canonical preparation changed its original commits".into()); + } + let mut next = original.clone(); + next.locals = function.locals.clone(); + next.blocks = function.blocks.clone(); + next.entry = function.entry; + function.field_updates = Some(next); + validate(function, types, function.field_updates.as_ref()) +} + +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types, before.field_updates.as_ref())?; + let mut shadow = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + refresh_canonical(function, before, types) +} + +pub(super) fn sum_transition( + function: &mut Function, + before: &Function, + types: &TypeInterner, +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + validate(before, types, before.field_updates.as_ref())?; + let mut shadow = function.clone(); + crate::breakpoint_regions::sum_transition(&mut shadow, before, types)?; + refresh_canonical(function, before, types) +} + +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + if !has_records(before) { + return Ok(()); + } + let witness = before + .field_updates + .as_ref() + .ok_or("field update compaction lost its witness")?; + if !crate::breakpoint_regions::blocks_equal(&before.blocks, &witness.blocks) + || before.entry != witness.entry + || before.locals != witness.locals + || before.params != witness.params + { + return Err("field update compaction started from changed current Source".into()); + } + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let reachable = cfg + .reverse_postorder() + .iter() + .copied() + .collect::>(); + if map.len() != before.blocks.len() + || before + .blocks + .iter() + .any(|block| map[block.id.index() as usize].is_some() != reachable.contains(&block.id)) + { + return Err("field update block map is not exact reachable compaction".into()); + } + let get = |id: crate::BlockId| { + map.get(id.index() as usize) + .copied() + .flatten() + .ok_or("field update surviving block has no exact map") + }; + let mut expected = before + .blocks + .iter() + .filter(|block| reachable.contains(&block.id)) + .cloned() + .collect::>(); + for block in &mut expected { + block.id = get(block.id)?; + crate::sequences::prune::block_targets(&mut block.terminator.kind, &|target| { + *target = map[target.index() as usize].expect("reachable target has map"); + }); + } + if !crate::breakpoint_regions::blocks_equal(&expected, &function.blocks) + || function.entry != get(before.entry)? + || function.locals != before.locals + || function.params != before.params + { + return Err("field update block compaction changed surviving operations".into()); + } + let mut next = witness.clone(); + next.blocks = function.blocks.clone(); + next.entry = function.entry; + // A removed complete update requires its own absent-commit certificate; + // this bounded transition retains every original update until then. + if next.sites.iter().any(|site| !function.blocks.iter().flat_map(|block| &block.statements).any(|statement| + matches!(&statement.kind, StatementKind::UpdateField { target, value, place } if target == &site.target && value == &site.value && place == &site.place))) { + return Err("field update compaction removed a commit without an abort certificate".into()); + } + function.field_updates = Some(next); + Ok(()) +} + +pub(crate) fn validate_function(function: &Function, types: &TypeInterner) -> Result<(), String> { + validate(function, types, function.field_updates.as_ref()) +} + +pub fn validate_field_updates(program: &Program, types: &TypeInterner) -> Result<(), String> { + for function in &program.functions { + validate(function, types, function.field_updates.as_ref())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{FileId, SourceOrigin}; + use std::collections::HashMap; + + const SOURCE: &str = "struct Record:\n value: int64\n sibling: int64\nfunction next_value(view record: Record) returns int64:\n return record.value + 1\nfunction update(mutable record: Record) returns Record:\n record.value = next_value(view record)\n return record\n"; + const SEQUENCE: &str = "struct Record:\n value: int64\n sibling: int64\nfunction update(mutable record: Record) returns Record:\n list[int64] steps = list(2, 3)\n for step in steps:\n record.value = record.value + step\n return record\n"; + fn source_program(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("normally checked HIR"); + let program = crate::lower(&high, &checked.interner).expect("checked-place MIR"); + (program, checked.interner) + } + fn update(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "update") + .unwrap() + } + #[test] + fn source_field_update_keeps_rhs_temporary_view_and_rejects_proof_corruption() { + let (program, types) = source_program(SOURCE); + validate_field_updates(&program, &types).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "update") + .unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("RHS call view ends before commit"); + for corruption in 0..4 { + let mut changed = program.clone(); + let function = update(&mut changed); + match corruption { + 0 => function.field_updates = None, + 1 => { + for block in &mut function.blocks { + block.statements.retain(|statement| { + !matches!( + statement.kind, + StatementKind::PrepareFieldUpdate { begin: true, .. } + ) + }); + } + } + 2 => { + for block in &mut function.blocks { + for statement in &mut block.statements { + if let StatementKind::UpdateField { target, .. } = &mut statement.kind { + let ExpressionKind::Field { field, .. } = &mut target.kind else { + panic!("checked field target") + }; + *field = hir::FieldId::new(1); + } + } + } + } + 3 => { + for block in &mut function.blocks { + if let Some(index) = block.statements.iter().position(|statement| { + matches!( + statement.kind, + StatementKind::PrepareFieldUpdate { begin: true, .. } + ) + }) { + let preparation = block.statements.remove(index); + let commit = block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::UpdateField { .. }) + }) + .unwrap(); + block.statements.insert(commit, preparation); + break; + } + } + } + _ => unreachable!(), + } + assert!( + validate_field_updates(&changed, &types).is_err(), + "corruption {corruption}" + ); + } + } + #[test] + fn source_field_update_survives_canonical_sequence_preparation_and_refuses_foreign_remap() { + let (mut program, types) = source_program(SEQUENCE); + crate::prepare_native_sequences(&mut program, &types); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + validate_field_updates(&program, &types) + .expect("canonical native preparation retains exact Source places"); + let before = update(&mut program).clone(); + assert!( + !before.blocks.iter().any(|block| matches!( + block.terminator.kind, + crate::TerminatorKind::ForEach { .. } + )) + ); + let mut function = before.clone(); + let map = vec![None; before.blocks.len()]; + assert!(remap_blocks(&mut function, &before, &map).is_err()); + function.blocks[0].statements.clear(); + assert!(validate(&function, &types, function.field_updates.as_ref()).is_err()); + } +} + +#[cfg(test)] +mod merge_tests { + use super::*; + #[test] + fn fresh_seal_rejects_preflight_missing_on_one_diamond_edge() { + let source = "struct Record:\n value: int64\nfunction update(mutable record: Record) returns Record:\n record.value = record.value + 1\n return record\n"; + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &std::collections::HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .unwrap(); + let program = crate::lower(&high, &checked.interner).unwrap(); + let original = &high.functions[0]; + let mut function = program.functions[0].clone(); + assert_eq!(function.blocks.len(), 1); + let mut continuation = function.blocks.remove(0); + let begin = continuation.statements.remove(0); + assert!(matches!( + begin.kind, + StatementKind::PrepareFieldUpdate { begin: true, .. } + )); + let span = begin.span; + continuation.id = crate::BlockId(3); + function.blocks = vec![ + crate::BasicBlock { + id: crate::BlockId(0), + statements: Vec::new(), + terminator: crate::Terminator { + kind: crate::TerminatorKind::Branch { + condition: hir::Expression { + kind: ExpressionKind::Bool(true), + ty: TypeInterner::BOOL, + span, + }, + then_block: crate::BlockId(1), + else_block: crate::BlockId(2), + }, + span, + }, + }, + crate::BasicBlock { + id: crate::BlockId(1), + statements: vec![begin], + terminator: crate::Terminator { + kind: crate::TerminatorKind::Goto(crate::BlockId(3)), + span, + }, + }, + crate::BasicBlock { + id: crate::BlockId(2), + statements: Vec::new(), + terminator: crate::Terminator { + kind: crate::TerminatorKind::Goto(crate::BlockId(3)), + span, + }, + }, + continuation, + ]; + assert!(seal(original, &function, &checked.interner).is_err()); + } +} + +#[cfg(test)] +mod reflected_tests; diff --git a/crates/jett_mir/src/field_updates/reflected_tests.rs b/crates/jett_mir/src/field_updates/reflected_tests.rs new file mode 100644 index 00000000..93edc8ae --- /dev/null +++ b/crates/jett_mir/src/field_updates/reflected_tests.rs @@ -0,0 +1,158 @@ +//! Source-derived ordinal composition and wrong-arm proof controls. +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + sync::Arc, +}; + +fn source_program() -> ( + hir::Program, + Program, + Arc, +) { + // Shared genuine Driver Source; keep its existing report body unchanged. + let source = include_str!( + "../../../../tests/native/field_update_reference/31_reflected_iteration_update.jett" + ); + let source = source + .split("function main(") + .next() + .expect("report precedes main"); + let file = FileId::new(0); + let list_file = FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut parsed = jett_parser::parse(include_str!("../../../../stdlib/list.jett"), list_file); + let mut project = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(project.errors.is_empty(), "{:?}", project.errors); + // Preserve the genuine report AST and its spans; supply its shipped list + // API through the same separate-file stdlib merge used by other MIR tests. + parsed.module.items.append(&mut project.module.items); + let original = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (list_file, SourceOrigin::Stdlib), + (file, SourceOrigin::Project), + ]), + &[], + jett_typecheck::CheckOptions::default(), + ) + .expect("normally checked ordinary reflected Source"), + ); + let high = hir::lower_checked_ordinary_program(&original).expect("original ordinal HIR"); + let program = crate::lower(&high, &original.checked().interner).expect("same-span ordinal MIR"); + (high, program, original) +} + +#[test] +fn same_outer_field_writes_keep_all_three_original_ordinal_contexts() { + let (high, mut program, original) = source_program(); + let high_report = high + .functions + .iter() + .find(|f| f.identity.declaration.name == "report") + .unwrap(); + let places = hir::field_update_places(&high_report.body); + assert_eq!(places.len(), 9); + assert_eq!( + places + .iter() + .map(|place| place.root()) + .collect::>() + .len(), + 1 + ); + assert_eq!( + places + .iter() + .map(|place| place.occurrence()) + .collect::>() + .len(), + 9 + ); + let ordinals = places + .iter() + .flat_map(|place| place.required_scopes()) + .filter_map(|scope| { + if let hir::FieldUpdateScopeSelection::ReflectedIteration(ordinal) = scope.selection { + Some(ordinal) + } else { + None + } + }) + .collect::>(); + for ordinal in 0..3 { + assert_eq!( + ordinals + .iter() + .filter(|selected| **selected == ordinal) + .count(), + 3 + ); + } + validate_field_updates(&program, &original.checked().interner).unwrap(); + crate::prepare_native_sequences(&mut program, &original.checked().interner); + crate::prepare_native_uninhabited_sums(&mut program, &original.checked().interner); + crate::prepare_native_generated_functions(&mut program, &original.checked().interner); + validate_field_updates(&program, &original.checked().interner) + .expect("exact ordinal Source proof survives canonical preparation"); +} + +#[test] +fn fresh_seal_refuses_same_shape_place_copied_or_swapped_between_ordinal_arms() { + let (high, program, original) = source_program(); + let high_report = high + .functions + .iter() + .find(|f| f.identity.declaration.name == "report") + .unwrap(); + let report = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "report") + .unwrap(); + let mut sites = Vec::new(); + for (block, body) in report.blocks.iter().enumerate() { + for (index, statement) in body.statements.iter().enumerate() { + if let StatementKind::UpdateField { place, .. } = &statement.kind { + if place.span() == hir::field_update_places(&high_report.body)[0].span() { + sites.push((block, index, place.clone())); + } + } + } + } + assert_eq!(sites.len(), 3); + for swap in [false, true] { + let mut changed = report.clone(); + // Ordinals 0 and 2 both bind int64 and update the same outer root/path. + // Swap every matching begin/commit pair; unique paired keys and the + // complete original Source-place multiset remain intact in swap=true. + let first = &sites[0].2; + let second = &sites[2].2; + assert_eq!(first.root(), second.root()); + assert_eq!(first.target(), second.target()); + assert_eq!(first.scoped_type_bindings(), second.scoped_type_bindings()); + for body in &mut changed.blocks { + for statement in &mut body.statements { + if let StatementKind::PrepareFieldUpdate { place, .. } + | StatementKind::UpdateField { place, .. } = &mut statement.kind + { + if place.occurrence() == second.occurrence() { + *place = first.clone(); + } else if swap && place.occurrence() == first.occurrence() { + *place = second.clone(); + } + } + } + } + let error = seal(high_report, &changed, &original.checked().interner) + .expect_err("wrong original ordinal placement must not mint a field witness"); + if swap { + assert!( + error.contains("initially authenticated Source"), + "independent original/current archive gate: {error}" + ); + } + } +} diff --git a/crates/jett_mir/src/generated_functions.rs b/crates/jett_mir/src/generated_functions.rs new file mode 100644 index 00000000..8494519b --- /dev/null +++ b/crates/jett_mir/src/generated_functions.rs @@ -0,0 +1,31 @@ +//! Compact local tables shared by enclosing and explicitly lifted inline bodies. +use super::*; + +#[cfg(test)] +mod tests; + +/// Compact generated inline functions independently of their enclosing function. +/// Enclosing functions lose only unused local slots, preserving every block. +/// Capture and ordinary parameter order remain the existing callable ABI. +pub fn prepare_native_generated_functions(program: &mut Program, types: &TypeInterner) { + if crate::actor_graph::validate_native_actor_graphs(program).is_err() { + return; + } + if validate_call_ownership(program, types).is_err() { + return; + } + for function in &mut program.functions { + let before = function.clone(); + let _prepared = if function.debug_kind == hir::FunctionDebugKind::Inline { + sequences::prune::unreachable(function) + } else { + sequences::prune::unused_locals(function) + }; + if crate::reflected_callable::prepared(function, &before).is_err() { + *function = before; + } + } + for function in &mut program.functions { + crate::actor_graph::refresh(function); + } +} diff --git a/crates/jett_mir/src/generated_functions/tests.rs b/crates/jett_mir/src/generated_functions/tests.rs new file mode 100644 index 00000000..09d8c3b7 --- /dev/null +++ b/crates/jett_mir/src/generated_functions/tests.rs @@ -0,0 +1,440 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::ExpressionKind; +use std::collections::{HashMap, HashSet}; + +fn lower_source(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let hir = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + ( + lower(&hir, &checked.interner).expect("MIR lowering"), + checked.interner, + ) +} + +fn assert_dense_reachable(function: &Function) { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let mut pending = vec![function.entry]; + let mut reachable = HashSet::new(); + while let Some(block) = pending.pop() { + if reachable.insert(block) { + pending.extend_from_slice(cfg.successors(block)); + } + } + assert_eq!(reachable.len(), function.blocks.len()); + for (index, block) in function.blocks.iter().enumerate() { + assert_eq!(block.id.index() as usize, index); + } + for (index, local) in function.locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + } +} + +fn assert_named_control_flow_and_abi_preserved(function: &Function, original: &Function) { + assert_eq!(function.id, original.id); + assert_eq!(function.identity, original.identity); + assert_eq!(function.debug_kind, original.debug_kind); + assert_eq!(function.return_type, original.return_type); + assert_eq!(function.span, original.span); + assert_eq!(function.entry, original.entry); + assert_eq!(function.capture_count, original.capture_count); + assert_eq!(function.params.len(), original.params.len()); + for (parameter, old_parameter) in function.params.iter().zip(&original.params) { + let mut expected = old_parameter.clone(); + expected.local = parameter.local; + assert_eq!(parameter, &expected); + let mut expected_local = original.local(old_parameter.local).unwrap().clone(); + expected_local.id = parameter.local; + assert_eq!(function.local(parameter.local), Some(&expected_local)); + } + assert_eq!(function.blocks.len(), original.blocks.len()); + let before = ControlFlowGraph::analyze(original).unwrap(); + let after = ControlFlowGraph::analyze(function).unwrap(); + for (block, old_block) in function.blocks.iter().zip(&original.blocks) { + assert_eq!(block.id, old_block.id); + assert_eq!(block.statements.len(), old_block.statements.len()); + assert_eq!(block.terminator.span, old_block.terminator.span); + assert_eq!( + std::mem::discriminant(&block.terminator.kind), + std::mem::discriminant(&old_block.terminator.kind), + ); + assert_eq!(after.successors(block.id), before.successors(old_block.id)); + for (statement, old_statement) in block.statements.iter().zip(&old_block.statements) { + assert_eq!(statement.span, old_statement.span); + assert_eq!( + std::mem::discriminant(&statement.kind), + std::mem::discriminant(&old_statement.kind), + ); + } + } + for (index, local) in function.locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + } +} + +const AFTER_LOOP: &str = r#"namespace app +function main(unused: int64) returns int64: + for impossible in list(): + trace impossible + int64 kept = 40 + function(view int64, int64) returns int64 callback = function(view borrowed: int64, value: int64) returns int64: return kept + value + return callback(view 10, 2) +"#; + +#[test] +fn live_captured_callback_discards_parent_never_slots_without_changing_capture_abi() { + let (mut program, types) = lower_source(AFTER_LOOP); + prepare_native_sequences(&mut program, &types); + prepare_native_uninhabited_sums(&mut program, &types); + let original = program + .functions + .iter() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .unwrap() + .clone(); + assert!( + original + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + let named = program + .functions + .iter() + .filter(|function| function.debug_kind != hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(); + prepare_native_generated_functions(&mut program, &types); + validate(&program).unwrap(); + let function = &program.functions[original.id.index() as usize]; + assert_dense_reachable(function); + assert!( + !function + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + assert_eq!(function.id, original.id); + assert_eq!(function.identity, original.identity); + assert_eq!(function.debug_kind, original.debug_kind); + assert_eq!(function.return_type, original.return_type); + assert_eq!(function.span, original.span); + assert_eq!(function.capture_count, 1); + assert_eq!(function.capture_count, original.capture_count); + assert_eq!(function.params.len(), original.params.len()); + for (parameter, old_parameter) in function.params.iter().zip(&original.params) { + let mut expected = old_parameter.clone(); + expected.local = parameter.local; + assert_eq!(parameter, &expected); + let mut expected_local = original.local(old_parameter.local).unwrap().clone(); + expected_local.id = parameter.local; + assert_eq!(function.local(parameter.local), Some(&expected_local)); + } + assert_eq!(function.params[0].name, "kept"); + assert_eq!(function.params[1].name, "borrowed"); + assert_eq!(function.params[1].mode, ParamMode::View); + assert_eq!(function.params[2].name, "value"); + assert!(function.blocks.iter().any(|block| matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: ExpressionKind::Binary { left, right, .. }, + .. + })) if matches!(left.kind, ExpressionKind::Local(local) if local == function.params[0].local) + && matches!(right.kind, ExpressionKind::Local(local) if local == function.params[2].local) + ))); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.debug_kind != hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(), + named, + "caller capture references remain in the caller's own local table" + ); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("captured parameter and body references remain valid"); + let prepared = program.clone(); + prepare_native_generated_functions(&mut program, &types); + assert_eq!(program, prepared); +} + +#[test] +fn callbacks_after_absent_error_arms_preserve_live_capture_and_dead_function_ids() { + let (mut program, types) = lower_source( + r#"namespace app +function main() returns int64: + int64 ready = ok(1) handle error: + function() returns int64 ignored = function() returns int64: return 9 + default ignored() + function() returns int64 callback = function() returns int64: return ready + return callback() +"#, + ); + prepare_native_uninhabited_sums(&mut program, &types); + let original = program.clone(); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .count(), + 2 + ); + prepare_native_generated_functions(&mut program, &types); + validate(&program).unwrap(); + assert_eq!(program.functions.len(), original.functions.len()); + for (function, previous) in program.functions.iter().zip(&original.functions) { + assert_eq!(function.id, previous.id); + if function.debug_kind != hir::FunctionDebugKind::Inline { + assert_eq!(function, previous); + continue; + } + assert_dense_reachable(function); + assert!( + previous + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + assert!( + !function + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + assert_eq!(function.capture_count, previous.capture_count); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("independent callback local table"); + } + let live = program + .functions + .iter() + .find(|function| { + function.debug_kind == hir::FunctionDebugKind::Inline && function.capture_count == 1 + }) + .unwrap(); + assert_eq!(live.params[0].name, "ready"); + assert!(live.blocks.iter().any(|block| matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(Expression { kind: ExpressionKind::Local(local), .. })) if *local == live.params[0].local + ))); +} + +#[test] +fn dead_inline_never_parameters_remain_uninhabited_instead_of_becoming_values() { + let (mut program, types) = lower_source( + r#"namespace app +function choose[T, E](outcome: result[T, E], fallback: T) returns T: + return outcome handle error: + function(E) returns nothing ignored = function(item: E) returns nothing: return nothing + ignored(error) + default fallback +function main() returns int64: + return choose(ok(7), 0) +"#, + ); + prepare_native_uninhabited_sums(&mut program, &types); + let original = program + .functions + .iter() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .unwrap() + .clone(); + assert_eq!(original.params[0].ty, TypeInterner::NEVER); + prepare_native_generated_functions(&mut program, &types); + validate(&program).unwrap(); + let function = &program.functions[original.id.index() as usize]; + assert_dense_reachable(function); + assert_eq!(function.id, original.id); + assert_eq!(function.capture_count, 0); + assert_eq!(function.params.len(), 1); + assert_eq!(function.params[0].ty, TypeInterner::NEVER); + assert_eq!(function.locals.len(), 1, "unused ABI parameter remains"); + assert_eq!(function.locals[0].ty, TypeInterner::NEVER); + assert_eq!(function.return_type, TypeInterner::NOTHING); + // Reachability, rather than invented storage for never, decides whether this + // separately lifted body needs a callable native declaration. +} + +#[test] +fn generated_metadata_selection_preserves_named_functions_even_with_inline_like_names() { + let (mut program, types) = lower_source(AFTER_LOOP); + let function = program + .functions + .iter_mut() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .unwrap(); + function.identity.declaration.name = "parent$inline123_456".into(); + function.debug_kind = hir::FunctionDebugKind::Named("app.source_visible_name".into()); + validate(&program).unwrap(); + let original = program.clone(); + prepare_native_generated_functions(&mut program, &types); + validate(&program).unwrap(); + for (function, previous) in program.functions.iter().zip(&original.functions) { + assert_named_control_flow_and_abi_preserved(function, previous); + } +} + +#[test] +fn generated_compaction_does_not_hide_invalid_unreachable_or_parameter_ids() { + for corruption in 0..3 { + let (mut program, types) = lower_source(AFTER_LOOP); + let function = program + .functions + .iter_mut() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .unwrap(); + match corruption { + 0 => function.entry = BlockId(u32::MAX), + 1 => function.params[0].local = LocalId::new(u32::MAX), + _ => function.blocks.push(BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![Statement { + kind: StatementKind::Trace(LocalId::new(u32::MAX)), + span: function.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }), + } + assert!(validate(&program).is_err()); + let original = program.clone(); + prepare_native_generated_functions(&mut program, &types); + assert_eq!(program, original); + assert!(validate(&program).is_err()); + } +} + +#[test] +fn factory_compaction_keeps_unused_descriptor_parameters_and_original_block_dependencies() { + let source = r#"namespace app +function consume_value[T](value: T) returns int64: + return 0 +function stored[T](values: list[T]) returns list[function(T) returns int64]: + int64 kept = 40 + function(T) returns int64 callback = function(ignored: T) returns int64: return kept + return list(callback) +function root() returns int64: + return consume_value(stored(list())) +"#; + let (original, types) = lower_source(source); + let factory = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "stored") + .expect("source factory"); + let factory_id = factory.id; + let detached = factory + .locals + .iter() + .find(|local| local.ty == TypeInterner::NEVER) + .expect("inline formal remains in the shared parent table") + .id; + let descriptor = original + .functions + .iter() + .find(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .expect("stored descriptor target"); + let descriptor_id = descriptor.id; + assert_eq!(descriptor.capture_count, 1); + assert_eq!(descriptor.params[1].ty, TypeInterner::NEVER); + assert_eq!(descriptor.params[1].mode, ParamMode::Owned); + + let mut compacted = original.clone(); + prepare_native_generated_functions(&mut compacted, &types); + validate(&compacted).unwrap(); + let compacted_factory = &compacted.functions[factory_id.index() as usize]; + assert_named_control_flow_and_abi_preserved(compacted_factory, factory); + assert!( + !compacted_factory + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + let StatementKind::Let { value, .. } = &compacted_factory.blocks[0].statements[1].kind else { + panic!("source callback binding"); + }; + let ExpressionKind::ClosureRef { function, captures } = &value.kind else { + panic!("captured descriptor construction"); + }; + assert_eq!(*function, descriptor_id); + assert_eq!(captures.len(), 1); + assert_eq!(compacted_factory.local(captures[0]).unwrap().name, "kept"); + move_values::MoveValuePlan::analyze(&compacted, compacted_factory, &types) + .expect("enclosing capture initialization and ownership survive local remapping"); + let compacted_descriptor = &compacted.functions[descriptor_id.index() as usize]; + assert_eq!(compacted_descriptor.identity, descriptor.identity); + assert_eq!(compacted_descriptor.capture_count, 1); + assert_eq!(compacted_descriptor.params.len(), 2); + for (parameter, previous) in compacted_descriptor.params.iter().zip(&descriptor.params) { + let mut expected = previous.clone(); + expected.local = parameter.local; + assert_eq!(parameter, &expected); + } + assert_eq!(compacted_descriptor.params[1].ty, TypeInterner::NEVER); + assert_eq!( + compacted_descriptor + .local(compacted_descriptor.params[1].local) + .unwrap() + .ty, + TypeInterner::NEVER + ); + + // A reference in an original disconnected block prevents the parent slot + // from being treated as unused. This pass must preserve that later proof + // obligation instead of using callable reachability to hide it. + let mut with_dependency = original.clone(); + let factory = &mut with_dependency.functions[factory_id.index() as usize]; + let dependency = BlockId(factory.blocks.len() as u32); + factory.blocks.push(BasicBlock { + id: dependency, + statements: vec![Statement { + kind: StatementKind::Trace(detached), + span: factory.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: factory.span, + }, + }); + validate(&with_dependency).unwrap(); + let before = with_dependency.functions[factory_id.index() as usize].clone(); + prepare_native_generated_functions(&mut with_dependency, &types); + validate(&with_dependency).unwrap(); + let factory = &with_dependency.functions[factory_id.index() as usize]; + assert_named_control_flow_and_abi_preserved(factory, &before); + let StatementKind::Trace(kept_dependency) = + factory.blocks[dependency.index() as usize].statements[0].kind + else { + panic!("original disconnected trace must survive"); + }; + assert_eq!( + factory.local(kept_dependency).unwrap().ty, + TypeInterner::NEVER + ); + assert_eq!( + factory.local(kept_dependency).unwrap().debug_ty, + TypeInterner::NEVER + ); +} diff --git a/crates/jett_mir/src/handlers.rs b/crates/jett_mir/src/handlers.rs new file mode 100644 index 00000000..af416bb1 --- /dev/null +++ b/crates/jett_mir/src/handlers.rs @@ -0,0 +1,5498 @@ +//! Extract source result/optional handlers into ordinary MIR CFG edges. +use super::*; +use jett_hir::{ExpressionKind, HandleKind}; +use jett_types::{Type, TypeInterner}; + +mod reflected; +mod resource_calls; +#[path = "handlers/resource_constructors.rs"] +mod resource_constructors; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum LoweringRequirement { + #[cfg(test)] + Handlers, + HandlersAndCallOwnership, +} + +#[cfg(test)] +fn has_extractable_handle(expression: &Expression) -> bool { + expression_needs_lowering(expression, LoweringRequirement::Handlers, &|_| false) +} + +#[cfg(test)] +fn needs_eager_lowering(expression: &Expression) -> bool { + expression_needs_lowering( + expression, + LoweringRequirement::HandlersAndCallOwnership, + &|_| false, + ) +} + +fn expression_needs_lowering( + expression: &Expression, + requirement: LoweringRequirement, + source_call: &impl Fn(&Expression) -> bool, +) -> bool { + let needs = |value: &Expression| expression_needs_lowering(value, requirement, source_call); + let includes_call_ownership = requirement == LoweringRequirement::HandlersAndCallOwnership; + match &expression.kind { + ExpressionKind::ActorGraphImport(_) => true, + ExpressionKind::ActorGraphStep { args, .. } => args.iter().any(needs), + ExpressionKind::Handle { + kind: + HandleKind::Result + | HandleKind::Optional + | HandleKind::Refinement { .. } + | HandleKind::ReflectedReturn { .. }, + .. + } => true, + ExpressionKind::View(value) | ExpressionKind::FunctionAdapter { value, .. } => needs(value), + ExpressionKind::Clone(value) => needs(value), + ExpressionKind::Run(value) | ExpressionKind::Join(value) => needs(value), + ExpressionKind::Coarsen(value) + | ExpressionKind::Declassify(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::InterfaceType(value) => needs(value), + ExpressionKind::Field { base, .. } => needs(base), + ExpressionKind::StateIs { value, .. } => needs(value), + ExpressionKind::Unary { value, .. } => needs(value), + ExpressionKind::Binary { left, right, .. } => needs(left) || needs(right), + ExpressionKind::Call { + args, ownership, .. + } => { + (includes_call_ownership + && (needs_call_owner_staging(ownership) || source_call(expression))) + || args.iter().any(needs) + } + ExpressionKind::ActorSpawn { args, .. } => args.iter().any(needs), + ExpressionKind::ActorMessage { actor, args, .. } => needs(actor) || args.iter().any(needs), + ExpressionKind::Intrinsic { + args, + refinement_predicates, + field_validation, + ownership, + .. + } => { + (includes_call_ownership && needs_call_owner_staging(ownership)) + || args.iter().any(needs) + || refinement_predicates.iter().any(|chain| !chain.is_empty()) + || matches!( + field_validation, + Some(hir::ReflectedFieldValidation::Validate(_)) + ) + } + ExpressionKind::IndirectCall { + callee, + args, + ownership, + .. + } => { + (includes_call_ownership && needs_call_owner_staging(ownership)) + || needs(callee) + || args.iter().any(needs) + } + ExpressionKind::ListConstruct { elements } => elements.iter().any(needs), + ExpressionKind::StringInterpolation(segments) => segments + .iter() + .any(|segment| matches!(segment, hir::StringSegment::Value(value) if needs(value))), + ExpressionKind::MapConstruct { entries } => entries + .iter() + .any(|entry| needs(&entry.key) || needs(&entry.value)), + ExpressionKind::StructConstruct { + fields, + refinement_predicates, + .. + } => { + fields.iter().any(needs) || refinement_predicates.iter().any(|chain| !chain.is_empty()) + } + ExpressionKind::BitfieldConstruct { fields, .. } => fields.iter().any(needs), + ExpressionKind::EnumConstruct { payloads, .. } + | ExpressionKind::MachineConstruct { payloads, .. } => payloads.iter().any(needs), + ExpressionKind::MachineTransition { + source, payloads, .. + } => needs(source) || payloads.iter().any(needs), + ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) => needs(value), + _ => false, + } +} + +pub(super) fn can_snapshot_view(types: &TypeInterner, ty: TypeId) -> bool { + fn supported( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + if !seen.insert(ty) { + return true; + } + match types.resolve(ty) { + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::TypeConstruction + | Type::Function { .. } => true, + Type::List(inner) => *inner == TypeInterner::NEVER || supported(types, *inner, seen), + Type::Set(inner) | Type::Secret(inner) | Type::Refinement { base: inner, .. } => { + supported(types, *inner, seen) + } + Type::Optional(inner) => { + *inner == TypeInterner::NEVER || supported(types, *inner, seen) + } + Type::Result(ok, error) => { + match (*ok == TypeInterner::NEVER, *error == TypeInterner::NEVER) { + (true, true) => false, + (true, false) => supported(types, *error, seen), + (false, true) => supported(types, *ok, seen), + (false, false) => supported(types, *ok, seen) && supported(types, *error, seen), + } + } + Type::Map(key, value) => { + *key == TypeInterner::NEVER + || *value == TypeInterner::NEVER + || (supported(types, *key, seen) && supported(types, *value, seen)) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field_ty)| supported(types, *field_ty, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, field_ty)| supported(types, *field_ty, seen)) + }), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| supported(types, field.ty, seen)), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, field_ty)| supported(types, *field_ty, seen)) + }) + } + Type::Resource(_) + | Type::Capability(_) + | Type::Actor(_) + | Type::Interface(_) + | Type::Never + | Type::Error => false, + } + } + + supported(types, ty, &mut std::collections::HashSet::new()) +} + +fn valid_ordered_owned_values( + types: &TypeInterner, + locals: &[Local], + values: &[Expression], + order: &[usize], +) -> bool { + // MIR has no borrowed temporary. Cloneable views can be snapshotted into + // owned locals before a later handler changes their source. + order.len() == values.len() + && order.iter().all(|&index| index < values.len()) + && order + .iter() + .copied() + .collect::>() + .len() + == values.len() + && values.iter().all(|value| { + !matches!(value.kind, ExpressionKind::View(_)) + || can_snapshot_view(types, value.ty) + || stable_deferred_view(locals, value) + }) +} + +fn stable_deferred_view(locals: &[Local], expression: &Expression) -> bool { + let ExpressionKind::View(value) = &expression.kind else { + return false; + }; + let ExpressionKind::Local(id) = value.kind else { + return false; + }; + let Some(root) = jett_hir::local_view_root(locals, id) else { + return false; + }; + locals + .get(root.index() as usize) + .is_some_and(|local| local.id == root && !local.mutable) +} + +/// Preserve an already-checked retained place until its one endpoint snapshot. +/// Generic value lowering may snapshot a Local below Coarsen/Declassify; that +/// inner acquisition would change the original Source binding/field witness. +fn retained_snapshot_place(expression: &Expression) -> Option { + let mut place = expression; + loop { + place = match &place.kind { + ExpressionKind::Local(_) => return Some(expression.clone()), + ExpressionKind::Field { base, .. } => base, + ExpressionKind::View(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::Declassify(value) => value, + _ => return None, + }; + } +} + +/// Ordered staging distinguishes actual borrows from an endpoint whose checked +/// Source effect requires an owning transfer before physical View access. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OrderedCallViewInput { + Unstaged, + Borrowed, + SourceOwningTransfer, + SourceHandledTemporary, +} + +/// Only the private original Source Handle witness can authorize this storage. +/// The physical View wrapper is distinct from its checked conversion endpoint. +fn source_handled_view_endpoint<'a>( + types: &TypeInterner, + ownership: &hir::CallOwnership, + index: usize, + expression: &'a Expression, +) -> Option<&'a Expression> { + use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, + }; + let hir::CallOwnership::Source(source) = ownership else { + return None; + }; + let argument = source.arguments.get(index)?; + if argument.staging != hir::ArgumentStaging::Original + || argument.origin != hir::CallerOrigin::OwnedExpression + || argument.syntax != Syntax::WrittenView + || argument.effect != Effect::RetainBorrow + || argument.physical_access != Access::View + { + return None; + } + let ExpressionKind::View(endpoint) = &expression.kind else { + return None; + }; + if endpoint.ty != expression.ty || matches!(endpoint.kind, ExpressionKind::View(_)) { + return None; + } + let original = + hir::validate_source_handled_operand(expression, argument, &source.bridge, types) + .ok() + .flatten() + .or_else(|| { + // A canonical physical View can wrap the original conversion tree. + // That tree must itself rejoin the same private checked occurrence. + hir::validate_source_handled_operand(endpoint, argument, &source.bridge, types) + .ok() + .flatten() + })?; + matches!( + original.kind, + ExpressionKind::Handle { + kind: HandleKind::Optional | HandleKind::Result, + .. + } + ) + .then_some(endpoint.as_ref()) +} + +fn ordered_call_view_input(ownership: &hir::CallOwnership, index: usize) -> OrderedCallViewInput { + use jett_typecheck::{CheckedCalleeAccess as Access, CheckedCallerEffect as Effect}; + match ownership { + hir::CallOwnership::Source(source) => { + let Some(argument) = source.arguments.get(index) else { + return OrderedCallViewInput::Unstaged; + }; + if argument.staging != hir::ArgumentStaging::Original + || argument.physical_access != Access::View + { + return OrderedCallViewInput::Unstaged; + } + match argument.effect { + Effect::RelinquishOwned => OrderedCallViewInput::SourceOwningTransfer, + Effect::TransferOwned if source.bridge != hir::CallBridge::Direct => { + OrderedCallViewInput::SourceOwningTransfer + } + Effect::RetainBorrow | Effect::ObserveData => OrderedCallViewInput::Borrowed, + _ => OrderedCallViewInput::Unstaged, + } + } + hir::CallOwnership::Generated(generated) => { + if generated.arguments.get(index).is_some_and(|argument| { + argument.callee_access == Access::View + && matches!( + argument.acquisition, + hir::GeneratedAcquisition::Borrow { .. } | hir::GeneratedAcquisition::Copy + ) + }) { + OrderedCallViewInput::Borrowed + } else { + OrderedCallViewInput::Unstaged + } + } + } +} + +/// Retained/generated raw borrows keep their existing origin eligibility. +/// A checked Source owning transfer instead materializes the full endpoint. +fn valid_ordered_borrowed_values( + types: &TypeInterner, + locals: &[Local], + values: &[Expression], + order: &[usize], + inputs: &[OrderedCallViewInput], +) -> bool { + inputs.len() == values.len() + && order.len() == values.len() + && order.iter().all(|&index| index < values.len()) + && order + .iter() + .copied() + .collect::>() + .len() + == values.len() + && values.iter().enumerate().all(|(index, value)| { + !matches!(value.kind, ExpressionKind::View(_)) + || matches!( + inputs[index], + OrderedCallViewInput::SourceOwningTransfer + | OrderedCallViewInput::SourceHandledTemporary + ) + || can_snapshot_view(types, value.ty) + || stable_deferred_view(locals, value) + || (inputs[index] == OrderedCallViewInput::Borrowed + && (crate::call_views::borrowed_source(types, locals, value).is_some() + || crate::call_views::temporary_root(types, locals, value).is_some())) + }) +} + +#[derive(Clone, Copy)] +enum ObservationSnapshot { + Direct, + Converted { + actual_type: TypeId, + source_span: Span, + }, +} + +fn observation_snapshot( + types: &TypeInterner, + value: &Expression, + ownership: &hir::CallOwnership, + index: usize, +) -> Option { + let hir::CallOwnership::Source(source) = ownership else { + return None; + }; + let argument = source.arguments.get(index)?; + argument.observation_proof()?; + if can_snapshot_view(types, value.ty) { + return Some(ObservationSnapshot::Direct); + } + let ExpressionKind::InterfaceCoerce { .. } = &value.kind else { + return None; + }; + let mut raw = value; + while let ExpressionKind::InterfaceCoerce { value, .. } = &raw.kind { + raw = value; + } + if raw.ty != argument.actual_type + || raw.span != argument.source_span + || !hir::observation_data_type(types, raw.ty) + || !can_snapshot_view(types, raw.ty) + { + return None; + } + Some(ObservationSnapshot::Converted { + actual_type: argument.actual_type, + source_span: argument.source_span, + }) +} + +fn snapshotable_local(types: &TypeInterner, expression: &Expression) -> bool { + if let ExpressionKind::InterfaceCoerce { value, .. } = &expression.kind { + return snapshotable_local(types, value); + } + matches!(expression.kind, ExpressionKind::Local(_)) + && crate::move_values::is_linear(types, expression.ty) + && can_snapshot_view(types, expression.ty) +} + +fn snapshot_local(expression: &Expression, lowered: Expression) -> Expression { + if let ( + ExpressionKind::InterfaceCoerce { value: source, .. }, + ExpressionKind::InterfaceCoerce { value, adapters }, + ) = (&expression.kind, &lowered.kind) + { + return Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(snapshot_local(source, value.as_ref().clone())), + adapters: adapters.clone(), + }, + ty: lowered.ty, + span: lowered.span, + }; + } + Expression { + kind: ExpressionKind::Clone(Box::new(lowered)), + ty: expression.ty, + span: expression.span, + } +} + +/// Staging preserves a checked call's borrowing modes. Owned arguments keep +/// their source ownership and must never acquire an implicit alias snapshot. +fn call_staging_inputs(values: &[Expression], modes: &[ParamMode]) -> Option> { + if values.len() != modes.len() { + return None; + } + values + .iter() + .zip(modes) + .map(|(value, mode)| { + if *mode == ParamMode::View { + Some(if matches!(value.kind, ExpressionKind::View(_)) { + value.clone() + } else { + Expression { + kind: ExpressionKind::View(Box::new(value.clone())), + ty: value.ty, + span: value.span, + } + }) + } else if matches!(value.kind, ExpressionKind::View(_)) { + // The frontend rejects an explicit view at an owned boundary. + // Leave invalid HIR unsupported instead of manufacturing a copy. + None + } else { + Some(value.clone()) + } + }) + .collect() +} + +fn needs_call_owner_staging(ownership: &hir::CallOwnership) -> bool { + (0..ownership.parameter_count()).any(|index| { + ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + || (ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) + && matches!( + ownership.source_parameter_effect(index), + Some( + jett_typecheck::CheckedCallerEffect::RelinquishOwned + | jett_typecheck::CheckedCallerEffect::TransferOwned + ) + )) + }) +} + +impl Builder<'_> { + /// Stage a checked invocation in lexical source order. Physical borrowing + /// never grants retention: a relinquished input first acquires its own slot. + fn lower_ordered_call_values( + &mut self, + original_call: &Expression, + values: &[Expression], + order: &[usize], + ownership: &mut hir::CallOwnership, + ) -> Option> { + if values + .iter() + .any(|value| crate::resource_type_pending(self.types, value.ty)) + { + self.resource_error = Some(LowerError { span: original_call.span, message: "pending ResourceOwnershipPlan: ordinary call-view staging cannot acquire Resource custody".into() }); + return None; + } + // A staging attempt changes CFG, locals, handlers and active scopes. + // Restore all of them if a later operand cannot prove its acquisition. + let checkpoint = self.clone(); + let original_ownership = ownership.clone(); + let result = self.try_lower_ordered_call_values(original_call, values, order, ownership); + if result.is_none() { + *self = checkpoint; + *ownership = original_ownership; + } + result + } + + fn lower_converted_observation_snapshot( + &mut self, + value: &Expression, + actual_type: TypeId, + source_span: Span, + ) -> Option { + if let ExpressionKind::InterfaceCoerce { + value: raw, + adapters, + } = &value.kind + { + return Some(Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(self.lower_converted_observation_snapshot( + raw, + actual_type, + source_span, + )?), + adapters: adapters.clone(), + }, + ty: value.ty, + span: value.span, + }); + } + if value.ty != actual_type || value.span != source_span { + return None; + } + Some(Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(value))), + ty: value.ty, + span: value.span, + }) + } + + fn try_lower_ordered_call_values( + &mut self, + original_call: &Expression, + values: &[Expression], + order: &[usize], + ownership: &mut hir::CallOwnership, + ) -> Option> { + if ownership.parameter_count() != values.len() { + return None; + } + // Authenticate original handled endpoints before scopes/CFG are changed. + let handled = values + .iter() + .enumerate() + .map(|(index, value)| { + source_handled_view_endpoint(self.types, ownership, index, value).cloned() + }) + .collect::>(); + let inputs = (0..values.len()) + .map(|index| { + if handled[index].is_some() { + OrderedCallViewInput::SourceHandledTemporary + } else { + ordered_call_view_input(ownership, index) + } + }) + .collect::>(); + if !valid_ordered_borrowed_values(self.types, &self.locals, values, order, &inputs) { + return None; + } + if let hir::CallOwnership::Source(source) = ownership { + if source + .arguments + .iter() + .any(|argument| argument.staging != hir::ArgumentStaging::Original) + { + return None; + } + } + // Refuse an unsupported snapshot before changing the builder or scopes. + if values.iter().enumerate().any(|(index, value)| { + ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + && observation_snapshot(self.types, value, ownership, index).is_none() + }) { + return None; + } + self.call_view_scopes.push(Vec::new()); + self.generation_capture + .begin_scope(original_call, self.types); + let mut lowered = values.to_vec(); + for &index in order { + lowered[index] = self.lower_ordered_call_argument( + index, + &values[index], + handled[index].as_ref(), + inputs[index], + ownership, + )?; + } + Some(lowered) + } + + #[inline(never)] + fn lower_ordered_call_argument( + &mut self, + index: usize, + actual: &Expression, + handled: Option<&Expression>, + input: OrderedCallViewInput, + ownership: &mut hir::CallOwnership, + ) -> Option { + if let Some(endpoint) = handled { + // Keep the complete original conversion and inner Handle header. + // This endpoint already produces an owner; no Clone is inserted. + let value = self.lower_value(endpoint); + if value.ty != endpoint.ty || value.span != endpoint.span { + return None; + } + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + endpoint.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: endpoint.ty, + span: endpoint.span, + })), + ty: actual.ty, + span: actual.span, + }; + return Some(self.stage_checked_call_view(index, None, owner, projection, ownership)?); + } + if input == OrderedCallViewInput::Borrowed + && matches!(ownership, hir::CallOwnership::Generated(_)) + { + return Some(self.lower_generated_call_view(index, actual, ownership)?); + } + if ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::ObserveData) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + { + let snapshot = observation_snapshot(self.types, actual, ownership, index)?; + let value = match snapshot { + ObservationSnapshot::Direct => Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(actual))), + ty: actual.ty, + span: actual.span, + }, + ObservationSnapshot::Converted { + actual_type, + source_span, + } => self.lower_converted_observation_snapshot(actual, actual_type, source_span)?, + }; + let owner = self.temporary(value.ty, value.span); + ownership.stage_observation(index, owner).ok()?; + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + return Some(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + }); + } + if input == OrderedCallViewInput::SourceOwningTransfer { + let ExpressionKind::View(original) = &actual.kind else { + return None; + }; + // Evaluate the full endpoint/producer, not a projected parent. + // An ordinary Let moves a linear owner; no snapshot is inserted. + let value = self.lower_value(original); + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + original.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: original.ty, + span: original.span, + })), + ty: actual.ty, + span: actual.span, + }; + return Some(self.stage_checked_call_view( + index, + Some(owner), + owner, + projection, + ownership, + )?); + } + if input == OrderedCallViewInput::Borrowed + && let hir::CallOwnership::Source(source) = ownership + && source.arguments[index].retained_snapshot_type() == Some(actual.ty) + && can_snapshot_view(self.types, actual.ty) + { + let endpoint = + retained_snapshot_place(actual).unwrap_or_else(|| self.lower_value(actual)); + let value = Expression { + kind: ExpressionKind::Clone(Box::new(endpoint)), + ty: actual.ty, + span: actual.span, + }; + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + })), + ty: actual.ty, + span: actual.span, + }; + let loan = self.call_view_temporary(projection.ty, owner, projection.span); + ownership.stage_retained_snapshot(index, owner, loan).ok()?; + self.push( + StatementKind::BeginCallView { + local: loan, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(loan); + return Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(loan), + ty: projection.ty, + span: projection.span, + })), + ty: projection.ty, + span: projection.span, + }); + } + if input == OrderedCallViewInput::Borrowed + && let Some((source, projection)) = self.call_view_initializer(actual) + { + return Some(self.stage_checked_call_view(index, None, source, projection, ownership)?); + } + if ownership.source_parameter_effect(index) + == Some(jett_typecheck::CheckedCallerEffect::TransferOwned) + && ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::Owned) + { + // Acquire the endpoint at this source position. A later + // handler cannot turn a required transfer into a snapshot. + let value = self.lower_value(actual); + let owner = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: owner, + value, + }, + actual.span, + ); + ownership.stage_acquisition(index, owner).ok()?; + return Some(Expression { + kind: ExpressionKind::Local(owner), + ty: actual.ty, + span: actual.span, + }); + } + let single = self.lower_ordered_owned_values(std::slice::from_ref(actual), &[0])?; + let lowered = single.into_iter().next()?; + if matches!( + ownership.source_parameter_effect(index), + Some( + jett_typecheck::CheckedCallerEffect::TransferOwned + | jett_typecheck::CheckedCallerEffect::Copy + ) + ) && let ExpressionKind::Local(local) = lowered.kind + { + ownership.stage_acquisition(index, local).ok()?; + } + Some(lowered) + } + + fn lower_generated_call_view( + &mut self, + index: usize, + value: &Expression, + ownership: &mut hir::CallOwnership, + ) -> Option { + let hir::CallOwnership::Generated(generated) = ownership else { + return None; + }; + let argument = generated.arguments.get(index)?; + if argument.staging != (hir::GeneratedArgumentStaging::Existing { loan: None }) { + return None; + } + let witness = argument.original_witness(); + let producer = witness.owned_producer(); + let snapshot = + witness.ordinary_snapshot_proof().is_some() && can_snapshot_view(self.types, value.ty); + let ExpressionKind::View(endpoint) = &value.kind else { + return None; + }; + let (source, projection, backing) = if producer || snapshot { + // Capture the full endpoint at this lexical argument position. + // A produced owner moves once; ordinary data uses its sealed copy. + let initial = if producer { + self.lower_value(endpoint) + } else { + Expression { + kind: ExpressionKind::Clone(Box::new(self.lower_value(value))), + ty: value.ty, + span: value.span, + } + }; + let owner = self.temporary(initial.ty, initial.span); + self.push( + StatementKind::Let { + local: owner, + value: initial, + }, + value.span, + ); + let projection = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(owner), + ty: value.ty, + span: endpoint.span, + })), + ty: value.ty, + span: value.span, + }; + (owner, projection, Some((owner, producer))) + } else { + let source = crate::call_views::borrowed_source(self.types, &self.locals, value)?; + (source, value.clone(), None) + }; + let loan = self.call_view_temporary(projection.ty, source, projection.span); + let staging = match backing { + Some((owner, true)) => hir::GeneratedArgumentStaging::OwnedProducer { owner, loan }, + Some((owner, false)) => hir::GeneratedArgumentStaging::OrdinarySnapshot { owner, loan }, + None => hir::GeneratedArgumentStaging::Existing { loan: Some(loan) }, + }; + ownership.stage_generated_parameter(index, staging).ok()?; + self.push( + StatementKind::BeginCallView { + local: loan, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(loan); + Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(loan), + ty: projection.ty, + span: projection.span, + })), + ty: projection.ty, + span: projection.span, + }) + } + + fn stage_checked_call_view( + &mut self, + index: usize, + acquired_owner: Option, + source: LocalId, + projection: Expression, + ownership: &mut hir::CallOwnership, + ) -> Option { + let generation = if acquired_owner.is_none() { + self.generation_capture + .candidate( + index, + source, + &projection, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + &self.locals, + self.types, + ) + .ok()? + } else { + None + }; + if let Some((generation, true)) = generation { + self.push( + StatementKind::OpenCallOwnerGeneration { + generation, + root: source, + }, + projection.span, + ); + } + let local = self.call_view_temporary(projection.ty, source, projection.span); + if let Some((generation, _)) = generation { + self.generation_capture + .capture_loan( + generation, + local, + index, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .ok()?; + } + ownership + .stage_parameter(index, acquired_owner, local) + .ok()?; + self.push( + StatementKind::BeginCallView { + local, + value: projection.clone(), + }, + projection.span, + ); + self.call_view_scopes.last_mut()?.push(local); + Some(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(local), + ty: projection.ty, + span: projection.span, + })), + ty: projection.ty, + span: projection.span, + }) + } + + fn refinement_predicate_input( + &self, + source: LocalId, + source_type: TypeId, + predicate: &hir::RefinementPredicate, + span: Span, + ) -> Expression { + let source_value = Expression { + kind: ExpressionKind::Local(source), + ty: source_type, + span, + }; + let mut input = Expression { + kind: ExpressionKind::Clone(Box::new(source_value)), + ty: source_type, + span, + }; + if input.ty != predicate.base_type && input.ty != predicate.input_type { + input = Expression { + kind: ExpressionKind::Coarsen(Box::new(input)), + ty: predicate.base_type, + span, + }; + } + if input.ty != predicate.input_type { + input = Expression { + kind: ExpressionKind::Declassify(Box::new(input)), + ty: predicate.input_type, + span, + }; + } + input + } + + pub(super) fn temporary(&mut self, ty: TypeId, span: Span) -> LocalId { + let id = LocalId::new(self.locals.len() as u32); + self.locals.push(Local { + id, + name: format!("$native{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: true, + view_source: None, + span, + }); + self.resource_capture + .local(&self.locals[id.index() as usize]); + id + } + + fn call_view_temporary(&mut self, ty: TypeId, source: LocalId, span: Span) -> LocalId { + let id = LocalId::new(self.locals.len() as u32); + self.locals.push(Local { + id, + name: format!("$callView{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: false, + view_source: Some(source), + span, + }); + self.view_params.push(id); + self.resource_capture + .local(&self.locals[id.index() as usize]); + id + } + + fn call_view_initializer(&mut self, expression: &Expression) -> Option<(LocalId, Expression)> { + if let Some(source) = + crate::call_views::borrowed_source(self.types, &self.locals, expression) + { + return Some((source, expression.clone())); + } + let root = crate::call_views::temporary_root(self.types, &self.locals, expression)?; + // Ordinary owning storage for the original producer, evaluated once. + // The stage borrows that storage; neither step clones erased authority. + let value = self.lower_value(&root); + let source = self.temporary(root.ty, root.span); + self.push( + StatementKind::Let { + local: source, + value, + }, + root.span, + ); + let rewritten = crate::call_views::replace_borrowed_root( + expression, + Expression { + kind: ExpressionKind::Local(source), + ty: root.ty, + span: root.span, + }, + )?; + if crate::call_views::borrowed_source(self.types, &self.locals, &rewritten) != Some(source) + { + return None; + } + Some((source, rewritten)) + } + + fn finish_call_view_scope(&mut self, expression: Expression) -> Expression { + let Some(scope) = self.call_view_scopes.pop() else { + return expression; + }; + let generations = self.generation_capture.end_scope(); + if scope.is_empty() { + debug_assert!(generations.is_empty()); + return expression; + } + // Materialize the complete consuming operation before ending its loans. + let local = self.temporary(expression.ty, expression.span); + self.push( + StatementKind::Let { + local, + value: expression.clone(), + }, + expression.span, + ); + for stage in scope.into_iter().rev() { + self.push(StatementKind::EndCallView { local: stage }, expression.span); + } + for generation in generations.into_iter().rev() { + self.push( + StatementKind::CloseCallOwnerGeneration { generation }, + expression.span, + ); + self.generation_capture.stop(generation); + } + Expression { + kind: ExpressionKind::Local(local), + ty: expression.ty, + span: expression.span, + } + } + + pub(super) fn end_call_views_since(&mut self, depth: usize, span: Span) { + let abandoned = self + .call_view_scopes + .iter() + .skip(depth) + .rev() + .flat_map(|scope| scope.iter().rev().copied()) + .collect::>(); + for local in abandoned { + self.push(StatementKind::EndCallView { local }, span); + } + for generation in self.generation_capture.abandoned(depth) { + self.push(StatementKind::CloseCallOwnerGeneration { generation }, span); + } + } + + fn lower_ordered_owned_values( + &mut self, + values: &[Expression], + order: &[usize], + ) -> Option> { + if let Some(value) = values + .iter() + .find(|value| crate::resource_type_pending(self.types, value.ty)) + { + self.resource_error = Some(LowerError { span: value.span, message: "pending ResourceOwnershipPlan: ordinary owning operand staging cannot acquire Resource custody".into() }); + return None; + } + if !valid_ordered_owned_values(self.types, &self.locals, values, order) { + return None; + } + let mut lowered = values.to_vec(); + for &index in order { + // Reading an immutable local through a view has no effect and + // cannot change which value is observed after an earlier handler. + // Keep noncopyable views at the call instead of cloning authority. + if stable_deferred_view(&self.locals, &values[index]) + && !can_snapshot_view(self.types, values[index].ty) + { + continue; + } + let mut value = self.lower_value(&values[index]); + if matches!(values[index].kind, ExpressionKind::View(_)) + && (crate::move_values::is_linear(self.types, value.ty) + || crate::move_values::is_copy_owned(self.types, value.ty)) + { + value = Expression { + kind: ExpressionKind::Clone(Box::new(value)), + ty: values[index].ty, + span: values[index].span, + }; + } + let local = self.temporary(value.ty, value.span); + self.push(StatementKind::Let { local, value }, values[index].span); + lowered[index] = Expression { + kind: ExpressionKind::Local(local), + ty: values[index].ty, + span: values[index].span, + }; + } + Some(lowered) + } + + /// Include authenticated ordinary endpoints of the checked Resource family + /// in the same source-order extraction used for handlers. + fn needs_eager_lowering(&self, expression: &Expression) -> bool { + expression_needs_lowering( + expression, + LoweringRequirement::HandlersAndCallOwnership, + &|value| { + self.resource_capture + .normalized_call_needed(value, self.types) + }, + ) + } + + pub(super) fn lower_value(&mut self, expression: &Expression) -> Expression { + if let ExpressionKind::ActorGraphImport(plan) = &expression.kind { + self.lower_block(plan.body()); + return Expression { + kind: ExpressionKind::Local(plan.output()), + ty: plan.output_type(), + span: expression.span, + }; + } + if self + .resource_capture + .constructor_needed(expression, self.types) + { + return match self.lower_resource_constructor(expression) { + Ok(value) => value, + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + expression.clone() + } + }; + } + if self + .resource_capture + .normalized_call_needed(expression, self.types) + { + return match self.lower_resource_call(expression) { + Ok(value) => value, + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + expression.clone() + } + }; + } + match self + .resource_capture + .preserve_original_call(expression, self.types) + { + Ok(true) => return expression.clone(), + Err(message) => { + self.resource_error = Some(LowerError { + span: expression.span, + message, + }); + return expression.clone(); + } + Ok(false) => {} + } + if let ExpressionKind::ResourceInvoke { args, .. } = &expression.kind { + // Typed preservation only. Extracting actuals would require the dedicated owner plan. + if args.iter().any(|value| self.needs_eager_lowering(value)) { + self.resource_error = Some(LowerError { span: expression.span, message: "pending ResourceOwnershipPlan: Resource actual extraction requires dedicated custody staging".into() }); + } + return expression.clone(); + } + + if let Some(lowered) = self.try_lower_value_wrapper(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_binary(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_refinement(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_collection(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_sum(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_intrinsic(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_actor(expression) { + return lowered; + } + if let Some(lowered) = self.try_lower_value_call(expression) { + return lowered; + } + self.lower_sum_handle(expression) + } + + #[inline(never)] + fn try_lower_value_wrapper(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::FunctionAdapter { value, function } = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::FunctionAdapter { + value: Box::new(self.lower_value(value)), + function: *function, + }; + return Some(lowered); + } + if let ExpressionKind::InterfaceCoerce { value, adapters } = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::InterfaceCoerce { + value: Box::new(self.lower_value(value)), + adapters: adapters.clone(), + }; + return Some(lowered); + } + if let ExpressionKind::InterfaceType(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::InterfaceType(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::RefinementValidated(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::RefinementValidated(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::DisplayResult(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::DisplayResult(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::EquatableResult(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::EquatableResult(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::RuntimeFailureMessage(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::RuntimeFailureMessage(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::View(value) = &expression.kind { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::View(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::Clone(value) = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Clone(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::Run(value) = &expression.kind { + let needs_snapshot = snapshotable_local(self.types, value); + if self.needs_eager_lowering(value) || needs_snapshot { + let mut lowered = expression.clone(); + let source = self.lower_value(value); + // `run` does not consume a cloneable local in the interpreter. + let source = if needs_snapshot { + snapshot_local(value, source) + } else { + source + }; + lowered.kind = ExpressionKind::Run(Box::new(source)); + return Some(lowered); + } + } + if let ExpressionKind::Join(value) = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Join(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::Coarsen(value) = &expression.kind + && (self.needs_eager_lowering(value) || snapshotable_local(self.types, value)) + { + let mut lowered = expression.clone(); + let source = self.lower_value(value); + let source = if snapshotable_local(self.types, value) { + snapshot_local(value, source) + } else { + source + }; + lowered.kind = ExpressionKind::Coarsen(Box::new(source)); + return Some(lowered); + } + if let ExpressionKind::Declassify(value) = &expression.kind + && (self.needs_eager_lowering(value) || snapshotable_local(self.types, value)) + { + let mut lowered = expression.clone(); + let source = self.lower_value(value); + let source = if snapshotable_local(self.types, value) { + snapshot_local(value, source) + } else { + source + }; + lowered.kind = ExpressionKind::Declassify(Box::new(source)); + return Some(lowered); + } + if let ExpressionKind::Field { + base, + owner_type, + field, + } = &expression.kind + && self.needs_eager_lowering(base) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Field { + base: Box::new(self.lower_value(base)), + owner_type: *owner_type, + field: *field, + }; + return Some(lowered); + } + if let ExpressionKind::StateIs { value, state } = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::StateIs { + value: Box::new(self.lower_value(value)), + state: *state, + }; + return Some(lowered); + } + if let ExpressionKind::Unary { op, value } = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Unary { + op: *op, + value: Box::new(self.lower_value(value)), + }; + return Some(lowered); + } + None + } + + #[inline(never)] + fn try_lower_value_binary(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::Binary { left, op, right } = &expression.kind + && matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) + && (self.needs_eager_lowering(left) || self.needs_eager_lowering(right)) + { + let left_value = self.lower_value(left); + if !self.needs_eager_lowering(right) { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Binary { + left: Box::new(left_value), + op: *op, + right: right.clone(), + }; + return Some(lowered); + } + let span = expression.span; + let left_local = self.temporary(TypeInterner::BOOL, left.span); + self.push( + StatementKind::Let { + local: left_local, + value: left_value, + }, + left.span, + ); + let right_block = self.new_block(right.span); + let bypass = self.new_block(span); + let continuation = self.new_block(span); + let output = self.temporary(TypeInterner::BOOL, span); + let (then_block, else_block) = if *op == hir::BinaryOp::And { + (right_block, bypass) + } else { + (bypass, right_block) + }; + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(left_local), + ty: TypeInterner::BOOL, + span: left.span, + }, + then_block, + else_block, + }, + span, + ); + self.current = bypass; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::Bool(*op == hir::BinaryOp::Or), + ty: TypeInterner::BOOL, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = right_block; + let right_value = self.lower_value(right); + self.push( + StatementKind::Let { + local: output, + value: right_value, + }, + span, + ); + self.close_to(continuation, span); + self.current = continuation; + return Some(Expression { + kind: ExpressionKind::Local(output), + ty: TypeInterner::BOOL, + span, + }); + } + if let ExpressionKind::Binary { left, op, right } = &expression.kind + && !matches!(op, hir::BinaryOp::And | hir::BinaryOp::Or) + && can_snapshot_view(self.types, left.ty) + && (self.needs_eager_lowering(left) || self.needs_eager_lowering(right)) + { + // Save the left value before extracting a handler from the right. + // Its failure block may mutate locals that the left side reads. + let mut left_value = self.lower_value(left); + if crate::move_values::is_linear(self.types, left.ty) { + let borrowed = if matches!(left_value.kind, ExpressionKind::View(_)) { + left_value + } else { + Expression { + kind: ExpressionKind::View(Box::new(left_value)), + ty: left.ty, + span: left.span, + } + }; + left_value = Expression { + kind: ExpressionKind::Clone(Box::new(borrowed)), + ty: left.ty, + span: left.span, + }; + } + let left_local = self.temporary(left.ty, left.span); + self.push( + StatementKind::Let { + local: left_local, + value: left_value, + }, + left.span, + ); + let right_value = self.lower_value(right); + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Binary { + left: Box::new(Expression { + kind: ExpressionKind::Local(left_local), + ty: left.ty, + span: left.span, + }), + op: *op, + right: Box::new(right_value), + }; + return Some(lowered); + } + None + } + + #[inline(never)] + fn try_lower_value_refinement(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::Handle { + target, + kind: HandleKind::ReflectedReturn { plan, .. }, + .. + } = &expression.kind + { + let value = self.lower_value(target); + let source = self.temporary(target.ty, expression.span); + self.push( + StatementKind::Let { + local: source, + value, + }, + expression.span, + ); + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: false, + }, + expression.span, + ); + let previous = self.reflected_produced_return; + self.reflected_produced_return = true; + self.lower_reflected_plan(source, plan, expression.span); + self.reflected_produced_return = previous; + let value = Expression { + kind: ExpressionKind::Local(source), + ty: target.ty, + span: expression.span, + }; + let mut ancestor = target.ty; + let mut coarsens = false; + while let Type::Refinement { base, .. } = self.types.resolve(ancestor) { + ancestor = *base; + if ancestor == expression.ty { + coarsens = true; + break; + } + } + return Some(Expression { + kind: if expression.ty == target.ty { + value.kind + } else if coarsens && matches!(&plan.action, hir::ReflectedFieldAction::Exact) { + ExpressionKind::Coarsen(Box::new(value)) + } else if matches!(self.types.resolve(expression.ty), Type::Refinement { .. }) { + ExpressionKind::RefinementValidated(Box::new(value)) + } else { + ExpressionKind::InterfaceCoerce { + value: Box::new(value), + adapters: Vec::new(), + } + }, + ty: expression.ty, + span: expression.span, + }); + } + if let ExpressionKind::Handle { + target, + kind: HandleKind::Refinement { predicates, .. }, + error_local, + failure, + } = &expression.kind + { + return Some(self.lower_refinement_handle( + expression, + target, + predicates, + *error_local, + failure, + )); + } + if let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: true, + refinement_predicates, + } = &expression.kind + && refinement_predicates.len() == fields.len() + && refinement_predicates.iter().any(|chain| !chain.is_empty()) + { + return Some(self.lower_refinement_struct_construct( + expression, + *struct_type, + fields, + evaluation_order, + refinement_predicates, + )); + } + if let ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::Validate(plans)), + .. + } = &expression.kind + { + // Invalid plans stay visible for native contract rejection; never + // turn missing or malformed proof metadata into an unchecked read. + return Some( + self.lower_reflected_field_read(expression, plans) + .unwrap_or_else(|| expression.clone()), + ); + } + if let ExpressionKind::Intrinsic { + intrinsic: hir::IntrinsicId::TypeConstructFinish, + refinement_predicates, + .. + } = &expression.kind + && refinement_predicates.iter().any(|chain| !chain.is_empty()) + { + return Some(self.lower_refinement_builder_finish(expression, refinement_predicates)); + } + None + } + + #[inline(never)] + fn try_lower_value_collection(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::ListConstruct { elements } = &expression.kind + && elements + .iter() + .any(|value| self.needs_eager_lowering(value)) + && let Some(elements) = + self.lower_ordered_owned_values(elements, &(0..elements.len()).collect::>()) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::ListConstruct { elements }; + return Some(lowered); + } + if let ExpressionKind::StringInterpolation(segments) = &expression.kind + && segments.iter().any(|segment| { + matches!(segment, hir::StringSegment::Value(value) if self.needs_eager_lowering(value)) + }) + { + let values = segments + .iter() + .filter_map(|segment| match segment { + hir::StringSegment::Text(_) => None, + hir::StringSegment::Value(value) => Some(value.clone()), + }) + .collect::>(); + if let Some(values) = + self.lower_ordered_owned_values(&values, &(0..values.len()).collect::>()) + { + let mut values = values.into_iter(); + let segments = segments + .iter() + .map(|segment| match segment { + hir::StringSegment::Text(text) => hir::StringSegment::Text(text.clone()), + hir::StringSegment::Value(_) => { + hir::StringSegment::Value(values.next().expect("interpolation value")) + } + }) + .collect(); + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::StringInterpolation(segments); + return Some(lowered); + } + } + if let ExpressionKind::MapConstruct { entries } = &expression.kind + && entries.iter().any(|entry| { + self.needs_eager_lowering(&entry.key) || self.needs_eager_lowering(&entry.value) + }) + { + let values: Vec<_> = entries + .iter() + .flat_map(|entry| [entry.key.clone(), entry.value.clone()]) + .collect(); + if let Some(values) = + self.lower_ordered_owned_values(&values, &(0..values.len()).collect::>()) + { + let entries = values + .chunks_exact(2) + .map(|pair| hir::MapEntry { + key: pair[0].clone(), + value: pair[1].clone(), + }) + .collect(); + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::MapConstruct { entries }; + return Some(lowered); + } + } + if let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements, + refinement_predicates, + } = &expression.kind + && fields.iter().any(|value| self.needs_eager_lowering(value)) + && let Some(fields) = self.lower_ordered_owned_values(fields, evaluation_order) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::StructConstruct { + struct_type: *struct_type, + fields, + evaluation_order: evaluation_order.clone(), + validates_refinements: *validates_refinements, + refinement_predicates: refinement_predicates.clone(), + }; + return Some(lowered); + } + if let ExpressionKind::BitfieldConstruct { + bitfield_type, + fields, + evaluation_order, + validates_widths, + } = &expression.kind + && fields.iter().any(|value| self.needs_eager_lowering(value)) + && let Some(fields) = self.lower_ordered_owned_values(fields, evaluation_order) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::BitfieldConstruct { + bitfield_type: *bitfield_type, + fields, + evaluation_order: evaluation_order.clone(), + validates_widths: *validates_widths, + }; + return Some(lowered); + } + if let ExpressionKind::EnumConstruct { + enum_type, + variant, + payloads, + evaluation_order, + } = &expression.kind + && payloads + .iter() + .any(|value| self.needs_eager_lowering(value)) + && let Some(payloads) = self.lower_ordered_owned_values(payloads, evaluation_order) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::EnumConstruct { + enum_type: *enum_type, + variant: *variant, + payloads, + evaluation_order: evaluation_order.clone(), + }; + return Some(lowered); + } + if let ExpressionKind::MachineConstruct { + state_type, + state, + payloads, + } = &expression.kind + && payloads + .iter() + .any(|value| self.needs_eager_lowering(value)) + && let Some(payloads) = + self.lower_ordered_owned_values(payloads, &(0..payloads.len()).collect::>()) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::MachineConstruct { + state_type: *state_type, + state: *state, + payloads, + }; + return Some(lowered); + } + if let ExpressionKind::MachineTransition { + source, + state_type, + target, + payloads, + } = &expression.kind + && (self.needs_eager_lowering(source) + || payloads + .iter() + .any(|value| self.needs_eager_lowering(value))) + { + let values = std::iter::once(source.as_ref().clone()) + .chain(payloads.iter().cloned()) + .collect::>(); + let order = (0..values.len()).collect::>(); + if let Some(mut values) = self.lower_ordered_owned_values(&values, &order) { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::MachineTransition { + source: Box::new(values.remove(0)), + state_type: *state_type, + target: *target, + payloads: values, + }; + return Some(lowered); + } + } + None + } + + #[inline(never)] + fn try_lower_value_sum(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::ResultOk(value) = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::ResultOk(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::ResultFail(value) = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::ResultFail(Box::new(self.lower_value(value))); + return Some(lowered); + } + if let ExpressionKind::OptionalSome(value) = &expression.kind + && self.needs_eager_lowering(value) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::OptionalSome(Box::new(self.lower_value(value))); + return Some(lowered); + } + None + } + + #[inline(never)] + fn try_lower_value_intrinsic(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + refinement_predicates, + field_validation, + args, + evaluation_order, + ownership, + } = &expression.kind + && (args.iter().any(|value| self.needs_eager_lowering(value)) + || needs_call_owner_staging(ownership)) + { + // Normalize closed physical View roles before the common preflight + // distinguishes a source owner transfer from a retained/raw borrow. + let inputs = args + .iter() + .enumerate() + .map(|(index, arg)| { + if ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) + && !matches!(arg.kind, ExpressionKind::View(_)) + { + Expression { + kind: ExpressionKind::View(Box::new(arg.clone())), + ty: arg.ty, + span: arg.span, + } + } else { + arg.clone() + } + }) + .collect::>(); + let mut ownership = ownership.clone(); + if let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Intrinsic { + intrinsic: *intrinsic, + type_arguments: type_arguments.clone(), + reflection_arguments: reflection_arguments.clone(), + refinement_predicates: refinement_predicates.clone(), + field_validation: field_validation.clone(), + args, + evaluation_order: evaluation_order.clone(), + ownership, + }; + return Some(self.finish_call_view_scope(lowered)); + } + } + None + } + + #[inline(never)] + fn try_lower_value_actor(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::ActorSpawn { + actor_type, + args, + evaluation_order, + constructor, + } = &expression.kind + && args.iter().any(|value| self.needs_eager_lowering(value)) + && let Some(args) = self.lower_ordered_owned_values(args, evaluation_order) + { + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::ActorSpawn { + actor_type: actor_type.clone(), + args, + evaluation_order: evaluation_order.clone(), + constructor: *constructor, + }; + return Some(lowered); + } + if let ExpressionKind::ActorMessage { + actor, + message, + handler, + args, + evaluation_order, + kind, + proof, + } = &expression.kind + && (self.needs_eager_lowering(actor) + || args.iter().any(|value| self.needs_eager_lowering(value))) + && valid_ordered_owned_values(self.types, &self.locals, args, evaluation_order) + { + let actor_value = self.lower_value(actor); + let actor_local = self.temporary(actor.ty, actor.span); + self.push( + StatementKind::Let { + local: actor_local, + value: actor_value, + }, + actor.span, + ); + let args = self + .lower_ordered_owned_values(args, evaluation_order) + .expect("validated actor message argument order"); + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::ActorMessage { + actor: Box::new(Expression { + kind: ExpressionKind::Local(actor_local), + ty: actor.ty, + span: actor.span, + }), + message: message.clone(), + handler: *handler, + args, + evaluation_order: evaluation_order.clone(), + kind: *kind, + proof: proof.clone(), + }; + return Some(lowered); + } + None + } + + #[inline(never)] + fn try_lower_value_call(&mut self, expression: &Expression) -> Option { + if let ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + ownership, + } = &expression.kind + && (self.needs_eager_lowering(callee) + || args.iter().any(|value| self.needs_eager_lowering(value)) + || needs_call_owner_staging(ownership)) + && !matches!(callee.kind, ExpressionKind::View(_)) + && let Type::Function { view_params, .. } = self.types.resolve( + crate::move_values::representation_type(self.types, callee.ty), + ) + && let Some(inputs) = call_staging_inputs( + args, + &view_params + .iter() + .map(|view| { + if *view { + ParamMode::View + } else { + ParamMode::Owned + } + }) + .collect::>(), + ) + { + let mut ownership = ownership.clone(); + let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) else { + return Some(expression.clone()); + }; + // Source evaluates call arguments before resolving a function value + // held in a mutable local. A handler may rebind that local. + let observed_callee = self.lower_value(callee); + let callee_value = Expression { + kind: ExpressionKind::Clone(Box::new(observed_callee)), + ty: callee.ty, + span: callee.span, + }; + let callee_local = self.temporary(callee.ty, callee.span); + self.push( + StatementKind::Let { + local: callee_local, + value: callee_value, + }, + callee.span, + ); + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::IndirectCall { + callee: Box::new(Expression { + kind: ExpressionKind::Local(callee_local), + ty: callee.ty, + span: callee.span, + }), + args, + evaluation_order: evaluation_order.clone(), + ownership, + }; + return Some(self.finish_call_view_scope(lowered)); + } + if let ExpressionKind::Call { + function, + args, + evaluation_order, + ownership, + } = &expression.kind + && (args.iter().any(|value| self.needs_eager_lowering(value)) + || needs_call_owner_staging(ownership)) + && let Some(modes) = self.function_param_modes.get(function) + && let Some(inputs) = call_staging_inputs(args, modes) + { + let mut ownership = ownership.clone(); + let Some(args) = self.lower_ordered_call_values( + expression, + &inputs, + evaluation_order, + &mut ownership, + ) else { + return Some(expression.clone()); + }; + let mut lowered = expression.clone(); + lowered.kind = ExpressionKind::Call { + function: *function, + args, + evaluation_order: evaluation_order.clone(), + ownership, + }; + return Some(self.finish_call_view_scope(lowered)); + } + None + } + + #[inline(never)] + pub(super) fn lower_borrowed_sum_handle( + &mut self, + seed: resource_ownership::BorrowedSumSeed, + ) -> Expression { + let original = seed.original.clone(); + let ExpressionKind::View(handle) = &original.kind else { + unreachable!() + }; + let ExpressionKind::Handle { + target, + error_local, + failure, + .. + } = &handle.kind + else { + unreachable!() + }; + let span = handle.span; + let source = self.call_view_temporary(target.ty, seed.backing.id, span); + let output = self.call_view_temporary(handle.ty, seed.backing.id, span); + let tag = self.temporary(TypeInterner::BOOL, span); + let initialize = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::Let { + local: source, + value: target.as_ref().clone(), + }, + span, + ); + let observe = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let success = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: success, + else_block: failed, + }, + span, + ); + self.current = success; + let project = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTake { + source, + target: output, + success: true, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + let failure_site = error_local.map(|error| { + let site = self + .resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated borrowed Handle capture"); + self.push( + StatementKind::SumTake { + source, + target: error, + success: false, + }, + span, + ); + site + }); + self.lower_block(failure); + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + let error = error_local + .and_then(|local| self.locals.get(local.index() as usize)) + .cloned(); + self.resource_capture.record_borrowed_sum( + seed, + self.locals[source.index() as usize].clone(), + self.locals[output.index() as usize].clone(), + self.locals[tag.index() as usize].clone(), + error, + initialize, + observe, + project, + failure_site, + failed, + continuation, + ); + Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(output), + ty: handle.ty, + span: handle.span, + })), + ty: original.ty, + span: original.span, + } + } + + #[inline(never)] + pub(super) fn lower_ordinary_borrowed_sum_handle( + &mut self, + seed: ordinary_borrowed_sums::Seed, + ) -> Expression { + let original = seed.original.clone(); + let ExpressionKind::View(handle) = &original.kind else { + unreachable!() + }; + let ExpressionKind::Handle { + target, + error_local, + failure, + .. + } = &handle.kind + else { + unreachable!() + }; + let span = handle.span; + let source = self.call_view_temporary(target.ty, seed.backing.id, span); + let output = self.call_view_temporary(handle.ty, seed.backing.id, span); + let tag = self.temporary(TypeInterner::BOOL, span); + let site = |builder: &Self| { + ordinary_borrowed_sums::OrdinarySumProjectionSite::new( + builder.current, + builder.blocks[builder.current.index() as usize] + .statements + .len(), + ) + }; + let initialize = site(self); + self.push( + StatementKind::Let { + local: source, + value: target.as_ref().clone(), + }, + span, + ); + let observe = site(self); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let success = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: success, + else_block: failed, + }, + span, + ); + self.current = success; + let project = site(self); + self.push( + StatementKind::SumTake { + source, + target: output, + success: true, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + let failure_site = error_local.map(|error| { + let at = site(self); + self.push( + StatementKind::SumTake { + source, + target: error, + success: false, + }, + span, + ); + at + }); + self.lower_block(failure); + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + let alias_site = site(self); + let error = error_local + .and_then(|local| self.locals.get(local.index() as usize)) + .cloned(); + self.ordinary_sum_capture.record( + seed, + self.locals[source.index() as usize].clone(), + self.locals[output.index() as usize].clone(), + self.locals[tag.index() as usize].clone(), + error, + initialize, + observe, + project, + failure_site, + alias_site, + failed, + continuation, + ); + Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(output), + ty: handle.ty, + span: handle.span, + })), + ty: original.ty, + span: original.span, + } + } + + #[inline(never)] + fn lower_sum_handle(&mut self, expression: &Expression) -> Expression { + let ExpressionKind::Handle { + target, + kind: HandleKind::Result | HandleKind::Optional, + error_local, + failure, + } = &expression.kind + else { + // Other expression-level control flow remains explicitly unsupported + // by native ownership validation, rather than being eagerly hoisted. + return expression.clone(); + }; + let span = expression.span; + let mut value = self.lower_value(target); + // A source handle leaves a local sum available for subsequent reads. + // SumTake consumes only this snapshot, including on a view parameter. + if snapshotable_local(self.types, target) { + value = snapshot_local(target, value); + } + let source = self.temporary(target.ty, span); + self.push( + StatementKind::Let { + local: source, + value, + }, + span, + ); + let tag = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let success = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: success, + else_block: failed, + }, + span, + ); + self.current = success; + self.push( + StatementKind::SumTake { + source, + target: output, + success: true, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + if let Some(error) = error_local { + self.push( + StatementKind::SumTake { + source, + target: *error, + success: false, + }, + span, + ); + } + self.handlers.push((output, continuation)); + self.lower_block(failure); + self.handlers.pop(); + // A failure path must yield or exit; never fabricate an initialized + // payload for an invalid fallthrough. + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } + + fn lower_reflected_field_read( + &mut self, + expression: &Expression, + plans: &[hir::ReflectedFieldPlan], + ) -> Option { + let ExpressionKind::Intrinsic { + intrinsic, + type_arguments, + args, + evaluation_order, + ownership, + .. + } = &expression.kind + else { + return None; + }; + let [owner_type, requested] = type_arguments.as_slice() else { + return None; + }; + if *requested != expression.ty { + return None; + } + hir::validate_reflected_field_plans(self.types, *intrinsic, *owner_type, *requested, plans) + .ok()?; + let layout = hir::reflected_field_layout(self.types, *intrinsic, *owner_type)?; + let metadata_type = args.get(1)?.ty; + if !hir::valid_reflected_field_metadata_type(self.types, metadata_type) { + return None; + } + let Type::Struct(metadata_id) = self.types.resolve(metadata_type) else { + return None; + }; + let metadata = self.types.resolve_struct(*metadata_id); + if args.len() != layout.len() + 2 + || args[0].ty != *owner_type + || args[1..].iter().any(|arg| arg.ty != metadata_type) + { + return None; + } + let member_type = metadata.fields[2].1; + if plans + .iter() + .all(|plan| matches!(plan.action, hir::ReflectedFieldAction::Exact)) + { + let mut raw = expression.clone(); + if let ExpressionKind::Intrinsic { + field_validation, .. + } = &mut raw.kind + { + *field_validation = Some(hir::ReflectedFieldValidation::Read); + } + return Some(self.lower_value(&raw)); + } + let inputs: Vec<_> = args + .iter() + .enumerate() + .map(|(index, arg)| { + if ownership.parameter_physical_access(index) + == Some(jett_typecheck::CheckedCalleeAccess::View) + && !matches!(arg.kind, ExpressionKind::View(_)) + { + Expression { + kind: ExpressionKind::View(Box::new(arg.clone())), + ty: arg.ty, + span: arg.span, + } + } else { + arg.clone() + } + }) + .collect(); + let mut ownership = ownership.clone(); + let args = + self.lower_ordered_call_values(expression, &inputs, evaluation_order, &mut ownership)?; + let mut raw = expression.clone(); + if let ExpressionKind::Intrinsic { + field_validation, + args: raw_args, + ownership: raw_ownership, + .. + } = &mut raw.kind + { + *field_validation = Some( + if plans + .iter() + .any(|plan| matches!(plan.action, hir::ReflectedFieldAction::CallableReturn(_))) + { + hir::ReflectedFieldValidation::ReadCallable(plans.to_vec()) + } else { + hir::ReflectedFieldValidation::Read + }, + ); + *raw_args = args.clone(); + *raw_ownership = ownership; + } + let span = expression.span; + let candidate = self.temporary(expression.ty, span); + // The existing raw getter completes every selector, metadata, owner, + // pending and payload read check before any proof predicate executes. + self.push( + StatementKind::Let { + local: candidate, + value: raw, + }, + span, + ); + let selected_member = if layout.iter().any(|(member, _, _)| member.is_some()) { + let member = self.temporary(member_type, span); + self.push( + StatementKind::Let { + local: member, + value: Expression { + kind: ExpressionKind::Field { + base: Box::new(args[1].clone()), + owner_type: metadata_type, + field: hir::FieldId::new(2), + }, + ty: member_type, + span, + }, + }, + span, + ); + let present = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source: member, + target: present, + }, + span, + ); + let accepted = self.new_block(span); + let absent = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(present), + ty: TypeInterner::BOOL, + span, + }, + then_block: accepted, + else_block: absent, + }, + span, + ); + self.current = absent; + self.terminate(TerminatorKind::Unreachable, span); + self.current = accepted; + let text = self.temporary(TypeInterner::STRING, span); + self.push( + StatementKind::SumTake { + source: member, + target: text, + success: true, + }, + span, + ); + Some(text) + } else { + None + }; + let continuation = self.new_block(span); + for ((member, index, _), plan) in layout.iter().zip(plans) { + let case = self.new_block(span); + let next = self.new_block(span); + let field = |index, ty| Expression { + kind: ExpressionKind::Field { + base: Box::new(args[1].clone()), + owner_type: metadata_type, + field: hir::FieldId::new(index), + }, + ty, + span, + }; + let index_matches = Expression { + kind: ExpressionKind::Binary { + left: Box::new(field(0, TypeInterner::INT64)), + op: hir::BinaryOp::Equal, + right: Box::new(Expression { + kind: ExpressionKind::Int(*index as i128), + ty: TypeInterner::INT64, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }; + let condition = if let Some(member) = member { + let member_matches = Expression { + kind: ExpressionKind::Binary { + left: Box::new(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(selected_member?), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::STRING, + span, + }), + op: hir::BinaryOp::Equal, + right: Box::new(Expression { + kind: ExpressionKind::String(member.clone()), + ty: TypeInterner::STRING, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }; + Expression { + kind: ExpressionKind::Binary { + left: Box::new(index_matches), + op: hir::BinaryOp::And, + right: Box::new(member_matches), + }, + ty: TypeInterner::BOOL, + span, + } + } else { + index_matches + }; + self.terminate( + TerminatorKind::Branch { + condition, + then_block: case, + else_block: next, + }, + span, + ); + self.current = case; + self.lower_reflected_plan(candidate, plan, span); + self.close_to(continuation, span); + self.current = next; + } + // A successful getter already proved one of these exact member/index + // pairs. Never fabricate a result if a malformed plan misses that pair. + self.terminate(TerminatorKind::Unreachable, span); + self.current = continuation; + Some(self.finish_call_view_scope(Expression { + kind: ExpressionKind::Local(candidate), + ty: expression.ty, + span, + })) + } + + fn lower_refinement_check( + &mut self, + input: Expression, + predicate: &hir::RefinementPredicate, + span: Span, + ) -> (LocalId, LocalId) { + let args = vec![input]; + let ownership = hir::CallOwnership::generated( + hir::GeneratedOperation::RefinementPredicate { + function: predicate.function, + }, + &args, + &[predicate.input_type], + &[jett_typecheck::CheckedCalleeAccess::Owned], + TypeInterner::BOOL, + &[0], + self.types, + ) + .expect("checked refinement predicate has a unary owned input"); + let error_text = self.temporary(TypeInterner::STRING, span); + self.push( + StatementKind::CheckRefinement { + local: error_text, + call: Expression { + kind: ExpressionKind::Call { + function: predicate.function, + args, + evaluation_order: vec![0], + ownership, + }, + ty: TypeInterner::BOOL, + span, + }, + type_name: predicate.type_name.clone(), + }, + span, + ); + let passed = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::Let { + local: passed, + value: Expression { + kind: ExpressionKind::Binary { + left: Box::new(Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::STRING, + span, + }), + op: hir::BinaryOp::Equal, + right: Box::new(Expression { + kind: ExpressionKind::String(String::new()), + ty: TypeInterner::STRING, + span, + }), + }, + ty: TypeInterner::BOOL, + span, + }, + }, + span, + ); + (error_text, passed) + } + + fn lower_refinement_handle( + &mut self, + expression: &Expression, + target: &Expression, + predicates: &[hir::RefinementPredicate], + error_local: Option, + failure: &hir::Block, + ) -> Expression { + if predicates.is_empty() { + return expression.clone(); + } + let span = expression.span; + let value = self.lower_value(target); + // Refinement validation reads a cloneable local without consuming the + // source value in the interpreter. Keep the candidate in its own owner. + let value = if snapshotable_local(self.types, target) { + snapshot_local(target, value) + } else { + value + }; + let source = self.temporary(target.ty, span); + self.push( + StatementKind::Let { + local: source, + value, + }, + span, + ); + let failed = self.new_block(failure.span); + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + + for predicate in predicates { + let input = self.refinement_predicate_input(source, target.ty, predicate, span); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let next = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(passed), + ty: TypeInterner::BOOL, + span, + }, + then_block: next, + else_block: rejected, + }, + span, + ); + self.current = rejected; + if let Some(error_local) = error_local { + self.push( + StatementKind::Let { + local: error_local, + value: Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + }, + }, + span, + ); + } + self.terminate(TerminatorKind::Goto(failed), span); + self.current = next; + } + + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::RefinementValidated(Box::new(Expression { + kind: ExpressionKind::Local(source), + ty: target.ty, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = failed; + self.handlers.push((output, continuation)); + self.lower_block(failure); + self.handlers.pop(); + if self.open() { + self.terminate(TerminatorKind::Unreachable, span); + } + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } + + fn lower_refinement_enum_builder_finish( + &mut self, + expression: &Expression, + enum_type: TypeId, + refinement_predicates: &[Vec], + ) -> Expression { + let Type::Enum(id) = self.types.resolve(enum_type) else { + return expression.clone(); + }; + let variants = self + .types + .resolve_enum(*id) + .variants + .iter() + .map(|variant| variant.fields.iter().map(|(_, ty)| *ty).collect::>()) + .collect::>(); + if variants.iter().map(Vec::len).sum::() != refinement_predicates.len() { + return expression.clone(); + } + let span = expression.span; + let mut raw = expression.clone(); + if let ExpressionKind::Intrinsic { + refinement_predicates, + .. + } = &mut raw.kind + { + refinement_predicates.clear(); + } + let raw = self.lower_value(&raw); + let source = self.temporary(expression.ty, span); + self.push( + StatementKind::Let { + local: source, + value: raw, + }, + span, + ); + let tag = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let accepted = self.new_block(span); + let failed = self.new_block(span); + let validated = self.new_block(span); + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: accepted, + else_block: failed, + }, + span, + ); + self.current = failed; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::Local(source), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = accepted; + let built = self.temporary(enum_type, span); + self.push( + StatementKind::SumTake { + source, + target: built, + success: true, + }, + span, + ); + let mut offset = 0; + let mut arms = Vec::with_capacity(variants.len()); + for (index, fields) in variants.iter().enumerate() { + let block = self.new_block(span); + let has_predicates = refinement_predicates[offset..offset + fields.len()] + .iter() + .any(|chain| !chain.is_empty()); + let bindings = if has_predicates { + fields + .iter() + .map(|ty| self.temporary(*ty, span)) + .collect::>() + } else { + Vec::new() + }; + arms.push(( + hir::VariantId::new(index as u32), + block, + bindings, + offset, + index, + )); + offset += fields.len(); + } + self.terminate( + TerminatorKind::Switch { + scrutinee: Expression { + kind: ExpressionKind::Clone(Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: enum_type, + span, + })), + ty: enum_type, + span, + }, + variants: arms + .iter() + .map(|(variant, block, bindings, _, _)| (*variant, *block, bindings.clone())) + .collect(), + otherwise: None, + }, + span, + ); + for (_, block, bindings, offset, variant_index) in arms { + self.current = block; + for (index, binding) in bindings.iter().enumerate() { + for predicate in &refinement_predicates[offset + index] { + let input = self.refinement_predicate_input( + *binding, + variants[variant_index][index], + predicate, + span, + ); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let next = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(passed), + ty: TypeInterner::BOOL, + span, + }, + then_block: next, + else_block: rejected, + }, + span, + ); + self.current = rejected; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultFail(Box::new(Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = next; + } + } + self.close_to(validated, span); + } + self.current = validated; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultOk(Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: enum_type, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } + + fn lower_refinement_machine_builder_finish( + &mut self, + expression: &Expression, + owner_type: TypeId, + refinement_predicates: &[Vec], + ) -> Expression { + let (machine, selected_state) = match self.types.resolve(owner_type) { + Type::Machine(machine) => (*machine, None), + Type::MachineState { machine, state } => (*machine, Some(*state)), + _ => return expression.clone(), + }; + let states = + self.types + .resolve_machine(machine) + .states + .iter() + .enumerate() + .filter(|(index, _)| { + selected_state.is_none_or(|state| state.index() as usize == *index) + }) + .map(|(index, state)| { + let narrowed = + if selected_state.is_some() { + owner_type + } else { + self.types.type_ids().find(|id| matches!( + self.types.resolve(*id), + Type::MachineState { machine: id_machine, state } + if *id_machine == machine && state.index() as usize == index + )).expect("checked reflected finish interns all machine states") + }; + ( + hir::StateId::new(index as u32), + narrowed, + state.fields.iter().map(|(_, ty)| *ty).collect::>(), + ) + }) + .collect::>(); + if states + .iter() + .map(|(_, _, fields)| fields.len()) + .sum::() + != refinement_predicates.len() + { + return expression.clone(); + } + let span = expression.span; + let mut raw = expression.clone(); + if let ExpressionKind::Intrinsic { + refinement_predicates, + .. + } = &mut raw.kind + { + refinement_predicates.clear(); + } + let raw = self.lower_value(&raw); + let source = self.temporary(expression.ty, span); + self.push( + StatementKind::Let { + local: source, + value: raw, + }, + span, + ); + let tag = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let accepted = self.new_block(span); + let failed = self.new_block(span); + let validated = self.new_block(span); + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: accepted, + else_block: failed, + }, + span, + ); + self.current = failed; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::Local(source), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = accepted; + let built = self.temporary(owner_type, span); + self.push( + StatementKind::SumTake { + source, + target: built, + success: true, + }, + span, + ); + let mut offset = 0; + for (state, narrowed_type, fields) in states { + let selected = self.new_block(span); + let next_state = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::StateIs { + value: Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: owner_type, + span, + }), + state, + }, + ty: TypeInterner::BOOL, + span, + }, + then_block: selected, + else_block: next_state, + }, + span, + ); + self.current = selected; + for (index, field_type) in fields.iter().enumerate() { + for predicate in &refinement_predicates[offset + index] { + let field = Expression { + kind: ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: narrowed_type, + span, + }), + owner_type: narrowed_type, + field: hir::FieldId::new(index as u32), + }, + ty: *field_type, + span, + }; + let value = self.temporary(*field_type, span); + self.push( + StatementKind::Let { + local: value, + value: Expression { + kind: ExpressionKind::Clone(Box::new(field)), + ty: *field_type, + span, + }, + }, + span, + ); + let input = + self.refinement_predicate_input(value, *field_type, predicate, span); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let next = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(passed), + ty: TypeInterner::BOOL, + span, + }, + then_block: next, + else_block: rejected, + }, + span, + ); + self.current = rejected; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultFail(Box::new(Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = next; + } + } + self.close_to(validated, span); + self.current = next_state; + offset += fields.len(); + } + self.terminate(TerminatorKind::Unreachable, span); + self.current = validated; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultOk(Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: owner_type, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } + + fn lower_refinement_builder_finish( + &mut self, + expression: &Expression, + refinement_predicates: &[Vec], + ) -> Expression { + let Type::Result(struct_type, _) = self.types.resolve(expression.ty) else { + return expression.clone(); + }; + let struct_type = *struct_type; + if matches!(self.types.resolve(struct_type), Type::Enum(_)) { + return self.lower_refinement_enum_builder_finish( + expression, + struct_type, + refinement_predicates, + ); + } + if matches!( + self.types.resolve(struct_type), + Type::Machine(_) | Type::MachineState { .. } + ) { + return self.lower_refinement_machine_builder_finish( + expression, + struct_type, + refinement_predicates, + ); + } + let Type::Struct(id) = self.types.resolve(struct_type) else { + return expression.clone(); + }; + let field_types = self + .types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(); + if field_types.len() != refinement_predicates.len() { + return expression.clone(); + } + let span = expression.span; + let mut raw = expression.clone(); + if let ExpressionKind::Intrinsic { + refinement_predicates, + .. + } = &mut raw.kind + { + refinement_predicates.clear(); + } + let raw = self.lower_value(&raw); + let source = self.temporary(expression.ty, span); + self.push( + StatementKind::Let { + local: source, + value: raw, + }, + span, + ); + let tag = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source, + target: tag, + }, + span, + ); + let accepted = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(tag), + ty: TypeInterner::BOOL, + span, + }, + then_block: accepted, + else_block: failed, + }, + span, + ); + self.current = failed; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::Local(source), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = accepted; + let built = self.temporary(struct_type, span); + self.push( + StatementKind::SumTake { + source, + target: built, + success: true, + }, + span, + ); + for (index, predicates) in refinement_predicates.iter().enumerate() { + for predicate in predicates { + let field = Expression { + kind: ExpressionKind::Field { + base: Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: struct_type, + span, + }), + owner_type: struct_type, + field: hir::FieldId::new(index as u32), + }, + ty: field_types[index], + span, + }; + let value = self.temporary(field.ty, span); + self.push( + StatementKind::Let { + local: value, + value: Expression { + kind: ExpressionKind::Clone(Box::new(field)), + ty: field_types[index], + span, + }, + }, + span, + ); + let input = + self.refinement_predicate_input(value, field_types[index], predicate, span); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let next = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(passed), + ty: TypeInterner::BOOL, + span, + }, + then_block: next, + else_block: rejected, + }, + span, + ); + self.current = rejected; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultFail(Box::new(Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = next; + } + } + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultOk(Box::new(Expression { + kind: ExpressionKind::Local(built), + ty: struct_type, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } + + fn lower_refinement_struct_construct( + &mut self, + expression: &Expression, + struct_type: TypeId, + fields: &[Expression], + evaluation_order: &[usize], + refinement_predicates: &[Vec], + ) -> Expression { + if evaluation_order.len() != fields.len() + || evaluation_order.iter().any(|&index| index >= fields.len()) + { + return expression.clone(); + } + let span = expression.span; + let continuation = self.new_block(span); + let output = self.temporary(expression.ty, span); + let mut evaluated = vec![None; fields.len()]; + for &index in evaluation_order { + let value = self.lower_value(&fields[index]); + let local = self.temporary(value.ty, value.span); + self.push(StatementKind::Let { local, value }, span); + evaluated[index] = Some(local); + } + let mut prepared = vec![None; fields.len()]; + for &index in evaluation_order { + let source = evaluated[index].expect("validated field evaluation order"); + let field = &fields[index]; + let mut field_type = field.ty; + for predicate in &refinement_predicates[index] { + let input = self.refinement_predicate_input(source, field.ty, predicate, span); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let next = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Local(passed), + ty: TypeInterner::BOOL, + span, + }, + then_block: next, + else_block: rejected, + }, + span, + ); + self.current = rejected; + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultFail(Box::new(Expression { + kind: ExpressionKind::Local(error_text), + ty: TypeInterner::STRING, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = next; + field_type = predicate.refined_type; + } + let field_value = if field_type != field.ty { + Expression { + kind: ExpressionKind::RefinementValidated(Box::new(Expression { + kind: ExpressionKind::Local(source), + ty: field.ty, + span, + })), + ty: field_type, + span, + } + } else { + Expression { + kind: ExpressionKind::Local(source), + ty: field.ty, + span, + } + }; + prepared[index] = Some(field_value); + } + let fields = prepared + .into_iter() + .map(|value| value.expect("validated field evaluation order")) + .collect(); + self.push( + StatementKind::Let { + local: output, + value: Expression { + kind: ExpressionKind::ResultOk(Box::new(Expression { + kind: ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order: (0..evaluation_order.len()).collect(), + validates_refinements: false, + refinement_predicates: Vec::new(), + }, + ty: struct_type, + span, + })), + ty: expression.ty, + span, + }, + }, + span, + ); + self.close_to(continuation, span); + self.current = continuation; + Expression { + kind: ExpressionKind::Local(output), + ty: expression.ty, + span, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod reflected_recursive; + + fn handler_source_hir(source: &str) -> (hir::Program, TypeInterner) { + handler_source_hir_with_equatable(source, false) + } + + fn handler_source_hir_with_equatable( + source: &str, + include_equatable: bool, + ) -> (hir::Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let mut parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let mut origins = + std::collections::HashMap::from([(file, jett_common::SourceOrigin::Project)]); + if include_equatable { + let prelude_file = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut prelude = jett_parser::parse( + "namespace stdlib\nexport interface Equatable:\n function equals(view self: Equatable, view other: Equatable) returns bool\n", + prelude_file, + ); + assert!(prelude.errors.is_empty()); + prelude.module.items.append(&mut parsed.module.items); + parsed.module.items = prelude.module.items; + origins.insert(prelude_file, jett_common::SourceOrigin::Stdlib); + } + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let hir = + jett_hir::lower(&parsed.module, &resolved, &checked, &origins).expect("HIR lowering"); + (hir, checked.interner) + } + + fn lower_handler_source(source: &str) -> (Program, TypeInterner) { + let (hir, types) = handler_source_hir(source); + (lower(&hir, &types).expect("MIR lowering"), types) + } + + fn inspected_handler_function(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap() + } + + #[test] + fn failed_checked_call_staging_restores_all_builder_state() { + let source = "function take(view first: list[int64], view second: list[int64]) returns int64:\n return 7\nfunction exercise(first: list[int64], second: list[int64]) returns int64:\n return take(first, second)\n"; + let (program, types) = handler_source_hir(source); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "exercise") + .unwrap(); + let call = function + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + hir::StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + let ExpressionKind::Call { + function: target, + args, + evaluation_order, + ownership, + } = &call.kind + else { + panic!("checked source invocation"); + }; + let modes = program + .functions + .iter() + .map(|function| { + ( + function.id, + function + .params + .iter() + .map(|param| param.mode) + .collect::>(), + ) + }) + .collect::>(); + let mut inputs = call_staging_inputs(args, &modes[target]).unwrap(); + // The first argument will already have acquired an owner and loan + // when the later malformed physical operand fails its stage proof. + inputs[1] = args[1].clone(); + let mut builder = Builder::new(function.span, &types, &modes); + builder.locals = function.locals.clone(); + let previous = builder.clone(); + let mut packet = ownership.clone(); + assert!( + builder + .lower_ordered_call_values(&call, &inputs, evaluation_order, &mut packet) + .is_none() + ); + assert_eq!(packet, *ownership); + assert_eq!(builder.blocks, previous.blocks); + assert_eq!(builder.current, previous.current); + assert_eq!(builder.locals, previous.locals); + assert_eq!(builder.loops, previous.loops); + assert_eq!(builder.handlers, previous.handlers); + assert_eq!(builder.view_params, previous.view_params); + assert_eq!(builder.call_view_scopes, previous.call_view_scopes); + } + + #[test] + fn native_reflected_root_reads_run_selector_guards_before_slot_predicates() { + for (owner, getter, declaration, expected) in [ + ( + "Record", + "field_value", + "struct Record:\n raw: int64\n positive: Positive\n higher: Higher\n sibling: Sibling\n", + 5, + ), + ( + "Event", + "variant_field_value", + "enum Event:\n first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling)\n second(higher: Higher, raw: int64)\n", + 7, + ), + ( + "Session", + "machine_field_value", + "machine Session:\n states:\n first(raw: int64, positive: Positive, higher: Higher, sibling: Sibling)\n second(higher: Higher, raw: int64)\n transitions:\n first to second\n", + 7, + ), + ] { + let source = format!( + "namespace app\ntype Positive = int64 where value > 0\ntype Higher = Positive where value > 10\ntype Sibling = int64 where value >= 0\n{declaration}function inspect(view source: {owner}, view field: TypeField) returns Higher:\n return type.{getter}[{owner}, Higher](view source, view field)\n" + ); + let (program, types) = lower_handler_source(&source); + validate(&program).unwrap(); + let function = inspected_handler_function(&program); + let raw_reads = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter_map(|s| match &s.kind { + StatementKind::Let { + local, + value: + Expression { + kind: + ExpressionKind::Intrinsic { + intrinsic, + field_validation: Some(hir::ReflectedFieldValidation::Read), + .. + }, + .. + }, + } if hir::is_reflected_field_intrinsic(*intrinsic) => Some(*local), + _ => None, + }) + .collect::>(); + assert_eq!(raw_reads.len(), 1, "{owner}: getter is staged once"); + let entry = &function.blocks[function.entry.index() as usize]; + assert!(entry.statements.iter().any( + |s| matches!(s.kind, StatementKind::Let { local, .. } if local == raw_reads[0]) + )); + assert!( + !entry + .statements + .iter() + .any(|s| matches!(s.kind, StatementKind::CheckRefinement { .. })), + "raw guard runs before predicates" + ); + let checks = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter(|s| matches!(s.kind, StatementKind::CheckRefinement { .. })) + .count(); + assert_eq!( + checks, expected, + "{owner}: exact slot skips, ancestor suffix only, sibling full chain" + ); + let members = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter(|s| matches!(s.kind, StatementKind::SumTake { .. })) + .count(); + assert_eq!( + members, + usize::from(owner != "Record"), + "only enum/machine extract the validated optional member" + ); + for block in &function.blocks { + if let TerminatorKind::Branch { + condition: + Expression { + kind: + ExpressionKind::Binary { + left, + op: hir::BinaryOp::And, + right, + }, + .. + }, + .. + } = &block.terminator.kind + { + assert_eq!(left.ty, TypeInterner::BOOL); + let ExpressionKind::Binary { + left, + op: hir::BinaryOp::Equal, + right, + } = &right.kind + else { + panic!("canonical member condition"); + }; + assert_eq!(left.ty, TypeInterner::STRING); + assert_eq!(right.ty, TypeInterner::STRING); + } + } + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + } + + #[test] + fn native_reflected_root_owned_reads_keep_candidate_and_error_cleanup() { + let (program, types) = lower_handler_source( + r#"namespace app +type Nonempty = list[int64] where true +struct Parcel: + exact: Nonempty + raw: list[int64] +function inspect(view source: Parcel, view field: TypeField) returns Nonempty: + return type.field_value[Parcel, Nonempty](view source, view field) +"#, + ); + validate(&program).unwrap(); + let function = inspected_handler_function(&program); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let candidate = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .find_map(|s| match &s.kind { + StatementKind::Let { + local, + value: + Expression { + kind: + ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::Read), + .. + }, + .. + }, + } => Some(*local), + _ => None, + }) + .unwrap(); + assert!( + plan.owned_locals.contains(&(candidate.index() as usize)), + "returned clone belongs to this function" + ); + let checks = function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter_map(|s| match &s.kind { + StatementKind::CheckRefinement { + local, type_name, .. + } => Some((*local, type_name.as_str())), + _ => None, + }) + .collect::>(); + assert_eq!( + checks.len(), + 1, + "exact declared list refinement skips its predicate" + ); + assert_eq!(checks[0].1, "app.Nonempty"); + let (block, message) = function + .blocks + .iter() + .enumerate() + .find_map(|(index, b)| { + b.statements.iter().find_map(|s| match &s.kind { + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailureMessage(message), + .. + }) => Some((index, message)), + _ => None, + }) + }) + .unwrap(); + assert!(matches!(message.kind, ExpressionKind::Local(local) if local == checks[0].0)); + assert!(plan.owned_locals.contains(&(checks[0].0.index() as usize))); + assert!(plan.live_in[block].contains(&(checks[0].0.index() as usize))); + assert!(matches!( + function.blocks[block].terminator.kind, + TerminatorKind::Unreachable + )); + } + + #[test] + fn native_reflected_root_mir_rejects_unlowered_and_wrong_predicate_contracts() { + let source = r#"namespace app +type Positive = int64 where value > 0 +type Sibling = int64 where value >= 0 +struct Record: + raw: int64 +function inspect(view source: Record, view field: TypeField) returns Positive: + return type.field_value[Record, Positive](view source, view field) +"#; + let (original, types) = lower_handler_source(source); + let sibling = original + .functions + .iter() + .find(|f| f.identity.declaration.name == "Sibling") + .unwrap() + .id; + for mutation in 0..3 { + let mut program = original.clone(); + let function = program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "inspect") + .unwrap(); + if mutation < 2 { + let value = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find_map(|s| match &mut s.kind { + StatementKind::Let { + value: + Expression { + kind: + ExpressionKind::Intrinsic { + field_validation, .. + }, + .. + }, + .. + } => Some(field_validation), + _ => None, + }) + .unwrap(); + *value = if mutation == 0 { + None + } else { + Some(hir::ReflectedFieldValidation::Validate(Vec::new())) + }; + } else { + let call = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.statements) + .find_map(|s| match &mut s.kind { + StatementKind::CheckRefinement { call, .. } => Some(call), + _ => None, + }) + .unwrap(); + let ExpressionKind::Call { function, .. } = &mut call.kind else { + panic!("predicate call"); + }; + *function = sibling; + } + let errors = validate(&program).expect_err("malformed reflected MIR"); + assert!(errors.iter().any(|e| e.message.contains(if mutation < 2 { + "proof plans were not lowered" + } else { + "exact predicate declaration" + }))); + } + let (mut hir, _) = handler_source_hir(source); + let function = hir + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "inspect") + .unwrap(); + let hir::StatementKind::Return(Some(Expression { + kind: + ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::Validate(plans)), + .. + }, + .. + })) = &mut function.body.statements[0].kind + else { + panic!("source plans"); + }; + plans[0].source_type = TypeInterner::BOOL; + let errors = lower(&hir, &types) + .expect_err("malformed source type plan cannot become unchecked Read"); + assert!( + errors.iter().any(|error| error + .message + .contains("reflected validation types disagree with declared field and request")), + "{errors:?}" + ); + } + + #[test] + fn native_return_refinement_stages_once_and_forwards_the_original_error() { + let (program, types) = lower_handler_source( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 10 +function source(raw: int64) returns int64: + return run raw +function inspect(raw: int64) returns Higher: + return source(raw) +"#, + ); + validate(&program).unwrap(); + let function = inspected_handler_function(&program); + let checks = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| { + if let StatementKind::CheckRefinement { type_name, .. } = &statement.kind { + Some(type_name.as_str()) + } else { + None + } + }) + .collect::>(); + assert_eq!(checks, ["app.Positive", "app.Higher"]); + let candidate_calls = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| + matches!(&statement.kind, StatementKind::Let { value: Expression { kind: ExpressionKind::Call { function: target, .. }, .. }, .. } + if program.functions[target.index() as usize].identity.declaration.name == "source") + ).count(); + assert_eq!(candidate_calls, 1, "return candidate evaluated once"); + let errors = function + .blocks + .iter() + .filter_map(|block| { + let message = + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailureMessage(message), + ty, + .. + }) => { + assert_eq!(*ty, TypeInterner::NOTHING); + assert_eq!(message.ty, TypeInterner::STRING); + let ExpressionKind::Local(local) = message.kind else { + panic!("borrow staged predicate error"); + }; + Some(local) + } + _ => None, + })?; + assert!(matches!(block.terminator.kind, TerminatorKind::Unreachable)); + Some(message) + }) + .collect::>(); + assert_eq!(errors.len(), 1); + let error = errors[0]; + assert_eq!(function.local(error).unwrap().name, "$return.error"); + let assignments = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| + matches!(statement.kind, StatementKind::Let { local, .. } if local == error) + ).count(); + assert_eq!( + assignments, 2, + "either rejected predicate forwards its exact error" + ); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(error.index() as usize))); + assert!( + plan.live_in + .iter() + .any(|live| live.contains(&(error.index() as usize))) + ); + } + + #[test] + fn native_return_dynamic_failure_keeps_nested_message_handlers_in_order() { + let (mut hir, types) = handler_source_hir( + r#"namespace app +function inspect(value: optional[string]) returns nothing: + string message = value handle: default "missing" + return nothing +"#, + ); + let function = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let hir::StatementKind::Let { value, .. } = &function.body.statements[0].kind else { + panic!("message handler"); + }; + let boundary = Expression { + kind: ExpressionKind::RuntimeFailureMessage(Box::new(value.clone())), + ty: TypeInterner::NOTHING, + span: value.span, + }; + assert!(has_extractable_handle(&boundary)); + function.body.statements[0].kind = hir::StatementKind::Expression(boundary); + let program = lower(&hir, &types).unwrap(); + validate(&program).unwrap(); + let function = inspected_handler_function(&program); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + ); + let message = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailureMessage(message), + .. + }) => Some(message), + _ => None, + }) + .unwrap(); + assert!( + matches!(message.kind, ExpressionKind::Local(_)), + "handler must be extracted before failure" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + + #[test] + fn native_return_owned_candidates_and_error_messages_keep_cleanup_ownership() { + let (program, types) = lower_handler_source( + r#"namespace app +type Nonempty = list[int64] where true +function inspect(items: list[int64]) returns Nonempty: + return items +"#, + ); + let function = inspected_handler_function(&program); + validate(&program).unwrap(); + let entry = &function.blocks[function.entry.index() as usize]; + let StatementKind::Let { + local: candidate, + value, + } = &entry.statements[0].kind + else { + panic!("staged candidate"); + }; + assert!(matches!(&value.kind, ExpressionKind::Clone(inner) + if matches!(inner.kind, ExpressionKind::Local(local) if local == function.params[0].local))); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + for local in [function.params[0].local, *candidate] { + assert!(plan.owned_locals.contains(&(local.index() as usize))); + } + let (index, message) = function + .blocks + .iter() + .enumerate() + .find_map(|(index, block)| { + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailureMessage(message), + .. + }) => Some((index, message)), + _ => None, + }) + }) + .unwrap(); + let ExpressionKind::Local(error) = message.kind else { + panic!("borrow original error"); + }; + assert!(plan.owned_locals.contains(&(error.index() as usize))); + assert!(plan.live_in[index].contains(&(error.index() as usize))); + let returns = function + .blocks + .iter() + .filter_map(|block| { + if let TerminatorKind::Return(Some(value)) = &block.terminator.kind { + Some(value) + } else { + None + } + }) + .collect::>(); + assert_eq!(returns.len(), 1); + assert_eq!(returns[0].ty, function.return_type); + let ExpressionKind::Local(output) = returns[0].kind else { + panic!("owned validated result"); + }; + assert!(plan.owned_locals.contains(&(output.index() as usize))); + } + + const NESTED_BYTE_CALL_SOURCE: &str = r#"namespace app +function render(view value: bytes) returns string: + return "hex" +function quote(value: string) returns string: + return value +function inspect(view value: bytes) returns string: + bytes item = clone value + return quote("0x{render(item)}") +"#; + + fn nested_byte_calls( + function: &Function, + target: hir::FunctionId, + ) -> Vec<(BlockId, usize, &Expression)> { + function + .blocks + .iter() + .flat_map(|block| { + block.statements.iter().enumerate().filter_map(move |(index, statement)| { + let StatementKind::Let { value, .. } = &statement.kind else { return None; }; + matches!(value.kind, ExpressionKind::Call { function, .. } if function == target) + .then_some((block.id, index, value)) + }) + }) + .collect() + } + + #[test] + fn nested_checked_byte_call_stages_owned_input_inside_interpolation_and_outer_call() { + let (hir, types) = handler_source_hir(NESTED_BYTE_CALL_SOURCE); + let original = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let result = original + .body + .statements + .iter() + .find_map(|statement| match &statement.kind { + hir::StatementKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + assert!( + !has_extractable_handle(result), + "no source handler caused the original refusal" + ); + assert!(needs_eager_lowering(result)); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let program = lower(&hir, &types).expect("nested ownership demand must reach render"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 1); + let (block, call_index, call) = calls[0]; + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &call.kind + else { + panic!("source-backed render call"); + }; + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + let hir::ArgumentStaging::Relinquished { owner, loan } = packet.arguments[0].staging else { + panic!("bare byte input acquired once before its physical View"); + }; + let item = function + .locals + .iter() + .find(|local| local.name == "item") + .unwrap() + .id; + let acquisitions = crate::validate_caller_acquisitions(&program, function, &types).unwrap(); + assert_eq!( + acquisitions.owner_initializer(owner).unwrap().binding, + Some(item) + ); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + let statements = &function.blocks[block.index() as usize].statements; + let owner_index = statements + .iter() + .position(|statement| { + matches!(&statement.kind, + StatementKind::Let { local, value } if *local == owner + && matches!(value.kind, ExpressionKind::Local(source) if source == item)) + }) + .expect("full endpoint moves without Clone"); + let begin = statements + .iter() + .position(|statement| { + matches!(statement.kind, + StatementKind::BeginCallView { local, .. } if local == loan) + }) + .unwrap(); + let end = statements + .iter() + .position(|statement| { + matches!(statement.kind, + StatementKind::EndCallView { local } if local == loan) + }) + .unwrap(); + assert!(owner_index < begin && begin < call_index && call_index < end); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + + #[test] + fn nested_checked_byte_call_keeps_written_view_before_last_bare_consumption() { + let source = NESTED_BYTE_CALL_SOURCE + .replace("0x{render(item)}", "0x{render(view item)}:{render(item)}"); + let (hir, types) = handler_source_hir(&source); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let program = lower(&hir, &types).expect("written view followed by one last owning input"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 2); + assert_eq!(calls[0].0, calls[1].0); + assert!( + calls[0].1 < calls[1].1, + "interpolation retains lexical evaluation order" + ); + let effects = calls + .iter() + .map(|(_, _, call)| { + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &call.kind + else { + panic!("source call"); + }; + (packet.arguments[0].effect, packet.arguments[0].staging) + }) + .collect::>(); + assert_eq!( + effects[0], + ( + jett_typecheck::CheckedCallerEffect::RetainBorrow, + hir::ArgumentStaging::Original + ) + ); + assert_eq!( + effects[1].0, + jett_typecheck::CheckedCallerEffect::RelinquishOwned + ); + assert!(matches!( + effects[1].1, + hir::ArgumentStaging::Relinquished { .. } + )); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + + #[test] + fn nested_checked_byte_call_cannot_revert_to_original_owning_stage() { + let (hir, types) = handler_source_hir(NESTED_BYTE_CALL_SOURCE); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "render") + .unwrap() + .id; + let mut program = lower(&hir, &types).expect("valid staged control"); + let function = inspected_handler_function(&program); + let (block, index, _) = nested_byte_calls(function, target)[0]; + let identity = function.id; + let function = program + .functions + .iter_mut() + .find(|function| function.id == identity) + .unwrap(); + let StatementKind::Let { value, .. } = + &mut function.blocks[block.index() as usize].statements[index].kind + else { + panic!("eager result"); + }; + let ExpressionKind::Call { + ownership: hir::CallOwnership::Source(packet), + .. + } = &mut value.kind + else { + panic!("render call"); + }; + packet.arguments[0].staging = hir::ArgumentStaging::Original; + let errors = crate::validate_call_ownership(&program, &types) + .expect_err("mandatory owning stage still enforced"); + assert!( + errors + .iter() + .any(|error| error.message.contains("requires explicit owning staging")), + "{errors:?}" + ); + } + + #[test] + fn nested_checked_byte_call_keeps_lazy_boolean_rhs_in_its_selected_cfg_arm() { + for operator in ["&&", "||"] { + let source = r#"namespace app +function check(view value: bytes) returns bool: + return true +function inspect(gate: bool, item: bytes) returns bool: + return gate OPERATOR check(item) +"# + .replace("OPERATOR", operator); + let (hir, types) = handler_source_hir(&source); + let target = hir + .functions + .iter() + .find(|function| function.identity.declaration.name == "check") + .unwrap() + .id; + let program = lower(&hir, &types).expect("lazy checked call demand"); + let function = inspected_handler_function(&program); + let calls = nested_byte_calls(function, target); + assert_eq!(calls.len(), 1); + let entry = &function.blocks[function.entry.index() as usize]; + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = entry.terminator.kind + else { + panic!("lazy branch required"); + }; + let (selected, bypass) = if operator == "&&" { + (then_block, else_block) + } else { + (else_block, then_block) + }; + assert_eq!(calls[0].0, selected); + assert_ne!(calls[0].0, function.entry); + assert!(function.blocks[bypass.index() as usize].statements.iter().any(|statement| + matches!(statement.kind, StatementKind::Let { value: Expression { kind: ExpressionKind::Bool(value), .. }, .. } + if value == (operator == "||")))); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + } + + #[test] + fn equatable_result_precedes_later_handlers_without_extra_owned_storage() { + for operator in ["==", "!="] { + let source = r#"namespace app +struct Item: + id: int64 +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return run true +function combine(first: bool, second: bool) returns bool: + return first && second +function inspect(view left: Item, view right: Item) returns bool: + optional[bool] later = none + return combine(left OPERATOR right, (later handle: default true)) +"# + .replace("OPERATOR", operator); + let (hir, types) = handler_source_hir_with_equatable(&source, true); + let program = lower(&hir, &types).expect("MIR lowering"); + validate(&program).expect("equality argument handler CFG"); + let function = inspected_handler_function(&program); + let entry = &function.blocks[function.entry.index() as usize]; + let guards = entry + .statements + .iter() + .enumerate() + .filter_map(|(index, statement)| { + let StatementKind::Let { local, value } = &statement.kind else { + return None; + }; + let boundary = if let ExpressionKind::Unary { + op: hir::UnaryOp::Not, + value, + } = &value.kind + { + value.as_ref() + } else { + value + }; + matches!(boundary.kind, ExpressionKind::EquatableResult(_)) + .then_some((index, *local)) + }) + .collect::>(); + assert_eq!(guards.len(), 1, "{operator}"); + let (guard_index, guard_local) = guards[0]; + let handler_index = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .unwrap(); + assert!(guard_index < handler_index, "{operator}"); + let checked = + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + !checked + .owned_locals + .contains(&(guard_local.index() as usize)) + ); + + let mut unchecked = program.clone(); + let lowered = unchecked + .functions + .iter_mut() + .find(|candidate| candidate.id == function.id) + .unwrap(); + let StatementKind::Let { value, .. } = + &mut lowered.blocks[function.entry.index() as usize].statements[guard_index].kind + else { + unreachable!(); + }; + if let ExpressionKind::Unary { value: inner, .. } = &mut value.kind { + let ExpressionKind::EquatableResult(call) = &inner.kind else { + unreachable!(); + }; + **inner = call.as_ref().clone(); + } else { + let ExpressionKind::EquatableResult(call) = &value.kind else { + unreachable!(); + }; + *value = call.as_ref().clone(); + } + let unchecked_plan = crate::move_values::MoveValuePlan::analyze( + &unchecked, + inspected_handler_function(&unchecked), + &types, + ) + .unwrap(); + assert_eq!( + checked.temporary_slots, unchecked_plan.temporary_slots, + "{operator}" + ); + assert_eq!( + checked.owned_locals, unchecked_plan.owned_locals, + "{operator}" + ); + assert_eq!( + checked.live_after_statement, unchecked_plan.live_after_statement, + "{operator}" + ); + } + } + + fn eager_generated_boundary_call<'a>( + function: &'a Function, + block: BlockId, + boundary_index: usize, + result: &Expression, + equality: bool, + ) -> &'a Expression { + let ExpressionKind::Local(output) = result.kind else { + panic!("checked boundary must observe its eagerly completed call result"); + }; + let metadata = function.local(output).expect("exact call result metadata"); + assert_eq!(metadata.ty, result.ty); + assert_eq!(metadata.debug_ty, result.ty); + assert_eq!(metadata.span, result.span); + assert!(metadata.view_source.is_none()); + assert!(function.parameter_for_local(output).is_none()); + let definitions = function + .blocks + .iter() + .flat_map(|block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + let StatementKind::Let { local, value } = &statement.kind else { + return None; + }; + (*local == output).then_some((block.id, index, value)) + }) + }) + .collect::>(); + assert_eq!(definitions.len(), 1, "one eagerly completed operation"); + let (defined_block, call_index, call) = definitions[0]; + assert_eq!(defined_block, block); + assert!(call_index < boundary_index); + assert_eq!(call.ty, result.ty); + assert_eq!(call.span, result.span); + let ExpressionKind::Call { + function: method, + args, + ownership: hir::CallOwnership::Generated(packet), + .. + } = &call.kind + else { + panic!("exact eagerly materialized generated method call"); + }; + match (&packet.operation, equality) { + (hir::GeneratedOperation::Equality { method: expected }, true) + | (hir::GeneratedOperation::Display { method: expected }, false) => { + assert_eq!(method, expected) + } + _ => panic!("result must belong to its exact checked boundary operation"), + } + assert_eq!(args.len(), if equality { 2 } else { 1 }); + assert_eq!(packet.arguments.len(), args.len()); + let statements = &function.blocks[block.index() as usize].statements; + for (value, argument) in args.iter().zip(&packet.arguments) { + let loan = match argument.staging { + hir::GeneratedArgumentStaging::Existing { loan: Some(loan) } + | hir::GeneratedArgumentStaging::OwnedProducer { loan, .. } + | hir::GeneratedArgumentStaging::OrdinarySnapshot { loan, .. } => loan, + _ => panic!("eager call operand must retain its exact scoped loan"), + }; + assert!(matches!(&value.kind, ExpressionKind::View(inner) + if matches!(inner.kind, ExpressionKind::Local(local) if local == loan))); + let begins = statements.iter().enumerate().filter_map(|(index, statement)| + matches!(statement.kind, StatementKind::BeginCallView { local, .. } if local == loan) + .then_some(index)).collect::>(); + let ends = statements + .iter() + .enumerate() + .filter_map(|(index, statement)| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == loan) + .then_some(index) + }) + .collect::>(); + assert_eq!(begins.len(), 1); + assert_eq!(ends.len(), 1); + assert!(begins[0] < call_index); + assert!(call_index < ends[0] && ends[0] < boundary_index); + } + call + } + + #[test] + fn equatable_result_retains_operand_handlers_before_call_and_negation() { + for operator in ["==", "!="] { + let source = r#"namespace app +struct Item: + id: int64 +implement Equatable for Item: + function equals(view self: Item, view other: Item) returns bool: + return true +function inspect(view left: optional[Item], view right: Item) returns bool: + return (left handle: default Item(id: 7)) OPERATOR right +"# + .replace("OPERATOR", operator); + let (hir, types) = handler_source_hir_with_equatable(&source, true); + let program = lower(&hir, &types).expect("MIR lowering"); + validate(&program).expect("equality operand handler CFG"); + let function = inspected_handler_function(&program); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + function.blocks[function.entry.index() as usize] + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + ); + let guards = function + .blocks + .iter() + .filter_map(|block| { + let TerminatorKind::Return(Some(value)) = &block.terminator.kind else { + return None; + }; + assert_eq!( + matches!( + value.kind, + ExpressionKind::Unary { + op: hir::UnaryOp::Not, + .. + } + ), + operator == "!=" + ); + let boundary = if let ExpressionKind::Unary { + op: hir::UnaryOp::Not, + value, + } = &value.kind + { + value.as_ref() + } else { + value + }; + let ExpressionKind::EquatableResult(call) = &boundary.kind else { + return None; + }; + Some((block.id, boundary, call)) + }) + .collect::>(); + assert_eq!(guards.len(), 1, "{operator}"); + let (block, boundary, call) = guards[0]; + assert_ne!(block, function.entry); + let call = eager_generated_boundary_call( + function, + block, + function.blocks[block.index() as usize].statements.len(), + call, + true, + ); + assert!(matches!(call.kind, ExpressionKind::Call { .. })); + assert!(!has_extractable_handle(call)); + assert!(!has_extractable_handle(boundary)); + } + } + + #[test] + fn display_result_is_checked_before_later_interpolation_handlers() { + let (program, types) = lower_handler_source( + r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return run "shown" +function inspect(view item: Item) returns string: + optional[string] later = none + return "{item}:{later handle: default "later"}" +"#, + ); + validate(&program).expect("interpolation handler CFG"); + let function = inspected_handler_function(&program); + let entry = &function.blocks[function.entry.index() as usize]; + let guards = entry + .statements + .iter() + .enumerate() + .filter_map(|(index, statement)| match &statement.kind { + StatementKind::Let { local, value } + if matches!(value.kind, ExpressionKind::DisplayResult(_)) => + { + Some((index, *local, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(guards.len(), 1); + let (guard_index, guard_local, guard) = guards[0]; + let ExpressionKind::DisplayResult(call) = &guard.kind else { + unreachable!(); + }; + assert!(matches!(call.kind, ExpressionKind::Call { .. })); + let handler_index = entry + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .expect("later optional handler"); + assert!(guard_index < handler_index); + assert!(function.blocks.iter().any(|block| { + matches!(&block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: ExpressionKind::StringInterpolation(segments), .. + })) if segments.iter().any(|segment| + matches!(segment, hir::StringSegment::Value(value) + if matches!(value.kind, ExpressionKind::Local(local) if local == guard_local)))) + })); + + let checked_plan = + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + checked_plan + .owned_locals + .contains(&(guard_local.index() as usize)) + ); + let mut unchecked = program.clone(); + let unchecked_function = unchecked + .functions + .iter_mut() + .find(|candidate| candidate.id == function.id) + .unwrap(); + let StatementKind::Let { value, .. } = + &mut unchecked_function.blocks[function.entry.index() as usize].statements[guard_index] + .kind + else { + unreachable!(); + }; + *value = call.as_ref().clone(); + let unchecked_plan = crate::move_values::MoveValuePlan::analyze( + &unchecked, + inspected_handler_function(&unchecked), + &types, + ) + .unwrap(); + assert_eq!(checked_plan.temporary_slots, unchecked_plan.temporary_slots); + assert_eq!(checked_plan.owned_locals, unchecked_plan.owned_locals); + assert_eq!( + checked_plan.live_after_statement, + unchecked_plan.live_after_statement + ); + } + + #[test] + fn display_result_keeps_receiver_handlers_inside_the_checked_call() { + let (program, types) = lower_handler_source( + r#"interface Displayable: + function display(view self: Displayable) returns string +namespace app +struct Item: + value: int64 +implement Displayable for Item: + function display(view self: Item) returns string: + return "shown" +function inspect(view item: optional[Item]) returns string: + return "{item handle: default Item(value: 7)}" +"#, + ); + validate(&program).expect("display receiver handler CFG"); + let function = inspected_handler_function(&program); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + function.blocks[function.entry.index() as usize] + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + ); + let guards = function + .blocks + .iter() + .flat_map(|block| { + block + .statements + .iter() + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) + }) + .filter_map(|(block, index, statement)| match &statement.kind { + StatementKind::Let { value, .. } + if matches!(value.kind, ExpressionKind::DisplayResult(_)) => + { + Some((block, index, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(guards.len(), 1); + let (block, guard_index, guard) = guards[0]; + assert_ne!(block, function.entry); + let ExpressionKind::DisplayResult(call) = &guard.kind else { + unreachable!(); + }; + let call = eager_generated_boundary_call(function, block, guard_index, call, false); + assert!(matches!(call.kind, ExpressionKind::Call { .. })); + assert!(!has_extractable_handle(call)); + assert!(!has_extractable_handle(guard)); + } + + #[test] + fn alias_let_preserves_coarsen_declassify_and_forwarded_backing() { + let (hir, types) = handler_source_hir( + r#"namespace app +type Numbers = list[int64] where true +function inspect(view refined: Numbers, view hidden: secret[list[int64]]) returns list[int64]: + list[int64] plain = coarsen refined + list[int64] forwarded = plain + list[int64] exposed = declassify hidden + list[int64] exposed_alias = exposed + trace forwarded + return clone exposed_alias +"#, + ); + let program = lower(&hir, &types).expect("MIR lowering"); + validate(&program).expect("preserved alias MIR"); + let function = inspected_handler_function(&program); + let original = hir + .functions + .iter() + .find(|original| original.id == function.id) + .unwrap(); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let mut aliases = 0; + for statement in &original.body.statements { + let hir::StatementKind::Let { local, value } = &statement.kind else { + continue; + }; + let metadata = function.local(*local).unwrap(); + let Some(source) = metadata.view_source else { + continue; + }; + aliases += 1; + let lowered = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { + local: target, + value, + } if target == local => Some(value), + _ => None, + }) + .unwrap(); + assert_eq!(lowered, value, "{} initializer was changed", metadata.name); + hir::validate_local_view_initializer( + lowered, + source, + function.local(source).unwrap().ty, + metadata.ty, + &types, + ) + .unwrap(); + assert!(!plan.owned_locals.contains(&(local.index() as usize))); + if metadata.name == "forwarded" || metadata.name == "exposed_alias" { + assert!(function.local(source).unwrap().view_source.is_some()); + } + } + assert_eq!(aliases, 4); + } + + #[test] + fn owned_let_keeps_coarsen_and_declassify_snapshots() { + let (program, types) = lower_handler_source( + r#"namespace app +type Numbers = list[int64] where true +function inspect(refined: Numbers, hidden: secret[list[int64]]) returns list[int64]: + list[int64] plain = coarsen refined + list[int64] exposed = declassify hidden + trace refined + trace hidden + trace plain + return exposed +"#, + ); + let function = inspected_handler_function(&program); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + for name in ["plain", "exposed"] { + let local = function + .locals + .iter() + .find(|local| local.name == name) + .unwrap(); + assert!(local.view_source.is_none()); + assert!(plan.owned_locals.contains(&(local.id.index() as usize))); + let value = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { + local: target, + value, + } if *target == local.id => Some(value), + _ => None, + }) + .unwrap(); + let (ExpressionKind::Coarsen(snapshot) | ExpressionKind::Declassify(snapshot)) = + &value.kind + else { + panic!("expected a qualification wrapper: {value:?}"); + }; + assert!(matches!(&snapshot.kind, ExpressionKind::Clone(value) + if matches!(value.kind, ExpressionKind::Local(_)))); + } + } + + #[test] + fn alias_let_does_not_sanitize_invalid_hir_initializers() { + for invalid in ["clone", "handle", "different source"] { + let (mut hir, mut types) = handler_source_hir( + r#"namespace app +function inspect(view source: list[int64], view other: list[int64]) returns list[int64]: + list[int64] borrowed = view source + return clone borrowed +"#, + ); + let function = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let source = function + .locals + .iter() + .find(|local| local.name == "source") + .unwrap(); + let other = function + .locals + .iter() + .find(|local| local.name == "other") + .unwrap(); + let input = Expression { + kind: ExpressionKind::Local(source.id), + ty: source.ty, + span: source.span, + }; + let other = Expression { + kind: ExpressionKind::Local(other.id), + ty: other.ty, + span: other.span, + }; + let clone = Expression { + kind: ExpressionKind::Clone(Box::new(input.clone())), + ..input.clone() + }; + let malformed = match invalid { + "clone" => clone, + "different source" => other, + "handle" => Expression { + kind: ExpressionKind::Handle { + target: Box::new(Expression { + kind: ExpressionKind::OptionalSome(Box::new(clone)), + ty: types.intern(Type::Optional(input.ty)), + span: input.span, + }), + kind: HandleKind::Optional, + error_local: None, + failure: hir::Block { + statements: vec![hir::Statement { + kind: hir::StatementKind::HandleDefault(Expression { + kind: ExpressionKind::Clone(Box::new(other)), + ..input.clone() + }), + span: input.span, + }], + span: input.span, + }, + }, + ..input.clone() + }, + _ => unreachable!(), + }; + let hir::StatementKind::Let { value, .. } = &mut function.body.statements[0].kind + else { + panic!("expected alias declaration"); + }; + *value = malformed; + hir::validate(&hir).expect("malformed alias remains structurally valid HIR"); + assert!( + hir::validate_backend_types(&hir, &types).is_err(), + "{invalid}" + ); + let errors = + lower(&hir, &types).expect_err("malformed alias refused before MIR acquisition"); + assert!( + errors + .iter() + .any(|error| error.message.contains("stable backing local")), + "{invalid}: {errors:?}" + ); + } + } + + fn staged_alias_source_argument( + function: &Function, + alias: LocalId, + ) -> (&hir::ArgumentOwnership, &Expression, BlockId, usize) { + let calls = function.blocks.iter().flat_map(|block| { + block.statements.iter().enumerate().filter_map(move |(index, statement)| { + let value = match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => value, + _ => return None, + }; + let ExpressionKind::Call { ownership: hir::CallOwnership::Source(packet), args, .. } = &value.kind + else { return None; }; + let argument = packet.arguments.iter().position(|argument| + matches!(&argument.origin, hir::CallerOrigin::Binding(fact) if fact.local == alias))?; + Some((&packet.arguments[argument], &args[argument], block.id, index)) + }) + }).collect::>(); + assert_eq!(calls.len(), 1, "one exact Source argument for the alias"); + calls[0] + } + + fn assert_staged_alias_has_retained_snapshot( + program: &Program, + function: &Function, + alias: LocalId, + types: &TypeInterner, + ) { + let (argument, actual, call_block, call_index) = + staged_alias_source_argument(function, alias); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + let hir::ArgumentStaging::RetainedSnapshot { owner, loan } = argument.staging else { + panic!("ordinary alias endpoint snapshot"); + }; + let physical = argument + .retained_snapshot_span() + .expect("sealed physical occurrence"); + assert_eq!(argument.retained_snapshot_type(), Some(actual.ty)); + assert_eq!(actual.span, physical); + assert!(matches!(&actual.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == loan))); + + let statements = || { + function.blocks.iter().flat_map(|block| { + block + .statements + .iter() + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) + }) + }; + let owners = statements() + .filter_map(|(block, index, statement)| match &statement.kind { + StatementKind::Let { local, value } if *local == owner => { + Some((block, index, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(owners.len(), 1, "unique owning endpoint initializer"); + let (owner_block, owner_index, initializer) = owners[0]; + let ExpressionKind::Clone(endpoint) = &initializer.kind else { + panic!("full endpoint Clone"); + }; + let ExpressionKind::View(original) = &endpoint.kind else { + panic!("unchanged written View"); + }; + assert!(matches!(original.kind, ExpressionKind::Local(id) if id == alias)); + assert_eq!(initializer.span, physical); + assert_eq!(endpoint.span, physical); + assert_eq!( + endpoint.span, argument.source_span, + "original written View occurrence" + ); + assert!(function.local(owner).unwrap().view_source.is_none()); + assert_eq!(function.local(loan).unwrap().view_source, Some(owner)); + + let begins = statements() + .filter_map(|(block, index, statement)| match &statement.kind { + StatementKind::BeginCallView { local, value } if *local == loan => { + Some((block, index, value)) + } + _ => None, + }) + .collect::>(); + assert_eq!(begins.len(), 1, "unique owner-backed Begin"); + let (begin_block, begin_index, projection) = begins[0]; + assert_eq!(begin_block, owner_block); + assert!(owner_index < begin_index); + assert!(matches!(&projection.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == owner))); + let tag_index = function.blocks[owner_block.index() as usize] + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .expect("later argument handler tag"); + assert!( + begin_index < tag_index, + "capture alias endpoint before later handler" + ); + + let ends = statements() + .filter_map(|(block, index, statement)| { + matches!(statement.kind, StatementKind::EndCallView { local } if local == loan) + .then_some((block, index)) + }) + .collect::>(); + assert_eq!(ends.len(), 1, "unique completion End"); + let (end_block, end_index) = ends[0]; + assert_eq!(end_block, call_block); + assert!(call_index < end_index); + + // Persistent alias storage remains precisely its checked initializer. + let alias_values = statements() + .filter_map(|(_, _, statement)| match &statement.kind { + StatementKind::Let { local, value } if *local == alias => Some(value), + _ => None, + }) + .collect::>(); + assert_eq!(alias_values.len(), 1); + let metadata = function.local(alias).unwrap(); + let backing = metadata + .view_source + .expect("original alias stays nonowning"); + hir::validate_local_view_initializer( + alias_values[0], + backing, + function.local(backing).unwrap().ty, + metadata.ty, + types, + ) + .unwrap(); + let plan = crate::move_values::MoveValuePlan::analyze(program, function, types).unwrap(); + assert!(plan.owned_locals.contains(&(owner.index() as usize))); + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(loan.index() as usize))); + assert!( + plan.live_after_statement[call_block.index() as usize][call_index] + .contains(&(owner.index() as usize)) + ); + assert!( + !plan.live_after_statement[end_block.index() as usize][end_index] + .contains(&(owner.index() as usize)), + "owner becomes dead after End" + ); + } + + fn assert_staged_alias_has_raw_borrow(function: &Function, alias: LocalId) { + let (argument, _, _, _) = staged_alias_source_argument(function, alias); + assert_eq!( + argument.effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + argument.syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert!( + argument.retained_snapshot_type().is_none(), + "no descriptor data-copy proof" + ); + assert!(matches!( + argument.staging, + hir::ArgumentStaging::Borrowed { .. } + )); + assert_staged_alias_is_borrowed(function, alias); + } + fn assert_staged_alias_is_borrowed(function: &Function, alias: LocalId) { + let statements = || function.blocks.iter().flat_map(|block| &block.statements); + let loans = statements() + .filter_map(|statement| match &statement.kind { + StatementKind::BeginCallView { local, value } + if matches!(&value.kind, ExpressionKind::View(value) + if matches!(value.kind, ExpressionKind::Local(id) if id == alias)) => + { + Some(*local) + } + _ => None, + }) + .collect::>(); + assert_eq!(loans.len(), 1, "exact alias loan"); + assert!(statements().any(|statement| matches!(statement.kind, + StatementKind::EndCallView { local } if local == loans[0]))); + assert!(!statements().any(|statement| matches!(&statement.kind, + StatementKind::Let { value, .. } if matches!(&value.kind, + ExpressionKind::Clone(value) if matches!(&value.kind, + ExpressionKind::View(value) if matches!(value.kind, + ExpressionKind::Local(id) if id == alias)))))); + } + + #[test] + fn handler_call_staging_preserves_explicit_local_alias_view_parameters() { + let (program, types) = lower_handler_source( + r#"namespace app +function take(view values: list[int64], amount: int64) returns int64: + return amount +function inspect() returns int64: + list[int64] source = list(1) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + int64 answer = take(view forwarded, (none handle: default 3)) + trace source + return answer +"#, + ); + validate(&program).expect("extracted handler CFG"); + let function = inspected_handler_function(&program); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let forwarded = function + .locals + .iter() + .find(|local| local.name == "forwarded") + .unwrap() + .id; + assert_staged_alias_has_retained_snapshot(&program, function, forwarded, &types); + } + + #[test] + fn handler_indirect_staging_snapshots_alias_callee_after_borrowed_arguments() { + let (program, types) = lower_handler_source( + r#"namespace app +function take(view values: list[int64], amount: int64) returns int64: + return amount +function inspect() returns int64: + list[int64] source = list(1) + list[int64] borrowed = view source + function(view list[int64], int64) returns int64 callback = take + function(view list[int64], int64) returns int64 callable = view callback + int64 answer = callable(view borrowed, (none handle: default 4)) + trace source + trace callback + trace callable + return answer +"#, + ); + validate(&program).expect("extracted indirect handler CFG"); + let function = inspected_handler_function(&program); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let callable = function + .locals + .iter() + .find(|local| local.name == "callable") + .unwrap() + .id; + let stages = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::Let { local, value } + if matches!(&value.kind, + ExpressionKind::Clone(value) if matches!(value.kind, + ExpressionKind::Local(id) if id == callable)) => + { + Some(*local) + } + _ => None, + }) + .collect::>(); + assert_eq!(stages.len(), 1); + let callee_stage = stages[0]; + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| { + matches!(&statement.kind, StatementKind::Let { value, .. } + if matches!(&value.kind, ExpressionKind::IndirectCall { callee, .. } + if matches!(callee.kind, ExpressionKind::Local(id) if id == callee_stage))) + }) + ); + assert!(function.blocks.iter().any(|block| { + block.statements.iter().any(|statement| { + matches!(&statement.kind, + StatementKind::Let { local, .. } if *local == callee_stage) + }) && matches!(block.terminator.kind, TerminatorKind::Return(_)) + })); + } + + #[test] + fn handler_call_staging_snapshots_data_but_keeps_function_alias_borrowed() { + let (program, types) = lower_handler_source( + r#"namespace app +function increment(value: int64) returns int64: + return value + 1 +function take(view text: secret[string], view callback: function(int64) returns int64, amount: int64) returns int64: + return callback(amount) +function inspect() returns int64: + secret[string] source = "kept" + secret[string] borrowed = view source + function(int64) returns int64 callback = increment + function(int64) returns int64 callable = view callback + int64 answer = take(view borrowed, view callable, (none handle: default 4)) + trace source + trace callback + return answer +"#, + ); + validate(&program).expect("copy-owned staged views"); + let function = inspected_handler_function(&program); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + let alias = |name| { + function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() + .id + }; + assert_staged_alias_has_retained_snapshot(&program, function, alias("borrowed"), &types); + assert_staged_alias_has_raw_borrow(function, alias("callable")); + } + + #[test] + fn handler_call_staging_keeps_owned_alias_boundaries_rejected() { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse( + r#"namespace app +function take(view values: list[int64], amount: int64) returns int64: + return amount +function inspect() returns int64: + list[int64] source = list(1) + list[int64] borrowed = view source + return take(view borrowed, (none handle: default 3)) +"#, + file, + ); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!(parsed.errors.is_empty()); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let mut hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &std::collections::HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .unwrap(); + hir.functions + .iter_mut() + .find(|function| function.identity.declaration.name == "take") + .unwrap() + .params[0] + .mode = ParamMode::Owned; + let errors = lower(&hir, &checked.interner) + .expect_err("physical formal mutation refused before staging"); + assert!( + errors.iter().any(|error| error.message.contains( + "source function ownership signature differs from the exact HIR declaration" + )), + "{errors:?}" + ); + + let span = Span::new(file, 0, 1); + let explicit = Expression { + kind: ExpressionKind::View(Box::new(Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: TypeInterner::STRING, + span, + })), + ty: TypeInterner::STRING, + span, + }; + assert!(call_staging_inputs(&[explicit], &[ParamMode::Owned]).is_none()); + } + + #[test] + fn aggregate_view_snapshot_rejects_nested_resources() { + let mut types = TypeInterner::new(); + let resource = types.intern(Type::Resource("Socket".to_string())); + let list_of_resources = types.intern(Type::List(resource)); + let list_of_integers = types.intern(Type::List(TypeInterner::INT64)); + + assert!(!can_snapshot_view(&types, resource)); + assert!(!can_snapshot_view(&types, list_of_resources)); + assert!(can_snapshot_view(&types, list_of_integers)); + } + + #[test] + fn sum_snapshot_ignores_only_absent_never_payloads() { + let mut types = TypeInterner::new(); + let resource = types.intern(Type::Resource("Socket".to_string())); + let secret_never = types.intern(Type::Secret(TypeInterner::NEVER)); + let absent = types.intern(Type::Optional(TypeInterner::NEVER)); + let no_success = types.intern(Type::Result(TypeInterner::NEVER, TypeInterner::STRING)); + let no_error = types.intern(Type::Result(TypeInterner::STRING, TypeInterner::NEVER)); + let impossible = types.intern(Type::Result(TypeInterner::NEVER, TypeInterner::NEVER)); + let resource_success = types.intern(Type::Result(resource, TypeInterner::NEVER)); + let resource_failure = types.intern(Type::Result(TypeInterner::NEVER, resource)); + let secret_payload = types.intern(Type::Optional(secret_never)); + let empty_list = types.intern(Type::List(TypeInterner::NEVER)); + + assert!(can_snapshot_view(&types, absent)); + assert!(can_snapshot_view(&types, no_success)); + assert!(can_snapshot_view(&types, no_error)); + assert!(can_snapshot_view(&types, empty_list)); + for rejected in [ + TypeInterner::NEVER, + impossible, + resource_success, + resource_failure, + secret_never, + secret_payload, + ] { + assert!(!can_snapshot_view(&types, rejected), "{rejected:?}"); + } + } + + #[test] + fn contextual_sum_snapshot_clones_the_original_local_before_conversion() { + let mut types = TypeInterner::new(); + let empty = types.intern(Type::List(TypeInterner::NEVER)); + let strings = types.intern(Type::List(TypeInterner::STRING)); + let original = types.intern(Type::Optional(empty)); + let contextual = types.intern(Type::Optional(strings)); + let span = Span::new(jett_common::FileId::new(0), 0, 1); + let local = Expression { + kind: ExpressionKind::Local(LocalId::new(0)), + ty: original, + span, + }; + let conversion = Expression { + kind: ExpressionKind::interface_coerce(Box::new(local.clone())), + ty: contextual, + span, + }; + assert!(snapshotable_local(&types, &conversion)); + let snapshot = snapshot_local(&conversion, conversion.clone()); + let ExpressionKind::InterfaceCoerce { value, .. } = snapshot.kind else { + panic!("expected retained contextual conversion"); + }; + let ExpressionKind::Clone(value) = value.kind else { + panic!("expected source clone before conversion"); + }; + assert_eq!(*value, local); + assert_eq!(snapshot.ty, contextual); + + let empty_map = types.intern(Type::Map(TypeInterner::STRING, TypeInterner::NEVER)); + let optional_map = types.intern(Type::Optional(empty_map)); + assert!(can_snapshot_view(&types, optional_map)); + let resource = types.intern(Type::Resource("Socket".into())); + let resource_list = types.intern(Type::List(resource)); + let optional_resource = types.intern(Type::Optional(resource_list)); + assert!(!can_snapshot_view(&types, optional_resource)); + assert!(!can_snapshot_view(&types, TypeInterner::NEVER)); + } +} diff --git a/crates/jett_mir/src/handlers/reflected.rs b/crates/jett_mir/src/handlers/reflected.rs new file mode 100644 index 00000000..df8b2867 --- /dev/null +++ b/crates/jett_mir/src/handlers/reflected.rs @@ -0,0 +1,421 @@ +//! Two ordered passes over a checked reflected producer tree. The candidate is +//! immutable: projections own private snapshots, never replacement payloads. +use super::*; + +#[derive(Clone, Copy)] +enum Pass { + Preflight, + Predicates, +} + +fn needed(plan: &hir::ReflectedFieldPlan, pass: Pass, produced: bool) -> bool { + use hir::ReflectedFieldAction as A; + match (&plan.action, pass) { + (A::Exact, _) => false, + (A::CallableReturn(_), _) => false, + (A::Predicates(predicates), Pass::Preflight) => produced && !predicates.is_empty(), + (A::Unsupported(_), Pass::Predicates) => false, + (A::Refine { base, predicates }, Pass::Preflight) => { + needed(base, pass, produced) || (produced && !predicates.is_empty()) + } + (A::List(child) | A::Set(child) | A::Optional(child), Pass::Predicates) => { + needed(child, pass, produced) + } + (A::Map { key, value }, Pass::Predicates) => { + needed(key, pass, produced) || needed(value, pass, produced) + } + (A::Result { ok, error }, Pass::Predicates) => { + needed(ok, pass, produced) || needed(error, pass, produced) + } + _ => true, + } +} + +fn local(id: LocalId, ty: TypeId, span: Span) -> Expression { + Expression { + kind: ExpressionKind::Local(id), + ty, + span, + } +} + +impl Builder<'_> { + pub(super) fn lower_reflected_plan( + &mut self, + candidate: LocalId, + plan: &hir::ReflectedFieldPlan, + span: Span, + ) { + self.reflected_pass(candidate, plan, Pass::Preflight, span); + // The second traversal starts only after the entire structural pass. + if self.open() { + self.reflected_pass(candidate, plan, Pass::Predicates, span); + } + } + + fn reflected_pass( + &mut self, + source: LocalId, + plan: &hir::ReflectedFieldPlan, + pass: Pass, + span: Span, + ) { + if !self.open() || !needed(plan, pass, self.reflected_produced_return) { + return; + } + use hir::ReflectedFieldAction as A; + match &plan.action { + A::Exact => {} + A::CallableReturn(_) => {} + A::Unsupported(reason) => { + self.push( + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailure(reason.message().into()), + ty: TypeInterner::NOTHING, + span, + }), + span, + ); + self.terminate(TerminatorKind::Unreachable, span); + } + A::Predicates(predicates) => { + if matches!(pass, Pass::Preflight) { + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: true, + }, + span, + ); + } else { + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + self.reflected_predicates(source, source_type, predicates, span); + } + } + A::Refine { base, predicates } => { + if self.reflected_produced_return + && matches!(pass, Pass::Preflight) + && !predicates.is_empty() + { + self.push( + StatementKind::ReflectedValueReady { + source, + inspect_pending: true, + }, + span, + ); + } + if needed(base, pass, self.reflected_produced_return) { + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + let base_type = if self.reflected_produced_return { + base.source_type + } else { + base.requested_type + }; + let snapshot = self.temporary(base_type, span); + self.push( + StatementKind::Let { + local: snapshot, + value: Expression { + kind: ExpressionKind::Coarsen(Box::new(Expression { + kind: ExpressionKind::Clone(Box::new(local( + source, + source_type, + span, + ))), + ty: source_type, + span, + })), + ty: base_type, + span, + }, + }, + span, + ); + self.reflected_pass(snapshot, base, pass, span); + } + if self.open() && matches!(pass, Pass::Predicates) { + let source_type = if self.reflected_produced_return { + self.locals[source.index() as usize].ty + } else { + plan.requested_type + }; + self.reflected_predicates(source, source_type, predicates, span); + } + } + A::List(child) => self.reflected_sequence( + source, + ReflectedContainerKind::List, + &[(SequencePart::Element, child.as_ref())], + pass, + span, + ), + A::Set(child) => self.reflected_sequence( + source, + ReflectedContainerKind::Set, + &[(SequencePart::Element, child.as_ref())], + pass, + span, + ), + A::Map { key, value } => self.reflected_sequence( + source, + ReflectedContainerKind::Map, + &[ + (SequencePart::Key, key.as_ref()), + (SequencePart::Value, value.as_ref()), + ], + pass, + span, + ), + A::Optional(child) => self.reflected_sum( + source, + ReflectedContainerKind::Optional, + child, + None, + pass, + span, + ), + A::Result { ok, error } => self.reflected_sum( + source, + ReflectedContainerKind::Result, + ok, + Some(error), + pass, + span, + ), + } + } + + fn reflected_sequence( + &mut self, + source: LocalId, + kind: ReflectedContainerKind, + children: &[(SequencePart, &hir::ReflectedFieldPlan)], + pass: Pass, + span: Span, + ) { + if matches!(pass, Pass::Preflight) { + self.push( + StatementKind::ReflectedContainerReady { source, kind }, + span, + ); + } + if !children + .iter() + .any(|(_, plan)| needed(plan, pass, self.reflected_produced_return)) + { + return; + } + let cursor = self.temporary(TypeInterner::INT64, span); + let length = self.temporary(TypeInterner::INT64, span); + self.push( + StatementKind::Let { + local: cursor, + value: Expression { + kind: ExpressionKind::Int(0), + ty: TypeInterner::INT64, + span, + }, + }, + span, + ); + self.push( + StatementKind::SequenceLength { + source: SequenceSource::Local(source), + target: length, + }, + span, + ); + let header = self.new_block(span); + let body = self.new_block(span); + let exit = self.new_block(span); + self.close_to(header, span); + self.current = header; + self.terminate( + TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: hir::BinaryOp::Less, + right: Box::new(local(length, TypeInterner::INT64, span)), + }, + ty: TypeInterner::BOOL, + span, + }, + then_block: body, + else_block: exit, + }, + span, + ); + self.current = body; + for (part, plan) in children { + if !self.open() || !needed(plan, pass, self.reflected_produced_return) { + continue; + } + let child_type = if self.reflected_produced_return { + plan.source_type + } else { + plan.requested_type + }; + let child = self.temporary(child_type, span); + self.push( + StatementKind::SequenceGet { + consume: false, + source: SequenceSource::Local(source), + index: cursor, + target: child, + part: *part, + }, + span, + ); + self.reflected_pass(child, plan, pass, span); + } + if self.open() { + self.push( + StatementKind::Assign { + target: local(cursor, TypeInterner::INT64, span), + value: Expression { + kind: ExpressionKind::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: hir::BinaryOp::Add, + right: Box::new(Expression { + kind: ExpressionKind::Int(1), + ty: TypeInterner::INT64, + span, + }), + }, + ty: TypeInterner::INT64, + span, + }, + }, + span, + ); + self.close_to(header, span); + } + self.current = exit; + } + + fn reflected_sum( + &mut self, + source: LocalId, + kind: ReflectedContainerKind, + ok: &hir::ReflectedFieldPlan, + error: Option<&hir::ReflectedFieldPlan>, + pass: Pass, + span: Span, + ) { + if matches!(pass, Pass::Preflight) { + self.push( + StatementKind::ReflectedContainerReady { source, kind }, + span, + ); + } + if !needed(ok, pass, self.reflected_produced_return) + && !error.is_some_and(|plan| needed(plan, pass, self.reflected_produced_return)) + { + return; + } + let present = self.temporary(TypeInterner::BOOL, span); + self.push( + StatementKind::SumTag { + source, + target: present, + }, + span, + ); + let accepted = self.new_block(span); + let failed = self.new_block(span); + let continuation = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: local(present, TypeInterner::BOOL, span), + then_block: accepted, + else_block: failed, + }, + span, + ); + for (block, child, success) in [(accepted, Some(ok), true), (failed, error, false)] { + self.current = block; + if let Some(child) = + child.filter(|plan| needed(plan, pass, self.reflected_produced_return)) + { + let source_type = self.locals[source.index() as usize].ty; + let snapshot = self.temporary(source_type, span); + self.push( + StatementKind::Let { + local: snapshot, + value: Expression { + kind: ExpressionKind::Clone(Box::new(local(source, source_type, span))), + ty: source_type, + span, + }, + }, + span, + ); + let child_type = if self.reflected_produced_return { + child.source_type + } else { + child.requested_type + }; + let payload = self.temporary(child_type, span); + self.push( + StatementKind::SumTake { + source: snapshot, + target: payload, + success, + }, + span, + ); + self.reflected_pass(payload, child, pass, span); + } + self.close_to(continuation, span); + } + self.current = continuation; + } + + fn reflected_predicates( + &mut self, + source: LocalId, + source_type: TypeId, + predicates: &[hir::RefinementPredicate], + span: Span, + ) { + for predicate in predicates { + let input = self.refinement_predicate_input(source, source_type, predicate, span); + let (error_text, passed) = self.lower_refinement_check(input, predicate, span); + let accepted = self.new_block(span); + let rejected = self.new_block(span); + self.terminate( + TerminatorKind::Branch { + condition: local(passed, TypeInterner::BOOL, span), + then_block: accepted, + else_block: rejected, + }, + span, + ); + self.current = rejected; + self.push( + StatementKind::Evaluate(Expression { + kind: ExpressionKind::RuntimeFailureMessage(Box::new(local( + error_text, + TypeInterner::STRING, + span, + ))), + ty: TypeInterner::NOTHING, + span, + }), + span, + ); + self.terminate(TerminatorKind::Unreachable, span); + self.current = accepted; + } + } +} diff --git a/crates/jett_mir/src/handlers/resource_calls.rs b/crates/jett_mir/src/handlers/resource_calls.rs new file mode 100644 index 00000000..236fe675 --- /dev/null +++ b/crates/jett_mir/src/handlers/resource_calls.rs @@ -0,0 +1,172 @@ +//! Canonical constructor extraction; no public MIR graph may enter this path. +use super::*; +use crate::resource_ownership::constructors::ResourceOperationRegion; + +impl Builder<'_> { + pub(super) fn resource_site(&self) -> ResourceSite { + self.resource_capture + .site( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) + .expect("authenticated Resource constructor") + } + + pub(super) fn lower_resource_call( + &mut self, + original: &Expression, + ) -> Result { + let checkpoint = self.clone(); + match self.try_lower_resource_call(original) { + Ok(value) => Ok(value), + Err(error) => { + *self = checkpoint; + Err(error) + } + } + } + + fn try_lower_resource_call(&mut self, original: &Expression) -> Result { + let ExpressionKind::Call { + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &original.kind + else { + return Err("pending Resource call region: handled indirect/resource-hook invocation timing is unproved".into()); + }; + // Authenticate the original tuple before any extraction or generated Local. + let begin = self.resource_site(); + let region = self.resource_capture.begin_normalized_call( + original, + self.resource_call_scopes.last().copied(), + begin, + self.types, + )?; + self.push( + StatementKind::ResourceCall(ResourceCallNode::Begin { region }), + original.span, + ); + self.resource_call_scopes.push(region); + self.resource_operation_scopes + .push(ResourceOperationRegion::Call(region)); + for (source_index, ¶meter) in evaluation_order.iter().enumerate() { + let actual = args + .get(parameter) + .ok_or("Resource call source permutation is invalid")?; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or("Resource call actual lost its original formal")?; + if fact.source_index != source_index { + return Err("Resource call actual changes its checked lexical order".into()); + } + let start = self.resource_site(); + let first_local = self.locals.len(); + let mut unsupported = false; + crate::resource_ownership::visit_expression(actual, &mut |value| { + unsupported |= matches!( + value.kind, + ExpressionKind::Handle { + kind: HandleKind::Refinement { .. }, + .. + } + ); + }); + if unsupported { + return Err( + "pending Resource call region: refinement handlers need their exact exit proof" + .into(), + ); + } + let endpoint = self.lower_value(actual); + if let Some(error) = &self.resource_error { + return Err(error.message.clone()); + } + let ordinary = if crate::resource_type_pending(self.types, endpoint.ty) { + None + } else { + Some(self.temporary(endpoint.ty, endpoint.span)) + }; + let stage = self.resource_site(); + self.resource_capture.normalized_actual( + region, + source_index, + parameter, + actual, + &endpoint, + start, + stage, + &self.locals[first_local..], + ordinary, + )?; + self.push( + StatementKind::ResourceCall(ResourceCallNode::Stage { + region, + source_index, + parameter, + value: endpoint, + ordinary, + }), + actual.span, + ); + } + let output = self.temporary(original.ty, original.span); + self.push( + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }), + original.span, + ); + self.push( + StatementKind::ResourceCall(ResourceCallNode::End { + region, + outcome: ResourceCompletion::Normal, + }), + original.span, + ); + if self.resource_call_scopes.pop() != Some(region) + || self.resource_operation_scopes.pop() != Some(ResourceOperationRegion::Call(region)) + { + return Err("Resource call constructor lost its exact operation stack".into()); + } + Ok(Expression { + kind: ExpressionKind::Local(output), + ty: original.ty, + span: original.span, + }) + } + + /// Suspend only the active operation suffix before a real Source Return read. + pub(crate) fn abandon_resource_calls( + &mut self, + span: Span, + ) -> ( + Vec, + Vec, + Vec, + ) { + let calls = std::mem::take(&mut self.resource_call_scopes); + let constructors = std::mem::take(&mut self.resource_constructor_scopes); + let operations = std::mem::take(&mut self.resource_operation_scopes); + for operation in operations.iter().rev() { + match *operation { + ResourceOperationRegion::Call(region) => self.push( + StatementKind::ResourceCall(ResourceCallNode::End { + region, + outcome: ResourceCompletion::Abort, + }), + span, + ), + ResourceOperationRegion::Constructor(constructor) => self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::End { + constructor, + outcome: ResourceCompletion::Abort, + }), + span, + ), + } + } + (calls, constructors, operations) + } +} diff --git a/crates/jett_mir/src/handlers/resource_constructors.rs b/crates/jett_mir/src/handlers/resource_constructors.rs new file mode 100644 index 00000000..11765f6b --- /dev/null +++ b/crates/jett_mir/src/handlers/resource_constructors.rs @@ -0,0 +1,104 @@ +//! Checked occupied constructor staging; a general public MIR reseal cannot enter it. +use super::*; +use crate::resource_ownership::constructors::ResourceOperationRegion; + +impl Builder<'_> { + pub(super) fn lower_resource_constructor( + &mut self, + original: &Expression, + ) -> Result { + let checkpoint = self.clone(); + match self.try_lower_resource_constructor(original) { + Ok(v) => Ok(v), + Err(e) => { + *self = checkpoint; + Err(e) + } + } + } + + fn try_lower_resource_constructor( + &mut self, + original: &Expression, + ) -> Result { + let ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } = &original.kind + else { + return Err("Resource constructor changed its original node".into()); + }; + let begin = self.resource_site(); + let constructor = self.resource_capture.begin_constructor_region( + original, + self.resource_operation_scopes.last().copied(), + begin, + self.types, + )?; + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Begin { constructor }), + original.span, + ); + self.resource_constructor_scopes.push(constructor); + self.resource_operation_scopes + .push(ResourceOperationRegion::Constructor(constructor)); + for (source_index, &field) in evaluation_order.iter().enumerate() { + let start = self.resource_site(); + let first_local = self.locals.len(); + let actual = fields + .get(field) + .ok_or("Resource constructor has a foreign field permutation")?; + let endpoint = self.lower_value(actual); + if let Some(error) = &self.resource_error { + return Err(error.message.clone()); + } + let stage = self.resource_site(); + self.resource_capture.constructor_field( + constructor, + source_index, + field, + actual, + &endpoint, + start, + stage, + &self.locals[first_local..], + )?; + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + constructor, + source_index, + field, + value: endpoint, + }), + actual.span, + ); + } + let output = self.temporary(original.ty, original.span); + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::Commit { + constructor, + output, + }), + original.span, + ); + self.push( + StatementKind::ResourceConstructor(ResourceConstructorNode::End { + constructor, + outcome: ResourceCompletion::Normal, + }), + original.span, + ); + if self.resource_constructor_scopes.pop() != Some(constructor) + || self.resource_operation_scopes.pop() + != Some(ResourceOperationRegion::Constructor(constructor)) + { + return Err("Resource constructor lost its exact active operation suffix".into()); + } + Ok(Expression { + kind: ExpressionKind::Local(output), + ty: original.ty, + span: original.span, + }) + } +} diff --git a/crates/jett_mir/src/handlers/tests/reflected_recursive.rs b/crates/jett_mir/src/handlers/tests/reflected_recursive.rs new file mode 100644 index 00000000..3da14fcc --- /dev/null +++ b/crates/jett_mir/src/handlers/tests/reflected_recursive.rs @@ -0,0 +1,228 @@ +use super::*; + +fn reflected_source(actual: &str, requested: &str) -> String { + format!( + r#"namespace app +type Positive = int64 where value > 0 +type Higher = Positive where value > 5 +type Values = list[Positive] where true +struct Record: + values: {actual} +enum Event: + content(values: {actual}) +machine Session: + states: + empty + content(values: {actual}) + transitions: + empty to content +function record_read(view source: Record, view field: TypeField) returns {requested}: + return type.field_value[Record, {requested}](view source, view field) +function variant_read(view source: Event, view field: TypeField) returns {requested}: + return type.variant_field_value[Event, {requested}](view source, view field) +function machine_read(view source: Session, view field: TypeField) returns {requested}: + return type.machine_field_value[Session, {requested}](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns {requested}: + return source into view type.field_value[Record, {requested}](view field) +function variant_pipe(view source: Event, view field: TypeField) returns {requested}: + return source into view type.variant_field_value[Event, {requested}](view field) +function machine_pipe(view source: Session, view field: TypeField) returns {requested}: + return source into view type.machine_field_value[Session, {requested}](view field) +"# + ) +} + +fn reads(program: &Program) -> impl Iterator { + program.functions.iter().filter(|function| { + function.identity.declaration.name.ends_with("_read") + || function.identity.declaration.name.ends_with("_pipe") + }) +} + +#[test] +fn recursive_reflected_producers_finish_structural_pass_before_predicates() { + let source = reflected_source( + "list[optional[result[map[int64, list[int64]], set[int64]]]]", + "list[optional[result[map[Positive, list[Positive]], set[Positive]]]]", + ); + let (program, types) = lower_handler_source(&source); + validate(&program).expect("recursive producer CFG"); + assert_eq!(reads(&program).count(), 6); + for function in reads(&program) { + let mut ready = Vec::new(); + let mut checks = Vec::new(); + let mut getters = 0; + for block in &function.blocks { + for statement in &block.statements { + match &statement.kind { + StatementKind::ReflectedContainerReady { source, kind } => { + assert!(kind.matches_type(&types, function.local(*source).unwrap().ty)); + ready.push((block.id.index(), *kind)); + } + StatementKind::CheckRefinement { .. } => checks.push(block.id.index()), + StatementKind::SequenceGet { consume, .. } => assert!(!consume), + StatementKind::Let { value, .. } + if matches!( + value.kind, + ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::Read), + .. + } + ) => + { + getters += 1; + } + _ => {} + } + } + } + assert_eq!(getters, 1, "one raw getter after staged source operands"); + for kind in [ + ReflectedContainerKind::List, + ReflectedContainerKind::Set, + ReflectedContainerKind::Map, + ReflectedContainerKind::Optional, + ReflectedContainerKind::Result, + ] { + assert!(ready.iter().any(|(_, observed)| *observed == kind)); + } + assert!( + ready.iter().map(|(block, _)| *block).max().unwrap() < *checks.iter().min().unwrap(), + "all structural blocks precede predicate traversal" + ); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("candidate snapshots have independent owners"); + assert!(!plan.owned_locals.is_empty()); + } +} + +#[test] +fn recursive_reflected_reuse_distinguishes_whole_exact_from_child_ancestor() { + for (actual, requested, readiness) in [ + ("list[Positive]", "list[Positive]", false), + ("Values", "Values", false), + ("Values", "list[Positive]", true), + ("list[Higher]", "list[Positive]", true), + ] { + let (program, types) = lower_handler_source(&reflected_source(actual, requested)); + for function in reads(&program) { + let statements: Vec<_> = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect(); + assert_eq!( + statements.iter().any(|statement| matches!( + statement.kind, + StatementKind::ReflectedContainerReady { .. } + )), + readiness, + "whole exact schema skips; generic base/changed wrapper keeps readiness: {actual}/{requested}" + ); + assert!( + !statements.iter().any(|statement| matches!( + statement.kind, + StatementKind::CheckRefinement { .. } + )), + "already proven child predicate must not rerun" + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + } +} + +#[test] +fn recursive_reflected_refine_validates_base_children_before_outer_predicate() { + let (program, types) = lower_handler_source(&reflected_source("list[int64]", "Values")); + for function in reads(&program) { + let checks: Vec<_> = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::CheckRefinement { type_name, .. } => Some(type_name.as_str()), + _ => None, + }) + .collect(); + assert_eq!(checks, ["app.Positive", "app.Values"]); + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedContainerReady { + kind: ReflectedContainerKind::List, + .. + } + )) + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } +} + +#[test] +fn recursive_reflected_map_projection_keeps_key_then_value_without_consuming_candidate() { + let (program, types) = lower_handler_source(&reflected_source( + "map[int64, int64]", + "map[Positive, Positive]", + )); + for function in reads(&program) { + let projections: Vec<_> = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + StatementKind::SequenceGet { part, consume, .. } => Some((part, consume)), + _ => None, + }) + .collect(); + assert_eq!( + projections, + [(SequencePart::Key, false), (SequencePart::Value, false)] + ); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } +} + +#[test] +fn recursive_reflected_root_requests_keep_base_readiness_or_shared_declared_prefix() { + for (actual, requested, readiness, suffix) in [ + ("Values", "Independent", true, vec!["app.Independent"]), + ("Values", "Strong", false, vec!["app.Strong"]), + ("Strong", "Sibling", false, vec!["app.Sibling"]), + ("Strong", "Values", false, vec![]), + ] { + let source = reflected_source(actual, requested).replace( + "type Values = list[Positive] where true", + "type Values = list[Positive] where true\ntype Independent = list[Positive] where true\ntype Strong = Values where true\ntype Sibling = Values where true", + ); + let (program, types) = lower_handler_source(&source); + validate(&program).unwrap(); + for function in reads(&program) { + let statements: Vec<_> = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect(); + assert_eq!( + statements.iter().any(|statement| matches!( + statement.kind, + StatementKind::ReflectedContainerReady { .. } + )), + readiness, + "independent root base preflights; shared declared root preserves proof" + ); + let checks: Vec<_> = statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::CheckRefinement { type_name, .. } => Some(type_name.as_str()), + _ => None, + }) + .collect(); + assert_eq!(checks, suffix, "{actual}/{requested}"); + crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + } + } +} diff --git a/crates/jett_mir/src/iteration_views.rs b/crates/jett_mir/src/iteration_views.rs new file mode 100644 index 00000000..50328037 --- /dev/null +++ b/crates/jett_mir/src/iteration_views.rs @@ -0,0 +1,721 @@ +//! Logical source views backed by an exact original or prepared sequence loop. +use super::*; +use jett_hir::{BinaryOp, ExpressionKind as E, IterationPart, ViewIterationBinding}; +use std::collections::BTreeSet; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct OriginalIteration { + function: FunctionId, + identity: FunctionIdentity, + header: BlockId, + body: BlockId, + exit: BlockId, + bindings: Vec<(LocalId, ViewIterationBinding)>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedViewIteration { + original: OriginalIteration, + source: SequenceSource, + cursor: LocalId, + length: LocalId, + preheader: BlockId, + runtime_span: Span, +} + +impl PreparedViewIteration { + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(header), Some(body), Some(exit), Some(preheader)) = ( + get(self.original.header), + get(self.original.body), + get(self.original.exit), + get(self.preheader), + ) else { + return false; + }; + self.original.header = header; + self.original.body = body; + self.original.exit = exit; + self.preheader = preheader; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(cursor), Some(length)) = + (get(self.source.root()), get(self.cursor), get(self.length)) + else { + return false; + }; + let Some(bindings) = self + .original + .bindings + .iter() + .map(|(local, proof)| get(*local).map(|local| (local, *proof))) + .collect::>>() + else { + return false; + }; + match &mut self.source { + SequenceSource::Local(local) | SequenceSource::Projected { owner: local, .. } => { + *local = source + } + } + self.cursor = cursor; + self.length = length; + self.original.bindings = bindings; + true + } +} + +/// A finite body region supplies backing at an invocation site only. +pub(super) struct IterationScope { + bindings: Vec<(LocalId, ViewIterationBinding)>, + region: BTreeSet, +} + +pub(super) fn apply( + scopes: &[IterationScope], + block: BlockId, + locals: &mut [hir::OwnershipLocalInfo], + types: &TypeInterner, +) { + for local in locals.iter_mut() { + local.view_iteration = None; + } + for scope in scopes + .iter() + .filter(|scope| scope.region.contains(&block.index())) + { + for &(local, proof) in &scope.bindings { + if !jett_typecheck::ownership::is_implicitly_copyable(types, proof.binder_type()) { + locals[local.index() as usize].view_iteration = Some(proof); + } + } + } +} + +/// Forwarded aliases retain the binder's region instead of escaping it. +pub(super) fn ensure_scope( + function: &Function, + scopes: &[IterationScope], + mut local: LocalId, + block: BlockId, +) -> Result<(), String> { + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(local.index()) { + return Err("call ownership iteration alias backing is cyclic".into()); + } + for scope in scopes { + if scope.bindings.iter().any(|(binder, _)| *binder == local) + && !scope.region.contains(&block.index()) + { + return Err( + "call ownership iteration binding is used outside its exact body".into(), + ); + } + } + let Some(source) = function.local(local).and_then(|local| local.view_source) else { + return Ok(()); + }; + local = source; + } +} + +fn read_plan( + function: &Function, + header: BlockId, + types: &TypeInterner, +) -> Result, String> { + let block = function + .blocks + .get(header.index() as usize) + .ok_or("call ownership iteration header is absent")?; + let TerminatorKind::ForEach { + key, + value, + by_view, + iterable, + body, + exit, + } = &block.terminator.kind + else { + return Ok(None); + }; + if !by_view { + return Ok(None); + } + if iterable.ty == TypeInterner::ERROR || iterable.ty.index() as usize >= types.len() { + return Err("call ownership viewed iterable has an invalid exact type".into()); + } + let key_part = match types.resolve(iterable.ty) { + jett_types::Type::Map(..) => IterationPart::Key, + jett_types::Type::List(_) | jett_types::Type::Set(_) | jett_types::Type::String + if value.is_none() => + { + IterationPart::Element + } + _ => return Err("call ownership viewed iteration has invalid binder roles".into()), + }; + let mut bindings = Vec::new(); + for (id, part) in + std::iter::once((*key, key_part)).chain(value.map(|id| (id, IterationPart::Value))) + { + if bindings.iter().any(|(existing, _)| *existing == id) { + return Err("call ownership iteration reuses its binder".into()); + } + let local = function + .local(id) + .ok_or("call ownership iteration binder is absent")?; + let proof = hir::checked_view_iteration_binding( + types, + block.terminator.span, + iterable.ty, + local.ty, + part, + )?; + binding_metadata(function, id, proof)?; + if definitions(function, id) != 1 { + return Err("call ownership iteration binder has another definition".into()); + } + bindings.push((id, proof)); + } + Ok(Some(OriginalIteration { + function: function.id, + identity: function.identity.clone(), + header, + body: *body, + exit: *exit, + bindings, + })) +} + +/// Capture only loops emitted from HIR already validated by the MIR entry gate. +pub(super) fn capture_original(function: &mut Function, types: &TypeInterner) { + function.original_view_iterations = function + .blocks + .iter() + .filter_map(|block| read_plan(function, block.id, types).ok().flatten()) + .collect(); +} + +pub(super) fn original_plan( + function: &Function, + header: BlockId, + types: &TypeInterner, +) -> Result, String> { + let actual = read_plan(function, header, types)?; + let saved = function + .original_view_iterations + .iter() + .filter(|plan| plan.header == header) + .collect::>(); + match (&actual, saved.as_slice()) { + (None, []) => Ok(None), + (Some(actual), [saved]) if actual == *saved => Ok(Some(actual.clone())), + _ => { + Err("call ownership original viewed loop differs from its validated HIR handoff".into()) + } + } +} + +impl OriginalIteration { + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(header), Some(body), Some(exit)) = + (get(self.header), get(self.body), get(self.exit)) + else { + return false; + }; + self.header = header; + self.body = body; + self.exit = exit; + true + } + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let Some(bindings) = self + .bindings + .iter() + .map(|(local, proof)| { + locals + .get(local.index() as usize) + .copied() + .flatten() + .map(|local| (local, *proof)) + }) + .collect::>>() + else { + return false; + }; + self.bindings = bindings; + true + } +} + +/// Only the canonical sequence rewrite calls this beside its actual mutation. +pub(super) fn retain_prepared( + function: &mut Function, + original: OriginalIteration, + source: SequenceSource, + cursor: LocalId, + length: LocalId, + preheader: BlockId, + runtime_span: Span, +) { + function + .prepared_view_iterations + .push(PreparedViewIteration { + original, + source, + cursor, + length, + preheader, + runtime_span, + }); +} + +pub(super) fn scopes( + function: &Function, + types: &TypeInterner, +) -> Result, String> { + let cfg = ControlFlowGraph::analyze(function) + .map_err(|_| "call ownership iteration CFG is invalid")?; + let mut result = Vec::new(); + let mut claimed = BTreeSet::new(); + for original in &function.original_view_iterations { + claim_bindings(original, &mut claimed)?; + let prepared = function + .prepared_view_iterations + .iter() + .filter(|record| record.original.header == original.header) + .collect::>(); + match prepared.as_slice() { + [] => { + if original_plan(function, original.header, types)?.as_ref() != Some(original) { + return Err("call ownership original viewed loop lost its exact raw or prepared association".into()); + } + result.push(IterationScope { + region: body_region( + function, + &cfg, + original.header, + original.body, + original.exit, + &BTreeSet::new(), + )?, + bindings: original.bindings.clone(), + }); + } + [record] if &record.original == original => { + result.push(validate_prepared(function, types, &cfg, record)?) + } + _ => { + return Err( + "call ownership viewed loop has duplicate or changed prepared association" + .into(), + ); + } + } + } + for block in &function.blocks { + if matches!( + block.terminator.kind, + TerminatorKind::ForEach { by_view: true, .. } + ) { + original_plan(function, block.id, types)? + .ok_or("call ownership viewed loop has no original association")?; + } + } + for record in &function.prepared_view_iterations { + if !function + .original_view_iterations + .iter() + .any(|original| original == &record.original) + { + return Err("call ownership prepared iteration has no validated original loop".into()); + } + } + Ok(result) +} + +fn claim_bindings(plan: &OriginalIteration, claimed: &mut BTreeSet) -> Result<(), String> { + for &(local, _) in &plan.bindings { + if !claimed.insert(local.index()) { + return Err("call ownership duplicate iteration binding authority".into()); + } + } + Ok(()) +} + +fn binding_metadata( + function: &Function, + id: LocalId, + proof: ViewIterationBinding, +) -> Result<(), String> { + let local = function + .local(id) + .ok_or("call ownership viewed iteration binder is absent")?; + let span = proof.loop_span(); + if local.mutable + || local.view_source.is_some() + || function.parameter_for_local(id).is_some() + || local.ty != proof.binder_type() + || local.debug_ty != local.ty + || local.span.file != span.file + || local.span.start < span.start + || local.span.end > span.end + { + return Err( + "call ownership viewed iteration binder changed its declaration or ABI role".into(), + ); + } + Ok(()) +} + +pub(super) fn definitions(function: &Function, id: LocalId) -> usize { + let mut count = 0; + for block in &function.blocks { + for statement in &block.statements { + count += usize::from(match &statement.kind { + StatementKind::ResourceConstructor(crate::ResourceConstructorNode::Commit { + output, + .. + }) => *output == id, + StatementKind::ResourceCall( + ResourceCallNode::Stage { + ordinary: Some(local), + .. + } + | ResourceCallNode::Invoke { output: local, .. }, + ) + | StatementKind::Let { local, .. } + | StatementKind::BeginCallView { local, .. } + | StatementKind::CheckRefinement { local, .. } => *local == id, + StatementKind::SequenceGet { target, .. } + | StatementKind::SequenceLength { target, .. } + | StatementKind::SumTake { target, .. } + | StatementKind::SumTag { target, .. } => *target == id, + StatementKind::Assign { target, .. } => { + matches!(target.kind, E::Local(local) if local == id) + } + _ => false, + }); + } + match &block.terminator.kind { + TerminatorKind::ForEach { key, value, .. } => { + count += usize::from(*key == id) + usize::from(*value == Some(id)) + } + TerminatorKind::Switch { variants, .. } => { + count += variants + .iter() + .map(|(_, _, bindings)| bindings.iter().filter(|&&local| local == id).count()) + .sum::() + } + _ => {} + } + } + count +} + +fn body_region( + function: &Function, + cfg: &ControlFlowGraph, + header: BlockId, + body: BlockId, + exit: BlockId, + ends: &BTreeSet, +) -> Result, String> { + let valid = |id: BlockId| (id.index() as usize) < function.blocks.len(); + if !valid(header) + || !valid(body) + || !valid(exit) + || header == body + || body == exit + || header == exit + { + return Err("call ownership iteration has invalid body/exit association".into()); + } + let mut outside = BTreeSet::new(); + let mut pending = vec![function.entry, exit]; + while let Some(block) = pending.pop() { + if block == header || !outside.insert(block.index()) { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + let mut region = BTreeSet::new(); + let mut pending = vec![body]; + while let Some(block) = pending.pop() { + if block == header + || outside.contains(&block.index()) + || ends.contains(&block.index()) + || !region.insert(block.index()) + { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + if !region.contains(&body.index()) { + return Err("call ownership viewed iteration body is reachable outside its loop".into()); + } + for &index in ®ion { + if cfg.predecessors(BlockId(index)).iter().any(|id| { + (*id == header && BlockId(index) != body) + || (*id != header && !region.contains(&id.index())) + }) { + return Err("call ownership iteration body has an external entry".into()); + } + } + Ok(region) +} + +fn local_expression(value: &Expression, id: LocalId, ty: TypeId, span: Span) -> bool { + value.ty == ty && value.span == span && matches!(value.kind, E::Local(local) if local == id) +} + +fn sequence_part(part: IterationPart) -> SequencePart { + match part { + IterationPart::Element => SequencePart::Element, + IterationPart::Key => SequencePart::Key, + IterationPart::Value => SequencePart::Value, + } +} + +fn validate_prepared( + function: &Function, + types: &TypeInterner, + cfg: &ControlFlowGraph, + record: &PreparedViewIteration, +) -> Result { + let plan = &record.original; + if plan.function != function.id + || plan.identity != function.identity + || plan.bindings.is_empty() + || plan.bindings.len() > 2 + { + return Err("call ownership prepared iteration lost its original function/binders".into()); + } + let iterable_type = plan.bindings[0].1.iterable_type(); + if record.source.ty(function) != Some(iterable_type) { + return Err("call ownership prepared iteration changed its exact source type".into()); + } + for &(local, proof) in &plan.bindings { + let checked = hir::checked_view_iteration_binding( + types, + proof.loop_span(), + iterable_type, + proof.binder_type(), + proof.part(), + )?; + if checked != proof + || proof.loop_span() != plan.bindings[0].1.loop_span() + || proof.iterable_type() != iterable_type + { + return Err("call ownership prepared iteration changed its exact endpoint".into()); + } + binding_metadata(function, local, proof)?; + if definitions(function, local) != 1 { + return Err("call ownership prepared iteration binder has another definition".into()); + } + } + for id in [record.cursor, record.length] { + if function.local(id).is_none_or(|local| { + local.ty != TypeInterner::INT64 || local.debug_ty != TypeInterner::INT64 + }) || function.parameter_for_local(id).is_some() + { + return Err("call ownership prepared iterator cursor/length metadata changed".into()); + } + } + let header = function + .blocks + .get(plan.header.index() as usize) + .ok_or("call ownership prepared iteration header is absent")?; + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &header.terminator.kind + else { + return Err("call ownership prepared iteration lost its selecting Branch".into()); + }; + let E::Binary { + left, + op: BinaryOp::Less, + right, + } = &condition.kind + else { + return Err("call ownership prepared iteration lost its exact cursor comparison".into()); + }; + if header.terminator.span != plan.bindings[0].1.loop_span() + || *then_block != plan.body + || condition.ty != TypeInterner::BOOL + || condition.span != record.runtime_span + || !local_expression( + left, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || !local_expression( + right, + record.length, + TypeInterner::INT64, + record.runtime_span, + ) + { + return Err("call ownership prepared iteration changed its cursor or entered body".into()); + } + let preheader = function + .blocks + .get(record.preheader.index() as usize) + .ok_or("call ownership iterator preheader is absent")?; + if !matches!(preheader.terminator.kind, TerminatorKind::Goto(target) if target == plan.header) { + return Err("call ownership iterator preheader bypasses its original header".into()); + } + let [start, zero, length] = preheader + .statements + .get(preheader.statements.len().saturating_sub(3)..) + .ok_or("call ownership iterator initialization is absent")? + else { + return Err("call ownership iterator initialization is incomplete".into()); + }; + if [start, zero, length] + .iter() + .any(|statement| statement.span != record.runtime_span) + || !matches!(&start.kind, StatementKind::IterationBorrow { source, token, start: true } + if *source == record.source && *token == record.cursor) + || !matches!(&zero.kind, StatementKind::Let { local, value } + if *local == record.cursor && value.ty == TypeInterner::INT64 && value.span == record.runtime_span && matches!(value.kind, E::Int(0))) + || !matches!(&length.kind, StatementKind::SequenceLength { source, target } + if *source == record.source && *target == record.length) + { + return Err("call ownership iterator lost its exact borrow/source initialization".into()); + } + let body = function + .blocks + .get(plan.body.index() as usize) + .ok_or("call ownership prepared iteration body is absent")?; + for (index, &(local, proof)) in plan.bindings.iter().enumerate() { + let statement = body + .statements + .get(index) + .ok_or("call ownership viewed extraction is absent")?; + if statement.span != record.runtime_span + || !matches!(&statement.kind, StatementKind::SequenceGet { + consume: false, source, index, target, part } if *source == record.source && *index == record.cursor + && *target == local && *part == sequence_part(proof.part())) + { + return Err( + "call ownership viewed extraction changed source/cursor/target/role or consumption" + .into(), + ); + } + } + let advance = body + .statements + .get(plan.bindings.len()) + .ok_or("call ownership iterator advance is absent")?; + let StatementKind::Assign { target, value } = &advance.kind else { + return Err("call ownership iterator advance changed its statement".into()); + }; + let E::Binary { + left, + op: BinaryOp::Add, + right, + } = &value.kind + else { + return Err("call ownership iterator advance lost its checked addition".into()); + }; + if advance.span != record.runtime_span + || value.ty != TypeInterner::INT64 + || value.span != record.runtime_span + || !local_expression( + target, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || !local_expression( + left, + record.cursor, + TypeInterner::INT64, + record.runtime_span, + ) + || right.ty != TypeInterner::INT64 + || right.span != record.runtime_span + || !matches!(right.kind, E::Int(1)) + { + return Err("call ownership iterator advance changed its exact cursor".into()); + } + let mut starts = 0; + let mut ends = BTreeSet::new(); + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::IterationBorrow { + source, + token, + start, + } = &statement.kind + && *token == record.cursor + { + if *source != record.source || statement.span != record.runtime_span { + return Err("call ownership iterator borrow changed its exact source".into()); + } + if *start { + starts += 1; + } else { + if block.statements.len() != 1 + || !matches!(block.terminator.kind, TerminatorKind::Goto(_)) + { + return Err("call ownership iterator end lost its canonical edge".into()); + } + ends.insert(block.id.index()); + } + } + } + } + if starts != 1 { + return Err("call ownership iterator has missing/duplicate borrow start".into()); + } + if !ends.contains(&else_block.index()) + || !matches!(function.blocks[else_block.index() as usize].terminator.kind, + TerminatorKind::Goto(exit) if exit == plan.exit) + { + return Err( + "call ownership iterator false edge bypasses its exact end and original exit".into(), + ); + } + let region = body_region(function, cfg, plan.header, plan.body, plan.exit, &ends)?; + if ends.iter().any(|&end| { + matches!(function.blocks[end as usize].terminator.kind, + TerminatorKind::Goto(target) if target == plan.header || region.contains(&target.index())) + }) { + return Err("call ownership iterator end re-enters its completed region".into()); + } + for block in std::iter::once(plan.header).chain(region.iter().copied().map(BlockId)) { + if cfg.successors(block).iter().any(|id| { + *id != plan.header && !region.contains(&id.index()) && !ends.contains(&id.index()) + }) { + return Err( + "call ownership iterator leaves its region without the exact borrow end".into(), + ); + } + } + if ends.iter().any(|&end| { + cfg.predecessors(BlockId(end)).is_empty() + || cfg + .predecessors(BlockId(end)) + .iter() + .any(|id| *id != plan.header && !region.contains(&id.index())) + }) { + return Err("call ownership iterator end is outside its exact loop region".into()); + } + Ok(IterationScope { + bindings: plan.bindings.clone(), + region, + }) +} diff --git a/crates/jett_mir/src/iteration_views/tests.rs b/crates/jett_mir/src/iteration_views/tests.rs new file mode 100644 index 00000000..312dca77 --- /dev/null +++ b/crates/jett_mir/src/iteration_views/tests.rs @@ -0,0 +1,227 @@ +use super::*; +use std::collections::HashMap; + +const SOURCE: &str = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function exercise(view items: list[Item], view other: list[Item]) returns int64: + mutable int64 total = 0 + for item in view items: + Item alias = view item + total = total + inspect(view alias) + return total +"#; + +fn source_program(source: &str) -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("source HIR"); + ( + crate::lower(&high, &checked.interner).expect("source MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} + +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} + +fn rejects(program: &Program, types: &TypeInterner, case: impl std::fmt::Display) { + let error = match crate::validate_call_ownership(program, types) { + Err(error) => error, + Ok(()) => panic!("forged iteration provenance accepted case {case}"), + }; + assert!( + error.iter().any(|e| e.message.contains("call ownership") + || e.message.contains("call view") + || e.message.contains("viewed iteration")), + "{error:?}" + ); +} + +#[test] +fn viewed_iteration_provenance_survives_native_sequence_preparation() { + let (mut program, types) = source_program(SOURCE); + assert!(exercise(&program).prepared_view_iterations.is_empty()); + crate::validate_call_ownership(&program, &types).expect("original typed loop"); + crate::prepare_native_sequences(&mut program, &types); + let function = exercise(&program); + assert_eq!(function.prepared_view_iterations.len(), 1); + let record = &function.prepared_view_iterations[0]; + let alias = function.locals.iter().find(|l| l.name == "alias").unwrap(); + let proved = scopes(function, &types).unwrap(); + ensure_scope(function, &proved, alias.id, record.original.body) + .expect("forwarded alias inside exact loop"); + assert!(ensure_scope(function, &proved, alias.id, record.original.exit).is_err()); + crate::validate_call_ownership(&program, &types).expect("prepared typed loop"); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(program, once, "preparation is idempotent"); + crate::sequences::prune::unused_locals(exercise_mut(&mut program)); + crate::validate_call_ownership(&program, &types).expect("metadata follows local compaction"); +} + +#[test] +fn viewed_iteration_prepared_association_rejects_changed_extraction_and_loans() { + let (mut original, types) = source_program(SOURCE); + crate::prepare_native_sequences(&mut original, &types); + assert_eq!(exercise(&original).prepared_view_iterations.len(), 1); + for case in 0..12 { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let record = function.prepared_view_iterations[0].clone(); + match case { + 0 => function.prepared_view_iterations.clear(), + 1 => function.prepared_view_iterations.push(record.clone()), + 2..=6 => { + let StatementKind::SequenceGet { + consume, + source, + index, + target, + part, + } = &mut function.blocks[record.original.body.index() as usize].statements[0].kind + else { + panic!("exact extraction") + }; + match case { + 2 => *consume = true, + 3 => *source = SequenceSource::Local(function.params[1].local), + 4 => *index = record.length, + 5 => *target = function.params[0].local, + 6 => *part = SequencePart::Value, + _ => unreachable!(), + } + } + 7 => { + let start = function.blocks[record.preheader.index() as usize] + .statements + .iter_mut() + .find(|s| matches!(s.kind, StatementKind::IterationBorrow { start: true, .. })) + .unwrap(); + let StatementKind::IterationBorrow { token, .. } = &mut start.kind else { + unreachable!() + }; + *token = record.length; + } + 8 => { + let end = function.blocks.iter_mut().find(|b| b.statements.iter().any(|s| + matches!(s.kind, StatementKind::IterationBorrow { token, start: false, .. } if token == record.cursor))).unwrap(); + end.statements.clear(); + } + 9 => function.prepared_view_iterations[0].original.function = FunctionId::new(999), + 10 => { + let TerminatorKind::Branch { else_block, .. } = &mut function.blocks + [record.original.header.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + *else_block = record.original.body; + } + 11 => { + let end = function.blocks.iter_mut().find(|b| b.statements.iter().any(|s| + matches!(s.kind, StatementKind::IterationBorrow { token, start: false, .. } if token == record.cursor))).unwrap(); + end.terminator.kind = TerminatorKind::Goto(record.original.header); + } + _ => unreachable!(), + } + rejects(&program, &types, case); + } +} + +#[test] +fn viewed_iteration_original_association_rejects_wrong_binder_and_mode() { + let (original, types) = source_program(SOURCE); + for case in 0..3 { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let header = function + .blocks + .iter_mut() + .find(|b| matches!(b.terminator.kind, TerminatorKind::ForEach { .. })) + .unwrap(); + let TerminatorKind::ForEach { + key, + by_view, + iterable, + .. + } = &mut header.terminator.kind + else { + unreachable!() + }; + match case { + 0 => *by_view = false, + 1 => *key = function.params[0].local, + 2 => iterable.ty = TypeInterner::STRING, + _ => unreachable!(), + } + rejects(&program, &types, case); + } +} + +#[test] +fn viewed_iteration_map_roles_and_nested_binders_remain_exact() { + let source = r#"namespace app +struct Item: + code: int64 +function inspect(view item: Item) returns int64: + return item.code +function exercise(view groups: map[string, list[Item]]) returns int64: + mutable int64 total = 0 + for key, items in view groups: + for item in view items: + total = total + inspect(view item) + return total +"#; + let (mut program, types) = source_program(source); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(exercise(&program).prepared_view_iterations.len(), 2); + crate::validate_call_ownership(&program, &types).expect("nested exact regions and map roles"); + let function = exercise_mut(&mut program); + let outer = function + .prepared_view_iterations + .iter() + .find(|r| r.original.bindings.len() == 2) + .unwrap() + .clone(); + let StatementKind::SequenceGet { part, .. } = + &mut function.blocks[outer.original.body.index() as usize].statements[1].kind + else { + panic!("map value extraction") + }; + *part = SequencePart::Key; + rejects(&program, &types, "map value role"); +} diff --git a/crates/jett_mir/src/lib.rs b/crates/jett_mir/src/lib.rs index 7781ed55..545e7c06 100644 --- a/crates/jett_mir/src/lib.rs +++ b/crates/jett_mir/src/lib.rs @@ -1,14 +1,78 @@ //! Jett's backend-neutral control-flow graph representation. +mod actor_handlers; + +mod field_updates; +pub use field_updates::validate_field_updates; +mod actor_graph; +pub use actor_graph::{ + native_graph_metadata_expression, native_graph_metadata_local, validate_native_actor_graphs, +}; mod analysis; +mod breakpoint_regions; +mod call_owner_generations; +mod call_ownership; +mod ordinary_borrowed_sums; +mod temporary_local_views; +pub use ordinary_borrowed_sums::{ + OrdinaryBorrowedSumProjection, OrdinarySumPayloadPath, OrdinarySumProjectionSite, +}; +mod resource_ownership; +pub use resource_ownership::{ + GraphHookProjectionProof, GraphSourceEdgeProof, GraphStorageProof, ResourceArgumentEffect, + ResourceArgumentSyntax, ResourceBorrowedSumProjection, ResourceCallActual, ResourceCallFormal, + ResourceCallNode, ResourceCallOperand, ResourceCallRegion, ResourceCallRegionId, + ResourceCallResult, ResourceCarrierChild, ResourceCarrierConstructor, ResourceCarrierEdge, + ResourceCarrierField, ResourceCarrierFunctionPlan, ResourceCarrierGeneration, + ResourceCarrierIndex, ResourceCarrierIteration, ResourceCarrierLoan, ResourceCarrierLoanId, + ResourceCarrierLoanSource, ResourceCarrierNode, ResourceCarrierObservation, + ResourceCarrierOperation, ResourceCarrierOperationId, ResourceCarrierOperationRole, + ResourceCarrierProjectionPath, ResourceCarrierShape, ResourceCarrierShapeId, + ResourceCarrierSlot, ResourceCarrierSlotId, ResourceCarrierState, ResourceCarrierValue, + ResourceCarrierVariant, ResourceCompanionPlan, ResourceCompletion, ResourceConstructorField, + ResourceConstructorId, ResourceConstructorNode, ResourceConstructorRegion, + ResourceEntryScopePlan, ResourceFrame, ResourceFrameId, ResourceFrameRole, + ResourceFunctionPlan, ResourceLexicalExit, ResourceLexicalExitId, ResourceLexicalExitKind, + ResourceLexicalExitPlan, ResourceLoan, ResourceLoanId, ResourceLoanSource, + ResourceNamedCallableProof, ResourceNativeSuiteInvocation, ResourceOccupancy, + ResourceOperation, ResourceOperationId, ResourceOperationRole, ResourceOwnerSlot, + ResourceOwnerSlotId, ResourceOwnershipPlan, ResourcePath, ResourcePosition, ResourceShape, + ResourceSite, ResourceSlotStorage, ResourceSumPayloadPath, native_graph_hook_projection, + native_graph_source_edge, native_graph_storage_for_constructor, + native_graph_storage_for_expression, native_graph_storage_for_formal, + native_graph_storage_for_local, native_graph_storage_for_return, + native_graph_storage_has_records, validate_resource_ownership, + validate_resource_ownership_for_entry, +}; +#[cfg(test)] +mod resource_manifest_tests; +pub use call_owner_generations::{ + CallGenerationSlot, CallGenerationStoragePlan, CallOwnerEscrowId, CallOwnerGenerationId, +}; +pub use call_ownership::{CallerAcquisitions, SourceAcquisition}; +mod call_views; +pub mod copy_values; +mod generated_functions; +mod reflected_callable; +pub use reflected_callable::validate_reflected_callable_plans; +mod handlers; +mod iteration_views; +mod match_bindings; +pub use generated_functions::prepare_native_generated_functions; +mod sequences; +pub use sequences::prepare_native_sequences; +mod uninhabited_sums; +pub use uninhabited_sums::prepare_native_uninhabited_sums; +pub mod move_values; pub use analysis::{AnalysisError, ControlFlowGraph}; +pub use jett_hir::{FunctionId, Local, LocalId, Param, ParamMode}; use jett_common::Span; -use jett_hir::{self as hir, Expression, FunctionIdentity, LocalId, VariantId}; -use jett_types::TypeId; +use jett_hir::{self as hir, Expression, FieldId, FunctionIdentity, VariantId}; +use jett_types::{Type, TypeId, TypeInterner}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BlockId(u32); impl BlockId { @@ -19,16 +83,64 @@ impl BlockId { #[derive(Debug, Clone, PartialEq)] pub struct Program { + pub resource_manifest: hir::ResourceManifest, pub functions: Vec, + pub equality_methods: std::collections::HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct Function { + pub id: FunctionId, pub identity: FunctionIdentity, + pub debug_kind: hir::FunctionDebugKind, + pub params: Vec, + pub capture_count: usize, pub return_type: TypeId, + pub locals: Vec, pub entry: BlockId, pub blocks: Vec, pub span: Span, + // Only canonical MIR preparation can authenticate a removed success edge. + prepared_absent_successes: Vec, + prepared_present_successes: Vec, + prepared_view_iterations: Vec, + original_view_iterations: Vec, + match_bindings: Option, + breakpoint_regions: Vec, + call_owner_generations: Vec, + resource_lowering: Option, + ordinary_borrowed_sums: Option, + temporary_local_views: Option, + reflected_callable: Option, + actor_graph: Option, + field_updates: Option, + actor_handler: Option, + actor_state_views: Vec, + actor_message_source: bool, +} + +impl Function { + /// Look up a local through the canonical dense local table. + pub fn local(&self, id: LocalId) -> Option<&Local> { + self.locals + .get(id.index() as usize) + .filter(|local| local.id == id) + } + + /// Look up the parameter metadata associated with a local, if any. + pub fn parameter_for_local(&self, id: LocalId) -> Option<&Param> { + self.params.iter().find(|param| param.local == id) + } + + /// Whether a local holds a view parameter or nonowning local alias. + pub fn is_view_local(&self, id: LocalId) -> bool { + hir::is_borrowed_local(&self.locals, &self.params, id) + } + + /// The stable backing local of a direct or forwarded local view. + pub fn view_root(&self, id: LocalId) -> Option { + hir::local_view_root(&self.locals, id) + } } #[derive(Debug, Clone, PartialEq)] @@ -43,17 +155,175 @@ pub struct Statement { pub kind: StatementKind, pub span: Span, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SequencePart { + Element, + Key, + Value, +} +/// Internal producer preflight; it has no source spelling or payload observer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReflectedContainerKind { + List, + Set, + Map, + Optional, + Result, +} +impl ReflectedContainerKind { + pub fn message(self) -> &'static str { + match self { + Self::List => "reflected field refinement: expected a ready list value", + Self::Set => "reflected field refinement: expected a ready set value", + Self::Map => "reflected field refinement: expected a ready map value", + Self::Optional => "reflected field refinement: expected a ready optional value", + Self::Result => "reflected field refinement: expected a ready result value", + } + } + pub fn matches_type(self, types: &TypeInterner, ty: TypeId) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + matches!( + (self, types.resolve(ty)), + (Self::List, jett_types::Type::List(_)) + | (Self::Set, jett_types::Type::Set(_)) + | (Self::Map, jett_types::Type::Map(..)) + | (Self::Optional, jett_types::Type::Optional(_)) + | (Self::Result, jett_types::Type::Result(..)) + ) + } +} +/// One checked struct-field step in a borrowed sequence projection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SequenceField { + pub owner_type: TypeId, + pub field: FieldId, +} + +/// A sequence owned by a local, or borrowed through a checked struct-field path. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SequenceSource { + Local(LocalId), + Projected { + owner: LocalId, + path: Vec, + ty: TypeId, + }, +} + +impl SequenceSource { + pub fn root(&self) -> LocalId { + match self { + Self::Local(local) => *local, + Self::Projected { owner, .. } => *owner, + } + } + + pub fn ty(&self, function: &Function) -> Option { + match self { + Self::Local(local) => function.local(*local).map(|local| local.ty), + Self::Projected { ty, .. } => Some(*ty), + } + } +} #[derive(Debug, Clone, PartialEq)] pub enum StatementKind { + /// Private constructor-owned lexical loan retirement; copied IDs grant nothing. + ResourceLexicalExit(ResourceLexicalExitId), + /// Constructor-authenticated Resource operation whose actuals span CFG blocks. + ResourceCall(ResourceCallNode), + ResourceConstructor(ResourceConstructorNode), + /// Empty, internal generation escrow; this is not an initialized Jett value. + OpenCallOwnerGeneration { + generation: CallOwnerGenerationId, + root: LocalId, + }, + /// Transfer the old owner once, after the independent RHS has been acquired. + ReplaceCallOwnerGeneration { + generation: CallOwnerGenerationId, + root: LocalId, + rhs_owner: LocalId, + }, + /// End the matching captured generation after its last call loan. + CloseCallOwnerGeneration { + generation: CallOwnerGenerationId, + }, + /// A compiler-internal, call-bounded borrow with an exact eager borrow proof. + /// Unlike a source Let alias, only this typed statement grants End authority. + BeginCallView { + local: LocalId, + value: Expression, + }, + /// End only the matching internal loan; the borrowed slot owns no destructor. + EndCallView { + local: LocalId, + }, + /// Reject a changed wrapper's outer pending depth before any producer predicate. + ReflectedContainerReady { + source: LocalId, + kind: ReflectedContainerKind, + }, + /// Private produced-return preflight, authenticated by its callable witness. + ReflectedValueReady { + source: LocalId, + inspect_pending: bool, + }, + SequenceLength { + source: SequenceSource, + target: LocalId, + }, + SequenceGet { + /// Transfer an initialized element from an exclusively owned iterator. + consume: bool, + source: SequenceSource, + index: LocalId, + target: LocalId, + part: SequencePart, + }, + IterationBorrow { + source: SequenceSource, + token: LocalId, + start: bool, + }, + /// Read a sum discriminant without consuming its owner. + SumTag { + source: LocalId, + target: LocalId, + }, + /// Consume the sum and transfer only its selected initialized payload. + SumTake { + source: LocalId, + target: LocalId, + success: bool, + }, Let { local: LocalId, value: Expression, }, + /// Evaluate a checked refinement predicate and materialize either an empty + /// success string or the interpreter-compatible failure message. + CheckRefinement { + local: LocalId, + call: Expression, + type_name: String, + }, Assign { target: Expression, value: Expression, }, + /// Check the fixed, sealed owner path before acquiring any RHS value. + PrepareFieldUpdate { + place: hir::FieldUpdatePlace, + begin: bool, + }, + /// Commit only after RHS staging; old field retirement follows transfer. + UpdateField { + target: Expression, + value: Expression, + place: hir::FieldUpdatePlace, + }, Evaluate(Expression), HandleDefault(Expression), Assert { @@ -61,7 +331,10 @@ pub enum StatementKind { message: Option, }, Trace(LocalId), - Breakpoint(Option), + Breakpoint { + condition: Option, + bindings: Vec, + }, } #[derive(Debug, Clone, PartialEq)] @@ -70,6 +343,17 @@ pub struct Terminator { pub span: Span, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReflectedTypeDispatchArm { + /// Canonical position exported by the checker for stable diagnostics. + pub iteration_index: usize, + /// Canonical concrete type identity matched against the runtime `TypeInfo`. + pub bound_type: TypeId, + /// Checker-owned structural identity for native runtime selection. + pub reflection_identity: String, + pub target: BlockId, +} + #[derive(Debug, Clone, PartialEq)] pub enum TerminatorKind { Return(Option), @@ -93,6 +377,14 @@ pub enum TerminatorKind { body: BlockId, exit: BlockId, }, + /// Select exactly one checker-specialized arm by source-visible reflected type + /// identity. `otherwise` is a defensive edge for malformed runtime + /// `TypeInfo` values and normally leads to `Unreachable`. + ReflectedTypeDispatch { + type_info: Expression, + arms: Vec, + otherwise: BlockId, + }, Unreachable, } @@ -110,77 +402,1005 @@ pub struct ValidationError { /// Validate structural MIR invariants before a backend consumes the program. pub fn validate(program: &Program) -> Result<(), Vec> { + if let Err(message) = actor_graph::validate_native_actor_graphs(program) { + return Err(vec![ValidationError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |f| f.span), + message, + }]); + } let mut errors = Vec::new(); - for function in &program.functions { - if function.entry.index() as usize >= function.blocks.len() { + let mut function_ids = std::collections::HashSet::new(); + for (index, function) in program.functions.iter().enumerate() { + if let Err(message) = actor_handlers::validate(function) { + errors.push(ValidationError { + span: function.span, + message, + }); + } + if function.id.index() as usize != index { errors.push(ValidationError { span: function.span, message: format!( + "function at index {index} has noncanonical ID {}", + function.id.index() + ), + }); + } + if !function_ids.insert(function.id) { + errors.push(ValidationError { + span: function.span, + message: format!("multiple functions use ID {}", function.id.index()), + }); + } + FunctionValidator { + function, + functions: &program.functions, + function_count: program.functions.len(), + resource_manifest: &program.resource_manifest, + errors: &mut errors, + } + .validate(); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Conservative operational refusal until the separate Resource CFG plan exists. +pub(crate) fn resource_type_pending(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) || visit(types, *value, seen) + } + Type::Function { + params, + return_type, + .. + } => { + params.iter().any(|ty| visit(types, *ty, seen)) || visit(types, *return_type, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, ty)| visit(types, *ty, seen)), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .any(|variant| variant.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + Type::Machine(id) | Type::MachineState { machine: id, .. } => types + .resolve_machine(*id) + .states + .iter() + .any(|state| state.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +struct FunctionValidator<'function, 'errors> { + function: &'function Function, + functions: &'function [Function], + function_count: usize, + resource_manifest: &'function hir::ResourceManifest, + errors: &'errors mut Vec, +} + +/// Validate every invocation before preparation, including unused bodies and +/// disconnected blocks. Metadata origins must never become execution liveness. +pub fn validate_call_ownership( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + validate(program)?; + validate_reflected_callable_plans(program, types)?; + let mut errors = Vec::new(); + if let Err(message) = program.resource_manifest.validate(types) { + // Type bounds are a prerequisite for custody and invocation queries. + // A malformed type cannot establish that a Resource witness is needed. + return Err(vec![ValidationError { + span: Span::new(jett_common::FileId::new(0), 0, 0), + message, + }]); + } + let validation = call_ownership::ProgramValidation::new(program); + for function in &program.functions { + if let Err(message) = actor_handlers::validate(function) { + errors.push(ValidationError { + span: function.span, + message, + }); + } + if let Err(message) = ordinary_borrowed_sums::validate(function, types) { + errors.push(ValidationError { + span: function.span, + message, + }); + continue; + } + if let Err(message) = validation.validate_function(function, types) { + errors.push(ValidationError { + span: function.span, + message, + }); + } + } + // Custody queries depend on valid invocation and original type metadata. + if !errors.is_empty() { + return Err(errors); + } + if let Err(mut witness_errors) = resource_ownership::validate_witnesses(program, types) { + errors.append(&mut witness_errors); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Acquire a function's sealed source ownership evidence before native lowering. +pub fn validate_caller_acquisitions<'a>( + program: &Program, + function: &'a Function, + types: &TypeInterner, +) -> Result, String> { + let acquisitions = call_ownership::validate_function(program, function, types)?; + if acquisitions.resource_pending + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + || resource_type_pending(types, function.return_type) + { + return Err("pending ResourceOwnershipPlan: typed Resource nodes grant no ordinary move, copy, borrow or drop authority".into()); + } + Ok(acquisitions) +} + +impl FunctionValidator<'_, '_> { + fn validate(&mut self) { + let function = self.function; + if function.capture_count > function.params.len() { + self.error( + function.span, + "closure capture count exceeds function parameter count", + ); + } + if function.entry.index() as usize >= function.blocks.len() { + self.error( + function.span, + format!( "function entry block {} is out of range", function.entry.index() ), - }); + ); + } + + let internal_call_views = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + StatementKind::BeginCallView { local, .. } => Some(local), + _ => None, + }) + .collect::>(); + for (index, local) in function.locals.iter().enumerate() { + if local.id.index() as usize != index { + self.error( + local.span, + format!( + "local at index {index} has noncanonical ID {}", + local.id.index() + ), + ); + } + if let Some(source) = local.view_source { + self.check_local(source, local.span, "local view source"); + if source.index() >= local.id.index() || function.view_root(local.id).is_none() { + self.error(local.span, "local view source must precede its binding"); + } + let actor_state_view = function.actor_state_view(local.id).unwrap_or(false); + if local.mutable + || (function.parameter_for_local(local.id).is_some() && !actor_state_view) + { + self.error( + local.span, + "local view aliases must be immutable local bindings", + ); + } + if !actor_state_view + && !internal_call_views.contains(&local.id) + && let Some(root) = function.view_root(local.id) + && function.local(root).is_some_and(|root| root.mutable) + && function + .parameter_for_local(root) + .is_some_and(|parameter| parameter.mode == hir::ParamMode::View) + { + self.error( + local.span, + "native local views require immutable view-parameter backing bindings", + ); + } + } + } + + let mut parameter_locals = std::collections::HashSet::new(); + for param in &function.params { + self.check_local(param.local, param.span, "parameter"); + if !parameter_locals.insert(param.local) { + self.error( + param.span, + format!( + "multiple parameters reference local {}", + param.local.index() + ), + ); + } + if let Some(local) = function.local(param.local) { + let metadata_matches = local.name == param.name + && local.ty == param.ty + && local.mutable == param.mutable + && local.span == param.span; + if !metadata_matches { + self.error( + param.span, + format!( + "parameter metadata does not match local {}", + param.local.index() + ), + ); + } + } } + for (index, block) in function.blocks.iter().enumerate() { if block.id.index() as usize != index { - errors.push(ValidationError { - span: function.span, - message: format!( + self.error( + function.span, + format!( "block at index {index} has noncanonical ID {}", block.id.index() ), - }); + ); } - let block_count = function.blocks.len(); - let mut check_target = |target: BlockId, edge: &str| { - if target.index() as usize >= block_count { - errors.push(ValidationError { - span: function.span, - message: format!("{edge} target {} is out of range", target.index()), - }); + for statement in &block.statements { + self.statement(statement); + } + self.terminator(&block.terminator); + } + } + + fn error(&mut self, span: Span, message: impl Into) { + self.errors.push(ValidationError { + span, + message: message.into(), + }); + } + + fn check_local(&mut self, local: LocalId, span: Span, context: &str) { + let local_count = self.function.locals.len(); + if local.index() as usize >= local_count { + self.error( + span, + format!( + "{context} references local {} outside function local table of length {local_count}", + local.index() + ), + ); + } + } + + fn check_sequence_source(&mut self, source: &SequenceSource, span: Span) { + let root = source.root(); + self.check_local(root, span, "sequence source"); + if let SequenceSource::Projected { path, .. } = source { + if path.is_empty() { + self.error(span, "projected sequence has no field path"); + } else if self + .function + .local(root) + .is_some_and(|local| local.ty != path[0].owner_type) + { + self.error( + span, + "projected sequence owner type does not match its local", + ); + } + } + } + + fn check_target(&mut self, target: BlockId, edge: &str) { + if target.index() as usize >= self.function.blocks.len() { + self.error( + self.function.span, + format!("{edge} target {} is out of range", target.index()), + ); + } + } + + fn check_function(&mut self, function: FunctionId, span: Span) { + if function.index() as usize >= self.function_count { + self.error( + span, + format!( + "direct call references function {} outside function table of length {}", + function.index(), + self.function_count + ), + ); + } + } + + fn check_evaluation_order(&mut self, order: &[usize], argument_count: usize, span: Span) { + let mut seen = vec![false; argument_count]; + let valid = order.len() == argument_count + && order + .iter() + .all(|&index| index < argument_count && !std::mem::replace(&mut seen[index], true)); + if !valid { + self.error( + span, + "evaluation order must be a permutation of the operand indexes", + ); + } + } + + fn statement(&mut self, statement: &Statement) { + match &statement.kind { + StatementKind::ResourceLexicalExit(_) => {} + StatementKind::ResourceConstructor(node) => match node { + ResourceConstructorNode::Stage { value, .. } => self.expression(value), + ResourceConstructorNode::Commit { output, .. } => { + self.check_local(*output, statement.span, "Resource constructor output") } - }; - match &block.terminator.kind { - TerminatorKind::Goto(target) => check_target(*target, "goto"), - TerminatorKind::Branch { - then_block, - else_block, - .. + _ => {} + }, + StatementKind::ResourceCall(node) => match node { + ResourceCallNode::Stage { + value, ordinary, .. } => { - check_target(*then_block, "branch then"); - check_target(*else_block, "branch else"); + self.expression(value); + if let Some(local) = ordinary { + self.check_local(*local, statement.span, "Resource call ordinary stage"); + } } - TerminatorKind::Switch { - variants, - otherwise, - .. - } => { - for (variant, target, _) in variants { - check_target(*target, &format!("switch variant {}", variant.index())); + ResourceCallNode::Invoke { output, .. } => { + self.check_local(*output, statement.span, "Resource call output") + } + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. } => {} + }, + StatementKind::OpenCallOwnerGeneration { root, .. } => { + self.check_local(*root, statement.span, "generation root") + } + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + self.check_local(*root, statement.span, "generation root"); + self.check_local(*rhs_owner, statement.span, "generation acquired RHS"); + } + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { + self.check_local(*source, statement.span, "reflected container source"); + } + StatementKind::SequenceLength { source, target } => { + self.check_sequence_source(source, statement.span); + self.check_local(*target, statement.span, "sequence length"); + } + StatementKind::SequenceGet { + source, + index, + target, + .. + } => { + self.check_sequence_source(source, statement.span); + self.check_local(*index, statement.span, "sequence index"); + self.check_local(*target, statement.span, "sequence element"); + } + StatementKind::IterationBorrow { source, token, .. } => { + self.check_sequence_source(source, statement.span); + self.check_local(*token, statement.span, "iteration loan token"); + } + + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + self.check_local(*source, statement.span, "sum source"); + self.check_local(*target, statement.span, "sum target"); + } + + StatementKind::Let { local, value } | StatementKind::BeginCallView { local, value } => { + self.check_local(*local, statement.span, "let statement"); + self.expression(value); + } + StatementKind::EndCallView { local } => { + self.check_local(*local, statement.span, "call view end"); + } + StatementKind::CheckRefinement { + local, + call, + type_name, + } => { + self.check_local(*local, statement.span, "refinement error text"); + self.expression(call); + let valid = if let hir::ExpressionKind::Call { function, args, .. } = &call.kind { + self.functions + .get(function.index() as usize) + .is_some_and(|predicate| { + hir::refinement_predicate_declaration_matches( + &predicate.identity.declaration, + type_name, + ) && predicate.capture_count == 0 + && predicate.params.len() == 1 + && predicate.params[0].mode == ParamMode::Owned + && predicate.return_type == jett_types::TypeInterner::BOOL + && args.len() == 1 + && args[0].ty == predicate.params[0].ty + && call.ty == jett_types::TypeInterner::BOOL + }) + } else { + false + }; + if !valid { + self.error( + statement.span, + "refinement check does not call its exact predicate declaration", + ); + } + } + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + self.expression(target); + self.expression(value); + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + self.expression(value); + } + StatementKind::Assert { condition, message } => { + self.expression(condition); + if let Some(message) = message { + self.expression(message); + } + } + StatementKind::Trace(local) => { + self.check_local(*local, statement.span, "trace statement"); + } + StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + self.expression(condition); + } + for binding in bindings { + self.check_local(*binding, statement.span, "breakpoint binding"); + } + } + } + } + + fn terminator(&mut self, terminator: &Terminator) { + match &terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + self.expression(value); + } + } + TerminatorKind::Respond(value) => self.expression(value), + TerminatorKind::Goto(target) => self.check_target(*target, "goto"), + TerminatorKind::Branch { + condition, + then_block, + else_block, + } => { + self.expression(condition); + self.check_target(*then_block, "branch then"); + self.check_target(*else_block, "branch else"); + } + TerminatorKind::Switch { + scrutinee, + variants, + otherwise, + } => { + self.expression(scrutinee); + for (variant, target, bindings) in variants { + self.check_target(*target, &format!("switch variant {}", variant.index())); + for binding in bindings { + self.check_local(*binding, terminator.span, "switch binding"); } - if let Some(target) = otherwise { - check_target(*target, "switch otherwise"); + } + if let Some(target) = otherwise { + self.check_target(*target, "switch otherwise"); + } + } + TerminatorKind::ForEach { + key, + value, + iterable, + body, + exit, + .. + } => { + self.check_local(*key, terminator.span, "for key"); + if let Some(value) = value { + self.check_local(*value, terminator.span, "for value"); + } + self.expression(iterable); + self.check_target(*body, "for body"); + self.check_target(*exit, "for exit"); + } + TerminatorKind::ReflectedTypeDispatch { + type_info, + arms, + otherwise, + } => { + self.expression(type_info); + if arms.is_empty() { + self.error(terminator.span, "reflected type dispatch has no arms"); + } + let mut iteration_indexes = std::collections::HashSet::new(); + let mut bound_types = std::collections::HashSet::new(); + let mut targets = std::collections::HashSet::new(); + for arm in arms { + if arm.reflection_identity.is_empty() { + self.error( + terminator.span, + "reflected type dispatch arm has no checked identity", + ); } + if !iteration_indexes.insert(arm.iteration_index) { + self.error( + terminator.span, + "reflected type dispatch contains a duplicate iteration index", + ); + } + if !bound_types.insert(&arm.reflection_identity) { + self.error( + terminator.span, + "reflected type dispatch contains a duplicate reflected type identity", + ); + } + targets.insert(arm.target); + self.check_target(arm.target, "reflected type dispatch arm"); } - TerminatorKind::ForEach { body, exit, .. } => { - check_target(*body, "for body"); - check_target(*exit, "for exit"); + if targets.contains(otherwise) { + self.error( + terminator.span, + "reflected type dispatch otherwise target overlaps an arm target", + ); } - TerminatorKind::Return(_) - | TerminatorKind::Respond(_) - | TerminatorKind::Unreachable => {} + self.check_target(*otherwise, "reflected type dispatch otherwise"); } + TerminatorKind::Unreachable => {} } } - if errors.is_empty() { - Ok(()) - } else { - Err(errors) + + fn hir_block(&mut self, block: &hir::Block) { + for statement in &block.statements { + self.hir_statement(statement); + } + } + + fn hir_statement(&mut self, statement: &hir::Statement) { + match &statement.kind { + hir::StatementKind::Let { local, value } => { + self.check_local(*local, statement.span, "nested let statement"); + self.expression(value); + } + hir::StatementKind::Assign { target, value } + | hir::StatementKind::UpdateField { target, value, .. } => { + self.expression(target); + self.expression(value); + } + hir::StatementKind::Return(value) => { + if let Some(value) = value { + self.expression(value); + } + } + hir::StatementKind::HandleDefault(value) + | hir::StatementKind::Expression(value) + | hir::StatementKind::Respond(value) => self.expression(value), + hir::StatementKind::If { + condition, + then_block, + else_block, + } => { + self.expression(condition); + self.hir_block(then_block); + if let Some(else_block) = else_block { + self.hir_block(else_block); + } + } + hir::StatementKind::While { condition, body } => { + self.expression(condition); + self.hir_block(body); + } + hir::StatementKind::For { + key, + value, + iterable, + body, + .. + } => { + self.check_local(*key, statement.span, "nested for key"); + if let Some(value) = value { + self.check_local(*value, statement.span, "nested for value"); + } + self.expression(iterable); + self.hir_block(body); + } + hir::StatementKind::Match { scrutinee, arms } => { + self.expression(scrutinee); + for arm in arms { + for binding in &arm.bindings { + self.check_local(*binding, arm.span, "nested match binding"); + } + self.hir_block(&arm.body); + } + } + hir::StatementKind::Break | hir::StatementKind::Continue => {} + hir::StatementKind::Assert { condition, message } => { + self.expression(condition); + if let Some(message) = message { + self.expression(message); + } + } + hir::StatementKind::Trace(local) => { + self.check_local(*local, statement.span, "nested trace statement"); + } + hir::StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + self.expression(condition); + } + for binding in bindings { + self.check_local(*binding, statement.span, "nested breakpoint binding"); + } + } + hir::StatementKind::Scope(block) => self.hir_block(block), + hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info); + let mut iteration_indexes = std::collections::HashSet::new(); + let mut bound_types = std::collections::HashSet::new(); + for arm in arms { + if !iteration_indexes.insert(arm.iteration_index) { + self.error( + statement.span, + "nested reflected type dispatch contains a duplicate iteration index", + ); + } + if !bound_types.insert(&arm.reflection_identity) { + self.error( + statement.span, + "nested reflected type dispatch contains a duplicate reflected type identity", + ); + } + self.hir_block(&arm.body); + } + if arms.is_empty() { + self.error(statement.span, "nested reflected type dispatch has no arms"); + } + } + } + } + + fn expression(&mut self, expression: &Expression) { + let ownership = match &expression.kind { + hir::ExpressionKind::Call { ownership, .. } + | hir::ExpressionKind::Intrinsic { ownership, .. } + | hir::ExpressionKind::IndirectCall { ownership, .. } + | hir::ExpressionKind::ResourceInvoke { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(ownership) = ownership { + ownership + .metadata_local_ids(|id| self.check_local(id, expression.span, "caller ownership")); + ownership.metadata_function_ids(|id| self.check_function(id, expression.span)); + } + match &expression.kind { + hir::ExpressionKind::ActorGraphImport(_) => self.error( + expression.span, + "actor graph import was not expanded at its activation", + ), + hir::ExpressionKind::ActorGraphStep { args, .. } => { + for arg in args { + self.expression(arg); + } + } + hir::ExpressionKind::Local(local) => { + self.check_local(*local, expression.span, "expression"); + } + hir::ExpressionKind::ResourceHookValue { hook } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "MIR Resource descriptor has no original manifest", + ); + } + } + hir::ExpressionKind::ResourceInvoke { + hook, + args, + evaluation_order, + .. + } => { + if !self.resource_manifest.contains_hook(hook) { + self.error( + expression.span, + "MIR Resource invocation has no original manifest", + ); + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for value in args { + self.expression(value); + } + } + hir::ExpressionKind::FunctionRef(function) => { + self.check_function(*function, expression.span); + } + hir::ExpressionKind::ClosureRef { function, captures } => { + self.check_function(*function, expression.span); + for capture in captures { + self.check_local(*capture, expression.span, "closure capture"); + } + } + hir::ExpressionKind::Binary { left, right, .. } => { + self.expression(left); + self.expression(right); + } + hir::ExpressionKind::Unary { value, .. } + | hir::ExpressionKind::ResultOk(value) + | hir::ExpressionKind::ResultFail(value) + | hir::ExpressionKind::OptionalSome(value) + | hir::ExpressionKind::Comptime { value, .. } + | hir::ExpressionKind::Declassify(value) + | hir::ExpressionKind::Coarsen(value) + | hir::ExpressionKind::RefinementValidated(value) + | hir::ExpressionKind::DisplayResult(value) + | hir::ExpressionKind::EquatableResult(value) + | hir::ExpressionKind::RuntimeFailureMessage(value) + | hir::ExpressionKind::InterfaceType(value) + | hir::ExpressionKind::Run(value) + | hir::ExpressionKind::Join(value) + | hir::ExpressionKind::Cancel(value) + | hir::ExpressionKind::View(value) + | hir::ExpressionKind::Clone(value) => self.expression(value), + hir::ExpressionKind::FunctionAdapter { value, function } => { + self.check_function(*function, expression.span); + self.expression(value); + } + hir::ExpressionKind::InterfaceCoerce { value, adapters } => { + for entry in adapters { + self.check_function(entry.function, expression.span); + } + self.expression(value); + } + hir::ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } => { + self.check_function(*function, expression.span); + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } + hir::ExpressionKind::Intrinsic { + intrinsic, + field_validation, + args, + evaluation_order, + .. + } => { + match ( + hir::is_reflected_field_intrinsic(*intrinsic), + field_validation, + ) { + (true, Some(hir::ReflectedFieldValidation::Read)) + | (true, Some(hir::ReflectedFieldValidation::ReadCallable(_))) + | (false, None) => {} + _ => self.error( + expression.span, + "reflected selector proof plans were not lowered", + ), + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } + hir::ExpressionKind::ActorSpawn { + args, + evaluation_order, + constructor, + .. + } => { + if let Some(constructor) = constructor { + self.check_function(*constructor, expression.span); + } + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } + hir::ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + } => { + self.expression(callee); + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } + hir::ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } + | hir::ExpressionKind::BitfieldConstruct { + fields, + evaluation_order, + .. + } + | hir::ExpressionKind::EnumConstruct { + payloads: fields, + evaluation_order, + .. + } => { + self.check_evaluation_order(evaluation_order, fields.len(), expression.span); + for field in fields { + self.expression(field); + } + } + hir::ExpressionKind::MachineConstruct { payloads, .. } => { + for payload in payloads { + self.expression(payload); + } + } + hir::ExpressionKind::MachineTransition { + source, payloads, .. + } => { + self.expression(source); + for payload in payloads { + self.expression(payload); + } + } + hir::ExpressionKind::ListConstruct { elements } => { + for element in elements { + self.expression(element); + } + } + hir::ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expression(&entry.key); + self.expression(&entry.value); + } + } + hir::ExpressionKind::Handle { + target, + error_local, + failure, + .. + } => { + self.expression(target); + if let Some(error_local) = error_local { + self.check_local(*error_local, expression.span, "handle error binding"); + } + self.hir_block(failure); + } + hir::ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.expression(value); + } + } + } + hir::ExpressionKind::StateIs { value, .. } => self.expression(value), + hir::ExpressionKind::InlineFunction { + params, + view_params, + local_floor, + body, + .. + } => { + if *local_floor as usize > self.function.locals.len() { + self.error( + expression.span, + "inline function local floor is out of range", + ); + } + for param in params { + self.check_local(*param, expression.span, "inline function parameter"); + } + for view_param in view_params { + self.check_local( + *view_param, + expression.span, + "inline function view parameter", + ); + if !params.contains(view_param) { + self.error( + expression.span, + "inline function view parameter is not a parameter", + ); + } + } + self.hir_block(body); + } + hir::ExpressionKind::ActorMessage { + actor, + args, + evaluation_order, + handler, + .. + } => { + self.check_function(*handler, expression.span); + self.expression(actor); + self.check_evaluation_order(evaluation_order, args.len(), expression.span); + for argument in args { + self.expression(argument); + } + } + hir::ExpressionKind::Field { base, .. } => self.expression(base), + hir::ExpressionKind::Int(_) + | hir::ExpressionKind::Constant { .. } + | hir::ExpressionKind::Float(_) + | hir::ExpressionKind::String(_) + | hir::ExpressionKind::Bool(_) + | hir::ExpressionKind::Nothing + | hir::ExpressionKind::PropertyCaseContext(_) + | hir::ExpressionKind::RuntimeFailure(_) + | hir::ExpressionKind::OptionalNone => {} + } } } -pub fn lower(program: &hir::Program) -> Result> { - if let Err(errors) = hir::validate(program) { +pub fn lower(program: &hir::Program, types: &TypeInterner) -> Result> { + hir::validate_actor_graph_execution(program, types).map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |f| f.span), + message, + }] + })?; + if let Err(errors) = + hir::validate(program).and_then(|()| hir::validate_backend_types(program, types)) + { return Err(errors .into_iter() .map(|error| LowerError { @@ -189,32 +1409,274 @@ pub fn lower(program: &hir::Program) -> Result> { }) .collect()); } - Ok(Program { - functions: program.functions.iter().map(lower_function).collect(), - }) + let resource_execution = + resource_ownership::authenticate_original(program, types).map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })?; + let function_param_modes = program + .functions + .iter() + .map(|function| { + ( + function.id, + function.params.iter().map(|param| param.mode).collect(), + ) + }) + .collect(); + let mut lowered = Program { + resource_manifest: program.resource_manifest.clone(), + equality_methods: program.equality_methods.clone(), + functions: program + .functions + .iter() + .map(|function| { + lower_function( + function, + types, + &function_param_modes, + &program.resource_manifest, + &program.resource_source, + &resource_execution, + ) + }) + .collect::, _>>() + .map_err(|error| vec![error])?, + }; + reflected_callable::capture( + &mut lowered, + program, + &function_param_modes, + &resource_execution, + ) + .map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })?; + temporary_local_views::capture(&mut lowered, program, types).map_err(|message| { + vec![LowerError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })?; + validate_call_ownership(&lowered, types).map_err(|errors| { + errors + .into_iter() + .map(|error| LowerError { + span: error.span, + message: error.message, + }) + .collect::>() + })?; + Ok(lowered) +} + +fn lower_function( + function: &hir::Function, + types: &TypeInterner, + function_param_modes: &std::collections::HashMap>, + resource_manifest: &hir::ResourceManifest, + resource_source: &hir::ResourceSourceArchive, + resource_execution: &resource_ownership::ResourceExecutionClosure, +) -> Result { + lower_function_body( + function, + types, + function_param_modes, + resource_manifest, + resource_source, + resource_execution, + ) } -fn lower_function(function: &hir::Function) -> Function { - let mut builder = Builder::new(function.body.span); +fn lower_function_body( + function: &hir::Function, + types: &TypeInterner, + function_param_modes: &std::collections::HashMap>, + resource_manifest: &hir::ResourceManifest, + resource_source: &hir::ResourceSourceArchive, + resource_execution: &resource_ownership::ResourceExecutionClosure, +) -> Result { + let mut builder = Builder::new(function.body.span, types, function_param_modes); + builder.locals = function.locals.clone(); + builder.ordinary_sum_capture = ordinary_borrowed_sums::Capture::authenticated(function, types) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + builder.resource_capture = resource_ownership::Capture::authenticated( + function, + resource_manifest, + resource_source, + types, + resource_execution, + ) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + builder.view_params = function + .params + .iter() + .filter(|param| param.mode == ParamMode::View) + .map(|param| param.local) + .collect(); + builder.view_params.extend( + function + .locals + .iter() + .filter(|local| local.view_source.is_some()) + .map(|local| local.id), + ); builder.lower_block(&function.body); - Function { + if let Some(error) = builder.resource_error.take() { + return Err(error); + } + if builder.open() && function.return_type == jett_types::TypeInterner::NOTHING { + builder.terminate(TerminatorKind::Return(None), function.body.span); + } + let breakpoint_capture = std::mem::take(&mut builder.breakpoint_capture); + let generation_capture = std::mem::take(&mut builder.generation_capture); + let resource_capture = std::mem::take(&mut builder.resource_capture); + let ordinary_sum_capture = std::mem::take(&mut builder.ordinary_sum_capture); + let match_capture = std::mem::take(&mut builder.match_capture); + let mut lowered = Function { + id: function.id, identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, return_type: function.return_type, + locals: builder.locals, entry: BlockId(0), blocks: builder.blocks, span: function.span, - } + prepared_absent_successes: Vec::new(), + prepared_present_successes: Vec::new(), + prepared_view_iterations: Vec::new(), + original_view_iterations: Vec::new(), + match_bindings: None, + breakpoint_regions: Vec::new(), + call_owner_generations: Vec::new(), + resource_lowering: None, + ordinary_borrowed_sums: None, + temporary_local_views: None, + reflected_callable: None, + actor_graph: None, + field_updates: None, + actor_handler: None, + actor_state_views: function.actor_state_views.clone(), + actor_message_source: hir::has_actor_messages(&function.body), + }; + lowered.breakpoint_regions = + breakpoint_capture + .seal(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.call_owner_generations = + generation_capture + .seal(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.resource_lowering = resource_capture + .finish(&lowered, types) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.ordinary_borrowed_sums = + ordinary_sum_capture + .finish(&lowered) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + iteration_views::capture_original(&mut lowered, types); + lowered.match_bindings = + match_capture + .finish(&lowered, types) + .map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.actor_graph = + actor_graph::seal(function, &lowered, types).map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.actor_handler = + actor_handlers::seal(function, &lowered).map_err(|message| LowerError { + span: function.span, + message, + })?; + lowered.field_updates = + field_updates::seal(function, &lowered, types).map_err(|message| LowerError { + span: function.span, + message, + })?; + Ok(lowered) } -struct Builder { +#[derive(Clone)] +struct Builder<'a> { + resource_error: Option, + types: &'a TypeInterner, + function_param_modes: &'a std::collections::HashMap>, blocks: Vec, current: BlockId, - loops: Vec<(BlockId, BlockId)>, + loops: Vec<(BlockId, BlockId, usize, usize)>, + lexical_scopes: Vec, + reflected_original_prefix: Option>, + call_view_scopes: Vec>, + field_update_scopes: Vec<(hir::FieldUpdatePlace, usize)>, + resource_call_scopes: Vec, + resource_constructor_scopes: Vec, + resource_operation_scopes: Vec, + locals: Vec, + handlers: Vec<(LocalId, BlockId)>, + view_params: Vec, + breakpoint_capture: breakpoint_regions::Capture, + generation_capture: call_owner_generations::Capture, + resource_capture: resource_ownership::Capture, + ordinary_sum_capture: ordinary_borrowed_sums::Capture, + match_capture: match_bindings::Capture, + reflected_produced_return: bool, } -impl Builder { - fn new(span: Span) -> Self { +impl<'a> Builder<'a> { + fn new( + span: Span, + types: &'a TypeInterner, + function_param_modes: &'a std::collections::HashMap>, + ) -> Self { Self { + types, + function_param_modes, + lexical_scopes: Vec::new(), + reflected_original_prefix: None, blocks: vec![BasicBlock { id: BlockId(0), statements: Vec::new(), @@ -225,6 +1687,21 @@ impl Builder { }], current: BlockId(0), loops: Vec::new(), + call_view_scopes: Vec::new(), + field_update_scopes: Vec::new(), + resource_call_scopes: Vec::new(), + resource_constructor_scopes: Vec::new(), + resource_operation_scopes: Vec::new(), + locals: Vec::new(), + handlers: Vec::new(), + view_params: Vec::new(), + resource_error: None, + breakpoint_capture: breakpoint_regions::Capture::default(), + generation_capture: call_owner_generations::Capture::default(), + resource_capture: resource_ownership::Capture::default(), + ordinary_sum_capture: ordinary_borrowed_sums::Capture::default(), + match_capture: match_bindings::Capture::default(), + reflected_produced_return: false, } } @@ -238,6 +1715,9 @@ impl Builder { span, }, }); + self.breakpoint_capture.block(id); + self.generation_capture.block(id); + self.resource_capture.block(id, span); id } @@ -249,13 +1729,37 @@ impl Builder { } fn terminate(&mut self, kind: TerminatorKind, span: Span) { - self.blocks[self.current.index() as usize].terminator = Terminator { kind, span }; + let value = Terminator { kind, span }; + self.breakpoint_capture.terminator(self.current, &value); + self.generation_capture.terminator(self.current, &value); + self.resource_capture.terminator(self.current, &value); + self.blocks[self.current.index() as usize].terminator = value; } fn push(&mut self, kind: StatementKind, span: Span) { + let index = self.blocks[self.current.index() as usize].statements.len(); + let value = Statement { kind, span }; + self.breakpoint_capture + .statement(self.current, index, &value); + self.generation_capture + .statement(self.current, index, &value); + self.resource_capture.statement(self.current, index, &value); + if let StatementKind::ResourceConstructor(node) = &value.kind { + if let Some(site) = self.resource_capture.site(self.current, index) { + self.resource_capture + .constructor_node(site, node, &self.locals); + } + } + if let StatementKind::ResourceCall(node) = &value.kind { + let site = self + .resource_capture + .site(self.current, index) + .expect("authenticated Resource node"); + self.resource_capture.normalized_node(site, node); + } self.blocks[self.current.index() as usize] .statements - .push(Statement { kind, span }); + .push(value); } fn close_to(&mut self, target: BlockId, span: Span) { @@ -265,45 +1769,346 @@ impl Builder { } fn lower_block(&mut self, block: &hir::Block) { + let lexical = match self + .resource_capture + .lexical_scope_under(block, self.reflected_original_prefix.as_deref()) + { + Ok(scope) => scope, + Err(message) => { + self.resource_error = Some(LowerError { + span: block.span, + message, + }); + None + } + }; + let entered = lexical.is_some(); + if let Some(scope) = lexical { + self.lexical_scopes.push(scope); + } for statement in &block.statements { if !self.open() { break; } self.lower_statement(statement); } + if entered { + if self.open() { + self.lower_lexical_exit( + ResourceLexicalExitKind::Fallthrough, + self.lexical_scopes.len() - 1, + None, + None, + block.span, + ); + } + self.lexical_scopes.pop(); + } + } + + fn lower_lexical_exit( + &mut self, + kind: ResourceLexicalExitKind, + floor: usize, + original: Option<&hir::Statement>, + target: Option, + span: Span, + ) { + let position = if kind == ResourceLexicalExitKind::Return { + ResourcePosition::Terminator + } else { + ResourcePosition::Statement(self.blocks[self.current.index() as usize].statements.len()) + }; + if let Some(id) = self.resource_capture.lexical_exit( + kind, + self.current, + position, + &self.lexical_scopes[floor..], + original, + target, + ) && kind != ResourceLexicalExitKind::Return + { + self.push(StatementKind::ResourceLexicalExit(id), span); + } } fn lower_statement(&mut self, statement: &hir::Statement) { match &statement.kind { - hir::StatementKind::Let { local, value } => self.push( - StatementKind::Let { - local: *local, - value: value.clone(), - }, - statement.span, - ), - hir::StatementKind::Assign { target, value } => self.push( - StatementKind::Assign { - target: target.clone(), - value: value.clone(), - }, - statement.span, - ), + hir::StatementKind::Let { local, value } => { + let seed = match self + .resource_capture + .borrowed_sum_seed(*local, value, self.types) + { + Ok(seed) => seed, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement.span, + message, + }); + None + } + }; + let ordinary_seed = match self.ordinary_sum_capture.seed(*local, value) { + Ok(seed) => seed, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement.span, + message, + }); + None + } + }; + let value = if let Some(seed) = ordinary_seed { + self.lower_ordinary_borrowed_sum_handle(seed) + } else if let Some(seed) = seed { + if let Some(scope) = self.lexical_scopes.last_mut() { + scope.active.push(*local); + } + self.lower_borrowed_sum_handle(seed) + } else if let Some(metadata) = self.locals.get(local.index() as usize) + && let Some(source) = metadata.view_source + && let Some(origin) = self + .locals + .get(source.index() as usize) + .filter(|origin| origin.id == source) + && hir::validate_local_view_initializer( + value, + source, + origin.ty, + metadata.ty, + self.types, + ) + .is_ok() + && (!resource_type_pending(self.types, metadata.ty) + || hir::borrowed_sum_view_initializer( + value, + source, + origin.ty, + metadata.ty, + self.types, + ) + .is_ok_and(|handle| handle.is_none())) + { + // Stable aliases contain only transparent reads. Taking an + // owned snapshot would replace their checked backing local. + value.clone() + } else { + self.lower_value(value) + }; + if let Some(header) = self.locals.get(local.index() as usize) { + match self.resource_capture.bind_owned_declaration( + statement, + &value, + header, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ) { + Ok(true) => { + if let Some(scope) = self.lexical_scopes.last_mut() { + scope.active.push(*local); + } + } + Ok(false) => {} + Err(message) => { + self.resource_error = Some(LowerError { + span: statement.span, + message, + }); + } + } + } + self.push( + StatementKind::Let { + local: *local, + value, + }, + statement.span, + ); + } + hir::StatementKind::UpdateField { + target, + value, + place, + } => { + self.push( + StatementKind::PrepareFieldUpdate { + place: place.clone(), + begin: true, + }, + statement.span, + ); + self.field_update_scopes + .push((place.clone(), self.loops.len())); + let value = self.lower_value(value); + self.field_update_scopes.pop(); + let staged = self.temporary(value.ty, value.span); + self.push( + StatementKind::Let { + local: staged, + value, + }, + statement.span, + ); + let value = Expression { + kind: hir::ExpressionKind::Local(staged), + ty: target.ty, + span: statement.span, + }; + self.push( + StatementKind::UpdateField { + target: target.clone(), + value, + place: place.clone(), + }, + statement.span, + ); + } + hir::StatementKind::Assign { target, value } => { + // Keep the current owner readable throughout the entire RHS. + let generation = match target.kind { + hir::ExpressionKind::Local(root) => self + .generation_capture + .protected(root) + .map(|generation| (generation, root)), + _ => None, + }; + let value = self.lower_value(value); + if let Some((generation, root)) = generation { + let rhs_owner = self.temporary(value.ty, value.span); + let acquired = ( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + self.push( + StatementKind::Let { + local: rhs_owner, + value, + }, + statement.span, + ); + let replacement = ( + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + self.generation_capture + .replacement(generation, statement, rhs_owner, acquired, replacement) + .expect("constructor-selected generation preserves its checked Assign"); + self.push( + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + }, + statement.span, + ); + } else { + self.push( + StatementKind::Assign { + target: target.clone(), + value, + }, + statement.span, + ); + } + } hir::StatementKind::Expression(value) => { - self.push(StatementKind::Evaluate(value.clone()), statement.span) + let value = self.lower_value(value); + self.push(StatementKind::Evaluate(value), statement.span); } hir::StatementKind::HandleDefault(value) => { - self.push(StatementKind::HandleDefault(value.clone()), statement.span) + let value = self.lower_value(value); + if let Some(&(local, continuation)) = self.handlers.last() { + self.push(StatementKind::Let { local, value }, statement.span); + self.terminate(TerminatorKind::Goto(continuation), statement.span); + } else { + self.push(StatementKind::HandleDefault(value), statement.span); + } } hir::StatementKind::Return(value) => { - self.terminate(TerminatorKind::Return(value.clone()), statement.span) + // This branch abandons surrounding calls before the return + // operand may move their owner. Other CFG branches retain them. + let (resource_scopes, constructor_scopes, operation_scopes) = + self.abandon_resource_calls(statement.span); + self.end_call_views_since(0, statement.span); + let abandoned = std::mem::take(&mut self.call_view_scopes); + let generations = self.generation_capture.suspend(); + let value = value.as_ref().map(|v| self.lower_value(v)); + self.call_view_scopes = abandoned; + self.generation_capture.restore(generations); + self.resource_call_scopes = resource_scopes; + self.resource_constructor_scopes = constructor_scopes; + self.resource_operation_scopes = operation_scopes; + self.lower_lexical_exit( + ResourceLexicalExitKind::Return, + 0, + Some(statement), + None, + statement.span, + ); + self.terminate(TerminatorKind::Return(value), statement.span); } hir::StatementKind::Break => { - let target = self.loops.last().expect("validated break has a loop").1; + let abandoned = self + .field_update_scopes + .iter() + .rev() + .filter(|(_, depth)| *depth >= self.loops.len()) + .map(|(place, _)| place.clone()) + .collect::>(); + for place in abandoned { + self.push( + StatementKind::PrepareFieldUpdate { + place, + begin: false, + }, + statement.span, + ); + } + if !self.resource_operation_scopes.is_empty() { + self.resource_error = Some(LowerError { span: statement.span, message: "pending Resource call region: Break needs its exact operation-depth exit proof".into() }); + } + let (_, target, depth, lexical_depth) = + *self.loops.last().expect("validated break has a loop"); + self.end_call_views_since(depth, statement.span); + self.lower_lexical_exit( + ResourceLexicalExitKind::Break, + lexical_depth, + Some(statement), + Some(target), + statement.span, + ); self.terminate(TerminatorKind::Goto(target), statement.span); } hir::StatementKind::Continue => { - let target = self.loops.last().expect("validated continue has a loop").0; + let abandoned = self + .field_update_scopes + .iter() + .rev() + .filter(|(_, depth)| *depth >= self.loops.len()) + .map(|(place, _)| place.clone()) + .collect::>(); + for place in abandoned { + self.push( + StatementKind::PrepareFieldUpdate { + place, + begin: false, + }, + statement.span, + ); + } + if !self.resource_operation_scopes.is_empty() { + self.resource_error = Some(LowerError { span: statement.span, message: "pending Resource call region: Continue needs its exact operation-depth exit proof".into() }); + } + let (target, _, depth, lexical_depth) = + *self.loops.last().expect("validated continue has a loop"); + self.end_call_views_since(depth, statement.span); + self.lower_lexical_exit( + ResourceLexicalExitKind::Continue, + lexical_depth, + Some(statement), + Some(target), + statement.span, + ); self.terminate(TerminatorKind::Goto(target), statement.span); } hir::StatementKind::If { @@ -324,23 +2129,66 @@ impl Builder { hir::StatementKind::Match { scrutinee, arms } => { self.lower_match(scrutinee, arms, statement.span) } - hir::StatementKind::Assert { condition, message } => self.push( - StatementKind::Assert { - condition: condition.clone(), - message: message.clone(), - }, - statement.span, - ), + hir::StatementKind::Assert { condition, message } => { + let condition = self.lower_value(condition); + self.push( + StatementKind::Assert { + condition, + message: message.clone(), + }, + statement.span, + ); + } hir::StatementKind::Trace(local) => { self.push(StatementKind::Trace(*local), statement.span) } - hir::StatementKind::Breakpoint(condition) => { - self.push(StatementKind::Breakpoint(condition.clone()), statement.span) + hir::StatementKind::Breakpoint { + condition, + bindings, + } => { + let capturing = condition.is_some(); + if capturing { + self.breakpoint_capture.begin( + statement, + self.current, + self.blocks[self.current.index() as usize].statements.len(), + ); + } + let condition = condition.as_ref().map(|value| self.lower_value(value)); + self.push( + StatementKind::Breakpoint { + condition, + bindings: bindings.clone(), + }, + statement.span, + ); + if capturing { + self.breakpoint_capture.end( + self.current, + self.blocks[self.current.index() as usize].statements.len() - 1, + ); + } } hir::StatementKind::Respond(value) => { - self.terminate(TerminatorKind::Respond(value.clone()), statement.span) + // Respond abandons the surrounding acquisition before evaluating + // its owned reply, while retaining its distinct actor exit kind. + let (resource_scopes, constructor_scopes, operation_scopes) = + self.abandon_resource_calls(statement.span); + self.end_call_views_since(0, statement.span); + let abandoned = std::mem::take(&mut self.call_view_scopes); + let generations = self.generation_capture.suspend(); + let value = self.lower_value(value); + self.call_view_scopes = abandoned; + self.generation_capture.restore(generations); + self.resource_call_scopes = resource_scopes; + self.resource_constructor_scopes = constructor_scopes; + self.resource_operation_scopes = operation_scopes; + self.terminate(TerminatorKind::Respond(value), statement.span) } hir::StatementKind::Scope(block) => self.lower_block(block), + hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { + self.lower_reflected_type_dispatch(type_info, arms, statement.span) + } } } @@ -354,6 +2202,7 @@ impl Builder { let then_block = self.new_block(then_body.span); let else_span = else_body.map_or(statement_span, |body| body.span); let else_block = self.new_block(else_span); + let condition = self.lower_value(condition); self.terminate( TerminatorKind::Branch { condition: condition.clone(), @@ -388,7 +2237,14 @@ impl Builder { let body_block = self.new_block(body.span); let exit = self.new_block(statement_span); self.terminate(TerminatorKind::Goto(condition_block), statement_span); + self.loops.push(( + condition_block, + exit, + self.call_view_scopes.len(), + self.lexical_scopes.len(), + )); self.current = condition_block; + let condition = self.lower_value(condition); self.terminate( TerminatorKind::Branch { condition: condition.clone(), @@ -397,7 +2253,6 @@ impl Builder { }, condition.span, ); - self.loops.push((condition_block, exit)); self.current = body_block; self.lower_block(body); self.close_to(condition_block, body.span); @@ -414,6 +2269,7 @@ impl Builder { body: &hir::Block, statement_span: Span, ) { + let iterable = self.lower_value(iterable); let header = self.new_block(iterable.span); let body_block = self.new_block(body.span); let exit = self.new_block(statement_span); @@ -428,9 +2284,14 @@ impl Builder { body: body_block, exit, }, - iterable.span, + statement_span, ); - self.loops.push((header, exit)); + self.loops.push(( + header, + exit, + self.call_view_scopes.len(), + self.lexical_scopes.len(), + )); self.current = body_block; self.lower_block(body); self.close_to(header, body.span); @@ -438,39 +2299,137 @@ impl Builder { self.current = exit; } - fn lower_match( + fn lower_match( + &mut self, + scrutinee: &Expression, + arms: &[hir::MatchArm], + statement_span: Span, + ) { + let arm_blocks = arms + .iter() + .map(|arm| self.new_block(arm.span)) + .collect::>(); + let mut variants = Vec::new(); + let mut otherwise = None; + for (arm, block) in arms.iter().zip(&arm_blocks) { + if let Some(variant) = arm.variant { + variants.push((variant, *block, arm.bindings.clone())); + } else { + otherwise = Some(*block); + } + } + let borrowed = + match match_bindings::requires_snapshot(self.types, scrutinee, &self.view_params) { + Ok(borrowed) => borrowed, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + return; + } + }; + let scrutinee = self.lower_value(scrutinee); + let scrutinee = if borrowed { + Expression { + kind: hir::ExpressionKind::Clone(Box::new(scrutinee.clone())), + ty: scrutinee.ty, + span: scrutinee.span, + } + } else { + scrutinee.clone() + }; + let match_header = self.current; + self.terminate( + TerminatorKind::Switch { + scrutinee: scrutinee.clone(), + variants, + otherwise, + }, + scrutinee.span, + ); + + let selector = self.blocks[match_header.index() as usize] + .terminator + .clone(); + let mut arm_regions = Vec::with_capacity(arms.len()); + let mut arm_exits = Vec::with_capacity(arms.len()); + for (arm, block) in arms.iter().zip(arm_blocks) { + let first_child = self.blocks.len(); + self.current = block; + self.lower_block(&arm.body); + let region = std::iter::once(block) + .chain(self.blocks[first_child..].iter().map(|block| block.id)) + .collect(); + arm_regions.push((block, region)); + arm_exits.push((self.current, self.open(), arm.span)); + } + if let Err(message) = self.match_capture.record( + match_header, + selector, + statement_span, + arm_regions, + &self.locals, + self.types, + ) { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + } + + if arm_exits.iter().any(|(_, falls_through, _)| *falls_through) { + let join = self.new_block(statement_span); + for (exit, _, span) in arm_exits { + self.current = exit; + self.close_to(join, span); + } + self.current = join; + } + } + + fn lower_reflected_type_dispatch( &mut self, - scrutinee: &Expression, - arms: &[hir::MatchArm], + type_info: &Expression, + arms: &[hir::ReflectedTypeArm], statement_span: Span, ) { + if let Some(proof) = self + .resource_capture + .reflected_field_dispatch(type_info, arms) + { + self.lower_reflected_field_dispatch(proof, statement_span); + return; + } let arm_blocks = arms .iter() - .map(|arm| self.new_block(arm.span)) + .map(|arm| self.new_block(arm.body.span)) .collect::>(); - let mut variants = Vec::new(); - let mut otherwise = None; - for (arm, block) in arms.iter().zip(&arm_blocks) { - if let Some(variant) = arm.variant { - variants.push((variant, *block, arm.bindings.clone())); - } else { - otherwise = Some(*block); - } - } + let otherwise = self.new_block(statement_span); + let dispatch_arms = arms + .iter() + .zip(&arm_blocks) + .map(|(arm, target)| ReflectedTypeDispatchArm { + iteration_index: arm.iteration_index, + bound_type: arm.bound_type, + reflection_identity: arm.reflection_identity.clone(), + target: *target, + }) + .collect(); self.terminate( - TerminatorKind::Switch { - scrutinee: scrutinee.clone(), - variants, + TerminatorKind::ReflectedTypeDispatch { + type_info: type_info.clone(), + arms: dispatch_arms, otherwise, }, - scrutinee.span, + type_info.span, ); let mut arm_exits = Vec::with_capacity(arms.len()); for (arm, block) in arms.iter().zip(arm_blocks) { self.current = block; self.lower_block(&arm.body); - arm_exits.push((self.current, self.open(), arm.span)); + arm_exits.push((self.current, self.open(), arm.body.span)); } if arm_exits.iter().any(|(_, falls_through, _)| *falls_through) { @@ -482,6 +2441,103 @@ impl Builder { self.current = join; } } + fn lower_reflected_field_dispatch( + &mut self, + proof: hir::ReflectedFieldDispatchProof, + statement_span: Span, + ) { + let Some(key) = self.locals.get(proof.key().index() as usize).cloned() else { + self.resource_error = Some(LowerError { + span: statement_span, + message: "Resource reflected binder lost its dense header".into(), + }); + return; + }; + let arms = proof.arms(); + let mut guards = vec![self.current]; + for _ in 1..arms.len() { + guards.push(self.new_block(statement_span)); + } + let dispatches = arms + .iter() + .map(|_| self.new_block(statement_span)) + .collect::>(); + let targets = arms + .iter() + .map(|arm| self.new_block(arm.body.span)) + .collect::>(); + let otherwise = self.new_block(statement_span); + let mut exits = Vec::new(); + for (index, arm) in arms.iter().enumerate() { + let condition = match resource_ownership::reflected_fields::ordinal_guard( + &proof, + proof.type_info(), + index, + ) { + Ok(condition) => condition, + Err(message) => { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + return; + } + }; + self.current = guards[index]; + self.terminate( + TerminatorKind::Branch { + condition, + then_block: dispatches[index], + else_block: guards.get(index + 1).copied().unwrap_or(otherwise), + }, + statement_span, + ); + self.current = dispatches[index]; + self.terminate( + TerminatorKind::ReflectedTypeDispatch { + type_info: proof.type_info().clone(), + arms: vec![ReflectedTypeDispatchArm { + iteration_index: index, + bound_type: arm.bound_type, + reflection_identity: arm.reflection_identity.clone(), + target: targets[index], + }], + otherwise, + }, + proof.type_info().span, + ); + self.current = targets[index]; + let prefix = proof.arm_container_path(index).map(<[_]>::to_vec); + if prefix.is_none() { + self.resource_error = Some(LowerError { + span: statement_span, + message: "Resource reflected body lost its exact original Scope path".into(), + }); + return; + } + let previous = std::mem::replace(&mut self.reflected_original_prefix, prefix); + self.lower_block(&arm.body); + self.reflected_original_prefix = previous; + exits.push((self.current, self.open(), arm.body.span)); + } + if exits.iter().any(|(_, open, _)| *open) { + let join = self.new_block(statement_span); + for (exit, _, span) in exits { + self.current = exit; + self.close_to(join, span); + } + self.current = join; + } + if let Err(message) = self + .resource_capture + .capture_reflected_field_dispatch(proof, key, guards, dispatches, targets, otherwise) + { + self.resource_error = Some(LowerError { + span: statement_span, + message, + }); + } + } } #[cfg(test)] @@ -492,6 +2548,46 @@ mod tests { use super::*; + const REFLECTED_DISPATCH_SOURCE: &str = r#"namespace app +struct User: + name: string + age: int64 +function reflected_names[T](view value: T) returns string: + mutable string output = "" + for field in type.fields[T](): + comptime type Field = field.type_info: + output = type.name[Field]() + return output +function main() returns string: + User user = User(name: "Ada", age: 37) + return reflected_names[User](view user) +"#; + + const REFLECTED_CONTROL_FLOW_SOURCE: &str = r#"namespace app +struct User: + name: string + age: int64 +function break_after_first[T]() returns string: + for field in type.fields[T](): + comptime type Field = field.type_info: + break + return "break" +function continue_all[T]() returns string: + for field in type.fields[T](): + comptime type Field = field.type_info: + continue + return "continue" +function return_first[T]() returns string: + for field in type.fields[T](): + comptime type Field = field.type_info: + return type.name[Field]() + return "empty" +function main() returns string: + string broken = break_after_first[User]() + string continued = continue_all[User]() + return return_first[User]() +"#; + fn lower_source(source: &str) -> Program { let file = FileId::new(0); let parsed = jett_parser::parse(source, file); @@ -517,7 +2613,7 @@ mod tests { &HashMap::from([(file, SourceOrigin::Project)]), ) .expect("HIR lowering"); - lower(&hir).expect("MIR lowering") + lower(&hir, &checked.interner).expect("MIR lowering") } fn reachable_blocks(function: &Function) -> HashSet { @@ -550,6 +2646,12 @@ mod tests { pending.push(*body); pending.push(*exit); } + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + pending.extend(arms.iter().map(|arm| arm.target)); + pending.push(*otherwise); + } TerminatorKind::Return(_) | TerminatorKind::Respond(_) | TerminatorKind::Unreachable => {} @@ -558,6 +2660,488 @@ mod tests { reachable } + fn returned_expression(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("expected a returned expression") + } + + #[test] + fn preserves_function_ids_parameters_locals_and_call_order() { + let signature = lower_source( + r#"namespace app +function combine(view left: int64, right: int64) returns int64: + int64 total = left + right + return total +"#, + ); + let function = &signature.functions[0]; + + assert_eq!(function.id.index(), 0); + assert_eq!(function.params.len(), 2); + assert_eq!(function.locals.len(), 3); + assert_eq!(function.params[0].mode, ParamMode::View); + assert_eq!(function.params[1].mode, ParamMode::Owned); + for parameter in &function.params { + assert_eq!( + function.parameter_for_local(parameter.local), + Some(parameter) + ); + let local = function.local(parameter.local).expect("parameter local"); + assert_eq!(local.name, parameter.name); + assert_eq!(local.ty, parameter.ty); + assert_eq!(local.mutable, parameter.mutable); + assert_eq!(local.span, parameter.span); + } + validate(&signature).expect("preserved signature must validate"); + + let mut calls = lower_source( + r#"namespace app +function add(first: int64, second: int64) returns int64: + return first + second +function call_add() returns int64: + return add(second: 2, first: 1) +"#, + ); + let callee_id = calls.functions[0].id; + let value = returned_expression(&mut calls.functions[1]); + let hir::ExpressionKind::Call { + function, + evaluation_order, + .. + } = &value.kind + else { + panic!("expected direct call"); + }; + assert_eq!(*function, callee_id); + assert_eq!(evaluation_order, &[1, 0]); + validate(&calls).expect("valid call order must validate"); + } + + #[test] + fn validation_rejects_noncanonical_and_duplicate_function_ids() { + let mut program = lower_source( + r#"namespace app +function first() returns int64: + return 1 +function second() returns int64: + return 2 +"#, + ); + program.functions[1].id = program.functions[0].id; + + let errors = validate(&program).expect_err("duplicate function ID must be rejected"); + let messages = errors + .iter() + .map(|error| error.message.as_str()) + .collect::>(); + assert!(messages.contains(&"function at index 1 has noncanonical ID 0")); + assert!(messages.contains(&"multiple functions use ID 0")); + } + + #[test] + fn validation_rejects_a_direct_call_outside_the_function_table() { + let mut program = lower_source( + r#"namespace app +function callee() returns int64: + return 1 +function caller() returns int64: + return callee() +function spare() returns int64: + return 2 +"#, + ); + let removed_id = program.functions[2].id; + let value = returned_expression(&mut program.functions[1]); + let hir::ExpressionKind::Call { function, .. } = &mut value.kind else { + panic!("expected direct call"); + }; + *function = removed_id; + program.functions.pop(); + + let errors = validate(&program).expect_err("unknown direct call must be rejected"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "direct call references function 2 outside function table of length 2" + ); + } + + #[test] + fn validation_rejects_a_non_permutation_evaluation_order() { + let mut program = lower_source( + r#"namespace app +function add(first: int64, second: int64) returns int64: + return first + second +function caller() returns int64: + return add(1, 2) +"#, + ); + let value = returned_expression(&mut program.functions[1]); + let hir::ExpressionKind::Call { + evaluation_order, .. + } = &mut value.kind + else { + panic!("expected direct call"); + }; + *evaluation_order = vec![0, 0]; + + let errors = validate(&program).expect_err("invalid order must be rejected"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "evaluation order must be a permutation of the operand indexes" + ); + } + + #[test] + fn enum_payload_handlers_preserve_named_argument_evaluation_order() { + let mut program = lower_source( + r#"namespace app +enum PairValue: + pair(left: int64, right: int64) +function make(mutable seed: int64, missing: optional[int64]) returns PairValue: + return PairValue.pair(right: seed, left: missing handle: + seed = 9 + default 5 + ) +"#, + ); + let returned = returned_expression(&mut program.functions[0]); + let hir::ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + } = &returned.kind + else { + panic!("expected enum construction after its payload handler"); + }; + assert_eq!(evaluation_order, &[1, 0]); + let hir::ExpressionKind::Local(right) = payloads[1].kind else { + panic!("expected saved right payload"); + }; + let function = &program.functions[0]; + assert!( + matches!( + &function.blocks[function.entry.index() as usize].statements[0].kind, + StatementKind::Let { + local, + value: Expression { kind: hir::ExpressionKind::Local(source), .. }, + } if *local == right && *source == function.params[0].local + ), + "the right payload must be saved before extracting the left payload's handler" + ); + validate(&program).expect("lowered enum handler order must validate"); + } + + #[test] + fn validation_rejects_an_enum_payload_order_that_is_not_a_permutation() { + let mut program = lower_source( + r#"namespace app +enum PairValue: + pair(left: int64, right: int64) +function make() returns PairValue: + return PairValue.pair(right: 4, left: 9) +"#, + ); + let expression = returned_expression(&mut program.functions[0]); + let hir::ExpressionKind::EnumConstruct { + evaluation_order, .. + } = &mut expression.kind + else { + panic!("expected enum construction"); + }; + assert_eq!(evaluation_order, &[1, 0]); + *evaluation_order = vec![1, 1]; + let errors = validate(&program).expect_err("invalid enum payload order must fail"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "evaluation order must be a permutation of the operand indexes" + ); + } + + #[test] + fn validation_rejects_duplicate_and_mismatched_parameter_metadata() { + let program = lower_source( + r#"namespace app +function first(left: int64, right: int64) returns int64: + return left +"#, + ); + + let mut duplicate = program.clone(); + duplicate.functions[0].params[1].local = duplicate.functions[0].params[0].local; + let errors = validate(&duplicate).expect_err("duplicate parameter local must be rejected"); + let messages = errors + .iter() + .map(|error| error.message.as_str()) + .collect::>(); + assert!(messages.contains(&"multiple parameters reference local 0")); + assert!(messages.contains(&"parameter metadata does not match local 0")); + + let mut mismatch = program; + mismatch.functions[0].params[0].name = "renamed".to_string(); + let errors = validate(&mismatch).expect_err("mismatched metadata must be rejected"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "parameter metadata does not match local 0" + ); + } + + #[test] + fn validation_rejects_noncanonical_and_missing_parameter_locals() { + let program = lower_source( + r#"namespace app +function first(left: int64, right: int64) returns int64: + return left +"#, + ); + + let mut noncanonical = program.clone(); + noncanonical.functions[0].locals[1].id = noncanonical.functions[0].locals[0].id; + let errors = validate(&noncanonical).expect_err("noncanonical local ID must be rejected"); + assert!( + errors + .iter() + .any(|error| error.message == "local at index 1 has noncanonical ID 0") + ); + + let mut missing = program; + missing.functions[0].locals.pop(); + let errors = validate(&missing).expect_err("missing parameter local must be rejected"); + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "parameter references local 1 outside function local table of length 1" + ); + } + + #[test] + fn lowers_reflected_type_dispatch_to_distinct_arm_blocks() { + let program = lower_source(REFLECTED_DISPATCH_SOURCE); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "reflected_names") + .expect("concrete reflected function should lower"); + let (dispatch_block, type_info, arms, otherwise) = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::ReflectedTypeDispatch { + type_info, + arms, + otherwise, + } => Some((block.id, type_info, arms, *otherwise)), + _ => None, + }) + .expect("expected reflected type dispatch terminator"); + + assert!(matches!(type_info.kind, hir::ExpressionKind::Field { .. })); + assert_eq!(arms.len(), 2); + assert_eq!( + arms.iter() + .map(|arm| arm.iteration_index) + .collect::>(), + [0, 1] + ); + assert_ne!(arms[0].bound_type, arms[1].bound_type); + assert_ne!(arms[0].target, arms[1].target); + assert!(arms.iter().all(|arm| arm.target != otherwise)); + assert!(matches!( + function.blocks[otherwise.index() as usize].terminator.kind, + TerminatorKind::Unreachable + )); + + let mut join = None; + for arm in arms { + let block = &function.blocks[arm.target.index() as usize]; + let [ + Statement { + kind: StatementKind::Assign { value, .. }, + .. + }, + ] = block.statements.as_slice() + else { + panic!("each reflected arm should contain its specialized assignment"); + }; + let hir::ExpressionKind::Intrinsic { type_arguments, .. } = &value.kind else { + panic!("expected specialized reflection intrinsic"); + }; + assert_eq!(type_arguments, &[arm.bound_type]); + let TerminatorKind::Goto(target) = block.terminator.kind else { + panic!("fallthrough arm should jump to the shared join"); + }; + assert_eq!(*join.get_or_insert(target), target); + } + + validate(&program).expect("lowered reflected dispatch must validate"); + let cfg = ControlFlowGraph::analyze(function).expect("reflected dispatch CFG"); + assert_eq!( + cfg.successors(dispatch_block), + &[arms[0].target, arms[1].target, otherwise] + ); + } + + #[test] + fn validation_rejects_invalid_reflected_dispatch_selector_and_targets() { + let mut program = lower_source(REFLECTED_DISPATCH_SOURCE); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "reflected_names") + .expect("concrete reflected function should lower"); + let TerminatorKind::ReflectedTypeDispatch { + type_info, + arms, + otherwise, + } = &mut function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::ReflectedTypeDispatch { .. } + ) + }) + .expect("expected reflected dispatch") + .terminator + .kind + else { + unreachable!(); + }; + let bound_type = arms[0].bound_type; + type_info.kind = hir::ExpressionKind::Intrinsic { + intrinsic: hir::IntrinsicId::TypeName, + type_arguments: vec![bound_type], + reflection_arguments: Vec::new(), + refinement_predicates: Vec::new(), + field_validation: None, + args: Vec::new(), + evaluation_order: vec![0], + ownership: hir::CallOwnership::generated( + hir::GeneratedOperation::ReflectionMetadata { + intrinsic: hir::IntrinsicId::TypeName, + }, + &[], + &[], + &[], + TypeInterner::STRING, + &[], + &TypeInterner::new(), + ) + .expect("closed metadata source"), + }; + arms[0].target = BlockId(u32::MAX); + *otherwise = BlockId(u32::MAX - 1); + + let errors = validate(&program).expect_err("invalid reflected dispatch must be rejected"); + let messages = errors + .iter() + .map(|error| error.message.as_str()) + .collect::>(); + assert!( + messages.contains(&"evaluation order must be a permutation of the operand indexes") + ); + assert!( + messages.contains(&"reflected type dispatch arm target 4294967295 is out of range") + ); + assert!( + messages + .contains(&"reflected type dispatch otherwise target 4294967294 is out of range") + ); + } + + #[test] + fn validation_rejects_duplicate_reflected_dispatch_arm_metadata() { + let mut program = lower_source(REFLECTED_DISPATCH_SOURCE); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "reflected_names") + .expect("concrete reflected function should lower"); + let TerminatorKind::ReflectedTypeDispatch { arms, .. } = &mut function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::ReflectedTypeDispatch { .. } + ) + }) + .expect("expected reflected dispatch") + .terminator + .kind + else { + unreachable!(); + }; + arms[1].iteration_index = arms[0].iteration_index; + arms[1].bound_type = arms[0].bound_type; + arms[1].reflection_identity = arms[0].reflection_identity.clone(); + + let errors = validate(&program).expect_err("duplicate arm metadata must be rejected"); + let messages = errors + .iter() + .map(|error| error.message.as_str()) + .collect::>(); + assert!(messages.contains(&"reflected type dispatch contains a duplicate iteration index")); + assert!( + messages + .contains(&"reflected type dispatch contains a duplicate reflected type identity") + ); + } + + #[test] + fn reflected_dispatch_preserves_return_break_and_continue_edges() { + let program = lower_source(REFLECTED_CONTROL_FLOW_SOURCE); + for (name, expected) in [ + ("break_after_first", "break"), + ("continue_all", "continue"), + ("return_first", "return"), + ] { + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap_or_else(|| panic!("concrete {name} function should lower")); + let (header, exit) = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + TerminatorKind::ForEach { exit, .. } => Some((block.id, exit)), + _ => None, + }) + .expect("expected reflected field loop"); + let arms = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::ReflectedTypeDispatch { arms, .. } => Some(arms), + _ => None, + }) + .expect("expected reflected dispatch"); + + assert_eq!(arms.len(), 2); + for arm in arms { + let terminator = &function.blocks[arm.target.index() as usize].terminator.kind; + match expected { + "break" => assert_eq!(terminator, &TerminatorKind::Goto(exit)), + "continue" => assert_eq!(terminator, &TerminatorKind::Goto(header)), + "return" => assert!(matches!(terminator, TerminatorKind::Return(Some(_)))), + _ => unreachable!(), + } + } + } + validate(&program).expect("reflected control-flow edges must validate"); + } + #[test] fn lowers_structured_control_flow_to_basic_blocks() { let program = lower_source( @@ -850,7 +3434,13 @@ actor Counter: respond 2 "#, ); - let handler = &program.functions[0]; + let handler = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == jett_hir::DeclarationKind::ActorHandler + }) + .expect("actor handler MIR"); assert_eq!(handler.blocks.len(), 3, "responding branches need no join"); let cfg = ControlFlowGraph::analyze(handler).expect("valid handler CFG"); for target in cfg.successors(handler.entry) { diff --git a/crates/jett_mir/src/match_bindings.rs b/crates/jett_mir/src/match_bindings.rs new file mode 100644 index 00000000..7b1c92bc --- /dev/null +++ b/crates/jett_mir/src/match_bindings.rs @@ -0,0 +1,516 @@ +//! Exact HIR match payload associations, scoped to their original arm CFG. +use super::*; +use std::collections::BTreeSet; + +// These wrappers change checked qualification, not the backing carrier. +// Peel only the actual refinement chain or explicit secret layer relation. +pub(super) fn requires_snapshot( + types: &TypeInterner, + mut expression: &Expression, + view_locals: &[LocalId], +) -> Result { + loop { + if expression.ty.index() as usize >= types.len() { + return Err("match scrutinee has an invalid checked carrier type".into()); + } + expression = match &expression.kind { + hir::ExpressionKind::View(value) if value.ty == expression.ty => return Ok(true), + hir::ExpressionKind::View(_) => { + return Err("match view changed its exact carrier type".into()); + } + hir::ExpressionKind::Field { .. } => return Ok(true), + hir::ExpressionKind::Local(local) => return Ok(view_locals.contains(local)), + hir::ExpressionKind::Coarsen(value) => { + let mut ty = value.ty; + let mut seen = BTreeSet::new(); + let mut matched = false; + while (ty.index() as usize) < types.len() && seen.insert(ty.index()) { + let jett_types::Type::Refinement { base, .. } = types.resolve(ty) else { + break; + }; + if *base == expression.ty { + matched = true; + break; + } + ty = *base; + } + if !matched { + return Err( + "match coarsen changed its exact refinement carrier relation".into(), + ); + } + value + } + hir::ExpressionKind::Declassify(value) => { + let mut ty = value.ty; + let mut seen = BTreeSet::new(); + while (ty.index() as usize) < types.len() && seen.insert(ty.index()) { + match types.resolve(ty) { + jett_types::Type::Refinement { base, .. } => ty = *base, + _ => break, + } + } + if ty.index() as usize >= types.len() + || !matches!(types.resolve(ty), jett_types::Type::Secret(inner) if *inner == expression.ty) + { + return Err("match declassify changed its exact secret carrier relation".into()); + } + value + } + _ => return Ok(false), + }; + } +} + +#[derive(Debug, Clone, PartialEq)] +struct Arm { + entry: BlockId, + region: BTreeSet, + bindings: Vec<(Local, hir::MatchBinding)>, +} + +#[derive(Debug, Clone, PartialEq)] +struct Match { + header: BlockId, + selector: Terminator, + arms: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Witness { + function: FunctionId, + identity: FunctionIdentity, + matches: Vec, +} + +#[derive(Clone, Default)] +pub(super) struct Capture { + matches: Vec, +} + +impl Capture { + // Called only by actual lowering of a HIR Match after the entry validator. + pub(super) fn record( + &mut self, + header: BlockId, + selector: Terminator, + match_span: Span, + regions: Vec<(BlockId, BTreeSet)>, + locals: &[Local], + types: &TypeInterner, + ) -> Result<(), String> { + let TerminatorKind::Switch { + scrutinee, + variants, + .. + } = &selector.kind + else { + return Err("call ownership match capture is not its lowered Switch".into()); + }; + let mut arms = Vec::new(); + for (variant, entry, ids) in variants { + let region = regions + .iter() + .find(|(block, _)| block == entry) + .ok_or("call ownership match capture lost its exact arm region")? + .1 + .clone(); + let mut bindings = Vec::new(); + for (field_index, id) in ids.iter().enumerate() { + let local = locals + .get(id.index() as usize) + .filter(|local| local.id == *id) + .ok_or("call ownership match capture lost its binder header")?; + if jett_typecheck::ownership::is_implicitly_copyable(types, local.ty) { + continue; + } + let proof = hir::checked_match_binding( + types, + match_span, + scrutinee.ty, + *variant, + field_index, + local.ty, + local.span, + )?; + bindings.push((local.clone(), proof)); + } + if !bindings.is_empty() { + arms.push(Arm { + entry: *entry, + region, + bindings, + }); + } + } + if !arms.is_empty() { + self.matches.push(Match { + header, + selector, + arms, + }); + } + Ok(()) + } + + pub(super) fn finish( + self, + function: &Function, + types: &TypeInterner, + ) -> Result, String> { + if self.matches.is_empty() { + return Ok(None); + } + let witness = Witness { + function: function.id, + identity: function.identity.clone(), + matches: self.matches, + }; + scopes_with(function, types, Some(&witness))?; + Ok(Some(witness)) + } +} + +pub(super) struct Scope { + region: BTreeSet, + bindings: Vec<(LocalId, hir::MatchBinding)>, +} + +pub(super) fn has_records(function: &Function) -> bool { + function.match_bindings.is_some() +} + +pub(super) fn apply(scopes: &[Scope], block: BlockId, locals: &mut [hir::OwnershipLocalInfo]) { + for local in locals.iter_mut() { + local.match_binding = None; + } + for scope in scopes.iter().filter(|scope| scope.region.contains(&block)) { + for &(local, proof) in &scope.bindings { + locals[local.index() as usize].match_binding = Some(proof); + } + } +} + +pub(super) fn ensure_scope( + function: &Function, + scopes: &[Scope], + mut local: LocalId, + block: BlockId, +) -> Result<(), String> { + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(local.index()) { + return Err("call ownership match alias backing is cyclic".into()); + } + for scope in scopes { + if scope.bindings.iter().any(|(binder, _)| *binder == local) + && !scope.region.contains(&block) + { + return Err("call ownership match binding is used outside its exact arm".into()); + } + } + let Some(source) = function.local(local).and_then(|local| local.view_source) else { + return Ok(()); + }; + local = source; + } +} + +pub(super) fn scopes(function: &Function, types: &TypeInterner) -> Result, String> { + scopes_with(function, types, function.match_bindings.as_ref()) +} + +fn scopes_with( + function: &Function, + types: &TypeInterner, + witness: Option<&Witness>, +) -> Result, String> { + let Some(witness) = witness else { + return Ok(Vec::new()); + }; + if witness.function != function.id || witness.identity != function.identity { + return Err("call ownership match proof belongs to another function".into()); + } + let cfg = + ControlFlowGraph::analyze(function).map_err(|_| "call ownership match CFG is invalid")?; + let mut claimed = BTreeSet::new(); + let mut result = Vec::new(); + for original in &witness.matches { + let block = function + .blocks + .get(original.header.index() as usize) + .filter(|block| block.id == original.header) + .ok_or("call ownership match selector is absent")?; + if !crate::breakpoint_regions::terminator_equal(&block.terminator, &original.selector) { + return Err("call ownership match changed its exact selecting Switch".into()); + } + let TerminatorKind::Switch { + scrutinee, + variants, + .. + } = &block.terminator.kind + else { + return Err("call ownership match lost its selecting Switch".into()); + }; + for arm in &original.arms { + if !arm.region.contains(&arm.entry) + || arm.region.contains(&original.header) + || arm.region.contains(&function.entry) + { + return Err("call ownership match changed its finite arm boundary".into()); + } + for &id in &arm.region { + if function + .blocks + .get(id.index() as usize) + .is_none_or(|block| block.id != id) + || cfg.predecessors(id).iter().any(|source| { + !arm.region.contains(source) + && !(*source == original.header && id == arm.entry) + }) + { + return Err("call ownership match arm has an external entry".into()); + } + } + let mut bindings = Vec::new(); + for (saved, proof) in &arm.bindings { + let current = function + .local(saved.id) + .ok_or("call ownership match binder is absent")?; + if current != saved + || current.mutable + || current.view_source.is_some() + || function.parameter_for_local(current.id).is_some() + || crate::iteration_views::definitions(function, current.id) != 1 + || !claimed.insert(current.id.index()) + { + return Err( + "call ownership match binder changed its declaration or defining edge" + .into(), + ); + } + let actual = hir::checked_match_binding( + types, + proof.match_span(), + scrutinee.ty, + proof.variant(), + proof.field_index(), + current.ty, + current.span, + )?; + if actual != *proof + || !variants.iter().any(|(variant, entry, fields)| { + *variant == proof.variant() + && *entry == arm.entry + && fields.get(proof.field_index()) == Some(¤t.id) + }) + { + return Err( + "call ownership match changed its exact enum/tag/field association".into(), + ); + } + bindings.push((current.id, *proof)); + } + result.push(Scope { + region: arm.region.clone(), + bindings, + }); + } + } + Ok(result) +} + +pub(super) fn remap_blocks(function: &mut Function, map: &[Option]) -> Result<(), String> { + let Some(mut witness) = function.match_bindings.clone() else { + return Ok(()); + }; + let get = |id: BlockId| map.get(id.index() as usize).copied().flatten(); + let mut matches = Vec::new(); + for mut record in witness.matches { + let Some(header) = get(record.header) else { + if record + .arms + .iter() + .any(|arm| arm.region.iter().any(|id| get(*id).is_some())) + { + return Err("call ownership match selector removal left an arm fragment".into()); + } + continue; + }; + record.header = header; + let missing = std::cell::Cell::new(false); + crate::sequences::prune::block_targets(&mut record.selector.kind, &|id| match get(*id) { + Some(new) => *id = new, + None => missing.set(true), + }); + if missing.get() { + return Err("call ownership match remap removed a selected arm".into()); + } + for arm in &mut record.arms { + arm.entry = get(arm.entry).ok_or("call ownership match remap lost its binder entry")?; + arm.region = arm.region.iter().filter_map(|id| get(*id)).collect(); + } + matches.push(record); + } + witness.matches = matches; + function.match_bindings = Some(witness); + Ok(()) +} + +pub(super) fn remap_locals(function: &mut Function, map: &[Option]) -> Result<(), String> { + let Some(mut witness) = function.match_bindings.clone() else { + return Ok(()); + }; + let get = |id: LocalId| map.get(id.index() as usize).copied().flatten(); + for record in &mut witness.matches { + let mut block = BasicBlock { + id: record.header, + statements: Vec::new(), + terminator: record.selector.clone(), + }; + let missing = std::cell::Cell::new(false); + crate::sequences::prune::block_locals( + &mut block, + &mut |id| match get(*id) { + Some(new) => *id = new, + None => missing.set(true), + }, + &mut |_| {}, + ); + if missing.get() { + return Err("call ownership match selector lost a retained local".into()); + } + record.selector = block.terminator; + for arm in &mut record.arms { + for (local, _) in &mut arm.bindings { + local.id = + get(local.id).ok_or("call ownership match binder lost its local remap")?; + } + } + } + function.match_bindings = Some(witness); + Ok(()) +} + +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let Some(mut witness) = before.match_bindings.clone() else { + return Ok(()); + }; + scopes(before, types)?; + let mut shadow = function.clone(); + shadow.breakpoint_regions = before.breakpoint_regions.clone(); + crate::breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + for record in &mut witness.matches { + // Sequence preparation never stages a new scrutinee: lower_match + // already lowered calls/handlers before Capture::record. Its only + // possible selector edit is an authenticated edge-cleanup split. + for (source, old, new) in &edit.redirects { + if *source != record.header { + continue; + } + if !crate::breakpoint_regions::terminator_equal(&record.selector, old) { + return Err( + "call ownership match redirect lost its exact selecting before-edge".into(), + ); + } + let TerminatorKind::Switch { + variants: old_variants, + otherwise: old_otherwise, + .. + } = &old.kind + else { + return Err("call ownership match redirect changed its selector kind".into()); + }; + let TerminatorKind::Switch { + variants: new_variants, + otherwise: new_otherwise, + .. + } = &new.kind + else { + return Err("call ownership match redirect is no longer a Switch".into()); + }; + if old_variants.len() != new_variants.len() + || old_otherwise.is_some() != new_otherwise.is_some() + || old_variants + .iter() + .zip(new_variants) + .any(|(old, new)| old.0 != new.0 || old.2 != new.2) + { + return Err("call ownership match redirect changed tag or payload binders".into()); + } + let mut expected = old.clone(); + let mut position = 0; + let targets = new_variants + .iter() + .map(|(_, target, _)| *target) + .chain(new_otherwise.iter().copied()) + .collect::>(); + if let TerminatorKind::Switch { + variants, + otherwise, + .. + } = &mut expected.kind + { + for (_, target, _) in variants { + *target = targets[position]; + position += 1; + } + if let Some(target) = otherwise { + *target = targets[position]; + } + } + if !crate::breakpoint_regions::terminator_equal(&expected, new) { + return Err( + "call ownership match redirect changed its scrutinee or Source span".into(), + ); + } + for arm in &mut record.arms { + if let Some((old, new)) = old_variants + .iter() + .zip(new_variants) + .find(|(old, _)| old.1 == arm.entry) + { + if old.1 != new.1 { + if !edit.blocks.iter().any(|block| block.id == new.1 + && matches!(block.terminator.kind, TerminatorKind::Goto(target) if target == old.1)) { + return Err("call ownership match redirect lacks its exact cleanup edge block".into()); + } + arm.entry = new.1; + arm.region.insert(new.1); + } + } + } + record.selector = new.clone(); + } + for arm in &mut record.arms { + for block in &edit.blocks { + if edit.redirects.iter().any(|(source, _, term)| { + if !arm.region.contains(source) { + return false; + } + let found = std::cell::Cell::new(false); + let mut kind = term.kind.clone(); + crate::sequences::prune::block_targets(&mut kind, &|target| { + if *target == block.id { + found.set(true); + } + }); + found.get() + }) { + arm.region.insert(block.id); + } + } + } + } + scopes_with(function, types, Some(&witness))?; + function.match_bindings = Some(witness); + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/match_bindings/tests.rs b/crates/jett_mir/src/match_bindings/tests.rs new file mode 100644 index 00000000..783a7ec0 --- /dev/null +++ b/crates/jett_mir/src/match_bindings/tests.rs @@ -0,0 +1,346 @@ +use super::*; +use std::collections::HashMap; + +const SOURCE: &str = r#"namespace app +struct Leaf: + code: int64 +struct Item: + leaf: Leaf +enum Choice: + first(item: Item) + second(item: Item) +function read_leaf(view leaf: Leaf) returns int64: + return leaf.code +function read(view item: Item) returns int64: + return read_leaf(view item.leaf) +function take(item: Item) returns int64: + return item.leaf.code +function identity(choice: Choice) returns Choice: + return choice +function exercise(view choice: Choice, view steps: list[int64]) returns int64: + mutable int64 total = 0 + match view choice: + first(item): + Item alias = view item + total = read(view item) + read_leaf(view item.leaf) + for step in view steps: + total = total + read(view alias) + step + second(item): + total = read(view item) + return total +function called(view choice: Choice, view steps: list[int64]) returns int64: + mutable int64 total = 0 + for step in view steps: + match identity(clone choice): + first(item): + total = total + read(view item) + step + second(item): + total = total + read_leaf(view item.leaf) + step + return total +function owned(choice: Choice) returns int64: + match choice: + first(item): + return take(item) + second(item): + return take(item) +"#; + +fn source_program() -> (Program, TypeInterner) { + let file = jett_common::FileId::new(0); + let parsed = jett_parser::parse(SOURCE, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, jett_common::SourceOrigin::Project)]), + ) + .expect("exact checked HIR"); + ( + crate::lower(&high, &checked.interner).expect("exact original match MIR"), + checked.interner, + ) +} + +fn exercise(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} +fn exercise_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| f.identity.declaration.name == "exercise") + .unwrap() +} + +#[test] +fn match_binding_transport_preserves_owned_modes_and_native_preparation() { + let (mut program, types) = source_program(); + crate::validate_call_ownership(&program, &types) + .expect("direct, projected, alias and owned binders"); + let function = exercise(&program); + let witness = function.match_bindings.as_ref().unwrap(); + let arm = &witness.matches[0].arms[0]; + let proved = scopes(function, &types).unwrap(); + let binder = arm.bindings[0].0.id; + let alias = function + .locals + .iter() + .find(|local| local.name == "alias") + .unwrap() + .id; + ensure_scope(function, &proved, binder, arm.entry).unwrap(); + ensure_scope(function, &proved, alias, arm.entry).unwrap(); + let outside = function + .blocks + .iter() + .find(|block| !arm.region.contains(&block.id) && block.id != witness.matches[0].header) + .unwrap() + .id; + assert!(ensure_scope(function, &proved, alias, outside).is_err()); + let owned = program + .functions + .iter() + .find(|f| f.identity.declaration.name == "owned") + .unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, owned, &types) + .expect("MatchBinding alone does not turn Source Owned into borrowed"); + crate::prepare_native_sequences(&mut program, &types); + assert!( + exercise(&program) + .blocks + .iter() + .all(|block| !matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let called = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "called") + .unwrap(); + assert!( + called + .blocks + .iter() + .all(|block| !matches!(block.terminator.kind, TerminatorKind::ForEach { .. })), + "call-produced scrutinee composition must actually prepare its viewed loop" + ); + scopes(called, &types).expect("exact selecting call survives canonical sequence edits"); + crate::prepare_native_uninhabited_sums(&mut program, &types); + crate::prepare_native_generated_functions(&mut program, &types); + crate::validate_call_ownership(&program, &types).expect("canonical match region remaps"); + let function = exercise_mut(&mut program); + assert!(crate::sequences::prune::unused_locals(function)); + scopes(function, &types).expect("match proof survives dense local remap"); + crate::validate_call_ownership(&program, &types).unwrap(); +} + +#[test] +fn original_match_transport_rejects_public_selector_binding_and_scope_corruption() { + let (original, types) = source_program(); + for case in 0..7 { + let mut program = original.clone(); + let function = exercise_mut(&mut program); + let record = function.match_bindings.as_ref().unwrap().matches[0].clone(); + match case { + 0 => function.match_bindings = None, + 1 => { + let TerminatorKind::Switch { variants, .. } = &mut function.blocks + [record.header.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + variants[0].0 = variants[1].0; + } + 2 => { + let TerminatorKind::Switch { variants, .. } = &mut function.blocks + [record.header.index() as usize] + .terminator + .kind + else { + unreachable!() + }; + let first = variants[0].2[0]; + variants[0].2[0] = variants[1].2[0]; + variants[1].2[0] = first; + } + 3 => { + let binder = record.arms[0].bindings[0].0.id; + function.locals[binder.index() as usize].span = record.arms[1].bindings[0].0.span; + } + 4 => { + let binder = record.arms[0].bindings[0].0.id; + function.blocks[record.arms[0].entry.index() as usize] + .statements + .insert( + 0, + Statement { + kind: StatementKind::Let { + local: binder, + value: Expression { + kind: hir::ExpressionKind::Nothing, + ty: function.locals[binder.index() as usize].ty, + span: function.locals[binder.index() as usize].span, + }, + }, + span: record.selector.span, + }, + ); + } + 5 => { + // The exact original invocation moves beyond its arm. No proof + // may be recreated from the destination block's types or names. + let arm = &record.arms[1]; + let entry = arm.entry.index() as usize; + let statement = function.blocks[entry] + .statements + .iter() + .position(|statement| { + let mut call = false; + let block = BasicBlock { + id: arm.entry, + statements: vec![statement.clone()], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: statement.span, + }, + }; + crate::resource_ownership::visit_block(&block, &mut |value| { + call |= matches!(value.kind, hir::ExpressionKind::Call { .. }) + }); + call + }) + .expect("second arm has its original invocation"); + let moved = function.blocks[entry].statements.remove(statement); + let join = function + .blocks + .iter() + .find(|block| { + !record.arms.iter().any(|arm| arm.region.contains(&block.id)) + && block.id != record.header + && matches!(block.terminator.kind, TerminatorKind::Return(_)) + }) + .unwrap() + .id; + function.blocks[join.index() as usize] + .statements + .insert(0, moved); + } + _ => { + let entry = record.arms[0].entry; + // An external CFG entry cannot acquire the selected variant. + function.entry = entry; + } + } + if (1..=4).contains(&case) { + assert!( + scopes(function, &types).is_err(), + "independent original match association must reject case {case}" + ); + } + assert!( + crate::validate_call_ownership(&program, &types).is_err(), + "forged match case {case}" + ); + } +} + +#[test] +fn transparent_match_views_snapshot_the_original_owner_before_payload_take() { + for (declaration, input, scrutinee) in [ + ("", "Choice", "(view choice)"), + ("", "secret[Choice]", "declassify(view choice)"), + ( + "type RefinedChoice = Choice where true\n", + "RefinedChoice", + "coarsen(view choice)", + ), + ] { + // Same actual transparent-view Source forms as the HIR controls. Add + // a second invocation to keep original ownership observably necessary. + let source = format!( + "namespace app\nstruct Payload:\n values: list[int64]\nenum Choice:\n item(data: Payload)\n{declaration}function observe(view data: Payload) returns int64:\n return list.length[int64](view data.values)\nfunction inspect(view choice: {input}) returns int64:\n match {scrutinee}:\n item(data):\n return observe(view data)\nfunction twice(view choice: {input}) returns int64:\n return inspect(view choice) + inspect(view choice)\n" + ); + let project = jett_common::FileId::new(0); + let list = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let mut parsed = jett_parser::parse(include_str!("../../../../stdlib/list.jett"), list); + let mut primary = jett_parser::parse(&source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + parsed.module.items.append(&mut primary.module.items); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{scrutinee}: {:?}", + checked.diagnostics + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([ + (project, jett_common::SourceOrigin::Project), + (list, jett_common::SourceOrigin::Stdlib), + ]), + ) + .expect("actual transparent-view Source HIR"); + let mut program = + crate::lower(&high, &checked.interner).expect("original owner match snapshot MIR"); + let inspect = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + let parameter = inspect.params[0].local; + let selector = inspect + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Switch { scrutinee, .. } => Some(scrutinee), + _ => None, + }) + .unwrap(); + let hir::ExpressionKind::Clone(snapshot) = &selector.kind else { + panic!("{scrutinee}: payload Switch must own an explicit independent snapshot") + }; + let mut backing = snapshot.as_ref(); + while let hir::ExpressionKind::View(value) + | hir::ExpressionKind::Coarsen(value) + | hir::ExpressionKind::Declassify(value) = &backing.kind + { + backing = value; + } + assert!(matches!(backing.kind, hir::ExpressionKind::Local(local) if local == parameter)); + crate::move_values::MoveValuePlan::analyze(&program, inspect, &checked.interner) + .expect("Switch clone observes and preserves the original view owner"); + let twice = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "twice") + .unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, twice, &checked.interner) + .expect("the same source owner remains available to both inspections"); + crate::prepare_native_sequences(&mut program, &checked.interner); + crate::validate_call_ownership(&program, &checked.interner) + .expect("transparent snapshot retains exact original arm authority"); + } +} diff --git a/crates/jett_mir/src/move_values.rs b/crates/jett_mir/src/move_values.rs new file mode 100644 index 00000000..7c8a7d0e --- /dev/null +++ b/crates/jett_mir/src/move_values.rs @@ -0,0 +1,2380 @@ +//! Linear native places, call loans, and immutable nonowning local aliases. +//! Definite availability is an intersection fixed point over actual CFG edges. +use crate::copy_values::{CopyValuePlan, switch_bindings_on_edge}; +use crate::{ + ControlFlowGraph, Function, ParamMode, Program, ResourceCallNode, StatementKind, TerminatorKind, +}; +use jett_hir::{BinaryOp, Expression, ExpressionKind, IntrinsicId, StringSegment}; +use jett_types::{Type, TypeId, TypeInterner}; +use std::collections::{BTreeMap, BTreeSet}; +type Set = BTreeSet; + +pub fn representation_type(types: &TypeInterner, ty: TypeId) -> TypeId { + let mut current = ty; + for _ in 0..types.len() { + if current.index() as usize >= types.len() { + break; + } + current = match types.resolve(current) { + Type::Secret(inner) | Type::Refinement { base: inner, .. } => *inner, + _ => break, + }; + } + current +} + +/// Interface boxes have an erased identity. A refinement over an interface +/// instead has its own nominal identity, despite sharing the handle layout. +pub fn is_erased_interface(types: &TypeInterner, mut ty: TypeId) -> bool { + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return false; + } + match types.resolve(ty) { + Type::Secret(inner) => ty = *inner, + Type::Interface(_) => return true, + _ => return false, + } + } + false +} + +pub fn is_string(types: &TypeInterner, ty: TypeId) -> bool { + let underlying = representation_type(types, ty); + (underlying.index() as usize) < types.len() && matches!(types.resolve(underlying), Type::String) +} + +pub fn is_function(types: &TypeInterner, ty: TypeId) -> bool { + let underlying = representation_type(types, ty); + (underlying.index() as usize) < types.len() + && matches!(types.resolve(underlying), Type::Function { .. }) +} + +pub fn is_copy_owned(types: &TypeInterner, ty: TypeId) -> bool { + is_string(types, ty) || is_function(types, ty) +} + +pub fn is_secret(types: &TypeInterner, ty: TypeId) -> bool { + let mut current = ty; + for _ in 0..types.len() { + if current.index() as usize >= types.len() { + break; + } + current = match types.resolve(current) { + Type::Secret(_) => return true, + Type::Refinement { base, .. } => *base, + _ => break, + }; + } + false +} + +pub fn is_linear(types: &TypeInterner, ty: TypeId) -> bool { + let ty = representation_type(types, ty); + if ty.index() as usize >= types.len() { + return false; + } + matches!( + types.resolve(ty), + Type::Bytes + | Type::TypeConstruction + | Type::Result(..) + | Type::Optional(_) + | Type::List(_) + | Type::Set(_) + | Type::Map(..) + | Type::Struct(_) + | Type::Interface(_) + | Type::Enum(_) + | Type::Bitfield(_) + | Type::Machine(_) + | Type::MachineState { .. } + ) +} + +pub fn intrinsic_borrows(id: IntrinsicId, index: usize, argument: &Expression) -> bool { + if matches!(id, IntrinsicId::Print | IntrinsicId::Println) { + let mut value = argument; + while let ExpressionKind::Coarsen(inner) | ExpressionKind::Declassify(inner) = &value.kind { + value = inner; + } + // Debug output accepts explicit views without taking their owner. + // Ordinary arguments retain their checked consumption behavior. + return matches!(value.kind, ExpressionKind::View(_)); + } + if matches!( + id, + IntrinsicId::TypeConstructVariantStart | IntrinsicId::TypeConstructMachineStart + ) && index == 0 + { + return true; + } + if matches!(id, IntrinsicId::SecretCompare | IntrinsicId::SecretRedact) { + return true; + } + if matches!( + id, + IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoHmacSha256 + ) { + return true; + } + (index == 1 + && matches!( + id, + IntrinsicId::SetRemove + | IntrinsicId::SetContains + | IntrinsicId::MapRemove + | IntrinsicId::MapHas + | IntrinsicId::MapGet + | IntrinsicId::TypeFieldValue + | IntrinsicId::TypeConstructPut + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue + )) + || (index == 0 + && matches!( + id, + IntrinsicId::BytesLength + | IntrinsicId::BytesSlice + | IntrinsicId::BytesToHex + | IntrinsicId::BytesToString + | IntrinsicId::BytesGet + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::ListLength + | IntrinsicId::ListIsSorted + | IntrinsicId::ListGetClone + | IntrinsicId::ListSum + | IntrinsicId::MathAverage + | IntrinsicId::MathMedian + | IntrinsicId::SetLength + | IntrinsicId::SetContains + | IntrinsicId::MapLength + | IntrinsicId::MapHas + | IntrinsicId::MapGet + | IntrinsicId::TypeVariantValue + | IntrinsicId::TypeMachineStateValue + | IntrinsicId::TypeFieldValue + | IntrinsicId::TypeVariantFieldValue + | IntrinsicId::TypeMachineFieldValue + )) +} + +pub struct MoveValuePlan; +impl MoveValuePlan { + pub fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, + ) -> Result { + Self::analyze_inner(program, function, types, None) + } + pub(crate) fn analyze_companion( + context: &crate::resource_ownership::CompanionContext<'_>, + ) -> Result { + Self::analyze_inner( + context.program, + context.function, + context.types, + Some(context), + ) + } + fn analyze_inner( + program: &Program, + function: &Function, + types: &TypeInterner, + companion: Option<&crate::resource_ownership::CompanionContext<'_>>, + ) -> Result { + if companion.is_none() + && (crate::resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| crate::resource_type_pending(types, local.ty))) + { + return Err("pending ResourceOwnershipPlan: ordinary MoveValuePlan cannot transfer or drop Resource custody".into()); + } + crate::field_updates::validate_function(function, types)?; + validate_local_view_initializers(function, types)?; + let call_views = crate::call_views::validate(function, types)?; + // The private companion context already borrows the exact fresh custody + // proof. Re-run complete Source call validation without the public + // ordinary admission gate; every ordinary acquisition check is retained. + let caller_acquisitions = match companion { + Some(_) => crate::call_ownership::validate_function(program, function, types)?, + None => crate::validate_caller_acquisitions(program, function, types)?, + }; + let mut plan = match companion { + Some(context) => CopyValuePlan::analyze_companion(context)?, + None => CopyValuePlan::analyze_storage(function, types, Some(program))?, + }; + if let Some(entry) = function.actor_handler_entry()? { + for role in entry.roles() { + if matches!( + role.role(), + jett_typecheck::CheckedActorBindingRole::Message { .. } + ) && (is_linear(types, role.fact().ty) || is_copy_owned(types, role.fact().ty)) + { + let index = role.local().index() as usize; + if !plan.owned_locals.contains(&index) { + plan.owned_locals.push(index); + } + } + } + } + if function.identity.declaration.kind == jett_hir::DeclarationKind::ActorHandler { + // Captured state is written back after a return/respond terminator. + // Keep its owning slots live even when the source body stops reading it. + let captures = function + .params + .iter() + .take(function.capture_count) + .map(|param| param.local.index() as usize) + .collect::(); + for live in plan.live_in.iter_mut().chain(&mut plan.live_out) { + live.extend(&captures); + } + for block in &mut plan.live_after_statement { + for live in block { + live.extend(&captures); + } + } + } + let cfg = ControlFlowGraph::analyze(function).map_err(|e| format!("{e:?}"))?; + let all: Set = (0..function.locals.len()).collect(); + let params: Set = function + .params + .iter() + .map(|p| p.local.index() as usize) + .collect(); + let mut outgoing = vec![all.clone(); function.blocks.len()]; + let mut outgoing_loans = vec![Set::new(); function.blocks.len()]; + let mut outgoing_aliases = vec![Set::new(); function.blocks.len()]; + let mut borrow_sources = BTreeMap::new(); + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::IterationBorrow { + source, + token, + start: true, + } = &statement.kind + { + let root = function + .view_root(source.root()) + .ok_or("iteration source has an invalid borrowed origin")?; + borrow_sources.insert(token.index() as usize, root.index() as usize); + } + } + let mut alias_sources = BTreeMap::new(); + for local in &function.locals { + if local.view_source.is_some() { + let root = function + .view_root(local.id) + .ok_or("borrowed local origin is invalid or cyclic")?; + alias_sources.insert(local.id.index() as usize, root.index() as usize); + } + } + let parameter_aliases = function + .params + .iter() + .filter(|parameter| { + function + .local(parameter.local) + .is_some_and(|local| local.view_source.is_some()) + }) + .map(|parameter| parameter.local.index() as usize) + .collect::(); + let incoming_loans = |id, outgoing: &[Set]| { + cfg.predecessors(id) + .iter() + .filter(|p| cfg.reverse_postorder().contains(p)) + .flat_map(|p| outgoing[p.index() as usize].iter().copied()) + .collect::() + }; + let incoming = |id: crate::BlockId, outgoing: &[Set]| { + if id == function.entry { + return params.clone(); + } + let mut state = all.clone(); + for pred in cfg + .predecessors(id) + .iter() + .filter(|p| cfg.reverse_postorder().contains(p)) + { + let edge = switch_bindings_on_edge(function, *pred, id); + let produced = outgoing[pred.index() as usize] + .union(&edge) + .copied() + .collect::(); + state = state.intersection(&produced).copied().collect(); + } + state + }; + loop { + let mut changed = false; + for &id in cfg.reverse_postorder() { + let state = incoming(id, &outgoing); + let (next, active, aliases) = Flow { + program, + types, + function, + state, + loans: Set::new(), + active: incoming_loans(id, &outgoing_loans), + aliases: if id == function.entry { + parameter_aliases.clone() + } else { + incoming_loans(id, &outgoing_aliases) + }, + borrow_sources: &borrow_sources, + alias_sources: &alias_sources, + call_views: &call_views, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, + validate: false, + companion, + } + .block(id)?; + changed |= next != outgoing[id.index() as usize] + || active != outgoing_loans[id.index() as usize] + || aliases != outgoing_aliases[id.index() as usize]; + outgoing_loans[id.index() as usize] = active; + outgoing_aliases[id.index() as usize] = aliases; + outgoing[id.index() as usize] = next; + } + if !changed { + break; + } + } + for &id in cfg.reverse_postorder() { + Flow { + program, + types, + function, + state: incoming(id, &outgoing), + loans: Set::new(), + active: incoming_loans(id, &outgoing_loans), + aliases: if id == function.entry { + parameter_aliases.clone() + } else { + incoming_loans(id, &outgoing_aliases) + }, + borrow_sources: &borrow_sources, + alias_sources: &alias_sources, + call_views: &call_views, + caller_acquisitions: &caller_acquisitions, + taking_binding: None, + validate: true, + companion, + } + .block(id)?; + } + Ok(plan) + } +} + +/// Every borrowed slot needs a typed origin proof, even when its definition is +/// unused or lies in a block outside the reachable ownership fixed point. +pub fn validate_local_view_initializers( + function: &Function, + types: &TypeInterner, +) -> Result<(), String> { + crate::ordinary_borrowed_sums::validate(function, types)?; + let stages = crate::call_views::validate(function, types)?; + let mut initialized = Set::new(); + for view in &function.actor_state_views { + view.validate(&function.locals, types)?; + } + for parameter in &function.params { + if function.actor_state_view(parameter.local)? { + initialized.insert(parameter.local.index() as usize); + } + } + for (block, index, statement) in function.blocks.iter().flat_map(|block| { + block + .statements + .iter() + .enumerate() + .map(move |(index, statement)| (block.id, index, statement)) + }) { + if let StatementKind::SumTake { + source, + target, + success: true, + } = statement.kind + && let Some(row) = function.ordinary_borrowed_sum_projection(block, index)? + { + if row.source() != source || row.output() != target { + return Err( + "ordinary borrowed initializer changed its constructor projection".into(), + ); + } + initialized.insert(target.index() as usize); + continue; + } + if let StatementKind::SumTake { + source, + target, + success: true, + } = statement.kind + && let Some(row) = function.resource_borrowed_sum_projection( + block, + crate::ResourcePosition::Statement(index), + )? + { + if row.source() != source || row.output() != target { + return Err( + "borrowed Resource initializer changed its constructor projection".into(), + ); + } + initialized.insert(target.index() as usize); + continue; + } + let (local, value) = match &statement.kind { + StatementKind::Let { local, value } => { + if stages.contains_key(&(local.index() as usize)) { + return Err("internal call view cannot use a source alias initializer".into()); + } + (local, value) + } + StatementKind::BeginCallView { local, value } => (local, value), + _ => continue, + }; + let Some(definition) = function.local(*local) else { + continue; + }; + if let Some(view) = function + .actor_state_views + .iter() + .find(|view| view.local() == *local) + { + if value != view.initializer() { + return Err( + "actor constructor changed original persistent view initializer".into(), + ); + } + } + let Some(source) = definition.view_source else { + continue; + }; + let origin = function + .local(source) + .ok_or("borrowed local initializer source is outside its function")?; + if !function.ordinary_borrowed_sum_alias(*local, value)? + && !function.resource_borrowed_sum_alias(*local, value)? + { + jett_hir::validate_local_view_initializer( + value, + source, + origin.ty, + definition.ty, + types, + )?; + } + initialized.insert(local.index() as usize); + } + for local in &function.locals { + if local.view_source.is_some() && !initialized.contains(&(local.id.index() as usize)) { + return Err("native borrowed alias has no validated initializer".into()); + } + } + Ok(()) +} + +struct Flow<'a> { + program: &'a Program, + function: &'a Function, + types: &'a TypeInterner, + state: Set, + loans: Set, + active: Set, + // May-created aliases never expire in this bounded native implementation. + // Choosing lexical or last-use loan expiration remains a separate contract. + aliases: Set, + borrow_sources: &'a BTreeMap, + alias_sources: &'a BTreeMap, + call_views: &'a BTreeMap, + caller_acquisitions: &'a crate::CallerAcquisitions<'a>, + taking_binding: Option, + validate: bool, + companion: Option<&'a crate::resource_ownership::CompanionContext<'a>>, +} +impl Flow<'_> { + fn block(mut self, id: crate::BlockId) -> Result<(Set, Set, Set), String> { + let block = &self.function.blocks[id.index() as usize]; + for (index, statement) in block.statements.iter().enumerate() { + match &statement.kind { + StatementKind::OpenCallOwnerGeneration { root, .. } => { + self.read(*root, "generation owner")?; + } + StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::PrepareFieldUpdate { place, begin: true } => { + place.validate(place.target(), &self.function.locals, self.types)?; + let root = place.root(); + self.read(root, "field update owner preflight")?; + self.require_owned_definition(root)?; + self.reject_aliased_owner_change(root.index() as usize)?; + if self.function.is_view_local(root) + || (self.validate + && self.active.iter().any(|token| { + self.borrow_sources.get(token) == Some(&(root.index() as usize)) + })) + { + return Err("cannot update a borrowed field owner".into()); + } + } + StatementKind::PrepareFieldUpdate { begin: false, .. } => {} + StatementKind::UpdateField { + target, + value, + place, + } => { + place.validate(target, &self.function.locals, self.types)?; + let root = place.root(); + self.read(root, "field update owner before commit")?; + self.expr(value, false)?; + self.read(root, "field update owner after RHS")?; + self.require_owned_definition(root)?; + self.reject_aliased_owner_change(root.index() as usize)?; + if self.function.is_view_local(root) + || (self.validate + && self.active.iter().any(|token| { + self.borrow_sources.get(token) == Some(&(root.index() as usize)) + })) + { + return Err("cannot commit through a borrowed field owner".into()); + } + } + StatementKind::ReplaceCallOwnerGeneration { + generation, + root, + rhs_owner, + } => { + let plan = self.caller_acquisitions.generation_storage(); + let slot = plan + .slot(*generation) + .ok_or("generation replacement has no validated storage")?; + if slot.root() != *root { + return Err("generation replacement changes its owner".into()); + } + let actual_root = self.read(*root, "generation owner")?; + self.read(*rhs_owner, "generation RHS owner")?; + self.require_owned_definition(*root)?; + self.require_owned_definition(*rhs_owner)?; + if self.validate + && self.aliases.iter().any(|loan| { + self.call_views.get(loan) == Some(&actual_root) + && !plan.covers_loan(*generation, crate::LocalId::new(*loan as u32)) + }) + { + return Err( + "generation replacement has an unrelated active call loan".into() + ); + } + if self.validate + && self.aliases.iter().any(|alias| { + !self.call_views.contains_key(alias) + && self.alias_sources.get(alias) == Some(&actual_root) + }) + { + return Err("generation replacement has a persistent source alias".into()); + } + if self.validate + && self + .active + .iter() + .any(|token| self.borrow_sources.get(token) == Some(&actual_root)) + { + return Err("generation replacement has an active iteration loan".into()); + } + self.reject_aliased_owner_change(rhs_owner.index() as usize)?; + self.state.remove(&(rhs_owner.index() as usize)); + self.state.insert(root.index() as usize); + } + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => { + self.read(*source, "reflected container source")?; + } + StatementKind::IterationBorrow { + source, + token, + start, + } => { + self.read(source.root(), "iteration source")?; + if *start { + self.active.insert(token.index() as usize); + } else { + self.active.remove(&(token.index() as usize)); + } + } + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + let root = self.read(source.root(), "sequence source")?; + if let StatementKind::SequenceGet { index, .. } = statement.kind { + self.read(index, "sequence index")?; + } + if let StatementKind::SequenceGet { consume: true, .. } = statement.kind { + if matches!(source, crate::SequenceSource::Projected { .. }) { + return Err("cannot consume a projected sequence field".into()); + } + if self.function.is_view_local(source.root()) { + return Err("cannot take element from borrowed sequence".into()); + } + self.reject_aliased_owner_change(root)?; + if self.validate + && self + .active + .iter() + .any(|token| self.borrow_sources.get(token) == Some(&root)) + { + return Err("cannot take element while sequence is borrowed".into()); + } + } + self.require_owned_definition(*target)?; + self.state.insert(target.index() as usize); + } + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + let source_id = source.index() as usize; + let root = self.read(*source, "sum source")?; + if matches!(statement.kind, StatementKind::SumTake { .. }) { + if self.function.is_view_local(*source) { + if let Some(row) = self + .function + .ordinary_borrowed_sum_projection(block.id, index)? + { + if row.source() != *source { + return Err( + "ordinary borrowed extraction changed its exact source" + .into(), + ); + } + if matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) { + if row.output() != *target + || !self.function.is_view_local(*target) + { + return Err( + "ordinary borrowed projection gained owning output" + .into(), + ); + } + self.aliases.insert(target.index() as usize); + } else { + if row.error() != Some(*target) { + return Err("ordinary borrowed extraction changed its failure local".into()); + } + self.require_owned_definition(*target)?; + } + self.state.insert(target.index() as usize); + continue; + } + let row = self + .function + .resource_borrowed_sum_projection( + block.id, + crate::ResourcePosition::Statement(index), + )? + .ok_or("cannot take payload from borrowed sum")?; + if row.source() != *source { + return Err( + "borrowed Resource extraction changed its current source" + .into(), + ); + } + if matches!( + statement.kind, + StatementKind::SumTake { success: true, .. } + ) { + if row.output() != *target || !self.function.is_view_local(*target) + { + return Err( + "borrowed Resource projection gained owning output".into(), + ); + } + self.aliases.insert(target.index() as usize); + } else { + self.require_owned_definition(*target)?; + } + self.state.insert(target.index() as usize); + continue; + } + self.reject_aliased_owner_change(root)?; + self.state.remove(&source_id); + } + self.require_owned_definition(*target)?; + self.state.insert(target.index() as usize); + } + StatementKind::BeginCallView { local, value } => { + let id = local.index() as usize; + if self.validate && self.aliases.contains(&id) { + return Err("internal call view is already active".into()); + } + self.expr(value, true)?; + self.aliases.insert(id); + self.state.insert(id); + } + StatementKind::ResourceLexicalExit(_) => { + let exit = self + .function + .resource_lexical_exit(block.id, crate::ResourcePosition::Statement(index))? + .ok_or("Resource lexical marker lost its private constructor proof")?; + for local in exit.current_locals(self.function)? { + self.aliases.remove(&(local.index() as usize)); + self.state.remove(&(local.index() as usize)); + } + } + StatementKind::EndCallView { local } => { + let id = local.index() as usize; + if !self.call_views.contains_key(&id) + || (self.validate + && (!self.aliases.contains(&id) || !self.state.contains(&id))) + { + return Err("internal call view end has no active initialization".into()); + } + self.aliases.remove(&id); + self.state.remove(&id); + } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: Some(local), + .. + }) + | StatementKind::Let { local, value } => { + let definition = self + .function + .local(*local) + .ok_or("native local definition is outside its function")?; + if let Some(source) = definition.view_source { + let origin = self + .function + .local(source) + .ok_or("borrowed local initializer source is outside its function")?; + if !self.function.ordinary_borrowed_sum_alias(*local, value)? + && !self.function.resource_borrowed_sum_alias(*local, value)? + { + jett_hir::validate_local_view_initializer( + value, + source, + origin.ty, + definition.ty, + self.types, + )?; + } + self.expr(value, true)?; + self.aliases.insert(local.index() as usize); + } else { + self.reject_aliased_owner_change(local.index() as usize)?; + let source = self + .caller_acquisitions + .owner_initializer(*local) + .and_then(|source| source.binding); + let previous = std::mem::replace(&mut self.taking_binding, source); + let result = self.expr(value, false); + self.taking_binding = previous; + result?; + } + self.state.insert(local.index() as usize); + // A copied primitive/String final binding owns its value. Its + // constructor-only read intermediates cannot keep the shell borrowed. + if let Some(intermediates) = self + .function + .ordinary_copied_sum_intermediates(*local, value)? + { + for intermediate in intermediates { + self.aliases.remove(&(intermediate.index() as usize)); + self.state.remove(&(intermediate.index() as usize)); + } + } + } + StatementKind::CheckRefinement { local, call, .. } => { + self.require_owned_definition(*local)?; + self.reject_aliased_owner_change(local.index() as usize)?; + self.expr(call, false)?; + self.state.insert(local.index() as usize); + } + StatementKind::Assign { target, value } => { + self.expr(value, false)?; + let ExpressionKind::Local(local) = target.kind else { + return Err("assignment needs a materialized place".into()); + }; + self.require_owned_definition(local)?; + self.reject_aliased_owner_change(local.index() as usize)?; + if self.validate + && self.active.iter().any(|token| { + self.borrow_sources.get(token) == Some(&(local.index() as usize)) + }) + { + return Err("cannot overwrite a borrowed iteration source".into()); + } + if self.function.is_view_local(local) + && (is_linear(self.types, target.ty) || is_function(self.types, target.ty)) + { + return Err("cannot overwrite a borrowed native place".into()); + } + self.state.insert(local.index() as usize); + } + StatementKind::ResourceCall(ResourceCallNode::Stage { + value, + ordinary: None, + .. + }) => { + if self.companion.is_none() { + return Err("Resource call stage requires its fresh companion plan".into()); + } + self.expr(value, false)?; + } + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, output }) => { + if self.companion.is_none() { + return Err("Resource call invoke requires its fresh companion plan".into()); + } + let record = self + .function + .resource_call_region(*region) + .ok_or("Resource call invoke lacks its sealed region")?; + for actual in record.actuals() { + if let Some(local) = actual.ordinary() { + self.read(local, "Resource call arrived ordinary endpoint")?; + let header = self + .function + .local(local) + .ok_or("Resource call endpoint header missing")?; + if is_linear(self.types, header.ty) + || is_copy_owned(self.types, header.ty) + { + self.state.remove(&(local.index() as usize)); + } + } + } + self.require_owned_definition(*output)?; + self.state.insert(output.index() as usize); + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. }, + ) => { + if self.companion.is_none() { + return Err("Resource call region requires its fresh companion plan".into()); + } + } + StatementKind::ResourceConstructor(node) => { + if self.companion.is_none() { + return Err("Resource constructor requires its fresh companion plan".into()); + } + self.function.resource_constructor(node.constructor())?; + match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + self.expr(value, false)? + } + crate::ResourceConstructorNode::Commit { output, .. } => { + self.state.insert(output.index() as usize); + } + _ => {} + } + } + StatementKind::Evaluate(value) => self.expr(value, false)?, + StatementKind::Assert { condition, message } => { + self.expr(condition, false)?; + if let Some(message) = message { + self.expr(message, false)?; + } + } + StatementKind::Trace(local) => { + self.read(*local, "trace target")?; + } + StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + self.expr(condition, false)?; + } + for local in bindings { + self.read(*local, "breakpoint binding")?; + } + } + _ => return Err("statement needs explicit native ownership lowering".into()), + } + self.loans.clear(); + } + match &block.terminator.kind { + TerminatorKind::Return(Some(v)) + | TerminatorKind::Respond(v) + | TerminatorKind::Branch { condition: v, .. } => self.expr(v, false)?, + TerminatorKind::Switch { scrutinee, .. } => self.expr(scrutinee, false)?, + TerminatorKind::ReflectedTypeDispatch { type_info, .. } => { + self.expr(type_info, true)? + } + TerminatorKind::Return(None) + | TerminatorKind::Goto(_) + | TerminatorKind::Unreachable => {} + _ => return Err("terminator needs explicit native ownership lowering".into()), + } + Ok((self.state, self.active, self.aliases)) + } + + fn read(&self, local: jett_hir::LocalId, description: &str) -> Result { + let root = self + .function + .view_root(local) + .ok_or("borrowed local origin is invalid or cyclic")?; + let mut current = local; + loop { + if self.validate && !self.state.contains(&(current.index() as usize)) { + return Err(format!("{description} is moved or uninitialized")); + } + let metadata = self + .function + .local(current) + .ok_or("borrowed local origin is outside its function")?; + match metadata.view_source { + Some(source) => current = source, + None => return Ok(root.index() as usize), + } + } + } + + fn require_owned_definition(&self, local: jett_hir::LocalId) -> Result<(), String> { + let metadata = self + .function + .local(local) + .ok_or("native local definition is outside its function")?; + if metadata.view_source.is_some() { + return Err("borrowed local alias requires its matching initialization".into()); + } + Ok(()) + } + + fn reject_aliased_owner_change(&self, root: usize) -> Result<(), String> { + if self.validate + && self + .aliases + .iter() + .any(|stage| self.call_views.get(stage) == Some(&root)) + { + return Err(format!("cannot move native place {root} while borrowed")); + } + if self.validate + && self + .aliases + .iter() + .any(|alias| self.alias_sources.get(alias) == Some(&root)) + { + return Err(format!( + "native owner {root} consumption or rebinding after creating a local view alias is not implemented" + )); + } + Ok(()) + } + + fn expr(&mut self, value: &Expression, borrowed: bool) -> Result<(), String> { + let source = self.caller_acquisitions.argument_binding(value); + let previous = self.taking_binding; + if source.is_some() { + self.taking_binding = source; + } + let result = self.expr_inner(value, borrowed); + self.taking_binding = previous; + result + } + + fn expr_inner(&mut self, value: &Expression, borrowed: bool) -> Result<(), String> { + if let Some(context) = self.companion { + if !context.contains(value) { + return Err("Resource companion move is outside its current function".into()); + } + if context.resource_expression(value) { + if context.carrier_expression(value) { + match &value.kind { + ExpressionKind::ListConstruct { elements } => { + for child in elements { + self.expr(child, false)?; + } + return Ok(()); + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expr(&entry.key, false)?; + self.expr(&entry.value, false)?; + } + return Ok(()); + } + ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } => { + for index in evaluation_order { + self.expr(&fields[*index], false)?; + } + return Ok(()); + } + ExpressionKind::EnumConstruct { + payloads, + evaluation_order, + .. + } => { + for index in evaluation_order { + self.expr(&payloads[*index], false)?; + } + return Ok(()); + } + ExpressionKind::MachineConstruct { payloads, .. } => { + for child in payloads { + self.expr(child, false)?; + } + return Ok(()); + } + ExpressionKind::Field { base, .. } => return self.expr(base, true), + ExpressionKind::InterfaceCoerce { value, adapters } + if adapters.is_empty() => + { + return self.expr(value, borrowed); + } + _ => {} + } + } + match &value.kind { + ExpressionKind::Local(local) => { self.read(*local, "Resource companion initialization")?; return Ok(()); } + ExpressionKind::View(inner) => return self.expr(inner, true), + ExpressionKind::Clone(inner) if context.descriptor_expression(value) => return self.expr(inner, false), + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::OptionalNone => return Ok(()), + ExpressionKind::ResultOk(inner) | ExpressionKind::ResultFail(inner) | ExpressionKind::OptionalSome(inner) => return self.expr(inner, false), + ExpressionKind::Call { .. } | ExpressionKind::IndirectCall { .. } | ExpressionKind::ResourceInvoke { .. } => {}, + _ => return Err("pending Resource companion: expression requires its dedicated custody transport".into()), + } + } + } + match &value.kind { + ExpressionKind::Local(local) => { + let id = local.index() as usize; + let root = self.read(*local, &format!("native place {id}"))?; + let borrowed_local = self.function.is_view_local(*local) + && (self + .function + .local(*local) + .is_some_and(|local| local.view_source.is_some()) + || is_linear(self.types, value.ty) + || is_function(self.types, value.ty)); + if !borrowed && borrowed_local { + return Err(format!("cannot move borrowed native place {id}")); + } + if borrowed + && self.taking_binding != Some(*local) + && (is_linear(self.types, value.ty) || is_function(self.types, value.ty)) + { + self.loans.insert(root); + } + // Verify/property owner reads clone, but the borrowed-local + // rejection above applies before that independent-owner path. + if self.taking_binding == Some(*local) + || (is_linear(self.types, value.ty) + && !borrowed + && !matches!( + self.function.identity.declaration.kind, + jett_hir::DeclarationKind::Verify | jett_hir::DeclarationKind::Property + )) + { + self.reject_aliased_owner_change(root)?; + if self.validate + && (self.loans.contains(&root) + || self + .active + .iter() + .any(|token| self.borrow_sources.get(token) == Some(&root))) + { + return Err(format!("cannot move native place {id} while borrowed")); + } + self.state.remove(&id); + } + } + ExpressionKind::View(v) => { + if !borrowed + && (is_linear(self.types, value.ty) + || is_function(self.types, value.ty) + || (is_string(self.types, value.ty) + && !wrapper_copies_result(self.types, value.ty))) + { + return Err("native view cannot escape into an owning value".into()); + } + self.expr(v, true)?; + } + ExpressionKind::Clone(v) => { + let saved = self.loans.clone(); + self.expr(v, true)?; + self.loans = saved; + } + ExpressionKind::Call { + function, + args, + evaluation_order, + .. + } => { + let saved = self.loans.clone(); + for &index in evaluation_order { + let view = self.program.functions[function.index() as usize].params[index].mode + == ParamMode::View; + let explicit_view = matches!(args[index].kind, ExpressionKind::View(_)) + && is_linear(self.types, args[index].ty); + self.expr(&args[index], view || explicit_view)?; + } + self.loans = saved; + } + ExpressionKind::ActorGraphImport(_) => return Err("unexpanded actor graph import has no move plan".into()), + ExpressionKind::ActorGraphStep { instruction, args } => { + for (index, arg) in args.iter().enumerate() { + let previous = self.taking_binding; + if (index == 2 && matches!(instruction.operation(), jett_hir::ActorGraphOperation::Fill { .. })) + || (index == 1 && matches!(instruction.operation(),jett_hir::ActorGraphOperation::RootFill {..})) { + if let ExpressionKind::Local(local) = arg.kind { self.taking_binding = Some(local); } + } + let result = self.expr(arg, false); + self.taking_binding = previous; + result?; + } + } + ExpressionKind::ActorSpawn { + args, + evaluation_order, + constructor, + .. + } => { + if let Some(transferred)=self.function.actor_constructor_view_transfers(value,self.types)? { + let saved=self.loans.clone(); + // Every operand is a privately sealed Local/View(Local), so + // this preflight has no effects or intervening Source calls. + for &index in evaluation_order {self.expr(&args[index],true)?;} + self.loans=saved.clone(); + for alias in &transferred {self.aliases.remove(&(alias.index() as usize));} + for &index in evaluation_order { + if !matches!(args[index].kind,ExpressionKind::View(_)) {self.expr(&args[index],false)?;} + } + for alias in transferred {self.state.remove(&(alias.index() as usize));} + self.loans=saved; + return Ok(()); + } + let saved = self.loans.clone(); + for &index in evaluation_order { + let view = constructor.is_some_and(|id| { + self.program.functions[id.index() as usize].params[index].mode + == ParamMode::View + }); + let explicit_view = matches!(args[index].kind, ExpressionKind::View(_)) + && is_linear(self.types, args[index].ty); + self.expr(&args[index], view || explicit_view)?; + } + self.loans = saved; + } + ExpressionKind::ActorMessage { + actor, + proof, + args, + evaluation_order, + .. + } => { + self.expr(actor, false)?; + self.function.actor_handler_entry()?; + let saved = self.loans.clone(); + for &index in evaluation_order { + let argument=proof.argument(index).ok_or("actor message lost exact checked argument")?; + let view=matches!(argument.effect,jett_typecheck::CheckedCallerEffect::RetainBorrow); + let explicit_view = matches!(args[index].kind, ExpressionKind::View(_)) + && is_linear(self.types, args[index].ty); + self.expr(&args[index], view || explicit_view)?; + } + self.loans = saved; + } + ExpressionKind::IndirectCall { + callee, + args, + evaluation_order, + .. + } => { + let Type::Function { view_params, .. } = self + .types + .resolve(representation_type(self.types, callee.ty)) + else { + return Err("indirect call has no function parameter modes".into()); + }; + if view_params.len() != args.len() { + return Err("indirect call argument and parameter counts differ".into()); + } + let saved = self.loans.clone(); + for &index in evaluation_order { + let explicit_view = matches!(args[index].kind, ExpressionKind::View(_)) + && is_linear(self.types, args[index].ty); + self.expr(&args[index], view_params[index] || explicit_view)?; + } + // Indirect calls evaluate arguments before the callee. Invoking + // a descriptor observes it without transferring its ownership. + self.expr(callee, true)?; + self.loans = saved; + } + ExpressionKind::Intrinsic { + intrinsic, + args, + evaluation_order, + .. + } => { + let saved = self.loans.clone(); + for &index in evaluation_order { + self.expr( + &args[index], + intrinsic_borrows(*intrinsic, index, &args[index]), + )?; + } + self.loans = saved; + } + ExpressionKind::Binary { left, op, right } => { + let operand_type = representation_type(self.types, left.ty); + let enum_equality = matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) + && (operand_type.index() as usize) < self.types.len() + && matches!(self.types.resolve(operand_type), Type::Enum(_)); + self.expr(left, enum_equality)?; + let before = self.state.clone(); + self.expr(right, enum_equality)?; + if matches!(op, BinaryOp::And | BinaryOp::Or) { + self.state = self.state.intersection(&before).copied().collect(); + } + } + ExpressionKind::Declassify(inner) | ExpressionKind::Coarsen(inner) => { + // These wrappers can expose an implicitly copied primitive from + // a nominal or secret view without transferring the source. + self.expr( + inner, + borrowed || wrapper_copies_result(self.types, value.ty), + )?; + } + ExpressionKind::RefinementValidated(value) => self.expr(value, borrowed)?, + ExpressionKind::Join(value) => { + if matches!(value.kind, ExpressionKind::View(_)) { + // The join leaf clones the viewed owner. Its transient + // read loan ends before a later owning argument is used. + let saved = self.loans.clone(); + self.expr(value, true)?; + self.loans = saved; + } else { + self.expr(value, false)?; + } + } + ExpressionKind::Run(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) => self.expr(value, false)?, + ExpressionKind::InterfaceType(value) | ExpressionKind::RuntimeFailureMessage(value) => { + self.expr(value, true)? + } + ExpressionKind::FunctionAdapter { value, .. } => self.expr(value, false)?, + ExpressionKind::InterfaceCoerce { value: inner, .. } => { + let unbox = is_erased_interface(self.types, inner.ty) + && !is_erased_interface(self.types, value.ty); + let handled_input = self + .caller_acquisitions + .handled_conversion_input(value) + .is_some_and(|proved| std::ptr::eq(proved, inner.as_ref())); + self.expr(inner, borrowed || unbox || handled_input)?; + } + ExpressionKind::OptionalNone => {} + ExpressionKind::StructConstruct { + fields, + evaluation_order, + .. + } + | ExpressionKind::EnumConstruct { + payloads: fields, + evaluation_order, + .. + } => { + for &index in evaluation_order { + self.expr(&fields[index], false)?; + } + } + ExpressionKind::MachineConstruct { payloads, .. } => { + for payload in payloads { + self.expr(payload, false)?; + } + } + ExpressionKind::MachineTransition { + source, payloads, .. + } => { + self.expr(source, false)?; + for payload in payloads { + self.expr(payload, false)?; + } + } + ExpressionKind::StateIs { value, .. } => self.expr(value, true)?, + ExpressionKind::BitfieldConstruct { + fields, + evaluation_order, + .. + } => { + for &index in evaluation_order { + self.expr(&fields[index], false)?; + } + } + ExpressionKind::Field { base, .. } => { + let saved = self.loans.clone(); + self.expr(base, true)?; + // An owned field read clones the selected value before ending + // the parent loan; a projected view retains the loan. + if !borrowed || !is_linear(self.types, value.ty) { + self.loans = saved; + } + } + ExpressionKind::ListConstruct { elements } => { + for element in elements { + self.expr(element, false)?; + } + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + self.expr(&entry.key, false)?; + self.expr(&entry.value, false)?; + } + } + ExpressionKind::StringInterpolation(segments) => { + for segment in segments { + if let StringSegment::Value(v) = segment { + self.expr(v, false)?; + } + } + } + ExpressionKind::ClosureRef { captures, .. } => { + for capture in captures { + self.read(*capture, "native capture")?; + let local = self + .function + .local(*capture) + .ok_or("invalid capture local")?; + if self.function.is_view_local(*capture) + && (local.view_source.is_some() + || is_linear(self.types, local.ty) + || is_function(self.types, local.ty)) + { + return Err("cannot capture a borrowed native place".into()); + } + } + } + ExpressionKind::ResourceInvoke { hook, args, evaluation_order, .. } if self.companion.is_some() => { + let Type::Function { view_params, .. } = self.types.resolve(hook.function_type()) else { return Err("Resource hook has no exact function signature".into()); }; + let saved = self.loans.clone(); + for ¶meter in evaluation_order { self.expr(&args[parameter], view_params[parameter])?; } + self.loans = saved; + } + ExpressionKind::ResourceHookValue { .. } | ExpressionKind::ResourceInvoke { .. } => return Err("pending ResourceOwnershipPlan: ordinary move expression cannot own Resource descriptors or invokes".into()), + ExpressionKind::Int(_) + | ExpressionKind::Float(_) + | ExpressionKind::Bool(_) + | ExpressionKind::String(_) + | ExpressionKind::FunctionRef(_) + | ExpressionKind::Nothing + | ExpressionKind::PropertyCaseContext(_) + | ExpressionKind::RuntimeFailure(_) => {} + _ => return Err("expression needs explicit native ownership lowering".into()), + } + Ok(()) + } +} + +fn wrapper_copies_result(types: &TypeInterner, ty: TypeId) -> bool { + matches!( + types.resolve(ty), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Nothing + ) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use jett_common::{FileId, SourceOrigin}; + use jett_hir::{DeclarationKind, LocalId}; + + use super::*; + + const ALIASES: &str = r#"function inspect(flag: bool) returns list[int64]: + list[int64] source = list(1) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + if flag: + trace forwarded + return clone forwarded +"#; + + const PROJECTED_ALIASES: &str = r#"struct Packet: + items: list[int64] +struct Envelope: + packet: Packet +function inspect(flag: bool) returns list[int64]: + Envelope source = Envelope(packet: Packet(items: list(1, 2))) + Envelope duplicate = clone source + list[int64] borrowed = view source.packet.items + list[int64] forwarded = borrowed + if flag: + trace forwarded + return clone forwarded +"#; + + fn lower_source(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + let mir = crate::lower(&hir, &checked.interner).expect("MIR lowering"); + (mir, checked.interner) + } + + fn inspected(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| function.identity.declaration.name == "inspect") + .unwrap() + } + + fn local_named(function: &Function, name: &str) -> LocalId { + function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() + .id + } + + fn local_expression(function: &Function, local: LocalId) -> Expression { + let metadata = function.local(local).unwrap(); + Expression { + kind: ExpressionKind::Local(local), + ty: metadata.ty, + span: metadata.span, + } + } + + fn replace_return(function: &mut Function, value: Expression) { + for block in &mut function.blocks { + if matches!(block.terminator.kind, TerminatorKind::Return(Some(_))) { + block.terminator.kind = TerminatorKind::Return(Some(value.clone())); + } + } + } + + fn alias_initializer_mut(function: &mut Function, local: LocalId) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { + local: target, + value, + } if *target == local => Some(value), + _ => None, + }) + .unwrap() + } + + fn projection_field_mut(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, ExpressionKind::Field { .. }) { + return value; + } + let ExpressionKind::View(inner) = &mut value.kind else { + panic!("projected alias has an explicit view of its field"); + }; + projection_field_mut(inner) + } + + fn projection_root_mut(value: &mut Expression) -> &mut Expression { + if matches!(value.kind, ExpressionKind::Local(_)) { + return value; + } + let inner = match &mut value.kind { + ExpressionKind::View(inner) | ExpressionKind::Field { base: inner, .. } => inner, + _ => panic!("projection is a transparent field path to a local"), + }; + projection_root_mut(inner) + } + + #[test] + fn explicit_view_join_keeps_exact_and_qualified_result_origins_available() { + for (source_type, result_type) in [ + ("result[int8, int64]", "result[int8, int64]"), + ( + "secret[result[int8, int64]]", + "result[secret[result[int8, int64]], string]", + ), + ] { + for view in [false, true] { + let mode = if view { "view " } else { "" }; + let source = format!( + "function inspect({mode}source: {source_type}) returns {result_type}:\n {result_type} joined = join view source\n trace source\n return joined\n" + ); + let (program, types) = lower_source(&source); + let function = &program.functions[inspected(&program)]; + let origin = local_named(function, "source").index() as usize; + let joined = local_named(function, "joined").index() as usize; + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert_eq!(plan.owned_locals.contains(&origin), !view); + assert!(plan.owned_locals.contains(&joined)); + let (block, statement) = function + .blocks + .iter() + .enumerate() + .find_map(|(block, body)| { + body.statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::Trace(local) + if local.index() as usize == origin) + }) + .map(|statement| (block, statement)) + }) + .unwrap(); + let previous = statement.checked_sub(1).expect("join precedes its trace"); + assert!(matches!(function.blocks[block].statements[previous].kind, + StatementKind::Let { local, .. } if local.index() as usize == joined)); + assert!(plan.live_after_statement[block][previous].contains(&origin)); + } + } + } + + #[test] + fn explicit_view_join_does_not_admit_bare_view_consumption_or_raw_view_escape() { + for (source_type, result_type) in [ + ("result[int8, int64]", "result[int8, int64]"), + ( + "secret[result[int8, int64]]", + "result[secret[result[int8, int64]], string]", + ), + ] { + let source = format!( + "function inspect(view source: {source_type}) returns {result_type}:\n return join view source\n" + ); + for kind in [ + DeclarationKind::Function, + DeclarationKind::Verify, + DeclarationKind::Property, + ] { + let (mut program, types) = lower_source(&source); + let index = inspected(&program); + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap(); + let origin = local_named(&program.functions[index], "source"); + let local = local_expression(&program.functions[index], origin); + let result_type = program.functions[index].return_type; + let function = &mut program.functions[index]; + function.identity.declaration.kind = kind; + replace_return( + function, + Expression { + kind: ExpressionKind::Join(Box::new(local.clone())), + ty: result_type, + span: local.span, + }, + ); + let error = MoveValuePlan::analyze(&program, &program.functions[index], &types) + .unwrap_err(); + assert!( + error.contains("cannot move borrowed native place"), + "{kind:?}: {error}" + ); + replace_return( + &mut program.functions[index], + Expression { + kind: ExpressionKind::View(Box::new(local.clone())), + ..local + }, + ); + let error = MoveValuePlan::analyze(&program, &program.functions[index], &types) + .unwrap_err(); + assert!( + error.contains("native view cannot escape into an owning value"), + "{kind:?}: {error}" + ); + } + } + } + + #[test] + fn explicit_view_join_finishes_its_loan_before_later_owned_call_arguments() { + let source = r#"function consume(first: result[int8, int64], second: result[int8, int64]) returns nothing: + return nothing +function inspect(source: result[int8, int64]) returns nothing: + consume(join view source, source) +"#; + let (mut program, types) = lower_source(source); + let index = inspected(&program); + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap(); + let function = &mut program.functions[index]; + let first = function + .blocks + .iter_mut() + .flat_map(|block| block.statements.iter_mut()) + .find_map(|statement| { + let StatementKind::Evaluate(Expression { + kind: ExpressionKind::Call { args, .. }, + .. + }) = &mut statement.kind + else { + return None; + }; + Some(&mut args[0]) + }) + .unwrap(); + let ExpressionKind::Join(view) = &mut first.kind else { + panic!("first argument joins an explicit view"); + }; + let ExpressionKind::View(local) = &view.kind else { + panic!("explicit borrowed operand"); + }; + // Ordinary joins still consume their operand. A second use in the + // following owned argument must retain the normal availability error. + *view = Box::new(local.as_ref().clone()); + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!(error.contains("moved or uninitialized"), "{error}"); + } + + #[test] + fn local_view_alias_plan_keeps_transitive_origins_live_across_cfg_and_debug_reads() { + let (mut program, types) = lower_source(ALIASES); + let index = inspected(&program); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let borrowed = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + let debug_block = function + .blocks + .iter_mut() + .find(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::Trace(_))) + }) + .unwrap(); + debug_block.statements.push(crate::Statement { + kind: StatementKind::Breakpoint { + condition: None, + bindings: vec![forwarded], + }, + span: debug_block.terminator.span, + }); + let function = &program.functions[index]; + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(source.index() as usize))); + assert!(!plan.owned_locals.contains(&(borrowed.index() as usize))); + assert!(!plan.owned_locals.contains(&(forwarded.index() as usize))); + let mut observed = false; + for live in plan.live_in.iter().chain(&plan.live_out).chain( + plan.live_after_statement + .iter() + .flat_map(|block| block.iter()), + ) { + if live.contains(&(forwarded.index() as usize)) { + observed = true; + assert!(live.contains(&(borrowed.index() as usize))); + assert!(live.contains(&(source.index() as usize))); + } + } + assert!(observed); + } + + #[test] + fn projected_local_views_keep_nested_owner_live_without_owning_payload_slots() { + let (program, types) = lower_source(PROJECTED_ALIASES); + crate::validate(&program).unwrap(); + let function = &program.functions[inspected(&program)]; + let source = local_named(function, "source"); + let borrowed = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + assert_eq!(function.local(borrowed).unwrap().view_source, Some(source)); + assert_eq!( + function.local(forwarded).unwrap().view_source, + Some(borrowed) + ); + assert_ne!( + function.local(source).unwrap().ty, + function.local(borrowed).unwrap().ty + ); + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(source.index() as usize))); + for alias in [borrowed, forwarded] { + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + } + let mut observed = false; + for live in plan.live_in.iter().chain(&plan.live_out).chain( + plan.live_after_statement + .iter() + .flat_map(|block| block.iter()), + ) { + if live.contains(&(forwarded.index() as usize)) { + observed = true; + assert!(live.contains(&(borrowed.index() as usize))); + assert!(live.contains(&(source.index() as usize))); + } + } + assert!(observed); + } + + #[test] + fn mutable_owned_projected_views_keep_transitive_backing_live_without_owning_aliases() { + let source = + PROJECTED_ALIASES.replace(" Envelope source =", " mutable Envelope source ="); + let (mut program, types) = lower_source(&source); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + } + crate::validate(&program).unwrap(); + let function = &program.functions[inspected(&program)]; + let root = local_named(function, "source"); + let alias = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(function.local(root).unwrap().mutable); + assert!(plan.owned_locals.contains(&(root.index() as usize))); + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(forwarded.index() as usize))); + let mut observed = false; + for live in plan.live_in.iter().chain(&plan.live_out).chain( + plan.live_after_statement + .iter() + .flat_map(|block| block.iter()), + ) { + if live.contains(&(forwarded.index() as usize)) { + observed = true; + assert!(live.contains(&(alias.index() as usize))); + assert!(live.contains(&(root.index() as usize))); + } + } + assert!(observed); + } + } + + #[test] + fn mutable_view_parameter_backing_is_not_an_owned_terminal() { + let (mut program, _) = lower_source( + "function inspect(view source: list[int64]) returns list[int64]:\n list[int64] alias = view source\n return clone alias\n", + ); + let index = inspected(&program); + let root = local_named(&program.functions[index], "source"); + program.functions[index].locals[root.index() as usize].mutable = true; + program.functions[index].params[0].mutable = true; + let errors = crate::validate(&program).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("immutable view-parameter backing")), + "{errors:?}" + ); + } + + #[test] + fn projected_local_views_validate_path_against_declared_mir_origin() { + for invalid in [ + "owner", + "index", + "endpoint", + "root type", + "root identity", + "clone", + "forwarded source", + ] { + let (mut program, types) = lower_source( + &PROJECTED_ALIASES + .replace(" Envelope source =", " mutable Envelope source ="), + ); + let index = inspected(&program); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let duplicate = local_named(function, "duplicate"); + let borrowed = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + let owner_type = function.local(source).unwrap().ty; + if invalid == "forwarded source" { + function.locals[forwarded.index() as usize].view_source = Some(source); + } else { + let value = alias_initializer_mut(function, borrowed); + match invalid { + "owner" | "index" => { + let field = projection_field_mut(value); + let ExpressionKind::Field { + owner_type: owner, + field, + .. + } = &mut field.kind + else { + unreachable!(); + }; + if invalid == "owner" { + *owner = owner_type; + } else { + *field = jett_hir::FieldId::new(u32::MAX); + } + } + "endpoint" => projection_field_mut(value).ty = TypeInterner::STRING, + "root type" => projection_root_mut(value).ty = TypeInterner::STRING, + "root identity" => { + projection_root_mut(value).kind = ExpressionKind::Local(duplicate) + } + "clone" => { + *value = Expression { + kind: ExpressionKind::Clone(Box::new(value.clone())), + ty: value.ty, + span: value.span, + }; + } + _ => unreachable!(), + } + } + // Every forged ID remains structurally in range. Typed ownership + // validation must still reject a path or origin that was laundered. + crate::validate(&program).unwrap(); + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!( + error.contains("borrowed alias") || error.contains("borrowed projection"), + "{invalid}: {error}" + ); + } + } + + #[test] + fn projected_local_views_do_not_invent_nominal_refinement_proofs() { + let source = r#"type Unproven = list[int64] where false +struct Packet: + items: list[int64] +function inspect(view source: Packet) returns nothing: + list[int64] borrowed = view source.items + return nothing +"#; + for invalid in ["target", "view", "conversion", "coarsen", "declassify"] { + let (mut program, types) = lower_source(source); + let index = inspected(&program); + let unproven = types.type_ids().find(|ty| matches!(types.resolve(*ty), Type::Refinement { name, .. } if name.ends_with("Unproven"))).unwrap(); + let function = &mut program.functions[index]; + let borrowed = local_named(function, "borrowed"); + if matches!(invalid, "target" | "view" | "conversion") { + function.locals[borrowed.index() as usize].ty = unproven; + } + let value = alias_initializer_mut(function, borrowed); + match invalid { + "target" => {} + "view" => value.ty = unproven, + "conversion" | "coarsen" | "declassify" => { + let inner = Box::new(value.clone()); + let ty = if invalid == "conversion" { + unproven + } else { + value.ty + }; + let kind = match invalid { + "conversion" => ExpressionKind::InterfaceCoerce { + value: inner, + adapters: Vec::new(), + }, + "coarsen" => ExpressionKind::Coarsen(inner), + "declassify" => ExpressionKind::Declassify(inner), + _ => unreachable!(), + }; + *value = Expression { + kind, + ty, + span: value.span, + }; + } + _ => unreachable!(), + } + // The field endpoint, root ID/type and dense local table remain + // valid. The missing nominal/conversion proof is the defect. + crate::validate(&program).unwrap(); + let error = + validate_local_view_initializers(&program.functions[index], &types).unwrap_err(); + assert!(error.contains("borrowed"), "{invalid}: {error}"); + } + let sibling_source = r#"type Established = list[int64] where true +type Unproven = list[int64] where false +struct Packet: + items: Established +function inspect(view source: Packet) returns nothing: + Established borrowed = view source.items + return nothing +"#; + let (mut program, types) = lower_source(sibling_source); + let index = inspected(&program); + let unproven = types.type_ids().find(|ty| matches!(types.resolve(*ty), Type::Refinement { name, .. } if name.ends_with("Unproven"))).unwrap(); + let function = &mut program.functions[index]; + let borrowed = local_named(function, "borrowed"); + function.locals[borrowed.index() as usize].ty = unproven; + let value = alias_initializer_mut(function, borrowed); + *value = Expression { + kind: ExpressionKind::InterfaceCoerce { + value: Box::new(value.clone()), + adapters: Vec::new(), + }, + ty: unproven, + span: value.span, + }; + crate::validate(&program).unwrap(); + let error = + validate_local_view_initializers(&program.functions[index], &types).unwrap_err(); + assert!(error.contains("borrowed"), "sibling refinement: {error}"); + } + + #[test] + fn projected_local_views_preserve_immutable_chains_and_persistent_root_loans() { + for mutable in ["borrowed", "forwarded"] { + let (mut program, _) = lower_source( + &PROJECTED_ALIASES + .replace(" Envelope source =", " mutable Envelope source ="), + ); + let index = inspected(&program); + let local = local_named(&program.functions[index], mutable); + program.functions[index].locals[local.index() as usize].mutable = true; + let errors = crate::validate(&program).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("immutable")), + "{mutable}: {errors:?}" + ); + } + for invalid in ["root consume", "root rebind", "payload escape"] { + let (mut program, types) = lower_source( + &PROJECTED_ALIASES + .replace(" Envelope source =", " mutable Envelope source ="), + ); + let index = inspected(&program); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let duplicate = local_named(function, "duplicate"); + let forwarded = local_named(function, "forwarded"); + if invalid == "root rebind" { + let target = local_expression(function, source); + let replacement = local_expression(function, duplicate); + let exit = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(Some(_)))) + .unwrap(); + exit.statements.push(crate::Statement { + span: target.span, + kind: StatementKind::Assign { + target, + value: replacement, + }, + }); + } else { + let local = if invalid == "root consume" { + source + } else { + forwarded + }; + let value = local_expression(function, local); + replace_return(function, value); + } + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + let expected = if invalid == "payload escape" { + "cannot move borrowed native place" + } else { + "after creating a local view alias is not implemented" + }; + assert!(error.contains(expected), "{invalid}: {error}"); + } + } + + #[test] + fn projected_local_views_preserve_generic_field_facts_and_view_parameter_storage() { + for target in ["bytes", "list[int64]"] { + let source = format!( + "struct Carrier[T]:\n value: T\nfunction inspect(view source: Carrier[{target}]) returns {target}:\n {target} borrowed = view source.value\n {target} forwarded = borrowed\n return clone forwarded\n" + ); + let (program, types) = lower_source(&source); + crate::validate(&program).unwrap(); + let function = &program.functions[inspected(&program)]; + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + for name in ["source", "borrowed", "forwarded"] { + let local = local_named(function, name); + assert!( + !plan.owned_locals.contains(&(local.index() as usize)), + "{target}: {name}" + ); + } + } + } + + #[test] + fn projected_local_views_remap_initializer_root_and_immediate_alias_chain() { + let source = PROJECTED_ALIASES.replace( + " Envelope source =", + " for element in list():\n trace element\n mutable Envelope source =", + ); + let (mut program, types) = lower_source(&source); + let index = inspected(&program); + let before = local_named(&program.functions[index], "source"); + crate::prepare_native_sequences(&mut program, &types); + crate::validate(&program).unwrap(); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let borrowed = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + assert!(source.index() < before.index()); + assert_eq!(function.local(borrowed).unwrap().view_source, Some(source)); + assert_eq!( + function.local(forwarded).unwrap().view_source, + Some(borrowed) + ); + assert_eq!(function.view_root(forwarded), Some(source)); + let source_type = function.local(source).unwrap().ty; + let root = projection_root_mut(alias_initializer_mut(function, borrowed)); + assert_eq!(root.kind, ExpressionKind::Local(source)); + assert_eq!(root.ty, source_type); + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap(); + } + + #[test] + fn projected_local_views_require_initializer_proof_even_outside_reachable_cfg() { + for invalid in ["orphan", "unreachable path"] { + let (mut program, types) = lower_source(PROJECTED_ALIASES); + let index = inspected(&program); + let function = &mut program.functions[index]; + let borrowed = local_named(function, "borrowed"); + if invalid == "orphan" { + for block in &mut function.blocks { + block.statements.retain(|statement| !matches!(statement.kind, StatementKind::Let { local, .. } if local == borrowed)); + } + } else { + let mut value = alias_initializer_mut(function, borrowed).clone(); + projection_root_mut(&mut value).ty = TypeInterner::STRING; + let span = value.span; + let id = crate::BlockId(function.blocks.len() as u32); + function.blocks.push(crate::BasicBlock { + id, + statements: vec![crate::Statement { + kind: StatementKind::Let { + local: borrowed, + value, + }, + span, + }], + terminator: crate::Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }); + } + crate::validate(&program).unwrap(); + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!( + error.contains("borrowed alias") || error.contains("borrowed projection"), + "{invalid}: {error}" + ); + } + } + + #[test] + fn local_view_alias_plan_rejects_invalid_origins_and_initializer_backing() { + for invalid in [ + "missing", + "cycle", + "different initializer", + "allocating initializer", + ] { + let (mut program, types) = lower_source(ALIASES); + let index = inspected(&program); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let borrowed = local_named(function, "borrowed"); + let forwarded = local_named(function, "forwarded"); + match invalid { + "missing" => { + function.locals[borrowed.index() as usize].view_source = + Some(LocalId::new(u32::MAX)) + } + "cycle" => function.locals[borrowed.index() as usize].view_source = Some(forwarded), + _ => { + let replacement = local_expression(function, source); + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { local, value } if *local == forwarded => { + Some(value) + } + _ => None, + }) + .unwrap(); + *value = if invalid == "allocating initializer" { + Expression { + kind: ExpressionKind::Clone(Box::new(replacement.clone())), + ..replacement + } + } else { + replacement + }; + } + } + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!( + error.contains("origin") || error.contains("initializer"), + "{invalid}: {error}" + ); + } + } + + #[test] + fn local_view_alias_plan_rejects_owned_escape_before_test_copy_exceptions() { + for kind in [ + DeclarationKind::Function, + DeclarationKind::Verify, + DeclarationKind::Property, + ] { + let (mut program, types) = lower_source(ALIASES); + let index = inspected(&program); + let function = &mut program.functions[index]; + let borrowed = local_named(function, "forwarded"); + let value = local_expression(function, borrowed); + replace_return(function, value); + function.identity.declaration.kind = kind; + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!( + error.contains("cannot move borrowed native place"), + "{kind:?}: {error}" + ); + } + } + + #[test] + fn local_view_alias_plan_rejects_wrapped_qualified_string_owned_escapes() { + for ty in ["secret[string]", "Label"] { + let source = format!( + "type Label = string where true\nfunction consume(value: {ty}) returns nothing:\n return nothing\nfunction inspect(view source: {ty}) returns {ty}:\n {ty} borrowed = view source\n consume(clone borrowed)\n return clone borrowed\n" + ); + for kind in [ + DeclarationKind::Function, + DeclarationKind::Verify, + DeclarationKind::Property, + ] { + for call in [false, true] { + let (mut program, types) = lower_source(&source); + let index = inspected(&program); + let consume = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "consume") + .unwrap() + .id; + let function = &program.functions[index]; + MoveValuePlan::analyze(&program, function, &types).unwrap(); + let borrowed = local_named(function, "borrowed"); + let local = local_expression(function, borrowed); + assert!(is_string(&types, local.ty)); + assert_eq!( + wrapper_copies_result(&types, local.ty), + jett_typecheck::ownership::is_implicitly_copyable(&types, local.ty) + ); + assert!(!wrapper_copies_result(&types, local.ty)); + let view = Expression { + kind: ExpressionKind::View(Box::new(local.clone())), + ..local + }; + let function = &mut program.functions[index]; + function.identity.declaration.kind = kind; + if call { + // Retain the exact source-derived target/signature packet, + // then forge only the owning argument's physical tree. + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Evaluate(value) + if matches!(value.kind, ExpressionKind::Call { function, .. } if function == consume) => Some(value), + _ => None, + }) + .expect("checked consuming call"); + let ExpressionKind::Call { + args, ownership, .. + } = &mut value.kind + else { + unreachable!("selected call expression"); + }; + assert!(matches!(ownership, jett_hir::CallOwnership::Source(_))); + assert!(matches!(args[0].kind, ExpressionKind::Clone(_))); + args[0] = view; + } else { + replace_return(function, view); + } + let error = MoveValuePlan::analyze(&program, &program.functions[index], &types) + .unwrap_err(); + assert!( + error.contains("native view cannot escape into an owning value") + || error.contains("call ownership") + || error.contains("source operand") + || error.contains("source caller") + || error.contains("source occurrence"), + "{ty}, {kind:?}, call={call}: {error}" + ); + } + } + } + } + + #[test] + fn local_view_alias_plan_preserves_explicit_primitive_and_string_copies() { + let (program, types) = lower_source( + r#"function inspect(view number: int64, view text: string) returns string: + int64 number_copy = view number + string text_copy = view text + return text_copy +"#, + ); + let function = &program.functions[inspected(&program)]; + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + for name in ["number_copy", "text_copy"] { + let local = function.local(local_named(function, name)).unwrap(); + assert!(local.view_source.is_none()); + assert_eq!( + wrapper_copies_result(&types, local.ty), + jett_typecheck::ownership::is_implicitly_copyable(&types, local.ty) + ); + assert!(wrapper_copies_result(&types, local.ty)); + } + assert!( + plan.owned_locals + .contains(&(local_named(function, "text_copy").index() as usize)) + ); + } + + #[test] + fn local_view_alias_plan_borrows_callback_descriptors_and_rejects_their_owned_escape() { + let source = r#"function increment(value: int64) returns int64: + return value + 1 +function inspect() returns function(int64) returns int64: + function(int64) returns int64 source = increment + function(int64) returns int64 borrowed = view source + function(int64) returns int64 copied = clone borrowed + int64 called = borrowed(3) + return copied +"#; + for kind in [ + DeclarationKind::Function, + DeclarationKind::Verify, + DeclarationKind::Property, + ] { + let (mut program, types) = lower_source(source); + let index = inspected(&program); + let function = &program.functions[index]; + let borrowed = local_named(function, "borrowed"); + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(!plan.owned_locals.contains(&(borrowed.index() as usize))); + assert!( + plan.owned_locals + .contains(&(local_named(function, "source").index() as usize)) + ); + assert!( + plan.owned_locals + .contains(&(local_named(function, "copied").index() as usize)) + ); + let value = local_expression(function, borrowed); + let function = &mut program.functions[index]; + replace_return(function, value); + function.identity.declaration.kind = kind; + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + let expected = if kind == DeclarationKind::Function { + "cannot move borrowed native place" + } else { + "call ownership source context is not its lexical owner" + }; + assert!(error.contains(expected), "{kind:?}: {error}"); + } + } + + #[test] + fn local_view_alias_plan_limits_owner_changes_without_selecting_alias_expiry() { + for rebind in [false, true] { + let (mut program, types) = lower_source(ALIASES); + let index = inspected(&program); + let function = &mut program.functions[index]; + let source = local_named(function, "source"); + let value = local_expression(function, source); + if rebind { + let borrowed = local_named(function, "borrowed"); + let copied = local_expression(function, borrowed); + let assignment = crate::Statement { + span: value.span, + kind: StatementKind::Assign { + target: value, + value: Expression { + kind: ExpressionKind::Clone(Box::new(copied.clone())), + ..copied + }, + }, + }; + let exit = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(Some(_)))) + .unwrap(); + exit.statements.push(assignment); + } else { + replace_return(function, value); + } + let error = + MoveValuePlan::analyze(&program, &program.functions[index], &types).unwrap_err(); + assert!( + error.contains("after creating a local view alias is not implemented"), + "{rebind}: {error}" + ); + } + let (program, types) = lower_source( + r#"function inspect(flag: bool) returns list[int64]: + list[int64] source = list(1) + if flag: + return source + list[int64] borrowed = view source + return clone borrowed +"#, + ); + MoveValuePlan::analyze(&program, &program.functions[inspected(&program)], &types).unwrap(); + } +} diff --git a/crates/jett_mir/src/ordinary_borrowed_sums.rs b/crates/jett_mir/src/ordinary_borrowed_sums.rs new file mode 100644 index 00000000..109a45f3 --- /dev/null +++ b/crates/jett_mir/src/ordinary_borrowed_sums.rs @@ -0,0 +1,1145 @@ +//! Constructor-certified ordinary Handle views. These records grant no Resource custody. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; +use std::sync::Arc; + +#[cfg(test)] +mod tests; + +// Erased interfaces/callables and actors do not expose all runtime payload +// types through a sum's TypeId. Their ordinary projection proof remains pending. +fn ordinary_type_pending(types: &TypeInterner, ty: TypeId) -> bool { + fn visit( + types: &TypeInterner, + ty: TypeId, + seen: &mut std::collections::HashSet, + ) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Interface(_) + | Type::Function { .. } + | Type::Actor(_) + | Type::Capability(_) + | Type::TypeConstruction + | Type::Error => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) || visit(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, ty)| visit(types, *ty, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .any(|field| visit(types, field.ty, seen)), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .any(|variant| variant.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + Type::Machine(id) | Type::MachineState { machine: id, .. } => types + .resolve_machine(*id) + .states + .iter() + .any(|state| state.fields.iter().any(|(_, ty)| visit(types, *ty, seen))), + _ => false, + } + } + visit(types, ty, &mut std::collections::HashSet::new()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OrdinarySumPayloadPath { + OptionalSome, + ResultOk, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OrdinarySumProjectionSite { + block: BlockId, + statement_index: usize, +} +impl OrdinarySumProjectionSite { + pub fn block(self) -> BlockId { + self.block + } + pub fn statement_index(self) -> usize { + self.statement_index + } + pub(super) fn new(block: BlockId, statement_index: usize) -> Self { + Self { + block, + statement_index, + } + } +} + +#[derive(Debug, Clone)] +pub struct OrdinaryBorrowedSumProjection { + original_path: Vec, + original: Expression, + original_alias: Local, + original_backing: Local, + mutable_backing: Option, + alias: Local, + backing: Local, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: OrdinarySumProjectionSite, + observe: OrdinarySumProjectionSite, + project: OrdinarySumProjectionSite, + failure: Option, + alias_site: OrdinarySumProjectionSite, + failed: BlockId, + continuation: BlockId, + path: OrdinarySumPayloadPath, +} +impl OrdinaryBorrowedSumProjection { + pub fn alias(&self) -> LocalId { + self.alias.id + } + pub fn backing(&self) -> LocalId { + self.backing.id + } + pub fn source(&self) -> LocalId { + self.source.id + } + pub fn output(&self) -> LocalId { + self.output.id + } + pub fn tag(&self) -> LocalId { + self.tag.id + } + pub fn error(&self) -> Option { + self.error.as_ref().map(|local| local.id) + } + pub fn sum_type(&self) -> TypeId { + self.backing.ty + } + pub fn payload_type(&self) -> TypeId { + self.alias.ty + } + pub fn path(&self) -> OrdinarySumPayloadPath { + self.path + } + pub fn original_handle(&self) -> &Expression { + let E::View(handle) = &self.original.kind else { + unreachable!() + }; + handle + } + pub fn initialize_site(&self) -> OrdinarySumProjectionSite { + self.initialize + } + pub fn observe_site(&self) -> OrdinarySumProjectionSite { + self.observe + } + pub fn success_site(&self) -> OrdinarySumProjectionSite { + self.project + } + pub fn failure_site(&self) -> Option { + self.failure + } + + fn same(&self, other: &Self) -> bool { + self.original_path == other.original_path + && breakpoint_regions::expressions_equal(&self.original, &other.original) + && self.original_alias == other.original_alias + && self.original_backing == other.original_backing + && self.mutable_backing == other.mutable_backing + && self.alias == other.alias + && self.backing == other.backing + && self.source == other.source + && self.output == other.output + && self.tag == other.tag + && self.error == other.error + && self.initialize == other.initialize + && self.observe == other.observe + && self.project == other.project + && self.failure == other.failure + && self.alias_site == other.alias_site + && self.failed == other.failed + && self.continuation == other.continuation + && self.path == other.path + } + fn statement<'a>( + &self, + function: &'a Function, + site: OrdinarySumProjectionSite, + ) -> Result<&'a Statement, String> { + function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.statement_index)) + .ok_or_else(|| "ordinary borrowed Handle lost its exact statement site".into()) + } + fn current(&self, function: &Function) -> Result<(), String> { + for header in [ + &self.alias, + &self.backing, + &self.source, + &self.output, + &self.tag, + ] + .into_iter() + .chain(self.error.iter()) + { + if function.local(header.id) != Some(header) { + return Err("ordinary borrowed Handle changed a sealed local header".into()); + } + } + if (self.alias.mutable && self.original_alias.view_source.is_some()) + || self.backing.mutable != self.original_backing.mutable + || self.mutable_backing.is_some() != self.original_backing.mutable + || self.source.mutable + || self.output.mutable + || self.alias.view_source != self.original_alias.view_source.map(|_| self.backing.id) + || self.source.view_source != Some(self.backing.id) + || self.output.view_source != Some(self.backing.id) + || self.source.ty != self.backing.ty + || self.output.ty != self.alias.ty + || self.tag.ty != TypeInterner::BOOL + || self.tag.view_source.is_some() + { + return Err("ordinary borrowed Handle lost its immutable nonowning headers".into()); + } + if !matches!(&self.statement(function, self.initialize)?.kind, StatementKind::Let { local, value } + if *local == self.source.id && value.ty == self.source.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.source.ty + && matches!(inner.kind, E::Local(backing) if backing == self.backing.id))) + || !matches!(self.statement(function, self.observe)?.kind, StatementKind::SumTag { source, target } + if source == self.source.id && target == self.tag.id) + || !matches!(self.statement(function, self.project)?.kind, StatementKind::SumTake { source, target, success: true } + if source == self.source.id && target == self.output.id) + || !matches!(&self.statement(function, self.alias_site)?.kind, StatementKind::Let { local, value } + if *local == self.alias.id && value.ty == self.alias.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.output.ty + && matches!(inner.kind, E::Local(output) if output == self.output.id))) + { + return Err( + "ordinary borrowed Handle changed its exact initializer or projection".into(), + ); + } + let branch = &function.blocks[self.observe.block.index() as usize]; + if self.initialize.block != self.observe.block + || self.initialize.statement_index >= self.observe.statement_index + || self.alias_site.block != self.continuation + || !matches!(&branch.terminator.kind, TerminatorKind::Branch { condition, then_block, else_block } + if condition.ty == TypeInterner::BOOL && matches!(condition.kind, E::Local(tag) if tag == self.tag.id) + && *then_block == self.project.block && *else_block == self.failed && then_block != else_block) + || !matches!(function.blocks[self.project.block.index() as usize].terminator.kind, + TerminatorKind::Goto(target) if target == self.continuation) + { + return Err("ordinary borrowed Handle lost its exact selecting tag edge".into()); + } + match (&self.error, self.failure) { + (Some(error), Some(site)) + if error.ty == TypeInterner::STRING + && error.view_source.is_none() + && !error.mutable + && site.block == self.failed + && matches!(self.statement(function, site)?.kind, StatementKind::SumTake { source, target, success: false } + if source == self.source.id && target == error.id) => {} + (None, None) if self.path == OrdinarySumPayloadPath::OptionalSome => {} + _ => { + return Err( + "ordinary borrowed Handle changed its owning String failure companion".into(), + ); + } + } + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + if !cfg.reverse_postorder().contains(&self.initialize.block) + || !cfg.reverse_postorder().contains(&self.project.block) + || !cfg.reverse_postorder().contains(&self.alias_site.block) + || cfg.predecessors(self.project.block) != [self.observe.block] + || cfg.predecessors(self.failed) != [self.observe.block] + { + return Err( + "ordinary borrowed Handle is disconnected from its constructor guard".into(), + ); + } + let definitions = function.blocks.iter().flat_map(|block| &block.statements); + let alias_count = definitions + .clone() + .filter(|statement| { + matches!(statement.kind, + StatementKind::Let { local, .. } if local == self.alias.id) + }) + .count(); + let output_count = definitions + .filter(|statement| { + matches!(statement.kind, + StatementKind::SumTake { target, .. } if target == self.output.id) + }) + .count(); + if alias_count != 1 || output_count != 1 { + return Err( + "ordinary borrowed Handle has copied or missing current declarations".into(), + ); + } + Ok(()) + } + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = map + .get(id.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed its guard")?; + Ok(()) + }; + for site in [ + &mut self.initialize, + &mut self.observe, + &mut self.project, + &mut self.alias_site, + ] + .into_iter() + .chain(self.failure.iter_mut()) + { + remap(&mut site.block)?; + } + remap(&mut self.failed)?; + remap(&mut self.continuation) + } + fn remap_locals(&mut self, map: &[Option]) -> Result<(), String> { + for header in [ + &mut self.alias, + &mut self.backing, + &mut self.source, + &mut self.output, + &mut self.tag, + ] + .into_iter() + .chain(self.error.iter_mut()) + { + header.id = map + .get(header.id.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed a sealed header")?; + if let Some(source) = &mut header.view_source { + *source = map + .get(source.index() as usize) + .copied() + .flatten() + .ok_or("ordinary borrowed Handle canonical map removed its backing")?; + } + } + Ok(()) + } + fn shift(&mut self, block: BlockId, count: usize) { + for site in [ + &mut self.initialize, + &mut self.observe, + &mut self.project, + &mut self.alias_site, + ] + .into_iter() + .chain(self.failure.iter_mut()) + { + if site.block == block { + site.statement_index += count; + } + } + } +} + +#[derive(Debug, Clone)] +struct Declaration { + path: Vec, + value: Expression, + alias: Local, + backing: Local, + path_kind: OrdinarySumPayloadPath, + mutable_backing: Option, +} +#[derive(Default)] +struct Inventory { + declarations: Vec, +} +fn child(path: &[usize], kind: usize, index: usize) -> Vec { + let mut child = path.to_vec(); + child.extend([kind, index]); + child +} +impl Inventory { + fn block( + &mut self, + block: &hir::Block, + path: Vec, + function: &hir::Function, + types: &TypeInterner, + ) -> Result<(), String> { + for (index, statement) in block.statements.iter().enumerate() { + let at = child(&path, 0, index); + if let H::Let { local, value } = &statement.kind { + let alias = function + .locals + .get(local.index() as usize) + .filter(|header| header.id == *local) + .ok_or("ordinary borrowed Handle has no original local header")?; + let written_source = match &value.kind { + E::View(handle) => match &handle.kind { + E::Handle { target, .. } => match &target.kind { + E::View(backing) => match backing.kind { + E::Local(source) => Some(source), + _ => None, + }, + _ => None, + }, + _ => None, + }, + _ => None, + }; + if let Some(source) = written_source { + let backing = function + .locals + .get(source.index() as usize) + .filter(|header| header.id == source) + .ok_or("ordinary borrowed Handle has no original backing header")?; + if !ordinary_type_pending(types, backing.ty) + && let Some(handle) = hir::borrowed_sum_view_initializer( + value, source, backing.ty, alias.ty, types, + )? + { + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, alias.ty); + let mutable_backing = if backing.mutable { + Some(function.mutable_sum_backing(*local, value, source)? + .ok_or("mutable borrowed Handle lacks original checked owner receipt")?.clone()) + } else { + None + }; + if alias.mutable && !copyable { + return Err( + "ordinary borrowed Handle requires an immutable backing and view alias".into() + ); + } + if alias.view_source != if copyable { None } else { Some(source) } { + return Err( + "ordinary borrowed Handle changed its checked alias ownership mode" + .into(), + ); + } + let E::Handle { kind, .. } = &handle.kind else { + unreachable!() + }; + let path_kind = match kind { + hir::HandleKind::Optional => OrdinarySumPayloadPath::OptionalSome, + hir::HandleKind::Result => { + if !matches!(types.resolve(backing.ty), Type::Result(_, failure) if *failure == TypeInterner::STRING) + { + return Err("ordinary borrowed Handle failure companion requires String".into()); + } + OrdinarySumPayloadPath::ResultOk + } + _ => { + return Err( + "ordinary borrowed Handle has an unsupported sum kind".into() + ); + } + }; + if self.declarations.iter().any(|old| old.alias.id == *local) { + return Err( + "ordinary borrowed Handle lacks a unique original declaration" + .into(), + ); + } + self.declarations.push(Declaration { + path: at.clone(), + value: value.clone(), + alias: alias.clone(), + backing: backing.clone(), + path_kind, + mutable_backing, + }); + } + } + } + let mut values = Vec::new(); + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => values.push(value), + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + values.extend([target, value]) + } + H::Return(value) => values.extend(value.iter()), + H::If { + condition, + then_block, + else_block, + } => { + values.push(condition); + self.block(then_block, child(&at, 2, 0), function, types)?; + if let Some(block) = else_block { + self.block(block, child(&at, 2, 1), function, types)?; + } + } + H::While { condition, body } => { + values.push(condition); + self.block(body, child(&at, 2, 0), function, types)?; + } + H::For { iterable, body, .. } => { + values.push(iterable); + self.block(body, child(&at, 2, 0), function, types)?; + } + H::Match { scrutinee, arms } => { + values.push(scrutinee); + for (index, arm) in arms.iter().enumerate() { + self.block(&arm.body, child(&at, 2, index), function, types)?; + } + } + H::Assert { condition, message } => { + values.push(condition); + values.extend(message.iter()); + } + H::Breakpoint { condition, .. } => values.extend(condition.iter()), + H::Scope(block) => self.block(block, child(&at, 2, 0), function, types)?, + H::ReflectedTypeDispatch { type_info, arms } => { + values.push(type_info); + for (index, arm) in arms.iter().enumerate() { + self.block(&arm.body, child(&at, 2, index), function, types)?; + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + for (index, value) in values.into_iter().enumerate() { + self.expression(value, child(&at, 1, index), function, types)?; + } + } + Ok(()) + } + fn expression( + &mut self, + value: &Expression, + path: Vec, + function: &hir::Function, + types: &TypeInterner, + ) -> Result<(), String> { + let mut children: Vec<&Expression> = Vec::new(); + match &value.kind { + // Dedicated graph construction owns the sealed import body. + E::ActorGraphImport(_) => {} + E::Binary { left, right, .. } => children.extend([left.as_ref(), right.as_ref()]), + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => children.push(value), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => children.extend(args), + E::IndirectCall { callee, args, .. } => { + children.push(callee); + children.extend(args); + } + E::ActorMessage { actor, args, .. } => { + children.push(actor); + children.extend(args); + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + children.extend(fields) + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + children.extend(payloads) + } + E::MachineTransition { + source, payloads, .. + } => { + children.push(source); + children.extend(payloads); + } + E::ListConstruct { elements } => children.extend(elements), + E::MapConstruct { entries } => { + for entry in entries { + children.extend([&entry.key, &entry.value]); + } + } + E::Handle { + target, failure, .. + } => { + children.push(target); + self.block(failure, child(&path, 3, 0), function, types)?; + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + children.push(value); + } + } + } + E::Field { base, .. } => children.push(base), + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + for (index, value) in children.into_iter().enumerate() { + self.expression(value, child(&path, 1, index), function, types)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct CurrentSeal { + id: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + params: Vec, + capture_count: usize, + return_type: TypeId, + span: Span, + locals: Vec, + entry: BlockId, + blocks: Vec, + rows: Vec, +} +impl CurrentSeal { + fn new(function: &Function, rows: &[OrdinaryBorrowedSumProjection]) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + span: function.span, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + rows: rows.to_vec(), + } + } + fn matches(&self, function: &Function) -> bool { + self.id == function.id + && self.identity == function.identity + && self.debug_kind == function.debug_kind + && self.params == function.params + && self.capture_count == function.capture_count + && self.return_type == function.return_type + && self.span == function.span + && self.locals == function.locals + && self.entry == function.entry + && breakpoint_regions::blocks_equal(&self.blocks, &function.blocks) + } +} +#[derive(Debug, Clone)] +pub(super) struct Witness { + original: Arc, + declarations: Arc>, + rows: Vec, + sealed: Arc, +} +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.original, &other.original) + && Arc::ptr_eq(&self.declarations, &other.declarations) + && Arc::ptr_eq(&self.sealed, &other.sealed) + && same_rows(&self.rows, &other.rows) + } +} +fn same_rows( + left: &[OrdinaryBorrowedSumProjection], + right: &[OrdinaryBorrowedSumProjection], +) -> bool { + left.len() == right.len() && left.iter().zip(right).all(|(left, right)| left.same(right)) +} +impl Witness { + fn current(&self, function: &Function) -> Result<(), String> { + if !self.sealed.matches(function) || !same_rows(&self.rows, &self.sealed.rows) { + return Err( + "ordinary borrowed Handle differs from its independently sealed constructor graph" + .into(), + ); + } + if self.original.id != function.id + || self.original.identity != function.identity + || self.rows.len() != self.declarations.len() + { + return Err("ordinary borrowed Handle lost its original function or complete declaration inventory".into()); + } + for (index, row) in self.rows.iter().enumerate() { + if row.mutable_backing.as_ref() + != self + .original + .mutable_sum_backing( + row.original_alias.id, + &row.original, + row.original_backing.id, + )? + .as_ref() + .copied() + { + return Err("mutable sum backing changed its original source receipt".into()); + } + row.current(function)?; + if self.rows[..index] + .iter() + .any(|old| old.original_alias.id == row.original_alias.id) + { + return Err("ordinary borrowed Handle duplicated an original declaration".into()); + } + if self + .declarations + .iter() + .filter(|declaration| { + declaration.path == row.original_path + && declaration.alias == row.original_alias + && declaration.backing == row.original_backing + && declaration.mutable_backing == row.mutable_backing + && declaration.path_kind == row.path + && breakpoint_regions::expressions_equal(&declaration.value, &row.original) + }) + .count() + != 1 + || self + .original + .locals + .get(row.original_alias.id.index() as usize) + != Some(&row.original_alias) + || self + .original + .locals + .get(row.original_backing.id.index() as usize) + != Some(&row.original_backing) + || row.original_alias.ty != row.alias.ty + || row.original_backing.ty != row.backing.ty + { + return Err( + "ordinary borrowed Handle changed its exact original declaration association" + .into(), + ); + } + } + Ok(()) + } +} + +#[derive(Default, Clone)] +pub(super) struct Capture { + original: Option>, + declarations: Arc>, + rows: Vec, +} +pub(super) struct Seed { + pub original: Expression, + pub backing: Local, + declaration: Declaration, +} +impl Capture { + pub(super) fn authenticated( + function: &hir::Function, + types: &TypeInterner, + ) -> Result { + let mut inventory = Inventory::default(); + inventory.block(&function.body, Vec::new(), function, types)?; + if inventory.declarations.is_empty() { + return Ok(Self::default()); + } + Ok(Self { + original: Some(Arc::new(function.clone())), + declarations: Arc::new(inventory.declarations), + rows: Vec::new(), + }) + } + pub(super) fn seed(&self, local: LocalId, value: &Expression) -> Result, String> { + let Some(declaration) = self + .declarations + .iter() + .find(|declaration| declaration.alias.id == local) + else { + return Ok(None); + }; + if !breakpoint_regions::expressions_equal(&declaration.value, value) + || self.rows.iter().any(|row| row.original_alias.id == local) + { + return Err( + "ordinary borrowed Handle changed or copied its original initializer".into(), + ); + } + Ok(Some(Seed { + original: declaration.value.clone(), + backing: declaration.backing.clone(), + declaration: declaration.clone(), + })) + } + #[allow(clippy::too_many_arguments)] + pub(super) fn record( + &mut self, + seed: Seed, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: OrdinarySumProjectionSite, + observe: OrdinarySumProjectionSite, + project: OrdinarySumProjectionSite, + failure: Option, + alias_site: OrdinarySumProjectionSite, + failed: BlockId, + continuation: BlockId, + ) { + self.rows.push(OrdinaryBorrowedSumProjection { + original_path: seed.declaration.path, + original: seed.original, + original_alias: seed.declaration.alias.clone(), + original_backing: seed.declaration.backing.clone(), + mutable_backing: seed.declaration.mutable_backing.clone(), + alias: seed.declaration.alias, + backing: seed.declaration.backing, + source, + output, + tag, + error, + initialize, + observe, + project, + failure, + alias_site, + failed, + continuation, + path: seed.declaration.path_kind, + }); + } + pub(super) fn finish(self, function: &Function) -> Result, String> { + let Some(original) = self.original else { + return Ok(None); + }; + let sealed = Arc::new(CurrentSeal::new(function, &self.rows)); + let witness = Witness { + original, + declarations: self.declarations, + rows: self.rows, + sealed, + }; + witness.current(function)?; + Ok(Some(witness)) + } +} + +pub(super) fn has_records(function: &Function) -> bool { + function.ordinary_borrowed_sums.is_some() +} +pub(super) fn validate_current(function: &Function) -> Result<(), String> { + function + .ordinary_borrowed_sums + .as_ref() + .map_or(Ok(()), |witness| witness.current(function)) +} +pub(super) fn validate(function: &Function, types: &TypeInterner) -> Result<(), String> { + validate_current(function)?; + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::SumTag { source, .. } | StatementKind::SumTake { source, .. } = + statement.kind + && function.is_view_local(source) + && let Some(header) = function.local(source) + && !resource_type_pending(types, header.ty) + && function + .ordinary_borrowed_sum_projection(block.id, index)? + .is_none() + { + return Err( + "ordinary borrowed sum operation has no constructor-certified projection" + .into(), + ); + } + } + } + let Some(witness) = &function.ordinary_borrowed_sums else { + return Ok(()); + }; + for declaration in witness.declarations.iter() { + if ordinary_type_pending(types, declaration.backing.ty) + || hir::borrowed_sum_view_initializer( + &declaration.value, + declaration.backing.id, + declaration.backing.ty, + declaration.alias.ty, + types, + )? + .is_none() + { + return Err( + "ordinary borrowed Handle changed its Resource-free original sum type".into(), + ); + } + let copyable = + jett_typecheck::ownership::is_implicitly_copyable(types, declaration.alias.ty); + if declaration.alias.view_source + != if copyable { + None + } else { + Some(declaration.backing.id) + } + { + return Err( + "ordinary borrowed Handle changed its checked final copy or view mode".into(), + ); + } + if declaration.path_kind == OrdinarySumPayloadPath::ResultOk + && !matches!(types.resolve(declaration.backing.ty), Type::Result(_, failure) if *failure == TypeInterner::STRING) + { + return Err("ordinary borrowed Handle changed its exact String failure type".into()); + } + } + Ok(()) +} +impl Function { + /// Read only after the complete constructor graph and original association are authenticated. + pub fn ordinary_borrowed_sum_projection( + &self, + block: BlockId, + statement_index: usize, + ) -> Result, String> { + let Some(witness) = &self.ordinary_borrowed_sums else { + return Ok(None); + }; + witness.current(self)?; + let site = OrdinarySumProjectionSite::new(block, statement_index); + Ok(witness + .rows + .iter() + .find(|row| row.observe == site || row.project == site || row.failure == Some(site))) + } + pub fn ordinary_borrowed_sum_alias( + &self, + local: LocalId, + value: &Expression, + ) -> Result { + let Some(witness) = &self.ordinary_borrowed_sums else { + return Ok(false); + }; + witness.current(self)?; + Ok(witness.rows.iter().any(|row| row.alias.id == local + && matches!(&self.blocks[row.alias_site.block.index() as usize].statements[row.alias_site.statement_index].kind, + StatementKind::Let { local: defined, value: current } if *defined == local && std::ptr::eq(current, value)))) + } + pub(crate) fn ordinary_copied_sum_intermediates( + &self, + local: LocalId, + value: &Expression, + ) -> Result, String> { + if !self.ordinary_borrowed_sum_alias(local, value)? { + return Ok(None); + } + let witness = self + .ordinary_borrowed_sums + .as_ref() + .expect("authenticated ordinary alias"); + Ok(witness + .rows + .iter() + .find(|row| row.alias.id == local && row.original_alias.view_source.is_none()) + .map(|row| [row.source.id, row.output.id])) + } +} + +fn transition( + function: &mut Function, + before: &Function, + expected: &Function, + rows: Vec, +) -> Result<(), String> { + let original = before + .ordinary_borrowed_sums + .as_ref() + .ok_or("ordinary borrowed Handle has no before witness")?; + original.current(before)?; + if function.ordinary_borrowed_sums.as_ref() != Some(original) + || !CurrentSeal::new(expected, &rows).matches(function) + { + return Err( + "ordinary borrowed Handle preparation differs from its exact authenticated edit".into(), + ); + } + // Identity canonical edits preserve the private proof identity as well as the graph. + if original.sealed.matches(function) && same_rows(&rows, &original.rows) { + original.current(function)?; + return Ok(()); + } + let mut witness = original.clone(); + witness.rows = rows; + witness.sealed = Arc::new(CurrentSeal::new(expected, &witness.rows)); + witness.current(function)?; + function.ordinary_borrowed_sums = Some(witness); + Ok(()) +} +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + witness.current(before)?; + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let mut canonical = vec![None; before.blocks.len()]; + let mut next = 0; + for block in &before.blocks { + if cfg.reverse_postorder().contains(&block.id) { + canonical[block.id.index() as usize] = Some(BlockId(next)); + next += 1; + } + } + if map != canonical { + return Err("ordinary borrowed Handle block map is not exact reachable compaction".into()); + } + let mut expected = before.clone(); + expected + .blocks + .retain(|block| map[block.id.index() as usize].is_some()); + let remap = |id: &mut BlockId| { + *id = map[id.index() as usize].expect("reachable edge remains reachable"); + }; + remap(&mut expected.entry); + for block in &mut expected.blocks { + remap(&mut block.id); + sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_blocks(map)?; + } + transition(function, before, &expected, rows) +} +pub(super) fn remap_locals( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + witness.current(before)?; + if map.len() != before.locals.len() { + return Err("ordinary borrowed Handle local map has a foreign width".into()); + } + let mut next = 0; + for id in map.iter().flatten() { + if id.index() as usize != next { + return Err("ordinary borrowed Handle local map is not dense compaction".into()); + } + next += 1; + } + let mut expected = before.clone(); + expected + .locals + .retain(|local| map[local.id.index() as usize].is_some()); + let mut valid = true; + let mut remap = |id: &mut LocalId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } else { + valid = false; + } + }; + for parameter in &mut expected.params { + remap(&mut parameter.local); + } + for local in &mut expected.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + let mut floor = |floor: &mut u32| { + if (*floor as usize) <= map.len() { + *floor = map[..*floor as usize] + .iter() + .filter(|id| id.is_some()) + .count() as u32; + } + }; + for block in &mut expected.blocks { + sequences::prune::block_locals(block, &mut remap, &mut floor); + } + if !valid { + return Err("ordinary borrowed Handle local map removed an execution read".into()); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_locals(map)?; + } + transition(function, before, &expected, rows) +} +pub(super) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let Some(witness) = &before.ordinary_borrowed_sums else { + return Ok(()); + }; + validate(before, types)?; + // The existing finite edit validator checks every block, prefix and redirected + // edge against the exact original ForEach. The shadow grants no breakpoint proof. + let mut shadow = function.clone(); + breakpoint_regions::sequence_transition(&mut shadow, before, edit.clone(), types)?; + if function.locals.len() < before.locals.len() + || function.locals[..before.locals.len()] != before.locals + || function.params != before.params + || function.entry != before.entry + { + return Err( + "ordinary borrowed Handle sequence preparation changed existing headers".into(), + ); + } + let mut rows = witness.rows.clone(); + if let Some((block, prefix)) = &edit.prefix { + for row in &mut rows { + row.shift(*block, prefix.len()); + } + } + transition(function, before, &shadow, rows) +} diff --git a/crates/jett_mir/src/ordinary_borrowed_sums/mutable_tests.rs b/crates/jett_mir/src/ordinary_borrowed_sums/mutable_tests.rs new file mode 100644 index 00000000..5ef2bd1c --- /dev/null +++ b/crates/jett_mir/src/ordinary_borrowed_sums/mutable_tests.rs @@ -0,0 +1,350 @@ +use super::*; + +const OWNED: &str = include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/03_mutable_owned_scoped_forwarded.jett" +); +const COPIED: &str = include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/15_mutable_backing_copied_payloads.jett" +); +const REPLACED: &str = include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums_mutable/16_copied_payload_survives_shell_replacement.jett" +); + +fn prepare_source03_for_ownership_plans(program: &mut crate::Program, types: &TypeInterner) { + let has_for = |program: &crate::Program| { + program + .functions + .iter() + .filter(|function| function.identity.declaration.namespace == "app") + .flat_map(|function| &function.blocks) + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + }; + assert!( + has_for(program), + "Source03 retains its genuine For loop before preparation" + ); + crate::validate_call_ownership(program, types).unwrap(); + for function in &program.functions { + validate(function, types).unwrap(); + } + // Like the original Source03 test, defer Copy/Move plans until canonical + // sequence lowering has replaced the real ForEach terminator. + crate::prepare_native_sequences(program, types); + assert!( + !has_for(program), + "Source03 must complete its actual For lowering" + ); +} + +#[test] +fn fresh_mutable_sum_lowering_requires_the_real_hir_owner_receipt() { + let source = r#"namespace app +function first() returns string: + mutable result[string, string] source = ok("first") + string alias = view((view source) handle error: return error) + return alias +function second() returns string: + mutable result[string, string] source = ok("second") + string alias = view((view source) handle error: return error) + return alias +"#; + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + resolved + .diagnostics + .iter() + .chain(&checked.diagnostics) + .all(|error| error.severity != jett_diagnostics::Severity::Error) + ); + let high = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .unwrap(); + crate::lower(&high, &checked.interner).unwrap(); + for invalid in ["missing", "foreign"] { + let mut changed = high.clone(); + if invalid == "missing" { + changed.functions[0].mutable_sum_backings.clear(); + } else { + changed.functions[0].mutable_sum_backings = + high.functions[1].mutable_sum_backings.clone(); + } + let errors = crate::lower(&changed, &checked.interner).unwrap_err(); + assert!( + errors.iter().any(|error| error.message.contains("mutable") + && (error.message.contains("receipt") || error.message.contains("context"))), + "{invalid}: {errors:?}" + ); + } +} + +#[test] +fn mutable_sum_roots_preserve_original_rows_and_nonowning_payload_liveness() { + for (fixture, source) in [OWNED, COPIED, REPLACED].into_iter().enumerate() { + for release in [false, true] { + let (mut program, types) = lowered(source, release); + if fixture == 0 { + prepare_source03_for_ownership_plans(&mut program, &types); + } + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + } + } + assert_plans(&program, &types, fixture + 100); + let mut mutable_rows = 0; + for function in &program.functions { + let Some(witness) = &function.ordinary_borrowed_sums else { + continue; + }; + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + for row in &witness.rows { + if !row.original_backing.mutable { + continue; + } + mutable_rows += 1; + assert!(row.mutable_backing.is_some()); + assert!(function.local(row.backing()).unwrap().mutable); + assert!( + plan.owned_locals + .contains(&(row.backing().index() as usize)) + ); + for local in [row.source(), row.output()] { + assert!(!function.local(local).unwrap().mutable); + assert_eq!( + function.local(local).unwrap().view_source, + Some(row.backing()) + ); + assert!(!plan.owned_locals.contains(&(local.index() as usize))); + } + let alias = function.local(row.alias()).unwrap(); + if alias.view_source.is_some() { + assert!(!alias.mutable); + assert!(!plan.owned_locals.contains(&(alias.id.index() as usize))); + let mut observed = false; + for live in plan + .live_in + .iter() + .chain(&plan.live_out) + .chain(plan.live_after_statement.iter().flatten()) + { + if live.contains(&(row.alias().index() as usize)) { + observed = true; + assert!(live.contains(&(row.backing().index() as usize))); + } + } + assert!(observed); + } else { + assert_eq!( + plan.owned_locals.contains(&(alias.id.index() as usize)), + alias.ty == TypeInterner::STRING + ); + let live = &plan.live_after_statement + [row.alias_site.block.index() as usize] + [row.alias_site.statement_index]; + assert!(!live.contains(&(row.source().index() as usize))); + assert!(!live.contains(&(row.output().index() as usize))); + } + } + } + assert!( + mutable_rows > 0, + "Source variant must reach the mutable guard" + ); + } + } + } +} + +#[test] +fn mutable_sum_rows_require_original_proof_even_after_test_only_current_reseal() { + let (program, types) = lowered(OWNED, false); + for invalid in ["missing", "foreign", "header", "owning output"] { + let mut changed = program.clone(); + let selected = changed + .functions + .iter() + .position(|function| { + function + .ordinary_borrowed_sums + .as_ref() + .is_some_and(|witness| { + witness.rows.iter().any(|row| row.original_backing.mutable) + }) + }) + .unwrap(); + let function = &mut changed.functions[selected]; + let witness = function.ordinary_borrowed_sums.as_mut().unwrap(); + let index = witness + .rows + .iter() + .position(|row| row.original_backing.mutable) + .unwrap(); + let row = witness.rows[index].clone(); + match invalid { + "missing" => witness.rows[index].mutable_backing = None, + "foreign" => { + witness.rows[index].mutable_backing = witness + .rows + .iter() + .find(|other| other.alias() != row.alias()) + .and_then(|other| other.mutable_backing.clone()) + } + "header" => function.locals[row.backing().index() as usize].mutable = false, + "owning output" => function.locals[row.output().index() as usize].view_source = None, + _ => unreachable!(), + } + let rows = function + .ordinary_borrowed_sums + .as_ref() + .unwrap() + .rows + .clone(); + let resealed = Arc::new(CurrentSeal::new(function, &rows)); + function.ordinary_borrowed_sums.as_mut().unwrap().sealed = resealed; + assert!(validate(function, &types).is_err(), "{invalid}"); + assert!( + MoveValuePlan::analyze(&changed, &changed.functions[selected], &types).is_err(), + "{invalid}" + ); + } +} + +#[test] +fn mutable_sum_persistent_alias_still_blocks_root_replacement_after_last_read() { + let (mut program, types) = lowered(OWNED, false); + prepare_source03_for_ownership_plans(&mut program, &types); + let index = program + .functions + .iter() + .position(|function| function.identity.declaration.name == "inspect_optional") + .unwrap(); + MoveValuePlan::analyze(&program, &program.functions[index], &types) + .expect("the prepared unmodified baseline must pass before the illegal owner write"); + let mut changed = program.clone(); + let function = &mut changed.functions[index]; + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + let target = Expression { + kind: E::Local(row.backing()), + ty: row.sum_type(), + span: function.span, + }; + let replacement = Expression { + kind: E::OptionalNone, + ty: row.sum_type(), + span: function.span, + }; + let failure_blocks = function + .ordinary_borrowed_sums + .as_ref() + .unwrap() + .rows + .iter() + .map(|row| row.failed) + .collect::>(); + let exit = function.blocks.iter_mut().filter(|block| + matches!(block.terminator.kind, TerminatorKind::Return(Some(ref value)) if matches!(value.kind, E::Nothing)) + && !failure_blocks.contains(&block.id)).last().unwrap(); + exit.statements.push(Statement { + kind: StatementKind::Assign { + target, + value: replacement, + }, + span: function.span, + }); + // Only a unit test may bypass the complete current-tree equality. The real + // original declaration/row proof stays intact, and Flow must still refuse + // mutation with its persistent user alias, even after that alias's last read. + let rows = function + .ordinary_borrowed_sums + .as_ref() + .unwrap() + .rows + .clone(); + let resealed = Arc::new(CurrentSeal::new(function, &rows)); + function.ordinary_borrowed_sums.as_mut().unwrap().sealed = resealed; + let error = MoveValuePlan::analyze(&changed, &changed.functions[index], &types).unwrap_err(); + assert!( + error.contains("after creating a local view alias"), + "{error}" + ); +} + +#[test] +fn mutable_owned_sum_receipt_survives_real_nonidentity_callback_compaction() { + // Reuse the canonical dead-slot/callback composition. Only the original + // parameter's access/acquisition and its actual call change to owned mutable. + let source = CALLBACK_COMPACTION + .replace("inspect(view source:", "inspect(mutable source:") + .replace("inspect(view source)", "inspect(source)"); + for release in [false, true] { + let (mut program, types) = lowered(&source, release); + let original = guarded_function(&program).clone(); + let original_row = original.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + assert!(original_row.original_backing.mutable); + assert!(original_row.mutable_backing.is_some()); + let retained = original + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let callback = original + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id; + assert!(crate::sequences::prune::unused_locals( + guarded_function_mut(&mut program) + )); + let compacted = guarded_function(&program); + let row = &compacted.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + assert_eq!(compacted.locals.len(), original.locals.len() - 2); + assert!( + !compacted + .locals + .iter() + .any(|local| local.name == "dead" || local.name == "value") + ); + assert_eq!( + compacted + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id + .index(), + retained.index() - 1 + ); + assert_eq!( + compacted + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id + .index(), + callback.index() - 2 + ); + // The generated projection headers follow the removed Source slots. + // These comparisons make an identity/no-op preparation fail the test. + assert_eq!(row.source().index(), original_row.source().index() - 2); + assert_eq!(row.output().index(), original_row.output().index() - 2); + assert_ne!(row.output(), original_row.output()); + assert_eq!(row.mutable_backing, original_row.mutable_backing); + assert_eq!(assert_plans(&program, &types, 116), 1); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(assert_plans(&program, &types, 116), 1); + } +} diff --git a/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs b/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs new file mode 100644 index 00000000..66be9ae2 --- /dev/null +++ b/crates/jett_mir/src/ordinary_borrowed_sums/tests.rs @@ -0,0 +1,1281 @@ +use super::*; +use crate::copy_values::CopyValuePlan; +use crate::move_values::MoveValuePlan; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::{ExpressionKind as E, HandleKind}; +use jett_types::{Type, TypeInterner}; +use std::collections::HashMap; + +const SOURCES: [&str; 12] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/01_optional_branches.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/02_result_branches.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/03_owned_scoped_forwarded.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/04_named_prefix_return.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/05_optional_terminal_prefix.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/06_result_terminal_prefix.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/07_outer_pending_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/08_outer_pending_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/09_outer_pending_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/10_outer_pending_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/11_pending_optional_payload.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/ordinary_borrowed_sums/12_pending_result_payload.jett" + ), +]; + +fn lowered(source: &str, release: bool) -> (crate::Program, TypeInterner) { + let project = FileId::new(0); + let list = FileId::new(jett_common::STDLIB_FILE_ID_START); + let bytes = FileId::new(jett_common::STDLIB_FILE_ID_START + 1); + let mut parsed = jett_parser::parse(include_str!("../../../../stdlib/list.jett"), list); + let mut byte_module = jett_parser::parse(include_str!("../../../../stdlib/bytes.jett"), bytes); + let mut primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(byte_module.errors.is_empty(), "{:?}", byte_module.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + parsed.module.items.append(&mut byte_module.module.items); + parsed.module.items.append(&mut primary.module.items); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let high = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([ + (project, SourceOrigin::Project), + (list, SourceOrigin::Stdlib), + (bytes, SourceOrigin::Stdlib), + ]), + ) + .expect("genuine frozen Source and shipped stdlib HIR"); + ( + crate::lower(&high, &checked.interner).expect("genuine borrowed Handle MIR"), + checked.interner, + ) +} + +fn guarded_function(program: &crate::Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.ordinary_borrowed_sums.is_some() + }) + .expect("original ordinary borrowed declaration") +} + +fn guarded_function_mut(program: &mut crate::Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.ordinary_borrowed_sums.is_some() + }) + .expect("original ordinary borrowed declaration") +} + +fn assert_plans(program: &crate::Program, types: &TypeInterner, fixture: usize) -> usize { + crate::validate_call_ownership(program, types).unwrap(); + let mut list_rows = 0; + for function in &program.functions { + let Some(witness) = &function.ordinary_borrowed_sums else { + continue; + }; + let copy = CopyValuePlan::analyze_storage(function, types, Some(program)) + .unwrap_or_else(|error| panic!("Source{fixture:02} Copy: {error}")); + let plan = MoveValuePlan::analyze(program, function, types) + .unwrap_or_else(|error| panic!("Source{fixture:02} Move: {error}")); + assert_eq!(plan.owned_locals, copy.owned_locals); + for row in &witness.rows { + list_rows += usize::from(matches!(types.resolve(row.payload_type()), Type::List(_))); + assert_eq!(function.local(row.backing()).unwrap().ty, row.sum_type()); + for local in [row.source(), row.output()] { + let header = function.local(local).unwrap(); + assert!(!header.mutable); + assert_eq!(header.view_source, Some(row.backing())); + assert!(function.is_view_local(local)); + assert!(!plan.owned_locals.contains(&(local.index() as usize))); + assert!(!copy.owned_locals.contains(&(local.index() as usize))); + } + let alias = function.local(row.alias()).unwrap(); + assert_eq!(alias.mutable, row.original_alias.mutable); + if alias.view_source.is_some() { + assert!( + !alias.mutable, + "a true borrowed final alias remains immutable" + ); + } + assert_eq!( + alias.view_source, + row.original_alias.view_source.map(|_| row.backing()) + ); + let owned_alias = alias.view_source.is_none() && alias.ty == TypeInterner::STRING; + assert_eq!( + plan.owned_locals.contains(&(alias.id.index() as usize)), + owned_alias + ); + assert_eq!( + copy.owned_locals.contains(&(alias.id.index() as usize)), + owned_alias + ); + assert_eq!(function.local(row.source()).unwrap().ty, row.sum_type()); + assert_eq!(function.local(row.output()).unwrap().ty, row.payload_type()); + assert_eq!(function.local(row.alias()).unwrap().ty, row.payload_type()); + let initialization = row.initialize_site(); + assert!( + matches!(&function.blocks[initialization.block().index() as usize].statements[initialization.statement_index()].kind, StatementKind::Let { local, value } if *local == row.source() && matches!(&value.kind, E::View(inner) if matches!(inner.kind, E::Local(backing) if backing == row.backing()))) + ); + let observed = row.observe_site(); + assert!( + matches!(function.blocks[observed.block().index() as usize].statements[observed.statement_index()].kind, StatementKind::SumTag { source, target } if source == row.source() && target == row.tag()) + ); + let projected = row.success_site(); + assert!( + matches!(function.blocks[projected.block().index() as usize].statements[projected.statement_index()].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + assert_eq!( + function + .ordinary_borrowed_sum_projection( + projected.block(), + projected.statement_index() + ) + .unwrap() + .unwrap() + .alias(), + row.alias() + ); + let alias_value = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == row.alias() => Some(value), + _ => None, + }) + .unwrap(); + assert!( + function + .ordinary_borrowed_sum_alias(row.alias(), alias_value) + .unwrap() + ); + let E::Handle { kind, .. } = &row.original_handle().kind else { + panic!("complete original Handle") + }; + match ( + types.resolve(row.sum_type()), + kind, + row.error(), + row.failure_site(), + ) { + (Type::Optional(payload), HandleKind::Optional, None, None) => { + assert_eq!(*payload, row.payload_type()) + } + (Type::Result(payload, failure), HandleKind::Result, Some(error), Some(site)) => { + assert_eq!(*payload, row.payload_type()); + assert_eq!(*failure, TypeInterner::STRING); + let header = function.local(error).unwrap(); + assert_eq!(header.ty, TypeInterner::STRING); + assert!(header.view_source.is_none()); + assert!(plan.owned_locals.contains(&(error.index() as usize))); + assert!(copy.owned_locals.contains(&(error.index() as usize))); + assert!(copy.temporary_slots > 0); + assert!( + matches!(function.blocks[site.block().index() as usize].statements[site.statement_index()].kind, StatementKind::SumTake { source, target, success: false } if source == row.source() && target == error) + ); + assert_eq!( + function + .ordinary_borrowed_sum_projection(site.block(), site.statement_index()) + .unwrap() + .unwrap() + .alias(), + row.alias() + ); + } + _ => panic!("exact sum, payload and owned failure companion"), + } + for (planner, live_in, live_out, live_after) in [ + ( + "Move", + &plan.live_in, + &plan.live_out, + &plan.live_after_statement, + ), + ( + "Copy", + ©.live_in, + ©.live_out, + ©.live_after_statement, + ), + ] { + let mut observed_live = false; + for live in live_in + .iter() + .chain(live_out) + .chain(live_after.iter().flatten()) + { + if (alias.view_source.is_some() + && live.contains(&(row.alias().index() as usize))) + || live.contains(&(row.output().index() as usize)) + || live.contains(&(row.source().index() as usize)) + { + observed_live = true; + assert!( + live.contains(&(row.backing().index() as usize)), + "Source{fixture:02} {planner} backing must outlive its payload view" + ); + } + } + assert!( + observed_live, + "Source{fixture:02} {planner} visits a live projection" + ); + } + assert_eq!( + plan.owned_locals + .contains(&(row.backing().index() as usize)), + !function.is_view_local(row.backing()) + ); + } + } + list_rows +} + +#[test] +fn ordinary_borrowed_sums_all_frozen_sources_keep_nonowning_payload_and_owned_failure_plans() { + let expected = [2, 2, 9, 2, 1, 1, 1, 1, 1, 1, 1, 1]; + for release in [false, true] { + for (index, source) in SOURCES.into_iter().enumerate() { + let (mut program, types) = lowered(source, release); + if index != 2 { + assert_eq!(assert_plans(&program, &types, index + 1), expected[index]); + } else { + crate::validate_call_ownership(&program, &types).unwrap(); + } + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "Source{:02} release={release}", + index + 1 + ); + assert!( + crate::sequences::prune::unused_locals(function), + "Source{:02} local remap release={release}", + index + 1 + ); + } + assert_eq!(assert_plans(&program, &types, index + 1), expected[index]); + } + } +} + +#[test] +fn ordinary_borrowed_sums_owned_scoped_forwarded_source_keeps_original_owner_across_real_loop_preparation() + { + for release in [false, true] { + let (mut program, types) = lowered(SOURCES[2], release); + let has_for = |program: &crate::Program| { + program + .functions + .iter() + .filter(|function| function.identity.declaration.namespace == "app") + .flat_map(|function| &function.blocks) + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + }; + assert!(has_for(&program)); + crate::validate_call_ownership(&program, &types).unwrap(); + crate::prepare_native_sequences(&mut program, &types); + assert!( + !has_for(&program), + "Source03 must complete its actual For lowering" + ); + for function in &program.functions { + if function.identity.declaration.namespace != "app" + || !matches!( + function.identity.declaration.name.as_str(), + "inspect_optional" | "inspect_result" + ) + { + continue; + } + let named = |name: &str| { + function + .locals + .iter() + .find(|local| local.name == name) + .unwrap() + .id + }; + let source = named("source"); + let alias = named("alias"); + let forwarded = named("forwarded"); + let copied = named("copied"); + assert_eq!(function.local(alias).unwrap().view_source, Some(source)); + assert_eq!(function.local(forwarded).unwrap().view_source, Some(alias)); + assert!(function.local(copied).unwrap().view_source.is_none()); + let plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(source.index() as usize))); + assert!(plan.owned_locals.contains(&(copied.index() as usize))); + assert!(!plan.owned_locals.contains(&(alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(forwarded.index() as usize))); + let mut used = false; + for live in plan.live_in.iter().chain(&plan.live_out).chain( + plan.live_after_statement + .iter() + .flat_map(|block| block.iter()), + ) { + if live.contains(&(forwarded.index() as usize)) { + used = true; + assert!(live.contains(&(alias.index() as usize))); + assert!(live.contains(&(source.index() as usize))); + } + } + assert!(used); + } + assert_eq!(assert_plans(&program, &types, 3), 9); + } +} + +#[test] +fn ordinary_borrowed_sums_scalar_count_preserves_checked_copyable_alias_mode() { + for release in [false, true] { + let (mut program, types) = lowered(SOURCES[2], release); + crate::prepare_native_sequences(&mut program, &types); + assert_eq!(assert_plans(&program, &types, 3), 9); + let scalar_rows = program + .functions + .iter() + .filter_map(|function| { + function + .ordinary_borrowed_sums + .as_ref() + .map(|witness| (function, witness)) + }) + .flat_map(|(function, witness)| witness.rows.iter().map(move |row| (function, row))) + .filter(|(_, row)| row.payload_type() == TypeInterner::INT64) + .collect::>(); + assert_eq!( + scalar_rows.len(), + 1, + "Source03 count[int64] has its own constructor record" + ); + let (function, row) = scalar_rows[0]; + assert_eq!(function.identity.declaration.name, "count"); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + assert_eq!( + function.local(row.source()).unwrap().view_source, + Some(row.backing()) + ); + let move_plan = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!( + !move_plan + .owned_locals + .contains(&(row.alias().index() as usize)) + ); + assert!( + !move_plan + .owned_locals + .contains(&(row.output().index() as usize)) + ); + } +} + +const STRING_COPY: &str = r#"namespace app +function inspect(view source: optional[string]) returns string: + string alias = view((view source) handle: + return "absent" + ) + return alias +function main() returns string: + optional[string] source = some("present") + return inspect(view source) +"#; + +#[test] +fn ordinary_borrowed_sums_string_success_copies_into_owned_final_alias() { + for release in [false, true] { + let (mut program, types) = lowered(STRING_COPY, release); + assert_eq!(assert_plans(&program, &types, 0), 0); + let function = guarded_function(&program); + let row = &function.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + assert_eq!(row.payload_type(), TypeInterner::STRING); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!(!function.is_view_local(row.alias())); + assert!(function.is_view_local(row.output())); + let copy = CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(copy.owned_locals.contains(&(row.alias().index() as usize))); + assert!(moved.owned_locals.contains(&(row.alias().index() as usize))); + assert!(!copy.owned_locals.contains(&(row.output().index() as usize))); + assert!( + !moved + .owned_locals + .contains(&(row.output().index() as usize)) + ); + assert!( + copy.temporary_slots > 0, + "final String View takes its ordinary owned copy" + ); + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + } + assert_eq!(assert_plans(&program, &types, 0), 0); + } +} + +const CALLBACK_COMPACTION: &str = r#"namespace app +function inspect(view source: optional[list[int64]]) returns int64: + list[int64] alias = view((view source) handle: + return 0 + int64 dead = 7 + return 0 + ) + int64 retained = 3 + function(int64) returns int64 callback = function(value: int64) returns int64: return retained + value + return list.sum[int64](view alias) + callback(2) +function main() returns int64: + optional[list[int64]] source = some(list(4, 5)) + return inspect(view source) +"#; + +const COPY_THEN_MOVE_OPTIONAL: &str = r#"namespace app +function consume(source: optional[int64]) returns int64: + return source handle: return 0 +function main() returns int64: + optional[int64] source = some(5) + int64 alias = view((view source) handle: return 0) + int64 owned = consume(source) + return alias + owned +"#; + +const COPY_THEN_MOVE_RESULT: &str = r#"namespace app +function consume(source: result[string, string]) returns string: + return source handle error: return error +function main() returns string: + result[string, string] source = ok("present") + string alias = view((view source) handle error: return error) + string owned = consume(source) + return "{alias}:{owned}" +"#; + +// Exact genuine Source15 from Root's mutable-copy reference control. +const MUTABLE_COPIED_PAYLOADS: &str = r#"namespace app +function copied_int(view stdout: Stdout) returns nothing: + optional[int64] source = some(3) + mutable int64 copied = view((view source) handle: + return nothing + ) + copied = copied + 1 + int64 original = source handle: + return nothing + Stdout.write(view stdout, "integer:{copied}:{original}\n") + return nothing +function copied_string(view stdout: Stdout) returns nothing: + result[string, string] source = ok("kept") + mutable string copied = view((view source) handle error: + return nothing + ) + copied = "changed" + string original = source handle error: + return nothing + Stdout.write(view stdout, "string:{copied}:{original}\n") + return nothing +function main(stdout: Stdout) returns nothing: + copied_int(view stdout) + copied_string(view stdout) + return nothing +"#; + +#[test] +fn ordinary_borrowed_sums_mutable_copied_payloads_rebind_before_original_owning_unwrap() { + for release in [false, true] { + let (mut program, types) = lowered(MUTABLE_COPIED_PAYLOADS, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + } + } + assert_eq!(assert_plans(&program, &types, 15), 0); + let mut copied_types = Vec::new(); + for function in &program.functions { + let Some(witness) = &function.ordinary_borrowed_sums else { + continue; + }; + assert_eq!(witness.rows.len(), 1); + let row = &witness.rows[0]; + copied_types.push(row.payload_type()); + assert!(row.original_alias.mutable); + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().mutable); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!(!function.local(row.backing()).unwrap().mutable); + assert!(!function.is_view_local(row.backing())); + for intermediate in [row.source(), row.output()] { + let header = function.local(intermediate).unwrap(); + assert!(!header.mutable); + assert_eq!(header.view_source, Some(row.backing())); + } + let continuation = &function.blocks[row.alias_site.block.index() as usize]; + let (assigned_at, replacement) = continuation.statements.iter().enumerate().find_map(|(index, statement)| match &statement.kind { + StatementKind::Assign { target, value } if matches!(target.kind, E::Local(local) if local == row.alias()) => Some((index, value)), + _ => None, + }).expect("the exact Source rebinds its final copied payload"); + assert!(assigned_at > row.alias_site.statement_index); + if row.payload_type() == TypeInterner::INT64 { + assert_eq!(function.identity.declaration.name, "copied_int"); + assert!( + matches!(&replacement.kind, E::Binary { left, right, .. } if matches!(left.kind, E::Local(local) if local == row.alias()) && matches!(right.kind, E::Int(1))) + ); + } else { + assert_eq!(row.payload_type(), TypeInterner::STRING); + assert_eq!(function.identity.declaration.name, "copied_string"); + assert!(matches!(&replacement.kind, E::String(value) if value == "changed")); + } + let later_owning_take = function + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .find_map(|statement| match statement.kind { + StatementKind::SumTake { + source, + target, + success: true, + } if source != row.source() + && !function.is_view_local(source) + && function.local(source).unwrap().ty == row.sum_type() + && function.local(target).unwrap().ty == row.payload_type() => + { + Some((block.id, source)) + } + _ => None, + }) + }) + .expect("later bare owning Handle unwraps its ordinary shell snapshot"); + assert_ne!(later_owning_take.0, row.project.block); + let cfg = ControlFlowGraph::analyze(function).unwrap(); + assert!(cfg.reverse_postorder().contains(&later_owning_take.0)); + let copy = + CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types) + .expect("rebinding a copied payload does not create or preserve a source loan"); + for live_after in [©.live_after_statement, &moved.live_after_statement] { + let after_rebind = + &live_after[row.alias_site.block.index() as usize][assigned_at]; + assert!(after_rebind.contains(&(row.alias().index() as usize))); + assert!(!after_rebind.contains(&(row.source().index() as usize))); + assert!(!after_rebind.contains(&(row.output().index() as usize))); + } + } + copied_types.sort_by_key(|ty| ty.index()); + let mut expected = vec![TypeInterner::INT64, TypeInterner::STRING]; + expected.sort_by_key(|ty| ty.index()); + assert_eq!(copied_types, expected); + } + } +} + +#[test] +fn ordinary_borrowed_sums_genuine_mutable_list_payload_still_refuses_native_borrow() { + let source = r#"namespace app +function inspect(view source: optional[list[int64]]) returns nothing: + mutable list[int64] alias = view((view source) handle: return nothing) + trace alias + return nothing +function main() returns nothing: + optional[list[int64]] source = some(list(3)) + inspect(view source) + return nothing +"#; + for release in [false, true] { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let errors = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect_err("genuine move-only payload View cannot declare a mutable borrowed final alias"); + assert!( + errors.iter().any(|error| error + .message + .contains("native borrowed alias requires immutable bindings")), + "{errors:?}" + ); + } +} + +#[test] +fn ordinary_borrowed_sums_copied_final_alias_retires_projection_loans_before_original_shell_transfer() + { + for release in [false, true] { + for source in [COPY_THEN_MOVE_OPTIONAL, COPY_THEN_MOVE_RESULT] { + let (mut program, types) = lowered(source, release); + for prepared in [false, true] { + if prepared { + crate::prepare_native_sequences(&mut program, &types); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + } + } + assert_eq!(assert_plans(&program, &types, 0), 0); + let function = guarded_function(&program); + let witness = function.ordinary_borrowed_sums.as_ref().unwrap(); + assert_eq!(witness.rows.len(), 1); + let row = &witness.rows[0]; + assert!(row.original_alias.view_source.is_none()); + assert!(function.local(row.alias()).unwrap().view_source.is_none()); + assert!( + !function.is_view_local(row.backing()), + "the original sum shell is owned" + ); + assert_eq!( + function.local(row.source()).unwrap().view_source, + Some(row.backing()) + ); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + let (call_block, call_index, call) = function.blocks.iter().find_map(|block| { + block.statements.iter().enumerate().find_map(|(index, statement)| match &statement.kind { + StatementKind::Let { value, .. } if matches!(value.kind, E::Call { function: target, .. } if program.functions[target.index() as usize].identity.declaration.name == "consume") => Some((block.id, index, value)), + _ => None, + }) + }).expect("actual owning helper invocation"); + assert_eq!(call_block, row.alias_site.block); + assert!( + call_index > row.alias_site.statement_index, + "copy precedes the source transfer" + ); + let acquisitions = + crate::validate_caller_acquisitions(&program, function, &types).unwrap(); + let [acquired] = acquisitions + .arguments(call) + .expect("current owning call acquisition") + else { + panic!("one original owned shell argument") + }; + assert_eq!(acquired.parameter_index, 0); + assert_eq!(acquired.source.binding, Some(row.backing())); + assert_eq!( + acquired.source.effect, + jett_typecheck::CheckedCallerEffect::TransferOwned + ); + assert_eq!(acquired.source.actual_type, row.sum_type()); + assert_eq!(acquired.source.storage_type, row.sum_type()); + let E::Call { + function: target, + args, + .. + } = &call.kind + else { + panic!("owning helper call") + }; + assert_eq!( + program.functions[target.index() as usize].params[0].mode, + ParamMode::Owned + ); + assert_eq!(acquisitions.argument_binding(&args[0]), Some(row.backing())); + let copy = + CopyValuePlan::analyze_storage(function, &types, Some(&program)).unwrap(); + let moved = MoveValuePlan::analyze(&program, function, &types).expect("a copied final alias leaves no active projection loan that can block moving its original shell"); + for live_after in [©.live_after_statement, &moved.live_after_statement] { + let live = &live_after[row.alias_site.block.index() as usize] + [row.alias_site.statement_index]; + assert!( + live.contains(&(row.alias().index() as usize)), + "the final copy is used after source transfer" + ); + assert!( + live.contains(&(row.backing().index() as usize)), + "the original backing is acquired by the later call" + ); + assert!(!live.contains(&(row.source().index() as usize))); + assert!(!live.contains(&(row.output().index() as usize))); + } + } + } + } +} + +#[test] +fn ordinary_borrowed_sums_genuine_callback_capture_survives_local_compaction() { + for release in [false, true] { + let (mut program, types) = lowered(CALLBACK_COMPACTION, release); + assert_eq!(assert_plans(&program, &types, 0), 1); + let original = guarded_function(&program).clone(); + let dead = original + .locals + .iter() + .find(|local| local.name == "dead") + .unwrap() + .id; + let retained = original + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let callback = original + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id; + let inline_parameter = original + .locals + .iter() + .find(|local| local.name == "value") + .unwrap() + .id; + assert!(dead.index() < retained.index()); + assert!(retained.index() < inline_parameter.index()); + assert!(inline_parameter.index() < callback.index()); + assert!(!original.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(statement.kind, StatementKind::Let { local, .. } if local == dead))); + let lifted = program + .functions + .iter() + .filter(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(); + assert_eq!(lifted.len(), 1); + assert_eq!(lifted[0].capture_count, 1); + assert_eq!(lifted[0].params[0].name, "retained"); + assert!(crate::sequences::prune::unused_locals( + guarded_function_mut(&mut program) + )); + let function = guarded_function(&program); + assert_eq!(function.locals.len(), original.locals.len() - 2); + assert!(!function.locals.iter().any(|local| local.name == "dead")); + assert!(!function.locals.iter().any(|local| local.name == "value")); + let moved_retained = function + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let moved_callback = function + .locals + .iter() + .find(|local| local.name == "callback") + .unwrap() + .id; + assert_eq!(moved_retained.index(), retained.index() - 1); + assert_eq!(moved_callback.index(), callback.index() - 2); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(&statement.kind, StatementKind::Let { local, value } if *local == moved_callback && matches!(&value.kind, E::ClosureRef { function, captures } if *function == lifted[0].id && captures == &[moved_retained])))); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.debug_kind == hir::FunctionDebugKind::Inline) + .cloned() + .collect::>(), + lifted, + "caller compaction preserves separately lifted callback ABI" + ); + assert_eq!(assert_plans(&program, &types, 0), 1); + let compacted = program.clone(); + assert!(crate::sequences::prune::unused_locals( + guarded_function_mut(&mut program) + )); + assert!( + program == compacted, + "second unused_locals must preserve the exact compacted callback program; release={release}" + ); + } +} + +const COMPOSED_ABSENT: &str = r#"namespace app +function inspect[T](view source: optional[list[int64]], absent: optional[T]) returns int64: + list[int64] alias = view((view source) handle: return 0) + T impossible = absent handle: return list.sum[int64](view alias) + trace impossible + return list.sum[int64](view alias) +function main() returns int64: + optional[list[int64]] source = some(list(4, 5)) + return inspect(view source, none) +"#; + +#[test] +fn ordinary_borrowed_sums_unrelated_optional_never_preparation_keeps_complete_valid_function() { + for release in [false, true] { + let (mut program, types) = lowered(COMPOSED_ABSENT, release); + assert_eq!(assert_plans(&program, &types, 0), 1); + let original = guarded_function(&program).clone(); + assert!( + original + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + let absent_site = original.blocks.iter().find_map(|block| block.statements.iter().find_map(|statement| match statement.kind { + StatementKind::SumTake { source, success: true, .. } if matches!(types.resolve(original.local(source).unwrap().ty), Type::Optional(payload) if *payload == TypeInterner::NEVER) => Some(block.id), + _ => None, + })).unwrap(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + let prepared = guarded_function(&program); + crate::validate_call_ownership(&program, &types).unwrap(); + assert_eq!(assert_plans(&program, &types, 0), 1); + if prepared != &original { + assert!(!prepared.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(statement.kind, StatementKind::SumTake { source, success: true, .. } if matches!(types.resolve(prepared.local(source).unwrap().ty), Type::Optional(payload) if *payload == TypeInterner::NEVER)))); + assert!( + ControlFlowGraph::analyze(prepared) + .unwrap() + .reverse_postorder() + .iter() + .all(|id| (id.index() as usize) < prepared.blocks.len()) + ); + } else { + assert!( + ControlFlowGraph::analyze(prepared) + .unwrap() + .reverse_postorder() + .contains(&absent_site), + "a rollback preserves the complete original graph" + ); + } + let once = program.clone(); + crate::prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!(program, once); + } +} + +fn assert_refused(program: &crate::Program, types: &TypeInterner) { + assert!(crate::validate_call_ownership(program, types).is_err()); + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "inspect" + }) + .unwrap(); + assert!(CopyValuePlan::analyze_storage(function, types, Some(program)).is_err()); + assert!(MoveValuePlan::analyze(program, function, types).is_err()); + if let Some(witness) = &function.ordinary_borrowed_sums { + if let Some(row) = witness.rows.first() { + assert!( + function + .ordinary_borrowed_sum_projection( + row.project.block, + row.project.statement_index + ) + .is_err() + ); + } + let mut changed = function.clone(); + assert!(!crate::sequences::prune::unreachable(&mut changed)); + assert_eq!( + changed, *function, + "failed block pruning rolls back the exact malformed input" + ); + assert!(!crate::sequences::prune::unused_locals(&mut changed)); + assert_eq!( + changed, *function, + "failed local compaction rolls back the exact malformed input" + ); + } + let mut changed = program.clone(); + crate::prepare_native_sequences(&mut changed, types); + assert_eq!( + changed, *program, + "validation refusal happens before preparation can hide the mutation" + ); +} + +fn reseal_only_blocks(function: &mut Function) { + let blocks = function.blocks.clone(); + Arc::make_mut(&mut function.ordinary_borrowed_sums.as_mut().unwrap().sealed).blocks = blocks; +} + +#[test] +fn ordinary_borrowed_sums_changed_headers_tag_edge_and_failure_are_refused() { + for source in [SOURCES[0], SOURCES[1]] { + let (program, types) = lowered(source, false); + assert_eq!(assert_plans(&program, &types, 0), 2); + for corruption in 0..9 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + match corruption { + 0 => function.locals[row.source().index() as usize].view_source = None, + 1 => { + function.locals[row.output().index() as usize].view_source = Some(row.source()) + } + 2 => function.locals[row.alias().index() as usize].mutable = true, + 3 => function.locals[row.backing().index() as usize].ty = row.payload_type(), + 4 => function.locals[row.tag().index() as usize].ty = TypeInterner::INT64, + 5 => { + function.blocks[row.observe.block.index() as usize].statements + [row.observe.statement_index] + .kind = StatementKind::SumTag { + source: row.backing(), + target: row.tag(), + } + } + 6 => { + let TerminatorKind::Branch { condition, .. } = &mut function.blocks + [row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + condition.kind = E::Bool(true); + } + 7 => { + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut function.blocks[row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + std::mem::swap(then_block, else_block); + } + _ => { + if let Some(error) = row.error() { + function.locals[error.index() as usize].view_source = Some(row.backing()); + } else { + function.blocks[row.project.block.index() as usize].statements + [row.project.statement_index] + .kind = StatementKind::SumTake { + source: row.source(), + target: row.output(), + success: false, + }; + } + } + } + assert_refused(&changed, &types); + } + } +} + +#[test] +fn ordinary_borrowed_sums_copied_missing_and_disconnected_projection_survive_cache_reseal_refusal() +{ + let (program, types) = lowered(SOURCES[1], false); + assert_eq!(assert_plans(&program, &types, 0), 2); + for corruption in 0..4 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + assert_eq!(function.local(row.output()).unwrap().ty, row.payload_type()); + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + match corruption { + 0 => { + let copied = function.blocks[row.project.block.index() as usize].statements + [row.project.statement_index] + .clone(); + function.blocks[row.project.block.index() as usize] + .statements + .push(copied); + } + 1 => { + function.blocks[row.project.block.index() as usize] + .statements + .remove(row.project.statement_index); + } + 2 => { + let TerminatorKind::Branch { then_block, .. } = &mut function.blocks + [row.observe.block.index() as usize] + .terminator + .kind + else { + panic!("genuine selecting edge") + }; + *then_block = row.continuation; + let cfg = ControlFlowGraph::analyze(function).unwrap(); + assert!(!cfg.reverse_postorder().contains(&row.project.block)); + assert!( + matches!(function.blocks[row.project.block.index() as usize].statements[row.project.statement_index].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + } + _ => { + let site = row.failure.unwrap(); + function.blocks[site.block.index() as usize].statements[site.statement_index] + .kind = StatementKind::SumTake { + source: row.source(), + target: row.error().unwrap(), + success: true, + }; + } + } + assert_refused(&changed, &types); + reseal_only_blocks(guarded_function_mut(&mut changed)); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_private_inventory_and_original_association_resist_row_reseal() { + let (program, types) = lowered(SOURCES[0], false); + for corruption in 0..6 { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let witness = function.ordinary_borrowed_sums.as_mut().unwrap(); + let independent = witness.sealed.clone(); + match corruption { + 0 => { + witness.rows.pop(); + } + 1 => { + witness.rows.push(witness.rows[0].clone()); + } + 2 => witness.rows[0].original_path.push(usize::MAX), + 3 => { + let sibling = witness.rows[1].clone(); + witness.rows[0].original_path = sibling.original_path; + witness.rows[0].original_alias = sibling.original_alias; + witness.rows[0].original_backing = sibling.original_backing; + witness.rows[0].original = sibling.original; + } + 4 => witness.rows[0].path = OrdinarySumPayloadPath::ResultOk, + _ => { + let (left, right) = witness.rows.split_at_mut(1); + std::mem::swap(&mut left[0].original_path, &mut right[0].original_path); + std::mem::swap(&mut left[0].original, &mut right[0].original); + std::mem::swap(&mut left[0].original_alias, &mut right[0].original_alias); + std::mem::swap( + &mut left[0].original_backing, + &mut right[0].original_backing, + ); + assert_ne!(left[0].original_alias.id, right[0].original_alias.id); + } + } + assert!(Arc::ptr_eq(&witness.sealed, &independent)); + assert!(!same_rows(&witness.rows, &independent.rows)); + assert_refused(&changed, &types); + if corruption == 5 { + // Both original keys remain genuine and unique; their association + // with current sites is independent private authority, not a cache. + reseal_only_blocks(guarded_function_mut(&mut changed)); + assert_refused(&changed, &types); + continue; + } + let witness = guarded_function_mut(&mut changed) + .ordinary_borrowed_sums + .as_mut() + .unwrap(); + let rows = witness.rows.clone(); + Arc::make_mut(&mut witness.sealed).rows = rows; + assert!(same_rows(&witness.rows, &witness.sealed.rows)); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_missing_and_foreign_witnesses_cannot_authorize_typed_views() { + let (program, types) = lowered(SOURCES[0], false); + let (foreign, _) = lowered(SOURCES[1], false); + for replacement in [ + None, + guarded_function(&foreign).ordinary_borrowed_sums.clone(), + ] { + let mut changed = program.clone(); + let function = guarded_function_mut(&mut changed); + let row = function.ordinary_borrowed_sums.as_ref().unwrap().rows[0].clone(); + function.ordinary_borrowed_sums = replacement; + assert_eq!( + function.local(row.output()).unwrap().view_source, + Some(row.backing()) + ); + assert!( + matches!(function.blocks[row.project.block.index() as usize].statements[row.project.statement_index].kind, StatementKind::SumTake { source, target, success: true } if source == row.source() && target == row.output()) + ); + assert_refused(&changed, &types); + } +} + +#[test] +fn ordinary_borrowed_sums_alias_query_requires_the_exact_current_initializer_reference() { + let (program, types) = lowered(SOURCES[0], false); + assert_eq!(assert_plans(&program, &types, 0), 2); + let function = guarded_function(&program); + let row = &function.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + let StatementKind::Let { value, .. } = &function.blocks[row.alias_site.block.index() as usize] + .statements[row.alias_site.statement_index] + .kind + else { + panic!("genuine alias initializer") + }; + assert!( + function + .ordinary_borrowed_sum_alias(row.alias(), value) + .unwrap() + ); + assert!( + !function + .ordinary_borrowed_sum_alias(row.alias(), &value.clone()) + .unwrap() + ); + assert!( + !function + .ordinary_borrowed_sum_alias(row.output(), value) + .unwrap() + ); +} + +#[test] +fn ordinary_borrowed_sums_malformed_maps_and_edits_leave_input_unchanged() { + let (mut program, types) = lowered(SOURCES[0], false); + for function in &mut program.functions { + assert!(crate::sequences::prune::unreachable(function)); + } + assert_eq!(assert_plans(&program, &types, 0), 2); + let before = guarded_function(&program); + let block_map = before + .blocks + .iter() + .map(|block| Some(block.id)) + .collect::>(); + let local_map = before + .locals + .iter() + .map(|local| Some(local.id)) + .collect::>(); + for corruption in 0..6 { + let mut function = before.clone(); + let result = match corruption { + 0 => { + let mut map = block_map.clone(); + map.pop(); + remap_blocks(&mut function, before, &map) + } + 1 => { + let mut map = block_map.clone(); + map[before.entry.index() as usize] = None; + remap_blocks(&mut function, before, &map) + } + 2 => { + let mut map = block_map.clone(); + map.swap(0, 1); + remap_blocks(&mut function, before, &map) + } + 3 => { + let mut map = local_map.clone(); + map.pop(); + remap_locals(&mut function, before, &map) + } + 4 => { + let mut map = local_map.clone(); + map.swap(0, 1); + remap_locals(&mut function, before, &map) + } + _ => { + let witness = before.ordinary_borrowed_sums.as_ref().unwrap(); + let mut map = local_map.clone(); + map[witness.rows[0].backing().index() as usize] = None; + let mut next = 0; + for local in map.iter_mut().flatten() { + *local = LocalId::new(next); + next += 1; + } + remap_locals(&mut function, before, &map) + } + }; + assert!(result.is_err(), "malformed canonical map {corruption}"); + assert_eq!(function, *before); + validate(&function, &types).unwrap(); + } + let mut changed = before.clone(); + let row = &before.ordinary_borrowed_sums.as_ref().unwrap().rows[0]; + changed.locals[row.output().index() as usize].view_source = None; + let malformed = changed.clone(); + assert!(remap_blocks(&mut changed, before, &block_map).is_err()); + assert_eq!(changed, malformed); + assert!(remap_locals(&mut changed, before, &local_map).is_err()); + assert_eq!(changed, malformed); +} + +#[path = "mutable_tests.rs"] +mod mutable_tests; diff --git a/crates/jett_mir/src/reflected_callable.rs b/crates/jett_mir/src/reflected_callable.rs new file mode 100644 index 00000000..d9cc326e --- /dev/null +++ b/crates/jett_mir/src/reflected_callable.rs @@ -0,0 +1,269 @@ +//! Independent replay and sealed graphs for checked lazy reflected adapters. +//! These records grant no Resource family, entry scope or runtime capability. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use super::*; + +#[derive(Debug)] +struct Original { + hir: hir::Program, + modes: HashMap>, + execution: resource_ownership::ResourceExecutionClosure, + required: HashSet, +} + +#[derive(Debug, Clone)] +struct Graph { + id: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + params: Vec, + capture_count: usize, + return_type: TypeId, + locals: Vec, + entry: BlockId, + blocks: Vec, + span: Span, +} + +impl PartialEq for Graph { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.identity == other.identity + && self.debug_kind == other.debug_kind + && self.params == other.params + && self.capture_count == other.capture_count + && self.return_type == other.return_type + && self.locals == other.locals + && self.entry == other.entry + && self.span == other.span + && breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} + +impl Graph { + fn capture(function: &Function) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + span: function.span, + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct Witness { + original: Arc, + source: Arc, + current: Arc, +} + +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.original, &other.original) + && Arc::ptr_eq(&self.source, &other.source) + && Arc::ptr_eq(&self.current, &other.current) + } +} + +pub(super) fn capture( + program: &mut Program, + original: &hir::Program, + modes: &HashMap>, + execution: &resource_ownership::ResourceExecutionClosure, +) -> Result<(), String> { + let mut required = HashSet::new(); + let mut has_adapters = false; + for function in &original.functions { + for plan in hir::reflected_callable_plans(function) { + if !plan.validate_original() { + return Err("reflected callable lacks original HIR constructor evidence".into()); + } + has_adapters = true; + required.insert(function.id); + required.insert(plan.function()); + required.insert( + plan.original_owner() + .ok_or("reflected callable original owner is missing")?, + ); + for predicate in plan.original_predicates() { + required.insert(predicate.id); + } + } + } + if !has_adapters { + return Ok(()); + } + // Original descriptor targets and capture producers remain authenticated, + // including helpers whose signatures alone cannot identify their bodies. + required.extend(original.functions.iter().map(|function| function.id)); + let original = Arc::new(Original { + hir: original.clone(), + modes: modes.clone(), + execution: execution.clone(), + required, + }); + for function in &mut program.functions { + if original.required.contains(&function.id) { + let graph = Arc::new(Graph::capture(function)); + function.reflected_callable = Some(Witness { + original: original.clone(), + source: graph.clone(), + current: graph, + }); + } + } + Ok(()) +} + +/// Called only beside canonical preparation after the complete input gate. +/// A changed public graph cannot become new authority through this function. +pub(super) fn prepared(function: &mut Function, before: &Function) -> Result<(), String> { + let Some(previous) = &before.reflected_callable else { + if function.reflected_callable.is_some() { + return Err("reflected callable witness appeared during preparation".into()); + } + return Ok(()); + }; + if previous.current.as_ref() != &Graph::capture(before) + || function.reflected_callable.as_ref() != Some(previous) + { + return Err("reflected callable preparation has no exact original current graph".into()); + } + let current = Arc::new(Graph::capture(function)); + let mut witness = previous.clone(); + witness.current = current; + function.reflected_callable = Some(witness); + Ok(()) +} + +fn private_expression(value: &Expression) -> bool { + use hir::ExpressionKind as E; + matches!( + &value.kind, + E::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::ReadCallable(_)), + .. + } + ) || matches!(&value.kind, E::IndirectCall { ownership: hir::CallOwnership::Generated(generated), .. } + if matches!(generated.operation, hir::GeneratedOperation::ReflectedCallableAdapter { .. })) +} + +fn has_private_operations(function: &Function) -> bool { + function.blocks.iter().any(|block| { + let mut found = block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::ReflectedValueReady { .. })); + resource_ownership::walk::mir_block(block, &mut |expression| { + found |= private_expression(expression) + }); + found + }) +} + +pub fn validate_reflected_callable_plans( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + let mut originals = Vec::>::new(); + for function in &program.functions { + let Some(witness) = &function.reflected_callable else { + let mut copied_adapter = false; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if let hir::ExpressionKind::FunctionAdapter { + function: target, .. + } = &value.kind + { + copied_adapter |= program + .functions + .get(target.index() as usize) + .is_some_and(has_private_operations); + } + }); + } + if has_private_operations(function) || copied_adapter { + errors.push(ValidationError { + span: function.span, + message: "reflected callable MIR operation has no original private witness" + .into(), + }); + } + continue; + }; + if !originals + .iter() + .any(|original| Arc::ptr_eq(original, &witness.original)) + { + originals.push(witness.original.clone()); + } + if witness.current.as_ref() != &Graph::capture(function) || witness.source.id != function.id + { + errors.push(ValidationError { + span: function.span, + message: "reflected callable current body, tuple, readiness or context changed" + .into(), + }); + continue; + } + let Some(original) = witness + .original + .hir + .functions + .get(function.id.index() as usize) + else { + errors.push(ValidationError { + span: function.span, + message: "reflected callable MIR lost its original function".into(), + }); + continue; + }; + match super::lower_function_body(original, types, &witness.original.modes, &witness.original.hir.resource_manifest, &witness.original.hir.resource_source, &witness.original.execution) { + Ok(reconstructed) if Graph::capture(&reconstructed) == *witness.source => {} + _ => errors.push(ValidationError { span: function.span, message: "reflected callable original adapter/predicate/tuple CFG cannot be reconstructed".into() }), + } + } + for original in originals { + if let Err(hir_errors) = hir::validate(&original.hir) + .and_then(|()| hir::validate_backend_types(&original.hir, types)) + { + errors.extend(hir_errors.into_iter().map(|error| ValidationError { + span: error.span, + message: error.message, + })); + } + for required in &original.required { + if !program + .functions + .get(required.index() as usize) + .is_some_and(|function| { + function + .reflected_callable + .as_ref() + .is_some_and(|witness| Arc::ptr_eq(&witness.original, &original)) + }) + { + errors.push(ValidationError { span: original.hir.functions[required.index() as usize].span, message: "reflected callable original owner, descriptor or predicate witness was removed".into() }); + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/reflected_callable/tests.rs b/crates/jett_mir/src/reflected_callable/tests.rs new file mode 100644 index 00000000..2e9f9b24 --- /dev/null +++ b/crates/jett_mir/src/reflected_callable/tests.rs @@ -0,0 +1,450 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; + +fn fixture( + source_return: &str, + requested_return: &str, +) -> (Program, Arc) { + let source = format!( + "namespace app\ntype Positive = int64 where value > 0\ntype Higher = Positive where value > 10\nfunction five() returns int64:\n return 5\nstruct Holder:\n callback: function() returns {source_return}\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns {requested_return}:\n return type.field_value[Holder, function() returns {requested_return}](view holder, view field)\n" + ); + source_fixture(&source) +} + +fn source_fixture(source: &str) -> (Program, Arc) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let hir = hir::lower_checked_ordinary_program(&checked).expect("ordinary original checked HIR"); + let mir = lower(&hir, &checked.checked().interner).expect("canonical reflected MIR"); + (mir, checked) +} + +fn adapter(program: &Program) -> usize { + program + .functions + .iter() + .position(|function| { + function.blocks.iter().any(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::ReflectedValueReady { .. }) + }) + }) + }) + .expect("private adapter") +} + +#[test] +fn read_is_lazy_and_invocation_orders_actual_result_preflight_predicate_publication() { + let (program, checked) = fixture("int64", "Positive"); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); + let reader = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert!( + !reader + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::CheckRefinement { .. } | StatementKind::ReflectedValueReady { .. } + )) + ); + let mut reads = 0usize; + for block in &reader.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if matches!( + value.kind, + hir::ExpressionKind::Intrinsic { + field_validation: Some(hir::ReflectedFieldValidation::ReadCallable(_)), + .. + } + ) { + reads += 1; + } + }); + } + assert_eq!(reads, 1); + let function = &program.functions[adapter(&program)]; + let statements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect::>(); + let call = statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::Let { + value: Expression { + kind: hir::ExpressionKind::IndirectCall { .. }, + .. + }, + .. + } + ) + }) + .unwrap(); + assert_eq!( + statements + .iter() + .filter(|statement| matches!( + statement.kind, + StatementKind::Let { + value: Expression { + kind: hir::ExpressionKind::IndirectCall { .. }, + .. + }, + .. + } + )) + .count(), + 1, + "actual source invocation once" + ); + let ready = statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + ) + }) + .unwrap(); + let predicate = statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::CheckRefinement { .. })) + .unwrap(); + assert!(call < ready && ready < predicate); + assert!(function.blocks.iter().any(|block| matches!( + &block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: hir::ExpressionKind::RefinementValidated(_), + .. + })) + ))); +} + +#[test] +fn ancestor_transport_has_no_new_pending_inspection_or_predicate() { + let (program, checked) = fixture("Higher", "Positive"); + let function = &program.functions[adapter(&program)]; + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: false, + .. + } + )) + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } | StatementKind::CheckRefinement { .. } + )) + ); + let returned = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("ancestor adapter return"); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + panic!("ancestor proof must be transported by typed coarsening"); + }; + assert!( + matches!(checked.checked().interner.resolve(inner.ty), Type::Refinement { base, .. } if *base == returned.ty) + ); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); +} + +#[test] +fn pending_scalar_and_unit_ancestor_returns_transport_the_original_proof_and_cannot_relabel_it() { + for (base, value) in [("int64", "0"), ("nothing", "nothing")] { + let source = format!( + "namespace app\ntype Ready = {base} where true\ntype Higher = Ready where true\nstruct Holder:\n callback: function() returns Higher\nfunction pending_source() returns Higher:\n Higher proved = {value} handle error:\n return pending_source()\n return run run proved\nfunction inspect(view holder: Holder, view field: TypeField) returns function() returns Ready:\n return type.field_value[Holder, function() returns Ready](view holder, view field)\n" + ); + let (mut program, checked) = source_fixture(&source); + let types = &checked.checked().interner; + let index = adapter(&program); + for prepared in [false, true] { + if prepared { + prepare_native_sequences(&mut program, types); + prepare_native_uninhabited_sums(&mut program, types); + prepare_native_generated_functions(&mut program, types); + } + validate_reflected_callable_plans(&program, types).unwrap(); + let function = &program.functions[index]; + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::CheckRefinement { .. } + | StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + )) + ); + let mut joins = 0; + let mut new_proofs = 0; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |expression| { + joins += usize::from(matches!(expression.kind, hir::ExpressionKind::Join(_))); + new_proofs += usize::from(matches!( + expression.kind, + hir::ExpressionKind::RefinementValidated(_) + )); + }); + } + assert_eq!(joins, 0, "{base} pending layers stay intact"); + assert_eq!( + new_proofs, 0, + "{base} existing ancestor proof is not revalidated" + ); + let returned = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("transported ancestor return"); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + panic!("{base} exact ancestor transport needs Coarsen"); + }; + assert!(matches!(inner.kind, hir::ExpressionKind::Local(_))); + assert!( + matches!(types.resolve(inner.ty), Type::Refinement { base, .. } if *base == returned.ty) + ); + let mut forged = program.clone(); + let returned = forged.functions[index] + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + let hir::ExpressionKind::Coarsen(inner) = &returned.kind else { + unreachable!() + }; + returned.kind = hir::ExpressionKind::RefinementValidated(inner.clone()); + assert!( + validate_reflected_callable_plans(&forged, types).is_err(), + "{base} public return relabel cannot refresh the private original graph" + ); + } + } +} + +#[test] +fn recursive_produced_return_completes_all_occupied_preflight_before_any_predicate() { + let (program, checked) = fixture("list[optional[int64]]", "list[optional[Positive]]"); + let function = &program.functions[adapter(&program)]; + let mut visited = HashSet::new(); + let mut pending = vec![(function.entry, false)]; + while let Some((block, predicate_seen)) = pending.pop() { + if !visited.insert((block, predicate_seen)) { + continue; + } + let block = &function.blocks[block.index() as usize]; + let mut seen = predicate_seen; + for statement in &block.statements { + if matches!(statement.kind, StatementKind::CheckRefinement { .. }) { + seen = true; + } + assert!( + !(seen + && matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } | StatementKind::ReflectedContainerReady { .. } + )), + "preflight must not run after an earlier predicate" + ); + } + match &block.terminator.kind { + TerminatorKind::Goto(next) => pending.push((*next, seen)), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + pending.push((*then_block, seen)); + pending.push((*else_block, seen)); + } + _ => {} + } + } + assert!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + )) + ); + validate_reflected_callable_plans(&program, &checked.checked().interner).unwrap(); +} + +#[test] +fn graph_and_tuple_and_body_and_witness_forgery_cannot_reseal() { + let (original, checked) = fixture("int64", "Positive"); + let types = &checked.checked().interner; + for mutation in 0..7 { + let mut program = original.clone(); + let index = adapter(&program); + match mutation { + 0 => { + let statement = program.functions[index] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!( + statement.kind, + StatementKind::ReflectedValueReady { + inspect_pending: true, + .. + } + ) + }) + .unwrap(); + let StatementKind::ReflectedValueReady { + inspect_pending, .. + } = &mut statement.kind + else { + unreachable!() + }; + *inspect_pending = false; + } + 1 => { + program.functions[index].reflected_callable = None; + } + 2 => { + for function in &mut program.functions { + function.reflected_callable = None; + } + } + 3 => { + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "five") + .unwrap(); + let TerminatorKind::Return(Some(value)) = &mut function.blocks + [function.entry.index() as usize] + .terminator + .kind + else { + panic!("source return") + }; + value.kind = hir::ExpressionKind::Int(0); + } + 4 => { + program.functions[index].identity.declaration.namespace = "other".into(); + } + 5 => { + let statement = program.functions[index] + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::CheckRefinement { .. }) + }) + .unwrap(); + let StatementKind::CheckRefinement { call, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::Call { args, .. } = &mut call.kind else { + panic!("predicate tuple") + }; + args[0].kind = hir::ExpressionKind::Int(7); + } + _ => { + let function = program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + && function.identity.declaration.namespace == "app" + }) + .unwrap(); + let returned = function + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .expect("original predicate result"); + returned.kind = hir::ExpressionKind::Bool(false); + } + } + assert!( + validate_reflected_callable_plans(&program, types).is_err(), + "forgery {mutation}" + ); + prepare_native_generated_functions(&mut program, types); + prepare_native_sequences(&mut program, types); + prepare_native_uninhabited_sums(&mut program, types); + assert!( + validate_reflected_callable_plans(&program, types).is_err(), + "preparation cannot reseal forgery {mutation}" + ); + } +} + +#[test] +fn canonical_preparation_preserves_private_original_and_current_graphs() { + let (mut program, checked) = fixture("int64", "Positive"); + let types = &checked.checked().interner; + prepare_native_generated_functions(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); + prepare_native_sequences(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); + prepare_native_uninhabited_sums(&mut program, types); + validate_reflected_callable_plans(&program, types).unwrap(); +} diff --git a/crates/jett_mir/src/resource_manifest_tests.rs b/crates/jett_mir/src/resource_manifest_tests.rs new file mode 100644 index 00000000..db102974 --- /dev/null +++ b/crates/jett_mir/src/resource_manifest_tests.rs @@ -0,0 +1,241 @@ +use crate::{StatementKind as S, TerminatorKind as T}; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::{CallOwnership, Expression, ExpressionKind as E}; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::{ResourceKernelRecipe, TypeInterner}; +use std::collections::HashMap; +use std::sync::Arc; +const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +"#; +const SOURCE: &str = r#"namespace app +function factory(view net: Network, label: int64) returns result[resource_probe.TestHandle, string]: + use resource_probe + return resource_probe.create(view net, label) +"#; +fn checked(release: bool) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(SUPPORT, stdlib); + let source = parse(SOURCE, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(source.errors.is_empty(), "{:?}", source.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource.name.span) + } else { + None + } + }) + .unwrap(); + parsed.module.items.extend(source.module.items); + parsed.errors.extend(source.errors); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +fn invoke(program: &crate::Program) -> &Expression { + program + .functions + .iter() + .flat_map(|function| &function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &block.terminator.kind + && matches!(value.kind, E::ResourceInvoke { .. }) + { + return Some(value); + } + block + .statements + .iter() + .find_map(|statement| match &statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .expect("Source ResourceInvoke survives typed lowering") +} +fn invoke_mut(program: &mut crate::Program) -> &mut Expression { + program + .functions + .iter_mut() + .flat_map(|function| &mut function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &mut block.terminator.kind + && matches!(value.kind, E::ResourceInvoke { .. }) + { + return Some(value); + } + block + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + S::Let { value, .. } | S::Evaluate(value) + if matches!(value.kind, E::ResourceInvoke { .. }) => + { + Some(value) + } + _ => None, + }) + }) + .unwrap() +} +#[test] +fn resource_manifest_survives_mir_without_ordinary_owner_authority() { + for release in [false, true] { + let original = checked(release); + let types = &original.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&original).unwrap(); + let mut mir = crate::lower(&hir, types).unwrap(); + assert_eq!(mir.resource_manifest, hir.resource_manifest); + let E::ResourceInvoke { + hook, + evaluation_order, + ownership: CallOwnership::Source(source), + .. + } = &invoke(&mir).kind + else { + panic!("original Source hook"); + }; + assert_eq!(hook.recipe(), ResourceKernelRecipe::NetworkFactory); + assert_eq!(evaluation_order, &[1, 0]); + assert!(mir.resource_manifest.contains_hook(hook)); + assert!( + source + .arguments + .iter() + .all(|argument| matches!(argument.staging, jett_hir::ArgumentStaging::Original)) + ); + crate::validate_call_ownership(&mir, types).unwrap(); + let operations = mir.functions.iter().filter(|function| function.blocks.iter().any(|block| { + matches!(&block.terminator.kind, T::Return(Some(Expression { kind: E::ResourceInvoke { hook, .. }, .. })) if hook.recipe() == ResourceKernelRecipe::NetworkFactory) + })).collect::>(); + let [function] = operations.as_slice() else { + panic!("one exact original factory body"); + }; + assert!( + crate::validate_caller_acquisitions(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::move_values::MoveValuePlan::analyze(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::copy_values::CopyValuePlan::analyze(function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + crate::prepare_native_sequences(&mut mir, types); + crate::validate_call_ownership(&mir, types).unwrap(); + assert_eq!(mir.resource_manifest, hir.resource_manifest); + } +} +#[test] +fn resource_manifest_mir_refuses_foreign_hook_and_original_packet_corruption() { + for release in [false, true] { + let original = checked(release); + let types = &original.checked().interner; + let hir = jett_hir::lower_checked_resource_program(&original).unwrap(); + let mir = crate::lower(&hir, types).unwrap(); + let foreign = jett_hir::lower_checked_resource_program(&checked(release)).unwrap(); + for mutation in 0..6 { + let mut changed = mir.clone(); + if mutation == 0 { + changed.resource_manifest = jett_hir::ResourceManifest::empty(); + } else if mutation == 5 { + let descriptor = changed + .functions + .iter_mut() + .flat_map(|function| &mut function.blocks) + .find_map(|block| { + if let T::Return(Some(value)) = &mut block.terminator.kind + && matches!(value.kind, E::ResourceHookValue { .. }) + { + Some(value) + } else { + None + } + }) + .expect("exact original descriptor result"); + descriptor.ty = TypeInterner::STRING; + } else { + let expression = invoke_mut(&mut changed); + let E::ResourceInvoke { + hook, + args, + evaluation_order, + ownership: CallOwnership::Source(source), + } = &mut expression.kind + else { + unreachable!() + }; + match mutation { + 1 => { + *hook = foreign + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == ResourceKernelRecipe::NetworkFactory) + .unwrap() + } + 2 => *evaluation_order = vec![0, 1], + 3 => { + source.arguments[0].staging = jett_hir::ArgumentStaging::Copied { + value: jett_hir::LocalId::new(0), + } + } + _ => args[1].ty = TypeInterner::STRING, + } + } + let errors = crate::validate_call_ownership(&changed, types).unwrap_err(); + if mutation == 5 { + assert!( + errors + .iter() + .any(|error| error.message.contains("Resource descriptor differs")), + "{errors:?}" + ); + } + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/borrowed_sums.rs b/crates/jett_mir/src/resource_ownership/borrowed_sums.rs new file mode 100644 index 00000000..e63d6ea0 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/borrowed_sums.rs @@ -0,0 +1,448 @@ +//! Constructor-owned, nonowning Handle projections. Public sum syntax is inert. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ResourceSumPayloadPath { + OptionalSome, + ResultOk, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceBorrowedSumProjection { + original_path: Vec, + original: Expression, + original_alias: Local, + original_backing: Local, + alias: Local, + backing: Local, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: ResourceSite, + observe: ResourceSite, + project: ResourceSite, + failure: Option, + failed: BlockId, + continuation: BlockId, + path: ResourceSumPayloadPath, +} +impl ResourceBorrowedSumProjection { + pub fn original_initializer(&self) -> &Expression { + &self.original + } + pub fn original_handle(&self) -> &Expression { + let E::View(handle) = &self.original.kind else { + unreachable!() + }; + handle + } + pub fn alias(&self) -> LocalId { + self.alias.id + } + pub fn backing(&self) -> LocalId { + self.backing.id + } + pub fn source(&self) -> LocalId { + self.source.id + } + pub fn output(&self) -> LocalId { + self.output.id + } + pub fn tag(&self) -> LocalId { + self.tag.id + } + pub fn path(&self) -> ResourceSumPayloadPath { + self.path + } + pub fn success_site(&self) -> ResourceSite { + self.project + } + pub fn failure_site(&self) -> Option { + self.failure + } + pub(super) fn original_local(&self) -> LocalId { + self.original_alias.id + } + pub(super) fn initialize_site(&self) -> ResourceSite { + self.initialize + } + pub(super) fn retirement_locals(&self) -> [LocalId; 3] { + [self.alias.id, self.output.id, self.source.id] + } + + fn current(&self, function: &Function) -> Result<(), String> { + let statement = |site: ResourceSite| -> Result<&Statement, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("borrowed Resource Handle site is not a statement".into()); + }; + if site.function != function.id { + return Err("borrowed Resource Handle has a foreign function".into()); + } + function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(index)) + .ok_or_else(|| "borrowed Resource Handle lost its exact current site".into()) + }; + for header in [ + &self.alias, + &self.backing, + &self.source, + &self.output, + &self.tag, + ] + .into_iter() + .chain(self.error.iter()) + { + if function.local(header.id) != Some(header) { + return Err("borrowed Resource Handle changed its sealed local header".into()); + } + } + if self.source.mutable + || self.output.mutable + || self.alias.mutable + || self.source.view_source != Some(self.backing.id) + || self.output.view_source != Some(self.backing.id) + || self.alias.view_source != Some(self.backing.id) + || self.source.ty != self.backing.ty + || self.output.ty != self.alias.ty + || self.tag.ty != TypeInterner::BOOL + || self.tag.view_source.is_some() + { + return Err("borrowed Resource Handle lost its immutable nonowning headers".into()); + } + let initialized = statement(self.initialize)?; + if !matches!(&initialized.kind, StatementKind::Let { local, value } + if *local == self.source.id && value.ty == self.source.ty + && matches!(&value.kind, E::View(inner) if inner.ty == self.source.ty && matches!(inner.kind, E::Local(backing) if backing == self.backing.id))) + || !matches!(statement(self.observe)?.kind, StatementKind::SumTag { source, target } if source == self.source.id && target == self.tag.id) + || !matches!(statement(self.project)?.kind, StatementKind::SumTake { source, target, success: true } if source == self.source.id && target == self.output.id) + { + return Err( + "borrowed Resource Handle changed its initializer, tag or projection".into(), + ); + } + let branch = &function.blocks[self.observe.block.index() as usize]; + if self.observe.block != self.initialize.block + || !matches!(&branch.terminator.kind, TerminatorKind::Branch { condition, then_block, else_block } + if condition.ty == TypeInterner::BOOL && matches!(condition.kind, E::Local(tag) if tag == self.tag.id) + && *then_block == self.project.block && *else_block == self.failed && then_block != else_block) + || !matches!(function.blocks[self.project.block.index() as usize].terminator.kind, TerminatorKind::Goto(target) if target == self.continuation) + { + return Err("borrowed Resource Handle lost its exact selecting CFG guard".into()); + } + match (&self.error, self.failure) { + (Some(error), Some(site)) + if site.block == self.failed + && matches!(statement(site)?.kind, StatementKind::SumTake { source, target, success: false } if source == self.source.id && target == error.id) => + {} + (None, None) => {} + _ => { + return Err( + "borrowed Resource Handle lost its exact failure companion read".into(), + ); + } + } + let alias_count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } if *local == self.alias.id + && value.ty == self.alias.ty && matches!(&value.kind, E::View(inner) if inner.ty == self.output.ty && matches!(inner.kind, E::Local(output) if output == self.output.id))) + }).count(); + let projection_count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(statement.kind, StatementKind::SumTake { target, success: true, .. } if target == self.output.id) + }).count(); + if alias_count != 1 || projection_count != 1 { + return Err( + "borrowed Resource Handle has copied or missing current projections".into(), + ); + } + let cfg = ControlFlowGraph::analyze(function).map_err(|error| format!("{error:?}"))?; + if !cfg.reverse_postorder().contains(&self.observe.block) + || !cfg.reverse_postorder().contains(&self.project.block) + || cfg.predecessors(self.project.block) != [self.observe.block] + || cfg.predecessors(self.failed) != [self.observe.block] + { + return Err( + "borrowed Resource Handle is disconnected from its exact selecting edge".into(), + ); + } + Ok(()) + } + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = + map.get(id.index() as usize).copied().flatten().ok_or( + "borrowed Resource Handle canonical map removed an authenticated edge", + )?; + Ok(()) + }; + for site in [&mut self.initialize, &mut self.observe, &mut self.project] + .into_iter() + .chain(self.failure.iter_mut()) + { + remap(&mut site.block)?; + } + remap(&mut self.failed)?; + remap(&mut self.continuation) + } + fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for header in [ + &mut self.alias, + &mut self.backing, + &mut self.source, + &mut self.output, + &mut self.tag, + ] + .into_iter() + .chain(self.error.iter_mut()) + { + remap(&mut header.id); + if let Some(source) = &mut header.view_source { + remap(source); + } + } + } +} + +pub(crate) struct BorrowedSumSeed { + pub(crate) original_path: Vec, + pub(crate) original: Expression, + pub(crate) alias: Local, + pub(crate) backing: Local, + pub(crate) path: ResourceSumPayloadPath, +} +impl Capture { + pub(crate) fn borrowed_sum_seed( + &self, + local: LocalId, + expression: &Expression, + types: &TypeInterner, + ) -> Result, String> { + let Some(witness) = &self.witness else { + return Ok(None); + }; + if !resource_type_pending(types, expression.ty) { + return Ok(None); + } + let mut found = None; + for declaration in lexical_borrows::declarations(witness) { + if declaration.local != local { + continue; + } + let value = &declaration.value; + if !crate::breakpoint_regions::expressions_equal(value, expression) { + return Err( + "borrowed Resource declaration changed its exact archived initializer".into(), + ); + } + let alias = witness + .original + .locals + .get(local.index() as usize) + .filter(|header| header.id == local) + .ok_or("borrowed Resource Handle original alias is absent")?; + let Some(source) = alias.view_source else { + continue; + }; + let backing = witness + .original + .locals + .get(source.index() as usize) + .filter(|header| header.id == source) + .ok_or("borrowed Resource Handle original backing is absent")?; + let Some(handle) = + hir::borrowed_sum_view_initializer(value, source, backing.ty, alias.ty, types)? + else { + continue; + }; + let E::Handle { kind, .. } = &handle.kind else { + unreachable!() + }; + let path = match kind { + hir::HandleKind::Optional => ResourceSumPayloadPath::OptionalSome, + hir::HandleKind::Result => ResourceSumPayloadPath::ResultOk, + _ => return Err("borrowed Resource Handle has an unsupported sum kind".into()), + }; + if found.is_some() || alias.mutable { + return Err( + "borrowed Resource Handle has no unique immutable original binding".into(), + ); + } + found = Some(BorrowedSumSeed { + original_path: declaration.path, + original: value.clone(), + alias: alias.clone(), + backing: backing.clone(), + path, + }); + } + Ok(found) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn record_borrowed_sum( + &mut self, + seed: BorrowedSumSeed, + source: Local, + output: Local, + tag: Local, + error: Option, + initialize: ResourceSite, + observe: ResourceSite, + project: ResourceSite, + failure: Option, + failed: BlockId, + continuation: BlockId, + ) { + if let Some(witness) = &mut self.witness { + witness.borrowed_sums.push(ResourceBorrowedSumProjection { + original_path: seed.original_path, + original: seed.original, + original_alias: seed.alias.clone(), + original_backing: seed.backing.clone(), + alias: seed.alias, + backing: seed.backing, + source, + output, + tag, + error, + initialize, + observe, + project, + failure, + failed, + continuation, + path: seed.path, + }); + } + } +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.borrowed_sums != witness.borrowed_seal { + return Err( + "borrowed Resource Handle changed its independent original/current association seal" + .into(), + ); + } + let expected = lexical_borrows::declarations(witness); + if expected.len() != witness.borrowed_sums.len() { + return Err( + "borrowed Resource Handle lost its complete archived projection inventory".into(), + ); + } + for (index, row) in witness.borrowed_sums.iter().enumerate() { + row.current(function)?; + if witness.borrowed_sums[..index] + .iter() + .any(|old| old.original_alias.id == row.original_alias.id) + { + return Err("borrowed Resource Handle original binding is duplicated".into()); + } + let count = expected + .iter() + .filter(|declaration| { + declaration.local == row.original_alias.id + && declaration.path == row.original_path + && crate::breakpoint_regions::expressions_equal( + &declaration.value, + &row.original, + ) + }) + .count(); + if count != 1 + || row.original_alias.mutable + || row.original_alias.view_source != Some(row.original_backing.id) + || row.original_alias.ty != row.original.ty + || row.original_alias.ty != row.alias.ty + || row.original_backing.ty != row.backing.ty + || witness + .original + .locals + .get(row.original_alias.id.index() as usize) + != Some(&row.original_alias) + || witness + .original + .locals + .get(row.original_backing.id.index() as usize) + != Some(&row.original_backing) + { + return Err("borrowed Resource Handle lost its exact archived original binding".into()); + } + } + Ok(()) +} +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.borrowed_seal = witness.borrowed_sums.clone(); +} +pub(super) fn remap_blocks( + rows: &mut [ResourceBorrowedSumProjection], + map: &[Option], +) -> Result<(), String> { + for row in rows { + row.remap_blocks(map)?; + } + Ok(()) +} +pub(super) fn remap_locals( + rows: &mut [ResourceBorrowedSumProjection], + remap: &mut impl FnMut(&mut LocalId), +) { + for row in rows { + row.remap_locals(remap); + } +} +impl Function { + /// Readonly exact current statement projection; public copied sum nodes grant nothing. + pub fn resource_borrowed_sum_projection( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Result, String> { + let Some(witness) = &self.resource_lowering else { + return Ok(None); + }; + witness.current(self)?; + let site = ResourceSite { + function: self.id, + block, + position, + }; + Ok(witness + .borrowed_sums + .iter() + .find(|row| row.observe == site || row.project == site || row.failure == Some(site))) + } + pub(crate) fn resource_borrowed_sum_alias( + &self, + local: LocalId, + value: &Expression, + ) -> Result { + let Some(witness) = &self.resource_lowering else { + return Ok(false); + }; + witness.current(self)?; + Ok(witness.borrowed_sums.iter().any(|row| row.alias.id == local && matches!(&value.kind, E::View(inner) if matches!(inner.kind, E::Local(output) if output == row.output.id)) + && self.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(&statement.kind, StatementKind::Let { local: defined, value: current } if *defined == local && std::ptr::eq(current, value))))) + } +} + +// Only an authenticated finite sequence prefix moves these current sites. +pub(super) fn shift_prefix(witness: &mut ResourceLoweringWitness, block: BlockId, count: usize) { + for row in &mut witness.borrowed_sums { + for site in [&mut row.initialize, &mut row.observe, &mut row.project] + .into_iter() + .chain(row.failure.iter_mut()) + { + if site.block == block + && let ResourcePosition::Statement(index) = &mut site.position + { + *index += count; + } + } + } + seal(witness); +} diff --git a/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs b/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs new file mode 100644 index 00000000..fff4b730 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/borrowed_sums_tests.rs @@ -0,0 +1,328 @@ +use super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOME: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/47_view_optional_some_written.jett" +); +const SOURCES: [&str; 12] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/45_view_optional_none_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/46_view_optional_none_bare.jett" + ), + SOME, + include_str!( + "../../../jett_driver/tests/native_conformance/resource/48_view_optional_some_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/49_view_result_fail_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/50_view_result_fail_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/51_view_result_ok_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/52_view_result_ok_bare.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/53_view_optional_written_argument_abort.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/54_view_result_bare_argument_abort.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/55_view_optional_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/56_view_result_bare_borrow_fail.jett" + ), +]; + +fn observer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name.starts_with("observe_") + }) + .unwrap() +} +fn observer_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name.starts_with("observe_") + }) + .unwrap() +} + +#[test] +fn resource_borrowed_sums_loan_order_preserves_exact_tag_path_and_parent() { + let projected = |parent, tag, path| ResourceLoanSource::ProjectedSumPayload { + parent: ResourceLoanId(parent), + tag: LocalId::new(tag), + path, + }; + let sources = [ + ResourceLoanSource::Owner(ResourceOwnerSlotId(0)), + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter: 0, + }, + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter: 1, + }, + ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(1), + parameter: 0, + }, + projected(0, 0, ResourceSumPayloadPath::OptionalSome), + projected(0, 1, ResourceSumPayloadPath::OptionalSome), + projected(0, 0, ResourceSumPayloadPath::ResultOk), + projected(1, 0, ResourceSumPayloadPath::OptionalSome), + ]; + for left in sources { + for right in sources { + assert_eq!(left.cmp(&right).is_eq(), left == right); + } + } + assert_eq!( + sources.into_iter().collect::>().len(), + sources.len() + ); +} + +#[test] +fn resource_borrowed_sums_preserve_nonowning_headers_and_all_source_call_shapes() { + for release in [false, true] { + for (fixture, source) in SOURCES.into_iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "borrowed canonical fixture {} release={release}", + fixture + 45 + ); + } + let ownership = validate_resource_ownership(&program, types).unwrap_or_else(|errors| { + panic!( + "borrowed fixture {} release={release}: {errors:?}", + fixture + 45 + ) + }); + let current = observer(&program); + let plan = ownership.function(current.id).unwrap(); + let projected = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::ProjectSumView { + source, + destination, + tag, + path, + } => Some((*source, *destination, *tag, *path, operation.site())), + _ => None, + }) + .expect("exact guarded borrowed payload projection"); + let (parent, child, tag, path, site) = projected; + let row = current + .resource_borrowed_sum_projection(site.block(), site.position()) + .unwrap() + .unwrap(); + assert_eq!(row.tag(), tag); + assert_eq!(row.path(), path); + assert!(matches!( + plan.loans()[parent.index()].source(), + ResourceLoanSource::IncomingViewFormal { .. } + )); + assert!(plan.loans()[parent.index()].shape().conditional()); + assert_eq!( + plan.loans()[child.index()].source(), + ResourceLoanSource::ProjectedSumPayload { parent, tag, path } + ); + assert!(!plan.loans()[child.index()].shape().conditional()); + for local in [row.source(), row.output(), row.alias()] { + assert!(current.is_view_local(local)); + assert!(!current.local(local).unwrap().mutable); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + assert!(!plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if *loan == parent))); + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan } if *loan == child))); + if fixture >= 4 && fixture != 8 && fixture != 10 { + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::ReadFailureCompanion { failure, .. } if *failure == TypeInterner::STRING))); + } + for function in ownership.functions() { + let companion = ResourceCompanionPlan::analyze(&ownership, function.function()) + .unwrap_or_else(|error| { + panic!( + "borrowed companion fixture {} release={release}: {error}", + fixture + 45 + ) + }); + let current = &program.functions[function.function().index() as usize]; + assert!( + current + .locals + .iter() + .filter(|local| resource_type_pending(types, local.ty)) + .all(|local| !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize))) + ); + } + let main = ownership + .functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == "main" + }) + .unwrap(); + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::BorrowSum { .. } + | ResourceOperationRole::PrepareSourceSumBorrow { .. } + ))); + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::EndSumBorrow { .. } + ))); + if fixture == 8 || fixture == 9 { + assert!(main.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::PrepareSourceSumBorrow { .. } + ))); + } + } + } +} + +#[test] +fn resource_borrowed_sums_refuse_header_tag_and_disconnected_projection_resealing() { + for release in [false, true] { + let checked = checked_support(SOME, SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let current = observer(&baseline); + let row = current + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .first() + .unwrap(); + let output = row.output(); + let source = row.source(); + let backing = row.backing(); + let site = row.success_site(); + for mutation in 0..8 { + let mut forged = baseline.clone(); + let current = observer_mut(&mut forged); + match mutation { + 0 => current.locals[output.index() as usize].view_source = None, + 1 => current.locals[output.index() as usize].mutable = true, + 2 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let StatementKind::SumTake { source, .. } = + &mut current.blocks[site.block().index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = backing; + } + 3 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let StatementKind::SumTake { success, .. } = + &mut current.blocks[site.block().index() as usize].statements[index].kind + else { + unreachable!() + }; + *success = false; + } + 4 => { + let row = current + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .first() + .unwrap(); + let tag = row.tag(); + let branch = current.blocks.iter_mut().find(|block| matches!(&block.terminator.kind, TerminatorKind::Branch { condition, .. } if matches!(condition.kind, hir::ExpressionKind::Local(local) if local == tag))).unwrap(); + let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + else { + unreachable!() + }; + *then_block = *else_block; + } + 5 => { + let ResourcePosition::Statement(index) = site.position() else { + unreachable!() + }; + let statement = + current.blocks[site.block().index() as usize].statements[index].clone(); + current.blocks.push(BasicBlock { + id: BlockId(current.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: current.span, + }, + }); + } + 6 => current + .resource_lowering + .as_mut() + .unwrap() + .borrowed_sums + .clear(), + 7 => { + current.locals[source.index() as usize].ty = + current.locals[output.index() as usize].ty + } + _ => unreachable!(), + } + // Public general cache resealing must not replace independent transport. + let witness = current.resource_lowering.as_mut().unwrap(); + witness.blocks = current.blocks.clone(); + witness.locals = current.locals.clone(); + assert!( + validate_resource_ownership(&forged, types).is_err(), + "mutation={mutation} release={release}" + ); + assert!( + !crate::sequences::prune::unreachable(observer_mut(&mut forged)), + "canonical mutation={mutation} release={release}" + ); + } + let mut missing = baseline.clone(); + observer_mut(&mut missing).resource_lowering = None; + assert!(validate_resource_ownership(&missing, types).is_err()); + let mut foreign = baseline.clone(); + let other = checked_support(SOME, SUPPORT, release); + observer_mut(&mut foreign).resource_lowering = + observer(&lowered(&other)).resource_lowering.clone(); + assert!(validate_resource_ownership(&foreign, types).is_err()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/carriers.rs b/crates/jett_mir/src/resource_ownership/carriers.rs new file mode 100644 index 00000000..138b1fb4 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers.rs @@ -0,0 +1,474 @@ +//! Constructor-issued typed aggregate transport. No graph row grants leaf custody. +//! Every runtime root is validated to contain zero selected Resource leaves before +//! it is committed; latent occupied shape metadata is retained without execution. +use super::*; +use hir::ExpressionKind as E; +use std::sync::Arc; + +mod flow; +mod graph; +mod iterations; +mod planner; + +identity!(ResourceCarrierShapeId); +identity!(ResourceCarrierSlotId); +identity!(ResourceCarrierLoanId); +identity!(ResourceCarrierOperationId); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierField { + pub ordinal: usize, + pub name: String, + pub shape: ResourceCarrierShapeId, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierVariant { + pub ordinal: usize, + pub name: String, + pub discriminant: i64, + pub fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierState { + pub ordinal: usize, + pub name: String, + pub fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierNode { + Ordinary, + Resource { + kind: hir::ResourceKindRef, + }, + Optional { + payload: ResourceCarrierShapeId, + }, + Result { + success: ResourceCarrierShapeId, + failure: ResourceCarrierShapeId, + }, + List { + element: ResourceCarrierShapeId, + }, + Map { + key: ResourceCarrierShapeId, + value: ResourceCarrierShapeId, + }, + Struct { + owner: jett_types::StructId, + arguments: Vec, + fields: Vec, + }, + Enum { + owner: jett_types::EnumId, + arguments: Vec, + variants: Vec, + }, + Machine { + owner: jett_types::MachineId, + states: Vec, + transitions: Vec<(usize, usize)>, + }, + MachineState { + machine: ResourceCarrierShapeId, + state: usize, + }, + Refinement { + name: String, + base: ResourceCarrierShapeId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierShape { + id: ResourceCarrierShapeId, + ty: TypeId, + node: ResourceCarrierNode, + contains_resource: bool, + refinement_predicate: Option, +} +impl ResourceCarrierShape { + pub fn id(&self) -> ResourceCarrierShapeId { + self.id + } + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn node(&self) -> &ResourceCarrierNode { + &self.node + } + pub fn contains_resource(&self) -> bool { + self.contains_resource + } + /// Original checked declaration/body association; metadata grants no proof + /// that a raw base value satisfies the refinement at runtime. + pub fn refinement_predicate(&self) -> Option<&hir::Function> { + self.refinement_predicate.as_ref() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierGeneration { + /// Runtime activation issues a fresh generation on each successful install. + FreshOnInstall, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierSlot { + id: ResourceCarrierSlotId, + frame: ResourceFrameId, + shape: ResourceCarrierShapeId, + storage: ResourceSlotStorage, +} +impl ResourceCarrierSlot { + pub fn id(&self) -> ResourceCarrierSlotId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> ResourceCarrierShapeId { + self.shape + } + pub fn storage(&self) -> &ResourceSlotStorage { + &self.storage + } + pub fn generation(&self) -> ResourceCarrierGeneration { + ResourceCarrierGeneration::FreshOnInstall + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierIndex { + Constant(usize), + Local(LocalId), + CurrentIteration { header: BlockId }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierEdge { + SwitchVariant { + source: BlockId, + variant: VariantId, + target: BlockId, + }, + SwitchOtherwise { + source: BlockId, + target: BlockId, + }, + IterationBody { + source: BlockId, + target: BlockId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierIteration { + pub header: BlockId, + pub body: BlockId, + pub exit: BlockId, + pub source: ResourceCarrierSlotId, + pub binders: Vec, + pub cursor: Option, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierProjectionPath { + Field { + owner: TypeId, + field: FieldId, + }, + Variant { + owner: TypeId, + variant: VariantId, + field: usize, + }, + State { + owner: TypeId, + state: usize, + field: usize, + }, + Element { + index: ResourceCarrierIndex, + }, + MapKey { + index: ResourceCarrierIndex, + }, + MapValue { + index: ResourceCarrierIndex, + }, + OptionalSome, + ResultOk, + ResultFail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierLoanSource { + Slot(ResourceCarrierSlotId), + Loan(ResourceCarrierLoanId), + IncomingViewFormal { + scope: ResourceFrameId, + parameter: usize, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierLoan { + id: ResourceCarrierLoanId, + frame: ResourceFrameId, + shape: ResourceCarrierShapeId, + source: ResourceCarrierLoanSource, + path: Vec, +} +impl ResourceCarrierLoan { + pub fn id(&self) -> ResourceCarrierLoanId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> ResourceCarrierShapeId { + self.shape + } + pub fn source(&self) -> ResourceCarrierLoanSource { + self.source + } + pub fn path(&self) -> &[ResourceCarrierProjectionPath] { + &self.path + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierValue { + Ordinary { ty: TypeId }, + Owned { slot: ResourceCarrierSlotId }, + Borrowed { loan: ResourceCarrierLoanId }, + LeafOwned { slot: ResourceOwnerSlotId }, + LeafBorrowed { loan: ResourceLoanId }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCarrierConstructor { + List, + Map, + Struct, + Enum { variant: VariantId }, + Machine { state: usize }, + OptionalNone, + OptionalSome, + ResultOk, + ResultFail, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceCarrierChild { + pub index: usize, + pub shape: ResourceCarrierShapeId, + pub value: ResourceCarrierValue, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCarrierObservation { + Length, + Tag, + State { + state: usize, + }, + Ordinary { + path: Vec, + ty: TypeId, + }, +} +#[derive(Debug, Clone)] +pub enum ResourceCarrierOperationRole { + BeginConstructor { + destination: ResourceCarrierSlotId, + constructor: ResourceCarrierConstructor, + }, + ConstructorChild { + destination: ResourceCarrierSlotId, + child: ResourceCarrierChild, + }, + CommitConstructor { + destination: ResourceCarrierSlotId, + }, + Transfer { + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + }, + QualifyMachine { + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + machine: ResourceCarrierShapeId, + state: usize, + }, + Borrow { + loan: ResourceCarrierLoanId, + }, + EndBorrow { + loan: ResourceCarrierLoanId, + }, + Observe { + source: ResourceCarrierLoanId, + observation: ResourceCarrierObservation, + target: Option, + }, + Project { + source: ResourceCarrierLoanId, + destination: ResourceCarrierLoanId, + }, + Extract { + source: ResourceCarrierLoanId, + path: Vec, + destination: ResourceCarrierSlotId, + }, + AdaptSum { + source: ResourceCarrierLoanId, + path: Vec, + destination: ResourceOwnerSlotId, + loan: Option, + lease_frame: ResourceFrameId, + }, + ExtractOrdinary { + source: ResourceCarrierLoanId, + path: Vec, + target: LocalId, + ty: TypeId, + }, + BeginIteration { + source: ResourceCarrierValue, + destination: ResourceCarrierSlotId, + body: BlockId, + exit: BlockId, + }, + RetireIteration { + source: ResourceCarrierSlotId, + binders: Vec, + }, + PublishReturn { + source: ResourceCarrierSlotId, + }, + Retire { + source: ResourceCarrierSlotId, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceCarrierOperation { + id: ResourceCarrierOperationId, + site: ResourceSite, + frame: ResourceFrameId, + role: ResourceCarrierOperationRole, + expression: Option, + edge: Option, +} +impl ResourceCarrierOperation { + pub fn id(&self) -> ResourceCarrierOperationId { + self.id + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn role(&self) -> &ResourceCarrierOperationRole { + &self.role + } + pub fn edge(&self) -> Option { + self.edge + } + pub fn is_expression_operation(&self) -> bool { + self.expression.is_some() + } + pub fn is_for_expression(&self, expression: &Expression) -> bool { + self.expression == Some(std::ptr::from_ref(expression).addr()) + } +} +#[derive(Debug)] +pub struct ResourceCarrierFunctionPlan { + shapes: Arc>, + slots: Vec, + loans: Vec, + operations: Vec, + iterations: Vec, +} +impl ResourceCarrierFunctionPlan { + pub fn shapes(&self) -> &[ResourceCarrierShape] { + &self.shapes + } + pub fn shape_for_type(&self, ty: TypeId) -> Option<&ResourceCarrierShape> { + self.shapes + .get(usize::try_from(ty.index()).ok()?) + .filter(|row| row.ty == ty) + } + pub fn shape(&self, id: ResourceCarrierShapeId) -> Option<&ResourceCarrierShape> { + self.shapes.get(id.index()).filter(|row| row.id == id) + } + pub fn slots(&self) -> &[ResourceCarrierSlot] { + &self.slots + } + pub fn loans(&self) -> &[ResourceCarrierLoan] { + &self.loans + } + pub fn operations(&self) -> &[ResourceCarrierOperation] { + &self.operations + } + pub fn iterations(&self) -> &[ResourceCarrierIteration] { + &self.iterations + } + pub fn operations_for_expression<'a>( + &'a self, + expression: &'a Expression, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.is_for_expression(expression)) + } + pub fn operations_at( + &self, + site: ResourceSite, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.site == site && operation.edge.is_none()) + } + pub fn operations_on_edge( + &self, + edge: ResourceCarrierEdge, + ) -> impl Iterator { + self.operations + .iter() + .filter(move |operation| operation.edge == Some(edge)) + } +} + +/// Leaf and function descriptor types retain their existing protocol. +pub(super) fn carrier_type(types: &TypeInterner, ty: TypeId) -> bool { + if !resource_type_pending(types, ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) | Type::Function { .. } => false, + Type::Optional(inner) if matches!(types.resolve(*inner), Type::Resource(_)) => false, + Type::Result(ok, fail) + if matches!(types.resolve(*ok), Type::Resource(_)) + && !resource_type_pending(types, *fail) => + { + false + } + _ => true, + } +} +pub(super) fn required(function: &Function, types: &TypeInterner) -> bool { + (carrier_type(types, function.return_type) + && crate::native_graph_storage_for_return(function, types).is_none()) + || function.locals.iter().any(|local| { + carrier_type(types, local.ty) + && !crate::actor_graph::native_graph_metadata_local(function, types, local) + && crate::native_graph_storage_for_local(function, types, local).is_none() + }) + || function.blocks.iter().any(|block| { + let mut found = false; + walk::mir_block(block, &mut |value| { + found |= carrier_type(types, value.ty) + && !crate::actor_graph::native_graph_metadata_expression(function, types, value) + && crate::native_graph_storage_for_expression(function, types, value).is_none() + }); + found + }) +} +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + planner::analyze(program, function, types) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/flow.rs b/crates/jett_mir/src/resource_ownership/carriers/flow.rs new file mode 100644 index 00000000..0be18003 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/flow.rs @@ -0,0 +1,1527 @@ +//! Independent current-CFG zero-leaf custody proof. IDs describe exact sealed +//! endpoints, never values that can grant native permission on their own. +use super::*; +use std::collections::VecDeque; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Origin { + Formal(usize), + Constructor(ResourceCarrierOperationId), + Extraction(ResourceCarrierOperationId), + Source(ResourceOperationId), + LeafAbsence(ResourceOperationId), +} +#[derive(Debug, Clone, PartialEq, Eq)] +enum Slot { + Vacant, + Building { + origin: Origin, + expected: Vec, + order: Vec, + arrived: usize, + }, + Live { + origin: Origin, + taken: Vec>, + }, + Moved, +} +#[derive(Debug, Clone, PartialEq, Eq)] +enum Leaf { + Vacant, + Empty { + origin: Origin, + }, + Occupied { + origin: Origin, + }, + Adapter { + operation: ResourceCarrierOperationId, + }, + Moved, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Root { + Slot(ResourceCarrierSlotId), + Incoming(ResourceCarrierLoanId), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Anchor { + root: Root, + origin: Origin, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Selector { + Sum(bool), + Variant(usize), + State(usize), + Iteration(BlockId), +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Selection { + anchor: Anchor, + path: Vec, + selector: Selector, +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct Probe { + anchor: Anchor, + path: Vec, + shape: ResourceCarrierShapeId, +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct State { + slots: Vec, + leaves: Vec, + loans: BTreeSet, + leaf_loans: BTreeSet, + frames: Vec, + returned: bool, + selections: Vec, + tags: BTreeMap, + switches: BTreeMap, + retired_projection: Option<(ResourceCarrierLoanId, ResourceCarrierSlotId, Origin)>, +} +struct Analysis<'a> { + function: &'a Function, + plan: &'a ResourceFunctionPlan, + carriers: &'a ResourceCarrierFunctionPlan, +} +impl Analysis<'_> { + fn frame(&self, state: &mut State, frame: ResourceFrameId) -> Result<(), String> { + if frame == ResourceFrameId(0) { + if state.frames.last().copied() != Some(frame) { + return Err("carrier operation lost its exact live Scope prefix".into()); + } + return Ok(()); + } + let row = self + .plan + .frames + .get(frame.index()) + .filter(|row| row.id == frame) + .ok_or("carrier operation frame missing")?; + if row.role == ResourceFrameRole::Return { + if row.parent.is_some() { + return Err("carrier provisional Return frame has a foreign parent".into()); + } + return Ok(()); + } + if state.frames.last().copied() == Some(frame) { + return Ok(()); + } + let parent = row + .parent + .ok_or("carrier Operation frame lost its parent")?; + if state.frames.last().copied() != Some(parent) { + return Err("carrier operation changed its exact frame prefix".into()); + } + state.frames.push(frame); + Ok(()) + } + fn live<'s>(&self, state: &'s State, slot: ResourceCarrierSlotId) -> Result<&'s Slot, String> { + let value = state + .slots + .get(slot.index()) + .ok_or("carrier slot outside its exact plan")?; + if !matches!(value, Slot::Live { .. }) { + return Err("carrier source is uninitialized, moved, or incompletely constructed at this CFG site".into()); + } + Ok(value) + } + fn root(&self, loan: ResourceCarrierLoanId) -> Result, String> { + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier loan parent graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .filter(|row| row.id == current) + .ok_or("carrier loan missing")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => return Ok(Some(slot)), + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::IncomingViewFormal { .. } => return Ok(None), + } + } + } + fn unleased(&self, state: &State, slot: ResourceCarrierSlotId) -> Result<(), String> { + for loan in &state.loans { + if self.root(*loan)? == Some(slot) { + return Err( + "carrier move/retirement has an exact live parent or child lease".into(), + ); + } + } + for loan in &state.leaf_loans { + if let ResourceLoanSource::CarrierSumProjection { operation } = + self.plan.loans[loan.index()].source + { + let ResourceCarrierOperationRole::AdaptSum { source, .. } = + self.carriers.operations[operation.index()].role + else { + return Err("carrier leaf loan lost its exact adapter role".into()); + }; + if self.root(source)? == Some(slot) { + return Err("carrier move/retirement has a live projected sum lease".into()); + } + } + } + Ok(()) + } + fn install( + &self, + state: &mut State, + slot: ResourceCarrierSlotId, + origin: Origin, + ) -> Result<(), String> { + if !matches!( + state.slots.get(slot.index()), + Some(Slot::Vacant | Slot::Moved) + ) { + return Err( + "carrier destination is already live outside an authenticated binder reset".into(), + ); + } + state.slots[slot.index()] = Slot::Live { + origin, + taken: Vec::new(), + }; + Ok(()) + } + fn take(&self, state: &mut State, slot: ResourceCarrierSlotId) -> Result { + self.live(state, slot)?; + self.unleased(state, slot)?; + state + .selections + .retain(|fact| fact.anchor.root != Root::Slot(slot)); + state + .tags + .retain(|_, probe| probe.anchor.root != Root::Slot(slot)); + state + .switches + .retain(|_, probe| probe.anchor.root != Root::Slot(slot)); + Ok(std::mem::replace( + &mut state.slots[slot.index()], + Slot::Moved, + )) + } + fn take_complete( + &self, + state: &mut State, + slot: ResourceCarrierSlotId, + ) -> Result { + if !matches!(self.live(state, slot)?, Slot::Live { taken, .. } if taken.is_empty()) { + return Err("carrier whole-root transfer, adoption, Source transport or publication has extracted children".into()); + } + self.take(state, slot) + } + fn view_complete(&self, state: &State, loan: ResourceCarrierLoanId) -> Result<(), String> { + self.loan_live(state, loan)?; + if let Some(root) = self.root(loan)? { + let path = self.absolute_path(loan, &[])?; + let Slot::Live { taken, .. } = self.live(state, root)? else { + return Err("carrier view Source root is not installed".into()); + }; + if taken + .iter() + .any(|prior| prior.starts_with(&path) || path.starts_with(prior)) + { + return Err( + "carrier view Source formal cannot receive an incomplete selected subtree" + .into(), + ); + } + } + Ok(()) + } + fn loan_live(&self, state: &State, loan: ResourceCarrierLoanId) -> Result<(), String> { + if !state.loans.contains(&loan) { + return Err( + "carrier observation/projection uses an ended or unreached exact loan".into(), + ); + } + if let Some(root) = self.root(loan)? { + self.live(state, root)?; + } + if self.projected_shape(state, loan, &[])? != self.carriers.loans[loan.index()].shape { + return Err( + "carrier active loan changed its exact parent generation/path shape".into(), + ); + } + Ok(()) + } + fn absolute_path( + &self, + loan: ResourceCarrierLoanId, + suffix: &[ResourceCarrierProjectionPath], + ) -> Result, String> { + let mut ancestors = Vec::new(); + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier path parent graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .ok_or("carrier path loan missing")?; + ancestors.push(row.path.clone()); + match row.source { + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::Slot(_) + | ResourceCarrierLoanSource::IncomingViewFormal { .. } => break, + } + } + let mut path = Vec::new(); + for ancestor in ancestors.into_iter().rev() { + path.extend(ancestor); + } + path.extend_from_slice(suffix); + Ok(path) + } + fn anchor(&self, state: &State, loan: ResourceCarrierLoanId) -> Result { + let mut current = loan; + let mut seen = BTreeSet::new(); + loop { + if !seen.insert(current) { + return Err("carrier anchor loan graph is cyclic".into()); + } + let row = self + .carriers + .loans + .get(current.index()) + .ok_or("carrier anchor loan missing")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => { + let Slot::Live { origin, .. } = self.live(state, slot)? else { + return Err("carrier anchor is not live".into()); + }; + return Ok(Anchor { + root: Root::Slot(slot), + origin: *origin, + }); + } + ResourceCarrierLoanSource::Loan(parent) => current = parent, + ResourceCarrierLoanSource::IncomingViewFormal { parameter, .. } => { + return Ok(Anchor { + root: Root::Incoming(current), + origin: Origin::Formal(parameter), + }); + } + } + } + } + fn selection( + &self, + state: &State, + anchor: Anchor, + path: &[ResourceCarrierProjectionPath], + selector: Selector, + ) -> bool { + state + .selections + .iter() + .any(|fact| fact.anchor == anchor && fact.path == path && fact.selector == selector) + } + fn select(&self, state: &mut State, probe: &Probe, selector: Selector) { + state + .selections + .retain(|fact| fact.anchor != probe.anchor || fact.path != probe.path); + state.selections.push(Selection { + anchor: probe.anchor, + path: probe.path.clone(), + selector, + }); + } + fn projected_shape( + &self, + state: &State, + loan: ResourceCarrierLoanId, + suffix: &[ResourceCarrierProjectionPath], + ) -> Result { + let anchor = self.anchor(state, loan)?; + let mut current = match anchor.root { + Root::Slot(slot) => self.carriers.slots[slot.index()].shape, + Root::Incoming(loan) => self.carriers.loans[loan.index()].shape, + }; + let path = self.absolute_path(loan, suffix)?; + if let Root::Slot(slot) = anchor.root { + let Slot::Live { taken, .. } = self.live(state, slot)? else { + return Err("carrier path root is not installed".into()); + }; + if taken.iter().any(|prior| path.starts_with(prior)) { + return Err("carrier projection reads an already extracted selected path".into()); + } + } + let mut prefix = Vec::new(); + for part in &path { + let shape = self + .carriers + .shape(current) + .ok_or("carrier selected path graph row missing")?; + current = match (part, &shape.node) { + (ResourceCarrierProjectionPath::Field { owner, field }, ResourceCarrierNode::Struct { fields, .. }) if *owner == shape.ty => fields.get(field.index() as usize).ok_or("carrier field outside its exact nominal definition")?.shape, + (ResourceCarrierProjectionPath::Variant { owner, variant, field }, ResourceCarrierNode::Enum { variants, .. }) if *owner == shape.ty => { + if !self.selection(state, anchor, &prefix, Selector::Variant(variant.index() as usize)) { return Err("carrier variant projection lacks its exact current selecting Switch edge".into()); } + variants.get(variant.index() as usize).and_then(|variant| variant.fields.get(*field)).ok_or("carrier variant payload outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::State { owner, state: selected, field }, ResourceCarrierNode::MachineState { machine, state: actual }) if *owner == shape.ty && selected == actual => { + let ResourceCarrierNode::Machine { states, .. } = &self.carriers.shape(*machine).ok_or("carrier machine graph missing")?.node else { return Err("carrier state owner is not a machine".into()); }; + states.get(*selected).and_then(|state| state.fields.get(*field)).ok_or("carrier state field outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::State { owner, state: selected, field }, ResourceCarrierNode::Machine { states, .. }) => { + let qualified = self.carriers.shape_for_type(*owner).ok_or("carrier field qualification graph missing")?; + if !matches!(qualified.node, ResourceCarrierNode::MachineState { machine, state } if machine == current && state == *selected) || !self.selection(state, anchor, &prefix, Selector::State(*selected)) { return Err("carrier state projection lacks its exact current StateIs edge/owner qualification".into()); } + states.get(*selected).and_then(|state| state.fields.get(*field)).ok_or("carrier state field outside its exact graph")?.shape + } + (ResourceCarrierProjectionPath::OptionalSome, ResourceCarrierNode::Optional { payload }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(true)) { return Err("carrier Optional payload lacks its exact selecting typed SumTag edge".into()); } + *payload + } + (ResourceCarrierProjectionPath::ResultOk, ResourceCarrierNode::Result { success, .. }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(true)) { return Err("carrier Result ok payload lacks its exact selecting typed SumTag edge".into()); } + *success + } + (ResourceCarrierProjectionPath::ResultFail, ResourceCarrierNode::Result { failure, .. }) => { + if !self.selection(state, anchor, &prefix, Selector::Sum(false)) { return Err("carrier Result fail payload lacks its exact selecting typed SumTag edge".into()); } + *failure + } + (ResourceCarrierProjectionPath::Element { index }, ResourceCarrierNode::List { element }) => { self.iteration_index(state, anchor, &prefix, *index)?; *element } + (ResourceCarrierProjectionPath::MapKey { index }, ResourceCarrierNode::Map { key, .. }) => { self.iteration_index(state, anchor, &prefix, *index)?; *key } + (ResourceCarrierProjectionPath::MapValue { index }, ResourceCarrierNode::Map { value, .. }) => { self.iteration_index(state, anchor, &prefix, *index)?; *value } + _ => return Err("carrier selected path changes its exact graph family, nominal owner or qualification".into()), + }; + prefix.push(part.clone()); + } + Ok(current) + } + fn iteration_index( + &self, + state: &State, + anchor: Anchor, + path: &[ResourceCarrierProjectionPath], + index: ResourceCarrierIndex, + ) -> Result<(), String> { + let Root::Slot(slot) = anchor.root else { + return Err( + "pending Resource carrier: borrowed iterator index has no owning loop root".into(), + ); + }; + let row = self + .carriers + .iterations + .iter() + .find(|row| { + row.source == slot + && match index { + ResourceCarrierIndex::Local(cursor) => row.cursor == Some(cursor), + ResourceCarrierIndex::CurrentIteration { header } => { + row.cursor.is_none() && row.header == header + } + ResourceCarrierIndex::Constant(_) => false, + } + }) + .ok_or("carrier dynamic index is outside its exact original/current iterator cursor")?; + if !path.is_empty() || !self.selection(state, anchor, path, Selector::Iteration(row.header)) + { + return Err( + "carrier indexed extraction lacks its exact selected current iterator body edge" + .into(), + ); + } + Ok(()) + } + fn probe(&self, state: &State, loan: ResourceCarrierLoanId) -> Result { + Ok(Probe { + anchor: self.anchor(state, loan)?, + path: self.absolute_path(loan, &[])?, + shape: self.projected_shape(state, loan, &[])?, + }) + } + fn extract( + &self, + state: &mut State, + source: ResourceCarrierLoanId, + path: &[ResourceCarrierProjectionPath], + ) -> Result<(), String> { + let root = self + .root(source)? + .ok_or("carrier owning extraction cannot consume an incoming view")?; + let origin = match self.live(state, root)? { + Slot::Live { origin, .. } => *origin, + _ => return Err("carrier extraction lost its installed root".into()), + }; + if state.retired_projection.take() != Some((source, root, origin)) { + return Err("carrier owning extraction lacks its immediately retired exact projection lease/generation".into()); + } + self.unleased(state, root)?; + self.projected_shape(state, source, path)?; + let key = self.absolute_path(source, path)?; + let Slot::Live { taken, .. } = &mut state.slots[root.index()] else { + return Err("carrier extraction root is not installed".into()); + }; + if taken + .iter() + .any(|prior| prior.starts_with(&key) || key.starts_with(prior)) + { + return Err("carrier selected path was already extracted at this generation".into()); + } + taken.push(key); + Ok(()) + } + fn constructor( + &self, + operation: &ResourceCarrierOperation, + destination: ResourceCarrierSlotId, + constructor: &ResourceCarrierConstructor, + ) -> Result<(Vec, Vec), String> { + let shape = self + .carriers + .shape(self.carriers.slots[destination.index()].shape) + .ok_or("carrier constructor graph row missing")?; + let mut dynamic = None; + let mut written_order = None; + if !operation.is_expression_operation() { + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("carrier constructor lost its checked Source witness")?; + if let Some(region) = witness + .constructors + .iter() + .find(|r| r.begin() == operation.site) + { + self.function.resource_constructor(region.id())?; + written_order = Some(region.evaluation_order().to_vec()); + } + } + let block = self + .function + .blocks + .get(operation.site.block.index() as usize) + .ok_or("carrier constructor site block missing")?; + walk::mir_block(block, &mut |expression| { + if !operation.is_for_expression(expression) { + return; + } + match &expression.kind { + E::ListConstruct { elements } => dynamic = Some(elements.len()), + E::MapConstruct { entries } => dynamic = entries.len().checked_mul(2), + E::StructConstruct { + evaluation_order, .. + } + | E::EnumConstruct { + evaluation_order, .. + } => written_order = Some(evaluation_order.clone()), + _ => {} + } + }); + let expected = match (constructor, &shape.node) { + (ResourceCarrierConstructor::List, ResourceCarrierNode::List { element }) => { + vec![ + *element; + dynamic.ok_or("carrier list constructor lost its exact current arity")? + ] + } + (ResourceCarrierConstructor::Map, ResourceCarrierNode::Map { key, value }) => (0 + ..dynamic.ok_or("carrier map constructor lost its exact current arity")?) + .map(|index| if index % 2 == 0 { *key } else { *value }) + .collect(), + (ResourceCarrierConstructor::Struct, ResourceCarrierNode::Struct { fields, .. }) => { + fields.iter().map(|field| field.shape).collect() + } + ( + ResourceCarrierConstructor::Enum { variant }, + ResourceCarrierNode::Enum { variants, .. }, + ) => variants + .get(variant.index() as usize) + .ok_or("carrier constructor variant outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect(), + ( + ResourceCarrierConstructor::Machine { state }, + ResourceCarrierNode::Machine { states, .. }, + ) => states + .get(*state) + .ok_or("carrier constructor state outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect(), + ( + ResourceCarrierConstructor::Machine { state }, + ResourceCarrierNode::MachineState { + machine, + state: selected, + }, + ) if state == selected => { + let ResourceCarrierNode::Machine { states, .. } = &self + .carriers + .shape(*machine) + .ok_or("carrier qualified machine graph missing")? + .node + else { + return Err("carrier qualified owner is not a machine".into()); + }; + states + .get(*state) + .ok_or("carrier qualified state outside its exact graph")? + .fields + .iter() + .map(|field| field.shape) + .collect() + } + (ResourceCarrierConstructor::OptionalNone, ResourceCarrierNode::Optional { .. }) => { + Vec::new() + } + ( + ResourceCarrierConstructor::OptionalSome, + ResourceCarrierNode::Optional { payload }, + ) => vec![*payload], + (ResourceCarrierConstructor::ResultOk, ResourceCarrierNode::Result { success, .. }) => { + vec![*success] + } + ( + ResourceCarrierConstructor::ResultFail, + ResourceCarrierNode::Result { failure, .. }, + ) => vec![*failure], + _ => { + return Err( + "carrier constructor role differs from its exact typed graph family".into(), + ); + } + }; + let order = written_order.unwrap_or_else(|| (0..expected.len()).collect()); + if order.len() != expected.len() + || order.iter().copied().collect::>() != (0..expected.len()).collect() + { + return Err( + "carrier constructor has a noncanonical lexical arrival permutation".into(), + ); + } + Ok((expected, order)) + } + fn carrier( + &self, + state: &mut State, + operation: &ResourceCarrierOperation, + ) -> Result<(), String> { + if !matches!( + operation.role, + ResourceCarrierOperationRole::PublishReturn { .. } + ) { + self.frame(state, operation.frame)?; + } + if !matches!( + operation.role, + ResourceCarrierOperationRole::Extract { .. } + | ResourceCarrierOperationRole::ExtractOrdinary { .. } + | ResourceCarrierOperationRole::AdaptSum { loan: None, .. } + ) { + state.retired_projection = None; + } + match &operation.role { + ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor, + } => { + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier constructor overwrites a live root".into()); + } + let (expected, order) = self.constructor(operation, *destination, constructor)?; + state.slots[destination.index()] = Slot::Building { + origin: Origin::Constructor(operation.id), + expected, + order, + arrived: 0, + }; + } + ResourceCarrierOperationRole::ConstructorChild { destination, child } => { + let Slot::Building { + expected, + order, + arrived, + .. + } = &state.slots[destination.index()] + else { + return Err("carrier constructor child precedes its exact Begin".into()); + }; + if order.get(*arrived) != Some(&child.index) + || expected.get(child.index) != Some(&child.shape) + { + return Err("carrier constructor child changes its exact lexical prefix or declared graph node".into()); + } + match child.value { + ResourceCarrierValue::Ordinary { ty } => { + let shape = self + .carriers + .shape(child.shape) + .ok_or("carrier ordinary child graph missing")?; + if shape.ty != ty || shape.contains_resource { + return Err("carrier constructor cannot adopt custody through an ordinary payload channel".into()); + } + } + ResourceCarrierValue::Owned { slot } => { + if self.carriers.slots[slot.index()].shape != child.shape { + return Err("carrier constructor adoption changes its child's exact installed graph node".into()); + } + self.take_complete(state, slot)?; + } + ResourceCarrierValue::LeafOwned { slot } => { + let endpoint = self + .plan + .slots + .get(slot.index()) + .ok_or("carrier child lost its exact leaf holder")?; + let node = &self + .carriers + .shape(child.shape) + .ok_or("carrier child lost its exact leaf graph")? + .node; + let valid = match (&endpoint.shape, node, state.leaves.get(slot.index())) { + ( + ResourceShape::Plain { kind }, + ResourceCarrierNode::Resource { kind: child_kind }, + Some(Leaf::Occupied { .. }), + ) => kind == child_kind, + ( + ResourceShape::Optional { .. } | ResourceShape::Result { .. }, + _, + Some(Leaf::Empty { .. }), + ) => true, + _ => false, + }; + if !valid { + return Err("carrier child lacks its exact initialized occupied or absent leaf producer".into()); + } + if state.leaf_loans.iter().any(|loan| { + self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(slot) + }) { + return Err( + "carrier child adoption has an exact live leaf lease".into() + ); + } + state.leaves[slot.index()] = Leaf::Moved; + } + _ => return Err("carrier constructor cannot adopt any borrowed child".into()), + } + let Slot::Building { arrived, .. } = &mut state.slots[destination.index()] else { + return Err("carrier constructor child lost its arrived prefix".into()); + }; + *arrived += 1; + } + ResourceCarrierOperationRole::CommitConstructor { destination } => { + let Slot::Building { + origin, + expected, + arrived, + .. + } = &state.slots[destination.index()] + else { + return Err("carrier constructor Commit lacks its exact arrived prefix".into()); + }; + if *arrived != expected.len() { + return Err( + "carrier constructor Commit precedes its complete exact acquired prefix" + .into(), + ); + } + state.slots[destination.index()] = Slot::Live { + origin: *origin, + taken: Vec::new(), + }; + } + ResourceCarrierOperationRole::Transfer { + source, + destination, + } => { + let value = self.take_complete(state, *source)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier transfer overwrites a live destination".into()); + } + state.slots[destination.index()] = value; + } + ResourceCarrierOperationRole::QualifyMachine { + source, + destination, + machine, + state: selected, + } => { + let source_node = &self + .carriers + .shape(self.carriers.slots[source.index()].shape) + .ok_or("carrier qualification source graph row missing")? + .node; + if !matches!(source_node, ResourceCarrierNode::MachineState { machine: owner, state } if owner == machine && state == selected) + || self.carriers.slots[destination.index()].shape != *machine + { + return Err( + "carrier qualification changes its exact machine owner or checked state" + .into(), + ); + } + let value = self.take_complete(state, *source)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier qualification overwrites a live destination".into()); + } + state.slots[destination.index()] = value; + } + ResourceCarrierOperationRole::Borrow { loan } => { + let row = self + .carriers + .loans + .get(loan.index()) + .ok_or("carrier Borrow lost its exact loan row")?; + match row.source { + ResourceCarrierLoanSource::Slot(slot) => { self.live(state, slot)?; } + ResourceCarrierLoanSource::Loan(parent) => self.loan_live(state, parent)?, + ResourceCarrierLoanSource::IncomingViewFormal { .. } => return Err("carrier incoming formal is a resident caller lease, not a child Borrow activation".into()), + } + if self.projected_shape(state, *loan, &[])? != row.shape { + return Err("carrier Borrow declared graph node differs from its exact parent/path projection".into()); + } + if !state.loans.insert(*loan) { + return Err("carrier Borrow repeats a live lease".into()); + } + } + ResourceCarrierOperationRole::EndBorrow { loan } => { + if state.loans.iter().any(|child| { + self.carriers.loans[child.index()].source + == ResourceCarrierLoanSource::Loan(*loan) + }) { + return Err("carrier parent lease ends before its exact child".into()); + } + if state.leaf_loans.iter().any(|child| matches!(self.plan.loans[child.index()].source, + ResourceLoanSource::CarrierSumProjection { operation } if matches!(self.carriers.operations[operation.index()].role, ResourceCarrierOperationRole::AdaptSum { source, .. } if source == *loan))) { return Err("carrier parent lease ends before its sum adapter child".into()); } + if !state.loans.remove(loan) { + return Err("carrier EndBorrow has no reached activation".into()); + } + if let Some(root) = self.root(*loan)? { + if let Slot::Live { origin, .. } = self.live(state, root)? { + state.retired_projection = Some((*loan, root, *origin)); + } + } + } + ResourceCarrierOperationRole::Observe { + source, + observation, + target, + } => { + self.loan_live(state, *source)?; + let probe = self.probe(state, *source)?; + let node = &self + .carriers + .shape(probe.shape) + .ok_or("carrier observation graph row missing")? + .node; + match observation { + ResourceCarrierObservation::Length + if matches!( + node, + ResourceCarrierNode::List { .. } | ResourceCarrierNode::Map { .. } + ) => {} + ResourceCarrierObservation::Tag + if matches!( + node, + ResourceCarrierNode::Optional { .. } + | ResourceCarrierNode::Result { .. } + | ResourceCarrierNode::Enum { .. } + ) => + { + if let Some(target) = target { + state.tags.insert(target.index(), probe); + } else if matches!( + self.function.blocks[operation.site.block.index() as usize] + .terminator + .kind, + TerminatorKind::Switch { .. } + ) { + state.switches.insert(operation.site.block.index(), probe); + } + } + ResourceCarrierObservation::State { state: selected } => { + let machine = match node { + ResourceCarrierNode::Machine { states, .. } => states, + ResourceCarrierNode::MachineState { machine, .. } => match &self + .carriers + .shape(*machine) + .ok_or("carrier state observation graph owner missing")? + .node + { + ResourceCarrierNode::Machine { states, .. } => states, + _ => { + return Err( + "carrier state observation owner is not a machine".into() + ); + } + }, + _ => { + return Err( + "carrier state observation has a different graph family".into(), + ); + } + }; + if *selected >= machine.len() { + return Err( + "carrier StateIs selector outside its exact machine graph".into() + ); + } + } + ResourceCarrierObservation::Ordinary { path, ty } => { + let shape = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier ordinary projection row missing")?; + if shape.ty != *ty || shape.contains_resource { + return Err( + "carrier observation cannot expose custody as ordinary payload" + .into(), + ); + } + } + _ => { + return Err( + "carrier observation role changes its exact typed graph family".into(), + ); + } + } + } + ResourceCarrierOperationRole::Project { + source, + destination, + } => { + self.loan_live(state, *source)?; + self.loan_live(state, *destination)?; + } + ResourceCarrierOperationRole::Extract { + source, + path, + destination, + } => { + if self.projected_shape(state, *source, path)? + != self.carriers.slots[destination.index()].shape + { + return Err("carrier extraction changes its exact selected graph node".into()); + } + self.extract(state, *source, path)?; + self.install(state, *destination, Origin::Extraction(operation.id))?; + } + ResourceCarrierOperationRole::ExtractOrdinary { + source, path, ty, .. + } => { + let shape = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier ordinary extraction graph row missing")?; + if shape.ty != *ty || shape.contains_resource { + return Err( + "carrier extraction cannot export custody through ordinary payload storage" + .into(), + ); + } + self.extract(state, *source, path)?; + } + ResourceCarrierOperationRole::AdaptSum { + source, + path, + destination, + loan, + .. + } => { + let projected = self + .carriers + .shape(self.projected_shape(state, *source, path)?) + .ok_or("carrier sum adapter graph row missing")?; + let expected = match &projected.node { + ResourceCarrierNode::Optional { payload } => match &self.carriers.shape(*payload).ok_or("carrier sum payload graph missing")?.node { ResourceCarrierNode::Resource { kind } => ResourceShape::Optional { kind: kind.clone() }, _ => return Err("carrier adapter Optional does not have a sole exact legacy Resource leaf".into()) }, + ResourceCarrierNode::Result { success, failure } => match &self.carriers.shape(*success).ok_or("carrier sum success graph missing")?.node { ResourceCarrierNode::Resource { kind } => ResourceShape::Result { kind: kind.clone(), failure: self.carriers.shape(*failure).ok_or("carrier sum failure graph missing")?.ty }, _ => return Err("carrier adapter Result does not have a sole exact legacy Resource leaf".into()) }, + _ => return Err("carrier adapter selected graph node is not an existing leaf sum".into()), + }; + if self.plan.slots[destination.index()].shape != expected { + return Err( + "carrier adapter changes its exact legacy sum kind/failure shape".into(), + ); + } + if loan.is_some() { + self.loan_live(state, *source)?; + } else { + self.extract(state, *source, path)?; + } + if !matches!( + state.leaves[destination.index()], + Leaf::Vacant | Leaf::Moved + ) { + return Err( + "carrier sum adapter overwrites an installed shell outside a binder reset" + .into(), + ); + } + state.leaves[destination.index()] = if loan.is_some() { + Leaf::Adapter { + operation: operation.id, + } + } else { + Leaf::Empty { + origin: Origin::Extraction(operation.id), + } + }; + } + ResourceCarrierOperationRole::Retire { source } => { + self.take(state, *source)?; + } + ResourceCarrierOperationRole::PublishReturn { source } => { + self.live(state, *source)?; + if !state.returned + || !state.frames.is_empty() + || self.plan.provisional_return().map(ResourceFrame::id) + != Some(self.carriers.slots[source.index()].frame) + { + return Err( + "carrier Return publication precedes exact successful Scope cleanup".into(), + ); + } + self.take_complete(state, *source)?; + } + ResourceCarrierOperationRole::RetireIteration { source, binders } => { + self.reset_iteration(state, *source, binders)? + } + ResourceCarrierOperationRole::BeginIteration { + source, + destination, + .. + } => { + let ResourceCarrierValue::Owned { slot } = source else { + return Err("carrier consuming iterator lacks its original owning root".into()); + }; + let value = self.take_complete(state, *slot)?; + if !matches!(state.slots[destination.index()], Slot::Vacant | Slot::Moved) { + return Err("carrier iterator root installs twice".into()); + } + state.slots[destination.index()] = value; + } + } + Ok(()) + } + fn reset_iteration( + &self, + state: &mut State, + source: ResourceCarrierSlotId, + binders: &[LocalId], + ) -> Result<(), String> { + self.live(state, source)?; + self.unleased(state, source)?; + state.selections.retain(|fact| { + fact.anchor.root != Root::Slot(source) + || !matches!(fact.selector, Selector::Iteration(_)) + }); + for binder in binders { + for slot in &self.carriers.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == *binder) + { + self.unleased(state, slot.id)?; + if matches!(state.slots[slot.id.index()], Slot::Building { .. }) { + return Err( + "carrier iteration binder has an incomplete arrived prefix".into() + ); + } + state.slots[slot.id.index()] = Slot::Vacant; + } + } + for slot in &self.plan.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == *binder) + { + if state.leaf_loans.iter().any(|loan| { + self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(slot.id) + }) { + return Err("carrier iteration binder reset has a live sum lease".into()); + } + state.leaves[slot.id.index()] = Leaf::Vacant; + } + } + } + let Slot::Live { taken, .. } = &mut state.slots[source.index()] else { + return Err("carrier iterator reset lost its installed root".into()); + }; + // Only the exact canonical iterator cursor advances these dynamic paths. + // The physical runtime retains holes and generation checks for every index. + let iteration = self + .carriers + .iterations + .iter() + .find(|row| row.source == source && row.binders == binders) + .ok_or("carrier iteration reset has no exact original/current loop association")?; + taken.retain(|path| { + !path.iter().any(|part| match part { + ResourceCarrierProjectionPath::Element { index } + | ResourceCarrierProjectionPath::MapKey { index } + | ResourceCarrierProjectionPath::MapValue { index } => match index { + ResourceCarrierIndex::Local(cursor) => iteration.cursor == Some(*cursor), + ResourceCarrierIndex::CurrentIteration { header } => { + iteration.cursor.is_none() && iteration.header == *header + } + ResourceCarrierIndex::Constant(_) => false, + }, + _ => false, + }) + }); + Ok(()) + } + fn operation(&self, state: &mut State, operation: &ResourceOperation) -> Result<(), String> { + if let ResourceOperationRole::Carrier { operation: id } = operation.role { + return self.carrier( + state, + self.carriers + .operations + .get(id.index()) + .ok_or("carrier operation reference missing")?, + ); + } + if !matches!( + operation.role, + ResourceOperationRole::CompleteReturnAfterCleanup { .. } + ) { + self.frame(state, operation.frame)?; + } + state.retired_projection = None; + match &operation.role { + ResourceOperationRole::CreateAbsentSum { destination } => { + if !matches!(state.leaves[destination.index()], Leaf::Vacant | Leaf::Moved) { return Err("carrier absence sum installs into a live shell".into()); } + state.leaves[destination.index()] = Leaf::Empty { origin: Origin::LeafAbsence(operation.id) }; + } + ResourceOperationRole::Transfer { source, destination } => { + let exact = self.plan.slots[source.index()].shape == self.plan.slots[destination.index()].shape; + let initialized = match (&self.plan.slots[source.index()].shape, &state.leaves[source.index()]) { + (ResourceShape::Plain { .. }, Leaf::Occupied { .. }) => true, + (ResourceShape::Optional { .. } | ResourceShape::Result { .. }, Leaf::Empty { .. }) => true, + _ => false, + }; + if !exact || !initialized || !matches!(state.leaves[destination.index()], Leaf::Vacant | Leaf::Moved) { return Err("carrier leaf transfer lacks exact initialized source/vacant destination".into()); } + if state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*source)) { return Err("carrier leaf transfer uses a source with an exact live sum lease".into()); } + state.leaves[destination.index()] = std::mem::replace(&mut state.leaves[source.index()], Leaf::Moved); + } + ResourceOperationRole::BorrowSum { loan } => { + match self.plan.loans[loan.index()].source { + ResourceLoanSource::Owner(slot) if matches!(state.leaves[slot.index()], Leaf::Empty { .. }) => {} + ResourceLoanSource::CarrierSumProjection { operation } => { + let ResourceCarrierOperationRole::AdaptSum { source, destination, loan: Some(expected), .. } = &self.carriers.operations[operation.index()].role else { return Err("carrier sum Borrow lost its exact adapter row".into()); }; + if *expected != *loan || state.leaves[destination.index()] != (Leaf::Adapter { operation }) { return Err("carrier sum Borrow changes its exact adapted shell".into()); } + self.loan_live(state, *source)?; + } + _ => return Err("carrier sum Borrow has no exact zero-leaf source".into()), + } + if !state.leaf_loans.insert(*loan) { return Err("carrier sum Borrow repeats a live activation".into()); } + } + ResourceOperationRole::EndSumBorrow { loan } => { + if !state.leaf_loans.remove(loan) { return Err("carrier sum EndBorrow has no exact activation".into()); } + if let ResourceLoanSource::CarrierSumProjection { operation } = self.plan.loans[loan.index()].source { + let ResourceCarrierOperationRole::AdaptSum { destination, .. } = self.carriers.operations[operation.index()].role else { return Err("carrier sum retirement lost adapter destination".into()); }; + state.leaves[destination.index()] = Leaf::Moved; + } + } + ResourceOperationRole::InvokeSourceFunction { operands, result, .. } => { + for operand in operands { + match operand { + ResourceCallOperand::CarrierOwned { slot, .. } => { + if self.carriers.slots[slot.index()].frame != operation.frame { return Err("carrier Source owned actual bypassed its exact Operation holder".into()); } + self.take_complete(state, *slot)?; + } + ResourceCallOperand::CarrierBorrowed { loan, .. } => self.view_complete(state, *loan)?, + ResourceCallOperand::Owned { slot, .. } => { + let initialized = match (&self.plan.slots[slot.index()].shape, &state.leaves[slot.index()]) { + (ResourceShape::Plain { .. }, Leaf::Occupied { .. }) => true, + (ResourceShape::Optional { .. } | ResourceShape::Result { .. }, Leaf::Empty { .. }) => true, + _ => false, + }; + if !initialized { return Err("carrier Source leaf actual lacks an exact initialized producer".into()); } + state.leaves[slot.index()] = Leaf::Moved; + } + ResourceCallOperand::Borrowed { loan, .. } if state.leaf_loans.contains(loan) => {} + ResourceCallOperand::Borrowed { .. } => return Err("carrier Source uses an ended sum lease".into()), + ResourceCallOperand::Ordinary { .. } => {} + } + } + match result { + ResourceCallResult::Carrier { slot } => self.install(state, *slot, Origin::Source(operation.id))?, + ResourceCallResult::Owned { .. } => return Err("pending Resource carrier: live/conditional leaf Source result needs the dedicated leaf flow".into()), + ResourceCallResult::Ordinary { .. } => {} + } + } + ResourceOperationRole::Complete { outcome } => { + let frame = operation.frame; + if state.frames.last().copied() != Some(frame) { return Err("carrier completion changed its exact pending prefix".into()); } + if state.loans.iter().any(|loan| self.carriers.loans[loan.index()].frame == frame && !matches!(self.carriers.loans[loan.index()].source, ResourceCarrierLoanSource::IncomingViewFormal { .. })) || state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].frame == frame && !matches!(self.plan.loans[loan.index()].source, ResourceLoanSource::IncomingViewFormal { .. })) { + return Err("carrier completion has an unretired exact owned loan".into()); + } + for slot in &self.carriers.slots { + if slot.frame == frame { + if matches!(state.slots[slot.id.index()], Slot::Building { .. }) && *outcome != ResourceCompletion::Abort { return Err("carrier normal completion has an incomplete acquired prefix".into()); } + state.slots[slot.id.index()] = if frame == ResourceFrameId(0) { Slot::Moved } else { Slot::Vacant }; + } + } + for slot in &self.plan.slots { if slot.frame == frame { state.leaves[slot.id.index()] = if frame == ResourceFrameId(0) { Leaf::Moved } else { Leaf::Vacant }; } } + state.frames.pop(); + if frame == ResourceFrameId(0) { state.returned = *outcome == ResourceCompletion::Return; } + } + ResourceOperationRole::CompleteReturnAfterCleanup { source } => { + if !state.returned || !state.frames.is_empty() || self.plan.provisional_return().map(ResourceFrame::id) != Some(self.plan.slots[source.index()].frame) || !matches!(state.leaves[source.index()], Leaf::Empty { .. }) { return Err("carrier leaf Return publication lacks exact successful Scope cleanup and empty provisional holder".into()); } + state.leaves[source.index()] = Leaf::Moved; + } + ResourceOperationRole::Drop { source, occupancy } => { + let initialized = matches!((&self.plan.slots[source.index()].shape, &state.leaves[source.index()], occupancy), + (ResourceShape::Plain { .. },Leaf::Occupied { .. },ResourceOccupancy::Occupied) + | (ResourceShape::Optional { .. } | ResourceShape::Result { .. },Leaf::Empty { .. },ResourceOccupancy::Empty)); + if !initialized || state.leaf_loans.iter().any(|loan| self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*source)) { return Err("carrier leaf discard lacks its initialized exact unleased endpoint".into()); } + state.leaves[source.index()] = Leaf::Moved; + } + _ => return Err("pending Resource carrier: this leaf/control operation needs its dedicated independent flow proof".into()), + } + Ok(()) + } + fn site(&self, state: &mut State, site: ResourceSite) -> Result<(), String> { + for operation in &self.plan.operations { + if operation.site == site { + if let ResourceOperationRole::Carrier { operation: id } = operation.role { + if self.carriers.operations[id.index()].edge.is_some() { + continue; + } + } + self.operation(state, operation)?; + } + } + state.retired_projection = None; + Ok(()) + } + fn edge(&self, state: &mut State, edge: ResourceCarrierEdge) -> Result<(), String> { + for operation in self.carriers.operations_on_edge(edge) { + self.carrier(state, operation)?; + } + Ok(()) + } + fn branch( + &self, + state: &mut State, + condition: &Expression, + selected: bool, + block: BlockId, + ) -> Result<(), String> { + if let E::Local(tag) = condition.kind + && let Some(probe) = state.tags.get(&tag.index()).cloned() + { + if condition.ty != TypeInterner::BOOL + || !matches!( + self.carriers + .shape(probe.shape) + .ok_or("carrier tag guard graph missing")? + .node, + ResourceCarrierNode::Optional { .. } | ResourceCarrierNode::Result { .. } + ) + { + return Err( + "carrier selecting Branch differs from its exact typed sum Tag destination" + .into(), + ); + } + self.select(state, &probe, Selector::Sum(selected)); + } + if selected && let E::StateIs { .. } = condition.kind { + let observations = self + .carriers + .operations_for_expression(condition) + .filter_map(|operation| match operation.role { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::State { state }, + .. + } if operation.site.block == block => Some((source, state)), + _ => None, + }) + .collect::>(); + let [(loan, state_id)] = observations.as_slice() else { + return Err( + "carrier StateIs Branch has no unique exact current state observation".into(), + ); + }; + let probe = self.probe(state, *loan)?; + self.select(state, &probe, Selector::State(*state_id)); + } + if selected + && let Some(iteration) = self + .carriers + .iterations + .iter() + .find(|row| row.header == block && row.cursor.is_some()) + { + let anchor = match self.live(state, iteration.source)? { + Slot::Live { origin, .. } => Anchor { + root: Root::Slot(iteration.source), + origin: *origin, + }, + _ => return Err("carrier iterator selecting edge lost its installed root".into()), + }; + let probe = Probe { + anchor, + path: Vec::new(), + shape: self.carriers.slots[iteration.source.index()].shape, + }; + self.select(state, &probe, Selector::Iteration(block)); + } + Ok(()) + } + fn select_switch( + &self, + state: &mut State, + source: BlockId, + variant: VariantId, + ) -> Result<(), String> { + let probe = + state.switches.get(&source.index()).cloned().ok_or( + "carrier Switch selected an edge without its exact reached Tag observation", + )?; + let ResourceCarrierNode::Enum { variants, .. } = &self + .carriers + .shape(probe.shape) + .ok_or("carrier Switch graph row missing")? + .node + else { + return Err("carrier Switch observation is not an enum".into()); + }; + if variant.index() as usize >= variants.len() { + return Err("carrier Switch edge selected a foreign graph variant".into()); + } + self.select(state, &probe, Selector::Variant(variant.index() as usize)); + Ok(()) + } + fn select_iteration(&self, state: &mut State, header: BlockId) -> Result<(), String> { + let row = self + .carriers + .iterations + .iter() + .find(|row| row.header == header && row.cursor.is_none()) + .ok_or("carrier raw iterator edge has no exact Source/current association")?; + let anchor = match self.live(state, row.source)? { + Slot::Live { origin, .. } => Anchor { + root: Root::Slot(row.source), + origin: *origin, + }, + _ => return Err("carrier raw iterator selecting edge lost its installed root".into()), + }; + let probe = Probe { + anchor, + path: Vec::new(), + shape: self.carriers.slots[row.source.index()].shape, + }; + self.select(state, &probe, Selector::Iteration(header)); + Ok(()) + } + fn normalized_entry(&self, state: &mut State, block: BlockId) -> Result<(), String> { + for iteration in &self.carriers.iterations { + if iteration.header != block { + continue; + } + if iteration.cursor.is_none() + && !matches!(state.slots[iteration.source.index()], Slot::Live { .. }) + { + self.site( + state, + ResourceSite { + function: self.function.id, + block, + position: ResourcePosition::Terminator, + }, + )?; + } + self.reset_iteration(state, iteration.source, &iteration.binders)?; + } + Ok(()) + } +} + +pub(super) fn validate( + function: &Function, + _types: &TypeInterner, + plan: &ResourceFunctionPlan, + carriers: &ResourceCarrierFunctionPlan, +) -> Result<(), String> { + let analysis = Analysis { + function, + plan, + carriers, + }; + let mut initial = State { + slots: vec![Slot::Vacant; carriers.slots.len()], + leaves: vec![Leaf::Vacant; plan.slots.len()], + loans: BTreeSet::new(), + leaf_loans: BTreeSet::new(), + frames: vec![ResourceFrameId(0)], + returned: false, + selections: Vec::new(), + tags: BTreeMap::new(), + switches: BTreeMap::new(), + retired_projection: None, + }; + for (parameter, param) in function.params.iter().enumerate() { + for slot in &carriers.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == param.local) + { + analysis.install(&mut initial, slot.id, Origin::Formal(parameter))?; + } + } + if param.mode == ParamMode::Owned { + for slot in &plan.slots { + if matches!(&slot.storage, ResourceSlotStorage::Local { header } if header.id == param.local) + && matches!(slot.shape, ResourceShape::Plain { .. }) + { + initial.leaves[slot.id.index()] = Leaf::Occupied { + origin: Origin::Formal(parameter), + }; + } + } + } + } + for loan in &carriers.loans { + if matches!( + loan.source, + ResourceCarrierLoanSource::IncomingViewFormal { .. } + ) { + initial.loans.insert(loan.id); + } + } + for loan in &plan.loans { + if matches!(loan.source, ResourceLoanSource::IncomingViewFormal { .. }) { + initial.leaf_loans.insert(loan.id); + } + } + analysis.normalized_entry(&mut initial, function.entry)?; + let mut states = vec![None; function.blocks.len()]; + states[usize::try_from(function.entry.index()) + .map_err(|_| "carrier entry index is not representable")?] = Some(initial); + let mut queue = VecDeque::from([function.entry]); + let limit = function + .blocks + .len() + .saturating_mul( + carriers + .slots + .len() + .saturating_add(carriers.loans.len()) + .saturating_add(1), + ) + .saturating_mul(8) + .saturating_add(1); + let mut steps = 0usize; + while let Some(block_id) = queue.pop_front() { + steps = steps + .checked_add(1) + .ok_or("carrier fixed point step overflow")?; + if steps > limit { + return Err( + "pending Resource carrier: exact custody fixed point exceeds its finite bound" + .into(), + ); + } + let index = usize::try_from(block_id.index()) + .map_err(|_| "carrier block index is not representable")?; + let mut state = states[index] + .as_ref() + .ok_or("carrier reachable block has no exact incoming state")? + .clone(); + let block = &function.blocks[index]; + for statement in 0..block.statements.len() { + analysis.site( + &mut state, + ResourceSite { + function: function.id, + block: block_id, + position: ResourcePosition::Statement(statement), + }, + )?; + } + let raw_iterator = carriers + .iterations + .iter() + .any(|row| row.header == block_id && row.cursor.is_none()); + if !raw_iterator { + analysis.site( + &mut state, + ResourceSite { + function: function.id, + block: block_id, + position: ResourcePosition::Terminator, + }, + )?; + } + let mut outputs = Vec::new(); + match &block.terminator.kind { + TerminatorKind::Goto(target) => outputs.push((*target, state)), + TerminatorKind::Branch { condition, then_block, else_block } => { + let mut yes = state.clone(); analysis.branch(&mut yes, condition, true, block_id)?; + analysis.branch(&mut state, condition, false, block_id)?; + outputs.push((*then_block, yes)); outputs.push((*else_block, state)); + } + TerminatorKind::ForEach { body, exit, .. } => { + let mut selected = state.clone(); + if carriers.iterations.iter().any(|row| row.header == block_id) { + analysis.select_iteration(&mut selected, block_id)?; + analysis.edge(&mut selected, ResourceCarrierEdge::IterationBody { source: block_id, target: *body })?; + } + outputs.push((*body, selected)); outputs.push((*exit, state)); + } + TerminatorKind::Switch { variants, otherwise, .. } => { + for (variant, target, _) in variants { + let mut selected = state.clone(); + if carriers.operations_on_edge(ResourceCarrierEdge::SwitchVariant { source: block_id, variant: *variant, target: *target }).next().is_some() { + analysis.select_switch(&mut selected, block_id, *variant)?; + analysis.edge(&mut selected, ResourceCarrierEdge::SwitchVariant { source: block_id, variant: *variant, target: *target })?; + } + outputs.push((*target, selected)); + } + if let Some(target) = otherwise { analysis.edge(&mut state, ResourceCarrierEdge::SwitchOtherwise { source: block_id, target: *target })?; outputs.push((*target, state)); } + } + TerminatorKind::Return(_) | TerminatorKind::Unreachable => {} + _ => return Err("pending Resource carrier: this terminator requires its independent custody edge proof".into()), + } + for (target, mut output) in outputs { + analysis.normalized_entry(&mut output, target)?; + let index = usize::try_from(target.index()) + .map_err(|_| "carrier successor index is not representable")?; + if let Some(current) = &mut states[index] { + // Selecting facts can be forgotten at joins. Custody, generation, + // arrived prefix and exact active lease sets must agree independently. + let mut merged = current.clone(); + merged + .selections + .retain(|fact| output.selections.contains(fact)); + merged + .tags + .retain(|key, probe| output.tags.get(key) == Some(probe)); + merged + .switches + .retain(|key, probe| output.switches.get(key) == Some(probe)); + output.selections = merged.selections.clone(); + output.tags = merged.tags.clone(); + output.switches = merged.switches.clone(); + if merged != output { + return Err("pending Resource carrier: CFG join changed exact root generation, selected paths, operation prefix, or loans".into()); + } + if *current != merged { + *current = merged; + queue.push_back(target); + } + } else { + states[index] = Some(output); + queue.push_back(target); + } + } + } + Ok(()) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/graph.rs b/crates/jett_mir/src/resource_ownership/carriers/graph.rs new file mode 100644 index 00000000..fc29e969 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/graph.rs @@ -0,0 +1,206 @@ +//! Exact checked type graph; recursion uses canonical TypeId backreferences. +use super::*; + +fn id(types: &TypeInterner, ty: TypeId) -> Result { + let index = + usize::try_from(ty.index()).map_err(|_| "carrier TypeId index is not representable")?; + if index >= types.len() { + return Err("carrier child type is outside its checked interner".into()); + } + Ok(ResourceCarrierShapeId(index)) +} +fn fields( + types: &TypeInterner, + fields: &[(String, TypeId)], +) -> Result, String> { + fields + .iter() + .enumerate() + .map(|(ordinal, (name, ty))| { + Ok(ResourceCarrierField { + ordinal, + name: name.clone(), + shape: id(types, *ty)?, + }) + }) + .collect() +} +fn arguments(types: &TypeInterner, ty: TypeId) -> Result, String> { + types + .nominal_type_arguments(ty) + .iter() + .map(|ty| id(types, *ty)) + .collect() +} +fn machine_type( + types: &TypeInterner, + owner: jett_types::MachineId, +) -> Result { + let ty = types + .type_ids() + .find(|ty| matches!(types.resolve(*ty), Type::Machine(id) if *id == owner)) + .ok_or("qualified carrier machine lost its unqualified checked owner")?; + id(types, ty) +} +fn node( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> Result { + Ok(match types.resolve(ty) { + Type::Resource(_) => ResourceCarrierNode::Resource { + kind: manifest + .kind_for_type(ty) + .ok_or("carrier Resource node has no exact manifest kind")?, + }, + Type::Optional(payload) => ResourceCarrierNode::Optional { + payload: id(types, *payload)?, + }, + Type::Result(success, failure) => ResourceCarrierNode::Result { + success: id(types, *success)?, + failure: id(types, *failure)?, + }, + Type::List(element) => ResourceCarrierNode::List { + element: id(types, *element)?, + }, + Type::Map(key, value) => ResourceCarrierNode::Map { + key: id(types, *key)?, + value: id(types, *value)?, + }, + Type::Struct(owner) => ResourceCarrierNode::Struct { + owner: *owner, + arguments: arguments(types, ty)?, + fields: fields(types, &types.resolve_struct(*owner).fields)?, + }, + Type::Enum(owner) => ResourceCarrierNode::Enum { + owner: *owner, + arguments: arguments(types, ty)?, + variants: types + .resolve_enum(*owner) + .variants + .iter() + .enumerate() + .map(|(ordinal, variant)| { + Ok(ResourceCarrierVariant { + ordinal, + name: variant.name.clone(), + discriminant: variant.discriminant, + fields: fields(types, &variant.fields)?, + }) + }) + .collect::>()?, + }, + Type::Machine(owner) => { + let definition = types.resolve_machine(*owner); + ResourceCarrierNode::Machine { + owner: *owner, + states: definition + .states + .iter() + .enumerate() + .map(|(ordinal, state)| { + Ok(ResourceCarrierState { + ordinal, + name: state.name.clone(), + fields: fields(types, &state.fields)?, + }) + }) + .collect::>()?, + transitions: definition + .transitions + .iter() + .map(|edge| { + Ok::<_, String>(( + usize::try_from(edge.from.index()) + .map_err(|_| "carrier state index is not representable")?, + usize::try_from(edge.to.index()) + .map_err(|_| "carrier state index is not representable")?, + )) + }) + .collect::>()?, + } + } + Type::MachineState { machine, state } => ResourceCarrierNode::MachineState { + machine: machine_type(types, *machine)?, + state: usize::try_from(state.index()) + .map_err(|_| "carrier state index is not representable")?, + }, + Type::Refinement { name, base } => ResourceCarrierNode::Refinement { + name: name.clone(), + base: id(types, *base)?, + }, + // Ordinary graph terminals are metadata only. In particular, descriptor + // signatures never become leaf owners by mentioning Resource types. + Type::Function { .. } => ResourceCarrierNode::Ordinary, + _ if !resource_type_pending(types, ty) => ResourceCarrierNode::Ordinary, + _ => { + return Err( + "pending Resource carrier: this checked type family needs its dedicated graph node" + .into(), + ); + } + }) +} +pub(super) fn capture( + source: &hir::ResourceSourceArchive, + manifest: &hir::ResourceManifest, + types: &TypeInterner, +) -> Result>, String> { + source.validate_types(types)?; + manifest.validate(types)?; + if source.manifest() != Some(manifest) { + return Err("carrier shape graph belongs to another checked manifest".into()); + } + let mut rows = Vec::with_capacity(types.len()); + for ty in types.type_ids() { + let shape = id(types, ty)?; + if shape.index() != rows.len() { + return Err("carrier type graph lost canonical dense ordinals".into()); + } + rows.push(ResourceCarrierShape { + id: shape, + ty, + node: node(manifest, types, ty)?, + contains_resource: !matches!(types.resolve(ty), Type::Function { .. }) + && resource_type_pending(types, ty), + refinement_predicate: predicate(source, types, ty)?, + }); + } + Ok(Arc::new(rows)) +} + +fn predicate( + source: &hir::ResourceSourceArchive, + types: &TypeInterner, + ty: TypeId, +) -> Result, String> { + let Type::Refinement { name, base } = types.resolve(ty) else { + return Ok(None); + }; + let mut input = *base; + while let Type::Refinement { base, .. } = types.resolve(input) { + input = *base; + } + if let Type::Secret(inner) = types.resolve(input) { + input = *inner; + } + let mut candidates = source.functions().iter().filter(|function| { + hir::refinement_predicate_declaration_matches(&function.identity.declaration, name) + && function.source_definition.is_some() + && function.capture_count == 0 + && function.params.len() == 1 + && function.params[0].ty == input + && function.params[0].mode == ParamMode::Owned + && !function.params[0].mutable + && function.return_type == TypeInterner::BOOL + }); + let selected = candidates + .next() + .ok_or("carrier refinement graph lost its original checked predicate association")?; + if candidates.next().is_some() { + return Err( + "carrier refinement graph has an ambiguous original predicate association".into(), + ); + } + Ok(Some(selected.clone())) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/iterations.rs b/crates/jett_mir/src/resource_ownership/carriers/iterations.rs new file mode 100644 index 00000000..51cf5a55 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/iterations.rs @@ -0,0 +1,719 @@ +//! Private association of canonical prepared iterators with their original For. +//! These rows describe current sites; they issue no owner, lease or Source permit. +use super::*; +use hir::{BinaryOp, StatementKind as H}; + +struct SourceFor<'a> { + key: LocalId, + value: Option, + by_view: bool, + iterable: &'a Expression, + span: Span, +} + +#[derive(Default)] +struct Inventory<'a> { + loops: Vec>, + binders: BTreeSet, +} + +impl<'a> Inventory<'a> { + fn block(&mut self, block: &'a hir::Block) -> Result<(), String> { + for statement in &block.statements { + match &statement.kind { + H::For { + key, + value, + by_view, + iterable, + body, + } => { + for binder in std::iter::once(*key).chain(value.iter().copied()) { + if !self.binders.insert(binder.index()) { + return Err( + "carrier iterator duplicated an original Source binder".into() + ); + } + } + self.loops.push(SourceFor { + key: *key, + value: *value, + by_view: *by_view, + iterable, + span: statement.span, + }); + self.expression(iterable)?; + self.block(body)?; + } + H::If { + condition, + then_block, + else_block, + } => { + self.expression(condition)?; + self.block(then_block)?; + if let Some(block) = else_block { + self.block(block)?; + } + } + H::While { condition, body } => { + self.expression(condition)?; + self.block(body)?; + } + H::Match { scrutinee, arms } => { + self.expression(scrutinee)?; + for arm in arms { + self.block(&arm.body)?; + } + } + H::Scope(block) => self.block(block)?, + H::ReflectedTypeDispatch { type_info, arms } => { + self.expression(type_info)?; + for arm in arms { + self.block(&arm.body)?; + } + } + H::Let { value, .. } + | H::Expression(value) + | H::HandleDefault(value) + | H::Respond(value) => self.expression(value)?, + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + self.expression(target)?; + self.expression(value)?; + } + H::Return(value) => { + if let Some(value) = value { + self.expression(value)?; + } + } + H::Assert { condition, message } => { + self.expression(condition)?; + if let Some(message) = message { + self.expression(message)?; + } + } + H::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + self.expression(condition)?; + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + } + Ok(()) + } + + fn expression(&mut self, expression: &'a Expression) -> Result<(), String> { + match &expression.kind { + // Dedicated graph construction owns the sealed import body. + E::ActorGraphImport(_) => {} + E::Handle { + target, failure, .. + } => { + self.expression(target)?; + self.block(failure)?; + } + E::Binary { left, right, .. } => { + self.expression(left)?; + self.expression(right)?; + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => self.expression(value)?, + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + self.expression(value)?; + } + } + E::IndirectCall { callee, args, .. } => { + self.expression(callee)?; + for value in args { + self.expression(value)?; + } + } + E::ActorMessage { actor, args, .. } => { + self.expression(actor)?; + for value in args { + self.expression(value)?; + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + self.expression(value)?; + } + } + E::EnumConstruct { payloads, .. } | E::MachineConstruct { payloads, .. } => { + for value in payloads { + self.expression(value)?; + } + } + E::MachineTransition { + source, payloads, .. + } => { + self.expression(source)?; + for value in payloads { + self.expression(value)?; + } + } + E::ListConstruct { elements } => { + for value in elements { + self.expression(value)?; + } + } + E::MapConstruct { entries } => { + for entry in entries { + self.expression(&entry.key)?; + self.expression(&entry.value)?; + } + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + self.expression(value)?; + } + } + } + E::Field { base, .. } => self.expression(base)?, + // A separate callable body belongs to its own function/local archive. + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + Ok(()) + } +} + +fn dense_block(function: &Function, id: BlockId) -> Result<&BasicBlock, String> { + let index = + usize::try_from(id.index()).map_err(|_| "carrier iterator block is not representable")?; + function + .blocks + .get(index) + .filter(|block| block.id == id) + .ok_or_else(|| "carrier iterator lost its dense current block".into()) +} + +fn local_expression(value: &Expression, local: LocalId, ty: TypeId, span: Span) -> bool { + value.ty == ty + && value.span == span + && matches!(&value.kind, E::Local(current) if *current == local) +} + +fn temporary(function: &Function, id: LocalId, ty: TypeId, span: Span) -> Result<(), String> { + let local = function + .local(id) + .ok_or("carrier iterator temporary lost its dense header")?; + if local.ty != ty + || local.debug_ty != ty + || local.span != span + || !local.mutable + || local.view_source.is_some() + || function.is_view_local(id) + || function.parameter_for_local(id).is_some() + { + return Err("carrier iterator changed its exact temporary declaration".into()); + } + Ok(()) +} + +fn source_expression( + witness: &ResourceLoweringWitness, + original: &Expression, + current: &Expression, +) -> bool { + if crate::breakpoint_regions::expressions_equal(original, current) { + return true; + } + let direct = witness + .calls + .iter() + .filter(|row| { + crate::breakpoint_regions::expressions_equal(&row.original, original) + && crate::breakpoint_regions::expressions_equal(&row.current, current) + }) + .count(); + let normalized = if let E::Local(local) = ¤t.kind { + witness + .regions + .iter() + .filter(|row| { + row.output() == Some(*local) + && crate::breakpoint_regions::expressions_equal(row.original(), original) + && original.ty == current.ty + }) + .count() + } else { + 0 + }; + direct + normalized == 1 +} + +fn binding( + function: &Function, + original: &hir::Function, + id: LocalId, + ty: TypeId, +) -> Result<(), String> { + let index = + usize::try_from(id.index()).map_err(|_| "carrier iterator binder is not representable")?; + let archived = original + .locals + .get(index) + .filter(|local| local.id == id) + .ok_or("carrier iterator binder has no exact original Source header")?; + let current = function + .local(id) + .ok_or("carrier iterator binder lost its current header")?; + if archived.ty != ty + || current.ty != ty + || current.debug_ty != ty + || archived.mutable + || current.mutable + || archived.view_source.is_some() + || current.view_source.is_some() + || function.is_view_local(id) + || function.parameter_for_local(id).is_some() + { + return Err("carrier iterator changed its exact original owning binder".into()); + } + Ok(()) +} + +fn source_slot( + carriers: &ResourceCarrierFunctionPlan, + header: &Local, +) -> Result { + let matches = carriers.slots().iter().filter(|slot| { + matches!(slot.storage(), ResourceSlotStorage::Local { header: current } if current == header) + && carriers.shape(slot.shape()).is_some_and(|shape| { + shape.ty() == header.ty && shape.contains_resource() + }) + }).collect::>(); + let [slot] = matches.as_slice() else { + return Err("carrier iterator has no unique exact local source slot".into()); + }; + Ok(slot.id()) +} + +fn extraction( + statement: &Statement, + source: LocalId, + cursor: LocalId, + target: LocalId, + part: SequencePart, + span: Span, +) -> bool { + statement.span == span + && matches!(&statement.kind, StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(current_source), + index, + target: current_target, + part: current_part, + } if *current_source == source && *index == cursor && *current_target == target && *current_part == part) +} + +fn advance(statement: &Statement, cursor: LocalId, span: Span) -> bool { + let StatementKind::Assign { target, value } = &statement.kind else { + return false; + }; + let E::Binary { + left, + op: BinaryOp::Add, + right, + } = &value.kind + else { + return false; + }; + statement.span == span + && value.ty == TypeInterner::INT64 + && value.span == span + && local_expression(target, cursor, TypeInterner::INT64, span) + && local_expression(left, cursor, TypeInterner::INT64, span) + && right.ty == TypeInterner::INT64 + && right.span == span + && matches!(&right.kind, E::Int(1)) +} + +fn preheader( + function: &Function, + cfg: &ControlFlowGraph, + header: BlockId, +) -> Result { + let mut outside = BTreeSet::new(); + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if block == header || !outside.insert(block.index()) { + continue; + } + pending.extend_from_slice(cfg.successors(block)); + } + let candidates = cfg + .predecessors(header) + .iter() + .copied() + .filter(|block| outside.contains(&block.index())) + .collect::>(); + let [preheader] = candidates.as_slice() else { + return Err("carrier iterator lost its unique exact preheader".into()); + }; + if !matches!(&dense_block(function, *preheader)?.terminator.kind, TerminatorKind::Goto(target) if *target == header) + { + return Err("carrier iterator preheader bypasses its exact header".into()); + } + Ok(*preheader) +} + +struct Prepared { + row: ResourceCarrierIteration, + preheader: BlockId, + length_site: usize, +} + +fn prepared( + function: &Function, + types: &TypeInterner, + carriers: &ResourceCarrierFunctionPlan, + witness: &ResourceLoweringWitness, + originals: &[SourceFor<'_>], + cfg: &ControlFlowGraph, + header: &BasicBlock, +) -> Result, String> { + let TerminatorKind::Branch { + condition, + then_block: body, + else_block: exit, + } = &header.terminator.kind + else { + return Ok(None); + }; + let body_block = dense_block(function, *body)?; + let Some(first) = body_block.statements.first() else { + return Ok(None); + }; + let StatementKind::SequenceGet { + source, + index: cursor, + target: key, + .. + } = &first.kind + else { + return Ok(None); + }; + let Some(ty) = source.ty(function) else { + return Err("carrier iterator lost its source type".into()); + }; + if ty.index() as usize >= types.len() { + return Err("carrier iterator source type is out of range".into()); + } + if !carrier_type(types, ty) { + return Ok(None); + } + let SequenceSource::Local(source) = source else { + return Err( + "pending Resource carrier: projected iteration needs its bounded parent lease".into(), + ); + }; + let source_header = function + .local(*source) + .ok_or("carrier iterator source has no dense local header")?; + let (key_type, value_type, key_part) = match types.resolve(ty) { + Type::List(element) => (*element, None, SequencePart::Element), + Type::Map(key, value) => (*key, Some(*value), SequencePart::Key), + _ => return Err("carrier iterator source is not its exact list or map".into()), + }; + let value = if value_type.is_some() { + let second = body_block + .statements + .get(1) + .ok_or("carrier map iterator lost its value extraction")?; + let StatementKind::SequenceGet { target, .. } = &second.kind else { + return Err("carrier map iterator reordered its exact value extraction".into()); + }; + Some(*target) + } else { + None + }; + let candidates = originals + .iter() + .filter(|row| row.key == *key && row.value == value && row.iterable.ty == ty) + .collect::>(); + let [original] = candidates.as_slice() else { + return Err("carrier iterator has no unique exact original Source For tuple".into()); + }; + if original.by_view + || matches!(&original.iterable.kind, E::View(_) | E::Field { .. }) + || matches!(&original.iterable.kind, E::Local(local) if function.is_view_local(*local)) + { + return Err( + "pending Resource carrier: borrowed iteration needs its exact Source lease".into(), + ); + } + binding(function, &witness.original, *key, key_type)?; + if let Some((binding_id, binding_type)) = value.zip(value_type) { + binding(function, &witness.original, binding_id, binding_type)?; + } + let span = original.iterable.span; + if key_type == TypeInterner::NEVER || value_type == Some(TypeInterner::NEVER) { + return Err( + "pending Resource carrier: uninhabited iteration needs its exact transport".into(), + ); + } + let E::Binary { + left, + op: BinaryOp::Less, + right, + } = &condition.kind + else { + return Err("carrier iterator changed its exact selecting comparison".into()); + }; + let E::Local(length) = &right.kind else { + return Err("carrier iterator lost its exact length local".into()); + }; + if condition.ty != TypeInterner::BOOL + || condition.span != span + || header.terminator.span != original.span + || !local_expression(left, *cursor, TypeInterner::INT64, span) + || !local_expression(right, *length, TypeInterner::INT64, span) + || *body == header.id + || *exit == header.id + || body == exit + || *cursor == *length + || *cursor == *source + || *length == *source + { + return Err("carrier iterator changed its exact selecting endpoints".into()); + } + temporary(function, *cursor, TypeInterner::INT64, span)?; + temporary(function, *length, TypeInterner::INT64, span)?; + temporary(function, *source, ty, span)?; + if !extraction(first, *source, *cursor, *key, key_part, span) { + return Err( + "carrier iterator changed its exact consuming key or element extraction".into(), + ); + } + let mut binders = vec![*key]; + if let Some(value) = value { + if value == *key { + return Err("carrier map iterator duplicated its original binder".into()); + } + let second = body_block + .statements + .get(1) + .ok_or("carrier map extraction disappeared")?; + if !extraction(second, *source, *cursor, value, SequencePart::Value, span) { + return Err("carrier map iterator changed Key then Value extraction".into()); + } + binders.push(value); + } + if binders + .iter() + .any(|binder| [*source, *cursor, *length].contains(binder)) + { + return Err("carrier iterator aliased its source or cursor with a Source binder".into()); + } + let increment = body_block + .statements + .get(binders.len()) + .ok_or("carrier iterator advance is absent")?; + if !advance(increment, *cursor, span) { + return Err("carrier iterator changed its exact cursor advance".into()); + } + if cfg + .predecessors(*body) + .iter() + .any(|predecessor| *predecessor != header.id) + { + return Err("carrier iterator body has an entry outside its exact selecting header".into()); + } + let preheader_id = preheader(function, cfg, header.id)?; + let preheader = dense_block(function, preheader_id)?; + let start = preheader + .statements + .len() + .checked_sub(3) + .ok_or("carrier iterator initialization is incomplete")?; + let [source_init, zero, length_init] = &preheader.statements[start..] else { + return Err("carrier iterator initialization changed its exact arity".into()); + }; + let StatementKind::Let { + local: source_local, + value: source_value, + } = &source_init.kind + else { + return Err("carrier iterator lost its owning source initializer".into()); + }; + if source_init.span != span + || *source_local != *source + || source_value.ty != ty + || !source_expression(witness, original.iterable, source_value) + || zero.span != span + || !matches!(&zero.kind, StatementKind::Let { local, value } if *local == *cursor + && value.ty == TypeInterner::INT64 && value.span == span && matches!(&value.kind, E::Int(0))) + || length_init.span != span + || !matches!(&length_init.kind, StatementKind::SequenceLength { + source: SequenceSource::Local(current_source), target + } if *current_source == *source && *target == *length) + { + return Err( + "carrier iterator changed its exact Source, zero or length initialization".into(), + ); + } + Ok(Some(Prepared { + row: ResourceCarrierIteration { + header: header.id, + body: *body, + exit: *exit, + source: source_slot(carriers, source_header)?, + binders, + cursor: Some(*cursor), + }, + preheader: preheader_id, + length_site: start + 2, + })) +} + +pub(super) fn capture( + function: &Function, + types: &TypeInterner, + carriers: &ResourceCarrierFunctionPlan, +) -> Result, String> { + let witness = function + .resource_lowering + .as_ref() + .ok_or("carrier iterator has no exact Source witness")?; + witness.source.validate_types(types)?; + witness.current(function)?; + let mut inventory = Inventory::default(); + inventory.block(&witness.original.body)?; + let cfg = ControlFlowGraph::analyze(function) + .map_err(|errors| format!("carrier iterator CFG is malformed: {errors:?}"))?; + let mut rows = Vec::new(); + let mut binders = BTreeSet::new(); + let mut sources = BTreeSet::new(); + let mut cursors = BTreeSet::new(); + let mut lengths = BTreeSet::new(); + let mut gets = BTreeSet::new(); + // Raw For rows remain the planner's operation metadata. They cannot share + // their original binders with a second prepared loop. + for block in &function.blocks { + if let TerminatorKind::ForEach { + key, + value, + iterable, + .. + } = &block.terminator.kind + && (iterable.ty.index() as usize) < types.len() + && carrier_type(types, iterable.ty) + { + for binder in std::iter::once(*key).chain(value.iter().copied()) { + if !binders.insert(binder.index()) { + return Err("carrier raw iterator reused its Source binder".into()); + } + } + } + } + for block in &function.blocks { + let Some(prepared) = prepared( + function, + types, + carriers, + witness, + &inventory.loops, + &cfg, + block, + )? + else { + continue; + }; + if !sources.insert(prepared.row.source.index()) + || !cursors.insert( + prepared + .row + .cursor + .ok_or("prepared carrier iterator has no cursor")? + .index(), + ) + || !lengths.insert((prepared.preheader.index(), prepared.length_site)) + { + return Err( + "carrier prepared iterator duplicated its source, cursor or initialization".into(), + ); + } + for (ordinal, binder) in prepared.row.binders.iter().enumerate() { + if !binders.insert(binder.index()) || !gets.insert((prepared.row.body.index(), ordinal)) + { + return Err( + "carrier prepared iterator reused its Source binder or extraction".into(), + ); + } + } + rows.push(prepared.row); + } + for block in &function.blocks { + for (position, statement) in block.statements.iter().enumerate() { + let source = match &statement.kind { + StatementKind::SequenceLength { source, .. } + | StatementKind::SequenceGet { source, .. } + | StatementKind::IterationBorrow { source, .. } => source, + _ => continue, + }; + let ty = source + .ty(function) + .ok_or("carrier sequence statement lost its exact source type")?; + if ty.index() as usize >= types.len() { + return Err("carrier sequence source type is out of range".into()); + } + if !carrier_type(types, ty) { + continue; + } + let site = (block.id.index(), position); + match &statement.kind { + StatementKind::SequenceLength { .. } if lengths.contains(&site) => {} + StatementKind::SequenceGet { .. } if gets.contains(&site) => {} + StatementKind::IterationBorrow { .. } => { + return Err("pending Resource carrier: borrowed iteration needs its exact current lease".into()); + } + _ => { + return Err( + "carrier sequence statement is outside its exact prepared Source iterator" + .into(), + ); + } + } + } + } + Ok(rows) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers/planner.rs b/crates/jett_mir/src/resource_ownership/carriers/planner.rs new file mode 100644 index 00000000..5a0216b8 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers/planner.rs @@ -0,0 +1,1742 @@ +//! Fresh projection of the complete authenticated current graph. The original +//! checker owns Source move legality; runtime generations own physical custody. +use super::*; +use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, +}; + +struct Planner<'p> { + program: &'p Program, + function: &'p Function, + types: &'p TypeInterner, + plan: ResourceFunctionPlan, + carriers: ResourceCarrierFunctionPlan, + carrier_locals: Vec>, + leaf_locals: Vec>, + site: ResourceSite, + expression: Option, + ordinal: usize, + frame: ResourceFrameId, + call_frames: BTreeMap, + iteration_slots: BTreeMap, + edge: Option, + ended_loans: BTreeSet, + constructor_frames: BTreeMap, + constructor_slots: BTreeMap, + incoming_frames: Vec<(ResourceSite, ResourceFrameId)>, +} +impl Planner<'_> { + fn shape_id(&self, ty: TypeId) -> Result { + self.carriers + .shape_for_type(ty) + .map(ResourceCarrierShape::id) + .ok_or_else(|| "carrier operation lost its exact checked type graph row".into()) + } + fn slot( + &mut self, + ty: TypeId, + storage: ResourceSlotStorage, + frame: ResourceFrameId, + ) -> Result { + let shape = self.shape_id(ty)?; + if !self + .carriers + .shape(shape) + .is_some_and(ResourceCarrierShape::contains_resource) + { + return Err( + "carrier slot cannot grant custody to ordinary data or a descriptor".into(), + ); + } + let id = ResourceCarrierSlotId(self.carriers.slots.len()); + self.carriers.slots.push(ResourceCarrierSlot { + id, + frame, + shape, + storage, + }); + Ok(id) + } + fn temporary(&mut self, ty: TypeId) -> Result { + self.slot( + ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + self.frame, + ) + } + fn leaf_slot( + &mut self, + ty: TypeId, + storage: ResourceSlotStorage, + frame: ResourceFrameId, + ) -> Result { + let shape = shape(&self.program.resource_manifest, self.types, ty)? + .ok_or("carrier leaf bridge has no exact existing leaf shape")?; + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage, + }); + Ok(id) + } + fn operation(&mut self, role: ResourceOperationRole) { + let id = ResourceOperationId(self.plan.operations.len()); + self.plan.operations.push(ResourceOperation { + id, + frame: self.frame, + site: self.site, + ordinal: self.ordinal, + role, + expression: self.expression, + named_indirect: None, + indirect_hook: None, + }); + } + fn carrier_operation( + &mut self, + role: ResourceCarrierOperationRole, + ) -> ResourceCarrierOperationId { + let id = ResourceCarrierOperationId(self.carriers.operations.len()); + self.carriers.operations.push(ResourceCarrierOperation { + id, + site: self.site, + frame: self.frame, + role, + expression: self.expression, + edge: self.edge, + }); + self.operation(ResourceOperationRole::Carrier { operation: id }); + id + } + fn frame(&mut self, role: ResourceFrameRole) -> ResourceFrameId { + let id = ResourceFrameId(self.plan.frames.len()); + self.plan.frames.push(ResourceFrame { + id, + role, + site: self.site, + parent: Some(self.frame), + ordinal: self.ordinal, + }); + id + } + fn end_loan(&mut self, loan: ResourceCarrierLoanId) { + if self.ended_loans.insert(loan) { + self.carrier_operation(ResourceCarrierOperationRole::EndBorrow { loan }); + } + } + fn loan( + &mut self, + value: ResourceCarrierValue, + ty: TypeId, + path: Vec, + ) -> Result { + let source = match value { + ResourceCarrierValue::Owned { slot } => ResourceCarrierLoanSource::Slot(slot), + ResourceCarrierValue::Borrowed { loan } => ResourceCarrierLoanSource::Loan(loan), + _ => { + return Err( + "carrier projection cannot borrow an ordinary or single-leaf value".into(), + ); + } + }; + let id = ResourceCarrierLoanId(self.carriers.loans.len()); + let shape = self.shape_id(ty)?; + self.carriers.loans.push(ResourceCarrierLoan { + id, + frame: self.frame, + shape, + source, + path, + }); + self.carrier_operation(ResourceCarrierOperationRole::Borrow { loan: id }); + Ok(id) + } + fn value_type(&self, value: ResourceCarrierValue) -> Result { + match value { + ResourceCarrierValue::Ordinary { ty } => Ok(ty), + ResourceCarrierValue::Owned { slot } => self + .carriers + .slots + .get(slot.index()) + .and_then(|slot| self.carriers.shape(slot.shape)) + .map(ResourceCarrierShape::ty) + .ok_or_else(|| "carrier input lost its installed slot shape".into()), + ResourceCarrierValue::Borrowed { loan } => self + .carriers + .loans + .get(loan.index()) + .and_then(|loan| self.carriers.shape(loan.shape)) + .map(ResourceCarrierShape::ty) + .ok_or_else(|| "carrier input lost its installed loan shape".into()), + ResourceCarrierValue::LeafOwned { slot } => match &self + .plan + .slots + .get(slot.index()) + .ok_or("carrier leaf input slot missing")? + .storage + { + ResourceSlotStorage::Local { header } => Ok(header.ty), + _ => { + let wanted = &self.plan.slots[slot.index()].shape; + self.types + .type_ids() + .find(|ty| { + shape(&self.program.resource_manifest, self.types, *ty) + .ok() + .flatten() + .as_ref() + == Some(wanted) + }) + .ok_or_else(|| "carrier leaf input lost its exact shape type".into()) + } + }, + ResourceCarrierValue::LeafBorrowed { .. } => { + Err("carrier constructor cannot adopt a borrowed leaf".into()) + } + } + } + fn begin_constructor( + &mut self, + expression: &Expression, + constructor: ResourceCarrierConstructor, + ) -> Result { + let destination = self.temporary(expression.ty)?; + self.carrier_operation(ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor, + }); + Ok(destination) + } + fn child( + &mut self, + destination: ResourceCarrierSlotId, + index: usize, + expected: TypeId, + value: ResourceCarrierValue, + ) -> Result<(), String> { + if self.value_type(value)? != expected { + return Err("carrier constructor child changes its exact checked type".into()); + } + if matches!( + value, + ResourceCarrierValue::Borrowed { .. } | ResourceCarrierValue::LeafBorrowed { .. } + ) { + return Err("carrier owning constructor cannot adopt a resident loan".into()); + } + let shape = self.shape_id(expected)?; + self.carrier_operation(ResourceCarrierOperationRole::ConstructorChild { + destination, + child: ResourceCarrierChild { + index, + shape, + value, + }, + }); + Ok(()) + } + fn commit(&mut self, destination: ResourceCarrierSlotId) -> ResourceCarrierValue { + self.carrier_operation(ResourceCarrierOperationRole::CommitConstructor { destination }); + ResourceCarrierValue::Owned { slot: destination } + } + fn exact_order(&self, order: &[usize], count: usize) -> Result<(), String> { + let mut seen = BTreeSet::new(); + if order.len() != count + || order + .iter() + .any(|index| *index >= count || !seen.insert(*index)) + { + return Err("carrier constructor changed its checked lexical evaluation order".into()); + } + Ok(()) + } + fn expression( + &mut self, + expression: &Expression, + owned: bool, + ) -> Result { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + self.ordinal = self + .ordinal + .checked_add(1) + .ok_or("carrier expression ordinal overflow")?; + let result = self.expression_inner(expression, owned); + self.expression = previous; + result + } + // A checked branch can narrow a machine Local without changing its + // physical storage. Only observational places may read that same owner; + // owning Local reads still use expression_inner's exact header check. + fn observational_place( + &mut self, + expression: &Expression, + ) -> Result { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + self.ordinal = self + .ordinal + .checked_add(1) + .ok_or("carrier expression ordinal overflow")?; + let result = match &expression.kind { + E::Local(local) => { + let header = self + .function + .local(*local) + .ok_or("carrier observation lost its exact dense local header")?; + match ( + self.types.resolve(header.ty), + self.types.resolve(expression.ty), + ) { + (Type::Machine(owner), Type::MachineState { machine, .. }) + if owner == machine => + { + let index = usize::try_from(local.index()) + .map_err(|_| "carrier local index is not representable")?; + let value = self.carrier_locals.get(index).copied().flatten().ok_or( + "carrier observation has no dedicated physical slot or formal loan", + )?; + if !matches!( + value, + ResourceCarrierValue::Owned { .. } + | ResourceCarrierValue::Borrowed { .. } + ) || self.value_type(value)? != header.ty + { + Err( + "carrier observation changed its physical local custody type" + .into(), + ) + } else { + Ok(value) + } + } + _ => self.expression_inner(expression, false), + } + } + E::View(inner) => { + if expression.ty != inner.ty { + Err("carrier observational View changed its exact checked type".into()) + } else { + self.observational_place(inner) + } + } + _ => self.expression_inner(expression, false), + }; + self.expression = previous; + result + } + fn expression_inner( + &mut self, + expression: &Expression, + owned: bool, + ) -> Result { + match &expression.kind { + E::Local(local) => { + let header = self.function.local(*local).ok_or("carrier read lost its exact dense local header")?; + if header.ty != expression.ty { return Err("carrier local read changed its checked type".into()); } + let index = usize::try_from(local.index()).map_err(|_| "carrier local index is not representable")?; + if let Some(value) = self.carrier_locals[index] { + if owned && matches!(value, ResourceCarrierValue::Borrowed { .. }) { return Err("carrier owning read cannot consume an incoming or resident view".into()); } + return Ok(value); + } + if let Some(value) = self.leaf_locals[index] { + if owned && matches!(value, ResourceCarrierValue::LeafBorrowed { .. }) { return Err("carrier leaf adapter cannot adopt a borrowed view".into()); } + return Ok(value); + } + if resource_type_pending(self.types, expression.ty) && !matches!(self.types.resolve(expression.ty), Type::Function { .. }) { + return Err("carrier read has no dedicated slot or formal loan".into()); + } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::View(inner) => { + if owned && resource_type_pending(self.types, expression.ty) { return Err("carrier View cannot be adopted by owning storage".into()); } + self.expression(inner, false) + } + E::Call { function, args, evaluation_order, ownership } => self.call(expression, *function, args, evaluation_order, ownership), + E::InterfaceCoerce { value, adapters } if carrier_type(self.types, expression.ty) && adapters.is_empty() => { + if !self.compatible(value.ty, expression.ty) { return Err("carrier coercion changes its exact checked qualification".into()); } + let input = self.expression(value, owned)?; + if value.ty == expression.ty { return Ok(input); } + let ResourceCarrierValue::Owned { slot: source } = input else { return Err("pending Resource carrier: borrowed qualification needs its exact readonly graph projection".into()); }; + let destination = self.temporary(expression.ty)?; + self.transfer(source, destination)?; + Ok(ResourceCarrierValue::Owned { slot: destination }) + } + E::Field { base, owner_type, field } if carrier_type(self.types, base.ty) => self.field(expression, base, *owner_type, *field, owned), + E::StateIs { value, state } if carrier_type(self.types, value.ty) => { + let input = self.observational_place(value)?; + let physical_type = self.value_type(input)?; + let source = self.loan(input, physical_type, Vec::new())?; + let state = usize::try_from(state.index()).map_err(|_| "carrier state selector is not representable")?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::State { state }, target: None }); + self.end_loan(source); + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::Intrinsic { intrinsic, args, evaluation_order, .. } => { + self.exact_order(evaluation_order, args.len())?; + if matches!(intrinsic, hir::IntrinsicId::ListLength | hir::IntrinsicId::MapLength) && args.len() == 1 && carrier_type(self.types, args[0].ty) { + let input = self.expression(&args[0], false)?; + let source = self.loan(input, args[0].ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::Length, target: None }); + self.end_loan(source); + return Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }); + } + if args.iter().any(|arg| resource_type_pending(self.types, arg.ty)) || carrier_type(self.types, expression.ty) { + return Err("pending Resource carrier: this intrinsic requires its dedicated custody operation".into()); + } + for index in evaluation_order { self.expression(&args[*index], true)?; } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + E::ListConstruct { elements } if carrier_type(self.types, expression.ty) => { + let Type::List(element) = self.types.resolve(expression.ty) else { return Err("carrier list constructor lost its exact list type".into()); }; + let element = *element; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::List)?; + for (index, child) in elements.iter().enumerate() { + if child.ty != element { return Err("carrier list child changes its exact element type".into()); } + let value = self.expression(child, true)?; + self.child(destination, index, element, value)?; + } + Ok(self.commit(destination)) + } + E::MapConstruct { entries } if carrier_type(self.types, expression.ty) => { + let Type::Map(key, value) = self.types.resolve(expression.ty) else { return Err("carrier map constructor lost its exact map type".into()); }; + let (key, value) = (*key, *value); + if resource_type_pending(self.types, key) { return Err("carrier map key must preserve the checked primitive key contract".into()); } + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Map)?; + for (index, entry) in entries.iter().enumerate() { + if entry.key.ty != key || entry.value.ty != value { return Err("carrier map entry changes its exact key/value types".into()); } + let key_value = self.expression(&entry.key, true)?; + self.child(destination, index.checked_mul(2).ok_or("carrier map ordinal overflow")?, key, key_value)?; + let payload = self.expression(&entry.value, true)?; + self.child(destination, index.checked_mul(2).and_then(|n| n.checked_add(1)).ok_or("carrier map ordinal overflow")?, value, payload)?; + } + Ok(self.commit(destination)) + } + E::StructConstruct { struct_type, fields, evaluation_order, .. } if carrier_type(self.types, expression.ty) => { + if *struct_type != expression.ty { return Err("carrier record changed its exact nominal type".into()); } + let Type::Struct(owner) = self.types.resolve(*struct_type) else { return Err("carrier record lost its nominal definition".into()); }; + let definition = self.types.resolve_struct(*owner); + let expected = definition.fields.iter().map(|(_, ty)| *ty).collect::>(); + if expected.len() != fields.len() { return Err("carrier record changed its declared field count".into()); } + self.exact_order(evaluation_order, fields.len())?; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Struct)?; + for index in evaluation_order { + let value = self.expression(&fields[*index], true)?; + self.child(destination, *index, expected[*index], value)?; + } + Ok(self.commit(destination)) + } + E::EnumConstruct { enum_type, variant, payloads, evaluation_order } if carrier_type(self.types, expression.ty) => { + if *enum_type != expression.ty { return Err("carrier enum changed its exact nominal owner".into()); } + let Type::Enum(owner) = self.types.resolve(*enum_type) else { return Err("carrier enum lost its exact definition".into()); }; + let expected = self.types.resolve_enum(*owner).variants.get(usize::try_from(variant.index()).map_err(|_| "carrier variant index is not representable")?) + .ok_or("carrier enum selected a foreign variant")?.fields.iter().map(|(_, ty)| *ty).collect::>(); + if expected.len() != payloads.len() { return Err("carrier enum changed its selected payload count".into()); } + self.exact_order(evaluation_order, payloads.len())?; + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::Enum { variant: *variant })?; + for index in evaluation_order { + let value = self.expression(&payloads[*index], true)?; + self.child(destination, *index, expected[*index], value)?; + } + Ok(self.commit(destination)) + } + E::MachineConstruct { state_type, state, payloads } if carrier_type(self.types, expression.ty) => self.machine(expression, *state_type, *state, payloads), + E::OptionalNone if !carrier_type(self.types, expression.ty) && shape(&self.program.resource_manifest, self.types, expression.ty)?.is_some() => { + let destination = self.leaf_slot(expression.ty, ResourceSlotStorage::Expression { site: self.site, ordinal: self.ordinal }, self.frame)?; + self.operation(ResourceOperationRole::CreateAbsentSum { destination }); + Ok(ResourceCarrierValue::LeafOwned { slot: destination }) + } + E::OptionalNone if carrier_type(self.types, expression.ty) => { + let destination = self.begin_constructor(expression, ResourceCarrierConstructor::OptionalNone)?; + Ok(self.commit(destination)) + } + E::OptionalSome(inner) | E::ResultOk(inner) | E::ResultFail(inner) if carrier_type(self.types, expression.ty) => { + let (constructor, expected) = match (&expression.kind, self.types.resolve(expression.ty)) { + (E::OptionalSome(_), Type::Optional(payload)) => (ResourceCarrierConstructor::OptionalSome, *payload), + (E::ResultOk(_), Type::Result(ok, _)) => (ResourceCarrierConstructor::ResultOk, *ok), + (E::ResultFail(_), Type::Result(_, fail)) => (ResourceCarrierConstructor::ResultFail, *fail), + _ => return Err("carrier sum changed its exact selected arm type".into()), + }; + let destination = self.begin_constructor(expression, constructor)?; + let value = self.expression(inner, true)?; + self.child(destination, 0, expected, value)?; + Ok(self.commit(destination)) + } + E::RefinementValidated(_) if carrier_type(self.types, expression.ty) => Err("pending Resource carrier: occupied or refined construction needs its original checked predicate transition".into()), + E::ResourceInvoke { .. } | E::IndirectCall { .. } if resource_type_pending(self.types, expression.ty) => Err("pending Resource carrier: live leaf producers remain under their dedicated custody plan".into()), + E::Clone(_) if resource_type_pending(self.types, expression.ty) => Err("carrier physical copies cannot create an owning root".into()), + _ if carrier_type(self.types, expression.ty) => Err("pending Resource carrier: this expression needs its exact constructor or projection transport".into()), + _ => { + let mut has_resource_child = false; + walk::expression(expression, &mut |child| { + if !std::ptr::eq(child, expression) && resource_type_pending(self.types, child.ty) && !matches!(self.types.resolve(child.ty), Type::Function { .. }) { + has_resource_child = true; + } + }); + if has_resource_child { + // Common ordinary bool/scalar operators contain an exact typed + // observation. Recurse through their direct operands only. + match &expression.kind { + E::Binary { left, right, .. } => { self.expression(left, false)?; self.expression(right, false)?; } + E::Unary { value, .. } => { self.expression(value, false)?; } + E::Run(value) => { + if expression.ty != value.ty { + return Err("carrier Run observation changed its exact checked value type".into()); + } + self.expression(value, false)?; + } + E::Join(value) => { + let Type::Result(success, failure) = self.types.resolve(expression.ty) else { + return Err("carrier Join observation lost its exact checked result type".into()); + }; + if value.ty != expression.ty && (*success != value.ty || *failure != TypeInterner::STRING) { + return Err("carrier Join observation changed its exact checked payload or failure type".into()); + } + self.expression(value, false)?; + } + _ => return Err("pending Resource carrier: nested ordinary observation needs its dedicated operation".into()), + } + } + Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }) + } + } + } + fn machine( + &mut self, + expression: &Expression, + state_type: TypeId, + state: hir::StateId, + payloads: &[Expression], + ) -> Result { + let Type::MachineState { + machine, + state: selected, + } = self.types.resolve(state_type) + else { + return Err("carrier machine constructor lost its exact checked state type".into()); + }; + if selected.index() != state.index() { + return Err("carrier machine constructor changed its checked state selector".into()); + } + match self.types.resolve(expression.ty) { + Type::Machine(owner) if owner == machine => {} + Type::MachineState { + machine: owner, + state: actual, + } if owner == machine && actual == selected => {} + _ => { + return Err( + "carrier machine constructor changed its checked owner or qualification".into(), + ); + } + } + let fields = self + .types + .resolve_machine(*machine) + .state(*selected) + .ok_or("carrier machine lost its selected state definition")? + .fields + .iter() + .map(|(_, ty)| *ty) + .collect::>(); + if fields.len() != payloads.len() { + return Err("carrier machine changed its selected payload count".into()); + } + let state = usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?; + let destination = + self.begin_constructor(expression, ResourceCarrierConstructor::Machine { state })?; + for (index, (expected, child)) in fields.into_iter().zip(payloads).enumerate() { + let value = self.expression(child, true)?; + self.child(destination, index, expected, value)?; + } + Ok(self.commit(destination)) + } + fn field( + &mut self, + expression: &Expression, + base: &Expression, + owner_type: TypeId, + field: FieldId, + owned: bool, + ) -> Result { + let expected = match self.types.resolve(owner_type) { + Type::Struct(owner) => self + .types + .resolve_struct(*owner) + .fields + .get( + usize::try_from(field.index()) + .map_err(|_| "carrier field selector is not representable")?, + ) + .map(|(_, ty)| *ty), + Type::MachineState { machine, state } => self + .types + .resolve_machine(*machine) + .state(*state) + .and_then(|state| state.fields.get(usize::try_from(field.index()).ok()?)) + .map(|(_, ty)| *ty), + _ => None, + } + .ok_or("carrier field lost its exact checked nominal owner/ordinal")?; + if expected != expression.ty { + return Err("carrier field changes its declared exact payload type".into()); + } + let input = match self.types.resolve(owner_type) { + Type::MachineState { .. } => self.observational_place(base)?, + _ => self.expression(base, false)?, + }; + let physical_type = self.value_type(input)?; + let source = self.loan(input, physical_type, Vec::new())?; + let path = match self.types.resolve(owner_type) { + Type::Struct(_) => ResourceCarrierProjectionPath::Field { + owner: owner_type, + field, + }, + Type::MachineState { state, .. } => ResourceCarrierProjectionPath::State { + owner: owner_type, + state: usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?, + field: usize::try_from(field.index()) + .map_err(|_| "carrier field selector is not representable")?, + }, + _ => return Err("carrier field lost its exact checked projection family".into()), + }; + let result = self.project(expression.ty, source, vec![path], owned, None)?; + if owned { + self.end_loan(source); + } + Ok(result) + } + fn project( + &mut self, + ty: TypeId, + source: ResourceCarrierLoanId, + path: Vec, + owned: bool, + target: Option, + ) -> Result { + if carrier_type(self.types, ty) { + if owned { + let destination = match target { + Some(local) => match self.carrier_locals[usize::try_from(local.index()) + .map_err(|_| "carrier binder index is not representable")?] + { + Some(ResourceCarrierValue::Owned { slot }) => slot, + _ => { + return Err( + "carrier extracted binder lacks its exact destination slot".into(), + ); + } + }, + None => self.temporary(ty)?, + }; + self.end_loan(source); + self.carrier_operation(ResourceCarrierOperationRole::Extract { + source, + path, + destination, + }); + Ok(ResourceCarrierValue::Owned { slot: destination }) + } else { + let destination = + self.loan(ResourceCarrierValue::Borrowed { loan: source }, ty, path)?; + // The single Borrow row already authenticates the exact parent + // and path; no second Project row may mint the same loan again. + Ok(ResourceCarrierValue::Borrowed { loan: destination }) + } + } else if let Some(shape) = shape(&self.program.resource_manifest, self.types, ty)? { + if !matches!( + shape, + ResourceShape::Optional { .. } | ResourceShape::Result { .. } + ) { + return Err("pending Resource carrier: projected live Resource leaves need their exact child custody transport".into()); + } + let destination = match target { + Some(local) if owned => match self.leaf_locals[usize::try_from(local.index()) + .map_err(|_| "leaf binder index is not representable")?] + { + Some(ResourceCarrierValue::LeafOwned { slot }) => slot, + _ => { + return Err( + "carrier leaf extraction lacks its dedicated destination slot".into(), + ); + } + }, + _ => self.leaf_slot( + ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + self.frame, + )?, + }; + let loan = if owned { + None + } else { + Some(ResourceLoanId(self.plan.loans.len())) + }; + if owned { + self.end_loan(source); + } + let operation = self.carrier_operation(ResourceCarrierOperationRole::AdaptSum { + source, + path, + destination, + loan, + lease_frame: self.frame, + }); + if let Some(id) = loan { + self.plan.loans.push(ResourceLoan { + id, + frame: self.frame, + shape, + source: ResourceLoanSource::CarrierSumProjection { operation }, + parameter: None, + }); + self.operation(ResourceOperationRole::BorrowSum { loan: id }); + Ok(ResourceCarrierValue::LeafBorrowed { loan: id }) + } else { + Ok(ResourceCarrierValue::LeafOwned { slot: destination }) + } + } else { + if owned && let Some(target) = target { + self.end_loan(source); + self.carrier_operation(ResourceCarrierOperationRole::ExtractOrdinary { + source, + path, + target, + ty, + }); + } else { + self.carrier_operation(ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::Ordinary { path, ty }, + target, + }); + } + self.end_loan(source); + Ok(ResourceCarrierValue::Ordinary { ty }) + } + } + fn call( + &mut self, + expression: &Expression, + function: FunctionId, + args: &[Expression], + order: &[usize], + ownership: &hir::CallOwnership, + ) -> Result { + let callee = self + .program + .functions + .get( + usize::try_from(function.index()) + .map_err(|_| "carrier callee index is not representable")?, + ) + .filter(|callee| callee.id == function) + .ok_or("carrier Source call lost its exact callee")?; + self.exact_order(order, args.len())?; + let relevant = has_execution_records(callee, self.types) + || args + .iter() + .any(|arg| resource_type_pending(self.types, arg.ty)) + || resource_type_pending(self.types, expression.ty); + if !relevant { + for index in order { + self.expression(&args[*index], true)?; + } + return Ok(ResourceCarrierValue::Ordinary { ty: expression.ty }); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err("carrier call cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || callee.params.len() != args.len() { + return Err("carrier call changed its original complete Source tuple".into()); + } + let parent = self.frame; + let call_ordinal = self.ordinal; + let frame = self.frame(ResourceFrameRole::Operation); + self.frame = frame; + let loan_start = self.carriers.loans.len(); + let leaf_loan_start = self.plan.loans.len(); + let mut operands = Vec::new(); + for parameter in order { + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == *parameter) + .ok_or("carrier actual lost its original Source formal tuple")?; + let arg = &args[*parameter]; + if !resource_type_pending(self.types, arg.ty) + || matches!(self.types.resolve(arg.ty), Type::Function { .. }) + { + self.expression(arg, true)?; + operands.push(ResourceCallOperand::Ordinary { + parameter: *parameter, + ty: arg.ty, + }); + continue; + } + if fact.staging != hir::ArgumentStaging::Original + || matches!(fact.effect, Effect::Copy | Effect::ObserveData) + { + return Err( + "carrier actual cannot use ordinary staging or copying authority".into(), + ); + } + let retaining = fact.effect == Effect::RetainBorrow; + if retaining + && (fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View) + { + return Err( + "carrier retained actual changed its written View/formal association".into(), + ); + } + if !retaining && fact.syntax != Syntax::Bare { + return Err("carrier owned actual changed its checked bare spelling".into()); + } + let input = if !retaining && fact.physical_access == Access::View { + if let E::View(inner) = &arg.kind { + inner.as_ref() + } else { + arg + } + } else { + arg + }; + let value = self.expression(input, !retaining)?; + match value { + ResourceCarrierValue::Owned { .. } + if retaining && fact.callee_access == Access::View => + { + let loan = self.loan(value, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::Owned { slot: source } => { + let slot = self.slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.transfer(source, slot)?; + if fact.callee_access == Access::Owned { + operands.push(ResourceCallOperand::CarrierOwned { + parameter: *parameter, + slot, + }); + } else { + let loan = + self.loan(ResourceCarrierValue::Owned { slot }, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + } + ResourceCarrierValue::Borrowed { .. } if fact.callee_access == Access::View => { + let loan = self.loan(value, arg.ty, Vec::new())?; + operands.push(ResourceCallOperand::CarrierBorrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::LeafOwned { slot: source } + if fact.callee_access == Access::Owned => + { + let slot = self.leaf_slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.operation(ResourceOperationRole::Transfer { + source, + destination: slot, + }); + operands.push(ResourceCallOperand::Owned { + parameter: *parameter, + slot, + }); + } + ResourceCarrierValue::LeafOwned { slot: source } + if fact.callee_access == Access::View => + { + let slot = if retaining { + source + } else { + let destination = self.leaf_slot( + arg.ty, + ResourceSlotStorage::Argument { + site: self.site, + call_ordinal, + parameter: *parameter, + }, + frame, + )?; + self.operation(ResourceOperationRole::Transfer { + source, + destination, + }); + destination + }; + let loan = ResourceLoanId(self.plan.loans.len()); + let shape = self.plan.slots[slot.index()].shape.clone(); + self.plan.loans.push(ResourceLoan { + id: loan, + frame, + shape, + source: ResourceLoanSource::Owner(slot), + parameter: Some(*parameter), + }); + self.operation(ResourceOperationRole::BorrowSum { loan }); + operands.push(ResourceCallOperand::Borrowed { + parameter: *parameter, + loan, + }); + } + ResourceCarrierValue::LeafBorrowed { loan } + if fact.callee_access == Access::View => + { + operands.push(ResourceCallOperand::Borrowed { + parameter: *parameter, + loan, + }) + } + _ => { + return Err( + "carrier actual changes its checked owning/view endpoint role".into(), + ); + } + } + } + let (result, value) = if carrier_type(self.types, expression.ty) { + let slot = self.slot( + expression.ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + parent, + )?; + ( + ResourceCallResult::Carrier { slot }, + ResourceCarrierValue::Owned { slot }, + ) + } else if shape(&self.program.resource_manifest, self.types, expression.ty)?.is_some() { + let slot = self.leaf_slot( + expression.ty, + ResourceSlotStorage::Expression { + site: self.site, + ordinal: self.ordinal, + }, + parent, + )?; + ( + ResourceCallResult::Owned { slot }, + ResourceCarrierValue::LeafOwned { slot }, + ) + } else { + ( + ResourceCallResult::Ordinary { ty: expression.ty }, + ResourceCarrierValue::Ordinary { ty: expression.ty }, + ) + }; + let completed_ordinal = self.ordinal; + self.ordinal = call_ordinal; + self.operation(ResourceOperationRole::InvokeSourceFunction { + function, + source: source.clone(), + formals: source + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + evaluation_order: order.to_vec(), + operands, + result, + }); + self.ordinal = completed_ordinal; + for index in (leaf_loan_start..self.plan.loans.len()).rev() { + let loan = ResourceLoanId(index); + if self.plan.loans[index].frame == frame { + self.operation(ResourceOperationRole::EndSumBorrow { loan }); + } + } + for index in (loan_start..self.carriers.loans.len()).rev() { + let loan = ResourceCarrierLoanId(index); + if self.carriers.loans[index].frame == frame { + self.end_loan(loan); + } + } + self.operation(ResourceOperationRole::Complete { + outcome: ResourceCompletion::Normal, + }); + self.frame = parent; + Ok(value) + } + fn compatible(&self, actual: TypeId, expected: TypeId) -> bool { + actual == expected + || matches!((self.types.resolve(actual), self.types.resolve(expected)), + (Type::MachineState { machine, .. }, Type::Machine(owner)) if machine == owner) + } + fn transfer( + &mut self, + source: ResourceCarrierSlotId, + destination: ResourceCarrierSlotId, + ) -> Result<(), String> { + let actual = self + .carriers + .shape(self.carriers.slots[source.index()].shape) + .ok_or("carrier transfer source shape missing")? + .ty; + let expected = self + .carriers + .shape(self.carriers.slots[destination.index()].shape) + .ok_or("carrier transfer destination shape missing")? + .ty; + if actual == expected { + self.carrier_operation(ResourceCarrierOperationRole::Transfer { + source, + destination, + }); + } else if let (Type::MachineState { machine, state }, Type::Machine(owner)) = + (self.types.resolve(actual), self.types.resolve(expected)) + { + if machine != owner { + return Err("carrier qualification changes its exact machine owner".into()); + } + let machine = self.shape_id(expected)?; + let state = usize::try_from(state.index()) + .map_err(|_| "carrier state selector is not representable")?; + self.carrier_operation(ResourceCarrierOperationRole::QualifyMachine { + source, + destination, + machine, + state, + }); + } else { + return Err( + "carrier transfer changes exact graph node without a checked qualification".into(), + ); + } + Ok(()) + } + fn store( + &mut self, + local: LocalId, + input: ResourceCarrierValue, + ty: TypeId, + ) -> Result<(), String> { + let header = self + .function + .local(local) + .ok_or("carrier storage lost its exact current local header")?; + if !self.compatible(ty, header.ty) { + return Err("carrier storage changes its exact nominal/generic/state type".into()); + } + let index = usize::try_from(local.index()) + .map_err(|_| "carrier destination index is not representable")?; + if carrier_type(self.types, header.ty) { + if self.function.is_view_local(local) { + let loan = self.loan(input, header.ty, Vec::new())?; + self.carrier_locals[index] = Some(ResourceCarrierValue::Borrowed { loan }); + } else { + let Some(ResourceCarrierValue::Owned { slot: destination }) = + self.carrier_locals[index] + else { + return Err("carrier destination lacks its dedicated owning slot".into()); + }; + let ResourceCarrierValue::Owned { slot: source } = input else { + return Err( + "carrier owned storage cannot adopt a borrowed or ordinary value".into(), + ); + }; + if source != destination { + self.transfer(source, destination)?; + } + } + } else if shape(&self.program.resource_manifest, self.types, header.ty)?.is_some() { + if self.function.is_view_local(local) { + match input { + ResourceCarrierValue::LeafBorrowed { .. } => { self.leaf_locals[index] = Some(input); } + _ => return Err("pending Resource carrier: this resident leaf alias needs its lexical constructor transport".into()), + } + } else { + let Some(ResourceCarrierValue::LeafOwned { slot: destination }) = + self.leaf_locals[index] + else { + return Err("carrier leaf storage lacks its dedicated owning slot".into()); + }; + let ResourceCarrierValue::LeafOwned { slot: source } = input else { + return Err( + "carrier leaf storage cannot adopt a borrowed or aggregate value".into(), + ); + }; + if source != destination { + self.operation(ResourceOperationRole::Transfer { + source, + destination, + }); + } + } + } else if !matches!(input, ResourceCarrierValue::Ordinary { .. }) { + return Err("carrier custody cannot enter ordinary companion storage".into()); + } + Ok(()) + } + fn source_loan( + &mut self, + source: &SequenceSource, + ) -> Result<(ResourceCarrierLoanId, TypeId), String> { + let ty = source + .ty(self.function) + .ok_or("carrier sequence lost its exact current source type")?; + let root = usize::try_from(source.root().index()) + .map_err(|_| "carrier sequence root is not representable")?; + let input = self + .carrier_locals + .get(root) + .and_then(|value| *value) + .ok_or("carrier sequence source lacks a dedicated carrier slot or loan")?; + let path = match source { + SequenceSource::Local(_) => Vec::new(), + SequenceSource::Projected { path, .. } => path + .iter() + .map(|field| ResourceCarrierProjectionPath::Field { + owner: field.owner_type, + field: field.field, + }) + .collect(), + }; + let loan = self.loan(input, ty, path)?; + Ok((loan, ty)) + } + fn sequence( + &mut self, + consume: bool, + source: &SequenceSource, + index: LocalId, + target: LocalId, + part: SequencePart, + ) -> Result<(), String> { + if self.function.local(index).map(|header| header.ty) != Some(TypeInterner::INT64) { + return Err("carrier sequence index lost its exact Int64 header".into()); + } + let (source, ty) = self.source_loan(source)?; + let (expected, path) = match (self.types.resolve(ty), part) { + (Type::List(element), SequencePart::Element) => ( + *element, + ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::Local(index), + }, + ), + (Type::Map(key, _), SequencePart::Key) => ( + *key, + ResourceCarrierProjectionPath::MapKey { + index: ResourceCarrierIndex::Local(index), + }, + ), + (Type::Map(_, value), SequencePart::Value) => ( + *value, + ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::Local(index), + }, + ), + _ => { + return Err( + "carrier sequence extraction changed its checked list/map component".into(), + ); + } + }; + if self.function.local(target).map(|header| header.ty) != Some(expected) { + return Err("carrier sequence extraction changes its exact current binder type".into()); + } + self.project(expected, source, vec![path], consume, Some(target))?; + self.end_loan(source); + Ok(()) + } + fn finish(&mut self, outcome: ResourceCompletion) { + // Dynamic activation tracks acquisition order. These rows specify exact + // eligible endpoints; cleanup does not invent local-number drop order. + self.operation(ResourceOperationRole::Complete { outcome }); + } + fn block(&mut self, block: &BasicBlock) -> Result<(), String> { + for (index, statement) in block.statements.iter().enumerate() { + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Statement(index), + }; + self.ordinal = 0; + self.expression = None; + self.frame = self + .incoming_frames + .iter() + .find(|(site, _)| *site == self.site) + .map(|(_, frame)| *frame) + .ok_or("carrier statement has no authenticated incoming constructor prefix")?; + self.plan.execution_frames.push((self.site, self.frame)); + match &statement.kind { + StatementKind::ResourceConstructor(node) => { + let region = self.function.resource_constructor(node.constructor())?; + let frame = *self.constructor_frames.get(®ion.id()).ok_or("carrier constructor lost its private Operation frame")?; + let destination = *self.constructor_slots.get(®ion.id()).ok_or("carrier constructor lost its private building root")?; + match node { + ResourceConstructorNode::Begin { .. } => { + self.frame = frame; + self.carrier_operation(ResourceCarrierOperationRole::BeginConstructor { destination, constructor: ResourceCarrierConstructor::Struct }); + } + ResourceConstructorNode::Stage { source_index, field, value, .. } => { + if self.frame != frame || region.fields().get(*source_index).is_none_or(|f| f.stage() != self.site || f.field() != *field) { + return Err("carrier constructor Stage differs from its exact current field occurrence".into()); + } + let input = self.expression(value, true)?; + self.child(destination, *field, region.field_types()[*field], input)?; + } + ResourceConstructorNode::Commit { output, .. } => { + if self.frame != frame || region.commit().is_none_or(|(site, header)| site != self.site || header.id != *output) { + return Err("carrier constructor Commit differs from its exact output occurrence".into()); + } + self.carrier_operation(ResourceCarrierOperationRole::CommitConstructor { destination }); + self.store(*output, ResourceCarrierValue::Owned { slot: destination }, region.original().ty)?; + } + ResourceConstructorNode::End { outcome, .. } => { + if self.frame != frame || !region.exits().contains(&(self.site, *outcome)) { + return Err("carrier constructor End differs from its exact reached prefix".into()); + } + self.finish(*outcome); + } + } + } + StatementKind::Let { local, value } => { + let input = self.expression(value, !self.function.is_view_local(*local))?; + self.store(*local, input, value.ty)?; + } + StatementKind::Assign { target, value } | StatementKind::UpdateField { target, value, .. } => { + if resource_type_pending(self.types, target.ty) { return Err("pending Resource carrier: replacement needs its established acquisition/destruction transition".into()); } + self.expression(target, false)?; + self.expression(value, true)?; + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + let input = self.expression(value, true)?; + match input { + ResourceCarrierValue::Owned { slot } => { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + ResourceCarrierValue::LeafOwned { slot } => self.operation(ResourceOperationRole::Drop { source: slot, occupancy: if matches!(self.plan.slots[slot.index()].shape,ResourceShape::Plain { .. }) { ResourceOccupancy::Occupied } else { ResourceOccupancy::Empty } }), + ResourceCarrierValue::Borrowed { .. } | ResourceCarrierValue::LeafBorrowed { .. } => return Err("carrier borrowed result cannot be discarded as an owning endpoint".into()), + ResourceCarrierValue::Ordinary { .. } => {} + } + } + StatementKind::SequenceLength { source, target } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => { + if self.function.local(*target).map(|header| header.ty) != Some(TypeInterner::INT64) { return Err("carrier sequence length lost its exact Int64 destination".into()); } + let (source, _) = self.source_loan(source)?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source, observation: ResourceCarrierObservation::Length, target: Some(*target) }); + self.end_loan(source); + } + StatementKind::SequenceGet { consume, source, index, target, part } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => self.sequence(*consume, source, *index, *target, *part)?, + StatementKind::SumTag { source, target } => { + let ty = self.function.local(*source).ok_or("carrier SumTag source header missing")?.ty; + if carrier_type(self.types, ty) { + let input = self.carrier_locals[usize::try_from(source.index()).map_err(|_| "carrier sum source index is not representable")?].ok_or("carrier SumTag source slot missing")?; + let loan = self.loan(input, ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source: loan, observation: ResourceCarrierObservation::Tag, target: Some(*target) }); + self.end_loan(loan); + } else if let Some(ResourceCarrierValue::LeafBorrowed { loan }) = self.leaf_locals[usize::try_from(source.index()).map_err(|_| "carrier leaf source index is not representable")?] { + self.operation(ResourceOperationRole::ObserveSumView { source: loan, target: *target }); + } + } + StatementKind::SumTake { source, target, success } => { + let ty = self.function.local(*source).ok_or("carrier SumTake source header missing")?.ty; + let output = self.function.local(*target).ok_or("carrier SumTake destination header missing")?.ty; + if carrier_type(self.types, ty) { + let (expected, path) = match (self.types.resolve(ty), success) { + (Type::Optional(inner), true) => (*inner, ResourceCarrierProjectionPath::OptionalSome), + (Type::Result(ok, _), true) => (*ok, ResourceCarrierProjectionPath::ResultOk), + (Type::Result(_, fail), false) => (*fail, ResourceCarrierProjectionPath::ResultFail), + _ => return Err("carrier SumTake selected a non-payload arm".into()), + }; + if output != expected { return Err("carrier SumTake changed its exact selected payload type".into()); } + let input = self.carrier_locals[usize::try_from(source.index()).map_err(|_| "carrier sum source index is not representable")?].ok_or("carrier SumTake source slot missing")?; + let loan = self.loan(input, ty, Vec::new())?; + self.project(output, loan, vec![path], !self.function.is_view_local(*target), Some(*target))?; + self.end_loan(loan); + if !self.function.is_view_local(*target) && let ResourceCarrierValue::Owned { slot } = input { + self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); + } + } else if resource_type_pending(self.types, ty) { + return Err("pending Resource carrier: nested leaf Handle needs its original borrowed-sum selecting edge".into()); + } + } + StatementKind::Assert { condition, message } => { self.expression(condition, false)?; if let Some(message) = message { self.expression(message, false)?; } } + StatementKind::CheckRefinement { call, .. } => { self.expression(call, false)?; } + StatementKind::Breakpoint { condition, bindings } => { + if bindings.iter().any(|local| self.function.local(*local).is_some_and(|local| resource_type_pending(self.types, local.ty))) { return Err("pending Resource carrier: debugger custody observation transport is unproved".into()); } + if let Some(condition) = condition { self.expression(condition, false)?; } + } + StatementKind::BeginCallView { local, value } => { + if resource_type_pending(self.types, value.ty) || self.function.local(*local).is_some_and(|header| resource_type_pending(self.types, header.ty)) { return Err("ordinary BeginCallView cannot mint a carrier loan".into()); } + self.expression(value, false)?; + } + StatementKind::ResourceCall(_) => return Err("pending Resource carrier: cross-block call requires its complete prepared carrier prefix".into()), + StatementKind::ResourceLexicalExit(_) => return Err("pending Resource carrier: lexical carrier aliases require their exact private exit rows".into()), + StatementKind::IterationBorrow { source, .. } if source.ty(self.function).is_some_and(|ty| carrier_type(self.types, ty)) => return Err("pending Resource carrier: borrowed iteration needs its exact bounded parent lease".into()), + _ => {} + } + } + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Terminator, + }; + self.ordinal = 0; + self.expression = None; + self.frame = self + .incoming_frames + .iter() + .find(|(site, _)| *site == self.site) + .map(|(_, frame)| *frame) + .ok_or("carrier terminator has no authenticated incoming constructor prefix")?; + self.plan.execution_frames.push((self.site, self.frame)); + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + let input = self.expression(value, true)?; + if !self.compatible(value.ty, self.function.return_type) { return Err("carrier Return changes its declared exact nominal/state type".into()); } + match input { + ResourceCarrierValue::Owned { slot: source } => { + let frame = self.plan.provisional_return().ok_or("carrier Return has no exact provisional frame")?.id(); + let destination = self.slot(self.function.return_type, ResourceSlotStorage::Return { site: self.site }, frame)?; + self.transfer(source, destination)?; + self.frame = ResourceFrameId(0); + self.finish(ResourceCompletion::Return); + self.carrier_operation(ResourceCarrierOperationRole::PublishReturn { source: destination }); + } + ResourceCarrierValue::LeafOwned { slot: source } => { + let frame = self.plan.provisional_return().ok_or("carrier leaf Return has no exact provisional frame")?.id(); + let destination = self.leaf_slot(self.function.return_type, ResourceSlotStorage::Return { site: self.site }, frame)?; + self.operation(ResourceOperationRole::Transfer { source, destination }); + self.frame = ResourceFrameId(0); + self.finish(ResourceCompletion::Return); + self.operation(ResourceOperationRole::CompleteReturnAfterCleanup { source: destination }); + } + ResourceCarrierValue::Ordinary { .. } => self.finish(ResourceCompletion::Return), + _ => return Err("carrier Return cannot publish a borrowed value".into()), + } + } else { self.finish(ResourceCompletion::Return); } + } + TerminatorKind::Branch { condition, .. } => { self.expression(condition, false)?; } + TerminatorKind::ForEach { key, value, by_view, iterable, body, exit } if carrier_type(self.types, iterable.ty) => { + control_flow::carrier_sequence(self.function, self.types, *key, *value, *by_view, iterable)?; + let input = self.expression(iterable, true)?; + let destination = self.temporary(iterable.ty)?; + self.iteration_slots.insert(block.id.index(), destination); + self.carrier_operation(ResourceCarrierOperationRole::BeginIteration { source: input, destination, body: *body, exit: *exit }); + let mut binders = vec![*key]; + if let Some(value) = value { binders.push(*value); } + self.carriers.iterations.push(ResourceCarrierIteration { header: block.id, body: *body, exit: *exit, source: destination, binders: binders.clone(), cursor: None }); + self.site = ResourceSite { function: self.function.id, block: *body, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::IterationBody { source: block.id, target: *body }); + for (ordinal, binder) in binders.iter().enumerate() { + let ty = self.function.local(*binder).ok_or("carrier For lost its exact original binder")?.ty; + let source = self.loan(ResourceCarrierValue::Owned { slot: destination }, iterable.ty, Vec::new())?; + let index = ResourceCarrierIndex::CurrentIteration { header: block.id }; + let path = match self.types.resolve(iterable.ty) { + Type::List(_) => ResourceCarrierProjectionPath::Element { index }, + Type::Map(..) if ordinal == 0 => ResourceCarrierProjectionPath::MapKey { index }, + Type::Map(..) => ResourceCarrierProjectionPath::MapValue { index }, + _ => return Err("carrier For lost its checked list/map component".into()), + }; + self.project(ty, source, vec![path], true, Some(*binder))?; + self.end_loan(source); + } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + TerminatorKind::Switch { scrutinee, variants, otherwise } if carrier_type(self.types, scrutinee.ty) => { + let input = self.expression(scrutinee, true)?; + let tag_loan = self.loan(input, scrutinee.ty, Vec::new())?; + self.carrier_operation(ResourceCarrierOperationRole::Observe { source: tag_loan, observation: ResourceCarrierObservation::Tag, target: None }); + self.end_loan(tag_loan); + let Type::Enum(owner) = self.types.resolve(scrutinee.ty) else { return Err("carrier Switch lost its checked enum owner".into()); }; + let mut seen = BTreeSet::new(); + for (variant, target, bindings) in variants { + let index = usize::try_from(variant.index()).map_err(|_| "carrier Switch selector is not representable")?; + let fields = self.types.resolve_enum(*owner).variants.get(index).ok_or("carrier Switch selected a foreign variant")?.fields.iter().map(|(_, ty)| *ty).collect::>(); + if !seen.insert(index) || fields.len() != bindings.len() { return Err("carrier Switch changed its exact selected binder count".into()); } + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::SwitchVariant { source: block.id, variant: *variant, target: *target }); + for (ordinal, (ty, binding)) in fields.into_iter().zip(bindings).enumerate() { + if self.function.local(*binding).map(|header| header.ty) != Some(ty) { return Err("carrier Switch changed its exact selected binder header".into()); } + let path = vec![ResourceCarrierProjectionPath::Variant { owner: scrutinee.ty, variant: *variant, field: ordinal }]; + // Runtime extraction is selected by the exact Switch edge. + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + let source = self.loan(input, scrutinee.ty, Vec::new())?; + self.project(ty, source, path, !self.function.is_view_local(*binding), Some(*binding))?; + self.end_loan(source); + } + if let ResourceCarrierValue::Owned { slot } = input { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + if let Some(target) = otherwise { + self.site = ResourceSite { function: self.function.id, block: *target, position: ResourcePosition::Statement(0) }; + self.edge = Some(ResourceCarrierEdge::SwitchOtherwise { source: block.id, target: *target }); + if let ResourceCarrierValue::Owned { slot } = input { self.carrier_operation(ResourceCarrierOperationRole::Retire { source: slot }); } + self.edge = None; + self.site = ResourceSite { function: self.function.id, block: block.id, position: ResourcePosition::Terminator }; + } + } + TerminatorKind::Unreachable => self.finish(ResourceCompletion::Abort), + TerminatorKind::Respond(_) | TerminatorKind::ReflectedTypeDispatch { .. } => return Err("pending Resource carrier: this control family needs its independent current-node proof".into()), + _ => {} + } + Ok(()) + } +} + +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + let witness = function + .resource_lowering + .as_ref() + .ok_or("carrier plan has no original checked Source constructor witness")?; + witness.source.validate_types(types)?; + witness.current(function)?; + if function.capture_count != 0 { + return Err("pending Resource carrier: aggregate captures need their exact environment custody transport".into()); + } + let cfg = ControlFlowGraph::analyze(function) + .map_err(|errors| format!("carrier CFG is malformed: {errors:?}"))?; + let site = ResourceSite { + function: function.id, + block: function.entry, + position: ResourcePosition::Terminator, + }; + let returned = resource_type_pending(types, function.return_type) + && !matches!(types.resolve(function.return_type), Type::Function { .. }); + let return_frame = returned.then_some(ResourceFrameId(1)); + let mut frames = vec![ResourceFrame { + id: ResourceFrameId(0), + role: ResourceFrameRole::Scope, + site, + parent: return_frame, + ordinal: 0, + }]; + if let Some(id) = return_frame { + frames.push(ResourceFrame { + id, + role: ResourceFrameRole::Return, + site, + parent: None, + ordinal: 0, + }); + } + let plan = ResourceFunctionPlan { + graph_descriptors: Vec::new(), + function: function.id, + identity: function.identity.clone(), + parameters: function.params.clone(), + return_type: function.return_type, + frames, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + execution_frames: Vec::new(), + lexical_exits: Vec::new(), + named_callable_producers: Vec::new(), + named_callable_values: named_callables::current_values(witness, function)?, + descriptor_return: witness.descriptors.returned().cloned(), + descriptor_values: returned_descriptors::current_values(witness, function)?, + descriptor_locals: witness + .descriptors + .locals() + .map(|(local, hook)| (local, hook.clone())) + .collect(), + carriers: None, + }; + let mut planner = Planner { + program, + function, + types, + plan, + carriers: ResourceCarrierFunctionPlan { + shapes: graph::capture(&witness.source, &program.resource_manifest, types)?, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + iterations: Vec::new(), + }, + carrier_locals: vec![None; function.locals.len()], + leaf_locals: vec![None; function.locals.len()], + site, + expression: None, + ordinal: 0, + frame: ResourceFrameId(0), + call_frames: BTreeMap::new(), + iteration_slots: BTreeMap::new(), + edge: None, + ended_loans: BTreeSet::new(), + constructor_frames: BTreeMap::new(), + constructor_slots: BTreeMap::new(), + incoming_frames: Vec::new(), + }; + for region in &witness.constructors { + let parent = match region.parent() { + None => ResourceFrameId(0), + Some(constructors::ResourceOperationRegion::Constructor(parent)) => *planner.constructor_frames.get(&parent) + .ok_or("carrier constructor has a missing exact parent Operation")?, + Some(constructors::ResourceOperationRegion::Call(_)) => return Err("pending Resource carrier: constructor under a staged Source call needs its authenticated mixed-prefix transport".into()), + }; + planner.site = region.begin(); + planner.frame = parent; + let frame = planner.frame(ResourceFrameRole::Operation); + planner.constructor_frames.insert(region.id(), frame); + let slot = planner.slot( + region.original().ty, + ResourceSlotStorage::Expression { + site: region.begin(), + ordinal: 0, + }, + frame, + )?; + planner.constructor_slots.insert(region.id(), slot); + } + planner.incoming_frames = constructor_frames(function, &cfg, &planner.constructor_frames)?; + for local in &function.locals { + let index = usize::try_from(local.id.index()) + .map_err(|_| "carrier local index is not representable")?; + if carrier_type(types, local.ty) && !function.is_view_local(local.id) { + let slot = planner.slot( + local.ty, + ResourceSlotStorage::Local { + header: local.clone(), + }, + ResourceFrameId(0), + )?; + planner.carrier_locals[index] = Some(ResourceCarrierValue::Owned { slot }); + } else if !carrier_type(types, local.ty) + && shape(&program.resource_manifest, types, local.ty)?.is_some() + && !function.is_view_local(local.id) + { + let slot = planner.leaf_slot( + local.ty, + ResourceSlotStorage::Local { + header: local.clone(), + }, + ResourceFrameId(0), + )?; + planner.leaf_locals[index] = Some(ResourceCarrierValue::LeafOwned { slot }); + } + } + for (parameter, param) in function.params.iter().enumerate() { + if param.mode != ParamMode::View { + continue; + } + let index = usize::try_from(param.local.index()) + .map_err(|_| "carrier formal index is not representable")?; + if carrier_type(types, param.ty) { + let id = ResourceCarrierLoanId(planner.carriers.loans.len()); + let shape = planner.shape_id(param.ty)?; + planner.carriers.loans.push(ResourceCarrierLoan { + id, + frame: ResourceFrameId(0), + shape, + source: ResourceCarrierLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + path: Vec::new(), + }); + planner.carrier_locals[index] = Some(ResourceCarrierValue::Borrowed { loan: id }); + } else if let Some(shape) = shape(&program.resource_manifest, types, param.ty)? { + let id = ResourceLoanId(planner.plan.loans.len()); + planner.plan.loans.push(ResourceLoan { + id, + frame: ResourceFrameId(0), + shape, + source: ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + parameter: None, + }); + planner.leaf_locals[index] = Some(ResourceCarrierValue::LeafBorrowed { loan: id }); + } + } + let mut reachable = vec![false; function.blocks.len()]; + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + let index = usize::try_from(block.index()) + .map_err(|_| "carrier block index is not representable")?; + if reachable[index] { + continue; + } + reachable[index] = true; + pending.extend_from_slice(cfg.successors(block)); + } + for block in &function.blocks { + if reachable[usize::try_from(block.id.index()) + .map_err(|_| "carrier block index is not representable")?] + { + planner.block(block)?; + } else if walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + { + return Err( + "carrier custody operation is disconnected from its authenticated entry CFG".into(), + ); + } + } + // Prepared sequence rows are accepted only after their complete canonical + // current loop and original Source For tuple have been joined privately. + let prepared = iterations::capture(function, types, &planner.carriers)?; + planner.carriers.iterations.extend(prepared); + let resets = planner.carriers.iterations.clone(); + for iteration in resets { + planner.site = ResourceSite { + function: function.id, + block: iteration.header, + position: ResourcePosition::Terminator, + }; + planner.frame = ResourceFrameId(0); + planner.expression = None; + planner.edge = None; + planner.carrier_operation(ResourceCarrierOperationRole::RetireIteration { + source: iteration.source, + binders: iteration.binders, + }); + } + flow::validate(function, types, &planner.plan, &planner.carriers)?; + planner.plan.carriers = Some(planner.carriers); + Ok(planner.plan) +} + +/// Reconstruct active constructor prefixes through actual CFG edges. Numeric +/// block order and a public marker are never frame authority. +fn constructor_frames( + function: &Function, + cfg: &ControlFlowGraph, + frames: &BTreeMap, +) -> Result, String> { + let mut incoming: Vec>> = vec![None; function.blocks.len()]; + incoming[function.entry.index() as usize] = Some(Vec::new()); + let mut queue = std::collections::VecDeque::from([function.entry]); + let mut sites = Vec::new(); + while let Some(id) = queue.pop_front() { + let block = function + .blocks + .get(id.index() as usize) + .filter(|b| b.id == id) + .ok_or("carrier constructor CFG selected a foreign block")?; + let mut active = incoming[id.index() as usize] + .clone() + .ok_or("carrier constructor CFG lost its incoming prefix")?; + let frame = |active: &[ResourceConstructorId]| -> Result { + active + .last() + .map(|id| { + frames + .get(id) + .copied() + .ok_or_else(|| "carrier constructor prefix lost its private frame".into()) + }) + .unwrap_or(Ok(ResourceFrameId(0))) + }; + for (index, statement) in block.statements.iter().enumerate() { + let site = ResourceSite { + function: function.id, + block: id, + position: ResourcePosition::Statement(index), + }; + sites.push((site, frame(&active)?)); + if let StatementKind::ResourceConstructor(node) = &statement.kind { + let region = function.resource_constructor(node.constructor())?; + match node { + ResourceConstructorNode::Begin { constructor } => { + let expected = active + .last() + .copied() + .map(constructors::ResourceOperationRegion::Constructor); + if region.begin() != site + || region.parent() != expected + || active.contains(constructor) + { + return Err("carrier constructor Begin differs from its exact active parent prefix".into()); + } + active.push(*constructor); + } + ResourceConstructorNode::End { + constructor, + outcome, + } => { + if active.pop() != Some(*constructor) + || !region.exits().contains(&(site, *outcome)) + { + return Err( + "carrier constructor End is not the exact active suffix".into() + ); + } + } + _ if active.last().copied() != Some(node.constructor()) => { + return Err( + "carrier constructor field or Commit has no reached exact prefix" + .into(), + ); + } + _ => {} + } + } + } + sites.push(( + ResourceSite { + function: function.id, + block: id, + position: ResourcePosition::Terminator, + }, + frame(&active)?, + )); + if matches!( + block.terminator.kind, + TerminatorKind::Return(_) | TerminatorKind::Unreachable + ) && !active.is_empty() + { + return Err("carrier terminal path leaves an uncompleted constructor prefix".into()); + } + for successor in cfg.successors(id) { + let next = incoming + .get_mut(successor.index() as usize) + .ok_or("carrier constructor CFG has a foreign successor")?; + match next { + Some(previous) if *previous != active => { + return Err("carrier constructor CFG joins different operation prefixes".into()); + } + Some(_) => {} + None => { + *next = Some(active.clone()); + queue.push_back(*successor); + } + } + } + } + Ok(sites) +} diff --git a/crates/jett_mir/src/resource_ownership/carriers_tests.rs b/crates/jett_mir/src/resource_ownership/carriers_tests.rs new file mode 100644 index 00000000..a2531b78 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/carriers_tests.rs @@ -0,0 +1,1731 @@ +use super::carriers::{ + ResourceCarrierConstructor, ResourceCarrierEdge, ResourceCarrierFunctionPlan, + ResourceCarrierIndex, ResourceCarrierLoanSource, ResourceCarrierNode, + ResourceCarrierObservation, ResourceCarrierOperationRole, ResourceCarrierProjectionPath, + ResourceCarrierShape, ResourceCarrierShapeId, +}; +use super::tests::{SUPPORT, checked_support}; +use super::*; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = include_str!("fixtures/17_absent_aggregate_shapes.jett"); +const CONSTRUCTORS: [&str; 12] = [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", +]; +const CONSUMING_SUFFIX: &str = r#" +function source17_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +export function consuming_absent_list() returns int64: + list[optional[resource_probe.TestHandle]] values = absent_tokens() + mutable int64 absent_count = 0 + for candidate in values: + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + return absent_count +export function consuming_absent_map() returns int64: + map[string, optional[resource_probe.TestHandle]] values = absent_map() + mutable int64 absent_count = 0 + for key, candidate in values: + if key != "empty": + return -1 + if source17_absent_optional(view candidate): + absent_count = absent_count + 1 + return absent_count +export function borrowed_absent_fallback() returns int64: + TokenEnvelope value = absent_envelope() + if source17_absent_optional(view value.fallback): + return value.marker + return 0 +export function matching_absent_choice() returns int64: + TokenChoice value = absent_choice() + match value: + vacant(marker, tokens): + return marker + other: + return 0 + return -1 +"#; + +fn materialized(checked: &Arc) -> hir::Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + assert!( + required + .values + .values() + .all(|value| matches!(value, jett_comptime::Value::Int64(17))) + ); + let mut program = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values( + &mut program, + &required.values, + &checked.checked().interner, + ) + .unwrap(); + hir::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + program +} + +fn lowered(checked: &Arc) -> Program { + let program = materialized(checked); + lower(&program, &checked.checked().interner).unwrap() +} + +fn named<'a>(plan: &'a ResourceOwnershipPlan<'_>, name: &str) -> &'a ResourceFunctionPlan { + plan.functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} Resource plan")) +} + +fn function<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} MIR function")) +} + +fn function_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("retained app.{name} MIR function")) +} + +fn carrier(plan: &ResourceFunctionPlan) -> &ResourceCarrierFunctionPlan { + plan.carriers() + .expect("typed aggregate transport, including latent occupied arms") +} + +fn result_shape(plan: &ResourceFunctionPlan) -> &ResourceCarrierShape { + carrier(plan).shape_for_type(plan.return_type()).unwrap() +} + +fn child(plan: &ResourceCarrierFunctionPlan, id: ResourceCarrierShapeId) -> &ResourceCarrierShape { + plan.shape(id) + .expect("a checked graph edge retains its exact target type") +} + +fn consuming_program(release: bool) -> (Arc, Program) { + let source = format!("{SOURCE}{CONSUMING_SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + (checked, program) +} + +fn original_for( + program: &Program, + name: &str, +) -> (BlockId, BlockId, BlockId, Vec, TypeId) { + function(program, name) + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::ForEach { + key, + value, + iterable, + body, + exit, + .. + } => { + let mut binders = vec![*key]; + if let Some(value) = value { + binders.push(*value); + } + Some((block.id, *body, *exit, binders, iterable.ty)) + } + _ => None, + }) + .expect("one original consuming For header") +} + +fn assert_leaf_binder(plan: &ResourceFunctionPlan, function: &Function, local: LocalId) { + let header = function.local(local).unwrap(); + assert!( + plan.owner_slots() + .iter() + .any(|slot| matches!(slot.storage(), + ResourceSlotStorage::Local { header: captured } if captured == header + ) && matches!(slot.shape(), ResourceShape::Optional { .. })), + "the original Optional[Resource] binder needs its exact dedicated leaf-sum slot" + ); +} + +fn assert_optional_view_call(plan: &ResourceFunctionPlan, helper: FunctionId) { + let loan = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, operands, .. + } if *function == helper => match operands.as_slice() { + [ResourceCallOperand::Borrowed { parameter: 0, loan }] => Some(*loan), + _ => None, + }, + _ => None, + }) + .expect("the real Optional view helper uses the bounded leaf-sum call protocol"); + assert!(matches!( + plan.loans()[loan.index()].source(), + ResourceLoanSource::Owner(_) + )); + assert!( + plan.operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceOperationRole::BorrowSum { loan: actual } if *actual == loan + )) + ); + assert!( + plan.operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceOperationRole::EndSumBorrow { loan: actual } if *actual == loan + )) + ); +} + +#[test] +fn resource_carrier_consuming_source17_for_rows_preserve_raw_and_prepared_cursor_and_binder_transport() + { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + let helper = function(&raw, "source17_absent_optional").id; + let raw_plan = validate_resource_ownership(&raw, types).unwrap(); + let mut prepared = raw.clone(); + crate::prepare_native_sequences(&mut prepared, types); + crate::validate(&prepared).unwrap(); + let prepared_plan = validate_resource_ownership(&prepared, types).unwrap(); + for name in ["consuming_absent_list", "consuming_absent_map"] { + let original = original_for(&raw, name); + let raw_function = named(&raw_plan, name); + let raw_carriers = carrier(raw_function); + let [raw_iteration] = raw_carriers.iterations() else { + panic!("one original For iteration row") + }; + assert_eq!( + (raw_iteration.header, raw_iteration.body, raw_iteration.exit), + (original.0, original.1, original.2) + ); + assert_eq!(raw_iteration.binders, original.3); + assert_eq!(raw_iteration.cursor, None); + assert_eq!( + child( + raw_carriers, + raw_carriers.slots()[raw_iteration.source.index()].shape() + ) + .ty(), + original.4 + ); + let raw_edge = ResourceCarrierEdge::IterationBody { + source: original.0, + target: original.1, + }; + assert!( + raw_carriers.operations_on_edge(raw_edge).any( + |operation| matches!(operation.role(), + ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::CurrentIteration { header } + }] | [ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::CurrentIteration { header } + }] if *header == original.0) + ) + ), + "raw consuming For extracts an absent leaf-sum shell on its exact selected body edge" + ); + assert!( + raw_function.loans().iter().all(|loan| !matches!( + loan.source(), + ResourceLoanSource::CarrierSumProjection { .. } + )), + "an owned For binder is an owner, not a borrowed carrier fallback" + ); + let prepared_function = named(&prepared_plan, name); + let graph = carrier(prepared_function); + let [iteration] = graph.iterations() else { + panic!("one prepared For iteration row") + }; + assert_eq!( + (iteration.header, iteration.body, iteration.exit), + (original.0, original.1, original.2) + ); + assert_eq!(iteration.binders, original.3); + let cursor = iteration + .cursor + .expect("the exact canonical sequence cursor survives in the carrier plan"); + let current = function(&prepared, name); + assert_eq!(current.local(cursor).unwrap().ty, TypeInterner::INT64); + assert_eq!( + child(graph, graph.slots()[iteration.source.index()].shape()).ty(), + original.4 + ); + let gets = current.blocks[usize::try_from(iteration.body.index()).unwrap()] + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::SequenceGet { + source, + index, + target, + part, + .. + } => Some((source, index, target, part)), + _ => None, + }) + .collect::>(); + assert_eq!(gets.len(), iteration.binders.len()); + for ((source, index, target, _), binder) in gets.iter().zip(&iteration.binders) { + assert_eq!(**index, cursor); + assert_eq!(**target, *binder); + assert_eq!(source.ty(current), Some(original.4)); + } + if name == "consuming_absent_map" { + assert_eq!(*gets[0].3, SequencePart::Key); + assert_eq!(*gets[1].3, SequencePart::Value); + assert_eq!( + current.local(iteration.binders[0]).unwrap().ty, + TypeInterner::STRING + ); + } else { + assert_eq!(*gets[0].3, SequencePart::Element); + } + assert!( + !graph.operations().iter().any(|operation| matches!( + operation.edge(), + Some(ResourceCarrierEdge::IterationBody { .. }) + )), + "prepared extraction runs at canonical SequenceGet statements" + ); + assert!( + graph + .operations() + .iter() + .any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::AdaptSum { path, loan: None, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Element { + index: ResourceCarrierIndex::Local(actual) + }] | [ResourceCarrierProjectionPath::MapValue { + index: ResourceCarrierIndex::Local(actual) + }] if *actual == cursor) + )) + ); + let binder = *iteration.binders.last().unwrap(); + assert_leaf_binder( + raw_function, + function(&raw, name), + *raw_iteration.binders.last().unwrap(), + ); + assert_leaf_binder(prepared_function, current, binder); + assert_optional_view_call(raw_function, helper); + assert_optional_view_call(prepared_function, helper); + assert!(prepared_function.loans().iter().all(|loan| !matches!( + loan.source(), + ResourceLoanSource::CarrierSumProjection { .. } + ))); + } + } +} + +#[test] +fn resource_carrier_source17_borrowed_fallback_adapts_a_parent_bound_sum_loan() { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let function = named(&plan, "borrowed_absent_fallback"); + let graph = carrier(function); + let owner_type = self::function(&program, "absent_envelope").return_type; + let projected = function + .loans() + .iter() + .filter_map(|loan| match loan.source() { + ResourceLoanSource::CarrierSumProjection { operation } => { + Some((loan, operation)) + } + _ => None, + }) + .collect::>(); + let [(loan, operation)] = projected.as_slice() else { + panic!("one exact borrowed fallback adapter") + }; + let adapter = &graph.operations()[operation.index()]; + let ResourceCarrierOperationRole::AdaptSum { + source, + path, + loan: adapted, + lease_frame, + .. + } = adapter.role() + else { + panic!("normal leaf-sum loan is issued only by its carrier projection operation") + }; + assert_eq!(*adapted, Some(loan.id())); + assert_eq!(*lease_frame, loan.frame()); + assert_eq!(adapter.frame(), loan.frame()); + assert!( + matches!(path.as_slice(), [ResourceCarrierProjectionPath::Field { owner, field }] + if *owner == owner_type && field.index() == 1) + ); + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + owner_type + ); + let helper = self::function(&program, "source17_absent_optional").id; + assert!(function.operations().iter().any(|operation| matches!(operation.role(), + ResourceOperationRole::InvokeSourceFunction { function: callee, operands, .. } + if *callee == helper && matches!(operands.as_slice(), + [ResourceCallOperand::Borrowed { parameter: 0, loan: actual }] if *actual == loan.id()) + ))); + } + } +} + +#[test] +fn resource_carrier_source17_vacant_match_extracts_exact_ordinary_and_carrier_binders_on_the_selected_edge() + { + for release in [false, true] { + let (checked, raw) = consuming_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let mir = function(&program, "matching_absent_choice"); + let (header, choice_type, variant, target, bindings) = mir + .blocks + .iter() + .find_map(|block| { + let TerminatorKind::Switch { + scrutinee, + variants, + .. + } = &block.terminator.kind + else { + return None; + }; + variants + .iter() + .find(|(variant, _, _)| variant.index() == 1) + .map(|(variant, target, bindings)| { + (block.id, scrutinee.ty, *variant, *target, bindings) + }) + }) + .unwrap(); + let [marker, tokens] = bindings.as_slice() else { + panic!("vacant's two original typed binders") + }; + assert_eq!(mir.local(*marker).unwrap().ty, TypeInterner::INT64); + assert!(matches!( + types.resolve(mir.local(*tokens).unwrap().ty), + Type::List(_) + )); + let graph = carrier(named(&plan, "matching_absent_choice")); + let edge = ResourceCarrierEdge::SwitchVariant { + source: header, + variant, + target, + }; + assert!(graph.operations_on_edge(edge).any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::ExtractOrdinary { path, target: actual, ty, .. } + if *actual == *marker && *ty == TypeInterner::INT64 + && matches!(path.as_slice(), [ResourceCarrierProjectionPath::Variant { owner, variant: selected, field: 0 }] + if *owner == choice_type && *selected == variant) + ))); + assert!(graph.operations_on_edge(edge).any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::Extract { path, destination, .. } + if matches!(path.as_slice(), [ResourceCarrierProjectionPath::Variant { owner, variant: selected, field: 1 }] + if *owner == choice_type && *selected == variant) + && matches!(graph.slots()[destination.index()].storage(), + ResourceSlotStorage::Local { header } if header == mir.local(*tokens).unwrap()) + )), "vacant tokens are a typed owned carrier binder even when their selected list is empty"); + } + } +} + +#[test] +fn resource_carrier_prepared_consuming_sequence_mutations_cannot_reuse_the_original_transport_seal() +{ + for release in [false, true] { + let (checked, mut program) = consuming_program(release); + let types = &checked.checked().interner; + validate_resource_ownership(&program, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..5 { + let mut changed = program.clone(); + let function = function_mut(&mut changed, "consuming_absent_map"); + let original_source = function + .locals + .iter() + .find(|local| local.name == "values") + .unwrap() + .id; + let count = function + .locals + .iter() + .find(|local| local.name == "absent_count") + .unwrap() + .id; + let body = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Value, + .. + } + ) + }) + }) + .unwrap(); + let key_position = body + .statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Key, + .. + } + ) + }) + .unwrap(); + let value_position = body + .statements + .iter() + .position(|statement| { + matches!( + statement.kind, + StatementKind::SequenceGet { + part: SequencePart::Value, + .. + } + ) + }) + .unwrap(); + if mutation == 2 { + body.statements.swap(key_position, value_position); + } else { + let key_target = match body.statements[key_position].kind { + StatementKind::SequenceGet { target, .. } => target, + _ => unreachable!(), + }; + let StatementKind::SequenceGet { + consume, + source, + index, + target, + .. + } = &mut body.statements[value_position].kind + else { + unreachable!() + }; + match mutation { + 0 => *index = count, + 1 => *source = SequenceSource::Local(original_source), + 3 => *consume = false, + 4 => *target = key_target, + _ => unreachable!(), + } + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "prepared index/source/key-value order/consume/binder mutation {mutation} cannot issue a new carrier proof" + ); + } + } +} + +#[test] +fn resource_carrier_whole_source17_preserves_all_exported_constructor_shapes() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + crate::validate(&program).unwrap(); + let plan = validate_resource_ownership(&program, types).unwrap(); + for name in CONSTRUCTORS { + let function = named(&plan, name); + let graph = carrier(function); + let shape = result_shape(function); + assert_eq!(shape.ty(), self::function(&program, name).return_type); + assert!( + shape.contains_resource(), + "empty values retain the type of {name}" + ); + assert!( + graph.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::PublishReturn { .. } + )), + "{name} publishes an exact typed return root" + ); + } + // This unused public view formal must not disappear because the selected + // constructor examples have no occupied children. Required controls also + // remain in the source family after real required-value materialization. + function(&program, "refined_shape_contract"); + function(&program, "absent_required_controls"); + let first = carrier(named(&plan, "empty_tokens")); + let second = carrier(named(&plan, "absent_exact_state")); + for shape in first.shapes() { + let same = second.shape_for_type(shape.ty()).unwrap(); + assert_eq!(shape.id(), same.id()); + assert_eq!(shape.node(), same.node()); + assert_eq!(shape.contains_resource(), same.contains_resource()); + } + } +} + +#[test] +fn resource_carrier_source17_collection_edges_keep_resource_and_ordinary_sum_arms() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let empty = named(&plan, "empty_tokens"); + let ResourceCarrierNode::List { element } = result_shape(empty).node() else { + panic!("empty_tokens keeps list[TestHandle]"); + }; + let ResourceCarrierNode::Resource { kind } = child(carrier(empty), *element).node() else { + panic!("empty list retains the original Resource leaf kind"); + }; + assert_eq!( + Some(kind.clone()), + program.resource_manifest.kind_for_type(kind.ty()) + ); + let resource_ty = kind.ty(); + let absent = named(&plan, "absent_tokens"); + let ResourceCarrierNode::List { element } = result_shape(absent).node() else { + panic!("absent_tokens keeps list[optional[TestHandle]]"); + }; + let ResourceCarrierNode::Optional { payload } = child(carrier(absent), *element).node() + else { + panic!("two None elements retain Optional layout"); + }; + assert_eq!(child(carrier(absent), *payload).ty(), resource_ty); + let absent_map = named(&plan, "absent_map"); + let ResourceCarrierNode::Map { key, value } = result_shape(absent_map).node() else { + panic!("absent_map keeps both key and value types"); + }; + assert_eq!(child(carrier(absent_map), *key).ty(), TypeInterner::STRING); + assert!(!child(carrier(absent_map), *key).contains_resource()); + assert!(matches!( + child(carrier(absent_map), *value).node(), + ResourceCarrierNode::Optional { .. } + )); + for name in ["empty_result_tokens", "failed_result_tokens"] { + let result = named(&plan, name); + let ResourceCarrierNode::Result { success, failure } = result_shape(result).node() + else { + panic!("both selected result arms retain one result type"); + }; + assert!(matches!( + child(carrier(result), *success).node(), + ResourceCarrierNode::List { .. } + )); + assert!(child(carrier(result), *success).contains_resource()); + assert_eq!(child(carrier(result), *failure).ty(), TypeInterner::STRING); + assert!(!child(carrier(result), *failure).contains_resource()); + } + assert_eq!( + result_shape(named(&plan, "empty_result_tokens")).id(), + result_shape(named(&plan, "failed_result_tokens")).id() + ); + } +} + +#[test] +fn resource_carrier_source17_nominal_generic_inactive_and_refined_layouts_are_complete() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + let envelope = named(&plan, "absent_envelope"); + let Type::Struct(expected_owner) = types.resolve(envelope.return_type()) else { + panic!("checked TokenEnvelope nominal owner"); + }; + let ResourceCarrierNode::Struct { + owner, + arguments, + fields, + } = result_shape(envelope).node() + else { + panic!("TokenEnvelope is not erased into an ordinary record"); + }; + assert_eq!(owner, expected_owner); + assert!(arguments.is_empty()); + assert_eq!( + fields + .iter() + .map(|field| (field.ordinal, field.name.as_str())) + .collect::>(), + [(0, "tokens"), (1, "fallback"), (2, "marker")] + ); + assert!(child(carrier(envelope), fields[0].shape).contains_resource()); + assert!(child(carrier(envelope), fields[1].shape).contains_resource()); + assert_eq!( + child(carrier(envelope), fields[2].shape).ty(), + TypeInterner::INT64 + ); + let boxed = named(&plan, "absent_box"); + let ResourceCarrierNode::Struct { + arguments, fields, .. + } = result_shape(boxed).node() + else { + panic!("concrete TokenBox[TestHandle] layout"); + }; + assert_eq!(arguments.len(), 1); + assert!(matches!( + child(carrier(boxed), arguments[0]).node(), + ResourceCarrierNode::Resource { .. } + )); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].name, "items"); + assert!(matches!( + child(carrier(boxed), fields[0].shape).node(), + ResourceCarrierNode::List { .. } + )); + let choice = named(&plan, "empty_choice"); + let ResourceCarrierNode::Enum { + owner, variants, .. + } = result_shape(choice).node() + else { + panic!("empty TokenChoice retains every variant"); + }; + let Type::Enum(expected_owner) = types.resolve(choice.return_type()) else { + panic!("checked TokenChoice nominal owner"); + }; + assert_eq!(owner, expected_owner); + assert_eq!( + variants + .iter() + .map(|variant| (variant.ordinal, variant.name.as_str())) + .collect::>(), + [(0, "empty"), (1, "vacant"), (2, "occupied")] + ); + assert_eq!( + variants + .iter() + .map(|variant| variant.discriminant) + .collect::>(), + [0, 1, 2] + ); + assert!(variants[0].fields.is_empty()); + assert_eq!(variants[1].fields.len(), 2); + assert_eq!(variants[1].fields[0].name, "marker"); + assert_eq!(variants[1].fields[1].name, "tokens"); + assert!(child(carrier(choice), variants[1].fields[1].shape).contains_resource()); + assert!(matches!( + child(carrier(choice), variants[2].fields[0].shape).node(), + ResourceCarrierNode::Resource { .. } + )); + let state = named(&plan, "absent_state"); + let ResourceCarrierNode::Machine { + states, + transitions, + .. + } = result_shape(state).node() + else { + panic!("unqualified TokenState retains every state"); + }; + assert_eq!( + states + .iter() + .map(|state| (state.ordinal, state.name.as_str())) + .collect::>(), + [(0, "vacant"), (1, "waiting"), (2, "occupied")] + ); + assert_eq!(transitions, &[(0, 1), (0, 2)]); + assert_eq!(states[0].fields[0].name, "fallback"); + assert_eq!(states[0].fields[1].name, "marker"); + assert!(matches!( + child(carrier(state), states[2].fields[0].shape).node(), + ResourceCarrierNode::Resource { .. } + )); + let exact = named(&plan, "absent_exact_state"); + let ResourceCarrierNode::MachineState { + machine, + state: qualified, + } = result_shape(exact).node() + else { + panic!("TokenState at vacant retains its qualification"); + }; + assert_eq!(*qualified, 0); + assert_eq!(child(carrier(exact), *machine).ty(), state.return_type()); + assert_ne!(exact.return_type(), state.return_type()); + let refined = named(&plan, "refined_shape_contract"); + assert_eq!(refined.parameters()[0].mode, ParamMode::View); + let graph = carrier(refined); + let shape = graph.shape_for_type(refined.parameters()[0].ty).unwrap(); + let ResourceCarrierNode::Struct { fields, .. } = shape.node() else { + panic!("unused RefinedEnvelope view formal retains its original fields"); + }; + assert_eq!( + fields + .iter() + .map(|field| field.name.as_str()) + .collect::>(), + ["marker", "tokens"] + ); + let ResourceCarrierNode::Refinement { name, base } = child(graph, fields[0].shape).node() + else { + panic!("PositiveMarker is not widened to int64"); + }; + assert!(name.ends_with("PositiveMarker")); + assert_eq!(child(graph, *base).ty(), TypeInterner::INT64); + assert!(!child(graph, fields[0].shape).contains_resource()); + let predicate = child(graph, fields[0].shape) + .refinement_predicate() + .expect("the refinement graph retains its original checked predicate body"); + assert!(hir::refinement_predicate_declaration_matches( + &predicate.identity.declaration, + name + )); + assert_eq!(predicate.return_type, TypeInterner::BOOL); + assert_eq!(predicate.params.len(), 1); + assert_eq!(predicate.params[0].ty, TypeInterner::INT64); + let archived = function(&program, "refined_shape_contract") + .resource_lowering + .as_ref() + .unwrap() + .source + .execution_functions() + .iter() + .find(|function| function.id == predicate.id) + .unwrap(); + assert!(crate::breakpoint_regions::hir_blocks_equal( + &predicate.body, + &archived.body + )); + assert_eq!(predicate.locals, archived.locals); + assert!(child(graph, fields[1].shape).contains_resource()); + assert!(graph.loans().iter().any(|loan| matches!( + loan.source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!( + !graph.operations().iter().any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::BeginConstructor { .. } + )), + "an unused view formal is metadata plus a bounded lease, never a minted owner" + ); + } +} + +#[test] +fn resource_carrier_constructor_children_keep_lexical_order_and_exact_current_occurrences() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let envelope = named(&plan, "absent_envelope"); + let graph = carrier(envelope); + let destination = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::BeginConstructor { + destination, + constructor: ResourceCarrierConstructor::Struct, + } => Some(*destination), + _ => None, + }) + .unwrap(); + let children = graph + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::ConstructorChild { + destination: actual, + child, + } if *actual == destination => Some(child.index), + _ => None, + }) + .collect::>(); + assert_eq!(children, [0, 1, 2]); + let began = graph.operations().iter().position(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::BeginConstructor { destination: actual, .. } if *actual == destination + )).unwrap(); + let committed = graph.operations().iter().position(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::CommitConstructor { destination: actual } if *actual == destination + )).unwrap(); + assert!(began < committed); + let current = function(&program, "absent_envelope"); + let region = staged_constructor(current); + assert_eq!(region.evaluation_order(), [0, 1, 2]); + assert!(matches!( + region.original().kind, + hir::ExpressionKind::StructConstruct { .. } + )); + let (commit, _) = region.commit().unwrap(); + assert!(graph.operations_at(commit).any(|operation| matches!( + operation.role(), ResourceCarrierOperationRole::CommitConstructor { destination: actual } + if *actual == destination + ))); + for field in region.fields() { + assert!(graph.operations_at(field.stage()).any(|operation| matches!( + operation.role(), ResourceCarrierOperationRole::ConstructorChild { destination: actual, child } + if *actual == destination && child.index == field.field() + ))); + } + let first = ®ion.fields()[0]; + let ResourcePosition::Statement(index) = first.stage().position() else { + unreachable!() + }; + let StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value: expression, + .. + }) = ¤t.blocks[first.stage().block().index() as usize].statements[index].kind + else { + unreachable!() + }; + assert!( + graph.operations_for_expression(expression).next().is_some(), + "the exact current list field retains its constructor-issued expression occurrence" + ); + let copied = (*expression).clone(); + assert_eq!( + graph.operations_for_expression(&copied).count(), + 0, + "a copied public expression has no constructor-issued current occurrence" + ); + for operation in graph.operations() { + assert!( + envelope + .frames() + .iter() + .any(|frame| frame.id() == operation.frame()) + ); + assert_eq!(operation.site().function(), envelope.function()); + } + } +} + +fn staged_constructor(function: &Function) -> &ResourceConstructorRegion { + let ids = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Begin { constructor }) => { + Some(*constructor) + } + _ => None, + }) + .collect::>(); + let [id] = ids.as_slice() else { + panic!("one exact staged struct constructor") + }; + function.resource_constructor(*id).unwrap() +} + +#[test] +fn resource_carrier_source_call_owned_view_and_ordinary_projection_keep_original_formal_tuples() { + const SUFFIX: &str = r#" +function forward_envelope(value: TokenEnvelope) returns TokenEnvelope: + return value +function inspect_envelope(view value: TokenEnvelope) returns int64: + return value.marker +export function carrier_transport() returns int64: + TokenEnvelope initial = absent_envelope() + int64 marker = inspect_envelope(view initial) + TokenEnvelope forwarded = forward_envelope(initial) + int64 second = inspect_envelope(view forwarded) + return marker + second +"#; + for release in [false, true] { + let source = format!("{SOURCE}{SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let scenario = named(&plan, "carrier_transport"); + let owning = function(&program, "forward_envelope").id; + let viewing = function(&program, "inspect_envelope").id; + let envelope_type = function(&program, "absent_envelope").return_type; + let owning_call = scenario + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, + source, + formals, + operands, + result, + .. + } if *function == owning => Some((source, formals, operands, result)), + _ => None, + }) + .unwrap(); + assert_eq!(owning_call.0.arguments.len(), 1); + assert_eq!(owning_call.1.len(), 1); + assert_eq!(owning_call.1[0].parameter(), 0); + assert_eq!(owning_call.1[0].source_index(), 0); + assert_eq!(owning_call.1[0].actual_type(), envelope_type); + assert_eq!(owning_call.1[0].parameter_type(), envelope_type); + assert_eq!(owning_call.1[0].access(), ParamMode::Owned); + assert_eq!(owning_call.1[0].syntax(), ResourceArgumentSyntax::Bare); + assert_eq!( + owning_call.1[0].effect(), + ResourceArgumentEffect::TransferOwned + ); + assert!(matches!( + owning_call.2.as_slice(), + [ResourceCallOperand::CarrierOwned { parameter: 0, .. }] + )); + assert!(matches!(owning_call.3, ResourceCallResult::Carrier { .. })); + let view_calls = scenario + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, + source, + formals, + operands, + .. + } if *function == viewing => Some((source, formals, operands)), + _ => None, + }) + .collect::>(); + assert_eq!(view_calls.len(), 2); + for (source, formals, operands) in view_calls { + assert_eq!(source.arguments.len(), 1); + assert_eq!( + source.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!(formals.len(), 1); + assert_eq!(formals[0].parameter(), 0); + assert_eq!(formals[0].source_index(), 0); + assert_eq!(formals[0].actual_type(), envelope_type); + assert_eq!(formals[0].parameter_type(), envelope_type); + assert_eq!(formals[0].access(), ParamMode::View); + assert_eq!(formals[0].syntax(), ResourceArgumentSyntax::WrittenView); + assert_eq!(formals[0].effect(), ResourceArgumentEffect::RetainBorrow); + assert!(matches!( + operands.as_slice(), + [ResourceCallOperand::CarrierBorrowed { parameter: 0, .. }] + )); + } + let inspected = named(&plan, "inspect_envelope"); + let graph = carrier(inspected); + let owner = inspected.parameters()[0].ty; + assert!(graph.loans().iter().any(|loan| matches!( + loan.source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!(graph.operations().iter().any(|operation| matches!(operation.role(), + ResourceCarrierOperationRole::Observe { + observation: ResourceCarrierObservation::Ordinary { path, ty }, .. + } if *ty == TypeInterner::INT64 + && matches!(path.as_slice(), [ResourceCarrierProjectionPath::Field { owner: actual, field }] + if *actual == owner && field.index() == 2) + )), "marker observation retains its nominal parent and exact field ordinal"); + assert!( + carrier(named(&plan, "forward_envelope")) + .operations() + .iter() + .any(|operation| matches!( + operation.role(), + ResourceCarrierOperationRole::PublishReturn { .. } + )) + ); + } +} + +fn constructor(function: &Function, wanted: impl Fn(&Expression) -> bool) -> Option<&Expression> { + for block in &function.blocks { + for statement in &block.statements { + if let StatementKind::Let { value, .. } | StatementKind::Evaluate(value) = + &statement.kind + && let Some(value) = matching_expression(value, &wanted) + { + return Some(value); + } + } + if let TerminatorKind::Return(Some(value)) = &block.terminator.kind + && let Some(value) = matching_expression(value, &wanted) + { + return Some(value); + } + } + None +} + +fn constructor_mut( + function: &mut Function, + wanted: impl Fn(&Expression) -> bool, +) -> &mut Expression { + for block in &mut function.blocks { + for statement in &mut block.statements { + if let StatementKind::Let { value, .. } | StatementKind::Evaluate(value) = + &mut statement.kind + && let Some(value) = matching_expression_mut(value, &wanted) + { + return value; + } + } + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + && let Some(value) = matching_expression_mut(value, &wanted) + { + return value; + } + } + panic!("exact original constructor occurrence"); +} + +fn matching_expression<'a>( + value: &'a Expression, + wanted: &impl Fn(&Expression) -> bool, +) -> Option<&'a Expression> { + if wanted(value) { + return Some(value); + } + match &value.kind { + hir::ExpressionKind::View(inner) | hir::ExpressionKind::RefinementValidated(inner) => { + matching_expression(inner, wanted) + } + hir::ExpressionKind::InterfaceCoerce { value, .. } + | hir::ExpressionKind::Comptime { value, .. } => matching_expression(value, wanted), + _ => None, + } +} + +fn matching_expression_mut<'a>( + value: &'a mut Expression, + wanted: &impl Fn(&Expression) -> bool, +) -> Option<&'a mut Expression> { + if wanted(value) { + return Some(value); + } + match &mut value.kind { + hir::ExpressionKind::View(inner) | hir::ExpressionKind::RefinementValidated(inner) => { + matching_expression_mut(inner, wanted) + } + hir::ExpressionKind::InterfaceCoerce { value, .. } + | hir::ExpressionKind::Comptime { value, .. } => matching_expression_mut(value, wanted), + _ => None, + } +} + +#[test] +fn resource_carrier_public_mir_field_tag_type_return_and_local_changes_cannot_issue_transport() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..8 { + let mut changed = program.clone(); + match mutation { + 0 | 1 | 2 => { + let other_type = function(&changed, "absent_box").return_type; + let current = function_mut(&mut changed, "absent_envelope"); + let (commit, output) = staged_constructor(current).commit().unwrap(); + let output = output.id; + if mutation == 2 { + current.locals[output.index() as usize].ty = other_type; + let ResourcePosition::Statement(index) = commit.position() else { + unreachable!() + }; + assert!( + matches!(current.blocks[commit.block().index() as usize].statements[index].kind, + StatementKind::ResourceConstructor(ResourceConstructorNode::Commit { output: id, .. }) if id == output) + ); + } else { + let mut altered = 0; + for node in current + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + { + if let StatementKind::ResourceConstructor( + ResourceConstructorNode::Stage { + source_index, + field, + .. + }, + ) = &mut node.kind + { + let (selected, second) = if mutation == 0 { + (field, 1) + } else { + (source_index, 2) + }; + if *selected == 0 { + *selected = second; + altered += 1; + } else if *selected == second { + *selected = 0; + altered += 1; + } + } + } + assert_eq!(altered, 2, "exact staged field/order permutation"); + } + } + 3 => { + let value = + constructor_mut(function_mut(&mut changed, "absent_choice"), |value| { + matches!(value.kind, hir::ExpressionKind::EnumConstruct { .. }) + }); + let hir::ExpressionKind::EnumConstruct { variant, .. } = &mut value.kind else { + unreachable!() + }; + *variant = hir::VariantId::new(2); + } + 4 => { + let value = + constructor_mut(function_mut(&mut changed, "absent_state"), |value| { + matches!(value.kind, hir::ExpressionKind::MachineConstruct { .. }) + }); + let hir::ExpressionKind::MachineConstruct { state, .. } = &mut value.kind + else { + unreachable!() + }; + *state = hir::StateId::new(1); + } + 5 => { + let other_type = function(&changed, "absent_tokens").return_type; + function_mut(&mut changed, "empty_tokens").return_type = other_type; + } + 6 => { + let other_type = function(&changed, "absent_envelope").return_type; + let function = function_mut(&mut changed, "refined_shape_contract"); + let local = function.params[0].local; + function.locals[usize::try_from(local.index()).unwrap()].ty = other_type; + } + 7 => { + let other_type = function(&changed, "empty_map").return_type; + let value = + constructor_mut(function_mut(&mut changed, "absent_map"), |value| { + matches!(value.kind, hir::ExpressionKind::MapConstruct { .. }) + }); + value.ty = other_type; + } + _ => unreachable!(), + } + assert!( + validate_witnesses(&changed, types).is_err(), + "original constructor witness, mutation {mutation}" + ); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "no carrier authority, mutation {mutation}" + ); + } + } +} + +#[test] +fn resource_carrier_foreign_checked_body_and_copied_witness_cannot_replace_the_current_archive() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let foreign = checked_support( + &SOURCE.replace("marker: 17", "marker: 19"), + SUPPORT, + release, + ); + let types = &checked.checked().interner; + let program = lowered(&checked); + let foreign_program = lowered(&foreign); + validate_resource_ownership(&program, types).unwrap(); + validate_resource_ownership(&foreign_program, &foreign.checked().interner).unwrap(); + let foreign_function = function(&foreign_program, "absent_envelope"); + for mutation in 0..3 { + let mut changed = program.clone(); + let function = function_mut(&mut changed, "absent_envelope"); + match mutation { + 0 => function.blocks = foreign_function.blocks.clone(), + 1 => function.resource_lowering = foreign_function.resource_lowering.clone(), + 2 => *function = foreign_function.clone(), + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "foreign body/witness {mutation}" + ); + } + } +} + +fn copied_types(original: &TypeInterner) -> TypeInterner { + let mut types = TypeInterner::new(); + macro_rules! copy_nominals { + ($variant:ident, $resolve:ident, $add:ident) => { + let definitions = original + .type_ids() + .filter_map(|ty| match original.resolve(ty) { + Type::$variant(id) => Some((id.index(), (*id, original.$resolve(*id).clone()))), + _ => None, + }) + .collect::>(); + for (id, definition) in definitions.into_values() { + assert_eq!(types.$add(definition), id); + } + }; + } + copy_nominals!(Struct, resolve_struct, add_struct); + copy_nominals!(Bitfield, resolve_bitfield, add_bitfield); + copy_nominals!(Enum, resolve_enum, add_enum); + copy_nominals!(Interface, resolve_interface, add_interface); + copy_nominals!(Actor, resolve_actor, add_actor); + copy_nominals!(Machine, resolve_machine, add_machine); + for ty in original.type_ids() { + assert_eq!(types.intern(original.resolve(ty).clone()), ty); + } + for ty in original.type_ids() { + let arguments = original.nominal_type_arguments(ty); + if !arguments.is_empty() { + types + .register_nominal_type_arguments(ty, arguments.to_vec()) + .unwrap(); + } + } + types +} + +#[test] +fn resource_carrier_stored_nominal_definitions_cannot_erase_latent_payloads_or_refined_fields() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let original_types = &checked.checked().interner; + let hir = materialized(&checked); + let program = lower(&hir, original_types).unwrap(); + let mut types = copied_types(original_types); + hir.resource_source.validate_types(&types).unwrap(); + types.intern(Type::Optional(TypeInterner::UINT64)); + hir.resource_source.validate_types(&types).unwrap(); + let Type::Struct(envelope_id) = + types.resolve(function(&program, "absent_envelope").return_type) + else { + unreachable!() + }; + let envelope_id = *envelope_id; + let envelope = types.resolve_struct(envelope_id).clone(); + let mut erased = envelope.clone(); + erased.fields[0].1 = TypeInterner::STRING; + types.update_struct(envelope_id, erased); + assert!(hir.resource_source.validate_types(&types).is_err()); + assert!(validate_resource_ownership(&program, &types).is_err()); + types.update_struct(envelope_id, envelope); + let Type::Enum(choice_id) = types.resolve(function(&program, "empty_choice").return_type) + else { + unreachable!() + }; + let choice_id = *choice_id; + let choice = types.resolve_enum(choice_id).clone(); + let mut erased = choice.clone(); + erased.variants[2].fields.clear(); + types.update_enum(choice_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "inactive occupied variant remains archived" + ); + assert!(validate_resource_ownership(&program, &types).is_err()); + types.update_enum(choice_id, choice); + let Type::Machine(machine_id) = + types.resolve(function(&program, "absent_state").return_type) + else { + unreachable!() + }; + let machine_id = *machine_id; + let machine = types.resolve_machine(machine_id).clone(); + let mut erased = machine.clone(); + erased.states[2].fields.clear(); + types.update_machine(machine_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "inactive occupied machine state remains archived" + ); + types.update_machine(machine_id, machine); + let refined = function(&program, "refined_shape_contract"); + let Type::Struct(refined_id) = types.resolve(refined.params[0].ty) else { + unreachable!() + }; + let refined_id = *refined_id; + let mut erased = types.resolve_struct(refined_id).clone(); + erased.fields[0].1 = TypeInterner::INT64; + types.update_struct(refined_id, erased); + assert!( + hir.resource_source.validate_types(&types).is_err(), + "the unused formal does not erase PositiveMarker" + ); + assert!(validate_resource_ownership(&program, &types).is_err()); + } +} + +const MACHINE_OBSERVATION_SUFFIX: &str = r#" +function source17_machine_absent_optional(view candidate: optional[resource_probe.TestHandle]) returns bool: + resource_probe.TestHandle token = view((view candidate) handle: + return true + ) + return false +export function observe_general_machine() returns bool: + TokenState value = absent_state() + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +export function observe_exact_machine() returns bool: + TokenState at vacant value = absent_exact_state() + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +export function observe_machine_view(view value: TokenState) returns bool: + if value at vacant: + if value.marker != "empty": + return false + return source17_machine_absent_optional(view value.fallback) + return false +"#; + +fn machine_observation_program(release: bool) -> (Arc, Program) { + let source = format!("{SOURCE}{MACHINE_OBSERVATION_SUFFIX}"); + assert!(source.starts_with(SOURCE)); + let checked = checked_support(&source, SUPPORT, release); + let program = lowered(&checked); + (checked, program) +} + +#[test] +fn resource_carrier_machine_state_observation_preserves_physical_root_and_guarded_field_paths() { + for release in [false, true] { + let (checked, raw) = machine_observation_program(release); + let types = &checked.checked().interner; + for prepare in [false, true] { + let mut program = raw.clone(); + if prepare { + crate::prepare_native_sequences(&mut program, types); + } + crate::validate(&program).unwrap(); + let plan = validate_resource_ownership(&program, types).unwrap(); + for name in [ + "observe_general_machine", + "observe_exact_machine", + "observe_machine_view", + ] { + let mir = function(&program, name); + let local = mir + .locals + .iter() + .find(|local| local.name == "value") + .unwrap(); + let graph = carrier(named(&plan, name)); + let state_observations = graph + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::State { state: 0 }, + .. + } => Some((*source, operation)), + _ => None, + }) + .collect::>(); + let [(source, operation)] = state_observations.as_slice() else { + panic!("one exact vacant StateIs observation for {name}"); + }; + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + local.ty + ); + let condition = mir + .blocks + .iter() + .find_map(|block| { + let TerminatorKind::Branch { condition, .. } = &block.terminator.kind + else { + return None; + }; + matches!(condition.kind, hir::ExpressionKind::StateIs { .. }) + .then_some(condition) + }) + .unwrap(); + assert!( + operation.is_for_expression(condition), + "the row retains the StateIs occurrence" + ); + let assert_state_path = |source: super::carriers::ResourceCarrierLoanId, + path: &[ResourceCarrierProjectionPath], + field: usize| { + assert_eq!( + child(graph, graph.loans()[source.index()].shape()).ty(), + local.ty + ); + let [ + ResourceCarrierProjectionPath::State { + owner, + state, + field: actual, + }, + ] = path + else { + panic!("machine field uses its checked State path"); + }; + assert_eq!((*state, *actual), (0, field)); + let Type::MachineState { machine, state } = types.resolve(*owner) else { + panic!("field path retains its qualified nominal owner"); + }; + assert_eq!(state.index(), 0); + assert!( + matches!(types.resolve(local.ty), + Type::Machine(physical) if physical == machine + ) || matches!(types.resolve(local.ty), + Type::MachineState { machine: physical, state: selected } + if physical == machine && selected.index() == 0 + ) + ); + }; + let marker = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::Observe { + source, + observation: ResourceCarrierObservation::Ordinary { path, ty }, + .. + } if *ty == TypeInterner::STRING => Some((*source, path)), + _ => None, + }) + .unwrap(); + assert_state_path(marker.0, marker.1, 1); + let adapter = graph + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::AdaptSum { + source, + path, + loan: Some(loan), + .. + } => Some((*source, path, *loan, operation.id())), + _ => None, + }) + .unwrap(); + assert_state_path(adapter.0, adapter.1, 0); + assert!( + matches!(named(&plan, name).loans()[adapter.2.index()].source(), + ResourceLoanSource::CarrierSumProjection { operation } if operation == adapter.3 + ) + ); + if name == "observe_machine_view" { + let ResourceCarrierLoanSource::Loan(parent) = + graph.loans()[source.index()].source() + else { + panic!("view observation borrows its physical incoming loan"); + }; + assert!(matches!( + graph.loans()[parent.index()].source(), + ResourceCarrierLoanSource::IncomingViewFormal { parameter: 0, .. } + )); + assert!( + !graph.slots().iter().any(|slot| matches!(slot.storage(), + ResourceSlotStorage::Local { header } if header.id == local.id + )), + "a view formal never becomes an owned carrier slot" + ); + } + } + let mir = function(&program, "observe_general_machine"); + let mut narrowed_local_base = false; + for block in &mir.blocks { + walk::mir_block(block, &mut |value| { + if let hir::ExpressionKind::Field { + base, owner_type, .. + } = &value.kind + { + narrowed_local_base |= base.ty == *owner_type + && matches!(types.resolve(base.ty), Type::MachineState { .. }); + } + }); + } + assert!( + narrowed_local_base, + "the positive source exercises the checked branch-narrowed place" + ); + for name in CONSTRUCTORS { + function(&program, name); + } + function(&program, "refined_shape_contract"); + function(&program, "absent_required_controls"); + } + } +} + +fn first_machine_field_mut(value: &mut Expression) -> Option<&mut Expression> { + if matches!(value.kind, hir::ExpressionKind::Field { .. }) { + return Some(value); + } + match &mut value.kind { + hir::ExpressionKind::Binary { left, right, .. } => { + first_machine_field_mut(left).or_else(|| first_machine_field_mut(right)) + } + hir::ExpressionKind::Unary { value, .. } | hir::ExpressionKind::View(value) => { + first_machine_field_mut(value) + } + _ => None, + } +} + +#[test] +fn resource_carrier_machine_observation_current_guard_and_narrowed_place_mutations_refuse() { + for release in [false, true] { + let (checked, program) = machine_observation_program(release); + let types = &checked.checked().interner; + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..5 { + let mut changed = program.clone(); + let mir = function_mut(&mut changed, "observe_general_machine"); + let local = mir + .locals + .iter() + .find(|local| local.name == "value") + .unwrap() + .clone(); + if mutation < 2 { + let branch = mir + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Branch { + condition, + then_block, + else_block, + } if matches!(condition.kind, hir::ExpressionKind::StateIs { .. }) => { + Some((condition, then_block, else_block)) + } + _ => None, + }) + .unwrap(); + if mutation == 0 { + let hir::ExpressionKind::StateIs { state, .. } = &mut branch.0.kind else { + unreachable!(); + }; + *state = hir::StateId::new(1); + } else { + std::mem::swap(branch.1, branch.2); + } + } else { + let field = mir + .blocks + .iter_mut() + .find_map(|block| { + let TerminatorKind::Branch { condition, .. } = &mut block.terminator.kind + else { + return None; + }; + first_machine_field_mut(condition) + }) + .unwrap(); + let hir::ExpressionKind::Field { + base, owner_type, .. + } = &mut field.kind + else { + unreachable!(); + }; + assert!(matches!( + types.resolve(*owner_type), + Type::MachineState { .. } + )); + match mutation { + 2 => *owner_type = local.ty, + 3 => { + assert_ne!(base.ty, local.ty); + base.ty = local.ty; + } + 4 => { + let qualified = *owner_type; + mir.locals + .iter_mut() + .find(|header| header.id == local.id) + .unwrap() + .ty = qualified; + } + _ => unreachable!(), + } + } + assert!( + validate_witnesses(&changed, types).is_err(), + "machine observational mutation {mutation} cannot replace its authenticated current occurrence" + ); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "machine observational mutation {mutation} cannot issue a fresh carrier permission" + ); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/companion.rs b/crates/jett_mir/src/resource_ownership/companion.rs new file mode 100644 index 00000000..7150c70f --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/companion.rs @@ -0,0 +1,242 @@ +//! Ordinary ownership remains independent of the fresh Resource custody proof. +use super::*; +use crate::copy_values::CopyValuePlan; +use std::collections::HashSet; +#[cfg(test)] +mod tests; + +/// Holds the fresh plan for the entire lifetime of its ordinary storage proof. +#[derive(Debug)] +pub struct ResourceCompanionPlan<'a, 'p> { + ownership: &'a ResourceOwnershipPlan<'p>, + function: FunctionId, + storage: CopyValuePlan, + caller_acquisitions: crate::CallerAcquisitions<'p>, +} +impl<'a, 'p> ResourceCompanionPlan<'a, 'p> { + pub fn analyze( + ownership: &'a ResourceOwnershipPlan<'p>, + function: FunctionId, + ) -> Result { + let context = CompanionContext::new(ownership, function)?; + let storage = crate::move_values::MoveValuePlan::analyze_companion(&context)?; + let caller_acquisitions = crate::call_ownership::validate_function( + context.program, + context.function, + context.types, + )?; + Ok(Self { + ownership, + function, + storage, + caller_acquisitions, + }) + } + pub fn ownership(&self) -> &'a ResourceOwnershipPlan<'p> { + self.ownership + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn storage(&self) -> &CopyValuePlan { + &self.storage + } + /// Complete Source acquisitions borrowed from this exact immutable program. + pub fn caller_acquisitions(&self) -> &crate::CallerAcquisitions<'p> { + &self.caller_acquisitions + } +} + +pub(crate) struct CompanionContext<'p> { + pub(crate) program: &'p Program, + pub(crate) function: &'p Function, + pub(crate) types: &'p TypeInterner, + expressions: BTreeSet, + named_values: BTreeSet, + descriptor_values: BTreeSet, + carrier_expressions: BTreeSet, + carrier_types: HashSet, + named_locals: HashSet, +} +impl<'p> CompanionContext<'p> { + fn new(ownership: &ResourceOwnershipPlan<'p>, function: FunctionId) -> Result { + let plan = ownership.function(function); + let current = ownership + .program + .functions + .get(function.index() as usize) + .filter(|current| current.id == function) + .ok_or("Resource companion lost its current function")?; + if plan.is_none() + && !current + .resource_lowering + .as_ref() + .is_some_and(|parent| graph_metadata::has_records(parent, current, ownership.types)) + { + return Err("Resource companion has neither an exact fresh function plan nor protected graph storage".into()); + } + let mut expressions = BTreeSet::new(); + let mut remember = |value: &Expression| { + walk::expression(value, &mut |value| { + expressions.insert(std::ptr::from_ref(value).addr()); + }) + }; + for block in ¤t.blocks { + for statement in &block.statements { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, + .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => remember(value), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + remember(target); + remember(value); + } + StatementKind::Assert { condition, message } => { + remember(condition); + if let Some(message) = message { + remember(message); + } + } + StatementKind::CheckRefinement { call, .. } => remember(call), + StatementKind::Breakpoint { condition, .. } => { + if let Some(condition) = condition { + remember(condition); + } + } + _ => {} // These exact typed statements are checked by both CFG analyses. + } + } + match &block.terminator.kind { + TerminatorKind::Return(Some(value)) + | TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => remember(value), + _ => {} + } + } + let mut carrier_expressions = BTreeSet::new(); + if let Some(plan) = plan + && let Some(carriers) = plan.carriers() + { + for block in ¤t.blocks { + walk::mir_block(block, &mut |value| { + let direct = carriers.operations_for_expression(value).next().is_some() + || plan.operations_for_expression(value).any(|operation| { + matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { + result: ResourceCallResult::Carrier { .. }, + .. + } + ) + }); + let local = matches!(value.kind, hir::ExpressionKind::Local(id) if carriers.slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == id)) || current.is_view_local(id) && carriers.shape_for_type(value.ty).is_some_and(ResourceCarrierShape::contains_resource)); + if direct + || local + || matches!( + value.kind, + hir::ExpressionKind::View(_) + | hir::ExpressionKind::InterfaceCoerce { .. } + ) + { + carrier_expressions.insert(std::ptr::from_ref(value).addr()); + } + }); + } + } + let carrier_types = + plan.and_then(|plan| plan.carriers()) + .map_or_else(HashSet::new, |carriers| { + carriers + .shapes() + .iter() + .filter(|shape| shape.contains_resource()) + .map(ResourceCarrierShape::ty) + .collect() + }); + Ok(Self { + carrier_expressions, + carrier_types, + program: ownership.program, + function: current, + types: ownership.types, + expressions, + descriptor_values: plan.map_or_else(BTreeSet::new, |plan| { + plan.descriptor_values + .iter() + .map(|(address, _)| *address) + .collect() + }), + named_values: plan.map_or_else(BTreeSet::new, |plan| { + plan.named_callable_values + .iter() + .map(|(address, _)| *address) + .collect() + }), + named_locals: current + .resource_lowering + .as_ref() + .map_or_else(HashSet::new, |witness| { + witness + .named_callables + .iter() + .map(named_callables::NamedCallableBinding::local) + .collect() + }), + }) + } + pub(crate) fn carrier_type(&self, ty: TypeId) -> bool { + self.carrier_types.contains(&ty) && carriers::carrier_type(self.types, ty) + } + pub(crate) fn carrier_expression(&self, value: &Expression) -> bool { + self.carrier_expressions + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn contains(&self, value: &Expression) -> bool { + self.expressions.contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_type(&self, ty: TypeId) -> bool { + resource_type_pending(self.types, ty) + } + pub(crate) fn graph_return(&self) -> bool { + crate::native_graph_storage_for_return(self.function, self.types).is_some() + } + pub(crate) fn graph_local(&self, local: &Local) -> bool { + crate::native_graph_storage_for_local(self.function, self.types, local).is_some() + } + pub(crate) fn graph_expression(&self, value: &Expression) -> bool { + crate::native_graph_storage_for_expression(self.function, self.types, value).is_some() + } + pub(crate) fn descriptor_expression(&self, value: &Expression) -> bool { + !crate::native_graph_storage_for_expression(self.function, self.types, value).is_some() + && self + .descriptor_values + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_expression(&self, value: &Expression) -> bool { + crate::native_graph_storage_for_expression(self.function, self.types, value).is_none() + && self.resource_type(value.ty) + && !self + .named_values + .contains(&std::ptr::from_ref(value).addr()) + } + pub(crate) fn resource_local(&self, local: &Local) -> bool { + crate::native_graph_storage_for_local(self.function, self.types, local).is_none() + && self.resource_type(local.ty) + && !self.named_locals.contains(&local.id) + } +} diff --git a/crates/jett_mir/src/resource_ownership/companion/tests.rs b/crates/jett_mir/src/resource_ownership/companion/tests.rs new file mode 100644 index 00000000..c5a8b620 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/companion/tests.rs @@ -0,0 +1,140 @@ +use super::super::tests::{checked, lowered}; +use super::*; +const SOURCE: &str = include_str!("../fixtures/connected_entry.jett"); +#[test] +fn resource_companions_keep_ordinary_failure_storage_and_public_custody_refusals() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let types = &checked.checked().interner; + let ownership = validate_resource_ownership(&program, types).unwrap(); + for function_plan in ownership.functions() { + let current = &program.functions[function_plan.function().index() as usize]; + let companion = ResourceCompanionPlan::analyze(&ownership, current.id).unwrap(); + assert!(std::ptr::eq(companion.ownership(), &ownership)); + assert_eq!(companion.function(), current.id); + for local in ¤t.locals { + if resource_type_pending(types, local.ty) { + assert!(crate::native_graph_storage_for_local(current, types, local).is_none()); + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + } + } + if current + .locals + .iter() + .any(|local| custody_type(types, local.ty)) + { + assert!( + crate::move_values::MoveValuePlan::analyze(&program, current, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + CopyValuePlan::analyze(current, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + } + for statement in current.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::SumTake { + target, + success: false, + .. + } = statement.kind + { + assert_eq!(current.local(target).unwrap().ty, TypeInterner::STRING); + assert!( + companion + .storage() + .owned_locals + .contains(&(target.index() as usize)) + ); + } + } + } + let lifecycle = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "lifecycle" + }) + .unwrap(); + let ordinary_result = lifecycle + .locals + .iter() + .find(|local| local.name == "observed") + .unwrap(); + assert_eq!( + types.resolve(ordinary_result.ty), + &Type::Result(TypeInterner::INT64, TypeInterner::STRING) + ); + let companion = ResourceCompanionPlan::analyze(&ownership, lifecycle.id).unwrap(); + assert!( + companion + .storage() + .owned_locals + .contains(&(ordinary_result.id.index() as usize)) + ); + assert!(companion.storage().temporary_slots > 0); + } +} +#[test] +fn resource_role_projection_requires_the_exact_current_expression_occurrence() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let mut found = 0; + for function_plan in ownership.functions() { + let current = &program.functions[function_plan.function().index() as usize]; + let context = CompanionContext::new(&ownership, current.id).unwrap(); + for statement in current.blocks.iter().flat_map(|block| &block.statements) { + let value = match &statement.kind { + StatementKind::Let { value, .. } | StatementKind::Evaluate(value) => value, + _ => continue, + }; + walk::expression(value, &mut |value| { + assert!(context.contains(value)); + let copied = value.clone(); + assert!(!context.contains(&copied)); + assert_eq!(function_plan.operations_for_expression(&copied).count(), 0); + for operation in function_plan.operations_for_expression(value) { + assert_eq!(operation.site().function(), current.id); + found += usize::from(matches!( + operation.role(), + ResourceOperationRole::InvokeHook { .. } + | ResourceOperationRole::InvokeSourceFunction { .. } + )); + } + }); + } + } + assert!(found >= 3); + } +} +#[test] +fn resource_companion_function_selection_cannot_adopt_an_unrelated_ordinary_body() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let outside = program + .functions + .iter() + .find(|function| ownership.function(function.id).is_none()) + .expect("unused ordinary support function"); + assert!(!crate::native_graph_storage_has_records( + outside, + &checked.checked().interner + )); + let error = ResourceCompanionPlan::analyze(&ownership, outside.id).unwrap_err(); + assert!(error.contains("exact fresh function plan"), "{error}"); + assert!(error.contains("nor protected graph storage"), "{error}"); + } +} diff --git a/crates/jett_mir/src/resource_ownership/constructors.rs b/crates/jett_mir/src/resource_ownership/constructors.rs new file mode 100644 index 00000000..6769a706 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/constructors.rs @@ -0,0 +1,510 @@ +//! Constructor-issued struct operation regions, separate from Source call authority. +use super::*; +use jett_hir::ExpressionKind; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResourceConstructorId(usize); +impl ResourceConstructorId { + pub fn index(self) -> usize { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResourceConstructorNode { + Begin { + constructor: ResourceConstructorId, + }, + Stage { + constructor: ResourceConstructorId, + source_index: usize, + field: usize, + value: Expression, + }, + Commit { + constructor: ResourceConstructorId, + output: LocalId, + }, + End { + constructor: ResourceConstructorId, + outcome: ResourceCompletion, + }, +} +impl ResourceConstructorNode { + pub fn constructor(&self) -> ResourceConstructorId { + match self { + Self::Begin { constructor } + | Self::Stage { constructor, .. } + | Self::Commit { constructor, .. } + | Self::End { constructor, .. } => *constructor, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceOperationRegion { + Call(ResourceCallRegionId), + Constructor(ResourceConstructorId), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceConstructorField { + source_index: usize, + field: usize, + original: Expression, + endpoint: Expression, + start: ResourceSite, + stage: ResourceSite, + locals: Vec, +} +impl ResourceConstructorField { + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn field(&self) -> usize { + self.field + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn endpoint(&self) -> &Expression { + &self.endpoint + } + pub fn start(&self) -> ResourceSite { + self.start + } + pub fn stage(&self) -> ResourceSite { + self.stage + } + pub fn locals(&self) -> &[Local] { + &self.locals + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceConstructorRegion { + id: ResourceConstructorId, + original: Expression, + order: Vec, + field_types: Vec, + parent: Option, + begin: ResourceSite, + fields: Vec, + commit: Option<(ResourceSite, Local)>, + exits: Vec<(ResourceSite, ResourceCompletion)>, +} +impl ResourceConstructorRegion { + pub fn id(&self) -> ResourceConstructorId { + self.id + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn evaluation_order(&self) -> &[usize] { + &self.order + } + pub fn field_types(&self) -> &[TypeId] { + &self.field_types + } + pub(crate) fn parent(&self) -> Option { + self.parent + } + pub fn begin(&self) -> ResourceSite { + self.begin + } + pub fn fields(&self) -> &[ResourceConstructorField] { + &self.fields + } + pub fn commit(&self) -> Option<(ResourceSite, &Local)> { + self.commit.as_ref().map(|(s, l)| (*s, l)) + } + pub fn exits(&self) -> &[(ResourceSite, ResourceCompletion)] { + &self.exits + } + + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + let ExpressionKind::StructConstruct { + fields, + evaluation_order, + struct_type, + .. + } = &self.original.kind + else { + return Err("Resource constructor lost its original nominal expression".into()); + }; + if *struct_type != self.original.ty + || *evaluation_order != self.order + || fields.len() != self.field_types.len() + || fields + .iter() + .zip(&self.field_types) + .any(|(field, ty)| field.ty != *ty) + || self.fields.iter().any(|field| { + fields.get(field.field).is_none_or(|original| { + !crate::breakpoint_regions::expressions_equal(original, &field.original) + }) + }) + { + return Err("Resource constructor changed its exact original field relation".into()); + } + let node = |site: ResourceSite| -> Result<&ResourceConstructorNode, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("Resource constructor lost its exact statement site".into()); + }; + let statement = function + .blocks + .get(site.block.index() as usize) + .filter(|b| b.id == site.block) + .and_then(|b| b.statements.get(index)) + .ok_or("Resource constructor statement disappeared")?; + let StatementKind::ResourceConstructor(node) = &statement.kind else { + return Err("Resource constructor statement changed its node family".into()); + }; + if site.function != function.id || node.constructor() != self.id { + return Err("Resource constructor has a foreign occurrence ID".into()); + } + Ok(node) + }; + if !matches!(node(self.begin)?, ResourceConstructorNode::Begin { .. }) + || self.fields.len() != self.order.len() + { + return Err("Resource constructor lost its complete original field tuple".into()); + } + for (source_index, field) in self.fields.iter().enumerate() { + if field.source_index != source_index + || self.order.get(source_index) != Some(&field.field) + || self.field_types.get(field.field) != Some(&field.endpoint.ty) + || !matches!(node(field.stage)?, ResourceConstructorNode::Stage { source_index:i, field:f, value, .. } + if *i == source_index && *f == field.field && crate::breakpoint_regions::expressions_equal(value, &field.endpoint)) + || field.locals.iter().any(|l| function.local(l.id) != Some(l)) + { + return Err( + "Resource constructor changed an exact field endpoint or generated header" + .into(), + ); + } + } + let (site, output) = self + .commit + .as_ref() + .ok_or("Resource constructor has no exact Commit")?; + if function.local(output.id) != Some(output) + || output.ty != self.original.ty + || !output.mutable + || output.view_source.is_some() + || !matches!(node(*site)?, ResourceConstructorNode::Commit { output:id, .. } if *id == output.id) + { + return Err("Resource constructor Commit changed its owned output header".into()); + } + for (site, outcome) in &self.exits { + if !matches!(node(*site)?, ResourceConstructorNode::End { outcome:found, .. } if found == outcome) + { + return Err("Resource constructor lost its exact completion occurrence".into()); + } + } + if self + .exits + .iter() + .filter(|(_, o)| *o == ResourceCompletion::Normal) + .count() + != 1 + || function + .blocks + .iter() + .flat_map(|b| &b.statements) + .filter(|s| { + matches!(&s.kind, + StatementKind::ResourceConstructor(n) if n.constructor() == self.id) + }) + .count() + != self.fields.len() + self.exits.len() + 2 + { + return Err("Resource constructor has missing or copied operation nodes".into()); + } + Ok(()) + } + + pub(super) fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |site: &mut ResourceSite| -> Result<(), String> { + site.block = map + .get(site.block.index() as usize) + .copied() + .flatten() + .ok_or("canonical selection removed a sealed Resource constructor endpoint")?; + Ok(()) + }; + remap(&mut self.begin)?; + for field in &mut self.fields { + remap(&mut field.start)?; + remap(&mut field.stage)?; + } + if let Some((site, _)) = &mut self.commit { + remap(site)?; + } + for (site, _) in &mut self.exits { + remap(site)?; + } + Ok(()) + } + + pub(super) fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for field in &mut self.fields { + for header in &mut field.locals { + remap(&mut header.id); + if let Some(s) = &mut header.view_source { + remap(s); + } + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(field.endpoint.clone()), + span: field.endpoint.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: field.endpoint.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(value) = wrapper.statements.remove(0).kind { + field.endpoint = value; + } + } + if let Some((_, output)) = &mut self.commit { + remap(&mut output.id); + } + } + + pub(super) fn shift_prefix(&mut self, block: BlockId, count: usize) { + let shift = |site: &mut ResourceSite| { + if site.block == block + && let ResourcePosition::Statement(i) = &mut site.position + { + *i += count; + } + }; + shift(&mut self.begin); + for f in &mut self.fields { + shift(&mut f.start); + shift(&mut f.stage); + } + if let Some((site, _)) = &mut self.commit { + shift(site); + } + for (site, _) in &mut self.exits { + shift(site); + } + } +} + +pub(super) fn records_equal( + a: &[ResourceConstructorRegion], + b: &[ResourceConstructorRegion], +) -> bool { + a.len() == b.len() + && a.iter().zip(b).all(|(a, b)| { + a.id == b.id + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && a.order == b.order + && a.field_types == b.field_types + && a.parent == b.parent + && a.begin == b.begin + && a.commit == b.commit + && a.exits == b.exits + && a.fields.len() == b.fields.len() + && a.fields.iter().zip(&b.fields).all(|(a, b)| { + a.source_index == b.source_index + && a.field == b.field + && a.start == b.start + && a.stage == b.stage + && a.locals == b.locals + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && crate::breakpoint_regions::expressions_equal(&a.endpoint, &b.endpoint) + }) + }) +} + +impl Capture { + pub(crate) fn constructor_needed(&self, value: &Expression, types: &TypeInterner) -> bool { + self.witness.is_some() + && matches!(&value.kind, ExpressionKind::StructConstruct { fields, .. } + if fields.iter().any(|v| resource_type_pending(types, v.ty))) + } + + pub(crate) fn begin_constructor_region( + &mut self, + original: &Expression, + parent: Option, + begin: ResourceSite, + types: &TypeInterner, + ) -> Result { + let w = self + .witness + .as_mut() + .ok_or("Resource constructor lacks checked Source authentication")?; + w.source.validate_types(types)?; + let ExpressionKind::StructConstruct { + struct_type, + fields, + evaluation_order, + validates_refinements: false, + refinement_predicates, + .. + } = &original.kind + else { + return Err( + "occupied refined constructor needs its exact checked predicate transition".into(), + ); + }; + if !refinement_predicates.is_empty() || original.ty != *struct_type { + return Err("Resource constructor changed its exact nominal type".into()); + } + let Type::Struct(id) = types.resolve(*struct_type) else { + return Err("Resource constructor lacks exact struct header".into()); + }; + let field_types = types + .resolve_struct(*id) + .fields + .iter() + .map(|(_, t)| *t) + .collect::>(); + let mut occurrences = 0; + walk::hir_block(&w.original.body, &mut |v| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal(v, original)) + }); + if occurrences != 1 + || fields.len() != field_types.len() + || fields.iter().zip(&field_types).any(|(f, t)| f.ty != *t) + || evaluation_order.len() != fields.len() + || evaluation_order.iter().copied().collect::>() + != (0..fields.len()).collect() + || w.constructors + .iter() + .any(|r| crate::breakpoint_regions::expressions_equal(&r.original, original)) + { + return Err( + "Resource constructor differs from its unique complete original checked struct" + .into(), + ); + } + let id = ResourceConstructorId(w.constructors.len()); + if matches!(parent, Some(ResourceOperationRegion::Constructor(p)) if p.index() >= id.index()) + { + return Err("Resource constructor has no private parent region".into()); + } + w.constructors.push(ResourceConstructorRegion { + id, + original: original.clone(), + order: evaluation_order.clone(), + field_types, + parent, + begin, + fields: vec![], + commit: None, + exits: vec![], + }); + Ok(id) + } + + pub(crate) fn constructor_field( + &mut self, + id: ResourceConstructorId, + source_index: usize, + field: usize, + original: &Expression, + endpoint: &Expression, + start: ResourceSite, + stage: ResourceSite, + locals: &[Local], + ) -> Result<(), String> { + let r = self + .witness + .as_mut() + .and_then(|w| w.constructors.get_mut(id.index())) + .ok_or("Resource field lost its private constructor")?; + if r.fields.len() != source_index + || r.order.get(source_index) != Some(&field) + || r.field_types.get(field) != Some(&endpoint.ty) + { + return Err("Resource field changed its original lexical occurrence/type".into()); + } + r.fields.push(ResourceConstructorField { + source_index, + field, + original: original.clone(), + endpoint: endpoint.clone(), + start, + stage, + locals: locals.to_vec(), + }); + Ok(()) + } + + pub(crate) fn constructor_node( + &mut self, + site: ResourceSite, + node: &ResourceConstructorNode, + locals: &[Local], + ) { + let Some(r) = self + .witness + .as_mut() + .and_then(|w| w.constructors.get_mut(node.constructor().index())) + else { + return; + }; + match node { + ResourceConstructorNode::Commit { output, .. } => { + if let Some(h) = locals + .get(output.index() as usize) + .filter(|h| h.id == *output) + { + r.commit = Some((site, h.clone())); + } + } + ResourceConstructorNode::End { outcome, .. } => r.exits.push((site, *outcome)), + _ => {} + } + } +} + +impl Function { + pub fn resource_constructor( + &self, + id: ResourceConstructorId, + ) -> Result<&ResourceConstructorRegion, String> { + let w = self + .resource_lowering + .as_ref() + .ok_or("Resource constructor lost original checked witness")?; + w.current(self)?; + w.constructors + .get(id.index()) + .filter(|r| r.id == id) + .ok_or_else(|| "Resource constructor has a foreign private occurrence".into()) + } +} + +pub(crate) fn validate_nodes(function: &Function) -> Result<(), String> { + let has_nodes = function.blocks.iter().any(|block| { + block + .statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::ResourceConstructor(_))) + }); + let has_inventory = function.resource_lowering.as_ref().is_some_and(|witness| { + !witness.constructors.is_empty() || !witness.constructor_seal.is_empty() + }); + if !has_nodes && !has_inventory { + return Ok(()); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource constructor canonical preparation lost its original checked witness")? + .current(function) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/constructors/tests.rs b/crates/jett_mir/src/resource_ownership/constructors/tests.rs new file mode 100644 index 00000000..5aa43863 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/constructors/tests.rs @@ -0,0 +1,564 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCES: [&str; 3] = [ + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/33_owned_struct_scope_drop_entry.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/34_owned_struct_reverse_entry.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/35_owned_struct_abandoned_prefix_entry.jett" + ), +]; + +// Historical originals retain their exact invalid viewed-entry Source bytes. +const INVALID_VIEWED_ENTRY_SOURCES: [&str; 3] = [ + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/30_owned_struct_scope_drop.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/31_owned_struct_reverse.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/32_owned_struct_abandoned_prefix.jett" + ), +]; + +#[test] +fn resource_occupied_struct_historical_viewed_entries_keep_exact_e0503_refusal() { + for release in [false, true] { + for source in INVALID_VIEWED_ENTRY_SOURCES { + let stdlib = jett_common::FileId::new(10_000); + let project = jett_common::FileId::new(0); + let mut parsed = jett_parser::parse(crate::resource_ownership::tests::SUPPORT, stdlib); + let primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty()); + assert!(primary.errors.is_empty()); + let declarations = parsed + .module + .items + .iter() + .filter_map(|item| match item { + jett_parser::ast::Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .collect::>(); + let [declaration] = declarations.as_slice() else { + panic!("historical Source must use the original sole checked support Resource"); + }; + let catalog = [ + ( + "kernel_create", + jett_types::ResourceKernelRecipe::NetworkFactory, + ), + ( + "kernel_borrow", + jett_types::ResourceKernelRecipe::NetworkBorrow, + ), + ("kernel_close", jett_types::ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| jett_resolve::ResourceKernelSpec { + resource_declaration: *declaration, + member: member.to_owned(), + recipe, + }) + .collect::>(); + parsed.module.items.extend(primary.module.items); + let checked = jett_typecheck::CheckedResourceProgram::prepare( + parsed, + std::collections::HashMap::from([ + (stdlib, jett_common::SourceOrigin::Stdlib), + (project, jett_common::SourceOrigin::Project), + ]), + &catalog, + jett_typecheck::CheckOptions { release }, + ); + let error = match checked { + Err(error) => error, + Ok(_) => panic!("historical viewed-main Source was admitted (release={release})"), + }; + let diagnostics = error + .diagnostics() + .expect("historical E0503 checker diagnostics"); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 503 + && diagnostic.span.file == project + && diagnostic.message + == "`main` must own runtime capability parameter `net`; remove `view`"), + "historical viewed-main Source lost exact E0503 (release={release}): {:?}", + diagnostics + .iter() + .map(|diagnostic| ( + diagnostic.code.code(), + diagnostic.message.as_str(), + diagnostic.span + )) + .collect::>() + ); + } + } +} + +fn helper(program: &Program) -> &Function { + program + .functions + .iter() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap() +} +fn helper_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|f| { + f.identity.declaration.namespace == "app" + && matches!( + f.identity.declaration.name.as_str(), + "retire_owned" | "retire_pair" + ) + }) + .unwrap() +} +fn reseal_general(function: &mut Function) { + let w = function.resource_lowering.as_mut().unwrap(); + w.blocks = function.blocks.clone(); + w.locals = function.locals.clone(); + w.parameters = function.params.clone(); +} +fn refused(program: &Program, types: &TypeInterner) { + assert!( + validate_resource_ownership(program, types).is_err(), + "forged constructor ownership was admitted" + ); + let mut selected = helper(program).clone(); + let before = selected.clone(); + assert!( + !crate::sequences::prune::unreachable(&mut selected), + "forged constructor prune accepted; private witness present={}", + before.resource_lowering.is_some() + ); + assert!( + crate::breakpoint_regions::blocks_equal(&selected.blocks, &before.blocks), + "refused prune must preserve public graph" + ); + let mut selected = before.clone(); + assert!(!crate::sequences::prune::unused_locals(&mut selected)); + assert_eq!(selected.locals, before.locals); + assert!(crate::breakpoint_regions::blocks_equal( + &selected.blocks, + &before.blocks + )); +} + +#[test] +fn resource_occupied_struct_constructor_preserves_owned_formals_lexical_order_and_operation_output() +{ + for release in [false, true] { + for (index, source) in SOURCES.into_iter().enumerate() { + let checked = checked(source, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for canonical in [false, true] { + if canonical { + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + } + crate::validate(&program).unwrap(); + validate_call_ownership(&program, types).unwrap(); + let function = helper(&program); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.constructors.len(), 1); + let region = function + .resource_constructor(witness.constructors[0].id()) + .unwrap(); + assert_eq!( + region.evaluation_order(), + if index == 1 { + &[1, 0, 2][..] + } else { + &[0, 1][..] + } + ); + let plan = validate_resource_ownership(&program, types).unwrap(); + let selected = plan.function(function.id).unwrap(); + let carriers = selected.carriers().unwrap(); + let begin = carriers + .operations() + .iter() + .find(|operation| { + operation.site() == region.begin() + && matches!( + operation.role(), + ResourceCarrierOperationRole::BeginConstructor { .. } + ) + }) + .unwrap(); + let ResourceCarrierOperationRole::BeginConstructor { destination, .. } = + begin.role() + else { + unreachable!() + }; + let frame = begin.frame(); + assert_eq!( + selected.frames()[frame.index()].role(), + ResourceFrameRole::Operation + ); + assert_eq!( + selected.frames()[frame.index()].parent(), + Some(selected.root_scope().id()) + ); + for (parameter, param) in function.params.iter().enumerate() { + assert_eq!(param.mode, ParamMode::Owned); + let slot = selected.owner_slots().iter().find(|slot| matches!(slot.storage(),ResourceSlotStorage::Local { header } if header.id == param.local)).unwrap(); + assert!(matches!(slot.shape(), ResourceShape::Plain { .. })); + assert!(carriers.operations().iter().any(|operation| matches!(operation.role(),ResourceCarrierOperationRole::ConstructorChild { child, .. } if child.value == ResourceCarrierValue::LeafOwned { slot:slot.id() })) ,"formal {parameter} must be adopted through its exact plain holder"); + } + let commit = carriers.operations().iter().find(|operation| matches!(operation.role(),ResourceCarrierOperationRole::CommitConstructor { destination:found } if found == destination)).unwrap(); + let (_, output) = region.commit().unwrap(); + assert!( + output.mutable, + "constructor output keeps its exact generated temporary header" + ); + let output_slot = carriers.slots().iter().find(|slot| matches!(slot.storage(),ResourceSlotStorage::Local { header } if header.id == output.id)).unwrap(); + assert_eq!(output_slot.frame(), selected.root_scope().id()); + let transfer = carriers.operations().iter().find(|operation| operation.site() == commit.site() && matches!(operation.role(),ResourceCarrierOperationRole::Transfer { source,destination:to } if source == destination && *to == output_slot.id())).unwrap(); + assert!(transfer.id().index() > commit.id().index()); + assert_eq!(transfer.frame(), frame); + for (site, outcome) in region.exits() { + assert!(selected.operations().iter().any(|operation| operation.site() == *site && operation.frame() == frame && matches!(operation.role(),ResourceOperationRole::Complete { outcome:found } if found == outcome))); + } + if index == 2 { + assert_eq!( + region + .exits() + .iter() + .filter(|(_, o)| *o == ResourceCompletion::Abort) + .count(), + 1 + ); + assert_ne!( + region.fields()[0].stage().block(), + region.fields()[1].stage().block() + ); + for field in region.fields() { + assert_eq!( + selected + .execution_frame(field.stage().block(), field.stage().position()), + Some(frame) + ); + } + } + assert!( + crate::copy_values::CopyValuePlan::analyze_storage( + function, + types, + Some(&program) + ) + .is_err() + ); + assert!( + crate::move_values::MoveValuePlan::analyze(&program, function, types).is_err() + ); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + } + } + } +} + +#[test] +fn occupied_struct_primitive_companions_mir_preserves_each_exact_original_endpoint() { + let sources = [ + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/36_owned_struct_primitive_companions.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/37_owned_struct_alias_companions.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/40_owned_struct_pending_companions.jett" + ), + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/42_owned_struct_pending_primitive_matrix.jett" + ), + ]; + for release in [false, true] { + for source in sources { + let support = format!( + "{}\n{}", + crate::resource_ownership::tests::SUPPORT, + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett" + ) + ); + let checked = + crate::resource_ownership::tests::checked_support(source, &support, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for canonical in [false, true] { + if canonical { + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name.starts_with("retire_") + }) + .unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.constructors.len(), 1); + let region = function + .resource_constructor(witness.constructors[0].id()) + .unwrap(); + let selected = plan.function(function.id).unwrap(); + let carriers = selected.carriers().unwrap(); + for field in region.fields() { + let child = + carriers + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceCarrierOperationRole::ConstructorChild { + child, .. + } if operation.site() == field.stage() => Some(child), + _ => None, + }) + .unwrap(); + assert_eq!(child.index, field.field()); + if !resource_type_pending(types, field.original().ty) { + assert!( + matches!(child.value, ResourceCarrierValue::Ordinary { ty } if ty == field.original().ty) + ); + } + } + let mut forged = program.clone(); + let function = forged + .functions + .iter_mut() + .find(|current| current.id == function.id) + .unwrap(); + let stage = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, + .. + }) if !resource_type_pending(types, value.ty) => Some(value), + _ => None, + }) + .unwrap(); + stage.ty = if stage.ty == TypeInterner::INT8 { + TypeInterner::INT16 + } else { + TypeInterner::INT8 + }; + assert!( + validate_resource_ownership(&forged, types).is_err(), + "changing width must not reseal an original constructor" + ); + } + } + } +} + +#[test] +fn occupied_struct_pending_qualifier_keeps_exact_observation_before_and_after_canonicalization() { + const SOURCE: &str = include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/38_owned_struct_pending_int64_transport.jett" + ); + let support = format!( + "{}\n{}", + crate::resource_ownership::tests::SUPPORT, + include_str!( + "../../../../jett_comptime/src/resource_execution/fixtures/resource_companion_probe.jett" + ) + ); + for release in [false, true] { + let checked = crate::resource_ownership::tests::checked_support(SOURCE, &support, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for canonical in [false, true] { + if canonical { + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + } + let plan = validate_resource_ownership(&program, types).unwrap(); + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "retire_pending" + }) + .unwrap(); + let observations = plan.function(function.id).unwrap().carriers().unwrap().operations().iter().filter(|operation| matches!(operation.role(), ResourceCarrierOperationRole::Observe { observation: ResourceCarrierObservation::Ordinary { ty, .. }, .. } if *ty == TypeInterner::INT64)).count(); + assert_eq!( + observations, 1, + "the nested Join keeps one exact typed field observation" + ); + let mut forged = program.clone(); + let changed = forged + .functions + .iter_mut() + .find(|candidate| candidate.id == function.id) + .unwrap() + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + let StatementKind::Let { value, .. } = &mut statement.kind else { + return None; + }; + let hir::ExpressionKind::Join(inner) = &mut value.kind else { + return None; + }; + matches!(inner.kind, hir::ExpressionKind::Field { .. }).then_some(inner) + }) + .unwrap(); + changed.ty = TypeInterner::INT8; + assert!( + validate_resource_ownership(&forged, types).is_err(), + "nested qualifier cannot redirect the original field width" + ); + } + } +} + +#[test] +fn resource_occupied_struct_constructor_refuses_changed_fields_copied_nodes_and_lost_proof_before_prune() + { + for release in [false, true] { + let checked = checked(SOURCES[1], release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..8 { + let mut forged = program.clone(); + let function = helper_mut(&mut forged); + let region = function.resource_lowering.as_ref().unwrap().constructors[0].clone(); + match mutation { + 0 => { + let site = region.fields()[0].stage(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + let StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + field, + .. + }) = &mut function.blocks[site.block().index() as usize].statements[i].kind + else { + unreachable!() + }; + *field = 0; + } + 1 => { + let site = region.begin(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + let copy = function.blocks[site.block().index() as usize].statements[i].clone(); + function.blocks[site.block().index() as usize] + .statements + .insert(i + 1, copy); + } + 2 => { + function.resource_lowering.as_mut().unwrap().constructors[0].fields[0] + .original = region.fields[1].original.clone(); + } + 3 => { + function.resource_lowering.as_mut().unwrap().constructors[0].fields[0].start = + region.fields[1].start; + } + 4 => { + function.resource_lowering.as_mut().unwrap().constructors[0] + .order + .swap(0, 1); + } + 5 => { + let (_, output) = region.commit().unwrap(); + function.locals[output.id.index() as usize].view_source = + Some(function.params[0].local); + } + 6 => { + function.resource_lowering = None; + refused(&forged, types); + continue; + } + 7 => { + let (_, output) = region.commit().unwrap(); + assert!(function.locals[output.id.index() as usize].mutable); + function.locals[output.id.index() as usize].mutable = false; + } + _ => unreachable!(), + } + reseal_general(function); + refused(&forged, types); + } + } +} + +#[test] +fn resource_occupied_struct_constructor_disconnected_stage_and_wrong_prefix_remain_refused() { + let checked = checked(SOURCES[2], false); + let types = &checked.checked().interner; + let program = lowered(&checked); + for disconnect in [false, true] { + let mut forged = program.clone(); + let function = helper_mut(&mut forged); + let region = function.resource_lowering.as_ref().unwrap().constructors[0].clone(); + let site = region.fields()[0].stage(); + let ResourcePosition::Statement(i) = site.position() else { + unreachable!() + }; + if disconnect { + let id = BlockId(u32::try_from(function.blocks.len()).unwrap()); + function.blocks.push(BasicBlock { + id, + statements: vec![ + function.blocks[site.block().index() as usize].statements[i].clone(), + ], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } else { + let end = region + .exits() + .iter() + .find(|(_, o)| *o == ResourceCompletion::Abort) + .unwrap() + .0; + let ResourcePosition::Statement(i) = end.position() else { + unreachable!() + }; + function.blocks[end.block().index() as usize] + .statements + .remove(i); + } + reseal_general(function); + refused(&forged, types); + } +} diff --git a/crates/jett_mir/src/resource_ownership/control_flow.rs b/crates/jett_mir/src/resource_ownership/control_flow.rs new file mode 100644 index 00000000..dcd8fea6 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/control_flow.rs @@ -0,0 +1,681 @@ +//! Control-flow transport preserves the independently proven custody state. +//! Sequence preparation may only replace an exact original ForEach by its canonical +//! consuming-list or map edit. The carrier planner owns custody and retirement. +use super::*; +use hir::{BinaryOp, ExpressionKind as E}; + +// Erased storage is not evidence of absent custody, even if its nominal +// signature mentions no Resource. This is a bounded compiler proof, not a +// restriction on valid Source programs. +fn resource_free(types: &TypeInterner, ty: TypeId, seen: &mut BTreeSet) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + if !seen.insert(ty.index()) { + return true; + } + match types.resolve(ty) { + Type::Resource(_) + | Type::Interface(_) + | Type::Actor(_) + | Type::Function { .. } + | Type::Capability(_) + | Type::TypeConstruction + | Type::Error => false, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => resource_free(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + resource_free(types, *key, seen) && resource_free(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| resource_free(types, field.ty, seen)), + Type::Enum(id) => types.resolve_enum(*id).variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)) + }), + Type::Machine(id) | Type::MachineState { machine: id, .. } => { + types.resolve_machine(*id).states.iter().all(|state| { + state + .fields + .iter() + .all(|(_, ty)| resource_free(types, *ty, seen)) + }) + } + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Never => true, + } +} +fn ordinary_type(types: &TypeInterner, ty: TypeId) -> Result<(), String> { + if !resource_free(types, ty, &mut BTreeSet::new()) { + return Err("pending ResourceOwnershipPlan: control-flow value needs its dedicated Resource carrier transport".into()); + } + Ok(()) +} +fn header<'a>( + function: &'a Function, + types: &TypeInterner, + local: LocalId, +) -> Result<&'a Local, String> { + let header = function + .local(local) + .ok_or("Resource control-flow binder lost its dense local header")?; + ordinary_type(types, header.ty)?; + Ok(header) +} +pub(super) fn consuming_list( + function: &Function, + types: &TypeInterner, + key: LocalId, + value: Option, + by_view: bool, + iterable: &Expression, +) -> Result { + let graph = crate::native_graph_storage_for_expression(function, types, iterable).is_some(); + if !graph { + ordinary_type(types, iterable.ty)?; + } + let Type::List(element) = types.resolve(iterable.ty) else { + return Err("pending ResourceOwnershipPlan: this iterator needs its dedicated ordinary sequence normalization".into()); + }; + if !graph { + ordinary_type(types, *element)?; + } + if by_view || value.is_some() || *element == TypeInterner::NEVER { + return Err("pending ResourceOwnershipPlan: borrowed or uninhabited iteration needs its exact sequence transport".into()); + } + let binding = function + .local(key) + .ok_or("Resource iterator lost its exact binder")?; + if graph { + if crate::native_graph_storage_for_local(function, types, binding).is_none() { + return Err("graph iterator lost its exact protected element binder".into()); + } + } else { + ordinary_type(types, binding.ty)?; + } + if binding.ty != *element || binding.view_source.is_some() { + return Err("Resource consuming iterator changed its exact ordinary element binder".into()); + } + Ok(*element) +} +fn carrier_binding( + function: &Function, + types: &TypeInterner, + local: LocalId, + expected: TypeId, +) -> Result<(), String> { + if expected.index() as usize >= types.len() || expected == TypeInterner::NEVER { + return Err("pending ResourceOwnershipPlan: uninhabited carrier iteration needs its exact sequence transport".into()); + } + let binding = function + .local(local) + .ok_or("Resource carrier iterator lost its dense binder header")?; + if binding.ty != expected + || binding.debug_ty != expected + || binding.mutable + || binding.view_source.is_some() + || function.is_view_local(local) + || function.parameter_for_local(local).is_some() + { + return Err("Resource carrier iterator changed its exact owning binder declaration".into()); + } + Ok(()) +} +/// Ordinary metadata iteration requires both the protected whole collection +/// occurrence and each nonordinary element binder. It grants no carrier custody. +pub(super) fn graph_sequence( + function: &Function, + types: &TypeInterner, + key: LocalId, + value: Option, + by_view: bool, + iterable: &Expression, +) -> Result<(), String> { + if crate::native_graph_storage_for_expression(function, types, iterable).is_none() + || by_view + || matches!(&iterable.kind, E::View(_) | E::Field { .. }) + || matches!(&iterable.kind, E::Local(local) if function.is_view_local(*local)) + { + return Err("graph iterator lost its exact protected owning collection".into()); + } + let bindings = match types.resolve(iterable.ty) { + Type::List(element) if value.is_none() => vec![(key, *element)], + Type::Map(key_ty, value_ty) => vec![ + (key, *key_ty), + ( + value.ok_or("graph map iterator needs both exact binders")?, + *value_ty, + ), + ], + _ => return Err("graph iterator needs its exact list/map transport".into()), + }; + for (id, ty) in bindings { + carrier_binding(function, types, id, ty)?; + if ordinary_type(types, ty).is_err() + && function.local(id).is_none_or(|header| { + crate::native_graph_storage_for_local(function, types, header).is_none() + }) + { + return Err("graph iterator lost its exact protected component binder".into()); + } + } + Ok(()) +} + +/// Validate only the exact typed consuming iterator. This creates no carrier, +/// owner, lease or absence proof; the independent carrier planner supplies them. +pub(super) fn carrier_sequence( + function: &Function, + types: &TypeInterner, + key: LocalId, + value: Option, + by_view: bool, + iterable: &Expression, +) -> Result<(), String> { + function + .resource_lowering + .as_ref() + .ok_or("Resource carrier iterator has no authenticated Source witness")? + .current(function)?; + let occurrences = function + .blocks + .iter() + .filter(|block| { + matches!(&block.terminator.kind, TerminatorKind::ForEach { + key: current_key, + value: current_value, + by_view: current_view, + iterable: current_iterable, + .. + } if *current_key == key + && *current_value == value + && *current_view == by_view + && crate::breakpoint_regions::expressions_equal(current_iterable, iterable)) + }) + .count(); + if occurrences != 1 { + return Err("Resource carrier iterator lost its unique exact current ForEach".into()); + } + if iterable.ty.index() as usize >= types.len() { + return Err("Resource carrier iterator lost its exact iterable type".into()); + } + if by_view + || matches!(&iterable.kind, E::View(_) | E::Field { .. }) + || matches!(&iterable.kind, E::Local(local) if function.is_view_local(*local)) + { + return Err("pending ResourceOwnershipPlan: borrowed carrier iteration needs its exact sequence transport".into()); + } + match types.resolve(iterable.ty) { + Type::List(element) => { + if value.is_some() { + return Err("Resource carrier list changed its sole element binder".into()); + } + carrier_binding(function, types, key, *element) + } + Type::Map(key_type, value_type) => { + let value = value.ok_or("pending ResourceOwnershipPlan: key-only carrier map iteration needs its exact residual-value transport")?; + if key == value { + return Err("Resource carrier map reused its key and value binder".into()); + } + // Checked map keys have no Resource custody; their ordinary storage + // remains separate from the value carrier extracted next. + ordinary_type(types, *key_type)?; + carrier_binding(function, types, key, *key_type)?; + carrier_binding(function, types, value, *value_type) + } + _ => Err("pending ResourceOwnershipPlan: this carrier iterator needs its dedicated sequence normalization".into()), + } +} +pub(super) fn ordinary_switch( + function: &Function, + types: &TypeInterner, + scrutinee: &Expression, + variants: &[(VariantId, BlockId, Vec)], + otherwise: Option, +) -> Result<(), String> { + ordinary_type(types, scrutinee.ty)?; + let Type::Enum(id) = types.resolve(scrutinee.ty) else { + return Err("Resource switch requires its exact ordinary enum discriminant".into()); + }; + let definition = types.resolve_enum(*id); + let mut seen = BTreeSet::new(); + for (variant, _, bindings) in variants { + let index = variant.index() as usize; + let fields = &definition + .variants + .get(index) + .ok_or("Resource switch selected a foreign enum variant")? + .fields; + if !seen.insert(index) || fields.len() != bindings.len() { + return Err("Resource switch changed its distinct ordinary variant bindings".into()); + } + for ((_, expected), binding) in fields.iter().zip(bindings) { + ordinary_type(types, *expected)?; + if header(function, types, *binding)?.ty != *expected { + return Err("Resource switch changed its exact ordinary payload type".into()); + } + } + } + if otherwise.is_none() && seen.len() != definition.variants.len() { + return Err("Resource switch lost its checked exhaustive variant edges".into()); + } + Ok(()) +} +fn sequence_element( + function: &Function, + types: &TypeInterner, + source: &SequenceSource, +) -> Result { + let SequenceSource::Local(local) = source else { + return Err( + "pending ResourceOwnershipPlan: projected sequence needs its exact aggregate transport" + .into(), + ); + }; + let source = function + .local(*local) + .ok_or("Resource sequence source header is missing")?; + let graph = crate::native_graph_storage_for_local(function, types, source).is_some(); + if !graph { + ordinary_type(types, source.ty)?; + } + let Type::List(element) = types.resolve(source.ty) else { + return Err("Resource sequence statement changed its exact ordinary list source".into()); + }; + if !graph { + ordinary_type(types, *element)?; + } + Ok(*element) +} +fn graph_sequence_part( + function: &Function, + types: &TypeInterner, + source: &SequenceSource, + part: Option, +) -> Option { + let SequenceSource::Local(id) = source else { + return None; + }; + let header = function.local(*id)?; + crate::native_graph_storage_for_local(function, types, header)?; + match (types.resolve(header.ty), part) { + (Type::List(element), None | Some(SequencePart::Element)) => Some(*element), + (Type::Map(key, _), None | Some(SequencePart::Key)) => Some(*key), + (Type::Map(_, value), Some(SequencePart::Value)) => Some(*value), + _ => None, + } +} +pub(super) fn sequence_length( + function: &Function, + types: &TypeInterner, + source: &SequenceSource, + target: LocalId, +) -> Result<(), String> { + if graph_sequence_part(function, types, source, None).is_none() { + sequence_element(function, types, source)?; + } + if header(function, types, target)?.ty != TypeInterner::INT64 { + return Err("Resource sequence length changed its ordinary integer destination".into()); + } + Ok(()) +} +pub(super) fn sequence_get( + function: &Function, + types: &TypeInterner, + consume: bool, + source: &SequenceSource, + index: LocalId, + target: LocalId, + part: SequencePart, +) -> Result<(), String> { + if let Some(element) = graph_sequence_part(function, types, source, Some(part)) { + let binding = function + .local(target) + .ok_or("graph sequence lost its exact element destination")?; + if !consume + || binding.ty != element + || header(function, types, index)?.ty != TypeInterner::INT64 + || (ordinary_type(types, element).is_err() + && crate::native_graph_storage_for_local(function, types, binding).is_none()) + { + return Err("graph sequence changed its exact owning component extraction".into()); + } + return Ok(()); + } + let element = sequence_element(function, types, source)?; + if !consume + || part != SequencePart::Element + || header(function, types, index)?.ty != TypeInterner::INT64 + || function.local(target).is_none_or(|binding| { + binding.ty != element + || (crate::native_graph_storage_for_local(function, types, binding).is_none() + && ordinary_type(types, binding.ty).is_err()) + }) + { + return Err( + "Resource consuming sequence changed its exact ordinary element extraction".into(), + ); + } + Ok(()) +} +fn local(id: LocalId, ty: TypeId, span: Span) -> Expression { + Expression { + kind: E::Local(id), + ty, + span, + } +} +fn temporary(function: &mut Function, ty: TypeId, span: Span) -> LocalId { + let id = LocalId::new(function.locals.len() as u32); + function.locals.push(Local { + id, + name: format!("$iterator{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: true, + view_source: None, + span, + }); + id +} +/// Reconstruct rather than trusting the edit transcript to choose statements, +/// headers, guards or edges. The before graph must still match all independent seals. +fn canonical_sequence( + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(Function, BlockId, usize), String> { + let block = before + .blocks + .get(edit.header.index() as usize) + .filter(|block| block.id == edit.header) + .ok_or("Resource sequence transition lost its exact original header")?; + let TerminatorKind::ForEach { + key, + value: value_binding, + by_view, + iterable, + body, + exit, + } = &block.terminator.kind + else { + return Err("Resource sequence transition has no exact original ForEach".into()); + }; + if iterable.ty.index() as usize >= types.len() { + return Err("Resource sequence transition lost its exact iterable type".into()); + } + if crate::native_graph_storage_for_expression(before, types, iterable).is_some() { + graph_sequence(before, types, *key, *value_binding, *by_view, iterable)?; + } else if resource_type_pending(types, iterable.ty) + || matches!(types.resolve(iterable.ty), Type::Map(..)) + { + carrier_sequence(before, types, *key, *value_binding, *by_view, iterable)?; + } else { + consuming_list(before, types, *key, *value_binding, *by_view, iterable)?; + } + let cfg = ControlFlowGraph::analyze(before) + .map_err(|errors| format!("Resource sequence CFG is malformed: {errors:?}"))?; + let mut outside = vec![false; before.blocks.len()]; + let mut pending = vec![before.entry]; + while let Some(current) = pending.pop() { + if current == edit.header || outside[current.index() as usize] { + continue; + } + outside[current.index() as usize] = true; + pending.extend_from_slice(cfg.successors(current)); + } + let preheaders = cfg + .predecessors(edit.header) + .iter() + .copied() + .filter(|id| outside[id.index() as usize]) + .collect::>(); + let [preheader] = preheaders.as_slice() else { + return Err("Resource sequence transition has no unique original preheader".into()); + }; + if !matches!(before.blocks[preheader.index() as usize].terminator.kind, TerminatorKind::Goto(target) if target == edit.header) + { + return Err("Resource sequence transition changed its exact preheader edge".into()); + } + let mut expected = before.clone(); + let span = iterable.span; + let cursor = temporary(&mut expected, TypeInterner::INT64, span); + let length = temporary(&mut expected, TypeInterner::INT64, span); + let source = temporary(&mut expected, iterable.ty, span); + let value = if let E::View(inner) = &iterable.kind { + *inner.clone() + } else { + iterable.clone() + }; + let initialization = vec![ + Statement { + kind: StatementKind::Let { + local: source, + value, + }, + span, + }, + Statement { + kind: StatementKind::Let { + local: cursor, + value: Expression { + kind: E::Int(0), + ty: TypeInterner::INT64, + span, + }, + }, + span, + }, + Statement { + kind: StatementKind::SequenceLength { + source: SequenceSource::Local(source), + target: length, + }, + span, + }, + ]; + let mut prefix = vec![ + Statement { + kind: StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(source), + index: cursor, + target: *key, + part: if matches!(types.resolve(iterable.ty), Type::Map(..)) { + SequencePart::Key + } else { + SequencePart::Element + }, + }, + span, + }, + Statement { + kind: StatementKind::Assign { + target: local(cursor, TypeInterner::INT64, span), + value: Expression { + kind: E::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Add, + right: Box::new(Expression { + kind: E::Int(1), + ty: TypeInterner::INT64, + span, + }), + }, + ty: TypeInterner::INT64, + span, + }, + }, + span, + }, + ]; + if let Some(value) = value_binding { + prefix.insert( + 1, + Statement { + kind: StatementKind::SequenceGet { + consume: true, + source: SequenceSource::Local(source), + index: cursor, + target: *value, + part: SequencePart::Value, + }, + span, + }, + ); + } + let prefix_length = prefix.len(); + let terminator = Terminator { + kind: TerminatorKind::Branch { + condition: Expression { + kind: E::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Less, + right: Box::new(local(length, TypeInterner::INT64, span)), + }, + ty: TypeInterner::BOOL, + span, + }, + then_block: *body, + else_block: *exit, + }, + span: block.terminator.span, + }; + if !crate::breakpoint_regions::terminator_equal(&edit.before, &block.terminator) + || !crate::breakpoint_regions::terminator_equal(&edit.after, &terminator) + || !edit.redirects.is_empty() + || !edit.blocks.is_empty() + || edit.append.len() != 1 + || edit.append[0].0 != *preheader + || edit.prefix.as_ref().map(|(id, _)| *id) != Some(*body) + { + return Err( + "Resource sequence transcript differs from the canonical consuming iterator edit" + .into(), + ); + } + let same_statements = |left: &[Statement], right: &[Statement]| { + left.len() == right.len() + && left + .iter() + .zip(right) + .all(|(left, right)| crate::breakpoint_regions::statement_equal(left, right)) + }; + if !same_statements(&edit.append[0].1, &initialization) + || !same_statements( + &edit + .prefix + .as_ref() + .ok_or("Resource sequence prefix disappeared")? + .1, + &prefix, + ) + { + return Err( + "Resource sequence transcript changed its exact source, cursor or extraction order" + .into(), + ); + } + expected.blocks[preheader.index() as usize] + .statements + .extend(initialization); + let statements = &mut expected.blocks[body.index() as usize].statements; + let mut all = prefix; + all.append(statements); + *statements = all; + expected.blocks[edit.header.index() as usize].terminator = terminator; + Ok((expected, *body, prefix_length)) +} +pub(crate) fn sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + if !function + .resource_lowering + .as_ref() + .is_some_and(|current| current.same(original)) + { + return Err("Resource sequence transition changed its exact before-witness".into()); + } + if !has_execution_records(before, types) + && !original.graph_metadata.needed() + && (!original.reflected_fields.is_empty() + || entry_scope::capture_needed(&original.original, &original.source)) + { + return reflected_fields::ordinary_sequence_transition(function, before, edit, types); + } + let (expected, body, prefix_length) = canonical_sequence(before, edit, types)?; + if function.id != expected.id + || function.identity != expected.identity + || function.params != expected.params + || function.locals != expected.locals + || function.capture_count != expected.capture_count + || function.return_type != expected.return_type + || function.entry != expected.entry + || function.span != expected.span + || function.debug_kind != expected.debug_kind + || !crate::breakpoint_regions::blocks_equal(&function.blocks, &expected.blocks) + { + return Err( + "Resource sequence transition changed more than its exact reconstructed canonical edit" + .into(), + ); + } + let mut witness = original.clone(); + witness.locals = expected.locals.clone(); + witness.blocks = expected.blocks.clone(); + borrowed_sums::shift_prefix(&mut witness, body, prefix_length); + lexical_borrows::shift_prefix(&mut witness, body, prefix_length); + for region in &mut witness.regions { + region.shift_prefix(body, prefix_length); + } + for region in &mut witness.constructors { + region.shift_prefix(body, prefix_length); + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for region in &mut constructor_seal { + region.shift_prefix(body, prefix_length); + } + witness.constructor_seal = constructor_seal.into(); + // The independent body advances only after reconstructing the same finite edit. + if original.descriptors.has_body() { + witness.descriptors.seal_body(&expected); + } + graph_metadata::sequence_transition(&mut witness.graph_metadata, before, &expected, edit)?; + witness.current(function)?; + function.resource_lowering = Some(witness); + Ok(()) +} diff --git a/crates/jett_mir/src/resource_ownership/control_flow_tests.rs b/crates/jett_mir/src/resource_ownership/control_flow_tests.rs new file mode 100644 index 00000000..19a283fe --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/control_flow_tests.rs @@ -0,0 +1,596 @@ +use super::tests::{checked_support, lowered}; +use super::*; +use crate::breakpoint_regions::SequenceEdit; +use hir::ExpressionKind as E; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCES: [&str; 5] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/01_for_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/02_for_owned_optional_break.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/03_for_owned_result_continue.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/04_match_owned_optional_first.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/05_match_owned_optional_second.jett" + ), +]; +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn edit(before: &Function, prepared: &Function) -> SequenceEdit { + let block = before + .blocks + .iter() + .find(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + .unwrap(); + let TerminatorKind::ForEach { body, .. } = block.terminator.kind else { + unreachable!() + }; + let preheader = before + .blocks + .iter() + .find(|candidate| { + matches!(candidate.terminator.kind, TerminatorKind::Goto(target) if target == block.id) + && prepared.blocks[candidate.id.index() as usize] + .statements + .len() + > candidate.statements.len() + }) + .unwrap(); + SequenceEdit { + header: block.id, + before: block.terminator.clone(), + after: prepared.blocks[block.id.index() as usize] + .terminator + .clone(), + append: vec![( + preheader.id, + prepared.blocks[preheader.id.index() as usize].statements[preheader.statements.len()..] + .to_vec(), + )], + prefix: Some(( + body, + prepared.blocks[body.index() as usize].statements[..2].to_vec(), + )), + redirects: Vec::new(), + blocks: Vec::new(), + } +} +fn site_can_reach(function: &Function, start: ResourceSite, end: ResourceSite) -> bool { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let position = |site: ResourceSite| match site.position() { + ResourcePosition::Statement(index) => index, + ResourcePosition::Terminator => function.blocks[site.block().index() as usize] + .statements + .len(), + }; + let mut pending = vec![(start.block(), position(start) + 1)]; + let mut seen = BTreeSet::new(); + while let Some((block, floor)) = pending.pop() { + if !seen.insert((block.index(), floor)) { + continue; + } + if block == end.block() && position(end) >= floor { + return true; + } + pending.extend(cfg.successors(block).iter().map(|target| (*target, 0))); + } + false +} +#[test] +fn resource_control_flow_original_for_and_match_retire_exact_loans_before_backing_reuse() { + for release in [false, true] { + for (index, source) in SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + let original = main(&program).clone(); + let original_rows = original + .resource_lowering + .as_ref() + .unwrap() + .borrowed_sums + .clone(); + crate::prepare_native_sequences(&mut program, types); + let function = main(&program); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })), + "Source{} release={release}", + index + 1 + ); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!(rows.len(), if index < 3 { 1 } else { 2 }); + for (old, row) in original_rows.iter().zip(rows) { + assert_eq!(old.original_initializer(), row.original_initializer()); + assert_eq!(old.alias(), row.alias()); + assert_eq!(old.backing(), row.backing()); + for local in row.retirement_locals() { + assert!(function.is_view_local(local)); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + let backing = plan.owner_slots().iter().find(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == row.backing())).unwrap().id(); + let projection = plan + .operations() + .iter() + .find(|operation| { + operation.site() == row.success_site() + && matches!( + operation.role(), + ResourceOperationRole::ProjectSumView { .. } + ) + }) + .unwrap(); + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = projection.role() + else { + unreachable!() + }; + let expected_kind = match index { + 1 => ResourceLexicalExitKind::Break, + 2 => ResourceLexicalExitKind::Continue, + _ => ResourceLexicalExitKind::Fallthrough, + }; + let exits = function + .blocks + .iter() + .flat_map(|block| { + (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)) + .filter_map(move |position| { + let exit = plan.lexical_exit(block.id, position)?; + (exit.kind() == expected_kind + && exit.clear_locals().contains(&row.alias())) + .then_some(( + ResourceSite { + function: function.id, + block: block.id, + position, + }, + exit, + )) + }) + }) + .collect::>(); + assert!(!exits.is_empty()); + for (_, exit) in &exits { + let operations = exit + .operation_ids() + .iter() + .map(|id| &plan.operations()[id.index()]) + .collect::>(); + let child_end = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).unwrap(); + assert!( + child_end < parent_end, + "Source{} child retires before the parent shell lease", + index + 1 + ); + assert!(!exit.clear_locals().contains(&row.backing())); + assert!( + exit.clear_locals() + .iter() + .all(|local| row.retirement_locals().contains(local)), + "an arm cannot retire its sibling alias" + ); + } + let moves = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == backing)).collect::>(); + assert_eq!( + moves.len(), + 1, + "original held is reused once after the borrowed scope" + ); + for (site, _) in exits { + assert!( + site_can_reach(function, site, moves[0].site()), + "the original backing is reused after this actual lexical boundary" + ); + } + for operation in plan.operations() { + if matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == backing) + { + assert!( + !site_can_reach(function, operation.site(), moves[0].site()), + "aborting cleanup cannot consume the backing on a path to its later owning unwrap" + ); + } + } + } + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!( + program, once, + "the no-op preparation must preserve every authenticated witness" + ); + } + } +} +#[test] +fn resource_control_flow_canonical_sequence_refuses_forged_transcripts_and_custody_edits() { + for release in [false, true] { + let checked = checked_support(SOURCES[0], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let before = main(&baseline); + let mut prepared = baseline.clone(); + crate::prepare_native_sequences(&mut prepared, types); + let good = main(&prepared); + let canonical = edit(before, good); + let mut replay = good.clone(); + replay.resource_lowering = before.resource_lowering.clone(); + control_flow::sequence_transition(&mut replay, before, &canonical, types).unwrap(); + assert_eq!(&replay, good); + for mutation in 0..10 { + let mut candidate = good.clone(); + candidate.resource_lowering = before.resource_lowering.clone(); + let mut transcript = canonical.clone(); + let (body, prefix) = transcript.prefix.as_mut().unwrap(); + let body = *body; + match mutation { + 0 => { + prefix.swap(0, 1); + candidate.blocks[body.index() as usize] + .statements + .swap(0, 1); + } + 1 => { + let TerminatorKind::Branch { condition, .. } = &mut transcript.after.kind + else { + unreachable!() + }; + let E::Binary { op, .. } = &mut condition.kind else { + unreachable!() + }; + *op = hir::BinaryOp::LessEqual; + candidate.blocks[transcript.header.index() as usize].terminator = + transcript.after.clone(); + } + 2 => { + let StatementKind::SequenceGet { consume, .. } = &mut prefix[0].kind else { + unreachable!() + }; + *consume = false; + candidate.blocks[body.index() as usize].statements[0] = prefix[0].clone(); + } + 3 => { + let StatementKind::SequenceGet { target, .. } = &mut prefix[0].kind else { + unreachable!() + }; + *target = before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(); + candidate.blocks[body.index() as usize].statements[0] = prefix[0].clone(); + } + 4 => { + let duplicate = candidate + .blocks + .iter() + .flat_map(|block| &block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + .unwrap() + .clone(); + prefix.push(duplicate.clone()); + candidate.blocks[body.index() as usize] + .statements + .insert(2, duplicate); + } + 5 => { + let TerminatorKind::Branch { else_block, .. } = &mut transcript.after.kind + else { + unreachable!() + }; + *else_block = body; + candidate.blocks[transcript.header.index() as usize].terminator = + transcript.after.clone(); + } + 6 => { + transcript.append[0].1.swap(0, 2); + let (preheader, values) = &transcript.append[0]; + let start = before.blocks[preheader.index() as usize].statements.len(); + candidate.blocks[preheader.index() as usize].statements[start..] + .clone_from_slice(values); + } + 7 => { + candidate.locals.last_mut().unwrap().ty = before + .local( + before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(), + ) + .unwrap() + .ty; + } + 8 => { + let statement = candidate + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + }) + .unwrap(); + let StatementKind::SumTake { source, .. } = &mut statement.kind else { + unreachable!() + }; + *source = before.resource_lowering.as_ref().unwrap().borrowed_sums[0].backing(); + } + 9 => { + transcript.prefix = None; + } + _ => unreachable!(), + } + let unchanged = candidate.resource_lowering.clone(); + assert!( + control_flow::sequence_transition(&mut candidate, before, &transcript, types) + .is_err(), + "mutation={mutation} release={release}" + ); + assert_eq!( + candidate.resource_lowering, unchanged, + "rejected edit grants no replacement witness" + ); + } + } +} +#[test] +fn resource_control_flow_prepared_for_and_match_refuse_resealed_duplicate_or_moved_loan_sites() { + for release in [false, true] { + for index in [0, 1, 2, 3, 4] { + let checked = checked_support(SOURCES[index], SUPPORT, release); + let types = &checked.checked().interner; + let mut baseline = lowered(&checked); + crate::prepare_native_sequences(&mut baseline, types); + validate_resource_ownership(&baseline, types).unwrap(); + for mutation in 0..4 { + let mut candidate = baseline.clone(); + let function = main_mut(&mut candidate); + let row = function.resource_lowering.as_ref().unwrap().borrowed_sums[0].clone(); + let project = row.success_site(); + let ResourcePosition::Statement(position) = project.position() else { + unreachable!() + }; + match mutation { + 0 => { + let duplicate = function.blocks[project.block().index() as usize] + .statements[position] + .clone(); + function.blocks[project.block().index() as usize] + .statements + .insert(position + 1, duplicate); + } + 1 => { + function.blocks[project.block().index() as usize] + .statements + .remove(position); + } + 2 => { + let exit = function + .blocks + .iter_mut() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + }) + .unwrap(); + let boundary = exit + .statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + .unwrap(); + let duplicate = exit.statements[boundary].clone(); + exit.statements.insert(0, duplicate); + } + 3 => { + let block = function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::Switch { .. } | TerminatorKind::Branch { .. } + ) + }) + .unwrap(); + match &mut block.terminator.kind { + TerminatorKind::Switch { variants, .. } => { + let target = variants[0].1; + variants[0].1 = variants[1].1; + variants[1].1 = target; + } + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + std::mem::swap(then_block, else_block); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = function.blocks.clone(); + witness.locals = function.locals.clone(); + assert!( + validate_resource_ownership(&candidate, types).is_err(), + "Source{} mutation={mutation} release={release}", + index + 1 + ); + assert!( + !crate::sequences::prune::unreachable(main_mut(&mut candidate)), + "canonical pruning cannot rehabilitate a forged loan graph" + ); + } + } + } +} + +// Draft append to resource_ownership/control_flow_tests.rs after Source registration. +// UNEXECUTED: Root must first compile/check both new Source programs and derive +// provider transcripts from the reference executor. No proposed outcome is acceptance. +const MULTI_LOOP_SOURCES: [&str; 2] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/07_consecutive_for_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/control_flow/08_nested_for_owned_optional_reuse.jett" + ), +]; +#[test] +fn resource_control_flow_repeated_sequence_transitions_preserve_all_sibling_and_nested_loans() { + for release in [false, true] { + for (index, source) in MULTI_LOOP_SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + let original = main(&program).clone(); + let original_witness = original.resource_lowering.as_ref().unwrap(); + assert_eq!( + original + .blocks + .iter() + .filter(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + .count(), + 2 + ); + assert_eq!(original_witness.borrowed_sums.len(), 2); + crate::prepare_native_sequences(&mut program, types); + let function = main(&program); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let plan = validate_resource_ownership(&program, types).unwrap(); + let custody = plan.function(function.id).unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.borrowed_sums.len(), 2); + for (before, after) in original_witness + .borrowed_sums + .iter() + .zip(&witness.borrowed_sums) + { + assert_eq!(before.original_initializer(), after.original_initializer()); + assert_eq!(before.alias(), after.alias()); + assert_eq!(before.backing(), after.backing()); + } + let rows = &witness.borrowed_sums; + assert_eq!(rows[0].backing(), rows[1].backing()); + assert_ne!(rows[0].alias(), rows[1].alias()); + if index == 0 { + let first_alias = function.local(rows[0].alias()).unwrap(); + let second_alias = function.local(rows[1].alias()).unwrap(); + assert_eq!(first_alias.name, "token"); + assert_eq!(second_alias.name, "token"); + assert_ne!(first_alias.span, second_alias.span); + } + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!( + statement.kind, + StatementKind::SequenceGet { consume: true, .. } + )) + .count(), + 2 + ); + let projects = custody + .operations() + .iter() + .filter_map(|operation| { + let ResourceOperationRole::ProjectSumView { + source, + destination, + .. + } = operation.role() + else { + return None; + }; + Some((operation, *source, *destination)) + }) + .collect::>(); + assert_eq!(projects.len(), 2); + for (position, row) in rows.iter().enumerate() { + let (_, parent, child) = projects + .iter() + .find(|(operation, _, _)| operation.site() == row.success_site()) + .unwrap(); + let exits = function + .blocks + .iter() + .flat_map(|block| { + (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)) + .filter_map(move |site| custody.lexical_exit(block.id, site)) + }) + .filter(|exit| { + exit.kind() == ResourceLexicalExitKind::Fallthrough + && exit.clear_locals().contains(&row.alias()) + }) + .collect::>(); + assert!(!exits.is_empty()); + for exit in exits { + let roles = exit + .operation_ids() + .iter() + .map(|id| custody.operations()[id.index()].role()) + .collect::>(); + let child_end = roles.iter().position(|role| matches!(role, ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = roles.iter().position(|role| matches!(role, ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).unwrap(); + assert!(child_end < parent_end); + assert!(!exit.clear_locals().contains(&row.backing())); + if index == 0 || position == 1 { + assert!( + !exit.clear_locals().contains(&rows[1 - position].alias()), + "sibling fallthrough or nested inner exit cannot retire another live scope" + ); + } + } + } + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/entry_scope.rs b/crates/jett_mir/src/resource_ownership/entry_scope.rs new file mode 100644 index 00000000..0c8e0cac --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/entry_scope.rs @@ -0,0 +1,163 @@ +//! A selected native wrapper Scope does not make its ordinary body a Source family. +use super::*; + +/// Wrapper-only metadata borrowed from a fully authenticated current Program. +#[derive(Debug)] +pub struct ResourceEntryScopePlan { + function: FunctionId, + identity: hir::FunctionIdentity, + parameters: Vec, + return_type: TypeId, + root_scope: ResourceFrame, + completion: ResourceOperation, +} +impl ResourceEntryScopePlan { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &hir::FunctionIdentity { + &self.identity + } + pub fn parameters(&self) -> &[Param] { + &self.parameters + } + pub fn return_type(&self) -> TypeId { + self.return_type + } + pub fn root_scope(&self) -> &ResourceFrame { + &self.root_scope + } + pub fn completion(&self) -> &ResourceOperation { + &self.completion + } +} + +pub(super) fn capture_needed( + function: &hir::Function, + source: &hir::ResourceSourceArchive, +) -> bool { + source.manifest().is_some() + && function.capture_count == 0 + && function.return_type == TypeInterner::NOTHING + && function + .params + .iter() + .all(|param| param.ty == TypeInterner::NETWORK) +} + +/// Selects an authenticated wrapper Scope without changing execution-family membership. +pub fn validate_resource_ownership_for_entry<'p>( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, +) -> Result, Vec> { + crate::validate(program)?; + let mut plan = validate_resource_ownership(program, types)?; + let span = program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }); + let selected = program + .functions + .get(entry.index() as usize) + .filter(|function| function.id == entry) + .ok_or_else(|| { + vec![ValidationError { + span, + message: "native Resource entry has no exact current function".into(), + }] + })?; + let refusal = |message: &str| { + vec![ValidationError { + span: selected.span, + message: message.into(), + }] + }; + if plan.required_only_function_ids().contains(&entry) { + return Err(refusal( + "required-only Resource helper cannot be a native entry", + )); + } + let witness = selected.resource_lowering.as_ref().ok_or_else(|| { + refusal("native Resource entry lacks its initially authenticated Source witness") + })?; + let original = plan + .authenticated_entry(entry) + .ok_or_else(|| refusal("native Resource entry has no exact original Source function"))?; + if !capture_needed(original, &witness.source) + || !capture_needed(&witness.original, &witness.source) + || selected.identity != original.identity + || selected.capture_count != 0 + || selected.return_type != original.return_type + || selected.params.len() != original.params.len() + || !selected + .params + .iter() + .zip(&original.params) + .all(|(current, archived)| { + current.name == archived.name + && current.ty == archived.ty + && current.mode == archived.mode + && current.mutable == archived.mutable + && current.span == archived.span + }) + { + return Err(refusal( + "native Resource entry changed its closed Network/Nothing Source header", + )); + } + if plan.function(entry).is_some() { + return Ok(plan); + } + if has_execution_records(selected, types) { + return Err(refusal( + "native entry cannot replace a missing Source execution-family plan", + )); + } + ControlFlowGraph::analyze(selected).map_err(|errors| { + errors + .into_iter() + .map(|error| ValidationError { + span: selected.span, + message: format!("native Resource entry CFG: {}", error.message), + }) + .collect::>() + })?; + let site = ResourceSite { + function: entry, + block: selected.entry, + position: ResourcePosition::Terminator, + }; + let frame = ResourceFrameId(0); + plan.entry_scope = Some(ResourceEntryScopePlan { + function: entry, + identity: selected.identity.clone(), + parameters: selected.params.clone(), + return_type: selected.return_type, + root_scope: ResourceFrame { + id: frame, + role: ResourceFrameRole::Scope, + site, + parent: None, + ordinal: 0, + }, + completion: ResourceOperation { + id: ResourceOperationId(0), + frame, + site, + ordinal: 0, + role: ResourceOperationRole::Complete { + outcome: ResourceCompletion::Return, + }, + expression: None, + named_indirect: None, + indirect_hook: None, + }, + }); + Ok(plan) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs b/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs new file mode 100644 index 00000000..b60a16cf --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/entry_scope/tests.rs @@ -0,0 +1,1041 @@ +use super::*; +use crate::resource_ownership::tests::{checked, checked_support, lowered}; +use jett_common::{FileId, SourceOrigin}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_parser::parse; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +include!( + "../../../../jett_codegen_cranelift/src/emit/resource_execution/tests/carrier_wrapper_sources.rs" +); + +const WHOLE_SOURCE: &str = include_str!("../fixtures/17_absent_aggregate_shapes.jett"); +const FIXTURE_SUPPORT: &str = + include_str!("../../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const PURE: &str = r#"namespace app +function helper() returns int64: + return 7 +function ordinary_probe(view net: Network) returns nothing: + int64 answer = helper() + if answer != 7: + return nothing + return nothing + int64 dead = 9 + return nothing +function peer(view net: Network) returns nothing: + return nothing +"#; + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap_or_else(|| panic!("checked app.{name}")) +} + +fn materialized(checked: &Arc) -> Program { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let required = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + required.diagnostics.is_empty(), + "{:?}", + required.diagnostics + ); + assert!(required.debug_events.is_empty()); + required.values.checked_required_values(checked).unwrap(); + assert!(!required.values.is_empty()); + let types = &checked.checked().interner; + let mut hir = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values(&mut hir, &required.values, types).unwrap(); + hir::complete_value_conversions(&mut hir, types).unwrap(); + crate::lower(&hir, types).unwrap() +} + +fn prepare(program: &mut Program, types: &TypeInterner) { + crate::prepare_native_sequences(program, types); + crate::prepare_native_uninhabited_sums(program, types); + crate::prepare_native_generated_functions(program, types); +} + +fn assert_bridge_metadata(program: &Program, types: &TypeInterner, name: &str) { + let selected = named(program, name); + let ordinary = validate_resource_ownership(program, types).unwrap(); + assert!(ordinary.entry_scope().is_none()); + assert!(ordinary.function(selected.id).is_none()); + let ownership = validate_resource_ownership_for_entry(program, types, selected.id).unwrap(); + let bridge = ownership + .entry_scope() + .expect("selected wrapper-only Scope"); + assert_eq!(bridge.function(), selected.id); + assert_eq!(bridge.identity(), &selected.identity); + assert_eq!(bridge.parameters(), selected.params.as_slice()); + assert_eq!(bridge.return_type(), TypeInterner::NOTHING); + assert_eq!(bridge.root_scope().role(), ResourceFrameRole::Scope); + assert_eq!(bridge.root_scope().parent(), None); + assert_eq!(bridge.root_scope().site().function(), selected.id); + assert_eq!(bridge.root_scope().site().block(), selected.entry); + assert_eq!( + bridge.root_scope().site().position(), + ResourcePosition::Terminator + ); + assert_eq!(bridge.completion().frame(), bridge.root_scope().id()); + assert_eq!(bridge.completion().site(), bridge.root_scope().site()); + assert!(matches!( + bridge.completion().role(), + ResourceOperationRole::Complete { .. } + )); + assert!(!bridge.completion().is_expression_operation()); + assert!(bridge.completion().named_indirect_target().is_none()); + assert!(bridge.completion().indirect_hook_target().is_none()); + assert!(ownership.function(selected.id).is_none()); + assert!( + !ownership + .functions() + .iter() + .any(|function| function.function() == selected.id) + ); + assert!(ownership.original_source(selected.id).is_some()); + assert!(!has_execution_records(selected, types)); + validate_call_ownership(program, types).unwrap(); +} + +fn assert_bridge(program: &Program, types: &TypeInterner, name: &str) { + assert_bridge_metadata(program, types, name); + let selected = named(program, name); + let copy = + crate::copy_values::CopyValuePlan::analyze_storage(selected, types, Some(program)).unwrap(); + let storage = crate::move_values::MoveValuePlan::analyze(program, selected, types).unwrap(); + assert_eq!(copy.owned_locals, storage.owned_locals); + validate_call_ownership(program, types).unwrap(); +} + +fn reseal_general_cache(function: &mut Function) { + let parameters = function.params.clone(); + let locals = function.locals.clone(); + let blocks = function.blocks.clone(); + let entry = function.entry; + let witness = function.resource_lowering.as_mut().unwrap(); + witness.parameters = parameters; + witness.locals = locals; + witness.blocks = blocks; + witness.entry = entry; +} + +fn assert_refused_before_prune( + mut program: Program, + types: &TypeInterner, + name: &str, + locally_exact_foreign: bool, +) { + let selected = named(&program, name).id; + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + let before = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert!( + program == before, + "refused canonical pruning must preserve the graph" + ); + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + if named(&program, name).resource_lowering.is_some() { + let function = named(&program, name); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.current(function).is_ok(), locally_exact_foreign); + if locally_exact_foreign { + // A sibling's changed Source makes the whole archive foreign even + // when this function's local body is still exactly authenticated. + let original = witness.original.clone(); + let source = witness.source.clone(); + assert!(function.locals.iter().any(|local| local.name == "dead")); + assert!(crate::sequences::prune::unreachable(named_mut( + &mut program, + name + ))); + let function = named(&program, name); + assert!(!function.locals.iter().any(|local| local.name == "dead")); + let witness = function.resource_lowering.as_ref().unwrap(); + witness.current(function).unwrap(); + assert!(original_function_equal(&witness.original, &original)); + assert!(witness.source == source); + let errors = match validate_resource_ownership(&program, types) { + Err(errors) => errors, + Ok(_) => panic!("local pruning admitted a foreign Source archive"), + }; + assert!( + errors.iter().any(|error| error + .message + .contains("mixes foreign original/materialized archives")), + "{errors:?}" + ); + } else { + assert!(!crate::sequences::prune::unreachable(named_mut( + &mut program, + name + ))); + assert!(program == before); + } + } + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); +} + +#[test] +fn ordinary_entry_pure_checked_candidates_have_initial_body_seals_without_comptime_or_name_authority() + { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.required_materializations().is_empty()); + let mut has_comptime = false; + for function in hir + .functions + .iter() + .filter(|function| function.identity.declaration.namespace == "app") + { + walk::hir_block(&function.body, &mut |value| { + has_comptime |= matches!(value.kind, hir::ExpressionKind::Comptime { .. }); + }); + } + assert!(!has_comptime); + let baseline = crate::lower(&hir, types).unwrap(); + for name in ["ordinary_probe", "peer"] { + let function = named(&baseline, name); + let witness = function + .resource_lowering + .as_ref() + .expect("initial proof-only capture"); + assert!(witness.descriptors.has_body()); + witness.descriptors.current(function).unwrap(); + assert_bridge(&baseline, types, name); + } + assert!( + validate_resource_ownership(&baseline, types) + .unwrap() + .function(named(&baseline, "helper").id) + .is_none() + ); + let probe = named(&baseline, "ordinary_probe"); + assert!(probe.locals.iter().any(|local| local.name == "dead")); + let before_locals = probe.locals.len(); + let mut compacted = baseline.clone(); + prepare(&mut compacted, types); + assert!(named(&compacted, "ordinary_probe").locals.len() < before_locals); + assert_bridge(&compacted, types, "ordinary_probe"); + assert_bridge(&compacted, types, "peer"); + } +} + +#[test] +fn ordinary_entry_default_archive_hir_remains_ordinary_and_cannot_mint_an_entry_scope() { + let source = "namespace app\nfunction ordinary_probe(view net: Network) returns nothing:\n return nothing\n"; + let file = FileId::new(0); + let parsed = parse(source, file); + assert!(parsed.errors.is_empty()); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let hir = hir::lower(&parsed.module, &resolved, &checked, &origins).unwrap(); + assert!(hir.resource_source.manifest().is_none()); + let program = crate::lower(&hir, &checked.interner).unwrap(); + let selected = named(&program, "ordinary_probe"); + assert!(selected.resource_lowering.is_none()); + let ordinary = validate_resource_ownership(&program, &checked.interner).unwrap(); + assert!(ordinary.function(selected.id).is_none()); + assert!(ordinary.entry_scope().is_none()); + assert!( + validate_resource_ownership_for_entry(&program, &checked.interner, selected.id).is_err() + ); +} + +#[test] +fn ordinary_entry_consuming_list_for_and_later_ordinary_caller_preserve_the_initial_proof() { + const SOURCE: &str = r#"namespace app +function main() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + return nothing +function reenter() returns nothing: + main() + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let main = named(&program, "main"); + let selected = main.id; + assert!( + main.resource_lowering + .as_ref() + .unwrap() + .descriptors + .has_body() + ); + assert!( + main.blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + let initial = validate_resource_ownership_for_entry(&program, types, selected).unwrap(); + assert!(initial.entry_scope().is_some()); + assert!(initial.function(selected).is_none()); + assert!(initial.function(named(&program, "reenter").id).is_none()); + prepare(&mut program, types); + assert!( + !named(&program, "main") + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + assert_bridge(&program, types, "main"); + assert_bridge(&program, types, "reenter"); + let main = named(&program, "main"); + let storage = crate::move_values::MoveValuePlan::analyze(&program, main, types).unwrap(); + let source = main + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| { + if let StatementKind::SequenceGet { + consume: true, + source, + .. + } = &statement.kind + { + Some(source.root()) + } else { + None + } + }) + .expect("actual canonical consuming-list element extraction"); + assert!( + matches!(types.resolve(main.local(source).unwrap().ty), Type::List(inner) if *inner == TypeInterner::INT64) + ); + assert!(storage.owned_locals.contains(&(source.index() as usize))); + } +} + +#[test] +fn ordinary_entry_source17_wrapper14_keeps_required_helper_and_main_ordinary_before_and_after_preparation() + { + assert_eq!(WRAPPERS.len(), 14); + let (name, suffix) = WRAPPERS[13]; + assert_eq!(name, "14_required_primitive_controls"); + let source = format!("{WHOLE_SOURCE}{suffix}"); + for release in [false, true] { + let checked = checked_support(&source, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = materialized(&checked); + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + } + assert_bridge(&program, types, "main"); + let main = named(&program, "main"); + let helper = named(&program, "absent_required_controls"); + let ownership = + validate_resource_ownership_for_entry(&program, types, main.id).unwrap(); + assert!(ownership.function(helper.id).is_none()); + assert!(!has_execution_records(helper, types)); + crate::copy_values::CopyValuePlan::analyze(helper, types).unwrap(); + crate::move_values::MoveValuePlan::analyze(&program, helper, types).unwrap(); + let archived = ownership.original_source(helper.id).unwrap(); + let mut required = false; + walk::hir_block(&archived.body, &mut |value| { + required |= matches!(value.kind, hir::ExpressionKind::Comptime { .. }); + }); + assert!( + required, + "the complete original required-value helper remains archived" + ); + for constructor in [ + "empty_tokens", + "absent_tokens", + "empty_map", + "absent_map", + "empty_result_tokens", + "failed_result_tokens", + "absent_envelope", + "absent_box", + "empty_choice", + "absent_choice", + "absent_state", + "absent_exact_state", + ] { + named(&program, constructor); + } + named(&program, "refined_shape_contract"); + } + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + assert!(!ownership.required_only_function_ids().is_empty()); + for &required_only in ownership.required_only_function_ids() { + let errors = + validate_resource_ownership_for_entry(&baseline, types, required_only).unwrap_err(); + assert!( + errors + .iter() + .any(|error| error.message.contains("required-only")) + ); + } + } +} + +#[test] +fn ordinary_entry_missing_foreign_or_copied_witness_refuses_before_canonical_pruning() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let foreign = lowered(&self::checked( + &PURE.replace("return 7", "return 8"), + release, + )); + for mutation in 0..4 { + let mut changed = baseline.clone(); + let replacement = match mutation { + 0 => None, + 1 => named(&foreign, "ordinary_probe").resource_lowering.clone(), + 2 => named(&baseline, "peer").resource_lowering.clone(), + 3 => None, + _ => unreachable!(), + }; + named_mut(&mut changed, "ordinary_probe").resource_lowering = replacement; + if mutation == 3 { + named_mut(&mut changed, "ordinary_probe").capture_count = 1; + crate::validate(&changed).unwrap(); + let errors = match validate_resource_ownership(&changed, types) { + Err(errors) => errors, + Ok(_) => panic!("missing original entry witness was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("constructor witness")), + "{:?}", + errors + ); + } + assert_refused_before_prune(changed, types, "ordinary_probe", mutation == 1); + } + } +} + +#[test] +fn ordinary_entry_changed_headers_and_dense_identity_cannot_be_resealed_into_authority() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let selected = named(&baseline, "ordinary_probe").id; + for mutation in 0..7 { + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "ordinary_probe"); + match mutation { + 0 => function.params[0].mode = ParamMode::Owned, + 1 => function.params[0].mutable = true, + 2 => function.params[0].ty = TypeInterner::INT64, + 3 => function.return_type = TypeInterner::INT64, + 4 => function.capture_count = 1, + 5 => function.identity.declaration.name = "replacement".into(), + 6 => function.id = named(&baseline, "peer").id, + _ => unreachable!(), + } + reseal_general_cache(function); + assert!( + validate_resource_ownership_for_entry(&changed, types, selected).is_err(), + "mutation={mutation}; release={release}" + ); + let before = changed.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.name + == if mutation == 5 { + "replacement" + } else { + "ordinary_probe" + } + }) + .unwrap(); + assert!(!crate::sequences::prune::unreachable(function)); + assert!(changed == before); + } + assert!( + validate_resource_ownership_for_entry(&baseline, types, FunctionId::new(u32::MAX)) + .is_err() + ); + } +} + +#[test] +fn ordinary_entry_independent_body_seal_refuses_resealed_whole_body_and_disconnected_replacement() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + for mutation in 0..3 { + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "ordinary_probe"); + match mutation { + 0 => { + let value = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| { + if let StatementKind::Let { value, .. } = &mut statement.kind { + Some(value) + } else { + None + } + }) + .unwrap(); + value.kind = hir::ExpressionKind::Int(99); + } + 1 => function.blocks[usize::try_from(function.entry.index()).unwrap()] + .statements + .clear(), + 2 => { + let mut copied = + function.blocks[usize::try_from(function.entry.index()).unwrap()].clone(); + copied.id = BlockId(u32::try_from(function.blocks.len()).unwrap()); + copied.terminator.kind = TerminatorKind::Return(None); + function.blocks.push(copied); + } + _ => unreachable!(), + } + reseal_general_cache(function); + assert_refused_before_prune(changed, types, "ordinary_probe", false); + } + } +} + +#[test] +fn ordinary_entry_independent_graph_is_mandatory_even_when_general_cache_is_exact() { + for release in [false, true] { + let checked = checked(PURE, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = named_mut(&mut program, "ordinary_probe"); + assert!( + function + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .has_body() + ); + function.resource_lowering.as_mut().unwrap().descriptors = + returned_descriptors::DescriptorWitness::default(); + assert_refused_before_prune(program, types, "ordinary_probe", false); + } +} + +#[test] +fn ordinary_entry_execution_family_retains_its_existing_body_scope_without_an_extra_bridge() { + const SOURCE: &str = include_str!("../fixtures/connected_entry.jett"); + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let selected = named(&program, "main").id; + let plan = validate_resource_ownership_for_entry(&program, types, selected).unwrap(); + assert!(plan.entry_scope().is_none()); + assert!(plan.function(selected).is_some()); + } +} + +const RAW_REFINEMENTS: &str = r#"namespace app +type NonZeroInt = int64 where value != 0 +type Positive = int64 where value > 0 +function quotient(value: int64, divisor: NonZeroInt) returns int64: + return value / divisor +function pure_entry() returns nothing: + list[int64] values = list(1, 2) + mutable int64 total = 0 + for number in values: + total = total + number + return nothing +function ordinary_probe(view net: Network) returns nothing: + pure_entry() + return nothing +"#; + +#[test] +fn ordinary_entry_default_archive_refinement_predicates_remain_ordinary_through_preparation() { + for release in [false, true] { + let file = FileId::new(0); + let parsed = parse(RAW_REFINEMENTS, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check_with_options( + &parsed.module, + &resolved, + jett_typecheck::CheckOptions { release }, + ); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + let origins = HashMap::from([(file, SourceOrigin::Project)]); + let hir = hir::lower(&parsed.module, &resolved, &checked, &origins).unwrap(); + assert!(hir.resource_source.manifest().is_none()); + assert_eq!( + hir.functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate + }) + .count(), + 2 + ); + let types = &checked.interner; + let baseline = crate::lower(&hir, types).unwrap(); + assert!( + named(&baseline, "pure_entry") + .blocks + .iter() + .any(|block| { matches!(block.terminator.kind, TerminatorKind::ForEach { .. }) }) + ); + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + assert!(!named(&program, "pure_entry").blocks.iter().any(|block| { + matches!(block.terminator.kind, TerminatorKind::ForEach { .. }) + })); + } + assert!( + program + .functions + .iter() + .all(|function| function.resource_lowering.is_none()) + ); + validate_call_ownership(&program, types).unwrap(); + let ordinary = validate_resource_ownership(&program, types).unwrap(); + assert!(ordinary.functions().is_empty()); + assert!(ordinary.entry_scope().is_none()); + for name in ["pure_entry", "ordinary_probe"] { + assert!( + validate_resource_ownership_for_entry( + &program, + types, + named(&program, name).id + ) + .is_err() + ); + } + } + } +} + +#[test] +fn ordinary_entry_checked_refinement_archive_retains_proofs_and_refuses_missing_authority() { + for release in [false, true] { + let checked = checked(RAW_REFINEMENTS, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + assert!(hir.resource_source.manifest().is_some()); + assert!(!hir.resource_manifest.kinds().is_empty()); + let baseline = crate::lower(&hir, types).unwrap(); + let predicates = baseline + .functions + .iter() + .filter(|function| { + function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate + }) + .collect::>(); + assert_eq!(predicates.len(), 2); + for predicate in predicates { + let witness = predicate + .resource_lowering + .as_ref() + .expect("checked refinement proof"); + assert_eq!(&witness.source, &hir.resource_source); + witness.current(predicate).unwrap(); + assert!(!has_execution_records(predicate, types)); + } + for prepared in [false, true] { + let mut program = baseline.clone(); + if prepared { + prepare(&mut program, types); + } + for name in ["pure_entry", "ordinary_probe"] { + if prepared { + assert_bridge(&program, types, name); + } else { + // The initial proof exists before executable For staging. + // Storage/ABI analysis follows certified preparation above. + assert_bridge_metadata(&program, types, name); + } + } + let selected = named(&program, "pure_entry").id; + named_mut(&mut program, "pure_entry").resource_lowering = None; + let errors = match validate_resource_ownership(&program, types) { + Err(errors) => errors, + Ok(_) => panic!("lost checked entry witness was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("constructor witness")) + ); + assert!(validate_resource_ownership_for_entry(&program, types, selected).is_err()); + } + // Removing the checked archive cannot turn a real Resource manifest + // into the ordinary raw-HIR path before initial constructor capture. + let mut no_archive = hir.clone(); + no_archive.resource_source = hir::ResourceSourceArchive::empty(); + let errors = match crate::lower(&no_archive, types) { + Err(errors) => errors, + Ok(_) => panic!("raw HIR with a Resource manifest was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("no original checked HIR archive")) + ); + } +} + +#[test] +fn ordinary_entry_raw_body_only_resource_descriptor_retains_mandatory_custody_capture() { + for release in [false, true] { + let checked = checked( + "namespace app\nfunction pure_entry() returns nothing:\n return nothing\n", + release, + ); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let mut function = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "pure_entry" + }) + .unwrap() + .clone(); + let descriptor = hir + .functions + .iter() + .find_map(|candidate| { + let mut found = None; + walk::hir_block(&candidate.body, &mut |value| { + if matches!(value.kind, hir::ExpressionKind::ResourceHookValue { .. }) { + found = Some(value.clone()); + } + }); + found + }) + .expect("genuine checked descriptor expression"); + assert!(!resource_type_pending(types, function.return_type)); + assert!( + function + .locals + .iter() + .all(|local| !resource_type_pending(types, local.ty)) + ); + function.body.statements.insert( + 0, + hir::Statement { + span: descriptor.span, + kind: hir::StatementKind::Expression(descriptor), + }, + ); + let no_archive = hir::ResourceSourceArchive::empty(); + let execution = ResourceExecutionClosure::default(); + assert!(runtime_custody_needed(&function, types, &execution)); + let capture = Capture::authenticated( + &function, + &hir.resource_manifest, + &no_archive, + types, + &execution, + ) + .unwrap(); + assert!( + capture.witness.is_some(), + "raw body-only custody must not be made ordinary" + ); + let mut raw = hir; + function.id = FunctionId::new(0); + raw.functions = vec![function]; + raw.resource_source = no_archive; + let errors = match crate::lower(&raw, types) { + Err(errors) => errors, + Ok(_) => panic!("raw body-only Resource descriptor was accepted"), + }; + assert!( + errors + .iter() + .any(|error| error.message.contains("no original checked HIR archive")) + ); + } +} + +#[test] +fn ordinary_entry_checked_scalar_refinement_lost_foreign_body_or_header_proofs_refuse() { + for release in [false, true] { + let checked = checked(RAW_REFINEMENTS, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let selected = baseline + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "Positive" + && function.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + }) + .expect("genuine generated scalar refinement predicate") + .id; + let foreign = lowered(&self::checked( + &RAW_REFINEMENTS.replace("value > 0", "value > 1"), + release, + )); + for mutation in 0..5 { + let mut changed = baseline.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.id == selected) + .unwrap(); + match mutation { + 0 => function.resource_lowering = None, + 1 => { + function.resource_lowering = None; + function.identity.declaration.kind = hir::DeclarationKind::Function; + } + 2 => { + let value = function + .blocks + .iter_mut() + .find_map(|block| { + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind + { + Some(value) + } else { + None + } + }) + .expect("actual refinement return operand"); + assert_eq!(value.ty, TypeInterner::BOOL); + value.kind = hir::ExpressionKind::Bool(false); + } + 3 => function.return_type = TypeInterner::NOTHING, + 4 => { + function.resource_lowering = foreign + .functions + .iter() + .find(|function| function.id == selected) + .unwrap() + .resource_lowering + .clone(); + } + _ => unreachable!(), + } + let errors = match validate_resource_ownership(&changed, types) { + Err(errors) => errors, + Ok(_) => panic!("refinement mutation {mutation} was accepted; release={release}"), + }; + if mutation < 2 { + assert!( + errors + .iter() + .any(|error| error.span == function_span(&baseline, selected) + && error.message.contains("constructor witness")) + ); + } + let before = changed.clone(); + crate::prepare_native_sequences(&mut changed, types); + assert!( + changed == before, + "invalid refinement proof was canonically resealed" + ); + } + } +} + +fn function_span(program: &Program, id: FunctionId) -> jett_common::Span { + program + .functions + .iter() + .find(|function| function.id == id) + .unwrap() + .span +} + +#[test] +fn ordinary_scope_interface_sequence_preserves_role_and_refuses_custody_or_edits() { + const SOURCE: &str = "namespace app\ninterface Named:\n function name(view self: Named) returns string\nactor Counter:\n mutable int64 count = 1\nimplement Named for Counter:\n function name(view self: Counter) returns string:\n return \"counter\"\nfunction main() returns nothing:\n Counter worker = spawn Counter()\n list[Named] items = list(worker)\n for item in items:\n println(Named.name(view item))\n return nothing\n"; + let file = FileId::new(0); + let checked = Arc::new( + CheckedResourceProgram::prepare( + parse(SOURCE, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let hir = hir::lower_checked_ordinary_program(&checked).unwrap(); + let types = &checked.checked().interner; + let baseline = crate::lower(&hir, types).unwrap(); + let before = named(&baseline, "main"); + assert!(before.resource_lowering.is_some()); + assert!(!has_execution_records(before, types)); + let header = before + .blocks + .iter() + .find(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + .unwrap(); + let TerminatorKind::ForEach { body, .. } = header.terminator.kind else { + unreachable!() + }; + let mut prepared = baseline.clone(); + crate::prepare_native_sequences(&mut prepared, types); + let after = named(&prepared, "main"); + assert!( + after + .blocks + .iter() + .all(|block| !matches!(block.terminator.kind, TerminatorKind::ForEach { .. })) + ); + assert!(!has_execution_records(after, types)); + validate_resource_ownership(&prepared, types).unwrap(); + let preheader = before + .blocks + .iter() + .find(|block| { + after.blocks[block.id.index() as usize].statements.len() > block.statements.len() + }) + .unwrap(); + let edit = crate::breakpoint_regions::SequenceEdit { + header: header.id, + before: header.terminator.clone(), + after: after.blocks[header.id.index() as usize].terminator.clone(), + append: vec![( + preheader.id, + after.blocks[preheader.id.index() as usize].statements[preheader.statements.len()..] + .to_vec(), + )], + prefix: Some(( + body, + after.blocks[body.index() as usize].statements[..2].to_vec(), + )), + redirects: Vec::new(), + blocks: Vec::new(), + }; + for forgery in 0..5 { + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "main"); + match forgery { + 0 => { + let TerminatorKind::ForEach { iterable, .. } = + &mut function.blocks[header.id.index() as usize].terminator.kind + else { + unreachable!() + }; + iterable.span.end += 1; + } + 1 => function.locals[0].span.end += 1, + 2 => function.blocks[function.entry.index() as usize] + .statements + .clear(), + 3 => { + function + .resource_lowering + .as_mut() + .unwrap() + .original + .span + .end += 1 + } + 4 => { + function.resource_lowering.as_mut().unwrap().source = + hir::ResourceSourceArchive::empty() + } + _ => unreachable!(), + } + let before_changed = named(&changed, "main").clone(); + assert!( + reflected_fields::ordinary_sequence_transition( + named_mut(&mut changed, "main"), + &before_changed, + &edit, + types + ) + .is_err(), + "accepted ordinary Scope forgery {forgery}" + ); + } + const CUSTODY: &str = "namespace app\nfunction real_owner(view net: Network) returns nothing:\n resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error:\n return nothing\n resource_probe.close(token)\n return nothing\nfunction relay(view net: Network) returns nothing:\n real_owner(view net)\n return nothing\n"; + let custody_checked = super::super::tests::checked(CUSTODY, false); + let custody_types = &custody_checked.checked().interner; + let custody = lowered(&custody_checked); + for name in ["real_owner", "relay"] { + let before = named(&custody, name); + assert!(entry_scope::capture_needed( + &before.resource_lowering.as_ref().unwrap().original, + &before.resource_lowering.as_ref().unwrap().source + )); + assert!(has_execution_records(before, custody_types)); + assert!( + reflected_fields::ordinary_sequence_transition( + &mut before.clone(), + before, + &edit, + custody_types + ) + .is_err(), + "actual Resource family entered ordinary route" + ); + } +} diff --git a/crates/jett_mir/src/resource_ownership/execution_closure.rs b/crates/jett_mir/src/resource_ownership/execution_closure.rs new file mode 100644 index 00000000..eccf4e39 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_closure.rs @@ -0,0 +1,238 @@ +//! Derived only after the whole original checked HIR archive is authenticated. +//! A current body or signature alone can never add a function to this family. +use super::*; + +#[derive(Debug, Clone, Default)] +pub(crate) struct ResourceExecutionClosure { + functions: BTreeSet, + metadata_handlers: BTreeSet, + graph_analysis: Option>, +} +impl PartialEq for ResourceExecutionClosure { + fn eq(&self, other: &Self) -> bool { + self.functions == other.functions + && self.metadata_handlers == other.metadata_handlers + && match (&self.graph_analysis, &other.graph_analysis) { + (None, None) => true, + (Some(left), Some(right)) => left.same_source(right), + _ => false, + } + } +} +impl Eq for ResourceExecutionClosure {} +impl ResourceExecutionClosure { + pub(super) fn from_archive( + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result { + let analysis = std::sync::Arc::new(graph_metadata::OriginalGraphAnalysis::prepare( + archive, types, + )?); + Self::from_analysis(archive, types, analysis) + } + + /// Recompute closure membership from original bodies while retaining only + /// the immutable equations authenticated for this exact execution archive. + pub(super) fn reconstructed( + &self, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result { + let analysis = self + .graph_analysis + .as_ref() + .ok_or("graph storage lost its private original archive analysis")? + .clone(); + Self::from_analysis(archive, types, analysis) + } + + fn from_analysis( + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + analysis: std::sync::Arc, + ) -> Result { + analysis.validate_archive(archive, types)?; + let mut family = Self::from_original(archive.execution_functions(), types, archive)?; + let mut candidates = BTreeSet::new(); + // A completed empty Graph table cannot retire any custody function. + // Keep independent closure reconstruction and archive/type checks above. + let metadata_functions = if analysis.has_rows() { + archive.execution_functions() + } else { + &[] + }; + for function in metadata_functions { + let metadata = analysis.capture(function, archive, types)?; + if !metadata.needed() { + continue; + } + let mut pure_metadata = (!resource_type_pending(types, function.return_type) + || metadata.original_storage_for_return().is_some()) + && function.locals.iter().all(|local| { + !resource_type_pending(types, local.ty) + || metadata.original_storage_for_local(local).is_some() + }); + walk::hir_block(&function.body, &mut |value| { + pure_metadata &= (!resource_type_pending(types, value.ty) + || metadata.original_storage_for_expression(value).is_some()) + && !matches!( + value.kind, + hir::ExpressionKind::ResourceInvoke { .. } + | hir::ExpressionKind::ResourceHookValue { .. } + ) + && !matches!(&value.kind,hir::ExpressionKind::ActorGraphStep {instruction,..} + if matches!(instruction.operation(),hir::ActorGraphOperation::HookBox {..})); + }); + if pure_metadata { + candidates.insert(function.id.index()); + } + } + // Metadata helpers can call each other, but an edge to any remaining + // custody function keeps the exact original caller in that family. + loop { + let mut retained = Vec::new(); + for function in archive + .execution_functions() + .iter() + .filter(|function| candidates.contains(&function.id.index())) + { + let mut calls_custody = false; + walk::hir_block(&function.body, &mut |value| { + if let hir::ExpressionKind::Call { + function: target, .. + } = &value.kind + { + calls_custody |= + family.contains(*target) && !candidates.contains(&target.index()); + } + }); + if calls_custody { + retained.push(function.id.index()); + } + } + if retained.is_empty() { + break; + } + for index in retained { + candidates.remove(&index); + } + } + for index in candidates { + family.metadata_handlers.insert(index); + family.functions.remove(&index); + } + + family.functions.retain(|index| { + !archive + .required_only_function_ids() + .iter() + .any(|function| function.index() == *index) + }); + family.graph_analysis = Some(analysis); + Ok(family) + } + + pub(super) fn has_no_original_graph_rows(&self, archive: &hir::ResourceSourceArchive) -> bool { + self.graph_analysis + .as_ref() + .is_some_and(|analysis| analysis.has_no_rows_for(archive)) + } + + pub(super) fn original_graph( + &self, + original: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result<&graph_metadata::Witness, String> { + self.graph_analysis + .as_ref() + .ok_or("graph storage lost its private original archive analysis")? + .capture(original, archive, types) + } + + pub(super) fn metadata_handler(&self, function: FunctionId) -> bool { + self.metadata_handlers.contains(&function.index()) + } + pub(super) fn contains(&self, function: FunctionId) -> bool { + self.functions.contains(&function.index()) + } + pub(super) fn direct_call(&self, value: &Expression) -> bool { + matches!(&value.kind, hir::ExpressionKind::Call { function, .. } if self.contains(*function)) + } + pub(super) fn from_original( + functions: &[hir::Function], + types: &TypeInterner, + archive: &hir::ResourceSourceArchive, + ) -> Result { + let mut family = Self::default(); + for (index, function) in functions.iter().enumerate() { + if function.id.index() as usize != index { + return Err( + "Resource original call closure has no exact dense function identity".into(), + ); + } + let mut needed = custody_type(types, function.return_type) + || function + .locals + .iter() + .any(|local| custody_type(types, local.ty)); + walk::hir_block(&function.body, &mut |value| { + needed |= custody_type(types, value.ty) + || matches!( + value.kind, + hir::ExpressionKind::ResourceHookValue { .. } + | hir::ExpressionKind::ResourceInvoke { .. } + ); + }); + if needed { + family.functions.insert(function.id.index()); + } + } + loop { + let mut predecessors = Vec::new(); + for function in functions { + if family.contains(function.id) { + continue; + } + let mut calls = false; + let mut malformed = false; + let mut ordinal = 0; + walk::hir_block(&function.body, &mut |value| { + ordinal += 1; + if let hir::ExpressionKind::Call { + function: target, + ownership, + .. + } = &value.kind + && family.contains(*target) + { + calls = true; + let suite = archive.native_suite_call(function.id, value, ordinal); + malformed |= !matches!(ownership, hir::CallOwnership::Source(_)) + && !suite.is_some_and(|proof| { + proof.target() == *target + && proof.belongs_to_archive(archive) + && proof.matches(function.id, value, ordinal) + }); + } + }); + if malformed { + return Err( + "Resource execution predecessor has neither an original Source call certificate nor its exact private suite receipt" + .into(), + ); + } + if calls { + predecessors.push(function.id); + } + } + if predecessors.is_empty() { + break; + } + family + .functions + .extend(predecessors.into_iter().map(|function| function.index())); + } + Ok(family) + } +} diff --git a/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs b/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs new file mode 100644 index 00000000..248ab1ed --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_closure_tests.rs @@ -0,0 +1,349 @@ +use super::tests::{checked, lowered}; +use super::*; +const SOURCE: &str = include_str!("fixtures/connected_entry.jett"); +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} +#[test] +fn resource_original_execution_closure_captures_resource_free_source_predecessors() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + let entry = named(&program, "main"); + assert_eq!(entry.return_type, TypeInterner::NOTHING); + assert!( + entry + .locals + .iter() + .all(|local| !custody_type(types, local.ty)) + ); + let entry_plan = plan + .function(entry.id) + .expect("exact original predecessor Scope"); + assert_eq!(entry_plan.root_scope().role(), ResourceFrameRole::Scope); + assert!(entry_plan.provisional_return().is_none()); + let operations = entry_plan + .operations() + .iter() + .filter_map(|operation| { + if let ResourceOperationRole::InvokeSourceFunction { + function, formals, .. + } = operation.role() + { + Some((*function, formals)) + } else { + None + } + }) + .collect::>(); + assert_eq!(operations.len(), 1); + assert_eq!(operations[0].0, named(&program, "lifecycle").id); + assert_eq!(operations[0].1.len(), 1); + assert_eq!(operations[0].1[0].parameter_type(), TypeInterner::NETWORK); + assert_eq!(operations[0].1[0].access(), ParamMode::View); + } +} +#[test] +fn resource_execution_closure_cannot_be_removed_or_recaptured_from_edited_public_graphs() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let entry_id = named(&program, "main").id; + let mut changed = program.clone(); + changed.functions[entry_id.index() as usize].resource_lowering = None; + assert!( + validate_resource_ownership(&changed, types) + .unwrap_err() + .iter() + .any(|error| error + .message + .contains("initially authenticated Source constructor witness")) + ); + let mut hir = hir::lower_checked_resource_program(&checked).unwrap(); + let index = hir + .functions + .iter() + .position(|function| function.id == entry_id) + .unwrap(); + let original = hir.functions[index].body.clone(); + hir.functions[index].body.statements.clear(); + assert!( + lower(&hir, types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("original checked HIR archive")) + ); + hir.functions[index].body = original; + let closure = authenticate_original(&hir, types).unwrap(); + assert!(closure.contains(entry_id)); + } +} +#[test] +fn resource_sum_shell_constructors_and_failure_extraction_have_exact_distinct_plan_roles() { + const ABSENT: &str = "namespace app\nfunction discard(value: optional[resource_probe.TestHandle]) returns nothing:\n return nothing\nfunction main(net: Network) returns nothing:\n use resource_probe\n discard(resource_probe.empty())\n resource_probe.TestHandle token = resource_probe.create(view net, 3) handle error:\n return nothing\n resource_probe.close(token)\n return nothing\n"; + for release in [false, true] { + let checked = checked(ABSENT, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + assert!( + plan.functions() + .iter() + .flat_map(|function| function.operations()) + .any(|operation| matches!( + operation.role(), + ResourceOperationRole::CreateAbsentSum { .. } + )) + ); + assert!(plan.functions().iter().flat_map(|function| function.operations()).any(|operation| matches!(operation.role(), ResourceOperationRole::TakeFailureCompanion { failure, target, .. } if *failure == TypeInterner::STRING && target.ty == *failure))); + for function in plan.functions() { + for operation in function.operations() { + if let ResourceOperationRole::CreateAbsentSum { destination } = operation.role() { + assert!(matches!( + function.owner_slots()[destination.index()].shape(), + ResourceShape::Optional { .. } + )); + } + } + } + } +} + +#[test] +fn resource_free_nested_call_to_custody_family_keeps_exact_source_abi_region() { + const SOURCE: &str = "namespace app\nfunction label(view net: Network) returns int64:\n use resource_probe\n resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error:\n return 0\n resource_probe.close(token)\n return 1\nfunction main(net: Network) returns nothing:\n string text = \"{label(view net)}\"\n return nothing\n"; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, types).unwrap(); + let main = named(&program, "main"); + let label = named(&program, "label"); + let regions = main + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, .. }) => { + Some(main.resource_call_region(*region).unwrap()) + } + _ => None, + }) + .collect::>(); + assert_eq!(regions.len(), 1); + let region = regions[0]; + assert_eq!(region.function(), label.id); + assert_eq!(region.parent(), None); + assert_eq!(region.evaluation_order(), &[0]); + assert_eq!(region.actuals().len(), 1); + assert_eq!(region.actuals()[0].source_index(), 0); + assert_eq!(region.actuals()[0].parameter(), 0); + assert!(region.actuals()[0].ordinary().is_some()); + assert_eq!( + main.local(region.output().unwrap()).unwrap().ty, + TypeInterner::INT64 + ); + assert!(matches!(®ion.original().kind, + hir::ExpressionKind::Call { function, ownership: hir::CallOwnership::Source(_), .. } + if *function == label.id)); + let calls = plan + .function(main.id) + .unwrap() + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::InvokeSourceFunction { + function, formals, .. + } => Some((*function, formals)), + _ => None, + }) + .collect::>(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].0, label.id); + assert_eq!(calls[0].1.len(), 1); + assert_eq!(calls[0].1[0].parameter_type(), TypeInterner::NETWORK); + assert_eq!(calls[0].1[0].access(), ParamMode::View); + assert_eq!(calls[0].1[0].effect(), ResourceArgumentEffect::RetainBorrow); + } +} + +const ORDINARY_ENDPOINT_ORDER: &str = r#"namespace app +function absent() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +function one() returns int64: + list[optional[resource_probe.TestHandle]] values = absent() + return 1 +function two() returns int64: + list[optional[resource_probe.TestHandle]] values = absent() + return 2 +function combine(first: int64, second: int64) returns int64: + list[optional[resource_probe.TestHandle]] values = absent() + return first + second +function ordered() returns bool: + return one() < two() +function named_ordered() returns int64: + return combine(second: one(), first: two()) +function logical_and(flag: bool) returns bool: + return flag and one() == 1 +function logical_or(flag: bool) returns bool: + return flag or two() == 2 +function main() returns nothing: + bool comparison = ordered() + int64 named = named_ordered() + bool conjunction = logical_and(false) + bool disjunction = logical_or(true) + return nothing +"#; + +fn invoked_regions(function: &Function) -> Vec<&ResourceCallRegion> { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::ResourceCall(ResourceCallNode::Invoke { region, .. }) => { + Some(function.resource_call_region(*region).unwrap()) + } + _ => None, + }) + .collect() +} + +#[test] +fn ordinary_resource_family_endpoints_preserve_comparison_and_named_actual_order() { + for release in [false, true] { + let checked = checked(ORDINARY_ENDPOINT_ORDER, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + let one = named(&program, "one").id; + let two = named(&program, "two").id; + let combine = named(&program, "combine").id; + let ordered = named(&program, "ordered"); + let calls = invoked_regions(ordered); + assert_eq!( + calls.iter().map(|call| call.function()).collect::>(), + [one, two] + ); + for call in &calls { + assert_eq!(call.parent(), None); + assert!(call.actuals().is_empty()); + assert_eq!( + ordered.local(call.output().unwrap()).unwrap().ty, + TypeInterner::INT64 + ); + assert!(matches!( + call.original().kind, + hir::ExpressionKind::Call { + ownership: hir::CallOwnership::Source(_), + .. + } + )); + } + let named_call = named(&program, "named_ordered"); + let calls = invoked_regions(named_call); + assert_eq!( + calls.iter().map(|call| call.function()).collect::>(), + [one, two, combine] + ); + let outer = calls[2]; + assert_eq!(outer.parent(), None); + assert_eq!(outer.evaluation_order(), [1, 0]); + assert_eq!( + outer + .actuals() + .iter() + .map(|actual| (actual.source_index(), actual.parameter())) + .collect::>(), + [(0, 1), (1, 0)] + ); + for (child, actual) in calls[..2].iter().zip(outer.actuals()) { + assert_eq!(child.parent(), Some(outer.id())); + assert!(actual.ordinary().is_some()); + assert!( + matches!(actual.endpoint().kind, hir::ExpressionKind::Local(local) if Some(local) == child.output()) + ); + } + let outer_id = outer.id(); + let owner = named_call.id; + let mut corrupted = program.clone(); + let function = &mut corrupted.functions[owner.index() as usize]; + let stage = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::ResourceCall(ResourceCallNode::Stage { + region, + source_index: 0, + parameter, + .. + }) if *region == outer_id => Some(parameter), + _ => None, + }) + .unwrap(); + *stage = 0; + function.resource_lowering.as_mut().unwrap().blocks = function.blocks.clone(); + assert!(validate_resource_ownership(&corrupted, types).is_err()); + } +} + +#[test] +fn ordinary_resource_family_rhs_stays_on_the_selected_short_circuit_branch() { + for release in [false, true] { + let checked = checked(ORDINARY_ENDPOINT_ORDER, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for (name, target, and) in [("logical_and", "one", true), ("logical_or", "two", false)] { + let function = named(&program, name); + let calls = invoked_regions(function); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].function(), named(&program, target).id); + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &function.blocks[0].terminator.kind + else { + panic!("checked flag must select the RHS or bypass"); + }; + assert!(matches!(condition.kind, hir::ExpressionKind::Local(_))); + let (selected, bypass) = if and { + (*then_block, *else_block) + } else { + (*else_block, *then_block) + }; + assert_eq!(calls[0].begin().block, selected); + assert_eq!(calls[0].invocation().unwrap().0.block, selected); + let bypass = &function.blocks[bypass.index() as usize]; + assert!( + bypass + .statements + .iter() + .all(|statement| !matches!(statement.kind, StatementKind::ResourceCall(_))) + ); + assert!(bypass.statements.iter().any(|statement| matches!(&statement.kind, + StatementKind::Let { value: Expression { kind: hir::ExpressionKind::Bool(value), .. }, .. } if *value == !and))); + let TerminatorKind::Goto(right_join) = + function.blocks[selected.index() as usize].terminator.kind + else { + panic!("RHS must join after its invocation"); + }; + let TerminatorKind::Goto(bypass_join) = bypass.terminator.kind else { + panic!("bypass must join without invoking RHS"); + }; + assert_eq!(right_join, bypass_join); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs b/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs new file mode 100644 index 00000000..943ebfe9 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/execution_frames_tests.rs @@ -0,0 +1,160 @@ +use super::tests::{checked, lowered}; +use super::*; + +const SOURCES: &[&str] = &[ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/14_first_handle_success.jett" + ), +]; + +#[test] +fn staged_scope_sum_sites_and_extracts_use_exact_active_execution_frame() { + for release in [false, true] { + for &source in SOURCES { + let checked = checked(source, release); + let program = lowered(&checked); + let main = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let flow = plan.function(main.id).unwrap(); + let begin = flow + .operations() + .iter() + .find(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .unwrap(); + let active = begin.frame(); + let scope = flow.root_scope().id(); + assert_ne!(active, scope); + assert_eq!( + flow.execution_frame(begin.site().block(), begin.site().position()), + Some(scope) + ); + let mut tags = 0; + let mut takes = 0; + for block in &main.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::SumTag { source, .. } = statement.kind { + let Some(slot) = flow.owner_slots().iter().find(|slot| + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == source)) else { continue }; + tags += 1; + assert_eq!(slot.frame(), scope); + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Statement(index)), + Some(active) + ); + let transfer = flow.operations().iter().find(|operation| + matches!(operation.role(), ResourceOperationRole::Transfer { destination, .. } if *destination == slot.id())).unwrap(); + assert_eq!(transfer.frame(), active); + } + if let StatementKind::SumTake { + source, success, .. + } = statement.kind + { + let Some(shell) = flow.owner_slots().iter().find(|slot| + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == source)) else { continue }; + takes += 1; + let operation = flow + .operations() + .iter() + .find(|operation| { + operation.site().block() == block.id + && operation.site().position() + == ResourcePosition::Statement(index) + && matches!( + operation.role(), + ResourceOperationRole::SumTake { .. } + | ResourceOperationRole::TakeFailureCompanion { .. } + ) + }) + .unwrap(); + assert_eq!(operation.frame(), active); + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Statement(index)), + Some(active) + ); + assert_eq!(shell.frame(), scope); + if success { + let ResourceOperationRole::SumTake { destination, .. } = + operation.role() + else { + panic!("success extraction") + }; + assert_eq!(flow.owner_slots()[destination.index()].frame(), scope); + } + } + } + if matches!(block.terminator.kind, TerminatorKind::Return(_)) { + assert_eq!( + flow.execution_frame(block.id, ResourcePosition::Terminator), + Some(scope) + ); + } + } + assert_eq!((tags, takes), (1, 2)); + assert_eq!( + flow.execution_frame(BlockId(u32::MAX), ResourcePosition::Terminator), + None + ); + assert_eq!( + flow.execution_frame(main.entry, ResourcePosition::Statement(usize::MAX)), + None + ); + } + } +} + +#[test] +fn staged_sum_frame_projection_cannot_authorize_copied_or_altered_current_graph() { + for release in [false, true] { + let checked = checked(SOURCES[0], release); + let original = lowered(&checked); + let main = original + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let (block, index) = main + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .enumerate() + .find_map(|(index, statement)| { + matches!(statement.kind, StatementKind::SumTag { .. }) + .then_some((block.id, index)) + }) + }) + .unwrap(); + for copied in [false, true] { + let mut changed = original.clone(); + let current = &mut changed.functions[main.id.index() as usize]; + if copied { + let statement = current.blocks[block.index() as usize].statements[index].clone(); + current.blocks[block.index() as usize] + .statements + .insert(index, statement); + } else { + let StatementKind::SumTag { source, .. } = + &mut current.blocks[block.index() as usize].statements[index].kind + else { + unreachable!() + }; + *source = current.params[0].local; + } + assert!(validate_resource_ownership(&changed, &checked.checked().interner).is_err()); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett b/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett new file mode 100644 index 00000000..3a1d96cf --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/17_absent_aggregate_shapes.jett @@ -0,0 +1,78 @@ +namespace app +struct TokenEnvelope: + tokens: list[resource_probe.TestHandle] + fallback: optional[resource_probe.TestHandle] + marker: int64 +struct TokenBox[T]: + items: list[T] +type PositiveMarker = int64 where value > 0 +struct RefinedEnvelope: + marker: PositiveMarker + tokens: list[resource_probe.TestHandle] +enum TokenChoice: + empty + vacant(marker: int64, tokens: list[resource_probe.TestHandle]) + occupied(token: resource_probe.TestHandle) +machine TokenState: + states: + vacant(fallback: optional[resource_probe.TestHandle], marker: string) + waiting(fallback: optional[resource_probe.TestHandle], marker: string) + occupied(token: resource_probe.TestHandle) + transitions: + vacant to waiting + vacant to occupied +export function empty_tokens() returns list[resource_probe.TestHandle]: + return list() +export function absent_tokens() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +export function empty_map() returns map[string, resource_probe.TestHandle]: + return map() +export function absent_map() returns map[string, optional[resource_probe.TestHandle]]: + return map("empty": none) +export function empty_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return ok(list()) +export function failed_result_tokens() returns result[list[resource_probe.TestHandle], string]: + return fail("empty") +export function absent_envelope() returns TokenEnvelope: + return TokenEnvelope(tokens: list(), fallback: none, marker: 17) +export function absent_box() returns TokenBox[resource_probe.TestHandle]: + return TokenBox[resource_probe.TestHandle](items: list()) +export function empty_choice() returns TokenChoice: + return TokenChoice.empty +export function absent_choice() returns TokenChoice: + return TokenChoice.vacant(17, list()) +export function absent_state() returns TokenState: + return TokenState(vacant, none, "empty") +export function absent_exact_state() returns TokenState at vacant: + return TokenState(vacant, none, "empty") +export function refined_shape_contract(view value: RefinedEnvelope) returns nothing: + return nothing +function absent_collection_controls() returns int64: + list[resource_probe.TestHandle] empty = empty_tokens() + list[optional[resource_probe.TestHandle]] absent = absent_tokens() + map[string, resource_probe.TestHandle] vacant = empty_map() + map[string, optional[resource_probe.TestHandle]] missing = absent_map() + result[list[resource_probe.TestHandle], string] successful = empty_result_tokens() + result[list[resource_probe.TestHandle], string] failure = failed_result_tokens() + return 17 +function absent_nominal_controls() returns int64: + TokenEnvelope envelope = absent_envelope() + TokenBox[resource_probe.TestHandle] box = absent_box() + TokenChoice empty = empty_choice() + TokenChoice vacant = absent_choice() + TokenState state = absent_state() + TokenState at vacant exact = absent_exact_state() + return 17 +int64 absent_namespace_value = comptime absent_nominal_controls() +export function absent_required_controls() returns int64: + int64 collections = comptime absent_collection_controls() + int64 nominal = comptime absent_nominal_controls() + return collections + nominal +verify absent_aggregate_verify: + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 +property absent_aggregate_property: + given marker: int64 + assert absent_collection_controls() == 17 + assert absent_nominal_controls() == 17 + assert marker == marker diff --git a/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett b/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett new file mode 100644 index 00000000..ecccd22d --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/connected_entry.jett @@ -0,0 +1,14 @@ +namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = resource_probe.borrow(view net, view moved) + resource_probe.close(moved) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing diff --git a/crates/jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett b/crates/jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett new file mode 100644 index 00000000..f37aafa7 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett @@ -0,0 +1,9 @@ +namespace app +function reseat_optional() returns nothing: + use resource_probe + mutable optional[resource_probe.TestHandle] token = none + token = token + return nothing +export function main(net: Network) returns nothing: + reseat_optional() + return nothing diff --git a/crates/jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett b/crates/jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett new file mode 100644 index 00000000..cab99fc6 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett @@ -0,0 +1,9 @@ +namespace app +function reseat_result() returns nothing: + use resource_probe + mutable result[resource_probe.TestHandle, string] token = fail("kept") + token = token + return nothing +export function main(net: Network) returns nothing: + reseat_result() + return nothing diff --git a/crates/jett_mir/src/resource_ownership/flow.rs b/crates/jett_mir/src/resource_ownership/flow.rs new file mode 100644 index 00000000..4bf5edbd --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/flow.rs @@ -0,0 +1,1654 @@ +//! Independent current-CFG occupancy proof. Abstract states are metadata, +//! never runtime tokens and never an ordinary linearity classifier. +use super::*; +mod normalized; +use hir::ExpressionKind as E; +use jett_typecheck::{ + CheckedCalleeAccess as Access, CheckedCallerEffect as Effect, CheckedCallerSyntax as Syntax, +}; +use normalized::{ActiveCall, PreparedCall}; +use std::collections::VecDeque; + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Value { + shape: ResourceShape, + occupancy: ResourceOccupancy, + owner: Option, + loan: Option, +} +#[derive(Clone, Debug, PartialEq, Eq)] +struct State { + values: Vec>, + moved: Vec, + descriptors: Vec>, + named_callables: Vec>, + tags: BTreeMap, + guards: BTreeMap, + leases: BTreeSet, + aborted: bool, + calls: Vec, +} + +fn expression_shape( + program: &Program, + function: &Function, + types: &TypeInterner, + value: &Expression, +) -> Result, String> { + if crate::native_graph_storage_for_expression(function, types, value).is_some() { + Ok(None) + } else { + shape(&program.resource_manifest, types, value.ty) + } +} +fn local_shape( + program: &Program, + function: &Function, + types: &TypeInterner, + local: &Local, +) -> Result, String> { + if crate::native_graph_storage_for_local(function, types, local).is_some() { + Ok(None) + } else { + shape(&program.resource_manifest, types, local.ty) + } +} +fn formal_shape( + program: &Program, + function: &Function, + types: &TypeInterner, + formal: &Param, +) -> Result, String> { + if crate::native_graph_storage_for_formal(function, types, formal).is_some() { + Ok(None) + } else { + shape(&program.resource_manifest, types, formal.ty) + } +} +fn return_shape( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result, String> { + if crate::native_graph_storage_for_return(function, types).is_some() { + Ok(None) + } else { + shape(&program.resource_manifest, types, function.return_type) + } +} + +type Key = (u32, u8, usize, usize); +fn key(site: ResourceSite, ordinal: usize) -> Key { + match site.position { + ResourcePosition::Statement(index) => (site.block.index(), 0, index, ordinal), + ResourcePosition::Terminator => (site.block.index(), 1, 0, ordinal), + } +} +struct Analysis<'p> { + program: &'p Program, + function: &'p Function, + types: &'p TypeInterner, + plan: ResourceFunctionPlan, + local_slots: Vec>, + expression_slots: BTreeMap<(Key, Option), ResourceOwnerSlotId>, + operation_frames: BTreeMap, + loan_sites: BTreeMap<(Key, usize, ResourceLoanSource), ResourceLoanId>, + site: ResourceSite, + ordinal: usize, + expression: Option, + emit: bool, + active_frame: ResourceFrameId, + return_frame: Option, + normalized_calls: BTreeMap, +} +impl<'p> Analysis<'p> { + fn record_execution_frame(&mut self) -> Result<(), String> { + if self.emit { + if self + .plan + .execution_frames + .iter() + .any(|(site, _)| *site == self.site) + { + return Err("Resource execution frame has duplicate current-site authority".into()); + } + self.plan + .execution_frames + .push((self.site, self.active_frame)); + } + Ok(()) + } + fn operation(&mut self, frame: ResourceFrameId, role: ResourceOperationRole) { + if self.emit { + let id = ResourceOperationId(self.plan.operations.len()); + self.plan.operations.push(ResourceOperation { + id, + frame, + site: self.site, + ordinal: self.ordinal, + role, + expression: self.expression, + named_indirect: None, + indirect_hook: None, + }); + } + } + fn frame(&mut self, ordinal: usize, role: ResourceFrameRole) -> ResourceFrameId { + let entry = key(self.site, ordinal); + if let Some(id) = self.operation_frames.get(&entry) { + return *id; + } + let id = ResourceFrameId(self.plan.frames.len()); + self.plan.frames.push(ResourceFrame { + id, + role, + site: self.site, + parent: Some(self.active_frame), + ordinal, + }); + self.operation_frames.insert(entry, id); + id + } + fn result_slot( + &mut self, + ordinal: usize, + shape: ResourceShape, + frame: ResourceFrameId, + ) -> ResourceOwnerSlotId { + let entry = (key(self.site, ordinal), None); + if let Some(id) = self.expression_slots.get(&entry) { + return *id; + } + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage: ResourceSlotStorage::Expression { + site: self.site, + ordinal, + }, + }); + self.expression_slots.insert(entry, id); + id + } + fn argument_slot( + &mut self, + ordinal: usize, + parameter: usize, + shape: ResourceShape, + frame: ResourceFrameId, + ) -> ResourceOwnerSlotId { + let entry = (key(self.site, ordinal), Some(parameter)); + if let Some(id) = self.expression_slots.get(&entry) { + return *id; + } + let id = ResourceOwnerSlotId(self.plan.slots.len()); + self.plan.slots.push(ResourceOwnerSlot { + id, + frame, + shape, + storage: ResourceSlotStorage::Argument { + site: self.site, + call_ordinal: ordinal, + parameter, + }, + }); + self.expression_slots.insert(entry, id); + id + } + fn loan( + &mut self, + ordinal: usize, + parameter: usize, + frame: ResourceFrameId, + source: ResourceLoanSource, + ) -> Result { + let entry = (key(self.site, ordinal), parameter, source); + if let Some(id) = self.loan_sites.get(&entry) { + return Ok(*id); + } + let id = ResourceLoanId(self.plan.loans.len()); + let shape = match source { + ResourceLoanSource::CarrierSumProjection { .. } => return Err( + "carrier sum adapters are issued only by the exact carrier projection constructor" + .into(), + ), + ResourceLoanSource::Owner(owner) => self.plan.slots[owner.index()].shape.clone(), + ResourceLoanSource::ProjectedSumPayload { parent, .. } => ResourceShape::Plain { + kind: self.plan.loans[parent.index()].shape.kind().clone(), + }, + ResourceLoanSource::IncomingViewFormal { .. } => { + unreachable!("incoming formal loans are initialized from their exact typed header") + } + }; + self.plan.loans.push(ResourceLoan { + id, + frame, + source, + shape, + parameter: (parameter != usize::MAX).then_some(parameter), + }); + self.loan_sites.insert(entry, id); + Ok(id) + } + fn borrow(&mut self, frame: ResourceFrameId, loan: ResourceLoanId) { + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::BorrowSum { loan } + } else { + ResourceOperationRole::Borrow { loan } + }; + self.operation(frame, role); + } + fn end_borrow(&mut self, frame: ResourceFrameId, loan: ResourceLoanId) { + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::EndSumBorrow { loan } + } else { + ResourceOperationRole::EndBorrow { loan } + }; + self.operation(frame, role); + } + fn clear(&self, state: &mut State, local: LocalId) { + state + .tags + .retain(|target, source| *target != local.index() && *source != local); + state.guards.remove(&local.index()); + state.descriptors[local.index() as usize] = None; + state.named_callables[local.index() as usize] = None; + } + fn descriptor_value(&self, state: &State, value: &Expression) -> Option { + let expected = self.plan.descriptor_value(value)?; + match &value.kind { + E::Local(local) => (state.descriptors[local.index() as usize].as_ref() + == Some(expected)) + .then(|| expected.clone()), + E::Clone(inner) => (self.descriptor_value(state, inner).as_ref() == Some(expected)) + .then(|| expected.clone()), + E::Call { .. } | E::ResourceHookValue { .. } => Some(expected.clone()), + _ => None, + } + } + fn named_binding(&self, state: &State, local: LocalId, value: &Expression) -> Option { + let witness = self.function.resource_lowering.as_ref()?; + witness + .named_callables + .iter() + .enumerate() + .find_map(|(index, binding)| { + if binding.local() != local + || !crate::breakpoint_regions::expressions_equal(binding.initializer(), value) + { + return None; + } + if let Some(parent) = binding.parent() { + let parent_local = witness.named_callables[parent].local(); + (state.named_callables[parent_local.index() as usize] == Some(parent)) + .then_some(index) + } else { + matches!(value.kind, E::FunctionRef(_)).then_some(index) + } + }) + } + fn named_indirect_target( + &self, + state: &State, + expression: &Expression, + ) -> Result, String> { + let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &expression.kind + else { + return Ok(None); + }; + let E::Local(local) = callee.kind else { + return Ok(None); + }; + let Some(index) = state.named_callables[local.index() as usize] else { + return Ok(None); + }; + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("Resource named call lost its constructor witness")?; + let binding = witness + .named_callables + .get(index) + .ok_or("Resource named call has no exact private producer")?; + let proof = binding.proof(); + let association = witness + .calls + .iter() + .find(|association| { + crate::breakpoint_regions::expressions_equal(&association.current, expression) + }) + .ok_or("Resource named call has no exact original Indirect occurrence")?; + let E::IndirectCall { + callee: original_callee, + .. + } = &association.original.kind + else { + return Err("Resource named call changed its original invocation kind".into()); + }; + if binding.local() != local + || !binding.original_callee_matches(original_callee) + || callee.ty != proof.signature_type() + || source.target + != (hir::CallTarget::Indirect { + signature_type: proof.signature_type(), + }) + || source.bridge != hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err("Resource named call differs from its exact immutable callee or original Source signature".into()); + } + let target = self + .program + .functions + .get(proof.function().index() as usize) + .filter(|target| target.id == proof.function()) + .ok_or("Resource named call lost its selected declaration")?; + let selected = target + .resource_lowering + .as_ref() + .ok_or("Resource named call has no selected original Source body")?; + if selected.source != witness.source || target.identity != *proof.identity() { + return Err("Resource named call selected a foreign archive or declaration".into()); + } + Ok(Some(proof.clone())) + } + fn read( + &self, + state: &mut State, + local: LocalId, + taking: bool, + ) -> Result, String> { + let index = local.index() as usize; + let metadata = self + .function + .local(local) + .ok_or("Resource read has no exact dense local header")?; + if local_shape(self.program, self.function, self.types, metadata)?.is_none() { + return Ok(None); + } + let value = state.values[index] + .as_ref() + .ok_or("Resource owner is uninitialized, moved or retired at this current CFG site")? + .clone(); + if taking { + if value.loan.is_some() { + return Err( + "Resource borrowed formal or alias cannot become an owned value".into(), + ); + } + let owner = value.owner; + if state.leases.iter().any(|loan| { + Some(self.plan.loans[loan.0].source) == owner.map(ResourceLoanSource::Owner) + }) { + return Err("Resource owner cannot move, close or replace while its exact lease remains live".into()); + } + state.values[index] = None; + state.moved[index] = true; + self.clear(state, local); + } + Ok(Some(value)) + } + fn store( + &mut self, + state: &mut State, + local: LocalId, + value: Option, + replace: bool, + self_rebind: bool, + ) -> Result<(), String> { + let index = local.index() as usize; + let metadata = self + .function + .local(local) + .ok_or("Resource destination has no exact local header")?; + let expected = local_shape(self.program, self.function, self.types, metadata)?; + let Some(expected) = expected else { + if value.is_some() { + return Err("Resource custody cannot be stored in an ordinary destination".into()); + } + self.clear(state, local); + return Ok(()); + }; + let mut value = + value.ok_or("Resource destination has no checked owning or resident-loan producer")?; + if value.shape != expected { + return Err("Resource destination changes its nominal kind or active shape".into()); + } + if self.function.is_view_local(local) { + if replace { + return Err( + "Resource view alias replacement requires its separate lease transport".into(), + ); + } + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied { + return Err("pending ResourceOwnershipPlan: conditional sum borrowing needs exact occupied-arm lease transport".into()); + } + if value.loan.is_none() { + let source = value + .owner + .ok_or("Resource alias has no exact owned backing")?; + let loan = self.loan( + self.ordinal, + usize::MAX, + ResourceFrameId(0), + ResourceLoanSource::Owner(source), + )?; + state.leases.insert(loan); + self.borrow(ResourceFrameId(0), loan); + value.loan = Some(loan); + } + state.values[index] = Some(value); + state.moved[index] = false; + self.clear(state, local); + return Ok(()); + } + if value.loan.is_some() { + return Err("Resource owning storage cannot adopt a borrowed carrier".into()); + } + let destination = self.local_slots[index] + .ok_or("Resource owning destination lacks its dedicated slot")?; + if replace && !metadata.mutable { + return Err("Resource assignment requires its exact mutable owning header".into()); + } + if let Some(old) = state.values[index].as_ref() { + if !replace || !metadata.mutable { + return Err( + "Resource live destination requires its exact mutable replacement constructor" + .into(), + ); + } + if state.leases.iter().any(|loan| { + self.plan.loans[loan.0].source == ResourceLoanSource::Owner(destination) + }) { + return Err("Resource replacement has a live old-holder lease".into()); + } + self.operation( + ResourceFrameId(0), + ResourceOperationRole::Replace { + destination, + replacement: value + .owner + .ok_or("Resource replacement lacks its evaluated RHS owner slot")?, + old: old.occupancy, + }, + ); + self.operation( + ResourceFrameId(0), + ResourceOperationRole::Drop { + source: destination, + occupancy: old.occupancy, + }, + ); + } + if let Some(source) = value.owner { + if source != destination { + self.operation( + self.active_frame, + ResourceOperationRole::Transfer { + source, + destination, + }, + ); + } else if replace { + // An absent sum owns its shell and any ordinary failure payload. + if !self_rebind + || !matches!( + (&value.shape, value.occupancy), + (ResourceShape::Plain { .. }, ResourceOccupancy::Occupied) + | ( + ResourceShape::Optional { .. } | ResourceShape::Result { .. }, + ResourceOccupancy::Empty + | ResourceOccupancy::Occupied + | ResourceOccupancy::Conditional, + ) + ) + || state.values[index].is_some() + { + return Err( + "Resource self-rebind needs its exact mutable owned Local reseat".into(), + ); + } + self.operation( + ResourceFrameId(0), + ResourceOperationRole::SelfRebind { slot: destination }, + ); + } + } + value.owner = Some(destination); + state.values[index] = Some(value); + state.moved[index] = false; + self.clear(state, local); + Ok(()) + } + fn finish(&mut self, state: &mut State, frame: ResourceFrameId, outcome: ResourceCompletion) { + // Incoming formal records denote resident caller leases, not child-owned tokens. + let loans = state.leases.iter().copied().collect::>(); + for loan in loans.into_iter().rev() { + self.end_borrow(frame, loan); + state.leases.remove(&loan); + } + for value in state.values.iter_mut().rev() { + if let Some(value) = value.take() { + if value.loan.is_none() + && let Some(source) = value.owner + { + self.operation( + frame, + ResourceOperationRole::Drop { + source, + occupancy: value.occupancy, + }, + ); + } + } + } + self.operation(frame, ResourceOperationRole::Complete { outcome }); + } + fn lexical_exit(&mut self, state: &mut State) -> Result<(), String> { + let Some(exit) = self + .function + .resource_lexical_exit(self.site.block, self.site.position)? + else { + return Err("Resource lexical retirement has no exact constructor exit".into()); + }; + let witness = self + .function + .resource_lowering + .as_ref() + .ok_or("Resource lexical retirement has no Source witness")?; + let mut clear_locals = Vec::new(); + let mut loans = BTreeSet::new(); + let mut owned_locals = Vec::new(); + for original in exit.declarations() { + if let Some(local) = owned_declarations::owner_current_local(witness, original) { + clear_locals.push(local); + owned_locals.push(local); + continue; + } + let row = witness + .borrowed_sums + .iter() + .find(|row| row.original_local() == original) + .ok_or("Resource lexical retirement lost an exact original alias")?; + for local in row.retirement_locals() { + if !clear_locals.contains(&local) { + clear_locals.push(local); + } + if let Some(value) = &state.values[local.index() as usize] + && let Some(loan) = value.loan + && state.leases.contains(&loan) + { + // A forwarded incoming/outer lease is never adopted by this declaration. + let same_site = |site: Key, current: ResourceSite| { + let expected = key(current, 0); + (site.0, site.1, site.2) == (expected.0, expected.1, expected.2) + }; + let created_here = self.loan_sites.iter().any(|((site, _, _), id)| { + *id == loan + && (same_site(*site, row.initialize_site()) + || same_site(*site, row.success_site())) + }); + if created_here { + loans.insert(loan); + } + } + } + } + let start = self.plan.operations.len(); + for loan in loans.into_iter().rev() { + if state.leases.iter().any(|child| matches!(self.plan.loans[child.index()].source, ResourceLoanSource::ProjectedSumPayload { parent, .. } if parent == loan)) { + return Err("Resource lexical parent retirement has a live child outside its exact exit".into()); + } + self.end_borrow(self.active_frame, loan); + state.leases.remove(&loan); + } + for local in owned_locals { + let index = local.index() as usize; + if let Some(value) = state.values[index].take() { + if value.loan.is_some() { + return Err("Resource owned lexical declaration became a borrowed alias".into()); + } + let source = value + .owner + .ok_or("Resource lexical owner lost its exact slot")?; + if state.leases.iter().any(|loan| { + self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(source) + }) { + return Err( + "Resource lexical owner has a live lease outside its exact exit".into(), + ); + } + self.operation( + self.plan.slots[source.index()].frame, + ResourceOperationRole::Drop { + source, + occupancy: value.occupancy, + }, + ); + } + } + for local in &clear_locals { + state.values[local.index() as usize] = None; + state.moved[local.index() as usize] = false; + self.clear(state, *local); + } + if self.emit { + self.plan.lexical_exits.push(ResourceLexicalExitPlan::new( + self.site, + exit.kind(), + (start..self.plan.operations.len()) + .map(ResourceOperationId) + .collect(), + clear_locals, + )); + } + Ok(()) + } + fn expression( + &mut self, + state: &mut State, + expression: &Expression, + taking: bool, + ) -> Result, String> { + let previous = self + .expression + .replace(std::ptr::from_ref(expression).addr()); + let result = self.expression_inner(state, expression, taking); + self.expression = previous; + result + } + fn expression_inner( + &mut self, + state: &mut State, + expression: &Expression, + taking: bool, + ) -> Result, String> { + if state.aborted { + return Ok(None); + } + self.ordinal += 1; + let ordinal = self.ordinal; + let expected = if crate::actor_graph::native_graph_metadata_expression( + self.function, + self.types, + expression, + ) { + None + } else { + expression_shape(self.program, self.function, self.types, expression)? + }; + match &expression.kind { + E::Local(local) => { + if self.plan.descriptor_value(expression).is_some() && self.descriptor_value(state, expression).is_none() { + return Err("Resource descriptor has no live exact producer at this current CFG read".into()); + } + self.read(state, *local, taking) + } + E::Clone(inner) if self.plan.descriptor_value(expression).is_some() => { + if self.descriptor_value(state, expression).is_none() { return Err("Resource descriptor alias lost its current exact producer".into()); } + self.expression(state, inner, false) + } + E::View(inner) => { + if taking && expected.is_some() { return Err("Resource View cannot be adopted as an owned result".into()); } + self.expression(state, inner, false) + } + E::ActorGraphStep {instruction,args} if matches!(instruction.operation(),hir::ActorGraphOperation::HookBox {..}) => { + let hir::ActorGraphOperation::HookBox {hook,..}=instruction.operation() else {unreachable!()}; + if self.plan.descriptor_value(expression) != Some(hook) + || !self.program.resource_manifest.contains_hook(hook) || expression.ty != hook.function_type() { + return Err("graph metadata descriptor lost its exact required projection row".into()); + } + self.operation(ResourceFrameId(0), ResourceOperationRole::Descriptor {hook:hook.clone()}); + if self.emit { + let id=self.plan.operations.last().ok_or("graph hook Descriptor row was not emitted")?.id; + self.plan.graph_descriptors.push((instruction.clone(),id)); + } + for arg in args { if self.expression(state,arg,false)?.is_some() {return Err("graph import ticket acquired Resource custody".into());} } + Ok(None) + } + E::ResourceHookValue { hook } => { + if !self.program.resource_manifest.contains_hook(hook) || expression.ty != hook.function_type() { return Err("Resource descriptor differs from its exact original manifest signature".into()); } + self.operation(ResourceFrameId(0), ResourceOperationRole::Descriptor { hook: hook.clone() }); + Ok(None) + } + E::ResourceInvoke { hook, args, evaluation_order, ownership } => self.invocation(state, expression, Some(hook.clone()), None, args, evaluation_order, ownership, ordinal, None), + E::Call { function, args, evaluation_order, ownership } => self.invocation(state, expression, None, Some(*function), args, evaluation_order, ownership, ordinal, None), + E::IndirectCall { callee, args, evaluation_order, ownership } => { + let hook = self.descriptor_value(state, callee); + if expected.is_some() || args.iter().any(|arg| resource_type_pending(self.types, arg.ty)) { + if let Some(hook) = hook { + if callee.ty != hook.function_type() { return Err("Resource indirect descriptor has a different exact signature".into()); } + self.invocation(state, expression, Some(hook), None, args, evaluation_order, ownership, ordinal, None) + } else if let Some(proof) = self.named_indirect_target(state, expression)? { + self.invocation(state, expression, None, Some(proof.function()), args, evaluation_order, ownership, ordinal, Some(proof)) + } else { + Err("pending ResourceOwnershipPlan: indirect custody call lacks an exact descriptor producer".into()) + } + } else { + for &index in evaluation_order { self.expression(state, &args[index], true)?; } + if !state.aborted { self.expression(state, callee, false)?; } + Ok(None) + } + } + E::OptionalNone if expected.is_some() => { + let shape = expected.unwrap(); + if !matches!(shape, ResourceShape::Optional { .. }) { return Err("Resource absence is not its exact Optional shape".into()); } + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + self.operation(self.active_frame, ResourceOperationRole::CreateAbsentSum { destination }); + Ok(Some(Value { shape, occupancy: ResourceOccupancy::Empty, owner: Some(destination), loan: None })) + }, + E::OptionalSome(inner) | E::ResultOk(inner) if expected.is_some() => { + let payload = self.expression(state, inner, true)?; + if state.aborted { return Ok(None); } + let mut value = payload.ok_or("Resource sum payload has no exact owning producer")?; + if value.loan.is_some() || value.shape.kind() != expected.as_ref().unwrap().kind() { return Err("Resource sum payload is borrowed or from another nominal kind".into()); } + let shape = expected.unwrap(); + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + let source = value.owner.ok_or("Resource sum adoption has no sole owning payload")?; + self.operation(self.active_frame, ResourceOperationRole::SumAdopt { source, destination }); + value.shape = shape; + value.owner = Some(destination); + Ok(Some(value)) + } + E::ResultFail(inner) if expected.is_some() => { + if self.expression(state, inner, true)?.is_some() { return Err("Resource result failure companion cannot carry custody".into()); } + if state.aborted { return Ok(None); } + let shape = expected.unwrap(); + let ResourceShape::Result { failure, .. } = &shape else { return Err("Resource failure is not its exact Result shape".into()); }; + if inner.ty != *failure { return Err("Resource failure companion changes its declared exact type".into()); } + let destination = self.result_slot(ordinal, shape.clone(), self.active_frame); + self.operation(self.active_frame, ResourceOperationRole::CreateFailureSum { destination, failure: *failure }); + Ok(Some(Value { shape, occupancy: ResourceOccupancy::Empty, owner: Some(destination), loan: None })) + } + E::RuntimeFailure(_) | E::RuntimeFailureMessage(_) => { + if let E::RuntimeFailureMessage(message) = &expression.kind { self.expression(state, message, false)?; } + state.aborted = true; + Ok(None) + } + _ if expected.is_some() => Err("pending ResourceOwnershipPlan: Resource expression needs its dedicated canonical producer or conversion transport".into()), + _ => { + let mut has_resource = false; + walk::expression(expression, &mut |value| { + if !std::ptr::eq(value, expression) { + has_resource |= resource_type_pending(self.types, value.ty) && !matches!(self.types.resolve(value.ty), Type::Function { .. }) && !crate::actor_graph::native_graph_metadata_expression(self.function,self.types,value) && crate::native_graph_storage_for_expression(self.function,self.types,value).is_none(); + if let E::Call { function, .. } = value.kind { + has_resource |= self.program.functions.get(function.index() as usize).is_some_and(|callee| callee.id == function && has_execution_records(callee, self.types)); + } + } + }); + if has_resource { return Err("pending ResourceOwnershipPlan: nested Resource evaluation requires exact source-order canonical staging".into()); } + let mut nested=Vec::new(); + walk::expression(expression,&mut |value| { + if !std::ptr::eq(value,expression) + && let E::ActorGraphStep {instruction,..}=&value.kind + && let hir::ActorGraphOperation::HookBox {hook,..}=instruction.operation() { + nested.push((std::ptr::from_ref(value).addr(),instruction.clone(),hook.clone(),self.plan.descriptor_value(value).cloned())); + } + }); + for (address,instruction,hook,proof) in nested { + if proof.as_ref()!=Some(&hook) {return Err("nested graph hook lost its exact protected Descriptor projection".into());} + let saved=self.expression;self.expression=Some(address);self.ordinal+=1; + self.operation(ResourceFrameId(0),ResourceOperationRole::Descriptor {hook}); + if self.emit {let id=self.plan.operations.last().ok_or("nested graph Descriptor was not emitted")?.id;self.plan.graph_descriptors.push((instruction,id));} + self.expression=saved; + } + + Ok(None) + } + } + } + fn invocation( + &mut self, + state: &mut State, + expression: &Expression, + hook: Option, + function: Option, + args: &[Expression], + order: &[usize], + ownership: &hir::CallOwnership, + ordinal: usize, + named_indirect: Option, + ) -> Result, String> { + let resource = hook.is_some() + || function.is_some_and(|id| { + self.program + .functions + .get(id.index() as usize) + .is_some_and(|callee| { + callee.id == id && has_execution_records(callee, self.types) + }) + }) + || expression_shape(self.program, self.function, self.types, expression)?.is_some() + || args.iter().any(|arg| { + expression_shape(self.program, self.function, self.types, arg) + .ok() + .flatten() + .is_some() + }); + if !resource { + for &index in order { + self.expression(state, &args[index], true)?; + } + return Ok(None); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err("Resource invocation cannot use Generated authority".into()); + }; + if source.arguments.len() != args.len() || order.len() != args.len() { + return Err("Resource invocation changed its original source tuple count".into()); + } + if let Some(hook) = &hook { + if !self.program.resource_manifest.contains_hook(hook) { + return Err("Resource invocation hook belongs to another checked program".into()); + } + } + if let Some(function) = function { + let callee = self + .program + .functions + .get(function.index() as usize) + .filter(|callee| callee.id == function) + .ok_or("Resource Source callee has no exact function")?; + if !has_execution_records(callee, self.types) { + return Err("Resource Source callee lacks its original custody witness".into()); + } + } + let frame = self.frame(ordinal, ResourceFrameRole::Operation); + let parent = self.active_frame; + self.active_frame = frame; + let mut operands = Vec::new(); + let mut loans = Vec::new(); + let mut temporary_owners = Vec::new(); + for ¶meter in order { + let arg = &args[parameter]; + let fact = source + .arguments + .iter() + .find(|fact| fact.parameter_index == parameter) + .ok_or("Resource actual has no exact original formal tuple")?; + let Some(_) = expression_shape(self.program, self.function, self.types, arg)? else { + self.expression(state, arg, true)?; + operands.push(ResourceCallOperand::Ordinary { + parameter, + ty: arg.ty, + }); + if state.aborted { + break; + } + continue; + }; + if fact.staging != hir::ArgumentStaging::Original { + return Err("pending ResourceOwnershipPlan: resource actual uses an ordinary staging record".into()); + } + let retaining = fact.effect == Effect::RetainBorrow; + if !retaining && !matches!(fact.effect, Effect::RelinquishOwned | Effect::TransferOwned) + { + return Err("Resource custody cannot use Copy or ObserveData".into()); + } + let input = if !retaining + && fact.syntax == Syntax::Bare + && fact.physical_access == Access::View + { + if let E::View(inner) = &arg.kind { + inner.as_ref() + } else { + arg + } + } else { + arg + }; + let evaluated = self.expression(state, input, !retaining)?; + if state.aborted { + break; + } + let mut value = + evaluated.ok_or("Resource actual has no exact live owner or resident lease")?; + if retaining { + if fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View { + return Err( + "Resource retained loan differs from written View/formal access".into(), + ); + } + let loan = if let Some(loan) = value.loan { + self.operation( + frame, + ResourceOperationRole::BoundedBorrowUse { loan, parameter }, + ); + loan + } else { + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied + { + return Err("pending ResourceOwnershipPlan: sum View actual needs its occupied-arm loan transport".into()); + } + let owner = value + .owner + .ok_or("Resource retained actual has no exact holder")?; + let loan = + self.loan(ordinal, parameter, frame, ResourceLoanSource::Owner(owner))?; + state.leases.insert(loan); + loans.push(loan); + self.borrow(frame, loan); + loan + }; + operands.push(ResourceCallOperand::Borrowed { parameter, loan }); + } else { + if value.loan.is_some() { + return Err("Resource owned actual cannot consume a resident loan".into()); + } + let temporary = self.argument_slot(ordinal, parameter, value.shape.clone(), frame); + if let Some(owner) = value.owner { + self.operation( + frame, + ResourceOperationRole::Transfer { + source: owner, + destination: temporary, + }, + ); + } + value.owner = Some(temporary); + if fact.callee_access == Access::View { + if !value.shape.conditional() && value.occupancy != ResourceOccupancy::Occupied + { + return Err("pending ResourceOwnershipPlan: bare sum to View needs its dedicated occupied transport".into()); + } + let loan = self.loan( + ordinal, + parameter, + frame, + ResourceLoanSource::Owner(temporary), + )?; + loans.push(loan); + self.borrow(frame, loan); + operands.push(ResourceCallOperand::Borrowed { parameter, loan }); + temporary_owners.push((temporary, value.occupancy)); + } else { + operands.push(ResourceCallOperand::Owned { + parameter, + slot: temporary, + }); + } + } + } + if !state.aborted + && let Some(proof) = &named_indirect + { + let E::IndirectCall { callee, .. } = &expression.kind else { + return Err( + "Resource named body proof is not attached to an original Indirect call".into(), + ); + }; + self.expression(state, callee, false)?; + if self.named_indirect_target(state, expression)?.as_ref() != Some(proof) { + return Err( + "Resource named callee lost its exact provenance after actual evaluation" + .into(), + ); + } + } + if !state.aborted + && hook.is_some() + && let E::IndirectCall { callee, .. } = &expression.kind + { + self.expression(state, callee, false)?; + if !state.aborted && self.descriptor_value(state, callee).as_ref() != hook.as_ref() { + return Err("Resource indirect hook lost its exact descriptor provenance after actual evaluation".into()); + } + if source.target + != (hir::CallTarget::Indirect { + signature_type: callee.ty, + }) + || source.bridge != hir::CallBridge::Direct + || !source.generated_operands.is_empty() + { + return Err( + "Resource indirect hook changed its original Indirect Source authority".into(), + ); + } + } + let outcome_shape = expression_shape(self.program, self.function, self.types, expression)?; + let output = outcome_shape + .as_ref() + .map(|shape| self.result_slot(ordinal, shape.clone(), parent)); + if !state.aborted { + let result = match output { + Some(slot) => ResourceCallResult::Owned { slot }, + None => ResourceCallResult::Ordinary { ty: expression.ty }, + }; + if let Some(hook) = hook { + self.operation( + frame, + ResourceOperationRole::InvokeHook { + hook: hook.clone(), + source: source.clone(), + evaluation_order: order.to_vec(), + operands: operands.clone(), + result, + }, + ); + if self.emit && matches!(expression.kind, E::IndirectCall { .. }) { + self.plan + .operations + .last_mut() + .ok_or("Resource indirect hook lost its Invoke operation")? + .indirect_hook = Some(hook.clone()); + } + match hook.recipe() { + jett_types::ResourceKernelRecipe::NetworkFactory => { + let destination = output + .ok_or("Resource factory has no exact success-bearing destination")?; + self.operation(frame, ResourceOperationRole::Acquire { hook, destination }); + } + jett_types::ResourceKernelRecipe::Finalize => { + let owner = operands + .iter() + .find_map(|operand| { + if let ResourceCallOperand::Owned { slot, .. } = operand { + Some(*slot) + } else { + None + } + }) + .ok_or("Resource Close has no consumed owning actual")?; + self.operation( + frame, + ResourceOperationRole::Close { + hook, + source: owner, + }, + ); + } + jett_types::ResourceKernelRecipe::NetworkBorrow => {} + } + } else { + self.operation( + frame, + ResourceOperationRole::InvokeSourceFunction { + function: function + .ok_or("Resource call has neither an exact hook nor a Source callee")?, + formals: source + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + source: source.clone(), + evaluation_order: order.to_vec(), + operands, + result, + }, + ); + if self.emit + && let Some(proof) = named_indirect + { + let invocation = self + .plan + .operations + .last_mut() + .ok_or("Resource named call has no invocation operation")?; + invocation.named_indirect = Some(proof); + } + } + } else { + // Every earlier owned actual must retain a cleanup destination even if the callee never runs. + for operand in &operands { + if let ResourceCallOperand::Owned { slot, .. } = operand { + temporary_owners.push((*slot, ResourceOccupancy::Occupied)); + } + } + } + for loan in loans.into_iter().rev() { + state.leases.remove(&loan); + self.end_borrow(frame, loan); + } + for (source, occupancy) in temporary_owners.into_iter().rev() { + self.operation(frame, ResourceOperationRole::Drop { source, occupancy }); + } + self.operation( + frame, + ResourceOperationRole::Complete { + outcome: if state.aborted { + ResourceCompletion::Abort + } else { + ResourceCompletion::Normal + }, + }, + ); + self.active_frame = parent; + Ok(if state.aborted { + None + } else { + outcome_shape.map(|shape| Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: output, + loan: None, + }) + }) + } + fn block( + &mut self, + block: &BasicBlock, + mut state: State, + ) -> Result, String> { + self.active_frame = state + .calls + .last() + .map_or(ResourceFrameId(0), |active| active.frame); + for (index, statement) in block.statements.iter().enumerate() { + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Statement(index), + }; + self.ordinal = 0; + if state.aborted { + break; + } + self.record_execution_frame()?; + match &statement.kind { + StatementKind::ResourceLexicalExit(_) => self.lexical_exit(&mut state)?, + StatementKind::ResourceConstructor(node) => { + if crate::native_graph_storage_for_constructor(self.function, self.types, node).is_none() { + return Err("occupied constructor requires its authenticated carrier flow".into()); + } + if let ResourceConstructorNode::Stage { value, .. } = node { + if self.expression(&mut state, value, true)?.is_some() { + return Err("graph metadata constructor field acquired Resource custody".into()); + } + } + } + StatementKind::ResourceCall(node) => self.normalized_node(&mut state, node)?, + StatementKind::Let { local, value } => { + let named = self.named_binding(&state, *local, value); + let descriptor = self.descriptor_value(&state, value); + let taking = !self.function.is_view_local(*local); + let result = self.expression(&mut state, value, taking)?; + if !state.aborted { + self.store(&mut state, *local, result, false, false)?; + let expected_descriptor = self.function.resource_lowering.as_ref().and_then(|witness| witness.descriptors.binding(*local, value)); + if descriptor.as_ref() != expected_descriptor { return Err("Resource descriptor binding lost its exact original immutable initializer".into()); } + state.descriptors[local.index() as usize] = descriptor; + state.named_callables[local.index() as usize] = named; + if self.emit && let Some(index) = named + && matches!(value.kind, E::FunctionRef(_)) + { + let proof = self.function.resource_lowering.as_ref() + .and_then(|witness| witness.named_callables.get(index)) + .ok_or("Resource named producer lost its constructor record")?.proof().clone(); + self.plan.named_callable_producers.push((std::ptr::from_ref(value).addr(), proof)); + } + } + } + StatementKind::Assign { target, value } | StatementKind::UpdateField { target, value, .. } => { + let E::Local(local) = target.kind else { return Err("pending ResourceOwnershipPlan: projected replacement needs its aggregate transport".into()); }; + let result = self.expression(&mut state, value, true)?; + if !state.aborted { + let self_rebind = matches!(value.kind, E::Local(source) if source == local) + && target.ty == value.ty + && self.function.local(local).map(|header| header.ty) == Some(value.ty); + self.store(&mut state, local, result, true, self_rebind)?; + } + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + if let Some(value) = self.expression(&mut state, value, true)? { + if value.loan.is_some() { return Err("Resource borrowed result cannot be discarded as owned".into()); } + if let Some(source) = value.owner { self.operation(ResourceFrameId(0), ResourceOperationRole::Drop { source, occupancy: value.occupancy }); } + } + } + StatementKind::SumTag { source, target } => { + if let Some(value) = self.read(&mut state, *source, false)? { + if !value.shape.conditional() { return Err("Resource tag needs an exact supported sum".into()); } + if self.function.local(*target).map(|local| local.ty) != Some(TypeInterner::BOOL) { return Err("Resource sum tag is not its exact Bool destination".into()); } + if let Some(loan) = value.loan { + let row = self.function.resource_borrowed_sum_projection(self.site.block, self.site.position)?.ok_or("borrowed Resource sum tag lacks its original Handle constructor relation")?; + if row.source() != *source || row.tag() != *target { return Err("borrowed Resource sum tag changed its exact source/output headers".into()); } + self.operation(self.active_frame, ResourceOperationRole::ObserveSumView { source: loan, target: *target }); + } + state.tags.insert(target.index(), *source); + } + } + StatementKind::SumTake { source, target, success } => { + if let Some(value) = self.read(&mut state, *source, false)? { + let (tag, selected) = state.guards.get(&source.index()).copied().ok_or("Resource SumTake has no exact selecting typed tag edge")?; + if selected != *success || (selected && value.occupancy != ResourceOccupancy::Occupied) || (!selected && value.occupancy != ResourceOccupancy::Empty) { return Err("Resource SumTake differs from its selected occupied arm".into()); } + if let Some(parent) = value.loan { + let row = self.function.resource_borrowed_sum_projection(self.site.block, self.site.position)?.ok_or("borrowed Resource sum extraction lacks its exact constructor relation")?; + if row.source() != *source || row.tag() != tag { return Err("borrowed Resource sum extraction changed its exact tag/header relation".into()); } + if selected { + if row.output() != *target || row.success_site() != self.site || !self.function.is_view_local(*target) || self.local_slots[target.index() as usize].is_some() { return Err("borrowed Resource payload gained an owning or foreign output".into()); } + let path = row.path(); + let loan = self.loan(self.ordinal, usize::MAX, ResourceFrameId(0), ResourceLoanSource::ProjectedSumPayload { parent, tag, path })?; + state.leases.insert(loan); + self.store(&mut state, *target, Some(Value { shape: ResourceShape::Plain { kind: value.shape.kind().clone() }, occupancy: ResourceOccupancy::Occupied, owner: None, loan: Some(loan) }), false, false)?; + self.operation(self.active_frame, ResourceOperationRole::ProjectSumView { source: parent, destination: loan, tag, path }); + } else { + let ResourceShape::Result { failure, .. } = value.shape else { return Err("borrowed Optional has no failure companion".into()); }; + let header = self.function.local(*target).ok_or("borrowed Resource failure read has no header")?.clone(); + if failure != TypeInterner::STRING || header.ty != failure || header.view_source.is_some() || row.failure_site() != Some(self.site) { return Err("borrowed Resource failure read lacks its exact String companion".into()); } + self.operation(self.active_frame, ResourceOperationRole::ReadFailureCompanion { source: parent, target: header, failure, tag }); + } + continue; + } + if selected { + let owner = value.owner.ok_or("Resource sum payload lacks its holder")?; + let mut payload = self.read(&mut state, *source, true)?.ok_or("Resource sum source was consumed before its exact take")?; + payload.shape = ResourceShape::Plain { kind: payload.shape.kind().clone() }; + payload.owner = Some(self.local_slots[target.index() as usize].ok_or("Resource sum success has no exact output slot")?); + self.store(&mut state, *target, Some(payload), false, false)?; + self.operation(self.active_frame, ResourceOperationRole::SumTake { source: owner, destination: self.local_slots[target.index() as usize].ok_or("Resource sum success has no dedicated output slot")?, tag, success: true }); + } else { + let ResourceShape::Result { failure, .. } = &value.shape else { return Err("pending ResourceOwnershipPlan: Optional empty-arm extraction has no ordinary companion".into()); }; + let header = self.function.local(*target).ok_or("Resource failure take has no exact destination")?.clone(); + if header.ty != *failure { return Err("Resource failure take changes its exact companion type".into()); } + let owner = value.owner.ok_or("Resource failure companion has no exact sum shell holder")?; + self.operation(self.active_frame, ResourceOperationRole::TakeFailureCompanion { source: owner, target: header, failure: *failure, tag }); + self.read(&mut state, *source, true)?; + if shape(&self.program.resource_manifest, self.types, self.function.local(*target).ok_or("Resource failure take has no destination")?.ty)?.is_some() { return Err("Resource failure companion cannot become an occupied owner".into()); } + } + } + } + StatementKind::Assert { condition, message } => { + self.expression(&mut state, condition, false)?; + if let Some(message) = message { self.expression(&mut state, message, false)?; } + } + StatementKind::Breakpoint { condition, bindings } => { + if bindings.iter().any(|local| state.values[local.index() as usize].is_some()) { return Err("pending ResourceOwnershipPlan: Resource debugger observation transport is unproved".into()); } + if let Some(condition) = condition { self.expression(&mut state, condition, false)?; } + } + StatementKind::Trace(local) => { + if state.values[local.index() as usize].is_some() { return Err("Resource trace cannot manufacture observation or copied custody".into()); } + } + StatementKind::BeginCallView { local, value } => { + if shape(&self.program.resource_manifest, self.types, self.function.local(*local).ok_or("loan local missing")?.ty)?.is_some() || resource_type_pending(self.types, value.ty) { return Err("pending ResourceOwnershipPlan: ordinary BeginCallView is not a Resource lease constructor".into()); } + } + StatementKind::EndCallView { .. } | StatementKind::OpenCallOwnerGeneration { .. } | StatementKind::ReplaceCallOwnerGeneration { .. } | StatementKind::PrepareFieldUpdate { .. } | StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::CheckRefinement { call, .. } => { self.expression(&mut state, call, false)?; } + StatementKind::SequenceLength { source, target } => { + control_flow::sequence_length(self.function, self.types, source, *target)?; + self.clear(&mut state, *target); + } + StatementKind::SequenceGet { consume, source, index, target, part } => { + control_flow::sequence_get(self.function, self.types, *consume, source, *index, *target, *part)?; + self.clear(&mut state, *target); + } + StatementKind::ReflectedContainerReady { .. } | StatementKind::ReflectedValueReady { .. } | StatementKind::IterationBorrow { .. } => return Err("pending ResourceOwnershipPlan: sequence or aggregate custody needs its dedicated normalization".into()), + } + } + self.site = ResourceSite { + function: self.function.id, + block: block.id, + position: ResourcePosition::Terminator, + }; + self.ordinal = 0; + if state.aborted { + if !state.calls.is_empty() { + return Err("pending Resource call region: infrastructure abort needs its exact acquired-prefix retirement".into()); + } + self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); + return Ok(Vec::new()); + } + self.record_execution_frame()?; + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if !state.calls.is_empty() { return Err("Resource Source Return reaches an unretired normalized operation suffix".into()); } + let mut outgoing = None; + if let Some(value) = value { + let descriptor = self.descriptor_value(&state, value); + let returned = self.expression(&mut state, value, true)?; + if !state.aborted && let Some(expected) = self.plan.descriptor_return() { + let witnessed = self.function.resource_lowering.as_ref().is_some_and(|witness| witness.descriptors.return_matches(value)); + if descriptor.as_ref() != Some(expected) || !witnessed { return Err("Resource descriptor Return differs from its exact current producer and archived return summary".into()); } + } + if let Some(returned) = returned { + if returned.loan.is_some() { return Err("Resource resident lease cannot escape as an owning return".into()); } + let expected = return_shape(self.program, self.function, self.types)?.ok_or("Resource return has an ordinary declared result")?; + if returned.shape != expected { return Err("Resource return changes its exact shape or kind".into()); } + if self.function.resource_lexical_exit(self.site.block, self.site.position)?.is_some() { self.lexical_exit(&mut state)?; } + let frame = self.return_frame.ok_or("Resource return lacks its initially selected provisional frame")?; + let destination = self.result_slot(usize::MAX, expected, frame); + self.plan.slots[destination.0].storage = ResourceSlotStorage::Return { site: self.site }; + if let Some(source) = returned.owner { self.operation(frame, ResourceOperationRole::Transfer { source, destination }); } + outgoing = Some(destination); + } else if !state.aborted && return_shape(self.program, self.function, self.types)?.is_some() { return Err("Resource return has no exact owned output".into()); } + } + if outgoing.is_none() && !state.aborted && self.function.resource_lexical_exit(self.site.block, self.site.position)?.is_some() { self.lexical_exit(&mut state)?; } + let completion = if state.aborted { ResourceCompletion::Abort } else { ResourceCompletion::Return }; + self.finish(&mut state, ResourceFrameId(0), completion); + if let Some(source) = outgoing { self.operation(self.return_frame.ok_or("Resource return frame disappeared")?, ResourceOperationRole::CompleteReturnAfterCleanup { source }); } + Ok(Vec::new()) + } + TerminatorKind::Unreachable => { if !state.calls.is_empty() { return Err("pending Resource call region: unreachable exit needs its exact operation suffix retirement".into()); } self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); Ok(Vec::new()) } + TerminatorKind::Goto(target) => Ok(vec![(*target, state)]), + TerminatorKind::Branch { condition, then_block, else_block } => { + self.expression(&mut state, condition, false)?; + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + let mut yes = state.clone(); let mut no = state; + if let E::Local(tag) = condition.kind && let Some(source) = yes.tags.get(&tag.index()).copied() { + let value = yes.values[source.index() as usize].as_ref().ok_or("Resource tag source was consumed before its selecting edge")?; + if !value.shape.conditional() { return Err("Resource selecting edge lost its exact sum tag source".into()); } + // Plan both authenticated CFG edges. A known empty/occupied + // producer does not remove either runtime SumTag edge; + // the exact selecting guard still gates each SumTake. + if let Some(value) = &mut yes.values[source.index() as usize] { value.occupancy = ResourceOccupancy::Occupied; } + if let Some(value) = &mut no.values[source.index() as usize] { value.occupancy = ResourceOccupancy::Empty; } + yes.guards.insert(source.index(), (tag, true)); no.guards.insert(source.index(), (tag, false)); + } + Ok(vec![(*then_block, yes), (*else_block, no)]) + } + TerminatorKind::ForEach { key, value, by_view, iterable, body, exit } => { + if crate::native_graph_storage_for_expression(self.function, self.types, iterable).is_some() { + control_flow::graph_sequence(self.function, self.types, *key, *value, *by_view, iterable)?; + } else { + control_flow::consuming_list(self.function, self.types, *key, *value, *by_view, iterable)?; + } + if self.expression(&mut state, iterable, true)?.is_some() { + return Err("Resource sequence cannot acquire or transport element custody".into()); + } + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + self.clear(&mut state, *key); + if let Some(value) = value { self.clear(&mut state, *value); } + Ok(vec![(*body, state.clone()), (*exit, state)]) + } + TerminatorKind::Switch { scrutinee, variants, otherwise } => { + control_flow::ordinary_switch(self.function, self.types, scrutinee, variants, *otherwise)?; + if self.expression(&mut state, scrutinee, true)?.is_some() { + return Err("Resource switch cannot acquire or transport discriminant custody".into()); + } + if state.aborted { self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); return Ok(Vec::new()); } + let mut successors = Vec::new(); + for (_, target, bindings) in variants { + let mut branch = state.clone(); + for local in bindings { self.clear(&mut branch, *local); } + successors.push((*target, branch)); + } + if let Some(target) = otherwise { successors.push((*target, state)); } + Ok(successors) + } + TerminatorKind::ReflectedTypeDispatch { type_info, arms, otherwise } => { + let witness = self.function.resource_lowering.as_ref() + .ok_or("Resource reflected body lacks its original Source witness")?; + if !reflected_fields::admits(witness, block.id) || arms.len() != 1 { + return Err("pending ResourceOwnershipPlan: selected control-flow custody normalization is unproved".into()); + } + if self.expression(&mut state, type_info, false)?.is_some() { + return Err("Resource reflected selector cannot carry Resource custody".into()); + } + if state.aborted { + self.finish(&mut state, ResourceFrameId(0), ResourceCompletion::Abort); + return Ok(Vec::new()); + } + Ok(vec![(arms[0].target, state.clone()), (*otherwise, state)]) + } + TerminatorKind::Respond(_) => Err("pending ResourceOwnershipPlan: selected control-flow custody normalization is unproved".into()), + } + } +} + +fn join(left: &mut State, right: &State) -> Result { + let before = left.clone(); + if left.calls != right.calls { + return Err("Resource CFG join changed its exact operation stack, arrived prefix or retirement obligations".into()); + } + if left.leases != right.leases { + return Err( + "Resource CFG join cannot extend or silently end a loan from only one path".into(), + ); + } + for index in 0..left.values.len() { + match (&mut left.values[index], &right.values[index]) { + (Some(a), Some(b)) if a.shape == b.shape && a.owner == b.owner && a.loan == b.loan => { + if a.occupancy != b.occupancy { a.occupancy = ResourceOccupancy::Conditional; } + } + (None, None) => {} + _ => return Err("pending ResourceOwnershipPlan: moved/uninitialized versus live CFG join requires conditional cleanup proof".into()), + } + left.moved[index] |= right.moved[index]; + if left.descriptors[index] != right.descriptors[index] { + left.descriptors[index] = None; + } + if left.named_callables[index] != right.named_callables[index] { + left.named_callables[index] = None; + } + } + left.tags + .retain(|id, source| right.tags.get(id) == Some(source)); + left.guards + .retain(|id, guard| right.guards.get(id) == Some(guard)); + Ok(*left != before) +} + +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + let original = &function + .resource_lowering + .as_ref() + .ok_or("Resource plan lacks its authenticated Source")? + .original; + let admitted = function + .resource_lowering + .as_ref() + .ok_or("Resource plan has no constructor witness")? + .borrowed_sums + .iter() + .map(|row| row.original_local()) + .chain( + function + .resource_lowering + .as_ref() + .into_iter() + .flat_map(|witness| { + witness + .lexical_owners + .iter() + .map(|row| row.original_local()) + }), + ) + .collect::>(); + if walk::nested_resource_declaration(&original.body, original, types, false, &admitted) { + return Err("pending ResourceOwnershipPlan: nested Resource declarations need exact constructor Scope records".into()); + } + if function.capture_count != 0 { + return Err( + "pending ResourceOwnershipPlan: Resource-bearing capture transport is unproved".into(), + ); + } + ControlFlowGraph::analyze(function) + .map_err(|errors| format!("Resource CFG is malformed: {errors:?}"))?; + let site = ResourceSite { + function: function.id, + block: function.entry, + position: ResourcePosition::Terminator, + }; + let return_frame = return_shape(program, function, types)?.map(|_| ResourceFrameId(1)); + let mut frames = vec![ResourceFrame { + id: ResourceFrameId(0), + role: ResourceFrameRole::Scope, + site, + parent: return_frame, + ordinal: 0, + }]; + if let Some(id) = return_frame { + frames.push(ResourceFrame { + id, + role: ResourceFrameRole::Return, + site, + parent: None, + ordinal: 0, + }); + } + let plan = ResourceFunctionPlan { + graph_descriptors: Vec::new(), + carriers: None, + function: function.id, + identity: function.identity.clone(), + parameters: function.params.clone(), + return_type: function.return_type, + frames, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + execution_frames: Vec::new(), + lexical_exits: Vec::new(), + descriptor_return: function + .resource_lowering + .as_ref() + .and_then(|witness| witness.descriptors.returned()) + .cloned(), + descriptor_values: returned_descriptors::current_values( + function + .resource_lowering + .as_ref() + .ok_or("Resource descriptors lost their constructor witness")?, + function, + )?, + descriptor_locals: function + .resource_lowering + .as_ref() + .map_or_else(Vec::new, |witness| { + witness + .descriptors + .locals() + .map(|(local, hook)| (local, hook.clone())) + .collect() + }), + named_callable_producers: Vec::new(), + named_callable_values: named_callables::current_values( + function + .resource_lowering + .as_ref() + .ok_or("Resource named values lost their constructor witness")?, + function, + )?, + }; + let mut analysis = Analysis { + program, + function, + types, + plan, + local_slots: vec![None; function.locals.len()], + expression_slots: BTreeMap::new(), + operation_frames: BTreeMap::new(), + loan_sites: BTreeMap::new(), + site, + ordinal: 0, + expression: None, + emit: false, + active_frame: ResourceFrameId(0), + return_frame, + normalized_calls: BTreeMap::new(), + }; + let mut initial = State { + values: vec![None; function.locals.len()], + moved: vec![false; function.locals.len()], + descriptors: vec![None; function.locals.len()], + named_callables: vec![None; function.locals.len()], + tags: BTreeMap::new(), + guards: BTreeMap::new(), + leases: BTreeSet::new(), + aborted: false, + calls: Vec::new(), + }; + for local in &function.locals { + if let Some(shape) = local_shape(program, function, types, local)? + && !function.is_view_local(local.id) + { + let id = ResourceOwnerSlotId(analysis.plan.slots.len()); + analysis.local_slots[local.id.index() as usize] = Some(id); + analysis.plan.slots.push(ResourceOwnerSlot { + id, + frame: ResourceFrameId(0), + shape, + storage: ResourceSlotStorage::Local { + header: local.clone(), + }, + }); + } + } + for (parameter, param) in function.params.iter().enumerate() { + if let Some(shape) = formal_shape(program, function, types, param)? { + let mut value = Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: analysis.local_slots[param.local.index() as usize], + loan: None, + }; + if param.mode == ParamMode::View { + let loan = ResourceLoanId(analysis.plan.loans.len()); + analysis.plan.loans.push(ResourceLoan { + id: loan, + frame: ResourceFrameId(0), + shape: value.shape.clone(), + parameter: None, + source: ResourceLoanSource::IncomingViewFormal { + scope: ResourceFrameId(0), + parameter, + }, + }); + value.loan = Some(loan); + } + initial.values[param.local.index() as usize] = Some(value); + } + } + let mut states = vec![None; function.blocks.len()]; + states[function.entry.index() as usize] = Some(initial); + let mut queue = VecDeque::from([function.entry]); + let mut steps = 0usize; + let limit = function + .blocks + .len() + .saturating_mul(function.locals.len().saturating_add(1)) + .saturating_mul(8) + .saturating_add(1); + while let Some(block) = queue.pop_front() { + steps += 1; + if steps > limit { + return Err("pending ResourceOwnershipPlan: custody fixed point exceeds the finite normalization bound".into()); + } + let input = states[block.index() as usize] + .as_ref() + .ok_or("Resource reachable block has no exact incoming state")? + .clone(); + for (target, output) in analysis.block(&function.blocks[block.index() as usize], input)? { + let changed = if let Some(current) = &mut states[target.index() as usize] { + join(current, &output)? + } else { + states[target.index() as usize] = Some(output); + true + }; + if changed { + queue.push_back(target); + } + } + } + analysis.emit = true; + for block in &function.blocks { + if let Some(state) = &states[block.id.index() as usize] { + analysis.block(block, state.clone())?; + } else if walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + { + return Err( + "Resource custody operation is disconnected from its authenticated entry CFG" + .into(), + ); + } + } + Ok(analysis.plan) +} diff --git a/crates/jett_mir/src/resource_ownership/flow/normalized.rs b/crates/jett_mir/src/resource_ownership/flow/normalized.rs new file mode 100644 index 00000000..fe6a6a86 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/flow/normalized.rs @@ -0,0 +1,507 @@ +//! Cross-block normalized Source operation proof. Every identity is constructor sealed. +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct ActiveCall { + region: ResourceCallRegionId, + pub(super) frame: ResourceFrameId, + parent: ResourceFrameId, + next: usize, + loans: Vec, + owners: Vec<(ResourceOwnerSlotId, ResourceOccupancy)>, + invoked: bool, +} + +#[derive(Clone)] +pub(super) struct PreparedCall { + frame: ResourceFrameId, + operands: Vec, + result: ResourceCallResult, + created_loans: Vec, +} + +impl Analysis<'_> { + fn prepare_normalized( + &mut self, + state: &State, + region: &ResourceCallRegion, + ) -> Result { + if let Some(prepared) = self.normalized_calls.get(®ion.id()) { + return Ok(prepared.clone()); + } + let frame = self.frame(0, ResourceFrameRole::Operation); + let begin = self.site; + let mut operands = Vec::new(); + let mut created_loans = Vec::new(); + for actual in region.actuals() { + let fact = region + .source() + .arguments + .iter() + .find(|fact| fact.parameter_index == actual.parameter()) + .ok_or("Resource normalized actual has no exact original Source tuple")?; + self.site = actual.stage(); + let Some(shape) = shape( + &self.program.resource_manifest, + self.types, + actual.endpoint().ty, + )? + else { + if actual.ordinary().is_none() { + return Err( + "Resource normalized ordinary actual lacks its exact initialized endpoint" + .into(), + ); + } + operands.push(ResourceCallOperand::Ordinary { + parameter: actual.parameter(), + ty: actual.endpoint().ty, + }); + continue; + }; + if actual.ordinary().is_some() || fact.staging != hir::ArgumentStaging::Original { + return Err( + "Resource normalized actual gained ordinary storage or staging authority" + .into(), + ); + } + match fact.effect { + Effect::TransferOwned | Effect::RelinquishOwned => { + if fact.syntax != Syntax::Bare { + return Err( + "Resource normalized owned actual changes its bare Source syntax" + .into(), + ); + } + let slot = self.argument_slot(0, actual.parameter(), shape, frame); + if fact.callee_access == Access::View { + let loan = self.loan( + 0, + actual.parameter(), + frame, + ResourceLoanSource::Owner(slot), + )?; + created_loans.push(loan); + operands.push(ResourceCallOperand::Borrowed { + parameter: actual.parameter(), + loan, + }); + } else { + operands.push(ResourceCallOperand::Owned { + parameter: actual.parameter(), + slot, + }); + } + } + Effect::RetainBorrow => { + if fact.syntax != Syntax::WrittenView || fact.callee_access != Access::View { + return Err( + "Resource normalized retained actual changed its written View tuple" + .into(), + ); + } + let E::View(inner) = &actual.endpoint().kind else { + return Err("pending Resource call region: retained Resource expression needs a proved stable place endpoint".into()); + }; + let E::Local(local) = inner.kind else { + return Err("pending Resource call region: retained Resource expression needs its exact Local holder".into()); + }; + let value = state + .values + .get(local.index() as usize) + .and_then(Option::as_ref) + .ok_or( + "Resource normalized retained holder is not live at its initial region", + )?; + let loan = if let Some(loan) = value.loan { + loan + } else { + let owner = value + .owner + .ok_or("Resource normalized retained holder lacks its exact owner")?; + let loan = self.loan( + 0, + actual.parameter(), + frame, + ResourceLoanSource::Owner(owner), + )?; + created_loans.push(loan); + loan + }; + operands.push(ResourceCallOperand::Borrowed { + parameter: actual.parameter(), + loan, + }); + } + Effect::Copy | Effect::ObserveData => { + return Err("Resource normalized custody cannot use Copy or ObserveData".into()); + } + } + } + self.site = begin; + let (_, output) = region + .invocation() + .ok_or("Resource normalized Source invocation lacks its exact output")?; + let result = match shape( + &self.program.resource_manifest, + self.types, + region.original().ty, + )? { + Some(_) => ResourceCallResult::Owned { + slot: self.local_slots[output.index() as usize] + .ok_or("Resource normalized output lacks its exact dedicated slot")?, + }, + None => ResourceCallResult::Ordinary { + ty: region.original().ty, + }, + }; + let prepared = PreparedCall { + frame, + operands, + result, + created_loans, + }; + self.normalized_calls.insert(region.id(), prepared.clone()); + Ok(prepared) + } + + pub(super) fn normalized_node( + &mut self, + state: &mut State, + node: &ResourceCallNode, + ) -> Result<(), String> { + let region = self + .function + .resource_call_region(node.region()) + .ok_or("Resource normalized node has no constructor-owned region")? + .clone(); + match node { + ResourceCallNode::Begin { .. } => { + if state.calls.last().map(|active| active.region) != region.parent() { + return Err( + "Resource normalized Begin changed its exact parent operation stack".into(), + ); + } + let prepared = self.prepare_normalized(state, ®ion)?; + let parent = self.active_frame; + if self.plan.frames[prepared.frame.index()].parent != Some(parent) { + return Err("Resource normalized Begin changed its frame parent".into()); + } + self.operation( + prepared.frame, + ResourceOperationRole::BeginSourceFunction { + region: region.id(), + function: region.function(), + source: region.source().clone(), + formals: region + .source() + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + evaluation_order: region.evaluation_order().to_vec(), + operands: prepared.operands.clone(), + result: prepared.result, + }, + ); + for &loan in &prepared.created_loans { + let parameter = self.plan.loans[loan.index()] + .parameter + .ok_or("Resource prepared Source loan lost its parameter")?; + let role = if self.plan.loans[loan.index()].shape.conditional() { + ResourceOperationRole::PrepareSourceSumBorrow { loan, parameter } + } else { + ResourceOperationRole::PrepareSourceBorrow { loan, parameter } + }; + self.operation(prepared.frame, role); + } + state.calls.push(ActiveCall { + region: region.id(), + frame: prepared.frame, + parent, + next: 0, + loans: Vec::new(), + owners: Vec::new(), + invoked: false, + }); + self.active_frame = prepared.frame; + } + ResourceCallNode::Stage { + source_index, + parameter, + value, + ordinary, + .. + } => { + let mut active = state + .calls + .pop() + .ok_or("Resource normalized Stage has no active operation")?; + if active.region != region.id() + || active.next != *source_index + || active.invoked + || region.evaluation_order().get(*source_index) != Some(parameter) + { + return Err("Resource normalized Stage changed its exact arrived prefix".into()); + } + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized Stage lacks its prepared call")? + .clone(); + let operand = prepared + .operands + .get(*source_index) + .ok_or("Resource normalized Stage lost its formal operand")? + .clone(); + let fact = region + .source() + .arguments + .iter() + .find(|fact| fact.parameter_index == *parameter) + .ok_or("Resource normalized Stage lost its Source fact")?; + let retaining = fact.effect == Effect::RetainBorrow; + let input = if !retaining && fact.syntax == Syntax::Bare { + match &value.kind { + E::View(inner) => inner.as_ref(), + _ => value, + } + } else { + value + }; + let evaluated = self.expression(state, input, !retaining)?; + if state.aborted { + return Err("pending Resource call region: infrastructure abort within a staged actual needs its exact prefix exit".into()); + } + match &operand { + ResourceCallOperand::CarrierOwned { .. } + | ResourceCallOperand::CarrierBorrowed { .. } => { + return Err( + "carrier operand reached the legacy single-leaf normalized flow".into(), + ); + } + ResourceCallOperand::Ordinary { ty, .. } => { + if evaluated.is_some() + || ordinary + .and_then(|local| self.function.local(local)) + .map(|header| header.ty) + != Some(*ty) + { + return Err("Resource normalized ordinary endpoint changed its exact type/header".into()); + } + } + ResourceCallOperand::Owned { .. } | ResourceCallOperand::Borrowed { .. } + if !retaining => + { + let value = evaluated + .ok_or("Resource normalized Stage has no live owning producer")?; + if value.loan.is_some() + || (!value.shape.conditional() + && value.occupancy != ResourceOccupancy::Occupied) + { + return Err( + "Resource normalized owned Stage is not its exact occupied owner" + .into(), + ); + } + let slot = match &operand { + ResourceCallOperand::CarrierOwned { .. } + | ResourceCallOperand::CarrierBorrowed { .. } => return Err( + "carrier operand reached the legacy single-leaf normalized flow" + .into(), + ), + ResourceCallOperand::Owned { slot, .. } => *slot, + ResourceCallOperand::Borrowed { loan, .. } => { + match self.plan.loans[loan.index()].source { + ResourceLoanSource::Owner(slot) => slot, + ResourceLoanSource::IncomingViewFormal { .. } + | ResourceLoanSource::CarrierSumProjection { .. } + | ResourceLoanSource::ProjectedSumPayload { .. } => return Err( + "Resource owned Stage changed into a resident formal loan" + .into(), + ), + } + } + ResourceCallOperand::Ordinary { .. } => { + return Err("Resource owned Stage lost its custody operand".into()); + } + }; + if value.shape != self.plan.slots[slot.index()].shape { + return Err( + "Resource normalized Stage changes its sealed owner shape".into() + ); + } + self.operation( + active.frame, + ResourceOperationRole::Transfer { + source: value.owner.ok_or( + "Resource normalized Stage lacks an exact source owner", + )?, + destination: slot, + }, + ); + active.owners.push((slot, value.occupancy)); + if let ResourceCallOperand::Borrowed { loan, .. } = operand { + state.leases.insert(loan); + active.loans.push(loan); + self.borrow(active.frame, loan); + } + } + ResourceCallOperand::Borrowed { loan, .. } => { + let value = evaluated + .ok_or("Resource normalized retained Stage lost its holder")?; + if value.shape != self.plan.loans[loan.index()].shape { + return Err( + "Resource normalized retained Stage changes its sealed loan shape" + .into(), + ); + } + if let Some(resident) = value.loan { + if resident != *loan { + return Err( + "Resource normalized retained Stage changed its forwarded loan" + .into(), + ); + } + self.operation( + active.frame, + ResourceOperationRole::BoundedBorrowUse { + loan: *loan, + parameter: *parameter, + }, + ); + } else { + if value.owner.map(ResourceLoanSource::Owner) + != Some(self.plan.loans[loan.index()].source) + { + return Err("Resource normalized retained Stage changed its original holder".into()); + } + state.leases.insert(*loan); + active.loans.push(*loan); + self.borrow(active.frame, *loan); + } + } + ResourceCallOperand::Owned { .. } => { + return Err( + "Resource normalized retained Stage cannot own its formal".into() + ); + } + } + self.operation( + active.frame, + ResourceOperationRole::StageSourceActual { + region: region.id(), + source_index: *source_index, + parameter: *parameter, + operand, + ordinary: *ordinary, + }, + ); + active.next += 1; + state.calls.push(active); + } + ResourceCallNode::Invoke { output, .. } => { + let active = state + .calls + .last_mut() + .ok_or("Resource normalized Invoke lacks its active operation")?; + if active.region != region.id() + || active.invoked + || active.next != region.evaluation_order().len() + { + return Err( + "Resource normalized Invoke has an incomplete or reused actual prefix" + .into(), + ); + } + active.invoked = true; + let frame = active.frame; + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized Invoke lost its prepared tuple")? + .clone(); + self.operation( + frame, + ResourceOperationRole::InvokeSourceFunction { + function: region.function(), + formals: region + .source() + .arguments + .iter() + .map(ResourceCallFormal::original) + .collect(), + source: region.source().clone(), + evaluation_order: region.evaluation_order().to_vec(), + operands: prepared.operands, + result: prepared.result, + }, + ); + let value = shape( + &self.program.resource_manifest, + self.types, + region.original().ty, + )? + .map(|shape| -> Result { + Ok(Value { + occupancy: if shape.conditional() { + ResourceOccupancy::Conditional + } else { + ResourceOccupancy::Occupied + }, + shape, + owner: match prepared.result { + ResourceCallResult::Owned { slot } => Some(slot), + ResourceCallResult::Ordinary { .. } => None, + ResourceCallResult::Carrier { .. } => { + return Err( + "carrier result reached the legacy single-leaf normalized flow" + .into(), + ); + } + }, + loan: None, + }) + }) + .transpose()?; + self.store(state, *output, value, false, false)?; + } + ResourceCallNode::End { outcome, .. } => { + let active = state + .calls + .pop() + .ok_or("Resource normalized End lacks its active operation")?; + if active.region != region.id() + || (*outcome == ResourceCompletion::Normal) != active.invoked + || *outcome == ResourceCompletion::Return + { + return Err( + "Resource normalized End changed its exact invoked/abandoned outcome" + .into(), + ); + } + for loan in active.loans.iter().rev() { + state.leases.remove(loan); + self.end_borrow(active.frame, *loan); + } + let prepared = self + .normalized_calls + .get(®ion.id()) + .ok_or("Resource normalized End lost its exact prepared operands")?; + let owners = active.owners.iter().rev().filter(|(slot, _)| *outcome == ResourceCompletion::Abort || prepared.operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Borrowed { loan, .. } if self.plan.loans[loan.index()].source == ResourceLoanSource::Owner(*slot)))).copied().collect::>(); + for (source, occupancy) in owners { + self.operation( + active.frame, + ResourceOperationRole::Drop { source, occupancy }, + ); + } + self.operation( + active.frame, + ResourceOperationRole::Complete { outcome: *outcome }, + ); + self.active_frame = active.parent; + } + } + Ok(()) + } +} diff --git a/crates/jett_mir/src/resource_ownership/graph_metadata.rs b/crates/jett_mir/src/resource_ownership/graph_metadata.rs new file mode 100644 index 00000000..47b7e2fc --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/graph_metadata.rs @@ -0,0 +1,565 @@ +//! Ordinary owned metadata rooted in protected checked actor graphs. +//! This proof selects storage only; invocation and Scope remain separate proofs. +use super::*; +use hir::ExpressionKind as E; + +mod current; +mod original; + +#[derive(Debug, Clone)] +enum Shape { + Plain { + plan: hir::ActorGraphPlan, + value: Expression, + }, + Hook(hir::ActorGraphInstruction), + HiddenRootToken { + fill: hir::ActorGraphInstruction, + root: Box, + }, + Actor { + plan: hir::ActorGraphPlan, + node: usize, + }, + Fields(Vec>), + Items(Vec>), + Entries(Vec<(Option, Option)>), + Some(Box), + Ok(Box), + Fail(Box), + Alternatives(Vec), +} +impl PartialEq for Shape { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::Plain { + plan: left, + value: a, + }, + Self::Plain { + plan: right, + value: b, + }, + ) => left == right && equal(a, b), + (Self::Hook(a), Self::Hook(b)) => a == b, + ( + Self::HiddenRootToken { + fill: a, + root: left, + }, + Self::HiddenRootToken { + fill: b, + root: right, + }, + ) => a == b && left == right, + ( + Self::Actor { + plan: a, + node: left, + }, + Self::Actor { + plan: b, + node: right, + }, + ) => a == b && left == right, + (Self::Fields(a), Self::Fields(b)) | (Self::Items(a), Self::Items(b)) => a == b, + (Self::Entries(a), Self::Entries(b)) => a == b, + (Self::Some(a), Self::Some(b)) + | (Self::Ok(a), Self::Ok(b)) + | (Self::Fail(a), Self::Fail(b)) => a == b, + (Self::Alternatives(a), Self::Alternatives(b)) => a == b, + _ => false, + } + } +} + +/// No public constructor. The type is a projection of a protected graph row. +#[derive(Debug, Clone, PartialEq)] +pub struct GraphStorageProof { + ty: TypeId, + shape: Shape, +} +impl GraphStorageProof { + pub fn ty(&self) -> TypeId { + self.ty + } + fn transferable(&self) -> bool { + !matches!(self.shape, Shape::HiddenRootToken { .. }) + } +} + +/// An owned graph hook box, never a scalar returned descriptor or Scope grant. +#[derive(Debug, Clone, PartialEq)] +pub struct GraphHookProjectionProof { + ty: TypeId, + hook: hir::ResourceHookRef, + producers: Vec, +} +impl GraphHookProjectionProof { + pub fn ty(&self) -> TypeId { + self.ty + } + pub fn hook(&self) -> &hir::ResourceHookRef { + &self.hook + } + pub fn producers(&self) -> &[hir::ActorGraphInstruction] { + &self.producers + } +} + +#[derive(Debug, Clone)] +struct ValueRow { + original: Expression, + current: Expression, + proof: GraphStorageProof, +} +impl PartialEq for ValueRow { + fn eq(&self, other: &Self) -> bool { + equal(&self.original, &other.original) + && equal(&self.current, &other.current) + && self.proof == other.proof + } +} +#[derive(Debug, Clone, PartialEq)] +struct HeaderRow { + original: Local, + current: Local, + proof: GraphStorageProof, +} +#[derive(Debug, Clone, PartialEq)] +struct FormalRow { + original: Param, + current: Param, + proof: GraphStorageProof, +} +/// Full original/current Source call and exact formal header; no tuple projection +/// is accepted as a replacement for either whole call. +#[derive(Debug, Clone)] +pub struct GraphSourceEdgeProof { + original_call: Expression, + current_call: Expression, + parameter: usize, + formal: Param, + actual: Expression, + proof: GraphStorageProof, +} +impl PartialEq for GraphSourceEdgeProof { + fn eq(&self, other: &Self) -> bool { + equal(&self.original_call, &other.original_call) + && equal(&self.current_call, &other.current_call) + && self.parameter == other.parameter + && self.formal == other.formal + && equal(&self.actual, &other.actual) + && self.proof == other.proof + } +} +impl GraphSourceEdgeProof { + pub fn original_call(&self) -> &Expression { + &self.original_call + } + pub fn current_call(&self) -> &Expression { + &self.current_call + } + pub fn parameter_index(&self) -> usize { + self.parameter + } + pub fn original_formal(&self) -> &Param { + &self.formal + } + pub fn original_actual(&self) -> &Expression { + &self.actual + } + pub fn storage(&self) -> &GraphStorageProof { + &self.proof + } +} + +#[derive(Debug, Clone)] +struct SequenceOutput { + current: Local, + original_iterable: Expression, + proof: GraphStorageProof, +} +impl PartialEq for SequenceOutput { + fn eq(&self, other: &Self) -> bool { + self.current == other.current + && equal(&self.original_iterable, &other.original_iterable) + && self.proof == other.proof + } +} + +#[derive(Debug, Clone, Default)] +pub(super) struct Witness { + owner: Option, + local_map: Vec<(Local, Local)>, + values: Vec, + headers: Vec, + formals: Vec, + returns: Vec, + returned: Option, + calls: Vec, + edges: Vec, + graph: Option, + constructor_outputs: Vec, + sequence_outputs: Vec, +} +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + match (&self.owner, &other.owner) { + (Some(a), Some(b)) if original_function_equal(a, b) => {} + (None, None) => {} + _ => return false, + } + self.local_map == other.local_map + && self.values == other.values + && self.headers == other.headers + && self.formals == other.formals + && self.returns == other.returns + && self.returned == other.returned + && expression_rows_equal(&self.calls, &other.calls) + && self.edges == other.edges + && self.graph == other.graph + && self.constructor_outputs == other.constructor_outputs + && self.sequence_outputs == other.sequence_outputs + } +} +impl Witness { + pub(super) fn needed(&self) -> bool { + !self.values.is_empty() + || !self.headers.is_empty() + || !self.formals.is_empty() + || self.returned.is_some() + || !self.calls.is_empty() + } + pub(super) fn original_storage_for_expression( + &self, + expression: &Expression, + ) -> Option<&GraphStorageProof> { + self.values + .iter() + .find(|row| equal(&row.original, expression)) + .map(|row| &row.proof) + } + pub(super) fn original_storage_for_local(&self, local: &Local) -> Option<&GraphStorageProof> { + self.headers + .iter() + .find(|row| row.original == *local) + .map(|row| &row.proof) + } + pub(super) fn original_storage_for_formal( + &self, + parameter: &Param, + ) -> Option<&GraphStorageProof> { + self.formals + .iter() + .find(|row| row.original == *parameter) + .map(|row| &row.proof) + } + pub(super) fn original_storage_for_return(&self) -> Option<&GraphStorageProof> { + self.returned.as_ref() + } + pub(super) fn has_source_call(&self, expression: &Expression) -> bool { + self.calls + .iter() + .any(|original| equal(original, expression)) + } + pub(super) fn original_source_edge( + &self, + expression: &Expression, + parameter: usize, + ) -> Option<&GraphSourceEdgeProof> { + self.edges + .iter() + .find(|edge| edge.parameter == parameter && equal(&edge.original_call, expression)) + } +} + +fn equal(left: &Expression, right: &Expression) -> bool { + crate::breakpoint_regions::expressions_equal(left, right) +} +fn expression_rows_equal(left: &[Expression], right: &[Expression]) -> bool { + left.len() == right.len() && left.iter().zip(right).all(|(a, b)| equal(a, b)) +} +fn merge(facts: impl IntoIterator) -> Option { + let mut alternatives = Vec::new(); + for fact in facts { + if alternatives + .first() + .is_some_and(|old: &GraphStorageProof| old.ty != fact.ty) + { + return None; + } + let rows = match fact.shape { + Shape::Alternatives(rows) => rows, + _ => vec![fact], + }; + for row in rows { + if !alternatives.contains(&row) { + alternatives.push(row); + } + } + } + match alternatives.len() { + 0 => None, + 1 => alternatives.pop(), + _ => Some(GraphStorageProof { + ty: alternatives[0].ty, + shape: Shape::Alternatives(alternatives), + }), + } +} +fn hook_projection(proof: &GraphStorageProof) -> Option { + let mut producers = Vec::new(); + fn collect( + proof: &GraphStorageProof, + producers: &mut Vec, + ) -> Option<()> { + match &proof.shape { + Shape::Hook(instruction) => { + if !producers.contains(instruction) { + producers.push(instruction.clone()); + } + } + Shape::Alternatives(rows) => { + for row in rows { + collect(row, producers)?; + } + } + _ => return None, + } + Some(()) + } + collect(proof, &mut producers)?; + let hir::ActorGraphOperation::HookBox { hook, .. } = producers.first()?.operation() else { + return None; + }; + if hook.function_type() != proof.ty || producers.iter().any(|instruction| + !matches!(instruction.operation(),hir::ActorGraphOperation::HookBox {hook:other,..} if other==hook) + || instruction.projected_hook().is_none()) { return None; } + Some(GraphHookProjectionProof { + ty: proof.ty, + hook: hook.clone(), + producers, + }) +} + +/// Function signature types are not occupied values here. Only this private +/// rooted proof permits that distinction; callers cannot query by TypeId. +fn storage_safe(types: &TypeInterner, ty: TypeId) -> bool { + fn visit(types: &TypeInterner, ty: TypeId, seen: &mut BTreeSet) -> bool { + if ty.index() as usize >= types.len() { + return false; + } + if !seen.insert(ty.index()) { + return true; + } + match types.resolve(ty) { + Type::Resource(_) | Type::Capability(_) | Type::TypeConstruction | Type::Error => false, + Type::Function { .. } => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => visit(types, *inner, seen), + Type::Map(key, value) | Type::Result(key, value) => { + visit(types, *key, seen) && visit(types, *value, seen) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field)| visit(types, *field, seen)), + Type::Actor(id) => { + let actor = types.resolve_actor(*id); + actor + .capability_params + .iter() + .chain(&actor.state_fields) + .all(|(_, field)| visit(types, *field, seen)) + } + // These shapes need a protected constructor/variant path in a later + // packet. The absence of a row is a proof obligation, not a ban. + Type::Interface(_) | Type::Enum(_) | Type::Machine(_) | Type::MachineState { .. } => { + false + } + _ => true, + } + } + visit(types, ty, &mut BTreeSet::new()) +} + +pub(super) use original::OriginalGraphAnalysis; + +#[cfg(test)] +fn capture( + original: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, +) -> Result { + OriginalGraphAnalysis::prepare(archive, types)? + .capture(original, archive, types) + .cloned() +} +pub(super) fn preserve_original_call(witness: &Witness, expression: &Expression) -> bool { + witness.has_source_call(expression) +} +pub(super) use current::{ + current, remap_blocks, remap_locals, seal, sequence_transition, validate, +}; +pub(super) fn has_records( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, +) -> bool { + parent.graph_metadata.needed() + && parent.current(function).is_ok() + && validate(parent, function, types).is_ok() +} + +// Only a completed private analysis for this exact archive can establish +// absence. Empty current rows are required too; malformed positive records +// continue through the independent original/current validation below. +fn no_original_or_current_records(parent: &ResourceLoweringWitness) -> bool { + !parent.graph_metadata.needed() && parent.execution.has_no_original_graph_rows(&parent.source) +} + +pub(super) fn storage_for_expression( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + expression: &Expression, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + current::selection(parent, function, types, expression) +} +pub(super) fn storage_for_local( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + local: &Local, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + (function + .local(local.id) + .is_some_and(|header| std::ptr::eq(header, local))) + .then_some(())?; + parent + .graph_metadata + .headers + .iter() + .find(|row| row.current == *local) + .map(|row| row.proof.clone()) + .or_else(|| { + parent + .graph_metadata + .constructor_outputs + .iter() + .find(|row| row.current == *local) + .map(|row| row.proof.clone()) + }) + .or_else(|| { + parent + .graph_metadata + .sequence_outputs + .iter() + .find(|row| row.current == *local) + .map(|row| row.proof.clone()) + }) +} +pub(super) fn storage_for_constructor( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + node: &ResourceConstructorNode, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + let occurrences = function.blocks.iter().flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, StatementKind::ResourceConstructor(actual) if std::ptr::eq(actual, node))) + .count(); + (occurrences == 1).then_some(())?; + parent + .graph_metadata + .constructor_outputs + .iter() + .find(|row| row.constructor == node.constructor()) + .map(|row| row.proof.clone()) +} +pub(super) fn storage_for_formal( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + parameter: &Param, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + function + .params + .iter() + .any(|header| std::ptr::eq(header, parameter)) + .then_some(())?; + parent + .graph_metadata + .formals + .iter() + .find(|row| row.current == *parameter) + .map(|row| row.proof.clone()) +} +pub(super) fn storage_for_return( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + parent.graph_metadata.returned.clone() +} +pub(super) fn projection_for_expression( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + expression: &Expression, +) -> Option { + hook_projection(&storage_for_expression( + parent, function, types, expression, + )?) +} +pub(super) fn source_edge( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + expression: &Expression, + parameter: usize, +) -> Option { + if no_original_or_current_records(parent) { + return None; + } + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + current::contains(function, expression).then_some(())?; + parent + .graph_metadata + .edges + .iter() + .find(|edge| edge.parameter == parameter && equal(&edge.current_call, expression)) + .cloned() +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/graph_metadata/current.rs b/crates/jett_mir/src/resource_ownership/graph_metadata/current.rs new file mode 100644 index 00000000..fc80e05f --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/graph_metadata/current.rs @@ -0,0 +1,653 @@ +//! Constructor-only Source transport and independent current graph seal. +use super::*; +mod constructor_transport; +pub(super) use constructor_transport::ConstructorOutput; +use constructor_transport::{Transport, outputs}; + +#[derive(Debug, Clone)] +pub(super) struct Snapshot { + id: FunctionId, + identity: FunctionIdentity, + debug: hir::FunctionDebugKind, + capture_count: usize, + span: jett_common::Span, + return_type: TypeId, + parameters: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, +} +impl PartialEq for Snapshot { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.identity == other.identity + && self.debug == other.debug + && self.capture_count == other.capture_count + && self.span == other.span + && self.return_type == other.return_type + && self.parameters == other.parameters + && self.locals == other.locals + && self.entry == other.entry + && crate::breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} +impl Snapshot { + fn new(function: &Function) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug: function.debug_kind.clone(), + capture_count: function.capture_count, + span: function.span, + return_type: function.return_type, + parameters: function.params.clone(), + locals: function.locals.clone(), + blocks: function.blocks.clone(), + entry: function.entry, + } + } + fn current(&self, function: &Function) -> Result<(), String> { + if self.id != function.id + || self.identity != function.identity + || self.debug != function.debug_kind + || self.capture_count != function.capture_count + || self.span != function.span + || self.return_type != function.return_type + || self.parameters != function.params + || self.locals != function.locals + || self.entry != function.entry + || !crate::breakpoint_regions::blocks_equal(&self.blocks, &function.blocks) + { + return Err("graph metadata changed its independent current Source/CFG seal".into()); + } + Ok(()) + } +} +pub(super) fn contains(function: &Function, expression: &Expression) -> bool { + let mut found = false; + for block in &function.blocks { + walk::mir_block(block, &mut |value| found |= std::ptr::eq(value, expression)); + } + found +} +fn count(function: &Function, expression: &Expression) -> usize { + let mut count = 0; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + count += usize::from(equal(value, expression)) + }); + } + count +} +fn remap_expression(value: &mut Expression, remap: &mut impl FnMut(&mut LocalId)) { + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(value.clone()), + span: value.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(value_remapped) = wrapper.statements.remove(0).kind { + *value = value_remapped; + } +} +fn transported( + value: &Expression, + witness: &Witness, + constructors: &[ResourceConstructorRegion], + function: &Function, +) -> Result { + Transport { + witness, + constructors, + function, + } + .expression(value) +} +fn header_same(original: &Local, current: &Local, witness: &Witness) -> bool { + let mut restored = current.clone(); + restored.id = original.id; + if let Some(source) = &mut restored.view_source { + let Some((original, _)) = witness + .local_map + .iter() + .find(|(_, current)| current.id == *source) + else { + return false; + }; + *source = original.id; + } + restored == *original +} + +/// Called once by the authenticated lowering constructor, after the complete +/// body is emitted. Public edits cannot call this or refresh this snapshot. +pub(in crate::resource_ownership) fn seal( + witness: &mut Witness, + function: &Function, + calls: &[OriginalCallAssociation], + constructors: &[ResourceConstructorRegion], + types: &TypeInterner, +) -> Result<(), String> { + if !witness.needed() { + return Ok(()); + } + if witness.graph.is_some() { + return Err("graph metadata cannot reseal an existing constructor witness".into()); + } + let original = witness + .owner + .as_ref() + .ok_or("graph metadata lost original owner")?; + if original.id != function.id + || original.identity != function.identity + || original.return_type != function.return_type + || original.span != function.span + || original.capture_count != function.capture_count + || original.debug_kind != function.debug_kind + || original.params != function.params + || function.locals.len() < original.locals.len() + || original + .locals + .iter() + .zip(&function.locals) + .any(|(left, right)| left != right) + { + return Err("graph metadata constructor changed its original Source/header prefix".into()); + } + witness.constructor_outputs = outputs(&witness.values, constructors, function)?; + let currents = witness + .values + .iter() + .chain(&witness.returns) + .map(|row| transported(&row.original, witness, constructors, function)) + .collect::, _>>()?; + for (row, current) in witness + .values + .iter_mut() + .chain(&mut witness.returns) + .zip(currents) + { + row.current = current; + } + let edge_currents = witness + .edges + .iter() + .map(|edge| transported(&edge.original_call, witness, constructors, function)) + .collect::, _>>()?; + for (edge, expected) in witness.edges.iter_mut().zip(edge_currents) { + let matching = calls + .iter() + .filter(|call| equal(&call.original, &edge.original_call)) + .collect::>(); + let [call] = matching.as_slice() else { + return Err( + "graph Source transport lost unique original/current call association".into(), + ); + }; + // The initial packet preserves whole metadata Source calls. Canonical + // staging that changes the packet requires its own producer transcript. + if !equal(&expected, &call.current) { + return Err( + "pending graph metadata: staged Source actual needs its exact producer transcript" + .into(), + ); + } + edge.current_call = call.current.clone(); + } + for original in &witness.calls { + let expected = transported(original, witness, constructors, function)?; + if calls + .iter() + .filter(|call| equal(&call.original, original) && equal(&call.current, &expected)) + .count() + != 1 + || count(function, &expected) != 1 + { + return Err("graph metadata call changed original tuple or current occurrence".into()); + } + } + for row in &witness.headers { + if function.local(row.current.id) != Some(&row.current) + || row.proof.ty != row.current.ty + || !storage_safe(types, row.proof.ty) + { + return Err("graph storage changed exact constructor local".into()); + } + if !function + .params + .iter() + .any(|parameter| parameter.local == row.current.id) + { + let initializers = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == row.current.id => Some(value), + _ => None, + }) + .collect::>(); + if let [initializer]=initializers.as_slice() { + if !witness.values.iter().any(|value|equal(&value.current,initializer) && value.proof==row.proof) { + return Err("graph storage local differs from protected original producer".into()); + } + } else if initializers.is_empty() && original_iteration(original,row.original.id).is_some_and(|iterable| + function.blocks.iter().filter(|block|matches!(&block.terminator.kind,TerminatorKind::ForEach {key,value,iterable:current,..} + if (*key==row.current.id || *value==Some(row.current.id)) && transported(iterable,witness,constructors,function).is_ok_and(|expected|equal(current,&expected)))).count()==1) { + // Exact raw ForEach binder. A later prepared sequence requires + // the independently reconstructed control-flow transition. + } else { + return Err("graph storage has no unique constructor local or iteration producer".into()); + } + } + } + for row in &witness.returns { + if !function + .blocks + .iter() + .any(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(value)) | TerminatorKind::Respond(value) => { + equal(value, &row.current) + } + _ => false, + }) + { + return Err("graph return lost original/current exit expression".into()); + } + } + witness.graph = Some(Snapshot::new(function)); + Ok(()) +} + +pub(in crate::resource_ownership) fn validate( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, +) -> Result<(), String> { + // The parent runs its full-body/header gate before this typed reconstruction. + // This function must not recurse through the parent's current seal. + let witness = &parent.graph_metadata; + let expected = parent + .execution + .original_graph(&parent.original, &parent.source, types)?; + if !constructors::records_equal(&parent.constructors, &parent.constructor_seal) { + return Err("graph transport lost its independent constructor occurrence seal".into()); + } + if witness.needed() != expected.needed() { + return Err("graph metadata lost original checked root membership".into()); + } + if !witness.needed() { + return Ok(()); + } + witness + .graph + .as_ref() + .ok_or("graph metadata lost independent current graph")? + .current(function)?; + let owner = witness + .owner + .as_ref() + .ok_or("graph metadata lost original Source owner")?; + if !original_function_equal(owner, &parent.original) + || witness.returned != expected.returned + || !expression_rows_equal(&witness.calls, &expected.calls) + || witness.values.len() != expected.values.len() + || witness.headers.len() != expected.headers.len() + || witness.formals.len() != expected.formals.len() + || witness.returns.len() != expected.returns.len() + || witness.edges.len() != expected.edges.len() + { + return Err( + "graph metadata differs from reconstructed original whole-root Source rows".into(), + ); + } + if witness.constructor_outputs != outputs(&expected.values, &parent.constructor_seal, function)? + { + return Err("graph transport changed its exact generated constructor output rows".into()); + } + for (row, original) in witness + .values + .iter() + .chain(&witness.returns) + .zip(expected.values.iter().chain(&expected.returns)) + { + if !equal(&row.original, &original.original) + || row.proof != original.proof + || !storage_safe(types, row.proof.ty) + || !equal( + &row.current, + &transported(&row.original, witness, &parent.constructor_seal, function)?, + ) + { + return Err( + "graph metadata changed original/current expression producer or rooted shape" + .into(), + ); + } + } + for row in &witness.sequence_outputs { + if function.local(row.current.id) != Some(&row.current) + || row.current.ty != row.proof.ty + || !storage_safe(types, row.proof.ty) + || !expected.values.iter().any(|value| { + equal(&value.original, &row.original_iterable) && value.proof == row.proof + }) + { + return Err("graph iterator changed its exact generated source/proof".into()); + } + let producer = transported( + &row.original_iterable, + witness, + &parent.constructor_seal, + function, + )?; + if function.blocks.iter().flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, + StatementKind::Let { local, value } if *local == row.current.id && equal(value, &producer))) + .count() != 1 + { + return Err("graph iterator lost its unique protected source initializer".into()); + } + } + for (index, (original, current)) in witness.local_map.iter().enumerate() { + if owner.locals.get(original.id.index() as usize) != Some(original) + || !header_same(original, current, witness) + || function.local(current.id) != Some(current) + || witness.local_map[..index] + .iter() + .any(|(old, now)| old.id == original.id || now.id == current.id) + { + return Err("graph metadata changed its canonical original/current local map".into()); + } + } + for (row, original) in witness.headers.iter().zip(&expected.headers) { + if row.original != original.original + || row.proof != original.proof + || !header_same(&row.original, &row.current, witness) + || function.local(row.current.id) != Some(&row.current) + { + return Err("graph metadata changed exact original/current local header".into()); + } + } + for (row, original) in witness.formals.iter().zip(&expected.formals) { + let mut restored = row.current.clone(); + restored.local = row.original.local; + if row.original != original.original + || row.proof != original.proof + || restored != row.original + || !function.params.contains(&row.current) + { + return Err( + "graph metadata changed exact original/current formal Source header".into(), + ); + } + } + for (edge, original) in witness.edges.iter().zip(&expected.edges) { + if !equal(&edge.original_call, &original.original_call) + || edge.formal != original.formal + || edge.parameter != original.parameter + || !equal(&edge.actual, &original.actual) + || edge.proof != original.proof + || !parent.calls.iter().any(|call| { + equal(&call.original, &edge.original_call) + && equal(&call.current, &edge.current_call) + }) + || !equal( + &edge.current_call, + &transported( + &edge.original_call, + witness, + &parent.constructor_seal, + function, + )?, + ) + || count(function, &edge.current_call) != 1 + { + return Err( + "graph metadata lost full original/current Source actual/formal tuple".into(), + ); + } + } + Ok(()) +} +pub(super) fn selection( + parent: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, + expression: &Expression, +) -> Option { + parent.current(function).ok()?; + validate(parent, function, types).ok()?; + contains(function, expression).then_some(())?; + parent + .graph_metadata + .values + .iter() + .find(|row| equal(&row.current, expression)) + .map(|row| row.proof.clone()) +} +pub(in crate::resource_ownership) fn current( + witness: &Witness, + function: &Function, +) -> Result<(), String> { + if !witness.needed() { + return Ok(()); + } + witness + .graph + .as_ref() + .ok_or("graph metadata lost independent current graph")? + .current(function) +} + +pub(in crate::resource_ownership) fn remap_locals( + witness: &mut Witness, + map: &[Option], + remap: &mut impl FnMut(&mut LocalId), +) { + if let Some(graph) = &mut witness.graph { + for parameter in &mut graph.parameters { + remap(&mut parameter.local); + } + graph.locals.retain(|local| { + map.get(local.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for local in &mut graph.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + for block in &mut graph.blocks { + crate::sequences::prune::block_locals(block, remap, &mut |_| {}); + } + } + witness.local_map.retain(|(_, current)| { + map.get(current.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for (_, current) in &mut witness.local_map { + remap(&mut current.id); + if let Some(source) = &mut current.view_source { + remap(source); + } + } + for row in &mut witness.headers { + remap(&mut row.current.id); + if let Some(source) = &mut row.current.view_source { + remap(source); + } + } + for row in &mut witness.sequence_outputs { + remap(&mut row.current.id); + if let Some(source) = &mut row.current.view_source { + remap(source); + } + } + for row in &mut witness.constructor_outputs { + remap(&mut row.current.id); + if let Some(source) = &mut row.current.view_source { + remap(source); + } + } + for row in &mut witness.formals { + remap(&mut row.current.local); + } + for row in witness.values.iter_mut().chain(&mut witness.returns) { + remap_expression(&mut row.current, remap); + } + for edge in &mut witness.edges { + remap_expression(&mut edge.current_call, remap); + } +} +fn original_iteration(function: &hir::Function, local: LocalId) -> Option<&Expression> { + fn find(block: &hir::Block, local: LocalId) -> Option<&Expression> { + for statement in &block.statements { + let found = match &statement.kind { + hir::StatementKind::For { + key, + value, + iterable, + .. + } if *key == local || *value == Some(local) => Some(iterable), + hir::StatementKind::For { body, .. } + | hir::StatementKind::While { body, .. } + | hir::StatementKind::Scope(body) => find(body, local), + hir::StatementKind::If { + then_block, + else_block, + .. + } => find(then_block, local) + .or_else(|| else_block.as_ref().and_then(|block| find(block, local))), + hir::StatementKind::Match { arms, .. } => { + arms.iter().find_map(|arm| find(&arm.body, local)) + } + hir::StatementKind::ReflectedTypeDispatch { arms, .. } => { + arms.iter().find_map(|arm| find(&arm.body, local)) + } + _ => None, + }; + if found.is_some() { + return found; + } + } + None + } + find(&function.body, local) +} +pub(in crate::resource_ownership) fn remap_blocks( + witness: &mut Witness, + map: &[Option], +) -> Result<(), String> { + let Some(graph) = &mut witness.graph else { + return Ok(()); + }; + let missing = std::cell::Cell::new(false); + let check = |id: &mut BlockId| { + if map.get(id.index() as usize).copied().flatten().is_none() { + missing.set(true); + } + }; + let mut entry = graph.entry; + check(&mut entry); + for block in &graph.blocks { + if map + .get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + { + let mut terminator = block.terminator.kind.clone(); + crate::sequences::prune::block_targets(&mut terminator, &check); + } + } + if missing.get() { + return Err("graph metadata canonical block map removes retained entry or edge".into()); + } + let remap = |id: &mut BlockId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } + }; + graph.blocks.retain(|block| { + map.get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + remap(&mut graph.entry); + for block in &mut graph.blocks { + remap(&mut block.id); + crate::sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + Ok(()) +} + +/// Advances only the canonical iterator body independently reconstructed by +/// control_flow. Original Source headers remain untouched; this records the +/// one generated owner initialized from the protected current iterable. +pub(in crate::resource_ownership) fn sequence_transition( + witness: &mut Witness, + before: &Function, + expected: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, +) -> Result<(), String> { + if !witness.needed() { + return Ok(()); + } + current(witness, before)?; + let TerminatorKind::ForEach { iterable, .. } = &edit.before.kind else { + return Err("graph sequence transport has no exact original iterator".into()); + }; + if let Some(row) = witness + .values + .iter() + .find(|row| equal(&row.current, iterable)) + { + let proof = row.proof.clone(); + let original_iterable = row.original.clone(); + let [(_, initialization)] = edit.append.as_slice() else { + return Err("graph sequence transport lost its unique preheader".into()); + }; + let Some(Statement { + kind: StatementKind::Let { local, value }, + .. + }) = initialization.first() + else { + return Err("graph sequence transport lost its canonical source initializer".into()); + }; + if !equal(value, iterable) { + return Err("graph sequence transport substituted its protected iterable".into()); + } + let current = expected + .local(*local) + .filter(|header| header.ty == proof.ty) + .ok_or("graph sequence transport changed its generated source header")? + .clone(); + if witness + .sequence_outputs + .iter() + .any(|row| row.current.id == current.id) + { + return Err("graph sequence transport repeated its generated source owner".into()); + } + witness.sequence_outputs.push(SequenceOutput { + current, + original_iterable, + proof, + }); + } + witness.graph = Some(Snapshot::new(expected)); + Ok(()) +} diff --git a/crates/jett_mir/src/resource_ownership/graph_metadata/current/constructor_transport.rs b/crates/jett_mir/src/resource_ownership/graph_metadata/current/constructor_transport.rs new file mode 100644 index 00000000..8ac67261 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/graph_metadata/current/constructor_transport.rs @@ -0,0 +1,161 @@ +//! Forward transport through the existing independently sealed constructor lifecycle. +use super::*; + +#[derive(Debug, Clone)] +pub(in crate::resource_ownership::graph_metadata) struct ConstructorOutput { + pub(in crate::resource_ownership::graph_metadata) constructor: ResourceConstructorId, + pub(super) original: Expression, + pub(in crate::resource_ownership::graph_metadata) current: Local, + pub(in crate::resource_ownership::graph_metadata) proof: GraphStorageProof, +} +impl PartialEq for ConstructorOutput { + fn eq(&self, other: &Self) -> bool { + self.constructor == other.constructor + && equal(&self.original, &other.original) + && self.current == other.current + && self.proof == other.proof + } +} + +pub(super) fn outputs( + values: &[ValueRow], + constructors: &[ResourceConstructorRegion], + function: &Function, +) -> Result, String> { + let mut outputs = Vec::new(); + for constructor in constructors { + let Some(value) = values + .iter() + .find(|value| equal(&value.original, constructor.original())) + else { + continue; + }; + constructor.current(function)?; + let (_, output) = constructor + .commit() + .ok_or("graph constructor lost its sealed Commit")?; + if value.proof.ty != output.ty { + return Err("graph constructor output changed its rooted producer type".into()); + } + outputs.push(ConstructorOutput { + constructor: constructor.id(), + original: constructor.original().clone(), + current: output.clone(), + proof: value.proof.clone(), + }); + } + Ok(outputs) +} + +pub(super) struct Transport<'a> { + pub(super) witness: &'a Witness, + pub(super) constructors: &'a [ResourceConstructorRegion], + pub(super) function: &'a Function, +} +impl Transport<'_> { + fn local(&self, original: LocalId) -> Result { + self.witness + .local_map + .iter() + .find(|(header, _)| header.id == original) + .map(|(_, current)| current.id) + .ok_or_else(|| "graph transport lost its exact original/current local".into()) + } + + fn constructor(&self, output: &ConstructorOutput) -> Result { + let constructor = self + .constructors + .get(output.constructor.index()) + .filter(|constructor| constructor.id() == output.constructor) + .ok_or("graph transport lost its independently sealed constructor")?; + constructor.current(self.function)?; + if !equal(constructor.original(), &output.original) + || constructor + .commit() + .is_none_or(|(_, current)| current != &output.current) + || output.proof.ty != output.current.ty + { + return Err("graph transport changed its exact constructor producer/output".into()); + } + for field in constructor.fields() { + if !equal(&self.expression(field.original())?, field.endpoint()) { + return Err("graph transport changed its exact constructor field endpoint".into()); + } + } + Ok(Expression { + kind: E::Local(output.current.id), + ty: output.original.ty, + span: output.original.span, + }) + } + + pub(super) fn expression(&self, value: &Expression) -> Result { + let mut matches = self + .witness + .constructor_outputs + .iter() + .filter(|output| equal(&output.original, value)); + if let Some(output) = matches.next() { + if matches.next().is_some() { + return Err("graph transport has duplicate original constructor producers".into()); + } + return self.constructor(output); + } + let mut result = value.clone(); + let ownership = match &mut result.kind { + E::Call { ownership, .. } + | E::IndirectCall { ownership, .. } + | E::Intrinsic { ownership, .. } + | E::ResourceInvoke { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(ownership) = ownership { + ownership.remap_metadata_locals(|local| self.local(local))?; + } + match &mut result.kind { + E::ActorGraphImport(plan) => result.kind = E::Local(self.local(plan.output())?), + E::Local(local) => *local = self.local(*local)?, + E::Clone(inner) + | E::View(inner) + | E::OptionalSome(inner) + | E::ResultOk(inner) + | E::ResultFail(inner) => **inner = self.expression(inner)?, + E::Field { base, .. } => **base = self.expression(base)?, + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for field in fields { + *field = self.expression(field)?; + } + } + E::ListConstruct { elements } => { + for element in elements { + *element = self.expression(element)?; + } + } + E::MapConstruct { entries } => { + for entry in entries { + entry.key = self.expression(&entry.key)?; + entry.value = self.expression(&entry.value)?; + } + } + E::Call { args, .. } | E::Intrinsic { args, .. } | E::ActorGraphStep { args, .. } => { + for argument in args { + *argument = self.expression(argument)?; + } + } + E::IndirectCall { callee, args, .. } => { + **callee = self.expression(callee)?; + for argument in args { + *argument = self.expression(argument)?; + } + } + E::ActorMessage { actor, args, .. } => { + **actor = self.expression(actor)?; + for argument in args { + *argument = self.expression(argument)?; + } + } + _ => {} + } + Ok(result) + } +} diff --git a/crates/jett_mir/src/resource_ownership/graph_metadata/original.rs b/crates/jett_mir/src/resource_ownership/graph_metadata/original.rs new file mode 100644 index 00000000..be539d53 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/graph_metadata/original.rs @@ -0,0 +1,1696 @@ +//! Reconstruct facts from the immutable archive. Unknown inputs stay unknown. +use super::*; +use hir::StatementKind as H; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Clone, Default)] +struct Rows { + values: Vec, + headers: Vec, + formals: Vec, + returns: Vec, + returned: Option, + calls: Vec, + edges: Vec, + incoming: Vec<(Expression, FunctionId, usize, Option)>, + messages: Vec<(Expression, FunctionId, Option)>, + unknown_return: bool, +} +impl PartialEq for Rows { + fn eq(&self, other: &Self) -> bool { + self.values == other.values + && self.headers == other.headers + && self.formals == other.formals + && self.returns == other.returns + && self.returned == other.returned + && expression_rows_equal(&self.calls, &other.calls) + && self.edges == other.edges + && self.incoming.len() == other.incoming.len() + && self.incoming.iter().zip(&other.incoming).all( + |( + (a, target, parameter, proof), + (b, other_target, other_parameter, other_proof), + )| { + equal(a, b) + && target == other_target + && parameter == other_parameter + && proof == other_proof + }, + ) + && self.messages.len() == other.messages.len() + && self.messages.iter().zip(&other.messages).all( + |((a, target, proof), (b, other_target, other_proof))| { + equal(a, b) && target == other_target && proof == other_proof + }, + ) + && self.unknown_return == other.unknown_return + } +} +#[derive(Debug, Clone, PartialEq)] +struct Factory { + plan: hir::ActorGraphPlan, + output: Option, + rows: Rows, + fields: Vec<(usize, usize, GraphStorageProof)>, + captures: Vec, +} +struct Analysis<'a> { + archive: &'a hir::ResourceSourceArchive, + types: &'a TypeInterner, + factories: &'a [Factory], + previous: &'a [Rows], +} + +fn remember(rows: &mut Rows, expression: &Expression, proof: &GraphStorageProof) { + if expression.ty != proof.ty { + return; + } + if let Some(row) = rows + .values + .iter_mut() + .find(|row| equal(&row.original, expression)) + { + // Equal occurrences retain every rooted alternative, never a type-only + // common denominator. Current selection still requires pointer membership. + if let Some(joined) = merge([row.proof.clone(), proof.clone()]) { + row.proof = joined; + } + } else { + rows.values.push(ValueRow { + original: expression.clone(), + current: expression.clone(), + proof: proof.clone(), + }); + } +} +fn local_header(function: &hir::Function, local: LocalId, ty: TypeId) -> Option<&Local> { + function + .locals + .get(local.index() as usize) + .filter(|header| header.id == local && header.ty == ty) +} +fn written(function: &hir::Function) -> HashSet { + fn walk_block(block: &hir::Block, found: &mut HashSet) { + for statement in &block.statements { + match &statement.kind { + H::Assign { target, .. } | H::UpdateField { target, .. } => { + let mut base = target; + while let E::Field { base: inner, .. } | E::View(inner) = &base.kind { + base = inner; + } + if let E::Local(local) = base.kind { + found.insert(local); + } + } + H::Scope(body) | H::While { body, .. } | H::For { body, .. } => { + walk_block(body, found) + } + H::If { + then_block, + else_block, + .. + } => { + walk_block(then_block, found); + if let Some(body) = else_block { + walk_block(body, found); + } + } + H::Match { arms, .. } => { + for arm in arms { + walk_block(&arm.body, found); + } + } + H::ReflectedTypeDispatch { arms, .. } => { + for arm in arms { + walk_block(&arm.body, found); + } + } + _ => {} + } + } + } + let mut found = HashSet::new(); + walk_block(&function.body, &mut found); + walk::hir_block(&function.body, &mut |value| match &value.kind { + E::Handle { failure, .. } => walk_block(failure, &mut found), + E::InlineFunction { body, .. } => walk_block(body, &mut found), + _ => {} + }); + found +} + +// This proof is deliberately smaller than purity: a pure actor helper may +// change actor state. Exact direct Source calls are followed transitively; +// unknown dispatch/effect paths withhold the frozen fact rather than becoming +// an assumed state-preservation edge. +fn unchanged_handler( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, + active: &mut HashSet, +) -> bool { + if !written(function).is_empty() || !active.insert(function.id) { + return false; + } + let mut unchanged = true; + walk::hir_block(&function.body, &mut |value| { + if !unchanged { + return; + } + match &value.kind { + E::Call { .. } => { + unchanged = source_call(value, archive) + .is_some_and(|(target, _, _)| unchanged_handler(target, archive, active)); + } + E::IndirectCall { .. } + | E::Intrinsic { .. } + | E::ActorMessage { .. } + | E::ActorSpawn { .. } + | E::Run(_) + | E::Join(_) + | E::Cancel(_) + | E::ResourceInvoke { .. } + | E::ActorGraphImport(_) + | E::ActorGraphStep { .. } => unchanged = false, + _ => {} + } + }); + active.remove(&function.id); + unchanged +} +fn plain( + plan: &hir::ActorGraphPlan, + value: &Expression, + types: &TypeInterner, +) -> Option { + storage_safe(types, value.ty).then(|| GraphStorageProof { + ty: value.ty, + shape: Shape::Plain { + plan: plan.clone(), + value: value.clone(), + }, + }) +} +fn fields( + proof: &GraphStorageProof, + field: usize, + owner: TypeId, + result: TypeId, + types: &TypeInterner, +) -> Option { + if proof.ty != owner { + return None; + } + let Type::Struct(id) = types.resolve(owner) else { + return None; + }; + if types.resolve_struct(*id).fields.get(field)?.1 != result { + return None; + } + let result_proof = match &proof.shape { + Shape::Fields(values) => values.get(field)?.clone()?, + Shape::Alternatives(alternatives) => merge( + alternatives + .iter() + .map(|alternative| fields(alternative, field, owner, result, types)) + .collect::>>()?, + )?, + _ => return None, + }; + (result_proof.ty == result).then_some(result_proof) +} +fn rooted_aggregate( + ty: TypeId, + values: Vec>, + list: bool, + types: &TypeInterner, +) -> Option { + if !storage_safe(types, ty) || !values.iter().any(Option::is_some) { + return None; + } + Some(GraphStorageProof { + ty, + shape: if list { + Shape::Items(values) + } else { + Shape::Fields(values) + }, + }) +} + +fn source_call<'a>( + value: &'a Expression, + archive: &'a hir::ResourceSourceArchive, +) -> Option<( + &'a hir::Function, + &'a [Expression], + &'a hir::SourceCallOwnership, +)> { + let E::Call { + function, + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + } = &value.kind + else { + return None; + }; + let target = archive + .execution_functions() + .get(function.index() as usize) + .filter(|target| target.id == *function)?; + if target.capture_count != 0 + || target.params.len() != args.len() + || target.return_type != value.ty + || source.bridge != hir::CallBridge::Direct + || !source.generated_operands.is_empty() + || source.arguments.len() != args.len() + || !matches!(&source.target,hir::CallTarget::Function(id) if id==function) + && !matches!(&source.target,hir::CallTarget::Declaration {identity,..} if identity==&target.identity) + || evaluation_order.len() != args.len() + { + return None; + } + let mut order = evaluation_order.clone(); + order.sort_unstable(); + if order != (0..args.len()).collect::>() { + return None; + } + for (index, ((actual, parameter), argument)) in args + .iter() + .zip(&target.params) + .zip(&source.arguments) + .enumerate() + { + if argument.parameter_index != index + || argument.actual_type != actual.ty + || argument.parameter_type != parameter.ty + || argument.staging != hir::ArgumentStaging::Original + || argument.source_witness().occurrence_type() != actual.ty + || argument.source_witness().context() != source.context + { + return None; + } + } + Some((target, args, source)) +} +fn required_child(ty: TypeId, proof: &Option, types: &TypeInterner) -> bool { + !resource_type_pending(types, ty) || proof.as_ref().is_some_and(|proof| proof.ty == ty) +} + +// Recognize the complete archived producer body, never a name or signature. +// These forms cannot create an occupied resource: the list starts empty and +// every appended item comes from the exact protected owning iterable. +struct CollectionProjection<'a> { + output: LocalId, + initial: &'a Expression, + iterable: &'a Expression, + key: LocalId, + value: LocalId, + assigned: &'a Expression, + returned: &'a Expression, + part: usize, +} +fn list_operation(function: &hir::Function, types: &TypeInterner) -> Option { + let [statement] = function.body.statements.as_slice() else { + return None; + }; + let H::Return(Some(value)) = &statement.kind else { + return None; + }; + let E::Intrinsic { + intrinsic, + type_arguments, + reflection_arguments, + refinement_predicates, + field_validation, + args, + evaluation_order, + .. + } = &value.kind + else { + return None; + }; + let Type::List(item) = types.resolve(function.return_type) else { + return None; + }; + let expected = match intrinsic { + hir::IntrinsicId::ListNew => Vec::new(), + hir::IntrinsicId::ListAppend => vec![function.return_type, *item], + _ => return None, + }; + if function.capture_count != 0 + || value.ty != function.return_type + || !storage_safe(types, value.ty) + || type_arguments.as_slice() != [*item] + || !reflection_arguments.is_empty() + || !refinement_predicates.is_empty() + || field_validation.is_some() + || function.params.len() != expected.len() + || args.len() != expected.len() + || evaluation_order != &(0..args.len()).collect::>() + { + return None; + } + for ((parameter, argument), expected) in function.params.iter().zip(args).zip(expected) { + if parameter.ty != expected + || parameter.mode != hir::ParamMode::Owned + || parameter.mutable + || argument.ty != expected + || !matches!(argument.kind, E::Local(id) if id == parameter.local) + || local_header(function, parameter.local, expected)? + .view_source + .is_some() + { + return None; + } + } + Some(*intrinsic) +} +fn collection_projection<'a>( + function: &'a hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, +) -> Option> { + let [initial, iteration, returned] = function.body.statements.as_slice() else { + return None; + }; + let H::Let { + local: output, + value: initial, + } = &initial.kind + else { + return None; + }; + let H::For { + key, + value: Some(value), + iterable, + body, + by_view: false, + } = &iteration.kind + else { + return None; + }; + let [assignment] = body.statements.as_slice() else { + return None; + }; + let H::Assign { + target, + value: assigned, + } = &assignment.kind + else { + return None; + }; + let H::Return(Some(returned)) = &returned.kind else { + return None; + }; + let [parameter] = function.params.as_slice() else { + return None; + }; + let Type::Map(key_ty, value_ty) = types.resolve(parameter.ty) else { + return None; + }; + let Type::List(item_ty) = types.resolve(function.return_type) else { + return None; + }; + let (new, new_args, _) = source_call(initial, archive)?; + let (append, append_args, _) = source_call(assigned, archive)?; + let [accumulator, item] = append_args else { + return None; + }; + let part = match item.kind { + E::Local(id) if id == *key && item.ty == *key_ty => 0, + E::Local(id) if id == *value && item.ty == *value_ty => 1, + _ => return None, + }; + let header = local_header(function, *output, function.return_type)?; + if function.capture_count != 0 + || parameter.mode != hir::ParamMode::Owned + || parameter.mutable + || !storage_safe(types, parameter.ty) + || !header.mutable + || header.view_source.is_some() + || !new_args.is_empty() + || list_operation(new, types) != Some(hir::IntrinsicId::ListNew) + || list_operation(append, types) != Some(hir::IntrinsicId::ListAppend) + || initial.ty != function.return_type + || assigned.ty != function.return_type + || target.ty != function.return_type + || returned.ty != function.return_type + || accumulator.ty != function.return_type + || item.ty != *item_ty + || iterable.ty != parameter.ty + || !matches!(iterable.kind, E::Local(id) if id == parameter.local) + || !matches!(target.kind, E::Local(id) if id == *output) + || !matches!(accumulator.kind, E::Local(id) if id == *output) + || !matches!(returned.kind, E::Local(id) if id == *output) + || local_header(function, *key, *key_ty)?.view_source.is_some() + || local_header(function, *value, *value_ty)? + .view_source + .is_some() + { + return None; + } + Some(CollectionProjection { + output: *output, + initial, + iterable, + key: *key, + value: *value, + assigned, + returned, + part, + }) +} +fn collection_formals( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, +) -> bool { + collection_projection(function, archive, types).is_some() + || list_operation(function, types) == Some(hir::IntrinsicId::ListAppend) + && archive.execution_functions().iter().any(|owner| { + collection_projection(owner, archive, types).is_some_and(|projection| { + source_call(projection.assigned, archive) + .is_some_and(|(append, _, _)| append.id == function.id) + }) + }) +} + +impl Analysis<'_> { + fn evaluate( + &self, + value: &Expression, + function: &hir::Function, + locals: &mut HashMap, + rows: &mut Rows, + root: Option<&hir::ActorGraphPlan>, + ) -> Option { + let proof = match &value.kind { + E::ActorGraphStep { instruction, args } => { + let plan = root?; + if !plan.instructions().contains(instruction) + || instruction + .validate(function.id, &function.identity, args, value.ty, self.types) + .is_err() + { + return None; + } + let hir::ActorGraphOperation::HookBox { hook, .. } = instruction.operation() else { + return None; + }; + let projection = instruction.projected_hook()?; + if projection.signature() != value.ty + || hook.function_type() != value.ty + || !projection.belongs_to(instruction.checked()) + { + return None; + } + GraphStorageProof { + ty: value.ty, + shape: Shape::Hook(instruction.clone()), + } + } + E::ActorGraphImport(plan) => { + let factory = self + .factories + .iter() + .find(|factory| factory.plan == *plan)?; + for row in &factory.rows.values { + remember(rows, &row.original, &row.proof); + } + for row in &factory.rows.headers { + locals.insert(row.original.id, row.proof.clone()); + if !rows.headers.iter().any(|old| old.original == row.original) { + rows.headers.push(row.clone()); + } + } + factory.output.clone()? + } + E::Local(local) => locals + .get(local) + .filter(|proof| proof.transferable()) + .cloned()?, + E::Clone(inner) | E::View(inner) if inner.ty == value.ty => { + self.evaluate(inner, function, locals, rows, root)? + } + E::Field { + base, + owner_type, + field, + } => { + let proof = self.evaluate(base, function, locals, rows, root)?; + fields( + &proof, + field.index() as usize, + *owner_type, + value.ty, + self.types, + )? + } + E::StructConstruct { + struct_type, + fields, + .. + } if *struct_type == value.ty => { + let Type::Struct(id) = self.types.resolve(value.ty) else { + return None; + }; + let shape = self.types.resolve_struct(*id); + if shape.fields.len() != fields.len() { + return None; + } + let mut proofs = Vec::new(); + for (expression, (_, ty)) in fields.iter().zip(&shape.fields) { + if expression.ty != *ty { + return None; + } + let proof = self.evaluate(expression, function, locals, rows, root); + if !required_child(*ty, &proof, self.types) { + return None; + } + proofs.push(proof); + } + rooted_aggregate(value.ty, proofs, false, self.types)? + } + E::ListConstruct { elements } => { + let Type::List(item) = self.types.resolve(value.ty) else { + return None; + }; + let mut proofs = Vec::new(); + for expression in elements { + if expression.ty != *item { + return None; + } + let proof = self.evaluate(expression, function, locals, rows, root); + if !required_child(*item, &proof, self.types) { + return None; + } + proofs.push(proof); + } + if elements.is_empty() { + plain(root?, value, self.types)? + } else { + rooted_aggregate(value.ty, proofs, true, self.types)? + } + } + E::MapConstruct { entries } => { + let Type::Map(key, item) = self.types.resolve(value.ty) else { + return None; + }; + let mut proofs = Vec::new(); + for entry in entries { + if entry.key.ty != *key || entry.value.ty != *item { + return None; + } + let key_proof = self.evaluate(&entry.key, function, locals, rows, root); + let value_proof = self.evaluate(&entry.value, function, locals, rows, root); + if !required_child(*key, &key_proof, self.types) + || !required_child(*item, &value_proof, self.types) + { + return None; + } + proofs.push((key_proof, value_proof)); + } + if !storage_safe(self.types, value.ty) { + return None; + } + if proofs + .iter() + .any(|(key, value)| key.is_some() || value.is_some()) + { + GraphStorageProof { + ty: value.ty, + shape: Shape::Entries(proofs), + } + } else { + plain(root?, value, self.types)? + } + } + E::OptionalSome(inner) => { + let Type::Optional(item) = self.types.resolve(value.ty) else { + return None; + }; + let proof = self.evaluate(inner, function, locals, rows, root)?; + if proof.ty != *item || !storage_safe(self.types, value.ty) { + return None; + } + GraphStorageProof { + ty: value.ty, + shape: Shape::Some(Box::new(proof)), + } + } + E::ResultOk(inner) | E::ResultFail(inner) => { + let Type::Result(ok, fail) = self.types.resolve(value.ty) else { + return None; + }; + let is_ok = matches!(value.kind, E::ResultOk(_)); + let proof = self.evaluate(inner, function, locals, rows, root)?; + if proof.ty != if is_ok { *ok } else { *fail } + || !storage_safe(self.types, value.ty) + { + return None; + } + GraphStorageProof { + ty: value.ty, + shape: if is_ok { + Shape::Ok(Box::new(proof)) + } else { + Shape::Fail(Box::new(proof)) + }, + } + } + E::Intrinsic { + intrinsic, args, .. + } if list_operation(function, self.types) == Some(*intrinsic) => { + match intrinsic { + hir::IntrinsicId::ListNew => { + // An empty helper has no incoming operand. It is selected + // only after a protected projection retained this exact + // archived direct call; no type or name can activate it. + if !self.previous.iter().any(|previous| { + previous.calls.iter().any(|call| { + source_call(call, self.archive) + .is_some_and(|(target, _, _)| target.id == function.id) + }) + }) { + return None; + } + GraphStorageProof { + ty: value.ty, + shape: Shape::Items(Vec::new()), + } + } + hir::IntrinsicId::ListAppend => { + let [items, item] = args.as_slice() else { + return None; + }; + let list = self.evaluate(items, function, locals, rows, root)?; + let item = self.evaluate(item, function, locals, rows, root)?; + let mut alternatives = vec![item]; + if let Some(existing) = iteration_part(&list, 0) { + alternatives.insert(0, existing); + } else if !matches!(&list.shape, Shape::Items(items) if items.is_empty()) { + return None; + } + GraphStorageProof { + ty: value.ty, + shape: Shape::Items(vec![Some(merge(alternatives)?)]), + } + } + _ => return None, + } + } + E::Call { .. } => { + let (target, args, _) = source_call(value, self.archive)?; + let mut relevant = false; + for (index, argument) in args.iter().enumerate() { + let proof = self.evaluate(argument, function, locals, rows, root); + rows.incoming + .push((value.clone(), target.id, index, proof.clone())); + if let Some(proof) = proof.filter(|proof| proof.ty == target.params[index].ty) { + relevant = true; + rows.edges.push(GraphSourceEdgeProof { + original_call: value.clone(), + current_call: value.clone(), + parameter: index, + formal: target.params[index].clone(), + actual: argument.clone(), + proof, + }); + } + } + let returned = self + .previous + .get(target.id.index() as usize)? + .returned + .clone(); + relevant |= returned.is_some(); + if relevant && !rows.calls.iter().any(|call| equal(call, value)) { + rows.calls.push(value.clone()); + } + returned? + } + E::ActorMessage { + actor, + handler, + args, + .. + } => { + let receiver = self.evaluate(actor, function, locals, rows, root); + rows.messages.push((value.clone(), *handler, receiver)); + for argument in args { + self.evaluate(argument, function, locals, rows, root); + } + return None; + } + // Every plain factory leaf is in the protected body and rooted in + // that same required graph. Public FunctionRef/closure/type rows + // outside this constructor never reach this branch with a root. + E::FunctionRef(_) + | E::ClosureRef { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::OptionalNone => plain(root?, value, self.types)?, + _ => return None, + }; + if proof.ty != value.ty || !storage_safe(self.types, proof.ty) { + return None; + } + remember(rows, value, &proof); + Some(proof) + } + fn projection_block( + &self, + function: &hir::Function, + locals: &mut HashMap, + rows: &mut Rows, + ) -> Option<()> { + let projection = collection_projection(function, self.archive, self.types)?; + let input = self.evaluate(projection.iterable, function, locals, rows, None)?; + let item = iteration_part(&input, projection.part)?; + let proof = GraphStorageProof { + ty: function.return_type, + shape: Shape::Items(vec![Some(item)]), + }; + // The accumulator fact describes a bounded set of possible protected + // producers, including the empty prefix. It is not a value/length fact. + // The exact empty initialization is a subset of that same bounded + // producer set. Keep its occurrence and mutable header in agreement; + // the ListNew helper's independent return remains the neutral empty set. + remember(rows, projection.initial, &proof); + rows.calls.push(projection.initial.clone()); + locals.insert(projection.output, proof.clone()); + let output = local_header(function, projection.output, proof.ty)?; + rows.headers.push(HeaderRow { + original: output.clone(), + current: output.clone(), + proof: proof.clone(), + }); + for (binder, part) in [(projection.key, 0), (projection.value, 1)] { + if let Some(fact) = iteration_part(&input, part) { + let header = local_header(function, binder, fact.ty)?; + locals.insert(binder, fact.clone()); + rows.headers.push(HeaderRow { + original: header.clone(), + current: header.clone(), + proof: fact, + }); + } + } + self.evaluate(projection.assigned, function, locals, rows, None); + remember(rows, projection.assigned, &proof); + remember(rows, projection.returned, &proof); + rows.returns.push(ValueRow { + original: projection.returned.clone(), + current: projection.returned.clone(), + proof, + }); + Some(()) + } + fn block( + &self, + block: &hir::Block, + function: &hir::Function, + locals: &mut HashMap, + rows: &mut Rows, + writes: &HashSet, + ) -> bool { + let mut live = true; + for statement in &block.statements { + if !live { + break; + } + match &statement.kind { + H::Let { local, value } => { + let proof = self.evaluate(value, function, locals, rows, None); + if let Some(header) = local_header(function, *local, value.ty) + && header.view_source.is_none() + && !writes.contains(local) + && let Some(proof) = proof + { + locals.insert(*local, proof.clone()); + rows.headers.push(HeaderRow { + original: header.clone(), + current: header.clone(), + proof, + }); + } else { + locals.remove(local); + } + } + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + self.evaluate(value, function, locals, rows, None); + let mut base = target; + while let E::Field { base: inner, .. } | E::View(inner) = &base.kind { + base = inner; + } + if let E::Local(local) = base.kind { + locals.remove(&local); + } + } + H::Return(Some(value)) | H::Respond(value) => { + match self.evaluate(value, function, locals, rows, None) { + Some(proof) if proof.ty == function.return_type => { + rows.returns.push(ValueRow { + original: value.clone(), + current: value.clone(), + proof, + }) + } + _ => rows.unknown_return = true, + } + live = false; + } + H::Return(None) => { + rows.unknown_return = true; + live = false; + } + H::Scope(body) => { + live = self.block(body, function, locals, rows, writes); + } + H::If { + condition, + then_block, + else_block, + } => { + self.evaluate(condition, function, locals, rows, None); + let mut left = locals.clone(); + let left_live = self.block(then_block, function, &mut left, rows, writes); + let mut right = locals.clone(); + let right_live = else_block + .as_ref() + .is_none_or(|body| self.block(body, function, &mut right, rows, writes)); + *locals = join_environments( + if left_live { Some(&left) } else { None }, + if right_live { Some(&right) } else { None }, + ); + live = left_live || right_live; + } + H::While { condition, body } => { + self.evaluate(condition, function, locals, rows, None); + // A loop body has no one-pass dominance proof. Visit calls + // only with unknown locals; no produced value escapes it. + let mut unknown = HashMap::new(); + self.block(body, function, &mut unknown, rows, writes); + locals.retain(|id, _| !writes.contains(id)); + } + H::For { + key, + value, + iterable, + body, + by_view, + } => { + let proof = self.evaluate(iterable, function, locals, rows, None); + let mut branch = locals.clone(); + branch.remove(key); + if let Some(value) = value { + branch.remove(value); + } + if let Some(proof) = proof { + for (binder, part) in [Some((*key, 0)), value.map(|id| (id, 1))] + .into_iter() + .flatten() + { + if let Some(fact) = iteration_part(&proof, part) + && let Some(header) = local_header(function, binder, fact.ty) + && !writes.contains(&binder) + { + branch.insert(binder, fact.clone()); + // The owning header selection is distinct from + // the existing by-view loop borrow certificate. + if !by_view && header.view_source.is_none() { + rows.headers.push(HeaderRow { + original: header.clone(), + current: header.clone(), + proof: fact, + }); + } + } + } + } + self.block(body, function, &mut branch, rows, writes); + locals.retain(|id, _| !writes.contains(id)); + } + H::Match { scrutinee, arms } => { + self.evaluate(scrutinee, function, locals, rows, None); + let mut joined: Option> = None; + let mut any_live = false; + for arm in arms { + let mut branch = locals.clone(); + for binder in &arm.bindings { + branch.remove(binder); + } + if self.block(&arm.body, function, &mut branch, rows, writes) { + any_live = true; + joined = Some(match joined { + Some(old) => join_environments(Some(&old), Some(&branch)), + None => branch, + }); + } + } + *locals = joined.unwrap_or_default(); + live = any_live; + } + H::ReflectedTypeDispatch { type_info, arms } => { + self.evaluate(type_info, function, locals, rows, None); + // Missing dispatch outcome remains a live unknown edge. + for arm in arms { + let mut branch = HashMap::new(); + self.block(&arm.body, function, &mut branch, rows, writes); + } + locals.clear(); + } + H::Expression(value) | H::HandleDefault(value) => { + self.evaluate(value, function, locals, rows, None); + } + H::Assert { condition, message } => { + self.evaluate(condition, function, locals, rows, None); + if let Some(value) = message { + self.evaluate(value, function, locals, rows, None); + } + } + H::Breakpoint { condition, .. } => { + if let Some(value) = condition { + self.evaluate(value, function, locals, rows, None); + } + } + H::Trace(_) => {} + H::Break | H::Continue => { + locals.clear(); + live = false; + } + } + } + live + } +} +fn join_environments( + left: Option<&HashMap>, + right: Option<&HashMap>, +) -> HashMap { + match (left, right) { + (Some(left), Some(right)) => left + .iter() + .filter_map(|(id, value)| Some((*id, merge([value.clone(), right.get(id)?.clone()])?))) + .collect(), + (Some(one), None) | (None, Some(one)) => one.clone(), + (None, None) => HashMap::new(), + } +} +fn iteration_part(proof: &GraphStorageProof, part: usize) -> Option { + match &proof.shape { + Shape::Items(items) if part == 0 => { + merge(items.iter().cloned().collect::>>()?) + } + Shape::Entries(items) => merge( + items + .iter() + .map(|(key, value)| { + if part == 0 { + key.clone() + } else { + value.clone() + } + }) + .collect::>>()?, + ), + Shape::Alternatives(rows) => merge( + rows.iter() + .map(|row| iteration_part(row, part)) + .collect::>>()?, + ), + _ => None, + } +} + +fn factory( + plan: &hir::ActorGraphPlan, + owner: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, +) -> Result { + plan.validate(owner, types)?; + let graph = plan + .required_proof() + .actor_graph() + .ok_or("graph metadata root lost its checked template")?; + let analysis = Analysis { + archive, + types, + factories: &[], + previous: &[], + }; + let mut rows = Rows::default(); + let mut locals: HashMap = HashMap::new(); + let mut tokens: HashMap = HashMap::new(); + let mut committed: Option = None; + let mut fields = Vec::new(); + let mut output = None; + for statement in &plan.body().statements { + let (binding, value) = match &statement.kind { + H::Let { local, value } => (Some(*local), value), + H::Expression(value) => (None, value), + _ => { + return Err("graph factory changed its protected straight-line constructor".into()); + } + }; + let proof = if let E::ActorGraphStep { instruction, args } = &value.kind { + if !plan.instructions().contains(instruction) { + return Err("graph storage step belongs to another root".into()); + } + instruction.validate(owner.id, &owner.identity, args, value.ty, types)?; + // Exact operand occurrences belong to this protected instruction, + // rather than inheriting storage from an equal-type local. + for argument in args { + if let E::Local(local) = argument.kind + && let Some(proof) = locals.get(&local).filter(|proof| proof.ty == argument.ty) + { + remember(&mut rows, argument, proof); + } + } + match instruction.operation() { + hir::ActorGraphOperation::ReserveBase { ty, fields: count } => { + let node = plan + .instructions() + .iter() + .filter(|step| { + matches!( + step.operation(), + hir::ActorGraphOperation::ReserveBase { .. } + ) + }) + .position(|step| step == instruction) + .ok_or("graph seed has no original reserve ordinal")?; + let frozen = graph + .nodes() + .get(node) + .ok_or("graph seed ordinal is absent")?; + if frozen.ty() != *ty + || frozen.captures().len() + frozen.state().len() != *count + || value.ty != *ty + { + return Err("graph seed changed nominal constructor layout".into()); + } + storage_safe(types, *ty).then(|| GraphStorageProof { + ty: *ty, + shape: Shape::Actor { + plan: plan.clone(), + node, + }, + }) + } + hir::ActorGraphOperation::ReservePending { ty, .. } => locals + .get(&local_argument(args, 1)?) + .cloned() + .filter(|proof| proof.ty == *ty), + hir::ActorGraphOperation::HookBox { hook, .. } => { + let projection = instruction + .projected_hook() + .ok_or("graph hook lost its protected typed path")?; + if projection.signature() != value.ty + || hook.function_type() != value.ty + || !projection.belongs_to(instruction.checked()) + { + return Err("graph hook changed checked required path/signature".into()); + } + Some(GraphStorageProof { + ty: value.ty, + shape: Shape::Hook(instruction.clone()), + }) + } + hir::ActorGraphOperation::Fill { ty, node, field } => { + let base = locals.get(&local_argument(args, 1)?); + if base.is_some() + && !matches!(base.map(|proof|&proof.shape),Some(Shape::Actor {plan:root,node:base_node}) if root==plan && base_node==node) + { + return Err("graph field lost its exact reserved actor node".into()); + } + if let Some(proof) = locals + .get(&local_argument(args, 2)?) + .cloned() + .filter(|proof| proof.ty == *ty) + { + fields.push((*node, *field, proof)); + } + None + } + hir::ActorGraphOperation::RootFill { ty } => { + if committed.is_some() || value.ty != TypeInterner::UINT64 { + return Err("graph RootFill changed its pre-Commit scalar token".into()); + } + if let Some(proof) = locals + .get(&local_argument(args, 1)?) + .cloned() + .filter(|proof| proof.ty == *ty && proof.transferable()) + && let Some(binding) = binding + { + let token = GraphStorageProof { + ty: TypeInterner::UINT64, + shape: Shape::HiddenRootToken { + fill: instruction.clone(), + root: Box::new(proof), + }, + }; + tokens.insert(binding, token.clone()); + Some(token) + } else { + None + } + } + hir::ActorGraphOperation::RootPublish { ty } => { + let commit = committed + .as_ref() + .ok_or("graph RootPublish precedes its protected Commit")?; + match tokens.get(&local_argument(args, 1)?) { + Some(GraphStorageProof { + shape: Shape::HiddenRootToken { fill, root }, + .. + }) if fill.same_import(instruction) + && commit.same_import(instruction) + && root.ty == *ty => + { + Some((**root).clone()) + } + _ => None, + } + } + hir::ActorGraphOperation::Commit => { + if committed.replace(instruction.clone()).is_some() { + return Err("graph factory repeated its protected Commit".into()); + } + None + } + hir::ActorGraphOperation::Begin => None, + } + } else { + analysis.evaluate(value, owner, &mut locals, &mut rows, Some(plan)) + }; + if let Some(proof) = proof { + remember(&mut rows, value, &proof); + if let Some(id) = binding { + let header = local_header(owner, id, value.ty) + .ok_or("graph metadata factory local lost its exact original header")?; + locals.insert(id, proof.clone()); + rows.headers.push(HeaderRow { + original: header.clone(), + current: header.clone(), + proof: proof.clone(), + }); + if id == plan.output() { + output = Some(proof); + } + } + } + } + Ok(Factory { + plan: plan.clone(), + output, + rows, + fields, + captures: plan.actor_capture_bindings()?, + }) +} +fn local_argument(args: &[Expression], index: usize) -> Result { + match args.get(index).map(|value| &value.kind) { + Some(E::Local(local)) => Ok(*local), + _ => Err("graph constructor operand lost exact original Local".into()), + } +} + +fn formal_inputs(index: usize, parameter: usize, previous: &[Rows]) -> Option { + let mut facts = Vec::new(); + for rows in previous { + for (_, target, formal, proof) in &rows.incoming { + if target.index() as usize == index && *formal == parameter { + facts.push(proof.clone()?); + } + } + } + merge(facts) +} +fn actor_nodes(proof: &GraphStorageProof) -> Option> { + match &proof.shape { + Shape::Actor { plan, node } => Some(vec![(plan.clone(), *node)]), + Shape::Alternatives(rows) => { + let mut nodes = Vec::new(); + for row in rows { + for node in actor_nodes(row)? { + if !nodes.contains(&node) { + nodes.push(node); + } + } + } + Some(nodes) + } + _ => None, + } +} +fn captured_inputs( + function: &hir::Function, + parameter: &Param, + previous: &[Rows], + factories: &[Factory], + dynamic: &HashSet, + archive: &hir::ResourceSourceArchive, +) -> Result, String> { + let mut candidates = Vec::new(); + for factory in factories { + for binding in &factory.captures { + if binding.handler() != function.id + || binding.parameter() != parameter + || dynamic.contains(&binding.actor_type()) + { + continue; + } + if binding.mutable() { + // The getter proves complete original handler coverage. A + // write in any handler invalidates this frozen field fact, + // including reads made by a different handler later. + let stable = factory + .captures + .iter() + .filter(|other| { + other.node() == binding.node() && other.field() == binding.field() + }) + .all(|other| { + archive + .execution_functions() + .get(other.handler().index() as usize) + .filter(|handler| handler.id == other.handler()) + .is_some_and(|handler| { + unchanged_handler(handler, archive, &mut HashSet::new()) + }) + }); + if !stable { + continue; + } + } + if let Some((_, _, proof)) = factory + .fields + .iter() + .find(|(node, field, _)| *node == binding.node() && *field == binding.field()) + { + candidates.push((factory.plan.clone(), binding.node(), proof.clone())); + } + } + } + if candidates.is_empty() { + return Ok(None); + } + // An unproved receiver of this same handler cannot inherit another actor + // instance's frozen state. Every reconstructed message edge is retained. + for rows in previous { + for (_, handler, receiver) in &rows.messages { + if *handler == function.id { + let Some(nodes) = receiver.as_ref().and_then(actor_nodes) else { + return Ok(None); + }; + if nodes.iter().any(|(plan, node)| { + !candidates + .iter() + .any(|(root, seed, _)| root == plan && seed == node) + }) { + return Ok(None); + } + } + } + } + Ok(merge(candidates.into_iter().map(|(_, _, proof)| proof))) +} + +/// Immutable equations for one exact checked execution archive. No caller can +/// supply rows, and selection still authenticates the complete owner and types. +#[derive(Debug)] +pub(in crate::resource_ownership) struct OriginalGraphAnalysis { + source: hir::ResourceSourceArchive, + rows: Vec, + // Derived only after the entire original analysis has completed. False + // permits negative metadata answers; it never supplies a storage proof. + has_rows: bool, +} +impl OriginalGraphAnalysis { + pub(in crate::resource_ownership) fn same_source(&self, other: &Self) -> bool { + self.source == other.source + } + pub(in crate::resource_ownership) fn has_rows(&self) -> bool { + self.has_rows + } + pub(in crate::resource_ownership) fn has_no_rows_for( + &self, + archive: &hir::ResourceSourceArchive, + ) -> bool { + &self.source == archive && !self.has_rows + } + pub(in crate::resource_ownership) fn prepare( + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result { + archive.validate_types(types)?; + let functions = archive.execution_functions(); + if functions + .iter() + .enumerate() + .any(|(index, function)| function.id.index() as usize != index) + { + return Err("graph storage archive lost its dense original function identity".into()); + } + let rows = reconstruct(archive, types)?; + let has_rows = rows.iter().any(Witness::needed); + Ok(Self { + source: archive.clone(), + rows, + has_rows, + }) + } + pub(in crate::resource_ownership) fn validate_archive( + &self, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result<(), String> { + if &self.source != archive { + return Err( + "graph storage analysis belongs to another checked execution archive".into(), + ); + } + archive.validate_types(types) + } + pub(in crate::resource_ownership) fn capture( + &self, + original: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + ) -> Result<&Witness, String> { + self.validate_archive(archive, types)?; + let index = original.id.index() as usize; + if !archive + .execution_functions() + .get(index) + .is_some_and(|function| original_function_equal(function, original)) + { + return Err("graph storage owner differs from whole original checked archive".into()); + } + self.rows + .get(index) + .ok_or_else(|| "graph owner ID is outside checked dense archive".into()) + } +} + +fn reconstruct( + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, +) -> Result, String> { + let functions = archive.execution_functions(); + let mut factories = Vec::new(); + let mut dynamic = HashSet::new(); + let mut indirect_targets = HashSet::new(); + for function in functions + .iter() + .filter(|function| !archive.required_only_function_ids().contains(&function.id)) + { + let mut plans = Vec::new(); + walk::hir_block(&function.body, &mut |value| { + if let E::ActorGraphImport(plan) = &value.kind { + if !plans.contains(plan) { + plans.push(plan.clone()); + } + } + if let E::ActorSpawn { + constructor: Some(target), + .. + } = &value.kind + && let Some(target) = functions.get(target.index() as usize) + { + dynamic.insert(target.return_type); + } + // This packet reconstructs direct Source incoming edges only. A + // first-class target cannot inherit their facts while its indirect + // callers/capture contexts remain unproved. + if let E::FunctionRef(target) + | E::ClosureRef { + function: target, .. + } = &value.kind + { + indirect_targets.insert(*target); + } + }); + for plan in plans { + factories.push(factory(&plan, function, archive, types)?); + } + } + if factories.is_empty() { + return Ok(vec![Witness::default(); functions.len()]); + } + let mut previous = vec![Rows::default(); functions.len()]; + // Positive equations have a finite rooted producer set. No fact is inferred + // from an unknown edge. Recursive shape growth is left pending, not assumed. + let bound = functions + .len() + .saturating_add(factories.len()) + .saturating_mul(4) + .max(8); + for _ in 0..bound { + let analysis = Analysis { + archive, + types, + factories: &factories, + previous: &previous, + }; + let mut next = Vec::new(); + for (index, function) in functions.iter().enumerate() { + if archive.required_only_function_ids().contains(&function.id) { + next.push(Rows::default()); + continue; + } + let mut rows = Rows::default(); + let mut locals = HashMap::new(); + let writes = written(function); + for (position, parameter) in function.params.iter().enumerate() { + if writes.contains(¶meter.local) || !storage_safe(types, parameter.ty) { + continue; + } + let proof = if position < function.capture_count { + captured_inputs( + function, parameter, &previous, &factories, &dynamic, archive, + )? + } else if (!archive.exported_function_ids().contains(&function.id) + || collection_formals(function, archive, types)) + && !indirect_targets.contains(&function.id) + { + formal_inputs(index, position, &previous) + } else { + None + }; + if let Some(proof) = proof.filter(|proof| proof.ty == parameter.ty) + && let Some(header) = local_header(function, parameter.local, parameter.ty) + { + locals.insert(parameter.local, proof.clone()); + rows.formals.push(FormalRow { + original: parameter.clone(), + current: parameter.clone(), + proof: proof.clone(), + }); + rows.headers.push(HeaderRow { + original: header.clone(), + current: header.clone(), + proof, + }); + } + } + let live = if analysis + .projection_block(function, &mut locals, &mut rows) + .is_some() + { + false + } else { + analysis.block(&function.body, function, &mut locals, &mut rows, &writes) + }; + // Count every original call/message site, including calls nested in + // expressions whose storage form is not reconstructed by this packet. + // Missing sites are unknown incoming edges, never silently omitted. + walk::hir_block(&function.body, &mut |value| { + if let E::Call { + function: target, + args, + .. + } = &value.kind + { + for parameter in 0..args.len() { + if !rows.incoming.iter().any(|(call, id, index, _)| { + *id == *target && *index == parameter && equal(call, value) + }) { + rows.incoming + .push((value.clone(), *target, parameter, None)); + } + } + } + if let E::ActorMessage { handler, .. } = &value.kind + && !rows + .messages + .iter() + .any(|(call, id, _)| *id == *handler && equal(call, value)) + { + rows.messages.push((value.clone(), *handler, None)); + } + }); + if !live && !rows.unknown_return { + rows.returned = merge(rows.returns.iter().map(|row| row.proof.clone())); + } + next.push(rows); + } + if next == previous { + return Ok(functions + .iter() + .zip(next) + .map(|(original, rows)| { + if archive.required_only_function_ids().contains(&original.id) { + return Witness::default(); + } + Witness { + owner: Some(original.clone()), + local_map: original + .locals + .iter() + .map(|header| (header.clone(), header.clone())) + .collect(), + values: rows.values, + headers: rows.headers, + formals: rows.formals, + returns: rows.returns, + returned: rows.returned, + calls: rows.calls, + edges: rows.edges, + graph: None, + constructor_outputs: Vec::new(), + sequence_outputs: Vec::new(), + } + }) + .collect()); + } + previous = next; + } + // No incomplete iteration becomes admission evidence. + Ok(vec![Witness::default(); functions.len()]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collection_projection_requires_the_complete_archived_producer_body() { + use jett_common::{FileId, SourceOrigin}; + let source = + include_str!("../../../../jett_driver/tests/actor_graph/fixtures/10_map_shared.jett"); + let mut parsed = jett_parser::parse(source, FileId::new(0)); + let mut origins = HashMap::from([(FileId::new(0), SourceOrigin::Project)]); + for (file, support) in [ + ( + FileId::new(10_000), + include_str!("../../../../../stdlib/map.jett"), + ), + ( + FileId::new(10_001), + include_str!("../../../../../stdlib/list.jett"), + ), + ] { + let support = jett_parser::parse(support, file); + assert!(support.errors.is_empty()); + let mut items = support.module.items; + items.append(&mut parsed.module.items); + parsed.module.items = items; + origins.insert(file, SourceOrigin::Stdlib); + } + let checked = std::sync::Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + origins, + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .unwrap(), + ); + let program = hir::lower_checked_ordinary_program(&checked).unwrap(); + let archive = &program.resource_source; + let types = &checked.checked().interner; + let function = archive + .execution_functions() + .iter() + .find(|function| { + function.identity.declaration.namespace == "map" + && function.identity.declaration.name == "values" + }) + .unwrap(); + let projection = collection_projection(function, archive, types).unwrap(); + let (append, _, _) = source_call(projection.assigned, archive).unwrap(); + assert_eq!( + list_operation(append, types), + Some(hir::IntrinsicId::ListAppend) + ); + + let mut extra_statement = function.clone(); + extra_statement + .body + .statements + .push(function.body.statements[2].clone()); + assert!(collection_projection(&extra_statement, archive, types).is_none()); + + let mut viewed_loop = function.clone(); + let H::For { by_view, .. } = &mut viewed_loop.body.statements[1].kind else { + unreachable!(); + }; + *by_view = true; + assert!(collection_projection(&viewed_loop, archive, types).is_none()); + + let mut different_item = function.clone(); + let H::For { body, .. } = &mut different_item.body.statements[1].kind else { + unreachable!(); + }; + let H::Assign { value, .. } = &mut body.statements[0].kind else { + unreachable!(); + }; + let E::Call { args, .. } = &mut value.kind else { + unreachable!(); + }; + // Preserve the Actor TypeId, but remove its exact loop occurrence. + args[1].kind = E::Local(LocalId::new(999)); + assert!(collection_projection(&different_item, archive, types).is_none()); + + let mut different_intrinsic = append.clone(); + let H::Return(Some(value)) = &mut different_intrinsic.body.statements[0].kind else { + unreachable!(); + }; + let E::Intrinsic { intrinsic, .. } = &mut value.kind else { + unreachable!(); + }; + *intrinsic = hir::IntrinsicId::ListLength; + assert!(list_operation(&different_intrinsic, types).is_none()); + + let mut mutable_formal = function.clone(); + mutable_formal.params[0].mutable = true; + assert!(collection_projection(&mutable_formal, archive, types).is_none()); + } + + #[test] + fn fixed_point_call_and_message_rows_compare_retained_float_bits() { + let expression = Expression { + kind: E::Float(f64::from_bits(0x7ff8_0000_0000_0011)), + ty: TypeInterner::FLOAT64, + span: jett_common::Span::new(jett_common::FileId::new(0), 0, 1), + }; + let mut rows = Rows::default(); + rows.calls.push(expression.clone()); + rows.incoming + .push((expression.clone(), FunctionId::new(0), 0, None)); + rows.messages + .push((expression.clone(), FunctionId::new(1), None)); + assert_eq!(rows, rows.clone()); + let mut changed = rows.clone(); + changed.messages[0].0.kind = E::Float(f64::from_bits(0x7ff8_0000_0000_0012)); + assert_ne!(rows, changed); + } +} diff --git a/crates/jett_mir/src/resource_ownership/graph_metadata/tests.rs b/crates/jett_mir/src/resource_ownership/graph_metadata/tests.rs new file mode 100644 index 00000000..ee060f99 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/graph_metadata/tests.rs @@ -0,0 +1,838 @@ +//! These controls are authored only. The graph worker runs the frozen packet. +use super::*; +use crate::resource_ownership::tests::{SUPPORT, checked_support}; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::{collections::HashMap, sync::Arc}; + +const SOURCE: &str = r#"namespace app +actor Keeper: + mutable function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + receive alive responds bool: + respond true +struct Bundle: + keeper: Keeper + dispose: function(resource_probe.TestHandle) returns nothing +export function make() returns Bundle: + return comptime Bundle(keeper: spawn Keeper(), dispose: resource_probe.descriptor()) +function relay(selected: Bundle) returns Bundle: + Bundle alias = selected + return clone alias +function main() returns nothing: + Bundle selected = make() + Bundle alias = clone selected + Bundle forwarded = relay(alias) + function(resource_probe.TestHandle) returns nothing dispose = forwarded.dispose + bool alive = ask forwarded.keeper.alive + return nothing +"#; +fn baked( + source: &str, + release: bool, +) -> (hir::Program, Arc) { + let checked = checked_support(source, SUPPORT, release); + let evaluated = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }), + Arc::new(HashMap::new()), + ); + assert!( + evaluated.diagnostics.is_empty(), + "{:?}", + evaluated.diagnostics + ); + assert!(evaluated.debug_events.is_empty()); + let mut program = hir::lower_checked_resource_program(&checked).unwrap(); + hir::bake_required_values( + &mut program, + &evaluated.values, + &checked.checked().interner, + &checked.checked().reflection_metadata, + &checked.checked().method_value_definitions, + ) + .unwrap(); + hir::complete_value_conversions(&mut program, &checked.checked().interner).unwrap(); + (program, checked) +} +fn named<'a>(program: &'a hir::Program, name: &str) -> &'a hir::Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} +fn original( + program: &hir::Program, + checked: &jett_typecheck::CheckedResourceProgram, + name: &str, +) -> Witness { + capture( + named(program, name), + &program.resource_source, + &checked.checked().interner, + ) + .unwrap() +} +#[test] +fn rooted_export_alias_formal_return_and_hook_projection_keep_original_producers() { + for release in [false, true] { + let (program, checked) = baked(SOURCE, release); + let make = original(&program, &checked, "make"); + assert!(make.original_storage_for_return().is_some()); + let relay = original(&program, &checked, "relay"); + assert!( + relay + .original_storage_for_formal(&named(&program, "relay").params[0]) + .is_some() + ); + assert!(relay.original_storage_for_return().is_some()); + let main = original(&program, &checked, "main"); + let header = named(&program, "main") + .locals + .iter() + .find(|local| local.name == "dispose") + .unwrap(); + let proof = main.original_storage_for_local(header).unwrap(); + let projection = hook_projection(proof).unwrap(); + assert_eq!(projection.ty(), header.ty); + assert_eq!( + projection.hook().recipe(), + jett_types::ResourceKernelRecipe::Finalize + ); + assert!(!projection.producers().is_empty()); + for producer in projection.producers() { + assert!(producer.projected_hook().unwrap().belongs_to(&checked)); + assert!( + producer + .projected_hook() + .unwrap() + .required() + .belongs_to(&checked) + ); + } + assert!(main.edges.iter().any(|edge| edge.parameter_index() == 0 + && edge.original_formal() == &named(&program, "relay").params[0])); + let handler = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::ActorHandler + }) + .unwrap(); + let captures = capture( + handler, + &program.resource_source, + &checked.checked().interner, + ) + .unwrap(); + assert!( + captures + .original_storage_for_formal(&handler.params[0]) + .is_some() + ); + } +} +#[test] +fn equal_signature_unknown_incoming_edge_cannot_inherit_a_graph_formal_or_return() { + let source=SOURCE.replace(" Bundle forwarded = relay(alias)", + " Bundle raw = Bundle(keeper: spawn Keeper(), dispose: resource_probe.descriptor())\n Bundle unproved = relay(raw)\n Bundle forwarded = relay(alias)"); + let (program, checked) = baked(&source, false); + let relay = original(&program, &checked, "relay"); + assert!( + relay + .original_storage_for_formal(&named(&program, "relay").params[0]) + .is_none() + ); + assert!(relay.original_storage_for_return().is_none()); + // The independent authentic exported required root remains available. + assert!( + original(&program, &checked, "make") + .original_storage_for_return() + .is_some() + ); +} +#[test] +fn changed_original_owner_cache_and_mutable_capture_refuse_before_storage_selection() { + let (program, checked) = baked(SOURCE, false); + let owner = named(&program, "make"); + let mut changed = owner.clone(); + changed.body.statements.clear(); + assert!( + capture( + &changed, + &program.resource_source, + &checked.checked().interner + ) + .is_err() + ); + let (foreign, _) = baked(SOURCE, false); + assert!( + capture( + named(&foreign, "make"), + &program.resource_source, + &checked.checked().interner + ) + .is_err() + ); + let source = SOURCE.replace( + " respond true", + " dispose = resource_probe.descriptor()\n respond true", + ); + let (changed, checked) = baked(&source, false); + let handler = changed + .functions + .iter() + .find(|function| function.identity.declaration.kind == hir::DeclarationKind::ActorHandler) + .unwrap(); + let proof = capture( + handler, + &changed.resource_source, + &checked.checked().interner, + ) + .unwrap(); + assert!( + proof + .original_storage_for_formal(&handler.params[0]) + .is_none() + ); +} +#[test] +fn writer_in_another_handler_cannot_lend_a_frozen_hook_to_a_reader() { + let source = SOURCE.replace("struct Bundle:", + " receive replace responds bool:\n dispose = resource_probe.descriptor()\n respond true\nstruct Bundle:"); + let (program, checked) = baked(&source, false); + let reader = program + .functions + .iter() + .find(|function| function.identity.declaration.kind == hir::DeclarationKind::ActorHandler) + .unwrap(); + let proof = capture( + reader, + &program.resource_source, + &checked.checked().interner, + ) + .unwrap(); + assert!( + proof + .original_storage_for_formal(&reader.params[0]) + .is_none() + ); +} + +#[test] +fn mutable_capture_requires_authenticated_transitive_helper_preservation() { + for (body, stable) in [ + (" return true", true), + ( + " mutable bool selected = false\n selected = true\n return selected", + false, + ), + ] { + let source = SOURCE + .replacen( + "actor Keeper:", + &format!("function observed() returns bool:\n{body}\nactor Keeper:"), + 1, + ) + .replacen(" respond true", " respond observed()", 1); + let (program, checked) = baked(&source, false); + let reader = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::ActorHandler + }) + .unwrap(); + let witness = capture( + reader, + &program.resource_source, + &checked.checked().interner, + ) + .unwrap(); + assert_eq!( + witness + .original_storage_for_formal(&reader.params[0]) + .is_some(), + stable + ); + } +} + +#[test] +fn required_actor_constructor_retires_only_without_a_retained_runtime_spawn() { + let (program, _) = baked(SOURCE, false); + let constructor = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::ActorConstructor + }) + .unwrap(); + assert!( + program + .resource_source + .required_only_function_ids() + .contains(&constructor.id) + ); + for handler in program + .functions + .iter() + .filter(|function| function.identity.declaration.kind == hir::DeclarationKind::ActorHandler) + { + assert!( + !program + .resource_source + .required_only_function_ids() + .contains(&handler.id) + ); + } + let runtime = SOURCE.replacen( + " Bundle alias = clone selected", + " Keeper dynamic = spawn Keeper()\n Bundle alias = clone selected", + 1, + ); + let (program, _) = baked(&runtime, false); + let constructor = program + .functions + .iter() + .find(|function| { + function.identity.declaration.kind == hir::DeclarationKind::ActorConstructor + }) + .unwrap(); + assert!( + !program + .resource_source + .required_only_function_ids() + .contains(&constructor.id) + ); +} + +#[test] +fn hidden_root_token_has_only_exact_scalar_storage_and_operand_rows() { + let (program, checked) = baked(SOURCE, false); + let owner = named(&program, "make"); + let witness = original(&program, &checked, "make"); + let token = witness + .values + .iter() + .find(|row| matches!(row.proof.shape, Shape::HiddenRootToken { .. })) + .unwrap(); + assert_eq!(token.proof.ty(), TypeInterner::UINT64); + assert!(!token.proof.transferable()); + assert!(hook_projection(&token.proof).is_none()); + let fill_header = witness + .headers + .iter() + .find(|row| row.proof == token.proof) + .unwrap(); + let plan = match &token.proof.shape { + Shape::HiddenRootToken { fill, .. } => fill, + _ => unreachable!(), + }; + assert!(matches!( + plan.operation(), + hir::ActorGraphOperation::RootFill { .. } + )); + let mut found = false; + walk::hir_block(&owner.body, &mut |value| { + if let E::ActorGraphImport(import) = &value.kind { + for statement in &import.body().statements { + let value = match &statement.kind { + hir::StatementKind::Let { value, .. } + | hir::StatementKind::Expression(value) => value, + _ => continue, + }; + if let E::ActorGraphStep { instruction, args } = &value.kind + && matches!( + instruction.operation(), + hir::ActorGraphOperation::RootPublish { .. } + ) + { + assert!(plan.same_import(instruction)); + assert!( + matches!(args[1].kind, E::Local(local) if local == fill_header.original.id) + ); + let proof = witness.original_storage_for_expression(&args[1]).unwrap(); + assert_eq!(proof, &token.proof); + assert!( + witness + .original_storage_for_expression(value) + .unwrap() + .transferable() + ); + found = true; + } + } + } + }); + assert!(found); + assert!(witness.returned.as_ref().unwrap().transferable()); +} + +#[test] +fn retained_float_bits_are_reflexive_and_keep_signed_zero_distinct() { + let (program, checked) = baked(SOURCE, false); + let witness = original(&program, &checked, "make"); + let plan = witness + .values + .iter() + .find_map(|row| match &row.proof.shape { + Shape::Actor { plan, .. } | Shape::Plain { plan, .. } => Some(plan.clone()), + _ => None, + }) + .unwrap(); + let value = Expression { + kind: E::Float(f64::from_bits(0x7ff8_0000_0000_0011)), + ty: TypeInterner::FLOAT64, + span: named(&program, "make").span, + }; + let proof = GraphStorageProof { + ty: value.ty, + shape: Shape::Plain { + plan, + value: value.clone(), + }, + }; + assert_eq!(proof, proof.clone()); + let row = ValueRow { + original: value.clone(), + current: value.clone(), + proof: proof.clone(), + }; + assert_eq!(row, row.clone()); + let mut rows = Witness::default(); + rows.calls.push(value.clone()); + rows.values.push(row); + assert_eq!(rows, rows.clone()); + let mut different = proof.clone(); + if let Shape::Plain { value, .. } = &mut different.shape { + value.kind = E::Float(f64::from_bits(0x7ff8_0000_0000_0012)); + } + assert_ne!(proof, different); + let mut positive = proof.clone(); + let mut negative = proof; + if let Shape::Plain { value, .. } = &mut positive.shape { + value.kind = E::Float(0.0); + } + if let Shape::Plain { value, .. } = &mut negative.shape { + value.kind = E::Float(-0.0); + } + assert_ne!(positive, negative); +} +#[test] +fn raw_function_and_occupied_resource_types_do_not_mint_storage_rows() { + let (program, checked) = baked(SOURCE, false); + let types = &checked.checked().interner; + let proof = original(&program, &checked, "main"); + let header = named(&program, "main") + .locals + .iter() + .find(|local| local.name == "dispose") + .unwrap(); + let synthetic = Expression { + kind: E::FunctionRef(named(&program, "main").id), + ty: header.ty, + span: header.span, + }; + assert!(proof.original_storage_for_expression(&synthetic).is_none()); + assert!(storage_safe(types, header.ty)); // Query alone conveys no proof. + let resource = types.resolve(header.ty); + let Type::Function { params, .. } = resource else { + panic!("stored hook signature") + }; + assert!(!storage_safe(types, params[0])); + let network = types + .type_ids() + .find(|id| matches!(types.resolve(*id), Type::Capability(_))); + if let Some(network) = network { + assert!(!storage_safe(types, network)); + } +} +#[test] +fn current_body_header_pointer_and_resealed_parent_forgeries_refuse() { + for release in [false, true] { + let (hir, checked) = baked(SOURCE, release); + let types = &checked.checked().interner; + let program = crate::lower(&hir, types).unwrap(); + let function = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + let parent = function.resource_lowering.as_ref().unwrap(); + validate(parent, function, types).unwrap(); + let expression = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } if matches!(value.kind, E::Field { .. }) => { + Some(value) + } + _ => None, + }) + .unwrap(); + assert!(storage_for_expression(parent, function, types, expression).is_some()); + assert!(storage_for_expression(parent, function, types, &expression.clone()).is_none()); + let mut changed = function.clone(); + changed.blocks[0].statements.clear(); + changed.resource_lowering.as_mut().unwrap().blocks = changed.blocks.clone(); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + let mut changed = function.clone(); + changed.locals[0].name.push_str("foreign"); + changed.resource_lowering.as_mut().unwrap().locals = changed.locals.clone(); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + let cloned_header = function + .locals + .iter() + .find(|local| { + parent + .graph_metadata + .original_storage_for_local(local) + .is_some() + }) + .unwrap() + .clone(); + assert!(storage_for_local(parent, function, types, &cloned_header).is_none()); + + let factory = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "make" + }) + .unwrap(); + let parent = factory.resource_lowering.as_ref().unwrap(); + let ownership = crate::validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(factory.id).unwrap(); + assert!( + plan.carriers().is_none(), + "protected constructor retains ordinary storage" + ); + assert_eq!( + plan.frames().len(), + 1, + "no constructor or return custody frames" + ); + assert!(plan.owner_slots().is_empty()); + assert!(plan.loans().is_empty()); + assert!( + plan.graph_descriptor_operations().next().is_some(), + "hook Descriptor rows survive staging" + ); + let output = parent.graph_metadata.constructor_outputs.first().unwrap(); + let header = factory.local(output.current.id).unwrap(); + let companion = crate::ResourceCompanionPlan::analyze(&ownership, factory.id).unwrap(); + assert!( + companion + .storage() + .owned_locals + .contains(&(header.id.index() as usize)) + ); + assert!( + companion.storage().temporary_slots > 0, + "boxed Graph producer owns a temporary" + ); + for current in program.functions.iter().filter(|current| { + current.identity.declaration.namespace == "app" + && matches!(current.identity.declaration.name.as_str(), "relay" | "main") + }) { + crate::ResourceCompanionPlan::analyze(&ownership, current.id).unwrap(); + } + assert!( + crate::copy_values::CopyValuePlan::analyze(factory, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!(storage_for_local(parent, factory, types, header).is_some()); + assert!(storage_for_local(parent, factory, types, &header.clone()).is_none()); + let nodes = factory + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match &statement.kind { + StatementKind::ResourceConstructor(node) => Some(node), + _ => None, + }) + .collect::>(); + assert_eq!(nodes.len(), 5, "Begin, two fields, Commit and End survive"); + for node in &nodes { + assert!(crate::native_graph_storage_for_constructor(factory, types, node).is_some()); + assert!( + crate::native_graph_storage_for_constructor(factory, types, &(*node).clone()) + .is_none() + ); + } + let mut changed = factory.clone(); + changed + .resource_lowering + .as_mut() + .unwrap() + .graph_metadata + .constructor_outputs + .clear(); + assert!(crate::native_graph_storage_for_return(&changed, types).is_none()); + assert!( + crate::native_graph_storage_for_local( + &changed, + types, + changed.local(header.id).unwrap() + ) + .is_none() + ); + let mut missing = program.clone(); + missing.functions[factory.id.index() as usize] = changed; + assert!(crate::validate_resource_ownership(&missing, types).is_err()); + let mut changed = factory.clone(); + let alternative = factory + .locals + .iter() + .find(|local| local.ty == header.ty && local.id != header.id) + .unwrap(); + changed + .resource_lowering + .as_mut() + .unwrap() + .graph_metadata + .constructor_outputs[0] + .current = alternative.clone(); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + let mut changed = factory.clone(); + changed.resource_lowering.as_mut().unwrap().constructor_seal = Arc::from([]); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + let mut changed = factory.clone(); + let foreign = parent + .graph_metadata + .values + .iter() + .find(|row| row.proof != output.proof) + .unwrap(); + changed + .resource_lowering + .as_mut() + .unwrap() + .graph_metadata + .constructor_outputs[0] + .proof = foreign.proof.clone(); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + let mut changed = factory.clone(); + let stage = changed + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, + .. + }) => Some(value), + _ => None, + }) + .unwrap(); + stage.span.end += 1; + changed.resource_lowering.as_mut().unwrap().blocks = changed.blocks.clone(); + assert!(validate(changed.resource_lowering.as_ref().unwrap(), &changed, types).is_err()); + } +} + +#[test] +fn batched_original_graph_analysis_keeps_exact_archive_owner_and_type_meanings() { + for release in [false, true] { + let (hir, checked) = baked(SOURCE, release); + let types = &checked.checked().interner; + let analysis = OriginalGraphAnalysis::prepare(&hir.resource_source, types).unwrap(); + let make = named(&hir, "make"); + let first = analysis.capture(make, &hir.resource_source, types).unwrap(); + let second = analysis.capture(make, &hir.resource_source, types).unwrap(); + assert!( + std::ptr::eq(first, second), + "one immutable row serves repeated selection" + ); + assert!(first.original_storage_for_return().is_some()); + assert!( + analysis + .capture(named(&hir, "relay"), &hir.resource_source, types) + .unwrap() + .original_storage_for_return() + .is_some() + ); + + let mut edited = make.clone(); + edited.body.statements.clear(); + assert!( + analysis + .capture(&edited, &hir.resource_source, types) + .is_err() + ); + let mut edited = make.clone(); + edited.locals[0].name.push_str("foreign"); + assert!( + analysis + .capture(&edited, &hir.resource_source, types) + .is_err() + ); + let (foreign, foreign_checked) = baked(SOURCE, release); + assert!( + analysis + .capture( + named(&foreign, "make"), + &foreign.resource_source, + &foreign_checked.checked().interner + ) + .is_err() + ); + + // The original TypeIds alone cannot replace their nominal definitions. + let mut missing = TypeInterner::new(); + for ty in types.type_ids() { + assert_eq!(missing.intern(types.resolve(ty).clone()), ty); + } + assert!( + analysis + .capture(make, &hir.resource_source, &missing) + .is_err() + ); + + let program = crate::lower(&hir, types).unwrap(); + let mut current = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "make" + }) + .unwrap() + .clone(); + current.resource_lowering.as_mut().unwrap().execution = + authenticate_original(&foreign, &foreign_checked.checked().interner).unwrap(); + assert!( + validate(current.resource_lowering.as_ref().unwrap(), ¤t, types).is_err(), + "equal-text foreign analysis cannot authenticate current rows" + ); + } +} + +#[test] +fn completed_empty_graph_analysis_keeps_resource_custody_and_general_current_validation() { + const ORDINARY: &str = r#"namespace app +function lifecycle(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 7) handle error: + return nothing + resource_probe.close(token) + return nothing +function main(net: Network) returns nothing: + lifecycle(view net) + return nothing +"#; + for release in [false, true] { + let checked = checked_support(ORDINARY, SUPPORT, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let execution = authenticate_original(&hir, types).unwrap(); + assert!(execution.has_no_original_graph_rows(&hir.resource_source)); + let (foreign, _) = baked(SOURCE, release); + assert!(!execution.has_no_original_graph_rows(&foreign.resource_source)); + assert!( + !ResourceExecutionClosure::default().has_no_original_graph_rows(&hir.resource_source) + ); + + let program = crate::lower(&hir, types).unwrap(); + let plan = crate::validate_resource_ownership(&program, types).unwrap(); + let entry = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + assert!( + plan.function(entry.id).is_some(), + "ordinary Resource Scope remains mandatory" + ); + for function in &program.functions { + if let Some(parent) = &function.resource_lowering { + assert!(!has_records(parent, function, types)); + assert!(storage_for_return(parent, function, types).is_none()); + for local in &function.locals { + assert!(storage_for_local(parent, function, types, local).is_none()); + } + for formal in &function.params { + assert!(storage_for_formal(parent, function, types, formal).is_none()); + } + for block in &function.blocks { + walk::mir_block(block, &mut |expression| { + assert!( + storage_for_expression(parent, function, types, expression).is_none() + ); + assert!(source_edge(parent, function, types, expression, 0).is_none()); + }); + } + } + } + let mut edited = program.clone(); + edited.functions[entry.id.index() as usize].locals[0] + .name + .push_str("foreign"); + assert!( + crate::validate_resource_ownership(&edited, types).is_err(), + "negative metadata answers cannot authenticate an edited Resource header" + ); + let mut edited = program.clone(); + edited.functions[entry.id.index() as usize].blocks.clear(); + assert!( + crate::validate_resource_ownership(&edited, types).is_err(), + "general current Resource/CFG validation still rejects edited bodies" + ); + } +} + +#[test] +fn positive_graph_analysis_never_enters_completed_empty_fastpath() { + for release in [false, true] { + let (hir, checked) = baked(SOURCE, release); + let types = &checked.checked().interner; + let execution = authenticate_original(&hir, types).unwrap(); + assert!(!execution.has_no_original_graph_rows(&hir.resource_source)); + let program = crate::lower(&hir, types).unwrap(); + let make = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "make" + }) + .unwrap(); + let parent = make.resource_lowering.as_ref().unwrap(); + assert!(!no_original_or_current_records(parent)); + assert!(storage_for_return(parent, make, types).is_some()); + let output = &parent.graph_metadata.constructor_outputs[0]; + let header = make.local(output.current.id).unwrap(); + assert!(storage_for_local(parent, make, types, header).is_some()); + assert!(storage_for_local(parent, make, types, &header.clone()).is_none()); + let mut edited = make.clone(); + edited.locals[header.id.index() as usize] + .name + .push_str("foreign"); + assert!( + storage_for_return(edited.resource_lowering.as_ref().unwrap(), &edited, types) + .is_none() + ); + } +} diff --git a/crates/jett_mir/src/resource_ownership/lexical_borrows.rs b/crates/jett_mir/src/resource_ownership/lexical_borrows.rs new file mode 100644 index 00000000..9ce84331 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/lexical_borrows.rs @@ -0,0 +1,610 @@ +//! Private lexical declaration and retirement custody. Source paths never remap. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceLexicalExitId(usize); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceLexicalExitKind { + Fallthrough, + Return, + Break, + Continue, +} +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceLexicalExit { + id: ResourceLexicalExitId, + kind: ResourceLexicalExitKind, + site: ResourceSite, + pub(super) scopes: Vec, + original_exit: Option, + target: Option, +} +impl ResourceLexicalExit { + pub fn kind(&self) -> ResourceLexicalExitKind { + self.kind + } + pub(super) fn declarations(&self) -> impl Iterator + '_ { + self.scopes + .iter() + .rev() + .flat_map(|scope| scope.active.iter().rev().copied()) + } + pub(crate) fn current_locals(&self, function: &Function) -> Result, String> { + let witness = function + .resource_lowering + .as_ref() + .ok_or("Resource lexical exit has no exact function witness")?; + let mut locals = Vec::new(); + for original in self.declarations() { + if let Some(local) = owned_declarations::owner_current_local(witness, original) { + if !locals.contains(&local) { + locals.push(local); + } + continue; + } + let row = witness + .borrowed_sums + .iter() + .find(|row| row.original_local() == original) + .ok_or("Resource lexical exit lost its current declaration headers")?; + for local in row.retirement_locals() { + if !locals.contains(&local) { + locals.push(local); + } + } + } + Ok(locals) + } +} +#[derive(Debug, Clone)] +pub struct ResourceLexicalExitPlan { + site: ResourceSite, + kind: ResourceLexicalExitKind, + operation_ids: Vec, + clear_locals: Vec, +} +impl ResourceLexicalExitPlan { + pub fn kind(&self) -> ResourceLexicalExitKind { + self.kind + } + pub fn operation_ids(&self) -> &[ResourceOperationId] { + &self.operation_ids + } + pub fn clear_locals(&self) -> &[LocalId] { + &self.clear_locals + } + pub(super) fn new( + site: ResourceSite, + kind: ResourceLexicalExitKind, + operation_ids: Vec, + clear_locals: Vec, + ) -> Self { + Self { + site, + kind, + operation_ids, + clear_locals, + } + } +} +impl ResourceFunctionPlan { + pub fn lexical_exit( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Option<&ResourceLexicalExitPlan> { + self.lexical_exits.iter().find(|exit| { + exit.site + == ResourceSite { + function: self.function, + block, + position, + } + }) + } +} +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LexicalScope { + path: Vec, + pub(crate) active: Vec, +} +#[derive(Debug, Clone)] +pub(super) struct Declaration { + pub(super) path: Vec, + pub(super) scope: Vec, + pub(super) local: LocalId, + pub(super) value: Expression, + pub(super) span: Span, +} +struct OriginalScope { + path: Vec, + body: hir::Block, +} +#[derive(Default)] +struct Inventory { + declarations: Vec, + all_declarations: Vec, + scopes: Vec, +} +fn child(path: &[usize], kind: usize, index: usize) -> Vec { + let mut next = path.to_vec(); + next.extend([kind, index]); + next +} +impl Inventory { + fn block(&mut self, block: &hir::Block, path: Vec, manifest: &hir::ResourceManifest) { + self.scopes.push(OriginalScope { + path: path.clone(), + body: block.clone(), + }); + for (index, statement) in block.statements.iter().enumerate() { + let at = child(&path, 0, index); + if let H::Let { local, value } = &statement.kind { + let declaration = Declaration { + path: at.clone(), + scope: path.clone(), + local: *local, + value: value.clone(), + span: statement.span, + }; + self.all_declarations.push(declaration.clone()); + if manifest.kind_for_type(value.ty).is_some() + && matches!(&value.kind, E::View(handle) if matches!(handle.kind, E::Handle { .. })) + { + self.declarations.push(declaration); + } + } + let mut values = Vec::new(); + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => values.push(value), + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + values.extend([target, value]) + } + H::Return(value) => values.extend(value.iter()), + H::If { + condition, + then_block, + else_block, + } => { + values.push(condition); + self.block(then_block, child(&at, 2, 0), manifest); + if let Some(block) = else_block { + self.block(block, child(&at, 2, 1), manifest); + } + } + H::While { condition, body } => { + values.push(condition); + self.block(body, child(&at, 2, 0), manifest); + } + H::For { iterable, body, .. } => { + values.push(iterable); + self.block(body, child(&at, 2, 0), manifest); + } + H::Match { scrutinee, arms } => { + values.push(scrutinee); + for (arm, value) in arms.iter().enumerate() { + self.block(&value.body, child(&at, 2, arm), manifest); + } + } + H::Assert { condition, message } => { + values.push(condition); + values.extend(message.iter()); + } + H::Breakpoint { condition, .. } => values.extend(condition.iter()), + H::Scope(block) => self.block(block, child(&at, 2, 0), manifest), + H::ReflectedTypeDispatch { type_info, arms } => { + values.push(type_info); + for (arm, value) in arms.iter().enumerate() { + self.block(&value.body, child(&at, 2, arm), manifest); + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + for (index, value) in values.into_iter().enumerate() { + self.expression(value, child(&at, 1, index), manifest); + } + } + } + fn expression( + &mut self, + value: &Expression, + path: Vec, + manifest: &hir::ResourceManifest, + ) { + let mut children: Vec<&Expression> = Vec::new(); + match &value.kind { + // Dedicated graph construction owns the sealed import body. + E::ActorGraphImport(_) => {} + E::Binary { left, right, .. } => children.extend([left.as_ref(), right.as_ref()]), + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => children.push(value), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => children.extend(args), + E::IndirectCall { callee, args, .. } => { + children.push(callee); + children.extend(args); + } + E::ActorMessage { actor, args, .. } => { + children.push(actor); + children.extend(args); + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + children.extend(fields) + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + children.extend(payloads) + } + E::MachineTransition { + source, payloads, .. + } => { + children.push(source); + children.extend(payloads); + } + E::ListConstruct { elements } => children.extend(elements), + E::MapConstruct { entries } => { + for entry in entries { + children.extend([&entry.key, &entry.value]); + } + } + E::Handle { + target, failure, .. + } => { + children.push(target); + self.block(failure, child(&path, 3, 0), manifest); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + children.push(value); + } + } + } + E::Field { base, .. } => children.push(base), + // A separate callable body has its own checked function/local archive. + E::InlineFunction { .. } + | E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } + for (index, value) in children.into_iter().enumerate() { + self.expression(value, child(&path, 1, index), manifest); + } + } +} +fn inventory(witness: &ResourceLoweringWitness) -> Inventory { + let mut inventory = Inventory::default(); + inventory.block(&witness.original.body, Vec::new(), &witness.manifest); + inventory +} +pub(super) fn declarations(witness: &ResourceLoweringWitness) -> Vec { + inventory(witness).declarations +} +pub(super) fn source_declarations( + function: &hir::Function, + manifest: &hir::ResourceManifest, +) -> Vec { + let mut inventory = Inventory::default(); + inventory.block(&function.body, Vec::new(), manifest); + inventory.all_declarations +} +impl Capture { + pub(crate) fn lexical_scope(&self, block: &hir::Block) -> Result, String> { + self.lexical_scope_under(block, None) + } + pub(crate) fn lexical_scope_under( + &self, + block: &hir::Block, + original_prefix: Option<&[usize]>, + ) -> Result, String> { + let Some(witness) = &self.witness else { + return Ok(None); + }; + let inventory = inventory(witness); + if inventory.declarations.is_empty() && witness.lexical_owners.is_empty() { + return Ok(None); + } + let mut scopes = inventory.scopes.iter().filter(|scope| { + original_prefix.is_none_or(|prefix| scope.path.starts_with(prefix)) + && crate::breakpoint_regions::hir_blocks_equal(&scope.body, block) + }); + let scope = scopes + .next() + .ok_or("Resource lexical block is absent from its exact original archive")?; + if scopes.next().is_some() { + return Err( + "pending Resource lexical block has ambiguous original specialization identity" + .into(), + ); + } + Ok(Some(LexicalScope { + path: scope.path.clone(), + active: Vec::new(), + })) + } + pub(crate) fn lexical_exit( + &mut self, + kind: ResourceLexicalExitKind, + block: BlockId, + position: ResourcePosition, + scopes: &[LexicalScope], + original_exit: Option<&hir::Statement>, + target: Option, + ) -> Option { + if scopes.iter().all(|scope| scope.active.is_empty()) { + return None; + } + let witness = self.witness.as_mut()?; + let site = ResourceSite { + function: witness.original.id, + block, + position, + }; + let id = ResourceLexicalExitId(witness.lexical_exits.len()); + witness.lexical_exits.push(ResourceLexicalExit { + id, + kind, + site, + scopes: scopes.to_vec(), + original_exit: original_exit.cloned(), + target, + }); + Some(id) + } +} +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.lexical_seal = witness.lexical_exits.clone(); + witness.lexical_owner_seal = witness.lexical_owners.clone(); +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.lexical_exits != witness.lexical_seal { + return Err( + "Resource lexical exits changed their independent constructor membership seal".into(), + ); + } + owned_declarations::current(witness, function)?; + let mut inventory = inventory(witness); + inventory.declarations.extend( + witness + .lexical_owners + .iter() + .map(owned_declarations::OwnedDeclaration::declaration), + ); + for declaration in &inventory.declarations { + if !witness + .lexical_exits + .iter() + .any(|exit| exit.declarations().any(|local| local == declaration.local)) + { + return Err("Resource lexical exits lost the complete original declaration retirement inventory".into()); + } + } + for (index, exit) in witness.lexical_exits.iter().enumerate() { + if exit.id.0 != index || exit.site.function != function.id { + return Err( + "Resource lexical exit has a foreign or duplicated constructor identity".into(), + ); + } + let mut keys = BTreeSet::new(); + for scope in &exit.scopes { + if !inventory + .scopes + .iter() + .any(|original| original.path == scope.path) + { + return Err("Resource lexical exit lost its original lexical block".into()); + } + for key in &scope.active { + if !keys.insert(key.index()) + || !inventory + .declarations + .iter() + .any(|row| row.local == *key && row.scope == scope.path) + { + return Err( + "Resource lexical exit changed its exact original declaration membership" + .into(), + ); + } + } + } + if let Some(original_exit) = &exit.original_exit { + let scope = exit + .scopes + .last() + .and_then(|scope| { + inventory + .scopes + .iter() + .find(|original| original.path == scope.path) + }) + .ok_or("Resource lexical exit lost its exact original containing block")?; + let matches = scope + .body + .statements + .iter() + .filter(|statement| { + let left = hir::Block { + statements: vec![(*statement).clone()], + span: original_exit.span, + }; + let right = hir::Block { + statements: vec![original_exit.clone()], + span: original_exit.span, + }; + crate::breakpoint_regions::hir_blocks_equal(&left, &right) + }) + .count(); + let kind = matches!( + (exit.kind, &original_exit.kind), + (ResourceLexicalExitKind::Return, H::Return(_)) + | (ResourceLexicalExitKind::Break, H::Break) + | (ResourceLexicalExitKind::Continue, H::Continue) + ); + if matches != 1 || !kind { + return Err( + "Resource lexical exit changed its exact archived control statement".into(), + ); + } + } else if exit.kind != ResourceLexicalExitKind::Fallthrough { + return Err("Resource lexical control exit lost its archived statement".into()); + } + let block = function + .blocks + .get(exit.site.block.index() as usize) + .filter(|block| block.id == exit.site.block) + .ok_or("Resource lexical exit lost its current block")?; + match (exit.kind, exit.site.position) { + (ResourceLexicalExitKind::Return, ResourcePosition::Terminator) + if matches!(block.terminator.kind, TerminatorKind::Return(_)) + && matches!( + exit.original_exit.as_ref().map(|statement| &statement.kind), + Some(H::Return(_)) + ) => {} + ( + ResourceLexicalExitKind::Fallthrough + | ResourceLexicalExitKind::Break + | ResourceLexicalExitKind::Continue, + ResourcePosition::Statement(index), + ) if matches!(block.statements.get(index).map(|statement| &statement.kind), Some(StatementKind::ResourceLexicalExit(id)) if *id == exit.id) => { + if let Some(target) = exit.target { + if index + 1 != block.statements.len() + || !matches!(block.terminator.kind, TerminatorKind::Goto(current) if current == target) + { + return Err( + "Resource lexical loop exit changed its exact boundary target".into(), + ); + } + } + } + _ => { + return Err( + "Resource lexical exit lost its exact constructor node or Return boundary" + .into(), + ); + } + } + let copies = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| matches!(statement.kind, StatementKind::ResourceLexicalExit(id) if id == exit.id)).count(); + if copies != usize::from(exit.kind != ResourceLexicalExitKind::Return) { + return Err("Resource lexical exit has copied or missing current nodes".into()); + } + } + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if let StatementKind::ResourceLexicalExit(id) = statement.kind + && !witness.lexical_exits.iter().any(|exit| { + exit.id == id + && exit.site + == ResourceSite { + function: function.id, + block: block.id, + position: ResourcePosition::Statement(index), + } + }) + { + return Err("Resource lexical exit node has no exact constructor authority".into()); + } + } + } + Ok(()) +} +pub(super) fn remap_blocks( + witness: &mut ResourceLoweringWitness, + map: &[Option], +) -> Result<(), String> { + owned_declarations::remap_blocks(witness, map)?; + for exit in &mut witness.lexical_exits { + exit.site.block = map + .get(exit.site.block.index() as usize) + .copied() + .flatten() + .ok_or("Resource canonical map removed an authenticated lexical exit")?; + if let Some(target) = &mut exit.target { + *target = map + .get(target.index() as usize) + .copied() + .flatten() + .ok_or("Resource canonical map removed an exact lexical loop target")?; + } + } + seal(witness); + Ok(()) +} +impl Function { + pub fn resource_lexical_exit( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Result, String> { + let Some(witness) = &self.resource_lowering else { + return Ok(None); + }; + witness.current(self)?; + Ok(witness.lexical_exits.iter().find(|exit| { + exit.site + == ResourceSite { + function: self.id, + block, + position, + } + })) + } +} + +pub(super) fn shift_prefix(witness: &mut ResourceLoweringWitness, block: BlockId, count: usize) { + owned_declarations::shift_prefix(witness, block, count); + for exit in &mut witness.lexical_exits { + if exit.site.block == block + && let ResourcePosition::Statement(index) = &mut exit.site.position + { + *index += count; + } + } + seal(witness); +} + +/// Only canonical local compaction moves current headers and endpoints. +pub(super) fn remap_locals( + witness: &mut ResourceLoweringWitness, + remap: &mut impl FnMut(&mut LocalId), +) { + owned_declarations::remap_locals(witness, remap); + seal(witness); +} diff --git a/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs b/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs new file mode 100644 index 00000000..b51acb79 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/lexical_borrows_tests.rs @@ -0,0 +1,782 @@ +use super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCES: [&str; 39] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/57_nested_view_optional_some_written.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/58_if_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/59_if_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/60_if_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/61_if_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/62_if_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/63_if_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/64_if_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/65_if_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/66_scope_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/67_scope_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/68_scope_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/69_scope_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/70_scope_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/71_scope_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/72_scope_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/73_scope_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/74_while_optional_written_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/75_while_optional_written_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/76_while_optional_bare_some.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/77_while_optional_bare_none.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/78_while_result_written_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/79_while_result_written_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/80_while_result_bare_ok.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/81_while_result_bare_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/82_if_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/83_if_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/84_scope_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/85_scope_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/86_while_owned_optional_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/87_while_owned_result_reuse.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/88_if_sibling_optional_bindings.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/89_scope_sibling_result_bindings.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/90_scope_optional_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/91_scope_result_bare_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/92_while_optional_bare_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/93_while_result_written_borrow_fail.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/94_while_owned_optional_break.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/95_while_owned_result_continue.jett" + ), +]; + +fn borrowed_function(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.borrowed_sums.is_empty()) + }) + .expect("fixture contains its original borrowed declaration") +} + +fn borrowed_function_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.borrowed_sums.is_empty()) + }) + .expect("fixture contains its original borrowed declaration") +} + +fn exits<'a>( + function: &Function, + plan: &'a ResourceFunctionPlan, +) -> Vec<(BlockId, ResourcePosition, &'a ResourceLexicalExitPlan)> { + let mut exits = Vec::new(); + for block in &function.blocks { + let positions = (0..block.statements.len()) + .map(ResourcePosition::Statement) + .chain(std::iter::once(ResourcePosition::Terminator)); + for position in positions { + if let Some(current) = function + .resource_lexical_exit(block.id, position) + .expect("constructor-owned lexical exit query") + { + let exit = plan + .lexical_exit(block.id, position) + .expect("fresh custody analysis retains the exact lexical exit"); + assert_eq!(exit.kind(), current.kind()); + for id in exit.operation_ids() { + let operation = &plan.operations()[id.index()]; + assert_eq!(operation.id(), *id); + assert_eq!(operation.site().block(), block.id); + assert_eq!(operation.site().position(), position); + assert!(matches!( + operation.role(), + ResourceOperationRole::EndBorrow { .. } + | ResourceOperationRole::EndSumBorrow { .. } + )); + } + let distinct = exit + .clear_locals() + .iter() + .map(|local| local.index()) + .collect::>(); + assert_eq!(distinct.len(), exit.clear_locals().len()); + assert!( + exit.clear_locals() + .iter() + .all(|local| function.local(*local).is_some()) + ); + exits.push((block.id, position, exit)); + } else { + assert!(plan.lexical_exit(block.id, position).is_none()); + } + } + } + exits +} + +fn ends_loan( + plan: &ResourceFunctionPlan, + exit: &ResourceLexicalExitPlan, + loan: ResourceLoanId, +) -> bool { + exit.operation_ids().iter().any(|id| { + matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndBorrow { loan: current } | ResourceOperationRole::EndSumBorrow { loan: current } if *current == loan) + }) +} + +// A lexical boundary may share a block with later statements or precede a loop +// backedge. Search instruction boundaries, rather than comparing block IDs. +fn site_can_reach(function: &Function, start: ResourceSite, end: ResourceSite) -> bool { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let position = |site: ResourceSite| match site.position() { + ResourcePosition::Statement(index) => index, + ResourcePosition::Terminator => function.blocks[site.block().index() as usize] + .statements + .len(), + }; + let mut pending = vec![(start.block(), position(start) + 1)]; + let mut seen = BTreeSet::new(); + while let Some((block, floor)) = pending.pop() { + if !seen.insert((block.index(), floor)) { + continue; + } + if block == end.block() && position(end) >= floor { + return true; + } + pending.extend(cfg.successors(block).iter().map(|target| (*target, 0))); + } + false +} +fn operation_precedes( + function: &Function, + start: &ResourceOperation, + end: &ResourceOperation, +) -> bool { + if start.site() == end.site() { + start.id().index() < end.id().index() + } else { + site_can_reach(function, start.site(), end.site()) + } +} +fn consumes_slot(role: &ResourceOperationRole, slot: ResourceOwnerSlotId) -> bool { + match role { + ResourceOperationRole::Transfer { source, .. } | ResourceOperationRole::SumTake { source, .. } + | ResourceOperationRole::TakeFailureCompanion { source, .. } | ResourceOperationRole::Close { source, .. } + | ResourceOperationRole::Drop { source, .. } | ResourceOperationRole::SumAdopt { source, .. } => *source == slot, + ResourceOperationRole::Replace { destination, replacement, .. } => *destination == slot || *replacement == slot, + ResourceOperationRole::SelfRebind { slot: current } => *current == slot, + ResourceOperationRole::InvokeSourceFunction { operands, .. } | ResourceOperationRole::InvokeHook { operands, .. } => operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Owned { slot: current, .. } if *current == slot)), + _ => false, + } +} + +fn assert_forged_refused(mut program: Program, types: &TypeInterner, control: &str) { + let function = borrowed_function_mut(&mut program); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = function.blocks.clone(); + witness.locals = function.locals.clone(); + assert!( + validate_resource_ownership(&program, types).is_err(), + "{control}" + ); + assert!( + !crate::sequences::prune::unreachable(borrowed_function_mut(&mut program)), + "canonical {control}" + ); +} + +#[test] +fn resource_lexical_borrows_shared_sources_keep_exact_exit_cleanup_after_canonical_pruning() { + for release in [false, true] { + let mut projections = 0; + for (index, source) in SOURCES.into_iter().enumerate() { + let fixture = index + 57; + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + for function in &mut program.functions { + assert!( + crate::sequences::prune::unreachable(function), + "Source{fixture} release={release}" + ); + } + let ownership = validate_resource_ownership(&program, types) + .unwrap_or_else(|errors| panic!("Source{fixture} release={release}: {errors:?}")); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + assert!(!exits.is_empty(), "Source{fixture} release={release}"); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!( + rows.len(), + if fixture == 88 || fixture == 89 { 2 } else { 1 } + ); + projections += rows.len(); + for operation in plan.operations() { + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = operation.role() + else { + continue; + }; + let row = function + .resource_borrowed_sum_projection( + operation.site().block(), + operation.site().position(), + ) + .unwrap() + .unwrap(); + let cleanup = exits + .iter() + .filter(|(_, _, exit)| ends_loan(plan, exit, *child)) + .collect::>(); + assert!(!cleanup.is_empty(), "Source{fixture} child={child:?}"); + for (_, _, exit) in cleanup { + for local in [row.alias(), row.source(), row.output()] { + assert!( + exit.clear_locals().contains(&local), + "Source{fixture} stale local={local:?}" + ); + assert!(function.is_view_local(local)); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local))); + } + } + if matches!( + plan.loans()[parent.index()].source(), + ResourceLoanSource::IncomingViewFormal { .. } + ) { + assert!(!plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)), "Source{fixture} must preserve its resident caller lease"); + } + } + let expected = match fixture { + 57 => ResourceLexicalExitKind::Return, + 94 => ResourceLexicalExitKind::Break, + 95 => ResourceLexicalExitKind::Continue, + _ => ResourceLexicalExitKind::Fallthrough, + }; + assert!(exits.iter().any(|(_, _, exit)| exit.kind() == expected && !exit.operation_ids().is_empty()), "Source{fixture} lacks {expected:?} cleanup"); + } + assert_eq!(projections, 41); + } +} + +#[test] +fn resource_lexical_borrows_owned_backing_survives_child_first_scope_and_loop_exits() { + for release in [false, true] { + for fixture in [82, 83, 84, 85, 86, 87, 94, 95] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + for operation in plan.operations() { + let ResourceOperationRole::ProjectSumView { + source: parent, + destination: child, + .. + } = operation.role() + else { + continue; + }; + let ResourceLoanSource::Owner(backing_slot) = plan.loans()[parent.index()].source() + else { + panic!("Source{fixture} must borrow its retained local sum") + }; + let row = function + .resource_borrowed_sum_projection( + operation.site().block(), + operation.site().position(), + ) + .unwrap() + .unwrap(); + assert!( + matches!(plan.owner_slots()[backing_slot.index()].storage(), ResourceSlotStorage::Local { header } if header.id == row.backing()) + ); + let normal = exits + .iter() + .find(|(_, _, exit)| { + ends_loan(plan, exit, *child) + && exit.kind() != ResourceLexicalExitKind::Return + }) + .expect("original inner body retires normally before backing reuse"); + assert!(!normal.2.clear_locals().contains(&row.backing())); + let child_end = normal.2.operation_ids().iter().position(|id| matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndBorrow { loan } if loan == child)).unwrap(); + let parent_end = normal.2.operation_ids().iter().position(|id| matches!(plan.operations()[id.index()].role(), ResourceOperationRole::EndSumBorrow { loan } if loan == parent)).expect("locally created parent shell lease retires"); + assert!( + child_end < parent_end, + "Source{fixture} child before parent" + ); + let transfers = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == backing_slot)).collect::>(); + assert_eq!( + transfers.len(), + 1, + "Source{fixture} moves its original held shell exactly once" + ); + let moved = transfers[0]; + let ResourceOperationRole::Transfer { + destination: shell, .. + } = moved.role() + else { + unreachable!() + }; + let exit_site = ResourceSite { + function: function.id, + block: normal.0, + position: normal.1, + }; + assert!( + site_can_reach(function, exit_site, moved.site()), + "Source{fixture} must reuse held after its actual lexical boundary" + ); + let ResourcePosition::Statement(index) = moved.site().position() else { + panic!("Source{fixture} owning Handle begins at its exact source Let") + }; + let ResourceSlotStorage::Local { + header: shell_header, + } = plan.owner_slots()[shell.index()].storage() + else { + panic!("Source{fixture} owning Handle has a dedicated generated source holder") + }; + assert!( + matches!(&function.blocks[moved.site().block().index() as usize].statements[index].kind, StatementKind::Let { local, value } if *local == shell_header.id && matches!(value.kind, hir::ExpressionKind::Local(backing) if backing == row.backing())), + "Source{fixture} transfers that exact original backing, without a snapshot or replacement" + ); + assert_eq!( + plan.owner_slots()[shell.index()].shape(), + plan.owner_slots()[backing_slot.index()].shape() + ); + for candidate in plan.operations() { + if candidate.id() != moved.id() + && consumes_slot(candidate.role(), backing_slot) + && operation_precedes(function, candidate, moved) + { + panic!( + "Source{fixture} consumes backing={} before its later unwrap at operation={}", + backing_slot.index(), + candidate.id().index() + ); + } + } + let takes = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::SumTake { source, success: true, .. } if source == shell)).collect::>(); + assert_eq!( + takes.len(), + 1, + "Source{fixture} extracts the transferred shell once" + ); + let take = takes[0]; + assert!(operation_precedes(function, moved, take)); + let ResourceOperationRole::SumTake { + destination: payload, + .. + } = take.role() + else { + unreachable!() + }; + let mut slot = *payload; + let mut previous = take; + let mut visited = BTreeSet::new(); + loop { + assert!( + visited.insert(slot.index()), + "Source{fixture} final payload transfer chain is acyclic" + ); + let closes = plan.operations().iter().filter(|operation| match operation.role() { + ResourceOperationRole::Close { source, .. } => *source == slot, + ResourceOperationRole::InvokeSourceFunction { function: callee, operands, .. } => { + let declaration = &program.functions[callee.index() as usize].identity.declaration; + declaration.namespace == "resource_probe" && declaration.name == "close" && operands.iter().any(|operand| matches!(operand, ResourceCallOperand::Owned { slot: current, .. } if *current == slot)) + } + _ => false, + }).collect::>(); + if !closes.is_empty() { + assert_eq!( + closes.len(), + 1, + "Source{fixture} closes the extracted payload exactly once" + ); + assert!(operation_precedes(function, previous, closes[0])); + break; + } + let next = plan.operations().iter().filter(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == slot)).collect::>(); + assert_eq!( + next.len(), + 1, + "Source{fixture} carries payload={} into one exact owning continuation", + slot.index() + ); + assert!(operation_precedes(function, previous, next[0])); + let ResourceOperationRole::Transfer { destination, .. } = next[0].role() else { + unreachable!() + }; + slot = *destination; + previous = next[0]; + } + if matches!(fixture, 86 | 87 | 94 | 95) { + let block = &function.blocks[normal.0.index() as usize]; + assert!( + matches!(block.terminator.kind, TerminatorKind::Goto(_)), + "Source{fixture} cleanup precedes its backedge/break/continue" + ); + } + } + } + } +} + +#[test] +fn resource_lexical_borrows_sibling_names_keep_separate_original_keys_and_backings() { + for release in [false, true] { + for fixture in [88, 89] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + assert_eq!(rows.len(), 2); + assert_eq!( + function.local(rows[0].alias()).unwrap().name, + function.local(rows[1].alias()).unwrap().name + ); + assert_ne!(rows[0].alias(), rows[1].alias()); + assert_ne!(rows[0].backing(), rows[1].backing()); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + for (index, row) in rows.iter().enumerate() { + assert!(exits.iter().any(|(_, _, exit)| exit.kind() + == ResourceLexicalExitKind::Fallthrough + && exit.clear_locals().contains(&row.alias()) + && !exit.clear_locals().contains(&rows[1 - index].alias()))); + } + } + } +} + +#[test] +fn resource_lexical_borrows_return_cleanup_is_after_the_observation_operand() { + for release in [false, true] { + let checked = checked_support(SOURCES[0], SUPPORT, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = borrowed_function(&program); + let plan = ownership.function(function.id).unwrap(); + let exits = exits(function, plan); + let child = plan + .operations() + .iter() + .find_map(|operation| match operation.role() { + ResourceOperationRole::ProjectSumView { destination, .. } => Some(*destination), + _ => None, + }) + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!(operation.role(), ResourceOperationRole::BoundedBorrowUse { loan, .. } if *loan == child))); + let returns = exits + .iter() + .filter(|(_, _, exit)| { + exit.kind() == ResourceLexicalExitKind::Return && ends_loan(plan, exit, child) + }) + .collect::>(); + assert!(!returns.is_empty()); + for (block, position, _) in returns { + assert_eq!(*position, ResourcePosition::Terminator); + assert!(matches!( + function.blocks[block.index() as usize].terminator.kind, + TerminatorKind::Return(_) + )); + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_copied_missing_and_swapped_exit_nodes_after_general_reseal() { + for release in [false, true] { + let checked = checked_support(SOURCES[31], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let function = borrowed_function(&baseline); + let nodes = function + .blocks + .iter() + .flat_map(|block| { + block + .statements + .iter() + .enumerate() + .filter_map(move |(index, statement)| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + .then_some((block.id, index)) + }) + }) + .collect::>(); + assert!(nodes.len() >= 2); + for mutation in 0..4 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + let (block, index) = nodes[0]; + match mutation { + 0 => { + let statement = + function.blocks[block.index() as usize].statements[index].clone(); + function.blocks.push(BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } + 1 => { + function.blocks[block.index() as usize] + .statements + .remove(index); + } + 2 => { + let other = + &function.blocks[nodes[1].0.index() as usize].statements[nodes[1].1].kind; + let StatementKind::ResourceLexicalExit(other) = other else { + unreachable!() + }; + let other = *other; + function.blocks[block.index() as usize].statements[index].kind = + StatementKind::ResourceLexicalExit(other); + } + 3 => { + let returning = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(_))) + .unwrap(); + returning.terminator.kind = TerminatorKind::Goto(function.entry); + } + _ => unreachable!(), + } + assert_forged_refused( + forged, + types, + &format!("exit mutation={mutation} release={release}"), + ); + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_duplicate_original_keys_sibling_backing_swaps_and_disconnected_takes() + { + for release in [false, true] { + for fixture in [88, 89] { + let checked = checked_support(SOURCES[fixture - 57], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let function = borrowed_function(&baseline); + let rows = &function.resource_lowering.as_ref().unwrap().borrowed_sums; + let source = rows[0].source(); + let other_backing = rows[1].backing(); + let project = rows[0].success_site(); + for mutation in 0..4 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + match mutation { + 0 => { + let witness = function.resource_lowering.as_mut().unwrap(); + witness.borrowed_sums[1] = witness.borrowed_sums[0].clone(); + } + 1 => { + function + .resource_lowering + .as_mut() + .unwrap() + .borrowed_sums + .pop(); + } + 2 => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| matches!(statement.kind, StatementKind::Let { local, .. } if local == source)).unwrap(); + let StatementKind::Let { value, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::View(inner) = &mut value.kind else { + unreachable!() + }; + inner.kind = hir::ExpressionKind::Local(other_backing); + } + 3 => { + let ResourcePosition::Statement(index) = project.position() else { + unreachable!() + }; + let statement = function.blocks[project.block().index() as usize] + .statements[index] + .clone(); + function.blocks.push(BasicBlock { + id: BlockId(function.blocks.len() as u32), + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.span, + }, + }); + } + _ => unreachable!(), + } + assert_forged_refused( + forged, + types, + &format!("Source{fixture} key mutation={mutation} release={release}"), + ); + } + } + } +} + +#[test] +fn resource_lexical_borrows_refuse_changed_exit_membership_with_unchanged_nodes_and_sites() { + for release in [false, true] { + let checked = checked_support(SOURCES[31], SUPPORT, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let witness = borrowed_function(&baseline) + .resource_lowering + .as_ref() + .unwrap(); + let candidates = witness + .lexical_exits + .iter() + .enumerate() + .filter_map(|(index, exit)| { + (exit.kind() == ResourceLexicalExitKind::Fallthrough + && exit.scopes.iter().any(|scope| !scope.active.is_empty())) + .then_some(index) + }) + .collect::>(); + assert_eq!(candidates.len(), 2); + let [left, right] = [candidates[0], candidates[1]]; + for mutation in 0..3 { + let mut forged = baseline.clone(); + let function = borrowed_function_mut(&mut forged); + let original_blocks = function.blocks.clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + let independent = witness.lexical_seal.clone(); + match mutation { + 0 => { + let left_scopes = witness.lexical_exits[left].scopes.clone(); + witness.lexical_exits[left].scopes = + witness.lexical_exits[right].scopes.clone(); + witness.lexical_exits[right].scopes = left_scopes; + } + 1 => { + for scope in &mut witness.lexical_exits[left].scopes { + scope.active.clear(); + } + } + 2 => { + let scope = witness.lexical_exits[left] + .scopes + .iter_mut() + .find(|scope| !scope.active.is_empty()) + .unwrap(); + scope.active.push(scope.active[0]); + } + _ => unreachable!(), + } + assert_eq!(witness.lexical_seal, independent); + assert!(crate::breakpoint_regions::blocks_equal( + &function.blocks, + &original_blocks + )); + assert_forged_refused( + forged, + types, + &format!("membership mutation={mutation} release={release}"), + ); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/mod.rs b/crates/jett_mir/src/resource_ownership/mod.rs new file mode 100644 index 00000000..a2f70245 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/mod.rs @@ -0,0 +1,1950 @@ +//! Initial Source-authenticated custody analysis, separate from ordinary Move/Copy. +//! A successful plan is metadata proof only; the Resource ABI remains mandatory. +use super::*; +use std::collections::{BTreeMap, BTreeSet}; + +macro_rules! identity { + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(usize); + impl $name { + pub fn index(self) -> usize { + self.0 + } + } + }; +} +mod borrowed_sums; +mod lexical_borrows; +mod owned_declarations; +pub(crate) use lexical_borrows::LexicalScope; +pub use lexical_borrows::{ + ResourceLexicalExit, ResourceLexicalExitId, ResourceLexicalExitKind, ResourceLexicalExitPlan, +}; +#[cfg(test)] +mod borrowed_sums_tests; +pub(crate) mod constructors; +#[cfg(test)] +mod execution_frames_tests; +#[cfg(test)] +mod lexical_borrows_tests; +#[cfg(test)] +mod nested_owners_tests; +mod normalized_calls; +pub(crate) use borrowed_sums::BorrowedSumSeed; +pub use borrowed_sums::{ResourceBorrowedSumProjection, ResourceSumPayloadPath}; +pub use constructors::{ + ResourceConstructorField, ResourceConstructorId, ResourceConstructorNode, + ResourceConstructorRegion, +}; +#[cfg(test)] +mod normalized_calls_tests; +pub use normalized_calls::{ + ResourceCallActual, ResourceCallNode, ResourceCallRegion, ResourceCallRegionId, +}; +mod carriers; +pub use carriers::{ + ResourceCarrierChild, ResourceCarrierConstructor, ResourceCarrierEdge, ResourceCarrierField, + ResourceCarrierFunctionPlan, ResourceCarrierGeneration, ResourceCarrierIndex, + ResourceCarrierIteration, ResourceCarrierLoan, ResourceCarrierLoanId, + ResourceCarrierLoanSource, ResourceCarrierNode, ResourceCarrierObservation, + ResourceCarrierOperation, ResourceCarrierOperationId, ResourceCarrierOperationRole, + ResourceCarrierProjectionPath, ResourceCarrierShape, ResourceCarrierShapeId, + ResourceCarrierSlot, ResourceCarrierSlotId, ResourceCarrierState, ResourceCarrierValue, + ResourceCarrierVariant, +}; +#[cfg(test)] +mod carriers_tests; +mod companion; +mod graph_metadata; +pub use graph_metadata::{GraphHookProjectionProof, GraphSourceEdgeProof, GraphStorageProof}; +mod control_flow; +#[cfg(test)] +mod control_flow_tests; +pub(crate) use companion::CompanionContext; +pub use companion::ResourceCompanionPlan; +mod entry_scope; +mod execution_closure; +pub use entry_scope::{ResourceEntryScopePlan, validate_resource_ownership_for_entry}; +mod named_callables; +mod native_suites; +mod returned_descriptors; +pub use named_callables::ResourceNamedCallableProof; +#[cfg(test)] +mod execution_closure_tests; +mod flow; +pub(crate) mod reflected_fields; +pub(super) use execution_closure::ResourceExecutionClosure; +#[cfg(test)] +mod original_calls_tests; +#[cfg(test)] +mod tests; +pub(crate) mod walk; + +identity!(ResourceFrameId); +identity!(ResourceOwnerSlotId); +identity!(ResourceLoanId); +identity!(ResourceOperationId); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourcePosition { + Statement(usize), + Terminator, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResourceSite { + function: FunctionId, + block: BlockId, + position: ResourcePosition, +} +impl ResourceSite { + pub fn function(self) -> FunctionId { + self.function + } + pub fn block(self) -> BlockId { + self.block + } + pub fn position(self) -> ResourcePosition { + self.position + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceShape { + Plain { + kind: hir::ResourceKindRef, + }, + Optional { + kind: hir::ResourceKindRef, + }, + Result { + kind: hir::ResourceKindRef, + failure: TypeId, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourcePath { + Some, + Ok, +} +impl ResourceShape { + pub fn kind(&self) -> &hir::ResourceKindRef { + match self { + Self::Plain { kind } | Self::Optional { kind } | Self::Result { kind, .. } => kind, + } + } + pub fn active_path(&self) -> &[ResourcePath] { + match self { + Self::Plain { .. } => &[], + Self::Optional { .. } => &[ResourcePath::Some], + Self::Result { .. } => &[ResourcePath::Ok], + } + } + fn conditional(&self) -> bool { + !matches!(self, Self::Plain { .. }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceFrameRole { + Scope, + Operation, + Return, +} +#[derive(Debug, Clone)] +pub struct ResourceFrame { + id: ResourceFrameId, + role: ResourceFrameRole, + site: ResourceSite, + parent: Option, + ordinal: usize, +} +impl ResourceFrame { + pub fn id(&self) -> ResourceFrameId { + self.id + } + pub fn role(&self) -> ResourceFrameRole { + self.role + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn parent(&self) -> Option { + self.parent + } + /// Expression occurrence within the exact containing-function statement or terminator. + pub fn ordinal(&self) -> usize { + self.ordinal + } +} + +#[derive(Debug, Clone)] +pub enum ResourceSlotStorage { + Local { + header: Local, + }, + Expression { + site: ResourceSite, + ordinal: usize, + }, + Argument { + site: ResourceSite, + call_ordinal: usize, + parameter: usize, + }, + Return { + site: ResourceSite, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceOwnerSlot { + id: ResourceOwnerSlotId, + frame: ResourceFrameId, + shape: ResourceShape, + storage: ResourceSlotStorage, +} +impl ResourceOwnerSlot { + pub fn id(&self) -> ResourceOwnerSlotId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn shape(&self) -> &ResourceShape { + &self.shape + } + pub fn storage(&self) -> &ResourceSlotStorage { + &self.storage + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceLoanSource { + CarrierSumProjection { + operation: ResourceCarrierOperationId, + }, + Owner(ResourceOwnerSlotId), + IncomingViewFormal { + scope: ResourceFrameId, + parameter: usize, + }, + ProjectedSumPayload { + parent: ResourceLoanId, + tag: LocalId, + path: ResourceSumPayloadPath, + }, +} +impl ResourceLoanSource { + // Preserve every typed identity in the private loan-site map. LocalId does + // not need a global ordering contract to distinguish selecting tag sites. + fn ordering_key(self) -> (u8, usize, usize, u32, u8) { + match self { + Self::CarrierSumProjection { operation } => (3, operation.index(), 0, 0, 0), + Self::Owner(owner) => (0, owner.index(), 0, 0, 0), + Self::IncomingViewFormal { scope, parameter } => (1, scope.index(), parameter, 0, 0), + Self::ProjectedSumPayload { parent, tag, path } => ( + 2, + parent.index(), + 0, + tag.index(), + match path { + ResourceSumPayloadPath::OptionalSome => 0, + ResourceSumPayloadPath::ResultOk => 1, + }, + ), + } + } +} +impl PartialOrd for ResourceLoanSource { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for ResourceLoanSource { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.ordering_key().cmp(&other.ordering_key()) + } +} +#[derive(Debug, Clone)] +pub struct ResourceLoan { + id: ResourceLoanId, + frame: ResourceFrameId, + source: ResourceLoanSource, + shape: ResourceShape, + parameter: Option, +} +impl ResourceLoan { + pub fn shape(&self) -> &ResourceShape { + &self.shape + } + pub fn id(&self) -> ResourceLoanId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + /// Sealed call parameter, independent of any reached invocation. + pub fn parameter(&self) -> Option { + self.parameter + } + pub fn source(&self) -> ResourceLoanSource { + self.source + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceOccupancy { + Occupied, + Conditional, + Empty, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCompletion { + Normal, + Return, + Abort, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceCallOperand { + CarrierOwned { + parameter: usize, + slot: ResourceCarrierSlotId, + }, + CarrierBorrowed { + parameter: usize, + loan: ResourceCarrierLoanId, + }, + Ordinary { + parameter: usize, + ty: TypeId, + }, + Owned { + parameter: usize, + slot: ResourceOwnerSlotId, + }, + Borrowed { + parameter: usize, + loan: ResourceLoanId, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCallResult { + Carrier { slot: ResourceCarrierSlotId }, + Ordinary { ty: TypeId }, + Owned { slot: ResourceOwnerSlotId }, +} + +/// Readonly projection of the original checked Source tuple, not a minting API. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceArgumentSyntax { + Bare, + WrittenView, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceArgumentEffect { + Copy, + TransferOwned, + RelinquishOwned, + RetainBorrow, + ObserveData, +} +#[derive(Debug, Clone)] +pub struct ResourceCallFormal { + parameter: usize, + source_index: usize, + actual_type: TypeId, + parameter_type: TypeId, + syntax: ResourceArgumentSyntax, + effect: ResourceArgumentEffect, + access: ParamMode, +} +impl ResourceCallFormal { + pub fn parameter(&self) -> usize { + self.parameter + } + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn actual_type(&self) -> TypeId { + self.actual_type + } + pub fn parameter_type(&self) -> TypeId { + self.parameter_type + } + pub fn syntax(&self) -> ResourceArgumentSyntax { + self.syntax + } + pub fn effect(&self) -> ResourceArgumentEffect { + self.effect + } + pub fn access(&self) -> ParamMode { + self.access + } + fn original(argument: &hir::ArgumentOwnership) -> Self { + use jett_typecheck::{ + CheckedCalleeAccess as A, CheckedCallerEffect as E, CheckedCallerSyntax as S, + }; + Self { + parameter: argument.parameter_index, + source_index: argument.source_index, + actual_type: argument.actual_type, + parameter_type: argument.parameter_type, + syntax: match argument.syntax { + S::Bare => ResourceArgumentSyntax::Bare, + S::WrittenView => ResourceArgumentSyntax::WrittenView, + }, + effect: match argument.effect { + E::Copy => ResourceArgumentEffect::Copy, + E::TransferOwned => ResourceArgumentEffect::TransferOwned, + E::RelinquishOwned => ResourceArgumentEffect::RelinquishOwned, + E::RetainBorrow => ResourceArgumentEffect::RetainBorrow, + E::ObserveData => ResourceArgumentEffect::ObserveData, + }, + access: match argument.callee_access { + A::Owned => ParamMode::Owned, + A::View => ParamMode::View, + }, + } + } +} + +/// Readonly metadata from one exact, privately constructed compiler suite call. +/// The boxed operation payload keeps the shared operation enum bounded. +#[derive(Debug, Clone)] +pub struct ResourceNativeSuiteInvocation { + function: FunctionId, + proof: hir::CheckedNativeSuiteCall, + formals: Vec, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, +} +impl ResourceNativeSuiteInvocation { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn proof(&self) -> &hir::CheckedNativeSuiteCall { + &self.proof + } + pub fn formals(&self) -> &[ResourceCallFormal] { + &self.formals + } + pub fn evaluation_order(&self) -> &[usize] { + &self.evaluation_order + } + pub fn operands(&self) -> &[ResourceCallOperand] { + &self.operands + } + pub fn result(&self) -> ResourceCallResult { + self.result + } +} + +/// Closed metadata roles. None is a public constructor for a validated plan. +#[derive(Debug, Clone)] +pub enum ResourceOperationRole { + Carrier { + operation: ResourceCarrierOperationId, + }, + BeginSourceFunction { + region: ResourceCallRegionId, + function: FunctionId, + source: hir::SourceCallOwnership, + formals: Vec, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + /// One installed Borrow authority row; execution activates it only at Stage. + PrepareSourceBorrow { + loan: ResourceLoanId, + parameter: usize, + }, + PrepareSourceSumBorrow { + loan: ResourceLoanId, + parameter: usize, + }, + StageSourceActual { + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + operand: ResourceCallOperand, + ordinary: Option, + }, + Acquire { + hook: hir::ResourceHookRef, + destination: ResourceOwnerSlotId, + }, + Transfer { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + }, + Borrow { + loan: ResourceLoanId, + }, + BorrowSum { + loan: ResourceLoanId, + }, + EndSumBorrow { + loan: ResourceLoanId, + }, + ObserveSumView { + source: ResourceLoanId, + target: LocalId, + }, + ProjectSumView { + source: ResourceLoanId, + destination: ResourceLoanId, + tag: LocalId, + path: ResourceSumPayloadPath, + }, + ReadFailureCompanion { + source: ResourceLoanId, + target: Local, + failure: TypeId, + tag: LocalId, + }, + BoundedBorrowUse { + loan: ResourceLoanId, + parameter: usize, + }, + EndBorrow { + loan: ResourceLoanId, + }, + InvokeHook { + hook: hir::ResourceHookRef, + source: hir::SourceCallOwnership, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + InvokeSourceFunction { + function: FunctionId, + formals: Vec, + source: hir::SourceCallOwnership, + evaluation_order: Vec, + operands: Vec, + result: ResourceCallResult, + }, + /// Exact canonical compiler suite edge; it never supplies Source authority. + InvokeNativeSuite { + invocation: Box, + }, + /// Move a sole plain payload into a fresh conditional shell. + SumAdopt { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + }, + CreateAbsentSum { + destination: ResourceOwnerSlotId, + }, + CreateFailureSum { + destination: ResourceOwnerSlotId, + failure: TypeId, + }, + TakeFailureCompanion { + source: ResourceOwnerSlotId, + target: Local, + failure: TypeId, + tag: LocalId, + }, + SumTake { + source: ResourceOwnerSlotId, + destination: ResourceOwnerSlotId, + tag: LocalId, + success: bool, + }, + Close { + hook: hir::ResourceHookRef, + source: ResourceOwnerSlotId, + }, + Replace { + destination: ResourceOwnerSlotId, + replacement: ResourceOwnerSlotId, + old: ResourceOccupancy, + }, + /// Exact successful mutable Local = Local reseat; no runtime effect or row. + SelfRebind { + slot: ResourceOwnerSlotId, + }, + /// Scope completion obligations are retired by the dynamic core log, never local-number order. + Drop { + source: ResourceOwnerSlotId, + occupancy: ResourceOccupancy, + }, + Complete { + outcome: ResourceCompletion, + }, + CompleteReturnAfterCleanup { + source: ResourceOwnerSlotId, + }, + Descriptor { + hook: hir::ResourceHookRef, + }, +} +#[derive(Debug, Clone)] +pub struct ResourceOperation { + id: ResourceOperationId, + frame: ResourceFrameId, + site: ResourceSite, + ordinal: usize, + role: ResourceOperationRole, + // Address identity is private, borrowed from this fresh plan's immutable Program. + expression: Option, + named_indirect: Option, + indirect_hook: Option, +} +impl ResourceOperation { + pub fn id(&self) -> ResourceOperationId { + self.id + } + pub fn frame(&self) -> ResourceFrameId { + self.frame + } + pub fn site(&self) -> ResourceSite { + self.site + } + pub fn ordinal(&self) -> usize { + self.ordinal + } + pub fn role(&self) -> &ResourceOperationRole { + &self.role + } + /// Separates constructor-captured expression roles from statement/terminator roles. + pub fn is_expression_operation(&self) -> bool { + self.expression.is_some() + } + /// Exact reached Indirect Source body selection, never target-name authority. + pub fn indirect_hook_target(&self) -> Option<&hir::ResourceHookRef> { + self.indirect_hook.as_ref() + } + pub fn named_indirect_target(&self) -> Option<&ResourceNamedCallableProof> { + self.named_indirect.as_ref() + } +} + +#[derive(Debug)] +pub struct ResourceFunctionPlan { + carriers: Option, + function: FunctionId, + identity: FunctionIdentity, + parameters: Vec, + return_type: TypeId, + frames: Vec, + slots: Vec, + loans: Vec, + operations: Vec, + execution_frames: Vec<(ResourceSite, ResourceFrameId)>, + lexical_exits: Vec, + named_callable_producers: Vec<(usize, ResourceNamedCallableProof)>, + named_callable_values: Vec<(usize, ResourceNamedCallableProof)>, + descriptor_return: Option, + descriptor_values: Vec<(usize, hir::ResourceHookRef)>, + descriptor_locals: Vec<(LocalId, hir::ResourceHookRef)>, + graph_descriptors: Vec<(hir::ActorGraphInstruction, ResourceOperationId)>, +} +impl ResourceFunctionPlan { + pub fn graph_descriptor_operations( + &self, + ) -> impl Iterator { + self.graph_descriptors + .iter() + .filter_map(|(instruction, id)| { + self.operations + .get(id.index()) + .map(|operation| (instruction, operation)) + }) + } + pub fn carriers(&self) -> Option<&ResourceCarrierFunctionPlan> { + self.carriers.as_ref() + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } + pub fn parameters(&self) -> &[Param] { + &self.parameters + } + pub fn return_type(&self) -> TypeId { + self.return_type + } + pub fn root_scope(&self) -> &ResourceFrame { + &self.frames[0] + } + pub fn provisional_return(&self) -> Option<&ResourceFrame> { + self.root_scope() + .parent + .and_then(|id| self.frames.get(id.index())) + .filter(|frame| frame.role == ResourceFrameRole::Return) + } + pub fn entry(&self) -> BlockId { + self.frames[0].site.block + } + pub fn frames(&self) -> &[ResourceFrame] { + &self.frames + } + pub fn owner_slots(&self) -> &[ResourceOwnerSlot] { + &self.slots + } + pub fn loans(&self) -> &[ResourceLoan] { + &self.loans + } + pub fn operations(&self) -> &[ResourceOperation] { + &self.operations + } + /// Incoming execution frame at an exact reachable current site, captured + /// only after the custody CFG fixed point and original witness validate. + pub fn execution_frame( + &self, + block: BlockId, + position: ResourcePosition, + ) -> Option { + let site = ResourceSite { + function: self.function, + block, + position, + }; + self.execution_frames + .iter() + .find_map(|(current, frame)| (*current == site).then_some(*frame)) + } + /// Exact current occurrence, never a span/type/signature lookup. + pub fn operations_for_expression( + &self, + expression: &Expression, + ) -> impl Iterator { + let address = std::ptr::from_ref(expression).addr(); + self.operations + .iter() + .filter(move |operation| operation.expression == Some(address)) + } + /// Only an authenticated original FunctionRef producer at this exact current occurrence. + /// Exact retained hook Return summary, rechecked against current reachable returns. + pub fn descriptor_return(&self) -> Option<&hir::ResourceHookRef> { + self.descriptor_return.as_ref() + } + /// Installation-owned metadata at this exact current expression occurrence. + pub fn descriptor_value(&self, expression: &Expression) -> Option<&hir::ResourceHookRef> { + let address = std::ptr::from_ref(expression).addr(); + self.descriptor_values + .iter() + .find_map(|(current, hook)| (*current == address).then_some(hook)) + } + pub fn descriptor_local(&self, local: LocalId) -> Option<&hir::ResourceHookRef> { + self.descriptor_locals + .iter() + .find_map(|(current, hook)| (*current == local).then_some(hook)) + } + pub fn named_callable_producer( + &self, + expression: &Expression, + ) -> Option<&ResourceNamedCallableProof> { + let address = std::ptr::from_ref(expression).addr(); + self.named_callable_producers + .iter() + .find_map(|(current, proof)| (*current == address).then_some(proof)) + } + /// Exact original producer, immutable alias initializer or reached callee descriptor value. + /// Callable signatures alone never make a physical Resource owner. + pub fn named_callable_value( + &self, + expression: &Expression, + ) -> Option<&ResourceNamedCallableProof> { + let address = std::ptr::from_ref(expression).addr(); + self.named_callable_values + .iter() + .find_map(|(current, proof)| (*current == address).then_some(proof)) + } +} +#[derive(Debug)] +pub struct ResourceOwnershipPlan<'p> { + program: &'p Program, + types: &'p TypeInterner, + functions: Vec, + required_only_functions: Vec, + entry_scope: Option, +} +impl<'p> ResourceOwnershipPlan<'p> { + pub fn program(&self) -> &'p Program { + self.program + } + pub fn types(&self) -> &'p TypeInterner { + self.types + } + /// Classification under this immutable validated graph; no ordinary carrier authority. + pub fn type_requires_custody(&self, ty: TypeId) -> bool { + resource_type_pending(self.types, ty) + } + pub fn carrier_shapes(&self) -> &[ResourceCarrierShape] { + self.functions + .iter() + .find_map(|function| function.carriers.as_ref()) + .map_or(&[], ResourceCarrierFunctionPlan::shapes) + } + pub fn functions(&self) -> &[ResourceFunctionPlan] { + &self.functions + } + /// Wrapper-only Scope metadata; it grants no Source execution-family membership. + pub fn entry_scope(&self) -> Option<&ResourceEntryScopePlan> { + self.entry_scope.as_ref() + } + /// Exact checked helpers omitted only as implicit native roots. This grants + /// no runtime body, carrier or invocation authority for those functions. + pub fn required_only_function_ids(&self) -> &[FunctionId] { + &self.required_only_functions + } + pub fn original_source(&self, id: FunctionId) -> Option<&'p hir::Function> { + self.program + .functions + .get(id.index() as usize) + .filter(|function| function.id == id) + .and_then(|function| function.resource_lowering.as_ref()) + .and_then(|witness| { + witness + .source + .functions() + .get(id.index() as usize) + .filter(|function| function.id == id) + }) + } + /// The exact original Source header or privately constructed suite header. + /// A canonical suite stays distinct from an original Source declaration. + pub fn authenticated_entry(&self, id: FunctionId) -> Option<&'p hir::Function> { + self.original_source(id).or_else(|| { + self.program + .functions + .get(id.index() as usize) + .filter(|function| function.id == id) + .and_then(|function| function.resource_lowering.as_ref()) + .and_then(|witness| witness.source.native_suite_function(id)) + }) + } + pub fn function(&self, id: FunctionId) -> Option<&ResourceFunctionPlan> { + self.functions + .iter() + .find(|function| function.function == id) + } +} + +#[derive(Debug, Clone, PartialEq)] +struct OriginalCallAssociation { + original: Expression, + current: Expression, +} +impl OriginalCallAssociation { + fn same(&self, other: &Self) -> bool { + crate::breakpoint_regions::expressions_equal(&self.original, &other.original) + && crate::breakpoint_regions::expressions_equal(&self.current, &other.current) + } +} +#[derive(Debug, Clone, PartialEq)] +pub(super) struct ResourceLoweringWitness { + original: hir::Function, + calls: Vec, + regions: Vec, + constructors: Vec, + constructor_seal: std::sync::Arc<[ResourceConstructorRegion]>, + reflected_fields: Vec, + reflected_seal: Vec, + borrowed_sums: Vec, + borrowed_seal: Vec, + lexical_exits: Vec, + lexical_seal: Vec, + lexical_owners: Vec, + lexical_owner_seal: Vec, + named_callables: Vec, + descriptors: returned_descriptors::DescriptorWitness, + graph_metadata: graph_metadata::Witness, + source: hir::ResourceSourceArchive, + execution: ResourceExecutionClosure, + manifest: hir::ResourceManifest, + parameters: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, +} + +#[derive(Clone, Default)] +pub(super) struct Capture { + witness: Option, +} +impl Capture { + pub(super) fn site(&self, block: BlockId, index: usize) -> Option { + self.function().map(|function| ResourceSite { + function, + block, + position: ResourcePosition::Statement(index), + }) + } + pub(super) fn function(&self) -> Option { + self.witness.as_ref().map(|witness| witness.original.id) + } + /// Only the checked lowering entry calls this, before any operand extraction. + pub(super) fn authenticated( + function: &hir::Function, + manifest: &hir::ResourceManifest, + source: &hir::ResourceSourceArchive, + types: &TypeInterner, + execution: &ResourceExecutionClosure, + ) -> Result { + let metadata = if source.execution_functions().is_empty() { + graph_metadata::Witness::default() + } else { + execution.original_graph(function, source, types)?.clone() + }; + // A raw ordinary predicate has no Resource archive authority. + // Keep custody detection independent so malformed raw Resource bodies + // still follow the existing mandatory refusal path. + let needed = (source.manifest().is_some() + && function.identity.declaration.kind == hir::DeclarationKind::RefinementPredicate) + || source.has_reflected_field_dispatch(function.id) + // Proof-only graph handlers still retain their exact original/current + // body seal even when their captured descriptor is immutable metadata. + || execution.metadata_handler(function.id) + || metadata.needed() + || runtime_custody_needed_with_metadata(function, types, execution, &metadata) + || source.required_only_function_ids().contains(&function.id) + || source + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + || entry_scope::capture_needed(function, source); + Ok(Self { + witness: needed.then(|| ResourceLoweringWitness { + original: function.clone(), + calls: Vec::new(), + regions: Vec::new(), + constructors: Vec::new(), + constructor_seal: std::sync::Arc::from([]), + reflected_fields: Vec::new(), + reflected_seal: Vec::new(), + borrowed_sums: Vec::new(), + borrowed_seal: Vec::new(), + lexical_exits: Vec::new(), + lexical_seal: Vec::new(), + lexical_owners: owned_declarations::capture_owned(function, manifest, types), + lexical_owner_seal: Vec::new(), + named_callables: named_callables::capture(function, source, types, execution), + descriptors: returned_descriptors::capture(function, source), + graph_metadata: metadata, + source: source.clone(), + execution: execution.clone(), + manifest: manifest.clone(), + parameters: function.params.clone(), + locals: function.locals.clone(), + blocks: vec![BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: function.body.span, + }, + }], + entry: BlockId(0), + }), + }) + } + pub(super) fn preserve_original_call( + &mut self, + expression: &Expression, + types: &TypeInterner, + ) -> Result { + let Some(witness) = &self.witness else { + return Ok(false); + }; + if native_suites::preserve(witness, expression)? { + return Ok(true); + } + let (args, ownership) = match &expression.kind { + hir::ExpressionKind::Call { + args, ownership, .. + } + | hir::ExpressionKind::IndirectCall { + args, ownership, .. + } + | hir::ExpressionKind::ResourceInvoke { + args, ownership, .. + } => (args, ownership), + _ => return Ok(false), + }; + let occupied = |ty| { + resource_type_pending(types, ty) && !matches!(types.resolve(ty), Type::Function { .. }) + }; + if !occupied(expression.ty) + && !args.iter().any(|value| occupied(value.ty)) + && !matches!(expression.kind, hir::ExpressionKind::ResourceInvoke { .. }) + && !witness.execution.direct_call(expression) + && !graph_metadata::preserve_original_call(&witness.graph_metadata, expression) + { + return Ok(false); + } + let hir::CallOwnership::Source(source) = ownership else { + return Err( + "Resource original call preservation requires exact Source authority".into(), + ); + }; + if source + .arguments + .iter() + .any(|argument| argument.staging != hir::ArgumentStaging::Original) + { + return Err("Resource original call preservation changed its initial staging".into()); + } + let mut handled = false; + for value in args { + walk::expression(value, &mut |value| { + handled |= matches!(value.kind, hir::ExpressionKind::Handle { .. }) + }); + } + if let hir::ExpressionKind::IndirectCall { callee, .. } = &expression.kind { + walk::expression(callee, &mut |value| { + handled |= matches!(value.kind, hir::ExpressionKind::Handle { .. }) + }); + } + if handled { + return Err("pending ResourceOwnershipPlan: eager handled actuals need the dedicated custody CFG normalizer".into()); + } + let mut occurrences = 0usize; + walk::hir_block(&witness.original.body, &mut |candidate| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + candidate, expression, + )); + }); + if occurrences != 1 { + return Err( + "Resource preservation has no unique exact original Source expression".into(), + ); + } + let mut candidates = Vec::new(); + walk::expression(expression, &mut |candidate| { + if source_custody_call(candidate, types, &witness.execution) + || graph_metadata::preserve_original_call(&witness.graph_metadata, candidate) + { + candidates.push(candidate.clone()); + } + }); + let witness = self + .witness + .as_mut() + .ok_or("Resource association lost its initial witness")?; + for candidate in candidates { + let mut original_count = 0; + walk::hir_block(&witness.original.body, &mut |original| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + original, &candidate, + )); + }); + if original_count != 1 { + return Err("Resource call association has no unique original Source tuple".into()); + } + if !witness.calls.iter().any(|record| { + crate::breakpoint_regions::expressions_equal(&record.current, &candidate) + }) { + witness.calls.push(OriginalCallAssociation { + original: candidate.clone(), + current: candidate, + }); + } + } + Ok(true) + } + pub(super) fn block(&mut self, block: BlockId, span: Span) { + if let Some(witness) = &mut self.witness { + witness.blocks.push(BasicBlock { + id: block, + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span, + }, + }); + } + } + pub(super) fn local(&mut self, local: &Local) { + if let Some(witness) = &mut self.witness { + witness.locals.push(local.clone()); + } + } + pub(super) fn statement(&mut self, block: BlockId, index: usize, value: &Statement) { + if let Some(witness) = &mut self.witness { + if let Some(destination) = witness.blocks.get_mut(block.index() as usize) { + // Sequential constructor writes only; a mismatch remains unsealable. + if destination.id == block && destination.statements.len() == index { + destination.statements.push(value.clone()); + } + } + } + } + pub(super) fn terminator(&mut self, block: BlockId, value: &Terminator) { + if let Some(witness) = &mut self.witness { + if let Some(destination) = witness + .blocks + .get_mut(block.index() as usize) + .filter(|destination| destination.id == block) + { + destination.terminator = value.clone(); + } + } + } + pub(super) fn finish( + mut self, + function: &Function, + types: &TypeInterner, + ) -> Result, String> { + if let Some(witness) = &mut self.witness { + witness.constructor_seal = witness.constructors.clone().into(); + lexical_borrows::seal(witness); + borrowed_sums::seal(witness); + reflected_fields::seal(witness); + graph_metadata::seal( + &mut witness.graph_metadata, + function, + &witness.calls, + &witness.constructor_seal, + types, + )?; + } + if let Some(witness) = &mut self.witness + && (!witness.constructors.is_empty() + || !witness.borrowed_sums.is_empty() + || !witness.lexical_owners.is_empty() + || entry_scope::capture_needed(&witness.original, &witness.source)) + { + witness.descriptors.seal_body(function); + } + if let Some(witness) = &self.witness { + witness.current(function)?; + } + if let Some(witness) = &mut self.witness { + if witness + .source + .required_only_function_ids() + .contains(&function.id) + || !witness.constructors.is_empty() + || !witness.borrowed_sums.is_empty() + || !witness.lexical_owners.is_empty() + || entry_scope::capture_needed(&witness.original, &witness.source) + || witness + .source + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + { + witness.descriptors.seal_body(function); + } else { + witness.descriptors.seal(function); + } + } + Ok(self.witness) + } +} +impl ResourceLoweringWitness { + fn current(&self, function: &Function) -> Result<(), String> { + if function.id != self.original.id + || function.identity != self.original.identity + || function.debug_kind != self.original.debug_kind + || function.params != self.parameters + || function.capture_count != self.original.capture_count + || function.return_type != self.original.return_type + || function.span != self.original.span + || function.entry != self.entry + || function.locals != self.locals + || !crate::breakpoint_regions::blocks_equal(&function.blocks, &self.blocks) + { + return Err("Resource ownership differs from its initially authenticated Source or constructor-emitted graph".into()); + } + self.descriptors.current(function)?; + graph_metadata::current(&self.graph_metadata, function)?; + if (!self.constructors.is_empty() + || !self.borrowed_sums.is_empty() + || !self.lexical_owners.is_empty() + || entry_scope::capture_needed(&self.original, &self.source)) + && !self.descriptors.has_body() + { + return Err("Resource Source proof lost its independent constructor body seal".into()); + } + if !constructors::records_equal(&self.constructors, &self.constructor_seal) { + return Err( + "Resource constructor lost its independent original/current occurrence seal".into(), + ); + } + for constructor in &self.constructors { + constructor.current(function)?; + let mut occurrences = 0; + walk::hir_block(&self.original.body, &mut |value| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + value, + constructor.original(), + )) + }); + if occurrences != 1 { + return Err( + "Resource constructor lost its unique original checked occurrence".into(), + ); + } + } + borrowed_sums::current(self, function)?; + lexical_borrows::current(self, function)?; + reflected_fields::current(self, function)?; + for region in &self.regions { + region.current(function)?; + let mut original_count = 0; + walk::hir_block(&self.original.body, &mut |value| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + region.original(), + )) + }); + if original_count != 1 { + return Err( + "Resource normalized call lost its unique original checked Source occurrence" + .into(), + ); + } + } + for (index, record) in self.calls.iter().enumerate() { + if self.calls[..index].iter().any(|earlier| { + crate::breakpoint_regions::expressions_equal(&earlier.original, &record.original) + || crate::breakpoint_regions::expressions_equal( + &earlier.current, + &record.current, + ) + }) { + return Err("Resource original call association is duplicated".into()); + } + let (mut original_count, mut current_count) = (0usize, 0usize); + walk::hir_block(&self.original.body, &mut |value| { + original_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + &record.original, + )) + }); + for block in &self.blocks { + walk::mir_block(block, &mut |value| { + current_count += usize::from(crate::breakpoint_regions::expressions_equal( + value, + &record.current, + )) + }); + } + if original_count != 1 || current_count != 1 { + return Err("Resource original call association lost its unique source or current occurrence".into()); + } + } + Ok(()) + } + fn same(&self, other: &Self) -> bool { + self.original.id == other.original.id + && self.original.identity == other.original.identity + && self.original.source_definition == other.original.source_definition + && self.original.params == other.original.params + && self.original.locals == other.original.locals + && self.original.return_type == other.original.return_type + && self.original.capture_count == other.original.capture_count + && self.original.span == other.original.span + && self.original.debug_kind == other.original.debug_kind + && crate::breakpoint_regions::hir_blocks_equal( + &self.original.body, + &other.original.body, + ) + && self.calls.len() == other.calls.len() + && self + .calls + .iter() + .zip(&other.calls) + .all(|(left, right)| left.same(right)) + && self.reflected_fields == other.reflected_fields + && self.reflected_seal == other.reflected_seal + && constructors::records_equal(&self.constructors, &other.constructors) + && std::sync::Arc::ptr_eq(&self.constructor_seal, &other.constructor_seal) + && self.regions.len() == other.regions.len() + && self.borrowed_sums == other.borrowed_sums + && self.borrowed_seal == other.borrowed_seal + && self.lexical_exits == other.lexical_exits + && self.lexical_seal == other.lexical_seal + && owned_declarations::same_owners(&self.lexical_owners, &other.lexical_owners) + && owned_declarations::same_owners(&self.lexical_owner_seal, &other.lexical_owner_seal) + && self + .regions + .iter() + .zip(&other.regions) + .all(|(a, b)| a.same(b)) + && self.named_callables == other.named_callables + && self.descriptors == other.descriptors + && self.graph_metadata == other.graph_metadata + && self.source == other.source + && self.execution == other.execution + && self.manifest == other.manifest + && self.parameters == other.parameters + && self.locals == other.locals + && self.entry == other.entry + && crate::breakpoint_regions::blocks_equal(&self.blocks, &other.blocks) + } +} + +fn original_function_equal(left: &hir::Function, right: &hir::Function) -> bool { + left.id == right.id + && left.identity == right.identity + && left.debug_kind == right.debug_kind + && left.source_definition == right.source_definition + && left.params == right.params + && left.capture_count == right.capture_count + && left.return_type == right.return_type + && left.locals == right.locals + && left.span == right.span + && crate::breakpoint_regions::hir_blocks_equal(&left.body, &right.body) +} +/// Executed before constructor capture; edited public HIR can never be re-sealed. +pub(super) fn authenticate_original( + program: &hir::Program, + types: &TypeInterner, +) -> Result { + let archive = &program.resource_source; + if archive.manifest().is_none() { + if !program.resource_manifest.kinds().is_empty() + || program.resource_manifest.hooks().next().is_some() + || program.functions.iter().any(|function| { + resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + }) + { + return Err("Resource lowering has no original checked HIR archive".into()); + } + return Ok(ResourceExecutionClosure::default()); + } + archive.validate_types(types)?; + if archive.manifest() != Some(&program.resource_manifest) + || archive.equality_methods() != Some(&program.equality_methods) + || archive.execution_functions().len() != program.functions.len() + || !archive + .execution_functions() + .iter() + .zip(&program.functions) + .all(|(left, right)| original_function_equal(left, right)) + { + return Err("Resource lowering differs from its original checked HIR archive".into()); + } + ResourceExecutionClosure::from_archive(archive, types) +} + +pub(super) fn visit_block(block: &BasicBlock, visit: &mut impl FnMut(&Expression)) { + walk::mir_block(block, visit); +} + +pub(super) fn visit_expression(value: &Expression, visit: &mut impl FnMut(&Expression)) { + walk::expression(value, visit); +} + +fn custody_type(types: &TypeInterner, ty: TypeId) -> bool { + resource_type_pending(types, ty) && !matches!(types.resolve(ty), Type::Function { .. }) +} +/// The original runtime classification, independent of required-value records. +fn runtime_custody_needed( + function: &hir::Function, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> bool { + runtime_custody_needed_with_metadata( + function, + types, + execution, + &graph_metadata::Witness::default(), + ) +} +fn runtime_custody_needed_with_metadata( + function: &hir::Function, + types: &TypeInterner, + execution: &ResourceExecutionClosure, + metadata: &graph_metadata::Witness, +) -> bool { + let mut needed = execution.contains(function.id) + || (resource_type_pending(types, function.return_type) + && metadata.original_storage_for_return().is_none()) + || function.locals.iter().any(|local| { + resource_type_pending(types, local.ty) + && metadata.original_storage_for_local(local).is_none() + }); + walk::hir_block(&function.body, &mut |value| { + needed |= matches!( + value.kind, + hir::ExpressionKind::ResourceHookValue { .. } + | hir::ExpressionKind::ResourceInvoke { .. } + ) || matches!(&value.kind,hir::ExpressionKind::ActorGraphStep {instruction,..} + if matches!(instruction.operation(),hir::ActorGraphOperation::HookBox {..})) + || (custody_type(types, value.ty) + && metadata.original_storage_for_expression(value).is_none()); + }); + needed +} +/// A proof-only required row seals a body without granting Resource execution. +/// The immutable archived body and checked execution family select this role. +pub(super) fn has_execution_records(function: &Function, types: &TypeInterner) -> bool { + function.resource_lowering.as_ref().is_some_and(|witness| { + runtime_custody_needed_with_metadata( + &witness.original, + types, + &witness.execution, + &witness.graph_metadata, + ) + }) +} +fn source_custody_call( + value: &Expression, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> bool { + let (args, ownership) = match &value.kind { + hir::ExpressionKind::Call { + args, ownership, .. + } + | hir::ExpressionKind::IndirectCall { + args, ownership, .. + } + | hir::ExpressionKind::ResourceInvoke { + args, ownership, .. + } => (args, ownership), + _ => return false, + }; + matches!(ownership, hir::CallOwnership::Source(source) if source.arguments.iter().all(|argument| argument.staging == hir::ArgumentStaging::Original)) + && (custody_type(types, value.ty) + || args.iter().any(|arg| custody_type(types, arg.ty)) + || matches!(value.kind, hir::ExpressionKind::ResourceInvoke { .. }) + || execution.direct_call(value)) +} +/// A readonly association lookup, never a constructor or ordinary owner claim. +pub(super) fn original_call_at( + function: &Function, + manifest: &hir::ResourceManifest, + types: &TypeInterner, + expression: &Expression, + block: BlockId, + statement: usize, +) -> Result { + let Some(witness) = function.resource_lowering.as_ref() else { + return Ok(false); + }; + if !witness + .calls + .iter() + .any(|record| crate::breakpoint_regions::expressions_equal(&record.current, expression)) + { + return Ok(false); + } + witness.source.validate_types(types)?; + if witness.execution != witness.execution.reconstructed(&witness.source, types)? { + return Err( + "Resource call execution closure differs from its original checked bodies".into(), + ); + } + witness.manifest.validate(types)?; + witness.current(function)?; + if &witness.manifest != manifest + || witness.source.manifest() != Some(manifest) + || !witness + .source + .execution_functions() + .iter() + .any(|original| original_function_equal(original, &witness.original)) + { + return Err( + "Resource call association differs from its original checked function or manifest" + .into(), + ); + } + let current = function + .blocks + .get(block.index() as usize) + .filter(|value| value.id == block) + .ok_or("Resource call association has no exact current block")?; + let mut pointers = 0; + walk::mir_site(current, statement, &mut |value| { + pointers += usize::from(std::ptr::eq(value, expression)) + }); + if pointers != 1 { + return Err( + "Resource call association differs from its exact current expression site".into(), + ); + } + Ok(true) +} +/// The finite connected actual shapes; descriptors and ordinary data remain ordinary. +pub(super) fn original_resource_actual( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> bool { + match types.resolve(ty) { + Type::Resource(_) => manifest.contains_type(ty), + Type::Optional(inner) => { + matches!(types.resolve(*inner), Type::Resource(_)) && manifest.contains_type(*inner) + } + Type::Result(ok, fail) => { + matches!(types.resolve(*ok), Type::Resource(_)) + && manifest.contains_type(*ok) + && !resource_type_pending(types, *fail) + } + _ => false, + } +} + +pub(super) fn validate_witnesses( + program: &Program, + types: &TypeInterner, +) -> Result<(), Vec> { + let mut errors = Vec::new(); + let source = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map(|witness| &witness.source); + let execution = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map(|witness| { + witness.source.validate_types(types)?; + witness.execution.reconstructed(&witness.source, types) + }) + .transpose() + .map_err(|message| { + vec![ValidationError { + span: program + .functions + .first() + .map_or(Span::new(jett_common::FileId::new(0), 0, 0), |function| { + function.span + }), + message, + }] + })? + .unwrap_or_default(); + for function in &program.functions { + let needed = execution.contains(function.id) + || source.is_some_and(|archive| { + archive + .execution_functions() + .iter() + .find(|original| original.id == function.id) + .is_some_and(|original| { + original.identity.declaration.kind + == hir::DeclarationKind::RefinementPredicate + || entry_scope::capture_needed(original, archive) + }) + }) + || source + .is_some_and(|archive| archive.required_only_function_ids().contains(&function.id)) + || source.is_some_and(|archive| { + archive + .required_materializations() + .iter() + .any(|row| row.function() == function.id) + }) + || resource_type_pending(types, function.return_type) + || function + .locals + .iter() + .any(|local| resource_type_pending(types, local.ty)) + || function.blocks.iter().any(|block| { + walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + }); + match &function.resource_lowering { + Some(witness) => { + let result = witness + .source + .validate_types(types) + .and_then(|()| witness.manifest.validate(types)) + .and_then(|()| { + if source != Some(&witness.source) { + return Err("Resource ownership mixes foreign original/materialized archives".into()); + } + if witness.execution != execution { + return Err("Resource execution closure differs from the original checked call graph".into()); + } + if witness.source.manifest() != Some(&witness.manifest) + || !witness.source.execution_functions().iter().any(|original| { + original_function_equal(original, &witness.original) + }) + { + return Err( + "Resource ownership lost its original checked HIR archive join" + .into(), + ); + } + if witness.manifest != program.resource_manifest { + return Err( + "Resource ownership witness belongs to another checked manifest" + .into(), + ); + } + witness.current(function)?; + graph_metadata::validate(witness,function,types)?; + named_callables::validate(witness, function, types)?; + returned_descriptors::validate(witness, function) + }); + if let Err(message) = result { + errors.push(ValidationError { + span: function.span, + message, + }); + } + } + None if needed => errors.push(ValidationError { + span: function.span, + message: + "Resource ownership has no initially authenticated Source constructor witness" + .into(), + }), + None => {} + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Fresh typed/CFG proof. This is not a runtime registration or ABI admission. +pub fn validate_resource_ownership<'p>( + program: &'p Program, + types: &'p TypeInterner, +) -> Result, Vec> { + validate_call_ownership(program, types)?; + validate_witnesses(program, types)?; + let required_only_functions = program + .functions + .iter() + .find_map(|function| function.resource_lowering.as_ref()) + .map_or_else(Vec::new, |witness| { + witness.source.required_only_function_ids().to_vec() + }); + let mut functions = Vec::new(); + let mut errors = Vec::new(); + for function in &program.functions { + if !has_execution_records(function, types) || required_only_functions.contains(&function.id) + { + continue; + } + let analyzed = if native_suites::is_suite(function) { + native_suites::analyze(program, function, types) + } else if carriers::required(function, types) { + carriers::analyze(program, function, types) + } else { + flow::analyze(program, function, types) + }; + match analyzed { + Ok(plan) => functions.push(plan), + Err(message) => errors.push(ValidationError { + span: function.span, + message, + }), + } + } + if errors.is_empty() { + Ok(ResourceOwnershipPlan { + program, + types, + functions, + required_only_functions, + entry_scope: None, + }) + } else { + Err(errors) + } +} + +fn shape( + manifest: &hir::ResourceManifest, + types: &TypeInterner, + ty: TypeId, +) -> Result, String> { + if ty.index() as usize >= types.len() { + return Err("Resource ownership type is outside the exact interner".into()); + } + let kind = |ty| { + manifest.kind_for_type(ty).ok_or_else(|| { + "Resource ownership kind has no original manifest declaration".to_string() + }) + }; + Ok(match types.resolve(ty) { + Type::Resource(_) => Some(ResourceShape::Plain { kind: kind(ty)? }), + Type::Optional(inner) if matches!(types.resolve(*inner), Type::Resource(_)) => Some(ResourceShape::Optional { kind: kind(*inner)? }), + Type::Result(ok, fail) if matches!(types.resolve(*ok), Type::Resource(_)) && !resource_type_pending(types, *fail) => Some(ResourceShape::Result { kind: kind(*ok)?, failure: *fail }), + Type::Function { .. } => None, // A signature is descriptor data, never occupied custody. + _ if resource_type_pending(types, ty) => return Err("pending ResourceOwnershipPlan: occupied Resource layout needs its dedicated aggregate or qualification transport".into()), + _ => None, + }) +} + +/// Exact original/current ordinal membership only; no runtime Resource grant. +pub(super) fn validate_field_update_scope( + function: &Function, + scope: &hir::RequiredScopeIdentity, + binding: &hir::ScopedTypeBinding, +) -> Result<(), String> { + let witness = function + .resource_lowering + .as_ref() + .ok_or("field update reflected scope lacks its original checked ordinal archive")?; + witness.current(function)?; + if witness + .reflected_fields + .iter() + .filter(|row| row.matches_field_update_scope(scope, binding)) + .count() + != 1 + { + return Err( + "field update reflected scope is not its exact original checked ordinal body".into(), + ); + } + Ok(()) +} + +pub(super) fn has_records(function: &Function) -> bool { + function.resource_lowering.is_some() +} +pub(super) fn validate_current(function: &Function) -> Result<(), String> { + function + .resource_lowering + .as_ref() + .map_or(Ok(()), |witness| witness.current(function)) +} + +pub(super) use control_flow::sequence_transition; + +/// Readonly selection from the protected original graph and exact current +/// occurrence/header. None of these queries constructs invocation or custody. +pub fn native_graph_storage_has_records(function: &Function, types: &TypeInterner) -> bool { + function + .resource_lowering + .as_ref() + .is_some_and(|parent| graph_metadata::has_records(parent, function, types)) +} +pub fn native_graph_storage_for_expression( + function: &Function, + types: &TypeInterner, + value: &Expression, +) -> Option { + graph_metadata::storage_for_expression( + function.resource_lowering.as_ref()?, + function, + types, + value, + ) +} +pub fn native_graph_storage_for_constructor( + function: &Function, + types: &TypeInterner, + node: &ResourceConstructorNode, +) -> Option { + graph_metadata::storage_for_constructor( + function.resource_lowering.as_ref()?, + function, + types, + node, + ) +} +pub fn native_graph_storage_for_local( + function: &Function, + types: &TypeInterner, + local: &Local, +) -> Option { + graph_metadata::storage_for_local(function.resource_lowering.as_ref()?, function, types, local) +} +pub fn native_graph_storage_for_formal( + function: &Function, + types: &TypeInterner, + parameter: &Param, +) -> Option { + graph_metadata::storage_for_formal( + function.resource_lowering.as_ref()?, + function, + types, + parameter, + ) +} +pub fn native_graph_storage_for_return( + function: &Function, + types: &TypeInterner, +) -> Option { + graph_metadata::storage_for_return(function.resource_lowering.as_ref()?, function, types) +} +pub fn native_graph_hook_projection( + function: &Function, + types: &TypeInterner, + value: &Expression, +) -> Option { + graph_metadata::projection_for_expression( + function.resource_lowering.as_ref()?, + function, + types, + value, + ) +} +pub fn native_graph_source_edge( + function: &Function, + types: &TypeInterner, + value: &Expression, + parameter: usize, +) -> Option { + graph_metadata::source_edge( + function.resource_lowering.as_ref()?, + function, + types, + value, + parameter, + ) +} + +/// Only the canonical unreachable-block deletion supplies this exact map. +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + let witness = function + .resource_lowering + .as_mut() + .ok_or("Resource canonical remap lost its constructor witness")?; + if !witness.same(original) { + return Err("Resource canonical remap changed its before-witness".into()); + } + witness.blocks.retain(|block| { + map.get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + let remap = |id: &mut BlockId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } + }; + for region in &mut witness.regions { + region.remap_blocks(map)?; + } + for constructor in &mut witness.constructors { + constructor.remap_blocks(map)?; + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for constructor in &mut constructor_seal { + constructor.remap_blocks(map)?; + } + witness.constructor_seal = constructor_seal.into(); + borrowed_sums::remap_blocks(&mut witness.borrowed_sums, map)?; + borrowed_sums::seal(witness); + lexical_borrows::remap_blocks(witness, map)?; + reflected_fields::remap_blocks(witness, map)?; + returned_descriptors::remap_blocks(&mut witness.descriptors, map)?; + graph_metadata::remap_blocks(&mut witness.graph_metadata, map)?; + remap(&mut witness.entry); + for block in &mut witness.blocks { + remap(&mut block.id); + crate::sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource canonical remap lost its witness")? + .current(function) +} + +pub(super) fn remap_locals( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(original) = before.resource_lowering.as_ref() else { + return Ok(()); + }; + original.current(before)?; + let witness = function + .resource_lowering + .as_mut() + .ok_or("Resource canonical local remap lost its constructor witness")?; + if !witness.same(original) { + return Err("Resource canonical local remap changed its before-witness".into()); + } + // Source IDs in original are archival. Only the independently captured current graph is remapped. + let mut valid = true; + let mut remap = |id: &mut LocalId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } else { + valid = false; + } + }; + for parameter in &mut witness.parameters { + remap(&mut parameter.local); + } + witness.locals.retain(|local| { + map.get(local.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for local in &mut witness.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + for block in &mut witness.blocks { + crate::sequences::prune::block_locals(block, &mut remap, &mut |_| {}); + } + for region in &mut witness.regions { + region.remap_locals(&mut remap); + } + for constructor in &mut witness.constructors { + constructor.remap_locals(&mut remap); + } + let mut constructor_seal = witness.constructor_seal.to_vec(); + for constructor in &mut constructor_seal { + constructor.remap_locals(&mut remap); + } + witness.constructor_seal = constructor_seal.into(); + borrowed_sums::remap_locals(&mut witness.borrowed_sums, &mut remap); + borrowed_sums::seal(witness); + lexical_borrows::remap_locals(witness, &mut remap); + named_callables::remap_locals(&mut witness.named_callables, &mut remap); + reflected_fields::remap_locals(witness, &mut remap); + returned_descriptors::remap_locals(&mut witness.descriptors, map, &mut remap); + graph_metadata::remap_locals(&mut witness.graph_metadata, map, &mut remap); + for record in &mut witness.calls { + let mut block = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(record.current.clone()), + span: record.current.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: record.current.span, + }, + }; + crate::sequences::prune::block_locals(&mut block, &mut remap, &mut |_| {}); + let StatementKind::Evaluate(value) = block.statements.remove(0).kind else { + return Err("Resource association metadata remap changed its wrapper".into()); + }; + record.current = value; + } + if !valid { + return Err("Resource canonical local remap lost an active current reference".into()); + } + function + .resource_lowering + .as_ref() + .ok_or("Resource canonical remap lost its witness")? + .current(function) +} diff --git a/crates/jett_mir/src/resource_ownership/named_callables.rs b/crates/jett_mir/src/resource_ownership/named_callables.rs new file mode 100644 index 00000000..fda1805b --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/named_callables.rs @@ -0,0 +1,451 @@ +//! Immutable named callable provenance, derived from the whole checked archive. +//! Descriptor storage stays ordinary; this proof only selects a Source body. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResourceNamedCallableProof { + function: FunctionId, + signature_type: TypeId, + identity: FunctionIdentity, +} +impl ResourceNamedCallableProof { + pub fn function(&self) -> FunctionId { + self.function + } + pub fn signature_type(&self) -> TypeId { + self.signature_type + } + pub fn identity(&self) -> &FunctionIdentity { + &self.identity + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct NamedCallableBinding { + original_header: Local, + original_value: Expression, + current_header: Local, + current_value: Expression, + parent: Option, + root: usize, + proof: ResourceNamedCallableProof, +} + +fn alias(value: &Expression) -> Option { + match &value.kind { + E::Local(local) => Some(*local), + E::Clone(inner) if inner.ty == value.ty && inner.span == value.span => alias(inner), + _ => None, + } +} + +fn target( + archive: &hir::ResourceSourceArchive, + function: FunctionId, + signature_type: TypeId, + types: &TypeInterner, +) -> Option { + let declaration = archive.functions().get(function.index() as usize)?; + let Type::Function { + params, + view_params, + return_type, + } = types.resolve(signature_type) + else { + return None; + }; + if declaration.id != function + || declaration.source_definition.is_none() + || !matches!(declaration.debug_kind, hir::FunctionDebugKind::Named(_)) + || declaration.identity.declaration.kind != hir::DeclarationKind::Function + || !declaration.identity.type_arguments.is_empty() + || !declaration.identity.scoped_type_bindings.is_empty() + || declaration.identity.specialization != Default::default() + || declaration.capture_count != 0 + || declaration.params.len() != params.len() + || view_params.len() != params.len() + || declaration.return_type != *return_type + || declaration + .params + .iter() + .zip(params) + .zip(view_params) + .any(|((parameter, ty), view)| { + parameter.ty != *ty || (parameter.mode == ParamMode::View) != *view + }) + { + return None; + } + Some(ResourceNamedCallableProof { + function, + signature_type, + identity: declaration.identity.clone(), + }) +} + +fn original_callee(value: &Expression) -> Option<&Expression> { + let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + else { + return None; + }; + let hir::CallTarget::Indirect { signature_type } = source.target else { + return None; + }; + (callee.ty == signature_type + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && source + .arguments + .iter() + .all(|argument| argument.staging == hir::ArgumentStaging::Original)) + .then_some(callee) +} + +/// The first supported producer is an original root-scope immutable Let. +/// Unsupported producers/escapes retain the existing pending ABI refusal. +pub(super) fn capture( + original: &hir::Function, + archive: &hir::ResourceSourceArchive, + types: &TypeInterner, + execution: &ResourceExecutionClosure, +) -> Vec { + let mut bindings: Vec = Vec::new(); + let mut permitted = BTreeSet::new(); + for statement in &original.body.statements { + let hir::StatementKind::Let { local, value } = &statement.kind else { + continue; + }; + let Some(header) = original + .locals + .get(local.index() as usize) + .filter(|header| { + header.id == *local + && !header.mutable + && header.view_source.is_none() + && header.ty == value.ty + }) + else { + continue; + }; + let (proof, parent, root) = if let E::FunctionRef(function) = value.kind { + let Some(proof) = target(archive, function, value.ty, types) else { + continue; + }; + (proof, None, bindings.len()) + } else if let Some(source) = alias(value) { + let Some((index, parent)) = bindings + .iter() + .enumerate() + .find(|(_, binding)| binding.original_header.id == source) + else { + continue; + }; + if parent.proof.signature_type != value.ty { + continue; + } + walk::expression(value, &mut |leaf| { + if matches!(leaf.kind, E::Local(id) if id == source) { + permitted.insert(std::ptr::from_ref(leaf).addr()); + } + }); + (parent.proof.clone(), Some(index), parent.root) + } else { + continue; + }; + bindings.push(NamedCallableBinding { + original_header: header.clone(), + original_value: value.clone(), + current_header: header.clone(), + current_value: value.clone(), + parent, + root, + proof, + }); + } + let mut reached = BTreeSet::new(); + walk::hir_block(&original.body, &mut |value| { + if let Some(callee) = original_callee(value) + && let E::Local(local) = callee.kind + && let Some(binding) = bindings + .iter() + .find(|binding| binding.original_header.id == local) + && binding.proof.signature_type == callee.ty + && source_custody_call(value, types, execution) + { + permitted.insert(std::ptr::from_ref(callee).addr()); + reached.insert(binding.root); + } + }); + let mut escaped = BTreeSet::new(); + metadata_escapes(&original.body, &bindings, &mut escaped); + walk::hir_block(&original.body, &mut |value| { + for binding in &bindings { + let escapes = match &value.kind { + E::Local(local) => { + *local == binding.original_header.id + && !permitted.contains(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.original_header.id), + _ => false, + }; + if escapes { + escaped.insert(binding.root); + } + } + }); + // Remap only private binding indexes. Archival Local/Function IDs stay exact. + let mut map = vec![None; bindings.len()]; + let mut next = 0; + for (index, binding) in bindings.iter().enumerate() { + if reached.contains(&binding.root) && !escaped.contains(&binding.root) { + map[index] = Some(next); + next += 1; + } + } + bindings + .into_iter() + .filter_map(|mut binding| { + binding.root = map[binding.root]?; + binding.parent = binding.parent.and_then(|parent| map[parent]); + Some(binding) + }) + .collect() +} + +fn header_same(original: &Local, current: &Local) -> bool { + let mut restored = current.clone(); + restored.id = original.id; + restored == *original +} + +pub(super) fn validate( + witness: &ResourceLoweringWitness, + function: &Function, + types: &TypeInterner, +) -> Result<(), String> { + let expected = capture( + &witness.original, + &witness.source, + types, + &witness.execution, + ); + if expected.len() != witness.named_callables.len() { + return Err("Resource named callable lost its original producer/use proof".into()); + } + for (binding, expected) in witness.named_callables.iter().zip(&expected) { + if binding.original_header != expected.original_header + || !crate::breakpoint_regions::expressions_equal( + &binding.original_value, + &expected.original_value, + ) + || binding.parent != expected.parent + || binding.root != expected.root + || binding.proof != expected.proof + || !header_same(&binding.original_header, &binding.current_header) + || function.local(binding.current_header.id) != Some(&binding.current_header) + { + return Err("Resource named callable differs from its exact archived declaration or immutable binding".into()); + } + let mut restored = binding.current_value.clone(); + let mut valid = true; + remap_expression(&mut restored, &mut |local| { + if let Some(original) = witness + .named_callables + .iter() + .find(|candidate| candidate.current_header.id == *local) + { + *local = original.original_header.id; + } else { + valid = false; + } + }); + if !valid + || !crate::breakpoint_regions::expressions_equal(&binding.original_value, &restored) + { + return Err("Resource named callable changed its exact original initializer".into()); + } + let count = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == binding.current_header.id + && crate::breakpoint_regions::expressions_equal(value, &binding.current_value)) + }) + .count(); + if count != 1 { + return Err("Resource named callable has no unique current immutable producer".into()); + } + } + current_values(witness, function)?; + Ok(()) +} + +fn metadata_escapes( + block: &hir::Block, + bindings: &[NamedCallableBinding], + escaped: &mut BTreeSet, +) { + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + hir::StatementKind::Trace(local) => std::slice::from_ref(local), + hir::StatementKind::Breakpoint { bindings, .. } => bindings, + _ => &[], + }; + for binding in bindings { + if locals.contains(&binding.original_header.id) { + escaped.insert(binding.root); + } + } + match &statement.kind { + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + metadata_escapes(then_block, bindings, escaped); + if let Some(block) = else_block { + metadata_escapes(block, bindings, escaped); + } + } + hir::StatementKind::While { body, .. } + | hir::StatementKind::For { body, .. } + | hir::StatementKind::Scope(body) => metadata_escapes(body, bindings, escaped), + hir::StatementKind::Match { arms, .. } => { + for arm in arms { + metadata_escapes(&arm.body, bindings, escaped); + } + } + hir::StatementKind::ReflectedTypeDispatch { arms, .. } => { + for arm in arms { + metadata_escapes(&arm.body, bindings, escaped); + } + } + _ => {} + } + } +} + +/// Original aliases/callees are rejoined to exact current occurrences, independently +/// of the constructor graph equality check. A copied/resealed graph adds no use role. +pub(super) fn current_values( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result, String> { + let mut values = BTreeMap::new(); + for binding in &witness.named_callables { + for statement in function.blocks.iter().flat_map(|block| &block.statements) { + if let StatementKind::Let { local, value } = &statement.kind + && *local == binding.current_header.id + && crate::breakpoint_regions::expressions_equal(value, &binding.current_value) + { + walk::expression(value, &mut |value| { + values.insert(std::ptr::from_ref(value).addr(), binding.proof.clone()); + }); + } + } + } + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if let E::IndirectCall { callee, .. } = &value.kind + && let E::Local(local) = callee.kind + && let Some(binding) = witness.named_callables.iter().find(|binding| binding.current_header.id == local) + && witness.calls.iter().any(|record| { + crate::breakpoint_regions::expressions_equal(&record.current, value) + && matches!(&record.original.kind, E::IndirectCall { callee, .. } if binding.original_callee_matches(callee)) + }) + { + values.insert(std::ptr::from_ref(callee.as_ref()).addr(), binding.proof.clone()); + } + }); + } + let mut escaped = false; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + for binding in &witness.named_callables { + escaped |= match &value.kind { + E::Local(local) => { + *local == binding.current_header.id + && !values.contains_key(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.current_header.id), + _ => false, + }; + } + }); + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + StatementKind::Trace(local) => std::slice::from_ref(local), + StatementKind::Breakpoint { bindings, .. } => bindings, + _ => &[], + }; + escaped |= witness + .named_callables + .iter() + .any(|binding| locals.contains(&binding.current_header.id)); + } + } + if escaped { + return Err( + "Resource named callable has an unproved current escape or callee occurrence".into(), + ); + } + Ok(values.into_iter().collect()) +} + +pub(super) fn remap_locals( + bindings: &mut [NamedCallableBinding], + remap: &mut impl FnMut(&mut LocalId), +) { + for binding in bindings { + remap(&mut binding.current_header.id); + remap_expression(&mut binding.current_value, remap); + } +} + +fn remap_expression(value: &mut Expression, remap: &mut impl FnMut(&mut LocalId)) { + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(value.clone()), + span: value.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(remapped) = wrapper.statements.remove(0).kind { + *value = remapped; + } +} + +impl NamedCallableBinding { + pub(super) fn original_callee_matches(&self, callee: &Expression) -> bool { + callee.ty == self.proof.signature_type + && matches!(callee.kind, E::Local(local) if local == self.original_header.id) + } + pub(super) fn local(&self) -> LocalId { + self.current_header.id + } + pub(super) fn parent(&self) -> Option { + self.parent + } + pub(super) fn initializer(&self) -> &Expression { + &self.current_value + } + pub(super) fn proof(&self) -> &ResourceNamedCallableProof { + &self.proof + } +} + +#[cfg(test)] +#[path = "named_callables_tests.rs"] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/named_callables_tests.rs b/crates/jett_mir/src/resource_ownership/named_callables_tests.rs new file mode 100644 index 00000000..4f293dbe --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/named_callables_tests.rs @@ -0,0 +1,337 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCE: &str = r#"namespace app +function wrong_target(token: resource_probe.TestHandle) returns nothing: + return nothing +function close_selected(token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = close_selected + resource_probe.TestHandle token = resource_probe.create(view net, 701) handle error: + return nothing + dispose(token) + return nothing +"#; + +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn scenario_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn producer(function: &Function) -> &Expression { + let signature = function.resource_lowering.as_ref().unwrap().named_callables[0] + .proof + .signature_type(); + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { value, .. } + if value.ty == signature && matches!(value.kind, E::FunctionRef(_)) => + { + Some(value) + } + _ => None, + }) + .unwrap() +} +fn indirect(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(value) if matches!(value.kind, E::IndirectCall { .. }) => { + Some(value) + } + _ => None, + }) + .unwrap() +} +fn initializer_mut(function: &mut Function) -> &mut Expression { + function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find_map(|statement| match &mut statement.kind { + StatementKind::Let { value, .. } if matches!(value.kind, E::FunctionRef(_)) => { + Some(value) + } + _ => None, + }) + .unwrap() +} + +#[test] +fn resource_named_indirect_keeps_original_source_and_exact_producer_use() { + for release in [false, true] { + for selected in ["close_selected", "wrong_target"] { + let source = + SOURCE.replace("dispose = close_selected", &format!("dispose = {selected}")); + let original = checked(&source, release); + let types = &original.checked().interner; + let program = lowered(&original); + let function = scenario(&program); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let descriptor = producer(function); + let proof = plan.named_callable_producer(descriptor).unwrap(); + assert_eq!(proof.identity().declaration.name, selected); + assert_eq!( + proof.function(), + match descriptor.kind { + E::FunctionRef(id) => id, + _ => unreachable!(), + } + ); + assert_eq!(proof.signature_type(), descriptor.ty); + assert!(plan.named_callable_producer(&descriptor.clone()).is_none()); + assert_eq!(plan.named_callable_value(descriptor), Some(proof)); + assert!(plan.named_callable_value(&descriptor.clone()).is_none()); + let call = indirect(function); + let operation = plan + .operations_for_expression(call) + .find(|operation| operation.named_indirect_target().is_some()) + .unwrap(); + assert_eq!(operation.named_indirect_target(), Some(proof)); + let ResourceOperationRole::InvokeSourceFunction { + function: target, + source, + evaluation_order, + .. + } = operation.role() + else { + panic!("selected Source operation"); + }; + assert_eq!(*target, proof.function()); + assert_eq!( + source.target, + hir::CallTarget::Indirect { + signature_type: proof.signature_type() + } + ); + assert_eq!(source.bridge, hir::CallBridge::Direct); + assert_eq!(evaluation_order, &[0]); + assert!( + plan.operations_for_expression(&call.clone()) + .next() + .is_none() + ); + let E::IndirectCall { callee, .. } = &call.kind else { + unreachable!() + }; + assert_eq!(plan.named_callable_value(callee), Some(proof)); + assert!( + plan.named_callable_value(&callee.as_ref().clone()) + .is_none() + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + assert!(companion.storage().temporary_slots >= 2); + for binding in &function.resource_lowering.as_ref().unwrap().named_callables { + assert!( + companion + .storage() + .owned_locals + .contains(&(binding.local().index() as usize)) + ); + } + } + } +} + +#[test] +fn resource_named_indirect_retains_immutable_alias_chain_and_canonical_local_maps() { + let source = SOURCE.replace( + " function(resource_probe.TestHandle) returns nothing dispose = close_selected", + " function(int64) returns int64 unused_callback = function(ignored: int64) returns int64: return ignored\n function(resource_probe.TestHandle) returns nothing selected = close_selected\n function(resource_probe.TestHandle) returns nothing dispose = selected", + ); + for release in [false, true] { + let original = checked(&source, release); + let types = &original.checked().interner; + let mut program = lowered(&original); + let before = scenario(&program) + .resource_lowering + .as_ref() + .unwrap() + .named_callables + .clone(); + assert_eq!(before.len(), 2); + crate::prepare_native_generated_functions(&mut program, types); + let function = scenario(&program); + let after = &function.resource_lowering.as_ref().unwrap().named_callables; + assert_eq!(after[0].original_header, before[0].original_header); + assert_eq!(after[1].original_header, before[1].original_header); + assert!(after[0].current_header.id.index() < before[0].current_header.id.index()); + assert!(after[1].current_header.id.index() < before[1].current_header.id.index()); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let proof = plan.named_callable_producer(producer(function)).unwrap(); + assert_eq!(proof.identity().declaration.name, "close_selected"); + assert_eq!( + plan.operations_for_expression(indirect(function)) + .filter(|operation| operation.named_indirect_target() == Some(proof)) + .count(), + 1 + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for binding in after { + assert!( + companion + .storage() + .owned_locals + .contains(&(binding.local().index() as usize)) + ); + } + } +} + +#[test] +fn resource_named_indirect_rejects_target_initializer_binding_and_reseal_swaps() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let program = lowered(&original); + let wrong = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "wrong_target") + .unwrap() + .id; + for mutation in 0..7 { + let mut changed = program.clone(); + let function = scenario_mut(&mut changed); + match mutation { + 0 => initializer_mut(function).kind = E::FunctionRef(wrong), + 1 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .proof + .function = wrong + } + 2 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .current_header + .mutable = true + } + 3 => { + function.resource_lowering.as_mut().unwrap().named_callables[0] + .current_header + .id = function.params[0].local + } + 4 | 5 => { + initializer_mut(function).kind = E::FunctionRef(wrong); + let blocks = function.blocks.clone(); + let changed_initializer = producer(function).clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = blocks; + if mutation == 5 { + witness.named_callables[0].current_value = changed_initializer; + } + } + 6 => { + let binding = function.resource_lowering.as_ref().unwrap().named_callables[0] + .current_header + .clone(); + function.blocks[0].statements.push(Statement { + kind: StatementKind::Evaluate(Expression { + kind: E::Local(binding.id), + ty: binding.ty, + span: binding.span, + }), + span: binding.span, + }); + let blocks = function.blocks.clone(); + function.resource_lowering.as_mut().unwrap().blocks = blocks; + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + let mut changed_hir = hir::lower_checked_resource_program(&original).unwrap(); + let caller = changed_hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let initializer = caller + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Let { value, .. } + if matches!(value.kind, E::FunctionRef(_)) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + initializer.kind = E::FunctionRef(wrong); + assert!(lower(&changed_hir, types).is_err()); + } +} + +#[test] +fn resource_named_indirect_does_not_mint_for_mutable_or_escaped_original_binding() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mut hir = hir::lower_checked_resource_program(&original).unwrap(); + let execution = authenticate_original(&hir, types).unwrap(); + let caller = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let binding = caller + .locals + .iter_mut() + .find(|local| local.name == "dispose") + .unwrap(); + binding.mutable = true; + assert!(capture(caller, &hir.resource_source, types, &execution).is_empty()); + let mut hir = hir::lower_checked_resource_program(&original).unwrap(); + let caller = hir + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let binding = caller + .locals + .iter() + .find(|local| local.name == "dispose") + .unwrap() + .clone(); + caller.body.statements.push(hir::Statement { + kind: hir::StatementKind::Return(Some(Expression { + kind: E::Local(binding.id), + ty: binding.ty, + span: binding.span, + })), + span: binding.span, + }); + assert!(capture(caller, &hir.resource_source, types, &execution).is_empty()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/native_suites.rs b/crates/jett_mir/src/resource_ownership/native_suites.rs new file mode 100644 index 00000000..0f321981 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/native_suites.rs @@ -0,0 +1,313 @@ +//! Private canonical suite calls activate checked bodies without inventing a +//! Source call certificate. Original helper calls keep their Source authority. +use super::*; + +fn canonical(witness: &ResourceLoweringWitness) -> Result, String> { + let Some(function) = witness.source.native_suite_function(witness.original.id) else { + return Ok(None); + }; + if !original_function_equal(function, &witness.original) { + return Err("native suite differs from its private complete HIR table".into()); + } + Ok(Some(function)) +} + +pub(super) fn is_suite(function: &Function) -> bool { + function + .resource_lowering + .as_ref() + .is_some_and(|witness| witness.source.native_suite_function(function.id).is_some()) +} + +/// Preserve the canonical expression for later positional replay. This grants +/// no invocation authority; analyze independently joins each exact occurrence. +pub(super) fn preserve( + witness: &ResourceLoweringWitness, + expression: &Expression, +) -> Result { + if canonical(witness)?.is_none() { + return Ok(false); + } + Ok(matches!(&expression.kind, + hir::ExpressionKind::Call { + function, + ownership: hir::CallOwnership::Generated(generated), + .. + } if matches!(&generated.operation, + hir::GeneratedOperation::NativeSuite { function: expected } if expected == function))) +} + +fn call_metadata( + program: &Program, + function: &Function, + expression: &Expression, + proof: &hir::CheckedNativeSuiteCall, + types: &TypeInterner, +) -> Result { + let hir::ExpressionKind::Call { + function: target, + args, + evaluation_order, + ownership: hir::CallOwnership::Generated(generated), + } = &expression.kind + else { + return Err("native suite receipt lost its exact generated Call".into()); + }; + let callee = program + .functions + .get(target.index() as usize) + .filter(|callee| callee.id == *target) + .ok_or("native suite call lost its exact checked declaration")?; + if proof.owner() != function.id + || proof.target() != *target + || proof.evaluation_order() != evaluation_order + || !proof.matches(function.id, expression, proof.expression_ordinal()) + || !matches!(&generated.operation, + hir::GeneratedOperation::NativeSuite { function } if function == target) + || expression.ty != TypeInterner::NOTHING + || callee.return_type != TypeInterner::NOTHING + || callee.capture_count != 0 + || !matches!( + callee.identity.declaration.kind, + hir::DeclarationKind::Verify | hir::DeclarationKind::Property + ) + || args.len() != callee.params.len() + || args.len() != generated.arguments.len() + || args.len() != evaluation_order.len() + { + return Err("native suite call changed its sealed target or full tuple".into()); + } + let mut seen = vec![false; args.len()]; + for ¶meter in evaluation_order { + let visited = seen + .get_mut(parameter) + .ok_or("native suite argument order is outside its exact tuple")?; + if *visited { + return Err("native suite argument order repeats a parameter".into()); + } + *visited = true; + } + let mut formals = Vec::with_capacity(args.len()); + let mut operands = Vec::with_capacity(args.len()); + for (parameter, (argument, formal)) in args.iter().zip(&callee.params).enumerate() { + let source_index = evaluation_order + .iter() + .position(|actual| *actual == parameter) + .ok_or("native suite argument order omitted a parameter")?; + let fact = &generated.arguments[parameter]; + if argument.ty != formal.ty + || fact.actual_type != argument.ty + || fact.parameter_type != formal.ty + || fact.callee_access != jett_typecheck::CheckedCalleeAccess::Owned + || formal.mode != ParamMode::Owned + || fact.acquisition != hir::GeneratedAcquisition::Copy + || resource_type_pending(types, argument.ty) + || !jett_typecheck::ownership::is_implicitly_copyable(types, argument.ty) + { + return Err("pending native Resource suite: generated inputs require exact ordinary copy formals".into()); + } + formals.push(ResourceCallFormal { + parameter, + source_index, + actual_type: argument.ty, + parameter_type: formal.ty, + syntax: ResourceArgumentSyntax::Bare, + effect: ResourceArgumentEffect::Copy, + access: ParamMode::Owned, + }); + operands.push(ResourceCallOperand::Ordinary { + parameter, + ty: argument.ty, + }); + } + Ok(ResourceNativeSuiteInvocation { + function: *target, + proof: proof.clone(), + formals, + evaluation_order: evaluation_order.clone(), + operands, + result: ResourceCallResult::Ordinary { + ty: TypeInterner::NOTHING, + }, + }) +} + +fn operation( + plan: &mut ResourceFunctionPlan, + frame: ResourceFrameId, + site: ResourceSite, + expression: Option<&Expression>, + role: ResourceOperationRole, +) { + plan.operations.push(ResourceOperation { + id: ResourceOperationId(plan.operations.len()), + frame, + site, + ordinal: usize::from(expression.is_some()), + role, + expression: expression.map(|value| std::ptr::from_ref(value).addr()), + named_indirect: None, + indirect_hook: None, + }); +} + +fn empty_plan(function: &Function, terminal: ResourceSite) -> ResourceFunctionPlan { + ResourceFunctionPlan { + function: function.id, + identity: function.identity.clone(), + parameters: function.params.clone(), + return_type: function.return_type, + frames: vec![ResourceFrame { + id: ResourceFrameId(0), + role: ResourceFrameRole::Scope, + site: terminal, + parent: None, + ordinal: 0, + }], + carriers: None, + slots: Vec::new(), + loans: Vec::new(), + operations: Vec::new(), + execution_frames: Vec::new(), + lexical_exits: Vec::new(), + named_callable_producers: Vec::new(), + named_callable_values: Vec::new(), + descriptor_return: None, + descriptor_values: Vec::new(), + descriptor_locals: Vec::new(), + graph_descriptors: Vec::new(), + } +} + +/// A factory wrapper is currently a flat, closed sequence of case contexts and +/// calls with ordinary copy inputs. Exact statement positions distinguish even +/// identical trial values; neither spans nor structural first-match selects one. +pub(super) fn analyze( + program: &Program, + function: &Function, + types: &TypeInterner, +) -> Result { + let witness = function + .resource_lowering + .as_ref() + .ok_or("native suite has no constructor witness")?; + witness.current(function)?; + let original = canonical(witness)?.ok_or("native suite has no private factory origin")?; + if !function.params.is_empty() + || function.capture_count != 0 + || function.return_type != TypeInterner::NOTHING + || !function.locals.is_empty() + || function.blocks.len() != 1 + { + return Err( + "pending native Resource suite: wrapper requires its closed flat original scope".into(), + ); + } + let block = &function.blocks[0]; + if block.id != function.entry + || !matches!(block.terminator.kind, TerminatorKind::Return(None)) + || block.statements.len() != original.body.statements.len() + { + return Err("native suite lost its exact flat statement/return association".into()); + } + ControlFlowGraph::analyze(function) + .map_err(|errors| format!("native suite CFG is malformed: {errors:?}"))?; + let terminal = ResourceSite { + function: function.id, + block: block.id, + position: ResourcePosition::Terminator, + }; + let mut plan = empty_plan(function, terminal); + let mut global_ordinal = 0; + for (index, (archived, current)) in original + .body + .statements + .iter() + .zip(&block.statements) + .enumerate() + { + let (hir::StatementKind::Expression(original), StatementKind::Evaluate(expression)) = + (&archived.kind, ¤t.kind) + else { + return Err("pending native Resource suite: wrapper statement needs its exact expression transport".into()); + }; + if archived.span != current.span + || !crate::breakpoint_regions::expressions_equal(original, expression) + { + return Err( + "native suite changed an exact original/current statement occurrence".into(), + ); + } + let ordinal = global_ordinal + 1; + walk::expression(original, &mut |_| global_ordinal += 1); + let site = ResourceSite { + function: function.id, + block: block.id, + position: ResourcePosition::Statement(index), + }; + plan.execution_frames.push((site, ResourceFrameId(0))); + match &expression.kind { + hir::ExpressionKind::PropertyCaseContext(_) => {} + hir::ExpressionKind::Call { .. } => { + let proof = witness + .source + .native_suite_call(function.id, original, ordinal) + .ok_or("native suite occurrence has no exact private call receipt")?; + if !proof.belongs_to_archive(&witness.source) + || !proof.matches(function.id, expression, ordinal) + { + return Err( + "native suite occurrence belongs to a different checked plan".into(), + ); + } + let invocation = call_metadata(program, function, expression, proof, types)?; + let callee = &program.functions[invocation.function.index() as usize]; + if !has_execution_records(callee, types) { + // Ordinary targets keep their existing generated-call ABI. + continue; + } + let frame = ResourceFrameId(plan.frames.len()); + plan.frames.push(ResourceFrame { + id: frame, + role: ResourceFrameRole::Operation, + site, + parent: Some(ResourceFrameId(0)), + ordinal: 1, + }); + operation( + &mut plan, + frame, + site, + Some(expression), + ResourceOperationRole::InvokeNativeSuite { + invocation: Box::new(invocation), + }, + ); + operation( + &mut plan, + frame, + site, + Some(expression), + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Normal, + }, + ); + } + _ => return Err("native suite contains a noncanonical factory expression".into()), + } + } + plan.execution_frames.push((terminal, ResourceFrameId(0))); + operation( + &mut plan, + ResourceFrameId(0), + terminal, + None, + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Return, + }, + ); + Ok(plan) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/native_suites/tests.rs b/crates/jett_mir/src/resource_ownership/native_suites/tests.rs new file mode 100644 index 00000000..41f39e8c --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/native_suites/tests.rs @@ -0,0 +1,183 @@ +use super::*; +use crate::resource_ownership::tests::checked; +use jett_common::FileId; +use jett_comptime::checked_types::CheckedExpressionTypes; +use std::sync::Arc; + +const SOURCE: &str = r#"namespace app +function absent() returns list[optional[resource_probe.TestHandle]]: + return list(none, none) +function probe() returns int64: + list[optional[resource_probe.TestHandle]] values = absent() + return 17 +int64 baked = comptime probe() +verify absent_values: + assert probe() == baked +property repeated_values: + given marker: bool + assert probe() == baked + assert marker == marker +"#; + +fn fixture( + property: bool, + release: bool, +) -> ( + Program, + Arc, + FunctionId, + usize, +) { + let checked = checked(SOURCE, release); + let evaluation = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + checked.checked().reflection_metadata.clone(), + Arc::new(CheckedExpressionTypes::from_checked_program(&checked)), + Arc::new(checked.checked().breakpoint_exclusions.clone()), + ); + assert!( + evaluation.diagnostics.is_empty(), + "{:?}", + evaluation.diagnostics + ); + let values = Arc::new(evaluation.values); + let mut program = hir::lower_checked_resource_program_with_test_bodies(&checked).unwrap(); + let types = &checked.checked().interner; + hir::materialize_checked_required_values(&mut program, &values, types).unwrap(); + let (entry, expected) = if property { + let cases = jett_comptime::verify::collect_checked_property_cases_with_checked_values( + checked.clone(), + values, + ) + .unwrap(); + let plan = hir::append_checked_native_property_suite( + &mut program, + &checked, + FileId::new(0), + &cases, + ) + .unwrap() + .unwrap(); + (plan.entry(), plan.trials().len()) + } else { + let entry = hir::append_checked_native_verify_suite(&mut program, &checked, FileId::new(0)) + .unwrap() + .unwrap(); + (entry, 1) + }; + hir::complete_value_conversions(&mut program, types).unwrap(); + ( + crate::lower(&program, types).unwrap(), + checked, + entry, + expected, + ) +} + +fn assert_plan(program: &Program, types: &TypeInterner, entry: FunctionId, expected: usize) { + let plan = validate_resource_ownership_for_entry(program, types, entry).unwrap(); + assert!(plan.entry_scope().is_none()); + assert!(plan.original_source(entry).is_none()); + assert!(plan.authenticated_entry(entry).is_some()); + let suite = plan.function(entry).unwrap(); + assert!(suite.parameters().is_empty()); + assert!(suite.owner_slots().is_empty()); + assert!(suite.loans().is_empty()); + assert!(suite.provisional_return().is_none()); + let invocations = suite + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::InvokeNativeSuite { invocation } => Some(invocation), + ResourceOperationRole::Complete { .. } => None, + other => panic!("unexpected suite custody authority: {other:?}"), + }) + .collect::>(); + assert_eq!(invocations.len(), expected); + for (index, invocation) in invocations.iter().enumerate() { + assert_eq!(invocation.proof().owner(), entry); + assert_eq!(invocation.proof().target(), invocation.function()); + assert!( + invocation.proof().belongs_to_archive( + &program.functions[entry.index() as usize] + .resource_lowering + .as_ref() + .unwrap() + .source, + ) + ); + assert!(invocations[..index].iter().all(|earlier| { + earlier.proof().expression_ordinal() < invocation.proof().expression_ordinal() + && !earlier.proof().same_receipt(invocation.proof()) + })); + assert!( + invocation + .operands() + .iter() + .all(|operand| matches!(operand, ResourceCallOperand::Ordinary { .. })) + ); + assert!( + invocation + .formals() + .iter() + .all(|formal| formal.effect() == ResourceArgumentEffect::Copy) + ); + } + for function in plan + .functions() + .iter() + .filter(|function| function.identity().declaration.namespace == "app") + { + assert!(!function.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::Acquire { .. } | ResourceOperationRole::InvokeHook { .. } + ))); + } +} + +#[test] +fn canonical_absent_resource_suites_keep_exact_occurrences_without_provider_inputs() { + for release in [false, true] { + for property in [false, true] { + let (mut program, checked, entry, expected) = fixture(property, release); + let types = &checked.checked().interner; + assert_plan(&program, types, entry, expected); + crate::prepare_native_sequences(&mut program, types); + crate::prepare_native_uninhabited_sums(&mut program, types); + crate::prepare_native_generated_functions(&mut program, types); + assert_plan(&program, types, entry, expected); + } + } +} + +#[test] +fn suite_context_removal_and_raw_generated_wrappers_cannot_recreate_factory_authority() { + let (program, checked, entry, expected) = fixture(true, false); + let types = &checked.checked().interner; + assert_plan(&program, types, entry, expected); + let mut erased = program.clone(); + let wrapper = &mut erased.functions[entry.index() as usize]; + wrapper.blocks[0].statements.remove(0); + assert!(validate_resource_ownership_for_entry(&erased, types, entry).is_err()); + let mut raw = program.clone(); + raw.functions[entry.index() as usize].resource_lowering = None; + assert!(validate_resource_ownership_for_entry(&raw, types, entry).is_err()); +} + +#[test] +fn changed_suite_target_stays_refused_when_general_current_cache_is_locally_exact() { + let (mut program, checked, entry, expected) = fixture(true, false); + let types = &checked.checked().interner; + assert_plan(&program, types, entry, expected); + let wrapper = &mut program.functions[entry.index() as usize]; + let StatementKind::Evaluate(expression) = &mut wrapper.blocks[0].statements[1].kind else { + panic!("canonical context then call"); + }; + let hir::ExpressionKind::Call { function, .. } = &mut expression.kind else { + panic!("canonical suite call"); + }; + *function = entry; + let witness = wrapper.resource_lowering.as_mut().unwrap(); + witness.blocks = wrapper.blocks.clone(); + assert!(validate_resource_ownership_for_entry(&program, types, entry).is_err()); +} diff --git a/crates/jett_mir/src/resource_ownership/nested_owners_tests.rs b/crates/jett_mir/src/resource_ownership/nested_owners_tests.rs new file mode 100644 index 00000000..3ac087fe --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/nested_owners_tests.rs @@ -0,0 +1,218 @@ +use super::tests::{checked, lowered}; +use super::*; + +const LOOP_SOURCE: &str = r#"namespace app +function scenario(view net: Network, mode: int64) returns nothing: + use resource_probe + mutable int64 remaining = 2 + while remaining > 0: + resource_probe.TestHandle token = resource_probe.create(view net, 4) handle error: + return nothing + remaining = remaining - 1 + if mode == 0: + continue + if mode == 1: + break + return nothing +"#; + +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .expect("original nested owner function") +} +fn scenario_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .expect("original nested owner function") +} + +fn assert_loop_retirements(program: &Program, types: &TypeInterner) { + let function = scenario(program); + let ownership = validate_resource_ownership(program, types).unwrap(); + let plan = ownership.function(function.id).unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.lexical_owners.len(), 1); + let original = witness.lexical_owners[0].original_local(); + let local = witness.lexical_owners[0].current_local().unwrap(); + let slot = plan.owner_slots().iter().find(|slot| { + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local) + }).expect("nested declaration retains its exact owned slot"); + assert_eq!(slot.frame(), plan.root_scope().id()); + assert_eq!( + plan.frames() + .iter() + .filter(|frame| { frame.role() == ResourceFrameRole::Scope }) + .count(), + 1, + "lexical retirement must preserve the resident RootScope" + ); + let mut kinds = Vec::new(); + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + if !matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) { + continue; + } + let position = ResourcePosition::Statement(index); + let current = function + .resource_lexical_exit(block.id, position) + .unwrap() + .expect("exact private lexical exit"); + assert_eq!(current.declarations().collect::>(), vec![original]); + assert_eq!(current.current_locals(function).unwrap(), vec![local]); + let exit = plan.lexical_exit(block.id, position).unwrap(); + assert_eq!(exit.kind(), current.kind()); + assert_eq!(exit.clear_locals(), &[local]); + assert_eq!(exit.operation_ids().len(), 1); + let operation = &plan.operations()[exit.operation_ids()[0].index()]; + assert_eq!(operation.site().function(), function.id); + assert_eq!(operation.site().block(), block.id); + assert_eq!(operation.site().position(), position); + assert_eq!(operation.frame(), slot.frame()); + assert!(!operation.is_expression_operation()); + assert!(matches!(operation.role(), ResourceOperationRole::Drop { + source, occupancy: ResourceOccupancy::Occupied, + } if *source == slot.id())); + assert!( + !plan.operations().iter().any(|operation| { + operation.site().block() == block.id + && operation.site().position() == position + && matches!(operation.role(), ResourceOperationRole::Complete { .. }) + }), + "a lexical boundary must not complete the RootScope" + ); + kinds.push(exit.kind()); + } + } + assert_eq!(kinds.len(), 3); + for expected in [ + ResourceLexicalExitKind::Continue, + ResourceLexicalExitKind::Break, + ResourceLexicalExitKind::Fallthrough, + ] { + assert_eq!(kinds.iter().filter(|kind| **kind == expected).count(), 1); + } +} + +#[test] +fn nested_owned_resource_loop_exits_retire_exact_owner_and_preserve_root_scope() { + for release in [false, true] { + let checked = checked(LOOP_SOURCE, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + assert_loop_retirements(&program, types); + for _ in 0..2 { + let function = scenario_mut(&mut program); + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + assert_loop_retirements(&program, types); + } + } +} + +fn reseal_general(function: &mut Function) { + let blocks = function.blocks.clone(); + let locals = function.locals.clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = blocks; + witness.locals = locals; +} +fn assert_refused(program: &Program, types: &TypeInterner, mutation: usize) { + assert!( + validate_resource_ownership(program, types).is_err(), + "owned lexical mutation {mutation} was admitted" + ); + let before = scenario(program).clone(); + let mut function = before.clone(); + assert!( + !crate::sequences::prune::unreachable(&mut function), + "owned lexical mutation {mutation} authorized unreachable pruning" + ); + assert!(crate::breakpoint_regions::blocks_equal( + &function.blocks, + &before.blocks + )); + assert_eq!(function.locals, before.locals); + let mut function = before.clone(); + assert!( + !crate::sequences::prune::unused_locals(&mut function), + "owned lexical mutation {mutation} authorized local compaction" + ); + assert!(crate::breakpoint_regions::blocks_equal( + &function.blocks, + &before.blocks + )); + assert_eq!(function.locals, before.locals); +} + +#[test] +fn nested_owned_resource_records_and_exit_membership_refuse_general_resealing() { + for release in [false, true] { + let checked = checked(LOOP_SOURCE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + validate_resource_ownership(&baseline, types).unwrap(); + for mutation in 0..4 { + let mut forged = baseline.clone(); + let function = scenario_mut(&mut forged); + match mutation { + 0 => { + function + .resource_lowering + .as_mut() + .unwrap() + .lexical_owners + .clear(); + } + 1 => { + let witness = function.resource_lowering.as_mut().unwrap(); + witness + .lexical_owners + .push(witness.lexical_owners[0].clone()); + } + 2 => { + let witness = function.resource_lowering.as_mut().unwrap(); + let scope = witness + .lexical_exits + .iter_mut() + .flat_map(|exit| &mut exit.scopes) + .find(|scope| !scope.active.is_empty()) + .unwrap(); + scope.active.clear(); + } + 3 => { + let (block, index) = function + .blocks + .iter() + .find_map(|block| { + block + .statements + .iter() + .position(|statement| { + matches!(statement.kind, StatementKind::ResourceLexicalExit(_)) + }) + .map(|index| (block.id, index)) + }) + .unwrap(); + function.blocks[block.index() as usize] + .statements + .remove(index); + } + _ => unreachable!(), + } + reseal_general(function); + crate::validate(&forged).unwrap(); + assert_refused(&forged, types, mutation); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/normalized_calls.rs b/crates/jett_mir/src/resource_ownership/normalized_calls.rs new file mode 100644 index 00000000..513c1cc0 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/normalized_calls.rs @@ -0,0 +1,450 @@ +//! Constructor-only authority for a Source call whose actuals span CFG blocks. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResourceCallRegionId(usize); +impl ResourceCallRegionId { + pub fn index(self) -> usize { + self.0 + } +} + +/// Public syntax is inert without the exact private constructor witness. +#[derive(Debug, Clone, PartialEq)] +pub enum ResourceCallNode { + Begin { + region: ResourceCallRegionId, + }, + Stage { + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + value: Expression, + ordinary: Option, + }, + Invoke { + region: ResourceCallRegionId, + output: LocalId, + }, + End { + region: ResourceCallRegionId, + outcome: ResourceCompletion, + }, +} +impl ResourceCallNode { + pub fn region(&self) -> ResourceCallRegionId { + match self { + Self::Begin { region } + | Self::Stage { region, .. } + | Self::Invoke { region, .. } + | Self::End { region, .. } => *region, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceCallActual { + source_index: usize, + parameter: usize, + original: Expression, + endpoint: Expression, + start: ResourceSite, + stage: ResourceSite, + locals: Vec, + ordinary: Option, +} +impl ResourceCallActual { + pub fn source_index(&self) -> usize { + self.source_index + } + pub fn parameter(&self) -> usize { + self.parameter + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn endpoint(&self) -> &Expression { + &self.endpoint + } + pub fn start(&self) -> ResourceSite { + self.start + } + pub fn stage(&self) -> ResourceSite { + self.stage + } + pub fn locals(&self) -> &[Local] { + &self.locals + } + pub fn ordinary(&self) -> Option { + self.ordinary + } +} + +/// Archival originals and current emitted endpoints intentionally have separate IDs. +#[derive(Debug, Clone, PartialEq)] +pub struct ResourceCallRegion { + id: ResourceCallRegionId, + original: Expression, + source: hir::SourceCallOwnership, + function: FunctionId, + order: Vec, + parent: Option, + begin: ResourceSite, + actuals: Vec, + invocation: Option<(ResourceSite, LocalId)>, + exits: Vec<(ResourceSite, ResourceCompletion)>, +} +impl ResourceCallRegion { + pub fn id(&self) -> ResourceCallRegionId { + self.id + } + pub fn original(&self) -> &Expression { + &self.original + } + pub fn source(&self) -> &hir::SourceCallOwnership { + &self.source + } + pub fn function(&self) -> FunctionId { + self.function + } + pub fn evaluation_order(&self) -> &[usize] { + &self.order + } + pub fn parent(&self) -> Option { + self.parent + } + pub fn begin(&self) -> ResourceSite { + self.begin + } + pub fn actuals(&self) -> &[ResourceCallActual] { + &self.actuals + } + pub fn invocation(&self) -> Option<(ResourceSite, LocalId)> { + self.invocation + } + pub fn output(&self) -> Option { + self.invocation.map(|(_, output)| output) + } + pub fn ordinary(&self, parameter: usize) -> Option { + self.actuals + .iter() + .find(|actual| actual.parameter == parameter) + .and_then(|actual| actual.ordinary) + } + pub fn exits(&self) -> &[(ResourceSite, ResourceCompletion)] { + &self.exits + } + pub(super) fn same(&self, other: &Self) -> bool { + self.id == other.id + && self.source == other.source + && self.function == other.function + && self.order == other.order + && self.parent == other.parent + && self.begin == other.begin + && self.invocation == other.invocation + && self.exits == other.exits + && crate::breakpoint_regions::expressions_equal(&self.original, &other.original) + && self.actuals.len() == other.actuals.len() + && self.actuals.iter().zip(&other.actuals).all(|(a, b)| { + a.source_index == b.source_index + && a.parameter == b.parameter + && a.start == b.start + && a.stage == b.stage + && a.locals == b.locals + && a.ordinary == b.ordinary + && crate::breakpoint_regions::expressions_equal(&a.original, &b.original) + && crate::breakpoint_regions::expressions_equal(&a.endpoint, &b.endpoint) + }) + } + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + let node = |site: ResourceSite| -> Result<&ResourceCallNode, String> { + let ResourcePosition::Statement(index) = site.position else { + return Err("Resource call region site is not a statement".into()); + }; + let statement = function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(index)) + .ok_or("Resource call region site is missing")?; + let StatementKind::ResourceCall(node) = &statement.kind else { + return Err("Resource call region site changed its typed node".into()); + }; + if site.function != function.id || node.region() != self.id { + return Err("Resource call region changed its function or opaque reference".into()); + } + Ok(node) + }; + if !matches!(node(self.begin)?, ResourceCallNode::Begin { .. }) + || self.order.len() != self.source.arguments.len() + || self.actuals.len() != self.order.len() + { + return Err( + "Resource call region lost its exact begin or complete Source tuple".into(), + ); + } + for (source_index, actual) in self.actuals.iter().enumerate() { + if actual.source_index != source_index + || self.order[source_index] != actual.parameter + || !matches!(node(actual.stage)?, ResourceCallNode::Stage { source_index: index, parameter, value, ordinary, .. } + if *index == source_index && *parameter == actual.parameter && *ordinary == actual.ordinary + && crate::breakpoint_regions::expressions_equal(value, &actual.endpoint)) + || actual + .locals + .iter() + .any(|header| function.local(header.id) != Some(header)) + { + return Err( + "Resource call actual lost its lexical endpoint or generated headers".into(), + ); + } + } + let (site, output) = self + .invocation + .ok_or("Resource call region has no exact invocation")?; + if !matches!(node(site)?, ResourceCallNode::Invoke { output: found, .. } if *found == output) + || function.local(output).map(|local| local.ty) != Some(self.original.ty) + { + return Err("Resource call region changed its invocation output".into()); + } + for (site, outcome) in &self.exits { + if !matches!(node(*site)?, ResourceCallNode::End { outcome: found, .. } if found == outcome) + { + return Err("Resource call region lost an exact completion site".into()); + } + } + let count = function.blocks.iter().flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, StatementKind::ResourceCall(value) if value.region() == self.id)).count(); + if count != 2 + self.actuals.len() + self.exits.len() + || !self + .exits + .iter() + .any(|(_, outcome)| *outcome == ResourceCompletion::Normal) + { + return Err( + "Resource call region has copied, disconnected or missing typed nodes".into(), + ); + } + Ok(()) + } + pub(super) fn shift_prefix(&mut self, block: BlockId, count: usize) { + let shift = |site: &mut ResourceSite| { + if site.block == block + && let ResourcePosition::Statement(index) = &mut site.position + { + *index += count; + } + }; + shift(&mut self.begin); + for actual in &mut self.actuals { + shift(&mut actual.start); + shift(&mut actual.stage); + } + if let Some((site, _)) = &mut self.invocation { + shift(site); + } + for (site, _) in &mut self.exits { + shift(site); + } + } + pub(super) fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + let remap = |site: &mut ResourceSite| -> Result<(), String> { + site.block = map.get(site.block.index() as usize).copied().flatten() + .ok_or("pending Resource call region: canonical selection removed a sealed evaluation or completion site")?; + Ok(()) + }; + remap(&mut self.begin)?; + for actual in &mut self.actuals { + remap(&mut actual.start)?; + remap(&mut actual.stage)?; + } + if let Some((site, _)) = &mut self.invocation { + remap(site)?; + } + for (site, _) in &mut self.exits { + remap(site)?; + } + Ok(()) + } + pub(super) fn remap_locals(&mut self, remap: &mut impl FnMut(&mut LocalId)) { + for actual in &mut self.actuals { + for header in &mut actual.locals { + remap(&mut header.id); + if let Some(source) = &mut header.view_source { + remap(source); + } + } + if let Some(local) = &mut actual.ordinary { + remap(local); + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(actual.endpoint.clone()), + span: actual.endpoint.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: actual.endpoint.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(endpoint) = wrapper.statements.remove(0).kind { + actual.endpoint = endpoint; + } + } + if let Some((_, output)) = &mut self.invocation { + remap(output); + } + } +} + +impl Capture { + pub(crate) fn normalized_call_needed(&self, value: &Expression, types: &TypeInterner) -> bool { + self.witness.as_ref().is_some_and(|witness| { + let ordinary_direct = match &value.kind { + hir::ExpressionKind::Call { args, .. } => { + witness.execution.direct_call(value) + && !resource_type_pending(types, value.ty) + && args.iter().all(|arg| !resource_type_pending(types, arg.ty)) + } + _ => false, + }; + // Aggregate custody retains its separate carrier transport. Only an + // ordinary endpoint may enter this path without a handled actual. + source_custody_call(value, types, &witness.execution) + && (ordinary_direct || { + let mut handled = false; + walk::expression(value, &mut |node| { + handled |= matches!(node.kind, hir::ExpressionKind::Handle { .. }) + }); + handled + }) + }) + } + pub(crate) fn begin_normalized_call( + &mut self, + original: &Expression, + parent: Option, + begin: ResourceSite, + types: &TypeInterner, + ) -> Result { + let witness = self + .witness + .as_mut() + .ok_or("Resource call region lacks initial Source authentication")?; + witness.source.validate_types(types)?; + let hir::ExpressionKind::Call { + function, + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + } = &original.kind + else { + return Err("pending Resource call region: indirect/hook handled custody calls require their separate invocation proof".into()); + }; + let mut occurrences = 0; + walk::hir_block(&witness.original.body, &mut |value| { + occurrences += usize::from(crate::breakpoint_regions::expressions_equal( + value, original, + )) + }); + if occurrences != 1 + || source.arguments.len() != args.len() + || source + .arguments + .iter() + .any(|fact| fact.staging != hir::ArgumentStaging::Original) + || evaluation_order.len() != args.len() + || evaluation_order + .iter() + .copied() + .collect::>() + .len() + != args.len() + || evaluation_order + .iter() + .any(|parameter| *parameter >= args.len()) + { + return Err( + "Resource call region differs from its unique complete original Source call".into(), + ); + } + let id = ResourceCallRegionId(witness.regions.len()); + if parent.is_some_and(|parent| parent.index() >= id.index()) { + return Err("Resource call region has no constructor-owned parent".into()); + } + witness.regions.push(ResourceCallRegion { + id, + original: original.clone(), + source: source.clone(), + function: *function, + order: evaluation_order.clone(), + parent, + begin, + actuals: Vec::new(), + invocation: None, + exits: Vec::new(), + }); + Ok(id) + } + pub(crate) fn normalized_actual( + &mut self, + region: ResourceCallRegionId, + source_index: usize, + parameter: usize, + original: &Expression, + endpoint: &Expression, + start: ResourceSite, + stage: ResourceSite, + locals: &[Local], + ordinary: Option, + ) -> Result<(), String> { + let record = self + .witness + .as_mut() + .and_then(|witness| witness.regions.get_mut(region.index())) + .ok_or("Resource actual lost its constructor region")?; + if record.actuals.len() != source_index + || record.order.get(source_index) != Some(¶meter) + { + return Err("Resource actual changed its exact lexical occurrence".into()); + } + record.actuals.push(ResourceCallActual { + source_index, + parameter, + original: original.clone(), + endpoint: endpoint.clone(), + start, + stage, + locals: locals.to_vec(), + ordinary, + }); + Ok(()) + } + pub(crate) fn normalized_node(&mut self, site: ResourceSite, node: &ResourceCallNode) { + let Some(record) = self + .witness + .as_mut() + .and_then(|witness| witness.regions.get_mut(node.region().index())) + else { + return; + }; + match node { + ResourceCallNode::Invoke { output, .. } => record.invocation = Some((site, *output)), + ResourceCallNode::End { outcome, .. } => record.exits.push((site, *outcome)), + ResourceCallNode::Begin { .. } | ResourceCallNode::Stage { .. } => {} + } + } +} + +impl Function { + /// Validity is granted only by fresh whole-program ownership validation. + pub fn resource_call_region(&self, id: ResourceCallRegionId) -> Option<&ResourceCallRegion> { + self.resource_lowering + .as_ref()? + .regions + .get(id.index()) + .filter(|region| region.id == id) + } +} diff --git a/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs b/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs new file mode 100644 index 00000000..02140647 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/normalized_calls_tests.rs @@ -0,0 +1,511 @@ +use super::tests::{checked, lowered}; +use super::*; +use hir::ExpressionKind as E; + +const ORIGINAL: &str = include_str!( + "../../../jett_comptime/src/resource_execution/fixtures/05_later_named_argument_failure_v2.jett" +); +const MINIMAL: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/09_later_handle_minimal.jett" +); +const SUCCESS: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/11_later_handle_success.jett" +); +const RETAINED: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/12_later_handle_written_view.jett" +); +const FIRST: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/13_first_handle_failure.jett" +); + +fn outer(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function + .resource_lowering + .as_ref() + .is_some_and(|witness| !witness.regions.is_empty()) + }) + .expect("one authenticated outer call producer") +} + +#[test] +fn normalized_source_call_preserves_original_tuple_and_constructor_cfg_in_both_profiles() { + for release in [false, true] { + for source in [ORIGINAL, MINIMAL, SUCCESS, RETAINED, FIRST] { + let checked = checked(source, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let mut originals = Vec::new(); + for function in &hir.functions { + walk::hir_block(&function.body, &mut |value| { + if let E::Call { + function: target, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + && hir.functions[target.index() as usize] + .identity + .declaration + .name + == "accept_staged" + { + assert_eq!(evaluation_order, &[1, 0, 2]); + assert!( + source + .arguments + .iter() + .all(|fact| fact.staging == hir::ArgumentStaging::Original) + ); + originals.push(value.clone()); + } + }); + } + assert_eq!(originals.len(), 1); + let mir = lower(&hir, types).unwrap(); + let function = outer(&mir); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.regions.len(), 1); + let region = &witness.regions[0]; + assert!(crate::breakpoint_regions::expressions_equal( + region.original(), + &originals[0] + )); + assert_eq!(region.evaluation_order(), &[1, 0, 2]); + assert_eq!( + region + .actuals() + .iter() + .map(ResourceCallActual::parameter) + .collect::>(), + [1, 0, 2] + ); + assert!(region.actuals()[0].ordinary().is_none()); + assert!(region.actuals()[1].ordinary().is_some()); + assert!(region.actuals()[2].ordinary().is_some()); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, StatementKind::BeginCallView { .. })) + ); + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let begins = flow + .operations() + .iter() + .filter(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .collect::>(); + assert_eq!(begins.len(), 1); + let frame = begins[0].frame(); + assert_eq!( + flow.frames()[frame.index()].parent(), + Some(ResourceFrameId(0)) + ); + let stages = flow + .operations() + .iter() + .filter(|operation| { + matches!( + operation.role(), + ResourceOperationRole::StageSourceActual { .. } + ) + }) + .collect::>(); + assert_eq!(stages.len(), 3); + assert!(stages.iter().all( + |operation| operation.frame() == frame && !operation.is_expression_operation() + )); + assert_eq!( + flow.operations() + .iter() + .filter(|operation| operation.frame() == frame + && matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + )) + .count(), + 1 + ); + let prepared = flow + .operations() + .iter() + .filter_map(|operation| match operation.role() { + ResourceOperationRole::PrepareSourceBorrow { loan, parameter } => { + Some((*loan, *parameter)) + } + _ => None, + }) + .collect::>(); + assert_eq!(prepared.len(), 1); + assert_eq!(prepared[0].1, 1); + assert_eq!(flow.loans()[prepared[0].0.index()].parameter(), Some(1)); + for (site, outcome) in region.exits() { + if *outcome != ResourceCompletion::Abort { + continue; + } + let end = flow + .operations() + .iter() + .find(|operation| { + operation.site() == *site + && operation.frame() == frame + && matches!( + operation.role(), + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Abort + } + ) + }) + .unwrap(); + assert!(!end.is_expression_operation()); + let ResourcePosition::Statement(index) = site.position() else { + panic!("typed abandoned statement"); + }; + assert!(matches!( + function.blocks[site.block().index() as usize] + .terminator + .kind, + TerminatorKind::Return(_) + )); + assert!( + index + < function.blocks[site.block().index() as usize] + .statements + .len() + ); + } + } + } +} + +#[test] +fn normalized_first_handled_actual_has_a_proved_empty_abandoned_prefix() { + for release in [false, true] { + let checked = checked(FIRST, release); + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + let plan = validate_resource_ownership(&mir, &checked.checked().interner).unwrap(); + let flow = plan.function(function.id).unwrap(); + let first = region.actuals()[0].start().block(); + let TerminatorKind::Branch { + else_block: failed, .. + } = function.blocks[first.index() as usize].terminator.kind + else { + panic!("first exact Handle branch"); + }; + let abort = region + .exits() + .iter() + .find(|(site, outcome)| site.block() == failed && *outcome == ResourceCompletion::Abort) + .unwrap() + .0; + assert!( + !flow + .operations() + .iter() + .any(|operation| operation.site() == abort + && matches!( + operation.role(), + ResourceOperationRole::EndBorrow { .. } + | ResourceOperationRole::Drop { .. } + )) + ); + } +} + +#[test] +fn normalized_call_public_node_and_cfg_edits_cannot_copy_or_reseal_authority() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let types = &checked.checked().interner; + let original = lowered(&checked); + let index = original + .functions + .iter() + .position(|function| function.id == outer(&original).id) + .unwrap(); + let region = outer(&original).resource_lowering.as_ref().unwrap().regions[0].clone(); + for mutation in 0..9 { + let mut edited = original.clone(); + let function = &mut edited.functions[index]; + let actual = region.actuals()[0].stage(); + let ResourcePosition::Statement(stage) = actual.position() else { + panic!("stage statement"); + }; + match mutation { + 0 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { + source_index, + .. + }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + *source_index += 1; + } + } + 1 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { + parameter, .. + }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + *parameter = 0; + } + } + 2 => { + if let StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) = + &mut function.blocks[actual.block().index() as usize].statements[stage].kind + { + value.kind = E::Clone(Box::new(value.clone())); + } + } + 3 => { + let statement = + function.blocks[actual.block().index() as usize].statements[stage].clone(); + function.blocks[actual.block().index() as usize] + .statements + .insert(stage, statement); + } + 4 => { + let ResourcePosition::Statement(begin) = region.begin().position() else { + panic!("begin statement"); + }; + function.blocks[region.begin().block().index() as usize] + .statements + .remove(begin); + } + 5 => { + let (site, _) = region.invocation().unwrap(); + let ResourcePosition::Statement(invoke) = site.position() else { + panic!("invoke statement"); + }; + function.blocks[site.block().index() as usize] + .statements + .remove(invoke); + } + 6 => { + let (site, _) = region + .exits() + .iter() + .find(|(_, outcome)| *outcome == ResourceCompletion::Abort) + .unwrap(); + let ResourcePosition::Statement(end) = site.position() else { + panic!("end statement"); + }; + function.blocks[site.block().index() as usize] + .statements + .remove(end); + } + 7 => function.entry = region.actuals()[1].stage().block(), + 8 => { + let block = + &mut function.blocks[region.actuals()[1].start().block().index() as usize]; + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut block.terminator.kind + { + std::mem::swap(then_block, else_block); + } else { + panic!("exact Handle selector"); + } + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&edited, types).is_err(), + "mutation {mutation} must lose constructor authority" + ); + } + } +} + +#[test] +fn normalized_call_canonical_compaction_preserves_archival_original_and_current_sites() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let mut mir = lowered(&checked); + let id = outer(&mir).id; + let original = outer(&mir).resource_lowering.as_ref().unwrap().regions[0] + .original() + .clone(); + let function = mir + .functions + .iter_mut() + .find(|function| function.id == id) + .unwrap(); + assert!(crate::sequences::prune::unreachable(function)); + assert!(crate::sequences::prune::unused_locals(function)); + assert!(crate::breakpoint_regions::expressions_equal( + function.resource_lowering.as_ref().unwrap().regions[0].original(), + &original + )); + validate_resource_ownership(&mir, &checked.checked().interner).unwrap(); + } +} + +#[test] +fn normalized_call_original_source_edits_refuse_before_constructor_extraction() { + for release in [false, true] { + let checked = checked(MINIMAL, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + for mutation in 0..4 { + let mut edited = original.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let call = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Expression(value) + if matches!(value.kind, E::Call { .. }) => + { + Some(value) + } + _ => None, + }) + .expect("original complete Source call"); + let call_span = call.span; + let E::Call { + args, + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &mut call.kind + else { + panic!("Source call"); + }; + match mutation { + 0 => evaluation_order.swap(0, 1), + 1 => { + if let hir::CallerOrigin::Binding(binding) = &mut source.arguments[1].origin { + binding.local = function.params[0].local; + } else { + panic!("original Resource Binding fact"); + } + } + 2 => source.arguments[1].syntax = jett_typecheck::CheckedCallerSyntax::WrittenView, + 3 => args[1].kind = E::Clone(Box::new(args[1].clone())), + _ => unreachable!(), + } + let errors = lower(&edited, types).unwrap_err(); + let expected_message = match mutation { + 0 => { + "call ownership lexical evaluation order disagrees with its source permutation" + } + 1 | 2 => "call ownership disagrees with its original source witness", + 3 => "call ownership binding does not match its actual typed operand", + _ => unreachable!(), + }; + assert_eq!(errors.len(), 1, "release={release}, mutation={mutation}"); + assert_eq!(errors[0].span, call_span); + assert_eq!(errors[0].message, expected_message); + } + } +} + +#[test] +fn normalized_handle_default_joins_the_same_operation_before_one_invocation() { + for release in [false, true] { + let source = MINIMAL.replace("\r\n", "\n").replace( + "marker: fail_marker() handle error:\n return nothing\n , net: view net", + "marker: (fail_marker() handle error: default 7), net: view net", + ); + assert_ne!(source, MINIMAL, "exact canonical original marker Handle"); + let checked = checked(&source, release); + let types = &checked.checked().interner; + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + assert_eq!(region.exits().len(), 1); + assert_eq!(region.exits()[0].1, ResourceCompletion::Normal); + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let begin = flow + .operations() + .iter() + .find(|operation| { + matches!( + operation.role(), + ResourceOperationRole::BeginSourceFunction { .. } + ) + }) + .unwrap(); + assert_eq!( + flow.operations() + .iter() + .filter(|operation| operation.frame() == begin.frame() + && matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + )) + .count(), + 1 + ); + assert!( + !flow + .operations() + .iter() + .any(|operation| operation.frame() == begin.frame() + && matches!( + operation.role(), + ResourceOperationRole::Complete { + outcome: ResourceCompletion::Abort + } + )) + ); + } +} + +#[test] +fn normalized_abandonment_retires_only_its_prefix_before_another_owner_return() { + const SOURCE: &str = r#"namespace app +function accept_staged(marker: int64, view token: resource_probe.TestHandle, view net: Network) returns nothing: + return nothing +function fail_marker() returns result[int64, string]: + return fail("later marker") +function scenario(view net: Network, token: resource_probe.TestHandle, other: resource_probe.TestHandle) returns resource_probe.TestHandle: + accept_staged(token: token, marker: fail_marker() handle error: + return other + , net: view net) + return other +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mir = lowered(&checked); + let function = outer(&mir); + let region = &function.resource_lowering.as_ref().unwrap().regions[0]; + let plan = validate_resource_ownership(&mir, types).unwrap(); + ResourceCompanionPlan::analyze(&plan, function.id).unwrap(); + let flow = plan.function(function.id).unwrap(); + let other = function.params[2].local; + let other_slot = flow.owner_slots().iter().find(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == other)).unwrap().id(); + for (site, outcome) in region.exits() { + if *outcome != ResourceCompletion::Abort { + continue; + } + assert!(!flow.operations().iter().any(|operation| operation.site() == *site && matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == other_slot))); + assert!(flow.operations().iter().any(|operation| operation.site().block() == site.block() && operation.site().position() == ResourcePosition::Terminator && matches!(operation.role(), ResourceOperationRole::Transfer { source, .. } if *source == other_slot))); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/original_calls_tests.rs b/crates/jett_mir/src/resource_ownership/original_calls_tests.rs new file mode 100644 index 00000000..18f15f7f --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/original_calls_tests.rs @@ -0,0 +1,226 @@ +use super::tests::{checked, lowered}; +use super::*; +use jett_typecheck::CheckedCallerEffect; + +const SOURCE: &str = r#"namespace app +function read_once(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2) handle error: + return nothing + result[int64, string] observed = read_once(token, view net) + return nothing +"#; +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn original_call(function: &Function) -> (&Expression, BlockId, usize) { + for block in &function.blocks { + for (index, statement) in block.statements.iter().enumerate() { + let (StatementKind::Let { value, .. } | StatementKind::Evaluate(value)) = + &statement.kind + else { + continue; + }; + let hir::ExpressionKind::Call { + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + else { + continue; + }; + if source + .arguments + .iter() + .any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned) + { + return (value, block.id, index); + } + } + } + panic!("exact original bare Resource to View Source call"); +} +#[test] +fn resource_original_call_association_keeps_custody_pending_without_ordinary_owner_claim() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let function = scenario(&program); + let (call, block, index) = original_call(function); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + call, + block, + index + ) + .unwrap() + ); + let acquisitions = + crate::call_ownership::validate_function(&program, function, types).unwrap(); + assert!(acquisitions.resource_pending); + let hir::ExpressionKind::Call { args, .. } = &call.kind else { + unreachable!() + }; + assert_eq!(acquisitions.argument_binding(&args[0]), None); + assert!(acquisitions.arguments(call).unwrap().is_empty()); + validate_resource_ownership(&program, types).unwrap(); + assert!( + crate::validate_caller_acquisitions(&program, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + let copied_query = call.clone(); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + &copied_query, + block, + index + ) + .unwrap_err() + .contains("exact current expression site") + ); + assert!( + original_call_at( + function, + &program.resource_manifest, + types, + call, + block, + index + 1 + ) + .is_err() + ); + } +} +#[test] +fn resource_original_call_association_rejects_lost_duplicated_and_changed_source_current_pairs() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + validate_resource_ownership(&program, types).unwrap(); + for mutation in 0..7 { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + let witness = function.resource_lowering.as_mut().unwrap(); + match mutation { + 0 => witness.calls.clear(), + 1 => witness.calls.push(witness.calls[0].clone()), + 2 => witness.calls[0].original.span.start += 1, + 3 => witness.calls[0].current.span.start += 1, + 4 => { + let block = function.blocks.iter_mut().find(|block| block.statements.iter().any(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned))))).unwrap(); + let statement = block.statements.iter().find(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned)))).unwrap().clone(); + block.statements.push(statement); + } + 5 | 6 => { + let statement = function.blocks.iter_mut().flat_map(|block| &mut block.statements).find(|statement| + matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(&value.kind, hir::ExpressionKind::Call { ownership: hir::CallOwnership::Source(source), .. } if source.arguments.iter().any(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned)))).unwrap(); + let StatementKind::Let { value, .. } = &mut statement.kind else { + unreachable!() + }; + let hir::ExpressionKind::Call { + evaluation_order, + ownership: hir::CallOwnership::Source(source), + .. + } = &mut value.kind + else { + unreachable!() + }; + if mutation == 5 { + evaluation_order.reverse(); + } else { + source.arguments[0].source_span.start += 1; + } + } + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + let mut prepared = program.clone(); + crate::prepare_native_generated_functions(&mut prepared, types); + validate_resource_ownership(&prepared, types).unwrap(); + let (call, block, index) = original_call(scenario(&prepared)); + assert!( + original_call_at( + scenario(&prepared), + &prepared.resource_manifest, + types, + call, + block, + index + ) + .unwrap() + ); + } +} +#[test] +fn resource_original_call_association_captures_unnamed_nested_optional_custody_result() { + const SOURCE: &str = r#"namespace app +function accept(value: optional[resource_probe.TestHandle]) returns nothing: + return nothing +function scenario() returns nothing: + use resource_probe + accept(resource_probe.empty()) + return nothing +"#; + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let function = scenario(&program); + assert!( + function + .locals + .iter() + .all(|local| !custody_type(types, local.ty)) + ); + assert!(!custody_type(types, function.return_type)); + let witness = function + .resource_lowering + .as_ref() + .expect("unnamed Resource expression retains original constructor authority"); + assert_eq!( + witness.calls.len(), + 2, + "outer Source consumer and original nested optional producer" + ); + validate_resource_ownership(&program, types).unwrap(); + let mut changed = program.clone(); + changed + .functions + .iter_mut() + .find(|function| function.id == scenario(&program).id) + .unwrap() + .resource_lowering = None; + assert!(validate_call_ownership(&changed, types).is_err()); + } +} diff --git a/crates/jett_mir/src/resource_ownership/owned_declarations.rs b/crates/jett_mir/src/resource_ownership/owned_declarations.rs new file mode 100644 index 00000000..ddd66751 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/owned_declarations.rs @@ -0,0 +1,292 @@ +//! Exact nested owned declarations minted only during fresh checked MIR lowering. +use super::*; + +#[derive(Debug, Clone)] +struct Initialization { + site: ResourceSite, + header: Local, + endpoint: Expression, +} +impl PartialEq for Initialization { + fn eq(&self, other: &Self) -> bool { + self.site == other.site + && self.header == other.header + && crate::breakpoint_regions::expressions_equal(&self.endpoint, &other.endpoint) + } +} + +#[derive(Debug, Clone)] +pub(super) struct OwnedDeclaration { + original_path: Vec, + original_scope: Vec, + original_header: Local, + original_span: Span, + original_value: Expression, + current: Option, +} +impl PartialEq for OwnedDeclaration { + fn eq(&self, other: &Self) -> bool { + self.original_path == other.original_path + && self.original_scope == other.original_scope + && self.original_header == other.original_header + && self.original_span == other.original_span + && crate::breakpoint_regions::expressions_equal( + &self.original_value, + &other.original_value, + ) + && self.current == other.current + } +} +impl OwnedDeclaration { + pub(super) fn original_local(&self) -> LocalId { + self.original_header.id + } + pub(super) fn current_local(&self) -> Option { + self.current.as_ref().map(|current| current.header.id) + } + pub(super) fn declaration(&self) -> lexical_borrows::Declaration { + lexical_borrows::Declaration { + path: self.original_path.clone(), + scope: self.original_scope.clone(), + local: self.original_header.id, + value: self.original_value.clone(), + span: self.original_span, + } + } +} + +/// Root declarations retain their existing function-completion custody. +/// Wider carriers retain their dedicated conservative admission path. +pub(super) fn capture_owned( + function: &hir::Function, + manifest: &hir::ResourceManifest, + types: &TypeInterner, +) -> Vec { + lexical_borrows::source_declarations(function, manifest) + .into_iter() + .filter_map(|declaration| { + let header = function + .locals + .get(declaration.local.index() as usize) + .filter(|header| header.id == declaration.local)?; + if declaration.scope.is_empty() + || header.view_source.is_some() + || !shape(manifest, types, header.ty).is_ok_and(|shape| shape.is_some()) + { + return None; + } + Some(OwnedDeclaration { + original_path: declaration.path, + original_scope: declaration.scope, + original_header: header.clone(), + original_span: declaration.span, + original_value: declaration.value, + current: None, + }) + }) + .collect() +} + +pub(super) fn same_owners(left: &[OwnedDeclaration], right: &[OwnedDeclaration]) -> bool { + left == right +} + +pub(super) fn owner_current_local( + witness: &ResourceLoweringWitness, + original: LocalId, +) -> Option { + witness + .lexical_owners + .iter() + .find(|row| row.original_local() == original) + .and_then(OwnedDeclaration::current_local) +} + +impl Capture { + /// Called only at the freshly lowered Let, before its exact node is pushed. + pub(crate) fn bind_owned_declaration( + &mut self, + original: &hir::Statement, + endpoint: &Expression, + header: &Local, + block: BlockId, + index: usize, + ) -> Result { + let Some(witness) = &mut self.witness else { + return Ok(false); + }; + let hir::StatementKind::Let { local, value } = &original.kind else { + return Ok(false); + }; + let mut rows = witness + .lexical_owners + .iter_mut() + .filter(|row| row.original_local() == *local); + let Some(row) = rows.next() else { + return Ok(false); + }; + if rows.next().is_some() + || row.current.is_some() + || row.original_span != original.span + || !crate::breakpoint_regions::expressions_equal(&row.original_value, value) + || row.original_header != *header + || header.view_source.is_some() + || endpoint.ty != header.ty + { + return Err("Resource owned declaration changed its unique original initializer or current header".into()); + } + row.current = Some(Initialization { + site: ResourceSite { + function: witness.original.id, + block, + position: ResourcePosition::Statement(index), + }, + header: header.clone(), + endpoint: endpoint.clone(), + }); + Ok(true) + } +} + +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if !same_owners(&witness.lexical_owners, &witness.lexical_owner_seal) { + return Err( + "Resource owned declarations changed their independent original/current seal".into(), + ); + } + let originals = lexical_borrows::source_declarations(&witness.original, &witness.manifest); + for (index, row) in witness.lexical_owners.iter().enumerate() { + let Some(current) = &row.current else { + return Err("Resource owned declaration lost its fresh current initialization".into()); + }; + if row.original_scope.is_empty() + || row.original_header.view_source.is_some() + || witness + .original + .locals + .get(row.original_local().index() as usize) + != Some(&row.original_header) + || originals + .iter() + .filter(|original| { + original.local == row.original_local() + && original.path == row.original_path + && original.scope == row.original_scope + && original.span == row.original_span + && crate::breakpoint_regions::expressions_equal( + &original.value, + &row.original_value, + ) + }) + .count() + != 1 + || witness.lexical_owners[..index].iter().any(|other| { + other.original_local() == row.original_local() + || other.original_path == row.original_path + || other.current_local() == row.current_local() + || other + .current + .as_ref() + .is_some_and(|other| other.site == current.site) + }) + { + return Err( + "Resource owned declaration lost its exact original lexical membership".into(), + ); + } + let ResourcePosition::Statement(position) = current.site.position else { + return Err("Resource owned initialization has no exact statement site".into()); + }; + let statement = function + .blocks + .get(current.site.block.index() as usize) + .filter(|block| block.id == current.site.block) + .and_then(|block| block.statements.get(position)) + .ok_or("Resource owned initialization lost its current statement")?; + if current.site.function != function.id + || function.local(current.header.id) != Some(¤t.header) + || current.header.view_source.is_some() + || current.header.ty != row.original_header.ty + || current.endpoint.ty != current.header.ty + || statement.span != row.original_span + || !matches!(&statement.kind, StatementKind::Let { local, value } + if *local == current.header.id + && crate::breakpoint_regions::expressions_equal(value, ¤t.endpoint)) + || function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, .. } + if *local == current.header.id) + }) + .count() + != 1 + { + return Err( + "Resource owned initialization changed its unique current Let relation".into(), + ); + } + } + Ok(()) +} + +pub(super) fn remap_blocks( + witness: &mut ResourceLoweringWitness, + map: &[Option], +) -> Result<(), String> { + for row in &mut witness.lexical_owners { + if let Some(current) = &mut row.current { + current.site.block = map + .get(current.site.block.index() as usize) + .copied() + .flatten() + .ok_or("Resource canonical map removed an owned initialization")?; + } + } + Ok(()) +} + +pub(super) fn remap_locals( + witness: &mut ResourceLoweringWitness, + remap: &mut impl FnMut(&mut LocalId), +) { + for row in &mut witness.lexical_owners { + let Some(current) = &mut row.current else { + continue; + }; + remap(&mut current.header.id); + if let Some(source) = &mut current.header.view_source { + remap(source); + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(current.endpoint.clone()), + span: current.endpoint.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: current.endpoint.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(endpoint) = wrapper.statements.remove(0).kind { + current.endpoint = endpoint; + } + } +} + +pub(super) fn shift_prefix(witness: &mut ResourceLoweringWitness, block: BlockId, count: usize) { + for row in &mut witness.lexical_owners { + if let Some(current) = &mut row.current + && current.site.block == block + && let ResourcePosition::Statement(index) = &mut current.site.position + { + *index += count; + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/reflected_field_source.jett b/crates/jett_mir/src/resource_ownership/reflected_field_source.jett new file mode 100644 index 00000000..44c19d0e --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_field_source.jett @@ -0,0 +1,24 @@ +namespace app +struct IterationShape: + first: int64 + second: int64 +export function main(net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle owner = resource_probe.create(view net, 871) handle error: + return nothing + optional[resource_probe.TestHandle] held = some(owner) + mutable int64 total = 0 + for field in type.fields[IterationShape](): + comptime type Element = field.type_info: + resource_probe.TestHandle token = view((view held) handle: + return nothing + ) + Element observed = resource_probe.borrow(view net, view token) handle error: + return nothing + total = total + observed + if total != 34: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle final_token = held handle: + return nothing + resource_probe.close(final_token) + return nothing diff --git a/crates/jett_mir/src/resource_ownership/reflected_fields.rs b/crates/jett_mir/src/resource_ownership/reflected_fields.rs new file mode 100644 index 00000000..4cb1bb10 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_fields.rs @@ -0,0 +1,365 @@ +//! Exact ordinal guards derived only from the original checked field-loop archive. +use super::*; +use hir::{BinaryOp, ExpressionKind as E}; + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct FieldDispatch { + original: hir::ReflectedFieldDispatchProof, + key: Local, + type_info: Expression, + guards: Vec, + dispatches: Vec, + targets: Vec, + paths: Vec>, + otherwise: BlockId, +} + +pub(crate) fn ordinal_guard( + proof: &hir::ReflectedFieldDispatchProof, + type_info: &Expression, + index: usize, +) -> Result { + let E::Field { + base, + owner_type, + field, + } = &type_info.kind + else { + return Err("Resource field dispatch lost its checked field selector".into()); + }; + if *field != proof.type_info_field() { + return Err("Resource field dispatch changed its TypeInfo member".into()); + } + let index = i128::try_from(index) + .map_err(|_| "Resource field ordinal exceeds the checked scalar representation")?; + Ok(Expression { + kind: E::Binary { + left: Box::new(Expression { + kind: E::Field { + base: base.clone(), + owner_type: *owner_type, + field: proof.field_index(), + }, + ty: TypeInterner::INT64, + span: type_info.span, + }), + op: BinaryOp::Equal, + right: Box::new(Expression { + kind: E::Int(index), + ty: TypeInterner::INT64, + span: type_info.span, + }), + }, + ty: TypeInterner::BOOL, + span: type_info.span, + }) +} + +impl Capture { + pub(crate) fn reflected_field_dispatch( + &self, + type_info: &Expression, + arms: &[hir::ReflectedTypeArm], + ) -> Option { + let witness = self.witness.as_ref()?; + witness + .source + .get_reflected_field_dispatch(witness.original.id, type_info, arms) + .cloned() + } + + pub(crate) fn capture_reflected_field_dispatch( + &mut self, + original: hir::ReflectedFieldDispatchProof, + key: Local, + guards: Vec, + dispatches: Vec, + targets: Vec, + otherwise: BlockId, + ) -> Result<(), String> { + let Some(witness) = &mut self.witness else { + return Err("Resource field dispatch has no original Source constructor".into()); + }; + let paths = (0..original.arms().len()) + .map(|index| { + original + .arm_container_path(index) + .map(<[_]>::to_vec) + .ok_or("Resource field dispatch lost its ordinal Scope path") + }) + .collect::, _>>()?; + witness.reflected_fields.push(FieldDispatch { + type_info: original.type_info().clone(), + original, + key, + guards, + dispatches, + targets, + paths, + otherwise, + }); + Ok(()) + } +} + +impl FieldDispatch { + pub(super) fn matches_field_update_scope( + &self, + scope: &hir::RequiredScopeIdentity, + binding: &hir::ScopedTypeBinding, + ) -> bool { + self.original.matches_field_update_scope(scope, binding) + } + fn current( + &self, + witness: &ResourceLoweringWitness, + function: &Function, + ) -> Result<(), String> { + let proof = witness + .source + .get_reflected_field_dispatch( + function.id, + self.original.type_info(), + self.original.arms(), + ) + .ok_or("Resource field dispatch lost its exact checked archive membership")?; + if proof != &self.original + || proof.function() != function.id + || self.guards.len() != proof.arms().len() + || self.dispatches.len() != proof.arms().len() + || self.targets.len() != proof.arms().len() + || self.paths.len() != proof.arms().len() + || function.local(self.key.id) != Some(&self.key) + { + return Err( + "Resource field dispatch changed its sealed ordinal/header inventory".into(), + ); + } + let E::Field { base, .. } = &self.type_info.kind else { + return Err("Resource field dispatch lost its current field binder".into()); + }; + if !matches!(base.kind, E::Local(key) if key == self.key.id) || base.ty != self.key.ty { + return Err("Resource field dispatch changed its exact current field binder".into()); + } + let block = |id: BlockId| { + function + .blocks + .get(id.index() as usize) + .filter(|block| block.id == id) + .ok_or("Resource field dispatch lost its current block") + }; + let mut sites = BTreeSet::new(); + for (index, arm) in proof.arms().iter().enumerate() { + if proof.iteration_index(index) != Some(index) + || proof.arm_container_path(index) != Some(self.paths[index].as_slice()) + || !sites.insert(self.guards[index]) + || !sites.insert(self.dispatches[index]) + || !sites.insert(self.targets[index]) + { + return Err( + "Resource field dispatch reordered ordinals or changed original Scope paths" + .into(), + ); + } + let next = self + .guards + .get(index + 1) + .copied() + .unwrap_or(self.otherwise); + let expected = ordinal_guard(proof, &self.type_info, index)?; + if !matches!(&block(self.guards[index])?.terminator.kind, + TerminatorKind::Branch { condition, then_block, else_block } + if crate::breakpoint_regions::expressions_equal(condition, &expected) + && *then_block == self.dispatches[index] && *else_block == next) + { + return Err( + "Resource field dispatch changed its exact ordinal selecting guard".into(), + ); + } + let expected = ReflectedTypeDispatchArm { + iteration_index: index, + bound_type: arm.bound_type, + reflection_identity: arm.reflection_identity.clone(), + target: self.targets[index], + }; + if !matches!(&block(self.dispatches[index])?.terminator.kind, + TerminatorKind::ReflectedTypeDispatch { type_info, arms, otherwise } + if crate::breakpoint_regions::expressions_equal(type_info, &self.type_info) + && arms.as_slice() == [expected] && *otherwise == self.otherwise) + { + return Err( + "Resource field dispatch changed its exact one-arm body/reflection join".into(), + ); + } + } + if !sites.insert(self.otherwise) + || !matches!( + block(self.otherwise)?.terminator.kind, + TerminatorKind::Unreachable + ) + { + return Err("Resource field dispatch changed its refusing metadata boundary".into()); + } + Ok(()) + } +} + +pub(super) fn seal(witness: &mut ResourceLoweringWitness) { + witness.reflected_seal = witness.reflected_fields.clone(); +} +pub(super) fn current( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + if witness.reflected_fields != witness.reflected_seal { + return Err( + "Resource field dispatch changed its independent constructor membership seal".into(), + ); + } + for (index, row) in witness.reflected_fields.iter().enumerate() { + if witness.reflected_fields[..index] + .iter() + .any(|previous| previous.original == row.original) + { + return Err("Resource field dispatch duplicated an original checked occurrence".into()); + } + row.current(witness, function)?; + } + Ok(()) +} +pub(super) fn admits(witness: &ResourceLoweringWitness, block: BlockId) -> bool { + witness + .reflected_fields + .iter() + .any(|row| row.dispatches.contains(&block)) +} + +/// Ordinary reflected helpers and exact wrapper-only Scope bodies retain the +/// established ordinary sequence edit. The finite edit cannot introduce +/// Resource data, graph storage, constructor custody or runtime execution. +pub(super) fn ordinary_sequence_transition( + function: &mut Function, + before: &Function, + edit: &crate::breakpoint_regions::SequenceEdit, + types: &TypeInterner, +) -> Result<(), String> { + let original = before + .resource_lowering + .as_ref() + .ok_or("Ordinary reflected sequence lost its original Source proof")?; + original.current(before)?; + if has_execution_records(before, types) + || (original.reflected_fields.is_empty() + && !entry_scope::capture_needed(&original.original, &original.source)) + || original.graph_metadata.needed() + || !original.constructors.is_empty() + || !original.constructor_seal.is_empty() + || !original.calls.is_empty() + || !original.regions.is_empty() + || !original.borrowed_sums.is_empty() + || !original.lexical_exits.is_empty() + || !function + .resource_lowering + .as_ref() + .is_some_and(|current| current.same(original)) + { + return Err("Ordinary reflected sequence cannot change a Resource custody witness".into()); + } + // This exact finite ordinary edit validator is also used by ordinary + // borrowed-sum and call-generation proof transitions. The shadow cannot + // install a new breakpoint, Scope, owner, provider or reflected proof. + let mut expected = function.clone(); + crate::breakpoint_regions::sequence_transition(&mut expected, before, edit.clone(), types)?; + if function.id != before.id + || function.identity != before.identity + || function.params != before.params + || function.capture_count != before.capture_count + || function.return_type != before.return_type + || function.entry != before.entry + || function.span != before.span + || function.debug_kind != before.debug_kind + || function.locals.len() < before.locals.len() + || function.locals[..before.locals.len()] != before.locals + || function.locals[before.locals.len()..].iter().any(|local| { + local.ty.index() as usize >= types.len() || resource_type_pending(types, local.ty) + }) + || function.blocks.iter().any(|block| { + walk::mir_block_has_resource(block) + || walk::mir_block_has_custody(block, function, types) + }) + { + return Err( + "Ordinary reflected sequence changed original headers or introduced custody".into(), + ); + } + let mut witness = original.clone(); + witness.locals = function.locals.clone(); + witness.blocks = function.blocks.clone(); + if let Some((body, prefix)) = &edit.prefix { + lexical_borrows::shift_prefix(&mut witness, *body, prefix.len()); + } + if original.descriptors.has_body() { + witness.descriptors.seal_body(function); + } + witness.current(function)?; + function.resource_lowering = Some(witness); + Ok(()) +} + +pub(super) fn remap_blocks( + witness: &mut ResourceLoweringWitness, + map: &[Option], +) -> Result<(), String> { + let remap = |id: &mut BlockId| -> Result<(), String> { + *id = map + .get(id.index() as usize) + .copied() + .flatten() + .ok_or("Resource field dispatch lost a live canonical ordinal block")?; + Ok(()) + }; + for row in &mut witness.reflected_fields { + for id in row + .guards + .iter_mut() + .chain(&mut row.dispatches) + .chain(&mut row.targets) + { + remap(id)?; + } + remap(&mut row.otherwise)?; + } + seal(witness); + Ok(()) +} +pub(super) fn remap_locals( + witness: &mut ResourceLoweringWitness, + remap: &mut impl FnMut(&mut LocalId), +) { + for row in &mut witness.reflected_fields { + remap(&mut row.key.id); + if let Some(id) = &mut row.key.view_source { + remap(id); + } + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(row.type_info.clone()), + span: row.type_info.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: row.type_info.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(value) = wrapper.statements.remove(0).kind { + row.type_info = value; + } + } + seal(witness); +} + +#[cfg(test)] +#[path = "reflected_fields_tests.rs"] +mod tests; diff --git a/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs b/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs new file mode 100644 index 00000000..b5bf7025 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/reflected_fields_tests.rs @@ -0,0 +1,379 @@ +use super::super::tests::{checked_support, lowered}; +use super::*; + +const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const SOURCE: &str = include_str!("reflected_field_source.jett"); + +fn main(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} +fn main_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap() +} + +fn reseal_current_graph(function: &mut Function) { + let copy = function.clone(); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.blocks = copy.blocks.clone(); + witness.descriptors.seal_body(©); + seal(witness); +} + +#[test] +fn reflected_field_original_equal_types_keep_distinct_checked_ordinals_and_scopes() { + let indexed = SOURCE.replace(" total = total + observed", " if field.index == 0:\n total = total + observed\n else:\n total = total + observed + 1").replace("total != 34", "total != 35"); + let named = SOURCE.replace(" total = total + observed", " if field.name == \"first\":\n total = total + observed\n else:\n total = total + observed + 1").replace("total != 34", "total != 35"); + for release in [false, true] { + for source in [SOURCE, indexed.as_str(), named.as_str()] { + let checked = checked_support(source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = main(&program); + let witness = function.resource_lowering.as_ref().unwrap(); + let [row] = witness.reflected_fields.as_slice() else { + panic!("exact original field binding"); + }; + assert_eq!(row.original.iteration_index(0), Some(0)); + assert_eq!(row.original.iteration_index(1), Some(1)); + assert_eq!( + row.original.arms()[0].bound_type, + row.original.arms()[1].bound_type + ); + assert_eq!( + row.original.arms()[0].reflection_identity, + row.original.arms()[1].reflection_identity + ); + assert_ne!(row.paths[0], row.paths[1]); + assert_ne!(row.targets[0], row.targets[1]); + assert_eq!(witness.borrowed_sums.len(), 2); + assert_ne!( + witness.borrowed_sums[0].alias(), + witness.borrowed_sums[1].alias() + ); + assert_eq!( + witness.borrowed_sums[0].backing(), + witness.borrowed_sums[1].backing() + ); + for dispatch in &row.dispatches { + let TerminatorKind::ReflectedTypeDispatch { arms, .. } = + &function.blocks[dispatch.index() as usize].terminator.kind + else { + panic!("selected one-arm dispatch"); + }; + assert_eq!(arms.len(), 1); + } + validate_resource_ownership(&program, types).unwrap(); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} + +#[test] +fn reflected_field_reordered_equal_type_guards_and_redirected_targets_cannot_reseal() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let original = lowered(&checked); + for mutation in 0..3 { + let mut edited = original.clone(); + let function = main_mut(&mut edited); + let row = function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields[0] + .clone(); + assert_eq!( + row.original.arms()[0].bound_type, + row.original.arms()[1].bound_type + ); + match mutation { + 0 => { + for (index, guard) in row.guards.iter().enumerate() { + let TerminatorKind::Branch { condition, .. } = + &mut function.blocks[guard.index() as usize].terminator.kind + else { + unreachable!(); + }; + *condition = + ordinal_guard(&row.original, &row.type_info, 1 - index).unwrap(); + } + } + 1 => { + let TerminatorKind::Branch { then_block, .. } = &mut function.blocks + [row.guards[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + *then_block = row.dispatches[1]; + } + 2 => { + let TerminatorKind::ReflectedTypeDispatch { arms, .. } = &mut function.blocks + [row.dispatches[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + arms[0].target = row.targets[1]; + } + _ => unreachable!(), + } + reseal_current_graph(function); + let expected = if mutation == 2 { + "exact one-arm body/reflection join" + } else { + "exact ordinal selecting guard" + }; + let reflected_message = + super::current(function.resource_lowering.as_ref().unwrap(), function).unwrap_err(); + assert!(reflected_message.contains(expected), "{reflected_message}"); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains(expected) + || (mutation != 0 + && message.contains( + "borrowed Resource Handle is disconnected from its exact selecting edge", + )), + "{message}" + ); + } + } +} + +#[test] +fn reflected_field_stale_scope_path_cannot_be_replaced_by_equal_type_body() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut edited = lowered(&checked); + let function = main_mut(&mut edited); + let witness = function.resource_lowering.as_mut().unwrap(); + witness.reflected_fields[0].paths[1] = witness.reflected_fields[0].paths[0].clone(); + seal(witness); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("reordered ordinals or changed original Scope paths"), + "{message}" + ); + } +} + +#[test] +fn reflected_field_public_hir_reordering_has_no_original_source_authority() { + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + for mutation in 0..3 { + let mut edited = original.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "main") + .unwrap(); + let statement = function + .body + .statements + .iter_mut() + .find(|statement| matches!(statement.kind, hir::StatementKind::For { .. })) + .unwrap(); + let hir::StatementKind::For { iterable, body, .. } = &mut statement.kind else { + unreachable!(); + }; + if mutation == 0 { + let hir::StatementKind::ReflectedTypeDispatch { arms, .. } = + &mut body.statements[0].kind + else { + unreachable!(); + }; + assert_eq!(arms[0].bound_type, arms[1].bound_type); + arms.swap(0, 1); + } else { + let hir::ExpressionKind::ListConstruct { elements } = &mut iterable.kind else { + unreachable!(); + }; + if mutation == 1 { + elements.swap(0, 1); + } else { + let hir::ExpressionKind::StructConstruct { fields, .. } = &mut elements[1].kind + else { + unreachable!(); + }; + let hir::ExpressionKind::StructConstruct { fields, .. } = &mut fields[9].kind + else { + unreachable!(); + }; + fields[0].kind = hir::ExpressionKind::String("foreign-int64".into()); + } + } + let message = crate::lower(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("original checked HIR archive"), + "{message}" + ); + } + } +} + +#[test] +fn reflected_field_plain_helper_gets_only_exact_archived_proof_without_runtime_scope() { + let helper = "function field_ordinal_total() returns int64:\n mutable int64 total = 0\n for field in type.fields[IterationShape]():\n comptime type Element = field.type_info:\n Element observed = 17\n if field.index == 0:\n total = total + observed\n else:\n total = total + observed + 1\n return total\n"; + let source = SOURCE.replace( + "export function main", + &format!("{helper}export function main"), + ); + for release in [false, true] { + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "field_ordinal_total") + .unwrap(); + let witness = function.resource_lowering.as_ref().unwrap(); + assert_eq!(witness.reflected_fields.len(), 1); + assert_eq!(witness.reflected_fields[0].original.arms().len(), 2); + assert_eq!( + witness.reflected_fields[0].original.arms()[0].bound_type, + witness.reflected_fields[0].original.arms()[1].bound_type + ); + assert!(!super::super::has_execution_records(function, types)); + let id = function.id; + let plan = validate_resource_ownership(&program, types).unwrap(); + assert!(plan.function(id).is_none()); + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + + let mut edited = program.clone(); + let function = edited + .functions + .iter_mut() + .find(|function| function.id == id) + .unwrap(); + let row = function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields[0] + .clone(); + let TerminatorKind::Branch { condition, .. } = &mut function.blocks + [row.guards[0].index() as usize] + .terminator + .kind + else { + unreachable!(); + }; + *condition = ordinal_guard(&row.original, &row.type_info, 1).unwrap(); + reseal_current_graph(function); + let message = validate_resource_ownership(&edited, types) + .unwrap_err() + .into_iter() + .map(|error| error.message) + .collect::>() + .join("\n"); + assert!( + message.contains("exact ordinal selecting guard"), + "{message}" + ); + } +} + +#[test] +fn reflected_field_plain_helper_preserves_mixed_ordinary_sequence_preparation() { + let head = "function field_ordinal_total() returns int64:\n mutable int64 total = 0\n for field in type.fields[IterationShape]():\n comptime type Element = field.type_info:\n Element observed = 17\n total = total + observed\n"; + let ordinary = [ + " for letter in \"ab\":\n total = total + 1\n", + " for key, value in map(1: 2):\n total = total + key + value\n", + " list[int64] values = list(1, 2)\n for observed in view values:\n total = total + observed\n", + " for impossible in list():\n total = total + 1\n", + ]; + for release in [false, true] { + for body in ordinary { + let helper = format!("{head}{body} return total\n"); + let source = SOURCE.replace( + "export function main", + &format!("{helper}export function main"), + ); + let checked = checked_support(&source, SUPPORT, release); + let types = &checked.checked().interner; + let mut program = lowered(&checked); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "field_ordinal_total") + .unwrap(); + assert_eq!( + function + .resource_lowering + .as_ref() + .unwrap() + .reflected_fields + .len(), + 1 + ); + assert!(!super::super::has_execution_records(function, types)); + let id = function.id; + crate::prepare_native_sequences(&mut program, types); + validate_resource_ownership(&program, types).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.id == id) + .unwrap(); + assert!( + !function + .blocks + .iter() + .any(|block| matches!(block.terminator.kind, TerminatorKind::ForEach { .. })), + "ordinary sequence preparation rolled back: {body}" + ); + assert!( + validate_resource_ownership(&program, types) + .unwrap() + .function(id) + .is_none() + ); + let once = program.clone(); + crate::prepare_native_sequences(&mut program, types); + assert_eq!(program, once); + } + } +} diff --git a/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs b/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs new file mode 100644 index 00000000..7828ac2b --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/required_descriptors_tests.rs @@ -0,0 +1,650 @@ +use super::*; +use crate::resource_ownership::tests::checked; +use crate::resource_ownership::tests::checked_support; +use jett_comptime::ExplicitComptimeValues; +use jett_comptime::checked_types::CheckedExpressionTypes; +use jett_typecheck::CheckedResourceProgram; +use std::{collections::HashMap, sync::Arc}; + +const FIXTURE_SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); +const PURE_CHOICE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/41_comptime_hook_pure_choice.jett" +); +const ABSENCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" +); + +fn required(checked: &Arc) -> ExplicitComptimeValues { + let expression_types = Arc::new(CheckedExpressionTypes { + resource_program: Some(checked.clone()), + expressions: checked + .checked() + .type_map + .iter() + .map(|(span, ty)| (*span, checked.checked().interner.type_name(*ty))) + .collect(), + ..Default::default() + }); + let captured = jett_comptime::evaluate_explicit_comptime_expressions_capture( + checked.module(), + Arc::new(jett_types::ReflectionMetadata::new()), + expression_types, + Arc::new(HashMap::new()), + ); + assert!( + captured.diagnostics.is_empty(), + "{:?}", + captured.diagnostics + ); + assert!(captured.debug_events.is_empty()); + captured.values +} + +fn materialized(checked: &Arc) -> hir::Program { + let mut program = hir::lower_checked_resource_program(checked).unwrap(); + hir::materialize_checked_required_values( + &mut program, + &required(checked), + &checked.checked().interner, + ) + .unwrap(); + program +} + +fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == name + }) + .unwrap() +} + +#[test] +fn resource_required_descriptors_preserve_checked_values_and_exact_current_occurrences() { + let sources = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/33_comptime_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/34_comptime_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/35_comptime_hook_borrow.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/36_comptime_hook_alias.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/37_comptime_hook_relay.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/38_comptime_hook_unused.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/39_immediate_comptime_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/40_comptime_hook_absence.jett" + ), + PURE_CHOICE, + include_str!( + "../../../jett_driver/tests/native_conformance/resource/42_comptime_hook_generic.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/43_comptime_hook_scoped.jett" + ), + ]; + for release in [false, true] { + for (fixture, source) in sources.into_iter().enumerate() { + let checked = checked_support(source, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let program = crate::lower(&hir, types).unwrap(); + let ownership = validate_resource_ownership(&program, types).unwrap_or_else(|errors| { + panic!( + "required fixture {} release={release}: {errors:?}; main CFG={:#?}", + fixture + 33, + named(&program, "main").blocks + ) + }); + assert_eq!( + ownership.required_only_function_ids(), + hir.resource_source.required_only_function_ids() + ); + let function = named(&program, "main"); + let plan = ownership.function(function.id).unwrap(); + let original = ownership.original_source(function.id).unwrap(); + let mut original_required = false; + walk::hir_block(&original.body, &mut |value| { + original_required |= matches!(value.kind, E::Comptime { .. }); + }); + assert!(original_required); + let witness = function.resource_lowering.as_ref().unwrap(); + assert!(witness.descriptors.graph.is_some()); + for row in hir.resource_source.required_materializations() { + if row.function() != function.id || row.hook().is_none() { + continue; + } + assert!(witness.descriptors.values.iter().any(|value| { + crate::breakpoint_regions::expressions_equal(&value.original, row.original()) + && crate::breakpoint_regions::expressions_equal( + &value.current, + row.current(), + ) + && Some(&value.hook) == row.hook() + })); + // An archive value is data, never the current MIR occurrence. + assert!(plan.descriptor_value(row.current()).is_none()); + } + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if let E::ResourceHookValue { hook } = &value.kind { + assert_eq!(plan.descriptor_value(value), Some(hook)); + assert!(plan.descriptor_value(&value.clone()).is_none()); + } + }); + } + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for local in &function.locals { + if plan.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + assert!(!plan.owner_slots().iter().any(|slot| matches!( + slot.storage(), ResourceSlotStorage::Local { header } if header.id == local.id + ))); + } + } + for required_only in ownership.required_only_function_ids() { + let helper = &program.functions[required_only.index() as usize]; + assert_eq!(helper.id, *required_only); + assert!(ownership.function(*required_only).is_none()); + assert!(ownership.original_source(*required_only).is_some()); + let descriptors = &helper.resource_lowering.as_ref().unwrap().descriptors; + assert!(descriptors.graph.is_some()); + assert!(descriptors.returned.is_none()); + assert!(descriptors.values.is_empty()); + assert!(descriptors.bindings.is_empty()); + } + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} + +#[test] +fn resource_required_descriptors_reject_unmaterialized_and_foreign_archive_substitution() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let original = hir::lower_checked_resource_program(&checked).unwrap(); + let mut current = materialized(&checked); + current.resource_source = original.resource_source; + assert!(crate::lower(¤t, types).is_err()); + + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + validate_resource_ownership(&baseline, types).unwrap(); + let foreign = self::checked(SOURCE, release); + let foreign_hir = materialized(&foreign); + let foreign_mir = crate::lower(&foreign_hir, &foreign.checked().interner).unwrap(); + let mut changed = baseline.clone(); + let function = named_mut(&mut changed, "scenario"); + function.resource_lowering.as_mut().unwrap().source = named(&foreign_mir, "scenario") + .resource_lowering + .as_ref() + .unwrap() + .source + .clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_only_helpers_refuse_resealed_control_flow_and_runtime_authority() { + for release in [false, true] { + let checked = checked_support(PURE_CHOICE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let helper_id = named(&baseline, "choose").id; + assert!(ownership.required_only_function_ids().contains(&helper_id)); + let mut changed = baseline.clone(); + let helper = named_mut(&mut changed, "choose"); + let branch = helper + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + *then_block = *else_block; + } + helper.resource_lowering.as_mut().unwrap().blocks = helper.blocks.clone(); + assert!(!crate::sequences::prune::unreachable(helper)); + assert!(validate_resource_ownership(&changed, types).is_err()); + + let mut changed = baseline.clone(); + let hook = named(&baseline, "main") + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .bindings[0] + .value + .hook + .clone(); + named_mut(&mut changed, "choose") + .resource_lowering + .as_mut() + .unwrap() + .descriptors + .returned = Some(hook); + assert!(validate_resource_ownership(&changed, types).is_err()); + + let mut changed = baseline.clone(); + named_mut(&mut changed, "choose").resource_lowering = None; + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_scalar_body_refuses_resealed_branch_before_canonical_pruning() { + const SCALAR: &str = r#"namespace app +export function main(net: Network) returns nothing: + use resource_probe + bool stop = comptime true + if stop: + int64 ignored = resource_probe.terminal_label() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let checked = checked(SCALAR, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let mut changed = crate::lower(&hir, types).unwrap(); + validate_resource_ownership(&changed, types).unwrap(); + let main = named_mut(&mut changed, "main"); + let descriptors = &main.resource_lowering.as_ref().unwrap().descriptors; + assert!(descriptors.values.is_empty()); + assert!(descriptors.returned.is_none()); + assert!( + hir.resource_source + .required_materializations() + .iter() + .any(|row| { + row.function() == main.id && matches!(row.current().kind, E::Bool(true)) + }) + ); + let branch = main + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + assert_ne!(*then_block, *else_block); + *then_block = *else_block; + } + main.resource_lowering.as_mut().unwrap().blocks = main.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let accepted = crate::sequences::prune::unreachable(named_mut(&mut changed, "main")); + assert!(!accepted); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_absence_plans_exact_tag_arms_and_refuses_disconnected_custody() { + for release in [false, true] { + let checked = checked_support(ABSENCE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let main = named(&baseline, "main"); + let plan = ownership.function(main.id).unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::CreateAbsentSum { .. } + ))); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::SumTake { success: true, .. } + ))); + let close = baseline + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "close" + }) + .unwrap(); + assert!(plan.operations().iter().any(|operation| matches!( + operation.role(), ResourceOperationRole::InvokeSourceFunction { function, .. } + if *function == close.id + ))); + assert!( + ownership + .function(close.id) + .unwrap() + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Close { .. })) + ); + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let branch = main + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { condition, .. } = &mut branch.terminator.kind { + condition.kind = E::Bool(false); + } + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("wrong selecting tag admitted; release={release}"), + }; + assert!( + error.contains("Resource SumTake has no exact selecting typed tag edge"), + "{error}" + ); + + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let mut disconnected = main + .blocks + .iter() + .find(|block| { + let mut close_call = false; + walk::mir_block(block, &mut |value| { + close_call |= + matches!(value.kind, E::Call { function, .. } if function == close.id); + }); + close_call + }) + .unwrap() + .clone(); + assert!(walk::mir_block_has_custody(&disconnected, main, types)); + disconnected.id = BlockId(main.blocks.len() as u32); + let disconnected_id = disconnected.id; + main.blocks.push(disconnected); + assert!( + !ControlFlowGraph::analyze(main) + .unwrap() + .reverse_postorder() + .contains(&disconnected_id) + ); + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("disconnected Resource close admitted; release={release}"), + }; + // The Return's lexical-exit query authenticates the entire current graph + // before flow reaches its final disconnected-custody walk. + assert_eq!( + error, + "Resource ownership differs from its initially authenticated Source or constructor-emitted graph" + ); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_absence_refuses_disconnected_sum_take_header() { + for release in [false, true] { + let checked = checked_support(ABSENCE, FIXTURE_SUPPORT, release); + let types = &checked.checked().interner; + let baseline = crate::lower(&materialized(&checked), types).unwrap(); + validate_resource_ownership(&baseline, types).unwrap(); + let mut changed = baseline.clone(); + let main = named_mut(&mut changed, "main"); + let mut disconnected = main + .blocks + .iter() + .find(|block| { + block.statements.iter().any(|statement| { + matches!(statement.kind, StatementKind::SumTake { success: true, .. }) + }) + }) + .unwrap() + .clone(); + assert!(walk::mir_block_has_custody(&disconnected, main, types)); + disconnected.id = BlockId(main.blocks.len() as u32); + let disconnected_id = disconnected.id; + main.blocks.push(disconnected); + assert!( + !ControlFlowGraph::analyze(main) + .unwrap() + .reverse_postorder() + .contains(&disconnected_id) + ); + let error = match crate::resource_ownership::flow::analyze( + &changed, + named(&changed, "main"), + types, + ) { + Err(error) => error, + Ok(_) => panic!("disconnected Resource SumTake header admitted; release={release}"), + }; + // The whole-body authentication refuses this disconnected typed header + // before flow reaches its final disconnected-custody walk. + assert_eq!( + error, + "Resource ownership differs from its initially authenticated Source or constructor-emitted graph" + ); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} + +#[test] +fn resource_required_header_detector_preserves_ordinary_sum_classification() { + const ORDINARY: &str = r#"namespace app +function ordinary() returns int64: + optional[int64] maybe = none + int64 number = maybe handle: + default 0 + return number +"#; + for release in [false, true] { + let checked = checked(ORDINARY, release); + let types = &checked.checked().interner; + let hir = hir::lower_checked_resource_program(&checked).unwrap(); + let program = crate::lower(&hir, types).unwrap(); + let function = named(&program, "ordinary"); + let mut header_count = 0; + for block in &function.blocks { + if block.statements.iter().any(|statement| { + matches!( + statement.kind, + StatementKind::SumTag { .. } | StatementKind::SumTake { .. } + ) + }) { + header_count += 1; + assert!(!walk::mir_block_has_custody(block, function, types)); + } + } + assert!(header_count > 0); + assert!( + validate_resource_ownership(&program, types) + .unwrap() + .function(function.id) + .is_none() + ); + } +} + +#[test] +fn resource_required_scalar_helper_refuses_changed_value_before_canonical_pruning() { + const HELPER: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/44_comptime_scalar_helper.jett" + ); + for release in [false, true] { + let checked = checked(HELPER, release); + let types = &checked.checked().interner; + let hir = materialized(&checked); + let baseline = crate::lower(&hir, types).unwrap(); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + let helper = named(&baseline, "selector"); + assert!(helper.resource_lowering.is_some()); + assert!(!has_execution_records(helper, types)); + assert!( + helper + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .graph + .is_some() + ); + assert!(ownership.function(helper.id).is_none()); + assert!(!ownership.required_only_function_ids().contains(&helper.id)); + assert!( + hir.resource_source + .required_materializations() + .iter() + .any(|row| { + row.function() == helper.id && matches!(row.current().kind, E::Bool(false)) + }) + ); + let mut compacted = baseline.clone(); + assert!(crate::sequences::prune::unreachable(named_mut( + &mut compacted, + "selector" + ))); + let compacted_plan = validate_resource_ownership(&compacted, types).unwrap(); + assert!(compacted_plan.function(helper.id).is_none()); + let original_helper = compacted_plan.original_source(helper.id).unwrap(); + assert!( + original_helper + .body + .statements + .iter() + .any(|statement| matches!( + &statement.kind, hir::StatementKind::Return(Some(value)) + if matches!(value.kind, E::Comptime { .. }) + )) + ); + let mut missing = baseline.clone(); + named_mut(&mut missing, "selector").resource_lowering = None; + assert!(validate_resource_ownership(&missing, types).is_err()); + let mut changed = baseline.clone(); + let helper = named_mut(&mut changed, "selector"); + let witness_present = helper.resource_lowering.is_some(); + let returned = helper + .blocks + .iter_mut() + .find_map(|block| match &mut block.terminator.kind { + TerminatorKind::Return(Some(value)) => Some(value), + _ => None, + }) + .unwrap(); + assert!(matches!(returned.kind, E::Bool(false))); + returned.kind = E::Bool(true); + helper.resource_lowering.as_mut().unwrap().blocks = helper.blocks.clone(); + let validation_accepted = validate_resource_ownership(&changed, types).is_ok(); + let transform_accepted = + crate::sequences::prune::unreachable(named_mut(&mut changed, "selector")); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !validation_accepted && !transform_accepted && !ownership_accepted, + "required scalar helper escaped authentication: witness present={witness_present}, validation accepted={validation_accepted}, transform accepted={transform_accepted}, ownership accepted={ownership_accepted}, release={release}" + ); + } +} + +const SOURCE: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing dispose = comptime resource_probe.descriptor() + resource_probe.TestHandle token = resource_probe.create(view net, 761) handle error: + return nothing + dispose(token) + return nothing +"#; + +#[test] +fn resource_required_descriptor_public_hook_substitution_does_not_mint_materialization() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let mut program = hir::lower_checked_resource_program(&checked).unwrap(); + let archive = program.resource_source.clone(); + let hook = program + .resource_manifest + .hooks() + .find(|hook| hook.recipe() == jett_types::ResourceKernelRecipe::Finalize) + .unwrap() + .clone(); + let function = program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "scenario") + .unwrap(); + let value = function + .body + .statements + .iter_mut() + .find_map(|statement| match &mut statement.kind { + hir::StatementKind::Let { value, .. } + if matches!(value.kind, E::Comptime { .. }) => + { + Some(value) + } + _ => None, + }) + .unwrap(); + assert_eq!(value.ty, hook.function_type()); + value.kind = E::ResourceHookValue { hook }; + assert_eq!(program.resource_source, archive); + assert!(archive.functions().iter().any(|function| { + let mut original_required = false; + walk::hir_block(&function.body, &mut |value| { + original_required |= matches!(value.kind, E::Comptime { .. }); + }); + original_required + })); + let errors = crate::lower(&program, types).unwrap_err(); + assert!(errors.iter().any(|error| { + error + .message + .contains("Resource lowering differs from its original checked HIR archive") + })); + } +} diff --git a/crates/jett_mir/src/resource_ownership/returned_descriptors.rs b/crates/jett_mir/src/resource_ownership/returned_descriptors.rs new file mode 100644 index 00000000..0e7ac8cf --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/returned_descriptors.rs @@ -0,0 +1,731 @@ +//! Exact returned hook metadata, retained independently of the current CFG. +//! A callable type never selects a hook; only its authenticated producer does. +use super::*; +use hir::ExpressionKind as E; + +#[derive(Debug, Clone, PartialEq)] +pub(super) struct DescriptorValue { + original: Expression, + current: Expression, + hook: hir::ResourceHookRef, +} +#[derive(Debug, Clone, PartialEq)] +pub(super) struct DescriptorBinding { + original: Local, + current: Local, + value: DescriptorValue, +} +#[derive(Debug, Clone, Default, PartialEq)] +pub(super) struct DescriptorWitness { + graph: Option, + returned: Option, + returns: Vec, + bindings: Vec, + values: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +struct DescriptorGraph { + parameters: Vec, + locals: Vec, + blocks: Vec, + entry: BlockId, +} +fn effects_only( + block: &hir::Block, + function: &hir::Function, + manifest: &hir::ResourceManifest, +) -> bool { + let metadata_type = |ty| manifest.hooks().any(|hook| hook.function_type() == ty); + let metadata_local = |local: LocalId| { + function + .locals + .get(local.index() as usize) + .is_some_and(|header| header.id == local && metadata_type(header.ty)) + }; + for statement in &block.statements { + match &statement.kind { + hir::StatementKind::Return(_) + | hir::StatementKind::Respond(_) + | hir::StatementKind::Break + | hir::StatementKind::Continue => return false, + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + if !effects_only(then_block, function, manifest) + || else_block + .as_ref() + .is_some_and(|block| !effects_only(block, function, manifest)) + { + return false; + } + } + hir::StatementKind::Scope(block) => { + if !effects_only(block, function, manifest) { + return false; + } + } + hir::StatementKind::While { .. } + | hir::StatementKind::For { .. } + | hir::StatementKind::Match { .. } + | hir::StatementKind::ReflectedTypeDispatch { .. } => return false, + hir::StatementKind::Trace(local) if metadata_local(*local) => return false, + hir::StatementKind::Breakpoint { bindings, .. } + if bindings.iter().any(|local| metadata_local(*local)) => + { + return false; + } + _ => {} + } + } + let mut valid = true; + walk::hir_block(block, &mut |value| { + valid &= !metadata_type(value.ty) + && !matches!( + value.kind, + E::ResourceHookValue { .. } | E::InlineFunction { .. } | E::ClosureRef { .. } + ); + if let E::Handle { failure, .. } = &value.kind { + valid &= effects_only(failure, function, manifest); + } + }); + valid +} +fn original_value( + value: &Expression, + locals: &BTreeMap, + summaries: &[Option], + manifest: &hir::ResourceManifest, + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) -> Option { + if let Some(hook) = required_hook(function, archive, value) { + return (manifest.contains_hook(hook) && hook.function_type() == value.ty) + .then(|| hook.clone()); + } + let hook = match &value.kind { + E::ResourceHookValue { hook } if manifest.contains_hook(hook) => hook.clone(), + E::Local(local) => locals.get(&local.index())?.clone(), + E::Clone(inner) if inner.ty == value.ty && inner.span == value.span => { + original_value(inner, locals, summaries, manifest, function, archive)? + } + E::Call { + function, + ownership: hir::CallOwnership::Source(source), + .. + } if (matches!(&source.target, hir::CallTarget::Function(id) if id == function) + || matches!(&source.target, hir::CallTarget::Declaration { .. })) + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && source + .arguments + .iter() + .all(|argument| argument.staging == hir::ArgumentStaging::Original) => + { + summaries.get(function.index() as usize)?.clone()? + } + _ => return None, + }; + (hook.function_type() == value.ty).then_some(hook) +} + +fn required_original<'a>( + function: &hir::Function, + archive: &'a hir::ResourceSourceArchive, + value: &Expression, +) -> Option<&'a Expression> { + archive.required_materializations().iter().find_map(|row| { + let hook = row.hook()?; + (row.function() == function.id + && row.identity() == &function.identity + && crate::breakpoint_regions::expressions_equal(row.current(), value) + && archive.materialized_hook(function.id, row.original(), value) == Some(hook)) + .then_some(row.original()) + }) +} + +fn required_hook<'a>( + function: &hir::Function, + archive: &'a hir::ResourceSourceArchive, + value: &Expression, +) -> Option<&'a hir::ResourceHookRef> { + let original = required_original(function, archive, value)?; + archive.materialized_hook(function.id, original, value) +} + +fn descriptor_value( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, + expression: &Expression, + hook: &hir::ResourceHookRef, +) -> DescriptorValue { + DescriptorValue { + original: required_original(function, archive, expression) + .unwrap_or(expression) + .clone(), + current: expression.clone(), + hook: hook.clone(), + } +} +fn immutable_local<'a>( + function: &'a hir::Function, + local: LocalId, + value: &Expression, +) -> Option<&'a Local> { + function + .locals + .get(local.index() as usize) + .filter(|header| { + header.id == local + && !header.mutable + && header.view_source.is_none() + && header.ty == value.ty + }) +} +fn root_summary( + function: &hir::Function, + summaries: &[Option], + manifest: &hir::ResourceManifest, + archive: &hir::ResourceSourceArchive, +) -> Option { + if archive.required_only_function_ids().contains(&function.id) + || function.capture_count != 0 + || function.source_definition.is_none() + || !function.identity.type_arguments.is_empty() + || !function.identity.scoped_type_bindings.is_empty() + || function.identity.specialization != Default::default() + { + return None; + } + let mut locals = BTreeMap::new(); + let mut returned = None; + let mut returns = 0; + for statement in &function.body.statements { + match &statement.kind { + hir::StatementKind::Let { local, value } + if immutable_local(function, *local, value).is_some() => + { + if let Some(hook) = + original_value(value, &locals, summaries, manifest, function, archive) + { + locals.insert(local.index(), hook); + } + } + hir::StatementKind::Return(Some(value)) => { + let hook = original_value(value, &locals, summaries, manifest, function, archive)?; + if function.return_type != hook.function_type() + || returned.as_ref().is_some_and(|old| old != &hook) + { + return None; + } + returned = Some(hook); + returns += 1; + } + hir::StatementKind::If { + then_block, + else_block, + .. + } => { + if !effects_only(then_block, function, manifest) + || else_block + .as_ref() + .is_some_and(|block| !effects_only(block, function, manifest)) + { + return None; + } + } + // Nested descriptor exits still need their own retained return transport. + hir::StatementKind::While { .. } + | hir::StatementKind::For { .. } + | hir::StatementKind::Scope(_) + | hir::StatementKind::Match { .. } + | hir::StatementKind::ReflectedTypeDispatch { .. } + | hir::StatementKind::Return(None) => return None, + _ => {} + } + } + (returns == 1).then_some(returned).flatten() +} +fn summaries(archive: &hir::ResourceSourceArchive) -> Vec> { + let mut summaries = vec![None; archive.functions().len()]; + let Some(manifest) = archive.manifest() else { + return summaries; + }; + // Relay chains are finite. Cycles cannot fabricate a descriptor base producer. + for _ in 0..archive.functions().len() { + let next: Vec<_> = archive + .execution_functions() + .iter() + .map(|function| root_summary(function, &summaries, manifest, archive)) + .collect(); + if next == summaries { + break; + } + summaries = next; + } + summaries +} +fn remember( + values: &mut Vec, + expression: &Expression, + hook: &hir::ResourceHookRef, + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) { + if !values + .iter() + .any(|value| crate::breakpoint_regions::expressions_equal(&value.current, expression)) + { + values.push(descriptor_value(function, archive, expression, hook)); + } + if let E::Clone(inner) = &expression.kind { + remember(values, inner, hook, function, archive); + } +} + +pub(super) fn capture( + function: &hir::Function, + archive: &hir::ResourceSourceArchive, +) -> DescriptorWitness { + if archive.required_only_function_ids().contains(&function.id) { + // The full lowering witness still authenticates this checked body. Its + // required-only role supplies no runtime descriptor occurrence or plan. + return DescriptorWitness::default(); + } + let summaries = summaries(archive); + let mut witness = DescriptorWitness { + returned: summaries + .get(function.id.index() as usize) + .cloned() + .flatten(), + ..Default::default() + }; + let Some(manifest) = archive.manifest() else { + return witness; + }; + let mut locals = BTreeMap::new(); + for statement in &function.body.statements { + if let hir::StatementKind::Let { local, value } = &statement.kind + && let Some(header) = immutable_local(function, *local, value) + && let Some(hook) = + original_value(value, &locals, &summaries, manifest, function, archive) + { + remember(&mut witness.values, value, &hook, function, archive); + locals.insert(local.index(), hook.clone()); + witness.bindings.push(DescriptorBinding { + original: header.clone(), + current: header.clone(), + value: descriptor_value(function, archive, value, &hook), + }); + } + if let hir::StatementKind::Return(Some(value)) = &statement.kind + && let Some(hook) = + original_value(value, &locals, &summaries, manifest, function, archive) + && witness.returned.as_ref() == Some(&hook) + { + remember(&mut witness.values, value, &hook, function, archive); + witness + .returns + .push(descriptor_value(function, archive, value, &hook)); + } + } + walk::hir_block(&function.body, &mut |value| { + if let E::ActorGraphStep { instruction, .. } = &value.kind + && matches!( + instruction.operation(), + hir::ActorGraphOperation::HookBox { .. } + ) + && let Some(hook) = required_hook(function, archive, value) + { + remember(&mut witness.values, value, hook, function, archive); + } + if let E::IndirectCall { + callee, + ownership: hir::CallOwnership::Source(source), + .. + } = &value.kind + && source.target + == (hir::CallTarget::Indirect { + signature_type: callee.ty, + }) + && source.bridge == hir::CallBridge::Direct + && source.generated_operands.is_empty() + && let Some(hook) = + original_value(callee, &locals, &summaries, manifest, function, archive) + { + remember(&mut witness.values, callee, &hook, function, archive); + } + }); + witness +} + +fn remap_expression(value: &mut Expression, remap: &mut impl FnMut(&mut LocalId)) { + let mut wrapper = BasicBlock { + id: BlockId(0), + statements: vec![Statement { + kind: StatementKind::Evaluate(value.clone()), + span: value.span, + }], + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: value.span, + }, + }; + crate::sequences::prune::block_locals(&mut wrapper, remap, &mut |_| {}); + if let StatementKind::Evaluate(remapped) = wrapper.statements.remove(0).kind { + *value = remapped; + } +} + +pub(super) fn remap_locals( + witness: &mut DescriptorWitness, + map: &[Option], + remap: &mut impl FnMut(&mut LocalId), +) { + if let Some(graph) = &mut witness.graph { + for parameter in &mut graph.parameters { + remap(&mut parameter.local); + } + graph.locals.retain(|local| { + map.get(local.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + for local in &mut graph.locals { + remap(&mut local.id); + if let Some(source) = &mut local.view_source { + remap(source); + } + } + for block in &mut graph.blocks { + crate::sequences::prune::block_locals(block, remap, &mut |_| {}); + } + } + for binding in &mut witness.bindings { + remap(&mut binding.current.id); + remap_expression(&mut binding.value.current, remap); + } + for value in witness.values.iter_mut().chain(&mut witness.returns) { + remap_expression(&mut value.current, remap); + } +} +pub(super) fn remap_blocks( + witness: &mut DescriptorWitness, + map: &[Option], +) -> Result<(), String> { + let Some(graph) = &mut witness.graph else { + return Ok(()); + }; + // Preflight every retained edge before deleting or renumbering any evidence. + // An unmapped original target must never survive as a coincidentally equal new ID. + let missing = std::cell::Cell::new(false); + let check = |id: &mut BlockId| { + if map.get(id.index() as usize).copied().flatten().is_none() { + missing.set(true); + } + }; + let mut entry = graph.entry; + check(&mut entry); + for block in &graph.blocks { + if map + .get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + { + let mut terminator = block.terminator.kind.clone(); + crate::sequences::prune::block_targets(&mut terminator, &check); + } + } + if missing.get() { + return Err( + "Resource descriptor canonical block map removes a retained entry or edge target" + .into(), + ); + } + let remap = |id: &mut BlockId| { + if let Some(mapped) = map.get(id.index() as usize).copied().flatten() { + *id = mapped; + } + }; + graph.blocks.retain(|block| { + map.get(block.id.index() as usize) + .copied() + .flatten() + .is_some() + }); + remap(&mut graph.entry); + for block in &mut graph.blocks { + remap(&mut block.id); + crate::sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + Ok(()) +} +fn mapped_value_valid(value: &DescriptorValue, witness: &ResourceLoweringWitness) -> bool { + if matches!(value.original.kind, E::Comptime { .. } | E::Constant { .. }) { + return witness.source.materialized_hook( + witness.original.id, + &value.original, + &value.current, + ) == Some(&value.hook); + } + if matches!(value.original.kind, E::Call { .. }) { + return witness.calls.iter().any(|call| { + crate::breakpoint_regions::expressions_equal(&call.original, &value.original) + && crate::breakpoint_regions::expressions_equal(&call.current, &value.current) + }); + } + let mut restored = value.current.clone(); + let mut valid = true; + remap_expression(&mut restored, &mut |local| { + if let Some(binding) = witness + .descriptors + .bindings + .iter() + .find(|binding| binding.current.id == *local) + { + *local = binding.original.id; + } else { + valid = false; + } + }); + valid && crate::breakpoint_regions::expressions_equal(&restored, &value.original) +} +fn header_same(original: &Local, current: &Local) -> bool { + let mut restored = current.clone(); + restored.id = original.id; + restored == *original +} + +pub(super) fn validate( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result<(), String> { + let expected = capture(&witness.original, &witness.source); + let current = &witness.descriptors; + current.current(function)?; + if (!witness.borrowed_sums.is_empty() + || super::entry_scope::capture_needed(&witness.original, &witness.source) + || witness + .source + .required_materializations() + .iter() + .any(|row| row.function() == function.id)) + && current.graph.is_none() + { + return Err( + "required Resource materialization lost its authenticated current body transport" + .into(), + ); + } + if witness + .source + .required_only_function_ids() + .contains(&function.id) + { + // No runtime operation or callable authority is projected for a + // required-only body. Source/header/type/call authentication ran first. + return (current.graph.is_some() + && current.returned.is_none() + && current.values.is_empty() + && current.bindings.is_empty() + && current.returns.is_empty()) + .then_some(()) + .ok_or_else(|| { + "required-only Resource helper acquired runtime descriptor authority".into() + }); + } + if current.graph.is_none() && (!current.values.is_empty() || current.returned.is_some()) { + return Err("Resource descriptor lost its authenticated current body transport".into()); + } + if current.returned != expected.returned + || current.bindings.len() != expected.bindings.len() + || current.values.len() != expected.values.len() + || current.returns.len() != expected.returns.len() + { + return Err("Resource descriptor lost its exact archived return or producer family".into()); + } + for (binding, original) in current.bindings.iter().zip(&expected.bindings) { + if binding.original != original.original + || binding.value.original != original.value.original + || binding.value.hook != original.value.hook + || !header_same(&binding.original, &binding.current) + || function.local(binding.current.id) != Some(&binding.current) + || !mapped_value_valid(&binding.value, witness) + { + return Err( + "Resource descriptor changed its exact immutable producer header or Source tuple" + .into(), + ); + } + let count = function.blocks.iter().flat_map(|block| &block.statements).filter(|statement| { + matches!(&statement.kind, StatementKind::Let { local, value } + if *local == binding.current.id && crate::breakpoint_regions::expressions_equal(value, &binding.value.current)) + }).count(); + if count != 1 { + return Err("Resource descriptor has no unique current immutable producer".into()); + } + } + for (value, original) in current.values.iter().zip(&expected.values) { + if value.original != original.original + || value.hook != original.hook + || !mapped_value_valid(value, witness) + { + return Err( + "Resource descriptor differs from its archived hook, producer or alias occurrence" + .into(), + ); + } + } + for (value, original) in current.returns.iter().zip(&expected.returns) { + if value.original != original.original + || value.hook != original.hook + || !mapped_value_valid(value, witness) + { + return Err("Resource descriptor differs from its exact retained Return".into()); + } + let count = function + .blocks + .iter() + .filter(|block| { + matches!(&block.terminator.kind, TerminatorKind::Return(Some(actual)) + if crate::breakpoint_regions::expressions_equal(actual, &value.current)) + }) + .count(); + if count != 1 { + return Err("Resource descriptor return has no unique current Return transport".into()); + } + } + current_values(witness, function)?; + Ok(()) +} + +pub(super) fn current_values( + witness: &ResourceLoweringWitness, + function: &Function, +) -> Result, String> { + let mut values = BTreeMap::new(); + for proof in &witness.descriptors.values { + let mut count = 0; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + if crate::breakpoint_regions::expressions_equal(value, &proof.current) { + values.insert(std::ptr::from_ref(value).addr(), proof.hook.clone()); + count += 1; + } + }); + } + if count != 1 { + return Err( + "Resource descriptor lost its unique original/current value occurrence".into(), + ); + } + } + let mut escaped = false; + for block in &function.blocks { + walk::mir_block(block, &mut |value| { + for binding in &witness.descriptors.bindings { + escaped |= match &value.kind { + E::Local(local) => { + *local == binding.current.id + && !values.contains_key(&std::ptr::from_ref(value).addr()) + } + E::ClosureRef { captures, .. } => captures.contains(&binding.current.id), + _ => false, + }; + } + escaped |= matches!(value.kind, E::ResourceHookValue { .. }) + && !values.contains_key(&std::ptr::from_ref(value).addr()); + }); + for statement in &block.statements { + let locals: &[LocalId] = match &statement.kind { + StatementKind::Trace(local) => std::slice::from_ref(local), + StatementKind::Breakpoint { bindings, .. } => bindings, + StatementKind::Assign { target, .. } if matches!(target.kind, E::Local(_)) => { + if let E::Local(local) = &target.kind { + std::slice::from_ref(local) + } else { + &[] + } + } + _ => &[], + }; + escaped |= witness + .descriptors + .bindings + .iter() + .any(|binding| locals.contains(&binding.current.id)); + } + } + if escaped { + return Err( + "Resource descriptor has an unproved current escape, mutation or producer".into(), + ); + } + Ok(values.into_iter().collect()) +} +impl DescriptorWitness { + pub(super) fn has_body(&self) -> bool { + self.graph.is_some() + } + pub(super) fn current(&self, function: &Function) -> Result<(), String> { + if let Some(graph) = &self.graph + && (graph.parameters != function.params + || graph.locals != function.locals + || graph.entry != function.entry + || !crate::breakpoint_regions::blocks_equal(&graph.blocks, &function.blocks)) + { + return Err("Resource descriptor body or headers differ from their authenticated current Source transport".into()); + } + Ok(()) + } + + pub(super) fn seal(&mut self, function: &Function) { + if self.returned.is_some() || !self.values.is_empty() { + self.seal_body(function); + } + } + pub(super) fn seal_body(&mut self, function: &Function) { + self.graph = Some(DescriptorGraph { + parameters: function.params.clone(), + locals: function.locals.clone(), + blocks: function.blocks.clone(), + entry: function.entry, + }); + } + pub(super) fn returned(&self) -> Option<&hir::ResourceHookRef> { + self.returned.as_ref() + } + pub(super) fn locals(&self) -> impl Iterator { + self.bindings + .iter() + .map(|binding| (binding.current.id, &binding.value.hook)) + } + pub(super) fn binding( + &self, + local: LocalId, + value: &Expression, + ) -> Option<&hir::ResourceHookRef> { + self.bindings.iter().find_map(|binding| { + (binding.current.id == local + && crate::breakpoint_regions::expressions_equal(&binding.value.current, value)) + .then_some(&binding.value.hook) + }) + } + pub(super) fn return_matches(&self, value: &Expression) -> bool { + self.returns + .iter() + .any(|proof| crate::breakpoint_regions::expressions_equal(&proof.current, value)) + } +} + +#[cfg(test)] +#[path = "returned_descriptors_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "required_descriptors_tests.rs"] +mod required_tests; diff --git a/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs b/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs new file mode 100644 index 00000000..b2d470ca --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/returned_descriptors_tests.rs @@ -0,0 +1,514 @@ +use super::*; +use crate::resource_ownership::tests::{checked, lowered}; + +const SOURCE: &str = r#"namespace app +function relay() returns function(resource_probe.TestHandle) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = resource_probe.descriptor() + function(resource_probe.TestHandle) returns nothing alias = selected + return alias +function scenario(view net: Network) returns nothing: + use resource_probe + function(resource_probe.TestHandle) returns nothing selected = relay() + function(resource_probe.TestHandle) returns nothing dispose = selected + resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error: + return nothing + dispose(token) + return nothing +"#; +fn scenario(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn scenario_mut(program: &mut Program) -> &mut Function { + program + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap() +} +fn indirect(function: &Function) -> &Expression { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Evaluate(value) if matches!(value.kind, E::IndirectCall { .. }) => { + Some(value) + } + _ => None, + }) + .unwrap() +} +#[test] +fn resource_returned_hook_preserves_alias_relay_and_indirect_authority() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, types).unwrap(); + let function = scenario(&program); + let plan = ownership.function(function.id).unwrap(); + let call = indirect(function); + let operation = plan + .operations_for_expression(call) + .find(|operation| operation.indirect_hook_target().is_some()) + .unwrap(); + let hook = operation.indirect_hook_target().unwrap(); + assert_eq!(hook.recipe(), jett_types::ResourceKernelRecipe::Finalize); + let ResourceOperationRole::InvokeHook { + source, operands, .. + } = operation.role() + else { + panic!("exact indirect hook operation"); + }; + assert_eq!( + source.target, + hir::CallTarget::Indirect { + signature_type: hook.function_type() + } + ); + assert_eq!(source.bridge, hir::CallBridge::Direct); + assert!(matches!( + &operands[..], + [ResourceCallOperand::Owned { parameter: 0, .. }] + )); + let E::IndirectCall { callee, .. } = &call.kind else { + unreachable!(); + }; + assert_eq!(plan.descriptor_value(callee), Some(hook)); + assert!(plan.descriptor_value(&callee.as_ref().clone()).is_none()); + assert!( + plan.operations_for_expression(&call.clone()) + .next() + .is_none() + ); + let companion = ResourceCompanionPlan::analyze(&ownership, function.id).unwrap(); + for local in &function.locals { + if plan.descriptor_local(local.id).is_some() { + assert!( + !companion + .storage() + .owned_locals + .contains(&(local.id.index() as usize)) + ); + assert!(!plan.owner_slots().iter().any(|slot| matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.id == local.id))); + } + } + for name in ["relay", "descriptor"] { + let returned = ownership + .functions() + .iter() + .find(|plan| plan.identity().declaration.name == name) + .unwrap(); + assert_eq!(returned.descriptor_return(), Some(hook)); + ResourceCompanionPlan::analyze(&ownership, returned.function()).unwrap(); + } + } +} +#[test] +fn resource_returned_hook_supports_immediate_and_unused_metadata() { + for release in [false, true] { + for body in [ + " resource_probe.TestHandle token = resource_probe.create(view net, 721) handle error:\n return nothing\n resource_probe.descriptor()(token)\n return nothing\n", + " function(resource_probe.TestHandle) returns nothing unused = resource_probe.descriptor()\n return nothing\n", + ] { + let source = format!( + "namespace app\nfunction scenario(view net: Network) returns nothing:\n use resource_probe\n{body}" + ); + let checked = checked(&source, release); + let program = lowered(&checked); + let ownership = + validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} +#[test] +fn resource_returned_hook_refuses_changed_retained_returns_and_resealed_escapes() { + for release in [false, true] { + let checked = checked(SOURCE, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let mut changed = baseline.clone(); + let function = scenario_mut(&mut changed); + let binding = function + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .bindings[0] + .clone(); + let escape = Expression { + kind: E::Local(binding.current.id), + ty: binding.current.ty, + span: binding.value.current.span, + }; + function.blocks[function.entry.index() as usize] + .statements + .push(Statement { + span: escape.span, + kind: StatementKind::Evaluate(escape), + }); + function.resource_lowering.as_mut().unwrap().blocks = function.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let mut changed = baseline.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "relay") + .unwrap(); + let block = function + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Return(Some(_)))) + .unwrap(); + if let TerminatorKind::Return(Some(value)) = &mut block.terminator.kind { + value.span = function.span; + } + function.resource_lowering.as_mut().unwrap().blocks = function.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let mut changed = baseline.clone(); + let function = scenario_mut(&mut changed); + function + .resource_lowering + .as_mut() + .unwrap() + .descriptors + .returned = baseline + .functions + .iter() + .find(|function| function.identity.declaration.name == "descriptor") + .unwrap() + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .returned + .clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} +use crate::resource_ownership::tests::{SUPPORT, checked_support}; +const DESCRIPTOR_SUPPORT: &str = r#"export function factory_descriptor() returns function(view Network, int64) returns result[TestHandle, string]: + return kernel_create +export function borrow_descriptor() returns function(view Network, view TestHandle) returns result[int64, string]: + return kernel_borrow +export function select_create(view net: Network, view anchor: TestHandle, fail_now: bool) returns function(view Network, int64) returns result[TestHandle, string]: + result[int64, string] observed = borrow(view net, view anchor) + if fail_now: + int64 ignored = terminal_label() + return kernel_create +"#; +const ALL_RECIPES: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + function(view Network, int64) returns result[resource_probe.TestHandle, string] acquire = resource_probe.factory_descriptor() + function(view Network, view resource_probe.TestHandle) returns result[int64, string] observe = resource_probe.borrow_descriptor() + function(resource_probe.TestHandle) returns nothing dispose = resource_probe.descriptor() + resource_probe.TestHandle token = acquire(view net, 731) handle error: + return nothing + result[int64, string] observed = observe(view net, view token) + dispose(token) + return nothing +"#; +#[test] +fn resource_returned_hook_proves_every_recipe_and_modes() { + for release in [false, true] { + let support = format!("{SUPPORT}{DESCRIPTOR_SUPPORT}"); + let checked = checked_support(ALL_RECIPES, &support, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let plan = ownership.function(scenario(&program).id).unwrap(); + let hooks: Vec<_> = plan + .operations() + .iter() + .filter_map(|operation| { + operation + .indirect_hook_target() + .map(|hook| (hook.recipe(), operation)) + }) + .collect(); + assert_eq!(hooks.len(), 3); + for (recipe, operation) in hooks { + let ResourceOperationRole::InvokeHook { + operands, result, .. + } = operation.role() + else { + panic!("exact hook operation"); + }; + match recipe { + jett_types::ResourceKernelRecipe::NetworkFactory => { + assert!(matches!(result, ResourceCallResult::Owned { .. })); + assert!( + operands + .iter() + .all(|operand| matches!(operand, ResourceCallOperand::Ordinary { .. })) + ); + } + jett_types::ResourceKernelRecipe::NetworkBorrow => { + assert!(matches!(result, ResourceCallResult::Ordinary { .. })); + assert!(operands.iter().any(|operand| matches!( + operand, + ResourceCallOperand::Borrowed { parameter: 1, .. } + ))); + } + jett_types::ResourceKernelRecipe::Finalize => { + assert!(matches!( + &operands[..], + [ResourceCallOperand::Owned { parameter: 0, .. }] + )); + } + } + } + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } +} +#[test] +fn resource_returned_hook_authenticates_callee_effects_after_actuals() { + for release in [false, true] { + let support = format!("{SUPPORT}{DESCRIPTOR_SUPPORT}"); + let source = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle anchor = resource_probe.create(view net, 732) handle error: + return nothing + resource_probe.TestHandle token = resource_probe.select_create(view net, view anchor, false)(view net, 733) handle error: + return nothing + resource_probe.close(token) + resource_probe.close(anchor) + return nothing +"#; + let checked = checked_support(source, &support, release); + let types = &checked.checked().interner; + let baseline = lowered(&checked); + let ownership = validate_resource_ownership(&baseline, types).unwrap(); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + let selected = baseline + .functions + .iter() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + assert_eq!( + ownership + .function(selected.id) + .unwrap() + .descriptor_return() + .unwrap() + .recipe(), + jett_types::ResourceKernelRecipe::NetworkFactory + ); + let mut changed = baseline.clone(); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let block = selected + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { condition, .. } = &mut block.terminator.kind { + condition.kind = E::Bool(true); + } + selected.resource_lowering.as_mut().unwrap().blocks = selected.blocks.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + } +} +#[test] +fn resource_returned_hook_shared_all_recipe_and_evaluation_order_sources_validate() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCES: [&str; 6] = [ + include_str!( + "../../../jett_driver/tests/native_conformance/resource/27_returned_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/28_returned_hook_borrow.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/29_immediate_returned_hook_close.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/31_immediate_returned_hook_callee_failure.jett" + ), + include_str!( + "../../../jett_driver/tests/native_conformance/resource/32_immediate_returned_hook_actual_failure.jett" + ), + ]; + for release in [false, true] { + for (index, source) in SOURCES.iter().enumerate() { + let checked = checked_support(source, SUPPORT, release); + let program = lowered(&checked); + let ownership = validate_resource_ownership(&program, &checked.checked().interner) + .unwrap_or_else(|errors| { + panic!("shared Source {} release {release}: {errors:?}", index + 27) + }); + let entry = program + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "main" + }) + .unwrap(); + assert!( + ownership + .function(entry.id) + .unwrap() + .operations() + .iter() + .any(|operation| operation.indirect_hook_target().is_some()) + ); + for plan in ownership.functions() { + ResourceCompanionPlan::analyze(&ownership, plan.function()).unwrap(); + } + } + } +} +#[test] +fn resource_returned_hook_refuses_resealed_body_before_canonical_pruning() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut changed = lowered(&checked); + validate_resource_ownership(&changed, types).unwrap(); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let branch = selected + .blocks + .iter_mut() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + if let TerminatorKind::Branch { + then_block, + else_block, + .. + } = &mut branch.terminator.kind + { + *then_block = *else_block; + } + selected.resource_lowering.as_mut().unwrap().blocks = selected.blocks.clone(); + assert!( + validate_resource_ownership(&changed, types).is_err(), + "untransformed copied body must be refused" + ); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let transform_accepted = crate::sequences::prune::unreachable(selected); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !transform_accepted && !ownership_accepted, + "canonical prune consumed a copied/resealed descriptor body: transform accepted={transform_accepted}, final ownership accepted={ownership_accepted}, release={release}" + ); + } +} +#[test] +fn resource_returned_hook_refuses_resealed_headers_before_canonical_local_pruning() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let types = &checked.checked().interner; + let mut changed = lowered(&checked); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let mut unproved = selected.locals[0].clone(); + unproved.id = LocalId::new(u32::try_from(selected.locals.len()).unwrap()); + unproved.name = "unproved_unused_header".into(); + unproved.ty = TypeInterner::BOOL; + unproved.debug_ty = TypeInterner::BOOL; + unproved.debug_type_name = None; + unproved.view_source = None; + unproved.mutable = false; + selected.locals.push(unproved); + selected.resource_lowering.as_mut().unwrap().locals = selected.locals.clone(); + assert!(validate_resource_ownership(&changed, types).is_err()); + let selected = changed + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let transform_accepted = crate::sequences::prune::unused_locals(selected); + let ownership_accepted = validate_resource_ownership(&changed, types).is_ok(); + assert!( + !transform_accepted && !ownership_accepted, + "canonical local prune consumed copied/resealed descriptor headers: transform accepted={transform_accepted}, final ownership accepted={ownership_accepted}, release={release}" + ); + } +} +#[test] +fn resource_returned_hook_refuses_unmapped_retained_descriptor_targets() { + const SUPPORT: &str = + include_str!("../../../jett_driver/tests/native_conformance/resource/resource_probe.jett"); + const SOURCE: &str = include_str!( + "../../../jett_driver/tests/native_conformance/resource/30_immediate_returned_hook_factory.jett" + ); + for release in [false, true] { + let checked = checked_support(SOURCE, SUPPORT, release); + let program = lowered(&checked); + let selected = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "select_create") + .unwrap(); + let mut proof = selected + .resource_lowering + .as_ref() + .unwrap() + .descriptors + .clone(); + let branch = selected + .blocks + .iter() + .find(|block| matches!(block.terminator.kind, TerminatorKind::Branch { .. })) + .unwrap(); + let TerminatorKind::Branch { then_block, .. } = branch.terminator.kind else { + unreachable!(); + }; + let mut map: Vec<_> = selected.blocks.iter().map(|block| Some(block.id)).collect(); + map[then_block.index() as usize] = None; + let before = proof.clone(); + assert!(remap_blocks(&mut proof, &map).is_err()); + assert_eq!( + proof, before, + "rejected mapping must preserve the retained evidence" + ); + } +} diff --git a/crates/jett_mir/src/resource_ownership/tests.rs b/crates/jett_mir/src/resource_ownership/tests.rs new file mode 100644 index 00000000..00b2cb92 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/tests.rs @@ -0,0 +1,824 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use jett_hir::ExpressionKind; +use jett_parser::{ast::Item, parse}; +use jett_resolve::ResourceKernelSpec; +use jett_typecheck::CheckedCallerEffect as Effect; +use jett_typecheck::{CheckOptions, CheckedResourceProgram}; +use jett_types::ResourceKernelRecipe; +use std::{collections::HashMap, sync::Arc}; + +pub(super) const SUPPORT: &str = r#"namespace resource_probe +export resource TestHandle +export function create(view net: Network, label: int64) returns result[TestHandle, string]: + return kernel_create(label: label, net: view net) +export function borrow(view net: Network, view token: TestHandle) returns result[int64, string]: + return kernel_borrow(view net, view token) +export function close(token: TestHandle) returns nothing: + kernel_close(token) + return nothing +export function empty() returns optional[TestHandle]: + return none +export function descriptor() returns function(TestHandle) returns nothing: + return kernel_close +export function terminal_label() returns int64: + list[int64] numbers = list(1) + list[int64] removed = list.__remove_at[int64](numbers, -1) + return 1 +"#; +const SOURCE: &str = r#"namespace app +function pass_owner(token: resource_probe.TestHandle) returns resource_probe.TestHandle: + return token +function pass_view(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function consume_pair(label: int64, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 1) handle error: + return nothing + resource_probe.TestHandle moved = pass_owner(token) + result[int64, string] observed = pass_view(view moved, view net) + resource_probe.close(moved) + return nothing +"#; +pub(super) fn checked(source: &str, release: bool) -> Arc { + checked_support(source, SUPPORT, release) +} +pub(super) fn checked_support( + source: &str, + support: &str, + release: bool, +) -> Arc { + let stdlib = FileId::new(10_000); + let project = FileId::new(0); + let mut parsed = parse(support, stdlib); + let primary = parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + let declaration = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Resource(resource) => Some(resource.name.span), + _ => None, + }) + .unwrap(); + parsed.module.items.extend(primary.module.items); + let catalog = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: declaration, + member: member.into(), + recipe, + }) + .collect::>(); + Arc::new( + CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (stdlib, SourceOrigin::Stdlib), + (project, SourceOrigin::Project), + ]), + &catalog, + CheckOptions { release }, + ) + .unwrap_or_else(|error| panic!("{error:?}; {:?}", error.diagnostics())), + ) +} +pub(super) fn lowered(checked: &Arc) -> Program { + let hir = hir::lower_checked_resource_program(checked).unwrap(); + lower(&hir, &checked.checked().interner).unwrap() +} +fn named<'a>(plan: &'a ResourceOwnershipPlan<'_>, name: &str) -> &'a ResourceFunctionPlan { + plan.functions() + .iter() + .find(|function| { + function.identity().declaration.namespace == "app" + && function.identity().declaration.name == name + }) + .expect("exact source test function") +} +#[test] +fn resource_ownership_source_factory_move_return_borrow_close_and_sum_cfg() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, types).unwrap(); + let scenario = named(&plan, "scenario"); + assert!(scenario.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::SumTake { success: true, .. } + ))); + assert!( + scenario + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Borrow { .. })) + ); + assert!(scenario.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::InvokeSourceFunction { .. } + ))); + let retained = named(&plan, "pass_view"); + assert!(retained.loans().iter().any(|loan| matches!( + loan.source(), + ResourceLoanSource::IncomingViewFormal { parameter: 0, .. } + ))); + assert!(retained.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::BoundedBorrowUse { parameter: 1, .. } + ))); + assert!( + !retained + .operations() + .iter() + .any(|operation| matches!(operation.role(), ResourceOperationRole::Acquire { .. })) + ); + let returned = named(&plan, "pass_owner"); + let return_frame = returned + .frames() + .iter() + .find(|frame| frame.role() == ResourceFrameRole::Return) + .unwrap(); + assert_eq!(returned.frames()[0].parent(), Some(return_frame.id())); + assert!(returned.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::CompleteReturnAfterCleanup { .. } + ))); + assert!( + plan.functions() + .iter() + .flat_map(|function| function.operations()) + .any(|operation| matches!(operation.role(), ResourceOperationRole::Close { .. })) + ); + // Metadata proof does not grant ordinary Move/Copy or native Resource admission. + let function = &mir.functions[scenario.function().index() as usize]; + assert!( + crate::move_values::MoveValuePlan::analyze(&mir, function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + assert!( + crate::copy_values::CopyValuePlan::analyze(function, types) + .unwrap_err() + .contains("pending ResourceOwnershipPlan") + ); + } +} +#[test] +fn resource_ownership_source_bare_to_view_consumes_one_call_holder_and_ends_its_loan() { + const SOURCE: &str = r#"namespace app +function read_once(view token: resource_probe.TestHandle, view net: Network) returns result[int64, string]: + use resource_probe + return resource_probe.borrow(view net, view token) +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 2) handle error: + return nothing + result[int64, string] observed = read_once(token, view net) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, &original.checked().interner).unwrap(); + let function = named(&plan, "scenario"); + let operations = function.operations(); + let (invoke, frame, loan) = operations + .iter() + .enumerate() + .find_map(|(index, operation)| { + let ResourceOperationRole::InvokeSourceFunction { + source, operands, .. + } = operation.role() + else { + return None; + }; + if !source + .arguments + .iter() + .any(|argument| argument.effect == Effect::RelinquishOwned) + { + return None; + } + let loan = operands.iter().find_map(|operand| { + if let ResourceCallOperand::Borrowed { parameter: 0, loan } = operand { + Some(*loan) + } else { + None + } + })?; + Some((index, operation.frame(), loan)) + }) + .expect("original bare owner to View formal"); + let source = function.loans()[loan.index()].source(); + let ResourceLoanSource::Owner(holder) = source else { + panic!("no owner from resident formal"); + }; + assert_eq!(function.owner_slots()[holder.index()].frame(), frame); + assert!(matches!( + function.owner_slots()[holder.index()].storage(), + ResourceSlotStorage::Argument { parameter: 0, .. } + )); + let ended = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::EndBorrow { loan: actual } if *actual == loan)).unwrap(); + let dropped = operations.iter().position(|operation| matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == holder)).unwrap(); + assert!(invoke < ended && ended < dropped); + } +} +#[test] +fn resource_ownership_source_named_order_holds_earlier_owner_for_later_terminal_actual() { + const SOURCE: &str = r#"namespace app +function consume_pair(label: int64, token: resource_probe.TestHandle) returns nothing: + use resource_probe + resource_probe.close(token) + return nothing +function scenario(view net: Network) returns nothing: + use resource_probe + resource_probe.TestHandle token = resource_probe.create(view net, 3) handle error: + return nothing + consume_pair(token: token, label: resource_probe.terminal_label()) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, &original.checked().interner).unwrap(); + let function = named(&plan, "scenario"); + let invocation = function.operations().iter().find(|operation| matches!(operation.role(), ResourceOperationRole::InvokeSourceFunction { evaluation_order, .. } if evaluation_order == &[1, 0])).unwrap(); + let ResourceOperationRole::InvokeSourceFunction { operands, .. } = invocation.role() else { + unreachable!() + }; + let holder = operands + .iter() + .find_map(|operand| { + if let ResourceCallOperand::Owned { parameter: 1, slot } = operand { + Some(*slot) + } else { + None + } + }) + .unwrap(); + assert_eq!( + function.owner_slots()[holder.index()].frame(), + invocation.frame() + ); + assert!(function.operations().iter().take(invocation.id().index()).any(|operation| matches!(operation.role(), ResourceOperationRole::Transfer { destination, .. } if *destination == holder))); + assert!( + function + .frames() + .iter() + .any(|frame| frame.id() == invocation.frame() + && frame.parent() == Some(ResourceFrameId(0))) + ); + // The actual failing Source helper is not executed here; its caller holder + // remains an operation cleanup destination, distinct from callee custody. + } +} +#[test] +fn resource_ownership_source_current_graph_and_original_witness_mutations_fail_closed() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mir = lowered(&original); + validate_resource_ownership(&mir, types).unwrap(); + for mutation in 0..7 { + let mut changed = mir.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + match mutation { + 0 => function.resource_lowering = None, + 1 => function.span.start += 1, + 2 => function.locals[0].mutable = !function.locals[0].mutable, + 3 => function.blocks[0].terminator.span.start += 1, + 4 => function.blocks[0].statements[0].span.start += 1, + 5 => function.identity.declaration.name.push_str("_foreign"), + 6 => changed.resource_manifest = hir::ResourceManifest::empty(), + _ => unreachable!(), + } + assert!( + validate_resource_ownership(&changed, types).is_err(), + "mutation {mutation}" + ); + } + } +} +#[test] +fn resource_ownership_source_checked_compaction_keeps_archive_and_dense_current_graph() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let mut mir = lowered(&original); + let archives = mir + .functions + .iter() + .filter_map(|function| { + function + .resource_lowering + .as_ref() + .map(|witness| (function.id, witness.original.clone())) + }) + .collect::>(); + crate::prepare_native_generated_functions(&mut mir, types); + validate_resource_ownership(&mir, types).unwrap(); + for (id, archived) in archives { + let witness = mir.functions[id.index() as usize] + .resource_lowering + .as_ref() + .unwrap(); + assert!(crate::breakpoint_regions::hir_blocks_equal( + &archived.body, + &witness.original.body + )); + assert_eq!(archived.locals, witness.original.locals); + for (index, local) in mir.functions[id.index() as usize].locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + } + } + } +} +#[test] +fn resource_ownership_bitwise_source_archive_and_constructor_capture_reject_replaced_nodes() { + let original = checked(SOURCE, false); + let types = &original.checked().interner; + let hir = hir::lower_checked_resource_program(&original).unwrap(); + let function = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "resource_probe" + && function.identity.declaration.name == "create" + }) + .unwrap(); + let mut capture = Capture::authenticated( + function, + &hir.resource_manifest, + &hir.resource_source, + types, + &authenticate_original(&hir, types).unwrap(), + ) + .unwrap(); + let hir::StatementKind::Return(Some(value)) = &function.body.statements[0].kind else { + panic!("source factory return"); + }; + assert!(capture.preserve_original_call(value, types).unwrap()); + let mut replaced = value.clone(); + replaced.span.start += 1; + assert!(capture.preserve_original_call(&replaced, types).is_err()); + let mut a = Expression { + kind: hir::ExpressionKind::Float(f64::from_bits(0x7ff8_0000_0000_0011)), + ty: TypeInterner::FLOAT64, + span: value.span, + }; + assert!(crate::breakpoint_regions::expressions_equal(&a, &a.clone())); + let mut b = a.clone(); + b.kind = hir::ExpressionKind::Float(f64::from_bits(0x7ff8_0000_0000_0012)); + assert!(!crate::breakpoint_regions::expressions_equal(&a, &b)); + a.kind = hir::ExpressionKind::Float(0.0); + b.kind = hir::ExpressionKind::Float(-0.0); + assert!(!crate::breakpoint_regions::expressions_equal(&a, &b)); +} +#[test] +fn resource_ownership_nested_region_uses_exact_retirement_without_fake_root_lifetime() { + const SOURCE: &str = r#"namespace app +function scenario(view net: Network) returns nothing: + use resource_probe + if true: + resource_probe.TestHandle token = resource_probe.create(view net, 4) handle error: + return nothing + resource_probe.close(token) + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let mir = lowered(&original); + let plan = validate_resource_ownership(&mir, &original.checked().interner).unwrap(); + let scenario = named(&plan, "scenario"); + let token = scenario.owner_slots().iter().find(|slot| { + matches!(slot.storage(), ResourceSlotStorage::Local { header } if header.name == "token") + }).expect("exact original nested owner slot"); + let ResourceSlotStorage::Local { header } = token.storage() else { + unreachable!() + }; + let function = &mir.functions[scenario.function().index() as usize]; + let exits = function + .blocks + .iter() + .flat_map(|block| { + (0..=block.statements.len()) + .filter_map(|index| { + let position = if index == block.statements.len() { + ResourcePosition::Terminator + } else { + ResourcePosition::Statement(index) + }; + scenario.lexical_exit(block.id, position) + }) + .collect::>() + }) + .collect::>(); + assert!( + exits + .iter() + .any(|exit| exit.clear_locals().contains(&header.id)) + ); + // Explicit close consumed this owner. The exact nested exit clears its + // binding, and neither lexical nor root completion can retire it again. + assert!(!scenario.operations().iter().any(|operation| { + matches!(operation.role(), ResourceOperationRole::Drop { source, .. } if *source == token.id()) + })); + validate_call_ownership(&mir, &original.checked().interner).unwrap(); + } +} + +#[test] +fn resource_ownership_original_hir_archive_cannot_authenticate_edited_body_or_empty_archive() { + for release in [false, true] { + let original = checked(SOURCE, release); + let types = &original.checked().interner; + let initial = hir::lower_checked_resource_program(&original).unwrap(); + for mutation in 0..3 { + let mut changed = initial.clone(); + if mutation == 0 { + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "scenario" + }) + .unwrap(); + let hir::StatementKind::Let { value, .. } = &mut function.body.statements[0].kind + else { + panic!("original token declaration"); + }; + let hir::ExpressionKind::Handle { target, .. } = &mut value.kind else { + panic!("original factory handle"); + }; + let hir::ExpressionKind::Call { args, .. } = &mut target.kind else { + panic!("original factory Source call"); + }; + let hir::ExpressionKind::Int(number) = &mut args[1].kind else { + panic!("original factory label occurrence"); + }; + *number += 1; + } else if mutation == 1 { + changed.resource_source = hir::ResourceSourceArchive::empty(); + } else { + changed.functions[0].debug_kind = hir::FunctionDebugKind::Inline; + } + // Existing public HIR gates do not themselves mint an original-body archive. + hir::validate(&changed).unwrap(); + hir::validate_backend_types(&changed, types).unwrap(); + assert!( + lower(&changed, types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("original checked HIR archive")) + ); + } + } +} +#[test] +fn resource_ownership_original_type_graph_detects_resource_field_removal_and_accepts_additions() { + const SOURCE: &str = r#"namespace app +struct Holder: + token: optional[resource_probe.TestHandle] +function inspect(holder: Holder) returns nothing: + return nothing +"#; + for release in [false, true] { + let original = checked(SOURCE, release); + let original_types = &original.checked().interner; + let hir = hir::lower_checked_resource_program(&original).unwrap(); + // Rebuild original nominal tables before copying their stable TypeIds. + let mut types = TypeInterner::new(); + let inspected = hir + .functions + .iter() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "inspect" + }) + .unwrap(); + let Type::Struct(id) = original_types.resolve(inspected.params[0].ty) else { + panic!("the checked inspect formal must be the Source Holder"); + }; + let id = *id; + let definition = original_types.resolve_struct(id).clone(); + assert!(matches!( + original_types.resolve(definition.fields[0].1), + Type::Optional(_) + )); + macro_rules! copy_nominals { + ($variant:ident, $resolve:ident, $add:ident) => { + let definitions = original_types + .type_ids() + .filter_map(|ty| match original_types.resolve(ty) { + Type::$variant(definition_id) => Some(( + definition_id.index(), + ( + *definition_id, + original_types.$resolve(*definition_id).clone(), + ), + )), + _ => None, + }) + .collect::>(); + for (definition_id, original_definition) in definitions.into_values() { + assert_eq!(types.$add(original_definition), definition_id); + } + }; + } + copy_nominals!(Struct, resolve_struct, add_struct); + copy_nominals!(Bitfield, resolve_bitfield, add_bitfield); + copy_nominals!(Enum, resolve_enum, add_enum); + copy_nominals!(Interface, resolve_interface, add_interface); + copy_nominals!(Actor, resolve_actor, add_actor); + copy_nominals!(Machine, resolve_machine, add_machine); + for ty in original_types.type_ids() { + assert_eq!(types.intern(original_types.resolve(ty).clone()), ty); + } + for ty in original_types.type_ids() { + let args = original_types.nominal_type_arguments(ty); + if !args.is_empty() { + types + .register_nominal_type_arguments(ty, args.to_vec()) + .unwrap(); + } + } + hir.resource_source.validate_types(&types).unwrap(); + types.intern(Type::Optional(TypeInterner::UINT64)); + hir.resource_source.validate_types(&types).unwrap(); + let mut erased = definition.clone(); + erased.fields.clear(); + types.update_struct(id, erased); + assert!( + hir.resource_source + .validate_types(&types) + .unwrap_err() + .contains("type meaning or nominal metadata") + ); + assert!( + lower(&hir, &types) + .unwrap_err() + .iter() + .any(|error| error.message.contains("type meaning or nominal metadata")) + ); + let mut foreign = definition; + foreign.fields[0].1 = TypeInterner::STRING; + types.update_struct(id, foreign); + assert!(hir.resource_source.validate_types(&types).is_err()); + } +} + +const MUTABLE_REPLACEMENT_SOURCE: &str = include_str!( + "../../../jett_codegen_cranelift/src/emit/resource_execution/replacement/mutable_assignment.jett" +); + +#[test] +fn resource_ownership_replacement_seals_the_actual_rhs_and_original_mutable_site() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let plan = validate_resource_ownership(&program, &original.checked().interner).unwrap(); + for name in ["replace_live", "failed_rhs_keeps_owner"] { + let function = named(&plan, name); + let replacements = function + .operations() + .iter() + .filter(|operation| { + matches!(operation.role(), ResourceOperationRole::Replace { .. }) + }) + .collect::>(); + assert_eq!(replacements.len(), 1, "{name}"); + let operation = replacements[0]; + let ResourceOperationRole::Replace { + destination, + replacement, + old, + } = operation.role() + else { + unreachable!(); + }; + assert_ne!(destination, replacement); + assert_eq!(*old, ResourceOccupancy::Occupied); + assert!(!operation.is_expression_operation()); + let current = &program.functions[function.function().index() as usize]; + let ResourcePosition::Statement(index) = operation.site().position() else { + panic!("assignment statement site"); + }; + let StatementKind::Assign { target, value } = + ¤t.blocks[operation.site().block().index() as usize].statements[index].kind + else { + panic!("exact original assignment"); + }; + let (ExpressionKind::Local(target), ExpressionKind::Local(rhs)) = + (&target.kind, &value.kind) + else { + panic!("canonical handled Source RHS and local destination"); + }; + let ResourceSlotStorage::Local { header } = + function.owner_slots()[destination.index()].storage() + else { + panic!("original mutable owning header"); + }; + assert_eq!(header.id, *target); + assert!(header.mutable); + assert_eq!( + function.owner_slots()[destination.index()].shape(), + function.owner_slots()[replacement.index()].shape() + ); + let ResourceSlotStorage::Local { header: rhs_header } = + function.owner_slots()[replacement.index()].storage() + else { + panic!("evaluated canonical RHS local"); + }; + assert_eq!(rhs_header.id, *rhs); + assert!(function.operations().iter().any(|other| other.site() == operation.site() + && matches!(other.role(), ResourceOperationRole::Transfer { source, destination: selected } + if source == replacement && selected == destination))); + } + } +} + +#[test] +fn resource_ownership_self_rebind_and_closed_destination_keep_distinct_exact_roles() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let plan = validate_resource_ownership(&program, &original.checked().interner).unwrap(); + let function = named(&plan, "rebind_self"); + let reseats = function + .operations() + .iter() + .filter(|operation| { + matches!(operation.role(), ResourceOperationRole::SelfRebind { .. }) + }) + .collect::>(); + assert_eq!(reseats.len(), 1); + let operation = reseats[0]; + let ResourceOperationRole::SelfRebind { slot } = operation.role() else { + unreachable!() + }; + let ResourceSlotStorage::Local { header } = function.owner_slots()[slot.index()].storage() + else { + panic!("owning local") + }; + assert!(header.mutable); + let current = &program.functions[function.function().index() as usize]; + let ResourcePosition::Statement(index) = operation.site().position() else { + panic!("assignment") + }; + let StatementKind::Assign { target, value } = + ¤t.blocks[operation.site().block().index() as usize].statements[index].kind + else { + panic!("self assignment") + }; + assert!(matches!(target.kind, ExpressionKind::Local(local) if local == header.id)); + assert!(matches!(value.kind, ExpressionKind::Local(local) if local == header.id)); + assert!( + !function + .operations() + .iter() + .any(|other| other.site() == operation.site() + && matches!( + other.role(), + ResourceOperationRole::Replace { .. } + | ResourceOperationRole::Transfer { .. } + | ResourceOperationRole::Drop { .. } + )) + ); + let closed = named(&plan, "rebind_after_close"); + assert!(!closed.operations().iter().any(|operation| matches!( + operation.role(), + ResourceOperationRole::Replace { .. } | ResourceOperationRole::SelfRebind { .. } + ))); + let current = &program.functions[closed.function().index() as usize]; + assert!(closed.operations().iter().any(|operation| { + let ResourcePosition::Statement(index) = operation.site().position() else { + return false; + }; + matches!( + current.blocks[operation.site().block().index() as usize].statements[index].kind, + StatementKind::Assign { .. } | StatementKind::UpdateField { .. } + ) && matches!(operation.role(), ResourceOperationRole::Transfer { .. }) + })); + } +} + +#[test] +fn resource_ownership_replacement_and_reseat_reject_changed_public_source_or_header() { + for release in [false, true] { + let original = checked(MUTABLE_REPLACEMENT_SOURCE, release); + let program = lowered(&original); + let types = &original.checked().interner; + for mutation in 0..3 { + let mut changed = program.clone(); + let function = changed + .functions + .iter_mut() + .find(|function| { + function.identity.declaration.namespace == "app" + && function.identity.declaration.name == "rebind_self" + }) + .unwrap(); + let assignment = function + .blocks + .iter_mut() + .flat_map(|block| &mut block.statements) + .find(|statement| { + matches!( + statement.kind, + StatementKind::Assign { .. } | StatementKind::UpdateField { .. } + ) + }) + .unwrap(); + let StatementKind::Assign { target, value } = &mut assignment.kind else { + unreachable!() + }; + let ExpressionKind::Local(local) = target.kind else { + unreachable!() + }; + if mutation == 0 { + function.locals[local.index() as usize].mutable = false; + } else if mutation == 1 { + value.kind = ExpressionKind::View(Box::new(value.clone())); + } else { + value.span.start += 1; + } + assert!(validate_resource_ownership(&changed, types).is_err()); + assert!(validate_witnesses(&changed, types).is_err()); + } + } +} + +// Shared source probe; each shape is an independently registered test. +fn check_direct_sum_self_reseat_shell(source: &str, selected: &str, result_shape: bool) { + for release in [false, true] { + let checked = checked(source, release); + let program = lowered(&checked); + let plan = validate_resource_ownership(&program, &checked.checked().interner).unwrap(); + let function = named(&plan, selected); + let reseats = function + .operations() + .iter() + .filter(|operation| { + matches!(operation.role(), ResourceOperationRole::SelfRebind { .. }) + }) + .collect::>(); + assert_eq!(reseats.len(), 1); + let operation = reseats[0]; + let ResourceOperationRole::SelfRebind { slot } = operation.role() else { + unreachable!() + }; + let shape = function.owner_slots()[slot.index()].shape(); + assert!(if result_shape { + matches!(shape, ResourceShape::Result { .. }) + } else { + matches!(shape, ResourceShape::Optional { .. }) + }); + assert!(!function.operations().iter().any(|other| { + other.site() == operation.site() + && matches!( + other.role(), + ResourceOperationRole::Replace { .. } + | ResourceOperationRole::Transfer { .. } + | ResourceOperationRole::Drop { .. } + ) + })); + } +} + +#[test] +fn resource_ownership_direct_sum_self_reseat_preserves_the_exact_shell_optional() { + check_direct_sum_self_reseat_shell( + include_str!("fixtures/direct_optional_self_reseat.jett"), + "reseat_optional", + false, + ); +} + +#[test] +fn resource_ownership_direct_sum_self_reseat_preserves_the_exact_shell_result() { + check_direct_sum_self_reseat_shell( + include_str!("fixtures/direct_result_self_reseat.jett"), + "reseat_result", + true, + ); +} diff --git a/crates/jett_mir/src/resource_ownership/walk.rs b/crates/jett_mir/src/resource_ownership/walk.rs new file mode 100644 index 00000000..fc5b2510 --- /dev/null +++ b/crates/jett_mir/src/resource_ownership/walk.rs @@ -0,0 +1,443 @@ +//! Dependency-only traversal; visits neither mint custody nor change context. +use super::*; +use hir::{ExpressionKind as E, StatementKind as H}; + +pub(super) fn expression(value: &Expression, visit: &mut impl FnMut(&Expression)) { + visit(value); + match &value.kind { + E::ActorGraphImport(plan) => hir_block(plan.body(), visit), + E::Binary { left, right, .. } => { + expression(left, visit); + expression(right, visit); + } + E::Unary { value, .. } + | E::ResultOk(value) + | E::ResultFail(value) + | E::OptionalSome(value) + | E::DisplayResult(value) + | E::EquatableResult(value) + | E::Declassify(value) + | E::Coarsen(value) + | E::RefinementValidated(value) + | E::InterfaceType(value) + | E::RuntimeFailureMessage(value) + | E::Run(value) + | E::Join(value) + | E::Cancel(value) + | E::View(value) + | E::Clone(value) + | E::Comptime { value, .. } + | E::InterfaceCoerce { value, .. } + | E::FunctionAdapter { value, .. } + | E::StateIs { value, .. } => expression(value, visit), + E::Call { args, .. } + | E::ResourceInvoke { args, .. } + | E::Intrinsic { args, .. } + | E::ActorSpawn { args, .. } + | E::ActorGraphStep { args, .. } => { + for value in args { + expression(value, visit); + } + } + E::IndirectCall { callee, args, .. } => { + expression(callee, visit); + for value in args { + expression(value, visit); + } + } + E::ActorMessage { actor, args, .. } => { + expression(actor, visit); + for value in args { + expression(value, visit); + } + } + E::StructConstruct { fields, .. } | E::BitfieldConstruct { fields, .. } => { + for value in fields { + expression(value, visit); + } + } + E::MachineConstruct { payloads, .. } | E::EnumConstruct { payloads, .. } => { + for value in payloads { + expression(value, visit); + } + } + E::MachineTransition { + source, payloads, .. + } => { + expression(source, visit); + for value in payloads { + expression(value, visit); + } + } + E::ListConstruct { elements } => { + for value in elements { + expression(value, visit); + } + } + E::MapConstruct { entries } => { + for entry in entries { + expression(&entry.key, visit); + expression(&entry.value, visit); + } + } + E::Handle { + target, failure, .. + } => { + expression(target, visit); + hir_block(failure, visit); + } + E::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + expression(value, visit); + } + } + } + E::InlineFunction { body, .. } => hir_block(body, visit), + E::Field { base, .. } => expression(base, visit), + E::Int(_) + | E::Float(_) + | E::String(_) + | E::Bool(_) + | E::Nothing + | E::Local(_) + | E::Constant { .. } + | E::FunctionRef(_) + | E::ResourceHookValue { .. } + | E::ClosureRef { .. } + | E::OptionalNone + | E::RuntimeFailure(_) + | E::PropertyCaseContext(_) => {} + } +} +pub(super) fn hir_block(block: &hir::Block, visit: &mut impl FnMut(&Expression)) { + for statement in &block.statements { + match &statement.kind { + H::Let { value, .. } + | H::HandleDefault(value) + | H::Expression(value) + | H::Respond(value) => expression(value, visit), + H::Assign { target, value } | H::UpdateField { target, value, .. } => { + expression(target, visit); + expression(value, visit); + } + H::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + H::If { + condition, + then_block, + else_block, + } => { + expression(condition, visit); + hir_block(then_block, visit); + if let Some(block) = else_block { + hir_block(block, visit); + } + } + H::While { condition, body } => { + expression(condition, visit); + hir_block(body, visit); + } + H::For { iterable, body, .. } => { + expression(iterable, visit); + hir_block(body, visit); + } + H::Match { scrutinee, arms } => { + expression(scrutinee, visit); + for arm in arms { + hir_block(&arm.body, visit); + } + } + H::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + H::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + H::Scope(block) => hir_block(block, visit), + H::ReflectedTypeDispatch { type_info, arms } => { + expression(type_info, visit); + for arm in arms { + hir_block(&arm.body, visit); + } + } + H::Break | H::Continue | H::Trace(_) => {} + } + } +} +pub(crate) fn mir_block(block: &BasicBlock, visit: &mut impl FnMut(&Expression)) { + for statement in &block.statements { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => expression(value, visit), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + expression(target, visit); + expression(value, visit); + } + StatementKind::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + ResourceConstructorNode::Begin { .. } + | ResourceConstructorNode::Commit { .. } + | ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => expression(value, visit), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } +} +pub(super) fn mir_block_has_custody( + block: &BasicBlock, + function: &Function, + types: &TypeInterner, +) -> bool { + let mut found = false; + mir_block(block, &mut |value| { + found |= resource_type_pending(types, value.ty) + && !matches!(types.resolve(value.ty), Type::Function { .. }); + }); + // Sums, sequence operations and Switch binders hold typed local headers + // rather than expression nodes. + // Normalized calls also own an exact operation prefix at header-only sites. + let occupied = |id| { + function + .local(id) + .is_some_and(|local| custody_type(types, local.ty)) + }; + found + || block + .statements + .iter() + .any(|statement| match &statement.kind { + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + occupied(*source) || occupied(*target) + } + StatementKind::SequenceLength { source, target } + | StatementKind::SequenceGet { source, target, .. } => { + occupied(source.root()) || occupied(*target) + } + StatementKind::IterationBorrow { source, .. } => occupied(source.root()), + StatementKind::ResourceCall(_) | StatementKind::ResourceConstructor(_) => true, + _ => false, + }) + || match &block.terminator.kind { + TerminatorKind::Switch { variants, .. } => variants + .iter() + .any(|(_, _, bindings)| bindings.iter().any(|local| occupied(*local))), + _ => false, + } +} +pub(super) fn mir_block_has_resource(block: &BasicBlock) -> bool { + let mut found = false; + mir_block(block, &mut |value| { + found |= matches!( + value.kind, + E::ResourceInvoke { .. } | E::ResourceHookValue { .. } + ); + }); + found +} + +/// Source scope admission is finite: no resource declaration gets a guessed root lifetime. +pub(super) fn nested_resource_declaration( + block: &hir::Block, + function: &hir::Function, + types: &TypeInterner, + nested: bool, + admitted: &[LocalId], +) -> bool { + for statement in &block.statements { + if let H::Let { local, .. } = &statement.kind + && nested + && !admitted.contains(local) + && function + .locals + .get(local.index() as usize) + .is_some_and(|local| resource_type_pending(types, local.ty)) + { + return true; + } + let found = match &statement.kind { + H::If { + then_block, + else_block, + .. + } => { + nested_resource_declaration(then_block, function, types, true, admitted) + || else_block.as_ref().is_some_and(|block| { + nested_resource_declaration(block, function, types, true, admitted) + }) + } + H::While { body, .. } | H::For { body, .. } | H::Scope(body) => { + nested_resource_declaration(body, function, types, true, admitted) + } + H::Match { arms, .. } => arms + .iter() + .any(|arm| nested_resource_declaration(&arm.body, function, types, true, admitted)), + H::ReflectedTypeDispatch { arms, .. } => arms + .iter() + .any(|arm| nested_resource_declaration(&arm.body, function, types, true, admitted)), + _ => false, + }; + if found { + return true; + } + let mut inside = false; + let single = hir::Block { + statements: vec![statement.clone()], + span: statement.span, + }; + hir_block(&single, &mut |value| { + if let E::Handle { failure, .. } = &value.kind { + inside |= nested_resource_declaration(failure, function, types, true, admitted); + } + }); + if inside { + return true; + } + } + false +} + +/// Visit only expressions physically owned by this exact current statement/terminator. +pub(super) fn mir_site(block: &BasicBlock, position: usize, visit: &mut impl FnMut(&Expression)) { + if let Some(statement) = block.statements.get(position) { + match &statement.kind { + StatementKind::ResourceConstructor(ResourceConstructorNode::Stage { + value, .. + }) + | StatementKind::ResourceCall(ResourceCallNode::Stage { value, .. }) + | StatementKind::Let { value, .. } + | StatementKind::BeginCallView { value, .. } + | StatementKind::CheckRefinement { call: value, .. } + | StatementKind::Evaluate(value) + | StatementKind::HandleDefault(value) => expression(value, visit), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + expression(target, visit); + expression(value, visit); + } + StatementKind::Assert { condition, message } => { + expression(condition, visit); + if let Some(value) = message { + expression(value, visit); + } + } + StatementKind::Breakpoint { condition, .. } => { + if let Some(value) = condition { + expression(value, visit); + } + } + StatementKind::ResourceCall( + ResourceCallNode::Begin { .. } + | ResourceCallNode::Invoke { .. } + | ResourceCallNode::End { .. }, + ) + | StatementKind::ResourceConstructor( + ResourceConstructorNode::Begin { .. } + | ResourceConstructorNode::Commit { .. } + | ResourceConstructorNode::End { .. }, + ) + | StatementKind::ResourceLexicalExit(_) + | StatementKind::OpenCallOwnerGeneration { .. } + | StatementKind::ReplaceCallOwnerGeneration { .. } + | StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } + | StatementKind::EndCallView { .. } + | StatementKind::ReflectedContainerReady { .. } + | StatementKind::ReflectedValueReady { .. } + | StatementKind::SequenceLength { .. } + | StatementKind::SequenceGet { .. } + | StatementKind::IterationBorrow { .. } + | StatementKind::SumTag { .. } + | StatementKind::SumTake { .. } + | StatementKind::Trace(_) => {} + } + } else if position == block.statements.len() { + match &block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + expression(value, visit); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } + | TerminatorKind::Switch { + scrutinee: value, .. + } + | TerminatorKind::ForEach { + iterable: value, .. + } + | TerminatorKind::ReflectedTypeDispatch { + type_info: value, .. + } => expression(value, visit), + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } + } +} diff --git a/crates/jett_mir/src/sequences.rs b/crates/jett_mir/src/sequences.rs new file mode 100644 index 00000000..d6b22474 --- /dev/null +++ b/crates/jett_mir/src/sequences.rs @@ -0,0 +1,587 @@ +//! Materialize native string, list, set, and map iterables once, before the loop backedge. +//! Consuming iteration takes initialized element places. Borrowed iteration +//! clones each element into a loop-local owner without moving the collection. +use super::*; +use jett_hir::{BinaryOp, ExpressionKind}; +use jett_types::{Type, TypeInterner}; +pub(crate) mod prune; +#[cfg(test)] +mod tests; +fn local(id: LocalId, ty: TypeId, span: Span) -> Expression { + Expression { + kind: ExpressionKind::Local(id), + ty, + span, + } +} +fn temporary(function: &mut Function, ty: TypeId, span: Span) -> LocalId { + let id = LocalId::new(function.locals.len() as u32); + function.locals.push(Local { + id, + name: format!("$iterator{}", id.index()), + ty, + debug_ty: ty, + debug_type_name: None, + mutable: true, + view_source: None, + span, + }); + id +} +fn projected_source( + value: &Expression, + function: &Function, + types: &TypeInterner, +) -> Option { + let mut path = Vec::new(); + let mut current = value; + loop { + match ¤t.kind { + ExpressionKind::Field { + base, + owner_type, + field, + } if matches!(types.resolve(*owner_type), Type::Struct(_)) => { + path.push(SequenceField { + owner_type: *owner_type, + field: *field, + }); + current = base; + } + ExpressionKind::View(inner) => current = inner, + ExpressionKind::Local(owner) if !path.is_empty() => { + path.reverse(); + if function.local(*owner)?.ty != path[0].owner_type { + return None; + } + return Some(SequenceSource::Projected { + owner: *owner, + path, + ty: value.ty, + }); + } + _ => return None, + } + } +} +pub fn prepare_native_sequences(program: &mut Program, types: &TypeInterner) { + if crate::actor_graph::validate_native_actor_graphs(program).is_err() { + return; + } + // This pass must not make malformed, unreachable input disappear before + // the caller reports its original validation errors. + if validate_call_ownership(program, types).is_err() { + return; + } + for function in &mut program.functions { + if crate::temporary_local_views::has_records(function) { + // Pending composition: this pass has no authenticated sequence edit + // replay for the temporary inventory. Keep the complete valid graph. + // An unprepared ForEach still receives the backend's existing refusal. + continue; + } + let before_pass = (crate::resource_ownership::has_records(function) + || function.reflected_callable.is_some() + || crate::ordinary_borrowed_sums::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + || crate::match_bindings::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); + let mut valid_regions = true; + let mut removed_uninhabited_body = false; + let count = function.blocks.len(); + for index in 0..count { + let header = function.blocks[index].id; + let TerminatorKind::ForEach { + key, + value: value_binding, + by_view, + iterable, + body, + exit, + } = function.blocks[index].terminator.kind.clone() + else { + continue; + }; + let Ok(iteration_plan) = crate::iteration_views::original_plan(function, header, types) + else { + continue; + }; + if iterable.ty.index() as usize >= types.len() { + continue; + } + let (element, map_value) = match types.resolve(iterable.ty) { + Type::List(element) | Type::Set(element) if value_binding.is_none() => { + (*element, None) + } + Type::Map(key, map_value) => (*key, value_binding.map(|_| *map_value)), + Type::String if value_binding.is_none() => (TypeInterner::STRING, None), + _ => continue, + }; + if element.index() as usize >= types.len() + || map_value.is_some_and(|ty| ty.index() as usize >= types.len()) + { + continue; + } + let uninhabited = match types.resolve(iterable.ty) { + Type::List(element) => *element == TypeInterner::NEVER, + Type::Map(key, value) => { + *key == TypeInterner::NEVER || *value == TypeInterner::NEVER + } + _ => false, + }; + // A preheader is the unique predecessor reachable from entry + // without crossing this header. Backedges are dominated by the + // header; disconnected blocks and numeric block order are irrelevant. + let Ok(cfg) = ControlFlowGraph::analyze(function) else { + continue; + }; + let mut outside = vec![false; function.blocks.len()]; + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if block == header || outside[block.index() as usize] { + continue; + } + outside[block.index() as usize] = true; + pending.extend_from_slice(cfg.successors(block)); + } + let candidates = cfg + .predecessors(header) + .iter() + .copied() + .filter(|id| outside[id.index() as usize]) + .collect::>(); + let [preheader] = candidates.as_slice() else { + continue; + }; + let preheader = preheader.index() as usize; + if !matches!(function.blocks[preheader].terminator.kind, TerminatorKind::Goto(target) if target == header) + { + continue; + } + let before_iteration = (crate::resource_ownership::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + || crate::match_bindings::has_records(function)) + .then(|| function.clone()); + let mut region_edit = crate::breakpoint_regions::SequenceEdit { + header, + before: function.blocks[index].terminator.clone(), + after: function.blocks[index].terminator.clone(), + append: Vec::new(), + prefix: None, + redirects: Vec::new(), + blocks: Vec::new(), + }; + let span = iterable.span; + let cursor = temporary(function, TypeInterner::INT64, span); + let length = temporary(function, TypeInterner::INT64, span); + let value = if let ExpressionKind::View(inner) = &iterable.kind { + *inner.clone() + } else { + iterable.clone() + }; + let projected = if matches!( + types.resolve(iterable.ty), + Type::List(_) | Type::Set(_) | Type::Map(..) | Type::String + ) { + projected_source(&value, function, types) + } else { + None + }; + // Field access implicitly views its parent. Iterate over that + // bounded projection and clone each loop binding as for `for view`. + let by_view = by_view || projected.is_some(); + let borrowed_local = if by_view { + if let ExpressionKind::Local(id) = value.kind { + Some(id) + } else { + None + } + } else { + None + }; + let mut init = Vec::new(); + let source = if let Some(source) = projected { + source + } else if let Some(id) = borrowed_local { + SequenceSource::Local(id) + } else { + let id = temporary(function, iterable.ty, span); + init.push(Statement { + kind: StatementKind::Let { local: id, value }, + span, + }); + SequenceSource::Local(id) + }; + if by_view { + init.push(Statement { + kind: StatementKind::IterationBorrow { + source: source.clone(), + token: cursor, + start: true, + }, + span, + }); + } + init.push(Statement { + kind: StatementKind::Let { + local: cursor, + value: Expression { + kind: ExpressionKind::Int(0), + ty: TypeInterner::INT64, + span, + }, + }, + span, + }); + init.push(Statement { + kind: StatementKind::SequenceLength { + source: source.clone(), + target: length, + }, + span, + }); + region_edit + .append + .push((BlockId(preheader as u32), init.clone())); + function.blocks[preheader].statements.extend(init); + if uninhabited { + // Keep evaluation and SequenceLength: it validates pending + // containers and projected ancestors even when no item exists. + // No runtime value can be extracted for the checked binder. + if by_view { + let end = Statement { + kind: StatementKind::IterationBorrow { + source, + token: cursor, + start: false, + }, + span, + }; + region_edit.append.push((header, vec![end.clone()])); + function.blocks[index].statements.push(end); + } + function.blocks[index].terminator.kind = TerminatorKind::Goto(exit); + region_edit.after = function.blocks[index].terminator.clone(); + if let Some(before_iteration) = &before_iteration + && (crate::resource_ownership::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::call_owner_generations::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::actor_handlers::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::field_updates::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::match_bindings::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::breakpoint_regions::sequence_transition( + function, + before_iteration, + region_edit, + types, + ) + .is_err()) + { + valid_regions = false; + break; + } + removed_uninhabited_body = true; + continue; + } + function.blocks[index].terminator.kind = TerminatorKind::Branch { + condition: Expression { + kind: ExpressionKind::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Less, + right: Box::new(local(length, TypeInterner::INT64, span)), + }, + ty: TypeInterner::BOOL, + span, + }, + then_block: body, + else_block: exit, + }; + let mut prefix = vec![ + Statement { + kind: StatementKind::SequenceGet { + consume: !by_view, + source: source.clone(), + index: cursor, + target: key, + part: if matches!(types.resolve(iterable.ty), Type::Map(..)) { + SequencePart::Key + } else { + SequencePart::Element + }, + }, + span, + }, + Statement { + kind: StatementKind::Assign { + target: local(cursor, TypeInterner::INT64, span), + value: Expression { + kind: ExpressionKind::Binary { + left: Box::new(local(cursor, TypeInterner::INT64, span)), + op: BinaryOp::Add, + right: Box::new(Expression { + kind: ExpressionKind::Int(1), + ty: TypeInterner::INT64, + span, + }), + }, + ty: TypeInterner::INT64, + span, + }, + }, + span, + }, + ]; + if let Some(value) = value_binding { + prefix.insert( + 1, + Statement { + kind: StatementKind::SequenceGet { + consume: !by_view, + source: source.clone(), + index: cursor, + target: value, + part: SequencePart::Value, + }, + span, + }, + ); + } + region_edit.prefix = Some((body, prefix.clone())); + prefix.append(&mut function.blocks[body.index() as usize].statements); + function.blocks[body.index() as usize].statements = prefix; + if by_view { + // Handler default may bypass the designated loop exit. Split + // every edge leaving this CFG region, ending only this token. + let mut pending = vec![exit]; + while let Some(block) = pending.pop() { + if block == header || outside[block.index() as usize] { + continue; + } + outside[block.index() as usize] = true; + pending.extend_from_slice(cfg.successors(block)); + } + let mut region = vec![false; outside.len()]; + let mut pending = vec![header]; + while let Some(block) = pending.pop() { + let i = block.index() as usize; + if outside[i] || region[i] { + continue; + } + region[i] = true; + pending.extend_from_slice(cfg.successors(block)); + } + for i in 0..region.len() { + if !region[i] { + continue; + } + for &target in cfg.successors(function.blocks[i].id) { + if region[target.index() as usize] { + continue; + } + let end = BlockId(function.blocks.len() as u32); + function.blocks.push(BasicBlock { + id: end, + statements: vec![Statement { + kind: StatementKind::IterationBorrow { + source: source.clone(), + token: cursor, + start: false, + }, + span, + }], + terminator: Terminator { + kind: TerminatorKind::Goto(target), + span, + }, + }); + region_edit.blocks.push( + function + .blocks + .last() + .expect("canonical edge split was appended") + .clone(), + ); + let old = function.blocks[i].terminator.clone(); + redirect_edge(&mut function.blocks[i].terminator.kind, target, end); + region_edit.redirects.push(( + function.blocks[i].id, + old, + function.blocks[i].terminator.clone(), + )); + } + } + } + region_edit.after = function.blocks[index].terminator.clone(); + // Redirect edits operate on the just-created canonical header, so + // its initial replacement precedes the exact redirect snapshots. + if let Some((_, old, _)) = region_edit + .redirects + .iter() + .find(|(block, _, _)| *block == header) + { + region_edit.after = old.clone(); + } + if let Some(before_iteration) = &before_iteration + && (crate::resource_ownership::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::ordinary_borrowed_sums::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::call_owner_generations::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::actor_handlers::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::field_updates::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::match_bindings::sequence_transition( + function, + before_iteration, + ®ion_edit, + types, + ) + .is_err() + || crate::breakpoint_regions::sequence_transition( + function, + before_iteration, + region_edit, + types, + ) + .is_err()) + { + valid_regions = false; + break; + } + if let Some(original) = iteration_plan { + crate::iteration_views::retain_prepared( + function, + original, + source, + cursor, + length, + BlockId(preheader as u32), + span, + ); + } + } + if !valid_regions + || crate::ordinary_borrowed_sums::validate_current(function).is_err() + || crate::resource_ownership::validate_current(function).is_err() + || (removed_uninhabited_body && !prune::unreachable(function)) + { + if let Some(before_pass) = before_pass { + *function = before_pass; + } + } else if let Some(before_pass) = before_pass { + if crate::reflected_callable::prepared(function, &before_pass).is_err() { + *function = before_pass; + } + } + } + for function in &mut program.functions { + crate::actor_graph::refresh(function); + } +} + +fn redirect_edge(kind: &mut TerminatorKind, old: BlockId, new: BlockId) { + let replace = |id: &mut BlockId| { + if *id == old { + *id = new; + } + }; + match kind { + TerminatorKind::Goto(id) => replace(id), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + replace(then_block); + replace(else_block); + } + TerminatorKind::ForEach { body, exit, .. } => { + replace(body); + replace(exit); + } + TerminatorKind::Switch { + variants, + otherwise, + .. + } => { + for (_, id, _) in variants { + replace(id); + } + if let Some(id) = otherwise { + replace(id); + } + } + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + for arm in arms { + replace(&mut arm.target); + } + replace(otherwise); + } + TerminatorKind::Return(_) | TerminatorKind::Respond(_) | TerminatorKind::Unreachable => {} + } +} diff --git a/crates/jett_mir/src/sequences/prune.rs b/crates/jett_mir/src/sequences/prune.rs new file mode 100644 index 00000000..116d2295 --- /dev/null +++ b/crates/jett_mir/src/sequences/prune.rs @@ -0,0 +1,632 @@ +//! Compact functions selected by bounded native preparation passes. +//! The input program is validated before each preparation; all references +//! below therefore name dense local/block tables, including unreachable input. +use super::*; + +pub(crate) fn unreachable(function: &mut Function) -> bool { + if crate::temporary_local_views::validate_current(function).is_err() { + return false; + } + if crate::resource_ownership::constructors::validate_nodes(function).is_err() { + return false; + } + let before = (crate::temporary_local_views::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function) + || crate::resource_ownership::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + || crate::match_bindings::has_records(function)) + .then(|| function.clone()); + let Ok(cfg) = ControlFlowGraph::analyze(function) else { + return false; + }; + let mut reachable = vec![false; function.blocks.len()]; + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if reachable[block.index() as usize] { + continue; + } + reachable[block.index() as usize] = true; + pending.extend_from_slice(cfg.successors(block)); + } + let mut blocks = vec![None; function.blocks.len()]; + function + .blocks + .retain(|block| reachable[block.id.index() as usize]); + for (index, block) in function.blocks.iter_mut().enumerate() { + let new = BlockId(index as u32); + blocks[block.id.index() as usize] = Some(new); + block.id = new; + } + let remap_block = |id: &mut BlockId| { + *id = blocks[id.index() as usize].expect("edge from reachable block has reachable target"); + }; + remap_block(&mut function.entry); + for block in &mut function.blocks { + block_targets(&mut block.terminator.kind, &remap_block); + } + function + .prepared_absent_successes + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .prepared_present_successes + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .prepared_view_iterations + .retain_mut(|record| record.remap_blocks(&blocks)); + function + .original_view_iterations + .retain_mut(|record| record.remap_blocks(&blocks)); + + if crate::match_bindings::remap_blocks(function, &blocks).is_err() + || before.as_ref().is_some_and(|before| { + crate::temporary_local_views::remap_blocks(function, before, &blocks).is_err() + || crate::actor_handlers::remap_blocks(function, before, &blocks).is_err() + || crate::field_updates::remap_blocks(function, before, &blocks).is_err() + || crate::call_owner_generations::remap_blocks(function, before, &blocks).is_err() + || crate::resource_ownership::remap_blocks(function, before, &blocks).is_err() + || crate::ordinary_borrowed_sums::remap_blocks(function, before, &blocks).is_err() + }) + || crate::breakpoint_regions::remap_blocks(function, &blocks).is_err() + || !unused_locals(function) + { + if let Some(before) = before { + *function = before; + } + return false; + } + true +} + +/// Remove absent frame slots without changing any original control-flow block. +/// Every parameter, binding, read, capture and debug or loan reference survives. +pub(crate) fn unused_locals(function: &mut Function) -> bool { + if crate::temporary_local_views::validate_current(function).is_err() { + return false; + } + if crate::resource_ownership::constructors::validate_nodes(function).is_err() { + return false; + } + if function.actor_message_source + || crate::actor_graph::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + { + return true; + } + let before = (crate::temporary_local_views::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function) + || crate::resource_ownership::has_records(function) + || crate::ordinary_borrowed_sums::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + || crate::match_bindings::has_records(function)) + .then(|| function.clone()); + let mut used = vec![false; function.locals.len()]; + for parameter in &function.params { + used[parameter.local.index() as usize] = true; + } + for block in &mut function.blocks { + block_locals( + block, + &mut |id| used[id.index() as usize] = true, + &mut |_| {}, + ); + } + loop { + let mut changed = false; + for local in &function.locals { + if used[local.id.index() as usize] + && let Some(source) = local.view_source + && !used[source.index() as usize] + { + used[source.index() as usize] = true; + changed = true; + } + } + if !changed { + break; + } + } + let mut locals = vec![None; function.locals.len()]; + function + .locals + .retain(|local| used[local.id.index() as usize]); + for (index, local) in function.locals.iter_mut().enumerate() { + let new = LocalId::new(index as u32); + locals[local.id.index() as usize] = Some(new); + local.id = new; + } + let mut remap_local = |id: &mut LocalId| { + *id = locals[id.index() as usize].expect("retained local reference was marked used"); + }; + let mut remap_floor = |floor: &mut u32| { + if (*floor as usize) <= locals.len() { + *floor = locals[..*floor as usize] + .iter() + .filter(|id| id.is_some()) + .count() as u32; + } + }; + for parameter in &mut function.params { + remap_local(&mut parameter.local); + } + for local in &mut function.locals { + if let Some(source) = &mut local.view_source { + remap_local(source); + } + } + for block in &mut function.blocks { + block_locals(block, &mut remap_local, &mut remap_floor); + } + // Certificate metadata creates no execution reads or otherwise dead slots. + function + .prepared_absent_successes + .retain_mut(|record| record.remap_locals(&locals)); + function + .prepared_present_successes + .retain_mut(|record| record.remap_locals(&locals)); + function + .prepared_view_iterations + .retain_mut(|record| record.remap_locals(&locals)); + function + .original_view_iterations + .retain_mut(|record| record.remap_locals(&locals)); + if crate::match_bindings::remap_locals(function, &locals).is_err() + || before.as_ref().is_some_and(|before| { + crate::temporary_local_views::remap_locals(function, before, &locals).is_err() + || crate::resource_ownership::remap_locals(function, before, &locals).is_err() + || crate::ordinary_borrowed_sums::remap_locals(function, before, &locals).is_err() + }) + || crate::call_owner_generations::remap_locals(function, &locals).is_err() + || crate::breakpoint_regions::remap_locals(function, &locals).is_err() + { + if let Some(before) = before { + *function = before; + } + return false; + } + true +} + +pub(crate) fn block_targets(kind: &mut TerminatorKind, visit: &impl Fn(&mut BlockId)) { + match kind { + TerminatorKind::Goto(target) => visit(target), + TerminatorKind::Branch { + then_block, + else_block, + .. + } => { + visit(then_block); + visit(else_block); + } + TerminatorKind::Switch { + variants, + otherwise, + .. + } => { + for (_, target, _) in variants { + visit(target); + } + if let Some(target) = otherwise { + visit(target); + } + } + TerminatorKind::ForEach { body, exit, .. } => { + visit(body); + visit(exit); + } + TerminatorKind::ReflectedTypeDispatch { + arms, otherwise, .. + } => { + for arm in arms { + visit(&mut arm.target); + } + visit(otherwise); + } + TerminatorKind::Return(_) | TerminatorKind::Respond(_) | TerminatorKind::Unreachable => {} + } +} + +fn source_local(source: &mut SequenceSource, visit: &mut impl FnMut(&mut LocalId)) { + match source { + SequenceSource::Local(local) | SequenceSource::Projected { owner: local, .. } => { + visit(local) + } + } +} + +pub(crate) fn block_locals( + block: &mut BasicBlock, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), +) { + visit_block_locals(block, visit, floor, true); +} + +/// Execution reads exclude structural caller provenance and staging witnesses. +pub(crate) fn block_runtime_locals( + block: &mut BasicBlock, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), +) { + visit_block_locals(block, visit, floor, false); +} + +fn visit_block_locals( + block: &mut BasicBlock, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), + metadata: bool, +) { + for statement in &mut block.statements { + match &mut statement.kind { + StatementKind::ResourceLexicalExit(_) => {} + StatementKind::ResourceConstructor(node) => match node { + crate::ResourceConstructorNode::Stage { value, .. } => { + expression(value, visit, floor, metadata) + } + crate::ResourceConstructorNode::Commit { output, .. } => visit(output), + _ => {} + }, + StatementKind::ResourceCall(node) => match node { + ResourceCallNode::Stage { + value, ordinary, .. + } => { + if let Some(local) = ordinary { + visit(local); + } + expression(value, visit, floor, metadata); + } + ResourceCallNode::Invoke { output, .. } => visit(output), + ResourceCallNode::Begin { .. } | ResourceCallNode::End { .. } => {} + }, + StatementKind::OpenCallOwnerGeneration { root, .. } => visit(root), + StatementKind::ReplaceCallOwnerGeneration { + root, rhs_owner, .. + } => { + visit(root); + visit(rhs_owner); + } + StatementKind::PrepareFieldUpdate { .. } + | StatementKind::CloseCallOwnerGeneration { .. } => {} + StatementKind::ReflectedContainerReady { source, .. } + | StatementKind::ReflectedValueReady { source, .. } => visit(source), + StatementKind::SequenceLength { source, target } => { + source_local(source, visit); + visit(target); + } + StatementKind::SequenceGet { + source, + target, + index, + .. + } => { + source_local(source, visit); + visit(target); + visit(index); + } + StatementKind::IterationBorrow { source, token, .. } => { + source_local(source, visit); + visit(token); + } + StatementKind::SumTag { source, target } + | StatementKind::SumTake { source, target, .. } => { + visit(source); + visit(target); + } + StatementKind::Let { local, value } + | StatementKind::BeginCallView { local, value } + | StatementKind::CheckRefinement { + local, call: value, .. + } => { + visit(local); + expression(value, visit, floor, metadata); + } + StatementKind::EndCallView { local } => visit(local), + StatementKind::Assign { target, value } + | StatementKind::UpdateField { target, value, .. } => { + expression(target, visit, floor, metadata); + expression(value, visit, floor, metadata); + } + StatementKind::Evaluate(value) | StatementKind::HandleDefault(value) => { + expression(value, visit, floor, metadata) + } + StatementKind::Assert { condition, message } => { + expression(condition, visit, floor, metadata); + if let Some(message) = message { + expression(message, visit, floor, metadata); + } + } + StatementKind::Trace(local) => visit(local), + StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(condition) = condition { + expression(condition, visit, floor, metadata); + } + for binding in bindings { + visit(binding); + } + } + } + } + match &mut block.terminator.kind { + TerminatorKind::Return(value) => { + if let Some(value) = value { + expression(value, visit, floor, metadata); + } + } + TerminatorKind::Respond(value) + | TerminatorKind::Branch { + condition: value, .. + } => expression(value, visit, floor, metadata), + TerminatorKind::Switch { + scrutinee, + variants, + .. + } => { + expression(scrutinee, visit, floor, metadata); + for (_, _, bindings) in variants { + for binding in bindings { + visit(binding); + } + } + } + TerminatorKind::ForEach { + key, + value, + iterable, + .. + } => { + visit(key); + if let Some(value) = value { + visit(value); + } + expression(iterable, visit, floor, metadata); + } + TerminatorKind::ReflectedTypeDispatch { type_info, .. } => { + expression(type_info, visit, floor, metadata) + } + TerminatorKind::Goto(_) | TerminatorKind::Unreachable => {} + } +} + +fn expression( + value: &mut Expression, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), + metadata: bool, +) { + if metadata { + let packet = match &mut value.kind { + ExpressionKind::Call { ownership, .. } + | ExpressionKind::IndirectCall { ownership, .. } + | ExpressionKind::Intrinsic { ownership, .. } + | ExpressionKind::ResourceInvoke { ownership, .. } => Some(ownership), + _ => None, + }; + if let Some(packet) = packet { + // Infallible structural remapping does not create execution reads. + let result = packet.remap_metadata_locals(|mut id| { + visit(&mut id); + Ok::<_, std::convert::Infallible>(id) + }); + match result { + Ok(()) => {} + Err(impossible) => match impossible {}, + } + } + } + match &mut value.kind { + // This immutable plan cannot be remapped by a generic local visitor. + ExpressionKind::ActorGraphImport(_) => {} + ExpressionKind::Local(local) => visit(local), + ExpressionKind::ClosureRef { captures, .. } => { + for capture in captures { + visit(capture); + } + } + ExpressionKind::Binary { left, right, .. } => { + expression(left, visit, floor, metadata); + expression(right, visit, floor, metadata); + } + ExpressionKind::Unary { value, .. } + | ExpressionKind::ResultOk(value) + | ExpressionKind::ResultFail(value) + | ExpressionKind::OptionalSome(value) + | ExpressionKind::Declassify(value) + | ExpressionKind::Coarsen(value) + | ExpressionKind::RefinementValidated(value) + | ExpressionKind::DisplayResult(value) + | ExpressionKind::EquatableResult(value) + | ExpressionKind::RuntimeFailureMessage(value) + | ExpressionKind::InterfaceCoerce { value, .. } + | ExpressionKind::FunctionAdapter { value, .. } + | ExpressionKind::InterfaceType(value) + | ExpressionKind::Comptime { value, .. } + | ExpressionKind::StateIs { value, .. } + | ExpressionKind::Run(value) + | ExpressionKind::Join(value) + | ExpressionKind::Cancel(value) + | ExpressionKind::Field { base: value, .. } + | ExpressionKind::View(value) + | ExpressionKind::Clone(value) => expression(value, visit, floor, metadata), + ExpressionKind::Call { args, .. } + | ExpressionKind::ResourceInvoke { args, .. } + | ExpressionKind::Intrinsic { args, .. } + | ExpressionKind::ActorSpawn { args, .. } + | ExpressionKind::ActorGraphStep { args, .. } + | ExpressionKind::StructConstruct { fields: args, .. } + | ExpressionKind::BitfieldConstruct { fields: args, .. } + | ExpressionKind::MachineConstruct { payloads: args, .. } + | ExpressionKind::EnumConstruct { payloads: args, .. } + | ExpressionKind::ListConstruct { elements: args } => { + for arg in args { + expression(arg, visit, floor, metadata); + } + } + ExpressionKind::IndirectCall { callee, args, .. } + | ExpressionKind::ActorMessage { + actor: callee, + args, + .. + } + | ExpressionKind::MachineTransition { + source: callee, + payloads: args, + .. + } => { + expression(callee, visit, floor, metadata); + for arg in args { + expression(arg, visit, floor, metadata); + } + } + ExpressionKind::MapConstruct { entries } => { + for entry in entries { + expression(&mut entry.key, visit, floor, metadata); + expression(&mut entry.value, visit, floor, metadata); + } + } + ExpressionKind::Handle { + target, + error_local, + failure, + .. + } => { + expression(target, visit, floor, metadata); + if let Some(local) = error_local { + visit(local); + } + hir_block(failure, visit, floor, metadata); + } + ExpressionKind::StringInterpolation(parts) => { + for part in parts { + if let hir::StringSegment::Value(value) = part { + expression(value, visit, floor, metadata); + } + } + } + ExpressionKind::InlineFunction { + params, + view_params, + local_floor, + body, + .. + } => { + for local in params.iter_mut().chain(view_params) { + visit(local); + } + floor(local_floor); + hir_block(body, visit, floor, metadata); + } + ExpressionKind::Int(_) + | ExpressionKind::Float(_) + | ExpressionKind::String(_) + | ExpressionKind::Bool(_) + | ExpressionKind::Nothing + | ExpressionKind::Constant { .. } + | ExpressionKind::ResourceHookValue { .. } + | ExpressionKind::FunctionRef(_) + | ExpressionKind::OptionalNone + | ExpressionKind::RuntimeFailure(_) + | ExpressionKind::PropertyCaseContext(_) => {} + } +} + +fn hir_block( + block: &mut hir::Block, + visit: &mut impl FnMut(&mut LocalId), + floor: &mut impl FnMut(&mut u32), + metadata: bool, +) { + for statement in &mut block.statements { + match &mut statement.kind { + hir::StatementKind::Let { local, value } => { + visit(local); + expression(value, visit, floor, metadata); + } + hir::StatementKind::Assign { target, value } + | hir::StatementKind::UpdateField { target, value, .. } => { + expression(target, visit, floor, metadata); + expression(value, visit, floor, metadata); + } + hir::StatementKind::Return(value) => { + if let Some(value) = value { + expression(value, visit, floor, metadata); + } + } + hir::StatementKind::HandleDefault(value) + | hir::StatementKind::Expression(value) + | hir::StatementKind::Respond(value) => expression(value, visit, floor, metadata), + hir::StatementKind::If { + condition, + then_block, + else_block, + } => { + expression(condition, visit, floor, metadata); + hir_block(then_block, visit, floor, metadata); + if let Some(block) = else_block { + hir_block(block, visit, floor, metadata); + } + } + hir::StatementKind::While { condition, body } => { + expression(condition, visit, floor, metadata); + hir_block(body, visit, floor, metadata); + } + hir::StatementKind::For { + key, + value, + iterable, + body, + .. + } => { + visit(key); + if let Some(value) = value { + visit(value); + } + expression(iterable, visit, floor, metadata); + hir_block(body, visit, floor, metadata); + } + hir::StatementKind::Match { scrutinee, arms } => { + expression(scrutinee, visit, floor, metadata); + for arm in arms { + for binding in &mut arm.bindings { + visit(binding); + } + hir_block(&mut arm.body, visit, floor, metadata); + } + } + hir::StatementKind::Assert { condition, message } => { + expression(condition, visit, floor, metadata); + if let Some(value) = message { + expression(value, visit, floor, metadata); + } + } + hir::StatementKind::Trace(local) => visit(local), + hir::StatementKind::Breakpoint { + condition, + bindings, + } => { + if let Some(value) = condition { + expression(value, visit, floor, metadata); + } + for binding in bindings { + visit(binding); + } + } + hir::StatementKind::Scope(block) => hir_block(block, visit, floor, metadata), + hir::StatementKind::ReflectedTypeDispatch { type_info, arms } => { + expression(type_info, visit, floor, metadata); + for arm in arms { + hir_block(&mut arm.body, visit, floor, metadata); + } + } + hir::StatementKind::Break | hir::StatementKind::Continue => {} + } + } +} diff --git a/crates/jett_mir/src/sequences/tests.rs b/crates/jett_mir/src/sequences/tests.rs new file mode 100644 index 00000000..547f829a --- /dev/null +++ b/crates/jett_mir/src/sequences/tests.rs @@ -0,0 +1,592 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::{HashMap, HashSet}; + +fn lower_source(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let hir = jett_hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + ( + lower(&hir, &checked.interner).expect("MIR lowering"), + checked.interner, + ) +} + +fn main_function(program: &Program) -> &Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == "main") + .unwrap() +} + +fn assert_reachable_and_dense(function: &Function) { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let mut reachable = HashSet::new(); + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if reachable.insert(block) { + pending.extend_from_slice(cfg.successors(block)); + } + } + assert_eq!(reachable.len(), function.blocks.len()); + for (index, block) in function.blocks.iter().enumerate() { + assert_eq!(block.id.index() as usize, index); + } + for (index, local) in function.locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + } +} + +#[test] +fn uninhabited_iteration_remaps_transitive_local_view_origins() { + let (mut program, types) = lower_source( + r#"namespace app +function main() returns int64: + for element in list(): + trace element + list[int64] source = list(7, 11) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + trace forwarded + return 7 +"#, + ); + let original = main_function(&program); + let source_before = original + .locals + .iter() + .find(|local| local.name == "source") + .unwrap() + .id; + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = main_function(&program); + assert_reachable_and_dense(function); + let source = function + .locals + .iter() + .find(|local| local.name == "source") + .unwrap(); + let borrowed = function + .locals + .iter() + .find(|local| local.name == "borrowed") + .unwrap(); + let forwarded = function + .locals + .iter() + .find(|local| local.name == "forwarded") + .unwrap(); + assert!(source.id.index() < source_before.index()); + assert_eq!(borrowed.view_source, Some(source.id)); + assert_eq!(forwarded.view_source, Some(borrowed.id)); + assert_eq!(function.view_root(forwarded.id), Some(source.id)); + let plan = crate::move_values::MoveValuePlan::analyze(&program, function, &types).unwrap(); + assert!(plan.owned_locals.contains(&(source.id.index() as usize))); + assert!(!plan.owned_locals.contains(&(borrowed.id.index() as usize))); + assert!(!plan.owned_locals.contains(&(forwarded.id.index() as usize))); +} + +const EMPTY_LOOP: &str = r#"namespace app +function main(flag: bool) returns int64: + int64 before = 10 + for value in list(): + trace value + int64 after = 30 + if flag: + return after + return before +"#; + +#[test] +fn uninhabited_iteration_compacts_dead_blocks_and_locals_with_debug_metadata() { + let (mut program, types) = lower_source(EMPTY_LOOP); + let original = main_function(&program).clone(); + let dead_binding = original + .locals + .iter() + .find(|local| local.name == "value") + .unwrap() + .id; + let function = &mut program.functions[original.id.index() as usize]; + let body = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + TerminatorKind::ForEach { body, .. } => Some(body), + _ => None, + }) + .unwrap(); + // Pin pruning independently of HIR's omission of checked dead source bodies. + function.blocks[body.index() as usize] + .statements + .push(Statement { + kind: StatementKind::Trace(dead_binding), + span: original.span, + }); + let after = function + .locals + .iter_mut() + .find(|local| local.name == "after") + .unwrap(); + after.debug_type_name = Some("source-visible integer".into()); + let expected_after = after.clone(); + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = main_function(&program); + assert_reachable_and_dense(function); + assert!( + !function + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + assert_eq!(function.params, original.params); + let after = function + .locals + .iter() + .find(|local| local.name == "after") + .unwrap(); + assert!(after.id.index() < expected_after.id.index()); + let mut remapped = expected_after; + remapped.id = after.id; + assert_eq!(after, &remapped); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, StatementKind::SequenceLength { .. })) + .count(), + 1 + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!( + statement.kind, + StatementKind::SequenceGet { .. } | StatementKind::Trace(_) + )) + ); + assert!(function.blocks.iter().any(|block| matches!(&block.terminator.kind, TerminatorKind::Return(Some(Expression { kind: ExpressionKind::Local(local), .. })) if *local == after.id))); + let prepared = program.clone(); + prepare_native_sequences(&mut program, &types); + assert_eq!(program, prepared, "preparation is idempotent"); +} + +#[test] +fn uninhabited_iteration_keeps_once_evaluation_pending_checks_and_view_loan() { + let (mut program, types) = lower_source( + r#"namespace app +function produce[T](items: list[T]) returns list[T]: + trace items + return items +function main() returns nothing: + for value in view run produce(list()): + trace value + return nothing +"#, + ); + let function = main_function(&program); + let original_iterable = function + .blocks + .iter() + .find_map(|block| match &block.terminator.kind { + TerminatorKind::ForEach { iterable, .. } => Some(iterable.clone()), + _ => None, + }) + .unwrap(); + let expected = if let ExpressionKind::View(value) = original_iterable.kind { + *value + } else { + original_iterable + }; + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = main_function(&program); + assert_reachable_and_dense(function); + let statements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect::>(); + assert_eq!(statements.iter().filter(|statement| matches!(&statement.kind, StatementKind::Let { value, .. } if value == &expected)).count(), 1); + let mut starts = Vec::new(); + let mut ends = Vec::new(); + let mut lengths = Vec::new(); + for statement in &statements { + match &statement.kind { + StatementKind::IterationBorrow { + source, + token, + start, + } => { + if *start { + starts.push((source.clone(), *token)); + } else { + ends.push((source.clone(), *token)); + } + } + StatementKind::SequenceLength { source, .. } => lengths.push(source.clone()), + StatementKind::SequenceGet { .. } => panic!("uninhabited binder must not be extracted"), + _ => {} + } + } + assert_eq!(starts.len(), 1); + assert_eq!(starts, ends); + assert_eq!(lengths, [starts[0].0.clone()]); +} + +#[test] +fn uninhabited_iteration_preserves_nested_loops_handlers_and_exits() { + let (mut program, types) = lower_source( + r#"namespace app +function main(flag: bool) returns int64: + mutable int64 count = 0 + for outer in list(1, 2): + for value in list(): + trace value + optional[int64] maybe = some(outer) + int64 next = maybe handle: + if flag: + break + default 5 + count = count + next + if flag: + continue + return count +"#, + ); + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = main_function(&program); + assert_reachable_and_dense(function); + let statements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect::>(); + assert_eq!( + statements + .iter() + .filter(|statement| matches!(statement.kind, StatementKind::SequenceLength { .. })) + .count(), + 2 + ); + assert_eq!( + statements + .iter() + .filter(|statement| matches!(statement.kind, StatementKind::SequenceGet { .. })) + .count(), + 1 + ); + assert!( + statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + ); + assert!( + statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SumTake { .. })) + ); + assert!( + !function + .locals + .iter() + .any(|local| local.ty == TypeInterner::NEVER) + ); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("retained CFG ownership plan"); +} + +#[test] +fn uninhabited_map_iteration_handles_either_component_without_extracting_binders() { + for (key, value) in [ + (TypeInterner::NEVER, TypeInterner::NEVER), + (TypeInterner::INT64, TypeInterner::NEVER), + (TypeInterner::NEVER, TypeInterner::STRING), + ] { + let (mut program, mut types) = lower_source( + r#"namespace app +function main() returns nothing: + for key, value in map(): + trace key + trace value + return nothing +"#, + ); + let function = &mut program.functions[0]; + let mut bindings = None; + for block in &mut function.blocks { + if let TerminatorKind::ForEach { + key: key_local, + value: Some(value_local), + iterable, + .. + } = &mut block.terminator.kind + { + iterable.ty = types.intern(Type::Map(key, value)); + bindings = Some((*key_local, *value_local)); + } + } + let (key_local, value_local) = bindings.unwrap(); + function.locals[key_local.index() as usize].ty = key; + function.locals[key_local.index() as usize].debug_ty = key; + function.locals[value_local.index() as usize].ty = value; + function.locals[value_local.index() as usize].debug_ty = value; + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = main_function(&program); + assert_reachable_and_dense(function); + assert!( + !function + .locals + .iter() + .any(|local| local.name == "key" || local.name == "value") + ); + assert!( + !function + .blocks + .iter() + .flat_map(|block| &block.statements) + .any(|statement| matches!(statement.kind, StatementKind::SequenceGet { .. })) + ); + } +} + +#[test] +fn uninhabited_projected_iteration_retains_ancestor_validation_and_matching_loans() { + let (mut program, types) = lower_source( + r#"namespace app +struct Holder[T]: + items: list[T] +function wrap[T](items: list[T]) returns Holder[T]: + return Holder[T](items: items) +function inspect[T](view holder: Holder[T]) returns nothing: + for value in holder.items: + trace value + return nothing +function inspect_items[T](items: list[T]) returns nothing: + Holder[T] holder = wrap[T](items) + inspect[T](view holder) + return nothing +function main() returns nothing: + inspect_items(list()) + return nothing +"#, + ); + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + let function = program + .functions + .iter() + .find(|function| function.identity.declaration.name == "inspect") + .unwrap(); + assert_reachable_and_dense(function); + let statements = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .collect::>(); + let source = statements + .iter() + .find_map(|statement| match &statement.kind { + StatementKind::SequenceLength { source, .. } => Some(source), + _ => None, + }) + .unwrap(); + let SequenceSource::Projected { owner, path, ty } = source else { + panic!("projected source must be retained"); + }; + assert_eq!(*owner, function.params[0].local); + assert_eq!(path.len(), 1); + assert!(matches!(types.resolve(*ty), Type::List(inner) if *inner == TypeInterner::NEVER)); + let loans = statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::IterationBorrow { + source: borrowed, + token, + start, + } => { + assert_eq!(borrowed, source); + Some((*token, *start)) + } + _ => None, + }) + .collect::>(); + assert_eq!(loans.len(), 2); + assert_eq!(loans[0], (loans[1].0, true)); + assert!(!loans[1].1); + assert!( + !statements + .iter() + .any(|statement| matches!(statement.kind, StatementKind::SequenceGet { .. })) + ); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("projected owner is only borrowed"); +} + +#[test] +fn uninhabited_iteration_remaps_capture_references_without_changing_lifted_functions() { + let (mut program, types) = lower_source( + r#"namespace app +function main(unused: int64) returns int64: + for value in list(): + trace value + int64 retained = 40 + function(int64) returns int64 callback = function(value: int64) returns int64: return retained + value + return callback(2) +"#, + ); + let main_id = main_function(&program).id; + let lifted = program + .functions + .iter() + .filter(|function| function.id != main_id) + .cloned() + .collect::>(); + prepare_native_sequences(&mut program, &types); + validate(&program).unwrap(); + assert_eq!( + program + .functions + .iter() + .filter(|function| function.id != main_id) + .cloned() + .collect::>(), + lifted + ); + let function = main_function(&program); + assert_reachable_and_dense(function); + assert_eq!(function.params.len(), 1, "unused ABI parameters remain"); + let retained = function + .locals + .iter() + .find(|local| local.name == "retained") + .unwrap() + .id; + let (target, captures) = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .find_map(|statement| match &statement.kind { + StatementKind::Let { + value: + Expression { + kind: ExpressionKind::ClosureRef { function, captures }, + .. + }, + .. + } => Some((*function, captures)), + _ => None, + }) + .unwrap(); + assert_eq!(captures, &[retained]); + assert!( + lifted + .iter() + .any(|function| function.id == target && function.capture_count == 1) + ); +} + +#[test] +fn uninhabited_iteration_does_not_hide_invalid_dead_ids_or_prune_other_functions() { + for corruption in 0..6 { + let (mut program, types) = lower_source(EMPTY_LOOP); + let function = &mut program.functions[0]; + let body = function + .blocks + .iter() + .find_map(|block| match block.terminator.kind { + TerminatorKind::ForEach { body, .. } => Some(body), + _ => None, + }) + .unwrap(); + let block = &mut function.blocks[body.index() as usize]; + match corruption { + 0 => block.statements.push(Statement { + kind: StatementKind::Trace(LocalId::new(u32::MAX)), + span: function.span, + }), + 1 => block.terminator.kind = TerminatorKind::Goto(BlockId(u32::MAX)), + 2 => block.statements.push(Statement { + kind: StatementKind::Evaluate(Expression { + kind: ExpressionKind::FunctionRef(FunctionId::new(u32::MAX)), + ty: TypeInterner::NOTHING, + span: function.span, + }), + span: function.span, + }), + _ => block.statements.push(Statement { + kind: StatementKind::Evaluate(Expression { + kind: ExpressionKind::InlineFunction { + scoped_type_bindings: Vec::new(), + params: Vec::new(), + view_params: match corruption { + 3 => vec![LocalId::new(u32::MAX)], + 4 => vec![function.params[0].local], + _ => Vec::new(), + }, + local_floor: if corruption == 5 { u32::MAX } else { 0 }, + body: hir::Block { + statements: Vec::new(), + span: function.span, + }, + }, + ty: TypeInterner::NOTHING, + span: function.span, + }), + span: function.span, + }), + } + let original = program.clone(); + let errors = validate(&program).unwrap_err(); + if corruption >= 3 { + let expected = match corruption { + 3 => "inline function view parameter references local", + 4 => "inline function view parameter is not a parameter", + _ => "inline function local floor is out of range", + }; + assert!(errors.iter().any(|error| error.message.contains(expected))); + } + prepare_native_sequences(&mut program, &types); + assert_eq!(program, original); + assert!(validate(&program).is_err()); + } + let (mut program, types) = lower_source( + r#"namespace app +function unrelated(unused: int64) returns int64: + return 7 +function main() returns nothing: + for value in list(): + trace value + return nothing +"#, + ); + let unrelated = program.functions[0].clone(); + prepare_native_sequences(&mut program, &types); + assert_eq!(program.functions[0], unrelated); + validate(&program).unwrap(); +} diff --git a/crates/jett_mir/src/temporary_local_views.rs b/crates/jett_mir/src/temporary_local_views.rs new file mode 100644 index 00000000..6990bf16 --- /dev/null +++ b/crates/jett_mir/src/temporary_local_views.rs @@ -0,0 +1,1521 @@ +//! Source-authenticated whole owners for function-body temporary local views. +//! The current graph is sealed separately from the immutable HIR occurrence. +//! Pins are cleanup obligations; they never establish availability or reads. + +use super::*; +use hir::{CallOwnership, ExpressionKind as E}; +use std::collections::BTreeSet; +use std::sync::Arc; + +struct Original { + // Retains the exact physical HIR input, including mutable_sum_backings and + // all independent opaque callable/source receipts. No HIR field is added. + source: hir::Function, + inventory: Vec, +} +impl std::fmt::Debug for Original { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Original") + .field("source", &self.source) + .field("occurrences", &self.inventory.len()) + .finish() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Site { + block: BlockId, + statement: usize, +} +impl Site { + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + self.block = map + .get(self.block.index() as usize) + .copied() + .flatten() + .ok_or("temporary view compaction removed its owning declaration")?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Acquisition { + Direct, + // The physical Call must complete before call loans end. The whole result + // then moves to the hidden owner; its ordinary intermediate slot is cleared. + Staged { + local: LocalId, + call: Site, + ends: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Row { + occurrence: usize, + owner: LocalId, + alias: LocalId, + initialize: Site, + publish: Site, + acquisition: Acquisition, +} +impl Row { + fn remap_blocks(&mut self, map: &[Option]) -> Result<(), String> { + self.initialize.remap_blocks(map)?; + self.publish.remap_blocks(map)?; + if let Acquisition::Staged { call, ends, .. } = &mut self.acquisition { + call.remap_blocks(map)?; + for site in ends { + site.remap_blocks(map)?; + } + } + Ok(()) + } + fn remap_locals(&mut self, map: &[Option]) -> Result<(), String> { + self.owner = mapped_local(map, self.owner)?; + self.alias = mapped_local(map, self.alias)?; + if let Acquisition::Staged { local, .. } = &mut self.acquisition { + *local = mapped_local(map, *local)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Graph { + id: FunctionId, + identity: FunctionIdentity, + debug_kind: hir::FunctionDebugKind, + params: Vec, + capture_count: usize, + return_type: TypeId, + locals: Vec, + entry: BlockId, + blocks: Vec, + span: Span, + actor_state_views: Vec, + actor_message_source: bool, +} +impl Graph { + fn new(function: &Function) -> Self { + Self { + id: function.id, + identity: function.identity.clone(), + debug_kind: function.debug_kind.clone(), + params: function.params.clone(), + capture_count: function.capture_count, + return_type: function.return_type, + locals: function.locals.clone(), + entry: function.entry, + blocks: function.blocks.clone(), + span: function.span, + actor_state_views: function.actor_state_views.clone(), + actor_message_source: function.actor_message_source, + } + } + fn matches(&self, function: &Function) -> bool { + self.id == function.id + && self.identity == function.identity + && self.debug_kind == function.debug_kind + && self.params == function.params + && self.capture_count == function.capture_count + && self.return_type == function.return_type + && self.locals == function.locals + && self.entry == function.entry + && self.span == function.span + && self.actor_state_views == function.actor_state_views + && self.actor_message_source == function.actor_message_source + // This comparison retains f64 bits and private callable identities. + && breakpoint_regions::blocks_equal(&self.blocks, &function.blocks) + } +} + +#[derive(Debug, Clone)] +struct Current { + graph: Graph, + rows: Vec, +} + +#[derive(Debug, Clone)] +pub(super) struct Witness { + original: Arc, + rows: Vec, + current: Arc, +} +impl PartialEq for Witness { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.original, &other.original) + && Arc::ptr_eq(&self.current, &other.current) + && self.rows == other.rows + } +} +impl Witness { + fn current(&self, function: &Function) -> Result<(), String> { + if !self.current.graph.matches(function) + || self.current.rows != self.rows + || self.original.source.id != function.id + || self.original.source.identity != function.identity + || self.rows.len() != self.original.inventory.len() + { + return Err("temporary local view changed its exact original inventory or current semantic graph".into()); + } + for (index, row) in self.rows.iter().enumerate() { + if row.occurrence != index { + return Err("temporary local view reordered its original occurrences".into()); + } + let proof = &self.original.inventory[index]; + if proof.original_scope().body_span() != self.original.source.body.span { + return Err( + "temporary local view is outside the supported original function scope".into(), + ); + } + validate_row(function, row)?; + } + Ok(()) + } +} + +pub(super) fn has_records(function: &Function) -> bool { + function.temporary_local_views.is_some() +} + +fn has_receipts(function: &Function) -> bool { + let mut found = false; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + let packet = match &value.kind { + E::Call { ownership, .. } + | E::IndirectCall { ownership, .. } + | E::Intrinsic { ownership, .. } + | E::ResourceInvoke { ownership, .. } => Some(ownership), + _ => None, + }; + found |= packet.is_some_and(CallOwnership::has_temporary_local_receipts); + }); + } + found +} + +pub(super) fn validate_current(function: &Function) -> Result<(), String> { + if let Some(witness) = &function.temporary_local_views { + witness.current(function)?; + } else if has_receipts(function) { + return Err("temporary local view invocation has no original private MIR witness".into()); + } + Ok(()) +} + +pub(super) fn validate(function: &Function, types: &TypeInterner) -> Result<(), String> { + validate_current(function)?; + let Some(witness) = &function.temporary_local_views else { + return Ok(()); + }; + for row in &witness.rows { + let owner = function + .local(row.owner) + .ok_or("temporary owner header is absent")?; + let alias = function + .local(row.alias) + .ok_or("temporary alias header is absent")?; + let (_, value) = let_at(function, row.publish)?; + if owner.mutable + || owner.view_source.is_some() + || alias.mutable + || alias.view_source != Some(row.owner) + || !move_values::is_linear(types, owner.ty) + || resource_type_pending(types, owner.ty) + || function.is_view_local(row.owner) + || function.parameter_for_local(row.owner).is_some() + || function.parameter_for_local(row.alias).is_some() + { + return Err("temporary local view requires one ordinary whole owning slot and one nonowning alias".into()); + } + hir::validate_local_view_initializer(value, row.owner, owner.ty, alias.ty, types)?; + } + Ok(()) +} + +/// The whole-program lowering entry point alone may mint initial MIR witnesses. +/// HIR joins its retained expected archive even when a call marker is missing. +/// Do not replace this call with an optional-marker scan of current expressions. +pub(super) fn capture( + program: &mut Program, + source: &hir::Program, + types: &TypeInterner, +) -> Result<(), String> { + if program.functions.len() != source.functions.len() { + return Err("temporary local view capture changed the original function inventory".into()); + } + for (function, original) in program.functions.iter_mut().zip(&source.functions) { + if function.id != original.id + || function.identity != original.identity + || function.debug_kind != original.debug_kind + || function.params != original.params + || function.capture_count != original.capture_count + || function.return_type != original.return_type + || function.span != original.span + || !function.locals.starts_with(&original.locals) + { + return Err( + "temporary local view capture changed the original function identity".into(), + ); + } + let inventory = hir::temporary_local_backings(source, original, types)?; + if inventory.is_empty() { + continue; + } + let mut rows = Vec::with_capacity(inventory.len()); + for (occurrence, proof) in inventory.iter().enumerate() { + let (source_owner, source_alias) = source_pair(original, proof)?; + if !proof.matches_owner_initializer(source_owner) + || !breakpoint_regions::expressions_equal( + source_alias, + proof.rewritten_initializer(), + ) + { + return Err( + "temporary local view changed the exact physical HIR initializer pair".into(), + ); + } + let (initialize, owner_value) = unique_let(function, proof.owner())?; + let (publish, alias_value) = unique_let(function, proof.alias())?; + if !breakpoint_regions::expressions_equal(alias_value, source_alias) { + return Err( + "temporary local view changed its original typed alias projection".into(), + ); + } + let acquisition = match owner_value.kind { + E::Call { .. } => { + same_source_producer(source_owner, owner_value)?; + Acquisition::Direct + } + E::Local(local) => { + let (call, value) = unique_let(function, local)?; + same_source_producer(source_owner, value)?; + if call.block != initialize.block || call.statement >= initialize.statement { + return Err("temporary acquisition crossed a block or control-flow merge".into()); + } + let staged = function.local(local).ok_or("temporary acquisition header is absent")?; + // finish_call_view_scope acquires its complete result with + // Builder::temporary. Its mutable flag describes generated + // storage, not a mutable Source binding. Require that exact + // appended constructor header; the persistent owner/alias + // still retain their original immutable HIR headers. + if (local.index() as usize) < original.locals.len() + || function.parameter_for_local(local).is_some() + || staged.name != format!("$native{}", local.index()) + || staged.ty != owner_value.ty + || staged.ty != value.ty + || staged.debug_ty != staged.ty + || staged.debug_type_name.is_some() + || staged.span != value.span + || staged.span != owner_value.span + || staged.view_source.is_some() + || !staged.mutable + { + return Err("temporary acquisition is not a generated whole owning result".into()); + } + let ends = ((call.statement + 1)..initialize.statement) + .map(|statement| Site { block: call.block, statement }) + .collect::>(); + if ends.is_empty() + || ends.iter().any(|site| !matches!( + function.blocks[site.block.index() as usize].statements[site.statement].kind, + StatementKind::EndCallView { .. } + )) + { + return Err("temporary acquisition changed its exact call-loan end chain".into()); + } + Acquisition::Staged { local, call, ends } + } + _ => return Err("pending temporary local view acquisition: expected a named whole-owner Call or its exact staged move".into()), + }; + let row = Row { + occurrence, + owner: proof.owner(), + alias: proof.alias(), + initialize, + publish, + acquisition, + }; + validate_row(function, &row)?; + rows.push(row); + } + let witness = Witness { + original: Arc::new(Original { + source: original.clone(), + inventory, + }), + current: Arc::new(Current { + graph: Graph::new(function), + rows: rows.clone(), + }), + rows, + }; + witness.current(function)?; + function.temporary_local_views = Some(witness); + validate(function, types)?; + } + Ok(()) +} + +fn source_pair<'a>( + function: &'a hir::Function, + proof: &hir::TemporaryLocalBacking, +) -> Result<(&'a Expression, &'a Expression), String> { + // The API authenticated the original occurrence and root scope. Check the + // actual physical input again; a matching public Local header is not proof. + let mut owner = None; + let mut alias = None; + for (index, statement) in function.body.statements.iter().enumerate() { + if let hir::StatementKind::Let { local, value } = &statement.kind { + let target = if *local == proof.owner() { + &mut owner + } else if *local == proof.alias() { + &mut alias + } else { + continue; + }; + if target.replace((index, value)).is_some() { + return Err( + "temporary local view duplicated its original physical HIR declaration".into(), + ); + } + } + } + let (owner_index, owner) = + owner.ok_or("temporary local view has no physical HIR owner initialization")?; + let (alias_index, alias) = + alias.ok_or("temporary local view has no physical HIR alias initialization")?; + if alias_index != owner_index + 1 { + return Err("temporary local view moved its physical HIR alias publication".into()); + } + Ok((owner, alias)) +} + +/// Operand extraction changes only the packet's per-argument staging. Retain +/// every independent private marker and opaque callable token in the comparison. +/// call_ownership's complete gate validates the actual argument stage operations +/// and their lexical source order after this initial association is captured. +fn same_source_producer(original: &Expression, physical: &Expression) -> Result<(), String> { + let ( + E::Call { + function: original_target, + args: original_args, + evaluation_order: original_order, + ownership: original_ownership, + }, + E::Call { + function: physical_target, + args: physical_args, + evaluation_order: physical_order, + ownership: CallOwnership::Source(physical_packet), + }, + ) = (&original.kind, &physical.kind) + else { + return Err("temporary producer requires its exact original named Source Call".into()); + }; + let CallOwnership::Source(original_packet) = original_ownership else { + return Err("temporary producer has no original Source acquisition receipt".into()); + }; + if !original_packet.generated_operands.is_empty() + || !physical_packet.generated_operands.is_empty() + || original_target != physical_target + || original.ty != physical.ty + || original.span != physical.span + || original_order != physical_order + || original_args.len() != physical_args.len() + || original_packet.arguments.len() != physical_packet.arguments.len() + { + return Err( + "pending temporary producer composition or changed original producer identity".into(), + ); + } + let mut restored = physical_packet.clone(); + for (current, previous) in restored + .arguments + .iter_mut() + .zip(&original_packet.arguments) + { + current.staging = previous.staging; + } + if restored != *original_packet { + return Err("temporary producer changed immutable Source call authority".into()); + } + for argument in &physical_packet.arguments { + if argument.staging == hir::ArgumentStaging::Original { + let index = argument.parameter_index; + if !original_args + .get(index) + .zip(physical_args.get(index)) + .is_some_and(|(a, b)| breakpoint_regions::expressions_equal(a, b)) + { + return Err("temporary producer changed an unstaged original operand".into()); + } + } + } + Ok(()) +} + +fn unique_let(function: &Function, local: LocalId) -> Result<(Site, &Expression), String> { + let mut found = None; + for block in &function.blocks { + for (statement, value) in block.statements.iter().enumerate() { + if let StatementKind::Let { + local: target, + value, + } = &value.kind + && *target == local + && found + .replace(( + Site { + block: block.id, + statement, + }, + value, + )) + .is_some() + { + return Err( + "temporary local view has duplicate owning or alias initialization".into(), + ); + } + } + } + found.ok_or_else(|| "temporary local view lost its complete initialization".into()) +} + +fn let_at(function: &Function, site: Site) -> Result<(LocalId, &Expression), String> { + match function + .blocks + .get(site.block.index() as usize) + .filter(|block| block.id == site.block) + .and_then(|block| block.statements.get(site.statement)) + .map(|statement| &statement.kind) + { + Some(StatementKind::Let { local, value }) => Ok((*local, value)), + _ => Err("temporary local view lost its exact initialization site".into()), + } +} + +fn runtime_occurrences(function: &Function, local: LocalId) -> usize { + let mut count = 0; + for mut block in function.blocks.clone() { + sequences::prune::block_runtime_locals( + &mut block, + &mut |id| { + count += usize::from(*id == local); + }, + &mut |_| {}, + ); + } + count +} + +fn validate_row(function: &Function, row: &Row) -> Result<(), String> { + let (owner_site, owner_value) = unique_let(function, row.owner)?; + let (alias_site, alias_value) = unique_let(function, row.alias)?; + let owner = function + .local(row.owner) + .ok_or("temporary owner header is absent")?; + let alias = function + .local(row.alias) + .ok_or("temporary alias header is absent")?; + if owner_site != row.initialize + || alias_site != row.publish + || owner_value.ty != owner.ty + || alias_value.ty != alias.ty + || row.publish.block != row.initialize.block + || row.publish.statement != row.initialize.statement + 1 + || runtime_occurrences(function, row.owner) != 2 + { + return Err( + "temporary whole owner was duplicated, consumed, moved or published at another site" + .into(), + ); + } + match &row.acquisition { + Acquisition::Direct if matches!(owner_value.kind, E::Call { .. }) => {} + Acquisition::Staged { local, call, ends } => { + let (actual, value) = unique_let(function, *local)?; + if actual != *call + || !matches!(value.kind, E::Call { .. }) + || !matches!(owner_value.kind, E::Local(id) if id == *local) + || runtime_occurrences(function, *local) != 2 + || call.block != row.initialize.block + || call.statement >= row.initialize.statement + || ends + != &((call.statement + 1)..row.initialize.statement) + .map(|statement| Site { + block: call.block, + statement, + }) + .collect::>() + || ends.iter().any(|site| { + !matches!( + function.blocks[site.block.index() as usize].statements[site.statement] + .kind, + StatementKind::EndCallView { .. } + ) + }) + { + return Err( + "temporary whole-owner staging no longer matches its exact acquisition chain" + .into(), + ); + } + } + _ => return Err("temporary whole-owner initializer changed acquisition shape".into()), + } + let cfg = ControlFlowGraph::analyze(function).map_err(|errors| format!("{errors:?}"))?; + let mut pending = cfg.successors(row.initialize.block).to_vec(); + let mut seen = BTreeSet::new(); + while let Some(block) = pending.pop() { + if block == row.initialize.block { + return Err( + "pending temporary local view activation: backing is acquired on a cycle".into(), + ); + } + if seen.insert(block) { + pending.extend_from_slice(cfg.successors(block)); + } + } + Ok(()) +} + +/// Called after definite initialization and ordinary liveness have succeeded. +/// No owner is inserted into initialization, alias facts or execution reads. +pub(super) fn pin_scope( + function: &Function, + types: &TypeInterner, + cfg: &ControlFlowGraph, + live_in: &mut [BTreeSet], + live_out: &mut [BTreeSet], + after: &mut [Vec>], +) -> Result<(), String> { + validate(function, types)?; + let Some(witness) = &function.temporary_local_views else { + return Ok(()); + }; + let owners = witness + .rows + .iter() + .map(|row| row.owner.index() as usize) + .collect::>(); + for id in cfg.reverse_postorder() { + let index = id.index() as usize; + live_in[index].extend(&owners); + live_out[index].extend(&owners); + for live in &mut after[index] { + live.extend(&owners); + } + } + Ok(()) +} + +fn mapped_local(map: &[Option], local: LocalId) -> Result { + map.get(local.index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + "temporary view compaction removed a live owner, alias or acquisition".into() + }) +} + +fn transition( + function: &mut Function, + before: &Function, + expected: &Function, + rows: Vec, +) -> Result<(), String> { + let witness = before + .temporary_local_views + .as_ref() + .ok_or("temporary transition has no before-witness")?; + witness.current(before)?; + if function.temporary_local_views.as_ref() != Some(witness) + || !Graph::new(expected).matches(function) + { + return Err("temporary preparation differs from exact canonical replay".into()); + } + if witness.current.graph.matches(function) && rows == witness.rows { + return witness.current(function); + } + let mut next = witness.clone(); + next.current = Arc::new(Current { + graph: Graph::new(expected), + rows: rows.clone(), + }); + next.rows = rows; + next.current(function)?; + function.temporary_local_views = Some(next); + Ok(()) +} + +pub(super) fn remap_blocks( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.temporary_local_views else { + return Ok(()); + }; + witness.current(before)?; + let cfg = ControlFlowGraph::analyze(before).map_err(|errors| format!("{errors:?}"))?; + let mut canonical = vec![None; before.blocks.len()]; + let mut next = 0_u32; + for block in &before.blocks { + if cfg.reverse_postorder().contains(&block.id) { + canonical[block.id.index() as usize] = Some(BlockId(next)); + next += 1; + } + } + if map != canonical { + return Err("temporary block map is not exact reachable dense compaction".into()); + } + let mut expected = before.clone(); + expected + .blocks + .retain(|block| map[block.id.index() as usize].is_some()); + let remap = |id: &mut BlockId| { + *id = map[id.index() as usize].expect("canonical reachable target has a map"); + }; + remap(&mut expected.entry); + for block in &mut expected.blocks { + remap(&mut block.id); + sequences::prune::block_targets(&mut block.terminator.kind, &remap); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_blocks(map)?; + } + transition(function, before, &expected, rows) +} + +fn canonical_local_map(function: &Function) -> Result>, String> { + let mut used = vec![false; function.locals.len()]; + let mut valid = true; + for parameter in &function.params { + if let Some(slot) = used.get_mut(parameter.local.index() as usize) { + *slot = true; + } else { + valid = false; + } + } + for mut block in function.blocks.clone() { + sequences::prune::block_locals( + &mut block, + &mut |id| { + if let Some(slot) = used.get_mut(id.index() as usize) { + *slot = true; + } else { + valid = false; + } + }, + &mut |_| {}, + ); + } + if !valid { + return Err("temporary local compaction input has an invalid local reference".into()); + } + loop { + let mut changed = false; + for local in &function.locals { + if used[local.id.index() as usize] + && let Some(source) = local.view_source + { + let slot = used + .get_mut(source.index() as usize) + .ok_or("temporary view has a foreign source local")?; + changed |= !*slot; + *slot = true; + } + } + if !changed { + break; + } + } + let mut next = 0_u32; + Ok(used + .into_iter() + .map(|used| { + used.then(|| { + let id = LocalId::new(next); + next += 1; + id + }) + }) + .collect()) +} + +pub(super) fn remap_locals( + function: &mut Function, + before: &Function, + map: &[Option], +) -> Result<(), String> { + let Some(witness) = &before.temporary_local_views else { + return Ok(()); + }; + witness.current(before)?; + if map != canonical_local_map(before)? { + return Err("temporary local map is not exact unused-local dense compaction".into()); + } + let mut expected = before.clone(); + expected + .locals + .retain(|local| map[local.id.index() as usize].is_some()); + for parameter in &mut expected.params { + parameter.local = mapped_local(map, parameter.local)?; + } + for local in &mut expected.locals { + local.id = mapped_local(map, local.id)?; + if let Some(source) = &mut local.view_source { + *source = mapped_local(map, *source)?; + } + } + let mut valid = true; + for block in &mut expected.blocks { + sequences::prune::block_locals( + block, + &mut |id| { + if let Ok(mapped) = mapped_local(map, *id) { + *id = mapped; + } else { + valid = false; + } + }, + &mut |floor| { + if let Some(prefix) = map.get(..*floor as usize) { + *floor = u32::try_from(prefix.iter().filter(|id| id.is_some()).count()) + .expect("canonical map width fits LocalId"); + } + }, + ); + } + if !valid { + return Err("temporary local replay removed an active reference".into()); + } + let mut rows = witness.rows.clone(); + for row in &mut rows { + row.remap_locals(map)?; + } + transition(function, before, &expected, rows) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + const SOURCE09: &str = include_str!( + "../../jett_driver/tests/native_conformance/local_view_aliases/09_temporary_root_boundary.jett" + ); + const FACTORY: &str = r#"namespace app +struct Packet: + data: bytes + sibling: bytes +function make_packet(view seed: bytes) returns Packet: + return Packet(data: clone seed, sibling: bytes.from_string("sibling")) +function exercise(view seed: bytes) returns bytes: + bytes borrowed = view make_packet(view seed).data + bytes forwarded = view borrowed + print("after alias creation") + return clone forwarded +"#; + const COMPACT: &str = r#"namespace app +struct Packet: + data: bytes +function make_packet() returns Packet: + return Packet(data: bytes.from_string("AB")) +function main() returns nothing: + function() returns int64 callback = function() returns int64: + int64 child_local = 7 + return child_local + bytes borrowed = view make_packet().data +"#; + fn lower_source(source: &str) -> (Program, Arc) { + let project = jett_common::FileId::new(0); + let list = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START); + let bytes = jett_common::FileId::new(jett_common::STDLIB_FILE_ID_START + 1); + // Keep the original project text and FileId. Load the shipped facades + // in dependency order, as the other MIR Source fixture helpers do. + let mut parsed = jett_parser::parse(include_str!("../../../stdlib/list.jett"), list); + let mut byte_module = jett_parser::parse(include_str!("../../../stdlib/bytes.jett"), bytes); + let mut primary = jett_parser::parse(source, project); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + assert!(byte_module.errors.is_empty(), "{:?}", byte_module.errors); + assert!(primary.errors.is_empty(), "{:?}", primary.errors); + parsed.module.items.append(&mut byte_module.module.items); + parsed.module.items.append(&mut primary.module.items); + let checked = Arc::new( + jett_typecheck::CheckedResourceProgram::prepare( + parsed, + HashMap::from([ + (project, jett_common::SourceOrigin::Project), + (list, jett_common::SourceOrigin::Stdlib), + (bytes, jett_common::SourceOrigin::Stdlib), + ]), + &[], + jett_typecheck::CheckOptions { release: false }, + ) + .expect("genuine checked Source program with shipped stdlib"), + ); + let source = hir::lower_checked_ordinary_program(&checked).expect("genuine checked HIR"); + let types = &checked.checked().interner; + let program = lower(&source, types).expect("genuine checked MIR"); + (program, checked) + } + fn named<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap() + } + fn named_mut<'a>(program: &'a mut Program, name: &str) -> &'a mut Function { + program + .functions + .iter_mut() + .find(|function| function.identity.declaration.name == name) + .unwrap() + } + fn first(function: &Function) -> Row { + function.temporary_local_views.as_ref().unwrap().rows[0].clone() + } + fn prepare(program: &mut Program, types: &TypeInterner) { + crate::prepare_native_sequences(program, types); + crate::prepare_native_uninhabited_sums(program, types); + crate::prepare_native_generated_functions(program, types); + } + + /// A direct private replay fixture, deliberately not Source-generation + /// evidence. Genuine Source09 supplies the immutable HIR receipt and exact + /// original operations. Insert one empty disconnected block at index zero, + /// relocate every original block/edge/row by one, and install the matching + /// test-only temporary current seal. Independent Resource receipts stay + /// unchanged and correctly reject this expanded graph. This supplies only + /// a preimage for direct temporary-replay tests, never real-pruner or + /// compiler-generated disconnected-Source acceptance evidence. + /// + /// Ordinary lower_if creates no dead join after two returning arms, so that + /// tempting Source fixture cannot establish a removable disconnected block. + fn synthetic_block_preimage( + owner_reachable: bool, + ) -> ( + Program, + Arc, + Function, + ) { + let (mut program, checked) = lower_source(SOURCE09); + let types = &checked.checked().interner; + let original = named(&program, "main").clone(); + validate(&original, types).unwrap(); + crate::validate_call_ownership(&program, types).unwrap(); + let mut expanded = original.clone(); + let shift = |id: &mut BlockId| { + *id = BlockId(id.index().checked_add(1).expect("small Source09 test CFG")); + }; + shift(&mut expanded.entry); + for block in &mut expanded.blocks { + shift(&mut block.id); + sequences::prune::block_targets(&mut block.terminator.kind, &shift); + } + expanded.blocks.insert( + 0, + BasicBlock { + id: BlockId(0), + statements: Vec::new(), + terminator: Terminator { + kind: TerminatorKind::Unreachable, + span: original.span, + }, + }, + ); + if !owner_reachable { + expanded.entry = BlockId(0); + } + let map = original + .blocks + .iter() + .map(|block| Some(BlockId(block.id.index().checked_add(1).unwrap()))) + .collect::>(); + let mut witness = original.temporary_local_views.as_ref().unwrap().clone(); + for row in &mut witness.rows { + row.remap_blocks(&map).unwrap(); + } + witness.current = Arc::new(Current { + graph: Graph::new(&expanded), + rows: witness.rows.clone(), + }); + expanded.temporary_local_views = Some(witness); + validate(&expanded, types).unwrap(); + let cfg = ControlFlowGraph::analyze(&expanded).unwrap(); + assert_eq!( + cfg.reverse_postorder() + .contains(&first(&expanded).initialize.block), + owner_reachable + ); + assert_eq!( + cfg.reverse_postorder().contains(&BlockId(0)), + !owner_reachable + ); + *named_mut(&mut program, "main") = expanded; + let errors = crate::validate_call_ownership(&program, types).unwrap_err(); + assert!(errors.iter().any(|error| error.message.contains( + "Resource ownership differs from its initially authenticated Source or constructor-emitted graph" + ))); + (program, checked, original) + } + + #[test] + fn unchanged_unused_source09_has_one_owned_slot_pinned_without_alias_reads() { + let (mut program, checked) = lower_source(SOURCE09); + let types = &checked.checked().interner; + let function = named(&program, "main"); + let row = first(function); + let plan = move_values::MoveValuePlan::analyze(&program, function, types).unwrap(); + let owner = row.owner.index() as usize; + assert!(plan.owned_locals.contains(&owner)); + assert!(!plan.owned_locals.contains(&(row.alias.index() as usize))); + let cfg = ControlFlowGraph::analyze(function).unwrap(); + for block in cfg.reverse_postorder() { + let index = block.index() as usize; + assert!(plan.live_in[index].contains(&owner)); + assert!(plan.live_out[index].contains(&owner)); + assert!( + plan.live_after_statement[index] + .iter() + .all(|set| set.contains(&owner)) + ); + } + let before = program.clone(); + prepare(&mut program, types); + assert_eq!( + program, before, + "identity preparation preserves witness identity" + ); + crate::validate_call_ownership(&program, types).unwrap(); + } + + #[test] + fn stable_view_factory_actual_has_one_direct_whole_result_acquisition() { + let (program, checked) = lower_source(FACTORY); + let types = &checked.checked().interner; + let function = named(&program, "exercise"); + let row = first(function); + assert_eq!(row.acquisition, Acquisition::Direct); + let (owner, initializer) = let_at(function, row.initialize).unwrap(); + assert_eq!(owner, row.owner); + let E::Call { + function: target, + args, + evaluation_order, + ownership: CallOwnership::Source(packet), + } = &initializer.kind + else { + panic!("the whole producer result initializes its owning slot directly"); + }; + let factory = named(&program, "make_packet"); + assert_eq!(*target, factory.id); + assert_eq!(factory.params[0].mode, ParamMode::View); + assert_eq!(evaluation_order, &[0]); + let E::View(endpoint) = &args[0].kind else { + panic!("written stable view actual"); + }; + let E::Local(seed) = endpoint.kind else { + panic!("original view parameter"); + }; + assert_eq!( + function.parameter_for_local(seed).unwrap().mode, + ParamMode::View + ); + assert_eq!( + packet.arguments[0].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + packet.arguments[0].physical_access, + jett_typecheck::CheckedCalleeAccess::View + ); + assert_eq!(packet.arguments[0].staging, hir::ArgumentStaging::Original); + // No eager operand or ownership transfer requires a call-loan scope: + // the checked view parameter remains an original borrowed operand. + let mut calls = 0; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + calls += usize::from(matches!( + value.kind, + E::Call { function, .. } if function == factory.id + )); + }); + } + assert_eq!(calls, 1); + assert_eq!(runtime_occurrences(function, row.owner), 2); + assert_eq!(row.publish.block, row.initialize.block); + assert_eq!(row.publish.statement, row.initialize.statement + 1); + crate::validate_call_ownership(&program, types).unwrap(); + let plan = move_values::MoveValuePlan::analyze(&program, function, types).unwrap(); + let return_block = function + .blocks + .iter() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: E::Clone(_), + .. + })) + ) + }) + .unwrap(); + assert!( + plan.live_out[return_block.id.index() as usize].contains(&(row.owner.index() as usize)) + ); + // The unviewed sibling belongs to Packet's whole owning slot. No leaf + // owner, runtime retain, emitted cleanup or measurement is invented here. + assert!(matches!( + types.resolve(function.local(row.owner).unwrap().ty), + Type::Struct(_) + )); + } + + #[test] + fn temporary_factory_actual_with_later_handle_stages_call_before_lexical_owner() { + // A written temporary alone need not force MIR staging. This genuine + // later handled actual requires lexical evaluation before the handler, + // preserving both the inner call loan and the outer whole Packet owner. + let source = FACTORY + .replace( + "function make_packet(view seed: bytes) returns Packet:", + "function make_packet(view seed: bytes, marker: int64) returns Packet:", + ) + .replace( + "function exercise(view seed: bytes) returns bytes:", + "function exercise(incoming: optional[int64]) returns bytes:", + ) + .replace( + "view make_packet(view seed).data", + "view make_packet(view bytes.from_string(\"AB\"), incoming handle:\n default 3\n ).data", + ); + let (program, checked) = lower_source(&source); + let types = &checked.checked().interner; + let function = named(&program, "exercise"); + let row = first(function); + let Acquisition::Staged { + local, + call, + ref ends, + } = row.acquisition + else { + panic!("the handled later actual requires the complete call before its loan ends"); + }; + assert_eq!(runtime_occurrences(function, local), 2); + assert_eq!(runtime_occurrences(function, row.owner), 2); + assert_eq!(call.block, row.initialize.block); + let (actual_call, value) = unique_let(function, local).unwrap(); + assert_eq!(actual_call, call); + let E::Call { + function: target, + evaluation_order, + ownership: CallOwnership::Source(packet), + .. + } = &value.kind + else { + panic!("genuine named factory acquisition"); + }; + assert_eq!(*target, named(&program, "make_packet").id); + assert_eq!(evaluation_order, &[0, 1]); + let hir::ArgumentStaging::Borrowed { loan } = packet.arguments[0].staging else { + panic!("the exact first temporary actual retains its checked loan"); + }; + assert_eq!( + packet.arguments[0].syntax, + jett_typecheck::CheckedCallerSyntax::WrittenView + ); + assert_eq!( + packet.arguments[0].effect, + jett_typecheck::CheckedCallerEffect::RetainBorrow + ); + let actual_owner = function.local(loan).unwrap().view_source.unwrap(); + let (actual_site, actual_value) = unique_let(function, actual_owner).unwrap(); + assert!(matches!(actual_value.kind, E::Call { function, .. } + if function == named(&program, "from_string").id)); + let begins = function + .blocks + .iter() + .flat_map(|block| { + block.statements.iter().enumerate().filter_map(move |(statement, value)| { + matches!(value.kind, StatementKind::BeginCallView { local, .. } if local == loan) + .then_some(Site { block: block.id, statement }) + }) + }) + .collect::>(); + let [begin] = begins.as_slice() else { + panic!("one loan for the first actual"); + }; + assert_eq!(actual_site.block, begin.block); + assert!(actual_site.statement < begin.statement); + let later_sum = function.blocks[begin.block.index() as usize] + .statements + .iter() + .position(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .unwrap(); + assert!( + begin.statement < later_sum, + "the first temporary actual is acquired before handling the second actual" + ); + let mut factory_calls = 0; + let mut actual_calls = 0; + for block in &function.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if let E::Call { + function: target, .. + } = value.kind + { + factory_calls += usize::from(target == named(&program, "make_packet").id); + actual_calls += usize::from(target == named(&program, "from_string").id); + } + }); + } + assert_eq!(factory_calls, 1); + assert_eq!(actual_calls, 1); + assert_eq!( + function.local(local).unwrap().ty, + named(&program, "make_packet").return_type + ); + assert!(function.local(local).unwrap().view_source.is_none()); + assert_eq!(ends.len(), 1); + assert!(ends.iter().all(|site| matches!( + function.blocks[site.block.index() as usize].statements[site.statement].kind, + StatementKind::EndCallView { local } if local == loan + ))); + assert!(call.statement < ends[0].statement); + assert!(ends[0].statement < row.initialize.statement); + assert_eq!(row.publish.statement, row.initialize.statement + 1); + let (_, owner_value) = let_at(function, row.initialize).unwrap(); + assert!(matches!(owner_value.kind, E::Local(id) if id == local)); + crate::validate_call_ownership(&program, types).unwrap(); + let plan = move_values::MoveValuePlan::analyze(&program, function, types).unwrap(); + assert!(plan.owned_locals.contains(&(row.owner.index() as usize))); + assert!(!plan.owned_locals.contains(&(row.alias.index() as usize))); + assert!(!plan.owned_locals.contains(&(loan.index() as usize))); + let returned = function + .blocks + .iter() + .find(|block| { + matches!( + block.terminator.kind, + TerminatorKind::Return(Some(Expression { + kind: E::Clone(_), + .. + })) + ) + }) + .unwrap(); + assert!( + plan.live_out[returned.id.index() as usize].contains(&(row.owner.index() as usize)) + ); + } + + #[test] + fn genuine_child_local_compaction_remaps_owner_alias_and_all_source_metadata() { + let (mut program, checked) = lower_source(COMPACT); + let types = &checked.checked().interner; + let before = named(&program, "main").clone(); + let old = first(&before); + let original = before + .temporary_local_views + .as_ref() + .unwrap() + .original + .clone(); + crate::prepare_native_generated_functions(&mut program, types); + let after = named(&program, "main"); + let new = first(after); + assert!(after.locals.len() < before.locals.len()); + assert!( + new.owner.index() < old.owner.index(), + "requires a genuine nonidentity map before the owner" + ); + assert!(new.alias.index() < old.alias.index()); + assert!(Arc::ptr_eq( + &original, + &after.temporary_local_views.as_ref().unwrap().original + )); + assert_eq!( + original.source.locals, + before + .temporary_local_views + .as_ref() + .unwrap() + .original + .source + .locals + ); + crate::validate_call_ownership(&program, types).unwrap(); + move_values::MoveValuePlan::analyze(&program, after, types).unwrap(); + } + + #[test] + fn identity_map_cannot_reseal_a_function_that_requires_nonidentity_compaction() { + let (program, _) = lower_source(COMPACT); + let before = named(&program, "main"); + let map = before + .locals + .iter() + .map(|local| Some(local.id)) + .collect::>(); + assert_ne!(map, canonical_local_map(before).unwrap()); + let mut unchanged = before.clone(); + assert!(remap_locals(&mut unchanged, before, &map).is_err()); + assert_eq!(&unchanged, before); + } + + #[test] + fn isolated_private_block_replay_restores_the_original_graph_and_independent_receipts() { + let (mut program, checked, original) = synthetic_block_preimage(true); + let types = &checked.checked().interner; + let before = named(&program, "main").clone(); + let old = first(&before); + let original_receipt = before + .temporary_local_views + .as_ref() + .unwrap() + .original + .clone(); + assert_eq!(before.blocks.len(), original.blocks.len() + 1); + assert!(before.entry.index() > original.entry.index()); + // The artificial preimage has no matching Resource Source graph. The + // public pruner must refuse it and preserve every independent receipt. + let mut refused = before.clone(); + assert!(!sequences::prune::unreachable(&mut refused)); + assert_eq!(refused, before); + + // Directly exercise only temporary replay using the known exact dense + // map of this synthetic one-gap preimage. Restoring the original graph + // also restores compatibility with its unchanged genuine Resource seal. + let map = std::iter::once(None) + .chain(original.blocks.iter().map(|block| Some(block.id))) + .collect::>(); + let mut candidate = before.clone(); + candidate + .blocks + .retain(|block| map[block.id.index() as usize].is_some()); + let relocate = |id: &mut BlockId| { + *id = map[id.index() as usize].unwrap(); + }; + relocate(&mut candidate.entry); + for block in &mut candidate.blocks { + relocate(&mut block.id); + sequences::prune::block_targets(&mut block.terminator.kind, &relocate); + } + remap_blocks(&mut candidate, &before, &map).unwrap(); + validate(&candidate, types).unwrap(); + assert_eq!(candidate.resource_lowering, original.resource_lowering); + *named_mut(&mut program, "main") = candidate; + let compacted = named(&program, "main"); + let new = first(compacted); + assert_eq!(compacted.blocks.len() + 1, before.blocks.len()); + assert_eq!( + new.initialize.block.index() + 1, + old.initialize.block.index() + ); + assert_eq!(new.publish.block.index() + 1, old.publish.block.index()); + assert!( + Graph::new(&original).matches(compacted), + "all original operations and headers survive exact replay" + ); + assert!(Arc::ptr_eq( + &original_receipt, + &compacted.temporary_local_views.as_ref().unwrap().original + )); + assert_eq!( + compacted.temporary_local_views.as_ref().unwrap().rows, + original.temporary_local_views.as_ref().unwrap().rows + ); + crate::validate_call_ownership(&program, types).unwrap(); + move_values::MoveValuePlan::analyze(&program, compacted, types).unwrap(); + } + + #[test] + fn private_block_replay_refuses_altered_dense_or_reachability_maps_without_mutation() { + let (program, _, original) = synthetic_block_preimage(true); + let before = named(&program, "main"); + // The fixture is the exact inverse of a one-gap canonical compaction. + // This is a direct temporary-replay control, not a valid whole-program + // preimage for the public pruner or a Source-generated dead block. + let canonical = std::iter::once(None) + .chain(original.blocks.iter().map(|block| Some(block.id))) + .collect::>(); + let mut wrong_target = canonical.clone(); + wrong_target[before.entry.index() as usize] = Some(before.entry); + let mut keep_disconnected = canonical.clone(); + keep_disconnected[0] = Some(BlockId(0)); + for altered in [wrong_target, keep_disconnected] { + assert_ne!(altered, canonical); + let mut unchanged = before.clone(); + assert!(remap_blocks(&mut unchanged, before, &altered).is_err()); + assert_eq!( + &unchanged, before, + "refused maps cannot change the graph or renew its seal" + ); + } + } + + #[test] + fn public_marked_call_transplant_cannot_lose_the_private_function_witness() { + let source = SOURCE09.replace( + "function main()", + concat!( + "function inspect(view data: bytes) returns nothing:\n return nothing\n", + "function empty() returns nothing:\n return nothing\nfunction main()", + ), + ) + " inspect(view borrowed)\n"; + let (original, checked) = lower_source(&source); + let types = &checked.checked().interner; + for target_name in ["make_packet", "inspect"] { + let target = named(&original, target_name).id; + let donor = named(&original, "main"); + let mut marked = Vec::new(); + for block in &donor.blocks { + resource_ownership::walk::mir_block(block, &mut |value| { + if matches!(&value.kind, E::Call { function, ownership, .. } + if *function == target && ownership.has_temporary_local_receipts()) + { + marked.push(value.clone()); + } + }); + } + let [call] = marked.as_slice() else { + panic!("one genuine marked {target_name} invocation"); + }; + // make_packet carries the producer marker; inspect carries the + // argument's temporary-origin receipt. Neither can mint a witness. + for disconnected in [false, true] { + let mut program = original.clone(); + let mut transplanted = named(&program, "empty").clone(); + assert!(!has_records(&transplanted)); + // Only public fields are copied into a genuine unmarked + // Function. External callers cannot set its private witness. + transplanted.locals = donor.locals.clone(); + let statement = Statement { + kind: StatementKind::Evaluate(call.clone()), + span: call.span, + }; + if disconnected { + let block = BlockId(u32::try_from(transplanted.blocks.len()).unwrap()); + transplanted.blocks.push(BasicBlock { + id: block, + statements: vec![statement], + terminator: Terminator { + kind: TerminatorKind::Return(None), + span: call.span, + }, + }); + assert!( + !ControlFlowGraph::analyze(&transplanted) + .unwrap() + .reverse_postorder() + .contains(&block) + ); + } else { + transplanted.blocks[transplanted.entry.index() as usize] + .statements + .push(statement); + } + assert!(has_receipts(&transplanted)); + assert!( + validate_current(&transplanted) + .unwrap_err() + .contains("no original private MIR witness") + ); + let index = transplanted.id.index() as usize; + program.functions[index] = transplanted; + let unchanged = program.clone(); + assert!(crate::validate_call_ownership(&program, types).is_err()); + prepare(&mut program, types); + assert_eq!(program, unchanged); + } + } + } + + #[test] + fn corrupt_unused_records_are_refused_by_all_preparation_and_standalone_planners() { + let (original, checked) = lower_source(SOURCE09); + let types = &checked.checked().interner; + for mutation in 0..5 { + let mut program = original.clone(); + let function = named_mut(&mut program, "main"); + let row = first(function); + match mutation { + 0 => { + function.blocks[row.initialize.block.index() as usize] + .statements + .remove(row.initialize.statement); + } + 1 => { + let value = function.blocks[row.initialize.block.index() as usize].statements + [row.initialize.statement] + .clone(); + function.blocks[row.initialize.block.index() as usize] + .statements + .insert(row.initialize.statement, value); + } + 2 => { + function.locals[row.owner.index() as usize].ty = TypeInterner::STRING; + } + 3 => { + function.blocks[row.publish.block.index() as usize] + .terminator + .kind = TerminatorKind::Unreachable; + } + 4 => { + function.temporary_local_views.as_mut().unwrap().rows[0].owner = row.alias; + } + _ => unreachable!(), + } + let unchanged = program.clone(); + assert!(crate::validate_call_ownership(&program, types).is_err()); + assert!(copy_values::CopyValuePlan::analyze(named(&program, "main"), types).is_err()); + assert!( + move_values::MoveValuePlan::analyze(&program, named(&program, "main"), types) + .is_err() + ); + prepare(&mut program, types); + assert_eq!( + program, unchanged, + "preparation must refuse before hiding mutation {mutation}" + ); + } + } + + #[test] + fn isolated_private_disconnected_initialization_corruption_is_not_pruned_away() { + let (mut program, checked, _) = synthetic_block_preimage(false); + let types = &checked.checked().interner; + let function = named_mut(&mut program, "main"); + validate(function, types).unwrap(); + let row = first(function); + let cfg = ControlFlowGraph::analyze(function).unwrap(); + assert!(!cfg.reverse_postorder().contains(&row.initialize.block)); + function.blocks[row.initialize.block.index() as usize] + .statements + .remove(row.initialize.statement); + assert!( + validate_current(function).is_err(), + "the temporary witness itself detects the missing initialization" + ); + let unchanged = program.clone(); + assert!(crate::validate_call_ownership(&program, types).is_err()); + prepare(&mut program, types); + assert_eq!(program, unchanged); + let function = named_mut(&mut program, "main"); + assert!(!sequences::prune::unreachable(function)); + assert!(!sequences::prune::unused_locals(function)); + assert_eq!(program, unchanged); + } +} diff --git a/crates/jett_mir/src/uninhabited_sums.rs b/crates/jett_mir/src/uninhabited_sums.rs new file mode 100644 index 00000000..a64a054c --- /dev/null +++ b/crates/jett_mir/src/uninhabited_sums.rs @@ -0,0 +1,223 @@ +//! Remove impossible handler arms without removing runtime sum validation. +use super::*; +use jett_hir::ExpressionKind; +use jett_types::Type; + +/// Private preparation authority. The original plan is never a public setter +/// and this record is minted only beside the canonical Branch-to-Goto rewrite. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedAbsentSuccess { + plan: call_ownership::AbsentSuccessPlan, +} + +impl PreparedAbsentSuccess { + pub(super) fn plan(&self) -> &call_ownership::AbsentSuccessPlan { + &self.plan + } + + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(selecting), Some(failure)) = (get(self.plan.selecting), get(self.plan.failure)) + else { + return false; + }; + self.plan.selecting = selecting; + self.plan.failure = failure; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(tag), Some(output)) = ( + get(self.plan.source), + get(self.plan.tag), + get(self.plan.output), + ) else { + return false; + }; + self.plan.source = source; + self.plan.tag = tag; + self.plan.output = output; + true + } +} + +/// Distinct private authority for an inhabited successful payload whose error +/// arm is Never. Original validation proves the removed false edge; this record +/// retains only the actual selected blocks, runtime tag and successful take. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct PreparedPresentSuccess { + plan: call_ownership::PresentSuccessPlan, +} + +impl PreparedPresentSuccess { + pub(super) fn plan(&self) -> &call_ownership::PresentSuccessPlan { + &self.plan + } + + pub(super) fn remap_blocks(&mut self, blocks: &[Option]) -> bool { + let get = |id: BlockId| blocks.get(id.index() as usize).copied().flatten(); + let (Some(selecting), Some(success)) = (get(self.plan.selecting), get(self.plan.success)) + else { + return false; + }; + self.plan.selecting = selecting; + self.plan.success = success; + true + } + + pub(super) fn remap_locals(&mut self, locals: &[Option]) -> bool { + let get = |id: LocalId| locals.get(id.index() as usize).copied().flatten(); + let (Some(source), Some(tag), Some(output)) = ( + get(self.plan.source), + get(self.plan.tag), + get(self.plan.output), + ) else { + return false; + }; + self.plan.source = source; + self.plan.tag = tag; + self.plan.output = output; + true + } +} + +#[cfg(test)] +mod tests; + +/// Select the inhabited arm of a checked optional/result handler. The final +/// `SumTag` still evaluates the sum and rejects pending values at runtime. +pub fn prepare_native_uninhabited_sums(program: &mut Program, types: &TypeInterner) { + if crate::actor_graph::validate_native_actor_graphs(program).is_err() { + return; + } + // Never hide an invalid ID in an arm that compaction would otherwise erase. + if validate_call_ownership(program, types).is_err() { + return; + } + // Obtain all plans while every original block and source occurrence exists. + // Any failure leaves the entire input unchanged, as does the gate above. + let Ok(plans) = program + .functions + .iter() + .map(|function| { + call_ownership::validate_function(program, function, types) + .map(|proof| (proof.absent_successes, proof.present_successes)) + }) + .collect::, _>>() + else { + return; + }; + for (function, (absent_plans, present_plans)) in program.functions.iter_mut().zip(plans) { + if crate::temporary_local_views::has_records(function) { + // Pending composition: selecting new sum edges needs exact temporary + // edit replay. The ordinary backend may still emit the original CFG. + continue; + } + let before = (crate::resource_ownership::has_records(function) + || function.reflected_callable.is_some() + || crate::ordinary_borrowed_sums::has_records(function) + || crate::field_updates::has_records(function) + || crate::actor_handlers::has_records(function) + || !function.breakpoint_regions.is_empty() + || crate::call_owner_generations::has_records(function)) + .then(|| function.clone()); + let mut changed = false; + for block in &mut function.blocks { + let Some(Statement { + kind: StatementKind::SumTag { source, target }, + .. + }) = block.statements.last() + else { + continue; + }; + let TerminatorKind::Branch { + condition, + then_block, + else_block, + } = &block.terminator.kind + else { + continue; + }; + if condition.ty != TypeInterner::BOOL + || !matches!(condition.kind, ExpressionKind::Local(local) if local == *target) + || function.locals[target.index() as usize].ty != TypeInterner::BOOL + { + continue; + } + let ty = function.locals[source.index() as usize].ty; + if ty.index() as usize >= types.len() { + continue; + } + let selected = match types.resolve(ty) { + Type::Optional(inner) if *inner == TypeInterner::NEVER => *else_block, + Type::Result(ok, error) + if *ok == TypeInterner::NEVER + && *error != TypeInterner::NEVER + && (error.index() as usize) < types.len() => + { + *else_block + } + Type::Result(ok, error) + if *ok != TypeInterner::NEVER + && *error == TypeInterner::NEVER + && (ok.index() as usize) < types.len() => + { + *then_block + } + _ => continue, + }; + // Retain every statement, including the source snapshot and SumTag. + // Replacing only this edge preserves evaluation and failure order. + for plan in absent_plans.iter().filter(|plan| { + plan.selecting == block.id + && plan.failure == selected + && plan.source == *source + && plan.tag == *target + }) { + function + .prepared_absent_successes + .push(PreparedAbsentSuccess { plan: plan.clone() }); + } + for plan in present_plans.iter().filter(|plan| { + plan.selecting == block.id + && plan.success == selected + && plan.source == *source + && plan.tag == *target + }) { + function + .prepared_present_successes + .push(PreparedPresentSuccess { plan: plan.clone() }); + } + block.terminator.kind = TerminatorKind::Goto(selected); + changed = true; + } + if changed { + // Every nested tag has its own checked source type, so one scan + // selects all eligible arms before a single dense compaction. + let regions_valid = before.as_ref().is_none_or(|before| { + crate::actor_handlers::sum_transition(function, before, types).is_ok() + && crate::field_updates::sum_transition(function, before, types).is_ok() + && crate::call_owner_generations::sum_transition(function, before, types) + .is_ok() + && crate::breakpoint_regions::sum_transition(function, before, types).is_ok() + }); + if !regions_valid + || crate::ordinary_borrowed_sums::validate_current(function).is_err() + || crate::resource_ownership::validate_current(function).is_err() + || !sequences::prune::unreachable(function) + { + if let Some(before) = before { + *function = before; + } + } else if let Some(before) = before { + if crate::reflected_callable::prepared(function, &before).is_err() { + *function = before; + } + } + } + } + for function in &mut program.functions { + crate::actor_graph::refresh(function); + } +} diff --git a/crates/jett_mir/src/uninhabited_sums/tests.rs b/crates/jett_mir/src/uninhabited_sums/tests.rs new file mode 100644 index 00000000..0ecf1c55 --- /dev/null +++ b/crates/jett_mir/src/uninhabited_sums/tests.rs @@ -0,0 +1,498 @@ +use super::*; +use jett_common::{FileId, SourceOrigin}; +use std::collections::{HashMap, HashSet}; + +fn lower_source(source: &str) -> (Program, TypeInterner) { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = jett_typecheck::check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + let hir = hir::lower( + &parsed.module, + &resolved, + &checked, + &HashMap::from([(file, SourceOrigin::Project)]), + ) + .expect("HIR lowering"); + ( + lower(&hir, &checked.interner).expect("MIR lowering"), + checked.interner, + ) +} + +fn named_function<'a>(program: &'a Program, name: &str) -> &'a Function { + program + .functions + .iter() + .find(|function| function.identity.declaration.name == name) + .unwrap() +} + +fn foreign_type_id(types: &TypeInterner) -> TypeId { + let mut foreign = TypeInterner::new(); + for index in 0..types.len() + 8 { + foreign.intern(Type::Refinement { + name: format!("ForeignUninhabitedTest{index}"), + base: TypeInterner::INT64, + }); + } + foreign.intern(Type::Refinement { + name: "ForeignUninhabitedTestFinal".into(), + base: TypeInterner::INT64, + }) +} + +fn assert_dense_reachable(function: &Function) { + let cfg = ControlFlowGraph::analyze(function).unwrap(); + let mut reachable = HashSet::new(); + let mut pending = vec![function.entry]; + while let Some(block) = pending.pop() { + if reachable.insert(block) { + pending.extend_from_slice(cfg.successors(block)); + } + } + assert_eq!(reachable.len(), function.blocks.len()); + for (index, block) in function.blocks.iter().enumerate() { + assert_eq!(block.id.index() as usize, index); + } + for (index, local) in function.locals.iter().enumerate() { + assert_eq!(local.id.index() as usize, index); + assert_ne!(local.ty, TypeInterner::NEVER); + } +} + +fn tags(function: &Function) -> usize { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, StatementKind::SumTag { .. })) + .count() +} + +fn takes(function: &Function) -> Vec { + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter_map(|statement| match statement.kind { + StatementKind::SumTake { success, .. } => Some(success), + _ => None, + }) + .collect() +} + +const OPTIONAL: &str = r#"namespace app +function inspect[T](value: optional[T]) returns nothing: + T item = value handle: + return nothing + trace item + return nothing +function main() returns nothing: + inspect(none) + return nothing +"#; + +#[test] +fn optional_never_retains_checked_tag_and_function_metadata_without_extracting() { + let (mut program, types) = lower_source(OPTIONAL); + let original = named_function(&program, "inspect").clone(); + assert_eq!(tags(&original), 1); + assert_eq!(takes(&original), [true]); + let original_main = named_function(&program, "main").clone(); + prepare_native_uninhabited_sums(&mut program, &types); + validate(&program).unwrap(); + let function = named_function(&program, "inspect"); + assert_dense_reachable(function); + assert_eq!(tags(function), 1, "runtime SumHandleTag guard is retained"); + assert!(takes(function).is_empty()); + assert_eq!(function.id, original.id); + assert_eq!(function.identity, original.identity); + assert_eq!(function.debug_kind, original.debug_kind); + assert_eq!(function.params, original.params); + assert_eq!(function.capture_count, original.capture_count); + assert_eq!(function.return_type, original.return_type); + assert_eq!(function.span, original.span); + assert_eq!(named_function(&program, "main"), &original_main); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("absent sum snapshot is cleaned up"); + let prepared = program.clone(); + prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!(program, prepared, "preparation is idempotent"); +} + +#[test] +fn result_never_selects_either_inhabited_arm_and_keeps_owned_error_extraction() { + let (mut program, types) = lower_source( + r#"namespace app +function keep[T, E](outcome: result[T, E]) returns optional[T]: + T item = outcome handle error: + trace error + return none + return some(item) +function consume[T](value: optional[T]) returns nothing: + return nothing +function main() returns nothing: + consume(keep(fail("failure"))) + consume(keep(ok(7))) + return nothing +"#, + ); + prepare_native_uninhabited_sums(&mut program, &types); + validate(&program).unwrap(); + let mut directions = Vec::new(); + for function in program + .functions + .iter() + .filter(|function| function.identity.declaration.name == "keep") + { + assert_dense_reachable(function); + assert_eq!(tags(function), 1); + let Type::Result(ok, error) = types.resolve(function.params[0].ty) else { + panic!("checked result parameter"); + }; + let success = *error == TypeInterner::NEVER; + assert_eq!(*ok == TypeInterner::NEVER, !success); + assert_eq!(takes(function), [success]); + let traces = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, StatementKind::Trace(_))) + .count(); + assert_eq!(traces, usize::from(!success)); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("selected payload and unselected sum owners are cleaned up"); + directions.push(success); + } + directions.sort(); + assert_eq!(directions, [false, true]); +} + +#[test] +fn nested_uninhabited_handlers_are_selected_in_one_pass_with_prior_effects() { + let (mut program, types) = lower_source( + r#"namespace app +function nested[T, E](outer: result[T, E], inner: optional[T]) returns int64: + T item = outer handle error: + trace error + T other = inner handle: + return 7 + trace other + return 8 + trace item + return 9 +function main() returns int64: + return nested(fail("failure"), none) +"#, + ); + prepare_native_uninhabited_sums(&mut program, &types); + validate(&program).unwrap(); + let function = named_function(&program, "nested"); + assert_dense_reachable(function); + assert_eq!(tags(function), 2); + assert_eq!(takes(function), [false]); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(statement.kind, StatementKind::Trace(_))) + .count(), + 1 + ); + let returns = function + .blocks + .iter() + .filter_map(|block| match &block.terminator.kind { + TerminatorKind::Return(Some(Expression { + kind: ExpressionKind::Int(value), + .. + })) => Some(*value), + _ => None, + }) + .collect::>(); + assert_eq!(returns, [7]); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("nested failure exits clean both snapshots"); +} + +#[test] +fn pending_sum_evaluation_is_retained_once_before_the_unchanged_runtime_guard() { + fn pending_input_shape(function: &Function) -> Vec<(TypeId, Span)> { + assert_eq!(tags(function), 1); + let block = function + .blocks + .iter() + .find(|block| { + matches!( + block.statements.last().map(|statement| &statement.kind), + Some(StatementKind::SumTag { .. }) + ) + }) + .unwrap(); + let StatementKind::SumTag { source, .. } = block.statements.last().unwrap().kind else { + unreachable!(); + }; + let definitions = block + .statements + .iter() + .filter_map(|statement| match &statement.kind { + StatementKind::Let { local, value } if *local == source => Some(value), + _ => None, + }) + .collect::>(); + assert_eq!( + definitions.len(), + 1, + "sum input is evaluated once before its tag" + ); + let outer = definitions[0]; + let ExpressionKind::Run(inner) = &outer.kind else { + panic!("sum input must retain its outer pending layer: {outer:?}"); + }; + let ExpressionKind::Run(cloned) = &inner.kind else { + panic!("sum input must retain its second pending layer: {inner:?}"); + }; + let ExpressionKind::Clone(input) = &cloned.kind else { + panic!("pending sum must own a separate input snapshot: {cloned:?}"); + }; + assert!( + matches!(input.kind, ExpressionKind::Local(local) if local == function.params[0].local) + ); + assert_eq!( + function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, StatementKind::Let { value, .. } if matches!(value.kind, ExpressionKind::Run(_)))) + .count(), + 1, + "the depth-two input is not evaluated again elsewhere" + ); + [outer, inner.as_ref(), cloned.as_ref(), input.as_ref()] + .map(|expression| (expression.ty, expression.span)) + .to_vec() + } + + let (mut program, types) = lower_source( + r#"namespace app +function inspect[T](value: optional[T]) returns nothing: + T item = (run run clone value) handle: + return nothing + trace item + return nothing +function main() returns nothing: + inspect(none) + return nothing +"#, + ); + let original = named_function(&program, "inspect"); + let input_shape = pending_input_shape(original); + prepare_native_uninhabited_sums(&mut program, &types); + validate(&program).unwrap(); + let function = named_function(&program, "inspect"); + assert_dense_reachable(function); + assert_eq!(pending_input_shape(function), input_shape); + let guards = function + .blocks + .iter() + .filter_map(|block| { + let StatementKind::SumTag { .. } = &block.statements.last()?.kind else { + return None; + }; + assert!(matches!(block.terminator.kind, TerminatorKind::Goto(_))); + Some(()) + }) + .count(); + assert_eq!(guards, 1); + assert!(takes(function).is_empty()); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("pending guard owns a separate snapshot until failure cleanup"); +} + +#[test] +fn owned_and_borrowed_local_sums_are_snapshotted_before_reuse() { + for borrowed in [false, true] { + let mode = if borrowed { "view " } else { "" }; + let source = format!( + r#"namespace app +function inspect[T]({mode}value: optional[T]) returns nothing: + T item = value handle: + trace value + return nothing + trace value + trace item + return nothing +function failed[T, E]({mode}outcome: result[T, E]) returns optional[T]: + T item = outcome handle error: + trace outcome + trace error + return none + trace outcome + return some(item) +function succeeded[T, E]({mode}outcome: result[T, E], fallback: T) returns T: + T item = outcome handle error: + trace outcome + default fallback + trace outcome + return item +function consume[T](value: optional[T]) returns nothing: + return nothing +function main() returns nothing: + inspect({mode}none) + consume(failed({mode}fail("failure"))) + succeeded({mode}ok(7), 0) + return nothing +"# + ); + let (mut program, types) = lower_source(&source); + prepare_native_uninhabited_sums(&mut program, &types); + validate(&program).unwrap(); + for name in ["inspect", "failed", "succeeded"] { + let function = named_function(&program, name); + assert_dense_reachable(function); + let owner = function.params[0].local; + assert_eq!(function.params[0].mode == ParamMode::View, borrowed); + let snapshots = function + .blocks + .iter() + .flat_map(|block| &block.statements) + .filter(|statement| matches!(&statement.kind, StatementKind::Let { value: Expression { kind: ExpressionKind::Clone(inner), .. }, .. } if matches!(inner.kind, ExpressionKind::Local(local) if local == owner))) + .count(); + assert_eq!(snapshots, 1, "{name} retains a separate owned snapshot"); + assert!(function.blocks.iter().flat_map(|block| &block.statements).any(|statement| matches!(statement.kind, StatementKind::Trace(local) if local == owner))); + move_values::MoveValuePlan::analyze(&program, function, &types) + .expect("source sum remains readable after selected handler path"); + } + } +} + +#[test] +fn source_or_tag_writes_and_mismatched_typed_metadata_are_not_reinterpreted() { + for mutation in 0..9 { + let (mut program, mut types) = lower_source(OPTIONAL); + let invalid_type = foreign_type_id(&types); + let function_id = named_function(&program, "inspect").id; + let function = &mut program.functions[function_id.index() as usize]; + let block = function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.statements.last().map(|statement| &statement.kind), + Some(StatementKind::SumTag { .. }) + ) + }) + .unwrap(); + let StatementKind::SumTag { source, target } = block.statements.last().unwrap().kind else { + unreachable!(); + }; + let TerminatorKind::Branch { condition, .. } = &mut block.terminator.kind else { + unreachable!(); + }; + match mutation { + 0 | 1 => { + let (local, ty, kind) = if mutation == 0 { + ( + source, + function.locals[source.index() as usize].ty, + ExpressionKind::OptionalNone, + ) + } else { + (target, TypeInterner::BOOL, ExpressionKind::Bool(false)) + }; + block.statements.push(Statement { + kind: StatementKind::Assign { + target: Expression { + kind: ExpressionKind::Local(local), + ty, + span: function.span, + }, + value: Expression { + kind, + ty, + span: function.span, + }, + }, + span: function.span, + }); + } + 2 => condition.kind = ExpressionKind::Local(function.params[0].local), + 3 => condition.ty = TypeInterner::INT64, + 4 => function.locals[target.index() as usize].ty = TypeInterner::INT64, + 5 => function.locals[source.index() as usize].ty = invalid_type, + 6 => { + function.locals[source.index() as usize].ty = + types.intern(Type::Result(TypeInterner::NEVER, TypeInterner::NEVER)) + } + 7 => { + function.locals[source.index() as usize].ty = + types.intern(Type::Secret(function.locals[source.index() as usize].ty)) + } + _ => { + function.locals[source.index() as usize].ty = + types.intern(Type::Result(TypeInterner::NEVER, invalid_type)) + } + } + validate(&program).unwrap(); + let original = program.clone(); + prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!(program, original, "conservative mutation {mutation}"); + } +} + +#[test] +fn malformed_live_or_dead_ids_are_reported_before_sum_compaction() { + for corruption in 0..4 { + let (mut program, types) = lower_source(OPTIONAL); + let function_id = named_function(&program, "inspect").id; + let function = &mut program.functions[function_id.index() as usize]; + let block = function + .blocks + .iter_mut() + .find(|block| { + matches!( + block.statements.last().map(|statement| &statement.kind), + Some(StatementKind::SumTag { .. }) + ) + }) + .unwrap(); + let StatementKind::SumTag { source, target } = + &mut block.statements.last_mut().unwrap().kind + else { + unreachable!(); + }; + let TerminatorKind::Branch { then_block, .. } = &mut block.terminator.kind else { + unreachable!(); + }; + match corruption { + 0 => *source = LocalId::new(u32::MAX), + 1 => *target = LocalId::new(u32::MAX), + 2 => *then_block = BlockId(u32::MAX), + _ => { + let dead = *then_block; + function.blocks[dead.index() as usize] + .statements + .push(Statement { + kind: StatementKind::Trace(LocalId::new(u32::MAX)), + span: function.span, + }); + } + } + let original = program.clone(); + assert!(validate(&program).is_err()); + prepare_native_uninhabited_sums(&mut program, &types); + assert_eq!(program, original); + assert!(validate(&program).is_err()); + } +} diff --git a/crates/jett_mir/tests/cfg_analysis.rs b/crates/jett_mir/tests/cfg_analysis.rs index 2b8e8062..21a8d445 100644 --- a/crates/jett_mir/tests/cfg_analysis.rs +++ b/crates/jett_mir/tests/cfg_analysis.rs @@ -28,7 +28,7 @@ fn lower_source(source: &str) -> jett_mir::Program { &HashMap::from([(file, SourceOrigin::Project)]), ) .expect("HIR lowering"); - jett_mir::lower(&hir).expect("MIR lowering") + jett_mir::lower(&hir, &checked.interner).expect("MIR lowering") } #[test] diff --git a/crates/jett_native_launcher/Cargo.toml b/crates/jett_native_launcher/Cargo.toml new file mode 100644 index 00000000..fd38923a --- /dev/null +++ b/crates/jett_native_launcher/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jett_native_launcher" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ["rlib", "staticlib"] + +[dependencies] +jett_runtime.workspace = true diff --git a/crates/jett_native_launcher/src/lib.rs b/crates/jett_native_launcher/src/lib.rs new file mode 100644 index 00000000..c469e966 --- /dev/null +++ b/crates/jett_native_launcher/src/lib.rs @@ -0,0 +1,777 @@ +//! Process launcher for version 1 Jett AOT objects. +//! +//! The static library supplies the C `main` symbol that owns one runtime +//! context for the duration of `jett_aot_v1_entry`. All external calls use the +//! fixed version 1 C ABI; no Rust ABI value crosses the object boundary. + +#[cfg(not(test))] +use std::ffi::c_char; +use std::ffi::{c_int, c_void}; +#[cfg(not(test))] +use std::io; +use std::io::Write; +use std::mem::{self, MaybeUninit}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::slice; + +use jett_runtime::native_abi::{ + JETT_RUNTIME_ABI_VERSION_V1, JettRuntimeContextV1, JettRuntimeResultV1, JettRuntimeStatusV1, + JettRuntimeStringSliceV1, jett_rt_v1_context_create, jett_rt_v1_context_destroy, +}; + +/// Successful status returned by the version 1 AOT entry point. +pub const JETT_AOT_ENTRY_SUCCESS_V1: u32 = 0; + +/// The launcher completed successfully. +pub const JETT_LAUNCHER_EXIT_SUCCESS: c_int = 0; +/// Runtime context creation failed. +pub const JETT_LAUNCHER_EXIT_CREATE_FAILURE: c_int = 70; +/// The generated Jett entry point returned a failure status. +pub const JETT_LAUNCHER_EXIT_ENTRY_FAILURE: c_int = 71; +/// Runtime context destruction failed after the context was created. +pub const JETT_LAUNCHER_EXIT_DESTROY_FAILURE: c_int = 72; +/// A recoverable unwind was caught inside the launcher. +pub const JETT_LAUNCHER_EXIT_PANIC: c_int = 73; + +const CREATE_PANIC_MESSAGE: &[u8] = b"jett launcher: runtime context creation panicked\n"; +const ENTRY_PANIC_MESSAGE: &[u8] = b"jett launcher: program entry panicked\n"; +const DESTROY_PANIC_MESSAGE: &[u8] = b"jett launcher: runtime context destruction panicked\n"; +#[cfg(not(test))] +const LAUNCHER_PANIC_MESSAGE: &[u8] = b"jett launcher: launcher panicked\n"; +const INVALID_RUNTIME_MESSAGE: &[u8] = b"invalid runtime failure message"; + +#[cfg(not(test))] +unsafe extern "C" { + fn jett_aot_v1_entry(context: *mut c_void) -> u32; +} + +#[derive(Debug, Clone, Copy)] +struct RuntimeCall { + status: JettRuntimeStatusV1, + result: JettRuntimeResultV1, +} + +trait RuntimeLifecycle { + fn create(&mut self, out_context: *mut JettRuntimeContextV1) -> RuntimeCall; + fn destroy(&mut self, context: *mut JettRuntimeContextV1) -> RuntimeCall; + fn finish_entry( + &mut self, + _context: *mut JettRuntimeContextV1, + outcome: EntryOutcome, + ) -> EntryOutcome { + outcome + } + fn failure(&mut self, _context: *mut JettRuntimeContextV1) -> Option { + None + } + fn failure_message(&mut self, _context: *mut JettRuntimeContextV1) -> Option> { + None + } +} + +#[derive(Default)] +struct NativeRuntime; + +fn resource_failure(context: *mut JettRuntimeContextV1) -> Option { + let mut result = MaybeUninit::uninit(); + // SAFETY: the caller retains the stationary live context and distinct output. + let status = unsafe { + jett_runtime::native_abi::jett_rt_v1_resource_program_failure(context, result.as_mut_ptr()) + }; + (status != JettRuntimeStatusV1::OK).then(|| RuntimeCall { + status, + // SAFETY: the non-overlapping valid result is initialized on return. + result: unsafe { result.assume_init() }, + }) +} + +unsafe fn copy_failure( + context: *mut JettRuntimeContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + // SAFETY: the caller supplies the same live context and exclusive copy outputs. + unsafe { + if resource_failure(context).is_some() { + jett_runtime::native_abi::jett_rt_v1_resource_program_failure_copy( + context, buffer, capacity, out_length, out_result, + ) + } else { + jett_runtime::native_abi::values::jett_rt_v1_value_failure_copy( + context, buffer, capacity, out_length, out_result, + ) + } + } +} + +impl RuntimeLifecycle for NativeRuntime { + fn create(&mut self, out_context: *mut JettRuntimeContextV1) -> RuntimeCall { + let mut result = MaybeUninit::::uninit(); + // SAFETY: `out_context` is supplied by `launch_with` as aligned, + // exclusively writable storage, and `result` is a distinct writable + // result record. The runtime initializes the result on every return. + let status = unsafe { + jett_rt_v1_context_create( + JETT_RUNTIME_ABI_VERSION_V1, + out_context, + result.as_mut_ptr(), + ) + }; + RuntimeCall { + status, + // SAFETY: the runtime v1 call contract initializes the result + // record for every returned status. + result: unsafe { result.assume_init() }, + } + } + + fn failure(&mut self, context: *mut JettRuntimeContextV1) -> Option { + if let Some(failure) = resource_failure(context) { + return Some(failure); + } + let mut result = MaybeUninit::uninit(); + // SAFETY: launch_with retains the stationary context until destruction. + let status = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_value_failure(context, result.as_mut_ptr()) + }; + if status == JettRuntimeStatusV1::OK { + None + } else { + Some(RuntimeCall { + status, + result: unsafe { result.assume_init() }, + }) + } + } + + fn failure_message(&mut self, context: *mut JettRuntimeContextV1) -> Option> { + let mut length = 0_u64; + let mut result = MaybeUninit::uninit(); + // SAFETY: the live context and distinct outputs remain valid through + // this call. A null, zero-capacity buffer queries the required length. + let status = unsafe { + copy_failure( + context, + std::ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ) + }; + if status != JettRuntimeStatusV1::OK { + return None; + } + let required = length; + let length = usize::try_from(required).ok()?; + let mut message = Vec::new(); + message.try_reserve_exact(length).ok()?; + message.resize(length, 0); + let mut copied = 0_u64; + // SAFETY: `message` owns `length` writable bytes, and all output + // records are distinct. The runtime copies while the context is live. + let status = unsafe { + copy_failure( + context, + message.as_mut_ptr(), + required, + &mut copied, + result.as_mut_ptr(), + ) + }; + (status == JettRuntimeStatusV1::OK && copied == required).then_some(message) + } + + fn finish_entry( + &mut self, + context: *mut JettRuntimeContextV1, + outcome: EntryOutcome, + ) -> EntryOutcome { + let (body, panicked) = match outcome { + EntryOutcome::Status(status) => (status, 0), + EntryOutcome::Panicked => (JettRuntimeStatusV1::PANIC.code(), 1), + }; + let mut selected_kind = 0; + // SAFETY: entry control returned; context and separate output remain live. + let status = unsafe { + jett_runtime::native_abi::jett_rt_v1_resource_program_unwind( + context, + body, + panicked, + &mut selected_kind, + ) + }; + if status == JettRuntimeStatusV1::OK { + return outcome; + } + if panicked != 0 && selected_kind == 2 { + EntryOutcome::Panicked + } else { + EntryOutcome::Status(status.code()) + } + } + + fn destroy(&mut self, context: *mut JettRuntimeContextV1) -> RuntimeCall { + let mut result = MaybeUninit::::uninit(); + // SAFETY: `launch_with` calls destroy only for the same initialized, + // stationary context whose creation succeeded. The result storage is + // distinct and exclusively writable for this call. + let status = unsafe { jett_rt_v1_context_destroy(context, result.as_mut_ptr()) }; + RuntimeCall { + status, + // SAFETY: the runtime v1 call contract initializes the result + // record for every returned status. + result: unsafe { result.assume_init() }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EntryOutcome { + Status(u32), + Panicked, +} + +#[derive(Debug, Clone, Copy)] +enum DestroyOutcome { + Completed(RuntimeCall), + Panicked, +} + +fn catch_operation(operation: impl FnOnce() -> T) -> Result { + catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| { + // A panic payload may have a panicking destructor. Leaking it is the + // only bounded way to guarantee that this C-facing path does not start + // a second unwind while handling the first one. + mem::forget(payload); + }) +} + +fn launch_with(runtime: &mut R, entry: E, stderr: &mut W) -> c_int +where + R: RuntimeLifecycle, + E: FnOnce(*mut c_void) -> u32, + W: Write, +{ + let mut context = MaybeUninit::::uninit(); + let create = match catch_operation(|| runtime.create(context.as_mut_ptr())) { + Ok(call) => call, + Err(()) => { + write_bytes(stderr, CREATE_PANIC_MESSAGE); + return JETT_LAUNCHER_EXIT_PANIC; + } + }; + if create.status != JettRuntimeStatusV1::OK { + write_runtime_failure(stderr, b"runtime context creation", create); + return JETT_LAUNCHER_EXIT_CREATE_FAILURE; + } + + // The context must remain at the exact address registered by create. Keep + // it in its original `MaybeUninit` storage rather than moving it through + // `assume_init`. + let context_pointer = context.as_mut_ptr().cast::(); + let entry_outcome = match catch_operation(|| entry(context_pointer)) { + Ok(status) => EntryOutcome::Status(status), + Err(()) => EntryOutcome::Panicked, + }; + let entry_outcome = + match catch_operation(|| runtime.finish_entry(context.as_mut_ptr(), entry_outcome)) { + Ok(outcome) => outcome, + Err(()) => EntryOutcome::Panicked, + }; + + let entry_failure = runtime.failure(context.as_mut_ptr()); + let entry_message = entry_failure + .as_ref() + .and_then(|_| runtime.failure_message(context.as_mut_ptr())); + + // Cleanup is attempted exactly once after every successful creation, + // including entry failures and recoverable entry panics. + let destroy_outcome = match catch_operation(|| runtime.destroy(context.as_mut_ptr())) { + Ok(call) => DestroyOutcome::Completed(call), + Err(()) => DestroyOutcome::Panicked, + }; + + let mut exit = match entry_outcome { + EntryOutcome::Status(JETT_AOT_ENTRY_SUCCESS_V1) => JETT_LAUNCHER_EXIT_SUCCESS, + EntryOutcome::Status(status) => { + if let Some(failure) = entry_failure { + let _ = stderr.write_all(b"runtime error: "); + let _ = stderr.write_all( + entry_message + .as_deref() + .or_else(|| runtime_message(failure.result.message)) + .unwrap_or(INVALID_RUNTIME_MESSAGE), + ); + let _ = stderr.write_all(b"\n"); + } else { + write_entry_failure(stderr, status); + } + JETT_LAUNCHER_EXIT_ENTRY_FAILURE + } + EntryOutcome::Panicked => { + write_bytes(stderr, ENTRY_PANIC_MESSAGE); + JETT_LAUNCHER_EXIT_PANIC + } + }; + + match destroy_outcome { + DestroyOutcome::Completed(call) if call.status == JettRuntimeStatusV1::OK => {} + DestroyOutcome::Completed(call) => { + write_runtime_failure(stderr, b"runtime context destruction", call); + // A cleanup failure means the runtime could not uphold its + // exactly-once resource contract. Treat it as the terminal + // infrastructure outcome even when the Jett entry failed first. + exit = JETT_LAUNCHER_EXIT_DESTROY_FAILURE; + } + DestroyOutcome::Panicked => { + write_bytes(stderr, DESTROY_PANIC_MESSAGE); + exit = JETT_LAUNCHER_EXIT_PANIC; + } + } + + exit +} + +fn write_entry_failure(stderr: &mut W, status: u32) { + let _ = writeln!( + stderr, + "jett launcher: program entry failed (status {status})" + ); +} + +fn write_runtime_failure(stderr: &mut W, phase: &[u8], call: RuntimeCall) { + let _ = stderr.write_all(b"jett launcher: "); + let _ = stderr.write_all(phase); + let _ = write!(stderr, " failed (status {})", call.status.code()); + let message = runtime_message(call.result.message).unwrap_or(INVALID_RUNTIME_MESSAGE); + if !message.is_empty() { + let _ = stderr.write_all(b": "); + let _ = stderr.write_all(message); + } + let _ = stderr.write_all(b"\n"); +} + +fn runtime_message(message: JettRuntimeStringSliceV1) -> Option<&'static [u8]> { + let length = usize::try_from(message.byte_length).ok()?; + if length == 0 { + return Some(&[]); + } + if message.data.is_null() { + return None; + } + // SAFETY: the runtime ABI promises that result messages point to immutable + // process-lifetime storage containing `byte_length` readable bytes. + Some(unsafe { slice::from_raw_parts(message.data, length) }) +} + +fn write_bytes(stderr: &mut W, message: &[u8]) { + let _ = stderr.write_all(message); +} + +#[cfg(not(test))] +fn run_native_launcher(stderr: &mut W) -> c_int { + let mut runtime = NativeRuntime; + let clock_script = std::env::var(jett_runtime::clock::TEST_SCRIPT_ENV).ok(); + let random_script = std::env::var(jett_runtime::random::TEST_SCRIPT_ENV).ok(); + let environment_snapshot = std::env::var(jett_runtime::environment::TEST_SNAPSHOT_ENV).ok(); + let graphics_script = std::env::var(jett_runtime::graphics::TEST_SCRIPT_ENV).ok(); + launch_with( + &mut runtime, + |context| { + if let Some(script) = &clock_script { + // SAFETY: `context` is live and stationary, and `script` remains + // readable for the duration of the configuration call. + let status = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_clock_configure_scripted( + context.cast(), + script.as_ptr(), + script.len() as u64, + ) + }; + if status != JETT_AOT_ENTRY_SUCCESS_V1 { + return status; + } + } + if let Some(script) = &random_script { + // SAFETY: `context` is live and stationary, and `script` remains + // readable for the duration of the configuration call. + let status = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_random_configure_scripted( + context.cast(), + script.as_ptr(), + script.len() as u64, + ) + }; + if status != JETT_AOT_ENTRY_SUCCESS_V1 { + return status; + } + } + if let Some(snapshot) = &environment_snapshot { + // SAFETY: the context is live and `snapshot` is readable for + // the duration of this configuration call. + let status = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_environment_configure_snapshot( + context.cast(), + snapshot.as_ptr(), + snapshot.len() as u64, + ) + }; + if status != JETT_AOT_ENTRY_SUCCESS_V1 { + return status; + } + } + if let Some(script) = &graphics_script { + // SAFETY: the context is live and `script` is readable for + // the duration of this configuration call. + let status = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_graphics_configure_scripted( + context.cast(), + script.as_ptr(), + script.len() as u64, + ) + }; + if status != JETT_AOT_ENTRY_SUCCESS_V1 { + return status; + } + } + // SAFETY: the generated object implements the committed version 1 + // C entry ABI, and `context` remains live and stationary until the + // entry call returns. + let status = unsafe { jett_aot_v1_entry(context) }; + if status != JETT_AOT_ENTRY_SUCCESS_V1 { + return status; + } + // A successful program must consume every scripted provider input. + // The check records a terminal runtime failure with the same + // provider priority and message as the interpreter runner. + unsafe { + jett_runtime::native_abi::values::jett_rt_v1_validate_scripted_consumption( + context.cast(), + ) + } + }, + stderr, + ) +} + +#[cfg(not(test))] +fn report_top_level_panic() { + if let Err(payload) = catch_unwind(AssertUnwindSafe(|| { + let stderr = io::stderr(); + write_bytes(&mut stderr.lock(), LAUNCHER_PANIC_MESSAGE); + })) { + mem::forget(payload); + } +} + +/// C process entry point linked with one generated Jett AOT object. +/// +/// Version 1 does not expose process arguments to generated code yet, so +/// `argc` and `argv` are deliberately ignored after the C runtime supplies +/// them. They remain in the signature to preserve the platform C entry ABI. +#[cfg(not(test))] +#[unsafe(no_mangle)] +pub extern "C" fn main(_argc: c_int, _argv: *mut *mut c_char) -> c_int { + match catch_unwind(AssertUnwindSafe(|| { + let stderr = io::stderr(); + run_native_launcher(&mut stderr.lock()) + })) { + Ok(exit) => exit, + Err(payload) => { + mem::forget(payload); + report_top_level_panic(); + JETT_LAUNCHER_EXIT_PANIC + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CREATE_FAILURE_MESSAGE: &[u8] = b"test create failure"; + const DESTROY_FAILURE_MESSAGE: &[u8] = b"test destroy failure"; + + #[derive(Default)] + struct TrackingRuntime { + native: NativeRuntime, + create_failure: bool, + destroy_failure: bool, + panic_on_destroy: bool, + create_calls: usize, + destroy_calls: usize, + } + + impl RuntimeLifecycle for TrackingRuntime { + fn create(&mut self, out_context: *mut JettRuntimeContextV1) -> RuntimeCall { + self.create_calls += 1; + if self.create_failure { + return failed_call( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + CREATE_FAILURE_MESSAGE, + ); + } + self.native.create(out_context) + } + + fn destroy(&mut self, context: *mut JettRuntimeContextV1) -> RuntimeCall { + self.destroy_calls += 1; + let native = self.native.destroy(context); + assert_eq!(native.status, JettRuntimeStatusV1::OK); + if self.panic_on_destroy { + panic!("injected destroy panic"); + } + if self.destroy_failure { + return failed_call( + JettRuntimeStatusV1::INVALID_CONTEXT, + DESTROY_FAILURE_MESSAGE, + ); + } + native + } + } + + fn failed_call(status: JettRuntimeStatusV1, message: &'static [u8]) -> RuntimeCall { + RuntimeCall { + status, + result: JettRuntimeResultV1 { + status, + message: JettRuntimeStringSliceV1 { + data: message.as_ptr(), + byte_length: u64::try_from(message.len()).unwrap_or(u64::MAX), + }, + }, + } + } + + #[test] + fn successful_entry_creates_and_destroys_one_context() { + let mut runtime = TrackingRuntime::default(); + let mut stderr = Vec::new(); + + let exit = launch_with(&mut runtime, |_| JETT_AOT_ENTRY_SUCCESS_V1, &mut stderr); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_SUCCESS); + assert_eq!(runtime.create_calls, 1); + assert_eq!(runtime.destroy_calls, 1); + assert!(stderr.is_empty()); + } + + #[test] + fn entry_failure_still_destroys_the_context() { + let mut runtime = TrackingRuntime::default(); + let mut stderr = Vec::new(); + + let exit = launch_with(&mut runtime, |_| 19, &mut stderr); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_ENTRY_FAILURE); + assert_eq!(runtime.create_calls, 1); + assert_eq!(runtime.destroy_calls, 1); + assert_eq!(stderr, b"jett launcher: program entry failed (status 19)\n"); + } + + #[test] + fn dynamic_assertion_message_is_copied_before_context_destruction() { + let mut runtime = NativeRuntime; + let mut stderr = Vec::new(); + let exit = launch_with( + &mut runtime, + |context| { + let context = context.cast::(); + let message = "expected 42, got 🧪"; + // SAFETY: launch_with supplies a live context and the source + // string remains readable for the literal call. + unsafe { + let handle = jett_runtime::native_abi::values::jett_rt_v1_string_literal( + context, + message.as_ptr(), + message.len() as u64, + ); + let status = jett_runtime::native_abi::values::jett_rt_v1_assert_fail_message( + context, handle, + ); + assert_eq!( + jett_runtime::native_abi::values::jett_rt_v1_string_release( + context, handle, + ), + 0 + ); + status + } + }, + &mut stderr, + ); + assert_eq!(exit, JETT_LAUNCHER_EXIT_ENTRY_FAILURE); + assert_eq!(stderr, "runtime error: expected 42, got 🧪\n".as_bytes()); + } + + #[test] + fn creation_failure_skips_entry_and_destruction() { + let mut runtime = TrackingRuntime { + create_failure: true, + ..TrackingRuntime::default() + }; + let mut stderr = Vec::new(); + let mut entry_called = false; + + let exit = launch_with( + &mut runtime, + |_| { + entry_called = true; + JETT_AOT_ENTRY_SUCCESS_V1 + }, + &mut stderr, + ); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_CREATE_FAILURE); + assert!(!entry_called); + assert_eq!(runtime.create_calls, 1); + assert_eq!(runtime.destroy_calls, 0); + assert_eq!( + stderr, + b"jett launcher: runtime context creation failed (status 7): test create failure\n" + ); + } + + #[test] + fn destruction_failure_has_its_own_exit_code() { + let mut runtime = TrackingRuntime { + destroy_failure: true, + ..TrackingRuntime::default() + }; + let mut stderr = Vec::new(); + + let exit = launch_with(&mut runtime, |_| JETT_AOT_ENTRY_SUCCESS_V1, &mut stderr); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_DESTROY_FAILURE); + assert_eq!(runtime.destroy_calls, 1); + assert_eq!( + stderr, + b"jett launcher: runtime context destruction failed (status 3): test destroy failure\n" + ); + } + + #[test] + fn destruction_failure_overrides_an_entry_failure() { + let mut runtime = TrackingRuntime { + destroy_failure: true, + ..TrackingRuntime::default() + }; + let mut stderr = Vec::new(); + + let exit = launch_with(&mut runtime, |_| 19, &mut stderr); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_DESTROY_FAILURE); + assert_eq!(runtime.destroy_calls, 1); + assert_eq!( + stderr, + b"jett launcher: program entry failed (status 19)\njett launcher: runtime context destruction failed (status 3): test destroy failure\n" + ); + } + + #[test] + fn recoverable_entry_panic_still_runs_cleanup() { + let mut runtime = TrackingRuntime::default(); + let mut stderr = Vec::new(); + + let exit = launch_with( + &mut runtime, + |_| panic!("injected entry panic"), + &mut stderr, + ); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_PANIC); + assert_eq!(runtime.destroy_calls, 1); + assert_eq!(stderr, ENTRY_PANIC_MESSAGE); + } + + #[test] + fn recoverable_destroy_panic_is_contained() { + let mut runtime = TrackingRuntime { + panic_on_destroy: true, + ..TrackingRuntime::default() + }; + let mut stderr = Vec::new(); + + let exit = launch_with(&mut runtime, |_| JETT_AOT_ENTRY_SUCCESS_V1, &mut stderr); + + assert_eq!(exit, JETT_LAUNCHER_EXIT_PANIC); + assert_eq!(runtime.destroy_calls, 1); + assert_eq!(stderr, DESTROY_PANIC_MESSAGE); + } + + #[test] + fn native_owned_value_leak_cannot_report_successful_failure_cleanup() { + let mut runtime = NativeRuntime; + let mut stderr = Vec::new(); + let exit = launch_with( + &mut runtime, + |context| { + let text = b"deliberately unreleased"; + let value = unsafe { + jett_runtime::native_abi::values::jett_rt_v1_string_literal( + context.cast(), + text.as_ptr(), + text.len() as u64, + ) + }; + assert_ne!(value, 0); + 19 + }, + &mut stderr, + ); + assert_eq!(exit, JETT_LAUNCHER_EXIT_DESTROY_FAILURE); + assert_eq!(stderr, b"jett launcher: program entry failed (status 19)\njett launcher: runtime context destruction failed (status 1): native value ownership leak\n"); + } + + #[test] + fn native_terminal_failure_releases_values_and_preserves_message() { + let mut runtime = NativeRuntime; + let mut stderr = Vec::new(); + let exit = launch_with( + &mut runtime, + |context| { + use jett_runtime::native_abi::values::*; + unsafe { + let context = context.cast(); + let value = jett_rt_v1_string_literal(context, b"ab".as_ptr(), 2); + assert_eq!(jett_rt_v1_string_repeat(context, value, i64::MAX), 0); + jett_rt_v1_string_release(context, value); + jett_rt_v1_value_status(context) + } + }, + &mut stderr, + ); + assert_eq!(exit, JETT_LAUNCHER_EXIT_ENTRY_FAILURE); + assert_eq!( + stderr, + b"runtime error: string.repeat: requested output is too large\n" + ); + } + #[test] + fn native_double_drop_overrides_terminal_failure_without_erasing_it() { + let mut runtime = NativeRuntime; + let mut stderr = Vec::new(); + let exit = launch_with( + &mut runtime, + |context| { + use jett_runtime::native_abi::values::*; + unsafe { + let context = context.cast(); + let value = jett_rt_v1_bytes_new(context); + let text = jett_rt_v1_string_literal(context, b"ab".as_ptr(), 2); + jett_rt_v1_string_repeat(context, text, i64::MAX); + jett_rt_v1_value_drop(context, value); + jett_rt_v1_value_drop(context, value); + jett_rt_v1_value_drop(context, text); + jett_rt_v1_value_status(context) + } + }, + &mut stderr, + ); + assert_eq!(exit, JETT_LAUNCHER_EXIT_DESTROY_FAILURE); + assert_eq!(stderr, b"runtime error: string.repeat: requested output is too large\njett launcher: runtime context destruction failed (status 1): native value cleanup failed\n"); + } +} diff --git a/crates/jett_parser/src/parser.rs b/crates/jett_parser/src/parser.rs index 548d7a17..bfee8ae4 100644 --- a/crates/jett_parser/src/parser.rs +++ b/crates/jett_parser/src/parser.rs @@ -1643,7 +1643,7 @@ impl<'src> Parser<'src> { let start = self.peek_token().span; let mutable = self.eat(TokenKind::Mutable).is_some(); let ty = self.parse_type(); - let name = self.parse_ident(); + let name = self.parse_binding_name(); self.expect(TokenKind::Eq); let value = self.parse_expr(); // Check for handle block after the expression @@ -1662,76 +1662,7 @@ impl<'src> Parser<'src> { /// A var decl starts with a type followed by an identifier followed by `=`. /// We look ahead to check this pattern. fn looks_like_var_decl(&self) -> bool { - // Must start with something that could be a type - if !self.is_type_start(self.peek()) { - return false; - } - - // Walk forward to find the pattern: Type [GenericArgs] Ident = - let mut lookahead = 0; - - // Skip the type name - let first = self.peek_nth(lookahead); - if !self.is_type_start(first) { - return false; - } - - // Handle function types: `function(T, U) returns V name =` - if first == TokenKind::Function { - lookahead += 1; - if self.peek_nth(lookahead) != TokenKind::LParen { - return false; - } - lookahead += 1; - let mut depth = 1; - while depth > 0 && lookahead < 40 { - match self.peek_nth(lookahead) { - TokenKind::LParen => depth += 1, - TokenKind::RParen => depth -= 1, - TokenKind::Eof => return false, - _ => {} - } - lookahead += 1; - } - // Skip `returns Type` — the return type can itself be complex - // (e.g. `function(int64) returns function(int64) returns int64`) - if self.peek_nth(lookahead) != TokenKind::Returns { - return false; - } - lookahead += 1; - // Skip the return type recursively (simplified: skip one or more tokens - // until we find an identifier followed by `=`) - return self.scan_past_type_for_var_decl(lookahead); - } - - lookahead += 1; - lookahead = self.skip_dotted_type_path(lookahead); - - // Handle generic args: Type[...] - if self.peek_nth(lookahead) == TokenKind::LBracket { - lookahead += 1; - let mut depth = 1; - while depth > 0 && lookahead < 20 { - match self.peek_nth(lookahead) { - TokenKind::LBracket => depth += 1, - TokenKind::RBracket => depth -= 1, - TokenKind::Eof => return false, - _ => {} - } - lookahead += 1; - } - } - lookahead = self.skip_state_type_qualifiers(lookahead); - - // Now we should see an identifier (or contextual keyword) followed by `=` - let name_kind = self.peek_nth(lookahead); - if (name_kind == TokenKind::Ident || self.is_contextual_ident(name_kind)) - && self.peek_nth(lookahead + 1) == TokenKind::Eq - { - return true; - } - - false + self.scan_past_type_for_var_decl(0) } fn is_type_start(&self, kind: TokenKind) -> bool { @@ -1765,58 +1696,99 @@ impl<'src> Parser<'src> { /// Starting at `lookahead`, skip past one type expression, then check for `ident =`. /// Used by `looks_like_var_decl` to handle function type return types. fn scan_past_type_for_var_decl(&self, mut lookahead: usize) -> bool { - if lookahead >= 60 { - return false; - } - let kind = self.peek_nth(lookahead); - if kind == TokenKind::Function { - // Nested function type: `function(…) returns T` - lookahead += 1; - if self.peek_nth(lookahead) != TokenKind::LParen { + // A function return type may itself be a function. Advance through + // those prefixes iteratively; the token stream bounds this lookahead. + while self.peek_nth(lookahead) == TokenKind::Function { + let Some(after_params) = self.skip_balanced_type_delimiters( + lookahead + 1, + TokenKind::LParen, + TokenKind::RParen, + ) else { return false; - } - lookahead += 1; - let mut depth = 1; - while depth > 0 && lookahead < 60 { - match self.peek_nth(lookahead) { - TokenKind::LParen => depth += 1, - TokenKind::RParen => depth -= 1, - TokenKind::Eof => return false, - _ => {} - } - lookahead += 1; - } - if self.peek_nth(lookahead) != TokenKind::Returns { + }; + if self.peek_nth(after_params) != TokenKind::Returns { return false; } - lookahead += 1; - return self.scan_past_type_for_var_decl(lookahead); + lookahead = after_params + 1; } - if !self.is_type_start(kind) { + if !self.is_type_start(self.peek_nth(lookahead)) { return false; } lookahead += 1; lookahead = self.skip_dotted_type_path(lookahead); - // Skip generic args: Type[…] if self.peek_nth(lookahead) == TokenKind::LBracket { - lookahead += 1; - let mut depth = 1; - while depth > 0 && lookahead < 60 { - match self.peek_nth(lookahead) { - TokenKind::LBracket => depth += 1, - TokenKind::RBracket => depth -= 1, - TokenKind::Eof => return false, - _ => {} - } - lookahead += 1; - } + let Some(after_args) = self.skip_balanced_type_delimiters( + lookahead, + TokenKind::LBracket, + TokenKind::RBracket, + ) else { + return false; + }; + lookahead = after_args; } lookahead = self.skip_state_type_qualifiers(lookahead); let name_kind = self.peek_nth(lookahead); - (name_kind == TokenKind::Ident || self.is_contextual_ident(name_kind)) + (name_kind == TokenKind::Ident + || self.is_contextual_ident(name_kind) + || self.is_reserved_identifier_at(lookahead)) && self.peek_nth(lookahead + 1) == TokenKind::Eq } + /// Return the relative token offset after one balanced delimiter group. + /// Lookahead never consumes input or crosses a logical line, and exhaustion + /// without the matching close is a failed recognition rather than a limit. + fn skip_balanced_type_delimiters( + &self, + lookahead: usize, + open: TokenKind, + close: TokenKind, + ) -> Option { + if self.peek_nth(lookahead) != open { + return None; + } + let mut depth = 0; + for (offset, token) in self + .tokens + .iter() + .skip(self.pos) + .enumerate() + .skip(lookahead) + { + match token.kind { + TokenKind::Eof | TokenKind::Newline => return None, + kind if kind == open => depth += 1, + kind if kind == close => { + depth -= 1; + if depth == 0 { + return Some(offset + 1); + } + } + _ => {} + } + } + None + } + + fn is_reserved_identifier_at(&self, lookahead: usize) -> bool { + self.tokens + .get(self.pos + lookahead) + .is_some_and(|token| self.is_reserved_identifier(token)) + } + + fn is_reserved_identifier(&self, token: &Token) -> bool { + if token.kind == TokenKind::Ident || self.is_contextual_ident(token.kind) { + return false; + } + + let text = self.token_text(token); + text.as_bytes() + .first() + .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_') + && text + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + } + fn skip_dotted_type_path(&self, mut lookahead: usize) -> usize { while self.peek_nth(lookahead) == TokenKind::Dot && (self.peek_nth(lookahead + 1) == TokenKind::Ident @@ -2457,24 +2429,13 @@ impl<'src> Parser<'src> { } fn looks_like_generic_args(&self) -> bool { - // Quick check: `[` followed by a type name and eventually `](` - if self.peek() != TokenKind::LBracket { + let Some(after_args) = + self.skip_balanced_type_delimiters(0, TokenKind::LBracket, TokenKind::RBracket) + else { return false; - } - let mut i = 1; - let mut depth = 1; - while depth > 0 && i < 20 { - match self.peek_nth(i) { - TokenKind::LBracket => depth += 1, - TokenKind::RBracket => depth -= 1, - TokenKind::Eof | TokenKind::Newline => return false, - _ => {} - } - i += 1; - } - // After `]`, we should see `(` - self.peek_nth(i) == TokenKind::LParen - || depth == 0 && self.peek_nth(i - 1 + 1) == TokenKind::LParen + }; + // The matching `]` must be immediately followed by the call's `(`. + self.peek_nth(after_args) == TokenKind::LParen } fn parse_generic_call(&mut self, callee: Expr) -> Expr { @@ -2532,6 +2493,24 @@ impl<'src> Parser<'src> { } } + fn parse_binding_name(&mut self) -> Ident { + let tok = self.peek_token().clone(); + if !self.is_reserved_identifier(&tok) { + return self.parse_ident(); + } + + let name = self.token_text(&tok).to_string(); + self.advance(); + self.error( + format!("reserved keyword `{name}` cannot be used as a binding name"), + tok.span, + ); + Ident { + name: "".to_string(), + span: tok.span, + } + } + /// Keywords that can appear as identifiers in certain contexts /// (e.g., parameter names, variable names, use aliases, field names). fn is_contextual_ident(&self, kind: TokenKind) -> bool { @@ -2991,6 +2970,38 @@ function main() returns nothing: } } + #[test] + fn reserved_keyword_binding_is_one_invalid_declaration() { + for reserved in ["result", "optional", "view"] { + let src = format!( + "function main() returns nothing:\n int64 {reserved} = 1\n return nothing\n" + ); + let result = parse_str(&src); + + assert_eq!( + result.errors.len(), + 1, + "unexpected diagnostics for `{reserved}`: {:?}", + result.errors + ); + assert_eq!( + result.errors[0].message, + format!("reserved keyword `{reserved}` cannot be used as a binding name") + ); + + let Item::Function(function) = &result.module.items[0] else { + panic!("expected function for `{reserved}`"); + }; + assert_eq!(function.body.stmts.len(), 2); + let Stmt::VarDecl(declaration) = &function.body.stmts[0] else { + panic!("expected invalid declaration to remain one statement for `{reserved}`"); + }; + assert_eq!(declaration.name.name, ""); + assert!(matches!(declaration.value, Expr::IntLiteral(1, _))); + assert!(matches!(function.body.stmts[1], Stmt::Return(_))); + } + } + // ----------------------------------------------------------------------- // Parsing if/else // ----------------------------------------------------------------------- @@ -4231,6 +4242,267 @@ function first(view items: list[int64]) returns optional[int64]: } } + const DEEP_GENERIC_TYPE: &str = "list[optional[result[map[int64, list[int64]], set[int64]]]]"; + + fn generic_type_owners<'a>(ty: &'a TypeExpr, owners: &mut Vec<&'a str>) { + match ty { + TypeExpr::Named(_) => {} + TypeExpr::Generic(owner, children, _) => { + owners.push(owner.name.as_str()); + for child in children { + generic_type_owners(child, owners); + } + } + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + generic_type_owners(inner, owners) + } + TypeExpr::Function(params, output, _) => { + for param in params { + generic_type_owners(param, owners); + } + generic_type_owners(output, owners); + } + } + } + + fn generic_call_in_expression(mut expression: &Expr) -> (&[TypeExpr], &[CallArg]) { + if let Expr::Pipeline(_, steps, _) = expression { + assert_eq!(steps.len(), 1); + assert!(steps[0].extra_args.is_empty()); + expression = &steps[0].function; + } + if let Expr::View(inner, _) = expression { + expression = inner; + } + let Expr::GenericCall(_, types, args, _) = expression else { + panic!("deep requested generic call: {expression:?}"); + }; + (types, args) + } + + #[test] + fn parse_deep_generic_calls_and_pipelines_preserve_complete_type_arguments() { + let token_source = format!("[Record, {DEEP_GENERIC_TYPE}]()"); + let tokens = jett_lexer::tokenize(&token_source, FileId::new(0)).tokens; + assert!( + tokens + .iter() + .position(|t| t.kind == TokenKind::LParen) + .unwrap() + > 20 + ); + for pipeline in [false, true] { + let call = if pipeline { + format!( + "source into view type.field_value[Record, {DEEP_GENERIC_TYPE}](view field)" + ) + } else { + format!("type.field_value[Record, {DEEP_GENERIC_TYPE}](view source, view field)") + }; + let source = format!( + "function read(view source: Record, view field: TypeField) returns {DEEP_GENERIC_TYPE}:\n return {call}\n" + ); + let result = parse_str(&source); + assert!(result.errors.is_empty(), "{pipeline}: {:?}", result.errors); + let (types, args) = generic_call_in_expression(extract_expr_from_return(&result)); + assert_eq!(types.len(), 2); + assert_eq!(args.len(), if pipeline { 1 } else { 2 }); + assert!(matches!(&types[0], TypeExpr::Named(name) if name.name == "Record")); + let span = types[1].span(); + assert_eq!( + &source[span.start as usize..span.end as usize], + DEEP_GENERIC_TYPE + ); + let mut owners = Vec::new(); + generic_type_owners(&types[1], &mut owners); + assert_eq!(owners, ["list", "optional", "result", "map", "list", "set"]); + + // A deep checked request must also remain a declaration when its + // annotation and its initializer both need the longer lookahead. + let local_source = format!( + "function read(view source: Record, view field: TypeField) returns {DEEP_GENERIC_TYPE}:\n {DEEP_GENERIC_TYPE} extracted = {call}\n return extracted\n" + ); + let local_result = parse_str(&local_source); + assert!( + local_result.errors.is_empty(), + "{pipeline}: {:?}", + local_result.errors + ); + let Item::Function(function) = &local_result.module.items[0] else { + panic!("function"); + }; + let Stmt::VarDecl(local) = &function.body.stmts[0] else { + panic!("typed getter local"); + }; + assert_eq!(local.name.name, "extracted"); + let local_span = local.ty.span(); + assert_eq!( + &local_source[local_span.start as usize..local_span.end as usize], + DEEP_GENERIC_TYPE + ); + let (types, args) = generic_call_in_expression(&local.value); + assert_eq!(types.len(), 2); + assert_eq!(args.len(), if pipeline { 1 } else { 2 }); + } + } + + #[test] + fn parse_deep_generic_callables_keep_parameter_and_return_type_trees() { + let requested = "list[function(list[optional[result[list[optional[list[optional[int64]]]], string]]], function(map[string, list[bool]]) returns result[bool, string]) returns optional[list[result[int64, string]]]]"; + let tokens = jett_lexer::tokenize(requested, FileId::new(0)).tokens; + assert!(tokens.len() > 60); + for pipeline in [false, true] { + let call = if pipeline { + format!("source into choose[{requested}]()") + } else { + format!("choose[{requested}](view source)") + }; + let source = format!( + "function select(view source: string) returns {requested}:\n return {call}\n" + ); + let result = parse_str(&source); + assert!(result.errors.is_empty(), "{pipeline}: {:?}", result.errors); + let (types, _) = generic_call_in_expression(extract_expr_from_return(&result)); + assert_eq!(types.len(), 1); + let TypeExpr::Generic(owner, items, _) = &types[0] else { + panic!("list callable"); + }; + assert_eq!(owner.name, "list"); + let TypeExpr::Function(params, output, _) = &items[0] else { + panic!("callable leaf"); + }; + assert_eq!(params.len(), 2); + assert!(matches!(params[1], TypeExpr::Function(_, _, _))); + assert!( + matches!(output.as_ref(), TypeExpr::Generic(owner, _, _) if owner.name == "optional") + ); + let mut owners = Vec::new(); + generic_type_owners(&types[0], &mut owners); + assert_eq!( + owners, + [ + "list", "list", "optional", "result", "list", "optional", "list", "optional", + "map", "list", "result", "optional", "list", "result" + ] + ); + } + } + + #[test] + fn parse_deep_generic_locals_and_function_return_chains_keep_declaration_shape() { + let deep = format!("{}int64{}", "list[".repeat(24), "]".repeat(24)); + let callback = format!( + "function({deep}, {deep}) returns {}{deep}", + "function() returns ".repeat(16) + ); + let qualified = format!("models.Holder[{deep}]"); + let source = format!( + "function declarations() returns nothing:\n mutable {deep} items = seed\n {callback} callback = seed\n {qualified} retained = seed\n models.Session at active selected = seed\n" + ); + let result = parse_str(&source); + assert!(result.errors.is_empty(), "{:?}", result.errors); + let Item::Function(function) = &result.module.items[0] else { + panic!("function"); + }; + assert_eq!(function.body.stmts.len(), 4); + for (index, expected) in [ + deep.as_str(), + callback.as_str(), + qualified.as_str(), + "models.Session at active", + ] + .into_iter() + .enumerate() + { + let Stmt::VarDecl(declaration) = &function.body.stmts[index] else { + panic!("deep local {index}"); + }; + assert_eq!(declaration.mutable, index == 0); + let span = declaration.ty.span(); + assert_eq!(&source[span.start as usize..span.end as usize], expected); + } + } + + fn lookahead_parser(source: &str) -> Parser<'_> { + Parser::new(source, jett_lexer::tokenize(source, FileId::new(0)).tokens) + } + + #[test] + fn type_lookahead_is_input_bounded_non_consuming_and_requires_complete_delimiters() { + let deep = format!("{}int64{}", "list[".repeat(128), "]".repeat(128)); + let generic = format!("[{deep}](source)"); + let parser = lookahead_parser(&generic); + assert!(parser.looks_like_generic_args()); + assert_eq!(parser.pos, 0); + assert!(parser.errors.is_empty()); + let prefixed_generic = format!("prefix [{deep}](source)"); + let mut parser = lookahead_parser(&prefixed_generic); + parser.pos = 1; + assert!(parser.looks_like_generic_args()); + assert_eq!(parser.pos, 1); + let local = format!("{}int64 callback = seed", "function() returns ".repeat(512)); + let parser = lookahead_parser(&local); + assert!(parser.looks_like_var_decl()); + assert_eq!(parser.pos, 0); + for malformed in [ + format!("[{deep}"), + format!("[{deep}]"), + format!("[{deep}]](source)"), + format!("[{deep}]name(source)"), + format!("[list[int64]\n](source)"), + format!("[{deep}]\n(source)"), + "[".repeat(2048), + ] { + let mut parser = lookahead_parser(&malformed); + assert!(!parser.looks_like_generic_args(), "{malformed}"); + assert_eq!(parser.pos, 0); + // Exhausting a hand-supplied token stream without its EOF sentinel + // must also terminate without peeking indefinitely beyond input. + parser.tokens.retain(|token| token.kind != TokenKind::Eof); + assert!( + !parser.looks_like_generic_args(), + "missing EOF: {malformed}" + ); + assert_eq!(parser.pos, 0); + } + for malformed in [ + format!("list[{deep} items = seed"), + "function(int64\n) returns int64 callback = seed".into(), + "function(int64)\nreturns int64 callback = seed".into(), + "function() returns".into(), + "function(int64) int64 callback = seed".into(), + "function(".repeat(2048), + format!("{deep}\nitems = seed"), + "models.Session at active".into(), + ] { + let parser = lookahead_parser(&malformed); + assert!(!parser.looks_like_var_decl(), "{malformed}"); + assert_eq!(parser.pos, 0); + } + } + + #[test] + fn parse_malformed_deep_generic_calls_and_locals_reports_errors_and_terminates() { + let deep = format!("{}int64{}", "list[".repeat(24), "]".repeat(24)); + for statement in [ + format!("return choose[{deep}(source)"), + format!("return choose[{deep}]"), + format!("return choose[{deep}]("), + format!("return source into choose[{deep}]("), + format!("return choose[list[int64]\n](source)"), + format!("list[{deep} items = seed"), + "function(int64\n) returns int64 callback = seed".into(), + ] { + let source = format!("function broken() returns nothing:\n {statement}\n"); + let result = parse_str(&source); + assert!(!result.errors.is_empty(), "accepted malformed {statement}"); + assert!( + result.errors.len() <= source.len(), + "unbounded recovery errors" + ); + } + } + #[test] fn parse_json_serialize_generic_call() { let src = "\ diff --git a/crates/jett_query/Cargo.toml b/crates/jett_query/Cargo.toml index 2c6d3591..de2927cb 100644 --- a/crates/jett_query/Cargo.toml +++ b/crates/jett_query/Cargo.toml @@ -10,6 +10,9 @@ jett_common.workspace = true jett_diagnostics.workspace = true jett_parser.workspace = true salsa.workspace = true +# Salsa interned expansion uses a private macro/runtime contract. +# 0.28.4 macro rules are incompatible with the pinned 0.28.2 runtime. +salsa-macro-rules = "=0.28.2" sha2.workspace = true hmac.workspace = true subtle.workspace = true diff --git a/crates/jett_resolve/src/lib.rs b/crates/jett_resolve/src/lib.rs index edd94312..ea969dc6 100644 --- a/crates/jett_resolve/src/lib.rs +++ b/crates/jett_resolve/src/lib.rs @@ -2,7 +2,15 @@ pub mod errors; pub mod resolver; +pub mod resource_hooks; pub mod scope; -pub use resolver::{ResolveResult, resolve}; +pub use resolver::{ResolveResult, resolve, resolve_with_resource_kernels}; +pub use resource_hooks::{ + ResolvedResourceKernel, ResolvedResourceKernels, ResourceKernelError, ResourceKernelSpec, + validate_resource_kernels, +}; pub use scope::{DefId, DefInfo, DefKind, DefVisibility, Scope, ScopeId, ScopeTable}; + +#[cfg(test)] +mod resource_hook_tests; diff --git a/crates/jett_resolve/src/resolver.rs b/crates/jett_resolve/src/resolver.rs index 90ad53b2..2e87ce69 100644 --- a/crates/jett_resolve/src/resolver.rs +++ b/crates/jett_resolve/src/resolver.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use jett_common::{FileId, Span}; +use jett_common::{FileId, SourceOrigin, Span}; use jett_diagnostics::{Diagnostic, DiagnosticSink}; use jett_parser::ast::{ ActorDef, AssertStmt, AssignStmt, Block, BreakpointStmt, CallArg, ComptimeTypeBindStmt, Expr, @@ -10,6 +10,9 @@ use jett_parser::ast::{ }; use crate::errors; +use crate::resource_hooks::{ + ResolvedResourceKernels, ResourceKernelError, ResourceKernelSpec, validate_specs, +}; use crate::scope::{DefId, DefKind, DefVisibility, ScopeId, ScopeTable}; /// The result of name resolution. @@ -21,19 +24,38 @@ pub struct ResolveResult { pub resolutions: HashMap, /// Map from namespace import definitions to their fully qualified targets. pub namespace_aliases: HashMap, + /// Sealed privileged declaration associations; empty in production. + pub resource_kernels: ResolvedResourceKernels, /// Diagnostics emitted during resolution. pub diagnostics: Vec, } /// Resolve all names in a parsed `Module`. pub fn resolve(module: &Module) -> ResolveResult { + finish_resolution(module, Resolver::new()) +} + +/// Resolve a compiler-owned private catalog. Never exposed to source/config. +pub fn resolve_with_resource_kernels( + module: &Module, + origins: &HashMap, + specs: &[ResourceKernelSpec], +) -> Result { + validate_specs(module, origins, specs)?; let mut resolver = Resolver::new(); + resolver.resource_kernel_specs = specs.to_vec(); + resolver.resource_kernels = ResolvedResourceKernels::with_catalog(origins, specs); + Ok(finish_resolution(module, resolver)) +} + +fn finish_resolution(module: &Module, mut resolver: Resolver) -> ResolveResult { resolver.resolve_module(module); resolver.check_unused(); ResolveResult { scope_table: resolver.scope_table, resolutions: resolver.resolutions, namespace_aliases: resolver.namespace_aliases, + resource_kernels: resolver.resource_kernels, diagnostics: resolver.sink.into_diagnostics(), } } @@ -47,6 +69,8 @@ struct Resolver { resolutions: HashMap, namespace_aliases: HashMap, sink: DiagnosticSink, + resource_kernel_specs: Vec, + resource_kernels: ResolvedResourceKernels, /// The current scope during the walk. current_scope: ScopeId, /// Namespace introduced by the latest `namespace` item in the current file. @@ -165,6 +189,8 @@ impl Resolver { resolutions: HashMap::new(), namespace_aliases: HashMap::new(), sink: DiagnosticSink::new(), + resource_kernel_specs: Vec::new(), + resource_kernels: ResolvedResourceKernels::default(), current_scope: root, current_namespace: None, current_file: None, @@ -302,6 +328,7 @@ impl Resolver { resource.exported, ) { self.resolutions.insert(resource.name.span, def_id); + self.declare_resource_kernels(resource.name.span, def_id, index); } } Item::TypeAlias(ta) => { @@ -324,6 +351,30 @@ impl Resolver { } } + fn declare_resource_kernels(&mut self, span: Span, resource: DefId, order: usize) { + let Some(namespace) = self.current_namespace.clone() else { + return; + }; + let specs = self + .resource_kernel_specs + .iter() + .filter(|spec| spec.resource_declaration == span) + .cloned() + .collect::>(); + for spec in specs { + if let Some(definition) = self.declare_namespaced_top_level( + &spec.member, + DefKind::Function, + span, + order, + false, + ) { + self.resource_kernels + .insert(&spec, definition, resource, namespace.clone()); + } + } + } + fn declare_function_top_level(&mut self, func: &FunctionDef, order: usize) { let canonical_name = self.current_symbol_name(&func.name.name); if let Some((_, def_id)) = self.mutual_declarations.get(&canonical_name).copied() { @@ -609,6 +660,13 @@ impl Resolver { Item::Enum(enm) => { self.resolve_enum(enm, index); } + Item::Machine(machine) => { + for state in &machine.states { + for field in &state.fields { + self.resolve_type_expr(&field.ty, index); + } + } + } Item::VarDecl(v) => { self.resolve_global_constant_initializer(v, index); } @@ -621,7 +679,7 @@ impl Resolver { Item::Actor(actor) => { self.resolve_actor(actor, index); } - // Namespace and Enum declarations have no bodies to walk. + // Remaining declarations have no references to walk. _ => {} } } @@ -638,6 +696,7 @@ impl Resolver { } fn resolve_global_constant_initializer(&mut self, decl: &VarDecl, item_index: usize) { + self.resolve_type_expr(&decl.ty, item_index); let previous_policy = self.project_namespace_policy; self.project_namespace_policy = ProjectNamespacePolicy::Forbid; self.resolve_expr(&decl.value, item_index); @@ -655,6 +714,7 @@ impl Resolver { // State fields are in scope for handlers. for field in &actor.state_fields { + self.resolve_type_expr(&field.ty, item_index); self.resolve_expr(&field.value, item_index); self.declare_local(&field.name.name, DefKind::Variable, field.name.span); } @@ -1240,11 +1300,6 @@ impl Resolver { return; } - // Builtin module names (e.g., `math`) are always in scope as callee prefixes. - if is_builtin_module(name) { - return; - } - if let Some(def_id) = self.lookup_local_non_root(name) { self.record_resolution(name, span, item_index, def_id, false); return; @@ -1265,6 +1320,12 @@ impl Resolver { return; } + // A declared name wins over a builtin module prefix when used bare. + // Dotted calls still resolve their builtin prefix separately. + if is_builtin_module(name) { + return; + } + if let Some(def_id) = self.unique_external_namespaced_leaf(name) { self.record_resolution(name, span, item_index, def_id, false); return; @@ -1892,6 +1953,24 @@ mod tests { } } + #[test] + fn source_function_named_like_builtin_prefix_resolves_as_function() { + let source = "namespace app\nfunction validate(value: int64) returns int64:\n return value + 1\nfunction main() returns nothing:\n println(validate(4))\n"; + let result = resolve(&parse_module(source)); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + let definition = def_by_name(&result, "app.validate"); + let call_start = source.rfind("validate(4)").expect("source call") as u32; + assert_eq!( + result.resolutions.get(&sp(call_start, call_start + 8)), + Some(&definition.id) + ); + } + #[test] fn stdlib_local_names_do_not_shadow_later_project_definitions() { let stdlib = parse_module_with_file( @@ -2700,6 +2779,64 @@ function main() returns int64: ); } + #[test] + fn machine_fields_resolve_namespaced_types() { + let module = parse_module( + "namespace app\ninterface Named:\n function name(view self: Named) returns string\nmachine Session:\n states:\n empty\n active(item: Named, items: list[Named])\n transitions:\n empty to active\n", + ); + let result = resolve(&module); + assert!( + !result + .diagnostics + .iter() + .any(|d| d.severity == Severity::Error), + "{:?}", + result.diagnostics + ); + let Item::Machine(machine) = module.items.last().unwrap() else { + panic!("expected machine"); + }; + let TypeExpr::Named(item) = &machine.states[1].fields[0].ty else { + panic!("expected named field type"); + }; + let id = result + .resolutions + .get(&item.span) + .expect("resolved machine field"); + assert_eq!(result.scope_table.def(*id).name, "app.Named"); + let TypeExpr::Generic(_, arguments, _) = &machine.states[1].fields[1].ty else { + panic!("expected list field type"); + }; + let TypeExpr::Named(item) = &arguments[0] else { + panic!("expected named element type"); + }; + assert_eq!(result.resolutions.get(&item.span), Some(id)); + } + + #[test] + fn machine_fields_enforce_visibility_and_declaration_order() { + for (source, code) in [ + ( + "machine Session:\n states:\n empty\n active(item: Later)\n transitions:\n empty to active\nstruct Later:\n count: int64\n", + 205, + ), + ( + "namespace api\nstruct Hidden:\n count: int64\nnamespace app\nmachine Session:\n states:\n empty\n active(item: api.Hidden)\n transitions:\n empty to active\n", + 207, + ), + ] { + let result = resolve(&parse_module(source)); + assert!( + result + .diagnostics + .iter() + .any(|d| d.severity == Severity::Error && d.code.code() == code), + "expected E{code}, got {:?}", + result.diagnostics + ); + } + } + // ---- Test: no forward reference enforcement ---- #[test] @@ -3545,4 +3682,57 @@ function main() returns int64: .expect("resource declaration should resolve to a definition"); assert_eq!(result.scope_table.def(*def_id).kind, DefKind::Resource); } + #[test] + fn actor_state_annotations_resolve_prior_nominal_types_in_their_namespace() { + let module = parse_module( + "namespace app\nstruct Inner:\n value: int64\nactor Store:\n mutable Inner inner = Inner(value: 1)\n receive read responds int64:\n respond inner.value\n", + ); + let result = resolve(&module); + assert!( + !result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "{:?}", + result.diagnostics + ); + let Item::Struct(record) = &module.items[1] else { + panic!("record"); + }; + let Item::Actor(actor) = &module.items[2] else { + panic!("actor"); + }; + let TypeExpr::Named(annotation) = &actor.state_fields[0].ty else { + panic!("nominal type"); + }; + let resolved = result + .resolutions + .get(&annotation.span) + .expect("checked state annotation"); + assert_eq!(result.scope_table.def(*resolved).span, record.name.span); + assert_eq!(result.scope_table.def(*resolved).kind, DefKind::Struct); + } + + #[test] + fn actor_state_annotations_keep_top_to_bottom_type_visibility() { + let module = parse_module( + "namespace app\nactor Store:\n mutable Later inner = Later(value: 1)\n receive read responds int64:\n respond inner.value\nstruct Later:\n value: int64\n", + ); + let result = resolve(&module); + let Item::Actor(actor) = &module.items[1] else { + panic!("actor"); + }; + let TypeExpr::Named(annotation) = &actor.state_fields[0].ty else { + panic!("nominal type"); + }; + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error + && diagnostic.span == annotation.span), + "{:?}", + result.diagnostics + ); + } } diff --git a/crates/jett_resolve/src/resource_hook_tests.rs b/crates/jett_resolve/src/resource_hook_tests.rs new file mode 100644 index 00000000..b398077c --- /dev/null +++ b/crates/jett_resolve/src/resource_hook_tests.rs @@ -0,0 +1,288 @@ +use std::collections::HashMap; + +use jett_common::{FileId, STDLIB_FILE_ID_START, SourceOrigin, Span}; +use jett_diagnostics::Severity; +use jett_parser::{ + ast::{Item, Module}, + parse, +}; +use jett_types::ResourceKernelRecipe; + +use crate::{ + DefId, DefKind, DefVisibility, ResourceKernelError, ResourceKernelSpec, resolve, + resolve_with_resource_kernels, validate_resource_kernels, +}; + +fn parsed(source: &str, file: FileId) -> Module { + let parsed = parse(source, file); + assert!( + !parsed + .errors + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "parse errors: {:?}", + parsed.errors + ); + parsed.module +} + +fn fixture() -> ( + Module, + HashMap, + Vec, +) { + let file = FileId::new(STDLIB_FILE_ID_START); + let module = parsed( + "namespace resource_probe\nexport resource TestHandle\n", + file, + ); + let Item::Resource(resource) = &module.items[1] else { + panic!("missing resource") + }; + let span = resource.name.span; + let specs = vec![ + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_create".into(), + recipe: ResourceKernelRecipe::NetworkFactory, + }, + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_borrow".into(), + recipe: ResourceKernelRecipe::NetworkBorrow, + }, + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_close".into(), + recipe: ResourceKernelRecipe::Finalize, + }, + ]; + (module, HashMap::from([(file, SourceOrigin::Stdlib)]), specs) +} + +#[test] +fn resource_kernels_are_real_private_definitions_without_source_span_replacement() { + let (module, origins, specs) = fixture(); + let resolved = resolve_with_resource_kernels(&module, &origins, &specs).unwrap(); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error) + ); + validate_resource_kernels(&module, &resolved).unwrap(); + let resource = resolved.resolutions[&specs[0].resource_declaration]; + assert_eq!(resolved.scope_table.def(resource).kind, DefKind::Resource); + assert_eq!(resolved.resource_kernels.iter().count(), 3); + for entry in resolved.resource_kernels.iter() { + assert_ne!(entry.definition, resource); + assert_eq!(entry.resource_definition, resource); + let definition = resolved.scope_table.def(entry.definition); + assert_eq!(definition.kind, DefKind::Function); + assert_eq!(definition.visibility, DefVisibility::Private); + assert_eq!(definition.namespace.as_deref(), Some("resource_probe")); + assert_eq!(definition.span, specs[0].resource_declaration); + } + assert!(resolve(&module).resource_kernels.is_empty()); +} + +#[test] +fn resource_catalog_requires_independent_stdlib_origin_and_actual_resource() { + let (module, origins, specs) = fixture(); + let file = specs[0].resource_declaration.file; + for origin in [ + SourceOrigin::Project, + SourceOrigin::Dependency("dependency".into()), + ] { + assert!(matches!( + resolve_with_resource_kernels(&module, &HashMap::from([(file, origin)]), &specs), + Err(ResourceKernelError::UntrustedOrigin(_)) + )); + } + assert!(matches!( + resolve_with_resource_kernels(&module, &HashMap::new(), &specs), + Err(ResourceKernelError::UntrustedOrigin(_)) + )); + let project = parsed( + "namespace resource_probe\nresource TestHandle\n", + FileId::new(0), + ); + let Item::Resource(resource) = &project.items[1] else { + panic!("missing resource") + }; + let mut project_specs = specs.clone(); + for spec in &mut project_specs { + spec.resource_declaration = resource.name.span; + } + assert!(matches!( + resolve_with_resource_kernels( + &project, + &HashMap::from([(FileId::new(0), SourceOrigin::Stdlib)]), + &project_specs + ), + Err(ResourceKernelError::UntrustedOrigin(_)) + )); + let mut missing = specs.clone(); + missing[0].resource_declaration = Span::new(file, 0, 1); + assert!(matches!( + resolve_with_resource_kernels(&module, &origins, &missing), + Err(ResourceKernelError::MissingResource(_)) + )); + let alias = parsed("namespace resource_probe\ntype TestHandle = int64\n", file); + let Item::TypeAlias(alias_declaration) = &alias.items[1] else { + panic!("missing alias") + }; + missing[0].resource_declaration = alias_declaration.name.span; + assert!(matches!( + resolve_with_resource_kernels(&alias, &origins, &missing[..1]), + Err(ResourceKernelError::MissingResource(_)) + )); +} + +#[test] +fn resource_catalog_rejects_ambiguous_members_and_unnamespaced_declarations() { + let (module, origins, specs) = fixture(); + let duplicate = vec![specs[0].clone(), specs[0].clone()]; + assert!(matches!( + resolve_with_resource_kernels(&module, &origins, &duplicate), + Err(ResourceKernelError::DuplicateCatalogEntry(_)) + )); + for member in ["", "other.kernel", "1kernel", "kernel-name"] { + let mut invalid = specs[0].clone(); + invalid.member = member.into(); + assert!(matches!( + resolve_with_resource_kernels(&module, &origins, &[invalid]), + Err(ResourceKernelError::InvalidMember(_)) + )); + } + let bare = parsed("resource TestHandle\n", specs[0].resource_declaration.file); + let Item::Resource(resource) = &bare.items[0] else { + panic!("missing resource") + }; + let mut spec = specs[0].clone(); + spec.resource_declaration = resource.name.span; + assert!(matches!( + resolve_with_resource_kernels(&bare, &origins, &[spec]), + Err(ResourceKernelError::MissingNamespace(_)) + )); +} + +#[test] +fn resource_kernel_calls_preserve_order_private_access_and_source_duplicate_errors() { + let file = FileId::new(STDLIB_FILE_ID_START); + let sources = [ + "namespace resource_probe\nfunction early(view net: Network) returns nothing:\n kernel_create(view net, 1) handle error:\n return nothing\n return nothing\nresource TestHandle\n", + "namespace resource_probe\nresource TestHandle\nfunction kernel_create() returns nothing:\n return nothing\n", + ]; + for (source, code) in sources.into_iter().zip([205, 204]) { + let module = parsed(source, file); + let resource = module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource) + } else { + None + } + }) + .unwrap(); + let spec = ResourceKernelSpec { + resource_declaration: resource.name.span, + member: "kernel_create".into(), + recipe: ResourceKernelRecipe::NetworkFactory, + }; + let resolved = resolve_with_resource_kernels( + &module, + &HashMap::from([(file, SourceOrigin::Stdlib)]), + &[spec], + ) + .unwrap(); + assert!( + resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == code + && diagnostic.severity == Severity::Error), + "expected E{code}, got {:?}", + resolved.diagnostics + ); + } + let (mut module, origins, specs) = fixture(); + module.items.extend(parsed("namespace caller\nfunction main(view net: Network) returns nothing:\n use resource_probe\n resource_probe.kernel_create(view net, 1) handle error:\n return nothing\n return nothing\n", FileId::new(0)).items); + let resolved = resolve_with_resource_kernels(&module, &origins, &specs).unwrap(); + assert!( + resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 207 + && diagnostic.severity == Severity::Error), + "expected private access error: {:?}", + resolved.diagnostics + ); +} + +#[test] +fn unused_resource_kernel_metadata_is_preflighted_without_panics() { + let (module, origins, specs) = fixture(); + for corruption in 0..8 { + let mut resolved = resolve_with_resource_kernels(&module, &origins, &specs).unwrap(); + let entry = resolved.resource_kernels.iter().last().unwrap().clone(); + let definition = &mut resolved.scope_table.definitions[entry.definition.index() as usize]; + match corruption { + 0 => definition.id = DefId::new(u32::MAX), + 1 => definition.kind = DefKind::Resource, + 2 => definition.visibility = DefVisibility::Public, + 3 => definition.namespace = Some("alternate".into()), + 4 => definition.name = "resource_probe.spoof".into(), + 5 => definition.span = Span::new(FileId::new(0), 0, 1), + 6 => { + resolved + .resolutions + .insert(specs[0].resource_declaration, entry.definition); + } + _ => { + resolved.scope_table.definitions[entry.resource_definition.index() as usize].kind = + DefKind::Struct; + } + } + assert!( + validate_resource_kernels(&module, &resolved).is_err(), + "corruption {corruption} admitted" + ); + } +} + +#[test] +fn source_definition_before_resource_cannot_become_a_catalog_hook() { + let file = FileId::new(STDLIB_FILE_ID_START); + let module = parsed( + "namespace resource_probe\nfunction kernel_create() returns nothing:\n return nothing\nresource TestHandle\n", + file, + ); + let Item::Resource(resource) = &module.items[2] else { + panic!("missing resource") + }; + let spec = ResourceKernelSpec { + resource_declaration: resource.name.span, + member: "kernel_create".into(), + recipe: ResourceKernelRecipe::NetworkFactory, + }; + let resolved = resolve_with_resource_kernels( + &module, + &HashMap::from([(file, SourceOrigin::Stdlib)]), + &[spec], + ) + .unwrap(); + assert!( + resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 204 + && diagnostic.severity == Severity::Error) + ); + assert!(matches!( + validate_resource_kernels(&module, &resolved), + Err(ResourceKernelError::UnresolvedResource(_)) + )); +} diff --git a/crates/jett_resolve/src/resource_hooks.rs b/crates/jett_resolve/src/resource_hooks.rs new file mode 100644 index 00000000..b9136c33 --- /dev/null +++ b/crates/jett_resolve/src/resource_hooks.rs @@ -0,0 +1,248 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use jett_common::{FileId, SourceOrigin, Span}; +use jett_parser::ast::{Item, Module}; +use jett_types::ResourceKernelRecipe; + +use crate::{DefId, DefKind, DefVisibility, ResolveResult}; + +/// Privileged compiler input. Source/configuration cannot supply this catalog. +#[derive(Debug, Clone)] +pub struct ResourceKernelSpec { + pub resource_declaration: Span, + pub member: String, + pub recipe: ResourceKernelRecipe, +} + +/// The immutable exact declaration association; no runtime provider authority. +#[derive(Debug, Clone)] +pub struct ResolvedResourceKernel { + pub definition: DefId, + pub resource_definition: DefId, + pub recipe: ResourceKernelRecipe, + resource_declaration: Span, + member: String, + namespace: String, +} + +/// Sealed resolver evidence, in source declaration order. +#[derive(Debug, Default)] +pub struct ResolvedResourceKernels { + entries: Vec, + requested: Vec, + origins: HashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceKernelError { + MissingResource(Span), + UntrustedOrigin(Span), + MissingNamespace(Span), + InvalidMember(Span), + DuplicateCatalogEntry(Span), + UnresolvedResource(Span), + InvalidDefinition(DefId), + InvalidAssociation(DefId), +} + +impl fmt::Display for ResourceKernelError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "invalid compiler resource kernel catalog: {self:?}" + ) + } +} + +impl std::error::Error for ResourceKernelError {} + +impl ResolvedResourceKernels { + pub fn iter(&self) -> impl Iterator { + self.entries.iter() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() && self.requested.is_empty() + } + + pub fn contains_definition(&self, definition: DefId) -> bool { + self.entries + .iter() + .any(|entry| entry.definition == definition) + } + + pub(crate) fn with_catalog( + origins: &HashMap, + specs: &[ResourceKernelSpec], + ) -> Self { + Self { + entries: Vec::new(), + requested: specs.to_vec(), + origins: origins.clone(), + } + } + + pub(crate) fn insert( + &mut self, + spec: &ResourceKernelSpec, + definition: DefId, + resource_definition: DefId, + namespace: String, + ) { + self.entries.push(ResolvedResourceKernel { + definition, + resource_definition, + recipe: spec.recipe, + resource_declaration: spec.resource_declaration, + member: spec.member.clone(), + namespace, + }); + } +} + +fn declared_resources(module: &Module) -> HashMap)> { + let mut resources = HashMap::new(); + let mut current_file = None; + let mut namespace = None; + for item in &module.items { + let file = match item { + Item::Namespace(decl) => decl.span.file, + Item::Resource(decl) => decl.span.file, + Item::Function(decl) => decl.span.file, + Item::Mutual(decl) => decl.span.file, + Item::Interface(decl) => decl.span.file, + Item::Implement(decl) => decl.span.file, + Item::Struct(decl) => decl.span.file, + Item::Bitfield(decl) => decl.span.file, + Item::Enum(decl) => decl.span.file, + Item::Machine(decl) => decl.span.file, + Item::Actor(decl) => decl.span.file, + Item::VarDecl(decl) => decl.span.file, + Item::Verify(decl) => decl.span.file, + Item::Property(decl) => decl.span.file, + Item::TypeAlias(decl) => decl.span.file, + }; + if current_file.is_some_and(|previous| previous != file) { + namespace = None; + } + current_file = Some(file); + match item { + Item::Namespace(decl) => namespace = Some(decl.name.name.clone()), + Item::Resource(decl) => { + resources.insert(decl.name.span, (decl.name.name.clone(), namespace.clone())); + } + _ => {} + } + } + resources +} + +pub(crate) fn validate_specs( + module: &Module, + origins: &HashMap, + specs: &[ResourceKernelSpec], +) -> Result<(), ResourceKernelError> { + let resources = declared_resources(module); + let mut selected = HashSet::new(); + for spec in specs { + let span = spec.resource_declaration; + let (_, namespace) = resources + .get(&span) + .ok_or(ResourceKernelError::MissingResource(span))?; + if !span.file.is_stdlib() || origins.get(&span.file) != Some(&SourceOrigin::Stdlib) { + return Err(ResourceKernelError::UntrustedOrigin(span)); + } + if namespace.is_none() { + return Err(ResourceKernelError::MissingNamespace(span)); + } + let mut characters = spec.member.chars(); + if !characters + .next() + .is_some_and(|character| character.is_ascii_alphabetic() || character == '_') + || !characters.all(|character| character.is_ascii_alphanumeric() || character == '_') + { + return Err(ResourceKernelError::InvalidMember(span)); + } + if !selected.insert((span, spec.member.as_str())) { + return Err(ResourceKernelError::DuplicateCatalogEntry(span)); + } + } + Ok(()) +} + +/// Validate every association before checker authority, including unused hooks. +pub fn validate_resource_kernels( + module: &Module, + resolved: &ResolveResult, +) -> Result<(), ResourceKernelError> { + let resources = declared_resources(module); + let table = &resolved.resource_kernels; + validate_specs(module, &table.origins, &table.requested)?; + for spec in &table.requested { + if !table.entries.iter().any(|entry| { + entry.resource_declaration == spec.resource_declaration + && entry.member == spec.member + && entry.recipe == spec.recipe + }) { + return Err(ResourceKernelError::UnresolvedResource( + spec.resource_declaration, + )); + } + } + if table.entries.len() != table.requested.len() { + return Err(ResourceKernelError::InvalidAssociation( + table.entries[0].definition, + )); + } + let mut seen = HashSet::new(); + for entry in table.iter() { + let span = entry.resource_declaration; + let (name, namespace) = resources + .get(&span) + .ok_or(ResourceKernelError::MissingResource(span))?; + if !span.file.is_stdlib() || table.origins.get(&span.file) != Some(&SourceOrigin::Stdlib) { + return Err(ResourceKernelError::UntrustedOrigin(span)); + } + if namespace.as_deref() != Some(entry.namespace.as_str()) { + return Err(ResourceKernelError::InvalidAssociation(entry.definition)); + } + let definitions = &resolved.scope_table.definitions; + let resource = definitions + .get(entry.resource_definition.index() as usize) + .filter(|definition| definition.id == entry.resource_definition) + .ok_or(ResourceKernelError::InvalidDefinition( + entry.resource_definition, + ))?; + let function = definitions + .get(entry.definition.index() as usize) + .filter(|definition| definition.id == entry.definition) + .ok_or(ResourceKernelError::InvalidDefinition(entry.definition))?; + let resource_name = format!("{}.{name}", entry.namespace); + let function_name = format!("{}.{}", entry.namespace, entry.member); + let bindings = resolved + .scope_table + .scopes + .first() + .map(|scope| &scope.bindings); + if resource.kind != DefKind::Resource + || resource.name != resource_name + || resource.span != span + || resource.namespace.as_deref() != Some(entry.namespace.as_str()) + || resolved.resolutions.get(&span) != Some(&entry.resource_definition) + || function.kind != DefKind::Function + || function.name != function_name + || function.span != span + || function.namespace.as_deref() != Some(entry.namespace.as_str()) + || function.visibility != DefVisibility::Private + || !bindings.is_some_and(|bindings| { + bindings.get(&function_name) == Some(&entry.definition) + && bindings.get(&resource_name) == Some(&entry.resource_definition) + }) + || !seen.insert(entry.definition) + { + return Err(ResourceKernelError::InvalidAssociation(entry.definition)); + } + } + Ok(()) +} diff --git a/crates/jett_runtime/Cargo.toml b/crates/jett_runtime/Cargo.toml index 6a02a373..ecc8b9f5 100644 --- a/crates/jett_runtime/Cargo.toml +++ b/crates/jett_runtime/Cargo.toml @@ -4,7 +4,21 @@ version.workspace = true edition.workspace = true license.workspace = true +[lib] +crate-type = ["rlib", "staticlib"] + [dependencies] +unicode-segmentation.workspace = true +subtle.workspace = true +rand.workspace = true +serde_json.workspace = true font8x8 = "0.3.1" # Keep the macOS focus-reporting adapter in graphics.rs aligned with this version. minifb = "=0.28.0" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61.2", features = ["Win32_Globalization"] } + +# Declare this compiler-test cfg without enabling it in this crate or dependencies. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(jett_resource_native_test_archive)"] } diff --git a/crates/jett_runtime/src/clock.rs b/crates/jett_runtime/src/clock.rs new file mode 100644 index 00000000..d81fdbe8 --- /dev/null +++ b/crates/jett_runtime/src/clock.rs @@ -0,0 +1,122 @@ +//! Wall-clock conversion shared by interpreted and native Clock providers. + +use std::collections::VecDeque; + +/// One deterministic sample supplied by a Clock conformance test. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClockTestSample { + Wall { + unix_seconds: i128, + subsecond_nanoseconds: u32, + }, + Unavailable, +} + +/// Opt-in test script consumed by the native launcher before program entry. +pub const TEST_SCRIPT_ENV: &str = "JETT_NATIVE_TEST_CLOCK_SCRIPT_V1"; +pub const INVALID_SCRIPT: &str = "Clock: invalid native test script"; + +pub fn encode_test_script(samples: &[ClockTestSample]) -> String { + samples + .iter() + .map(|sample| match sample { + ClockTestSample::Wall { + unix_seconds, + subsecond_nanoseconds, + } => format!("W,{unix_seconds},{subsecond_nanoseconds}"), + ClockTestSample::Unavailable => "U".to_string(), + }) + .collect::>() + .join(";") +} + +pub fn decode_test_script(script: &str) -> Result, &'static str> { + if script.is_empty() { + return Ok(VecDeque::new()); + } + script + .split(';') + .map(|entry| { + if entry == "U" { + return Ok(ClockTestSample::Unavailable); + } + let Some(wall) = entry.strip_prefix("W,") else { + return Err(INVALID_SCRIPT); + }; + let Some((seconds, nanoseconds)) = wall.split_once(',') else { + return Err(INVALID_SCRIPT); + }; + let unix_seconds = seconds.parse().map_err(|_| INVALID_SCRIPT)?; + let subsecond_nanoseconds = nanoseconds.parse().map_err(|_| INVALID_SCRIPT)?; + Ok(ClockTestSample::Wall { + unix_seconds, + subsecond_nanoseconds, + }) + }) + .collect() +} + +pub const INVALID_SAMPLE: &str = "Clock.now: invalid test sample"; +pub const OUT_OF_RANGE: &str = "Clock.now: timestamp is outside int64 millisecond range"; + +pub fn checked_clock_milliseconds( + unix_seconds: i128, + nanoseconds: u32, +) -> Result { + if nanoseconds >= 1_000_000_000 { + return Err(INVALID_SAMPLE); + } + let milliseconds = unix_seconds + .checked_mul(1_000) + .and_then(|value| value.checked_add(i128::from(nanoseconds / 1_000_000))) + .ok_or(OUT_OF_RANGE)?; + i64::try_from(milliseconds).map_err(|_| OUT_OF_RANGE) +} + +pub fn raw_wall_clock_sample(value: std::time::SystemTime) -> (i128, u32) { + match value.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => (i128::from(duration.as_secs()), duration.subsec_nanos()), + Err(error) => { + let duration = error.duration(); + let seconds = i128::from(duration.as_secs()); + let nanoseconds = duration.subsec_nanos(); + if nanoseconds == 0 { + (-seconds, 0) + } else { + (-seconds - 1, 1_000_000_000 - nanoseconds) + } + } + } +} + +pub fn production_wall_clock_sample() -> (i128, u32) { + raw_wall_clock_sample(std::time::SystemTime::now()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clock_test_script_roundtrips_signed_extremes_and_unavailable_samples() { + let samples = [ + ClockTestSample::Wall { + unix_seconds: i128::MIN, + subsecond_nanoseconds: 1_000_000_000, + }, + ClockTestSample::Unavailable, + ClockTestSample::Wall { + unix_seconds: i128::MAX, + subsecond_nanoseconds: 0, + }, + ]; + assert_eq!( + decode_test_script(&encode_test_script(&samples)), + Ok(samples.into()) + ); + assert_eq!(decode_test_script(""), Ok(VecDeque::new())); + for invalid in ["W", "W,1", "W,1,2,3", "U;", "W,no,1", "W,1,-1"] { + assert_eq!(decode_test_script(invalid), Err(INVALID_SCRIPT)); + } + } +} diff --git a/crates/jett_runtime/src/crypto.rs b/crates/jett_runtime/src/crypto.rs new file mode 100644 index 00000000..5c09d3fd --- /dev/null +++ b/crates/jett_runtime/src/crypto.rs @@ -0,0 +1,302 @@ +//! Shared byte digests for the interpreter and native runtime. + +// --------------------------------------------------------------------------- +// SHA-256 helper (no external crate dependency) +// --------------------------------------------------------------------------- + +pub fn sha256_digest(data: &[u8]) -> Vec { + #[rustfmt::skip] + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + ]; + let mut h: [u32; 8] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ]; + let bit_len = (data.len() as u64).wrapping_mul(8); + let mut msg = data.to_vec(); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0x00); + } + msg.extend_from_slice(&bit_len.to_be_bytes()); + for chunk in msg.chunks(64) { + let mut w = [0u32; 64]; + for i in 0..16 { + w[i] = u32::from_be_bytes([ + chunk[i * 4], + chunk[i * 4 + 1], + chunk[i * 4 + 2], + chunk[i * 4 + 3], + ]); + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for i in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ ((!e) & g); + let temp1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let temp2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(temp1); + d = c; + c = b; + b = a; + a = temp1.wrapping_add(temp2); + } + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + h[5] = h[5].wrapping_add(f); + h[6] = h[6].wrapping_add(g); + h[7] = h[7].wrapping_add(hh); + } + h.into_iter() + .flat_map(u32::to_be_bytes) + .collect::>() +} + +pub fn hmac_sha256_digest(key: &[u8], message: &[u8]) -> Vec { + const BLOCK_SIZE: usize = 64; + let normalized_key = if key.len() > BLOCK_SIZE { + sha256_digest(key) + } else { + key.to_vec() + }; + let mut key_block = [0u8; BLOCK_SIZE]; + key_block[..normalized_key.len()].copy_from_slice(&normalized_key); + + let mut inner = Vec::with_capacity(BLOCK_SIZE + message.len()); + inner.extend(key_block.iter().map(|byte| byte ^ 0x36)); + inner.extend_from_slice(message); + let inner_digest = sha256_digest(&inner); + + let mut outer = Vec::with_capacity(BLOCK_SIZE + inner_digest.len()); + outer.extend(key_block.iter().map(|byte| byte ^ 0x5c)); + outer.extend_from_slice(&inner_digest); + sha256_digest(&outer) +} + +// --------------------------------------------------------------------------- +// SHA-512 helper (no external crate dependency) +// --------------------------------------------------------------------------- + +pub fn sha512_digest(data: &[u8]) -> Vec { + #[rustfmt::skip] + const K: [u64; 80] = [ + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, + 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, + 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, + 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, + 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, + 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, + 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, + ]; + let mut h: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + let bit_len = (data.len() as u128).wrapping_mul(8); + let mut msg = data.to_vec(); + msg.push(0x80); + while msg.len() % 128 != 112 { + msg.push(0x00); + } + msg.extend_from_slice(&bit_len.to_be_bytes()); + for chunk in msg.chunks(128) { + let mut w = [0u64; 80]; + for i in 0..16 { + w[i] = u64::from_be_bytes([ + chunk[i * 8], + chunk[i * 8 + 1], + chunk[i * 8 + 2], + chunk[i * 8 + 3], + chunk[i * 8 + 4], + chunk[i * 8 + 5], + chunk[i * 8 + 6], + chunk[i * 8 + 7], + ]); + } + for i in 16..80 { + let s0 = w[i - 15].rotate_right(1) ^ w[i - 15].rotate_right(8) ^ (w[i - 15] >> 7); + let s1 = w[i - 2].rotate_right(19) ^ w[i - 2].rotate_right(61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for i in 0..80 { + let s1 = e.rotate_right(14) ^ e.rotate_right(18) ^ e.rotate_right(41); + let ch = (e & f) ^ ((!e) & g); + let temp1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = a.rotate_right(28) ^ a.rotate_right(34) ^ a.rotate_right(39); + let maj = (a & b) ^ (a & c) ^ (b & c); + let temp2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(temp1); + d = c; + c = b; + b = a; + a = temp1.wrapping_add(temp2); + } + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + h[5] = h[5].wrapping_add(f); + h[6] = h[6].wrapping_add(g); + h[7] = h[7].wrapping_add(hh); + } + h.into_iter() + .flat_map(u64::to_be_bytes) + .collect::>() +} + +// --------------------------------------------------------------------------- +// MD5 helper (no external crate dependency) +// --------------------------------------------------------------------------- + +pub fn md5_digest(data: &[u8]) -> Vec { + #[rustfmt::skip] + const S: [u32; 64] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + ]; + #[rustfmt::skip] + const K: [u32; 64] = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + ]; + let mut a0: u32 = 0x67452301; + let mut b0: u32 = 0xefcdab89; + let mut c0: u32 = 0x98badcfe; + let mut d0: u32 = 0x10325476; + let bit_len = (data.len() as u64).wrapping_mul(8); + let mut msg = data.to_vec(); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0x00); + } + msg.extend_from_slice(&bit_len.to_le_bytes()); + for chunk in msg.chunks(64) { + let mut m = [0u32; 16]; + for i in 0..16 { + m[i] = u32::from_le_bytes([ + chunk[i * 4], + chunk[i * 4 + 1], + chunk[i * 4 + 2], + chunk[i * 4 + 3], + ]); + } + let mut a = a0; + let mut b = b0; + let mut c = c0; + let mut d = d0; + for i in 0..64usize { + let (f, g) = if i < 16 { + ((b & c) | ((!b) & d), i) + } else if i < 32 { + ((d & b) | ((!d) & c), (5 * i + 1) % 16) + } else if i < 48 { + (b ^ c ^ d, (3 * i + 5) % 16) + } else { + (c ^ (b | (!d)), (7 * i) % 16) + }; + let temp = d; + d = c; + c = b; + b = b.wrapping_add( + a.wrapping_add(f) + .wrapping_add(K[i]) + .wrapping_add(m[g]) + .rotate_left(S[i]), + ); + a = temp; + } + a0 = a0.wrapping_add(a); + b0 = b0.wrapping_add(b); + c0 = c0.wrapping_add(c); + d0 = d0.wrapping_add(d); + } + let mut out = [0u8; 16]; + out[0..4].copy_from_slice(&a0.to_le_bytes()); + out[4..8].copy_from_slice(&b0.to_le_bytes()); + out[8..12].copy_from_slice(&c0.to_le_bytes()); + out[12..16].copy_from_slice(&d0.to_le_bytes()); + out.to_vec() +} diff --git a/crates/jett_runtime/src/csv.rs b/crates/jett_runtime/src/csv.rs new file mode 100644 index 00000000..bf2617e2 --- /dev/null +++ b/crates/jett_runtime/src/csv.rs @@ -0,0 +1,284 @@ +//! Strict CSV parsing and quoting shared by interpreted and native execution. + +use std::collections::HashSet; + +// --------------------------------------------------------------------------- +// CSV helpers +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CsvFieldState { + Start, + Unquoted, + Quoted, + AfterQuote, +} + +/// Parse strict CSV records. Quotes may only open at the start of a field, +/// doubled quotes escape a quote inside a quoted field, and a closing quote +/// must be followed by a delimiter, record terminator, or end of input. Record +/// terminators are LF or CRLF; a bare CR is rejected outside quoted fields. +/// Leading and trailing whitespace in unquoted fields is data and is never +/// trimmed implicitly. +pub fn parse_csv_records(input: &str) -> Result>, String> { + let input = input.strip_prefix('\u{feff}').unwrap_or(input); + // Empty CSV contains no records. A physical line terminator is different: + // it is consumed below and preserves one explicitly empty record. + if input.is_empty() { + return Ok(Vec::new()); + } + + let mut records = Vec::new(); + let mut row = Vec::new(); + let mut current = String::new(); + let mut state = CsvFieldState::Start; + let mut has_record_data = false; + let mut record_number = 1usize; + let mut chars = input.chars().peekable(); + + while let Some(ch) = chars.next() { + match state { + CsvFieldState::Start => match ch { + '"' => { + has_record_data = true; + state = CsvFieldState::Quoted; + } + ',' => { + has_record_data = true; + row.push(std::mem::take(&mut current)); + } + '\n' => { + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + record_number += 1; + } + '\r' => { + if chars.peek() != Some(&'\n') { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "bare carriage return; use LF or CRLF record endings", + )); + } + chars.next(); + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + record_number += 1; + } + _ => { + has_record_data = true; + current.push(ch); + state = CsvFieldState::Unquoted; + } + }, + CsvFieldState::Unquoted => match ch { + '"' => { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "unexpected quote in unquoted field", + )); + } + ',' => { + row.push(std::mem::take(&mut current)); + state = CsvFieldState::Start; + } + '\n' => { + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + state = CsvFieldState::Start; + record_number += 1; + } + '\r' => { + if chars.peek() != Some(&'\n') { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "bare carriage return; use LF or CRLF record endings", + )); + } + chars.next(); + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + state = CsvFieldState::Start; + record_number += 1; + } + _ => current.push(ch), + }, + CsvFieldState::Quoted => { + if ch == '"' { + if chars.peek() == Some(&'"') { + current.push('"'); + chars.next(); + } else { + state = CsvFieldState::AfterQuote; + } + } else { + current.push(ch); + } + } + CsvFieldState::AfterQuote => match ch { + ',' => { + row.push(std::mem::take(&mut current)); + state = CsvFieldState::Start; + } + '\n' => { + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + state = CsvFieldState::Start; + record_number += 1; + } + '\r' => { + if chars.peek() != Some(&'\n') { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "bare carriage return; use LF or CRLF record endings", + )); + } + chars.next(); + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + true, + ); + state = CsvFieldState::Start; + record_number += 1; + } + _ => { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "unexpected character after closing quote", + )); + } + }, + } + } + + if state == CsvFieldState::Quoted { + return Err(csv_parse_error( + record_number, + row.len() + 1, + "unterminated quoted field", + )); + } + + // A trailing record terminator has already preserved its record and must + // not add another empty record at end of input. + push_csv_record( + &mut records, + &mut row, + &mut current, + &mut has_record_data, + false, + ); + Ok(records) +} + +fn csv_parse_error(record: usize, field: usize, message: &str) -> String { + format!("CSV parse error at record {record}, field {field}: {message}") +} + +fn push_csv_record( + records: &mut Vec>, + row: &mut Vec, + current: &mut String, + has_record_data: &mut bool, + preserve_empty: bool, +) { + if preserve_empty || *has_record_data || !current.is_empty() || !row.is_empty() { + row.push(std::mem::take(current)); + records.push(std::mem::take(row)); + } else { + row.clear(); + current.clear(); + } + *has_record_data = false; +} + +/// Quote a CSV field if it contains commas, quotes, or newlines. +pub fn csv_quote_field(s: &str) -> String { + if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') { + let escaped = s.replace('"', "\"\""); + format!("\"{escaped}\"") + } else { + s.to_string() + } +} + +pub fn parse_csv_with_header(input: &str) -> Result>, String> { + let records = parse_csv_records(input)?; + let mut records = records.into_iter(); + let Some(headers) = records.next() else { + return Ok(Vec::new()); + }; + let mut seen_headers = HashSet::new(); + for (index, header) in headers.iter().enumerate() { + if header.is_empty() { + return Err(format!( + "CSV header error at field {}: header must not be empty", + index + 1 + )); + } + if !seen_headers.insert(header.as_str()) { + return Err(format!( + "CSV header error at field {}: duplicate header '{}'", + index + 1, + header + )); + } + } + let mut rows = Vec::new(); + for (index, cols) in records.enumerate() { + if cols.len() != headers.len() { + return Err(format!( + "CSV header error at record {}: expected {} fields, got {}", + index + 2, + headers.len(), + cols.len() + )); + } + rows.push(headers.iter().cloned().zip(cols).collect()); + } + Ok(rows) +} + +pub fn stringify_csv_records(rows: &[Vec]) -> String { + rows.iter() + .map(|row| { + row.iter() + .map(|field| csv_quote_field(field)) + .collect::>() + .join(",") + }) + .collect::>() + .join("\n") +} diff --git a/crates/jett_runtime/src/encoding.rs b/crates/jett_runtime/src/encoding.rs new file mode 100644 index 00000000..259a6df0 --- /dev/null +++ b/crates/jett_runtime/src/encoding.rs @@ -0,0 +1,170 @@ +//! Shared checked encoding kernels for interpreted and native execution. + +pub fn base64_encode(data: &[u8]) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = String::new(); + let mut i = 0; + while i < data.len() { + let b0 = data[i] as u32; + let b1 = if i + 1 < data.len() { + data[i + 1] as u32 + } else { + 0 + }; + let b2 = if i + 2 < data.len() { + data[i + 2] as u32 + } else { + 0 + }; + let combined = (b0 << 16) | (b1 << 8) | b2; + out.push(ALPHABET[((combined >> 18) & 63) as usize] as char); + out.push(ALPHABET[((combined >> 12) & 63) as usize] as char); + if i + 1 < data.len() { + out.push(ALPHABET[((combined >> 6) & 63) as usize] as char); + } else { + out.push('='); + } + if i + 2 < data.len() { + out.push(ALPHABET[(combined & 63) as usize] as char); + } else { + out.push('='); + } + i += 3; + } + out +} + +pub fn base64_decode(s: &str) -> Result, &'static str> { + fn char_val(c: u8) -> u32 { + match c { + b'A'..=b'Z' => (c - b'A') as u32, + b'a'..=b'z' => (c - b'a' + 26) as u32, + b'0'..=b'9' => (c - b'0' + 52) as u32, + b'+' => 62, + b'/' => 63, + b'=' => 0, + _ => unreachable!("alphabet was validated before decoding"), + } + } + + let bytes = s.as_bytes(); + if bytes.len() % 4 != 0 { + return Err("invalid length"); + } + if bytes.iter().any(|byte| { + !matches!( + byte, + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' + ) + }) { + return Err("invalid character"); + } + + let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count(); + if padding > 2 + || bytes[..bytes.len().saturating_sub(padding)].contains(&b'=') + || (padding > 0 && bytes.len() < 4) + { + return Err("invalid padding"); + } + + let mut out = Vec::new(); + for chunk in bytes.chunks_exact(4) { + let chunk_padding = usize::from(chunk[3] == b'=') + usize::from(chunk[2] == b'='); + let v0 = char_val(chunk[0]); + let v1 = char_val(chunk[1]); + let v2 = char_val(chunk[2]); + let v3 = char_val(chunk[3]); + if (chunk_padding == 2 && v1 & 0x0F != 0) || (chunk_padding == 1 && v2 & 0x03 != 0) { + return Err("non-zero trailing bits"); + } + let combined = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3; + out.push(((combined >> 16) & 0xFF) as u8); + if chunk_padding < 2 { + out.push(((combined >> 8) & 0xFF) as u8); + } + if chunk_padding == 0 { + out.push((combined & 0xFF) as u8); + } + } + Ok(out) +} + +pub fn encoding_hex_decode(s: &str) -> Result, &'static str> { + fn nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } + } + + let raw = s.as_bytes(); + if raw.len() % 2 != 0 { + return Err("odd-length hex string"); + } + raw.chunks_exact(2) + .map(|pair| { + let high = nibble(pair[0]).ok_or("invalid hex characters")?; + let low = nibble(pair[1]).ok_or("invalid hex characters")?; + Ok((high << 4) | low) + }) + .collect() +} + +pub fn percent_encode(value: &str, form: bool) -> String { + let mut output = String::new(); + for byte in value.bytes() { + let literal = byte.is_ascii_alphanumeric() + || if form { + matches!(byte, b'*' | b'-' | b'.' | b'_') + } else { + matches!(byte, b'-' | b'.' | b'_' | b'~') + }; + if literal { + output.push(byte as char); + } else if form && byte == b' ' { + output.push('+'); + } else { + output.push_str(&format!("%{byte:02X}")); + } + } + output +} + +pub fn percent_decode(value: &str, form: bool) -> Result { + fn nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } + } + + let malformed = "malformed percent escape"; + let invalid_utf8 = "decoded bytes are not valid UTF-8"; + let input = value.as_bytes(); + let mut output = Vec::with_capacity(input.len()); + let mut index = 0; + while index < input.len() { + if input[index] == b'%' { + if index + 2 >= input.len() { + return Err(malformed); + } + let high = nibble(input[index + 1]).ok_or(malformed)?; + let low = nibble(input[index + 2]).ok_or(malformed)?; + output.push((high << 4) | low); + index += 3; + } else { + output.push(if form && input[index] == b'+' { + b' ' + } else { + input[index] + }); + index += 1; + } + } + String::from_utf8(output).map_err(|_| invalid_utf8) +} diff --git a/crates/jett_runtime/src/environment.rs b/crates/jett_runtime/src/environment.rs new file mode 100644 index 00000000..57f2b5ef --- /dev/null +++ b/crates/jett_runtime/src/environment.rs @@ -0,0 +1,264 @@ +//! Immutable launch snapshot shared by interpreted and native Environment providers. + +use serde_json::{Value, json}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EnvironmentTestText { + Unicode(String), + InvalidUnicode, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvironmentTestEntry { + pub name: EnvironmentTestText, + pub value: EnvironmentTestText, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvironmentTestSnapshot { + pub arguments: Vec, + pub entries: Vec, +} + +pub const TEST_SNAPSHOT_ENV: &str = "JETT_NATIVE_TEST_ENVIRONMENT_SNAPSHOT_V1"; +pub const INVALID_SNAPSHOT: &str = "Environment: invalid native test snapshot"; + +fn text_json(value: &EnvironmentTestText) -> Value { + match value { + EnvironmentTestText::Unicode(value) => json!({"unicode": value}), + EnvironmentTestText::InvalidUnicode => json!({"invalid_unicode": true}), + } +} + +pub fn encode_test_snapshot(snapshot: &EnvironmentTestSnapshot) -> String { + let arguments = snapshot.arguments.iter().map(text_json).collect::>(); + let entries = snapshot + .entries + .iter() + .map(|entry| json!({"name": text_json(&entry.name), "value": text_json(&entry.value)})) + .collect::>(); + json!({"arguments": arguments, "entries": entries}).to_string() +} + +fn test_text(value: &Value) -> Result { + let object = value.as_object().ok_or(INVALID_SNAPSHOT)?; + if object.len() != 1 { + return Err(INVALID_SNAPSHOT); + } + if let Some(value) = object.get("unicode") { + return value + .as_str() + .map(|value| EnvironmentTestText::Unicode(value.to_owned())) + .ok_or(INVALID_SNAPSHOT); + } + if object.get("invalid_unicode") == Some(&Value::Bool(true)) { + return Ok(EnvironmentTestText::InvalidUnicode); + } + Err(INVALID_SNAPSHOT) +} + +pub fn decode_test_snapshot(script: &str) -> Result { + let value: Value = serde_json::from_str(script).map_err(|_| INVALID_SNAPSHOT)?; + let object = value.as_object().ok_or(INVALID_SNAPSHOT)?; + if object.len() != 2 { + return Err(INVALID_SNAPSHOT); + } + let arguments = object + .get("arguments") + .and_then(Value::as_array) + .ok_or(INVALID_SNAPSHOT)? + .iter() + .map(test_text) + .collect::, _>>()?; + let entries = object + .get("entries") + .and_then(Value::as_array) + .ok_or(INVALID_SNAPSHOT)? + .iter() + .map(|entry| { + let object = entry.as_object().ok_or(INVALID_SNAPSHOT)?; + if object.len() != 2 { + return Err(INVALID_SNAPSHOT); + } + Ok(EnvironmentTestEntry { + name: test_text(object.get("name").ok_or(INVALID_SNAPSHOT)?)?, + value: test_text(object.get("value").ok_or(INVALID_SNAPSHOT)?)?, + }) + }) + .collect::, _>>()?; + Ok(EnvironmentTestSnapshot { arguments, entries }) +} + +#[derive(Debug, Clone)] +enum FrozenLaunchText { + Unicode(String), + InvalidUnicode, +} + +#[derive(Debug, Clone)] +struct FrozenEnvironmentEntry { + name: FrozenLaunchText, + value: FrozenLaunchText, +} + +#[derive(Debug, Clone)] +pub struct LaunchEnvironmentSnapshot { + arguments: Vec, + entries: Vec, +} + +impl LaunchEnvironmentSnapshot { + pub fn production() -> Result { + let arguments = std::env::args_os() + .skip(1) + .map(frozen_launch_text_from_os) + .map(|argument| match argument { + FrozenLaunchText::Unicode(argument) => Ok(argument), + FrozenLaunchText::InvalidUnicode => { + Err("Environment: argument is not valid Unicode") + } + }) + .collect::, _>>()?; + let entries = std::env::vars_os() + .map(|(name, value)| FrozenEnvironmentEntry { + name: frozen_launch_text_from_os(name), + value: frozen_launch_text_from_os(value), + }) + .collect(); + Ok(Self { arguments, entries }) + } + + pub fn injected(snapshot: EnvironmentTestSnapshot) -> Result { + let arguments = snapshot + .arguments + .into_iter() + .map(|argument| match argument { + EnvironmentTestText::Unicode(argument) => Ok(argument), + EnvironmentTestText::InvalidUnicode => { + Err("Environment: argument is not valid Unicode") + } + }) + .collect::, _>>()?; + let entries = snapshot + .entries + .into_iter() + .map(|entry| FrozenEnvironmentEntry { + name: match entry.name { + EnvironmentTestText::Unicode(name) => FrozenLaunchText::Unicode(name), + EnvironmentTestText::InvalidUnicode => FrozenLaunchText::InvalidUnicode, + }, + value: match entry.value { + EnvironmentTestText::Unicode(value) => FrozenLaunchText::Unicode(value), + EnvironmentTestText::InvalidUnicode => FrozenLaunchText::InvalidUnicode, + }, + }) + .collect(); + Ok(Self { arguments, entries }) + } + + pub fn arguments(&self) -> &[String] { + &self.arguments + } + + pub fn get(&self, key: &str) -> Result, &'static str> { + if key.is_empty() || key.contains('=') || key.contains('\0') { + return Err("Environment.get: invalid variable name"); + } + let matching_entry = self.entries.iter().find(|entry| match &entry.name { + FrozenLaunchText::Unicode(name) => environment_names_equal(name, key), + FrozenLaunchText::InvalidUnicode => false, + }); + match matching_entry.map(|entry| &entry.value) { + None => Ok(None), + Some(FrozenLaunchText::Unicode(value)) => Ok(Some(value.clone())), + Some(FrozenLaunchText::InvalidUnicode) => { + Err("Environment.get: value is not valid Unicode") + } + } + } +} + +fn frozen_launch_text_from_os(value: std::ffi::OsString) -> FrozenLaunchText { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + return match String::from_utf8(value.into_vec()) { + Ok(value) => FrozenLaunchText::Unicode(value), + Err(_) => FrozenLaunchText::InvalidUnicode, + }; + } + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + return match String::from_utf16(&value.encode_wide().collect::>()) { + Ok(value) => FrozenLaunchText::Unicode(value), + Err(_) => FrozenLaunchText::InvalidUnicode, + }; + } +} + +fn environment_names_equal(captured: &str, requested: &str) -> bool { + #[cfg(windows)] + { + use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal}; + let captured = captured.encode_utf16().collect::>(); + let requested = requested.encode_utf16().collect::>(); + let result = unsafe { + CompareStringOrdinal( + captured.as_ptr(), + captured.len() as i32, + requested.as_ptr(), + requested.len() as i32, + 1, + ) + }; + result == CSTR_EQUAL + } + #[cfg(not(windows))] + { + captured == requested + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn injected_snapshot_roundtrips_invalid_values_and_first_duplicate() { + let snapshot = EnvironmentTestSnapshot { + arguments: vec![EnvironmentTestText::Unicode(String::new())], + entries: vec![ + EnvironmentTestEntry { + name: EnvironmentTestText::Unicode("KEY".into()), + value: EnvironmentTestText::InvalidUnicode, + }, + EnvironmentTestEntry { + name: EnvironmentTestText::Unicode("KEY".into()), + value: EnvironmentTestText::Unicode("later".into()), + }, + ], + }; + let decoded = decode_test_snapshot(&encode_test_snapshot(&snapshot)).unwrap(); + assert_eq!(decoded, snapshot); + let frozen = LaunchEnvironmentSnapshot::injected(decoded).unwrap(); + assert_eq!(frozen.arguments(), &[String::new()]); + assert_eq!( + frozen.get("KEY"), + Err("Environment.get: value is not valid Unicode") + ); + assert_eq!(frozen.get("MISSING"), Ok(None)); + assert_eq!( + frozen.get(""), + Err("Environment.get: invalid variable name") + ); + assert_eq!( + LaunchEnvironmentSnapshot::injected(EnvironmentTestSnapshot { + arguments: vec![EnvironmentTestText::InvalidUnicode], + entries: vec![], + }) + .err(), + Some("Environment: argument is not valid Unicode") + ); + } +} diff --git a/crates/jett_runtime/src/graphics.rs b/crates/jett_runtime/src/graphics.rs index 9bd40d78..0c6e1ab3 100644 --- a/crates/jett_runtime/src/graphics.rs +++ b/crates/jett_runtime/src/graphics.rs @@ -72,6 +72,124 @@ pub enum Key { Space, } +/// Deterministic session input shared by interpreter and native parity runs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TestEvent { + Key(Key), + Close, + HostError(String), +} + +pub const TEST_SCRIPT_ENV: &str = "JETT_NATIVE_TEST_GRAPHICS_SCRIPT_V1"; +pub const INVALID_TEST_SCRIPT: &str = "Graphics: invalid test script"; + +impl Key { + pub fn variant_index(self) -> u64 { + match self { + Self::Up => 0, + Self::Down => 1, + Self::Left => 2, + Self::Right => 3, + Self::W => 4, + Self::A => 5, + Self::S => 6, + Self::D => 7, + Self::U => 8, + Self::Z => 9, + Self::R => 10, + Self::N => 11, + Self::P => 12, + Self::Enter => 13, + Self::Space => 14, + } + } + + fn script_name(self) -> &'static str { + match self { + Self::Up => "up", + Self::Down => "down", + Self::Left => "left", + Self::Right => "right", + Self::W => "w", + Self::A => "a", + Self::S => "s", + Self::D => "d", + Self::U => "u", + Self::Z => "z", + Self::R => "r", + Self::N => "n", + Self::P => "p", + Self::Enter => "enter", + Self::Space => "space", + } + } + + fn from_script_name(name: &str) -> Option { + Some(match name { + "up" => Self::Up, + "down" => Self::Down, + "left" => Self::Left, + "right" => Self::Right, + "w" => Self::W, + "a" => Self::A, + "s" => Self::S, + "d" => Self::D, + "u" => Self::U, + "z" => Self::Z, + "r" => Self::R, + "n" => Self::N, + "p" => Self::P, + "enter" => Self::Enter, + "space" => Self::Space, + _ => return None, + }) + } +} + +pub fn encode_test_script(events: &[TestEvent]) -> String { + let events = events + .iter() + .map(|event| match event { + TestEvent::Key(key) => serde_json::json!({ "key": key.script_name() }), + TestEvent::Close => serde_json::Value::String("close".to_owned()), + TestEvent::HostError(message) => serde_json::json!({ "host_error": message }), + }) + .collect::>(); + serde_json::to_string(&events).expect("graphics test script is JSON serializable") +} + +pub fn decode_test_script(text: &str) -> Result, String> { + let value: serde_json::Value = + serde_json::from_str(text).map_err(|_| INVALID_TEST_SCRIPT.to_owned())?; + let events = value.as_array().ok_or(INVALID_TEST_SCRIPT)?; + events + .iter() + .map(|event| { + if event == "close" { + return Ok(TestEvent::Close); + } + let object = event.as_object().ok_or(INVALID_TEST_SCRIPT)?; + if object.len() != 1 { + return Err(INVALID_TEST_SCRIPT.to_owned()); + } + if let Some(key) = object.get("key") { + let key = key + .as_str() + .and_then(Key::from_script_name) + .ok_or(INVALID_TEST_SCRIPT)?; + return Ok(TestEvent::Key(key)); + } + if let Some(message) = object.get("host_error") { + return message + .as_str() + .map(|message| TestEvent::HostError(message.to_owned())) + .ok_or_else(|| INVALID_TEST_SCRIPT.to_owned()); + } + Err(INVALID_TEST_SCRIPT.to_owned()) + }) + .collect() +} + pub fn validate_config(config: &Config) -> Result<(), String> { dimensions(config.width, config.height)?; if config.title.len() > MAX_TITLE_BYTES || config.title.contains('\0') { @@ -430,6 +548,35 @@ fn portable_key(key: minifb::Key) -> Option { mod tests { use super::*; + #[test] + fn scripted_graphics_events_roundtrip_and_reject_unknown_shapes() { + let events = [ + TestEvent::Key(Key::Right), + TestEvent::HostError("window: lost \"focus\"".to_owned()), + TestEvent::Close, + ]; + let encoded = encode_test_script(&events); + assert_eq!( + decode_test_script(&encoded) + .unwrap() + .into_iter() + .collect::>(), + events + ); + for invalid in [ + "", + "{}", + "[\"right\"]", + "[{\"key\":\"escape\"}]", + "[{\"close\":true}]", + ] { + assert_eq!( + decode_test_script(invalid), + Err(INVALID_TEST_SCRIPT.to_owned()) + ); + } + } + const BLACK: Color = Color { red: 0, green: 0, diff --git a/crates/jett_runtime/src/lib.rs b/crates/jett_runtime/src/lib.rs index aff89e60..bab2f83d 100644 --- a/crates/jett_runtime/src/lib.rs +++ b/crates/jett_runtime/src/lib.rs @@ -1,6 +1,16 @@ //! Backend-neutral runtime services for Jett execution contexts. +pub mod clock; +pub mod crypto; +pub mod csv; +pub mod encoding; +pub mod environment; pub mod graphics; +pub mod math; +pub mod native_abi; +pub mod random; +mod resource_custody; +pub mod uuid; use std::any::Any; use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; @@ -13,11 +23,11 @@ type Finalizer = Box; type PanicPayload = Box; fn discard_panic_payload(payload: PanicPayload) { - if let Err(secondary) = catch_unwind(AssertUnwindSafe(|| drop(payload))) { - // A panic payload can itself panic when dropped. Do not recursively - // drop that secondary payload: provider cleanup must remain bounded. - std::mem::forget(secondary); - } + // Panic payload destruction is user-controlled. Forgetting the payload is + // the only bounded operation: catching one destructor panic is not enough + // when another field destructor can panic during that unwind and abort the + // process before the catch boundary regains control. + std::mem::forget(payload); } fn retain_first_failure(first: &mut Option, result: std::thread::Result<()>) { diff --git a/crates/jett_runtime/src/math.rs b/crates/jett_runtime/src/math.rs new file mode 100644 index 00000000..0742aefe --- /dev/null +++ b/crates/jett_runtime/src/math.rs @@ -0,0 +1,31 @@ +//! Floating-point reductions shared by the interpreter and native runtime. + +pub fn float_midpoint(left: f64, right: f64) -> f64 { + if left == right { + return left; + } + if !left.is_finite() || !right.is_finite() { + return (left + right) / 2.0; + } + if left.is_sign_negative() == right.is_sign_negative() { + left + (right - left) / 2.0 + } else { + left / 2.0 + right / 2.0 + } +} + +pub fn float_average(values: &[f64]) -> f64 { + let sum = values.iter().sum::(); + // Rescaling can underflow small residuals after large values cancel. + // Keep ordinary summation whenever it did not overflow. + if sum.is_finite() { + return sum / values.len() as f64; + } + let scale = values.iter().map(|value| value.abs()).fold(0.0, f64::max); + if scale == 0.0 || !scale.is_finite() { + return sum / values.len() as f64; + } + + let scaled_sum = values.iter().map(|value| value / scale).sum::(); + scaled_sum / values.len() as f64 * scale +} diff --git a/crates/jett_runtime/src/native_abi.rs b/crates/jett_runtime/src/native_abi.rs new file mode 100644 index 00000000..97788d58 --- /dev/null +++ b/crates/jett_runtime/src/native_abi.rs @@ -0,0 +1,1039 @@ +//! Version 1 of the native compiler/runtime ABI. +//! +//! This module is the narrow C ABI used by ahead-of-time generated code. Rust +//! ABI values never cross this boundary: every shared value has a fixed-width +//! scalar or `repr(C)` layout, runtime state is reached through an opaque +//! token. Lifecycle operations write an explicit [`JettRuntimeResultV1`] out +//! parameter; the typed leaves in [`values`] use the context-local terminal +//! failure channel, checked by generated code after every fallible call. +//! +//! # Ownership and lifetimes +//! +//! - [`jett_rt_v1_context_create`] initializes one caller-owned context record. +//! The record must remain at that address and immutable until its successful +//! destruction and all calls using it have returned. +//! - The record contains only scalar identity. Runtime-owned state stays in a +//! synchronized registry, so copied, moved, forged, stale, and repeatedly +//! destroyed records fail without interpreting caller bytes as a pointer. +//! - Destroying a context atomically retires its registry entry, prevents new +//! calls from acquiring leases, waits for existing leases, and then releases +//! internal state exactly once. The record bytes remain unchanged and may be +//! reused after destruction returns. +//! - [`JettRuntimeStringSliceV1`] is always borrowed. Its bytes are never +//! retained by a runtime call. Message slices written to +//! [`JettRuntimeResultV1`] refer to immutable process-lifetime storage and +//! must not be freed or mutated by the caller. +//! - All non-null pointer arguments must satisfy the validity requirements +//! documented on their functions. The runtime can reject invalid record +//! contents, but it cannot validate arbitrary, dangling, or unreadable +//! addresses. A zero-length input string may use a null data pointer. +//! - Callable symbols return only a transparent 32-bit status. Records are +//! passed by pointer so generated calls do not depend on target-specific +//! aggregate argument or return conventions. +//! +//! Exported calls catch recoverable Rust panics implemented by unwinding and +//! map them to [`JettRuntimeStatusV1::PANIC`]. Aborting panic hooks, allocator +//! failure, hardware faults, and other process-aborting conditions cannot be +//! recovered here. No Rust unwind is allowed to cross this ABI. + +#[cfg(not(target_pointer_width = "64"))] +compile_error!("native runtime ABI v1 requires a 64-bit target"); + +#[cfg(not(panic = "unwind"))] +compile_error!("native runtime ABI v1 requires panic=unwind"); + +use std::collections::HashMap; +use std::io::{self, Write}; +use std::mem::{align_of, offset_of, size_of}; +use std::num::NonZeroU64; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::ptr; +use std::slice; +use std::str; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock}; + +use crate::{ResourceRegistry, discard_panic_payload}; + +mod resource; +pub use resource::{ + jett_rt_v1_resource_program_failure, jett_rt_v1_resource_program_failure_copy, + jett_rt_v1_resource_program_unwind, +}; +pub mod values; + +/// The native runtime ABI version implemented by this module. +pub const JETT_RUNTIME_ABI_VERSION_V1: u32 = 1; + +const LIVE_CONTEXT_TAG: u32 = 0x4a52_5431; + +const ABI_MISMATCH_MESSAGE: &[u8] = b"runtime ABI version mismatch"; +const CONTEXT_OUTPUT_NULL_MESSAGE: &[u8] = b"context output pointer is null"; +const CONTEXT_NULL_MESSAGE: &[u8] = b"runtime context pointer is null"; +const CONTEXT_INVALID_MESSAGE: &[u8] = b"runtime context is not live"; +const CONTEXT_EXHAUSTED_MESSAGE: &[u8] = b"runtime context capacity exhausted"; +const STRING_SLICE_NULL_MESSAGE: &[u8] = b"string slice pointer is null"; +const STRING_NULL_MESSAGE: &[u8] = b"string data pointer is null"; +const STRING_LENGTH_MESSAGE: &[u8] = b"string byte length exceeds host address space"; +const STRING_UTF8_MESSAGE: &[u8] = b"string data is not valid UTF-8"; +const STDOUT_WRITE_MESSAGE: &[u8] = b"stdout write failed"; +const STDERR_WRITE_MESSAGE: &[u8] = b"stderr write failed"; +const PANIC_MESSAGE: &[u8] = b"runtime operation panicked"; + +/// A stable, fixed-width status code returned by native runtime operations. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JettRuntimeStatusV1(u32); + +impl JettRuntimeStatusV1 { + pub const OK: Self = Self(0); + pub const INVALID_ARGUMENT: Self = Self(1); + pub const ABI_VERSION_MISMATCH: Self = Self(2); + pub const INVALID_CONTEXT: Self = Self(3); + pub const INVALID_UTF8: Self = Self(4); + pub const IO_FAILURE: Self = Self(5); + pub const LENGTH_OUT_OF_RANGE: Self = Self(6); + pub const RESOURCE_EXHAUSTED: Self = Self(7); + pub const PANIC: Self = Self(255); + + pub const fn code(self) -> u32 { + self.0 + } +} + +/// A borrowed UTF-8 string represented as a pointer and byte length. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JettRuntimeStringSliceV1 { + pub data: *const u8, + pub byte_length: u64, +} + +impl JettRuntimeStringSliceV1 { + pub const fn empty() -> Self { + Self { + data: ptr::null(), + byte_length: 0, + } + } + + fn from_static(value: &'static [u8]) -> Self { + let byte_length = u64::try_from(value.len()).unwrap_or(u64::MAX); + Self { + data: value.as_ptr(), + byte_length, + } + } +} + +/// The uniform result record written by every native runtime operation. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JettRuntimeResultV1 { + pub status: JettRuntimeStatusV1, + pub message: JettRuntimeStringSliceV1, +} + +impl JettRuntimeResultV1 { + fn ok() -> Self { + Self { + status: JettRuntimeStatusV1::OK, + message: JettRuntimeStringSliceV1::empty(), + } + } + + fn failure(status: JettRuntimeStatusV1, message: &'static [u8]) -> Self { + Self { + status, + message: JettRuntimeStringSliceV1::from_static(message), + } + } +} + +/// A caller-owned scalar identity record for per-program runtime state. +#[repr(C)] +#[derive(Debug)] +pub struct JettRuntimeContextV1 { + token: u64, + abi_version: u32, + state_tag: u32, +} + +impl JettRuntimeContextV1 { + const fn retired() -> Self { + Self { + token: 0, + abi_version: 0, + state_tag: 0, + } + } + + fn has_live_shape(&self) -> bool { + self.token != 0 + && self.abi_version == JETT_RUNTIME_ABI_VERSION_V1 + && self.state_tag == LIVE_CONTEXT_TAG + } +} + +const _: () = { + assert!(size_of::<*const ()>() == 8); + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 16); + assert!(align_of::() == 8); + assert!(offset_of!(JettRuntimeStringSliceV1, data) == 0); + assert!(offset_of!(JettRuntimeStringSliceV1, byte_length) == 8); + assert!(size_of::() == 24); + assert!(align_of::() == 8); + assert!(offset_of!(JettRuntimeResultV1, status) == 0); + assert!(offset_of!(JettRuntimeResultV1, message) == 8); + assert!(size_of::() == 16); + assert!(align_of::() == 8); + assert!(offset_of!(JettRuntimeContextV1, token) == 0); + assert!(offset_of!(JettRuntimeContextV1, abi_version) == 8); + assert!(offset_of!(JettRuntimeContextV1, state_tag) == 12); +}; + +struct NativeContextState { + values: values::NativeValues, + resources: ResourceRegistry, + resource_state: Option, +} + +impl NativeContextState { + fn new() -> Self { + Self { + resources: ResourceRegistry::new(), + resource_state: None, + values: values::NativeValues::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ContextRegistryKey { + owner_address: usize, + token: u64, + abi_version: u32, +} + +#[derive(Default)] +struct NativeContextLifecycle { + active_leases: usize, +} + +struct NativeContextEntry { + lifecycle: Mutex, + leases_drained: Condvar, + state: Mutex>, +} + +impl NativeContextEntry { + fn new() -> Self { + Self { + lifecycle: Mutex::new(NativeContextLifecycle::default()), + leases_drained: Condvar::new(), + state: Mutex::new(Some(NativeContextState::new())), + } + } + + fn wait_for_leases_and_take_state(&self) -> Option { + let mut lifecycle = lock_unpoisoned(&self.lifecycle); + while lifecycle.active_leases != 0 { + lifecycle = self + .leases_drained + .wait(lifecycle) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + } + drop(lifecycle); + lock_unpoisoned(&self.state).take() + } +} + +struct NativeContextLease { + entry: Arc, +} + +impl Drop for NativeContextLease { + fn drop(&mut self) { + let mut lifecycle = lock_unpoisoned(&self.entry.lifecycle); + if lifecycle.active_leases == 0 { + return; + } + lifecycle.active_leases -= 1; + if lifecycle.active_leases == 0 { + self.entry.leases_drained.notify_all(); + } + } +} + +struct NativeContextRegistry { + contexts: HashMap>, + next_token: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RegisterContextError { + OwnerAlreadyLive, + TokenExhausted, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AcquireContextError { + Invalid, + LeaseExhausted, +} + +impl NativeContextRegistry { + fn new() -> Self { + Self { + contexts: HashMap::new(), + next_token: NonZeroU64::new(1), + } + } + + fn take_token(&mut self) -> Option { + let token = self.next_token?; + self.next_token = token.get().checked_add(1).and_then(NonZeroU64::new); + Some(token.get()) + } + + fn has_live_owner(&self, owner_address: usize) -> bool { + self.contexts + .keys() + .any(|key| key.owner_address == owner_address) + } + + fn register( + &mut self, + owner_address: usize, + abi_version: u32, + entry: Arc, + ) -> Result { + if self.has_live_owner(owner_address) { + return Err(RegisterContextError::OwnerAlreadyLive); + } + let token = self + .take_token() + .ok_or(RegisterContextError::TokenExhausted)?; + let key = ContextRegistryKey { + owner_address, + token, + abi_version, + }; + self.contexts.insert(key, entry); + Ok(key) + } + + fn retire(&mut self, key: ContextRegistryKey) -> Option> { + self.contexts.remove(&key) + } +} + +fn native_context_registry() -> &'static Mutex { + static REGISTRY: OnceLock> = OnceLock::new(); + REGISTRY.get_or_init(|| Mutex::new(NativeContextRegistry::new())) +} + +fn lock_unpoisoned(mutex: &Mutex) -> MutexGuard<'_, T> { + mutex + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +fn context_key( + context: *const JettRuntimeContextV1, +) -> Result { + let Some(record) = (unsafe { context.as_ref() }) else { + return Err(JettRuntimeStatusV1::INVALID_ARGUMENT); + }; + if !record.has_live_shape() { + return Err(JettRuntimeStatusV1::INVALID_CONTEXT); + } + Ok(ContextRegistryKey { + owner_address: context.addr(), + token: record.token, + abi_version: record.abi_version, + }) +} + +fn acquire_context(key: ContextRegistryKey) -> Result { + let registry = lock_unpoisoned(native_context_registry()); + let entry = registry + .contexts + .get(&key) + .cloned() + .ok_or(AcquireContextError::Invalid)?; + let mut lifecycle = lock_unpoisoned(&entry.lifecycle); + let active_leases = lifecycle + .active_leases + .checked_add(1) + .ok_or(AcquireContextError::LeaseExhausted)?; + lifecycle.active_leases = active_leases; + drop(lifecycle); + drop(registry); + Ok(NativeContextLease { entry }) +} + +fn retire_context(key: ContextRegistryKey) -> Option> { + lock_unpoisoned(native_context_registry()).retire(key) +} + +#[cfg(test)] +fn context_is_registered(context: *const JettRuntimeContextV1) -> bool { + let Ok(key) = context_key(context) else { + return false; + }; + lock_unpoisoned(native_context_registry()) + .contexts + .contains_key(&key) +} + +#[unsafe(export_name = "jett_rt_v1_abi_version")] +pub static JETT_RT_V1_ABI_VERSION_SYMBOL: u32 = JETT_RUNTIME_ABI_VERSION_V1; + +/// Check whether the caller's ABI version matches this runtime. +/// +/// # Safety +/// +/// If non-null, `out_result` must be aligned and valid for one exclusive write +/// of a [`JettRuntimeResultV1`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_check_abi( + expected_version: u32, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out_result, || { + if expected_version == JETT_RUNTIME_ABI_VERSION_V1 { + JettRuntimeResultV1::ok() + } else { + JettRuntimeResultV1::failure( + JettRuntimeStatusV1::ABI_VERSION_MISMATCH, + ABI_MISMATCH_MESSAGE, + ) + } + }) +} + +/// Initialize one caller-owned runtime context record. +/// +/// The output must be exclusively writable, nonoverlapping storage. Concurrent +/// creation at the same address is not permitted. On success the record becomes +/// immutable until destruction and all concurrent calls return. +/// +/// # Safety +/// +/// Non-null pointers must be aligned, valid for their records, and satisfy the +/// exclusivity, lifetime, and nonoverlap requirements above. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_context_create( + expected_version: u32, + out_context: *mut JettRuntimeContextV1, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out_result, || { + if out_context.is_null() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + CONTEXT_OUTPUT_NULL_MESSAGE, + ); + } + + let owner_address = out_context.addr(); + if lock_unpoisoned(native_context_registry()).has_live_owner(owner_address) { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + } + unsafe { ptr::write(out_context, JettRuntimeContextV1::retired()) }; + if expected_version != JETT_RUNTIME_ABI_VERSION_V1 { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::ABI_VERSION_MISMATCH, + ABI_MISMATCH_MESSAGE, + ); + } + + let entry = Arc::new(NativeContextEntry::new()); + let registration = lock_unpoisoned(native_context_registry()).register( + owner_address, + JETT_RUNTIME_ABI_VERSION_V1, + entry, + ); + let key = match registration { + Ok(key) => key, + Err(RegisterContextError::OwnerAlreadyLive) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + } + Err(RegisterContextError::TokenExhausted) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + CONTEXT_EXHAUSTED_MESSAGE, + ); + } + }; + unsafe { + ptr::write( + out_context, + JettRuntimeContextV1 { + token: key.token, + abi_version: key.abi_version, + state_tag: LIVE_CONTEXT_TAG, + }, + ) + }; + JettRuntimeResultV1::ok() + }) +} + +/// Destroy one live runtime context without modifying its identity record. +/// +/// Retirement is atomic with lease acquisition. Existing calls finish before +/// cleanup; copied, moved, forged, stale, and repeated records are rejected. +/// +/// # Safety +/// +/// Non-null pointers must be aligned, initialized, readable, immutable for the +/// call, alive until return, and nonoverlapping with the writable result. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_context_destroy( + context: *mut JettRuntimeContextV1, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out_result, || { + if context.is_null() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + CONTEXT_NULL_MESSAGE, + ); + } + let key = match context_key(context.cast_const()) { + Ok(key) => key, + Err(status) => { + return JettRuntimeResultV1::failure(status, CONTEXT_INVALID_MESSAGE); + } + }; + let Some(entry) = retire_context(key) else { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + }; + let Some(mut state) = entry.wait_for_leases_and_take_state() else { + return JettRuntimeResultV1::failure(JettRuntimeStatusV1::PANIC, PANIC_MESSAGE); + }; + let resource_cleanup_failed = if let Some(resource) = &mut state.resource_state { + resource.safety_unwind(&mut state.values, &mut state.resources) + } else { + false + }; + let graphics_cleanup_failed = state.values.release_graphics_session().is_err(); + let actor_cleanup_failed = state.values.release_actors().is_err(); + if resource_cleanup_failed || graphics_cleanup_failed || actor_cleanup_failed { + state.values.cleanup_failed = true; + } + let leaked = !state.values.is_empty(); + let cleanup_failed = state.values.cleanup_failed; + drop(state); + if cleanup_failed { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"native value cleanup failed", + ); + } + if leaked { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"native value ownership leak", + ); + } + JettRuntimeResultV1::ok() + }) +} + +/// Write one borrowed Jett string to the process stdout byte stream. +/// +/// # Safety +/// +/// Non-null record pointers must be aligned, initialized, readable, immutable, +/// alive through the call, and nonoverlapping with the writable result. A +/// nonempty byte range must be contained in one readable allocation. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_stdout_write( + context: *const JettRuntimeContextV1, + string: *const JettRuntimeStringSliceV1, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out_result, || { + if context.is_null() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + CONTEXT_NULL_MESSAGE, + ); + } + let key = match context_key(context) { + Ok(key) => key, + Err(status) => { + return JettRuntimeResultV1::failure(status, CONTEXT_INVALID_MESSAGE); + } + }; + let _lease = match acquire_context(key) { + Ok(lease) => lease, + Err(AcquireContextError::Invalid) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + } + Err(AcquireContextError::LeaseExhausted) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + CONTEXT_EXHAUSTED_MESSAGE, + ); + } + }; + let Some(string) = (unsafe { string.as_ref() }) else { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + STRING_SLICE_NULL_MESSAGE, + ); + }; + let Ok(byte_length) = usize::try_from(string.byte_length) else { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE, + STRING_LENGTH_MESSAGE, + ); + }; + if byte_length > isize::MAX as usize { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE, + STRING_LENGTH_MESSAGE, + ); + } + let bytes = if byte_length == 0 { + &[] + } else { + if string.data.is_null() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_ARGUMENT, + STRING_NULL_MESSAGE, + ); + } + if string.data.addr().checked_add(byte_length).is_none() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE, + STRING_LENGTH_MESSAGE, + ); + } + unsafe { slice::from_raw_parts(string.data, byte_length) } + }; + if str::from_utf8(bytes).is_err() { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_UTF8, + STRING_UTF8_MESSAGE, + ); + } + match write_all_bytes(io::stdout().lock(), bytes) { + Ok(()) => JettRuntimeResultV1::ok(), + Err(_) => { + JettRuntimeResultV1::failure(JettRuntimeStatusV1::IO_FAILURE, STDOUT_WRITE_MESSAGE) + } + } + }) +} + +fn write_all_bytes(mut output: impl Write, bytes: &[u8]) -> io::Result<()> { + output.write_all(bytes) +} + +fn complete_call( + out_result: *mut JettRuntimeResultV1, + operation: impl FnOnce() -> JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + if out_result.is_null() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + let result = match catch_unwind(AssertUnwindSafe(operation)) { + Ok(result) => result, + Err(payload) => { + discard_panic_payload(payload); + JettRuntimeResultV1::failure(JettRuntimeStatusV1::PANIC, PANIC_MESSAGE) + } + }; + let status = result.status; + unsafe { ptr::write(out_result, result) }; + status +} + +#[cfg(test)] +mod tests { + use std::mem::MaybeUninit; + use std::panic::panic_any; + use std::ptr::NonNull; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier, mpsc}; + use std::thread; + use std::time::{Duration, Instant}; + + use crate::{AuthorityProvenance, ResourceTypeId}; + + use super::*; + + fn message(result: JettRuntimeResultV1) -> &'static str { + if result.message.byte_length == 0 { + return ""; + } + let length = usize::try_from(result.message.byte_length).expect("test message fits usize"); + let bytes = unsafe { slice::from_raw_parts(result.message.data, length) }; + str::from_utf8(bytes).expect("runtime diagnostic is UTF-8") + } + + fn check_abi(expected_version: u32) -> (JettRuntimeStatusV1, JettRuntimeResultV1) { + let mut result = MaybeUninit::uninit(); + let status = unsafe { jett_rt_v1_check_abi(expected_version, result.as_mut_ptr()) }; + (status, unsafe { result.assume_init() }) + } + + fn create_context( + expected_version: u32, + context: *mut JettRuntimeContextV1, + ) -> (JettRuntimeStatusV1, JettRuntimeResultV1) { + let mut result = MaybeUninit::uninit(); + let status = + unsafe { jett_rt_v1_context_create(expected_version, context, result.as_mut_ptr()) }; + (status, unsafe { result.assume_init() }) + } + + fn destroy_context( + context: *mut JettRuntimeContextV1, + ) -> (JettRuntimeStatusV1, JettRuntimeResultV1) { + let mut result = MaybeUninit::uninit(); + let status = unsafe { jett_rt_v1_context_destroy(context, result.as_mut_ptr()) }; + (status, unsafe { result.assume_init() }) + } + + fn write_stdout( + context: *const JettRuntimeContextV1, + string: *const JettRuntimeStringSliceV1, + ) -> (JettRuntimeStatusV1, JettRuntimeResultV1) { + let mut result = MaybeUninit::uninit(); + let status = unsafe { jett_rt_v1_stdout_write(context, string, result.as_mut_ptr()) }; + (status, unsafe { result.assume_init() }) + } + + fn install_cleanup_probe(context: *const JettRuntimeContextV1, count: Arc) { + let key = context_key(context).unwrap(); + let lease = acquire_context(key).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + state + .as_mut() + .unwrap() + .resources + .insert( + ResourceTypeId::new(1), + (), + AuthorityProvenance::new(1, 1), + move |_| { + count.fetch_add(1, Ordering::SeqCst); + }, + ) + .unwrap(); + } + + #[test] + fn abi_version_status_tags_and_layouts_are_stable() { + assert_eq!(JETT_RUNTIME_ABI_VERSION_V1, 1); + assert_eq!(JETT_RT_V1_ABI_VERSION_SYMBOL, 1); + for (status, code) in [ + (JettRuntimeStatusV1::OK, 0), + (JettRuntimeStatusV1::INVALID_ARGUMENT, 1), + (JettRuntimeStatusV1::ABI_VERSION_MISMATCH, 2), + (JettRuntimeStatusV1::INVALID_CONTEXT, 3), + (JettRuntimeStatusV1::INVALID_UTF8, 4), + (JettRuntimeStatusV1::IO_FAILURE, 5), + (JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE, 6), + (JettRuntimeStatusV1::RESOURCE_EXHAUSTED, 7), + (JettRuntimeStatusV1::PANIC, 255), + ] { + assert_eq!(status.code(), code); + } + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 8); + assert_eq!(offset_of!(JettRuntimeContextV1, token), 0); + assert_eq!(offset_of!(JettRuntimeContextV1, abi_version), 8); + assert_eq!(offset_of!(JettRuntimeContextV1, state_tag), 12); + } + + #[test] + fn version_check_reports_a_deterministic_mismatch() { + assert_eq!(check_abi(1).0, JettRuntimeStatusV1::OK); + let (status, result) = check_abi(2); + assert_eq!(status, JettRuntimeStatusV1::ABI_VERSION_MISMATCH); + assert_eq!(message(result), "runtime ABI version mismatch"); + } + + #[test] + fn context_lifecycle_keeps_record_immutable() { + let mut context = JettRuntimeContextV1::retired(); + assert_eq!(create_context(1, &mut context).0, JettRuntimeStatusV1::OK); + let published = (context.token, context.abi_version, context.state_tag); + assert!(context_is_registered(&context)); + assert_eq!( + write_stdout(&context, &JettRuntimeStringSliceV1::empty()).0, + JettRuntimeStatusV1::OK + ); + assert_eq!(destroy_context(&mut context).0, JettRuntimeStatusV1::OK); + assert_eq!( + (context.token, context.abi_version, context.state_tag), + published + ); + assert_eq!( + destroy_context(&mut context).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + } + + #[test] + fn copied_moved_and_forged_records_cannot_reach_state() { + let mut original = JettRuntimeContextV1::retired(); + assert_eq!(create_context(1, &mut original).0, JettRuntimeStatusV1::OK); + let mut copied = JettRuntimeContextV1 { + token: original.token, + abi_version: original.abi_version, + state_tag: original.state_tag, + }; + let empty = JettRuntimeStringSliceV1::empty(); + assert_eq!( + write_stdout(&copied, &empty).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + assert_eq!( + destroy_context(&mut copied).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + assert!(context_is_registered(&original)); + + let token = original.token; + original.token = token.wrapping_add(1).max(1); + assert_eq!( + write_stdout(&original, &empty).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + original.token = token; + assert_eq!(destroy_context(&mut original).0, JettRuntimeStatusV1::OK); + } + + #[test] + fn stale_token_fails_after_recreating_at_same_address() { + let mut context = JettRuntimeContextV1::retired(); + assert_eq!(create_context(1, &mut context).0, JettRuntimeStatusV1::OK); + let stale_token = context.token; + assert_eq!(destroy_context(&mut context).0, JettRuntimeStatusV1::OK); + assert_eq!(create_context(1, &mut context).0, JettRuntimeStatusV1::OK); + let live_token = context.token; + assert!(live_token > stale_token); + context.token = stale_token; + assert_eq!( + write_stdout(&context, &JettRuntimeStringSliceV1::empty()).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + context.token = live_token; + assert_eq!(destroy_context(&mut context).0, JettRuntimeStatusV1::OK); + } + + #[test] + fn tokens_are_unique_and_exhaustion_never_wraps() { + let mut first = JettRuntimeContextV1::retired(); + let mut second = JettRuntimeContextV1::retired(); + assert_eq!(create_context(1, &mut first).0, JettRuntimeStatusV1::OK); + assert_eq!(create_context(1, &mut second).0, JettRuntimeStatusV1::OK); + assert!(second.token > first.token); + assert_eq!(destroy_context(&mut first).0, JettRuntimeStatusV1::OK); + assert_eq!(destroy_context(&mut second).0, JettRuntimeStatusV1::OK); + + let mut registry = NativeContextRegistry { + contexts: HashMap::new(), + next_token: NonZeroU64::new(u64::MAX), + }; + assert_eq!(registry.take_token(), Some(u64::MAX)); + assert_eq!(registry.take_token(), None); + assert_eq!(registry.take_token(), None); + } + + #[test] + fn destroy_retires_then_waits_for_in_flight_lease() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + assert_eq!(create_context(1, &mut *context).0, JettRuntimeStatusV1::OK); + let cleanup_count = Arc::new(AtomicUsize::new(0)); + install_cleanup_probe(&*context, Arc::clone(&cleanup_count)); + let key = context_key(&*context).unwrap(); + let lease = acquire_context(key).unwrap(); + let address = (&*context as *const JettRuntimeContextV1).addr(); + let (sender, receiver) = mpsc::channel(); + let destroyer = thread::spawn(move || { + sender + .send(destroy_context(address as *mut JettRuntimeContextV1).0) + .unwrap(); + }); + + let deadline = Instant::now() + Duration::from_secs(2); + while context_is_registered(&*context) { + assert!(Instant::now() < deadline, "destroy did not retire context"); + thread::yield_now(); + } + assert_eq!( + acquire_context(key).err(), + Some(AcquireContextError::Invalid) + ); + assert!(receiver.recv_timeout(Duration::from_millis(25)).is_err()); + assert_eq!(cleanup_count.load(Ordering::SeqCst), 0); + drop(lease); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + JettRuntimeStatusV1::OK + ); + destroyer.join().unwrap(); + assert_eq!(cleanup_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn concurrent_double_destroy_cleans_once() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + assert_eq!(create_context(1, &mut *context).0, JettRuntimeStatusV1::OK); + let cleanup_count = Arc::new(AtomicUsize::new(0)); + install_cleanup_probe(&*context, Arc::clone(&cleanup_count)); + let address = (&*context as *const JettRuntimeContextV1).addr(); + let barrier = Arc::new(Barrier::new(3)); + let mut workers = Vec::new(); + for _ in 0..2 { + let barrier = Arc::clone(&barrier); + workers.push(thread::spawn(move || { + barrier.wait(); + destroy_context(address as *mut JettRuntimeContextV1) + .0 + .code() + })); + } + barrier.wait(); + let mut statuses = workers + .into_iter() + .map(|worker| worker.join().unwrap()) + .collect::>(); + statuses.sort_unstable(); + assert_eq!(statuses, [0, 3]); + assert_eq!(cleanup_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn cleanup_panic_is_reported_once_after_retirement() { + let mut context = JettRuntimeContextV1::retired(); + assert_eq!(create_context(1, &mut context).0, JettRuntimeStatusV1::OK); + let lease = acquire_context(context_key(&context).unwrap()).unwrap(); + let count = Arc::new(AtomicUsize::new(0)); + let finalizer_count = Arc::clone(&count); + lock_unpoisoned(&lease.entry.state) + .as_mut() + .unwrap() + .resources + .insert( + ResourceTypeId::new(1), + (), + AuthorityProvenance::new(1, 1), + move |_| { + finalizer_count.fetch_add(1, Ordering::SeqCst); + panic!("cleanup failure"); + }, + ) + .unwrap(); + drop(lease); + assert_eq!(destroy_context(&mut context).0, JettRuntimeStatusV1::PANIC); + assert_eq!(count.load(Ordering::SeqCst), 1); + assert_eq!( + destroy_context(&mut context).0, + JettRuntimeStatusV1::INVALID_CONTEXT + ); + assert_eq!(count.load(Ordering::SeqCst), 1); + } + + #[test] + fn invalid_inputs_and_lengths_have_stable_failures() { + assert_eq!( + create_context(1, ptr::null_mut()).0, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let mut context = JettRuntimeContextV1::retired(); + assert_eq!( + create_context(9, &mut context).0, + JettRuntimeStatusV1::ABI_VERSION_MISMATCH + ); + assert!(!context.has_live_shape()); + assert_eq!( + destroy_context(ptr::null_mut()).0, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + unsafe { jett_rt_v1_check_abi(1, ptr::null_mut()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + + assert_eq!(create_context(1, &mut context).0, JettRuntimeStatusV1::OK); + let null_data = JettRuntimeStringSliceV1 { + data: ptr::null(), + byte_length: 1, + }; + assert_eq!( + write_stdout(&context, &null_data).0, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let too_long = JettRuntimeStringSliceV1 { + data: NonNull::::dangling().as_ptr(), + byte_length: (isize::MAX as u64) + 1, + }; + let (status, result) = write_stdout(&context, &too_long); + assert_eq!(status, JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE); + assert_eq!( + message(result), + "string byte length exceeds host address space" + ); + let invalid_utf8_bytes = [0xff]; + let invalid_utf8 = JettRuntimeStringSliceV1 { + data: invalid_utf8_bytes.as_ptr(), + byte_length: 1, + }; + assert_eq!( + write_stdout(&context, &invalid_utf8).0, + JettRuntimeStatusV1::INVALID_UTF8 + ); + assert_eq!(destroy_context(&mut context).0, JettRuntimeStatusV1::OK); + } + + #[test] + fn stdout_writer_preserves_exact_bytes_without_newline() { + let mut output = Vec::new(); + write_all_bytes(&mut output, b"hello, world").unwrap(); + assert_eq!(output, b"hello, world"); + } + + struct PanicOnDrop(Arc); + + impl Drop for PanicOnDrop { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + panic!("panic payload must not be dropped"); + } + } + + #[test] + fn recoverable_panic_is_contained_without_dropping_payload() { + let drops = Arc::new(AtomicUsize::new(0)); + let panic_drops = Arc::clone(&drops); + let mut result = MaybeUninit::uninit(); + let status = complete_call(result.as_mut_ptr(), || panic_any(PanicOnDrop(panic_drops))); + let result = unsafe { result.assume_init() }; + assert_eq!(status, JettRuntimeStatusV1::PANIC); + assert_eq!(message(result), "runtime operation panicked"); + assert_eq!(drops.load(Ordering::SeqCst), 0); + } +} diff --git a/crates/jett_runtime/src/native_abi/resource.rs b/crates/jett_runtime/src/native_abi/resource.rs new file mode 100644 index 00000000..e0262011 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource.rs @@ -0,0 +1,1278 @@ +//! Private typed native Resource protocol. No Source or C operation entry is minted here. + +use super::*; +use crate::resource_custody::{ + BorrowedResourceToken, CustodyError, JettResourceCallResultV1, NativeFrameRole, + NativeLayoutInstallation, NativeLoanSource, NativeOperation, NativeParent, NativePayloadStep, + NativeRecipe, NativeShape, NativeSourceInvocation, NativeSourceResult, NativeSourceValue, + NativeSumLoanSource, OwnedResourceToken, PreparedResourceAcquisition, RESOURCE_DOMAIN_ERROR, + RESOURCE_DOMAIN_OK, RegisteredNativeLayout, ResourceCleanupFailure, ResourceCleanupOutcome, + ResourceCustody, ResourceFrameKind, ResourceFrameToken, ResourceLayoutError, ResourcePurpose, + ResourceTransitionFailure, +}; +use crate::{AuthorityProvenance, RegistryError}; + +mod borrowed_sums; +mod carriers; +mod carriers_sum; +mod graph_descriptors; +mod leaves; +mod operations; +#[cfg(test)] +pub(super) mod ordinary_reservation_faults; +mod program_entry; +mod provider; +mod sum_replacement; +pub use program_entry::{ + jett_rt_v1_resource_program_failure, jett_rt_v1_resource_program_failure_copy, + jett_rt_v1_resource_program_unwind, +}; +mod source; +#[cfg(test)] +use ordinary_reservation_faults::{ReservationFaults, ReservationSite}; +#[cfg(all(test, jett_resource_native_test_archive))] +mod test_archive_api; +#[cfg(all(test, jett_resource_native_test_archive))] +mod test_archive_main; +#[cfg(all(test, jett_resource_native_test_archive))] +pub(super) mod test_observations; +pub(super) use graph_descriptors::CheckedGraphHookProof; +#[cfg(test)] +pub(super) use provider::{DecodedScript, NativeTestEvent}; +use provider::{InstalledProvider, NativeProviderIdentity, NativeRestrictionIdentity}; + +const MAX_ATTEMPTS: usize = 64; +const REFUSED: &[u8] = b"native Resource protocol refused"; +const CLEANUP_FAILED: &[u8] = b"native Resource cleanup failed"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum NativeResourceError { + Context, + AlreadyInstalled, + MissingInstallation, + InvalidEntry, + InvalidHandle, + WrongFamily, + WrongFrame, + WrongOperation, + WrongGrant, + WrongPurpose, + ActiveAttempt, + BodyFailed, + SourceBodyStatus(JettRuntimeStatusV1), + UnsupportedSourceBoundary, + UnsupportedCarrierCustody, + UnsupportedCarrierObservation, + UnprovedCarrierRefinement, + Capacity, + Layout(ResourceLayoutError), + Custody(CustodyError), + Registry(RegistryError), + Cleanup(ResourceCleanupFailure), + Ordinary(JettRuntimeStatusV1), + OrdinaryStorage(JettRuntimeStatusV1), +} +type ResourceResult = Result; +impl From for NativeResourceError { + fn from(error: ResourceLayoutError) -> Self { + Self::Layout(error) + } +} +impl From for NativeResourceError { + fn from(error: CustodyError) -> Self { + Self::Custody(error) + } +} +impl From for NativeResourceError { + fn from(error: RegistryError) -> Self { + Self::Registry(error) + } +} +impl NativeResourceError { + pub(super) fn status(self) -> JettRuntimeStatusV1 { + match self { + Self::Capacity => JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + Self::Context => JettRuntimeStatusV1::INVALID_CONTEXT, + Self::Ordinary(status) + | Self::OrdinaryStorage(status) + | Self::SourceBodyStatus(status) => status, + Self::Cleanup(ResourceCleanupFailure::FinalizerPanic) => JettRuntimeStatusV1::PANIC, + _ => JettRuntimeStatusV1::INVALID_ARGUMENT, + } + } +} +fn ordinary_error(error: (JettRuntimeStatusV1, &'static [u8])) -> NativeResourceError { + NativeResourceError::OrdinaryStorage(error.0) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct ResourceHandleId(NonZeroU64); +impl ResourceHandleId { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::InvalidHandle) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} +fn next_handle(ids: &mut values::ReservedNativeIdentities) -> ResourceResult { + ResourceHandleId::new(ids.take().map_err(ordinary_error)?) +} + +/// This key and state borrow always come from the same stationary context lease. +pub(super) struct AuthenticatedResourceContext { + key: ContextRegistryKey, + lease: NativeContextLease, +} +impl AuthenticatedResourceContext { + pub(super) fn acquire(context: *const JettRuntimeContextV1) -> ResourceResult { + let key = context_key(context).map_err(|_| NativeResourceError::Context)?; + let lease = acquire_context(key).map_err(|_| NativeResourceError::Context)?; + Ok(Self { key, lease }) + } + pub(super) fn with_state( + &self, + operation: impl FnOnce(ContextRegistryKey, &mut NativeContextState) -> ResourceResult, + ) -> ResourceResult { + let mut state = lock_unpoisoned(&self.lease.entry.state); + let state = state.as_mut().ok_or(NativeResourceError::Context)?; + if state + .resource_state + .as_ref() + .is_some_and(|resource| resource.context != self.key) + { + return Err(NativeResourceError::Context); + } + operation(self.key, state) + } + pub(super) fn with_resource( + &self, + operation: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult, + ) -> ResourceResult { + self.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + match catch_unwind(AssertUnwindSafe(|| { + operation(resource, &mut state.values, &mut state.resources) + })) { + Ok(Ok(value)) => Ok(value), + Ok(Err(error)) => { + resource.record_failure(error); + Err(error) + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut resource.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + Err(NativeResourceError::BodyFailed) + } + } + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeEntry { + pub(super) function: u32, + pub(super) signature: u32, + pub(super) scope: u32, +} + +struct NativeFrameEntry { + template: u32, + attempt: ResourceHandleId, + parent: Option, + token: ResourceFrameToken, + incoming: Vec>, +} +struct NativeResidentLoan { + sum: bool, + parent_loan: ResourceHandleId, + caller_frame: ResourceHandleId, + invoke_operation: u32, + callee_parameter: u32, +} +struct NativeOwnerEntry { + frame: ResourceHandleId, + slot: u32, + token: OwnedResourceToken, +} +struct NativeLoanEntry { + frame: ResourceHandleId, + borrow_operation: u32, + source_slot: u32, + source_frame: ResourceHandleId, + token: BorrowedResourceToken, + parent_sum: Option, +} +struct NativeSumLoan { + frame: ResourceHandleId, + borrow_operation: u32, + shell: ResourceHandleId, + source_frame: ResourceHandleId, + source_slot: u32, + shape: u32, + token: Option, +} +struct NativePreparedCall { + operation: u32, + target_operation: u32, + hook: u32, + frame: ResourceHandleId, + descriptor: Option, +} +struct NativePreparedEntry { + destination_frame: ResourceHandleId, + call: ResourceHandleId, + frame: ResourceHandleId, + destination: u32, + kind: u32, + network: ResourceHandleId, + token: PreparedResourceAcquisition, + publication_ids: values::ReservedNativeIdentities, +} +struct NativeNetworkGrant { + context: ContextRegistryKey, + layout: Arc, + provider: NativeProviderIdentity, + restriction: NativeRestrictionIdentity, + ordinary_network: u64, + provenance: AuthorityProvenance, +} +enum NativeResourceEntry { + Program { layout: Arc }, + Frame(NativeFrameEntry), + Owner(NativeOwnerEntry), + Loan(NativeLoanEntry), + SumLoan(NativeSumLoan), + Prepared(NativePreparedEntry), + Call(NativePreparedCall), + Descriptor { hook: u32, signature: u32 }, + NetworkGrant(NativeNetworkGrant), + Sum(NativeResourceSum), + SourceCall(source::NativeSourceCall), + CarrierRoot(carriers::NativeCarrierRoot), + CarrierLoan(carriers::NativeCarrierLoan), + CarrierBuilder(carriers::NativeCarrierBuilder), +} +struct NativeResourceSum { + frame: ResourceHandleId, + slot: u32, + shape: u32, + payload: NativeSumPayload, +} +enum NativeSumPayload { + None, + Some(OwnedResourceToken), + Ok(OwnedResourceToken), + Fail { shape: u32, string: u64 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AttemptPhase { + Running, + Completing, +} +/// Solely issued by the exact installed Resource entry constructor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ResourceOrdinaryOwner { + context: ContextRegistryKey, + program: ResourceHandleId, + entry: NativeEntry, + root: ResourceHandleId, + attempt: ResourceHandleId, + purpose: ResourcePurpose, + ordinal: usize, +} +impl ResourceOrdinaryOwner { + #[cfg(all(test, jett_resource_native_test_archive))] + pub(super) fn observation_identity(self) -> Option { + Some(test_observations::AttemptIdentity { + id: self.attempt.raw(), + ordinal: u32::try_from(self.ordinal).ok()?, + }) + } + pub(super) fn ordinal(self) -> usize { + self.ordinal + } + pub(super) fn within_attempt_budget(self) -> bool { + self.ordinal < MAX_ATTEMPTS + } + pub(super) fn continues_installation(self, previous: Self) -> bool { + self.context == previous.context + && self.program == previous.program + && self.entry == previous.entry + && self.attempt.raw() > previous.attempt.raw() + && self.root != previous.root + && previous.ordinal.checked_add(1) == Some(self.ordinal) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ResourceOrdinaryCompletion { + owner: ResourceOrdinaryOwner, + completion: NativeResourceCompletion, + diagnostic_ready: bool, +} +impl ResourceOrdinaryCompletion { + pub(super) fn owner(self) -> ResourceOrdinaryOwner { + self.owner + } + pub(super) fn matches_owner(self) -> bool { + self.completion.attempt == self.owner.attempt.raw() + && self.completion.reserved == 0 + && self.completion.selected_kind <= 3 + } + pub(super) fn diagnostic_ready(self) -> bool { + self.diagnostic_ready && self.matches_owner() + } +} +struct NativeAttempt { + id: ResourceHandleId, + ordinary_owner: ResourceOrdinaryOwner, + entry: NativeEntry, + purpose: ResourcePurpose, + root_frame: ResourceHandleId, + root_body_status: Option, + body: Option, + cleanup: Option, + phase: AttemptPhase, +} +enum NativeBodyFailure { + Resource(NativeResourceError), + HostPanic, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeResourceCompletion { + pub(super) attempt: u64, + pub(super) body_status: u32, + pub(super) cleanup_status: u32, + pub(super) selected_kind: u32, + pub(super) reserved: u32, +} +struct NativeCompletedAttempt { + completion: NativeResourceCompletion, + terminal_status: JettRuntimeStatusV1, + ordinary: ResourceOrdinaryCompletion, + resource_message: Vec, + ordinary_message: Vec, + report_error: Option, +} + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeResourceCounts { + pub(super) owners: u64, + pub(super) loans: u64, + pub(super) frames: u64, + pub(super) registry: u64, + pub(super) owner_handles: u64, + pub(super) loan_handles: u64, + pub(super) frame_handles: u64, + pub(super) provisional: u64, + pub(super) ordinary_empty: u32, + pub(super) reserved: u32, +} + +pub(super) struct NativeResourceState { + context: ContextRegistryKey, + installation: NativeLayoutInstallation, + layout: Arc, + installed_wire: Vec, + entry_owner: program_entry::EntryOwner, + program_handle: ResourceHandleId, + custody: ResourceCustody, + handles: HashMap, + carrier_adapters: HashMap, + active_frames: Vec, + attempt: Option, + completed: Vec, + #[cfg(test)] + reservation_faults: ReservationFaults, + provider: InstalledProvider, + entry: NativeEntry, + network: Option, +} + +impl NativeResourceState { + fn validate_installation(&self) -> ResourceResult<()> { + if !self + .installation + .installed() + .is_some_and(|layout| Arc::ptr_eq(layout, &self.layout)) + { + return Err(NativeResourceError::MissingInstallation); + } + match self.handles.get(&self.program_handle) { + Some(NativeResourceEntry::Program { layout }) if Arc::ptr_eq(layout, &self.layout) => { + Ok(()) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + fn reserve(&mut self, count: usize) -> ResourceResult { + self.handles + .try_reserve(count) + .map_err(|_| NativeResourceError::Capacity)?; + values::reserve_native_identities( + u64::try_from(count).map_err(|_| NativeResourceError::Capacity)?, + ) + .map_err(ordinary_error) + } + fn running(&self, ordinary: &values::NativeValues) -> ResourceResult<&NativeAttempt> { + self.validate_installation()?; + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + ordinary + .validate_resource_ordinary_attempt(attempt.ordinary_owner) + .map_err(ordinary_error)?; + if attempt.phase != AttemptPhase::Running + || attempt.body.is_some() + || attempt.cleanup.is_some() + { + return Err(NativeResourceError::BodyFailed); + } + if let Some(failure) = ordinary.resource_failure() { + return Err(NativeResourceError::Ordinary(failure.status)); + } + if ordinary.cleanup_failed { + return Err(NativeResourceError::BodyFailed); + } + Ok(attempt) + } + fn frame(&self, handle: ResourceHandleId) -> ResourceResult<&NativeFrameEntry> { + let NativeResourceEntry::Frame(frame) = self + .handles + .get(&handle) + .ok_or(NativeResourceError::InvalidHandle)? + else { + return Err(NativeResourceError::WrongFamily); + }; + if self.attempt.as_ref().map(|attempt| attempt.id) != Some(frame.attempt) + || !self.active_frames.contains(&handle) + { + return Err(NativeResourceError::WrongFrame); + } + Ok(frame) + } + fn operation(&self, ordinal: u32) -> ResourceResult<&NativeOperation> { + self.layout + .operations() + .get(ordinal as usize) + .map(|record| record.operation()) + .ok_or(NativeResourceError::WrongOperation) + } + fn operation_frame(&self, ordinal: u32, frame: ResourceHandleId) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + let template = self.frame(frame)?.template; + let operation = self.operation(ordinal)?; + let actual = match operation { + NativeOperation::Acquire { frame, .. } + | NativeOperation::Transfer { frame, .. } + | NativeOperation::Borrow { frame, .. } + | NativeOperation::BoundedBorrowUse { frame, .. } + | NativeOperation::EndBorrow { frame, .. } + | NativeOperation::InvokeBorrow { frame, .. } + | NativeOperation::Close { frame, .. } + | NativeOperation::Drop { frame, .. } + | NativeOperation::SumAdopt { frame, .. } + | NativeOperation::SumTake { frame, .. } + | NativeOperation::SumDrop { frame, .. } + | NativeOperation::Replace { frame, .. } + | NativeOperation::Complete { frame, .. } + | NativeOperation::InvokeSourceFunction { frame, .. } + | NativeOperation::TakeFailureCompanion { frame, .. } + | NativeOperation::PublishReturn { frame, .. } + | NativeOperation::CreateAbsentSum { frame, .. } + | NativeOperation::CreateFailureSum { frame, .. } + | NativeOperation::BorrowSum { frame, .. } + | NativeOperation::ObserveSumView { frame, .. } + | NativeOperation::ProjectSumView { frame, .. } + | NativeOperation::ReadFailureCompanion { frame, .. } + | NativeOperation::EndSumBorrow { frame, .. } => *frame, + NativeOperation::Descriptor { .. } | NativeOperation::InvokeDescriptor { .. } => { + return Err(NativeResourceError::WrongOperation); + } + NativeOperation::Carrier { record } => self + .layout + .carriers() + .operations + .get(*record as usize) + .ok_or(NativeResourceError::WrongOperation)? + .operation + .frame(), + }; + if template != actual { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + fn cleanup(&mut self, outcome: ResourceCleanupOutcome) -> ResourceResult<()> { + if let Some(error) = outcome.failure() { + if let Some(attempt) = &mut self.attempt { + attempt.cleanup.get_or_insert(error); + } + Err(NativeResourceError::Cleanup(error)) + } else { + Ok(()) + } + } + fn record_failure(&mut self, error: NativeResourceError) { + if let Some(attempt) = &mut self.attempt { + match error { + NativeResourceError::Ordinary(_) => {} // current ordinary storage remains authoritative + NativeResourceError::Cleanup(error) => { + attempt.cleanup.get_or_insert(error); + } + other => { + attempt + .body + .get_or_insert(NativeBodyFailure::Resource(other)); + } + } + } + } + + pub(super) fn begin_entry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, + entry: NativeEntry, + purpose: ResourcePurpose, + ) -> ResourceResult { + self.validate_installation()?; + if entry != self.entry || self.attempt.is_some() { + return Err(NativeResourceError::ActiveAttempt); + } + #[cfg(test)] + if self.completed.len() >= self.provider.attempts() { + return Err(NativeResourceError::InvalidEntry); + } + if self.completed.len() >= MAX_ATTEMPTS + || self.custody.live_owners() != 0 + || self.custody.live_loans() != 0 + || self.custody.active_frames() != 0 + || registry.live_count() != 0 + || !self.active_frames.is_empty() + || !self.only_installation_handles() + { + return Err(NativeResourceError::InvalidEntry); + } + if let Some((status, _)) = ordinary.session_failure() { + return Err(NativeResourceError::Ordinary(status)); + } + if ordinary.cleanup_failed || !ordinary.is_empty() { + return Err(NativeResourceError::BodyFailed); + } + let mut ids = self.reserve(2)?; + #[cfg(test)] + self.reservation_faults.checkpoint( + ReservationSite::BeginActiveFrames, + self.active_frames.len() == self.active_frames.capacity(), + )?; + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + #[cfg(test)] + self.reservation_faults.checkpoint( + ReservationSite::BeginHistory, + self.completed.len() == self.completed.capacity(), + )?; + self.completed + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let id = next_handle(&mut ids)?; + let root_frame = next_handle(&mut ids)?; + let ordinary_owner = ResourceOrdinaryOwner { + context: self.context, + program: self.program_handle, + entry, + root: root_frame, + attempt: id, + purpose, + ordinal: self.completed.len(), + }; + let predecessor = self.completed.last().map(|completed| completed.ordinary); + let prepared_ordinary = ordinary + .prepare_resource_ordinary_attempt(ordinary_owner, predecessor) + .map_err(ordinary_error)?; + let token = self + .custody + .begin_frame(ResourceFrameKind::Scope, purpose)?; + self.handles.insert( + root_frame, + NativeResourceEntry::Frame(NativeFrameEntry { + template: entry.scope, + attempt: id, + parent: None, + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(root_frame); + self.attempt = Some(NativeAttempt { + id, + ordinary_owner, + entry, + purpose, + root_frame, + root_body_status: None, + body: None, + cleanup: None, + phase: AttemptPhase::Running, + }); + prepared_ordinary.publish(); + Ok(id) + } + fn only_installation_handles(&self) -> bool { + self.carrier_adapters.is_empty() + && self.custody.live_carrier_roots() == 0 + && self.handles.values().all(|entry| { + matches!( + entry, + NativeResourceEntry::Program { .. } + | NativeResourceEntry::NetworkGrant(_) + | NativeResourceEntry::Descriptor { .. } + ) + }) + } + pub(super) fn entry_network( + &self, + attempt: ResourceHandleId, + parameter: u32, + ) -> ResourceResult { + self.validate_installation()?; + self.runtime_purpose()?; + let running = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if running.id != attempt || running.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + let formal = self.layout.signatures()[self.entry.signature as usize] + .parameters() + .get(parameter as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + if !matches!( + self.layout.shapes().get(formal.shape() as usize), + Some(NativeShape::Network) + ) { + return Err(NativeResourceError::InvalidEntry); + } + match self + .handles + .get(&self.network.ok_or(NativeResourceError::WrongGrant)?) + { + Some(NativeResourceEntry::NetworkGrant(grant)) => Ok(grant.ordinary_network), + _ => Err(NativeResourceError::WrongGrant), + } + } + /// Observe only the sole retired root Scope of this exact Runtime attempt. + /// Body/ordinary/cleanup failures are independent and must not gate this read. + pub(super) fn entry_outcome( + &self, + scope: ResourceHandleId, + function: u32, + signature: u32, + template: u32, + ) -> ResourceResult { + self.validate_installation()?; + self.runtime_purpose()?; + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let selected = NativeEntry { + function, + signature, + scope: template, + }; + if attempt.phase != AttemptPhase::Running + || attempt.entry != self.entry + || selected != self.entry + || attempt.root_frame != scope + { + return Err(NativeResourceError::InvalidEntry); + } + let row = self + .layout + .frames() + .get(template as usize) + .ok_or(NativeResourceError::WrongFrame)?; + if row.role() != NativeFrameRole::Scope + || row.site().function() != function + || row.signature() != signature + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + { + return Err(NativeResourceError::WrongFrame); + } + attempt + .root_body_status + .ok_or(NativeResourceError::WrongFrame) + } + + pub(super) fn complete_entry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt_id: ResourceHandleId, + body_status: u32, + host_panic: bool, + ) -> ResourceResult { + let attempt = self + .attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)?; + if attempt.id != attempt_id || attempt.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + ordinary + .validate_resource_ordinary_attempt(attempt.ordinary_owner) + .map_err(ordinary_error)?; + attempt.phase = AttemptPhase::Completing; + if host_panic { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + if self.layout.wire_version() == 3 { + // Typed carrier companions and parent leases retire while their frames are live. + while let Some(frame) = self.active_frames.last().copied() { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + match error { + NativeResourceError::Cleanup(error) => { + self.attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)? + .cleanup + .get_or_insert(error); + } + _ => { + ordinary.cleanup_failed = true; + } + } + if self.active_frames.last() == Some(&frame) { + // Refused retirement retains the exact obligations; no success receipt. + return Err(error); + } + } + } + if !self.only_installation_handles() { + // Frame retirement must discharge every holder and staging receipt. + // Keep any skipped variant observable instead of erasing its obligation. + ordinary.cleanup_failed = true; + return Err(NativeResourceError::BodyFailed); + } + } + // Unwind the real core before retiring token shells or copying diagnostics. + let cleanup = self.custody.unwind_all(registry); + if let Some(error) = cleanup.failure() { + self.attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)? + .cleanup + .get_or_insert(error); + } + let mut companion_failed = false; + self.handles.retain(|_, entry| { + if let NativeResourceEntry::Sum(sum) = entry { + if let NativeSumPayload::Fail { shape, string } = &sum.payload { + companion_failed |= ordinary + .drop_resource_typed_companion(&self.layout, *shape, *string) + .is_err(); + } + } + matches!( + entry, + NativeResourceEntry::Program { .. } + | NativeResourceEntry::NetworkGrant(_) + | NativeResourceEntry::Descriptor { .. } + ) + }); + self.active_frames.clear(); + self.carrier_adapters.clear(); + if companion_failed { + ordinary.cleanup_failed = true; + } + // Begin requires empty ordinary storage and seals its exact attempt owner. + // Actor state created by this entry therefore retires with that attempt, + // after Resource companions release their borrowed actor references. + // Context destruction may call this cleanup again, but the actor table + // and unfinished import tickets have already been drained exactly once. + ordinary + .retire_resource_actors( + self.attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)? + .ordinary_owner, + ) + .map_err(ordinary_error)?; + if self.custody.live_owners() != 0 + || self.custody.live_loans() != 0 + || self.custody.active_frames() != 0 + || registry.live_count() != 0 + { + return Err(NativeResourceError::BodyFailed); + } + if !ordinary.is_empty() { + // Resource retirement cannot hide an ordinary frame leak. Keep the + // Completing attempt and its error; neither can authorize reentry. + ordinary.cleanup_failed = true; + return Err(NativeResourceError::BodyFailed); + } + let attempt = self + .attempt + .take() + .ok_or(NativeResourceError::InvalidEntry)?; + let ordinary_failure = ordinary.resource_failure(); + let recorded_body_failure = attempt.body.is_some() || ordinary_failure.is_some(); + let cleanup_status = attempt + .cleanup + .map(|error| NativeResourceError::Cleanup(error).status().code()) + .unwrap_or(if ordinary.cleanup_failed { + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + } else { + 0 + }); + let selected_kind = if cleanup_status != 0 { + 3 + } else if host_panic || matches!(attempt.body, Some(NativeBodyFailure::HostPanic)) { + 2 + } else if body_status != 0 || recorded_body_failure { + 1 + } else { + 0 + }; + let completion = NativeResourceCompletion { + attempt: attempt_id.raw(), + body_status, + cleanup_status, + selected_kind, + reserved: 0, + }; + // Retain the original body field above. A refused leaf or an ordinary + // first error can also fail a body whose generated status was zero. + let terminal_status = if cleanup_status != 0 { + JettRuntimeStatusV1(cleanup_status) + } else if selected_kind == 2 { + JettRuntimeStatusV1::PANIC + } else if body_status != 0 { + JettRuntimeStatusV1(body_status) + } else if let Some(failure) = &attempt.body { + match failure { + NativeBodyFailure::Resource(error) => error.status(), + NativeBodyFailure::HostPanic => JettRuntimeStatusV1::PANIC, + } + } else if let Some(failure) = &ordinary_failure { + failure.status + } else { + JettRuntimeStatusV1::OK + }; + let resource_message = if attempt.cleanup.is_some() { + Some(CLEANUP_FAILED) + } else if ordinary.cleanup_failed { + Some(b"native value cleanup failed".as_slice()) + } else { + match attempt.body { + Some(NativeBodyFailure::HostPanic) => Some(PANIC_MESSAGE), + Some(NativeBodyFailure::Resource(_)) => Some(REFUSED), + None => None, + } + }; + let diagnostic: ResourceResult<(Vec, Vec)> = (|| { + #[cfg(not(test))] + let resource_message = copy_diagnostic(None, resource_message.unwrap_or_default())?; + #[cfg(test)] + let resource_message = copy_diagnostic( + None, + resource_message.unwrap_or_default(), + &mut self.reservation_faults, + ReservationSite::ResourceDiagnostic, + )?; + let ordinary_message = if let Some(failure) = ordinary_failure { + #[cfg(not(test))] + { + copy_diagnostic(failure.prefix, failure.message)? + } + #[cfg(test)] + { + copy_diagnostic( + failure.prefix, + failure.message, + &mut self.reservation_faults, + ReservationSite::OrdinaryDiagnostic, + )? + } + } else { + Vec::new() + }; + Ok((resource_message, ordinary_message)) + })(); + let (resource_message, ordinary_message, report_error) = match diagnostic { + Ok((resource, ordinary)) => (resource, ordinary, None), + Err(error) => (Vec::new(), Vec::new(), Some(error)), + }; + let ordinary_completion = ResourceOrdinaryCompletion { + owner: attempt.ordinary_owner, + completion, + diagnostic_ready: report_error.is_none(), + }; + ordinary + .seal_resource_ordinary_attempt(ordinary_completion) + .map_err(ordinary_error)?; + // A report failure retains a completed tombstone; cleanup and retirement are not retried. + self.completed.push(NativeCompletedAttempt { + completion, + terminal_status, + ordinary: ordinary_completion, + resource_message, + ordinary_message, + report_error, + }); + if let Some(error) = report_error { + return Err(error); + } + Ok(completion) + } + pub(super) fn completed_attempt( + &self, + attempt: ResourceHandleId, + ) -> ResourceResult { + let completed = self + .completed + .iter() + .find(|entry| entry.completion.attempt == attempt.raw()) + .ok_or(NativeResourceError::InvalidEntry)?; + if let Some(error) = completed.report_error { + return Err(error); + } + Ok(completed.completion) + } + pub(super) fn completed_messages( + &self, + attempt: ResourceHandleId, + ) -> ResourceResult<(&[u8], &[u8])> { + let completed = self + .completed + .iter() + .find(|entry| entry.completion.attempt == attempt.raw()) + .ok_or(NativeResourceError::InvalidEntry)?; + if let Some(error) = completed.report_error { + return Err(error); + } + Ok((&completed.resource_message, &completed.ordinary_message)) + } + #[cfg(test)] + pub(super) fn script_remaining(&self) -> usize { + self.provider.remaining() + } + #[cfg(test)] + pub(super) fn observe_events( + &self, + observe: impl FnOnce(&[provider::NativeTestEvent]) -> T, + ) -> T { + self.provider.observe_events(observe) + } + pub(super) fn counts( + &self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + ) -> NativeResourceCounts { + let mut counts = NativeResourceCounts { + owners: self.custody.live_owners() as u64, + loans: self.custody.live_loans() as u64, + frames: self.custody.active_frames() as u64, + registry: registry.live_count() as u64, + ordinary_empty: u32::from(ordinary.is_empty()), + ..NativeResourceCounts::default() + }; + for entry in self.handles.values() { + match entry { + NativeResourceEntry::Owner(_) => counts.owner_handles += 1, + NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(_) | NativeSumPayload::Ok(_), + .. + }) => counts.owner_handles += 1, + NativeResourceEntry::Sum(_) if self.layout.wire_version() == 3 => { + counts.owner_handles += 1; + } + NativeResourceEntry::Loan(_) | NativeResourceEntry::SumLoan(_) => { + counts.loan_handles += 1 + } + NativeResourceEntry::CarrierRoot(_) => counts.owner_handles += 1, + NativeResourceEntry::CarrierLoan(_) => counts.loan_handles += 1, + NativeResourceEntry::CarrierBuilder(_) => counts.provisional += 1, + NativeResourceEntry::SourceCall(_) + | NativeResourceEntry::Call(_) + | NativeResourceEntry::Prepared(_) + if self.layout.wire_version() == 3 => + { + counts.provisional += 1; + } + NativeResourceEntry::Frame(frame) => { + counts.frame_handles += 1; + if self.layout.frames()[frame.template as usize].role() + == NativeFrameRole::Return + { + counts.provisional += 1; + } + } + _ => {} + } + } + if self.layout.wire_version() == 3 { + // A sealed ended adapter still owns its shell until frame retirement. + counts.provisional += self.carrier_adapters.len() as u64; + // A private root has exactly one native envelope. Report any orphan + // as a live obligation without changing the matched-envelope counts. + let envelopes = self + .handles + .values() + .filter(|entry| match entry { + NativeResourceEntry::CarrierRoot(root) => root.custody.is_some(), + NativeResourceEntry::CarrierBuilder(builder) => builder.custody.is_some(), + _ => false, + }) + .count(); + counts.provisional += + self.custody.live_carrier_roots().saturating_sub(envelopes) as u64; + } + counts + } + pub(super) fn safety_unwind( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + ) -> bool { + let mut failed = false; + if self.layout.wire_version() == 3 { + while let Some(frame) = self.active_frames.last().copied() { + if self.retire_frame(ordinary, registry, frame).is_err() { + failed = true; + if self.active_frames.last() == Some(&frame) { + break; + } + } + } + } + let outcome = self.custody.unwind_all(registry); + failed |= outcome.failure().is_some(); + if self.layout.wire_version() == 3 { + // A skipped holder variant cannot turn context backstop cleanup into success. + failed |= !self.only_installation_handles(); + failed |= self.safety_retire_carriers(ordinary).is_err(); + } + for entry in self.handles.values() { + if let NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Fail { string, .. }, + .. + }) = entry + { + failed |= ordinary.drop_resource_ordinary_companion(*string).is_err(); + } + } + self.handles.clear(); + self.carrier_adapters.clear(); + self.active_frames.clear(); + self.attempt = None; + failed + || self.custody.live_owners() != 0 + || self.custody.live_carrier_roots() != 0 + || self.custody.live_loans() != 0 + || registry.live_count() != 0 + } +} + +fn copy_diagnostic( + prefix: Option<&[u8]>, + message: &[u8], + #[cfg(test)] faults: &mut ReservationFaults, + #[cfg(test)] site: ReservationSite, +) -> ResourceResult> { + let prefix = prefix.unwrap_or_default(); + let length = prefix + .len() + .checked_add(message.len()) + .ok_or(NativeResourceError::Capacity)?; + if length > 64 * 1024 { + return Err(NativeResourceError::Capacity); + } + let mut bytes = Vec::new(); + #[cfg(test)] + faults.checkpoint(site, length != 0)?; + bytes + .try_reserve_exact(length) + .map_err(|_| NativeResourceError::Capacity)?; + bytes.extend_from_slice(prefix); + bytes.extend_from_slice(message); + Ok(bytes) +} + +fn validate_entry(layout: &RegisteredNativeLayout, entry: NativeEntry) -> ResourceResult { + let frame = layout + .frames() + .get(entry.scope as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + let signature = layout + .signatures() + .get(entry.signature as usize) + .ok_or(NativeResourceError::InvalidEntry)?; + if frame.role() != NativeFrameRole::Scope + || frame.site().function() != entry.function + || frame.signature() != entry.signature + || frame.parents() != [NativeParent::Root] + || !matches!( + layout.shapes().get(signature.result() as usize), + Some(NativeShape::Nothing) + ) + { + return Err(NativeResourceError::InvalidEntry); + } + let mut network = false; + for formal in signature.parameters() { + match layout.shapes().get(formal.shape() as usize) { + Some(NativeShape::Network) => network = true, + _ => return Err(NativeResourceError::InvalidEntry), + } + } + Ok(network) +} + +fn install_candidate( + key: ContextRegistryKey, + state: &mut NativeContextState, + bytes: &[u8], + entry: NativeEntry, + mut provider: InstalledProvider, +) -> ResourceResult { + if state.resource_state.is_some() { + return Err(NativeResourceError::AlreadyInstalled); + } + if state.values.resource_failure().is_some() + || state.values.cleanup_failed + || !state.values.is_empty() + || state.resources.live_count() != 0 + { + return Err(NativeResourceError::BodyFailed); + } + let mut installation = NativeLayoutInstallation::new(&state.resources); + let layout = installation.install(&state.resources, bytes)?; + let needs_network = validate_entry(&layout, entry)?; + let pair_network = needs_network && provider.enabled(); + // Own the exact authenticated bytes before publishing provider/grant state. + let mut installed_wire = Vec::new(); + installed_wire + .try_reserve_exact(bytes.len()) + .map_err(|_| NativeResourceError::Capacity)?; + installed_wire.extend_from_slice(bytes); + let custody = ResourceCustody::new(layout.program().clone(), &state.resources)?; + let mut handles = HashMap::new(); + handles + .try_reserve(2) + .map_err(|_| NativeResourceError::Capacity)?; + let mut ids = values::reserve_native_identities(5).map_err(ordinary_error)?; + let program_handle = next_handle(&mut ids)?; + let network_handle = if pair_network { + Some(next_handle(&mut ids)?) + } else { + None + }; + let provider_id = NativeProviderIdentity::new(ids.take().map_err(ordinary_error)?)?; + let restriction = NativeRestrictionIdentity::new(ids.take().map_err(ordinary_error)?)?; + provider.bind(provider_id, network_handle)?; + let staged = if pair_network { + Some( + state + .values + .prepare_resource_network(&mut ids) + .map_err(ordinary_error)?, + ) + } else { + None + }; + handles.insert( + program_handle, + NativeResourceEntry::Program { + layout: layout.clone(), + }, + ); + // No fallible publication follows this commit while the authenticated lease owns the mutex. + if let (Some(handle), Some(staged)) = (network_handle, staged) { + let ordinary_network = state.values.commit_resource_network(staged); + handles.insert( + handle, + NativeResourceEntry::NetworkGrant(NativeNetworkGrant { + context: key, + layout: layout.clone(), + provider: provider_id, + restriction, + ordinary_network, + provenance: AuthorityProvenance::new(provider_id.raw(), restriction.raw()), + }), + ); + } + state.resource_state = Some(NativeResourceState { + context: key, + installation, + layout, + installed_wire, + entry_owner: program_entry::EntryOwner::External, + program_handle, + custody, + handles, + carrier_adapters: HashMap::new(), + active_frames: Vec::new(), + attempt: None, + completed: Vec::new(), + #[cfg(test)] + reservation_faults: ReservationFaults::default(), + provider, + entry, + network: network_handle, + }); + Ok(program_handle) +} + +pub(super) fn install_disabled( + authenticated: &AuthenticatedResourceContext, + bytes: &[u8], + entry: NativeEntry, +) -> ResourceResult { + authenticated.with_state(|key, state| { + install_candidate(key, state, bytes, entry, InstalledProvider::Disabled) + }) +} +#[cfg(test)] +pub(super) fn install_scripted( + authenticated: &AuthenticatedResourceContext, + bytes: &[u8], + entry: NativeEntry, + script: DecodedScript, +) -> ResourceResult { + authenticated.with_state(|key, state| { + install_candidate( + key, + state, + bytes, + entry, + InstalledProvider::scripted(script)?, + ) + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs b/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs new file mode 100644 index 00000000..3b1a75b5 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/borrowed_sum_tests.rs @@ -0,0 +1,569 @@ +//! Native protocol controls. Genuine Source acceptance is a separate driver gate. +use super::super::*; +use super::{assert_empty, context, events, script, words}; + +struct Fixture { + bytes: Vec, + result: bool, + create: u32, + source_written: u32, + source_bare: u32, + observe: u32, + project: u32, + read: Option, + end_child: u32, + invoke: u32, + complete_inner: u32, + complete_scope: u32, + complete_root: u32, + take: u32, + drop_written: u32, + drop_bare: u32, + take_failure: Option, + caller_project: u32, + caller_end: u32, + close: u32, +} +fn fixture(result: bool, corrupt: u8) -> Fixture { + let shape = if result { 6 } else { 5 }; + let path = if result { 2 } else { 1 }; + let mut signatures = vec![ + vec![0, 2, 6, 0, 2, 1, 1], + vec![1, 2, 7, 0, 2, 4, 2], + vec![2, 1, 3, 4, 1], + vec![3, 1, 3, 0, 2], + vec![4, 2, 3, 0, 2, shape, 2], + ]; + if corrupt == 1 { + *signatures[4].last_mut().unwrap() = 1; + } + let shapes = vec![ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + ]; + let frames = vec![ + vec![0, 1, 0, 0, 0, 0, 3, 1, 0, 0], + vec![1, 2, 0, 0, 0, 1, 3, 1, 1, 0], + vec![2, 1, 1, 0, 0, 2, 4, 1, 1, 1], + vec![3, 2, 1, 0, 0, 3, 4, 1, 1, 2], + ]; + let slots = vec![ + vec![0, 0, shape, 1, path], + vec![1, 1, shape, 1, path], + vec![2, 1, 6, 1, 2], + vec![3, 1, 4, 0], + ]; + let mut ops: Vec<(u32, Vec)> = Vec::new(); + let mut add = |function, row| { + let id = ops.len() as u32; + ops.push((function, row)); + id + }; + add(0, vec![1, 1, 0, 2]); // 0 factory + add(0, vec![10, 1, 2, 3]); // 1 unwrap owning factory result + add(0, vec![9, 1, 3, 0]); // 2 adopt owner into caller sum + add(0, vec![2, 1, 0, 1]); // 3 whole bare argument holder transfer + let create = add( + 0, + if result { + vec![20, 1, 1, 2] + } else { + vec![19, 1, 1] + }, + ); + add(0, vec![2, 1, 1, 0]); // 5 preserve initial None/Fail in caller Scope + add(0, vec![13, 1]); // 6 complete operation + add(0, vec![21, 1, 0, 1]); // 7 written shell lease + add(0, vec![21, 1, 1, 1]); // 8 bare argument holder lease + add(0, vec![25, 1, 7]); // 9 end written shell lease + add(0, vec![25, 1, 8]); // 10 end bare shell lease + let source_row = |bare| { + let mut row = vec![16, 1, 1, 4, 2, 0, 2, 0, 1, 2]; + row.extend([0, 0, 0, 0, 2, 4, 2, 1]); + row.extend([ + 1, + 1, + shape, + shape, + if bare { 1 } else { 2 }, + if bare { 3 } else { 4 }, + 2, + 4, + 1, + if bare { + if corrupt == 6 { 7 } else { 8 } + } else { + 7 + }, + ]); + row.extend([1, 3]); + row + }; + let source_written = add(0, source_row(false)); + let source_bare = add(0, source_row(true)); + let observe = add(1, vec![22, 2, 2, 2, if corrupt == 2 { 0 } else { 1 }]); + let project = add( + 1, + vec![ + 23, + 2, + 2, + 2, + 1, + if corrupt == 3 { + if path == 1 { 2 } else { 1 } + } else { + path + }, + 2, + ], + ); + let end_child = add(1, vec![5, 2, project]); + let read = result.then(|| add(1, vec![24, 2, 2, 2, 1, if corrupt == 4 { 1 } else { 2 }])); + let invoke = add( + if corrupt == 7 { 0 } else { 1 }, + vec![ + 6, + if corrupt == 7 { 1 } else { 3 }, + 1, + 3, + if corrupt == 5 { 7 } else { project }, + ], + ); + let complete_inner = add(1, vec![13, 3]); + let complete_scope = add(1, vec![13, 2]); + let complete_root = add(0, vec![13, 0]); + let take = add(0, vec![10, 1, 0, 3]); + let drop_written = add(0, vec![11, 1, 0]); + let drop_bare = add(0, vec![11, 1, 1]); + let take_failure = result.then(|| add(0, vec![17, 1, 0, 2])); + let caller_project = add(0, vec![23, 1, 1, 7, path, 1]); + let caller_end = add(0, vec![5, 1, caller_project]); + let close = add(0, vec![7, 1, 2, 3]); + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[2, 0]); + bytes.extend(0u64.to_le_bytes()); + words( + &mut bytes, + &[ + 1, + 3, + signatures.len() as u32, + shapes.len() as u32, + frames.len() as u32, + slots.len() as u32, + ops.len() as u32, + 0, + ], + ); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + for row in signatures + .into_iter() + .chain(shapes) + .chain(frames) + .chain(slots) + { + words(&mut bytes, &row); + } + for (index, (function, row)) in ops.into_iter().enumerate() { + words( + &mut bytes, + &[index as u32, function, 0, 0, 100 + index as u32], + ); + words(&mut bytes, &row); + } + let len = bytes.len() as u64; + bytes[16..24].copy_from_slice(&len.to_le_bytes()); + Fixture { + bytes, + result, + create, + source_written, + source_bare, + observe, + project, + read, + end_child, + invoke, + complete_inner, + complete_scope, + complete_root, + take, + drop_written, + drop_bare, + take_failure, + caller_project, + caller_end, + close, + } +} +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +fn setup( + auth: &AuthenticatedResourceContext, + f: &Fixture, + occupied: bool, + rows: &[(u32, i64, i64, &str)], +) -> ( + ResourceHandleId, + ResourceHandleId, + ResourceHandleId, + ResourceHandleId, + u64, +) { + install_scripted(auth, &f.bytes, entry(), script(rows, 1)).unwrap(); + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + let net = s.entry_network(attempt, 0)?; + let shell = if occupied { + let call = s.prepare_hook(v, 0, op, None)?; + let prepared = s.prepare_factory(v, r, call, net)?; + let value = s.factory(v, r, prepared, net, 801)?; + let owner = s.sum_take(v, r, 1, op, ResourceHandleId::new(value.value)?, op)?; + s.sum_adopt(v, r, 2, op, owner, root)? + } else { + let shell = if f.result { + let mut ids = v + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = v + .publish_resource_error("empty".to_owned(), &mut ids) + .map_err(ordinary_error)?; + s.failure_sum(v, op, f.create, text)? + } else { + s.absent_sum(v, op, f.create)? + }; + s.transfer(v, r, 5, op, shell, root)? + }; + Ok((attempt, root, op, shell, net)) + }) + .unwrap() +} +fn finish( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + root: ResourceHandleId, + op: ResourceHandleId, + f: &Fixture, +) { + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + s.scope_complete(v, r, root, f.complete_root, 0)?; + let outcome = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!( + ( + outcome.body_status, + outcome.cleanup_status, + outcome.selected_kind + ), + (0, 0, 0) + ); + Ok(()) + }) + .unwrap(); + assert_empty(auth); +} + +#[test] +fn native_resource_borrowed_sum_absent_and_failure_views_keep_shell_and_copy_failure() { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let (attempt, root, op, shell, net) = setup(auth, &f, false, &[]); + auth.with_resource(|s, v, r| { + let parent = s.sum_borrow(v, r, 7, op, shell)?; + assert_eq!(s.custody.live_owners(), 0); + assert_eq!(s.custody.live_loans(), 0); + assert_eq!(s.counts(v, r).loan_handles, 1); + assert_eq!( + s.transfer(v, r, 3, op, shell, op), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_drop(v, r, f.drop_written, op, shell), + Err(CustodyError::ActiveBorrow.into()) + ); + let call = s.source_prepare(v, op, f.source_written)?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_parameter(v, scope, 1)?, parent.raw()); + assert_eq!(s.sum_view_tag(v, r, f.observe, scope, parent)?, 0); + assert!(s.sum_view_project(v, r, f.project, scope, parent).is_err()); + if let Some(read) = f.read { + for _ in 0..2 { + let copy = s.sum_failure_read(v, r, read, scope, parent)?; + v.drop_resource_typed_companion(&s.layout, 2, copy) + .map_err(ordinary_error)?; + assert!(s.handles.contains_key(&shell)); + } + } + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + s.sum_borrow_end(r, 9, op, parent)?; + s.carrier(shell, 0, r)?; + if let Some(take) = f.take_failure { + let text = s.failure_companion_take(v, op, take, shell)?; + v.drop_resource_typed_companion(&s.layout, 2, text) + .map_err(ordinary_error)?; + } else { + s.sum_drop(v, r, f.drop_written, op, shell)?; + } + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert!(events(auth).is_empty()); + }); + } +} + +#[test] +fn native_resource_borrowed_sum_occupied_written_and_bare_keep_one_owner_across_hook_outcomes() { + for result in [false, true] { + for bare in [false, true] { + for failed in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let rows = if failed { + vec![(1, 801, 0, ""), (5, 801, 0, "borrow failed")] + } else { + vec![(1, 801, 0, ""), (4, 801, 17, "")] + }; + let (attempt, root, op, original, net) = setup(auth, &f, true, &rows); + auth.with_resource(|s, v, r| { + let shell = if bare { + s.transfer(v, r, 3, op, original, op)? + } else { + original + }; + let parent = s.sum_borrow(v, r, if bare { 8 } else { 7 }, op, shell)?; + let call = s.source_prepare( + v, + op, + if bare { + f.source_bare + } else { + f.source_written + }, + )?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.custody.live_owners(), 1); + assert_eq!(s.sum_view_tag(v, r, f.observe, scope, parent)?, 1); + let child = s.sum_view_project(v, r, f.project, scope, parent)?; + let inner = s.begin_operation_frame(v, 3, scope)?; + let hook = s.prepare_hook(v, f.invoke, inner, None)?; + let output = s.invoke_borrow(v, r, hook, net, child)?; + v.drop_resource_typed_companion(&s.layout, 7, output) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, inner)?; + s.borrow_end(f.end_child, scope, child)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + s.sum_borrow_end(r, if bare { 10 } else { 9 }, op, parent)?; + if bare { + assert!(!s.handles.contains_key(&original)); + s.sum_drop(v, r, f.drop_bare, op, shell)?; + } else { + s.carrier(original, 0, r)?; + let owner = s.sum_take(v, r, f.take, op, original, op)?; + s.close_or_drop(r, f.close, op, owner)?; + } + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert_eq!( + events(auth), + vec![(801, 1), (801, if failed { 4 } else { 3 }), (801, 5)] + ); + }); + } + } + } +} + +#[test] +fn native_resource_borrowed_sum_child_prevents_parent_end_and_owning_adoption_until_retired() { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let (attempt, root, op, shell, _) = setup(auth, &f, true, &[(1, 801, 0, "")]); + auth.with_resource(|s, v, r| { + let parent = s.sum_borrow(v, r, 7, op, shell)?; + let child = s.sum_view_project(v, r, f.caller_project, op, parent)?; + assert_eq!( + s.sum_borrow_end(r, 9, op, parent), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_take(v, r, f.take, op, shell, op), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!( + s.sum_drop(v, r, f.drop_written, op, shell), + Err(CustodyError::ActiveBorrow.into()) + ); + assert_eq!(s.custody.live_owners(), 1); + assert_eq!(s.custody.live_loans(), 2); + assert!( + s.exact_loan( + child, + NativeLoanSource::ExistingBorrow { + operation: f.caller_project + } + ) + .is_err() + ); + s.borrow_end(f.caller_end, op, child)?; + s.sum_borrow_end(r, 9, op, parent)?; + assert!(s.sum_borrow_end(r, 9, op, parent).is_err()); + assert!( + s.exact_loan( + child, + NativeLoanSource::ProjectedSumPayload { + operation: f.caller_project + } + ) + .is_err() + ); + let owner = s.sum_take(v, r, f.take, op, shell, op)?; + s.close_or_drop(r, f.close, op, owner)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt, root, op, &f); + assert_eq!(events(auth), vec![(801, 1), (801, 5)]); + }); + } +} + +#[test] +fn native_resource_borrowed_sum_abandoned_prefix_retires_leases_before_holder_cleanup() { + for occupied in [false, true] { + for result in [false, true] { + context(|auth| { + let f = fixture(result, 0); + let rows = if occupied { + vec![(1, 801, 0, "")] + } else { + vec![] + }; + let (attempt, root, op, original, net) = setup(auth, &f, occupied, &rows); + auth.with_resource(|s, v, r| { + let shell = s.transfer(v, r, 3, op, original, op)?; + let parent = s.sum_borrow(v, r, 8, op, shell)?; + let call = s.source_prepare(v, op, f.source_bare)?; + s.source_actual(v, r, call, 0, 0, net)?; + s.source_actual(v, r, call, 1, 1, parent.raw())?; + // No SourceEnter or provider invocation: suffix retirement handles the prefix. + s.end_operation_frame(v, r, op)?; + assert_eq!(s.counts(v, r).loan_handles, 0); + assert_eq!(s.custody.live_owners(), 0); + s.scope_complete(v, r, root, f.complete_root, 0)?; + assert_eq!(s.complete_entry(v, r, attempt, 0, false)?.selected_kind, 0); + Ok(()) + }) + .unwrap(); + assert_empty(auth); + assert_eq!( + events(auth), + if occupied { + vec![(801, 1), (801, 5)] + } else { + vec![] + } + ); + }); + } + } +} + +#[test] +fn native_resource_borrowed_sum_registration_rejects_wrong_formal_path_companion_and_issuer() { + for corrupt in 1..=7 { + context(|auth| { + let f = fixture(true, corrupt); + assert!( + install_disabled(auth, &f.bytes, entry()).is_err(), + "corruption {corrupt}" + ); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + }); + } + context(|auth| { + let mut f = fixture(false, 0); + f.bytes[8..12].copy_from_slice(&1u32.to_le_bytes()); + assert!(install_disabled(auth, &f.bytes, entry()).is_err()); + }); +} + +#[test] +fn native_resource_borrowed_sum_leaf_output_preflight_is_before_lookup_or_effects() { + let mut ctx = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *ctx, &mut result) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*ctx as *const JettRuntimeContextV1; + let mut sentinel = 0x5555u64; + unsafe { + assert_eq!( + leaves::jett_rt_v1_resource_sum_borrow(pointer, 0, 0, 0, std::ptr::null_mut()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_view_tag(pointer, 0, 0, 0, pointer.cast_mut().cast()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_view_project( + pointer, + 0, + 0, + 0, + (&mut sentinel as *mut u64).cast::().add(1).cast() + ), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + leaves::jett_rt_v1_resource_sum_failure_read(pointer, 0, 0, 0, std::ptr::null_mut()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + } + assert_eq!(sentinel, 0x5555); + let auth = AuthenticatedResourceContext::acquire(pointer).unwrap(); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *ctx, &mut result) }, + JettRuntimeStatusV1::OK + ); +} diff --git a/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs b/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs new file mode 100644 index 00000000..313b72e3 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/borrowed_sums.rs @@ -0,0 +1,420 @@ +//! Nonowning conditional carriers. Shell identity stays separate from payload custody. +use super::*; + +impl NativeResourceState { + pub(super) fn validate_sum_parent( + &self, + handle: ResourceHandleId, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + self.sum_lease(handle, registry).map(|_| ()) + } + pub(super) fn projected_sum_parent( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeSumLoanSource, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + if sum.slot != loan.source_slot || sum.frame != loan.source_frame || sum.shape != loan.shape + { + return Err(NativeResourceError::WrongOperation); + } + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + match source { + NativeSumLoanSource::ExistingBorrow { operation } => { + if operation != loan.borrow_operation + || !matches!( + self.operation(operation)?, + NativeOperation::BorrowSum { .. } + ) + { + return Err(NativeResourceError::WrongOperation); + } + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + self.resident_sum_at(self.activation_frame(frame, scope)?, handle, parameter)?; + } + NativeSumLoanSource::CarrierProjected { operation } => { + self.carrier_adapter_source(frame, handle, operation)?; + } + } + Ok(()) + } + fn sum_lease( + &self, + handle: ResourceHandleId, + registry: &ResourceRegistry, + ) -> ResourceResult<&NativeSumLoan> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + if sum.frame != loan.source_frame || sum.slot != loan.source_slot || sum.shape != loan.shape + { + return Err(NativeResourceError::WrongOperation); + } + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + self.carrier_frame_at(loan.frame, loan.shell, loan.source_slot)?; + match (&sum.payload, &loan.token) { + (NativeSumPayload::Some(owner) | NativeSumPayload::Ok(owner), Some(token)) => { + if self.custody.validate_owned(owner, registry)? + != self.custody.validate_borrowed(token, registry)? + { + return Err(NativeResourceError::WrongOperation); + } + } + (NativeSumPayload::None | NativeSumPayload::Fail { .. }, None) => {} + _ => return Err(NativeResourceError::WrongFamily), + } + Ok(loan) + } + pub(super) fn source_sum_loan( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeSumLoanSource, + shape: Option, + registry: &ResourceRegistry, + ) -> ResourceResult<&NativeSumLoan> { + let loan = self.sum_lease(handle, registry)?; + if shape.is_some_and(|shape| shape != loan.shape) { + return Err(NativeResourceError::WrongFamily); + } + match source { + NativeSumLoanSource::ExistingBorrow { operation } => { + if loan.borrow_operation != operation + || !matches!( + self.operation(operation)?, + NativeOperation::BorrowSum { .. } + ) + { + return Err(NativeResourceError::WrongOperation); + } + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = self.activation_frame(frame, scope)?; + self.resident_sum_at(scope, handle, parameter)?; + } + NativeSumLoanSource::CarrierProjected { operation } => { + self.carrier_adapter_source(frame, handle, operation)?; + } + } + Ok(loan) + } + pub(super) fn sum_unborrowed(&self, shell: ResourceHandleId) -> ResourceResult<()> { + if self + .handles + .values() + .any(|entry| matches!(entry, NativeResourceEntry::SumLoan(loan) if loan.shell == shell)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + Ok(()) + } + pub(super) fn frame_shells_unborrowed(&self, frame: ResourceHandleId) -> ResourceResult<()> { + for entry in self.handles.values() { + if let NativeResourceEntry::SumLoan(loan) = entry { + if loan.source_frame == frame && loan.frame != frame { + return Err(CustodyError::ActiveBorrow.into()); + } + } + } + Ok(()) + } + pub(super) fn sum_borrow( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + shell: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, lease_frame) = match self.operation(operation)? { + NativeOperation::BorrowSum { + source_sum_slot, + lease_frame, + .. + } => (*source_sum_slot, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + self.carrier(shell, source, registry)?; + self.carrier_frame_at(frame, shell, source)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let (source_frame, shape) = (sum.frame, sum.shape); + let token = match &sum.payload { + NativeSumPayload::Some(owner) | NativeSumPayload::Ok(owner) => { + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + Some(self.custody.begin_borrow(owner, &header.token, registry)?) + } + NativeSumPayload::Fail { shape, string } => { + ordinary + .validate_resource_ordinary(*string, &self.layout, *shape) + .map_err(ordinary_error)?; + None + } + NativeSumPayload::None => None, + }; + self.handles.insert( + handle, + NativeResourceEntry::SumLoan(NativeSumLoan { + frame, + borrow_operation: operation, + shell, + source_frame, + source_slot: source, + shape, + token, + }), + ); + Ok(handle) + } + pub(super) fn sum_view_tag( + &self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::ObserveSumView { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + let loan = self.source_sum_loan(frame, handle, source, None, registry)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&loan.shell) else { + return Err(NativeResourceError::WrongFamily); + }; + Ok(u32::from(matches!( + sum.payload, + NativeSumPayload::Some(_) | NativeSumPayload::Ok(_) + ))) + } + pub(super) fn sum_view_project( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, path, lease_frame) = match self.operation(operation)? { + NativeOperation::ProjectSumView { + source, + path, + lease_frame, + .. + } => (*source, *path, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + let parent = self.source_sum_loan(frame, handle, source, None, registry)?; + let (shell, source_slot, source_frame) = + (parent.shell, parent.source_slot, parent.source_frame); + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let owner = match (&sum.payload, path) { + (NativeSumPayload::Some(token), NativePayloadStep::Some) + | (NativeSumPayload::Ok(token), NativePayloadStep::Ok) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let token = self.custody.begin_borrow(owner, &header.token, registry)?; + self.handles.insert( + output, + NativeResourceEntry::Loan(NativeLoanEntry { + frame, + borrow_operation: operation, + source_slot, + source_frame, + token, + parent_sum: Some(handle), + }), + ); + Ok(output) + } + pub(super) fn sum_failure_read( + &mut self, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, shape) = match self.operation(operation)? { + NativeOperation::ReadFailureCompanion { + source, + failure_shape, + .. + } => (*source, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + let parent = self.source_sum_loan(frame, handle, source, None, registry)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&parent.shell) else { + return Err(NativeResourceError::WrongFamily); + }; + let NativeSumPayload::Fail { + shape: actual, + string, + } = sum.payload + else { + return Err(NativeResourceError::WrongOperation); + }; + if actual != shape { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .clone_resource_string_companion(&self.layout, shape, string) + .map_err(ordinary_error) + } + fn sum_borrow_end_inner(&mut self, handle: ResourceHandleId) -> ResourceResult<()> { + if self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Loan(loan) if loan.parent_sum == Some(handle))) { return Err(CustodyError::ActiveBorrow.into()); } + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if self.carrier_adapters.contains_key(&loan.shell) { + self.carrier_adapter_validate(loan.shell, handle)?; + } + let Some(NativeResourceEntry::SumLoan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Some(token) = &mut loan.token { + if let Err(error) = self.custody.end_borrow(token) { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(error.into()); + } + } + if let Some(origin) = self.carrier_adapters.get(&loan.shell) { + if origin.sum_loan != handle { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(NativeResourceError::WrongOperation); + } + let carrier_loan = origin.loan; + if let Err(error) = self.carrier_end_loan(loan.frame, carrier_loan) { + self.handles + .insert(handle, NativeResourceEntry::SumLoan(loan)); + return Err(error); + } + // Retain the shell's seal until its lease frame retires. Ending a + // view does not authorize adoption by an ordinary legacy slot. + } + Ok(()) + } + pub(super) fn sum_borrow_end( + &mut self, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.operation_frame(operation, frame)?; + let borrow = match self.operation(operation)? { + NativeOperation::EndSumBorrow { borrow, .. } => *borrow, + _ => return Err(NativeResourceError::WrongOperation), + }; + let source = match self.operation(borrow)? { + NativeOperation::BorrowSum { .. } => { + NativeSumLoanSource::ExistingBorrow { operation: borrow } + } + NativeOperation::Carrier { .. } => { + NativeSumLoanSource::CarrierProjected { operation: borrow } + } + _ => return Err(NativeResourceError::WrongOperation), + }; + let loan = self.source_sum_loan(frame, handle, source, None, registry)?; + if loan.frame != frame { + return Err(NativeResourceError::WrongFrame); + } + self.sum_borrow_end_inner(handle) + } + /// Complete only this frame's child projections, then its parent shell leases. + /// Incoming parent handles belong to the caller and are never adopted here. + pub(super) fn retire_sum_views(&mut self, frame: ResourceHandleId) -> ResourceResult<()> { + self.frame_shells_unborrowed(frame)?; + if !self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Loan(loan) if loan.frame == frame && loan.parent_sum.is_some()) || matches!(entry, NativeResourceEntry::SumLoan(loan) if loan.frame == frame)) { return Ok(()); } + let mut children = Vec::new(); + let mut parents = Vec::new(); + children + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + parents + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (&id, entry) in &self.handles { + match entry { + NativeResourceEntry::Loan(loan) + if loan.frame == frame && loan.parent_sum.is_some() => + { + children.push(id) + } + NativeResourceEntry::SumLoan(loan) if loan.frame == frame => parents.push(id), + _ => {} + } + } + let mut first: Option = None; + for handle in children.into_iter().rev() { + let Some(NativeResourceEntry::Loan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.end_borrow(&mut loan.token) { + self.handles.insert(handle, NativeResourceEntry::Loan(loan)); + first.get_or_insert(error.into()); + } + } + for handle in parents.into_iter().rev() { + if let Err(error) = self.sum_borrow_end_inner(handle) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers.rs b/crates/jett_runtime/src/native_abi/resource/carriers.rs new file mode 100644 index 00000000..5608586f --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers.rs @@ -0,0 +1,1337 @@ +//! Opaque v3 zero-leaf trees. Live leaf custody is deliberately not synthesized. +use super::*; +use crate::resource_custody::{CarrierRootToken, OwnedResourceToken}; +#[path = "carriers/occupied.rs"] +mod occupied; +use crate::resource_custody::{ + NativeCarrierChildSource as ChildSource, NativeCarrierDestination as Destination, + NativeCarrierLoanSource as LoanSource, NativeCarrierNode as Node, + NativeCarrierObservation as Observation, NativeCarrierOperation as Op, + NativeCarrierPath as Path, NativeCarrierSelector as Selector, NativeCarrierSource as Source, +}; + +pub(super) struct NativeCarrierTree { + pub(super) node: u32, + pub(super) selector: u32, + pub(super) ordinary: Option<(u32, u64)>, + /// Present only on scalar Ordinary nodes; strings retain their own depth. + pub(super) pending_depth: u64, + pub(super) owner: Option, + pub(super) children: Vec>, + pub(super) acquisition: Vec, +} +pub(super) struct NativeCarrierRoot { + pub(super) frame: ResourceHandleId, + pub(super) slot: u32, + pub(super) generation: u64, + pub(super) tree: NativeCarrierTree, + pub(super) custody: Option, +} +pub(super) struct NativeCarrierLoan { + pub(super) frame: ResourceHandleId, + pub(super) operation: u32, + pub(super) root: ResourceHandleId, + pub(super) generation: u64, + pub(super) path: Vec, + pub(super) parent: Option, + pub(super) incoming: Option<(u32, u32, ResourceHandleId)>, +} +pub(super) struct NativeCarrierBuilder { + pub(super) frame: ResourceHandleId, + pub(super) operation: u32, + pub(super) destination: u32, + pub(super) selector: u32, + pub(super) children: Vec>, + pub(super) acquisition: Vec, + pub(super) next: usize, + pub(super) custody: Option, +} +pub(super) struct NativeCarrierSumOrigin { + pub(super) loan: ResourceHandleId, + pub(super) operation: u32, + pub(super) sum_loan: ResourceHandleId, +} +fn index(selector: Selector, dynamic: u64) -> ResourceResult { + usize::try_from(match selector { + Selector::Static(i) => u64::from(i), + Selector::Dynamic => dynamic, + }) + .map_err(|_| NativeResourceError::WrongOperation) +} +fn normalized(path: &[Path], dynamic: u64) -> ResourceResult> { + let mut out = Vec::new(); + out.try_reserve_exact(path.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for p in path { + out.push(match *p { + Path::List(s) => Path::List(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + Path::MapKey(s) => Path::MapKey(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + Path::MapValue(s) => Path::MapValue(Selector::Static( + u32::try_from(index(s, dynamic)?) + .map_err(|_| NativeResourceError::WrongOperation)?, + )), + p => p, + }); + } + Ok(out) +} +fn child_index(tree: &NativeCarrierTree, step: Path) -> ResourceResult { + Ok(match step { + Path::Field(i) => i as usize, + Path::EnumPayload { variant, field } if variant == tree.selector => field as usize, + Path::MachinePayload { state, field } if state == tree.selector => field as usize, + Path::List(s) => index(s, 0)?, + Path::MapKey(s) => index(s, 0)? + .checked_mul(2) + .ok_or(NativeResourceError::Capacity)?, + Path::MapValue(s) => index(s, 0)? + .checked_mul(2) + .and_then(|i| i.checked_add(1)) + .ok_or(NativeResourceError::Capacity)?, + Path::Some | Path::Ok if tree.selector == 1 => 0, + Path::Fail if tree.selector == 0 => 0, + _ => return Err(NativeResourceError::WrongOperation), + }) +} +pub(super) fn selected<'a>( + mut tree: &'a NativeCarrierTree, + path: &[Path], +) -> ResourceResult<&'a NativeCarrierTree> { + for p in path { + let i = child_index(tree, *p)?; + tree = tree + .children + .get(i) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + } + Ok(tree) +} +pub(super) fn take_selected( + tree: &mut NativeCarrierTree, + path: &[Path], +) -> ResourceResult { + let (last, prefix) = path + .split_last() + .ok_or(NativeResourceError::WrongOperation)?; + let mut current = tree; + for p in prefix { + let i = child_index(current, *p)?; + current = current + .children + .get_mut(i) + .and_then(Option::as_mut) + .ok_or(NativeResourceError::WrongOperation)?; + } + let i = child_index(current, *last)?; + current + .children + .get_mut(i) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongOperation) +} +impl NativeResourceState { + pub(super) fn carrier_operation( + &self, + operation: u32, + frame: ResourceHandleId, + ) -> ResourceResult { + if self.layout.wire_version() != 3 { + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + let NativeOperation::Carrier { record } = *self.operation(operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let row = self + .layout + .carriers() + .operations + .get(record as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if row.site != self.layout.operations()[operation as usize].site() { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + if self.frame(frame)?.template != row.operation.frame() { + return Err(NativeResourceError::WrongFrame); + } + Ok(row.operation.clone()) + } + pub(super) fn carrier_slot_empty( + &self, + frame: ResourceHandleId, + slot: u32, + ) -> ResourceResult<()> { + let s = self + .layout + .carriers() + .slots + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if s.frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + if self.handles.values().any(|e|matches!(e,NativeResourceEntry::CarrierRoot(r) if r.frame==frame&&r.slot==slot)||matches!(e,NativeResourceEntry::CarrierBuilder(b) if b.frame==frame&&b.destination==slot)){return Err(NativeResourceError::WrongOperation);} + Ok(()) + } + fn validate_tree( + &self, + ordinary: &values::NativeValues, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + let mut pending = Vec::new(); + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending.push(tree); + while let Some(t) = pending.pop() { + let n = self.layout.carriers().node(t.node)?; + if !matches!(n, Node::Ordinary { .. }) && t.pending_depth != 0 { + return Err(NativeResourceError::WrongFamily); + } + if !matches!(n, Node::Resource { .. }) && t.owner.is_some() { + return Err(NativeResourceError::WrongFamily); + } + if t.acquisition + .iter() + .enumerate() + .any(|(i, p)| *p >= t.children.len() || t.acquisition[..i].contains(p)) + || t.children + .iter() + .enumerate() + .any(|(i, c)| c.is_some() && !t.acquisition.contains(&i)) + { + return Err(NativeResourceError::WrongOperation); + } + match n { + Node::Resource { .. } => { + if t.selector != 0 + || t.owner.is_none() + || t.ordinary.is_some() + || !t.children.is_empty() + || !t.acquisition.is_empty() + { + return Err(NativeResourceError::WrongFamily); + } + continue; + } + Node::Refinement { .. } => { + return Err(NativeResourceError::UnprovedCarrierRefinement); + } + Node::Ordinary { shape } => { + if !t.children.is_empty() || t.ordinary.map(|v| v.0) != Some(*shape) { + return Err(NativeResourceError::WrongFamily); + } + ordinary + .validate_resource_carrier_companion( + t.ordinary.ok_or(NativeResourceError::WrongFamily)?.1, + &self.layout, + *shape, + t.pending_depth, + ) + .map_err(ordinary_error)?; + continue; + } + Node::List { element } => { + if t.selector != 0 + || t.ordinary.is_some() + || t.children.iter().flatten().any(|c| c.node != *element) + { + return Err(NativeResourceError::WrongFamily); + } + } + Node::Map { key, value } => { + if t.selector != 0 + || t.ordinary.is_some() + || t.children.len() % 2 != 0 + || t.children.iter().enumerate().any(|(i, c)| { + c.as_ref() + .is_some_and(|c| c.node != if i % 2 == 0 { *key } else { *value }) + }) + { + return Err(NativeResourceError::WrongFamily); + } + } + _ => { + let children = self + .layout + .carriers() + .selected_children(t.node, t.selector)?; + if t.ordinary.is_some() + || t.children.len() != children.len() + || t.children + .iter() + .zip(children) + .any(|(c, n)| c.as_ref().is_some_and(|c| c.node != n)) + { + return Err(NativeResourceError::WrongFamily); + } + } + } + pending + .try_reserve(t.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + pending.extend(t.children.iter().flatten()); + } + Ok(()) + } + pub(super) fn validate_complete_tree( + &self, + ordinary: &values::NativeValues, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + self.validate_tree(ordinary, tree)?; + let mut pending = Vec::new(); + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending.push(tree); + while let Some(t) = pending.pop() { + if t.children.iter().any(Option::is_none) { + return Err(NativeResourceError::WrongOperation); + } + pending + .try_reserve(t.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + pending.extend(t.children.iter().flatten()); + } + Ok(()) + } + pub(super) fn carrier_root( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<&NativeCarrierRoot> { + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let row = self + .layout + .carriers() + .slots + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if root.slot != slot + || root.generation != handle.raw() + || root.tree.node != row.node + || root.frame != self.activation_frame(frame, row.frame)? + { + return Err(NativeResourceError::WrongFrame); + } + self.frame(root.frame)?; + self.validate_tree(ordinary, &root.tree)?; + self.validate_carrier_ownership(root.custody.as_ref(), &root.tree)?; + if let Some(token) = &root.custody { + self.custody.validate_carrier_location( + token, + &self.frame(root.frame)?.token, + &self.layout.carrier_slot(slot)?, + false, + )?; + } + Ok(root) + } + pub(super) fn carrier_loan( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: LoanSource, + ) -> ResourceResult<&NativeCarrierLoan> { + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.frame(loan.frame)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + if root.generation != loan.generation { + return Err(NativeResourceError::InvalidHandle); + } + self.frame(root.frame)?; + self.validate_tree(ordinary, &root.tree)?; + self.validate_carrier_ownership(root.custody.as_ref(), &root.tree)?; + if let Some(token) = &root.custody { + self.custody.validate_carrier_location( + token, + &self.frame(root.frame)?.token, + &self.layout.carrier_slot(root.slot)?, + false, + )?; + } + self.validate_tree(ordinary, selected(&root.tree, &loan.path)?)?; + if let Some(parent) = loan.parent { + let Some(NativeResourceEntry::CarrierLoan(parent_loan)) = self.handles.get(&parent) + else { + return Err(NativeResourceError::InvalidHandle); + }; + if parent_loan.root != loan.root + || parent_loan.generation != loan.generation + || !loan.path.starts_with(&parent_loan.path) + { + return Err(NativeResourceError::WrongOperation); + } + } + match source { + LoanSource::ExistingBorrow { operation } => { + if loan.operation != operation + || loan.incoming.is_some() + || self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame + { + return Err(NativeResourceError::WrongOperation); + } + } + LoanSource::IncomingViewFormal { scope, parameter } => { + let Some((expected, p, call)) = loan.incoming else { + return Err(NativeResourceError::WrongOperation); + }; + let scope_frame = self.activation_frame(frame, scope)?; + if expected != scope || p != parameter || loan.frame != scope_frame { + return Err(NativeResourceError::WrongFrame); + } + let active = self.source_call(call)?; + if active.scope != Some(scope_frame) || active.phase != source::SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + } + } + Ok(loan) + } + pub(super) fn carrier_location( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: Source, + path: &[Path], + dynamic: u64, + ) -> ResourceResult<(ResourceHandleId, u64, Vec, Option)> { + let (root, generation, mut full, parent) = match source { + Source::Slot(slot) => { + let r = self.carrier_root(ordinary, frame, handle, slot)?; + (handle, r.generation, Vec::new(), None) + } + Source::Loan(source) => { + let l = self.carrier_loan(ordinary, frame, handle, source)?; + (l.root, l.generation, l.path.clone(), Some(handle)) + } + }; + let tail = normalized(path, dynamic)?; + full.try_reserve(tail.len()) + .map_err(|_| NativeResourceError::Capacity)?; + full.extend(tail); + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.get(&root) else { + return Err(NativeResourceError::WrongFamily); + }; + self.layout.carriers().path_node(r.tree.node, &full)?; + selected(&r.tree, &full)?; + Ok((root, generation, full, parent)) + } + pub(super) fn carrier_unborrowed(&self, root: ResourceHandleId) -> ResourceResult<()> { + if self + .handles + .values() + .any(|e| matches!(e,NativeResourceEntry::CarrierLoan(l) if l.root==root)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + Ok(()) + } + pub(super) fn carrier_construct_begin( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Construct { + destination, + selector, + children, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + self.carrier_slot_empty(frame, destination)?; + let mut values = Vec::new(); + let mut acquisition = Vec::new(); + values + .try_reserve_exact(children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + acquisition + .try_reserve_exact(children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + values.resize_with(children.len(), || None); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::CarrierBuilder(NativeCarrierBuilder { + frame, + operation, + destination, + selector, + children: values, + acquisition, + next: 0, + custody: None, + }), + ); + Ok(handle) + } + pub(super) fn carrier_child( + &mut self, + ordinary: &values::NativeValues, + builder: ResourceHandleId, + ordinal: u32, + bits: u64, + ) -> ResourceResult<()> { + self.carrier_child_value(ordinary, builder, ordinal, bits, 0) + } + + fn carrier_child_value( + &mut self, + ordinary: &values::NativeValues, + builder: ResourceHandleId, + ordinal: u32, + bits: u64, + pending_depth: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let (frame, operation, next) = (b.frame, b.operation, b.next); + let Op::Construct { children, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + if ordinal as usize != next { + return Err(NativeResourceError::WrongOperation); + } + let child = children + .get(next) + .ok_or(NativeResourceError::WrongOperation)?; + if pending_depth != 0 && !matches!(child.source, ChildSource::Ordinary { .. }) { + return Err(NativeResourceError::WrongOperation); + } + if b.children + .get(child.index as usize) + .ok_or(NativeResourceError::WrongOperation)? + .is_some() + { + return Err(NativeResourceError::WrongOperation); + } + let tree = match child.source { + ChildSource::Ordinary { shape } => { + ordinary + .validate_resource_carrier_companion(bits, &self.layout, shape, pending_depth) + .map_err(ordinary_error)?; + NativeCarrierTree { + node: child.node, + selector: 0, + ordinary: Some((shape, bits)), + pending_depth, + owner: None, + children: Vec::new(), + acquisition: Vec::new(), + } + } + ChildSource::Move { slot } => { + let handle = ResourceHandleId::new(bits)?; + let root = self.carrier_root(ordinary, frame, handle, slot)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + r.tree + } + ChildSource::LeafOwner { .. } => { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + ChildSource::LeafSum { slot } => self.carrier_take_leaf_sum( + ordinary, + frame, + ResourceHandleId::new(bits)?, + slot, + child.node, + )?, + }; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get_mut(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let target = b + .children + .get_mut(child.index as usize) + .ok_or(NativeResourceError::WrongOperation)?; + *target = Some(tree); + b.acquisition.push(child.index as usize); + b.next += 1; + Ok(()) + } + pub(super) fn carrier_commit( + &mut self, + ordinary: &values::NativeValues, + builder: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(b)) = self.handles.get(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + let Op::Construct { children, .. } = self.carrier_operation(b.operation, b.frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + self.validate_carrier_builder(ordinary, b)?; + if b.next != children.len() || b.children.iter().any(Option::is_none) { + return Err(NativeResourceError::WrongOperation); + } + let node = self.layout.carriers().slots[b.destination as usize].node; + for child in b.children.iter().flatten() { + self.validate_complete_tree(ordinary, child)?; + } + let is_ordinary = matches!(self.layout.carriers().node(node)?, Node::Ordinary { .. }); + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::CarrierBuilder(mut b)) = self.handles.remove(&builder) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Some(root) = &b.custody { + if let Err(error) = self.custody.complete_carrier_root(root) { + self.handles + .insert(builder, NativeResourceEntry::CarrierBuilder(b)); + return Err(error.into()); + } + } + let tree = if is_ordinary { + b.children + .get_mut(0) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongFamily)? + } else { + NativeCarrierTree { + node, + selector: b.selector, + ordinary: None, + pending_depth: 0, + owner: None, + children: b.children, + acquisition: b.acquisition, + } + }; + self.handles.insert( + output, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame: b.frame, + slot: b.destination, + generation: output.raw(), + custody: b.custody, + tree, + }), + ); + Ok(output) + } + pub(super) fn carrier_move_reserved( + &mut self, + ordinary: &values::NativeValues, + handle: ResourceHandleId, + source: u32, + destination: u32, + source_frame: ResourceHandleId, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + let root = self.carrier_root(ordinary, source_frame, handle, source)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + self.carrier_slot_empty(frame, destination)?; + let expected = self.layout.carriers().slots[destination as usize].node; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if root.tree.node != expected { + return Err(NativeResourceError::WrongFamily); + } + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + root.frame = frame; + root.slot = destination; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + pub(super) fn carrier_transfer( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Transfer { + source, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.carrier_move_reserved(ordinary, handle, source, destination, frame, target, output) + } + pub(super) fn carrier_qualify( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::QualifyMachine { + source, + destination, + machine, + state, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, handle, source)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(handle)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + if root.tree.selector != state + || self.layout.carriers().node(root.tree.node)? + != &(Node::MachineState { machine, state }) + || self.layout.carriers().slots[destination as usize].node != machine + { + return Err(NativeResourceError::WrongFamily); + } + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + self.carrier_slot_empty(target, destination)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + root.tree.node = machine; + root.slot = destination; + root.frame = target; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + pub(super) fn carrier_borrow( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Borrow { + source, + path, + lease_frame, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + let (root, generation, path, parent) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::CarrierLoan(NativeCarrierLoan { + frame, + operation, + root, + generation, + path, + parent, + incoming: None, + }), + ); + Ok(output) + } + pub(super) fn carrier_end_loan( + &mut self, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if loan.frame != frame + || self + .handles + .values() + .any(|e| matches!(e,NativeResourceEntry::CarrierLoan(l) if l.parent==Some(handle))) + || self + .carrier_adapters + .values() + .any(|a| a.loan == handle && self.handles.contains_key(&a.sum_loan)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + self.handles.remove(&handle); + Ok(()) + } + pub(super) fn carrier_end( + &mut self, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::EndBorrow { borrow, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let Some(NativeResourceEntry::CarrierLoan(l)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if l.operation != borrow || l.incoming.is_some() { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_end_loan(frame, handle) + } + pub(super) fn carrier_observe( + &self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Observe { + source, + path, + observation, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let (root, _, path, _) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let Some(NativeResourceEntry::CarrierRoot(r)) = self.handles.get(&root) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = selected(&r.tree, &path)?; + match observation { + Observation::Length => Ok(u64::try_from( + match self.layout.carriers().node(tree.node)? { + Node::List { .. } => tree.children.len(), + Node::Map { .. } => tree.children.len() / 2, + _ => return Err(NativeResourceError::WrongFamily), + }, + ) + .map_err(|_| NativeResourceError::Capacity)?), + Observation::Tag => match self.layout.carriers().node(tree.node)? { + Node::Enum { variants, .. } => Ok(variants + .get(tree.selector as usize) + .ok_or(NativeResourceError::WrongFamily)? + .discriminant as u64), + Node::Optional { .. } | Node::Result { .. } => Ok(u64::from(tree.selector)), + _ => Err(NativeResourceError::WrongFamily), + }, + Observation::State => Ok(u64::from(tree.selector)), + Observation::Ordinary { shape, clone } => { + let (actual, bits) = tree.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != shape || tree.pending_depth != 0 { + return Err(NativeResourceError::WrongFamily); + } + self.carrier_observe_ordinary(ordinary, shape, clone, bits) + } + } + } + fn carrier_observe_ordinary( + &self, + ordinary: &mut values::NativeValues, + shape: u32, + clone: bool, + bits: u64, + ) -> ResourceResult { + let required = Observation::ordinary_clone_policy(self.layout.shapes(), shape) + .ok_or(NativeResourceError::UnsupportedCarrierObservation)?; + if clone != required { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .validate_resource_carrier_companion(bits, &self.layout, shape, 0) + .map_err(ordinary_error)?; + if required { + ordinary + .clone_resource_carrier_string_companion(&self.layout, shape, bits) + .map_err(ordinary_error) + } else { + Ok(bits) + } + } + + pub(super) fn carrier_observe_scalar_task( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult<(u64, u64)> { + self.running(ordinary)?; + let Op::Observe { + source, + path, + observation: + Observation::Ordinary { + shape, + clone: false, + }, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if !matches!( + self.layout.shapes().get(shape as usize), + Some( + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing + ) + ) { + return Err(NativeResourceError::WrongFamily); + } + let (root, _, path, _) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&root) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = selected(&root.tree, &path)?; + let (actual, bits) = tree.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != shape { + return Err(NativeResourceError::WrongFamily); + } + ordinary + .validate_resource_carrier_companion(bits, &self.layout, shape, tree.pending_depth) + .map_err(ordinary_error)?; + Ok((bits, tree.pending_depth)) + } + pub(super) fn carrier_extract( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Extract { + source, + path, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let (_, _, path, _) = self.carrier_location( + ordinary, + frame, + handle, + Source::Slot(source), + &path, + dynamic, + )?; + self.carrier_unborrowed(handle)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let projected = selected(&root.tree, &path)?; + self.validate_complete_tree(ordinary, projected)?; + match destination { + Destination::Slot(slot) => { + if projected.node != self.layout.carriers().slots[slot as usize].node { + return Err(NativeResourceError::WrongFamily); + } + } + Destination::Ordinary { shape } => { + if projected.ordinary.map(|v| v.0) != Some(shape) || projected.pending_depth != 0 { + return Err(NativeResourceError::WrongFamily); + } + } + } + if path.is_empty() { + return Err(NativeResourceError::WrongOperation); + } + let output = match destination { + Destination::Slot(slot) => { + let target = self + .activation_frame(frame, self.layout.carriers().slots[slot as usize].frame)?; + self.carrier_slot_empty(target, slot)?; + let mut ids = self.reserve(1)?; + Some((next_handle(&mut ids)?, target, slot)) + } + Destination::Ordinary { .. } => None, + }; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = take_selected(&mut root.tree, &path)?; + if let Some((out, target, slot)) = output { + self.handles.insert( + out, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame: target, + slot, + generation: out.raw(), + custody: None, + tree, + }), + ); + Ok(out.raw()) + } else { + let Destination::Ordinary { shape } = destination else { + return Err(NativeResourceError::WrongFamily); + }; + let (actual, bits) = tree.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != shape { + return Err(NativeResourceError::WrongFamily); + } + Ok(bits) + } + } + fn carrier_take_leaf_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + node: u32, + ) -> ResourceResult { + self.carrier_frame_at(frame, handle, slot)?; + self.sum_unborrowed(handle)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != slot + || self.layout.slots().get(slot as usize).map(|s| s.shape()) != Some(sum.shape) + || self.carrier_adapters.contains_key(&handle) + || !self.layout.carrier_equivalent_sum(node, sum.shape) + { + return Err(NativeResourceError::WrongFamily); + } + let (selector, child) = match sum.payload { + NativeSumPayload::None => (0, None), + NativeSumPayload::Fail { shape, string } => { + ordinary + .validate_resource_ordinary(string, &self.layout, shape) + .map_err(ordinary_error)?; + let Node::Result { fail, .. } = self.layout.carriers().node(node)? else { + return Err(NativeResourceError::WrongFamily); + }; + ( + 0, + Some(NativeCarrierTree { + node: *fail, + selector: 0, + ordinary: Some((shape, string)), + pending_depth: 0, + owner: None, + children: Vec::new(), + acquisition: Vec::new(), + }), + ) + } + _ => return Err(NativeResourceError::UnsupportedCarrierCustody), + }; + let mut children = Vec::new(); + let mut acquisition = Vec::new(); + children + .try_reserve_exact(usize::from(child.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + acquisition + .try_reserve_exact(usize::from(child.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + if let Some(child) = child { + children.push(Some(child)); + acquisition.push(0); + } + self.handles.remove(&handle); + Ok(NativeCarrierTree { + node, + selector, + ordinary: None, + pending_depth: 0, + owner: None, + children, + acquisition, + }) + } + fn prepare_carrier_cleanup<'a>( + &self, + trees: impl Iterator, + ) -> ResourceResult> { + let mut scan = Vec::new(); + for tree in trees { + scan.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + scan.push(tree); + } + let mut count = 0usize; + while let Some(tree) = scan.pop() { + count = count.checked_add(1).ok_or(NativeResourceError::Capacity)?; + scan.try_reserve(tree.children.len()) + .map_err(|_| NativeResourceError::Capacity)?; + scan.extend(tree.children.iter().flatten()); + } + let mut pending = Vec::new(); + pending + .try_reserve_exact(count) + .map_err(|_| NativeResourceError::Capacity)?; + Ok(pending) + } + fn drop_carrier_tree_prepared( + &self, + ordinary: &mut values::NativeValues, + tree: NativeCarrierTree, + mut pending: Vec, + ) -> ResourceResult<()> { + pending.push(tree); + let mut first = None; + while let Some(mut tree) = pending.pop() { + if let Some((shape, bits)) = tree.ordinary { + if let Err(e) = ordinary + .drop_resource_carrier_companion(&self.layout, shape, bits, tree.pending_depth) + .map_err(ordinary_error) + { + first.get_or_insert(e); + } + } + // LIFO visiting makes the latest lexical acquisition retire first. + for i in tree.acquisition { + if let Some(child) = tree.children.get_mut(i).and_then(Option::take) { + pending.push(child); + } + } + } + first.map_or(Ok(()), Err) + } + pub(super) fn carrier_retire( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::Retire { source, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, handle, source)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + let pending = self.prepare_carrier_cleanup(std::iter::once(&root.tree))?; + self.carrier_unborrowed(handle)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.drop_carrier_tree_prepared(ordinary, root.tree, pending) + } + pub(super) fn carrier_reset_iteration( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Op::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + self.carrier_root(ordinary, frame, handle, source)?; + self.carrier_unborrowed(handle)?; + let mut holders = Vec::new(); + holders + .try_reserve_exact(carrier_binders.len() + leaf_sum_binders.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for slot in carrier_binders { + let target = + self.activation_frame(frame, self.layout.carriers().slots[slot as usize].frame)?; + if self.handles.values().any(|e|matches!(e,NativeResourceEntry::CarrierBuilder(b) if b.frame==target&&b.destination==slot)){return Err(NativeResourceError::WrongOperation);} + if let Some(id) = self.handles.iter().find_map(|(id, e)| { + matches!(e,NativeResourceEntry::CarrierRoot(r) if r.frame==target&&r.slot==slot) + .then_some(*id) + }) { + let root = self.carrier_root(ordinary, frame, id, slot)?; + self.carrier_unborrowed(id)?; + if root.custody.is_some() { + return Err(NativeResourceError::UnsupportedCarrierCustody); + } + holders.push(( + id, + Some(self.prepare_carrier_cleanup(std::iter::once(&root.tree))?), + )); + } + } + for slot in leaf_sum_binders { + let target = + self.activation_frame(frame, self.layout.slots()[slot as usize].frame())?; + let id = self.handles.iter().find_map(|(id, e)| { + matches!(e,NativeResourceEntry::Sum(s) if s.frame==target&&s.slot==slot) + .then_some(*id) + }); + if let Some(id)=id{ + self.carrier_frame_at(frame,id,slot)?;self.sum_unborrowed(id)?; + let Some(NativeResourceEntry::Sum(sum))=self.handles.get(&id) else{return Err(NativeResourceError::WrongFamily);}; + if self.carrier_adapters.contains_key(&id)||sum.shape!=self.layout.slots()[slot as usize].shape(){return Err(NativeResourceError::WrongFamily);} + match sum.payload{NativeSumPayload::None=>{},NativeSumPayload::Fail{shape,string}=>ordinary.validate_resource_ordinary(string,&self.layout,shape).map_err(ordinary_error)?,_=>return Err(NativeResourceError::UnsupportedCarrierCustody)} + holders.push((id,None)); + }else if self.handles.values().any(|e|matches!(e,NativeResourceEntry::Owner(o) if o.frame==target&&o.slot==slot)||matches!(e,NativeResourceEntry::Prepared(p) if p.destination_frame==target&&p.destination==slot)){return Err(NativeResourceError::UnsupportedCarrierCustody);} + } + // Issued identities reflect the lexical acquisition order across both slot families. + holders.sort_by_key(|(id, _)| std::cmp::Reverse(id.raw())); + let mut first = None; + for (id, pending) in holders { + let result = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => self.drop_carrier_tree_prepared( + ordinary, + root.tree, + pending.ok_or(NativeResourceError::WrongFamily)?, + ), + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Fail { shape, string }, + .. + })) => ordinary + .drop_resource_typed_companion(&self.layout, shape, string) + .map_err(ordinary_error), + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::None, + .. + })) => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } + pub(super) fn safety_retire_carriers( + &mut self, + ordinary: &mut values::NativeValues, + ) -> ResourceResult<()> { + // The context has already been retired and waited for its leases. This + // backstop never publishes data or grants a reusable Source loan. + self.handles + .retain(|_, entry| !matches!(entry, NativeResourceEntry::CarrierLoan(_))); + let mut holders = Vec::new(); + holders + .try_reserve(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + holders.extend(self.handles.iter().filter_map(|(id, entry)| { + matches!( + entry, + NativeResourceEntry::CarrierRoot(_) | NativeResourceEntry::CarrierBuilder(_) + ) + .then_some(*id) + })); + holders.sort_by_key(|id| std::cmp::Reverse(id.raw())); + let mut first = None; + for id in holders { + let pending = match self.handles.get(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => { + self.prepare_carrier_cleanup(std::iter::once(&root.tree))? + } + Some(NativeResourceEntry::CarrierBuilder(builder)) => { + let mut pending = + self.prepare_carrier_cleanup(builder.children.iter().flatten())?; + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending + } + _ => continue, + }; + let (tree, custody) = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(root)) => (root.tree, root.custody), + Some(NativeResourceEntry::CarrierBuilder(builder)) => ( + NativeCarrierTree { + node: self + .layout + .carriers() + .slots + .get(builder.destination as usize) + .ok_or(NativeResourceError::WrongOperation)? + .node, + selector: builder.selector, + ordinary: None, + pending_depth: 0, + owner: None, + children: builder.children, + acquisition: builder.acquisition, + }, + builder.custody, + ), + _ => continue, + }; + if let Some(token) = custody.as_ref() { + if let Err(error) = self.custody.retire_carrier_metadata(token) { + first.get_or_insert(NativeResourceError::Custody(error)); + } + } + if let Err(error) = self.drop_carrier_tree_prepared(ordinary, tree, pending) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} + +#[cfg(test)] +#[path = "carriers_tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs b/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs new file mode 100644 index 00000000..0e99b5e2 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers/occupied.rs @@ -0,0 +1,729 @@ +//! Occupied carrier transport joins typed paths to the Core ownership ledger. +use super::*; + +fn child_path( + graph: &crate::resource_custody::NativeCarrierLayout, + node: u32, + selector: u32, + index: usize, +) -> ResourceResult { + let i = u32::try_from(index).map_err(|_| NativeResourceError::Capacity)?; + Ok(match graph.node(node)? { + Node::Struct { .. } => Path::Field(i), + Node::Optional { .. } if selector == 1 && index == 0 => Path::Some, + Node::Result { .. } if index == 0 => { + if selector == 1 { + Path::Ok + } else { + Path::Fail + } + } + Node::Enum { .. } => Path::EnumPayload { + variant: selector, + field: i, + }, + Node::Machine { .. } => Path::MachinePayload { + state: selector, + field: i, + }, + Node::MachineState { state, .. } if *state == selector => Path::MachinePayload { + state: selector, + field: i, + }, + Node::List { .. } => Path::List(Selector::Static(i)), + Node::Map { .. } if index % 2 == 0 => Path::MapKey(Selector::Static(i / 2)), + Node::Map { .. } => Path::MapValue(Selector::Static(i / 2)), + _ => return Err(NativeResourceError::WrongFamily), + }) +} + +fn copied_path(path: &[Path], next: Option) -> ResourceResult> { + let mut out = Vec::new(); + out.try_reserve_exact(path.len() + usize::from(next.is_some())) + .map_err(|_| NativeResourceError::Capacity)?; + out.extend_from_slice(path); + out.extend(next); + Ok(out) +} + +fn owners<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a NativeCarrierTree, + path: &[Path], + out: &mut Vec<(&'a OwnedResourceToken, Vec)>, +) -> ResourceResult<()> { + if let Some(owner) = &tree.owner { + out.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + out.push((owner, copied_path(path, None)?)); + } + for i in &tree.acquisition { + if let Some(child) = tree.children.get(*i).and_then(Option::as_ref) { + owners( + graph, + child, + &copied_path(path, Some(child_path(graph, tree.node, tree.selector, *i)?))?, + out, + )?; + } + } + Ok(()) +} + +// Borrow each distinct child once in physical order, then sort its rank vector +// into lexical acquisition order. No owner is copied or inferred from bits. +fn owners_mut<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a mut NativeCarrierTree, + from: &[Path], + prefix: &[Path], + rank: &[usize], + out: &mut Vec<(&'a mut OwnedResourceToken, Vec, Vec, Vec)>, +) -> ResourceResult<()> { + if let Some(owner) = &mut tree.owner { + let mut order = Vec::new(); + order + .try_reserve_exact(rank.len()) + .map_err(|_| NativeResourceError::Capacity)?; + order.extend_from_slice(rank); + let mut to = copied_path(prefix, None)?; + to.try_reserve(from.len()) + .map_err(|_| NativeResourceError::Capacity)?; + to.extend_from_slice(from); + out.try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + out.push((owner, copied_path(from, None)?, to, order)); + } + for (i, child) in tree.children.iter_mut().enumerate() { + if let Some(child) = child { + let order = tree + .acquisition + .iter() + .position(|p| *p == i) + .ok_or(NativeResourceError::WrongOperation)?; + let mut next_rank = Vec::new(); + next_rank + .try_reserve_exact(rank.len() + 1) + .map_err(|_| NativeResourceError::Capacity)?; + next_rank.extend_from_slice(rank); + next_rank.push(order); + owners_mut( + graph, + child, + &copied_path(from, Some(child_path(graph, tree.node, tree.selector, i)?))?, + prefix, + &next_rank, + out, + )?; + } + } + Ok(()) +} + +fn retirement_owners<'a>( + graph: &crate::resource_custody::NativeCarrierLayout, + tree: &'a mut NativeCarrierTree, +) -> ResourceResult)>> { + let mut ordered = Vec::new(); + owners_mut(graph, tree, &[], &[], &[], &mut ordered)?; + ordered.sort_by(|a, b| a.3.cmp(&b.3)); + let mut result = Vec::new(); + result + .try_reserve_exact(ordered.len()) + .map_err(|_| NativeResourceError::Capacity)?; + result.extend(ordered.into_iter().map(|(owner, from, _, _)| (owner, from))); + Ok(result) +} + +impl NativeResourceState { + /// Additive scalar transport uses the same authenticated constructor row. + /// Depth is data and cannot select a Resource adoption or another schema. + pub(in crate::native_abi::resource) fn carrier_child_scalar_task( + &mut self, + ordinary: &values::NativeValues, + builder_id: ResourceHandleId, + ordinal: u32, + bits: u64, + pending_depth: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(builder)) = self.handles.get(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + self.validate_carrier_builder(ordinary, builder)?; + let Op::Construct { children, .. } = + self.carrier_operation(builder.operation, builder.frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if ordinal as usize != builder.next { + return Err(NativeResourceError::WrongOperation); + } + let child = children + .get(builder.next) + .ok_or(NativeResourceError::WrongOperation)?; + let ChildSource::Ordinary { shape } = child.source else { + return Err(NativeResourceError::WrongOperation); + }; + if !matches!( + self.layout.shapes().get(shape as usize), + Some( + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing + ) + ) { + return Err(NativeResourceError::WrongFamily); + } + self.carrier_child_value(ordinary, builder_id, ordinal, bits, pending_depth) + } + + pub(super) fn validate_carrier_ownership( + &self, + root: Option<&CarrierRootToken>, + tree: &NativeCarrierTree, + ) -> ResourceResult<()> { + let mut tokens = Vec::new(); + owners(self.layout.carriers(), tree, &[], &mut tokens)?; + match root { + Some(root) => self + .custody + .validate_carrier_inventory(root, &tokens) + .map_err(Into::into), + None if tokens.is_empty() => Ok(()), + None => Err(NativeResourceError::WrongFamily), + } + } + + pub(super) fn validate_carrier_builder( + &self, + ordinary: &values::NativeValues, + builder: &NativeCarrierBuilder, + ) -> ResourceResult<()> { + let Op::Construct { children, .. } = + self.carrier_operation(builder.operation, builder.frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if builder.next > children.len() + || builder.acquisition.len() != builder.next + || builder + .acquisition + .iter() + .zip(&children) + .any(|(i, c)| *i != c.index as usize) + || builder.children.iter().enumerate().any(|(i, c)| { + c.is_some() + != children[..builder.next] + .iter() + .any(|c| c.index as usize == i) + }) + { + return Err(NativeResourceError::WrongOperation); + } + let node = self.layout.carriers().slots[builder.destination as usize].node; + let mut tokens = Vec::new(); + for i in &builder.acquisition { + let tree = builder.children[*i] + .as_ref() + .ok_or(NativeResourceError::WrongOperation)?; + if children[..builder.next] + .iter() + .find(|c| c.index as usize == *i) + .is_none_or(|c| c.node != tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, tree)?; + let prefix = if matches!(self.layout.carriers().node(node)?, Node::Ordinary { .. }) { + Vec::new() + } else { + vec![child_path( + self.layout.carriers(), + node, + builder.selector, + *i, + )?] + }; + owners(self.layout.carriers(), tree, &prefix, &mut tokens)?; + } + match &builder.custody { + Some(root) => { + self.custody.validate_carrier_location( + root, + &self.frame(builder.frame)?.token, + &self.layout.carrier_slot(builder.destination)?, + true, + )?; + self.custody.validate_carrier_inventory(root, &tokens)?; + } + None if tokens.is_empty() => {} + None => return Err(NativeResourceError::WrongFamily), + } + Ok(()) + } + + fn builder_root(&mut self, builder: &mut NativeCarrierBuilder) -> ResourceResult<()> { + if builder.custody.is_none() { + let slot = self.layout.carrier_slot(builder.destination)?; + let Some(NativeResourceEntry::Frame(frame)) = self.handles.get(&builder.frame) else { + return Err(NativeResourceError::WrongFrame); + }; + builder.custody = Some(self.custody.begin_carrier_root(&frame.token, &slot)?); + } + Ok(()) + } + + pub(in crate::native_abi::resource) fn carrier_child_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + builder_id: ResourceHandleId, + ordinal: u32, + bits: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let Some(NativeResourceEntry::CarrierBuilder(builder)) = self.handles.get(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + self.validate_carrier_builder(ordinary, builder)?; + let Op::Construct { children, .. } = + self.carrier_operation(builder.operation, builder.frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if ordinal as usize != builder.next { + return Err(NativeResourceError::WrongOperation); + } + let child = children + .get(builder.next) + .ok_or(NativeResourceError::WrongOperation)? + .clone(); + if !matches!(child.source, ChildSource::LeafOwner { .. }) { + // Whole occupied carrier children use the same typed root migration. + if let ChildSource::Move { slot } = child.source { + let input = ResourceHandleId::new(bits)?; + let root = self.carrier_root(ordinary, builder.frame, input, slot)?; + self.validate_complete_tree(ordinary, &root.tree)?; + if root.custody.is_some() { + return self.carrier_occupied_child( + ordinary, registry, builder_id, ordinal, input, child, + ); + } + } + return self.carrier_child(ordinary, builder_id, ordinal, bits); + } + let ChildSource::LeafOwner { slot } = child.source else { + unreachable!() + }; + let input = ResourceHandleId::new(bits)?; + self.carrier_frame_at(builder.frame, input, slot)?; + let owner = self.owner(input, slot)?; + let Node::Resource { kind } = self.layout.carriers().node(child.node)? else { + return Err(NativeResourceError::WrongFamily); + }; + let shape = self.layout.slots()[slot as usize].shape(); + if self.layout.shapes().get(shape as usize) != Some(&NativeShape::Resource { kind: *kind }) + { + return Err(NativeResourceError::WrongFamily); + } + self.custody.validate_owned(&owner.token, registry)?; + let Some(NativeResourceEntry::CarrierBuilder(mut builder)) = + self.handles.remove(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = (|| { + self.builder_root(&mut builder)?; + let node = self.layout.carriers().slots[builder.destination as usize].node; + let path = [child_path( + self.layout.carriers(), + node, + builder.selector, + child.index as usize, + )?]; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.get_mut(&input) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody.transfer_into_carrier( + &mut owner.token, + builder + .custody + .as_ref() + .ok_or(NativeResourceError::WrongFamily)?, + &path, + registry, + )?; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.remove(&input) else { + unreachable!() + }; + builder.children[child.index as usize] = Some(NativeCarrierTree { + node: child.node, + selector: 0, + ordinary: None, + pending_depth: 0, + owner: Some(owner.token), + children: Vec::new(), + acquisition: Vec::new(), + }); + builder.acquisition.push(child.index as usize); + builder.next += 1; + Ok(()) + })(); + self.handles + .insert(builder_id, NativeResourceEntry::CarrierBuilder(builder)); + result + } + + fn migrate_carrier_tree( + &mut self, + tree: &mut NativeCarrierTree, + source: &CarrierRootToken, + destination: &CarrierRootToken, + prefix: &[Path], + registry: &ResourceRegistry, + complete: bool, + ) -> ResourceResult<()> { + let mut ordered = Vec::new(); + owners_mut(self.layout.carriers(), tree, &[], prefix, &[], &mut ordered)?; + ordered.sort_by(|a, b| a.3.cmp(&b.3)); + let mut transfers = Vec::new(); + transfers + .try_reserve_exact(ordered.len()) + .map_err(|_| NativeResourceError::Capacity)?; + transfers.extend( + ordered + .into_iter() + .map(|(token, from, to, _)| (token, from, to)), + ); + self.custody.move_carrier_inventory( + source, + destination, + &mut transfers, + registry, + complete, + )?; + Ok(()) + } + + fn carrier_occupied_child( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + builder_id: ResourceHandleId, + _ordinal: u32, + input: ResourceHandleId, + child: crate::resource_custody::NativeCarrierChild, + ) -> ResourceResult<()> { + self.carrier_unborrowed(input)?; + let Some(NativeResourceEntry::CarrierBuilder(mut builder)) = + self.handles.remove(&builder_id) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = (|| { + self.builder_root(&mut builder)?; + let node = self.layout.carriers().slots[builder.destination as usize].node; + let path = [child_path( + self.layout.carriers(), + node, + builder.selector, + child.index as usize, + )?]; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) + else { + return Err(NativeResourceError::WrongFamily); + }; + let result = match (root.custody.as_ref(), builder.custody.as_ref()) { + (Some(source), Some(destination)) => self.migrate_carrier_tree( + &mut root.tree, + source, + destination, + &path, + registry, + false, + ), + _ => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(error); + } + builder.children[child.index as usize] = Some(root.tree); + builder.acquisition.push(child.index as usize); + builder.next += 1; + Ok(()) + })(); + self.handles + .insert(builder_id, NativeResourceEntry::CarrierBuilder(builder)); + result + } + + pub(in crate::native_abi::resource) fn carrier_move_reserved_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + input: ResourceHandleId, + source: u32, + destination: u32, + source_frame: ResourceHandleId, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + let root = self.carrier_root(ordinary, source_frame, input, source)?; + self.validate_complete_tree(ordinary, &root.tree)?; + self.carrier_unborrowed(input)?; + self.carrier_slot_empty(frame, destination)?; + if root.tree.node != self.layout.carriers().slots[destination as usize].node { + return Err(NativeResourceError::WrongFamily); + } + if root.custody.is_none() { + return self.carrier_move_reserved( + ordinary, + input, + source, + destination, + source_frame, + frame, + output, + ); + } + let slot = self.layout.carrier_slot(destination)?; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let destination_root = self.custody.begin_carrier_root(&header.token, &slot)?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) else { + unreachable!() + }; + let result = match root.custody.as_ref() { + Some(source_root) => self.migrate_carrier_tree( + &mut root.tree, + source_root, + &destination_root, + &[], + registry, + true, + ), + None => Err(NativeResourceError::WrongFamily), + }; + if let Err(error) = result { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + self.custody.retire_carrier_metadata(&destination_root)?; + return Err(error); + } + root.custody = Some(destination_root); + root.frame = frame; + root.slot = destination; + root.generation = output.raw(); + self.handles + .insert(output, NativeResourceEntry::CarrierRoot(root)); + Ok(output) + } + + pub(in crate::native_abi::resource) fn carrier_transfer_with_registry( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + frame: ResourceHandleId, + operation: u32, + input: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let Op::Transfer { + source, + destination, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let target = self.activation_frame( + frame, + self.layout.carriers().slots[destination as usize].frame, + )?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.carrier_move_reserved_with_registry( + ordinary, + registry, + input, + source, + destination, + frame, + target, + output, + ) + } + + pub(in crate::native_abi::resource) fn carrier_retire_with_registry( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + operation: u32, + input: ResourceHandleId, + ) -> ResourceResult<()> { + let Op::Retire { source, .. } = self.carrier_operation(operation, frame)? else { + return Err(NativeResourceError::WrongOperation); + }; + let root = self.carrier_root(ordinary, frame, input, source)?; + self.carrier_unborrowed(input)?; + if root.custody.is_none() { + return self.carrier_retire(ordinary, frame, operation, input); + } + let mut tokens = Vec::new(); + owners(self.layout.carriers(), &root.tree, &[], &mut tokens)?; + self.custody.preflight_carrier_retirement( + root.custody + .as_ref() + .ok_or(NativeResourceError::WrongFamily)?, + &tokens, + registry, + )?; + let pending = self.prepare_carrier_cleanup(std::iter::once(&root.tree))?; + let Some(NativeResourceEntry::CarrierRoot(mut root)) = self.handles.remove(&input) else { + unreachable!() + }; + let preparation = retirement_owners(self.layout.carriers(), &mut root.tree); + let cleanup = match preparation { + Ok(mut owners) => match root.custody.as_ref() { + Some(custody) => { + self.custody + .close_carrier_inventory(custody, &mut owners, registry) + } + None => { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(NativeResourceError::WrongFamily); + } + }, + Err(error) => { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return Err(error); + } + }; + if root + .custody + .as_ref() + .is_none_or(|custody| !self.custody.carrier_retirement_completed(custody)) + { + self.handles + .insert(input, NativeResourceEntry::CarrierRoot(root)); + return match self.cleanup(cleanup) { + Err(error) => Err(error), + Ok(()) => Err(NativeResourceError::WrongFamily), + }; + } + // Core marks each owner retired before invoking its trusted finalizer. + let core_result = self.cleanup(cleanup); + let ordinary_result = self.drop_carrier_tree_prepared(ordinary, root.tree, pending); + core_result?; + ordinary_result + } + + pub(in crate::native_abi::resource) fn prepare_carrier_frame( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + ) -> ResourceResult { + if self.handles.values().any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame != frame + && matches!(self.handles.get(&l.root), Some(NativeResourceEntry::CarrierRoot(r)) if r.frame == frame))) { + return Err(CustodyError::ActiveBorrow.into()); + } + loop { + let next = self.handles.iter().find_map(|(id, e)| + (matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame == frame) + && !self.handles.values().any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.parent == Some(*id)))).then_some(*id)); + let Some(id) = next else { + break; + }; + self.carrier_end_loan(frame, id)?; + } + if self + .handles + .values() + .any(|e| matches!(e, NativeResourceEntry::CarrierLoan(l) if l.frame == frame)) + { + return Err(CustodyError::ActiveBorrow.into()); + } + let mut holders = Vec::new(); + holders + .try_reserve_exact(self.handles.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (id, entry) in &self.handles { + let pending = match entry { + NativeResourceEntry::CarrierRoot(root) if root.frame == frame => { + self.carrier_root(ordinary, frame, *id, root.slot)?; + self.prepare_carrier_cleanup(std::iter::once(&root.tree))? + } + NativeResourceEntry::CarrierBuilder(builder) if builder.frame == frame => { + self.validate_carrier_builder(ordinary, builder)?; + let mut pending = + self.prepare_carrier_cleanup(builder.children.iter().flatten())?; + pending + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + pending + } + _ => continue, + }; + holders.push((*id, pending)); + } + holders.sort_by_key(|(id, _)| std::cmp::Reverse(id.raw())); + Ok(PreparedCarrierFrame { frame, holders }) + } + + pub(in crate::native_abi::resource) fn retire_carrier_frame_prepared( + &mut self, + ordinary: &mut values::NativeValues, + prepared: PreparedCarrierFrame, + ) -> ResourceResult<()> { + // This runs only after unified Core end_frame has discharged every scalar + // and carrier ticket in one acquisition order. It never finalizes owners. + let mut first = None; + for (id, pending) in prepared.holders { + match self.handles.get(&id) { + Some(NativeResourceEntry::CarrierRoot(r)) if r.frame == prepared.frame => { + if let Some(root) = &r.custody { + self.custody.retire_carrier_metadata(root)?; + } + } + Some(NativeResourceEntry::CarrierBuilder(b)) if b.frame == prepared.frame => { + if let Some(root) = &b.custody { + self.custody.retire_carrier_metadata(root)?; + } + } + _ => return Err(NativeResourceError::WrongFrame), + } + let tree = match self.handles.remove(&id) { + Some(NativeResourceEntry::CarrierRoot(r)) => r.tree, + Some(NativeResourceEntry::CarrierBuilder(b)) => NativeCarrierTree { + node: self.layout.carriers().slots[b.destination as usize].node, + selector: b.selector, + ordinary: None, + pending_depth: 0, + owner: None, + children: b.children, + acquisition: b.acquisition, + }, + _ => unreachable!(), + }; + if let Err(error) = self.drop_carrier_tree_prepared(ordinary, tree, pending) { + first.get_or_insert(error); + } + } + first.map_or(Ok(()), Err) + } +} + +pub(in crate::native_abi::resource) struct PreparedCarrierFrame { + frame: ResourceHandleId, + holders: Vec<(ResourceHandleId, Vec)>, +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs b/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs new file mode 100644 index 00000000..40d4de3b --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_sum.rs @@ -0,0 +1,392 @@ +//! V3 adapters keep absent legacy sums attached to their carrier projection. +use super::carriers::{ + NativeCarrierLoan, NativeCarrierSumOrigin, NativeCarrierTree, selected, take_selected, +}; +use super::*; +use crate::resource_custody::{ + NativeCarrierLoanSource, NativeCarrierNode, NativeCarrierOperation, NativeCarrierPath, + NativeCarrierSelector, NativeCarrierSource, +}; + +#[derive(Clone, Copy)] +enum AbsentCompanion { + None, + Fail { shape: u32, bits: u64 }, +} + +fn adapter_projection_matches( + expected: &[NativeCarrierPath], + actual: &[NativeCarrierPath], +) -> bool { + expected.len() == actual.len() + && expected + .iter() + .zip(actual) + .all(|(expected, actual)| match (expected, actual) { + ( + NativeCarrierPath::List(NativeCarrierSelector::Dynamic), + NativeCarrierPath::List(NativeCarrierSelector::Static(_)), + ) + | ( + NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic), + NativeCarrierPath::MapKey(NativeCarrierSelector::Static(_)), + ) + | ( + NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic), + NativeCarrierPath::MapValue(NativeCarrierSelector::Static(_)), + ) => true, + _ => expected == actual, + }) +} + +impl NativeResourceState { + fn carrier_absence(&self, tree: &NativeCarrierTree) -> ResourceResult { + match self.layout.carriers().node(tree.node)? { + NativeCarrierNode::Optional { .. } + if tree.selector == 0 && tree.children.is_empty() && tree.ordinary.is_none() => + { + Ok(AbsentCompanion::None) + } + NativeCarrierNode::Result { fail, .. } + if tree.selector == 0 && tree.children.len() == 1 && tree.ordinary.is_none() => + { + let child = tree.children[0] + .as_ref() + .ok_or(NativeResourceError::WrongOperation)?; + let NativeCarrierNode::Ordinary { shape } = self.layout.carriers().node(*fail)? + else { + return Err(NativeResourceError::UnsupportedCarrierCustody); + }; + if child.node != *fail || !child.children.is_empty() || child.pending_depth != 0 { + return Err(NativeResourceError::WrongFamily); + } + let (actual, bits) = child.ordinary.ok_or(NativeResourceError::WrongFamily)?; + if actual != *shape { + return Err(NativeResourceError::WrongFamily); + } + Ok(AbsentCompanion::Fail { + shape: *shape, + bits, + }) + } + NativeCarrierNode::Optional { .. } | NativeCarrierNode::Result { .. } => { + Err(NativeResourceError::UnsupportedCarrierCustody) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + + /// This checks the private origin as well as the public legacy sum metadata. + /// An ended adapter remains sealed, but cannot be used as a live view. + pub(super) fn carrier_adapter_validate( + &self, + shell: ResourceHandleId, + sum_loan: ResourceHandleId, + ) -> ResourceResult<()> { + let origin = self + .carrier_adapters + .get(&shell) + .ok_or(NativeResourceError::WrongOperation)?; + if origin.sum_loan != sum_loan { + return Err(NativeResourceError::WrongOperation); + } + let Some(NativeResourceEntry::SumLoan(sum_lease)) = self.handles.get(&sum_loan) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::CarrierLoan(loan)) = self.handles.get(&origin.loan) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&shell) else { + return Err(NativeResourceError::InvalidHandle); + }; + self.frame(loan.frame)?; + self.frame(root.frame)?; + if loan.generation != root.generation + || root.generation != loan.root.raw() + || loan.operation != origin.operation + || loan.incoming.is_some() + || sum_lease.shell != shell + || sum_lease.token.is_some() + || sum_lease.frame != loan.frame + || sum.frame != loan.frame + || sum_lease.source_frame != sum.frame + || sum_lease.source_slot != sum.slot + || sum_lease.shape != sum.shape + || sum_lease.borrow_operation != origin.operation + { + return Err(NativeResourceError::WrongOperation); + } + if let Some(parent) = loan.parent { + let Some(NativeResourceEntry::CarrierLoan(parent)) = self.handles.get(&parent) else { + return Err(NativeResourceError::InvalidHandle); + }; + self.frame(parent.frame)?; + if parent.root != loan.root + || parent.generation != loan.generation + || !loan.path.starts_with(&parent.path) + { + return Err(NativeResourceError::WrongOperation); + } + } + let NativeOperation::Carrier { record } = *self.operation(origin.operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let row = self + .layout + .carriers() + .operations + .get(usize::try_from(record).map_err(|_| NativeResourceError::WrongOperation)?) + .ok_or(NativeResourceError::WrongOperation)?; + let NativeCarrierOperation::AdaptSum { + frame: template, + source, + path, + sum_slot, + lease_frame, + borrowed: true, + } = &row.operation + else { + return Err(NativeResourceError::WrongOperation); + }; + // A caller lease remains valid while its callee is the active frame. + // Checking an origin must not require the issuing frame to be on top. + if self.layout.wire_version() != 3 + || row.site + != self.layout.operations()[usize::try_from(origin.operation) + .map_err(|_| NativeResourceError::WrongOperation)?] + .site() + || sum.slot != *sum_slot + || template != lease_frame + || *lease_frame != self.frame(loan.frame)?.template + { + return Err(NativeResourceError::WrongOperation); + } + let tail = match source { + NativeCarrierSource::Slot(slot) => { + if root.slot != *slot || loan.parent.is_some() { + return Err(NativeResourceError::WrongOperation); + } + loan.path.as_slice() + } + NativeCarrierSource::Loan(source) => { + let parent_handle = loan.parent.ok_or(NativeResourceError::WrongOperation)?; + let Some(NativeResourceEntry::CarrierLoan(parent)) = + self.handles.get(&parent_handle) + else { + return Err(NativeResourceError::InvalidHandle); + }; + match source { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + if parent.operation != *operation || parent.incoming.is_some() { + return Err(NativeResourceError::WrongOperation); + } + } + NativeCarrierLoanSource::IncomingViewFormal { scope, parameter } => { + let Some((actual_scope, actual_parameter, call)) = parent.incoming else { + return Err(NativeResourceError::WrongOperation); + }; + let active_call = self.source_call(call)?; + if actual_scope != *scope + || actual_parameter != *parameter + || self.frame(parent.frame)?.template != *scope + || active_call.scope != Some(parent.frame) + || active_call.phase != source::SourcePhase::Entered + { + return Err(NativeResourceError::WrongOperation); + } + } + } + loan.path + .get(parent.path.len()..) + .ok_or(NativeResourceError::WrongOperation)? + } + }; + if !adapter_projection_matches(path, tail) { + return Err(NativeResourceError::WrongOperation); + } + let tree = selected(&root.tree, &loan.path)?; + if !self.layout.carrier_equivalent_sum(tree.node, sum.shape) { + return Err(NativeResourceError::WrongFamily); + } + match (self.carrier_absence(tree)?, &sum.payload) { + (AbsentCompanion::None, NativeSumPayload::None) => Ok(()), + ( + AbsentCompanion::Fail { shape, bits }, + NativeSumPayload::Fail { + shape: actual, + string, + }, + ) if shape == *actual && bits == *string => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + } + } + + pub(super) fn carrier_adapter_source( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + operation: u32, + ) -> ResourceResult<()> { + let Some(NativeResourceEntry::SumLoan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if self.layout.wire_version() != 3 || loan.borrow_operation != operation { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_adapter_validate(loan.shell, handle)?; + if self.activation_frame(frame, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + + pub(super) fn carrier_adapt( + &mut self, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + dynamic: u64, + ) -> ResourceResult { + self.running(ordinary)?; + let NativeCarrierOperation::AdaptSum { + source, + path, + sum_slot, + lease_frame, + borrowed, + .. + } = self.carrier_operation(operation, frame)? + else { + return Err(NativeResourceError::WrongOperation); + }; + if lease_frame != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + // A borrowed adapter's shell is owned solely by this lease frame. + self.slot_empty(frame, sum_slot)?; + let shape = self.layout.slots() + [usize::try_from(sum_slot).map_err(|_| NativeResourceError::WrongOperation)?] + .shape(); + let (root_handle, generation, full_path, parent) = + self.carrier_location(ordinary, frame, handle, source, &path, dynamic)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&root_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let tree = selected(&root.tree, &full_path)?; + if !self.layout.carrier_equivalent_sum(tree.node, shape) { + return Err(NativeResourceError::WrongFamily); + } + let companion = self.carrier_absence(tree)?; + if let AbsentCompanion::Fail { shape, bits } = companion { + ordinary + .validate_resource_ordinary(bits, &self.layout, shape) + .map_err(ordinary_error)?; + } + if borrowed { + self.carrier_adapters + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let mut ids = self.reserve(3)?; + let shell = next_handle(&mut ids)?; + let sum_loan = next_handle(&mut ids)?; + let carrier_loan = next_handle(&mut ids)?; + // The only owned companion copied by this bridge is an exact String. + let payload = match companion { + AbsentCompanion::None => NativeSumPayload::None, + AbsentCompanion::Fail { shape, bits } => NativeSumPayload::Fail { + shape, + string: ordinary + .clone_resource_string_companion(&self.layout, shape, bits) + .map_err(ordinary_error)?, + }, + }; + self.handles.insert( + carrier_loan, + NativeResourceEntry::CarrierLoan(NativeCarrierLoan { + frame, + operation, + root: root_handle, + generation, + path: full_path, + parent, + incoming: None, + }), + ); + self.handles.insert( + shell, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot: sum_slot, + shape, + payload, + }), + ); + self.handles.insert( + sum_loan, + NativeResourceEntry::SumLoan(NativeSumLoan { + frame, + borrow_operation: operation, + shell, + source_frame: frame, + source_slot: sum_slot, + shape, + token: None, + }), + ); + self.carrier_adapters.insert( + shell, + NativeCarrierSumOrigin { + loan: carrier_loan, + operation, + sum_loan, + }, + ); + Ok(sum_loan) + } else { + if !matches!(source, NativeCarrierSource::Slot(_)) || parent.is_some() { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_unborrowed(root_handle)?; + let mut ids = self.reserve(1)?; + let shell = next_handle(&mut ids)?; + if full_path.is_empty() { + let Some(NativeResourceEntry::CarrierRoot(_)) = self.handles.remove(&root_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + } else { + let Some(NativeResourceEntry::CarrierRoot(root)) = + self.handles.get_mut(&root_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + let _tree = take_selected(&mut root.tree, &full_path)?; + } + let payload = match companion { + AbsentCompanion::None => NativeSumPayload::None, + AbsentCompanion::Fail { shape, bits } => NativeSumPayload::Fail { + shape, + string: bits, + }, + }; + self.handles.insert( + shell, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot: sum_slot, + shape, + payload, + }), + ); + Ok(shell) + } + } +} + +#[cfg(test)] +#[path = "carriers_sum_tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs b/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs new file mode 100644 index 00000000..38eb6a87 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_sum_tests.rs @@ -0,0 +1,432 @@ +use super::*; +use crate::resource_custody::{ + NativeCarrierChildSource, NativeCarrierOperation as CarrierOp, NativeCarrierPath as Path, + NativeCarrierSelector as Selector, NativeCarrierSource, +}; + +fn carrier_op(state: &NativeResourceState, select: impl Fn(&CarrierOp) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(ordinal, row)| { + let NativeOperation::Carrier { record } = row.operation() else { + return None; + }; + select(&state.layout.carriers().operations[usize::try_from(*record).unwrap()].operation) + .then(|| u32::try_from(ordinal).unwrap()) + }) + .expect("fixture carrier operation") +} + +fn legacy_op(state: &NativeResourceState, select: impl Fn(&NativeOperation) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(ordinal, row)| select(row.operation()).then(|| u32::try_from(ordinal).unwrap())) + .expect("fixture legacy operation") +} + +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 0, + scope: 0, + } +} + +fn bridge_context(operation: impl FnOnce(&AuthenticatedResourceContext)) { + super::super::tests::context(|auth| { + install_disabled( + auth, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + operation(auth); + }); +} + +fn start_record( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId, ResourceHandleId)> { + let attempt = state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + let none_op = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 0, selector: 0, children, .. } if children.is_empty()) + }); + let none_builder = state.carrier_construct_begin(ordinary, frame, none_op)?; + let none = state.carrier_commit(ordinary, none_builder)?; + let record_op = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, NativeCarrierChildSource::Move { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, record_op)?; + state.carrier_child(ordinary, builder, 0, 7)?; + state.carrier_child(ordinary, builder, 1, none.raw())?; + let record = state.carrier_commit(ordinary, builder)?; + let transfer = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::Transfer { + source: 2, + destination: 9, + .. + } + ) + }); + let record = state.carrier_transfer(ordinary, frame, transfer, record)?; + Ok((attempt, frame, record)) +} + +fn finish( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt: ResourceHandleId, + frame: ResourceHandleId, +) -> ResourceResult<()> { + state.end_operation_frame(ordinary, registry, frame)?; + assert!(state.carrier_adapters.is_empty()); + assert!(!state.handles.values().any(|entry| matches!( + entry, + NativeResourceEntry::CarrierRoot(_) + | NativeResourceEntry::CarrierLoan(_) + | NativeResourceEntry::CarrierBuilder(_) + | NativeResourceEntry::Sum(_) + | NativeResourceEntry::SumLoan(_) + ))); + assert_eq!( + state + .complete_entry(ordinary, registry, attempt, 0, false)? + .selected_kind, + 0 + ); + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) +} + +#[test] +fn adapter_projection_rejects_a_different_same_shape_sibling() { + assert!(!adapter_projection_matches( + &[Path::Field(0)], + &[Path::Field(1)] + )); + assert!(!adapter_projection_matches( + &[Path::EnumPayload { + variant: 0, + field: 0 + }], + &[Path::EnumPayload { + variant: 1, + field: 0 + }], + )); + assert!(!adapter_projection_matches( + &[Path::MachinePayload { state: 0, field: 0 }], + &[Path::MachinePayload { state: 1, field: 0 }], + )); +} + +#[test] +fn adapter_dynamic_selection_keeps_its_collection_projection_kind() { + assert!(adapter_projection_matches( + &[Path::Field(2), Path::MapValue(Selector::Dynamic)], + &[Path::Field(2), Path::MapValue(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::Field(2), Path::MapValue(Selector::Dynamic)], + &[Path::Field(2), Path::MapKey(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::List(Selector::Static(4))], + &[Path::List(Selector::Static(5))], + )); + assert!(!adapter_projection_matches( + &[Path::List(Selector::Dynamic)], + &[Path::List(Selector::Static(5)), Path::Some], + )); +} + +#[test] +fn borrowed_adapter_survives_a_source_scope_and_pins_then_releases_the_exact_root() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: true, + .. + } + ) + }); + let loan = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + let Some(NativeResourceEntry::SumLoan(sum)) = state.handles.get(&loan) else { + panic!("adapter sum loan"); + }; + let shell = sum.shell; + let origin_loan = state.carrier_adapters[&shell].loan; + assert!(matches!(state.handles.get(&origin_loan), + Some(NativeResourceEntry::CarrierLoan(loan)) if loan.root == record)); + let reverse = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::Transfer { + source: 9, + destination: 2, + .. + } + ) + }); + assert_eq!( + state.carrier_transfer(ordinary, frame, reverse, record), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + + let invoke = legacy_op(state, |operation| { + matches!( + operation, + NativeOperation::InvokeSourceFunction { callee: 1, .. } + ) + }); + let call = state.source_prepare(ordinary, frame, invoke)?; + state.source_actual(ordinary, registry, call, 0, 0, loan.raw())?; + let scope = state.source_enter(ordinary, registry, call)?; + assert_eq!(state.source_parameter(ordinary, scope, 0)?, loan.raw()); + let tag = legacy_op(state, |operation| { + matches!( + operation, + NativeOperation::ObserveSumView { + frame: 2, + source: NativeSumLoanSource::IncomingViewFormal { + scope: 2, + parameter: 0, + }, + } + ) + }); + assert_eq!(state.sum_view_tag(ordinary, registry, tag, scope, loan)?, 0); + let complete = legacy_op(state, |operation| { + matches!(operation, NativeOperation::Complete { frame: 2 }) + }); + state.scope_complete(ordinary, registry, scope, complete, 0)?; + state.source_status(ordinary, call)?; + + let end = legacy_op(state, |operation| { + matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt) + }); + state.sum_borrow_end(registry, end, frame, loan)?; + assert!(!state.handles.contains_key(&loan)); + assert!(!state.handles.contains_key(&origin_loan)); + assert!(state.carrier_adapters.contains_key(&shell)); + let before_refusals = state.counts(ordinary, registry); + assert_eq!(before_refusals.loan_handles, 0); + assert_eq!(before_refusals.owner_handles, 2); + assert_eq!(before_refusals.provisional, 2); + assert_eq!(before_refusals.frame_handles, 2); + assert_eq!(before_refusals.registry, 0); + assert!(matches!(state.handles.get(&shell), + Some(NativeResourceEntry::Sum(sum)) + if sum.frame == frame && sum.slot == 0 && sum.shape == 5 + && matches!(sum.payload, NativeSumPayload::None))); + // The seal denies the owning family even after its exact loan ends. + assert_eq!( + state.carrier(shell, 0, registry), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.sum_tag(shell), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + state.sum_borrow_end(registry, end, frame, loan), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!(state.counts(ordinary, registry), before_refusals); + assert!(state.carrier_adapters.contains_key(&shell)); + assert_eq!( + state.carrier_root(ordinary, frame, record, 9)?.generation, + record.raw() + ); + state.carrier_transfer(ordinary, frame, reverse, record)?; + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn owned_adapter_extraction_returns_to_a_carrier_once() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: false, + .. + } + ) + }); + let shell = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + assert_eq!(state.sum_tag(shell)?, 0); + assert!(!state.carrier_adapters.contains_key(&shell)); + assert_eq!( + state.carrier_adapt(ordinary, frame, adapt, record, 0), + Err(NativeResourceError::WrongOperation) + ); + let construct = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, + NativeCarrierChildSource::LeafSum { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, construct)?; + state.carrier_child(ordinary, builder, 0, 19)?; + state.carrier_child(ordinary, builder, 1, shell.raw())?; + let restored = state.carrier_commit(ordinary, builder)?; + assert!(!state.handles.contains_key(&shell)); + assert_eq!( + state.sum_tag(shell), + Err(NativeResourceError::InvalidHandle) + ); + state.carrier_root(ordinary, frame, restored, 2)?; + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn borrowed_adapter_refuses_forged_projection_and_legacy_owned_child_adoption() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let adapt = carrier_op(state, |operation| { + matches!( + operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Slot(9), + borrowed: true, + .. + } + ) + }); + let loan = state.carrier_adapt(ordinary, frame, adapt, record, 0)?; + let Some(NativeResourceEntry::SumLoan(sum)) = state.handles.get(&loan) else { + panic!("adapter sum loan"); + }; + let shell = sum.shell; + let origin_loan = state.carrier_adapters[&shell].loan; + let Some(NativeResourceEntry::CarrierLoan(origin)) = + state.handles.get_mut(&origin_loan) + else { + panic!("adapter carrier loan"); + }; + origin.path[0] = Path::Field(0); + assert_eq!( + state.carrier_adapter_validate(shell, loan), + Err(NativeResourceError::WrongOperation) + ); + assert!(state.handles.contains_key(&loan)); + assert!(state.handles.contains_key(&record)); + let Some(NativeResourceEntry::CarrierLoan(origin)) = + state.handles.get_mut(&origin_loan) + else { + panic!("adapter carrier loan"); + }; + origin.path[0] = Path::Field(1); + state.carrier_adapter_validate(shell, loan)?; + let end = legacy_op(state, |operation| { + matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt) + }); + state.sum_borrow_end(registry, end, frame, loan)?; + + let construct = carrier_op(state, |operation| { + matches!(operation, + CarrierOp::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, + NativeCarrierChildSource::LeafSum { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, construct)?; + state.carrier_child(ordinary, builder, 0, 23)?; + assert_eq!( + state.carrier_child(ordinary, builder, 1, shell.raw()), + Err(NativeResourceError::WrongFamily) + ); + assert!(state.carrier_adapters.contains_key(&shell)); + assert!(state.handles.contains_key(&shell)); + assert!(state.handles.contains_key(&record)); + finish(state, ordinary, registry, attempt, frame) + }) + .unwrap(); + }); +} + +#[test] +fn nested_adapter_keeps_each_parent_lease_until_its_child_ends() { + bridge_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, record) = start_record(state, ordinary, registry)?; + let full_op = carrier_op(state, |operation| matches!(operation, + CarrierOp::Borrow { source: NativeCarrierSource::Slot(9), path, .. } + if path.is_empty())); + let full = state.carrier_borrow(ordinary, frame, full_op, record, 0)?; + let child_op = carrier_op(state, |operation| matches!(operation, + CarrierOp::Borrow { + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation: parent }), + path, .. + } if *parent == full_op && path.as_slice() == &[Path::Field(1)])); + let child = state.carrier_borrow(ordinary, frame, child_op, full, 0)?; + let adapt = carrier_op(state, |operation| matches!(operation, + CarrierOp::AdaptSum { + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation: parent }), + path, borrowed: true, .. + } if *parent == child_op && path.is_empty())); + let loan = state.carrier_adapt(ordinary, frame, adapt, child, 0)?; + let child_end = carrier_op(state, |operation| matches!(operation, + CarrierOp::EndBorrow { borrow, .. } if *borrow == child_op)); + let full_end = carrier_op(state, |operation| matches!(operation, + CarrierOp::EndBorrow { borrow, .. } if *borrow == full_op)); + assert_eq!(state.carrier_end(frame, child_end, child), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow))); + assert_eq!(state.carrier_end(frame, full_end, full), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow))); + let tag = legacy_op(state, |operation| matches!(operation, + NativeOperation::ObserveSumView { + source: NativeSumLoanSource::CarrierProjected { operation: origin }, .. + } if *origin == adapt)); + assert_eq!(state.sum_view_tag(ordinary, registry, tag, frame, loan)?, 0); + let end = legacy_op(state, |operation| matches!(operation, + NativeOperation::EndSumBorrow { borrow, .. } if *borrow == adapt)); + state.sum_borrow_end(registry, end, frame, loan)?; + state.carrier_end(frame, child_end, child)?; + state.carrier_end(frame, full_end, full)?; + let reverse = carrier_op(state, |operation| matches!(operation, + CarrierOp::Transfer { source: 9, destination: 2, .. })); + state.carrier_transfer(ordinary, frame, reverse, record)?; + finish(state, ordinary, registry, attempt, frame) + }).unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs b/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs new file mode 100644 index 00000000..f093e8d7 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/carriers_tests.rs @@ -0,0 +1,760 @@ +//! Host protocol negative controls. These tests are not Source admission evidence. +use super::*; + +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 0, + scope: 0, + } +} +fn context(test: impl FnOnce(&AuthenticatedResourceContext)) { + super::super::tests::context(|auth| { + install_disabled( + auth, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + test(auth); + }); +} +fn refused_cleanup_context(test: impl FnOnce(&AuthenticatedResourceContext)) { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let status = unsafe { + jett_rt_v1_context_create( + JETT_RUNTIME_ABI_VERSION_V1, + &mut *context, + &mut JettRuntimeResultV1::ok(), + ) + }; + assert_eq!(status, JettRuntimeStatusV1::OK); + let authenticated = AuthenticatedResourceContext::acquire(&*context).unwrap(); + install_disabled( + &authenticated, + &crate::resource_custody::carrier_runtime_fixture_bytes(), + entry(), + ) + .unwrap(); + test(&authenticated); + drop(authenticated); + let status = + unsafe { jett_rt_v1_context_destroy(&mut *context, &mut JettRuntimeResultV1::ok()) }; + assert_eq!(status, JettRuntimeStatusV1::INVALID_ARGUMENT); +} +fn op(state: &NativeResourceState, predicate: impl Fn(&Op) -> bool) -> u32 { + state + .layout + .operations() + .iter() + .enumerate() + .find_map(|(i, row)| { + let NativeOperation::Carrier { record } = row.operation() else { + return None; + }; + predicate(&state.layout.carriers().operations[*record as usize].operation) + .then(|| u32::try_from(i).unwrap()) + }) + .expect("fixture carrier operation") +} +fn start( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId)> { + let attempt = state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + Ok((attempt, frame)) +} +fn none( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + slot: u32, +) -> ResourceResult { + let operation = op( + state, + |r| matches!(r,Op::Construct{destination,selector:0,children,..} if *destination==slot&&children.is_empty()), + ); + let builder = state.carrier_construct_begin(ordinary, frame, operation)?; + state.carrier_commit(ordinary, builder) +} +fn record( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + frame: ResourceHandleId, + marker: u64, +) -> ResourceResult { + let optional = none(state, ordinary, frame, 0)?; + let operation = op( + state, + |r| matches!(r,Op::Construct{destination:2,children,..} if children.iter().any(|c|matches!(c.source,ChildSource::Move{slot:0}))), + ); + let builder = state.carrier_construct_begin(ordinary, frame, operation)?; + state.carrier_child(ordinary, builder, 0, marker)?; + state.carrier_child(ordinary, builder, 1, optional.raw())?; + state.carrier_commit(ordinary, builder) +} +fn finish( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + attempt: ResourceHandleId, +) -> ResourceResult<()> { + let result = state.complete_entry(ordinary, registry, attempt, 0, false)?; + assert_eq!(result.selected_kind, 0); + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) +} + +#[test] +fn staged_constructor_checks_lexical_prefix_before_consuming_children() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,frame)=start(state,ordinary,registry)?;let optional=none(state,ordinary,frame,0)?; + let constructor=op(state,|r|matches!(r,Op::Construct{destination:2,children,..} if children.iter().any(|c|matches!(c.source,ChildSource::Move{slot:0})))); + let builder=state.carrier_construct_begin(ordinary,frame,constructor)?; + assert_eq!(state.carrier_child(ordinary,builder,1,optional.raw()),Err(NativeResourceError::WrongOperation)); + assert_eq!(state.carrier_commit(ordinary,builder),Err(NativeResourceError::WrongOperation)); + assert!(state.handles.contains_key(&optional));state.carrier_child(ordinary,builder,0,41)?; + state.carrier_child(ordinary,builder,1,optional.raw())?;let root=state.carrier_commit(ordinary,builder)?; + assert!(!state.handles.contains_key(&builder));assert!(!state.handles.contains_key(&optional)); + assert_eq!(state.carrier_commit(ordinary,builder),Err(NativeResourceError::WrongFamily)); + assert_eq!(state.carrier_root(ordinary,frame,root,2)?.tree.node,8);assert_eq!(registry.live_count(),0); + finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +#[test] +fn transfer_issues_a_fresh_generation_and_preserves_the_exact_nominal_stamp() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,frame)=start(state,ordinary,registry)?;let input=record(state,ordinary,frame,17)?; + let transfer=op(state,|r|matches!(r,Op::Transfer{source:2,destination:9,..})); + let output=state.carrier_transfer(ordinary,frame,transfer,input)?;assert_ne!(input,output); + assert_eq!(state.carrier_root(ordinary,frame,input,2).err(),Some(NativeResourceError::WrongFamily)); + let observer=op(state,|r|matches!(r,Op::Observe{source:Source::Slot(9),path,observation:Observation::Ordinary{shape:0,clone:false},..} if path==&[Path::Field(0)])); + assert_eq!(state.carrier_observe(ordinary,frame,observer,output,0)?,17); + assert_eq!(state.carrier_root(ordinary,frame,output,3).err(),Some(NativeResourceError::WrongFrame)); + let Some(NativeResourceEntry::CarrierRoot(root))=state.handles.get_mut(&output) else{panic!("root")};root.tree.node=16; + assert_eq!(state.carrier_root(ordinary,frame,output,9).err(),Some(NativeResourceError::WrongFrame)); + let Some(NativeResourceEntry::CarrierRoot(root))=state.handles.get_mut(&output) else{panic!("root")};root.tree.node=8; + finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +#[test] +fn carrier_scalar_task_transport_keeps_all_layers_across_typed_transfer_and_observation() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let optional = none(state, ordinary, frame, 0)?; + let constructor = op(state, |row| matches!(row, Op::Construct { destination: 2, children, .. } if children.iter().any(|child| matches!(child.source, ChildSource::Move { slot: 0 })))); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child_scalar_task(ordinary, builder, 0, 17, 3)?; + assert_eq!(state.carrier_child_scalar_task(ordinary, builder, 1, optional.raw(), 1), Err(NativeResourceError::WrongOperation)); + assert!(state.handles.contains_key(&optional), "a refused scalar endpoint must not adopt another family"); + state.carrier_child(ordinary, builder, 1, optional.raw())?; + let input = state.carrier_commit(ordinary, builder)?; + let transfer = op(state, |row| matches!(row, Op::Transfer { source: 2, destination: 9, .. })); + let output = state.carrier_transfer(ordinary, frame, transfer, input)?; + let observer = op(state, |row| matches!(row, Op::Observe { source: Source::Slot(9), path, observation: Observation::Ordinary { shape: 0, clone: false }, .. } if path == &[Path::Field(0)])); + assert_eq!(state.carrier_observe_scalar_task(ordinary, frame, observer, output, 0)?, (17, 3)); + assert_eq!(state.carrier_observe(ordinary, frame, observer, output, 0), Err(NativeResourceError::WrongFamily), "the old one-word endpoint cannot discard layers"); + assert_eq!(state.carrier_observe_scalar_task(ordinary, frame, observer, output, 0)?, (17, 3)); + finish(state, ordinary, registry, attempt) + }).unwrap(); + }); +} + +fn rejected_fake_leaf( + state: &mut NativeResourceState, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, +) -> ResourceResult<(ResourceHandleId, ResourceHandleId, ResourceHandleId)> { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = state.reserve(1)?; + let fake = next_handle(&mut ids)?; + state.handles.insert( + fake, + NativeResourceEntry::CarrierRoot(NativeCarrierRoot { + frame, + slot: 10, + generation: fake.raw(), + custody: None, + tree: NativeCarrierTree { + node: 3, + selector: 0, + ordinary: None, + pending_depth: 0, + owner: None, + children: vec![], + acquisition: vec![], + }, + }), + ); + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 0, + selector: 1, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + assert_eq!( + state.carrier_child(ordinary, builder, 0, fake.raw()), + Err(NativeResourceError::WrongFamily) + ); + let Some(NativeResourceEntry::CarrierBuilder(builder)) = state.handles.get(&builder) else { + panic!("builder") + }; + assert_eq!(builder.next, 0); + assert!(builder.acquisition.is_empty()); + assert!(state.handles.contains_key(&fake)); + assert_eq!(registry.live_count(), 0); + Ok((attempt, frame, fake)) +} + +#[test] +fn selected_live_leaf_refuses_before_a_fake_handle_is_consumed() { + refused_cleanup_context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, fake) = rejected_fake_leaf(state, ordinary, registry)?; + assert_eq!( + finish(state, ordinary, registry, attempt), + Err(NativeResourceError::WrongFamily) + ); + assert!(state.handles.contains_key(&fake)); + assert_eq!(state.active_frames.last(), Some(&frame)); + assert_eq!( + state.attempt.as_ref().unwrap().phase, + AttemptPhase::Completing + ); + assert!(ordinary.cleanup_failed); + assert_eq!(registry.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn restored_private_fake_leaf_allows_clean_builder_completion() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame, fake) = rejected_fake_leaf(state, ordinary, registry)?; + assert!(matches!( + state.carrier_root(ordinary, frame, fake, 10), + Err(NativeResourceError::WrongFamily) + )); + assert!(matches!( + state.handles.remove(&fake), + Some(NativeResourceEntry::CarrierRoot(_)) + )); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn partial_string_prefix_retires_once_after_a_later_stage_refuses() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = ordinary + .publish_resource_error("kept prefix".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 1, + selector: 0, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, text)?; + assert_eq!( + state.carrier_child(ordinary, builder, 1, 0), + Err(NativeResourceError::WrongOperation) + ); + assert!(!ordinary.is_empty()); + finish(state, ordinary, registry, attempt)?; + assert!( + ordinary + .drop_resource_typed_companion(&state.layout, 1, text) + .is_err() + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn latent_generic_state_and_refinement_metadata_never_synthesizes_live_custody() { + context(|auth| { + auth.with_resource(|state,ordinary,registry|{ + let (attempt,_frame)=start(state,ordinary,registry)?; + let scalar=||NativeCarrierTree{node:0,selector:0,ordinary:Some((0,17)),pending_depth:0,owner:None,children:vec![],acquisition:vec![]}; + let absent=||NativeCarrierTree{node:4,selector:0,ordinary:None,pending_depth:0,owner:None,children:vec![],acquisition:vec![]}; + let generic=NativeCarrierTree{node:13,selector:0,ordinary:None,pending_depth:0,owner:None,children:vec![Some(absent())],acquisition:vec![0]}; + state.validate_complete_tree(ordinary,&generic)?; + assert!(matches!(state.layout.carriers().node(13)?,Node::Struct{arguments,..} if arguments==&[3])); + assert!(matches!(state.layout.carriers().node(14)?,Node::Struct{arguments,..} if arguments==&[4])); + let state_value=NativeCarrierTree{node:11,selector:0,ordinary:None,pending_depth:0,owner:None,children:vec![Some(scalar())],acquisition:vec![0]};state.validate_complete_tree(ordinary,&state_value)?; + let wrong_state=NativeCarrierTree{selector:1,..state_value};assert!(state.validate_complete_tree(ordinary,&wrong_state).is_err()); + let refined=NativeCarrierTree{node:12,selector:0,ordinary:None,pending_depth:0,owner:None,children:vec![Some(scalar())],acquisition:vec![0]}; + assert_eq!(state.validate_complete_tree(ordinary,&refined),Err(NativeResourceError::UnprovedCarrierRefinement)); + assert_eq!(registry.live_count(),0);finish(state,ordinary,registry,attempt) + }).unwrap(); + }); +} + +#[test] +fn extraction_tombstones_each_path_once_and_iteration_retains_original_length() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let a = none(state, ordinary, frame, 0)?; + let b = none(state, ordinary, frame, 11)?; + let constructor = op( + state, + |r| matches!(r,Op::Construct{destination:6,children,..} if children.len()==2), + ); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, a.raw())?; + state.carrier_child(ordinary, builder, 1, b.raw())?; + let list = state.carrier_commit(ordinary, builder)?; + let length = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(6), + observation: Observation::Length, + .. + } + ) + }); + let extract = op(state, |r| { + matches!( + r, + Op::Extract { + source: 6, + destination: Destination::Slot(12), + .. + } + ) + }); + let reset = op(state, |r| matches!(r, Op::ResetIteration { source: 6, .. })); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + let first = state.carrier_extract(ordinary, frame, extract, list, 0)?; + assert_eq!(state.carrier_observe(ordinary, frame, length, list, 0)?, 2); + assert_eq!( + state.carrier_extract(ordinary, frame, extract, list, 0), + Err(NativeResourceError::WrongOperation) + ); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + assert!(!state.handles.contains_key(&ResourceHandleId::new(first)?)); + let second = state.carrier_extract(ordinary, frame, extract, list, 1)?; + assert_ne!(first, second); + state.carrier_reset_iteration(ordinary, frame, reset, list)?; + assert_eq!(state.carrier_observe(ordinary, frame, length, list, 0)?, 2); + assert_eq!( + state.carrier_extract(ordinary, frame, extract, list, 1), + Err(NativeResourceError::WrongOperation) + ); + let Some(NativeResourceEntry::CarrierRoot(root)) = state.handles.get(&list) else { + panic!("iterator") + }; + assert_eq!( + state.validate_complete_tree(ordinary, &root.tree), + Err(NativeResourceError::WrongOperation) + ); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn checked_machine_widening_preserves_state_and_enum_tag_uses_its_discriminant() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 13, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, 17)?; + let qualified = state.carrier_commit(ordinary, builder)?; + let qualify = op(state, |r| { + matches!( + r, + Op::QualifyMachine { + source: 13, + destination: 14, + machine: 10, + state: 0, + .. + } + ) + }); + let general = state.carrier_qualify(ordinary, frame, qualify, qualified)?; + assert_ne!(general, qualified); + assert_eq!( + state.carrier_root(ordinary, frame, qualified, 13).err(), + Some(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.carrier_root(ordinary, frame, general, 14)?.tree.node, + 10 + ); + let observe_state = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(14), + observation: Observation::State, + .. + } + ) + }); + assert_eq!( + state.carrier_observe(ordinary, frame, observe_state, general, 0)?, + 0 + ); + assert_eq!( + state.carrier_qualify(ordinary, frame, qualify, general), + Err(NativeResourceError::WrongFrame) + ); + let constructor = op(state, |r| { + matches!( + r, + Op::Construct { + destination: 15, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, 17)?; + let enumeration = state.carrier_commit(ordinary, builder)?; + let tag = op(state, |r| { + matches!( + r, + Op::Observe { + source: Source::Slot(15), + observation: Observation::Tag, + .. + } + ) + }); + assert_eq!( + state.carrier_observe(ordinary, frame, tag, enumeration, 0)?, + (-7i64) as u64 + ); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn forged_scalar_clone_flag_refuses_without_changing_the_parent_or_obligation() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let parent = record(state, ordinary, frame, 17)?; + let before = state.counts(ordinary, registry); + assert_eq!( + state.carrier_observe_ordinary(ordinary, 0, true, 17), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(state.counts(ordinary, registry), before); + let marker = state + .carrier_root(ordinary, frame, parent, 2)? + .tree + .children[0] + .as_ref() + .unwrap() + .ordinary + .unwrap(); + assert_eq!(marker, (0, 17)); + assert_eq!(state.carrier_observe_ordinary(ordinary, 0, false, 17)?, 17); + assert_eq!(state.carrier_observe_ordinary(ordinary, 3, false, 1)?, 1); + assert_eq!(state.carrier_observe_ordinary(ordinary, 2, false, 0)?, 0); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +#[test] +fn forged_string_alias_refuses_and_a_real_clone_retires_independently() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let mut ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = ordinary + .publish_resource_error("parent-owned text".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let constructor = op(state, |row| { + matches!( + row, + Op::Construct { + destination: 1, + selector: 0, + .. + } + ) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + state.carrier_child(ordinary, builder, 0, text)?; + let parent = state.carrier_commit(ordinary, builder)?; + let before = state.counts(ordinary, registry); + assert_eq!( + state.carrier_observe_ordinary(ordinary, 1, false, text), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(state.counts(ordinary, registry), before); + let cloned = state.carrier_observe_ordinary(ordinary, 1, true, text)?; + ordinary + .drop_resource_typed_companion(&state.layout, 1, cloned) + .map_err(ordinary_error)?; + ordinary + .validate_resource_ordinary(text, &state.layout, 1) + .map_err(ordinary_error)?; + assert!(state.carrier_root(ordinary, frame, parent, 1).is_ok()); + finish(state, ordinary, registry, attempt)?; + assert!( + ordinary + .drop_resource_typed_companion(&state.layout, 1, text) + .is_err() + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn unsupported_owning_observation_flags_refuse_before_inspecting_a_fake_handle() { + context(|auth| { + auth.with_resource(|state, ordinary, registry| { + let (attempt, _) = start(state, ordinary, registry)?; + let before = state.counts(ordinary, registry); + for shape in [5, 6] { + for clone in [false, true] { + assert_eq!( + state.carrier_observe_ordinary(ordinary, shape, clone, u64::MAX), + Err(NativeResourceError::UnsupportedCarrierObservation) + ); + } + } + assert_eq!(state.counts(ordinary, registry), before); + assert!(ordinary.is_empty()); + finish(state, ordinary, registry, attempt) + }) + .unwrap(); + }); +} + +unsafe extern "C" { + fn jett_rt_v1_resource_carrier_child_scalar_task( + context: *const JettRuntimeContextV1, + builder: u64, + ordinal: u32, + bits: u64, + pending_depth: u64, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_carrier_observe_scalar_task( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out_bits: *mut u64, + out_depth: *mut u64, + ) -> JettRuntimeStatusV1; +} + +#[test] +fn carrier_scalar_task_c_outputs_are_atomic_on_refusal_and_preserve_all_layers() { + // Refused operations record their first error, so each case owns a fresh attempt. + for case in 0..12 { + context(|auth| scalar_task_c_output_case(auth, case)); + } +} + +fn scalar_task_c_output_case(auth: &AuthenticatedResourceContext, case: u32) { + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + let (attempt, frame, builder, constructor, optional) = auth + .with_resource(|state, ordinary, registry| { + let (attempt, frame) = start(state, ordinary, registry)?; + let optional = none(state, ordinary, frame, 0)?; + let constructor = op(state, |row| { + matches!(row, Op::Construct { destination: 2, children, .. } + if children.iter().any(|child| matches!(child.source, ChildSource::Move { slot: 0 }))) + }); + let builder = state.carrier_construct_begin(ordinary, frame, constructor)?; + Ok((attempt, frame, builder, constructor, optional)) + }) + .unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_carrier_child_scalar_task(pointer, builder.raw(), 0, 17, 3) }, + JettRuntimeStatusV1::OK + ); + let (output, observer) = auth + .with_resource(|state, ordinary, _| { + state.carrier_child(ordinary, builder, 1, optional.raw())?; + let input = state.carrier_commit(ordinary, builder)?; + let transfer = op(state, |row| { + matches!( + row, + Op::Transfer { + source: 2, + destination: 9, + .. + } + ) + }); + let output = state.carrier_transfer(ordinary, frame, transfer, input)?; + let observer = op(state, |row| { + matches!(row, Op::Observe { source: Source::Slot(9), path, + observation: Observation::Ordinary { shape: 0, clone: false }, .. } + if path == &[Path::Field(0)]) + }); + Ok((output, observer)) + }) + .unwrap(); + let retired = JettRuntimeContextV1::retired(); + let mut words = [0x0123_4567_89ab_cdefu64, 0xfedc_ba98_7654_3210]; + let sentinel = words; + let bits = std::ptr::addr_of_mut!(words[0]); + let depth = std::ptr::addr_of_mut!(words[1]); + let (ctx, call_frame, operation, input, out_bits, out_depth) = match case { + 0 => ( + std::ptr::null(), + frame.raw(), + observer, + output.raw(), + bits, + depth, + ), + 1 => ( + &retired as *const _, + frame.raw(), + observer, + output.raw(), + bits, + depth, + ), + 2 => (pointer, frame.raw(), observer, 0, bits, depth), + 3 => (pointer, frame.raw(), observer, u64::MAX, bits, depth), + 4 => (pointer, 0, observer, output.raw(), bits, depth), + 5 => (pointer, u64::MAX, observer, output.raw(), bits, depth), + 6 => (pointer, frame.raw(), constructor, output.raw(), bits, depth), + 7 => ( + pointer, + frame.raw(), + observer, + output.raw(), + std::ptr::null_mut(), + depth, + ), + 8 => ( + pointer, + frame.raw(), + observer, + output.raw(), + bits, + std::ptr::null_mut(), + ), + 9 => (pointer, frame.raw(), observer, output.raw(), bits, bits), + 10 => (pointer, frame.raw(), observer, output.raw(), depth, depth), + _ => (pointer, frame.raw(), observer, output.raw(), bits, depth), + }; + let status = unsafe { + jett_rt_v1_resource_carrier_observe_scalar_task( + ctx, call_frame, operation, input, 0, out_bits, out_depth, + ) + }; + if case == 11 { + assert_eq!(status, JettRuntimeStatusV1::OK); + assert_eq!(words, [17, 3]); + } else { + let expected = if case <= 1 { + JettRuntimeStatusV1::INVALID_CONTEXT + } else { + JettRuntimeStatusV1::INVALID_ARGUMENT + }; + assert_eq!(status, expected, "case {case}"); + assert_eq!( + words, sentinel, + "both outputs remain unchanged in case {case}" + ); + } + let recorded_failure = (2..=6).contains(&case); + if !recorded_failure { + assert_eq!( + unsafe { + jett_rt_v1_resource_carrier_observe_scalar_task( + pointer, + frame.raw(), + observer, + output.raw(), + 0, + bits, + depth, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(words, [17, 3]); + } + auth.with_resource(|state, ordinary, registry| { + assert!( + state.handles.contains_key(&output), + "refusal never consumes the carrier" + ); + let completion = state.complete_entry(ordinary, registry, attempt, 0, false)?; + assert_eq!(completion.selected_kind, u32::from(recorded_failure)); + assert_eq!(completion.cleanup_status, 0); + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) + }) + .unwrap(); +} diff --git a/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs b/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs new file mode 100644 index 00000000..0e83ff79 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/entry_outcome_tests.rs @@ -0,0 +1,416 @@ +//! Root observation controls; distinct from Source/native linked acceptance. +use super::*; + +#[derive(Debug, PartialEq, Eq)] +struct Observation { + counts: NativeResourceCounts, + body: Option, + cleanup: Option, + root_body: Option, + ordinary: Option, + script_remaining: usize, +} + +fn observation(auth: &AuthenticatedResourceContext) -> Observation { + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let attempt = resource.attempt.as_ref().unwrap(); + Ok(Observation { + counts: resource.counts(&state.values, &state.resources), + body: attempt.body.as_ref().map(|failure| match failure { + NativeBodyFailure::Resource(error) => error.status().code(), + NativeBodyFailure::HostPanic => JettRuntimeStatusV1::PANIC.code(), + }), + cleanup: attempt.cleanup, + root_body: attempt.root_body_status, + ordinary: state + .values + .resource_failure() + .map(|failure| failure.status.code()), + script_remaining: resource.script_remaining(), + }) + }) + .unwrap() +} + +fn observe(auth: &AuthenticatedResourceContext, scope: ResourceHandleId) -> ResourceResult { + auth.with_state(|_, state| { + state + .resource_state + .as_ref() + .unwrap() + .entry_outcome(scope, 0, 3, 0) + }) +} + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn begin( + auth: &AuthenticatedResourceContext, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId) { + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + Ok((attempt, root, op)) + }) + .unwrap() +} + +#[test] +fn native_resource_entry_outcome_c_refusals_are_readonly_and_original_body_survives_first_error() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, root, op, _) = setup(auth, &f, &[]); + let initial = observation(auth); + let mut out = 9001; + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), initial); + + let child = auth + .with_resource(|s, v, r| { + let shell = s.absent_sum(v, op, f.create.unwrap())?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let child = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, child, 0)?)?; + let inner = s.begin_operation_frame(v, 4, child)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, child, f.complete_scope, 0)?; + let returned = s.return_publish(v, r, child, f.publish.unwrap(), provisional)?; + s.source_status(v, call)?; + s.sum_drop(v, r, f.close, op, returned)?; + Ok(child) + }) + .unwrap(); + let before_child_query = observation(auth); + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + child.raw(), + 1, + 4, + 3, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), before_child_query); + + auth.with_resource(|s, v, r| s.end_operation_frame(v, r, op)) + .unwrap(); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(auth)) }, 0); + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_scope_complete( + pointer(auth), + root.raw(), + f.complete_root, + 71, + ) + }, + JettRuntimeStatusV1::OK + ); + let completed_root = observation(auth); + assert_eq!(completed_root.root_body, Some(71)); + assert!(completed_root.body.is_some() && completed_root.ordinary.is_some()); + assert_eq!(completed_root.counts.frames, 0); + for (scope, function, signature, template) in [ + (root, 1, 3, 0), + (root, 0, 4, 0), + (root, 0, 3, 3), + (child, 0, 3, 0), + ] { + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + scope.raw(), + function, + signature, + template, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + assert_eq!(observation(auth), completed_root); + } + for _ in 0..2 { + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 71); + assert_eq!(observation(auth), completed_root); + } + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, out, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 1)); + assert_eq!(s.completed_messages(attempt)?.1, b"assertion failed"); + Ok(()) + }) + .unwrap(); + out = 9001; + assert_ne!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 9001); + }); +} + +#[test] +fn native_resource_entry_outcome_zero_is_independent_of_cleanup_panic_and_c_output_preflight() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + // Move the owner through the real helper Return into the root holder; + // otherwise Operation cleanup would finalize it before root completion. + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let child = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, child, 0)?)?; + let inner = s.begin_operation_frame(v, 4, child)?; + let destination = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, destination)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, child, f.complete_scope, 0)?; + s.return_publish(v, r, child, f.publish.unwrap(), provisional)?; + s.source_status(v, call) + }) + .unwrap(); + let before = observation(auth); + assert_eq!(before.counts.owners, 1); + for out in [ + ptr::null_mut::(), + pointer(auth).cast_mut().cast::(), + pointer(auth) + .cast::() + .wrapping_add(1) + .cast_mut() + .cast::(), + (usize::MAX - 3) as *mut u32, + ] { + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + out, + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(observation(auth), before); + } + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.scope_complete(v, r, root, f.complete_root, 0), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + Ok(()) + }) + .unwrap(); + let after = observation(auth); + assert_eq!(after.root_body, Some(0)); + assert_eq!(after.cleanup, Some(ResourceCleanupFailure::FinalizerPanic)); + assert_eq!( + ( + after.counts.owners, + after.counts.frames, + after.counts.registry + ), + (0, 0, 0) + ); + let mut out = 9001; + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_outcome( + pointer(auth), + root.raw(), + 0, + 3, + 0, + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(out, 0); + assert_eq!(observation(auth), after); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, out, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (0, 3)); + assert_ne!(completion.cleanup_status, 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_entry_outcome_is_retained_only_after_actual_root_retirement() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, root, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, op)?; + let mut foreign = ResourceRegistry::new(); + assert!(matches!( + s.scope_complete(v, &mut foreign, root, f.complete_root, 0), + Err(NativeResourceError::Cleanup(_)) + )); + assert!(s.frame(root).is_ok()); + assert_eq!(s.attempt.as_ref().unwrap().root_body_status, None); + Ok(()) + }) + .unwrap(); + let before = observation(auth); + assert!(observe(auth, root).is_err()); + assert_eq!(observation(auth), before); + auth.with_resource(|s, v, r| { + // The prior cleanup refusal remains selected; this does not reset it. + assert_eq!( + s.scope_complete(v, r, root, f.complete_root, 0), + Err(NativeResourceError::BodyFailed) + ); + assert!(s.frame(root).is_err()); + Ok(()) + }) + .unwrap(); + assert_eq!(observe(auth, root), Ok(0)); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 3); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_entry_outcome_refuses_foreign_and_previous_attempt_roots() { + context(|auth| { + let f = fixture(5, false, false); + install_scripted(auth, &f.bytes, entry(), script(&[], 2)).unwrap(); + let (first, first_root, first_op) = begin(auth); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, first_op)?; + s.scope_complete(v, r, first_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + assert_eq!(observe(auth, first_root), Ok(0)); + auth.with_resource(|s, v, r| s.complete_entry(v, r, first, 0, false)) + .unwrap(); + let (second, second_root, second_op) = begin(auth); + let current = observation(auth); + assert!(observe(auth, first_root).is_err()); + assert_eq!(observation(auth), current); + auth.with_resource(|s, v, r| { + s.end_operation_frame(v, r, second_op)?; + s.scope_complete(v, r, second_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + let retired = observation(auth); + context(|foreign_auth| { + let (foreign_attempt, foreign_root, foreign_op, _) = setup(foreign_auth, &f, &[]); + foreign_auth + .with_resource(|s, v, r| { + s.end_operation_frame(v, r, foreign_op)?; + s.scope_complete(v, r, foreign_root, f.complete_root, 0)?; + Ok(()) + }) + .unwrap(); + let foreign_before = observation(foreign_auth); + assert_ne!(foreign_root, second_root); + assert!(observe(auth, foreign_root).is_err()); + assert!(observe(foreign_auth, second_root).is_err()); + assert_eq!(observation(auth), retired); + assert_eq!(observation(foreign_auth), foreign_before); + assert_eq!(observe(foreign_auth, foreign_root), Ok(0)); + foreign_auth + .with_resource(|s, v, r| s.complete_entry(v, r, foreign_attempt, 0, false)) + .unwrap(); + }); + assert!(observe(auth, first_root).is_err()); + assert_eq!(observe(auth, second_root), Ok(0)); + auth.with_resource(|s, v, r| s.complete_entry(v, r, second, 0, false)) + .unwrap(); + assert!(observe(auth, second_root).is_err()); + }); +} + +#[test] +fn native_resource_entry_outcome_refuses_required_worker_purpose_without_latching() { + context(|auth| { + let f = fixture(5, false, false); + install_scripted(auth, &f.bytes, entry(), script(&[], 1)).unwrap(); + let (attempt, root) = auth + .with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::ExplicitComptime)?; + let root = s.root_frame(attempt)?; + s.scope_complete(v, r, root, f.complete_root, 0)?; + Ok((attempt, root)) + }) + .unwrap(); + let before = observation(auth); + assert_eq!(observe(auth, root), Err(NativeResourceError::WrongPurpose)); + assert_eq!(observation(auth), before); + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + Ok(()) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/graph_descriptors.rs b/crates/jett_runtime/src/native_abi/resource/graph_descriptors.rs new file mode 100644 index 00000000..6b404a57 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/graph_descriptors.rs @@ -0,0 +1,102 @@ +//! Metadata-only bridge from exact Resource Descriptor rows to frozen graph fields. +use super::*; + +/// Its private constructor seals the original scalar descriptor to one installation. +/// This record contains no Scope, grant, owner, loan, callable code or environment. +#[derive(Clone)] +pub(in crate::native_abi) struct CheckedGraphHookProof { + context: ContextRegistryKey, + program: ResourceHandleId, + layout: Arc, + descriptor: ResourceHandleId, + descriptor_operation: u32, + hook: u32, + signature: u32, +} + +impl CheckedGraphHookProof { + pub(in crate::native_abi) fn matches_schema( + &self, + operation: u32, + hook: u32, + signature: u32, + ) -> bool { + self.descriptor_operation == operation && self.hook == hook && self.signature == signature + } +} + +impl NativeResourceState { + pub(super) fn graph_descriptor_box( + &self, + ordinary: &mut values::NativeValues, + ticket: u64, + descriptor_operation: u32, + descriptor: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let hook = match self.operation(descriptor_operation)? { + NativeOperation::Descriptor { hook } => *hook, + _ => return Err(NativeResourceError::WrongOperation), + }; + let signature = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)? + .signature(); + match self.handles.get(&descriptor) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if *actual == hook && *actual_signature == signature => {} + _ => return Err(NativeResourceError::WrongFamily), + } + let proof = CheckedGraphHookProof { + context: self.context, + program: self.program_handle, + layout: Arc::clone(&self.layout), + descriptor, + descriptor_operation, + hook, + signature, + }; + ordinary + .mint_checked_graph_hook(ticket, proof) + .map_err(ordinary_error) + } + + /// Used only by the existing exact indirect Resource row before its frame checks. + /// The returned ID is the original, installation-lived scalar Descriptor family. + pub(super) fn graph_descriptor_scalar( + &self, + ordinary: &values::NativeValues, + operation: u32, + boxed: ResourceHandleId, + ) -> ResourceResult { + self.validate_installation()?; + let (hook, signature) = match self.operation(operation)? { + NativeOperation::InvokeDescriptor { + hook, signature, .. + } => (*hook, *signature), + _ => return Err(NativeResourceError::WrongOperation), + }; + let proof = ordinary + .published_checked_graph_hook(boxed.raw()) + .map_err(ordinary_error)?; + if proof.context != self.context + || proof.program != self.program_handle + || !Arc::ptr_eq(&proof.layout, &self.layout) + || proof.hook != hook + || proof.signature != signature + { + return Err(NativeResourceError::WrongFamily); + } + match self.handles.get(&proof.descriptor) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if *actual == hook && *actual_signature == signature => Ok(proof.descriptor), + _ => Err(NativeResourceError::WrongFamily), + } + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/graph_descriptors/tests.rs b/crates/jett_runtime/src/native_abi/resource/graph_descriptors/tests.rs new file mode 100644 index 00000000..fc96cb15 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/graph_descriptors/tests.rs @@ -0,0 +1,523 @@ +//! Host controls for the private checked metadata carrier, using real ABI leaves. +use super::*; + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn setup( + auth: &AuthenticatedResourceContext, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId) { + install_disabled(auth, &layout_with_entry_network(false), entry()).unwrap(); + auth.with_resource(|state, ordinary, registry| { + let attempt = state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + let descriptor = state.descriptor(ordinary, 12)?; + Ok((attempt, frame, descriptor)) + }) + .unwrap() +} + +fn finish(auth: &AuthenticatedResourceContext, attempt: ResourceHandleId, expected_body: u32) { + auth.with_resource(|state, ordinary, registry| { + ordinary.release_actors().map_err(ordinary_error)?; + assert!(ordinary.is_empty()); + let completion = state.complete_entry(ordinary, registry, attempt, 0, false)?; + assert_eq!(completion.selected_kind, expected_body); + Ok(()) + }) + .unwrap(); + assert_empty(auth); +} + +fn schema(operation: u32, hook: u32, signature: u32) -> Vec { + let mut debug = b"JD\x01".to_vec(); + words(&mut debug, &[1, 0, 1]); + debug.push(13); // Function; supplement7 carries protected hook metadata. + let mut bytes = b"JA\x01".to_vec(); + words(&mut bytes, &[debug.len() as u32]); + bytes.extend(debug); + words(&mut bytes, &[1, 0]); + bytes.push(7); + words(&mut bytes, &[operation, hook, signature]); + bytes +} + +fn boxed(auth: &AuthenticatedResourceContext, ticket: u64, descriptor: ResourceHandleId) -> u64 { + let mut out = 0; + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_graph_descriptor_box( + pointer(auth), + ticket, + 12, + descriptor.raw(), + &mut out, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_ne!(out, 0); + out +} + +fn fill( + auth: &AuthenticatedResourceContext, + ticket: u64, + base: u64, + field: u64, + value: u64, + bytes: &[u8], +) -> u32 { + unsafe { + values::jett_rt_v1_actor_graph_fill( + pointer(auth), + ticket, + base, + field, + value, + 1, + 0, + bytes.as_ptr(), + bytes.len() as u64, + ) + } +} + +fn capture(auth: &AuthenticatedResourceContext) { + let captured = + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer(auth), ptr::null(), 0) }; + assert_ne!(captured, 0); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), captured) }, + 0 + ); +} + +#[test] +fn checked_graph_hook_mint_requires_exact_descriptor_operation_family_signature_and_ticket() { + context(|auth| { + let (attempt, _, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + auth.with_resource(|state, ordinary, _| { + assert_eq!( + state.graph_descriptor_box(ordinary, ticket, 0, descriptor), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + state.graph_descriptor_box(ordinary, ticket, 13, descriptor), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + state.graph_descriptor_box(ordinary, ticket, 12, state.program_handle), + Err(NativeResourceError::WrongFamily) + ); + state.handles.insert( + descriptor, + NativeResourceEntry::Descriptor { + hook: 1, + signature: 1, + }, + ); + assert_eq!( + state.graph_descriptor_box(ordinary, ticket, 12, descriptor), + Err(NativeResourceError::WrongFamily) + ); + state.handles.insert( + descriptor, + NativeResourceEntry::Descriptor { + hook: 0, + signature: 1, + }, + ); + assert_eq!( + state.graph_descriptor_box(ordinary, ticket, 12, descriptor), + Err(NativeResourceError::WrongFamily) + ); + state.handles.insert( + descriptor, + NativeResourceEntry::Descriptor { + hook: 0, + signature: 0, + }, + ); + assert_eq!( + state.graph_descriptor_box(ordinary, 0, 12, descriptor), + Err(NativeResourceError::OrdinaryStorage( + JettRuntimeStatusV1::INVALID_ARGUMENT + )) + ); + Ok(()) + }) + .unwrap(); + let value = boxed(auth, ticket, descriptor); + auth.with_resource(|state, ordinary, _| { + assert!( + state + .graph_descriptor_scalar(ordinary, 13, ResourceHandleId::new(value)?) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), value) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(auth), ticket) }, + 0 + ); + finish(auth, attempt, 0); + }); +} + +#[test] +fn checked_graph_hook_schema_mismatches_roll_back_without_taking_caller_ownership() { + for wrong in [(13, 0, 0), (12, 1, 0), (12, 0, 1)] { + context(|auth| { + let (attempt, _, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let base = + unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), ticket, 1) }; + let value = boxed(auth, ticket, descriptor); + assert_eq!( + fill( + auth, + ticket, + base, + 0, + value, + &schema(wrong.0, wrong.1, wrong.2) + ), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + capture(auth); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), value) }, + 0 + ); + let next = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + assert!(next > ticket); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(auth), next) }, + 0 + ); + finish(auth, attempt, 0); + }); + } +} + +#[test] +fn checked_graph_hook_published_metadata_copies_keep_scalar_identity_and_exact_indirect_checks() { + context(|auth| { + let (attempt, frame, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let base = unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), ticket, 1) }; + let original = boxed(auth, ticket, descriptor); + let value = unsafe { values::jett_rt_v1_struct_clone(pointer(auth), original) }; + assert_ne!(original, value); + assert_ne!(value, 0); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), original) }, + 0 + ); + auth.with_resource(|state, ordinary, _| { + assert!( + state + .prepare_hook(ordinary, 13, frame, Some(ResourceHandleId::new(value)?)) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!(fill(auth, ticket, base, 0, value, &schema(12, 0, 0)), 0); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_commit(pointer(auth), ticket) }, + 0 + ); + let copy = unsafe { values::jett_rt_v1_struct_clone(pointer(auth), value) }; + let pending = unsafe { values::jett_rt_v1_record_run(pointer(auth), copy) }; + let joined = unsafe { values::jett_rt_v1_record_task_join(pointer(auth), pending) }; + assert_ne!(copy, 0); + assert_ne!(pending, 0); + assert_ne!(joined, 0); + auth.with_resource(|state, ordinary, _| { + assert!( + state + .graph_descriptor_scalar(ordinary, 13, ResourceHandleId::new(pending)?) + .is_err() + ); + assert_eq!( + state.graph_descriptor_scalar(ordinary, 12, ResourceHandleId::new(copy)?), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + state.graph_descriptor_scalar(ordinary, 13, ResourceHandleId::new(joined)?)?, + descriptor + ); + let call = + state.prepare_hook(ordinary, 13, frame, Some(ResourceHandleId::new(value)?))?; + assert_eq!(state.checked_call(call)?.descriptor, Some(descriptor)); + assert_eq!(state.checked_call(call)?.target_operation, 0); + state.handles.remove(&call); + assert_eq!( + state.prepare_hook(ordinary, 0, frame, Some(ResourceHandleId::new(value)?)), + Err(NativeResourceError::WrongOperation) + ); + Ok(()) + }) + .unwrap(); + for value in [copy, pending, joined] { + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), value) }, + 0 + ); + } + finish(auth, attempt, 0); + }); +} + +#[test] +fn checked_graph_hook_refuses_ordinary_callable_and_field_abi_in_both_phases() { + for publish in [false, true] { + context(|auth| { + let (attempt, _, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let value = boxed(auth, ticket, descriptor); + if publish { + let base = unsafe { + values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), ticket, 1) + }; + assert_eq!(fill(auth, ticket, base, 0, value, &schema(12, 0, 0)), 0); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_commit(pointer(auth), ticket) }, + 0 + ); + } + assert_eq!( + unsafe { values::jett_rt_v1_function_callable_check(pointer(auth), value) }, + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + capture(auth); + assert_eq!( + unsafe { values::jett_rt_v1_struct_field(pointer(auth), value, 0) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + capture(auth); + if !publish { + // The ordinary failure already aborted the ticket; unfilled box stays caller-owned. + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), value) }, + 0 + ); + } + finish(auth, attempt, 0); + }); + } +} + +#[test] +fn checked_graph_hook_abort_drops_each_metadata_clone_and_old_factory_boxes_stay_unpublished() { + context(|auth| { + let (attempt, _, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let base = unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), ticket, 2) }; + let value = boxed(auth, ticket, descriptor); + let copy = unsafe { values::jett_rt_v1_struct_clone(pointer(auth), value) }; + assert_eq!(fill(auth, ticket, base, 0, value, &schema(12, 0, 0)), 0); + assert_eq!(fill(auth, ticket, base, 1, copy, &schema(12, 0, 0)), 0); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(auth), ticket) }, + 0 + ); + auth.with_resource(|_, ordinary, _| { + assert!(ordinary.is_empty()); + Ok(()) + }) + .unwrap(); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let stale = boxed(auth, ticket, descriptor); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(auth), ticket) }, + 0 + ); + let next = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let base = unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), next, 1) }; + assert!(stale < next); + auth.with_resource(|state, ordinary, _| { + assert!( + state + .graph_descriptor_scalar(ordinary, 13, ResourceHandleId::new(stale)?) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + fill(auth, next, base, 0, stale, &schema(12, 0, 0)), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + capture(auth); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), stale) }, + 0 + ); + finish(auth, attempt, 0); + }); +} + +#[test] +fn checked_graph_hook_foreign_descriptors_and_boxes_never_select_same_context_authority() { + context(|first| { + context(|second| { + let (first_attempt, _, first_descriptor) = setup(first); + let (second_attempt, _, second_descriptor) = setup(second); + let first_ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(first)) }; + let second_ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(second)) }; + first + .with_resource(|state, ordinary, _| { + assert_eq!( + state.graph_descriptor_box(ordinary, first_ticket, 12, second_descriptor), + Err(NativeResourceError::WrongFamily) + ); + assert!( + state + .graph_descriptor_box(ordinary, second_ticket, 12, first_descriptor) + .is_err() + ); + Ok(()) + }) + .unwrap(); + let alien = boxed(second, second_ticket, second_descriptor); + let base = unsafe { + values::jett_rt_v1_actor_graph_reserve_base(pointer(first), first_ticket, 1) + }; + assert_eq!( + fill(first, first_ticket, base, 0, alien, &schema(12, 0, 0)), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + capture(first); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(second), alien) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(second), second_ticket) }, + 0 + ); + finish(first, first_attempt, 0); + finish(second, second_attempt, 0); + }) + }); +} + +#[test] +fn checked_graph_hook_failed_leaf_aborts_staging_and_preserves_older_actor_and_first_failure() { + context(|auth| { + let (attempt, _, descriptor) = setup(auth); + let older = unsafe { values::jett_rt_v1_struct_new(pointer(auth), 0) }; + assert_eq!( + unsafe { values::jett_rt_v1_actor_register(pointer(auth), older) }, + older + ); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let base = unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer(auth), ticket, 1) }; + let value = boxed(auth, ticket, descriptor); + assert_eq!(fill(auth, ticket, base, 0, value, &schema(12, 0, 0)), 0); + let mut out = 123; + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_graph_descriptor_box( + pointer(auth), + ticket, + 13, + descriptor.raw(), + &mut out, + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(out, 123); + assert_eq!( + unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + capture(auth); + assert_eq!( + unsafe { values::jett_rt_v1_actor_message_check(pointer(auth), older) }, + 0 + ); + let next = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + assert!(next > ticket); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_abort(pointer(auth), next) }, + 0 + ); + finish(auth, attempt, 1); + }); +} + +#[test] +fn checked_graph_hook_root_fill_publishes_metadata_without_dummy_actor_and_returns_owned_root() { + context(|auth| { + let (attempt, frame, descriptor) = setup(auth); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer(auth)) }; + let root = boxed(auth, ticket, descriptor); + let bytes = schema(12, 0, 0); + assert_eq!( + unsafe { + values::jett_rt_v1_actor_graph_root_fill( + pointer(auth), + ticket, + root, + 1, + 0, + bytes.as_ptr(), + bytes.len() as u64, + ) + }, + 0 + ); + auth.with_resource(|state, ordinary, _| { + assert!( + state + .graph_descriptor_scalar(ordinary, 13, ResourceHandleId::new(root)?) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_commit(pointer(auth), ticket) }, + 0 + ); + auth.with_resource(|state, ordinary, _| { + let call = + state.prepare_hook(ordinary, 13, frame, Some(ResourceHandleId::new(root)?))?; + assert_eq!(state.checked_call(call)?.descriptor, Some(descriptor)); + state.handles.remove(&call); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), root) }, + 0 + ); + finish(auth, attempt, 0); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/leaves.rs b/crates/jett_runtime/src/native_abi/resource/leaves.rs new file mode 100644 index 00000000..b5b0605e --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/leaves.rs @@ -0,0 +1,833 @@ +//! Exact compiler-internal Resource leaves. No provider installation or key adoption. +use super::*; +#[path = "leaves/carriers.rs"] +mod carrier_leaves; + +/// Addresses cannot establish lifetime/exclusivity; those remain the unsafe C +/// caller contract. Reject null/misaligned/overflow/known context overlaps before effect. +pub(super) fn output_ok(context: *const JettRuntimeContextV1, out: *mut T) -> bool { + let start = out.addr(); + let Some(end) = start.checked_add(std::mem::size_of::()) else { + return false; + }; + let ctx = context.addr(); + let Some(ctx_end) = ctx.checked_add(std::mem::size_of::()) else { + return false; + }; + !out.is_null() && start % std::mem::align_of::() == 0 && (end <= ctx || ctx_end <= start) +} +pub(super) fn disjoint(a: *mut A, b: *mut B) -> bool { + let Some(a_end) = a.addr().checked_add(std::mem::size_of::()) else { + return false; + }; + let Some(b_end) = b.addr().checked_add(std::mem::size_of::()) else { + return false; + }; + a_end <= b.addr() || b_end <= a.addr() +} +fn status( + context: *const JettRuntimeContextV1, + op: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + match catch_unwind(AssertUnwindSafe(|| { + AuthenticatedResourceContext::acquire(context)?.with_resource(op) + })) { + Ok(Ok(())) => JettRuntimeStatusV1::OK, + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} +unsafe fn output( + context: *const JettRuntimeContextV1, + out: *mut T, + op: impl FnOnce( + &mut NativeResourceState, + &mut values::NativeValues, + &mut ResourceRegistry, + ) -> ResourceResult, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + status(context, |state, ordinary, registry| { + let value = op(state, ordinary, registry)?; + // SAFETY: the C caller guarantees live exclusive storage; shape/overlap was checked above. + unsafe { + ptr::write(out, value); + } + Ok(()) + }) +} +fn id(raw: u64) -> ResourceResult { + ResourceHandleId::new(raw) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_scope_validate( + context: *const JettRuntimeContextV1, + scope: u64, + function: u32, + signature: u32, + template: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + let scope = id(scope)?; + s.scope_validate(v, scope, function, signature, template)?; + #[cfg(all(test, jett_resource_native_test_archive))] + s.observe_property_source_entry(v, scope); + Ok(()) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_operation_begin( + context: *const JettRuntimeContextV1, + parent: u64, + template: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.begin_operation_frame(v, template, id(parent)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_operation_complete( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.operation_frame(operation, id(frame)?)?; + if !matches!(s.operation(operation)?, NativeOperation::Complete { .. }) { + return Err(NativeResourceError::WrongOperation); + } + s.end_operation_frame(v, r, id(frame)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_hook_prepare( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + descriptor: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + #[cfg(all(test, jett_resource_native_test_archive))] + test_observations::observe_ingress( + context, + test_observations::AcquisitionIngress::HookPrepare { operation }, + ); + unsafe { + output(context, out_value, |s, v, r| { + s.prepare_hook( + v, + operation, + id(frame)?, + if descriptor == 0 { + None + } else { + Some(id(descriptor)?) + }, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_factory_prepare( + context: *const JettRuntimeContextV1, + call: u64, + network: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + #[cfg(all(test, jett_resource_native_test_archive))] + test_observations::observe_ingress( + context, + test_observations::AcquisitionIngress::FactoryPrepare, + ); + unsafe { + output(context, out_value, |s, v, r| { + s.prepare_factory(v, r, id(call)?, network) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_factory_commit( + context: *const JettRuntimeContextV1, + prepared: u64, + network: u64, + label: i64, + out_value: *mut JettResourceCallResultV1, +) -> JettRuntimeStatusV1 { + #[cfg(all(test, jett_resource_native_test_archive))] + test_observations::observe_ingress( + context, + test_observations::AcquisitionIngress::FactoryCommit, + ); + unsafe { + output(context, out_value, |s, v, r| { + s.factory(v, r, id(prepared)?, network, label) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_begin( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.borrow_begin(v, r, operation, id(frame)?, id(owner)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_commit( + context: *const JettRuntimeContextV1, + call: u64, + network: u64, + loan: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.invoke_borrow(v, r, id(call)?, network, id(loan)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_borrow_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.borrow_end(operation, id(frame)?, id(loan)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_close( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.close_or_drop(r, operation, id(frame)?, id(owner)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_descriptor_close( + context: *const JettRuntimeContextV1, + call: u64, + owner: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + let call_id = id(call)?; + let call = s.checked_call(call_id)?; + let frame = call.frame; + let operation = call.target_operation; + if call.descriptor.is_none() { + return Err(NativeResourceError::WrongOperation); + } + s.close_or_drop(r, operation, frame, id(owner)?)?; + s.handles.remove(&call_id); + Ok(()) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_descriptor( + context: *const JettRuntimeContextV1, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.descriptor(v, operation).map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and output satisfy the existing stationary/exclusive output contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_graph_descriptor_box( + context: *const JettRuntimeContextV1, + ticket: u64, + descriptor_operation: u32, + descriptor: u64, + out_box: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_box, |s, v, _| { + match id(descriptor).and_then(|descriptor| { + s.graph_descriptor_box(v, ticket, descriptor_operation, descriptor) + }) { + Ok(boxed) => Ok(boxed), + Err(error) => { + // Preserve the native failure before rolling back this factory. + v.rollback_graph_hook_failure((error.status(), REFUSED)); + Err(error) + } + } + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_transfer( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.transfer( + v, + r, + operation, + id(frame)?, + id(value)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_destination_frame( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.running(v)?; + s.destination_frame(id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_adopt( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + owner: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.sum_adopt( + v, + r, + operation, + id(frame)?, + id(owner)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_tag( + context: *const JettRuntimeContextV1, + frame: u64, + value: u64, + out_value: *mut u32, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.running(v)?; + s.frame(id(frame)?)?; + if s.active_frames.last() != Some(&id(frame)?) { + return Err(NativeResourceError::WrongFrame); + } + s.sum_tag(id(value)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_take( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, + destination_frame: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + if s.destination_frame(id(frame)?, operation)? != id(destination_frame)? { + return Err(NativeResourceError::WrongFrame); + } + s.sum_take( + v, + r, + operation, + id(frame)?, + id(value)?, + id(destination_frame)?, + ) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_drop( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + value: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.sum_drop(v, r, operation, id(frame)?, id(value)?) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_failure_companion_take( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + sum: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.failure_companion_take(v, id(frame)?, operation, id(sum)?) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_absent_sum( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.absent_sum(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_failure_sum( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + companion: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.failure_sum(v, id(frame)?, operation, companion) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_replace( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + old: u64, + replacement: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.replace(v, r, operation, id(frame)?, id(old)?, id(replacement)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_prepare( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_prepare(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_actual( + context: *const JettRuntimeContextV1, + call: u64, + source_index: u32, + parameter: u32, + value: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.source_actual(v, r, id(call)?, source_index, parameter, value) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_enter( + context: *const JettRuntimeContextV1, + call: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_enter(v, r, id(call)?).map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_parameter( + context: *const JettRuntimeContextV1, + scope: u64, + parameter: u32, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.source_parameter(v, id(scope)?, parameter) + }) + } +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_source_status( + context: *const JettRuntimeContextV1, + call: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| s.source_status(v, id(call)?)) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_scope_complete( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + body_status: u32, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.scope_complete(v, r, id(scope)?, operation, body_status) + }) +} + +/// # Safety +/// Context and outputs satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_return_publish( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + provisional: u64, + out_value: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_value, |s, v, r| { + s.return_publish(v, r, id(scope)?, operation, id(provisional)?) + .map(ResourceHandleId::raw) + }) + } +} + +/// # Safety +/// Output records are aligned, live, exclusive and mutually disjoint. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_scope( + context: *const JettRuntimeContextV1, + out_scope: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + unsafe { + entry_lookup(context, out_scope, out_result, |s| { + s.validate_installation()?; + s.runtime_purpose()?; + let attempt = s + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if attempt.phase != AttemptPhase::Running || s.entry != attempt.entry { + return Err(NativeResourceError::InvalidEntry); + } + let root = s.frame(attempt.root_frame)?; + if root.parent.is_some() || root.template != s.entry.scope { + return Err(NativeResourceError::WrongFrame); + } + Ok(attempt.root_frame.raw()) + }) + } +} +/// # Safety +/// Output records are aligned, live, exclusive and mutually disjoint. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_network( + context: *const JettRuntimeContextV1, + parameter: u32, + out_capability: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + unsafe { + entry_lookup(context, out_capability, out_result, |s| { + let attempt = s + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)? + .id; + s.entry_network(attempt, parameter) + }) + } +} +unsafe fn entry_lookup( + context: *const JettRuntimeContextV1, + out: *mut u64, + out_result: *mut JettRuntimeResultV1, + lookup: impl FnOnce(&NativeResourceState) -> ResourceResult, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out) || !output_ok(context, out_result) || !disjoint(out, out_result) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || { + let result = AuthenticatedResourceContext::acquire(context).and_then(|context| { + context.with_resource(|s, v, _| { + s.running(v)?; + lookup(s) + }) + }); + match result { + Ok(value) => { + unsafe { ptr::write(out, value) }; + JettRuntimeResultV1::ok() + } + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +/// # Safety +/// Context is stationary and output storage is aligned, live and exclusive. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_entry_outcome( + context: *const JettRuntimeContextV1, + root_scope: u64, + function: u32, + signature: u32, + template: u32, + out_body: *mut u32, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out_body) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + // Refusal is observation only: with_resource would latch it as a body fault. + let outcome = catch_unwind(AssertUnwindSafe(|| { + AuthenticatedResourceContext::acquire(context)?.with_state(|_, state| { + state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)? + .entry_outcome(id(root_scope)?, function, signature, template) + }) + })); + match outcome { + Ok(Ok(body)) => { + // SAFETY: the caller supplies live exclusive storage; preflight + // refused known overlap/range/alignment faults before any state read. + unsafe { ptr::write(out_body, body) }; + JettRuntimeStatusV1::OK + } + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} + +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_borrow( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + sum: u64, + out_loan: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_loan, |s, v, r| { + s.sum_borrow(v, r, operation, id(frame)?, id(sum)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_view_tag( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_tag: *mut u32, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_tag, |s, v, r| { + s.sum_view_tag(v, r, operation, id(frame)?, id(loan)?) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_view_project( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_child: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_child, |s, v, r| { + s.sum_view_project(v, r, operation, id(frame)?, id(loan)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output satisfy the stationary-context and exclusive output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_failure_read( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, + out_companion: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out_companion, |s, v, r| { + s.sum_failure_read(v, r, operation, id(frame)?, id(loan)?) + }) + } +} +/// # Safety +/// Context satisfies the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_sum_borrow_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + loan: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, _v, r| { + s.sum_borrow_end(r, operation, id(frame)?, id(loan)?) + }) +} diff --git a/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs b/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs new file mode 100644 index 00000000..1bf3ac84 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/leaves/carriers.rs @@ -0,0 +1,250 @@ +//! ABI v1 opaque handles; each operation is joined to an installed v3 carrier row. +use super::*; + +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_construct_begin( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_construct_begin(v, id(frame)?, operation) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context obeys the stationary-context contract; bits conveys the row's owned child. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_child( + context: *const JettRuntimeContextV1, + builder: u64, + ordinal: u32, + bits: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.carrier_child_with_registry(v, r, id(builder)?, ordinal, bits) + }) +} +/// # Safety +/// Context obeys the stationary-context contract; the installed row owns the child. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_child_scalar_task( + context: *const JettRuntimeContextV1, + builder: u64, + ordinal: u32, + bits: u64, + pending_depth: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, _| { + s.carrier_child_scalar_task(v, id(builder)?, ordinal, bits, pending_depth) + }) +} + +/// # Safety +/// Both outputs obey the stationary-context, exclusive-output and disjoint contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_observe_scalar_task( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out_bits: *mut u64, + out_depth: *mut u64, +) -> JettRuntimeStatusV1 { + if !output_ok(context, out_bits) + || !output_ok(context, out_depth) + || !disjoint(out_bits, out_depth) + { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + status(context, |s, v, _| { + let (bits, depth) = + s.carrier_observe_scalar_task(v, id(frame)?, operation, id(input)?, selection)?; + // SAFETY: caller supplies live exclusive, validated disjoint output words. + unsafe { + ptr::write(out_bits, bits); + ptr::write(out_depth, depth); + } + Ok(()) + }) +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_commit( + context: *const JettRuntimeContextV1, + builder: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_commit(v, id(builder)?).map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_transfer( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, r| { + s.carrier_transfer_with_registry(v, r, id(frame)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_qualify( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_qualify(v, id(frame)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_borrow( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_borrow(v, id(frame)?, operation, id(input)?, selection) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_observe( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_observe(v, id(frame)?, operation, id(input)?, selection) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_extract( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_extract(v, id(frame)?, operation, id(input)?, selection) + }) + } +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_adapt( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, + selection: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, _| { + s.carrier_adapt(v, id(frame)?, operation, id(input)?, selection) + .map(ResourceHandleId::raw) + }) + } +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_end( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, _, _| { + s.carrier_end(id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_retire( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, r| { + s.carrier_retire_with_registry(v, r, id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context obeys the stationary-context contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_reset_iteration( + context: *const JettRuntimeContextV1, + frame: u64, + operation: u32, + input: u64, +) -> JettRuntimeStatusV1 { + status(context, |s, v, _| { + s.carrier_reset_iteration(v, id(frame)?, operation, id(input)?) + }) +} +/// # Safety +/// Context and output obey the stationary-context and exclusive-output contracts. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_carrier_publish( + context: *const JettRuntimeContextV1, + scope: u64, + operation: u32, + input: u64, + out: *mut u64, +) -> JettRuntimeStatusV1 { + unsafe { + output(context, out, |s, v, r| { + s.carrier_publish(v, r, id(scope)?, operation, id(input)?) + .map(ResourceHandleId::raw) + }) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/operations.rs b/crates/jett_runtime/src/native_abi/resource/operations.rs new file mode 100644 index 00000000..b08310f8 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/operations.rs @@ -0,0 +1,1053 @@ +use super::*; + +impl NativeResourceState { + pub(super) fn root_frame(&self, attempt: ResourceHandleId) -> ResourceResult { + let running = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + if running.id != attempt || running.phase != AttemptPhase::Running { + return Err(NativeResourceError::InvalidEntry); + } + Ok(running.root_frame) + } + pub(super) fn begin_operation_frame( + &mut self, + ordinary: &values::NativeValues, + template: u32, + parent: ResourceHandleId, + ) -> ResourceResult { + let attempt = self.running(ordinary)?.id; + if self.active_frames.last() != Some(&parent) { + return Err(NativeResourceError::WrongFrame); + } + let parent_template = self.frame(parent)?.template; + let frame = self + .layout + .frames() + .get(template as usize) + .ok_or(NativeResourceError::WrongFrame)?; + if frame.role() != NativeFrameRole::Operation { + // A Source invocation/Return constructor must supply its separate checked handoff. + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + if !frame + .parents() + .contains(&NativeParent::Frame(parent_template)) + { + return Err(NativeResourceError::WrongFrame); + } + let mut ids = self.reserve(1)?; + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let handle = next_handle(&mut ids)?; + let purpose = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)? + .purpose; + let token = self + .custody + .begin_frame(ResourceFrameKind::Operation, purpose)?; + self.handles.insert( + handle, + NativeResourceEntry::Frame(NativeFrameEntry { + template, + attempt, + parent: Some(parent), + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(handle); + Ok(handle) + } + pub(super) fn end_operation_frame( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + ) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + if self.layout.frames()[self.frame(frame)?.template as usize].role() + != NativeFrameRole::Operation + { + return Err(NativeResourceError::WrongFrame); + } + self.retire_frame(ordinary, registry, frame) + } + pub(super) fn retire_frame( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + frame: ResourceHandleId, + ) -> ResourceResult<()> { + if self.active_frames.last() != Some(&frame) { + return Err(NativeResourceError::WrongFrame); + } + let Some(NativeResourceEntry::Frame(_)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + self.frame_shells_unborrowed(frame)?; + let view_retirement = self.retire_sum_views(frame); + let carrier_retirement = if self.layout.wire_version() == 3 { + view_retirement?; + // End metadata leases and reserve companion cleanup while generations live. + Some(self.prepare_carrier_frame(ordinary, frame)?) + } else { + None + }; + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let cleanup = self.custody.end_frame(&header.token, registry); + if self.custody.active_frames() == self.active_frames.len() { + // A refused cleanup has not retired this frame. Keep its tokens available. + return self.cleanup(cleanup); + } + let core_result = self.cleanup(cleanup); + let carrier_result = if let Some(prepared) = carrier_retirement { + self.retire_carrier_frame_prepared(ordinary, prepared) + } else { + Ok(()) + }; + let mut ordinary_failed = false; + self.handles.retain(|_, entry| match entry { + NativeResourceEntry::Frame(_) => true, + NativeResourceEntry::Owner(owner) => owner.frame != frame, + NativeResourceEntry::Loan(loan) => loan.frame != frame, + NativeResourceEntry::SumLoan(loan) => loan.frame != frame, + NativeResourceEntry::Prepared(prepared) => prepared.frame != frame, + NativeResourceEntry::Call(call) => call.frame != frame, + NativeResourceEntry::SourceCall(call) => call.frame != frame, + NativeResourceEntry::Sum(sum) if sum.frame == frame => { + if let NativeSumPayload::Fail { shape, string } = &sum.payload { + ordinary_failed |= ordinary + .drop_resource_typed_companion(&self.layout, *shape, *string) + .is_err(); + } + false + } + _ => true, + }); + self.handles.remove(&frame); + self.active_frames.pop(); + self.carrier_adapters + .retain(|shell, _| self.handles.contains_key(shell)); + if ordinary_failed { + ordinary.cleanup_failed = true; + } + core_result?; + carrier_result?; + view_retirement + } + pub(super) fn runtime_purpose(&self) -> ResourceResult<()> { + if self.attempt.as_ref().map(|attempt| attempt.purpose) != Some(ResourcePurpose::Runtime) { + return Err(NativeResourceError::WrongPurpose); + } + Ok(()) + } + pub(super) fn owner( + &self, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<&NativeOwnerEntry> { + match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => { + self.frame(owner.frame)?; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + self.carrier_frame_at(current, handle, slot)?; + Ok(owner) + } + Some(NativeResourceEntry::Owner(_)) => Err(NativeResourceError::WrongOperation), + Some(_) => Err(NativeResourceError::WrongFamily), + None => Err(NativeResourceError::InvalidHandle), + } + } + pub(super) fn slot_frame(&self, slot: u32, frame: ResourceHandleId) -> ResourceResult<()> { + let layout_slot = self + .layout + .slots() + .get(slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if layout_slot.frame() != self.frame(frame)?.template { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + fn grant( + &self, + ordinary: &values::NativeValues, + network_bits: u64, + ) -> ResourceResult<(ResourceHandleId, AuthorityProvenance)> { + let handle = self.network.ok_or(NativeResourceError::WrongGrant)?; + let Some(NativeResourceEntry::NetworkGrant(grant)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongGrant); + }; + if grant.context != self.context + || !Arc::ptr_eq(&grant.layout, &self.layout) + || !self.provider.matches(grant.provider, handle) + || grant.ordinary_network != network_bits + || !ordinary.validates_resource_network(network_bits) + || grant.provenance + != AuthorityProvenance::new(grant.provider.raw(), grant.restriction.raw()) + { + return Err(NativeResourceError::WrongGrant); + } + Ok((handle, grant.provenance)) + } + pub(super) fn descriptor( + &mut self, + ordinary: &values::NativeValues, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + let hook = match self.operation(operation)? { + NativeOperation::Descriptor { hook } => *hook, + _ => return Err(NativeResourceError::WrongOperation), + }; + let signature = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)? + .signature(); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles + .insert(handle, NativeResourceEntry::Descriptor { hook, signature }); + Ok(handle) + } + pub(super) fn prepare_hook( + &mut self, + ordinary: &values::NativeValues, + operation: u32, + frame: ResourceHandleId, + descriptor: Option, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + // Graph boxes have a distinct ordinary-owned family. Only this exact + // indirect row may resolve published metadata to its original scalar. + let descriptor = match descriptor { + Some(handle) if ordinary.published_checked_graph_hook(handle.raw()).is_ok() => { + Some(self.graph_descriptor_scalar(ordinary, operation, handle)?) + } + descriptor => descriptor, + }; + let (target, hook) = match (self.operation(operation)?, descriptor) { + ( + NativeOperation::Acquire { hook, .. } + | NativeOperation::InvokeBorrow { hook, .. } + | NativeOperation::Close { hook, .. }, + None, + ) => (operation, *hook), + ( + NativeOperation::InvokeDescriptor { + hook, + signature, + target, + }, + Some(handle), + ) => { + match self.handles.get(&handle) { + Some(NativeResourceEntry::Descriptor { + hook: actual_hook, + signature: actual_signature, + }) if actual_hook == hook && actual_signature == signature => {} + _ => return Err(NativeResourceError::WrongFamily), + } + (*target, *hook) + } + _ => return Err(NativeResourceError::WrongOperation), + }; + self.operation_frame(target, frame)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::Call(NativePreparedCall { + operation, + target_operation: target, + hook, + frame, + descriptor, + }), + ); + Ok(handle) + } + pub(super) fn checked_call( + &self, + handle: ResourceHandleId, + ) -> ResourceResult<&NativePreparedCall> { + let Some(NativeResourceEntry::Call(call)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.operation_frame(call.target_operation, call.frame)?; + match (self.operation(call.operation)?, call.descriptor) { + ( + NativeOperation::InvokeDescriptor { + hook, + signature, + target, + }, + Some(descriptor), + ) if *target == call.target_operation && *hook == call.hook => { + match self.handles.get(&descriptor) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if actual == hook && actual_signature == signature => {} + _ => return Err(NativeResourceError::WrongFamily), + } + } + ( + NativeOperation::Acquire { hook, .. } + | NativeOperation::InvokeBorrow { hook, .. } + | NativeOperation::Close { hook, .. }, + None, + ) if *hook == call.hook && call.operation == call.target_operation => {} + _ => return Err(NativeResourceError::WrongOperation), + } + Ok(call) + } + pub(super) fn prepare_factory( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + call_handle: ResourceHandleId, + network_bits: u64, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let call = self.checked_call(call_handle)?; + let frame = call.frame; + let hook = call.hook; + let destination = match self.operation(call.target_operation)? { + NativeOperation::Acquire { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + let hook = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if hook.recipe() != NativeRecipe::NetworkFactory { + return Err(NativeResourceError::WrongOperation); + } + let kind = hook.kind(); + let destination_frame = + self.destination_frame(frame, self.checked_call(call_handle)?.target_operation)?; + self.slot_empty(destination_frame, destination)?; + let slot = &self.layout.slots()[destination as usize]; + if slot.shape() != self.layout.signatures()[hook.signature() as usize].result() + || slot.path() != [NativePayloadStep::Ok] + { + return Err(NativeResourceError::WrongOperation); + } + let network = self.grant(ordinary, network_bits)?.0; + self.layout.preflight_physical_acquisition(registry)?; + // Both Resource result branches and ordinary error storage are reserved before the provider. + let publication_ids = self.reserve(1)?; + let mut prepared_ids = self.reserve(1)?; + let handle = next_handle(&mut prepared_ids)?; + ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; // capacity only; unused identity range is burned + let frame_token = match self.handles.get(&destination_frame) { + Some(NativeResourceEntry::Frame(frame)) => &frame.token, + _ => return Err(NativeResourceError::WrongFrame), + }; + let holder = self + .custody + .holder(frame_token, &self.layout.slot(destination)?)?; + let token = self + .custody + .prepare_acquisition(&self.layout.kind(kind)?, &holder)?; + self.handles.insert( + handle, + NativeResourceEntry::Prepared(NativePreparedEntry { + call: call_handle, + frame, + destination, + destination_frame, + kind, + network, + token, + publication_ids, + }), + ); + Ok(handle) + } + pub(super) fn factory( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + prepared_handle: ResourceHandleId, + network_bits: u64, + label: i64, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let grant = self.grant(ordinary, network_bits)?; + let Some(NativeResourceEntry::Prepared(prepared)) = self.handles.get(&prepared_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + let call = self.checked_call(prepared.call)?; + let actual_destination = match self.operation(call.target_operation)? { + NativeOperation::Acquire { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + if prepared.network != grant.0 + || prepared.frame != call.frame + || prepared.destination != actual_destination + || self.layout.hooks()[call.hook as usize].kind() != prepared.kind + { + return Err(NativeResourceError::WrongOperation); + } + self.layout.preflight_physical_acquisition(registry)?; + let mut ordinary_ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + // Rejoin occupancy immediately before the effect; this is not token reconstruction. + self.slot_frame(prepared.destination, prepared.destination_frame)?; + let destination_frame = self.destination_frame(call.frame, call.target_operation)?; + if destination_frame != prepared.destination_frame { + return Err(NativeResourceError::WrongFrame); + } + if self.handles.values().any(|entry| matches!(entry, NativeResourceEntry::Owner(owner) + if owner.frame == destination_frame && owner.slot == prepared.destination) + || matches!(entry, NativeResourceEntry::Sum(sum) if sum.frame == destination_frame && sum.slot == prepared.destination)) { + return Err(NativeResourceError::WrongOperation); + } + let frame = match self.handles.get(&prepared.destination_frame) { + Some(NativeResourceEntry::Frame(frame)) => &frame.token, + _ => return Err(NativeResourceError::WrongFrame), + }; + let holder = self + .custody + .holder(frame, &self.layout.slot(prepared.destination)?)?; + self.custody + .prepare_acquisition(&self.layout.kind(prepared.kind)?, &holder)?; + #[cfg(all(test, jett_resource_native_test_archive))] + ordinary.test_observations.provider_construct_dispatch(); + let result = catch_unwind(AssertUnwindSafe(|| self.provider.construct(label))); + let result = match result { + Ok(Ok(result)) => result, + Ok(Err(error)) => { + self.record_failure(error); + return Err(error); + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut self.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + return Err(NativeResourceError::BodyFailed); + } + }; + let Some(NativeResourceEntry::Prepared(mut prepared)) = + self.handles.remove(&prepared_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + self.handles.remove(&prepared.call); + let handle = next_handle(&mut prepared.publication_ids)?; + let shape = self.layout.slots()[prepared.destination as usize].shape(); + let (domain, payload) = match result { + Ok(constructed) => { + let provider::ConstructedPayload { payload, finalizer } = constructed; + let token = match self.custody.commit_acquisition( + registry, + prepared.token, + grant.1, + payload, + finalizer, + ) { + Ok(token) => token, + Err(ResourceTransitionFailure::Rejected(error)) => { + let error = NativeResourceError::Custody(error); + self.record_failure(error); + return Err(error); + } + Err(ResourceTransitionFailure::Cleanup(error)) => { + let error = NativeResourceError::Cleanup(error); + self.record_failure(error); + return Err(error); + } + }; + (RESOURCE_DOMAIN_OK, NativeSumPayload::Ok(token)) + } + Err(error) => { + let fail_shape = match self.layout.shapes().get(shape as usize) { + Some(NativeShape::Result { fail, .. }) => *fail, + _ => return Err(NativeResourceError::WrongOperation), + }; + let string = ordinary + .publish_resource_error(error, &mut ordinary_ids) + .map_err(ordinary_error)?; + ( + RESOURCE_DOMAIN_ERROR, + NativeSumPayload::Fail { + shape: fail_shape, + string, + }, + ) + } + }; + self.handles.insert( + handle, + NativeResourceEntry::Sum(NativeResourceSum { + frame: prepared.destination_frame, + slot: prepared.destination, + shape, + payload, + }), + ); + Ok(JettResourceCallResultV1 { + domain, + reserved: 0, + value: handle.raw(), + }) + } + pub(super) fn sum_tag(&self, handle: ResourceHandleId) -> ResourceResult { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongOperation); + } + match self.handles.get(&handle) { + Some(NativeResourceEntry::Sum(sum)) => { + self.frame(sum.frame)?; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + self.carrier_frame_at(current, handle, sum.slot)?; + Ok(match &sum.payload { + NativeSumPayload::None | NativeSumPayload::Fail { .. } => 0, + _ => 1, + }) + } + Some(_) => Err(NativeResourceError::WrongFamily), + None => Err(NativeResourceError::InvalidHandle), + } + } + pub(super) fn sum_adopt( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::SumAdopt { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.owner(owner_handle, source)?; + self.slot_empty(destination_frame, destination)?; + let slot = &self.layout.slots()[destination as usize]; + let shape = slot.shape(); + let step = match slot.path() { + [NativePayloadStep::Some] => NativePayloadStep::Some, + [NativePayloadStep::Ok] => NativePayloadStep::Ok, + _ => return Err(NativeResourceError::WrongOperation), + }; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let holder = self.custody.holder( + &self.frame(destination_frame)?.token, + &self.layout.slot(destination)?, + )?; + let Some(NativeResourceEntry::Owner(mut owner)) = self.handles.remove(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.transfer(&mut owner.token, &holder, registry) { + self.handles + .insert(owner_handle, NativeResourceEntry::Owner(owner)); + return Err(error.into()); + } + let payload = if step == NativePayloadStep::Some { + NativeSumPayload::Some(owner.token) + } else { + NativeSumPayload::Ok(owner.token) + }; + self.handles.insert( + handle, + NativeResourceEntry::Sum(NativeResourceSum { + frame: destination_frame, + slot: destination, + shape, + payload, + }), + ); + Ok(handle) + } + pub(super) fn replace_plain( + &mut self, + ordinary: &values::NativeValues, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + old_handle: ResourceHandleId, + replacement_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (old_slot, replacement_slot) = match self.operation(operation)? { + NativeOperation::Replace { + old, replacement, .. + } => (*old, *replacement), + _ => return Err(NativeResourceError::WrongOperation), + }; + if old_handle == replacement_handle { + return Err(NativeResourceError::WrongOperation); + } + let old_frame = self.owner(old_handle, old_slot)?.frame; + self.owner(replacement_handle, replacement_slot)?; + let holder = self + .custody + .holder(&self.frame(old_frame)?.token, &self.layout.slot(old_slot)?)?; + let (Some(NativeResourceEntry::Owner(old)), Some(NativeResourceEntry::Owner(replacement))) = ( + self.handles.get(&old_handle), + self.handles.get(&replacement_handle), + ) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody + .preflight_replacement(&old.token, &replacement.token, &holder, registry)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Owner(mut old)) = self.handles.remove(&old_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = self.custody.close(&mut old.token, registry); + if self.custody.validate_owned(&old.token, registry).is_ok() { + self.handles + .insert(old_handle, NativeResourceEntry::Owner(old)); + // The RHS still has its one independent owner, available for exact scope cleanup. + return self + .cleanup(cleanup) + .and(Err(NativeResourceError::WrongOperation)); + } + let Some(NativeResourceEntry::Owner(mut replacement)) = + self.handles.remove(&replacement_handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + if cleanup.failure().is_some() { + let replacement_cleanup = self.custody.close(&mut replacement.token, registry); + if self + .custody + .validate_owned(&replacement.token, registry) + .is_ok() + { + self.handles + .insert(replacement_handle, NativeResourceEntry::Owner(replacement)); + } + self.cleanup(cleanup)?; + return self + .cleanup(replacement_cleanup) + .and(Err(NativeResourceError::BodyFailed)); + } + if let Err(error) = self + .custody + .transfer(&mut replacement.token, &holder, registry) + { + self.handles + .insert(replacement_handle, NativeResourceEntry::Owner(replacement)); + return Err(error.into()); + } + replacement.frame = old_frame; + replacement.slot = old_slot; + self.handles + .insert(handle, NativeResourceEntry::Owner(replacement)); + Ok(handle) + } + pub(super) fn sum_take( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + sum_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + if self.carrier_adapters.contains_key(&sum_handle) { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::SumTake { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.slot_empty(destination_frame, destination)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&sum_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != source { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_frame_at(frame, sum_handle, source)?; + self.sum_unborrowed(sum_handle)?; + let token = match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.custody.validate_owned(token, registry)?; + let mut ids = self.reserve(1)?; + let new_handle = next_handle(&mut ids)?; + let holder = self.custody.holder( + &self.frame(destination_frame)?.token, + &self.layout.slot(destination)?, + )?; + let Some(NativeResourceEntry::Sum(mut sum)) = self.handles.remove(&sum_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let token = match &mut sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => { + self.handles + .insert(sum_handle, NativeResourceEntry::Sum(sum)); + return Err(NativeResourceError::WrongOperation); + } + }; + if let Err(error) = self.custody.transfer(token, &holder, registry) { + self.handles + .insert(sum_handle, NativeResourceEntry::Sum(sum)); + return Err(error.into()); + } + let token = match sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.handles.insert( + new_handle, + NativeResourceEntry::Owner(NativeOwnerEntry { + frame: destination_frame, + slot: destination, + token, + }), + ); + Ok(new_handle) + } + pub(super) fn transfer( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + destination_frame: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, destination) = match self.operation(operation)? { + NativeOperation::Transfer { + source, + destination, + .. + } => (*source, *destination), + _ => return Err(NativeResourceError::WrongOperation), + }; + self.carrier_frame_at(frame, owner_handle, source)?; + self.move_carrier( + registry, + owner_handle, + source, + destination, + destination_frame, + ) + } + + pub(super) fn borrow_begin( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let (source, lease_frame) = match self.operation(operation)? { + NativeOperation::Borrow { + source, + lease_frame, + .. + } => (*source, *lease_frame), + _ => return Err(NativeResourceError::WrongOperation), + }; + if self.frame(frame)?.template != lease_frame { + return Err(NativeResourceError::WrongFrame); + } + self.owner(owner_handle, source)?; + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + let Some(NativeResourceEntry::Owner(owner)) = self.handles.get(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let source_frame = owner.frame; + let Some(NativeResourceEntry::Frame(frame_header)) = self.handles.get(&frame) else { + return Err(NativeResourceError::WrongFrame); + }; + let token = self + .custody + .begin_borrow(&owner.token, &frame_header.token, registry)?; + self.handles.insert( + handle, + NativeResourceEntry::Loan(NativeLoanEntry { + frame, + borrow_operation: operation, + source_slot: source, + source_frame, + token, + parent_sum: None, + }), + ); + Ok(handle) + } + pub(super) fn exact_loan( + &self, + handle: ResourceHandleId, + source: NativeLoanSource, + ) -> ResourceResult<&NativeLoanEntry> { + let Some(NativeResourceEntry::Loan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + match source { + NativeLoanSource::ExistingBorrow { operation } + if operation == loan.borrow_operation + && loan.parent_sum.is_none() + && matches!(self.operation(operation)?, NativeOperation::Borrow { .. }) => + { + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + if self.activation_frame(current, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + } + NativeLoanSource::ProjectedSumPayload { operation } + if operation == loan.borrow_operation && loan.parent_sum.is_some() => + { + let NativeOperation::ProjectSumView { source, .. } = self.operation(operation)? + else { + return Err(NativeResourceError::WrongOperation); + }; + let current = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + if self.activation_frame(current, self.frame(loan.frame)?.template)? != loan.frame { + return Err(NativeResourceError::WrongFrame); + } + let parent = loan.parent_sum.ok_or(NativeResourceError::WrongFamily)?; + // The issuer's own incoming/formal association remains required. + self.projected_sum_parent(loan.frame, parent, *source)?; + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + self.validate_resident(handle, scope, parameter)?; + } + _ => return Err(NativeResourceError::WrongOperation), + } + self.frame(loan.frame)?; + self.frame(loan.source_frame)?; + Ok(loan) + } + pub(super) fn invoke_borrow( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + call_handle: ResourceHandleId, + network_bits: u64, + loan_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.runtime_purpose()?; + let call = self.checked_call(call_handle)?; + let (hook, source) = match self.operation(call.target_operation)? { + NativeOperation::InvokeBorrow { hook, source, .. } => (*hook, *source), + _ => return Err(NativeResourceError::WrongOperation), + }; + let hook = self + .layout + .hooks() + .get(hook as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if hook.recipe() != NativeRecipe::NetworkBorrow { + return Err(NativeResourceError::WrongOperation); + } + let kind = hook.kind(); + let loan = self.exact_loan(loan_handle, source)?; + if self.layout.slot_kind(loan.source_slot)? != kind { + return Err(NativeResourceError::WrongOperation); + } + let key = self.custody.validate_borrowed(&loan.token, registry)?; + let provenance = self.grant(ordinary, network_bits)?.1; + let kind_id = self.layout.kind_id(kind)?; + let mut output_ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let result = catch_unwind(AssertUnwindSafe(|| { + registry.access::(key, kind_id, &provenance, |payload| { + self.provider.borrow(payload) + }) + })); + self.handles.remove(&call_handle); + let result = match result { + Ok(Ok(Ok(result))) => result, + Ok(Ok(Err(error))) => { + self.record_failure(error); + return Err(error); + } + Ok(Err(error)) => { + let error = NativeResourceError::Registry(error); + self.record_failure(error); + return Err(error); + } + Err(panic) => { + discard_panic_payload(panic); + if let Some(attempt) = &mut self.attempt { + attempt.body.get_or_insert(NativeBodyFailure::HostPanic); + } + return Err(NativeResourceError::BodyFailed); + } + }; + ordinary + .publish_resource_borrow_result(result, &mut output_ids) + .map_err(ordinary_error) + } + pub(super) fn borrow_end( + &mut self, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.operation_frame(operation, frame)?; + let borrow = match self.operation(operation)? { + NativeOperation::EndBorrow { borrow, .. } => *borrow, + _ => return Err(NativeResourceError::WrongOperation), + }; + let source = match self.operation(borrow)? { + NativeOperation::Borrow { .. } => { + NativeLoanSource::ExistingBorrow { operation: borrow } + } + NativeOperation::ProjectSumView { .. } => { + NativeLoanSource::ProjectedSumPayload { operation: borrow } + } + _ => return Err(NativeResourceError::WrongOperation), + }; + self.exact_loan(handle, source)?; + let Some(NativeResourceEntry::Loan(mut loan)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if let Err(error) = self.custody.end_borrow(&mut loan.token) { + self.handles.insert(handle, NativeResourceEntry::Loan(loan)); + return Err(error.into()); + } + Ok(()) + } + pub(super) fn close_or_drop( + &mut self, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + owner_handle: ResourceHandleId, + ) -> ResourceResult<()> { + self.runtime_purpose()?; + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::Close { hook, source, .. } + if self + .layout + .hooks() + .get(*hook as usize) + .is_some_and(|hook| hook.recipe() == NativeRecipe::Finalize) => + { + *source + } + NativeOperation::Drop { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.owner(owner_handle, source)?; + let Some(NativeResourceEntry::Owner(mut owner)) = self.handles.remove(&owner_handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = self.custody.close(&mut owner.token, registry); + if self.custody.validate_owned(&owner.token, registry).is_ok() { + self.handles + .insert(owner_handle, NativeResourceEntry::Owner(owner)); + } + self.cleanup(cleanup) + } + pub(super) fn sum_drop( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongOperation); + } + self.operation_frame(operation, frame)?; + let source = match self.operation(operation)? { + NativeOperation::SumDrop { source, .. } => *source, + _ => return Err(NativeResourceError::WrongOperation), + }; + match self.handles.get(&handle) { + Some(NativeResourceEntry::Sum(sum)) if sum.slot == source => {} + _ => return Err(NativeResourceError::WrongOperation), + } + self.carrier_frame_at(frame, handle, source)?; + self.sum_unborrowed(handle)?; + let Some(NativeResourceEntry::Sum(mut sum)) = self.handles.remove(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let cleanup = match &mut sum.payload { + NativeSumPayload::Ok(token) | NativeSumPayload::Some(token) => { + self.custody.close(token, registry) + } + NativeSumPayload::Fail { shape, string } => { + if let Err(error) = + ordinary.drop_resource_typed_companion(&self.layout, *shape, *string) + { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + ordinary.cleanup_failed = true; + return Err(ordinary_error(error)); + } + ResourceCleanupOutcome::default() + } + NativeSumPayload::None => ResourceCleanupOutcome::default(), + }; + if let NativeSumPayload::Ok(token) | NativeSumPayload::Some(token) = &sum.payload { + if self.custody.validate_owned(token, registry).is_ok() { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + } + } + self.cleanup(cleanup) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs new file mode 100644 index 00000000..eccb493e --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_attempt_owner_tests.rs @@ -0,0 +1,187 @@ +//! Private forged-proof negatives; these mint no public Source or ABI authority. +use super::*; + +#[test] +fn ordinary_attempt_owner_checks_every_retained_identity_component() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_state(|_, state| { + let owner = state + .resource_state + .as_ref() + .unwrap() + .attempt + .as_ref() + .unwrap() + .ordinary_owner; + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + let mut wrong_context = owner; + wrong_context.context.token = wrong_context.context.token.wrapping_add(1); + let mut wrong_program = owner; + wrong_program.program = ResourceHandleId::new(owner.program.raw() + 1).unwrap(); + let mut wrong_entry = owner; + wrong_entry.entry.function += 1; + let mut wrong_signature = owner; + wrong_signature.entry.signature += 1; + let mut wrong_scope = owner; + wrong_scope.entry.scope += 1; + let mut wrong_root = owner; + wrong_root.root = ResourceHandleId::new(owner.root.raw() + 1).unwrap(); + let mut wrong_attempt = owner; + wrong_attempt.attempt = ResourceHandleId::new(owner.attempt.raw() + 1).unwrap(); + let mut wrong_purpose = owner; + wrong_purpose.purpose = ResourcePurpose::Property; + let mut wrong_ordinal = owner; + wrong_ordinal.ordinal += 1; + for forged in [ + wrong_context, + wrong_program, + wrong_entry, + wrong_signature, + wrong_scope, + wrong_root, + wrong_attempt, + wrong_purpose, + wrong_ordinal, + ] { + assert!( + state + .values + .validate_resource_ordinary_attempt(forged) + .is_err() + ); + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + } + Ok(()) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + }); +} + +#[test] +fn malformed_ordinary_completion_cannot_seal_or_replace_the_running_channel() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_state(|_, state| { + let owner = state + .resource_state + .as_ref() + .unwrap() + .attempt + .as_ref() + .unwrap() + .ordinary_owner; + let base = ResourceOrdinaryCompletion { + owner, + completion: NativeResourceCompletion { + attempt: owner.attempt.raw(), + body_status: 0, + cleanup_status: 0, + selected_kind: 0, + reserved: 0, + }, + diagnostic_ready: true, + }; + let mut wrong_attempt = base; + wrong_attempt.completion.attempt += 1; + let mut wrong_reserved = base; + wrong_reserved.completion.reserved = 1; + let mut wrong_channel = base; + wrong_channel.completion.selected_kind = 4; + let mut wrong_owner = base; + wrong_owner.owner.ordinal += 1; + for forged in [wrong_attempt, wrong_reserved, wrong_channel, wrong_owner] { + assert!(state.values.seal_resource_ordinary_attempt(forged).is_err()); + state + .values + .validate_resource_ordinary_attempt(owner) + .unwrap(); + } + assert!(state.resource_state.as_ref().unwrap().completed.is_empty()); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + }); +} + +#[test] +fn sealed_ordinary_predecessor_requires_the_exact_saved_completion_and_ordinal() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let first = complete(auth, attempt, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let previous = resource.completed.last().unwrap().ordinary; + let owner = previous.owner; + assert!( + state + .values + .validate_resource_ordinary_attempt(owner) + .is_err() + ); + assert!( + state + .values + .seal_resource_ordinary_attempt(previous) + .is_err() + ); + let mut next = owner; + next.attempt = ResourceHandleId::new(owner.attempt.raw() + 10).unwrap(); + next.root = ResourceHandleId::new(owner.root.raw() + 10).unwrap(); + next.ordinal += 1; + let mut wrong_saved = previous; + wrong_saved.completion.body_status = 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(next, Some(wrong_saved)) + .is_err() + ); + let mut wrong_ordinal = next; + wrong_ordinal.ordinal += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(wrong_ordinal, Some(previous)) + .is_err() + ); + let mut foreign = next; + foreign.context.token = foreign.context.token.wrapping_add(1); + assert!( + state + .values + .prepare_resource_ordinary_attempt(foreign, Some(previous)) + .is_err() + ); + let mut wrong_entry = next; + wrong_entry.entry.signature += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(wrong_entry, Some(previous)) + .is_err() + ); + assert_eq!(resource.completed_attempt(attempt)?, first); + assert!(resource.completed_messages(attempt)?.1.is_empty()); + Ok(()) + }) + .unwrap(); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert!(second.raw() > attempt.raw()); + complete(auth, second, 0); + assert_empty(auth); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs new file mode 100644 index 00000000..dfe5cf60 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_error_reentry_tests.rs @@ -0,0 +1,694 @@ +//! Host lifetime controls. These are not Source admission or linked acceptance. +//! Registered as a child of `resource/tests.rs` to reuse its genuine fixture. +use super::*; +use std::ptr; + +const BOUNDS: &[u8] = b"list.__remove_at: index -1 out of bounds"; + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn current_message(auth: &AuthenticatedResourceContext) -> Vec { + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + ptr::null_mut(), + 0, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + let mut bytes = vec![0; usize::try_from(length).unwrap()]; + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + length, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + bytes +} + +fn saved( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, +) -> (NativeResourceCompletion, Vec, Vec) { + auth.with_resource(|state, _, _| { + let completion = state.completed_attempt(attempt)?; + let (resource, ordinary) = state.completed_messages(attempt)?; + Ok((completion, resource.to_vec(), ordinary.to_vec())) + }) + .unwrap() +} + +fn set_prefix(auth: &AuthenticatedResourceContext, prefix: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + prefix.as_ptr(), + u64::try_from(prefix.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_eq!(values::jett_rt_v1_property_case_set(pointer(auth), text), 0); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn bounds_failure(auth: &AuthenticatedResourceContext, index: i64) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!(values::jett_rt_v1_list_append(pointer(auth), list, 1), list); + assert_eq!( + values::jett_rt_v1_list_remove_at(pointer(auth), list, index), + 0 + ); + assert_eq!( + values::jett_rt_v1_value_status(pointer(auth)), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + } +} + +fn dynamic_failure(auth: &AuthenticatedResourceContext, message: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + message.as_ptr(), + u64::try_from(message.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_ne!( + values::jett_rt_v1_assert_fail_message(pointer(auth), text), + 0 + ); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn successful_leaf(auth: &AuthenticatedResourceContext) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!( + values::jett_rt_v1_list_append(pointer(auth), list, 17), + list + ); + assert_eq!(values::jett_rt_v1_list_length(pointer(auth), list), 1); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + assert_eq!(values::jett_rt_v1_value_status(pointer(auth)), 0); + } +} + +fn begin_only(auth: &AuthenticatedResourceContext) -> ResourceResult { + // Deliberately use with_state: a refused host preflight is not a Source body fault. + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime, + ) + }) +} + +fn assert_no_effects(auth: &AuthenticatedResourceContext) { + assert_empty(auth); + assert!(events(auth).is_empty()); + auth.with_resource(|state, _, _| { + assert_eq!(state.script_remaining(), 0); + Ok(()) + }) + .unwrap(); +} + +fn poisoned_context(operation: impl FnOnce(&AuthenticatedResourceContext)) { + let mut record = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *record, &mut result) + }, + JettRuntimeStatusV1::OK + ); + let authenticated = AuthenticatedResourceContext::acquire(&*record).unwrap(); + operation(&authenticated); + drop(authenticated); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *record, &mut result) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(result.status, JettRuntimeStatusV1::INVALID_ARGUMENT); + // No failure-taking operation or cleanup-poison reset is used for teardown. +} + +#[test] +fn ordinary_error_reentry_signed_bounds_twice_keeps_both_records_and_grant() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + assert_eq!(current_message(auth), BOUNDS); + let first_completion = complete(auth, first, 1); + assert_eq!( + ( + first_completion.body_status, + first_completion.cleanup_status, + first_completion.selected_kind + ), + (1, 0, 1) + ); + let first_saved = saved(auth, first); + assert_eq!(first_saved.2, BOUNDS); + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert!(second.raw() > first.raw()); + assert_eq!(second_grant, grant); + assert!(current_message(auth).is_empty()); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + bounds_failure(auth, -1); + let second_completion = complete(auth, second, 1); + assert_eq!( + ( + second_completion.body_status, + second_completion.cleanup_status, + second_completion.selected_kind + ), + (1, 0, 1) + ); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_distinct_signed_errors_capture_distinct_prefixes() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + set_prefix(auth, b"property 'first' trial 2: "); + bounds_failure(auth, -1); + complete(auth, first, 17); + let first_saved = saved(auth, first); + assert_eq!(first_saved.0.body_status, 17); + assert_eq!( + first_saved.2, + b"property 'first' trial 2: list.__remove_at: index -1 out of bounds" + ); + + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + set_prefix(auth, b"property 'second' trial 9: "); + bounds_failure(auth, 7); + complete(auth, second, 23); + assert_eq!( + saved(auth, second).2, + b"property 'second' trial 9: list.__remove_at: index 7 out of bounds" + ); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_failure_then_success_then_different_failure_runs_real_leaves() { + context(|auth| { + install(auth, &[], 3); + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, first, 1); + let first_saved = saved(auth, first); + let (second, _, _, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + successful_leaf(auth); + let success = complete(auth, second, 0); + assert_eq!( + ( + success.body_status, + success.cleanup_status, + success.selected_kind + ), + (0, 0, 0) + ); + assert!(current_message(auth).is_empty()); + let success_saved = saved(auth, second); + assert!(success_saved.1.is_empty() && success_saved.2.is_empty()); + + let (third, _, _, third_grant) = start(auth, ResourcePurpose::Runtime); + assert!(first.raw() < second.raw() && second.raw() < third.raw()); + assert_eq!(grant, third_grant); + dynamic_failure(auth, b"third terminal message"); + complete(auth, third, 29); + assert_eq!(saved(auth, third).2, b"third terminal message"); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(saved(auth, second), success_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_success_then_error_does_not_reuse_success_channel() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + successful_leaf(auth); + complete(auth, first, 0); + let first_saved = saved(auth, first); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, second, 1); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_first_failure_blocks_later_normal_leaf_without_replacing_message() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let replacement = b"must never replace the first diagnostic"; + let text = unsafe { + values::jett_rt_v1_string_literal( + pointer(auth), + replacement.as_ptr(), + u64::try_from(replacement.len()).unwrap(), + ) + }; + assert_ne!(text, 0); + bounds_failure(auth, -1); + assert_ne!( + unsafe { values::jett_rt_v1_assert_fail_message(pointer(auth), text) }, + 0 + ); + assert_eq!(unsafe { values::jett_rt_v1_list_new(pointer(auth), 0) }, 0); + assert_eq!(current_message(auth), BOUNDS); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), text) }, + 0 + ); + complete(auth, attempt, 31); + assert_eq!(saved(auth, attempt).2, BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_pre_entry_session_failure_is_not_issued_an_attempt() { + context(|auth| { + install(auth, &[], 2); + dynamic_failure(auth, b"session before first entry"); + let message = current_message(auth); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + assert_eq!(current_message(auth), message); + auth.with_resource(|state, _, _| { + assert!(state.attempt.is_none() && state.completed.is_empty()); + Ok(()) + }) + .unwrap(); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_between_entry_error_is_session_owned_and_old_receipt_survives() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + successful_leaf(auth); + complete(auth, first, 0); + let first_saved = saved(auth, first); + dynamic_failure(auth, b"session between entries"); + assert_eq!(current_message(auth), b"session between entries"); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(current_message(auth), b"session between entries"); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_refused_begin_preserves_sealed_error_and_can_retry_after_owner_drop() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, first, 1); + let first_saved = saved(auth, first); + let text = + unsafe { values::jett_rt_v1_string_literal(pointer(auth), b"resident".as_ptr(), 8) }; + assert_ne!(text, 0); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(current_message(auth), BOUNDS); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), text) }, + 0 + ); + let second = begin_only(auth).unwrap(); + successful_leaf(auth); + assert_eq!(complete(auth, second, 0).selected_kind, 0); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_wrong_entry_active_and_duplicate_completion_do_not_advance_history() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert!( + resource + .begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime + ) + .is_err() + ); + let wrong = ResourceHandleId::new(first.raw().checked_add(1).unwrap())?; + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, wrong, 0, false) + .is_err() + ); + assert!(resource.completed.is_empty()); + Ok(()) + }) + .unwrap(); + assert_eq!(current_message(auth), BOUNDS); + complete(auth, first, 1); + let first_saved = saved(auth, first); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + let mut wrong_entry = entry(); + wrong_entry.signature = wrong_entry.signature.checked_add(1).unwrap(); + assert!( + resource + .begin_entry( + &mut state.values, + &state.resources, + wrong_entry, + ResourcePurpose::Runtime + ) + .is_err() + ); + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, first, 0, false) + .is_err() + ); + assert_eq!(resource.completed.len(), 1); + Ok(()) + }) + .unwrap(); + assert_eq!(saved(auth, first), first_saved); + let second = begin_only(auth).unwrap(); + successful_leaf(auth); + complete(auth, second, 0); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_foreign_attempt_cannot_complete_or_query_this_context() { + context(|first_auth| { + context(|second_auth| { + install(first_auth, &[], 1); + install(second_auth, &[], 1); + let (first, _, _, _) = start(first_auth, ResourcePurpose::Runtime); + let (second, _, _, _) = start(second_auth, ResourcePurpose::Runtime); + assert_ne!(first, second); + bounds_failure(first_auth, -1); + dynamic_failure(second_auth, b"second context error"); + for (auth, foreign) in [(first_auth, second), (second_auth, first)] { + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert!( + resource + .complete_entry( + &mut state.values, + &mut state.resources, + foreign, + 0, + false + ) + .is_err() + ); + assert!(resource.completed_attempt(foreign).is_err()); + assert!(resource.completed_messages(foreign).is_err()); + Ok(()) + }) + .unwrap(); + } + complete(first_auth, first, 1); + complete(second_auth, second, 1); + assert_eq!(saved(first_auth, first).2, BOUNDS); + assert_eq!(saved(second_auth, second).2, b"second context error"); + assert_no_effects(first_auth); + assert_no_effects(second_auth); + }); + }); +} + +#[test] +fn ordinary_error_reentry_exhausted_budget_preserves_the_last_completed_error() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let before = saved(auth, attempt); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_maximum_retained_history_is_bounded_and_exact() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let mut history = Vec::new(); + for _ in 0..MAX_ATTEMPTS { + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let observation = saved(auth, attempt); + assert_eq!(observation.2, BOUNDS); + history.push((attempt, observation)); + } + assert_eq!(history.len(), MAX_ATTEMPTS); + assert!(begin_only(auth).is_err()); + assert!(begin_only(auth).is_err()); + for (attempt, observation) in history { + assert_eq!(saved(auth, attempt), observation); + } + assert_eq!(current_message(auth), BOUNDS); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_diagnostic_overflow_retains_tombstone_without_cleanup_retry() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let message = vec![b'x'; 64 * 1024 + 1]; + dynamic_failure(auth, &message); + assert_eq!(current_message(auth), message); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert_eq!( + resource.complete_entry(&mut state.values, &mut state.resources, attempt, 1, false), + Err(NativeResourceError::Capacity) + ); + assert_eq!(resource.completed.len(), 1); + assert_eq!( + resource.completed_attempt(attempt), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + resource.completed_messages(attempt), + Err(NativeResourceError::Capacity) + ); + assert!( + resource + .complete_entry(&mut state.values, &mut state.resources, attempt, 1, false) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert!(begin_only(auth).is_err()); + assert_eq!(current_message(auth), message); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_real_cleanup_poison_preserves_first_error_and_blocks_constructor() { + poisoned_context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let list = unsafe { values::jett_rt_v1_list_new(pointer(auth), 0) }; + assert_ne!(list, 0); + assert_eq!( + unsafe { values::jett_rt_v1_list_append(pointer(auth), list, 1) }, + list + ); + assert_eq!( + unsafe { values::jett_rt_v1_list_remove_at(pointer(auth), list, -1) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), list) }, + 0 + ); + assert_ne!( + unsafe { values::jett_rt_v1_value_drop(pointer(auth), list) }, + 0 + ); + assert_eq!(current_message(auth), BOUNDS); + let completion = complete(auth, attempt, 1); + assert_eq!(completion.selected_kind, 3); + assert_ne!(completion.cleanup_status, 0); + let before = saved(auth, attempt); + assert_eq!(before.2, BOUNDS); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_sealed_take_refuses_and_poison_cannot_erase_history() { + poisoned_context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, attempt, 1); + let before = saved(auth, attempt); + assert_eq!( + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer(auth), ptr::null(), 0) }, + 0 + ); + assert_eq!(saved(auth, attempt), before); + // The invalid between-entry take belongs to the sticky session channel. + // Its poison may take observation precedence; the sealed body stays exact. + let session_message = current_message(auth); + assert!(!session_message.is_empty()); + assert_eq!(before.2, BOUNDS); + assert!(begin_only(auth).is_err()); + assert_eq!(saved(auth, attempt), before); + assert_eq!(current_message(auth), session_message); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_active_refinement_capture_remains_data_before_completion() { + context(|auth| { + install(auth, &[], 2); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + let captured = unsafe { + values::jett_rt_v1_failure_take_prefixed_text(pointer(auth), b"handled: ".as_ptr(), 9) + }; + assert_ne!(captured, 0); + assert_eq!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + let expected = b"handled: list.__remove_at: index -1 out of bounds"; + let expected_text = unsafe { + values::jett_rt_v1_string_literal( + pointer(auth), + expected.as_ptr(), + u64::try_from(expected.len()).unwrap(), + ) + }; + assert_ne!(expected_text, 0); + assert_eq!( + unsafe { values::jett_rt_v1_string_equal(pointer(auth), captured, expected_text) }, + 1 + ); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), captured) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_string_release(pointer(auth), expected_text) }, + 0 + ); + successful_leaf(auth); + assert_eq!(complete(auth, first, 0).selected_kind, 0); + let first_saved = saved(auth, first); + assert!(first_saved.2.is_empty()); + let (second, _, _, _) = start(auth, ResourcePurpose::Runtime); + bounds_failure(auth, -1); + complete(auth, second, 1); + assert_eq!(saved(auth, second).2, BOUNDS); + assert_eq!(saved(auth, first), first_saved); + assert_no_effects(auth); + }); +} + +#[test] +fn ordinary_error_reentry_historical_resource_cleanup_panic_still_allows_clean_next_entry() { + context(|auth| { + install(auth, &[(3, 41, 0, ""), (1, 42, 0, "")], 2); + let (first, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 41); + let first_completion = complete(auth, first, 0); + assert_eq!( + ( + first_completion.body_status, + first_completion.cleanup_status, + first_completion.selected_kind + ), + (0, JettRuntimeStatusV1::PANIC.code(), 3) + ); + let first_saved = saved(auth, first); + assert_eq!(first_saved.1, CLEANUP_FAILED); + assert!(first_saved.2.is_empty()); + assert_empty(auth); + + let (second, _, frame, second_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(grant, second_grant); + construct(auth, frame, second_grant, 42); + assert_eq!(complete(auth, second, 0).selected_kind, 0); + assert_eq!(saved(auth, first), first_saved); + assert_eq!(events(auth), vec![(41, 1), (41, 5), (42, 1), (42, 5)]); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.script_remaining(), 0); + Ok(()) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs new file mode 100644 index 00000000..20bf34df --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_failure_tests.rs @@ -0,0 +1,816 @@ +//! Deterministic host allocation-refusal controls, not linked Source acceptance. +use super::*; +use crate::native_abi::resource::ordinary_reservation_faults::ReservationSite; +use std::ptr; + +const BOUNDS: &[u8] = b"list.__remove_at: index -1 out of bounds"; +type History = Vec<(NativeResourceCompletion, Vec, Vec)>; + +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} + +fn message(auth: &AuthenticatedResourceContext) -> Vec { + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + ptr::null_mut(), + 0, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + let mut bytes = vec![0; usize::try_from(length).unwrap()]; + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + length, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + bytes +} + +fn prefix(auth: &AuthenticatedResourceContext, bytes: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + bytes.as_ptr(), + u64::try_from(bytes.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_eq!(values::jett_rt_v1_property_case_set(pointer(auth), text), 0); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn bounds(auth: &AuthenticatedResourceContext) { + unsafe { + let list = values::jett_rt_v1_list_new(pointer(auth), 0); + assert_ne!(list, 0); + assert_eq!(values::jett_rt_v1_list_append(pointer(auth), list, 1), list); + assert_eq!( + values::jett_rt_v1_list_remove_at(pointer(auth), list, -1), + 0 + ); + assert_eq!( + values::jett_rt_v1_value_status(pointer(auth)), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(values::jett_rt_v1_value_drop(pointer(auth), list), 0); + } +} + +fn dynamic_failure(auth: &AuthenticatedResourceContext, bytes: &[u8]) { + unsafe { + let text = values::jett_rt_v1_string_literal( + pointer(auth), + bytes.as_ptr(), + u64::try_from(bytes.len()).unwrap(), + ); + assert_ne!(text, 0); + assert_ne!( + values::jett_rt_v1_assert_fail_message(pointer(auth), text), + 0 + ); + assert_eq!(values::jett_rt_v1_string_release(pointer(auth), text), 0); + } +} + +fn begin_raw(auth: &AuthenticatedResourceContext) -> ResourceResult { + // Constructor preflight refusal is not an entered Source body failure. + auth.with_state(|_, state| { + state.resource_state.as_mut().unwrap().begin_entry( + &mut state.values, + &state.resources, + entry(), + ResourcePurpose::Runtime, + ) + }) +} + +fn complete_raw( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body: u32, +) -> ResourceResult { + auth.with_state(|_, state| { + state.resource_state.as_mut().unwrap().complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body, + false, + ) + }) +} + +fn history(auth: &AuthenticatedResourceContext) -> History { + auth.with_resource(|state, _, _| { + Ok(state + .completed + .iter() + .map(|row| { + ( + row.completion, + row.resource_message.clone(), + row.ordinary_message.clone(), + ) + }) + .collect()) + }) + .unwrap() +} + +fn channel_state( + auth: &AuthenticatedResourceContext, +) -> ( + usize, + Option, + Option, +) { + auth.with_state(|_, state| Ok(state.values.test_resource_ordinary_channel_state())) + .unwrap() +} + +fn arm(auth: &AuthenticatedResourceContext, site: ReservationSite) { + auth.with_state(|_, state| { + match site { + ReservationSite::OrdinaryChannel => { + state.values.test_arm_resource_ordinary_reservation() + } + ReservationSite::BeginActiveFrames + | ReservationSite::BeginHistory + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => state + .resource_state + .as_mut() + .unwrap() + .reservation_faults + .arm(site), + } + Ok(()) + }) + .unwrap(); +} + +fn fault_snapshot( + auth: &AuthenticatedResourceContext, + site: ReservationSite, +) -> (usize, usize, bool) { + auth.with_state(|_, state| { + Ok(match site { + ReservationSite::OrdinaryChannel => { + state.values.test_resource_ordinary_reservation_snapshot() + } + ReservationSite::BeginActiveFrames + | ReservationSite::BeginHistory + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => state + .resource_state + .as_ref() + .unwrap() + .reservation_faults + .snapshot(site), + }) + }) + .unwrap() +} + +fn assert_injected(before: (usize, usize, bool), after: (usize, usize, bool)) { + assert!(before.2); + assert_eq!(after, (before.0 + 1, before.1 + 1, false)); +} + +fn expected_begin_error(site: ReservationSite) -> NativeResourceError { + match site { + ReservationSite::OrdinaryChannel => { + NativeResourceError::OrdinaryStorage(JettRuntimeStatusV1::RESOURCE_EXHAUSTED) + } + ReservationSite::BeginActiveFrames | ReservationSite::BeginHistory => { + NativeResourceError::Capacity + } + ReservationSite::ResourceDiagnostic | ReservationSite::OrdinaryDiagnostic => { + panic!("not an entry preflight site") + } + } +} + +fn assert_idle(auth: &AuthenticatedResourceContext, remaining: usize) { + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert!(state.attempt.is_none()); + assert!(state.only_installation_handles()); + assert_eq!(state.script_remaining(), remaining); + Ok(()) + }) + .unwrap(); +} + +fn fill_to_growth(auth: &AuthenticatedResourceContext, site: ReservationSite) -> History { + let (first, _, _, grant) = start(auth, ResourcePurpose::Runtime); + bounds(auth); + assert_eq!(complete(auth, first, 1).selected_kind, 1); + let capacity = auth + .with_state(|_, state| { + Ok(match site { + ReservationSite::BeginHistory => { + state.resource_state.as_ref().unwrap().completed.capacity() + } + ReservationSite::OrdinaryChannel => { + state.values.test_resource_ordinary_channel_capacity().1 + } + ReservationSite::BeginActiveFrames + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => panic!("not a growable history site"), + }) + }) + .unwrap(); + // Derived from actual Vec capacity, never a fixed assumed growth factor. + assert!(capacity >= 1 && capacity < MAX_ATTEMPTS); + for _ in 1..capacity { + let (attempt, _, _, next_grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(next_grant, grant); + bounds(auth); + assert_eq!(complete(auth, attempt, 1).selected_kind, 1); + } + auth.with_state(|_, state| { + match site { + ReservationSite::BeginHistory => { + let history = &state.resource_state.as_ref().unwrap().completed; + assert_eq!(history.len(), history.capacity()); + } + ReservationSite::OrdinaryChannel => { + let (length, capacity) = state.values.test_resource_ordinary_channel_capacity(); + assert_eq!(length, capacity); + } + ReservationSite::BeginActiveFrames + | ReservationSite::ResourceDiagnostic + | ReservationSite::OrdinaryDiagnostic => panic!("not a growable history site"), + } + Ok(()) + }) + .unwrap(); + assert_idle(auth, 0); + history(auth) +} + +#[test] +fn ordinary_reservation_first_begin_refusals_publish_no_owner_or_provider_effect() { + for site in [ + ReservationSite::BeginActiveFrames, + ReservationSite::BeginHistory, + ReservationSite::OrdinaryChannel, + ] { + context(|auth| { + install(auth, &[(1, 811, 0, "")], 1); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!(begin_raw(auth), Err(expected_begin_error(site))); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(channel_state(auth), (0, None, None)); + assert!(history(auth).is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 1); + assert!(complete_raw(auth, ResourceHandleId::new(123_456).unwrap(), 0).is_err()); + assert_eq!(channel_state(auth), (0, None, None)); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 811); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_eq!(events(auth), vec![(811, 1), (811, 5)]); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_next_history_or_channel_growth_preserves_every_sealed_error() { + for site in [ + ReservationSite::BeginHistory, + ReservationSite::OrdinaryChannel, + ] { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let saved = fill_to_growth(auth, site); + let saved_channel = channel_state(auth); + let saved_message = message(auth); + assert_eq!(saved_message, BOUNDS); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!(begin_raw(auth), Err(expected_begin_error(site))); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), saved_message); + assert_idle(auth, 0); + let (next, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert!(message(auth).is_empty()); + assert_eq!(complete(auth, next, 0).selected_kind, 0); + let current = history(auth); + assert_eq!(¤t[..saved.len()], &saved); + assert_eq!(current.len(), saved.len() + 1); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_invalid_predecessor_never_reaches_or_consumes_channel_allocation() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let saved = fill_to_growth(auth, ReservationSite::OrdinaryChannel); + let saved_channel = channel_state(auth); + let saved_message = message(auth); + arm(auth, ReservationSite::OrdinaryChannel); + let before = fault_snapshot(auth, ReservationSite::OrdinaryChannel); + auth.with_state(|_, state| { + let previous = state + .resource_state + .as_ref() + .unwrap() + .completed + .last() + .unwrap() + .ordinary; + let mut forged = previous.owner; + forged.attempt = ResourceHandleId::new(forged.attempt.raw() + 10).unwrap(); + forged.root = ResourceHandleId::new(forged.root.raw() + 10).unwrap(); + forged.ordinal += 1; + forged.context.token = forged.context.token.wrapping_add(1); + assert!( + state + .values + .prepare_resource_ordinary_attempt(forged, Some(previous)) + .is_err() + ); + let mut wrong_saved = previous; + wrong_saved.completion.body_status += 1; + let mut next = previous.owner; + next.attempt = ResourceHandleId::new(next.attempt.raw() + 10).unwrap(); + next.root = ResourceHandleId::new(next.root.raw() + 10).unwrap(); + next.ordinal += 1; + assert!( + state + .values + .prepare_resource_ordinary_attempt(next, Some(wrong_saved)) + .is_err() + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + before + ); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(history(auth), saved); + assert_eq!(message(auth), saved_message); + assert_eq!( + begin_raw(auth), + Err(expected_begin_error(ReservationSite::OrdinaryChannel)) + ); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + ); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(history(auth), saved); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_wrong_or_active_entry_cannot_consume_prepublication_fault() { + context(|auth| { + install(auth, &[], 1); + arm(auth, ReservationSite::BeginActiveFrames); + let before = fault_snapshot(auth, ReservationSite::BeginActiveFrames); + auth.with_state(|_, state| { + let mut wrong = entry(); + wrong.signature += 1; + assert_eq!( + state.resource_state.as_mut().unwrap().begin_entry( + &mut state.values, + &state.resources, + wrong, + ResourcePurpose::Runtime, + ), + Err(NativeResourceError::ActiveAttempt) + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginActiveFrames), + before + ); + assert_eq!(begin_raw(auth), Err(NativeResourceError::Capacity)); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::BeginActiveFrames), + ); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + arm(auth, ReservationSite::BeginHistory); + let active_before = fault_snapshot(auth, ReservationSite::BeginHistory); + let channel = channel_state(auth); + assert_eq!(begin_raw(auth), Err(NativeResourceError::ActiveAttempt)); + assert_eq!(channel_state(auth), channel); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + active_before + ); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + active_before + ); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_sealing_uses_preflight_storage_and_keeps_pending_growth_faults() { + context(|auth| { + install(auth, &[(1, 821, 0, "")], 1); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 821); + prefix(auth, b"case 17: "); + bounds(auth); + let original_message = message(auth); + arm(auth, ReservationSite::BeginHistory); + arm(auth, ReservationSite::OrdinaryChannel); + let history_fault = fault_snapshot(auth, ReservationSite::BeginHistory); + let channel_fault = fault_snapshot(auth, ReservationSite::OrdinaryChannel); + let completion = complete(auth, attempt, 1); + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (1, 0, 1) + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + history_fault + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + channel_fault + ); + assert_eq!(channel_state(auth).2.unwrap().completion, completion); + assert_eq!(history(auth)[0].2, original_message); + assert_eq!(message(auth), original_message); + assert_eq!(events(auth), vec![(821, 1), (821, 5)]); + let saved = history(auth); + assert!(complete_raw(auth, attempt, 0).is_err()); + assert_eq!(history(auth), saved); + assert_eq!( + fault_snapshot(auth, ReservationSite::BeginHistory), + history_fault + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + channel_fault + ); + assert_idle(auth, 0); + }); +} + +fn assert_diagnostic_tombstone( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + cleanup: u32, + selected_kind: u32, + original_message: &[u8], +) { + assert_eq!(message(auth), original_message); + let selected = channel_state(auth).2.unwrap(); + assert!(!selected.diagnostic_ready()); + assert_eq!( + ( + selected.completion.body_status, + selected.completion.cleanup_status, + selected.completion.selected_kind + ), + (1, cleanup, selected_kind) + ); + auth.with_resource(|state, ordinary, _| { + let row = state.completed.last().unwrap(); + assert_eq!(row.completion.attempt, attempt.raw()); + assert_eq!(row.ordinary, selected); + assert_eq!(row.report_error, Some(NativeResourceError::Capacity)); + assert!(row.resource_message.is_empty() && row.ordinary_message.is_empty()); + assert_eq!( + state.completed_attempt(attempt), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + state.completed_messages(attempt), + Err(NativeResourceError::Capacity) + ); + assert!(!ordinary.cleanup_failed); + Ok(()) + }) + .unwrap(); + let saved = history(auth); + let before_events = events(auth); + assert!(complete_raw(auth, attempt, 0).is_err()); + assert!(begin_raw(auth).is_err()); + assert_eq!(history(auth), saved); + assert_eq!(events(auth), before_events); + assert_eq!(channel_state(auth).2, Some(selected)); + assert_eq!(message(auth), original_message); + assert_idle(auth, 0); +} + +#[test] +fn ordinary_reservation_ordinary_diagnostic_failure_seals_after_real_resource_retirement() { + context(|auth| { + install(auth, &[(1, 831, 0, "")], 3); + let (first, _, _, first_grant) = start(auth, ResourcePurpose::Runtime); + prefix(auth, b"earlier case: "); + bounds(auth); + assert_eq!(complete(auth, first, 1).selected_kind, 1); + let earlier_history = history(auth); + let earlier_proof = channel_state(auth).2.unwrap(); + assert_idle(auth, 1); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + assert_eq!(first_grant, grant); + construct(auth, frame, grant, 831); + prefix(auth, b"case 23: "); + bounds(auth); + let original_message = message(auth); + let expected = [b"case 23: ".as_slice(), BOUNDS].concat(); + assert_eq!(original_message, expected); + arm(auth, ReservationSite::OrdinaryDiagnostic); + let before = fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_injected( + before, + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + ); + assert_eq!(events(auth), vec![(831, 1), (831, 5)]); + assert_diagnostic_tombstone(auth, attempt, 0, 1, &original_message); + assert_eq!(&history(auth)[..earlier_history.len()], &earlier_history); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed[0].ordinary, earlier_proof); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn ordinary_reservation_either_diagnostic_failure_keeps_cleanup_selection_and_first_bounds_error() { + for site in [ + ReservationSite::ResourceDiagnostic, + ReservationSite::OrdinaryDiagnostic, + ] { + context(|auth| { + install(auth, &[(3, 841, 0, "")], 2); + let (attempt, _, frame, grant) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, grant, 841); + bounds(auth); + assert_eq!(message(auth), BOUNDS); + arm(auth, site); + let before = fault_snapshot(auth, site); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_injected(before, fault_snapshot(auth, site)); + assert_eq!(events(auth), vec![(841, 1), (841, 5)]); + assert_diagnostic_tombstone( + auth, + attempt, + JettRuntimeStatusV1::PANIC.code(), + 3, + BOUNDS, + ); + }); + } +} + +#[test] +fn ordinary_reservation_empty_diagnostics_do_not_request_or_consume_allocator_fault() { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + arm(auth, ReservationSite::ResourceDiagnostic); + let before = fault_snapshot(auth, ReservationSite::ResourceDiagnostic); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, 0); + assert_eq!( + fault_snapshot(auth, ReservationSite::ResourceDiagnostic), + before + ); + assert!(channel_state(auth).2.unwrap().diagnostic_ready()); + let saved = history(auth); + assert!(saved[0].1.is_empty() && saved[0].2.is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); +} + +#[test] +fn ordinary_reservation_length_refusal_precedes_and_preserves_pending_allocator_fault() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let original = vec![b'x'; 64 * 1024 + 1]; + dynamic_failure(auth, &original); + arm(auth, ReservationSite::OrdinaryDiagnostic); + let before = fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + before + ); + assert_diagnostic_tombstone(auth, attempt, 0, 1, &original); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryDiagnostic), + before + ); + assert!(events(auth).is_empty()); + }); +} + +#[test] +fn ordinary_reservation_faults_are_owned_by_one_context_and_never_poison_another() { + context(|first| { + context(|second| { + install(first, &[], 1); + install(second, &[], 1); + arm(first, ReservationSite::OrdinaryChannel); + let first_before = fault_snapshot(first, ReservationSite::OrdinaryChannel); + let (second_attempt, _, _, _) = start(second, ResourcePurpose::Runtime); + assert_eq!(complete(second, second_attempt, 0).selected_kind, 0); + assert_eq!( + fault_snapshot(first, ReservationSite::OrdinaryChannel), + first_before + ); + assert_eq!( + fault_snapshot(second, ReservationSite::OrdinaryChannel).1, + 0 + ); + assert_eq!( + begin_raw(first), + Err(expected_begin_error(ReservationSite::OrdinaryChannel)) + ); + assert_injected( + first_before, + fault_snapshot(first, ReservationSite::OrdinaryChannel), + ); + assert_eq!(channel_state(first), (0, None, None)); + let (first_attempt, _, _, _) = start(first, ResourcePurpose::Runtime); + assert_eq!(complete(first, first_attempt, 0).selected_kind, 0); + assert!(message(first).is_empty() && message(second).is_empty()); + assert!(events(first).is_empty() && events(second).is_empty()); + assert_idle(first, 0); + assert_idle(second, 0); + }); + }); +} + +#[test] +fn ordinary_reservation_custody_failure_after_preparation_publishes_no_channel_or_frame() { + use crate::resource_custody::FrameReservationSite; + for site in [ + FrameReservationSite::Frames, + FrameReservationSite::ActiveFrames, + ] { + context(|auth| { + install(auth, &[], 1); + let before = auth + .with_state(|_, state| { + let custody = &mut state.resource_state.as_mut().unwrap().custody; + custody.test_arm_frame_reservation(site); + Ok(custody.test_frame_reservation_snapshot(site)) + }) + .unwrap(); + assert_eq!( + begin_raw(auth), + Err(NativeResourceError::Custody( + CustodyError::CapacityExhausted + )) + ); + let after = auth + .with_state(|_, state| { + Ok(state + .resource_state + .as_ref() + .unwrap() + .custody + .test_frame_reservation_snapshot(site)) + }) + .unwrap(); + assert_injected(before, after); + // The ordinary Vec reservation succeeded before the real core failure, + // but dropping the prepared borrow did not publish its owner or record. + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + (1, 0, false) + ); + assert_eq!(channel_state(auth), (0, None, None)); + assert!(history(auth).is_empty()); + assert!(message(auth).is_empty()); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!( + fault_snapshot(auth, ReservationSite::OrdinaryChannel), + (1, 0, false) + ); + assert_eq!(channel_state(auth).0, 1); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert!(channel_state(auth).2.unwrap().diagnostic_ready()); + assert_idle(auth, 0); + }); + } +} + +#[test] +fn ordinary_reservation_earlier_history_growth_does_not_make_failed_diagnostics_reenterable() { + context(|auth| { + install(auth, &[], u32::try_from(MAX_ATTEMPTS).unwrap()); + let (first, _, _, _) = start(auth, ResourcePurpose::Runtime); + let capacity = auth + .with_resource(|state, _, _| Ok(state.completed.capacity())) + .unwrap(); + assert!(capacity >= 1 && capacity < MAX_ATTEMPTS); + let mut prepared_first = Some(first); + let mut tombstone = None; + for ordinal in 0..capacity { + let attempt = prepared_first + .take() + .unwrap_or_else(|| start(auth, ResourcePurpose::Runtime).0); + bounds(auth); + if ordinal + 1 == capacity { + arm(auth, ReservationSite::OrdinaryDiagnostic); + assert_eq!( + complete_raw(auth, attempt, 1), + Err(NativeResourceError::Capacity) + ); + tombstone = Some(attempt); + } else { + assert_eq!(complete(auth, attempt, 1).selected_kind, 1); + } + } + let tombstone = tombstone.unwrap(); + let saved = history(auth); + let saved_channel = channel_state(auth); + let original_message = message(auth); + assert_eq!(original_message, BOUNDS); + assert!(!saved_channel.2.unwrap().diagnostic_ready()); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed.len(), state.completed.capacity()); + assert_eq!( + state.completed_attempt(tombstone), + Err(NativeResourceError::Capacity) + ); + Ok(()) + }) + .unwrap(); + arm(auth, ReservationSite::BeginHistory); + let before = fault_snapshot(auth, ReservationSite::BeginHistory); + // Preserve the real constructor order: Resource history reserves earlier + // than exact ordinary predecessor validation. Both refusals are inert. + assert_eq!(begin_raw(auth), Err(NativeResourceError::Capacity)); + assert_injected(before, fault_snapshot(auth, ReservationSite::BeginHistory)); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), original_message); + assert_idle(auth, 0); + assert_eq!( + begin_raw(auth), + Err(NativeResourceError::OrdinaryStorage( + JettRuntimeStatusV1::INVALID_ARGUMENT + )) + ); + assert_eq!(history(auth), saved); + assert_eq!(channel_state(auth), saved_channel); + assert_eq!(message(auth), original_message); + assert!(events(auth).is_empty()); + assert_idle(auth, 0); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs new file mode 100644 index 00000000..e086ea78 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/ordinary_reservation_faults.rs @@ -0,0 +1,71 @@ +//! Private unit-test faults at real fallible ordinary-entry reservation sites. +//! This module, its instance state and every checkpoint are absent without cfg(test). +use super::{NativeResourceError, ResourceResult}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::native_abi) enum ReservationSite { + BeginActiveFrames, + BeginHistory, + OrdinaryChannel, + ResourceDiagnostic, + OrdinaryDiagnostic, +} + +impl ReservationSite { + fn index(self) -> usize { + match self { + Self::BeginActiveFrames => 0, + Self::BeginHistory => 1, + Self::OrdinaryChannel => 2, + Self::ResourceDiagnostic => 3, + Self::OrdinaryDiagnostic => 4, + } + } +} + +#[derive(Default)] +pub(in crate::native_abi) struct ReservationFaults { + armed: Option, + growth_requests: [usize; 5], + injected_refusals: [usize; 5], +} + +impl ReservationFaults { + pub(in crate::native_abi) fn arm(&mut self, site: ReservationSite) { + assert!( + self.armed.is_none(), + "only one allocation fault may be pending per state" + ); + self.armed = Some(site); + } + + // A reserve that reuses capacity cannot produce an allocator failure. The + // hook is directly before try_reserve and follows the guards preceding that + // actual reservation. The ordinary-channel hook follows the exact predecessor proof. + pub(in crate::native_abi) fn checkpoint( + &mut self, + site: ReservationSite, + allocation_needed: bool, + ) -> ResourceResult<()> { + if !allocation_needed { + return Ok(()); + } + let index = site.index(); + self.growth_requests[index] += 1; + if self.armed == Some(site) { + self.armed = None; + self.injected_refusals[index] += 1; + return Err(NativeResourceError::Capacity); + } + Ok(()) + } + + pub(in crate::native_abi) fn snapshot(&self, site: ReservationSite) -> (usize, usize, bool) { + let index = site.index(); + ( + self.growth_requests[index], + self.injected_refusals[index], + self.armed == Some(site), + ) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/program_entry.rs b/crates/jett_runtime/src/native_abi/resource/program_entry.rs new file mode 100644 index 00000000..8031f99a --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/program_entry.rs @@ -0,0 +1,383 @@ +//! Generated-object startup. No Source provider or runtime-purpose selector. +use super::*; +use std::mem::{align_of, size_of}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum EntryOwner { + External, + Generated, +} + +#[repr(C)] +struct ObjectManifest { + layout: *const u8, + layout_length: u64, + function: u32, + signature: u32, + scope: u32, + reserved: u32, +} +const _: () = assert!(size_of::() == 32 && align_of::() == 8); + +#[derive(Clone, Copy)] +struct Region { + start: usize, + end: usize, +} +fn region(pointer: *const T, count: usize) -> ResourceResult { + let length = size_of::() + .checked_mul(count) + .filter(|length| *length <= isize::MAX as usize) + .ok_or(NativeResourceError::Capacity)?; + let start = pointer.addr(); + if pointer.is_null() || start % align_of::() != 0 { + return Err(NativeResourceError::WrongOperation); + } + let end = start + .checked_add(length) + .ok_or(NativeResourceError::Capacity)?; + Ok(Region { start, end }) +} +fn separate(regions: &[Region]) -> ResourceResult<()> { + for (index, left) in regions.iter().enumerate() { + for right in ®ions[index + 1..] { + if left.start < right.end && right.start < left.end { + return Err(NativeResourceError::WrongOperation); + } + } + } + Ok(()) +} + +fn begin( + authenticated: &AuthenticatedResourceContext, + wire: &[u8], + entry: NativeEntry, +) -> ResourceResult { + authenticated.with_state(|key, state| { + if state.resource_state.is_none() { + install_candidate(key, state, wire, entry, InstalledProvider::Disabled)?; + state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)? + .entry_owner = EntryOwner::Generated; + } + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + resource.validate_installation()?; + if resource.entry != entry || resource.installed_wire != wire { + return Err(NativeResourceError::InvalidEntry); + } + if resource.entry_owner == EntryOwner::External { + // The matched test main already owns begin/complete. This is never + // an alternate production installation or attempt-adoption path. + #[cfg(all(test, jett_resource_native_test_archive))] + if matches!(resource.provider, InstalledProvider::Scripted(_)) + && resource.attempt.as_ref().is_some_and(|attempt| { + attempt.phase == AttemptPhase::Running + && attempt.entry == entry + && attempt.purpose == ResourcePurpose::Runtime + }) + { + return Ok(0); + } + return Err(NativeResourceError::ActiveAttempt); + } + let attempt = resource.begin_entry( + &mut state.values, + &state.resources, + entry, + ResourcePurpose::Runtime, + )?; + Ok(attempt.raw()) + }) +} + +/// # Safety +/// The stationary context is live. Manifest/layout remain readable for this +/// call; output records are distinct, aligned, exclusive writable storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_begin( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + out_attempt: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let regions = (|| { + let records = [ + region(context, 1)?, + region(manifest, 1)?, + region(out_attempt, 1)?, + region(out_result, 1)?, + ]; + separate(&records)?; + // SAFETY: caller supplies readable storage; known overlaps were refused. + let manifest = unsafe { &*manifest }; + let length = + usize::try_from(manifest.layout_length).map_err(|_| NativeResourceError::Capacity)?; + let layout = region(manifest.layout, length)?; + for record in records { + separate(&[layout, record])?; + } + if manifest.reserved != 0 || length == 0 { + return Err(NativeResourceError::InvalidEntry); + } + Ok(length) + })(); + let Ok(length) = regions else { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + }; + complete_call(out_result, || { + // SAFETY: caller keeps the validated readable regions stable for the call. + let manifest = unsafe { &*manifest }; + let wire = unsafe { std::slice::from_raw_parts(manifest.layout, length) }; + let entry = NativeEntry { + function: manifest.function, + signature: manifest.signature, + scope: manifest.scope, + }; + match AuthenticatedResourceContext::acquire(context) + .and_then(|authenticated| begin(&authenticated, wire, entry)) + { + Ok(attempt) => { + // SAFETY: validated exclusive output; no effect follows the write. + unsafe { ptr::write(out_attempt, attempt) }; + JettRuntimeResultV1::ok() + } + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +fn complete( + authenticated: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body_status: u32, + panicked: bool, +) -> ResourceResult { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + resource.validate_installation()?; + if resource.entry_owner != EntryOwner::Generated { + return Err(NativeResourceError::WrongOperation); + } + resource.runtime_purpose()?; + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body_status, + panicked, + )?; + let completed = resource + .completed + .last() + .ok_or(NativeResourceError::InvalidEntry)?; + if completed.completion.attempt != attempt.raw() { + return Err(NativeResourceError::InvalidEntry); + } + Ok(completed.terminal_status) + }) +} + +fn status_call( + operation: impl FnOnce() -> ResourceResult, +) -> JettRuntimeStatusV1 { + match catch_unwind(AssertUnwindSafe(operation)) { + Ok(Ok(status)) => status, + Ok(Err(error)) => error.status(), + Err(panic) => { + discard_panic_payload(panic); + JettRuntimeStatusV1::PANIC + } + } +} + +/// Retire precisely the generated attempt, preserving original body status. +/// # Safety +/// `context` is the live stationary context from the successful begin call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body_status: u32, +) -> JettRuntimeStatusV1 { + status_call(|| { + complete( + &AuthenticatedResourceContext::acquire(context)?, + ResourceHandleId::new(attempt)?, + body_status, + false, + ) + }) +} + +/// Launcher fallback after a recoverable entry panic. Ordinary contexts and +/// externally managed test attempts are no-ops. Completing custody is never retried. +/// # Safety +/// `context` is live and stationary; call only after entry control has returned. +/// `out_selected_kind` is distinct aligned exclusive writable storage. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_unwind( + context: *const JettRuntimeContextV1, + body_status: u32, + panicked: u32, + out_selected_kind: *mut u32, +) -> JettRuntimeStatusV1 { + if !leaves::output_ok(context, out_selected_kind) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + status_call(|| { + if panicked > 1 { + return Err(NativeResourceError::WrongOperation); + } + let authenticated = AuthenticatedResourceContext::acquire(context)?; + let attempt = authenticated.with_state(|_, state| { + let Some(resource) = state.resource_state.as_ref() else { + return Ok(None); + }; + if resource.entry_owner != EntryOwner::Generated { + return Ok(None); + } + match resource.attempt.as_ref() { + Some(attempt) if attempt.phase == AttemptPhase::Running => Ok(Some(attempt.id)), + Some(_) => Err(NativeResourceError::ActiveAttempt), + None => Ok(None), + } + })?; + let status = match attempt { + Some(attempt) => complete(&authenticated, attempt, body_status, panicked != 0)?, + None => JettRuntimeStatusV1::OK, + }; + let kind = if attempt.is_some() { + observation(context, |completed| { + Ok(completed.map(|c| c.completion.selected_kind).unwrap_or(0)) + })? + } else { + 0 + }; + // SAFETY: the caller supplies checked exclusive output storage. + unsafe { ptr::write(out_selected_kind, kind) }; + Ok(status) + }) +} + +fn observation( + context: *const JettRuntimeContextV1, + read: impl FnOnce(Option<&NativeCompletedAttempt>) -> ResourceResult, +) -> ResourceResult { + AuthenticatedResourceContext::acquire(context)?.with_state(|_, state| { + let completed = state + .resource_state + .as_ref() + .filter(|resource| { + resource.entry_owner == EntryOwner::Generated && resource.attempt.is_none() + }) + .and_then(|resource| resource.completed.last()); + if let Some(error) = completed.and_then(|completed| completed.report_error) { + return Err(error); + } + read(completed) + }) +} + +/// Read the retained terminal outcome without changing any first-error channel. +/// # Safety +/// Live stationary context and distinct aligned writable result record. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_failure( + context: *const JettRuntimeContextV1, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + if !leaves::output_ok(context, out_result) { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || { + match observation(context, |completed| { + Ok(completed + .map(|c| c.terminal_status) + .unwrap_or(JettRuntimeStatusV1::OK)) + }) { + Ok(status) if status == JettRuntimeStatusV1::OK => JettRuntimeResultV1::ok(), + Ok(status) => JettRuntimeResultV1::failure(status, REFUSED), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +fn message(completed: &NativeCompletedAttempt) -> &[u8] { + let message = + if completed.completion.selected_kind >= 2 || completed.ordinary_message.is_empty() { + &completed.resource_message + } else { + &completed.ordinary_message + }; + if message.is_empty() && completed.terminal_status != JettRuntimeStatusV1::OK { + b"native Resource entry failed" + } else { + message + } +} + +/// Copy retained diagnostics while their context is alive. Null/zero queries length. +/// # Safety +/// Live stationary context; output records and any buffer are writable, distinct, +/// and exclusive for the duration of the call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_program_failure_copy( + context: *const JettRuntimeContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let checked = (|| { + let records = [ + region(context, 1)?, + region(out_length, 1)?, + region(out_result, 1)?, + ]; + separate(&records)?; + let capacity = usize::try_from(capacity).map_err(|_| NativeResourceError::Capacity)?; + if !buffer.is_null() { + let buffer = region(buffer, capacity)?; + for record in records { + separate(&[buffer, record])?; + } + } else if capacity != 0 { + return Err(NativeResourceError::WrongOperation); + } + Ok(capacity) + })(); + let Ok(capacity) = checked else { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + }; + complete_call(out_result, || { + match observation(context, |completed| { + let message = completed.map(message).unwrap_or_default(); + if !buffer.is_null() && capacity < message.len() { + return Err(NativeResourceError::Capacity); + } + // SAFETY: validated exclusive regions; borrowed bytes remain pinned by the lease. + unsafe { + ptr::write(out_length, message.len() as u64); + if !buffer.is_null() { + ptr::copy_nonoverlapping(message.as_ptr(), buffer, message.len()); + } + } + Ok(()) + }) { + Ok(()) => JettRuntimeResultV1::ok(), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + } + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs b/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs new file mode 100644 index 00000000..01a09a9a --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/program_entry/tests.rs @@ -0,0 +1,710 @@ +//! Startup controls, not a real provider or normal-archive execution receipt. +use super::*; +use crate::native_abi::resource::tests::{context, entry, layout_with_entry_network}; + +fn pointer(authenticated: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + authenticated.key.owner_address as *const JettRuntimeContextV1 +} +fn manifest(wire: &[u8]) -> ObjectManifest { + let entry = entry(); + ObjectManifest { + layout: wire.as_ptr(), + layout_length: wire.len() as u64, + function: entry.function, + signature: entry.signature, + scope: entry.scope, + reserved: 0, + } +} +fn retained( + authenticated: &AuthenticatedResourceContext, +) -> (usize, NativeResourceCompletion, JettRuntimeStatusV1) { + authenticated + .with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + let completed = resource.completed.last().unwrap(); + Ok(( + resource.completed.len(), + completed.completion, + completed.terminal_status, + )) + }) + .unwrap() +} + +fn graph_schema(tag: u8, hook: bool) -> Vec { + let mut debug = b"JD\x01".to_vec(); + for word in [1_u32, 0, 1] { + debug.extend_from_slice(&word.to_le_bytes()); + } + debug.push(tag); + let mut bytes = b"JA\x01".to_vec(); + bytes.extend_from_slice(&(debug.len() as u32).to_le_bytes()); + bytes.extend(debug); + bytes.extend_from_slice(&u32::from(hook).to_le_bytes()); + if hook { + bytes.extend_from_slice(&0_u32.to_le_bytes()); + bytes.push(7); + for word in [12_u32, 0, 0] { + bytes.extend_from_slice(&word.to_le_bytes()); + } + } + bytes +} + +fn stage_graph_hook_actor(auth: &AuthenticatedResourceContext) -> (u64, u64, u64) { + let pointer = pointer(auth); + let mut descriptor = 0; + assert_eq!( + unsafe { leaves::jett_rt_v1_resource_descriptor(pointer, 12, &mut descriptor) }, + JettRuntimeStatusV1::OK + ); + let ticket = unsafe { values::jett_rt_v1_actor_graph_begin(pointer) }; + let actor = unsafe { values::jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 1) }; + let mut original = 0; + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_graph_descriptor_box( + pointer, + ticket, + 12, + descriptor, + &mut original, + ) + }, + JettRuntimeStatusV1::OK + ); + let boxed = unsafe { values::jett_rt_v1_struct_clone(pointer, original) }; + assert_ne!(boxed, 0); + assert_ne!(boxed, original); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer, original) }, + 0 + ); + let hook = graph_schema(13, true); + assert_eq!( + unsafe { + values::jett_rt_v1_actor_graph_fill( + pointer, + ticket, + actor, + 0, + boxed, + 1, + 0, + hook.as_ptr(), + hook.len() as u64, + ) + }, + 0 + ); + (ticket, actor, boxed) +} + +fn publish_graph_hook_actor(auth: &AuthenticatedResourceContext) -> u64 { + let pointer = pointer(auth); + let (ticket, actor, boxed) = stage_graph_hook_actor(auth); + let root = graph_schema(14, false); + assert_eq!( + unsafe { + values::jett_rt_v1_actor_graph_root_fill( + pointer, + ticket, + actor, + 0, + 0, + root.as_ptr(), + root.len() as u64, + ) + }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_actor_graph_commit(pointer, ticket) }, + 0 + ); + assert_eq!( + unsafe { values::jett_rt_v1_actor_message_check(pointer, actor) }, + 0 + ); + auth.with_state(|_, state| { + assert!(!state.values.is_empty()); + assert!(state.values.published_checked_graph_hook(boxed).is_ok()); + Ok(()) + }) + .unwrap(); + boxed +} + +#[test] +fn generated_startup_retires_graph_hook_actor_once_and_reentry_stays_empty() { + context(|auth| { + let wire = layout_with_entry_network(false); + for ordinal in 1..=2 { + let attempt = begin(auth, &wire, entry()).unwrap(); + let boxed = publish_graph_hook_actor(auth); + assert_ne!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt + 1, 0) }, + JettRuntimeStatusV1::OK + ); + auth.with_state(|_, state| { + assert!(!state.values.is_empty()); + assert!(state.values.published_checked_graph_hook(boxed).is_ok()); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 0) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, ordinal); + assert_eq!(retained(auth).1.selected_kind, 0); + auth.with_state(|_, state| { + assert!(state.values.is_empty()); + assert!(state.values.published_checked_graph_hook(boxed).is_err()); + assert!(!state.values.cleanup_failed); + Ok(()) + }) + .unwrap(); + let mut kind = 99; + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 0, 0, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(kind, 0); + assert_eq!(retained(auth).0, ordinal); + } + }); +} + +#[test] +fn generated_startup_aborts_unfinished_graph_hook_import_before_empty_audit() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + stage_graph_hook_actor(auth); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 5) }, + JettRuntimeStatusV1(5) + ); + assert_eq!(retained(auth).1.body_status, 5); + assert_eq!(retained(auth).1.cleanup_status, 0); + assert_eq!(retained(auth).1.selected_kind, 1); + auth.with_state(|_, state| { + assert!(state.values.is_empty()); + assert!(!state.values.cleanup_failed); + Ok(()) + }) + .unwrap(); + let next = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), next, 0) }, + JettRuntimeStatusV1::OK + ); + }); +} + +#[test] +fn generated_startup_graph_actor_retirement_preserves_first_body_failure() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + publish_graph_hook_actor(auth); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(auth)) }, 0); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 7) }, + JettRuntimeStatusV1(7) + ); + assert_eq!(retained(auth).1.body_status, 7); + assert_eq!(retained(auth).1.cleanup_status, 0); + assert_eq!(retained(auth).1.selected_kind, 1); + let mut bytes = [0_u8; 64]; + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + bytes.len() as u64, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(&bytes[..length as usize], b"assertion failed"); + let next = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), next, 0) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 2); + }); +} + +#[test] +fn generated_startup_graph_actor_cleanup_poison_keeps_failure_and_blocks_reentry() { + let mut record = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *record, &mut result) + }, + JettRuntimeStatusV1::OK + ); + let auth = AuthenticatedResourceContext::acquire(&*record).unwrap(); + let wire = layout_with_entry_network(false); + let attempt = begin(&auth, &wire, entry()).unwrap(); + publish_graph_hook_actor(&auth); + let list = unsafe { values::jett_rt_v1_list_new(pointer(&auth), 0) }; + assert_ne!(list, 0); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(&auth)) }, 0); + assert_eq!( + unsafe { values::jett_rt_v1_value_drop(pointer(&auth), list) }, + 0 + ); + assert_ne!( + unsafe { values::jett_rt_v1_value_drop(pointer(&auth), list) }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(&auth), attempt, 7) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(retained(&auth).1.body_status, 7); + assert_eq!(retained(&auth).1.selected_kind, 3); + assert_ne!(retained(&auth).1.cleanup_status, 0); + let mut bytes = [0_u8; 64]; + let mut length = 0; + assert_eq!( + unsafe { + jett_rt_v1_resource_program_failure_copy( + pointer(&auth), + bytes.as_mut_ptr(), + bytes.len() as u64, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(&bytes[..length as usize], b"native value cleanup failed"); + assert_eq!( + unsafe { + values::jett_rt_v1_value_failure_copy( + pointer(&auth), + bytes.as_mut_ptr(), + bytes.len() as u64, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(&bytes[..length as usize], b"assertion failed"); + auth.with_state(|_, state| { + assert!(state.values.is_empty()); + assert!(state.values.cleanup_failed); + Ok(()) + }) + .unwrap(); + assert!(begin(&auth, &wire, entry()).is_err()); + assert_eq!(retained(&auth).0, 1); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *record, &mut result) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); +} + +#[test] +fn generated_startup_retains_wire_and_disabled_refusal() { + context(|auth| { + let wire = layout_with_entry_network(true); + let object = manifest(&wire); + let mut attempt = 99; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin(pointer(auth), &object, &mut attempt, &mut result) + }, + JettRuntimeStatusV1::OK + ); + assert_ne!(attempt, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + assert_eq!(resource.installed_wire, wire); + assert!(!resource.provider.enabled()); + assert!(resource.network.is_none()); + assert_eq!( + resource.entry_network(ResourceHandleId::new(attempt)?, 0), + Err(NativeResourceError::WrongGrant) + ); + assert_eq!( + resource.attempt.as_ref().unwrap().purpose, + ResourcePurpose::Runtime + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 0) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).1.body_status, 0); + }); +} + +#[test] +fn exact_reentry_preserves_history_and_refuses_swapped_manifest() { + context(|auth| { + let wire = layout_with_entry_network(false); + let first = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(first).unwrap(), 5, false) + .unwrap() + .code(), + 5 + ); + let mut swapped = wire.clone(); + *swapped.last_mut().unwrap() ^= 1; + assert!(matches!( + begin(auth, &swapped, entry()), + Err(NativeResourceError::InvalidEntry) + )); + assert_eq!(retained(auth).0, 1); + assert_eq!(retained(auth).1.body_status, 5); + let second = begin(auth, &wire, entry()).unwrap(); + assert_ne!(first, second); + assert_eq!( + complete(auth, ResourceHandleId::new(second).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 2); + auth.with_state(|_, state| { + let resource = state.resource_state.as_ref().unwrap(); + assert_eq!(resource.completed[0].completion.body_status, 5); + assert_eq!(resource.completed[0].terminal_status.code(), 5); + assert_eq!(resource.custody.live_owners(), 0); + assert_eq!(resource.custody.live_loans(), 0); + assert_eq!(state.resources.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn active_reentry_and_wrong_context_do_not_select_or_retire_an_attempt() { + context(|first_context| { + context(|second_context| { + let wire = layout_with_entry_network(false); + let attempt = begin(first_context, &wire, entry()).unwrap(); + assert!(matches!( + begin(first_context, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + let second = begin(second_context, &wire, entry()).unwrap(); + assert!( + complete( + second_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .is_err() + ); + assert_eq!( + complete( + first_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!( + complete( + second_context, + ResourceHandleId::new(second).unwrap(), + 0, + false + ) + .unwrap(), + JettRuntimeStatusV1::OK + ); + assert!( + complete( + first_context, + ResourceHandleId::new(attempt).unwrap(), + 0, + false + ) + .is_err() + ); + assert_eq!(retained(first_context).0, 1); + }) + }); +} + +#[test] +fn ordinary_first_error_is_copied_without_rewriting_body_or_poisoning_reentry() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer(auth)) }, 0); + assert_eq!( + complete(auth, ResourceHandleId::new(attempt).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let (_, completed, _) = retained(auth); + assert_eq!(completed.body_status, 0); + assert_eq!(completed.selected_kind, 1); + let mut bytes = [0_u8; 64]; + let mut length = 0; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_failure_copy( + pointer(auth), + bytes.as_mut_ptr(), + bytes.len() as u64, + &mut length, + &mut result, + ) + }, + JettRuntimeStatusV1::OK + ); + assert_eq!(&bytes[..length as usize], b"assertion failed"); + let next = begin(auth, &wire, entry()).unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(next).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 2); + }); +} + +#[test] +fn first_resource_failure_and_cleanup_priority_retain_original_completion() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.record_failure(NativeResourceError::Capacity); + resource.record_failure(NativeResourceError::WrongGrant); + Ok(()) + }) + .unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(attempt).unwrap(), 0, false).unwrap(), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED + ); + assert_eq!(retained(auth).1.body_status, 0); + let next = begin(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + state + .resource_state + .as_mut() + .unwrap() + .record_failure(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic, + )); + Ok(()) + }) + .unwrap(); + assert_eq!( + complete(auth, ResourceHandleId::new(next).unwrap(), 5, true).unwrap(), + JettRuntimeStatusV1::PANIC + ); + assert_eq!(retained(auth).1.body_status, 5); + assert_eq!( + retained(auth).1.cleanup_status, + JettRuntimeStatusV1::PANIC.code() + ); + assert_eq!(retained(auth).1.selected_kind, 3); + }); +} + +#[test] +fn recoverable_panic_completes_once_and_ordinary_context_is_noop() { + context(|auth| { + let mut kind = 99; + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(kind, 0); + let wire = layout_with_entry_network(false); + begin(auth, &wire, entry()).unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::PANIC + ); + assert_eq!(kind, 2); + assert_eq!(retained(auth).1.body_status, 255); + assert_eq!(retained(auth).0, 1); + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 255, 1, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(retained(auth).0, 1); + }); +} + +#[test] +fn context_overlap_and_reserved_manifest_refuse_before_installation() { + context(|auth| { + let wire = layout_with_entry_network(false); + let mut object = manifest(&wire); + let mut attempt = 99; + let mut result = JettRuntimeResultV1::ok(); + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin( + pointer(auth), + &object, + &mut attempt, + pointer(auth).cast_mut().cast(), + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + object.reserved = 1; + assert_eq!( + unsafe { + jett_rt_v1_resource_program_begin(pointer(auth), &object, &mut attempt, &mut result) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(attempt, 99); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn external_nonruntime_attempts_cannot_be_adopted_by_generated_startup() { + for purpose in [ + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + let wire = layout_with_entry_network(false); + install_disabled(auth, &wire, entry()).unwrap(); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry(&mut state.values, &state.resources, entry(), purpose)?; + Ok(()) + }) + .unwrap(); + assert!(matches!( + begin(auth, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + let mut kind = 99; + assert_eq!( + unsafe { jett_rt_v1_resource_program_unwind(pointer(auth), 0, 0, &mut kind) }, + JettRuntimeStatusV1::OK + ); + assert_eq!(kind, 0); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + let attempt = resource.attempt.as_ref().unwrap().id; + assert_eq!(resource.attempt.as_ref().unwrap().purpose, purpose); + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + 0, + false, + )?; + Ok(()) + }) + .unwrap(); + }); + } +} + +#[cfg(all(test, jett_resource_native_test_archive))] +#[test] +fn private_scripted_handoff_borrows_only_the_exact_runtime_attempt() { + use crate::native_abi::resource::tests::script; + for purpose in [ + ResourcePurpose::Runtime, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + let wire = layout_with_entry_network(false); + install_scripted(auth, &wire, entry(), script(&[], 1)).unwrap(); + let attempt = auth + .with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + resource.begin_entry(&mut state.values, &state.resources, entry(), purpose) + }) + .unwrap(); + if purpose == ResourcePurpose::Runtime { + assert_eq!(begin(auth, &wire, entry()).unwrap(), 0); + } else { + assert!(matches!( + begin(auth, &wire, entry()), + Err(NativeResourceError::ActiveAttempt) + )); + } + assert!(complete(auth, attempt, 0, false).is_err()); + auth.with_state(|_, state| { + let resource = state.resource_state.as_mut().unwrap(); + assert_eq!(resource.entry_owner, EntryOwner::External); + assert_eq!(resource.attempt.as_ref().unwrap().id, attempt); + resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + 0, + false, + )?; + assert_eq!(resource.completed.len(), 1); + Ok(()) + }) + .unwrap(); + }); + } +} + +#[test] +fn occupied_field_leaf_refuses_genuine_published_graph_hook_without_mutating_custody() { + context(|auth| { + let wire = layout_with_entry_network(false); + let attempt = begin(auth, &wire, entry()).unwrap(); + let boxed = publish_graph_hook_actor(auth); + assert_eq!( + unsafe { values::jett_rt_v1_struct_replace_occupied(pointer(auth), boxed, 0, 0, 0, 0) }, + 0 + ); + assert_ne!(unsafe { values::jett_rt_v1_value_status(pointer(auth)) }, 0); + auth.with_state(|_, state| { + assert!(state.values.published_checked_graph_hook(boxed).is_ok()); + Ok(()) + }) + .unwrap(); + assert_eq!( + unsafe { jett_rt_v1_resource_program_complete(pointer(auth), attempt, 7) }, + JettRuntimeStatusV1(7) + ); + auth.with_state(|_, state| { + assert!(state.values.is_empty()); + assert!(!state.values.cleanup_failed); + Ok(()) + }) + .unwrap(); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/provider.rs b/crates/jett_runtime/src/native_abi/resource/provider.rs new file mode 100644 index 00000000..81ffb184 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/provider.rs @@ -0,0 +1,366 @@ +use super::*; +#[cfg(test)] +use std::collections::VecDeque; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeProviderIdentity(NonZeroU64); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct NativeRestrictionIdentity(NonZeroU64); +impl NativeProviderIdentity { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::WrongGrant) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} +impl NativeRestrictionIdentity { + pub(super) fn new(raw: u64) -> ResourceResult { + NonZeroU64::new(raw) + .map(Self) + .ok_or(NativeResourceError::WrongGrant) + } + pub(super) fn raw(self) -> u64 { + self.0.get() + } +} + +pub(super) struct NativePayload { + label: i64, +} +pub(super) struct ConstructedPayload { + pub(super) payload: NativePayload, + pub(super) finalizer: Box, +} +pub(super) enum InstalledProvider { + Disabled, + #[cfg(test)] + Scripted(ScriptedProvider), +} +impl InstalledProvider { + pub(super) fn enabled(&self) -> bool { + match self { + Self::Disabled => false, + #[cfg(test)] + Self::Scripted(_) => true, + } + } + pub(super) fn bind( + &mut self, + identity: NativeProviderIdentity, + grant: Option, + ) -> ResourceResult<()> { + match self { + Self::Disabled => { + let _ = (identity, grant); + Ok(()) + } + #[cfg(test)] + Self::Scripted(provider) => { + provider.identity = Some(identity); + provider.grant = grant; + Ok(()) + } + } + } + pub(super) fn matches( + &self, + identity: NativeProviderIdentity, + grant: ResourceHandleId, + ) -> bool { + match self { + Self::Disabled => { + let _ = (identity, grant); + false + } + #[cfg(test)] + Self::Scripted(provider) => { + provider.identity == Some(identity) && provider.grant == Some(grant) + } + } + } + pub(super) fn construct( + &mut self, + label: i64, + ) -> ResourceResult> { + match self { + Self::Disabled => { + let _ = label; + Err(NativeResourceError::WrongGrant) + } + #[cfg(test)] + Self::Scripted(provider) => provider.construct(label), + } + } + pub(super) fn borrow( + &mut self, + payload: &mut NativePayload, + ) -> ResourceResult> { + match self { + Self::Disabled => { + let _ = payload; + Err(NativeResourceError::WrongGrant) + } + #[cfg(test)] + Self::Scripted(provider) => provider.borrow(payload.label), + } + } + #[cfg(test)] + pub(super) fn scripted(script: DecodedScript) -> ResourceResult { + let mut events = Vec::new(); + events + .try_reserve_exact( + script + .operations + .len() + .checked_mul(2) + .ok_or(NativeResourceError::Capacity)?, + ) + .map_err(|_| NativeResourceError::Capacity)?; + Ok(Self::Scripted(ScriptedProvider { + identity: None, + grant: None, + operations: script.operations, + telemetry: Arc::new(Mutex::new(Telemetry { + events, + next_sequence: 1, + })), + attempts: script.attempts, + })) + } + #[cfg(test)] + pub(super) fn attempts(&self) -> usize { + match self { + Self::Disabled => MAX_ATTEMPTS, + Self::Scripted(provider) => provider.attempts, + } + } + #[cfg(test)] + pub(super) fn remaining(&self) -> usize { + match self { + Self::Disabled => 0, + Self::Scripted(provider) => provider.operations.len(), + } + } + #[cfg(test)] + pub(super) fn observe_events(&self, operation: impl FnOnce(&[NativeTestEvent]) -> T) -> T { + match self { + Self::Disabled => operation(&[]), + Self::Scripted(provider) => operation(&lock_unpoisoned(&provider.telemetry).events), + } + } +} + +#[repr(C)] +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::native_abi) struct NativeTestEvent { + pub(in crate::native_abi) sequence: u64, + pub(in crate::native_abi) label: i64, + pub(in crate::native_abi) kind: u32, + pub(in crate::native_abi) reserved: u32, +} +#[cfg(test)] +struct Telemetry { + events: Vec, + next_sequence: u64, +} +#[cfg(test)] +impl Telemetry { + fn event(&mut self, label: i64, kind: u32) { + // Full event capacity and the small script bound were checked at installation. + self.events.push(NativeTestEvent { + sequence: self.next_sequence, + label, + kind, + reserved: 0, + }); + self.next_sequence += 1; + } +} +#[cfg(test)] +pub(super) struct ScriptedProvider { + identity: Option, + grant: Option, + operations: VecDeque, + telemetry: Arc>, + attempts: usize, +} +#[cfg(test)] +enum ScriptOperation { + ConstructOk(i64), + ConstructFail(i64, String), + ConstructFinalizerPanic(i64), + BorrowOk(i64, i64), + BorrowFail(i64, String), + BorrowPanic(i64), +} +#[cfg(test)] +impl ScriptedProvider { + fn construct(&mut self, label: i64) -> ResourceResult> { + match self.operations.front() { + Some( + ScriptOperation::ConstructOk(expected) + | ScriptOperation::ConstructFail(expected, _) + | ScriptOperation::ConstructFinalizerPanic(expected), + ) if *expected == label => {} + _ => return Err(NativeResourceError::WrongOperation), + } + let row = self + .operations + .pop_front() + .ok_or(NativeResourceError::WrongOperation)?; + let panic_on_drop = matches!(&row, ScriptOperation::ConstructFinalizerPanic(_)); + match row { + ScriptOperation::ConstructFail(_, error) => { + lock_unpoisoned(&self.telemetry).event(label, 2); + Ok(Err(error)) + } + ScriptOperation::ConstructOk(_) | ScriptOperation::ConstructFinalizerPanic(_) => { + lock_unpoisoned(&self.telemetry).event(label, 1); + let telemetry = self.telemetry.clone(); + Ok(Ok(ConstructedPayload { + payload: NativePayload { label }, + finalizer: Box::new(move |payload| { + lock_unpoisoned(&telemetry).event(payload.label, 5); + if panic_on_drop { + panic!("scripted native Resource finalizer panic"); + } + }), + })) + } + _ => Err(NativeResourceError::WrongOperation), + } + } + fn borrow(&mut self, label: i64) -> ResourceResult> { + match self.operations.front() { + Some( + ScriptOperation::BorrowOk(expected, _) + | ScriptOperation::BorrowFail(expected, _) + | ScriptOperation::BorrowPanic(expected), + ) if *expected == label => {} + _ => return Err(NativeResourceError::WrongOperation), + } + let kind = if matches!( + self.operations.front(), + Some(ScriptOperation::BorrowFail(..)) + ) { + 4 + } else { + 3 + }; + // The reference records Borrowed before popping and panicking. + lock_unpoisoned(&self.telemetry).event(label, kind); + match self + .operations + .pop_front() + .ok_or(NativeResourceError::WrongOperation)? + { + ScriptOperation::BorrowOk(_, result) => Ok(Ok(result)), + ScriptOperation::BorrowFail(_, error) => Ok(Err(error)), + ScriptOperation::BorrowPanic(_) => panic!("scripted native Resource borrow panic"), + _ => Err(NativeResourceError::WrongOperation), + } + } +} + +#[cfg(test)] +pub(in crate::native_abi) struct DecodedScript { + attempts: usize, + operations: VecDeque, +} +#[cfg(test)] +impl DecodedScript { + pub(in crate::native_abi) fn decode(bytes: &[u8]) -> ResourceResult { + if bytes.len() > 1024 * 1024 { + return Err(NativeResourceError::Capacity); + } + let mut reader = ScriptReader { bytes, offset: 0 }; + if reader.bytes(8)? != b"JTRST001" + || reader.u32()? != 1 + || reader.u32()? != 0 + || reader.u64()? != bytes.len() as u64 + { + return Err(NativeResourceError::WrongOperation); + } + let attempts = reader.u32()? as usize; + let count = reader.u32()? as usize; + if !(1..=MAX_ATTEMPTS).contains(&attempts) || count > 4096 { + return Err(NativeResourceError::Capacity); + } + let mut operations = VecDeque::new(); + operations + .try_reserve(count) + .map_err(|_| NativeResourceError::Capacity)?; + for _ in 0..count { + let tag = reader.u32()?; + let label = reader.u64()? as i64; + operations.push_back(match tag { + 1 => ScriptOperation::ConstructOk(label), + 2 => ScriptOperation::ConstructFail(label, reader.string()?), + 3 => ScriptOperation::ConstructFinalizerPanic(label), + 4 => ScriptOperation::BorrowOk(label, reader.u64()? as i64), + 5 => ScriptOperation::BorrowFail(label, reader.string()?), + 6 => ScriptOperation::BorrowPanic(label), + _ => return Err(NativeResourceError::WrongOperation), + }); + } + if reader.offset != bytes.len() { + return Err(NativeResourceError::WrongOperation); + } + Ok(Self { + attempts, + operations, + }) + } +} +#[cfg(test)] +struct ScriptReader<'a> { + bytes: &'a [u8], + offset: usize, +} +#[cfg(test)] +impl<'a> ScriptReader<'a> { + fn bytes(&mut self, count: usize) -> ResourceResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or(NativeResourceError::Capacity)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(NativeResourceError::WrongOperation)?; + self.offset = end; + Ok(bytes) + } + fn u32(&mut self) -> ResourceResult { + let bytes: [u8; 4] = self + .bytes(4)? + .try_into() + .map_err(|_| NativeResourceError::WrongOperation)?; + Ok(u32::from_le_bytes(bytes)) + } + fn u64(&mut self) -> ResourceResult { + let bytes: [u8; 8] = self + .bytes(8)? + .try_into() + .map_err(|_| NativeResourceError::WrongOperation)?; + Ok(u64::from_le_bytes(bytes)) + } + fn string(&mut self) -> ResourceResult { + let length = self.u32()? as usize; + if length > 64 * 1024 { + return Err(NativeResourceError::Capacity); + } + let text = std::str::from_utf8(self.bytes(length)?) + .map_err(|_| NativeResourceError::WrongOperation)?; + let mut owned = String::new(); + owned + .try_reserve_exact(text.len()) + .map_err(|_| NativeResourceError::Capacity)?; + owned.push_str(text); + Ok(owned) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/source.rs b/crates/jett_runtime/src/native_abi/resource/source.rs new file mode 100644 index 00000000..3d3eff10 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/source.rs @@ -0,0 +1,1804 @@ +//! Active Source handoffs. Immutable rows describe joins; only live core tokens own values. +use super::*; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum SourcePhase { + Prepared, + Entered, + ScopeCompleted, + Completed, + Aborted, +} +#[derive(Clone, Copy)] +struct SourceCompletion { + scope: ResourceHandleId, + return_frame: Option, + original_body_status: u32, + failure: Option, +} + +pub(super) struct NativeSourceCall { + pub(super) frame: ResourceHandleId, + operation: u32, + actuals: Vec>, + parameters: Vec, + next_source: usize, + pub(super) scope: Option, + return_frame: Option, + destination: Option, + body_status: u32, + pub(super) phase: SourcePhase, + completion: Option, +} + +impl NativeResourceState { + pub(super) fn source_call( + &self, + handle: ResourceHandleId, + ) -> ResourceResult<&NativeSourceCall> { + match self.handles.get(&handle) { + Some(NativeResourceEntry::SourceCall(call)) => Ok(call), + _ => Err(NativeResourceError::WrongFamily), + } + } + fn source_row( + &self, + operation: u32, + ) -> ResourceResult<(u32, u32, u32, NativeSourceInvocation)> { + match self.operation(operation)? { + NativeOperation::InvokeSourceFunction { + callee, + signature, + callee_scope, + source: Some(source), + .. + } if matches!(self.layout.wire_version(), 2 | 3) => { + Ok((*callee, *signature, *callee_scope, source.clone())) + } + _ => Err(NativeResourceError::UnsupportedSourceBoundary), + } + } + fn call_for_scope(&self, scope: ResourceHandleId) -> ResourceResult { + let mut found = None; + for (&id, entry) in &self.handles { + if let NativeResourceEntry::SourceCall(call) = entry { + if call.scope == Some(scope) + && matches!( + call.phase, + SourcePhase::Entered | SourcePhase::ScopeCompleted + ) + { + if found.replace(id).is_some() { + return Err(NativeResourceError::WrongFrame); + } + } + } + } + found.ok_or(NativeResourceError::WrongFrame) + } + /// Resolve within this activation only. A recursive caller with the same + /// template/function is separated by the current Scope boundary. + pub(super) fn activation_frame( + &self, + start: ResourceHandleId, + template: u32, + ) -> ResourceResult { + let function = self.layout.frames()[self.frame(start)?.template as usize] + .site() + .function(); + let mut current = start; + loop { + let header = self.frame(current)?; + let row = &self.layout.frames()[header.template as usize]; + if row.site().function() != function { + return Err(NativeResourceError::WrongFrame); + } + if header.template == template { + return Ok(current); + } + let parent = header.parent.ok_or(NativeResourceError::WrongFrame)?; + if row.role() == NativeFrameRole::Scope { + let parent_header = self.frame(parent)?; + let parent_row = &self.layout.frames()[parent_header.template as usize]; + if parent_row.role() == NativeFrameRole::Return + && parent_row.site().function() == function + && parent_header.template == template + { + return Ok(parent); + } + return Err(NativeResourceError::WrongFrame); + } + current = parent; + } + } + pub(super) fn validate_resident( + &self, + loan: ResourceHandleId, + scope: u32, + parameter: u32, + ) -> ResourceResult<()> { + let start = *self + .active_frames + .last() + .ok_or(NativeResourceError::WrongFrame)?; + let scope_handle = self.activation_frame(start, scope)?; + self.resident_at(scope_handle, loan, parameter) + } + fn resident_at( + &self, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + if resident.sum || resident.parent_loan != loan || resident.callee_parameter != parameter { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + match self.handles.get(&loan) { + Some(NativeResourceEntry::Loan(value)) => { + self.frame(value.frame)?; + self.frame(value.source_frame)?; + Ok(()) + } + _ => Err(NativeResourceError::WrongFamily), + } + } + pub(super) fn resident_sum_at( + &self, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + if !resident.sum || resident.parent_loan != loan || resident.callee_parameter != parameter { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + { + return Err(NativeResourceError::WrongFrame); + } + let (_, _, _, row) = self.source_row(call.operation)?; + if !matches!( + row.formals + .get(parameter as usize) + .map(|formal| formal.value), + Some(NativeSourceValue::ResidentSumView { .. }) + ) || !matches!( + self.handles.get(&loan), + Some(NativeResourceEntry::SumLoan(_)) + ) { + return Err(NativeResourceError::WrongFamily); + } + Ok(()) + } + /// Carrier views have a private child lease. The incoming formal header + /// retains its caller lease, and the child is bound to this exact call. + pub(super) fn resident_carrier_at( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + loan: ResourceHandleId, + parameter: u32, + ) -> ResourceResult<()> { + let header = self.frame(scope)?; + let resident = header + .incoming + .get(parameter as usize) + .and_then(Option::as_ref) + .ok_or(NativeResourceError::WrongOperation)?; + let call_handle = self.call_for_scope(scope)?; + let call = self.source_call(call_handle)?; + if resident.sum + || resident.callee_parameter != parameter + || call.operation != resident.invoke_operation + || call.frame != resident.caller_frame + || call.phase != SourcePhase::Entered + || call.parameters.get(parameter as usize) != Some(&loan.raw()) + || call.actuals.get(parameter as usize).copied().flatten() + != Some(resident.parent_loan.raw()) + { + return Err(NativeResourceError::WrongOperation); + } + self.frame(resident.caller_frame)?; + let (_, _, template, row) = self.source_row(call.operation)?; + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + let NativeSourceValue::CarrierView { source } = formal.value else { + return Err(NativeResourceError::WrongFamily); + }; + let parent = self.carrier_loan( + ordinary, + resident.caller_frame, + resident.parent_loan, + source, + )?; + let Some(NativeResourceEntry::CarrierLoan(child)) = self.handles.get(&loan) else { + return Err(NativeResourceError::WrongFamily); + }; + if child.frame != scope + || child.incoming != Some((template, parameter, call_handle)) + || child.parent != Some(resident.parent_loan) + || child.root != parent.root + || child.generation != parent.generation + || child.path != parent.path + { + return Err(NativeResourceError::WrongOperation); + } + self.source_carrier_view( + ordinary, + scope, + loan, + crate::resource_custody::NativeCarrierLoanSource::IncomingViewFormal { + scope: template, + parameter, + }, + formal.callee_shape, + ) + } + fn source_carrier_owned( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + shape: u32, + ) -> ResourceResult<()> { + let root = self.carrier_root(ordinary, frame, handle, slot)?; + if root.frame != frame + || !matches!(self.layout.shapes().get(shape as usize), + Some(NativeShape::Carrier { node }) if *node == root.tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, &root.tree) + } + fn source_carrier_view( + &self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: crate::resource_custody::NativeCarrierLoanSource, + shape: u32, + ) -> ResourceResult<()> { + let loan = self.carrier_loan(ordinary, frame, handle, source)?; + let Some(NativeResourceEntry::CarrierRoot(root)) = self.handles.get(&loan.root) else { + return Err(NativeResourceError::InvalidHandle); + }; + let tree = carriers::selected(&root.tree, &loan.path)?; + if !matches!(self.layout.shapes().get(shape as usize), + Some(NativeShape::Carrier { node }) if *node == tree.node) + { + return Err(NativeResourceError::WrongFamily); + } + self.validate_complete_tree(ordinary, tree) + } + fn source_loan( + &self, + frame: ResourceHandleId, + handle: ResourceHandleId, + source: NativeLoanSource, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + match source { + NativeLoanSource::ProjectedSumPayload { .. } => { + let loan = self.exact_loan(handle, source)?; + self.custody.validate_borrowed(&loan.token, registry)?; + let parent = loan.parent_sum.ok_or(NativeResourceError::WrongFamily)?; + self.validate_sum_parent(parent, registry)?; + let mut ancestor = frame; + while ancestor != loan.frame { + ancestor = self + .frame(ancestor)? + .parent + .ok_or(NativeResourceError::WrongFrame)?; + } + } + NativeLoanSource::ExistingBorrow { operation } => { + let loan = self.exact_loan(handle, source)?; + let mut ancestor = frame; + loop { + if ancestor == loan.frame { + break; + } + ancestor = self + .frame(ancestor)? + .parent + .ok_or(NativeResourceError::WrongFrame)?; + } + if self.layout.operations()[operation as usize] + .site() + .function() + != self.layout.frames()[self.frame(frame)?.template as usize] + .site() + .function() + { + return Err(NativeResourceError::WrongOperation); + } + self.custody.validate_borrowed(&loan.token, registry)?; + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = self.activation_frame(frame, scope)?; + self.resident_at(scope, handle, parameter)?; + let Some(NativeResourceEntry::Loan(loan)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + self.custody.validate_borrowed(&loan.token, registry)?; + } + } + Ok(()) + } + fn ordinary_actual( + &self, + ordinary: &values::NativeValues, + value: u64, + shape: u32, + ) -> ResourceResult<()> { + if let Some(NativeShape::HookDescriptor { hook }) = self.layout.shapes().get(shape as usize) + { + let signature = self.layout.hooks()[*hook as usize].signature(); + return match self.handles.get(&ResourceHandleId::new(value)?) { + Some(NativeResourceEntry::Descriptor { + hook: actual, + signature: actual_signature, + }) if actual == hook && *actual_signature == signature => Ok(()), + _ => Err(NativeResourceError::WrongFamily), + }; + } + ordinary + .validate_resource_ordinary(value, &self.layout, shape) + .map_err(ordinary_error) + } + pub(super) fn carrier( + &self, + handle: ResourceHandleId, + slot: u32, + registry: &ResourceRegistry, + ) -> ResourceResult<()> { + if self.carrier_adapters.contains_key(&handle) { + return Err(NativeResourceError::WrongFamily); + } + match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => { + self.frame(owner.frame)?; + if !matches!( + self.layout.shapes()[self.layout.slots()[slot as usize].shape() as usize], + NativeShape::Resource { .. } + ) { + return Err(NativeResourceError::WrongFamily); + } + self.custody.validate_owned(&owner.token, registry)?; + } + Some(NativeResourceEntry::Sum(sum)) + if sum.slot == slot && sum.shape == self.layout.slots()[slot as usize].shape() => + { + self.frame(sum.frame)?; + match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + self.custody.validate_owned(token, registry)?; + } + NativeSumPayload::None | NativeSumPayload::Fail { .. } => {} + } + } + _ => return Err(NativeResourceError::WrongFamily), + } + Ok(()) + } + pub(super) fn carrier_frame_at( + &self, + current: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<()> { + let actual = match self.handles.get(&handle) { + Some(NativeResourceEntry::Owner(owner)) if owner.slot == slot => owner.frame, + Some(NativeResourceEntry::Sum(sum)) if sum.slot == slot => sum.frame, + _ => return Err(NativeResourceError::WrongFamily), + }; + let expected = + self.activation_frame(current, self.layout.slots()[slot as usize].frame())?; + if actual != expected { + return Err(NativeResourceError::WrongFrame); + } + Ok(()) + } + pub(super) fn slot_empty(&self, frame: ResourceHandleId, slot: u32) -> ResourceResult<()> { + self.slot_frame(slot, frame)?; + if self.handles.values().any(|entry| match entry { + NativeResourceEntry::Owner(owner) => owner.frame == frame && owner.slot == slot, + NativeResourceEntry::Sum(sum) => sum.frame == frame && sum.slot == slot, + NativeResourceEntry::Prepared(value) => { + value.frame == frame && value.destination == slot + } + _ => false, + }) { + return Err(NativeResourceError::WrongOperation); + } + Ok(()) + } + /// This private join is used only after an exact operation or Source boundary + /// has selected both slots. It never turns an empty shell into an owner. + pub(super) fn move_carrier( + &mut self, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source: u32, + destination: u32, + frame: ResourceHandleId, + ) -> ResourceResult { + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.move_carrier_reserved(registry, handle, source, destination, frame, output) + } + pub(super) fn move_carrier_reserved( + &mut self, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source: u32, + destination: u32, + frame: ResourceHandleId, + output: ResourceHandleId, + ) -> ResourceResult { + self.carrier(handle, source, registry)?; + self.sum_unborrowed(handle)?; + self.slot_empty(frame, destination)?; + let source_row = &self.layout.slots()[source as usize]; + let destination_row = &self.layout.slots()[destination as usize]; + if source_row.shape() != destination_row.shape() + || source_row.path() != destination_row.path() + { + return Err(NativeResourceError::WrongOperation); + } + let holder = self + .custody + .holder(&self.frame(frame)?.token, &self.layout.slot(destination)?)?; + let mut entry = self + .handles + .remove(&handle) + .ok_or(NativeResourceError::InvalidHandle)?; + let outcome = match &mut entry { + NativeResourceEntry::Owner(owner) => { + self.custody.transfer(&mut owner.token, &holder, registry) + } + NativeResourceEntry::Sum(sum) => match &mut sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + self.custody.transfer(token, &holder, registry) + } + NativeSumPayload::None | NativeSumPayload::Fail { .. } => Ok(()), + }, + _ => { + self.handles.insert(handle, entry); + return Err(NativeResourceError::WrongFamily); + } + }; + if let Err(error) = outcome { + self.handles.insert(handle, entry); + return Err(error.into()); + } + match &mut entry { + NativeResourceEntry::Owner(owner) => { + owner.frame = frame; + owner.slot = destination; + } + NativeResourceEntry::Sum(sum) => { + sum.frame = frame; + sum.slot = destination; + } + _ => unreachable!(), + } + self.handles.insert(output, entry); + Ok(output) + } + pub(super) fn destination_frame( + &self, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.operation_frame(operation, frame)?; + let destination = match self.operation(operation)? { + NativeOperation::Acquire { destination, .. } + | NativeOperation::Transfer { destination, .. } + | NativeOperation::SumAdopt { destination, .. } + | NativeOperation::SumTake { destination, .. } => *destination, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.activation_frame(frame, self.layout.slots()[destination as usize].frame()) + } + pub(super) fn scope_validate( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + function: u32, + signature: u32, + template: u32, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let header = self.frame(scope)?; + let row = &self.layout.frames()[header.template as usize]; + if self.active_frames.last() != Some(&scope) + || header.template != template + || row.role() != NativeFrameRole::Scope + || row.site().function() != function + || row.signature() != signature + { + return Err(NativeResourceError::WrongFrame); + } + if header.parent.is_none() { + if self.attempt.as_ref().map(|a| a.root_frame) != Some(scope) { + return Err(NativeResourceError::WrongFrame); + } + } else { + let call = self.source_call(self.call_for_scope(scope)?)?; + let (callee, sig, target, _) = self.source_row(call.operation)?; + if call.phase != SourcePhase::Entered + || callee != function + || sig != signature + || target != template + { + return Err(NativeResourceError::WrongFrame); + } + } + Ok(()) + } + pub(super) fn source_prepare( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (_, _, _, row) = self.source_row(operation)?; + let destination = match row.result { + NativeSourceResult::Owned { + caller_destination_frame, + caller_destination_slot, + .. + } => { + let destination = self.activation_frame(frame, caller_destination_frame)?; + self.slot_empty(destination, caller_destination_slot)?; + Some(destination) + } + NativeSourceResult::Ordinary { .. } => None, + NativeSourceResult::Carrier { + caller_destination_frame, + caller_destination_slot, + .. + } => { + let destination = self.activation_frame(frame, caller_destination_frame)?; + self.carrier_slot_empty(destination, caller_destination_slot)?; + Some(destination) + } + }; + let mut actuals = Vec::new(); + let mut parameters = Vec::new(); + actuals + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + parameters + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + actuals.resize(row.formals.len(), None); + parameters.resize(row.formals.len(), 0); + let mut ids = self.reserve(1)?; + let handle = next_handle(&mut ids)?; + self.handles.insert( + handle, + NativeResourceEntry::SourceCall(NativeSourceCall { + frame, + operation, + actuals, + parameters, + next_source: 0, + scope: None, + return_frame: None, + destination, + body_status: 0, + phase: SourcePhase::Prepared, + completion: None, + }), + ); + Ok(handle) + } + pub(super) fn source_actual( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + handle: ResourceHandleId, + source_index: u32, + parameter: u32, + value: u64, + ) -> ResourceResult<()> { + self.running(ordinary)?; + let call = self.source_call(handle)?; + self.operation_frame(call.operation, call.frame)?; + let (_, _, _, row) = self.source_row(call.operation)?; + if call.phase != SourcePhase::Prepared + || call.next_source != source_index as usize + || row.evaluation_order.get(source_index as usize) != Some(¶meter) + { + return Err(NativeResourceError::WrongOperation); + } + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if formal.source_index != source_index || call.actuals[parameter as usize].is_some() { + return Err(NativeResourceError::WrongOperation); + } + match formal.value { + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, value, formal.actual_shape)? + } + NativeSourceValue::Owned { + caller_argument_slot, + .. + } => { + let id = ResourceHandleId::new(value)?; + self.carrier(id, caller_argument_slot, registry)?; + match self.handles.get(&id) { + Some(NativeResourceEntry::Owner(owner)) if owner.frame == call.frame => {} + Some(NativeResourceEntry::Sum(sum)) if sum.frame == call.frame => {} + _ => return Err(NativeResourceError::WrongFrame), + } + } + NativeSourceValue::ResidentView { source } => { + self.source_loan(call.frame, ResourceHandleId::new(value)?, source, registry)? + } + NativeSourceValue::ResidentSumView { source } => { + self.source_sum_loan( + call.frame, + ResourceHandleId::new(value)?, + source, + Some(formal.actual_shape), + registry, + )?; + } + NativeSourceValue::CarrierOwned { + caller_argument_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + call.frame, + ResourceHandleId::new(value)?, + caller_argument_slot, + formal.actual_shape, + )?; + } + NativeSourceValue::CarrierView { source } => { + self.source_carrier_view( + ordinary, + call.frame, + ResourceHandleId::new(value)?, + source, + formal.actual_shape, + )?; + } + } + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.actuals[parameter as usize] = Some(value); + call.next_source += 1; + Ok(()) + } + fn begin_source_frame( + &mut self, + template: u32, + parent: ResourceHandleId, + role: NativeFrameRole, + handle: ResourceHandleId, + ) -> ResourceResult { + if self.active_frames.last() != Some(&parent) { + return Err(NativeResourceError::WrongFrame); + } + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let purpose = attempt.purpose; + let attempt_id = attempt.id; + let row = &self.layout.frames()[template as usize]; + if row.role() != role + || !row + .parents() + .contains(&NativeParent::Frame(self.frame(parent)?.template)) + { + return Err(NativeResourceError::WrongFrame); + } + self.active_frames + .try_reserve(1) + .map_err(|_| NativeResourceError::Capacity)?; + let kind = match role { + NativeFrameRole::Scope => ResourceFrameKind::Scope, + NativeFrameRole::Return => ResourceFrameKind::Return, + _ => return Err(NativeResourceError::WrongFrame), + }; + let token = self.custody.begin_frame(kind, purpose)?; + self.handles.insert( + handle, + NativeResourceEntry::Frame(NativeFrameEntry { + template, + attempt: attempt_id, + parent: Some(parent), + token, + incoming: Vec::new(), + }), + ); + self.active_frames.push(handle); + Ok(handle) + } + pub(super) fn source_enter( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let call = self.source_call(handle)?; + let frame = call.frame; + let operation = call.operation; + self.operation_frame(operation, frame)?; + let (_, _, template, row) = self.source_row(operation)?; + if call.phase != SourcePhase::Prepared + || call.next_source != row.formals.len() + || call.actuals.iter().any(Option::is_none) + { + return Err(NativeResourceError::WrongOperation); + } + // Allocate every adapter-owned carrier before opening a callee or moving a token. + let mut actuals = Vec::new(); + actuals + .try_reserve_exact(call.actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + actuals.extend(call.actuals.iter().map(|v| v.unwrap_or(0))); + let mut ids = self.reserve(2 + actuals.len())?; + let reserved_return = next_handle(&mut ids)?; + let reserved_scope = next_handle(&mut ids)?; + let mut outputs = Vec::new(); + outputs + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut incoming = Vec::new(); + incoming + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut carrier_views = Vec::new(); + carrier_views + .try_reserve_exact(actuals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + outputs.push(next_handle(&mut ids)?); + let staged = if let NativeSourceValue::CarrierView { source } = formal.value { + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + self.source_carrier_view(ordinary, frame, actual, source, formal.actual_shape)?; + let parent = self.carrier_loan(ordinary, frame, actual, source)?; + let mut path = Vec::new(); + path.try_reserve_exact(parent.path.len()) + .map_err(|_| NativeResourceError::Capacity)?; + path.extend_from_slice(&parent.path); + Some(carriers::NativeCarrierLoan { + frame, + operation: parent.operation, + root: parent.root, + generation: parent.generation, + path, + parent: Some(actual), + incoming: Some((template, formal.parameter, handle)), + }) + } else { + None + }; + carrier_views.push(staged); + incoming.push( + if matches!( + formal.value, + NativeSourceValue::ResidentView { .. } + | NativeSourceValue::ResidentSumView { .. } + | NativeSourceValue::CarrierView { .. } + ) { + Some(NativeResidentLoan { + sum: matches!(formal.value, NativeSourceValue::ResidentSumView { .. }), + parent_loan: ResourceHandleId::new(actuals[formal.parameter as usize])?, + caller_frame: frame, + invoke_operation: operation, + callee_parameter: formal.parameter, + }) + } else { + None + }, + ); + } + self.active_frames + .try_reserve(2) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + let actual = actuals[formal.parameter as usize]; + match formal.value { + NativeSourceValue::Owned { + caller_argument_slot, + .. + } => self.carrier( + ResourceHandleId::new(actual)?, + caller_argument_slot, + registry, + )?, + NativeSourceValue::ResidentView { source } => { + self.source_loan(frame, ResourceHandleId::new(actual)?, source, registry)? + } + NativeSourceValue::ResidentSumView { source } => { + self.source_sum_loan( + frame, + ResourceHandleId::new(actual)?, + source, + Some(formal.actual_shape), + registry, + )?; + } + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, actual, formal.actual_shape)? + } + NativeSourceValue::CarrierOwned { + caller_argument_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + frame, + ResourceHandleId::new(actual)?, + caller_argument_slot, + formal.actual_shape, + )?; + } + NativeSourceValue::CarrierView { source } => { + self.source_carrier_view( + ordinary, + frame, + ResourceHandleId::new(actual)?, + source, + formal.actual_shape, + )?; + } + } + } + let return_frame = if let Some(template) = row.callee_return { + Some(self.begin_source_frame( + template, + frame, + NativeFrameRole::Return, + reserved_return, + )?) + } else { + None + }; + let scope_result = self.begin_source_frame( + template, + return_frame.unwrap_or(frame), + NativeFrameRole::Scope, + reserved_scope, + ); + let scope = match scope_result { + Ok(scope) => scope, + Err(error) => { + if let Some(return_frame) = return_frame { + self.retire_frame(ordinary, registry, return_frame)?; + } + return Err(error); + } + }; + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.scope = Some(scope); + call.return_frame = return_frame; + call.phase = SourcePhase::Entered; + } + let Some(NativeResourceEntry::Frame(header)) = self.handles.get_mut(&scope) else { + return Err(NativeResourceError::WrongFrame); + }; + header.incoming = incoming; + // All holders are checked empty and all core transfer bookkeeping is reserved + // before the first owner leaves its caller argument slot. + let preflight: ResourceResult<()> = (|| { + let mut slots = Vec::new(); + slots + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut token_handles = Vec::new(); + token_handles + .try_reserve_exact(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + let mut owned_handles = std::collections::HashSet::new(); + owned_handles + .try_reserve(row.formals.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for formal in &row.formals { + if let NativeSourceValue::Owned { + callee_parameter_slot, + .. + } = formal.value + { + self.slot_empty(scope, callee_parameter_slot)?; + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + if !owned_handles.insert(actual) { + return Err(NativeResourceError::WrongOperation); + } + self.sum_unborrowed(actual)?; + match self.handles.get(&actual) { + Some(NativeResourceEntry::Owner(_)) + | Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(_) | NativeSumPayload::Ok(_), + .. + })) => { + token_handles.push(actual); + slots.push(self.layout.slot(callee_parameter_slot)?); + } + _ => {} // None/Fail shells have no owner generation to transfer. + } + } else if let NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } = formal.value + { + let actual = ResourceHandleId::new(actuals[formal.parameter as usize])?; + if !owned_handles.insert(actual) { + return Err(NativeResourceError::WrongOperation); + } + self.source_carrier_owned( + ordinary, + frame, + actual, + caller_argument_slot, + formal.actual_shape, + )?; + self.carrier_unborrowed(actual)?; + self.carrier_slot_empty(scope, callee_parameter_slot)?; + let destination = self + .layout + .carriers() + .slots + .get(callee_parameter_slot as usize) + .ok_or(NativeResourceError::WrongOperation)?; + if !matches!(self.layout.shapes().get(formal.callee_shape as usize), + Some(NativeShape::Carrier { node }) if *node == destination.node) + { + return Err(NativeResourceError::WrongFamily); + } + } + } + let mut transfers = Vec::new(); + transfers + .try_reserve_exact(slots.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for (index, handle) in token_handles.iter().enumerate() { + let token = match self.handles.get(handle) { + Some(NativeResourceEntry::Owner(owner)) => &owner.token, + Some(NativeResourceEntry::Sum(NativeResourceSum { + payload: NativeSumPayload::Some(token) | NativeSumPayload::Ok(token), + .. + })) => token, + _ => return Err(NativeResourceError::WrongFamily), + }; + transfers.push((token, &slots[index])); + } + let Some(NativeResourceEntry::Frame(header)) = self.handles.get(&scope) else { + return Err(NativeResourceError::WrongFrame); + }; + self.custody + .preflight_transfer_batch(&header.token, &transfers, registry)?; + Ok(()) + })(); + if let Err(error) = preflight { + self.abort_source(ordinary, registry, handle)?; + return Err(error); + } + for formal in &row.formals { + let actual = actuals[formal.parameter as usize]; + let acquired = match formal.value { + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => self + .move_carrier_reserved( + registry, + ResourceHandleId::new(actual)?, + caller_argument_slot, + callee_parameter_slot, + scope, + outputs[formal.parameter as usize], + ) + .map(ResourceHandleId::raw), + NativeSourceValue::ResidentView { .. } + | NativeSourceValue::ResidentSumView { .. } => Ok(actual), + NativeSourceValue::Ordinary => Ok(actual), + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => self + .carrier_move_reserved_with_registry( + ordinary, + registry, + ResourceHandleId::new(actual)?, + caller_argument_slot, + callee_parameter_slot, + frame, + scope, + outputs[formal.parameter as usize], + ) + .map(ResourceHandleId::raw), + NativeSourceValue::CarrierView { .. } => carrier_views + .get_mut(formal.parameter as usize) + .and_then(Option::take) + .ok_or(NativeResourceError::WrongOperation) + .map(|mut lease| { + lease.frame = scope; + let output = outputs[formal.parameter as usize]; + self.handles + .insert(output, NativeResourceEntry::CarrierLoan(lease)); + output.raw() + }), + }; + match acquired { + Ok(value) => { + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) + else { + return Err(NativeResourceError::WrongFamily); + }; + call.parameters[formal.parameter as usize] = value; + } + Err(error) => { + self.abort_source(ordinary, registry, handle)?; + return Err(error); + } + } + } + Ok(scope) + } + fn abort_source( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + let scope = call.scope; + let return_frame = call.return_frame; + let mut first = None; + for frame in [scope, return_frame].into_iter().flatten() { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + first.get_or_insert(error); + } + } + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.phase = SourcePhase::Aborted; + } + first.map_or(Ok(()), Err) + } + pub(super) fn source_parameter( + &self, + ordinary: &values::NativeValues, + scope: ResourceHandleId, + parameter: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.frame(scope)?; + let call = self.source_call(self.call_for_scope(scope)?)?; + if call.phase != SourcePhase::Entered { + return Err(NativeResourceError::WrongFrame); + } + let value = call + .parameters + .get(parameter as usize) + .copied() + .ok_or(NativeResourceError::WrongOperation)?; + let (_, _, _, row) = self.source_row(call.operation)?; + let formal = row + .formals + .get(parameter as usize) + .ok_or(NativeResourceError::WrongOperation)?; + match formal.value { + NativeSourceValue::Owned { + callee_parameter_slot, + .. + } => match self.handles.get(&ResourceHandleId::new(value)?) { + Some(NativeResourceEntry::Owner(owner)) + if owner.frame == scope && owner.slot == callee_parameter_slot => {} + Some(NativeResourceEntry::Sum(sum)) + if sum.frame == scope && sum.slot == callee_parameter_slot => {} + _ => return Err(NativeResourceError::WrongFamily), + }, + NativeSourceValue::ResidentView { .. } => { + self.resident_at(scope, ResourceHandleId::new(value)?, parameter)? + } + NativeSourceValue::ResidentSumView { .. } => { + self.resident_sum_at(scope, ResourceHandleId::new(value)?, parameter)?; + } + NativeSourceValue::Ordinary => { + self.ordinary_actual(ordinary, value, formal.callee_shape)? + } + NativeSourceValue::CarrierOwned { + callee_parameter_slot, + .. + } => { + self.source_carrier_owned( + ordinary, + scope, + ResourceHandleId::new(value)?, + callee_parameter_slot, + formal.callee_shape, + )?; + } + NativeSourceValue::CarrierView { .. } => { + self.resident_carrier_at( + ordinary, + scope, + ResourceHandleId::new(value)?, + parameter, + )?; + } + } + Ok(value) + } + #[cfg(all(test, jett_resource_native_test_archive))] + fn property_observation_identity( + &self, + handle: ResourceHandleId, + ) -> ResourceResult { + let call = self.source_call(handle)?; + let scope = call.scope.ok_or(NativeResourceError::WrongFrame)?; + match self.operation(call.operation)? { + NativeOperation::InvokeSourceFunction { + callee, + signature, + callee_scope, + source: Some(_), + .. + } => Ok(test_observations::CallIdentity { + call: handle.raw(), + scope: scope.raw(), + function: *callee, + signature: *signature, + template: *callee_scope, + operation: call.operation, + }), + _ => Err(NativeResourceError::WrongOperation), + } + } + #[cfg(all(test, jett_resource_native_test_archive))] + pub(super) fn observe_property_source_entry( + &self, + ordinary: &mut values::NativeValues, + scope: ResourceHandleId, + ) { + use test_observations::{ + ALLOCATION, BOUNDARY, Channel, EVENT_LIMIT, Parameter, ParameterValue, + }; + if ordinary.test_observation_property_context().is_none() { + return; + } + let length = ordinary + .test_observation_property_context() + .map_or(0, <[u8]>::len); + if let Err(flag) = ordinary.test_observations.context_budget(length) { + ordinary.test_observations.incomplete(flag); + return; + } + let context = + match test_observations::copy_context(ordinary.test_observation_property_context()) { + Ok(Some(context)) => context, + Ok(None) => return, + Err(flag) => { + ordinary.test_observations.incomplete(flag); + return; + } + }; + let prepared = (|| { + let handle = self.call_for_scope(scope).map_err(|_| BOUNDARY)?; + let call = self.source_call(handle).map_err(|_| BOUNDARY)?; + let identity = self + .property_observation_identity(handle) + .map_err(|_| BOUNDARY)?; + let owner = self + .attempt + .as_ref() + .ok_or(BOUNDARY)? + .ordinary_owner + .observation_identity() + .ok_or(BOUNDARY)?; + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = self.operation(call.operation).map_err(|_| BOUNDARY)? + else { + return Err(BOUNDARY); + }; + if source.formals.len() > test_observations::MAX_PARAMETERS { + return Err(EVENT_LIMIT); + } + let mut parameters = Vec::new(); + parameters + .try_reserve_exact(source.formals.len()) + .map_err(|_| ALLOCATION)?; + for formal in &source.formals { + let bits = *call + .parameters + .get(formal.parameter as usize) + .ok_or(BOUNDARY)?; + let value = if matches!(formal.value, NativeSourceValue::Ordinary) + && matches!( + self.layout.shapes().get(formal.callee_shape as usize), + Some(NativeShape::Integer { + bits: 64, + signed: true + }) + ) { + ParameterValue::Int64 { bits } + } else { + ParameterValue::Unobserved + }; + parameters.push(Parameter { + parameter: formal.parameter, + shape: formal.callee_shape, + value, + }); + } + Ok((Channel::Attempt(owner), identity, parameters)) + })(); + match prepared { + Ok((channel, identity, parameters)) => ordinary + .test_observations + .call_enter(channel, &context, identity, parameters), + Err(flag) => ordinary.test_observations.incomplete(flag), + } + } + #[cfg(all(test, jett_resource_native_test_archive))] + fn observe_property_source_completion( + &self, + ordinary: &mut values::NativeValues, + handle: ResourceHandleId, + completion: SourceCompletion, + ) { + let identity = match self.property_observation_identity(handle) { + Ok(identity) => identity, + Err(_) => { + ordinary + .test_observations + .incomplete(test_observations::BOUNDARY); + return; + } + }; + if !ordinary.test_observations.has_entered_call(handle.raw()) { + return; + } + let length = ordinary + .test_observation_property_context() + .map_or(0, <[u8]>::len); + if let Err(flag) = ordinary.test_observations.context_budget(length) { + ordinary.test_observations.incomplete(flag); + return; + } + let context = + match test_observations::copy_context(ordinary.test_observation_property_context()) { + Ok(context) => context, + Err(flag) => { + ordinary.test_observations.incomplete(flag); + return; + } + }; + ordinary.test_observations.call_complete( + context.as_deref(), + identity, + completion.original_body_status, + completion.failure.map(|failure| failure.status().code()), + ); + } + fn record_source_completion( + &mut self, + ordinary: &mut values::NativeValues, + handle: ResourceHandleId, + failure: Option, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + let scope = call.scope.ok_or(NativeResourceError::WrongFrame)?; + if !matches!(call.phase, SourcePhase::Completed | SourcePhase::Aborted) + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + || call.return_frame.is_some_and(|frame| { + self.handles.contains_key(&frame) || self.active_frames.contains(&frame) + }) + { + // Refused retirement and pending publication carry no completion authority. + return Ok(()); + } + #[cfg(all(test, jett_resource_native_test_archive))] + let first_publication = call.completion.is_none(); + let completion = SourceCompletion { + scope, + return_frame: call.return_frame, + original_body_status: call.body_status, + failure, + }; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.completion = Some(completion); + #[cfg(all(test, jett_resource_native_test_archive))] + if first_publication { + self.observe_property_source_completion(ordinary, handle, completion); + } + #[cfg(not(all(test, jett_resource_native_test_archive)))] + let _ = ordinary; + Ok(()) + } + + pub(super) fn source_status( + &self, + ordinary: &values::NativeValues, + handle: ResourceHandleId, + ) -> ResourceResult<()> { + let call = self.source_call(handle)?; + self.operation_frame(call.operation, call.frame)?; + self.source_row(call.operation)?; + let completion = call.completion.ok_or(NativeResourceError::BodyFailed)?; + if call.scope != Some(completion.scope) + || call.return_frame != completion.return_frame + || call.body_status != completion.original_body_status + || self.handles.contains_key(&completion.scope) + || self.active_frames.contains(&completion.scope) + || completion.return_frame.is_some_and(|frame| { + self.handles.contains_key(&frame) || self.active_frames.contains(&frame) + }) + { + return Err(NativeResourceError::WrongFrame); + } + match call.phase { + SourcePhase::Aborted => { + Err(completion.failure.ok_or(NativeResourceError::BodyFailed)?) + } + SourcePhase::Completed + if completion.original_body_status == 0 && completion.failure.is_none() => + { + self.running(ordinary)?; + Ok(()) + } + _ => Err(NativeResourceError::BodyFailed), + } + } + + #[cfg(test)] + pub(super) fn source_completed_body_status( + &self, + handle: ResourceHandleId, + ) -> ResourceResult> { + Ok(self + .source_call(handle)? + .completion + .map(|completion| completion.original_body_status)) + } + pub(super) fn scope_complete( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + body_status: u32, + ) -> ResourceResult<()> { + self.operation_frame(operation, scope)?; + if !matches!(self.operation(operation)?, NativeOperation::Complete { .. }) + || self.layout.frames()[self.frame(scope)?.template as usize].role() + != NativeFrameRole::Scope + { + return Err(NativeResourceError::WrongOperation); + } + let root = self.frame(scope)?.parent.is_none(); + if root { + let attempt = self + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + let header = self.frame(scope)?; + let row = &self.layout.frames()[header.template as usize]; + if attempt.phase != AttemptPhase::Running + || attempt.root_frame != scope + || attempt.entry != self.entry + || attempt.root_body_status.is_some() + || header.template != self.entry.scope + || row.site().function() != self.entry.function + || row.signature() != self.entry.signature + { + return Err(NativeResourceError::InvalidEntry); + } + } + let call = if !root { + Some(self.call_for_scope(scope)?) + } else { + None + }; + if let Some(handle) = call { + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.body_status = body_status; + } + let mut cleanup = self.retire_frame(ordinary, registry, scope); + // A refused cleanup can keep its frame live. Retain the original + // status only after this exact root has actually retired, even if its + // finalizer failed. Do not substitute the selected cleanup/body error. + if root && !self.handles.contains_key(&scope) && !self.active_frames.contains(&scope) { + let attempt = self + .attempt + .as_mut() + .ok_or(NativeResourceError::InvalidEntry)?; + attempt.root_body_status = Some(body_status); + } + // An exact propagated ordinary first-error status is not a second + // Resource refusal. Keep the original status in the completion below. + let ordinary_body_failure = ordinary + .resource_failure() + .filter(|failure| body_status != 0 && failure.status.code() == body_status) + .map(|failure| NativeResourceError::Ordinary(failure.status)); + let body = if body_status != 0 { + Err(ordinary_body_failure.unwrap_or(NativeResourceError::BodyFailed)) + } else { + self.running(ordinary).map(|_| ()) + }; + if let Some(handle) = call { + let return_frame = self.source_call(handle)?.return_frame; + if cleanup.is_err() || body.is_err() { + if let Some(frame) = return_frame { + if let Err(error) = self.retire_frame(ordinary, registry, frame) { + if cleanup.is_ok() { + cleanup = Err(error); + } + } + } + if let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) { + call.phase = SourcePhase::Aborted; + } + } else if let Some(NativeResourceEntry::SourceCall(call)) = + self.handles.get_mut(&handle) + { + call.phase = if return_frame.is_some() { + SourcePhase::ScopeCompleted + } else { + SourcePhase::Completed + }; + } + } + if let Some(handle) = call { + // Preserve the original nonzero body independently of selected cleanup. + // A clean body whose real Scope cleanup failed must still stop its caller. + let failure = if body_status != 0 { + Some( + ordinary_body_failure.unwrap_or(NativeResourceError::SourceBodyStatus( + JettRuntimeStatusV1(body_status), + )), + ) + } else { + cleanup + .as_ref() + .err() + .copied() + .or_else(|| body.as_ref().err().copied()) + }; + self.record_source_completion(ordinary, handle, failure)?; + } + cleanup?; + body + } + pub(super) fn return_publish( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + provisional: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + let handle = self.call_for_scope(scope)?; + let call = self.source_call(handle)?; + if call.phase != SourcePhase::ScopeCompleted || call.body_status != 0 { + return Err(NativeResourceError::WrongFrame); + } + let (_, _, template, row) = self.source_row(call.operation)?; + let source = match self.operation(operation)? { + NativeOperation::PublishReturn { + frame, + source_return_slot, + } if *frame == template => *source_return_slot, + _ => return Err(NativeResourceError::WrongOperation), + }; + let NativeSourceResult::Owned { + caller_destination_slot, + permitted_return_slots, + .. + } = row.result + else { + return Err(NativeResourceError::WrongOperation); + }; + if !permitted_return_slots.contains(&source) { + return Err(NativeResourceError::WrongOperation); + } + let return_frame = call.return_frame.ok_or(NativeResourceError::WrongFrame)?; + let destination = call.destination.ok_or(NativeResourceError::WrongFrame)?; + if self.active_frames.last() != Some(&return_frame) { + return Err(NativeResourceError::WrongFrame); + } + self.carrier(provisional, source, registry)?; + self.carrier_frame_at(return_frame, provisional, source)?; + if self.handles.iter().any(|(&id, entry)| { + id != provisional + && match entry { + NativeResourceEntry::Owner(owner) => owner.frame == return_frame, + NativeResourceEntry::Sum(sum) => sum.frame == return_frame, + NativeResourceEntry::Loan(loan) => loan.frame == return_frame, + NativeResourceEntry::SumLoan(loan) => loan.frame == return_frame, + NativeResourceEntry::Prepared(prepared) => prepared.frame == return_frame, + NativeResourceEntry::Call(call) => call.frame == return_frame, + NativeResourceEntry::SourceCall(call) => call.frame == return_frame, + NativeResourceEntry::CarrierRoot(root) => root.frame == return_frame, + NativeResourceEntry::CarrierLoan(loan) => loan.frame == return_frame, + NativeResourceEntry::CarrierBuilder(builder) => builder.frame == return_frame, + _ => false, + } + }) { + return Err(NativeResourceError::WrongOperation); + } + let output = self.move_carrier( + registry, + provisional, + source, + caller_destination_slot, + destination, + )?; + self.retire_frame(ordinary, registry, return_frame)?; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.phase = SourcePhase::Completed; + self.record_source_completion(ordinary, handle, None)?; + Ok(output) + } + pub(super) fn carrier_publish( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + scope: ResourceHandleId, + operation: u32, + provisional: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + if self.layout.wire_version() != 3 { + return Err(NativeResourceError::UnsupportedSourceBoundary); + } + let handle = self.call_for_scope(scope)?; + let call = self.source_call(handle)?; + if call.phase != SourcePhase::ScopeCompleted + || call.body_status != 0 + || self.handles.contains_key(&scope) + || self.active_frames.contains(&scope) + { + return Err(NativeResourceError::WrongFrame); + } + let (callee, signature, template, row) = self.source_row(call.operation)?; + let NativeOperation::Carrier { record } = self.operation(operation)? else { + return Err(NativeResourceError::WrongOperation); + }; + let carrier_record = self + .layout + .carriers() + .operations + .get(*record as usize) + .ok_or(NativeResourceError::WrongOperation)?; + let crate::resource_custody::NativeCarrierOperation::PublishReturn { frame, source } = + &carrier_record.operation + else { + return Err(NativeResourceError::WrongOperation); + }; + let (frame, source) = (*frame, *source); + if frame != template + || carrier_record.site != self.layout.operations()[operation as usize].site() + || carrier_record.site.function() != callee + { + return Err(NativeResourceError::WrongOperation); + } + let NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } = row.result + else { + return Err(NativeResourceError::WrongOperation); + }; + if !permitted_return_slots.contains(&source) { + return Err(NativeResourceError::WrongOperation); + } + let return_frame = call.return_frame.ok_or(NativeResourceError::WrongFrame)?; + let destination = call.destination.ok_or(NativeResourceError::WrongFrame)?; + let caller_frame = call.frame; + let return_header = self.frame(return_frame)?; + let return_row = &self.layout.frames()[return_header.template as usize]; + if self.active_frames.last() != Some(&return_frame) + || return_header.template != callee_return_frame + || return_header.parent != Some(caller_frame) + || return_row.role() != NativeFrameRole::Return + || return_row.site().function() != callee + || return_row.signature() != signature + || row.callee_return != Some(callee_return_frame) + || self.activation_frame(caller_frame, caller_destination_frame)? != destination + { + return Err(NativeResourceError::WrongFrame); + } + self.source_carrier_owned(ordinary, return_frame, provisional, source, shape)?; + self.carrier_unborrowed(provisional)?; + self.carrier_slot_empty(destination, caller_destination_slot)?; + if self.handles.iter().any(|(&id, entry)| { + id != provisional + && match entry { + NativeResourceEntry::Owner(value) => value.frame == return_frame, + NativeResourceEntry::Sum(value) => value.frame == return_frame, + NativeResourceEntry::Loan(value) => value.frame == return_frame, + NativeResourceEntry::SumLoan(value) => value.frame == return_frame, + NativeResourceEntry::Prepared(value) => value.frame == return_frame, + NativeResourceEntry::Call(value) => value.frame == return_frame, + NativeResourceEntry::SourceCall(value) => value.frame == return_frame, + NativeResourceEntry::CarrierRoot(value) => value.frame == return_frame, + NativeResourceEntry::CarrierLoan(value) => value.frame == return_frame, + NativeResourceEntry::CarrierBuilder(value) => value.frame == return_frame, + _ => false, + } + }) { + return Err(NativeResourceError::WrongOperation); + } + // Publication remains provisional until the exact Return frame retires. + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + let output = self.carrier_move_reserved_with_registry( + ordinary, + registry, + provisional, + source, + caller_destination_slot, + return_frame, + destination, + output, + )?; + self.retire_frame(ordinary, registry, return_frame)?; + let Some(NativeResourceEntry::SourceCall(call)) = self.handles.get_mut(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + call.phase = SourcePhase::Completed; + self.record_source_completion(ordinary, handle, None)?; + Ok(output) + } + pub(super) fn absent_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let slot = match self.operation(operation)? { + NativeOperation::CreateAbsentSum { + destination_slot, .. + } => *destination_slot, + _ => return Err(NativeResourceError::WrongOperation), + }; + self.slot_empty(frame, slot)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot, + shape: self.layout.slots()[slot as usize].shape(), + payload: NativeSumPayload::None, + }), + ); + Ok(output) + } + pub(super) fn failure_sum( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + companion: u64, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (slot, shape) = match self.operation(operation)? { + NativeOperation::CreateFailureSum { + destination_slot, + failure_shape, + .. + } => (*destination_slot, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + ordinary + .validate_resource_ordinary(companion, &self.layout, shape) + .map_err(ordinary_error)?; + self.slot_empty(frame, slot)?; + let mut ids = self.reserve(1)?; + let output = next_handle(&mut ids)?; + self.handles.insert( + output, + NativeResourceEntry::Sum(NativeResourceSum { + frame, + slot, + shape: self.layout.slots()[slot as usize].shape(), + payload: NativeSumPayload::Fail { + shape, + string: companion, + }, + }), + ); + Ok(output) + } + pub(super) fn failure_companion_take( + &mut self, + ordinary: &values::NativeValues, + frame: ResourceHandleId, + operation: u32, + handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (source, shape) = match self.operation(operation)? { + NativeOperation::TakeFailureCompanion { + source_sum_slot, + failure_shape, + .. + } => (*source_sum_slot, *failure_shape), + _ => return Err(NativeResourceError::WrongOperation), + }; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + if sum.slot != source { + return Err(NativeResourceError::WrongOperation); + } + self.carrier_frame_at(frame, handle, source)?; + self.sum_unborrowed(handle)?; + let NativeSumPayload::Fail { + shape: actual, + string, + } = sum.payload + else { + return Err(NativeResourceError::WrongFamily); + }; + if actual != shape { + return Err(NativeResourceError::WrongOperation); + } + ordinary + .validate_resource_ordinary(string, &self.layout, shape) + .map_err(ordinary_error)?; + self.handles.remove(&handle); + Ok(string) + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/source_tests.rs b/crates/jett_runtime/src/native_abi/resource/source_tests.rs new file mode 100644 index 00000000..6a96a6e4 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/source_tests.rs @@ -0,0 +1,857 @@ +//! Malformed-host and real core transition controls, distinct from Source-native acceptance. +use super::super::*; +use super::{context, script, words}; + +struct Fixture { + bytes: Vec, + source: u32, + transfer_return: u32, + complete_scope: u32, + publish: Option, + complete_op: u32, + complete_root: u32, + complete_inner: u32, + close: u32, + create: Option, + borrow: Option, + invoke_borrow: Option, + end_borrow: Option, + take_fail: Option, +} +fn fixture(shape: u32, resident: bool, reorder: bool) -> Fixture { + let signatures = vec![ + vec![0, 2, 6, 0, 2, 1, 1], + vec![1, 2, 7, 0, 2, 4, 2], + vec![2, 1, 3, 4, 1], + vec![3, 1, 3, 0, 2], + if resident { + vec![4, 2, 3, 4, 2, 0, 2] + } else { + vec![4, 1, shape, shape, 1] + }, + ]; + let shapes = vec![ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + ]; + let parent = if resident { 1 } else { 2 }; + let frames = vec![ + vec![0, 1, 0, 0, 0, 0, 3, 1, 0, 0], + vec![1, 2, 0, 0, 0, 1, 3, 1, 1, 0], + vec![2, 3, 1, 0, 0, 2, 4, 1, 1, 1], + vec![3, 1, 1, 0, 0, 3, 4, 1, 1, parent], + vec![4, 2, 1, 0, 0, 4, 4, 1, 1, 3], + ]; + let path = if shape == 5 { + vec![1, 1] + } else if shape == 6 { + vec![1, 2] + } else { + vec![0] + }; + let mut slots = Vec::new(); + for (id, frame, ty) in [ + (0, 0, shape), + (1, 1, shape), + (2, 1, 6), + (3, 2, shape), + (4, 3, shape), + (5, 4, shape), + (6, 1, 4), + (7, 0, 4), + ] { + let mut row = vec![id, frame, ty]; + row.extend(if ty == 6 { + vec![1, 2] + } else if ty == 4 { + vec![0] + } else { + path.clone() + }); + slots.push(row); + } + let mut ops: Vec<(u32, Vec)> = Vec::new(); + let mut add = |function, row: Vec| { + let id = ops.len() as u32; + ops.push((function, row)); + id + }; + add(0, vec![1, 1, 0, 2]); // factory -> typed Result slot + add(0, vec![10, 1, 2, 6]); // occupied result -> independent plain slot + add( + 0, + vec![2, 1, 6, if !resident && shape == 4 { 1 } else { 7 }], + ); + let borrow = if resident { + Some(add(0, vec![3, 1, 7, 1])) + } else { + None + }; + let mut row = vec![16, 1, 1, 4, 3]; + if resident { + row.push(0); + row.extend([ + 2, + if reorder { 1 } else { 0 }, + if reorder { 0 } else { 1 }, + 2, + ]); + row.extend([ + 0, + if reorder { 1 } else { 0 }, + 4, + 4, + 2, + 4, + 2, + 3, + 1, + borrow.unwrap(), + ]); + row.extend([1, if reorder { 0 } else { 1 }, 0, 0, 1, 1, 2, 1]); + row.extend([1, 3]); + } else { + row.extend([1, 2, 1, 0, 1, 0, 0, shape, shape, 1, 2, 1, 2, 1, 4]); + row.extend([2, shape, 0, 0, 2, 1, 3]); + } + let source = add(0, row); + let transfer_return = if resident { + u32::MAX + } else { + add(1, vec![2, 4, 4, 3]) + }; + let invoke_borrow = if resident { + Some(add(1, vec![6, 4, 1, 2, 3, 0])) + } else { + None + }; + let complete_inner = add(1, vec![13, 4]); + let complete_scope = add(1, vec![13, 3]); + let publish = if resident { + None + } else { + Some(add(1, vec![18, 3, 3])) + }; + let end_borrow = borrow.map(|borrow| add(0, vec![5, 1, borrow])); + let complete_op = add(0, vec![13, 1]); + let complete_root = add(0, vec![13, 0]); + let close = if !resident && shape != 4 { + add(0, vec![11, 1, 0]) + } else { + add(0, vec![7, 1, 2, if resident { 7 } else { 0 }]) + }; + let create = if shape == 5 { + Some(add(0, vec![19, 1, 1])) + } else if shape == 6 { + Some(add(0, vec![20, 1, 1, 2])) + } else { + None + }; + let take_fail = if shape == 6 { + Some(add(0, vec![17, 1, 0, 2])) + } else { + None + }; + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[2, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words( + &mut bytes, + &[ + 1, + 3, + signatures.len() as u32, + shapes.len() as u32, + frames.len() as u32, + slots.len() as u32, + ops.len() as u32, + 0, + ], + ); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + for row in signatures + .into_iter() + .chain(shapes) + .chain(frames) + .chain(slots) + { + words(&mut bytes, &row); + } + for (id, (function, row)) in ops.into_iter().enumerate() { + words(&mut bytes, &[id as u32, function, 0, 0, 100 + id as u32]); + words(&mut bytes, &row); + } + let len = bytes.len() as u64; + bytes[16..24].copy_from_slice(&len.to_le_bytes()); + Fixture { + bytes, + source, + transfer_return, + complete_scope, + publish, + complete_op, + complete_root, + complete_inner, + close, + create, + borrow, + invoke_borrow, + end_borrow, + take_fail, + } +} +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +fn setup( + auth: &AuthenticatedResourceContext, + f: &Fixture, + rows: &[(u32, i64, i64, &str)], +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId, u64) { + install_scripted(auth, &f.bytes, entry(), script(rows, 1)).unwrap(); + auth.with_resource(|s, v, r| { + let attempt = s.begin_entry(v, r, entry(), ResourcePurpose::Runtime)?; + let root = s.root_frame(attempt)?; + let op = s.begin_operation_frame(v, 1, root)?; + let network = s.entry_network(attempt, 0)?; + Ok((attempt, root, op, network)) + }) + .unwrap() +} +fn acquire( + auth: &AuthenticatedResourceContext, + op: ResourceHandleId, + network: u64, +) -> ResourceHandleId { + auth.with_resource(|s, v, r| { + let call = s.prepare_hook(v, 0, op, None)?; + let prepared = s.prepare_factory(v, r, call, network)?; + let result = s.factory(v, r, prepared, network, 501)?; + let value = s.sum_take(v, r, 1, op, ResourceHandleId::new(result.value)?, op)?; + s.transfer(v, r, 2, op, value, s.destination_frame(op, 2)?) + }) + .unwrap() +} +fn finish(auth: &AuthenticatedResourceContext, attempt: ResourceHandleId) { + auth.with_resource(|s, v, r| { + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn native_resource_source_owned_activation_parameter_return_and_publication_are_clone_free() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + s.scope_validate(v, scope, 1, 4, 3)?; + assert!(!s.handles.contains_key(&owner)); + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let return_frame = s.destination_frame(inner, f.transfer_return)?; + let provisional = + s.transfer(v, r, f.transfer_return, inner, parameter, return_frame)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + assert_eq!(s.counts(v, r).provisional, 1); + // Same slot/type from a different activation cannot satisfy Return publication. + let NativeResourceEntry::Owner(header) = s.handles.get_mut(&provisional).unwrap() + else { + panic!("plain provisional owner") + }; + let recorded_frame = header.frame; + header.frame = root; + assert_eq!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional), + Err(NativeResourceError::WrongFrame) + ); + let NativeResourceEntry::Owner(header) = s.handles.get_mut(&provisional).unwrap() + else { + panic!("unchanged owner") + }; + header.frame = recorded_frame; + let output = s.return_publish(v, r, scope, f.publish.unwrap(), provisional)?; + assert!(!s.handles.contains_key(&provisional)); + s.source_status(v, call)?; + assert_eq!(s.owner(output, 0)?.frame, root); + s.close_or_drop(r, f.close, op, output)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + auth.with_resource(|s, _, _| { + s.observe_events(|events| { + assert_eq!( + events.iter().map(|e| e.kind).collect::>(), + vec![1, 5] + ) + }); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_named_actual_order_and_resident_view_preserve_one_parent_loan() { + context(|auth| { + let f = fixture(4, true, true); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, ""), (4, 501, 7, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let loan = s.borrow_begin(v, r, f.borrow.unwrap(), op, owner)?; + let call = s.source_prepare(v, op, f.source)?; + // Formal zero is the SECOND source actual. No evaluation occurs inside these leaves. + s.source_actual(v, r, call, 0, 1, network)?; + s.source_actual(v, r, call, 1, 0, loan.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_parameter(v, scope, 0)?, loan.raw()); + let inner = s.begin_operation_frame(v, 4, scope)?; + let hook = s.prepare_hook(v, f.invoke_borrow.unwrap(), inner, None)?; + let result = s.invoke_borrow(v, r, hook, network, loan)?; + v.drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + s.source_status(v, call)?; + assert_eq!(s.counts(v, r).loans, 1); + s.borrow_end(f.end_borrow.unwrap(), op, loan)?; + s.close_or_drop(r, f.close, op, owner)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_absent_shell_moves_through_formal_and_return_without_owner_recovery() { + context(|auth| { + let f = fixture(5, false, false); + let (attempt, _, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + let shell = s.absent_sum(v, op, f.create.unwrap())?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let destination = s.destination_frame(inner, f.transfer_return)?; + let value = s.transfer(v, r, f.transfer_return, inner, parameter, destination)?; + assert_eq!(s.sum_tag(value)?, 0); + assert_eq!(s.custody.live_owners(), 0); + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + let value = s.return_publish(v, r, scope, f.publish.unwrap(), value)?; + s.source_status(v, call)?; + s.sum_drop(v, r, f.close, op, value)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_failure_shell_companion_moves_once_and_never_adopts_a_resource_owner() { + context(|auth| { + let f = fixture(6, false, false); + let (attempt, _, op, _) = setup(auth, &f, &[]); + auth.with_resource(|s, v, r| { + let mut ids = v + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + let text = v + .publish_resource_error("ordinary domain failure".to_owned(), &mut ids) + .map_err(ordinary_error)?; + let shell = s.failure_sum(v, op, f.create.unwrap(), text)?; + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, shell.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let value = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + s.scope_complete(v, r, scope, f.complete_scope, 0)?; + let value = s.return_publish(v, r, scope, f.publish.unwrap(), value)?; + let taken = s.failure_companion_take(v, op, f.take_fail.unwrap(), value)?; + assert_eq!(taken, text); + assert_eq!(s.custody.live_owners(), 0); + assert!(!s.handles.contains_key(&value)); + v.drop_resource_ordinary_companion(taken) + .map_err(ordinary_error)?; + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_missing_or_duplicate_actuals_cannot_open_a_callee() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + // Inspect direct refused transitions without latching the separate body channel. + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + assert_eq!( + s.source_enter(v, r, call), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(s.custody.active_frames(), 2); + s.source_actual(v, r, call, 0, 0, owner.raw())?; + assert_eq!( + s.source_actual(v, r, call, 0, 0, owner.raw()), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!(s.custody.live_owners(), 1); + assert!(s.handles.contains_key(&owner)); + s.end_operation_frame(v, r, op)?; + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_failed_scope_cancels_provisional_return_before_any_publication() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 71), + Err(NativeResourceError::BodyFailed) + ); + assert!(!s.handles.contains_key(&provisional)); + assert_eq!(s.counts(v, r).provisional, 0); + assert_eq!(s.source_completed_body_status(call)?, Some(71)); + let original = NativeResourceError::SourceBodyStatus(JettRuntimeStatusV1(71)); + assert_eq!(s.source_status(v, call), Err(original)); + assert_eq!(original.status().code(), 71); + // A different current activation or tuple cannot borrow this outcome. + let root = s.root_frame(attempt)?; + s.active_frames.push(root); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + s.active_frames.pop(); + let Some(NativeResourceEntry::Frame(header)) = s.handles.get_mut(&op) else { + panic!("exact live caller operation") + }; + let template = header.template; + header.template = 0; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + let Some(NativeResourceEntry::Frame(header)) = s.handles.get_mut(&op) else { + panic!("unchanged live caller operation") + }; + header.template = template; + assert_eq!(s.source_status(v, call), Err(original)); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFamily) + ); + let completion = s.complete_entry(v, r, attempt, 71, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 1)); + assert_eq!(s.counts(v, r).owners, 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_c_outputs_refuse_overlap_before_opening_any_operation_or_provider() { + let mut raw = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let ptr = &mut *raw as *mut JettRuntimeContextV1; + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); + let auth = AuthenticatedResourceContext::acquire(ptr).unwrap(); + let f = fixture(4, false, false); + let (attempt, root, _, _) = setup(&auth, &f, &[(1, 501, 0, "")]); + // A writable result overlapping the immutable Context cannot be an output carrier. + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_operation_begin(ptr, root.raw(), 1, ptr.cast::()) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!( + unsafe { + leaves::jett_rt_v1_resource_entry_scope( + ptr, + &mut 0u64, + ptr.cast::(), + ) + }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + auth.with_resource(|s, v, r| { + assert_eq!(s.custody.active_frames(), 2); + assert_eq!(s.script_remaining(), 1); + let completion = s.complete_entry(v, r, attempt, 0, false)?; + assert_eq!(completion.selected_kind, 0); + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn native_resource_source_entry_getters_return_real_root_and_paired_network_without_new_frames() { + let mut raw = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let ptr = &mut *raw as *mut JettRuntimeContextV1; + assert_eq!( + unsafe { jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); + let auth = AuthenticatedResourceContext::acquire(ptr).unwrap(); + let f = fixture(4, false, false); + let (attempt, root, _, network) = setup(&auth, &f, &[]); + let mut scope = 0; + let mut capability = 0; + assert_eq!( + unsafe { leaves::jett_rt_v1_resource_entry_scope(ptr, &mut scope, &mut result) }, + JettRuntimeStatusV1::OK + ); + assert_eq!( + unsafe { leaves::jett_rt_v1_resource_entry_network(ptr, 0, &mut capability, &mut result) }, + JettRuntimeStatusV1::OK + ); + assert_eq!((scope, capability), (root.raw(), network)); + auth.with_resource(|s, v, r| { + assert_eq!(s.custody.active_frames(), 2); + s.complete_entry(v, r, attempt, 0, false)?; + Ok(()) + }) + .unwrap(); + drop(auth); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(ptr, &mut result) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn native_resource_source_return_cleanup_panic_precedes_nonzero_observed_body_status() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let inner = s.begin_operation_frame(v, 4, scope)?; + let dest = s.destination_frame(inner, f.transfer_return)?; + let provisional = s.transfer(v, r, f.transfer_return, inner, parameter, dest)?; + s.end_operation_frame(v, r, inner)?; + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 71), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert!(!s.handles.contains_key(&provisional)); + assert_eq!(s.counts(v, r).provisional, 0); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), provisional) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + let completion = s.complete_entry(v, r, attempt, 71, false)?; + assert_eq!((completion.body_status, completion.selected_kind), (71, 3)); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + Ok(()) + }) + .unwrap(); + }); +} + +#[path = "entry_outcome_tests.rs"] +mod entry_outcome_tests; + +#[test] +fn native_resource_source_clean_body_cleanup_panic_has_exact_completed_failure_status() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, _, op, network) = setup(auth, &f, &[(3, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + let parameter = ResourceHandleId::new(s.source_parameter(v, scope, 0)?)?; + let cleanup = NativeResourceError::Cleanup(ResourceCleanupFailure::FinalizerPanic); + assert_eq!( + s.scope_complete(v, r, scope, f.complete_scope, 0), + Err(cleanup) + ); + assert_eq!(s.source_completed_body_status(call)?, Some(0)); + assert_eq!(s.source_status(v, call), Err(cleanup)); + assert_eq!(cleanup.status(), JettRuntimeStatusV1::PANIC); + assert!(!s.handles.contains_key(&scope)); + assert!(!s.handles.contains_key(¶meter)); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), parameter) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + let completion = s.complete_entry(v, r, attempt, 255, false)?; + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (255, 255, 3) + ); + let counts = s.counts(v, r); + assert_eq!( + ( + counts.owners, + counts.loans, + counts.frames, + counts.registry, + counts.provisional + ), + (0, 0, 0, 0, 0) + ); + s.observe_events(|events| { + assert_eq!( + events.iter().map(|event| event.kind).collect::>(), + [1, 5] + ) + }); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_source_unfinished_and_merely_retired_frames_have_no_completion_receipt() { + context(|auth| { + let f = fixture(4, false, false); + let (attempt, root, op, network) = setup(auth, &f, &[(1, 501, 0, "")]); + let owner = acquire(auth, op, network); + auth.with_resource(|s, v, r| { + assert_eq!( + s.source_prepare(v, op, f.close), + Err(NativeResourceError::UnsupportedSourceBoundary) + ); + assert_eq!( + s.source_status(v, root), + Err(NativeResourceError::WrongFamily) + ); + let call = s.source_prepare(v, op, f.source)?; + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + assert_eq!( + s.source_enter(v, r, call), + Err(NativeResourceError::WrongOperation) + ); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFrame) + ); + s.retire_frame(v, r, scope)?; + let return_frame = *s.active_frames.last().unwrap(); + assert_ne!(return_frame, op); + s.retire_frame(v, r, return_frame)?; + // Physical retirement alone never mints a trusted Source completion. + assert_eq!(s.source_completed_body_status(call)?, None); + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::BodyFailed) + ); + assert!( + s.return_publish(v, r, scope, f.publish.unwrap(), owner) + .is_err() + ); + s.end_operation_frame(v, r, op)?; + assert_eq!( + s.source_status(v, call), + Err(NativeResourceError::WrongFamily) + ); + Ok(()) + }) + .unwrap(); + finish(auth, attempt); + }); +} + +#[test] +fn native_resource_source_matching_ordinary_failure_preserves_exact_status_and_independent_errors() +{ + for case in 0..5 { + context(|auth| { + let f = fixture(4, false, false); + let finalizer = if case == 2 { 3 } else { 1 }; + let (attempt, _, op, network) = setup(auth, &f, &[(finalizer, 501, 0, "")]); + let owner = acquire(auth, op, network); + let (call, scope) = auth + .with_resource(|s, v, r| { + let call = s.source_prepare(v, op, f.source)?; + s.source_actual(v, r, call, 0, 0, owner.raw())?; + let scope = s.source_enter(v, r, call)?; + Ok((call, scope)) + }) + .unwrap(); + if case == 3 { + assert!( + auth.with_resource(|s, v, _| s.prepare_hook(v, 999, scope, None)) + .is_err() + ); + } + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + if case != 4 { + unsafe { + values::jett_rt_v1_nothing_equal(pointer, 1, 0, 0); + assert_eq!(values::jett_rt_v1_value_status(pointer), 1); + } + } + let body_status = if case == 1 { 71 } else { 1 }; + let ordinary = NativeResourceError::Ordinary(JettRuntimeStatusV1::INVALID_ARGUMENT); + let expected_scope = match case { + 1 | 4 => NativeResourceError::BodyFailed, + 2 => NativeResourceError::Cleanup(ResourceCleanupFailure::FinalizerPanic), + _ => ordinary, + }; + assert_eq!( + auth.with_resource(|s, v, r| { + s.scope_complete(v, r, scope, f.complete_scope, body_status) + }), + Err(expected_scope) + ); + let expected_source = if matches!(case, 1 | 4) { + NativeResourceError::SourceBodyStatus(JettRuntimeStatusV1(body_status)) + } else { + ordinary + }; + assert_eq!( + auth.with_resource(|s, v, _| s.source_status(v, call)), + Err(expected_source) + ); + auth.with_resource(|s, v, r| { + assert_eq!(s.source_completed_body_status(call)?, Some(body_status)); + assert!(!s.handles.contains_key(&scope)); + assert_eq!(s.custody.live_owners(), 0); + assert_eq!(r.live_count(), 0); + s.end_operation_frame(v, r, op)?; + let completion = s.complete_entry(v, r, attempt, body_status, false)?; + assert_eq!(completion.body_status, body_status); + assert_eq!(completion.cleanup_status, if case == 2 { 255 } else { 0 }); + assert_eq!(completion.selected_kind, if case == 2 { 3 } else { 1 }); + let (resource, ordinary_message) = s.completed_messages(attempt)?; + let ordinary_expected: &[u8] = if case == 4 { + b"" + } else { + b"unsupported binary operation: pending(nothing) Eq nothing" + }; + assert_eq!(ordinary_message, ordinary_expected); + let resource_expected: &[u8] = match case { + 0 => b"", + 2 => CLEANUP_FAILED, + _ => REFUSED, + }; + assert_eq!(resource, resource_expected, "case {case}"); + assert_eq!( + s.counts(v, r), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) + }) + .unwrap(); + }); + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/sum_replacement.rs b/crates/jett_runtime/src/native_abi/resource/sum_replacement.rs new file mode 100644 index 00000000..baedb2d5 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/sum_replacement.rs @@ -0,0 +1,263 @@ +//! Whole-binding replacement for two exact live direct-sum shells. +//! Plain replacement retains its original protocol in operations.rs. +use super::*; + +struct PreparedSumReplacement { + old: ResourceHandleId, + replacement: ResourceHandleId, + old_slot: u32, + replacement_slot: u32, + destination_frame: ResourceHandleId, + output: ResourceHandleId, +} + +enum SumRetirement { + Retired(ResourceResult<()>), + Refused(NativeResourceError), +} + +impl NativeResourceState { + pub(super) fn replace( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + old: ResourceHandleId, + replacement: ResourceHandleId, + ) -> ResourceResult { + // Selecting a lane reads only the installed row. The original Plain lane + // keeps its existing validation and error order, including wrong-purpose. + let sum = match self.operation(operation) { + Ok(NativeOperation::Replace { old, .. }) => self + .layout + .slots() + .get(*old as usize) + .and_then(|slot| self.layout.shapes().get(slot.shape() as usize)) + .is_some_and(|shape| { + matches!( + shape, + NativeShape::Optional { .. } | NativeShape::Result { .. } + ) + }), + _ => false, + }; + if !sum { + return self.replace_plain(ordinary, registry, operation, frame, old, replacement); + } + let prepared = + self.prepare_sum_replacement(ordinary, registry, operation, frame, old, replacement)?; + match self.retire_replaced_sum(ordinary, registry, prepared.old) { + SumRetirement::Refused(error) => return Err(error), + SumRetirement::Retired(Err(error)) => { + // Old cleanup has consumed its token/shell. Retire the separately + // owned RHS without replacing the old failure with a later one. + let _ = self.retire_replaced_sum(ordinary, registry, prepared.replacement); + return Err(error); + } + SumRetirement::Retired(Ok(())) => {} + } + // This checked mover revalidates the actual source holder. If a late + // recheck refuses, it retains RHS at that holder for terminal cleanup. + self.move_carrier_reserved( + registry, + prepared.replacement, + prepared.replacement_slot, + prepared.old_slot, + prepared.destination_frame, + prepared.output, + ) + } + + fn prepare_sum_replacement( + &mut self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + operation: u32, + frame: ResourceHandleId, + old_handle: ResourceHandleId, + replacement_handle: ResourceHandleId, + ) -> ResourceResult { + self.running(ordinary)?; + self.operation_frame(operation, frame)?; + let (old_slot, replacement_slot) = match self.operation(operation)? { + NativeOperation::Replace { + old, replacement, .. + } => (*old, *replacement), + _ => return Err(NativeResourceError::WrongOperation), + }; + if old_handle == replacement_handle || old_slot == replacement_slot { + return Err(NativeResourceError::WrongOperation); + } + let old_row = &self.layout.slots()[old_slot as usize]; + let replacement_row = &self.layout.slots()[replacement_slot as usize]; + if old_row.shape() != replacement_row.shape() + || old_row.path() != replacement_row.path() + || old_row.frame() != replacement_row.frame() + || old_row.frame() != self.frame(frame)?.template + || self.layout.frames()[old_row.frame() as usize].role() != NativeFrameRole::Scope + { + return Err(NativeResourceError::WrongOperation); + } + let old = self.replacement_sum(ordinary, registry, frame, old_handle, old_slot)?; + let replacement = self.replacement_sum( + ordinary, + registry, + frame, + replacement_handle, + replacement_slot, + )?; + if old.frame != frame || replacement.frame != frame { + return Err(NativeResourceError::WrongFrame); + } + let destination_frame = old.frame; + let old_companion = Self::replacement_companion(old); + let replacement_companion = Self::replacement_companion(replacement); + let occupied = Self::replacement_token(old).is_some() + || Self::replacement_token(replacement).is_some(); + if occupied { + self.runtime_purpose()?; + } + ordinary + .preflight_resource_sum_companions(&self.layout, old_companion, replacement_companion) + .map_err(ordinary_error)?; + let destination = self + .custody + .holder(&self.frame(frame)?.token, &self.layout.slot(old_slot)?)?; + let source = self.custody.holder( + &self.frame(frame)?.token, + &self.layout.slot(replacement_slot)?, + )?; + let (Some(NativeResourceEntry::Sum(old)), Some(NativeResourceEntry::Sum(replacement))) = ( + self.handles.get(&old_handle), + self.handles.get(&replacement_handle), + ) else { + return Err(NativeResourceError::WrongFamily); + }; + // Borrow tokens directly from the disjoint handle table; no owner is + // copied into this ephemeral prepared transaction. + self.custody.preflight_direct_sum_replacement( + Self::replacement_token(old), + Self::replacement_token(replacement), + &destination, + &source, + registry, + )?; + let mut ids = self.reserve(1)?; + Ok(PreparedSumReplacement { + old: old_handle, + replacement: replacement_handle, + old_slot, + replacement_slot, + destination_frame, + output: next_handle(&mut ids)?, + }) + } + + fn replacement_sum( + &self, + ordinary: &values::NativeValues, + registry: &ResourceRegistry, + frame: ResourceHandleId, + handle: ResourceHandleId, + slot: u32, + ) -> ResourceResult<&NativeResourceSum> { + self.carrier(handle, slot, registry)?; + self.carrier_frame_at(frame, handle, slot)?; + self.sum_unborrowed(handle)?; + let Some(NativeResourceEntry::Sum(sum)) = self.handles.get(&handle) else { + return Err(NativeResourceError::WrongFamily); + }; + let row = &self.layout.slots()[slot as usize]; + let valid = match ( + self.layout.shapes().get(row.shape() as usize), + row.path(), + &sum.payload, + ) { + ( + Some(NativeShape::Optional { child }), + [NativePayloadStep::Some], + NativeSumPayload::None | NativeSumPayload::Some(_), + ) => matches!( + self.layout.shapes().get(*child as usize), + Some(NativeShape::Resource { .. }) + ), + ( + Some(NativeShape::Result { ok, .. }), + [NativePayloadStep::Ok], + NativeSumPayload::Ok(_), + ) => matches!( + self.layout.shapes().get(*ok as usize), + Some(NativeShape::Resource { .. }) + ), + ( + Some(NativeShape::Result { ok, fail }), + [NativePayloadStep::Ok], + NativeSumPayload::Fail { shape, string }, + ) if shape == fail => { + ordinary + .validate_resource_ordinary(*string, &self.layout, *shape) + .map_err(ordinary_error)?; + matches!( + self.layout.shapes().get(*ok as usize), + Some(NativeShape::Resource { .. }) + ) + } + _ => false, + }; + if !valid { + return Err(NativeResourceError::WrongFamily); + } + Ok(sum) + } + + fn replacement_token(sum: &NativeResourceSum) -> Option<&OwnedResourceToken> { + match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => Some(token), + NativeSumPayload::None | NativeSumPayload::Fail { .. } => None, + } + } + + fn replacement_companion(sum: &NativeResourceSum) -> Option<(u32, u64)> { + match &sum.payload { + NativeSumPayload::Fail { shape, string } => Some((*shape, *string)), + NativeSumPayload::None | NativeSumPayload::Some(_) | NativeSumPayload::Ok(_) => None, + } + } + + fn retire_replaced_sum( + &mut self, + ordinary: &mut values::NativeValues, + registry: &mut ResourceRegistry, + handle: ResourceHandleId, + ) -> SumRetirement { + let Some(NativeResourceEntry::Sum(mut sum)) = self.handles.remove(&handle) else { + return SumRetirement::Refused(NativeResourceError::WrongFamily); + }; + match &mut sum.payload { + NativeSumPayload::None => SumRetirement::Retired(Ok(())), + NativeSumPayload::Fail { shape, string } => { + match ordinary.drop_resource_typed_companion(&self.layout, *shape, *string) { + Ok(()) => SumRetirement::Retired(Ok(())), + Err(error) => { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + ordinary.cleanup_failed = true; + SumRetirement::Refused(ordinary_error(error)) + } + } + } + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + let cleanup = self.custody.close(token, registry); + if self.custody.validate_owned(token, registry).is_ok() { + self.handles.insert(handle, NativeResourceEntry::Sum(sum)); + return SumRetirement::Refused( + self.cleanup(cleanup) + .err() + .unwrap_or(NativeResourceError::WrongOperation), + ); + } + SumRetirement::Retired(self.cleanup(cleanup)) + } + } + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/sum_replacement_tests.rs b/crates/jett_runtime/src/native_abi/resource/sum_replacement_tests.rs new file mode 100644 index 00000000..5ec4428c --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/sum_replacement_tests.rs @@ -0,0 +1,1247 @@ +//! Host protocol controls; Source admission and native acceptance are separate gates. +use super::super::*; +use super::{assert_empty, context, events, script, words}; + +const REPLACE: u32 = 7; +const DROP_OLD: u32 = 8; +const DROP_RHS: u32 = 9; +const BORROW_OLD: u32 = 12; +const BORROW_RHS: u32 = 13; +const END_OLD: u32 = 14; +const END_RHS: u32 = 15; +const SENTINEL: u64 = 0x5555_6666_7777_8888; + +#[derive(Clone, Copy, Debug)] +enum Failure { + String, + Integer, + OptionalString, +} +#[derive(Clone, Copy, Debug)] +enum Family { + Optional, + Result(Failure), +} +impl Family { + fn shape(self) -> u32 { + match self { + Self::Optional => 5, + Self::Result(Failure::String) => 6, + Self::Result(Failure::Integer) => 9, + Self::Result(Failure::OptionalString) => 10, + } + } + fn failure_shape(self) -> Option { + match self { + Self::Optional => None, + Self::Result(Failure::String) => Some(2), + Self::Result(Failure::Integer) => Some(1), + Self::Result(Failure::OptionalString) => Some(8), + } + } + fn path(self) -> u32 { + match self { + Self::Optional => 1, + Self::Result(_) => 2, + } + } +} +fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +fn fixture(family: Family) -> Vec { + // Independent canonical wire, authenticated by the real registration issuer. + // Both replacement slots belong to the exact same Scope, not setup Operation. + let signatures = [ + vec![0, 2, 6, 0, 2, 1, 1], + vec![1, 2, 7, 0, 2, 4, 2], + vec![2, 1, 3, 4, 1], + vec![3, 1, 3, 0, 2], + ]; + let shapes = [ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + vec![8, 8, 2], + vec![9, 9, 4, 1], + vec![10, 9, 4, 8], + ]; + let frames = [ + vec![0, 1, 0, 0, 0, 0, 3, 1, 0, 0], + vec![1, 2, 0, 0, 0, 1, 3, 1, 1, 0], + ]; + let slots = [ + vec![0, 0, family.shape(), 1, family.path()], + vec![1, 0, family.shape(), 1, family.path()], + vec![2, 1, 6, 1, 2], + vec![3, 1, 4, 0], + vec![4, 1, family.shape(), 1, family.path()], + ]; + let operations = [ + vec![1, 1, 0, 2], // 0 factory into Operation-local result + vec![10, 1, 2, 3], // 1 take the one occupied factory token + vec![9, 1, 3, 0], // 2 adopt into old Scope slot + vec![9, 1, 3, 1], // 3 adopt into RHS Scope slot + match family.failure_shape() { + None => vec![19, 1, 4], + Some(shape) => vec![20, 1, 4, shape], + }, // 4 create absent Operation-local shell + vec![2, 1, 4, 0], // 5 move absence into old Scope slot + vec![2, 1, 4, 1], // 6 move absence into RHS Scope slot + vec![12, 0, 0, 1], // 7 one compound Scope replacement + vec![11, 0, 0], + vec![11, 0, 1], // 8/9 drop exact sum + vec![13, 1], + vec![13, 0], // 10/11 complete Operation/Scope + vec![21, 0, 0, 0], + vec![21, 0, 1, 0], // 12/13 shell leases + vec![25, 0, 12], + vec![25, 0, 13], // 14/15 end exact leases + ]; + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[2, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words( + &mut bytes, + &[ + 1, + 3, + signatures.len() as u32, + shapes.len() as u32, + frames.len() as u32, + slots.len() as u32, + operations.len() as u32, + 0, + ], + ); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + for row in signatures + .into_iter() + .chain(shapes) + .chain(frames) + .chain(slots) + { + words(&mut bytes, &row); + } + for (index, row) in operations.into_iter().enumerate() { + words(&mut bytes, &[index as u32, 0, 0, 0, 100 + index as u32]); + words(&mut bytes, &row); + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes +} +fn pointer(auth: &AuthenticatedResourceContext) -> *const JettRuntimeContextV1 { + auth.key.owner_address as *const JettRuntimeContextV1 +} +fn text(auth: &AuthenticatedResourceContext, value: &str) -> u64 { + auth.with_resource(|_, ordinary, _| { + let mut ids = ordinary + .prepare_resource_ordinary_output() + .map_err(ordinary_error)?; + ordinary + .publish_resource_error(value.to_owned(), &mut ids) + .map_err(ordinary_error) + }) + .unwrap() +} +fn companion(auth: &AuthenticatedResourceContext, family: Family, label: i64) -> u64 { + match family { + Family::Optional => 0, + Family::Result(Failure::Integer) => u64::from_ne_bytes(label.to_ne_bytes()), + Family::Result(Failure::String) => text(auth, &format!("failure-{label}")), + Family::Result(Failure::OptionalString) => { + let payload = text(auth, &format!("nested-failure-{label}")); + let value = unsafe { values::jett_rt_v1_sum_new(pointer(auth), 1, payload, 1) }; + assert_ne!(value, 0); + value + } + } +} +fn start( + auth: &AuthenticatedResourceContext, + purpose: ResourcePurpose, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId, u64) { + auth.with_resource(|state, ordinary, registry| { + let attempt = state.begin_entry(ordinary, registry, entry(), purpose)?; + let root = state.root_frame(attempt)?; + let operation = state.begin_operation_frame(ordinary, 1, root)?; + let network = if purpose == ResourcePurpose::Runtime { + state.entry_network(attempt, 0)? + } else { + 0 + }; + Ok((attempt, root, operation, network)) + }) + .unwrap() +} +fn make( + auth: &AuthenticatedResourceContext, + family: Family, + root: ResourceHandleId, + operation: ResourceHandleId, + network: u64, + slot: u32, + occupied: bool, + label: i64, + supplied_companion: Option, +) -> ResourceHandleId { + let failure = if occupied { + 0 + } else { + supplied_companion.unwrap_or_else(|| companion(auth, family, label)) + }; + auth.with_resource(|state, ordinary, registry| { + if occupied { + let call = state.prepare_hook(ordinary, 0, operation, None)?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + let produced = state.factory(ordinary, registry, prepared, network, label)?; + assert_eq!(produced.domain, RESOURCE_DOMAIN_OK); + let owner = state.sum_take( + ordinary, + registry, + 1, + operation, + ResourceHandleId::new(produced.value)?, + operation, + )?; + state.sum_adopt(ordinary, registry, 2 + slot, operation, owner, root) + } else { + let sum = if family.failure_shape().is_some() { + state.failure_sum(ordinary, operation, 4, failure)? + } else { + state.absent_sum(ordinary, operation, 4)? + }; + state.transfer(ordinary, registry, 5 + slot, operation, sum, root) + } + }) + .unwrap() +} +fn finish_setup(auth: &AuthenticatedResourceContext, operation: ResourceHandleId) { + auth.with_resource(|state, ordinary, registry| { + state.end_operation_frame(ordinary, registry, operation) + }) + .unwrap(); +} +fn replace( + auth: &AuthenticatedResourceContext, + root: ResourceHandleId, + old: ResourceHandleId, + rhs: ResourceHandleId, +) -> (JettRuntimeStatusV1, u64) { + let mut output = SENTINEL; + let status = unsafe { + leaves::jett_rt_v1_resource_replace( + pointer(auth), + root.raw(), + REPLACE, + old.raw(), + rhs.raw(), + &mut output, + ) + }; + (status, output) +} +fn shell_rows(state: &NativeResourceState, registry: &ResourceRegistry) -> Vec { + let mut rows = state + .handles + .iter() + .filter_map(|(id, entry)| { + let NativeResourceEntry::Sum(sum) = entry else { + return None; + }; + let payload = match &sum.payload { + NativeSumPayload::None => "none".to_owned(), + NativeSumPayload::Fail { shape, string } => format!("fail:{shape}:{string}"), + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + let key = state.custody.validate_owned(token, registry).unwrap(); + format!( + "{}:{key:?}:{token:?}", + if matches!(&sum.payload, NativeSumPayload::Some(_)) { + "some" + } else { + "ok" + } + ) + } + }; + Some(format!( + "{}:{}:{}:{}:{payload}", + id.raw(), + sum.frame.raw(), + sum.slot, + sum.shape + )) + }) + .collect::>(); + rows.sort(); + rows +} +fn snapshot( + auth: &AuthenticatedResourceContext, +) -> (NativeResourceCounts, Vec, usize, Vec<(i64, u32)>) { + let (counts, shells, remaining) = auth + .with_resource(|state, ordinary, registry| { + Ok(( + state.counts(ordinary, registry), + shell_rows(state, registry), + state.provider.remaining(), + )) + }) + .unwrap(); + (counts, shells, remaining, events(auth)) +} +fn assert_shell_count(auth: &AuthenticatedResourceContext, expected: usize) { + auth.with_resource(|state, _, registry| { + assert_eq!(shell_rows(state, registry).len(), expected); + Ok(()) + }) + .unwrap(); +} +fn complete( + auth: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + selected: u32, + cleanup: u32, +) { + let outcome = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, 0, false) + }) + .unwrap(); + assert_eq!( + ( + outcome.body_status, + outcome.cleanup_status, + outcome.selected_kind + ), + (0, cleanup, selected) + ); + assert_empty(auth); + assert_shell_count(auth, 0); + auth.with_resource(|state, _, _| { + assert_eq!(state.provider.remaining(), 0); + Ok(()) + }) + .unwrap(); +} +fn key(auth: &AuthenticatedResourceContext, handle: ResourceHandleId) -> Option { + auth.with_resource(|state, _, registry| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get(&handle) else { + panic!("the exact live sum shell"); + }; + Ok(match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => Some(format!( + "{:?}", + state.custody.validate_owned(token, registry)? + )), + NativeSumPayload::None | NativeSumPayload::Fail { .. } => None, + }) + }) + .unwrap() +} +fn assert_published( + auth: &AuthenticatedResourceContext, + family: Family, + root: ResourceHandleId, + old: ResourceHandleId, + rhs: ResourceHandleId, + installed: ResourceHandleId, + occupied: bool, + rhs_key: Option, +) { + assert_ne!(installed, old); + assert_ne!(installed, rhs); + auth.with_resource(|state, _, _| { + assert!(!state.handles.contains_key(&old)); + assert!(!state.handles.contains_key(&rhs)); + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get(&installed) else { + panic!("one fresh published sum shell"); + }; + assert_eq!((sum.frame, sum.slot, sum.shape), (root, 0, family.shape())); + assert_eq!(state.sum_tag(installed)?, u32::from(occupied)); + Ok(()) + }) + .unwrap(); + assert_eq!(key(auth, installed), rhs_key); + assert_shell_count(auth, 1); +} + +#[test] +fn native_resource_sum_replace_eight_transitions_preserve_exact_rhs_identity_and_old_cleanup_order() +{ + for family in [Family::Optional, Family::Result(Failure::String)] { + for old_occupied in [false, true] { + for rhs_occupied in [false, true] { + context(|auth| { + let mut rows = Vec::new(); + if old_occupied { + rows.push((1, 1101, 0, "")); + } + if rhs_occupied { + rows.push((1, 1102, 0, "")); + } + install_scripted(auth, &fixture(family), entry(), script(&rows, 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make( + auth, + family, + root, + operation, + network, + 0, + old_occupied, + 1101, + None, + ); + let rhs = make( + auth, + family, + root, + operation, + network, + 1, + rhs_occupied, + 1102, + None, + ); + finish_setup(auth, operation); + assert_shell_count(auth, 2); + let rhs_key = key(auth, rhs); + let rhs_generation = auth + .with_resource(|state, _, _| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get(&rhs) + else { + panic!("exact RHS shell"); + }; + Ok(match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + Some(format!("{token:?}")) + } + NativeSumPayload::None | NativeSumPayload::Fail { .. } => None, + }) + }) + .unwrap(); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!( + status, + JettRuntimeStatusV1::OK, + "{family:?}/{old_occupied}/{rhs_occupied}" + ); + let installed = ResourceHandleId::new(output).unwrap(); + assert_published( + auth, + family, + root, + old, + rhs, + installed, + rhs_occupied, + rhs_key, + ); + if let Some(generation) = rhs_generation { + auth.with_resource(|state, _, _| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get(&installed) + else { + panic!("fresh destination shell"); + }; + let token = match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => { + token + } + _ => panic!("preserved occupied payload"), + }; + assert_ne!( + format!("{token:?}"), + generation, + "the checked holder transfer advances its exact token generation" + ); + Ok(()) + }) + .unwrap(); + } + let mut expected = rows.iter().map(|row| (row.1, 1)).collect::>(); + if old_occupied { + expected.push((1101, 5)); + } + assert_eq!(events(auth), expected); + auth.with_resource(|state, ordinary, registry| { + state.sum_drop(ordinary, registry, DROP_OLD, root, installed) + }) + .unwrap(); + if rhs_occupied { + expected.push((1102, 5)); + } + assert_eq!(events(auth), expected); + assert_shell_count(auth, 0); + complete(auth, attempt, 0, 0); + }); + } + } + } +} + +#[test] +fn native_resource_sum_replace_primitive_and_nested_ordinary_failure_transitions() { + for failure in [Failure::Integer, Failure::OptionalString] { + let family = Family::Result(failure); + for old_occupied in [false, true] { + for rhs_occupied in [false, true] { + context(|auth| { + let mut rows = Vec::new(); + if old_occupied { + rows.push((1, 1201, 0, "")); + } + if rhs_occupied { + rows.push((1, 1202, 0, "")); + } + install_scripted(auth, &fixture(family), entry(), script(&rows, 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make( + auth, + family, + root, + operation, + network, + 0, + old_occupied, + 1201, + None, + ); + let rhs = make( + auth, + family, + root, + operation, + network, + 1, + rhs_occupied, + 1202, + None, + ); + finish_setup(auth, operation); + let rhs_key = key(auth, rhs); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!( + status, + JettRuntimeStatusV1::OK, + "{failure:?}/{old_occupied}/{rhs_occupied}" + ); + assert_published( + auth, + family, + root, + old, + rhs, + ResourceHandleId::new(output).unwrap(), + rhs_occupied, + rhs_key, + ); + complete(auth, attempt, 0, 0); + }); + } + } + } +} + +#[test] +fn native_resource_sum_replace_explicit_string_clone_survives_old_companion_retirement() { + context(|auth| { + let family = Family::Result(Failure::String); + install_scripted(auth, &fixture(family), entry(), script(&[], 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let original = text(auth, "shared ordinary failure"); + let copied = auth + .with_resource(|state, ordinary, _| { + ordinary + .clone_resource_string_companion(&state.layout, 2, original) + .map_err(ordinary_error) + }) + .unwrap(); + assert_eq!( + copied, original, + "the existing explicit String clone retains one identity" + ); + let old = make( + auth, + family, + root, + operation, + network, + 0, + false, + 0, + Some(original), + ); + let rhs = make( + auth, + family, + root, + operation, + network, + 1, + false, + 0, + Some(copied), + ); + finish_setup(auth, operation); + let (status, installed) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::OK); + auth.with_resource(|state, ordinary, registry| { + ordinary + .validate_resource_ordinary(copied, &state.layout, 2) + .map_err(ordinary_error)?; + state.sum_drop( + ordinary, + registry, + DROP_OLD, + root, + ResourceHandleId::new(installed)?, + )?; + assert!( + ordinary + .validate_resource_ordinary(copied, &state.layout, 2) + .is_err() + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0, 0); + assert!(events(auth).is_empty()); + }); +} + +#[test] +fn native_resource_sum_replace_clean_reentry_uses_one_retained_context_and_grant() { + context(|auth| { + let family = Family::Result(Failure::String); + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 1301, 0, ""), (1, 1302, 0, "")], 2), + ) + .unwrap(); + let installation = auth + .with_resource(|state, _, _| Ok((state.program_handle, state.network))) + .unwrap(); + let mut previous_attempt = None; + for label in [1301, 1302] { + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + assert_ne!(Some(attempt), previous_attempt); + previous_attempt = Some(attempt); + assert_eq!( + auth.with_resource(|state, _, _| Ok((state.program_handle, state.network))) + .unwrap(), + installation + ); + let old = make( + auth, family, root, operation, network, 0, false, label, None, + ); + let rhs = make(auth, family, root, operation, network, 1, true, label, None); + finish_setup(auth, operation); + let (status, _) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::OK); + let outcome = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, 0, false) + }) + .unwrap(); + assert_eq!( + ( + outcome.body_status, + outcome.cleanup_status, + outcome.selected_kind + ), + (0, 0, 0) + ); + assert_empty(auth); + assert_shell_count(auth, 0); + } + assert_eq!( + events(auth), + vec![(1301, 1), (1301, 5), (1302, 1), (1302, 5)] + ); + auth.with_resource(|state, _, _| { + assert_eq!(state.provider.remaining(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_sum_replace_absent_old_and_rhs_shell_leases_refuse_before_effects() { + for family in [Family::Optional, Family::Result(Failure::String)] { + for borrow_old in [false, true] { + context(|auth| { + install_scripted(auth, &fixture(family), entry(), script(&[], 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, false, 1401, None); + let rhs = make(auth, family, root, operation, network, 1, false, 1402, None); + finish_setup(auth, operation); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.sum_borrow( + ordinary, + registry, + if borrow_old { BORROW_OLD } else { BORROW_RHS }, + root, + if borrow_old { old } else { rhs }, + ) + }) + .unwrap(); + let before = snapshot(auth); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!( + status, + NativeResourceError::Custody(CustodyError::ActiveBorrow).status() + ); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + auth.with_resource(|state, _, registry| { + state.sum_borrow_end( + registry, + if borrow_old { END_OLD } else { END_RHS }, + root, + loan, + ) + }) + .unwrap(); + // An exported refusal retains the existing first-error latch. + complete(auth, attempt, 1, 0); + assert!(events(auth).is_empty()); + }); + } + } +} + +#[test] +fn native_resource_sum_replace_occupied_payload_loan_on_either_operand_is_preflight_only() { + for family in [Family::Optional, Family::Result(Failure::String)] { + for borrow_old in [false, true] { + context(|auth| { + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 1501, 0, ""), (1, 1502, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 1501, None); + let rhs = make(auth, family, root, operation, network, 1, true, 1502, None); + finish_setup(auth, operation); + let mut loan = auth + .with_resource(|state, _, registry| { + let Some(NativeResourceEntry::Sum(sum)) = + state.handles.get(&if borrow_old { old } else { rhs }) + else { + panic!("occupied sum"); + }; + let token = match &sum.payload { + NativeSumPayload::Some(token) | NativeSumPayload::Ok(token) => token, + _ => panic!("occupied payload"), + }; + let Some(NativeResourceEntry::Frame(borrower)) = state.handles.get(&root) + else { + panic!("exact Scope borrower"); + }; + state + .custody + .begin_borrow(token, &borrower.token, registry) + .map_err(NativeResourceError::from) + }) + .unwrap(); + let before = snapshot(auth); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!( + status, + NativeResourceError::Custody(CustodyError::ActiveBorrow).status() + ); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + auth.with_resource(|state, _, _| { + state + .custody + .end_borrow(&mut loan) + .map_err(NativeResourceError::from) + }) + .unwrap(); + complete(auth, attempt, 1, 0); + assert_eq!( + events(auth), + vec![(1501, 1), (1502, 1), (1502, 5), (1501, 5)] + ); + }); + } + } +} + +#[test] +fn native_resource_sum_replace_stale_duplicate_wrong_family_wrong_row_and_wrong_frame_have_no_cleanup_effect() + { + for invalid in 0..5 { + context(|auth| { + let family = Family::Result(Failure::String); + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 1601, 0, ""), (1, 1602, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 1601, None); + let rhs = make(auth, family, root, operation, network, 1, true, 1602, None); + finish_setup(auth, operation); + let before = snapshot(auth); + let mut output = SENTINEL; + let (frame, row, candidate) = match invalid { + 0 => (root.raw(), REPLACE, operation.raw()), // retired setup frame + 1 => (root.raw(), REPLACE, old.raw()), // duplicate input + 2 => ( + root.raw(), + REPLACE, + auth.with_resource(|state, _, _| Ok(state.program_handle.raw())) + .unwrap(), + ), + 3 => (root.raw(), DROP_RHS, rhs.raw()), // wrong operation family + 4 => (old.raw(), REPLACE, rhs.raw()), // nonframe input + _ => unreachable!(), + }; + let status = unsafe { + leaves::jett_rt_v1_resource_replace( + pointer(auth), + frame, + row, + old.raw(), + candidate, + &mut output, + ) + }; + assert_ne!(status, JettRuntimeStatusV1::OK); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + complete(auth, attempt, 1, 0); + assert_eq!( + events(auth), + vec![(1601, 1), (1602, 1), (1602, 5), (1601, 5)] + ); + }); + } +} + +#[test] +fn native_resource_sum_replace_output_preflight_precedes_lookup_and_preserves_a_running_attempt() { + context(|auth| { + let family = Family::Optional; + install_scripted(auth, &fixture(family), entry(), script(&[], 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, false, 0, None); + let rhs = make(auth, family, root, operation, network, 1, false, 0, None); + finish_setup(auth, operation); + let before = snapshot(auth); + let mut sentinel = SENTINEL; + unsafe { + for output in [ + std::ptr::null_mut(), + pointer(auth).cast_mut().cast::(), + (&mut sentinel as *mut u64) + .cast::() + .add(1) + .cast::(), + ] { + assert_eq!( + leaves::jett_rt_v1_resource_replace(pointer(auth), 0, u32::MAX, 0, 0, output), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(sentinel, SENTINEL); + assert_eq!(snapshot(auth), before); + } + } + let (status, _) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::OK); + complete(auth, attempt, 0, 0); + }); +} + +#[test] +fn native_resource_sum_replace_pure_absence_is_permitted_for_each_existing_purpose() { + for purpose in [ + ResourcePurpose::Runtime, + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + for family in [ + Family::Optional, + Family::Result(Failure::String), + Family::Result(Failure::Integer), + Family::Result(Failure::OptionalString), + ] { + context(|auth| { + install_scripted(auth, &fixture(family), entry(), script(&[], 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, purpose); + let old = make(auth, family, root, operation, network, 0, false, 1701, None); + let rhs = make(auth, family, root, operation, network, 1, false, 1702, None); + finish_setup(auth, operation); + assert_shell_count(auth, 2); + let (status, installed) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::OK, "{purpose:?}/{family:?}"); + assert_published( + auth, + family, + root, + old, + rhs, + ResourceHandleId::new(installed).unwrap(), + false, + None, + ); + complete(auth, attempt, 0, 0); + assert!(events(auth).is_empty()); + }); + } + } +} + +#[test] +fn native_resource_sum_replace_malformed_occupied_payload_never_gains_a_pure_purpose_bypass() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + for family in [Family::Optional, Family::Result(Failure::String)] { + for occupied_old in [false, true] { + context(|auth| { + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 1801, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make( + auth, + family, + root, + operation, + network, + 0, + occupied_old, + 1801, + None, + ); + let rhs = make( + auth, + family, + root, + operation, + network, + 1, + !occupied_old, + 1801, + None, + ); + finish_setup(auth, operation); + // Malformed-state control only: pure Source cannot mint this token. + auth.with_resource(|state, _, _| { + state.attempt.as_mut().unwrap().purpose = purpose; + Ok(()) + }) + .unwrap(); + let before = snapshot(auth); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!(status, NativeResourceError::WrongPurpose.status()); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + auth.with_resource(|state, _, _| { + state.attempt.as_mut().unwrap().purpose = ResourcePurpose::Runtime; + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 1, 0); + assert_eq!(events(auth), vec![(1801, 1), (1801, 5)]); + }); + } + } + } +} + +#[test] +fn native_resource_sum_replace_old_finalizer_panic_retires_rhs_without_publication_and_keeps_first_cleanup() + { + for family in [Family::Optional, Family::Result(Failure::String)] { + for rhs_panics in [false, true] { + context(|auth| { + install_scripted( + auth, + &fixture(family), + entry(), + script( + &[ + (3, 1901, 0, ""), + (if rhs_panics { 3 } else { 1 }, 1902, 0, ""), + ], + 1, + ), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 1901, None); + let rhs = make(auth, family, root, operation, network, 1, true, 1902, None); + finish_setup(auth, operation); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::PANIC); + assert_eq!(output, SENTINEL); + assert_eq!( + events(auth), + vec![(1901, 1), (1902, 1), (1901, 5), (1902, 5)] + ); + assert_shell_count(auth, 0); + auth.with_resource(|state, _, _| { + assert!(!state.handles.contains_key(&old)); + assert!(!state.handles.contains_key(&rhs)); + assert_eq!( + state.attempt.as_ref().unwrap().cleanup, + Some(ResourceCleanupFailure::FinalizerPanic) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 3, JettRuntimeStatusV1::PANIC.code()); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_messages(attempt)?.0, CLEANUP_FAILED); + Ok(()) + }) + .unwrap(); + }); + } + } +} + +#[test] +fn native_resource_sum_replace_invalid_failure_companion_is_rejected_before_old_finalizer() { + context(|auth| { + let family = Family::Result(Failure::String); + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 2001, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 2001, None); + let rhs = make(auth, family, root, operation, network, 1, false, 2002, None); + finish_setup(auth, operation); + let companion = auth + .with_resource(|state, ordinary, _| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get(&rhs) else { + panic!("RHS shell"); + }; + let NativeSumPayload::Fail { shape, string } = sum.payload else { + panic!("failure companion"); + }; + ordinary + .drop_resource_typed_companion(&state.layout, shape, string) + .map_err(ordinary_error)?; + Ok((shape, string)) + }) + .unwrap(); + let before = snapshot(auth); + let (status, output) = replace(auth, root, old, rhs); + assert_ne!(status, JettRuntimeStatusV1::OK); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + assert_eq!(events(auth), vec![(2001, 1)]); + // Repair only for exact terminal cleanup, never to retry or authorize Replace. + let fresh = text(auth, "repair only for cleanup"); + auth.with_resource(|state, _, _| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get_mut(&rhs) else { + panic!("retained malformed RHS"); + }; + let NativeSumPayload::Fail { shape, string } = &mut sum.payload else { + panic!("retained failure companion"); + }; + assert_eq!((*shape, *string), companion); + *string = fresh; + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 1, 0); + assert_eq!(events(auth), vec![(2001, 1), (2001, 5)]); + }); +} + +#[test] +fn native_resource_sum_replace_old_finalizer_panic_also_retires_absent_rhs_shell_and_ordinary_companion() + { + for family in [ + Family::Optional, + Family::Result(Failure::String), + Family::Result(Failure::Integer), + Family::Result(Failure::OptionalString), + ] { + context(|auth| { + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(3, 2101, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 2101, None); + let rhs = make(auth, family, root, operation, network, 1, false, 2102, None); + finish_setup(auth, operation); + assert_shell_count(auth, 2); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::PANIC); + assert_eq!(output, SENTINEL); + assert_shell_count(auth, 0); + auth.with_resource(|state, ordinary, registry| { + assert!(!state.handles.contains_key(&old)); + assert!(!state.handles.contains_key(&rhs)); + assert_eq!(state.counts(ordinary, registry).ordinary_empty, 1); + Ok(()) + }) + .unwrap(); + assert_eq!(events(auth), vec![(2101, 1), (2101, 5)]); + complete(auth, attempt, 3, JettRuntimeStatusV1::PANIC.code()); + }); + } +} + +#[test] +fn native_resource_sum_replace_incoming_finalizer_failure_occurs_at_later_scope_cleanup() { + for family in [Family::Optional, Family::Result(Failure::String)] { + context(|auth| { + install_scripted( + auth, + &fixture(family), + entry(), + script(&[(1, 2201, 0, ""), (3, 2202, 0, "")], 1), + ) + .unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old = make(auth, family, root, operation, network, 0, true, 2201, None); + let rhs = make(auth, family, root, operation, network, 1, true, 2202, None); + finish_setup(auth, operation); + let rhs_key = key(auth, rhs); + let (status, output) = replace(auth, root, old, rhs); + assert_eq!(status, JettRuntimeStatusV1::OK); + assert_published( + auth, + family, + root, + old, + rhs, + ResourceHandleId::new(output).unwrap(), + true, + rhs_key, + ); + assert_eq!(events(auth), vec![(2201, 1), (2202, 1), (2201, 5)]); + auth.with_resource(|state, _, _| { + assert!(state.attempt.as_ref().unwrap().body.is_none()); + assert!(state.attempt.as_ref().unwrap().cleanup.is_none()); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 3, JettRuntimeStatusV1::PANIC.code()); + assert_eq!( + events(auth), + vec![(2201, 1), (2202, 1), (2201, 5), (2202, 5)] + ); + }); + } +} + +#[test] +fn native_resource_sum_replace_shared_nested_ordinary_sum_is_not_an_explicit_string_clone() { + context(|auth| { + let family = Family::Result(Failure::OptionalString); + install_scripted(auth, &fixture(family), entry(), script(&[], 1)).unwrap(); + let (attempt, root, operation, network) = start(auth, ResourcePurpose::Runtime); + let old_companion = companion(auth, family, 2301); + let rhs_companion = companion(auth, family, 2302); + let old = make( + auth, + family, + root, + operation, + network, + 0, + false, + 0, + Some(old_companion), + ); + let rhs = make( + auth, + family, + root, + operation, + network, + 1, + false, + 0, + Some(rhs_companion), + ); + finish_setup(auth, operation); + // Unsafe ABI-state alias: one ordinary sum identity cannot own two edges. + auth.with_resource(|state, _, _| { + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get_mut(&rhs) else { + panic!("RHS shell"); + }; + let NativeSumPayload::Fail { string, .. } = &mut sum.payload else { + panic!("RHS failure"); + }; + *string = old_companion; + Ok(()) + }) + .unwrap(); + let before = snapshot(auth); + let (status, output) = replace(auth, root, old, rhs); + assert_ne!(status, JettRuntimeStatusV1::OK); + assert_eq!(output, SENTINEL); + assert_eq!(snapshot(auth), before); + auth.with_resource(|state, ordinary, _| { + ordinary + .validate_resource_ordinary(old_companion, &state.layout, 8) + .map_err(ordinary_error)?; + ordinary + .validate_resource_ordinary(rhs_companion, &state.layout, 8) + .map_err(ordinary_error)?; + // Restore distinct independently owned edges only for terminal cleanup. + let Some(NativeResourceEntry::Sum(sum)) = state.handles.get_mut(&rhs) else { + panic!("RHS shell"); + }; + let NativeSumPayload::Fail { string, .. } = &mut sum.payload else { + panic!("RHS failure"); + }; + assert_eq!(*string, old_companion); + *string = rhs_companion; + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 1, 0); + assert!(events(auth).is_empty()); + }); +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs new file mode 100644 index 00000000..bb7597da --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_api.rs @@ -0,0 +1,397 @@ +//! Private matched-archive exports. Parent wiring requires BOTH private cfgs. +//! Observations borrow the live context and never clear its failure channels. + +use super::*; +use std::mem::{align_of, offset_of, size_of}; + +const MAX_BYTES: usize = 1024 * 1024; +const MAX_EVENTS: usize = 8192; + +#[repr(C)] +pub(super) struct ObjectManifest { + layout: *const u8, + layout_length: u64, + entry_function: u32, + entry_signature: u32, + entry_scope: u32, + reserved: u32, +} + +#[repr(C)] +pub(super) struct ArchiveInfo { + runtime_abi: u32, + test_protocol: u32, + layout_wire: u32, + pointer_bits: u32, + archive_profile: u32, + reserved: [u32; 3], +} + +const _: () = { + assert!(size_of::() == 32 && align_of::() == 8); + assert!(offset_of!(ObjectManifest, entry_function) == 16); + assert!(size_of::() == 32 && align_of::() == 4); + assert!(size_of::() == 24); + assert!(size_of::() == 72); + assert!(size_of::() == 24); +}; + +#[derive(Clone, Copy)] +struct Region { + start: usize, + end: usize, +} + +fn region(pointer: *const T, count: usize) -> ResourceResult { + if pointer.is_null() || (pointer as usize) % align_of::() != 0 { + return Err(NativeResourceError::WrongOperation); + } + let length = size_of::() + .checked_mul(count) + .filter(|length| *length <= isize::MAX as usize) + .ok_or(NativeResourceError::Capacity)?; + let start = pointer as usize; + let end = start + .checked_add(length) + .ok_or(NativeResourceError::Capacity)?; + Ok(Region { start, end }) +} + +fn disjoint(regions: &[Region]) -> ResourceResult<()> { + for (index, left) in regions.iter().enumerate() { + for right in ®ions[index + 1..] { + if left.start < right.end && right.start < left.end { + return Err(NativeResourceError::WrongOperation); + } + } + } + Ok(()) +} + +fn checked_length(length: u64, maximum: usize) -> ResourceResult { + usize::try_from(length) + .ok() + .filter(|length| *length <= maximum) + .ok_or(NativeResourceError::Capacity) +} + +fn result_call( + out_result: *mut JettRuntimeResultV1, + operation: impl FnOnce() -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + if region(out_result, 1).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + complete_call(out_result, || match operation() { + Ok(()) => JettRuntimeResultV1::ok(), + Err(error) => JettRuntimeResultV1::failure(error.status(), REFUSED), + }) +} + +fn outputs( + context: *const JettRuntimeContextV1, + output: *mut T, + out_result: *mut JettRuntimeResultV1, +) -> ResourceResult<()> { + disjoint(&[ + region(context, 1)?, + region(output, 1)?, + region(out_result, 1)?, + ]) +} + +fn context_call( + context: *const JettRuntimeContextV1, + output: *mut T, + out_result: *mut JettRuntimeResultV1, + operation: impl FnOnce(&AuthenticatedResourceContext) -> ResourceResult<()>, +) -> JettRuntimeStatusV1 { + // Do not let complete_call write through a pointer overlapping the context. + if outputs(context, output, out_result).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + result_call(out_result, || { + operation(&AuthenticatedResourceContext::acquire(context)?) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_archive_info( + out_info: *mut ArchiveInfo, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + if region(out_info, 1) + .and_then(|info| disjoint(&[info, region(out_result, 1)?])) + .is_err() + { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + result_call(out_result, || { + // This shared codec constant is a prerequisite of the v2 runtime packet. + unsafe { + ptr::write( + out_info, + ArchiveInfo { + runtime_abi: JETT_RUNTIME_ABI_VERSION_V1, + test_protocol: 2, + layout_wire: crate::resource_custody::NATIVE_RESOURCE_LAYOUT_WIRE_VERSION, + pointer_bits: usize::BITS, + archive_profile: u32::from(!cfg!(debug_assertions)), + reserved: [0; 3], + }, + ); + } + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_install( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + script: *const u8, + script_length: u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + // Validate result/context separation before the result writer is entered. + if outputs(context, manifest.cast_mut(), out_result).is_err() { + return JettRuntimeStatusV1::INVALID_ARGUMENT; + } + let manifest = unsafe { &*manifest }; + let lengths = (|| { + let layout_length = checked_length(manifest.layout_length, MAX_BYTES)?; + let script_length = checked_length(script_length, MAX_BYTES)?; + if layout_length == 0 || script_length == 0 { + return Err(NativeResourceError::WrongOperation); + } + disjoint(&[ + region(context, 1)?, + region(manifest, 1)?, + region(out_result, 1)?, + region(manifest.layout, layout_length)?, + region(script, script_length)?, + ])?; + Ok((layout_length, script_length)) + })(); + let (layout_length, script_length) = match lengths { + Ok(lengths) => lengths, + // A refusal must not overwrite any readonly input through out_result. + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + if manifest.reserved != 0 { + return Err(NativeResourceError::InvalidEntry); + } + let layout = unsafe { slice::from_raw_parts(manifest.layout, layout_length) }; + let version = layout.get(8..12).ok_or(NativeResourceError::InvalidEntry)?; + if ![2u32, 3].iter().any(|known| version == known.to_le_bytes()) { + return Err(NativeResourceError::InvalidEntry); + } + let script = + DecodedScript::decode(unsafe { slice::from_raw_parts(script, script_length) })?; + install_scripted( + &authenticated, + layout, + NativeEntry { + function: manifest.entry_function, + signature: manifest.entry_signature, + scope: manifest.entry_scope, + }, + script, + )?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_entry_begin( + context: *const JettRuntimeContextV1, + out_attempt: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_attempt, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + let attempt = resource.begin_entry( + &mut state.values, + &state.resources, + resource.entry, + ResourcePurpose::Runtime, + )?; + unsafe { ptr::write(out_attempt, attempt.raw()) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_entry_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body_status: u32, + body_panicked: u32, + out_completion: *mut NativeResourceCompletion, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_completion, out_result, |authenticated| { + if body_panicked > 1 { + return Err(NativeResourceError::WrongOperation); + } + let attempt = ResourceHandleId::new(attempt)?; + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + let completion = resource.complete_entry( + &mut state.values, + &mut state.resources, + attempt, + body_status, + body_panicked != 0, + )?; + unsafe { ptr::write(out_completion, completion) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_counts( + context: *const JettRuntimeContextV1, + out_counts: *mut NativeResourceCounts, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_counts, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let counts = match &state.resource_state { + Some(resource) => resource.counts(&state.values, &state.resources), + None => NativeResourceCounts { + registry: state.resources.live_count() as u64, + ordinary_empty: u32::from(state.values.is_empty()), + ..NativeResourceCounts::default() + }, + }; + unsafe { ptr::write(out_counts, counts) }; + Ok(()) + }) + }) +} + +fn copy_regions( + context: *const JettRuntimeContextV1, + buffer: *mut T, + capacity: u64, + maximum: usize, + out_count: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> ResourceResult { + let capacity = checked_length(capacity, maximum)?; + let regions = [ + region(context, 1)?, + region(out_count, 1)?, + region(out_result, 1)?, + ]; + disjoint(®ions)?; + if capacity != 0 { + let buffer = region(buffer, capacity)?; + for other in regions { + disjoint(&[buffer, other])?; + } + } + Ok(capacity) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_events_copy( + context: *const JettRuntimeContextV1, + buffer: *mut NativeTestEvent, + capacity: u64, + out_count: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let capacity = match copy_regions(context, buffer, capacity, MAX_EVENTS, out_count, out_result) + { + Ok(capacity) => capacity, + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + authenticated.with_state(|_, state| { + let copy = |events: &[NativeTestEvent]| { + unsafe { ptr::write(out_count, events.len() as u64) }; + if capacity == 0 { + return Ok(()); + } + if capacity < events.len() { + return Err(NativeResourceError::Capacity); + } + unsafe { ptr::copy_nonoverlapping(events.as_ptr(), buffer, events.len()) }; + Ok(()) + }; + match &state.resource_state { + Some(resource) => resource.observe_events(copy), + None => copy(&[]), + } + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_failure_copy( + context: *const JettRuntimeContextV1, + attempt: u64, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + let capacity = match copy_regions(context, buffer, capacity, 65_536, out_length, out_result) { + Ok(capacity) => capacity, + Err(_) => return JettRuntimeStatusV1::INVALID_ARGUMENT, + }; + result_call(out_result, || { + let authenticated = AuthenticatedResourceContext::acquire(context)?; + let attempt = ResourceHandleId::new(attempt)?; + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)?; + // This private observation reports only the Resource channel. + // The matched launcher copies the ordinary channel separately. + let (message, _) = resource.completed_messages(attempt)?; + unsafe { ptr::write(out_length, message.len() as u64) }; + if capacity == 0 { + return Ok(()); + } + if capacity < message.len() { + return Err(NativeResourceError::Capacity); + } + unsafe { ptr::copy_nonoverlapping(message.as_ptr(), buffer, message.len()) }; + Ok(()) + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_resource_test_script_remaining( + context: *const JettRuntimeContextV1, + out_remaining: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + context_call(context, out_remaining, out_result, |authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)?; + unsafe { ptr::write(out_remaining, resource.script_remaining() as u64) }; + Ok(()) + }) + }) +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs new file mode 100644 index 00000000..28834fef --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_main.rs @@ -0,0 +1,782 @@ +//! Linked only into the single matched private compiler-test runtime archive. +//! The parent module must gate this file on both test and the private archive cfg. +#![cfg(all(test, jett_resource_native_test_archive))] + +use crate::native_abi::{ + JETT_RUNTIME_ABI_VERSION_V1, JettRuntimeContextV1, JettRuntimeResultV1, JettRuntimeStatusV1, + jett_rt_v1_context_create, jett_rt_v1_context_destroy, +}; +use std::ffi::{c_char, c_int, c_void}; +use std::mem::{MaybeUninit, align_of, offset_of, size_of}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::ptr; + +#[path = "test_archive_main/report.rs"] +mod report; +use report::{Attempt, Report}; +#[path = "test_archive_main/controls.rs"] +mod controls; + +const EXIT_CREATE: c_int = 70; +const EXIT_ENTRY: c_int = 71; +const EXIT_DESTROY: c_int = 72; +const EXIT_PANIC: c_int = 73; +const EXIT_INFRASTRUCTURE: c_int = 74; +const MAX_SCRIPT: usize = 1_048_576; + +#[repr(C)] +struct ObjectManifest { + layout: *const u8, + length: u64, + function: u32, + signature: u32, + scope: u32, + reserved: u32, +} +#[repr(C)] +struct ArchiveInfo { + runtime_abi: u32, + protocol: u32, + wire: u32, + pointer_bits: u32, + profile: u32, + reserved: [u32; 3], +} +#[repr(C)] +struct Completion { + attempt: u64, + body_status: u32, + cleanup_status: u32, + selected_kind: u32, + reserved: u32, +} +#[repr(C)] +struct Counts { + owners: u64, + loans: u64, + frames: u64, + registry_entries: u64, + owner_handles: u64, + loan_handles: u64, + frame_handles: u64, + provisional_returns: u64, + ordinary_empty: u32, + reserved: u32, +} +impl Counts { + fn clean(&self) -> bool { + self.reserved == 0 + && self.ordinary_empty == 1 + && [ + self.owners, + self.loans, + self.frames, + self.registry_entries, + self.owner_handles, + self.loan_handles, + self.frame_handles, + self.provisional_returns, + ] + .into_iter() + .all(|value| value == 0) + } +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Event { + sequence: u64, + label: i64, + kind: u32, + reserved: u32, +} + +const _: () = { + assert!(size_of::() == 32 && align_of::() == 8); + assert!(offset_of!(ObjectManifest, function) == 16 && offset_of!(ObjectManifest, scope) == 24); + assert!(size_of::() == 32 && align_of::() == 4); + assert!(size_of::() == 24 && offset_of!(Completion, body_status) == 8); + assert!(size_of::() == 72 && offset_of!(Counts, ordinary_empty) == 64); + assert!(size_of::() == 24 && offset_of!(Event, kind) == 16); +}; + +unsafe extern "C" { + fn jett_aot_v1_entry(context: *mut c_void) -> u32; + fn jett_aot_resource_v1_manifest(out: *mut ObjectManifest) -> u32; + fn jett_rt_v1_resource_test_archive_info( + out: *mut ArchiveInfo, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_install( + context: *const JettRuntimeContextV1, + manifest: *const ObjectManifest, + script: *const u8, + length: u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_entry_begin( + context: *const JettRuntimeContextV1, + attempt: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_entry_complete( + context: *const JettRuntimeContextV1, + attempt: u64, + body: u32, + panicked: u32, + completion: *mut Completion, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_counts( + context: *const JettRuntimeContextV1, + counts: *mut Counts, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_events_copy( + context: *const JettRuntimeContextV1, + events: *mut Event, + capacity: u64, + count: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_failure_copy( + context: *const JettRuntimeContextV1, + attempt: u64, + buffer: *mut u8, + capacity: u64, + length: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; + fn jett_rt_v1_resource_test_script_remaining( + context: *const JettRuntimeContextV1, + remaining: *mut u64, + result: *mut JettRuntimeResultV1, + ) -> JettRuntimeStatusV1; +} + +fn contained(operation: impl FnOnce() -> T) -> Result { + catch_unwind(AssertUnwindSafe(operation)).map_err(crate::discard_panic_payload) +} +fn ok(status: JettRuntimeStatusV1) -> Result<(), ()> { + if status == JettRuntimeStatusV1::OK { + Ok(()) + } else { + Err(()) + } +} + +struct ActiveAttempt { + id: u64, + index: usize, + body_status: Option, + panicked: u32, + completion_attempted: bool, +} +struct Session { + context: Box>, + created: bool, + destroy_attempts: u32, + destroy_status: u32, + active: Option, +} +impl Session { + fn new() -> Self { + Self { + context: Box::new(MaybeUninit::uninit()), + created: false, + destroy_attempts: 0, + destroy_status: 0, + active: None, + } + } + fn pointer(&mut self) -> *mut JettRuntimeContextV1 { + self.context.as_mut_ptr() + } + fn create(&mut self) -> Result<(), ()> { + let mut result = MaybeUninit::uninit(); + // Distinct aligned outputs; the Box keeps the context stationary until destroy. + ok(unsafe { + jett_rt_v1_context_create( + JETT_RUNTIME_ABI_VERSION_V1, + self.pointer(), + result.as_mut_ptr(), + ) + })?; + self.created = true; + Ok(()) + } + fn destroy_once(&mut self) { + if !self.created || self.destroy_attempts != 0 { + return; + } + self.destroy_attempts = 1; + let mut result = MaybeUninit::uninit(); + self.destroy_status = match contained(|| unsafe { + jett_rt_v1_context_destroy(self.pointer(), result.as_mut_ptr()) + }) { + Ok(status) => status.code(), + Err(()) => JettRuntimeStatusV1::PANIC.code(), + }; + } +} +impl Drop for Session { + fn drop(&mut self) { + self.destroy_once(); + } +} + +fn script() -> Result<(Vec, usize), ()> { + let encoded = std::env::var("JETT_RESOURCE_NATIVE_TEST_SCRIPT_V1").map_err(|_| ())?; + if encoded.len() > MAX_SCRIPT * 2 || encoded.len() % 2 != 0 { + return Err(()); + } + let mut decoded = Vec::new(); + decoded + .try_reserve_exact(encoded.len() / 2) + .map_err(|_| ())?; + fn nibble(value: u8) -> Result { + match value { + b'0'..=b'9' => Ok(value - b'0'), + b'a'..=b'f' => Ok(value - b'a' + 10), + b'A'..=b'F' => Ok(value - b'A' + 10), + _ => Err(()), + } + } + for pair in encoded.as_bytes().chunks_exact(2) { + decoded.push((nibble(pair[0])? << 4) | nibble(pair[1])?); + } + if decoded.len() < 32 + || &decoded[..8] != b"JTRST001" + || decoded[8..12] != 1u32.to_le_bytes() + || decoded[12..16] != [0; 4] + || u64::from_le_bytes(decoded[16..24].try_into().map_err(|_| ())?) != decoded.len() as u64 + { + return Err(()); + } + let attempts = u32::from_le_bytes(decoded[24..28].try_into().map_err(|_| ())?) as usize; + let operations = u32::from_le_bytes(decoded[28..32].try_into().map_err(|_| ())?); + if !(1..=report::MAX_ATTEMPTS).contains(&attempts) || operations > 4096 { + return Err(()); + } + // The sole runtime decoder validates every closed operation before installation. + Ok((decoded, attempts)) +} + +fn install(session: &mut Session, script: &[u8]) -> Result<(), ()> { + let mut result = MaybeUninit::uninit(); + let mut info = MaybeUninit::uninit(); + ok(unsafe { jett_rt_v1_resource_test_archive_info(info.as_mut_ptr(), result.as_mut_ptr()) })?; + let info = unsafe { info.assume_init() }; + if info.runtime_abi != JETT_RUNTIME_ABI_VERSION_V1 + || info.protocol != 2 + || info.wire != crate::resource_custody::NATIVE_RESOURCE_LAYOUT_WIRE_VERSION + || info.pointer_bits != 64 + || info.profile != u32::from(!cfg!(debug_assertions)) + || info.reserved != [0; 3] + { + return Err(()); + } + let mut manifest = MaybeUninit::uninit(); + if unsafe { jett_aot_resource_v1_manifest(manifest.as_mut_ptr()) } != 0 { + return Err(()); + } + let manifest = unsafe { manifest.assume_init() }; + if manifest.reserved != 0 + || manifest.layout.is_null() + || manifest.length == 0 + || manifest.length > isize::MAX as u64 + { + return Err(()); + } + ok(unsafe { + jett_rt_v1_resource_test_install( + session.pointer(), + &manifest, + script.as_ptr(), + script.len() as u64, + result.as_mut_ptr(), + ) + }) +} + +type CopyMessage = unsafe extern "C" fn( + *const JettRuntimeContextV1, + *mut u8, + u64, + *mut u64, + *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1; +fn message(context: *const JettRuntimeContextV1, copy: CopyMessage) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut length = 0; + ok(unsafe { + copy( + context, + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(length).map_err(|_| ())?; + if required > report::MAX_MESSAGE { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize(required, 0); + let mut copied = 0; + ok(unsafe { + copy( + context, + output.as_mut_ptr(), + length, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != length { + return Err(()); + } + Ok(output) +} +fn resource_message(context: *const JettRuntimeContextV1, attempt: u64) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut length = 0; + ok(unsafe { + jett_rt_v1_resource_test_failure_copy( + context, + attempt, + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(length).map_err(|_| ())?; + if required > report::MAX_MESSAGE { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize(required, 0); + let mut copied = 0; + ok(unsafe { + jett_rt_v1_resource_test_failure_copy( + context, + attempt, + output.as_mut_ptr(), + length, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != length { + return Err(()); + } + Ok(output) +} +fn events(context: *const JettRuntimeContextV1) -> Result, ()> { + let mut result = MaybeUninit::uninit(); + let mut count = 0; + ok(unsafe { + jett_rt_v1_resource_test_events_copy( + context, + ptr::null_mut(), + 0, + &mut count, + result.as_mut_ptr(), + ) + })?; + let required = usize::try_from(count).map_err(|_| ())?; + if required > report::MAX_EVENTS { + return Err(()); + } + let mut output = Vec::new(); + output.try_reserve_exact(required).map_err(|_| ())?; + output.resize( + required, + Event { + sequence: 0, + label: 0, + kind: 0, + reserved: 0, + }, + ); + let mut copied = 0; + ok(unsafe { + jett_rt_v1_resource_test_events_copy( + context, + output.as_mut_ptr(), + count, + &mut copied, + result.as_mut_ptr(), + ) + })?; + if copied != count { + return Err(()); + } + let mut previous = 0; + for event in &output { + if event.sequence <= previous || event.reserved != 0 || !(1..=5).contains(&event.kind) { + return Err(()); + } + previous = event.sequence; + } + Ok(output) +} + +fn observations( + context: *const JettRuntimeContextV1, +) -> Result { + // Read-only authenticated snapshot: no with_resource, failure, reset or script mutation. + super::test_observations::snapshot(context).map_err(|_| ()) +} + +// Every query is independent. One refused copy must not suppress later +// pre-destroy observations or erase a prefix already collected. +fn counts(context: *const JettRuntimeContextV1) -> Result { + let mut result = MaybeUninit::uninit(); + let mut output = MaybeUninit::uninit(); + ok(unsafe { + jett_rt_v1_resource_test_counts(context, output.as_mut_ptr(), result.as_mut_ptr()) + })?; + Ok(unsafe { output.assume_init() }) +} +fn remaining(context: *const JettRuntimeContextV1) -> Result { + let mut result = MaybeUninit::uninit(); + let mut output = 0; + ok(unsafe { + jett_rt_v1_resource_test_script_remaining(context, &mut output, result.as_mut_ptr()) + })?; + Ok(output) +} +fn ordinary_status(context: *const JettRuntimeContextV1) -> Result { + let mut output = MaybeUninit::uninit(); + // This read returns the current first failure without taking or resetting it. + let status = unsafe { + crate::native_abi::values::jett_rt_v1_value_failure(context, output.as_mut_ptr()) + }; + let output = unsafe { output.assume_init() }; + if output.status != status { + return Err(()); + } + Ok(status.code()) +} +fn query(operation: impl FnOnce() -> Result) -> Result { + contained(operation)? +} +fn complete_active(session: &mut Session, report: &mut Report) { + let Some(active) = session.active.as_mut() else { + return; + }; + if active.completion_attempted { + return; + } + active.completion_attempted = true; + let id = active.id; + let index = active.index; + let body = active + .body_status + .unwrap_or(JettRuntimeStatusV1::PANIC.code()); + let panicked = if active.body_status.is_some() { + active.panicked + } else { + 1 + }; + // An orchestration abort before an observed entry result still requires + // cleanup, but cannot produce an accepted body-status observation. + if active.body_status.is_none() { + report.refused = true; + } + let completion = query(|| { + let mut result = MaybeUninit::uninit(); + let mut output = MaybeUninit::uninit(); + ok(unsafe { + jett_rt_v1_resource_test_entry_complete( + session.pointer(), + id, + body, + panicked, + output.as_mut_ptr(), + result.as_mut_ptr(), + ) + })?; + Ok(unsafe { output.assume_init() }) + }); + match completion { + Ok(value) => { + // Preserve the completed header before any fallible later copy. + let valid = value.attempt == id + && value.body_status == body + && value.reserved == 0 + && value.selected_kind <= 3; + report.attempts[index].completion = Some(value); + if !valid { + report.refused = true; + } + } + Err(()) => report.refused = true, + } + // A refused completion is never retried. Destroy's safety cleanup remains + // the final fallback and its pre-destroy counts must expose any live state. + session.active = None; +} +fn observe_attempt(session: &mut Session, report: &mut Report, index: usize) { + let context = session.pointer(); + let id = report.attempts[index].id; + match query(|| counts(context)) { + Ok(value) => report.attempts[index].counts = Some(value), + Err(()) => report.refused = true, + } + match query(|| ordinary_status(context)) { + Ok(value) => report.attempts[index].ordinary_status = Some(value), + Err(()) => report.refused = true, + } + match query(|| { + message( + context, + crate::native_abi::values::jett_rt_v1_value_failure_copy, + ) + }) { + Ok(value) => report.attempts[index].ordinary_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| resource_message(context, id)) { + Ok(value) => report.attempts[index].resource_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| events(context)) { + Ok(value) => { + report.attempts[index].event_sequence_end = + Some(value.last().map_or(0, |event| event.sequence)); + report.events = Some(value); + } + Err(()) => report.refused = true, + } + match query(|| observations(context)) { + Ok(value) => report.observations = Some(value), + Err(()) => report.refused = true, + } + match query(|| remaining(context)) { + Ok(value) => report.remaining = Some(value), + Err(()) => report.refused = true, + } +} +fn observe_final(session: &mut Session, report: &mut Report) { + if !session.created { + return; + } + if contained(|| complete_active(session, report)).is_err() { + report.refused = true; + } + if let Some(index) = report.attempts.len().checked_sub(1) { + if !report.attempts[index].observations_complete() + && contained(|| observe_attempt(session, report, index)).is_err() + { + report.refused = true; + } + } + let context = session.pointer(); + match query(|| counts(context)) { + Ok(value) => report.final_counts = Some(value), + Err(()) => report.refused = true, + } + match query(|| ordinary_status(context)) { + Ok(value) => report.final_ordinary_status = Some(value), + Err(()) => report.refused = true, + } + match query(|| { + message( + context, + crate::native_abi::values::jett_rt_v1_value_failure_copy, + ) + }) { + Ok(value) => report.final_ordinary_message = Some(value), + Err(()) => report.refused = true, + } + match query(|| events(context)) { + Ok(value) => report.events = Some(value), + Err(()) => report.refused = true, + } + match query(|| observations(context)) { + Ok(value) => report.observations = Some(value), + Err(()) => report.refused = true, + } + match query(|| remaining(context)) { + Ok(value) => report.remaining = Some(value), + Err(()) => report.refused = true, + } +} +struct AttemptOutcome { + exit: c_int, + reenter: bool, +} +fn run_attempt(session: &mut Session, report: &mut Report) -> Result { + report.attempts.try_reserve(1).map_err(|_| ())?; + let mut result = MaybeUninit::uninit(); + let mut attempt = 0; + ok(unsafe { + jett_rt_v1_resource_test_entry_begin(session.pointer(), &mut attempt, result.as_mut_ptr()) + })?; + if attempt == 0 { + return Err(()); + } + let index = report.attempts.len(); + report.attempts.push(Attempt::pending(attempt)); + session.active = Some(ActiveAttempt { + id: attempt, + index, + body_status: None, + panicked: 0, + completion_attempted: false, + }); + let (body_status, panicked) = + match contained(|| unsafe { jett_aot_v1_entry(session.pointer().cast()) }) { + Ok(status) => (status, 0), + Err(()) => (JettRuntimeStatusV1::PANIC.code(), 1), + }; + report.attempts[index].observed_body_status = Some(body_status); + let active = session.active.as_mut().ok_or(())?; + active.body_status = Some(body_status); + active.panicked = panicked; + // Completion follows the entry immediately, including a caught entry panic. + complete_active(session, report); + observe_attempt(session, report, index); + if report.refused { + return Err(()); + } + let observed = &report.attempts[index]; + let completion = observed.completion.as_ref().ok_or(())?; + let counts = observed.counts.as_ref().ok_or(())?; + let ordinary = observed.ordinary_status.ok_or(())?; + if !counts.clean() { + return Err(()); + } + let exit = match completion.selected_kind { + 0 => 0, + 1 | 3 => EXIT_ENTRY, + 2 => EXIT_PANIC, + _ => return Err(()), + }; + // Errors remain immutable attempt observations. Only entry_begin can issue + // the next ordinary channel after predecessor, session poison and retirement + // checks. The launcher neither resets nor takes a failure. + let _ = ordinary; // Preserve the required known observation for this attempt. + Ok(AttemptOutcome { + exit, + reenter: true, + }) +} +fn run(session: &mut Session, report: &mut Report) -> c_int { + let (script, attempts) = match script() { + Ok(value) => value, + Err(()) => { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + }; + if session.create().is_err() { + report.refused = true; + return EXIT_CREATE; + } + if install(session, &script).is_err() { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + let mut exit = 0; + for _ in 0..attempts { + match run_attempt(session, report) { + Ok(outcome) => { + if exit == 0 { + exit = outcome.exit; + } + if !outcome.reenter { + break; + } + } + Err(()) => { + report.refused = true; + return EXIT_INFRASTRUCTURE; + } + } + } + // The final observation pass also runs for every earlier return or panic. + exit +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn main(_argc: c_int, _argv: *mut *mut c_char) -> c_int { + match controls::dispatch() { + Ok(Some(exit)) => return exit, + Ok(None) => {} + Err(()) => return EXIT_INFRASTRUCTURE, + } + // The file itself and its parent require both private cfgs. Ordinary runtime + // and normal unit-test binaries retain Rust's existing panic hook. + let outer = contained(|| { + // Do not leave an earlier successful report after a refused current run. + match std::fs::remove_file("resource-native-report-v1.bin") { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(_) => return EXIT_INFRASTRUCTURE, + } + std::panic::set_hook(Box::new(|_| {})); + let mut session = Session::new(); + let mut report = Report::default(); + let mut exit = match contained(|| run(&mut session, &mut report)) { + Ok(exit) => exit, + Err(()) => { + report.refused = true; + EXIT_INFRASTRUCTURE + } + }; + // Observations precede the sole teardown even after orchestration panic. + if contained(|| observe_final(&mut session, &mut report)).is_err() { + report.refused = true; + exit = EXIT_INFRASTRUCTURE; + } + if report.refused + || report + .observations + .as_ref() + .is_some_and(|snapshot| snapshot.flags != 0) + { + exit = EXIT_INFRASTRUCTURE; + } + if exit == 0 + && (report.remaining != Some(0) + || !report.final_counts.as_ref().is_some_and(Counts::clean)) + { + report.refused = true; + exit = EXIT_INFRASTRUCTURE; + } + session.destroy_once(); + report.created = session.created; + report.destroy_attempts = session.destroy_attempts; + report.destroy_status = session.destroy_status; + if session.created && session.destroy_status != 0 { + exit = EXIT_DESTROY; + } + // The report cannot encode unknown custody/messages/remaining or a missing snapshot. Preserve + // the internal completed prefix, refuse the report, and exit nonzero. + let bytes = match report.encode() { + Ok(bytes) => bytes, + Err(()) => { + return if exit == EXIT_DESTROY { + exit + } else { + EXIT_INFRASTRUCTURE + }; + } + }; + if std::fs::write("resource-native-report-v1.bin", bytes).is_ok() { + return exit; + } + report.write_status = JettRuntimeStatusV1::IO_FAILURE.code(); + if let Ok(bytes) = report.encode() { + let _ = std::fs::write("resource-native-report-v1.bin", bytes); + } + EXIT_INFRASTRUCTURE + }); + outer.unwrap_or(EXIT_INFRASTRUCTURE) +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_main/controls.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_main/controls.rs new file mode 100644 index 00000000..7b053f41 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_main/controls.rs @@ -0,0 +1,656 @@ +//! Opt-in host ABI controls; never invokes or certifies a compiled Source body. +#![cfg(all(test, jett_resource_native_test_archive))] +use super::*; +use crate::native_abi::resource::{ + AuthenticatedResourceContext, NativeBodyFailure, NativeResourceError, leaves, test_observations, +}; +use crate::resource_custody::{JettResourceCallResultV1, NativeOperation, ResourcePurpose}; + +const MODE_ENV: &str = "JETT_RESOURCE_NATIVE_TEST_CONTROL_V2"; +const CONTROL_FILE: &str = "resource-native-host-abi-control-v2.bin"; +const SOURCE_REPORT_FILE: &str = "resource-native-report-v1.bin"; +const MAX_CONTROL: usize = 16 * 1024 * 1024 + 256; + +#[derive(Clone, Copy)] +#[repr(u32)] +enum Mode { + HookNullOutput = 1, + HookContextOverlapOutput = 2, + HookUnknownOperation = 3, + FactoryPrepareNullOutput = 4, + FactoryPrepareContextOverlapOutput = 5, + FactoryPrepareZeroCall = 6, + FactoryCommitNullOutput = 7, + FactoryCommitContextOverlapOutput = 8, + FactoryCommitZeroPrepared = 9, + PropertyAcquireHook = 10, + RecorderOnly = 11, + OversizedPropertyContext = 12, +} +impl Mode { + fn parse(value: &str) -> Result { + match value { + "hook-null-output" => Ok(Self::HookNullOutput), + "hook-context-overlap-output" => Ok(Self::HookContextOverlapOutput), + "hook-unknown-operation" => Ok(Self::HookUnknownOperation), + "factory-prepare-null-output" => Ok(Self::FactoryPrepareNullOutput), + "factory-prepare-context-overlap-output" => { + Ok(Self::FactoryPrepareContextOverlapOutput) + } + "factory-prepare-zero-call" => Ok(Self::FactoryPrepareZeroCall), + "factory-commit-null-output" => Ok(Self::FactoryCommitNullOutput), + "factory-commit-context-overlap-output" => Ok(Self::FactoryCommitContextOverlapOutput), + "factory-commit-zero-prepared" => Ok(Self::FactoryCommitZeroPrepared), + "property-acquire-hook" => Ok(Self::PropertyAcquireHook), + "recorder-only" => Ok(Self::RecorderOnly), + "oversized-property-context" => Ok(Self::OversizedPropertyContext), + _ => Err(()), + } + } + fn purpose(self) -> ResourcePurpose { + match self { + Self::PropertyAcquireHook => ResourcePurpose::Property, + _ => ResourcePurpose::Runtime, + } + } + fn expected_status(self) -> u32 { + match self { + Self::OversizedPropertyContext => JettRuntimeStatusV1::OK.code(), + Self::RecorderOnly => unreachable!("recorder mode has no host status"), + _ => JettRuntimeStatusV1::INVALID_ARGUMENT.code(), + } + } + fn expected_body(self) -> RecordedBody { + match self { + Self::HookNullOutput + | Self::HookContextOverlapOutput + | Self::FactoryPrepareNullOutput + | Self::FactoryPrepareContextOverlapOutput + | Self::FactoryCommitNullOutput + | Self::FactoryCommitContextOverlapOutput => RecordedBody::None, + Self::HookUnknownOperation => RecordedBody::WrongOperation, + Self::FactoryPrepareZeroCall | Self::FactoryCommitZeroPrepared => { + RecordedBody::InvalidHandle + } + Self::PropertyAcquireHook => RecordedBody::WrongPurpose, + Self::OversizedPropertyContext => RecordedBody::None, + Self::RecorderOnly => unreachable!("recorder mode has no host body"), + } + } + fn counters(self) -> [u64; 5] { + match self { + Self::HookNullOutput | Self::HookContextOverlapOutput | Self::PropertyAcquireHook => { + [1, 1, 0, 0, 0] + } + Self::HookUnknownOperation => [1, 0, 0, 0, 0], + Self::OversizedPropertyContext => [0; 5], + Self::FactoryPrepareNullOutput + | Self::FactoryPrepareContextOverlapOutput + | Self::FactoryPrepareZeroCall => [0, 0, 1, 0, 0], + Self::FactoryCommitNullOutput + | Self::FactoryCommitContextOverlapOutput + | Self::FactoryCommitZeroPrepared => [0, 0, 0, 1, 0], + Self::RecorderOnly => unreachable!("recorder mode has no host counters"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum RecordedBody { + None, + WrongPurpose, + InvalidHandle, + WrongOperation, + WrongFrame, + OtherResource(u32), + HostPanic, +} + +struct OversizeProbe { + before_length: u64, + after_length: u64, + bytes_preserved: bool, + cleared: bool, +} + +struct Outcome { + mode: Mode, + host_status: Option, + recorder_result: Option, + recorded_body: Option, + oversize_probe: Option, + acquire_operation: Option, + attempt: Option, + report: Option, + failed: bool, + created: bool, + destroy_attempts: u32, + destroy_status: Option, + write_status: u32, +} +impl Outcome { + fn pending(mode: Mode) -> Self { + Self { + mode, + host_status: None, + recorder_result: None, + recorded_body: None, + oversize_probe: None, + acquire_operation: None, + attempt: None, + report: None, + failed: false, + created: false, + destroy_attempts: 0, + destroy_status: None, + write_status: 0, + } + } +} + +// This uses the original installed layout and genuine begin_entry issuer. The +// Property control deliberately supplies its real root, because purpose rejects +// before operation-frame validation. No operation activation is manufactured. +fn begin_host( + context: *const JettRuntimeContextV1, + mode: Mode, +) -> Result<(u64, u64, Option), ()> { + AuthenticatedResourceContext::acquire(context) + .and_then(|authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_mut() + .ok_or(NativeResourceError::MissingInstallation)?; + resource.validate_installation()?; + let operation = if matches!(mode, Mode::OversizedPropertyContext) { + None + } else { + let ordinal = resource + .layout + .operations() + .iter() + .enumerate() + .find_map(|(ordinal, row)| { + matches!(row.operation(), NativeOperation::Acquire { .. }) + .then_some(ordinal) + }) + .ok_or(NativeResourceError::WrongOperation)?; + Some(u32::try_from(ordinal).map_err(|_| NativeResourceError::Capacity)?) + }; + let attempt = resource.begin_entry( + &mut state.values, + &state.resources, + resource.entry, + mode.purpose(), + )?; + let root = resource.root_frame(attempt)?; + Ok((attempt.raw(), root.raw(), operation)) + }) + }) + .map_err(|_| ()) +} + +fn observed_body(context: *const JettRuntimeContextV1) -> Result { + AuthenticatedResourceContext::acquire(context) + .and_then(|authenticated| { + authenticated.with_state(|_, state| { + let resource = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)?; + let attempt = resource + .attempt + .as_ref() + .ok_or(NativeResourceError::InvalidEntry)?; + Ok(match &attempt.body { + None => RecordedBody::None, + Some(NativeBodyFailure::HostPanic) => RecordedBody::HostPanic, + Some(NativeBodyFailure::Resource(error)) => match error { + NativeResourceError::WrongPurpose => RecordedBody::WrongPurpose, + NativeResourceError::InvalidHandle => RecordedBody::InvalidHandle, + NativeResourceError::WrongOperation => RecordedBody::WrongOperation, + NativeResourceError::WrongFrame => RecordedBody::WrongFrame, + other => RecordedBody::OtherResource(other.status().code()), + }, + }) + }) + }) + .map_err(|_| ()) +} + +fn invoke_oversized( + context: *mut JettRuntimeContextV1, + root: u64, + outcome: &mut Outcome, +) -> Result<(), ()> { + let length = test_observations::MAX_CONTEXT.checked_add(1).ok_or(())?; + let mut oversized = Vec::new(); + oversized.try_reserve_exact(length).map_err(|_| ())?; + oversized.resize(length, b'x'); + let authenticated = AuthenticatedResourceContext::acquire(context).map_err(|_| ())?; + let (function, signature, template, before_length) = authenticated + .with_state(|_, state| { + let entry = state + .resource_state + .as_ref() + .ok_or(NativeResourceError::MissingInstallation)? + .entry; + state + .values + .test_observation_set_property_context(Some(oversized)); + let bytes = state + .values + .test_observation_property_context() + .ok_or(NativeResourceError::InvalidEntry)?; + let before_length = + u64::try_from(bytes.len()).map_err(|_| NativeResourceError::Capacity)?; + Ok((entry.function, entry.signature, entry.scope, before_length)) + }) + .map_err(|_| ())?; + // The real root header is valid. Oversize telemetry must refuse copying + // before any SourceCall lookup, without changing this successful leaf. + let status = unsafe { + leaves::jett_rt_v1_resource_scope_validate(context, root, function, signature, template) + }; + outcome.host_status = Some(status.code()); + let probe = authenticated + .with_state(|_, state| { + let bytes = state + .values + .test_observation_property_context() + .ok_or(NativeResourceError::InvalidEntry)?; + let after_length = + u64::try_from(bytes.len()).map_err(|_| NativeResourceError::Capacity)?; + let bytes_preserved = bytes.len() == length && bytes.iter().all(|byte| *byte == b'x'); + state.values.test_observation_set_property_context(None); + let cleared = state.values.test_observation_property_context().is_none(); + Ok(OversizeProbe { + before_length, + after_length, + bytes_preserved, + cleared, + }) + }) + .map_err(|_| ())?; + outcome.oversize_probe = Some(probe); + Ok(()) +} + +fn invoke_host( + context: *mut JettRuntimeContextV1, + mode: Mode, + root: u64, + acquire: u32, +) -> JettRuntimeStatusV1 { + let mut scalar = MaybeUninit::::uninit(); + let mut result = MaybeUninit::::uninit(); + // Each malformed output is rejected by the real leaf before any write. + // Other cases use valid exclusive output storage and a live stationary context. + unsafe { + match mode { + Mode::HookNullOutput => { + leaves::jett_rt_v1_resource_hook_prepare(context, root, acquire, 0, ptr::null_mut()) + } + Mode::HookContextOverlapOutput => { + leaves::jett_rt_v1_resource_hook_prepare(context, root, acquire, 0, context.cast()) + } + Mode::HookUnknownOperation => leaves::jett_rt_v1_resource_hook_prepare( + context, + root, + u32::MAX, + 0, + scalar.as_mut_ptr(), + ), + Mode::PropertyAcquireHook => leaves::jett_rt_v1_resource_hook_prepare( + context, + root, + acquire, + 0, + scalar.as_mut_ptr(), + ), + Mode::FactoryPrepareNullOutput => { + leaves::jett_rt_v1_resource_factory_prepare(context, 0, 0, ptr::null_mut()) + } + Mode::FactoryPrepareContextOverlapOutput => { + leaves::jett_rt_v1_resource_factory_prepare(context, 0, 0, context.cast()) + } + Mode::FactoryPrepareZeroCall => { + leaves::jett_rt_v1_resource_factory_prepare(context, 0, 0, scalar.as_mut_ptr()) + } + Mode::FactoryCommitNullOutput => { + leaves::jett_rt_v1_resource_factory_commit(context, 0, 0, 0, ptr::null_mut()) + } + Mode::FactoryCommitContextOverlapOutput => { + leaves::jett_rt_v1_resource_factory_commit(context, 0, 0, 0, context.cast()) + } + Mode::FactoryCommitZeroPrepared => { + leaves::jett_rt_v1_resource_factory_commit(context, 0, 0, 0, result.as_mut_ptr()) + } + Mode::RecorderOnly | Mode::OversizedPropertyContext => { + unreachable!("mode uses its distinct bounded control route") + } + } + } +} + +fn expected_host(outcome: &Outcome, report: &Report) -> bool { + if outcome.host_status != Some(outcome.mode.expected_status()) + || outcome.recorded_body != Some(outcome.mode.expected_body()) + || report.refused + || report.attempts.len() != 1 + || !report.final_counts.as_ref().is_some_and(Counts::clean) + || !report.events.as_ref().is_some_and(Vec::is_empty) + || report.remaining.is_none() + || !report.created + || report.destroy_attempts != 1 + || report.destroy_status != 0 + { + return false; + } + let attempt = &report.attempts[0]; + let Some(completion) = &attempt.completion else { + return false; + }; + let Some(snapshot) = &report.observations else { + return false; + }; + let Some(observation) = snapshot.attempts.first() else { + return false; + }; + let Some(end) = &observation.end else { + return false; + }; + let expected = outcome.mode.counters(); + let oversized = matches!(outcome.mode, Mode::OversizedPropertyContext); + let selected_kind = u32::from(!oversized); + let flags = if oversized { + test_observations::BYTE_LIMIT + } else { + 0 + }; + let context_ok = if oversized { + outcome.oversize_probe.as_ref().is_some_and(|probe| { + probe.before_length == test_observations::MAX_CONTEXT as u64 + 1 + && probe.after_length == probe.before_length + && probe.bytes_preserved + && probe.cleared + }) + } else { + outcome.oversize_probe.is_none() + }; + let history_ok = if oversized { + snapshot.history.len() == 3 + && matches!( + &snapshot.history[1].kind, + test_observations::HistoryKind::Clear + ) + } else { + snapshot.history.len() == 2 + }; + outcome.attempt == Some(completion.attempt) + && attempt.observed_body_status == outcome.host_status + && Some(completion.body_status) == outcome.host_status + && completion.cleanup_status == 0 + && completion.selected_kind == selected_kind + && attempt.counts.as_ref().is_some_and(Counts::clean) + && attempt.ordinary_status == Some(0) + && snapshot.flags == flags + && snapshot.counters.values == expected + && context_ok + && history_ok + && snapshot.attempts.len() == 1 + && observation.identity.id == completion.attempt + && observation.identity.ordinal == 0 + && observation.begin.counters.values == [0; 5] + && end.counters.values == expected + && matches!( + snapshot.history.first().map(|event| &event.kind), + Some(test_observations::HistoryKind::Begin) + ) + && matches!( + snapshot.history.last().map(|event| &event.kind), + Some(test_observations::HistoryKind::End) + ) +} + +fn run_host(mode: Mode) -> Outcome { + let mut outcome = Outcome::pending(mode); + let mut session = Session::new(); + let mut report = Report::default(); + let ran = contained(|| -> Result<(), ()> { + let (script, attempts) = script()?; + if attempts != 1 { + return Err(()); + } + session.create()?; + install(&mut session, &script)?; + report.attempts.try_reserve(1).map_err(|_| ())?; + let (id, root, acquire) = begin_host(session.pointer(), mode)?; + outcome.acquire_operation = acquire; + outcome.attempt = Some(id); + report.attempts.push(Attempt::pending(id)); + session.active = Some(ActiveAttempt { + id, + index: 0, + body_status: None, + panicked: 0, + completion_attempted: false, + }); + let status = if matches!(mode, Mode::OversizedPropertyContext) { + invoke_oversized(session.pointer(), root, &mut outcome)?; + outcome.host_status.ok_or(())? + } else { + invoke_host(session.pointer(), mode, root, acquire.ok_or(())?).code() + }; + outcome.host_status = Some(status); + // This completion body field is the actual host ABI leaf return, not an + // observed Source body. Only the outer host envelope gives it that origin. + report.attempts[0].observed_body_status = Some(status); + session.active.as_mut().ok_or(())?.body_status = Some(status); + outcome.recorded_body = Some(observed_body(session.pointer())?); + complete_active(&mut session, &mut report); + observe_attempt(&mut session, &mut report, 0); + Ok(()) + }); + if !matches!(ran, Ok(Ok(()))) { + outcome.failed = true; + } + // Preserve an already returned host status even if a subsequent diagnostic + // probe failed before the ordinary completion bookkeeping was reached. + if let (Some(status), Some(active)) = (outcome.host_status, session.active.as_mut()) { + if active.body_status.is_none() { + active.body_status = Some(status); + report.attempts[active.index].observed_body_status = Some(status); + } + } + if contained(|| observe_final(&mut session, &mut report)).is_err() { + report.refused = true; + outcome.failed = true; + } + session.destroy_once(); + report.created = session.created; + report.destroy_attempts = session.destroy_attempts; + report.destroy_status = session.destroy_status; + outcome.created = session.created; + outcome.destroy_attempts = session.destroy_attempts; + outcome.destroy_status = (session.destroy_attempts != 0).then_some(session.destroy_status); + if !expected_host(&outcome, &report) { + outcome.failed = true; + } + outcome.report = Some(report); + outcome +} + +struct Encoder(Vec); +impl Encoder { + fn bytes(&mut self, bytes: &[u8]) -> Result<(), ()> { + let length = self.0.len().checked_add(bytes.len()).ok_or(())?; + if length > MAX_CONTROL { + return Err(()); + } + self.0.try_reserve(bytes.len()).map_err(|_| ())?; + self.0.extend_from_slice(bytes); + Ok(()) + } + fn u32(&mut self, value: u32) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn u64(&mut self, value: u64) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn optional_u32(&mut self, value: Option) -> Result<(), ()> { + self.u32(u32::from(value.is_some()))?; + if let Some(value) = value { + self.u32(value)?; + } + Ok(()) + } + fn optional_u64(&mut self, value: Option) -> Result<(), ()> { + self.u32(u32::from(value.is_some()))?; + if let Some(value) = value { + self.u64(value)?; + } + Ok(()) + } +} + +fn encode(outcome: &Outcome) -> Result<(Vec, bool), ()> { + let pure = matches!(outcome.mode, Mode::RecorderOnly); + let nested = match &outcome.report { + Some(report) => report.encode().ok(), + None => None, + }; + if pure + && (outcome.host_status.is_some() + || outcome.recorded_body.is_some() + || outcome.oversize_probe.is_some() + || outcome.acquire_operation.is_some() + || outcome.attempt.is_some() + || outcome.report.is_some() + || outcome.created + || outcome.destroy_attempts != 0 + || outcome.destroy_status.is_some()) + { + return Err(()); + } + let failed = outcome.failed + || (!pure && nested.is_none()) + || (pure && outcome.recorder_result != Some(true)); + let mut output = Encoder(Vec::new()); + output.bytes(b"JTRHC002")?; + output.u32(2)?; + output.u32(0)?; + output.u64(0)?; + output.u32(outcome.mode as u32)?; + output.u32(if pure { 2 } else { 1 })?; // Pure recorder / host ABI leaf. + output.u32(u32::from(failed))?; + output.u32(outcome.write_status)?; + if pure { + match outcome.recorder_result { + Some(true) => output.u32(2)?, + Some(false) => output.u32(3)?, + None => output.u32(0)?, + } + } else if let Some(status) = outcome.host_status { + output.u32(1)?; + output.u32(status)?; + } else { + output.u32(0)?; + } + match outcome.recorded_body { + None => output.u32(0)?, // Unavailable; distinct from an observed empty body error. + Some(RecordedBody::None) => output.u32(1)?, + Some(RecordedBody::WrongPurpose) => output.u32(2)?, + Some(RecordedBody::InvalidHandle) => output.u32(3)?, + Some(RecordedBody::WrongOperation) => output.u32(4)?, + Some(RecordedBody::WrongFrame) => output.u32(5)?, + Some(RecordedBody::OtherResource(status)) => { + output.u32(6)?; + output.u32(status)?; + } + Some(RecordedBody::HostPanic) => output.u32(7)?, + } + output.u32(u32::from(outcome.oversize_probe.is_some()))?; + if let Some(probe) = &outcome.oversize_probe { + output.u64(probe.before_length)?; + output.u64(probe.after_length)?; + output.u32(u32::from(probe.bytes_preserved))?; + output.u32(u32::from(probe.cleared))?; + } + output.optional_u32(outcome.acquire_operation)?; + output.optional_u64(outcome.attempt)?; + if outcome.attempt.is_some() { + output.u32(if matches!(outcome.mode, Mode::PropertyAcquireHook) { + 2 + } else { + 1 + })?; + } + output.u32(u32::from(outcome.created))?; + output.u32(outcome.destroy_attempts)?; + output.optional_u32(outcome.destroy_status)?; + output.u32(u32::from(nested.is_some()))?; + if let Some(nested) = nested { + output.u64(nested.len() as u64)?; + output.bytes(&nested)?; + } + let length = output.0.len() as u64; + output.0[16..24].copy_from_slice(&length.to_le_bytes()); + Ok((output.0, failed)) +} + +fn remove_previous() -> Result<(), ()> { + for file in [CONTROL_FILE, SOURCE_REPORT_FILE] { + match std::fs::remove_file(file) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(_) => return Err(()), + } + } + Ok(()) +} +fn publish(mut outcome: Outcome) -> Result { + let (bytes, failed) = encode(&outcome)?; + if std::fs::write(CONTROL_FILE, bytes).is_ok() { + return Ok(if failed { EXIT_INFRASTRUCTURE } else { 0 }); + } + outcome.failed = true; + outcome.write_status = JettRuntimeStatusV1::IO_FAILURE.code(); + if let Some(report) = &mut outcome.report { + report.write_status = outcome.write_status; + } + if let Ok((bytes, _)) = encode(&outcome) { + let _ = std::fs::write(CONTROL_FILE, bytes); + } + Ok(EXIT_INFRASTRUCTURE) +} + +pub(super) fn dispatch() -> Result, ()> { + let Some(requested) = std::env::var_os(MODE_ENV) else { + return Ok(None); + }; + contained(|| { + // An unknown mode cannot leave an earlier successful control or Source report. + remove_previous()?; + let mode = Mode::parse(requested.to_str().ok_or(())?)?; + std::panic::set_hook(Box::new(|_| {})); + let outcome = if matches!(mode, Mode::RecorderOnly) { + let mut outcome = Outcome::pending(mode); + match contained(|| { + test_observations::recorder_controls().and_then(|()| report::writer_controls()) + }) { + Ok(Ok(())) => outcome.recorder_result = Some(true), + Ok(Err(())) => { + outcome.recorder_result = Some(false); + outcome.failed = true; + } + Err(()) => outcome.failed = true, + } + outcome + } else { + run_host(mode) + }; + publish(outcome).map(Some) + })? +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs b/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs new file mode 100644 index 00000000..412614a5 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_archive_main/report.rs @@ -0,0 +1,1059 @@ +//! Canonical owned report transport for the private compiler-test main. +//! These records contain observations, never registry keys or custody tokens. +use super::super::test_observations::{ + AttemptIdentity, Boundary, CallIdentity, Channel, Counters as ObservationCounters, HistoryKind, + KNOWN_FLAGS, MAX_CONTEXT, MAX_CONTEXT_BYTES, MAX_HISTORY, MAX_PARAMETERS, ParameterValue, + Snapshot, +}; +use super::{Completion, Counts, Event}; + +pub(super) const MAX_ATTEMPTS: usize = 64; +pub(super) const MAX_EVENTS: usize = 8192; +pub(super) const MAX_MESSAGE: usize = 65_536; +const MAX_REPORT: usize = 16 * 1024 * 1024; + +pub(super) struct Attempt { + pub(super) id: u64, + pub(super) observed_body_status: Option, + pub(super) completion: Option, + pub(super) counts: Option, + pub(super) ordinary_status: Option, + pub(super) ordinary_message: Option>, + pub(super) resource_message: Option>, + pub(super) event_sequence_end: Option, +} +impl Attempt { + pub(super) fn pending(id: u64) -> Self { + Self { + id, + observed_body_status: None, + completion: None, + counts: None, + ordinary_status: None, + ordinary_message: None, + resource_message: None, + event_sequence_end: None, + } + } + pub(super) fn observations_complete(&self) -> bool { + self.observed_body_status.is_some() + && self.completion.is_some() + && self.counts.is_some() + && self.ordinary_status.is_some() + && self.ordinary_message.is_some() + && self.resource_message.is_some() + && self.event_sequence_end.is_some() + } +} + +#[derive(Default)] +pub(super) struct Report { + pub(super) observations: Option, + pub(super) attempts: Vec, + pub(super) events: Option>, + pub(super) remaining: Option, + pub(super) final_counts: Option, + pub(super) final_ordinary_status: Option, + pub(super) final_ordinary_message: Option>, + pub(super) refused: bool, + pub(super) created: bool, + pub(super) destroy_attempts: u32, + pub(super) destroy_status: u32, + pub(super) write_status: u32, +} + +struct Encoder(Vec); +impl Encoder { + fn bytes(&mut self, value: &[u8]) -> Result<(), ()> { + let length = self.0.len().checked_add(value.len()).ok_or(())?; + if length > MAX_REPORT { + return Err(()); + } + self.0.try_reserve(value.len()).map_err(|_| ())?; + self.0.extend_from_slice(value); + Ok(()) + } + fn u32(&mut self, value: u32) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn u64(&mut self, value: u64) -> Result<(), ()> { + self.bytes(&value.to_le_bytes()) + } + fn completion(&mut self, value: &Completion) -> Result<(), ()> { + if value.attempt == 0 || value.reserved != 0 || value.selected_kind > 3 { + return Err(()); + } + self.u64(value.attempt)?; + for field in [ + value.body_status, + value.cleanup_status, + value.selected_kind, + 0, + ] { + self.u32(field)?; + } + Ok(()) + } + fn counts(&mut self, value: &Counts) -> Result<(), ()> { + if value.reserved != 0 || value.ordinary_empty > 1 { + return Err(()); + } + for field in [ + value.owners, + value.loans, + value.frames, + value.registry_entries, + value.owner_handles, + value.loan_handles, + value.frame_handles, + value.provisional_returns, + ] { + self.u64(field)?; + } + self.u32(value.ordinary_empty)?; + self.u32(0) + } + fn attempt(&mut self, value: &Attempt) -> Result<(), ()> { + if !value.observations_complete() { + return Err(()); + } + let completion = value.completion.as_ref().ok_or(())?; + let counts = value.counts.as_ref().ok_or(())?; + let ordinary = value.ordinary_message.as_ref().ok_or(())?; + let resource = value.resource_message.as_ref().ok_or(())?; + let end = value.event_sequence_end.ok_or(())?; + if value.id != completion.attempt + || value.observed_body_status != Some(completion.body_status) + || ordinary.len() > MAX_MESSAGE + || resource.len() > MAX_MESSAGE + { + return Err(()); + } + self.completion(completion)?; + self.counts(counts)?; + self.u32(ordinary.len() as u32)?; + self.u32(resource.len() as u32)?; + self.u64(end)?; + self.bytes(ordinary)?; + self.bytes(resource) + } +} + +fn counters_follow(now: ObservationCounters, previous: ObservationCounters) -> bool { + now.values[1] <= now.values[0] + && now + .values + .iter() + .zip(previous.values) + .all(|(now, before)| *now >= before) +} + +fn validate_observations(snapshot: &Snapshot, attempts: &[Attempt]) -> Result<(), ()> { + let bad = || (); + let zero = ObservationCounters { values: [0; 5] }; + let identity_known = |identity: AttemptIdentity| { + attempts + .get(identity.ordinal as usize) + .is_some_and(|attempt| attempt.id == identity.id) + }; + let mut previous = zero; + let mut previous_ordinal = None; + let mut boundary_sequence = 0; + for attempt in &snapshot.attempts { + if !identity_known(attempt.identity) + || previous_ordinal.is_some_and(|ordinal| attempt.identity.ordinal <= ordinal) + || !counters_follow(attempt.begin.counters, previous) + { + return Err(bad()); + } + previous_ordinal = Some(attempt.identity.ordinal); + previous = attempt.begin.counters; + for (boundary, beginning) in [(Some(attempt.begin), true), (attempt.end, false)] { + let Some(boundary) = boundary else { continue }; + if !counters_follow(boundary.counters, previous) { + return Err(bad()); + } + previous = boundary.counters; + if let Some(sequence) = boundary.sequence { + if sequence <= boundary_sequence { + return Err(bad()); + } + let event = snapshot + .history + .get(usize::try_from(sequence - 1).map_err(|_| bad())?) + .ok_or_else(bad)?; + if event.sequence != sequence + || event.channel != Channel::Attempt(attempt.identity) + || if beginning { + !matches!(event.kind, HistoryKind::Begin) + } else { + !matches!(event.kind, HistoryKind::End) + } + { + return Err(bad()); + } + boundary_sequence = sequence; + } + } + } + if !counters_follow(snapshot.counters, previous) { + return Err(bad()); + } + let mut active = None; + let mut current_context: Option<&[u8]> = None; + let mut call_stack = Vec::new(); + call_stack + .try_reserve_exact(snapshot.history.len()) + .map_err(|_| ())?; + for (index, event) in snapshot.history.iter().enumerate() { + if event.sequence != u64::try_from(index).map_err(|_| bad())? + 1 { + return Err(bad()); + } + if let Channel::Attempt(identity) = event.channel { + if !identity_known(identity) { + return Err(bad()); + } + if snapshot.flags == 0 && active.is_some_and(|current| current != identity) { + return Err(bad()); + } + } + match &event.kind { + HistoryKind::Begin => { + let Channel::Attempt(identity) = event.channel else { + return Err(bad()); + }; + if (snapshot.flags == 0 && active.is_some()) || event.context.is_some() { + return Err(bad()); + } + active = Some(identity); + current_context = None; + if snapshot.flags == 0 + && !snapshot.attempts.iter().any(|attempt| { + attempt.identity == identity + && attempt.begin.sequence == Some(event.sequence) + }) + { + return Err(bad()); + } + } + HistoryKind::End => { + let Channel::Attempt(identity) = event.channel else { + return Err(bad()); + }; + if snapshot.flags == 0 && active != Some(identity) { + return Err(bad()); + } + if snapshot.flags == 0 + && !snapshot.attempts.iter().any(|attempt| { + attempt.identity == identity + && attempt + .end + .is_some_and(|end| end.sequence == Some(event.sequence)) + }) + { + return Err(bad()); + } + if snapshot.flags == 0 && !call_stack.is_empty() { + return Err(bad()); + } + if snapshot.flags == 0 && event.context.as_deref() != current_context { + return Err(bad()); + } + active = None; + current_context = None; + } + HistoryKind::Set if event.context.is_none() => return Err(bad()), + HistoryKind::Clear if event.context.is_some() => return Err(bad()), + HistoryKind::CallEnter { call, parameters } => { + if !matches!(event.channel, Channel::Attempt(_)) + || event.context.is_none() + || call.call == 0 + || call.scope == 0 + { + return Err(bad()); + } + if snapshot.flags == 0 { + if event.context.as_deref() != current_context { + return Err(bad()); + } + call_stack.push(event.sequence); + } + if snapshot.history[..index].iter().any(|prior| matches!(&prior.kind, HistoryKind::CallEnter { call: old, .. } if old.call == call.call || old.scope == call.scope)) { return Err(bad()); } + if parameters.iter().enumerate().any(|(index, parameter)| { + parameters[..index] + .iter() + .any(|prior| prior.parameter == parameter.parameter) + }) { + return Err(bad()); + } + } + HistoryKind::CallComplete { + call, + entered_sequence, + body_status, + failure_status, + } => { + if *failure_status == Some(0) || (failure_status.is_none() && *body_status != 0) { + return Err(bad()); + } + if snapshot.flags == 0 && event.context.as_deref() != current_context { + return Err(bad()); + } + if snapshot.flags == 0 && call_stack.pop() != *entered_sequence { + return Err(bad()); + } + if let Some(sequence) = entered_sequence { + if *sequence == 0 || *sequence >= event.sequence { + return Err(bad()); + } + let entered = snapshot + .history + .get(usize::try_from(sequence - 1).map_err(|_| bad())?) + .ok_or_else(bad)?; + if entered.channel != event.channel + || !matches!(&entered.kind, HistoryKind::CallEnter { call: original, .. } if original == call) + { + return Err(bad()); + } + if snapshot.history[..index].iter().any(|prior| matches!(&prior.kind, HistoryKind::CallComplete { entered_sequence: Some(old), .. } if old == sequence)) { return Err(bad()); } + } else if snapshot.flags == 0 { + return Err(bad()); + } + } + HistoryKind::Set | HistoryKind::Clear => { + if snapshot.flags == 0 && event.channel == Channel::Session && active.is_some() { + return Err(bad()); + } + if matches!(event.channel, Channel::Attempt(_)) { + current_context = event.context.as_deref(); + } + } + } + if snapshot.flags == 0 + && matches!(event.channel, Channel::Attempt(_)) + && active.is_none() + && !matches!(event.kind, HistoryKind::End) + { + return Err(bad()); + } + } + + if snapshot.flags == 0 && snapshot.history.iter().any(|event| matches!(event.kind, HistoryKind::CallEnter { .. }) + && !snapshot.history.iter().any(|completed| matches!(&completed.kind, HistoryKind::CallComplete { entered_sequence: Some(sequence), .. } if *sequence == event.sequence))) { + return Err(bad()); + } + if snapshot.flags == 0 + && (active.is_some() + || !(snapshot.attempts.len() == attempts.len() + && snapshot.attempts.iter().all(|attempt| { + attempt.begin.sequence.is_some() + && attempt.end.is_some_and(|end| end.sequence.is_some()) + }) + && snapshot.history.iter().all(|event| { + !matches!( + event.kind, + HistoryKind::CallComplete { + entered_sequence: None, + .. + } + ) + }))) + { + return Err(bad()); + } + Ok(()) +} + +impl Encoder { + fn optional_u64(&mut self, value: Option) -> Result<(), ()> { + self.u32(u32::from(value.is_some()))?; + if let Some(value) = value { + self.u64(value)?; + } + Ok(()) + } + fn optional_u32(&mut self, value: Option) -> Result<(), ()> { + self.u32(u32::from(value.is_some()))?; + if let Some(value) = value { + self.u32(value)?; + } + Ok(()) + } + fn observation_counters(&mut self, counters: ObservationCounters) -> Result<(), ()> { + for value in counters.values { + self.u64(value)?; + } + Ok(()) + } + fn observation_boundary(&mut self, boundary: Boundary) -> Result<(), ()> { + self.optional_u64(boundary.sequence)?; + self.observation_counters(boundary.counters) + } + fn observation_identity(&mut self, identity: AttemptIdentity) -> Result<(), ()> { + self.u64(identity.id)?; + self.u32(identity.ordinal) + } + fn call_identity(&mut self, call: CallIdentity) -> Result<(), ()> { + self.u64(call.call)?; + self.u64(call.scope)?; + for value in [call.function, call.signature, call.template, call.operation] { + self.u32(value)?; + } + Ok(()) + } + fn observations(&mut self, snapshot: &Snapshot, attempts: &[Attempt]) -> Result<(), ()> { + if snapshot.flags & !KNOWN_FLAGS != 0 || snapshot.attempts.len() > MAX_ATTEMPTS || snapshot.history.len() > MAX_HISTORY + || snapshot.history.iter().any(|event| matches!(&event.kind, HistoryKind::CallEnter { parameters, .. } if parameters.len() > MAX_PARAMETERS)) { return Err(()); } + validate_observations(snapshot, attempts)?; + let mut context_bytes = 0u64; + for event in &snapshot.history { + let length = event.context.as_ref().map_or(0, Vec::len); + if length > MAX_CONTEXT { + return Err(()); + } + context_bytes = context_bytes + .checked_add(u64::try_from(length).map_err(|_| ())?) + .ok_or(())?; + } + if context_bytes > MAX_CONTEXT_BYTES as u64 { + return Err(()); + } + self.observation_counters(snapshot.counters)?; + self.u32(snapshot.flags)?; + self.u32(u32::try_from(snapshot.attempts.len()).map_err(|_| ())?)?; + self.u32(u32::try_from(snapshot.history.len()).map_err(|_| ())?)?; + self.u64(context_bytes)?; + self.u64(u64::try_from(snapshot.history.len()).map_err(|_| ())?)?; + for attempt in &snapshot.attempts { + self.observation_identity(attempt.identity)?; + self.observation_boundary(attempt.begin)?; + self.u32(u32::from(attempt.end.is_some()))?; + if let Some(end) = attempt.end { + self.observation_boundary(end)?; + } + } + for event in &snapshot.history { + self.u64(event.sequence)?; + self.u32(match event.kind { + HistoryKind::Begin => 1, + HistoryKind::Set => 2, + HistoryKind::CallEnter { .. } => 3, + HistoryKind::CallComplete { .. } => 4, + HistoryKind::Clear => 5, + HistoryKind::End => 6, + })?; + match event.channel { + Channel::Session => self.u32(0)?, + Channel::Attempt(identity) => { + self.u32(1)?; + self.observation_identity(identity)?; + } + } + self.u32(u32::from(event.context.is_some()))?; + if let Some(context) = &event.context { + self.u32(u32::try_from(context.len()).map_err(|_| ())?)?; + self.bytes(context)?; + } + match &event.kind { + HistoryKind::CallEnter { call, parameters } => { + if parameters.len() > MAX_PARAMETERS { + return Err(()); + } + self.call_identity(*call)?; + self.u32(u32::try_from(parameters.len()).map_err(|_| ())?)?; + for parameter in parameters { + self.u32(parameter.parameter)?; + self.u32(parameter.shape)?; + match parameter.value { + ParameterValue::Unobserved => self.u32(0)?, + ParameterValue::Int64 { bits } => { + self.u32(1)?; + self.u64(bits)?; + } + } + } + } + HistoryKind::CallComplete { + call, + entered_sequence, + body_status, + failure_status, + } => { + self.call_identity(*call)?; + self.optional_u64(*entered_sequence)?; + self.u32(*body_status)?; + self.optional_u32(*failure_status)?; + } + HistoryKind::Begin | HistoryKind::End | HistoryKind::Set | HistoryKind::Clear => {} + } + } + Ok(()) + } +} + +impl Report { + pub(super) fn encode(&self) -> Result, ()> { + if self.refused + || self.attempts.len() > MAX_ATTEMPTS + || self.events.as_ref().ok_or(())?.len() > MAX_EVENTS + || self.remaining.is_none() + || self.final_counts.is_none() + || self.final_ordinary_status.is_none() + || self.final_ordinary_message.is_none() + || self.destroy_attempts > 1 + || (self.created && self.destroy_attempts != 1) + || self + .final_counts + .as_ref() + .is_some_and(|counts| counts.reserved != 0 || counts.ordinary_empty > 1) + || (!self.created && self.destroy_attempts != 0) + { + return Err(()); + } + let events = self.events.as_ref().ok_or(())?; + let mut output = Encoder(Vec::new()); + output.bytes(b"JTRRP002")?; + output.u32(2)?; + output.u32(0)?; + output.u64(0)?; + output.u32(self.attempts.len() as u32)?; + output.u32(events.len() as u32)?; + output.u64(self.remaining.ok_or(())?)?; + let mut last_attempt = 0; + let mut last_end = 0; + for attempt in &self.attempts { + let completion = attempt.completion.as_ref().ok_or(())?; + let end = attempt.event_sequence_end.ok_or(())?; + if completion.attempt <= last_attempt || end < last_end { + return Err(()); + } + output.attempt(attempt)?; + last_attempt = completion.attempt; + last_end = end; + } + let mut last_sequence = 0; + for event in events { + if event.sequence <= last_sequence + || event.reserved != 0 + || !(1..=5).contains(&event.kind) + { + return Err(()); + } + output.u64(event.sequence)?; + output.u64(event.label as u64)?; + output.u32(event.kind)?; + output.u32(0)?; + last_sequence = event.sequence; + } + if last_end > last_sequence { + return Err(()); + } + output.observations(self.observations.as_ref().ok_or(())?, &self.attempts)?; + for field in [ + self.destroy_status, + u32::from(self.created), + self.destroy_attempts, + self.write_status, + ] { + output.u32(field)?; + } + let length = output.0.len() as u64; + output.0[16..24].copy_from_slice(&length.to_le_bytes()); + Ok(output.0) + } +} + +// Runs through the actual private writer without acquiring a Runtime context or Source authority. +pub(super) fn writer_controls() -> Result<(), ()> { + let baseline = writer_control_report(); + let bytes = baseline.encode()?; + if bytes.get(..8) != Some(&b"JTRRP002"[..]) + || bytes.get(8..12) != Some(&2u32.to_le_bytes()[..]) + || bytes.get(16..24) != Some(&u64::try_from(bytes.len()).map_err(|_| ())?.to_le_bytes()[..]) + { + return Err(()); + } + for mutation in 0..25 { + let mut changed = writer_control_report(); + changed.encode()?; + match mutation { + 0 => changed.observations = None, + 1 => changed.attempts[0].completion.as_mut().ok_or(())?.reserved = 1, + 2 => changed.attempts[0].counts.as_mut().ok_or(())?.reserved = 1, + 3 => changed.attempts[0].observed_body_status = None, + 4 => changed.refused = true, + 5 => changed.final_counts.as_mut().ok_or(())?.ordinary_empty = 2, + 6 => changed.attempts[0].completion.as_mut().ok_or(())?.attempt = 42, + 7..=24 => { + let snapshot = changed.observations.as_mut().ok_or(())?; + match mutation { + 7 => snapshot.flags = KNOWN_FLAGS + 1, + 8 => snapshot.counters.values[1] = 4, + 9 => snapshot.counters.values[0] = 2, + 10 => snapshot.attempts[0].identity.id = 42, + 11 => snapshot.attempts[0].identity.ordinal = 1, + 12 => snapshot.history[2].sequence = 2, + 13 => { + if let HistoryKind::CallComplete { failure_status, .. } = + &mut snapshot.history[4].kind + { + *failure_status = Some(0); + } else { + return Err(()); + } + } + 14 => { + if let HistoryKind::CallComplete { body_status, .. } = + &mut snapshot.history[4].kind + { + *body_status = 71; + } else { + return Err(()); + } + } + 15 => { + if let HistoryKind::CallComplete { + entered_sequence, .. + } = &mut snapshot.history[4].kind + { + *entered_sequence = None; + } else { + return Err(()); + } + } + 16 => { + if let HistoryKind::CallComplete { call, .. } = + &mut snapshot.history[4].kind + { + call.scope += 1; + } else { + return Err(()); + } + } + 17 => snapshot.history[4].context = Some(b"wrong".to_vec()), + 18 => snapshot.history[7].context = Some(Vec::new()), + 19 => { + snapshot.history[1].kind = HistoryKind::Clear; + snapshot.history[1].context = None; + } + 20 => snapshot.history[1].channel = Channel::Session, + 21 => { + let child = + std::mem::replace(&mut snapshot.history[4].kind, HistoryKind::Clear); + let parent = std::mem::replace(&mut snapshot.history[5].kind, child); + snapshot.history[4].kind = parent; + } + 22 => snapshot.history[5].kind = HistoryKind::Set, + 23 => { + let HistoryKind::CallEnter { call: parent, .. } = &snapshot.history[2].kind + else { + return Err(()); + }; + let parent = *parent; + let HistoryKind::CallEnter { call, .. } = &mut snapshot.history[3].kind + else { + return Err(()); + }; + *call = parent; + } + 24 => { + let HistoryKind::CallEnter { parameters, .. } = + &mut snapshot.history[2].kind + else { + return Err(()); + }; + let parameter = *parameters.first().ok_or(())?; + parameters.push(parameter); + } + _ => return Err(()), + } + } + _ => return Err(()), + } + if changed.encode().is_ok() { + return Err(()); + } + } + // Completion records current bytes, so a real intervening Clear must remain valid. + let mut cleared = writer_control_report(); + let snapshot = cleared.observations.as_mut().ok_or(())?; + let clear = snapshot.history.remove(6); + snapshot.history.insert(4, clear); + for (index, event) in snapshot.history.iter_mut().enumerate() { + event.sequence = u64::try_from(index).map_err(|_| ())? + 1; + } + snapshot.history[5].context = None; + snapshot.history[6].context = None; + cleared.encode()?; + Ok(()) +} + +fn writer_control_report() -> Report { + use super::super::test_observations::{AttemptObservation, HistoryEvent, Parameter}; + let counts = || Counts { + owners: 0, + loans: 0, + frames: 0, + registry_entries: 0, + owner_handles: 0, + loan_handles: 0, + frame_handles: 0, + provisional_returns: 0, + ordinary_empty: 1, + reserved: 0, + }; + let identity = AttemptIdentity { id: 41, ordinal: 0 }; + let zero = ObservationCounters { values: [0; 5] }; + let counters = ObservationCounters { + values: [3, 1, 1, 1, 1], + }; + let parent = CallIdentity { + call: 11, + scope: 12, + function: 7, + signature: 8, + template: 9, + operation: 10, + }; + let child = CallIdentity { + call: 21, + scope: 22, + function: 17, + signature: 18, + template: 19, + operation: 20, + }; + let history = [ + (None, HistoryKind::Begin), + (Some(b"trial".to_vec()), HistoryKind::Set), + ( + Some(b"trial".to_vec()), + HistoryKind::CallEnter { + call: parent, + parameters: vec![Parameter { + parameter: 0, + shape: 1, + value: ParameterValue::Int64 { bits: 42 }, + }], + }, + ), + ( + Some(b"trial".to_vec()), + HistoryKind::CallEnter { + call: child, + parameters: vec![Parameter { + parameter: 0, + shape: 2, + value: ParameterValue::Unobserved, + }], + }, + ), + ( + Some(b"trial".to_vec()), + HistoryKind::CallComplete { + call: child, + entered_sequence: Some(4), + body_status: 0, + failure_status: None, + }, + ), + ( + Some(b"trial".to_vec()), + HistoryKind::CallComplete { + call: parent, + entered_sequence: Some(3), + body_status: 0, + failure_status: None, + }, + ), + (None, HistoryKind::Clear), + (None, HistoryKind::End), + ] + .into_iter() + .zip(1u64..) + .map(|((context, kind), sequence)| HistoryEvent { + sequence, + channel: Channel::Attempt(identity), + context, + kind, + }) + .collect(); + let observations = Snapshot { + counters, + flags: 0, + attempts: vec![AttemptObservation { + identity, + begin: Boundary { + sequence: Some(1), + counters: zero, + }, + end: Some(Boundary { + sequence: Some(8), + counters, + }), + }], + history, + }; + let mut attempt = Attempt::pending(41); + attempt.observed_body_status = Some(0); + attempt.completion = Some(Completion { + attempt: 41, + body_status: 0, + cleanup_status: 0, + selected_kind: 0, + reserved: 0, + }); + attempt.counts = Some(counts()); + attempt.ordinary_status = Some(0); + attempt.ordinary_message = Some(Vec::new()); + attempt.resource_message = Some(Vec::new()); + attempt.event_sequence_end = Some(0); + Report { + observations: Some(observations), + attempts: vec![attempt], + events: Some(Vec::new()), + remaining: Some(2), + final_counts: Some(counts()), + final_ordinary_status: Some(0), + final_ordinary_message: Some(Vec::new()), + refused: false, + created: true, + destroy_attempts: 1, + destroy_status: 0, + write_status: 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn counts() -> Counts { + Counts { + owners: 0, + loans: 0, + frames: 0, + registry_entries: 0, + owner_handles: 0, + loan_handles: 0, + frame_handles: 0, + provisional_returns: 0, + ordinary_empty: 1, + reserved: 0, + } + } + + fn observations() -> Snapshot { + use super::super::super::test_observations::{AttemptObservation, HistoryEvent, Parameter}; + let identity = AttemptIdentity { id: 41, ordinal: 0 }; + let counters = ObservationCounters { values: [0; 5] }; + let call = CallIdentity { + call: 11, + scope: 12, + function: 7, + signature: 8, + template: 9, + operation: 10, + }; + Snapshot { + counters, + flags: 0, + attempts: vec![AttemptObservation { + identity, + begin: Boundary { + sequence: Some(1), + counters, + }, + end: Some(Boundary { + sequence: Some(6), + counters, + }), + }], + history: [ + (None, HistoryKind::Begin), + (Some(Vec::new()), HistoryKind::Set), + ( + Some(Vec::new()), + HistoryKind::CallEnter { + call, + parameters: vec![Parameter { + parameter: 0, + shape: 1, + value: ParameterValue::Int64 { bits: 42 }, + }], + }, + ), + ( + Some(Vec::new()), + HistoryKind::CallComplete { + call, + entered_sequence: Some(3), + body_status: 0, + failure_status: None, + }, + ), + (None, HistoryKind::Clear), + (None, HistoryKind::End), + ] + .into_iter() + .enumerate() + .map(|(index, (context, kind))| HistoryEvent { + sequence: index as u64 + 1, + channel: Channel::Attempt(identity), + context, + kind, + }) + .collect(), + } + } + + fn report() -> Report { + let mut attempt = Attempt::pending(41); + attempt.observed_body_status = Some(71); + attempt.completion = Some(Completion { + attempt: 41, + body_status: 71, + cleanup_status: 0, + selected_kind: 1, + reserved: 0, + }); + attempt.counts = Some(counts()); + attempt.ordinary_status = Some(0); + attempt.ordinary_message = Some(Vec::new()); + attempt.resource_message = Some(Vec::new()); + attempt.event_sequence_end = Some(0); + let baseline = Report { + observations: Some(observations()), + attempts: vec![attempt], + events: Some(Vec::new()), + remaining: Some(0), + final_counts: Some(counts()), + final_ordinary_status: Some(0), + final_ordinary_message: Some(Vec::new()), + refused: false, + created: true, + destroy_attempts: 1, + destroy_status: 0, + write_status: 0, + }; + baseline + .encode() + .expect("valid uncorrupted v2 baseline before any mutation"); + baseline + } + #[test] + fn private_report_v2_keeps_original_status_and_existing_binary_offsets() { + let bytes = report().encode().unwrap(); + assert_eq!(bytes.len(), 668); + assert_eq!(&bytes[..8], b"JTRRP002"); + assert_eq!(&bytes[8..12], &2u32.to_le_bytes()); + assert_eq!(&bytes[40..48], &41u64.to_le_bytes()); + assert_eq!(&bytes[48..52], &71u32.to_le_bytes()); + assert_eq!(&bytes[56..60], &1u32.to_le_bytes()); + } + #[test] + fn private_report_completed_prefix_survives_a_late_unknown_copy_without_encoding_success() { + let mut value = report(); + value.attempts[0].resource_message = None; + assert!(value.encode().is_err()); + let completed = value.attempts[0].completion.as_ref().unwrap(); + assert_eq!((completed.attempt, completed.body_status), (41, 71)); + assert!(value.attempts[0].counts.as_ref().unwrap().clean()); + assert_eq!(value.attempts[0].ordinary_message.as_deref(), Some(&[][..])); + } + #[test] + fn private_report_unknown_observation_differs_from_a_known_empty_carrier() { + for field in 0..6 { + let mut value = report(); + match field { + 0 => value.events = None, + 1 => value.remaining = None, + 2 => value.attempts[0].counts = None, + 3 => value.attempts[0].ordinary_status = None, + 4 => value.final_ordinary_message = None, + 5 => value.final_counts = None, + _ => unreachable!(), + } + assert!(value.encode().is_err(), "unknown observation {field}"); + } + assert!(report().encode().is_ok()); + } + #[test] + fn private_report_never_forges_original_body_or_exactly_once_teardown() { + let mut value = report(); + value.attempts[0].completion.as_mut().unwrap().body_status = 17; + assert!(value.encode().is_err()); + for attempts in [0, 2] { + let mut value = report(); + value.destroy_attempts = attempts; + assert!(value.encode().is_err()); + } + let mut value = report(); + value.refused = true; + assert!(value.encode().is_err()); + } + #[test] + fn private_report_v2_refuses_missing_or_invalid_snapshot_after_valid_baseline() { + report().encode().unwrap(); + let mut missing = report(); + missing.observations = None; + assert!(missing.encode().is_err()); + for mutation in 0..12 { + let mut value = report(); + let snapshot = value.observations.as_mut().unwrap(); + match mutation { + 0 => snapshot.flags = KNOWN_FLAGS + 1, + 1 => snapshot.counters.values[1] = 1, + 2 => snapshot.attempts[0].identity.id = 42, + 3 => snapshot.attempts[0].end.as_mut().unwrap().sequence = Some(1), + 4 => snapshot.history[1].sequence = 1, + 5 => { + if let HistoryKind::CallComplete { call, .. } = &mut snapshot.history[3].kind { + call.scope += 1; + } + } + 6 => { + if let HistoryKind::CallComplete { + entered_sequence, .. + } = &mut snapshot.history[3].kind + { + *entered_sequence = Some(4); + } + } + 7 => snapshot.history[3].kind = HistoryKind::Set, + 8 => { + if let HistoryKind::CallComplete { failure_status, .. } = + &mut snapshot.history[3].kind + { + *failure_status = Some(0); + } + } + 9 => { + if let HistoryKind::CallComplete { body_status, .. } = + &mut snapshot.history[3].kind + { + *body_status = 71; + } + } + 10 => snapshot.history[3].context = None, + 11 => snapshot.history[5].context = Some(vec![]), + _ => unreachable!(), + } + assert!(value.encode().is_err(), "snapshot mutation {mutation}"); + } + } + #[test] + fn private_report_v2_encodes_explicit_incomplete_boundaries_and_empty_context() { + report().encode().unwrap(); + let mut value = report(); + let snapshot = value.observations.as_mut().unwrap(); + snapshot.flags = 8; + snapshot.attempts[0].begin.sequence = None; + assert_eq!(snapshot.history[1].context.as_deref(), Some(&[][..])); + assert_eq!(snapshot.history[4].context, None); + let bytes = value.encode().unwrap(); + assert_eq!(&bytes[..8], b"JTRRP002"); + let mut missing = report(); + missing.observations = None; + assert!(missing.encode().is_err()); + } +} diff --git a/crates/jett_runtime/src/native_abi/resource/test_observations.rs b/crates/jett_runtime/src/native_abi/resource/test_observations.rs new file mode 100644 index 00000000..e600a138 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/test_observations.rs @@ -0,0 +1,580 @@ +//! Observations only: no Source, provider, value or custody authority. +#![cfg(all(test, jett_resource_native_test_archive))] +use super::*; + +pub(in crate::native_abi) const MAX_HISTORY: usize = 8192; +pub(in crate::native_abi) const MAX_CONTEXT: usize = 65_536; +pub(in crate::native_abi) const MAX_CONTEXT_BYTES: usize = 1_048_576; +pub(in crate::native_abi) const MAX_PARAMETERS: usize = 1024; +pub(in crate::native_abi) const COUNTER_OVERFLOW: u32 = 1; +pub(in crate::native_abi) const EVENT_LIMIT: u32 = 2; +pub(in crate::native_abi) const BYTE_LIMIT: u32 = 4; +pub(in crate::native_abi) const ALLOCATION: u32 = 8; +pub(in crate::native_abi) const SEQUENCE_OVERFLOW: u32 = 16; +pub(in crate::native_abi) const BOUNDARY: u32 = 32; +pub(in crate::native_abi) const KNOWN_FLAGS: u32 = 63; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(in crate::native_abi) struct Counters { + pub values: [u64; 5], +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct AttemptIdentity { + pub id: u64, + pub ordinal: u32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) enum Channel { + Session, + Attempt(AttemptIdentity), +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct CallIdentity { + pub call: u64, + pub scope: u64, + pub function: u32, + pub signature: u32, + pub template: u32, + pub operation: u32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) enum ParameterValue { + Int64 { bits: u64 }, + Unobserved, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct Parameter { + pub parameter: u32, + pub shape: u32, + pub value: ParameterValue, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct Boundary { + pub sequence: Option, + pub counters: Counters, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct AttemptObservation { + pub identity: AttemptIdentity, + pub begin: Boundary, + pub end: Option, +} +#[derive(Debug, PartialEq, Eq)] +pub(in crate::native_abi) enum HistoryKind { + Begin, + End, + Set, + Clear, + CallEnter { + call: CallIdentity, + parameters: Vec, + }, + CallComplete { + call: CallIdentity, + entered_sequence: Option, + body_status: u32, + failure_status: Option, + }, +} +#[derive(Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct HistoryEvent { + pub sequence: u64, + pub channel: Channel, + pub context: Option>, + pub kind: HistoryKind, +} +#[derive(Debug, PartialEq, Eq)] +pub(in crate::native_abi) struct Snapshot { + pub counters: Counters, + pub flags: u32, + pub attempts: Vec, + pub history: Vec, +} +#[derive(Clone, Copy)] +struct EnteredCall { + call: CallIdentity, + channel: Channel, + sequence: Option, + complete: bool, +} + +#[derive(Default)] +pub(in crate::native_abi) struct Recorder { + counters: Counters, + flags: u32, + sequence: u64, + context_bytes: usize, + active: Option, + attempts: Vec, + history: Vec, + calls: Vec, +} +impl Recorder { + pub(in crate::native_abi) fn incomplete(&mut self, flag: u32) { + self.flags |= flag; + } + fn increment(&mut self, index: usize) { + if let Some(next) = self.counters.values[index].checked_add(1) { + self.counters.values[index] = next; + } else { + self.incomplete(COUNTER_OVERFLOW); + } + } + fn ingress(&mut self, ingress: AcquisitionIngress, acquire: bool) { + match ingress { + AcquisitionIngress::HookPrepare { .. } => { + self.increment(0); + if acquire { + self.increment(1); + } + } + AcquisitionIngress::FactoryPrepare => self.increment(2), + AcquisitionIngress::FactoryCommit => self.increment(3), + } + } + pub(in crate::native_abi) fn provider_construct_dispatch(&mut self) { + self.increment(4); + } + pub(in crate::native_abi) fn context_budget(&self, length: usize) -> Result<(), u32> { + if self.history.len() >= MAX_HISTORY { + return Err(EVENT_LIMIT); + } + if length > MAX_CONTEXT + || self + .context_bytes + .checked_add(length) + .is_none_or(|total| total > MAX_CONTEXT_BYTES) + { + return Err(BYTE_LIMIT); + } + if self.sequence.checked_add(1).is_none() { + return Err(SEQUENCE_OVERFLOW); + } + Ok(()) + } + fn append( + &mut self, + channel: Channel, + context: Option<&[u8]>, + kind: HistoryKind, + ) -> Option { + if self.history.len() >= MAX_HISTORY { + self.incomplete(EVENT_LIMIT); + return None; + } + let length = context.map_or(0, <[u8]>::len); + let Some(total) = self.context_bytes.checked_add(length) else { + self.incomplete(BYTE_LIMIT); + return None; + }; + if length > MAX_CONTEXT || total > MAX_CONTEXT_BYTES { + self.incomplete(BYTE_LIMIT); + return None; + } + let Some(sequence) = self.sequence.checked_add(1) else { + self.incomplete(SEQUENCE_OVERFLOW); + return None; + }; + let copied = match copy_context(context) { + Ok(copied) => copied, + Err(flag) => { + self.incomplete(flag); + return None; + } + }; + if self.history.try_reserve(1).is_err() { + self.incomplete(ALLOCATION); + return None; + } + self.history.push(HistoryEvent { + sequence, + channel, + context: copied, + kind, + }); + self.sequence = sequence; + self.context_bytes = total; + Some(sequence) + } + pub(in crate::native_abi) fn begin(&mut self, identity: AttemptIdentity) { + if self.active.is_some() + || identity.id == 0 + || identity.ordinal as usize != self.attempts.len() + || self + .attempts + .last() + .is_some_and(|previous| previous.identity.id >= identity.id) + { + self.incomplete(BOUNDARY); + } + self.active = Some(identity); + if self.attempts.len() >= MAX_ATTEMPTS { + self.incomplete(EVENT_LIMIT); + return; + } + let sequence = self.append(Channel::Attempt(identity), None, HistoryKind::Begin); + let observation = AttemptObservation { + identity, + begin: Boundary { + sequence, + counters: self.counters, + }, + end: None, + }; + if self.attempts.try_reserve(1).is_err() { + self.incomplete(ALLOCATION); + return; + } + self.attempts.push(observation); + } + pub(in crate::native_abi) fn end(&mut self, identity: AttemptIdentity, context: Option<&[u8]>) { + if self.active != Some(identity) { + self.incomplete(BOUNDARY); + } + if self + .calls + .iter() + .any(|call| call.channel == Channel::Attempt(identity) && !call.complete) + { + self.incomplete(BOUNDARY); + } + let sequence = self.append(Channel::Attempt(identity), context, HistoryKind::End); + let boundary = Boundary { + sequence, + counters: self.counters, + }; + match self + .attempts + .iter_mut() + .find(|attempt| attempt.identity == identity) + { + Some(attempt) if attempt.end.is_none() => attempt.end = Some(boundary), + _ => self.flags |= BOUNDARY, + } + self.active = None; + } + pub(in crate::native_abi) fn property_change( + &mut self, + channel: Channel, + context: Option<&[u8]>, + ) { + if let Channel::Attempt(identity) = channel { + if self.active != Some(identity) { + self.incomplete(BOUNDARY); + } + } + let kind = if context.is_some() { + HistoryKind::Set + } else { + HistoryKind::Clear + }; + self.append(channel, context, kind); + } + pub(in crate::native_abi) fn call_enter( + &mut self, + channel: Channel, + context: &[u8], + call: CallIdentity, + parameters: Vec, + ) { + if self + .calls + .iter() + .any(|previous| previous.call.call == call.call || previous.call.scope == call.scope) + { + self.incomplete(BOUNDARY); + return; + } + if !matches!(channel, Channel::Attempt(identity) if self.active == Some(identity)) + || call.call == 0 + || call.scope == 0 + || parameters.len() > MAX_PARAMETERS + { + self.incomplete(BOUNDARY); + return; + } + if self.calls.len() >= MAX_HISTORY { + self.incomplete(EVENT_LIMIT); + return; + } + if self.calls.try_reserve(1).is_err() { + self.incomplete(ALLOCATION); + return; + } + let sequence = self.append( + channel, + Some(context), + HistoryKind::CallEnter { call, parameters }, + ); + self.calls.push(EnteredCall { + call, + channel, + sequence, + complete: false, + }); + } + pub(in crate::native_abi) fn has_entered_call(&self, call: u64) -> bool { + self.calls.iter().any(|entry| entry.call.call == call) + } + pub(in crate::native_abi) fn call_complete( + &mut self, + context: Option<&[u8]>, + call: CallIdentity, + body_status: u32, + failure_status: Option, + ) { + let Some(index) = self + .calls + .iter() + .position(|entry| entry.call.call == call.call) + else { + // Calls which never had a Property context are outside this observation claim. + return; + }; + let entry = self.calls[index]; + if entry.call != call || entry.complete { + self.incomplete(BOUNDARY); + return; + } + if self + .calls + .iter() + .skip(index + 1) + .any(|entry| !entry.complete) + { + self.incomplete(BOUNDARY); + } + self.append( + entry.channel, + context, + HistoryKind::CallComplete { + call, + entered_sequence: entry.sequence, + body_status, + failure_status, + }, + ); + self.calls[index].complete = true; + } + pub(in crate::native_abi) fn snapshot(&self) -> ResourceResult { + let mut attempts = Vec::new(); + attempts + .try_reserve_exact(self.attempts.len()) + .map_err(|_| NativeResourceError::Capacity)?; + attempts.extend_from_slice(&self.attempts); + let mut history = Vec::new(); + history + .try_reserve_exact(self.history.len()) + .map_err(|_| NativeResourceError::Capacity)?; + for event in &self.history { + history.push(copy_event(event).map_err(|_| NativeResourceError::Capacity)?); + } + Ok(Snapshot { + counters: self.counters, + flags: self.flags, + attempts, + history, + }) + } +} +pub(super) fn copy_context(context: Option<&[u8]>) -> Result>, u32> { + let Some(context) = context else { + return Ok(None); + }; + if context.len() > MAX_CONTEXT { + return Err(BYTE_LIMIT); + } + let mut copy = Vec::new(); + copy.try_reserve_exact(context.len()) + .map_err(|_| ALLOCATION)?; + copy.extend_from_slice(context); + Ok(Some(copy)) +} +fn copy_event(event: &HistoryEvent) -> Result { + let kind = match &event.kind { + HistoryKind::Begin => HistoryKind::Begin, + HistoryKind::End => HistoryKind::End, + HistoryKind::Set => HistoryKind::Set, + HistoryKind::Clear => HistoryKind::Clear, + HistoryKind::CallEnter { call, parameters } => { + let mut copied = Vec::new(); + copied + .try_reserve_exact(parameters.len()) + .map_err(|_| ALLOCATION)?; + copied.extend_from_slice(parameters); + HistoryKind::CallEnter { + call: *call, + parameters: copied, + } + } + HistoryKind::CallComplete { + call, + entered_sequence, + body_status, + failure_status, + } => HistoryKind::CallComplete { + call: *call, + entered_sequence: *entered_sequence, + body_status: *body_status, + failure_status: *failure_status, + }, + }; + Ok(HistoryEvent { + sequence: event.sequence, + channel: event.channel, + context: copy_context(event.context.as_deref())?, + kind, + }) +} + +pub(super) enum AcquisitionIngress { + HookPrepare { operation: u32 }, + FactoryPrepare, + FactoryCommit, +} +fn acquire_operation(resource: &NativeResourceState, operation: u32) -> bool { + match resource + .layout + .operations() + .get(operation as usize) + .map(|row| row.operation()) + { + Some(NativeOperation::Acquire { .. }) => true, + Some(NativeOperation::InvokeDescriptor { target, .. }) => matches!( + resource + .layout + .operations() + .get(*target as usize) + .map(|row| row.operation()), + Some(NativeOperation::Acquire { .. }) + ), + _ => false, + } +} +pub(super) fn observe_ingress(context: *const JettRuntimeContextV1, ingress: AcquisitionIngress) { + let Ok(authenticated) = AuthenticatedResourceContext::acquire(context) else { + return; + }; + let _ = authenticated.with_state(|_, state| { + let acquire = match &ingress { + AcquisitionIngress::HookPrepare { operation } => state + .resource_state + .as_ref() + .is_some_and(|resource| acquire_operation(resource, *operation)), + _ => false, + }; + state.values.test_observations.ingress(ingress, acquire); + Ok(()) + }); +} +pub(in crate::native_abi) fn snapshot( + context: *const JettRuntimeContextV1, +) -> ResourceResult { + AuthenticatedResourceContext::acquire(context)? + .with_state(|_, state| state.values.test_observations.snapshot()) +} + +/// Private executable-mode checks of journal transport semantics, not Source execution. +pub(super) fn recorder_controls() -> Result<(), ()> { + let first = AttemptIdentity { id: 11, ordinal: 0 }; + let second = AttemptIdentity { id: 19, ordinal: 1 }; + let call = CallIdentity { + call: 31, + scope: 32, + function: 7, + signature: 8, + template: 9, + operation: 10, + }; + let mut recorder = Recorder::default(); + recorder.begin(first); + recorder.property_change(Channel::Attempt(first), Some(&[0xff, 0, 1])); + recorder.call_enter( + Channel::Attempt(first), + &[0xff, 0, 1], + call, + vec![Parameter { + parameter: 0, + shape: 3, + value: ParameterValue::Int64 { bits: u64::MAX }, + }], + ); + recorder.property_change(Channel::Attempt(first), Some(b"changed")); + recorder.call_complete(Some(b"changed"), call, 0, None); + recorder.property_change(Channel::Attempt(first), None); + recorder.end(first, None); + recorder.property_change(Channel::Session, Some(b"legacy")); + recorder.begin(second); + recorder.property_change(Channel::Attempt(second), Some(b"")); + recorder.property_change(Channel::Attempt(second), None); + recorder.end(second, None); + let snapshot = recorder.snapshot().map_err(|_| ())?; + if snapshot.flags != 0 + || snapshot.counters != Counters::default() + || snapshot.attempts.len() != 2 + || snapshot.history.len() != 12 + || snapshot.history[1].context.as_deref() != Some(&[0xff, 0, 1][..]) + || snapshot.history[2].context.as_deref() != Some(&[0xff, 0, 1][..]) + || snapshot.history[7].channel != Channel::Session + || snapshot.history[9].context.as_deref() != Some(&b""[..]) + || !matches!( + snapshot.history[4].kind, + HistoryKind::CallComplete { + entered_sequence: Some(3), + body_status: 0, + failure_status: None, + .. + } + ) + || snapshot.history[4].context.as_deref() != Some(&b"changed"[..]) + { + return Err(()); + } + // Repeated prologue validation is not another activation or trial. + recorder.call_enter(Channel::Attempt(second), b"repeat", call, Vec::new()); + if recorder.flags & BOUNDARY == 0 || recorder.history.len() != 12 { + return Err(()); + } + // A saturating stored prefix plus an explicit flag is never a claimed exact zero. + let mut overflow = Recorder::default(); + overflow.counters.values[0] = u64::MAX; + overflow.ingress(AcquisitionIngress::HookPrepare { operation: 0 }, false); + if overflow.counters.values[0] != u64::MAX || overflow.flags != COUNTER_OVERFLOW { + return Err(()); + } + let mut bytes = Recorder::default(); + bytes.property_change(Channel::Session, Some(&vec![0; MAX_CONTEXT + 1])); + if bytes.flags != BYTE_LIMIT || !bytes.history.is_empty() { + return Err(()); + } + let mut bounded = Recorder::default(); + for _ in 0..MAX_HISTORY { + bounded.property_change(Channel::Session, None); + } + bounded.property_change(Channel::Session, None); + if bounded.flags != EVENT_LIMIT + || bounded.history.len() != MAX_HISTORY + || bounded.sequence != MAX_HISTORY as u64 + { + return Err(()); + } + let mut cumulative = Recorder::default(); + let context = vec![b'x'; MAX_CONTEXT]; + for _ in 0..MAX_CONTEXT_BYTES / MAX_CONTEXT { + cumulative.property_change(Channel::Session, Some(&context)); + } + cumulative.property_change(Channel::Session, Some(&context)); + if cumulative.flags != BYTE_LIMIT + || cumulative.context_bytes != MAX_CONTEXT_BYTES + || cumulative.history.len() != MAX_CONTEXT_BYTES / MAX_CONTEXT + { + return Err(()); + } + let mut sequence = Recorder::default(); + sequence.sequence = u64::MAX; + sequence.property_change(Channel::Session, None); + if sequence.flags != SEQUENCE_OVERFLOW || !sequence.history.is_empty() { + return Err(()); + } + Ok(()) +} diff --git a/crates/jett_runtime/src/native_abi/resource/tests.rs b/crates/jett_runtime/src/native_abi/resource/tests.rs new file mode 100644 index 00000000..c16cca90 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/resource/tests.rs @@ -0,0 +1,994 @@ +//! Host protocol controls, not Source admission or linked native acceptance. +use super::*; +use std::mem::{align_of, offset_of, size_of}; + +fn words(bytes: &mut Vec, values: &[u32]) { + for value in values { + bytes.extend_from_slice(&value.to_le_bytes()); + } +} +fn layout() -> Vec { + layout_with_entry_network(true) +} +pub(super) fn layout_with_entry_network(network: bool) -> Vec { + // Independent canonical wire data is checked by the sole real registration issuer. + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[1, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[1, 3, 4, 9, 2, 4, 17, 0]); + words(&mut bytes, &[0]); + for row in [[0, 0, 1, 0], [1, 0, 2, 1], [2, 0, 3, 2]] { + words(&mut bytes, &row); + } + // Factory, Borrow, Finalize and the containing entry-function signature. + words(&mut bytes, &[0, 2, 6, 0, 2, 1, 1]); + words(&mut bytes, &[1, 2, 7, 0, 2, 4, 2]); + words(&mut bytes, &[2, 1, 3, 4, 1]); + if network { + words(&mut bytes, &[3, 1, 3, 0, 2]); + } else { + words(&mut bytes, &[3, 0, 3]); + } + for row in [ + vec![0, 6], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 5], + vec![4, 7, 0], + vec![5, 8, 4], + vec![6, 9, 4, 2], + vec![7, 9, 1, 2], + vec![8, 10, 0], + ] { + words(&mut bytes, &row); + } + // Exact containing-function signature for both Scope and Operation. + words(&mut bytes, &[0, 1, 0, 0, 0, 0, 3, 1, 0, 0]); + words(&mut bytes, &[1, 2, 0, 0, 0, 1, 3, 1, 1, 0]); + for row in [ + vec![0, 0, 4, 0], + vec![1, 1, 4, 0], + vec![2, 1, 6, 1, 2], + vec![3, 0, 5, 1, 1], + ] { + words(&mut bytes, &row); + } + let operations: [&[u32]; 17] = [ + &[1, 1, 0, 2], + &[10, 1, 2, 1], + &[2, 1, 1, 0], + &[3, 1, 0, 1], + &[6, 1, 1, 1, 3], + &[5, 1, 3], + &[7, 1, 2, 0], + &[8, 1, 1], + &[9, 1, 1, 2], + &[11, 1, 2], + &[12, 1, 0, 1], + &[13, 0], + &[14, 0], + &[15, 0, 0, 0], + &[9, 1, 1, 3], + &[10, 1, 3, 1], + &[2, 1, 0, 1], + ]; + for (ordinal, operation) in operations.into_iter().enumerate() { + words(&mut bytes, &[ordinal as u32, 0, 0, 0, 100 + ordinal as u32]); + words(&mut bytes, operation); + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + bytes +} +pub(super) fn script(rows: &[(u32, i64, i64, &str)], attempts: u32) -> DecodedScript { + let mut bytes = b"JTRST001".to_vec(); + words(&mut bytes, &[1, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[attempts, rows.len() as u32]); + for &(tag, label, result, error) in rows { + words(&mut bytes, &[tag]); + bytes.extend_from_slice(&label.to_le_bytes()); + match tag { + 2 | 5 => { + words(&mut bytes, &[error.len() as u32]); + bytes.extend_from_slice(error.as_bytes()); + } + 4 => bytes.extend_from_slice(&result.to_le_bytes()), + _ => {} + } + } + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + DecodedScript::decode(&bytes).unwrap() +} +pub(super) fn entry() -> NativeEntry { + NativeEntry { + function: 0, + signature: 3, + scope: 0, + } +} +pub(super) fn context(operation: impl FnOnce(&AuthenticatedResourceContext) -> T) -> T { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = JettRuntimeResultV1::ok(); + let status = unsafe { + jett_rt_v1_context_create(JETT_RUNTIME_ABI_VERSION_V1, &mut *context, &mut result) + }; + assert_eq!(status, JettRuntimeStatusV1::OK); + let authenticated = AuthenticatedResourceContext::acquire(&*context).unwrap(); + let result = operation(&authenticated); + drop(authenticated); // release the actual lease before retirement waits for it + let status = + unsafe { jett_rt_v1_context_destroy(&mut *context, &mut JettRuntimeResultV1::ok()) }; + assert_eq!(status, JettRuntimeStatusV1::OK); + result +} +fn install( + authenticated: &AuthenticatedResourceContext, + rows: &[(u32, i64, i64, &str)], + attempts: u32, +) { + install_scripted(authenticated, &layout(), entry(), script(rows, attempts)).unwrap(); +} +fn start( + authenticated: &AuthenticatedResourceContext, + purpose: ResourcePurpose, +) -> (ResourceHandleId, ResourceHandleId, ResourceHandleId, u64) { + authenticated + .with_resource(|state, ordinary, registry| { + let attempt = state.begin_entry(ordinary, registry, entry(), purpose)?; + let root = state.root_frame(attempt)?; + let network = if purpose == ResourcePurpose::Runtime { + state.entry_network(attempt, 0)? + } else { + 0 + }; + let operation = state.begin_operation_frame(ordinary, 1, root)?; + Ok((attempt, root, operation, network)) + }) + .unwrap() +} +fn construct( + authenticated: &AuthenticatedResourceContext, + frame: ResourceHandleId, + network: u64, + label: i64, +) -> ResourceHandleId { + authenticated + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + let result = state.factory(ordinary, registry, prepared, network, label)?; + assert_eq!(result.domain, RESOURCE_DOMAIN_OK); + let sum = ResourceHandleId::new(result.value)?; + state.sum_take(ordinary, registry, 1, frame, sum, frame) + }) + .unwrap() +} +fn complete( + authenticated: &AuthenticatedResourceContext, + attempt: ResourceHandleId, + body: u32, +) -> NativeResourceCompletion { + authenticated + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, body, false) + }) + .unwrap() +} +fn events(authenticated: &AuthenticatedResourceContext) -> Vec<(i64, u32)> { + authenticated + .with_resource(|state, _, _| { + Ok(state.provider.observe_events(|events| { + events + .iter() + .map(|event| (event.label, event.kind)) + .collect() + })) + }) + .unwrap() +} +fn assert_empty(authenticated: &AuthenticatedResourceContext) { + authenticated + .with_resource(|state, ordinary, registry| { + assert_eq!( + state.counts(ordinary, registry), + NativeResourceCounts { + ordinary_empty: 1, + ..NativeResourceCounts::default() + } + ); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn native_resource_state_production_default_and_pure_descriptor_installation_have_no_provider_grant() + { + context(|auth| { + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + Ok(()) + }) + .unwrap(); + install_disabled(auth, &layout_with_entry_network(false), entry()).unwrap(); + auth.with_resource(|state, ordinary, registry| { + let attempt = + state.begin_entry(ordinary, registry, entry(), ResourcePurpose::Runtime)?; + let root = state.root_frame(attempt)?; + let frame = state.begin_operation_frame(ordinary, 1, root)?; + let descriptor = state.descriptor(ordinary, 12)?; + let call = state.prepare_hook(ordinary, 13, frame, Some(descriptor))?; + assert_eq!( + state.prepare_factory(ordinary, registry, call, 0), + Err(NativeResourceError::WrongGrant) + ); + assert!(state.network.is_none()); + assert!(!state.provider.enabled()); + state.complete_entry(ordinary, registry, attempt, 0, false)?; + Ok(()) + }) + .unwrap(); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_real_core_construct_borrow_move_close_and_reentry() { + context(|auth| { + install( + auth, + &[(1, 501, 0, ""), (4, 501, 7, ""), (1, 502, 0, "")], + 2, + ); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 501); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }) + .unwrap(); + auth.with_resource(|state, ordinary, registry| { + // A wrong family is rejected without borrowing or provider consumption. + assert_eq!( + state.transfer(ordinary, registry, 2, frame, loan, root), + Err(NativeResourceError::WrongFamily) + ); + ordinary + .drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner)?; + state.end_operation_frame(ordinary, registry, frame) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + let _owner = construct(auth, frame, network, 502); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!( + events(auth), + vec![(501, 1), (501, 3), (501, 5), (502, 1), (502, 5)] + ); + }); +} + +#[test] +fn native_resource_state_installation_and_network_pair_are_atomic_and_context_exact() { + context(|auth| { + assert_ne!( + unsafe { + values::jett_rt_v1_grant_network( + auth.key.owner_address as *const JettRuntimeContextV1, + ) + }, + 0 + ); + assert_eq!( + install_scripted(auth, &layout(), entry(), script(&[], 1)), + Err(NativeResourceError::OrdinaryStorage( + JettRuntimeStatusV1::INVALID_ARGUMENT + )) + ); + auth.with_state(|_, state| { + assert!(state.resource_state.is_none()); + assert_eq!(state.resources.live_count(), 0); + Ok(()) + }) + .unwrap(); + }); + context(|first| { + context(|second| { + install(first, &[(1, 1, 0, "")], 1); + install(second, &[], 1); + let (_, _, frame, network) = start(first, ResourcePurpose::Runtime); + first + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + assert_eq!( + state.prepare_factory(ordinary, registry, call, network + 1), + Err(NativeResourceError::WrongGrant) + ); + Ok(()) + }) + .unwrap(); + let foreign = first + .with_resource(|state, _, _| Ok(state.program_handle)) + .unwrap(); + second + .with_resource(|state, _, _| { + assert!(!state.handles.contains_key(&foreign)); + Ok(()) + }) + .unwrap(); + assert!(events(first).is_empty()); + // Destruction safety cleanup is not substituted for the explicit completion gate. + let attempt = first + .with_resource(|state, _, _| Ok(state.attempt.as_ref().unwrap().id)) + .unwrap(); + complete(first, attempt, 0); + assert_empty(first); + }) + }); +} + +#[test] +fn native_resource_state_factory_domain_error_is_data_and_ordinary_companion_is_retired() { + context(|auth| { + install(auth, &[(2, 4, 0, "domain failure")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 0, frame, None)?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + let result = state.factory(ordinary, registry, prepared, network, 4)?; + assert_eq!(result.domain, RESOURCE_DOMAIN_ERROR); + let handle = ResourceHandleId::new(result.value)?; + assert_eq!(state.sum_tag(handle)?, 0); + assert_eq!(state.counts(ordinary, registry).owners, 0); + state.sum_drop(ordinary, registry, 9, frame, handle) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(4, 2)]); + }); +} + +#[test] +fn native_resource_state_active_loan_refuses_move_and_preserves_one_owner() { + context(|auth| { + install(auth, &[(1, 6, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 6); + auth.with_resource(|state, ordinary, registry| { + // Move into the caller scope before borrowing. + let owner = state.transfer(ordinary, registry, 2, frame, owner, root)?; + let loan = state.borrow_begin(ordinary, registry, 3, frame, owner)?; + assert_eq!( + state.transfer(ordinary, registry, 16, frame, owner, frame), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!( + state.close_or_drop(registry, 6, frame, owner), + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow) + )) + ); + assert_eq!(state.counts(ordinary, registry).owners, 1); + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner) + }) + .unwrap(); + // The attempted cleanup failure remains latched independently of token retirement. + assert_eq!(complete(auth, attempt, 0).selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(6, 1), (6, 5)]); + }); +} + +#[test] +fn native_resource_state_borrow_panic_records_before_pop_and_cleanup_wins() { + context(|auth| { + install(auth, &[(3, 7, 0, ""), (6, 7, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 7); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth.with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }); + assert_eq!(result, Err(NativeResourceError::BodyFailed)); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, JettRuntimeStatusV1::PANIC.code()); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(7, 1), (7, 3), (7, 5)]); + auth.with_resource(|state, _, _| { + assert_eq!(state.provider.remaining(), 0); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_state_required_purpose_rejects_hook_without_provider_effect() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + context(|auth| { + install(auth, &[(1, 9, 0, "")], 1); + let (attempt, _, frame, _) = start(auth, purpose); + auth.with_resource(|state, ordinary, _| { + assert_eq!( + state.entry_network(attempt, 0), + Err(NativeResourceError::WrongPurpose) + ); + let descriptor = state.descriptor(ordinary, 12)?; + assert!(matches!( + state.handles.get(&descriptor), + Some(NativeResourceEntry::Descriptor { .. }) + )); + assert_eq!( + state.prepare_hook(ordinary, 0, frame, None), + Err(NativeResourceError::WrongPurpose) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert!(events(auth).is_empty()); + }); + } +} + +#[test] +fn native_resource_state_sum_moves_do_not_clone_registry_payload() { + context(|auth| { + install(auth, &[(1, 11, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 11); + auth.with_resource(|state, ordinary, registry| { + let sum = state.sum_adopt(ordinary, registry, 14, frame, owner, root)?; + assert_eq!(state.sum_tag(sum)?, 1); + assert_eq!(state.counts(ordinary, registry).owners, 1); + assert!(!state.handles.contains_key(&owner)); + let owner = state.sum_take(ordinary, registry, 15, frame, sum, frame)?; + assert!(!state.handles.contains_key(&sum)); + state.close_or_drop(registry, 7, frame, owner) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(11, 1), (11, 5)]); + }); +} + +#[test] +fn native_resource_state_descriptor_and_prepared_handles_cannot_be_replayed() { + context(|auth| { + install(auth, &[(1, 12, 0, "")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + let descriptor = state.descriptor(ordinary, 12)?; + let call = state.prepare_hook(ordinary, 13, frame, Some(descriptor))?; + let prepared = state.prepare_factory(ordinary, registry, call, network)?; + state.factory(ordinary, registry, prepared, network, 12)?; + assert_eq!( + state.factory(ordinary, registry, prepared, network, 12), + Err(NativeResourceError::WrongFamily) + ); + assert_eq!( + state.prepare_hook(ordinary, 13, frame, Some(state.program_handle)), + Err(NativeResourceError::WrongFamily) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(12, 1), (12, 5)]); + }); +} + +#[test] +fn native_resource_state_ordinary_failure_is_attempt_owned_without_reset() { + context(|auth| { + install(auth, &[], 2); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_ne!( + unsafe { + values::jett_rt_v1_assert_fail( + auth.key.owner_address as *const JettRuntimeContextV1, + ) + }, + 0 + ); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_attempt(attempt)?, completion); + assert_eq!(state.completed_messages(attempt)?.1, b"assertion failed"); + Ok(()) + }) + .unwrap(); + auth.with_state(|_, state| { + assert!(state.values.resource_failure().is_some()); + Ok(()) + }) + .unwrap(); + // The exact completed error remains immutable. A fresh constructor owns + // the next execution; no post-completion capture or reset is performed. + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + }); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + let text = + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer, ptr::null(), 0) }; + assert_ne!(text, 0); + auth.with_state(|_, state| { + state + .values + .drop_resource_ordinary_companion(text) + .map_err(ordinary_error) + }) + .unwrap(); + // A Source handler legally turned the ordinary error into data before completion. + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 0); + assert_empty(auth); + }); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + let refusal = + auth.with_resource(|state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None)); + assert_eq!(refusal, Err(NativeResourceError::WrongOperation)); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + let text = + unsafe { values::jett_rt_v1_failure_take_prefixed_text(pointer, ptr::null(), 0) }; + auth.with_state(|_, state| { + state + .values + .drop_resource_ordinary_companion(text) + .map_err(ordinary_error) + }) + .unwrap(); + // Clearing only the ordinary channel cannot erase the Resource-owned refusal. + let completion = complete(auth, attempt, 0); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_reverse_drop_and_replacement_cleanup_are_real_once_only_core_events() { + context(|auth| { + install(auth, &[(1, 21, 0, ""), (3, 22, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let first = construct(auth, frame, network, 21); + auth.with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, first, root) + }) + .unwrap(); + construct(auth, frame, network, 22); + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(21, 1), (22, 1), (22, 5), (21, 5)]); + }); + for panic_on_old in [false, true] { + context(|auth| { + install( + auth, + &[ + (if panic_on_old { 3 } else { 1 }, 31, 0, ""), + (1, 32, 0, ""), + ], + 1, + ); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let first = construct(auth, frame, network, 31); + let first = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, first, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 32); + let result = auth.with_resource(|state, ordinary, registry| { + state.replace(ordinary, registry, 10, frame, first, replacement) + }); + if panic_on_old { + assert_eq!( + result, + Err(NativeResourceError::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + } else { + assert!(result.is_ok()); + } + let completion = complete(auth, attempt, 0); + assert_eq!(completion.selected_kind, if panic_on_old { 3 } else { 0 }); + assert_empty(auth); + assert_eq!(events(auth), vec![(31, 1), (32, 1), (31, 5), (32, 5)]); + }); + } +} + +#[test] +fn native_resource_state_closed_script_and_completion_layout_refuse_malformed_inputs() { + assert_eq!(size_of::(), 24); + assert_eq!(align_of::(), 8); + assert_eq!(offset_of!(NativeResourceCompletion, selected_kind), 16); + assert_eq!(size_of::(), 72); + assert_eq!(size_of::(), 24); + assert!(DecodedScript::decode(b"JTRST001").is_err()); + context(|auth| { + install(auth, &[], 1); + let (attempt, _, _, _) = start(auth, ResourcePurpose::Runtime); + auth.with_resource(|state, ordinary, registry| { + assert_eq!( + state.complete_entry( + ordinary, + registry, + ResourceHandleId::new(attempt.raw() + 1)?, + 0, + false + ), + Err(NativeResourceError::InvalidEntry) + ); + Ok(()) + }) + .unwrap(); + complete(auth, attempt, 0); + auth.with_resource(|state, ordinary, registry| { + assert_eq!( + state.complete_entry(ordinary, registry, attempt, 0, false), + Err(NativeResourceError::InvalidEntry) + ); + Ok(()) + }) + .unwrap(); + assert_empty(auth); + }); +} + +#[test] +fn native_resource_state_completion_keeps_observed_nonzero_status_distinct_from_recorded_failures() +{ + for resource_failure in [false, true] { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + if resource_failure { + assert_eq!( + auth.with_resource( + |state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None) + ), + Err(NativeResourceError::WrongOperation) + ); + } else { + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_ne!(unsafe { values::jett_rt_v1_assert_fail(pointer) }, 0); + } + let observed = JettRuntimeStatusV1::IO_FAILURE.code(); + assert_ne!( + observed, + NativeResourceError::WrongOperation.status().code() + ); + assert_ne!(observed, JettRuntimeStatusV1::INVALID_ARGUMENT.code()); + let completion = complete(auth, attempt, observed); + assert_eq!(completion.body_status, observed); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 1); + assert_empty(auth); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_attempt(attempt)?, completion); + let (resource, ordinary) = state.completed_messages(attempt)?; + if resource_failure { + assert_eq!(resource, REFUSED); + assert!(ordinary.is_empty()); + } else { + assert!(resource.is_empty()); + assert_eq!(ordinary, b"assertion failed"); + } + Ok(()) + }) + .unwrap(); + }); + } +} + +#[test] +fn native_resource_state_completion_selects_host_panic_and_cleanup_without_rewriting_observed_status() + { + context(|auth| { + install(auth, &[], 1); + let (attempt, _, frame, _) = start(auth, ResourcePurpose::Runtime); + assert_eq!( + auth.with_resource(|state, ordinary, _| state.prepare_hook(ordinary, 999, frame, None)), + Err(NativeResourceError::WrongOperation) + ); + let completion = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, 0, true) + }) + .unwrap(); + assert_eq!(completion.body_status, 0); + assert_eq!(completion.cleanup_status, 0); + assert_eq!(completion.selected_kind, 2); + assert_empty(auth); + auth.with_resource(|state, _, _| { + // The earlier Resource refusal remains recorded independently of the host outcome. + assert_eq!(state.completed_messages(attempt)?.0, REFUSED); + Ok(()) + }) + .unwrap(); + }); + context(|auth| { + install(auth, &[(3, 41, 0, "")], 1); + let (attempt, _, frame, network) = start(auth, ResourcePurpose::Runtime); + construct(auth, frame, network, 41); + let observed = JettRuntimeStatusV1::IO_FAILURE.code(); + let completion = auth + .with_resource(|state, ordinary, registry| { + state.complete_entry(ordinary, registry, attempt, observed, true) + }) + .unwrap(); + assert_eq!(completion.body_status, observed); + assert_eq!(completion.cleanup_status, JettRuntimeStatusV1::PANIC.code()); + assert_eq!(completion.selected_kind, 3); + assert_empty(auth); + assert_eq!(events(auth), vec![(41, 1), (41, 5)]); + auth.with_resource(|state, _, _| { + assert_eq!(state.completed_messages(attempt)?.0, CLEANUP_FAILED); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn native_resource_state_borrow_domain_error_retires_ordinary_sum_and_string() { + context(|auth| { + install(auth, &[(1, 503, 0, ""), (5, 503, 0, "borrow failure")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let owner = construct(auth, frame, network, 503); + let owner = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, owner, root) + }) + .unwrap(); + let loan = auth + .with_resource(|state, ordinary, registry| { + state.borrow_begin(ordinary, registry, 3, frame, owner) + }) + .unwrap(); + let result = auth + .with_resource(|state, ordinary, registry| { + let call = state.prepare_hook(ordinary, 4, frame, None)?; + state.invoke_borrow(ordinary, registry, call, network, loan) + }) + .unwrap(); + let pointer = auth.key.owner_address as *const JettRuntimeContextV1; + assert_eq!(unsafe { values::jett_rt_v1_sum_tag(pointer, result) }, 0); + auth.with_resource(|state, ordinary, registry| { + assert!(ordinary.resource_failure().is_none()); + assert_eq!(state.counts(ordinary, registry).ordinary_empty, 0); + ordinary + .drop_resource_ordinary_companion(result) + .map_err(ordinary_error)?; + assert_eq!(state.counts(ordinary, registry).ordinary_empty, 1); + assert!(ordinary.drop_resource_ordinary_companion(result).is_err()); + state.borrow_end(5, frame, loan)?; + state.close_or_drop(registry, 6, frame, owner)?; + state.end_operation_frame(ordinary, registry, frame) + }) + .unwrap(); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(503, 1), (503, 4), (503, 5)]); + }); +} + +#[path = "source_tests.rs"] +mod source_tests; + +#[test] +fn native_resource_replacement_preflight_keeps_both_owners_before_old_or_rhs_loan_refusal() { + for borrow_old in [false, true] { + context(|auth| { + install(auth, &[(1, 41, 0, ""), (1, 42, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let original = construct(auth, frame, network, 41); + let old = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, original, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 42); + auth.with_resource(|state, ordinary, registry| { + let NativeResourceEntry::Owner(borrowed) = state + .handles + .get(&if borrow_old { old } else { replacement }) + .unwrap() + else { + panic!("exact scripted owner"); + }; + let NativeResourceEntry::Frame(borrower) = state.handles.get(&frame).unwrap() + else { + panic!("exact operation frame"); + }; + let mut loan = + state + .custody + .begin_borrow(&borrowed.token, &borrower.token, registry)?; + let before = state.counts(ordinary, registry); + assert_eq!((before.owners, before.registry, before.loans), (2, 2, 1)); + let old_key = state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?; + let rhs_key = state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?; + assert_eq!( + state.replace(ordinary, registry, 10, frame, old, replacement), + Err(NativeResourceError::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!(state.counts(ordinary, registry), before); + assert_eq!( + state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?, + old_key + ); + assert_eq!( + state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?, + rhs_key + ); + assert_eq!( + state.provider.observe_events(|events| events + .iter() + .map(|event| (event.label, event.kind)) + .collect::>()), + vec![(41, 1), (42, 1)] + ); + state.custody.end_borrow(&mut loan)?; + let installed = state.replace(ordinary, registry, 10, frame, old, replacement)?; + assert!(!state.handles.contains_key(&old)); + assert!(!state.handles.contains_key(&replacement)); + assert_eq!( + state + .custody + .validate_owned(&state.owner(installed, 0)?.token, registry)?, + rhs_key + ); + assert_eq!( + ( + state.counts(ordinary, registry).owners, + registry.live_count() + ), + (1, 1) + ); + Ok(()) + }) + .unwrap(); + assert_eq!(events(auth), vec![(41, 1), (42, 1), (41, 5)]); + let completion = complete(auth, attempt, 0); + assert_eq!( + ( + completion.body_status, + completion.cleanup_status, + completion.selected_kind + ), + (0, 0, 0) + ); + assert_empty(auth); + assert_eq!(events(auth), vec![(41, 1), (42, 1), (41, 5), (42, 5)]); + }); + } +} + +#[test] +fn native_resource_replacement_preflight_stale_rhs_handle_has_no_finalizer_effect() { + context(|auth| { + install(auth, &[(1, 51, 0, ""), (1, 52, 0, "")], 1); + let (attempt, root, frame, network) = start(auth, ResourcePurpose::Runtime); + let stale = construct(auth, frame, network, 51); + let old = auth + .with_resource(|state, ordinary, registry| { + state.transfer(ordinary, registry, 2, frame, stale, root) + }) + .unwrap(); + let replacement = construct(auth, frame, network, 52); + auth.with_resource(|state, ordinary, registry| { + let before = state.counts(ordinary, registry); + assert_eq!((before.owners, before.registry), (2, 2)); + assert!(!state.handles.contains_key(&stale)); + assert_eq!( + state.replace(ordinary, registry, 10, frame, old, stale), + Err(NativeResourceError::InvalidHandle) + ); + assert_eq!(state.counts(ordinary, registry), before); + state + .custody + .validate_owned(&state.owner(old, 0)?.token, registry)?; + state + .custody + .validate_owned(&state.owner(replacement, 1)?.token, registry)?; + assert_eq!( + state.provider.observe_events(|events| events + .iter() + .map(|event| (event.label, event.kind)) + .collect::>()), + vec![(51, 1), (52, 1)] + ); + state.replace(ordinary, registry, 10, frame, old, replacement)?; + Ok(()) + }) + .unwrap(); + assert_eq!(events(auth), vec![(51, 1), (52, 1), (51, 5)]); + assert_eq!(complete(auth, attempt, 0).selected_kind, 0); + assert_empty(auth); + assert_eq!(events(auth), vec![(51, 1), (52, 1), (51, 5), (52, 5)]); + }); +} + +#[path = "borrowed_sum_tests.rs"] +mod borrowed_sum_tests; + +#[path = "sum_replacement_tests.rs"] +mod sum_replacement_tests; + +#[path = "ordinary_attempt_owner_tests.rs"] +mod ordinary_attempt_owners; +#[path = "ordinary_error_reentry_tests.rs"] +mod ordinary_error_reentry; + +#[path = "ordinary_reservation_failure_tests.rs"] +mod ordinary_reservation_failures; + +#[path = "graph_descriptors/tests.rs"] +mod graph_descriptor_tests; diff --git a/crates/jett_runtime/src/native_abi/values.rs b/crates/jett_runtime/src/native_abi/values.rs new file mode 100644 index 00000000..e02b1997 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values.rs @@ -0,0 +1,11386 @@ +//! Typed native leaf operations. See docs/active/native_value_abi.md. +//! Every pointer must refer to a live stationary ABI context, except literal +//! bytes which are borrowed for the call. No Rust value crosses this ABI. +use super::*; +use crate::clock; +use crate::crypto; +use crate::csv; +use crate::encoding; +use crate::environment::{self, LaunchEnvironmentSnapshot}; +use crate::graphics; +use crate::math; +use crate::random::{self, RandomProvider}; +use std::cell::RefCell; +use std::cmp::Ordering as CompareOrdering; +use std::sync::atomic::{AtomicU64, Ordering}; +use subtle::ConstantTimeEq; +use unicode_segmentation::UnicodeSegmentation; + +mod actor_graph_import; +mod checked_graph_hooks; +mod debug_equal; +mod debug_format; +#[cfg(test)] +mod field_update_tests; +mod graphics_decode; +pub mod interface_conversion; +mod ordinary_attempts; +mod resource_sum_replacement; + +#[cfg(test)] +mod interface_argument_tests; + +pub type NativeHandle = u64; +type Failure = (JettRuntimeStatusV1, &'static [u8]); +type LeafResult = Result; +const INVALID_HANDLE: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native string handle", +); +const INVALID_STRING_SCALAR_INDEX: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native string scalar index", +); +const INVALID_STRUCT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native struct handle or field", +); +const INVALID_INTERFACE_ARGUMENT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"interface dispatch concrete argument owner mismatch", +); +const INVALID_FUNCTION: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native function descriptor or label", +); +const INVALID_ACTOR: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native actor handle or field", +); +const UNSUPPORTED_ACTOR_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending actor", +); +const INVALID_CAPABILITY: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native capability authority or task", +); +const INVALID_GRAPHICS: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native Graphics authority or session", +); +const INVALID_CONSTRUCTION: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native TypeConstruction builder", +); +const INVALID_CONSTRUCTION_LAYOUT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native TypeConstruction layout", +); +const INVALID_REFLECTED_FIELD: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"type.field_value: field metadata does not match the checked owner and requested type", +); +const INVALID_REFLECTED_VARIANT_FIELD: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"type.variant_field_value: field metadata does not match the active variant and requested type", +); +const INVALID_REFLECTED_MACHINE_FIELD: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"type.machine_field_value: field metadata does not match the active state and requested type", +); +const INVALID_REFLECTED_OWNER: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid reflected enum or machine value", +); +const INVALID_TYPE_INFO: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid reflected TypeInfo for native type dispatch", +); +const INVALID_TYPE_ARG_INDEX: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"type.arg index is out of range for the checked type", +); +const INVALID_LIST: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native list handle", +); +const INVALID_PENDING_SCALAR_CHECK: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native pending scalar check", +); +const INVALID_PENDING_HANDLE_CHECK: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native pending handle check", +); +const INVALID_SET: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native set handle or element", +); +const PENDING_SET_LENGTH: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"set.__length expects a set argument", +); +const PENDING_SET_CONTAINS: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"set.__contains expects a set as first argument", +); +const PENDING_SET_ADD: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"set.__add expects a set as first argument", +); +const PENDING_SET_REMOVE: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"set.__remove expects a set as first argument", +); +const PENDING_ITERABLE: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"for loop requires a list, string, map, or set value", +); +const INVALID_MAP: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native map handle or entry", +); +const PENDING_MAP_LENGTH: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__length expects a map argument", +); +const PENDING_MAP_HAS: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__has expects a map as first argument", +); +const PENDING_MAP_GET: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__get expects a map as first argument", +); +const PENDING_MAP_INSERT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__insert expects a map as first argument", +); +const PENDING_MAP_REMOVE: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__remove expects a map as first argument", +); +const PENDING_MAP_FROM_LISTS: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"map.__from_lists expects two list arguments", +); +const INVALID_BYTES: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native bytes handle", +); +const INVALID_BITFIELD_LAYOUT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native bitfield layout", +); +const INVALID_TRACE_LABEL: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native trace label", +); +const INVALID_FAILURE_COPY: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native failure message copy buffer", +); +const INVALID_SUM: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native sum handle or tag", +); +const EXHAUSTED: Failure = ( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + b"native value capacity exhausted", +); +const INVALID_NOTHING_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native nothing comparison", +); +const UNSUPPORTED_NOTHING_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending nothing", +); +const UNSUPPORTED_SCALAR_BINARY: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending scalar", +); +const PENDING_BOOL_NEGATION: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"'not' requires a boolean operand", +); +const PENDING_NUMERIC_NEGATION: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unary '-' requires a numeric operand", +); +const PENDING_BOOL_CONDITION: Failure = + (JettRuntimeStatusV1::INVALID_ARGUMENT, b"expected boolean"); +const UNSUPPORTED_STRING_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending string", +); +const UNSUPPORTED_ENUM_COMPARISON: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"unsupported binary operation on pending enum", +); +const PENDING_FUNCTION_CALL: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"expected function value", +); +const INVALID_DISPLAY_RESULT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"Displayable.display did not return a ready string", +); +const PENDING_ACTOR_MESSAGE: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"send/ask: expected actor value", +); + +fn format_nothing(depth: u64) -> LeafResult { + format_pending_value("nothing", depth) +} + +fn format_pending_value(inner: &str, depth: u64) -> LeafResult { + let depth = usize::try_from(depth).map_err(|_| EXHAUSTED)?; + let length = depth + .checked_mul("pending()".len()) + .and_then(|length| length.checked_add(inner.len())) + .ok_or(EXHAUSTED)?; + let mut text = String::new(); + text.try_reserve_exact(length).map_err(|_| EXHAUSTED)?; + for _ in 0..depth { + text.push_str("pending("); + } + text.push_str(inner); + for _ in 0..depth { + text.push(')'); + } + Ok(text) +} + +struct NativeString { + text: String, + references: u64, + pending_depth: u64, +} +/// Stable discriminants for optional and result storage (not terminal status). +pub const SUM_FAILURE: u32 = 0; +pub const SUM_SUCCESS: u32 = 1; +pub const DEBUG_NOTHING_KIND: u32 = 12; +pub const DEBUG_BYTES_KIND: u32 = 13; + +/// Native function records own their environment (when present) and display +/// label. Code addresses are borrowed and are never inspected by debug output. +pub const NATIVE_FUNCTION_FIELD_COUNT: u64 = 3; +pub const NATIVE_FUNCTION_CODE_FIELD: u64 = 0; +pub const NATIVE_FUNCTION_ENVIRONMENT_FIELD: u64 = 1; +pub const NATIVE_FUNCTION_LABEL_FIELD: u64 = 2; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NativeDebugTag { + Primitive, + Nothing, + Bytes, + List, + Set, + Map, + Optional, + Result, + Record, + Enum, + Machine, + Capability, + Alias, + Function, + Actor, + TypeConstruction, + Redacted, + Interface, + Uninhabited, +} + +impl NativeDebugTag { + fn from_raw(value: u8) -> LeafResult { + Ok(match value { + 0 => Self::Primitive, + 1 => Self::Nothing, + 2 => Self::Bytes, + 3 => Self::List, + 4 => Self::Set, + 5 => Self::Map, + 6 => Self::Optional, + 7 => Self::Result, + 8 => Self::Record, + 9 => Self::Enum, + 10 => Self::Machine, + 11 => Self::Capability, + 12 => Self::Alias, + 13 => Self::Function, + 14 => Self::Actor, + 15 => Self::TypeConstruction, + 16 => Self::Redacted, + 17 => Self::Interface, + 18 => Self::Uninhabited, + _ => return Err(INVALID_TRACE_LABEL), + }) + } +} + +enum NativeGraphicsSession { + Window(graphics::Session), + Scripted { width: i64, height: i64 }, +} + +struct ActiveGraphicsSession { + identity: u64, + session: NativeGraphicsSession, +} + +thread_local! { + static GRAPHICS_SESSION: RefCell> = const { RefCell::new(None) }; +} +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NativeSortKind { + Int8, + Int16, + Int32, + Int64, + Uint8, + Uint16, + Uint32, + Uint64, + Float32, + Float64, + Bool, + String, +} +impl NativeSortKind { + fn from_raw(value: u32) -> LeafResult { + Ok(match value { + 0 => Self::Int8, + 1 => Self::Int16, + 2 => Self::Int32, + 3 => Self::Int64, + 4 => Self::Uint8, + 5 => Self::Uint16, + 6 => Self::Uint32, + 7 => Self::Uint64, + 8 => Self::Float32, + 9 => Self::Float64, + 10 => Self::Bool, + 11 => Self::String, + _ => return Err(INVALID_LIST), + }) + } + fn from_comparison_raw(value: u32) -> LeafResult> { + if value == u32::MAX { + return Ok(None); + } + Self::from_raw(value).map(Some) + } + fn compare(self, left: u64, right: u64) -> CompareOrdering { + match self { + Self::Int8 => (left as u8 as i8).cmp(&(right as u8 as i8)), + Self::Int16 => (left as u16 as i16).cmp(&(right as u16 as i16)), + Self::Int32 => (left as u32 as i32).cmp(&(right as u32 as i32)), + Self::Int64 => (left as i64).cmp(&(right as i64)), + Self::Uint8 | Self::Uint16 | Self::Uint32 | Self::Uint64 | Self::Bool => { + left.cmp(&right) + } + Self::Float32 => f32::from_bits(left as u32) + .partial_cmp(&f32::from_bits(right as u32)) + .unwrap_or(CompareOrdering::Equal), + Self::Float64 => f64::from_bits(left) + .partial_cmp(&f64::from_bits(right)) + .unwrap_or(CompareOrdering::Equal), + Self::String => unreachable!("string ordering uses the string registry"), + } + } + fn valid_bits(self, bits: u64) -> bool { + match self { + Self::Int8 | Self::Uint8 => bits <= u8::MAX as u64, + Self::Int16 | Self::Uint16 => bits <= u16::MAX as u64, + Self::Int32 | Self::Uint32 | Self::Float32 => bits <= u32::MAX as u64, + Self::Bool => bits <= 1, + Self::Int64 | Self::Uint64 | Self::Float64 | Self::String => true, + } + } +} +#[repr(u32)] +#[derive(Clone, Copy)] +pub enum NativePendingBinaryOp { + Add, + Sub, + Mul, + Div, + Modulo, + Eq, + NotEq, + Lt, + Gt, + LtEq, + GtEq, + And, + Or, +} + +#[repr(u32)] +#[derive(Clone, Copy)] +pub enum NativeSequenceKind { + List, + String, + Map, + Set, +} + +impl NativeSequenceKind { + fn from_raw(raw: u32) -> LeafResult { + Ok(match raw { + 0 => Self::List, + 1 => Self::String, + 2 => Self::Map, + 3 => Self::Set, + _ => return Err(INVALID_LIST), + }) + } +} + +impl NativePendingBinaryOp { + fn from_raw(raw: u32) -> LeafResult { + Ok(match raw { + 0 => Self::Add, + 1 => Self::Sub, + 2 => Self::Mul, + 3 => Self::Div, + 4 => Self::Modulo, + 5 => Self::Eq, + 6 => Self::NotEq, + 7 => Self::Lt, + 8 => Self::Gt, + 9 => Self::LtEq, + 10 => Self::GtEq, + 11 => Self::And, + 12 => Self::Or, + _ => return Err(INVALID_TRACE_LABEL), + }) + } + + fn label(self) -> &'static str { + match self { + Self::Add => "Add", + Self::Sub => "Sub", + Self::Mul => "Mul", + Self::Div => "Div", + Self::Modulo => "Modulo", + Self::Eq => "Eq", + Self::NotEq => "NotEq", + Self::Lt => "Lt", + Self::Gt => "Gt", + Self::LtEq => "LtEq", + Self::GtEq => "GtEq", + Self::And => "And", + Self::Or => "Or", + } + } +} + +#[derive(Clone, Copy)] +struct NativeField { + bits: u64, + owned: bool, + pending_depth: u64, +} +struct NativeStruct { + fields: Vec>, + pending_depth: u64, +} +#[derive(Clone)] +struct NativeBuilderInfo { + owner: String, + metadata_owner: String, + unsupported_kind: Option, + variant: Option, + state: Option, + target_state: Option, + field_offset: usize, + field_names: Vec, + field_type_names: Vec, + field_types: Vec, + field_debug_layouts: Vec>, + put_order: Vec, + validation: Vec, +} +#[derive(Clone)] +enum BuilderFieldValidation { + None, + SignedBits(u32), + UnsignedBits(u32), + EnumBits(u32, String, Vec<(String, i64)>), +} +struct NativeList { + elements: Vec>, + element_pending_depths: HashMap, + owned: bool, + pending_depth: u64, +} +struct NativeSet { + elements: Vec>, + element_pending_depths: HashMap, + strings: bool, + pending_depth: u64, +} +#[derive(Clone, Copy)] +struct NativeMapEntry { + key: u64, + value: u64, + key_pending_depth: u64, + value_pending_depth: u64, + key_taken: bool, +} +struct NativeMap { + entries: Vec>, + key_strings: bool, + value_owned: bool, + pending_depth: u64, +} +struct NativeSum { + tag: u32, + bits: u64, + owned: bool, + pending_depth: u64, + payload_pending_depth: u64, +} +struct NativeBitfieldLayout { + name: String, + network_order: bool, + fields: Vec, +} +enum NativeBitfieldField { + Bits { + name: String, + width: u8, + enum_type: Option<(String, Vec)>, + }, + Payload { + name: String, + }, +} +enum DecodedBitfieldField { + Plain(u64), + Enum(u32), + Payload(Vec), +} +#[derive(Clone)] +struct NativeDebugLayout { + root: usize, + nodes: Vec, +} +#[derive(Clone)] +enum NativeDebugNode { + Primitive(u32), + Nothing, + Bytes, + List(usize), + Set(usize), + Map(usize, usize), + Optional(usize), + Result(usize, usize), + Record(String, Vec<(String, usize)>), + Enum(String, Vec<(String, Vec)>), + Machine(String, Vec<(String, Vec)>), + Capability(String), + Alias(usize), + Function, + Actor, + TypeConstruction, + Redacted, + Interface, + Uninhabited, +} +impl NativeDebugLayout { + fn parse(bytes: &[u8]) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { bytes, position: 0 }; + if cursor.take(3).map_err(|_| INVALID_TRACE_LABEL)? != b"JD\x01" { + return Err(INVALID_TRACE_LABEL); + } + let count = usize::try_from(cursor.u32().map_err(|_| INVALID_TRACE_LABEL)?) + .map_err(|_| INVALID_TRACE_LABEL)?; + let root = usize::try_from(cursor.u32().map_err(|_| INVALID_TRACE_LABEL)?) + .map_err(|_| INVALID_TRACE_LABEL)?; + if count > bytes.len() || root >= count { + return Err(INVALID_TRACE_LABEL); + } + let mut nodes = Vec::new(); + nodes.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + let length = usize::try_from(cursor.u32().map_err(|_| INVALID_TRACE_LABEL)?) + .map_err(|_| INVALID_TRACE_LABEL)?; + let node_bytes = cursor.take(length).map_err(|_| INVALID_TRACE_LABEL)?; + let mut node = BitfieldLayoutCursor { + bytes: node_bytes, + position: 0, + }; + let tag = NativeDebugTag::from_raw(node.byte().map_err(|_| INVALID_TRACE_LABEL)?)?; + let index = |node: &mut BitfieldLayoutCursor<'_>| -> LeafResult { + usize::try_from(node.u32().map_err(|_| INVALID_TRACE_LABEL)?) + .map_err(|_| INVALID_TRACE_LABEL) + }; + let name = |node: &mut BitfieldLayoutCursor<'_>| -> LeafResult { + node.name().map_err(|_| INVALID_TRACE_LABEL) + }; + let parsed = match tag { + NativeDebugTag::Primitive => { + NativeDebugNode::Primitive(node.u32().map_err(|_| INVALID_TRACE_LABEL)?) + } + NativeDebugTag::Nothing => NativeDebugNode::Nothing, + NativeDebugTag::Bytes => NativeDebugNode::Bytes, + NativeDebugTag::List => NativeDebugNode::List(index(&mut node)?), + NativeDebugTag::Set => NativeDebugNode::Set(index(&mut node)?), + NativeDebugTag::Map => NativeDebugNode::Map(index(&mut node)?, index(&mut node)?), + NativeDebugTag::Optional => NativeDebugNode::Optional(index(&mut node)?), + NativeDebugTag::Result => { + NativeDebugNode::Result(index(&mut node)?, index(&mut node)?) + } + NativeDebugTag::Record => { + let type_name = name(&mut node)?; + let fields = index(&mut node)?; + if fields > node_bytes.len() { + return Err(INVALID_TRACE_LABEL); + } + let mut definitions = Vec::new(); + definitions + .try_reserve_exact(fields) + .map_err(|_| EXHAUSTED)?; + for _ in 0..fields { + definitions.push((name(&mut node)?, index(&mut node)?)); + } + NativeDebugNode::Record(type_name, definitions) + } + NativeDebugTag::Enum | NativeDebugTag::Machine => { + let type_name = name(&mut node)?; + let count = index(&mut node)?; + if count > node_bytes.len() { + return Err(INVALID_TRACE_LABEL); + } + let mut variants = Vec::new(); + variants.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + let variant_name = name(&mut node)?; + let fields = index(&mut node)?; + if fields > node_bytes.len() / 4 { + return Err(INVALID_TRACE_LABEL); + } + let mut types = Vec::new(); + types.try_reserve_exact(fields).map_err(|_| EXHAUSTED)?; + for _ in 0..fields { + types.push(index(&mut node)?); + } + variants.push((variant_name, types)); + } + if tag == NativeDebugTag::Enum { + NativeDebugNode::Enum(type_name, variants) + } else { + NativeDebugNode::Machine(type_name, variants) + } + } + NativeDebugTag::Capability => NativeDebugNode::Capability(name(&mut node)?), + NativeDebugTag::Alias => NativeDebugNode::Alias(index(&mut node)?), + NativeDebugTag::Function => NativeDebugNode::Function, + NativeDebugTag::Actor => NativeDebugNode::Actor, + NativeDebugTag::TypeConstruction => NativeDebugNode::TypeConstruction, + NativeDebugTag::Redacted => NativeDebugNode::Redacted, + NativeDebugTag::Interface => NativeDebugNode::Interface, + NativeDebugTag::Uninhabited => NativeDebugNode::Uninhabited, + }; + if node.position != node_bytes.len() { + return Err(INVALID_TRACE_LABEL); + } + nodes.push(parsed); + } + if cursor.position != bytes.len() { + return Err(INVALID_TRACE_LABEL); + } + Ok(Self { root, nodes }) + } +} +struct BitfieldLayoutCursor<'a> { + bytes: &'a [u8], + position: usize, +} +impl BitfieldLayoutCursor<'_> { + fn take(&mut self, count: usize) -> LeafResult<&[u8]> { + let end = self + .position + .checked_add(count) + .ok_or(INVALID_BITFIELD_LAYOUT)?; + let value = self + .bytes + .get(self.position..end) + .ok_or(INVALID_BITFIELD_LAYOUT)?; + self.position = end; + Ok(value) + } + fn byte(&mut self) -> LeafResult { + Ok(self.take(1)?[0]) + } + fn u32(&mut self) -> LeafResult { + Ok(u32::from_le_bytes( + self.take(4)?.try_into().expect("four bytes"), + )) + } + fn i64(&mut self) -> LeafResult { + Ok(i64::from_le_bytes( + self.take(8)?.try_into().expect("eight bytes"), + )) + } + fn name(&mut self) -> LeafResult { + let length = usize::try_from(self.u32()?).map_err(|_| INVALID_BITFIELD_LAYOUT)?; + let bytes = self.take(length)?; + Ok(std::str::from_utf8(bytes) + .map_err(|_| INVALID_BITFIELD_LAYOUT)? + .to_owned()) + } +} +impl NativeBitfieldLayout { + fn parse(bytes: &[u8]) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { bytes, position: 0 }; + if cursor.take(3)? != b"JB\x01" { + return Err(INVALID_BITFIELD_LAYOUT); + } + let network_order = match cursor.byte()? { + 0 => false, + 1 => true, + _ => return Err(INVALID_BITFIELD_LAYOUT), + }; + let name = cursor.name()?; + let count = usize::try_from(cursor.u32()?).map_err(|_| INVALID_BITFIELD_LAYOUT)?; + if count > bytes.len() { + return Err(INVALID_BITFIELD_LAYOUT); + } + let mut fields = Vec::new(); + fields.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + let kind = cursor.byte()?; + let width = cursor.byte()?; + let field_name = cursor.name()?; + let field = match kind { + 0 if (1..=64).contains(&width) => NativeBitfieldField::Bits { + name: field_name, + width, + enum_type: None, + }, + 1 if (1..=64).contains(&width) => { + let enum_name = cursor.name()?; + let variant_count = + usize::try_from(cursor.u32()?).map_err(|_| INVALID_BITFIELD_LAYOUT)?; + if variant_count > bytes.len() / 8 { + return Err(INVALID_BITFIELD_LAYOUT); + } + let mut discriminants = Vec::new(); + discriminants + .try_reserve_exact(variant_count) + .map_err(|_| EXHAUSTED)?; + for _ in 0..variant_count { + discriminants.push(cursor.i64()?); + } + NativeBitfieldField::Bits { + name: field_name, + width, + enum_type: Some((enum_name, discriminants)), + } + } + 2 if width == 0 => NativeBitfieldField::Payload { name: field_name }, + _ => return Err(INVALID_BITFIELD_LAYOUT), + }; + fields.push(field); + } + if cursor.position != bytes.len() { + return Err(INVALID_BITFIELD_LAYOUT); + } + Ok(Self { + name, + network_order, + fields, + }) + } + fn decode(&self, bytes: &[u8]) -> Result, String> { + let mut bit_index = 0_usize; + let mut fields = Vec::with_capacity(self.fields.len()); + for field in &self.fields { + match field { + NativeBitfieldField::Bits { + name, + width, + enum_type, + } => { + let width = *width as usize; + let numeric = if width > 8 + && width % 8 == 0 + && !self.network_order + && bit_index % 8 == 0 + { + let end = bit_index / 8 + width / 8; + if end > bytes.len() { + return Err(format!( + "bitfield '{}.from_bytes' expected at least {} byte(s), got {}", + self.name, + end, + bytes.len() + )); + } + let mut numeric = 0_u64; + for (shift, byte) in bytes[bit_index / 8..end].iter().enumerate() { + numeric |= u64::from(*byte) << (shift * 8); + } + bit_index += width; + numeric + } else { + let mut numeric = 0_u64; + for _ in 0..width { + if bit_index / 8 >= bytes.len() { + return Err(format!( + "bitfield '{}.from_bytes' expected {} bit(s), got {} byte(s)", + self.name, + bit_index + width, + bytes.len() + )); + } + let bit = (bytes[bit_index / 8] >> (7 - bit_index % 8)) & 1; + numeric = (numeric << 1) | u64::from(bit); + bit_index += 1; + } + numeric + }; + if let Some((enum_name, discriminants)) = enum_type { + let variant = discriminants + .iter() + .position(|candidate| *candidate >= 0 && *candidate as u64 == numeric); + let Some(variant) = variant else { + return Err(format!( + "bitfield '{}' field '{}': enum '{}' has no variant for value {}", + self.name, name, enum_name, numeric + )); + }; + fields.push(DecodedBitfieldField::Enum(variant as u32)); + } else { + fields.push(DecodedBitfieldField::Plain(numeric)); + } + } + NativeBitfieldField::Payload { name } => { + if bit_index % 8 != 0 { + return Err(format!( + "bitfield '{}' payload field '{}' must begin on a byte boundary", + self.name, name + )); + } + fields.push(DecodedBitfieldField::Payload( + bytes[bit_index / 8..].to_vec(), + )); + bit_index = bytes.len() * 8; + } + } + } + let consumed = bit_index.div_ceil(8); + if consumed != bytes.len() { + return Err(format!( + "bitfield '{}.from_bytes' expected {} byte(s), got {}", + self.name, + consumed, + bytes.len() + )); + } + Ok(fields) + } +} +#[derive(Clone, Copy)] +struct NativeActorIdentity { + instance: NativeHandle, + ordinal: u64, +} + +#[derive(Default)] +pub(super) struct NativeValues { + #[cfg(all(test, jett_resource_native_test_archive))] + pub(in crate::native_abi) test_observations: resource::test_observations::Recorder, + #[cfg(test)] + allocation_budget: Option, + strings: HashMap, + bytes: HashMap>, + bytes_pending: HashMap, + sums: HashMap, + lists: HashMap, + sets: HashMap, + maps: HashMap, + structs: HashMap, + equalities: HashMap, + actors: HashMap, + actor_graph_imports: HashMap, + checked_graph_hooks: HashMap, + checked_graph_hooks_created: u64, + checked_graph_hooks_destroyed: u64, + capability_tasks: HashMap, + opaque_capabilities: HashMap<&'static str, u64>, + next_actor_ordinal: u64, + builders: HashMap, + structs_created: u64, + structs_destroyed: u64, + lists_created: u64, + lists_destroyed: u64, + sets_created: u64, + sets_destroyed: u64, + maps_created: u64, + maps_destroyed: u64, + sums_created: u64, + sums_destroyed: u64, + bytes_created: u64, + bytes_destroyed: u64, + resource_ordinary_channels: ordinary_attempts::ResourceOrdinaryChannels, + failure: Option, + dynamic_failure_message: Option>, + // Diagnostic bytes only; this metadata retains no owned Jett values. + property_case_context: Option>, + pub(super) cleanup_failed: bool, + stdout: Option, + clock: Option, + clock_script: Option>, + random: Option, + random_provider: Option, + environment: Option, + environment_snapshot: Option, + graphics: Option, + graphics_script: Option>, + graphics_session: Option, +} +impl NativeValues { + // These capability families currently have no interpreter I/O provider. + // Their checked values still support moves, views, calls, and task wrapping. + fn grant_opaque_capability(&mut self, kind: &'static str) -> LeafResult { + if let Some(&authority) = self.opaque_capabilities.get(kind) { + return Ok(authority); + } + let authority = next_identity()?; + self.opaque_capabilities.insert(kind, authority); + Ok(authority) + } + + fn capability_authority(&self, value: u64) -> LeafResult<(u64, u64)> { + let (authority, depth) = self + .capability_tasks + .get(&value) + .copied() + .unwrap_or((value, 0)); + if [ + self.stdout, + self.clock, + self.random, + self.environment, + self.graphics, + ] + .into_iter() + .flatten() + .chain(self.opaque_capabilities.values().copied()) + .any(|token| token == authority) + { + Ok((authority, depth)) + } else { + Err(INVALID_CAPABILITY) + } + } + + fn run_capability(&mut self, value: u64) -> LeafResult { + let (authority, depth) = self.capability_authority(value)?; + let depth = depth.checked_add(1).ok_or(EXHAUSTED)?; + let task = next_identity()?; + self.capability_tasks.insert(task, (authority, depth)); + self.capability_tasks.remove(&value); + Ok(task) + } + + fn join_capability(&mut self, value: u64) -> LeafResult { + let (authority, depth) = self.capability_authority(value)?; + if depth == 0 && self.environment != Some(authority) && self.graphics != Some(authority) { + return self.parsed_sum(Err("task was cancelled".to_owned())); + } + let joined = if depth <= 1 { + authority + } else { + next_identity()? + }; + let result = self.parsed_sum(Ok(joined))?; + if depth > 1 { + self.capability_tasks.insert(joined, (authority, depth - 1)); + } + self.capability_tasks.remove(&value); + Ok(result) + } + + fn debug_capability(&self, value: u64, name: &str) -> LeafResult { + let (authority, depth) = self.capability_authority(value)?; + let inner = if self.environment == Some(authority) || self.graphics == Some(authority) { + format!("<{name} capability>") + } else { + "nothing".to_owned() + }; + format_pending_value(&inner, depth) + } + + fn validate_scripted_consumption(&mut self) -> LeafResult { + let remaining = [ + ( + "Random", + self.random_provider + .as_ref() + .and_then(RandomProvider::scripted_samples_remaining), + ), + ( + "Clock", + self.clock_script.as_ref().map(|samples| samples.len()), + ), + ( + "Graphics", + self.graphics_script.as_ref().map(|events| events.len()), + ), + ] + .into_iter() + .find_map(|(capability, remaining)| { + remaining + .filter(|count| *count != 0) + .map(|count| (capability, count)) + }); + let Some((capability, remaining)) = remaining else { + return Ok(0); + }; + let suffix = if remaining == 1 { "sample" } else { "samples" }; + self.set_dynamic_failure_message( + format!("{capability}: test provider has {remaining} unconsumed {suffix}").into_bytes(), + ); + Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"test provider has unconsumed samples", + )) + } + #[cfg(test)] + fn allocation_checkpoint(&mut self) -> LeafResult<()> { + if let Some(budget) = &mut self.allocation_budget { + *budget = budget.checked_sub(1).ok_or(EXHAUSTED)?; + } + Ok(()) + } + pub(super) fn is_empty(&self) -> bool { + self.structs.is_empty() + && self.equalities.is_empty() + && self.actors.is_empty() + && self.actor_graph_imports.is_empty() + && self.checked_graph_hooks.is_empty() + && self.checked_graph_hooks_created == self.checked_graph_hooks_destroyed + && self.builders.is_empty() + && self.structs_created == self.structs_destroyed + && self.strings.is_empty() + && self.bytes.is_empty() + && self.bytes_pending.is_empty() + && self.bytes_created == self.bytes_destroyed + && self.sums.is_empty() + && self.sums_created == self.sums_destroyed + && self.lists.is_empty() + && self.lists_created == self.lists_destroyed + && self.sets.is_empty() + && self.sets_created == self.sets_destroyed + && self.maps.is_empty() + && self.maps_created == self.maps_destroyed + && self.graphics_session.is_none() + } + fn insert(&mut self, text: String) -> LeafResult { + self.insert_pending(text, 0) + } + fn insert_pending(&mut self, text: String, pending_depth: u64) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let id = next_identity()?; + self.strings.insert( + id, + NativeString { + text, + references: 1, + pending_depth, + }, + ); + Ok(id) + } + fn run_string(&mut self, id: u64) -> LeafResult { + let value = self.strings.get(&id).ok_or(INVALID_HANDLE)?; + let depth = value.pending_depth.checked_add(1).ok_or(EXHAUSTED)?; + self.insert_pending(value.text.clone(), depth) + } + fn join_string(&mut self, id: u64) -> LeafResult { + let value = self.strings.get(&id).ok_or(INVALID_HANDLE)?; + self.insert_pending(value.text.clone(), value.pending_depth.saturating_sub(1)) + } + fn equal_string(&mut self, left: u64, right: u64, not_equal: bool) -> LeafResult { + let left_value = self.strings.get(&left).ok_or(INVALID_HANDLE)?; + let right_value = self.strings.get(&right).ok_or(INVALID_HANDLE)?; + if left_value.pending_depth != 0 || right_value.pending_depth != 0 { + let left = self.format_string(left)?; + let right = self.format_string(right)?; + let operation = if not_equal { "NotEq" } else { "Eq" }; + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); + return Err(UNSUPPORTED_STRING_COMPARISON); + } + let equal = self.same_string_value(left, right)?; + Ok(u32::from(if not_equal { !equal } else { equal })) + } + fn check_enum_comparison( + &mut self, + left: u64, + right: u64, + not_equal: u32, + layout: &[u8], + ) -> LeafResult { + if not_equal > 1 { + return Err(INVALID_STRUCT); + } + let left_depth = self.structs.get(&left).ok_or(INVALID_STRUCT)?.pending_depth; + let right_depth = self + .structs + .get(&right) + .ok_or(INVALID_STRUCT)? + .pending_depth; + if left_depth == 0 && right_depth == 0 { + return Ok(0); + } + let layout = NativeDebugLayout::parse(layout).map_err(|_| INVALID_STRUCT)?; + if !matches!( + layout.nodes.get(layout.root), + Some(NativeDebugNode::Enum(..)) + ) { + return Err(INVALID_STRUCT); + } + let left = layout.format_value(self, left, layout.root)?; + let right = layout.format_value(self, right, layout.root)?; + let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); + Err(UNSUPPORTED_ENUM_COMPARISON) + } + fn same_string_value(&self, left: u64, right: u64) -> LeafResult { + let left = self.strings.get(&left).ok_or(INVALID_HANDLE)?; + let right = self.strings.get(&right).ok_or(INVALID_HANDLE)?; + Ok(left.pending_depth == right.pending_depth && left.text == right.text) + } + fn format_string(&self, id: u64) -> LeafResult { + let value = self.strings.get(&id).ok_or(INVALID_HANDLE)?; + format_pending_value(&value.text, value.pending_depth) + } + fn check_display_result(&mut self, id: u64) -> LeafResult { + let value = self.strings.get(&id).ok_or(INVALID_HANDLE)?; + if value.pending_depth == 0 { + return Ok(0); + } + let value = self.format_string(id)?; + self.set_dynamic_failure_message( + format!("Displayable.display returned {value} instead of string").into_bytes(), + ); + Err(INVALID_DISPLAY_RESULT) + } + fn display_string(&mut self, id: u64) -> LeafResult { + let depth = self.strings.get(&id).ok_or(INVALID_HANDLE)?.pending_depth; + if depth == 0 { + self.retain(id) + } else { + let text = self.format_string(id)?; + self.insert(text) + } + } + fn debug_value(&self, bits: u64, kind: u32) -> LeafResult { + if kind == DEBUG_NOTHING_KIND { + return format_nothing(bits); + } + if kind == DEBUG_BYTES_KIND { + let mut text = "bytes(".to_owned(); + for (index, byte) in self.bytes(bits)?.iter().enumerate() { + if index != 0 { + text.push_str(", "); + } + text.push_str(&byte.to_string()); + } + text.push(')'); + return format_pending_value(&text, self.bytes_depth(bits)?); + } + let kind = NativeSortKind::from_raw(kind).map_err(|_| INVALID_TRACE_LABEL)?; + Ok(match kind { + NativeSortKind::Int8 => (bits as u8 as i8).to_string(), + NativeSortKind::Int16 => (bits as u16 as i16).to_string(), + NativeSortKind::Int32 => (bits as u32 as i32).to_string(), + NativeSortKind::Int64 => (bits as i64).to_string(), + NativeSortKind::Uint8 => (bits as u8).to_string(), + NativeSortKind::Uint16 => (bits as u16).to_string(), + NativeSortKind::Uint32 => (bits as u32).to_string(), + NativeSortKind::Uint64 => bits.to_string(), + NativeSortKind::Float32 => f64::from(f32::from_bits(bits as u32)).to_string(), + NativeSortKind::Float64 => f64::from_bits(bits).to_string(), + NativeSortKind::Bool => match bits { + 0 => "false".to_owned(), + 1 => "true".to_owned(), + _ => return Err(INVALID_TRACE_LABEL), + }, + NativeSortKind::String => self.format_string(bits)?, + }) + } + fn function_debug_label(&self, id: u64) -> LeafResult<&str> { + let descriptor = self.structs.get(&id).ok_or(INVALID_FUNCTION)?; + let field_count = + usize::try_from(NATIVE_FUNCTION_FIELD_COUNT).map_err(|_| INVALID_FUNCTION)?; + if descriptor.fields.len() != field_count { + return Err(INVALID_FUNCTION); + } + let code = self + .struct_field(id, NATIVE_FUNCTION_CODE_FIELD) + .map_err(|_| INVALID_FUNCTION)?; + let environment = self + .struct_field(id, NATIVE_FUNCTION_ENVIRONMENT_FIELD) + .map_err(|_| INVALID_FUNCTION)?; + let label = self + .struct_field(id, NATIVE_FUNCTION_LABEL_FIELD) + .map_err(|_| INVALID_FUNCTION)?; + if code.owned + || code.bits == 0 + || environment.owned != (environment.bits != 0) + || (environment.bits != 0 && !self.structs.contains_key(&environment.bits)) + || !label.owned + { + return Err(INVALID_FUNCTION); + } + let text = self.text(label.bits).map_err(|_| INVALID_FUNCTION)?; + if !text.starts_with("function(") + || !text.ends_with(')') + || text.chars().any(char::is_control) + { + return Err(INVALID_FUNCTION); + } + Ok(text) + } + fn check_function_callable(&mut self, id: u64) -> LeafResult { + let label = self.function_debug_label(id)?.to_owned(); + let depth = self.structs.get(&id).ok_or(INVALID_FUNCTION)?.pending_depth; + if depth == 0 { + return Ok(0); + } + let pending = format_pending_value(&label, depth)?; + self.set_dynamic_failure_message( + format!("expected function value, got {pending}").into_bytes(), + ); + Err(PENDING_FUNCTION_CALL) + } + fn debug_append(&mut self, builder: u64, label: &str, bits: u64, kind: u32) -> LeafResult { + let value = self.debug_value(bits, kind)?; + let text = &mut self.strings.get_mut(&builder).ok_or(INVALID_HANDLE)?.text; + text.push_str(label); + text.push_str(&value); + Ok(0) + } + fn debug_append_pending_scalar( + &mut self, + builder: u64, + label: &str, + bits: u64, + kind: u32, + depth: u64, + ) -> LeafResult { + let value = self.debug_value(bits, kind)?; + let value = format_pending_value(&value, depth)?; + let text = &mut self.strings.get_mut(&builder).ok_or(INVALID_HANDLE)?.text; + text.push_str(label); + text.push_str(&value); + Ok(0) + } + fn debug_append_aggregate( + &mut self, + builder: u64, + label: &str, + bits: u64, + layout: &[u8], + ) -> LeafResult { + let layout = NativeDebugLayout::parse(layout)?; + let value = layout.format_value(self, bits, layout.root)?; + let text = &mut self.strings.get_mut(&builder).ok_or(INVALID_HANDLE)?.text; + text.push_str(label); + text.push_str(&value); + Ok(0) + } + /// Observe the selected ordinary payload without acquiring ownership. + /// Its exact type and stable backing belong to the checked MIR projection. + fn sum_payload_borrow(&self, value: u64, tag: u32) -> LeafResult { + let sum = self.sums.get(&value).ok_or(INVALID_SUM)?; + if tag > SUM_SUCCESS || sum.tag != tag || sum.pending_depth != 0 { + return Err(INVALID_SUM); + } + Ok(sum.bits) + } + fn sum_handle_tag(&mut self, value: u64, layout: &[u8]) -> LeafResult { + let sum = self.sums.get(&value).ok_or(INVALID_SUM)?; + let tag = sum.tag; + let pending = sum.pending_depth != 0; + if !pending { + return Ok(tag); + } + let layout = NativeDebugLayout::parse(layout)?; + let value = layout.format_value(self, value, layout.root)?; + self.set_dynamic_failure_message( + format!("handle block requires a result or optional value, got {value}").into_bytes(), + ); + Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"cannot handle a pending sum", + )) + } + fn debug_emit(&mut self, builder: u64) -> LeafResult { + let mut stderr = io::stderr().lock(); + write_all_bytes(&mut stderr, self.text(builder)?.as_bytes()) + .and_then(|_| write_all_bytes(&mut stderr, b"\n")) + .and_then(|_| stderr.flush()) + .map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDERR_WRITE_MESSAGE))?; + self.release(builder) + } + fn insert_bytes(&mut self, bytes: Vec) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let id = next_identity()?; + self.bytes.insert(id, bytes); + self.bytes_created += 1; + Ok(id) + } + fn bytes(&self, id: u64) -> LeafResult<&[u8]> { + self.bytes.get(&id).map(Vec::as_slice).ok_or(INVALID_BYTES) + } + fn bytes_depth(&self, id: u64) -> LeafResult { + self.bytes(id)?; + Ok(self.bytes_pending.get(&id).copied().unwrap_or(0)) + } + fn copy_bytes_with_depth(&mut self, id: u64, depth: u64) -> LeafResult { + let data = self.bytes(id)?.to_vec(); + let copy = self.insert_bytes(data)?; + if depth != 0 { + self.bytes_pending.insert(copy, depth); + } + Ok(copy) + } + fn clone_bytes(&mut self, id: u64) -> LeafResult { + let depth = self.bytes_depth(id)?; + self.copy_bytes_with_depth(id, depth) + } + fn run_bytes(&mut self, id: u64) -> LeafResult { + let depth = self.bytes_depth(id)?.checked_add(1).ok_or(EXHAUSTED)?; + self.copy_bytes_with_depth(id, depth) + } + fn join_bytes(&mut self, id: u64) -> LeafResult { + let depth = self.bytes_depth(id)?.saturating_sub(1); + self.copy_bytes_with_depth(id, depth) + } + fn same_bytes_value(&self, left: u64, right: u64) -> LeafResult { + Ok(self.bytes_depth(left)? == self.bytes_depth(right)? + && self.bytes(left)? == self.bytes(right)?) + } + fn write_bitfield_bits( + &mut self, + id: u64, + numeric: u64, + width: u32, + network_order: u32, + bit_offset: u64, + ) -> LeafResult { + if !(1..=64).contains(&width) + || network_order > 1 + || (width < 64 && numeric >= (1_u64 << width)) + { + return Err(INVALID_STRUCT); + } + let offset = usize::try_from(bit_offset).map_err(|_| EXHAUSTED)?; + let end = offset.checked_add(width as usize).ok_or(EXHAUSTED)?; + let byte_count = end.checked_add(7).ok_or(EXHAUSTED)? / 8; + let bytes = self.bytes.get_mut(&id).ok_or(INVALID_BYTES)?; + if byte_count > bytes.len() { + bytes + .try_reserve_exact(byte_count - bytes.len()) + .map_err(|_| EXHAUSTED)?; + bytes.resize(byte_count, 0); + } + let little_endian_bytes = + width > 8 && width % 8 == 0 && network_order == 0 && offset % 8 == 0; + for bit in 0..width as usize { + let shift = if little_endian_bytes { + (bit / 8) * 8 + (7 - bit % 8) + } else { + width as usize - 1 - bit + }; + if (numeric >> shift) & 1 != 0 { + let position = offset + bit; + bytes[position / 8] |= 1 << (7 - position % 8); + } + } + Ok(0) + } + fn extend_bitfield_payload(&mut self, id: u64, payload: u64) -> LeafResult { + let source = self.lists.get(&payload).ok_or(INVALID_LIST)?; + if source.owned + || source + .elements + .iter() + .any(|element| element.is_none_or(|value| value > u8::MAX as u64)) + { + return Err(INVALID_LIST); + } + let bytes = self.bytes.get_mut(&id).ok_or(INVALID_BYTES)?; + bytes + .try_reserve_exact(source.elements.len()) + .map_err(|_| EXHAUSTED)?; + for element in &source.elements { + bytes.push(element.expect("validated payload element") as u8); + } + Ok(0) + } + fn check_bitfield_payload_pending(&mut self, payload: u64, prefix: &[u8]) -> LeafResult { + let source = self.lists.get(&payload).ok_or(INVALID_LIST)?; + if source.owned { + return Err(INVALID_LIST); + } + let message = if source.pending_depth != 0 { + let mut values = Vec::new(); + values + .try_reserve_exact(source.elements.len()) + .map_err(|_| EXHAUSTED)?; + for (index, element) in source.elements.iter().enumerate() { + let element = element.ok_or(INVALID_LIST)?; + let depth = source + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0); + values.push(format_pending_value(&element.to_string(), depth)?); + } + let list = format!("list({})", values.join(", ")); + let value = format_pending_value(&list, source.pending_depth)?; + Some(format!( + "payload expects list[uint8] or bytes, found {value}" + )) + } else { + let pending = source + .elements + .iter() + .enumerate() + .find_map(|(index, element)| { + let depth = source + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0); + (depth != 0).then_some((element, depth)) + }); + if let Some((element, depth)) = pending { + let element = element.ok_or(INVALID_LIST)?; + let value = format_pending_value(&element.to_string(), depth)?; + Some(format!("payload expects list[uint8], found {value}")) + } else { + None + } + }; + if let Some(message) = message { + let mut full = prefix.to_vec(); + full.extend_from_slice(message.as_bytes()); + self.set_dynamic_failure_message(full); + return Err(INVALID_PENDING_HANDLE_CHECK); + } + Ok(0) + } + fn decode_bitfield(&mut self, input: u64, descriptor: &[u8]) -> LeafResult { + let layout = NativeBitfieldLayout::parse(descriptor)?; + let decoded = layout.decode(self.bytes(input)?); + match decoded { + Err(message) => { + let text = self.insert(message)?; + match self.sum(SUM_FAILURE, text, true) { + Ok(result) => Ok(result), + Err(error) => { + self.drop_value(text)?; + Err(error) + } + } + } + Ok(fields) => { + let record = self.new_struct(fields.len() as u64)?; + let materialize = (|| -> LeafResult<()> { + for (index, field) in fields.into_iter().enumerate() { + let value = match field { + DecodedBitfieldField::Plain(bits) => NativeField { + bits, + owned: false, + pending_depth: 0, + }, + DecodedBitfieldField::Enum(variant) => { + let nested = self.new_struct(1)?; + self.structs.get_mut(&nested).ok_or(INVALID_STRUCT)?.fields[0] = + Some(NativeField { + bits: u64::from(variant), + owned: false, + pending_depth: 0, + }); + NativeField { + bits: nested, + owned: true, + pending_depth: 0, + } + } + DecodedBitfieldField::Payload(bytes) => { + let mut elements = Vec::new(); + elements + .try_reserve_exact(bytes.len()) + .map_err(|_| EXHAUSTED)?; + elements + .extend(bytes.into_iter().map(|byte| Some(u64::from(byte)))); + let list = self.new_list(false)?; + self.lists.get_mut(&list).ok_or(INVALID_LIST)?.elements = elements; + NativeField { + bits: list, + owned: true, + pending_depth: 0, + } + } + }; + self.structs.get_mut(&record).ok_or(INVALID_STRUCT)?.fields[index] = + Some(value); + } + Ok(()) + })(); + if let Err(error) = materialize { + self.drop_value(record)?; + return Err(error); + } + match self.sum(SUM_SUCCESS, record, true) { + Ok(result) => Ok(result), + Err(error) => { + self.drop_value(record)?; + Err(error) + } + } + } + } + } + fn sum(&mut self, tag: u32, bits: u64, owned: bool) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + if tag > SUM_SUCCESS { + return Err(INVALID_SUM); + } + let id = next_identity()?; + self.sums.insert( + id, + NativeSum { + tag, + bits, + owned, + pending_depth: 0, + payload_pending_depth: 0, + }, + ); + self.sums_created += 1; + Ok(id) + } + fn parsed_sum(&mut self, parsed: Result) -> LeafResult { + match parsed { + Ok(bits) => self.sum(SUM_SUCCESS, bits, false), + Err(error) => { + let payload = self.insert(error)?; + match self.sum(SUM_FAILURE, payload, true) { + Ok(value) => Ok(value), + Err(error) => { + self.drop_value(payload)?; + Err(error) + } + } + } + } + } + fn join_nothing(&mut self, depth: u64) -> LeafResult { + self.parsed_sum( + depth + .checked_sub(1) + .ok_or_else(|| "task was cancelled".to_owned()), + ) + } + fn equal_nothing(&mut self, left: u64, right: u64, not_equal: u32) -> LeafResult { + if not_equal > 1 { + return Err(INVALID_NOTHING_COMPARISON); + } + if left == 0 && right == 0 { + return Ok(u32::from(not_equal == 0)); + } + let left = format_nothing(left)?; + let right = format_nothing(right)?; + let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); + Err(UNSUPPORTED_NOTHING_COMPARISON) + } + fn check_pending_scalar_binary( + &mut self, + left_bits: u64, + left_kind: u32, + left_depth: u64, + right_bits: u64, + right_kind: u32, + right_depth: u64, + operation: u32, + ) -> LeafResult { + let operation = NativePendingBinaryOp::from_raw(operation)?; + if left_depth == 0 && right_depth == 0 { + return Ok(0); + } + let left = format_pending_value(&self.debug_value(left_bits, left_kind)?, left_depth)?; + let right = format_pending_value(&self.debug_value(right_bits, right_kind)?, right_depth)?; + self.set_dynamic_failure_message( + format!( + "unsupported binary operation: {left} {} {right}", + operation.label() + ) + .into_bytes(), + ); + Err(UNSUPPORTED_SCALAR_BINARY) + } + fn check_pending_bool_condition( + &mut self, + bits: u64, + depth: u64, + context: u32, + ) -> LeafResult { + if bits > 1 { + return Err(INVALID_TRACE_LABEL); + } + if depth == 0 { + return Ok(0); + } + let value = format_pending_value(if bits == 0 { "false" } else { "true" }, depth)?; + let message = match context { + 0 => format!("expected boolean, got {value}"), + 1 => "assert condition must be a boolean".to_owned(), + 2 => format!("breakpoint condition must be bool, got {value}"), + _ => return Err(INVALID_TRACE_LABEL), + }; + self.set_dynamic_failure_message(message.into_bytes()); + Err(PENDING_BOOL_CONDITION) + } + fn byte_result(&mut self, decoded: Result, String>) -> LeafResult { + let (tag, payload) = match decoded { + Ok(bytes) => (SUM_SUCCESS, self.insert_bytes(bytes)?), + Err(error) => (SUM_FAILURE, self.insert(error)?), + }; + match self.sum(tag, payload, true) { + Ok(value) => Ok(value), + Err(error) => { + self.drop_value(payload)?; + Err(error) + } + } + } + fn string_result(&mut self, decoded: Result) -> LeafResult { + let (tag, payload) = match decoded { + Ok(text) => (SUM_SUCCESS, self.insert(text)?), + Err(error) => (SUM_FAILURE, self.insert(error)?), + }; + match self.sum(tag, payload, true) { + Ok(value) => Ok(value), + Err(error) => { + self.drop_value(payload)?; + Err(error) + } + } + } + fn owned_sum(&mut self, tag: u32, payload: u64) -> LeafResult { + match self.sum(tag, payload, true) { + Ok(value) => Ok(value), + Err(error) => { + self.drop_value(payload)?; + Err(error) + } + } + } + fn push_owned_list_element(&mut self, id: u64, element: u64) -> LeafResult<()> { + let result = self + .lists + .get_mut(&id) + .ok_or(INVALID_LIST) + .and_then(|list| { + if !list.owned { + return Err(INVALID_LIST); + } + list.elements.try_reserve(1).map_err(|_| EXHAUSTED)?; + list.elements.push(Some(element)); + Ok(()) + }); + if result.is_err() { + self.drop_value(element)?; + } + result + } + fn csv_rows(&mut self, rows: Vec>) -> LeafResult { + let outer = self.new_list(true)?; + for row in rows { + let result = self + .string_list(row) + .and_then(|inner| self.push_owned_list_element(outer, inner)); + if let Err(error) = result { + self.drop_value(outer)?; + return Err(error); + } + } + Ok(outer) + } + fn csv_string_pairs(&mut self, entries: Vec<(String, String)>) -> LeafResult { + let map = self.new_map(1, 1)?; + for (key, value) in entries { + let pair = (|| { + let key = self.insert(key)?; + let value = match self.insert(value) { + Ok(value) => value, + Err(error) => { + self.drop_value(key)?; + return Err(error); + } + }; + if let Err(error) = self.map_append_literal(map, key, value) { + self.drop_value(key)?; + self.drop_value(value)?; + return Err(error); + } + Ok(()) + })(); + if let Err(error) = pair { + self.drop_value(map)?; + return Err(error); + } + } + Ok(map) + } + fn csv_header_rows(&mut self, rows: Vec>) -> LeafResult { + let outer = self.new_list(true)?; + for entries in rows { + let result = self + .csv_string_pairs(entries) + .and_then(|map| self.push_owned_list_element(outer, map)); + if let Err(error) = result { + self.drop_value(outer)?; + return Err(error); + } + } + Ok(outer) + } + fn csv_records(&mut self, rows: u64) -> LeafResult>> { + let outer = self.lists.get(&rows).ok_or(INVALID_LIST)?; + if !outer.owned { + return Err(INVALID_LIST); + } + let mut records = Vec::new(); + records + .try_reserve_exact(outer.elements.len()) + .map_err(|_| EXHAUSTED)?; + for row in &outer.elements { + let inner = self + .lists + .get(&row.ok_or(INVALID_LIST)?) + .ok_or(INVALID_LIST)?; + if !inner.owned { + return Err(INVALID_LIST); + } + if inner.pending_depth != 0 { + self.set_dynamic_failure_message( + b"csv.__stringify expects list[list[string]]".to_vec(), + ); + return Err(INVALID_PENDING_HANDLE_CHECK); + } + let mut fields = Vec::new(); + fields + .try_reserve_exact(inner.elements.len()) + .map_err(|_| EXHAUSTED)?; + for field in &inner.elements { + let field = self + .strings + .get(&field.ok_or(INVALID_LIST)?) + .ok_or(INVALID_HANDLE)?; + fields.push(format_pending_value(&field.text, field.pending_depth)?); + } + records.push(fields); + } + Ok(records) + } + fn new_list(&mut self, owned: bool) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let id = next_identity()?; + self.lists.insert( + id, + NativeList { + elements: Vec::new(), + element_pending_depths: HashMap::new(), + owned, + pending_depth: 0, + }, + ); + self.lists_created += 1; + Ok(id) + } + fn list_index_failure( + &mut self, + operation: &'static str, + index: i64, + fallback: Failure, + ) -> LeafResult { + self.set_dynamic_failure_message( + format!("{operation}: index {index} out of bounds").into_bytes(), + ); + Err(fallback) + } + fn check_list_arguments( + &self, + value: u64, + kind: u32, + first_depth: u64, + second_depth: u64, + ) -> LeafResult { + let message: &'static [u8] = match kind { + 0 => b"list.__length expects a list argument", + 1 => b"list.__append expects a list as first argument", + 2 => b"list.__get_clone expects a list and an int64 index", + 3 => b"list.__insert_at expects a list, an int64 index, and a value", + 4 => b"list.__remove_at expects a list and an int64 index", + 5 => b"list.__sort expects a list argument", + 6 => b"list.__sort_by_index expects a list of lists and an int64 index", + 7 => b"list.__is_sorted expects a list argument", + 8 => b"list.__swap expects a list and two int64 indices", + _ => return Err(INVALID_LIST), + }; + let list = self.lists.get(&value).ok_or(INVALID_LIST)?; + if list.pending_depth != 0 || first_depth != 0 || second_depth != 0 { + return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, message)); + } + Ok(0) + } + fn check_index_count_arguments( + &self, + value: u64, + kind: u32, + first_depth: u64, + second_depth: u64, + third_depth: u64, + ) -> LeafResult { + let (receiver_depth, message): (u64, &'static [u8]) = match kind { + 0 => (0, b"range expects int64 arguments"), + 1 => ( + self.bytes_depth(value)?, + b"bytes.__get expects a bytes value and an int64 index", + ), + 2 => ( + self.bytes_depth(value)?, + b"bytes.__slice expects a bytes value and two int64 indices", + ), + 3 => ( + self.strings + .get(&value) + .ok_or(INVALID_HANDLE)? + .pending_depth, + b"string.__slice expects a string and two int64 indices", + ), + 4 => ( + self.strings + .get(&value) + .ok_or(INVALID_HANDLE)? + .pending_depth, + b"string.__repeat expects a string and an int64", + ), + _ => return Err(INVALID_LIST), + }; + if receiver_depth != 0 || first_depth != 0 || second_depth != 0 || third_depth != 0 { + return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, message)); + } + Ok(0) + } + fn reject_pending_scalars( + &mut self, + first_depth: u64, + second_depth: u64, + third_depth: u64, + message: &[u8], + ) -> LeafResult { + if first_depth == 0 && second_depth == 0 && third_depth == 0 { + return Ok(0); + } + self.set_dynamic_failure_message(message.to_vec()); + Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"pending scalar intrinsic operand", + )) + } + fn reject_pending_handle(&mut self, handle: u64, kind: u32, message: &[u8]) -> LeafResult { + let depth = match kind { + 0 => { + self.strings + .get(&handle) + .ok_or(INVALID_HANDLE)? + .pending_depth + } + 1 => self.bytes_depth(handle)?, + 2 => self.lists.get(&handle).ok_or(INVALID_LIST)?.pending_depth, + 3 => self.capability_authority(handle)?.1, + 4 => { + self.structs + .get(&handle) + .ok_or(INVALID_STRUCT)? + .pending_depth + } + 5 => self.sets.get(&handle).ok_or(INVALID_SET)?.pending_depth, + 6 => self.maps.get(&handle).ok_or(INVALID_MAP)?.pending_depth, + 7 => self.sums.get(&handle).ok_or(INVALID_SUM)?.pending_depth, + _ => return Err(INVALID_PENDING_HANDLE_CHECK), + }; + if depth == 0 { + return Ok(0); + } + self.set_dynamic_failure_message(message.to_vec()); + Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"pending handle intrinsic operand", + )) + } + fn new_set(&mut self, strings: u32) -> LeafResult { + if strings > 1 { + return Err(INVALID_SET); + } + #[cfg(test)] + self.allocation_checkpoint()?; + let id = next_identity()?; + self.sets.insert( + id, + NativeSet { + elements: Vec::new(), + element_pending_depths: HashMap::new(), + strings: strings != 0, + pending_depth: 0, + }, + ); + self.sets_created += 1; + Ok(id) + } + fn new_map(&mut self, key_strings: u32, value_owned: u32) -> LeafResult { + if key_strings > 1 || value_owned > 1 { + return Err(INVALID_MAP); + } + #[cfg(test)] + self.allocation_checkpoint()?; + let id = next_identity()?; + self.maps.insert( + id, + NativeMap { + entries: Vec::new(), + key_strings: key_strings != 0, + value_owned: value_owned != 0, + pending_depth: 0, + }, + ); + self.maps_created += 1; + Ok(id) + } + fn map_position(&self, id: u64, key: u64) -> LeafResult> { + self.map_position_depth(id, key, 0) + } + fn require_plain_map(&self, id: u64, error: Failure) -> LeafResult<()> { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + if map.pending_depth != 0 { + return Err(error); + } + Ok(()) + } + fn map_position_depth(&self, id: u64, key: u64, depth: u64) -> LeafResult> { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + if map.key_strings { + if depth != 0 { + return Err(INVALID_MAP); + } + for (index, entry) in map.entries.iter().enumerate() { + if let Some(entry) = entry { + if !entry.key_taken && self.same_string_value(entry.key, key)? { + return Ok(Some(index)); + } + } + } + Ok(None) + } else { + Ok(map.entries.iter().position(|entry| { + entry.is_some_and(|entry| { + !entry.key_taken && entry.key == key && entry.key_pending_depth == depth + }) + })) + } + } + fn map_insert(&mut self, id: u64, key: u64, value: u64) -> LeafResult { + self.map_insert_depth(id, key, 0, value, 0) + } + fn map_insert_depth( + &mut self, + id: u64, + key: u64, + key_depth: u64, + value: u64, + value_depth: u64, + ) -> LeafResult { + self.require_plain_map(id, PENDING_MAP_INSERT)?; + let position = self.map_position_depth(id, key, key_depth)?; + let map = self.maps.get_mut(&id).ok_or(INVALID_MAP)?; + let key_strings = map.key_strings; + let value_owned = map.value_owned; + if position.is_none() { + map.entries.try_reserve(1).map_err(|_| EXHAUSTED)?; + map.entries.push(Some(NativeMapEntry { + key, + value, + key_pending_depth: key_depth, + value_pending_depth: value_depth, + key_taken: false, + })); + } else { + let old = map.entries[position.ok_or(INVALID_MAP)?] + .as_mut() + .ok_or(INVALID_MAP)?; + let old_value = std::mem::replace(&mut old.value, value); + old.value_pending_depth = value_depth; + if key_strings { + self.drop_value(key)?; + } + if value_owned { + self.drop_value(old_value)?; + } + } + Ok(id) + } + fn map_append_literal(&mut self, id: u64, key: u64, value: u64) -> LeafResult { + self.map_append_literal_depth(id, key, 0, value, 0) + } + fn map_append_literal_depth( + &mut self, + id: u64, + key: u64, + key_depth: u64, + value: u64, + value_depth: u64, + ) -> LeafResult { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + if map.key_strings { + self.text(key)?; + if key_depth != 0 { + return Err(INVALID_MAP); + } + } + let map = self.maps.get_mut(&id).ok_or(INVALID_MAP)?; + map.entries.try_reserve(1).map_err(|_| EXHAUSTED)?; + map.entries.push(Some(NativeMapEntry { + key, + value, + key_pending_depth: key_depth, + value_pending_depth: value_depth, + key_taken: false, + })); + Ok(id) + } + fn map_remove(&mut self, id: u64, key: u64) -> LeafResult { + self.map_remove_depth(id, key, 0) + } + fn map_remove_depth(&mut self, id: u64, key: u64, depth: u64) -> LeafResult { + self.require_plain_map(id, PENDING_MAP_REMOVE)?; + while let Some(index) = self.map_position_depth(id, key, depth)? { + let map = self.maps.get_mut(&id).ok_or(INVALID_MAP)?; + let key_strings = map.key_strings; + let value_owned = map.value_owned; + let entry = map.entries.remove(index).ok_or(INVALID_MAP)?; + if key_strings { + self.drop_value(entry.key)?; + } + if value_owned { + self.drop_value(entry.value)?; + } + } + Ok(id) + } + fn map_get(&mut self, id: u64, key: u64) -> LeafResult { + self.map_get_depth(id, key, 0) + } + fn map_get_depth(&mut self, id: u64, key: u64, depth: u64) -> LeafResult { + self.require_plain_map(id, PENDING_MAP_GET)?; + let Some(index) = self.map_position_depth(id, key, depth)? else { + return self.sum(SUM_FAILURE, 0, false); + }; + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + let value_owned = map.value_owned; + let entry = map.entries[index].ok_or(INVALID_MAP)?; + let bits = entry.value; + let value_depth = entry.value_pending_depth; + let bits = if value_owned { + self.clone_value(bits)? + } else { + bits + }; + match self.sum(SUM_SUCCESS, bits, value_owned) { + Ok(result) => { + self.sums + .get_mut(&result) + .ok_or(INVALID_SUM)? + .payload_pending_depth = value_depth; + Ok(result) + } + Err(error) => { + if value_owned { + self.drop_value(bits)?; + } + Err(error) + } + } + } + fn map_element(&mut self, id: u64, index: i64, key: bool, consume: bool) -> LeafResult { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + let index = usize::try_from(index).map_err(|_| INVALID_MAP)?; + let entry = map + .entries + .get(index) + .copied() + .flatten() + .ok_or(INVALID_MAP)?; + if entry.key_taken != (!key && consume) { + return Err(INVALID_MAP); + } + let (bits, owned) = if key { + (entry.key, map.key_strings) + } else { + (entry.value, map.value_owned) + }; + if !consume { + return if owned { + self.clone_value(bits) + } else { + Ok(bits) + }; + } + let map = self.maps.get_mut(&id).ok_or(INVALID_MAP)?; + let slot = map.entries.get_mut(index).ok_or(INVALID_MAP)?; + if key { + slot.as_mut().ok_or(INVALID_MAP)?.key_taken = true; + } else { + *slot = None; + } + Ok(bits) + } + fn map_element_depth(&self, id: u64, index: i64, key: u32) -> LeafResult { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + let index = usize::try_from(index).map_err(|_| INVALID_MAP)?; + let entry = map + .entries + .get(index) + .copied() + .flatten() + .ok_or(INVALID_MAP)?; + match key { + 0 => Ok(entry.value_pending_depth), + 1 if !entry.key_taken => Ok(entry.key_pending_depth), + _ => Err(INVALID_MAP), + } + } + fn clone_map(&mut self, id: u64) -> LeafResult { + let map = self.maps.get(&id).ok_or(INVALID_MAP)?; + let (entries, key_strings, value_owned, pending_depth) = ( + map.entries.clone(), + map.key_strings, + map.value_owned, + map.pending_depth, + ); + let output = self.new_map(u32::from(key_strings), u32::from(value_owned))?; + if let Err(error) = self + .maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .try_reserve_exact(entries.len()) + .map_err(|_| EXHAUSTED) + { + self.drop_value(output)?; + return Err(error); + } + for entry in entries { + let Some(entry) = entry else { + self.maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .push(None); + continue; + }; + if entry.key_taken { + self.drop_value(output)?; + return Err(INVALID_MAP); + } + let key = if key_strings { + match self.clone_value(entry.key) { + Ok(key) => key, + Err(error) => { + self.drop_value(output)?; + return Err(error); + } + } + } else { + entry.key + }; + let value = if value_owned { + match self.clone_value(entry.value) { + Ok(value) => value, + Err(error) => { + if key_strings { + self.drop_value(key)?; + } + self.drop_value(output)?; + return Err(error); + } + } + } else { + entry.value + }; + self.maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .push(Some(NativeMapEntry { + key, + value, + key_pending_depth: entry.key_pending_depth, + value_pending_depth: entry.value_pending_depth, + key_taken: false, + })); + } + self.maps.get_mut(&output).ok_or(INVALID_MAP)?.pending_depth = pending_depth; + Ok(output) + } + fn run_map(&mut self, id: u64) -> LeafResult { + let depth = self + .maps + .get(&id) + .ok_or(INVALID_MAP)? + .pending_depth + .checked_add(1) + .ok_or(EXHAUSTED)?; + let pending = self.clone_map(id)?; + self.maps + .get_mut(&pending) + .ok_or(INVALID_MAP)? + .pending_depth = depth; + Ok(pending) + } + fn join_map(&mut self, id: u64) -> LeafResult { + let depth = self + .maps + .get(&id) + .ok_or(INVALID_MAP)? + .pending_depth + .saturating_sub(1); + let joined = self.clone_map(id)?; + self.maps.get_mut(&joined).ok_or(INVALID_MAP)?.pending_depth = depth; + Ok(joined) + } + fn map_from_lists( + &mut self, + keys: u64, + values: u64, + key_strings: u32, + value_owned: u32, + ) -> LeafResult { + if key_strings > 1 || value_owned > 1 { + return Err(INVALID_MAP); + } + let key_list = self.lists.get(&keys).ok_or(INVALID_LIST)?; + let value_list = self.lists.get(&values).ok_or(INVALID_LIST)?; + if key_list.owned != (key_strings != 0) || value_list.owned != (value_owned != 0) { + return Err(INVALID_MAP); + } + if key_list.pending_depth != 0 || value_list.pending_depth != 0 { + return Err(PENDING_MAP_FROM_LISTS); + } + let (key_elements, value_elements, key_depths, value_depths) = ( + key_list.elements.clone(), + value_list.elements.clone(), + key_list.element_pending_depths.clone(), + value_list.element_pending_depths.clone(), + ); + let output = self.new_map(key_strings, value_owned)?; + for (index, (key, value)) in key_elements.into_iter().zip(value_elements).enumerate() { + let Some(key) = key else { + self.drop_value(output)?; + return Err(INVALID_LIST); + }; + let Some(value) = value else { + self.drop_value(output)?; + return Err(INVALID_LIST); + }; + let key = if key_strings != 0 { + match self.clone_value(key) { + Ok(key) => key, + Err(error) => { + self.drop_value(output)?; + return Err(error); + } + } + } else { + key + }; + let value = if value_owned != 0 { + match self.clone_value(value) { + Ok(value) => value, + Err(error) => { + if key_strings != 0 { + self.drop_value(key)?; + } + self.drop_value(output)?; + return Err(error); + } + } + } else { + value + }; + let key_depth = key_depths.get(&index).copied().unwrap_or(0); + let value_depth = value_depths.get(&index).copied().unwrap_or(0); + if let Err(error) = self.map_insert_depth(output, key, key_depth, value, value_depth) { + if key_strings != 0 { + self.drop_value(key)?; + } + if value_owned != 0 { + self.drop_value(value)?; + } + self.drop_value(output)?; + return Err(error); + } + } + self.drop_value(keys)?; + self.drop_value(values)?; + Ok(output) + } + fn set_position(&self, id: u64, key: u64) -> LeafResult> { + self.set_position_depth(id, key, 0) + } + fn require_plain_set(&self, id: u64, error: Failure) -> LeafResult<()> { + let set = self.sets.get(&id).ok_or(INVALID_SET)?; + if set.pending_depth != 0 { + return Err(error); + } + Ok(()) + } + fn check_iterable(&self, value: u64, raw_kind: u32) -> LeafResult { + let depth = match NativeSequenceKind::from_raw(raw_kind)? { + NativeSequenceKind::List => self.lists.get(&value).ok_or(INVALID_LIST)?.pending_depth, + NativeSequenceKind::String => { + self.strings + .get(&value) + .ok_or(INVALID_HANDLE)? + .pending_depth + } + NativeSequenceKind::Map => self.maps.get(&value).ok_or(INVALID_MAP)?.pending_depth, + NativeSequenceKind::Set => self.sets.get(&value).ok_or(INVALID_SET)?.pending_depth, + }; + if depth != 0 { + return Err(PENDING_ITERABLE); + } + Ok(0) + } + fn set_position_depth(&self, id: u64, key: u64, depth: u64) -> LeafResult> { + let set = self.sets.get(&id).ok_or(INVALID_SET)?; + if set.strings { + if depth != 0 { + return Err(INVALID_SET); + } + for (index, &element) in set.elements.iter().enumerate() { + if let Some(element) = element { + if self.same_string_value(element, key)? { + return Ok(Some(index)); + } + } + } + Ok(None) + } else { + Ok(set + .elements + .iter() + .enumerate() + .position(|(index, element)| { + *element == Some(key) + && set.element_pending_depths.get(&index).copied().unwrap_or(0) == depth + })) + } + } + fn set_add(&mut self, id: u64, key: u64) -> LeafResult { + self.set_add_depth(id, key, 0) + } + fn set_add_depth(&mut self, id: u64, key: u64, depth: u64) -> LeafResult { + self.require_plain_set(id, PENDING_SET_ADD)?; + let duplicate = self.set_position_depth(id, key, depth)?.is_some(); + if duplicate { + if self.sets.get(&id).ok_or(INVALID_SET)?.strings { + self.drop_value(key)?; + } + return Ok(id); + } + let set = self.sets.get_mut(&id).ok_or(INVALID_SET)?; + set.elements.try_reserve(1).map_err(|_| EXHAUSTED)?; + if depth != 0 { + set.element_pending_depths + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + set.element_pending_depths.insert(set.elements.len(), depth); + } + set.elements.push(Some(key)); + Ok(id) + } + fn set_remove(&mut self, id: u64, key: u64) -> LeafResult { + self.set_remove_depth(id, key, 0) + } + fn set_remove_depth(&mut self, id: u64, key: u64, depth: u64) -> LeafResult { + self.require_plain_set(id, PENDING_SET_REMOVE)?; + if let Some(index) = self.set_position_depth(id, key, depth)? { + let set = self.sets.get_mut(&id).ok_or(INVALID_SET)?; + let old = set.elements.remove(index); + set.element_pending_depths.remove(&index); + let mut moved = set + .element_pending_depths + .keys() + .copied() + .filter(|position| *position > index) + .collect::>(); + moved.sort_unstable(); + for position in moved { + if let Some(depth) = set.element_pending_depths.remove(&position) { + set.element_pending_depths.insert(position - 1, depth); + } + } + if set.strings { + self.drop_value(old.ok_or(INVALID_SET)?)?; + } + } + Ok(id) + } + fn clone_set(&mut self, id: u64) -> LeafResult { + let set = self.sets.get(&id).ok_or(INVALID_SET)?; + let (elements, element_pending_depths, strings, pending_depth) = ( + set.elements.clone(), + set.element_pending_depths.clone(), + set.strings, + set.pending_depth, + ); + let output = self.new_set(u32::from(strings))?; + if let Err(error) = self + .sets + .get_mut(&output) + .ok_or(INVALID_SET)? + .elements + .try_reserve_exact(elements.len()) + .map_err(|_| EXHAUSTED) + { + self.drop_value(output)?; + return Err(error); + } + for element in elements { + let value = if strings { + match element.map(|element| self.clone_value(element)).transpose() { + Ok(value) => value, + Err(error) => { + self.drop_value(output)?; + return Err(error); + } + } + } else { + element + }; + self.sets + .get_mut(&output) + .ok_or(INVALID_SET)? + .elements + .push(value); + } + let copy = self.sets.get_mut(&output).ok_or(INVALID_SET)?; + copy.element_pending_depths = element_pending_depths; + copy.pending_depth = pending_depth; + Ok(output) + } + fn run_set(&mut self, id: u64) -> LeafResult { + let depth = self + .sets + .get(&id) + .ok_or(INVALID_SET)? + .pending_depth + .checked_add(1) + .ok_or(EXHAUSTED)?; + let pending = self.clone_set(id)?; + self.sets + .get_mut(&pending) + .ok_or(INVALID_SET)? + .pending_depth = depth; + Ok(pending) + } + fn join_set(&mut self, id: u64) -> LeafResult { + let depth = self + .sets + .get(&id) + .ok_or(INVALID_SET)? + .pending_depth + .saturating_sub(1); + let joined = self.clone_set(id)?; + self.sets.get_mut(&joined).ok_or(INVALID_SET)?.pending_depth = depth; + Ok(joined) + } + fn insert_list(&mut self, value: u64, index: i64, bits: u64, depth: u64) -> LeafResult { + let invalid = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"list.__insert_at: index out of bounds".as_slice(), + ); + let position = match usize::try_from(index) { + Ok(position) => position, + Err(_) => return self.list_index_failure("list.__insert_at", index, invalid), + }; + let length = self.lists.get(&value).ok_or(INVALID_LIST)?.elements.len(); + if position > length { + return self.list_index_failure("list.__insert_at", index, invalid); + } + let list = self.lists.get_mut(&value).ok_or(INVALID_LIST)?; + if depth != 0 && list.owned { + return Err(INVALID_LIST); + } + list.elements.try_reserve(1).map_err(|_| EXHAUSTED)?; + if depth != 0 { + list.element_pending_depths + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + } + let mut moved = list + .element_pending_depths + .keys() + .copied() + .filter(|key| *key >= position) + .collect::>(); + moved.sort_unstable_by(|left, right| right.cmp(left)); + for key in moved { + if let Some(existing_depth) = list.element_pending_depths.remove(&key) { + list.element_pending_depths.insert(key + 1, existing_depth); + } + } + if depth != 0 { + list.element_pending_depths.insert(position, depth); + } + list.elements.insert(position, Some(bits)); + Ok(value) + } + fn sort_list(&mut self, id: u64, raw_kind: u32) -> LeafResult { + let kind = NativeSortKind::from_raw(raw_kind)?; + let list = self.lists.get_mut(&id).ok_or(INVALID_LIST)?; + if list.owned != (kind == NativeSortKind::String) + || list.elements.iter().any(|element| { + element.is_none_or(|bits| { + !kind.valid_bits(bits) + || (kind == NativeSortKind::String && !self.strings.contains_key(&bits)) + }) + }) + { + return Err(INVALID_LIST); + } + let mut sorted = Vec::new(); + sorted + .try_reserve_exact(list.elements.len()) + .map_err(|_| EXHAUSTED)?; + for (index, element) in list.elements.iter().enumerate() { + let bits = element.ok_or(INVALID_LIST)?; + let depth = list + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0); + sorted.push((bits, depth)); + } + sorted.sort_by(|(left, left_depth), (right, right_depth)| { + if *left_depth != 0 || *right_depth != 0 { + CompareOrdering::Equal + } else if kind == NativeSortKind::String { + let left = &self.strings[left]; + let right = &self.strings[right]; + if left.pending_depth != 0 || right.pending_depth != 0 { + CompareOrdering::Equal + } else { + left.text.cmp(&right.text) + } + } else { + kind.compare(*left, *right) + } + }); + list.element_pending_depths.clear(); + for (index, (bits, depth)) in sorted.into_iter().enumerate() { + list.elements[index] = Some(bits); + if depth != 0 { + list.element_pending_depths.insert(index, depth); + } + } + Ok(id) + } + + fn list_is_sorted(&self, id: u64, raw_kind: u32) -> LeafResult { + let kind = NativeSortKind::from_comparison_raw(raw_kind)?; + let list = self.lists.get(&id).ok_or(INVALID_LIST)?; + let Some(kind) = kind else { + return Ok(true); + }; + if list.owned != (kind == NativeSortKind::String) { + return Err(INVALID_LIST); + } + let mut previous = None; + for (index, element) in list.elements.iter().enumerate() { + let bits = element.ok_or(INVALID_LIST)?; + if !kind.valid_bits(bits) + || (kind == NativeSortKind::String && !self.strings.contains_key(&bits)) + { + return Err(INVALID_LIST); + } + let depth = list + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0); + if let Some((left, left_depth)) = previous { + let ordered = if depth != 0 || left_depth != 0 { + true + } else if kind == NativeSortKind::String { + let left = &self.strings[&left]; + let right = &self.strings[&bits]; + left.pending_depth != 0 || right.pending_depth != 0 || left.text <= right.text + } else { + match kind { + NativeSortKind::Float32 => { + f32::from_bits(left as u32) <= f32::from_bits(bits as u32) + } + NativeSortKind::Float64 => f64::from_bits(left) <= f64::from_bits(bits), + _ => kind.compare(left, bits) != CompareOrdering::Greater, + } + }; + if !ordered { + return Ok(false); + } + } + previous = Some((bits, depth)); + } + Ok(true) + } + + fn sort_list_by_index(&mut self, id: u64, index: i64, raw_kind: u32) -> LeafResult { + let kind = NativeSortKind::from_comparison_raw(raw_kind)?; + let outer = self.lists.get(&id).ok_or(INVALID_LIST)?; + if !outer.owned { + return Err(INVALID_LIST); + } + let mut keyed = Vec::new(); + keyed + .try_reserve_exact(outer.elements.len()) + .map_err(|_| EXHAUSTED)?; + let index = usize::try_from(index).ok(); + for element in &outer.elements { + let row_id = element.ok_or(INVALID_LIST)?; + let row = self.lists.get(&row_id).ok_or(INVALID_LIST)?; + if let Some(kind) = kind + && row.owned != (kind == NativeSortKind::String) + { + return Err(INVALID_LIST); + } + if row.pending_depth != 0 + || index + .and_then(|index| row.element_pending_depths.get(&index)) + .is_some_and(|depth| *depth != 0) + { + keyed.push((row_id, None)); + continue; + } + let key = index.and_then(|index| row.elements.get(index)).copied(); + let key = match key { + Some(Some(bits)) => { + if let Some(kind) = kind + && (!kind.valid_bits(bits) + || (kind == NativeSortKind::String + && !self.strings.contains_key(&bits))) + { + return Err(INVALID_LIST); + } + Some(bits) + } + Some(None) => return Err(INVALID_LIST), + None => None, + }; + let pending = kind == Some(NativeSortKind::String) + && key.is_some_and(|bits| self.strings[&bits].pending_depth != 0); + let key = if pending { None } else { key }; + keyed.push((row_id, key)); + } + if let Some(kind) = kind { + keyed.sort_by(|(_, left), (_, right)| match (left, right) { + (Some(left), Some(right)) if kind == NativeSortKind::String => { + self.strings[left].text.cmp(&self.strings[right].text) + } + (Some(left), Some(right)) => kind.compare(*left, *right), + _ => CompareOrdering::Equal, + }); + } + let outer = self.lists.get_mut(&id).ok_or(INVALID_LIST)?; + for (slot, (row_id, _)) in outer.elements.iter_mut().zip(keyed) { + *slot = Some(row_id); + } + Ok(id) + } + + fn sum_primitive_list(&self, id: u64, raw_kind: u32) -> LeafResult { + let kind = NativeSortKind::from_raw(raw_kind)?; + if matches!(kind, NativeSortKind::Bool | NativeSortKind::String) { + return Err(INVALID_LIST); + } + let list = self.lists.get(&id).ok_or(INVALID_LIST)?; + if list.owned { + return Err(INVALID_LIST); + } + if list.pending_depth != 0 { + return Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"list.__sum: argument must be a list", + )); + } + if list.element_pending_depths.contains_key(&0) { + return Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"list.__sum: list elements must be int64 or float64", + )); + } + if !list.element_pending_depths.is_empty() { + return Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"list.__sum: mixed types", + )); + } + let mut total = 0_u64; + for element in &list.elements { + let bits = element.ok_or(INVALID_LIST)?; + if !kind.valid_bits(bits) { + return Err(INVALID_LIST); + } + total = match kind { + NativeSortKind::Int8 | NativeSortKind::Uint8 => { + u64::from((total as u8).wrapping_add(bits as u8)) + } + NativeSortKind::Int16 | NativeSortKind::Uint16 => { + u64::from((total as u16).wrapping_add(bits as u16)) + } + NativeSortKind::Int32 | NativeSortKind::Uint32 => { + u64::from((total as u32).wrapping_add(bits as u32)) + } + NativeSortKind::Int64 | NativeSortKind::Uint64 => total.wrapping_add(bits), + NativeSortKind::Float32 => u64::from( + (f32::from_bits(total as u32) + f32::from_bits(bits as u32)).to_bits(), + ), + NativeSortKind::Float64 => (f64::from_bits(total) + f64::from_bits(bits)).to_bits(), + NativeSortKind::Bool | NativeSortKind::String => { + unreachable!("numeric kind checked above") + } + }; + } + Ok(total) + } + + fn math_numbers( + &self, + id: u64, + raw_kind: u32, + empty: Failure, + invalid_element: Failure, + invalid_list: Failure, + ) -> LeafResult> { + let kind = NativeSortKind::from_raw(raw_kind)?; + if matches!(kind, NativeSortKind::Bool | NativeSortKind::String) { + return Err(INVALID_LIST); + } + let list = self.lists.get(&id).ok_or(INVALID_LIST)?; + if list.owned { + return Err(INVALID_LIST); + } + if list.pending_depth != 0 { + return Err(invalid_list); + } + if list.elements.is_empty() { + return Err(empty); + } + if !list.element_pending_depths.is_empty() { + return Err(invalid_element); + } + list.elements + .iter() + .map(|value| { + let bits = (*value).ok_or(INVALID_LIST)?; + if !kind.valid_bits(bits) { + return Err(INVALID_LIST); + } + Ok(match kind { + NativeSortKind::Int8 => f64::from(bits as u8 as i8), + NativeSortKind::Int16 => f64::from(bits as u16 as i16), + NativeSortKind::Int32 => f64::from(bits as u32 as i32), + NativeSortKind::Int64 => (bits as i64) as f64, + NativeSortKind::Uint8 + | NativeSortKind::Uint16 + | NativeSortKind::Uint32 + | NativeSortKind::Uint64 => bits as f64, + NativeSortKind::Float32 => f64::from(f32::from_bits(bits as u32)), + NativeSortKind::Float64 => f64::from_bits(bits), + NativeSortKind::Bool | NativeSortKind::String => { + unreachable!("numeric kind checked above") + } + }) + }) + .collect() + } + fn string_list(&mut self, parts: Vec) -> LeafResult { + let mut elements = Vec::new(); + elements + .try_reserve_exact(parts.len()) + .map_err(|_| EXHAUSTED)?; + let id = self.new_list(true)?; + for part in parts { + match self.insert(part) { + Ok(value) => elements.push(Some(value)), + Err(error) => { + self.lists.get_mut(&id).ok_or(INVALID_LIST)?.elements = elements; + self.drop_value(id)?; + return Err(error); + } + } + } + self.lists.get_mut(&id).ok_or(INVALID_LIST)?.elements = elements; + Ok(id) + } + fn join_strings(&mut self, value: u64, separator: u64) -> LeafResult { + let list = self.lists.get(&value).ok_or(INVALID_LIST)?; + if !list.owned { + return Err(INVALID_LIST); + } + for element in &list.elements { + let string = self + .strings + .get(&element.ok_or(INVALID_LIST)?) + .ok_or(INVALID_HANDLE)?; + if string.pending_depth != 0 { + let value = format_pending_value(&string.text, string.pending_depth)?; + self.set_dynamic_failure_message( + format!("string.__join requires a list of strings, found {value}").into_bytes(), + ); + return Err(INVALID_PENDING_HANDLE_CHECK); + } + } + let separator = self.text(separator)?; + let mut parts = Vec::new(); + parts + .try_reserve_exact(list.elements.len()) + .map_err(|_| EXHAUSTED)?; + let mut length = separator + .len() + .checked_mul(list.elements.len().saturating_sub(1)) + .ok_or(EXHAUSTED)?; + for element in &list.elements { + let text = self.text(element.ok_or(INVALID_LIST)?)?; + length = length.checked_add(text.len()).ok_or(EXHAUSTED)?; + parts.push(text); + } + let mut output = String::new(); + output.try_reserve_exact(length).map_err(|_| EXHAUSTED)?; + for (index, part) in parts.into_iter().enumerate() { + if index != 0 { + output.push_str(separator); + } + output.push_str(part); + } + self.insert(output) + } + fn range(&mut self, start: i64, end: i64, step: i64) -> LeafResult { + if step == 0 { + return Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"range step cannot be zero", + )); + } + let too_large = ( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + b"range: requested output is too large" as &'static [u8], + ); + let distance = if step > 0 && start < end { + (i128::from(end) - i128::from(start)) as u128 + } else if step < 0 && start > end { + (i128::from(start) - i128::from(end)) as u128 + } else { + 0 + }; + let count = distance.div_ceil(i128::from(step).unsigned_abs()); + let capacity = usize::try_from(count).map_err(|_| too_large)?; + let mut elements = Vec::new(); + elements + .try_reserve_exact(capacity) + .map_err(|_| too_large)?; + let mut value = start; + while (step > 0 && value < end) || (step < 0 && value > end) { + elements.push(Some(value as u64)); + let Some(next) = value.checked_add(step) else { + break; + }; + value = next; + } + let id = self.new_list(false)?; + self.lists.get_mut(&id).ok_or(INVALID_LIST)?.elements = elements; + Ok(id) + } + fn clone_list(&mut self, id: u64) -> LeafResult { + let list = self.lists.get(&id).ok_or(INVALID_LIST)?; + let (elements, element_pending_depths, owned, pending_depth) = ( + list.elements.clone(), + list.element_pending_depths.clone(), + list.owned, + list.pending_depth, + ); + let mut output = Vec::new(); + output + .try_reserve_exact(elements.len()) + .map_err(|_| EXHAUSTED)?; + for bits in elements { + let Some(bits) = bits else { + output.push(None); + continue; + }; + if owned { + match self.clone_value(bits) { + Ok(value) => output.push(Some(value)), + Err(error) => { + for v in output.into_iter().flatten() { + self.drop_value(v)?; + } + return Err(error); + } + } + } else { + output.push(Some(bits)); + } + } + let id = match self.new_list(owned) { + Ok(id) => id, + Err(error) => { + if owned { + for v in output.into_iter().flatten() { + self.drop_value(v)?; + } + } + return Err(error); + } + }; + let list = self.lists.get_mut(&id).ok_or(INVALID_LIST)?; + list.elements = output; + list.element_pending_depths = element_pending_depths; + list.pending_depth = pending_depth; + Ok(id) + } + fn run_list(&mut self, id: u64) -> LeafResult { + let depth = self + .lists + .get(&id) + .ok_or(INVALID_LIST)? + .pending_depth + .checked_add(1) + .ok_or(EXHAUSTED)?; + let pending = self.clone_list(id)?; + self.lists + .get_mut(&pending) + .ok_or(INVALID_LIST)? + .pending_depth = depth; + Ok(pending) + } + fn join_list(&mut self, id: u64) -> LeafResult { + let depth = self + .lists + .get(&id) + .ok_or(INVALID_LIST)? + .pending_depth + .saturating_sub(1); + let joined = self.clone_list(id)?; + self.lists + .get_mut(&joined) + .ok_or(INVALID_LIST)? + .pending_depth = depth; + Ok(joined) + } + // Interface boxes store task depth on the box, so running and joining an + // erased value behave exactly like running and joining its concrete value. + fn owned_pending_depth(&self, value: u64) -> LeafResult { + if let Some(value) = self.checked_graph_hooks.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.strings.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.structs.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.lists.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.sets.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.maps.get(&value) { + return Ok(value.pending_depth); + } + if let Some(value) = self.sums.get(&value) { + return Ok(value.pending_depth); + } + self.bytes_depth(value) + } + + fn clone_with_pending_depth(&mut self, value: u64, depth: u64) -> LeafResult { + if self.checked_graph_hooks.contains_key(&value) { + return self.clone_checked_graph_hook(value, Some(depth)); + } + // Strings share storage when cloned. Changing their metadata would also + // change the original binding, so make a separate string here. + if let Some(value) = self.strings.get(&value) { + return self.insert_pending(value.text.clone(), depth); + } + let cloned = self.clone_value(value)?; + if let Some(value) = self.structs.get_mut(&cloned) { + value.pending_depth = depth; + } else if let Some(value) = self.lists.get_mut(&cloned) { + value.pending_depth = depth; + } else if let Some(value) = self.sets.get_mut(&cloned) { + value.pending_depth = depth; + } else if let Some(value) = self.maps.get_mut(&cloned) { + value.pending_depth = depth; + } else if let Some(value) = self.sums.get_mut(&cloned) { + value.pending_depth = depth; + } else if self.bytes.contains_key(&cloned) { + if depth == 0 { + self.bytes_pending.remove(&cloned); + } else { + self.bytes_pending.insert(cloned, depth); + } + } else { + self.drop_value(cloned)?; + return Err(INVALID_HANDLE); + } + Ok(cloned) + } + + fn function_adapter(&mut self, source: u64, code: u64) -> LeafResult { + if code == 0 { + return Err(INVALID_FUNCTION); + } + self.function_debug_label(source)?; + let depth = self.owned_pending_depth(source)?; + let label = self.struct_field(source, NATIVE_FUNCTION_LABEL_FIELD)?.bits; + let output = self.new_struct(NATIVE_FUNCTION_FIELD_COUNT)?; + let result = (|| { + self.structs + .get_mut(&output) + .ok_or(INVALID_FUNCTION)? + .fields[NATIVE_FUNCTION_CODE_FIELD as usize] = Some(NativeField { + bits: code, + owned: false, + pending_depth: 0, + }); + // One captured function and its scalar-depth slot use the same + // environment layout as ordinary extracted closures. + let environment = self.new_struct(2)?; + self.structs + .get_mut(&output) + .ok_or(INVALID_FUNCTION)? + .fields[NATIVE_FUNCTION_ENVIRONMENT_FIELD as usize] = Some(NativeField { + bits: environment, + owned: true, + pending_depth: 0, + }); + let captured = self.clone_with_pending_depth(source, 0)?; + self.structs + .get_mut(&environment) + .ok_or(INVALID_FUNCTION)? + .fields = vec![ + Some(NativeField { + bits: captured, + owned: true, + pending_depth: 0, + }), + Some(NativeField { + bits: 0, + owned: false, + pending_depth: 0, + }), + ]; + let label = self.clone_value(label)?; + let descriptor = self.structs.get_mut(&output).ok_or(INVALID_FUNCTION)?; + descriptor.fields[NATIVE_FUNCTION_LABEL_FIELD as usize] = Some(NativeField { + bits: label, + owned: true, + pending_depth: 0, + }); + descriptor.pending_depth = depth; + Ok(output) + })(); + if result.is_err() { + let _ = self.drop_value(output); + } + result + } + + fn interface_box( + &mut self, + concrete: u64, + bits: u64, + owned: bool, + depth: u64, + layout: &[u8], + ) -> LeafResult { + NativeDebugLayout::parse(layout)?; + let depth = if owned { + depth + .checked_add(self.owned_pending_depth(bits)?) + .ok_or(EXHAUSTED)? + } else { + depth + }; + let value = self.new_struct(3)?; + self.structs + .get_mut(&value) + .ok_or(INVALID_STRUCT)? + .pending_depth = depth; + let result = (|| { + self.structs.get_mut(&value).ok_or(INVALID_STRUCT)?.fields[0] = Some(NativeField { + bits: concrete, + owned: false, + pending_depth: 0, + }); + let bits = if owned { + self.clone_with_pending_depth(bits, 0)? + } else { + bits + }; + self.structs.get_mut(&value).ok_or(INVALID_STRUCT)?.fields[1] = Some(NativeField { + bits, + owned, + pending_depth: 0, + }); + let layout = self.insert_bytes(layout.to_vec())?; + self.structs.get_mut(&value).ok_or(INVALID_STRUCT)?.fields[2] = Some(NativeField { + bits: layout, + owned: true, + pending_depth: 0, + }); + Ok(value) + })(); + if result.is_err() { + let _ = self.drop_value(value); + } + result + } + + fn new_struct(&mut self, count: u64) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let count = usize::try_from(count).map_err(|_| EXHAUSTED)?; + let mut fields = Vec::new(); + fields.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + fields.resize(count, None); + let id = next_identity()?; + self.structs.insert( + id, + NativeStruct { + fields, + pending_depth: 0, + }, + ); + self.structs_created += 1; + Ok(id) + } + fn register_actor(&mut self, id: u64) -> LeafResult { + let Some(record) = self.structs.get(&id) else { + return Err(INVALID_ACTOR); + }; + if record.fields.iter().any(Option::is_none) + || record.pending_depth != 0 + || self.actors.contains_key(&id) + { + return Err(INVALID_ACTOR); + } + let next = self.next_actor_ordinal.checked_add(1).ok_or(EXHAUSTED)?; + self.actors.insert( + id, + NativeActorIdentity { + instance: id, + ordinal: self.next_actor_ordinal, + }, + ); + self.next_actor_ordinal = next; + Ok(id) + } + fn actor_identity_parts(&self, id: u64) -> LeafResult<(NativeActorIdentity, u64)> { + let identity = *self.actors.get(&id).ok_or(INVALID_ACTOR)?; + let record = self.structs.get(&id).ok_or(INVALID_ACTOR)?; + let base_identity = self.actors.get(&identity.instance).ok_or(INVALID_ACTOR)?; + let base = self.structs.get(&identity.instance).ok_or(INVALID_ACTOR)?; + if base_identity.instance != identity.instance + || base_identity.ordinal != identity.ordinal + || base.pending_depth != 0 + || base.fields.iter().any(Option::is_none) + || (id != identity.instance && (!record.fields.is_empty() || record.pending_depth == 0)) + { + return Err(INVALID_ACTOR); + } + Ok((identity, record.pending_depth)) + } + fn same_actor_identity(&self, left: u64, right: u64) -> LeafResult { + // Exact nominal actor types come from validated MIR/layout. IDs must + // still be registered in this context even when their bits are equal. + let (left_identity, left_depth) = self.actor_identity_parts(left)?; + let (right_identity, right_depth) = self.actor_identity_parts(right)?; + Ok(left_identity.instance == right_identity.instance && left_depth == right_depth) + } + fn equal_actor_identity(&mut self, left: u64, right: u64, not_equal: u32) -> LeafResult { + if not_equal > 1 { + return Err(INVALID_ACTOR); + } + let (left_identity, left_depth) = self.actor_identity_parts(left)?; + let (right_identity, right_depth) = self.actor_identity_parts(right)?; + if left_depth != 0 || right_depth != 0 { + let left = + format_pending_value(&format!("actor#{}", left_identity.ordinal), left_depth)?; + let right = + format_pending_value(&format!("actor#{}", right_identity.ordinal), right_depth)?; + let operation = if not_equal == 0 { "Eq" } else { "NotEq" }; + self.set_dynamic_failure_message( + format!("unsupported binary operation: {left} {operation} {right}").into_bytes(), + ); + return Err(UNSUPPORTED_ACTOR_COMPARISON); + } + Ok(u32::from( + (left_identity.instance == right_identity.instance) != (not_equal != 0), + )) + } + fn actor_pending_alias( + &mut self, + identity: NativeActorIdentity, + depth: u64, + ) -> LeafResult { + if depth == 0 { + return Ok(identity.instance); + } + self.actors.try_reserve(1).map_err(|_| EXHAUSTED)?; + let alias = self.new_struct(0)?; + self.structs + .get_mut(&alias) + .ok_or(INVALID_ACTOR)? + .pending_depth = depth; + self.actors.insert(alias, identity); + Ok(alias) + } + fn run_actor(&mut self, id: u64) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + self.actor_pending_alias(identity, depth.checked_add(1).ok_or(EXHAUSTED)?) + } + fn join_actor(&mut self, id: u64) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + self.actor_pending_alias(identity, depth.saturating_sub(1)) + } + fn check_actor_message(&mut self, id: u64) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + let ordinal = identity.ordinal; + if depth == 0 { + return Ok(0); + } + let actor = format_pending_value(&format!("actor#{ordinal}"), depth)?; + self.set_dynamic_failure_message( + format!("send/ask: expected actor value, got {actor}").into_bytes(), + ); + Err(PENDING_ACTOR_MESSAGE) + } + fn replace_actor_field( + &mut self, + id: u64, + index: u64, + bits: u64, + owned: bool, + ) -> LeafResult { + self.replace_actor_field_depth(id, index, bits, 0, owned) + } + // Publication transfers the incoming slot only after all checks succeed. + // The caller clears that slot before separately retiring the displaced value. + // This physical swap supplies no Source mutation or Resource authority. + fn replace_actor_field_occupied( + &mut self, + id: u64, + index: u64, + replacement: NativeField, + ) -> LeafResult { + let (identity, depth) = self.actor_identity_parts(id)?; + if identity.instance != id + || depth != 0 + || self.builders.contains_key(&id) + || self.checked_graph_hooks.contains_key(&id) + || self.actor_graph_imports.contains_key(&id) + { + return Err(INVALID_ACTOR); + } + let index = usize::try_from(index).map_err(|_| INVALID_ACTOR)?; + let record = self.structs.get(&id).ok_or(INVALID_ACTOR)?; + let displaced = record + .fields + .get(index) + .copied() + .flatten() + .ok_or(INVALID_ACTOR)?; + if displaced.owned != replacement.owned { + return Err(INVALID_ACTOR); + } + for field in record + .fields + .iter() + .flatten() + .chain(std::iter::once(&replacement)) + { + if field.owned { + if field.pending_depth != 0 + || field.bits == id + || self.actors.contains_key(&field.bits) + { + return Err(INVALID_ACTOR); + } + self.owned_pending_depth(field.bits) + .map_err(|_| INVALID_ACTOR)?; + } + } + // The displaced edge remains owned until its caller-held value is + // retired. Only explicitly retained strings may share a direct edge. + if replacement.owned { + let existing_edges = record + .fields + .iter() + .flatten() + .filter(|field| field.owned && field.bits == replacement.bits) + .count(); + if let Some(string) = self.strings.get(&replacement.bits) { + let required = u64::try_from(existing_edges) + .ok() + .and_then(|edges| edges.checked_add(1)) + .ok_or(INVALID_ACTOR)?; + if string.references < required { + return Err(INVALID_ACTOR); + } + } else if existing_edges != 0 { + return Err(INVALID_ACTOR); + } + } + let current = self + .structs + .get_mut(&id) + .and_then(|record| record.fields.get_mut(index)) + .and_then(Option::as_mut) + .ok_or(INVALID_ACTOR)?; + Ok(std::mem::replace(current, replacement)) + } + fn replace_actor_field_depth( + &mut self, + id: u64, + index: u64, + bits: u64, + pending_depth: u64, + owned: bool, + ) -> LeafResult { + if owned && pending_depth != 0 { + return Err(INVALID_ACTOR); + } + if !self.actors.contains_key(&id) { + return Err(INVALID_ACTOR); + } + let slot = self + .structs + .get_mut(&id) + .and_then(|record| { + usize::try_from(index) + .ok() + .and_then(|i| record.fields.get_mut(i)) + }) + .ok_or(INVALID_ACTOR)?; + let old = slot + .replace(NativeField { + bits, + owned, + pending_depth, + }) + .ok_or(INVALID_ACTOR)?; + if old.owned { + self.drop_value(old.bits)?; + } + Ok(0) + } + pub(super) fn release_actors(&mut self) -> LeafResult<()> { + let mut first_failure = self.abort_unfinished_actor_graphs().err(); + for (id, _) in std::mem::take(&mut self.actors) { + if let Err(error) = self.drop_value(id) { + first_failure.get_or_insert(error); + } + } + first_failure.map_or(Ok(()), Err) + } + + fn graphics_validate_scene(&mut self, width: i64, height: i64, value: u64) -> LeafResult { + let scene = self.graphics_scene(value)?; + self.parsed_sum(graphics::render_scene(width, height, &scene).map(|_| 0)) + } + + fn graphics_validate_config(&mut self, value: u64) -> LeafResult { + let config = self.graphics_config(value)?; + self.parsed_sum(graphics::validate_config(&config).map(|_| 0)) + } + + fn graphics_open(&mut self, authority: u64, value: u64) -> LeafResult { + if self.graphics != Some(authority) { + return Err(INVALID_GRAPHICS); + } + if self.graphics_session.is_some() || GRAPHICS_SESSION.with(|slot| slot.borrow().is_some()) + { + return self.parsed_sum(Err( + "graphics.run: nested sessions are unsupported".to_owned() + )); + } + let config = self.graphics_config(value)?; + if let Err(message) = graphics::validate_config(&config) { + return self.parsed_sum(Err(message)); + } + let session = if self.graphics_script.is_some() { + NativeGraphicsSession::Scripted { + width: config.width, + height: config.height, + } + } else { + match graphics::Session::new(config) { + Ok(session) => NativeGraphicsSession::Window(session), + Err(message) => return self.parsed_sum(Err(message)), + } + }; + let identity = next_identity()?; + GRAPHICS_SESSION.with(|slot| { + *slot.borrow_mut() = Some(ActiveGraphicsSession { identity, session }); + }); + self.graphics_session = Some(identity); + match self.sum(SUM_SUCCESS, identity, false) { + Ok(result) => Ok(result), + Err(error) => { + GRAPHICS_SESSION.with(|slot| { + slot.borrow_mut().take(); + }); + self.graphics_session = None; + Err(error) + } + } + } + + fn graphics_present(&mut self, identity: u64, value: u64) -> LeafResult { + if self.graphics_session != Some(identity) { + return Err(INVALID_GRAPHICS); + } + let scene = self.graphics_scene(value)?; + let outcome = GRAPHICS_SESSION.with(|slot| { + let mut active = slot.borrow_mut(); + let active = active.as_mut().ok_or(INVALID_GRAPHICS)?; + if active.identity != identity { + return Err(INVALID_GRAPHICS); + } + Ok(match &mut active.session { + NativeGraphicsSession::Window(session) => session.present(&scene), + NativeGraphicsSession::Scripted { width, height } => { + graphics::render_scene(*width, *height, &scene).map(|_| ()) + } + }) + })?; + self.parsed_sum(outcome.map(|_| 0)) + } + + fn graphics_next_key(&mut self, identity: u64) -> LeafResult { + if self.graphics_session != Some(identity) { + return Err(INVALID_GRAPHICS); + } + let outcome = GRAPHICS_SESSION.with(|slot| { + let mut active = slot.borrow_mut(); + let active = active.as_mut().ok_or(INVALID_GRAPHICS)?; + if active.identity != identity { + return Err(INVALID_GRAPHICS); + } + Ok(match &mut active.session { + NativeGraphicsSession::Window(session) => session.next_key(), + NativeGraphicsSession::Scripted { .. } => match self + .graphics_script + .as_mut() + .ok_or(INVALID_GRAPHICS)? + .pop_front() + { + Some(graphics::TestEvent::Key(key)) => Ok(Some(key)), + Some(graphics::TestEvent::Close) => Ok(None), + Some(graphics::TestEvent::HostError(message)) => Err(message), + None => Err("Graphics: test provider exhausted".to_owned()), + }, + }) + })?; + match outcome { + Ok(Some(key)) => { + let optional = self.sum(SUM_SUCCESS, key.variant_index(), false)?; + self.owned_sum(SUM_SUCCESS, optional) + } + Ok(None) => { + let optional = self.sum(SUM_FAILURE, 0, false)?; + self.owned_sum(SUM_SUCCESS, optional) + } + Err(message) => self.parsed_sum(Err(message)), + } + } + + fn graphics_close(&mut self, identity: u64) -> LeafResult { + if self.graphics_session != Some(identity) { + return Err(INVALID_GRAPHICS); + } + GRAPHICS_SESSION.with(|slot| { + let mut active = slot.borrow_mut(); + if active + .as_ref() + .is_none_or(|active| active.identity != identity) + { + return Err(INVALID_GRAPHICS); + } + active.take(); + Ok(()) + })?; + self.graphics_session = None; + Ok(0) + } + + pub(super) fn release_graphics_session(&mut self) -> LeafResult<()> { + if let Some(identity) = self.graphics_session { + self.graphics_close(identity)?; + } + Ok(()) + } + fn new_builder(&mut self, layout: &[u8]) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { + bytes: layout, + position: 0, + }; + let version: [u8; 3] = cursor + .take(3) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)? + .try_into() + .expect("three bytes"); + if &version == b"JC\x05" { + let owner = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let kind = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if cursor.position != layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let id = self.new_struct(0)?; + self.builders.insert( + id, + NativeBuilderInfo { + metadata_owner: owner.clone(), + owner, + unsupported_kind: Some(kind), + variant: None, + state: None, + target_state: None, + field_offset: 0, + field_names: Vec::new(), + field_type_names: Vec::new(), + field_types: Vec::new(), + field_debug_layouts: Vec::new(), + put_order: Vec::new(), + validation: Vec::new(), + }, + ); + return Ok(id); + } + if !matches!( + &version, + b"JC\x02" | b"JC\x03" | b"JC\x06" | b"JC\x07" | b"JC\x0a" + ) { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let owner = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let metadata_owner = if &version == b"JC\x0a" { + cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)? + } else { + owner.clone() + }; + let count = usize::try_from(cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if count > layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let mut field_names = Vec::new(); + let mut field_type_names = Vec::new(); + let mut field_types = Vec::new(); + field_names + .try_reserve_exact(count) + .map_err(|_| EXHAUSTED)?; + field_types + .try_reserve_exact(count) + .map_err(|_| EXHAUSTED)?; + field_type_names + .try_reserve_exact(count) + .map_err(|_| EXHAUSTED)?; + for _ in 0..count { + field_names.push(cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?); + field_type_names.push(cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?); + field_types.push(cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?); + } + let mut validation = Vec::new(); + validation.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + if matches!(&version, b"JC\x03" | b"JC\x07") { + for _ in 0..count { + let kind = cursor.byte().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let rule = match kind { + 0 => BuilderFieldValidation::None, + 1 | 2 | 3 => { + let width = cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if width > 64 { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + match kind { + 1 => BuilderFieldValidation::SignedBits(width), + 2 => BuilderFieldValidation::UnsignedBits(width), + _ => { + let name = + cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let variants = usize::try_from( + cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?, + ) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if variants > layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let mut values = Vec::new(); + values.try_reserve_exact(variants).map_err(|_| EXHAUSTED)?; + for _ in 0..variants { + values.push(( + cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?, + cursor.i64().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?, + )); + } + BuilderFieldValidation::EnumBits(width, name, values) + } + } + } + _ => return Err(INVALID_CONSTRUCTION_LAYOUT), + }; + validation.push(rule); + } + } else { + validation.resize(count, BuilderFieldValidation::None); + } + let mut field_debug_layouts = Vec::new(); + field_debug_layouts + .try_reserve_exact(count) + .map_err(|_| EXHAUSTED)?; + if matches!(&version, b"JC\x06" | b"JC\x07" | b"JC\x0a") { + for _ in 0..count { + let length = + usize::try_from(cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let bytes = cursor + .take(length) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if !bytes.is_empty() { + NativeDebugLayout::parse(bytes).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + } + field_debug_layouts.push(bytes.to_vec()); + } + } else { + field_debug_layouts.resize_with(count, Vec::new); + } + if cursor.position != layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let id = self.new_struct(count as u64)?; + self.builders.insert( + id, + NativeBuilderInfo { + metadata_owner, + owner, + unsupported_kind: None, + variant: None, + state: None, + target_state: None, + field_offset: 0, + field_names, + field_type_names, + field_types, + field_debug_layouts, + put_order: Vec::new(), + validation, + }, + ); + Ok(id) + } + fn builder_start_failure(&mut self, message: String) -> LeafResult { + let error = self.insert(message)?; + match self.sum(SUM_FAILURE, error, true) { + Ok(result) => Ok(result), + Err(failure) => { + self.drop_value(error)?; + Err(failure) + } + } + } + fn new_member_builder( + &mut self, + layout: &[u8], + metadata: u64, + metadata_debug_layout: &[u8], + machine: bool, + ) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { + bytes: layout, + position: 0, + }; + let version = cursor.take(3).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let rich_debug = version == if machine { b"JC\x09" } else { b"JC\x08" }; + if !rich_debug && version != if machine { b"JC\x05" } else { b"JC\x04" } { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let owner = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let expected_metadata_owner = if machine { + cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)? + } else { + owner.clone() + }; + let target_state = if machine { + let target = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + (!target.is_empty()).then_some(target) + } else { + None + }; + let count = usize::try_from(cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if count > layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + if self + .structs + .get(&metadata) + .ok_or(INVALID_CONSTRUCTION)? + .pending_depth + != 0 + { + let debug = NativeDebugLayout::parse(metadata_debug_layout) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let value = debug + .format_value(self, metadata, debug.root) + .map_err(|_| INVALID_CONSTRUCTION)?; + let (caller, kind) = if machine { + ("type.construct_machine_start", "TypeMachineState") + } else { + ("type.construct_variant_start", "TypeVariant") + }; + return self + .builder_start_failure(format!("{caller}: argument must be {kind}, got {value}")); + } + let index = usize::try_from(self.struct_field(metadata, 0)?.bits) + .map_err(|_| INVALID_CONSTRUCTION)?; + let metadata_owner = self.text(self.struct_field(metadata, 1)?.bits)?.to_owned(); + let metadata_name = self.text(self.struct_field(metadata, 2)?.bits)?.to_owned(); + let metadata_discriminant = if machine { + 0 + } else { + self.struct_field(metadata, 3)?.bits as i64 + }; + let field_list = self + .struct_field(metadata, if machine { 4 } else { 5 })? + .bits; + if metadata_owner != expected_metadata_owner { + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: state metadata belongs to machine '{metadata_owner}', expected '{expected_metadata_owner}'") + } else { + format!("type.construct_variant_start: variant metadata belongs to '{metadata_owner}', expected '{expected_metadata_owner}'") + }); + } + let mut selected = None; + for variant_index in 0..count { + let name = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let discriminant = if machine { + 0 + } else { + cursor.i64().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)? + }; + let field_count = + usize::try_from(cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if field_count > layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let mut names = Vec::new(); + let mut type_names = Vec::new(); + let mut types = Vec::new(); + let mut debug_layouts = Vec::new(); + for _ in 0..field_count { + let field_name = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let type_name = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let canonical_type = cursor.name().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let debug_layout = if rich_debug { + let length = + usize::try_from(cursor.u32().map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let bytes = cursor + .take(length) + .map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if !bytes.is_empty() { + NativeDebugLayout::parse(bytes).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + } + bytes.to_vec() + } else { + Vec::new() + }; + if variant_index == index { + names.push(field_name); + type_names.push(type_name); + types.push(canonical_type); + debug_layouts.push(debug_layout); + } + } + if variant_index == index { + selected = Some((name, discriminant, names, type_names, types, debug_layouts)); + } + } + if cursor.position != layout.len() { + return Err(INVALID_CONSTRUCTION_LAYOUT); + } + let Some(( + name, + discriminant, + field_names, + field_type_names, + field_types, + field_debug_layouts, + )) = selected + else { + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: machine '{expected_metadata_owner}' has no state at index {index}") + } else { + format!("type.construct_variant_start: enum '{owner}' has no variant at index {index}") + }); + }; + if name != metadata_name { + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: state metadata '{metadata_name}' does not match state '{name}' on '{expected_metadata_owner}'") + } else { + format!("type.construct_variant_start: variant metadata '{metadata_name}' does not match variant '{name}' on '{owner}'") + }); + } + if !machine && discriminant != metadata_discriminant { + return self.builder_start_failure(format!( + "type.construct_variant_start: variant '{owner}.{name}' has discriminant {discriminant}, metadata reports {metadata_discriminant}" + )); + } + let metadata_fields = &self + .lists + .get(&field_list) + .ok_or(INVALID_CONSTRUCTION)? + .elements; + if metadata_fields.len() != field_names.len() { + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: state '{expected_metadata_owner}.{name}' expects {} payload field(s), metadata reports {}", field_names.len(), metadata_fields.len()) + } else { + format!("type.construct_variant_start: variant '{owner}.{name}' expects {} payload field(s), metadata reports {}", field_names.len(), metadata_fields.len()) + }); + } + for (position, field) in metadata_fields.iter().enumerate() { + let (field_index, field_owner, member, field_name, type_name) = + self.builder_field_metadata(field.ok_or(INVALID_CONSTRUCTION)?)?; + if field_owner != expected_metadata_owner || member.as_deref() != Some(name.as_str()) { + let actual = member.map_or(field_owner.clone(), |member| { + format!("{field_owner}.{member}") + }); + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: field metadata belongs to '{actual}', expected '{expected_metadata_owner}.{name}'") + } else { + format!("type.construct_variant_start: field metadata belongs to '{actual}', expected '{expected_metadata_owner}.{name}'") + }); + } + if field_index != position + || field_name != field_names[position] + || type_name != field_type_names[position] + { + return self.builder_start_failure(if machine { + format!("type.construct_machine_start: payload field metadata at index {position} does not match state '{expected_metadata_owner}.{name}'") + } else { + format!("type.construct_variant_start: payload field metadata at index {position} does not match variant '{owner}.{name}'") + }); + } + } + let builder = self.new_struct((field_names.len() + 1) as u64)?; + self.structs + .get_mut(&builder) + .ok_or(INVALID_CONSTRUCTION)? + .fields[0] = Some(NativeField { + bits: index as u64, + owned: false, + pending_depth: 0, + }); + self.builders.insert( + builder, + NativeBuilderInfo { + owner, + metadata_owner: expected_metadata_owner, + unsupported_kind: None, + variant: (!machine).then(|| name.clone()), + state: machine.then_some(name), + target_state, + field_offset: 1, + validation: vec![BuilderFieldValidation::None; field_names.len()], + field_names, + field_type_names, + field_types, + field_debug_layouts, + put_order: Vec::new(), + }, + ); + match self.sum(SUM_SUCCESS, builder, true) { + Ok(result) => Ok(result), + Err(failure) => { + self.drop_value(builder)?; + Err(failure) + } + } + } + fn builder_field_metadata( + &self, + field: u64, + ) -> LeafResult<(usize, String, Option, String, String)> { + let index = + usize::try_from(self.struct_field(field, 0)?.bits).map_err(|_| INVALID_CONSTRUCTION)?; + let owner = self.text(self.struct_field(field, 1)?.bits)?.to_owned(); + let member = self + .sums + .get(&self.struct_field(field, 2)?.bits) + .ok_or(INVALID_CONSTRUCTION)?; + let member = if member.tag == SUM_SUCCESS { + Some(self.text(member.bits)?.to_owned()) + } else if member.tag == SUM_FAILURE { + None + } else { + return Err(INVALID_CONSTRUCTION); + }; + let name = self.text(self.struct_field(field, 3)?.bits)?.to_owned(); + let field_type = self.text(self.struct_field(field, 4)?.bits)?.to_owned(); + Ok((index, owner, member, name, field_type)) + } + fn builder_failure( + &mut self, + message: String, + builder: u64, + bits: u64, + owned: bool, + ) -> LeafResult { + let error = self.insert(message)?; + let result = match self.sum(SUM_FAILURE, error, true) { + Ok(result) => result, + Err(failure) => { + self.drop_value(error)?; + return Err(failure); + } + }; + self.drop_value(builder)?; + if owned { + self.drop_value(bits)?; + } + Ok(result) + } + fn reject_pending_builder(&mut self, builder: u64, operation: &str) -> LeafResult<()> { + let depth = self + .structs + .get(&builder) + .ok_or(INVALID_CONSTRUCTION)? + .pending_depth; + if depth == 0 { + return Ok(()); + } + let layout = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::TypeConstruction], + }; + let value = layout.format_value(self, builder, 0)?; + self.set_dynamic_failure_message( + format!("type.{operation}: first argument must be TypeConstruction, got {value}") + .into_bytes(), + ); + Err(INVALID_CONSTRUCTION) + } + fn builder_put( + &mut self, + builder: u64, + field: u64, + bits: u64, + owned: bool, + expected_owner: &str, + provided_type: &str, + canonical_type: &str, + metadata_layout: &[u8], + ) -> LeafResult { + self.builder_put_depth( + builder, + field, + bits, + 0, + owned, + expected_owner, + provided_type, + canonical_type, + metadata_layout, + ) + } + fn builder_put_depth( + &mut self, + builder: u64, + field: u64, + bits: u64, + pending_depth: u64, + owned: bool, + expected_owner: &str, + provided_type: &str, + canonical_type: &str, + metadata_layout: &[u8], + ) -> LeafResult { + self.reject_pending_builder(builder, "construct_put")?; + let info = self + .builders + .get(&builder) + .cloned() + .ok_or(INVALID_CONSTRUCTION)?; + if info.owner == expected_owner && info.unsupported_kind.is_some() { + return self.builder_failure( + format!("type.construct_put supports only structs, bitfields, enums, and machines, got '{expected_owner}'"), + builder, + bits, + owned, + ); + } + if let Some(message) = + self.pending_type_field_message(field, metadata_layout, "type.construct_put")? + { + return self.builder_failure(message, builder, bits, owned); + } + let (index, actual_owner, member, name, metadata_type) = + self.builder_field_metadata(field)?; + let expected_member = info.variant.as_ref().or(info.state.as_ref()); + let expected_label = expected_member.map_or_else( + || info.metadata_owner.clone(), + |member| format!("{}.{member}", info.metadata_owner), + ); + let slot = index.saturating_add(info.field_offset); + let error = if info.owner != expected_owner { + Some(format!( + "type.construct_put: builder for '{}' cannot construct '{}'", + info.owner, expected_owner + )) + } else if actual_owner != info.metadata_owner + || member.as_deref() != expected_member.map(String::as_str) + { + let actual_label = member.as_ref().map_or(actual_owner.clone(), |member| { + format!("{actual_owner}.{member}") + }); + Some(format!( + "type.construct_put: field metadata belongs to '{actual_label}', expected '{expected_label}'" + )) + } else if index >= info.field_names.len() { + Some(if info.variant.is_some() { + format!( + "type.construct_put: variant '{expected_label}' has no payload field at index {index}" + ) + } else if info.state.is_some() { + format!( + "type.construct_put: state '{expected_label}' has no payload field at index {index}" + ) + } else { + format!("type.construct_put: type '{expected_owner}' has no field at index {index}") + }) + } else if info.field_names[index] != name { + Some(if info.variant.is_some() { + format!( + "type.construct_put: field metadata '{name}' does not match payload field '{}' on variant '{expected_label}'", + info.field_names[index] + ) + } else if info.state.is_some() { + format!( + "type.construct_put: field metadata '{name}' does not match payload field '{}' on state '{expected_label}'", + info.field_names[index] + ) + } else { + format!( + "type.construct_put: field metadata '{name}' does not match field '{}' on '{expected_owner}'", + info.field_names[index] + ) + }) + } else if info.field_type_names[index] != metadata_type + && info.field_types[index] != metadata_type + { + Some(format!( + "type.construct_put: field metadata for '{name}' has type '{metadata_type}', but '{expected_owner}' reports '{}'", + info.field_type_names[index] + )) + } else if info.field_types[index] != canonical_type { + Some(format!( + "type.construct_put: field '{name}' has type '{}', provided as '{provided_type}'", + info.field_type_names[index] + )) + } else if self + .structs + .get(&builder) + .and_then(|record| record.fields.get(slot)) + .and_then(|slot| slot.as_ref()) + .is_some() + { + Some(format!( + "type.construct_put: field '{name}' was provided more than once" + )) + } else { + None + }; + if let Some(error) = error { + return self.builder_failure(error, builder, bits, owned); + } + self.builders + .get_mut(&builder) + .ok_or(INVALID_CONSTRUCTION)? + .put_order + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + let result = self.sum(SUM_SUCCESS, builder, true)?; + self.structs + .get_mut(&builder) + .ok_or(INVALID_CONSTRUCTION)? + .fields[slot] = Some(NativeField { + bits, + owned, + pending_depth, + }); + self.builders + .get_mut(&builder) + .ok_or(INVALID_CONSTRUCTION)? + .put_order + .push(index); + Ok(result) + } + fn builder_finish(&mut self, builder: u64, expected_owner: &str) -> LeafResult { + self.reject_pending_builder(builder, "construct_finish")?; + let info = self + .builders + .get(&builder) + .cloned() + .ok_or(INVALID_CONSTRUCTION)?; + if info.owner != expected_owner { + return self.builder_failure( + format!( + "type.construct_finish: builder for '{}' cannot construct '{}'", + info.owner, expected_owner + ), + builder, + 0, + false, + ); + } + if info.unsupported_kind.is_some() { + return self.builder_failure( + format!("type.construct_finish supports only structs, bitfields, enums, and machines, got '{expected_owner}'"), + builder, + 0, + false, + ); + } + if let (Some(state), Some(target)) = (&info.state, &info.target_state) + && state != target + { + return self.builder_failure( + format!( + "type.construct_finish: machine target '{} at {target}' cannot finish state '{state}'", + info.metadata_owner + ), + builder, + 0, + false, + ); + } + let record = self.structs.get(&builder).ok_or(INVALID_CONSTRUCTION)?; + if let Some((slot, _)) = record + .fields + .iter() + .enumerate() + .skip(info.field_offset) + .find(|(_, field)| field.is_none()) + { + let index = slot - info.field_offset; + return self.builder_failure( + if let Some(variant) = &info.variant { + format!("type.construct_finish: variant '{expected_owner}.{variant}' is missing required payload field '{}'", info.field_names[index]) + } else if let Some(state) = &info.state { + format!("type.construct_finish: state '{}.{state}' is missing required payload field '{}'", info.metadata_owner, info.field_names[index]) + } else { + format!("type.construct_finish: '{expected_owner}' is missing required field '{}'", info.field_names[index]) + }, + builder, + 0, + false, + ); + } + for (index, rule) in info.validation.iter().enumerate() { + let field = self.struct_field(builder, (index + info.field_offset) as u64)?; + let bits = field.bits; + let field_name = &info.field_names[index]; + let bitfield_name = &info.owner; + let message = if field.pending_depth != 0 { + match rule { + BuilderFieldValidation::SignedBits(_) + | BuilderFieldValidation::UnsignedBits(_) => { + Some(format!("field '{field_name}' expects int64 or uint64")) + } + BuilderFieldValidation::EnumBits(_, enum_name, _) => { + Some(format!("field '{field_name}' expects enum '{enum_name}'")) + } + BuilderFieldValidation::None => None, + } + } else { + match rule { + BuilderFieldValidation::None => None, + BuilderFieldValidation::SignedBits(width) => { + let value = bits as i64; + let max = if *width == 64 { + u64::MAX + } else { + (1_u64 << width) - 1 + }; + (value < 0 || bits > max).then(|| format!( + "bitfield '{bitfield_name}' field '{field_name}' is {width} bit(s) wide and cannot hold '{value}'" + )) + } + BuilderFieldValidation::UnsignedBits(width) => { + let max = if *width == 64 { + u64::MAX + } else { + (1_u64 << width) - 1 + }; + (bits > max).then(|| format!( + "bitfield '{bitfield_name}' field '{field_name}' is {width} bit(s) wide and cannot hold '{bits}'" + )) + } + BuilderFieldValidation::EnumBits(width, enum_name, variants) => { + let tag = usize::try_from(self.struct_field(bits, 0)?.bits) + .map_err(|_| INVALID_CONSTRUCTION)?; + let (variant_name, discriminant) = + variants.get(tag).ok_or(INVALID_CONSTRUCTION)?; + if *discriminant < 0 { + Some(format!( + "enum '{enum_name}.{variant_name}' has negative discriminant {discriminant}" + )) + } else { + let max = if *width == 64 { + u64::MAX + } else { + (1_u64 << width) - 1 + }; + ((*discriminant as u64) > max).then(|| format!( + "bitfield '{bitfield_name}' field '{field_name}' is {width} bit(s) wide and cannot hold enum variant '{enum_name}.{variant_name}'" + )) + } + } + } + }; + if let Some(message) = message { + return self.builder_failure(message, builder, 0, false); + } + } + let result = self.sum(SUM_SUCCESS, builder, true)?; + self.builders.remove(&builder); + Ok(result) + } + fn struct_field(&self, id: u64, index: u64) -> LeafResult { + self.structs + .get(&id) + .and_then(|s| usize::try_from(index).ok().and_then(|i| s.fields.get(i))) + .copied() + .flatten() + .ok_or(INVALID_STRUCT) + } + fn check_struct_pending_access( + &mut self, + value: u64, + layout: &[u8], + message: &str, + render: bool, + ) -> LeafResult { + if self + .structs + .get(&value) + .ok_or(INVALID_STRUCT)? + .pending_depth + == 0 + { + return Ok(0); + } + let message = if render { + // An unavailable layout must not make ready access unsupported or + // disclose an opaque payload while reporting pending access. + let actual = if layout.is_empty() { + "[redacted]".to_owned() + } else { + let layout = NativeDebugLayout::parse(layout).map_err(|_| INVALID_STRUCT)?; + layout + .format_value(self, value, layout.root) + .map_err(|_| INVALID_STRUCT)? + }; + format!("{message}{actual}") + } else { + message.to_owned() + }; + self.set_dynamic_failure_message(message.into_bytes()); + Err(INVALID_STRUCT) + } + fn enum_equal(&self, left: u64, right: u64, layout: &[u8]) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { + bytes: layout, + position: 0, + }; + if cursor.take(3).map_err(|_| INVALID_STRUCT)? != b"JE\x01" { + return Err(INVALID_STRUCT); + } + let count = usize::try_from(cursor.u32().map_err(|_| INVALID_STRUCT)?) + .map_err(|_| INVALID_STRUCT)?; + if count > layout.len() { + return Err(INVALID_STRUCT); + } + let left_tag = + usize::try_from(self.struct_field(left, 0)?.bits).map_err(|_| INVALID_STRUCT)?; + let right_tag = + usize::try_from(self.struct_field(right, 0)?.bits).map_err(|_| INVALID_STRUCT)?; + if left_tag >= count || right_tag >= count { + return Err(INVALID_STRUCT); + } + let mut equal = left_tag == right_tag; + for variant in 0..count { + let fields = usize::try_from(cursor.u32().map_err(|_| INVALID_STRUCT)?) + .map_err(|_| INVALID_STRUCT)?; + let kinds = cursor.take(fields).map_err(|_| INVALID_STRUCT)?; + if variant != left_tag || !equal { + continue; + } + for (index, kind) in kinds.iter().enumerate() { + let slot = u64::try_from(index + 1).map_err(|_| INVALID_STRUCT)?; + let left_value = self.struct_field(left, slot)?.bits; + let right_value = self.struct_field(right, slot)?.bits; + let same = match kind { + b'i' => left_value == right_value, + b'f' => f32::from_bits(left_value as u32) == f32::from_bits(right_value as u32), + b'd' => f64::from_bits(left_value) == f64::from_bits(right_value), + b's' => self.same_string_value(left_value, right_value)?, + _ => return Err(INVALID_STRUCT), + }; + if !same { + equal = false; + } + } + } + if cursor.position != layout.len() { + return Err(INVALID_STRUCT); + } + Ok(u32::from(equal)) + } + fn aggregate_enum_equal(&self, left: u64, right: u64, layout: &[u8]) -> LeafResult { + let layout = NativeDebugLayout::parse(layout).map_err(|_| INVALID_STRUCT)?; + if !matches!( + layout.nodes.get(layout.root), + Some(NativeDebugNode::Enum(..)) + ) { + return Err(INVALID_STRUCT); + } + Ok(u32::from(layout.equal_value( + self, + left, + right, + layout.root, + )?)) + } + fn checked_type_arg_index( + &mut self, + index: i64, + count: u64, + depth: u64, + type_name: &str, + ) -> LeafResult { + let message = if depth != 0 { + let index = format_pending_value(&index.to_string(), depth)?; + format!("type.arg expects a non-negative int64 index, got {index}") + } else if index < 0 { + format!("type.arg expects a non-negative int64 index, got {index}") + } else if index as u64 >= count { + format!("type.arg index {index} is out of range for type '{type_name}'") + } else { + return Ok(index as u64); + }; + self.set_dynamic_failure_message(message.into_bytes()); + Err(INVALID_TYPE_ARG_INDEX) + } + fn reflected_field_owner_label(&self, field: u64, mismatch: Failure) -> LeafResult { + let owner = self.text(self.struct_field(field, 1)?.bits)?; + let member = self + .sums + .get(&self.struct_field(field, 2)?.bits) + .ok_or(mismatch)?; + if member.tag == SUM_SUCCESS { + Ok(format!("{owner}.{}", self.text(member.bits)?)) + } else { + Ok(owner.to_owned()) + } + } + fn check_reflected_field_owner( + &mut self, + actual: u64, + expected_owner: &str, + expected_member: Option<&str>, + metadata_layout: &[u8], + mismatch: Failure, + caller: &str, + ) -> LeafResult { + if let Some(message) = self + .pending_type_field_message(actual, metadata_layout, caller) + .map_err(|_| mismatch)? + { + self.set_dynamic_failure_message(message.into_bytes()); + return Err(mismatch); + } + let actual_owner = self.text(self.struct_field(actual, 1)?.bits)?; + let actual_member = self + .sums + .get(&self.struct_field(actual, 2)?.bits) + .ok_or(mismatch)?; + let actual_member_name = if actual_member.tag == SUM_SUCCESS { + Some(self.text(actual_member.bits)?) + } else { + None + }; + let member_matches = actual_member_name == expected_member; + if actual_owner == expected_owner && member_matches { + return Ok(0); + } + let actual_label = self.reflected_field_owner_label(actual, mismatch)?; + let expected_label = expected_member.map_or_else( + || expected_owner.to_owned(), + |member| format!("{expected_owner}.{member}"), + ); + self.set_dynamic_failure_message( + format!( + "{caller}: field metadata belongs to '{actual_label}', expected '{expected_label}'" + ) + .into_bytes(), + ); + Err(mismatch) + } + fn pending_type_field_message( + &self, + actual: u64, + metadata_layout: &[u8], + caller: &str, + ) -> LeafResult> { + if self + .structs + .get(&actual) + .ok_or(INVALID_STRUCT)? + .pending_depth + == 0 + { + return Ok(None); + } + let layout = NativeDebugLayout::parse(metadata_layout)?; + let value = layout.format_value(self, actual, layout.root)?; + Ok(Some(format!( + "{caller}: second argument must be TypeField, got {value}" + ))) + } + fn check_reflected_owner_pending( + &mut self, + value: u64, + owner_name: &str, + debug_layout: &[u8], + kind: u32, + ) -> LeafResult { + if self + .structs + .get(&value) + .ok_or(INVALID_REFLECTED_OWNER)? + .pending_depth + == 0 + { + return Ok(0); + } + let layout = NativeDebugLayout::parse(debug_layout).map_err(|_| INVALID_REFLECTED_OWNER)?; + let actual = layout + .format_value(self, value, layout.root) + .map_err(|_| INVALID_REFLECTED_OWNER)?; + let (caller, expected) = match kind { + 0 => ("type.variant_value", "enum"), + 1 => ("type.machine_state_value", "machine"), + 2 => ("type.field_value", "struct"), + 3 => ("type.variant_field_value", "enum"), + 4 => ("type.machine_field_value", "machine"), + _ => return Err(INVALID_REFLECTED_OWNER), + }; + self.set_dynamic_failure_message( + format!("{caller}: expected {expected} value for '{owner_name}', got {actual}") + .into_bytes(), + ); + Err(INVALID_REFLECTED_OWNER) + } + fn reflected_field_index( + &mut self, + actual: u64, + expected: u64, + mismatch: Failure, + caller: &str, + ) -> LeafResult { + if expected == 0 { + return Err(mismatch); + } + let owner_type_matches = self.text(self.struct_field(actual, 1)?.bits)? + == self.text(self.struct_field(expected, 1)?.bits)?; + let actual_member = self + .sums + .get(&self.struct_field(actual, 2)?.bits) + .ok_or(mismatch)?; + let expected_member = self + .sums + .get(&self.struct_field(expected, 2)?.bits) + .ok_or(mismatch)?; + let owner_member_matches = actual_member.tag == expected_member.tag + && (actual_member.tag != SUM_SUCCESS + || self.text(actual_member.bits)? == self.text(expected_member.bits)?); + if !owner_type_matches || !owner_member_matches { + let actual_owner = self.reflected_field_owner_label(actual, mismatch)?; + let expected_owner = self.reflected_field_owner_label(expected, mismatch)?; + self.set_dynamic_failure_message( + format!( + "{caller}: field metadata belongs to '{actual_owner}', expected '{expected_owner}'" + ) + .into_bytes(), + ); + return Err(mismatch); + } + let index = self.struct_field(actual, 0)?.bits; + if index != self.struct_field(expected, 0)?.bits { + return Err(mismatch); + } + for position in [3, 4] { + let actual_text = self.text(self.struct_field(actual, position)?.bits)?; + let expected_text = self.text(self.struct_field(expected, position)?.bits)?; + if actual_text != expected_text { + return Err(mismatch); + } + } + Ok(index) + } + fn reflected_checked_field_index( + &mut self, + actual: u64, + expected: u64, + compatible: u64, + requested_type: &str, + mismatch: Failure, + caller: &str, + ) -> LeafResult { + let index = self.reflected_field_index(actual, expected, mismatch, caller)?; + if compatible == 0 { + let name = self.text(self.struct_field(expected, 3)?.bits)?; + let field_type = self.text(self.struct_field(expected, 4)?.bits)?; + self.set_dynamic_failure_message( + format!( + "{caller}: field '{name}' has type '{field_type}', requested '{requested_type}'" + ) + .into_bytes(), + ); + return Err(mismatch); + } + Ok(index) + } + fn type_info_identity(&self, value: u64, preserve_aliases: bool) -> LeafResult { + enum Frame { + Enter(Option), + Leave(u64), + } + let mut work = vec![Frame::Enter(Some(value))]; + let mut active = std::collections::HashSet::new(); + let mut identity = String::new(); + while let Some(frame) = work.pop() { + let value = match frame { + Frame::Leave(value) => { + active.remove(&value); + continue; + } + Frame::Enter(value) => value.ok_or(INVALID_TYPE_INFO)?, + }; + if !active.insert(value) { + return Err(INVALID_TYPE_INFO); + } + work.push(Frame::Leave(value)); + let type_name = self.text(self.struct_field(value, 0)?.bits)?; + let kind = self.text(self.struct_field(value, 1)?.bits)?; + let secret = self.struct_field(value, 4)?.bits; + if secret > 1 { + return Err(INVALID_TYPE_INFO); + } + let arguments = self + .lists + .get(&self.struct_field(value, 5)?.bits) + .ok_or(INVALID_TYPE_INFO)?; + if !preserve_aliases && kind == "alias" { + let [Some(base)] = arguments.elements.as_slice() else { + return Err(INVALID_TYPE_INFO); + }; + work.push(Frame::Enter(Some(*base))); + continue; + } + let name = match kind { + "list" | "set" | "map" | "optional" | "result" | "secret" | "function" => "", + "struct" if !arguments.elements.is_empty() => type_name + .split_once('[') + .map_or(type_name, |(base, _)| base), + _ => type_name, + }; + let prefix = format!( + "{}:{}{}:{}{}:{}", + kind.len(), + kind, + name.len(), + name, + secret, + arguments.elements.len() + ); + identity.try_reserve(prefix.len()).map_err(|_| EXHAUSTED)?; + identity.push_str(&prefix); + for argument in arguments.elements.iter().rev() { + work.push(Frame::Enter(*argument)); + } + } + Ok(identity) + } + // This operation grants no Source mutation or Resource custody authority. + // The checked backend fixes nominal identity and ordinal. Every fallible + // registry/occupancy check precedes the single nonallocating occupied swap. + fn replace_struct_field( + &mut self, + id: u64, + index: u64, + replacement: NativeField, + ) -> LeafResult { + if self.actors.contains_key(&id) + || self.builders.contains_key(&id) + || self.checked_graph_hooks.contains_key(&id) + || self.actor_graph_imports.contains_key(&id) + { + return Err(INVALID_STRUCT); + } + let index = usize::try_from(index).map_err(|_| INVALID_STRUCT)?; + let record = self.structs.get(&id).ok_or(INVALID_STRUCT)?; + if record.pending_depth != 0 || record.fields.iter().any(Option::is_none) { + return Err(INVALID_STRUCT); + } + let displaced = record + .fields + .get(index) + .copied() + .flatten() + .ok_or(INVALID_STRUCT)?; + if displaced.owned != replacement.owned + || (replacement.owned && replacement.pending_depth != 0) + { + return Err(INVALID_STRUCT); + } + // Owned fields must still name live ordinary registry values. Actor + // identities are never ordinary owning handles; graph hooks remain + // opaque owning children and are retired by their existing Drop path. + for field in record + .fields + .iter() + .flatten() + .chain(std::iter::once(&replacement)) + { + if field.owned { + if field.bits == id || self.actors.contains_key(&field.bits) { + return Err(INVALID_STRUCT); + } + self.owned_pending_depth(field.bits)?; + } + } + // A live registry handle is not a second unique owner. The old slot + // remains owned until its returned value is retired, and siblings keep + // their ownership throughout. Only immutable strings share an ID after + // an explicit retain; function copies have distinct cloned descriptors. + // This checks direct edges, not whole-heap ownership or custody. + if replacement.owned { + let existing_edges = record + .fields + .iter() + .flatten() + .filter(|field| field.owned && field.bits == replacement.bits) + .count(); + if let Some(string) = self.strings.get(&replacement.bits) { + let required = u64::try_from(existing_edges) + .ok() + .and_then(|edges| edges.checked_add(1)) + .ok_or(INVALID_STRUCT)?; + if string.references < required { + return Err(INVALID_STRUCT); + } + } else if existing_edges != 0 { + return Err(INVALID_STRUCT); + } + } + let slot = self + .structs + .get_mut(&id) + .and_then(|record| record.fields.get_mut(index)) + .ok_or(INVALID_STRUCT)?; + let Some(current) = slot.as_mut() else { + return Err(INVALID_STRUCT); + }; + Ok(std::mem::replace(current, replacement)) + } + + fn take_struct_field(&mut self, id: u64, index: u64) -> LeafResult { + self.structs + .get_mut(&id) + .and_then(|s| { + usize::try_from(index) + .ok() + .and_then(|i| s.fields.get_mut(i)) + }) + .and_then(Option::take) + .ok_or(INVALID_STRUCT) + } + fn clone_struct(&mut self, id: u64) -> LeafResult { + // Function value copies may carry protected metadata without a struct ABI. + if self.checked_graph_hooks.contains_key(&id) { + return self.clone_checked_graph_hook(id, None); + } + let source = self.structs.get(&id).ok_or(INVALID_STRUCT)?; + let (fields, pending_depth) = (source.fields.clone(), source.pending_depth); + let builder = self.builders.get(&id).cloned(); + let output = self.new_struct(fields.len() as u64)?; + for (index, field) in fields.into_iter().enumerate() { + let Some(mut field) = field else { + continue; + }; + if field.owned { + match self.clone_value(field.bits) { + Ok(bits) => field.bits = bits, + Err(error) => { + self.drop_value(output)?; + return Err(error); + } + } + } + self.structs.get_mut(&output).ok_or(INVALID_STRUCT)?.fields[index] = Some(field); + } + if let Some(builder) = builder { + self.builders.insert(output, builder); + } + self.structs + .get_mut(&output) + .ok_or(INVALID_STRUCT)? + .pending_depth = pending_depth; + Ok(output) + } + fn run_record(&mut self, id: u64) -> LeafResult { + if let Some(value) = self.checked_graph_hooks.get(&id) { + let depth = value.pending_depth.checked_add(1).ok_or(EXHAUSTED)?; + return self.clone_checked_graph_hook(id, Some(depth)); + } + let depth = self + .structs + .get(&id) + .ok_or(INVALID_STRUCT)? + .pending_depth + .checked_add(1) + .ok_or(EXHAUSTED)?; + let pending = self.clone_struct(id)?; + self.structs + .get_mut(&pending) + .ok_or(INVALID_STRUCT)? + .pending_depth = depth; + Ok(pending) + } + fn join_record(&mut self, id: u64) -> LeafResult { + if let Some(value) = self.checked_graph_hooks.get(&id) { + let depth = value.pending_depth.saturating_sub(1); + return self.clone_checked_graph_hook(id, Some(depth)); + } + let depth = self + .structs + .get(&id) + .ok_or(INVALID_STRUCT)? + .pending_depth + .saturating_sub(1); + let joined = self.clone_struct(id)?; + self.structs + .get_mut(&joined) + .ok_or(INVALID_STRUCT)? + .pending_depth = depth; + Ok(joined) + } + fn run_sum(&mut self, id: u64) -> LeafResult { + let depth = self + .sums + .get(&id) + .ok_or(INVALID_SUM)? + .pending_depth + .checked_add(1) + .ok_or(EXHAUSTED)?; + let pending = self.clone_value(id)?; + self.sums + .get_mut(&pending) + .ok_or(INVALID_SUM)? + .pending_depth = depth; + Ok(pending) + } + fn join_sum(&mut self, id: u64) -> LeafResult { + let depth = self + .sums + .get(&id) + .ok_or(INVALID_SUM)? + .pending_depth + .saturating_sub(1); + let joined = self.clone_value(id)?; + self.sums.get_mut(&joined).ok_or(INVALID_SUM)?.pending_depth = depth; + Ok(joined) + } + fn clone_value(&mut self, id: u64) -> LeafResult { + if self.checked_graph_hooks.contains_key(&id) { + return self.clone_checked_graph_hook(id, None); + } + if self.structs.contains_key(&id) { + return self.clone_struct(id); + } + if self.lists.contains_key(&id) { + return self.clone_list(id); + } + if self.sets.contains_key(&id) { + return self.clone_set(id); + } + if self.maps.contains_key(&id) { + return self.clone_map(id); + } + if let Some(sum) = self.sums.get(&id) { + let (tag, bits, owned, pending_depth, payload_pending_depth) = ( + sum.tag, + sum.bits, + sum.owned, + sum.pending_depth, + sum.payload_pending_depth, + ); + let bits = if owned { self.clone_value(bits)? } else { bits }; + return match self.sum(tag, bits, owned) { + Ok(id) => { + let copy = self.sums.get_mut(&id).ok_or(INVALID_SUM)?; + copy.pending_depth = pending_depth; + copy.payload_pending_depth = payload_pending_depth; + Ok(id) + } + Err(error) => { + if owned { + self.drop_value(bits)?; + } + Err(error) + } + }; + } + if self.bytes.contains_key(&id) { + return self.clone_bytes(id); + } + self.retain(id) + } + fn drop_value(&mut self, id: u64) -> LeafResult { + if self.checked_graph_hooks.remove(&id).is_some() { + self.checked_graph_hooks_destroyed += 1; + return Ok(0); + } + if let Some(cursor) = self.equalities.remove(&id) { + return self.equality_drop(cursor); + } + if self.actors.contains_key(&id) { + return Err(INVALID_ACTOR); + } + if let Some(value) = self.structs.remove(&id) { + self.builders.remove(&id); + self.structs_destroyed += 1; + for field in value.fields.into_iter().flatten() { + if field.owned { + self.drop_value(field.bits)?; + } + } + return Ok(0); + } + if let Some(list) = self.lists.remove(&id) { + self.lists_destroyed += 1; + if list.owned { + for value in list.elements.into_iter().flatten() { + self.drop_value(value)?; + } + } + return Ok(0); + } + if let Some(set) = self.sets.remove(&id) { + self.sets_destroyed += 1; + if set.strings { + for value in set.elements.into_iter().flatten() { + self.drop_value(value)?; + } + } + return Ok(0); + } + if let Some(map) = self.maps.remove(&id) { + self.maps_destroyed += 1; + for entry in map.entries.into_iter().flatten() { + if map.key_strings && !entry.key_taken { + self.drop_value(entry.key)?; + } + if map.value_owned { + self.drop_value(entry.value)?; + } + } + return Ok(0); + } + if let Some(sum) = self.sums.remove(&id) { + self.sums_destroyed += 1; + if sum.owned { + self.drop_value(sum.bits)?; + } + return Ok(0); + } + if self.bytes.remove(&id).is_some() { + self.bytes_pending.remove(&id); + self.bytes_destroyed += 1; + return Ok(0); + } + self.release(id) + } + fn text(&self, id: u64) -> LeafResult<&str> { + self.strings + .get(&id) + .map(|s| s.text.as_str()) + .ok_or(INVALID_HANDLE) + } + fn retain(&mut self, id: u64) -> LeafResult { + let value = self.strings.get_mut(&id).ok_or(INVALID_HANDLE)?; + value.references = value.references.checked_add(1).ok_or(EXHAUSTED)?; + Ok(id) + } + fn release(&mut self, id: u64) -> LeafResult { + if id == 0 { + return Ok(0); + } + let value = self.strings.get_mut(&id).ok_or(INVALID_HANDLE)?; + value.references -= 1; + if value.references == 0 { + self.strings.remove(&id); + } + Ok(0) + } +} +fn native_split<'a>(haystack: &'a str, delimiter: &str) -> Vec<&'a str> { + if delimiter.is_empty() { + let graphemes = native_graphemes(haystack); + let mut parts = Vec::with_capacity(graphemes.len() + 2); + parts.push(""); + parts.extend(graphemes); + parts.push(""); + return parts; + } + + let mut parts = Vec::new(); + let mut part_start = 0; + native_scan_grapheme_matches(haystack, delimiter, |start, end, _| { + parts.push(&haystack[part_start..start]); + part_start = end; + true + }); + parts.push(&haystack[part_start..]); + parts +} + +fn native_replace(haystack: &str, needle: &str, replacement: &str) -> LeafResult { + let parts = native_split(haystack, needle); + let separators = parts.len().saturating_sub(1); + let mut length = replacement.len().checked_mul(separators).ok_or(EXHAUSTED)?; + for part in &parts { + length = length.checked_add(part.len()).ok_or(EXHAUSTED)?; + } + let mut output = String::new(); + output.try_reserve_exact(length).map_err(|_| EXHAUSTED)?; + for (index, part) in parts.into_iter().enumerate() { + if index != 0 { + output.push_str(replacement); + } + output.push_str(part); + } + Ok(output) +} + +fn native_change_first(value: &str, case: impl Fn(char) -> String) -> String { + let Some(first) = value.graphemes(true).next() else { + return String::new(); + }; + let mut chars = first.chars(); + let changed = chars.next().map(case).unwrap_or_default(); + format!("{changed}{}{}", chars.as_str(), &value[first.len()..]) +} + +fn native_slugify(value: &str) -> String { + value + .to_lowercase() + .chars() + .map(|character| { + if character.is_alphanumeric() { + character + } else { + '-' + } + }) + .collect::() + .split('-') + .filter(|part| !part.is_empty()) + .collect::>() + .join("-") +} + +/// Visit non-overlapping matches whose start and end are grapheme boundaries. +/// Returning false stops after the current match, as index_of requires. +fn native_scan_grapheme_matches( + haystack: &str, + needle: &str, + mut visit: impl FnMut(usize, usize, usize) -> bool, +) { + if needle.is_empty() || needle.len() > haystack.len() { + return; + } + let boundaries = native_grapheme_boundaries(haystack); + let needle = needle.as_bytes(); + let mut prefix = vec![0; needle.len()]; + for index in 1..needle.len() { + let mut matched = prefix[index - 1]; + while matched > 0 && needle[index] != needle[matched] { + matched = prefix[matched - 1]; + } + if needle[index] == needle[matched] { + matched += 1; + } + prefix[index] = matched; + } + + let mut matched = 0; + let mut start_boundary = 0; + let mut end_boundary = 0; + // KMP avoids comparing a long near-match again at every grapheme boundary. + // Both boundary cursors move only forward, so the entire scan is linear. + for (index, &byte) in haystack.as_bytes().iter().enumerate() { + while matched > 0 && byte != needle[matched] { + matched = prefix[matched - 1]; + } + if byte == needle[matched] { + matched += 1; + } + if matched != needle.len() { + continue; + } + + let end = index + 1; + let start = end - needle.len(); + while boundaries[start_boundary] < start { + start_boundary += 1; + } + while boundaries[end_boundary] < end { + end_boundary += 1; + } + if boundaries[start_boundary] == start && boundaries[end_boundary] == end { + if !visit(start, end, start_boundary) { + return; + } + matched = 0; + } else { + // A rejected byte match can overlap a later valid grapheme match. + matched = prefix[matched - 1]; + } + } +} + +fn native_grapheme_boundaries(s: &str) -> Vec { + let mut boundaries = vec![0]; + let mut offset = 0; + for cluster in native_graphemes(s) { + offset += cluster.len(); + boundaries.push(offset); + } + boundaries +} + +fn native_graphemes(s: &str) -> Vec<&str> { + UnicodeSegmentation::graphemes(s, true).collect() +} + +fn native_lines(text: &str) -> Vec { + let bytes = text.as_bytes(); + let mut parts = Vec::new(); + let mut start = 0; + let mut index = 0; + while index < bytes.len() { + if matches!(bytes[index], 13 | 10) { + parts.push(text[start..index].to_owned()); + if bytes[index] == 13 && bytes.get(index + 1) == Some(&10) { + index += 1; + } + index += 1; + start = index; + } else { + index += 1; + } + } + if start < bytes.len() { + parts.push(text[start..].to_owned()); + } + parts +} +fn next_identity() -> LeafResult { + reserve_native_identities(1)?.take() +} + +// Resource machinery samples the ordinary channel; it never takes or resets it. +pub(super) struct ResourceOrdinaryFailure<'a> { + pub(super) status: JettRuntimeStatusV1, + pub(super) message: &'a [u8], + pub(super) prefix: Option<&'a [u8]>, +} + +pub(super) struct ReservedNativeIdentities { + next: u64, + end: u64, +} + +pub(super) fn reserve_native_identities(count: u64) -> LeafResult { + static NEXT: AtomicU64 = AtomicU64::new(1); + if count == 0 { + return Err(EXHAUSTED); + } + let next = NEXT + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + if next == 0 { + None + } else { + next.checked_add(count) + } + }) + .map_err(|_| EXHAUSTED)?; + Ok(ReservedNativeIdentities { + next, + end: next + count, + }) +} + +impl ReservedNativeIdentities { + pub(super) fn take(&mut self) -> LeafResult { + if self.next == 0 || self.next >= self.end { + return Err(EXHAUSTED); + } + let value = self.next; + self.next += 1; + Ok(value) + } +} + +// A staged record cannot be created from an independently supplied Network bit. +pub(super) struct PreparedResourceNetwork { + authority: u64, +} + +impl NativeValues { + pub(super) fn resource_failure(&self) -> Option> { + self.current_failure() + .map(|(status, message)| ResourceOrdinaryFailure { + status, + message: self.current_dynamic_failure_message().unwrap_or(message), + prefix: self.current_property_case_context(), + }) + } + + pub(super) fn prepare_resource_network( + &mut self, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + if self.opaque_capabilities.contains_key("Network") { + return Err(INVALID_HANDLE); + } + self.opaque_capabilities + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + Ok(PreparedResourceNetwork { + authority: identities.take()?, + }) + } + + pub(super) fn commit_resource_network(&mut self, prepared: PreparedResourceNetwork) -> u64 { + let authority = prepared.authority; + // The caller holds the same state mutex from preparation through publication. + self.opaque_capabilities.insert("Network", authority); + authority + } + + /// Check ordinary carriers at the exact installed schema. This cannot recover + /// Resource tokens: occupied shapes are refused and the Resource table is separate. + pub(super) fn validate_resource_ordinary( + &self, + value: u64, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + ) -> LeafResult<()> { + use crate::resource_custody::NativeShape; + let mut shape = shape; + let mut value = value; + loop { + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::Integer { .. } | NativeShape::Float { .. } => return Ok(()), + NativeShape::Bool if value <= 1 => return Ok(()), + NativeShape::Nothing if value == 0 => return Ok(()), + NativeShape::String + if self + .strings + .get(&value) + .is_some_and(|s| s.references > 0 && s.pending_depth == 0) => + { + return Ok(()); + } + NativeShape::Network if self.validates_resource_network(value) => return Ok(()), + NativeShape::HookDescriptor { .. } => return Err(INVALID_HANDLE), + NativeShape::Optional { child } => { + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + if sum.pending_depth != 0 || sum.payload_pending_depth != 0 || sum.tag > 1 { + return Err(INVALID_HANDLE); + } + if sum.tag == 0 { + return if !sum.owned && sum.bits == 0 { + Ok(()) + } else { + Err(INVALID_HANDLE) + }; + } + let payload = layout.shapes().get(*child as usize).ok_or(INVALID_HANDLE)?; + let owned = matches!( + payload, + NativeShape::String + | NativeShape::Optional { .. } + | NativeShape::Result { .. } + ); + if sum.owned != owned { + return Err(INVALID_HANDLE); + } + shape = *child; + value = sum.bits; + } + NativeShape::Result { ok, fail } => { + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + if sum.pending_depth != 0 || sum.payload_pending_depth != 0 || sum.tag > 1 { + return Err(INVALID_HANDLE); + } + shape = if sum.tag == SUM_SUCCESS { *ok } else { *fail }; + let payload = layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)?; + let owned = matches!( + payload, + NativeShape::String + | NativeShape::Optional { .. } + | NativeShape::Result { .. } + ); + if sum.owned != owned { + return Err(INVALID_HANDLE); + } + value = sum.bits; + } + _ => return Err(INVALID_HANDLE), + } + } + } + + /// Copy only the exact ordinary String failure arm; never clone Resource data. + pub(super) fn clone_resource_string_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + ) -> LeafResult { + if !matches!( + layout.shapes().get(shape as usize), + Some(crate::resource_custody::NativeShape::String) + ) { + return Err(INVALID_HANDLE); + } + self.validate_resource_ordinary(value, layout, shape)?; + self.clone_value(value) + } + + /// Ordinary carrier children transport pending data without inspecting it. + /// Scalar depth is stored separately; strings retain their handle's depth. + pub(super) fn validate_resource_carrier_companion( + &self, + value: u64, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + scalar_depth: u64, + ) -> LeafResult<()> { + use crate::resource_custody::NativeShape; + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::Integer { bits, .. } if *bits < 64 && value >> *bits != 0 => { + Err(INVALID_HANDLE) + } + NativeShape::Float { bits: 32 } if value >> 32 != 0 => Err(INVALID_HANDLE), + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing => self.validate_resource_ordinary(value, layout, shape), + NativeShape::String if scalar_depth == 0 => { + let string = self.strings.get(&value).ok_or(INVALID_HANDLE)?; + if string.references == 0 { + return Err(INVALID_HANDLE); + } + Ok(()) + } + _ if scalar_depth == 0 => self.validate_resource_ordinary(value, layout, shape), + _ => Err(INVALID_HANDLE), + } + } + + pub(super) fn clone_resource_carrier_string_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + ) -> LeafResult { + if !matches!( + layout.shapes().get(shape as usize), + Some(crate::resource_custody::NativeShape::String) + ) { + return Err(INVALID_HANDLE); + } + self.validate_resource_carrier_companion(value, layout, shape, 0)?; + self.clone_value(value) + } + + pub(super) fn drop_resource_carrier_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + scalar_depth: u64, + ) -> LeafResult<()> { + self.validate_resource_carrier_companion(value, layout, shape, scalar_depth)?; + if matches!( + layout.shapes().get(shape as usize), + Some(crate::resource_custody::NativeShape::String) + ) { + self.drop_value(value).map(|_| ()) + } else { + self.drop_resource_typed_companion(layout, shape, value) + } + } + + pub(super) fn drop_resource_typed_companion( + &mut self, + layout: &crate::resource_custody::RegisteredNativeLayout, + shape: u32, + value: u64, + ) -> LeafResult<()> { + use crate::resource_custody::NativeShape; + self.validate_resource_ordinary(value, layout, shape)?; + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::String | NativeShape::Optional { .. } | NativeShape::Result { .. } => { + self.drop_value(value).map(|_| ()) + } + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing + | NativeShape::Network => Ok(()), + _ => Err(INVALID_HANDLE), + } + } + + pub(super) fn validates_resource_network(&self, authority: u64) -> bool { + authority != 0 && self.opaque_capabilities.get("Network") == Some(&authority) + } + + pub(super) fn prepare_resource_ordinary_output( + &mut self, + ) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + self.strings.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.sums.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.sums_created.checked_add(1).ok_or(EXHAUSTED)?; + reserve_native_identities(2) + } + + pub(super) fn publish_resource_error( + &mut self, + text: String, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + let id = identities.take()?; + self.strings.insert( + id, + NativeString { + text, + references: 1, + pending_depth: 0, + }, + ); + Ok(id) + } + + pub(super) fn publish_resource_borrow_result( + &mut self, + result: Result, + identities: &mut ReservedNativeIdentities, + ) -> LeafResult { + let (tag, bits, owned) = match result { + Ok(value) => (SUM_SUCCESS, value as u64, false), + Err(text) => ( + SUM_FAILURE, + self.publish_resource_error(text, identities)?, + true, + ), + }; + let id = identities.take()?; + self.sums.insert( + id, + NativeSum { + tag, + bits, + owned, + pending_depth: 0, + payload_pending_depth: 0, + }, + ); + self.sums_created += 1; + Ok(id) + } + + pub(super) fn drop_resource_ordinary_companion(&mut self, handle: u64) -> LeafResult<()> { + self.drop_value(handle).map(|_| ()) + } +} + +// Hold the existing context lease through the operation. Cleanup is permitted +// after failure; normal operations cannot run once the first failure is set. +trait FailureDefault { + fn failure_default() -> Self; +} +impl FailureDefault for u64 { + fn failure_default() -> Self { + 0 + } +} +impl FailureDefault for u32 { + fn failure_default() -> Self { + JettRuntimeStatusV1::INVALID_CONTEXT.code() + } +} +impl FailureDefault for i64 { + fn failure_default() -> Self { + 0 + } +} +impl FailureDefault for f64 { + fn failure_default() -> Self { + 0.0 + } +} +fn leaf( + context: *const JettRuntimeContextV1, + cleanup: bool, + operation: impl FnOnce(&mut NativeValues) -> LeafResult, +) -> T { + let Ok(key) = context_key(context) else { + return T::failure_default(); + }; + let Ok(lease) = acquire_context(key) else { + return T::failure_default(); + }; + let mut state = lock_unpoisoned(&lease.entry.state); + let Some(state) = state.as_mut() else { + return T::failure_default(); + }; + if state.values.operation_failure().is_some() && !cleanup { + return T::failure_default(); + } + match catch_unwind(AssertUnwindSafe(|| operation(&mut state.values))) { + Ok(Ok(value)) => value, + outcome => { + let error = match outcome { + Ok(Err(error)) => error, + Err(payload) => { + discard_panic_payload(payload); + (JettRuntimeStatusV1::PANIC, PANIC_MESSAGE) + } + Ok(Ok(_)) => unreachable!(), + }; + state.values.cleanup_failed |= cleanup; + state.values.record_ordinary_failure(error); + // Native status failure ends an unfinished compiler graph factory. + // Roll back while this same context state mutex is still held; + // callers may capture the first error and continue the context. + if let Err(abort_error) = state.values.abort_unfinished_actor_graphs() { + state.values.cleanup_failed = true; + state.values.record_ordinary_failure(abort_error); + } + T::failure_default() + } + } +} + +/// Configure an opt-in deterministic Clock provider before native program entry. +/// An empty script selects an exhausted provider rather than the wall clock. +/// +/// # Safety +/// `data` must point to `length` readable bytes when `length` is nonzero. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_clock_configure_scripted( + context: *const JettRuntimeContextV1, + data: *const u8, + length: u64, +) -> u32 { + leaf(context, false, |s| { + let invalid = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + clock::INVALID_SCRIPT.as_bytes(), + ); + if s.clock.is_some() || s.clock_script.is_some() { + return Err(invalid); + } + let length = usize::try_from(length).map_err(|_| invalid)?; + if length > isize::MAX as usize || (length != 0 && data.is_null()) { + return Err(invalid); + } + let bytes = if length == 0 { + &[][..] + } else { + // SAFETY: the caller supplies a readable slice for this call. + unsafe { std::slice::from_raw_parts(data, length) } + }; + let script = std::str::from_utf8(bytes).map_err(|_| invalid)?; + s.clock_script = Some(clock::decode_test_script(script).map_err(|_| invalid)?); + Ok(0) + }) +} + +/// Configure a deterministic Random provider before the capability is granted. +/// +/// # Safety +/// `data` must point to `length` readable bytes when `length` is nonzero. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_random_configure_scripted( + context: *const JettRuntimeContextV1, + data: *const u8, + length: u64, +) -> u32 { + leaf(context, false, |s| { + let invalid = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + random::INVALID_SCRIPT.as_bytes(), + ); + if s.random.is_some() || s.random_provider.is_some() { + return Err(invalid); + } + let length = usize::try_from(length).map_err(|_| invalid)?; + if length > isize::MAX as usize || (length != 0 && data.is_null()) { + return Err(invalid); + } + let bytes = if length == 0 { + &[][..] + } else { + // SAFETY: the caller supplies a readable slice for this call. + unsafe { std::slice::from_raw_parts(data, length) } + }; + let script = std::str::from_utf8(bytes).map_err(|_| invalid)?; + s.random_provider = Some(RandomProvider::scripted( + random::decode_test_script(script).map_err(|_| invalid)?, + )); + Ok(0) + }) +} + +/// Configure an isolated Environment launch snapshot before program entry. +/// +/// # Safety +/// `data` must point to `length` readable bytes when `length` is nonzero. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_environment_configure_snapshot( + context: *const JettRuntimeContextV1, + data: *const u8, + length: u64, +) -> u32 { + leaf(context, false, |s| { + let invalid = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + environment::INVALID_SNAPSHOT.as_bytes(), + ); + if s.environment.is_some() || s.environment_snapshot.is_some() { + return Err(invalid); + } + let length = usize::try_from(length).map_err(|_| invalid)?; + if length > isize::MAX as usize || (length != 0 && data.is_null()) { + return Err(invalid); + } + let bytes = if length == 0 { + &[][..] + } else { + // SAFETY: the caller supplies a readable slice for this call. + unsafe { std::slice::from_raw_parts(data, length) } + }; + let script = std::str::from_utf8(bytes).map_err(|_| invalid)?; + let snapshot = environment::decode_test_snapshot(script).map_err(|_| invalid)?; + s.environment_snapshot = Some( + LaunchEnvironmentSnapshot::injected(snapshot) + .map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?, + ); + Ok(0) + }) +} + +/// Configure deterministic Graphics events before program entry. No window is +/// opened until the compiled graphics session requests one. +/// +/// # Safety +/// `data` must point to `length` readable bytes when `length` is nonzero. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_graphics_configure_scripted( + context: *const JettRuntimeContextV1, + data: *const u8, + length: u64, +) -> u32 { + leaf(context, false, |s| { + let invalid = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + graphics::INVALID_TEST_SCRIPT.as_bytes(), + ); + if s.graphics.is_some() || s.graphics_script.is_some() { + return Err(invalid); + } + let length = usize::try_from(length).map_err(|_| invalid)?; + if length > isize::MAX as usize || (length != 0 && data.is_null()) { + return Err(invalid); + } + let bytes = if length == 0 { + &[][..] + } else { + // SAFETY: the caller supplies a readable slice for this call. + unsafe { std::slice::from_raw_parts(data, length) } + }; + let script = std::str::from_utf8(bytes).map_err(|_| invalid)?; + s.graphics_script = Some(graphics::decode_test_script(script).map_err(|_| invalid)?); + Ok(0) + }) +} + +/// Complete a successful native entry by rejecting unconsumed deterministic +/// provider samples. An entry that already failed must skip this check so its +/// original runtime error remains the terminal failure. +/// +/// # Safety +/// `context` must identify a live, stationary runtime context. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_validate_scripted_consumption( + context: *const JettRuntimeContextV1, +) -> u32 { + leaf(context, false, NativeValues::validate_scripted_consumption) +} + +/// Scalar signature schema consumed by Cranelift, never inferred from names. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AbiScalar { + Pointer, + I32, + I64, + F64, +} +macro_rules! leaves { + ($( $variant:ident, $name:ident, $cleanup:literal, ($($arg:ident: $rust:ty => $abi:ident),*), $ret:ty => $retabi:ident, $body:expr; )*) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum NativeLeaf { $( $variant, )* } + impl NativeLeaf { + pub fn symbol(self) -> &'static str { match self { $( Self::$variant => stringify!($name), )* } } + pub fn parameters(self) -> &'static [AbiScalar] { match self { $( Self::$variant => &[AbiScalar::Pointer, $( AbiScalar::$abi, )*], )* } } + pub fn result(self) -> AbiScalar { match self { $( Self::$variant => AbiScalar::$retabi, )* } } + } + $( + /// Typed leaf operation; result ownership follows its operation contract. + /// # Safety + /// Context must be readable, stationary and live for the call. + /// Pointer/length inputs must describe one readable allocation. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $name(context: *const JettRuntimeContextV1, $( $arg: $rust ),*) -> $ret { + leaf(context, $cleanup, $body) + } + )* + } +} +leaves! { + BuilderNew, jett_rt_v1_builder_new, false, (layout_pointer: u64 => I64, layout_length: u64 => I64), u64 => I64, + |s| { if layout_pointer == 0 { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.new_builder(layout) }; + BuilderVariantNew, jett_rt_v1_builder_variant_new, false, (layout_pointer: u64 => I64, layout_length: u64 => I64, metadata: u64 => I64, debug_pointer: u64 => I64, debug_length: u64 => I64), u64 => I64, + |s| { if layout_pointer == 0 || debug_pointer == 0 { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let debug_length = usize::try_from(debug_length).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if length > isize::MAX as usize || debug_length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + let debug = unsafe { std::slice::from_raw_parts(debug_pointer as *const u8, debug_length) }; + s.new_member_builder(layout, metadata, debug, false) }; + BuilderMachineNew, jett_rt_v1_builder_machine_new, false, (layout_pointer: u64 => I64, layout_length: u64 => I64, metadata: u64 => I64, debug_pointer: u64 => I64, debug_length: u64 => I64), u64 => I64, + |s| { if layout_pointer == 0 || debug_pointer == 0 { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + let debug_length = usize::try_from(debug_length).map_err(|_| INVALID_CONSTRUCTION_LAYOUT)?; + if length > isize::MAX as usize || debug_length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION_LAYOUT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + let debug = unsafe { std::slice::from_raw_parts(debug_pointer as *const u8, debug_length) }; + s.new_member_builder(layout, metadata, debug, true) }; + BuilderPut, jett_rt_v1_builder_put, false, (builder: u64 => I64, field: u64 => I64, bits: u64 => I64, owned: u32 => I32, owner_pointer: u64 => I64, owner_length: u64 => I64, type_pointer: u64 => I64, type_length: u64 => I64, canonical_pointer: u64 => I64, canonical_length: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u64 => I64, + |s| { if owned > 1 || owner_pointer == 0 || type_pointer == 0 || canonical_pointer == 0 || layout_pointer == 0 { return Err(INVALID_CONSTRUCTION); } + let owner_length = usize::try_from(owner_length).map_err(|_| INVALID_CONSTRUCTION)?; + let type_length = usize::try_from(type_length).map_err(|_| INVALID_CONSTRUCTION)?; + let canonical_length = usize::try_from(canonical_length).map_err(|_| INVALID_CONSTRUCTION)?; + let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_CONSTRUCTION)?; + if owner_length > isize::MAX as usize || type_length > isize::MAX as usize || canonical_length > isize::MAX as usize || layout_length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION); } + let owner = unsafe { std::slice::from_raw_parts(owner_pointer as *const u8, owner_length) }; + let provided_type = unsafe { std::slice::from_raw_parts(type_pointer as *const u8, type_length) }; + let canonical_type = unsafe { std::slice::from_raw_parts(canonical_pointer as *const u8, canonical_length) }; + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, layout_length) }; + let owner = std::str::from_utf8(owner).map_err(|_| INVALID_CONSTRUCTION)?; + let provided_type = std::str::from_utf8(provided_type).map_err(|_| INVALID_CONSTRUCTION)?; + let canonical_type = std::str::from_utf8(canonical_type).map_err(|_| INVALID_CONSTRUCTION)?; + s.builder_put(builder, field, bits, owned != 0, owner, provided_type, canonical_type, layout) }; + BuilderPutScalarTask, jett_rt_v1_builder_put_scalar_task, false, (builder: u64 => I64, field: u64 => I64, bits: u64 => I64, depth: u64 => I64, owned: u32 => I32, owner_pointer: u64 => I64, owner_length: u64 => I64, type_pointer: u64 => I64, type_length: u64 => I64, canonical_pointer: u64 => I64, canonical_length: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u64 => I64, + |s| { if owned > 1 || owner_pointer == 0 || type_pointer == 0 || canonical_pointer == 0 || layout_pointer == 0 { return Err(INVALID_CONSTRUCTION); } + let owner_length = usize::try_from(owner_length).map_err(|_| INVALID_CONSTRUCTION)?; + let type_length = usize::try_from(type_length).map_err(|_| INVALID_CONSTRUCTION)?; + let canonical_length = usize::try_from(canonical_length).map_err(|_| INVALID_CONSTRUCTION)?; + let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_CONSTRUCTION)?; + if owner_length > isize::MAX as usize || type_length > isize::MAX as usize || canonical_length > isize::MAX as usize || layout_length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION); } + let owner = unsafe { std::slice::from_raw_parts(owner_pointer as *const u8, owner_length) }; + let provided_type = unsafe { std::slice::from_raw_parts(type_pointer as *const u8, type_length) }; + let canonical_type = unsafe { std::slice::from_raw_parts(canonical_pointer as *const u8, canonical_length) }; + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, layout_length) }; + let owner = std::str::from_utf8(owner).map_err(|_| INVALID_CONSTRUCTION)?; + let provided_type = std::str::from_utf8(provided_type).map_err(|_| INVALID_CONSTRUCTION)?; + let canonical_type = std::str::from_utf8(canonical_type).map_err(|_| INVALID_CONSTRUCTION)?; + s.builder_put_depth(builder, field, bits, depth, owned != 0, owner, provided_type, canonical_type, layout) }; + BuilderFinish, jett_rt_v1_builder_finish, false, (builder: u64 => I64, owner_pointer: u64 => I64, owner_length: u64 => I64), u64 => I64, + |s| { if owner_pointer == 0 { return Err(INVALID_CONSTRUCTION); } + let owner_length = usize::try_from(owner_length).map_err(|_| INVALID_CONSTRUCTION)?; + if owner_length > isize::MAX as usize { return Err(INVALID_CONSTRUCTION); } + let owner = unsafe { std::slice::from_raw_parts(owner_pointer as *const u8, owner_length) }; + let owner = std::str::from_utf8(owner).map_err(|_| INVALID_CONSTRUCTION)?; + s.builder_finish(builder, owner) }; + FunctionAdapter, jett_rt_v1_function_adapter, false, (source: u64 => I64, code: u64 => I64), u64 => I64, + |s| s.function_adapter(source, code); + InterfaceValidate, jett_rt_v1_interface_validate, false, (value: u64 => I64, layout: *const u8 => Pointer, length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize || layout.is_null() { return Err(INVALID_STRUCT); } + let bytes = unsafe { std::slice::from_raw_parts(layout, length) }; + let conversion = interface_conversion::NativeInterfaceConversion::parse(bytes)?; + conversion.validate_owners(s, NativeField { bits: value, owned: true, pending_depth: 0 })?; + Ok(0) }; + InterfaceConvert, jett_rt_v1_interface_convert, false, (value: u64 => I64, layout: *const u8 => Pointer, length: u64 => I64), u64 => I64, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize || layout.is_null() { return Err(INVALID_STRUCT); } + let bytes = unsafe { std::slice::from_raw_parts(layout, length) }; + let conversion = interface_conversion::NativeInterfaceConversion::parse(bytes)?; + let field = conversion.convert(s, NativeField { bits: value, owned: true, pending_depth: 0 })?; + if !field.owned { return Err(INVALID_STRUCT); } + Ok(field.bits) }; + InterfaceBox, jett_rt_v1_interface_box, false, (concrete: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64, layout: *const u8 => Pointer, length: u64 => I64), u64 => I64, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_STRUCT)?; + if owned > 1 || length > isize::MAX as usize || (length != 0 && layout.is_null()) { return Err(INVALID_STRUCT); } + let layout = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, length) } }; + s.interface_box(concrete, bits, owned != 0, depth, layout) }; + InterfaceType, jett_rt_v1_interface_type, false, (value: u64 => I64), u64 => I64, + |s| { let record = s.structs.get(&value).ok_or(INVALID_STRUCT)?; + if record.pending_depth != 0 || s.struct_field(value, 1)?.pending_depth != 0 { return Ok(u64::MAX); } + Ok(s.struct_field(value, 0)?.bits) }; + InterfaceUnbox, jett_rt_v1_interface_unbox, false, (value: u64 => I64, concrete: u64 => I64), u64 => I64, + |s| { if s.struct_field(value, 0)?.bits != concrete { return Err(INVALID_STRUCT); } + let field = s.struct_field(value, 1)?; + let depth = s.structs.get(&value).ok_or(INVALID_STRUCT)?.pending_depth; + if field.owned { s.clone_with_pending_depth(field.bits, depth) } else { Ok(field.bits) } }; + InterfacePendingDepth, jett_rt_v1_interface_pending_depth, false, (value: u64 => I64), u64 => I64, + |s| s.structs.get(&value).map(|v| v.pending_depth).ok_or(INVALID_STRUCT); + RuntimeFailMessage, jett_rt_v1_runtime_fail_message, false, (message: u64 => I64), u32 => I32, + |s| { s.set_dynamic_failure_message(s.text(message)?.as_bytes().to_vec()); + Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"native runtime failure")) }; + StructNew, jett_rt_v1_struct_new, false, (count: u64 => I64), u64 => I64, + |s| s.new_struct(count); + StructInit, jett_rt_v1_struct_init, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32), u32 => I32, + |s| { if owned > 1 { return Err(INVALID_STRUCT); } + let slot = s.structs.get_mut(&value).and_then(|v| usize::try_from(index).ok().and_then(|i| v.fields.get_mut(i))).ok_or(INVALID_STRUCT)?; + if slot.is_some() { return Err(INVALID_STRUCT); } + *slot = Some(NativeField { bits, owned: owned != 0, pending_depth: 0 }); Ok(0) }; + StructInitScalarTask, jett_rt_v1_struct_init_scalar_task, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, depth: u64 => I64), u32 => I32, + |s| { let slot = s.structs.get_mut(&value).and_then(|v| usize::try_from(index).ok().and_then(|i| v.fields.get_mut(i))).ok_or(INVALID_STRUCT)?; + if slot.is_some() { return Err(INVALID_STRUCT); } + *slot = Some(NativeField { bits, owned: false, pending_depth: depth }); Ok(0) }; + StructPendingAccessCheck, jett_rt_v1_struct_pending_access_check, false, (value: u64 => I64, layout: *const u8 => Pointer, layout_length: u64 => I64, message: *const u8 => Pointer, message_length: u64 => I64, render: u32 => I32), u32 => I32, + |s| { let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_STRUCT)?; + let message_length = usize::try_from(message_length).map_err(|_| INVALID_STRUCT)?; + if render > 1 || layout_length > isize::MAX as usize || message_length > isize::MAX as usize + || (layout_length != 0 && layout.is_null()) || (message_length != 0 && message.is_null()) { return Err(INVALID_STRUCT); } + let layout = if layout_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, layout_length) } }; + let message = if message_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(message, message_length) } }; + let message = std::str::from_utf8(message).map_err(|_| INVALID_STRUCT)?; + s.check_struct_pending_access(value, layout, message, render != 0) }; + StructField, jett_rt_v1_struct_field, false, (value: u64 => I64, index: u64 => I64), u64 => I64, + |s| Ok(s.struct_field(value, index)?.bits); + StructFieldPendingDepth, jett_rt_v1_struct_field_pending_depth, false, (value: u64 => I64, index: u64 => I64), u64 => I64, + |s| Ok(s.struct_field(value, index)?.pending_depth); + ActorRegister, jett_rt_v1_actor_register, false, (value: u64 => I64), u64 => I64, + |s| s.register_actor(value); + ActorGraphBegin, jett_rt_v1_actor_graph_begin, false, (), u64 => I64, + |s| s.begin_actor_graph(); + ActorGraphReserveBase, jett_rt_v1_actor_graph_reserve_base, false, (ticket: u64 => I64, field_count: u64 => I64), u64 => I64, + |s| s.reserve_actor_graph_base(ticket, field_count); + ActorGraphReservePending, jett_rt_v1_actor_graph_reserve_pending, false, (ticket: u64 => I64, base: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.reserve_actor_graph_pending(ticket, base, depth); + ActorGraphFill, jett_rt_v1_actor_graph_fill, false, (ticket: u64 => I64, base: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64, layout: *const u8 => Pointer, layout_length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(layout_length).map_err(|_| actor_graph_import::INVALID_IMPORT)?; + if owned > 1 || length > isize::MAX as usize || layout.is_null() { return Err(actor_graph_import::INVALID_IMPORT); } + let bytes = unsafe { std::slice::from_raw_parts(layout, length) }; + s.fill_actor_graph(ticket, base, index, NativeField { bits, owned: owned != 0, pending_depth: depth }, bytes) }; + ActorGraphCommit, jett_rt_v1_actor_graph_commit, false, (ticket: u64 => I64), u32 => I32, + |s| s.commit_actor_graph(ticket); + ActorGraphRootFill, jett_rt_v1_actor_graph_root_fill, false, (ticket: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64, layout: *const u8 => Pointer, layout_length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(layout_length).map_err(|_| actor_graph_import::INVALID_IMPORT)?; + if owned > 1 || length > isize::MAX as usize || layout.is_null() { return Err(actor_graph_import::INVALID_IMPORT); } + let bytes = unsafe { std::slice::from_raw_parts(layout, length) }; + s.fill_actor_graph_root(ticket, NativeField { bits, owned: owned != 0, pending_depth: depth }, bytes) }; + ActorGraphAbort, jett_rt_v1_actor_graph_abort, true, (ticket: u64 => I64), u32 => I32, + |s| s.abort_actor_graph(ticket); + ActorIdentityEqual, jett_rt_v1_actor_identity_equal, false, (left: u64 => I64, right: u64 => I64, not_equal: u32 => I32), u32 => I32, + |s| s.equal_actor_identity(left, right, not_equal); + ActorRun, jett_rt_v1_actor_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_actor(value); + ActorTaskJoin, jett_rt_v1_actor_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_actor(value); + CapabilityRun, jett_rt_v1_capability_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_capability(value); + CapabilityTaskJoin, jett_rt_v1_capability_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_capability(value); + ActorMessageCheck, jett_rt_v1_actor_message_check, false, (value: u64 => I64), u32 => I32, + |s| s.check_actor_message(value); + ActorReplace, jett_rt_v1_actor_replace, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32), u32 => I32, + |s| { if owned > 1 { return Err(INVALID_ACTOR); } + s.replace_actor_field(value, index, bits, owned != 0) }; + ActorReplaceOccupied, jett_rt_v1_actor_replace_occupied, false, (actor: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64), u64 => I64, + |s| { if owned > 1 { return Err(INVALID_ACTOR); } + Ok(s.replace_actor_field_occupied(actor, index, NativeField { bits, owned: owned != 0, pending_depth: depth })?.bits) }; + ActorReplaceScalarTask, jett_rt_v1_actor_replace_scalar_task, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, depth: u64 => I64), u32 => I32, + |s| s.replace_actor_field_depth(value, index, bits, depth, false); + ReflectedFieldIndex, jett_rt_v1_reflected_field_index, false, (actual: u64 => I64, expected: u64 => I64), u64 => I64, + |s| s.reflected_field_index(actual, expected, INVALID_REFLECTED_FIELD, "type.field_value"); + ReflectedVariantFieldIndex, jett_rt_v1_reflected_variant_field_index, false, (actual: u64 => I64, expected: u64 => I64), u64 => I64, + |s| s.reflected_field_index(actual, expected, INVALID_REFLECTED_VARIANT_FIELD, "type.variant_field_value"); + ReflectedMachineFieldIndex, jett_rt_v1_reflected_machine_field_index, false, (actual: u64 => I64, expected: u64 => I64), u64 => I64, + |s| s.reflected_field_index(actual, expected, INVALID_REFLECTED_MACHINE_FIELD, "type.machine_field_value"); + ReflectedCheckedFieldIndex, jett_rt_v1_reflected_checked_field_index, false, (actual: u64 => I64, expected: u64 => I64, compatible: u64 => I64, requested_type: *const u8 => Pointer, requested_length: u64 => I64, kind: u32 => I32), u64 => I64, + |s| { let length = usize::try_from(requested_length).map_err(|_| INVALID_REFLECTED_FIELD)?; + if length > isize::MAX as usize || (length != 0 && requested_type.is_null()) { return Err(INVALID_REFLECTED_FIELD); } + let bytes = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(requested_type, length) } }; + let requested = std::str::from_utf8(bytes).map_err(|_| INVALID_REFLECTED_FIELD)?; + let (mismatch, caller) = match kind { + 0 => (INVALID_REFLECTED_FIELD, "type.field_value"), + 1 => (INVALID_REFLECTED_VARIANT_FIELD, "type.variant_field_value"), + 2 => (INVALID_REFLECTED_MACHINE_FIELD, "type.machine_field_value"), + _ => return Err(INVALID_REFLECTED_FIELD), + }; + s.reflected_checked_field_index(actual, expected, compatible, requested, mismatch, caller) }; + ReflectedFieldOwnerCheck, jett_rt_v1_reflected_field_owner_check, false, (actual: u64 => I64, owner: *const u8 => Pointer, owner_length: u64 => I64, member: *const u8 => Pointer, member_length: u64 => I64, has_member: u32 => I32, kind: u32 => I32, layout: *const u8 => Pointer, layout_length: u64 => I64), u32 => I32, + |s| { let owner_length = usize::try_from(owner_length).map_err(|_| INVALID_REFLECTED_FIELD)?; + let member_length = usize::try_from(member_length).map_err(|_| INVALID_REFLECTED_FIELD)?; + if owner_length > isize::MAX as usize || (owner_length != 0 && owner.is_null()) + || member_length > isize::MAX as usize || (member_length != 0 && member.is_null()) + || has_member > 1 { return Err(INVALID_REFLECTED_FIELD); } + let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_REFLECTED_FIELD)?; + if layout_length > isize::MAX as usize || (layout_length != 0 && layout.is_null()) { return Err(INVALID_REFLECTED_FIELD); } + let owner = if owner_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(owner, owner_length) } }; + let member = if member_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(member, member_length) } }; + let layout = if layout_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, layout_length) } }; + let owner = std::str::from_utf8(owner).map_err(|_| INVALID_REFLECTED_FIELD)?; + let member = std::str::from_utf8(member).map_err(|_| INVALID_REFLECTED_FIELD)?; + let (mismatch, caller) = match kind { + 0 => (INVALID_REFLECTED_FIELD, "type.field_value"), + 1 => (INVALID_REFLECTED_VARIANT_FIELD, "type.variant_field_value"), + 2 => (INVALID_REFLECTED_MACHINE_FIELD, "type.machine_field_value"), + _ => return Err(INVALID_REFLECTED_FIELD), + }; + s.check_reflected_field_owner(actual, owner, (has_member != 0).then_some(member), layout, mismatch, caller) }; + ReflectedOwnerPendingCheck, jett_rt_v1_reflected_owner_pending_check, false, (value: u64 => I64, owner: *const u8 => Pointer, owner_length: u64 => I64, layout: *const u8 => Pointer, layout_length: u64 => I64, kind: u32 => I32), u32 => I32, + |s| { let owner_length = usize::try_from(owner_length).map_err(|_| INVALID_REFLECTED_OWNER)?; + let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_REFLECTED_OWNER)?; + if owner_length > isize::MAX as usize || (owner_length != 0 && owner.is_null()) + || layout_length > isize::MAX as usize || (layout_length != 0 && layout.is_null()) { return Err(INVALID_REFLECTED_OWNER); } + let owner = if owner_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(owner, owner_length) } }; + let layout = if layout_length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, layout_length) } }; + let owner = std::str::from_utf8(owner).map_err(|_| INVALID_REFLECTED_OWNER)?; + s.check_reflected_owner_pending(value, owner, layout, kind) }; + ReflectedFieldMetadataPendingCheck, jett_rt_v1_reflected_field_metadata_pending_check, false, (metadata: u64 => I64, layout: *const u8 => Pointer, layout_length: u64 => I64, kind: u32 => I32), u32 => I32, + |s| { let (caller, mismatch) = match kind { + 1 => ("type.variant_field_value", INVALID_REFLECTED_VARIANT_FIELD), + 2 => ("type.machine_field_value", INVALID_REFLECTED_MACHINE_FIELD), + _ => return Err(INVALID_REFLECTED_FIELD), + }; + let length = usize::try_from(layout_length).map_err(|_| mismatch)?; + if length > isize::MAX as usize || (length != 0 && layout.is_null()) { return Err(mismatch); } + let layout = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(layout, length) } }; + if let Some(message) = s.pending_type_field_message(metadata, layout, caller).map_err(|_| mismatch)? { + s.set_dynamic_failure_message(message.into_bytes()); + Err(mismatch) + } else { + Ok(0) + } }; + TypeInfoMatches, jett_rt_v1_type_info_matches, false, (actual: u64 => I64, expected: *const u8 => Pointer, length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_TYPE_INFO)?; + if length > isize::MAX as usize || (length != 0 && expected.is_null()) { return Err(INVALID_TYPE_INFO); } + let expected = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(expected, length) } }; + Ok(u32::from(s.type_info_identity(actual, false)?.as_bytes() == expected)) }; + TypeInfoReflectionMatches, jett_rt_v1_type_info_reflection_matches, false, (actual: u64 => I64, expected: *const u8 => Pointer, length: u64 => I64), u32 => I32, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_TYPE_INFO)?; + if length > isize::MAX as usize || (length != 0 && expected.is_null()) { return Err(INVALID_TYPE_INFO); } + let expected = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(expected, length) } }; + Ok(u32::from(s.type_info_identity(actual, true)?.as_bytes() == expected)) }; + TypeArgIndex, jett_rt_v1_type_arg_index, false, (index: i64 => I64, count: u64 => I64), u64 => I64, + |_s| { let index = u64::try_from(index).map_err(|_| INVALID_TYPE_ARG_INDEX)?; + if index >= count { return Err(INVALID_TYPE_ARG_INDEX); } + Ok(index) }; + TypeArgCheckedIndex, jett_rt_v1_type_arg_checked_index, false, (index: i64 => I64, count: u64 => I64, depth: u64 => I64, type_name: *const u8 => Pointer, type_name_length: u64 => I64), u64 => I64, + |s| { let length = usize::try_from(type_name_length).map_err(|_| INVALID_TYPE_ARG_INDEX)?; + if length > isize::MAX as usize || (length != 0 && type_name.is_null()) { return Err(INVALID_TYPE_ARG_INDEX); } + let bytes = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(type_name, length) } }; + let name = std::str::from_utf8(bytes).map_err(|_| INVALID_TYPE_ARG_INDEX)?; + s.checked_type_arg_index(index, count, depth, name) }; + MachineExpectState, jett_rt_v1_machine_expect_state, false, (value: u64 => I64, state: u64 => I64), u32 => I32, + |s| { if s.struct_field(value, 0)?.bits == state { Ok(0) } + else { Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"machine state does not match narrowed type")) } }; + StructReplaceOccupied, jett_rt_v1_struct_replace_occupied, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64), u64 => I64, + |s| { if owned > 1 { return Err(INVALID_STRUCT); } + Ok(s.replace_struct_field(value, index, NativeField { bits, owned: owned != 0, pending_depth: depth })?.bits) }; + StructTake, jett_rt_v1_struct_take, false, (value: u64 => I64, index: u64 => I64), u64 => I64, + |s| Ok(s.take_struct_field(value, index)?.bits); + StructClone, jett_rt_v1_struct_clone, false, (value: u64 => I64), u64 => I64, + |s| s.clone_struct(value); + RecordRun, jett_rt_v1_record_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_record(value); + RecordTaskJoin, jett_rt_v1_record_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_record(value); + FunctionCallableCheck, jett_rt_v1_function_callable_check, false, (value: u64 => I64), u32 => I32, + |s| s.check_function_callable(value); + StringChars, jett_rt_v1_string_chars, false, (value: u64 => I64), u64 => I64, + |s| { let parts = s.text(value)?.graphemes(true).map(str::to_owned).collect(); s.string_list(parts) }; + StringScalarCount, jett_rt_v1_string_scalar_count, false, (value: u64 => I64), i64 => I64, + |s| Ok(s.text(value)?.chars().count() as i64); + StringScalarAt, jett_rt_v1_string_scalar_at, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let index = usize::try_from(index).map_err(|_| INVALID_STRING_SCALAR_INDEX)?; + let scalar = s.text(value)?.chars().nth(index).ok_or(INVALID_STRING_SCALAR_INDEX)?; + s.insert(scalar.to_string()) }; + StringWords, jett_rt_v1_string_words, false, (value: u64 => I64), u64 => I64, + |s| { let parts = s.text(value)?.split_whitespace().map(str::to_owned).collect(); s.string_list(parts) }; + StringLines, jett_rt_v1_string_lines, false, (value: u64 => I64), u64 => I64, + |s| { let parts = native_lines(s.text(value)?); s.string_list(parts) }; + SequenceCheckIterable, jett_rt_v1_sequence_check_iterable, false, (value: u64 => I64, kind: u32 => I32), u32 => I32, + |s| s.check_iterable(value, kind); + StringSplit, jett_rt_v1_string_split, false, (value: u64 => I64, delimiter: u64 => I64), u64 => I64, + |s| { let parts = native_split(s.text(value)?, s.text(delimiter)?).into_iter().map(str::to_owned).collect(); s.string_list(parts) }; + StringJoin, jett_rt_v1_string_join, false, (value: u64 => I64, separator: u64 => I64), u64 => I64, + |s| s.join_strings(value, separator); + Range, jett_rt_v1_range_int64, false, (start: i64 => I64, end: i64 => I64, step: i64 => I64), u64 => I64, + |s| s.range(start, end, step); + CheckIndexCountArguments, jett_rt_v1_check_index_count_arguments, false, (value: u64 => I64, kind: u32 => I32, first_depth: u64 => I64, second_depth: u64 => I64, third_depth: u64 => I64), u32 => I32, + |s| s.check_index_count_arguments(value, kind, first_depth, second_depth, third_depth); + RejectPendingScalars, jett_rt_v1_reject_pending_scalars, false, (first_depth: u64 => I64, second_depth: u64 => I64, third_depth: u64 => I64, message_pointer: u64 => I64, message_length: u64 => I64), u32 => I32, + |s| { if message_pointer == 0 { return Err(INVALID_PENDING_SCALAR_CHECK); } + let length = usize::try_from(message_length).map_err(|_| INVALID_PENDING_SCALAR_CHECK)?; + if length > isize::MAX as usize { return Err(INVALID_PENDING_SCALAR_CHECK); } + let message = unsafe { std::slice::from_raw_parts(message_pointer as *const u8, length) }; + s.reject_pending_scalars(first_depth, second_depth, third_depth, message) }; + RejectPendingHandle, jett_rt_v1_reject_pending_handle, false, (handle: u64 => I64, kind: u32 => I32, message_pointer: u64 => I64, message_length: u64 => I64), u32 => I32, + |s| { if message_pointer == 0 { return Err(INVALID_PENDING_HANDLE_CHECK); } + let length = usize::try_from(message_length).map_err(|_| INVALID_PENDING_HANDLE_CHECK)?; + if length > isize::MAX as usize { return Err(INVALID_PENDING_HANDLE_CHECK); } + let message = unsafe { std::slice::from_raw_parts(message_pointer as *const u8, length) }; + s.reject_pending_handle(handle, kind, message) }; + DisplayResultCheck, jett_rt_v1_display_result_check, false, (value: u64 => I64), u32 => I32, + |s| s.check_display_result(value); + ListElementTake, jett_rt_v1_list_element_take, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let list = s.lists.get_mut(&value).ok_or(INVALID_LIST)?; + let index = usize::try_from(index).map_err(|_| INVALID_LIST)?; + let bits = list.elements.get_mut(index).and_then(Option::take).ok_or(INVALID_LIST)?; + list.element_pending_depths.remove(&index); + Ok(bits) }; + + ListElementPendingDepth, jett_rt_v1_list_element_pending_depth, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let list = s.lists.get(&value).ok_or(INVALID_LIST)?; + let index = usize::try_from(index).map_err(|_| INVALID_LIST)?; + list.elements.get(index).copied().flatten().ok_or(INVALID_LIST)?; + Ok(list.element_pending_depths.get(&index).copied().unwrap_or(0)) }; + + ListElementClone, jett_rt_v1_list_element_clone, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let list = s.lists.get(&value).ok_or(INVALID_LIST)?; + let bits = usize::try_from(index).ok().and_then(|i| list.elements.get(i)).copied().flatten().ok_or(INVALID_LIST)?; + if list.owned { s.clone_value(bits) } else { Ok(bits) } }; + + ListSumInt, jett_rt_v1_list_sum_int64, false, (value: u64 => I64), i64 => I64, + |s| s.sum_primitive_list(value, NativeSortKind::Int64 as u32).map(|bits| bits as i64); + + ListSumPrimitive, jett_rt_v1_list_sum_primitive, false, (value: u64 => I64, kind: u32 => I32), u64 => I64, + |s| s.sum_primitive_list(value, kind); + + MathAverage, jett_rt_v1_math_average, false, (value: u64 => I64, kind: u32 => I32), f64 => F64, + |s| { let numbers = s.math_numbers(value, kind, (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.average: list is empty"), (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.average expects a list of numeric values"), (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.__average expects a list of numbers"))?; + Ok(math::float_average(&numbers)) }; + MathMedian, jett_rt_v1_math_median, false, (value: u64 => I64, kind: u32 => I32), f64 => F64, + |s| { let mut numbers = s.math_numbers(value, kind, (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.median: list is empty"), (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.median expects a list of numeric values"), (JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.__median expects a list of numbers"))?; + numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(CompareOrdering::Equal)); + let middle = numbers.len() / 2; + Ok(if numbers.len() % 2 == 0 { math::float_midpoint(numbers[middle - 1], numbers[middle]) } else { numbers[middle] }) }; + + ListNew, jett_rt_v1_list_new, false, (owned: u32 => I32), u64 => I64, + |s| { if owned > 1 { return Err(INVALID_LIST); } s.new_list(owned != 0) }; + ListCheckArguments, jett_rt_v1_list_check_arguments, false, (value: u64 => I64, kind: u32 => I32, first_depth: u64 => I64, second_depth: u64 => I64), u32 => I32, + |s| s.check_list_arguments(value, kind, first_depth, second_depth); + ListInsertAt, jett_rt_v1_list_insert_at, false, (value: u64 => I64, index: i64 => I64, bits: u64 => I64), u64 => I64, + |s| s.insert_list(value, index, bits, 0); + ListInsertAtScalarTask, jett_rt_v1_list_insert_at_scalar_task, false, (value: u64 => I64, index: i64 => I64, bits: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.insert_list(value, index, bits, depth); + ListRemoveAt, jett_rt_v1_list_remove_at, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let invalid = (JettRuntimeStatusV1::INVALID_ARGUMENT, b"list.__remove_at: index out of bounds".as_slice()); + let position = match usize::try_from(index) { + Ok(position) => position, + Err(_) => return s.list_index_failure("list.__remove_at", index, invalid), + }; + let list = s.lists.get(&value).ok_or(INVALID_LIST)?; + let Some(bits) = list.elements.get(position).copied().flatten() else { + return s.list_index_failure("list.__remove_at", index, invalid); + }; + if list.owned { s.drop_value(bits)?; } + let list = s.lists.get_mut(&value).ok_or(INVALID_LIST)?; + list.elements.remove(position); + list.element_pending_depths.remove(&position); + let mut moved = list.element_pending_depths.keys().copied().filter(|key| *key > position).collect::>(); + moved.sort_unstable(); + for key in moved { if let Some(depth) = list.element_pending_depths.remove(&key) { + list.element_pending_depths.insert(key - 1, depth); } } + Ok(value) }; + ListSort, jett_rt_v1_list_sort, false, (value: u64 => I64, kind: u32 => I32), u64 => I64, + |s| s.sort_list(value, kind); + ListSortByIndex, jett_rt_v1_list_sort_by_index, false, (value: u64 => I64, index: i64 => I64, kind: u32 => I32), u64 => I64, + |s| s.sort_list_by_index(value, index, kind); + ListIsSorted, jett_rt_v1_list_is_sorted, false, (value: u64 => I64, kind: u32 => I32), u32 => I32, + |s| s.list_is_sorted(value, kind).map(u32::from); + ListSwap, jett_rt_v1_list_swap, false, (value: u64 => I64, first: i64 => I64, second: i64 => I64), u64 => I64, + |s| { let list = s.lists.get_mut(&value).ok_or(INVALID_LIST)?; + let invalid = (JettRuntimeStatusV1::INVALID_ARGUMENT, b"list.__swap: index out of bounds".as_slice()); + let first = usize::try_from(first).map_err(|_| invalid)?; + let second = usize::try_from(second).map_err(|_| invalid)?; + if first >= list.elements.len() || second >= list.elements.len() { return Err(invalid); } + list.elements.swap(first, second); + let first_depth = list.element_pending_depths.remove(&first); + let second_depth = list.element_pending_depths.remove(&second); + if let Some(depth) = first_depth { list.element_pending_depths.insert(second, depth); } + if let Some(depth) = second_depth { list.element_pending_depths.insert(first, depth); } + Ok(value) }; + SetNew, jett_rt_v1_set_new, false, (strings: u32 => I32), u64 => I64, + |s| s.new_set(strings); + SetAdd, jett_rt_v1_set_add, false, (value: u64 => I64, key: u64 => I64), u64 => I64, + |s| s.set_add(value, key); + SetAddScalarTask, jett_rt_v1_set_add_scalar_task, false, (value: u64 => I64, key: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.set_add_depth(value, key, depth); + SetRemove, jett_rt_v1_set_remove, false, (value: u64 => I64, key: u64 => I64), u64 => I64, + |s| s.set_remove(value, key); + SetRemoveScalarTask, jett_rt_v1_set_remove_scalar_task, false, (value: u64 => I64, key: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.set_remove_depth(value, key, depth); + SetContains, jett_rt_v1_set_contains, false, (value: u64 => I64, key: u64 => I64), u32 => I32, + |s| { s.require_plain_set(value, PENDING_SET_CONTAINS)?; + Ok(u32::from(s.set_position(value, key)?.is_some())) }; + SetContainsScalarTask, jett_rt_v1_set_contains_scalar_task, false, (value: u64 => I64, key: u64 => I64, depth: u64 => I64), u32 => I32, + |s| { s.require_plain_set(value, PENDING_SET_CONTAINS)?; + Ok(u32::from(s.set_position_depth(value, key, depth)?.is_some())) }; + SetLength, jett_rt_v1_set_length, false, (value: u64 => I64), i64 => I64, + |s| { s.require_plain_set(value, PENDING_SET_LENGTH)?; + Ok(s.sets.get(&value).ok_or(INVALID_SET)?.elements.len() as i64) }; + SetClone, jett_rt_v1_set_clone, false, (value: u64 => I64), u64 => I64, + |s| s.clone_set(value); + SetRun, jett_rt_v1_set_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_set(value); + SetTaskJoin, jett_rt_v1_set_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_set(value); + SetElementTake, jett_rt_v1_set_element_take, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let set = s.sets.get_mut(&value).ok_or(INVALID_SET)?; + let index = usize::try_from(index).map_err(|_| INVALID_SET)?; + let bits = set.elements.get_mut(index).and_then(Option::take).ok_or(INVALID_SET)?; + set.element_pending_depths.remove(&index); + Ok(bits) }; + SetElementPendingDepth, jett_rt_v1_set_element_pending_depth, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let set = s.sets.get(&value).ok_or(INVALID_SET)?; + let index = usize::try_from(index).map_err(|_| INVALID_SET)?; + set.elements.get(index).copied().flatten().ok_or(INVALID_SET)?; + Ok(set.element_pending_depths.get(&index).copied().unwrap_or(0)) }; + SetElementClone, jett_rt_v1_set_element_clone, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let set = s.sets.get(&value).ok_or(INVALID_SET)?; + let element = set.elements.get(usize::try_from(index).map_err(|_| INVALID_SET)?) + .copied().flatten().ok_or(INVALID_SET)?; + if set.strings { s.retain(element) } else { Ok(element) } }; + MapNew, jett_rt_v1_map_new, false, (key_strings: u32 => I32, value_owned: u32 => I32), u64 => I64, + |s| s.new_map(key_strings, value_owned); + MapInsert, jett_rt_v1_map_insert, false, (map: u64 => I64, key: u64 => I64, value: u64 => I64), u64 => I64, + |s| s.map_insert(map, key, value); + MapInsertScalarTasks, jett_rt_v1_map_insert_scalar_tasks, false, (map: u64 => I64, key: u64 => I64, key_depth: u64 => I64, value: u64 => I64, value_depth: u64 => I64), u64 => I64, + |s| s.map_insert_depth(map, key, key_depth, value, value_depth); + MapAppendLiteral, jett_rt_v1_map_append_literal, false, (map: u64 => I64, key: u64 => I64, value: u64 => I64), u64 => I64, + |s| s.map_append_literal(map, key, value); + MapAppendLiteralScalarTasks, jett_rt_v1_map_append_literal_scalar_tasks, false, (map: u64 => I64, key: u64 => I64, key_depth: u64 => I64, value: u64 => I64, value_depth: u64 => I64), u64 => I64, + |s| s.map_append_literal_depth(map, key, key_depth, value, value_depth); + MapRemove, jett_rt_v1_map_remove, false, (map: u64 => I64, key: u64 => I64), u64 => I64, + |s| s.map_remove(map, key); + MapRemoveScalarTask, jett_rt_v1_map_remove_scalar_task, false, (map: u64 => I64, key: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.map_remove_depth(map, key, depth); + MapHas, jett_rt_v1_map_has, false, (map: u64 => I64, key: u64 => I64), u32 => I32, + |s| { s.require_plain_map(map, PENDING_MAP_HAS)?; + Ok(u32::from(s.map_position(map, key)?.is_some())) }; + MapHasScalarTask, jett_rt_v1_map_has_scalar_task, false, (map: u64 => I64, key: u64 => I64, depth: u64 => I64), u32 => I32, + |s| { s.require_plain_map(map, PENDING_MAP_HAS)?; + Ok(u32::from(s.map_position_depth(map, key, depth)?.is_some())) }; + MapLength, jett_rt_v1_map_length, false, (map: u64 => I64), i64 => I64, + |s| { s.require_plain_map(map, PENDING_MAP_LENGTH)?; + Ok(s.maps.get(&map).ok_or(INVALID_MAP)?.entries.len() as i64) }; + MapGet, jett_rt_v1_map_get, false, (map: u64 => I64, key: u64 => I64), u64 => I64, + |s| s.map_get(map, key); + MapGetScalarTask, jett_rt_v1_map_get_scalar_task, false, (map: u64 => I64, key: u64 => I64, depth: u64 => I64), u64 => I64, + |s| s.map_get_depth(map, key, depth); + MapClone, jett_rt_v1_map_clone, false, (map: u64 => I64), u64 => I64, + |s| s.clone_map(map); + MapRun, jett_rt_v1_map_run, false, (map: u64 => I64), u64 => I64, + |s| s.run_map(map); + MapTaskJoin, jett_rt_v1_map_task_join, false, (map: u64 => I64), u64 => I64, + |s| s.join_map(map); + MapFromLists, jett_rt_v1_map_from_lists, false, (keys: u64 => I64, values: u64 => I64, key_strings: u32 => I32, value_owned: u32 => I32), u64 => I64, + |s| s.map_from_lists(keys, values, key_strings, value_owned); + MapKeyTake, jett_rt_v1_map_key_take, false, (map: u64 => I64, index: i64 => I64), u64 => I64, + |s| s.map_element(map, index, true, true); + MapKeyClone, jett_rt_v1_map_key_clone, false, (map: u64 => I64, index: i64 => I64), u64 => I64, + |s| s.map_element(map, index, true, false); + MapValueTake, jett_rt_v1_map_value_take, false, (map: u64 => I64, index: i64 => I64), u64 => I64, + |s| s.map_element(map, index, false, true); + MapValueClone, jett_rt_v1_map_value_clone, false, (map: u64 => I64, index: i64 => I64), u64 => I64, + |s| s.map_element(map, index, false, false); + MapElementPendingDepth, jett_rt_v1_map_element_pending_depth, false, (map: u64 => I64, index: i64 => I64, key: u32 => I32), u64 => I64, + |s| s.map_element_depth(map, index, key); + ListLength, jett_rt_v1_list_length, false, (value: u64 => I64), i64 => I64, + |s| Ok(s.lists.get(&value).ok_or(INVALID_LIST)?.elements.len() as i64); + ListAppend, jett_rt_v1_list_append, false, (value: u64 => I64, bits: u64 => I64), u64 => I64, + |s| { let list = s.lists.get_mut(&value).ok_or(INVALID_LIST)?; + list.elements.try_reserve(1).map_err(|_| EXHAUSTED)?; + list.elements.push(Some(bits)); Ok(value) }; + ListAppendScalarTask, jett_rt_v1_list_append_scalar_task, false, (value: u64 => I64, bits: u64 => I64, depth: u64 => I64), u64 => I64, + |s| { let list = s.lists.get_mut(&value).ok_or(INVALID_LIST)?; + if list.owned { return Err(INVALID_LIST); } + list.elements.try_reserve(1).map_err(|_| EXHAUSTED)?; + if depth != 0 { list.element_pending_depths.try_reserve(1).map_err(|_| EXHAUSTED)?; + list.element_pending_depths.insert(list.elements.len(), depth); } + list.elements.push(Some(bits)); Ok(value) }; + ListGet, jett_rt_v1_list_get_clone, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let list = s.lists.get(&value).ok_or(INVALID_LIST)?; + let owned = list.owned; + let index = usize::try_from(index).ok(); + let found = index.and_then(|i| list.elements.get(i)).copied().flatten(); + let depth = index.and_then(|i| list.element_pending_depths.get(&i)).copied().unwrap_or(0); + let Some(bits) = found else { return s.sum(SUM_FAILURE, 0, false); }; + let bits = if owned { s.clone_value(bits)? } else { bits }; + match s.sum(SUM_SUCCESS, bits, owned) { Ok(v) => { + s.sums.get_mut(&v).ok_or(INVALID_SUM)?.payload_pending_depth = depth; + Ok(v) }, Err(e) => { if owned { s.drop_value(bits)?; } Err(e) } } }; + ListClone, jett_rt_v1_list_clone, false, (value: u64 => I64), u64 => I64, + |s| s.clone_list(value); + ListRun, jett_rt_v1_list_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_list(value); + ListTaskJoin, jett_rt_v1_list_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_list(value); + + ParseInt, jett_rt_v1_parse_int, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?; let parsed = text.parse::().map(|v| v as u64).map_err(|_| format!("int64.from_string: cannot parse '{text}' as int64")); s.parsed_sum(parsed) }; + ParseUint, jett_rt_v1_parse_uint, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?; let parsed = text.parse::().map_err(|_| format!("uint64.from_string: cannot parse '{text}' as uint64")); s.parsed_sum(parsed) }; + ParseFloat, jett_rt_v1_parse_float, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?; let parsed = text.parse::().map(f64::to_bits).map_err(|_| format!("float64.from_string: cannot parse '{text}' as float64")); s.parsed_sum(parsed) }; + FloatFromInt, jett_rt_v1_float64_from_int64, false, (value: i64 => I64), u64 => I64, + |s| { let converted = value as f64; + let parsed = if converted as i128 == i128::from(value) { Ok(converted.to_bits()) } + else { Err("float64.from_int64: value is not exactly representable as float64".into()) }; + s.parsed_sum(parsed) }; + IntFromFloat, jett_rt_v1_int64_from_float64, false, (value: f64 => F64), u64 => I64, + |s| { let parsed = if value.is_finite() && value.fract() == 0.0 + && value >= i64::MIN as f64 && value < 9_223_372_036_854_775_808.0 { + Ok((value as i64) as u64) + } else { Err("int64.from_float64: value is not exactly representable as int64".into()) }; + s.parsed_sum(parsed) }; + + SumNew, jett_rt_v1_sum_new, false, (tag: u32 => I32, bits: u64 => I64, owned: u32 => I32), u64 => I64, + |s| { if owned > 1 { return Err(INVALID_SUM); } s.sum(tag, bits, owned != 0) }; + SumNewScalarTask, jett_rt_v1_sum_new_scalar_task, false, (tag: u32 => I32, bits: u64 => I64, depth: u64 => I64), u64 => I64, + |s| { let id = s.sum(tag, bits, false)?; + s.sums.get_mut(&id).ok_or(INVALID_SUM)?.payload_pending_depth = depth; + Ok(id) }; + SumTag, jett_rt_v1_sum_tag, false, (value: u64 => I64), u32 => I32, + |s| s.sums.get(&value).map(|v| v.tag).ok_or(INVALID_SUM); + SumHandleTag, jett_rt_v1_sum_handle_tag, false, (value: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u32 => I32, + |s| { if layout_pointer == 0 { return Err(INVALID_SUM); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_SUM)?; + if length > isize::MAX as usize { return Err(INVALID_SUM); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.sum_handle_tag(value, layout) }; + SumPayloadPendingDepth, jett_rt_v1_sum_payload_pending_depth, false, (value: u64 => I64), u64 => I64, + |s| s.sums.get(&value).map(|v| v.payload_pending_depth).ok_or(INVALID_SUM); + // The selected payload remains borrowed from the unchanged owning sum. + SumPayloadBorrow, jett_rt_v1_sum_payload_borrow, false, (value: u64 => I64, tag: u32 => I32), u64 => I64, + |s| s.sum_payload_borrow(value, tag); + SumTake, jett_rt_v1_sum_take, false, (value: u64 => I64, tag: u32 => I32), u64 => I64, + |s| { if s.sums.get(&value).is_none_or(|v| v.tag != tag) { return Err(INVALID_SUM); } + let sum = s.sums.remove(&value).ok_or(INVALID_SUM)?; + s.sums_destroyed += 1; Ok(sum.bits) }; + SumClone, jett_rt_v1_sum_clone, false, (value: u64 => I64), u64 => I64, + |s| { if !s.sums.contains_key(&value) { return Err(INVALID_SUM); } s.clone_value(value) }; + SumRun, jett_rt_v1_sum_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_sum(value); + SumTaskJoin, jett_rt_v1_sum_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_sum(value); + BytesGet, jett_rt_v1_bytes_get, false, (value: u64 => I64, index: i64 => I64), u64 => I64, + |s| { let found = usize::try_from(index).ok().and_then(|i| s.bytes(value).ok()?.get(i)).copied(); + s.bytes(value)?; + s.sum(u32::from(found.is_some()), found.unwrap_or(0) as u64, false) }; + BytesToString, jett_rt_v1_bytes_to_string, false, (value: u64 => I64), u64 => I64, + |s| { let decoded = String::from_utf8(s.bytes(value)?.to_vec()); + let (tag, text) = match decoded { Ok(text) => (SUM_SUCCESS, text), Err(e) => (SUM_FAILURE, format!("invalid UTF-8: {e}")) }; + let payload = s.insert(text)?; + match s.sum(tag, payload, true) { Ok(v) => Ok(v), Err(e) => { s.drop_value(payload)?; Err(e) } } }; + BytesFromHex, jett_rt_v1_bytes_from_hex, false, (value: u64 => I64), u64 => I64, + |s| { let raw = s.text(value)?; let raw = raw.strip_prefix("0x").unwrap_or(raw); + let decoded = if raw.len() % 2 != 0 { Err("bytes.from_hex: expected even-length hex string") } + else if !raw.bytes().all(|b| b.is_ascii_hexdigit()) { Err("bytes.from_hex: expected hex string") } + else { (0..raw.len()).step_by(2).map(|i| u8::from_str_radix(&raw[i..i+2], 16).map_err(|_| "bytes.from_hex: expected hex string")).collect::, _>>() }; + let (tag, payload) = match decoded { Ok(data) => (SUM_SUCCESS, s.insert_bytes(data)?), Err(error) => (SUM_FAILURE, s.insert(error.to_owned())?) }; + match s.sum(tag, payload, true) { Ok(v) => Ok(v), Err(e) => { s.drop_value(payload)?; Err(e) } } }; + + EncodingBase64Encode, jett_rt_v1_encoding_base64_encode, false, (value: u64 => I64), u64 => I64, + |s| { let encoded = encoding::base64_encode(s.bytes(value)?); s.insert(encoded) }; + EncodingBase64Decode, jett_rt_v1_encoding_base64_decode, false, (value: u64 => I64), u64 => I64, + |s| { let decoded = encoding::base64_decode(s.text(value)?).map_err(str::to_owned); s.byte_result(decoded) }; + EncodingHexDecode, jett_rt_v1_encoding_hex_decode, false, (value: u64 => I64), u64 => I64, + |s| { let decoded = encoding::encoding_hex_decode(s.text(value)?).map_err(str::to_owned); s.byte_result(decoded) }; + EncodingUrlEncode, jett_rt_v1_encoding_url_encode, false, (value: u64 => I64), u64 => I64, + |s| { let encoded = encoding::percent_encode(s.text(value)?, false); s.insert(encoded) }; + EncodingUrlDecode, jett_rt_v1_encoding_url_decode, false, (value: u64 => I64), u64 => I64, + |s| { let decoded = encoding::percent_decode(s.text(value)?, false).map_err(str::to_owned); s.string_result(decoded) }; + EncodingFormEncode, jett_rt_v1_encoding_form_encode, false, (value: u64 => I64), u64 => I64, + |s| { let encoded = encoding::percent_encode(s.text(value)?, true); s.insert(encoded) }; + EncodingFormDecode, jett_rt_v1_encoding_form_decode, false, (value: u64 => I64), u64 => I64, + |s| { let decoded = encoding::percent_decode(s.text(value)?, true).map_err(str::to_owned); s.string_result(decoded) }; + + CsvParse, jett_rt_v1_csv_parse, false, (value: u64 => I64), u64 => I64, + |s| { match csv::parse_csv_records(s.text(value)?) { + Ok(rows) => { let payload = s.csv_rows(rows)?; s.owned_sum(SUM_SUCCESS, payload) }, + Err(error) => { let payload = s.insert(error)?; s.owned_sum(SUM_FAILURE, payload) } + } }; + CsvParseWithHeader, jett_rt_v1_csv_parse_with_header, false, (value: u64 => I64), u64 => I64, + |s| { match csv::parse_csv_with_header(s.text(value)?) { + Ok(rows) => { let payload = s.csv_header_rows(rows)?; s.owned_sum(SUM_SUCCESS, payload) }, + Err(error) => { let payload = s.insert(error)?; s.owned_sum(SUM_FAILURE, payload) } + } }; + CsvStringify, jett_rt_v1_csv_stringify, false, (value: u64 => I64), u64 => I64, + |s| { let rows = s.csv_records(value)?; s.insert(csv::stringify_csv_records(&rows)) }; + + SecretCompareString, jett_rt_v1_secret_compare_string, false, (left: u64 => I64, right: u64 => I64), u32 => I32, + |s| { let left = s.text(left)?.as_bytes(); let right = s.text(right)?.as_bytes(); + Ok(u32::from(left.len() == right.len() && bool::from(left.ct_eq(right)))) }; + SecretCompareBytes, jett_rt_v1_secret_compare_bytes, false, (left: u64 => I64, right: u64 => I64), u32 => I32, + |s| { let left = s.bytes(left)?; let right = s.bytes(right)?; + Ok(u32::from(left.len() == right.len() && bool::from(left.ct_eq(right)))) }; + + CryptoSha256, jett_rt_v1_crypto_sha256, false, (value: u64 => I64), u64 => I64, + |s| { let digest = crypto::sha256_digest(s.bytes(value)?); s.insert_bytes(digest) }; + CryptoSha512, jett_rt_v1_crypto_sha512, false, (value: u64 => I64), u64 => I64, + |s| { let digest = crypto::sha512_digest(s.bytes(value)?); s.insert_bytes(digest) }; + CryptoMd5, jett_rt_v1_crypto_md5, false, (value: u64 => I64), u64 => I64, + |s| { let digest = crypto::md5_digest(s.bytes(value)?); s.insert_bytes(digest) }; + CryptoHmacSha256, jett_rt_v1_crypto_hmac_sha256, false, (key: u64 => I64, message: u64 => I64), u64 => I64, + |s| { let digest = crypto::hmac_sha256_digest(s.bytes(key)?, s.bytes(message)?); s.insert_bytes(digest) }; + + DropValue, jett_rt_v1_value_drop, true, (value: u64 => I64), u32 => I32, + |s| s.drop_value(value); + BytesNew, jett_rt_v1_bytes_new, false, (), u64 => I64, + |s| s.insert_bytes(Vec::new()); + BitfieldWriteBits, jett_rt_v1_bitfield_write_bits, false, (value: u64 => I64, numeric: u64 => I64, width: u32 => I32, network_order: u32 => I32, bit_offset: u64 => I64), u32 => I32, + |s| s.write_bitfield_bits(value, numeric, width, network_order, bit_offset); + BitfieldExtendPayload, jett_rt_v1_bitfield_extend_payload, false, (value: u64 => I64, payload: u64 => I64), u32 => I32, + |s| s.extend_bitfield_payload(value, payload); + BitfieldCheckPayloadPending, jett_rt_v1_bitfield_check_payload_pending, false, (payload: u64 => I64, prefix_pointer: u64 => I64, prefix_length: u64 => I64), u32 => I32, + |s| { if prefix_pointer == 0 { return Err(INVALID_PENDING_HANDLE_CHECK); } + let length = usize::try_from(prefix_length).map_err(|_| INVALID_PENDING_HANDLE_CHECK)?; + if length > isize::MAX as usize { return Err(INVALID_PENDING_HANDLE_CHECK); } + let prefix = unsafe { std::slice::from_raw_parts(prefix_pointer as *const u8, length) }; + s.check_bitfield_payload_pending(payload, prefix) }; + BitfieldDecode, jett_rt_v1_bitfield_decode, false, (value: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u64 => I64, + |s| { if layout_pointer == 0 { return Err(INVALID_BITFIELD_LAYOUT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_BITFIELD_LAYOUT)?; + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.decode_bitfield(value, layout) }; + BytesClone, jett_rt_v1_bytes_clone, false, (value: u64 => I64), u64 => I64, + |s| s.clone_bytes(value); + BytesRun, jett_rt_v1_bytes_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_bytes(value); + BytesTaskJoin, jett_rt_v1_bytes_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_bytes(value); + BytesLength, jett_rt_v1_bytes_length, false, (value: u64 => I64), i64 => I64, + |s| Ok(s.bytes(value)?.len() as i64); + BytesFromString, jett_rt_v1_bytes_from_string, false, (value: u64 => I64), u64 => I64, + |s| { let data = s.text(value)?.as_bytes().to_vec(); s.insert_bytes(data) }; + BytesSlice, jett_rt_v1_bytes_slice, false, (value: u64 => I64, start: i64 => I64, end: i64 => I64), u64 => I64, + |s| { let data = s.bytes(value)?; let len = data.len() as i64; + let start = start.clamp(0, len) as usize; let end = end.clamp(0, len) as usize; + let result = data[start.min(end)..end].to_vec(); s.insert_bytes(result) }; + BytesConcat, jett_rt_v1_bytes_concat, false, (first: u64 => I64, second: u64 => I64), u64 => I64, + |s| { let a = s.bytes(first)?; let b = s.bytes(second)?; + let len = a.len().checked_add(b.len()).ok_or(EXHAUSTED)?; + let mut result = Vec::new(); result.try_reserve_exact(len).map_err(|_| EXHAUSTED)?; + result.extend_from_slice(a); result.extend_from_slice(b); s.insert_bytes(result) }; + BytesToHex, jett_rt_v1_bytes_to_hex, false, (value: u64 => I64), u64 => I64, + |s| { use std::fmt::Write; let bytes = s.bytes(value)?; + let mut text = String::new(); text.try_reserve_exact(bytes.len().checked_mul(2).ok_or(EXHAUSTED)?).map_err(|_| EXHAUSTED)?; + for byte in bytes { write!(text, "{byte:02x}").map_err(|_| EXHAUSTED)?; } + s.insert(text) }; + + Sqrt, jett_rt_v1_math_sqrt, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.sqrt()); + Floor, jett_rt_v1_math_floor, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.floor()); + Ceil, jett_rt_v1_math_ceil, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.ceil()); + Round, jett_rt_v1_math_round, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.round()); + Log, jett_rt_v1_math_log, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.ln()); + Log2, jett_rt_v1_math_log2, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.log2()); + Log10, jett_rt_v1_math_log10, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.log10()); + Sin, jett_rt_v1_math_sin, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.sin()); + Cos, jett_rt_v1_math_cos, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.cos()); + Tan, jett_rt_v1_math_tan, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.tan()); + FloatAbs, jett_rt_v1_math_floatabs, false, (value: f64 => F64), f64 => F64, + |_| Ok(value.abs()); + FloatMin, jett_rt_v1_math_floatmin, false, (first: f64 => F64, second: f64 => F64), f64 => F64, + |_| Ok(first.min(second)); + FloatMax, jett_rt_v1_math_floatmax, false, (first: f64 => F64, second: f64 => F64), f64 => F64, + |_| Ok(first.max(second)); + FloatRemainder, jett_rt_v1_float_remainder, false, (first: f64 => F64, second: f64 => F64), f64 => F64, + |_| Ok(first % second); + Pow, jett_rt_v1_math_pow, false, (first: f64 => F64, second: f64 => F64), f64 => F64, + |_| Ok(first.powf(second)); + Pi, jett_rt_v1_math_pi, false, (), f64 => F64, + |_| Ok(std::f64::consts::PI); + E, jett_rt_v1_math_e, false, (), f64 => F64, + |_| Ok(std::f64::consts::E); + IntAbs, jett_rt_v1_math_int_abs, false, (value: i64 => I64), i64 => I64, + |_| Ok(value.wrapping_abs()); + IntMin, jett_rt_v1_math_int_min, false, (first: i64 => I64, second: i64 => I64), i64 => I64, + |_| Ok(first.min(second)); + IntMax, jett_rt_v1_math_int_max, false, (first: i64 => I64, second: i64 => I64), i64 => I64, + |_| Ok(first.max(second)); + Mod, jett_rt_v1_math_mod, false, (first: i64 => I64, second: i64 => I64), i64 => I64, + |_| if second == 0 { Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.mod: division by zero")) } else { Ok(first.wrapping_rem(second)) }; + Gcd, jett_rt_v1_math_gcd, false, (first: i64 => I64, second: i64 => I64), i64 => I64, + |_| Ok(unsigned_gcd(first.unsigned_abs(), second.unsigned_abs()) as i64); + Lcm, jett_rt_v1_math_lcm, false, (first: i64 => I64, second: i64 => I64), i64 => I64, + |_| { + let a = first.unsigned_abs(); let b = second.unsigned_abs(); + if a == 0 || b == 0 { Ok(0) } else { Ok((a / unsigned_gcd(a,b)).wrapping_mul(b) as i64) } + }; + Factorial, jett_rt_v1_math_factorial, false, (value: i64 => I64), i64 => I64, + |_| { + if value < 0 { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.factorial: argument must be non-negative")); } + let mut result = 1_i64; + for n in 2..=value { result = result.wrapping_mul(n); if result == 0 { break; } } + Ok(result) + }; + Clamp, jett_rt_v1_math_clamp, false, (value: f64 => F64, lower: f64 => F64, upper: f64 => F64), f64 => F64, + |_| { + if lower.is_nan() || upper.is_nan() { Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.clamp bounds must not be NaN")) } + else if lower > upper { Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"math.clamp requires lower bound <= upper bound")) } + else { Ok(value.clamp(lower, upper)) } + }; + Status, jett_rt_v1_value_status, true, (), u32 => I32, + |s| Ok(s.current_failure().map_or(0, |e| e.0.code())); + FailureTakePrefixedText, jett_rt_v1_failure_take_prefixed_text, true, (prefix: *const u8 => Pointer, length: u64 => I64), u64 => I64, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_FAILURE_COPY)?; + if length > isize::MAX as usize || (length != 0 && prefix.is_null()) { return Err(INVALID_FAILURE_COPY); } + if s.cleanup_failed || !s.can_capture_refinement_failure() { return Err(INVALID_FAILURE_COPY); } + let (_, static_message) = s.current_failure().ok_or(INVALID_FAILURE_COPY)?; + let prefix = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(prefix, length) } }; + let message = s.current_dynamic_failure_message().unwrap_or(static_message); + let prefix = std::str::from_utf8(prefix).map_err(|_| INVALID_FAILURE_COPY)?; + let message = std::str::from_utf8(message).map_err(|_| INVALID_FAILURE_COPY)?; + let mut text = String::new(); + text.try_reserve(prefix.len().checked_add(message.len()).ok_or(EXHAUSTED)?).map_err(|_| EXHAUSTED)?; + text.push_str(prefix); + text.push_str(message); + let value = s.insert(text)?; + s.finish_refinement_failure_capture(); + Ok(value) }; + RefinementPendingBoolText, jett_rt_v1_refinement_pending_bool_text, true, (bits: u64 => I64, depth: u64 => I64, name: *const u8 => Pointer, length: u64 => I64), u64 => I64, + |s| { let length = usize::try_from(length).map_err(|_| INVALID_FAILURE_COPY)?; + if length > isize::MAX as usize || (length != 0 && name.is_null()) { return Err(INVALID_FAILURE_COPY); } + let name = if length == 0 { &[][..] } else { unsafe { std::slice::from_raw_parts(name, length) } }; + let name = std::str::from_utf8(name).map_err(|_| INVALID_FAILURE_COPY)?; + let value = match bits { 0 => "false", 1 => "true", _ => return Err(INVALID_TRACE_LABEL) }; + let value = format_pending_value(value, depth)?; + s.insert(format!("refinement constraint for '{name}' must return bool, got {value}")) }; + PropertyCaseSet, jett_rt_v1_property_case_set, false, (message: u64 => I64), u32 => I32, + |s| { s.set_property_case_context(Some(s.text(message)?.as_bytes().to_vec())); Ok(0) }; + PropertyCaseClear, jett_rt_v1_property_case_clear, false, (), u32 => I32, + |s| { s.set_property_case_context(None); Ok(0) }; + AssertFail, jett_rt_v1_assert_fail, false, (), u32 => I32, + |_s| Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"assertion failed")); + AssertFailMessage, jett_rt_v1_assert_fail_message, false, (message: u64 => I64), u32 => I32, + |s| { let text = s.text(message)?.as_bytes(); + let mut owned = Vec::new(); + owned.try_reserve_exact(text.len()).map_err(|_| EXHAUSTED)?; + owned.extend_from_slice(text); + s.set_dynamic_failure_message(owned); + Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"assertion failed")) }; + Retain, jett_rt_v1_string_retain, false, (value: u64 => I64), u64 => I64, + |s| s.retain(value); + Release, jett_rt_v1_string_release, true, (value: u64 => I64), u32 => I32, + |s| s.release(value); + Literal, jett_rt_v1_string_literal, false, (data: *const u8 => Pointer, length: u64 => I64), u64 => I64, + |s| { + if length > isize::MAX as u64 { return Err((JettRuntimeStatusV1::LENGTH_OUT_OF_RANGE, STRING_LENGTH_MESSAGE)); } + if length != 0 && data.is_null() { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, STRING_NULL_MESSAGE)); } + let bytes = if length == 0 { &[] } else { unsafe { slice::from_raw_parts(data, length as usize) } }; + let text = str::from_utf8(bytes).map_err(|_| (JettRuntimeStatusV1::INVALID_UTF8, STRING_UTF8_MESSAGE))?; + s.insert(text.to_owned()) + }; + Concat, jett_rt_v1_string_concat, false, (left: u64 => I64, right: u64 => I64), u64 => I64, + |s| { + let a = s.text(left)?; let b = s.text(right)?; + let length = a.len().checked_add(b.len()).ok_or(EXHAUSTED)?; + let mut text = String::new(); text.try_reserve_exact(length).map_err(|_| EXHAUSTED)?; + text.push_str(a); text.push_str(b); s.insert(text) + }; + CharCount, jett_rt_v1_string_char_count, false, (value: u64 => I64), u64 => I64, + |s| Ok(s.text(value)?.graphemes(true).count() as u64); + StringIndexOf, jett_rt_v1_string_index_of, false, (value: u64 => I64, needle: u64 => I64), u64 => I64, + |s| { + let mut index = if s.text(needle)?.is_empty() { Some(0) } else { None }; + native_scan_grapheme_matches(s.text(value)?, s.text(needle)?, |_, _, offset| { + index = Some(offset); + false + }); + match index { + Some(offset) => s.sum(SUM_SUCCESS, offset as u64, false), + None => s.sum(SUM_FAILURE, 0, false), + } + }; + StringCount, jett_rt_v1_string_count, false, (value: u64 => I64, needle: u64 => I64), u64 => I64, + |s| { + let mut count = 0_u64; + native_scan_grapheme_matches(s.text(value)?, s.text(needle)?, |_, _, _| { + count += 1; + true + }); + Ok(count) + }; + StringReplace, jett_rt_v1_string_replace, false, (value: u64 => I64, needle: u64 => I64, replacement: u64 => I64), u64 => I64, + |s| { let output = native_replace(s.text(value)?, s.text(needle)?, s.text(replacement)?)?; s.insert(output) }; + StringSlugify, jett_rt_v1_string_slugify, false, (value: u64 => I64), u64 => I64, + |s| { let output = native_slugify(s.text(value)?); s.insert(output) }; + StringToUpperFirst, jett_rt_v1_string_to_upper_first, false, (value: u64 => I64), u64 => I64, + |s| { let output = native_change_first(s.text(value)?, |first| first.to_uppercase().collect()); s.insert(output) }; + StringToLowerFirst, jett_rt_v1_string_to_lower_first, false, (value: u64 => I64), u64 => I64, + |s| { let output = native_change_first(s.text(value)?, |first| first.to_lowercase().collect()); s.insert(output) }; + Slice, jett_rt_v1_string_slice, false, (value: u64 => I64, start: i64 => I64, end: i64 => I64), u64 => I64, + |s| { + let parts = s.text(value)?.graphemes(true).collect::>(); + let length = parts.len() as i64; + let start = start.clamp(0, length) as usize; + let end = end.clamp(0, length) as usize; + let text = parts[start.min(end)..end].concat(); + s.insert(text) + }; + Upper, jett_rt_v1_string_upper, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?.to_uppercase(); s.insert(text) }; + Lower, jett_rt_v1_string_lower, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?.to_lowercase(); s.insert(text) }; + Trim, jett_rt_v1_string_trim, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?.trim().to_owned(); s.insert(text) }; + TrimStart, jett_rt_v1_string_trim_start, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?.trim_start().to_owned(); s.insert(text) }; + TrimEnd, jett_rt_v1_string_trim_end, false, (value: u64 => I64), u64 => I64, + |s| { let text = s.text(value)?.trim_end().to_owned(); s.insert(text) }; + IsAlpha, jett_rt_v1_string_is_alpha, false, (value: u64 => I64), u32 => I32, + |s| { let text = s.text(value)?; Ok(u32::from(!text.is_empty() && text.chars().all(char::is_alphabetic))) }; + IsNumeric, jett_rt_v1_string_is_numeric, false, (value: u64 => I64), u32 => I32, + |s| { let text = s.text(value)?; Ok(u32::from(!text.is_empty() && text.chars().all(|c| c.is_ascii_digit()))) }; + Repeat, jett_rt_v1_string_repeat, false, (value: u64 => I64, count: i64 => I64), u64 => I64, + |s| { + let text = s.text(value)?; + let count = usize::try_from(count.max(0)).unwrap_or(usize::MAX); + if text.is_empty() || count == 0 { return s.insert(String::new()); } + let error = (JettRuntimeStatusV1::RESOURCE_EXHAUSTED, b"string.repeat: requested output is too large".as_slice()); + let length = text.len().checked_mul(count).ok_or(error)?; + let mut result = String::new(); result.try_reserve_exact(length).map_err(|_| error)?; + for _ in 0..count { result.push_str(text); } + s.insert(result) + }; + Equal, jett_rt_v1_string_equal, false, (left: u64 => I64, right: u64 => I64), u32 => I32, + |s| s.equal_string(left, right, false); + StringNotEqual, jett_rt_v1_string_not_equal, false, (left: u64 => I64, right: u64 => I64), u32 => I32, + |s| s.equal_string(left, right, true); + StringRun, jett_rt_v1_string_run, false, (value: u64 => I64), u64 => I64, + |s| s.run_string(value); + StringTaskJoin, jett_rt_v1_string_task_join, false, (value: u64 => I64), u64 => I64, + |s| s.join_string(value); + StringTaskFormat, jett_rt_v1_string_task_format, false, (value: u64 => I64), u64 => I64, + |s| s.display_string(value); + ScalarTaskFormat, jett_rt_v1_scalar_task_format, false, (bits: u64 => I64, kind: u32 => I32, depth: u64 => I64), u64 => I64, + |s| { let inner = s.debug_value(bits, kind)?; s.insert(format_pending_value(&inner, depth)?) }; + NothingRun, jett_rt_v1_nothing_run, false, (depth: u64 => I64), u64 => I64, + |_s| depth.checked_add(1).ok_or(EXHAUSTED); + NothingJoin, jett_rt_v1_nothing_join, false, (depth: u64 => I64), u64 => I64, + |s| s.join_nothing(depth); + NothingFormat, jett_rt_v1_nothing_format, false, (depth: u64 => I64), u64 => I64, + |s| s.insert(format_nothing(depth)?); + NothingEqual, jett_rt_v1_nothing_equal, false, (left: u64 => I64, right: u64 => I64, not_equal: u32 => I32), u32 => I32, + |s| s.equal_nothing(left, right, not_equal); + ScalarPendingUnaryCheck, jett_rt_v1_scalar_pending_unary_check, false, (depth: u64 => I64, negate: u32 => I32), u32 => I32, + |_s| { if negate > 1 { return Err(INVALID_TRACE_LABEL); } + if depth == 0 { Ok(0) } else if negate == 0 { Err(PENDING_BOOL_NEGATION) } + else { Err(PENDING_NUMERIC_NEGATION) } }; + ScalarPendingBinaryCheck, jett_rt_v1_scalar_pending_binary_check, false, (left_bits: u64 => I64, left_kind: u32 => I32, left_depth: u64 => I64, right_bits: u64 => I64, right_kind: u32 => I32, right_depth: u64 => I64, operation: u32 => I32), u32 => I32, + |s| s.check_pending_scalar_binary(left_bits, left_kind, left_depth, right_bits, right_kind, right_depth, operation); + ScalarPendingBoolConditionCheck, jett_rt_v1_scalar_pending_bool_condition_check, false, (bits: u64 => I64, depth: u64 => I64, context: u32 => I32), u32 => I32, + |s| s.check_pending_bool_condition(bits, depth, context); + EnumEqual, jett_rt_v1_enum_equal, false, (left: u64 => I64, right: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u32 => I32, + |s| { if layout_pointer == 0 { return Err(INVALID_STRUCT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize { return Err(INVALID_STRUCT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.enum_equal(left, right, layout) }; + EnumPendingComparisonCheck, jett_rt_v1_enum_pending_comparison_check, false, (left: u64 => I64, right: u64 => I64, not_equal: u32 => I32, layout_pointer: u64 => I64, layout_length: u64 => I64), u32 => I32, + |s| { if layout_pointer == 0 { return Err(INVALID_STRUCT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize { return Err(INVALID_STRUCT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.check_enum_comparison(left, right, not_equal, layout) }; + EnumEqualAggregate, jett_rt_v1_enum_equal_aggregate, false, (left: u64 => I64, right: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u32 => I32, + |s| { if layout_pointer == 0 { return Err(INVALID_STRUCT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize { return Err(INVALID_STRUCT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.aggregate_enum_equal(left, right, layout) }; + EnumEqualityStart, jett_rt_v1_enum_equality_start, false, (left: u64 => I64, right: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u64 => I64, + |s| { if layout_pointer == 0 { return Err(INVALID_STRUCT); } + let length = usize::try_from(layout_length).map_err(|_| INVALID_STRUCT)?; + if length > isize::MAX as usize { return Err(INVALID_STRUCT); } + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, length) }; + s.equality_start(left, right, layout) }; + EnumEqualityNext, jett_rt_v1_enum_equality_next, false, (cursor: u64 => I64), u32 => I32, + |s| s.equality_next(cursor); + EnumEqualityArgument, jett_rt_v1_enum_equality_argument, false, (cursor: u64 => I64, argument: u32 => I32), u64 => I64, + |s| s.equality_argument(cursor, argument); + EnumEqualityAnswer, jett_rt_v1_enum_equality_answer, false, (cursor: u64 => I64, equal: u32 => I32, depth: u64 => I64), u32 => I32, + |s| s.equality_answer(cursor, equal, depth); + FromInt, jett_rt_v1_string_from_int, false, (value: i64 => I64), u64 => I64, + |s| s.insert(value.to_string()); + FromUint, jett_rt_v1_string_from_uint, false, (value: u64 => I64), u64 => I64, + |s| s.insert(value.to_string()); + FromFloat, jett_rt_v1_string_from_float, false, (value: f64 => F64), u64 => I64, + |s| s.insert(value.to_string()); + FromBool, jett_rt_v1_string_from_bool, false, (value: u32 => I32), u64 => I64, + |s| if value > 1 { Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid native bool")) } else { s.insert((value != 0).to_string()) }; + GrantStdout, jett_rt_v1_grant_stdout, false, (), u64 => I64, + |s| { if let Some(token) = s.stdout { return Ok(token); } let token = next_identity()?; s.stdout = Some(token); Ok(token) }; + GrantStderr, jett_rt_v1_grant_stderr, false, (), u64 => I64, + |s| s.grant_opaque_capability("Stderr"); + GrantStdin, jett_rt_v1_grant_stdin, false, (), u64 => I64, + |s| s.grant_opaque_capability("Stdin"); + GrantFilesystem, jett_rt_v1_grant_filesystem, false, (), u64 => I64, + |s| s.grant_opaque_capability("Filesystem"); + GrantNetwork, jett_rt_v1_grant_network, false, (), u64 => I64, + |s| s.grant_opaque_capability("Network"); + GrantProcess, jett_rt_v1_grant_process, false, (), u64 => I64, + |s| s.grant_opaque_capability("Process"); + GrantLog, jett_rt_v1_grant_log, false, (), u64 => I64, + |s| s.grant_opaque_capability("Log"); + GrantClock, jett_rt_v1_grant_clock, false, (), u64 => I64, + |s| { if let Some(token) = s.clock { return Ok(token); } let token = next_identity()?; s.clock = Some(token); Ok(token) }; + ClockNow, jett_rt_v1_clock_now, false, (authority: u64 => I64), i64 => I64, + |s| { if s.clock != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Clock authority")); } + if let Some(script) = &mut s.clock_script { + let sample = script.front().copied().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, b"Clock.now: test clock exhausted".as_slice()))?; + match sample { + clock::ClockTestSample::Unavailable => { + script.pop_front(); + Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"Clock.now: wall clock unavailable")) + } + clock::ClockTestSample::Wall { unix_seconds, subsecond_nanoseconds } => { + let value = clock::checked_clock_milliseconds(unix_seconds, subsecond_nanoseconds) + .map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?; + script.pop_front(); + Ok(value) + } + } + } else { + let (seconds, nanoseconds) = clock::production_wall_clock_sample(); + clock::checked_clock_milliseconds(seconds, nanoseconds).map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes())) + } }; + GrantRandom, jett_rt_v1_grant_random, false, (), u64 => I64, + |s| { if let Some(token) = s.random { return Ok(token); } + if s.random_provider.is_none() { + s.random_provider = Some(RandomProvider::production().map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?); + } + let token = next_identity()?; s.random = Some(token); Ok(token) }; + RandomBounded, jett_rt_v1_random_bounded, false, (authority: u64 => I64, lower: i64 => I64, upper: i64 => I64), i64 => I64, + |s| { if s.random != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Random authority")); } + let width = u64::try_from(i128::from(upper) - i128::from(lower)) + .ok().filter(|width| *width > 0) + .ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, b"random.__bounded received invalid bounds".as_slice()))?; + let offset = s.random_provider.as_mut().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, random::ENTROPY_UNAVAILABLE.as_bytes()))? + .bounded_offset(width).map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?; + i64::try_from(i128::from(lower) + i128::from(offset)) + .map_err(|_| (JettRuntimeStatusV1::INVALID_ARGUMENT, b"random.__bounded received invalid bounds".as_slice())) }; + RandomUnit53, jett_rt_v1_random_unit53, false, (authority: u64 => I64), f64 => F64, + |s| { if s.random != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Random authority")); } + let bits = s.random_provider.as_mut().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, random::ENTROPY_UNAVAILABLE.as_bytes()))? + .unit53().map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?; + Ok(bits as f64 / 9_007_199_254_740_992.0) }; + RandomBool, jett_rt_v1_random_bool, false, (authority: u64 => I64), u32 => I32, + |s| { if s.random != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Random authority")); } + let value = s.random_provider.as_mut().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, random::ENTROPY_UNAVAILABLE.as_bytes()))? + .boolean().map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?; + Ok(u32::from(value)) }; + UuidNew, jett_rt_v1_uuid_new, false, (), u64 => I64, + |s| { let value = crate::uuid::new_v4() + .map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?; + s.insert(value) }; + GrantEnvironment, jett_rt_v1_grant_environment, false, (), u64 => I64, + |s| { if let Some(token) = s.environment { return Ok(token); } + if s.environment_snapshot.is_none() { + s.environment_snapshot = Some(LaunchEnvironmentSnapshot::production() + .map_err(|message| (JettRuntimeStatusV1::INVALID_ARGUMENT, message.as_bytes()))?); + } + let token = next_identity()?; s.environment = Some(token); Ok(token) }; + GrantGraphics, jett_rt_v1_grant_graphics, false, (), u64 => I64, + |s| { if let Some(token) = s.graphics { return Ok(token); } + let token = next_identity()?; s.graphics = Some(token); Ok(token) }; + GraphicsValidateConfig, jett_rt_v1_graphics_validate_config, false, (config: u64 => I64), u64 => I64, + |s| s.graphics_validate_config(config); + GraphicsValidateScene, jett_rt_v1_graphics_validate_scene, false, (width: i64 => I64, height: i64 => I64, scene: u64 => I64), u64 => I64, + |s| s.graphics_validate_scene(width, height, scene); + GraphicsOpen, jett_rt_v1_graphics_open, false, (authority: u64 => I64, config: u64 => I64), u64 => I64, + |s| s.graphics_open(authority, config); + GraphicsPresent, jett_rt_v1_graphics_present, false, (session: u64 => I64, scene: u64 => I64), u64 => I64, + |s| s.graphics_present(session, scene); + GraphicsNextKey, jett_rt_v1_graphics_next_key, false, (session: u64 => I64), u64 => I64, + |s| s.graphics_next_key(session); + GraphicsClose, jett_rt_v1_graphics_close, true, (session: u64 => I64), u32 => I32, + |s| s.graphics_close(session); + EnvironmentArgs, jett_rt_v1_environment_args, false, (authority: u64 => I64), u64 => I64, + |s| { if s.environment != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Environment authority")); } + let arguments = s.environment_snapshot.as_ref().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, b"Environment: launch data unavailable".as_slice()))? + .arguments().to_vec(); + s.string_list(arguments) }; + EnvironmentGet, jett_rt_v1_environment_get, false, (authority: u64 => I64, key: u64 => I64), u64 => I64, + |s| { if s.environment != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Environment authority")); } + let found = s.environment_snapshot.as_ref().ok_or((JettRuntimeStatusV1::INVALID_ARGUMENT, b"Environment: launch data unavailable".as_slice()))? + .get(s.text(key)?); + match found { + Ok(Some(value)) => { let text = s.insert(value)?; + let optional = s.owned_sum(SUM_SUCCESS, text)?; + s.owned_sum(SUM_SUCCESS, optional) }, + Ok(None) => { let optional = s.sum(SUM_FAILURE, 0, false)?; + s.owned_sum(SUM_SUCCESS, optional) }, + Err(message) => { let text = s.insert(message.to_owned())?; + s.owned_sum(SUM_FAILURE, text) }, + } }; + Stdout, jett_rt_v1_string_stdout, false, (authority: u64 => I64, value: u64 => I64), u32 => I32, + |s| { + let (authority, _) = s.capability_authority(authority) + .map_err(|_| (JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Stdout authority".as_slice()))?; + if s.stdout != Some(authority) { return Err((JettRuntimeStatusV1::INVALID_ARGUMENT, b"invalid Stdout authority")); } + let text = s.format_string(value)?; + { let mut stdout = io::stdout().lock(); write_all_bytes(&mut stdout, text.as_bytes()).and_then(|_| stdout.flush()).map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDOUT_WRITE_MESSAGE))?; } Ok(0) + }; + DebugPrint, jett_rt_v1_string_debug_print, false, (value: u64 => I64), u32 => I32, + |s| { { let mut stderr = io::stderr().lock(); write_all_bytes(&mut stderr, s.text(value)?.as_bytes()).and_then(|_| stderr.flush()).map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDERR_WRITE_MESSAGE))?; } Ok(0) }; + DebugAppend, jett_rt_v1_debug_append, false, (builder: u64 => I64, label_pointer: u64 => I64, label_length: u64 => I64, bits: u64 => I64, kind: u32 => I32), u32 => I32, + |s| { if label_pointer == 0 { return Err(INVALID_TRACE_LABEL); } + let length = usize::try_from(label_length).map_err(|_| INVALID_TRACE_LABEL)?; + if length > isize::MAX as usize { return Err(INVALID_TRACE_LABEL); } + let label = unsafe { std::slice::from_raw_parts(label_pointer as *const u8, length) }; + let label = std::str::from_utf8(label).map_err(|_| INVALID_TRACE_LABEL)?; + s.debug_append(builder, label, bits, kind) }; + DebugAppendPendingScalar, jett_rt_v1_debug_append_pending_scalar, false, (builder: u64 => I64, label_pointer: u64 => I64, label_length: u64 => I64, bits: u64 => I64, kind: u32 => I32, depth: u64 => I64), u32 => I32, + |s| { if label_pointer == 0 { return Err(INVALID_TRACE_LABEL); } + let length = usize::try_from(label_length).map_err(|_| INVALID_TRACE_LABEL)?; + if length > isize::MAX as usize { return Err(INVALID_TRACE_LABEL); } + let label = unsafe { std::slice::from_raw_parts(label_pointer as *const u8, length) }; + let label = std::str::from_utf8(label).map_err(|_| INVALID_TRACE_LABEL)?; + s.debug_append_pending_scalar(builder, label, bits, kind, depth) }; + DebugAppendAggregate, jett_rt_v1_debug_append_aggregate, false, (builder: u64 => I64, label_pointer: u64 => I64, label_length: u64 => I64, bits: u64 => I64, layout_pointer: u64 => I64, layout_length: u64 => I64), u32 => I32, + |s| { if label_pointer == 0 || layout_pointer == 0 { return Err(INVALID_TRACE_LABEL); } + let label_length = usize::try_from(label_length).map_err(|_| INVALID_TRACE_LABEL)?; + let layout_length = usize::try_from(layout_length).map_err(|_| INVALID_TRACE_LABEL)?; + if label_length > isize::MAX as usize || layout_length > isize::MAX as usize { return Err(INVALID_TRACE_LABEL); } + let label = unsafe { std::slice::from_raw_parts(label_pointer as *const u8, label_length) }; + let label = std::str::from_utf8(label).map_err(|_| INVALID_TRACE_LABEL)?; + let layout = unsafe { std::slice::from_raw_parts(layout_pointer as *const u8, layout_length) }; + s.debug_append_aggregate(builder, label, bits, layout) }; + DebugEmit, jett_rt_v1_debug_emit, false, (builder: u64 => I64), u32 => I32, + |s| s.debug_emit(builder); + TraceInt64, jett_rt_v1_trace_int64, false, (prefix_pointer: u64 => I64, prefix_length: u64 => I64, value: i64 => I64), u32 => I32, + |_s| { if prefix_pointer == 0 { return Err(INVALID_TRACE_LABEL); } + let length = usize::try_from(prefix_length).map_err(|_| INVALID_TRACE_LABEL)?; + let prefix = unsafe { std::slice::from_raw_parts(prefix_pointer as *const u8, length) }; + let mut stderr = io::stderr().lock(); + write_all_bytes(&mut stderr, prefix) + .and_then(|_| write_all_bytes(&mut stderr, value.to_string().as_bytes())) + .and_then(|_| write_all_bytes(&mut stderr, b"\n")) + .and_then(|_| stderr.flush()) + .map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDERR_WRITE_MESSAGE))?; + Ok(0) }; + BreakpointEmpty, jett_rt_v1_breakpoint_empty, false, (condition: u32 => I32), u32 => I32, + |_s| { if condition != 0 { + let mut stderr = io::stderr().lock(); + write_all_bytes(&mut stderr, b"breakpoint hit\n") + .and_then(|_| stderr.flush()) + .map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDERR_WRITE_MESSAGE))?; + } Ok(0) }; + BreakpointInt64, jett_rt_v1_breakpoint_int64, false, (condition: u32 => I32, prefix_pointer: u64 => I64, prefix_length: u64 => I64, value: i64 => I64), u32 => I32, + |_s| { if condition != 0 { + if prefix_pointer == 0 { return Err(INVALID_TRACE_LABEL); } + let length = usize::try_from(prefix_length).map_err(|_| INVALID_TRACE_LABEL)?; + let prefix = unsafe { std::slice::from_raw_parts(prefix_pointer as *const u8, length) }; + let mut stderr = io::stderr().lock(); + write_all_bytes(&mut stderr, prefix) + .and_then(|_| write_all_bytes(&mut stderr, value.to_string().as_bytes())) + .and_then(|_| write_all_bytes(&mut stderr, b"\n")) + .and_then(|_| stderr.flush()) + .map_err(|_| (JettRuntimeStatusV1::IO_FAILURE, STDERR_WRITE_MESSAGE))?; + } Ok(0) }; +} + +/// Read the first terminal failure without clearing it. Static message storage +/// remains valid after context destruction, like all v1 result messages. +/// # Safety +/// Same context and result pointer requirements as the lifecycle ABI. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_value_failure( + context: *const JettRuntimeContextV1, + out: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out, || { + let key = match context_key(context) { + Ok(k) => k, + Err(status) => return JettRuntimeResultV1::failure(status, CONTEXT_INVALID_MESSAGE), + }; + let lease = match acquire_context(key) { + Ok(l) => l, + Err(_) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + } + }; + let state = lock_unpoisoned(&lease.entry.state); + match state.as_ref().and_then(|s| s.values.current_failure()) { + Some((status, message)) => JettRuntimeResultV1::failure(status, message), + None => JettRuntimeResultV1::ok(), + } + }) +} + +/// Copy the current terminal failure message into caller-owned storage. +/// A null buffer with zero capacity queries the required byte length. Unlike +/// `jett_rt_v1_value_failure`, this also returns dynamically produced messages +/// and the active compiler-generated property/trial prefix. +/// +/// # Safety +/// `context` must be a live context. `out_result` and `out_length` must be +/// distinct, aligned, writable records. A non-null `buffer` must be writable +/// for `capacity` bytes and not overlap either output record. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn jett_rt_v1_value_failure_copy( + context: *const JettRuntimeContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut JettRuntimeResultV1, +) -> JettRuntimeStatusV1 { + complete_call(out_result, || { + if out_length.is_null() || (buffer.is_null() && capacity != 0) { + return JettRuntimeResultV1::failure(INVALID_FAILURE_COPY.0, INVALID_FAILURE_COPY.1); + } + let capacity = match usize::try_from(capacity) { + Ok(value) if value <= isize::MAX as usize => value, + _ => { + return JettRuntimeResultV1::failure( + INVALID_FAILURE_COPY.0, + INVALID_FAILURE_COPY.1, + ); + } + }; + let key = match context_key(context) { + Ok(key) => key, + Err(status) => return JettRuntimeResultV1::failure(status, CONTEXT_INVALID_MESSAGE), + }; + let lease = match acquire_context(key) { + Ok(lease) => lease, + Err(_) => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + } + }; + let state = lock_unpoisoned(&lease.entry.state); + let Some(state) = state.as_ref() else { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::INVALID_CONTEXT, + CONTEXT_INVALID_MESSAGE, + ); + }; + let message = state + .values + .current_dynamic_failure_message() + .or_else(|| state.values.current_failure().map(|(_, message)| message)) + .unwrap_or_default(); + let prefix = if state.values.current_failure().is_some() { + state + .values + .current_property_case_context() + .unwrap_or_default() + } else { + &[] + }; + let length = match prefix + .len() + .checked_add(message.len()) + .and_then(|n| u64::try_from(n).ok()) + { + Some(length) => length, + None => { + return JettRuntimeResultV1::failure( + JettRuntimeStatusV1::RESOURCE_EXHAUSTED, + EXHAUSTED.1, + ); + } + }; + // SAFETY: `out_length` is a caller-provided writable u64 by contract. + unsafe { ptr::write(out_length, length) }; + if buffer.is_null() && capacity == 0 { + return JettRuntimeResultV1::ok(); + } + if (capacity as u64) < length { + return JettRuntimeResultV1::failure(INVALID_FAILURE_COPY.0, INVALID_FAILURE_COPY.1); + } + if !prefix.is_empty() { + // SAFETY: the checked capacity includes both prefix and message. + unsafe { ptr::copy_nonoverlapping(prefix.as_ptr(), buffer, prefix.len()) }; + } + if !message.is_empty() { + // SAFETY: the caller provides a nonoverlapping writable buffer of + // at least `capacity` bytes, checked against the message length. + unsafe { + ptr::copy_nonoverlapping(message.as_ptr(), buffer.add(prefix.len()), message.len()) + }; + } + JettRuntimeResultV1::ok() + }) +} + +fn unsigned_gcd(mut a: u64, mut b: u64) -> u64 { + while b != 0 { + (a, b) = (b, a % b); + } + a +} + +#[cfg(test)] +mod tests { + use super::*; + use std::mem::MaybeUninit; + + include!("values/actor_identity_tests.rs"); + + #[test] + fn float_remainder_leaf_preserves_ieee_values_without_runtime_failure() { + let context = Context::new(); + for (left, right, expected) in [ + (5.5, 2.0, 1.5_f64), + (-5.5, 2.0, -1.5), + (5.5, -2.0, 1.5), + (-5.5, -2.0, -1.5), + (-0.0, 3.0, -0.0), + (1.0, f64::INFINITY, 1.0), + (f64::MAX, f64::MIN_POSITIVE, 0.0), + (-f64::MAX, f64::MIN_POSITIVE, -0.0), + (f64::from_bits(5), f64::from_bits(2), f64::from_bits(1)), + ] { + let actual = unsafe { jett_rt_v1_float_remainder(context.pointer(), left, right) }; + assert_eq!(actual.to_bits(), expected.to_bits()); + } + for (left, right) in [ + (1.0, 0.0), + (f64::INFINITY, 2.0), + (f64::NAN, 2.0), + (2.0, f64::NAN), + ] { + assert!(unsafe { jett_rt_v1_float_remainder(context.pointer(), left, right) }.is_nan()); + } + assert_eq!(unsafe { jett_rt_v1_value_status(context.pointer()) }, 0); + } + + struct Context(Box); + impl Context { + fn new() -> Self { + let mut value = Box::new(JettRuntimeContextV1::retired()); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *value, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + Self(value) + } + fn destroy(self, expected: JettRuntimeStatusV1) { + let mut context = std::mem::ManuallyDrop::new(self); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context.0, result.as_mut_ptr()) }, + expected + ); + unsafe { + drop(ptr::read(&context.0)); + } + } + fn pointer(&self) -> *const JettRuntimeContextV1 { + &*self.0 + } + fn text(&self, text: &str) -> u64 { + unsafe { jett_rt_v1_string_literal(self.pointer(), text.as_ptr(), text.len() as u64) } + } + fn count(&self) -> usize { + let lease = acquire_context(context_key(self.pointer()).unwrap()).unwrap(); + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .strings + .len() + } + } + impl Drop for Context { + fn drop(&mut self) { + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *self.0, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + } + } + + const DEBUG_PRINT_CHANNEL_PROBE: &str = "JETT_RUNTIME_DEBUG_PRINT_CHANNEL_PROBE"; + + fn capture_debug_print_channel_probe(mode: &str) -> std::process::Output { + // A child harness isolates the actual process streams from parallel tests. + // The exact filter prevents the parent assertions from spawning recursively. + std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "native_abi::values::tests::debug_print_channel_probe", + "--nocapture", + "--test-threads=1", + ]) + .env(DEBUG_PRINT_CHANNEL_PROBE, mode) + .output() + .unwrap() + } + + #[test] + fn debug_print_uses_diagnostic_stderr_and_preserves_exact_borrowed_bytes() { + let output = capture_debug_print_channel_probe("bytes"); + assert!(output.status.success(), "child probe failed: {output:?}"); + let expected = concat!( + "JETT-DEBUG-PARTIAL \u{03bb}\u{1f642}\0 ", + "trace runtime-channel: int64 = 7\n", + "breakpoint hit\n", + "JETT-DEBUG-LINE [redacted]\r\ninner\n\n", + "JETT-DEBUG-TAIL", + ); + assert_eq!(output.stderr, expected.as_bytes()); + // libtest owns its own stdout banner, so compare the application marker + // and reject diagnostic payloads rather than asserting an empty stream. + assert!( + output + .stdout + .windows(b"JETT-APPLICATION-ONLY\n".len()) + .any(|bytes| bytes == b"JETT-APPLICATION-ONLY\n") + ); + for marker in [ + b"JETT-DEBUG-".as_slice(), + b"trace runtime-channel: int64 = ".as_slice(), + b"breakpoint hit".as_slice(), + ] { + assert!( + !output + .stdout + .windows(marker.len()) + .any(|bytes| bytes == marker) + ); + } + } + + #[test] + fn debug_print_bad_handle_emits_nothing_and_keeps_first_failure() { + let output = capture_debug_print_channel_probe("invalid-handle"); + assert!(output.status.success(), "child probe failed: {output:?}"); + assert!(output.stderr.is_empty(), "{output:?}"); + assert!( + !output + .stdout + .windows(b"JETT-DEBUG-SUPPRESSED".len()) + .any(|bytes| bytes == b"JETT-DEBUG-SUPPRESSED") + ); + } + + fn emit_borrowed_debug_print(context: &Context, text: &str) { + let previous_count = context.count(); + let value = context.text(text); + let expected = context.text(text); + assert_eq!(context.count(), previous_count + 2); + unsafe { + assert_eq!(jett_rt_v1_string_debug_print(context.pointer(), value), 0); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + assert_eq!( + jett_rt_v1_string_equal(context.pointer(), value, expected), + 1 + ); + assert_eq!(context.count(), previous_count + 2); + assert_eq!(jett_rt_v1_string_release(context.pointer(), value), 0); + assert_eq!(jett_rt_v1_string_release(context.pointer(), expected), 0); + } + assert_eq!(context.count(), previous_count); + } + + fn debug_print_bytes_probe() { + let context = Context::new(); + // Emit without granting Stdout or Stderr: this is compiler observation. + emit_borrowed_debug_print(&context, "JETT-DEBUG-PARTIAL \u{03bb}\u{1f642}\0 "); + let prefix = b"trace runtime-channel: int64 = "; + unsafe { + assert_eq!( + jett_rt_v1_trace_int64( + context.pointer(), + prefix.as_ptr() as u64, + prefix.len() as u64, + 7, + ), + 0 + ); + assert_eq!(jett_rt_v1_breakpoint_empty(context.pointer(), 0), 0); + assert_eq!(jett_rt_v1_breakpoint_empty(context.pointer(), 1), 0); + } + emit_borrowed_debug_print(&context, ""); + emit_borrowed_debug_print(&context, "JETT-DEBUG-LINE [redacted]\r\ninner\n\n"); + emit_borrowed_debug_print(&context, "JETT-DEBUG-TAIL"); + let application = context.text("JETT-APPLICATION-ONLY\n"); + unsafe { + let authority = jett_rt_v1_grant_stdout(context.pointer()); + assert_ne!(authority, 0); + assert_eq!( + jett_rt_v1_string_stdout(context.pointer(), authority, application), + 0 + ); + assert_eq!(jett_rt_v1_string_release(context.pointer(), application), 0); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + } + assert_eq!(context.count(), 0); + } + + fn debug_print_invalid_handle_probe() { + let context = Context::new(); + let suppressed = context.text("JETT-DEBUG-SUPPRESSED"); + unsafe { + assert_ne!(jett_rt_v1_string_debug_print(context.pointer(), 0), 0); + assert_ne!( + jett_rt_v1_string_debug_print(context.pointer(), suppressed), + 0 + ); + let mut failure = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure(context.pointer(), failure.as_mut_ptr()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let failure = failure.assume_init(); + assert_eq!( + slice::from_raw_parts(failure.message.data, failure.message.byte_length as usize), + b"invalid native string handle" + ); + assert_eq!(jett_rt_v1_string_release(context.pointer(), suppressed), 0); + } + assert_eq!(context.count(), 0); + // A retained entry failure does not make successful cleanup fail. + context.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn debug_print_channel_probe() { + match std::env::var(DEBUG_PRINT_CHANNEL_PROBE).as_deref() { + Ok("bytes") => debug_print_bytes_probe(), + Ok("invalid-handle") => debug_print_invalid_handle_probe(), + Err(std::env::VarError::NotPresent) => {} + mode => panic!("unexpected debug-print channel probe mode: {mode:?}"), + } + } + + fn function_debug_layout_bytes(nodes: &[Vec], root: u32) -> Vec { + let mut bytes = b"JD\x01".to_vec(); + bytes.extend_from_slice(&u32::try_from(nodes.len()).unwrap().to_le_bytes()); + bytes.extend_from_slice(&root.to_le_bytes()); + for node in nodes { + bytes.extend_from_slice(&u32::try_from(node.len()).unwrap().to_le_bytes()); + bytes.extend_from_slice(node); + } + bytes + } + + fn identity_test_info( + values: &mut NativeValues, + name: &str, + kind: &str, + args: Vec, + handles: &mut Vec, + ) -> u64 { + let name = values.insert(name.into()).unwrap(); + let kind = values.insert(kind.into()).unwrap(); + let arguments = values.new_list(false).unwrap(); + values.lists.get_mut(&arguments).unwrap().elements = args.into_iter().map(Some).collect(); + let info = values.new_struct(6).unwrap(); + for (index, bits) in [(0, name), (1, kind), (4, 0), (5, arguments)] { + values.structs.get_mut(&info).unwrap().fields[index] = Some(NativeField { + bits, + pending_depth: 0, + owned: false, + }); + } + handles.extend([name, kind, arguments, info]); + info + } + + #[test] + fn deep_type_info_identity_preserves_aliases_and_shared_children() { + let mut values = NativeValues::default(); + let mut handles = Vec::new(); + let leaf = identity_test_info(&mut values, "int64", "primitive", vec![], &mut handles); + let mut current = leaf; + for _ in 0..4096 { + current = + identity_test_info(&mut values, "Alias", "alias", vec![current], &mut handles); + } + assert_eq!( + values.type_info_identity(current, false), + Ok("9:primitive5:int640:0".into()) + ); + let preserved = format!("{}9:primitive5:int640:0", "5:alias5:Alias0:1".repeat(4096)); + assert_eq!(values.type_info_identity(current, true), Ok(preserved)); + let pair = identity_test_info( + &mut values, + "unused", + "map", + vec![current, current], + &mut handles, + ); + assert_eq!( + values.type_info_identity(pair, false), + Ok("3:map0:0:29:primitive5:int640:09:primitive5:int640:0".into()) + ); + for handle in handles { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn type_info_identity_rejects_cycles_and_malformed_aliases() { + let mut values = NativeValues::default(); + let mut handles = Vec::new(); + let alias = identity_test_info(&mut values, "Alias", "alias", vec![], &mut handles); + assert_eq!( + values.type_info_identity(alias, false), + Err(INVALID_TYPE_INFO) + ); + let args = values.struct_field(alias, 5).unwrap().bits; + values + .lists + .get_mut(&args) + .unwrap() + .elements + .push(Some(alias)); + for preserve in [false, true] { + assert_eq!( + values.type_info_identity(alias, preserve), + Err(INVALID_TYPE_INFO) + ); + } + values.lists.get_mut(&args).unwrap().elements[0] = None; + assert_eq!( + values.type_info_identity(alias, true), + Err(INVALID_TYPE_INFO) + ); + for handle in handles { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + fn debug_function_descriptor(values: &mut NativeValues, label: &str, captured: bool) -> u64 { + let environment = if captured { + let secret = values.insert("private capture".into()).unwrap(); + let environment = values.new_struct(1).unwrap(); + values.structs.get_mut(&environment).unwrap().fields[0] = Some(NativeField { + bits: secret, + owned: true, + pending_depth: 0, + }); + environment + } else { + 0 + }; + let label = values.insert(label.into()).unwrap(); + let descriptor = values.new_struct(NATIVE_FUNCTION_FIELD_COUNT).unwrap(); + values.structs.get_mut(&descriptor).unwrap().fields = vec![ + Some(NativeField { + // Deliberately not a callable address: debug must never invoke it. + bits: u64::MAX, + owned: false, + pending_depth: 0, + }), + Some(NativeField { + bits: environment, + owned: captured, + pending_depth: 0, + }), + Some(NativeField { + bits: label, + owned: true, + pending_depth: 0, + }), + ]; + descriptor + } + + #[test] + fn uninhabited_debug_nodes_allow_empty_containers_but_reject_payloads() { + let mut values = NativeValues::default(); + let mut list = vec![NativeDebugTag::List as u8]; + list.extend_from_slice(&0_u32.to_le_bytes()); + let mut optional = vec![NativeDebugTag::Optional as u8]; + optional.extend_from_slice(&0_u32.to_le_bytes()); + let nodes = [vec![NativeDebugTag::Uninhabited as u8], list, optional]; + let list_bytes = function_debug_layout_bytes(&nodes, 1); + let optional_bytes = function_debug_layout_bytes(&nodes, 2); + let list_layout = NativeDebugLayout::parse(&list_bytes).unwrap(); + let optional_layout = NativeDebugLayout::parse(&optional_bytes).unwrap(); + let empty_list = values.new_list(false).unwrap(); + let absent = values.sum(SUM_FAILURE, 0, false).unwrap(); + let present = values.sum(SUM_SUCCESS, 0, false).unwrap(); + let text = values.insert("before".to_owned()).unwrap(); + assert_eq!( + list_layout.format_value(&values, empty_list, 1), + Ok("list()".into()) + ); + assert_eq!( + optional_layout.format_value(&values, absent, 2), + Ok("none".into()) + ); + assert_eq!( + optional_layout.format_value(&values, present, 2), + Err(INVALID_TRACE_LABEL) + ); + values + .lists + .get_mut(&empty_list) + .unwrap() + .elements + .push(Some(0)); + assert_eq!( + values.debug_append_aggregate(text, "", empty_list, &list_bytes), + Err(INVALID_TRACE_LABEL) + ); + assert_eq!(values.text(text), Ok("before")); + for handle in [empty_list, absent, present, text] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn special_debug_tags_are_additive_and_have_no_payload() { + for (expected, tag) in [ + NativeDebugTag::Primitive, + NativeDebugTag::Nothing, + NativeDebugTag::Bytes, + NativeDebugTag::List, + NativeDebugTag::Set, + NativeDebugTag::Map, + NativeDebugTag::Optional, + NativeDebugTag::Result, + NativeDebugTag::Record, + NativeDebugTag::Enum, + NativeDebugTag::Machine, + NativeDebugTag::Capability, + NativeDebugTag::Alias, + NativeDebugTag::Function, + NativeDebugTag::Actor, + NativeDebugTag::TypeConstruction, + NativeDebugTag::Redacted, + NativeDebugTag::Interface, + NativeDebugTag::Uninhabited, + ] + .into_iter() + .enumerate() + { + let expected = u8::try_from(expected).unwrap(); + assert_eq!(tag as u8, expected); + assert_eq!(NativeDebugTag::from_raw(expected), Ok(tag)); + } + assert_eq!(NativeDebugTag::from_raw(19), Err(INVALID_TRACE_LABEL)); + let bytes = function_debug_layout_bytes(&[vec![13]], 0); + let layout = NativeDebugLayout::parse(&bytes).unwrap(); + assert!(matches!( + layout.nodes.as_slice(), + [NativeDebugNode::Function] + )); + let malformed = function_debug_layout_bytes(&[vec![13, 0]], 0); + assert!(matches!( + NativeDebugLayout::parse(&malformed), + Err(INVALID_TRACE_LABEL) + )); + assert_eq!( + ( + NATIVE_FUNCTION_FIELD_COUNT, + NATIVE_FUNCTION_CODE_FIELD, + NATIVE_FUNCTION_ENVIRONMENT_FIELD, + NATIVE_FUNCTION_LABEL_FIELD, + ), + (3, 0, 1, 2) + ); + } + + #[test] + fn redacted_debug_nodes_hide_payloads_and_pending_depth_without_reading_handles() { + let mut alias = vec![NativeDebugTag::Alias as u8]; + alias.extend_from_slice(&0u32.to_le_bytes()); + let layout = NativeDebugLayout::parse(&function_debug_layout_bytes( + &[vec![NativeDebugTag::Redacted as u8], alias], + 1, + )) + .unwrap(); + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + let mut list_layout = layout.clone(); + list_layout.nodes.push(NativeDebugNode::List(layout.root)); + list_layout.root = list_layout.nodes.len() - 1; + for bits in [0, 4321, u64::MAX] { + assert_eq!( + layout.format_value(&values, bits, layout.root), + Ok("[redacted]".to_owned()) + ); + let contents = values.lists.get_mut(&list).unwrap(); + contents.elements = vec![Some(bits)]; + contents.element_pending_depths.insert(0, u64::MAX); + assert_eq!( + list_layout.format_value(&values, list, list_layout.root), + Ok("list([redacted])".to_owned()) + ); + } + assert_eq!( + layout.equal_value(&values, 1, 1, layout.root), + Err(INVALID_STRUCT) + ); + let public = NativeDebugLayout { + root: 1, + nodes: vec![ + NativeDebugNode::Primitive(NativeSortKind::String as u32), + NativeDebugNode::List(0), + ], + }; + let marker = values.insert("[redacted]".into()).unwrap(); + let contents = values.lists.get_mut(&list).unwrap(); + contents.elements = vec![Some(marker)]; + contents.element_pending_depths.insert(0, 2); + assert_eq!( + public.format_value(&values, list, public.root), + Ok("list(pending(pending([redacted])))".to_owned()) + ); + values.drop_value(list).unwrap(); + values.drop_value(marker).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn function_adapters_preserve_labels_and_pending_depth_without_mutating_source() { + let mut values = NativeValues::default(); + let original = debug_function_descriptor(&mut values, "function(callback)", true); + let pending = values.run_record(original).unwrap(); + let adapted = values.function_adapter(pending, 123).unwrap(); + let joined = values.join_record(adapted).unwrap(); + assert_eq!( + values.function_debug_label(adapted).unwrap(), + "function(callback)" + ); + assert_eq!(values.owned_pending_depth(pending), Ok(1)); + assert_eq!(values.owned_pending_depth(adapted), Ok(1)); + assert_eq!(values.owned_pending_depth(joined), Ok(0)); + let environment = values + .struct_field(joined, NATIVE_FUNCTION_ENVIRONMENT_FIELD) + .unwrap() + .bits; + let captured = values.struct_field(environment, 0).unwrap().bits; + assert_eq!(values.owned_pending_depth(captured), Ok(0)); + for handle in [original, pending, adapted, joined] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn function_adapter_allocation_failures_release_partial_environments() { + for budget in 0..4 { + let mut values = NativeValues::default(); + let source = debug_function_descriptor(&mut values, "function(callback)", true); + values.allocation_budget = Some(budget); + assert_eq!(values.function_adapter(source, 123), Err(EXHAUSTED)); + assert_eq!( + values.function_debug_label(source).unwrap(), + "function(callback)" + ); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn interface_boxes_preserve_pending_aliases_and_redaction() { + let mut values = NativeValues::default(); + let source = values.insert_pending("hidden".into(), 2).unwrap(); + let hidden = function_debug_layout_bytes(&[vec![NativeDebugTag::Redacted as u8]], 0); + let boxed = values.interface_box(42, source, true, 0, &hidden).unwrap(); + let clone = values.clone_struct(boxed).unwrap(); + let joined = values.join_record(clone).unwrap(); + assert_eq!(values.owned_pending_depth(source), Ok(2)); + assert_eq!(values.owned_pending_depth(boxed), Ok(2)); + assert_eq!(values.owned_pending_depth(joined), Ok(1)); + let payload = values.struct_field(joined, 1).unwrap(); + assert_eq!(values.owned_pending_depth(payload.bits), Ok(0)); + let layout = NativeDebugLayout::parse(&function_debug_layout_bytes( + &[vec![NativeDebugTag::Interface as u8]], + 0, + )) + .unwrap(); + assert_eq!( + layout.format_value(&values, joined, 0), + Ok("[redacted]".into()) + ); + for handle in [source, boxed, clone, joined] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn interface_box_allocation_failures_release_partial_payloads() { + let layout = function_debug_layout_bytes(&[vec![NativeDebugTag::Redacted as u8]], 0); + // Box, cloned string, and layout each have a separate allocation. + for budget in 0..3 { + let mut values = NativeValues::default(); + let original = values.insert_pending("secret".into(), 1).unwrap(); + values.allocation_budget = Some(budget); + assert_eq!( + values.interface_box(7, original, true, 0, &layout), + Err(EXHAUSTED) + ); + assert_eq!(values.strings.len(), 1); + assert_eq!(values.owned_pending_depth(original), Ok(1)); + values.drop_value(original).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn deep_interface_conversion_descriptors_preserve_values_and_cleanup() { + use interface_conversion::NativeInterfaceConversion as C; + let mut primitive = vec![NativeDebugTag::Primitive as u8]; + primitive.extend_from_slice(&(NativeSortKind::Int64 as u32).to_le_bytes()); + let mut conversion = C::Box { + concrete: 9, + owned: false, + nothing: false, + layout: function_debug_layout_bytes(&[primitive], 0), + }; + for _ in 0..130 { + conversion = C::List(Box::new(conversion)); + } + let encoded = conversion.encode(); + let parsed = C::parse(&encoded).unwrap(); + assert_eq!(parsed, conversion); + for end in 0..encoded.len() { + assert!(C::parse(&encoded[..end]).is_err()); + } + let mut extra = encoded.clone(); + extra.push(0); + assert!(C::parse(&extra).is_err()); + for budget in [Some(0), Some(64), Some(129), Some(130), Some(131), None] { + let mut values = NativeValues::default(); + let mut source = 7; + for depth in 0..130 { + let list = values.new_list(depth != 0).unwrap(); + values + .lists + .get_mut(&list) + .unwrap() + .elements + .push(Some(source)); + source = list; + } + values.allocation_budget = budget; + let converted = parsed.convert( + &mut values, + NativeField { + bits: source, + owned: true, + pending_depth: 0, + }, + ); + if budget.is_some() { + assert_eq!(converted.map(|field| field.bits), Err(EXHAUSTED)); + assert_eq!(values.lists.len(), 130); + assert!(values.structs.is_empty()); + assert!(values.bytes.is_empty()); + } else { + let converted = converted.unwrap(); + let mut payload = converted.bits; + for _ in 0..130 { + payload = values.lists[&payload].elements[0].unwrap(); + } + assert_eq!(values.struct_field(payload, 0).unwrap().bits, 9); + assert_eq!(values.struct_field(payload, 1).unwrap().bits, 7); + values.drop_value(converted.bits).unwrap(); + } + values.drop_value(source).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn nested_interface_conversion_failures_release_partial_containers() { + use interface_conversion::NativeInterfaceConversion as C; + let boxed = C::Box { + concrete: 9, + owned: true, + nothing: false, + layout: function_debug_layout_bytes(&[vec![NativeDebugTag::Redacted as u8]], 0), + }; + let conversion = C::List(Box::new(boxed.clone())); + assert_eq!(C::parse(&conversion.encode()), Ok(conversion.clone())); + for budget in 0..7 { + let mut values = NativeValues::default(); + let a = values.insert("first".into()).unwrap(); + let b = values.insert("second".into()).unwrap(); + let source = values.new_list(true).unwrap(); + values.lists.get_mut(&source).unwrap().elements = vec![Some(a), Some(b)]; + values.allocation_budget = Some(budget); + assert_eq!( + conversion + .convert( + &mut values, + NativeField { + bits: source, + owned: true, + pending_depth: 0 + } + ) + .map(|field| field.bits), + Err(EXHAUSTED) + ); + assert_eq!(values.strings.len(), 2); + assert_eq!(values.lists.len(), 1); + assert!(values.structs.is_empty()); + assert!(values.bytes.is_empty()); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); + } + let mapped = C::Map(Box::new(boxed.clone()), true); + for budget in [1, 4] { + let mut values = NativeValues::default(); + let source = values.new_map(1, 1).unwrap(); + for name in ["first", "second"] { + let key = values.insert(format!("key-{name}")).unwrap(); + let value = values.insert(name.into()).unwrap(); + values + .maps + .get_mut(&source) + .unwrap() + .entries + .push(Some(NativeMapEntry { + key, + value, + key_pending_depth: 0, + value_pending_depth: 0, + key_taken: false, + })); + } + values.allocation_budget = Some(budget); + assert_eq!( + mapped + .convert( + &mut values, + NativeField { + bits: source, + owned: true, + pending_depth: 0 + } + ) + .map(|field| field.bits), + Err(EXHAUSTED) + ); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); + } + let sum_conversion = C::Optional(Box::new(boxed)); + let mut values = NativeValues::default(); + let payload = values.insert("payload".into()).unwrap(); + let source = values.sum(SUM_SUCCESS, payload, true).unwrap(); + values.allocation_budget = Some(3); + assert_eq!( + sum_conversion + .convert( + &mut values, + NativeField { + bits: source, + owned: true, + pending_depth: 0 + } + ) + .map(|field| field.bits), + Err(EXHAUSTED) + ); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn callback_container_conversions_preserve_sources_and_rollback_every_allocation() { + use interface_conversion::NativeInterfaceConversion as C; + for shape in 0..3 { + let mut succeeded = false; + for budget in 0..64 { + let mut values = NativeValues::default(); + let original = debug_function_descriptor(&mut values, "function(callback)", true); + let source_callback = values.run_record(original).unwrap(); + values.drop_value(original).unwrap(); + let callback = C::FunctionAdapter { code: 123 }; + let (source, conversion) = + match shape { + 0 => { + let other = values.clone_value(source_callback).unwrap(); + let source = values.new_list(true).unwrap(); + values.lists.get_mut(&source).unwrap().elements = + vec![Some(source_callback), Some(other)]; + (source, C::List(Box::new(callback))) + } + 1 => ( + values.sum(SUM_SUCCESS, source_callback, true).unwrap(), + C::Optional(Box::new(callback)), + ), + _ => { + let source = values.new_map(1, 1).unwrap(); + let key = values.insert("key".into()).unwrap(); + values.maps.get_mut(&source).unwrap().entries.push(Some( + NativeMapEntry { + key, + value: source_callback, + key_pending_depth: 0, + value_pending_depth: 0, + key_taken: false, + }, + )); + (source, C::Map(Box::new(callback), true)) + } + }; + values.allocation_budget = Some(budget); + let result = conversion.convert( + &mut values, + NativeField { + bits: source, + owned: true, + pending_depth: 0, + }, + ); + assert_eq!(values.owned_pending_depth(source_callback), Ok(1)); + assert_eq!( + values.function_debug_label(source_callback).unwrap(), + "function(callback)" + ); + match result { + Ok(output) => { + let adapted = match shape { + 0 => values.lists[&output.bits].elements[0].unwrap(), + 1 => values.sums[&output.bits].bits, + _ => values.maps[&output.bits].entries[0].as_ref().unwrap().value, + }; + assert_eq!( + values + .struct_field(adapted, NATIVE_FUNCTION_CODE_FIELD) + .unwrap() + .bits, + 123 + ); + assert_eq!(values.owned_pending_depth(adapted), Ok(1)); + values.drop_value(output.bits).unwrap(); + succeeded = true; + } + Err(error) => assert_eq!(error, EXHAUSTED, "shape {shape}, budget {budget}"), + } + values.drop_value(source).unwrap(); + assert!(values.is_empty(), "shape {shape}, budget {budget}"); + if succeeded { + break; + } + } + assert!(succeeded, "shape {shape} never completed"); + } + } + + #[test] + fn callback_conversion_descriptors_report_exact_function_relocations() { + use interface_conversion::NativeInterfaceConversion as C; + let conversion = C::Result( + Box::new(C::List(Box::new(C::FunctionAdapter { code: 17 }))), + Box::new(C::Optional(Box::new(C::FunctionAdapter { code: 19 }))), + ); + let (bytes, functions) = conversion.encode_with_function_offsets(); + assert_eq!( + functions.iter().map(|(_, code)| *code).collect::>(), + vec![17, 19] + ); + for (offset, code) in functions { + assert_eq!(&bytes[offset..offset + 8], &code.to_le_bytes()); + } + assert_eq!(C::parse(&bytes), Ok(conversion)); + for end in 0..bytes.len() { + assert!(C::parse(&bytes[..end]).is_err()); + } + assert!(C::parse(&C::FunctionAdapter { code: 0 }.encode()).is_err()); + } + + #[test] + fn interface_conversion_descriptors_reject_truncated_or_extra_bytes() { + use interface_conversion::NativeInterfaceConversion as C; + let conversion = C::Result( + Box::new(C::List(Box::new(C::Copy { owned: true }))), + Box::new(C::Copy { owned: false }), + ); + let bytes = conversion.encode(); + for end in 0..bytes.len() { + assert!(C::parse(&bytes[..end]).is_err()); + } + let mut extra = bytes.clone(); + extra.push(0); + assert!(C::parse(&extra).is_err()); + assert!(C::parse(&[0, 2]).is_err()); + assert_eq!(C::parse(&bytes), Ok(conversion)); + } + + #[test] + fn actor_debug_uses_registration_order_without_exposing_state_or_handle() { + let mut values = NativeValues::default(); + let first = values.new_struct(0).unwrap(); + let second = values.new_struct(0).unwrap(); + values.register_actor(first).unwrap(); + values.register_actor(second).unwrap(); + let layout = NativeDebugLayout::parse(&function_debug_layout_bytes( + &[vec![NativeDebugTag::Actor as u8]], + 0, + )) + .unwrap(); + assert_eq!(layout.format_value(&values, first, 0).unwrap(), "actor#0"); + assert_eq!(layout.format_value(&values, second, 0).unwrap(), "actor#1"); + assert!(matches!( + layout.format_value(&values, u64::MAX, 0), + Err(INVALID_ACTOR) + )); + values.release_actors().unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn capability_tasks_preserve_authority_and_release_consumed_handles() { + let mut values = NativeValues::default(); + let authority = next_identity().unwrap(); + values.stdout = Some(authority); + assert_eq!( + values.debug_capability(authority, "Stdout").unwrap(), + "nothing" + ); + + let first = values.run_capability(authority).unwrap(); + assert_eq!( + values.debug_capability(first, "Stdout").unwrap(), + "pending(nothing)" + ); + let nested = values.run_capability(first).unwrap(); + assert!(!values.capability_tasks.contains_key(&first)); + let outer = values.join_capability(nested).unwrap(); + let inner = values.sums.get(&outer).unwrap().bits; + assert_eq!( + values.debug_capability(inner, "Stdout").unwrap(), + "pending(nothing)" + ); + values.drop_value(outer).unwrap(); + let completed = values.join_capability(inner).unwrap(); + assert_eq!(values.sums.get(&completed).unwrap().bits, authority); + values.drop_value(completed).unwrap(); + assert!(values.capability_tasks.is_empty()); + + let cancelled = values.join_capability(authority).unwrap(); + let failure = values.sums.get(&cancelled).unwrap(); + assert_eq!(failure.tag, SUM_FAILURE); + assert_eq!(values.text(failure.bits).unwrap(), "task was cancelled"); + values.drop_value(cancelled).unwrap(); + + let environment = next_identity().unwrap(); + values.environment = Some(environment); + assert_eq!( + values.debug_capability(environment, "Environment").unwrap(), + "" + ); + let resolved = values.join_capability(environment).unwrap(); + let value = values.sums.get(&resolved).unwrap(); + assert_eq!(value.tag, SUM_SUCCESS); + assert_eq!(value.bits, environment); + values.drop_value(resolved).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn opaque_capability_grants_are_nominal_and_context_bound() { + let mut values = NativeValues::default(); + let mut other = NativeValues::default(); + let mut authorities = std::collections::HashSet::new(); + for kind in ["Stderr", "Stdin", "Filesystem", "Network", "Process", "Log"] { + let authority = values.grant_opaque_capability(kind).unwrap(); + assert!(authorities.insert(authority)); + assert_eq!(values.grant_opaque_capability(kind).unwrap(), authority); + assert_ne!(other.grant_opaque_capability(kind).unwrap(), authority); + assert!(other.capability_authority(authority).is_err()); + let task = values.run_capability(authority).unwrap(); + assert_eq!( + values.debug_capability(task, kind).unwrap(), + "pending(nothing)" + ); + let result = values.join_capability(task).unwrap(); + assert_eq!(values.sums.get(&result).unwrap().bits, authority); + values.drop_value(result).unwrap(); + } + assert!(values.capability_tasks.is_empty()); + assert!(values.is_empty()); + } + + #[test] + fn function_debug_formats_labels_without_consuming_or_exposing_captures() { + for (label, captured) in [ + ("function(app.increment)", false), + ("function(first, second)", true), + ("function()", false), + ] { + let mut values = NativeValues::default(); + let descriptor = debug_function_descriptor(&mut values, label, captured); + let clone = values.clone_value(descriptor).unwrap(); + let layout = function_debug_layout_bytes(&[vec![NativeDebugTag::Function as u8]], 0); + let builder = values.insert("trace ".into()).unwrap(); + let strings_before = values.strings.len(); + assert_eq!( + values.debug_append_aggregate(builder, "callback = ", descriptor, &layout), + Ok(0) + ); + assert_eq!( + values.text(builder), + Ok(format!("trace callback = {label}").as_str()) + ); + assert_eq!(values.strings.len(), strings_before); + assert_eq!(values.function_debug_label(descriptor), Ok(label)); + values.drop_value(descriptor).unwrap(); + assert_eq!(values.function_debug_label(clone), Ok(label)); + values.drop_value(clone).unwrap(); + values.drop_value(builder).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn function_debug_formats_nested_owned_values_and_rejects_equality() { + let mut values = NativeValues::default(); + let descriptor = debug_function_descriptor(&mut values, "function(value)", true); + let optional = values.sum(SUM_SUCCESS, descriptor, true).unwrap(); + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = vec![Some(optional)]; + let mut optional_node = vec![NativeDebugTag::Optional as u8]; + optional_node.extend_from_slice(&0_u32.to_le_bytes()); + let mut list_node = vec![NativeDebugTag::List as u8]; + list_node.extend_from_slice(&1_u32.to_le_bytes()); + let bytes = function_debug_layout_bytes( + &[ + vec![NativeDebugTag::Function as u8], + optional_node, + list_node, + ], + 2, + ); + let layout = NativeDebugLayout::parse(&bytes).unwrap(); + assert_eq!( + layout.format_value(&values, list, layout.root), + Ok("list(some(function(value)))".into()) + ); + assert_eq!( + layout.equal_value(&values, descriptor, descriptor, 0), + Err(INVALID_STRUCT) + ); + assert_eq!( + layout.equal_value(&values, list, list, layout.root), + Err(INVALID_STRUCT) + ); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn function_debug_rejects_invalid_descriptors_without_consuming_fields() { + let mut values = NativeValues::default(); + let descriptor = debug_function_descriptor(&mut values, "function(value)", true); + let original = values.structs[&descriptor].fields.clone(); + let environment = values + .struct_field(descriptor, NATIVE_FUNCTION_ENVIRONMENT_FIELD) + .unwrap() + .bits; + let label = values + .struct_field(descriptor, NATIVE_FUNCTION_LABEL_FIELD) + .unwrap() + .bits; + let layout = function_debug_layout_bytes(&[vec![NativeDebugTag::Function as u8]], 0); + let builder = values.insert("unchanged".into()).unwrap(); + let strings_before = values.strings.len(); + assert_eq!(values.function_debug_label(0), Err(INVALID_FUNCTION)); + assert_eq!(values.function_debug_label(label), Err(INVALID_FUNCTION)); + for (slot, replacement) in [ + (NATIVE_FUNCTION_CODE_FIELD, None), + (NATIVE_FUNCTION_ENVIRONMENT_FIELD, None), + (NATIVE_FUNCTION_LABEL_FIELD, None), + ( + NATIVE_FUNCTION_CODE_FIELD, + Some(NativeField { + bits: 0, + owned: false, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_CODE_FIELD, + Some(NativeField { + bits: u64::MAX, + owned: true, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_ENVIRONMENT_FIELD, + Some(NativeField { + bits: 0, + owned: true, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_ENVIRONMENT_FIELD, + Some(NativeField { + bits: environment, + owned: false, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_ENVIRONMENT_FIELD, + Some(NativeField { + bits: label, + owned: true, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_LABEL_FIELD, + Some(NativeField { + bits: label, + owned: false, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_LABEL_FIELD, + Some(NativeField { + bits: environment, + owned: true, + pending_depth: 0, + }), + ), + ( + NATIVE_FUNCTION_LABEL_FIELD, + Some(NativeField { + bits: 0, + owned: true, + pending_depth: 0, + }), + ), + ] { + values.structs.get_mut(&descriptor).unwrap().fields[usize::try_from(slot).unwrap()] = + replacement; + assert_eq!( + values.debug_append_aggregate(builder, "callback = ", descriptor, &layout), + Err(INVALID_FUNCTION), + "invalid field {slot}" + ); + assert_eq!(values.text(builder), Ok("unchanged")); + assert_eq!(values.text(label), Ok("function(value)")); + assert_eq!(values.strings.len(), strings_before); + values + .structs + .get_mut(&descriptor) + .unwrap() + .fields + .clone_from(&original); + } + for count in [2, 4] { + values + .structs + .get_mut(&descriptor) + .unwrap() + .fields + .resize(count, None); + assert_eq!( + values.function_debug_label(descriptor), + Err(INVALID_FUNCTION) + ); + values + .structs + .get_mut(&descriptor) + .unwrap() + .fields + .clone_from(&original); + } + values.drop_value(descriptor).unwrap(); + values.drop_value(builder).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn function_debug_rejects_invalid_label_text_and_preserves_cleanup() { + for label in [ + "", + "function", + "callback()", + "function(first\nsecond)", + "function(value)\0", + ] { + let mut values = NativeValues::default(); + let descriptor = debug_function_descriptor(&mut values, label, true); + assert_eq!( + values.function_debug_label(descriptor), + Err(INVALID_FUNCTION) + ); + values.drop_value(descriptor).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn function_debug_failure_keeps_owned_values_available_for_terminal_cleanup() { + let context = Context::new(); + let pointer = context.pointer(); + let (descriptor, builder) = { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + let values = &mut state.as_mut().unwrap().values; + ( + debug_function_descriptor(values, "invalid label", true), + values.insert("unchanged".into()).unwrap(), + ) + }; + let prefix = b"callback = "; + let layout = function_debug_layout_bytes(&[vec![NativeDebugTag::Function as u8]], 0); + unsafe { + assert_ne!( + jett_rt_v1_debug_append_aggregate( + pointer, + builder, + prefix.as_ptr() as u64, + u64::try_from(prefix.len()).unwrap(), + descriptor, + layout.as_ptr() as u64, + u64::try_from(layout.len()).unwrap(), + ), + 0 + ); + assert_eq!( + jett_rt_v1_value_status(pointer), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + } + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.failure, Some(INVALID_FUNCTION)); + assert_eq!(values.text(builder), Ok("unchanged")); + assert_eq!(values.structs.len(), 2); + assert_eq!(values.strings.len(), 3); + } + unsafe { + assert_eq!(jett_rt_v1_value_drop(pointer, descriptor), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, builder), 0); + } + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + assert!( + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .is_empty() + ); + } + + #[test] + fn nothing_nesting_join_format_and_cleanup() { + let context = Context::new(); + let pointer = context.pointer(); + unsafe { + let once = jett_rt_v1_nothing_run(pointer, 0); + let twice = jett_rt_v1_nothing_run(pointer, once); + assert_eq!((once, twice), (1, 2)); + for (depth, expected) in [ + (0, "nothing"), + (once, "pending(nothing)"), + (twice, "pending(pending(nothing))"), + ] { + let formatted = jett_rt_v1_nothing_format(pointer, depth); + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + { + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!(state.as_ref().unwrap().values.text(formatted), Ok(expected)); + } + assert_eq!(jett_rt_v1_value_drop(pointer, formatted), 0); + } + let joined = jett_rt_v1_nothing_join(pointer, twice); + assert_eq!(jett_rt_v1_sum_tag(pointer, joined), SUM_SUCCESS); + let remaining = jett_rt_v1_sum_take(pointer, joined, SUM_SUCCESS); + assert_eq!(remaining, 1); + let joined = jett_rt_v1_nothing_join(pointer, remaining); + assert_eq!(jett_rt_v1_sum_take(pointer, joined, SUM_SUCCESS), 0); + let cancelled = jett_rt_v1_nothing_join(pointer, 0); + assert_eq!(jett_rt_v1_sum_tag(pointer, cancelled), SUM_FAILURE); + let error = jett_rt_v1_sum_take(pointer, cancelled, SUM_FAILURE); + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + { + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state.as_ref().unwrap().values.text(error), + Ok("task was cancelled") + ); + } + assert_eq!(jett_rt_v1_value_drop(pointer, error), 0); + assert_eq!(jett_rt_v1_value_status(pointer), 0); + assert!( + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .is_empty() + ); + } + } + + #[test] + fn pending_string_depth_survives_aliases_and_join() { + let mut values = NativeValues::default(); + let plain = values.insert("Ada".to_owned()).unwrap(); + let alias = values.retain(plain).unwrap(); + let pending = values.run_string(plain).unwrap(); + let nested = values.run_string(pending).unwrap(); + assert_eq!(values.format_string(alias).unwrap(), "Ada"); + assert_eq!(values.format_string(pending).unwrap(), "pending(Ada)"); + assert_eq!( + values.format_string(nested).unwrap(), + "pending(pending(Ada))" + ); + let formatted = values.display_string(nested).unwrap(); + assert_eq!(values.text(formatted), Ok("pending(pending(Ada))")); + let joined = values.join_string(nested).unwrap(); + assert_eq!(values.format_string(joined).unwrap(), "pending(Ada)"); + let plain_joined = values.join_string(joined).unwrap(); + assert_eq!(values.format_string(plain_joined).unwrap(), "Ada"); + for handle in [ + alias, + plain, + pending, + nested, + formatted, + joined, + plain_joined, + ] { + values.release(handle).unwrap(); + } + assert!(values.is_empty()); + } + + fn display_result_string_snapshot( + values: &NativeValues, + ) -> std::collections::BTreeMap { + values + .strings + .iter() + .map(|(id, value)| { + ( + *id, + (value.text.clone(), value.references, value.pending_depth), + ) + }) + .collect() + } + + #[test] + fn display_result_check_preserves_strings_and_requires_every_pending_layer_to_join() { + for text in ["", "shown", "é🦀", "pending(shown)"] { + let mut values = NativeValues::default(); + let ready = values.insert(text.into()).unwrap(); + let alias = values.retain(ready).unwrap(); + let pending = values.run_string(ready).unwrap(); + let nested = values.run_string(pending).unwrap(); + let partial = values.join_string(nested).unwrap(); + let joined = values.join_string(partial).unwrap(); + let snapshot = display_result_string_snapshot(&values); + for (handle, expected) in [ + (ready, None), + (alias, None), + ( + pending, + Some(format!( + "Displayable.display returned pending({text}) instead of string" + )), + ), + ( + nested, + Some(format!( + "Displayable.display returned pending(pending({text})) instead of string" + )), + ), + ( + partial, + Some(format!( + "Displayable.display returned pending({text}) instead of string" + )), + ), + (joined, None), + ] { + values.dynamic_failure_message = None; + assert_eq!( + values.check_display_result(handle), + if expected.is_some() { + Err(INVALID_DISPLAY_RESULT) + } else { + Ok(0) + } + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + expected.as_ref().map(|message| message.as_bytes()) + ); + assert_eq!(display_result_string_snapshot(&values), snapshot); + } + for handle in [ready, alias, pending, nested, partial, joined] { + values.release(handle).unwrap(); + } + assert!(values.is_empty()); + } + } + + #[test] + fn display_result_check_rejects_invalid_string_handles_without_observing_other_values() { + let mut values = NativeValues::default(); + let ready = values.insert("kept".into()).unwrap(); + let stale = values.insert("stale".into()).unwrap(); + values.release(stale).unwrap(); + let bytes = values.insert_bytes(vec![65]).unwrap(); + let mut foreign_values = NativeValues::default(); + let foreign = foreign_values.insert("foreign".into()).unwrap(); + let snapshot = display_result_string_snapshot(&values); + for handle in [0, u64::MAX, stale, bytes, foreign] { + assert_eq!(values.check_display_result(handle), Err(INVALID_HANDLE)); + assert!(values.dynamic_failure_message.is_none()); + assert_eq!(display_result_string_snapshot(&values), snapshot); + assert_eq!(values.bytes(bytes), Ok([65].as_slice())); + } + values.release(ready).unwrap(); + values.drop_value(bytes).unwrap(); + foreign_values.release(foreign).unwrap(); + assert!(values.is_empty()); + assert!(foreign_values.is_empty()); + } + + #[test] + fn display_result_check_leaf_has_the_borrowed_status_abi() { + assert_eq!( + NativeLeaf::DisplayResultCheck.symbol(), + "jett_rt_v1_display_result_check" + ); + assert_eq!( + NativeLeaf::DisplayResultCheck.parameters(), + &[AbiScalar::Pointer, AbiScalar::I64] + ); + assert_eq!(NativeLeaf::DisplayResultCheck.result(), AbiScalar::I32); + } + + #[test] + fn display_result_check_leaf_preserves_ownership_status_and_first_failure() { + for text in ["", "shown", "é🦀"] { + for (selected, rendered) in [ + (0, None), + (1, Some(format!("pending({text})"))), + (2, Some(format!("pending(pending({text}))"))), + (3, Some(format!("pending({text})"))), + (4, None), + ] { + let context = Context::new(); + unsafe { + let ready = context.text(text); + let pending = jett_rt_v1_string_run(context.pointer(), ready); + let nested = jett_rt_v1_string_run(context.pointer(), pending); + let partial = jett_rt_v1_string_task_join(context.pointer(), nested); + let joined = jett_rt_v1_string_task_join(context.pointer(), partial); + let handles = [ready, pending, nested, partial, joined]; + let selected = handles[selected]; + let alias = jett_rt_v1_string_retain(context.pointer(), selected); + assert_eq!(alias, selected); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let snapshot = { + let state = lock_unpoisoned(&lease.entry.state); + display_result_string_snapshot(&state.as_ref().unwrap().values) + }; + let expected = rendered.map(|rendered| { + format!("Displayable.display returned {rendered} instead of string") + }); + assert_eq!( + jett_rt_v1_display_result_check(context.pointer(), selected), + if expected.is_some() { + u32::failure_default() + } else { + 0 + } + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + if expected.is_some() { + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + } else { + 0 + } + ); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(display_result_string_snapshot(values), snapshot); + assert_eq!( + values.dynamic_failure_message.as_deref(), + expected.as_ref().map(|message| message.as_bytes()) + ); + assert!(!values.cleanup_failed); + } + if let Some(expected) = expected { + // Normal leaves cannot replace an existing terminal error. + assert_eq!( + jett_rt_v1_display_result_check(context.pointer(), 0), + u32::failure_default() + ); + assert_eq!( + jett_rt_v1_assert_fail(context.pointer()), + u32::failure_default() + ); + let mut result = MaybeUninit::uninit(); + let mut length = 0; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + let mut message = vec![0; usize::try_from(length).unwrap()]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + message.as_mut_ptr(), + length, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(message, expected.as_bytes()); + } + for handle in handles.into_iter().chain([alias]) { + assert_eq!(jett_rt_v1_string_release(context.pointer(), handle), 0); + } + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + } + drop(lease); + } + context.destroy(JettRuntimeStatusV1::OK); + } + } + } + + #[test] + fn display_result_check_leaf_rejects_foreign_stale_and_invalid_handles() { + let foreign_context = Context::new(); + let foreign = foreign_context.text("foreign"); + for invalid in [0, u64::MAX, foreign] { + let context = Context::new(); + let ready = context.text("kept"); + unsafe { + assert_eq!( + jett_rt_v1_display_result_check(context.pointer(), invalid), + u32::failure_default() + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.failure, Some(INVALID_HANDLE)); + assert!(values.dynamic_failure_message.is_none()); + assert_eq!(values.text(ready), Ok("kept")); + assert_eq!(values.strings[&ready].references, 1); + } + assert_eq!(jett_rt_v1_string_release(context.pointer(), ready), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + let context = Context::new(); + let stale = context.text("stale"); + unsafe { + assert_eq!(jett_rt_v1_string_release(context.pointer(), stale), 0); + assert_eq!( + jett_rt_v1_display_result_check(context.pointer(), stale), + u32::failure_default() + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!( + jett_rt_v1_string_release(foreign_context.pointer(), foreign), + 0 + ); + assert_eq!( + jett_rt_v1_display_result_check(ptr::null(), 0), + u32::failure_default() + ); + } + context.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn pending_bytes_depth_survives_clone_and_join() { + let mut values = NativeValues::default(); + let plain = values.insert_bytes(vec![65]).unwrap(); + let pending = values.run_bytes(plain).unwrap(); + let nested = values.run_bytes(pending).unwrap(); + let cloned = values.clone_bytes(nested).unwrap(); + assert_eq!(values.bytes_depth(plain), Ok(0)); + assert_eq!(values.bytes_depth(pending), Ok(1)); + assert_eq!(values.bytes_depth(nested), Ok(2)); + assert_eq!(values.bytes_depth(cloned), Ok(2)); + assert!(values.same_bytes_value(nested, cloned).unwrap()); + assert!(!values.same_bytes_value(plain, pending).unwrap()); + assert_eq!( + values.debug_value(cloned, DEBUG_BYTES_KIND).unwrap(), + "pending(pending(bytes(65)))" + ); + let joined = values.join_bytes(cloned).unwrap(); + assert_eq!(values.bytes_depth(joined), Ok(1)); + let last = values.join_bytes(joined).unwrap(); + assert_eq!(values.bytes_depth(last), Ok(0)); + for handle in [plain, pending, nested, cloned, joined, last] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn nothing_debug_and_aggregate_equality_preserve_pending_depth() { + let mut values = NativeValues::default(); + let layout = NativeDebugLayout { + root: 1, + nodes: vec![NativeDebugNode::Nothing, NativeDebugNode::List(0)], + }; + for (depth, expected) in [(0, "nothing"), (2, "pending(pending(nothing))")] { + assert_eq!( + values.debug_value(depth, DEBUG_NOTHING_KIND), + Ok(expected.into()) + ); + assert_eq!(layout.format_value(&values, depth, 0), Ok(expected.into())); + } + let left = values.new_list(false).unwrap(); + let right = values.new_list(false).unwrap(); + values.lists.get_mut(&left).unwrap().elements = vec![Some(0), Some(2)]; + values.lists.get_mut(&right).unwrap().elements = vec![Some(0), Some(2)]; + assert_eq!( + layout.format_value(&values, left, layout.root), + Ok("list(nothing, pending(pending(nothing)))".into()) + ); + assert_eq!( + layout.equal_value(&values, left, right, layout.root), + Ok(true) + ); + values.lists.get_mut(&right).unwrap().elements[1] = Some(1); + assert_eq!( + layout.equal_value(&values, left, right, layout.root), + Ok(false) + ); + values.drop_value(left).unwrap(); + values.drop_value(right).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn nothing_equality_matches_interpreter_failure_text() { + let plain = Context::new(); + unsafe { + assert_eq!(jett_rt_v1_nothing_equal(plain.pointer(), 0, 0, 0), 1); + assert_eq!(jett_rt_v1_nothing_equal(plain.pointer(), 0, 0, 1), 0); + assert_eq!(jett_rt_v1_value_status(plain.pointer()), 0); + } + for (left, right, not_equal, expected) in [ + ( + 1, + 0, + 0, + "unsupported binary operation: pending(nothing) Eq nothing", + ), + ( + 0, + 2, + 1, + "unsupported binary operation: nothing NotEq pending(pending(nothing))", + ), + ( + 1, + 1, + 0, + "unsupported binary operation: pending(nothing) Eq pending(nothing)", + ), + ( + 2, + 1, + 1, + "unsupported binary operation: pending(pending(nothing)) NotEq pending(nothing)", + ), + ] { + let context = Context::new(); + let pointer = context.pointer(); + unsafe { + assert_eq!( + jett_rt_v1_nothing_equal(pointer, left, right, not_equal), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + jett_rt_v1_value_status(pointer), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + let mut message = vec![0; expected.len()]; + let mut length = 0; + let mut result = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure_copy( + pointer, + message.as_mut_ptr(), + message.len() as u64, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(length, expected.len() as u64); + assert_eq!(message, expected.as_bytes()); + } + } + } + + #[test] + fn nothing_depth_overflow_and_invalid_comparison_fail_cleanly() { + let overflow = Context::new(); + let formatting = Context::new(); + let invalid = Context::new(); + unsafe { + assert_eq!(jett_rt_v1_nothing_run(overflow.pointer(), u64::MAX), 0); + assert_eq!( + jett_rt_v1_value_status(overflow.pointer()), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + assert_eq!(jett_rt_v1_nothing_format(formatting.pointer(), u64::MAX), 0); + assert_eq!( + jett_rt_v1_value_status(formatting.pointer()), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + assert_eq!( + jett_rt_v1_nothing_equal(invalid.pointer(), 0, 0, 2), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + jett_rt_v1_value_status(invalid.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + } + } + + #[test] + fn nothing_join_and_format_roll_back_each_allocation_boundary() { + for (depth, allocations) in [(0, 2), (2, 1)] { + for budget in 0..=allocations { + let mut values = NativeValues::default(); + values.allocation_budget = Some(budget); + match values.join_nothing(depth) { + Ok(sum) => { + assert_eq!(budget, allocations); + values.drop_value(sum).unwrap(); + } + Err(error) => assert_eq!(error, EXHAUSTED), + } + assert!( + values.is_empty(), + "nothing join leaked with budget {budget}" + ); + } + } + for budget in 0..=1 { + let context = Context::new(); + let pointer = context.pointer(); + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + lock_unpoisoned(&lease.entry.state) + .as_mut() + .unwrap() + .values + .allocation_budget = Some(budget); + unsafe { + let formatted = jett_rt_v1_nothing_format(pointer, 2); + if budget == 0 { + assert_eq!(formatted, 0); + assert_eq!( + jett_rt_v1_value_status(pointer), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + } else { + assert_ne!(formatted, 0); + assert_eq!(jett_rt_v1_value_drop(pointer, formatted), 0); + } + } + assert!( + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .is_empty() + ); + } + } + + #[test] + fn graphics_script_configuration_is_context_local_and_precedes_grant() { + let scripted = Context::new(); + let production = Context::new(); + let script = graphics::encode_test_script(&[ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ]); + unsafe { + assert_eq!( + jett_rt_v1_graphics_configure_scripted( + scripted.pointer(), + script.as_ptr(), + script.len() as u64, + ), + 0 + ); + let scripted_token = jett_rt_v1_grant_graphics(scripted.pointer()); + assert_ne!(scripted_token, 0); + assert_eq!( + jett_rt_v1_grant_graphics(scripted.pointer()), + scripted_token + ); + assert_ne!( + jett_rt_v1_grant_graphics(production.pointer()), + scripted_token + ); + } + let lease = acquire_context(context_key(scripted.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state + .as_ref() + .unwrap() + .values + .graphics_script + .as_ref() + .unwrap() + .len(), + 2 + ); + drop(state); + drop(lease); + scripted.destroy(JettRuntimeStatusV1::OK); + production.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn scripted_graphics_session_validates_presents_keys_and_closes() { + let context = Context::new(); + let pointer = context.pointer(); + let script = graphics::encode_test_script(&[ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ]); + unsafe { + assert_eq!( + jett_rt_v1_graphics_configure_scripted( + pointer, + script.as_ptr(), + script.len() as u64 + ), + 0 + ); + let authority = jett_rt_v1_grant_graphics(pointer); + let title = context.text("headless"); + let config = jett_rt_v1_struct_new(pointer, 3); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 0, title, 1), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 1, 16, 0), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 2, 16, 0), 0); + + let color = jett_rt_v1_struct_new(pointer, 3); + for field in 0..3 { + assert_eq!(jett_rt_v1_struct_init(pointer, color, field, 0, 0), 0); + } + let rectangles = jett_rt_v1_list_new(pointer, 1); + let texts = jett_rt_v1_list_new(pointer, 1); + let scene = jett_rt_v1_struct_new(pointer, 3); + assert_eq!(jett_rt_v1_struct_init(pointer, scene, 0, color, 1), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, scene, 1, rectangles, 1), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, scene, 2, texts, 1), 0); + + let validated = jett_rt_v1_graphics_validate_scene(pointer, 16, 16, scene); + assert_eq!(jett_rt_v1_sum_tag(pointer, validated), SUM_SUCCESS); + assert_eq!(jett_rt_v1_value_drop(pointer, validated), 0); + let opened = jett_rt_v1_graphics_open(pointer, authority, config); + assert_eq!(jett_rt_v1_sum_tag(pointer, opened), SUM_SUCCESS); + let session = jett_rt_v1_sum_take(pointer, opened, SUM_SUCCESS); + let presented = jett_rt_v1_graphics_present(pointer, session, scene); + assert_eq!(jett_rt_v1_sum_tag(pointer, presented), SUM_SUCCESS); + assert_eq!(jett_rt_v1_value_drop(pointer, presented), 0); + + let first = jett_rt_v1_graphics_next_key(pointer, session); + assert_eq!(jett_rt_v1_sum_tag(pointer, first), SUM_SUCCESS); + let key = jett_rt_v1_sum_take(pointer, first, SUM_SUCCESS); + assert_eq!( + jett_rt_v1_sum_take(pointer, key, SUM_SUCCESS), + graphics::Key::Right.variant_index() + ); + let second = jett_rt_v1_graphics_next_key(pointer, session); + assert_eq!(jett_rt_v1_sum_tag(pointer, second), SUM_SUCCESS); + let close = jett_rt_v1_sum_take(pointer, second, SUM_SUCCESS); + assert_eq!(jett_rt_v1_sum_tag(pointer, close), SUM_FAILURE); + assert_eq!(jett_rt_v1_value_drop(pointer, close), 0); + assert_eq!(jett_rt_v1_graphics_close(pointer, session), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, scene), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, config), 0); + assert_eq!(jett_rt_v1_value_status(pointer), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn context_destruction_closes_an_unfinished_scripted_graphics_session() { + let context = Context::new(); + let pointer = context.pointer(); + let script = graphics::encode_test_script(&[graphics::TestEvent::Close]); + unsafe { + assert_eq!( + jett_rt_v1_graphics_configure_scripted( + pointer, + script.as_ptr(), + script.len() as u64 + ), + 0 + ); + let authority = jett_rt_v1_grant_graphics(pointer); + let title = context.text("cleanup"); + let config = jett_rt_v1_struct_new(pointer, 3); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 0, title, 1), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 1, 8, 0), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 2, 8, 0), 0); + let opened = jett_rt_v1_graphics_open(pointer, authority, config); + assert_eq!(jett_rt_v1_sum_tag(pointer, opened), SUM_SUCCESS); + assert_ne!(jett_rt_v1_sum_take(pointer, opened, SUM_SUCCESS), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, config), 0); + } + assert!(GRAPHICS_SESSION.with(|slot| slot.borrow().is_some())); + context.destroy(JettRuntimeStatusV1::OK); + assert!(GRAPHICS_SESSION.with(|slot| slot.borrow().is_none())); + } + + #[test] + fn invalid_graphics_config_is_a_domain_result_without_opening_a_session() { + let context = Context::new(); + let pointer = context.pointer(); + let script = graphics::encode_test_script(&[graphics::TestEvent::Close]); + unsafe { + assert_eq!( + jett_rt_v1_graphics_configure_scripted( + pointer, + script.as_ptr(), + script.len() as u64 + ), + 0 + ); + let authority = jett_rt_v1_grant_graphics(pointer); + let title = context.text("invalid"); + let config = jett_rt_v1_struct_new(pointer, 3); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 0, title, 1), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 1, 0, 0), 0); + assert_eq!(jett_rt_v1_struct_init(pointer, config, 2, 8, 0), 0); + let outcome = jett_rt_v1_graphics_open(pointer, authority, config); + assert_eq!(jett_rt_v1_sum_tag(pointer, outcome), SUM_FAILURE); + assert_eq!(jett_rt_v1_value_status(pointer), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, outcome), 0); + assert_eq!(jett_rt_v1_value_drop(pointer, config), 0); + } + assert!(GRAPHICS_SESSION.with(|slot| slot.borrow().is_none())); + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn strings_release_immediately_and_retain_preserves_aliases() { + let context = Context::new(); + let value = context.text("hé\0llo"); + assert_ne!(value, 0); + assert_eq!(context.count(), 1); + unsafe { + assert_eq!(jett_rt_v1_string_retain(context.pointer(), value), value); + assert_eq!(jett_rt_v1_string_release(context.pointer(), value), 0); + assert_eq!(context.count(), 1); + assert_eq!(jett_rt_v1_string_equal(context.pointer(), value, value), 1); + assert_eq!(jett_rt_v1_string_release(context.pointer(), value), 0); + assert_eq!(context.count(), 0); + assert_eq!(jett_rt_v1_string_release(context.pointer(), 0), 0); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + } + } + #[test] + fn foreign_and_stale_handles_fail_but_cleanup_remains_available() { + let first = Context::new(); + let second = Context::new(); + let a = first.text("first"); + let b = second.text("second"); + unsafe { + assert_eq!(jett_rt_v1_string_retain(second.pointer(), a), 0); + assert_ne!(jett_rt_v1_value_status(second.pointer()), 0); + assert_eq!( + jett_rt_v1_string_literal(second.pointer(), b"no".as_ptr(), 2), + 0 + ); + assert_eq!(jett_rt_v1_string_release(second.pointer(), b), 0); + assert_eq!(second.count(), 0); + assert_eq!(jett_rt_v1_string_release(first.pointer(), a), 0); + assert_eq!(jett_rt_v1_string_retain(first.pointer(), a), 0); + assert_ne!(jett_rt_v1_value_status(first.pointer()), 0); + assert_eq!(first.count(), 0); + assert_ne!(jett_rt_v1_value_status(ptr::null()), 0); + } + } + #[test] + fn stdout_authority_is_context_bound_and_failure_is_first_wins() { + let first = Context::new(); + let second = Context::new(); + let empty = second.text(""); + unsafe { + let token = jett_rt_v1_grant_stdout(first.pointer()); + let other = jett_rt_v1_grant_stdout(second.pointer()); + assert_ne!(token, other); + assert_eq!(jett_rt_v1_string_stdout(second.pointer(), other, empty), 0); + jett_rt_v1_string_stdout(second.pointer(), token, empty); + jett_rt_v1_string_release(second.pointer(), u64::MAX); + let mut failure = MaybeUninit::uninit(); + assert_ne!( + jett_rt_v1_value_failure(second.pointer(), failure.as_mut_ptr()), + JettRuntimeStatusV1::OK + ); + let failure = failure.assume_init(); + assert_eq!( + slice::from_raw_parts(failure.message.data, failure.message.byte_length as usize), + b"invalid Stdout authority" + ); + assert_eq!(jett_rt_v1_string_release(second.pointer(), empty), 0); + } + second.destroy(JettRuntimeStatusV1::INVALID_ARGUMENT); + } + #[test] + fn native_assert_failure_preserves_default_message() { + let context = Context::new(); + unsafe { + assert_ne!(jett_rt_v1_assert_fail(context.pointer()), 0); + let mut failure = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure(context.pointer(), failure.as_mut_ptr()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let failure = failure.assume_init(); + assert_eq!( + slice::from_raw_parts(failure.message.data, failure.message.byte_length as usize), + b"assertion failed" + ); + let mut length = 0_u64; + let mut result = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(length, b"assertion failed".len() as u64); + let mut copied = vec![0; length as usize]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + copied.as_mut_ptr(), + length, + &mut length, + result.as_mut_ptr(), + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(copied, b"assertion failed"); + } + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn failed_predicate_text_can_be_captured_and_execution_can_resume() { + let context = Context::new(); + let prefix = b"constraint: "; + unsafe { + assert_ne!(jett_rt_v1_assert_fail(context.pointer()), 0); + let captured = jett_rt_v1_failure_take_prefixed_text( + context.pointer(), + prefix.as_ptr(), + prefix.len() as u64, + ); + assert_ne!(captured, 0); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + let expected = context.text("constraint: assertion failed"); + assert_eq!( + jett_rt_v1_string_equal(context.pointer(), captured, expected), + 1 + ); + assert_eq!(jett_rt_v1_string_release(context.pointer(), captured), 0); + assert_eq!(jett_rt_v1_string_release(context.pointer(), expected), 0); + assert_eq!(context.count(), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn native_property_context_decorates_only_terminal_failure_copies() { + for clear in [false, true] { + let context = Context::new(); + let label = context.text("property 'target' trial 4: "); + unsafe { + assert_eq!(jett_rt_v1_property_case_set(context.pointer(), label), 0); + assert_eq!(jett_rt_v1_string_release(context.pointer(), label), 0); + // Handled refinement failures retain their original message, and + // a successful copy query has no diagnostic merely for being in a trial. + assert_ne!(jett_rt_v1_assert_fail(context.pointer()), 0); + let captured = + jett_rt_v1_failure_take_prefixed_text(context.pointer(), ptr::null(), 0); + let expected = context.text("assertion failed"); + assert_eq!( + jett_rt_v1_string_equal(context.pointer(), captured, expected), + 1 + ); + assert_eq!(jett_rt_v1_string_release(context.pointer(), captured), 0); + assert_eq!(jett_rt_v1_string_release(context.pointer(), expected), 0); + let mut length = 99; + let mut result = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(length, 0); + if clear { + assert_eq!(jett_rt_v1_property_case_clear(context.pointer()), 0); + } + let message = context.text("backend 🧪"); + assert_ne!( + jett_rt_v1_assert_fail_message(context.pointer(), message), + 0 + ); + assert_eq!(jett_rt_v1_string_release(context.pointer(), message), 0); + let expected = if clear { + "backend 🧪" + } else { + "property 'target' trial 4: backend 🧪" + }; + for _ in 0..2 { + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(length, expected.len() as u64); + let mut short = [0x7f; 2]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + short.as_mut_ptr(), + 2, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + assert_eq!(short, [0x7f; 2]); + let mut copied = vec![0; length as usize]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + copied.as_mut_ptr(), + length, + &mut length, + result.as_mut_ptr() + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(copied, expected.as_bytes()); + } + assert_eq!(context.count(), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + } + #[test] + fn native_assert_failure_copies_dynamic_message_without_changing_v1_static_result() { + let context = Context::new(); + let message = "expected 42, got 🧪"; + let handle = context.text(message); + unsafe { + assert_ne!(jett_rt_v1_assert_fail_message(context.pointer(), handle), 0); + let mut failure = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure(context.pointer(), failure.as_mut_ptr()), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let failure = failure.assume_init(); + assert_eq!( + slice::from_raw_parts(failure.message.data, failure.message.byte_length as usize), + b"assertion failed" + ); + + let mut length = 0_u64; + let mut result = MaybeUninit::uninit(); + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + ptr::null_mut(), + 0, + &mut length, + result.as_mut_ptr(), + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(length, message.len() as u64); + let mut too_small = [0_u8; 2]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + too_small.as_mut_ptr(), + too_small.len() as u64, + &mut length, + result.as_mut_ptr(), + ), + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let mut copied = vec![0; message.len()]; + assert_eq!( + jett_rt_v1_value_failure_copy( + context.pointer(), + copied.as_mut_ptr(), + copied.len() as u64, + &mut length, + result.as_mut_ptr(), + ), + JettRuntimeStatusV1::OK + ); + assert_eq!(copied, message.as_bytes()); + assert_eq!(jett_rt_v1_string_release(context.pointer(), handle), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn clock_authority_is_context_bound_and_samples_wall_time() { + let first = Context::new(); + let second = Context::new(); + unsafe { + let token = jett_rt_v1_grant_clock(first.pointer()); + let other = jett_rt_v1_grant_clock(second.pointer()); + assert_ne!(token, other); + assert_eq!(token, jett_rt_v1_grant_clock(first.pointer())); + let before = clock::production_wall_clock_sample(); + let now = jett_rt_v1_clock_now(first.pointer(), token); + let after = clock::production_wall_clock_sample(); + let before = clock::checked_clock_milliseconds(before.0, before.1).unwrap(); + let after = clock::checked_clock_milliseconds(after.0, after.1).unwrap(); + assert!((before..=after).contains(&now)); + assert_eq!(jett_rt_v1_value_status(first.pointer()), 0); + assert_eq!(jett_rt_v1_clock_now(second.pointer(), token), 0); + let lease = acquire_context(context_key(second.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state.as_ref().unwrap().values.failure, + Some(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid Clock authority".as_slice() + )) + ); + } + } + #[test] + fn successful_native_entry_rejects_unconsumed_scripts_in_interpreter_order() { + let context = Context::new(); + unsafe { + assert_eq!( + jett_rt_v1_validate_scripted_consumption(context.pointer()), + 0 + ); + let random_script = random::encode_test_script(&[ + random::RandomTestSample::Boolean(true), + random::RandomTestSample::Boolean(false), + ]); + assert_eq!( + jett_rt_v1_random_configure_scripted( + context.pointer(), + random_script.as_ptr(), + random_script.len() as u64, + ), + 0 + ); + let clock_script = clock::encode_test_script(&[clock::ClockTestSample::Unavailable]); + assert_eq!( + jett_rt_v1_clock_configure_scripted( + context.pointer(), + clock_script.as_ptr(), + clock_script.len() as u64, + ), + 0 + ); + let graphics_script = graphics::encode_test_script(&[graphics::TestEvent::Close]); + assert_eq!( + jett_rt_v1_graphics_configure_scripted( + context.pointer(), + graphics_script.as_ptr(), + graphics_script.len() as u64, + ), + 0 + ); + assert_ne!( + jett_rt_v1_validate_scripted_consumption(context.pointer()), + 0 + ); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state + .as_ref() + .unwrap() + .values + .dynamic_failure_message + .as_deref(), + Some(b"Random: test provider has 2 unconsumed samples".as_slice()) + ); + } + } + + #[test] + fn scripted_clock_samples_are_context_bound_and_exhaustion_is_terminal() { + let first = Context::new(); + let second = Context::new(); + let samples = [ + clock::ClockTestSample::Wall { + unix_seconds: -1, + subsecond_nanoseconds: 999_999_999, + }, + clock::ClockTestSample::Wall { + unix_seconds: 42, + subsecond_nanoseconds: 123_456_789, + }, + ]; + let script = clock::encode_test_script(&samples); + unsafe { + assert_eq!( + jett_rt_v1_clock_configure_scripted( + first.pointer(), + script.as_ptr(), + script.len() as u64 + ), + 0 + ); + let token = jett_rt_v1_grant_clock(first.pointer()); + let foreign = jett_rt_v1_grant_clock(second.pointer()); + assert_eq!(jett_rt_v1_clock_now(first.pointer(), token), -1); + assert_eq!(jett_rt_v1_clock_now(first.pointer(), token), 42_123); + assert_eq!(jett_rt_v1_clock_now(first.pointer(), token), 0); + assert_ne!(jett_rt_v1_value_status(first.pointer()), 0); + let _ = jett_rt_v1_clock_now(second.pointer(), foreign); + assert_eq!(jett_rt_v1_value_status(second.pointer()), 0); + let lease = acquire_context(context_key(first.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state.as_ref().unwrap().values.failure, + Some(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"Clock.now: test clock exhausted".as_slice() + )) + ); + } + } + #[test] + fn scripted_random_samples_match_scalar_kernels_and_exhaustion() { + let context = Context::new(); + let samples = [ + random::RandomTestSample::Bounded(u64::MAX - 1), + random::RandomTestSample::Unit53((1_u64 << 53) - 1), + random::RandomTestSample::Boolean(true), + ]; + let script = random::encode_test_script(&samples); + unsafe { + assert_eq!( + jett_rt_v1_random_configure_scripted( + context.pointer(), + script.as_ptr(), + script.len() as u64, + ), + 0 + ); + let token = jett_rt_v1_grant_random(context.pointer()); + assert_ne!(token, 0); + assert_eq!( + jett_rt_v1_random_bounded(context.pointer(), token, i64::MIN, i64::MAX), + i64::MAX - 1 + ); + assert_eq!( + jett_rt_v1_random_unit53(context.pointer(), token), + 0.9999999999999999 + ); + assert_eq!(jett_rt_v1_random_bool(context.pointer(), token), 1); + assert_eq!( + jett_rt_v1_random_bool(context.pointer(), token), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state.as_ref().unwrap().values.failure, + Some(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + random::TEST_EXHAUSTED.as_bytes() + )) + ); + } + } + #[test] + fn context_destruction_reports_owned_value_leaks() { + let mut context = Context::new(); + context.text("unreleased"); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context.0, result.as_mut_ptr()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + // The context has already retired; drop its storage without a second destroy. + let context = std::mem::ManuallyDrop::new(context); + unsafe { + drop(ptr::read(&context.0)); + } + } + #[test] + fn panic_becomes_terminal_failure_and_releases_existing_values() { + let context = Context::new(); + let value = context.text("held"); + let result: u64 = leaf(context.pointer(), false, |_| panic!("injected leaf panic")); + assert_eq!(result, 0); + unsafe { + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::PANIC.code() + ); + jett_rt_v1_string_release(context.pointer(), value); + } + assert_eq!(context.count(), 0); + } + #[test] + fn bytes_have_distinct_storage_and_exact_destruction_counts() { + let context = Context::new(); + let text = context.text("raw"); + unsafe { + let a = jett_rt_v1_bytes_from_string(context.pointer(), text); + let b = jett_rt_v1_bytes_clone(context.pointer(), a); + assert_ne!(a, b); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let mut state = lock_unpoisoned(&lease.entry.state); + let values = &mut state.as_mut().unwrap().values; + values.bytes.get_mut(&a).unwrap()[0] = 255; + assert_eq!(values.bytes(a).unwrap(), &[255, 97, 119]); + assert_eq!(values.bytes(b).unwrap(), b"raw"); + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 0)); + } + jett_rt_v1_value_drop(context.pointer(), a); + jett_rt_v1_value_drop(context.pointer(), b); + jett_rt_v1_value_drop(context.pointer(), text); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 2)); + assert!(values.is_empty()); + } + } + #[test] + fn bytes_registry_leak_is_not_hidden_by_empty_string_registry() { + let mut context = Context::new(); + unsafe { + jett_rt_v1_bytes_new(context.pointer()); + } + assert_eq!(context.count(), 0); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context.0, result.as_mut_ptr()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let context = std::mem::ManuallyDrop::new(context); + unsafe { + drop(ptr::read(&context.0)); + } + } + #[test] + fn sum_tags_take_only_selected_payload_and_cleanup_nested_owners() { + let context = Context::new(); + unsafe { + let data = jett_rt_v1_bytes_new(context.pointer()); + let inner = jett_rt_v1_sum_new(context.pointer(), SUM_SUCCESS, data, 1); + let outer = jett_rt_v1_sum_new(context.pointer(), SUM_SUCCESS, inner, 1); + let clone = jett_rt_v1_sum_clone(context.pointer(), outer); + assert_ne!(clone, outer); + assert_eq!(jett_rt_v1_sum_tag(context.pointer(), outer), SUM_SUCCESS); + let taken = jett_rt_v1_sum_take(context.pointer(), outer, SUM_SUCCESS); + assert_eq!(taken, inner); + jett_rt_v1_value_drop(context.pointer(), taken); + // Invalid tag extraction must not consume the still-owned record. + assert_eq!( + jett_rt_v1_sum_take(context.pointer(), clone, SUM_FAILURE), + 0 + ); + assert_ne!(jett_rt_v1_value_status(context.pointer()), 0); + jett_rt_v1_value_drop(context.pointer(), clone); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 2)); + assert_eq!((values.sums_created, values.sums_destroyed), (4, 4)); + assert!(values.is_empty()); + } + } + #[test] + fn sum_handles_reject_outer_pending_depth_and_preserve_owned_payloads() { + let mut values = NativeValues::default(); + let mut primitive = vec![NativeDebugTag::Primitive as u8]; + primitive.extend_from_slice(&(NativeSortKind::String as u32).to_le_bytes()); + let mut optional = vec![NativeDebugTag::Optional as u8]; + optional.extend_from_slice(&1u32.to_le_bytes()); + let layout = function_debug_layout_bytes(&[optional, primitive], 0); + let text = values.insert("payload".into()).unwrap(); + let ready = values.sum(SUM_SUCCESS, text, true).unwrap(); + let pending = values.run_sum(ready).unwrap(); + let twice = values.run_sum(pending).unwrap(); + assert_eq!(values.sum_handle_tag(ready, &layout), Ok(SUM_SUCCESS)); + assert!(values.sum_handle_tag(twice, &layout).is_err()); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(b"handle block requires a result or optional value, got pending(pending(some(payload)))".as_slice()) + ); + assert!(values.sums.contains_key(&twice)); + let malformed = values.sum_handle_tag(twice, b"invalid"); + assert!(malformed.is_err()); + let inner_pending = values.insert("inner".into()).unwrap(); + values + .strings + .get_mut(&inner_pending) + .unwrap() + .pending_depth = 1; + let outer_ready = values.sum(SUM_SUCCESS, inner_pending, true).unwrap(); + assert_eq!(values.sum_handle_tag(outer_ready, &layout), Ok(SUM_SUCCESS)); + for value in [ready, pending, twice, outer_ready] { + values.drop_value(value).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn sum_registry_leak_is_independent_of_payload_ownership() { + let mut context = Context::new(); + unsafe { + jett_rt_v1_sum_new(context.pointer(), SUM_SUCCESS, 42, 0); + } + assert_eq!(context.count(), 0); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context.0, result.as_mut_ptr()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let context = std::mem::ManuallyDrop::new(context); + unsafe { + drop(ptr::read(&context.0)); + } + } + + #[test] + fn struct_pending_access_preserves_ready_and_pending_owned_records() { + let mut values = NativeValues::default(); + let bytes = values.insert_bytes(vec![65]).unwrap(); + values.bytes_pending.insert(bytes, 2); + let ready = values.new_struct(2).unwrap(); + values.structs.get_mut(&ready).unwrap().fields = vec![ + Some(NativeField { + bits: bytes, + owned: true, + pending_depth: 0, + }), + Some(NativeField { + bits: 7, + owned: false, + pending_depth: 3, + }), + ]; + let pending = values.run_record(ready).unwrap(); + let twice = values.run_record(pending).unwrap(); + assert_eq!( + values.check_struct_pending_access(ready, b"invalid", "field access: ", true), + Ok(0) + ); + assert_eq!( + values.check_struct_pending_access(twice, b"invalid", "transition rejected", false), + Err(INVALID_STRUCT) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(b"transition rejected".as_slice()) + ); + for (owner, depth) in [(ready, 0), (pending, 1), (twice, 2)] { + let record = values.structs.get(&owner).unwrap(); + assert_eq!(record.pending_depth, depth); + assert!(record.fields.iter().all(Option::is_some)); + let bytes = values.struct_field(owner, 0).unwrap(); + assert!(bytes.owned); + assert_eq!(values.bytes(bytes.bits), Ok([65].as_slice())); + assert_eq!(values.bytes_depth(bytes.bits), Ok(2)); + let scalar = values.struct_field(owner, 1).unwrap(); + assert_eq!( + (scalar.bits, scalar.owned, scalar.pending_depth), + (7, false, 3) + ); + } + assert_eq!((values.structs_created, values.structs_destroyed), (3, 0)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (3, 0)); + for owner in [ready, pending, twice] { + values.drop_value(owner).unwrap(); + } + assert_eq!((values.structs_created, values.structs_destroyed), (3, 3)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (3, 3)); + assert!(values.is_empty()); + } + + #[test] + fn struct_pending_access_typed_errors_preserve_depth_and_redaction() { + let mut integer = vec![NativeDebugTag::Primitive as u8]; + integer.extend_from_slice(&(NativeSortKind::Int64 as u32).to_le_bytes()); + let mut string = vec![NativeDebugTag::Primitive as u8]; + string.extend_from_slice(&(NativeSortKind::String as u32).to_le_bytes()); + let mut record = vec![NativeDebugTag::Record as u8]; + record.extend_from_slice(&5_u32.to_le_bytes()); + record.extend_from_slice(b"Vault"); + record.extend_from_slice(&3_u32.to_le_bytes()); + for (name, node) in [("number", 0_u32), ("hidden", 1), ("label", 2)] { + record.extend_from_slice(&u32::try_from(name.len()).unwrap().to_le_bytes()); + record.extend_from_slice(name.as_bytes()); + record.extend_from_slice(&node.to_le_bytes()); + } + let layout = function_debug_layout_bytes( + &[ + integer, + vec![NativeDebugTag::Redacted as u8], + string, + record, + ], + 3, + ); + let mut values = NativeValues::default(); + let label = values.insert("[redacted]".to_owned()).unwrap(); + values.strings.get_mut(&label).unwrap().pending_depth = 1; + let owner = values.new_struct(3).unwrap(); + let record = values.structs.get_mut(&owner).unwrap(); + record.pending_depth = 2; + record.fields = vec![ + Some(NativeField { + bits: 7, + owned: false, + pending_depth: 1, + }), + Some(NativeField { + bits: u64::MAX, + owned: false, + pending_depth: u64::MAX, + }), + Some(NativeField { + bits: label, + owned: true, + pending_depth: 0, + }), + ]; + assert_eq!( + values.check_struct_pending_access( + owner, + &layout, + "field access is not supported on ", + true, + ), + Err(INVALID_STRUCT) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(b"field access is not supported on pending(pending(Vault(number: pending(7), hidden: [redacted], label: pending([redacted]))))".as_slice()) + ); + assert_eq!(values.struct_field(owner, 1).unwrap().bits, u64::MAX); + assert_eq!(values.structs[&owner].pending_depth, 2); + assert_eq!(values.strings[&label].pending_depth, 1); + values.drop_value(owner).unwrap(); + assert_eq!((values.structs_created, values.structs_destroyed), (1, 1)); + assert!(values.is_empty()); + } + + #[test] + fn struct_pending_access_formats_only_requested_pending_values() { + let mut values = NativeValues::default(); + let owner = values.new_struct(1).unwrap(); + // Ready access does not inspect even an uninitialized opaque field. + assert_eq!( + values.check_struct_pending_access(owner, b"invalid", "got ", true), + Ok(0) + ); + values.structs.get_mut(&owner).unwrap().pending_depth = 1; + assert_eq!( + values.check_struct_pending_access(owner, b"invalid", "transition rejected", false), + Err(INVALID_STRUCT) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(b"transition rejected".as_slice()) + ); + assert_eq!( + values.check_struct_pending_access(owner, &[], "got ", true), + Err(INVALID_STRUCT) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(b"got [redacted]".as_slice()) + ); + values.dynamic_failure_message = None; + assert_eq!( + values.check_struct_pending_access(owner, b"invalid", "got ", true), + Err(INVALID_STRUCT) + ); + assert!(values.dynamic_failure_message.is_none()); + assert!(values.structs[&owner].fields[0].is_none()); + assert_eq!( + values.check_struct_pending_access(0, &[], "got ", false), + Err(INVALID_STRUCT) + ); + values.drop_value(owner).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn struct_pending_access_leaf_validates_abi_without_consuming_owner() { + let layout = b"invalid"; + let message = b"got "; + let invalid_utf8 = [0xff]; + for (layout, layout_length, message, message_length, render) in [ + (layout.as_ptr(), 7, message.as_ptr(), 4, 2), + (ptr::null(), 1, message.as_ptr(), 4, 1), + (ptr::null(), 0, ptr::null(), 1, 0), + (layout.as_ptr(), u64::MAX, message.as_ptr(), 4, 1), + (ptr::null(), 0, message.as_ptr(), u64::MAX, 0), + (ptr::null(), 0, invalid_utf8.as_ptr(), 1, 0), + ] { + let context = Context::new(); + unsafe { + let owner = jett_rt_v1_struct_new(context.pointer(), 1); + assert_eq!( + jett_rt_v1_struct_init_scalar_task(context.pointer(), owner, 0, 7, 3), + 0 + ); + assert_eq!( + jett_rt_v1_struct_pending_access_check( + context.pointer(), + owner, + layout, + layout_length, + message, + message_length, + render, + ), + u32::failure_default() + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.structs[&owner].pending_depth, 0); + let field = values.struct_field(owner, 0).unwrap(); + assert_eq!( + (field.bits, field.owned, field.pending_depth), + (7, false, 3) + ); + drop(state); + jett_rt_v1_value_drop(context.pointer(), owner); + } + } + let context = Context::new(); + unsafe { + let owner = jett_rt_v1_struct_new(context.pointer(), 0); + assert_eq!( + jett_rt_v1_struct_pending_access_check( + context.pointer(), + owner, + ptr::null(), + 0, + ptr::null(), + 0, + 1, + ), + 0 + ); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + jett_rt_v1_value_drop(context.pointer(), owner); + } + } + #[test] + fn foreign_and_stale_bytes_cannot_be_used_or_double_dropped() { + let first = Context::new(); + let second = Context::new(); + unsafe { + let value = jett_rt_v1_bytes_new(first.pointer()); + assert_eq!(jett_rt_v1_bytes_clone(second.pointer(), value), 0); + assert_ne!(jett_rt_v1_value_status(second.pointer()), 0); + jett_rt_v1_value_drop(first.pointer(), value); + assert_eq!(jett_rt_v1_bytes_clone(first.pointer(), value), 0); + jett_rt_v1_value_drop(first.pointer(), value); + assert_ne!(jett_rt_v1_value_status(first.pointer()), 0); + } + first.destroy(JettRuntimeStatusV1::INVALID_ARGUMENT); + } + #[test] + fn lists_transfer_and_clone_nested_payloads_with_exact_destruction() { + let context = Context::new(); + unsafe { + let payload = jett_rt_v1_bytes_new(context.pointer()); + let list = jett_rt_v1_list_new(context.pointer(), 1); + assert_eq!( + jett_rt_v1_list_append(context.pointer(), list, payload), + list + ); + let copy = jett_rt_v1_list_clone(context.pointer(), list); + let item = jett_rt_v1_list_get_clone(context.pointer(), copy, 0); + let taken = jett_rt_v1_sum_take(context.pointer(), item, SUM_SUCCESS); + assert_ne!(taken, payload); + jett_rt_v1_value_drop(context.pointer(), taken); + jett_rt_v1_value_drop(context.pointer(), list); + jett_rt_v1_value_drop(context.pointer(), copy); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (3, 3)); + assert_eq!((values.lists_created, values.lists_destroyed), (2, 2)); + assert!(values.is_empty()); + } + } + #[test] + fn list_insert_remove_transfers_owned_elements_and_rejects_invalid_indices() { + let context = Context::new(); + unsafe { + let first = jett_rt_v1_bytes_new(context.pointer()); + let second = jett_rt_v1_bytes_new(context.pointer()); + let list = jett_rt_v1_list_new(context.pointer(), 1); + assert_eq!( + jett_rt_v1_list_insert_at(context.pointer(), list, 0, first), + list + ); + assert_eq!( + jett_rt_v1_list_insert_at(context.pointer(), list, 1, second), + list + ); + assert_eq!(jett_rt_v1_list_remove_at(context.pointer(), list, 0), list); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.lists[&list].elements, vec![Some(second)]); + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 1)); + drop(state); + jett_rt_v1_value_drop(context.pointer(), list); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 2)); + assert!(values.is_empty()); + } + + for (insert, index, message) in [ + (true, 2, "list.__insert_at: index 2 out of bounds"), + (true, -1, "list.__insert_at: index -1 out of bounds"), + (false, -1, "list.__remove_at: index -1 out of bounds"), + (false, 1, "list.__remove_at: index 1 out of bounds"), + ] { + let context = Context::new(); + unsafe { + let list = jett_rt_v1_list_new(context.pointer(), 0); + assert_eq!(jett_rt_v1_list_append(context.pointer(), list, 7), list); + let result = if insert { + jett_rt_v1_list_insert_at(context.pointer(), list, index, 9) + } else { + jett_rt_v1_list_remove_at(context.pointer(), list, index) + }; + assert_eq!(result, 0); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.lists[&list].elements, vec![Some(7)]); + assert_eq!( + values.failure.map(|failure| failure.0), + Some(JettRuntimeStatusV1::INVALID_ARGUMENT) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(message.as_bytes()) + ); + drop(state); + jett_rt_v1_value_drop(context.pointer(), list); + } + context.destroy(JettRuntimeStatusV1::OK); + } + } + #[test] + fn list_only_leak_fails_context_destruction() { + let context = Context::new(); + unsafe { + jett_rt_v1_list_new(context.pointer(), 0); + } + context.destroy(JettRuntimeStatusV1::INVALID_ARGUMENT); + } + #[test] + fn primitive_list_sum_wraps_widths_rounds_floats_and_keeps_empty_zero() { + let cases = [ + (NativeSortKind::Int8, [127, 1], 128), + (NativeSortKind::Int16, [32767, 1], 32768), + (NativeSortKind::Int32, [i32::MAX as u64, 1], 1_u64 << 31), + (NativeSortKind::Int64, [i64::MAX as u64, 1], 1_u64 << 63), + (NativeSortKind::Uint8, [255, 2], 1), + (NativeSortKind::Uint16, [65535, 2], 1), + (NativeSortKind::Uint32, [u32::MAX as u64, 2], 1), + (NativeSortKind::Uint64, [u64::MAX, 2], 1), + ( + NativeSortKind::Float32, + [1.5_f32.to_bits() as u64, 2.25_f32.to_bits() as u64], + 3.75_f32.to_bits() as u64, + ), + ( + NativeSortKind::Float64, + [1.5_f64.to_bits(), 2.25_f64.to_bits()], + 3.75_f64.to_bits(), + ), + ]; + for (kind, input, expected) in cases { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + assert_eq!(values.sum_primitive_list(list, kind as u32), Ok(0)); + values.lists.get_mut(&list).unwrap().elements = input.map(Some).to_vec(); + assert_eq!( + values.sum_primitive_list(list, kind as u32), + Ok(expected), + "{kind:?}" + ); + assert_eq!(values.lists[&list].elements, input.map(Some)); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + for (kind, bits, expected) in [ + ( + NativeSortKind::Float32, + [ + 16777216.0_f32.to_bits() as u64, + 1.0_f32.to_bits() as u64, + (-16777216.0_f32).to_bits() as u64, + ], + 0, + ), + ( + NativeSortKind::Float64, + [ + 16777216.0_f64.to_bits(), + 1.0_f64.to_bits(), + (-16777216.0_f64).to_bits(), + ], + 1.0_f64.to_bits(), + ), + ] { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + values.lists.get_mut(&list).unwrap().elements = bits.map(Some).to_vec(); + assert_eq!(values.sum_primitive_list(list, kind as u32), Ok(expected)); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn primitive_list_sum_rejects_invalid_carriers_without_consuming_owners() { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + for kind in [ + NativeSortKind::Bool as u32, + NativeSortKind::String as u32, + u32::MAX, + ] { + assert_eq!(values.sum_primitive_list(list, kind), Err(INVALID_LIST)); + } + values.lists.get_mut(&list).unwrap().elements = vec![Some(256)]; + assert_eq!( + values.sum_primitive_list(list, NativeSortKind::Int8 as u32), + Err(INVALID_LIST) + ); + values.lists.get_mut(&list).unwrap().elements = vec![None]; + assert_eq!( + values.sum_primitive_list(list, NativeSortKind::Uint64 as u32), + Err(INVALID_LIST) + ); + values.drop_value(list).unwrap(); + let owned = values.new_list(true).unwrap(); + assert_eq!( + values.sum_primitive_list(owned, NativeSortKind::Int64 as u32), + Err(INVALID_LIST) + ); + values.drop_value(owned).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn primitive_list_sum_preserves_ieee_infinity_and_nan() { + for kind in [NativeSortKind::Float32, NativeSortKind::Float64] { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + let (positive, negative) = if kind == NativeSortKind::Float32 { + ( + u64::from(f32::INFINITY.to_bits()), + u64::from(f32::NEG_INFINITY.to_bits()), + ) + } else { + (f64::INFINITY.to_bits(), f64::NEG_INFINITY.to_bits()) + }; + values.lists.get_mut(&list).unwrap().elements = vec![Some(positive)]; + assert_eq!(values.sum_primitive_list(list, kind as u32), Ok(positive)); + values + .lists + .get_mut(&list) + .unwrap() + .elements + .push(Some(negative)); + let nan = values.sum_primitive_list(list, kind as u32).unwrap(); + assert!(if kind == NativeSortKind::Float32 { + f32::from_bits(nan as u32).is_nan() + } else { + f64::from_bits(nan).is_nan() + }); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn primitive_list_sum_retains_pending_errors_without_consuming_input() { + for (kind, bits) in [ + (NativeSortKind::Int8, 1), + (NativeSortKind::Uint64, 1), + (NativeSortKind::Float32, u64::from(1.0_f32.to_bits())), + (NativeSortKind::Float64, 1.0_f64.to_bits()), + ] { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + values.lists.get_mut(&list).unwrap().elements = vec![Some(bits), Some(bits)]; + for (index, message) in [ + ( + 0, + b"list.__sum: list elements must be int64 or float64".as_slice(), + ), + (1, b"list.__sum: mixed types".as_slice()), + ] { + values.lists.get_mut(&list).unwrap().element_pending_depths = + HashMap::from([(index, 1)]); + assert_eq!( + values.sum_primitive_list(list, kind as u32), + Err((JettRuntimeStatusV1::INVALID_ARGUMENT, message)), + "{kind:?}" + ); + assert_eq!(values.lists[&list].elements, [Some(bits), Some(bits)]); + } + values.lists.get_mut(&list).unwrap().pending_depth = 1; + assert_eq!( + values.sum_primitive_list(list, kind as u32), + Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"list.__sum: argument must be a list".as_slice() + )) + ); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn math_aggregates_decode_all_numeric_widths_and_borrow_their_inputs() { + let cases = [ + (NativeSortKind::Int8, [128, 127], [-128.0, 127.0], -0.5), + ( + NativeSortKind::Int16, + [32768, 32767], + [-32768.0, 32767.0], + -0.5, + ), + ( + NativeSortKind::Int32, + [2147483648, 2147483647], + [-2147483648.0, 2147483647.0], + -0.5, + ), + ( + NativeSortKind::Int64, + [1_u64 << 63, (1_u64 << 63) - 1], + [-9223372036854775808.0, 9223372036854775808.0], + 0.0, + ), + (NativeSortKind::Uint8, [0, 255], [0.0, 255.0], 127.5), + (NativeSortKind::Uint16, [0, 65535], [0.0, 65535.0], 32767.5), + ( + NativeSortKind::Uint32, + [0, 4294967295], + [0.0, 4294967295.0], + 2147483647.5, + ), + ( + NativeSortKind::Uint64, + [u64::MAX - 1, u64::MAX], + [18446744073709551616.0; 2], + 18446744073709551616.0, + ), + ( + NativeSortKind::Uint64, + [(1_u64 << 53) + 1, (1_u64 << 53) + 3], + [9007199254740992.0, 9007199254740996.0], + 9007199254740994.0, + ), + ( + NativeSortKind::Float32, + [ + u64::from((-f32::MAX).to_bits()), + u64::from(f32::MAX.to_bits()), + ], + [-f64::from(f32::MAX), f64::from(f32::MAX)], + 0.0, + ), + ( + NativeSortKind::Float32, + [u64::from(0.1_f32.to_bits()); 2], + [f64::from(0.1_f32); 2], + f64::from(0.1_f32), + ), + ( + NativeSortKind::Float32, + [1; 2], + [f64::from(f32::from_bits(1)); 2], + f64::from(f32::from_bits(1)), + ), + ( + NativeSortKind::Float64, + [1.5_f64.to_bits(), 2.5_f64.to_bits()], + [1.5, 2.5], + 2.0, + ), + ( + NativeSortKind::Float64, + [f64::MAX.to_bits(); 2], + [f64::MAX; 2], + f64::MAX, + ), + ]; + for (kind, input, decoded, expected) in cases { + let context = Context::new(); + unsafe { + let list = jett_rt_v1_list_new(context.pointer(), 0); + for bits in input { + assert_eq!(jett_rt_v1_list_append(context.pointer(), list, bits), list); + } + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!( + values.math_numbers( + list, + kind as u32, + INVALID_LIST, + INVALID_LIST, + INVALID_LIST + ), + Ok(decoded.to_vec()) + ); + } + assert_eq!( + jett_rt_v1_math_average(context.pointer(), list, kind as u32).to_bits(), + expected.to_bits(), + "average {kind:?}" + ); + assert_eq!( + jett_rt_v1_math_median(context.pointer(), list, kind as u32).to_bits(), + expected.to_bits(), + "median {kind:?}" + ); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.lists[&list].elements, input.map(Some)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + } + jett_rt_v1_value_drop(context.pointer(), list); + assert!( + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .is_empty() + ); + } + } + } + + #[test] + fn math_aggregate_float_carriers_preserve_nan_and_infinity() { + for kind in [NativeSortKind::Float32, NativeSortKind::Float64] { + for input in [ + [f64::NAN, 1.0], + [f64::INFINITY, f64::INFINITY], + [f64::NEG_INFINITY, f64::NEG_INFINITY], + [f64::INFINITY, f64::NEG_INFINITY], + ] { + let context = Context::new(); + unsafe { + let list = jett_rt_v1_list_new(context.pointer(), 0); + for value in input { + let bits = if kind == NativeSortKind::Float32 { + u64::from((value as f32).to_bits()) + } else { + value.to_bits() + }; + jett_rt_v1_list_append(context.pointer(), list, bits); + } + for result in [ + jett_rt_v1_math_average(context.pointer(), list, kind as u32), + jett_rt_v1_math_median(context.pointer(), list, kind as u32), + ] { + if input[0] == input[1] { + assert_eq!(result, input[0]); + } else { + assert!(result.is_nan()); + } + } + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + jett_rt_v1_value_drop(context.pointer(), list); + } + } + } + } + + #[test] + fn math_aggregates_reject_malformed_or_nonnumeric_carriers_without_consuming_input() { + for (kind, bits) in [ + (NativeSortKind::Int8, 256), + (NativeSortKind::Uint8, 256), + (NativeSortKind::Int16, 65536), + (NativeSortKind::Uint16, 65536), + (NativeSortKind::Int32, u64::from(u32::MAX) + 1), + (NativeSortKind::Uint32, u64::from(u32::MAX) + 1), + (NativeSortKind::Float32, u64::from(u32::MAX) + 1), + (NativeSortKind::Bool, 0), + (NativeSortKind::String, 0), + ] { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + values + .lists + .get_mut(&list) + .unwrap() + .elements + .push(Some(bits)); + assert_eq!( + values.math_numbers(list, kind as u32, INVALID_LIST, INVALID_LIST, INVALID_LIST), + Err(INVALID_LIST) + ); + assert_eq!(values.lists[&list].elements, [Some(bits)]); + assert_eq!( + values.math_numbers(list, u32::MAX, INVALID_LIST, INVALID_LIST, INVALID_LIST), + Err(INVALID_LIST) + ); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + let mut values = NativeValues::default(); + let owned = values.new_list(true).unwrap(); + let text = values.insert("held".into()).unwrap(); + values + .lists + .get_mut(&owned) + .unwrap() + .elements + .push(Some(text)); + assert_eq!( + values.math_numbers( + owned, + NativeSortKind::Int8 as u32, + INVALID_LIST, + INVALID_LIST, + INVALID_LIST + ), + Err(INVALID_LIST) + ); + assert_eq!(values.strings[&text].references, 1); + values.drop_value(owned).unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn empty_numeric_aggregates_report_their_checked_runtime_errors() { + for (operation, expected) in [ + ( + jett_rt_v1_math_average + as unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> f64, + b"math.average: list is empty".as_slice(), + ), + ( + jett_rt_v1_math_median + as unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> f64, + b"math.median: list is empty".as_slice(), + ), + ] { + for raw_kind in 0..10 { + let context = Context::new(); + unsafe { + let list = jett_rt_v1_list_new(context.pointer(), 0); + assert_eq!(operation(context.pointer(), list, raw_kind), 0.0); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert_eq!( + state.as_ref().unwrap().values.failure, + Some((JettRuntimeStatusV1::INVALID_ARGUMENT, expected)) + ); + drop(state); + jett_rt_v1_value_drop(context.pointer(), list); + } + } + } + } + + #[test] + fn math_aggregates_preserve_pending_failure_precedence_and_borrowed_input() { + for (operation, element_error, list_error) in [ + ( + jett_rt_v1_math_average + as unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> f64, + b"math.average expects a list of numeric values".as_slice(), + b"math.__average expects a list of numbers".as_slice(), + ), + ( + jett_rt_v1_math_median + as unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> f64, + b"math.median expects a list of numeric values".as_slice(), + b"math.__median expects a list of numbers".as_slice(), + ), + ] { + for raw_kind in 0..10 { + for outer_pending in [false, true] { + for depth in [1, 2] { + let context = Context::new(); + unsafe { + let list = jett_rt_v1_list_new(context.pointer(), 0); + jett_rt_v1_list_append(context.pointer(), list, 1); + let lease = + acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let mut state = lock_unpoisoned(&lease.entry.state); + let row = + state.as_mut().unwrap().values.lists.get_mut(&list).unwrap(); + row.element_pending_depths.insert(0, depth); + if outer_pending { + row.pending_depth = depth; + } + } + assert_eq!(operation(context.pointer(), list, raw_kind), 0.0); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!( + values.failure, + Some(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + if outer_pending { + list_error + } else { + element_error + } + )) + ); + assert_eq!(values.lists[&list].elements, [Some(1)]); + assert_eq!( + values.lists[&list].element_pending_depths.get(&0), + Some(&depth) + ); + } + jett_rt_v1_value_drop(context.pointer(), list); + assert!( + lock_unpoisoned(&lease.entry.state) + .as_ref() + .unwrap() + .values + .is_empty() + ); + } + } + } + } + } + } + #[test] + fn consuming_list_elements_transfer_identity_and_drop_only_initialized_slots() { + let context = Context::new(); + unsafe { + let first = jett_rt_v1_bytes_new(context.pointer()); + let second = jett_rt_v1_bytes_new(context.pointer()); + let list = jett_rt_v1_list_new(context.pointer(), 1); + jett_rt_v1_list_append(context.pointer(), list, first); + jett_rt_v1_list_append(context.pointer(), list, second); + assert_eq!( + jett_rt_v1_list_element_take(context.pointer(), list, 0), + first + ); + // A repeated take fails before changing either remaining owner. + assert_eq!(jett_rt_v1_list_element_take(context.pointer(), list, 0), 0); + assert_ne!(jett_rt_v1_value_status(context.pointer()), 0); + jett_rt_v1_value_drop(context.pointer(), list); + jett_rt_v1_value_drop(context.pointer(), first); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 2)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 1)); + assert!(!values.cleanup_failed); + assert!(values.is_empty()); + } + } + #[test] + fn partial_nested_list_clone_allocation_failure_releases_cloned_prefix() { + let context = Context::new(); + unsafe { + let a = jett_rt_v1_bytes_new(context.pointer()); + let b = jett_rt_v1_bytes_new(context.pointer()); + let inner = jett_rt_v1_list_new(context.pointer(), 1); + jett_rt_v1_list_append(context.pointer(), inner, a); + jett_rt_v1_list_append(context.pointer(), inner, b); + let outer = jett_rt_v1_list_new(context.pointer(), 1); + jett_rt_v1_list_append(context.pointer(), outer, inner); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let mut state = lock_unpoisoned(&lease.entry.state); + // Clone first byte, fail allocation of second byte. The source + // lists and both originals must remain owned by the caller. + state.as_mut().unwrap().values.allocation_budget = Some(1); + } + assert_eq!(jett_rt_v1_list_clone(context.pointer(), outer), 0); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.bytes_created, values.bytes_destroyed), (3, 1)); + assert!(values.bytes.contains_key(&a) && values.bytes.contains_key(&b)); + assert_eq!((values.lists_created, values.lists_destroyed), (2, 0)); + } + jett_rt_v1_value_drop(context.pointer(), outer); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + } + } + #[test] + fn string_segmentation_allocation_failure_cleans_partial_owned_list() { + let context = Context::new(); + let text = context.text("abc"); + let lease = acquire_context(context_key(context.pointer()).unwrap()).unwrap(); + { + let mut state = lock_unpoisoned(&lease.entry.state); + state.as_mut().unwrap().values.allocation_budget = Some(2); + } + unsafe { + assert_eq!(jett_rt_v1_string_chars(context.pointer(), text), 0); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.lists_created, values.lists_destroyed), (1, 1)); + assert_eq!(values.strings.len(), 1); + assert_eq!(values.text(text).unwrap(), "abc"); + } + jett_rt_v1_value_drop(context.pointer(), text); + } + } + #[test] + fn structs_borrow_fields_and_clone_nested_owners_with_exact_destruction() { + let context = Context::new(); + unsafe { + let p = context.pointer(); + let text = context.text("shared"); + let bytes = jett_rt_v1_bytes_from_string(p, text); + let inner = jett_rt_v1_struct_new(p, 3); + assert_eq!(jett_rt_v1_struct_init(p, inner, 2, bytes, 1), 0); + assert_eq!(jett_rt_v1_struct_init(p, inner, 0, text, 1), 0); + assert_eq!(jett_rt_v1_struct_init(p, inner, 1, u64::MAX, 0), 0); + let outer = jett_rt_v1_struct_new(p, 1); + jett_rt_v1_struct_init(p, outer, 0, inner, 1); + let copy = jett_rt_v1_struct_clone(p, outer); + let copied_inner = jett_rt_v1_struct_field(p, copy, 0); + assert_ne!(inner, copied_inner); + assert_eq!(jett_rt_v1_struct_field(p, inner, 2), bytes); + assert_ne!(jett_rt_v1_struct_field(p, copied_inner, 2), bytes); + assert_eq!(jett_rt_v1_struct_field(p, copied_inner, 0), text); + assert_eq!(jett_rt_v1_struct_field(p, copied_inner, 1), u64::MAX); + jett_rt_v1_value_drop(p, outer); + assert_ne!(jett_rt_v1_struct_field(p, copied_inner, 2), 0); + jett_rt_v1_value_drop(p, copy); + let lease = acquire_context(context_key(p).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!((values.structs_created, values.structs_destroyed), (4, 4)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (2, 2)); + assert!(values.is_empty()); + } + } + #[test] + fn actors_replace_owned_state_and_release_it_with_the_context() { + let context = Context::new(); + unsafe { + let p = context.pointer(); + let initial = context.text("initial"); + let actor = jett_rt_v1_struct_new(p, 1); + assert_eq!(jett_rt_v1_struct_init(p, actor, 0, initial, 1), 0); + assert_eq!(jett_rt_v1_actor_register(p, actor), actor); + let updated = context.text("updated"); + assert_eq!(jett_rt_v1_actor_replace(p, actor, 0, updated, 1), 0); + assert_eq!(jett_rt_v1_struct_field(p, actor, 0), updated); + assert_eq!(context.count(), 1, "old actor state must be released"); + assert_eq!(jett_rt_v1_value_status(p), 0); + } + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn reflected_field_validation_checks_owner_member_name_and_type() { + fn metadata( + values: &mut NativeValues, + index: u64, + owner: &str, + member: Option<&str>, + name: &str, + ty: &str, + ) -> u64 { + let owner = values.insert(owner.to_owned()).unwrap(); + let member = match member { + Some(member) => { + let text = values.insert(member.to_owned()).unwrap(); + values.sum(SUM_SUCCESS, text, true).unwrap() + } + None => values.sum(SUM_FAILURE, 0, false).unwrap(), + }; + let name = values.insert(name.to_owned()).unwrap(); + let ty = values.insert(ty.to_owned()).unwrap(); + let record = values.new_struct(5).unwrap(); + values.structs.get_mut(&record).unwrap().fields = vec![ + Some(NativeField { + bits: index, + owned: false, + pending_depth: 0, + }), + Some(NativeField { + bits: owner, + owned: true, + pending_depth: 0, + }), + Some(NativeField { + bits: member, + owned: true, + pending_depth: 0, + }), + Some(NativeField { + bits: name, + owned: true, + pending_depth: 0, + }), + Some(NativeField { + bits: ty, + owned: true, + pending_depth: 0, + }), + ]; + record + } + + let mut values = NativeValues::default(); + let expected = metadata(&mut values, 2, "Shape", Some("circle"), "radius", "float64"); + let matching = metadata(&mut values, 2, "Shape", Some("circle"), "radius", "float64"); + let wrong_owner = metadata(&mut values, 2, "Other", Some("circle"), "radius", "float64"); + let wrong_member = metadata(&mut values, 2, "Shape", Some("square"), "radius", "float64"); + let wrong_name = metadata(&mut values, 2, "Shape", Some("circle"), "size", "float64"); + let wrong_type = metadata(&mut values, 2, "Shape", Some("circle"), "radius", "int64"); + let wrong_index = metadata(&mut values, 1, "Shape", Some("circle"), "radius", "float64"); + assert_eq!( + values.reflected_field_index( + matching, + expected, + INVALID_REFLECTED_VARIANT_FIELD, + "type.variant_field_value" + ), + Ok(2) + ); + for candidate in [ + wrong_owner, + wrong_member, + wrong_name, + wrong_type, + wrong_index, + ] { + assert_eq!( + values.reflected_field_index( + candidate, + expected, + INVALID_REFLECTED_VARIANT_FIELD, + "type.variant_field_value" + ), + Err(INVALID_REFLECTED_VARIANT_FIELD) + ); + } + for record in [ + expected, + matching, + wrong_owner, + wrong_member, + wrong_name, + wrong_type, + wrong_index, + ] { + values.drop_value(record).unwrap(); + } + assert!(values.is_empty()); + } + #[test] + fn struct_take_transfers_a_field_without_dropping_it_with_the_record() { + let context = Context::new(); + unsafe { + let p = context.pointer(); + let text = context.text("payload"); + let record = jett_rt_v1_struct_new(p, 2); + assert_eq!(jett_rt_v1_struct_init(p, record, 0, 7, 0), 0); + assert_eq!(jett_rt_v1_struct_init(p, record, 1, text, 1), 0); + assert_eq!(jett_rt_v1_struct_take(p, record, 1), text); + jett_rt_v1_value_drop(p, record); + let lease = acquire_context(context_key(p).unwrap()).unwrap(); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.text(text).unwrap(), "payload"); + assert_eq!((values.structs_created, values.structs_destroyed), (1, 1)); + } + jett_rt_v1_value_drop(p, text); + let state = lock_unpoisoned(&lease.entry.state); + assert!(state.as_ref().unwrap().values.is_empty()); + } + } + #[test] + fn structs_partial_clone_rolls_back_each_allocation_boundary() { + for budget in 0..4 { + let context = Context::new(); + unsafe { + let p = context.pointer(); + let outer = jett_rt_v1_struct_new(p, 2); + let child = jett_rt_v1_struct_new(p, 1); + let a = jett_rt_v1_bytes_new(p); + let b = jett_rt_v1_bytes_new(p); + jett_rt_v1_struct_init(p, child, 0, a, 1); + jett_rt_v1_struct_init(p, outer, 0, child, 1); + jett_rt_v1_struct_init(p, outer, 1, b, 1); + let lease = acquire_context(context_key(p).unwrap()).unwrap(); + lock_unpoisoned(&lease.entry.state) + .as_mut() + .unwrap() + .values + .allocation_budget = Some(budget); + assert_eq!(jett_rt_v1_struct_clone(p, outer), 0); + assert_eq!( + jett_rt_v1_value_status(p), + JettRuntimeStatusV1::RESOURCE_EXHAUSTED.code() + ); + { + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.structs.len(), 2); + assert_eq!(values.bytes.len(), 2); + assert!(values.bytes.contains_key(&a) && values.bytes.contains_key(&b)); + } + jett_rt_v1_value_drop(p, outer); + let state = lock_unpoisoned(&lease.entry.state); + assert!(state.as_ref().unwrap().values.is_empty()); + assert!(!state.as_ref().unwrap().values.cleanup_failed); + } + } + } + #[test] + fn structs_invalid_projection_and_double_drop_preserve_first_failure() { + let context = Context::new(); + unsafe { + let p = context.pointer(); + let value = jett_rt_v1_struct_new(p, 2); + let bytes = jett_rt_v1_bytes_new(p); + jett_rt_v1_struct_init(p, value, 0, bytes, 1); + assert_eq!(jett_rt_v1_struct_field(p, value, 1), 0); + assert_ne!(jett_rt_v1_value_status(p), 0); + jett_rt_v1_value_drop(p, value); + jett_rt_v1_value_drop(p, value); + let lease = acquire_context(context_key(p).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!(values.failure, Some(INVALID_STRUCT)); + assert!(values.cleanup_failed); + assert!(values.is_empty()); + assert_eq!((values.structs_created, values.structs_destroyed), (1, 1)); + } + context.destroy(JettRuntimeStatusV1::INVALID_ARGUMENT); + } + #[test] + fn structs_empty_record_is_an_owner_and_leak_fails_destruction() { + let context = Context::new(); + unsafe { + assert_ne!(jett_rt_v1_struct_new(context.pointer(), 0), 0); + } + context.destroy(JettRuntimeStatusV1::INVALID_ARGUMENT); + } + #[test] + fn machine_state_guard_fails_before_projecting_a_wrong_payload() { + let context = Context::new(); + unsafe { + let pointer = context.pointer(); + let value = jett_rt_v1_struct_new(pointer, 1); + assert_eq!(jett_rt_v1_struct_init(pointer, value, 0, 1, 0), 0); + assert_eq!(jett_rt_v1_machine_expect_state(pointer, value, 1), 0); + assert_ne!(jett_rt_v1_machine_expect_state(pointer, value, 2), 0); + assert_ne!(jett_rt_v1_value_status(pointer), 0); + jett_rt_v1_value_drop(pointer, value); + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!( + values.failure, + Some(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"machine state does not match narrowed type".as_slice() + )) + ); + assert!(values.is_empty()); + } + context.destroy(JettRuntimeStatusV1::OK); + } + #[test] + fn bitfield_decode_rolls_back_each_owned_allocation() { + fn name(descriptor: &mut Vec, value: &str) { + descriptor.extend_from_slice(&(value.len() as u32).to_le_bytes()); + descriptor.extend_from_slice(value.as_bytes()); + } + let mut descriptor = b"JB\x01\x01".to_vec(); + name(&mut descriptor, "app.Packet"); + descriptor.extend_from_slice(&3_u32.to_le_bytes()); + descriptor.extend_from_slice(&[0, 8]); + name(&mut descriptor, "kind"); + descriptor.extend_from_slice(&[1, 8]); + name(&mut descriptor, "protocol"); + name(&mut descriptor, "app.Protocol"); + descriptor.extend_from_slice(&2_u32.to_le_bytes()); + descriptor.extend_from_slice(&1_i64.to_le_bytes()); + descriptor.extend_from_slice(&2_i64.to_le_bytes()); + descriptor.extend_from_slice(&[2, 0]); + name(&mut descriptor, "payload"); + + for budget in 0..4 { + let mut values = NativeValues::default(); + let input = values.insert_bytes(vec![5, 2, 7, 9]).unwrap(); + values.allocation_budget = Some(budget); + assert_eq!(values.decode_bitfield(input, &descriptor), Err(EXHAUSTED)); + assert_eq!(values.bytes(input).unwrap(), &[5, 2, 7, 9]); + assert_eq!(values.structs_created, values.structs_destroyed); + assert_eq!(values.lists_created, values.lists_destroyed); + assert_eq!(values.sums_created, values.sums_destroyed); + values.drop_value(input).unwrap(); + assert!(values.is_empty()); + } + for budget in 0..2 { + let mut values = NativeValues::default(); + let input = values.insert_bytes(vec![5]).unwrap(); + values.allocation_budget = Some(budget); + assert_eq!(values.decode_bitfield(input, &descriptor), Err(EXHAUSTED)); + assert!(values.strings.is_empty()); + values.drop_value(input).unwrap(); + assert!(values.is_empty()); + } + } + #[test] + fn list_comparison_decodes_every_primitive_carrier_and_the_unsupported_sentinel() { + for kind in [ + NativeSortKind::Int8, + NativeSortKind::Int16, + NativeSortKind::Int32, + NativeSortKind::Int64, + NativeSortKind::Uint8, + NativeSortKind::Uint16, + NativeSortKind::Uint32, + NativeSortKind::Uint64, + NativeSortKind::Float32, + NativeSortKind::Float64, + NativeSortKind::Bool, + NativeSortKind::String, + ] { + assert_eq!( + NativeSortKind::from_comparison_raw(kind as u32), + Ok(Some(kind)) + ); + } + assert_eq!(NativeSortKind::from_comparison_raw(u32::MAX), Ok(None)); + for invalid in [12, 255, u32::MAX - 1] { + assert_eq!( + NativeSortKind::from_comparison_raw(invalid), + Err(INVALID_LIST) + ); + } + } + #[test] + fn list_sort_orders_all_primitive_widths_and_preserves_string_owners() { + let cases = [ + (NativeSortKind::Int8, [255, 2, 253], [253, 255, 2]), + (NativeSortKind::Int16, [65535, 2, 65533], [65533, 65535, 2]), + ( + NativeSortKind::Int32, + [u32::MAX as u64, 2, (u32::MAX - 2) as u64], + [(u32::MAX - 2) as u64, u32::MAX as u64, 2], + ), + ( + NativeSortKind::Int64, + [u64::MAX, 2, u64::MAX - 2], + [u64::MAX - 2, u64::MAX, 2], + ), + (NativeSortKind::Uint8, [255, 2, 1], [1, 2, 255]), + (NativeSortKind::Uint16, [65535, 2, 1], [1, 2, 65535]), + ( + NativeSortKind::Uint32, + [u32::MAX as u64, 2, 1], + [1, 2, u32::MAX as u64], + ), + (NativeSortKind::Uint64, [u64::MAX, 2, 1], [1, 2, u64::MAX]), + ( + NativeSortKind::Float32, + [1.5_f32.to_bits() as u64, (-2.25_f32).to_bits() as u64, 0], + [(-2.25_f32).to_bits() as u64, 0, 1.5_f32.to_bits() as u64], + ), + ( + NativeSortKind::Float64, + [1.5_f64.to_bits(), (-2.25_f64).to_bits(), 0], + [(-2.25_f64).to_bits(), 0, 1.5_f64.to_bits()], + ), + (NativeSortKind::Bool, [1, 0, 1], [0, 1, 1]), + ]; + for (kind, input, expected) in cases { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + values.lists.get_mut(&list).unwrap().elements = input.map(Some).to_vec(); + assert_eq!(values.list_is_sorted(list, kind as u32), Ok(false)); + assert_eq!(values.sort_list(list, kind as u32), Ok(list)); + assert_eq!(values.lists[&list].elements, expected.map(Some)); + assert_eq!(values.list_is_sorted(list, kind as u32), Ok(true)); + + let outer = values.new_list(true).unwrap(); + for bits in input { + let row = values.new_list(false).unwrap(); + values + .lists + .get_mut(&row) + .unwrap() + .elements + .push(Some(bits)); + values + .lists + .get_mut(&outer) + .unwrap() + .elements + .push(Some(row)); + } + assert_eq!(values.sort_list_by_index(outer, 0, kind as u32), Ok(outer)); + let ordered_keys = values.lists[&outer] + .elements + .iter() + .map(|row| values.lists[&row.unwrap()].elements[0].unwrap()) + .collect::>(); + assert_eq!(ordered_keys, expected); + values.drop_value(outer).unwrap(); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + + let mut values = NativeValues::default(); + let zebra = values.insert("zebra".into()).unwrap(); + let apple = values.insert("apple".into()).unwrap(); + let eclair = values.insert("éclair".into()).unwrap(); + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = + vec![Some(zebra), Some(eclair), Some(apple)]; + assert_eq!( + values.list_is_sorted(list, NativeSortKind::String as u32), + Ok(false) + ); + assert_eq!( + values.sort_list(list, NativeSortKind::String as u32), + Ok(list) + ); + assert_eq!( + values.lists[&list].elements, + [Some(apple), Some(zebra), Some(eclair)] + ); + assert_eq!( + values.list_is_sorted(list, NativeSortKind::String as u32), + Ok(true) + ); + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + #[test] + fn float_list_sortedness_uses_ieee_comparisons_and_skips_pending_pairs() { + let cases: &[(&[f64], bool)] = &[ + (&[], true), + (&[f64::NAN], true), + (&[1.0], true), + (&[f64::NAN, 1.0], false), + (&[1.0, f64::NAN], false), + (&[f64::NAN, f64::NAN], false), + (&[1.0, 2.0], true), + (&[2.0, 1.0], false), + (&[-0.0, 0.0], true), + (&[0.0, -0.0], true), + (&[f64::NEG_INFINITY, 1.0], true), + (&[1.0, f64::INFINITY], true), + (&[f64::INFINITY, 1.0], false), + (&[1.0, f64::NEG_INFINITY], false), + (&[f64::INFINITY, f64::INFINITY], true), + ]; + for kind in [NativeSortKind::Float32, NativeSortKind::Float64] { + for &(input, expected) in cases { + let mut values = NativeValues::default(); + let bits = input + .iter() + .map(|value| { + if kind == NativeSortKind::Float32 { + u64::from((*value as f32).to_bits()) + } else { + value.to_bits() + } + }) + .collect::>(); + let list = values.new_list(false).unwrap(); + values.lists.get_mut(&list).unwrap().elements = + bits.iter().copied().map(Some).collect(); + assert_eq!(values.list_is_sorted(list, kind as u32), Ok(expected)); + if input.len() == 2 { + for index in 0..2 { + for depth in [1, 2] { + values + .lists + .get_mut(&list) + .unwrap() + .element_pending_depths + .insert(index, depth); + assert_eq!(values.list_is_sorted(list, kind as u32), Ok(true)); + values + .lists + .get_mut(&list) + .unwrap() + .element_pending_depths + .clear(); + assert_eq!(values.list_is_sorted(list, kind as u32), Ok(expected)); + } + } + if input.iter().any(|value| value.is_nan()) { + assert_eq!(values.sort_list(list, kind as u32), Ok(list)); + assert_eq!( + values.lists[&list].elements, + bits.iter().copied().map(Some).collect::>() + ); + let outer = values.new_list(true).unwrap(); + let mut rows = Vec::new(); + for bits in &bits { + let row = values.new_list(false).unwrap(); + values + .lists + .get_mut(&row) + .unwrap() + .elements + .push(Some(*bits)); + values + .lists + .get_mut(&outer) + .unwrap() + .elements + .push(Some(row)); + rows.push(Some(row)); + } + assert_eq!(values.sort_list_by_index(outer, 0, kind as u32), Ok(outer)); + assert_eq!(values.lists[&outer].elements, rows); + values.drop_value(outer).unwrap(); + } + } + values.drop_value(list).unwrap(); + assert!(values.is_empty()); + } + } + } + #[test] + fn list_sort_preserves_pending_string_order_and_owners_until_joined() { + for (left_depth, right_depth) in [(1, 0), (2, 0), (0, 1), (0, 2), (1, 2), (2, 1)] { + let mut values = NativeValues::default(); + let left = values.insert_pending("b".into(), left_depth).unwrap(); + let right = values.insert_pending("a".into(), right_depth).unwrap(); + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = vec![Some(left), Some(right)]; + + assert_eq!( + values.list_is_sorted(list, NativeSortKind::String as u32), + Ok(true) + ); + assert_eq!( + values.sort_list(list, NativeSortKind::String as u32), + Ok(list) + ); + assert_eq!(values.lists[&list].elements, [Some(left), Some(right)]); + assert_eq!(values.strings.len(), 2); + for (handle, depth, text) in [(left, left_depth, "b"), (right, right_depth, "a")] { + let string = &values.strings[&handle]; + assert_eq!(string.pending_depth, depth); + assert_eq!(string.references, 1); + assert_eq!(string.text, text); + } + + let mut ready = [left, right]; + for (index, depth) in [left_depth, right_depth].into_iter().enumerate() { + for remaining in (0..depth).rev() { + let old = ready[index]; + let joined = values.join_string(old).unwrap(); + values.lists.get_mut(&list).unwrap().elements[index] = Some(joined); + values.release(old).unwrap(); + ready[index] = joined; + assert_eq!(values.strings[&joined].pending_depth, remaining); + assert_eq!(values.strings[&joined].references, 1); + assert_eq!(values.strings.len(), 2); + } + } + assert_eq!( + values.list_is_sorted(list, NativeSortKind::String as u32), + Ok(false) + ); + assert_eq!( + values.sort_list(list, NativeSortKind::String as u32), + Ok(list) + ); + assert_eq!( + values.lists[&list].elements, + [Some(ready[1]), Some(ready[0])] + ); + assert_eq!( + values.list_is_sorted(list, NativeSortKind::String as u32), + Ok(true) + ); + assert_eq!(values.strings.len(), 2); + for (handle, text) in [(ready[0], "b"), (ready[1], "a")] { + let string = &values.strings[&handle]; + assert_eq!(string.pending_depth, 0); + assert_eq!(string.references, 1); + assert_eq!(string.text, text); + } + values.drop_value(list).unwrap(); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 1)); + assert!(values.is_empty()); + } + } + #[test] + fn indexed_list_sort_preserves_pending_rows_and_keys_until_joined() { + for kind in [NativeSortKind::Int64, NativeSortKind::String] { + for pending_rows in [false, true] { + for depths in [[1, 0], [2, 0], [0, 1], [0, 2], [1, 2], [2, 1]] { + let mut values = NativeValues::default(); + let outer = values.new_list(true).unwrap(); + let mut rows = [0; 2]; + let mut keys = [0; 2]; + for (index, depth) in depths.into_iter().enumerate() { + let key_depth = if pending_rows { 0 } else { depth }; + let bits = if kind == NativeSortKind::String { + let text = if index == 0 { "b" } else { "a" }; + values.insert_pending(text.into(), key_depth).unwrap() + } else if index == 0 { + 2 + } else { + 1 + }; + let row = values.new_list(kind == NativeSortKind::String).unwrap(); + let record = values.lists.get_mut(&row).unwrap(); + record.elements.push(Some(bits)); + if pending_rows { + record.pending_depth = depth; + } else if kind != NativeSortKind::String && key_depth != 0 { + record.element_pending_depths.insert(0, key_depth); + } + rows[index] = row; + keys[index] = bits; + values + .lists + .get_mut(&outer) + .unwrap() + .elements + .push(Some(row)); + } + let live_strings = values.strings.len(); + assert_eq!(values.sort_list_by_index(outer, 0, kind as u32), Ok(outer)); + assert_eq!(values.lists[&outer].elements, rows.map(Some)); + assert_eq!(values.lists.len(), 3); + assert_eq!(values.strings.len(), live_strings); + for (index, row) in rows.into_iter().enumerate() { + let record = &values.lists[&row]; + assert_eq!(record.elements, [Some(keys[index])]); + assert_eq!( + record.pending_depth, + if pending_rows { depths[index] } else { 0 } + ); + if kind == NativeSortKind::String { + let key = &values.strings[&keys[index]]; + assert_eq!( + key.pending_depth, + if pending_rows { 0 } else { depths[index] } + ); + assert_eq!(key.references, 1); + } else { + assert_eq!( + record.element_pending_depths.get(&0).copied().unwrap_or(0), + if pending_rows { 0 } else { depths[index] } + ); + } + } + for (index, depth) in depths.into_iter().enumerate() { + for remaining in (0..depth).rev() { + if pending_rows { + let old = rows[index]; + let joined = values.join_list(old).unwrap(); + values.lists.get_mut(&outer).unwrap().elements[index] = + Some(joined); + values.drop_value(old).unwrap(); + rows[index] = joined; + assert_eq!(values.lists[&joined].pending_depth, remaining); + } else if kind == NativeSortKind::String { + let old = keys[index]; + let joined = values.join_string(old).unwrap(); + values.lists.get_mut(&rows[index]).unwrap().elements[0] = + Some(joined); + values.release(old).unwrap(); + keys[index] = joined; + assert_eq!(values.strings[&joined].pending_depth, remaining); + } else { + // Scalar join computes the remaining depth in generated code. + let row = values.lists.get_mut(&rows[index]).unwrap(); + if remaining == 0 { + row.element_pending_depths.remove(&0); + } else { + row.element_pending_depths.insert(0, remaining); + } + } + } + } + assert_eq!(values.sort_list_by_index(outer, 0, kind as u32), Ok(outer)); + assert_eq!( + values.lists[&outer].elements, + [Some(rows[1]), Some(rows[0])] + ); + assert_eq!(values.lists.len(), 3); + assert_eq!(values.strings.len(), live_strings); + values.drop_value(outer).unwrap(); + assert!(values.is_empty()); + } + } + } + } + #[test] + fn indexed_list_sort_skips_uninitialized_pending_payloads_but_rejects_ready_ones() { + for (kind, pending_row) in [ + (NativeSortKind::Int64, true), + (NativeSortKind::String, true), + (NativeSortKind::Int64, false), + ] { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let pending = values.new_list(kind == NativeSortKind::String).unwrap(); + let row = values.lists.get_mut(&pending).unwrap(); + row.elements.push(None); + if pending_row { + row.pending_depth = depth; + } else { + row.element_pending_depths.insert(0, depth); + } + let ready = values.new_list(kind == NativeSortKind::String).unwrap(); + let bits = if kind == NativeSortKind::String { + values.insert("a".into()).unwrap() + } else { + 1 + }; + values + .lists + .get_mut(&ready) + .unwrap() + .elements + .push(Some(bits)); + let outer = values.new_list(true).unwrap(); + values.lists.get_mut(&outer).unwrap().elements = vec![Some(pending), Some(ready)]; + assert_eq!(values.sort_list_by_index(outer, 0, kind as u32), Ok(outer)); + assert_eq!(values.lists[&outer].elements, [Some(pending), Some(ready)]); + assert_eq!(values.lists[&pending].elements, [None]); + + let row = values.lists.get_mut(&pending).unwrap(); + row.pending_depth = 0; + row.element_pending_depths.clear(); + assert_eq!( + values.sort_list_by_index(outer, 0, kind as u32), + Err(INVALID_LIST) + ); + assert_eq!(values.lists[&outer].elements, [Some(pending), Some(ready)]); + values.drop_value(outer).unwrap(); + assert!(values.is_empty()); + } + } + } + #[test] + fn indexed_list_sort_preserves_rows_and_missing_keys() { + let mut values = NativeValues::default(); + let outer = values.new_list(true).unwrap(); + let mut rows = Vec::new(); + for text in ["b", "a", "a"] { + let key = values.insert(text.into()).unwrap(); + let row = values.new_list(true).unwrap(); + values.lists.get_mut(&row).unwrap().elements.push(Some(key)); + values + .lists + .get_mut(&outer) + .unwrap() + .elements + .push(Some(row)); + rows.push(row); + } + assert_eq!( + values.sort_list_by_index(outer, -1, NativeSortKind::String as u32), + Ok(outer) + ); + assert_eq!( + values.lists[&outer].elements, + rows.iter().copied().map(Some).collect::>() + ); + assert_eq!( + values.sort_list_by_index(outer, 0, NativeSortKind::String as u32), + Ok(outer) + ); + assert_eq!( + values.lists[&outer].elements, + [Some(rows[1]), Some(rows[2]), Some(rows[0])] + ); + assert!( + values + .list_is_sorted(rows[1], NativeSortKind::String as u32) + .unwrap() + ); + values.drop_value(outer).unwrap(); + assert!(values.is_empty()); + + let mut values = NativeValues::default(); + let flags = values.new_list(false).unwrap(); + values.lists.get_mut(&flags).unwrap().elements = vec![Some(1), Some(0)]; + assert_eq!( + values.list_is_sorted(flags, NativeSortKind::Bool as u32), + Ok(false) + ); + assert_eq!(values.list_is_sorted(flags, u32::MAX), Ok(true)); + values.drop_value(flags).unwrap(); + assert!(values.is_empty()); + } + #[test] + fn set_duplicate_strings_and_clones_release_exactly_one_owner_each() { + let mut values = NativeValues::default(); + let first = values.insert("ada".into()).unwrap(); + let duplicate = values.insert("ada".into()).unwrap(); + let set = values.new_set(1).unwrap(); + assert_eq!(values.set_add(set, first), Ok(set)); + assert_eq!(values.set_add(set, duplicate), Ok(set)); + assert_eq!(values.sets[&set].elements, [Some(first)]); + assert!(!values.strings.contains_key(&duplicate)); + + let copy = values.clone_set(set).unwrap(); + assert_eq!(values.strings[&first].references, 2); + assert_eq!(values.set_remove(set, first), Ok(set)); + assert!(values.sets[&set].elements.is_empty()); + assert_eq!(values.strings[&first].references, 1); + assert_eq!(values.set_position(copy, first), Ok(Some(0))); + values.drop_value(set).unwrap(); + values.drop_value(copy).unwrap(); + assert_eq!((values.sets_created, values.sets_destroyed), (2, 2)); + assert!(values.is_empty()); + } + #[test] + fn partial_set_iteration_drops_only_remaining_owned_elements() { + let mut values = NativeValues::default(); + let first = values.insert("first".into()).unwrap(); + let second = values.insert("second".into()).unwrap(); + let set = values.new_set(1).unwrap(); + values.set_add(set, first).unwrap(); + values.set_add(set, second).unwrap(); + let borrowed = values.retain(first).unwrap(); + let taken = values + .sets + .get_mut(&set) + .unwrap() + .elements + .get_mut(0) + .and_then(Option::take) + .unwrap(); + assert_eq!(taken, first); + values.drop_value(set).unwrap(); + assert!(!values.strings.contains_key(&second)); + assert_eq!(values.strings[&first].references, 2); + values.drop_value(taken).unwrap(); + values.drop_value(borrowed).unwrap(); + assert!(values.is_empty()); + } + #[test] + fn map_literal_duplicates_and_insert_have_distinct_semantics_and_exact_cleanup() { + let mut values = NativeValues::default(); + let map = values.new_map(1, 1).unwrap(); + let first = values.insert("same".into()).unwrap(); + let second = values.insert("same".into()).unwrap(); + let old = values.insert("old".into()).unwrap(); + let newer = values.insert("new".into()).unwrap(); + values.map_append_literal(map, first, old).unwrap(); + values.map_append_literal(map, second, newer).unwrap(); + assert_eq!(values.maps[&map].entries.len(), 2); + assert_eq!(values.map_position(map, first), Ok(Some(0))); + let replacement_key = values.insert("same".into()).unwrap(); + let replacement = values.insert("replacement".into()).unwrap(); + values + .map_insert(map, replacement_key, replacement) + .unwrap(); + assert!(!values.strings.contains_key(&replacement_key)); + assert!(!values.strings.contains_key(&old)); + let copy = values.clone_map(map).unwrap(); + values.map_remove(map, first).unwrap(); + assert!(values.maps[&map].entries.is_empty()); + assert_eq!(values.map_position(copy, second), Ok(Some(0))); + values.drop_value(map).unwrap(); + values.drop_value(copy).unwrap(); + assert_eq!((values.maps_created, values.maps_destroyed), (2, 2)); + assert!(values.is_empty()); + } + #[test] + fn partial_map_iteration_releases_only_remaining_fields() { + let mut values = NativeValues::default(); + let key = values.insert("key".into()).unwrap(); + let value = values.insert("value".into()).unwrap(); + let map = values.new_map(1, 1).unwrap(); + values.map_insert(map, key, value).unwrap(); + let taken_key = values.map_element(map, 0, true, true).unwrap(); + values.drop_value(map).unwrap(); + assert!(!values.strings.contains_key(&value)); + assert!(values.strings.contains_key(&taken_key)); + values.drop_value(taken_key).unwrap(); + assert!(values.is_empty()); + } + #[test] + fn csv_nested_values_roll_back_each_allocation_boundary() { + for budget in 0..10 { + let mut values = NativeValues::default(); + values.allocation_budget = Some(budget); + match values.csv_rows(vec![vec!["a".into(), "b".into()], vec!["c".into()]]) { + Ok(rows) => { + values.drop_value(rows).unwrap(); + } + Err(error) => assert_eq!(error, EXHAUSTED), + } + assert!(values.is_empty(), "CSV rows leaked at budget {budget}"); + } + for budget in 0..12 { + let mut values = NativeValues::default(); + values.allocation_budget = Some(budget); + match values.csv_header_rows(vec![vec![ + ("name".into(), "Ada".into()), + ("age".into(), "30".into()), + ]]) { + Ok(rows) => { + values.drop_value(rows).unwrap(); + } + Err(error) => assert_eq!(error, EXHAUSTED), + } + assert!( + values.is_empty(), + "CSV header rows leaked at budget {budget}" + ); + } + let mut values = NativeValues::default(); + let rows = values.csv_rows(vec![vec!["value".into()]]).unwrap(); + values.allocation_budget = Some(0); + assert_eq!(values.owned_sum(SUM_SUCCESS, rows), Err(EXHAUSTED)); + assert!(values.is_empty()); + } +} + +#[cfg(test)] +mod reflected_container_tests; + +#[cfg(test)] +mod resource_companion_tests; + +#[cfg(test)] +mod ordinary_borrowed_sum_tests; diff --git a/crates/jett_runtime/src/native_abi/values/actor_field_update_tests.rs b/crates/jett_runtime/src/native_abi/values/actor_field_update_tests.rs new file mode 100644 index 00000000..539659b6 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/actor_field_update_tests.rs @@ -0,0 +1,380 @@ +use super::*; + +fn actor_record(values: &mut NativeValues, fields: &[NativeField]) -> u64 { + let id = record(values, fields); + values.register_actor(id).unwrap() +} + +#[test] +fn actor_occupied_catalog_has_exact_versioned_transfer_signature() { + assert_eq!( + NativeLeaf::ActorReplaceOccupied.symbol(), + "jett_rt_v1_actor_replace_occupied" + ); + assert_eq!( + NativeLeaf::ActorReplaceOccupied.parameters(), + &[ + AbiScalar::Pointer, + AbiScalar::I64, + AbiScalar::I64, + AbiScalar::I64, + AbiScalar::I32, + AbiScalar::I64, + ] + ); + assert_eq!(NativeLeaf::ActorReplaceOccupied.result(), AbiScalar::I64); +} + +#[test] +fn actor_occupied_scalar_swap_preserves_siblings_and_changes_task_depth_without_allocation() { + let mut values = NativeValues::default(); + let actor = actor_record(&mut values, &[scalar(7, 2), scalar(19, 0)]); + values.allocation_budget = Some(0); + let displaced = values + .replace_actor_field_occupied(actor, 0, scalar(8, 4)) + .unwrap(); + assert_eq!( + (displaced.bits, displaced.pending_depth, displaced.owned), + (7, 2, false) + ); + assert_eq!(values.struct_field(actor, 0).unwrap().pending_depth, 4); + assert_eq!(values.struct_field(actor, 1).unwrap().bits, 19); + // A persistent alias is not an ordinary owning handle. + assert_eq!( + values + .replace_actor_field_occupied(actor, 1, scalar(actor, 0)) + .unwrap() + .bits, + 19 + ); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn actor_occupied_refuses_nonactors_pending_aliases_and_unpublished_graph_storage() { + let mut values = NativeValues::default(); + let actor = actor_record(&mut values, &[scalar(1, 0)]); + let pending = values.run_actor(actor).unwrap(); + let ordinary = record(&mut values, &[scalar(2, 0)]); + let ticket = values.begin_actor_graph().unwrap(); + let staged = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let staged_pending = values + .reserve_actor_graph_pending(ticket, staged, 1) + .unwrap(); + let mut foreign = NativeValues::default(); + let foreign_actor = actor_record(&mut foreign, &[scalar(3, 0)]); + for invalid in [ + 0, + ordinary, + pending, + ticket, + staged, + staged_pending, + foreign_actor, + ] { + assert_eq!( + values + .replace_actor_field_occupied(invalid, 0, scalar(9, 0)) + .err(), + Some(INVALID_ACTOR) + ); + assert_eq!(values.struct_field(actor, 0).unwrap().bits, 1); + } + assert_eq!( + values + .replace_actor_field_occupied(actor, 1, scalar(9, 0)) + .err(), + Some(INVALID_ACTOR) + ); + values.abort_actor_graph(ticket).unwrap(); + values.drop_value(ordinary).unwrap(); + values.release_actors().unwrap(); + foreign.release_actors().unwrap(); + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn actor_occupied_missing_slot_is_never_initialized_and_preserves_incoming_owner() { + let mut values = NativeValues::default(); + let before = values.insert_bytes(vec![1]).unwrap(); + let incoming = values.insert_bytes(vec![2]).unwrap(); + let actor = actor_record(&mut values, &[owning(before), scalar(17, 3)]); + let saved = values.structs.get_mut(&actor).unwrap().fields[0].take(); + assert_eq!( + values + .replace_actor_field_occupied(actor, 0, owning(incoming)) + .err(), + Some(INVALID_ACTOR) + ); + assert!(values.structs[&actor].fields[0].is_none()); + assert_eq!(values.struct_field(actor, 1).unwrap().pending_depth, 3); + assert!(values.bytes.contains_key(&before)); + assert!(values.bytes.contains_key(&incoming)); + values.structs.get_mut(&actor).unwrap().fields[0] = saved; + values.drop_value(incoming).unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn actor_occupied_rejects_mismatched_owned_representation_and_dead_children_before_publication() { + let mut values = NativeValues::default(); + let before = values.insert_bytes(vec![1]).unwrap(); + let incoming = values.insert_bytes(vec![2]).unwrap(); + let actor = actor_record(&mut values, &[owning(before), scalar(17, 0)]); + for (index, field) in [ + (0, scalar(9, 0)), + (1, owning(incoming)), + (0, owning(u64::MAX)), + (0, owning(actor)), + ( + 0, + NativeField { + pending_depth: 1, + ..owning(incoming) + }, + ), + ] { + assert_eq!( + values + .replace_actor_field_occupied(actor, index, field) + .err(), + Some(INVALID_ACTOR) + ); + assert_eq!(values.struct_field(actor, 0).unwrap().bits, before); + assert!(values.bytes.contains_key(&before)); + assert!(values.bytes.contains_key(&incoming)); + } + values.structs.get_mut(&actor).unwrap().fields[0] + .as_mut() + .unwrap() + .pending_depth = 1; + assert_eq!( + values + .replace_actor_field_occupied(actor, 0, owning(incoming)) + .err(), + Some(INVALID_ACTOR) + ); + values.structs.get_mut(&actor).unwrap().fields[0] + .as_mut() + .unwrap() + .pending_depth = 0; + values.drop_value(incoming).unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn actor_occupied_leaf_refuses_noncanonical_owned_flag_without_mutation() { + field_leaf_context(JettRuntimeStatusV1::INVALID_ARGUMENT, |pointer| { + let actor = leaf_values(pointer, |values| actor_record(values, &[scalar(7, 0)])); + assert_eq!( + unsafe { jett_rt_v1_actor_replace_occupied(pointer, actor, 0, 8, 2, 0) }, + 0 + ); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(actor, 0).unwrap().bits, 7); + values.release_actors().unwrap(); + }); + }); +} + +#[test] +fn actor_occupied_refuses_builder_overlay_without_mutating_state() { + let mut values = NativeValues::default(); + let actor = actor_record(&mut values, &[scalar(7, 0)]); + values.builders.insert( + actor, + NativeBuilderInfo { + owner: "Record".into(), + metadata_owner: "Record".into(), + unsupported_kind: None, + variant: None, + state: None, + target_state: None, + field_offset: 0, + field_names: vec!["value".into()], + field_type_names: vec!["int64".into()], + field_types: vec!["int64".into()], + field_debug_layouts: vec![Vec::new()], + put_order: vec![0], + validation: vec![BuilderFieldValidation::None], + }, + ); + assert_eq!( + values + .replace_actor_field_occupied(actor, 0, scalar(8, 0)) + .err(), + Some(INVALID_ACTOR) + ); + assert_eq!(values.struct_field(actor, 0).unwrap().bits, 7); + assert!(values.builders.contains_key(&actor)); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn actor_occupied_leaf_refuses_unique_reuse_and_sibling_alias_before_commit() { + for kind in 0..5 { + field_leaf_context(JettRuntimeStatusV1::INVALID_ARGUMENT, |pointer| { + let (actor, before, incoming) = leaf_values(pointer, |values| { + let before = match kind { + 0 => values.insert_bytes(vec![1]).unwrap(), + 1 => values.new_list(false).unwrap(), + 2 => record(values, &[scalar(7, 0)]), + _ => function_record(values), + }; + let incoming = if kind == 4 { + values.new_list(false).unwrap() + } else { + before + }; + let fields = if kind == 4 { + vec![owning(before), owning(incoming)] + } else { + vec![owning(before)] + }; + (actor_record(values, &fields), before, incoming) + }); + assert_eq!( + unsafe { jett_rt_v1_actor_replace_occupied(pointer, actor, 0, incoming, 1, 0) }, + 0 + ); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(actor, 0).unwrap().bits, before); + assert!(values.owned_pending_depth(incoming).is_ok()); + values.release_actors().unwrap(); + }); + }); + } +} + +#[test] +fn actor_occupied_leaf_requires_each_incoming_string_credit_and_never_drops_the_old_edge() { + for fields in [1_usize, 2] { + for retained in [false, true] { + let expected = if retained { + JettRuntimeStatusV1::OK + } else { + JettRuntimeStatusV1::INVALID_ARGUMENT + }; + field_leaf_context(expected, |pointer| { + let (actor, text) = leaf_values(pointer, |values| { + let text = values.insert("shared actor state".into()).unwrap(); + for _ in 1..fields { + values.retain(text).unwrap(); + } + (actor_record(values, &vec![owning(text); fields]), text) + }); + if retained { + assert_eq!(unsafe { jett_rt_v1_string_retain(pointer, text) }, text); + } + let displaced = + unsafe { jett_rt_v1_actor_replace_occupied(pointer, actor, 0, text, 1, 0) }; + assert_eq!(displaced, if retained { text } else { 0 }); + leaf_values(pointer, |values| { + assert_eq!( + values.strings[&text].references, + fields as u64 + u64::from(retained) + ) + }); + if retained { + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, displaced) }, 0); + } + leaf_values(pointer, |values| values.release_actors().unwrap()); + }); + } + } +} + +#[test] +fn actor_occupied_leaf_allows_distinct_function_copy_and_owned_child_task_depth() { + field_leaf_context(JettRuntimeStatusV1::OK, |pointer| { + let (actor, before, replacement) = leaf_values(pointer, |values| { + let before = function_record(values); + let replacement = values.clone_struct(before).unwrap(); + values.structs.get_mut(&replacement).unwrap().pending_depth = 2; + (actor_record(values, &[owning(before)]), before, replacement) + }); + assert_eq!( + unsafe { jett_rt_v1_actor_replace_occupied(pointer, actor, 0, replacement, 1, 0) }, + before + ); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(actor, 0).unwrap().bits, replacement); + assert_eq!(values.owned_pending_depth(replacement).unwrap(), 2); + assert!(values.structs.contains_key(&before)); + }); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, before) }, 0); + leaf_values(pointer, |values| values.release_actors().unwrap()); + }); +} + +#[test] +fn actor_occupied_leaf_publishes_before_old_cleanup_failure_and_transfers_incoming_slot_once() { + let mut context = JettRuntimeContextV1::retired(); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + { + let pointer = std::ptr::from_ref(&context); + let (actor, old, incoming, caller) = leaf_values(pointer, |values| { + let old = record(values, &[owning(u64::MAX)]); + let incoming = values.insert_bytes(vec![9]).unwrap(); + let actor = actor_record(values, &[owning(old)]); + let caller = record(values, &[owning(incoming)]); + (actor, old, incoming, caller) + }); + let displaced = + unsafe { jett_rt_v1_actor_replace_occupied(pointer, actor, 0, incoming, 1, 0) }; + assert_eq!(displaced, old); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + // Match the generated handoff: clear the incoming owner slot first. + assert_eq!( + unsafe { jett_rt_v1_struct_take(pointer, caller, 0) }, + incoming + ); + leaf_values(pointer, |values| { + assert!(values.structs[&caller].fields[0].is_none()); + assert!(values.structs.contains_key(&old)); + }); + unsafe { jett_rt_v1_value_drop(pointer, displaced) }; + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(actor, 0).unwrap().bits, incoming); + assert!(values.bytes.contains_key(&incoming)); + assert!(!values.structs.contains_key(&old)); + }); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, caller) }, 0); + leaf_values(pointer, |values| { + values.release_actors().unwrap(); + assert_eq!((values.bytes_created, values.bytes_destroyed), (1, 1)); + assert_eq!(values.structs_created, values.structs_destroyed); + assert!(values.cleanup_failed); + assert!(values.is_empty()); + }); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + } + // Registry cleanup does not erase the terminal displaced-value cleanup failure. + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let result = unsafe { result.assume_init() }; + assert_eq!(result.status, JettRuntimeStatusV1::INVALID_ARGUMENT); + let length = usize::try_from(result.message.byte_length).unwrap(); + assert_eq!(length, b"native value cleanup failed".len()); + assert!(!result.message.data.is_null()); + let message = unsafe { std::slice::from_raw_parts(result.message.data, length) }; + assert_eq!(message, b"native value cleanup failed"); +} diff --git a/crates/jett_runtime/src/native_abi/values/actor_graph_import.rs b/crates/jett_runtime/src/native_abi/values/actor_graph_import.rs new file mode 100644 index 00000000..be8ff105 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/actor_graph_import.rs @@ -0,0 +1,1234 @@ +//! Private two-phase import of a compiler-sealed frozen actor graph. +//! JA1 proves carrier shapes, not nominal types, refinements, or callback selection. +//! Those remain exact checked-plan obligations of the compiler graph factory. +use super::*; +use std::collections::HashSet; + +pub(super) const INVALID_IMPORT: Failure = ( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"invalid native actor graph import ticket, shape, or field", +); + +pub(super) struct Import { + bases: Vec, + records: HashMap, + pending: HashMap, + // Transferred trees are absent from all ordinary leaf registries until commit. + ordinary: NativeValues, + roots: Vec, + output: Option, +} + +enum Supplement { + Hidden(usize), + Function { + code: u64, + environment: Option, + }, + Interface { + concrete: u64, + child: usize, + }, + Builder { + owner: String, + offset: usize, + fields: Vec, + }, + Resource, + ListEntries(Vec), + MapEntries(Vec<(usize, usize)>), + CheckedHook { + descriptor_operation: u32, + hook: u32, + signature: u32, + }, +} + +struct ImportLayout { + debug: NativeDebugLayout, + supplements: HashMap, +} + +impl ImportLayout { + fn parse(bytes: &[u8]) -> LeafResult { + let mut cursor = BitfieldLayoutCursor { bytes, position: 0 }; + if cursor.take(3).map_err(|_| INVALID_IMPORT)? != b"JA\x01" { + return Err(INVALID_IMPORT); + } + let length = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + let debug = NativeDebugLayout::parse(cursor.take(length).map_err(|_| INVALID_IMPORT)?) + .map_err(|error| { + if error == EXHAUSTED { + error + } else { + INVALID_IMPORT + } + })?; + let count = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + if count > debug.nodes.len() { + return Err(INVALID_IMPORT); + } + let mut supplements = HashMap::new(); + supplements.try_reserve(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + let index = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + let node = debug.nodes.get(index).ok_or(INVALID_IMPORT)?; + let supplement = match cursor.byte().map_err(|_| INVALID_IMPORT)? { + 0 if matches!(node, NativeDebugNode::Redacted) => { + Supplement::Hidden(cursor.u32().map_err(|_| INVALID_IMPORT)? as usize) + } + 1 if matches!(node, NativeDebugNode::Function) => { + let code = u64::from_le_bytes( + cursor + .take(8) + .map_err(|_| INVALID_IMPORT)? + .try_into() + .map_err(|_| INVALID_IMPORT)?, + ); + if code == 0 { + return Err(INVALID_IMPORT); + } + let environment = cursor.u32().map_err(|_| INVALID_IMPORT)?; + Supplement::Function { + code, + environment: (environment != u32::MAX).then_some(environment as usize), + } + } + 2 if matches!(node, NativeDebugNode::Interface) => { + let concrete = u64::from_le_bytes( + cursor + .take(8) + .map_err(|_| INVALID_IMPORT)? + .try_into() + .map_err(|_| INVALID_IMPORT)?, + ); + let child = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + Supplement::Interface { concrete, child } + } + 3 if matches!(node, NativeDebugNode::TypeConstruction) => { + let owner = cursor.name().map_err(|_| INVALID_IMPORT)?; + let offset = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + let count = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + if count > bytes.len() / 4 { + return Err(INVALID_IMPORT); + } + let mut fields = Vec::new(); + fields.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + fields.push(cursor.u32().map_err(|_| INVALID_IMPORT)? as usize); + } + Supplement::Builder { + owner, + offset, + fields, + } + } + 4 if matches!(node, NativeDebugNode::Redacted) => Supplement::Resource, + 5 if matches!(node, NativeDebugNode::List(_)) => { + let count = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + if count > bytes.len() / 4 { + return Err(INVALID_IMPORT); + } + let mut children = Vec::new(); + children.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + children.push(cursor.u32().map_err(|_| INVALID_IMPORT)? as usize); + } + Supplement::ListEntries(children) + } + 6 if matches!(node, NativeDebugNode::Map(..)) => { + let count = cursor.u32().map_err(|_| INVALID_IMPORT)? as usize; + if count > bytes.len() / 8 { + return Err(INVALID_IMPORT); + } + let mut children = Vec::new(); + children.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + children.push(( + cursor.u32().map_err(|_| INVALID_IMPORT)? as usize, + cursor.u32().map_err(|_| INVALID_IMPORT)? as usize, + )); + } + Supplement::MapEntries(children) + } + 7 if matches!(node, NativeDebugNode::Function) => Supplement::CheckedHook { + descriptor_operation: cursor.u32().map_err(|_| INVALID_IMPORT)?, + hook: cursor.u32().map_err(|_| INVALID_IMPORT)?, + signature: cursor.u32().map_err(|_| INVALID_IMPORT)?, + }, + _ => return Err(INVALID_IMPORT), + }; + if supplements.insert(index, supplement).is_some() { + return Err(INVALID_IMPORT); + } + } + if cursor.position != bytes.len() { + return Err(INVALID_IMPORT); + } + let result = Self { debug, supplements }; + result.validate_indices()?; + Ok(result) + } + + fn validate_indices(&self) -> LeafResult<()> { + let check = |index: usize| { + self.debug + .nodes + .get(index) + .map(|_| ()) + .ok_or(INVALID_IMPORT) + }; + for node in &self.debug.nodes { + match node { + NativeDebugNode::List(c) + | NativeDebugNode::Set(c) + | NativeDebugNode::Optional(c) + | NativeDebugNode::Alias(c) => check(*c)?, + NativeDebugNode::Map(a, b) | NativeDebugNode::Result(a, b) => { + check(*a)?; + check(*b)?; + } + NativeDebugNode::Record(_, fields) => { + for (_, child) in fields { + check(*child)?; + } + } + NativeDebugNode::Enum(_, variants) | NativeDebugNode::Machine(_, variants) => { + for (_, fields) in variants { + for child in fields { + check(*child)?; + } + } + } + _ => {} + } + } + for supplement in self.supplements.values() { + match supplement { + Supplement::Hidden(c) | Supplement::Interface { child: c, .. } => check(*c)?, + Supplement::Function { + environment: Some(c), + .. + } => check(*c)?, + Supplement::Builder { fields, .. } => { + for child in fields { + check(*child)?; + } + } + Supplement::ListEntries(children) => { + for child in children { + check(*child)?; + } + } + Supplement::MapEntries(children) => { + for (key, value) in children { + check(*key)?; + check(*value)?; + } + } + _ => {} + } + } + // Refuse transparent cycles even in inactive arms. Unvisited type-only + // opaque nodes need no value-specific evidence until an entry selects them. + for start in 0..self.debug.nodes.len() { + let mut index = start; + let mut finished = false; + for _ in 0..self.debug.nodes.len() { + match self.debug.nodes.get(index).ok_or(INVALID_IMPORT)? { + NativeDebugNode::Alias(child) => index = *child, + NativeDebugNode::Redacted => match self.supplements.get(&index) { + Some(Supplement::Hidden(child)) => index = *child, + _ => { + finished = true; + break; + } + }, + _ => { + finished = true; + break; + } + } + } + if !finished { + return Err(INVALID_IMPORT); + } + } + Ok(()) + } + + fn resolve(&self, mut index: usize) -> LeafResult { + for _ in 0..self.debug.nodes.len() { + match self.debug.nodes.get(index).ok_or(INVALID_IMPORT)? { + NativeDebugNode::Alias(child) => index = *child, + NativeDebugNode::Redacted => match self.supplements.get(&index) { + Some(Supplement::Hidden(child)) => index = *child, + Some(Supplement::Resource) => return Ok(index), + _ => return Err(INVALID_IMPORT), + }, + _ => return Ok(index), + } + } + Err(INVALID_IMPORT) + } + + fn owns(&self, index: usize) -> LeafResult { + Ok( + match self + .debug + .nodes + .get(self.resolve(index)?) + .ok_or(INVALID_IMPORT)? + { + NativeDebugNode::Primitive(kind) => *kind == NativeSortKind::String as u32, + // These native scalar shapes carry no ordinary-owned registry value. + // Empty/inactive carriers may mention them without containing authority. + // Validator::value still refuses every actual visit to those shapes. + NativeDebugNode::Nothing + | NativeDebugNode::Actor + | NativeDebugNode::Capability(_) + | NativeDebugNode::Redacted + | NativeDebugNode::Uninhabited => false, + _ => true, + }, + ) + } + + fn key_strings(&self, index: usize) -> LeafResult { + match self + .debug + .nodes + .get(self.resolve(index)?) + .ok_or(INVALID_IMPORT)? + { + NativeDebugNode::Primitive(kind) if *kind <= 7 || *kind == 10 || *kind == 11 => { + Ok(*kind == 11) + } + _ => Err(INVALID_IMPORT), + } + } +} + +#[derive(Clone, Copy)] +enum OwnedKind { + CheckedHook, + String, + Bytes, + Struct, + List, + Set, + Map, + Sum, +} + +struct Validator<'a> { + values: &'a NativeValues, + import: &'a Import, + ticket: u64, + layout: &'a ImportLayout, + work: Vec<(usize, NativeField)>, + owned: Vec<(u64, OwnedKind)>, + seen: HashSet, + string_edges: HashMap, +} + +impl<'a> Validator<'a> { + fn push(&mut self, index: usize, field: NativeField) -> LeafResult<()> { + self.work.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.work.push((index, field)); + Ok(()) + } + + fn claim(&mut self, id: u64, kind: OwnedKind) -> LeafResult<()> { + if id <= self.ticket { + return Err(INVALID_IMPORT); + } + self.seen.try_reserve(1).map_err(|_| EXHAUSTED)?; + if !self.seen.insert(id) { + return Err(INVALID_IMPORT); + } + self.owned.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.owned.push((id, kind)); + Ok(()) + } + + fn string(&mut self, id: u64) -> LeafResult<()> { + if id <= self.ticket || !self.values.strings.contains_key(&id) { + return Err(INVALID_IMPORT); + } + if let Some(edges) = self.string_edges.get_mut(&id) { + *edges = edges.checked_add(1).ok_or(EXHAUSTED)?; + } else { + self.string_edges.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.claim(id, OwnedKind::String)?; + self.string_edges.insert(id, 1); + } + Ok(()) + } + + fn record(&mut self, id: u64) -> LeafResult<&'a NativeStruct> { + if self.values.actors.contains_key(&id) || self.values.equalities.contains_key(&id) { + return Err(INVALID_IMPORT); + } + self.claim(id, OwnedKind::Struct)?; + self.values.structs.get(&id).ok_or(INVALID_IMPORT) + } + + fn actor(&self, field: NativeField) -> LeafResult<()> { + if field.owned || field.pending_depth != 0 { + return Err(INVALID_IMPORT); + } + if self.import.records.contains_key(&field.bits) { + return Ok(()); + } + let (base, depth) = self.import.pending.get(&field.bits).ok_or(INVALID_IMPORT)?; + if *depth == 0 || !self.import.records.contains_key(base) { + return Err(INVALID_IMPORT); + } + Ok(()) + } + + fn validate(mut self, root: NativeField) -> LeafResult> { + self.push(self.layout.debug.root, root)?; + while let Some((index, field)) = self.work.pop() { + let index = self.layout.resolve(index)?; + if field.owned != self.layout.owns(index)? || (field.owned && field.pending_depth != 0) + { + return Err(INVALID_IMPORT); + } + self.value(index, field)?; + } + // Immutable strings may have internal owning aliases (for example an + // adapter and its captured source label). No unvisited owner may remain. + for (&id, &edges) in &self.string_edges { + if self + .values + .strings + .get(&id) + .ok_or(INVALID_IMPORT)? + .references + != edges + { + return Err(INVALID_IMPORT); + } + } + Ok(self.owned) + } + + fn value(&mut self, index: usize, field: NativeField) -> LeafResult<()> { + let bits = field.bits; + let layout = self.layout; + let values = self.values; + match layout.debug.nodes.get(index).ok_or(INVALID_IMPORT)? { + NativeDebugNode::Primitive(kind) => { + let kind = NativeSortKind::from_raw(*kind).map_err(|_| INVALID_IMPORT)?; + if !kind.valid_bits(bits) { + return Err(INVALID_IMPORT); + } + if matches!(kind, NativeSortKind::String) { + self.string(bits)?; + } + } + NativeDebugNode::Nothing => {} + NativeDebugNode::Actor => self.actor(field)?, + NativeDebugNode::Bytes => { + self.values.bytes.get(&bits).ok_or(INVALID_IMPORT)?; + self.claim(bits, OwnedKind::Bytes)?; + } + NativeDebugNode::List(child) | NativeDebugNode::Set(child) => { + self.sequence(index, bits, *child)? + } + NativeDebugNode::Map(key, value) => self.map(index, bits, *key, *value)?, + NativeDebugNode::Optional(child) | NativeDebugNode::Result(child, _) => { + let sum = values.sums.get(&bits).ok_or(INVALID_IMPORT)?; + if sum.tag > SUM_SUCCESS { + return Err(INVALID_IMPORT); + } + self.claim(bits, OwnedKind::Sum)?; + if sum.tag == SUM_FAILURE + && matches!(self.layout.debug.nodes[index], NativeDebugNode::Optional(_)) + { + if sum.owned || sum.bits != 0 || sum.payload_pending_depth != 0 { + return Err(INVALID_IMPORT); + } + } else { + let child = match &self.layout.debug.nodes[index] { + NativeDebugNode::Result(_, fail) if sum.tag == SUM_FAILURE => *fail, + _ => *child, + }; + self.push( + child, + NativeField { + bits: sum.bits, + owned: sum.owned, + pending_depth: sum.payload_pending_depth, + }, + )?; + } + } + NativeDebugNode::Record(_, fields) => { + let record = self.record(bits)?; + if record.fields.len() != fields.len() || self.values.builders.contains_key(&bits) { + return Err(INVALID_IMPORT); + } + let fields_to_visit = record.fields.clone(); + for ((_, child), field) in fields.iter().zip(fields_to_visit) { + self.push(*child, field.ok_or(INVALID_IMPORT)?)?; + } + } + NativeDebugNode::Enum(_, variants) | NativeDebugNode::Machine(_, variants) => { + let record = self.record(bits)?; + let tag = record + .fields + .first() + .copied() + .flatten() + .ok_or(INVALID_IMPORT)?; + if tag.owned || tag.pending_depth != 0 { + return Err(INVALID_IMPORT); + } + let tag = usize::try_from(tag.bits).map_err(|_| INVALID_IMPORT)?; + let (_, fields) = variants.get(tag).ok_or(INVALID_IMPORT)?; + if record.fields.len() != fields.len() + 1 { + return Err(INVALID_IMPORT); + } + let fields_to_visit = record.fields[1..].to_vec(); + for (child, field) in fields.iter().zip(fields_to_visit) { + self.push(*child, field.ok_or(INVALID_IMPORT)?)?; + } + } + NativeDebugNode::Function => self.function(index, bits)?, + NativeDebugNode::Interface => self.interface(index, bits)?, + NativeDebugNode::TypeConstruction => self.builder(index, bits)?, + NativeDebugNode::Capability(_) + | NativeDebugNode::Redacted + | NativeDebugNode::Uninhabited + | NativeDebugNode::Alias(_) => return Err(INVALID_IMPORT), + } + Ok(()) + } + + fn sequence(&mut self, index: usize, bits: u64, child: usize) -> LeafResult<()> { + let values = self.values; + let layout = self.layout; + let owned = self.layout.owns(child)?; + let (elements, depths) = match &self.layout.debug.nodes[index] { + NativeDebugNode::List(_) => { + let list = values.lists.get(&bits).ok_or(INVALID_IMPORT)?; + if list.owned != owned { + return Err(INVALID_IMPORT); + } + self.claim(bits, OwnedKind::List)?; + (&list.elements, &list.element_pending_depths) + } + _ => { + let set = values.sets.get(&bits).ok_or(INVALID_IMPORT)?; + if set.strings != self.layout.key_strings(child)? { + return Err(INVALID_IMPORT); + } + self.claim(bits, OwnedKind::Set)?; + (&set.elements, &set.element_pending_depths) + } + }; + if depths.keys().any(|index| *index >= elements.len()) || (owned && !depths.is_empty()) { + return Err(INVALID_IMPORT); + } + let concrete = match layout.supplements.get(&index) { + Some(Supplement::ListEntries(children)) if children.len() == elements.len() => { + Some(children) + } + Some(Supplement::ListEntries(_)) => return Err(INVALID_IMPORT), + None => None, + _ => return Err(INVALID_IMPORT), + }; + for (position, bits) in elements.iter().enumerate() { + let child = concrete.map_or(child, |children| children[position]); + self.push( + child, + NativeField { + bits: bits.ok_or(INVALID_IMPORT)?, + owned, + pending_depth: depths.get(&position).copied().unwrap_or(0), + }, + )?; + } + Ok(()) + } + + fn map(&mut self, index: usize, bits: u64, key: usize, value: usize) -> LeafResult<()> { + let values = self.values; + let layout = self.layout; + let map = values.maps.get(&bits).ok_or(INVALID_IMPORT)?; + if map.key_strings != self.layout.key_strings(key)? + || map.value_owned != self.layout.owns(value)? + { + return Err(INVALID_IMPORT); + } + self.claim(bits, OwnedKind::Map)?; + let concrete = match layout.supplements.get(&index) { + Some(Supplement::MapEntries(children)) if children.len() == map.entries.len() => { + Some(children) + } + Some(Supplement::MapEntries(_)) => return Err(INVALID_IMPORT), + None => None, + _ => return Err(INVALID_IMPORT), + }; + for (position, entry) in map.entries.iter().enumerate() { + let entry = entry.ok_or(INVALID_IMPORT)?; + if entry.key_taken { + return Err(INVALID_IMPORT); + } + let (key, value) = concrete.map_or((key, value), |children| children[position]); + if layout.key_strings(key)? != map.key_strings { + return Err(INVALID_IMPORT); + } + self.push( + key, + NativeField { + bits: entry.key, + owned: map.key_strings, + pending_depth: entry.key_pending_depth, + }, + )?; + self.push( + value, + NativeField { + bits: entry.value, + owned: map.value_owned, + pending_depth: entry.value_pending_depth, + }, + )?; + } + Ok(()) + } + + fn function(&mut self, index: usize, bits: u64) -> LeafResult<()> { + let layout = self.layout; + if let Some(Supplement::CheckedHook { + descriptor_operation, + hook, + signature, + }) = layout.supplements.get(&index) + { + let value = self + .values + .checked_graph_hooks + .get(&bits) + .ok_or(INVALID_IMPORT)?; + if !value.matches_factory(self.ticket, *descriptor_operation, *hook, *signature) { + return Err(INVALID_IMPORT); + } + return self.claim(bits, OwnedKind::CheckedHook); + } + let Supplement::Function { code, environment } = + layout.supplements.get(&index).ok_or(INVALID_IMPORT)? + else { + return Err(INVALID_IMPORT); + }; + let record = self.record(bits)?; + if record.fields.len() != 3 { + return Err(INVALID_IMPORT); + } + let code_field = record.fields[0].ok_or(INVALID_IMPORT)?; + let env = record.fields[1].ok_or(INVALID_IMPORT)?; + let label = record.fields[2].ok_or(INVALID_IMPORT)?; + if code_field.owned + || code_field.pending_depth != 0 + || code_field.bits != *code + || !label.owned + || label.pending_depth != 0 + { + return Err(INVALID_IMPORT); + } + self.values + .function_debug_label(bits) + .map_err(|_| INVALID_IMPORT)?; + match environment { + Some(child) => self.push(*child, env)?, + None if env.bits == 0 && !env.owned && env.pending_depth == 0 => {} + None => return Err(INVALID_IMPORT), + } + self.string(label.bits) + } + + fn interface(&mut self, index: usize, bits: u64) -> LeafResult<()> { + let layout = self.layout; + let Supplement::Interface { concrete, child } = + layout.supplements.get(&index).ok_or(INVALID_IMPORT)? + else { + return Err(INVALID_IMPORT); + }; + let record = self.record(bits)?; + if record.fields.len() != 3 { + return Err(INVALID_IMPORT); + } + let type_id = record.fields[0].ok_or(INVALID_IMPORT)?; + let value = record.fields[1].ok_or(INVALID_IMPORT)?; + let schema = record.fields[2].ok_or(INVALID_IMPORT)?; + if type_id.owned + || type_id.pending_depth != 0 + || type_id.bits != *concrete + || !schema.owned + || schema.pending_depth != 0 + { + return Err(INVALID_IMPORT); + } + NativeDebugLayout::parse(self.values.bytes.get(&schema.bits).ok_or(INVALID_IMPORT)?) + .map_err(|_| INVALID_IMPORT)?; + self.claim(schema.bits, OwnedKind::Bytes)?; + self.push(*child, value) + } + + fn builder(&mut self, index: usize, bits: u64) -> LeafResult<()> { + let layout = self.layout; + let values = self.values; + let Supplement::Builder { + owner, + offset, + fields, + } = layout.supplements.get(&index).ok_or(INVALID_IMPORT)? + else { + return Err(INVALID_IMPORT); + }; + let info = values.builders.get(&bits).ok_or(INVALID_IMPORT)?; + if info.owner != *owner + || info.field_offset != *offset + || info.field_names.len() != fields.len() + || info.field_debug_layouts.len() != fields.len() + || info.unsupported_kind.is_some() + { + return Err(INVALID_IMPORT); + } + let record = self.record(bits)?; + if record.fields.len() != fields.len().checked_add(*offset).ok_or(INVALID_IMPORT)? { + return Err(INVALID_IMPORT); + } + for prefix in &record.fields[..*offset] { + let prefix = prefix.ok_or(INVALID_IMPORT)?; + if prefix.owned || prefix.pending_depth != 0 { + return Err(INVALID_IMPORT); + } + } + let mut occupied = HashSet::new(); + occupied + .try_reserve(info.put_order.len()) + .map_err(|_| EXHAUSTED)?; + for index in &info.put_order { + if *index >= fields.len() || !occupied.insert(*index) { + return Err(INVALID_IMPORT); + } + } + let provided = record.fields[*offset..].to_vec(); + for (index, (child, field)) in fields.iter().zip(provided).enumerate() { + if field.is_some() != occupied.contains(&index) { + return Err(INVALID_IMPORT); + } + NativeDebugLayout::parse(&info.field_debug_layouts[index]) + .map_err(|_| INVALID_IMPORT)?; + if let Some(field) = field { + self.push(*child, field)?; + } + } + Ok(()) + } +} + +impl NativeValues { + pub(super) fn begin_actor_graph(&mut self) -> LeafResult { + // One sequential evaluator activation owns one unfinished context import. + if !self.actor_graph_imports.is_empty() { + return Err(INVALID_IMPORT); + } + #[cfg(test)] + self.allocation_checkpoint()?; + self.actor_graph_imports + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + let ticket = next_identity()?; + self.actor_graph_imports.insert( + ticket, + Import { + bases: Vec::new(), + records: HashMap::new(), + pending: HashMap::new(), + ordinary: NativeValues::default(), + roots: Vec::new(), + output: None, + }, + ); + Ok(ticket) + } + + pub(super) fn reserve_actor_graph_base(&mut self, ticket: u64, count: u64) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let count = usize::try_from(count).map_err(|_| EXHAUSTED)?; + let import = self + .actor_graph_imports + .get_mut(&ticket) + .ok_or(INVALID_IMPORT)?; + import.records.try_reserve(1).map_err(|_| EXHAUSTED)?; + import.bases.try_reserve(1).map_err(|_| EXHAUSTED)?; + let mut fields = Vec::new(); + fields.try_reserve_exact(count).map_err(|_| EXHAUSTED)?; + fields.resize(count, None); + let created = self.structs_created.checked_add(1).ok_or(EXHAUSTED)?; + let id = next_identity()?; + import.records.insert( + id, + NativeStruct { + fields, + pending_depth: 0, + }, + ); + import.bases.push(id); + self.structs_created = created; + Ok(id) + } + + pub(super) fn reserve_actor_graph_pending( + &mut self, + ticket: u64, + base: u64, + depth: u64, + ) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let import = self + .actor_graph_imports + .get_mut(&ticket) + .ok_or(INVALID_IMPORT)?; + if depth == 0 || !import.records.contains_key(&base) { + return Err(INVALID_IMPORT); + } + import.pending.try_reserve(1).map_err(|_| EXHAUSTED)?; + let created = self.structs_created.checked_add(1).ok_or(EXHAUSTED)?; + let id = next_identity()?; + import.pending.insert(id, (base, depth)); + self.structs_created = created; + Ok(id) + } + + pub(super) fn fill_actor_graph( + &mut self, + ticket: u64, + base: u64, + index: u64, + field: NativeField, + bytes: &[u8], + ) -> LeafResult { + let index = usize::try_from(index).map_err(|_| INVALID_IMPORT)?; + let import = self + .actor_graph_imports + .get(&ticket) + .ok_or(INVALID_IMPORT)?; + let slot = import + .records + .get(&base) + .and_then(|record| record.fields.get(index)) + .ok_or(INVALID_IMPORT)?; + if slot.is_some() { + return Err(INVALID_IMPORT); + } + let owned = self.prepare_actor_graph_tree(ticket, field, bytes)?; + // All validation and allocations precede the first transfer. + let mut import = self + .actor_graph_imports + .remove(&ticket) + .ok_or(INVALID_IMPORT)?; + let slot = match import + .records + .get_mut(&base) + .and_then(|record| record.fields.get_mut(index)) + { + Some(slot) => slot, + None => { + self.actor_graph_imports.insert(ticket, import); + return Err(INVALID_IMPORT); + } + }; + transfer_tree(self, &mut import.ordinary, &mut import.roots, field, owned); + *slot = Some(field); + self.actor_graph_imports.insert(ticket, import); + Ok(0) + } + + pub(super) fn fill_actor_graph_root( + &mut self, + ticket: u64, + field: NativeField, + bytes: &[u8], + ) -> LeafResult { + if self + .actor_graph_imports + .get(&ticket) + .ok_or(INVALID_IMPORT)? + .output + .is_some() + { + return Err(INVALID_IMPORT); + } + let owned = self.prepare_actor_graph_tree(ticket, field, bytes)?; + let mut import = self + .actor_graph_imports + .remove(&ticket) + .ok_or(INVALID_IMPORT)?; + transfer_tree(self, &mut import.ordinary, &mut import.roots, field, owned); + import.output = Some(field); + self.actor_graph_imports.insert(ticket, import); + Ok(0) + } + + fn prepare_actor_graph_tree( + &mut self, + ticket: u64, + field: NativeField, + bytes: &[u8], + ) -> LeafResult> { + let layout = ImportLayout::parse(bytes)?; + let import = self + .actor_graph_imports + .get(&ticket) + .ok_or(INVALID_IMPORT)?; + let owned = Validator { + values: self, + import, + ticket, + layout: &layout, + work: Vec::new(), + owned: Vec::new(), + seen: HashSet::new(), + string_edges: HashMap::new(), + } + .validate(field)?; + #[cfg(test)] + self.allocation_checkpoint()?; + let import = self + .actor_graph_imports + .get_mut(&ticket) + .ok_or(INVALID_IMPORT)?; + reserve_transferred_capacity(&mut import.ordinary, &owned)?; + if field.owned { + import.roots.try_reserve(1).map_err(|_| EXHAUSTED)?; + } + Ok(owned) + } + + pub(super) fn commit_actor_graph(&mut self, ticket: u64) -> LeafResult { + let import = self + .actor_graph_imports + .get(&ticket) + .ok_or(INVALID_IMPORT)?; + if (import.bases.is_empty() && import.output.is_none()) + || import + .records + .values() + .any(|record| record.fields.iter().any(Option::is_none)) + { + return Err(INVALID_IMPORT); + } + let count = u64::try_from(import.bases.len()).map_err(|_| EXHAUSTED)?; + let next_ordinal = self + .next_actor_ordinal + .checked_add(count) + .ok_or(EXHAUSTED)?; + let count = import + .records + .len() + .checked_add(import.pending.len()) + .ok_or(EXHAUSTED)?; + let records = count + .checked_add(import.ordinary.structs.len()) + .ok_or(EXHAUSTED)?; + let mut aliases = Vec::new(); + aliases + .try_reserve_exact(import.pending.len()) + .map_err(|_| EXHAUSTED)?; + for (&id, &(base, depth)) in &import.pending { + let offset = import + .bases + .iter() + .position(|candidate| *candidate == base) + .ok_or(INVALID_IMPORT)?; + if depth == 0 { + return Err(INVALID_IMPORT); + } + aliases.push(( + id, + NativeActorIdentity { + instance: base, + ordinal: self.next_actor_ordinal + offset as u64, + }, + depth, + )); + } + #[cfg(test)] + self.allocation_checkpoint()?; + self.structs.try_reserve(records).map_err(|_| EXHAUSTED)?; + self.actors.try_reserve(count).map_err(|_| EXHAUSTED)?; + reserve_publication_capacity(self, ticket)?; + // The leaf holds the context state mutex: no fallible work after removal. + let import = self + .actor_graph_imports + .remove(&ticket) + .ok_or(INVALID_IMPORT)?; + let first_ordinal = self.next_actor_ordinal; + for (offset, id) in import.bases.iter().copied().enumerate() { + self.actors.insert( + id, + NativeActorIdentity { + instance: id, + ordinal: first_ordinal + offset as u64, + }, + ); + } + for (id, identity, depth) in aliases { + self.actors.insert(id, identity); + self.structs.insert( + id, + NativeStruct { + fields: Vec::new(), + pending_depth: depth, + }, + ); + } + self.structs.extend(import.records); + publish_ordinary(self, import.ordinary); + self.next_actor_ordinal = next_ordinal; + Ok(0) + } + + pub(super) fn abort_actor_graph(&mut self, ticket: u64) -> LeafResult { + let mut import = self + .actor_graph_imports + .remove(&ticket) + .ok_or(INVALID_IMPORT)?; + let mut first_failure = None; + for root in import.roots { + if let Err(error) = import.ordinary.drop_value(root) { + first_failure.get_or_insert(error); + } + } + self.structs_destroyed += import.records.len() as u64 + import.pending.len() as u64; + self.structs_destroyed += import.ordinary.structs_destroyed; + self.lists_destroyed += import.ordinary.lists_destroyed; + self.sets_destroyed += import.ordinary.sets_destroyed; + self.maps_destroyed += import.ordinary.maps_destroyed; + self.sums_destroyed += import.ordinary.sums_destroyed; + self.bytes_destroyed += import.ordinary.bytes_destroyed; + self.checked_graph_hooks_destroyed += import.ordinary.checked_graph_hooks_destroyed; + if !import.ordinary.registries_empty() { + first_failure.get_or_insert(INVALID_IMPORT); + } + first_failure.map_or(Ok(0), Err) + } + + pub(super) fn abort_unfinished_actor_graphs(&mut self) -> LeafResult<()> { + let mut first_failure = None; + while let Some(&ticket) = self.actor_graph_imports.keys().next() { + if let Err(error) = self.abort_actor_graph(ticket) { + first_failure.get_or_insert(error); + } + } + first_failure.map_or(Ok(()), Err) + } + + fn registries_empty(&self) -> bool { + self.strings.is_empty() + && self.bytes.is_empty() + && self.bytes_pending.is_empty() + && self.sums.is_empty() + && self.lists.is_empty() + && self.sets.is_empty() + && self.maps.is_empty() + && self.structs.is_empty() + && self.builders.is_empty() + && self.actors.is_empty() + && self.equalities.is_empty() + && self.actor_graph_imports.is_empty() + && self.checked_graph_hooks.is_empty() + } +} + +fn reserve_transferred_capacity( + values: &mut NativeValues, + owned: &[(u64, OwnedKind)], +) -> LeafResult<()> { + let count = |predicate: fn(OwnedKind) -> bool| { + owned.iter().filter(|(_, kind)| predicate(*kind)).count() + }; + values + .checked_graph_hooks + .try_reserve(count(|k| matches!(k, OwnedKind::CheckedHook))) + .map_err(|_| EXHAUSTED)?; + values + .strings + .try_reserve(count(|k| matches!(k, OwnedKind::String))) + .map_err(|_| EXHAUSTED)?; + let bytes = count(|k| matches!(k, OwnedKind::Bytes)); + values.bytes.try_reserve(bytes).map_err(|_| EXHAUSTED)?; + values + .bytes_pending + .try_reserve(bytes) + .map_err(|_| EXHAUSTED)?; + let records = count(|k| matches!(k, OwnedKind::Struct)); + values.structs.try_reserve(records).map_err(|_| EXHAUSTED)?; + values + .builders + .try_reserve(records) + .map_err(|_| EXHAUSTED)?; + values + .lists + .try_reserve(count(|k| matches!(k, OwnedKind::List))) + .map_err(|_| EXHAUSTED)?; + values + .sets + .try_reserve(count(|k| matches!(k, OwnedKind::Set))) + .map_err(|_| EXHAUSTED)?; + values + .maps + .try_reserve(count(|k| matches!(k, OwnedKind::Map))) + .map_err(|_| EXHAUSTED)?; + values + .sums + .try_reserve(count(|k| matches!(k, OwnedKind::Sum))) + .map_err(|_| EXHAUSTED)?; + Ok(()) +} + +fn transfer_tree( + source: &mut NativeValues, + destination: &mut NativeValues, + roots: &mut Vec, + field: NativeField, + owned: Vec<(u64, OwnedKind)>, +) { + for (id, kind) in owned { + transfer_value(source, destination, id, kind); + } + if field.owned { + roots.push(field.bits); + } +} + +fn transfer_value( + source: &mut NativeValues, + destination: &mut NativeValues, + id: u64, + kind: OwnedKind, +) { + match kind { + OwnedKind::CheckedHook => { + if let Some(value) = source.checked_graph_hooks.remove(&id) { + destination.checked_graph_hooks.insert(id, value); + } + } + OwnedKind::String => { + if let Some(value) = source.strings.remove(&id) { + destination.strings.insert(id, value); + } + } + OwnedKind::Bytes => { + if let Some(value) = source.bytes.remove(&id) { + destination.bytes.insert(id, value); + } + if let Some(depth) = source.bytes_pending.remove(&id) { + destination.bytes_pending.insert(id, depth); + } + } + OwnedKind::Struct => { + if let Some(value) = source.structs.remove(&id) { + destination.structs.insert(id, value); + } + if let Some(info) = source.builders.remove(&id) { + destination.builders.insert(id, info); + } + } + OwnedKind::List => { + if let Some(value) = source.lists.remove(&id) { + destination.lists.insert(id, value); + } + } + OwnedKind::Set => { + if let Some(value) = source.sets.remove(&id) { + destination.sets.insert(id, value); + } + } + OwnedKind::Map => { + if let Some(value) = source.maps.remove(&id) { + destination.maps.insert(id, value); + } + } + OwnedKind::Sum => { + if let Some(value) = source.sums.remove(&id) { + destination.sums.insert(id, value); + } + } + } +} + +fn reserve_publication_capacity(values: &mut NativeValues, ticket: u64) -> LeafResult<()> { + let import = values + .actor_graph_imports + .get(&ticket) + .ok_or(INVALID_IMPORT)?; + let staged = &import.ordinary; + values + .checked_graph_hooks + .try_reserve(staged.checked_graph_hooks.len()) + .map_err(|_| EXHAUSTED)?; + values + .strings + .try_reserve(staged.strings.len()) + .map_err(|_| EXHAUSTED)?; + values + .bytes + .try_reserve(staged.bytes.len()) + .map_err(|_| EXHAUSTED)?; + values + .bytes_pending + .try_reserve(staged.bytes_pending.len()) + .map_err(|_| EXHAUSTED)?; + values + .builders + .try_reserve(staged.builders.len()) + .map_err(|_| EXHAUSTED)?; + values + .lists + .try_reserve(staged.lists.len()) + .map_err(|_| EXHAUSTED)?; + values + .sets + .try_reserve(staged.sets.len()) + .map_err(|_| EXHAUSTED)?; + values + .maps + .try_reserve(staged.maps.len()) + .map_err(|_| EXHAUSTED)?; + values + .sums + .try_reserve(staged.sums.len()) + .map_err(|_| EXHAUSTED)?; + Ok(()) +} + +fn publish_ordinary(values: &mut NativeValues, mut staged: NativeValues) { + for value in staged.checked_graph_hooks.values_mut() { + value.publish(); + } + values + .checked_graph_hooks + .extend(staged.checked_graph_hooks); + values.strings.extend(staged.strings); + values.bytes.extend(staged.bytes); + values.bytes_pending.extend(staged.bytes_pending); + values.builders.extend(staged.builders); + values.structs.extend(staged.structs); + values.lists.extend(staged.lists); + values.sets.extend(staged.sets); + values.maps.extend(staged.maps); + values.sums.extend(staged.sums); +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/values/actor_graph_import/tests.rs b/crates/jett_runtime/src/native_abi/values/actor_graph_import/tests.rs new file mode 100644 index 00000000..86d07437 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/actor_graph_import/tests.rs @@ -0,0 +1,1297 @@ +//! Authored static candidate tests; execution remains an independent gate. +use super::*; + +fn raw(bits: u64) -> NativeField { + NativeField { + bits, + owned: false, + pending_depth: 0, + } +} +fn owned(bits: u64) -> NativeField { + NativeField { + bits, + owned: true, + pending_depth: 0, + } +} +fn word(value: u32) -> Vec { + value.to_le_bytes().to_vec() +} +fn node(tag: u8, payload: &[u8]) -> Vec { + let mut result = vec![tag]; + result.extend_from_slice(payload); + result +} +fn name(value: &str) -> Vec { + let mut result = word(value.len() as u32); + result.extend_from_slice(value.as_bytes()); + result +} +fn debug(root: u32, nodes: &[Vec]) -> Vec { + let mut result = b"JD\x01".to_vec(); + result.extend(word(nodes.len() as u32)); + result.extend(word(root)); + for node in nodes { + result.extend(word(node.len() as u32)); + result.extend(node); + } + result +} +fn supplement(index: u32, kind: u8, payload: &[u8]) -> Vec { + let mut result = word(index); + result.push(kind); + result.extend_from_slice(payload); + result +} +fn schema(root: u32, nodes: &[Vec], supplements: &[Vec]) -> Vec { + let debug = debug(root, nodes); + let mut result = b"JA\x01".to_vec(); + result.extend(word(debug.len() as u32)); + result.extend(debug); + result.extend(word(supplements.len() as u32)); + for supplement in supplements { + result.extend(supplement); + } + result +} +fn actor_schema() -> Vec { + schema(0, &[node(14, &[])], &[]) +} +fn string_schema() -> Vec { + schema(0, &[node(0, &word(11))], &[]) +} +fn record_node(children: &[u32]) -> Vec { + let mut payload = name("Environment"); + payload.extend(word(children.len() as u32)); + for (index, child) in children.iter().enumerate() { + payload.extend(name(&format!("capture{index}"))); + payload.extend(word(*child)); + } + node(8, &payload) +} +fn function_schema(code: u64, child: u32, captured: Vec) -> Vec { + let mut payload = code.to_le_bytes().to_vec(); + payload.extend(word(1)); + schema( + 0, + &[node(13, &[]), record_node(&[child]), captured], + &[supplement(0, 1, &payload)], + ) +} +fn descriptor(values: &mut NativeValues, code: u64, captured: NativeField) -> (u64, u64, u64) { + let environment = values.new_struct(1).unwrap(); + values.structs.get_mut(&environment).unwrap().fields[0] = Some(captured); + let label = values.insert("function(candidate)".into()).unwrap(); + let descriptor = values.new_struct(3).unwrap(); + values.structs.get_mut(&descriptor).unwrap().fields = vec![ + Some(raw(code)), + Some(owned(environment)), + Some(owned(label)), + ]; + (descriptor, environment, label) +} + +#[test] +fn graph_base_cycles_and_pending_wrappers_publish_together_without_dispatch() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let first = values.reserve_actor_graph_base(ticket, 2).unwrap(); + let second = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let pending = values + .reserve_actor_graph_pending(ticket, second, 3) + .unwrap(); + for handle in [first, second, pending] { + assert!(values.actor_identity_parts(handle).is_err()); + assert!(values.check_actor_message(handle).is_err()); + assert!(values.struct_field(handle, 0).is_err()); + } + let actor = actor_schema(); + values + .fill_actor_graph(ticket, first, 0, raw(second), &actor) + .unwrap(); + values + .fill_actor_graph(ticket, first, 1, raw(pending), &actor) + .unwrap(); + values + .fill_actor_graph(ticket, second, 0, raw(first), &actor) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.struct_field(first, 0).unwrap().bits, second); + assert_eq!(values.struct_field(second, 0).unwrap().bits, first); + let (identity, depth) = values.actor_identity_parts(pending).unwrap(); + assert_eq!((identity.instance, depth), (second, 3)); + let another = values.run_actor(pending).unwrap(); + assert_eq!(values.actor_identity_parts(another).unwrap().1, 4); + assert_eq!(values.actor_identity_parts(pending).unwrap().1, 3); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn abort_isolates_new_ownership_and_preserves_older_actors_and_failure() { + let mut values = NativeValues::default(); + let older = values.new_struct(0).unwrap(); + values.register_actor(older).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + values.reserve_actor_graph_pending(ticket, base, 2).unwrap(); + let text = values.insert("fresh".into()).unwrap(); + let list = values.new_list(true).unwrap(); + values + .lists + .get_mut(&list) + .unwrap() + .elements + .push(Some(text)); + let layout = schema(0, &[node(3, &word(1)), node(0, &word(11))], &[]); + values + .fill_actor_graph(ticket, base, 0, owned(list), &layout) + .unwrap(); + assert!(!values.lists.contains_key(&list)); + assert!(!values.strings.contains_key(&text)); + assert!(values.clone_value(list).is_err()); + values.record_ordinary_failure((JettRuntimeStatusV1::INVALID_ARGUMENT, b"original failure")); + let prior = values.current_failure(); + values.abort_actor_graph(ticket).unwrap(); + assert_eq!(values.current_failure(), prior); + assert!(!values.cleanup_failed); + assert!(values.actor_identity_parts(older).is_ok()); + assert_eq!(values.next_actor_ordinal, 1); + assert_eq!(values.structs_created - values.structs_destroyed, 1); + assert_eq!(values.lists_created, values.lists_destroyed); + assert!(values.actor_graph_imports.is_empty()); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn tickets_foreign_older_owned_actor_and_duplicate_fills_refuse() { + let mut values = NativeValues::default(); + let mut foreign = NativeValues::default(); + let older = values.new_struct(0).unwrap(); + values.register_actor(older).unwrap(); + let foreign_ticket = foreign.begin_actor_graph().unwrap(); + let alien = foreign.reserve_actor_graph_base(foreign_ticket, 0).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let actor = actor_schema(); + assert_eq!( + values.reserve_actor_graph_base(foreign_ticket, 0), + Err(INVALID_IMPORT) + ); + assert_eq!( + values.reserve_actor_graph_pending(ticket, older, 1), + Err(INVALID_IMPORT) + ); + assert_eq!( + values.reserve_actor_graph_pending(ticket, base, 0), + Err(INVALID_IMPORT) + ); + for value in [older, alien] { + assert_eq!( + values.fill_actor_graph(ticket, base, 0, raw(value), &actor), + Err(INVALID_IMPORT) + ); + } + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(base), &actor), + Err(INVALID_IMPORT) + ); + values + .fill_actor_graph(ticket, base, 0, raw(base), &actor) + .unwrap(); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, raw(base), &actor), + Err(INVALID_IMPORT) + ); + values.abort_actor_graph(ticket).unwrap(); + assert_eq!(values.commit_actor_graph(ticket), Err(INVALID_IMPORT)); + assert_eq!(values.abort_actor_graph(ticket), Err(INVALID_IMPORT)); + foreign.abort_actor_graph(foreign_ticket).unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn incomplete_and_exhausted_commit_publish_nothing_and_abort_needs_no_allocation() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + assert_eq!(values.commit_actor_graph(ticket), Err(INVALID_IMPORT)); + let text = values.insert("ready".into()).unwrap(); + values + .fill_actor_graph(ticket, base, 0, owned(text), &string_schema()) + .unwrap(); + values.allocation_budget = Some(0); + assert_eq!(values.commit_actor_graph(ticket), Err(EXHAUSTED)); + assert!(values.actors.is_empty()); + assert!(!values.structs.contains_key(&base)); + values.abort_actor_graph(ticket).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn hidden_refinement_and_interface_children_preserve_staged_actor_reference() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let metadata = values.insert_bytes(debug(0, &[node(14, &[])])).unwrap(); + let interface = values.new_struct(3).unwrap(); + values.structs.get_mut(&interface).unwrap().fields = + vec![Some(raw(77)), Some(raw(base)), Some(owned(metadata))]; + let mut interface_proof = 77u64.to_le_bytes().to_vec(); + interface_proof.extend(word(3)); + let layout = schema( + 0, + &[ + node(16, &[]), + node(12, &word(2)), + node(17, &[]), + node(14, &[]), + ], + &[ + supplement(0, 0, &word(1)), + supplement(2, 2, &interface_proof), + ], + ); + values + .fill_actor_graph(ticket, base, 0, owned(interface), &layout) + .unwrap(); + assert!(!values.structs.contains_key(&interface)); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.struct_field(interface, 1).unwrap().bits, base); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn exact_closure_code_and_typed_captures_validate_before_owned_transfer() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let (callback, environment, label) = descriptor(&mut values, 123, raw(base)); + let incorrect = function_schema(124, 2, node(14, &[])); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(callback), &incorrect), + Err(INVALID_IMPORT) + ); + assert!(values.structs.contains_key(&callback)); + assert!(values.strings.contains_key(&label)); + let correct = function_schema(123, 2, node(14, &[])); + values + .fill_actor_graph(ticket, base, 0, owned(callback), &correct) + .unwrap(); + assert!(!values.structs.contains_key(&environment)); + assert!(values.function_debug_label(callback).is_err()); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!( + values.function_debug_label(callback).unwrap(), + "function(candidate)" + ); + assert_eq!(values.struct_field(environment, 0).unwrap().bits, base); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn nested_capability_resource_and_foreign_ordinary_owned_values_refuse() { + let mut values = NativeValues::default(); + let mut foreign = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let capability = values.grant_opaque_capability("Network").unwrap(); + let (callback, _, _) = descriptor(&mut values, 123, raw(capability)); + let layout = function_schema(123, 2, node(11, &name("Network"))); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(callback), &layout), + Err(INVALID_IMPORT) + ); + let resource = schema(0, &[node(16, &[])], &[supplement(0, 4, &[])]); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, raw(999), &resource), + Err(INVALID_IMPORT) + ); + let alien = foreign.insert("foreign".into()).unwrap(); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(alien), &string_schema()), + Err(INVALID_IMPORT) + ); + values.drop_value(callback).unwrap(); + values.abort_actor_graph(ticket).unwrap(); + foreign.drop_value(alien).unwrap(); + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn builder_metadata_keeps_exact_owner_and_partial_typed_field_lifecycle() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let builder = values.new_struct(2).unwrap(); + values.structs.get_mut(&builder).unwrap().fields[0] = Some(raw(base)); + values.builders.insert( + builder, + NativeBuilderInfo { + owner: "Packet".into(), + metadata_owner: "Packet".into(), + unsupported_kind: None, + variant: None, + state: None, + target_state: None, + field_offset: 0, + field_names: vec!["actor".into(), "label".into()], + field_type_names: vec!["Receiver".into(), "string".into()], + field_types: vec!["Receiver".into(), "string".into()], + field_debug_layouts: vec![debug(0, &[node(14, &[])]), debug(0, &[node(0, &word(11))])], + put_order: vec![0], + validation: vec![BuilderFieldValidation::None, BuilderFieldValidation::None], + }, + ); + let mut proof = name("Packet"); + proof.extend(word(0)); + proof.extend(word(2)); + proof.extend(word(1)); + proof.extend(word(2)); + let layout = schema( + 0, + &[node(15, &[]), node(14, &[]), node(0, &word(11))], + &[supplement(0, 3, &proof)], + ); + values + .fill_actor_graph(ticket, base, 0, owned(builder), &layout) + .unwrap(); + assert!(!values.builders.contains_key(&builder)); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.builders[&builder].put_order, [0]); + assert!(values.structs[&builder].fields[1].is_none()); + assert_eq!(values.struct_field(builder, 0).unwrap().bits, base); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn private_layout_refuses_duplicate_wrong_missing_out_of_range_and_cyclic_evidence() { + let hidden = node(16, &[]); + let valid = supplement(0, 0, &word(1)); + let malformed = [ + schema(0, &[hidden.clone(), node(14, &[])], &[valid.clone(), valid]), + schema(0, &[hidden.clone()], &[supplement(0, 0, &word(0))]), + schema(0, &[hidden], &[supplement(0, 0, &word(7))]), + schema(0, &[node(14, &[])], &[supplement(0, 0, &word(0))]), + schema(0, &[node(12, &word(1)), node(12, &word(0))], &[]), + ]; + for bytes in malformed { + assert!(ImportLayout::parse(&bytes).is_err()); + } + let missing = schema(0, &[node(16, &[])], &[]); + assert!(ImportLayout::parse(&missing).unwrap().owns(0).is_err()); + let mut trailing = actor_schema(); + trailing.push(0); + assert!(ImportLayout::parse(&trailing).is_err()); +} + +#[test] +fn repeated_factory_imports_are_fresh_and_context_teardown_aborts_unfinished_import() { + let mut values = NativeValues::default(); + let mut first = None; + for _ in 0..2 { + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 0).unwrap(); + values.commit_actor_graph(ticket).unwrap(); + if let Some(previous) = first { + assert_ne!(base, previous); + } + first = Some(base); + } + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let text = values.insert("unfinished".into()).unwrap(); + values + .fill_actor_graph(ticket, base, 0, owned(text), &string_schema()) + .unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); + assert_eq!(values.structs_created, values.structs_destroyed); +} + +#[test] +fn graph_leaf_scalar_schema_is_explicit_and_owner_actor_register_signature_is_unchanged() { + use AbiScalar::{I32, I64, Pointer}; + assert_eq!(NativeLeaf::ActorRegister.parameters(), &[Pointer, I64]); + assert_eq!(NativeLeaf::ActorGraphBegin.parameters(), &[Pointer]); + assert_eq!( + NativeLeaf::ActorGraphReserveBase.parameters(), + &[Pointer, I64, I64] + ); + assert_eq!( + NativeLeaf::ActorGraphReservePending.parameters(), + &[Pointer, I64, I64, I64] + ); + assert_eq!( + NativeLeaf::ActorGraphFill.parameters(), + &[Pointer, I64, I64, I64, I64, I32, I64, Pointer, I64] + ); + assert_eq!(NativeLeaf::ActorGraphCommit.parameters(), &[Pointer, I64]); + assert_eq!(NativeLeaf::ActorGraphAbort.parameters(), &[Pointer, I64]); + assert_eq!( + NativeLeaf::ActorGraphAbort.symbol(), + "jett_rt_v1_actor_graph_abort" + ); +} + +#[test] +fn ordinary_leaf_failure_automatically_aborts_and_allows_captured_error_reentry() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let older = unsafe { jett_rt_v1_struct_new(pointer, 1) }; + assert_eq!( + unsafe { jett_rt_v1_struct_init(pointer, older, 0, 17, 0) }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_actor_register(pointer, older) }, older); + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + let base = unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 1) }; + let text = b"owned"; + let value = unsafe { jett_rt_v1_string_literal(pointer, text.as_ptr(), text.len() as u64) }; + let schema = string_schema(); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_fill( + pointer, + ticket, + base, + 0, + value, + 1, + 0, + schema.as_ptr(), + schema.len() as u64, + ) + }, + 0 + ); + // Failed u32 leaves return the existing sentinel; value_status reports + // the separately retained first ordinary failure. + assert_eq!( + unsafe { jett_rt_v1_actor_message_check(pointer, base) }, + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + // The caller must not issue a stale explicit Abort after automatic rollback. + assert_eq!(unsafe { jett_rt_v1_actor_graph_begin(pointer) }, 0); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.actor_graph_imports.is_empty()); + assert!(!values.cleanup_failed); + assert!(values.actor_identity_parts(older).is_ok()); + assert_eq!(values.struct_field(older, 0).unwrap().bits, 17); + assert_eq!(values.structs_created - values.structs_destroyed, 1); + assert_eq!(values.current_failure().unwrap(), INVALID_ACTOR); + } + // Exercise the genuine handler capture contract, including its owned text. + let prefix = b"handler: "; + let captured = unsafe { + jett_rt_v1_failure_take_prefixed_text(pointer, prefix.as_ptr(), prefix.len() as u64) + }; + assert_ne!(captured, 0); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert_eq!( + values.text(captured).unwrap(), + "handler: invalid native actor handle or field" + ); + assert!(values.current_failure().is_none()); + } + assert_eq!(unsafe { jett_rt_v1_string_release(pointer, captured) }, 0); + let next_ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + assert!(next_ticket > ticket); + let next_base = unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, next_ticket, 0) }; + assert!(next_base > base); + assert_eq!( + unsafe { jett_rt_v1_actor_graph_commit(pointer, next_ticket) }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn actual_context_teardown_aborts_unfinished_import_without_an_ownership_leak() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + let base = unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 1) }; + let text = b"unfinished"; + let value = unsafe { jett_rt_v1_string_literal(pointer, text.as_ptr(), text.len() as u64) }; + let schema = string_schema(); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_fill( + pointer, + ticket, + base, + 0, + value, + 1, + 0, + schema.as_ptr(), + schema.len() as u64, + ) + }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn list_entry_schemas_validate_distinct_exact_callback_addresses_and_cardinality() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let mut callbacks = Vec::new(); + for code in [123, 456] { + let label = values.insert("function(element)".into()).unwrap(); + let callback = values.new_struct(3).unwrap(); + values.structs.get_mut(&callback).unwrap().fields = + vec![Some(raw(code)), Some(raw(0)), Some(owned(label))]; + callbacks.push(callback); + } + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = callbacks.iter().copied().map(Some).collect(); + // Node1 describes only the list element type; nodes2/3 carry actual code proofs. + let nodes = [ + node(3, &word(1)), + node(13, &[]), + node(13, &[]), + node(13, &[]), + ]; + let function_proof = |index, code: u64| { + let mut payload = code.to_le_bytes().to_vec(); + payload.extend(word(u32::MAX)); + supplement(index, 1, &payload) + }; + let mut entries = word(2); + entries.extend(word(2)); + entries.extend(word(3)); + let correct = schema( + 0, + &nodes, + &[ + supplement(0, 5, &entries), + function_proof(2, 123), + function_proof(3, 456), + ], + ); + let mut short = word(1); + short.extend(word(2)); + let incorrect = schema( + 0, + &nodes, + &[ + supplement(0, 5, &short), + function_proof(2, 123), + function_proof(3, 456), + ], + ); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(list), &incorrect), + Err(INVALID_IMPORT) + ); + assert!(values.lists.contains_key(&list)); + values + .fill_actor_graph(ticket, base, 0, owned(list), &correct) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.struct_field(callbacks[0], 0).unwrap().bits, 123); + assert_eq!(values.struct_field(callbacks[1], 0).unwrap().bits, 456); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn map_entry_schemas_validate_distinct_exact_interface_owners() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let other = values.reserve_actor_graph_base(ticket, 0).unwrap(); + let mut interfaces = Vec::new(); + for (concrete, actor) in [(77, base), (88, other)] { + let metadata = values.insert_bytes(debug(0, &[node(14, &[])])).unwrap(); + let interface = values.new_struct(3).unwrap(); + values.structs.get_mut(&interface).unwrap().fields = + vec![Some(raw(concrete)), Some(raw(actor)), Some(owned(metadata))]; + interfaces.push(interface); + } + let map = values.new_map(0, 1).unwrap(); + values.map_append_literal(map, 0, interfaces[0]).unwrap(); + values.map_append_literal(map, 1, interfaces[1]).unwrap(); + let mut map_children = word(1); + map_children.extend(word(2)); + let nodes = [ + node(5, &map_children), + node(0, &word(7)), + node(17, &[]), + node(17, &[]), + node(17, &[]), + node(14, &[]), + ]; + let proof = |index, concrete: u64| { + let mut payload = concrete.to_le_bytes().to_vec(); + payload.extend(word(5)); + supplement(index, 2, &payload) + }; + let mut entries = word(2); + entries.extend(word(1)); + entries.extend(word(3)); + entries.extend(word(1)); + entries.extend(word(4)); + let layout = schema( + 0, + &nodes, + &[supplement(0, 6, &entries), proof(3, 77), proof(4, 88)], + ); + values + .fill_actor_graph(ticket, base, 0, owned(map), &layout) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.struct_field(interfaces[0], 0).unwrap().bits, 77); + assert_eq!(values.struct_field(interfaces[1], 0).unwrap().bits, 88); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn failed_automatic_abort_poison_preserves_first_error_message_and_older_actor() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let older = unsafe { jett_rt_v1_struct_new(pointer, 1) }; + assert_eq!( + unsafe { jett_rt_v1_struct_init(pointer, older, 0, 17, 0) }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_actor_register(pointer, older) }, older); + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + let base = unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 1) }; + let text = b"rollback fault"; + let value = unsafe { jett_rt_v1_string_literal(pointer, text.as_ptr(), text.len() as u64) }; + let schema = string_schema(); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_fill( + pointer, + ticket, + base, + 0, + value, + 1, + 0, + schema.as_ptr(), + schema.len() as u64, + ) + }, + 0 + ); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + // Private test-only corruption forces the otherwise infallible typed drop + // to fail; no ordinary leaf can reach isolated transferred storage. + assert!( + state + .as_mut() + .unwrap() + .values + .actor_graph_imports + .get_mut(&ticket) + .unwrap() + .ordinary + .strings + .remove(&value) + .is_some() + ); + } + let failed: u32 = leaf(pointer, false, |values| { + values.set_dynamic_failure_message(b"primary sentinel".to_vec()); + Err(INVALID_ACTOR) + }); + assert_eq!(failed, JettRuntimeStatusV1::INVALID_CONTEXT.code()); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + // Cleanup poison forbids the genuine handler capture from clearing failure. + assert_eq!( + unsafe { jett_rt_v1_failure_take_prefixed_text(pointer, std::ptr::null(), 0) }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.actor_graph_imports.is_empty()); + assert!(values.cleanup_failed); + assert_eq!(values.current_failure(), Some(INVALID_ACTOR)); + assert_eq!( + values.current_dynamic_failure_message(), + Some(&b"primary sentinel"[..]) + ); + assert!(values.actor_identity_parts(older).is_ok()); + assert_eq!(values.struct_field(older, 0).unwrap().bits, 17); + assert_eq!(values.structs_created - values.structs_destroyed, 1); + } + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT + ); + let result = unsafe { result.assume_init() }; + let message = unsafe { + std::slice::from_raw_parts(result.message.data, result.message.byte_length as usize) + }; + assert_eq!(message, b"native value cleanup failed"); +} + +#[test] +fn successful_result_failure_value_keeps_import_active_and_publishes_normally() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + let base = unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 1) }; + let fail_value = unsafe { jett_rt_v1_sum_new(pointer, SUM_FAILURE, 42, 0) }; + assert_ne!(fail_value, 0); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + assert!( + state + .as_ref() + .unwrap() + .values + .actor_graph_imports + .contains_key(&ticket) + ); + } + let mut children = word(1); + children.extend(word(1)); + let schema = schema(0, &[node(7, &children), node(0, &word(7))], &[]); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_fill( + pointer, + ticket, + base, + 0, + fail_value, + 1, + 0, + schema.as_ptr(), + schema.len() as u64, + ) + }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_actor_graph_commit(pointer, ticket) }, 0); + assert_eq!( + unsafe { jett_rt_v1_sum_tag(pointer, fail_value) }, + SUM_FAILURE + ); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn mapped_native_panic_aborts_staging_before_original_status_is_observed() { + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + assert_ne!( + unsafe { jett_rt_v1_actor_graph_reserve_base(pointer, ticket, 0) }, + 0 + ); + let failed: u32 = leaf(pointer, false, |_| panic!("private native import fault")); + assert_eq!(failed, JettRuntimeStatusV1::INVALID_CONTEXT.code()); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::PANIC.code() + ); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.actor_graph_imports.is_empty()); + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + assert_eq!( + values.current_failure(), + Some((JettRuntimeStatusV1::PANIC, PANIC_MESSAGE)) + ); + } + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} + +#[test] +fn empty_list_and_map_placeholders_carry_no_capability_resource_or_never_authority() { + for placeholder in [node(11, &name("Network")), node(16, &[]), node(18, &[])] { + let is_resource = placeholder[0] == 16; + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 2).unwrap(); + let actual_proof = if is_resource { + vec![supplement(0, 4, &[])] + } else { + vec![] + }; + let actual = schema(0, &[placeholder.clone()], &actual_proof); + assert!(!ImportLayout::parse(&actual).unwrap().owns(0).unwrap()); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, raw(0), &actual), + Err(INVALID_IMPORT) + ); + let list = values.new_list(false).unwrap(); + let map = values.new_map(0, 0).unwrap(); + let list_proof = if is_resource { + vec![supplement(1, 4, &[])] + } else { + vec![] + }; + let list_schema = schema(0, &[node(3, &word(1)), placeholder.clone()], &list_proof); + let mut map_children = word(1); + map_children.extend(word(2)); + let map_proof = if is_resource { + vec![supplement(2, 4, &[])] + } else { + vec![] + }; + let map_schema = schema( + 0, + &[node(5, &map_children), node(0, &word(7)), placeholder], + &map_proof, + ); + values + .fill_actor_graph(ticket, base, 0, owned(list), &list_schema) + .unwrap(); + values + .fill_actor_graph(ticket, base, 1, owned(map), &map_schema) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert!(values.lists[&list].elements.is_empty()); + assert!(values.maps[&map].entries.is_empty()); + values.release_actors().unwrap(); + assert!(values.is_empty()); + } +} + +#[test] +fn inactive_optional_and_result_arms_do_not_visit_authority_or_never_placeholders() { + for placeholder in [node(11, &name("Network")), node(16, &[]), node(18, &[])] { + let is_resource = placeholder[0] == 16; + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 2).unwrap(); + let proof = if is_resource { + vec![supplement(1, 4, &[])] + } else { + vec![] + }; + let none = values.sum(SUM_FAILURE, 0, false).unwrap(); + let none_schema = schema(0, &[node(6, &word(1)), placeholder.clone()], &proof); + values + .fill_actor_graph(ticket, base, 0, owned(none), &none_schema) + .unwrap(); + let message = values.insert("ordinary fail".into()).unwrap(); + let fail = values.sum(SUM_FAILURE, message, true).unwrap(); + let mut children = word(1); + children.extend(word(2)); + let fail_schema = schema( + 0, + &[node(7, &children), placeholder, node(0, &word(11))], + &proof, + ); + values + .fill_actor_graph(ticket, base, 1, owned(fail), &fail_schema) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.sums[&none].tag, SUM_FAILURE); + assert_eq!(values.sums[&fail].bits, message); + values.release_actors().unwrap(); + assert!(values.is_empty()); + } +} + +#[test] +fn ordinary_function_adapter_internal_label_aliases_transfer_once_and_drop_per_edge() { + for publish in [false, true] { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let label = values.insert("function(source)".into()).unwrap(); + let source = values.new_struct(3).unwrap(); + values.structs.get_mut(&source).unwrap().fields = + vec![Some(raw(321)), Some(raw(0)), Some(owned(label))]; + let adapter = values.function_adapter(source, 654).unwrap(); + values.drop_value(source).unwrap(); + assert_eq!(values.strings[&label].references, 2); + let environment = values.struct_field(adapter, 1).unwrap().bits; + let captured = values.struct_field(environment, 0).unwrap().bits; + assert_eq!(values.struct_field(adapter, 2).unwrap().bits, label); + assert_eq!(values.struct_field(captured, 2).unwrap().bits, label); + let mut outer = 654u64.to_le_bytes().to_vec(); + outer.extend(word(1)); + let mut inner = 321u64.to_le_bytes().to_vec(); + inner.extend(word(u32::MAX)); + let layout = schema( + 0, + &[ + node(13, &[]), + record_node(&[2, 3]), + node(13, &[]), + node(0, &word(7)), + ], + &[supplement(0, 1, &outer), supplement(2, 1, &inner)], + ); + values + .fill_actor_graph(ticket, base, 0, owned(adapter), &layout) + .unwrap(); + assert!(!values.strings.contains_key(&label)); + let staged = &values.actor_graph_imports[&ticket].ordinary; + assert_eq!(staged.strings.len(), 1); + assert_eq!(staged.strings[&label].references, 2); + if publish { + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.strings[&label].references, 2); + values.release_actors().unwrap(); + } else { + values.abort_actor_graph(ticket).unwrap(); + } + assert!(values.is_empty()); + assert_eq!(values.structs_created, values.structs_destroyed); + } +} + +#[test] +fn unaccounted_external_older_foreign_strings_and_shared_mutable_owners_still_refuse() { + let mut values = NativeValues::default(); + let mut foreign = NativeValues::default(); + let older = values.insert("older".into()).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let base = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let fresh = values.insert("external alias".into()).unwrap(); + values.retain(fresh).unwrap(); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(fresh), &string_schema()), + Err(INVALID_IMPORT) + ); + assert_eq!(values.strings[&fresh].references, 2); + assert!( + values.actor_graph_imports[&ticket] + .ordinary + .strings + .is_empty() + ); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(older), &string_schema()), + Err(INVALID_IMPORT) + ); + let alien = foreign.insert("foreign".into()).unwrap(); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(alien), &string_schema()), + Err(INVALID_IMPORT) + ); + let list = values.new_list(false).unwrap(); + let record = values.new_struct(2).unwrap(); + values.structs.get_mut(&record).unwrap().fields = vec![Some(owned(list)), Some(owned(list))]; + let layout = schema( + 0, + &[record_node(&[1, 1]), node(3, &word(2)), node(0, &word(7))], + &[], + ); + assert_eq!( + values.fill_actor_graph(ticket, base, 0, owned(record), &layout), + Err(INVALID_IMPORT) + ); + assert!(values.structs.contains_key(&record)); + assert!(values.lists.contains_key(&list)); + // Repair only this deliberately malformed test input's duplicate owner. + values.structs.get_mut(&record).unwrap().fields[1] = None; + values.drop_value(record).unwrap(); + values.drop_value(fresh).unwrap(); + values.drop_value(fresh).unwrap(); + values.drop_value(older).unwrap(); + foreign.drop_value(alien).unwrap(); + values.abort_actor_graph(ticket).unwrap(); + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn graph_output_root_is_isolated_until_commit_without_a_dummy_actor_or_ordinal() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let actor = values.reserve_actor_graph_base(ticket, 0).unwrap(); + let text = values.insert("root".into()).unwrap(); + let root = values.new_struct(2).unwrap(); + values.structs.get_mut(&root).unwrap().fields = vec![Some(raw(actor)), Some(owned(text))]; + let layout = schema( + 0, + &[record_node(&[1, 2]), node(14, &[]), node(0, &word(11))], + &[], + ); + assert_eq!( + values.fill_actor_graph_root(ticket, owned(root), &layout), + Ok(0) + ); + assert!(!values.structs.contains_key(&root)); + assert!(!values.strings.contains_key(&text)); + assert!(values.struct_field(root, 0).is_err()); + assert!(values.actor_identity_parts(actor).is_err()); + let staged = &values.actor_graph_imports[&ticket]; + assert_eq!(staged.output.unwrap().bits, root); + assert!(staged.ordinary.structs.contains_key(&root)); + assert_eq!(values.next_actor_ordinal, 0); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.struct_field(root, 0).unwrap().bits, actor); + assert_eq!(values.text(text).unwrap(), "root"); + assert_eq!(values.actors.len(), 1); + assert!(!values.actors.contains_key(&root)); + assert_eq!(values.next_actor_ordinal, 1); + values.drop_value(root).unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn graph_output_only_import_publishes_owned_and_nonowned_roots_without_actor_identity() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let text = values.insert("output only".into()).unwrap(); + values + .fill_actor_graph_root(ticket, owned(text), &string_schema()) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.text(text).unwrap(), "output only"); + assert!(values.actors.is_empty()); + assert_eq!(values.next_actor_ordinal, 0); + values.drop_value(text).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let scalar = schema(0, &[node(0, &word(10))], &[]); + values + .fill_actor_graph_root(ticket, raw(0), &scalar) + .unwrap(); + values.commit_actor_graph(ticket).unwrap(); + assert_eq!(values.next_actor_ordinal, 0); + assert!(values.is_empty()); +} + +#[test] +fn graph_output_abort_drops_internal_adapter_label_aliases_and_preserves_older_actor() { + let mut values = NativeValues::default(); + let older = values.new_struct(0).unwrap(); + values.register_actor(older).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let label = values.insert("function(source)".into()).unwrap(); + let source = values.new_struct(3).unwrap(); + values.structs.get_mut(&source).unwrap().fields = + vec![Some(raw(321)), Some(raw(0)), Some(owned(label))]; + let root = values.function_adapter(source, 654).unwrap(); + values.drop_value(source).unwrap(); + let mut outer = 654u64.to_le_bytes().to_vec(); + outer.extend(word(1)); + let mut inner = 321u64.to_le_bytes().to_vec(); + inner.extend(word(u32::MAX)); + let layout = schema( + 0, + &[ + node(13, &[]), + record_node(&[2, 3]), + node(13, &[]), + node(0, &word(7)), + ], + &[supplement(0, 1, &outer), supplement(2, 1, &inner)], + ); + values + .fill_actor_graph_root(ticket, owned(root), &layout) + .unwrap(); + assert_eq!( + values.actor_graph_imports[&ticket].ordinary.strings[&label].references, + 2 + ); + values.abort_actor_graph(ticket).unwrap(); + assert!(values.actor_identity_parts(older).is_ok()); + assert_eq!(values.next_actor_ordinal, 1); + assert_eq!(values.structs_created - values.structs_destroyed, 1); + assert!(values.strings.is_empty()); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn graph_output_duplicate_external_foreign_and_allocation_refusals_preserve_caller_owners() { + let mut values = NativeValues::default(); + let ticket = values.begin_actor_graph().unwrap(); + let first = values.insert("first".into()).unwrap(); + values + .fill_actor_graph_root(ticket, owned(first), &string_schema()) + .unwrap(); + let second = values.insert("second".into()).unwrap(); + assert_eq!( + values.fill_actor_graph_root(ticket, owned(second), &string_schema()), + Err(INVALID_IMPORT) + ); + assert!(values.strings.contains_key(&second)); + values.abort_actor_graph(ticket).unwrap(); + values.drop_value(second).unwrap(); + let ticket = values.begin_actor_graph().unwrap(); + let shared = values.insert("external".into()).unwrap(); + values.retain(shared).unwrap(); + assert_eq!( + values.fill_actor_graph_root(ticket, owned(shared), &string_schema()), + Err(INVALID_IMPORT) + ); + assert_eq!(values.strings[&shared].references, 2); + values.drop_value(shared).unwrap(); + values.drop_value(shared).unwrap(); + let mut foreign = NativeValues::default(); + let alien = foreign.insert("alien".into()).unwrap(); + assert_eq!( + values.fill_actor_graph_root(ticket, owned(alien), &string_schema()), + Err(INVALID_IMPORT) + ); + foreign.drop_value(alien).unwrap(); + let fresh = values.insert("allocation".into()).unwrap(); + values.allocation_budget = Some(0); + assert_eq!( + values.fill_actor_graph_root(ticket, owned(fresh), &string_schema()), + Err(EXHAUSTED) + ); + assert!(values.strings.contains_key(&fresh)); + values.allocation_budget = None; + values.drop_value(fresh).unwrap(); + values.abort_actor_graph(ticket).unwrap(); + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn graph_root_fill_native_failure_aborts_owned_output_before_genuine_capture_and_reentry() { + use AbiScalar::{I32, I64, Pointer}; + assert_eq!( + NativeLeaf::ActorGraphRootFill.parameters(), + &[Pointer, I64, I64, I32, I64, Pointer, I64] + ); + let mut context = Box::new(JettRuntimeContextV1::retired()); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + let pointer = &*context as *const JettRuntimeContextV1; + let ticket = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + let text = b"root"; + let root = unsafe { jett_rt_v1_string_literal(pointer, text.as_ptr(), text.len() as u64) }; + let layout = string_schema(); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_root_fill( + pointer, + ticket, + root, + 1, + 0, + layout.as_ptr(), + layout.len() as u64, + ) + }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_string_scalar_count(pointer, root) }, 0); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let state = lock_unpoisoned(&lease.entry.state); + let values = &state.as_ref().unwrap().values; + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + assert!(!values.strings.contains_key(&root)); + } + let captured = unsafe { jett_rt_v1_failure_take_prefixed_text(pointer, std::ptr::null(), 0) }; + assert_ne!(captured, 0); + assert_eq!(unsafe { jett_rt_v1_string_release(pointer, captured) }, 0); + let next = unsafe { jett_rt_v1_actor_graph_begin(pointer) }; + assert!(next > ticket); + let scalar = schema(0, &[node(0, &word(10))], &[]); + assert_eq!( + unsafe { + jett_rt_v1_actor_graph_root_fill( + pointer, + next, + 0, + 0, + 0, + scalar.as_ptr(), + scalar.len() as u64, + ) + }, + 0 + ); + assert_eq!(unsafe { jett_rt_v1_actor_graph_commit(pointer, next) }, 0); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} diff --git a/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs b/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs new file mode 100644 index 00000000..369ab152 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/actor_identity_tests.rs @@ -0,0 +1,219 @@ +mod actor_identity { + use super::*; + + fn actor(values: &mut NativeValues, state: u64) -> u64 { + let id = values.new_struct(1).unwrap(); + values.structs.get_mut(&id).unwrap().fields[0] = Some(NativeField { + bits: state, + owned: false, + pending_depth: 0, + }); + values.register_actor(id).unwrap() + } + + #[test] + fn native_actor_identity_aliases_equal_and_distinct_equal_state_actors_differ() { + let mut values = NativeValues::default(); + let first = actor(&mut values, 7); + let second = actor(&mut values, 7); + assert_ne!(first, second); + for (left, right, equal) in [(first, first, true), (first, second, false)] { + assert_eq!(values.same_actor_identity(left, right), Ok(equal)); + assert_eq!( + values.equal_actor_identity(left, right, 0), + Ok(u32::from(equal)) + ); + assert_eq!( + values.equal_actor_identity(left, right, 1), + Ok(u32::from(!equal)) + ); + } + values.replace_actor_field(first, 0, 88, false).unwrap(); + assert_eq!(values.equal_actor_identity(first, first, 0), Ok(1)); + assert_eq!(values.equal_actor_identity(first, second, 0), Ok(0)); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, 2); + assert_eq!(values.structs_destroyed, 2); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_validates_current_context_even_for_equal_bits() { + let mut values = NativeValues::default(); + let mut other_context = NativeValues::default(); + let live = actor(&mut values, 7); + let foreign = actor(&mut other_context, 7); + let unregistered = values.new_struct(0).unwrap(); + assert_ne!(live, foreign); + for invalid in [0, foreign, unregistered] { + assert_eq!( + values.same_actor_identity(invalid, invalid), + Err(INVALID_ACTOR) + ); + assert_eq!( + values.equal_actor_identity(live, invalid, 0), + Err(INVALID_ACTOR) + ); + assert_eq!( + values.equal_actor_identity(invalid, live, 1), + Err(INVALID_ACTOR) + ); + } + assert_eq!( + values.equal_actor_identity(live, live, 2), + Err(INVALID_ACTOR) + ); + values.drop_value(unregistered).unwrap(); + values.release_actors().unwrap(); + assert_eq!(values.same_actor_identity(live, live), Err(INVALID_ACTOR)); + other_context.release_actors().unwrap(); + assert!(values.is_empty()); + assert!(other_context.is_empty()); + } + + #[test] + fn native_actor_identity_requires_a_complete_live_backing_record() { + let mut values = NativeValues::default(); + let live = actor(&mut values, 7); + let saved = values.structs.get_mut(&live).unwrap().fields[0].take(); + assert_eq!(values.same_actor_identity(live, live), Err(INVALID_ACTOR)); + values.structs.get_mut(&live).unwrap().fields[0] = saved; + values.release_actors().unwrap(); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_nested_layout_validates_ids_and_compares_pending_depth() { + let mut values = NativeValues::default(); + let first = actor(&mut values, 7); + let second = actor(&mut values, 7); + let layout = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Actor], + }; + assert_eq!(layout.equal_value(&values, first, first, 0), Ok(true)); + assert_eq!(layout.equal_value(&values, first, second, 0), Ok(false)); + assert_eq!(layout.equal_value(&values, 0, 0, 0), Err(INVALID_ACTOR)); + let pending = values.run_actor(first).unwrap(); + let also_pending = values.run_actor(first).unwrap(); + let twice = values.run_actor(pending).unwrap(); + assert_ne!(pending, also_pending); + assert_eq!(values.equal_actor_identity(first, first, 0), Ok(1)); + assert_eq!( + layout.equal_value(&values, pending, also_pending, 0), + Ok(true) + ); + assert_eq!(layout.equal_value(&values, first, pending, 0), Ok(false)); + assert_eq!(layout.equal_value(&values, pending, twice, 0), Ok(false)); + let once = values.join_actor(twice).unwrap(); + assert_eq!(layout.equal_value(&values, pending, once, 0), Ok(true)); + assert_eq!(values.join_actor(once), Ok(first)); + assert_eq!(values.structs[&first].fields[0].unwrap().bits, 7); + values.release_actors().unwrap(); + assert!(values.is_empty()); + for (operator, label) in [(0, "Eq"), (1, "NotEq")] { + // Each operation needs a fresh ordinary failure channel. The first + // failure is persistent within an owning context. + let mut values = NativeValues::default(); + let instance = actor(&mut values, 7); + let pending = values.run_actor(instance).unwrap(); + assert_eq!( + values.equal_actor_identity(pending, pending, operator), + Err(UNSUPPORTED_ACTOR_COMPARISON) + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some( + format!( + "unsupported binary operation: pending(actor#0) {label} pending(actor#0)" + ) + .as_bytes() + ) + ); + values.release_actors().unwrap(); + assert!(values.is_empty()); + } + } + + #[test] + fn native_actor_identity_leaf_preserves_context_failure_on_foreign_ids() { + let context = Context::new(); + let foreign_context = Context::new(); + unsafe { + let local = jett_rt_v1_struct_new(context.pointer(), 0); + let foreign = jett_rt_v1_struct_new(foreign_context.pointer(), 0); + assert_eq!(jett_rt_v1_actor_register(context.pointer(), local), local); + assert_eq!( + jett_rt_v1_actor_register(foreign_context.pointer(), foreign), + foreign + ); + assert_eq!( + jett_rt_v1_actor_identity_equal(context.pointer(), local, local, 0), + 1 + ); + assert_eq!(jett_rt_v1_value_status(context.pointer()), 0); + assert_eq!( + jett_rt_v1_actor_identity_equal(context.pointer(), foreign, foreign, 0), + JettRuntimeStatusV1::INVALID_CONTEXT.code() + ); + assert_eq!( + jett_rt_v1_value_status(context.pointer()), + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + assert_eq!(jett_rt_v1_value_status(foreign_context.pointer()), 0); + } + // The foreign handle error remains observable until retirement; clean + // context retirement releases the registered actor and returns OK. + context.destroy(JettRuntimeStatusV1::OK); + foreign_context.destroy(JettRuntimeStatusV1::OK); + } + + #[test] + fn native_actor_identity_pending_allocation_failure_preserves_ready_aliases() { + let mut values = NativeValues::default(); + let live = actor(&mut values, 7); + values.allocation_budget = Some(0); + assert_eq!(values.run_actor(live), Err(EXHAUSTED)); + assert_eq!(values.equal_actor_identity(live, live, 0), Ok(1)); + assert_eq!(values.structs_created, 1); + assert_eq!(values.actors.len(), 1); + values.allocation_budget = None; + let pending = values.run_actor(live).unwrap(); + assert_eq!( + values.actor_identity_parts(pending).unwrap().0.instance, + live + ); + assert_eq!(values.actor_identity_parts(pending).unwrap().0.ordinal, 0); + assert_eq!(values.next_actor_ordinal, 1); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, values.structs_destroyed); + assert!(values.is_empty()); + } + + #[test] + fn native_actor_identity_pending_aliases_retire_owned_state_once() { + let mut values = NativeValues::default(); + let text = values.insert("one owned state payload".into()).unwrap(); + let instance = values.new_struct(1).unwrap(); + values.structs.get_mut(&instance).unwrap().fields[0] = Some(NativeField { + bits: text, + owned: true, + pending_depth: 0, + }); + values.register_actor(instance).unwrap(); + let pending = values.run_actor(instance).unwrap(); + let nested = values.run_actor(pending).unwrap(); + assert_eq!(values.join_actor(pending), Ok(instance)); + assert_eq!( + values.actor_identity_parts(nested).unwrap().0.instance, + instance + ); + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.structs_created, 3); + assert_eq!(values.structs[&pending].fields.len(), 0); + assert_eq!(values.structs[&nested].fields.len(), 0); + values.release_actors().unwrap(); + assert_eq!(values.structs_created, values.structs_destroyed); + assert!(values.is_empty()); + } +} diff --git a/crates/jett_runtime/src/native_abi/values/checked_graph_hooks.rs b/crates/jett_runtime/src/native_abi/values/checked_graph_hooks.rs new file mode 100644 index 00000000..43719e83 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/checked_graph_hooks.rs @@ -0,0 +1,104 @@ +//! Distinct ordinary-owned boxes for checked Resource hook metadata. +use super::*; +use crate::native_abi::resource::CheckedGraphHookProof; + +#[derive(Clone, Copy)] +pub(super) enum Phase { + Factory(u64), + Published, +} + +#[derive(Clone)] +pub(super) struct CheckedGraphHook { + pub(super) proof: CheckedGraphHookProof, + pub(super) phase: Phase, + pub(super) pending_depth: u64, +} + +impl CheckedGraphHook { + pub(super) fn matches_factory( + &self, + ticket: u64, + operation: u32, + hook: u32, + signature: u32, + ) -> bool { + matches!(self.phase, Phase::Factory(actual) if actual == ticket) + && self.pending_depth == 0 + && self.proof.matches_schema(operation, hook, signature) + } + pub(super) fn publish(&mut self) { + self.phase = Phase::Published; + } +} + +impl NativeValues { + fn insert_checked_graph_hook(&mut self, value: CheckedGraphHook) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let created = self + .checked_graph_hooks_created + .checked_add(1) + .ok_or(EXHAUSTED)?; + self.checked_graph_hooks + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + let id = next_identity()?; + self.checked_graph_hooks.insert(id, value); + self.checked_graph_hooks_created = created; + Ok(id) + } + + pub(in crate::native_abi) fn mint_checked_graph_hook( + &mut self, + ticket: u64, + proof: CheckedGraphHookProof, + ) -> LeafResult { + if !self.actor_graph_imports.contains_key(&ticket) { + return Err(actor_graph_import::INVALID_IMPORT); + } + self.insert_checked_graph_hook(CheckedGraphHook { + proof, + phase: Phase::Factory(ticket), + pending_depth: 0, + }) + } + + pub(in crate::native_abi) fn published_checked_graph_hook( + &self, + id: u64, + ) -> LeafResult<&CheckedGraphHookProof> { + let value = self.checked_graph_hooks.get(&id).ok_or(INVALID_FUNCTION)?; + if !matches!(value.phase, Phase::Published) || value.pending_depth != 0 { + return Err(INVALID_FUNCTION); + } + Ok(&value.proof) + } + + pub(super) fn clone_checked_graph_hook( + &mut self, + id: u64, + depth: Option, + ) -> LeafResult { + let mut value = self + .checked_graph_hooks + .get(&id) + .ok_or(INVALID_FUNCTION)? + .clone(); + if let Some(depth) = depth { + value.pending_depth = depth; + } + self.insert_checked_graph_hook(value) + } + + pub(in crate::native_abi) fn rollback_graph_hook_failure( + &mut self, + error: (JettRuntimeStatusV1, &'static [u8]), + ) { + self.record_ordinary_failure(error); + if let Err(cleanup) = self.abort_unfinished_actor_graphs() { + self.cleanup_failed = true; + self.record_ordinary_failure(cleanup); + } + } +} diff --git a/crates/jett_runtime/src/native_abi/values/debug_equal.rs b/crates/jett_runtime/src/native_abi/values/debug_equal.rs new file mode 100644 index 00000000..5d1b699a --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/debug_equal.rs @@ -0,0 +1,705 @@ +//! Iterative equality for the existing supported aggregate enum payloads. +use super::*; +use std::collections::HashSet; + +type Visit = (usize, u64, u64); + +enum Frame { + Value(usize, u64, u64), + Fields(usize, Option, Option), + Elements { + node: usize, + left: Option, + right: Option, + left_depth: u64, + right_depth: u64, + failure: Failure, + }, + Entry(usize, usize, Option, Option), + Leave(Visit), +} + +struct Comparison<'a> { + layout: &'a NativeDebugLayout, + values: &'a NativeValues, + active: &'a mut HashSet, + work: &'a mut Vec, +} + +impl NativeDebugLayout { + pub(super) fn equal_value( + &self, + values: &NativeValues, + left: u64, + right: u64, + index: usize, + ) -> LeafResult { + let mut active = HashSet::new(); + let mut work = vec![Frame::Value(index, left, right)]; + let mut comparison = Comparison { + layout: self, + values, + active: &mut active, + work: &mut work, + }; + while let Some(frame) = comparison.work.pop() { + if !comparison.step(frame)? { + return Ok(false); + } + } + Ok(true) + } +} + +/// Runtime traversal state only. Generated code invokes source methods after +/// the leaf has returned and released the runtime context lock. +pub(super) struct EqualityCursor { + layout: NativeDebugLayout, + custom: HashMap, + active: HashSet, + work: Vec, + request: Option, + complete: Option, +} + +struct EqualityRequest { + arguments: [u64; 2], + owned: bool, +} + +impl EqualityCursor { + fn parse(bytes: &[u8], left: u64, right: u64) -> LeafResult { + let mut input = bytes.strip_prefix(b"JQ\x01").ok_or(INVALID_STRUCT)?; + fn number(input: &mut &[u8]) -> LeafResult { + let (head, tail) = input.split_at_checked(4).ok_or(INVALID_STRUCT)?; + *input = tail; + Ok(u32::from_le_bytes( + head.try_into().map_err(|_| INVALID_STRUCT)?, + )) + } + let count = number(&mut input)?; + if count as usize > input.len() / 4 { + return Err(INVALID_STRUCT); + } + let mut custom = HashMap::new(); + custom.try_reserve(count as usize).map_err(|_| EXHAUSTED)?; + for ordinal in 0..count { + let node = number(&mut input)? as usize; + if custom.insert(node, ordinal).is_some() { + return Err(INVALID_STRUCT); + } + } + let layout = NativeDebugLayout::parse(input)?; + for node in custom.keys() { + if !matches!(layout.nodes.get(*node), Some(NativeDebugNode::Record(_, fields)) if fields.is_empty()) + { + return Err(INVALID_STRUCT); + } + } + let root = layout.root; + Ok(Self { + layout, + custom, + active: HashSet::new(), + work: vec![Frame::Value(root, left, right)], + request: None, + complete: None, + }) + } + + fn advance(&mut self, values: &mut NativeValues) -> LeafResult { + if let Some(equal) = self.complete { + return Ok(u32::from(equal)); + } + if self.request.is_some() { + return Err(INVALID_STRUCT); + } + while let Some(frame) = self.work.pop() { + if let Frame::Value(node, left, right) = frame { + if let Some(&ordinal) = self.custom.get(&node) { + let left_depth = values + .structs + .get(&left) + .ok_or(INVALID_STRUCT)? + .pending_depth; + let right_depth = values + .structs + .get(&right) + .ok_or(INVALID_STRUCT)? + .pending_depth; + if left_depth != right_depth { + self.complete = Some(false); + return Ok(0); + } + let arguments = if left_depth == 0 { + [left, right] + } else { + // A view shell unwraps the pending record without copying + // or inspecting fields that the method may ignore. + let left = values.equality_view(left)?; + match values.equality_view(right) { + Ok(right) => [left, right], + Err(error) => { + values.drop_value(left)?; + return Err(error); + } + } + }; + self.request = Some(EqualityRequest { + arguments, + owned: left_depth != 0, + }); + return ordinal.checked_add(2).ok_or(EXHAUSTED); + } + } + let equal = Comparison { + layout: &self.layout, + values, + active: &mut self.active, + work: &mut self.work, + } + .step(frame)?; + if !equal { + self.complete = Some(false); + return Ok(0); + } + } + self.complete = Some(true); + Ok(1) + } + + fn retire_request(&mut self, values: &mut NativeValues) -> LeafResult<()> { + if let Some(request) = self.request.take() { + if request.owned { + for argument in request.arguments { + values.drop_value(argument)?; + } + } + } + Ok(()) + } +} + +impl NativeValues { + fn equality_view(&mut self, id: u64) -> LeafResult { + let fields = self.structs.get(&id).ok_or(INVALID_STRUCT)?.fields.clone(); + let view = self.new_struct(fields.len() as u64)?; + self.structs.get_mut(&view).ok_or(INVALID_STRUCT)?.fields = fields + .into_iter() + .map(|field| { + field.map(|field| NativeField { + owned: false, + ..field + }) + }) + .collect(); + Ok(view) + } + + pub(super) fn equality_start( + &mut self, + left: u64, + right: u64, + bytes: &[u8], + ) -> LeafResult { + #[cfg(test)] + self.allocation_checkpoint()?; + let cursor = EqualityCursor::parse(bytes, left, right)?; + let id = next_identity()?; + self.equalities.try_reserve(1).map_err(|_| EXHAUSTED)?; + self.equalities.insert(id, cursor); + Ok(id) + } + + pub(super) fn equality_next(&mut self, id: u64) -> LeafResult { + let mut cursor = self.equalities.remove(&id).ok_or(INVALID_STRUCT)?; + let result = cursor.advance(self); + self.equalities.insert(id, cursor); + result + } + + pub(super) fn equality_argument(&self, id: u64, argument: u32) -> LeafResult { + let request = self + .equalities + .get(&id) + .and_then(|cursor| cursor.request.as_ref()) + .ok_or(INVALID_STRUCT)?; + request + .arguments + .get(argument as usize) + .copied() + .ok_or(INVALID_STRUCT) + } + + pub(super) fn equality_answer(&mut self, id: u64, equal: u32, depth: u64) -> LeafResult { + if depth != 0 { + return Err(( + JettRuntimeStatusV1::INVALID_ARGUMENT, + b"Equatable.equals must return bool", + )); + } + if equal > 1 { + return Err(INVALID_STRUCT); + } + let mut cursor = self.equalities.remove(&id).ok_or(INVALID_STRUCT)?; + let result = if cursor.request.is_some() { + cursor.retire_request(self).map(|()| { + if equal == 0 { + cursor.complete = Some(false); + } + 0 + }) + } else { + Err(INVALID_STRUCT) + }; + self.equalities.insert(id, cursor); + result + } + + pub(super) fn equality_drop(&mut self, mut cursor: EqualityCursor) -> LeafResult { + cursor.retire_request(self)?; + Ok(0) + } +} + +impl Comparison<'_> { + fn step(&mut self, frame: Frame) -> LeafResult { + match frame { + Frame::Value(index, left, right) => self.value(index, left, right), + Frame::Leave(visit) => { + self.active.remove(&visit); + Ok(true) + } + Frame::Fields(index, left, right) => { + let left = left.ok_or(INVALID_STRUCT)?; + let right = right.ok_or(INVALID_STRUCT)?; + if left.pending_depth != right.pending_depth { + return Ok(false); + } + self.work.push(Frame::Value(index, left.bits, right.bits)); + Ok(true) + } + Frame::Elements { + node, + left, + right, + left_depth, + right_depth, + failure, + } => { + if left_depth != right_depth { + return Ok(false); + } + self.work.push(Frame::Value( + node, + left.ok_or(failure)?, + right.ok_or(failure)?, + )); + Ok(true) + } + Frame::Entry(key, value, left, right) => { + let left = left.ok_or(INVALID_MAP)?; + let right = right.ok_or(INVALID_MAP)?; + if left.key_taken || right.key_taken { + return Err(INVALID_MAP); + } + if left.key_pending_depth != right.key_pending_depth + || left.value_pending_depth != right.value_pending_depth + { + return Ok(false); + } + self.work.push(Frame::Value(value, left.value, right.value)); + self.work.push(Frame::Value(key, left.key, right.key)); + Ok(true) + } + } + } + + fn value(&mut self, index: usize, left_bits: u64, right_bits: u64) -> LeafResult { + let visit = (index, left_bits, right_bits); + if !self.active.insert(visit) { + return Err(INVALID_STRUCT); + } + self.work.push(Frame::Leave(visit)); + let values = self.values; + match self.layout.nodes.get(index).ok_or(INVALID_STRUCT)? { + NativeDebugNode::Primitive(raw) => Ok( + match NativeSortKind::from_raw(*raw).map_err(|_| INVALID_STRUCT)? { + NativeSortKind::Float32 => { + f32::from_bits(left_bits as u32) == f32::from_bits(right_bits as u32) + } + NativeSortKind::Float64 => { + f64::from_bits(left_bits) == f64::from_bits(right_bits) + } + NativeSortKind::String => values.same_string_value(left_bits, right_bits)?, + _ => left_bits == right_bits, + }, + ), + NativeDebugNode::Actor => values.same_actor_identity(left_bits, right_bits), + NativeDebugNode::Nothing => Ok(left_bits == right_bits), + NativeDebugNode::Bytes => values.same_bytes_value(left_bits, right_bits), + NativeDebugNode::Alias(base) => { + self.work.push(Frame::Value(*base, left_bits, right_bits)); + Ok(true) + } + node @ (NativeDebugNode::List(element) | NativeDebugNode::Set(element)) => { + let (left, right, left_depths, right_depths, left_depth, right_depth, failure) = + match node { + NativeDebugNode::List(_) => { + let left = values.lists.get(&left_bits).ok_or(INVALID_LIST)?; + let right = values.lists.get(&right_bits).ok_or(INVALID_LIST)?; + ( + &left.elements, + &right.elements, + &left.element_pending_depths, + &right.element_pending_depths, + left.pending_depth, + right.pending_depth, + INVALID_LIST, + ) + } + _ => { + let left = values.sets.get(&left_bits).ok_or(INVALID_SET)?; + let right = values.sets.get(&right_bits).ok_or(INVALID_SET)?; + ( + &left.elements, + &right.elements, + &left.element_pending_depths, + &right.element_pending_depths, + left.pending_depth, + right.pending_depth, + INVALID_SET, + ) + } + }; + if left_depth != right_depth || left.len() != right.len() { + return Ok(false); + } + for (position, (left, right)) in left.iter().zip(right).enumerate().rev() { + self.work.push(Frame::Elements { + node: *element, + left: *left, + right: *right, + left_depth: left_depths.get(&position).copied().unwrap_or(0), + right_depth: right_depths.get(&position).copied().unwrap_or(0), + failure, + }); + } + Ok(true) + } + NativeDebugNode::Map(key, value) => { + let left = values.maps.get(&left_bits).ok_or(INVALID_MAP)?; + let right = values.maps.get(&right_bits).ok_or(INVALID_MAP)?; + if left.pending_depth != right.pending_depth + || left.entries.len() != right.entries.len() + { + return Ok(false); + } + for (left, right) in left.entries.iter().zip(&right.entries).rev() { + self.work.push(Frame::Entry(*key, *value, *left, *right)); + } + Ok(true) + } + node @ (NativeDebugNode::Optional(element) | NativeDebugNode::Result(element, _)) => { + let left = values.sums.get(&left_bits).ok_or(INVALID_SUM)?; + let right = values.sums.get(&right_bits).ok_or(INVALID_SUM)?; + if left.pending_depth != right.pending_depth + || left.payload_pending_depth != right.payload_pending_depth + || left.tag != right.tag + { + return Ok(false); + } + let payload = match (node, left.tag) { + (NativeDebugNode::Optional(_), SUM_FAILURE) => return Ok(true), + (_, SUM_SUCCESS) => *element, + (NativeDebugNode::Result(_, error), SUM_FAILURE) => *error, + _ => return Err(INVALID_SUM), + }; + self.work.push(Frame::Value(payload, left.bits, right.bits)); + Ok(true) + } + NativeDebugNode::Record(_, fields) => { + let left = values.structs.get(&left_bits).ok_or(INVALID_STRUCT)?; + let right = values.structs.get(&right_bits).ok_or(INVALID_STRUCT)?; + if left.pending_depth != right.pending_depth { + return Ok(false); + } + if left.fields.len() != fields.len() || right.fields.len() != fields.len() { + return Err(INVALID_STRUCT); + } + for (position, (_, field_type)) in fields.iter().enumerate().rev() { + self.work.push(Frame::Fields( + *field_type, + left.fields[position], + right.fields[position], + )); + } + Ok(true) + } + NativeDebugNode::Enum(_, variants) | NativeDebugNode::Machine(_, variants) => { + let left = values.structs.get(&left_bits).ok_or(INVALID_STRUCT)?; + let right = values.structs.get(&right_bits).ok_or(INVALID_STRUCT)?; + if left.pending_depth != right.pending_depth { + return Ok(false); + } + let left_tag = usize::try_from( + left.fields + .first() + .copied() + .flatten() + .ok_or(INVALID_STRUCT)? + .bits, + ) + .map_err(|_| INVALID_STRUCT)?; + let right_tag = usize::try_from( + right + .fields + .first() + .copied() + .flatten() + .ok_or(INVALID_STRUCT)? + .bits, + ) + .map_err(|_| INVALID_STRUCT)?; + if left_tag != right_tag { + return Ok(false); + } + let (_, fields) = variants.get(left_tag).ok_or(INVALID_STRUCT)?; + if left.fields.len() != fields.len() + 1 || right.fields.len() != fields.len() + 1 { + return Err(INVALID_STRUCT); + } + for (position, field_type) in fields.iter().enumerate().rev() { + self.work.push(Frame::Fields( + *field_type, + left.fields[position + 1], + right.fields[position + 1], + )); + } + Ok(true) + } + NativeDebugNode::Capability(_) + | NativeDebugNode::Uninhabited + | NativeDebugNode::Redacted + | NativeDebugNode::Interface + | NativeDebugNode::Function + | NativeDebugNode::TypeConstruction => Err(INVALID_STRUCT), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn field(bits: u64) -> Option { + Some(NativeField { + bits, + owned: false, + pending_depth: 0, + }) + } + + fn custom_cursor(values: &mut NativeValues, left: u64, right: u64) -> u64 { + let id = next_identity().unwrap(); + values.equalities.insert( + id, + EqualityCursor { + layout: NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Record("Opaque".into(), vec![])], + }, + custom: HashMap::from([(0, 0)]), + active: HashSet::new(), + work: vec![Frame::Value(0, left, right)], + request: None, + complete: None, + }, + ); + id + } + + #[test] + fn custom_cursors_are_independent_and_ignore_struct_fields() { + let mut values = NativeValues::default(); + let left = values.new_struct(1).unwrap(); + let right = values.new_struct(1).unwrap(); + for id in [left, right] { + values.structs.get_mut(&id).unwrap().pending_depth = 1; + } + let first = custom_cursor(&mut values, left, right); + assert_eq!(values.equality_next(first), Ok(2)); + let argument = values.equality_argument(first, 0).unwrap(); + assert_ne!(argument, left); + assert_eq!(values.structs[&argument].pending_depth, 0); + assert!(values.structs[&argument].fields[0].is_none()); + let nested = custom_cursor(&mut values, left, right); + assert_eq!(values.equality_next(nested), Ok(2)); + values.equality_answer(nested, 0, 0).unwrap(); + assert_eq!(values.equality_next(nested), Ok(0)); + values.drop_value(nested).unwrap(); + assert_eq!(values.equality_argument(first, 0), Ok(argument)); + values.equality_answer(first, 1, 0).unwrap(); + assert!(!values.structs.contains_key(&argument)); + assert_eq!(values.equality_next(first), Ok(1)); + for id in [first, left, right] { + values.drop_value(id).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn custom_cursor_cleans_partial_views_and_pending_method_results() { + let mut values = NativeValues::default(); + let left = values.new_struct(1).unwrap(); + let right = values.new_struct(1).unwrap(); + for id in [left, right] { + values.structs.get_mut(&id).unwrap().pending_depth = 1; + } + let partial = custom_cursor(&mut values, left, right); + values.allocation_budget = Some(1); + assert_eq!(values.equality_next(partial), Err(EXHAUSTED)); + values.allocation_budget = None; + values.drop_value(partial).unwrap(); + assert_eq!(values.structs.len(), 2); + let cursor = custom_cursor(&mut values, left, right); + assert_eq!(values.equality_next(cursor), Ok(2)); + assert!(values.equality_answer(cursor, 1, 1).is_err()); + assert_eq!(values.structs.len(), 4); + for id in [cursor, left, right] { + values.drop_value(id).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn custom_cursor_preserves_deep_iterative_traversal() { + let mut values = NativeValues::default(); + let leaf = values.new_struct(1).unwrap(); + let mut root = leaf; + let mut handles = vec![leaf]; + for _ in 0..4096 { + let next = values.new_struct(2).unwrap(); + values.structs.get_mut(&next).unwrap().fields = vec![field(1), field(root)]; + handles.push(next); + root = next; + } + let base = values.new_struct(2).unwrap(); + values.structs.get_mut(&base).unwrap().fields = vec![field(0), field(leaf)]; + values.structs.get_mut(&handles[1]).unwrap().fields[1] = field(base); + handles.push(base); + let cursor = custom_cursor(&mut values, root, root); + let state = values.equalities.get_mut(&cursor).unwrap(); + state.layout.nodes = vec![ + NativeDebugNode::Enum( + "Chain".into(), + vec![("leaf".into(), vec![1]), ("next".into(), vec![0])], + ), + NativeDebugNode::Record("Opaque".into(), vec![]), + ]; + state.custom = HashMap::from([(1, 0)]); + assert_eq!(values.equality_next(cursor), Ok(2)); + assert_eq!(values.equality_argument(cursor, 0), Ok(leaf)); + values.equality_answer(cursor, 1, 0).unwrap(); + assert_eq!(values.equality_next(cursor), Ok(1)); + values.drop_value(cursor).unwrap(); + for id in handles { + values.drop_value(id).unwrap(); + } + assert!(values.is_empty()); + } + + fn chain(values: &mut NativeValues, leaf: f64) -> (u64, Vec) { + let base = values.new_struct(2).unwrap(); + values.structs.get_mut(&base).unwrap().fields = vec![field(0), field(leaf.to_bits())]; + let mut handles = vec![base]; + let mut current = base; + for _ in 0..4096 { + let next = values.new_struct(2).unwrap(); + values.structs.get_mut(&next).unwrap().fields = vec![field(1), field(current)]; + handles.push(next); + current = next; + } + (current, handles) + } + + #[test] + fn deep_equality_preserves_nan_signed_zero_and_shared_children() { + let layout = NativeDebugLayout { + root: 0, + nodes: vec![ + NativeDebugNode::Enum( + "Chain".into(), + vec![ + ("leaf".into(), vec![1]), + ("next".into(), vec![0]), + ("pair".into(), vec![0, 0]), + ], + ), + NativeDebugNode::Primitive(NativeSortKind::Float64 as u32), + ], + }; + let mut values = NativeValues::default(); + let (left, mut handles) = chain(&mut values, 0.0); + let (right, other) = chain(&mut values, -0.0); + assert_eq!(layout.equal_value(&values, left, right, 0), Ok(true)); + values.structs.get_mut(&other[0]).unwrap().fields[1] = field(1.0f64.to_bits()); + assert_eq!(layout.equal_value(&values, left, right, 0), Ok(false)); + values.structs.get_mut(&other[0]).unwrap().fields[1] = field(f64::NAN.to_bits()); + assert_eq!(layout.equal_value(&values, right, right, 0), Ok(false)); + let pair = values.new_struct(3).unwrap(); + values.structs.get_mut(&pair).unwrap().fields = vec![field(2), field(left), field(left)]; + assert_eq!(layout.equal_value(&values, pair, pair, 0), Ok(true)); + handles.extend(other); + handles.push(pair); + for handle in handles { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn equality_rejects_cycles_and_checks_later_fields_only_after_equal_prefixes() { + let mut values = NativeValues::default(); + let left = values.new_struct(2).unwrap(); + let right = values.new_struct(2).unwrap(); + values.structs.get_mut(&left).unwrap().fields = vec![field(1), None]; + values.structs.get_mut(&right).unwrap().fields = vec![field(2), None]; + let layout = NativeDebugLayout { + root: 0, + nodes: vec![ + NativeDebugNode::Record( + "Pair".into(), + vec![("first".into(), 1), ("second".into(), 1)], + ), + NativeDebugNode::Primitive(NativeSortKind::Int64 as u32), + ], + }; + assert_eq!(layout.equal_value(&values, left, right, 0), Ok(false)); + values.structs.get_mut(&right).unwrap().fields[0] = field(1); + assert_eq!( + layout.equal_value(&values, left, right, 0), + Err(INVALID_STRUCT) + ); + let aliases = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Alias(0)], + }; + assert_eq!(aliases.equal_value(&values, 0, 0, 0), Err(INVALID_STRUCT)); + let recursive = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Record( + "Loop".into(), + vec![("first".into(), 0), ("second".into(), 0)], + )], + }; + values.structs.get_mut(&left).unwrap().fields = vec![field(left), field(left)]; + assert_eq!( + recursive.equal_value(&values, left, left, 0), + Err(INVALID_STRUCT) + ); + for handle in [left, right] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } +} diff --git a/crates/jett_runtime/src/native_abi/values/debug_format.rs b/crates/jett_runtime/src/native_abi/values/debug_format.rs new file mode 100644 index 00000000..d9c1ce62 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/debug_format.rs @@ -0,0 +1,486 @@ +//! Iterative typed observation of finite values, with active-path cycle checks. +use super::*; +use std::collections::HashSet; +use std::rc::Rc; + +type Layout = Rc; +type Visit = (usize, usize, u64); + +enum Frame { + Value(Layout, usize, u64, u64), + Text(String), + Leave(Layout, usize, u64), +} + +struct Formatter<'a> { + values: &'a NativeValues, + layouts: HashMap, Layout>, + active: HashSet, + work: Vec, + text: String, +} + +impl NativeDebugLayout { + pub(super) fn format_value( + &self, + values: &NativeValues, + bits: u64, + index: usize, + ) -> LeafResult { + let mut formatter = Formatter { + values, + layouts: HashMap::new(), + active: HashSet::new(), + work: vec![Frame::Value(Rc::new(self.clone()), index, bits, 0)], + text: String::new(), + }; + while let Some(frame) = formatter.work.pop() { + match frame { + Frame::Text(text) => formatter.append(&text)?, + Frame::Leave(layout, index, bits) => { + formatter.active.remove(&visit(&layout, index, bits)); + } + Frame::Value(layout, index, bits, pending) => { + formatter.value(layout, index, bits, pending)?; + } + } + } + Ok(formatter.text) + } +} + +fn visit(layout: &Layout, index: usize, bits: u64) -> Visit { + (Rc::as_ptr(layout) as usize, index, bits) +} + +fn resolved_index(layout: &NativeDebugLayout, mut index: usize) -> LeafResult { + // Alias cycles are malformed schemas, whereas recursive value nodes + // are valid when their runtime payload has a finite base case. + for _ in 0..layout.nodes.len() { + match layout.nodes.get(index).ok_or(INVALID_TRACE_LABEL)? { + NativeDebugNode::Alias(base) => index = *base, + _ => return Ok(index), + } + } + Err(INVALID_TRACE_LABEL) +} + +impl Formatter<'_> { + fn append(&mut self, text: &str) -> LeafResult<()> { + self.text.try_reserve(text.len()).map_err(|_| EXHAUSTED)?; + self.text.push_str(text); + Ok(()) + } + + fn pending(&mut self, depth: u64) -> LeafResult<()> { + if depth == 0 { + return Ok(()); + } + let count = usize::try_from(depth).map_err(|_| EXHAUSTED)?; + let size = count.checked_mul(8).ok_or(EXHAUSTED)?; + self.text.try_reserve(size).map_err(|_| EXHAUSTED)?; + let mut suffix = String::new(); + suffix.try_reserve(count).map_err(|_| EXHAUSTED)?; + for _ in 0..count { + self.text.push_str("pending("); + suffix.push(')'); + } + self.work.push(Frame::Text(suffix)); + Ok(()) + } + + fn layout(&mut self, bytes: &[u8]) -> LeafResult { + if let Some(layout) = self.layouts.get(bytes) { + return Ok(Rc::clone(layout)); + } + let layout = Rc::new(NativeDebugLayout::parse(bytes)?); + self.layouts.insert(bytes.to_vec(), Rc::clone(&layout)); + Ok(layout) + } + + fn field(&mut self, layout: &Layout, index: usize, field: NativeField) { + self.work.push(Frame::Value( + Rc::clone(layout), + index, + field.bits, + field.pending_depth, + )); + } + + fn value(&mut self, layout: Layout, index: usize, bits: u64, pending: u64) -> LeafResult<()> { + let index = resolved_index(&layout, index)?; + let node = layout.nodes.get(index).ok_or(INVALID_TRACE_LABEL)?; + if matches!( + node, + NativeDebugNode::Alias(_) | NativeDebugNode::Uninhabited + ) { + return Err(INVALID_TRACE_LABEL); + } + if matches!(node, NativeDebugNode::Redacted) { + return self.append("[redacted]"); + } + if !self.active.insert(visit(&layout, index, bits)) { + return Err(INVALID_TRACE_LABEL); + } + // Retain the layout until leaving the path so its identity stays live. + self.work + .push(Frame::Leave(Rc::clone(&layout), index, bits)); + self.pending(pending)?; + let values = self.values; + match node { + NativeDebugNode::Interface => { + let field = values.struct_field(bits, 1)?; + let schema = values.struct_field(bits, 2)?.bits; + let inner = self.layout(values.bytes(schema)?)?; + let root = resolved_index(&inner, inner.root)?; + if matches!(inner.nodes[root], NativeDebugNode::Redacted) { + return self.append("[redacted]"); + } + let depth = values + .structs + .get(&bits) + .ok_or(INVALID_STRUCT)? + .pending_depth; + let depth = depth.checked_add(field.pending_depth).ok_or(EXHAUSTED)?; + self.work.push(Frame::Value( + Rc::clone(&inner), + inner.root, + field.bits, + depth, + )); + } + NativeDebugNode::Primitive(kind) => self.append(&values.debug_value(bits, *kind)?)?, + NativeDebugNode::Nothing => self.append(&format_nothing(bits)?)?, + NativeDebugNode::Bytes => self.append(&values.debug_value(bits, DEBUG_BYTES_KIND)?)?, + NativeDebugNode::Capability(name) => { + self.append(&values.debug_capability(bits, name)?)? + } + NativeDebugNode::Function => { + self.pending( + values + .structs + .get(&bits) + .ok_or(INVALID_STRUCT)? + .pending_depth, + )?; + self.append(values.function_debug_label(bits)?)?; + } + NativeDebugNode::Actor => { + let (identity, depth) = values.actor_identity_parts(bits)?; + self.pending(depth)?; + self.append(&format!("actor#{}", identity.ordinal))?; + } + NativeDebugNode::TypeConstruction => { + let builder = values.builders.get(&bits).ok_or(INVALID_CONSTRUCTION)?; + let record = values.structs.get(&bits).ok_or(INVALID_CONSTRUCTION)?; + self.pending(record.pending_depth)?; + self.append(&format!("TypeConstruction[{}", builder.owner))?; + if let Some(variant) = &builder.variant { + self.append(&format!(".{variant}"))?; + } + if let Some(state) = &builder.state { + self.append(&format!("@{state}"))?; + } + self.append("](")?; + self.work.push(Frame::Text(")".into())); + for (position, &field_index) in builder.put_order.iter().enumerate().rev() { + let field = record + .fields + .get(field_index + builder.field_offset) + .and_then(Option::as_ref) + .ok_or(INVALID_CONSTRUCTION)?; + let name = builder + .field_names + .get(field_index) + .ok_or(INVALID_CONSTRUCTION)?; + let bytes = builder + .field_debug_layouts + .get(field_index) + .ok_or(INVALID_CONSTRUCTION)?; + let inner = self.layout(bytes)?; + self.field(&inner, inner.root, *field); + let separator = if position > 0 { ", " } else { "" }; + self.work.push(Frame::Text(format!("{separator}{name}: "))); + } + } + NativeDebugNode::List(element) | NativeDebugNode::Set(element) => { + let (elements, depths, depth, name, failure) = match node { + NativeDebugNode::List(_) => { + let list = values.lists.get(&bits).ok_or(INVALID_LIST)?; + ( + &list.elements, + &list.element_pending_depths, + list.pending_depth, + "list", + INVALID_LIST, + ) + } + _ => { + let set = values.sets.get(&bits).ok_or(INVALID_SET)?; + ( + &set.elements, + &set.element_pending_depths, + set.pending_depth, + "set", + INVALID_SET, + ) + } + }; + self.pending(depth)?; + self.append(&format!("{name}("))?; + self.work.push(Frame::Text(")".into())); + for (position, item) in elements.iter().enumerate().rev() { + self.work.push(Frame::Value( + Rc::clone(&layout), + *element, + item.ok_or(failure)?, + depths.get(&position).copied().unwrap_or(0), + )); + if position > 0 { + self.work.push(Frame::Text(", ".into())); + } + } + } + NativeDebugNode::Map(key, value) => { + let map = values.maps.get(&bits).ok_or(INVALID_MAP)?; + self.pending(map.pending_depth)?; + self.append("map(")?; + self.work.push(Frame::Text(")".into())); + for (position, entry) in map.entries.iter().enumerate().rev() { + let entry = entry.ok_or(INVALID_MAP)?; + if entry.key_taken { + return Err(INVALID_MAP); + } + self.work.push(Frame::Value( + Rc::clone(&layout), + *value, + entry.value, + entry.value_pending_depth, + )); + self.work.push(Frame::Text(": ".into())); + self.work.push(Frame::Value( + Rc::clone(&layout), + *key, + entry.key, + entry.key_pending_depth, + )); + if position > 0 { + self.work.push(Frame::Text(", ".into())); + } + } + } + NativeDebugNode::Optional(element) | NativeDebugNode::Result(element, _) => { + let sum = values.sums.get(&bits).ok_or(INVALID_SUM)?; + self.pending(sum.pending_depth)?; + let (prefix, child) = match (node, sum.tag) { + (NativeDebugNode::Optional(_), SUM_FAILURE) => { + self.append("none")?; + return Ok(()); + } + (NativeDebugNode::Optional(_), SUM_SUCCESS) => ("some(", *element), + (NativeDebugNode::Result(_, error), SUM_FAILURE) => ("fail(", *error), + (NativeDebugNode::Result(_, _), SUM_SUCCESS) => ("ok(", *element), + _ => return Err(INVALID_SUM), + }; + self.append(prefix)?; + self.work.push(Frame::Text(")".into())); + self.work.push(Frame::Value( + Rc::clone(&layout), + child, + sum.bits, + sum.payload_pending_depth, + )); + } + NativeDebugNode::Record(name, fields) => { + let record = values.structs.get(&bits).ok_or(INVALID_STRUCT)?; + if record.fields.len() != fields.len() { + return Err(INVALID_STRUCT); + } + self.pending(record.pending_depth)?; + self.append(&format!("{name}("))?; + self.work.push(Frame::Text(")".into())); + for (position, (name, child)) in fields.iter().enumerate().rev() { + self.field( + &layout, + *child, + record.fields[position].ok_or(INVALID_STRUCT)?, + ); + let separator = if position > 0 { ", " } else { "" }; + self.work.push(Frame::Text(format!("{separator}{name}: "))); + } + } + NativeDebugNode::Enum(name, variants) | NativeDebugNode::Machine(name, variants) => { + let record = values.structs.get(&bits).ok_or(INVALID_STRUCT)?; + let tag = usize::try_from( + record + .fields + .first() + .copied() + .flatten() + .ok_or(INVALID_STRUCT)? + .bits, + ) + .map_err(|_| INVALID_STRUCT)?; + let (variant, fields) = variants.get(tag).ok_or(INVALID_STRUCT)?; + if record.fields.len() != fields.len() + 1 { + return Err(INVALID_STRUCT); + } + self.pending(record.pending_depth)?; + let separator = if matches!(node, NativeDebugNode::Enum(..)) { + "." + } else { + "@" + }; + self.append(&format!("{name}{separator}{variant}"))?; + if !fields.is_empty() { + self.append("(")?; + self.work.push(Frame::Text(")".into())); + for (position, child) in fields.iter().enumerate().rev() { + self.field( + &layout, + *child, + record.fields[position + 1].ok_or(INVALID_STRUCT)?, + ); + if position > 0 { + self.work.push(Frame::Text(", ".into())); + } + } + } + } + NativeDebugNode::Alias(_) + | NativeDebugNode::Uninhabited + | NativeDebugNode::Redacted => unreachable!(), + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn field(bits: u64) -> Option { + Some(NativeField { + bits, + owned: false, + pending_depth: 0, + }) + } + + #[test] + fn deep_finite_values_use_a_work_stack_and_shared_children_remain_valid() { + let layout = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Enum( + "Chain".into(), + vec![ + ("end".into(), vec![]), + ("next".into(), vec![0]), + ("pair".into(), vec![0, 0]), + ], + )], + }; + let mut values = NativeValues::default(); + let mut handles = Vec::new(); + let mut current = values.new_struct(1).unwrap(); + values.structs.get_mut(¤t).unwrap().fields[0] = field(0); + handles.push(current); + for _ in 0..4096 { + let next = values.new_struct(2).unwrap(); + values.structs.get_mut(&next).unwrap().fields = vec![field(1), field(current)]; + handles.push(next); + current = next; + } + let expected = format!( + "{}Chain.end{}", + "Chain.next(".repeat(4096), + ")".repeat(4096) + ); + assert_eq!( + layout.format_value(&values, current, 0), + Ok(expected.clone()) + ); + let pair = values.new_struct(3).unwrap(); + values.structs.get_mut(&pair).unwrap().fields = + vec![field(2), field(current), field(current)]; + assert_eq!( + layout.format_value(&values, pair, 0), + Ok(format!("Chain.pair({expected}, {expected})")) + ); + handles.push(pair); + for handle in handles { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } + + #[test] + fn alias_and_payload_cycles_fail_without_recursing() { + let mut values = NativeValues::default(); + let alias = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Alias(0)], + }; + assert_eq!(alias.format_value(&values, 0, 0), Err(INVALID_TRACE_LABEL)); + let record = values.new_struct(1).unwrap(); + values.structs.get_mut(&record).unwrap().fields[0] = field(record); + let recursive = NativeDebugLayout { + root: 0, + nodes: vec![NativeDebugNode::Record( + "Loop".into(), + vec![("value".into(), 0)], + )], + }; + assert_eq!( + recursive.format_value(&values, record, 0), + Err(INVALID_TRACE_LABEL) + ); + values.drop_value(record).unwrap(); + assert!(values.is_empty()); + } + + fn one_node(tag: NativeDebugTag) -> Vec { + let mut bytes = b"JD\x01".to_vec(); + for word in [1u32, 0, 1] { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.push(tag as u8); + bytes + } + + #[test] + fn dynamic_layout_cycles_fail_atomically_and_redaction_skips_invalid_payloads() { + let mut values = NativeValues::default(); + let bytes = one_node(NativeDebugTag::Interface); + let schema = values.insert_bytes(bytes.clone()).unwrap(); + let interface = values.new_struct(3).unwrap(); + values.structs.get_mut(&interface).unwrap().fields = + vec![field(0), field(interface), field(schema)]; + let text = values.insert("before".into()).unwrap(); + assert_eq!( + values.debug_append_aggregate(text, "", interface, &bytes), + Err(INVALID_TRACE_LABEL) + ); + assert_eq!(values.text(text), Ok("before")); + let redacted = values + .insert_bytes(one_node(NativeDebugTag::Redacted)) + .unwrap(); + let record = values.structs.get_mut(&interface).unwrap(); + record.fields[1] = Some(NativeField { + bits: u64::MAX, + pending_depth: u64::MAX, + owned: false, + }); + record.fields[2] = field(redacted); + record.pending_depth = 2; + assert_eq!( + values.debug_append_aggregate(text, "", interface, &bytes), + Ok(0) + ); + assert_eq!(values.text(text), Ok("before[redacted]")); + for handle in [interface, schema, redacted, text] { + values.drop_value(handle).unwrap(); + } + assert!(values.is_empty()); + } +} diff --git a/crates/jett_runtime/src/native_abi/values/field_update_tests.rs b/crates/jett_runtime/src/native_abi/values/field_update_tests.rs new file mode 100644 index 00000000..06fbe0da --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/field_update_tests.rs @@ -0,0 +1,373 @@ +use super::*; +#[path = "actor_field_update_tests.rs"] +mod actor_field_update_tests; + +fn scalar(bits: u64, pending_depth: u64) -> NativeField { + NativeField { + bits, + owned: false, + pending_depth, + } +} +fn owning(bits: u64) -> NativeField { + NativeField { + bits, + owned: true, + pending_depth: 0, + } +} +fn record(values: &mut NativeValues, fields: &[NativeField]) -> u64 { + let id = values.new_struct(fields.len() as u64).unwrap(); + for (slot, field) in values + .structs + .get_mut(&id) + .unwrap() + .fields + .iter_mut() + .zip(fields) + { + *slot = Some(*field); + } + id +} + +#[test] +fn occupied_replacement_preserves_siblings_depth_and_clone_independence() { + let mut values = NativeValues::default(); + let text = values.insert("before".into()).unwrap(); + let parent = record(&mut values, &[scalar(7, 3), owning(text)]); + let clone = values.clone_struct(parent).unwrap(); + let after = values.insert("after".into()).unwrap(); + values.allocation_budget = Some(0); + let old = values + .replace_struct_field(parent, 1, owning(after)) + .unwrap(); + assert_eq!(old.bits, text); + assert!(old.owned); + assert_eq!(values.struct_field(parent, 0).unwrap().pending_depth, 3); + assert_eq!( + values + .text(values.struct_field(clone, 1).unwrap().bits) + .unwrap(), + "before" + ); + assert_eq!( + values + .text(values.struct_field(parent, 1).unwrap().bits) + .unwrap(), + "after" + ); + // Return transfers displaced ownership; neither side was silently dropped. + values.drop_value(old.bits).unwrap(); + values.drop_value(parent).unwrap(); + values.drop_value(clone).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn occupied_replacement_precommit_refusals_preserve_both_owners() { + let mut values = NativeValues::default(); + let before = values.insert_bytes(vec![1]).unwrap(); + let replacement = values.insert_bytes(vec![2]).unwrap(); + let parent = record(&mut values, &[owning(before)]); + for (index, field) in [ + (1, owning(replacement)), + (0, scalar(8, 0)), + (0, owning(u64::MAX)), + (0, owning(parent)), + ] { + assert!(values.replace_struct_field(parent, index, field).is_err()); + assert_eq!(values.struct_field(parent, 0).unwrap().bits, before); + assert!(values.bytes.contains_key(&before)); + assert!(values.bytes.contains_key(&replacement)); + } + values.structs.get_mut(&parent).unwrap().pending_depth = 1; + assert!( + values + .replace_struct_field(parent, 0, owning(replacement)) + .is_err() + ); + values.structs.get_mut(&parent).unwrap().pending_depth = 0; + let empty = values.new_struct(1).unwrap(); + assert!( + values + .replace_struct_field(empty, 0, owning(replacement)) + .is_err() + ); + let mut foreign = NativeValues::default(); + assert!( + foreign + .replace_struct_field(parent, 0, owning(replacement)) + .is_err() + ); + values.drop_value(empty).unwrap(); + values.drop_value(parent).unwrap(); + values.drop_value(replacement).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn occupied_replacement_rejects_actor_and_unpublished_graph_storage() { + let mut values = NativeValues::default(); + let actor = record(&mut values, &[scalar(1, 0)]); + values.register_actor(actor).unwrap(); + assert!(values.replace_struct_field(actor, 0, scalar(2, 0)).is_err()); + let ticket = values.begin_actor_graph().unwrap(); + let staged = values.reserve_actor_graph_base(ticket, 1).unwrap(); + let pending = values + .reserve_actor_graph_pending(ticket, staged, 1) + .unwrap(); + for id in [ticket, staged, pending] { + assert!(values.replace_struct_field(id, 0, scalar(2, 0)).is_err()); + } + values.abort_actor_graph(ticket).unwrap(); + values.release_actors().unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn occupied_replacement_catalog_signature_is_versioned_and_exact() { + assert_eq!( + NativeLeaf::StructReplaceOccupied.symbol(), + "jett_rt_v1_struct_replace_occupied" + ); + assert_eq!( + NativeLeaf::StructReplaceOccupied.parameters(), + &[ + AbiScalar::Pointer, + AbiScalar::I64, + AbiScalar::I64, + AbiScalar::I64, + AbiScalar::I32, + AbiScalar::I64 + ] + ); + assert_eq!(NativeLeaf::StructReplaceOccupied.result(), AbiScalar::I64); +} + +#[test] +fn occupied_scalar_replacement_returns_displaced_depth_and_installs_new_depth() { + let mut values = NativeValues::default(); + let parent = record(&mut values, &[scalar(5, 2)]); + let displaced = values + .replace_struct_field(parent, 0, scalar(8, 4)) + .unwrap(); + assert_eq!( + (displaced.bits, displaced.pending_depth, displaced.owned), + (5, 2, false) + ); + assert_eq!(values.struct_field(parent, 0).unwrap().pending_depth, 4); + values.drop_value(parent).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn occupied_replacement_cleanup_failure_cannot_retake_replacement() { + let mut values = NativeValues::default(); + let damaged = record(&mut values, &[owning(u64::MAX)]); + let parent = record(&mut values, &[owning(damaged)]); + let replacement = values.insert_bytes(vec![9]).unwrap(); + let old = values + .replace_struct_field(parent, 0, owning(replacement)) + .unwrap(); + assert!(values.drop_value(old.bits).is_err()); + assert_eq!(values.struct_field(parent, 0).unwrap().bits, replacement); + values.drop_value(parent).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn occupied_replacement_refuses_live_builder_shell_without_displacing_it() { + let mut values = NativeValues::default(); + let parent = record(&mut values, &[scalar(3, 0)]); + values.builders.insert( + parent, + NativeBuilderInfo { + owner: "Record".into(), + metadata_owner: "Record".into(), + unsupported_kind: None, + variant: None, + state: None, + target_state: None, + field_offset: 0, + field_names: vec!["value".into()], + field_type_names: vec!["int64".into()], + field_types: vec!["int64".into()], + field_debug_layouts: vec![Vec::new()], + put_order: vec![0], + validation: vec![BuilderFieldValidation::None], + }, + ); + assert!( + values + .replace_struct_field(parent, 0, scalar(4, 0)) + .is_err() + ); + assert_eq!(values.struct_field(parent, 0).unwrap().bits, 3); + values.drop_value(parent).unwrap(); + assert!(values.is_empty()); +} + +// These controls call the actual normal ABI leaf, including its failure gate +// and cleanup path. Test-only state access observes precommit rollback. +fn leaf_values( + pointer: *const JettRuntimeContextV1, + action: impl FnOnce(&mut NativeValues) -> T, +) -> T { + let lease = acquire_context(context_key(pointer).unwrap()).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + action(&mut state.as_mut().unwrap().values) +} + +fn field_leaf_context( + expected: JettRuntimeStatusV1, + action: impl FnOnce(*const JettRuntimeContextV1), +) { + let mut context = JettRuntimeContextV1::retired(); + let mut result = std::mem::MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + action(&context); + assert_eq!( + unsafe { jett_rt_v1_value_status(&context) }, + expected.code() + ); + assert!(leaf_values(&context, |values| values.is_empty())); + // Context destruction reports cleanup, independently of the earlier leaf error. + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut context, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); +} + +fn function_record(values: &mut NativeValues) -> u64 { + let label = values.insert("field-update-test".into()).unwrap(); + record(values, &[scalar(u64::MAX, 0), scalar(0, 0), owning(label)]) +} + +#[test] +fn occupied_leaf_rejects_same_unique_owner_before_commit_and_cleans_once() { + for kind in 0..4 { + field_leaf_context(JettRuntimeStatusV1::INVALID_ARGUMENT, |pointer| { + let (parent, child) = leaf_values(pointer, |values| { + let child = match kind { + 0 => values.insert_bytes(vec![1, 2]).unwrap(), + 1 => values.new_list(false).unwrap(), + 2 => record(values, &[scalar(3, 0)]), + _ => function_record(values), + }; + (record(values, &[owning(child), scalar(19, 2)]), child) + }); + assert_eq!( + unsafe { jett_rt_v1_struct_replace_occupied(pointer, parent, 0, child, 1, 0) }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_value_status(pointer) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(parent, 0).unwrap().bits, child); + assert_eq!(values.struct_field(parent, 1).unwrap().bits, 19); + assert_eq!(values.struct_field(parent, 1).unwrap().pending_depth, 2); + assert!(values.owned_pending_depth(child).is_ok()); + }); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, parent) }, 0); + }); + } +} + +#[test] +fn occupied_leaf_rejects_unique_sibling_alias_and_preserves_both_fields() { + field_leaf_context(JettRuntimeStatusV1::INVALID_ARGUMENT, |pointer| { + let (parent, before, sibling) = leaf_values(pointer, |values| { + let before = values.new_list(false).unwrap(); + let sibling = values.new_list(false).unwrap(); + ( + record(values, &[owning(before), owning(sibling)]), + before, + sibling, + ) + }); + assert_eq!( + unsafe { jett_rt_v1_struct_replace_occupied(pointer, parent, 0, sibling, 1, 0) }, + 0 + ); + assert_ne!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(parent, 0).unwrap().bits, before); + assert_eq!(values.struct_field(parent, 1).unwrap().bits, sibling); + assert!(values.lists.contains_key(&before)); + assert!(values.lists.contains_key(&sibling)); + }); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, parent) }, 0); + }); +} + +#[test] +fn occupied_leaf_requires_string_budget_and_allows_retained_identical_edges() { + for fields in [1_usize, 2] { + for retain_replacement in [false, true] { + let expected = if retain_replacement { + JettRuntimeStatusV1::OK + } else { + JettRuntimeStatusV1::INVALID_ARGUMENT + }; + field_leaf_context(expected, |pointer| { + let (parent, text) = leaf_values(pointer, |values| { + let text = values.insert("shared".into()).unwrap(); + for _ in 1..fields { + values.retain(text).unwrap(); + } + (record(values, &vec![owning(text); fields]), text) + }); + if retain_replacement { + assert_eq!(unsafe { jett_rt_v1_string_retain(pointer, text) }, text); + } + let displaced = + unsafe { jett_rt_v1_struct_replace_occupied(pointer, parent, 0, text, 1, 0) }; + assert_eq!(displaced, if retain_replacement { text } else { 0 }); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(parent, 0).unwrap().bits, text); + assert_eq!( + values.strings[&text].references, + fields as u64 + u64::from(retain_replacement) + ); + }); + if retain_replacement { + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, displaced) }, 0); + leaf_values(pointer, |values| { + assert_eq!(values.strings[&text].references, fields as u64); + }); + } else { + assert_ne!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + } + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, parent) }, 0); + }); + } + } +} + +#[test] +fn occupied_leaf_accepts_distinct_function_copy_and_retires_displaced_descriptor() { + field_leaf_context(JettRuntimeStatusV1::OK, |pointer| { + let (parent, original) = leaf_values(pointer, |values| { + let original = function_record(values); + (record(values, &[owning(original)]), original) + }); + let copied = unsafe { jett_rt_v1_struct_clone(pointer, original) }; + assert_ne!(copied, 0); + assert_ne!(original, copied); + let displaced = + unsafe { jett_rt_v1_struct_replace_occupied(pointer, parent, 0, copied, 1, 0) }; + assert_eq!(displaced, original); + leaf_values(pointer, |values| { + assert_eq!(values.struct_field(parent, 0).unwrap().bits, copied); + }); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, displaced) }, 0); + assert_eq!(unsafe { jett_rt_v1_value_drop(pointer, parent) }, 0); + assert_eq!(unsafe { jett_rt_v1_value_status(pointer) }, 0); + }); +} diff --git a/crates/jett_runtime/src/native_abi/values/graphics_decode.rs b/crates/jett_runtime/src/native_abi/values/graphics_decode.rs new file mode 100644 index 00000000..15f0284b --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/graphics_decode.rs @@ -0,0 +1,209 @@ +//! Strict decoding of the existing, checked graphics record layouts. +//! +//! Decode in reference field order before running domain validation. Pending +//! values are terminal shape errors, including metadata on otherwise ready +//! handles; decoding must never observe a pending value's payload. +use super::*; + +#[derive(Clone, Copy)] +enum GraphicsRecord { + Config, + Scene, + Color, + Rect, + Text, +} + +impl GraphicsRecord { + fn pending(self) -> Failure { + let message: &'static [u8] = match self { + Self::Config => b"graphics: expected graphics.Config", + Self::Scene => b"graphics: expected graphics.Scene", + Self::Color => b"graphics: expected graphics.Color", + Self::Rect => b"graphics: expected graphics.Rect", + Self::Text => b"graphics: expected graphics.Text", + }; + graphics_type_error(message) + } +} + +fn graphics_type_error(message: &'static [u8]) -> Failure { + (JettRuntimeStatusV1::INVALID_ARGUMENT, message) +} + +fn field(record: &NativeStruct, index: usize) -> LeafResult { + record + .fields + .get(index) + .copied() + .flatten() + .ok_or(INVALID_STRUCT) +} + +fn integer(record: &NativeStruct, index: usize, pending: &'static [u8]) -> LeafResult { + let value = field(record, index)?; + if value.pending_depth != 0 { + return Err(graphics_type_error(pending)); + } + if value.owned { + return Err(INVALID_STRUCT); + } + Ok(value.bits as i64) +} + +fn record_field(record: &NativeStruct, index: usize, kind: GraphicsRecord) -> LeafResult { + let value = field(record, index)?; + if value.pending_depth != 0 { + return Err(kind.pending()); + } + if !value.owned { + return Err(INVALID_STRUCT); + } + Ok(value.bits) +} + +fn record_element(list: &NativeList, index: usize, kind: GraphicsRecord) -> LeafResult { + if list + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0) + != 0 + { + return Err(kind.pending()); + } + list.elements + .get(index) + .copied() + .flatten() + .ok_or(INVALID_LIST) +} + +impl NativeValues { + fn graphics_record(&self, value: u64, kind: GraphicsRecord) -> LeafResult<&NativeStruct> { + let record = self.structs.get(&value).ok_or(INVALID_STRUCT)?; + if record.pending_depth != 0 { + return Err(kind.pending()); + } + Ok(record) + } + + fn graphics_string( + &self, + record: &NativeStruct, + index: usize, + pending: &'static [u8], + ) -> LeafResult { + let value = field(record, index)?; + if value.pending_depth != 0 { + return Err(graphics_type_error(pending)); + } + if !value.owned { + return Err(INVALID_STRUCT); + } + let string = self.strings.get(&value.bits).ok_or(INVALID_HANDLE)?; + if string.pending_depth != 0 { + return Err(graphics_type_error(pending)); + } + Ok(string.text.clone()) + } + + fn graphics_list( + &self, + record: &NativeStruct, + index: usize, + pending: &'static [u8], + ) -> LeafResult<&NativeList> { + let value = field(record, index)?; + if value.pending_depth != 0 { + return Err(graphics_type_error(pending)); + } + if !value.owned { + return Err(INVALID_STRUCT); + } + let list = self.lists.get(&value.bits).ok_or(INVALID_LIST)?; + if list.pending_depth != 0 { + return Err(graphics_type_error(pending)); + } + if !list.owned { + return Err(INVALID_LIST); + } + Ok(list) + } + + pub(super) fn graphics_config(&self, value: u64) -> LeafResult { + let record = self.graphics_record(value, GraphicsRecord::Config)?; + Ok(graphics::Config { + title: self.graphics_string( + record, + 0, + b"graphics: graphics.Config.title must be string", + )?, + width: integer(record, 1, b"graphics: graphics.Config.width must be int64")?, + height: integer(record, 2, b"graphics: graphics.Config.height must be int64")?, + }) + } + + fn graphics_color(&self, value: u64) -> LeafResult { + let record = self.graphics_record(value, GraphicsRecord::Color)?; + Ok(graphics::Color { + red: integer(record, 0, b"graphics: graphics.Color.red must be int64")?, + green: integer(record, 1, b"graphics: graphics.Color.green must be int64")?, + blue: integer(record, 2, b"graphics: graphics.Color.blue must be int64")?, + }) + } + + fn graphics_rect(&self, value: u64) -> LeafResult { + let record = self.graphics_record(value, GraphicsRecord::Rect)?; + Ok(graphics::Rect { + x: integer(record, 0, b"graphics: graphics.Rect.x must be int64")?, + y: integer(record, 1, b"graphics: graphics.Rect.y must be int64")?, + width: integer(record, 2, b"graphics: graphics.Rect.width must be int64")?, + height: integer(record, 3, b"graphics: graphics.Rect.height must be int64")?, + color: self.graphics_color(record_field(record, 4, GraphicsRecord::Color)?)?, + }) + } + + fn graphics_text(&self, value: u64) -> LeafResult { + let record = self.graphics_record(value, GraphicsRecord::Text)?; + Ok(graphics::Text { + x: integer(record, 0, b"graphics: graphics.Text.x must be int64")?, + y: integer(record, 1, b"graphics: graphics.Text.y must be int64")?, + text: self.graphics_string( + record, + 2, + b"graphics: graphics.Text.text must be string", + )?, + scale: integer(record, 3, b"graphics: graphics.Text.scale must be int64")?, + color: self.graphics_color(record_field(record, 4, GraphicsRecord::Color)?)?, + }) + } + + pub(super) fn graphics_scene(&self, value: u64) -> LeafResult { + let record = self.graphics_record(value, GraphicsRecord::Scene)?; + let background = self.graphics_color(record_field(record, 0, GraphicsRecord::Color)?)?; + let rectangles = self.graphics_list( + record, + 1, + b"graphics: graphics.Scene.rectangles must be a list", + )?; + let rectangles = (0..rectangles.elements.len()) + .map(|index| { + self.graphics_rect(record_element(rectangles, index, GraphicsRecord::Rect)?) + }) + .collect::>>()?; + let texts = + self.graphics_list(record, 2, b"graphics: graphics.Scene.texts must be a list")?; + let texts = (0..texts.elements.len()) + .map(|index| self.graphics_text(record_element(texts, index, GraphicsRecord::Text)?)) + .collect::>>()?; + Ok(graphics::Scene { + background, + rectangles, + texts, + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/jett_runtime/src/native_abi/values/graphics_decode/tests.rs b/crates/jett_runtime/src/native_abi/values/graphics_decode/tests.rs new file mode 100644 index 00000000..ecc18e61 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/graphics_decode/tests.rs @@ -0,0 +1,696 @@ +use super::*; +use std::collections::BTreeMap; + +fn scalar(bits: u64) -> NativeField { + NativeField { + bits, + owned: false, + pending_depth: 0, + } +} + +fn owned(bits: u64) -> NativeField { + NativeField { + bits, + owned: true, + pending_depth: 0, + } +} + +fn record(values: &mut NativeValues, fields: Vec) -> u64 { + let value = values.new_struct(fields.len() as u64).unwrap(); + values.structs.get_mut(&value).unwrap().fields = fields.into_iter().map(Some).collect(); + value +} + +fn color(values: &mut NativeValues) -> u64 { + record(values, vec![scalar(1), scalar(2), scalar(3)]) +} + +struct Fixture { + config: u64, + title: u64, + scene: u64, + background: u64, + rectangles: u64, + rect: u64, + rect_color: u64, + texts: u64, + text: u64, + text_string: u64, + text_color: u64, +} + +impl Fixture { + fn new(values: &mut NativeValues) -> Self { + let title = values.insert_pending("headless".into(), 0).unwrap(); + let config = record(values, vec![owned(title), scalar(16), scalar(16)]); + let background = color(values); + let rect_color = color(values); + let rect = record( + values, + vec![ + scalar(0), + scalar(0), + scalar(1), + scalar(1), + owned(rect_color), + ], + ); + let rectangles = values.new_list(true).unwrap(); + values + .lists + .get_mut(&rectangles) + .unwrap() + .elements + .push(Some(rect)); + let text_color = color(values); + let text_string = values.insert_pending("A".into(), 0).unwrap(); + let text = record( + values, + vec![ + scalar(0), + scalar(0), + owned(text_string), + scalar(1), + owned(text_color), + ], + ); + let texts = values.new_list(true).unwrap(); + values + .lists + .get_mut(&texts) + .unwrap() + .elements + .push(Some(text)); + let scene = record( + values, + vec![owned(background), owned(rectangles), owned(texts)], + ); + Self { + config, + title, + scene, + background, + rectangles, + rect, + rect_color, + texts, + text, + text_string, + text_color, + } + } + + fn destroy(self, values: &mut NativeValues) { + values.drop_value(self.config).unwrap(); + values.drop_value(self.scene).unwrap(); + assert!(values.is_empty()); + assert_eq!(values.structs_created, values.structs_destroyed); + assert_eq!(values.lists_created, values.lists_destroyed); + assert_eq!(values.sums_created, values.sums_destroyed); + } + + fn config_order(&self) -> Vec<(Site, &'static str)> { + vec![ + ( + Site::Record(self.config), + "graphics: expected graphics.Config", + ), + ( + Site::Field(self.config, 0), + "graphics: graphics.Config.title must be string", + ), + ( + Site::String(self.title), + "graphics: graphics.Config.title must be string", + ), + ( + Site::Field(self.config, 1), + "graphics: graphics.Config.width must be int64", + ), + ( + Site::Field(self.config, 2), + "graphics: graphics.Config.height must be int64", + ), + ] + } + + fn scene_order(&self) -> Vec<(Site, &'static str)> { + vec![ + ( + Site::Record(self.scene), + "graphics: expected graphics.Scene", + ), + ( + Site::Field(self.scene, 0), + "graphics: expected graphics.Color", + ), + ( + Site::Record(self.background), + "graphics: expected graphics.Color", + ), + ( + Site::Field(self.background, 0), + "graphics: graphics.Color.red must be int64", + ), + ( + Site::Field(self.background, 1), + "graphics: graphics.Color.green must be int64", + ), + ( + Site::Field(self.background, 2), + "graphics: graphics.Color.blue must be int64", + ), + ( + Site::Field(self.scene, 1), + "graphics: graphics.Scene.rectangles must be a list", + ), + ( + Site::List(self.rectangles), + "graphics: graphics.Scene.rectangles must be a list", + ), + ( + Site::Element(self.rectangles, 0), + "graphics: expected graphics.Rect", + ), + (Site::Record(self.rect), "graphics: expected graphics.Rect"), + ( + Site::Field(self.rect, 0), + "graphics: graphics.Rect.x must be int64", + ), + ( + Site::Field(self.rect, 1), + "graphics: graphics.Rect.y must be int64", + ), + ( + Site::Field(self.rect, 2), + "graphics: graphics.Rect.width must be int64", + ), + ( + Site::Field(self.rect, 3), + "graphics: graphics.Rect.height must be int64", + ), + ( + Site::Field(self.rect, 4), + "graphics: expected graphics.Color", + ), + ( + Site::Record(self.rect_color), + "graphics: expected graphics.Color", + ), + ( + Site::Field(self.rect_color, 0), + "graphics: graphics.Color.red must be int64", + ), + ( + Site::Field(self.rect_color, 1), + "graphics: graphics.Color.green must be int64", + ), + ( + Site::Field(self.rect_color, 2), + "graphics: graphics.Color.blue must be int64", + ), + ( + Site::Field(self.scene, 2), + "graphics: graphics.Scene.texts must be a list", + ), + ( + Site::List(self.texts), + "graphics: graphics.Scene.texts must be a list", + ), + ( + Site::Element(self.texts, 0), + "graphics: expected graphics.Text", + ), + (Site::Record(self.text), "graphics: expected graphics.Text"), + ( + Site::Field(self.text, 0), + "graphics: graphics.Text.x must be int64", + ), + ( + Site::Field(self.text, 1), + "graphics: graphics.Text.y must be int64", + ), + ( + Site::Field(self.text, 2), + "graphics: graphics.Text.text must be string", + ), + ( + Site::String(self.text_string), + "graphics: graphics.Text.text must be string", + ), + ( + Site::Field(self.text, 3), + "graphics: graphics.Text.scale must be int64", + ), + ( + Site::Field(self.text, 4), + "graphics: expected graphics.Color", + ), + ( + Site::Record(self.text_color), + "graphics: expected graphics.Color", + ), + ( + Site::Field(self.text_color, 0), + "graphics: graphics.Color.red must be int64", + ), + ( + Site::Field(self.text_color, 1), + "graphics: graphics.Color.green must be int64", + ), + ( + Site::Field(self.text_color, 2), + "graphics: graphics.Color.blue must be int64", + ), + ] + } +} + +#[derive(Clone, Copy, Debug)] +enum Site { + Record(u64), + Field(u64, usize), + String(u64), + List(u64), + Element(u64, usize), +} + +impl Site { + fn set(self, values: &mut NativeValues, depth: u64) { + match self { + Self::Record(id) => values.structs.get_mut(&id).unwrap().pending_depth = depth, + Self::Field(id, index) => { + values.structs.get_mut(&id).unwrap().fields[index] + .as_mut() + .unwrap() + .pending_depth = depth + } + Self::String(id) => values.strings.get_mut(&id).unwrap().pending_depth = depth, + Self::List(id) => values.lists.get_mut(&id).unwrap().pending_depth = depth, + Self::Element(id, index) => { + let depths = &mut values.lists.get_mut(&id).unwrap().element_pending_depths; + if depth == 0 { + depths.remove(&index); + } else { + depths.insert(index, depth); + } + } + } + } +} + +// Include every graphics carrier's ownership, payload and pending metadata. +fn snapshot(values: &NativeValues) -> String { + let strings: BTreeMap<_, _> = values + .strings + .iter() + .map(|(id, value)| (*id, (&value.text, value.references, value.pending_depth))) + .collect(); + let records: BTreeMap<_, _> = values + .structs + .iter() + .map(|(id, value)| { + ( + *id, + ( + value.pending_depth, + value + .fields + .iter() + .map(|field| { + field.map(|field| (field.bits, field.owned, field.pending_depth)) + }) + .collect::>(), + ), + ) + }) + .collect(); + let lists: BTreeMap<_, _> = values + .lists + .iter() + .map(|(id, value)| { + ( + *id, + ( + value.pending_depth, + value.owned, + &value.elements, + value + .element_pending_depths + .iter() + .map(|(index, depth)| (*index, *depth)) + .collect::>(), + ), + ) + }) + .collect(); + format!("{strings:?}/{records:?}/{lists:?}") +} + +fn assert_error(result: LeafResult, message: &str) { + let (status, actual) = result.unwrap_err(); + assert_eq!(status, JettRuntimeStatusV1::INVALID_ARGUMENT); + assert_eq!(std::str::from_utf8(actual).unwrap(), message); +} + +#[test] +fn graphics_decode_rejects_every_pending_shape_without_changing_sources() { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + for (config, cases) in [ + (true, fixture.config_order()), + (false, fixture.scene_order()), + ] { + for (site, message) in cases { + site.set(&mut values, depth); + let before = snapshot(&values); + if config { + assert_error(values.graphics_config(fixture.config), message); + } else { + assert_error(values.graphics_scene(fixture.scene), message); + } + assert_eq!(snapshot(&values), before, "{site:?}"); + site.set(&mut values, 0); + } + } + fixture.destroy(&mut values); + } +} + +#[test] +fn graphics_decode_reports_first_fault_in_reference_field_order() { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + for (config, cases) in [ + (true, fixture.config_order()), + (false, fixture.scene_order()), + ] { + for (site, _) in &cases { + site.set(&mut values, depth); + } + for (site, message) in cases { + if config { + assert_error(values.graphics_config(fixture.config), message); + } else { + assert_error(values.graphics_scene(fixture.scene), message); + } + site.set(&mut values, 0); + } + } + assert_eq!(values.graphics_config(fixture.config).unwrap().width, 16); + assert_eq!( + values.graphics_scene(fixture.scene).unwrap().texts[0].text, + "A" + ); + fixture.destroy(&mut values); + } +} + +#[test] +fn graphics_decode_does_not_observe_payloads_below_pending_metadata() { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + // A pending owner cannot expose even a missing first field. + let saved = std::mem::take(&mut values.structs.get_mut(&fixture.config).unwrap().fields); + Site::Record(fixture.config).set(&mut values, depth); + assert_error( + values.graphics_config(fixture.config), + "graphics: expected graphics.Config", + ); + Site::Record(fixture.config).set(&mut values, 0); + assert_eq!(values.graphics_config(fixture.config), Err(INVALID_STRUCT)); + values.structs.get_mut(&fixture.config).unwrap().fields = saved; + + // Field metadata precedes both an invalid handle and its ownership flag. + let saved = values.structs.get_mut(&fixture.config).unwrap().fields[0].replace(scalar(0)); + Site::Field(fixture.config, 0).set(&mut values, depth); + assert_error( + values.graphics_config(fixture.config), + "graphics: graphics.Config.title must be string", + ); + Site::Field(fixture.config, 0).set(&mut values, 0); + assert_eq!(values.graphics_config(fixture.config), Err(INVALID_STRUCT)); + values.structs.get_mut(&fixture.config).unwrap().fields[0] + .as_mut() + .unwrap() + .owned = true; + assert_eq!(values.graphics_config(fixture.config), Err(INVALID_HANDLE)); + values.structs.get_mut(&fixture.config).unwrap().fields[0] = saved; + + let saved = values.lists.get_mut(&fixture.rectangles).unwrap().elements[0].take(); + values.lists.get_mut(&fixture.rectangles).unwrap().owned = false; + Site::List(fixture.rectangles).set(&mut values, depth); + assert_error( + values.graphics_scene(fixture.scene), + "graphics: graphics.Scene.rectangles must be a list", + ); + Site::List(fixture.rectangles).set(&mut values, 0); + assert_eq!(values.graphics_scene(fixture.scene), Err(INVALID_LIST)); + values.lists.get_mut(&fixture.rectangles).unwrap().owned = true; + Site::Element(fixture.rectangles, 0).set(&mut values, depth); + assert_error( + values.graphics_scene(fixture.scene), + "graphics: expected graphics.Rect", + ); + Site::Element(fixture.rectangles, 0).set(&mut values, 0); + assert_eq!(values.graphics_scene(fixture.scene), Err(INVALID_LIST)); + values.lists.get_mut(&fixture.rectangles).unwrap().elements[0] = saved; + + // Ready scalar fields reject ownership metadata instead of treating a + // live handle as an integer. Restore it before normal source cleanup. + values.structs.get_mut(&fixture.config).unwrap().fields[1] + .as_mut() + .unwrap() + .owned = true; + assert_eq!(values.graphics_config(fixture.config), Err(INVALID_STRUCT)); + values.structs.get_mut(&fixture.config).unwrap().fields[1] + .as_mut() + .unwrap() + .owned = false; + fixture.destroy(&mut values); + } +} + +#[test] +fn graphics_decode_completes_shape_checks_before_domain_validation() { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + values.structs.get_mut(&fixture.config).unwrap().fields[1] + .as_mut() + .unwrap() + .bits = 0; + Site::Field(fixture.config, 2).set(&mut values, 2); + assert_error( + values.graphics_validate_config(fixture.config), + "graphics: graphics.Config.height must be int64", + ); + Site::Field(fixture.config, 2).set(&mut values, 0); + let outcome = values.graphics_validate_config(fixture.config).unwrap(); + assert_eq!(values.sums[&outcome].tag, SUM_FAILURE); + assert_eq!( + values.text(values.sums[&outcome].bits).unwrap(), + graphics::validate_config(&values.graphics_config(fixture.config).unwrap()).unwrap_err() + ); + values.drop_value(outcome).unwrap(); + + values.structs.get_mut(&fixture.background).unwrap().fields[0] + .as_mut() + .unwrap() + .bits = 256; + Site::Field(fixture.text, 3).set(&mut values, 2); + assert_error( + values.graphics_validate_scene(16, 16, fixture.scene), + "graphics: graphics.Text.scale must be int64", + ); + Site::Field(fixture.text, 3).set(&mut values, 0); + let outcome = values + .graphics_validate_scene(16, 16, fixture.scene) + .unwrap(); + assert_eq!(values.sums[&outcome].tag, SUM_FAILURE); + assert_eq!( + values.text(values.sums[&outcome].bits).unwrap(), + graphics::render_scene(16, 16, &values.graphics_scene(fixture.scene).unwrap()).unwrap_err() + ); + values.drop_value(outcome).unwrap(); + fixture.destroy(&mut values); +} + +#[test] +fn graphics_decode_accepts_joined_record_roots_without_consuming_originals() { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + for (source, kind) in [ + (fixture.config, GraphicsRecord::Config), + (fixture.scene, GraphicsRecord::Scene), + (fixture.background, GraphicsRecord::Color), + (fixture.rect, GraphicsRecord::Rect), + (fixture.text, GraphicsRecord::Text), + ] { + let original = snapshot(&values); + let mut joined = values.clone_with_pending_depth(source, depth).unwrap(); + for remaining in (0..depth).rev() { + assert_eq!( + values.graphics_record(joined, kind).err(), + Some(kind.pending()) + ); + let previous = joined; + joined = values.join_record(previous).unwrap(); + assert_eq!(values.structs[&previous].pending_depth, remaining + 1); + assert_eq!(values.structs[&joined].pending_depth, remaining); + values.drop_value(previous).unwrap(); + } + match kind { + GraphicsRecord::Config => assert_eq!( + values.graphics_config(joined), + values.graphics_config(source) + ), + GraphicsRecord::Scene => { + assert_eq!(values.graphics_scene(joined), values.graphics_scene(source)) + } + GraphicsRecord::Color => { + assert_eq!(values.graphics_color(joined), values.graphics_color(source)) + } + GraphicsRecord::Rect => { + assert_eq!(values.graphics_rect(joined), values.graphics_rect(source)) + } + GraphicsRecord::Text => { + assert_eq!(values.graphics_text(joined), values.graphics_text(source)) + } + } + values.drop_value(joined).unwrap(); + assert_eq!(snapshot(&values), original); + } + fixture.destroy(&mut values); + } +} + +#[test] +fn graphics_decode_accepts_joined_string_and_list_fields_without_consuming_originals() { + for depth in [1, 2] { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + for (owner, index, source, string, config, message) in [ + ( + fixture.config, + 0, + fixture.title, + true, + true, + "graphics: graphics.Config.title must be string", + ), + ( + fixture.text, + 2, + fixture.text_string, + true, + false, + "graphics: graphics.Text.text must be string", + ), + ( + fixture.scene, + 1, + fixture.rectangles, + false, + false, + "graphics: graphics.Scene.rectangles must be a list", + ), + ( + fixture.scene, + 2, + fixture.texts, + false, + false, + "graphics: graphics.Scene.texts must be a list", + ), + ] { + let original = snapshot(&values); + let mut joined = values.clone_with_pending_depth(source, depth).unwrap(); + for remaining in (0..depth).rev() { + values.structs.get_mut(&owner).unwrap().fields[index] + .as_mut() + .unwrap() + .bits = joined; + if config { + assert_error(values.graphics_config(fixture.config), message); + } else { + assert_error(values.graphics_scene(fixture.scene), message); + } + values.structs.get_mut(&owner).unwrap().fields[index] + .as_mut() + .unwrap() + .bits = source; + let previous = joined; + joined = if string { + values.join_string(previous).unwrap() + } else { + values.join_list(previous).unwrap() + }; + assert_eq!(values.owned_pending_depth(previous), Ok(remaining + 1)); + assert_eq!(values.owned_pending_depth(joined), Ok(remaining)); + values.drop_value(previous).unwrap(); + } + values.structs.get_mut(&owner).unwrap().fields[index] + .as_mut() + .unwrap() + .bits = joined; + assert!(values.graphics_config(fixture.config).is_ok()); + assert!(values.graphics_scene(fixture.scene).is_ok()); + values.structs.get_mut(&owner).unwrap().fields[index] + .as_mut() + .unwrap() + .bits = source; + values.drop_value(joined).unwrap(); + assert_eq!(snapshot(&values), original); + } + fixture.destroy(&mut values); + } +} + +#[test] +fn graphics_decode_failures_precede_provider_use_and_preserve_session_cleanup() { + let mut values = NativeValues::default(); + let fixture = Fixture::new(&mut values); + let authority = next_identity().unwrap(); + values.graphics = Some(authority); + let events = graphics::decode_test_script(&graphics::encode_test_script(&[ + graphics::TestEvent::Key(graphics::Key::Right), + graphics::TestEvent::Close, + ])) + .unwrap(); + values.graphics_script = Some(events.clone()); + Site::Field(fixture.config, 1).set(&mut values, 2); + assert_error( + values.graphics_open(authority, fixture.config), + "graphics: graphics.Config.width must be int64", + ); + assert!(values.graphics_session.is_none()); + assert!(GRAPHICS_SESSION.with(|slot| slot.borrow().is_none())); + assert_eq!(values.graphics_script.as_ref(), Some(&events)); + Site::Field(fixture.config, 1).set(&mut values, 0); + + let opened = values.graphics_open(authority, fixture.config).unwrap(); + assert_eq!(values.sums[&opened].tag, SUM_SUCCESS); + let session = values.sums[&opened].bits; + values.drop_value(opened).unwrap(); + Site::Field(fixture.text, 0).set(&mut values, 1); + let before = snapshot(&values); + assert_error( + values.graphics_present(session, fixture.scene), + "graphics: graphics.Text.x must be int64", + ); + assert_eq!(snapshot(&values), before); + assert_eq!(values.graphics_script.as_ref(), Some(&events)); + assert_eq!(values.graphics_session, Some(session)); + // This is the same cleanup path used by native context destruction after + // a terminal leaf failure; it must still work with pending owned sources. + values.release_graphics_session().unwrap(); + assert!(GRAPHICS_SESSION.with(|slot| slot.borrow().is_none())); + assert!(values.graphics_session.is_none()); + fixture.destroy(&mut values); +} diff --git a/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs b/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs new file mode 100644 index 00000000..2bf0c0f1 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/interface_argument_tests.rs @@ -0,0 +1,146 @@ +use super::*; +use interface_conversion::NativeInterfaceConversion as C; + +fn boxed_scalar(values: &mut NativeValues, owner: u64, depth: u64) -> u64 { + // Identity metadata is independent of the redacted payload's display. + let mut layout = b"JD\x01".to_vec(); + layout.extend_from_slice(&1_u32.to_le_bytes()); + layout.extend_from_slice(&0_u32.to_le_bytes()); + layout.extend_from_slice(&1_u32.to_le_bytes()); + layout.push(NativeDebugTag::Redacted as u8); + values + .interface_box(owner, 7, false, depth, &layout) + .unwrap() +} + +fn unbox(owner: u64) -> C { + C::Unbox { + concrete: owner, + owned: false, + nothing: false, + } +} + +fn input(bits: u64) -> NativeField { + NativeField { + bits, + owned: true, + pending_depth: 0, + } +} + +#[test] +fn interface_owner_preflight_rejects_exact_wrong_owner_without_conversion() { + let mut values = NativeValues::default(); + let original = boxed_scalar(&mut values, 10, 0); + let structs = values.structs.len(); + let bytes = values.bytes.len(); + values.allocation_budget = Some(0); + assert_eq!( + unbox(9).validate_owners(&values, input(original)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + assert_eq!(values.structs.len(), structs); + assert_eq!(values.bytes.len(), bytes); + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!(values.struct_field(original, 0).unwrap().bits, 10); + values.allocation_budget = None; + values.drop_value(original).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_matching_pending_payload_preserves_depth_and_ownership() { + let mut values = NativeValues::default(); + let original = boxed_scalar(&mut values, 9, 2); + values.allocation_budget = Some(0); + assert_eq!(unbox(9).validate_owners(&values, input(original)), Ok(())); + assert_eq!(values.owned_pending_depth(original), Ok(2)); + assert_eq!(values.allocation_budget, Some(0)); + values.allocation_budget = None; + values.drop_value(original).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_nested_list_optional_catches_later_mismatch_without_clones() { + let mut values = NativeValues::default(); + let matching = boxed_scalar(&mut values, 9, 0); + let wrong = boxed_scalar(&mut values, 10, 0); + let first = values.sum(SUM_SUCCESS, matching, true).unwrap(); + let second = values.sum(SUM_SUCCESS, wrong, true).unwrap(); + let list = values.new_list(true).unwrap(); + values.lists.get_mut(&list).unwrap().elements = vec![Some(first), Some(second)]; + let conversion = C::List(Box::new(C::Optional(Box::new(unbox(9))))); + let inventory = ( + values.lists.len(), + values.sums.len(), + values.structs.len(), + values.bytes.len(), + ); + values.allocation_budget = Some(0); + assert_eq!( + conversion.validate_owners(&values, input(list)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + assert_eq!( + ( + values.lists.len(), + values.sums.len(), + values.structs.len(), + values.bytes.len() + ), + inventory + ); + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!(values.struct_field(matching, 0).unwrap().bits, 9); + assert_eq!(values.struct_field(wrong, 0).unwrap().bits, 10); + values.allocation_budget = None; + values.drop_value(list).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_skips_absent_and_inactive_owner_payloads() { + let mut values = NativeValues::default(); + let absent = values.sum(SUM_FAILURE, 0, false).unwrap(); + let error = values.sum(SUM_FAILURE, 17, false).unwrap(); + assert_eq!( + C::Optional(Box::new(unbox(9))).validate_owners(&values, input(absent)), + Ok(()) + ); + assert_eq!( + C::Result(Box::new(unbox(9)), Box::new(C::Copy { owned: false })) + .validate_owners(&values, input(error)), + Ok(()) + ); + values.drop_value(absent).unwrap(); + values.drop_value(error).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_unavailable_container_does_not_read_children() { + let mut values = NativeValues::default(); + let source = values.new_list(true).unwrap(); + values.lists.get_mut(&source).unwrap().pending_depth = 1; + values.lists.get_mut(&source).unwrap().elements = vec![Some(u64::MAX)]; + assert_eq!( + C::List(Box::new(unbox(9))).validate_owners(&values, input(source)), + Err(INVALID_INTERFACE_ARGUMENT) + ); + values.lists.get_mut(&source).unwrap().elements.clear(); + values.drop_value(source).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn interface_owner_preflight_preserves_missing_handle_guard() { + let values = NativeValues::default(); + assert_eq!( + unbox(9).validate_owners(&values, input(u64::MAX)), + Err(INVALID_STRUCT) + ); + assert!(!C::Copy { owned: true }.requires_owner_validation()); + assert!(!C::FunctionAdapter { code: 123 }.requires_owner_validation()); +} diff --git a/crates/jett_runtime/src/native_abi/values/interface_conversion.rs b/crates/jett_runtime/src/native_abi/values/interface_conversion.rs new file mode 100644 index 00000000..31ee2d77 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/interface_conversion.rs @@ -0,0 +1,573 @@ +//! Compiler-described conversions across nested interface-compatible boundaries. +use super::*; + +/// A finite checked conversion tree. Nominal payloads stop at a box or copy; +/// this descriptor never searches declarations or executes source expressions. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NativeInterfaceConversion { + Copy { + owned: bool, + }, + Box { + concrete: u64, + owned: bool, + nothing: bool, + layout: Vec, + }, + Unbox { + concrete: u64, + owned: bool, + nothing: bool, + }, + List(Box), + Optional(Box), + Result(Box, Box), + Map(Box, bool), + FunctionAdapter { + code: u64, + }, +} + +impl NativeInterfaceConversion { + pub fn requires_owner_validation(&self) -> bool { + match self { + Self::Unbox { .. } => true, + Self::List(inner) | Self::Optional(inner) | Self::Map(inner, _) => { + inner.requires_owner_validation() + } + Self::Result(ok, error) => { + ok.requires_owner_validation() || error.requires_owner_validation() + } + Self::Copy { .. } | Self::Box { .. } | Self::FunctionAdapter { .. } => false, + } + } + + pub(super) fn validate_owners( + &self, + values: &NativeValues, + input: NativeField, + ) -> LeafResult<()> { + if !self.requires_owner_validation() { + return Ok(()); + } + match self { + Self::Unbox { + concrete, owned, .. + } => { + if !input.owned { + return Err(INVALID_STRUCT); + } + // Type metadata is available without inspecting, cloning or + // joining the concrete payload, including a pending payload. + if values.struct_field(input.bits, 0)?.bits != *concrete { + return Err(INVALID_INTERFACE_ARGUMENT); + } + if values.struct_field(input.bits, 1)?.owned != *owned { + return Err(INVALID_STRUCT); + } + Ok(()) + } + Self::List(element) => { + let list = values.lists.get(&input.bits).ok_or(INVALID_LIST)?; + if input.pending_depth != 0 || list.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + for (index, value) in list.elements.iter().enumerate() { + if let Some(bits) = value { + element.validate_owners( + values, + NativeField { + bits: *bits, + owned: list.owned, + pending_depth: list + .element_pending_depths + .get(&index) + .copied() + .unwrap_or(0), + }, + )?; + } + } + Ok(()) + } + Self::Map(element, strings) => { + let map = values.maps.get(&input.bits).ok_or(INVALID_MAP)?; + if input.pending_depth != 0 || map.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + if map.key_strings != *strings && map.entries.iter().any(Option::is_some) { + return Err(INVALID_MAP); + } + for entry in map.entries.iter().flatten() { + if entry.key_taken { + return Err(INVALID_MAP); + } + element.validate_owners( + values, + NativeField { + bits: entry.value, + owned: map.value_owned, + pending_depth: entry.value_pending_depth, + }, + )?; + } + Ok(()) + } + Self::Optional(ok) => Self::validate_sum_owners(values, input, ok, None), + Self::Result(ok, error) => Self::validate_sum_owners(values, input, ok, Some(error)), + Self::Copy { .. } | Self::Box { .. } | Self::FunctionAdapter { .. } => Ok(()), + } + } + + fn validate_sum_owners( + values: &NativeValues, + input: NativeField, + ok: &Self, + error: Option<&Self>, + ) -> LeafResult<()> { + let sum = values.sums.get(&input.bits).ok_or(INVALID_SUM)?; + if input.pending_depth != 0 || sum.pending_depth != 0 { + return Err(INVALID_INTERFACE_ARGUMENT); + } + let conversion = if sum.tag == SUM_SUCCESS { + Some(ok) + } else { + error + }; + if let Some(conversion) = conversion { + conversion.validate_owners( + values, + NativeField { + bits: sum.bits, + owned: sum.owned, + pending_depth: sum.payload_pending_depth, + }, + )?; + } + Ok(()) + } + + pub fn encode(&self) -> Vec { + self.encode_with_function_offsets().0 + } + /// Returns function-address fields for the compiler's object relocations. + /// At runtime these fields contain linked addresses, never function IDs. + pub fn encode_with_function_offsets(&self) -> (Vec, Vec<(usize, u64)>) { + let mut bytes = Vec::new(); + let mut functions = Vec::new(); + self.append(&mut bytes, &mut functions); + (bytes, functions) + } + fn append(&self, bytes: &mut Vec, functions: &mut Vec<(usize, u64)>) { + match self { + Self::Copy { owned } => bytes.extend_from_slice(&[0, u8::from(*owned)]), + Self::Box { + concrete, + owned, + nothing, + layout, + } => { + bytes.extend_from_slice(&[1, u8::from(*owned), u8::from(*nothing)]); + bytes.extend_from_slice(&concrete.to_le_bytes()); + bytes.extend_from_slice(&(layout.len() as u64).to_le_bytes()); + bytes.extend_from_slice(layout); + } + Self::Unbox { + concrete, + owned, + nothing, + } => { + bytes.extend_from_slice(&[2, u8::from(*owned), u8::from(*nothing)]); + bytes.extend_from_slice(&concrete.to_le_bytes()); + } + Self::List(inner) | Self::Optional(inner) => { + bytes.push(match self { + Self::List(_) => 3, + _ => 4, + }); + inner.append(bytes, functions); + } + Self::Map(inner, strings) => { + bytes.extend_from_slice(&[6, u8::from(*strings)]); + inner.append(bytes, functions); + } + Self::Result(ok, error) => { + bytes.push(5); + ok.append(bytes, functions); + error.append(bytes, functions); + } + Self::FunctionAdapter { code } => { + bytes.push(7); + functions.push((bytes.len(), *code)); + bytes.extend_from_slice(&code.to_le_bytes()); + } + } + } + pub(super) fn parse(bytes: &[u8]) -> LeafResult { + fn take<'a>(bytes: &mut &'a [u8], count: usize) -> LeafResult<&'a [u8]> { + let (head, tail) = bytes.split_at_checked(count).ok_or(INVALID_STRUCT)?; + *bytes = tail; + Ok(head) + } + fn number(bytes: &mut &[u8]) -> LeafResult { + Ok(u64::from_le_bytes( + take(bytes, 8)?.try_into().map_err(|_| INVALID_STRUCT)?, + )) + } + fn flag(bytes: &mut &[u8]) -> LeafResult { + match take(bytes, 1)?[0] { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(INVALID_STRUCT), + } + } + use NativeInterfaceConversion as C; + enum Parent { + List, + Optional, + Map(bool), + ResultOk, + ResultError(C), + } + let mut remaining = bytes; + let mut parents = Vec::new(); + loop { + let mut value = match take(&mut remaining, 1)?[0] { + 0 => C::Copy { + owned: flag(&mut remaining)?, + }, + tag @ (1 | 2) => { + let owned = flag(&mut remaining)?; + let nothing = flag(&mut remaining)?; + if owned && nothing { + return Err(INVALID_STRUCT); + } + let concrete = number(&mut remaining)?; + if tag == 1 { + let length = + usize::try_from(number(&mut remaining)?).map_err(|_| INVALID_STRUCT)?; + let layout = take(&mut remaining, length)?.to_vec(); + NativeDebugLayout::parse(&layout)?; + C::Box { + concrete, + owned, + nothing, + layout, + } + } else { + C::Unbox { + concrete, + owned, + nothing, + } + } + } + 3 => { + parents.push(Parent::List); + continue; + } + 4 => { + parents.push(Parent::Optional); + continue; + } + 5 => { + parents.push(Parent::ResultOk); + continue; + } + 6 => { + parents.push(Parent::Map(flag(&mut remaining)?)); + continue; + } + 7 => { + let code = number(&mut remaining)?; + if code == 0 { + return Err(INVALID_STRUCT); + } + C::FunctionAdapter { code } + } + _ => return Err(INVALID_STRUCT), + }; + loop { + value = match parents.pop() { + Some(Parent::List) => C::List(Box::new(value)), + Some(Parent::Optional) => C::Optional(Box::new(value)), + Some(Parent::Map(strings)) => C::Map(Box::new(value), strings), + Some(Parent::ResultOk) => { + parents.push(Parent::ResultError(value)); + break; + } + Some(Parent::ResultError(ok)) => C::Result(Box::new(ok), Box::new(value)), + None => { + if !remaining.is_empty() { + return Err(INVALID_STRUCT); + } + return Ok(value); + } + }; + } + } + } + + fn owned(&self) -> bool { + match self { + Self::Copy { owned } | Self::Unbox { owned, .. } => *owned, + _ => true, + } + } + pub(super) fn convert( + &self, + values: &mut NativeValues, + input: NativeField, + ) -> LeafResult { + let output = match self { + Self::Copy { owned } => { + if input.owned != *owned { + return Err(INVALID_STRUCT); + } + return Ok(NativeField { + bits: if *owned { + values.clone_value(input.bits)? + } else { + input.bits + }, + ..input + }); + } + Self::Box { + concrete, + owned, + nothing, + layout, + } => { + if input.owned != *owned { + return Err(INVALID_STRUCT); + } + let (bits, depth) = if *nothing { + (0, input.bits) + } else { + (input.bits, input.pending_depth) + }; + values.interface_box(*concrete, bits, *owned, depth, layout)? + } + Self::Unbox { + concrete, + owned, + nothing, + } => { + if !input.owned || values.struct_field(input.bits, 0)?.bits != *concrete { + return Err(INVALID_STRUCT); + } + let payload = values.struct_field(input.bits, 1)?; + if payload.owned != *owned { + return Err(INVALID_STRUCT); + } + let depth = values + .owned_pending_depth(input.bits)? + .checked_add(input.pending_depth) + .ok_or(EXHAUSTED)?; + return Ok(NativeField { + bits: if *owned { + values.clone_with_pending_depth(payload.bits, depth)? + } else if *nothing { + depth + } else { + payload.bits + }, + owned: *owned, + pending_depth: if *owned || *nothing { 0 } else { depth }, + }); + } + Self::List(element) => element.convert_list(values, input.bits)?, + Self::Map(element, strings) => element.convert_map(values, input.bits, *strings)?, + Self::Optional(ok) => Self::convert_sum(values, input.bits, ok, None)?, + Self::Result(ok, error) => Self::convert_sum(values, input.bits, ok, Some(error))?, + Self::FunctionAdapter { code } => { + if !input.owned { + return Err(INVALID_STRUCT); + } + values.function_adapter(input.bits, *code)? + } + }; + Ok(NativeField { + bits: output, + owned: true, + pending_depth: 0, + }) + } + fn convert_list(&self, values: &mut NativeValues, source: u64) -> LeafResult { + let list = values.lists.get(&source).ok_or(INVALID_LIST)?; + let (elements, depths, owned, pending) = ( + list.elements.clone(), + list.element_pending_depths.clone(), + list.owned, + list.pending_depth, + ); + let output = values.new_list(self.owned())?; + let result = (|| { + values + .lists + .get_mut(&output) + .ok_or(INVALID_LIST)? + .elements + .try_reserve_exact(elements.len()) + .map_err(|_| EXHAUSTED)?; + for (index, bits) in elements.into_iter().enumerate() { + let Some(bits) = bits else { + values + .lists + .get_mut(&output) + .ok_or(INVALID_LIST)? + .elements + .push(None); + continue; + }; + let converted = self.convert( + values, + NativeField { + bits, + owned, + pending_depth: depths.get(&index).copied().unwrap_or(0), + }, + )?; + let list = values.lists.get_mut(&output).ok_or(INVALID_LIST)?; + list.elements.push(Some(converted.bits)); + if converted.pending_depth != 0 { + list.element_pending_depths + .insert(index, converted.pending_depth); + } + } + values + .lists + .get_mut(&output) + .ok_or(INVALID_LIST)? + .pending_depth = pending; + Ok(output) + })(); + if result.is_err() { + let _ = values.drop_value(output); + } + result + } + fn convert_map( + &self, + values: &mut NativeValues, + source: u64, + target_strings: bool, + ) -> LeafResult { + let map = values.maps.get(&source).ok_or(INVALID_MAP)?; + let (entries, strings, owned, pending) = ( + map.entries.clone(), + map.key_strings, + map.value_owned, + map.pending_depth, + ); + if strings != target_strings && entries.iter().any(Option::is_some) { + return Err(INVALID_MAP); + } + let output = values.new_map(u32::from(target_strings), u32::from(self.owned()))?; + let result = (|| { + values + .maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .try_reserve_exact(entries.len()) + .map_err(|_| EXHAUSTED)?; + for entry in entries { + let Some(entry) = entry else { + values + .maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .push(None); + continue; + }; + if entry.key_taken { + return Err(INVALID_MAP); + } + let key = if strings { + values.clone_value(entry.key)? + } else { + entry.key + }; + let converted = match self.convert( + values, + NativeField { + bits: entry.value, + owned, + pending_depth: entry.value_pending_depth, + }, + ) { + Ok(value) => value, + Err(error) => { + if strings { + values.drop_value(key)?; + } + return Err(error); + } + }; + values + .maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .entries + .push(Some(NativeMapEntry { + key, + value: converted.bits, + value_pending_depth: converted.pending_depth, + ..entry + })); + } + values + .maps + .get_mut(&output) + .ok_or(INVALID_MAP)? + .pending_depth = pending; + Ok(output) + })(); + if result.is_err() { + let _ = values.drop_value(output); + } + result + } + fn convert_sum( + values: &mut NativeValues, + source: u64, + ok: &Self, + error: Option<&Self>, + ) -> LeafResult { + let sum = values.sums.get(&source).ok_or(INVALID_SUM)?; + let (tag, pending, input) = ( + sum.tag, + sum.pending_depth, + NativeField { + bits: sum.bits, + owned: sum.owned, + pending_depth: sum.payload_pending_depth, + }, + ); + let conversion = if tag == SUM_SUCCESS { Some(ok) } else { error }; + let payload = match conversion { + Some(conversion) => conversion.convert(values, input)?, + None => NativeField { + bits: 0, + owned: false, + pending_depth: 0, + }, + }; + let output = match values.sum(tag, payload.bits, payload.owned) { + Ok(output) => output, + Err(error) => { + if payload.owned { + values.drop_value(payload.bits)?; + } + return Err(error); + } + }; + let sum = values.sums.get_mut(&output).ok_or(INVALID_SUM)?; + sum.pending_depth = pending; + sum.payload_pending_depth = payload.pending_depth; + Ok(output) + } +} diff --git a/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs b/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs new file mode 100644 index 00000000..097f39bf --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/ordinary_attempts.rs @@ -0,0 +1,352 @@ +//! Context-owned ordinary first-error channels for exact Resource entry lifetimes. +//! Old records are sealed observations; a fresh constructor selects a new record. +use super::*; +#[cfg(test)] +use crate::native_abi::resource::ordinary_reservation_faults::{ + ReservationFaults, ReservationSite, +}; +use crate::native_abi::resource::{ResourceOrdinaryCompletion, ResourceOrdinaryOwner}; + +#[derive(Default)] +struct AttemptFailureChannel { + failure: Option, + dynamic_failure_message: Option>, + property_case_context: Option>, +} + +#[derive(Clone, Copy)] +enum OrdinaryAttemptPhase { + Running, + Sealed(ResourceOrdinaryCompletion), +} + +struct OrdinaryAttemptChannel { + owner: ResourceOrdinaryOwner, + phase: OrdinaryAttemptPhase, + error: AttemptFailureChannel, +} + +#[derive(Default)] +pub(super) struct ResourceOrdinaryChannels { + records: Vec, + selected: Option, + #[cfg(test)] + reservation_faults: ReservationFaults, +} + +/// Borrows the exact values state through Resource frame publication. Its private +/// fields and sole constructor prevent a raw ID from selecting a failure channel. +pub(in crate::native_abi) struct PreparedResourceOrdinaryAttempt<'a> { + values: &'a mut NativeValues, + owner: ResourceOrdinaryOwner, +} + +impl PreparedResourceOrdinaryAttempt<'_> { + pub(in crate::native_abi) fn publish(self) { + let index = self.values.resource_ordinary_channels.records.len(); + self.values + .resource_ordinary_channels + .records + .push(OrdinaryAttemptChannel { + owner: self.owner, + phase: OrdinaryAttemptPhase::Running, + error: AttemptFailureChannel::default(), + }); + self.values.resource_ordinary_channels.selected = Some(index); + #[cfg(all(test, jett_resource_native_test_archive))] + match self.owner.observation_identity() { + Some(identity) => self.values.test_observations.begin(identity), + None => self + .values + .test_observations + .incomplete(resource::test_observations::BOUNDARY), + } + } +} + +impl ResourceOrdinaryChannels { + fn selected(&self) -> Option<&OrdinaryAttemptChannel> { + self.selected.and_then(|index| self.records.get(index)) + } + + fn running_mut(&mut self) -> Option<&mut AttemptFailureChannel> { + let record = self + .selected + .and_then(|index| self.records.get_mut(index))?; + match record.phase { + OrdinaryAttemptPhase::Running => Some(&mut record.error), + OrdinaryAttemptPhase::Sealed(_) => None, + } + } +} + +impl NativeValues { + pub(in crate::native_abi) fn session_failure(&self) -> Option { + self.failure + } + + // Between-entry operations belong to the legacy session channel. They never + // mutate a sealed observation, and any such error still blocks the next begin. + pub(super) fn operation_failure(&self) -> Option { + self.failure.or_else(|| { + let record = self.resource_ordinary_channels.selected()?; + match record.phase { + OrdinaryAttemptPhase::Running => record.error.failure, + OrdinaryAttemptPhase::Sealed(_) => None, + } + }) + } + + pub(super) fn current_failure(&self) -> Option { + self.failure.or_else(|| { + self.resource_ordinary_channels + .selected() + .and_then(|record| record.error.failure) + }) + } + + pub(super) fn current_dynamic_failure_message(&self) -> Option<&[u8]> { + if self.failure.is_some() { + self.dynamic_failure_message.as_deref() + } else if let Some(record) = self.resource_ordinary_channels.selected() { + record.error.dynamic_failure_message.as_deref() + } else { + self.dynamic_failure_message.as_deref() + } + } + + pub(super) fn current_property_case_context(&self) -> Option<&[u8]> { + if self.failure.is_some() { + self.property_case_context.as_deref() + } else if let Some(record) = self.resource_ordinary_channels.selected() { + record.error.property_case_context.as_deref() + } else { + self.property_case_context.as_deref() + } + } + + pub(super) fn set_dynamic_failure_message(&mut self, message: Vec) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + if channel.failure.is_none() { + channel.dynamic_failure_message = Some(message); + } + } else if self.failure.is_none() { + self.dynamic_failure_message = Some(message); + } + } + + #[cfg(all(test, jett_resource_native_test_archive))] + pub(in crate::native_abi) fn test_observation_property_context(&self) -> Option<&[u8]> { + self.current_property_case_context() + } + #[cfg(all(test, jett_resource_native_test_archive))] + pub(in crate::native_abi) fn test_observation_set_property_context( + &mut self, + context: Option>, + ) { + self.set_property_case_context(context); + } + + pub(super) fn set_property_case_context(&mut self, context: Option>) { + #[cfg(all(test, jett_resource_native_test_archive))] + let observation_owner = self + .resource_ordinary_channels + .selected() + .filter(|record| matches!(record.phase, OrdinaryAttemptPhase::Running)) + .and_then(|record| record.owner.observation_identity()); + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + if channel.failure.is_none() { + #[cfg(all(test, jett_resource_native_test_archive))] + match observation_owner { + Some(identity) => self.test_observations.property_change( + resource::test_observations::Channel::Attempt(identity), + context.as_deref(), + ), + None => self + .test_observations + .incomplete(resource::test_observations::BOUNDARY), + } + channel.property_case_context = context; + } + } else if self.failure.is_none() { + #[cfg(all(test, jett_resource_native_test_archive))] + self.test_observations.property_change( + resource::test_observations::Channel::Session, + context.as_deref(), + ); + self.property_case_context = context; + } + } + + pub(super) fn record_ordinary_failure(&mut self, error: Failure) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + channel.failure.get_or_insert(error); + } else { + self.failure.get_or_insert(error); + } + } + + pub(super) fn can_capture_refinement_failure(&self) -> bool { + !matches!( + self.resource_ordinary_channels + .selected() + .map(|record| &record.phase), + Some(OrdinaryAttemptPhase::Sealed(_)) + ) + } + + // This is the existing Source refinement conversion only. It cannot reach a + // sealed terminal attempt and is never called by the entry constructor. + pub(super) fn finish_refinement_failure_capture(&mut self) { + if let Some(channel) = self.resource_ordinary_channels.running_mut() { + channel.failure = None; + channel.dynamic_failure_message = None; + } else { + self.failure = None; + self.dynamic_failure_message = None; + } + } + + pub(in crate::native_abi) fn prepare_resource_ordinary_attempt( + &mut self, + owner: ResourceOrdinaryOwner, + predecessor: Option, + ) -> LeafResult> { + if self.failure.is_some() + || self.cleanup_failed + || !self.is_empty() + || !owner.within_attempt_budget() + || owner.ordinal() != self.resource_ordinary_channels.records.len() + { + return Err(INVALID_HANDLE); + } + match (self.resource_ordinary_channels.selected(), predecessor) { + (None, None) if self.resource_ordinary_channels.records.is_empty() => {} + (Some(record), Some(previous)) => match record.phase { + OrdinaryAttemptPhase::Sealed(sealed) + if sealed == previous + && sealed.diagnostic_ready() + && record.owner == sealed.owner() + && owner.continues_installation(record.owner) => {} + OrdinaryAttemptPhase::Running | OrdinaryAttemptPhase::Sealed(_) => { + return Err(INVALID_HANDLE); + } + }, + _ => return Err(INVALID_HANDLE), + } + #[cfg(test)] + { + let channels = &mut self.resource_ordinary_channels; + channels + .reservation_faults + .checkpoint( + ReservationSite::OrdinaryChannel, + channels.records.len() == channels.records.capacity(), + ) + .map_err(|_| EXHAUSTED)?; + } + self.resource_ordinary_channels + .records + .try_reserve(1) + .map_err(|_| EXHAUSTED)?; + Ok(PreparedResourceOrdinaryAttempt { + values: self, + owner, + }) + } + + pub(in crate::native_abi) fn retire_resource_actors( + &mut self, + owner: ResourceOrdinaryOwner, + ) -> LeafResult<()> { + self.validate_resource_ordinary_attempt(owner)?; + if let Err(error) = self.release_actors() { + self.cleanup_failed = true; + self.record_ordinary_failure(error); + } + Ok(()) + } + + pub(in crate::native_abi) fn validate_resource_ordinary_attempt( + &self, + owner: ResourceOrdinaryOwner, + ) -> LeafResult<()> { + match self.resource_ordinary_channels.selected() { + Some(record) + if record.owner == owner + && matches!(record.phase, OrdinaryAttemptPhase::Running) => + { + Ok(()) + } + _ => Err(INVALID_HANDLE), + } + } + + pub(in crate::native_abi) fn seal_resource_ordinary_attempt( + &mut self, + completion: ResourceOrdinaryCompletion, + ) -> LeafResult<()> { + self.validate_resource_ordinary_attempt(completion.owner())?; + if !completion.matches_owner() { + return Err(INVALID_HANDLE); + } + let record = self + .resource_ordinary_channels + .selected + .and_then(|index| self.resource_ordinary_channels.records.get_mut(index)) + .ok_or(INVALID_HANDLE)?; + record.phase = OrdinaryAttemptPhase::Sealed(completion); + #[cfg(all(test, jett_resource_native_test_archive))] + match record.owner.observation_identity() { + Some(identity) => self + .test_observations + .end(identity, record.error.property_case_context.as_deref()), + None => self + .test_observations + .incomplete(resource::test_observations::BOUNDARY), + } + Ok(()) + } +} + +#[cfg(test)] +impl NativeValues { + pub(in crate::native_abi) fn test_arm_resource_ordinary_reservation(&mut self) { + self.resource_ordinary_channels + .reservation_faults + .arm(ReservationSite::OrdinaryChannel); + } + + pub(in crate::native_abi) fn test_resource_ordinary_reservation_snapshot( + &self, + ) -> (usize, usize, bool) { + self.resource_ordinary_channels + .reservation_faults + .snapshot(ReservationSite::OrdinaryChannel) + } + + pub(in crate::native_abi) fn test_resource_ordinary_channel_capacity(&self) -> (usize, usize) { + let channels = &self.resource_ordinary_channels; + (channels.records.len(), channels.records.capacity()) + } + + pub(in crate::native_abi) fn test_resource_ordinary_channel_state( + &self, + ) -> ( + usize, + Option, + Option, + ) { + let channels = &self.resource_ordinary_channels; + let selected = channels.selected(); + let completion = selected.and_then(|record| match record.phase { + OrdinaryAttemptPhase::Running => None, + OrdinaryAttemptPhase::Sealed(completion) => Some(completion), + }); + ( + channels.records.len(), + selected.map(|record| record.owner), + completion, + ) + } +} diff --git a/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs b/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs new file mode 100644 index 00000000..685b1a22 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/ordinary_borrowed_sum_tests.rs @@ -0,0 +1,356 @@ +//! Nonconsuming ordinary sum reads keep the original shell and payload owners. +use super::*; +use std::mem::MaybeUninit; + +struct BorrowContext { + record: Box, + live: bool, +} + +impl BorrowContext { + fn new() -> Self { + let mut record = Box::new(JettRuntimeContextV1::retired()); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_create(1, &mut *record, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + Self { record, live: true } + } + + fn pointer(&self) -> *const JettRuntimeContextV1 { + &*self.record + } + + fn values(&self, inspect: impl FnOnce(&mut NativeValues) -> R) -> R { + let lease = acquire_context(context_key(self.pointer()).unwrap()).unwrap(); + let mut state = lock_unpoisoned(&lease.entry.state); + inspect(&mut state.as_mut().unwrap().values) + } + + fn retire(&mut self) { + assert!(self.live); + let mut result = MaybeUninit::uninit(); + assert_eq!( + unsafe { jett_rt_v1_context_destroy(&mut *self.record, result.as_mut_ptr()) }, + JettRuntimeStatusV1::OK + ); + self.live = false; + } +} + +impl Drop for BorrowContext { + fn drop(&mut self) { + if self.live { + self.retire(); + } + } +} + +fn scalar_sum(context: &BorrowContext, tag: u32, bits: u64, depth: u64) -> u64 { + let sum = unsafe { jett_rt_v1_sum_new_scalar_task(context.pointer(), tag, bits, depth) }; + assert_ne!(sum, 0); + sum +} + +fn drop_sum(context: &BorrowContext, sum: u64) { + assert_eq!(unsafe { jett_rt_v1_value_drop(context.pointer(), sum) }, 0); +} + +#[test] +fn ordinary_borrowed_sum_leaf_schema_matches_the_checked_native_call() { + assert_eq!( + NativeLeaf::SumPayloadBorrow.symbol(), + "jett_rt_v1_sum_payload_borrow" + ); + assert_eq!( + NativeLeaf::SumPayloadBorrow.parameters(), + &[AbiScalar::Pointer, AbiScalar::I64, AbiScalar::I32] + ); + assert_eq!(NativeLeaf::SumPayloadBorrow.result(), AbiScalar::I64); + let _: unsafe extern "C" fn(*const JettRuntimeContextV1, u64, u32) -> u64 = + jett_rt_v1_sum_payload_borrow; +} + +#[test] +fn ordinary_borrowed_sum_repeated_nested_reads_keep_identity_without_allocating() { + let context = BorrowContext::new(); + let pointer = context.pointer(); + let bytes = unsafe { jett_rt_v1_bytes_new(pointer) }; + let list = unsafe { jett_rt_v1_list_new(pointer, 1) }; + assert_eq!( + unsafe { jett_rt_v1_list_append(pointer, list, bytes) }, + list + ); + let inner = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, list, 1) }; + let outer = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, inner, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + for _ in 0..3 { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, outer, SUM_SUCCESS) }, + inner + ); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, inner, SUM_SUCCESS) }, + list + ); + } + context.values(|values| { + assert_eq!(values.allocation_budget, Some(0)); + assert_eq!((values.sums_created, values.sums_destroyed), (2, 0)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (1, 0)); + assert!(values.sums[&outer].owned && values.sums[&inner].owned); + assert_eq!(values.lists[&list].elements, vec![Some(bytes)]); + assert!(values.bytes.contains_key(&bytes)); + assert!(values.failure.is_none()); + assert!(values.dynamic_failure_message.is_none()); + }); + drop_sum(&context, outer); + context.values(|values| { + assert_eq!((values.sums_created, values.sums_destroyed), (2, 2)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 1)); + assert_eq!((values.bytes_created, values.bytes_destroyed), (1, 1)); + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); +} + +#[test] +fn ordinary_borrowed_sum_failure_string_needs_an_explicit_owned_reference() { + let context = BorrowContext::new(); + let text = context.values(|values| { + let text = values.insert("failure payload".into()).unwrap(); + values.strings.get_mut(&text).unwrap().pending_depth = 3; + text + }); + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), SUM_FAILURE, text, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + for _ in 0..3 { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_FAILURE) }, + text + ); + } + context.values(|values| { + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.strings[&text].pending_depth, 3); + assert_eq!(values.sums[&sum].payload_pending_depth, 0); + assert_eq!(values.allocation_budget, Some(0)); + }); + let retained = unsafe { jett_rt_v1_string_retain(context.pointer(), text) }; + assert_eq!(retained, text); + context.values(|values| assert_eq!(values.strings[&text].references, 2)); + drop_sum(&context, sum); + context.values(|values| { + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.strings[&text].pending_depth, 3); + assert_eq!(values.text(text), Ok("failure payload")); + }); + assert_eq!( + unsafe { jett_rt_v1_string_release(context.pointer(), retained) }, + 0 + ); + context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_scalar_bits_and_pending_depth_are_not_flattened() { + for tag in [SUM_FAILURE, SUM_SUCCESS] { + for (bits, depth) in [(0, 0), (u64::MAX, 1), (7, 2), (0, u64::MAX)] { + let context = BorrowContext::new(); + let sum = scalar_sum(&context, tag, bits, depth); + context.values(|values| values.allocation_budget = Some(0)); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, tag) }, + bits + ); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_pending_depth(context.pointer(), sum) }, + depth + ); + assert_eq!(unsafe { jett_rt_v1_value_status(context.pointer()) }, 0); + context.values(|values| { + let shell = &values.sums[&sum]; + assert_eq!((shell.tag, shell.bits, shell.owned), (tag, bits, false)); + assert_eq!( + (shell.pending_depth, shell.payload_pending_depth), + (0, depth) + ); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + assert_eq!(values.allocation_budget, Some(0)); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); + } + } +} + +#[test] +fn ordinary_borrowed_sum_ready_shell_keeps_pending_list_and_element_depth() { + let context = BorrowContext::new(); + let pointer = context.pointer(); + let list = unsafe { jett_rt_v1_list_new(pointer, 0) }; + assert_eq!( + unsafe { jett_rt_v1_list_append_scalar_task(pointer, list, 5, 2) }, + list + ); + context.values(|values| values.lists.get_mut(&list).unwrap().pending_depth = 3); + let sum = unsafe { jett_rt_v1_sum_new(pointer, SUM_SUCCESS, list, 1) }; + context.values(|values| values.allocation_budget = Some(0)); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, sum, SUM_SUCCESS) }, + list + ); + assert_eq!( + unsafe { jett_rt_v1_list_element_pending_depth(pointer, list, 0) }, + 2 + ); + context.values(|values| { + assert_eq!(values.lists[&list].pending_depth, 3); + assert_eq!(values.lists[&list].element_pending_depths.get(&0), Some(&2)); + assert_eq!(values.sums[&sum].pending_depth, 0); + assert_eq!(values.sums[&sum].payload_pending_depth, 0); + assert_eq!(values.allocation_budget, Some(0)); + assert!(values.failure.is_none()); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_wrong_tags_and_outer_pending_keep_the_owned_shell() { + for (tag, selected, outer_depth) in [ + (SUM_SUCCESS, SUM_FAILURE, 0), + (SUM_FAILURE, SUM_SUCCESS, 0), + (SUM_SUCCESS, 2, 0), + (SUM_SUCCESS, u32::MAX, 0), + (SUM_SUCCESS, SUM_SUCCESS, 1), + (SUM_FAILURE, SUM_FAILURE, 2), + (SUM_SUCCESS, SUM_SUCCESS, u64::MAX), + ] { + let context = BorrowContext::new(); + let list = unsafe { jett_rt_v1_list_new(context.pointer(), 0) }; + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), tag, list, 1) }; + context.values(|values| { + let shell = values.sums.get_mut(&sum).unwrap(); + shell.pending_depth = outer_depth; + shell.payload_pending_depth = 7; + values.allocation_budget = Some(0); + }); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, selected) }, + 0 + ); + assert_eq!( + unsafe { jett_rt_v1_value_status(context.pointer()) }, + JettRuntimeStatusV1::INVALID_ARGUMENT.code() + ); + context.values(|values| { + let shell = &values.sums[&sum]; + assert_eq!((shell.tag, shell.bits, shell.owned), (tag, list, true)); + assert_eq!( + (shell.pending_depth, shell.payload_pending_depth), + (outer_depth, 7) + ); + assert_eq!(values.failure, Some(INVALID_SUM)); + assert!(values.dynamic_failure_message.is_none()); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + assert_eq!(values.allocation_budget, Some(0)); + }); + drop_sum(&context, sum); + context.values(|values| { + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); + } +} + +#[test] +fn ordinary_borrowed_sum_foreign_stale_and_non_sum_ids_keep_other_owners() { + let foreign_context = BorrowContext::new(); + let foreign = scalar_sum(&foreign_context, SUM_SUCCESS, 41, 0); + for invalid_kind in 0..5 { + let context = BorrowContext::new(); + let list = unsafe { jett_rt_v1_list_new(context.pointer(), 0) }; + let sum = unsafe { jett_rt_v1_sum_new(context.pointer(), SUM_SUCCESS, list, 1) }; + let stale = scalar_sum(&context, SUM_SUCCESS, 43, 0); + drop_sum(&context, stale); + let invalid = match invalid_kind { + 0 => 0, + 1 => u64::MAX, + 2 => foreign, + 3 => list, + 4 => stale, + _ => unreachable!(), + }; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), invalid, SUM_SUCCESS) }, + 0 + ); + // This leaf is not a cleanup operation: terminal failure inhibits it. + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); + context.values(|values| { + assert_eq!(values.failure, Some(INVALID_SUM)); + assert_eq!(values.sums[&sum].bits, list); + assert!(values.sums[&sum].owned); + assert!(values.lists.contains_key(&list)); + assert_eq!((values.sums_created, values.sums_destroyed), (2, 1)); + assert_eq!((values.lists_created, values.lists_destroyed), (1, 0)); + }); + drop_sum(&context, sum); + context.values(|values| { + assert!(values.is_empty()); + assert!(!values.cleanup_failed); + }); + } + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(foreign_context.pointer(), foreign, SUM_SUCCESS) }, + 41 + ); + drop_sum(&foreign_context, foreign); + foreign_context.values(|values| assert!(values.is_empty())); +} + +#[test] +fn ordinary_borrowed_sum_null_copied_mismatched_and_retired_contexts_are_inert() { + let mut context = BorrowContext::new(); + let sum = scalar_sum(&context, SUM_SUCCESS, 47, 0); + let copied = JettRuntimeContextV1 { + token: context.record.token, + abi_version: context.record.abi_version, + state_tag: context.record.state_tag, + }; + let retired = JettRuntimeContextV1::retired(); + for pointer in [ptr::null(), &copied, &retired] { + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(pointer, sum, SUM_SUCCESS) }, + 0 + ); + } + context.record.abi_version = 2; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); + context.record.abi_version = JETT_RUNTIME_ABI_VERSION_V1; + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 47 + ); + context.values(|values| { + assert!(values.failure.is_none()); + assert_eq!((values.sums_created, values.sums_destroyed), (1, 0)); + }); + drop_sum(&context, sum); + context.values(|values| assert!(values.is_empty())); + context.retire(); + assert_eq!( + unsafe { jett_rt_v1_sum_payload_borrow(context.pointer(), sum, SUM_SUCCESS) }, + 0 + ); +} diff --git a/crates/jett_runtime/src/native_abi/values/reflected_container_tests.rs b/crates/jett_runtime/src/native_abi/values/reflected_container_tests.rs new file mode 100644 index 00000000..d88a7a49 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/reflected_container_tests.rs @@ -0,0 +1,127 @@ +use super::*; + +fn wrapper(values: &mut NativeValues, kind: u32) -> u64 { + match kind { + 2 => values.new_list(false).unwrap(), + 5 => values.new_set(0).unwrap(), + 6 => values.new_map(0, 0).unwrap(), + 7 => values.sum(SUM_SUCCESS, 7, false).unwrap(), + _ => unreachable!(), + } +} + +fn pending(values: &mut NativeValues, source: u64, kind: u32) -> u64 { + match kind { + 2 => values.run_list(source).unwrap(), + 5 => values.run_set(source).unwrap(), + 6 => values.run_map(source).unwrap(), + 7 => values.run_sum(source).unwrap(), + _ => unreachable!(), + } +} + +#[test] +fn reflected_container_readiness_preserves_source_and_exact_depth_without_observation() { + for (kind, message) in [ + (2, "reflected field refinement: expected a ready list value"), + (5, "reflected field refinement: expected a ready set value"), + (6, "reflected field refinement: expected a ready map value"), + ( + 7, + "reflected field refinement: expected a ready optional value", + ), + ( + 7, + "reflected field refinement: expected a ready result value", + ), + ] { + let mut values = NativeValues::default(); + let source = wrapper(&mut values, kind); + let once = pending(&mut values, source, kind); + let twice = pending(&mut values, once, kind); + assert_eq!( + values.reject_pending_handle(source, kind, message.as_bytes()), + Ok(0) + ); + assert!(values.dynamic_failure_message.is_none()); + for (value, depth) in [(once, 1), (twice, 2)] { + assert!( + values + .reject_pending_handle(value, kind, message.as_bytes()) + .is_err() + ); + assert_eq!( + values.dynamic_failure_message.as_deref(), + Some(message.as_bytes()) + ); + assert_eq!(values.owned_pending_depth(value), Ok(depth)); + assert_eq!(values.owned_pending_depth(source), Ok(0)); + } + for value in [source, once, twice] { + values.drop_value(value).unwrap(); + } + assert!(values.is_empty()); + } +} + +#[test] +fn reflected_container_readiness_rejects_wrong_kind_foreign_stale_and_unknown_tags() { + let mut values = NativeValues::default(); + let mut foreign = NativeValues::default(); + let message = b"reflected field refinement: expected a ready map value"; + for kind in [2, 5, 6, 7] { + let source = wrapper(&mut values, kind); + for wrong in [2, 5, 6, 7].into_iter().filter(|wrong| *wrong != kind) { + assert!( + values + .reject_pending_handle(source, wrong, message) + .is_err() + ); + assert!(values.dynamic_failure_message.is_none()); + assert_eq!(values.owned_pending_depth(source), Ok(0)); + } + let alien = wrapper(&mut foreign, kind); + assert!(values.reject_pending_handle(alien, kind, message).is_err()); + assert!(values.dynamic_failure_message.is_none()); + foreign.drop_value(alien).unwrap(); + assert!( + values + .reject_pending_handle(source, u32::MAX, message) + .is_err() + ); + values.drop_value(source).unwrap(); + assert!(values.reject_pending_handle(source, kind, message).is_err()); + assert!(values.dynamic_failure_message.is_none()); + } + assert!(values.is_empty()); + assert!(foreign.is_empty()); +} + +#[test] +fn reflected_container_readiness_ignores_pending_children_and_keeps_owned_payloads() { + let mut values = NativeValues::default(); + let list = values.new_list(false).unwrap(); + values.lists.get_mut(&list).unwrap().elements.push(Some(7)); + values + .lists + .get_mut(&list) + .unwrap() + .element_pending_depths + .insert(0, 2); + let text = values.insert("Agent-λ🙂".into()).unwrap(); + values.strings.get_mut(&text).unwrap().pending_depth = 2; + let sum = values.sum(SUM_SUCCESS, text, true).unwrap(); + for (value, kind) in [(list, 2), (sum, 7)] { + assert_eq!( + values.reject_pending_handle(value, kind, b"must not inspect a child"), + Ok(0) + ); + } + assert!(values.dynamic_failure_message.is_none()); + assert_eq!(values.lists[&list].element_pending_depths[&0], 2); + assert_eq!(values.strings[&text].pending_depth, 2); + assert_eq!(values.strings[&text].text, "Agent-λ🙂"); + values.drop_value(list).unwrap(); + values.drop_value(sum).unwrap(); + assert!(values.is_empty()); +} diff --git a/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs b/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs new file mode 100644 index 00000000..f6d9fc92 --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/resource_companion_tests.rs @@ -0,0 +1,184 @@ +//! Malformed-host controls for the actual typed Resource failure-companion drop. +use super::*; +use crate::resource_custody::{NativeLayoutInstallation, RegisteredNativeLayout}; +use std::sync::Arc; + +fn layout() -> Arc { + let mut bytes = b"JTRSC001".to_vec(); + let words = |bytes: &mut Vec, values: &[u32]| { + for value in values { + bytes.extend_from_slice(&value.to_le_bytes()); + } + }; + words(&mut bytes, &[2, 0]); + bytes.extend_from_slice(&0u64.to_le_bytes()); + words(&mut bytes, &[0, 0, 1, 8, 1, 0, 0, 0]); + words(&mut bytes, &[0, 0, 0]); // pure Nothing signature + for row in [ + vec![0, 5], + vec![1, 1, 64, 1], + vec![2, 4], + vec![3, 8, 1], + vec![4, 8, 2], + vec![5, 8, 4], + vec![6, 9, 1, 4], + vec![7, 9, 4, 1], + ] { + words(&mut bytes, &row); + } + words(&mut bytes, &[0, 1, 0, 0, 0, 0, 0, 1, 0, 0]); + let length = bytes.len() as u64; + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + let registry = ResourceRegistry::new(); + NativeLayoutInstallation::new(®istry) + .install(®istry, &bytes) + .unwrap() +} + +#[test] +fn resource_ordinary_companion_owned_integer_cannot_drop_an_unrelated_live_string() { + let layout = layout(); + let mut values = NativeValues::default(); + let unrelated = values.insert("unrelated owner".into()).unwrap(); + let malformed = values.sum(SUM_SUCCESS, unrelated, true).unwrap(); + assert!( + values + .validate_resource_ordinary(malformed, &layout, 3) + .is_err() + ); + assert!( + values + .drop_resource_typed_companion(&layout, 3, malformed) + .is_err() + ); + assert!(values.strings.contains_key(&unrelated)); + assert!(values.sums.contains_key(&malformed)); + values.sums.get_mut(&malformed).unwrap().owned = false; + values + .drop_resource_typed_companion(&layout, 3, malformed) + .unwrap(); + assert!(values.strings.contains_key(&unrelated)); + values.drop_value(unrelated).unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn resource_ordinary_companion_nested_failure_requires_each_immediate_owner_bit() { + let layout = layout(); + for missing_owner in 0..2 { + let mut values = NativeValues::default(); + let text = values.insert("failure payload".into()).unwrap(); + let inner = values.sum(SUM_SUCCESS, text, missing_owner != 0).unwrap(); + let outer = values.sum(SUM_FAILURE, inner, missing_owner != 1).unwrap(); + assert!( + values + .validate_resource_ordinary(outer, &layout, 6) + .is_err() + ); + assert!( + values + .drop_resource_typed_companion(&layout, 6, outer) + .is_err() + ); + assert!(values.strings.contains_key(&text)); + assert!(values.sums.contains_key(&inner) && values.sums.contains_key(&outer)); + values.sums.get_mut(&inner).unwrap().owned = true; + values.sums.get_mut(&outer).unwrap().owned = true; + values + .drop_resource_typed_companion(&layout, 6, outer) + .unwrap(); + assert!(values.is_empty()); + assert!( + values + .drop_resource_typed_companion(&layout, 6, outer) + .is_err() + ); + } +} + +#[test] +fn resource_ordinary_companion_absence_and_primitive_failure_do_not_own_payload_bits() { + let layout = layout(); + let mut values = NativeValues::default(); + let unrelated = values.insert("still live".into()).unwrap(); + let failed = values.sum(SUM_FAILURE, unrelated, true).unwrap(); + assert!( + values + .drop_resource_typed_companion(&layout, 7, failed) + .is_err() + ); + assert!(values.strings.contains_key(&unrelated)); + values.sums.get_mut(&failed).unwrap().owned = false; + values + .drop_resource_typed_companion(&layout, 7, failed) + .unwrap(); + assert!(values.strings.contains_key(&unrelated)); + let absent = values.sum(SUM_FAILURE, 0, false).unwrap(); + values + .drop_resource_typed_companion(&layout, 5, absent) + .unwrap(); + let inner = values.sum(SUM_SUCCESS, unrelated, true).unwrap(); + let nested = values.sum(SUM_SUCCESS, inner, true).unwrap(); + values + .drop_resource_typed_companion(&layout, 5, nested) + .unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn resource_carrier_companion_keeps_pending_string_layers_through_clone_and_drop() { + let layout = layout(); + let mut values = NativeValues::default(); + let text = values.insert_pending("retained".into(), 3).unwrap(); + assert!( + values.validate_resource_ordinary(text, &layout, 2).is_err(), + "an inspection remains unavailable" + ); + values + .validate_resource_carrier_companion(text, &layout, 2, 0) + .unwrap(); + assert!( + values + .validate_resource_carrier_companion(text, &layout, 2, 1) + .is_err(), + "a string handle cannot acquire a second scalar depth" + ); + let clone = values + .clone_resource_carrier_string_companion(&layout, 2, text) + .unwrap(); + assert_eq!(values.strings[&clone].pending_depth, 3); + values + .drop_resource_carrier_companion(&layout, 2, text, 0) + .unwrap(); + assert_eq!(values.strings[&clone].pending_depth, 3); + values + .drop_resource_carrier_companion(&layout, 2, clone, 0) + .unwrap(); + assert!(values.is_empty()); +} + +#[test] +fn resource_carrier_companion_pending_nothing_keeps_its_exact_payload_contract() { + let layout = layout(); + let mut values = NativeValues::default(); + for depth in [0, 1, 3, u64::MAX] { + values + .validate_resource_carrier_companion(0, &layout, 0, depth) + .unwrap(); + assert!( + values + .validate_resource_carrier_companion(1, &layout, 0, depth) + .is_err(), + "pending layers never authorize non-Nothing bits" + ); + assert!( + values + .drop_resource_carrier_companion(&layout, 0, 1, depth) + .is_err() + ); + values + .drop_resource_carrier_companion(&layout, 0, 0, depth) + .unwrap(); + assert!(values.is_empty()); + } +} diff --git a/crates/jett_runtime/src/native_abi/values/resource_sum_replacement.rs b/crates/jett_runtime/src/native_abi/values/resource_sum_replacement.rs new file mode 100644 index 00000000..0c4c434d --- /dev/null +++ b/crates/jett_runtime/src/native_abi/values/resource_sum_replacement.rs @@ -0,0 +1,257 @@ +//! Validate the two ordinary failure obligations before a Resource sum retires. +use super::*; +use crate::resource_custody::{NativeShape, RegisteredNativeLayout}; + +#[derive(Default)] +struct CompanionClaims { + sums: Vec, + string: Option, +} + +impl NativeValues { + pub(in crate::native_abi) fn preflight_resource_sum_companions( + &self, + layout: &RegisteredNativeLayout, + old: Option<(u32, u64)>, + replacement: Option<(u32, u64)>, + ) -> LeafResult<()> { + let old = self.resource_sum_companion_claims(layout, old)?; + let replacement = self.resource_sum_companion_claims(layout, replacement)?; + // Ordinary sum cloning allocates distinct shells. Equal shells cannot + // represent two owning failure obligations, including nested children. + if old.sums.iter().any(|id| replacement.sums.contains(id)) { + return Err(INVALID_HANDLE); + } + if let (Some(old), Some(replacement)) = (old.string, replacement.string) { + if old == replacement && self.strings.get(&old).ok_or(INVALID_HANDLE)?.references < 2 { + return Err(INVALID_HANDLE); + } + } + Ok(()) + } + + fn resource_sum_companion_claims( + &self, + layout: &RegisteredNativeLayout, + companion: Option<(u32, u64)>, + ) -> LeafResult { + let Some((mut shape, mut value)) = companion else { + return Ok(CompanionClaims::default()); + }; + self.validate_resource_ordinary(value, layout, shape)?; + let mut claims = CompanionClaims::default(); + loop { + match layout.shapes().get(shape as usize).ok_or(INVALID_HANDLE)? { + NativeShape::String => { + claims.string = Some(value); + return Ok(claims); + } + NativeShape::Optional { child } => { + self.resource_sum_claim(&mut claims, value)?; + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + if sum.tag == SUM_FAILURE { + return Ok(claims); + } + shape = *child; + value = sum.bits; + } + NativeShape::Result { ok, fail } => { + self.resource_sum_claim(&mut claims, value)?; + let sum = self.sums.get(&value).ok_or(INVALID_HANDLE)?; + shape = if sum.tag == SUM_SUCCESS { *ok } else { *fail }; + value = sum.bits; + } + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing + | NativeShape::Network => return Ok(claims), + _ => return Err(INVALID_HANDLE), + } + } + } + + fn resource_sum_claim(&self, claims: &mut CompanionClaims, value: u64) -> LeafResult<()> { + // A malformed cyclic/repeated selected shell must fail before drop_value + // can remove an outer shell and discover its child has gone stale. + if claims.sums.contains(&value) { + return Err(INVALID_HANDLE); + } + claims.sums.try_reserve(1).map_err(|_| EXHAUSTED)?; + claims.sums.push(value); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ResourceRegistry; + use crate::resource_custody::NativeLayoutInstallation; + + fn layout() -> Arc { + fn words(bytes: &mut Vec, values: &[u32]) { + for value in values { + bytes.extend(value.to_le_bytes()); + } + } + // Independent ordinary shapes, admitted by the real layout issuer. + let mut bytes = b"JTRSC001".to_vec(); + words(&mut bytes, &[1, 0]); + bytes.extend(0u64.to_le_bytes()); + words(&mut bytes, &[0, 0, 1, 6, 1, 0, 0, 0]); + for row in [ + vec![0, 0, 2], + vec![0, 1, 64, 1], + vec![1, 4], + vec![2, 5], + vec![3, 8, 1], + vec![4, 8, 3], + vec![5, 9, 0, 1], + vec![0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + ] { + words(&mut bytes, &row); + } + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); + let registry = ResourceRegistry::new(); + NativeLayoutInstallation::new(®istry) + .install(®istry, &bytes) + .unwrap() + } + + #[test] + fn companion_replacement_requires_real_string_clone_and_preserves_exact_references() { + let layout = layout(); + let mut values = NativeValues::default(); + let text = values.insert("shared".into()).unwrap(); + let pair = Some((1, text)); + assert!( + values + .preflight_resource_sum_companions(&layout, pair, pair) + .is_err() + ); + assert_eq!(values.strings[&text].references, 1); + assert_eq!(values.clone_value(text).unwrap(), text); + values + .preflight_resource_sum_companions(&layout, pair, pair) + .unwrap(); + assert_eq!(values.strings[&text].references, 2); + values + .drop_resource_typed_companion(&layout, 1, text) + .unwrap(); + assert_eq!(values.strings[&text].references, 1); + values.validate_resource_ordinary(text, &layout, 1).unwrap(); + values + .drop_resource_typed_companion(&layout, 1, text) + .unwrap(); + assert!(values.strings.is_empty()); + } + + #[test] + fn companion_replacement_nested_clones_have_distinct_shells_and_one_shared_string() { + let layout = layout(); + let mut values = NativeValues::default(); + let text = values.insert("nested".into()).unwrap(); + let inner = values.sum(SUM_SUCCESS, text, true).unwrap(); + let old = values.sum(SUM_SUCCESS, inner, true).unwrap(); + let replacement = values.clone_value(old).unwrap(); + assert_ne!(old, replacement); + assert_ne!(values.sums[&old].bits, values.sums[&replacement].bits); + assert_eq!(values.strings[&text].references, 2); + values + .preflight_resource_sum_companions(&layout, Some((4, old)), Some((4, replacement))) + .unwrap(); + values + .drop_resource_typed_companion(&layout, 4, old) + .unwrap(); + assert!(!values.sums.contains_key(&old)); + assert!(!values.sums.contains_key(&inner)); + assert_eq!(values.strings[&text].references, 1); + values + .validate_resource_ordinary(replacement, &layout, 4) + .unwrap(); + values + .drop_resource_typed_companion(&layout, 4, replacement) + .unwrap(); + assert!(values.sums.is_empty()); + assert!(values.strings.is_empty()); + assert_eq!(values.sums_created, values.sums_destroyed); + } + + #[test] + fn companion_replacement_rejects_shared_nested_shell_before_any_drop() { + let layout = layout(); + let mut values = NativeValues::default(); + let text = values.insert("one child".into()).unwrap(); + let inner = values.sum(SUM_SUCCESS, text, true).unwrap(); + let old = values.sum(SUM_SUCCESS, inner, true).unwrap(); + let invalid = values.sum(SUM_SUCCESS, inner, true).unwrap(); + assert!( + values + .preflight_resource_sum_companions(&layout, Some((4, old)), Some((4, invalid))) + .is_err() + ); + assert_eq!(values.sums.len(), 3); + assert_eq!(values.strings[&text].references, 1); + // Undo only the deliberately malformed second ownership claim. + let duplicate = values.sums.remove(&invalid).unwrap(); + assert_eq!(duplicate.bits, inner); + values.sums_destroyed += 1; + values + .drop_resource_typed_companion(&layout, 4, old) + .unwrap(); + assert!(values.sums.is_empty()); + assert!(values.strings.is_empty()); + } + + #[test] + fn companion_replacement_rejects_repeated_shell_and_stale_child_before_any_drop() { + let layout = layout(); + let mut values = NativeValues::default(); + let cyclic = values.sum(SUM_SUCCESS, 0, true).unwrap(); + values.sums.get_mut(&cyclic).unwrap().bits = cyclic; + assert!( + values + .preflight_resource_sum_companions(&layout, Some((4, cyclic)), None) + .is_err() + ); + assert_eq!(values.sums.len(), 1); + let stale = values.sum(SUM_SUCCESS, u64::MAX, true).unwrap(); + assert!( + values + .preflight_resource_sum_companions(&layout, Some((3, stale)), None) + .is_err() + ); + assert_eq!(values.sums.len(), 2); + // These malformed shells own no actual ordinary payload. + for shell in [cyclic, stale] { + values.sums.remove(&shell).unwrap(); + values.sums_destroyed += 1; + } + assert!(values.sums.is_empty()); + assert!(values.strings.is_empty()); + } + + #[test] + fn companion_replacement_primitive_and_none_branches_need_no_ordinary_storage() { + let layout = layout(); + let mut values = NativeValues::default(); + values + .preflight_resource_sum_companions(&layout, Some((0, 17)), Some((0, 29))) + .unwrap(); + let old = values.sum(SUM_FAILURE, 0, false).unwrap(); + let replacement = values.sum(SUM_FAILURE, 0, false).unwrap(); + values + .preflight_resource_sum_companions(&layout, Some((4, old)), Some((4, replacement))) + .unwrap(); + values + .drop_resource_typed_companion(&layout, 4, old) + .unwrap(); + values + .drop_resource_typed_companion(&layout, 4, replacement) + .unwrap(); + assert!(values.sums.is_empty()); + assert!(values.strings.is_empty()); + } +} diff --git a/crates/jett_runtime/src/random.rs b/crates/jett_runtime/src/random.rs new file mode 100644 index 00000000..82995ae6 --- /dev/null +++ b/crates/jett_runtime/src/random.rs @@ -0,0 +1,160 @@ +//! Context-bound production and scripted Random providers. + +use std::collections::VecDeque; + +use rand::{RngCore, SeedableRng, rngs::StdRng}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RandomTestSample { + Bounded(u64), + Unit53(u64), + Boolean(bool), +} + +pub const TEST_SCRIPT_ENV: &str = "JETT_NATIVE_TEST_RANDOM_SCRIPT_V1"; +pub const INVALID_SCRIPT: &str = "Random: invalid native test script"; +pub const ENTROPY_UNAVAILABLE: &str = "Random: entropy unavailable"; +pub const TEST_EXHAUSTED: &str = "Random: test provider exhausted"; +pub const INVALID_SAMPLE: &str = "Random: invalid test sample"; + +pub fn encode_test_script(samples: &[RandomTestSample]) -> String { + samples + .iter() + .map(|sample| match sample { + RandomTestSample::Bounded(value) => format!("B,{value}"), + RandomTestSample::Unit53(value) => format!("U,{value}"), + RandomTestSample::Boolean(false) => "F".to_string(), + RandomTestSample::Boolean(true) => "T".to_string(), + }) + .collect::>() + .join(";") +} + +pub fn decode_test_script(script: &str) -> Result, &'static str> { + if script.is_empty() { + return Ok(VecDeque::new()); + } + script + .split(';') + .map(|entry| { + if entry == "F" { + return Ok(RandomTestSample::Boolean(false)); + } + if entry == "T" { + return Ok(RandomTestSample::Boolean(true)); + } + let (kind, value) = entry.split_once(',').ok_or(INVALID_SCRIPT)?; + let value = value.parse::().map_err(|_| INVALID_SCRIPT)?; + match kind { + "B" => Ok(RandomTestSample::Bounded(value)), + "U" => Ok(RandomTestSample::Unit53(value)), + _ => Err(INVALID_SCRIPT), + } + }) + .collect() +} + +pub enum RandomProvider { + Production(StdRng), + Scripted(VecDeque), +} + +impl RandomProvider { + pub fn production() -> Result { + let mut seed = ::Seed::default(); + rand::rngs::OsRng + .try_fill_bytes(seed.as_mut()) + .map_err(|_| ENTROPY_UNAVAILABLE)?; + Ok(Self::Production(StdRng::from_seed(seed))) + } + + pub fn scripted(samples: VecDeque) -> Self { + Self::Scripted(samples) + } + + pub fn scripted_samples_remaining(&self) -> Option { + match self { + Self::Production(_) => None, + Self::Scripted(samples) => Some(samples.len()), + } + } + + pub fn bounded_offset(&mut self, width: u64) -> Result { + debug_assert!(width > 0); + match self { + Self::Production(rng) => Ok(unbiased_bounded_offset(width, || rng.next_u64())), + Self::Scripted(samples) => match samples.front().copied() { + None => Err(TEST_EXHAUSTED), + Some(RandomTestSample::Bounded(offset)) if offset < width => { + samples.pop_front(); + Ok(offset) + } + Some(_) => Err(INVALID_SAMPLE), + }, + } + } + + pub fn unit53(&mut self) -> Result { + match self { + Self::Production(rng) => Ok(rng.next_u64() >> 11), + Self::Scripted(samples) => match samples.front().copied() { + None => Err(TEST_EXHAUSTED), + Some(RandomTestSample::Unit53(bits)) if bits < (1_u64 << 53) => { + samples.pop_front(); + Ok(bits) + } + Some(_) => Err(INVALID_SAMPLE), + }, + } + } + + pub fn boolean(&mut self) -> Result { + match self { + Self::Production(rng) => Ok(rng.next_u32() & 1 == 1), + Self::Scripted(samples) => match samples.front().copied() { + None => Err(TEST_EXHAUSTED), + Some(RandomTestSample::Boolean(value)) => { + samples.pop_front(); + Ok(value) + } + Some(_) => Err(INVALID_SAMPLE), + }, + } + } +} + +pub fn unbiased_bounded_offset(width: u64, mut next_word: impl FnMut() -> u64) -> u64 { + debug_assert!(width > 0); + let rejection_threshold = width.wrapping_neg() % width; + loop { + let word = next_word(); + if word >= rejection_threshold { + return word % width; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scripted_samples_roundtrip_and_validate_before_advancing() { + let samples = [ + RandomTestSample::Bounded(u64::MAX), + RandomTestSample::Unit53(0), + RandomTestSample::Boolean(false), + RandomTestSample::Boolean(true), + ]; + assert_eq!( + decode_test_script(&encode_test_script(&samples)), + Ok(samples.into()) + ); + for invalid in ["B", "U,no", "B,-1", "B,1,2", "T;"] { + assert_eq!(decode_test_script(invalid), Err(INVALID_SCRIPT)); + } + let mut provider = RandomProvider::scripted(VecDeque::from([RandomTestSample::Bounded(4)])); + assert_eq!(provider.bounded_offset(4), Err(INVALID_SAMPLE)); + assert_eq!(provider.scripted_samples_remaining(), Some(1)); + } +} diff --git a/crates/jett_runtime/src/resource_custody.rs b/crates/jett_runtime/src/resource_custody.rs new file mode 100644 index 00000000..5140d6d7 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody.rs @@ -0,0 +1,1081 @@ +//! Registry-backed ownership protocol, independent of compiler/interpreter data. +//! Native ABI transport remains a separate boundary from layout registration. + +use std::any::Any; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::{ + AuthorityProvenance, RegistryError, ResourceKey, ResourceRegistry, ResourceTypeId, + discard_panic_payload, +}; + +#[path = "resource_custody/carrier_holders.rs"] +mod carrier_holders; +use carrier_holders::CarrierHolderRoot; +pub(crate) use carrier_holders::{CarrierRootToken, RegisteredCarrierSlot}; + +static NEXT_CUSTODY: AtomicU64 = AtomicU64::new(1); + +#[path = "resource_custody/registration.rs"] +mod registration; +#[cfg(test)] +pub(crate) use registration::carrier_runtime_fixture_bytes; +pub(crate) use registration::{ + JettResourceCallResultV1, NATIVE_RESOURCE_LAYOUT_WIRE_VERSION, NativeAccess, + NativeCarrierChild, NativeCarrierChildSource, NativeCarrierDestination, NativeCarrierField, + NativeCarrierLayout, NativeCarrierLoanSource, NativeCarrierMember, NativeCarrierNode, + NativeCarrierObservation, NativeCarrierOperation, NativeCarrierOperationRecord, + NativeCarrierPath, NativeCarrierSelector, NativeCarrierSlot, NativeCarrierSource, NativeFormal, + NativeFrame, NativeFrameRole, NativeHook, NativeLayoutInstallation, NativeLoanSource, + NativeOperation, NativeOperationRecord, NativeParent, NativePayloadStep, NativePosition, + NativeRecipe, NativeShape, NativeSignature, NativeSite, NativeSlot, NativeSourceEffect, + NativeSourceFormal, NativeSourceInvocation, NativeSourceResult, NativeSourceSyntax, + NativeSourceValue, NativeSumLoanSource, RESOURCE_DOMAIN_ERROR, RESOURCE_DOMAIN_OK, + RegisteredNativeLayout, ResourceLayoutError, +}; + +#[derive(Debug)] +struct ProgramLayout { + context: u64, + kinds: Vec, + slots: Vec, +} + +/// Only a constructor-owned registration can create this immutable identity. +/// The private registration child is the sole production issuer; tests also +/// have a private issuer. Neither accepts a registry key as an owner. +#[derive(Debug, Clone)] +pub(crate) struct RegisteredResourceProgram(Arc); + +#[derive(Debug, Clone)] +pub(crate) struct RegisteredResourceKind { + program: Arc, + index: usize, +} + +#[derive(Debug, Clone)] +pub(crate) struct RegisteredOwnerSlot { + program: Arc, + index: usize, +} + +impl RegisteredResourceProgram { + pub(crate) fn kind(&self, index: usize) -> Result { + self.0.kinds.get(index).ok_or(CustodyError::WrongKind)?; + Ok(RegisteredResourceKind { + program: self.0.clone(), + index, + }) + } + + pub(crate) fn slot(&self, index: usize) -> Result { + self.0.slots.get(index).ok_or(CustodyError::UnknownSlot)?; + Ok(RegisteredOwnerSlot { + program: self.0.clone(), + index, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourcePurpose { + Runtime, + NamespaceConstant, + ExplicitComptime, + Verify, + Property, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceFrameKind { + Scope, + Operation, + Return, +} + +#[derive(Debug)] +pub(crate) struct ResourceFrameToken { + core: u64, + index: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HolderAddress { + Slot(usize), + Carrier { root: usize, path: usize }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Holder { + frame: usize, + address: HolderAddress, +} + +#[derive(Debug)] +pub(crate) struct ResourceHolderToken { + core: u64, + holder: Holder, +} + +/// Neither Clone/Copy nor a finalizing Rust Drop. The ledger owns cleanup. +#[derive(Debug)] +pub(crate) struct OwnedResourceToken { + core: u64, + owner: usize, + holder_generation: u64, +} + +#[derive(Debug)] +pub(crate) struct BorrowedResourceToken { + core: u64, + loan: usize, +} + +/// Preflight is unoccupied metadata, not a provider grant or resource owner. +#[derive(Debug)] +pub(crate) struct PreparedResourceAcquisition { + core: u64, + holder: Holder, + kind: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CustodyError { + WrongContext, + WrongProgram, + WrongKind, + UnknownSlot, + InvalidFrame, + OutOfOrderFrame, + InvalidOwner, + StaleHolderGeneration, + InvalidLoan, + ActiveBorrow, + OccupiedHolder, + WrongPurpose, + CapacityExhausted, + UnownedRegistry, + Registry(RegistryError), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceCleanupFailure { + Custody(CustodyError), + FinalizerPanic, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct ResourceCleanupOutcome { + first: Option, +} + +impl ResourceCleanupOutcome { + pub(crate) fn failure(&self) -> Option { + self.first + } + fn fail(&mut self, failure: ResourceCleanupFailure) { + self.first.get_or_insert(failure); + } + fn merge(&mut self, other: Self) { + if let Some(failure) = other.first { + self.fail(failure); + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceTransitionFailure { + Rejected(CustodyError), + Cleanup(ResourceCleanupFailure), +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceBodyOutcome { + Completed(Result), + HostPanic, +} + +impl ResourceBodyOutcome { + pub(crate) fn caught(outcome: std::thread::Result>) -> Self { + match outcome { + Ok(result) => Self::Completed(result), + Err(payload) => { + discard_panic_payload(payload); + Self::HostPanic + } + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ResourceCompletionFailure { + Body(E), + BodyPanic, + Cleanup(ResourceCleanupFailure), +} + +pub(crate) fn complete_resource( + body: ResourceBodyOutcome, + cleanup: ResourceCleanupOutcome, +) -> Result> { + if let Some(failure) = cleanup.first { + return Err(ResourceCompletionFailure::Cleanup(failure)); + } + match body { + ResourceBodyOutcome::Completed(Ok(value)) => Ok(value), + ResourceBodyOutcome::Completed(Err(error)) => Err(ResourceCompletionFailure::Body(error)), + ResourceBodyOutcome::HostPanic => Err(ResourceCompletionFailure::BodyPanic), + } +} + +#[derive(Clone, Copy)] +struct Acquisition { + owner: usize, + holder: Holder, + generation: u64, +} + +struct Frame { + kind: ResourceFrameKind, + purpose: ResourcePurpose, + live: bool, + acquisitions: Vec, + loans: Vec, +} + +struct Owner { + kind: usize, + key: ResourceKey, + authority: AuthorityProvenance, + holder: Holder, + generation: u64, + live: bool, +} + +struct Loan { + owner: usize, + generation: u64, + frame: usize, + live: bool, +} + +/// Private test-only distinction between the two real begin-frame allocations. +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FrameReservationSite { + Frames, + ActiveFrames, +} + +#[cfg(test)] +impl FrameReservationSite { + fn index(self) -> usize { + match self { + Self::Frames => 0, + Self::ActiveFrames => 1, + } + } +} + +pub(crate) struct ResourceCustody { + id: u64, + program: RegisteredResourceProgram, + frames: Vec, + active: Vec, + owners: Vec, + loans: Vec, + #[cfg(test)] + frame_reservation_fault: Option, + #[cfg(test)] + frame_reservation_observations: [(usize, usize); 2], + carrier_roots: Vec, +} + +impl ResourceCustody { + pub(crate) fn new( + program: RegisteredResourceProgram, + registry: &ResourceRegistry, + ) -> Result { + if program.0.context != registry.context_id { + return Err(CustodyError::WrongContext); + } + if registry.live_count() != 0 { + return Err(CustodyError::UnownedRegistry); + } + let id = NEXT_CUSTODY + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + next.checked_add(1) + }) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(Self { + id, + program, + frames: Vec::new(), + active: Vec::new(), + owners: Vec::new(), + loans: Vec::new(), + #[cfg(test)] + frame_reservation_fault: None, + #[cfg(test)] + frame_reservation_observations: [(0, 0); 2], + carrier_roots: Vec::new(), + }) + } + + fn registry(&self, registry: &ResourceRegistry) -> Result<(), CustodyError> { + if self.program.0.context == registry.context_id { + Ok(()) + } else { + Err(CustodyError::WrongContext) + } + } + + fn frame(&self, index: usize) -> Result<&Frame, CustodyError> { + self.frames + .get(index) + .filter(|frame| frame.live) + .ok_or(CustodyError::InvalidFrame) + } + + fn frame_token(&self, token: &ResourceFrameToken) -> Result { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.frame(token.index)?; + Ok(token.index) + } + + fn holder_token(&self, token: &ResourceHolderToken) -> Result { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.frame(token.holder.frame)?; + self.holder_kind(token.holder)?; + Ok(token.holder) + } + + fn available(&self, holder: Holder) -> Result<(), CustodyError> { + if self + .owners + .iter() + .any(|owner| owner.live && owner.holder == holder) + { + return Err(CustodyError::OccupiedHolder); + } + Ok(()) + } + + fn runtime_frame(&self, frame: usize) -> Result<(), CustodyError> { + if self.frame(frame)?.purpose == ResourcePurpose::Runtime { + Ok(()) + } else { + Err(CustodyError::WrongPurpose) + } + } + + fn kind(&self, token: &RegisteredResourceKind) -> Result { + if !Arc::ptr_eq(&token.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .kinds + .get(token.index) + .ok_or(CustodyError::WrongKind)?; + Ok(token.index) + } + + #[cfg(test)] + pub(crate) fn test_arm_frame_reservation(&mut self, site: FrameReservationSite) { + assert!( + self.frame_reservation_fault.is_none(), + "one pending frame reservation fault" + ); + self.frame_reservation_fault = Some(site); + } + + #[cfg(test)] + fn test_frame_reservation_checkpoint( + &mut self, + site: FrameReservationSite, + growing: bool, + ) -> Result<(), CustodyError> { + if !growing { + return Ok(()); + } + let observation = &mut self.frame_reservation_observations[site.index()]; + observation.0 += 1; + if self.frame_reservation_fault == Some(site) { + self.frame_reservation_fault = None; + observation.1 += 1; + return Err(CustodyError::CapacityExhausted); + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn test_frame_reservation_snapshot( + &self, + site: FrameReservationSite, + ) -> (usize, usize, bool) { + let (growth, refusal) = self.frame_reservation_observations[site.index()]; + (growth, refusal, self.frame_reservation_fault == Some(site)) + } + + pub(crate) fn begin_frame( + &mut self, + kind: ResourceFrameKind, + purpose: ResourcePurpose, + ) -> Result { + if let Some(parent) = self.active.last() { + if self.frame(*parent)?.purpose != purpose { + return Err(CustodyError::WrongPurpose); + } + } else if self.live_owners() != 0 + || self.live_loans() != 0 + || self.live_carrier_roots() != 0 + { + return Err(CustodyError::InvalidFrame); + } + #[cfg(test)] + self.test_frame_reservation_checkpoint( + FrameReservationSite::Frames, + self.frames.len() == self.frames.capacity(), + )?; + self.frames + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + #[cfg(test)] + self.test_frame_reservation_checkpoint( + FrameReservationSite::ActiveFrames, + self.active.len() == self.active.capacity(), + )?; + self.active + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let index = self.frames.len(); + self.frames.push(Frame { + kind, + purpose, + live: true, + acquisitions: Vec::new(), + loans: Vec::new(), + }); + self.active.push(index); + Ok(ResourceFrameToken { + core: self.id, + index, + }) + } + + pub(crate) fn holder( + &self, + frame: &ResourceFrameToken, + slot: &RegisteredOwnerSlot, + ) -> Result { + let frame = self.frame_token(frame)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .slots + .get(slot.index) + .ok_or(CustodyError::UnknownSlot)?; + Ok(ResourceHolderToken { + core: self.id, + holder: Holder { + frame, + address: HolderAddress::Slot(slot.index), + }, + }) + } + + fn acquisition_storage(&mut self, holder: Holder, kind: usize) -> Result<(), CustodyError> { + self.runtime_frame(holder.frame)?; + if self.holder_kind(holder)? != kind { + return Err(CustodyError::WrongKind); + } + self.available(holder)?; + self.owners + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + pub(crate) fn prepare_acquisition( + &mut self, + kind: &RegisteredResourceKind, + destination: &ResourceHolderToken, + ) -> Result { + let kind = self.kind(kind)?; + let holder = self.holder_token(destination)?; + self.acquisition_storage(holder, kind)?; + Ok(PreparedResourceAcquisition { + core: self.id, + holder, + kind, + }) + } + + pub(crate) fn commit_acquisition( + &mut self, + registry: &mut ResourceRegistry, + prepared: PreparedResourceAcquisition, + authority: AuthorityProvenance, + payload: T, + finalizer: F, + ) -> Result + where + T: Any + Send, + F: FnOnce(T) + Send + 'static, + { + let checked = (|| { + self.registry(registry)?; + if prepared.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.program + .0 + .kinds + .get(prepared.kind) + .ok_or(CustodyError::WrongKind)?; + self.frame(prepared.holder.frame)?; + let next_slot = registry + .free_slots + .last() + .copied() + .unwrap_or(registry.slots.len()); + u32::try_from(next_slot).map_err(|_| CustodyError::CapacityExhausted)?; + self.acquisition_storage(prepared.holder, prepared.kind) + })(); + if let Err(error) = checked { + let cleanup = catch_unwind(AssertUnwindSafe(|| finalizer(payload))); + return Err(match cleanup { + Ok(()) => ResourceTransitionFailure::Rejected(error), + Err(panic) => { + discard_panic_payload(panic); + ResourceTransitionFailure::Cleanup(ResourceCleanupFailure::FinalizerPanic) + } + }); + } + let registry_type = self.program.0.kinds[prepared.kind]; + let inserted = catch_unwind(AssertUnwindSafe(|| { + registry.insert(registry_type, payload, authority, finalizer) + })); + let key = match inserted { + Ok(Ok(key)) => key, + Ok(Err(error)) => { + return Err(ResourceTransitionFailure::Rejected(CustodyError::Registry( + error, + ))); + } + Err(panic) => { + discard_panic_payload(panic); + return Err(ResourceTransitionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic, + )); + } + }; + // All ledger capacity is reserved before physical insertion. No fallible + // publication remains between insertion and the logged owning record. + let owner = self.owners.len(); + self.owners.push(Owner { + kind: prepared.kind, + key, + authority, + holder: prepared.holder, + generation: 1, + live: true, + }); + self.frames[prepared.holder.frame] + .acquisitions + .push(Acquisition { + owner, + holder: prepared.holder, + generation: 1, + }); + Ok(OwnedResourceToken { + core: self.id, + owner, + holder_generation: 1, + }) + } + + fn owner(&self, token: &OwnedResourceToken) -> Result<&Owner, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + let owner = self + .owners + .get(token.owner) + .filter(|owner| owner.live) + .ok_or(CustodyError::InvalidOwner)?; + if owner.generation != token.holder_generation { + return Err(CustodyError::StaleHolderGeneration); + } + self.frame(owner.holder.frame)?; + if self.holder_kind(owner.holder)? != owner.kind { + return Err(CustodyError::WrongKind); + } + Ok(owner) + } + + fn validate_registry_owner( + &self, + owner: &Owner, + registry: &ResourceRegistry, + ) -> Result { + self.registry(registry)?; + let registry_type = *self + .program + .0 + .kinds + .get(owner.kind) + .ok_or(CustodyError::WrongKind)?; + let index = registry + .validate_slot(owner.key, registry_type) + .map_err(CustodyError::Registry)?; + let entry = registry.slots[index] + .entry + .as_ref() + .ok_or(CustodyError::Registry(RegistryError::Retired))?; + if entry.authority != owner.authority { + return Err(CustodyError::Registry(RegistryError::AuthorityMismatch)); + } + Ok(owner.key) + } + + fn borrowed(&self, owner: usize) -> bool { + self.loans + .iter() + .any(|loan| loan.live && loan.owner == owner) + } + + pub(crate) fn validate_owned( + &self, + owner: &OwnedResourceToken, + registry: &ResourceRegistry, + ) -> Result { + self.validate_registry_owner(self.owner(owner)?, registry) + } + + fn checked_transfer( + &self, + token: &OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(Holder, u64), CustodyError> { + let holder = self.holder_token(destination)?; + self.runtime_frame(holder.frame)?; + let owner = self.owner(token)?; + self.validate_registry_owner(owner, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + if self.holder_kind(holder)? != owner.kind { + return Err(CustodyError::WrongKind); + } + self.available(holder)?; + let generation = owner + .generation + .checked_add(1) + .ok_or(CustodyError::CapacityExhausted)?; + Ok((holder, generation)) + } + + /// Authenticate the exact occupied destination and reserve the impending + /// transfer before replacement runs either owner's finalizer. No reusable + /// transfer permission is returned; the ordinary transfer rechecks it later. + pub(crate) fn preflight_replacement( + &mut self, + old: &OwnedResourceToken, + replacement: &OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let holder = self.holder_token(destination)?; + self.runtime_frame(holder.frame)?; + let old_owner = self.owner(old)?; + self.validate_registry_owner(old_owner, registry)?; + if old_owner.holder != holder || old.owner == replacement.owner { + return Err(CustodyError::InvalidOwner); + } + let replacement_owner = self.owner(replacement)?; + self.runtime_frame(replacement_owner.holder.frame)?; + self.validate_registry_owner(replacement_owner, registry)?; + if self.borrowed(old.owner) || self.borrowed(replacement.owner) { + return Err(CustodyError::ActiveBorrow); + } + if self.holder_kind(holder)? != replacement_owner.kind { + return Err(CustodyError::WrongKind); + } + if self + .owners + .iter() + .enumerate() + .any(|(index, owner)| index != old.owner && owner.live && owner.holder == holder) + { + return Err(CustodyError::OccupiedHolder); + } + replacement_owner + .generation + .checked_add(1) + .ok_or(CustodyError::CapacityExhausted)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + /// Preflight only a direct-sum whole-binding replacement. Absent shells do + /// not fabricate owners; occupied tokens authenticate both real holders. + /// No token or reusable transfer permission escapes this reservation. + pub(crate) fn preflight_direct_sum_replacement( + &mut self, + old: Option<&OwnedResourceToken>, + replacement: Option<&OwnedResourceToken>, + destination: &ResourceHolderToken, + source: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let destination_holder = self.holder_token(destination)?; + let source_holder = self.holder_token(source)?; + if destination_holder == source_holder + || self.holder_kind(destination_holder)? != self.holder_kind(source_holder)? + { + return Err(CustodyError::InvalidOwner); + } + for (token, holder) in [(old, destination_holder), (replacement, source_holder)] { + let Some(token) = token else { + self.available(holder)?; + continue; + }; + self.runtime_frame(holder.frame)?; + let owner = self.owner(token)?; + self.validate_registry_owner(owner, registry)?; + if owner.holder != holder || self.holder_kind(holder)? != owner.kind { + return Err(CustodyError::InvalidOwner); + } + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + if self + .owners + .iter() + .enumerate() + .any(|(index, other)| index != token.owner && other.live && other.holder == holder) + { + return Err(CustodyError::OccupiedHolder); + } + } + match (old, replacement) { + (Some(old), Some(replacement)) => { + self.preflight_replacement(old, replacement, destination, registry) + } + (None, Some(replacement)) => { + let (holder, _) = self.checked_transfer(replacement, destination, registry)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted) + } + (Some(_), None) | (None, None) => Ok(()), + } + } + + /// A Source activation reserves its entire exact formal handoff before any + /// owner moves. References are existing live tokens and registered metadata; + /// this creates neither ownership nor a reusable transfer permission. + pub(crate) fn preflight_transfer_batch( + &mut self, + frame: &ResourceFrameToken, + transfers: &[(&OwnedResourceToken, &RegisteredOwnerSlot)], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let frame_index = self.frame_token(frame)?; + let mut checked: Vec<(usize, Holder)> = Vec::new(); + checked + .try_reserve_exact(transfers.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for &(token, slot) in transfers { + let destination = self.holder(frame, slot)?; + let (holder, _) = self.checked_transfer(token, &destination, registry)?; + if checked + .iter() + .any(|(owner, previous)| *owner == token.owner || *previous == holder) + { + return Err(CustodyError::OccupiedHolder); + } + checked.push((token.owner, holder)); + } + self.frames[frame_index] + .acquisitions + .try_reserve(transfers.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + Ok(()) + } + + pub(crate) fn transfer( + &mut self, + token: &mut OwnedResourceToken, + destination: &ResourceHolderToken, + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + let (holder, generation) = self.checked_transfer(token, destination, registry)?; + self.frames[holder.frame] + .acquisitions + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.owners[token.owner].holder = holder; + self.owners[token.owner].generation = generation; + self.frames[holder.frame].acquisitions.push(Acquisition { + owner: token.owner, + holder, + generation, + }); + token.holder_generation = generation; + Ok(()) + } + + pub(crate) fn begin_borrow( + &mut self, + token: &OwnedResourceToken, + frame: &ResourceFrameToken, + registry: &ResourceRegistry, + ) -> Result { + let frame = self.frame_token(frame)?; + self.runtime_frame(frame)?; + let owner = self.owner(token)?; + self.validate_registry_owner(owner, registry)?; + let owner_position = self + .active + .iter() + .position(|value| *value == owner.holder.frame) + .ok_or(CustodyError::InvalidFrame)?; + let borrower_position = self + .active + .iter() + .position(|value| *value == frame) + .ok_or(CustodyError::InvalidFrame)?; + if borrower_position < owner_position { + return Err(CustodyError::InvalidLoan); + } + let generation = owner.generation; + self.loans + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + self.frames[frame] + .loans + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let loan = self.loans.len(); + self.loans.push(Loan { + owner: token.owner, + generation, + frame, + live: true, + }); + self.frames[frame].loans.push(loan); + Ok(BorrowedResourceToken { + core: self.id, + loan, + }) + } + + fn loan(&self, token: &BorrowedResourceToken) -> Result<&Loan, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + let loan = self + .loans + .get(token.loan) + .filter(|loan| loan.live) + .ok_or(CustodyError::InvalidLoan)?; + self.frame(loan.frame)?; + let owner = self + .owners + .get(loan.owner) + .filter(|owner| owner.live && owner.generation == loan.generation) + .ok_or(CustodyError::InvalidLoan)?; + self.frame(owner.holder.frame)?; + Ok(loan) + } + + pub(crate) fn validate_borrowed( + &self, + token: &BorrowedResourceToken, + registry: &ResourceRegistry, + ) -> Result { + let loan = self.loan(token)?; + self.validate_registry_owner(&self.owners[loan.owner], registry) + } + + pub(crate) fn end_borrow( + &mut self, + token: &mut BorrowedResourceToken, + ) -> Result<(), CustodyError> { + self.loan(token)?; + self.loans[token.loan].live = false; + Ok(()) + } + + fn finalize( + &mut self, + owner: usize, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + if self.borrowed(owner) { + outcome.fail(ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow)); + return outcome; + } + let record = &self.owners[owner]; + if let Err(error) = self.validate_registry_owner(record, registry) { + // An externally retired/stale entry is already unavailable. Retire + // its obligation too, never invoke a different generation's finalizer. + if matches!( + error, + CustodyError::Registry( + RegistryError::UnknownSlot + | RegistryError::StaleGeneration + | RegistryError::Retired + ) + ) { + self.owners[owner].live = false; + } + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + let key = record.key; + let registry_type = self.program.0.kinds[record.kind]; + self.owners[owner].live = false; + match catch_unwind(AssertUnwindSafe(|| registry.close(key, registry_type))) { + Ok(Ok(())) => {} + Ok(Err(error)) => outcome.fail(ResourceCleanupFailure::Custody( + CustodyError::Registry(error), + )), + Err(panic) => { + discard_panic_payload(panic); + outcome.fail(ResourceCleanupFailure::FinalizerPanic); + } + } + outcome + } + + pub(crate) fn close( + &mut self, + token: &mut OwnedResourceToken, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + let checked = self + .registry(registry) + .and_then(|()| self.owner(token)) + .and_then(|owner| { + self.runtime_frame(owner.holder.frame)?; + self.validate_registry_owner(owner, registry).map(|_| ()) + }); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + self.finalize(token.owner, registry) + } + + fn end_index( + &mut self, + frame: usize, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + let checked = self + .registry(registry) + .and_then(|()| self.frame(frame).map(|_| ())) + .and_then(|()| { + if self.active.last() == Some(&frame) { + Ok(()) + } else { + Err(CustodyError::OutOfOrderFrame) + } + }); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + for loan in &self.frames[frame].loans { + self.loans[*loan].live = false; + } + let mut acquisitions = std::mem::take(&mut self.frames[frame].acquisitions); + for acquisition in acquisitions.iter().rev() { + let owner = &self.owners[acquisition.owner]; + if !owner.live + || owner.holder != acquisition.holder + || owner.generation != acquisition.generation + { + continue; + } + outcome.merge(self.finalize(acquisition.owner, registry)); + } + acquisitions.retain(|acquisition| { + let owner = &self.owners[acquisition.owner]; + owner.live + && owner.holder == acquisition.holder + && owner.generation == acquisition.generation + }); + if acquisitions.is_empty() { + self.frames[frame].live = false; + self.active.pop(); + } else { + // A malformed longer-lived lease must not turn refusal into frame + // retirement. Retain its acquisitions for a later valid completion. + self.frames[frame].acquisitions = acquisitions; + } + outcome + } + + pub(crate) fn end_frame( + &mut self, + frame: &ResourceFrameToken, + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + match self.frame_token(frame) { + Ok(index) => self.end_index(index, registry), + Err(error) => ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::Custody(error)), + }, + } + } + + pub(crate) fn unwind_all(&mut self, registry: &mut ResourceRegistry) -> ResourceCleanupOutcome { + let mut outcome = ResourceCleanupOutcome::default(); + while let Some(frame) = self.active.last().copied() { + outcome.merge(self.end_index(frame, registry)); + if self.active.last() == Some(&frame) { + break; + } + } + outcome + } + + pub(crate) fn live_owners(&self) -> usize { + self.owners.iter().filter(|owner| owner.live).count() + } + pub(crate) fn live_loans(&self) -> usize { + self.loans.iter().filter(|loan| loan.live).count() + } + pub(crate) fn active_frames(&self) -> usize { + self.active.len() + } +} + +#[cfg(test)] +#[path = "resource_custody/tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs b/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs new file mode 100644 index 00000000..67b940c9 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/carrier_holder_tests.rs @@ -0,0 +1,472 @@ +use super::*; +use crate::resource_custody::carrier_holders::{CarrierRootToken, RegisteredCarrierSlot}; + +// Deliberate constructor-owned test metadata; no public path/word issues a token. +fn carrier_slot( + program: &RegisteredResourceProgram, + index: usize, + wrong_second: bool, +) -> RegisteredCarrierSlot { + let field = |name: &[u8], node| NativeCarrierField { + name: name.to_vec(), + node, + }; + RegisteredCarrierSlot { + program: program.0.clone(), + graph: Arc::new(NativeCarrierLayout { + nodes: vec![ + NativeCarrierNode::Resource { kind: 0 }, + NativeCarrierNode::Ordinary { shape: 0 }, + NativeCarrierNode::Struct { + identity: b"app.OwnedPair".to_vec(), + arguments: vec![], + fields: vec![ + field(b"first", 0), + field(b"second", if wrong_second { 3 } else { 0 }), + field(b"marker", 1), + ], + }, + NativeCarrierNode::Resource { kind: 1 }, + ], + slots: (0..4) + .map(|_| NativeCarrierSlot { frame: 0, node: 2 }) + .collect(), + operations: vec![], + }), + index, + } +} + +fn begin_root( + core: &mut ResourceCustody, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, +) -> CarrierRootToken { + core.begin_carrier_root(frame, &carrier_slot(program, slot, false)) + .unwrap() +} + +#[test] +fn carrier_holders_interleave_scalar_carrier_scalar_and_release_original_slot() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _a = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let mut b = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier(&mut b, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + core.complete_carrier_root(&root).unwrap(); + let _c = acquire( + &mut core, + &mut registry, + &program, + &frame, + 3, + 3, + &events, + false, + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3, 2, 1]); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); + + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 4, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier(&mut first, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + // The occupied tree remains live while its old scalar slot is reused. + let _replacement = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 5, + &events, + false, + ); + assert_eq!(core.live_owners(), 2); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3, 2, 1, 5, 4]); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_caller_callee_move_is_exact_and_invalidates_old_generation() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let caller = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &caller, + 0, + 10, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &caller, + 1, + 11, + &events, + false, + ); + let source = begin_root(&mut core, &program, &caller, 0); + // Constructor lexical order differs from declared field order. + core.transfer_into_carrier( + &mut second, + &source, + &[NativeCarrierPath::Field(1)], + ®istry, + ) + .unwrap(); + core.transfer_into_carrier( + &mut first, + &source, + &[NativeCarrierPath::Field(0)], + ®istry, + ) + .unwrap(); + core.complete_carrier_root(&source).unwrap(); + let stale = OwnedResourceToken { + core: first.core, + owner: first.owner, + holder_generation: first.holder_generation, + }; + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let destination = begin_root(&mut core, &program, &callee, 1); + core.move_carrier_inventory( + &source, + &destination, + &mut [ + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ], + ®istry, + true, + ) + .unwrap(); + assert!(matches!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + )); + assert!(matches!( + core.validate_carrier_ticket(&source, &[NativeCarrierPath::Field(0)], &first), + Err(CustodyError::StaleHolderGeneration) + )); + assert_eq!(core.end_frame(&callee, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [10, 11]); + core.retire_carrier_metadata(&destination).unwrap(); + assert_eq!(core.end_frame(&caller, &mut registry).failure(), None); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_late_batch_wrong_kind_rolls_back_and_can_retry() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 20, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 21, + &events, + false, + ); + let source = begin_root(&mut core, &program, &frame, 0); + core.transfer_into_carrier( + &mut first, + &source, + &[NativeCarrierPath::Field(0)], + ®istry, + ) + .unwrap(); + core.transfer_into_carrier( + &mut second, + &source, + &[NativeCarrierPath::Field(1)], + ®istry, + ) + .unwrap(); + core.complete_carrier_root(&source).unwrap(); + let before = (first.holder_generation, second.holder_generation); + let wrong = core + .begin_carrier_root(&frame, &carrier_slot(&program, 1, true)) + .unwrap(); + let result = core.move_carrier_inventory( + &source, + &wrong, + &mut [ + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ], + ®istry, + true, + ); + assert_eq!(result, Err(CustodyError::WrongKind)); + assert_eq!(before, (first.holder_generation, second.holder_generation)); + core.validate_carrier_inventory( + &source, + &[ + (&first, vec![NativeCarrierPath::Field(0)]), + (&second, vec![NativeCarrierPath::Field(1)]), + ], + ) + .unwrap(); + assert!(events.lock().unwrap().is_empty()); + core.retire_carrier_metadata(&wrong).unwrap(); + let good = begin_root(&mut core, &program, &frame, 1); + core.move_carrier_inventory( + &source, + &good, + &mut [ + ( + &mut first, + vec![NativeCarrierPath::Field(0)], + vec![NativeCarrierPath::Field(0)], + ), + ( + &mut second, + vec![NativeCarrierPath::Field(1)], + vec![NativeCarrierPath::Field(1)], + ), + ], + ®istry, + true, + ) + .unwrap(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [21, 20]); + core.retire_carrier_metadata(&good).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_wrong_path_root_generation_foreign_and_borrow_refuse_without_effects() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 30, + &events, + false, + ); + let root = begin_root(&mut core, &program, &frame, 0); + assert_eq!( + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(2)], ®istry), + Err(CustodyError::WrongKind) + ); + let stale = CarrierRootToken { + core: root.core, + index: root.index, + generation: root.generation + 1, + }; + assert_eq!( + core.transfer_into_carrier( + &mut owner, + &stale, + &[NativeCarrierPath::Field(0)], + ®istry + ), + Err(CustodyError::StaleHolderGeneration) + ); + let foreign = CarrierRootToken { + core: root.core + 1, + index: root.index, + generation: root.generation, + }; + assert_eq!( + core.transfer_into_carrier( + &mut owner, + &foreign, + &[NativeCarrierPath::Field(0)], + ®istry + ), + Err(CustodyError::WrongProgram) + ); + let mut loan = core.begin_borrow(&owner, &frame, ®istry).unwrap(); + assert_eq!( + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(0)], ®istry), + Err(CustodyError::ActiveBorrow) + ); + core.end_borrow(&mut loan).unwrap(); + core.transfer_into_carrier(&mut owner, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + let other = begin_root(&mut core, &program, &frame, 1); + assert_eq!( + core.validate_carrier_ticket(&other, &[NativeCarrierPath::Field(0)], &owner), + Err(CustodyError::InvalidOwner) + ); + assert_eq!( + core.validate_carrier_inventory(&root, &[]), + Err(CustodyError::InvalidOwner) + ); + assert_eq!( + core.validate_carrier_inventory( + &root, + &[ + (&owner, vec![NativeCarrierPath::Field(0)]), + (&owner, vec![NativeCarrierPath::Field(0)]) + ] + ), + Err(CustodyError::OccupiedHolder) + ); + assert!(events.lock().unwrap().is_empty()); + core.retire_carrier_metadata(&other).unwrap(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + core.retire_carrier_metadata(&root).unwrap(); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} + +#[test] +fn carrier_holders_partial_operation_panic_retires_once_and_continues_unified_order() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(vec![])); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _parent = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 40, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 41, + &events, + false, + ); + let root = begin_root(&mut core, &program, &operation, 0); + core.transfer_into_carrier(&mut first, &root, &[NativeCarrierPath::Field(0)], ®istry) + .unwrap(); + let _later_scalar = acquire( + &mut core, + &mut registry, + &program, + &operation, + 1, + 42, + &events, + true, + ); + assert_eq!( + core.end_frame(&operation, &mut registry).failure(), + Some(ResourceCleanupFailure::FinalizerPanic) + ); + assert_eq!(*events.lock().unwrap(), [42, 41]); + core.retire_carrier_metadata(&root).unwrap(); + assert_eq!(core.end_frame(&parent, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [42, 41, 40]); + clean(&core, ®istry); + assert_eq!(core.live_carrier_roots(), 0); +} diff --git a/crates/jett_runtime/src/resource_custody/carrier_holders.rs b/crates/jett_runtime/src/resource_custody/carrier_holders.rs new file mode 100644 index 00000000..8e38ebe0 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/carrier_holders.rs @@ -0,0 +1,447 @@ +//! Private typed carrier holders. Physical tree bits never issue ownership. +use super::*; + +/// Issued only by the validated NativeLayoutInstallation child. +#[derive(Debug, Clone)] +pub(crate) struct RegisteredCarrierSlot { + pub(super) program: Arc, + pub(super) graph: Arc, + pub(super) index: usize, +} + +/// Neither Clone/Copy nor a finalizing Rust Drop. A move consumes this identity. +#[derive(Debug)] +pub(crate) struct CarrierRootToken { + pub(super) core: u64, + pub(super) index: usize, + pub(super) generation: u64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CarrierPhase { + Building, + Ready, + Retired, +} + +struct CarrierHolderPath { + path: Vec, + kind: usize, +} + +pub(super) struct CarrierHolderRoot { + frame: usize, + slot: RegisteredCarrierSlot, + generation: u64, + phase: CarrierPhase, + paths: Vec, +} + +impl ResourceCustody { + fn carrier_root_record( + &self, + token: &CarrierRootToken, + ) -> Result<&CarrierHolderRoot, CustodyError> { + if token.core != self.id { + return Err(CustodyError::WrongProgram); + } + self.carrier_roots + .get(token.index) + .filter(|r| r.generation == token.generation && r.phase != CarrierPhase::Retired) + .ok_or(CustodyError::StaleHolderGeneration) + } + + pub(crate) fn begin_carrier_root( + &mut self, + frame: &ResourceFrameToken, + slot: &RegisteredCarrierSlot, + ) -> Result { + let frame = self.frame_token(frame)?; + self.runtime_frame(frame)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) { + return Err(CustodyError::WrongProgram); + } + slot.graph + .slots + .get(slot.index) + .ok_or(CustodyError::UnknownSlot)?; + if self.carrier_roots.iter().any(|r| { + r.phase != CarrierPhase::Retired && r.frame == frame && r.slot.index == slot.index + }) { + return Err(CustodyError::OccupiedHolder); + } + self.carrier_roots + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let index = self.carrier_roots.len(); + self.carrier_roots.push(CarrierHolderRoot { + frame, + slot: slot.clone(), + generation: 1, + phase: CarrierPhase::Building, + paths: Vec::new(), + }); + Ok(CarrierRootToken { + core: self.id, + index, + generation: 1, + }) + } + + fn carrier_holder( + &mut self, + token: &CarrierRootToken, + path: &[NativeCarrierPath], + ) -> Result { + let record = self.carrier_root_record(token)?; + self.runtime_frame(record.frame)?; + // A dynamic selection must be normalized by the exact reached runtime row. + if path.iter().any(|p| { + matches!( + p, + NativeCarrierPath::List(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic) + ) + }) { + return Err(CustodyError::UnknownSlot); + } + let node = record.slot.graph.slots[record.slot.index].node; + let leaf = record + .slot + .graph + .path_node(node, path) + .map_err(|_| CustodyError::UnknownSlot)?; + let NativeCarrierNode::Resource { kind } = record + .slot + .graph + .node(leaf) + .map_err(|_| CustodyError::UnknownSlot)? + else { + return Err(CustodyError::WrongKind); + }; + let kind = *kind as usize; + self.program + .0 + .kinds + .get(kind) + .ok_or(CustodyError::WrongKind)?; + let frame = record.frame; + let path_index = if let Some(i) = record.paths.iter().position(|p| p.path == path) { + if record.paths[i].kind != kind { + return Err(CustodyError::WrongKind); + } + i + } else { + let mut owned_path = Vec::new(); + owned_path + .try_reserve_exact(path.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + owned_path.extend_from_slice(path); + let record = &mut self.carrier_roots[token.index]; + record + .paths + .try_reserve(1) + .map_err(|_| CustodyError::CapacityExhausted)?; + let i = record.paths.len(); + record.paths.push(CarrierHolderPath { + path: owned_path, + kind, + }); + i + }; + Ok(ResourceHolderToken { + core: self.id, + holder: Holder { + frame, + address: HolderAddress::Carrier { + root: token.index, + path: path_index, + }, + }, + }) + } + + pub(crate) fn transfer_into_carrier( + &mut self, + owner: &mut OwnedResourceToken, + root: &CarrierRootToken, + path: &[NativeCarrierPath], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(root)?.phase != CarrierPhase::Building { + return Err(CustodyError::InvalidOwner); + } + let destination = self.carrier_holder(root, path)?; + self.transfer(owner, &destination, registry) + } + + pub(crate) fn validate_carrier_ticket( + &self, + root: &CarrierRootToken, + path: &[NativeCarrierPath], + owner: &OwnedResourceToken, + ) -> Result<(), CustodyError> { + let record = self.carrier_root_record(root)?; + let owner = self.owner(owner)?; + let HolderAddress::Carrier { + root: index, + path: p, + } = owner.holder.address + else { + return Err(CustodyError::InvalidOwner); + }; + if index != root.index + || owner.holder.frame != record.frame + || record + .paths + .get(p) + .is_none_or(|r| r.path != path || r.kind != owner.kind) + { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + /// The complete physical tree and Core inventory must match in both directions. + pub(crate) fn validate_carrier_inventory( + &self, + root: &CarrierRootToken, + owners: &[(&OwnedResourceToken, Vec)], + ) -> Result<(), CustodyError> { + self.carrier_root_record(root)?; + for (i, (owner, path)) in owners.iter().enumerate() { + self.validate_carrier_ticket(root, path, owner)?; + if owners[..i] + .iter() + .any(|(previous, p)| previous.owner == owner.owner || p == path) + { + return Err(CustodyError::OccupiedHolder); + } + } + let live = self.owners.iter().filter(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == root.index)).count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + pub(crate) fn complete_carrier_root( + &mut self, + root: &CarrierRootToken, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(root)?.phase != CarrierPhase::Building { + return Err(CustodyError::InvalidOwner); + } + self.carrier_roots[root.index].phase = CarrierPhase::Ready; + Ok(()) + } + + pub(crate) fn validate_carrier_location( + &self, + root: &CarrierRootToken, + frame: &ResourceFrameToken, + slot: &RegisteredCarrierSlot, + building: bool, + ) -> Result<(), CustodyError> { + let frame = self.frame_token(frame)?; + let record = self.carrier_root_record(root)?; + if !Arc::ptr_eq(&slot.program, &self.program.0) + || record.slot.index != slot.index + || record.slot.graph.as_ref() != slot.graph.as_ref() + || record.frame != frame + || record.phase + != if building { + CarrierPhase::Building + } else { + CarrierPhase::Ready + } + { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + } + + /// A reached tree is transferred in its lexical acquisition order. All checks + /// and capacity reservation precede the first owning-token mutation. + pub(crate) fn move_carrier_inventory( + &mut self, + source: &CarrierRootToken, + destination: &CarrierRootToken, + owners: &mut [( + &mut OwnedResourceToken, + Vec, + Vec, + )], + registry: &ResourceRegistry, + complete_destination: bool, + ) -> Result<(), CustodyError> { + if self.carrier_root_record(source)?.phase != CarrierPhase::Ready { + return Err(CustodyError::InvalidOwner); + } + let target = self.carrier_root_record(destination)?; + if target.phase != CarrierPhase::Building || source.index == destination.index { + return Err(CustodyError::OccupiedHolder); + } + let target_frame = target.frame; + let mut checked = Vec::new(); + checked + .try_reserve_exact(owners.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for (token, from, to) in owners.iter() { + self.validate_carrier_ticket(source, from, token)?; + let holder = self.carrier_holder(destination, to)?; + let (holder, generation) = self.checked_transfer(token, &holder, registry)?; + if checked + .iter() + .any(|(previous, h, _)| *previous == token.owner || *h == holder) + { + return Err(CustodyError::OccupiedHolder); + } + checked.push((token.owner, holder, generation)); + } + let live = self.owners.iter().filter(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == source.index)).count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + self.frames[target_frame] + .acquisitions + .try_reserve(owners.len()) + .map_err(|_| CustodyError::CapacityExhausted)?; + for ((token, _, _), (owner, holder, generation)) in owners.iter_mut().zip(checked) { + self.owners[owner].holder = holder; + self.owners[owner].generation = generation; + self.frames[target_frame].acquisitions.push(Acquisition { + owner, + holder, + generation, + }); + token.holder_generation = generation; + } + self.carrier_roots[source.index].phase = CarrierPhase::Retired; + if complete_destination { + self.carrier_roots[destination.index].phase = CarrierPhase::Ready; + } + Ok(()) + } + + pub(crate) fn retire_carrier_metadata( + &mut self, + root: &CarrierRootToken, + ) -> Result<(), CustodyError> { + self.carrier_root_record(root)?; + if self.owners.iter().any(|o| o.live + && matches!(o.holder.address, HolderAddress::Carrier { root: r, .. } if r == root.index)) { + return Err(CustodyError::InvalidOwner); + } + self.carrier_roots[root.index].phase = CarrierPhase::Retired; + Ok(()) + } + + pub(crate) fn preflight_carrier_retirement( + &self, + root: &CarrierRootToken, + owners: &[(&OwnedResourceToken, Vec)], + registry: &ResourceRegistry, + ) -> Result<(), CustodyError> { + self.validate_carrier_inventory(root, owners)?; + for (token, _) in owners { + self.validate_registry_owner(self.owner(token)?, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + } + Ok(()) + } + + pub(crate) fn close_carrier_inventory( + &mut self, + root: &CarrierRootToken, + owners: &mut [(&mut OwnedResourceToken, Vec)], + registry: &mut ResourceRegistry, + ) -> ResourceCleanupOutcome { + // The caller preflights the exact inventory before removing its shell. + // Recheck all identity/count/borrow facts without allocating a receipt. + let mut outcome = ResourceCleanupOutcome::default(); + let checked = (|| { + self.carrier_root_record(root)?; + for (i, (token, path)) in owners.iter().enumerate() { + self.validate_carrier_ticket(root, path, token)?; + self.validate_registry_owner(self.owner(token)?, registry)?; + if self.borrowed(token.owner) { + return Err(CustodyError::ActiveBorrow); + } + if owners[..i] + .iter() + .any(|(p, old)| p.owner == token.owner || old == path) + { + return Err(CustodyError::OccupiedHolder); + } + } + let live = self + .owners + .iter() + .filter(|o| { + o.live + && matches!(o.holder.address, + HolderAddress::Carrier { root: r, .. } if r == root.index) + }) + .count(); + if live != owners.len() { + return Err(CustodyError::InvalidOwner); + } + Ok(()) + })(); + if let Err(error) = checked { + outcome.fail(ResourceCleanupFailure::Custody(error)); + return outcome; + } + for (token, _) in owners.iter_mut().rev() { + outcome.merge(self.close(token, registry)); + } + if let Err(error) = self.retire_carrier_metadata(root) { + outcome.fail(ResourceCleanupFailure::Custody(error)); + } + outcome + } + + pub(super) fn holder_kind(&self, holder: Holder) -> Result { + self.frame(holder.frame)?; + match holder.address { + HolderAddress::Slot(slot) => self + .program + .0 + .slots + .get(slot) + .copied() + .ok_or(CustodyError::UnknownSlot), + HolderAddress::Carrier { root, path } => { + let r = self + .carrier_roots + .get(root) + .filter(|r| r.phase != CarrierPhase::Retired && r.frame == holder.frame) + .ok_or(CustodyError::StaleHolderGeneration)?; + r.paths + .get(path) + .map(|p| p.kind) + .ok_or(CustodyError::UnknownSlot) + } + } + } + + pub(crate) fn live_carrier_roots(&self) -> usize { + self.carrier_roots + .iter() + .filter(|r| r.phase != CarrierPhase::Retired) + .count() + } + + pub(crate) fn carrier_retirement_completed(&self, root: &CarrierRootToken) -> bool { + root.core == self.id + && self.carrier_roots.get(root.index).is_some_and(|record| { + record.generation == root.generation && record.phase == CarrierPhase::Retired + }) + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration.rs b/crates/jett_runtime/src/resource_custody/registration.rs new file mode 100644 index 00000000..38c76ef5 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration.rs @@ -0,0 +1,281 @@ +//! Sole native core metadata issuer. Registration never installs a provider or owner. + +use super::{ + CustodyError, ProgramLayout, RegisteredCarrierSlot, RegisteredOwnerSlot, + RegisteredResourceKind, RegisteredResourceProgram, +}; +use crate::{ResourceRegistry, ResourceTypeId}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[path = "registration/carriers.rs"] +mod carriers; +#[cfg(test)] +#[path = "registration/carriers_tests.rs"] +mod carriers_tests; +pub(crate) use carriers::*; +#[cfg(test)] +pub(crate) use carriers_tests::runtime_fixture_bytes as carrier_runtime_fixture_bytes; +#[path = "registration/schema.rs"] +mod schema; +#[path = "registration/source_validation.rs"] +mod source_validation; +#[path = "registration/validation.rs"] +mod validation; +#[path = "registration/wire.rs"] +mod wire; +pub(crate) use schema::{ + NativeAccess, NativeFormal, NativeFrame, NativeFrameRole, NativeHook, NativeLoanSource, + NativeOperation, NativeOperationRecord, NativeParent, NativePayloadStep, NativePosition, + NativeRecipe, NativeShape, NativeSignature, NativeSite, NativeSlot, NativeSourceEffect, + NativeSourceFormal, NativeSourceInvocation, NativeSourceResult, NativeSourceSyntax, + NativeSourceValue, NativeSumLoanSource, +}; + +pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3; + +static NEXT_NATIVE_KIND: AtomicU64 = AtomicU64::new(1); + +pub(crate) const RESOURCE_DOMAIN_OK: u32 = 0; +pub(crate) const RESOURCE_DOMAIN_ERROR: u32 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceLayoutError { + Header, + Reserved, + Length, + Truncated, + Trailing, + WireLimit, + UnknownTag, + DenseOrdinal, + InvalidReference, + NonCanonical, + UnsupportedLayout, + RecipeMismatch, + PayloadPath, + ShapeMismatch, + FrameMismatch, + OperationMismatch, + CapacityExhausted, + AlreadyInstalled, + WrongContext, + RegistryBusy, + RegistryShutdown, + PhysicalStorage, +} +impl std::fmt::Display for ResourceLayoutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "native Resource layout refused: {self:?}") + } +} +impl std::error::Error for ResourceLayoutError {} + +/// Fixed data record only. Its value is not a registry key or core token. +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(crate) struct JettResourceCallResultV1 { + pub(crate) domain: u32, + pub(crate) reserved: u32, + pub(crate) value: u64, +} + +#[derive(Debug)] +pub(crate) struct RegisteredNativeLayout { + core: RegisteredResourceProgram, + wire: schema::WireLayout, +} +impl RegisteredNativeLayout { + pub(crate) fn carrier_slot(&self, ordinal: u32) -> Result { + self.wire + .carriers + .slots + .get(ordinal as usize) + .ok_or(CustodyError::UnknownSlot)?; + Ok(RegisteredCarrierSlot { + program: self.core.0.clone(), + graph: Arc::new(self.wire.carriers.clone()), + index: ordinal as usize, + }) + } + pub(crate) fn carriers(&self) -> &NativeCarrierLayout { + &self.wire.carriers + } + pub(crate) fn carrier_source_node( + &self, + source: NativeCarrierSource, + ) -> Result { + carriers::source_node(&self.wire, source) + } + pub(crate) fn carrier_equivalent_sum(&self, node: u32, shape: u32) -> bool { + carriers::equivalent_sum(&self.wire, node, shape) + } + pub(crate) fn wire_version(&self) -> u32 { + self.wire.version + } + pub(crate) fn program(&self) -> &RegisteredResourceProgram { + &self.core + } + pub(crate) fn kind(&self, ordinal: u32) -> Result { + self.core.kind(ordinal as usize) + } + pub(crate) fn slot(&self, ordinal: u32) -> Result { + self.core.slot(ordinal as usize) + } + pub(crate) fn kind_id(&self, ordinal: u32) -> Result { + self.core + .0 + .kinds + .get(ordinal as usize) + .copied() + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn slot_kind(&self, ordinal: u32) -> Result { + self.core + .0 + .slots + .get(ordinal as usize) + .map(|kind| *kind as u32) + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn kinds_len(&self) -> usize { + self.core.0.kinds.len() + } + pub(crate) fn hooks(&self) -> &[NativeHook] { + &self.wire.hooks + } + pub(crate) fn signatures(&self) -> &[NativeSignature] { + &self.wire.signatures + } + pub(crate) fn shapes(&self) -> &[NativeShape] { + &self.wire.shapes + } + pub(crate) fn frames(&self) -> &[NativeFrame] { + &self.wire.frames + } + pub(crate) fn slots(&self) -> &[NativeSlot] { + &self.wire.slots + } + pub(crate) fn operations(&self) -> &[NativeOperationRecord] { + &self.wire.operations + } + + /// Before a provider effect; core and future opaque-token table preflights + /// remain independently mandatory. No payload/grant/preparation token is returned. + pub(crate) fn preflight_physical_acquisition( + &self, + registry: &mut ResourceRegistry, + ) -> Result<(), ResourceLayoutError> { + if registry.context_id != self.core.0.context { + return Err(ResourceLayoutError::WrongContext); + } + if registry.shutting_down { + return Err(ResourceLayoutError::RegistryShutdown); + } + if registry.next_creation_sequence == u64::MAX { + return Err(ResourceLayoutError::CapacityExhausted); + } + if let Some(&index) = registry.free_slots.last() { + checked_physical_slot(index)?; + let slot = registry + .slots + .get(index) + .ok_or(ResourceLayoutError::PhysicalStorage)?; + if slot.entry.is_some() || slot.generation == 0 { + return Err(ResourceLayoutError::PhysicalStorage); + } + if slot.generation == u64::MAX { + return Err(ResourceLayoutError::CapacityExhausted); + } + } else { + checked_physical_slot(registry.slots.len())?; + registry + .slots + .try_reserve(1) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + } + Ok(()) + } +} + +fn checked_physical_slot(index: usize) -> Result<(), ResourceLayoutError> { + u32::try_from(index) + .map(|_| ()) + .map_err(|_| ResourceLayoutError::CapacityExhausted) +} + +fn issued_kinds( + counter: &AtomicU64, + count: usize, +) -> Result, ResourceLayoutError> { + let mut kinds = wire::storage(count)?; + if count == 0 { + return Ok(kinds); + } + let count = u64::try_from(count).map_err(|_| ResourceLayoutError::CapacityExhausted)?; + let first = counter + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + if next == 0 { + return None; + } + next.checked_add(count) + .filter(|end| *end <= u64::from(u32::MAX) + 1) + }) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for offset in 0..count { + kinds.push(ResourceTypeId::new((first + offset) as u32)); + } + Ok(kinds) +} + +/// One stationary runtime context owns this holder. Fields cannot be assembled +/// from independently supplied program/kind/slot metadata. +pub(crate) struct NativeLayoutInstallation { + context: u64, + installed: Option>, +} +impl NativeLayoutInstallation { + pub(crate) fn new(registry: &ResourceRegistry) -> Self { + Self { + context: registry.context_id, + installed: None, + } + } + pub(crate) fn installed(&self) -> Option<&Arc> { + self.installed.as_ref() + } + pub(crate) fn install( + &mut self, + registry: &ResourceRegistry, + bytes: &[u8], + ) -> Result, ResourceLayoutError> { + if registry.context_id != self.context { + return Err(ResourceLayoutError::WrongContext); + } + if self.installed.is_some() { + return Err(ResourceLayoutError::AlreadyInstalled); + } + if registry.shutting_down { + return Err(ResourceLayoutError::RegistryShutdown); + } + if registry.live_count != 0 { + return Err(ResourceLayoutError::RegistryBusy); + } + let wire = wire::decode(bytes)?; + let slots = validation::validate(&wire)?; + // All fallible table/projection allocation and checks precede identity issuance. + let kinds = issued_kinds(&NEXT_NATIVE_KIND, wire.kinds.len())?; + let core = RegisteredResourceProgram(Arc::new(ProgramLayout { + context: self.context, + kinds, + slots, + })); + let layout = Arc::new(RegisteredNativeLayout { core, wire }); + self.installed = Some(layout.clone()); + Ok(layout) + } +} + +#[cfg(test)] +#[path = "registration/tests.rs"] +mod tests; diff --git a/crates/jett_runtime/src/resource_custody/registration/carriers.rs b/crates/jett_runtime/src/resource_custody/registration/carriers.rs new file mode 100644 index 00000000..f5045697 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/carriers.rs @@ -0,0 +1,1156 @@ +//! V3 carrier consistency records. None of these records is Source authority. +use super::{ResourceLayoutError, schema::*}; +use std::collections::HashSet; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct NativeCarrierField { + pub(crate) name: Vec, + pub(crate) node: u32, +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct NativeCarrierMember { + pub(crate) name: Vec, + pub(crate) discriminant: i64, + pub(crate) fields: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierNode { + Ordinary { + shape: u32, + }, + Resource { + kind: u32, + }, + Optional { + child: u32, + }, + Result { + ok: u32, + fail: u32, + }, + List { + element: u32, + }, + Map { + key: u32, + value: u32, + }, + Struct { + identity: Vec, + arguments: Vec, + fields: Vec, + }, + Enum { + identity: Vec, + arguments: Vec, + variants: Vec, + }, + Machine { + identity: Vec, + states: Vec, + transitions: Vec<(u32, u32)>, + }, + MachineState { + machine: u32, + state: u32, + }, + Refinement { + identity: Vec, + base: u32, + predicate: Vec, + }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierSelector { + Static(u32), + Dynamic, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierPath { + Field(u32), + EnumPayload { variant: u32, field: u32 }, + MachinePayload { state: u32, field: u32 }, + List(NativeCarrierSelector), + MapKey(NativeCarrierSelector), + MapValue(NativeCarrierSelector), + Some, + Ok, + Fail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierLoanSource { + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierSource { + Slot(u32), + Loan(NativeCarrierLoanSource), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeCarrierChildSource { + Ordinary { shape: u32 }, + Move { slot: u32 }, + LeafSum { slot: u32 }, + LeafOwner { slot: u32 }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierChild { + pub(crate) index: u32, + pub(crate) node: u32, + pub(crate) source: NativeCarrierChildSource, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeCarrierObservation { + Length, + Tag, + State, + Ordinary { shape: u32, clone: bool }, +} +impl NativeCarrierObservation { + /// Borrowed observation may copy scalar bits or clone one exact String. + /// Other ordinary owners require their own typed cloning transport. + pub(crate) fn ordinary_clone_policy(shapes: &[NativeShape], shape: u32) -> Option { + match shapes.get(shape as usize)? { + NativeShape::String => Some(true), + NativeShape::Integer { .. } + | NativeShape::Float { .. } + | NativeShape::Bool + | NativeShape::Nothing => Some(false), + _ => None, + } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeCarrierDestination { + Slot(u32), + Ordinary { shape: u32 }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierSlot { + pub(crate) frame: u32, + pub(crate) node: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeCarrierOperation { + Construct { + frame: u32, + destination: u32, + selector: u32, + children: Vec, + }, + Transfer { + frame: u32, + source: u32, + destination: u32, + }, + Borrow { + frame: u32, + source: NativeCarrierSource, + path: Vec, + node: u32, + lease_frame: u32, + }, + EndBorrow { + frame: u32, + borrow: u32, + }, + Observe { + frame: u32, + source: NativeCarrierSource, + path: Vec, + observation: NativeCarrierObservation, + }, + Extract { + frame: u32, + source: u32, + path: Vec, + destination: NativeCarrierDestination, + }, + AdaptSum { + frame: u32, + source: NativeCarrierSource, + path: Vec, + sum_slot: u32, + lease_frame: u32, + borrowed: bool, + }, + Retire { + frame: u32, + source: u32, + }, + PublishReturn { + frame: u32, + source: u32, + }, + ResetIteration { + frame: u32, + source: u32, + carrier_binders: Vec, + leaf_sum_binders: Vec, + }, + QualifyMachine { + frame: u32, + source: u32, + destination: u32, + machine: u32, + state: u32, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeCarrierOperationRecord { + pub(crate) site: NativeSite, + pub(crate) operation: NativeCarrierOperation, +} +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct NativeCarrierLayout { + pub(crate) nodes: Vec, + pub(crate) slots: Vec, + pub(crate) operations: Vec, +} +impl NativeCarrierOperation { + pub(crate) fn frame(&self) -> u32 { + match self { + Self::Construct { frame, .. } + | Self::Transfer { frame, .. } + | Self::Borrow { frame, .. } + | Self::EndBorrow { frame, .. } + | Self::Observe { frame, .. } + | Self::Extract { frame, .. } + | Self::AdaptSum { frame, .. } + | Self::Retire { frame, .. } + | Self::PublishReturn { frame, .. } + | Self::ResetIteration { frame, .. } + | Self::QualifyMachine { frame, .. } => *frame, + } + } +} +impl NativeCarrierLayout { + pub(crate) fn node(&self, node: u32) -> Result<&NativeCarrierNode, ResourceLayoutError> { + self.nodes + .get(node as usize) + .ok_or(ResourceLayoutError::InvalidReference) + } + pub(crate) fn selected_children( + &self, + node: u32, + selector: u32, + ) -> Result, ResourceLayoutError> { + let fields = |fields: &[NativeCarrierField]| fields.iter().map(|f| f.node).collect(); + Ok(match self.node(node)? { + NativeCarrierNode::Ordinary { .. } if selector == 0 => vec![node], + NativeCarrierNode::Optional { .. } if selector == 0 => vec![], + NativeCarrierNode::Optional { child } if selector == 1 => vec![*child], + NativeCarrierNode::Result { fail, .. } if selector == 0 => vec![*fail], + NativeCarrierNode::Result { ok, .. } if selector == 1 => vec![*ok], + NativeCarrierNode::Struct { fields: fs, .. } if selector == 0 => fields(fs), + NativeCarrierNode::Enum { variants, .. } => fields( + &variants + .get(selector as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .fields, + ), + NativeCarrierNode::Machine { states, .. } => fields( + &states + .get(selector as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .fields, + ), + NativeCarrierNode::MachineState { machine, state } if selector == *state => { + self.selected_children(*machine, selector)? + } + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } if selector == 0 => { + vec![] + } + _ => return Err(ResourceLayoutError::UnsupportedLayout), + }) + } + pub(crate) fn path_node( + &self, + mut node: u32, + path: &[NativeCarrierPath], + ) -> Result { + for step in path { + node = match (self.node(node)?, step) { + (NativeCarrierNode::Struct { fields, .. }, NativeCarrierPath::Field(i)) => { + fields + .get(*i as usize) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::Enum { variants, .. }, + NativeCarrierPath::EnumPayload { variant, field }, + ) => { + variants + .get(*variant as usize) + .and_then(|v| v.fields.get(*field as usize)) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::Machine { states, .. }, + NativeCarrierPath::MachinePayload { state, field }, + ) => { + states + .get(*state as usize) + .and_then(|s| s.fields.get(*field as usize)) + .ok_or(ResourceLayoutError::PayloadPath)? + .node + } + ( + NativeCarrierNode::MachineState { + machine, + state: expected, + }, + NativeCarrierPath::MachinePayload { state, field }, + ) if state == expected => self.path_node( + *machine, + &[NativeCarrierPath::MachinePayload { + state: *state, + field: *field, + }], + )?, + (NativeCarrierNode::List { element }, NativeCarrierPath::List(_)) => *element, + (NativeCarrierNode::Map { key, .. }, NativeCarrierPath::MapKey(_)) => *key, + (NativeCarrierNode::Map { value, .. }, NativeCarrierPath::MapValue(_)) => *value, + (NativeCarrierNode::Optional { child }, NativeCarrierPath::Some) => *child, + (NativeCarrierNode::Result { ok, .. }, NativeCarrierPath::Ok) => *ok, + (NativeCarrierNode::Result { fail, .. }, NativeCarrierPath::Fail) => *fail, + _ => return Err(ResourceLayoutError::PayloadPath), + }; + } + Ok(node) + } +} + +pub(super) fn validate(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let graph = &layout.carriers; + if layout.version != 3 && graph != &NativeCarrierLayout::default() { + return Err(ResourceLayoutError::UnsupportedLayout); + } + let mut unique = HashSet::new(); + let mut identities = HashSet::new(); + unique + .try_reserve(graph.nodes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + identities + .try_reserve(graph.nodes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for (i, node) in graph.nodes.iter().enumerate() { + if !unique.insert(node) { + return Err(ResourceLayoutError::NonCanonical); + } + let child = |id: u32| { + if (id as usize) < i { + Ok(()) + } else { + Err(ResourceLayoutError::InvalidReference) + } + }; + let fields = |fs: &[NativeCarrierField]| { + let mut names = HashSet::new(); + names + .try_reserve(fs.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for f in fs { + if f.name.is_empty() + || std::str::from_utf8(&f.name).is_err() + || !names.insert(&f.name) + { + return Err(ResourceLayoutError::NonCanonical); + } + child(f.node)?; + } + Ok(()) + }; + let members = |ms: &[NativeCarrierMember]| { + let mut names = HashSet::new(); + names + .try_reserve(ms.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for m in ms { + if m.name.is_empty() + || std::str::from_utf8(&m.name).is_err() + || !names.insert(&m.name) + { + return Err(ResourceLayoutError::NonCanonical); + } + fields(&m.fields)?; + } + Ok(()) + }; + match node { + NativeCarrierNode::Ordinary { shape: shape_id } => { + let shape = layout + .shapes + .get(*shape_id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if super::source_validation::occupied(layout, *shape_id)? + || matches!(shape, NativeShape::HookDescriptor { .. }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierNode::Resource { kind } => { + layout + .kinds + .get(*kind as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + } + NativeCarrierNode::Optional { child: id } | NativeCarrierNode::List { element: id } => { + child(*id)? + } + NativeCarrierNode::Result { ok, fail } => { + child(*ok)?; + child(*fail)?; + } + NativeCarrierNode::Map { key, value } => { + child(*key)?; + child(*value)?; + if !map_key(layout, *key)? { + return Err(ResourceLayoutError::UnsupportedLayout); + } + } + NativeCarrierNode::Struct { + identity, + arguments, + fields: fs, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + for a in arguments { + child(*a)?; + } + fields(fs)?; + } + NativeCarrierNode::Enum { + identity, + arguments, + variants, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + for a in arguments { + child(*a)?; + } + members(variants)?; + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } => { + if identity.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + members(states)?; + let mut pairs = HashSet::new(); + for (a, b) in transitions { + if *a as usize >= states.len() + || *b as usize >= states.len() + || !pairs.insert((*a, *b)) + { + return Err(ResourceLayoutError::InvalidReference); + } + } + } + NativeCarrierNode::MachineState { machine, state } => { + child(*machine)?; + if !matches!(graph.node(*machine)?,NativeCarrierNode::Machine{states,..} if (*state as usize) { + child(*base)?; + if identity.is_empty() || predicate.is_empty() || !identities.insert(identity) { + return Err(ResourceLayoutError::NonCanonical); + } + } + } + } + for s in &graph.slots { + graph.node(s.node)?; + layout + .frames + .get(s.frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + } + for (i, row) in graph.operations.iter().enumerate() { + let frame = layout + .frames + .get(row.operation.frame() as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if frame.site.function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + let wrappers = layout + .operations + .iter() + .filter(|r| { + r.site == row.site + && matches!(r.operation,NativeOperation::Carrier{record} if record as usize==i) + }) + .count(); + if wrappers != 1 { + return Err(ResourceLayoutError::OperationMismatch); + } + validate_operation(layout, row)?; + } + Ok(()) +} + +pub(crate) fn source_node( + layout: &WireLayout, + source: NativeCarrierSource, +) -> Result { + match source { + NativeCarrierSource::Slot(slot) => layout + .carriers + .slots + .get(slot as usize) + .map(|s| s.node) + .ok_or(ResourceLayoutError::InvalidReference), + NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation }) => { + let NativeOperation::Carrier { record } = layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + match layout + .carriers + .operations + .get(record as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + { + NativeCarrierOperation::Borrow { node, .. } => Ok(node), + _ => Err(ResourceLayoutError::OperationMismatch), + } + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::IncomingViewFormal { + scope, + parameter, + }) => { + let f = layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let formal = layout + .signatures + .get(f.signature as usize) + .and_then(|s| s.parameters.get(parameter as usize)) + .ok_or(ResourceLayoutError::InvalidReference)?; + if f.role != NativeFrameRole::Scope || formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match layout.shapes.get(formal.shape as usize) { + Some(NativeShape::Carrier { node }) => Ok(*node), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} +fn validate_operation( + layout: &WireLayout, + row: &NativeCarrierOperationRecord, +) -> Result<(), ResourceLayoutError> { + let graph = &layout.carriers; + let slot = |id: u32| { + graph + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference) + }; + let path = |source, path: &[NativeCarrierPath]| { + if path + .iter() + .filter(|s| { + matches!( + s, + NativeCarrierPath::List(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapKey(NativeCarrierSelector::Dynamic) + | NativeCarrierPath::MapValue(NativeCarrierSelector::Dynamic) + ) + }) + .count() + > 1 + { + return Err(ResourceLayoutError::PayloadPath); + } + graph.path_node(source_node(layout, source)?, path) + }; + let source = |source: NativeCarrierSource| { + let function = match source { + NativeCarrierSource::Slot(id) => { + layout + .frames + .get(slot(id)?.frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { operation }) => { + layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierSource::Loan(NativeCarrierLoanSource::IncomingViewFormal { + scope, + .. + }) => { + layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + }; + if function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(()) + }; + match &row.operation { + NativeCarrierOperation::Construct { children, .. } => { + for child in children { + match child.source { + NativeCarrierChildSource::Move { slot } => { + source(NativeCarrierSource::Slot(slot))? + } + NativeCarrierChildSource::LeafSum { slot } + | NativeCarrierChildSource::LeafOwner { slot } => { + let s = layout + .slots + .get(slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.frames[s.frame as usize].site.function != row.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + _ => {} + } + } + } + NativeCarrierOperation::Transfer { + source: id, + destination, + .. + } + | NativeCarrierOperation::QualifyMachine { + source: id, + destination, + .. + } => { + source(NativeCarrierSource::Slot(*id))?; + source(NativeCarrierSource::Slot(*destination))?; + } + NativeCarrierOperation::Borrow { source: id, .. } + | NativeCarrierOperation::Observe { source: id, .. } + | NativeCarrierOperation::AdaptSum { source: id, .. } => source(*id)?, + NativeCarrierOperation::EndBorrow { borrow, .. } => { + if layout + .operations + .get(*borrow as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + != row.site.function + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + NativeCarrierOperation::Extract { + source: id, + destination, + .. + } => { + source(NativeCarrierSource::Slot(*id))?; + if let NativeCarrierDestination::Slot(id) = destination { + source(NativeCarrierSource::Slot(*id))?; + } + } + NativeCarrierOperation::Retire { source: id, .. } + | NativeCarrierOperation::PublishReturn { source: id, .. } + | NativeCarrierOperation::ResetIteration { source: id, .. } => { + source(NativeCarrierSource::Slot(*id))? + } + } + match &row.operation { + NativeCarrierOperation::Construct { + frame, + destination, + selector, + children, + } => { + let target = slot(*destination)?; + if target.frame != *frame { + return Err(ResourceLayoutError::FrameMismatch); + } + let mut indices = HashSet::new(); + let expected = graph.selected_children(target.node, *selector)?; + for c in children { + graph.node(c.node)?; + if !indices.insert(c.index) { + return Err(ResourceLayoutError::NonCanonical); + } + match c.source { + NativeCarrierChildSource::Ordinary { shape } => { + if graph.node(c.node)? != &(NativeCarrierNode::Ordinary { shape }) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::Move { slot: id } => { + if slot(id)?.node != c.node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::LeafOwner { slot: id } => { + let s = layout + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let NativeCarrierNode::Resource { kind } = graph.node(c.node)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if layout.shapes.get(s.shape as usize) + != Some(&NativeShape::Resource { kind: *kind }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierChildSource::LeafSum { slot: id } => { + let s = layout + .slots + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if !equivalent_sum(layout, c.node, s.shape) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + match graph.node(target.node)? { + NativeCarrierNode::List { element } => { + if children + .iter() + .enumerate() + .any(|(i, c)| c.index as usize != i || c.node != *element) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierNode::Map { key, value } => { + if children.len() % 2 != 0 + || children.iter().enumerate().any(|(i, c)| { + c.index as usize != i + || c.node != if i % 2 == 0 { *key } else { *value } + }) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + _ => { + if children.len() != expected.len() + || children + .iter() + .any(|c| expected.get(c.index as usize) != Some(&c.node)) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + NativeCarrierOperation::Transfer { + source, + destination, + .. + } => { + if source == destination || slot(*source)?.node != slot(*destination)?.node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Borrow { + source, + path: p, + node, + lease_frame, + frame, + } => { + if path(*source, p)? != *node || lease_frame != frame { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::EndBorrow { borrow, .. } => { + let NativeOperation::Carrier { record } = layout + .operations + .get(*borrow as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + if !matches!( + graph.operations.get(record as usize).map(|r| &r.operation), + Some(NativeCarrierOperation::Borrow { .. }) + ) { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeCarrierOperation::Observe { + source, + path: p, + observation, + .. + } => { + let n = graph.node(path(*source, p)?)?; + let ok = match observation { + NativeCarrierObservation::Length => matches!( + n, + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } + ), + NativeCarrierObservation::Tag => matches!( + n, + NativeCarrierNode::Optional { .. } + | NativeCarrierNode::Result { .. } + | NativeCarrierNode::Enum { .. } + ), + NativeCarrierObservation::State => matches!( + n, + NativeCarrierNode::Machine { .. } | NativeCarrierNode::MachineState { .. } + ), + NativeCarrierObservation::Ordinary { shape, clone } => { + if NativeCarrierObservation::ordinary_clone_policy(&layout.shapes, *shape) + != Some(*clone) + { + return Err(ResourceLayoutError::OperationMismatch); + } + n == &(NativeCarrierNode::Ordinary { shape: *shape }) + } + }; + if !ok { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Extract { + source, + path: p, + destination, + .. + } => { + let node = path(NativeCarrierSource::Slot(*source), p)?; + match destination { + NativeCarrierDestination::Slot(id) => { + if slot(*id)?.node != node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierDestination::Ordinary { shape } => { + if graph.node(node)? != &(NativeCarrierNode::Ordinary { shape: *shape }) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + NativeCarrierOperation::AdaptSum { + source, + path: p, + sum_slot, + lease_frame, + frame, + borrowed, + } => { + let node = path(*source, p)?; + let sum = layout + .slots + .get(*sum_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if lease_frame != frame + || sum.frame != *frame + || (!borrowed && matches!(source, NativeCarrierSource::Loan(_))) + || !equivalent_sum(layout, node, sum.shape) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeCarrierOperation::Retire { source, .. } => { + slot(*source)?; + } + NativeCarrierOperation::PublishReturn { source, frame } => { + let source = slot(*source)?; + let f = &layout.frames[*frame as usize]; + let r = &layout.frames[source.frame as usize]; + if f.role != NativeFrameRole::Scope + || r.role != NativeFrameRole::Return + || f.site.function != r.site.function + || f.signature != r.signature + || !f.parents.contains(&NativeParent::Frame(source.frame)) + || !matches!(layout.shapes.get(layout.signatures[f.signature as usize].result as usize),Some(NativeShape::Carrier{node}) if *node==source.node) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeCarrierOperation::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + frame, + } => { + let source_slot = slot(*source)?; + if !matches!( + graph.node(source_slot.node)?, + NativeCarrierNode::List { .. } | NativeCarrierNode::Map { .. } + ) { + return Err(ResourceLayoutError::ShapeMismatch); + } + let mut unique = HashSet::new(); + for id in carrier_binders { + if *id == *source || !unique.insert(*id) || slot(*id)?.frame != *frame { + return Err(ResourceLayoutError::OperationMismatch); + } + } + let mut unique = HashSet::new(); + for id in leaf_sum_binders { + let leaf = layout + .slots + .get(*id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if !unique.insert(*id) + || leaf.frame != *frame + || !matches!( + layout.shapes.get(leaf.shape as usize), + Some(NativeShape::Optional { .. } | NativeShape::Result { .. }) + ) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + } + NativeCarrierOperation::QualifyMachine { + source, + destination, + machine, + state, + .. + } => { + if source == destination + || graph.node(slot(*source)?.node)? + != &(NativeCarrierNode::MachineState { + machine: *machine, + state: *state, + }) + || slot(*destination)?.node != *machine + || !matches!(graph.node(*machine)?,NativeCarrierNode::Machine{states,..} if (*state as usize) Result { + for _ in 0..=layout.carriers.nodes.len() { + match layout.carriers.node(node)? { + NativeCarrierNode::Refinement { base, .. } => node = *base, + NativeCarrierNode::Ordinary { shape } => { + return Ok(matches!( + layout.shapes.get(*shape as usize), + Some(NativeShape::Integer { .. } | NativeShape::String | NativeShape::Bool) + )); + } + _ => return Ok(false), + } + } + Err(ResourceLayoutError::InvalidReference) +} +pub(crate) fn equivalent_sum(layout: &WireLayout, node: u32, shape: u32) -> bool { + match ( + layout.carriers.node(node), + layout.shapes.get(shape as usize), + ) { + ( + Ok(NativeCarrierNode::Optional { child }), + Some(NativeShape::Optional { child: shape }), + ) => { + matches!((layout.carriers.node(*child),layout.shapes.get(*shape as usize)),(Ok(NativeCarrierNode::Resource{kind:a}),Some(NativeShape::Resource{kind:b})) if a==b) + } + ( + Ok(NativeCarrierNode::Result { ok, fail }), + Some(NativeShape::Result { + ok: sok, + fail: sfail, + }), + ) => { + matches!((layout.carriers.node(*ok),layout.shapes.get(*sok as usize),layout.carriers.node(*fail)),(Ok(NativeCarrierNode::Resource{kind:a}),Some(NativeShape::Resource{kind:b}),Ok(NativeCarrierNode::Ordinary{shape})) if a==b&&shape==sfail) + } + _ => false, + } +} + +pub(super) fn validate_formal( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee_scope: u32, + formal: &NativeSourceFormal, + caller_slots: &mut HashSet, + callee_slots: &mut HashSet, +) -> Result<(), ResourceLayoutError> { + if layout.version != 3 || formal.actual_shape != formal.callee_shape { + return Err(ResourceLayoutError::ShapeMismatch); + } + let Some(NativeShape::Carrier { node }) = layout.shapes.get(formal.actual_shape as usize) + else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + match formal.value { + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => { + let a = layout + .carriers + .slots + .get(caller_argument_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let b = layout + .carriers + .slots + .get(callee_parameter_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if formal.syntax != NativeSourceSyntax::Bare + || formal.effect != NativeSourceEffect::TransferOwned + || formal.access != NativeAccess::Owned + || a.node != *node + || b.node != *node + || a.frame != caller_frame + || b.frame != callee_scope + || !caller_slots.insert(caller_argument_slot) + || !callee_slots.insert(callee_parameter_slot) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeSourceValue::CarrierView { source } => { + if formal.access != NativeAccess::View + || !matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ) + || source_node(layout, NativeCarrierSource::Loan(source))? != *node + { + return Err(ResourceLayoutError::OperationMismatch); + } + let function = match source { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + layout + .operations + .get(operation as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + NativeCarrierLoanSource::IncomingViewFormal { scope, .. } => { + layout + .frames + .get(scope as usize) + .ok_or(ResourceLayoutError::InvalidReference)? + .site + .function + } + }; + if function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + _ => return Err(ResourceLayoutError::OperationMismatch), + } + Ok(()) +} +pub(super) fn validate_result( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee: u32, + signature: &NativeSignature, + scope: &NativeFrame, + source: &NativeSourceInvocation, +) -> Result<(), ResourceLayoutError> { + let NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } = &source.result + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let Some(NativeShape::Carrier { node }) = layout.shapes.get(*shape as usize) else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + let destination = layout + .carriers + .slots + .get(*caller_destination_slot as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let return_frame = layout + .frames + .get(*callee_return_frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + let destination_frame = layout + .frames + .get(*caller_destination_frame as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.version != 3 + || *shape != signature.result + || source.callee_return != Some(*callee_return_frame) + || return_frame.role != NativeFrameRole::Return + || return_frame.site.function != callee + || return_frame.signature != scope.signature + || !return_frame + .parents + .contains(&NativeParent::Frame(caller_frame)) + || !scope + .parents + .contains(&NativeParent::Frame(*callee_return_frame)) + || destination.frame != *caller_destination_frame + || destination.node != *node + || destination_frame.site.function != record.site.function + || permitted_return_slots.is_empty() + || permitted_return_slots.windows(2).any(|p| p[0] >= p[1]) + { + return Err(ResourceLayoutError::OperationMismatch); + } + for id in permitted_return_slots { + let s = layout + .carriers + .slots + .get(*id as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if s.frame != *callee_return_frame || s.node != *node { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + Ok(()) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs b/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs new file mode 100644 index 00000000..2c8dfb6b --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/carriers_tests.rs @@ -0,0 +1,1995 @@ +//! Registration consistency tests only. Graph rows do not authorize live custody +//! or prove a refinement predicate; those remain compiler/runtime obligations. +use super::schema::WireLayout; +use super::wire::{decode, encode_records}; +use super::*; +use crate::ResourceRegistry; + +const INTEGER: u32 = 0; +const STRING: u32 = 1; +const NOTHING: u32 = 2; +const RESOURCE_SHAPE: u32 = 4; +const OPTIONAL_SHAPE: u32 = 5; +const RESULT_SHAPE: u32 = 6; +const WHOLE_SHAPE: u32 = 7; +const RESOURCE_NODE: u32 = 3; +const OPTIONAL_NODE: u32 = 4; +const RESULT_NODE: u32 = 5; +const LIST_NODE: u32 = 6; +const MAP_NODE: u32 = 7; +const RECORD_NODE: u32 = 8; +const ENUM_NODE: u32 = 9; +const MACHINE_NODE: u32 = 10; +const STATE_NODE: u32 = 11; +const REFINEMENT_NODE: u32 = 12; +const GENERIC_RESOURCE_NODE: u32 = 13; +const GENERIC_OPTIONAL_NODE: u32 = 14; +const WHOLE_NODE: u32 = 15; +const OTHER_RECORD_NODE: u32 = 16; + +fn site(index: u32) -> NativeSite { + NativeSite { + function: 0, + block: 0, + position: NativePosition::Statement(index), + } +} + +fn field(name: &str, node: u32) -> NativeCarrierField { + NativeCarrierField { + name: name.as_bytes().to_vec(), + node, + } +} + +fn member(name: &str, discriminant: i64, fields: Vec) -> NativeCarrierMember { + NativeCarrierMember { + name: name.as_bytes().to_vec(), + discriminant, + fields, + } +} + +fn fixture() -> WireLayout { + let record_fields = vec![field("marker", 0), field("channel", OPTIONAL_NODE)]; + WireLayout { + version: 3, + kinds: vec![0], + hooks: vec![], + signatures: vec![NativeSignature { + ordinal: 0, + parameters: vec![], + result: NOTHING, + }], + shapes: vec![ + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::String, + NativeShape::Nothing, + NativeShape::Bool, + NativeShape::Resource { kind: 0 }, + NativeShape::Optional { + child: RESOURCE_SHAPE, + }, + NativeShape::Result { + ok: RESOURCE_SHAPE, + fail: STRING, + }, + NativeShape::Carrier { node: WHOLE_NODE }, + ], + frames: vec![ + NativeFrame { + ordinal: 0, + role: NativeFrameRole::Scope, + site: site(0), + signature: 0, + parents: vec![NativeParent::Root], + }, + NativeFrame { + ordinal: 1, + role: NativeFrameRole::Operation, + site: site(1), + signature: 0, + parents: vec![NativeParent::Frame(0)], + }, + ], + slots: vec![ + NativeSlot { + ordinal: 0, + frame: 1, + shape: OPTIONAL_SHAPE, + path: vec![NativePayloadStep::Some], + }, + NativeSlot { + ordinal: 1, + frame: 1, + shape: RESULT_SHAPE, + path: vec![NativePayloadStep::Ok], + }, + ], + operations: vec![], + carriers: NativeCarrierLayout { + nodes: vec![ + NativeCarrierNode::Ordinary { shape: INTEGER }, + NativeCarrierNode::Ordinary { shape: STRING }, + NativeCarrierNode::Ordinary { shape: 3 }, + NativeCarrierNode::Resource { kind: 0 }, + NativeCarrierNode::Optional { + child: RESOURCE_NODE, + }, + NativeCarrierNode::Result { + ok: RESOURCE_NODE, + fail: 1, + }, + NativeCarrierNode::List { + element: OPTIONAL_NODE, + }, + NativeCarrierNode::Map { + key: 0, + value: RESULT_NODE, + }, + NativeCarrierNode::Struct { + identity: b"transport.Record".to_vec(), + arguments: vec![], + fields: record_fields.clone(), + }, + NativeCarrierNode::Enum { + identity: b"transport.ChannelState".to_vec(), + arguments: vec![RESOURCE_NODE], + variants: vec![ + member("Closed", -7, vec![field("marker", 0)]), + member("Open", 41, vec![field("channel", RESOURCE_NODE)]), + ], + }, + NativeCarrierNode::Machine { + identity: b"transport.ChannelMachine".to_vec(), + states: vec![ + member("Empty", 11, vec![field("marker", 0)]), + member("Live", 22, vec![field("channel", RESOURCE_NODE)]), + ], + transitions: vec![(0, 1), (1, 0)], + }, + NativeCarrierNode::MachineState { + machine: MACHINE_NODE, + state: 0, + }, + NativeCarrierNode::Refinement { + identity: b"transport.PositiveMarker".to_vec(), + base: 0, + predicate: b"value > 0".to_vec(), + }, + NativeCarrierNode::Struct { + identity: b"transport.Cell[transport.Channel]".to_vec(), + arguments: vec![RESOURCE_NODE], + fields: vec![field("channel", OPTIONAL_NODE)], + }, + NativeCarrierNode::Struct { + identity: b"transport.Cell[optional[transport.Channel]]".to_vec(), + arguments: vec![OPTIONAL_NODE], + fields: vec![field("channel", OPTIONAL_NODE)], + }, + NativeCarrierNode::Struct { + identity: b"transport.Whole".to_vec(), + arguments: vec![], + fields: vec![ + field("record", RECORD_NODE), + field("entries", LIST_NODE), + field("lookup", MAP_NODE), + field("state", ENUM_NODE), + field("machine", MACHINE_NODE), + field("empty", STATE_NODE), + ], + }, + NativeCarrierNode::Struct { + identity: b"other.Record".to_vec(), + arguments: vec![], + fields: record_fields, + }, + ], + slots: vec![ + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RESULT_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OTHER_RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: GENERIC_RESOURCE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: GENERIC_OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: LIST_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: MAP_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: WHOLE_NODE, + }, + ], + operations: vec![], + }, + } +} + +#[test] +fn carrier_registration_leaf_owner_has_distinct_wire_tag_and_exact_resource_slot() { + let mut layout = fixture(); + let node = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(NativeCarrierNode::Struct { + identity: b"app.OwnedEnvelope".to_vec(), + arguments: vec![], + fields: vec![field("token", RESOURCE_NODE), field("marker", 0)], + }); + let destination = u32::try_from(layout.carriers.slots.len()).unwrap(); + layout + .carriers + .slots + .push(NativeCarrierSlot { frame: 1, node }); + layout.slots.push(NativeSlot { + ordinal: 2, + frame: 1, + shape: RESOURCE_SHAPE, + path: vec![], + }); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination, + selector: 0, + children: vec![ + NativeCarrierChild { + index: 0, + node: RESOURCE_NODE, + source: NativeCarrierChildSource::LeafOwner { slot: 2 }, + }, + ordinary_child(1, 0, INTEGER), + ], + }, + ); + assert_eq!( + parsed(&layout).unwrap(), + layout, + "real encoder/decoder must preserve distinct LeafOwner tag4" + ); + for mutation in 0..5 { + let mut forged = layout.clone(); + match mutation { + 0 => forged.slots[2].shape = OPTIONAL_SHAPE, + 1 => forged.slots[2].path = vec![NativePayloadStep::Some], + 2 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[0].source = NativeCarrierChildSource::LeafSum { slot: 2 }; + } + 3 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[0].node = OPTIONAL_NODE; + } + 4 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut forged.carriers.operations.last_mut().unwrap().operation + else { + unreachable!() + }; + children[1].index = 0; + } + _ => unreachable!(), + } + assert!(parsed(&forged).is_err(), "LeafOwner mismatch {mutation}"); + } +} + +fn legacy(version: u32) -> WireLayout { + let mut layout = fixture(); + layout.version = version; + layout.carriers = NativeCarrierLayout::default(); + assert_eq!( + layout.shapes.pop(), + Some(NativeShape::Carrier { node: WHOLE_NODE }) + ); + layout +} + +fn parsed(layout: &WireLayout) -> Result { + let decoded = decode(&encode_records(layout))?; + validation::validate(&decoded)?; + Ok(decoded) +} + +fn operation(layout: &mut WireLayout, value: NativeCarrierOperation) -> u32 { + let ordinal = u32::try_from(layout.operations.len()).unwrap(); + let record = u32::try_from(layout.carriers.operations.len()).unwrap(); + let site = site(100 + ordinal); + layout.operations.push(NativeOperationRecord { + ordinal, + site, + operation: NativeOperation::Carrier { record }, + }); + layout + .carriers + .operations + .push(NativeCarrierOperationRecord { + site, + operation: value, + }); + ordinal +} + +fn ordinary_child(index: u32, node: u32, shape: u32) -> NativeCarrierChild { + NativeCarrierChild { + index, + node, + source: NativeCarrierChildSource::Ordinary { shape }, + } +} + +fn carrier_child(index: u32, node: u32, slot: u32) -> NativeCarrierChild { + NativeCarrierChild { + index, + node, + source: NativeCarrierChildSource::Move { slot }, + } +} + +fn suffix_offset(bytes: &[u8]) -> usize { + bytes + .windows(8) + .position(|part| part == b"JTCAR003") + .unwrap() +} + +fn write_word(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn set_length(bytes: &mut [u8]) { + let length = u64::try_from(bytes.len()).unwrap(); + bytes[16..24].copy_from_slice(&length.to_le_bytes()); +} + +#[test] +fn carrier_registration_roundtrips_complete_unused_nominal_and_latent_metadata() { + let layout = fixture(); + assert_eq!(parsed(&layout).unwrap(), layout); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let mut bytes = encode_records(&layout); + let installed = installation.install(®istry, &bytes).unwrap(); + bytes.fill(0); + assert_eq!(installed.carriers(), &layout.carriers); + assert_eq!(installed.wire_version(), 3); + assert_eq!(installed.kinds_len(), 1); + assert_eq!(installed.slots().len(), 2); + assert_eq!(registry.live_count(), 0); + assert!(matches!( + installed.carriers().node(REFINEMENT_NODE).unwrap(), + NativeCarrierNode::Refinement { identity, base: 0, predicate } + if identity.as_slice() == b"transport.PositiveMarker" && predicate.as_slice() == b"value > 0" + )); + assert!(source_validation::occupied(&layout, WHOLE_SHAPE).unwrap()); +} + +#[test] +fn carrier_registration_keeps_legacy_wire_versions_and_refuses_the_v3_suffix() { + for version in [1, 2] { + let layout = legacy(version); + let bytes = encode_records(&layout); + assert_eq!(parsed(&layout).unwrap(), layout); + assert!(!bytes.windows(8).any(|part| part == b"JTCAR003")); + let mut trailing = bytes; + trailing.extend_from_slice(b"JTCAR003"); + trailing.extend_from_slice(&[0; 16]); + set_length(&mut trailing); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + } +} + +#[test] +fn carrier_registration_new_shape_and_operation_tags_require_wire_v3() { + for version in [1, 2] { + let mut shape = legacy(version); + shape.shapes.push(NativeShape::Carrier { node: 0 }); + assert_eq!( + decode(&encode_records(&shape)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut op = legacy(version); + op.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::Carrier { record: 0 }, + }); + assert_eq!( + decode(&encode_records(&op)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + } +} + +#[test] +fn carrier_registration_new_source_roles_and_adapter_loan_require_wire_v3() { + for value in [ + NativeSourceValue::CarrierOwned { + caller_argument_slot: 0, + callee_parameter_slot: 0, + }, + NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: 0 }, + }, + ] { + let mut layout = legacy(2); + layout.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 0, + callee_scope: 0, + source: Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: INTEGER, + callee_shape: INTEGER, + syntax: NativeSourceSyntax::Bare, + effect: NativeSourceEffect::TransferOwned, + access: NativeAccess::Owned, + value, + }], + result: NativeSourceResult::Ordinary { shape: NOTHING }, + }), + }, + }); + assert_eq!( + decode(&encode_records(&layout)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + } + let mut result = legacy(2); + result.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 0, + callee_scope: 0, + source: Some(NativeSourceInvocation { + callee_return: Some(0), + evaluation_order: vec![], + formals: vec![], + result: NativeSourceResult::Carrier { + shape: INTEGER, + caller_destination_frame: 1, + caller_destination_slot: 0, + callee_return_frame: 0, + permitted_return_slots: vec![0], + }, + }), + }, + }); + assert_eq!( + decode(&encode_records(&result)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut adapter = legacy(2); + adapter.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::CarrierProjected { operation: 0 }, + }, + }); + assert_eq!( + decode(&encode_records(&adapter)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); +} + +#[test] +fn carrier_registration_suffix_rejects_missing_magic_reserved_counts_tags_and_dense_rows() { + let bytes = encode_records(&fixture()); + let suffix = suffix_offset(&bytes); + for end in suffix..bytes.len() { + let mut prefix = bytes[..end].to_vec(); + set_length(&mut prefix); + assert!(decode(&prefix).is_err(), "carrier suffix prefix {end}"); + } + for (offset, value, expected) in [ + (suffix + 20, 1, ResourceLayoutError::Reserved), + (suffix + 8, 4097, ResourceLayoutError::WireLimit), + (suffix + 24, 1, ResourceLayoutError::DenseOrdinal), + (suffix + 28, 99, ResourceLayoutError::UnknownTag), + ] { + let mut altered = bytes.clone(); + write_word(&mut altered, offset, value); + assert_eq!(decode(&altered).unwrap_err(), expected); + } + let mut magic = bytes.clone(); + magic[suffix] = b'X'; + assert_eq!(decode(&magic).unwrap_err(), ResourceLayoutError::Header); + let mut trailing = bytes; + trailing.push(0); + set_length(&mut trailing); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + let mut oversized = fixture(); + let NativeCarrierNode::Struct { identity, .. } = &mut oversized.carriers.nodes[8] else { + unreachable!() + }; + *identity = vec![b'x'; 4097]; + assert_eq!( + decode(&encode_records(&oversized)).unwrap_err(), + ResourceLayoutError::WireLimit + ); +} + +#[test] +fn carrier_registration_validates_every_unused_graph_reference_and_ordinary_leaf() { + for case in 0..7 { + let mut layout = fixture(); + let expected = match case { + 0 => { + layout.carriers.nodes[4] = NativeCarrierNode::Optional { child: 4 }; + ResourceLayoutError::InvalidReference + } + 1 => { + layout.carriers.nodes[3] = NativeCarrierNode::Resource { kind: 1 }; + ResourceLayoutError::InvalidReference + } + 2 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { + shape: RESOURCE_SHAPE, + }; + ResourceLayoutError::ShapeMismatch + } + 3 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { + shape: OPTIONAL_SHAPE, + }; + ResourceLayoutError::ShapeMismatch + } + 4 => { + layout.carriers.nodes[0] = NativeCarrierNode::Ordinary { shape: WHOLE_SHAPE }; + ResourceLayoutError::ShapeMismatch + } + 5 => { + layout + .carriers + .nodes + .push(NativeCarrierNode::Resource { kind: 0 }); + ResourceLayoutError::NonCanonical + } + 6 => { + layout.carriers.slots[0].node = 99; + ResourceLayoutError::InvalidReference + } + _ => unreachable!(), + }; + assert_eq!( + parsed(&layout).unwrap_err(), + expected, + "unused graph case {case}" + ); + } +} + +#[test] +fn carrier_registration_rejects_duplicate_names_invalid_machine_state_and_unbound_refinement() { + for case in 0..7 { + let mut layout = fixture(); + match case { + 0 => { + let NativeCarrierNode::Struct { fields, .. } = &mut layout.carriers.nodes[8] else { + unreachable!() + }; + fields[1].name = fields[0].name.clone(); + } + 1 => { + let NativeCarrierNode::Enum { variants, .. } = &mut layout.carriers.nodes[9] else { + unreachable!() + }; + variants[1].name = variants[0].name.clone(); + } + 2 => { + let NativeCarrierNode::Machine { transitions, .. } = &mut layout.carriers.nodes[10] + else { + unreachable!() + }; + transitions.push((0, 1)); + } + 3 => { + layout.carriers.nodes[11] = NativeCarrierNode::MachineState { + machine: MACHINE_NODE, + state: 2, + } + } + 4 => { + layout.carriers.nodes[11] = NativeCarrierNode::MachineState { + machine: ENUM_NODE, + state: 0, + } + } + 5 => { + let NativeCarrierNode::Refinement { predicate, .. } = + &mut layout.carriers.nodes[12] + else { + unreachable!() + }; + predicate.clear(); + } + 6 => { + let NativeCarrierNode::Refinement { identity, .. } = &mut layout.carriers.nodes[12] + else { + unreachable!() + }; + *identity = b"transport.Record".to_vec(); + } + _ => unreachable!(), + } + assert!(parsed(&layout).is_err(), "nominal metadata case {case}"); + } +} + +#[test] +fn carrier_registration_latent_live_alternatives_do_not_reject_absent_constructor_metadata() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 0, + selector: 0, + children: vec![], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + assert_eq!( + layout.carriers.selected_children(OPTIONAL_NODE, 0).unwrap(), + Vec::::new() + ); + assert_eq!( + layout.carriers.selected_children(OPTIONAL_NODE, 1).unwrap(), + vec![RESOURCE_NODE] + ); + assert_eq!( + layout.carriers.selected_children(ENUM_NODE, 0).unwrap(), + vec![0] + ); + assert_eq!( + layout.carriers.selected_children(ENUM_NODE, 1).unwrap(), + vec![RESOURCE_NODE] + ); + assert_eq!( + layout.carriers.selected_children(STATE_NODE, 0).unwrap(), + vec![0] + ); + assert!(layout.carriers.selected_children(STATE_NODE, 1).is_err()); +} + +#[test] +fn carrier_registration_constructor_lexical_order_preserves_explicit_storage_indices() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 2, + selector: 0, + children: vec![ + carrier_child(1, OPTIONAL_NODE, 0), + ordinary_child(0, 0, INTEGER), + ], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..4 { + let mut invalid = layout.clone(); + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => children[1].index = 1, + 1 => children[1].index = 2, + 2 => children[1] = ordinary_child(0, 1, STRING), + 3 => children[1].source = NativeCarrierChildSource::Ordinary { shape: STRING }, + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "constructor row case {case}"); + } +} + +#[test] +fn carrier_registration_collection_constructor_rejects_reordered_elements_and_map_halves() { + let mut list = fixture(); + operation( + &mut list, + NativeCarrierOperation::Construct { + frame: 1, + destination: 6, + selector: 0, + children: vec![carrier_child(0, OPTIONAL_NODE, 0)], + }, + ); + assert!(parsed(&list).is_ok()); + let NativeCarrierOperation::Construct { children, .. } = + &mut list.carriers.operations[0].operation + else { + unreachable!() + }; + children[0].index = 1; + assert!(parsed(&list).is_err()); + let mut map = fixture(); + operation( + &mut map, + NativeCarrierOperation::Construct { + frame: 1, + destination: 7, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, RESULT_NODE, 1), + ], + }, + ); + assert!(parsed(&map).is_ok()); + for case in 0..3 { + let mut invalid = map.clone(); + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => { + children.pop(); + } + 1 => children.swap(0, 1), + 2 => children[0] = ordinary_child(0, 1, STRING), + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "map constructor case {case}"); + } +} + +#[test] +fn carrier_registration_nominal_and_generic_slots_cannot_transfer_by_matching_fields() { + for (source, destination) in [(2, 3), (4, 5), (0, 0)] { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source, + destination, + }, + ); + assert!( + parsed(&layout).is_err(), + "carrier slot substitution {source} -> {destination}" + ); + } +} + +#[test] +fn carrier_registration_carrier_rows_require_one_exact_site_wrapper() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Retire { + frame: 1, + source: 0, + }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..4 { + let mut invalid = layout.clone(); + match case { + 0 => invalid.operations.clear(), + 1 => invalid.operations[0].site.position = NativePosition::Statement(200), + 2 => { + let mut duplicate = invalid.operations[0].clone(); + duplicate.ordinal = 1; + invalid.operations.push(duplicate); + } + 3 => invalid.carriers.operations[0].site.function = 1, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier site wrapper case {case}" + ); + } +} + +#[test] +fn carrier_registration_borrow_and_projection_nodes_are_exact_and_bounded() { + let mut layout = fixture(); + let borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(1)], + node: OPTIONAL_NODE, + lease_frame: 1, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: borrow, + }), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { frame: 1, borrow }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..4 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::Borrow { node, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *node = RESOURCE_NODE; + } + 1 => { + let NativeCarrierOperation::Borrow { path, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *path = vec![NativeCarrierPath::Field(99)]; + } + 2 => { + let NativeCarrierOperation::Borrow { lease_frame, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *lease_frame = 0; + } + 3 => { + let NativeCarrierOperation::EndBorrow { borrow, .. } = + &mut invalid.carriers.operations[2].operation + else { + unreachable!() + }; + *borrow = 1; + } + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "carrier borrow row case {case}"); + } + let mut dynamic = fixture(); + operation( + &mut dynamic, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(6), + path: vec![NativeCarrierPath::List(NativeCarrierSelector::Dynamic)], + observation: NativeCarrierObservation::Tag, + }, + ); + assert!(parsed(&dynamic).is_ok()); + let NativeCarrierOperation::Observe { path, .. } = + &mut dynamic.carriers.operations[0].operation + else { + unreachable!() + }; + path.push(NativeCarrierPath::List(NativeCarrierSelector::Dynamic)); + assert_eq!( + parsed(&dynamic).unwrap_err(), + ResourceLayoutError::PayloadPath + ); +} + +#[test] +fn carrier_registration_ordinary_observations_do_not_reclassify_resource_nodes() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { + shape: INTEGER, + clone: false, + }, + }, + ); + assert!(parsed(&layout).is_ok()); + let valid = layout.clone(); + let NativeCarrierOperation::Observe { path, .. } = &mut layout.carriers.operations[0].operation + else { + unreachable!() + }; + *path = vec![NativeCarrierPath::Field(1), NativeCarrierPath::Some]; + for (shape, clone, expected) in [ + ( + RESOURCE_SHAPE, + false, + ResourceLayoutError::OperationMismatch, + ), + (RESOURCE_SHAPE, true, ResourceLayoutError::OperationMismatch), + (INTEGER, false, ResourceLayoutError::ShapeMismatch), + (STRING, true, ResourceLayoutError::ShapeMismatch), + ] { + let NativeCarrierOperation::Observe { observation, .. } = + &mut layout.carriers.operations[0].operation + else { + unreachable!() + }; + *observation = NativeCarrierObservation::Ordinary { shape, clone }; + let bytes = encode_records(&layout); + assert_eq!( + decode(&bytes).unwrap(), + layout, + "forgery has a valid wire encoding" + ); + assert_eq!(parsed(&layout).unwrap_err(), expected); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + assert_eq!( + installation.install(®istry, &bytes).unwrap_err(), + expected + ); + assert!(installation.installed().is_none()); + assert_eq!(registry.live_count(), 0); + let installed = installation + .install(®istry, &encode_records(&valid)) + .unwrap(); + assert_eq!( + installed.carriers().node(RESOURCE_NODE).unwrap(), + &NativeCarrierNode::Resource { kind: 0 } + ); + assert_eq!(registry.live_count(), 0); + } +} + +#[test] +fn carrier_registration_leaf_sum_adapter_requires_matching_kind_and_shape() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(2), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 2, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + NativeCarrierChild { + index: 1, + node: OPTIONAL_NODE, + source: NativeCarrierChildSource::LeafSum { slot: 0 }, + }, + ], + }, + ); + assert!(parsed(&layout).is_ok()); + for case in 0..3 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::AdaptSum { sum_slot, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *sum_slot = 1; + } + 1 => { + let NativeCarrierOperation::Construct { children, .. } = + &mut invalid.carriers.operations[1].operation + else { + unreachable!() + }; + children[1].source = NativeCarrierChildSource::LeafSum { slot: 1 }; + } + 2 => { + invalid.kinds.push(1); + invalid.carriers.nodes[3] = NativeCarrierNode::Resource { kind: 1 }; + } + _ => unreachable!(), + } + assert!(parsed(&invalid).is_err(), "leaf sum adapter case {case}"); + } +} + +#[test] +fn carrier_registration_iteration_reset_names_only_distinct_binder_slots() { + let mut layout = fixture(); + operation( + &mut layout, + NativeCarrierOperation::ResetIteration { + frame: 1, + source: 6, + carrier_binders: vec![0], + leaf_sum_binders: vec![0], + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..6 { + let mut invalid = layout.clone(); + match case { + 0 => { + let NativeCarrierOperation::ResetIteration { source, .. } = + &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *source = 2; + } + 1 => { + let NativeCarrierOperation::ResetIteration { + carrier_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + carrier_binders.push(0); + } + 2 => { + let NativeCarrierOperation::ResetIteration { + carrier_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + *carrier_binders = vec![6]; + } + 3 => { + let NativeCarrierOperation::ResetIteration { + leaf_sum_binders, .. + } = &mut invalid.carriers.operations[0].operation + else { + unreachable!() + }; + leaf_sum_binders.push(0); + } + 4 => invalid.carriers.slots[0].frame = 0, + 5 => invalid.slots[0].shape = NOTHING, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "iteration reset binder case {case}" + ); + } +} + +#[test] +fn carrier_registration_publish_requires_matching_scope_return_and_result_node() { + let mut layout = fixture(); + layout.signatures[0].result = WHOLE_SHAPE; + layout.frames[0].parents.push(NativeParent::Frame(2)); + layout.frames.push(NativeFrame { + ordinal: 2, + role: NativeFrameRole::Return, + site: site(2), + signature: 0, + parents: vec![NativeParent::Root], + }); + layout.carriers.slots.push(NativeCarrierSlot { + frame: 2, + node: WHOLE_NODE, + }); + operation( + &mut layout, + NativeCarrierOperation::PublishReturn { + frame: 0, + source: 9, + }, + ); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..5 { + let mut invalid = layout.clone(); + match case { + 0 => invalid.frames[0].parents = vec![NativeParent::Root], + 1 => invalid.frames[2].role = NativeFrameRole::Operation, + 2 => invalid.carriers.slots[9].node = RECORD_NODE, + 3 => invalid.frames[2].site.function = 1, + 4 => invalid.signatures[0].result = NOTHING, + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier publication linkage case {case}" + ); + } +} + +fn source_owned() -> WireLayout { + let mut layout = fixture(); + layout.signatures.push(NativeSignature { + ordinal: 1, + parameters: vec![NativeFormal { + shape: WHOLE_SHAPE, + access: NativeAccess::Owned, + }], + result: WHOLE_SHAPE, + }); + let callee_site = |index| NativeSite { + function: 1, + block: 0, + position: NativePosition::Statement(index), + }; + layout.frames.extend([ + NativeFrame { + ordinal: 2, + role: NativeFrameRole::Return, + site: callee_site(0), + signature: 1, + parents: vec![NativeParent::Frame(1)], + }, + NativeFrame { + ordinal: 3, + role: NativeFrameRole::Scope, + site: callee_site(1), + signature: 1, + parents: vec![NativeParent::Frame(2)], + }, + ]); + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 0, + node: WHOLE_NODE, + }, + NativeCarrierSlot { + frame: 2, + node: WHOLE_NODE, + }, + NativeCarrierSlot { + frame: 3, + node: WHOLE_NODE, + }, + ]); + layout.operations.push(NativeOperationRecord { + ordinal: 0, + site: site(100), + operation: NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 1, + callee_scope: 3, + source: Some(NativeSourceInvocation { + callee_return: Some(2), + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: WHOLE_SHAPE, + callee_shape: WHOLE_SHAPE, + syntax: NativeSourceSyntax::Bare, + effect: NativeSourceEffect::TransferOwned, + access: NativeAccess::Owned, + value: NativeSourceValue::CarrierOwned { + caller_argument_slot: 8, + callee_parameter_slot: 11, + }, + }], + result: NativeSourceResult::Carrier { + shape: WHOLE_SHAPE, + caller_destination_frame: 0, + caller_destination_slot: 9, + callee_return_frame: 2, + permitted_return_slots: vec![10], + }, + }), + }, + }); + layout +} + +#[test] +fn carrier_registration_source_boundary_keeps_exact_owned_slots_and_return_permission() { + let layout = source_owned(); + assert_eq!(parsed(&layout).unwrap(), layout); + for case in 0..10 { + let mut invalid = layout.clone(); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut invalid.operations[0].operation + else { + unreachable!() + }; + match case { + 0 => source.formals[0].value = NativeSourceValue::Ordinary, + 1 => source.formals[0].actual_shape = INTEGER, + 2 => { + source.formals[0].value = NativeSourceValue::CarrierOwned { + caller_argument_slot: 7, + callee_parameter_slot: 11, + } + } + 3 => { + source.formals[0].value = NativeSourceValue::CarrierOwned { + caller_argument_slot: 8, + callee_parameter_slot: 8, + } + } + 4 => source.formals[0].syntax = NativeSourceSyntax::WrittenView, + 5 => source.formals[0].effect = NativeSourceEffect::Copy, + 6 => source.result = NativeSourceResult::Ordinary { shape: WHOLE_SHAPE }, + 7 => { + let NativeSourceResult::Carrier { + permitted_return_slots, + .. + } = &mut source.result + else { + unreachable!() + }; + *permitted_return_slots = vec![9]; + } + 8 => { + let NativeSourceResult::Carrier { + permitted_return_slots, + .. + } = &mut source.result + else { + unreachable!() + }; + permitted_return_slots.clear(); + } + 9 => source.callee_return = Some(0), + _ => unreachable!(), + } + assert!( + parsed(&invalid).is_err(), + "carrier Source boundary case {case}" + ); + } +} + +#[test] +fn carrier_registration_source_view_uses_the_exact_existing_carrier_borrow() { + let mut layout = source_owned(); + let mut call = layout.operations.pop().unwrap(); + layout.signatures[1].parameters[0].access = NativeAccess::View; + let borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(8), + path: vec![], + node: WHOLE_NODE, + lease_frame: 1, + }, + ); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut call.operation + else { + unreachable!() + }; + source.formals[0].syntax = NativeSourceSyntax::WrittenView; + source.formals[0].effect = NativeSourceEffect::RetainBorrow; + source.formals[0].access = NativeAccess::View; + source.formals[0].value = NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: borrow }, + }; + call.ordinal = 1; + call.site = site(101); + layout.operations.push(call); + assert_eq!(parsed(&layout).unwrap(), layout); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut layout.operations[1].operation + else { + unreachable!() + }; + source.formals[0].value = NativeSourceValue::CarrierView { + source: NativeCarrierLoanSource::ExistingBorrow { operation: 1 }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); +} + +/// Exact slot indices for tests of the registered runtime fixture below. +pub(crate) const RUNTIME_CARRIER_FIXTURE_SLOTS: [(&str, u32); 9] = [ + ("optional", 0), + ("result", 1), + ("record", 2), + ("list", 6), + ("map", 7), + ("transferred_record", 9), + ("latent_resource", 10), + ("second_optional", 11), + ("iteration_binder", 12), +]; + +fn native_operation(layout: &mut WireLayout, function: u32, value: NativeOperation) -> u32 { + let ordinal = u32::try_from(layout.operations.len()).unwrap(); + layout.operations.push(NativeOperationRecord { + ordinal, + site: NativeSite { + function, + block: 0, + position: NativePosition::Statement(100 + ordinal), + }, + operation: value, + }); + ordinal +} + +/// Test bytes use the real registrar and decoder. This helper never constructs a +/// production registry identity, owner token, loan token, or Source proof. +pub(crate) fn runtime_fixture_bytes() -> Vec { + let mut layout = fixture(); + let slots = RUNTIME_CARRIER_FIXTURE_SLOTS; + let optional = slots[0].1; + let result = slots[1].1; + let record = slots[2].1; + let list = slots[3].1; + let map = slots[4].1; + let transferred_record = slots[5].1; + let latent_resource = slots[6].1; + let second_optional = slots[7].1; + let iteration_binder = slots[8].1; + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 1, + node: RECORD_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: RESOURCE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: OPTIONAL_NODE, + }, + ]); + for destination in [optional, second_optional, iteration_binder] { + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination, + selector: 0, + children: vec![], + }, + ); + } + // A selected live arm is consistent metadata, but runtime construction of a + // reached Resource node must refuse before acquiring an aggregate owner. + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: optional, + selector: 1, + children: vec![carrier_child(0, RESOURCE_NODE, latent_resource)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: record, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, OPTIONAL_NODE, optional), + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: record, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + NativeCarrierChild { + index: 1, + node: OPTIONAL_NODE, + source: NativeCarrierChildSource::LeafSum { slot: 0 }, + }, + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source: record, + destination: transferred_record, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Transfer { + frame: 1, + source: transferred_record, + destination: record, + }, + ); + let full_borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![], + node: RECORD_NODE, + lease_frame: 1, + }, + ); + let child_borrow = operation( + &mut layout, + NativeCarrierOperation::Borrow { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: full_borrow, + }), + path: vec![NativeCarrierPath::Field(1)], + node: OPTIONAL_NODE, + lease_frame: 1, + }, + ); + let adapter = operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + let nested_adapter = operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: child_borrow, + }), + path: vec![], + sum_slot: 0, + lease_frame: 1, + borrowed: true, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { + frame: 1, + borrow: child_borrow, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::EndBorrow { + frame: 1, + borrow: full_borrow, + }, + ); + for operation in [adapter, nested_adapter] { + native_operation( + &mut layout, + 0, + NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::CarrierProjected { operation }, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::EndSumBorrow { + frame: 1, + borrow: operation, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(transferred_record), + path: vec![NativeCarrierPath::Field(1)], + sum_slot: 0, + lease_frame: 1, + borrowed: false, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Extract { + frame: 1, + source: transferred_record, + path: vec![NativeCarrierPath::Field(0)], + destination: NativeCarrierDestination::Ordinary { shape: INTEGER }, + }, + ); + for source in [record, transferred_record] { + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(source), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { + shape: INTEGER, + clone: false, + }, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Loan(NativeCarrierLoanSource::ExistingBorrow { + operation: child_borrow, + }), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(list), + path: vec![], + observation: NativeCarrierObservation::Length, + }, + ); + for children in [ + vec![], + vec![carrier_child(0, OPTIONAL_NODE, optional)], + vec![ + carrier_child(0, OPTIONAL_NODE, optional), + carrier_child(1, OPTIONAL_NODE, second_optional), + ], + ] { + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: list, + selector: 0, + children, + }, + ); + } + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: result, + selector: 0, + children: vec![ordinary_child(0, 1, STRING)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: map, + selector: 0, + children: vec![ + ordinary_child(0, 0, INTEGER), + carrier_child(1, RESULT_NODE, result), + ], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Extract { + frame: 1, + source: list, + path: vec![NativeCarrierPath::List(NativeCarrierSelector::Dynamic)], + destination: NativeCarrierDestination::Slot(iteration_binder), + }, + ); + operation( + &mut layout, + NativeCarrierOperation::AdaptSum { + frame: 1, + source: NativeCarrierSource::Slot(iteration_binder), + path: vec![], + sum_slot: 0, + lease_frame: 1, + borrowed: false, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::ResetIteration { + frame: 1, + source: list, + carrier_binders: vec![iteration_binder], + leaf_sum_binders: vec![0], + }, + ); + for source in [ + optional, + result, + record, + transferred_record, + list, + map, + second_optional, + iteration_binder, + ] { + operation( + &mut layout, + NativeCarrierOperation::Retire { frame: 1, source }, + ); + } + native_operation( + &mut layout, + 0, + NativeOperation::CreateAbsentSum { + frame: 1, + destination_slot: 0, + }, + ); + let sum_borrow = native_operation( + &mut layout, + 0, + NativeOperation::BorrowSum { + frame: 1, + source_sum_slot: 0, + lease_frame: 1, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::ObserveSumView { + frame: 1, + source: NativeSumLoanSource::ExistingBorrow { + operation: sum_borrow, + }, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::EndSumBorrow { + frame: 1, + borrow: sum_borrow, + }, + ); + native_operation( + &mut layout, + 0, + NativeOperation::SumDrop { + frame: 1, + source: 0, + }, + ); + native_operation(&mut layout, 0, NativeOperation::Complete { frame: 1 }); + native_operation(&mut layout, 0, NativeOperation::Complete { frame: 0 }); + + layout.signatures.push(NativeSignature { + ordinal: 1, + parameters: vec![NativeFormal { + shape: OPTIONAL_SHAPE, + access: NativeAccess::View, + }], + result: NOTHING, + }); + layout.frames.push(NativeFrame { + ordinal: 2, + role: NativeFrameRole::Scope, + site: NativeSite { + function: 1, + block: 0, + position: NativePosition::Statement(0), + }, + signature: 1, + parents: vec![NativeParent::Frame(1)], + }); + native_operation( + &mut layout, + 0, + NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 1, + callee_scope: 2, + source: Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: OPTIONAL_SHAPE, + callee_shape: OPTIONAL_SHAPE, + syntax: NativeSourceSyntax::WrittenView, + effect: NativeSourceEffect::RetainBorrow, + access: NativeAccess::View, + value: NativeSourceValue::ResidentSumView { + source: NativeSumLoanSource::CarrierProjected { operation: adapter }, + }, + }], + result: NativeSourceResult::Ordinary { shape: NOTHING }, + }), + }, + ); + native_operation( + &mut layout, + 1, + NativeOperation::ObserveSumView { + frame: 2, + source: NativeSumLoanSource::IncomingViewFormal { + scope: 2, + parameter: 0, + }, + }, + ); + native_operation(&mut layout, 1, NativeOperation::Complete { frame: 2 }); + layout.carriers.slots.extend([ + NativeCarrierSlot { + frame: 1, + node: STATE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: MACHINE_NODE, + }, + NativeCarrierSlot { + frame: 1, + node: ENUM_NODE, + }, + ]); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 13, + selector: 0, + children: vec![ordinary_child(0, 0, INTEGER)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::QualifyMachine { + frame: 1, + source: 13, + destination: 14, + machine: MACHINE_NODE, + state: 0, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(14), + path: vec![], + observation: NativeCarrierObservation::State, + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Construct { + frame: 1, + destination: 15, + selector: 0, + children: vec![ordinary_child(0, 0, INTEGER)], + }, + ); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(15), + path: vec![], + observation: NativeCarrierObservation::Tag, + }, + ); + encode_records(&layout) +} + +#[test] +fn carrier_registration_runtime_fixture_is_a_closed_v3_layout() { + let decoded = decode(&runtime_fixture_bytes()).unwrap(); + assert_eq!(decoded.version, 3); + assert_eq!(decoded.signatures[0].parameters, vec![]); + assert_eq!(decoded.signatures[0].result, NOTHING); + assert_eq!(validation::validate(&decoded).unwrap(), vec![0, 0]); + let registry = ResourceRegistry::new(); + NativeLayoutInstallation::new(®istry) + .install(®istry, &runtime_fixture_bytes()) + .unwrap(); + assert_eq!(registry.live_count(), 0); +} + +fn ordinary_observation_fixture(shape: NativeShape, clone: bool) -> WireLayout { + let mut layout = fixture(); + let shape = if let Some(index) = layout.shapes.iter().position(|row| row == &shape) { + u32::try_from(index).unwrap() + } else { + let index = u32::try_from(layout.shapes.len()).unwrap(); + layout.shapes.push(shape); + index + }; + let ordinary = NativeCarrierNode::Ordinary { shape }; + let node = if let Some(index) = layout + .carriers + .nodes + .iter() + .position(|row| row == &ordinary) + { + u32::try_from(index).unwrap() + } else { + let index = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(ordinary); + index + }; + let record = u32::try_from(layout.carriers.nodes.len()).unwrap(); + layout.carriers.nodes.push(NativeCarrierNode::Struct { + identity: b"transport.ObservedOrdinaryLeaf".to_vec(), + arguments: vec![], + fields: vec![field("value", node), field("channel", OPTIONAL_NODE)], + }); + let slot = u32::try_from(layout.carriers.slots.len()).unwrap(); + layout.carriers.slots.push(NativeCarrierSlot { + frame: 1, + node: record, + }); + operation( + &mut layout, + NativeCarrierOperation::Observe { + frame: 1, + source: NativeCarrierSource::Slot(slot), + path: vec![NativeCarrierPath::Field(0)], + observation: NativeCarrierObservation::Ordinary { shape, clone }, + }, + ); + layout +} + +#[test] +fn carrier_registration_scalar_observation_refuses_a_forged_clone_flag() { + for shape in [ + NativeShape::Integer { + bits: 8, + signed: true, + }, + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::Integer { + bits: 64, + signed: false, + }, + NativeShape::Float { bits: 32 }, + NativeShape::Float { bits: 64 }, + NativeShape::Bool, + NativeShape::Nothing, + ] { + let valid = ordinary_observation_fixture(shape.clone(), false); + assert_eq!(parsed(&valid).unwrap(), valid, "scalar shape {shape:?}"); + let forged = ordinary_observation_fixture(shape.clone(), true); + assert_eq!( + decode(&encode_records(&forged)).unwrap(), + forged, + "clone flag is a valid wire flag for {shape:?}" + ); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "scalar observation must copy bits for {shape:?}" + ); + } +} + +#[test] +fn carrier_registration_string_observation_refuses_a_forged_borrowed_owner_flag() { + let valid = ordinary_observation_fixture(NativeShape::String, true); + assert_eq!(parsed(&valid).unwrap(), valid); + let forged = ordinary_observation_fixture(NativeShape::String, false); + assert_eq!(decode(&encode_records(&forged)).unwrap(), forged); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "string observation must create its independent ordinary owner" + ); +} + +#[test] +fn carrier_registration_owning_ordinary_sum_observation_refuses_both_clone_flags() { + for shape in [ + NativeShape::Optional { child: INTEGER }, + NativeShape::Result { + ok: INTEGER, + fail: STRING, + }, + ] { + let mut metadata = ordinary_observation_fixture(shape.clone(), false); + metadata.operations.clear(); + metadata.carriers.operations.clear(); + assert_eq!( + parsed(&metadata).unwrap(), + metadata, + "ordinary sum metadata itself is consistent for {shape:?}" + ); + for clone in [false, true] { + let forged = ordinary_observation_fixture(shape.clone(), clone); + assert_eq!(decode(&encode_records(&forged)).unwrap(), forged); + assert_eq!( + parsed(&forged).unwrap_err(), + ResourceLayoutError::OperationMismatch, + "owning sum observation remains pending for {shape:?}, clone={clone}" + ); + } + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration/schema.rs b/crates/jett_runtime/src/resource_custody/registration/schema.rs new file mode 100644 index 00000000..2eb54377 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/schema.rs @@ -0,0 +1,404 @@ +//! Plain immutable wire records. None is a Source or custody authority. +use super::carriers::{NativeCarrierLayout, NativeCarrierLoanSource}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeRecipe { + NetworkFactory, + NetworkBorrow, + Finalize, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeAccess { + Owned, + View, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativeFrameRole { + Scope, + Operation, + Return, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativePayloadStep { + Some, + Ok, + Fail, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum NativePosition { + Statement(u32), + Terminator, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct NativeSite { + pub(super) function: u32, + pub(super) block: u32, + pub(super) position: NativePosition, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum NativeParent { + Root, + Frame(u32), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum NativeShape { + Integer { bits: u32, signed: bool }, + Float { bits: u32 }, + Bool, + String, + Nothing, + Network, + Resource { kind: u32 }, + Optional { child: u32 }, + Result { ok: u32, fail: u32 }, + HookDescriptor { hook: u32 }, + Carrier { node: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct NativeFormal { + pub(super) shape: u32, + pub(super) access: NativeAccess, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSignature { + pub(super) ordinal: u32, + pub(super) parameters: Vec, + pub(super) result: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeHook { + pub(super) ordinal: u32, + pub(super) kind: u32, + pub(super) recipe: NativeRecipe, + pub(super) signature: u32, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeFrame { + pub(super) ordinal: u32, + pub(super) role: NativeFrameRole, + pub(super) site: NativeSite, + pub(super) signature: u32, + pub(super) parents: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSlot { + pub(super) ordinal: u32, + pub(super) frame: u32, + pub(super) shape: u32, + pub(super) path: Vec, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeLoanSource { + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, + ProjectedSumPayload { operation: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSumLoanSource { + CarrierProjected { operation: u32 }, + ExistingBorrow { operation: u32 }, + IncomingViewFormal { scope: u32, parameter: u32 }, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceSyntax { + Bare, + WrittenView, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceEffect { + Copy, + TransferOwned, + RelinquishOwned, + RetainBorrow, + ObserveData, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NativeSourceValue { + CarrierOwned { + caller_argument_slot: u32, + callee_parameter_slot: u32, + }, + CarrierView { + source: NativeCarrierLoanSource, + }, + Ordinary, + Owned { + caller_argument_slot: u32, + callee_parameter_slot: u32, + }, + ResidentView { + source: NativeLoanSource, + }, + ResidentSumView { + source: NativeSumLoanSource, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSourceFormal { + pub(crate) parameter: u32, + pub(crate) source_index: u32, + pub(crate) actual_shape: u32, + pub(crate) callee_shape: u32, + pub(crate) syntax: NativeSourceSyntax, + pub(crate) effect: NativeSourceEffect, + pub(crate) access: NativeAccess, + pub(crate) value: NativeSourceValue, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeSourceResult { + Carrier { + shape: u32, + caller_destination_frame: u32, + caller_destination_slot: u32, + callee_return_frame: u32, + permitted_return_slots: Vec, + }, + Ordinary { + shape: u32, + }, + Owned { + shape: u32, + caller_destination_frame: u32, + caller_destination_slot: u32, + callee_return_frame: u32, + permitted_return_slots: Vec, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeSourceInvocation { + pub(crate) callee_return: Option, + pub(crate) evaluation_order: Vec, + pub(crate) formals: Vec, + pub(crate) result: NativeSourceResult, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum NativeOperation { + Carrier { + record: u32, + }, + Acquire { + frame: u32, + hook: u32, + destination: u32, + }, + Transfer { + frame: u32, + source: u32, + destination: u32, + }, + Borrow { + frame: u32, + source: u32, + lease_frame: u32, + }, + BoundedBorrowUse { + frame: u32, + source: NativeLoanSource, + callee_frame: u32, + }, + EndBorrow { + frame: u32, + borrow: u32, + }, + InvokeBorrow { + frame: u32, + hook: u32, + source: NativeLoanSource, + }, + Close { + frame: u32, + hook: u32, + source: u32, + }, + Drop { + frame: u32, + source: u32, + }, + SumAdopt { + frame: u32, + source: u32, + destination: u32, + }, + SumTake { + frame: u32, + source: u32, + destination: u32, + }, + SumDrop { + frame: u32, + source: u32, + }, + Replace { + frame: u32, + old: u32, + replacement: u32, + }, + Complete { + frame: u32, + }, + Descriptor { + hook: u32, + }, + InvokeDescriptor { + hook: u32, + signature: u32, + target: u32, + }, + InvokeSourceFunction { + frame: u32, + callee: u32, + signature: u32, + callee_scope: u32, + /// None is strict historical v1 metadata; it never enables Source activation. + source: Option, + }, + TakeFailureCompanion { + frame: u32, + source_sum_slot: u32, + failure_shape: u32, + }, + PublishReturn { + frame: u32, + source_return_slot: u32, + }, + CreateAbsentSum { + frame: u32, + destination_slot: u32, + }, + CreateFailureSum { + frame: u32, + destination_slot: u32, + failure_shape: u32, + }, + BorrowSum { + frame: u32, + source_sum_slot: u32, + lease_frame: u32, + }, + ObserveSumView { + frame: u32, + source: NativeSumLoanSource, + }, + ProjectSumView { + frame: u32, + source: NativeSumLoanSource, + path: NativePayloadStep, + lease_frame: u32, + }, + ReadFailureCompanion { + frame: u32, + source: NativeSumLoanSource, + failure_shape: u32, + }, + EndSumBorrow { + frame: u32, + borrow: u32, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NativeOperationRecord { + pub(super) ordinal: u32, + pub(super) site: NativeSite, + pub(super) operation: NativeOperation, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct WireLayout { + pub(super) carriers: NativeCarrierLayout, + pub(super) version: u32, + pub(super) kinds: Vec, + pub(super) hooks: Vec, + pub(super) signatures: Vec, + pub(super) shapes: Vec, + pub(super) frames: Vec, + pub(super) slots: Vec, + pub(super) operations: Vec, +} + +impl NativeSite { + pub(crate) fn function(&self) -> u32 { + self.function + } + pub(crate) fn block(&self) -> u32 { + self.block + } + pub(crate) fn position(&self) -> NativePosition { + self.position + } +} +impl NativeFormal { + pub(crate) fn shape(&self) -> u32 { + self.shape + } + pub(crate) fn access(&self) -> NativeAccess { + self.access + } +} +impl NativeSignature { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn parameters(&self) -> &[NativeFormal] { + &self.parameters + } + pub(crate) fn result(&self) -> u32 { + self.result + } +} +impl NativeHook { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn kind(&self) -> u32 { + self.kind + } + pub(crate) fn recipe(&self) -> NativeRecipe { + self.recipe + } + pub(crate) fn signature(&self) -> u32 { + self.signature + } +} +impl NativeFrame { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn role(&self) -> NativeFrameRole { + self.role + } + pub(crate) fn site(&self) -> NativeSite { + self.site + } + pub(crate) fn signature(&self) -> u32 { + self.signature + } + pub(crate) fn parents(&self) -> &[NativeParent] { + &self.parents + } +} +impl NativeSlot { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn frame(&self) -> u32 { + self.frame + } + pub(crate) fn shape(&self) -> u32 { + self.shape + } + pub(crate) fn path(&self) -> &[NativePayloadStep] { + &self.path + } +} +impl NativeOperationRecord { + pub(crate) fn ordinal(&self) -> u32 { + self.ordinal + } + pub(crate) fn site(&self) -> NativeSite { + self.site + } + pub(crate) fn operation(&self) -> &NativeOperation { + &self.operation + } +} diff --git a/crates/jett_runtime/src/resource_custody/registration/source_validation.rs b/crates/jett_runtime/src/resource_custody/registration/source_validation.rs new file mode 100644 index 00000000..84fd878e --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/source_validation.rs @@ -0,0 +1,518 @@ +//! Complete Source wire consistency. The compiler retains Source/CFG authority. +use super::{ResourceLayoutError, schema::*, validation::slot_kind}; +use std::collections::HashSet; + +fn get(values: &[T], id: u32) -> Result<&T, ResourceLayoutError> { + values + .get(id as usize) + .ok_or(ResourceLayoutError::InvalidReference) +} +pub(super) fn occupied(layout: &WireLayout, shape: u32) -> Result { + // Shape validation already established child-before-parent and Resource-free + // Result failure arms. Walk iteratively so bounded foreign nesting cannot + // turn table preflight into recursive stack growth. + let mut current = shape; + for _ in 0..=layout.shapes.len() { + match *get(&layout.shapes, current)? { + NativeShape::Resource { .. } | NativeShape::Carrier { .. } => return Ok(true), + NativeShape::Optional { child } => current = child, + NativeShape::Result { ok, .. } => current = ok, + _ => return Ok(false), + } + } + Err(ResourceLayoutError::InvalidReference) +} +fn frame<'a>( + layout: &'a WireLayout, + id: u32, + function: u32, + role: NativeFrameRole, +) -> Result<&'a NativeFrame, ResourceLayoutError> { + let result = get(&layout.frames, id)?; + if result.site.function != function || result.role != role { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(result) +} +fn source_kind( + layout: &WireLayout, + source: NativeLoanSource, + function: u32, +) -> Result { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + if get(&layout.operations, operation)?.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + projected_kind(layout, operation) + } + NativeLoanSource::ExistingBorrow { operation } => { + let record = get(&layout.operations, operation)?; + if record.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + let NativeOperation::Borrow { source, .. } = record.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + slot_kind(layout, get(&layout.slots, source)?) + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = frame(layout, scope, function, NativeFrameRole::Scope)?; + let signature = get(&layout.signatures, scope.signature)?; + let formal = get(&signature.parameters, parameter)?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match *get(&layout.shapes, formal.shape)? { + NativeShape::Resource { kind } => Ok(kind), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} + +pub(super) fn sum_source_shape( + layout: &WireLayout, + source: NativeSumLoanSource, + function: u32, +) -> Result { + let shape = match source { + NativeSumLoanSource::CarrierProjected { operation } => { + let row = get(&layout.operations, operation)?; + if layout.version != 3 || row.site.function != function { + return Err(ResourceLayoutError::OperationMismatch); + } + let NativeOperation::Carrier { record } = row.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let super::carriers::NativeCarrierOperation::AdaptSum { + sum_slot, + borrowed: true, + .. + } = get(&layout.carriers.operations, record)?.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + get(&layout.slots, sum_slot)?.shape + } + NativeSumLoanSource::ExistingBorrow { operation } => { + let row = get(&layout.operations, operation)?; + if row.site.function != function { + return Err(ResourceLayoutError::FrameMismatch); + } + let NativeOperation::BorrowSum { + source_sum_slot, .. + } = row.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + get(&layout.slots, source_sum_slot)?.shape + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + let scope = frame(layout, scope, function, NativeFrameRole::Scope)?; + let formal = get( + &get(&layout.signatures, scope.signature)?.parameters, + parameter, + )?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + formal.shape + } + }; + conditional_kind(layout, shape)?; + Ok(shape) +} +pub(super) fn conditional_kind( + layout: &WireLayout, + shape: u32, +) -> Result<(u32, NativePayloadStep), ResourceLayoutError> { + let (child, path) = match *get(&layout.shapes, shape)? { + NativeShape::Optional { child } => (child, NativePayloadStep::Some), + NativeShape::Result { ok, .. } => (ok, NativePayloadStep::Ok), + _ => return Err(ResourceLayoutError::ShapeMismatch), + }; + let NativeShape::Resource { kind } = *get(&layout.shapes, child)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + Ok((kind, path)) +} +pub(super) fn projected_kind( + layout: &WireLayout, + operation: u32, +) -> Result { + let row = get(&layout.operations, operation)?; + let NativeOperation::ProjectSumView { source, path, .. } = row.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let shape = sum_source_shape(layout, source, row.site.function)?; + let (kind, expected) = conditional_kind(layout, shape)?; + if path != expected { + return Err(ResourceLayoutError::PayloadPath); + } + Ok(kind) +} + +pub(super) fn invocation( + layout: &WireLayout, + record: &NativeOperationRecord, + caller_frame: u32, + callee: u32, + signature: u32, + callee_scope: u32, + source: &NativeSourceInvocation, +) -> Result<(), ResourceLayoutError> { + if !matches!(layout.version, 2 | 3) { + return Err(ResourceLayoutError::OperationMismatch); + } + frame( + layout, + caller_frame, + record.site.function, + NativeFrameRole::Operation, + )?; + let scope = frame(layout, callee_scope, callee, NativeFrameRole::Scope)?; + let signature_record = get(&layout.signatures, signature)?; + if scope.signature != signature + || source.formals.len() != signature_record.parameters.len() + || source.evaluation_order.len() != source.formals.len() + { + return Err(ResourceLayoutError::OperationMismatch); + } + let mut source_indices = super::wire::storage::(source.formals.len())?; + source_indices.resize(source.formals.len(), false); + let mut caller_slots = HashSet::new(); + let mut callee_slots = HashSet::new(); + caller_slots + .try_reserve(source.formals.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + callee_slots + .try_reserve(source.formals.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for (index, formal) in source.formals.iter().enumerate() { + let target = &signature_record.parameters[index]; + let source_index = formal.source_index as usize; + if formal.parameter as usize != index + || source_index >= source.formals.len() + || source_indices[source_index] + || source.evaluation_order[source_index] != formal.parameter + || formal.callee_shape != target.shape + || formal.access != target.access + { + return Err(ResourceLayoutError::OperationMismatch); + } + source_indices[source_index] = true; + get(&layout.shapes, formal.actual_shape)?; + match formal.value { + NativeSourceValue::CarrierOwned { .. } | NativeSourceValue::CarrierView { .. } => { + super::carriers::validate_formal( + layout, + record, + caller_frame, + callee_scope, + formal, + &mut caller_slots, + &mut callee_slots, + )? + } + NativeSourceValue::Ordinary => { + if occupied(layout, formal.actual_shape)? || occupied(layout, formal.callee_shape)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => { + let caller = get(&layout.slots, caller_argument_slot)?; + let target = get(&layout.slots, callee_parameter_slot)?; + if formal.syntax != NativeSourceSyntax::Bare + || formal.effect != NativeSourceEffect::TransferOwned + || formal.access != NativeAccess::Owned + || formal.actual_shape != formal.callee_shape + || caller.frame != caller_frame + || target.frame != callee_scope + || caller.shape != formal.actual_shape + || target.shape != formal.callee_shape + || caller.path != target.path + || !caller_slots.insert(caller_argument_slot) + || !callee_slots.insert(callee_parameter_slot) + { + return Err(ResourceLayoutError::OperationMismatch); + } + if slot_kind(layout, caller)? != slot_kind(layout, target)? { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeSourceValue::ResidentSumView { source } => { + let tuple = matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ); + if !tuple + || formal.access != NativeAccess::View + || formal.actual_shape != formal.callee_shape + || sum_source_shape(layout, source, record.site.function)? + != formal.callee_shape + { + return Err(ResourceLayoutError::OperationMismatch); + } + if formal.effect == NativeSourceEffect::RelinquishOwned { + let NativeSumLoanSource::ExistingBorrow { operation } = source else { + return Err(ResourceLayoutError::OperationMismatch); + }; + let NativeOperation::BorrowSum { + frame, + source_sum_slot, + lease_frame, + } = get(&layout.operations, operation)?.operation + else { + return Err(ResourceLayoutError::OperationMismatch); + }; + if frame != caller_frame + || lease_frame != caller_frame + || get(&layout.slots, source_sum_slot)?.frame != caller_frame + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + } + NativeSourceValue::ResidentView { source } => { + let NativeShape::Resource { kind } = *get(&layout.shapes, formal.callee_shape)? + else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + let tuple = matches!( + (formal.syntax, formal.effect), + ( + NativeSourceSyntax::Bare, + NativeSourceEffect::RelinquishOwned + ) | ( + NativeSourceSyntax::WrittenView, + NativeSourceEffect::RetainBorrow + ) + ); + if !tuple + || formal.access != NativeAccess::View + || formal.actual_shape != formal.callee_shape + || source_kind(layout, source, record.site.function)? != kind + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + } + } + match &source.result { + NativeSourceResult::Carrier { .. } => super::carriers::validate_result( + layout, + record, + caller_frame, + callee, + signature_record, + scope, + source, + )?, + NativeSourceResult::Ordinary { shape } => { + if *shape != signature_record.result + || occupied(layout, *shape)? + || source.callee_return.is_some() + || !scope.parents.contains(&NativeParent::Frame(caller_frame)) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + let return_frame = frame( + layout, + *callee_return_frame, + callee, + NativeFrameRole::Return, + )?; + let destination = get(&layout.slots, *caller_destination_slot)?; + let destination_frame = get(&layout.frames, *caller_destination_frame)?; + if *shape != signature_record.result + || !occupied(layout, *shape)? + || source.callee_return != Some(*callee_return_frame) + || return_frame.signature != signature + || !return_frame + .parents + .contains(&NativeParent::Frame(caller_frame)) + || !scope + .parents + .contains(&NativeParent::Frame(*callee_return_frame)) + || destination.frame != *caller_destination_frame + || destination.shape != *shape + || destination_frame.site.function != record.site.function + || permitted_return_slots.is_empty() + || permitted_return_slots + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(ResourceLayoutError::OperationMismatch); + } + for id in permitted_return_slots { + let slot = get(&layout.slots, *id)?; + if slot.frame != *callee_return_frame + || slot.shape != *shape + || slot.path != destination.path + || slot_kind(layout, slot)? != slot_kind(layout, destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + } + } + Ok(()) +} + +pub(super) fn operation( + layout: &WireLayout, + record: &NativeOperationRecord, +) -> Result<(), ResourceLayoutError> { + if !matches!(layout.version, 2 | 3) { + return Err(ResourceLayoutError::OperationMismatch); + } + let get_frame = |id| -> Result<&NativeFrame, ResourceLayoutError> { + let result = get(&layout.frames, id)?; + if result.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(result) + }; + match record.operation { + NativeOperation::TakeFailureCompanion { + frame, + source_sum_slot, + failure_shape, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, source_sum_slot)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, slot.shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if fail != failure_shape + || occupied(layout, fail)? + || slot.path != [NativePayloadStep::Ok] + || get(&layout.frames, slot.frame)?.site.function != record.site.function + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::PublishReturn { + frame, + source_return_slot, + } => { + let scope = get_frame(frame)?; + let slot = get(&layout.slots, source_return_slot)?; + let return_frame = get_frame(slot.frame)?; + if scope.role != NativeFrameRole::Scope + || return_frame.role != NativeFrameRole::Return + || scope.signature != return_frame.signature + || slot.shape != get(&layout.signatures, return_frame.signature)?.result + || !scope.parents.contains(&NativeParent::Frame(slot.frame)) + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + NativeOperation::CreateAbsentSum { + frame, + destination_slot, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, destination_slot)?; + if slot.frame != frame + || slot.path != [NativePayloadStep::Some] + || !matches!(get(&layout.shapes,slot.shape)?,NativeShape::Optional { child } + if matches!(get(&layout.shapes,*child)?,NativeShape::Resource { .. })) + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::CreateFailureSum { + frame, + destination_slot, + failure_shape, + } => { + get_frame(frame)?; + let slot = get(&layout.slots, destination_slot)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, slot.shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if slot.frame != frame + || slot.path != [NativePayloadStep::Ok] + || fail != failure_shape + || occupied(layout, fail)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::BorrowSum { + frame, + source_sum_slot, + lease_frame, + } => { + get_frame(frame)?; + if lease_frame != frame { + return Err(ResourceLayoutError::FrameMismatch); + } + let slot = get(&layout.slots, source_sum_slot)?; + if get_frame(slot.frame)?.site.function != record.site.function + || slot.path != [conditional_kind(layout, slot.shape)?.1] + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::ObserveSumView { frame, source } => { + get_frame(frame)?; + sum_source_shape(layout, source, record.site.function)?; + } + NativeOperation::ProjectSumView { + frame, lease_frame, .. + } => { + get_frame(frame)?; + if lease_frame != frame { + return Err(ResourceLayoutError::FrameMismatch); + } + projected_kind(layout, record.ordinal)?; + } + NativeOperation::ReadFailureCompanion { + frame, + source, + failure_shape, + } => { + get_frame(frame)?; + let shape = sum_source_shape(layout, source, record.site.function)?; + let NativeShape::Result { fail, .. } = *get(&layout.shapes, shape)? else { + return Err(ResourceLayoutError::ShapeMismatch); + }; + if fail != failure_shape || !matches!(get(&layout.shapes, fail)?, NativeShape::String) { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + NativeOperation::EndSumBorrow { frame, borrow } => { + get_frame(frame)?; + let row = get(&layout.operations, borrow)?; + if row.site.function != record.site.function + || !(matches!(row.operation, NativeOperation::BorrowSum { frame: f, .. } if f == frame) + || matches!(row.operation,NativeOperation::Carrier{record:id} if layout.version==3 && matches!(layout.carriers.operations.get(id as usize).map(|r|&r.operation),Some(super::carriers::NativeCarrierOperation::AdaptSum{frame:f,borrowed:true,..}) if *f==frame))) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + _ => return Err(ResourceLayoutError::OperationMismatch), + } + Ok(()) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/tests.rs b/crates/jett_runtime/src/resource_custody/registration/tests.rs new file mode 100644 index 00000000..cf46a7f0 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/tests.rs @@ -0,0 +1,936 @@ +use super::schema::WireLayout; +use super::wire::{decode, encode_records}; +use super::*; +use crate::AuthorityProvenance; +use crate::resource_custody::{ + OwnedResourceToken, ResourceBodyOutcome, ResourceCleanupFailure, ResourceCompletionFailure, + ResourceCustody, ResourceFrameKind, ResourcePurpose, complete_resource, +}; +use std::mem::{align_of, offset_of, size_of}; +use std::sync::Mutex; + +fn site(function: u32, index: u32) -> NativeSite { + NativeSite { + function, + block: 0, + position: NativePosition::Statement(index), + } +} +fn canonical() -> WireLayout { + let view = |shape| NativeFormal { + shape, + access: NativeAccess::View, + }; + let owned = |shape| NativeFormal { + shape, + access: NativeAccess::Owned, + }; + let signature = |ordinal, parameters, result| NativeSignature { + ordinal, + parameters, + result, + }; + let frame = |ordinal, role, function, index, signature, parents| NativeFrame { + ordinal, + role, + site: site(function, index), + signature, + parents, + }; + let slot = |ordinal, frame, shape, path| NativeSlot { + ordinal, + frame, + shape, + path, + }; + use NativeOperation::*; + let loan = NativeLoanSource::ExistingBorrow { operation: 3 }; + let incoming = NativeLoanSource::IncomingViewFormal { + scope: 3, + parameter: 0, + }; + let operations = vec![ + Acquire { + frame: 1, + hook: 0, + destination: 2, + }, + SumTake { + frame: 1, + source: 2, + destination: 1, + }, + Transfer { + frame: 0, + source: 1, + destination: 0, + }, + Borrow { + frame: 1, + source: 0, + lease_frame: 1, + }, + InvokeBorrow { + frame: 1, + hook: 1, + source: loan, + }, + BoundedBorrowUse { + frame: 1, + source: loan, + callee_frame: 3, + }, + EndBorrow { + frame: 1, + borrow: 3, + }, + Close { + frame: 1, + hook: 2, + source: 0, + }, + Drop { + frame: 0, + source: 1, + }, + SumAdopt { + frame: 1, + source: 1, + destination: 2, + }, + SumDrop { + frame: 1, + source: 2, + }, + Replace { + frame: 0, + old: 0, + replacement: 1, + }, + Complete { frame: 0 }, + Descriptor { hook: 0 }, + InvokeDescriptor { + hook: 0, + signature: 0, + target: 0, + }, + InvokeSourceFunction { + frame: 1, + callee: 1, + signature: 4, + callee_scope: 3, + source: None, + }, + InvokeBorrow { + frame: 1, + hook: 1, + source: incoming, + }, + BoundedBorrowUse { + frame: 1, + source: incoming, + callee_frame: 3, + }, + ] + .into_iter() + .enumerate() + .map(|(ordinal, operation)| NativeOperationRecord { + ordinal: ordinal as u32, + site: site(0, 100 + ordinal as u32), + operation, + }) + .collect(); + WireLayout { + carriers: super::carriers::NativeCarrierLayout::default(), + version: 1, + kinds: vec![0, 1], + hooks: vec![ + NativeHook { + ordinal: 0, + kind: 0, + recipe: NativeRecipe::NetworkFactory, + signature: 0, + }, + NativeHook { + ordinal: 1, + kind: 0, + recipe: NativeRecipe::NetworkBorrow, + signature: 1, + }, + NativeHook { + ordinal: 2, + kind: 0, + recipe: NativeRecipe::Finalize, + signature: 2, + }, + NativeHook { + ordinal: 3, + kind: 1, + recipe: NativeRecipe::NetworkFactory, + signature: 5, + }, + ], + signatures: vec![ + signature(0, vec![view(0), owned(1)], 6), + signature(1, vec![view(0), view(4)], 7), + signature(2, vec![owned(4)], 3), + signature(3, vec![], 3), + signature(4, vec![view(4)], 3), + signature(5, vec![view(0), owned(1)], 13), + ], + shapes: vec![ + NativeShape::Network, + NativeShape::Integer { + bits: 64, + signed: true, + }, + NativeShape::String, + NativeShape::Nothing, + NativeShape::Resource { kind: 0 }, + NativeShape::Resource { kind: 1 }, + NativeShape::Result { ok: 4, fail: 2 }, + NativeShape::Result { ok: 1, fail: 2 }, + NativeShape::Optional { child: 4 }, + NativeShape::HookDescriptor { hook: 0 }, + NativeShape::Bool, + NativeShape::Float { bits: 64 }, + NativeShape::Integer { + bits: 8, + signed: false, + }, + NativeShape::Result { ok: 5, fail: 2 }, + ], + frames: vec![ + frame(0, NativeFrameRole::Scope, 0, 0, 3, vec![NativeParent::Root]), + frame( + 1, + NativeFrameRole::Operation, + 0, + 1, + 3, + vec![NativeParent::Frame(0)], + ), + frame( + 2, + NativeFrameRole::Return, + 0, + 2, + 3, + vec![NativeParent::Frame(0)], + ), + frame( + 3, + NativeFrameRole::Scope, + 1, + 0, + 4, + vec![NativeParent::Frame(1)], + ), + ], + slots: vec![ + slot(0, 0, 4, vec![]), + slot(1, 1, 4, vec![]), + slot(2, 1, 6, vec![NativePayloadStep::Ok]), + slot(3, 0, 8, vec![NativePayloadStep::Some]), + slot(4, 0, 5, vec![]), + ], + operations, + } +} +fn parsed(layout: &WireLayout) -> Result { + let decoded = decode(&encode_records(layout))?; + validation::validate(&decoded)?; + Ok(decoded) +} +fn installed(registry: &ResourceRegistry) -> Arc { + NativeLayoutInstallation::new(registry) + .install(registry, &encode_records(&canonical())) + .unwrap() +} + +fn source_v2() -> WireLayout { + let mut layout = canonical(); + layout.version = 2; + let NativeOperation::InvokeSourceFunction { source, .. } = &mut layout.operations[15].operation + else { + panic!("canonical Source row") + }; + *source = Some(NativeSourceInvocation { + callee_return: None, + evaluation_order: vec![0], + formals: vec![NativeSourceFormal { + parameter: 0, + source_index: 0, + actual_shape: 4, + callee_shape: 4, + syntax: NativeSourceSyntax::WrittenView, + effect: NativeSourceEffect::RetainBorrow, + access: NativeAccess::View, + value: NativeSourceValue::ResidentView { + source: NativeLoanSource::ExistingBorrow { operation: 3 }, + }, + }], + result: NativeSourceResult::Ordinary { shape: 3 }, + }); + layout +} + +#[test] +fn native_registration_source_v2_roundtrips_the_complete_boundary_without_upgrading_v1() { + let layout = source_v2(); + assert_eq!(parsed(&layout).unwrap(), layout); + let registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let installed = installation + .install(®istry, &encode_records(&layout)) + .unwrap(); + assert_eq!(installed.wire_version(), 2); + assert_eq!(parsed(&canonical()).unwrap().version, 1); + let mut missing = layout.clone(); + let NativeOperation::InvokeSourceFunction { source, .. } = + &mut missing.operations[15].operation + else { + unreachable!() + }; + *source = None; + assert!(parsed(&missing).is_err()); + let mut wrong_version = layout; + wrong_version.version = 1; + assert!(parsed(&wrong_version).is_err()); +} + +#[test] +fn native_registration_source_v2_rejects_unused_permutation_formal_and_resident_substitutions() { + for case in 0..7 { + let mut layout = source_v2(); + let NativeOperation::InvokeSourceFunction { + source: Some(source), + .. + } = &mut layout.operations[15].operation + else { + unreachable!() + }; + match case { + 0 => source.evaluation_order[0] = 1, + 1 => source.formals[0].source_index = 1, + 2 => source.formals[0].syntax = NativeSourceSyntax::Bare, + 3 => source.formals[0].effect = NativeSourceEffect::TransferOwned, + 4 => source.formals[0].value = NativeSourceValue::Ordinary, + 5 => { + source.formals[0].value = NativeSourceValue::ResidentView { + source: NativeLoanSource::ExistingBorrow { operation: 0 }, + } + } + 6 => source.formals[0].actual_shape = 5, + _ => unreachable!(), + } + assert!( + parsed(&layout).is_err(), + "unused Source metadata case {case}" + ); + } +} + +#[test] +fn native_registration_source_v2_rejects_unbound_return_and_occupied_failure_companion_roles() { + for operation in [ + NativeOperation::PublishReturn { + frame: 3, + source_return_slot: 0, + }, + NativeOperation::TakeFailureCompanion { + frame: 1, + source_sum_slot: 2, + failure_shape: 4, + }, + NativeOperation::CreateFailureSum { + frame: 1, + destination_slot: 2, + failure_shape: 4, + }, + ] { + let mut layout = source_v2(); + let ordinal = layout.operations.len() as u32; + layout.operations.push(NativeOperationRecord { + ordinal, + site: site(1, 999), + operation, + }); + assert!(parsed(&layout).is_err()); + } +} + +#[test] +fn native_registration_roundtrips_closed_records_and_owns_input() { + let layout = canonical(); + assert_eq!(parsed(&layout).unwrap(), layout); + let mut registry = ResourceRegistry::new(); + let mut installation = NativeLayoutInstallation::new(®istry); + let mut bytes = encode_records(&layout); + let registered = installation.install(®istry, &bytes).unwrap(); + bytes.fill(0); + assert!(Arc::ptr_eq(installation.installed().unwrap(), ®istered)); + assert_eq!(registered.hooks()[0].recipe(), NativeRecipe::NetworkFactory); + assert_eq!(registered.shapes(), layout.shapes); + assert_eq!(registered.operations(), layout.operations); + assert_eq!( + (registered.kinds_len(), registered.slot_kind(2).unwrap()), + (2, 0) + ); + assert_ne!(registered.kind_id(0).unwrap(), ResourceTypeId::new(0)); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + assert_eq!(registry.live_count(), 0); + assert_eq!( + ( + size_of::(), + align_of::() + ), + (16, 8) + ); + assert_eq!( + ( + offset_of!(JettResourceCallResultV1, domain), + offset_of!(JettResourceCallResultV1, reserved), + offset_of!(JettResourceCallResultV1, value) + ), + (0, 4, 8) + ); + assert_eq!((RESOURCE_DOMAIN_OK, RESOURCE_DOMAIN_ERROR), (0, 1)); +} + +#[test] +fn native_registration_refuses_wire_headers_lengths_tags_and_ordinals() { + let bytes = encode_records(&canonical()); + for end in 0..bytes.len() { + assert!(decode(&bytes[..end]).is_err(), "prefix {end}"); + } + let mutate = |offset, value: u32| { + let mut altered = bytes.clone(); + altered[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + altered + }; + for unsupported_version in [0, NATIVE_RESOURCE_LAYOUT_WIRE_VERSION + 1, u32::MAX] { + assert_eq!( + decode(&mutate(8, unsupported_version)).unwrap_err(), + ResourceLayoutError::Header + ); + } + assert_eq!( + decode(&mutate(12, 1)).unwrap_err(), + ResourceLayoutError::Reserved + ); + assert_eq!( + decode(&mutate(52, 1)).unwrap_err(), + ResourceLayoutError::Reserved + ); + assert_eq!( + decode(&mutate(24, 4097)).unwrap_err(), + ResourceLayoutError::WireLimit + ); + assert_eq!( + decode(&mutate(56, 1)).unwrap_err(), + ResourceLayoutError::DenseOrdinal + ); + // Header56 + two kind rows8 + hook ordinal/kind8 = first recipe tag72. + assert_eq!( + decode(&mutate(72, 99)).unwrap_err(), + ResourceLayoutError::UnknownTag + ); + let mut trailing = bytes.clone(); + trailing.push(0); + let length = trailing.len() as u64; + trailing[16..24].copy_from_slice(&length.to_le_bytes()); + assert_eq!( + decode(&trailing).unwrap_err(), + ResourceLayoutError::Trailing + ); + assert_eq!( + decode(&vec![0; wire::MAX_BYTES + 1]).unwrap_err(), + ResourceLayoutError::WireLimit + ); +} + +#[test] +fn native_registration_validates_unused_rows_recipes_shapes_and_active_paths() { + let mut layout = canonical(); + layout.hooks[3].signature = 0; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::RecipeMismatch + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::String); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + let mut duplicate = layout.signatures[0].clone(); + duplicate.ordinal = layout.signatures.len() as u32; + layout.signatures.push(duplicate); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + layout.shapes[8] = NativeShape::Optional { child: 8 }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::Optional { child: 8 }); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::UnsupportedLayout + ); + let mut layout = canonical(); + layout.shapes.push(NativeShape::Result { ok: 1, fail: 4 }); + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::UnsupportedLayout + ); + let mut layout = canonical(); + layout.slots[2].path = vec![NativePayloadStep::Fail]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::PayloadPath + ); + let mut layout = canonical(); + layout.slots[4].shape = 99; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + // Equal tuples can describe distinct original hook declarations/storage sites. + let mut layout = canonical(); + let mut hook = layout.hooks[0].clone(); + hook.ordinal = 4; + layout.hooks.push(hook); + let mut slot = layout.slots[0].clone(); + slot.ordinal = 5; + layout.slots.push(slot); + assert!(parsed(&layout).is_ok()); +} + +#[test] +fn native_registration_checks_all_frame_operation_and_loan_source_relations() { + let mut layout = canonical(); + layout.frames[1].parents = vec![NativeParent::Frame(0), NativeParent::Frame(0)]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::NonCanonical + ); + let mut layout = canonical(); + layout.frames[1].parents = vec![NativeParent::Frame(99)]; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let mut layout = canonical(); + layout.operations[4].operation = NativeOperation::InvokeBorrow { + frame: 1, + hook: 0, + source: NativeLoanSource::ExistingBorrow { operation: 3 }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[6].operation = NativeOperation::EndBorrow { + frame: 1, + borrow: 12, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[14].operation = NativeOperation::InvokeDescriptor { + hook: 0, + signature: 0, + target: 13, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::OperationMismatch + ); + let mut layout = canonical(); + layout.operations[15].operation = NativeOperation::InvokeSourceFunction { + frame: 1, + callee: 0, + signature: 4, + callee_scope: 3, + source: None, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::FrameMismatch + ); + let mut layout = canonical(); + layout.operations[16].operation = NativeOperation::InvokeBorrow { + frame: 1, + hook: 1, + source: NativeLoanSource::IncomingViewFormal { + scope: 0, + parameter: 0, + }, + }; + assert_eq!( + parsed(&layout).unwrap_err(), + ResourceLayoutError::InvalidReference + ); + let registry = ResourceRegistry::new(); + let registered = installed(®istry); + let core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + // Lawful metadata never mints a resident loan, active scope, owner or provider. + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn native_registration_function_frame_signatures_cannot_depend_on_instruction_site() { + let layout = canonical(); + assert_ne!(layout.frames[0].site, layout.frames[1].site); + assert_ne!(layout.frames[0].site, layout.frames[2].site); + assert!(parsed(&layout).is_ok()); + for frame in [1, 2] { + let mut altered = layout.clone(); + altered.frames[frame].signature = 0; + assert_eq!( + parsed(&altered).unwrap_err(), + ResourceLayoutError::FrameMismatch + ); + } +} + +#[test] +fn native_registration_issuer_checks_context_program_kind_slot_and_exhaustion() { + let mut registry = ResourceRegistry::new(); + let foreign = ResourceRegistry::new(); + let bytes = encode_records(&canonical()); + let mut installation = NativeLayoutInstallation::new(®istry); + assert_eq!( + installation.install(&foreign, &bytes).unwrap_err(), + ResourceLayoutError::WrongContext + ); + let registered = installation.install(®istry, &bytes).unwrap(); + assert_eq!( + installation.install(®istry, &bytes).unwrap_err(), + ResourceLayoutError::AlreadyInstalled + ); + let other = installed(®istry); + assert_ne!(registered.kind_id(0).unwrap(), other.kind_id(0).unwrap()); + assert!(matches!( + ResourceCustody::new(registered.program().clone(), &foreign), + Err(CustodyError::WrongContext) + )); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&frame, ®istered.slot(0).unwrap()).unwrap(); + assert_eq!( + core.prepare_acquisition(&other.kind(0).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongProgram + ); + assert_eq!( + core.prepare_acquisition(®istered.kind(1).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongKind + ); + assert!(matches!( + registered.slot(99), + Err(CustodyError::UnknownSlot) + )); + assert!(matches!(registered.kind(99), Err(CustodyError::WrongKind))); + let counter = AtomicU64::new(u64::from(u32::MAX)); + assert_eq!( + issued_kinds(&counter, 2).unwrap_err(), + ResourceLayoutError::CapacityExhausted + ); + assert_eq!( + issued_kinds(&counter, 1).unwrap(), + vec![ResourceTypeId::new(u32::MAX)] + ); + assert_eq!( + issued_kinds(&counter, 1).unwrap_err(), + ResourceLayoutError::CapacityExhausted + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); +} + +#[test] +fn native_registration_physical_preflight_refuses_before_provider_or_publication() { + let bytes = encode_records(&canonical()); + let mut busy = ResourceRegistry::new(); + let busy_key = busy + .insert( + ResourceTypeId::new(7), + (), + AuthorityProvenance::new(1, 1), + |_| {}, + ) + .unwrap(); + assert_eq!( + NativeLayoutInstallation::new(&busy) + .install(&busy, &bytes) + .unwrap_err(), + ResourceLayoutError::RegistryBusy + ); + busy.close(busy_key, ResourceTypeId::new(7)).unwrap(); + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + registry.shutting_down = true; + assert_eq!( + NativeLayoutInstallation::new(®istry) + .install(®istry, &bytes) + .unwrap_err(), + ResourceLayoutError::RegistryShutdown + ); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::RegistryShutdown) + ); + registry.shutting_down = false; + registry.next_creation_sequence = u64::MAX; + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::CapacityExhausted) + ); + registry.next_creation_sequence = 1; + registry.free_slots.push(0); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::PhysicalStorage) + ); + registry.free_slots.clear(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + assert_eq!(registry.live_count(), 0); + registry.slots.push(crate::Slot { + generation: u64::MAX, + entry: None, + }); + registry.free_slots.push(0); + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::CapacityExhausted) + ); + registry.slots[0].generation = 0; + assert_eq!( + registered.preflight_physical_acquisition(&mut registry), + Err(ResourceLayoutError::PhysicalStorage) + ); + registry.slots[0].generation = 1; + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + if let Some(index) = (u32::MAX as usize).checked_add(1) { + assert_eq!( + checked_physical_slot(index), + Err(ResourceLayoutError::CapacityExhausted) + ); + } + let mut foreign = ResourceRegistry::new(); + assert_eq!( + registered.preflight_physical_acquisition(&mut foreign), + Err(ResourceLayoutError::WrongContext) + ); +} + +#[test] +fn native_registration_required_purposes_cannot_acquire_from_signature_metadata() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let frame = core.begin_frame(ResourceFrameKind::Scope, purpose).unwrap(); + let holder = core.holder(&frame, ®istered.slot(0).unwrap()).unwrap(); + assert_eq!( + core.prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap_err(), + CustodyError::WrongPurpose + ); + assert_eq!( + (core.live_owners(), core.live_loans(), registry.live_count()), + (0, 0, 0) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + } +} + +#[test] +fn native_registration_real_core_preserves_holder_generation_loans_and_once_cleanup() { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::::new())); + let root = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&root, ®istered.slot(0).unwrap()).unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let mut owner = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 501i64, + move |value| captured.lock().unwrap().push(value), + ) + .unwrap(); + let stale = OwnedResourceToken { + core: owner.core, + owner: owner.owner, + holder_generation: owner.holder_generation, + }; + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let other_holder = core + .holder(&operation, ®istered.slot(1).unwrap()) + .unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &other_holder) + .unwrap(); + let captured = events.clone(); + let _other = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 502i64, + move |value| captured.lock().unwrap().push(value), + ) + .unwrap(); + let destination = core + .holder(&operation, ®istered.slot(2).unwrap()) + .unwrap(); + let mut loan = core.begin_borrow(&owner, &operation, ®istry).unwrap(); + assert!(core.validate_borrowed(&loan, ®istry).is_ok()); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::ActiveBorrow) + ); + core.end_borrow(&mut loan).unwrap(); + core.transfer(&mut owner, &destination, ®istry).unwrap(); + assert_eq!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + ); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [501, 502]); + assert_eq!(core.end_frame(&root, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [501, 502]); + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn native_registration_real_core_continues_cleanup_and_preserves_cleanup_precedence() { + let mut registry = ResourceRegistry::new(); + let registered = installed(®istry); + let mut core = ResourceCustody::new(registered.program().clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::::new())); + let root = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = core.holder(&root, ®istered.slot(0).unwrap()).unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let _first = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 601i64, + move |label| captured.lock().unwrap().push(label), + ) + .unwrap(); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let holder = core + .holder(&operation, ®istered.slot(1).unwrap()) + .unwrap(); + registered + .preflight_physical_acquisition(&mut registry) + .unwrap(); + let prepared = core + .prepare_acquisition(®istered.kind(0).unwrap(), &holder) + .unwrap(); + let captured = events.clone(); + let _second = core + .commit_acquisition( + &mut registry, + prepared, + AuthorityProvenance::new(5, 7), + 602i64, + move |label| { + captured.lock().unwrap().push(label); + panic!("selected registered-layout finalizer failure"); + }, + ) + .unwrap(); + let cleanup = core.unwind_all(&mut registry); + assert_eq!( + complete_resource( + ResourceBodyOutcome::<(), &str>::Completed(Err("selected body failure")), + cleanup + ), + Err(ResourceCompletionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert_eq!(*events.lock().unwrap(), [602, 601]); + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [602, 601]); +} diff --git a/crates/jett_runtime/src/resource_custody/registration/validation.rs b/crates/jett_runtime/src/resource_custody/registration/validation.rs new file mode 100644 index 00000000..a94a8ae7 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/validation.rs @@ -0,0 +1,486 @@ +//! Layout consistency only. Original Source/CFG proof is the compiler's obligation. + +use super::ResourceLayoutError; +use super::schema::*; +use super::wire::storage; +use std::collections::{HashMap, HashSet}; + +fn get(values: &[T], ordinal: u32) -> Result<&T, ResourceLayoutError> { + values + .get(ordinal as usize) + .ok_or(ResourceLayoutError::InvalidReference) +} + +fn validate_shapes(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut unique = HashSet::new(); + unique + .try_reserve(layout.shapes.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + let mut occupied = storage::(layout.shapes.len())?; + for (index, shape) in layout.shapes.iter().enumerate() { + if !unique.insert(shape) { + return Err(ResourceLayoutError::NonCanonical); + } + let contains_resource = match *shape { + NativeShape::Integer { bits, .. } if [8, 16, 32, 64].contains(&bits) => false, + NativeShape::Float { bits } if [32, 64].contains(&bits) => false, + NativeShape::Integer { .. } | NativeShape::Float { .. } => { + return Err(ResourceLayoutError::UnsupportedLayout); + } + NativeShape::Bool + | NativeShape::String + | NativeShape::Nothing + | NativeShape::Network => false, + NativeShape::Resource { kind } => { + get(&layout.kinds, kind)?; + true + } + NativeShape::Optional { child } => { + if child as usize >= index { + return Err(ResourceLayoutError::InvalidReference); + } + let has_resource = occupied[child as usize]; + if has_resource + && !matches!(layout.shapes[child as usize], NativeShape::Resource { .. }) + { + return Err(ResourceLayoutError::UnsupportedLayout); + } + has_resource + } + NativeShape::Result { ok, fail } => { + if ok as usize >= index || fail as usize >= index { + return Err(ResourceLayoutError::InvalidReference); + } + if occupied[fail as usize] + || (occupied[ok as usize] + && !matches!(layout.shapes[ok as usize], NativeShape::Resource { .. })) + { + return Err(ResourceLayoutError::UnsupportedLayout); + } + occupied[ok as usize] + } + NativeShape::Carrier { node } => { + if layout.version != 3 { + return Err(ResourceLayoutError::UnsupportedLayout); + } + layout.carriers.node(node)?; + true + } + NativeShape::HookDescriptor { hook } => { + get(&layout.hooks, hook)?; + false + } + }; + occupied.push(contains_resource); + } + Ok(()) +} + +fn validate_signatures(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut unique = HashSet::new(); + unique + .try_reserve(layout.signatures.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for signature in &layout.signatures { + get(&layout.shapes, signature.result)?; + for formal in &signature.parameters { + get(&layout.shapes, formal.shape)?; + } + if !unique.insert((signature.parameters.as_slice(), signature.result)) { + return Err(ResourceLayoutError::NonCanonical); + } + } + Ok(()) +} + +fn shape_is(layout: &WireLayout, ordinal: u32, expected: NativeShape) -> bool { + layout.shapes.get(ordinal as usize) == Some(&expected) +} + +fn validate_hooks(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + for hook in &layout.hooks { + get(&layout.kinds, hook.kind)?; + let signature = get(&layout.signatures, hook.signature)?; + let formal = |index: usize, shape: NativeShape, access| { + signature.parameters.get(index).is_some_and(|parameter| { + parameter.access == access && shape_is(layout, parameter.shape, shape) + }) + }; + let resource = NativeShape::Resource { kind: hook.kind }; + let integer = NativeShape::Integer { + bits: 64, + signed: true, + }; + let returns_result = |ok: NativeShape| { + let NativeShape::Result { ok: value, fail } = layout.shapes[signature.result as usize] + else { + return false; + }; + shape_is(layout, value, ok) && shape_is(layout, fail, NativeShape::String) + }; + let matches = match hook.recipe { + NativeRecipe::NetworkFactory => { + signature.parameters.len() == 2 + && formal(0, NativeShape::Network, NativeAccess::View) + && formal(1, integer, NativeAccess::Owned) + && returns_result(resource) + } + NativeRecipe::NetworkBorrow => { + signature.parameters.len() == 2 + && formal(0, NativeShape::Network, NativeAccess::View) + && formal(1, resource, NativeAccess::View) + && returns_result(integer) + } + NativeRecipe::Finalize => { + signature.parameters.len() == 1 + && formal(0, resource, NativeAccess::Owned) + && shape_is(layout, signature.result, NativeShape::Nothing) + } + }; + if !matches { + return Err(ResourceLayoutError::RecipeMismatch); + } + } + Ok(()) +} + +fn validate_frames(layout: &WireLayout) -> Result<(), ResourceLayoutError> { + let mut signatures = HashMap::new(); + signatures + .try_reserve(layout.frames.len()) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + for frame in &layout.frames { + get(&layout.signatures, frame.signature)?; + if let Some(previous) = signatures.insert(frame.site.function, frame.signature) { + if previous != frame.signature { + return Err(ResourceLayoutError::FrameMismatch); + } + } + if frame.parents.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(ResourceLayoutError::NonCanonical); + } + for parent in &frame.parents { + if let NativeParent::Frame(index) = parent { + get(&layout.frames, *index)?; + } + } + } + Ok(()) +} + +pub(super) fn slot_kind( + layout: &WireLayout, + slot: &NativeSlot, +) -> Result { + let mut shape = get(&layout.shapes, slot.shape)?; + for step in &slot.path { + shape = match (shape, step) { + (NativeShape::Optional { child }, NativePayloadStep::Some) => { + get(&layout.shapes, *child)? + } + (NativeShape::Result { ok, .. }, NativePayloadStep::Ok) => get(&layout.shapes, *ok)?, + (NativeShape::Result { fail, .. }, NativePayloadStep::Fail) => { + get(&layout.shapes, *fail)? + } + _ => return Err(ResourceLayoutError::PayloadPath), + }; + } + if let NativeShape::Resource { kind } = shape { + Ok(*kind) + } else { + Err(ResourceLayoutError::PayloadPath) + } +} + +fn source_kind(layout: &WireLayout, source: NativeLoanSource) -> Result { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + super::source_validation::projected_kind(layout, operation) + } + NativeLoanSource::ExistingBorrow { operation } => { + let record = get(&layout.operations, operation)?; + let NativeOperation::Borrow { source, .. } = record.operation else { + return Err(ResourceLayoutError::OperationMismatch); + }; + slot_kind(layout, get(&layout.slots, source)?) + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + let frame = get(&layout.frames, scope)?; + if frame.role != NativeFrameRole::Scope { + return Err(ResourceLayoutError::FrameMismatch); + } + let signature = get(&layout.signatures, frame.signature)?; + let formal = get(&signature.parameters, parameter)?; + if formal.access != NativeAccess::View { + return Err(ResourceLayoutError::OperationMismatch); + } + match *get(&layout.shapes, formal.shape)? { + NativeShape::Resource { kind } => Ok(kind), + _ => Err(ResourceLayoutError::ShapeMismatch), + } + } + } +} + +fn validate_operation( + layout: &WireLayout, + record: &NativeOperationRecord, +) -> Result<(), ResourceLayoutError> { + let frame = |index| -> Result<&NativeFrame, ResourceLayoutError> { + let frame = get(&layout.frames, index)?; + if frame.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + Ok(frame) + }; + let slot = |index| get(&layout.slots, index); + let kind = |index| slot_kind(layout, slot(index)?); + let hook = |index, recipe, kind| -> Result<(), ResourceLayoutError> { + let hook = get(&layout.hooks, index)?; + if hook.recipe != recipe || hook.kind != kind { + return Err(ResourceLayoutError::OperationMismatch); + } + Ok(()) + }; + let same_slots = |a, b| -> Result<(), ResourceLayoutError> { + if a == b { + return Err(ResourceLayoutError::OperationMismatch); + } + let (a, b) = (slot(a)?, slot(b)?); + if a.shape != b.shape || a.path != b.path { + return Err(ResourceLayoutError::ShapeMismatch); + } + Ok(()) + }; + use NativeOperation::*; + if let Carrier { record: carrier } = record.operation { + let row = layout + .carriers + .operations + .get(carrier as usize) + .ok_or(ResourceLayoutError::InvalidReference)?; + if layout.version != 3 || row.site != record.site { + return Err(ResourceLayoutError::OperationMismatch); + } + return Ok(()); + } + if let InvokeBorrow { + source: NativeLoanSource::ProjectedSumPayload { operation }, + .. + } + | BoundedBorrowUse { + source: NativeLoanSource::ProjectedSumPayload { operation }, + .. + } = record.operation + { + if get(&layout.operations, operation)?.site.function != record.site.function { + return Err(ResourceLayoutError::FrameMismatch); + } + } + match record.operation { + Carrier { .. } => return Err(ResourceLayoutError::OperationMismatch), + Acquire { + frame: f, + hook: h, + destination, + } => { + frame(f)?; + hook(h, NativeRecipe::NetworkFactory, kind(destination)?)?; + } + Transfer { + frame: f, + source, + destination, + } => { + frame(f)?; + same_slots(source, destination)?; + } + Borrow { + frame: f, + source, + lease_frame, + } => { + frame(f)?; + kind(source)?; + get(&layout.frames, lease_frame)?; + } + BoundedBorrowUse { + frame: f, + source, + callee_frame, + } => { + frame(f)?; + let kind = source_kind(layout, source)?; + let callee = get(&layout.frames, callee_frame)?; + if callee.role != NativeFrameRole::Scope { + return Err(ResourceLayoutError::FrameMismatch); + } + let signature = get(&layout.signatures, callee.signature)?; + if !signature.parameters.iter().any(|p| { + p.access == NativeAccess::View + && shape_is(layout, p.shape, NativeShape::Resource { kind }) + }) { + return Err(ResourceLayoutError::OperationMismatch); + } + } + EndBorrow { frame: f, borrow } => { + frame(f)?; + if get(&layout.operations, borrow)?.site.function != record.site.function + || !matches!( + get(&layout.operations, borrow)?.operation, + Borrow { .. } | ProjectSumView { .. } + ) + { + return Err(ResourceLayoutError::OperationMismatch); + } + } + InvokeBorrow { + frame: f, + hook: h, + source, + } => { + frame(f)?; + hook(h, NativeRecipe::NetworkBorrow, source_kind(layout, source)?)?; + } + Close { + frame: f, + hook: h, + source, + } => { + frame(f)?; + hook(h, NativeRecipe::Finalize, kind(source)?)?; + } + Drop { frame: f, source } => { + frame(f)?; + kind(source)?; + } + SumAdopt { + frame: f, + source, + destination, + } => { + frame(f)?; + let (source_slot, destination_slot) = (slot(source)?, slot(destination)?); + if !source_slot.path.is_empty() + || destination_slot.path.len() != 1 + || kind(source)? != kind(destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + SumTake { + frame: f, + source, + destination, + } => { + frame(f)?; + let (source_slot, destination_slot) = (slot(source)?, slot(destination)?); + if source_slot.path.len() != 1 + || !destination_slot.path.is_empty() + || kind(source)? != kind(destination)? + { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + SumDrop { frame: f, source } => { + frame(f)?; + if slot(source)?.path.len() != 1 { + return Err(ResourceLayoutError::ShapeMismatch); + } + } + Replace { + frame: f, + old, + replacement, + } => { + frame(f)?; + same_slots(old, replacement)?; + } + Complete { frame: f } => { + frame(f)?; + } + Descriptor { hook } => { + get(&layout.hooks, hook)?; + } + InvokeDescriptor { + hook, + signature, + target, + } => { + let declared = get(&layout.hooks, hook)?; + if declared.signature != signature { + return Err(ResourceLayoutError::OperationMismatch); + } + let target = get(&layout.operations, target)?; + let (target_hook, recipe) = match target.operation { + Acquire { hook, .. } => (hook, NativeRecipe::NetworkFactory), + InvokeBorrow { hook, .. } => (hook, NativeRecipe::NetworkBorrow), + Close { hook, .. } => (hook, NativeRecipe::Finalize), + _ => return Err(ResourceLayoutError::OperationMismatch), + }; + if target_hook != hook || declared.recipe != recipe { + return Err(ResourceLayoutError::OperationMismatch); + } + } + InvokeSourceFunction { + frame: f, + callee, + signature, + callee_scope, + ref source, + } => { + frame(f)?; + if let Some(source) = source { + return super::source_validation::invocation( + layout, + record, + f, + callee, + signature, + callee_scope, + source, + ); + } + if layout.version != 1 { + return Err(ResourceLayoutError::OperationMismatch); + } + let scope = get(&layout.frames, callee_scope)?; + if scope.role != NativeFrameRole::Scope + || scope.site.function != callee + || scope.signature != signature + { + return Err(ResourceLayoutError::FrameMismatch); + } + } + BorrowSum { .. } + | ObserveSumView { .. } + | ProjectSumView { .. } + | ReadFailureCompanion { .. } + | EndSumBorrow { .. } + | TakeFailureCompanion { .. } + | PublishReturn { .. } + | CreateAbsentSum { .. } + | CreateFailureSum { .. } => { + return super::source_validation::operation(layout, record); + } + } + Ok(()) +} + +/// Returns the complete slot-kind projection before the issuer allocates identities. +pub(super) fn validate(layout: &WireLayout) -> Result, ResourceLayoutError> { + validate_shapes(layout)?; + validate_signatures(layout)?; + validate_hooks(layout)?; + validate_frames(layout)?; + let mut slot_kinds = storage(layout.slots.len())?; + for slot in &layout.slots { + get(&layout.frames, slot.frame)?; + slot_kinds.push(slot_kind(layout, slot)? as usize); + } + super::carriers::validate(layout)?; + for operation in &layout.operations { + validate_operation(layout, operation)?; + } + Ok(slot_kinds) +} diff --git a/crates/jett_runtime/src/resource_custody/registration/wire.rs b/crates/jett_runtime/src/resource_custody/registration/wire.rs new file mode 100644 index 00000000..76e5c5ee --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/wire.rs @@ -0,0 +1,1074 @@ +//! Bounded owned decoding; wire bytes contain no Source or registry-key authority. + +use super::ResourceLayoutError; +use super::carriers::{NativeCarrierLayout, NativeCarrierLoanSource}; +use super::schema::*; +#[path = "wire/carriers.rs"] +mod carriers; + +pub(super) const MAX_BYTES: usize = 1024 * 1024; +pub(super) const MAX_ROWS: usize = 4096; +const HEADER_BYTES: usize = 56; +const MAGIC: &[u8; 8] = b"JTRSC001"; + +struct Reader<'a> { + bytes: &'a [u8], + position: usize, +} +impl<'a> Reader<'a> { + fn take(&mut self) -> Result<[u8; N], ResourceLayoutError> { + let end = self + .position + .checked_add(N) + .ok_or(ResourceLayoutError::WireLimit)?; + let value = self + .bytes + .get(self.position..end) + .ok_or(ResourceLayoutError::Truncated)?; + self.position = end; + Ok(value.try_into().expect("fixed checked slice")) + } + fn word(&mut self) -> Result { + Ok(u32::from_le_bytes(self.take()?)) + } + fn count(&mut self) -> Result { + let count = self.word()? as usize; + if count > MAX_ROWS { + return Err(ResourceLayoutError::WireLimit); + } + Ok(count) + } + fn ordinal(&mut self, index: usize) -> Result<(), ResourceLayoutError> { + if self.word()? as usize != index { + return Err(ResourceLayoutError::DenseOrdinal); + } + Ok(()) + } + fn site(&mut self) -> Result { + let function = self.word()?; + let block = self.word()?; + let tag = self.word()?; + let index = self.word()?; + let position = match (tag, index) { + (0, index) => NativePosition::Statement(index), + (1, 0) => NativePosition::Terminator, + (1, _) => return Err(ResourceLayoutError::Reserved), + _ => return Err(ResourceLayoutError::UnknownTag), + }; + Ok(NativeSite { + function, + block, + position, + }) + } + fn loan(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 1 => NativeLoanSource::ExistingBorrow { + operation: self.word()?, + }, + 2 => NativeLoanSource::IncomingViewFormal { + scope: self.word()?, + parameter: self.word()?, + }, + 3 if matches!(version, 2 | 3) => NativeLoanSource::ProjectedSumPayload { + operation: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn sum_loan(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 3 if version == 3 => NativeSumLoanSource::CarrierProjected { + operation: self.word()?, + }, + 1 => NativeSumLoanSource::ExistingBorrow { + operation: self.word()?, + }, + 2 => NativeSumLoanSource::IncomingViewFormal { + scope: self.word()?, + parameter: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn shape(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 1 => { + let bits = self.word()?; + let signed = match self.word()? { + 0 => false, + 1 => true, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeShape::Integer { bits, signed } + } + 2 => NativeShape::Float { bits: self.word()? }, + 3 => NativeShape::Bool, + 4 => NativeShape::String, + 5 => NativeShape::Nothing, + 6 => NativeShape::Network, + 7 => NativeShape::Resource { kind: self.word()? }, + 8 => NativeShape::Optional { + child: self.word()?, + }, + 9 => NativeShape::Result { + ok: self.word()?, + fail: self.word()?, + }, + 10 => NativeShape::HookDescriptor { hook: self.word()? }, + 11 if version == 3 => NativeShape::Carrier { node: self.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn operation(&mut self, version: u32) -> Result { + Ok(match self.word()? { + 26 if version == 3 => NativeOperation::Carrier { + record: self.word()?, + }, + 1 => NativeOperation::Acquire { + frame: self.word()?, + hook: self.word()?, + destination: self.word()?, + }, + 2 => NativeOperation::Transfer { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 3 => NativeOperation::Borrow { + frame: self.word()?, + source: self.word()?, + lease_frame: self.word()?, + }, + 4 => NativeOperation::BoundedBorrowUse { + frame: self.word()?, + source: self.loan(version)?, + callee_frame: self.word()?, + }, + 5 => NativeOperation::EndBorrow { + frame: self.word()?, + borrow: self.word()?, + }, + 6 => NativeOperation::InvokeBorrow { + frame: self.word()?, + hook: self.word()?, + source: self.loan(version)?, + }, + 7 => NativeOperation::Close { + frame: self.word()?, + hook: self.word()?, + source: self.word()?, + }, + 8 => NativeOperation::Drop { + frame: self.word()?, + source: self.word()?, + }, + 9 => NativeOperation::SumAdopt { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 10 => NativeOperation::SumTake { + frame: self.word()?, + source: self.word()?, + destination: self.word()?, + }, + 11 => NativeOperation::SumDrop { + frame: self.word()?, + source: self.word()?, + }, + 12 => NativeOperation::Replace { + frame: self.word()?, + old: self.word()?, + replacement: self.word()?, + }, + 13 => NativeOperation::Complete { + frame: self.word()?, + }, + 14 => NativeOperation::Descriptor { hook: self.word()? }, + 15 => NativeOperation::InvokeDescriptor { + hook: self.word()?, + signature: self.word()?, + target: self.word()?, + }, + 16 => NativeOperation::InvokeSourceFunction { + frame: self.word()?, + callee: self.word()?, + signature: self.word()?, + callee_scope: self.word()?, + source: if matches!(version, 2 | 3) { + Some(self.source_invocation(version)?) + } else { + None + }, + }, + 17 if matches!(version, 2 | 3) => NativeOperation::TakeFailureCompanion { + frame: self.word()?, + source_sum_slot: self.word()?, + failure_shape: self.word()?, + }, + 18 if matches!(version, 2 | 3) => NativeOperation::PublishReturn { + frame: self.word()?, + source_return_slot: self.word()?, + }, + 19 if matches!(version, 2 | 3) => NativeOperation::CreateAbsentSum { + frame: self.word()?, + destination_slot: self.word()?, + }, + 20 if matches!(version, 2 | 3) => NativeOperation::CreateFailureSum { + frame: self.word()?, + destination_slot: self.word()?, + failure_shape: self.word()?, + }, + 21 if matches!(version, 2 | 3) => NativeOperation::BorrowSum { + frame: self.word()?, + source_sum_slot: self.word()?, + lease_frame: self.word()?, + }, + 22 if matches!(version, 2 | 3) => NativeOperation::ObserveSumView { + frame: self.word()?, + source: self.sum_loan(version)?, + }, + 23 if matches!(version, 2 | 3) => NativeOperation::ProjectSumView { + frame: self.word()?, + source: self.sum_loan(version)?, + path: match self.word()? { + 1 => NativePayloadStep::Some, + 2 => NativePayloadStep::Ok, + _ => return Err(ResourceLayoutError::UnknownTag), + }, + lease_frame: self.word()?, + }, + 24 if matches!(version, 2 | 3) => NativeOperation::ReadFailureCompanion { + frame: self.word()?, + source: self.sum_loan(version)?, + failure_shape: self.word()?, + }, + 25 if matches!(version, 2 | 3) => NativeOperation::EndSumBorrow { + frame: self.word()?, + borrow: self.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) + } + fn source_invocation( + &mut self, + version: u32, + ) -> Result { + let callee_return = match self.word()? { + 0 => None, + 1 => Some(self.word()?), + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let count = self.count()?; + let mut evaluation_order = storage(count)?; + for _ in 0..count { + evaluation_order.push(self.word()?); + } + let count = self.count()?; + let mut formals = storage(count)?; + for _ in 0..count { + let parameter = self.word()?; + let source_index = self.word()?; + let actual_shape = self.word()?; + let callee_shape = self.word()?; + let syntax = match self.word()? { + 1 => NativeSourceSyntax::Bare, + 2 => NativeSourceSyntax::WrittenView, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let effect = match self.word()? { + 1 => NativeSourceEffect::Copy, + 2 => NativeSourceEffect::TransferOwned, + 3 => NativeSourceEffect::RelinquishOwned, + 4 => NativeSourceEffect::RetainBorrow, + 5 => NativeSourceEffect::ObserveData, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let access = match self.word()? { + 1 => NativeAccess::Owned, + 2 => NativeAccess::View, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let value = match self.word()? { + 5 if version == 3 => NativeSourceValue::CarrierOwned { + caller_argument_slot: self.word()?, + callee_parameter_slot: self.word()?, + }, + 6 if version == 3 => NativeSourceValue::CarrierView { + source: carriers::loan(self)?, + }, + 1 => NativeSourceValue::Ordinary, + 2 => NativeSourceValue::Owned { + caller_argument_slot: self.word()?, + callee_parameter_slot: self.word()?, + }, + 3 => NativeSourceValue::ResidentView { + source: self.loan(2)?, + }, + 4 => NativeSourceValue::ResidentSumView { + source: self.sum_loan(version)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + formals.push(NativeSourceFormal { + parameter, + source_index, + actual_shape, + callee_shape, + syntax, + effect, + access, + value, + }); + } + let result = match self.word()? { + 3 if version == 3 => { + let shape = self.word()?; + let caller_destination_frame = self.word()?; + let caller_destination_slot = self.word()?; + let callee_return_frame = self.word()?; + let permitted_return_slots = { + let n = self.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(self.word()?); + } + v + }; + NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } + } + 1 => NativeSourceResult::Ordinary { + shape: self.word()?, + }, + 2 => { + let shape = self.word()?; + let caller_destination_frame = self.word()?; + let caller_destination_slot = self.word()?; + let callee_return_frame = self.word()?; + let count = self.count()?; + let mut permitted_return_slots = storage(count)?; + for _ in 0..count { + permitted_return_slots.push(self.word()?); + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } + } + _ => return Err(ResourceLayoutError::UnknownTag), + }; + Ok(NativeSourceInvocation { + callee_return, + evaluation_order, + formals, + result, + }) + } +} + +pub(super) fn storage(count: usize) -> Result, ResourceLayoutError> { + let mut values = Vec::new(); + values + .try_reserve_exact(count) + .map_err(|_| ResourceLayoutError::CapacityExhausted)?; + Ok(values) +} + +pub(super) fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_BYTES { + return Err(ResourceLayoutError::WireLimit); + } + if bytes.len() < HEADER_BYTES { + return Err(ResourceLayoutError::Truncated); + } + let mut input = Reader { bytes, position: 0 }; + if &input.take::<8>()? != MAGIC { + return Err(ResourceLayoutError::Header); + } + let version = input.word()?; + if ![1, 2, 3].contains(&version) { + return Err(ResourceLayoutError::Header); + } + if input.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + if u64::from_le_bytes(input.take()?) != bytes.len() as u64 { + return Err(ResourceLayoutError::Length); + } + let counts = [ + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + input.count()?, + ]; + if input.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + let mut kinds = storage(counts[0])?; + for i in 0..counts[0] { + input.ordinal(i)?; + kinds.push(i as u32); + } + let mut hooks = storage(counts[1])?; + for i in 0..counts[1] { + input.ordinal(i)?; + let kind = input.word()?; + let recipe = match input.word()? { + 1 => NativeRecipe::NetworkFactory, + 2 => NativeRecipe::NetworkBorrow, + 3 => NativeRecipe::Finalize, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + hooks.push(NativeHook { + ordinal: i as u32, + kind, + recipe, + signature: input.word()?, + }); + } + let mut signatures = storage(counts[2])?; + for i in 0..counts[2] { + input.ordinal(i)?; + let count = input.count()?; + let result = input.word()?; + let mut parameters = storage(count)?; + for _ in 0..count { + let shape = input.word()?; + let access = match input.word()? { + 1 => NativeAccess::Owned, + 2 => NativeAccess::View, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + parameters.push(NativeFormal { shape, access }); + } + signatures.push(NativeSignature { + ordinal: i as u32, + parameters, + result, + }); + } + let mut shapes = storage(counts[3])?; + for i in 0..counts[3] { + input.ordinal(i)?; + shapes.push(input.shape(version)?); + } + let mut frames = storage(counts[4])?; + for i in 0..counts[4] { + input.ordinal(i)?; + let role = match input.word()? { + 1 => NativeFrameRole::Scope, + 2 => NativeFrameRole::Operation, + 3 => NativeFrameRole::Return, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + let site = input.site()?; + let signature = input.word()?; + let count = input.count()?; + let mut parents = storage(count)?; + for _ in 0..count { + let tag = input.word()?; + let index = input.word()?; + parents.push(match (tag, index) { + (0, 0) => NativeParent::Root, + (0, _) => return Err(ResourceLayoutError::Reserved), + (1, index) => NativeParent::Frame(index), + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + frames.push(NativeFrame { + ordinal: i as u32, + role, + site, + signature, + parents, + }); + } + let mut slots = storage(counts[5])?; + for i in 0..counts[5] { + input.ordinal(i)?; + let frame = input.word()?; + let shape = input.word()?; + let count = input.count()?; + let mut path = storage(count)?; + for _ in 0..count { + path.push(match input.word()? { + 1 => NativePayloadStep::Some, + 2 => NativePayloadStep::Ok, + 3 => NativePayloadStep::Fail, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + slots.push(NativeSlot { + ordinal: i as u32, + frame, + shape, + path, + }); + } + let mut operations = storage(counts[6])?; + for i in 0..counts[6] { + input.ordinal(i)?; + let site = input.site()?; + operations.push(NativeOperationRecord { + ordinal: i as u32, + site, + operation: input.operation(version)?, + }); + } + let carriers = if version == 3 { + carriers::decode(&mut input)? + } else { + NativeCarrierLayout::default() + }; + if input.position != bytes.len() { + return Err(ResourceLayoutError::Trailing); + } + Ok(WireLayout { + carriers, + version, + kinds, + hooks, + signatures, + shapes, + frames, + slots, + operations, + }) +} + +// Test encoder pins the same explicit tags/order. It intentionally admits mutated +// records so negative tests exercise production decoding/validation, not a second issuer. +#[cfg(test)] +pub(super) fn encode_records(layout: &WireLayout) -> Vec { + struct Writer(Vec); + impl Writer { + fn word(&mut self, value: u32) { + self.0.extend_from_slice(&value.to_le_bytes()); + } + fn site(&mut self, site: NativeSite) { + self.word(site.function); + self.word(site.block); + match site.position { + NativePosition::Statement(i) => { + self.word(0); + self.word(i); + } + NativePosition::Terminator => { + self.word(1); + self.word(0); + } + } + } + fn loan(&mut self, source: NativeLoanSource) { + match source { + NativeLoanSource::ProjectedSumPayload { operation } => { + self.word(3); + self.word(operation); + } + NativeLoanSource::ExistingBorrow { operation } => { + self.word(1); + self.word(operation); + } + NativeLoanSource::IncomingViewFormal { scope, parameter } => { + self.word(2); + self.word(scope); + self.word(parameter); + } + } + } + fn sum_loan(&mut self, source: NativeSumLoanSource) { + match source { + NativeSumLoanSource::CarrierProjected { operation } => { + self.word(3); + self.word(operation); + } + NativeSumLoanSource::ExistingBorrow { operation } => { + self.word(1); + self.word(operation); + } + NativeSumLoanSource::IncomingViewFormal { scope, parameter } => { + self.word(2); + self.word(scope); + self.word(parameter); + } + } + } + fn operation(&mut self, operation: &NativeOperation) { + use NativeOperation::*; + match *operation { + NativeOperation::Carrier { record } => { + self.word(26); + self.word(record); + } + Acquire { + frame, + hook, + destination, + } => { + self.word(1); + self.word(frame); + self.word(hook); + self.word(destination); + } + Transfer { + frame, + source, + destination, + } => { + self.word(2); + self.word(frame); + self.word(source); + self.word(destination); + } + Borrow { + frame, + source, + lease_frame, + } => { + self.word(3); + self.word(frame); + self.word(source); + self.word(lease_frame); + } + BoundedBorrowUse { + frame, + source, + callee_frame, + } => { + self.word(4); + self.word(frame); + self.loan(source); + self.word(callee_frame); + } + EndBorrow { frame, borrow } => { + self.word(5); + self.word(frame); + self.word(borrow); + } + InvokeBorrow { + frame, + hook, + source, + } => { + self.word(6); + self.word(frame); + self.word(hook); + self.loan(source); + } + Close { + frame, + hook, + source, + } => { + self.word(7); + self.word(frame); + self.word(hook); + self.word(source); + } + Drop { frame, source } => { + self.word(8); + self.word(frame); + self.word(source); + } + SumAdopt { + frame, + source, + destination, + } => { + self.word(9); + self.word(frame); + self.word(source); + self.word(destination); + } + SumTake { + frame, + source, + destination, + } => { + self.word(10); + self.word(frame); + self.word(source); + self.word(destination); + } + SumDrop { frame, source } => { + self.word(11); + self.word(frame); + self.word(source); + } + Replace { + frame, + old, + replacement, + } => { + self.word(12); + self.word(frame); + self.word(old); + self.word(replacement); + } + Complete { frame } => { + self.word(13); + self.word(frame); + } + Descriptor { hook } => { + self.word(14); + self.word(hook); + } + InvokeDescriptor { + hook, + signature, + target, + } => { + self.word(15); + self.word(hook); + self.word(signature); + self.word(target); + } + InvokeSourceFunction { + frame, + callee, + signature, + callee_scope, + ref source, + } => { + self.word(16); + self.word(frame); + self.word(callee); + self.word(signature); + self.word(callee_scope); + if let Some(source) = source { + self.source_invocation(source); + } + } + TakeFailureCompanion { + frame, + source_sum_slot, + failure_shape, + } => { + self.word(17); + self.word(frame); + self.word(source_sum_slot); + self.word(failure_shape); + } + PublishReturn { + frame, + source_return_slot, + } => { + self.word(18); + self.word(frame); + self.word(source_return_slot); + } + CreateAbsentSum { + frame, + destination_slot, + } => { + self.word(19); + self.word(frame); + self.word(destination_slot); + } + CreateFailureSum { + frame, + destination_slot, + failure_shape, + } => { + self.word(20); + self.word(frame); + self.word(destination_slot); + self.word(failure_shape); + } + BorrowSum { + frame, + source_sum_slot, + lease_frame, + } => { + self.word(21); + self.word(frame); + self.word(source_sum_slot); + self.word(lease_frame); + } + ObserveSumView { frame, source } => { + self.word(22); + self.word(frame); + self.sum_loan(source); + } + ProjectSumView { + frame, + source, + path, + lease_frame, + } => { + self.word(23); + self.word(frame); + self.sum_loan(source); + self.word(match path { + NativePayloadStep::Some => 1, + NativePayloadStep::Ok => 2, + NativePayloadStep::Fail => 3, + }); + self.word(lease_frame); + } + ReadFailureCompanion { + frame, + source, + failure_shape, + } => { + self.word(24); + self.word(frame); + self.sum_loan(source); + self.word(failure_shape); + } + EndSumBorrow { frame, borrow } => { + self.word(25); + self.word(frame); + self.word(borrow); + } + } + } + fn source_invocation(&mut self, source: &NativeSourceInvocation) { + match source.callee_return { + None => self.word(0), + Some(frame) => { + self.word(1); + self.word(frame); + } + } + self.word(source.evaluation_order.len() as u32); + for parameter in &source.evaluation_order { + self.word(*parameter); + } + self.word(source.formals.len() as u32); + for formal in &source.formals { + self.word(formal.parameter); + self.word(formal.source_index); + self.word(formal.actual_shape); + self.word(formal.callee_shape); + self.word(match formal.syntax { + NativeSourceSyntax::Bare => 1, + NativeSourceSyntax::WrittenView => 2, + }); + self.word(match formal.effect { + NativeSourceEffect::Copy => 1, + NativeSourceEffect::TransferOwned => 2, + NativeSourceEffect::RelinquishOwned => 3, + NativeSourceEffect::RetainBorrow => 4, + NativeSourceEffect::ObserveData => 5, + }); + self.word(match formal.access { + NativeAccess::Owned => 1, + NativeAccess::View => 2, + }); + match formal.value { + NativeSourceValue::CarrierOwned { + caller_argument_slot, + callee_parameter_slot, + } => { + self.word(5); + self.word(caller_argument_slot); + self.word(callee_parameter_slot); + } + NativeSourceValue::CarrierView { source } => { + self.word(6); + carriers::loan_out(&mut self.0, source); + } + NativeSourceValue::Ordinary => self.word(1), + NativeSourceValue::Owned { + caller_argument_slot, + callee_parameter_slot, + } => { + self.word(2); + self.word(caller_argument_slot); + self.word(callee_parameter_slot); + } + NativeSourceValue::ResidentView { source } => { + self.word(3); + self.loan(source); + } + NativeSourceValue::ResidentSumView { source } => { + self.word(4); + self.sum_loan(source); + } + } + } + match &source.result { + NativeSourceResult::Carrier { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + self.word(3); + self.word(*shape); + self.word(*caller_destination_frame); + self.word(*caller_destination_slot); + self.word(*callee_return_frame); + self.word(permitted_return_slots.len() as u32); + for slot in permitted_return_slots { + self.word(*slot); + } + } + NativeSourceResult::Ordinary { shape } => { + self.word(1); + self.word(*shape); + } + NativeSourceResult::Owned { + shape, + caller_destination_frame, + caller_destination_slot, + callee_return_frame, + permitted_return_slots, + } => { + self.word(2); + self.word(*shape); + self.word(*caller_destination_frame); + self.word(*caller_destination_slot); + self.word(*callee_return_frame); + self.word(permitted_return_slots.len() as u32); + for slot in permitted_return_slots { + self.word(*slot); + } + } + } + } + } + let mut out = Writer(Vec::new()); + out.0.extend_from_slice(MAGIC); + out.word(layout.version); + out.word(0); + out.0.extend_from_slice(&0u64.to_le_bytes()); + for count in [ + layout.kinds.len(), + layout.hooks.len(), + layout.signatures.len(), + layout.shapes.len(), + layout.frames.len(), + layout.slots.len(), + layout.operations.len(), + ] { + out.word(count as u32); + } + out.word(0); + for kind in &layout.kinds { + out.word(*kind); + } + for hook in &layout.hooks { + out.word(hook.ordinal); + out.word(hook.kind); + out.word(match hook.recipe { + NativeRecipe::NetworkFactory => 1, + NativeRecipe::NetworkBorrow => 2, + NativeRecipe::Finalize => 3, + }); + out.word(hook.signature); + } + for signature in &layout.signatures { + out.word(signature.ordinal); + out.word(signature.parameters.len() as u32); + out.word(signature.result); + for parameter in &signature.parameters { + out.word(parameter.shape); + out.word(match parameter.access { + NativeAccess::Owned => 1, + NativeAccess::View => 2, + }); + } + } + for (i, shape) in layout.shapes.iter().enumerate() { + out.word(i as u32); + match *shape { + NativeShape::Integer { bits, signed } => { + out.word(1); + out.word(bits); + out.word(u32::from(signed)); + } + NativeShape::Float { bits } => { + out.word(2); + out.word(bits); + } + NativeShape::Bool => out.word(3), + NativeShape::String => out.word(4), + NativeShape::Nothing => out.word(5), + NativeShape::Network => out.word(6), + NativeShape::Resource { kind } => { + out.word(7); + out.word(kind); + } + NativeShape::Optional { child } => { + out.word(8); + out.word(child); + } + NativeShape::Result { ok, fail } => { + out.word(9); + out.word(ok); + out.word(fail); + } + NativeShape::Carrier { node } => { + out.word(11); + out.word(node); + } + NativeShape::HookDescriptor { hook } => { + out.word(10); + out.word(hook); + } + } + } + for frame in &layout.frames { + out.word(frame.ordinal); + out.word(match frame.role { + NativeFrameRole::Scope => 1, + NativeFrameRole::Operation => 2, + NativeFrameRole::Return => 3, + }); + out.site(frame.site); + out.word(frame.signature); + out.word(frame.parents.len() as u32); + for parent in &frame.parents { + match *parent { + NativeParent::Root => { + out.word(0); + out.word(0); + } + NativeParent::Frame(index) => { + out.word(1); + out.word(index); + } + } + } + } + for slot in &layout.slots { + out.word(slot.ordinal); + out.word(slot.frame); + out.word(slot.shape); + out.word(slot.path.len() as u32); + for step in &slot.path { + out.word(match step { + NativePayloadStep::Some => 1, + NativePayloadStep::Ok => 2, + NativePayloadStep::Fail => 3, + }); + } + } + for operation in &layout.operations { + out.word(operation.ordinal); + out.site(operation.site); + out.operation(&operation.operation); + } + if layout.version == 3 { + out.0.extend_from_slice(&carriers::encode(&layout.carriers)); + } + let length = out.0.len() as u64; + out.0[16..24].copy_from_slice(&length.to_le_bytes()); + out.0 +} diff --git a/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs b/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs new file mode 100644 index 00000000..0fea1fe4 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/registration/wire/carriers.rs @@ -0,0 +1,670 @@ +//! Explicit v3 suffix codec; old records are never reinterpreted. +use super::super::carriers::*; +use super::*; +const MAGIC: &[u8; 8] = b"JTCAR003"; +fn blob(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let end = r + .position + .checked_add(n) + .ok_or(ResourceLayoutError::WireLimit)?; + let bytes = r + .bytes + .get(r.position..end) + .ok_or(ResourceLayoutError::Truncated)?; + let mut out = storage(n)?; + out.extend_from_slice(bytes); + r.position = end; + Ok(out) +} +fn ids(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(r.word()?); + } + Ok(v) +} +fn fields(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(NativeCarrierField { + name: blob(r)?, + node: r.word()?, + }); + } + Ok(v) +} +fn members(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(NativeCarrierMember { + name: blob(r)?, + discriminant: i64::from_le_bytes(r.take()?), + fields: fields(r)?, + }); + } + Ok(v) +} +pub(super) fn loan(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 1 => NativeCarrierLoanSource::ExistingBorrow { + operation: r.word()?, + }, + 2 => NativeCarrierLoanSource::IncomingViewFormal { + scope: r.word()?, + parameter: r.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn source(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 1 => NativeCarrierSource::Slot(r.word()?), + 2 => NativeCarrierSource::Loan(loan(r)?), + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn selector(r: &mut Reader<'_>) -> Result { + Ok(match r.word()? { + 0 => NativeCarrierSelector::Static(r.word()?), + 1 => NativeCarrierSelector::Dynamic, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +fn path(r: &mut Reader<'_>) -> Result, ResourceLayoutError> { + let n = r.count()?; + let mut v = storage(n)?; + for _ in 0..n { + v.push(match r.word()? { + 1 => NativeCarrierPath::Field(r.word()?), + 2 => NativeCarrierPath::EnumPayload { + variant: r.word()?, + field: r.word()?, + }, + 3 => NativeCarrierPath::MachinePayload { + state: r.word()?, + field: r.word()?, + }, + 4 => NativeCarrierPath::List(selector(r)?), + 5 => NativeCarrierPath::MapKey(selector(r)?), + 6 => NativeCarrierPath::MapValue(selector(r)?), + 7 => NativeCarrierPath::Some, + 8 => NativeCarrierPath::Ok, + 9 => NativeCarrierPath::Fail, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + Ok(v) +} +fn flag(r: &mut Reader<'_>) -> Result { + match r.word()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ResourceLayoutError::Reserved), + } +} +fn operation(r: &mut Reader<'_>) -> Result { + let tag = r.word()?; + let frame = r.word()?; + Ok(match tag { + 1 => { + let destination = r.word()?; + let selector = r.word()?; + let n = r.count()?; + let mut children = storage(n)?; + for _ in 0..n { + let index = r.word()?; + let node = r.word()?; + let source = match r.word()? { + 1 => NativeCarrierChildSource::Ordinary { shape: r.word()? }, + 2 => NativeCarrierChildSource::Move { slot: r.word()? }, + 3 => NativeCarrierChildSource::LeafSum { slot: r.word()? }, + 4 => NativeCarrierChildSource::LeafOwner { slot: r.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + children.push(NativeCarrierChild { + index, + node, + source, + }); + } + NativeCarrierOperation::Construct { + frame, + destination, + selector, + children, + } + } + 2 => NativeCarrierOperation::Transfer { + frame, + source: r.word()?, + destination: r.word()?, + }, + 3 => NativeCarrierOperation::Borrow { + frame, + source: source(r)?, + path: path(r)?, + node: r.word()?, + lease_frame: r.word()?, + }, + 4 => NativeCarrierOperation::EndBorrow { + frame, + borrow: r.word()?, + }, + 5 => { + let source = source(r)?; + let path = path(r)?; + let observation = match r.word()? { + 1 => NativeCarrierObservation::Length, + 2 => NativeCarrierObservation::Tag, + 3 => NativeCarrierObservation::State, + 4 => NativeCarrierObservation::Ordinary { + shape: r.word()?, + clone: flag(r)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeCarrierOperation::Observe { + frame, + source, + path, + observation, + } + } + 6 => { + let source = r.word()?; + let path = path(r)?; + let destination = match r.word()? { + 1 => NativeCarrierDestination::Slot(r.word()?), + 2 => NativeCarrierDestination::Ordinary { shape: r.word()? }, + _ => return Err(ResourceLayoutError::UnknownTag), + }; + NativeCarrierOperation::Extract { + frame, + source, + path, + destination, + } + } + 7 => NativeCarrierOperation::AdaptSum { + frame, + source: source(r)?, + path: path(r)?, + sum_slot: r.word()?, + lease_frame: r.word()?, + borrowed: flag(r)?, + }, + 8 => NativeCarrierOperation::Retire { + frame, + source: r.word()?, + }, + 9 => NativeCarrierOperation::PublishReturn { + frame, + source: r.word()?, + }, + 10 => NativeCarrierOperation::ResetIteration { + frame, + source: r.word()?, + carrier_binders: ids(r)?, + leaf_sum_binders: ids(r)?, + }, + 11 => NativeCarrierOperation::QualifyMachine { + frame, + source: r.word()?, + destination: r.word()?, + machine: r.word()?, + state: r.word()?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }) +} +pub(super) fn decode(r: &mut Reader<'_>) -> Result { + if &r.take::<8>()? != MAGIC { + return Err(ResourceLayoutError::Header); + } + let ns = r.count()?; + let ss = r.count()?; + let os = r.count()?; + if r.word()? != 0 { + return Err(ResourceLayoutError::Reserved); + } + let mut nodes = storage(ns)?; + for i in 0..ns { + r.ordinal(i)?; + nodes.push(match r.word()? { + 1 => NativeCarrierNode::Ordinary { shape: r.word()? }, + 2 => NativeCarrierNode::Resource { kind: r.word()? }, + 3 => NativeCarrierNode::Optional { child: r.word()? }, + 4 => NativeCarrierNode::Result { + ok: r.word()?, + fail: r.word()?, + }, + 5 => NativeCarrierNode::List { element: r.word()? }, + 6 => NativeCarrierNode::Map { + key: r.word()?, + value: r.word()?, + }, + 7 => NativeCarrierNode::Struct { + identity: blob(r)?, + arguments: ids(r)?, + fields: fields(r)?, + }, + 8 => NativeCarrierNode::Enum { + identity: blob(r)?, + arguments: ids(r)?, + variants: members(r)?, + }, + 9 => { + let identity = blob(r)?; + let states = members(r)?; + let n = r.count()?; + let mut transitions = storage(n)?; + for _ in 0..n { + transitions.push((r.word()?, r.word()?)); + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } + } + 10 => NativeCarrierNode::MachineState { + machine: r.word()?, + state: r.word()?, + }, + 11 => NativeCarrierNode::Refinement { + identity: blob(r)?, + base: r.word()?, + predicate: blob(r)?, + }, + _ => return Err(ResourceLayoutError::UnknownTag), + }); + } + let mut slots = storage(ss)?; + for i in 0..ss { + r.ordinal(i)?; + slots.push(NativeCarrierSlot { + frame: r.word()?, + node: r.word()?, + }); + } + let mut operations = storage(os)?; + for i in 0..os { + r.ordinal(i)?; + operations.push(NativeCarrierOperationRecord { + site: r.site()?, + operation: operation(r)?, + }); + } + Ok(NativeCarrierLayout { + nodes, + slots, + operations, + }) +} +#[cfg(test)] +fn word(v: &mut Vec, n: u32) { + v.extend_from_slice(&n.to_le_bytes()); +} +#[cfg(test)] +fn blob_out(v: &mut Vec, b: &[u8]) { + word(v, b.len() as u32); + v.extend_from_slice(b); +} +#[cfg(test)] +fn ids_out(v: &mut Vec, ids: &[u32]) { + word(v, ids.len() as u32); + for n in ids { + word(v, *n); + } +} +#[cfg(test)] +fn fields_out(v: &mut Vec, fs: &[NativeCarrierField]) { + word(v, fs.len() as u32); + for f in fs { + blob_out(v, &f.name); + word(v, f.node); + } +} +#[cfg(test)] +fn members_out(v: &mut Vec, ms: &[NativeCarrierMember]) { + word(v, ms.len() as u32); + for m in ms { + blob_out(v, &m.name); + v.extend_from_slice(&m.discriminant.to_le_bytes()); + fields_out(v, &m.fields); + } +} +#[cfg(test)] +pub(super) fn loan_out(v: &mut Vec, s: NativeCarrierLoanSource) { + match s { + NativeCarrierLoanSource::ExistingBorrow { operation } => { + word(v, 1); + word(v, operation); + } + NativeCarrierLoanSource::IncomingViewFormal { scope, parameter } => { + word(v, 2); + word(v, scope); + word(v, parameter); + } + } +} +#[cfg(test)] +fn source_out(v: &mut Vec, s: NativeCarrierSource) { + match s { + NativeCarrierSource::Slot(slot) => { + word(v, 1); + word(v, slot); + } + NativeCarrierSource::Loan(s) => { + word(v, 2); + loan_out(v, s); + } + } +} +#[cfg(test)] +fn selector_out(v: &mut Vec, s: NativeCarrierSelector) { + match s { + NativeCarrierSelector::Static(i) => { + word(v, 0); + word(v, i); + } + NativeCarrierSelector::Dynamic => word(v, 1), + } +} +#[cfg(test)] +fn path_out(v: &mut Vec, ps: &[NativeCarrierPath]) { + word(v, ps.len() as u32); + for p in ps { + match *p { + NativeCarrierPath::Field(i) => { + word(v, 1); + word(v, i); + } + NativeCarrierPath::EnumPayload { variant, field } => { + word(v, 2); + word(v, variant); + word(v, field); + } + NativeCarrierPath::MachinePayload { state, field } => { + word(v, 3); + word(v, state); + word(v, field); + } + NativeCarrierPath::List(s) => { + word(v, 4); + selector_out(v, s); + } + NativeCarrierPath::MapKey(s) => { + word(v, 5); + selector_out(v, s); + } + NativeCarrierPath::MapValue(s) => { + word(v, 6); + selector_out(v, s); + } + NativeCarrierPath::Some => word(v, 7), + NativeCarrierPath::Ok => word(v, 8), + NativeCarrierPath::Fail => word(v, 9), + } + } +} +#[cfg(test)] +pub(super) fn encode(g: &NativeCarrierLayout) -> Vec { + let mut v = MAGIC.to_vec(); + for n in [g.nodes.len(), g.slots.len(), g.operations.len()] { + word(&mut v, n as u32); + } + word(&mut v, 0); + for (i, n) in g.nodes.iter().enumerate() { + word(&mut v, i as u32); + match n { + NativeCarrierNode::Ordinary { shape } => { + word(&mut v, 1); + word(&mut v, *shape); + } + NativeCarrierNode::Resource { kind } => { + word(&mut v, 2); + word(&mut v, *kind); + } + NativeCarrierNode::Optional { child } => { + word(&mut v, 3); + word(&mut v, *child); + } + NativeCarrierNode::Result { ok, fail } => { + word(&mut v, 4); + word(&mut v, *ok); + word(&mut v, *fail); + } + NativeCarrierNode::List { element } => { + word(&mut v, 5); + word(&mut v, *element); + } + NativeCarrierNode::Map { key, value } => { + word(&mut v, 6); + word(&mut v, *key); + word(&mut v, *value); + } + NativeCarrierNode::Struct { + identity, + arguments, + fields, + } => { + word(&mut v, 7); + blob_out(&mut v, identity); + ids_out(&mut v, arguments); + fields_out(&mut v, fields); + } + NativeCarrierNode::Enum { + identity, + arguments, + variants, + } => { + word(&mut v, 8); + blob_out(&mut v, identity); + ids_out(&mut v, arguments); + members_out(&mut v, variants); + } + NativeCarrierNode::Machine { + identity, + states, + transitions, + } => { + word(&mut v, 9); + blob_out(&mut v, identity); + members_out(&mut v, states); + word(&mut v, transitions.len() as u32); + for (a, b) in transitions { + word(&mut v, *a); + word(&mut v, *b); + } + } + NativeCarrierNode::MachineState { machine, state } => { + word(&mut v, 10); + word(&mut v, *machine); + word(&mut v, *state); + } + NativeCarrierNode::Refinement { + identity, + base, + predicate, + } => { + word(&mut v, 11); + blob_out(&mut v, identity); + word(&mut v, *base); + blob_out(&mut v, predicate); + } + } + } + for (i, s) in g.slots.iter().enumerate() { + word(&mut v, i as u32); + word(&mut v, s.frame); + word(&mut v, s.node); + } + for (i, row) in g.operations.iter().enumerate() { + word(&mut v, i as u32); + word(&mut v, row.site.function); + word(&mut v, row.site.block); + match row.site.position { + NativePosition::Statement(i) => { + word(&mut v, 0); + word(&mut v, i); + } + NativePosition::Terminator => { + word(&mut v, 1); + word(&mut v, 0); + } + } + let op = &row.operation; + let tag = match op { + NativeCarrierOperation::Construct { .. } => 1, + NativeCarrierOperation::Transfer { .. } => 2, + NativeCarrierOperation::Borrow { .. } => 3, + NativeCarrierOperation::EndBorrow { .. } => 4, + NativeCarrierOperation::Observe { .. } => 5, + NativeCarrierOperation::Extract { .. } => 6, + NativeCarrierOperation::AdaptSum { .. } => 7, + NativeCarrierOperation::Retire { .. } => 8, + NativeCarrierOperation::PublishReturn { .. } => 9, + NativeCarrierOperation::ResetIteration { .. } => 10, + NativeCarrierOperation::QualifyMachine { .. } => 11, + }; + word(&mut v, tag); + word(&mut v, op.frame()); + match op { + NativeCarrierOperation::Construct { + destination, + selector, + children, + .. + } => { + word(&mut v, *destination); + word(&mut v, *selector); + word(&mut v, children.len() as u32); + for c in children { + word(&mut v, c.index); + word(&mut v, c.node); + match c.source { + NativeCarrierChildSource::Ordinary { shape } => { + word(&mut v, 1); + word(&mut v, shape); + } + NativeCarrierChildSource::Move { slot } => { + word(&mut v, 2); + word(&mut v, slot); + } + NativeCarrierChildSource::LeafOwner { slot } => { + word(&mut v, 4); + word(&mut v, slot); + } + NativeCarrierChildSource::LeafSum { slot } => { + word(&mut v, 3); + word(&mut v, slot); + } + } + } + } + NativeCarrierOperation::Transfer { + source, + destination, + .. + } => { + word(&mut v, *source); + word(&mut v, *destination); + } + NativeCarrierOperation::Borrow { + source, + path, + node, + lease_frame, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + word(&mut v, *node); + word(&mut v, *lease_frame); + } + NativeCarrierOperation::EndBorrow { borrow, .. } => word(&mut v, *borrow), + NativeCarrierOperation::Observe { + source, + path, + observation, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + match observation { + NativeCarrierObservation::Length => word(&mut v, 1), + NativeCarrierObservation::Tag => word(&mut v, 2), + NativeCarrierObservation::State => word(&mut v, 3), + NativeCarrierObservation::Ordinary { shape, clone } => { + word(&mut v, 4); + word(&mut v, *shape); + word(&mut v, u32::from(*clone)); + } + } + } + NativeCarrierOperation::Extract { + source, + path, + destination, + .. + } => { + word(&mut v, *source); + path_out(&mut v, path); + match destination { + NativeCarrierDestination::Slot(s) => { + word(&mut v, 1); + word(&mut v, *s); + } + NativeCarrierDestination::Ordinary { shape } => { + word(&mut v, 2); + word(&mut v, *shape); + } + } + } + NativeCarrierOperation::AdaptSum { + source, + path, + sum_slot, + lease_frame, + borrowed, + .. + } => { + source_out(&mut v, *source); + path_out(&mut v, path); + word(&mut v, *sum_slot); + word(&mut v, *lease_frame); + word(&mut v, u32::from(*borrowed)); + } + NativeCarrierOperation::Retire { source, .. } + | NativeCarrierOperation::PublishReturn { source, .. } => word(&mut v, *source), + NativeCarrierOperation::ResetIteration { + source, + carrier_binders, + leaf_sum_binders, + .. + } => { + word(&mut v, *source); + ids_out(&mut v, carrier_binders); + ids_out(&mut v, leaf_sum_binders); + } + NativeCarrierOperation::QualifyMachine { + source, + destination, + machine, + state, + .. + } => { + word(&mut v, *source); + word(&mut v, *destination); + word(&mut v, *machine); + word(&mut v, *state); + } + } + } + v +} diff --git a/crates/jett_runtime/src/resource_custody/tests.rs b/crates/jett_runtime/src/resource_custody/tests.rs new file mode 100644 index 00000000..d5b6b6cd --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/tests.rs @@ -0,0 +1,830 @@ +use super::*; +use std::collections::HashSet; +use std::sync::Mutex; + +type Events = Arc>>; + +// Constructor-owned test registration only. No production issuer or provider. +fn registration( + registry: &ResourceRegistry, + kinds: Vec, + slots: Vec, +) -> Result { + let mut nominal = HashSet::new(); + if !kinds.iter().all(|kind| nominal.insert(*kind)) + || slots.iter().any(|kind| *kind >= kinds.len()) + { + return Err(CustodyError::WrongKind); + } + Ok(RegisteredResourceProgram(Arc::new(ProgramLayout { + context: registry.context_id, + kinds, + slots, + }))) +} + +fn test_program(registry: &ResourceRegistry) -> RegisteredResourceProgram { + registration( + registry, + vec![ResourceTypeId::new(100), ResourceTypeId::new(101)], + vec![0, 0, 1, 0], + ) + .unwrap() +} + +fn authority() -> AuthorityProvenance { + AuthorityProvenance::new(5, 7) +} + +fn destination( + core: &ResourceCustody, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, +) -> ResourceHolderToken { + core.holder(frame, &program.slot(slot).unwrap()).unwrap() +} + +fn acquire( + core: &mut ResourceCustody, + registry: &mut ResourceRegistry, + program: &RegisteredResourceProgram, + frame: &ResourceFrameToken, + slot: usize, + label: i64, + events: &Events, + panics: bool, +) -> OwnedResourceToken { + let holder = destination(core, program, frame, slot); + let kind = program.kind(program.0.slots[slot]).unwrap(); + let prepared = core.prepare_acquisition(&kind, &holder).unwrap(); + let events = events.clone(); + core.commit_acquisition(registry, prepared, authority(), label, move |value| { + events.lock().unwrap().push(value); + if panics { + panic!("selected custody finalizer"); + } + }) + .unwrap() +} + +fn clean(core: &ResourceCustody, registry: &ResourceRegistry) { + assert_eq!( + ( + core.live_owners(), + core.live_loans(), + core.active_frames(), + registry.live_count() + ), + (0, 0, 0, 0) + ); +} + +#[test] +fn custody_registration_joins_context_program_kind_and_slot_before_acquisition() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let foreign_registry = ResourceRegistry::new(); + assert!(matches!( + ResourceCustody::new(program.clone(), &foreign_registry), + Err(CustodyError::WrongContext) + )); + assert!(matches!( + registration(®istry, vec![ResourceTypeId::new(100); 2], vec![0]), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + registration(®istry, vec![ResourceTypeId::new(100)], vec![1]), + Err(CustodyError::WrongKind) + )); + let other = registration(®istry, program.0.kinds.clone(), program.0.slots.clone()).unwrap(); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + assert!(matches!( + core.prepare_acquisition(&other.kind(0).unwrap(), &holder), + Err(CustodyError::WrongProgram) + )); + assert!(matches!( + core.holder(&frame, &other.slot(0).unwrap()), + Err(CustodyError::WrongProgram) + )); + assert!(matches!( + core.prepare_acquisition(&program.kind(1).unwrap(), &holder), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + program.kind(usize::MAX), + Err(CustodyError::WrongKind) + )); + assert!(matches!( + program.slot(usize::MAX), + Err(CustodyError::UnknownSlot) + )); + assert_eq!((core.live_owners(), registry.live_count()), (0, 0)); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); +} + +#[test] +fn custody_registry_nominal_key_generation_and_provenance_remain_exact() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let key = core.validate_owned(&owner, ®istry).unwrap(); + let other_registry = ResourceRegistry::new(); + assert_eq!( + core.validate_owned(&owner, &other_registry), + Err(CustodyError::WrongContext) + ); + core.owners[owner.owner].key.resource_type = ResourceTypeId::new(101); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::Registry(RegistryError::WrongType)) + ); + core.owners[owner.owner].key = key; + core.owners[owner.owner].authority = AuthorityProvenance::new(5, 99); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::AuthorityMismatch + ))) + ); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert!(events.lock().unwrap().is_empty()); + core.owners[owner.owner].authority = authority(); + core.owners[owner.owner].key.generation += 1; + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::Registry(RegistryError::StaleGeneration)) + ); + core.owners[owner.owner].key = key; + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidOwner)) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [1]); + clean(&core, ®istry); +} + +#[test] +fn custody_returned_holder_generation_controls_reverse_current_acquisition() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let outer = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let original = destination(&core, &program, &outer, 0); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &outer, + 0, + 1, + &events, + false, + ); + let stale = OwnedResourceToken { + core: owner.core, + owner: owner.owner, + holder_generation: owner.holder_generation, + }; + let _second = acquire( + &mut core, + &mut registry, + &program, + &outer, + 1, + 2, + &events, + false, + ); + let inner = core + .begin_frame(ResourceFrameKind::Return, ResourcePurpose::Runtime) + .unwrap(); + let returned = destination(&core, &program, &inner, 0); + core.transfer(&mut owner, &returned, ®istry).unwrap(); + assert_eq!( + core.validate_owned(&stale, ®istry), + Err(CustodyError::StaleHolderGeneration) + ); + core.transfer(&mut owner, &original, ®istry).unwrap(); + assert_eq!(core.end_frame(&inner, &mut registry).failure(), None); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.end_frame(&outer, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [1, 2]); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::InvalidOwner) + ); + clean(&core, ®istry); +} + +#[test] +fn custody_live_lease_blocks_move_close_and_older_frame_borrow_before_effect() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let scope = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 3, + &events, + false, + ); + assert!(matches!( + core.begin_borrow(&owner, &scope, ®istry), + Err(CustodyError::InvalidLoan) + )); + let mut loan = core.begin_borrow(&owner, &operation, ®istry).unwrap(); + let destination = destination(&core, &program, &scope, 0); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::ActiveBorrow) + ); + assert_eq!( + core.close(&mut owner, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::ActiveBorrow)) + ); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + core.validate_owned(&owner, ®istry) + ); + assert!(events.lock().unwrap().is_empty()); + core.end_borrow(&mut loan).unwrap(); + assert_eq!(core.end_borrow(&mut loan), Err(CustodyError::InvalidLoan)); + core.transfer(&mut owner, &destination, ®istry).unwrap(); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + Err(CustodyError::InvalidLoan) + ); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!(core.end_frame(&scope, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [3]); + clean(&core, ®istry); +} + +#[test] +fn custody_chronological_scope_operation_unwind_releases_callee_loan_first() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let caller = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let _caller_owner = acquire( + &mut core, + &mut registry, + &program, + &caller, + 0, + 1, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let operation_owner = acquire( + &mut core, + &mut registry, + &program, + &operation, + 0, + 2, + &events, + false, + ); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let loan = core + .begin_borrow(&operation_owner, &callee, ®istry) + .unwrap(); + let _callee_owner = acquire( + &mut core, + &mut registry, + &program, + &callee, + 0, + 3, + &events, + true, + ); + assert_eq!( + core.end_frame(&operation, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody( + CustodyError::OutOfOrderFrame + )) + ); + assert!(events.lock().unwrap().is_empty()); + let body = ResourceBodyOutcome::<(), &str>::HostPanic; + assert_eq!( + complete_resource(body, core.unwind_all(&mut registry)), + Err(ResourceCompletionFailure::Cleanup( + ResourceCleanupFailure::FinalizerPanic + )) + ); + assert_eq!(*events.lock().unwrap(), [3, 2, 1]); + assert_eq!( + core.validate_borrowed(&loan, ®istry), + Err(CustodyError::InvalidLoan) + ); + clean(&core, ®istry); +} + +#[test] +fn custody_required_purpose_cannot_be_overridden_by_nested_runtime_frame() { + for purpose in [ + ResourcePurpose::NamespaceConstant, + ResourcePurpose::ExplicitComptime, + ResourcePurpose::Verify, + ResourcePurpose::Property, + ] { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core.begin_frame(ResourceFrameKind::Scope, purpose).unwrap(); + let holder = destination(&core, &program, &frame, 0); + assert!(matches!( + core.prepare_acquisition(&program.kind(0).unwrap(), &holder), + Err(CustodyError::WrongPurpose) + )); + assert!(matches!( + core.begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime), + Err(CustodyError::WrongPurpose) + )); + assert_eq!( + ( + core.live_owners(), + registry.live_count(), + core.active_frames() + ), + (0, 0, 1) + ); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); + } +} + +#[test] +fn custody_failed_prepared_publication_cleans_only_the_unpublished_payload() { + for panics in [false, true] { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + let preparation = core + .prepare_acquisition(&program.kind(0).unwrap(), &holder) + .unwrap(); + let _original = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let observed = events.clone(); + let failure = core + .commit_acquisition( + &mut registry, + preparation, + authority(), + 99_i64, + move |payload| { + observed.lock().unwrap().push(payload); + if panics { + panic!("unpublished payload finalizer"); + } + }, + ) + .unwrap_err(); + assert_eq!( + failure, + if panics { + ResourceTransitionFailure::Cleanup(ResourceCleanupFailure::FinalizerPanic) + } else { + ResourceTransitionFailure::Rejected(CustodyError::OccupiedHolder) + } + ); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert_eq!(*events.lock().unwrap(), [99]); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [99, 1]); + clean(&core, ®istry); + } +} + +#[test] +fn custody_dropped_carrier_token_and_failed_caller_publication_do_not_lose_cleanup() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 7, + &events, + false, + ); + let physical = core.validate_owned(&owner, ®istry).unwrap(); + let physical_copy = physical; + assert_eq!(physical, physical_copy); + drop(owner); + assert!(events.lock().unwrap().is_empty()); + assert_eq!((core.live_owners(), registry.live_count()), (1, 1)); + assert!(matches!( + ResourceCustody::new(program.clone(), ®istry), + Err(CustodyError::UnownedRegistry) + )); + let body = ResourceBodyOutcome::<(), &str>::Completed(Err("caller publication failed")); + assert_eq!( + complete_resource(body, core.unwind_all(&mut registry)), + Err(ResourceCompletionFailure::Body("caller publication failed")) + ); + assert_eq!(*events.lock().unwrap(), [7]); + clean(&core, ®istry); +} + +#[test] +fn custody_repeated_entry_preserves_identity_and_rejects_stale_frames_and_tokens() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let id = core.id; + for label in 1..=3 { + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + label, + &events, + label == 2, + ); + let cleanup = core.unwind_all(&mut registry); + assert_eq!( + cleanup.failure(), + if label == 2 { + Some(ResourceCleanupFailure::FinalizerPanic) + } else { + None + } + ); + assert_eq!( + core.end_frame(&frame, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)) + ); + assert_eq!( + core.validate_owned(&owner, ®istry), + Err(CustodyError::InvalidOwner) + ); + assert_eq!(core.id, id); + clean(&core, ®istry); + } + assert_eq!(*events.lock().unwrap(), [1, 2, 3]); +} + +#[test] +fn custody_counter_exhaustion_refuses_transfer_without_reassigning_holder() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut owner = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + core.owners[owner.owner].generation = u64::MAX; + owner.holder_generation = u64::MAX; + let before = core.owners[owner.owner].holder; + let log = core.frames[frame.index].acquisitions.len(); + let destination = destination(&core, &program, &frame, 1); + assert_eq!( + core.transfer(&mut owner, &destination, ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.owners[owner.owner].holder, before); + assert_eq!(core.frames[frame.index].acquisitions.len(), log); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.close(&mut owner, &mut registry).failure(), None); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + clean(&core, ®istry); +} + +#[test] +fn custody_registry_shutdown_and_postinsert_cleanup_failures_remain_separate() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let holder = destination(&core, &program, &frame, 0); + let prepared = core + .prepare_acquisition(&program.kind(0).unwrap(), &holder) + .unwrap(); + registry.shutdown(); + let observed = events.clone(); + assert_eq!( + core.commit_acquisition(&mut registry, prepared, authority(), 8_i64, move |value| { + observed.lock().unwrap().push(value) + }) + .unwrap_err(), + ResourceTransitionFailure::Rejected(CustodyError::Registry(RegistryError::ShuttingDown)) + ); + assert_eq!(*events.lock().unwrap(), [8]); + assert_eq!(core.unwind_all(&mut registry).failure(), None); + clean(&core, ®istry); + + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let _second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + let key = core.validate_owned(&first, ®istry).unwrap(); + registry.close(key, ResourceTypeId::new(100)).unwrap(); + let outcome = core.unwind_all(&mut registry); + assert!(matches!( + outcome.failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::StaleGeneration + ))) + )); + assert_eq!(*events.lock().unwrap(), [1, 2]); + clean(&core, ®istry); +} + +#[test] +fn custody_completion_has_cleanup_first_matrix_without_changing_domain_values() { + for cleanup in [ + None, + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)), + Some(ResourceCleanupFailure::FinalizerPanic), + ] { + for body in [ + ResourceBodyOutcome::Completed(Ok(7_u8)), + ResourceBodyOutcome::Completed(Err("entry")), + ResourceBodyOutcome::HostPanic, + ] { + let expected = match cleanup { + Some(failure) => Err(ResourceCompletionFailure::Cleanup(failure)), + None => match &body { + ResourceBodyOutcome::Completed(Ok(value)) => Ok(*value), + ResourceBodyOutcome::Completed(Err(error)) => { + Err(ResourceCompletionFailure::Body(*error)) + } + ResourceBodyOutcome::HostPanic => Err(ResourceCompletionFailure::BodyPanic), + }, + }; + assert_eq!( + complete_resource(body, ResourceCleanupOutcome { first: cleanup }), + expected + ); + } + } + let domain = ResourceBodyOutcome::, &str>::Completed(Ok(Err("domain"))); + assert_eq!( + complete_resource(domain, ResourceCleanupOutcome::default()), + Ok(Err("domain")) + ); + let caught = ResourceBodyOutcome::<(), &str>::caught(catch_unwind(AssertUnwindSafe( + || -> Result<(), &str> { + panic!("body panic"); + }, + ))); + assert_eq!( + complete_resource(caught, ResourceCleanupOutcome::default()), + Err(ResourceCompletionFailure::BodyPanic) + ); + let mut first = ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)), + }; + first.merge(ResourceCleanupOutcome { + first: Some(ResourceCleanupFailure::FinalizerPanic), + }); + assert_eq!( + first.failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::InvalidFrame)) + ); +} + +#[test] +fn custody_failed_provenance_cleanup_preserves_unretired_frame_and_continues_eligible_owners() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let frame = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &frame, + 0, + 1, + &events, + false, + ); + let _second = acquire( + &mut core, + &mut registry, + &program, + &frame, + 1, + 2, + &events, + false, + ); + core.owners[first.owner].authority = AuthorityProvenance::new(5, 99); + assert_eq!( + core.end_frame(&frame, &mut registry).failure(), + Some(ResourceCleanupFailure::Custody(CustodyError::Registry( + RegistryError::AuthorityMismatch + ))) + ); + assert_eq!(*events.lock().unwrap(), [2]); + assert_eq!( + ( + core.live_owners(), + registry.live_count(), + core.active_frames() + ), + (1, 1, 1) + ); + core.owners[first.owner].authority = authority(); + assert_eq!(core.end_frame(&frame, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [2, 1]); + clean(&core, ®istry); +} + +#[path = "tests/source_batch.rs"] +mod source_batch; + +#[test] +fn custody_replacement_preflight_generation_capacity_refuses_before_old_finalization() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let events = Arc::new(Mutex::new(Vec::new())); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let scope = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut old = acquire( + &mut core, + &mut registry, + &program, + &scope, + 0, + 61, + &events, + false, + ); + let operation = core + .begin_frame(ResourceFrameKind::Operation, ResourcePurpose::Runtime) + .unwrap(); + let mut replacement = acquire( + &mut core, + &mut registry, + &program, + &operation, + 1, + 62, + &events, + false, + ); + let holder = destination(&core, &program, &scope, 0); + let old_key = core.validate_owned(&old, ®istry).unwrap(); + let rhs_key = core.validate_owned(&replacement, ®istry).unwrap(); + let before_holder = core.owners[replacement.owner].holder; + let before_generation = replacement.holder_generation; + let before_logs = ( + core.frames[scope.index].acquisitions.len(), + core.frames[operation.index].acquisitions.len(), + ); + // Deterministic private capacity fault, not an allocation/global-ID claim. + core.owners[replacement.owner].generation = u64::MAX; + replacement.holder_generation = u64::MAX; + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!( + core.preflight_replacement(&old, &replacement, &holder, ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.validate_owned(&old, ®istry), Ok(old_key)); + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!((core.live_owners(), registry.live_count()), (2, 2)); + assert_eq!(core.owners[replacement.owner].holder, before_holder); + assert_eq!(replacement.holder_generation, u64::MAX); + assert_eq!( + ( + core.frames[scope.index].acquisitions.len(), + core.frames[operation.index].acquisitions.len() + ), + before_logs + ); + assert!(events.lock().unwrap().is_empty()); + core.owners[replacement.owner].generation = before_generation; + replacement.holder_generation = before_generation; + core.preflight_replacement(&old, &replacement, &holder, ®istry) + .unwrap(); + assert!(events.lock().unwrap().is_empty()); + assert_eq!(core.close(&mut old, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61]); + core.transfer(&mut replacement, &holder, ®istry).unwrap(); + assert_eq!(core.validate_owned(&replacement, ®istry), Ok(rhs_key)); + assert_eq!(replacement.holder_generation, before_generation + 1); + assert_eq!(core.end_frame(&operation, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61]); + assert_eq!(core.end_frame(&scope, &mut registry).failure(), None); + assert_eq!(*events.lock().unwrap(), [61, 62]); + clean(&core, ®istry); +} + +#[path = "carrier_holder_tests.rs"] +mod carrier_holder_tests; diff --git a/crates/jett_runtime/src/resource_custody/tests/source_batch.rs b/crates/jett_runtime/src/resource_custody/tests/source_batch.rs new file mode 100644 index 00000000..f4427278 --- /dev/null +++ b/crates/jett_runtime/src/resource_custody/tests/source_batch.rs @@ -0,0 +1,118 @@ +use super::*; + +#[test] +fn custody_source_batch_late_active_borrow_refuses_without_moving_a_prefix() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let mut first = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 501, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &parent, + 1, + 502, + &events, + false, + ); + let mut loan = core.begin_borrow(&second, &parent, ®istry).unwrap(); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let slot0 = program.slot(0).unwrap(); + let slot1 = program.slot(1).unwrap(); + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry), + Err(CustodyError::ActiveBorrow) + ); + assert_eq!( + ( + core.owners[first.owner].holder.frame, + core.owners[second.owner].holder.frame + ), + (parent.index, parent.index) + ); + assert!(core.frames[callee.index].acquisitions.is_empty()); + assert!(events.lock().unwrap().is_empty()); + core.end_borrow(&mut loan).unwrap(); + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry) + .unwrap(); + assert!(core.frames[callee.index].acquisitions.capacity() >= 2); + let holder0 = core.holder(&callee, &slot0).unwrap(); + let holder1 = core.holder(&callee, &slot1).unwrap(); + core.transfer(&mut first, &holder0, ®istry).unwrap(); + core.transfer(&mut second, &holder1, ®istry).unwrap(); + assert!(core.end_frame(&callee, &mut registry).failure().is_none()); + assert!(core.end_frame(&parent, &mut registry).failure().is_none()); + clean(&core, ®istry); + assert_eq!(*events.lock().unwrap(), vec![502, 501]); +} + +#[test] +fn custody_source_batch_late_generation_overflow_and_duplicate_owner_refuse_before_prefix() { + let mut registry = ResourceRegistry::new(); + let program = test_program(®istry); + let mut core = ResourceCustody::new(program.clone(), ®istry).unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + let parent = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let first = acquire( + &mut core, + &mut registry, + &program, + &parent, + 0, + 501, + &events, + false, + ); + let mut second = acquire( + &mut core, + &mut registry, + &program, + &parent, + 1, + 502, + &events, + false, + ); + let callee = core + .begin_frame(ResourceFrameKind::Scope, ResourcePurpose::Runtime) + .unwrap(); + let slot0 = program.slot(0).unwrap(); + let slot1 = program.slot(1).unwrap(); + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&first, &slot1)], ®istry), + Err(CustodyError::OccupiedHolder) + ); + core.owners[second.owner].generation = u64::MAX; + second.holder_generation = u64::MAX; + assert_eq!( + core.preflight_transfer_batch(&callee, &[(&first, &slot0), (&second, &slot1)], ®istry), + Err(CustodyError::CapacityExhausted) + ); + assert_eq!(core.owners[first.owner].holder.frame, parent.index); + assert_eq!(first.holder_generation, 1); + assert!(core.frames[callee.index].acquisitions.is_empty()); + assert!(events.lock().unwrap().is_empty()); + core.owners[second.owner].generation = 1; + second.holder_generation = 1; + assert!(core.unwind_all(&mut registry).failure().is_none()); + clean(&core, ®istry); + assert_eq!(*events.lock().unwrap(), vec![502, 501]); +} diff --git a/crates/jett_runtime/src/uuid.rs b/crates/jett_runtime/src/uuid.rs new file mode 100644 index 00000000..65415069 --- /dev/null +++ b/crates/jett_runtime/src/uuid.rs @@ -0,0 +1,51 @@ +//! UUID v4 generation shared by the interpreter and native runtime. + +use rand::RngCore; + +pub const ENTROPY_UNAVAILABLE: &str = "uuid.new: OS entropy unavailable"; + +pub fn new_v4() -> Result { + let mut bytes = [0_u8; 16]; + rand::rngs::OsRng + .try_fill_bytes(&mut bytes) + .map_err(|_| ENTROPY_UNAVAILABLE)?; + Ok(format_v4(bytes)) +} + +fn format_v4(mut bytes: [u8; 16]) -> String { + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15] + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v4_format_sets_version_variant_and_canonical_separators() { + assert_eq!( + format_v4([0xff; 16]), + "ffffffff-ffff-4fff-bfff-ffffffffffff" + ); + assert_eq!(format_v4([0; 16]), "00000000-0000-4000-8000-000000000000"); + } +} diff --git a/crates/jett_runtime/tests/native_abi_external.rs b/crates/jett_runtime/tests/native_abi_external.rs new file mode 100644 index 00000000..3ce0ee36 --- /dev/null +++ b/crates/jett_runtime/tests/native_abi_external.rs @@ -0,0 +1,182 @@ +//! Exercise the runtime through independently declared C-compatible records and +//! exact link names rather than through Rust item references. + +use std::mem::{MaybeUninit, align_of, offset_of, size_of}; +use std::ptr; + +#[repr(C)] +struct ExternalContextV1 { + token: u64, + abi_version: u32, + state_tag: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ExternalStringSliceV1 { + data: *const u8, + byte_length: u64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct ExternalResultV1 { + status: u32, + message: ExternalStringSliceV1, +} + +unsafe extern "C" { + #[link_name = "jett_rt_v1_abi_version"] + static EXTERNAL_ABI_VERSION: u32; + + #[link_name = "jett_rt_v1_check_abi"] + fn external_check_abi(expected_version: u32, out_result: *mut ExternalResultV1) -> u32; + + #[link_name = "jett_rt_v1_context_create"] + fn external_context_create( + expected_version: u32, + out_context: *mut ExternalContextV1, + out_result: *mut ExternalResultV1, + ) -> u32; + + #[link_name = "jett_rt_v1_context_destroy"] + fn external_context_destroy( + context: *mut ExternalContextV1, + out_result: *mut ExternalResultV1, + ) -> u32; + + #[link_name = "jett_rt_v1_stdout_write"] + fn external_stdout_write( + context: *const ExternalContextV1, + string: *const ExternalStringSliceV1, + out_result: *mut ExternalResultV1, + ) -> u32; + + #[link_name = "jett_rt_v1_string_literal"] + fn external_string_literal( + context: *const ExternalContextV1, + data: *const u8, + length: u64, + ) -> u64; + + #[link_name = "jett_rt_v1_string_run"] + fn external_string_run(context: *const ExternalContextV1, value: u64) -> u64; + + #[link_name = "jett_rt_v1_string_release"] + fn external_string_release(context: *const ExternalContextV1, value: u64) -> u32; + + #[link_name = "jett_rt_v1_display_result_check"] + fn external_display_result_check(context: *const ExternalContextV1, value: u64) -> u32; + + #[link_name = "jett_rt_v1_value_status"] + fn external_value_status(context: *const ExternalContextV1) -> u32; + + #[link_name = "jett_rt_v1_value_failure_copy"] + fn external_failure_copy( + context: *const ExternalContextV1, + buffer: *mut u8, + capacity: u64, + out_length: *mut u64, + out_result: *mut ExternalResultV1, + ) -> u32; +} + +fn call_result(call: impl FnOnce(*mut ExternalResultV1) -> u32) -> (u32, ExternalResultV1) { + let mut result = MaybeUninit::uninit(); + let status = call(result.as_mut_ptr()); + (status, unsafe { result.assume_init() }) +} + +#[test] +fn independently_declared_ms_c_abi_links_and_runs() { + // Force the Rust dependency onto the link line while all ABI operations + // below are resolved exclusively through their exact external symbols. + drop(jett_runtime::ResourceRegistry::new()); + + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 8); + assert_eq!(offset_of!(ExternalContextV1, token), 0); + assert_eq!(offset_of!(ExternalContextV1, abi_version), 8); + assert_eq!(offset_of!(ExternalContextV1, state_tag), 12); + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 24); + assert_eq!(align_of::(), 8); + assert_eq!(offset_of!(ExternalResultV1, status), 0); + assert_eq!(offset_of!(ExternalResultV1, message), 8); + assert_eq!(unsafe { EXTERNAL_ABI_VERSION }, 1); + + let (status, checked) = call_result(|out| unsafe { external_check_abi(1, out) }); + assert_eq!(status, 0); + assert_eq!(checked.status, status); + + let mut context = MaybeUninit::::uninit(); + let (status, created) = + call_result(|out| unsafe { external_context_create(1, context.as_mut_ptr(), out) }); + assert_eq!(status, 0); + assert_eq!(created.status, status); + // Keep using the exact storage address passed to create. Moving the + // record is deliberately rejected by the ABI's owner-address binding. + let context = unsafe { &mut *context.as_mut_ptr() }; + + let empty = ExternalStringSliceV1 { + data: ptr::null(), + byte_length: 0, + }; + let (status, wrote) = call_result(|out| unsafe { external_stdout_write(context, &empty, out) }); + assert_eq!(status, 0); + assert_eq!(wrote.status, status); + + let (status, destroyed) = call_result(|out| unsafe { external_context_destroy(context, out) }); + assert_eq!(status, 0); + assert_eq!(destroyed.status, status); + + let (status, repeated) = call_result(|out| unsafe { external_context_destroy(context, out) }); + assert_eq!(status, 3); + assert_eq!(repeated.status, status); +} + +#[test] +fn independently_declared_display_result_check_links_and_preserves_borrowed_strings() { + drop(jett_runtime::ResourceRegistry::new()); + let mut context = MaybeUninit::::uninit(); + let (status, created) = + call_result(|out| unsafe { external_context_create(1, context.as_mut_ptr(), out) }); + assert_eq!(status, 0); + assert_eq!(created.status, 0); + let context = unsafe { &mut *context.as_mut_ptr() }; + let text = "é🦀"; + unsafe { + let ready = + external_string_literal(context, text.as_ptr(), u64::try_from(text.len()).unwrap()); + assert_ne!(ready, 0); + assert_eq!(external_display_result_check(context, ready), 0); + assert_eq!(external_value_status(context), 0); + let pending = external_string_run(context, ready); + let nested = external_string_run(context, pending); + assert_ne!(external_display_result_check(context, nested), 0); + assert_eq!(external_value_status(context), 1); + let expected = "Displayable.display returned pending(pending(é🦀)) instead of string"; + let mut message = vec![0; expected.len()]; + let mut length = 0; + let (status, copied) = call_result(|out| { + external_failure_copy( + context, + message.as_mut_ptr(), + u64::try_from(message.len()).unwrap(), + &mut length, + out, + ) + }); + assert_eq!(status, 0); + assert_eq!(copied.status, 0); + assert_eq!(usize::try_from(length).unwrap(), expected.len()); + assert_eq!(message, expected.as_bytes()); + for handle in [ready, pending, nested] { + assert_eq!(external_string_release(context, handle), 0); + } + } + let (status, destroyed) = call_result(|out| unsafe { external_context_destroy(context, out) }); + assert_eq!(status, 0); + assert_eq!(destroyed.status, 0); +} diff --git a/crates/jett_runtime/tests/resource_registry.rs b/crates/jett_runtime/tests/resource_registry.rs index 5d820fab..044e6af8 100644 --- a/crates/jett_runtime/tests/resource_registry.rs +++ b/crates/jett_runtime/tests/resource_registry.rs @@ -1,15 +1,22 @@ use std::panic::{AssertUnwindSafe, catch_unwind, panic_any}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use jett_runtime::{AuthorityProvenance, ResourceRegistry, ResourceTypeId}; -struct PanicOnDrop; +struct PanicOnDrop { + drops: Arc, +} impl Drop for PanicOnDrop { fn drop(&mut self) { - // The secondary payload also panics on drop, checking that disposal - // handles provider failure without unbounded recursive unwinding. - panic_any(PanicOnDrop); + self.drops.fetch_add(1, Ordering::SeqCst); + // If a regression drops this payload, make its destructor panic with + // another payload. The counter distinguishes forgetting the original + // payload from merely catching this destructor panic. + panic_any(PanicOnDrop { + drops: Arc::clone(&self.drops), + }); } } @@ -261,11 +268,13 @@ fn cleanup_panics_do_not_abort_an_existing_unwind() { } #[test] -fn panic_payload_destruction_does_not_skip_older_resources() { +fn suppressed_panic_payload_is_forgotten_without_skipping_older_resources() { let events = Arc::new(Mutex::new(Vec::new())); + let payload_drops = Arc::new(AtomicUsize::new(0)); let mut registry = ResourceRegistry::new(); for name in ["oldest", "middle", "newest"] { let events = Arc::clone(&events); + let payload_drops = Arc::clone(&payload_drops); registry .insert( ResourceTypeId::new(1), @@ -275,7 +284,9 @@ fn panic_payload_destruction_does_not_skip_older_resources() { events.lock().unwrap().push(name); match name { "newest" => panic!("first provider failure"), - "middle" => panic_any(PanicOnDrop), + "middle" => panic_any(PanicOnDrop { + drops: payload_drops, + }), _ => {} } }, @@ -291,19 +302,24 @@ fn panic_payload_destruction_does_not_skip_older_resources() { registry.shutdown(); drop(registry); assert_eq!(*events.lock().unwrap(), ["newest", "middle", "oldest"]); + assert_eq!(payload_drops.load(Ordering::SeqCst), 0); } #[test] fn suppressed_finalizer_payload_does_not_replace_the_detach_failure() { let events = Arc::new(Mutex::new(Vec::new())); + let payload_drops = Arc::new(AtomicUsize::new(0)); let finalized = Arc::clone(&events); + let finalizer_payload_drops = Arc::clone(&payload_drops); let mut registry = ResourceRegistry::new(); let resource_type = ResourceTypeId::new(1); let authority = AuthorityProvenance::new(1, 1); let key = registry .insert(resource_type, (), authority, move |_| { finalized.lock().unwrap().push("finalized"); - panic_any(PanicOnDrop); + panic_any(PanicOnDrop { + drops: finalizer_payload_drops, + }); }) .unwrap(); registry @@ -315,12 +331,15 @@ fn suppressed_finalizer_payload_does_not_replace_the_detach_failure() { assert_eq!(registry.live_count(), 0); drop(registry); assert_eq!(*events.lock().unwrap(), ["finalized"]); + assert_eq!(payload_drops.load(Ordering::SeqCst), 0); } #[test] -fn panic_payload_destruction_does_not_abort_an_existing_unwind() { +fn panic_payload_is_forgotten_during_an_existing_unwind() { let events = Arc::new(Mutex::new(Vec::new())); + let payload_drops = Arc::new(AtomicUsize::new(0)); let finalized = Arc::clone(&events); + let finalizer_payload_drops = Arc::clone(&payload_drops); let failure = catch_unwind(move || { let mut registry = ResourceRegistry::new(); registry @@ -330,7 +349,9 @@ fn panic_payload_destruction_does_not_abort_an_existing_unwind() { AuthorityProvenance::new(1, 1), move |_| { finalized.lock().unwrap().push("finalized"); - panic_any(PanicOnDrop); + panic_any(PanicOnDrop { + drops: finalizer_payload_drops, + }); }, ) .unwrap(); @@ -339,4 +360,5 @@ fn panic_payload_destruction_does_not_abort_an_existing_unwind() { .unwrap_err(); assert_eq!(failure.downcast_ref::<&str>(), Some(&"original failure")); assert_eq!(*events.lock().unwrap(), ["finalized"]); + assert_eq!(payload_drops.load(Ordering::SeqCst), 0); } diff --git a/crates/jett_typecheck/Cargo.toml b/crates/jett_typecheck/Cargo.toml index a4b1dc56..ae47ac8c 100644 --- a/crates/jett_typecheck/Cargo.toml +++ b/crates/jett_typecheck/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true [dependencies] jett_common.workspace = true jett_diagnostics.workspace = true +jett_intrinsics.workspace = true jett_parser.workspace = true jett_types.workspace = true jett_resolve.workspace = true diff --git a/crates/jett_typecheck/src/actor_messages.rs b/crates/jett_typecheck/src/actor_messages.rs new file mode 100644 index 00000000..fc774765 --- /dev/null +++ b/crates/jett_typecheck/src/actor_messages.rs @@ -0,0 +1,405 @@ +//! Checked actor messages and persistent handler roles from one compiler session. +//! +//! These facts do not grant Resource custody or live instance authority. A +//! consumer must join them to the retained original actor and handler body. +use std::collections::HashMap; +use std::sync::Arc; + +use jett_common::Span; +use jett_parser::ast; +use jett_resolve::scope::DefId; +use jett_types::{ActorId, TypeId}; + +use crate::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedBodyFacts, CheckedComptimeTypeBinding, + CheckedOwnershipContext, +}; + +#[derive(Debug, Clone)] +pub(crate) struct ActorCheckSession(Arc<()>); + +impl ActorCheckSession { + pub(crate) fn new() -> Self { + Self(Arc::new(())) + } +} + +impl PartialEq for ActorCheckSession { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for ActorCheckSession {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CheckedActorHandlerTarget { + pub actor_definition: DefId, + pub actor: ActorId, + pub handler_index: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedActorBindingRole { + Capability { parameter: usize }, + State { field: usize }, + Message { parameter: usize }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CheckedActorHandlerBinding { + pub role: CheckedActorBindingRole, + pub fact: CheckedBindingFact, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedActorMessageKind { + Send, + Ask, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedActorMessageCall { + pub target: CheckedActorHandlerTarget, + pub kind: CheckedActorMessageKind, + pub context: CheckedOwnershipContext, + pub source_span: Span, + pub receiver_span: Span, + pub receiver_type: TypeId, + pub response_type: TypeId, + /// Original argument occurrences in lexical source order. + pub arguments: Vec, + origin: ActorMessageOrigin, +} + +impl CheckedActorMessageCall { + pub(crate) fn new( + session: ActorCheckSession, + target: CheckedActorHandlerTarget, + kind: CheckedActorMessageKind, + context: CheckedOwnershipContext, + source_span: Span, + receiver_span: Span, + receiver_type: TypeId, + response_type: TypeId, + arguments: Vec, + ) -> Self { + let origin = ActorMessageOrigin { + session, + occurrence: Arc::new(()), + target, + kind, + context, + source_span, + receiver_span, + receiver_type, + response_type, + arguments: arguments.clone(), + }; + Self { + target, + kind, + context, + source_span, + receiver_span, + receiver_type, + response_type, + arguments, + origin, + } + } +} + +#[derive(Debug, Clone)] +struct ActorMessageOrigin { + session: ActorCheckSession, + occurrence: Arc<()>, + target: CheckedActorHandlerTarget, + kind: CheckedActorMessageKind, + context: CheckedOwnershipContext, + source_span: Span, + receiver_span: Span, + receiver_type: TypeId, + response_type: TypeId, + arguments: Vec, +} + +impl ActorMessageOrigin { + fn matches(&self, call: &CheckedActorMessageCall) -> bool { + self.target == call.target + && self.kind == call.kind + && self.context == call.context + && self.source_span == call.source_span + && self.receiver_span == call.receiver_span + && self.receiver_type == call.receiver_type + && self.response_type == call.response_type + && self.arguments == call.arguments + } +} + +impl PartialEq for ActorMessageOrigin { + fn eq(&self, other: &Self) -> bool { + self.session == other.session + && Arc::ptr_eq(&self.occurrence, &other.occurrence) + && self.target == other.target + && self.kind == other.kind + && self.context == other.context + && self.source_span == other.source_span + && self.receiver_span == other.receiver_span + && self.receiver_type == other.receiver_type + && self.response_type == other.response_type + && self.arguments == other.arguments + } +} + +impl Eq for ActorMessageOrigin {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CheckedActorMessageRoot { + Ordinary, + Generic(usize), + ActorHandler(CheckedActorHandlerTarget), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct ActorMessageSite { + root: CheckedActorMessageRoot, + scopes: Vec<(Span, usize)>, + source_span: Span, +} + +#[derive(Debug, Default)] +pub(crate) struct ActorMessageSites(HashMap); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedActorHandlerExitKind { + Fallthrough, + Respond, + Return, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CheckedActorHandlerExit { + pub span: Span, + pub kind: CheckedActorHandlerExitKind, +} + +/// An exact original handler header and its selected body facts. +/// +/// The private body cannot be replaced or edited through public observations. +/// Its ownership receipt is granted only by accepting ownership traversal and +/// successful checking of the complete program, never by breakpoint observation. +#[derive(Debug, Clone)] +pub struct CheckedActorHandler { + target: CheckedActorHandlerTarget, + body_span: Span, + bindings: Vec, + response_type: TypeId, + body: CheckedBodyFacts, + exits: Vec, + session: ActorCheckSession, + source_actor: String, + ownership_seal: Option, +} + +impl CheckedActorHandler { + pub(crate) fn new( + target: CheckedActorHandlerTarget, + body_span: Span, + bindings: Vec, + response_type: TypeId, + body: CheckedBodyFacts, + session: ActorCheckSession, + source_actor: &ast::ActorDef, + ) -> Self { + Self { + target, + body_span, + bindings, + response_type, + body, + exits: Vec::new(), + session, + source_actor: format!("{source_actor:?}"), + ownership_seal: None, + } + } + + pub fn target(&self) -> CheckedActorHandlerTarget { + self.target + } + + pub fn matches_source_actor(&self, actor: &ast::ActorDef) -> bool { + self.source_actor == format!("{actor:?}") + } + + pub fn body_span(&self) -> Span { + self.body_span + } + + pub fn bindings(&self) -> &[CheckedActorHandlerBinding] { + &self.bindings + } + + pub fn response_type(&self) -> TypeId { + self.response_type + } + + pub fn body(&self) -> &CheckedBodyFacts { + &self.body + } + + pub fn exits(&self) -> &[CheckedActorHandlerExit] { + &self.exits + } + + pub fn ownership_validated(&self) -> bool { + self.ownership_seal + .as_ref() + .is_some_and(|seal| seal.matches(self)) + } + + pub(crate) fn body_mut(&mut self) -> &mut CheckedBodyFacts { + self.ownership_seal = None; + &mut self.body + } + + pub(crate) fn record_exits(&mut self, exits: Vec) { + self.ownership_seal = None; + self.exits = exits; + } + + pub(crate) fn authorize_ownership(&mut self) { + self.ownership_seal = Some(ActorHandlerOwnershipSeal { + target: self.target, + body_span: self.body_span, + bindings: self.bindings.clone(), + response_type: self.response_type, + exits: self.exits.clone(), + }); + } +} + +#[derive(Debug, Clone)] +struct ActorHandlerOwnershipSeal { + target: CheckedActorHandlerTarget, + body_span: Span, + bindings: Vec, + response_type: TypeId, + exits: Vec, +} + +impl ActorHandlerOwnershipSeal { + fn matches(&self, handler: &CheckedActorHandler) -> bool { + self.target == handler.target + && self.body_span == handler.body_span + && self.bindings == handler.bindings + && self.response_type == handler.response_type + && self.exits == handler.exits + } +} + +impl crate::CheckResult { + pub fn actor_handler(&self, target: CheckedActorHandlerTarget) -> Option<&CheckedActorHandler> { + self.actor_handlers.get(&target).filter(|handler| { + handler.target == target + && handler.session == self.actor_check_session + && handler.ownership_validated() + }) + } + + /// Snapshot/session sanity only; executable consumers also select the exact + /// original occurrence and call `validates_actor_message_at` for its body. + pub fn validates_actor_message(&self, call: &CheckedActorMessageCall) -> bool { + self.diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error) + && call.origin.session == self.actor_check_session + && call.origin.matches(call) + } + + pub fn validates_actor_message_at( + &self, + call: &CheckedActorMessageCall, + root: CheckedActorMessageRoot, + scopes: &[(Span, usize)], + source_span: Span, + ) -> bool { + source_span == call.source_span + && self.validates_actor_message(call) + && self.actor_message_sites.0.get(&ActorMessageSite { + root, + scopes: scopes.to_vec(), + source_span, + }) == Some(&call.origin) + } + + pub(crate) fn freeze_actor_message_sites(&mut self) { + let mut sites = ActorMessageSites::default(); + capture_message_sites( + self, + CheckedActorMessageRoot::Ordinary, + &[], + &self.actor_message_calls, + &self.comptime_type_bindings, + &mut sites, + ); + for (index, body) in self.generic_function_instantiations.iter().enumerate() { + capture_message_sites( + self, + CheckedActorMessageRoot::Generic(index), + &[], + &body.actor_message_calls, + &body.comptime_type_bindings, + &mut sites, + ); + } + for (target, handler) in &self.actor_handlers { + capture_message_sites( + self, + CheckedActorMessageRoot::ActorHandler(*target), + &[], + &handler.body.actor_message_calls, + &handler.body.comptime_type_bindings, + &mut sites, + ); + } + self.actor_message_sites = sites; + } +} + +fn capture_message_sites( + checked: &crate::CheckResult, + root: CheckedActorMessageRoot, + scopes: &[(Span, usize)], + calls: &HashMap, + nested: &HashMap>, + sites: &mut ActorMessageSites, +) { + for (source_span, call) in calls { + if *source_span == call.source_span && checked.validates_actor_message(call) { + sites.0.insert( + ActorMessageSite { + root, + scopes: scopes.to_vec(), + source_span: *source_span, + }, + call.origin.clone(), + ); + } + } + for (owner, bodies) in nested { + for (index, binding) in bodies.iter().enumerate() { + let mut selected = scopes.to_vec(); + selected.push((*owner, index)); + capture_message_sites( + checked, + root, + &selected, + &binding.body.actor_message_calls, + &binding.body.comptime_type_bindings, + sites, + ); + } + } +} diff --git a/crates/jett_typecheck/src/assignment_places.rs b/crates/jett_typecheck/src/assignment_places.rs new file mode 100644 index 00000000..de30c6be --- /dev/null +++ b/crates/jett_typecheck/src/assignment_places.rs @@ -0,0 +1,393 @@ +//! Checked field paths, not runtime ownership or mutation authority. +//! +//! Facts belong to one exact checked body, including its concrete generic and +//! lexical comptime selections. A consumer must authenticate original/current +//! Source and independently establish live storage, generation and absence of +//! dependent loans before preparing or committing any replacement. + +use std::collections::{HashMap, HashSet}; + +use jett_common::Span; +use jett_types::{MachineId, MachineStateId, StructId, Type, TypeId, TypeInterner}; + +use crate::{CheckedBindingFact, CheckedBindingMode}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedFieldOwner { + Struct(StructId), + MachineState { + machine: MachineId, + state: MachineStateId, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedFieldUpdateResourceRequirement { + Ordinary, + RequiresCustody, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedFieldProjection { + /// Original projection use; no field declaration span is fabricated. + pub source_span: Span, + /// Exact checked parent type, including its Secret qualification. + pub owner_type: TypeId, + pub owner: CheckedFieldOwner, + pub field_index: usize, + /// Complete stored type, retaining nominal refinements. + pub declared_field_type: TypeId, + /// Checked access type after qualification propagation. + pub field_type: TypeId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedAssignmentPlace { + pub assignment_span: Span, + /// Declaration identity and declared storage type, not a liveness proof. + pub root: CheckedBindingFact, + pub root_use_span: Span, + /// Current checked type; may narrow declared Machine storage to one state. + pub root_type: TypeId, + pub projections: Vec, + /// Entire root classification, including unchanged Resource siblings. + pub resource: CheckedFieldUpdateResourceRequirement, + /// Set only after successful independent ownership traversal of this exact + /// concrete/scoped context and successful checking of the whole program. + pub(crate) ownership_seal: Option, +} + +impl CheckedAssignmentPlace { + pub fn ownership_validated(&self) -> bool { + self.ownership_seal + .as_ref() + .is_some_and(|seal| seal.matches(self)) + } +} + +/// This snapshots checked metadata only, never a Source runtime value/owner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AssignmentOwnershipSeal { + assignment_span: Span, + root: CheckedBindingFact, + root_use_span: Span, + root_type: TypeId, + projections: Vec, + resource: CheckedFieldUpdateResourceRequirement, +} + +impl AssignmentOwnershipSeal { + fn matches(&self, place: &CheckedAssignmentPlace) -> bool { + self.assignment_span == place.assignment_span + && self.root == place.root + && self.root_use_span == place.root_use_span + && self.root_type == place.root_type + && self.projections == place.projections + && self.resource == place.resource + } + + fn capture(place: &CheckedAssignmentPlace) -> Self { + Self { + assignment_span: place.assignment_span, + root: place.root, + root_use_span: place.root_use_span, + root_type: place.root_type, + projections: place.projections.clone(), + resource: place.resource, + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub(crate) enum AssignmentContextRoot { + #[default] + Ordinary, + Generic(usize), + ActorHandler(crate::CheckedActorHandlerTarget), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub(crate) struct AssignmentContext { + pub root: AssignmentContextRoot, + pub scopes: Vec<(Span, usize)>, +} + +impl AssignmentContext { + pub fn scoped(&self, binding: Span, index: usize) -> Self { + let mut context = self.clone(); + context.scopes.push((binding, index)); + context + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct AssignmentSite { + pub context: AssignmentContext, + pub span: Span, +} + +/// Stamp only facts independently visited in their exact selected context. +/// The caller supplies an empty set whenever any Source checking error exists. +pub(crate) fn seal_assignment_ownership( + places: &mut HashMap, + bindings: &mut HashMap>, + context: &AssignmentContext, + validated: &HashSet, +) { + for (span, place) in places { + let admitted = validated.contains(&AssignmentSite { + context: context.clone(), + span: *span, + }); + place.ownership_seal = admitted.then(|| AssignmentOwnershipSeal::capture(place)); + } + for (span, bodies) in bindings { + for (index, body) in bodies.iter_mut().enumerate() { + seal_assignment_ownership( + &mut body.body.assignment_places, + &mut body.body.comptime_type_bindings, + &context.scoped(*span, index), + validated, + ); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedAssignmentPlaceError { + EmptyPath, + RootBinding, + SourceAssociation, + StorageType, + RefinedOwner, + BitfieldOwner, + CompilerMetadataOwner, + OwnerType, + FieldOrdinal, + FieldType, + Qualification, + ResourceClassification, +} + +/// Inspect physical owned data, not phantom generic arguments or actor state. +/// This classification supplies no Resource transport/replacement permission. +pub fn field_update_resource_requirement( + types: &TypeInterner, + root: TypeId, +) -> CheckedFieldUpdateResourceRequirement { + fn contains(types: &TypeInterner, ty: TypeId, visited: &mut HashSet) -> bool { + if !visited.insert(ty) { + return false; + } + match types.resolve(ty) { + Type::Resource(_) => true, + Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) + | Type::Secret(inner) + | Type::Refinement { base: inner, .. } => contains(types, *inner, visited), + Type::Map(left, right) | Type::Result(left, right) => { + contains(types, *left, visited) || contains(types, *right, visited) + } + Type::Struct(id) => types + .resolve_struct(*id) + .fields + .iter() + .any(|(_, ty)| contains(types, *ty, visited)), + Type::Bitfield(id) => types + .resolve_bitfield(*id) + .fields + .iter() + .any(|field| contains(types, field.ty, visited)), + Type::Enum(id) => types + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| &variant.fields) + .any(|(_, ty)| contains(types, *ty, visited)), + Type::Machine(id) => types + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| &state.fields) + .any(|(_, ty)| contains(types, *ty, visited)), + Type::MachineState { machine, state } => types + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state + .fields + .iter() + .any(|(_, ty)| contains(types, *ty, visited)) + }), + Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::TypeConstruction + | Type::Never + | Type::Capability(_) + | Type::Interface(_) + | Type::Actor(_) + | Type::Function { .. } + | Type::Error => false, + } + } + if contains(types, root, &mut HashSet::new()) { + CheckedFieldUpdateResourceRequirement::RequiresCustody + } else { + CheckedFieldUpdateResourceRequirement::Ordinary + } +} + +fn inside(child: Span, parent: Span) -> bool { + child.file == parent.file && child.start >= parent.start && child.end <= parent.end +} + +fn checked_type(types: &TypeInterner, ty: TypeId) -> Result<&Type, CheckedAssignmentPlaceError> { + if usize::try_from(ty.index()).is_ok_and(|index| index < types.len()) { + Ok(types.resolve(ty)) + } else { + Err(CheckedAssignmentPlaceError::OwnerType) + } +} + +fn nominal_owner_type( + types: &TypeInterner, + ty: TypeId, +) -> Result<(TypeId, bool), CheckedAssignmentPlaceError> { + match checked_type(types, ty)? { + Type::Secret(inner) => match checked_type(types, *inner)? { + Type::Refinement { .. } => Err(CheckedAssignmentPlaceError::RefinedOwner), + Type::Bitfield(_) => Err(CheckedAssignmentPlaceError::BitfieldOwner), + Type::Secret(_) => Err(CheckedAssignmentPlaceError::OwnerType), + _ => Ok((*inner, true)), + }, + Type::Refinement { .. } => Err(CheckedAssignmentPlaceError::RefinedOwner), + Type::Bitfield(_) => Err(CheckedAssignmentPlaceError::BitfieldOwner), + _ => Ok((ty, false)), + } +} + +/// Resolve only a canonical owner and ordinal. This does not inspect a Value. +pub fn checked_field_owner( + types: &TypeInterner, + owner_type: TypeId, +) -> Result<(CheckedFieldOwner, &[(String, TypeId)]), CheckedAssignmentPlaceError> { + let (nominal, _) = nominal_owner_type(types, owner_type)?; + match checked_type(types, nominal)? { + Type::Struct(id) if types.is_compiler_metadata_struct(*id) => { + Err(CheckedAssignmentPlaceError::CompilerMetadataOwner) + } + Type::Struct(id) => Ok(( + CheckedFieldOwner::Struct(*id), + &types.resolve_struct(*id).fields, + )), + Type::MachineState { machine, state } => { + let fields = &types + .resolve_machine(*machine) + .state(*state) + .ok_or(CheckedAssignmentPlaceError::OwnerType)? + .fields; + Ok(( + CheckedFieldOwner::MachineState { + machine: *machine, + state: *state, + }, + fields, + )) + } + _ => Err(CheckedAssignmentPlaceError::OwnerType), + } +} + +fn storage_accepts_checked_root(types: &TypeInterner, storage: TypeId, current: TypeId) -> bool { + if storage == current { + return true; + } + match (checked_type(types, storage), checked_type(types, current)) { + (Ok(Type::Machine(expected)), Ok(Type::MachineState { machine, .. })) => { + expected == machine + } + (Ok(Type::Secret(expected)), Ok(Type::Secret(actual))) => { + storage_accepts_checked_root(types, *expected, *actual) + } + _ => false, + } +} + +fn field_qualification_matches( + types: &TypeInterner, + secret_owner: bool, + declared: TypeId, + selected: TypeId, +) -> bool { + if !secret_owner || matches!(types.resolve(declared), Type::Secret(_)) { + declared == selected + } else { + matches!(types.resolve(selected), Type::Secret(inner) if *inner == declared) + } +} + +/// Revalidate shape against this session's canonical definitions after remapping. +/// Resource classification is checked against declared whole-root storage; +/// it never supplies custody authority. The interner must itself belong to the +/// authenticated checked program, not a caller-fabricated nominal registry. +pub fn validate_assignment_place_shape( + place: &CheckedAssignmentPlace, + types: &TypeInterner, +) -> Result<(), CheckedAssignmentPlaceError> { + if place.projections.is_empty() { + return Err(CheckedAssignmentPlaceError::EmptyPath); + } + if !place.root.mutable || place.root.mode != CheckedBindingMode::Owned { + return Err(CheckedAssignmentPlaceError::RootBinding); + } + if !inside(place.root_use_span, place.assignment_span) { + return Err(CheckedAssignmentPlaceError::SourceAssociation); + } + checked_type(types, place.root.ty)?; + checked_type(types, place.root_type)?; + if !storage_accepts_checked_root(types, place.root.ty, place.root_type) { + return Err(CheckedAssignmentPlaceError::StorageType); + } + if field_update_resource_requirement(types, place.root.ty) != place.resource { + return Err(CheckedAssignmentPlaceError::ResourceClassification); + } + let mut current = place.root_type; + for projection in &place.projections { + if !inside(projection.source_span, place.assignment_span) { + return Err(CheckedAssignmentPlaceError::SourceAssociation); + } + if projection.owner_type != current { + return Err(CheckedAssignmentPlaceError::OwnerType); + } + let (owner, fields) = checked_field_owner(types, current)?; + if projection.owner != owner { + return Err(CheckedAssignmentPlaceError::OwnerType); + } + let (_, declared) = fields + .get(projection.field_index) + .ok_or(CheckedAssignmentPlaceError::FieldOrdinal)?; + checked_type(types, projection.field_type)?; + if projection.declared_field_type != *declared { + return Err(CheckedAssignmentPlaceError::FieldType); + } + let (_, secret) = nominal_owner_type(types, current)?; + if !field_qualification_matches(types, secret, *declared, projection.field_type) { + return Err(CheckedAssignmentPlaceError::Qualification); + } + current = projection.field_type; + } + Ok(()) +} diff --git a/crates/jett_typecheck/src/caller_ownership.rs b/crates/jett_typecheck/src/caller_ownership.rs new file mode 100644 index 00000000..96890b95 --- /dev/null +++ b/crates/jett_typecheck/src/caller_ownership.rs @@ -0,0 +1,444 @@ +//! Checked caller disposition, independent of physical callee access. +//! +//! These facts belong to one resolution session and one concrete checked body. +//! An inserted physical borrow never supplies a source `view` witness. +use jett_common::Span; +use jett_intrinsics::IntrinsicId; +use jett_resolve::scope::DefId; +use jett_types::TypeId; + +use crate::{ + CheckedBindingMode, CheckedGenericCall, CheckedInterfaceCall, CheckedMethodCall, + CheckedViewSource, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCallerSyntax { + Bare, + WrittenView, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCalleeAccess { + Owned, + View, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedCallerEffect { + Copy, + RetainBorrow, + TransferOwned, + RelinquishOwned, + ObserveData, +} + +/// This context describes the source occurrence, never its instantiating caller. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum CheckedOwnershipContext { + #[default] + Ordinary, + Verify, + Property, + BreakpointExpression, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CheckedBindingFact { + pub definition: DefId, + pub declaration_span: Span, + pub ty: TypeId, + pub mode: CheckedBindingMode, + pub mutable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedCallerOrigin { + Binding(CheckedBindingFact), + BorrowedProjection { source: CheckedViewSource }, + OwnedFieldCopy { parent: CheckedViewSource }, + OwnedExpression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedArgumentOwnership { + pub source_span: Span, + pub source_index: usize, + pub parameter_index: usize, + pub actual_type: TypeId, + pub parameter_type: TypeId, + pub syntax: CheckedCallerSyntax, + pub origin: CheckedCallerOrigin, + pub callee_access: CheckedCalleeAccess, + pub effect: CheckedCallerEffect, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedInvocationTarget { + Resolved(DefId), + Generic(CheckedGenericCall), + Method(CheckedMethodCall), + Interface(CheckedInterfaceCall), + Indirect(TypeId), + Intrinsic(IntrinsicId), +} + +/// Each role is checked against a closed intrinsic's source operand contract. +/// HIR-added metadata is separate from these original source occurrences. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedIntrinsicOperandRole { + Copy { ty: TypeId }, + Owned { ty: TypeId }, + View { ty: TypeId }, + PrintArgument { ty: TypeId }, +} + +impl CheckedIntrinsicOperandRole { + pub fn ty(self) -> TypeId { + match self { + Self::Copy { ty } + | Self::Owned { ty } + | Self::View { ty } + | Self::PrintArgument { ty } => ty, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckedInvocationShape { + Function { + signature_type: TypeId, + }, + Intrinsic { + intrinsic: IntrinsicId, + result_type: TypeId, + operands: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedCallOwnership { + pub target: CheckedInvocationTarget, + pub shape: CheckedInvocationShape, + pub context: CheckedOwnershipContext, + /// Original occurrences in lexical source order, with explicit formal joins. + pub arguments: Vec, +} + +/// Closed source operand access; this never authorizes retaining a bare caller. +/// The successful checker signature supplies types, while this table seals arity +/// and access independently from backend physical borrowing. +pub fn intrinsic_operand_access( + id: IntrinsicId, + index: usize, + arity: usize, +) -> Option { + use IntrinsicId::*; + let accepted_arity = match id { + Print | Println => true, + Range => (1..=3).contains(&arity), + BytesNew + | ListNew + | MapNew + | SetNew + | MathPi + | MathE + | TypeBitfieldFields + | TypeBitfieldLayout + | TypeConstructStart + | TypeFields + | TypeHasSecret + | TypeInfo + | TypeKind + | TypeKindTag + | TypeMachineLayout + | TypeMachineStates + | TypeMachineTransitions + | TypeName + | TypePrimitiveTag + | TypeVariants + | UuidNew => arity == 0, + BitfieldFromBytes + | BitfieldToBytes + | BytesFromHex + | BytesFromString + | BytesLength + | BytesToHex + | BytesToString + | ClockNow + | CryptoMd5 + | CryptoSha256 + | CryptoSha512 + | CsvParse + | CsvParseWithHeader + | CsvStringify + | EncodingBase64Decode + | EncodingBase64Encode + | EncodingFormDecode + | EncodingFormEncode + | EncodingHexDecode + | EncodingUrlDecode + | EncodingUrlEncode + | EnvironmentArgs + | Float32FromFloat64 + | Float64FromInt64 + | Float64FromString + | Int64FromFloat64 + | Int64FromString + | JsonParse + | JsonParseExact + | JsonSerialize + | JsonSerializePublic + | ListIsSorted + | ListLength + | ListSort + | ListSum + | MapLength + | MathAbs + | MathAverage + | MathCeil + | MathCos + | MathFactorial + | MathFloor + | MathKernelAbs + | MathLog + | MathLog10 + | MathLog2 + | MathMedian + | MathRound + | MathSin + | MathSqrt + | MathTan + | SecretRedact + | SetLength + | StringCharCount + | StringChars + | StringFromBool + | StringFromFloat64 + | StringFromInt64 + | StringFromUint64 + | StringIsAlpha + | StringIsNumeric + | StringLines + | StringLower + | StringSlugify + | StringToLowerFirst + | StringToUpperFirst + | StringTrim + | StringTrimEnd + | StringTrimStart + | StringUpper + | StringWords + | TestMockClock + | TestMockRandom + | TypeArg + | TypeConstructFinish + | TypeConstructMachineStart + | TypeConstructVariantStart + | TypeMachineStateValue + | TypeVariantValue + | Uint64FromString + | RandomBool + | RandomUnitFloat64 => arity == 1, + BytesConcat + | BytesGet + | CryptoHmacSha256 + | EnvironmentGet + | FilesystemReadFile + | ListAppend + | ListGetClone + | ListGroupBy + | ListRemoveAt + | ListSortBy + | ListSortByIndex + | LogEmit + | MapFromLists + | MapGet + | MapHas + | MapRemove + | MathGcd + | MathKernelMax + | MathKernelMin + | MathLcm + | MathMax + | MathMin + | MathMod + | MathPow + | SecretCompare + | SetAdd + | SetContains + | SetRemove + | StdoutWrite + | StringCount + | StringIndexOf + | StringJoin + | StringRepeat + | StringSplit + | TestMockEnvironment + | TypeFieldValue + | TypeMachineFieldValue + | TypeVariantFieldValue => arity == 2, + BytesSlice | FilesystemWriteFile | ListInsertAt | ListSwap | MapInsert | MathClamp + | StringReplace | StringSlice | TypeConstructPut | RandomBounded => arity == 3, + GraphicsRun => arity == 5, + }; + if !accepted_arity || index >= arity { + return None; + } + let view = match id { + Print | Println => false, + BytesLength + | BytesSlice + | BytesToString + | BytesGet + | BytesToHex + | EncodingBase64Encode + | ListLength + | ListGetClone + | ListIsSorted + | ListSum + | MathAverage + | MathMedian + | MapLength + | SetLength + | JsonSerialize + | JsonSerializePublic + | TypeVariantValue + | TypeMachineStateValue + | TypeConstructVariantStart + | TypeConstructMachineStart + | ClockNow + | EnvironmentGet + | EnvironmentArgs + | FilesystemReadFile + | FilesystemWriteFile + | StdoutWrite + | RandomBounded + | RandomBool + | RandomUnitFloat64 + | LogEmit + | GraphicsRun => index == 0, + MapGet + | MapHas + | SetContains + | TypeFieldValue + | TypeVariantFieldValue + | TypeMachineFieldValue + | CryptoHmacSha256 + | SecretCompare => true, + CryptoMd5 | CryptoSha256 | CryptoSha512 | SecretRedact => true, + MapRemove | SetRemove | TypeConstructPut => index == 1, + BitfieldFromBytes + | BitfieldToBytes + | BytesConcat + | BytesFromHex + | BytesFromString + | BytesNew + | CsvParse + | CsvParseWithHeader + | CsvStringify + | EncodingBase64Decode + | EncodingFormDecode + | EncodingFormEncode + | EncodingHexDecode + | EncodingUrlDecode + | EncodingUrlEncode + | Float32FromFloat64 + | Float64FromInt64 + | Float64FromString + | Int64FromFloat64 + | Int64FromString + | JsonParse + | JsonParseExact + | ListAppend + | ListGroupBy + | ListInsertAt + | ListNew + | ListRemoveAt + | ListSort + | ListSortBy + | ListSortByIndex + | ListSwap + | MapFromLists + | MapInsert + | MapNew + | MathAbs + | MathCeil + | MathClamp + | MathCos + | MathE + | MathFactorial + | MathFloor + | MathGcd + | MathKernelAbs + | MathKernelMax + | MathKernelMin + | MathLcm + | MathLog + | MathLog10 + | MathLog2 + | MathMax + | MathMin + | MathMod + | MathPi + | MathPow + | MathRound + | MathSin + | MathSqrt + | MathTan + | Range + | SetAdd + | SetNew + | StringCharCount + | StringChars + | StringCount + | StringFromBool + | StringFromFloat64 + | StringFromInt64 + | StringFromUint64 + | StringIndexOf + | StringIsAlpha + | StringIsNumeric + | StringJoin + | StringLines + | StringLower + | StringRepeat + | StringReplace + | StringSlice + | StringSlugify + | StringSplit + | StringToLowerFirst + | StringToUpperFirst + | StringTrim + | StringTrimEnd + | StringTrimStart + | StringUpper + | StringWords + | TestMockClock + | TestMockEnvironment + | TestMockRandom + | TypeArg + | TypeBitfieldFields + | TypeBitfieldLayout + | TypeConstructFinish + | TypeConstructStart + | TypeFields + | TypeHasSecret + | TypeInfo + | TypeKind + | TypeKindTag + | TypeMachineLayout + | TypeMachineStates + | TypeMachineTransitions + | TypeName + | TypePrimitiveTag + | TypeVariants + | Uint64FromString + | UuidNew => false, + }; + Some(if view { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }) +} diff --git a/crates/jett_typecheck/src/capability.rs b/crates/jett_typecheck/src/capability.rs index 67f4cd7f..040bb7c9 100644 --- a/crates/jett_typecheck/src/capability.rs +++ b/crates/jett_typecheck/src/capability.rs @@ -8,25 +8,11 @@ //! Stdout, Stderr, Stdin, Filesystem, Network, Clock, Random, Process, Environment, Log, Graphics use jett_parser::ast::TypeExpr; - -/// The set of built-in capability type names. -const CAPABILITY_TYPES: &[&str] = &[ - "Stdout", - "Stderr", - "Stdin", - "Filesystem", - "Network", - "Clock", - "Random", - "Process", - "Environment", - "Log", - "Graphics", -]; +use jett_types::CapabilityKind; /// Returns `true` if `name` is one of the built-in capability types. pub fn is_capability_type(name: &str) -> bool { - CAPABILITY_TYPES.contains(&name) + CapabilityKind::from_name(name).is_some() } /// Returns `true` if the given [`TypeExpr`] refers to a capability type. diff --git a/crates/jett_typecheck/src/checker.rs b/crates/jett_typecheck/src/checker.rs index e6df8f03..5a833d46 100644 --- a/crates/jett_typecheck/src/checker.rs +++ b/crates/jett_typecheck/src/checker.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use jett_common::{FileId, Span}; use jett_diagnostics::{Diagnostic, DiagnosticSink}; +use jett_intrinsics::IntrinsicId; use jett_parser::ast::{ self, BinOp, Block, Expr, FunctionDef, Item, Module, Stmt, StringPart, TypeExpr, UnaryOp, VerifyBlock, @@ -10,20 +11,57 @@ use jett_parser::ast::{ use jett_resolve::resolver::ResolveResult; use jett_resolve::scope::{DefId, DefKind}; use jett_types::{ - ActorDef as TypeActorDef, ActorMessageDef, BitfieldDef as TypeBitfieldDef, + ActorDef as TypeActorDef, ActorId, ActorMessageDef, BitfieldDef as TypeBitfieldDef, BitfieldFieldDef as TypeBitfieldFieldDef, BitfieldFieldKind as TypeBitfieldFieldKind, - BitfieldId, EnumDef as TypeEnumDef, FunctionSig, InterfaceDef as TypeInterfaceDef, - MachineDef as TypeMachineDef, MachineId, MachineStateDef as TypeMachineStateDef, - MachineStateId, MachineTransitionDef as TypeMachineTransitionDef, ReflectionBitfieldFieldInfo, + BitfieldId, CapabilityKind, EnumDef as TypeEnumDef, FunctionSig, + InterfaceDef as TypeInterfaceDef, MachineDef as TypeMachineDef, MachineId, + MachineStateDef as TypeMachineStateDef, MachineStateId, + MachineTransitionDef as TypeMachineTransitionDef, ReflectionBitfieldFieldInfo, ReflectionBitfieldInfo, ReflectionFieldInfo, ReflectionMachineInfo, ReflectionMachineStateInfo, ReflectionMachineTransitionInfo, ReflectionMetadata, ReflectionTypeInfo, ReflectionVariantInfo, StructDef as TypeStructDef, StructId, Type, TypeId, TypeInterner, VariantDef, }; +use crate::actor_messages::{ + CheckedActorBindingRole, CheckedActorHandler, CheckedActorHandlerBinding, + CheckedActorHandlerTarget, CheckedActorMessageCall, CheckedActorMessageKind, +}; +use crate::assignment_places::{ + AssignmentContext, AssignmentContextRoot, CheckedAssignmentPlace, CheckedFieldProjection, + CheckedFieldUpdateResourceRequirement, checked_field_owner, seal_assignment_ownership, + validate_assignment_place_shape, +}; +use crate::caller_ownership::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedCallOwnership, CheckedCalleeAccess, + CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax, CheckedIntrinsicOperandRole, + CheckedInvocationShape, CheckedInvocationTarget, CheckedOwnershipContext, + intrinsic_operand_access, +}; use crate::capability; use crate::errors; +use crate::resource_hooks::{CheckedResourceHook, ResourceHookError}; +mod actor_messages; mod graphics_audit; +mod nonzero_facts; + +use nonzero_facts::NonzeroFact; + +#[cfg(test)] +mod list_sum_domain_tests; + +#[cfg(test)] +mod assignment_place_tests; + +#[cfg(test)] +mod resource_copy_tests; + +/// Resource copy authority for an exact field place, not an evaluation mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ResourceProjectionMode { + OwningCopy, + Borrowed, +} /// One accepted concrete generic function body produced by type checking. #[derive(Debug, Clone)] @@ -32,22 +70,149 @@ pub struct CheckedGenericFunctionInstantiation { pub definition: DefId, /// Concrete type arguments in source type-parameter order. pub concrete_args: Vec, + /// Checked source-kind and reflection facts that distinguish concrete + /// bodies whose canonical `TypeId` arguments are otherwise identical. + pub specialization: CheckedGenericSpecialization, /// Fully substituted parameter types. pub parameter_types: Vec, /// Fully substituted return type. pub return_type: TypeId, /// Expression types captured while checking this concrete body. pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, + /// Checked declared type labels, retaining aliases per concrete body. + pub debug_type_names: HashMap, + /// Physical copy/borrow facts, keyed by local declaration-name span. + pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub assignment_places: HashMap, + pub call_ownership: HashMap, + pub actor_message_calls: HashMap, /// Nested generic calls selected while checking this concrete body. pub generic_calls: HashMap, + /// Closed compiler operation selected for each accepted intrinsic call. + pub intrinsic_ids: HashMap, + /// Concrete type operands for accepted compiler-owned generic calls. + /// + /// These operands are not recoverable from the runtime arguments or result + /// type for reflection operations such as `type.name[T]()`. HIR consumes + /// this map instead of resolving source `TypeExpr` syntax again. + pub intrinsic_type_arguments: HashMap>, + /// Source-aware type metadata for reflection calls in this body. + pub intrinsic_reflection_arguments: HashMap>, /// Source arguments normalized to parameter order for calls in this body. pub call_argument_orders: HashMap, /// Concrete source-defined method targets selected in this body. pub method_calls: HashMap, + pub interface_calls: HashMap, + /// Source method bodies or interface slots selected as function values. + pub method_values: HashMap, /// Concrete struct construction targets selected in this body. pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, /// Raw call-result types for pipeline steps before any step-local handle. pub pipeline_step_call_types: HashMap, + /// Checker-owned compile-time control-flow choices for this concrete body. + /// + /// HIR must consume these choices instead of re-evaluating reflection or + /// visiting branches that were deliberately not checked. + pub static_selections: HashMap, + /// Concrete expansions of trusted `comptime type` statements in this body. + pub comptime_type_bindings: HashMap>, +} + +/// Checker facts for one concrete expansion of a `comptime type` body. +#[derive(Debug, Clone)] +pub struct CheckedComptimeTypeBinding { + /// How lowering selects this concrete body. + /// + /// Lowering must not execute every exported reflected-iteration body for + /// every runtime loop iteration. It may statically unroll the reflected + /// loop or emit a compiler-owned dispatch on source-visible reflected type + /// identity (with the iteration index retained for stable ordering). + pub selection: CheckedComptimeTypeSelection, + /// The concrete type bound to the statement's scoped type name. + pub bound_type: TypeId, + /// Source-visible reflection facts, including transparent aliases. + pub reflection: ReflectionTypeInfo, + /// Facts produced while checking the body under `bound_type`. + pub body: CheckedBodyFacts, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedComptimeTypeSelection { + /// A direct trusted initializer such as `type.info[T]()` or + /// `type.arg[T](literal)`; its body executes once at this source point. + Unconditional, + /// The canonical position in a trusted reflected loop source. Exactly the + /// matching body executes for one runtime reflected element. + ReflectedIteration(usize), +} + +/// Typed facts belonging to one checker-selected source-body expansion. +/// +/// The structure is recursive because trusted reflection loops can contain +/// nested `comptime type` statements. Span-keyed top-level maps cannot encode +/// multiple checks of the same source body with different bound types. +#[derive(Debug, Clone, Default)] +pub struct CheckedBodyFacts { + pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, + /// Checked declared type labels, retaining aliases per concrete body. + pub debug_type_names: HashMap, + pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub assignment_places: HashMap, + pub call_ownership: HashMap, + pub actor_message_calls: HashMap, + pub generic_calls: HashMap, + pub intrinsic_ids: HashMap, + pub intrinsic_type_arguments: HashMap>, + pub intrinsic_reflection_arguments: HashMap>, + pub call_argument_orders: HashMap, + pub method_calls: HashMap, + pub interface_calls: HashMap, + pub method_values: HashMap, + pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, + pub pipeline_step_call_types: HashMap, + pub static_selections: HashMap, + pub comptime_type_bindings: HashMap>, +} + +/// Checked initializer ownership, independent of declaration readonly policy. +/// +/// A view records provenance only. It does not select a borrow lifetime or +/// promise that lowering can preserve the view through a value conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedBindingMode { + Owned, + View { source: CheckedViewSource }, +} + +/// Immediate source of a checked borrowed initializer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedViewSource { + /// Session-local backing binding; forwarded aliases are not flattened. + /// A projected initializer retains its typed field path in HIR. + Binding(DefId), + /// A temporary or another expression without a stable binding root. + Other, +} + +/// One compile-time control-flow choice made while checking a concrete generic +/// function body. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedStaticSelection { + IfThen, + IfElseIf(usize), + IfElse, + IfNoBranch, + MatchArm(usize), } /// The concrete generic target selected for one checked call expression. @@ -55,6 +220,19 @@ pub struct CheckedGenericFunctionInstantiation { pub struct CheckedGenericCall { pub definition: DefId, pub concrete_args: Vec, + pub specialization: CheckedGenericSpecialization, +} + +/// Checker-owned facts that distinguish generic bodies after canonical type +/// interning has erased source aliases or other reflection-visible details. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct CheckedGenericSpecialization { + pub type_argument_kinds: Vec, + pub type_argument_reflections: Vec, + pub type_info_kinds: Vec<(usize, String)>, + pub type_info_primitives: Vec<(usize, Option)>, + pub type_kind_values: Vec<(usize, String)>, + pub type_primitive_values: Vec<(usize, String)>, } /// The checked permutation from parameter order to source argument order. @@ -71,7 +249,9 @@ pub struct CheckedMethodDefinition { pub source_span: Span, pub owner_type: TypeId, pub owner_name: String, + pub method_name: String, pub interface_name: Option, + pub interface_type: Option, pub parameter_types: Vec, pub return_type: TypeId, } @@ -82,6 +262,26 @@ pub struct CheckedMethodCall { pub source_span: Span, } +/// Checked interface slot selected when the receiver has an erased interface type. +/// The concrete body is selected from checked implementations at runtime. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedInterfaceCall { + pub interface_type: TypeId, + pub method_index: usize, +} + +/// Checked executable target selected for a qualified method-value expression. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CheckedMethodValue { + /// A concrete method body with its receiver as an explicit parameter. + Source { source_span: Span }, + /// An interface dispatcher selected by the explicit receiver at invocation. + Interface { + interface_type: TypeId, + method_index: usize, + }, +} + /// The concrete struct type selected for a checked construction call. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CheckedStructConstruction { @@ -92,36 +292,174 @@ pub struct CheckedStructConstruction { /// The result of type checking. #[derive(Debug)] pub struct CheckResult { + // Original checked implementation identities for readonly conversion admission. + ordinary_interface_implementations: HashSet<(TypeId, TypeId)>, + pub(crate) actor_check_session: crate::actor_messages::ActorCheckSession, + pub(crate) actor_message_sites: crate::actor_messages::ActorMessageSites, + pub release: bool, + /// Source bindings unavailable for observation after checked consumption. + pub breakpoint_exclusions: HashMap>, /// Diagnostics (errors and warnings) emitted during type checking. pub diagnostics: Vec, /// Map from expression spans to their inferred type. pub type_map: HashMap, + /// Contextually inferred source type before expectation-added outer Secret promotion. + pub source_type_map: HashMap, + /// Checked declared type labels, retaining aliases per concrete body. + pub debug_type_names: HashMap, + /// Physical copy/borrow facts outside generic bodies, by declaration name. + pub binding_modes: HashMap, + pub binding_facts: HashMap, + pub assignment_places: HashMap, + pub call_ownership: HashMap, + pub actor_message_calls: HashMap, /// Session-local resolved definitions and their checked types. /// /// Lowering uses this map with the resolver's `DefId` join keys. Durable /// HIR identity is derived separately from source origin and canonical /// declaration metadata. pub definition_types: HashMap, + pub actor_handlers: HashMap, + /// Privileged exact hook identities; empty for ordinary checking. + pub resource_hooks: HashMap, /// Generic calls made outside generic function bodies, keyed by call span. pub generic_calls: HashMap, + /// Closed compiler operation selected for each accepted intrinsic call. + pub intrinsic_ids: HashMap, + /// Concrete type operands for compiler-owned generic calls outside generic + /// function bodies, keyed by call span. + pub intrinsic_type_arguments: HashMap>, + pub intrinsic_reflection_arguments: HashMap>, /// Source arguments normalized to parameter order, keyed by call span. pub call_argument_orders: HashMap, /// Source-defined method bodies in deterministic declaration order. pub method_definitions: Vec, + /// Exact explicit struct equality targets, independent of expression spans. + pub equality_methods: HashMap, /// Concrete source-defined method targets, keyed by call span. pub method_calls: HashMap, + pub interface_calls: HashMap, + /// Source method bodies or interface slots, keyed by expression span. + pub method_values: HashMap, + /// Source method bodies selected by concrete owner/member lookup. This + /// excludes implementation bodies shadowed by an inherent method. + pub method_value_definitions: HashSet, /// Checked struct construction targets, keyed by call span. pub struct_constructions: HashMap, + /// Physical incoming pipeline types sealed before callee checking. + pub pipeline_step_input_types: HashMap, /// Raw call-result types for pipeline steps before any step-local handle. pub pipeline_step_call_types: HashMap, /// Accepted concrete generic bodies in deterministic discovery order. pub generic_function_instantiations: Vec, + /// Concrete expansions of trusted `comptime type` statements outside + /// generic function bodies. + pub comptime_type_bindings: HashMap>, /// The type interner, containing all types encountered during checking. pub interner: TypeInterner, /// Checked reflection metadata snapshot for comptime reflection builtins. pub reflection_metadata: Arc, } +impl CheckResult { + /// The ordinary checked conversion relation; this grants no new return proof. + pub fn ordinary_types_compatible(&self, expected: TypeId, got: TypeId) -> bool { + ordinary_types_compatible( + &self.interner, + &|pair| self.ordinary_interface_implementations.contains(pair), + expected, + got, + ) + } + + /// Contravariant input admission, retaining exact view and capability types. + /// Return strengthening is independently selected by the reflected plan. + pub fn ordinary_callable_parameters_compatible( + &self, + actual: TypeId, + requested: TypeId, + ) -> bool { + if actual.index() as usize >= self.interner.len() + || requested.index() as usize >= self.interner.len() + { + return false; + } + let ( + Type::Function { + params: source, + view_params: source_views, + .. + }, + Type::Function { + params: target, + view_params: target_views, + .. + }, + ) = ( + self.interner.resolve(actual), + self.interner.resolve(requested), + ) + else { + return false; + }; + source.len() == target.len() + && source_views == target_views + && source + .iter() + .zip(target) + .all(|(source, target)| self.ordinary_types_compatible(*source, *target)) + } +} + +fn ordinary_types_compatible( + types: &TypeInterner, + has_implementation: &impl Fn(&(TypeId, TypeId)) -> bool, + expected: TypeId, + got: TypeId, +) -> bool { + if expected.index() as usize >= types.len() || got.index() as usize >= types.len() { + return false; + } + if expected == got + || expected == TypeInterner::ERROR + || got == TypeInterner::ERROR + || got == TypeInterner::NEVER + { + return true; + } + let compatible = |a, b| ordinary_types_compatible(types, has_implementation, a, b); + match (types.resolve(expected), types.resolve(got)) { + (Type::Interface(_), Type::Interface(_)) => expected == got, + (Type::Interface(_), _) => has_implementation(&(expected, got)), + (Type::Secret(a), Type::Secret(b)) => compatible(*a, *b), + (Type::Secret(a), _) => compatible(*a, got), + (Type::List(a), Type::List(b)) + | (Type::Optional(a), Type::Optional(b)) + | (Type::Set(a), Type::Set(b)) => compatible(*a, *b), + (Type::Map(a, b), Type::Map(c, d)) | (Type::Result(a, b), Type::Result(c, d)) => { + compatible(*a, *c) && compatible(*b, *d) + } + ( + Type::Function { + params: a, + view_params: av, + return_type: ar, + }, + Type::Function { + params: b, + view_params: bv, + return_type: br, + }, + ) => { + a.len() == b.len() + && av == bv + && a.iter().zip(b).all(|(a, b)| compatible(*b, *a)) + && compatible(*ar, *br) + } + (Type::Machine(a), Type::MachineState { machine: b, .. }) => a == b, + _ => false, + } +} /// Mode-specific policy applied while type checking. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CheckOptions { @@ -140,34 +478,221 @@ pub fn check_with_options( resolve: &ResolveResult, options: CheckOptions, ) -> CheckResult { + let mut checker = TypeChecker::new(resolve, options); + let ordinary = resolve.resource_kernels.is_empty(); + if ordinary { + checker.check_module(module); + } else { + checker.sink.emit(Diagnostic::error( + 0, + "compiler resource catalogs require check_with_resource_kernels", + module.span, + )); + } + finish_check(checker, module, options, ordinary) +} + +/// The only checker entry granting validated private resource declaration facts. +pub fn check_with_resource_kernels( + module: &Module, + resolve: &ResolveResult, + options: CheckOptions, +) -> Result { + jett_resolve::validate_resource_kernels(module, resolve)?; + if resolve + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + { + return Err(ResourceHookError::SourceResolutionFailed); + } let mut checker = TypeChecker::new(resolve, options); checker.check_module(module); + if let Some(error) = checker.resource_hook_error.take() { + return Err(error); + } + let checked = finish_check(checker, module, options, true); + crate::resource_hooks::validate_resource_hook_identities(module, resolve, &checked)?; + Ok(checked) +} - let complexity_diagnostics = crate::complexity::check_complexity(module); +fn finish_check( + mut checker: TypeChecker<'_>, + module: &Module, + options: CheckOptions, + checked_bodies: bool, +) -> CheckResult { + let complexity_diagnostics = if checked_bodies { + crate::complexity::check_complexity(module) + } else { + Vec::new() + }; // Run ownership analysis (linear type checking) after type checking. - let ownership_diagnostics = crate::ownership::check_ownership(module, &checker.interner); + let (ownership_diagnostics, breakpoint_exclusions, mut validated_field_updates, actor_reports) = + if checked_bodies { + crate::ownership::OwnershipChecker::new(&checker.interner) + .with_checked_facts( + checker.resolve, + &checker.binding_facts, + &checker.assignment_places, + &checker.call_ownership, + &checker.actor_message_calls, + &checker.type_map, + &checker.comptime_type_bindings, + &checker.generic_function_instantiations, + &checker.actor_handlers, + ) + .check_module_with_field_updates(module) + } else { + (Vec::new(), HashMap::new(), HashSet::new(), HashMap::new()) + }; let reflection_metadata = Arc::new(checker.build_reflection_metadata()); + let equality_methods = checker + .named_types + .get("Equatable") + .map(|interface| { + checker + .interface_method_definitions + .iter() + .filter_map(|((candidate, owner, name), &index)| { + (*candidate == *interface + && name == "equals" + && matches!(checker.interner.resolve(*owner), Type::Struct(_))) + .then_some(( + *owner, + CheckedMethodCall { + source_span: checker.method_definitions[index].source_span, + }, + )) + }) + .collect() + }) + .unwrap_or_default(); let mut diagnostics = checker.sink.into_diagnostics(); diagnostics.extend(complexity_diagnostics); - diagnostics.extend(ownership_diagnostics); + let typed_rebinding_errors = diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.code() == 404) + .map(|diagnostic| (diagnostic.span, diagnostic.code, diagnostic.message.clone())) + .collect::>(); + diagnostics.extend(ownership_diagnostics.into_iter().filter(|diagnostic| { + // The typed boundary check has the precise declaration provenance and + // guidance. Suppress only its exact legacy consume-view counterpart. + !checker + .diagnosed_owned_view_consumptions + .get(&diagnostic.span) + .is_some_and(|previous| { + previous.code == diagnostic.code && previous.message == diagnostic.message + }) + && !(diagnostic.code.code() == 404 + && typed_rebinding_errors.iter().any(|(span, code, message)| { + *span == diagnostic.span + && *code == diagnostic.code + && *message == diagnostic.message + })) + })); + + // An error in any selected body invalidates all admission records, including + // public diagnostics that a downstream caller might later edit or remove. + if diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + { + validated_field_updates.clear(); + } + let program_valid = !diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error); + for (target, handler) in &mut checker.actor_handlers { + if let Some(report) = actor_reports.get(target) { + handler.record_exits(report.exits.clone()); + if program_valid && report.validated { + let body = handler.body_mut(); + seal_assignment_ownership( + &mut body.assignment_places, + &mut body.comptime_type_bindings, + &AssignmentContext { + root: AssignmentContextRoot::ActorHandler(*target), + scopes: Vec::new(), + }, + &validated_field_updates, + ); + handler.authorize_ownership(); + } + } + } + seal_assignment_ownership( + &mut checker.assignment_places, + &mut checker.comptime_type_bindings, + &AssignmentContext::default(), + &validated_field_updates, + ); + for (index, body) in checker + .generic_function_instantiations + .iter_mut() + .enumerate() + { + seal_assignment_ownership( + &mut body.assignment_places, + &mut body.comptime_type_bindings, + &AssignmentContext { + root: AssignmentContextRoot::Generic(index), + scopes: Vec::new(), + }, + &validated_field_updates, + ); + } - CheckResult { + let method_value_definitions = checker + .method_definitions_by_owner + .values() + // This map selects Source bodies by concrete owner/member. Interface + // value slots have no source body and never enter this definition set. + .map(|&index| checker.method_definitions[index].source_span) + .collect(); + + let mut result = CheckResult { + ordinary_interface_implementations: checker.interface_impls.keys().copied().collect(), + release: options.release, + breakpoint_exclusions, diagnostics, type_map: checker.type_map, + source_type_map: checker.source_type_map, + debug_type_names: checker.debug_type_names, + binding_modes: checker.binding_modes, + binding_facts: checker.binding_facts, + assignment_places: checker.assignment_places, + call_ownership: checker.call_ownership, + actor_message_calls: checker.actor_message_calls, + actor_handlers: checker.actor_handlers, + actor_check_session: checker.actor_check_session, + actor_message_sites: crate::actor_messages::ActorMessageSites::default(), definition_types: checker.type_env, + resource_hooks: checker.resource_hooks, generic_calls: checker.generic_calls, + intrinsic_ids: checker.intrinsic_ids, + intrinsic_type_arguments: checker.intrinsic_type_arguments, + intrinsic_reflection_arguments: checker.intrinsic_reflection_arguments, call_argument_orders: checker.call_argument_orders, method_definitions: checker.method_definitions, + equality_methods, method_calls: checker.method_calls, + interface_calls: checker.interface_calls, + method_values: checker.method_values, + method_value_definitions, struct_constructions: checker.struct_constructions, + pipeline_step_input_types: checker.pipeline_step_input_types, pipeline_step_call_types: checker.pipeline_step_call_types, generic_function_instantiations: checker.generic_function_instantiations, + comptime_type_bindings: checker.comptime_type_bindings, interner: checker.interner, reflection_metadata, - } + }; + result.freeze_actor_message_sites(); + result } #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] @@ -215,25 +740,18 @@ impl StaticReflectionEnumValue { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ReflectionBranchContext { - TopLevel, - StaticReflectionBranch, - RuntimeBranch, -} - -impl ReflectionBranchContext { - fn permits_shape_reflection(self) -> bool { - matches!(self, Self::TopLevel | Self::StaticReflectionBranch) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MachineStateTruth { Is, IsNot, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AssignmentBinding { + OwnedMutable, + View, +} + #[derive(Debug, Default)] struct ClosureCaptureScope { /// Definitions declared inside this closure rather than captured by it. @@ -246,11 +764,45 @@ struct ClosureCaptureScope { struct ActiveGenericInstantiation { manifest_index: usize, type_map: HashMap, + source_type_map: HashMap, + debug_type_names: HashMap, + binding_modes: HashMap, + binding_facts: HashMap, + assignment_places: HashMap, + call_ownership: HashMap, + actor_message_calls: HashMap, + invocation_signatures: HashMap, TypeId)>, generic_calls: HashMap, + intrinsic_ids: HashMap, + intrinsic_type_arguments: HashMap>, + intrinsic_reflection_arguments: HashMap>, call_argument_orders: HashMap, method_calls: HashMap, + interface_calls: HashMap, + method_values: HashMap, struct_constructions: HashMap, + pipeline_step_input_types: HashMap, pipeline_step_call_types: HashMap, + static_selections: HashMap, + comptime_type_bindings: HashMap>, +} + +fn merge_static_selections( + destination: &mut HashMap, + incoming: HashMap, +) -> Vec { + let mut conflicts = Vec::new(); + for (span, selection) in incoming { + match destination.get(&span) { + Some(existing) if *existing != selection => conflicts.push(span), + Some(_) => {} + None => { + destination.insert(span, selection); + } + } + } + conflicts.sort_by_key(|span| (span.file.index(), span.start, span.end)); + conflicts } // --------------------------------------------------------------------------- @@ -264,6 +816,8 @@ struct TypeChecker<'a> { sink: DiagnosticSink, /// DefId → TypeId for variables, parameters, and functions. type_env: HashMap, + resource_hooks: HashMap, + resource_hook_error: Option, /// Declaration span → DefId for locally declared names. decl_defs: HashMap, /// User-defined type name → TypeId. @@ -274,6 +828,10 @@ struct TypeChecker<'a> { resolving_type_aliases: HashSet, /// Expression span → TypeId (the output type map). type_map: HashMap, + source_type_map: HashMap, + debug_type_names: HashMap, + /// Source declarations retain nested aliases when inferring generic witnesses. + declared_source_types: HashMap, /// The expected return type for the function currently being checked. current_return_type: Option, /// (interface, concrete type) -> implemented method signatures. @@ -307,16 +865,28 @@ struct TypeChecker<'a> { trusted_stdlib_function_signatures: HashMap, TypeId)>, /// Name of the function currently being type-checked (None outside functions). current_function_name: Option, + constant_declarations: HashSet, + /// Declaration identity preserves assignment policy across scopes and + /// generic rechecks. Other bindings are immutable by default. + assignment_bindings: HashMap, + /// Exact legacy consume-view diagnostics replaced by typed boundary errors. + diagnosed_owned_view_consumptions: HashMap, + diagnosed_caller_view_arguments: HashSet, + /// A generic recheck may revisit the same invalid owning payload source. + diagnosed_resource_payload_views: HashSet, + /// A written return annotation may be resolved in several specializations. + diagnosed_view_return_annotations: HashSet, /// Whether the function currently being type-checked is pure. current_function_pure: bool, /// Whether we are inside a verify block. in_verify_block: bool, + ownership_context: CheckedOwnershipContext, /// Whether we are inside a property block. in_property_block: bool, /// Nesting depth inside an explicit `comptime` expression. comptime_expr_depth: usize, - /// Lexically visible proofs that integer bindings are nonzero. - nonzero_fact_scopes: Vec>, + /// Lexically visible nonzero observations of exact bindings and field places. + nonzero_fact_scopes: Vec>, /// The name of the verify block currently being checked (for error messages). current_verify_name: Option, /// Nesting depth inside a handle-block body. Used to validate `default`. @@ -343,6 +913,8 @@ struct TypeChecker<'a> { reflection_variants_by_id: HashMap)>, /// Active type variable substitution during monomorphization (type_param_name → TypeId). type_var_subst: HashMap, + /// Source-aware metadata for the concrete generic type parameters. + type_var_reflections: HashMap, /// Source-level reflected kind tags for active type variables. This keeps /// simple aliases visible to `type.kind_tag[T]()` while their TypeId may /// resolve to the alias base type. @@ -360,9 +932,9 @@ struct TypeChecker<'a> { /// reflected primitive facts. reflection_type_primitive_value_scopes: Vec>, /// Trusted field types currently available from direct `type.fields[T]()` loops. - reflected_field_type_scopes: Vec>>, + reflected_field_type_scopes: Vec>>, /// Trusted TypeInfo types currently available from direct reflected `args` loops. - reflected_type_info_scopes: Vec>>, + reflected_type_info_scopes: Vec>>, /// Trusted TypeVariant owners currently available from direct `type.variants[T]()` loops. reflected_variant_type_scopes: Vec>, /// Trusted TypeMachineState owners currently available from direct `type.machine_states[T]()` loops. @@ -373,27 +945,50 @@ struct TypeChecker<'a> { generic_function_templates: HashMap, /// Generic function instantiations whose bodies have already been checked. checked_generic_function_instantiations: - HashSet<(String, Vec, Vec, ReflectionParamFacts)>, + HashSet<(String, Vec, CheckedGenericSpecialization)>, /// Canonical concrete identity to its deterministic manifest slot. - generic_instantiation_indices: HashMap<(DefId, Vec), usize>, + generic_instantiation_indices: + HashMap<(DefId, Vec, CheckedGenericSpecialization), usize>, /// Ordered, concrete checked-program handoff consumed by HIR. generic_function_instantiations: Vec, /// Concrete generic calls made outside a generic function body. generic_calls: HashMap, + /// Closed compiler operations selected outside generic function bodies. + intrinsic_ids: HashMap, + /// Concrete type operands for compiler-owned generic calls outside a + /// generic function body. + intrinsic_type_arguments: HashMap>, + intrinsic_reflection_arguments: HashMap>, /// Checked source-to-parameter permutations outside generic bodies. call_argument_orders: HashMap, /// Source method bodies exported to HIR. method_definitions: Vec, + /// Exact source-formal access selected with each checked method body. + method_parameter_modes: HashMap>, /// (owner type, method name) -> exported method definition index. method_definitions_by_owner: HashMap<(TypeId, String), usize>, /// (interface type, owner type, method name) -> definition index. interface_method_definitions: HashMap<(TypeId, TypeId, String), usize>, /// Checked method calls outside generic bodies. method_calls: HashMap, + interface_calls: HashMap, + /// Checked source method values outside generic bodies. + method_values: HashMap, + /// Physical local binding modes outside generic bodies. + binding_modes: HashMap, + binding_facts: HashMap, + assignment_places: HashMap, + call_ownership: HashMap, + actor_message_calls: HashMap, + invocation_signatures: HashMap, TypeId)>, /// Checked struct constructions outside generic bodies. struct_constructions: HashMap, + /// Physical incoming pipeline types outside generic bodies. + pipeline_step_input_types: HashMap, /// Raw pipeline call-result types outside generic bodies. pipeline_step_call_types: HashMap, + /// Trusted `comptime type` expansions outside generic function bodies. + comptime_type_bindings: HashMap>, /// Per-instantiation facts currently being collected. Nested generic body /// checks push another scope so their spans never overwrite the caller's. active_generic_instantiations: Vec, @@ -402,6 +997,10 @@ struct TypeChecker<'a> { specialize_reflection_branches: bool, // -- Actor support -- + actor_check_session: crate::actor_messages::ActorCheckSession, + actor_definitions: HashMap, + actor_message_modes: HashMap>, + actor_handlers: HashMap, /// The expected `responds T` type for the receive handler being checked. /// `None` when not inside a receive handler. current_respond_type: Option, @@ -413,6 +1012,7 @@ impl<'a> TypeChecker<'a> { .scope_table .definitions .iter() + .filter(|def| !resolve.resource_kernels.contains_definition(def.id)) .map(|def| (def.span, def.id)) .collect(); @@ -422,12 +1022,17 @@ impl<'a> TypeChecker<'a> { options, sink: DiagnosticSink::new(), type_env: HashMap::new(), + resource_hooks: HashMap::new(), + resource_hook_error: None, decl_defs, named_types: HashMap::new(), trusted_stdlib_named_types: HashMap::new(), type_aliases: HashMap::new(), resolving_type_aliases: HashSet::new(), type_map: HashMap::new(), + source_type_map: HashMap::new(), + debug_type_names: HashMap::new(), + declared_source_types: HashMap::new(), current_return_type: None, interface_impls: HashMap::new(), impl_methods_by_type: HashMap::new(), @@ -445,8 +1050,15 @@ impl<'a> TypeChecker<'a> { graphics_authority_params: HashSet::new(), trusted_stdlib_function_signatures: HashMap::new(), current_function_name: None, + constant_declarations: HashSet::new(), + assignment_bindings: HashMap::new(), + diagnosed_owned_view_consumptions: HashMap::new(), + diagnosed_caller_view_arguments: HashSet::new(), + diagnosed_resource_payload_views: HashSet::new(), + diagnosed_view_return_annotations: HashSet::new(), current_function_pure: false, in_verify_block: false, + ownership_context: CheckedOwnershipContext::Ordinary, in_property_block: false, comptime_expr_depth: 0, nonzero_fact_scopes: Vec::new(), @@ -462,6 +1074,7 @@ impl<'a> TypeChecker<'a> { reflection_machines_by_id: HashMap::new(), reflection_variants_by_id: HashMap::new(), type_var_subst: HashMap::new(), + type_var_reflections: HashMap::new(), type_var_kind_tags: HashMap::new(), reflection_type_info_kind_scopes: Vec::new(), reflection_type_info_primitive_scopes: Vec::new(), @@ -476,16 +1089,34 @@ impl<'a> TypeChecker<'a> { generic_instantiation_indices: HashMap::new(), generic_function_instantiations: Vec::new(), generic_calls: HashMap::new(), + intrinsic_ids: HashMap::new(), + intrinsic_type_arguments: HashMap::new(), + intrinsic_reflection_arguments: HashMap::new(), call_argument_orders: HashMap::new(), method_definitions: Vec::new(), + method_parameter_modes: HashMap::new(), method_definitions_by_owner: HashMap::new(), interface_method_definitions: HashMap::new(), method_calls: HashMap::new(), + interface_calls: HashMap::new(), + method_values: HashMap::new(), + binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + assignment_places: HashMap::new(), + call_ownership: HashMap::new(), + actor_message_calls: HashMap::new(), + invocation_signatures: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), + comptime_type_bindings: HashMap::new(), active_generic_instantiations: Vec::new(), specialize_reflection_branches: false, current_respond_type: None, + actor_check_session: crate::actor_messages::ActorCheckSession::new(), + actor_definitions: HashMap::new(), + actor_message_modes: HashMap::new(), + actor_handlers: HashMap::new(), }; checker.install_builtin_metadata_types(); checker @@ -511,6 +1142,7 @@ impl<'a> TypeChecker<'a> { "machine_type", "machine_state_type", "unknown_type", + "resource_type", ]), }); let type_kind_ty = self.interner.intern(Type::Enum(type_kind_eid)); @@ -550,7 +1182,7 @@ impl<'a> TypeChecker<'a> { self.named_types .insert("TypeBitfieldFieldShape".to_string(), bitfield_shape_ty); - let type_info_sid = self.interner.add_struct(TypeStructDef { + let type_info_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeInfo".to_string(), fields: Vec::new(), methods: Vec::new(), @@ -577,7 +1209,7 @@ impl<'a> TypeChecker<'a> { .insert("TypeInfo".to_string(), type_info_ty); let optional_string_ty = self.interner.intern(Type::Optional(TypeInterner::STRING)); - let type_field_sid = self.interner.add_struct(TypeStructDef { + let type_field_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeField".to_string(), fields: vec![ ("index".to_string(), TypeInterner::INT64), @@ -598,7 +1230,7 @@ impl<'a> TypeChecker<'a> { .insert("TypeField".to_string(), type_field_ty); let optional_type_info_ty = self.interner.intern(Type::Optional(type_info_ty)); - let type_bitfield_field_sid = self.interner.add_struct(TypeStructDef { + let type_bitfield_field_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeBitfieldField".to_string(), fields: vec![ ("index".to_string(), TypeInterner::INT64), @@ -616,7 +1248,7 @@ impl<'a> TypeChecker<'a> { .insert("TypeBitfieldField".to_string(), type_bitfield_field_ty); let type_bitfield_fields_ty = self.interner.intern(Type::List(type_bitfield_field_ty)); - let type_bitfield_sid = self.interner.add_struct(TypeStructDef { + let type_bitfield_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeBitfield".to_string(), fields: vec![ ("network_order".to_string(), TypeInterner::BOOL), @@ -629,7 +1261,7 @@ impl<'a> TypeChecker<'a> { .insert("TypeBitfield".to_string(), type_bitfield_ty); let type_variant_fields_ty = self.interner.intern(Type::List(type_field_ty)); - let type_variant_sid = self.interner.add_struct(TypeStructDef { + let type_variant_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeVariant".to_string(), fields: vec![ ("index".to_string(), TypeInterner::INT64), @@ -646,7 +1278,7 @@ impl<'a> TypeChecker<'a> { .insert("TypeVariant".to_string(), type_variant_ty); let type_machine_state_fields_ty = self.interner.intern(Type::List(type_field_ty)); - let type_machine_state_sid = self.interner.add_struct(TypeStructDef { + let type_machine_state_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeMachineState".to_string(), fields: vec![ ("index".to_string(), TypeInterner::INT64), @@ -661,17 +1293,18 @@ impl<'a> TypeChecker<'a> { self.named_types .insert("TypeMachineState".to_string(), type_machine_state_ty); - let type_machine_transition_sid = self.interner.add_struct(TypeStructDef { - name: "TypeMachineTransition".to_string(), - fields: vec![ - ("index".to_string(), TypeInterner::INT64), - ("source_index".to_string(), TypeInterner::INT64), - ("source".to_string(), TypeInterner::STRING), - ("target_index".to_string(), TypeInterner::INT64), - ("target".to_string(), TypeInterner::STRING), - ], - methods: Vec::new(), - }); + let type_machine_transition_sid = + self.interner.add_compiler_metadata_struct(TypeStructDef { + name: "TypeMachineTransition".to_string(), + fields: vec![ + ("index".to_string(), TypeInterner::INT64), + ("source_index".to_string(), TypeInterner::INT64), + ("source".to_string(), TypeInterner::STRING), + ("target_index".to_string(), TypeInterner::INT64), + ("target".to_string(), TypeInterner::STRING), + ], + methods: Vec::new(), + }); let type_machine_transition_ty = self .interner .intern(Type::Struct(type_machine_transition_sid)); @@ -683,7 +1316,7 @@ impl<'a> TypeChecker<'a> { let type_machine_states_ty = self.interner.intern(Type::List(type_machine_state_ty)); let type_machine_transitions_ty = self.interner.intern(Type::List(type_machine_transition_ty)); - let type_machine_sid = self.interner.add_struct(TypeStructDef { + let type_machine_sid = self.interner.add_compiler_metadata_struct(TypeStructDef { name: "TypeMachine".to_string(), fields: vec![ ("states".to_string(), type_machine_states_ty), @@ -729,6 +1362,8 @@ impl<'a> TypeChecker<'a> { Type::Bytes => "bytes".to_string(), Type::Nothing => "nothing".to_string(), Type::TypeConstruction => "TypeConstruction".to_string(), + Type::Never => "".to_string(), + Type::Capability(kind) => kind.name().to_string(), Type::List(inner) => format!("list[{}]", self.type_name(*inner)), Type::Map(k, v) => format!("map[{}, {}]", self.type_name(*k), self.type_name(*v)), Type::Set(inner) => format!("set[{}]", self.type_name(*inner)), @@ -752,9 +1387,17 @@ impl<'a> TypeChecker<'a> { } Type::Function { params, + view_params, return_type, } => { - let params: Vec = params.iter().map(|p| self.type_name(*p)).collect(); + let params: Vec = params + .iter() + .zip(view_params) + .map(|(param, view)| { + let name = self.type_name(*param); + if *view { format!("view {name}") } else { name } + }) + .collect(); format!( "function({}) returns {}", params.join(", "), @@ -949,20 +1592,6 @@ impl<'a> TypeChecker<'a> { .is_some_and(|ty| matches!(self.interner.resolve(*ty), Type::Bitfield(_))) } - fn is_reflection_metadata_type_name(name: &str) -> bool { - matches!( - name, - "TypeInfo" - | "TypeField" - | "TypeBitfield" - | "TypeBitfieldField" - | "TypeMachine" - | "TypeMachineState" - | "TypeMachineTransition" - | "TypeVariant" - ) - } - /// Returns true if the type is numeric (any integer or float type). fn is_numeric(&self, id: TypeId) -> bool { matches!( @@ -1091,15 +1720,17 @@ impl<'a> TypeChecker<'a> { | Expr::Paren(inner, _) | Expr::Comptime(inner, _) | Expr::Coarsen(inner, _) => { - let inner_ty = self.type_map.get(&inner.span()).copied().unwrap_or(ty); + let inner_ty = self.checked_expression_type(inner.span()).unwrap_or(ty); self.expression_is_proven_nonzero(inner, inner_ty) } - Expr::Ident(ident) => self.ident_def_id(ident).is_some_and(|def_id| { - self.nonzero_fact_scopes - .iter() - .rev() - .any(|scope| scope.contains(&def_id)) - }), + Expr::Ident(_) | Expr::FieldAccess(_, _, _) => { + self.nonzero_fact(expr).is_some_and(|fact| { + self.nonzero_fact_scopes + .iter() + .rev() + .any(|scope| scope.contains(&fact)) + }) + } _ => false, } } @@ -1168,6 +1799,7 @@ impl<'a> TypeChecker<'a> { | Type::String | Type::Bool | Type::Nothing + | Type::Never | Type::Error => false, Type::Refinement { base, .. } => self.json_read_requires_view(*base), _ => true, @@ -1233,7 +1865,9 @@ impl<'a> TypeChecker<'a> { } fn type_requires_handle_error(&self, expected: TypeId, got: TypeId) -> bool { - if matches!(self.interner.resolve(expected), Type::Result(_, _)) { + if self.types_compatible(expected, got) + || matches!(self.interner.resolve(expected), Type::Result(_, _)) + { return false; } match self.interner.resolve(got) { @@ -1243,10 +1877,12 @@ impl<'a> TypeChecker<'a> { } fn type_requires_bare_handle(&self, expected: TypeId, got: TypeId) -> bool { - if matches!( - self.interner.resolve(expected), - Type::Result(_, _) | Type::Optional(_) - ) { + if self.types_compatible(expected, got) + || matches!( + self.interner.resolve(expected), + Type::Result(_, _) | Type::Optional(_) + ) + { return false; } match self.interner.resolve(got) { @@ -1376,6 +2012,31 @@ impl<'a> TypeChecker<'a> { !Self::is_impure_builtin(name) && self.purity_map.get(name).copied().unwrap_or(true) } + fn check_call_purity(&mut self, callee_name: &str, span: Span) { + if self.current_function_pure + && let Some(caller_name) = &self.current_function_name + { + self.sink + .emit(errors::pure_calls_impure(caller_name, callee_name, span)); + } + if self.in_verify_block + && let Some(verify_name) = &self.current_verify_name + { + self.sink + .emit(errors::verify_calls_impure(verify_name, callee_name, span)); + } + if self.comptime_expr_depth > 0 { + self.sink + .emit(errors::comptime_calls_impure(callee_name, span)); + } + } + + fn function_parameters_are_pure(&self, params: &[TypeId]) -> bool { + params + .iter() + .all(|param| !matches!(self.interner.resolve(*param), Type::Capability(_))) + } + fn is_secret_safe_builtin(name: &str) -> bool { matches!(name, "secret.redact" | "secret.compare") } @@ -1461,6 +2122,13 @@ impl<'a> TypeChecker<'a> { } } + fn check_resource_copy(&mut self, ty: TypeId, span: Span) { + if self.type_contains_resource_data(ty) { + self.sink + .emit(errors::resource_cannot_be_cloned(&self.type_name(ty), span)); + } + } + fn type_contains_resource_data(&self, ty: TypeId) -> bool { let mut visited = HashSet::new(); self.type_contains_resource_data_inner(ty, &mut visited) @@ -1555,6 +2223,28 @@ impl<'a> TypeChecker<'a> { } fn build_reflection_metadata(&mut self) -> ReflectionMetadata { + fn source_operands( + metadata: &mut ReflectionMetadata, + types: &HashMap>, + reflection: &HashMap>, + bindings: &HashMap>, + ) { + for (span, infos) in reflection { + if let Some(args) = types.get(span) { + for (ty, info) in args.iter().zip(infos) { + metadata.insert_source_type_info(*ty, info.clone()); + } + } + } + for binding in bindings.values().flatten() { + source_operands( + metadata, + &binding.body.intrinsic_type_arguments, + &binding.body.intrinsic_reflection_arguments, + &binding.body.comptime_type_bindings, + ); + } + } let mut metadata = ReflectionMetadata::new(); let type_ids = self.interner.type_ids().collect::>(); @@ -1592,6 +2282,20 @@ impl<'a> TypeChecker<'a> { metadata.insert_type_variants_for_id(type_id, type_name, variants); } + source_operands( + &mut metadata, + &self.intrinsic_type_arguments, + &self.intrinsic_reflection_arguments, + &self.comptime_type_bindings, + ); + for instance in &self.generic_function_instantiations { + source_operands( + &mut metadata, + &instance.intrinsic_type_arguments, + &instance.intrinsic_reflection_arguments, + &instance.comptime_type_bindings, + ); + } metadata } @@ -1679,7 +2383,11 @@ impl<'a> TypeChecker<'a> { Type::Machine(_) => "machine", Type::MachineState { .. } => "machine_state", Type::Resource(_) => "resource", - Type::Interface(_) | Type::Actor(_) | Type::Error => "unknown", + Type::Interface(_) + | Type::Actor(_) + | Type::Capability(_) + | Type::Never + | Type::Error => "unknown", } } @@ -1714,7 +2422,11 @@ impl<'a> TypeChecker<'a> { Type::Machine(_) => "machine_type", Type::MachineState { .. } => "machine_state_type", Type::Resource(_) => "resource_type", - Type::Interface(_) | Type::Actor(_) | Type::Error => "unknown_type", + Type::Interface(_) + | Type::Actor(_) + | Type::Capability(_) + | Type::Never + | Type::Error => "unknown_type", } } @@ -2033,6 +2745,11 @@ impl<'a> TypeChecker<'a> { if let TypeExpr::View(inner, _) = ty { return self.reflection_type_info_for_type_expr(inner, namespace, resolved_ty); } + if let TypeExpr::Named(ident) = ty + && let Some(info) = self.type_var_reflections.get(&ident.name) + { + return info.clone(); + } let type_name = self.reflection_type_expr_display(ty, namespace); let args = self.reflection_type_info_args_for_type_expr(ty, namespace, resolved_ty); @@ -2163,7 +2880,11 @@ impl<'a> TypeChecker<'a> { fn reflection_type_expr_display(&self, ty: &TypeExpr, namespace: Option<&str>) -> String { match ty { - TypeExpr::Named(ident) => self.reflection_type_name_in_namespace(ident, namespace), + TypeExpr::Named(ident) => self + .type_var_reflections + .get(&ident.name) + .map(|info| info.type_name.clone()) + .unwrap_or_else(|| self.reflection_type_name_in_namespace(ident, namespace)), TypeExpr::Generic(ident, args, _) => { let args = args .iter() @@ -2202,55 +2923,397 @@ impl<'a> TypeChecker<'a> { } } - fn reflection_type_name_in_namespace( - &self, - ident: &ast::Ident, - namespace: Option<&str>, - ) -> String { - if ident.name.contains('.') { - return ident.name.clone(); - } - if Self::reflection_primitive_tag_for_name(&ident.name).is_some() { - return ident.name.clone(); - } - if let Some(namespace) = namespace { - let qualified = format!("{namespace}.{}", ident.name); - if self.reflection_type_name_is_registered(&qualified) { - return qualified; - } - } - ident.name.clone() + fn reflection_arguments_for_type_args( + &mut self, + type_args: &[TypeExpr], + concrete_args: &[TypeId], + ) -> Vec { + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + type_args + .iter() + .zip(concrete_args) + .map(|(source, &ty)| { + self.reflection_type_info_for_type_expr(source, namespace.as_deref(), ty) + }) + .collect() } - fn reflection_type_name_is_registered(&self, name: &str) -> bool { - self.named_types.contains_key(name) - || self.type_aliases.contains_key(name) - || self.generic_struct_templates.contains_key(name) + fn reflection_arguments_for_inferred_types( + &self, + concrete_args: &[TypeId], + ) -> Vec { + concrete_args + .iter() + .map(|&ty| self.reflection_type_info_for_type(ty)) + .collect() } - fn secret_field_names(&self, ty: TypeId) -> Vec { - match self.interner.resolve(ty) { - Type::Struct(sid) => self - .interner - .resolve_struct(*sid) - .fields - .iter() - .filter(|(_, field_ty)| self.type_contains_secret_data(*field_ty)) - .map(|(name, _)| name.clone()) - .collect(), - Type::Bitfield(bid) => self - .interner - .resolve_bitfield(*bid) - .fields - .iter() - .filter(|field| self.type_contains_secret_data(field.ty)) - .map(|field| field.name.clone()) - .collect(), - _ => Vec::new(), + fn inferred_argument_reflection( + &mut self, + expression: Option<&Expr>, + actual: TypeId, + ) -> ReflectionTypeInfo { + let source = expression.and_then(|expression| { + let mut expression = expression; + while let Expr::View(inner, _) | Expr::Paren(inner, _) = expression { + expression = inner; + } + if let Expr::Ident(ident) = expression + && let Some(definition) = self.ident_def_id(ident) + && let Some(ty) = self.declared_source_types.get(&definition) + { + return Some(( + ty.clone(), + self.resolve.scope_table.def(definition).namespace.clone(), + )); + } + if let Expr::InlineFn(params, return_type, _, span) = expression { + return Some(( + Self::source_callback_type(params, return_type.as_ref(), *span), + self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }), + )); + } + let name = self.resolved_expr_name(expression)?; + let function = self.graphics_callback_definitions.get(&name)?; + if !function.type_params.is_empty() { + return None; + } + Some(( + Self::source_callback_type( + &function.params, + function.return_type.as_ref(), + function.span, + ), + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()), + )) + }); + let mut info = match source { + Some((source, namespace)) => { + self.reflection_type_info_for_type_expr(&source, namespace.as_deref(), actual) + } + None => self.reflection_type_info_for_type(actual), + }; + // The reference inference peels a whole argument's aliases, then keeps + // aliases encountered inside its list, sum or callable signature. + while info.kind == "alias" && info.args.len() == 1 { + info = info.args.remove(0); } + info } - fn json_non_string_map_key_types(&self, ty: TypeId) -> Vec { + fn source_callback_type( + params: &[ast::Param], + return_type: Option<&TypeExpr>, + span: Span, + ) -> TypeExpr { + TypeExpr::Function( + params + .iter() + .map(|param| { + if param.view && !matches!(param.ty, TypeExpr::View(_, _)) { + TypeExpr::View(Box::new(param.ty.clone()), param.span) + } else { + param.ty.clone() + } + }) + .collect(), + Box::new(return_type.cloned().unwrap_or_else(|| { + TypeExpr::Named(ast::Ident { + name: "nothing".into(), + span, + }) + })), + span, + ) + } + + fn reflection_arguments_for_inferred_call( + &mut self, + template: &FunctionDef, + concrete_args: &[TypeId], + actual_types: &[TypeId], + expressions: &[Option<&Expr>], + ) -> Vec { + let parameters = template + .type_params + .iter() + .map(|parameter| parameter.name.as_str()) + .collect::>(); + let mut witnesses = HashMap::new(); + for ((parameter, &actual), expression) in + template.params.iter().zip(actual_types).zip(expressions) + { + let info = self.inferred_argument_reflection(*expression, actual); + self.collect_inferred_reflection( + ¶meter.ty, + actual, + info, + ¶meters, + &mut witnesses, + ); + } + template + .type_params + .iter() + .zip(concrete_args) + .map(|(parameter, &concrete)| { + let Some((source, info)) = witnesses.remove(¶meter.name) else { + return self.reflection_type_info_for_type(concrete); + }; + if source == concrete { + info + } else if self.merge_inferred_never(source, concrete, false) == Some(concrete) { + let contextual = self.reflection_type_info_for_type(concrete); + self.merge_inferred_reflection(source, concrete, concrete, info, contextual) + } else { + self.reflection_type_info_for_type(concrete) + } + }) + .collect() + } + + fn apply_inferred_reflection_kinds( + template: &FunctionDef, + reflections: &[ReflectionTypeInfo], + kinds: &mut HashMap, + ) { + for (parameter, reflection) in template.type_params.iter().zip(reflections) { + kinds.insert( + parameter.name.clone(), + ReflectionTypeInfo::kind_tag_variant(&reflection.kind).to_string(), + ); + } + } + + fn collect_inferred_reflection( + &mut self, + expected: &TypeExpr, + actual: TypeId, + info: ReflectionTypeInfo, + parameters: &HashSet<&str>, + witnesses: &mut HashMap, + ) { + match expected { + TypeExpr::Named(parameter) if parameters.contains(parameter.name.as_str()) => { + match witnesses.remove(¶meter.name) { + Some((first, first_info)) => { + let merged = self + .merge_inferred_never(first, actual, false) + .unwrap_or(first); + let info = + self.merge_inferred_reflection(first, actual, merged, first_info, info); + witnesses.insert(parameter.name.clone(), (merged, info)); + } + None => { + witnesses.insert(parameter.name.clone(), (actual, info)); + } + } + } + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + self.collect_inferred_reflection(inner, actual, info, parameters, witnesses); + } + TypeExpr::Generic(owner, args, _) + if owner.name == self.reflection_kind_for_type(actual) => + { + let actual_args = self.type_info_arg_types_for_type(actual); + let infos = self.inferred_child_reflections(actual, &info); + for ((expected, actual), info) in args.iter().zip(actual_args).zip(infos) { + self.collect_inferred_reflection(expected, actual, info, parameters, witnesses); + } + } + TypeExpr::Function(params, result, _) => { + let Type::Function { + params: actual_params, + return_type, + .. + } = self.interner.resolve(actual).clone() + else { + return; + }; + let mut infos = self.inferred_child_reflections(actual, &info).into_iter(); + for (expected, actual) in params.iter().zip(actual_params) { + let info = infos + .next() + .unwrap_or_else(|| self.reflection_type_info_for_type(actual)); + self.collect_inferred_reflection(expected, actual, info, parameters, witnesses); + } + let info = infos + .next() + .unwrap_or_else(|| self.reflection_type_info_for_type(return_type)); + self.collect_inferred_reflection(result, return_type, info, parameters, witnesses); + } + _ => {} + } + } + + fn inferred_child_reflections( + &self, + ty: TypeId, + info: &ReflectionTypeInfo, + ) -> Vec { + let children = self.type_info_arg_types_for_type(ty); + if info.kind == self.reflection_kind_for_type(ty) && info.args.len() == children.len() { + info.args.clone() + } else { + children + .into_iter() + .map(|ty| self.reflection_type_info_for_type(ty)) + .collect() + } + } + + fn merge_inferred_reflection( + &self, + first: TypeId, + next: TypeId, + merged: TypeId, + first_info: ReflectionTypeInfo, + next_info: ReflectionTypeInfo, + ) -> ReflectionTypeInfo { + if merged == first { + return first_info; + } + if first == TypeInterner::NEVER && merged == next { + return next_info; + } + let children = self.type_info_arg_types_for_type(merged); + let first_children = self.type_info_arg_types_for_type(first); + let next_children = self.type_info_arg_types_for_type(next); + if children.len() != first_children.len() || children.len() != next_children.len() { + return self.reflection_type_info_for_type(merged); + } + let args = children + .into_iter() + .zip(first_children.into_iter().zip(next_children)) + .zip( + self.inferred_child_reflections(first, &first_info) + .into_iter() + .zip(self.inferred_child_reflections(next, &next_info)), + ) + .map(|((merged, (first, next)), (first_info, next_info))| { + self.merge_inferred_reflection(first, next, merged, first_info, next_info) + }) + .collect::>(); + let name = self.inferred_reflection_display(merged, &args); + self.reflection_type_info_for_type_named_with_args(merged, name, args) + } + + fn inferred_reflection_display(&self, ty: TypeId, args: &[ReflectionTypeInfo]) -> String { + let names = args + .iter() + .map(|argument| argument.type_name.as_str()) + .collect::>(); + match self.interner.resolve(ty) { + Type::List(_) + | Type::Set(_) + | Type::Map(_, _) + | Type::Optional(_) + | Type::Result(_, _) + | Type::Secret(_) => { + format!( + "{}[{}]", + self.reflection_kind_for_type(ty), + names.join(", ") + ) + } + Type::Function { view_params, .. } => { + let Some((result, params)) = names.split_last() else { + return self.type_name(ty); + }; + let params = params + .iter() + .zip(view_params) + .map(|(name, view)| { + if *view { + format!("view {name}") + } else { + (*name).to_string() + } + }) + .collect::>(); + format!("function({}) returns {result}", params.join(", ")) + } + _ => self.type_name(ty), + } + } + + fn reflection_type_name_in_namespace( + &self, + ident: &ast::Ident, + namespace: Option<&str>, + ) -> String { + let expanded = self.resolved_or_expanded_name(&ident.name, ident.span); + if self.reflection_type_name_is_registered(&expanded) { + return expanded; + } + if let Some(definition) = self.resolve.resolutions.get(&ident.span) { + let resolved = self.resolve.scope_table.def(*definition); + if matches!( + resolved.kind, + DefKind::Type + | DefKind::Struct + | DefKind::Enum + | DefKind::Bitfield + | DefKind::Interface + | DefKind::Machine + | DefKind::Actor + | DefKind::Resource + ) && self.reflection_type_name_is_registered(&resolved.name) + { + return resolved.name.clone(); + } + } + if ident.name.contains('.') { + return ident.name.clone(); + } + if Self::reflection_primitive_tag_for_name(&ident.name).is_some() { + return ident.name.clone(); + } + if let Some(namespace) = namespace { + let qualified = format!("{namespace}.{}", ident.name); + if self.reflection_type_name_is_registered(&qualified) { + return qualified; + } + } + ident.name.clone() + } + + fn reflection_type_name_is_registered(&self, name: &str) -> bool { + self.named_types.contains_key(name) + || self.type_aliases.contains_key(name) + || self.generic_struct_templates.contains_key(name) + } + + fn secret_field_names(&self, ty: TypeId) -> Vec { + match self.interner.resolve(ty) { + Type::Struct(sid) => self + .interner + .resolve_struct(*sid) + .fields + .iter() + .filter(|(_, field_ty)| self.type_contains_secret_data(*field_ty)) + .map(|(name, _)| name.clone()) + .collect(), + Type::Bitfield(bid) => self + .interner + .resolve_bitfield(*bid) + .fields + .iter() + .filter(|field| self.type_contains_secret_data(field.ty)) + .map(|field| field.name.clone()) + .collect(), + _ => Vec::new(), + } + } + + fn json_non_string_map_key_types(&self, ty: TypeId) -> Vec { let mut visited = HashSet::new(); let mut key_types = HashSet::new(); let mut keys = Vec::new(); @@ -2325,10 +3388,12 @@ impl<'a> TypeChecker<'a> { | Type::Bool | Type::Bytes | Type::Nothing + | Type::Never | Type::Error => {} Type::TypeConstruction | Type::Interface(_) | Type::Actor(_) + | Type::Capability(_) | Type::Resource(_) | Type::Function { .. } => { self.push_json_unsupported_type(ty, unsupported_types, unsupported); @@ -2644,96 +3709,458 @@ impl<'a> TypeChecker<'a> { } } - fn types_compatible(&self, expected: TypeId, got: TypeId) -> bool { - if expected == got || expected == TypeInterner::ERROR || got == TypeInterner::ERROR { - return true; - } - - match (self.interner.resolve(expected), self.interner.resolve(got)) { - (Type::Interface(_), Type::Interface(_)) => expected == got, - (Type::Interface(_), _) => self.interface_impls.contains_key(&(expected, got)), - (Type::Secret(expected_inner), Type::Secret(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Secret(expected_inner), _) => self.types_compatible(*expected_inner, got), - (Type::List(expected_inner), Type::List(got_inner)) - | (Type::Optional(expected_inner), Type::Optional(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Set(expected_inner), Type::Set(got_inner)) => { - self.types_compatible(*expected_inner, *got_inner) - } - (Type::Map(expected_key, expected_val), Type::Map(got_key, got_val)) - | (Type::Result(expected_key, expected_val), Type::Result(got_key, got_val)) => { - self.types_compatible(*expected_key, *got_key) - && self.types_compatible(*expected_val, *got_val) + // Select only shapes whose reflected serializer can be checked as concrete + // source today; other JSON calls retain their existing intrinsic lowering. + fn native_json_source_supported(&self, ty: TypeId, visiting: &mut HashSet) -> bool { + match self.interner.resolve(ty) { + Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 => true, + // Inferred empty carriers have no Never payload to serialize. + // Their checked source bodies still precede uninhabited lowering. + Type::Never => true, + Type::List(element) => self.native_json_source_supported(*element, visiting), + Type::Set(element) => { + self.is_primitive_hashable_type(*element) + && self.native_json_source_supported(*element, visiting) + } + Type::Map(key, value) if *key == TypeInterner::STRING => { + self.native_json_source_supported(*value, visiting) + } + Type::Optional(inner) => self.native_json_source_supported(*inner, visiting), + Type::Result(ok, err) => { + self.native_json_source_supported(*ok, visiting) + && self.native_json_source_supported(*err, visiting) } - ( - Type::Function { - params: expected_params, - return_type: expected_return, - }, - Type::Function { - params: got_params, - return_type: got_return, - }, - ) => { - expected_params.len() == got_params.len() - && expected_params + Type::Secret(_) => false, + Type::Refinement { base, .. } => self.native_json_source_supported(*base, visiting), + Type::Enum(id) if self.interner.resolve_enum(*id).name == "json.JsonTree" => true, + Type::Enum(id) => { + if !visiting.insert(ty) { + return true; + } + let supported = self + .interner + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter()) + .all(|(_, field_ty)| self.native_json_source_supported(*field_ty, visiting)); + visiting.remove(&ty); + supported + } + Type::Bitfield(_) => self.native_json_bitfield_source_supported(ty), + Type::Struct(id) => { + // A checked finite self-reference reuses the same source + // specialization; the remaining fields still decide support. + if !visiting.insert(ty) { + return true; + } + let supported = + self.interner + .resolve_struct(*id) + .fields .iter() - .zip(got_params.iter()) - .all(|(expected, got)| self.types_compatible(*expected, *got)) - && self.types_compatible(*expected_return, *got_return) + .all(|(_, field_ty)| { + self.type_contains_secret_data(*field_ty) + || self.native_json_source_supported(*field_ty, visiting) + }); + visiting.remove(&ty); + supported + } + Type::Machine(id) => { + if !visiting.insert(ty) { + return false; + } + let supported = self + .interner + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter()) + .all(|(_, field_ty)| { + self.type_contains_secret_data(*field_ty) + || self.native_json_source_supported(*field_ty, visiting) + }); + visiting.remove(&ty); + supported } - // This compatibility is intentionally one-way. Crossing a bare - // machine boundary erases exact state; recovering it requires an - // explicit `at` guard rather than remembered caller provenance. - (Type::Machine(expected_machine), Type::MachineState { machine, .. }) => { - expected_machine == machine + Type::MachineState { machine, state } => { + if !visiting.insert(ty) { + return false; + } + let supported = self + .interner + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state.fields.iter().all(|(_, field_ty)| { + self.type_contains_secret_data(*field_ty) + || self.native_json_source_supported(*field_ty, visiting) + }) + }); + visiting.remove(&ty); + supported } _ => false, } } - /// Emit a builtin type-argument arity error without hiding the call shape. - fn expect_no_type_args(&mut self, builtin_name: &str, type_args: &[TypeExpr], span: Span) { - if !type_args.is_empty() { - self.sink.emit(errors::unknown_type( - &format!( - "{builtin_name} (expected 0 type arguments, got {})", - type_args.len() - ), - span, - )); - } + fn native_json_enum_source_supported(&self, ty: TypeId) -> bool { + let Type::Enum(id) = self.interner.resolve(ty) else { + return false; + }; + let definition = self.interner.resolve_enum(*id); + definition.name != "json.JsonTree" + && definition + .variants + .iter() + .flat_map(|variant| variant.fields.iter()) + .all(|(_, field_ty)| { + self.native_json_source_supported(*field_ty, &mut HashSet::new()) + }) } - fn no_type_args_signature( - &mut self, - builtin_name: &str, - type_args: &[TypeExpr], - span: Span, - params: Vec, - return_type: TypeId, - ) -> Option<(Vec, TypeId)> { - self.expect_no_type_args(builtin_name, type_args, span); - Some((params, return_type)) + fn native_json_bitfield_source_supported(&self, ty: TypeId) -> bool { + let Type::Bitfield(id) = self.interner.resolve(ty) else { + return false; + }; + self.interner + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| match self.interner.resolve(field.ty) { + Type::Enum(enum_id) => { + let definition = self.interner.resolve_enum(*enum_id); + definition.name != "json.JsonTree" + && definition + .variants + .iter() + .all(|variant| variant.fields.is_empty()) + } + _ => self.native_json_source_supported(field.ty, &mut HashSet::new()), + }) } - /// Extract T from an optional single-argument builtin type list. - /// Uses ERROR as a wildcard when type args are absent. - fn optional_type_arg( - &mut self, - builtin_name: &str, - type_args: &[TypeExpr], - span: Span, - ) -> TypeId { - match type_args.len() { - 0 => TypeInterner::ERROR, - 1 => self.resolve_type_expr(&type_args[0]), - got => { - self.sink.emit(errors::unknown_type( - &format!("{builtin_name} (expected 0 or 1 type arguments, got {got})"), + fn native_json_bitfield_parse_supported(&self, ty: TypeId) -> bool { + let Type::Bitfield(id) = self.interner.resolve(ty) else { + return false; + }; + self.interner + .resolve_bitfield(*id) + .fields + .iter() + .all(|field| match self.interner.resolve(field.ty) { + Type::Enum(enum_id) => { + let definition = self.interner.resolve_enum(*enum_id); + definition.name != "json.JsonTree" + && definition + .variants + .iter() + .all(|variant| variant.fields.is_empty()) + } + _ => self.native_json_parse_source_supported_inner(field.ty, &mut HashSet::new()), + }) + } + + fn native_json_parse_source_supported(&self, ty: TypeId) -> bool { + self.native_json_parse_source_supported_inner(ty, &mut HashSet::new()) + } + + fn native_json_enum_parse_supported(&self, ty: TypeId) -> bool { + let Type::Enum(id) = self.interner.resolve(ty) else { + return false; + }; + let definition = self.interner.resolve_enum(*id); + definition.name != "json.JsonTree" + && definition + .variants + .iter() + .flat_map(|variant| variant.fields.iter()) + .all(|(_, field_ty)| { + self.native_json_parse_source_supported_inner(*field_ty, &mut HashSet::new()) + }) + } + + fn native_json_parse_source_supported_inner( + &self, + ty: TypeId, + visiting: &mut HashSet, + ) -> bool { + match self.interner.resolve(ty) { + Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 => true, + // The checked unknown-kind decoder returns only a string error. + // Empty carriers retain their inferred uninhabited payload slot. + Type::Never => true, + Type::List(element) => { + self.native_json_parse_source_supported_inner(*element, visiting) + } + Type::Set(element) => { + self.is_primitive_hashable_type(*element) + && self.native_json_parse_source_supported_inner(*element, visiting) + } + Type::Map(key, value) if *key == TypeInterner::STRING => { + self.native_json_parse_source_supported_inner(*value, visiting) + } + Type::Optional(inner) => { + self.native_json_parse_source_supported_inner(*inner, visiting) + } + Type::Result(ok, err) => { + self.native_json_parse_source_supported_inner(*ok, visiting) + && self.native_json_parse_source_supported_inner(*err, visiting) + } + Type::Secret(inner) => self.native_json_parse_source_supported_inner(*inner, visiting), + Type::Enum(id) if self.interner.resolve_enum(*id).name == "json.JsonTree" => true, + Type::Enum(id) => { + if !visiting.insert(ty) { + return true; + } + let supported = self + .interner + .resolve_enum(*id) + .variants + .iter() + .flat_map(|variant| variant.fields.iter()) + .all(|(_, field_ty)| { + self.native_json_parse_source_supported_inner(*field_ty, visiting) + }); + visiting.remove(&ty); + supported + } + Type::Bitfield(_) => self.native_json_bitfield_parse_supported(ty), + Type::Refinement { base, .. } => { + self.native_json_parse_source_supported_inner(*base, visiting) + } + Type::Struct(id) => { + // Source decode may recurse through the same finite struct. + if !visiting.insert(ty) { + return true; + } + let supported = + self.interner + .resolve_struct(*id) + .fields + .iter() + .all(|(_, field_ty)| { + self.native_json_parse_source_supported_inner(*field_ty, visiting) + }); + visiting.remove(&ty); + supported + } + Type::Machine(id) => { + if !visiting.insert(ty) { + return false; + } + let supported = self + .interner + .resolve_machine(*id) + .states + .iter() + .flat_map(|state| state.fields.iter()) + .all(|(_, field_ty)| { + self.native_json_parse_source_supported_inner(*field_ty, visiting) + }); + visiting.remove(&ty); + supported + } + Type::MachineState { machine, state } => { + if !visiting.insert(ty) { + return false; + } + let supported = self + .interner + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state.fields.iter().all(|(_, field_ty)| { + self.native_json_parse_source_supported_inner(*field_ty, visiting) + }) + }); + visiting.remove(&ty); + supported + } + _ => false, + } + } + + fn types_compatible(&self, expected: TypeId, got: TypeId) -> bool { + ordinary_types_compatible( + &self.interner, + &|pair| self.interface_impls.contains_key(pair), + expected, + got, + ) + } + /// Fill only absent inferred slots. Existing concrete widths and nominal + /// owners must match; this is not a promotion or refinement coarsening. + /// Function parameters reverse the direction because callable substitution + /// is contravariant, including when the missing slot is nested in a sum. + fn merge_inferred_never( + &mut self, + first: TypeId, + next: TypeId, + contravariant: bool, + ) -> Option { + if first == next { + return Some(first); + } + if first == TypeInterner::NEVER || next == TypeInterner::NEVER { + return Some(if contravariant { + TypeInterner::NEVER + } else if first == TypeInterner::NEVER { + next + } else { + first + }); + } + let merged = match ( + self.interner.resolve(first).clone(), + self.interner.resolve(next).clone(), + ) { + (Type::List(first), Type::List(next)) => { + Type::List(self.merge_inferred_never(first, next, contravariant)?) + } + (Type::Set(first), Type::Set(next)) => { + Type::Set(self.merge_inferred_never(first, next, contravariant)?) + } + (Type::Optional(first), Type::Optional(next)) => { + Type::Optional(self.merge_inferred_never(first, next, contravariant)?) + } + (Type::Secret(first), Type::Secret(next)) => { + Type::Secret(self.merge_inferred_never(first, next, contravariant)?) + } + (Type::Map(first_key, first_value), Type::Map(next_key, next_value)) => Type::Map( + self.merge_inferred_never(first_key, next_key, contravariant)?, + self.merge_inferred_never(first_value, next_value, contravariant)?, + ), + (Type::Result(first_ok, first_error), Type::Result(next_ok, next_error)) => { + Type::Result( + self.merge_inferred_never(first_ok, next_ok, contravariant)?, + self.merge_inferred_never(first_error, next_error, contravariant)?, + ) + } + ( + Type::Function { + params: first_params, + view_params: first_views, + return_type: first_return, + }, + Type::Function { + params: next_params, + view_params: next_views, + return_type: next_return, + }, + ) if first_params.len() == next_params.len() && first_views == next_views => { + Type::Function { + params: first_params + .into_iter() + .zip(next_params) + .map(|(first, next)| self.merge_inferred_never(first, next, !contravariant)) + .collect::>()?, + view_params: first_views, + return_type: self.merge_inferred_never( + first_return, + next_return, + contravariant, + )?, + } + } + _ => return None, + }; + Some(self.interner.intern(merged)) + } + + fn contains_inferred_never(&self, ty: TypeId) -> bool { + match self.interner.resolve(ty) { + Type::Never => true, + Type::List(inner) | Type::Set(inner) | Type::Optional(inner) | Type::Secret(inner) => { + self.contains_inferred_never(*inner) + } + Type::Map(key, value) | Type::Result(key, value) => { + self.contains_inferred_never(*key) || self.contains_inferred_never(*value) + } + Type::Function { + params, + return_type, + .. + } => { + params.iter().any(|ty| self.contains_inferred_never(*ty)) + || self.contains_inferred_never(*return_type) + } + _ => false, + } + } + + /// Emit a builtin type-argument arity error without hiding the call shape. + fn expect_no_type_args(&mut self, builtin_name: &str, type_args: &[TypeExpr], span: Span) { + if !type_args.is_empty() { + self.sink.emit(errors::unknown_type( + &format!( + "{builtin_name} (expected 0 type arguments, got {})", + type_args.len() + ), + span, + )); + } + } + + fn no_type_args_signature( + &mut self, + builtin_name: &str, + type_args: &[TypeExpr], + span: Span, + params: Vec, + return_type: TypeId, + ) -> Option<(Vec, TypeId)> { + self.expect_no_type_args(builtin_name, type_args, span); + // Keep the operand shape for independent checking, but an invalid + // invocation cannot select an ownership handoff or a usable result. + let checked_return = if type_args.is_empty() { + return_type + } else { + TypeInterner::ERROR + }; + Some((params, checked_return)) + } + + /// Extract T from an optional single-argument builtin type list. + /// Uses ERROR as a wildcard when type args are absent. + fn optional_type_arg( + &mut self, + builtin_name: &str, + type_args: &[TypeExpr], + span: Span, + ) -> TypeId { + match type_args.len() { + 0 => TypeInterner::ERROR, + 1 => self.resolve_type_expr(&type_args[0]), + got => { + self.sink.emit(errors::unknown_type( + &format!("{builtin_name} (expected 0 or 1 type arguments, got {got})"), span, )); TypeInterner::ERROR @@ -2766,8 +4193,19 @@ impl<'a> TypeChecker<'a> { } fn function_type(&mut self, params: Vec, return_type: TypeId) -> TypeId { + let view_params = vec![false; params.len()]; + self.function_type_with_modes(params, view_params, return_type) + } + + fn function_type_with_modes( + &mut self, + params: Vec, + view_params: Vec, + return_type: TypeId, + ) -> TypeId { self.interner.intern(Type::Function { params, + view_params, return_type, }) } @@ -2801,8 +4239,27 @@ impl<'a> TypeChecker<'a> { } fn resolved_expr_name(&self, expr: &Expr) -> Option { + if let Expr::FieldAccess(_, field, _) = expr { + if let Some((owner, _)) = self.method_signature_for_callee(expr) { + return Some(format!("{}.{}", self.type_name(owner), field.name)); + } + } match expr { Expr::Ident(ident) => Some(self.resolved_symbol_name(&ident.name, ident.span)), + Expr::FieldAccess(base, field, _) + if matches!(field.name.as_str(), "to_bytes" | "from_bytes") => + { + let resolved_owner = + self.resolve + .resolutions + .get(&expr.span()) + .and_then(|definition| { + let info = self.resolve.scope_table.def(*definition); + (info.kind == DefKind::Bitfield).then(|| info.name.clone()) + }); + let owner = resolved_owner.or_else(|| self.expanded_dotted_expr_name(base))?; + Some(format!("{owner}.{}", field.name,)) + } Expr::FieldAccess(_, _, _) => self.expanded_dotted_expr_name(expr), _ => None, } @@ -2813,162 +4270,182 @@ impl<'a> TypeChecker<'a> { callee: &Expr, type_args: &[TypeExpr], span: Span, + ) -> Option<(Vec, TypeId)> { + let signature = self.builtin_signature_unrecorded(callee, type_args, span); + if let Some(signature) = &signature { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.invocation_signatures.insert(span, signature.clone()); + } else { + self.invocation_signatures.insert(span, signature.clone()); + } + } + signature + } + + fn builtin_signature_unrecorded( + &mut self, + callee: &Expr, + type_args: &[TypeExpr], + span: Span, ) -> Option<(Vec, TypeId)> { let name = self.resolved_expr_name(callee)?; + // This is the sole acceptance boundary for compiler-provided call + // signatures. A new string arm cannot become a builtin until it has a + // shared closed identity. + let intrinsic = IntrinsicId::from_callable_name(&name)?; let private_stdlib_kernel = matches!( - name.as_str(), - "list.__new" - | "list.__append" - | "list.__length" - | "list.__get_clone" - | "list.__sort" - | "list.__insert_at" - | "list.__remove_at" - | "list.__swap" - | "list.__sort_by_index" - | "list.__is_sorted" - | "list.__sum" - | "list.__sort_by" - | "list.__group_by" - | "math.__abs" - | "math.__min" - | "math.__max" - | "math.__sqrt" - | "math.__pow" - | "math.__floor" - | "math.__ceil" - | "math.__round" - | "math.__clamp" - | "math.__log" - | "math.__log2" - | "math.__log10" - | "math.__average" - | "math.__median" - | "math.__pi" - | "math.__e" - | "math.__sin" - | "math.__cos" - | "math.__tan" - | "math.__mod" - | "math.__gcd" - | "math.__lcm" - | "math.__factorial" - | "string.__from_int64" - | "string.__from_uint64" - | "string.__from_float64" - | "string.__from_bool" - | "string.__char_count" - | "string.__chars" - | "string.__slice" - | "string.__index_of" - | "string.__count" - | "string.__trim" - | "string.__trim_start" - | "string.__trim_end" - | "string.__upper" - | "string.__lower" - | "string.__replace" - | "string.__split" - | "string.__join" - | "string.__repeat" - | "string.__slugify" - | "string.__words" - | "string.__lines" - | "string.__to_upper_first" - | "string.__to_lower_first" - | "string.__is_numeric" - | "string.__is_alpha" - | "map.__new" - | "map.__length" - | "map.__has" - | "map.__get" - | "map.__insert" - | "map.__remove" - | "map.__from_lists" - | "set.__new" - | "set.__add" - | "set.__remove" - | "set.__contains" - | "set.__length" - | "bytes.__new" - | "bytes.__length" - | "bytes.__slice" - | "bytes.__concat" - | "bytes.__from_string" - | "bytes.__to_string" - | "bytes.__get" - | "bytes.__to_hex" - | "bytes.__from_hex" - | "encoding.__base64_encode" - | "encoding.__base64_decode" - | "encoding.__hex_decode" - | "encoding.__url_encode" - | "encoding.__url_decode" - | "encoding.__form_encode" - | "encoding.__form_decode" - | "crypto.__sha256" - | "crypto.__sha512" - | "crypto.__md5" - | "crypto.__hmac_sha256" - | "csv.__parse" - | "csv.__stringify" - | "csv.__parse_with_header" - | "random.__bounded" - | "random.__unit_float64" - | "random.__bool" - | "Clock.__now" - | "Environment.__get" - | "Environment.__args" - | "test.mock.__random" - | "test.mock.__clock" - | "test.mock.__environment" - | "log.__emit" - | "graphics.__run" + intrinsic, + IntrinsicId::ListNew + | IntrinsicId::ListAppend + | IntrinsicId::ListLength + | IntrinsicId::ListGetClone + | IntrinsicId::ListSort + | IntrinsicId::ListInsertAt + | IntrinsicId::ListRemoveAt + | IntrinsicId::ListSwap + | IntrinsicId::ListSortByIndex + | IntrinsicId::ListIsSorted + | IntrinsicId::ListSum + | IntrinsicId::ListSortBy + | IntrinsicId::ListGroupBy + | IntrinsicId::MathKernelAbs + | IntrinsicId::MathKernelMin + | IntrinsicId::MathKernelMax + | IntrinsicId::MathSqrt + | IntrinsicId::MathPow + | IntrinsicId::MathFloor + | IntrinsicId::MathCeil + | IntrinsicId::MathRound + | IntrinsicId::MathClamp + | IntrinsicId::MathLog + | IntrinsicId::MathLog2 + | IntrinsicId::MathLog10 + | IntrinsicId::MathAverage + | IntrinsicId::MathMedian + | IntrinsicId::MathPi + | IntrinsicId::MathE + | IntrinsicId::MathSin + | IntrinsicId::MathCos + | IntrinsicId::MathTan + | IntrinsicId::MathMod + | IntrinsicId::MathGcd + | IntrinsicId::MathLcm + | IntrinsicId::MathFactorial + | IntrinsicId::StringFromInt64 + | IntrinsicId::StringFromUint64 + | IntrinsicId::StringFromFloat64 + | IntrinsicId::StringFromBool + | IntrinsicId::StringCharCount + | IntrinsicId::StringChars + | IntrinsicId::StringSlice + | IntrinsicId::StringIndexOf + | IntrinsicId::StringCount + | IntrinsicId::StringTrim + | IntrinsicId::StringTrimStart + | IntrinsicId::StringTrimEnd + | IntrinsicId::StringUpper + | IntrinsicId::StringLower + | IntrinsicId::StringReplace + | IntrinsicId::StringSplit + | IntrinsicId::StringJoin + | IntrinsicId::StringRepeat + | IntrinsicId::StringSlugify + | IntrinsicId::StringWords + | IntrinsicId::StringLines + | IntrinsicId::StringToUpperFirst + | IntrinsicId::StringToLowerFirst + | IntrinsicId::StringIsNumeric + | IntrinsicId::StringIsAlpha + | IntrinsicId::MapNew + | IntrinsicId::MapLength + | IntrinsicId::MapHas + | IntrinsicId::MapGet + | IntrinsicId::MapInsert + | IntrinsicId::MapRemove + | IntrinsicId::MapFromLists + | IntrinsicId::SetNew + | IntrinsicId::SetAdd + | IntrinsicId::SetRemove + | IntrinsicId::SetContains + | IntrinsicId::SetLength + | IntrinsicId::BytesNew + | IntrinsicId::BytesLength + | IntrinsicId::BytesSlice + | IntrinsicId::BytesConcat + | IntrinsicId::BytesFromString + | IntrinsicId::BytesToString + | IntrinsicId::BytesGet + | IntrinsicId::BytesToHex + | IntrinsicId::BytesFromHex + | IntrinsicId::EncodingBase64Encode + | IntrinsicId::EncodingBase64Decode + | IntrinsicId::EncodingHexDecode + | IntrinsicId::EncodingUrlEncode + | IntrinsicId::EncodingUrlDecode + | IntrinsicId::EncodingFormEncode + | IntrinsicId::EncodingFormDecode + | IntrinsicId::CryptoSha256 + | IntrinsicId::CryptoSha512 + | IntrinsicId::CryptoMd5 + | IntrinsicId::CryptoHmacSha256 + | IntrinsicId::CsvParse + | IntrinsicId::CsvStringify + | IntrinsicId::CsvParseWithHeader + | IntrinsicId::RandomBounded + | IntrinsicId::RandomUnitFloat64 + | IntrinsicId::RandomBool + | IntrinsicId::ClockNow + | IntrinsicId::EnvironmentGet + | IntrinsicId::EnvironmentArgs + | IntrinsicId::TestMockRandom + | IntrinsicId::TestMockClock + | IntrinsicId::TestMockEnvironment + | IntrinsicId::LogEmit + | IntrinsicId::GraphicsRun ); if private_stdlib_kernel && !span.file.is_stdlib() { self.sink .emit(errors::not_callable("private stdlib kernel", span)); } - if let Some((type_name, method_name)) = name.rsplit_once('.') { - if let Some(&type_id) = self.named_types.get(type_name) { - if matches!(self.interner.resolve(type_id), Type::Bitfield(_)) { - match method_name { - "to_bytes" => { - if !type_args.is_empty() { - self.sink.emit(errors::unknown_type( - &format!( - "{name} (expected 0 type arguments, got {})", - type_args.len() - ), - span, - )); - } - return Some((vec![type_id], TypeInterner::BYTES)); - } - "from_bytes" => { - if !type_args.is_empty() { - self.sink.emit(errors::unknown_type( - &format!( - "{name} (expected 0 type arguments, got {})", - type_args.len() - ), - span, - )); - } - return Some(( - vec![TypeInterner::BYTES], - self.interner - .intern(Type::Result(type_id, TypeInterner::STRING)), - )); - } - _ => {} + if let Some((type_name, _)) = name.rsplit_once('.') + && let Some(&type_id) = self.named_types.get(type_name) + && matches!(self.interner.resolve(type_id), Type::Bitfield(_)) + { + match intrinsic { + IntrinsicId::BitfieldToBytes => { + if !type_args.is_empty() { + self.sink.emit(errors::unknown_type( + &format!( + "{name} (expected 0 type arguments, got {})", + type_args.len() + ), + span, + )); + } + return Some((vec![type_id], TypeInterner::BYTES)); + } + IntrinsicId::BitfieldFromBytes => { + if !type_args.is_empty() { + self.sink.emit(errors::unknown_type( + &format!( + "{name} (expected 0 type arguments, got {})", + type_args.len() + ), + span, + )); } + return Some(( + vec![TypeInterner::BYTES], + self.interner + .intern(Type::Result(type_id, TypeInterner::STRING)), + )); } + _ => {} } } - match name.as_str() { - "int64.from_float64" => { + match intrinsic { + IntrinsicId::Int64FromFloat64 => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::FLOAT64], @@ -2976,7 +4453,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::INT64, TypeInterner::STRING)), )) } - "int64.from_string" => { + IntrinsicId::Int64FromString => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -2984,7 +4461,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::INT64, TypeInterner::STRING)), )) } - "uint64.from_string" => { + IntrinsicId::Uint64FromString => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -2992,7 +4469,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::UINT64, TypeInterner::STRING)), )) } - "float64.from_string" => { + IntrinsicId::Float64FromString => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -3000,35 +4477,42 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::FLOAT64, TypeInterner::STRING)), )) } - "string.__from_int64" => self.no_type_args_signature( + IntrinsicId::Float32FromFloat64 => self.no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::FLOAT64], + TypeInterner::FLOAT32, + ), + IntrinsicId::StringFromInt64 => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::INT64], TypeInterner::STRING, ), - "string.__from_uint64" => self.no_type_args_signature( + IntrinsicId::StringFromUint64 => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::UINT64], TypeInterner::STRING, ), - "string.__from_float64" => self.no_type_args_signature( + IntrinsicId::StringFromFloat64 => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::FLOAT64], TypeInterner::STRING, ), - "string.__from_bool" => self.no_type_args_signature( + IntrinsicId::StringFromBool => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::BOOL], TypeInterner::STRING, ), - "float64.from_int64" => { + IntrinsicId::Float64FromInt64 => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::INT64], @@ -3036,21 +4520,22 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::FLOAT64, TypeInterner::STRING)), )) } - "string.__char_count" => self.no_type_args_signature( + IntrinsicId::StringCharCount => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::STRING], TypeInterner::INT64, ), - "string.__trim" | "string.__upper" | "string.__lower" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::STRING], - TypeInterner::STRING, - ), - "string.__replace" => self.no_type_args_signature( + IntrinsicId::StringTrim | IntrinsicId::StringUpper | IntrinsicId::StringLower => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::STRING], + TypeInterner::STRING, + ), + IntrinsicId::StringReplace => self.no_type_args_signature( &name, type_args, span, @@ -3061,29 +4546,29 @@ impl<'a> TypeChecker<'a> { ], TypeInterner::STRING, ), - "string.__split" => { + IntrinsicId::StringSplit => { self.expect_no_type_args(&name, type_args, span); let list_ty = self.interner.intern(Type::List(TypeInterner::STRING)); Some((vec![TypeInterner::STRING, TypeInterner::STRING], list_ty)) } - "string.__join" => { + IntrinsicId::StringJoin => { self.expect_no_type_args(&name, type_args, span); let list_ty = self.interner.intern(Type::List(TypeInterner::STRING)); Some((vec![list_ty, TypeInterner::STRING], TypeInterner::STRING)) } - "Filesystem.read_file" => { + IntrinsicId::FilesystemReadFile => { self.expect_no_type_args(&name, type_args, span); Some(( - vec![TypeInterner::ERROR, TypeInterner::STRING], + vec![TypeInterner::FILESYSTEM, TypeInterner::STRING], self.interner .intern(Type::Result(TypeInterner::STRING, TypeInterner::STRING)), )) } - "Filesystem.write_file" => { + IntrinsicId::FilesystemWriteFile => { self.expect_no_type_args(&name, type_args, span); Some(( vec![ - TypeInterner::ERROR, + TypeInterner::FILESYSTEM, TypeInterner::STRING, TypeInterner::STRING, ], @@ -3091,14 +4576,14 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::NOTHING, TypeInterner::STRING)), )) } - "Stdout.write" => self.no_type_args_signature( + IntrinsicId::StdoutWrite => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::ERROR, TypeInterner::STRING], + vec![TypeInterner::STDOUT, TypeInterner::STRING], TypeInterner::NOTHING, ), - "json.parse" | "json.parse_exact" => { + IntrinsicId::JsonParse | IntrinsicId::JsonParseExact => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3113,7 +4598,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(value_ty, TypeInterner::STRING)); Some((vec![TypeInterner::STRING], result_ty)) } - "json.serialize" | "json.serialize_public" => { + IntrinsicId::JsonSerialize | IntrinsicId::JsonSerializePublic => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3125,7 +4610,7 @@ impl<'a> TypeChecker<'a> { let value_ty = self.resolve_type_expr(&type_args[0]); Some((vec![value_ty], TypeInterner::STRING)) } - "type.name" | "type.kind" => { + IntrinsicId::TypeName | IntrinsicId::TypeKind => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3136,7 +4621,7 @@ impl<'a> TypeChecker<'a> { let _ = self.resolve_type_expr(&type_args[0]); Some((vec![], TypeInterner::STRING)) } - "type.kind_tag" => { + IntrinsicId::TypeKindTag => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3153,7 +4638,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR), )) } - "type.primitive_tag" => { + IntrinsicId::TypePrimitiveTag => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3169,7 +4654,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], self.interner.intern(Type::Optional(primitive_ty)))) } - "type.has_secret" => { + IntrinsicId::TypeHasSecret => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3180,7 +4665,7 @@ impl<'a> TypeChecker<'a> { let _ = self.resolve_type_expr(&type_args[0]); Some((vec![], TypeInterner::BOOL)) } - "type.info" => { + IntrinsicId::TypeInfo => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3196,7 +4681,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], type_info_ty)) } - "type.arg" => { + IntrinsicId::TypeArg => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3215,7 +4700,7 @@ impl<'a> TypeChecker<'a> { // Reflected construction deliberately exposes one explicit builder // lifecycle. Keep construct_start/put/finish canonical instead of // adding a parallel hidden-builder block or callback spelling. - "type.construct_start" => { + IntrinsicId::TypeConstructStart => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3238,7 +4723,7 @@ impl<'a> TypeChecker<'a> { } Some((vec![], TypeInterner::TYPE_CONSTRUCTION)) } - "type.construct_variant_start" => { + IntrinsicId::TypeConstructVariantStart => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3269,7 +4754,7 @@ impl<'a> TypeChecker<'a> { )), )) } - "type.construct_machine_start" => { + IntrinsicId::TypeConstructMachineStart => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3303,7 +4788,7 @@ impl<'a> TypeChecker<'a> { )), )) } - "type.construct_put" => { + IntrinsicId::TypeConstructPut => { if type_args.len() != 2 { self.sink.emit(errors::unknown_type( &format!( @@ -3355,7 +4840,7 @@ impl<'a> TypeChecker<'a> { )), )) } - "type.construct_finish" => { + IntrinsicId::TypeConstructFinish => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3364,6 +4849,16 @@ impl<'a> TypeChecker<'a> { return Some((vec![TypeInterner::TYPE_CONSTRUCTION], TypeInterner::ERROR)); } let target_ty = self.resolve_type_expr(&type_args[0]); + if let Type::Machine(machine) = self.interner.resolve(target_ty) { + let machine = *machine; + let states = self.interner.resolve_machine(machine).states.len(); + for index in 0..states { + self.interner.intern(Type::MachineState { + machine, + state: MachineStateId::new(index as u32), + }); + } + } if !matches!( self.interner.resolve(target_ty), Type::Struct(_) @@ -3386,7 +4881,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(target_ty, TypeInterner::STRING)), )) } - "type.fields" => { + IntrinsicId::TypeFields => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3402,7 +4897,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], self.interner.intern(Type::List(type_field_ty)))) } - "type.bitfield_layout" => { + IntrinsicId::TypeBitfieldLayout => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3418,7 +4913,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], type_bitfield_ty)) } - "type.bitfield_fields" => { + IntrinsicId::TypeBitfieldFields => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3437,7 +4932,7 @@ impl<'a> TypeChecker<'a> { self.interner.intern(Type::List(type_bitfield_field_ty)), )) } - "type.machine_layout" => { + IntrinsicId::TypeMachineLayout => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3453,7 +4948,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], type_machine_ty)) } - "type.machine_states" => { + IntrinsicId::TypeMachineStates => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3472,7 +4967,7 @@ impl<'a> TypeChecker<'a> { self.interner.intern(Type::List(type_machine_state_ty)), )) } - "type.machine_transitions" => { + IntrinsicId::TypeMachineTransitions => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3491,7 +4986,7 @@ impl<'a> TypeChecker<'a> { self.interner.intern(Type::List(type_machine_transition_ty)), )) } - "type.machine_state_value" => { + IntrinsicId::TypeMachineStateValue => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3501,6 +4996,7 @@ impl<'a> TypeChecker<'a> { } let value_ty = self.resolve_type_expr(&type_args[0]); + self.check_resource_root_value_reflection(value_ty, &name, span); let type_machine_state_ty = self .named_types .get("TypeMachineState") @@ -3508,7 +5004,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![value_ty], type_machine_state_ty)) } - "type.variants" => { + IntrinsicId::TypeVariants => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3524,7 +5020,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![], self.interner.intern(Type::List(type_variant_ty)))) } - "type.variant_value" => { + IntrinsicId::TypeVariantValue => { if type_args.len() != 1 { self.sink.emit(errors::unknown_type( &format!("{name} (expected 1 type argument, got {})", type_args.len()), @@ -3534,6 +5030,7 @@ impl<'a> TypeChecker<'a> { } let value_ty = self.resolve_type_expr(&type_args[0]); + self.check_resource_root_value_reflection(value_ty, &name, span); let type_variant_ty = self .named_types .get("TypeVariant") @@ -3541,7 +5038,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![value_ty], type_variant_ty)) } - "type.field_value" => { + IntrinsicId::TypeFieldValue => { if type_args.len() != 2 { self.sink.emit(errors::unknown_type( &format!( @@ -3555,6 +5052,14 @@ impl<'a> TypeChecker<'a> { let value_ty = self.resolve_type_expr(&type_args[0]); let return_ty = self.resolve_type_expr(&type_args[1]); + if matches!(self.interner.resolve(value_ty), Type::Resource(_)) { + self.sink.emit(errors::type_mismatch( + &format!("non-resource value for {name}"), + &self.type_name(value_ty), + span, + )); + } + self.check_resource_copy(return_ty, span); let type_field_ty = self .named_types .get("TypeField") @@ -3562,7 +5067,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![value_ty, type_field_ty], return_ty)) } - "type.machine_field_value" => { + IntrinsicId::TypeMachineFieldValue => { if type_args.len() != 2 { self.sink.emit(errors::unknown_type( &format!( @@ -3576,6 +5081,14 @@ impl<'a> TypeChecker<'a> { let value_ty = self.resolve_type_expr(&type_args[0]); let return_ty = self.resolve_type_expr(&type_args[1]); + if matches!(self.interner.resolve(value_ty), Type::Resource(_)) { + self.sink.emit(errors::type_mismatch( + &format!("non-resource value for {name}"), + &self.type_name(value_ty), + span, + )); + } + self.check_resource_copy(return_ty, span); let type_field_ty = self .named_types .get("TypeField") @@ -3583,7 +5096,7 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![value_ty, type_field_ty], return_ty)) } - "type.variant_field_value" => { + IntrinsicId::TypeVariantFieldValue => { if type_args.len() != 2 { self.sink.emit(errors::unknown_type( &format!( @@ -3597,6 +5110,14 @@ impl<'a> TypeChecker<'a> { let value_ty = self.resolve_type_expr(&type_args[0]); let return_ty = self.resolve_type_expr(&type_args[1]); + if matches!(self.interner.resolve(value_ty), Type::Resource(_)) { + self.sink.emit(errors::type_mismatch( + &format!("non-resource value for {name}"), + &self.type_name(value_ty), + span, + )); + } + self.check_resource_copy(return_ty, span); let type_field_ty = self .named_types .get("TypeField") @@ -3604,27 +5125,27 @@ impl<'a> TypeChecker<'a> { .unwrap_or(TypeInterner::ERROR); Some((vec![value_ty, type_field_ty], return_ty)) } - "secret.redact" => { + IntrinsicId::SecretRedact => { self.expect_no_type_args(&name, type_args, span); let secret_ty = self.interner.intern(Type::Secret(TypeInterner::ERROR)); Some((vec![secret_ty], TypeInterner::STRING)) } - "secret.compare" => { + IntrinsicId::SecretCompare => { self.expect_no_type_args(&name, type_args, span); let secret_ty = self.interner.intern(Type::Secret(TypeInterner::ERROR)); Some((vec![secret_ty, secret_ty], TypeInterner::BOOL)) } - "bytes.__new" => { + IntrinsicId::BytesNew => { self.no_type_args_signature(&name, type_args, span, vec![], TypeInterner::BYTES) } - "bytes.__length" => self.no_type_args_signature( + IntrinsicId::BytesLength => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::BYTES], TypeInterner::INT64, ), - "bytes.__slice" => self.no_type_args_signature( + IntrinsicId::BytesSlice => self.no_type_args_signature( &name, type_args, span, @@ -3635,21 +5156,21 @@ impl<'a> TypeChecker<'a> { ], TypeInterner::BYTES, ), - "bytes.__concat" => self.no_type_args_signature( + IntrinsicId::BytesConcat => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::BYTES, TypeInterner::BYTES], TypeInterner::BYTES, ), - "bytes.__from_string" => self.no_type_args_signature( + IntrinsicId::BytesFromString => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::STRING], TypeInterner::BYTES, ), - "bytes.__to_string" => { + IntrinsicId::BytesToString => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::BYTES], @@ -3657,21 +5178,21 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::STRING, TypeInterner::STRING)), )) } - "bytes.__get" => { + IntrinsicId::BytesGet => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::BYTES, TypeInterner::INT64], self.interner.intern(Type::Optional(TypeInterner::INT64)), )) } - "bytes.__to_hex" => self.no_type_args_signature( + IntrinsicId::BytesToHex => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::BYTES], TypeInterner::STRING, ), - "bytes.__from_hex" => { + IntrinsicId::BytesFromHex => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -3679,30 +5200,31 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::BYTES, TypeInterner::STRING)), )) } - "list.__new" => { + IntrinsicId::ListNew => { let inner = self.optional_type_arg(&name, type_args, span); Some((vec![], self.interner.intern(Type::List(inner)))) } - "list.__append" => { + IntrinsicId::ListAppend => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty, inner], list_ty)) } - "list.__length" => { + IntrinsicId::ListLength => { let inner = self.optional_type_arg(&name, type_args, span); Some(( vec![self.interner.intern(Type::List(inner))], TypeInterner::INT64, )) } - "list.__get_clone" => { + IntrinsicId::ListGetClone => { let inner = self.optional_type_arg(&name, type_args, span); + self.check_resource_copy(inner, span); Some(( vec![self.interner.intern(Type::List(inner)), TypeInterner::INT64], self.interner.intern(Type::Optional(inner)), )) } - "list.__sort" => { + IntrinsicId::ListSort => { let inner = self.optional_type_arg(&name, type_args, span); if inner != TypeInterner::ERROR && !self.is_orderable_list_element(inner) { self.sink.emit(errors::type_mismatch( @@ -3714,17 +5236,17 @@ impl<'a> TypeChecker<'a> { let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty], list_ty)) } - "list.__insert_at" => { + IntrinsicId::ListInsertAt => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty, TypeInterner::INT64, inner], list_ty)) } - "list.__remove_at" => { + IntrinsicId::ListRemoveAt => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty, TypeInterner::INT64], list_ty)) } - "list.__swap" => { + IntrinsicId::ListSwap => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); Some(( @@ -3732,7 +5254,7 @@ impl<'a> TypeChecker<'a> { list_ty, )) } - "list.__sort_by_index" => { + IntrinsicId::ListSortByIndex => { let inner = self.optional_type_arg(&name, type_args, span); if inner != TypeInterner::ERROR && !self.is_orderable_list_element(inner) { self.sink.emit(errors::type_mismatch( @@ -3745,7 +5267,7 @@ impl<'a> TypeChecker<'a> { let rows_ty = self.interner.intern(Type::List(row_ty)); Some((vec![rows_ty, TypeInterner::INT64], rows_ty)) } - "list.__is_sorted" => { + IntrinsicId::ListIsSorted => { let inner = self.optional_type_arg(&name, type_args, span); if inner != TypeInterner::ERROR && !self.is_orderable_list_element(inner) { self.sink.emit(errors::type_mismatch( @@ -3757,26 +5279,44 @@ impl<'a> TypeChecker<'a> { let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty], TypeInterner::BOOL)) } - "list.__sum" => { - let inner = self.optional_type_arg(&name, type_args, span); - let list_ty = self.interner.intern(Type::List(inner)); - Some((vec![list_ty], inner)) - } - // Private math kernels - "math.__abs" => { - let inner = self.optional_type_arg(&name, type_args, span); - if inner != TypeInterner::ERROR - && !matches!(inner, TypeInterner::INT64 | TypeInterner::FLOAT64) - { - self.sink.emit(errors::type_mismatch( - "int64 or float64", + IntrinsicId::ListSum => { + let inner = if let [element] = type_args { + self.resolve_type_expr(element) + } else { + self.sink.emit(errors::unknown_type( + &format!("{name} (expected 1 type argument, got {})", type_args.len()), + span, + )); + TypeInterner::ERROR + }; + // Transparent aliases already resolve to their target TypeId. + // Do not coarsen refinements or unwrap secret element types: + // neither has a summation identity and closure contract. + if inner != TypeInterner::ERROR && !self.is_numeric(inner) { + self.sink.emit(errors::type_mismatch( + "numeric primitive for list.sum", + &self.type_name(inner), + span, + )); + } + let list_ty = self.interner.intern(Type::List(inner)); + Some((vec![list_ty], inner)) + } + // Private math kernels + IntrinsicId::MathKernelAbs => { + let inner = self.optional_type_arg(&name, type_args, span); + if inner != TypeInterner::ERROR + && !matches!(inner, TypeInterner::INT64 | TypeInterner::FLOAT64) + { + self.sink.emit(errors::type_mismatch( + "int64 or float64", &self.type_name(inner), span, )); } Some((vec![inner], inner)) } - "math.__min" | "math.__max" => { + IntrinsicId::MathKernelMin | IntrinsicId::MathKernelMax => { let inner = self.optional_type_arg(&name, type_args, span); if inner != TypeInterner::ERROR && !matches!(inner, TypeInterner::INT64 | TypeInterner::FLOAT64) @@ -3789,29 +5329,32 @@ impl<'a> TypeChecker<'a> { } Some((vec![inner, inner], inner)) } - "math.__sqrt" | "math.__log" | "math.__log2" | "math.__log10" => self - .no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::FLOAT64], - TypeInterner::FLOAT64, - ), - "math.__pow" => self.no_type_args_signature( + IntrinsicId::MathSqrt + | IntrinsicId::MathLog + | IntrinsicId::MathLog2 + | IntrinsicId::MathLog10 => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::FLOAT64, TypeInterner::FLOAT64], + vec![TypeInterner::FLOAT64], TypeInterner::FLOAT64, ), - "math.__floor" | "math.__ceil" | "math.__round" => self.no_type_args_signature( + IntrinsicId::MathPow => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::FLOAT64], + vec![TypeInterner::FLOAT64, TypeInterner::FLOAT64], TypeInterner::FLOAT64, ), - "math.__clamp" => self.no_type_args_signature( + IntrinsicId::MathFloor | IntrinsicId::MathCeil | IntrinsicId::MathRound => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::FLOAT64], + TypeInterner::FLOAT64, + ), + IntrinsicId::MathClamp => self.no_type_args_signature( &name, type_args, span, @@ -3822,7 +5365,7 @@ impl<'a> TypeChecker<'a> { ], TypeInterner::FLOAT64, ), - "math.__average" | "math.__median" => { + IntrinsicId::MathAverage | IntrinsicId::MathMedian => { let inner = match type_args.len() { 0 => TypeInterner::FLOAT64, 1 => { @@ -3849,24 +5392,26 @@ impl<'a> TypeChecker<'a> { let list_ty = self.interner.intern(Type::List(inner)); Some((vec![list_ty], TypeInterner::FLOAT64)) } - "math.__pi" | "math.__e" => { + IntrinsicId::MathPi | IntrinsicId::MathE => { self.no_type_args_signature(&name, type_args, span, vec![], TypeInterner::FLOAT64) } - "math.__sin" | "math.__cos" | "math.__tan" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::FLOAT64], - TypeInterner::FLOAT64, - ), - "math.__mod" | "math.__gcd" | "math.__lcm" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::INT64, TypeInterner::INT64], - TypeInterner::INT64, - ), - "math.__factorial" => self.no_type_args_signature( + IntrinsicId::MathSin | IntrinsicId::MathCos | IntrinsicId::MathTan => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::FLOAT64], + TypeInterner::FLOAT64, + ), + IntrinsicId::MathMod | IntrinsicId::MathGcd | IntrinsicId::MathLcm => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::INT64, TypeInterner::INT64], + TypeInterner::INT64, + ), + IntrinsicId::MathFactorial => self.no_type_args_signature( &name, type_args, span, @@ -3874,69 +5419,70 @@ impl<'a> TypeChecker<'a> { TypeInterner::INT64, ), // string extras - "string.__trim_start" | "string.__trim_end" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::STRING], - TypeInterner::STRING, - ), + IntrinsicId::StringTrimStart | IntrinsicId::StringTrimEnd => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::STRING], + TypeInterner::STRING, + ), // Private string segmentation kernels return list[string]. - "string.__chars" | "string.__words" | "string.__lines" => { + IntrinsicId::StringChars | IntrinsicId::StringWords | IntrinsicId::StringLines => { self.expect_no_type_args(&name, type_args, span); let list_str = self.interner.intern(Type::List(TypeInterner::STRING)); Some((vec![TypeInterner::STRING], list_str)) } // Private graphics kernel; public data and signature are source-owned. - "graphics.__run" => { + IntrinsicId::GraphicsRun => { let state = self.optional_type_arg(&name, type_args, span); let config = *self.named_types.get("graphics.Config")?; let key = *self.named_types.get("graphics.Key")?; let scene = *self.named_types.get("graphics.Scene")?; let update = self.function_type(vec![state, key], state); - let render = self.function_type(vec![state], scene); + let render = self.function_type_with_modes(vec![state], vec![true], scene); let result = self .interner .intern(Type::Result(TypeInterner::NOTHING, TypeInterner::STRING)); Some(( - vec![TypeInterner::ERROR, config, state, update, render], + vec![TypeInterner::GRAPHICS, config, state, update, render], result, )) } // Private random kernels; public signatures live in stdlib/random.jett. - "random.__bounded" => self.no_type_args_signature( + IntrinsicId::RandomBounded => self.no_type_args_signature( &name, type_args, span, vec![ - TypeInterner::ERROR, + TypeInterner::RANDOM, TypeInterner::INT64, TypeInterner::INT64, ], TypeInterner::INT64, ), - "random.__unit_float64" => self.no_type_args_signature( + IntrinsicId::RandomUnitFloat64 => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::ERROR], + vec![TypeInterner::RANDOM], TypeInterner::FLOAT64, ), - "random.__bool" => self.no_type_args_signature( + IntrinsicId::RandomBool => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::ERROR], + vec![TypeInterner::RANDOM], TypeInterner::BOOL, ), - "string.__repeat" => self.no_type_args_signature( + IntrinsicId::StringRepeat => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::STRING, TypeInterner::INT64], TypeInterner::STRING, ), - "string.__slice" => self.no_type_args_signature( + IntrinsicId::StringSlice => self.no_type_args_signature( &name, type_args, span, @@ -3947,77 +5493,77 @@ impl<'a> TypeChecker<'a> { ], TypeInterner::STRING, ), - "string.__slugify" => self.no_type_args_signature( + IntrinsicId::StringSlugify => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::STRING], TypeInterner::STRING, ), - "map.__new" => { + IntrinsicId::MapNew => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![], map_ty)) } - "map.__length" => { + IntrinsicId::MapLength => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![map_ty], TypeInterner::INT64)) } - "map.__has" => { + IntrinsicId::MapHas => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![map_ty, k], TypeInterner::BOOL)) } - "map.__get" => { + IntrinsicId::MapGet => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![map_ty, k], self.interner.intern(Type::Optional(v)))) } - "map.__insert" => { + IntrinsicId::MapInsert => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![map_ty, k, v], map_ty)) } - "map.__remove" => { + IntrinsicId::MapRemove => { let (k, v) = self.map_type_args(&name, type_args, span); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![map_ty, k], map_ty)) } - "map.__from_lists" => { + IntrinsicId::MapFromLists => { let (k, v) = self.map_type_args(&name, type_args, span); let keys_ty = self.interner.intern(Type::List(k)); let values_ty = self.interner.intern(Type::List(v)); let map_ty = self.interner.intern(Type::Map(k, v)); Some((vec![keys_ty, values_ty], map_ty)) } - "set.__new" => { + IntrinsicId::SetNew => { let inner = self.set_type_arg(&name, type_args, span); Some((vec![], self.interner.intern(Type::Set(inner)))) } - "set.__add" | "set.__remove" => { + IntrinsicId::SetAdd | IntrinsicId::SetRemove => { let inner = self.set_type_arg(&name, type_args, span); let set_ty = self.interner.intern(Type::Set(inner)); Some((vec![set_ty, inner], set_ty)) } - "set.__contains" => { + IntrinsicId::SetContains => { let inner = self.set_type_arg(&name, type_args, span); let set_ty = self.interner.intern(Type::Set(inner)); Some((vec![set_ty, inner], TypeInterner::BOOL)) } - "set.__length" => { + IntrinsicId::SetLength => { let inner = self.set_type_arg(&name, type_args, span); let set_ty = self.interner.intern(Type::Set(inner)); Some((vec![set_ty], TypeInterner::INT64)) } - "list.__sort_by" => { + IntrinsicId::ListSortBy => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); let key_fn_ty = self.function_type(vec![inner], TypeInterner::INT64); Some((vec![list_ty, key_fn_ty], list_ty)) } - "list.__group_by" => { + IntrinsicId::ListGroupBy => { let inner = self.optional_type_arg(&name, type_args, span); let list_ty = self.interner.intern(Type::List(inner)); let group_map_ty = self @@ -4026,45 +5572,47 @@ impl<'a> TypeChecker<'a> { let key_fn_ty = self.function_type(vec![inner], TypeInterner::STRING); Some((vec![list_ty, key_fn_ty], group_map_ty)) } - "uuid.new" => { + IntrinsicId::UuidNew => { self.no_type_args_signature(&name, type_args, span, vec![], TypeInterner::STRING) } // char-level string operations - "string.__index_of" => { + IntrinsicId::StringIndexOf => { self.expect_no_type_args(&name, type_args, span); let opt_int = self.interner.intern(Type::Optional(TypeInterner::INT64)); Some((vec![TypeInterner::STRING, TypeInterner::STRING], opt_int)) } - "string.__count" => self.no_type_args_signature( + IntrinsicId::StringCount => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::STRING, TypeInterner::STRING], TypeInterner::INT64, ), - "string.__to_upper_first" | "string.__to_lower_first" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::STRING], - TypeInterner::STRING, - ), - "string.__is_numeric" | "string.__is_alpha" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::STRING], - TypeInterner::BOOL, - ), + IntrinsicId::StringToUpperFirst | IntrinsicId::StringToLowerFirst => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::STRING], + TypeInterner::STRING, + ), + IntrinsicId::StringIsNumeric | IntrinsicId::StringIsAlpha => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::STRING], + TypeInterner::BOOL, + ), // Private encoding kernels; public signatures live in stdlib/encoding.jett. - "encoding.__base64_encode" => self.no_type_args_signature( + IntrinsicId::EncodingBase64Encode => self.no_type_args_signature( &name, type_args, span, vec![TypeInterner::BYTES], TypeInterner::STRING, ), - "encoding.__base64_decode" | "encoding.__hex_decode" => { + IntrinsicId::EncodingBase64Decode | IntrinsicId::EncodingHexDecode => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -4072,14 +5620,15 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(TypeInterner::BYTES, TypeInterner::STRING)), )) } - "encoding.__url_encode" | "encoding.__form_encode" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::STRING], - TypeInterner::STRING, - ), - "encoding.__url_decode" | "encoding.__form_decode" => { + IntrinsicId::EncodingUrlEncode | IntrinsicId::EncodingFormEncode => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::STRING], + TypeInterner::STRING, + ), + IntrinsicId::EncodingUrlDecode | IntrinsicId::EncodingFormDecode => { self.expect_no_type_args(&name, type_args, span); Some(( vec![TypeInterner::STRING], @@ -4088,68 +5637,72 @@ impl<'a> TypeChecker<'a> { )) } // Private crypto kernels; public signatures live in stdlib/crypto.jett. - "crypto.__sha256" | "crypto.__sha512" | "crypto.__md5" => self.no_type_args_signature( - &name, - type_args, - span, - vec![TypeInterner::BYTES], - TypeInterner::BYTES, - ), - "crypto.__hmac_sha256" => { + IntrinsicId::CryptoSha256 | IntrinsicId::CryptoSha512 | IntrinsicId::CryptoMd5 => self + .no_type_args_signature( + &name, + type_args, + span, + vec![TypeInterner::BYTES], + TypeInterner::BYTES, + ), + IntrinsicId::CryptoHmacSha256 => { self.expect_no_type_args(&name, type_args, span); let secret_bytes = self.interner.intern(Type::Secret(TypeInterner::BYTES)); Some((vec![secret_bytes, TypeInterner::BYTES], secret_bytes)) } - "Clock.__now" => self.no_type_args_signature( + IntrinsicId::ClockNow => self.no_type_args_signature( &name, type_args, span, - vec![TypeInterner::ERROR], + vec![TypeInterner::CLOCK], TypeInterner::INT64, ), - "Environment.__get" => { + IntrinsicId::EnvironmentGet => { self.expect_no_type_args(&name, type_args, span); let optional_string = self.interner.intern(Type::Optional(TypeInterner::STRING)); let result = self .interner .intern(Type::Result(optional_string, TypeInterner::STRING)); - Some((vec![TypeInterner::ERROR, TypeInterner::STRING], result)) + Some(( + vec![TypeInterner::ENVIRONMENT, TypeInterner::STRING], + result, + )) } - "Environment.__args" => { + IntrinsicId::EnvironmentArgs => { self.expect_no_type_args(&name, type_args, span); let list_string = self.interner.intern(Type::List(TypeInterner::STRING)); - Some((vec![TypeInterner::ERROR], list_string)) + Some((vec![TypeInterner::ENVIRONMENT], list_string)) } - "test.mock.__random" => { + IntrinsicId::TestMockRandom => { self.expect_no_type_args(&name, type_args, span); let step = *self.named_types.get("test.mock.RandomStep")?; let steps = self.interner.intern(Type::List(step)); - Some((vec![steps], TypeInterner::ERROR)) + Some((vec![steps], TypeInterner::RANDOM)) } - "test.mock.__clock" => { + IntrinsicId::TestMockClock => { self.expect_no_type_args(&name, type_args, span); let step = *self.named_types.get("test.mock.ClockStep")?; let steps = self.interner.intern(Type::List(step)); - Some((vec![steps], TypeInterner::ERROR)) + Some((vec![steps], TypeInterner::CLOCK)) } - "test.mock.__environment" => { + IntrinsicId::TestMockEnvironment => { self.expect_no_type_args(&name, type_args, span); let arguments = self.interner.intern(Type::List(TypeInterner::STRING)); let entry = *self.named_types.get("test.mock.EnvironmentEntry")?; let entries = self.interner.intern(Type::List(entry)); - Some((vec![arguments, entries], TypeInterner::ERROR)) + Some((vec![arguments, entries], TypeInterner::ENVIRONMENT)) } - "log.__emit" => { + IntrinsicId::LogEmit => { self.expect_no_type_args(&name, type_args, span); let event = *self.named_types.get("log.Event")?; let error = *self.named_types.get("log.Error")?; let result = self .interner .intern(Type::Result(TypeInterner::NOTHING, error)); - Some((vec![TypeInterner::ERROR, event], result)) + Some((vec![TypeInterner::LOG, event], result)) } // Private CSV kernels; public signatures live in stdlib/csv.jett. - "csv.__parse" => { + IntrinsicId::CsvParse => { self.expect_no_type_args(&name, type_args, span); let list_string = self.interner.intern(Type::List(TypeInterner::STRING)); let rows_ty = self.interner.intern(Type::List(list_string)); @@ -4158,13 +5711,13 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(rows_ty, TypeInterner::STRING)); Some((vec![TypeInterner::STRING], result_ty)) } - "csv.__stringify" => { + IntrinsicId::CsvStringify => { self.expect_no_type_args(&name, type_args, span); let list_string = self.interner.intern(Type::List(TypeInterner::STRING)); let rows_ty = self.interner.intern(Type::List(list_string)); Some((vec![rows_ty], TypeInterner::STRING)) } - "csv.__parse_with_header" => { + IntrinsicId::CsvParseWithHeader => { self.expect_no_type_args(&name, type_args, span); let row_ty = self .interner @@ -4175,7 +5728,16 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(rows_ty, TypeInterner::STRING)); Some((vec![TypeInterner::STRING], result_ty)) } - _ => None, + // These signatures are resolved by dedicated paths before this + // central compiler-builtin table. + IntrinsicId::BitfieldFromBytes + | IntrinsicId::BitfieldToBytes + | IntrinsicId::MathAbs + | IntrinsicId::MathMax + | IntrinsicId::MathMin + | IntrinsicId::Print + | IntrinsicId::Println + | IntrinsicId::Range => None, } } @@ -4183,7 +5745,160 @@ impl<'a> TypeChecker<'a> { // Module // ------------------------------------------------------------------ + fn check_written_function_return_views(&mut self, module: &Module) { + // Signature syntax is checked even for unused generic declarations. + // Do not resolve named aliases or inspect deferred function bodies here. + for item in &module.items { + match item { + Item::Function(function) => self.check_function_return_view_syntax( + &function.params, + function.return_type.as_ref(), + ), + Item::Mutual(block) => { + for function in &block.declarations { + self.check_function_return_view_syntax( + &function.params, + function.return_type.as_ref(), + ); + } + } + Item::Interface(interface) => { + for method in &interface.methods { + self.check_function_return_view_syntax( + &method.params, + method.return_type.as_ref(), + ); + } + } + Item::Struct(definition) => { + for field in &definition.fields { + self.check_callable_return_view_syntax(&field.ty); + } + for method in &definition.methods { + self.check_function_return_view_syntax( + &method.params, + method.return_type.as_ref(), + ); + } + } + Item::Implement(block) => { + for method in &block.methods { + self.check_function_return_view_syntax( + &method.params, + method.return_type.as_ref(), + ); + } + } + _ => {} + } + } + } + + fn check_function_return_view_syntax( + &mut self, + params: &[ast::Param], + return_type: Option<&TypeExpr>, + ) { + for param in params { + self.check_callable_return_view_syntax(¶m.ty); + } + if let Some(return_type) = return_type { + self.check_return_view_annotation(return_type); + self.check_callable_return_view_syntax(return_type); + } + } + + fn check_return_view_annotation(&mut self, return_type: &TypeExpr) { + if Self::type_is_view(return_type) + && self + .diagnosed_view_return_annotations + .insert(return_type.span()) + { + self.sink.emit(crate::ownership::cannot_declare_view_return( + return_type.span(), + )); + } + } + + fn check_callable_return_view_syntax(&mut self, ty: &TypeExpr) { + match ty { + TypeExpr::Function(params, result, _) => { + self.check_return_view_annotation(result); + for param in params { + self.check_callable_return_view_syntax(param); + } + self.check_callable_return_view_syntax(result); + } + TypeExpr::Generic(_, args, _) => { + for arg in args { + self.check_callable_return_view_syntax(arg); + } + } + TypeExpr::View(inner, _) | TypeExpr::StateQualified(inner, _, _) => { + self.check_callable_return_view_syntax(inner); + } + TypeExpr::Named(_) => {} + } + } + + fn register_resource_kernels(&mut self) -> Result<(), ResourceHookError> { + for entry in self.resolve.resource_kernels.iter() { + let resource_type = *self.type_env.get(&entry.resource_definition).ok_or( + ResourceHookError::MissingResourceType(entry.resource_definition), + )?; + let function_type = entry + .recipe + .intern_signature(&mut self.interner, resource_type) + .ok_or(ResourceHookError::InvalidResourceType( + entry.resource_definition, + ))?; + self.type_env.insert(entry.definition, function_type); + self.resource_hooks.insert( + entry.definition, + CheckedResourceHook { + definition: entry.definition, + resource_definition: entry.resource_definition, + resource_type, + kind: entry.recipe.kind(), + function_type, + }, + ); + let definition = self.resolve.scope_table.def(entry.definition); + let Type::Function { + params, + return_type, + .. + } = self.interner.resolve(function_type) + else { + return Err(ResourceHookError::InvalidFunctionShape(entry.definition)); + }; + self.function_signatures + .insert(definition.name.clone(), (params.clone(), *return_type)); + self.function_parameter_names.insert( + definition.name.clone(), + entry + .recipe + .parameter_names() + .iter() + .map(|name| (*name).to_owned()) + .collect(), + ); + let is_pure = self.function_parameters_are_pure(params); + self.purity_map.insert(definition.name.clone(), is_pure); + } + Ok(()) + } + fn check_module(&mut self, module: &Module) { + self.check_written_function_return_views(module); + self.constant_declarations = module + .items + .iter() + .filter_map(|item| match item { + Item::VarDecl(decl) if !decl.mutable => Some(decl.name.span), + _ => None, + }) + .collect(); self.collect_type_aliases(module); // First pass: predeclare all user-defined types so function signatures, @@ -4206,6 +5921,11 @@ impl<'a> TypeChecker<'a> { } } + if let Err(error) = self.register_resource_kernels() { + self.resource_hook_error = Some(error); + return; + } + // Second pass: fill in the struct/enum contents now that all names exist. let mut current_file = None; let mut current_namespace = None; @@ -4381,7 +6101,16 @@ impl<'a> TypeChecker<'a> { } Item::Bitfield(_) => {} Item::Actor(def) => self.check_actor(def, current_namespace.as_deref()), - Item::VarDecl(decl) => self.check_var_decl(decl), + Item::VarDecl(decl) => { + if decl.mutable { + self.sink + .emit(errors::mutable_global(&decl.name.name, decl.name.span)); + } else if !self.is_constant_initializer(&decl.value) { + self.sink + .emit(errors::constant_initializer(decl.value.span())); + } + self.check_var_decl(decl); + } Item::Verify(verify) => self.check_verify_block(verify), Item::Property(prop) => self.check_property_block(prop), _ => {} @@ -4481,6 +6210,7 @@ impl<'a> TypeChecker<'a> { self.register_trusted_stdlib_named_type(&trusted_name, ty, def.name.span); if let Some(def_id) = self.declaration_def_id(def.name.span) { self.type_env.insert(def_id, ty); + self.actor_definitions.insert(aid, def_id); } } @@ -4659,6 +6389,22 @@ impl<'a> TypeChecker<'a> { }) .collect(); + if let Some(&actor_definition) = self.actor_definitions.get(&aid) { + for (handler_index, handler) in def.handlers.iter().enumerate() { + self.actor_message_modes.insert( + CheckedActorHandlerTarget { + actor_definition, + actor: aid, + handler_index, + }, + handler + .params + .iter() + .map(|param| param.view || Self::type_is_view(¶m.ty)) + .collect(), + ); + } + } self.interner.update_actor( aid, TypeActorDef { @@ -4678,86 +6424,132 @@ impl<'a> TypeChecker<'a> { let Type::Actor(aid) = *self.interner.resolve(actor_ty) else { return; }; + let Some(&actor_definition) = self.actor_definitions.get(&aid) else { + return; + }; let actor_def = self.interner.resolve_actor(aid).clone(); + let mut captures = Vec::new(); - // Register state fields and capability params in a fresh type env scope. - let mut local_env: HashMap = HashMap::new(); - for (name, ty) in &actor_def.capability_params { - local_env.insert(name.clone(), *ty); - } - for (name, ty) in &actor_def.state_fields { - local_env.insert(name.clone(), *ty); - } - - for (param_ast, (_, param_ty)) in def + for (parameter, (param, (_, ty))) in def .capability_params .iter() - .zip(actor_def.capability_params.iter()) + .zip(&actor_def.capability_params) + .enumerate() { - if let Some(def_id) = self.declaration_def_id(param_ast.name.span) { - self.type_env.insert(def_id, *param_ty); + self.record_debug_type_name(¶m.name, ¶m.ty); + if let Some(definition) = self.declaration_def_id(param.name.span) { + self.type_env.insert(definition, *ty); + self.record_expression_type(param.name.span, *ty); + } + self.record_assignment_binding( + ¶m.name, + param.mutable, + param.view || Self::type_is_view(¶m.ty), + ); + if let Some(&fact) = self.binding_facts.get(¶m.name.span) { + captures.push(CheckedActorHandlerBinding { + role: CheckedActorBindingRole::Capability { parameter }, + fact, + }); } } - // Type-check state field initializers. - for field in &def.state_fields { - let declared_ty = self.resolve_type_expr(&field.ty); - let init_ty = self.check_expr_for_expected(&field.value, declared_ty, true); + // Resolve/check sequential initializers before installing their own + // binding, exactly as the resolver's top-to-bottom actor scope does. + for (field_index, (field, (_, declared_ty))) in def + .state_fields + .iter() + .zip(&actor_def.state_fields) + .enumerate() + { + self.record_debug_type_name(&field.name, &field.ty); + let init_ty = self.check_expr_for_expected(&field.value, *declared_ty, true); + let mode = self.checked_binding_mode(field, init_ty); + if let Some(definition) = self.declaration_def_id(field.name.span) { + self.type_env.insert(definition, *declared_ty); + self.record_expression_type(field.name.span, *declared_ty); + } + self.record_binding_mode(field.name.span, mode); + self.record_assignment_binding( + &field.name, + field.mutable, + matches!(mode, CheckedBindingMode::View { .. }), + ); + if let Some(&fact) = self.binding_facts.get(&field.name.span) { + captures.push(CheckedActorHandlerBinding { + role: CheckedActorBindingRole::State { field: field_index }, + fact, + }); + } if init_ty != TypeInterner::ERROR - && declared_ty != TypeInterner::ERROR - && !self.types_compatible(declared_ty, init_ty) + && *declared_ty != TypeInterner::ERROR + && !self.types_compatible(*declared_ty, init_ty) { self.sink.emit(errors::type_mismatch( - &self.type_name(declared_ty), + &self.type_name(*declared_ty), &self.type_name(init_ty), field.value.span(), )); } } - // Type-check each handler body. let prev_return = self.current_return_type; let prev_respond = self.current_respond_type; let prev_function_name = self.current_function_name.clone(); - - for (handler_ast, handler_def) in def.handlers.iter().zip(actor_def.messages.iter()) { - // Set up respond type. - let responds_ty = if handler_def.responds == TypeInterner::NOTHING { - None - } else { - Some(handler_def.responds) - }; - self.current_respond_type = responds_ty; + for (handler_index, (handler, message)) in + def.handlers.iter().zip(&actor_def.messages).enumerate() + { + self.current_respond_type = + (message.responds != TypeInterner::NOTHING).then_some(message.responds); self.current_return_type = None; - self.current_function_name = - Some(format!("{}.{}", actor_def.name, handler_ast.name.name)); - - // Register message params in the type env temporarily. - for (param_ast, (_, param_ty)) in - handler_ast.params.iter().zip(handler_def.params.iter()) + self.current_function_name = Some(format!("{}.{}", actor_def.name, handler.name.name)); + let mut bindings = captures.clone(); + for (parameter, (param, (_, ty))) in + handler.params.iter().zip(&message.params).enumerate() { - if let Some(def_id) = self.declaration_def_id(param_ast.name.span) { - self.type_env.insert(def_id, *param_ty); + self.record_debug_type_name(¶m.name, ¶m.ty); + if let Some(definition) = self.declaration_def_id(param.name.span) { + self.type_env.insert(definition, *ty); + self.record_expression_type(param.name.span, *ty); } - } - - // Register local_env vars into type_env using resolve declarations. - for field in &def.state_fields { - if let Some(def_id) = self.declaration_def_id(field.name.span) { - let ty = self.resolve_type_expr(&field.ty); - self.type_env.insert(def_id, ty); - } - } - for cap in &def.capability_params { - if let Some(def_id) = self.declaration_def_id(cap.name.span) { - let ty = self.resolve_type_expr(&cap.ty); - self.type_env.insert(def_id, ty); + self.record_assignment_binding( + ¶m.name, + param.mutable, + param.view || Self::type_is_view(¶m.ty), + ); + if let Some(&fact) = self.binding_facts.get(¶m.name.span) { + bindings.push(CheckedActorHandlerBinding { + role: CheckedActorBindingRole::Message { parameter }, + fact, + }); } } - - self.check_block(&handler_ast.body); + self.check_block(&handler.body); + let mut body = self.checked_body_facts(handler.body.span); + for binding in &bindings { + body.binding_facts + .insert(binding.fact.declaration_span, binding.fact); + body.binding_modes + .insert(binding.fact.declaration_span, binding.fact.mode); + } + let target = CheckedActorHandlerTarget { + actor_definition, + actor: aid, + handler_index, + }; + self.actor_handlers.insert( + target, + CheckedActorHandler::new( + target, + handler.body.span, + bindings, + message.responds, + body, + self.actor_check_session.clone(), + def, + ), + ); } - self.current_return_type = prev_return; self.current_respond_type = prev_respond; self.current_function_name = prev_function_name; @@ -5208,6 +7000,7 @@ impl<'a> TypeChecker<'a> { let (param_types, return_type) = self.function_decl_signature(decl); let fn_type = self.interner.intern(Type::Function { params: param_types, + view_params: decl.params.iter().map(|param| param.view).collect(), return_type, }); @@ -5231,6 +7024,7 @@ impl<'a> TypeChecker<'a> { let fn_type = self.interner.intern(Type::Function { params: param_types, + view_params: func.params.iter().map(|param| param.view).collect(), return_type, }); @@ -5613,35 +7407,27 @@ impl<'a> TypeChecker<'a> { function_name: &str, func: &FunctionDef, concrete_args: &[TypeId], + type_argument_reflections: Vec, subst: HashMap, kind_subst: HashMap, param_facts: ReflectionParamFacts, ) { let uses_type_param_reflection = self.generic_function_uses_type_param_reflection(func); - let branch_specializable = uses_type_param_reflection - && self.generic_function_reflection_is_branch_specializable(func); - if uses_type_param_reflection && !branch_specializable { - return; - } - let specialize_reflection_branches = branch_specializable || !param_facts.is_empty(); + // Trusted facts remain available inside closures and arbitrary expression + // contexts. Unknown guards fall back to checking every branch normally. + let specialize_reflection_branches = uses_type_param_reflection || !param_facts.is_empty(); let definition = self.declaration_def_id(func.name.span); - - let kind_key = func - .type_params - .iter() - .map(|param| { - kind_subst - .get(¶m.name) - .cloned() - .unwrap_or_else(|| "unknown_type".to_string()) - }) - .collect::>(); + let specialization = Self::generic_specialization( + func, + &kind_subst, + &type_argument_reflections, + ¶m_facts, + ); let cache_key = ( function_name.to_string(), concrete_args.to_vec(), - kind_key, - param_facts.clone(), + specialization.clone(), ); if !self .checked_generic_function_instantiations @@ -5657,6 +7443,14 @@ impl<'a> TypeChecker<'a> { let instantiated_name = format!("{function_name}[{}]", type_arg_names.join(", ")); let old_subst = std::mem::replace(&mut self.type_var_subst, subst); + let old_reflections = std::mem::replace( + &mut self.type_var_reflections, + func.type_params + .iter() + .map(|param| param.name.clone()) + .zip(type_argument_reflections) + .collect(), + ); let old_kind_subst = std::mem::replace(&mut self.type_var_kind_tags, kind_subst); let parameter_types = func .params @@ -5669,7 +7463,7 @@ impl<'a> TypeChecker<'a> { .map(|ty| self.resolve_type_expr(ty)) .unwrap_or(TypeInterner::NOTHING); let manifest_index = definition.map(|definition| { - let identity = (definition, concrete_args.to_vec()); + let identity = (definition, concrete_args.to_vec(), specialization.clone()); if let Some(index) = self.generic_instantiation_indices.get(&identity) { *index } else { @@ -5679,14 +7473,30 @@ impl<'a> TypeChecker<'a> { .push(CheckedGenericFunctionInstantiation { definition, concrete_args: concrete_args.to_vec(), + specialization: specialization.clone(), parameter_types: parameter_types.clone(), return_type, type_map: HashMap::new(), + source_type_map: HashMap::new(), + debug_type_names: HashMap::new(), + binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + assignment_places: HashMap::new(), + call_ownership: HashMap::new(), + actor_message_calls: HashMap::new(), generic_calls: HashMap::new(), + intrinsic_ids: HashMap::new(), + intrinsic_type_arguments: HashMap::new(), + intrinsic_reflection_arguments: HashMap::new(), call_argument_orders: HashMap::new(), method_calls: HashMap::new(), + interface_calls: HashMap::new(), + method_values: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), + static_selections: HashMap::new(), + comptime_type_bindings: HashMap::new(), }); index } @@ -5700,6 +7510,12 @@ impl<'a> TypeChecker<'a> { let old_handle_body_depth = self.handle_body_depth; let old_respond_type = self.current_respond_type; let old_specialize_reflection_branches = self.specialize_reflection_branches; + // Nested generic checks reuse source DefIds for parameters and locals. + // Preserve the outer concrete types while checking the inner body. + let old_type_env = self.type_env.clone(); + // The instantiated body belongs to its own declaration, not to the + // closure whose call triggered instantiation. + let old_closure_capture_scopes = std::mem::take(&mut self.closure_capture_scopes); self.in_verify_block = false; self.in_property_block = false; @@ -5713,11 +7529,27 @@ impl<'a> TypeChecker<'a> { .push(ActiveGenericInstantiation { manifest_index, type_map: HashMap::new(), + source_type_map: HashMap::new(), + debug_type_names: HashMap::new(), + binding_modes: HashMap::new(), + binding_facts: HashMap::new(), + assignment_places: HashMap::new(), + call_ownership: HashMap::new(), + actor_message_calls: HashMap::new(), + invocation_signatures: HashMap::new(), generic_calls: HashMap::new(), + intrinsic_ids: HashMap::new(), + intrinsic_type_arguments: HashMap::new(), + intrinsic_reflection_arguments: HashMap::new(), call_argument_orders: HashMap::new(), method_calls: HashMap::new(), + interface_calls: HashMap::new(), + method_values: HashMap::new(), struct_constructions: HashMap::new(), + pipeline_step_input_types: HashMap::new(), pipeline_step_call_types: HashMap::new(), + static_selections: HashMap::new(), + comptime_type_bindings: HashMap::new(), }); } @@ -5730,22 +7562,52 @@ impl<'a> TypeChecker<'a> { .active_generic_instantiations .pop() .expect("generic instantiation fact scope must be balanced"); - let entry = &mut self.generic_function_instantiations[active.manifest_index]; - entry.type_map.extend(active.type_map); - entry.generic_calls.extend(active.generic_calls); - entry - .call_argument_orders - .extend(active.call_argument_orders); - entry.method_calls.extend(active.method_calls); - entry - .struct_constructions - .extend(active.struct_constructions); - entry - .pipeline_step_call_types - .extend(active.pipeline_step_call_types); + let conflicts = { + let entry = &mut self.generic_function_instantiations[active.manifest_index]; + entry.type_map.extend(active.type_map); + entry.source_type_map.extend(active.source_type_map); + entry.debug_type_names.extend(active.debug_type_names); + entry.binding_modes.extend(active.binding_modes); + entry.binding_facts.extend(active.binding_facts); + entry.assignment_places.extend(active.assignment_places); + entry.call_ownership.extend(active.call_ownership); + entry.actor_message_calls.extend(active.actor_message_calls); + entry.generic_calls.extend(active.generic_calls); + entry.intrinsic_ids.extend(active.intrinsic_ids); + entry + .intrinsic_type_arguments + .extend(active.intrinsic_type_arguments); + entry + .intrinsic_reflection_arguments + .extend(active.intrinsic_reflection_arguments); + entry + .call_argument_orders + .extend(active.call_argument_orders); + entry.method_calls.extend(active.method_calls); + entry.interface_calls.extend(active.interface_calls); + entry.method_values.extend(active.method_values); + entry + .struct_constructions + .extend(active.struct_constructions); + entry + .pipeline_step_input_types + .extend(active.pipeline_step_input_types); + entry + .pipeline_step_call_types + .extend(active.pipeline_step_call_types); + entry + .comptime_type_bindings + .extend(active.comptime_type_bindings); + merge_static_selections(&mut entry.static_selections, active.static_selections) + }; + for span in conflicts { + self.sink + .emit(errors::conflicting_generic_static_selection(span)); + } } self.type_var_subst = old_subst; + self.type_var_reflections = old_reflections; self.type_var_kind_tags = old_kind_subst; self.current_return_type = old_return_type; self.current_function_name = old_function_name; @@ -5756,12 +7618,44 @@ impl<'a> TypeChecker<'a> { self.handle_body_depth = old_handle_body_depth; self.current_respond_type = old_respond_type; self.specialize_reflection_branches = old_specialize_reflection_branches; + self.type_env = old_type_env; + self.closure_capture_scopes = old_closure_capture_scopes; } - fn record_expression_type(&mut self, span: Span, ty: TypeId) { - self.type_map.insert(span, ty); - if let Some(active) = self.active_generic_instantiations.last_mut() { - active.type_map.insert(span, ty); + fn record_debug_type_name(&mut self, name: &ast::Ident, ty: &TypeExpr) { + if let Some(definition) = self.declaration_def_id(name.span) { + self.declared_source_types.insert(definition, ty.clone()); + } + let namespace = self + .declaration_def_id(name.span) + .and_then(|id| self.resolve.scope_table.def(id).namespace.as_deref()); + let label = self.reflection_type_expr_display(ty, namespace); + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.debug_type_names.insert(name.span, label); + } else { + self.debug_type_names.insert(name.span, label); + } + } + + fn record_expression_type(&mut self, span: Span, ty: TypeId) { + self.type_map.insert(span, ty); + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.type_map.insert(span, ty); + } + } + + fn record_source_expression_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.source_type_map.insert(span, ty); + } else { + self.source_type_map.insert(span, ty); + } + } + + fn checked_source_expression_type(&self, span: Span) -> Option { + match self.active_generic_instantiations.last() { + Some(active) => active.source_type_map.get(&span).copied(), + None => self.source_type_map.get(&span).copied(), } } @@ -5774,16 +7668,31 @@ impl<'a> TypeChecker<'a> { interface_type: Option, ) { let index = self.method_definitions.len(); + self.method_parameter_modes.insert( + method.span, + signature.params.iter().map(|(_, _, view)| *view).collect(), + ); self.method_definitions.push(CheckedMethodDefinition { source_span: method.span, owner_type, owner_name: owner_name.to_string(), + method_name: method.name.name.clone(), interface_name: interface_type.map(|interface| self.type_name(interface)), + interface_type, parameter_types: signature.params.iter().map(|(_, ty, _)| *ty).collect(), return_type: signature.return_type, }); - self.method_definitions_by_owner - .insert((owner_type, method.name.name.clone()), index); + let owner_key = (owner_type, method.name.name.clone()); + // Concrete type lookup prefers an inherent method over an interface + // implementation. Preserve that same choice for executable targets; + // interface-qualified dispatch has its own exact implementation map. + let existing_inherent = self + .method_definitions_by_owner + .get(&owner_key) + .is_some_and(|&existing| self.method_definitions[existing].interface_name.is_none()); + if interface_type.is_none() || !existing_inherent { + self.method_definitions_by_owner.insert(owner_key, index); + } if let Some(interface_type) = interface_type { self.interface_method_definitions.insert( (interface_type, owner_type, method.name.name.clone()), @@ -5800,6 +7709,65 @@ impl<'a> TypeChecker<'a> { } } + fn record_intrinsic_id(&mut self, span: Span, name: &str) { + let Some(intrinsic) = IntrinsicId::from_callable_name(name) else { + self.sink.emit(errors::not_callable( + &format!("unclassified compiler intrinsic `{name}`"), + span, + )); + return; + }; + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.intrinsic_ids.insert(span, intrinsic); + } else { + self.intrinsic_ids.insert(span, intrinsic); + } + } + + fn record_intrinsic_type_arguments(&mut self, span: Span, type_args: &[TypeExpr]) { + if type_args.is_empty() { + return; + } + let resolved = type_args + .iter() + .map(|type_arg| self.resolve_type_expr(type_arg)) + .collect::>(); + if resolved.contains(&TypeInterner::ERROR) { + return; + } + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.intrinsic_type_arguments.insert(span, resolved); + } else { + self.intrinsic_type_arguments.insert(span, resolved); + } + } + + fn record_intrinsic_reflection_arguments(&mut self, span: Span, type_args: &[TypeExpr]) { + let intrinsic = self + .active_generic_instantiations + .last() + .and_then(|active| active.intrinsic_ids.get(&span)) + .or_else(|| self.intrinsic_ids.get(&span)); + if !intrinsic.is_some_and(|id| id.canonical_name().starts_with("type.")) { + return; + } + let concrete_args = type_args + .iter() + .map(|arg| self.resolve_type_expr(arg)) + .collect::>(); + if concrete_args.contains(&TypeInterner::ERROR) { + return; + } + let reflected = self.reflection_arguments_for_type_args(type_args, &concrete_args); + if let Some(active) = self.active_generic_instantiations.last_mut() { + active + .intrinsic_reflection_arguments + .insert(span, reflected); + } else { + self.intrinsic_reflection_arguments.insert(span, reflected); + } + } + fn record_call_argument_order(&mut self, span: Span, source_indices: Vec) { let order = CheckedCallArgumentOrder { source_indices }; if let Some(active) = self.active_generic_instantiations.last_mut() { @@ -5818,503 +7786,1057 @@ impl<'a> TypeChecker<'a> { } } - fn record_struct_construction( - &mut self, - span: Span, - struct_type: TypeId, - validates_refinements: bool, - ) { - let construction = CheckedStructConstruction { - struct_type, - validates_refinements, + fn record_source_method_value(&mut self, owner_type: TypeId, name: &str, span: Span) { + let Some(&index) = self + .method_definitions_by_owner + .get(&(owner_type, name.to_string())) + else { + return; + }; + let value = CheckedMethodValue::Source { + source_span: self.method_definitions[index].source_span, }; + self.record_method_value(span, value); + } + + fn record_method_value(&mut self, span: Span, value: CheckedMethodValue) { if let Some(active) = self.active_generic_instantiations.last_mut() { - active.struct_constructions.insert(span, construction); + active.method_values.insert(span, value); } else { - self.struct_constructions.insert(span, construction); + self.method_values.insert(span, value); } } - fn record_pipeline_step_call_type(&mut self, span: Span, ty: TypeId) { + fn checked_expression_type(&self, span: Span) -> Option { + match self.active_generic_instantiations.last() { + Some(active) => active.type_map.get(&span).copied(), + None => self.type_map.get(&span).copied(), + } + } + + fn record_binding_fact(&mut self, fact: CheckedBindingFact) { if let Some(active) = self.active_generic_instantiations.last_mut() { - active.pipeline_step_call_types.insert(span, ty); + active.binding_facts.insert(fact.declaration_span, fact); } else { - self.pipeline_step_call_types.insert(span, ty); + self.binding_facts.insert(fact.declaration_span, fact); } } - fn record_generic_call_for_template( - &mut self, - span: Span, - func: &FunctionDef, - concrete_args: &[TypeId], - ) { - let Some(definition) = self.declaration_def_id(func.name.span) else { - return; + fn caller_binding_fact(&self, definition: DefId) -> Option { + let span = self.resolve.scope_table.def(definition).span; + let recorded = match self.active_generic_instantiations.last() { + Some(active) => active.binding_facts.get(&span), + None => self.binding_facts.get(&span), }; - self.record_generic_call_target( - span, - CheckedGenericCall { - definition, - concrete_args: concrete_args.to_vec(), + if let Some(fact) = recorded { + return Some(*fact); + } + // Captures may be declared outside this body. + // Their identity and concrete type still come from this checked session. + let ty = *self.type_env.get(&definition)?; + let assignment = self.assignment_bindings.get(&span); + Some(CheckedBindingFact { + definition, + declaration_span: span, + ty, + mode: if assignment == Some(&AssignmentBinding::View) + && !crate::ownership::is_implicitly_copyable(&self.interner, ty) + { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned }, - ); - } - - fn generic_function_uses_type_param_reflection(&self, func: &FunctionDef) -> bool { - let type_params = func - .type_params - .iter() - .map(|param| param.name.clone()) - .collect::>(); - self.block_uses_type_param_reflection(&func.body, &type_params) - } - - fn generic_function_reflection_is_branch_specializable(&self, func: &FunctionDef) -> bool { - let type_params = func - .type_params - .iter() - .map(|param| param.name.clone()) - .collect::>(); - self.block_reflection_is_branch_specializable(&func.body, &type_params) + mutable: assignment == Some(&AssignmentBinding::OwnedMutable), + }) } - fn block_reflection_is_branch_specializable( - &self, - block: &Block, - type_params: &HashSet, - ) -> bool { - self.block_reflection_is_branch_specializable_in_context( - block, - type_params, - ReflectionBranchContext::TopLevel, - ) + fn caller_definition_is_producer(&self, definition: DefId) -> bool { + let declared = self.resolve.scope_table.def(definition); + matches!(declared.kind, DefKind::Function | DefKind::Constant) + || self.constant_declarations.contains(&declared.span) } - fn block_reflection_is_branch_specializable_in_context( - &self, - block: &Block, - type_params: &HashSet, - context: ReflectionBranchContext, - ) -> bool { - block - .stmts + fn caller_is_qualified_unit_enum(&self, expression: &Expr) -> bool { + let Expr::FieldAccess(_, variant, span) = expression else { + return false; + }; + let Some(definition) = self.resolve.resolutions.get(span) else { + return false; + }; + if self.resolve.scope_table.def(*definition).kind != DefKind::Enum { + return false; + } + let Some(&enum_ty) = self.type_env.get(definition) else { + return false; + }; + if self.checked_source_expression_type(*span) != Some(enum_ty) { + return false; + } + let Type::Enum(id) = *self.interner.resolve(enum_ty) else { + return false; + }; + self.interner + .resolve_enum(id) + .variants .iter() - .all(|stmt| self.stmt_reflection_is_branch_specializable(stmt, type_params, context)) + .any(|declared| declared.name == variant.name && declared.fields.is_empty()) } - fn stmt_reflection_is_branch_specializable( - &self, - stmt: &Stmt, - type_params: &HashSet, - context: ReflectionBranchContext, - ) -> bool { - if context == ReflectionBranchContext::StaticReflectionBranch { - return true; - } - - match stmt { - Stmt::If(if_stmt) => { - let condition_static = - self.expr_is_potential_static_reflection_condition(&if_stmt.condition); - if self.expr_uses_type_param_reflection(&if_stmt.condition, type_params) - && !condition_static - { - return false; - } - - let then_context = if condition_static { - ReflectionBranchContext::StaticReflectionBranch - } else { - ReflectionBranchContext::RuntimeBranch - }; - let then_ok = self.block_reflection_is_branch_specializable_in_context( - &if_stmt.then_block, - type_params, - then_context, - ); - - let mut all_conditions_static = condition_static; - let else_ifs_ok = if_stmt.else_ifs.iter().all(|(condition, block)| { - let else_if_static = - self.expr_is_potential_static_reflection_condition(condition); - all_conditions_static &= else_if_static; - if self.expr_uses_type_param_reflection(condition, type_params) - && !else_if_static - { - return false; - } - let else_if_context = if else_if_static { - ReflectionBranchContext::StaticReflectionBranch - } else { - ReflectionBranchContext::RuntimeBranch - }; - self.block_reflection_is_branch_specializable_in_context( - block, - type_params, - else_if_context, - ) + fn caller_origin( + &mut self, + expression: Option<&Expr>, + written_view: bool, + ) -> CheckedCallerOrigin { + let Some(mut expression) = expression else { + return CheckedCallerOrigin::OwnedExpression; + }; + while let Expr::Paren(inner, _) + | Expr::View(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) = expression + { + expression = inner; + } + if matches!(expression, Expr::FieldAccess(..)) { + let resolved_function = self + .resolve + .resolutions + .get(&expression.span()) + .is_some_and(|definition| { + self.resolve.scope_table.def(*definition).kind == DefKind::Function }); - - then_ok - && else_ifs_ok - && if_stmt.else_block.as_ref().map_or(true, |block| { - let else_context = if all_conditions_static { - ReflectionBranchContext::StaticReflectionBranch - } else { - ReflectionBranchContext::RuntimeBranch - }; - self.block_reflection_is_branch_specializable_in_context( - block, - type_params, - else_context, - ) - }) - } - Stmt::VarDecl(decl) => { - !self.expr_uses_type_param_reflection(&decl.value, type_params) - || (context.permits_shape_reflection() - && (self - .expr_is_direct_reflection_statement_source(&decl.value, type_params) - || (!decl.mutable - && self.expr_is_reflection_local_fact_source( - &decl.value, - type_params, - )))) - } - Stmt::Return(ret) => ret.value.as_ref().map_or(true, |expr| { - !self.expr_uses_type_param_reflection(expr, type_params) - || (context.permits_shape_reflection() - && self.expr_is_direct_reflection_statement_source(expr, type_params)) - }), - Stmt::ComptimeTypeBind(bind) => { - self.comptime_type_bind_reflection_is_specializable(bind, type_params, context) - } - Stmt::For(for_stmt) => { - self.for_reflection_is_branch_specializable(for_stmt, type_params, context) - } - Stmt::Match(match_stmt) => { - self.match_reflection_is_branch_specializable(match_stmt, type_params, context) + let checked_method = match self.active_generic_instantiations.last() { + Some(active) => active.method_values.contains_key(&expression.span()), + None => self.method_values.contains_key(&expression.span()), + }; + // HIR lowers these exact checked declarations to FunctionRef or + // zero-payload EnumConstruct, not to a runtime field. A type or + // namespace spelling alone cannot replace declaration identity. + if resolved_function || checked_method || self.caller_is_qualified_unit_enum(expression) + { + return CheckedCallerOrigin::OwnedExpression; } - _ => !self.stmt_uses_type_param_reflection(stmt, type_params), } - } - - fn comptime_type_bind_reflection_is_specializable( - &self, - bind: &ast::ComptimeTypeBindStmt, - type_params: &HashSet, - context: ReflectionBranchContext, - ) -> bool { - let source_mentions_type_param = comptime_type_info_binding(&bind.value) - .or_else(|| comptime_type_arg_binding(&bind.value).map(|(ty, _)| ty)) - .is_some_and(|ty| Self::type_expr_mentions_type_param(ty, type_params)); - - if source_mentions_type_param { - return context.permits_shape_reflection() - && self.block_reflection_is_branch_specializable_in_context( - &bind.body, - type_params, - context, - ); + let root = Self::assignment_root(expression).and_then(|ident| self.ident_def_id(ident)); + if root.is_some_and(|definition| { + self.caller_definition_is_producer(definition) + || !matches!( + self.resolve.scope_table.def(definition).kind, + DefKind::Variable | DefKind::Param + ) + }) { + // Named descriptors, baked constants and static type/namespace + // projections produce values; none supplies a caller-local root. + return CheckedCallerOrigin::OwnedExpression; } - - !self.expr_uses_type_param_reflection(&bind.value, type_params) - && self.block_reflection_is_branch_specializable_in_context( - &bind.body, - type_params, - context, - ) - } - - fn for_reflection_is_branch_specializable( - &self, - for_stmt: &ast::ForStmt, - type_params: &HashSet, - context: ReflectionBranchContext, - ) -> bool { - if let Some(owner_ty) = direct_reflected_loop_owner_type(&for_stmt.iterable) { - if Self::type_expr_mentions_type_param(owner_ty, type_params) { - return context.permits_shape_reflection() - && self.block_reflection_is_branch_specializable_in_context( - &for_stmt.body, - type_params, - ReflectionBranchContext::StaticReflectionBranch, - ); + let binding = root.and_then(|definition| self.caller_binding_fact(definition)); + if let Some(fact) = binding { + // Captured roots may be declared outside this body. Retain only the + // exact fact referenced by this occurrence in its concrete frame. + self.record_binding_fact(fact); + } + let source = root + .map(CheckedViewSource::Binding) + .unwrap_or(CheckedViewSource::Other); + match expression { + Expr::Ident(_) => binding + .map(CheckedCallerOrigin::Binding) + .unwrap_or(CheckedCallerOrigin::OwnedExpression), + Expr::FieldAccess(..) if written_view => { + CheckedCallerOrigin::BorrowedProjection { source } } + Expr::FieldAccess(..) => CheckedCallerOrigin::OwnedFieldCopy { parent: source }, + _ => CheckedCallerOrigin::OwnedExpression, } - - !self.expr_uses_type_param_reflection(&for_stmt.iterable, type_params) - && self.block_reflection_is_branch_specializable_in_context( - &for_stmt.body, - type_params, - context, - ) } - fn match_reflection_is_branch_specializable( + fn invocation_facts( &self, - match_stmt: &ast::MatchStmt, - type_params: &HashSet, - _context: ReflectionBranchContext, - ) -> bool { - let scrutinee_static = self.expr_is_potential_static_reflection_value(&match_stmt.expr); - if self.expr_uses_type_param_reflection(&match_stmt.expr, type_params) && !scrutinee_static - { - return false; - } - - let arm_context = if scrutinee_static { - ReflectionBranchContext::StaticReflectionBranch + span: Span, + ) -> ( + Option, + Option, + Option, + Option, + Option, + Option<(Vec, TypeId)>, + ) { + if let Some(active) = self.active_generic_instantiations.last() { + ( + active.generic_calls.get(&span).cloned(), + active.method_calls.get(&span).cloned(), + active.interface_calls.get(&span).cloned(), + active.intrinsic_ids.get(&span).copied(), + active.call_argument_orders.get(&span).cloned(), + active.invocation_signatures.get(&span).cloned(), + ) } else { - ReflectionBranchContext::RuntimeBranch - }; - - match_stmt.arms.iter().all(|arm| { - self.block_reflection_is_branch_specializable_in_context( - &arm.body, - type_params, - arm_context, + ( + self.generic_calls.get(&span).cloned(), + self.method_calls.get(&span).cloned(), + self.interface_calls.get(&span).cloned(), + self.intrinsic_ids.get(&span).copied(), + self.call_argument_orders.get(&span).cloned(), + self.invocation_signatures.get(&span).cloned(), ) - }) - } - - fn expr_is_reflection_local_fact_source( - &self, - expr: &Expr, - type_params: &HashSet, - ) -> bool { - self.expr_is_type_info_reflection(expr, type_params) - || self.expr_is_type_kind_reflection(expr, type_params) - || self.expr_is_type_primitive_reflection_value(expr, type_params) - } - - fn expr_is_direct_reflection_statement_source( - &self, - expr: &Expr, - type_params: &HashSet, - ) -> bool { - match expr { - Expr::Paren(inner, _) => { - self.expr_is_direct_reflection_statement_source(inner, type_params) - } - Expr::Handle(target, _, _, _) => { - self.expr_is_direct_reflection_statement_source(target, type_params) - } - Expr::GenericCall(callee, type_args, _, _) => { - self.resolved_expr_name(callee).is_some_and(|name| { - matches!( - name.as_str(), - "type.variant_value" - | "type.construct_start" - | "type.construct_variant_start" - | "type.construct_machine_start" - | "type.construct_finish" - ) - }) && type_args - .iter() - .any(|arg| Self::type_expr_mentions_type_param(arg, type_params)) - } - _ => false, - } - } - - fn expr_is_potential_static_reflection_condition(&self, expr: &Expr) -> bool { - match expr { - Expr::BoolLiteral(_, _) => true, - Expr::Paren(inner, _) => self.expr_is_potential_static_reflection_condition(inner), - Expr::Unary(UnaryOp::Not, inner, _) => { - self.expr_is_potential_static_reflection_condition(inner) - } - Expr::Binary(lhs, BinOp::And | BinOp::Or, rhs, _) => { - self.expr_is_potential_static_reflection_condition(lhs) - && self.expr_is_potential_static_reflection_condition(rhs) - } - Expr::Binary(lhs, BinOp::Eq | BinOp::NotEq, rhs, _) => { - (self.expr_is_potential_static_reflection_value(lhs) - && self.expr_is_static_reflection_literal(rhs)) - || (self.expr_is_static_reflection_literal(lhs) - && self.expr_is_potential_static_reflection_value(rhs)) - } - _ => false, } } - fn expr_is_potential_static_reflection_value(&self, expr: &Expr) -> bool { - match expr { - Expr::Paren(inner, _) => self.expr_is_potential_static_reflection_value(inner), - Expr::Ident(_) => true, - Expr::GenericCall(callee, _, args, _) => { - args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.kind_tag") - } - Expr::FieldAccess(_, field, _) if field.name == "kind_tag" => true, - Expr::Handle(target, _, body, _) => { - self.expr_is_potential_optional_type_primitive_reflection(target) - && Self::handle_default_expr(body) - .is_some_and(|default| self.expr_is_type_primitive_literal(default)) - } - _ => self.expr_is_static_reflection_value(expr, &HashSet::new()), + fn caller_observation_data(&self, ty: TypeId, visited: &mut HashSet) -> bool { + if !visited.insert(ty) { + return true; } - } - - fn expr_is_potential_optional_type_primitive_reflection(&self, expr: &Expr) -> bool { - match expr { - Expr::Paren(inner, _) => { - self.expr_is_potential_optional_type_primitive_reflection(inner) - } - Expr::FieldAccess(_, field, _) if field.name == "primitive_tag" => true, - Expr::GenericCall(callee, _, args, _) => { - args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.primitive_tag") - } - _ => false, + match self.interner.resolve(ty) { + Type::Capability(_) + | Type::Resource(_) + | Type::Actor(_) + | Type::Interface(_) + | Type::TypeConstruction => false, + Type::Secret(inner) + | Type::Refinement { base: inner, .. } + | Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) => self.caller_observation_data(*inner, visited), + Type::Map(left, right) | Type::Result(left, right) => { + self.caller_observation_data(*left, visited) + && self.caller_observation_data(*right, visited) + } + Type::Struct(id) => self + .interner + .resolve_struct(*id) + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)), + Type::Enum(id) => self + .interner + .resolve_enum(*id) + .variants + .iter() + .all(|variant| { + variant + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::Machine(id) => self + .interner + .resolve_machine(*id) + .states + .iter() + .all(|state| { + state + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::MachineState { machine, state } => self + .interner + .resolve_machine(*machine) + .state(*state) + .is_some_and(|state| { + state + .fields + .iter() + .all(|(_, ty)| self.caller_observation_data(*ty, visited)) + }), + Type::Bitfield(_) + | Type::Function { .. } + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Float32 + | Type::Float64 + | Type::String + | Type::Bool + | Type::Bytes + | Type::Nothing + | Type::Never + | Type::Error => true, } } - fn expr_is_static_reflection_value(&self, expr: &Expr, type_params: &HashSet) -> bool { - self.expr_is_type_kind_reflection(expr, type_params) - || self.expr_is_type_primitive_reflection_value(expr, type_params) - } - - fn expr_is_static_reflection_literal(&self, expr: &Expr) -> bool { - self.expr_is_type_kind_literal(expr) || self.expr_is_type_primitive_literal(expr) - } - - fn expr_is_type_kind_reflection(&self, expr: &Expr, type_params: &HashSet) -> bool { - match expr { - Expr::GenericCall(callee, type_args, args, _) => { - args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.kind_tag") - && type_args.len() == 1 - && Self::type_expr_mentions_type_param(&type_args[0], type_params) - } - Expr::FieldAccess(base, field, _) if field.name == "kind_tag" => { - self.expr_is_type_info_reflection(base, type_params) + fn record_invocation_ownership( + &mut self, + callee: &Expr, + span: Span, + result: TypeId, + occurrences: &[(Option<&Expr>, Span, bool, Option)], + ) { + if result == TypeInterner::ERROR { + return; + } + let callee = Self::unparenthesized(callee); + let (mut generic, method, interface, intrinsic, order, intrinsic_signature) = + self.invocation_facts(span); + // The closed public JSON operation owns the source disposition. Its + // checked generic helper is an independent implementation handoff. + if intrinsic.is_some_and(|id| { + matches!( + id, + IntrinsicId::JsonSerialize + | IntrinsicId::JsonSerializePublic + | IntrinsicId::JsonParse + | IntrinsicId::JsonParseExact + ) + }) { + generic = None; + } + let (target, mut params, modes, intrinsic_shape, signature_result) = if let Some(call) = + generic + { + let Some(body) = self.generic_function_instantiations.iter().find(|body| { + body.definition == call.definition + && body.concrete_args == call.concrete_args + && body.specialization == call.specialization + }) else { + return; + }; + let Some(template) = self.generic_function_templates.values().find(|template| { + self.declaration_def_id(template.name.span) == Some(call.definition) + }) else { + return; + }; + ( + CheckedInvocationTarget::Generic(call), + body.parameter_types.clone(), + template + .params + .iter() + .map(|param| param.view || Self::type_is_view(¶m.ty)) + .collect::>(), + None, + body.return_type, + ) + } else if let Some(call) = method { + let Some(definition) = self + .method_definitions + .iter() + .find(|definition| definition.source_span == call.source_span) + else { + return; + }; + let Some(modes) = self.method_parameter_modes.get(&call.source_span).cloned() else { + return; + }; + ( + CheckedInvocationTarget::Method(call), + definition.parameter_types.clone(), + modes, + None, + definition.return_type, + ) + } else if let Some(call) = interface { + let Type::Interface(id) = self.interner.resolve(call.interface_type) else { + return; + }; + let Some(signature) = self + .interner + .resolve_interface(*id) + .methods + .get(call.method_index) + else { + return; + }; + ( + CheckedInvocationTarget::Interface(call), + signature.params.iter().map(|(_, ty, _)| *ty).collect(), + signature.params.iter().map(|(_, _, view)| *view).collect(), + None, + signature.return_type, + ) + } else if let Some(intrinsic) = intrinsic { + let params = if let Some((params, _)) = intrinsic_signature { + params + } else if matches!( + intrinsic, + IntrinsicId::Print + | IntrinsicId::Println + | IntrinsicId::Range + | IntrinsicId::MathAbs + | IntrinsicId::MathMin + | IntrinsicId::MathMax + ) { + let Some(types) = occurrences + .iter() + .map(|(_, _, _, ty)| *ty) + .collect::>>() + else { + return; + }; + types + } else { + return; + }; + let Some(modes) = (0..params.len()) + .map(|index| { + intrinsic_operand_access(intrinsic, index, params.len()) + .map(|access| access == CheckedCalleeAccess::View) + }) + .collect::>>() + else { + return; + }; + ( + CheckedInvocationTarget::Intrinsic(intrinsic), + params, + modes, + Some(intrinsic), + result, + ) + } else { + let definition = self.resolve.resolutions.get(&callee.span()).copied(); + let signature_type = definition + .and_then(|definition| self.type_env.get(&definition).copied()) + .or_else(|| self.checked_expression_type(callee.span())); + let Some(signature_type) = signature_type else { + return; + }; + let Type::Function { + params, + view_params, + return_type, + } = self.interner.resolve(signature_type).clone() + else { + return; + }; + let target = if let Some(definition) = definition.filter(|definition| { + self.resolve.scope_table.def(*definition).kind == DefKind::Function + }) { + CheckedInvocationTarget::Resolved(definition) + } else { + CheckedInvocationTarget::Indirect(signature_type) + }; + (target, params, view_params, None, return_type) + }; + if params.len() != occurrences.len() || modes.len() != params.len() { + return; + } + // Secret operations have a closed polymorphic payload role, not a + // function signature containing the builtin's internal ERROR wildcard. + if intrinsic_shape.is_some_and(|intrinsic| { + matches!( + intrinsic, + IntrinsicId::SecretCompare | IntrinsicId::SecretRedact + ) + }) { + let Some(concrete) = occurrences + .iter() + .map(|(_, _, _, ty)| *ty) + .collect::>>() + else { + return; + }; + params = concrete; + } + let order = order + .map(|order| order.source_indices) + .unwrap_or_else(|| (0..params.len()).collect()); + if order.len() != params.len() { + return; + } + let mut inverse = vec![None; params.len()]; + for (parameter_index, &source_index) in order.iter().enumerate() { + let Some(slot) = inverse.get_mut(source_index) else { + return; + }; + if slot.replace(parameter_index).is_some() { + return; } - Expr::Paren(inner, _) => self.expr_is_type_kind_reflection(inner, type_params), - _ => false, } - } - - fn expr_is_type_primitive_reflection_value( - &self, - expr: &Expr, - type_params: &HashSet, - ) -> bool { - match expr { - Expr::Handle(target, _, body, _) => { - self.expr_is_optional_type_primitive_reflection(target, type_params) - && Self::handle_default_expr(body) - .is_some_and(|default| self.expr_is_type_primitive_literal(default)) + let mut arguments = Vec::with_capacity(params.len()); + let mut roles = Vec::with_capacity(params.len()); + for (source_index, &(expression, source_span, written_view, actual_type)) in + occurrences.iter().enumerate() + { + let (Some(parameter_index), Some(actual_type)) = (inverse[source_index], actual_type) + else { + return; + }; + if actual_type == TypeInterner::ERROR || params[parameter_index] == TypeInterner::ERROR + { + return; } - Expr::Paren(inner, _) => { - self.expr_is_type_primitive_reflection_value(inner, type_params) + let origin = self.caller_origin(expression, written_view); + let access = if modes[parameter_index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + let access = if intrinsic_shape.is_some_and(|intrinsic| { + matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) + }) && written_view + { + CheckedCalleeAccess::View + } else { + access + }; + let copyable = crate::ownership::is_implicitly_copyable(&self.interner, actual_type); + let observe = self.ownership_context != CheckedOwnershipContext::Ordinary + && self.caller_observation_data(actual_type, &mut HashSet::new()); + // Ordinary function checking has already validated source syntax + // (including the pipeline step's original E0375 label). Closed + // intrinsic operands have their own sealed role gate here. + if intrinsic_shape.is_some() { + self.check_argument_view( + access == CheckedCalleeAccess::View, + written_view, + source_span, + ); } - _ => false, - } - } - - fn expr_is_optional_type_primitive_reflection( - &self, - expr: &Expr, - type_params: &HashSet, - ) -> bool { - match expr { - Expr::GenericCall(callee, type_args, args, _) => { - args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.primitive_tag") - && type_args.len() == 1 - && Self::type_expr_mentions_type_param(&type_args[0], type_params) + if !observe + && !written_view + && let Some(expression) = expression + { + self.check_owned_argument( + access == CheckedCalleeAccess::View, + expression, + params[parameter_index], + actual_type, + ); } - Expr::FieldAccess(base, field, _) if field.name == "primitive_tag" => { - self.expr_is_type_info_reflection(base, type_params) + let effect = if copyable { + CheckedCallerEffect::Copy + } else if observe { + CheckedCallerEffect::ObserveData + } else if written_view { + CheckedCallerEffect::RetainBorrow + } else if access == CheckedCalleeAccess::View { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::TransferOwned + }; + arguments.push(CheckedArgumentOwnership { + source_span, + source_index, + parameter_index, + actual_type, + parameter_type: params[parameter_index], + syntax: if written_view { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + }, + origin, + callee_access: access, + effect, + }); + } + let shape = if let Some(intrinsic) = intrinsic_shape { + for (parameter_index, &ty) in params.iter().enumerate() { + roles.push( + if matches!(intrinsic, IntrinsicId::Print | IntrinsicId::Println) { + CheckedIntrinsicOperandRole::PrintArgument { ty } + } else if crate::ownership::is_implicitly_copyable(&self.interner, ty) { + CheckedIntrinsicOperandRole::Copy { ty } + } else if modes[parameter_index] { + CheckedIntrinsicOperandRole::View { ty } + } else { + CheckedIntrinsicOperandRole::Owned { ty } + }, + ); } - Expr::Paren(inner, _) => { - self.expr_is_optional_type_primitive_reflection(inner, type_params) + CheckedInvocationShape::Intrinsic { + intrinsic, + result_type: result, + operands: roles, } - _ => false, + } else { + let signature_type = self.interner.intern(Type::Function { + params, + view_params: modes, + return_type: signature_result, + }); + CheckedInvocationShape::Function { signature_type } + }; + let packet = CheckedCallOwnership { + target, + shape, + context: self.ownership_context, + arguments, + }; + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.call_ownership.insert(span, packet); + } else { + self.call_ownership.insert(span, packet); } } - fn expr_is_type_info_reflection(&self, expr: &Expr, type_params: &HashSet) -> bool { - match expr { - Expr::GenericCall(callee, type_args, args, _) => { - args.is_empty() - && self - .resolved_expr_name(callee) - .is_some_and(|name| name == "type.info") - && type_args.len() == 1 - && Self::type_expr_mentions_type_param(&type_args[0], type_params) - } - Expr::Paren(inner, _) => self.expr_is_type_info_reflection(inner, type_params), - _ => false, + fn record_binding_mode(&mut self, span: Span, mode: CheckedBindingMode) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.binding_modes.insert(span, mode); + } else { + self.binding_modes.insert(span, mode); } } - fn expr_is_type_kind_literal(&self, expr: &Expr) -> bool { - self.type_kind_literal_name(expr).is_some() + fn clear_interface_method_value(&mut self, span: Span) { + let values = if let Some(active) = self.active_generic_instantiations.last_mut() { + &mut active.method_values + } else { + &mut self.method_values + }; + if matches!( + values.get(&span), + Some(CheckedMethodValue::Interface { .. }) + ) { + values.remove(&span); + } } - fn expr_is_type_primitive_literal(&self, expr: &Expr) -> bool { - self.type_primitive_literal_name(expr).is_some() + fn record_struct_construction( + &mut self, + span: Span, + struct_type: TypeId, + validates_refinements: bool, + ) { + let construction = CheckedStructConstruction { + struct_type, + validates_refinements, + }; + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.struct_constructions.insert(span, construction); + } else { + self.struct_constructions.insert(span, construction); + } } - fn type_kind_literal_name(&self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.type_kind_literal_name(inner), - Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypeKind" => { - Some(variant.name.clone()) - } - Expr::FieldAccess(_, _, _) => { - let name = self.expanded_dotted_expr_name(expr)?; - let (type_name, variant_name) = name.rsplit_once('.')?; - (type_name == "TypeKind").then(|| variant_name.to_string()) - } - _ => None, + fn record_pipeline_step_input_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.pipeline_step_input_types.insert(span, ty); + } else { + self.pipeline_step_input_types.insert(span, ty); } } - fn type_primitive_literal_name(&self, expr: &Expr) -> Option { - match expr { - Expr::Paren(inner, _) => self.type_primitive_literal_name(inner), - Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypePrimitive" => { - Some(variant.name.clone()) - } - Expr::FieldAccess(_, _, _) => { - let name = self.expanded_dotted_expr_name(expr)?; - let (type_name, variant_name) = name.rsplit_once('.')?; - (type_name == "TypePrimitive").then(|| variant_name.to_string()) - } - _ => None, + fn record_pipeline_step_call_type(&mut self, span: Span, ty: TypeId) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.pipeline_step_call_types.insert(span, ty); + } else { + self.pipeline_step_call_types.insert(span, ty); } } - fn handle_default_expr(body: &Block) -> Option<&Expr> { - let Stmt::Expr(expr_stmt) = body.stmts.last()? else { - return None; - }; - let Expr::Default(default_value, _) = &expr_stmt.expr else { - return None; - }; - Some(default_value) + fn span_is_within(span: Span, owner: Span) -> bool { + span.file == owner.file && span.start >= owner.start && span.end <= owner.end } - fn eval_static_type_condition(&mut self, expr: &Expr) -> Option { - match expr { - Expr::BoolLiteral(value, _) => Some(*value), - Expr::Paren(inner, _) => self.eval_static_type_condition(inner), - Expr::Unary(UnaryOp::Not, inner, _) => { - self.eval_static_type_condition(inner).map(|value| !value) - } + fn facts_in_span(source: &HashMap, owner: Span) -> HashMap { + source + .iter() + .filter(|(span, _)| Self::span_is_within(**span, owner)) + .map(|(span, value)| (*span, value.clone())) + .collect() + } + + fn clear_facts_in_span(source: &mut HashMap, owner: Span) { + source.retain(|span, _| !Self::span_is_within(*span, owner)); + } + + /// Retain only dependencies of actual local view declarations. The supplied + /// map belongs to this concrete body; missing captures remain unsupported. + fn retain_local_view_dependencies( + &self, + bindings: &HashMap, + facts: &mut HashMap, + ) { + let mut pending = facts.values().copied().collect::>(); + let mut visited = HashSet::new(); + while let Some(fact) = pending.pop() { + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(definition), + } = fact.mode + else { + continue; + }; + if !visited.insert(definition) { + continue; + } + let Some(declaration) = self + .resolve + .scope_table + .definitions + .get(definition.index() as usize) + .filter(|declaration| declaration.id == definition) + else { + continue; + }; + let Some(source) = bindings.get(&declaration.span).filter(|source| { + source.definition == definition && source.declaration_span == declaration.span + }) else { + continue; + }; + if facts + .get(&declaration.span) + .is_some_and(|retained| retained != source) + { + continue; + } + facts.insert(declaration.span, *source); + pending.push(*source); + } + } + + fn body_binding_facts( + &self, + bindings: &HashMap, + calls: &HashMap, + owner: Span, + ) -> HashMap { + let mut facts = Self::facts_in_span(bindings, owner); + let assignments = self + .active_generic_instantiations + .last() + .map(|active| &active.assignment_places) + .unwrap_or(&self.assignment_places); + for place in assignments + .values() + .filter(|place| Self::span_is_within(place.assignment_span, owner)) + { + if bindings.get(&place.root.declaration_span) == Some(&place.root) { + facts.insert(place.root.declaration_span, place.root); + } + } + let actor_calls = self + .active_generic_instantiations + .last() + .map(|active| &active.actor_message_calls) + .unwrap_or(&self.actor_message_calls); + for (_, call) in actor_calls + .iter() + .filter(|(span, _)| Self::span_is_within(**span, owner)) + { + // Receiver expressions can be projected or computed. Retain only + // their actual resolver references from this selected fact scope. + for (_, definition) in self + .resolve + .resolutions + .iter() + .filter(|(span, _)| Self::span_is_within(**span, call.receiver_span)) + { + let declaration = self.resolve.scope_table.def(*definition).span; + if let Some(fact) = bindings + .get(&declaration) + .filter(|fact| fact.definition == *definition) + { + facts.insert(declaration, *fact); + } + } + } + let call_arguments = calls + .iter() + .map(|(span, call)| (span, &call.arguments)) + .chain( + actor_calls + .iter() + .map(|(span, call)| (span, &call.arguments)), + ); + for (_, arguments) in call_arguments.filter(|(span, _)| Self::span_is_within(**span, owner)) + { + for argument in arguments { + match &argument.origin { + CheckedCallerOrigin::Binding(fact) => { + facts.insert(fact.declaration_span, *fact); + } + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(definition), + } + | CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(definition), + } => { + let span = self.resolve.scope_table.def(*definition).span; + if let Some(fact) = bindings + .get(&span) + .filter(|fact| fact.definition == *definition) + { + facts.insert(span, *fact); + } + } + _ => {} + } + } + } + self.retain_local_view_dependencies(bindings, &mut facts); + facts + } + + fn clear_checked_body_facts(&mut self, owner: Span) { + // Expression types are also retained in the root map for the existing + // interpreter handoff, even while a generic fact scope is active. + Self::clear_facts_in_span(&mut self.type_map, owner); + Self::clear_facts_in_span(&mut self.debug_type_names, owner); + if let Some(active) = self.active_generic_instantiations.last_mut() { + Self::clear_facts_in_span(&mut active.type_map, owner); + Self::clear_facts_in_span(&mut active.source_type_map, owner); + Self::clear_facts_in_span(&mut active.debug_type_names, owner); + Self::clear_facts_in_span(&mut active.binding_modes, owner); + Self::clear_facts_in_span(&mut active.binding_facts, owner); + Self::clear_facts_in_span(&mut active.assignment_places, owner); + Self::clear_facts_in_span(&mut active.call_ownership, owner); + Self::clear_facts_in_span(&mut active.actor_message_calls, owner); + Self::clear_facts_in_span(&mut active.invocation_signatures, owner); + Self::clear_facts_in_span(&mut active.generic_calls, owner); + Self::clear_facts_in_span(&mut active.intrinsic_ids, owner); + Self::clear_facts_in_span(&mut active.intrinsic_type_arguments, owner); + Self::clear_facts_in_span(&mut active.intrinsic_reflection_arguments, owner); + Self::clear_facts_in_span(&mut active.call_argument_orders, owner); + Self::clear_facts_in_span(&mut active.method_calls, owner); + Self::clear_facts_in_span(&mut active.interface_calls, owner); + Self::clear_facts_in_span(&mut active.method_values, owner); + Self::clear_facts_in_span(&mut active.struct_constructions, owner); + Self::clear_facts_in_span(&mut active.pipeline_step_input_types, owner); + Self::clear_facts_in_span(&mut active.pipeline_step_call_types, owner); + Self::clear_facts_in_span(&mut active.static_selections, owner); + Self::clear_facts_in_span(&mut active.comptime_type_bindings, owner); + } else { + Self::clear_facts_in_span(&mut self.source_type_map, owner); + Self::clear_facts_in_span(&mut self.binding_modes, owner); + Self::clear_facts_in_span(&mut self.binding_facts, owner); + Self::clear_facts_in_span(&mut self.assignment_places, owner); + Self::clear_facts_in_span(&mut self.call_ownership, owner); + Self::clear_facts_in_span(&mut self.actor_message_calls, owner); + Self::clear_facts_in_span(&mut self.invocation_signatures, owner); + Self::clear_facts_in_span(&mut self.generic_calls, owner); + Self::clear_facts_in_span(&mut self.intrinsic_ids, owner); + Self::clear_facts_in_span(&mut self.intrinsic_type_arguments, owner); + Self::clear_facts_in_span(&mut self.intrinsic_reflection_arguments, owner); + Self::clear_facts_in_span(&mut self.call_argument_orders, owner); + Self::clear_facts_in_span(&mut self.method_calls, owner); + Self::clear_facts_in_span(&mut self.interface_calls, owner); + Self::clear_facts_in_span(&mut self.method_values, owner); + Self::clear_facts_in_span(&mut self.struct_constructions, owner); + Self::clear_facts_in_span(&mut self.pipeline_step_input_types, owner); + Self::clear_facts_in_span(&mut self.pipeline_step_call_types, owner); + Self::clear_facts_in_span(&mut self.comptime_type_bindings, owner); + } + } + + fn checked_body_facts(&self, owner: Span) -> CheckedBodyFacts { + if let Some(active) = self.active_generic_instantiations.last() { + return CheckedBodyFacts { + type_map: Self::facts_in_span(&active.type_map, owner), + source_type_map: Self::facts_in_span(&active.source_type_map, owner), + debug_type_names: Self::facts_in_span(&active.debug_type_names, owner), + binding_modes: Self::facts_in_span(&active.binding_modes, owner), + binding_facts: self.body_binding_facts( + &active.binding_facts, + &active.call_ownership, + owner, + ), + call_ownership: Self::facts_in_span(&active.call_ownership, owner), + actor_message_calls: Self::facts_in_span(&active.actor_message_calls, owner), + assignment_places: Self::facts_in_span(&active.assignment_places, owner), + generic_calls: Self::facts_in_span(&active.generic_calls, owner), + intrinsic_ids: Self::facts_in_span(&active.intrinsic_ids, owner), + intrinsic_type_arguments: Self::facts_in_span( + &active.intrinsic_type_arguments, + owner, + ), + intrinsic_reflection_arguments: Self::facts_in_span( + &active.intrinsic_reflection_arguments, + owner, + ), + call_argument_orders: Self::facts_in_span(&active.call_argument_orders, owner), + method_calls: Self::facts_in_span(&active.method_calls, owner), + interface_calls: Self::facts_in_span(&active.interface_calls, owner), + method_values: Self::facts_in_span(&active.method_values, owner), + struct_constructions: Self::facts_in_span(&active.struct_constructions, owner), + pipeline_step_input_types: Self::facts_in_span( + &active.pipeline_step_input_types, + owner, + ), + pipeline_step_call_types: Self::facts_in_span( + &active.pipeline_step_call_types, + owner, + ), + static_selections: Self::facts_in_span(&active.static_selections, owner), + comptime_type_bindings: Self::facts_in_span(&active.comptime_type_bindings, owner), + }; + } + CheckedBodyFacts { + type_map: Self::facts_in_span(&self.type_map, owner), + source_type_map: Self::facts_in_span(&self.source_type_map, owner), + debug_type_names: Self::facts_in_span(&self.debug_type_names, owner), + binding_modes: Self::facts_in_span(&self.binding_modes, owner), + binding_facts: self.body_binding_facts( + &self.binding_facts, + &self.call_ownership, + owner, + ), + call_ownership: Self::facts_in_span(&self.call_ownership, owner), + actor_message_calls: Self::facts_in_span(&self.actor_message_calls, owner), + assignment_places: Self::facts_in_span(&self.assignment_places, owner), + generic_calls: Self::facts_in_span(&self.generic_calls, owner), + intrinsic_ids: Self::facts_in_span(&self.intrinsic_ids, owner), + intrinsic_type_arguments: Self::facts_in_span(&self.intrinsic_type_arguments, owner), + intrinsic_reflection_arguments: Self::facts_in_span( + &self.intrinsic_reflection_arguments, + owner, + ), + call_argument_orders: Self::facts_in_span(&self.call_argument_orders, owner), + method_calls: Self::facts_in_span(&self.method_calls, owner), + interface_calls: Self::facts_in_span(&self.interface_calls, owner), + method_values: Self::facts_in_span(&self.method_values, owner), + struct_constructions: Self::facts_in_span(&self.struct_constructions, owner), + pipeline_step_input_types: Self::facts_in_span(&self.pipeline_step_input_types, owner), + pipeline_step_call_types: Self::facts_in_span(&self.pipeline_step_call_types, owner), + static_selections: HashMap::new(), + comptime_type_bindings: Self::facts_in_span(&self.comptime_type_bindings, owner), + } + } + + fn record_comptime_type_bindings( + &mut self, + span: Span, + bindings: Vec, + ) { + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.comptime_type_bindings.insert(span, bindings); + } else { + self.comptime_type_bindings.insert(span, bindings); + } + } + + fn record_static_selection(&mut self, span: Span, selection: CheckedStaticSelection) { + let conflict = self + .active_generic_instantiations + .last_mut() + .is_some_and(|active| match active.static_selections.get(&span) { + Some(existing) => *existing != selection, + None => { + active.static_selections.insert(span, selection); + false + } + }); + if conflict { + self.sink + .emit(errors::conflicting_generic_static_selection(span)); + } + } + + fn record_generic_call_for_template( + &mut self, + span: Span, + func: &FunctionDef, + concrete_args: &[TypeId], + kind_subst: &HashMap, + type_argument_reflections: &[ReflectionTypeInfo], + param_facts: &ReflectionParamFacts, + ) { + let Some(definition) = self.declaration_def_id(func.name.span) else { + return; + }; + self.record_generic_call_target( + span, + CheckedGenericCall { + definition, + concrete_args: concrete_args.to_vec(), + specialization: Self::generic_specialization( + func, + kind_subst, + type_argument_reflections, + param_facts, + ), + }, + ); + } + + fn generic_specialization( + func: &FunctionDef, + kind_subst: &HashMap, + type_argument_reflections: &[ReflectionTypeInfo], + param_facts: &ReflectionParamFacts, + ) -> CheckedGenericSpecialization { + CheckedGenericSpecialization { + type_argument_kinds: func + .type_params + .iter() + .map(|param| { + kind_subst + .get(¶m.name) + .cloned() + .unwrap_or_else(|| "unknown_type".to_string()) + }) + .collect(), + type_argument_reflections: type_argument_reflections.to_vec(), + type_info_kinds: param_facts.type_info_kinds.clone(), + type_info_primitives: param_facts.type_info_primitives.clone(), + type_kind_values: param_facts.type_kind_values.clone(), + type_primitive_values: param_facts.type_primitive_values.clone(), + } + } + + fn generic_function_uses_type_param_reflection(&self, func: &FunctionDef) -> bool { + let type_params = func + .type_params + .iter() + .map(|param| param.name.clone()) + .collect::>(); + self.block_uses_type_param_reflection(&func.body, &type_params) + } + + fn type_kind_literal_name(&self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.type_kind_literal_name(inner), + Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypeKind" => { + Some(variant.name.clone()) + } + Expr::FieldAccess(_, _, _) => { + let name = self.expanded_dotted_expr_name(expr)?; + let (type_name, variant_name) = name.rsplit_once('.')?; + (type_name == "TypeKind").then(|| variant_name.to_string()) + } + _ => None, + } + } + + fn type_primitive_literal_name(&self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.type_primitive_literal_name(inner), + Expr::EnumVariant(type_name, variant, _) if type_name.name == "TypePrimitive" => { + Some(variant.name.clone()) + } + Expr::FieldAccess(_, _, _) => { + let name = self.expanded_dotted_expr_name(expr)?; + let (type_name, variant_name) = name.rsplit_once('.')?; + (type_name == "TypePrimitive").then(|| variant_name.to_string()) + } + _ => None, + } + } + + fn handle_default_expr(body: &Block) -> Option<&Expr> { + let Stmt::Expr(expr_stmt) = body.stmts.last()? else { + return None; + }; + let Expr::Default(default_value, _) = &expr_stmt.expr else { + return None; + }; + Some(default_value) + } + + fn eval_static_type_condition(&mut self, expr: &Expr) -> Option { + match expr { + Expr::BoolLiteral(value, _) => Some(*value), + Expr::Paren(inner, _) => self.eval_static_type_condition(inner), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.has_secret") => + { + let type_id = self.reflection_type_arg_id(type_args)?; + Some(self.type_contains_secret_data(type_id)) + } + Expr::Unary(UnaryOp::Not, inner, _) => { + self.eval_static_type_condition(inner).map(|value| !value) + } Expr::Binary(lhs, BinOp::And, rhs, _) => match self.eval_static_type_condition(lhs)? { false => Some(false), true => self.eval_static_type_condition(rhs), @@ -6324,9 +8846,21 @@ impl<'a> TypeChecker<'a> { false => self.eval_static_type_condition(rhs), }, Expr::Binary(lhs, BinOp::Eq | BinOp::NotEq, rhs, _) => { - let lhs_value = self.eval_static_reflection_enum_value(lhs)?; - let rhs_value = self.eval_static_reflection_enum_value(rhs)?; - let equal = lhs_value == rhs_value; + let lhs_name = self.eval_static_type_name_value(lhs); + let rhs_name = self.eval_static_type_name_value(rhs); + let equal = if let (Some(name), Some(literal)) = + (lhs_name, Self::static_string_literal(rhs)) + { + name == literal + } else if let (Some(literal), Some(name)) = + (Self::static_string_literal(lhs), rhs_name) + { + literal == name + } else { + let lhs_value = self.eval_static_reflection_enum_value(lhs)?; + let rhs_value = self.eval_static_reflection_enum_value(rhs)?; + lhs_value == rhs_value + }; Some(if matches!(expr, Expr::Binary(_, BinOp::Eq, _, _)) { equal } else { @@ -6351,6 +8885,44 @@ impl<'a> TypeChecker<'a> { .map(StaticReflectionEnumValue::TypePrimitive) } + fn eval_static_type_name_value(&mut self, expr: &Expr) -> Option { + match expr { + Expr::Paren(inner, _) => self.eval_static_type_name_value(inner), + Expr::GenericCall(callee, type_args, args, _) + if args.is_empty() + && self + .resolved_expr_name(callee) + .is_some_and(|name| name == "type.name") => + { + let [type_arg] = type_args.as_slice() else { + return None; + }; + let type_id = self.reflection_type_arg_id(type_args)?; + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + Some( + self.reflection_type_info_for_type_expr( + type_arg, + namespace.as_deref(), + type_id, + ) + .type_name, + ) + } + _ => None, + } + } + + fn static_string_literal(expr: &Expr) -> Option<&str> { + match expr { + Expr::Paren(inner, _) => Self::static_string_literal(inner), + Expr::StringLiteral(value, _) => Some(value), + _ => None, + } + } + fn eval_type_kind_value(&mut self, expr: &Expr) -> Option { match expr { Expr::Paren(inner, _) => self.eval_type_kind_value(inner), @@ -6483,105 +9055,6 @@ impl<'a> TypeChecker<'a> { .any(|stmt| self.stmt_uses_type_param_reflection(stmt, type_params)) } - fn block_type_param_reflection_span( - &self, - block: &Block, - type_params: &HashSet, - ) -> Option { - block - .stmts - .iter() - .find_map(|stmt| self.stmt_type_param_reflection_span(stmt, type_params)) - } - - fn if_branch_type_param_reflection_span( - &self, - if_stmt: &ast::IfStmt, - type_params: &HashSet, - ) -> Option { - self.block_type_param_reflection_span(&if_stmt.then_block, type_params) - .or_else(|| { - if_stmt.else_ifs.iter().find_map(|(_, block)| { - self.block_type_param_reflection_span(block, type_params) - }) - }) - .or_else(|| { - if_stmt - .else_block - .as_ref() - .and_then(|block| self.block_type_param_reflection_span(block, type_params)) - }) - } - - fn stmt_type_param_reflection_span( - &self, - stmt: &Stmt, - type_params: &HashSet, - ) -> Option { - match stmt { - Stmt::VarDecl(decl) => self - .expr_uses_type_param_reflection(&decl.value, type_params) - .then(|| decl.value.span()), - Stmt::Assign(assign) => self - .expr_uses_type_param_reflection(&assign.target, type_params) - .then(|| assign.target.span()) - .or_else(|| { - self.expr_uses_type_param_reflection(&assign.value, type_params) - .then(|| assign.value.span()) - }), - Stmt::Return(ret) => ret.value.as_ref().and_then(|expr| { - self.expr_uses_type_param_reflection(expr, type_params) - .then(|| expr.span()) - }), - Stmt::Respond(resp) => self - .expr_uses_type_param_reflection(&resp.value, type_params) - .then(|| resp.value.span()), - Stmt::ComptimeTypeBind(bind) => self - .expr_uses_type_param_reflection(&bind.value, type_params) - .then(|| bind.value.span()) - .or_else(|| self.block_type_param_reflection_span(&bind.body, type_params)), - Stmt::If(if_stmt) => self - .expr_uses_type_param_reflection(&if_stmt.condition, type_params) - .then(|| if_stmt.condition.span()) - .or_else(|| self.if_branch_type_param_reflection_span(if_stmt, type_params)), - Stmt::For(for_stmt) => self - .expr_uses_type_param_reflection(&for_stmt.iterable, type_params) - .then(|| for_stmt.iterable.span()) - .or_else(|| self.block_type_param_reflection_span(&for_stmt.body, type_params)), - Stmt::While(while_stmt) => self - .expr_uses_type_param_reflection(&while_stmt.condition, type_params) - .then(|| while_stmt.condition.span()) - .or_else(|| self.block_type_param_reflection_span(&while_stmt.body, type_params)), - Stmt::Match(match_stmt) => self - .expr_uses_type_param_reflection(&match_stmt.expr, type_params) - .then(|| match_stmt.expr.span()) - .or_else(|| { - match_stmt.arms.iter().find_map(|arm| { - self.block_type_param_reflection_span(&arm.body, type_params) - }) - }), - Stmt::Expr(expr_stmt) => self - .expr_uses_type_param_reflection(&expr_stmt.expr, type_params) - .then(|| expr_stmt.expr.span()), - Stmt::Assert(assert_stmt) => self - .expr_uses_type_param_reflection(&assert_stmt.condition, type_params) - .then(|| assert_stmt.condition.span()) - .or_else(|| { - assert_stmt.message.as_ref().and_then(|message| { - self.expr_uses_type_param_reflection(message, type_params) - .then(|| message.span()) - }) - }), - Stmt::Breakpoint(breakpoint_stmt) => { - breakpoint_stmt.condition.as_ref().and_then(|expr| { - self.expr_uses_type_param_reflection(expr, type_params) - .then(|| expr.span()) - }) - } - Stmt::Trace(_) | Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => None, - } - } - fn stmt_uses_type_param_reflection(&self, stmt: &Stmt, type_params: &HashSet) -> bool { match stmt { Stmt::VarDecl(decl) => self.expr_uses_type_param_reflection(&decl.value, type_params), @@ -6709,11 +9182,10 @@ impl<'a> TypeChecker<'a> { }) }) } - Expr::InlineFn(_, return_ty, body, _) => { - return_ty - .as_ref() - .is_some_and(|ty| Self::type_expr_mentions_type_param(ty, type_params)) - || self.block_uses_type_param_reflection(body, type_params) + // Generic signature annotations require ordinary substitution; + // only operations inside the closure body can use reflection. + Expr::InlineFn(_, _, body, _) => { + self.block_uses_type_param_reflection(body, type_params) } Expr::IntLiteral(_, _) | Expr::FloatLiteral(_, _) @@ -6755,6 +9227,10 @@ impl<'a> TypeChecker<'a> { } fn check_function_impl(&mut self, func: &FunctionDef, function_name: String) { + let saved_ownership_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Ordinary, + ); self.graphics_method_definitions .insert(func.span, func.clone()); let is_main = func.name.name == "main" && !function_name.contains('.'); @@ -6794,10 +9270,16 @@ impl<'a> TypeChecker<'a> { // Bind parameter types into the type environment. for param in &func.params { + self.record_debug_type_name(¶m.name, ¶m.ty); let param_type = self.resolve_type_expr(¶m.ty); if let Some(def_id) = self.declaration_def_id(param.name.span) { self.type_env.insert(def_id, param_type); } + self.record_assignment_binding( + ¶m.name, + param.mutable, + param.view || Self::type_is_view(¶m.ty), + ); } self.check_block(&func.body); @@ -6805,26 +9287,37 @@ impl<'a> TypeChecker<'a> { self.current_return_type = None; self.current_function_name = None; self.current_function_pure = false; + self.ownership_context = saved_ownership_context; } fn check_verify_block(&mut self, verify: &VerifyBlock) { + let saved_context = + std::mem::replace(&mut self.ownership_context, CheckedOwnershipContext::Verify); self.in_verify_block = true; self.current_verify_name = Some(verify.name.name.clone()); self.check_block(&verify.body); self.in_verify_block = false; self.current_verify_name = None; + self.ownership_context = saved_context; } fn check_property_block(&mut self, prop: &ast::PropertyBlock) { + let saved_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Property, + ); self.in_property_block = true; for given in &prop.givens { + self.record_debug_type_name(&given.name, &given.ty); let given_type = self.resolve_type_expr(&given.ty); if let Some(def_id) = self.declaration_def_id(given.name.span) { self.type_env.insert(def_id, given_type); + self.record_assignment_binding(&given.name, false, false); } } self.check_block(&prop.body); self.in_property_block = false; + self.ownership_context = saved_context; } // ------------------------------------------------------------------ @@ -6885,7 +9378,30 @@ impl<'a> TypeChecker<'a> { return; } - self.check_comptime_type_bind_body(&bind.name.name, bound_ty, &bind.body); + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + let reflection = self.reflection_type_info_for_type_expr( + bound_type_expr, + namespace.as_deref(), + bound_ty, + ); + let body = self.check_comptime_type_bind_body( + &bind.name.name, + bound_ty, + reflection.clone(), + &bind.body, + ); + self.record_comptime_type_bindings( + bind.span, + vec![CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::Unconditional, + bound_type: bound_ty, + reflection, + body, + }], + ); return; } @@ -6899,7 +9415,35 @@ impl<'a> TypeChecker<'a> { let arg_types = self.type_info_arg_types_for_type_expr(source_type_expr); if let Some(&bound_ty) = arg_types.get(index) { if bound_ty != TypeInterner::ERROR { - self.check_comptime_type_bind_body(&bind.name.name, bound_ty, &bind.body); + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + let source_info = self.reflection_type_info_for_type_expr( + source_type_expr, + namespace.as_deref(), + source_ty, + ); + let reflection = source_info + .args + .get(index) + .cloned() + .unwrap_or_else(|| self.reflection_type_info_for_type(bound_ty)); + let body = self.check_comptime_type_bind_body( + &bind.name.name, + bound_ty, + reflection.clone(), + &bind.body, + ); + self.record_comptime_type_bindings( + bind.span, + vec![CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::Unconditional, + bound_type: bound_ty, + reflection, + body, + }], + ); } return; } @@ -6911,23 +9455,54 @@ impl<'a> TypeChecker<'a> { } if let Some(field_name) = reflected_field_type_info_binding(&bind.value) { - if let Some(field_types) = self.reflected_field_types_for_name(field_name) { - for field_ty in field_types { + if let Some(field_types) = self.reflected_field_candidates_for_name(field_name) { + let mut bindings = Vec::new(); + for (iteration_index, (field_ty, reflection)) in field_types.into_iter().enumerate() + { if field_ty != TypeInterner::ERROR { - self.check_comptime_type_bind_body(&bind.name.name, field_ty, &bind.body); + let body = self.check_comptime_type_bind_body( + &bind.name.name, + field_ty, + reflection.clone(), + &bind.body, + ); + bindings.push(CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::ReflectedIteration( + iteration_index, + ), + bound_type: field_ty, + reflection, + body, + }); } } + self.record_comptime_type_bindings(bind.span, bindings); return; } } if let Some(info_name) = reflected_type_info_binding(&bind.value) { if let Some(info_types) = self.reflected_type_info_types_for_name(info_name) { - for info_ty in info_types { + let mut bindings = Vec::new(); + for (iteration_index, (info_ty, reflection)) in info_types.into_iter().enumerate() { if info_ty != TypeInterner::ERROR { - self.check_comptime_type_bind_body(&bind.name.name, info_ty, &bind.body); - } - } + let body = self.check_comptime_type_bind_body( + &bind.name.name, + info_ty, + reflection.clone(), + &bind.body, + ); + bindings.push(CheckedComptimeTypeBinding { + selection: CheckedComptimeTypeSelection::ReflectedIteration( + iteration_index, + ), + bound_type: info_ty, + reflection, + body, + }); + } + } + self.record_comptime_type_bindings(bind.span, bindings); return; } } @@ -6937,17 +9512,47 @@ impl<'a> TypeChecker<'a> { self.check_block(&bind.body); } - fn check_comptime_type_bind_body(&mut self, name: &str, bound_ty: TypeId, body: &Block) { + fn check_comptime_type_bind_body( + &mut self, + name: &str, + bound_ty: TypeId, + reflection: ReflectionTypeInfo, + body: &Block, + ) -> CheckedBodyFacts { + // The same source spans may be checked repeatedly for different + // reflected elements. Clear the previous flat facts so this snapshot + // contains only the current concrete expansion; the last expansion + // remains in the legacy flat maps for interpreter compatibility. + self.clear_checked_body_facts(body.span); + let kind_tag = ReflectionTypeInfo::kind_tag_variant(&reflection.kind).to_string(); let previous = self.type_var_subst.insert(name.to_string(), bound_ty); + let previous_reflection = self + .type_var_reflections + .insert(name.to_string(), reflection); + let previous_kind = self.type_var_kind_tags.insert(name.to_string(), kind_tag); self.check_block(body); if let Some(previous) = previous { self.type_var_subst.insert(name.to_string(), previous); } else { self.type_var_subst.remove(name); } + if let Some(previous) = previous_reflection { + self.type_var_reflections.insert(name.to_string(), previous); + } else { + self.type_var_reflections.remove(name); + } + if let Some(previous) = previous_kind { + self.type_var_kind_tags.insert(name.to_string(), previous); + } else { + self.type_var_kind_tags.remove(name); + } + self.checked_body_facts(body.span) } - fn reflected_field_types_for_name(&self, name: &str) -> Option> { + fn reflected_field_candidates_for_name( + &self, + name: &str, + ) -> Option> { self.reflected_field_type_scopes .iter() .rev() @@ -6955,7 +9560,10 @@ impl<'a> TypeChecker<'a> { .cloned() } - fn reflected_type_info_types_for_name(&self, name: &str) -> Option> { + fn reflected_type_info_types_for_name( + &self, + name: &str, + ) -> Option> { self.reflected_type_info_scopes .iter() .rev() @@ -7142,6 +9750,56 @@ impl<'a> TypeChecker<'a> { } } + fn reflected_field_infos_for_owner(&self, owner_ty: TypeId) -> Vec { + match self.interner.resolve(owner_ty) { + Type::Struct(_) | Type::Bitfield(_) => self + .reflection_fields_by_id + .get(&owner_ty) + .map(|(_, fields)| fields.iter().map(|field| field.type_info.clone()).collect()) + .unwrap_or_default(), + Type::Enum(_) => self + .reflection_variants_by_id + .get(&owner_ty) + .map(|(_, variants)| { + variants + .iter() + .flat_map(|variant| { + variant.fields.iter().map(|field| field.type_info.clone()) + }) + .collect() + }) + .unwrap_or_default(), + Type::Machine(_) => self + .reflection_machines_by_id + .get(&owner_ty) + .map(|(_, machine)| { + machine + .states + .iter() + .flat_map(|state| state.fields.iter().map(|field| field.type_info.clone())) + .collect() + }) + .unwrap_or_default(), + Type::MachineState { machine, state } => { + let base_ty = self.interner.type_ids().find( + |ty| matches!(self.interner.resolve(*ty), Type::Machine(id) if id == machine), + ); + base_ty + .and_then(|ty| self.reflection_machines_by_id.get(&ty)) + .and_then(|(_, machine)| machine.states.get(state.index() as usize)) + .map(|state| { + state + .fields + .iter() + .map(|field| field.type_info.clone()) + .collect() + }) + .unwrap_or_default() + } + _ => Vec::new(), + } + } + fn reflected_variant_field_types_for_owner(&self, owner_ty: TypeId) -> Vec { match self.interner.resolve(owner_ty) { Type::Enum(eid) => self @@ -7177,22 +9835,51 @@ impl<'a> TypeChecker<'a> { fn reflected_type_info_arg_types_for_iterable( &mut self, iterable: &Expr, - ) -> Option> { + ) -> Option> { let source = reflected_type_info_args_source(iterable)?; let source_types = match source { - ReflectedTypeInfoSource::Direct(ty) => vec![self.resolve_type_expr(ty)], + ReflectedTypeInfoSource::Direct(ty) => { + let concrete = self.resolve_type_expr(ty); + let namespace = self.current_function_name.as_deref().and_then(|name| { + name.rsplit_once('.') + .map(|(namespace, _)| namespace.to_string()) + }); + vec![( + concrete, + self.reflection_type_info_for_type_expr(ty, namespace.as_deref(), concrete), + )] + } ReflectedTypeInfoSource::Field(field_name) => { - self.reflected_field_types_for_name(field_name)? + self.reflected_field_candidates_for_name(field_name)? } ReflectedTypeInfoSource::TypeInfo(info_name) => { self.reflected_type_info_types_for_name(info_name)? } }; - Some( source_types .into_iter() - .flat_map(|ty| self.type_info_arg_types_for_type(ty)) + .flat_map(|(ty, reflection)| { + // An alias exposes its base as one reflection argument even when + // canonical interning has already erased that alias layer. + let types = if reflection.kind == "alias" { + vec![ty] + } else { + self.type_info_arg_types_for_type(ty) + }; + types + .into_iter() + .enumerate() + .map(|(index, ty)| { + let info = reflection + .args + .get(index) + .cloned() + .unwrap_or_else(|| self.reflection_type_info_for_type(ty)); + (ty, info) + }) + .collect::>() + }) .collect(), ) } @@ -7213,6 +9900,7 @@ impl<'a> TypeChecker<'a> { Type::Function { params, return_type, + .. } => params .iter() .copied() @@ -7226,6 +9914,18 @@ impl<'a> TypeChecker<'a> { match ty { TypeExpr::View(inner, _) => self.type_info_arg_types_for_type_expr(inner), TypeExpr::StateQualified(_, _, _) => Vec::new(), + TypeExpr::Named(ident) + if self + .type_var_reflections + .get(&ident.name) + .is_some_and(|info| info.kind == "alias") => + { + let alias_name = &self.type_var_reflections[&ident.name].type_name; + let Some(alias) = self.type_aliases.get(alias_name).cloned() else { + return Vec::new(); + }; + vec![self.resolve_type_expr(&alias.base_type)] + } TypeExpr::Named(ident) if self.type_aliases.contains_key(&ident.name) => { let alias = self .type_aliases @@ -7249,9 +9949,25 @@ impl<'a> TypeChecker<'a> { } } - fn push_reflected_field_type_scope(&mut self, field_name: &str, field_types: Vec) { + fn push_reflected_field_type_scope( + &mut self, + field_name: &str, + field_types: Vec, + source_infos: Vec, + ) { + let candidates = field_types + .into_iter() + .enumerate() + .map(|(index, ty)| { + let info = source_infos + .get(index) + .cloned() + .unwrap_or_else(|| self.reflection_type_info_for_type(ty)); + (ty, info) + }) + .collect(); let mut scope = HashMap::new(); - scope.insert(field_name.to_string(), field_types); + scope.insert(field_name.to_string(), candidates); self.reflected_field_type_scopes.push(scope); } @@ -7259,7 +9975,11 @@ impl<'a> TypeChecker<'a> { self.reflected_field_type_scopes.pop(); } - fn push_reflected_type_info_scope(&mut self, info_name: &str, info_types: Vec) { + fn push_reflected_type_info_scope( + &mut self, + info_name: &str, + info_types: Vec<(TypeId, ReflectionTypeInfo)>, + ) { let mut scope = HashMap::new(); scope.insert(info_name.to_string(), info_types); self.reflected_type_info_scopes.push(scope); @@ -7280,6 +10000,18 @@ impl<'a> TypeChecker<'a> { } fn push_reflected_machine_state_type_scope(&mut self, state_name: &str, owner_ty: TypeId) { + // type.machine_states[T]() exposes every declared state even when T + // names one state. Its state.fields loops need every corresponding + // checked body; direct machine_state_value[T]().fields stays selected. + let machine = match self.interner.resolve(owner_ty) { + Type::MachineState { machine, .. } => Some(*machine), + _ => None, + }; + let owner_ty = if let Some(machine) = machine { + self.interner.intern(Type::Machine(machine)) + } else { + owner_ty + }; let mut scope = HashMap::new(); scope.insert(state_name.to_string(), owner_ty); self.reflected_machine_state_type_scopes.push(scope); @@ -7295,7 +10027,12 @@ impl<'a> TypeChecker<'a> { fn check_breakpoint(&mut self, breakpoint_stmt: &ast::BreakpointStmt) { if let Some(condition) = &breakpoint_stmt.condition { + let saved_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::BreakpointExpression, + ); let cond_type = self.check_expr(condition); + self.ownership_context = saved_context; if cond_type != TypeInterner::ERROR && cond_type != TypeInterner::BOOL { self.sink.emit(errors::condition_not_bool( &self.type_name(cond_type), @@ -7313,6 +10050,13 @@ impl<'a> TypeChecker<'a> { } Some(expected) => { let val_ty = self.check_expr_for_expected(&resp.value, expected, false); + if (Self::is_explicit_view(&resp.value) + || !crate::ownership::is_implicitly_copyable(&self.interner, val_ty)) + && self.initializer_is_view(&resp.value) + { + self.sink + .emit(crate::ownership::cannot_return_view(resp.value.span())); + } if val_ty != TypeInterner::ERROR && expected != TypeInterner::ERROR && !self.types_compatible(expected, val_ty) @@ -7333,18 +10077,44 @@ impl<'a> TypeChecker<'a> { expected_ty: TypeId, allow_refinement_handle: bool, ) -> TypeId { + let mut source_ty = None; let ty = match expr { + Expr::Paren(inner, _) => { + let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); + source_ty = self.checked_source_expression_type(inner.span()); + ty + } + Expr::Run(inner, _) => { + let saved_in_property_block = self.in_property_block; + self.in_property_block = false; + let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); + self.in_property_block = saved_in_property_block; + source_ty = self.checked_source_expression_type(inner.span()); + ty + } Expr::IntLiteral(value, _) if self.int_literal_matches_expected_type(*value, expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::FloatLiteral(_, _) if self.float_literal_matches_expected_type(expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::Unary(UnaryOp::Neg, inner, _) if self.negated_literal_matches_expected_type(inner, expected_ty) => { + source_ty = Some( + self.direct_secret_inner_type(expected_ty) + .unwrap_or(expected_ty), + ); expected_ty } Expr::Binary(lhs, op, rhs, span) @@ -7362,7 +10132,7 @@ impl<'a> TypeChecker<'a> { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::List(expected_element) => self.check_list_construct_for_expected( elems, expected_ty, @@ -7370,13 +10140,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::MapConstruct(entries, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Map(expected_key, expected_value) => self .check_map_construct_for_expected( entries, @@ -7386,13 +10160,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Some(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Optional(expected_payload) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -7400,13 +10178,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Ok(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Result(expected_payload, _) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -7414,13 +10196,17 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::Fail(inner, _span) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner).clone() { + let ty = match self.interner.resolve(expected_inner).clone() { Type::Result(_, expected_payload) => self.check_wrapper_payload_for_expected( inner, expected_ty, @@ -7428,16 +10214,31 @@ impl<'a> TypeChecker<'a> { allow_refinement_handle, ), _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty } Expr::None(_) => { let expected_inner = self .direct_secret_inner_type(expected_ty) .unwrap_or(expected_ty); - match self.interner.resolve(expected_inner) { + let ty = match self.interner.resolve(expected_inner) { Type::Optional(_) => expected_ty, _ => self.check_expr(expr), + }; + if ty == expected_ty { + source_ty = Some(expected_inner); } + ty + } + Expr::Comptime(inner, _) => { + self.comptime_expr_depth += 1; + let ty = self.check_expr_for_expected(inner, expected_ty, allow_refinement_handle); + self.comptime_expr_depth -= 1; + source_ty = self.checked_source_expression_type(inner.span()); + ty } Expr::Coarsen(inner, span) => { let inner_ty = self.check_expr(inner); @@ -7461,10 +10262,12 @@ impl<'a> TypeChecker<'a> { if allow_refinement_handle && self.is_refinement_type(expected_ty) => { let target_ty = self.check_expr(target); - if matches!( - self.interner.resolve(target_ty), - Type::Result(_, _) | Type::Optional(_) - ) { + if !self.can_refine_from(target_ty, expected_ty) + && matches!( + self.interner.resolve(target_ty), + Type::Result(_, _) | Type::Optional(_) + ) + { self.check_handle_with_target_type(target_ty, bind_name.as_ref(), body, *span) } else { self.check_refinement_handle_with_input_type( @@ -7477,8 +10280,37 @@ impl<'a> TypeChecker<'a> { ) } } + Expr::Handle(target, bind_name, body, span) => { + let target_ty = self.check_expr(target); + let contextual_target = match self.interner.resolve(target_ty).clone() { + Type::Optional(success) => self + .merge_inferred_never(success, expected_ty, false) + .filter(|merged| *merged == expected_ty) + .map(|success| self.interner.intern(Type::Optional(success))), + Type::Result(success, error) => self + .merge_inferred_never(success, expected_ty, false) + .filter(|merged| *merged == expected_ty) + .map(|success| self.interner.intern(Type::Result(success, error))), + _ => None, + } + .unwrap_or(target_ty); + // Check defaults in the output context without changing the + // producer's checked signature. HIR inserts a conversion for + // inhabited success shapes that contain inferred empty slots. + self.check_handle_with_target_type( + contextual_target, + bind_name.as_ref(), + body, + *span, + ) + } _ => { - let actual_ty = self.check_expr(expr); + let actual_ty = match expr { + Expr::Call(callee, args, span) => { + self.check_call(callee, &[], args, *span, Some(expected_ty)) + } + _ => self.check_expr(expr), + }; if actual_ty != TypeInterner::ERROR && self.type_requires_handle_error(expected_ty, actual_ty) { @@ -7504,11 +10336,22 @@ impl<'a> TypeChecker<'a> { )); expected_ty } else { + source_ty = Some(actual_ty); actual_ty } } }; + self.record_source_expression_type(expr.span(), source_ty.unwrap_or(ty)); + + let ty = if let Type::Secret(inner) = self.interner.resolve(expected_ty) + && *inner != TypeInterner::ERROR + && ty == *inner + { + expected_ty + } else { + ty + }; self.record_expression_type(expr.span(), ty); ty } @@ -7546,6 +10389,8 @@ impl<'a> TypeChecker<'a> { if let Some(name) = bind_name { if let Some(def_id) = self.declaration_def_id(name.span) { self.type_env.insert(def_id, TypeInterner::STRING); + self.record_expression_type(name.span, TypeInterner::STRING); + self.record_assignment_binding(name, false, false); self.record_closure_local(def_id); } } @@ -7604,8 +10449,12 @@ impl<'a> TypeChecker<'a> { } fn check_var_decl(&mut self, decl: &ast::VarDecl) { + self.record_debug_type_name(&decl.name, &decl.ty); let declared_type = self.resolve_type_expr(&decl.ty); let init_type = self.check_expr_for_expected(&decl.value, declared_type, true); + self.record_expression_type(decl.name.span, declared_type); + let binding_mode = self.checked_binding_mode(decl, init_type); + self.record_binding_mode(decl.name.span, binding_mode); // Bind the variable's DefId to its declared type. if let Some(def_id) = self.declaration_def_id(decl.name.span) { @@ -7613,10 +10462,15 @@ impl<'a> TypeChecker<'a> { self.record_closure_local(def_id); if !decl.mutable && self.expression_is_proven_nonzero(&decl.value, init_type) { if let Some(scope) = self.nonzero_fact_scopes.last_mut() { - scope.insert(def_id); + scope.insert(NonzeroFact::Binding(def_id)); } } } + self.record_assignment_binding( + &decl.name, + decl.mutable, + Self::type_is_view(&decl.ty) || self.initializer_is_view(&decl.value), + ); // Check that the initializer type matches the declared type (skip if Error). if !self.types_compatible(declared_type, init_type) { @@ -7631,18 +10485,78 @@ impl<'a> TypeChecker<'a> { self.record_reflection_local_fact(decl, declared_type, init_type); } - fn check_assign(&mut self, assign: &ast::AssignStmt) { - if let Expr::Ident(ident) = &assign.target { - if let Some(def_id) = self.ident_def_id(ident) { - self.clear_reflection_local_fact(def_id); - for scope in &mut self.nonzero_fact_scopes { - scope.remove(&def_id); - } + fn is_constant_initializer(&self, expression: &Expr) -> bool { + match expression { + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Comptime(_, _) => true, + Expr::Ident(ident) => self.resolve.resolutions.get(&ident.span).is_some_and(|id| { + self.constant_declarations + .contains(&self.resolve.scope_table.def(*id).span) + }), + Expr::Paren(inner, _) => self.is_constant_initializer(inner), + Expr::Unary(ast::UnaryOp::Neg, inner, _) => { + matches!( + inner.as_ref(), + Expr::IntLiteral(_, _) | Expr::FloatLiteral(_, _) + ) } + _ => false, } + } - let target_type = self.check_expr(&assign.target); + fn check_assign(&mut self, assign: &ast::AssignStmt) { + if let Some(root) = Self::assignment_root(&assign.target) + && let Some(definition) = self.ident_def_id(root) + && self + .constant_declarations + .contains(&self.resolve.scope_table.def(definition).span) + { + self.sink.emit(crate::ownership::cannot_rebind_immutable( + &root.name, root.span, + )); + } else if !matches!(&assign.target, Expr::Ident(_)) { + self.check_projected_assignment_target(&assign.target); + } + let target_type = if Self::assignment_root(&assign.target).is_some() + && !self.check_assignment_owner_policy(&assign.target) + { + TypeInterner::ERROR + } else { + self.check_expr_with_resource_projection( + &assign.target, + ResourceProjectionMode::Borrowed, + ) + }; + if target_type != TypeInterner::ERROR { + self.record_assignment_place(assign); + } + // The RHS may observe the old root under existing local facts. Clear + // changed-root facts after checking it, before any subsequent statement. let value_type = self.check_expr_for_expected(&assign.value, target_type, false); + let mut projected_target = &assign.target; + while let Expr::Paren(inner, _) = projected_target { + projected_target = inner; + } + if matches!(projected_target, Expr::FieldAccess(_, _, _)) + && target_type != TypeInterner::ERROR + && !crate::ownership::is_implicitly_copyable(&self.interner, target_type) + && self.initializer_is_view(&assign.value) + { + self.sink.emit(crate::ownership::cannot_rebind_from_view( + assign.value.span(), + )); + } + if let Some(root) = Self::assignment_root(&assign.target) + && let Some(definition) = self.ident_def_id(root) + { + self.clear_reflection_local_fact(definition); + self.invalidate_nonzero_root(definition); + } + self.check_view_rebinding(assign, target_type, value_type); if !self.types_compatible(target_type, value_type) { self.sink.emit(errors::assign_type_mismatch( @@ -7653,46 +10567,592 @@ impl<'a> TypeChecker<'a> { } } - fn check_return(&mut self, ret: &ast::ReturnStmt) { - let ret_type = match &ret.value { - Some(expr) => { - if let Some(expected) = self.current_return_type { - self.check_expr_for_expected(expr, expected, false) + fn check_assignment_owner_policy(&mut self, target: &Expr) -> bool { + match target { + Expr::Ident(_) => true, + Expr::Paren(inner, _) => self.check_assignment_owner_policy(inner), + Expr::FieldAccess(base, _, span) => { + if !self.check_assignment_owner_policy(base) { + return false; + } + let mut owner = self + .check_expr_with_resource_projection(base, ResourceProjectionMode::Borrowed); + while let Type::Secret(inner) = self.interner.resolve(owner) { + owner = *inner; + } + let message = match self.interner.resolve(owner) { + Type::Struct(id) if self.interner.is_compiler_metadata_struct(*id) => Some( + "cannot assign through compiler-produced reflection metadata; replace the complete genuine value instead", + ), + Type::Refinement { .. } => Some( + "cannot assign through a nominal refinement; replace the complete validated value instead", + ), + Type::Bitfield(_) => Some( + "cannot assign through a bitfield; construct and rebind the complete value instead", + ), + Type::Error => return false, + _ => None, + }; + if let Some(message) = message { + self.sink.emit(Diagnostic::error(404, message, *span)); + false } else { - self.check_expr(expr) + true } } - None => TypeInterner::NOTHING, + _ => false, + } + } + + fn record_assignment_place(&mut self, assign: &ast::AssignStmt) { + let Some(root_ident) = Self::assignment_root(&assign.target) else { + return; + }; + let Some(definition) = self.ident_def_id(root_ident) else { + return; + }; + let declaration = self.resolve.scope_table.def(definition).span; + if self.assignment_bindings.get(&declaration) != Some(&AssignmentBinding::OwnedMutable) { + return; + } + let Some(root) = self.caller_binding_fact(definition) else { + return; + }; + let Some(root_type) = self.checked_expression_type(root_ident.span) else { + return; + }; + let mut projections = Vec::new(); + if !self.collect_assignment_projections(&assign.target, &mut projections) + || projections.is_empty() + { + return; + } + let resource = if self.type_contains_resource_data(root.ty) { + CheckedFieldUpdateResourceRequirement::RequiresCustody + } else { + CheckedFieldUpdateResourceRequirement::Ordinary }; + let place = CheckedAssignmentPlace { + assignment_span: assign.span, + root, + root_use_span: root_ident.span, + root_type, + projections, + resource, + ownership_seal: None, + }; + // A malformed or unsupported layout receives no mutation fact. The + // source's existing type/place diagnostics remain authoritative. + if validate_assignment_place_shape(&place, &self.interner).is_err() { + return; + } + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.assignment_places.insert(assign.span, place); + } else { + self.assignment_places.insert(assign.span, place); + } + } - if let Some(expected) = self.current_return_type { - if !self.satisfies_expected_type(expected, ret_type) { - self.sink.emit(errors::return_type_mismatch( - &self.type_name(expected), - &self.type_name(ret_type), - ret.span, - )); + fn collect_assignment_projections( + &self, + expression: &Expr, + projections: &mut Vec, + ) -> bool { + match expression { + Expr::Ident(_) => true, + Expr::Paren(inner, _) => self.collect_assignment_projections(inner, projections), + Expr::FieldAccess(base, field, span) => { + if !self.collect_assignment_projections(base, projections) { + return false; + } + let Some(owner_type) = self.checked_expression_type(base.span()) else { + return false; + }; + let Some(field_type) = self.checked_expression_type(*span) else { + return false; + }; + let Ok((owner, fields)) = checked_field_owner(&self.interner, owner_type) else { + return false; + }; + let Some((field_index, (_, declared_field_type))) = fields + .iter() + .enumerate() + .find(|(_, (name, _))| name == &field.name) + else { + return false; + }; + projections.push(CheckedFieldProjection { + source_span: *span, + owner_type, + owner, + field_index, + declared_field_type: *declared_field_type, + field_type, + }); + true } + _ => false, } } - fn check_condition_expr(&mut self, condition: &Expr) { - let cond_type = self.check_expr(condition); - if cond_type != TypeInterner::ERROR && cond_type != TypeInterner::BOOL { - self.sink.emit(errors::condition_not_bool( - &self.type_name(cond_type), - condition.span(), + fn assignment_root(target: &Expr) -> Option<&ast::Ident> { + match target { + Expr::Ident(ident) => Some(ident), + Expr::FieldAccess(base, _, _) | Expr::Paren(base, _) => Self::assignment_root(base), + _ => None, + } + } + + fn check_view_rebinding( + &mut self, + assign: &ast::AssignStmt, + target_type: TypeId, + value_type: TypeId, + ) { + if target_type == TypeInterner::ERROR || value_type == TypeInterner::ERROR { + return; + } + let mut target = &assign.target; + while let Expr::Paren(inner, _) = target { + target = inner; + } + let Expr::Ident(target) = target else { + return; + }; + let Some(definition) = self.ident_def_id(target) else { + return; + }; + let span = self.resolve.scope_table.def(definition).span; + if self.assignment_bindings.get(&span) == Some(&AssignmentBinding::OwnedMutable) + && !self.constant_declarations.contains(&span) + && !crate::ownership::is_implicitly_copyable(&self.interner, value_type) + && self.initializer_is_view(&assign.value) + { + self.sink.emit(crate::ownership::cannot_rebind_from_view( + assign.value.span(), )); } } - fn check_if(&mut self, if_stmt: &ast::IfStmt) { - if self.specialize_reflection_branches { - if let Some(selected_branch) = self.static_reflection_if_branch(if_stmt) { - self.check_condition_expr(&if_stmt.condition); - for (else_if_cond, _) in &if_stmt.else_ifs { - self.check_condition_expr(else_if_cond); - } + fn record_assignment_binding(&mut self, name: &ast::Ident, mutable: bool, view: bool) { + let Some(definition) = self.declaration_def_id(name.span) else { + return; + }; + let span = self.resolve.scope_table.def(definition).span; + if let Some(&ty) = self.type_env.get(&definition) { + let checked_mode = self + .active_generic_instantiations + .last() + .and_then(|active| active.binding_modes.get(&name.span)) + .copied() + .or_else(|| { + self.active_generic_instantiations + .is_empty() + .then(|| self.binding_modes.get(&name.span).copied()) + .flatten() + }) + .unwrap_or( + if view && !crate::ownership::is_implicitly_copyable(&self.interner, ty) { + CheckedBindingMode::View { + source: CheckedViewSource::Other, + } + } else { + CheckedBindingMode::Owned + }, + ); + self.record_binding_fact(CheckedBindingFact { + definition, + declaration_span: name.span, + ty, + mode: checked_mode, + mutable, + }); + } + let mode = if view { + Some(AssignmentBinding::View) + } else if mutable { + Some(AssignmentBinding::OwnedMutable) + } else { + None + }; + if let Some(mode) = mode { + self.assignment_bindings.insert(span, mode); + } else { + self.assignment_bindings.remove(&span); + } + } + + fn type_is_view(ty: &TypeExpr) -> bool { + match ty { + TypeExpr::View(_, _) => true, + TypeExpr::StateQualified(inner, _, _) => Self::type_is_view(inner), + _ => false, + } + } + + fn check_owned_resource_payload(&mut self, payload: &Expr, payload_ty: TypeId) { + if payload_ty != TypeInterner::ERROR + && self.type_contains_resource_data(payload_ty) + && self.initializer_is_view(payload) + { + if self.diagnosed_resource_payload_views.insert(payload.span()) { + self.sink.emit(Diagnostic::error( + 401, + "cannot store a resource view in an owned payload; move an owned value instead", + payload.span(), + )); + } + let mut subject = payload; + while let Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) = subject + { + subject = inner; + } + if let Expr::Ident(ident) = subject { + self.diagnosed_owned_view_consumptions.insert( + ident.span, + crate::ownership::cannot_consume_view(&ident.name, ident.span), + ); + } + } + } + + fn check_resource_root_value_reflection(&mut self, owner_ty: TypeId, name: &str, span: Span) { + if matches!(self.interner.resolve(owner_ty), Type::Resource(_)) { + self.sink.emit(errors::type_mismatch( + &format!("non-resource value for {name}"), + &self.type_name(owner_ty), + span, + )); + } + } + + fn initializer_is_view(&self, value: &Expr) -> bool { + match value { + Expr::View(_, _) => true, + Expr::Ident(ident) => self.ident_def_id(ident).is_some_and(|definition| { + let span = self.resolve.scope_table.def(definition).span; + self.assignment_bindings.get(&span) == Some(&AssignmentBinding::View) + && self.type_env.get(&definition).is_some_and(|ty| { + !crate::ownership::is_implicitly_copyable(&self.interner, *ty) + }) + }), + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + self.initializer_is_view(inner) + } + // A plain field initializer copies the field into ownership, even + // when its parent is borrowed. Clone also deliberately owns a copy. + _ => false, + } + } + + fn checked_binding_mode( + &self, + declaration: &ast::VarDecl, + initializer_type: TypeId, + ) -> CheckedBindingMode { + if crate::ownership::is_implicitly_copyable(&self.interner, initializer_type) + || !(Self::type_is_view(&declaration.ty) + || self.initializer_is_view(&declaration.value)) + { + return CheckedBindingMode::Owned; + } + + let mut value = &declaration.value; + let mut explicit_view = false; + loop { + match value { + Expr::View(inner, _) => { + explicit_view = true; + value = inner; + } + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + value = inner; + } + _ => break, + } + } + // Readonly annotations do not turn an ownership-acquiring operation + // into a borrow. Explicitly viewing its result remains a temporary view. + if !explicit_view && matches!(value, Expr::Clone(_, _) | Expr::FieldAccess(_, _, _)) { + return CheckedBindingMode::Owned; + } + let source = self + .borrowed_handle_binding_root(&declaration.value, initializer_type) + .or_else(|| Self::assignment_root(value)) + .and_then(|ident| self.ident_def_id(ident)) + .map(CheckedViewSource::Binding) + .unwrap_or(CheckedViewSource::Other); + CheckedBindingMode::View { source } + } + + /// A borrowed sum payload retains the exact whole binding as its backing. + /// This is provenance only; HIR still checks the complete typed projection. + fn borrowed_handle_binding_root<'source>( + &self, + initializer: &'source Expr, + output: TypeId, + ) -> Option<&'source ast::Ident> { + let Expr::View(value, _) = Self::unparenthesized(initializer) else { + return None; + }; + let Expr::Handle(target, error, failure, _) = Self::unparenthesized(value) else { + return None; + }; + if Self::handle_body_has_default(failure) { + return None; + } + let target_ty = self.checked_expression_type(target.span())?; + match self.interner.resolve(target_ty) { + Type::Optional(payload) if *payload == output && error.is_none() => {} + Type::Result(payload, _) if *payload == output && error.is_some() => {} + _ => return None, + } + let Expr::View(backing, _) = Self::unparenthesized(target) else { + return None; + }; + let Expr::Ident(binding) = Self::unparenthesized(backing) else { + return None; + }; + let definition = self.ident_def_id(binding)?; + if !matches!( + self.resolve.scope_table.def(definition).kind, + DefKind::Variable | DefKind::Param + ) || self.checked_expression_type(backing.span()) != Some(target_ty) + { + return None; + } + Some(binding) + } + + /// Defaults in nested Handle bodies yield to that nested Handle, whereas + /// defaults anywhere in this lexical body can yield an alternate payload. + fn handle_body_has_default(body: &Block) -> bool { + body.stmts.iter().any(|statement| match statement { + Stmt::VarDecl(value) => Self::handle_expression_has_default(&value.value), + Stmt::Assign(value) => { + Self::handle_expression_has_default(&value.target) + || Self::handle_expression_has_default(&value.value) + } + Stmt::Return(value) => value + .value + .as_ref() + .is_some_and(Self::handle_expression_has_default), + Stmt::Respond(value) => Self::handle_expression_has_default(&value.value), + Stmt::ComptimeTypeBind(value) => { + Self::handle_expression_has_default(&value.value) + || Self::handle_body_has_default(&value.body) + } + Stmt::If(value) => { + Self::handle_expression_has_default(&value.condition) + || Self::handle_body_has_default(&value.then_block) + || value.else_ifs.iter().any(|(condition, body)| { + Self::handle_expression_has_default(condition) + || Self::handle_body_has_default(body) + }) + || value + .else_block + .as_ref() + .is_some_and(Self::handle_body_has_default) + } + Stmt::For(value) => { + Self::handle_expression_has_default(&value.iterable) + || Self::handle_body_has_default(&value.body) + } + Stmt::While(value) => { + Self::handle_expression_has_default(&value.condition) + || Self::handle_body_has_default(&value.body) + } + Stmt::Match(value) => { + Self::handle_expression_has_default(&value.expr) + || value + .arms + .iter() + .any(|arm| Self::handle_body_has_default(&arm.body)) + } + Stmt::Expr(value) => Self::handle_expression_has_default(&value.expr), + Stmt::Assert(value) => { + Self::handle_expression_has_default(&value.condition) + || value + .message + .as_ref() + .is_some_and(Self::handle_expression_has_default) + } + Stmt::Breakpoint(value) => value + .condition + .as_ref() + .is_some_and(Self::handle_expression_has_default), + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => false, + }) + } + + fn handle_expression_has_default(value: &Expr) -> bool { + match value { + Expr::Default(_, _) => true, + // Evaluating a nested Handle's target still belongs to this body; + // its failure body has its own nearest handler. + Expr::Handle(target, _, _, _) => Self::handle_expression_has_default(target), + Expr::Binary(left, _, right, _) => { + Self::handle_expression_has_default(left) + || Self::handle_expression_has_default(right) + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => Self::handle_expression_has_default(value), + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + Self::handle_expression_has_default(callee) + || args + .iter() + .any(|arg| Self::handle_expression_has_default(&arg.value)) + } + Expr::ListConstruct(values, _) => { + values.iter().any(Self::handle_expression_has_default) + } + Expr::MapConstruct(entries, _) => entries.iter().any(|(key, value)| { + Self::handle_expression_has_default(key) + || Self::handle_expression_has_default(value) + }), + Expr::StringInterpolation(parts, _) => parts.iter().any(|part| match part { + ast::StringPart::Literal(_) => false, + ast::StringPart::Expr(value) => Self::handle_expression_has_default(value), + }), + Expr::Pipeline(value, steps, _) => { + Self::handle_expression_has_default(value) + || steps.iter().any(|step| { + Self::handle_expression_has_default(&step.function) + || step + .extra_args + .iter() + .any(|arg| Self::handle_expression_has_default(&arg.value)) + }) + } + Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::InlineFn(_, _, _, _) + | Expr::Error(_) => false, + } + } + + fn assignment_place_is_view(&self, target: &Expr) -> bool { + match target { + Expr::View(_, _) => true, + Expr::Ident(ident) => self.ident_def_id(ident).is_some_and(|definition| { + let span = self.resolve.scope_table.def(definition).span; + self.assignment_bindings.get(&span) == Some(&AssignmentBinding::View) + }), + Expr::FieldAccess(inner, _, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) => self.assignment_place_is_view(inner), + _ => false, + } + } + + fn check_projected_assignment_target(&mut self, target: &Expr) { + if self.assignment_place_is_view(target) { + self.sink + .emit(crate::ownership::cannot_assign_through_view(target.span())); + return; + } + let Some(root) = Self::assignment_root(target) else { + self.sink + .emit(crate::ownership::cannot_assign_to_temporary(target.span())); + return; + }; + let Some(definition) = self.ident_def_id(root) else { + return; + }; + let span = self.resolve.scope_table.def(definition).span; + if self.assignment_bindings.get(&span) != Some(&AssignmentBinding::OwnedMutable) { + self.sink.emit(crate::ownership::cannot_rebind_immutable( + &root.name, root.span, + )); + } + } + + fn check_return(&mut self, ret: &ast::ReturnStmt) { + let ret_type = match &ret.value { + Some(expr) => { + if let Some(expected) = self.current_return_type { + self.check_expr_for_expected(expr, expected, false) + } else { + self.check_expr(expr) + } + } + None => TypeInterner::NOTHING, + }; + + if let Some(expected) = self.current_return_type { + if expected != TypeInterner::ERROR + && ret_type != TypeInterner::ERROR + && let Some(value) = &ret.value + && (Self::is_explicit_view(value) + || !crate::ownership::is_implicitly_copyable(&self.interner, ret_type)) + && self.initializer_is_view(value) + { + self.sink + .emit(crate::ownership::cannot_return_view(value.span())); + } + if !self.satisfies_expected_type(expected, ret_type) { + self.sink.emit(errors::return_type_mismatch( + &self.type_name(expected), + &self.type_name(ret_type), + ret.span, + )); + } + } + } + + fn check_condition_expr(&mut self, condition: &Expr) { + let cond_type = self.check_expr(condition); + if cond_type != TypeInterner::ERROR && cond_type != TypeInterner::BOOL { + self.sink.emit(errors::condition_not_bool( + &self.type_name(cond_type), + condition.span(), + )); + } + } + + fn check_if(&mut self, if_stmt: &ast::IfStmt) { + if self.specialize_reflection_branches { + if let Some(selected_branch) = self.static_reflection_if_branch(if_stmt) { + let selection = if selected_branch == 0 { + CheckedStaticSelection::IfThen + } else if selected_branch <= if_stmt.else_ifs.len() { + CheckedStaticSelection::IfElseIf(selected_branch - 1) + } else if if_stmt.else_block.is_some() { + CheckedStaticSelection::IfElse + } else { + CheckedStaticSelection::IfNoBranch + }; + self.record_static_selection(if_stmt.span, selection); + self.check_condition_expr(&if_stmt.condition); + for (else_if_cond, _) in &if_stmt.else_ifs { + self.check_condition_expr(else_if_cond); + } if selected_branch == 0 { let narrowing = self.machine_state_narrowing_for_branch( @@ -7729,18 +11189,6 @@ impl<'a> TypeChecker<'a> { } return; } - - let active_type_params = self.type_var_subst.keys().cloned().collect::>(); - if let Some(span) = - self.if_branch_type_param_reflection_span(if_stmt, &active_type_params) - { - self.check_condition_expr(&if_stmt.condition); - for (else_if_cond, _) in &if_stmt.else_ifs { - self.check_condition_expr(else_if_cond); - } - self.sink.emit(errors::invalid_comptime_type_binding(span)); - return; - } } self.check_condition_expr(&if_stmt.condition); @@ -7789,64 +11237,11 @@ impl<'a> TypeChecker<'a> { matches!(block.stmts.last(), Some(Stmt::Return(_))) } - fn nonzero_defs_from_conditions<'b>( - &self, - conditions: impl IntoIterator, - ) -> HashSet { - let mut facts = HashSet::new(); - for (condition, truth) in conditions { - self.collect_nonzero_defs(condition, truth, &mut facts); - } - facts - } - - fn collect_nonzero_defs(&self, condition: &Expr, truth: bool, facts: &mut HashSet) { - match condition { - Expr::Paren(inner, _) => self.collect_nonzero_defs(inner, truth, facts), - Expr::Unary(UnaryOp::Not, inner, _) => self.collect_nonzero_defs(inner, !truth, facts), - Expr::Binary(lhs, BinOp::And, rhs, _) if truth => { - self.collect_nonzero_defs(lhs, true, facts); - self.collect_nonzero_defs(rhs, true, facts); - } - Expr::Binary(lhs, BinOp::Or, rhs, _) if !truth => { - self.collect_nonzero_defs(lhs, false, facts); - self.collect_nonzero_defs(rhs, false, facts); - } - Expr::Binary(lhs, op @ (BinOp::Eq | BinOp::NotEq), rhs, _) => { - let proves_nonzero = matches!(op, BinOp::NotEq) == truth; - if !proves_nonzero { - return; - } - let ident = if Self::is_zero_integer_literal(rhs) { - Self::narrowable_ident_expr(lhs) - } else if Self::is_zero_integer_literal(lhs) { - Self::narrowable_ident_expr(rhs) - } else { - None - }; - if let Some(def_id) = ident.and_then(|ident| self.ident_def_id(ident)) { - facts.insert(def_id); - } - } - _ => {} - } - } - - fn is_zero_integer_literal(expr: &Expr) -> bool { - match expr { - Expr::IntLiteral(value, _) => *value == 0, - Expr::Unary(UnaryOp::Neg, inner, _) | Expr::Paren(inner, _) => { - Self::is_zero_integer_literal(inner) - } - _ => false, - } - } - fn check_block_with_type_override_and_nonzero( &mut self, block: &Block, override_ty: Option<(DefId, TypeId)>, - nonzero_defs: HashSet, + nonzero_defs: HashSet, ) { self.nonzero_fact_scopes.push(nonzero_defs); self.check_block_with_type_override(block, override_ty); @@ -8049,17 +11444,6 @@ impl<'a> TypeChecker<'a> { None } - fn match_arm_type_param_reflection_span( - &self, - match_stmt: &ast::MatchStmt, - type_params: &HashSet, - ) -> Option { - match_stmt - .arms - .iter() - .find_map(|arm| self.block_type_param_reflection_span(&arm.body, type_params)) - } - fn check_for(&mut self, for_stmt: &ast::ForStmt) { let iterable_type = self.check_expr(&for_stmt.iterable); @@ -8087,8 +11471,10 @@ impl<'a> TypeChecker<'a> { // Bind the loop variable (key for maps, element for lists/strings). if let Some(def_id) = self.declaration_def_id(for_stmt.variable.span) { self.type_env.insert(def_id, elem_type); + self.record_expression_type(for_stmt.variable.span, elem_type); self.record_closure_local(def_id); } + self.record_assignment_binding(&for_stmt.variable, false, for_stmt.view); // Bind the optional value variable (only for map iteration). if let Some(ref val_var) = for_stmt.value_variable { @@ -8099,8 +11485,10 @@ impl<'a> TypeChecker<'a> { }; if let Some(def_id) = self.declaration_def_id(val_var.span) { self.type_env.insert(def_id, val_type); + self.record_expression_type(val_var.span, val_type); self.record_closure_local(def_id); } + self.record_assignment_binding(val_var, false, for_stmt.view); } let pushed_variant_scope = @@ -8130,7 +11518,8 @@ impl<'a> TypeChecker<'a> { } else { self.reflected_field_types_for_owner(owner_ty) }; - self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types); + let infos = self.reflected_field_infos_for_owner(owner_ty); + self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types, infos); true } else if let Some(owner_ty_expr) = comptime_type_variant_fields_binding(&for_stmt.iterable) { @@ -8140,7 +11529,8 @@ impl<'a> TypeChecker<'a> { } else { self.reflected_variant_field_types_for_owner(owner_ty) }; - self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types); + let infos = self.reflected_field_infos_for_owner(owner_ty); + self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types, infos); true } else if let Some(owner_ty_expr) = comptime_type_machine_fields_binding(&for_stmt.iterable) { @@ -8150,7 +11540,8 @@ impl<'a> TypeChecker<'a> { } else { self.reflected_machine_field_types_for_owner(owner_ty) }; - self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types); + let infos = self.reflected_field_infos_for_owner(owner_ty); + self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types, infos); true } else if let Some(fields_owner_name) = reflected_machine_state_fields_binding(&for_stmt.iterable) @@ -8158,12 +11549,14 @@ impl<'a> TypeChecker<'a> { { if let Some(owner_ty) = self.reflected_machine_state_owner_for_name(fields_owner_name) { let field_types = self.reflected_machine_field_types_for_owner(owner_ty); - self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types); + let infos = self.reflected_field_infos_for_owner(owner_ty); + self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types, infos); true } else if let Some(owner_ty) = self.reflected_variant_owner_for_name(fields_owner_name) { let field_types = self.reflected_variant_field_types_for_owner(owner_ty); - self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types); + let infos = self.reflected_field_infos_for_owner(owner_ty); + self.push_reflected_field_type_scope(&for_stmt.variable.name, field_types, infos); true } else { false @@ -8181,6 +11574,7 @@ impl<'a> TypeChecker<'a> { false }; + self.invalidate_nonzero_loop_facts(&for_stmt.body, None); self.check_block(&for_stmt.body); if pushed_type_info_scope { @@ -8198,6 +11592,7 @@ impl<'a> TypeChecker<'a> { } fn check_while(&mut self, while_stmt: &ast::WhileStmt) { + self.invalidate_nonzero_loop_facts(&while_stmt.body, Some(&while_stmt.condition)); let cond_type = self.check_expr(&while_stmt.condition); if cond_type != TypeInterner::ERROR && cond_type != TypeInterner::BOOL { self.sink.emit(errors::condition_not_bool( @@ -8254,24 +11649,18 @@ impl<'a> TypeChecker<'a> { } else { None }; - let unknown_reflection_span = if self.specialize_reflection_branches - && selected_static_arm.is_none() - { - let active_type_params = self.type_var_subst.keys().cloned().collect::>(); - if self.expr_uses_type_param_reflection(&match_stmt.expr, &active_type_params) { - Some(match_stmt.expr.span()) - } else { - self.match_arm_type_param_reflection_span(match_stmt, &active_type_params) - } - } else { - None - }; + if let Some(selected_arm) = selected_static_arm { + self.record_static_selection( + match_stmt.span, + CheckedStaticSelection::MatchArm(selected_arm), + ); + } let mut covered = HashSet::new(); let mut has_other = false; + let borrowed_payloads = self.initializer_is_view(&match_stmt.expr); for (arm_index, arm) in match_stmt.arms.iter().enumerate() { - let check_body = unknown_reflection_span.is_none() - && selected_static_arm.map_or(true, |selected| selected == arm_index); + let check_body = selected_static_arm.map_or(true, |selected| selected == arm_index); match &arm.pattern { ast::Pattern::Ident(name) => { @@ -8311,6 +11700,12 @@ impl<'a> TypeChecker<'a> { { if let Some(def_id) = self.declaration_def_id(binding.span) { self.type_env.insert(def_id, *field_ty); + self.record_expression_type(binding.span, *field_ty); + self.record_assignment_binding( + binding, + false, + borrowed_payloads, + ); self.record_closure_local(def_id); } } @@ -8333,10 +11728,6 @@ impl<'a> TypeChecker<'a> { } } - if let Some(span) = unknown_reflection_span { - self.sink.emit(errors::invalid_comptime_type_binding(span)); - } - if !has_other { for variant in &enum_def.variants { if !covered.contains(&variant.name) { @@ -8373,6 +11764,14 @@ impl<'a> TypeChecker<'a> { } fn check_expr(&mut self, expr: &Expr) -> TypeId { + self.check_expr_with_resource_projection(expr, ResourceProjectionMode::OwningCopy) + } + + fn check_expr_with_resource_projection( + &mut self, + expr: &Expr, + projection: ResourceProjectionMode, + ) -> TypeId { // Provider constructors are direct property operations, not ordinary // function values that can escape through aliases or callbacks. if matches!(expr, Expr::Ident(_) | Expr::FieldAccess(_, _, _)) @@ -8411,13 +11810,17 @@ impl<'a> TypeChecker<'a> { Expr::Ident(ident) => self.check_ident(ident), Expr::Binary(lhs, op, rhs, span) => self.check_binary(lhs, *op, rhs, *span), Expr::Unary(op, operand, span) => self.check_unary(*op, operand, *span), - Expr::Call(callee, args, span) => self.check_call(callee, &[], args, *span), + Expr::Call(callee, args, span) => self.check_call(callee, &[], args, *span, None), Expr::GenericCall(callee, type_args, args, span) => { - self.check_call(callee, type_args, args, *span) + self.check_call(callee, type_args, args, *span, None) + } + Expr::Paren(inner, _) => self.check_expr_with_resource_projection(inner, projection), + Expr::FieldAccess(base, field, span) => { + self.check_field_access(base, field, *span, projection) + } + Expr::View(inner, _) => { + self.check_expr_with_resource_projection(inner, ResourceProjectionMode::Borrowed) } - Expr::Paren(inner, _) => self.check_expr(inner), - Expr::FieldAccess(base, field, span) => self.check_field_access(base, field, *span), - Expr::View(inner, _) => self.check_expr(inner), Expr::Comptime(inner, _) => { self.comptime_expr_depth += 1; let ty = self.check_expr(inner); @@ -8434,24 +11837,29 @@ impl<'a> TypeChecker<'a> { Expr::Ok(inner, _span) => { let inner_ty = self.check_expr(inner); - // ok(T) → result[T, ] — the error type is unknown without context. - // For now, produce result[T, nothing]. + self.check_owned_resource_payload(inner, inner_ty); + // The absent failure payload is uninhabited until context + // supplies a concrete error type. self.interner - .intern(Type::Result(inner_ty, TypeInterner::ERROR)) + .intern(Type::Result(inner_ty, TypeInterner::NEVER)) } Expr::Fail(inner, _span) => { let inner_ty = self.check_expr(inner); - // fail(E) → result[, E] + self.check_owned_resource_payload(inner, inner_ty); + // The absent success payload is uninhabited until context + // supplies a concrete success type. self.interner - .intern(Type::Result(TypeInterner::ERROR, inner_ty)) + .intern(Type::Result(TypeInterner::NEVER, inner_ty)) } Expr::Some(inner, _span) => { let inner_ty = self.check_expr(inner); + self.check_owned_resource_payload(inner, inner_ty); self.interner.intern(Type::Optional(inner_ty)) } Expr::None(_) => { - // none → optional[] (unknown inner type without context) - self.interner.intern(Type::Optional(TypeInterner::ERROR)) + // `none` has no payload, so its context-free payload type is + // the compiler-internal uninhabited type rather than Error. + self.interner.intern(Type::Optional(TypeInterner::NEVER)) } Expr::Default(inner, span) => { if self.handle_body_depth == 0 { @@ -8483,7 +11891,7 @@ impl<'a> TypeChecker<'a> { TypeInterner::STRING } Expr::Declassify(inner, span) => { - let inner_ty = self.check_expr(inner); + let inner_ty = self.check_expr_with_resource_projection(inner, projection); if let Some(unwrapped) = self.secret_inner_type(inner_ty) { unwrapped } else { @@ -8495,7 +11903,7 @@ impl<'a> TypeChecker<'a> { } } Expr::Coarsen(inner, _) => { - let inner_ty = self.check_expr(inner); + let inner_ty = self.check_expr_with_resource_projection(inner, projection); if inner_ty != TypeInterner::ERROR && !self.is_refinement_type(inner_ty) { self.sink.emit(errors::coarsen_requires_refinement( &self.type_name(inner_ty), @@ -8507,19 +11915,17 @@ impl<'a> TypeChecker<'a> { Expr::Pipeline(initial, steps, _) => self.check_pipeline(initial, steps), Expr::At(inner, state, span) => self.check_machine_state_check(inner, state, *span), Expr::Spawn(inner, _) => self.check_spawn(inner), - Expr::Send(inner, _) => { - self.check_send_ask_inner(inner); + Expr::Send(inner, span) => { + self.check_send_ask_inner(inner, *span, CheckedActorMessageKind::Send); TypeInterner::NOTHING } - Expr::Ask(inner, _) => self.check_send_ask_inner(inner), + Expr::Ask(inner, span) => { + self.check_send_ask_inner(inner, *span, CheckedActorMessageKind::Ask) + } Expr::Clone(inner, span) => { - let inner_ty = self.check_expr(inner); - if self.type_contains_resource_data(inner_ty) { - self.sink.emit(errors::resource_cannot_be_cloned( - &self.type_name(inner_ty), - *span, - )); - } + let inner_ty = self + .check_expr_with_resource_projection(inner, ResourceProjectionMode::Borrowed); + self.check_resource_copy(inner_ty, *span); inner_ty } Expr::Run(inner, _) => { @@ -8551,11 +11957,18 @@ impl<'a> TypeChecker<'a> { self.check_enum_variant(type_name, variant, &[], *span) } Expr::InlineFn(params, return_type, body, inline_span) => { + self.check_function_return_view_syntax(params, return_type.as_ref()); // Type-check the inline function body with parameters bound. let saved_return_type = self.current_return_type; let saved_fn_name = self.current_function_name.take(); let saved_pure = self.current_function_pure; + let saved_in_verify_block = self.in_verify_block; + let saved_ownership_context = std::mem::replace( + &mut self.ownership_context, + CheckedOwnershipContext::Ordinary, + ); let saved_in_property_block = self.in_property_block; + let saved_comptime_expr_depth = self.comptime_expr_depth; let saved_graphics_callback = self.in_graphics_callback; let ret = return_type @@ -8565,55 +11978,77 @@ impl<'a> TypeChecker<'a> { self.current_return_type = Some(ret); self.in_graphics_callback = saved_graphics_callback || self.graphics_inline_callbacks.contains(inline_span); - // Callback purity is audited once all method targets are known; - // checking it here would make mutual declarations order-sensitive. - self.current_function_pure = false; + let param_types: Vec<_> = params + .iter() + .map(|param| self.resolve_type_expr(¶m.ty)) + .collect(); + self.current_function_name = Some("".to_string()); + self.current_function_pure = self.function_parameters_are_pure(¶m_types); + // Creating a closure does not execute its body. Check its own + // signature's effects independently from the enclosing context. + self.in_verify_block = false; self.in_property_block = false; + self.comptime_expr_depth = 0; self.closure_capture_scopes .push(ClosureCaptureScope::default()); - for param in params { - let param_type = self.resolve_type_expr(¶m.ty); + for (param, ¶m_type) in params.iter().zip(¶m_types) { + self.record_debug_type_name(¶m.name, ¶m.ty); if let Some(def_id) = self.declaration_def_id(param.name.span) { self.type_env.insert(def_id, param_type); self.record_closure_local(def_id); } + self.record_assignment_binding( + ¶m.name, + param.mutable, + param.view || Self::type_is_view(¶m.ty), + ); } - let param_types: Vec<_> = params - .iter() - .map(|p| self.resolve_type_expr(&p.ty)) - .collect(); - self.check_block(body); self.closure_capture_scopes.pop(); self.current_return_type = saved_return_type; self.current_function_name = saved_fn_name; self.current_function_pure = saved_pure; + self.in_verify_block = saved_in_verify_block; + self.ownership_context = saved_ownership_context; self.in_property_block = saved_in_property_block; + self.comptime_expr_depth = saved_comptime_expr_depth; self.in_graphics_callback = saved_graphics_callback; self.interner.intern(Type::Function { params: param_types, + view_params: params.iter().map(|param| param.view).collect(), return_type: ret, }) } }; - // Record the type for this expression span. + // Record the source operation before any surrounding expectation adds + // its physical conversion at the same expression occurrence. + self.record_source_expression_type(expr.span(), ty); self.record_expression_type(expr.span(), ty); ty } fn check_pipeline(&mut self, initial: &Expr, steps: &[ast::PipelineStep]) -> TypeId { let mut current_ty = self.check_expr(initial); + let mut input_span = initial.span(); for (index, step) in steps.iter().enumerate() { + // Callee expressions can share this span. Seal the physical virtual + // input independently before checking that source occurrence. + self.record_pipeline_step_input_type(step.span, current_ty); // The interpreter uses the checked input type to infer source-generic // arguments for the synthetic first argument of a pipeline call. self.record_expression_type(step.span, current_ty); - current_ty = - self.check_pipeline_step(current_ty, step, (index == 0).then_some(initial)); + current_ty = self.check_pipeline_step( + current_ty, + step, + (index == 0).then_some(initial), + input_span, + ); + input_span = step.span; } current_ty } @@ -8626,11 +12061,21 @@ impl<'a> TypeChecker<'a> { _ => (&step.function, false), }; match function { - Expr::Call(callee, args, _) => (callee, &[], args, piped_as_view), - Expr::GenericCall(callee, type_args, args, _) => { - (callee, type_args, args, piped_as_view) + Expr::Call(callee, args, _) => { + (Self::unparenthesized(callee), &[], args, piped_as_view) } - _ => (function, &[], &step.extra_args, piped_as_view), + Expr::GenericCall(callee, type_args, args, _) => ( + Self::unparenthesized(callee), + type_args, + args, + piped_as_view, + ), + _ => ( + Self::unparenthesized(function), + &[], + &step.extra_args, + piped_as_view, + ), } } @@ -8639,15 +12084,16 @@ impl<'a> TypeChecker<'a> { current_ty: TypeId, step: &ast::PipelineStep, initial: Option<&Expr>, + input_span: Span, ) -> TypeId { let (function, type_args, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); let step_ty = if self.resolved_expr_name(function).as_deref() == Some("graphics.run") { // Preserve the source authority expression while sharing every gate // with ordinary calls, including named arguments and type inference. // A prior pipeline result cannot be a declared capability parameter. - let input = initial.cloned().unwrap_or(Expr::Error(step.span)); + let input = initial.cloned().unwrap_or(Expr::Error(input_span)); let authority = if piped_as_view { - Expr::View(Box::new(input), step.span) + Expr::View(Box::new(input), input_span) } else { input }; @@ -8657,61 +12103,79 @@ impl<'a> TypeChecker<'a> { value: authority, }]; args.extend_from_slice(extra_args); - self.check_call(function, type_args, &args, step.span) + self.check_call(function, type_args, &args, step.span, None) } else { - self.check_pipeline_step_call(current_ty, step) + self.check_pipeline_step_call(current_ty, step, initial, input_span) }; self.record_pipeline_step_call_type(step.span, step_ty); - if let Some(handle) = &step.handle { - return self.check_handle_with_target_type( + let output_ty = if let Some(handle) = &step.handle { + self.check_handle_with_target_type( step_ty, handle.error_name.as_ref(), &handle.body, handle.span, - ); - } - step_ty + ) + } else { + step_ty + }; + self.record_source_expression_type(step.span, output_ty); + output_ty } - fn check_pipeline_step_call(&mut self, current_ty: TypeId, step: &ast::PipelineStep) -> TypeId { - let (function, type_args, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); - let callee_name = self.resolved_expr_name(function); - self.check_test_mock_constructor_call(callee_name.as_deref(), step.span); - let callee_is_pure = callee_name - .as_deref() - .map(|name| self.named_call_is_pure(name)) - .unwrap_or(false); - + fn check_pipeline_step_call( + &mut self, + current_ty: TypeId, + step: &ast::PipelineStep, + initial: Option<&Expr>, + input_span: Span, + ) -> TypeId { + let result = self.check_pipeline_step_call_inner(current_ty, step, initial); + let (callee, _, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + let mut occurrences = vec![( + initial, + input_span, + piped_as_view || initial.is_some_and(Self::is_explicit_view), + self.checked_source_expression_type(input_span), + )]; + occurrences.extend(extra_args.iter().map(|arg| { + ( + Some(&arg.value), + arg.value.span(), + Self::is_explicit_view(&arg.value), + self.checked_source_expression_type(arg.value.span()), + ) + })); + self.record_invocation_ownership(callee, step.span, result, &occurrences); + result + } + + fn check_pipeline_step_call_inner( + &mut self, + current_ty: TypeId, + step: &ast::PipelineStep, + initial: Option<&Expr>, + ) -> TypeId { + let (function, type_args, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + let piped_as_view = piped_as_view || initial.is_some_and(Self::is_explicit_view); + let callee_name = self.resolved_expr_name(function); + self.check_test_mock_constructor_call(callee_name.as_deref(), step.span); + let mut callee_is_pure = callee_name + .as_deref() + .map(|name| self.named_call_is_pure(name)) + .unwrap_or(false); + if let Some(callee_name) = callee_name.as_deref() && !callee_is_pure { - if self.current_function_pure - && let Some(caller_name) = &self.current_function_name - { - self.sink.emit(errors::pure_calls_impure( - caller_name, - callee_name, - step.span, - )); - } - if self.in_verify_block - && let Some(verify_name) = &self.current_verify_name - { - self.sink.emit(errors::verify_calls_impure( - verify_name, - callee_name, - step.span, - )); - } - if self.comptime_expr_depth > 0 { - self.sink - .emit(errors::comptime_calls_impure(callee_name, step.span)); - } + self.check_call_purity(callee_name, step.span); } - if let Some(builtin_name) = callee_name.as_deref() { - match builtin_name { - "math.abs" => { + if let Some(builtin_name) = callee_name.as_deref() + && let Some(intrinsic) = IntrinsicId::from_callable_name(builtin_name) + { + match intrinsic { + IntrinsicId::MathAbs => { + self.record_intrinsic_id(step.span, builtin_name); return self.check_math_abs_pipeline_step( builtin_name, current_ty, @@ -8720,7 +12184,8 @@ impl<'a> TypeChecker<'a> { step.span, ); } - "math.min" | "math.max" => { + IntrinsicId::MathMin | IntrinsicId::MathMax => { + self.record_intrinsic_id(step.span, builtin_name); return self.check_math_min_max_pipeline_step( builtin_name, current_ty, @@ -8733,12 +12198,22 @@ impl<'a> TypeChecker<'a> { } } + let diagnostic_count = self.sink.diagnostics().len(); let builtin_signature = self.builtin_signature(function, type_args, step.span); + if builtin_signature.is_some() && self.sink.diagnostics().len() == diagnostic_count { + if let Some(name) = callee_name.as_deref() { + self.record_intrinsic_id(step.span, name); + } + self.record_intrinsic_type_arguments(step.span, type_args); + self.record_intrinsic_reflection_arguments(step.span, type_args); + } if builtin_signature.is_none() && type_args.is_empty() { if let Some(return_type) = self.check_inferred_generic_function_pipeline_step( callee_name.as_deref(), current_ty, extra_args, + piped_as_view, + initial, step.span, ) { return return_type; @@ -8750,6 +12225,8 @@ impl<'a> TypeChecker<'a> { type_args, current_ty, extra_args, + piped_as_view, + initial, step.span, ) { return return_type; @@ -8776,17 +12253,31 @@ impl<'a> TypeChecker<'a> { None }; + let mut function_view_modes = None; let (param_types, return_type) = if let Some(signature) = builtin_signature { signature } else if let Some(signature) = user_function_signature { + function_view_modes = self.function_value_view_modes(function); signature } else { let callee_ty = self.check_expr(function); match self.interner.resolve(callee_ty).clone() { Type::Function { params, + view_params, return_type, - } => (params, return_type), + } => { + let signature_is_pure = self.function_parameters_are_pure(¶ms); + if !signature_is_pure && (callee_is_pure || callee_name.is_none()) { + self.check_call_purity( + callee_name.as_deref().unwrap_or(""), + step.span, + ); + } + callee_is_pure = signature_is_pure; + function_view_modes = Some(view_params); + (params, return_type) + } _ => { for arg in extra_args { self.check_expr(&arg.value); @@ -8804,15 +12295,17 @@ impl<'a> TypeChecker<'a> { let func_name = callee_name .clone() .unwrap_or_else(|| "".to_string()); - let parameter_names = user_parameter_names.or_else(|| { - method_metadata.as_ref().map(|(_, signature)| { - signature - .params - .iter() - .map(|(name, _, _)| name.clone()) - .collect() + let parameter_names = user_parameter_names + .or_else(|| { + method_metadata.as_ref().map(|(_, signature)| { + signature + .params + .iter() + .map(|(name, _, _)| name.clone()) + .collect() + }) }) - }); + .or_else(|| self.enum_constructor_parameter_names(function)); let argument_order = if let Some(parameter_names) = parameter_names { let Some(order) = self.bind_pipeline_arguments(&func_name, ¶meter_names, extra_args, step.span) @@ -8824,6 +12317,13 @@ impl<'a> TypeChecker<'a> { }; self.record_call_argument_order(step.span, order.clone()); order + } else if function_view_modes.is_some() + && self.reject_unbound_argument_names(&func_name, extra_args) + { + for arg in extra_args { + self.check_expr(&arg.value); + } + return return_type; } else if arg_count != param_types.len() { self.sink.emit(errors::argument_count_mismatch( &func_name, @@ -8843,6 +12343,18 @@ impl<'a> TypeChecker<'a> { let mut checked_arg_types = Vec::with_capacity(arg_count); for (parameter_index, &source_index) in argument_order.iter().enumerate() { let param_ty = param_types[parameter_index]; + if let Some(view_parameter) = function_view_modes + .as_ref() + .and_then(|modes| modes.get(parameter_index)) + { + self.check_pipeline_argument_view( + *view_parameter, + source_index, + piped_as_view, + extra_args, + step.span, + ); + } let (arg_ty, arg_span) = if source_index == 0 { (current_ty, step.span) } else { @@ -8852,6 +12364,20 @@ impl<'a> TypeChecker<'a> { arg.value.span(), ) }; + if let Some(view_parameter) = function_view_modes + .as_ref() + .and_then(|modes| modes.get(parameter_index)) + { + self.check_pipeline_owned_argument( + *view_parameter, + source_index, + piped_as_view, + initial, + extra_args, + param_ty, + arg_ty, + ); + } checked_arg_types.push(arg_ty); tainted_return |= self.check_argument_against_param_type( callee_name.as_deref(), @@ -8886,10 +12412,32 @@ impl<'a> TypeChecker<'a> { return_type, ); - if let Some(callee_name) = callee_name.as_deref() - && tainted_return - && Self::is_secret_liftable_call(callee_name, callee_is_pure) + if let Some(name @ ("json.serialize" | "json.serialize_public")) = callee_name.as_deref() + && matches!( + self.interner.resolve(current_ty), + Type::Struct(_) + | Type::Refinement { .. } + | Type::Machine(_) + | Type::MachineState { .. } + | Type::List(_) + | Type::Set(_) + | Type::Map(_, _) + | Type::Optional(_) + | Type::Result(_, _) + ) + && self.native_json_source_supported(current_ty, &mut HashSet::new()) + { + self.check_source_facade_instantiation(name, current_ty, step.span); + } + + if let Some(name @ ("json.parse" | "json.parse_exact")) = callee_name.as_deref() + && let Type::Result(value_ty, _) = self.interner.resolve(return_type) + && self.native_json_parse_source_supported(*value_ty) { + self.check_source_facade_instantiation(name, *value_ty, step.span); + } + + if tainted_return { return self.maybe_wrap_secret(return_type, true); } @@ -8901,6 +12449,8 @@ impl<'a> TypeChecker<'a> { callee_name: Option<&str>, current_ty: TypeId, extra_args: &[ast::CallArg], + piped_as_view: bool, + initial: Option<&Expr>, span: Span, ) -> Option { let function_name = callee_name?; @@ -8930,13 +12480,23 @@ impl<'a> TypeChecker<'a> { } }) .collect::>(); - let Some(inferred) = self.infer_generic_signature(&template, &actual_types) else { + let Some(mut inferred) = self.infer_generic_signature(&template, &actual_types, None) + else { self.emit_cannot_infer_generic(function_name, &template, span); return Some(TypeInterner::ERROR); }; let mut arguments_match = true; - for (&source_index, &expected) in argument_order.iter().zip(&inferred.param_types) { + for (parameter_index, (&source_index, &expected)) in + argument_order.iter().zip(&inferred.param_types).enumerate() + { + self.check_pipeline_argument_view( + template.params[parameter_index].view, + source_index, + piped_as_view, + extra_args, + span, + ); let (got, arg_span) = if source_index == 0 { (current_ty, span) } else { @@ -8946,7 +12506,22 @@ impl<'a> TypeChecker<'a> { arg.value.span(), ) }; - if !self.types_compatible(expected, got) { + self.check_pipeline_owned_argument( + template.params[parameter_index].view, + source_index, + piped_as_view, + initial, + extra_args, + expected, + got, + ); + if !self.graphics_callback_mode_only_mismatch( + function_name, + parameter_index, + expected, + got, + ) && !self.types_compatible(expected, got) + { arguments_match = false; self.sink.emit(errors::type_mismatch( &self.type_name(expected), @@ -8957,14 +12532,44 @@ impl<'a> TypeChecker<'a> { } if arguments_match { - self.record_generic_call_for_template(span, &template, &inferred.concrete_args); + let param_facts = ReflectionParamFacts::default(); + let expressions = argument_order + .iter() + .map(|&index| { + if index == 0 { + None + } else { + Some(&extra_args[index - 1].value) + } + }) + .collect::>(); + let type_argument_reflections = self.reflection_arguments_for_inferred_call( + &template, + &inferred.concrete_args, + &actual_types, + &expressions, + ); + Self::apply_inferred_reflection_kinds( + &template, + &type_argument_reflections, + &mut inferred.kind_subst, + ); + self.record_generic_call_for_template( + span, + &template, + &inferred.concrete_args, + &inferred.kind_subst, + &type_argument_reflections, + ¶m_facts, + ); self.check_generic_function_instantiation( function_name, &template, &inferred.concrete_args, + type_argument_reflections, inferred.subst, inferred.kind_subst, - ReflectionParamFacts::default(), + param_facts, ); } Some(inferred.return_type) @@ -8976,6 +12581,8 @@ impl<'a> TypeChecker<'a> { type_args: &[TypeExpr], current_ty: TypeId, extra_args: &[ast::CallArg], + piped_as_view: bool, + initial: Option<&Expr>, span: Span, ) -> Option { let function_name = callee_name?; @@ -9064,7 +12671,16 @@ impl<'a> TypeChecker<'a> { self.record_call_argument_order(span, argument_order.clone()); let mut arguments_match = true; - for (&source_index, &expected) in argument_order.iter().zip(¶m_types) { + for (parameter_index, (&source_index, &expected)) in + argument_order.iter().zip(¶m_types).enumerate() + { + self.check_pipeline_argument_view( + template.params[parameter_index].view, + source_index, + piped_as_view, + extra_args, + span, + ); let (got, arg_span) = if source_index == 0 { (current_ty, span) } else { @@ -9074,7 +12690,22 @@ impl<'a> TypeChecker<'a> { arg.value.span(), ) }; - if !self.types_compatible(expected, got) { + self.check_pipeline_owned_argument( + template.params[parameter_index].view, + source_index, + piped_as_view, + initial, + extra_args, + expected, + got, + ); + if !self.graphics_callback_mode_only_mismatch( + function_name, + parameter_index, + expected, + got, + ) && !self.types_compatible(expected, got) + { arguments_match = false; self.sink.emit(errors::type_mismatch( &self.type_name(expected), @@ -9085,14 +12716,25 @@ impl<'a> TypeChecker<'a> { } if arguments_match { - self.record_generic_call_for_template(span, &template, &concrete_args); + let param_facts = ReflectionParamFacts::default(); + let type_argument_reflections = + self.reflection_arguments_for_type_args(type_args, &concrete_args); + self.record_generic_call_for_template( + span, + &template, + &concrete_args, + &kind_subst, + &type_argument_reflections, + ¶m_facts, + ); self.check_generic_function_instantiation( function_name, &template, &concrete_args, + type_argument_reflections, subst, kind_subst, - ReflectionParamFacts::default(), + param_facts, ); } @@ -9319,7 +12961,12 @@ impl<'a> TypeChecker<'a> { TypeInterner::ERROR } - fn check_send_ask_inner(&mut self, inner: &Expr) -> TypeId { + fn check_send_ask_inner( + &mut self, + inner: &Expr, + outer_span: Span, + kind: CheckedActorMessageKind, + ) -> TypeId { // inner is `actor_expr.handler_name` or `actor_expr.handler_name(args)` // We check the actor expression and any args, and return the responds type. let (actor_expr, message_name, message_span, args) = match inner { @@ -9347,8 +12994,24 @@ impl<'a> TypeChecker<'a> { // Look up the handler and return its responds type. if let Type::Actor(aid) = *self.interner.resolve(actor_ty) { let actor_def = self.interner.resolve_actor(aid).clone(); - if let Some(msg) = actor_def.messages.iter().find(|m| m.name == *message_name) { + if let Some((handler_index, msg)) = actor_def + .messages + .iter() + .enumerate() + .find(|(_, m)| m.name == *message_name) + { self.check_actor_message_args(&actor_def.name, msg, args, inner.span()); + self.record_actor_message_call( + outer_span, + kind, + inner, + actor_expr, + actor_ty, + aid, + handler_index, + msg.responds, + args.unwrap_or(&[]), + ); return msg.responds; } self.sink.emit(errors::type_has_no_member( @@ -9567,6 +13230,77 @@ impl<'a> TypeChecker<'a> { } } + fn is_direct_collection_equality_type(&self, mut ty: TypeId) -> bool { + loop { + match self.interner.resolve(ty) { + Type::Secret(inner) | Type::Refinement { base: inner, .. } => ty = *inner, + Type::Bytes + | Type::List(_) + | Type::Map(_, _) + | Type::Set(_) + | Type::Optional(_) + | Type::Result(_, _) => return true, + _ => return false, + } + } + } + + fn is_erased_interface_equality_type(&self, mut ty: TypeId) -> bool { + loop { + match self.interner.resolve(ty) { + Type::Secret(inner) | Type::Refinement { base: inner, .. } => ty = *inner, + Type::Interface(_) => return true, + _ => return false, + } + } + } + + fn aggregate_equality_owners(&self, ty: TypeId) -> Vec { + let mut pending = vec![ty]; + let mut seen = HashSet::new(); + let mut owners = Vec::new(); + while let Some(ty) = pending.pop() { + if !seen.insert(ty) { + continue; + } + match self.interner.resolve(ty) { + Type::Struct(_) | Type::Interface(_) => owners.push(ty), + Type::Secret(inner) + | Type::Refinement { base: inner, .. } + | Type::List(inner) + | Type::Set(inner) + | Type::Optional(inner) => pending.push(*inner), + Type::Map(key, value) | Type::Result(key, value) => pending.extend([*value, *key]), + Type::Enum(id) => pending.extend( + self.interner + .resolve_enum(*id) + .variants + .iter() + .rev() + .flat_map(|variant| variant.fields.iter().rev().map(|(_, ty)| *ty)), + ), + Type::Machine(id) | Type::MachineState { machine: id, .. } => pending.extend( + self.interner + .resolve_machine(*id) + .states + .iter() + .rev() + .flat_map(|state| state.fields.iter().rev().map(|(_, ty)| *ty)), + ), + Type::Bitfield(id) => pending.extend( + self.interner + .resolve_bitfield(*id) + .fields + .iter() + .rev() + .map(|field| field.ty), + ), + _ => {} + } + } + owners + } + fn check_binary(&mut self, lhs: &Expr, op: BinOp, rhs: &Expr, span: Span) -> TypeId { let (lhs_ty, rhs_ty) = if Self::is_numeric_literal(lhs) && !Self::is_numeric_literal(rhs) { let rhs_ty = self.check_expr(rhs); @@ -9669,6 +13403,22 @@ impl<'a> TypeChecker<'a> { )); return TypeInterner::ERROR; } + if self.is_direct_collection_equality_type(lhs_base) { + self.sink.emit(errors::direct_collection_equality( + &self.type_name(lhs_ty), + Self::binop_str(op), + span, + )); + return TypeInterner::ERROR; + } + if self.is_erased_interface_equality_type(lhs_base) { + self.sink.emit(errors::erased_interface_equality( + &self.type_name(lhs_ty), + Self::binop_str(op), + span, + )); + return TypeInterner::ERROR; + } if !self.user_struct_has_explicit_equality(lhs_base) { self.sink.emit(errors::equality_requires_equatable( &self.type_name(lhs_base), @@ -9677,6 +13427,44 @@ impl<'a> TypeChecker<'a> { )); return TypeInterner::ERROR; } + if matches!( + self.interner.resolve(self.fully_coarsened_type(lhs_base)), + Type::Enum(_) + ) { + for owner in self.aggregate_equality_owners(lhs_base) { + if self.is_erased_interface_equality_type(owner) { + self.sink.emit(errors::erased_interface_equality( + &self.type_name(owner), + Self::binop_str(op), + span, + )); + return TypeInterner::ERROR; + } + if !self.user_struct_has_explicit_equality(owner) { + self.sink.emit(errors::equality_requires_equatable( + &self.type_name(owner), + Self::binop_str(op), + span, + )); + return TypeInterner::ERROR; + } + } + } + if matches!(self.interner.resolve(lhs_base), Type::Struct(_)) { + // Equality is a checked method call, not structural data + // comparison. Preserve its exact body identity for HIR, + // including concrete generic/body fact snapshots. + if let Some(&interface) = self.named_types.get("Equatable") + && let Some(&index) = self.interface_method_definitions.get(&( + interface, + lhs_base, + "equals".to_string(), + )) + { + let source_span = self.method_definitions[index].source_span; + self.record_method_call(span, source_span); + } + } self.maybe_wrap_secret(TypeInterner::BOOL, tainted) } @@ -9892,6 +13680,13 @@ impl<'a> TypeChecker<'a> { self.expect_no_type_args(name, type_args, span); for arg in args { let arg_ty = self.check_expr(&arg.value); + if matches!(self.interner.resolve(arg_ty), Type::Resource(_)) { + self.sink.emit(errors::type_mismatch( + "non-resource value for ordinary printing", + &self.type_name(arg_ty), + arg.value.span(), + )); + } if self.is_secret_type(arg_ty) { self.sink.emit(errors::secret_exposure( name, @@ -9935,7 +13730,7 @@ impl<'a> TypeChecker<'a> { match self.math_numeric_policy_base(current_ty) { Some((base, tainted)) => { - self.check_math_source_facade_instantiation(name, base, span); + self.check_source_facade_instantiation(name, base, span); self.maybe_wrap_secret(base, tainted) } None => { @@ -10014,7 +13809,7 @@ impl<'a> TypeChecker<'a> { return TypeInterner::ERROR; } - self.check_math_source_facade_instantiation(name, base, span); + self.check_source_facade_instantiation(name, base, span); self.maybe_wrap_secret(base, left_tainted || right_tainted) } @@ -10029,12 +13824,7 @@ impl<'a> TypeChecker<'a> { } } - fn check_math_source_facade_instantiation( - &mut self, - name: &str, - concrete: TypeId, - call_span: Span, - ) { + fn check_source_facade_instantiation(&mut self, name: &str, concrete: TypeId, call_span: Span) { let Some(template) = self.generic_function_templates.get(name).cloned() else { return; }; @@ -10042,14 +13832,25 @@ impl<'a> TypeChecker<'a> { return; }; let subst = HashMap::from([(type_param.name.clone(), concrete)]); - self.record_generic_call_for_template(call_span, &template, &[concrete]); + let kind_subst = HashMap::new(); + let param_facts = ReflectionParamFacts::default(); + let type_argument_reflections = self.reflection_arguments_for_inferred_types(&[concrete]); + self.record_generic_call_for_template( + call_span, + &template, + &[concrete], + &kind_subst, + &type_argument_reflections, + ¶m_facts, + ); self.check_generic_function_instantiation( name, &template, &[concrete], + type_argument_reflections, subst, - HashMap::new(), - ReflectionParamFacts::default(), + kind_subst, + param_facts, ); } @@ -10077,7 +13878,7 @@ impl<'a> TypeChecker<'a> { match self.math_numeric_policy_base(arg_ty) { Some((base, tainted)) => { - self.check_math_source_facade_instantiation(name, base, span); + self.check_source_facade_instantiation(name, base, span); self.maybe_wrap_secret(base, tainted) } None => { @@ -10151,7 +13952,7 @@ impl<'a> TypeChecker<'a> { return TypeInterner::ERROR; } - self.check_math_source_facade_instantiation(name, base, span); + self.check_source_facade_instantiation(name, base, span); self.maybe_wrap_secret(base, left_tainted || right_tainted) } @@ -10161,13 +13962,47 @@ impl<'a> TypeChecker<'a> { type_args: &[TypeExpr], args: &[ast::CallArg], span: Span, + expected_return_type: Option, + ) -> TypeId { + let result = self.check_call_inner(callee, type_args, args, span, expected_return_type); + let occurrences = args + .iter() + .map(|arg| { + ( + Some(&arg.value), + arg.value.span(), + Self::is_explicit_view(&arg.value), + self.checked_source_expression_type(arg.value.span()), + ) + }) + .collect::>(); + self.record_invocation_ownership(callee, span, result, &occurrences); + result + } + + fn check_call_inner( + &mut self, + callee: &Expr, + type_args: &[TypeExpr], + args: &[ast::CallArg], + span: Span, + expected_return_type: Option, ) -> TypeId { + let callee = Self::unparenthesized(callee); let callee_name = self.resolved_expr_name(callee); - let callee_is_pure = callee_name + let mut callee_is_pure = callee_name .as_deref() .map(|name| self.named_call_is_pure(name)) .unwrap_or(false); + let diagnostic_count = self.sink.diagnostics().len(); let builtin_signature = self.builtin_signature(callee, type_args, span); + if builtin_signature.is_some() && self.sink.diagnostics().len() == diagnostic_count { + if let Some(name) = callee_name.as_deref() { + self.record_intrinsic_id(span, name); + } + self.record_intrinsic_type_arguments(span, type_args); + self.record_intrinsic_reflection_arguments(span, type_args); + } if let Some(name @ ("time.now_ms" | "time.now_s")) = callee_name.as_deref() { let replacement = if name == "time.now_ms" { @@ -10233,45 +14068,28 @@ impl<'a> TypeChecker<'a> { // Extract the callee name so we can look it up in the purity map. if let Some(callee_name) = callee_name.as_deref() { if !callee_is_pure { - // E0500: pure function calls impure function - if self.current_function_pure { - if let Some(caller_name) = &self.current_function_name { - self.sink - .emit(errors::pure_calls_impure(caller_name, &callee_name, span)); - } - } - // Verify blocks are the current value-level comptime entrypoint. - // Eligibility is exactly purity: do not add a second allowlist - // for user, dependency, or stdlib functions here. - // E0501: verify block calls impure function - if self.in_verify_block { - if let Some(verify_name) = &self.current_verify_name { - self.sink.emit(errors::verify_calls_impure( - verify_name, - &callee_name, - span, - )); - } - } - if self.comptime_expr_depth > 0 { - self.sink - .emit(errors::comptime_calls_impure(callee_name, span)); - } + self.check_call_purity(callee_name, span); } } - if let Some(builtin_name) = callee_name.as_deref() { - match builtin_name { - "range" => { + if let Some(builtin_name) = callee_name.as_deref() + && let Some(intrinsic) = IntrinsicId::from_callable_name(builtin_name) + { + match intrinsic { + IntrinsicId::Range => { + self.record_intrinsic_id(span, builtin_name); return self.check_range_builtin_call(builtin_name, type_args, args, span); } - "print" | "println" => { + IntrinsicId::Print | IntrinsicId::Println => { + self.record_intrinsic_id(span, builtin_name); return self.check_print_builtin_call(builtin_name, type_args, args, span); } - "math.abs" => { + IntrinsicId::MathAbs => { + self.record_intrinsic_id(span, builtin_name); return self.check_math_abs_call_policy(builtin_name, type_args, args, span); } - "math.min" | "math.max" => { + IntrinsicId::MathMin | IntrinsicId::MathMax => { + self.record_intrinsic_id(span, builtin_name); return self.check_math_min_max_call_policy( builtin_name, type_args, @@ -10288,7 +14106,12 @@ impl<'a> TypeChecker<'a> { && let Some(function_name) = callee_name.as_deref() && self.generic_function_templates.contains_key(function_name) { - return self.check_inferred_generic_function_call(function_name, args, span); + return self.check_inferred_generic_function_call( + function_name, + args, + span, + expected_return_type, + ); } // Check for generic function call: `name[T](args...)`. @@ -10379,12 +14202,30 @@ impl<'a> TypeChecker<'a> { self.record_call_argument_order(span, argument_order.clone()); self.check_graphics_run_policy(function_name, args, &argument_order); let mut arguments_match = true; - for (&source_index, &expected) in argument_order.iter().zip(param_types.iter()) + for (parameter_index, (&source_index, &expected)) in + argument_order.iter().zip(param_types.iter()).enumerate() { let arg = &args[source_index]; + self.check_argument_view( + template.params[parameter_index].view, + Self::is_explicit_view(&arg.value), + arg.value.span(), + ); let got = self.check_expr_for_expected(&arg.value, expected, false); + self.check_owned_argument( + template.params[parameter_index].view, + &arg.value, + expected, + got, + ); self.check_graphics_opaque_argument(function_name, expected, got, arg); - if !self.types_compatible(expected, got) { + if !self.graphics_callback_mode_only_mismatch( + function_name, + parameter_index, + expected, + got, + ) && !self.types_compatible(expected, got) + { arguments_match = false; self.sink.emit(errors::type_mismatch( &self.type_name(expected), @@ -10403,11 +14244,21 @@ impl<'a> TypeChecker<'a> { ¶m_types, &ordered_args, ); - self.record_generic_call_for_template(span, &template, &concrete_args); + let type_argument_reflections = + self.reflection_arguments_for_type_args(type_args, &concrete_args); + self.record_generic_call_for_template( + span, + &template, + &concrete_args, + &kind_subst, + &type_argument_reflections, + ¶m_facts, + ); self.check_generic_function_instantiation( function_name, &template, &concrete_args, + type_argument_reflections, subst, kind_subst, param_facts, @@ -10464,14 +14315,6 @@ impl<'a> TypeChecker<'a> { if let Some(type_id) = self.named_types.get(type_name).copied() { match self.interner.resolve(type_id).clone() { Type::Struct(sid) => { - if Self::is_reflection_metadata_type_name(type_name) { - self.sink - .emit(errors::reflection_metadata_constructor(type_name, span)); - for arg in args { - self.check_expr(&arg.value); - } - return TypeInterner::ERROR; - } return self.check_struct_constructor(sid, args, span); } Type::Bitfield(bid) => { @@ -10510,9 +14353,11 @@ impl<'a> TypeChecker<'a> { None }; + let mut function_view_modes = None; let (param_types, return_type) = if let Some(signature) = builtin_signature { signature } else if let Some(signature) = user_function_signature { + function_view_modes = self.function_value_view_modes(callee); signature } else { if let Some(function_name) = callee_name.as_deref() @@ -10548,8 +14393,20 @@ impl<'a> TypeChecker<'a> { match self.interner.resolve(callee_ty).clone() { Type::Function { params, + view_params, return_type, - } => (params, return_type), + } => { + let signature_is_pure = self.function_parameters_are_pure(¶ms); + if !signature_is_pure && (callee_is_pure || callee_name.is_none()) { + self.check_call_purity( + callee_name.as_deref().unwrap_or(""), + span, + ); + } + callee_is_pure = signature_is_pure; + function_view_modes = Some(view_params); + (params, return_type) + } Type::Struct(sid) if self.is_struct_type_name_expr(callee) => { return self.check_struct_constructor(sid, args, span); } @@ -10573,15 +14430,17 @@ impl<'a> TypeChecker<'a> { let func_name = callee_name .clone() .unwrap_or_else(|| "".to_string()); - let parameter_names = user_parameter_names.or_else(|| { - method_metadata.as_ref().map(|(_, signature)| { - signature - .params - .iter() - .map(|(name, _, _)| name.clone()) - .collect() + let parameter_names = user_parameter_names + .or_else(|| { + method_metadata.as_ref().map(|(_, signature)| { + signature + .params + .iter() + .map(|(name, _, _)| name.clone()) + .collect() + }) }) - }); + .or_else(|| self.enum_constructor_parameter_names(callee)); let argument_order = if let Some(parameter_names) = parameter_names { let Some(order) = self.bind_call_arguments(&func_name, ¶meter_names, args, span) else { @@ -10592,6 +14451,13 @@ impl<'a> TypeChecker<'a> { }; self.record_call_argument_order(span, order.clone()); order + } else if function_view_modes.is_some() + && self.reject_unbound_argument_names(&func_name, args) + { + for arg in args { + self.check_expr(&arg.value); + } + return return_type; } else if args.len() != param_types.len() { self.sink.emit(errors::argument_count_mismatch( &func_name, @@ -10614,7 +14480,22 @@ impl<'a> TypeChecker<'a> { for (i, &source_index) in argument_order.iter().enumerate() { let arg = &args[source_index]; let param_ty = param_types[i]; + if let Some(view_parameter) = function_view_modes + .as_ref() + .and_then(|modes: &Vec| modes.get(i)) + { + self.check_argument_view( + *view_parameter, + Self::is_explicit_view(&arg.value), + arg.value.span(), + ); + } let arg_ty = self.check_expr_for_expected(&arg.value, param_ty, false); + if let Some(view_parameter) = + function_view_modes.as_ref().and_then(|modes| modes.get(i)) + { + self.check_owned_argument(*view_parameter, &arg.value, param_ty, arg_ty); + } checked_arg_types.push(arg_ty); if self.is_refinement_type(param_ty) @@ -10672,7 +14553,10 @@ impl<'a> TypeChecker<'a> { continue; } - if !self.types_compatible(param_ty, arg_ty) { + let graphics_mode_only_mismatch = callee_name.as_deref().is_some_and(|name| { + self.graphics_callback_mode_only_mismatch(name, i, param_ty, arg_ty) + }); + if !graphics_mode_only_mismatch && !self.types_compatible(param_ty, arg_ty) { let param_name = format!("#{}", i + 1); self.sink.emit(errors::argument_type_mismatch( ¶m_name, @@ -10712,13 +14596,106 @@ impl<'a> TypeChecker<'a> { return_type, ); - if let Some(callee_name) = callee_name.as_deref() { - if tainted_return && Self::is_secret_liftable_call(callee_name, callee_is_pure) { - return self.maybe_wrap_secret(return_type, true); - } - } - - return_type + if matches!( + callee_name.as_deref(), + Some("json.serialize" | "json.serialize_public") + ) && let Some(&value_ty) = checked_arg_types.first() + && self.native_json_enum_source_supported(value_ty) + { + self.check_source_facade_instantiation( + "json.json_serialize_native_enum", + value_ty, + span, + ); + } + + if matches!( + callee_name.as_deref(), + Some("json.serialize" | "json.serialize_public") + ) && let Some(&value_ty) = checked_arg_types.first() + && self.native_json_bitfield_source_supported(value_ty) + { + self.check_source_facade_instantiation( + "json.json_serialize_native_bitfield", + value_ty, + span, + ); + } + + if let Some(name @ ("json.parse" | "json.parse_exact")) = callee_name.as_deref() + && let Type::Result(value_ty, _) = self.interner.resolve(return_type) + && self.native_json_enum_parse_supported(*value_ty) + { + let source = if name == "json.parse" { + "json.json_parse_native_enum" + } else { + "json.json_parse_exact_native_enum" + }; + self.check_source_facade_instantiation(source, *value_ty, span); + } + + if let Some(name @ ("json.parse" | "json.parse_exact")) = callee_name.as_deref() + && let Type::Result(value_ty, _) = self.interner.resolve(return_type) + && self.native_json_bitfield_parse_supported(*value_ty) + { + let source = if name == "json.parse" { + "json.json_parse_native_bitfield" + } else { + "json.json_parse_exact_native_bitfield" + }; + self.check_source_facade_instantiation(source, *value_ty, span); + } + + if let Some(name @ ("json.serialize" | "json.serialize_public")) = callee_name.as_deref() + && let Some(&value_ty) = checked_arg_types.first() + && matches!( + self.interner.resolve(value_ty), + Type::Struct(_) + | Type::Refinement { .. } + | Type::Machine(_) + | Type::MachineState { .. } + | Type::List(_) + | Type::Set(_) + | Type::Map(_, _) + | Type::Optional(_) + | Type::Result(_, _) + ) + && self.native_json_source_supported(value_ty, &mut HashSet::new()) + { + self.check_source_facade_instantiation(name, value_ty, span); + } + + if let Some(name @ ("json.parse" | "json.parse_exact")) = callee_name.as_deref() + && let Type::Result(value_ty, _) = self.interner.resolve(return_type) + && matches!( + self.interner.resolve(*value_ty), + Type::Struct(_) + | Type::Refinement { .. } + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Secret(_) + | Type::List(_) + | Type::Set(_) + | Type::Map(_, _) + | Type::Optional(_) + | Type::Result(_, _) + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Uint16 + | Type::Uint32 + | Type::Float32 + ) + && self.native_json_parse_source_supported(*value_ty) + { + self.check_source_facade_instantiation(name, *value_ty, span); + } + + if tainted_return { + return self.maybe_wrap_secret(return_type, true); + } + + return_type } fn bind_call_arguments( @@ -10874,9 +14851,18 @@ impl<'a> TypeChecker<'a> { let Expr::FieldAccess(base, field, _) = callee else { return None; }; - let base_name = Self::extract_dotted_name(base)?; - let base_name = self.resolved_or_expanded_name(&base_name, base.span()); - let owner_type = self.named_types.get(&base_name).copied()?; + let resolved_owner = self + .resolve + .resolutions + .get(&callee.span()) + .and_then(|definition| { + let name = &self.resolve.scope_table.def(*definition).name; + self.named_types.get(name).copied() + }); + let owner_type = resolved_owner.or_else(|| { + let base_name = self.expanded_dotted_expr_name(base)?; + self.named_types.get(&base_name).copied() + })?; let signature = match self.interner.resolve(owner_type) { Type::Interface(iid) => self .interner @@ -10917,6 +14903,27 @@ impl<'a> TypeChecker<'a> { let Expr::FieldAccess(_, field, _) = callee else { return; }; + if checked_arg_types.first() == Some(&declared_owner) + && let Type::Interface(id) = self.interner.resolve(declared_owner) + && let Some(method_index) = self + .interner + .resolve_interface(*id) + .methods + .iter() + .position(|method| method.name == field.name) + { + let call = CheckedInterfaceCall { + interface_type: declared_owner, + method_index, + }; + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.interface_calls.insert(span, call); + } else { + self.interface_calls.insert(span, call); + } + self.clear_interface_method_value(callee.span()); + return; + } let definition_index = match self.interner.resolve(declared_owner) { Type::Interface(_) => checked_arg_types.first().and_then(|owner_type| { self.interface_method_definitions @@ -10931,6 +14938,7 @@ impl<'a> TypeChecker<'a> { if let Some(index) = definition_index { let source_span = self.method_definitions[index].source_span; self.record_method_call(span, source_span); + self.clear_interface_method_value(callee.span()); } } @@ -10973,6 +14981,221 @@ impl<'a> TypeChecker<'a> { } } + fn graphics_callback_mode_only_mismatch( + &self, + function_name: &str, + parameter_index: usize, + expected: TypeId, + actual: TypeId, + ) -> bool { + // The graphics callback audit reports E0372 for ownership modes. Keep + // E0300 for an actual parameter or return-type mismatch. + if function_name != "graphics.run" || !matches!(parameter_index, 3 | 4) { + return false; + } + matches!( + (self.interner.resolve(expected), self.interner.resolve(actual)), + ( + Type::Function { + params: expected_params, + return_type: expected_return, + .. + }, + Type::Function { + params: actual_params, + return_type: actual_return, + .. + } + ) if expected_params == actual_params && expected_return == actual_return + ) + } + + fn function_value_view_modes(&self, callee: &Expr) -> Option> { + self.resolve + .resolutions + .get(&callee.span()) + .and_then(|definition| self.type_env.get(definition)) + .and_then(|callee_type| match self.interner.resolve(*callee_type) { + Type::Function { view_params, .. } => Some(view_params.clone()), + _ => None, + }) + } + + fn unparenthesized(mut expression: &Expr) -> &Expr { + while let Expr::Paren(inner, _) = expression { + expression = inner; + } + expression + } + + fn enum_constructor_parameter_names(&self, callee: &Expr) -> Option> { + let (type_name, variant) = match Self::unparenthesized(callee) { + Expr::EnumVariant(owner, variant, _) => { + (self.resolved_symbol_name(&owner.name, owner.span), variant) + } + Expr::FieldAccess(owner, variant, span) => { + let name = if let Expr::Ident(owner) = owner.as_ref() { + self.resolved_symbol_name(&owner.name, owner.span) + } else { + let name = Self::extract_dotted_name(owner)?; + self.resolved_or_expanded_name(&name, *span) + }; + (name, variant) + } + _ => return None, + }; + let enum_type = self.named_types.get(&type_name)?; + let Type::Enum(enum_id) = self.interner.resolve(*enum_type) else { + return None; + }; + self.interner + .resolve_enum(*enum_id) + .variants + .iter() + .find(|candidate| candidate.name == variant.name) + .map(|variant| { + variant + .fields + .iter() + .map(|(name, _)| name.clone()) + .collect() + }) + } + + fn reject_unbound_argument_names( + &mut self, + function_name: &str, + args: &[ast::CallArg], + ) -> bool { + let mut rejected = false; + for argument in args { + if let Some(name) = &argument.name { + self.sink.emit(errors::unknown_named_argument( + function_name, + &name.name, + name.span, + )); + rejected = true; + } + } + rejected + } + + fn is_explicit_view(expression: &Expr) -> bool { + match expression { + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + Self::is_explicit_view(inner) + } + Expr::View(_, _) => true, + // Clone acquires ownership, and FieldAccess reads a copied endpoint. + // Neither propagates an inner explicit View to this argument. + _ => false, + } + } + + fn check_argument_view(&mut self, view_parameter: bool, explicit_view: bool, span: Span) { + if explicit_view + && !view_parameter + && !self + .sink + .diagnostics() + .iter() + .any(|diagnostic| diagnostic.code.code() == 375 && diagnostic.span == span) + { + self.sink + .emit(errors::view_argument_requires_view_parameter(span)); + } + } + + fn check_owned_argument( + &mut self, + view_parameter: bool, + argument: &Expr, + expected: TypeId, + actual: TypeId, + ) { + if (self.ownership_context != CheckedOwnershipContext::Ordinary + && self.caller_observation_data(actual, &mut HashSet::new())) + || Self::is_explicit_view(argument) + || expected == TypeInterner::ERROR + || actual == TypeInterner::ERROR + || crate::ownership::is_implicitly_copyable(&self.interner, actual) + || !self.initializer_is_view(argument) + { + return; + } + if !self.diagnosed_caller_view_arguments.insert(argument.span()) { + return; + } + self.sink.emit(if view_parameter { + Diagnostic::error( + 401, + "cannot pass a borrowed value bare; write `view` to retain its owner", + argument.span(), + ) + } else { + crate::ownership::cannot_pass_view_to_owned_parameter(argument.span()) + }); + + let mut subject = argument; + while let Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) = subject + { + subject = inner; + } + if let Expr::Ident(ident) = subject { + self.diagnosed_owned_view_consumptions.insert( + ident.span, + crate::ownership::cannot_consume_view(&ident.name, ident.span), + ); + } + } + + fn check_pipeline_owned_argument( + &mut self, + view_parameter: bool, + source_index: usize, + piped_as_view: bool, + initial: Option<&Expr>, + extra_args: &[ast::CallArg], + expected: TypeId, + actual: TypeId, + ) { + let argument = if source_index == 0 { + // Explicit pipeline borrowing already has the E0375 gate. Only the + // first step has an original expression; later call results own + // their values and must not inherit the initial borrow provenance. + if piped_as_view { + return; + } + initial + } else { + Some(&extra_args[source_index - 1].value) + }; + if let Some(argument) = argument { + self.check_owned_argument(view_parameter, argument, expected, actual); + } + } + + fn check_pipeline_argument_view( + &mut self, + view_parameter: bool, + source_index: usize, + piped_as_view: bool, + extra_args: &[ast::CallArg], + span: Span, + ) { + let (explicit_view, argument_span) = if source_index == 0 { + (piped_as_view, span) + } else { + let argument = &extra_args[source_index - 1].value; + (Self::is_explicit_view(argument), argument.span()) + }; + self.check_argument_view(view_parameter, explicit_view, argument_span); + } + fn check_graphics_callback(&mut self, callback: &Expr, label: &str, views: &[bool]) { self.graphics_pending_callbacks.push(( callback.clone(), @@ -11068,8 +15291,8 @@ impl<'a> TypeChecker<'a> { arg: &ast::CallArg, ) { if function_name == "graphics.run" - && expected != TypeInterner::ERROR - && got == TypeInterner::ERROR + && !matches!(self.interner.resolve(expected), Type::Capability(_)) + && matches!(self.interner.resolve(got), Type::Capability(_)) { self.sink.emit(errors::graphics_contract( "graphics data and callbacks cannot be capability values", @@ -11083,6 +15306,7 @@ impl<'a> TypeChecker<'a> { function_name: &str, args: &[ast::CallArg], span: Span, + expected_return_type: Option, ) -> TypeId { let template = self .generic_function_templates @@ -11109,7 +15333,9 @@ impl<'a> TypeChecker<'a> { .iter() .map(|&source_index| self.check_expr(&args[source_index].value)) .collect::>(); - let Some(inferred) = self.infer_generic_signature(&template, &actual_types) else { + let Some(mut inferred) = + self.infer_generic_signature(&template, &actual_types, expected_return_type) + else { self.emit_cannot_infer_generic(function_name, &template, span); return TypeInterner::ERROR; }; @@ -11118,7 +15344,7 @@ impl<'a> TypeChecker<'a> { && inferred .concrete_args .iter() - .any(|&ty| ty == TypeInterner::ERROR) + .any(|&ty| matches!(self.interner.resolve(ty), Type::Capability(_) | Type::Error)) { self.sink .emit(errors::random_capability_element(function_name, span)); @@ -11126,11 +15352,30 @@ impl<'a> TypeChecker<'a> { } let mut arguments_match = true; - for (&source_index, &expected) in argument_order.iter().zip(&inferred.param_types) { + for (parameter_index, (&source_index, &expected)) in + argument_order.iter().zip(&inferred.param_types).enumerate() + { let arg = &args[source_index]; + self.check_argument_view( + template.params[parameter_index].view, + Self::is_explicit_view(&arg.value), + arg.value.span(), + ); let got = self.check_expr_for_expected(&arg.value, expected, false); + self.check_owned_argument( + template.params[parameter_index].view, + &arg.value, + expected, + got, + ); self.check_graphics_opaque_argument(function_name, expected, got, arg); - if !self.types_compatible(expected, got) { + if !self.graphics_callback_mode_only_mismatch( + function_name, + parameter_index, + expected, + got, + ) && !self.types_compatible(expected, got) + { arguments_match = false; self.sink.emit(errors::type_mismatch( &self.type_name(expected), @@ -11150,11 +15395,34 @@ impl<'a> TypeChecker<'a> { &inferred.param_types, &ordered_args, ); - self.record_generic_call_for_template(span, &template, &inferred.concrete_args); + let expressions = ordered_args + .iter() + .map(|arg| Some(&arg.value)) + .collect::>(); + let type_argument_reflections = self.reflection_arguments_for_inferred_call( + &template, + &inferred.concrete_args, + &actual_types, + &expressions, + ); + Self::apply_inferred_reflection_kinds( + &template, + &type_argument_reflections, + &mut inferred.kind_subst, + ); + self.record_generic_call_for_template( + span, + &template, + &inferred.concrete_args, + &inferred.kind_subst, + &type_argument_reflections, + ¶m_facts, + ); self.check_generic_function_instantiation( function_name, &template, &inferred.concrete_args, + type_argument_reflections, inferred.subst, inferred.kind_subst, param_facts, @@ -11167,6 +15435,7 @@ impl<'a> TypeChecker<'a> { &mut self, template: &FunctionDef, actual_types: &[TypeId], + expected_return_type: Option, ) -> Option { let type_params = template .type_params @@ -11177,6 +15446,19 @@ impl<'a> TypeChecker<'a> { for (param, &actual) in template.params.iter().zip(actual_types) { self.infer_type_params_from_type(¶m.ty, actual, &type_params, &mut subst); } + if let (Some(return_type), Some(expected)) = + (template.return_type.as_ref(), expected_return_type) + && template + .type_params + .iter() + .all(|param| subst.contains_key(¶m.name)) + { + // An empty argument may infer `never`. The result context can + // supply the concrete element type without replacing any type + // argument already established by a value-bearing argument. It + // cannot infer a previously unknown type argument by itself. + self.infer_type_params_from_type(return_type, expected, &type_params, &mut subst); + } let concrete_args = template .type_params .iter() @@ -11217,7 +15499,7 @@ impl<'a> TypeChecker<'a> { } fn infer_type_params_from_type( - &self, + &mut self, expected: &TypeExpr, actual: TypeId, type_params: &HashSet<&str>, @@ -11225,9 +15507,15 @@ impl<'a> TypeChecker<'a> { ) { match expected { TypeExpr::Named(ident) if type_params.contains(ident.name.as_str()) => { - match subst.get_mut(&ident.name) { - Some(inferred) if *inferred == TypeInterner::ERROR => *inferred = actual, - Some(_) => {} + match subst.get(&ident.name).copied() { + Some(TypeInterner::ERROR) => { + subst.insert(ident.name.clone(), actual); + } + Some(inferred) => { + if let Some(merged) = self.merge_inferred_never(inferred, actual, false) { + subst.insert(ident.name.clone(), merged); + } + } None => { subst.insert(ident.name.clone(), actual); } @@ -11237,20 +15525,20 @@ impl<'a> TypeChecker<'a> { self.infer_type_params_from_type(inner, actual, type_params, subst); } TypeExpr::Generic(owner, args, _) => { - match (owner.name.as_str(), self.interner.resolve(actual)) { + match (owner.name.as_str(), self.interner.resolve(actual).clone()) { ("list", Type::List(inner)) | ("set", Type::Set(inner)) | ("optional", Type::Optional(inner)) | ("secret", Type::Secret(inner)) if args.len() == 1 => { - self.infer_type_params_from_type(&args[0], *inner, type_params, subst); + self.infer_type_params_from_type(&args[0], inner, type_params, subst); } ("map", Type::Map(key, value)) | ("result", Type::Result(key, value)) if args.len() == 2 => { - self.infer_type_params_from_type(&args[0], *key, type_params, subst); - self.infer_type_params_from_type(&args[1], *value, type_params, subst); + self.infer_type_params_from_type(&args[0], key, type_params, subst); + self.infer_type_params_from_type(&args[1], value, type_params, subst); } _ => {} } @@ -11259,14 +15547,15 @@ impl<'a> TypeChecker<'a> { if let Type::Function { params: actual_params, return_type: actual_return, - } = self.interner.resolve(actual) + .. + } = self.interner.resolve(actual).clone() { - for (expected, &actual) in params.iter().zip(actual_params) { + for (expected, actual) in params.iter().zip(actual_params) { self.infer_type_params_from_type(expected, actual, type_params, subst); } self.infer_type_params_from_type( return_type, - *actual_return, + actual_return, type_params, subst, ); @@ -11299,7 +15588,37 @@ impl<'a> TypeChecker<'a> { )); } - fn check_field_access(&mut self, base: &Expr, field: &ast::Ident, span: Span) -> TypeId { + fn check_field_access( + &mut self, + base: &Expr, + field: &ast::Ident, + span: Span, + projection: ResourceProjectionMode, + ) -> TypeId { + let owner_span = if self.resolve.resolutions.contains_key(&base.span()) { + base.span() + } else { + span + }; + if let Some(owner) = Self::extract_dotted_name(base) + && let owner = self.resolved_or_expanded_name(&owner, owner_span) + && self.generic_struct_templates.contains_key(&owner) + && self + .resolve + .resolutions + .get(&owner_span) + .is_none_or(|definition| { + matches!( + self.resolve.scope_table.def(*definition).kind, + DefKind::Struct | DefKind::Namespace + ) + }) + { + // A generic declaration is not a concrete method owner. Reuse the + // missing-type-arguments diagnostic instead of letting an untyped + // namespace path escape into the backend as an error type. + return self.monomorphize_struct(&owner, &[], base.span()); + } if !span.file.is_stdlib() && let Some(prefix) = Self::extract_dotted_name(base) && self.resolved_or_expanded_name(&format!("{prefix}.{}", field.name), span) @@ -11339,9 +15658,12 @@ impl<'a> TypeChecker<'a> { return method_ty; } } - if let Some(type_id) = self.named_types.get(&base_ident.name).copied() { + let owner_name = self.resolved_symbol_name(&base_ident.name, owner_span); + if let Some(type_id) = self.named_types.get(&owner_name).copied() { if matches!(self.interner.resolve(type_id), Type::Enum(_)) { - return self.check_enum_variant(base_ident, field, &[], span); + // Compiler-defined reflection enums have a named type but + // their type name is not a runtime value in `type_env`. + return self.check_enum_variant_by_type(type_id, field, &[], span); } if let Some(method_ty) = self.check_interface_method(type_id, field, span) { return method_ty; @@ -11369,12 +15691,13 @@ impl<'a> TypeChecker<'a> { } } - let base_ty = self.check_expr(base); + let base_ty = + self.check_expr_with_resource_projection(base, ResourceProjectionMode::Borrowed); if base_ty == TypeInterner::ERROR { return TypeInterner::ERROR; } - match self.interner.resolve(base_ty) { + let field_ty = match self.interner.resolve(base_ty) { Type::Secret(inner) => match self.interner.resolve(*inner) { Type::Struct(sid) => { let struct_def = self.interner.resolve_struct(*sid); @@ -11484,7 +15807,11 @@ impl<'a> TypeChecker<'a> { )); TypeInterner::ERROR } + }; + if projection == ResourceProjectionMode::OwningCopy { + self.check_resource_copy(field_ty, span); } + field_ty } fn machine_state_field_type( @@ -11552,15 +15879,25 @@ impl<'a> TypeChecker<'a> { interface_def .methods .iter() - .find(|m| m.name == field.name) - .cloned(), + .enumerate() + .find(|(_, method)| method.name == field.name) + .map(|(index, method)| (index, method.clone())), ) }; - if let Some(method) = method { + if let Some((method_index, method)) = method { + self.record_method_value( + span, + CheckedMethodValue::Interface { + interface_type: type_id, + method_index, + }, + ); let params = method.params.iter().map(|(_, ty, _)| *ty).collect(); + let view_params = method.params.iter().map(|(_, _, view)| *view).collect(); return Some(self.interner.intern(Type::Function { params, + view_params, return_type: method.return_type, })); } @@ -11587,9 +15924,12 @@ impl<'a> TypeChecker<'a> { .find(|m| m.name == field.name) .cloned() { + self.record_source_method_value(type_id, &field.name, span); let params = method.params.iter().map(|(_, ty, _)| *ty).collect(); + let view_params = method.params.iter().map(|(_, _, view)| *view).collect(); return Some(self.interner.intern(Type::Function { params, + view_params, return_type: method.return_type, })); } @@ -11600,6 +15940,7 @@ impl<'a> TypeChecker<'a> { "to_bytes" => { return Some(self.interner.intern(Type::Function { params: vec![type_id], + view_params: vec![false], return_type: TypeInterner::BYTES, })); } @@ -11609,6 +15950,7 @@ impl<'a> TypeChecker<'a> { .intern(Type::Result(type_id, TypeInterner::STRING)); return Some(self.interner.intern(Type::Function { params: vec![TypeInterner::BYTES], + view_params: vec![false], return_type: result_ty, })); } @@ -11622,9 +15964,12 @@ impl<'a> TypeChecker<'a> { .and_then(|methods| methods.get(&field.name)) .cloned() { + self.record_source_method_value(type_id, &field.name, span); let params = method.params.iter().map(|(_, ty, _)| *ty).collect(); + let view_params = method.params.iter().map(|(_, _, view)| *view).collect(); return Some(self.interner.intern(Type::Function { params, + view_params, return_type: method.return_type, })); } @@ -11687,8 +16032,9 @@ impl<'a> TypeChecker<'a> { return enum_ty; } - let params = variant_def.fields.iter().map(|(_, ty)| *ty).collect(); + let params: Vec<_> = variant_def.fields.iter().map(|(_, ty)| *ty).collect(); return self.interner.intern(Type::Function { + view_params: vec![false; params.len()], params, return_type: enum_ty, }); @@ -11731,6 +16077,16 @@ impl<'a> TypeChecker<'a> { span: Span, ) -> TypeId { let struct_def = self.interner.resolve_struct(sid).clone(); + if self.interner.is_compiler_metadata_struct(sid) { + self.sink.emit(errors::reflection_metadata_constructor( + &struct_def.name, + span, + )); + for arg in args { + self.check_expr(&arg.value); + } + return TypeInterner::ERROR; + } let mut assigned = vec![false; struct_def.fields.len()]; let mut source_indices = vec![None; struct_def.fields.len()]; let mut arguments_valid = true; @@ -11790,6 +16146,7 @@ impl<'a> TypeChecker<'a> { } else { self.check_expr_for_expected(&arg.value, expected_ty, false) }; + self.check_owned_resource_payload(&arg.value, arg_ty); if self.is_refinement_type(expected_ty) && self.can_refine_from(arg_ty, expected_ty) { continue; } @@ -11994,6 +16351,7 @@ impl<'a> TypeChecker<'a> { for (arg, (field_name, expected_ty)) in payload_args.iter().zip(state_def.fields.iter()) { let arg_ty = self.check_expr_for_expected(&arg.value, *expected_ty, false); + self.check_owned_resource_payload(&arg.value, arg_ty); if !self.types_compatible(*expected_ty, arg_ty) { self.sink.emit(errors::argument_type_mismatch( field_name, @@ -12052,6 +16410,15 @@ impl<'a> TypeChecker<'a> { } }; + if source_state.is_some() && self.type_contains_resource_data(source_ty) { + self.check_argument_view( + false, + Self::is_explicit_view(&args[0].value), + args[0].value.span(), + ); + self.check_owned_argument(false, &args[0].value, source_ty, source_ty); + } + let target_ident = match &args[1].value { Expr::Ident(ident) if args[1].name.is_none() => ident, _ => { @@ -12116,6 +16483,7 @@ impl<'a> TypeChecker<'a> { for (arg, (field_name, expected_ty)) in payload_args.iter().zip(target_def.fields.iter()) { let arg_ty = self.check_expr_for_expected(&arg.value, *expected_ty, false); + self.check_owned_resource_payload(&arg.value, arg_ty); if !self.types_compatible(*expected_ty, arg_ty) { self.sink.emit(errors::argument_type_mismatch( field_name, @@ -12209,17 +16577,24 @@ impl<'a> TypeChecker<'a> { fn check_list_construct(&mut self, elems: &[Expr]) -> TypeId { if elems.is_empty() { - // Empty list: list[] since we can't infer the element type. - return self.interner.intern(Type::List(TypeInterner::ERROR)); + // An empty collection has no element value. Keep that fact typed + // as an uninhabited element until an expected type supplies one. + return self.interner.intern(Type::List(TypeInterner::NEVER)); } let first_ty = self.check_expr(&elems[0]); - let (element_ty, mut tainted) = self.strip_secret_type(first_ty); + self.check_owned_resource_payload(&elems[0], first_ty); + let (mut element_ty, mut tainted) = self.strip_secret_type(first_ty); for elem in &elems[1..] { let elem_ty = self.check_expr(elem); + self.check_owned_resource_payload(elem, elem_ty); let (elem_base_ty, elem_secret) = self.strip_secret_type(elem_ty); - if !self.types_compatible(element_ty, elem_base_ty) - && !self.types_compatible(elem_base_ty, element_ty) + if let Some(merged) = self.merge_inferred_never(element_ty, elem_base_ty, false) { + element_ty = merged; + } else if !self.types_compatible(element_ty, elem_base_ty) + && (self.contains_inferred_never(element_ty) + || self.contains_inferred_never(elem_base_ty) + || !self.types_compatible(elem_base_ty, element_ty)) { self.sink.emit(errors::type_mismatch( &self.type_name(element_ty), @@ -12247,6 +16622,7 @@ impl<'a> TypeChecker<'a> { expected_element_ty, allow_refinement_handle, ); + self.check_owned_resource_payload(elem, elem_ty); if !self.types_compatible(expected_element_ty, elem_ty) { self.sink.emit(errors::type_mismatch( &self.type_name(expected_element_ty), @@ -12263,19 +16639,24 @@ impl<'a> TypeChecker<'a> { if entries.is_empty() { return self .interner - .intern(Type::Map(TypeInterner::ERROR, TypeInterner::ERROR)); + .intern(Type::Map(TypeInterner::NEVER, TypeInterner::NEVER)); } let first_key_ty = self.check_expr(&entries[0].0); let first_value_ty = self.check_expr(&entries[0].1); - let (key_ty, mut key_tainted) = self.strip_secret_type(first_key_ty); - let (value_ty, mut value_tainted) = self.strip_secret_type(first_value_ty); + self.check_owned_resource_payload(&entries[0].1, first_value_ty); + let (mut key_ty, mut key_tainted) = self.strip_secret_type(first_key_ty); + let (mut value_ty, mut value_tainted) = self.strip_secret_type(first_value_ty); for (key_expr, value_expr) in &entries[1..] { let entry_key_ty = self.check_expr(key_expr); let (entry_key_base_ty, entry_key_secret) = self.strip_secret_type(entry_key_ty); - if !self.types_compatible(key_ty, entry_key_base_ty) - && !self.types_compatible(entry_key_base_ty, key_ty) + if let Some(merged) = self.merge_inferred_never(key_ty, entry_key_base_ty, false) { + key_ty = merged; + } else if !self.types_compatible(key_ty, entry_key_base_ty) + && (self.contains_inferred_never(key_ty) + || self.contains_inferred_never(entry_key_base_ty) + || !self.types_compatible(entry_key_base_ty, key_ty)) { self.sink.emit(errors::type_mismatch( &self.type_name(key_ty), @@ -12286,9 +16667,14 @@ impl<'a> TypeChecker<'a> { key_tainted |= entry_key_secret; let entry_value_ty = self.check_expr(value_expr); + self.check_owned_resource_payload(value_expr, entry_value_ty); let (entry_value_base_ty, entry_value_secret) = self.strip_secret_type(entry_value_ty); - if !self.types_compatible(value_ty, entry_value_base_ty) - && !self.types_compatible(entry_value_base_ty, value_ty) + if let Some(merged) = self.merge_inferred_never(value_ty, entry_value_base_ty, false) { + value_ty = merged; + } else if !self.types_compatible(value_ty, entry_value_base_ty) + && (self.contains_inferred_never(value_ty) + || self.contains_inferred_never(entry_value_base_ty) + || !self.types_compatible(entry_value_base_ty, value_ty)) { self.sink.emit(errors::type_mismatch( &self.type_name(value_ty), @@ -12331,6 +16717,7 @@ impl<'a> TypeChecker<'a> { expected_value_ty, allow_refinement_handle, ); + self.check_owned_resource_payload(value_expr, value_ty); if !self.types_compatible(expected_value_ty, value_ty) { self.sink.emit(errors::type_mismatch( &self.type_name(expected_value_ty), @@ -12368,6 +16755,7 @@ impl<'a> TypeChecker<'a> { expected_payload_ty, allow_refinement_handle, ); + self.check_owned_resource_payload(payload, payload_ty); if !self.types_compatible(expected_payload_ty, payload_ty) { self.sink.emit(errors::type_mismatch( &self.type_name(expected_payload_ty), @@ -12411,6 +16799,8 @@ impl<'a> TypeChecker<'a> { if let Some(name) = bind_name { if let Some(def_id) = self.declaration_def_id(name.span) { self.type_env.insert(def_id, err_ty); + self.record_expression_type(name.span, err_ty); + self.record_assignment_binding(name, false, false); self.record_closure_local(def_id); } } @@ -12453,6 +16843,7 @@ impl<'a> TypeChecker<'a> { match last_stmt { Stmt::Return(_) => {} + Stmt::Respond(_) if self.current_respond_type.is_some() => {} Stmt::Expr(expr_stmt) => { if let Expr::Default(default_value, _) = &expr_stmt.expr { if success_ty != TypeInterner::ERROR { @@ -12461,7 +16852,7 @@ impl<'a> TypeChecker<'a> { .get(&expr_stmt.expr.span()) .copied() .unwrap_or(TypeInterner::ERROR); - if !self.types_compatible(success_ty, default_ty) { + if success_ty != default_ty { default_ty = self.check_expr_for_expected(default_value, success_ty, false); self.record_expression_type(expr_stmt.expr.span(), default_ty); @@ -12504,13 +16895,19 @@ impl<'a> TypeChecker<'a> { self.resolve_type_expr(inner) } TypeExpr::Function(param_types, return_type, _span) => { + self.check_return_view_annotation(return_type); let params = param_types .iter() .map(|t| self.resolve_type_expr(t)) .collect(); + let view_params = param_types + .iter() + .map(|parameter| matches!(parameter, TypeExpr::View(_, _))) + .collect(); let ret = self.resolve_type_expr(return_type); self.interner.intern(Type::Function { params, + view_params, return_type: ret, }) } @@ -12576,13 +16973,14 @@ impl<'a> TypeChecker<'a> { _ if self.type_aliases.contains_key(&lookup_name) => { self.resolve_type_alias(&lookup_name, span) } - // Capability types are recognised but opaque — no further type - // checking is performed on values of these types. - _ if capability::is_capability_type(name) => TypeInterner::ERROR, - _ => { - self.sink.emit(errors::unknown_type(name, span)); - TypeInterner::ERROR - } + // Capabilities are opaque values, but retain exact nominal identity. + _ => match CapabilityKind::from_name(name) { + Some(kind) => TypeInterner::capability(kind), + None => { + self.sink.emit(errors::unknown_type(name, span)); + TypeInterner::ERROR + } + }, } } @@ -12782,6 +17180,17 @@ impl<'a> TypeChecker<'a> { methods: Vec::new(), }); let ty = self.interner.intern(Type::Struct(sid)); + if self + .interner + .register_nominal_type_arguments(ty, type_args.to_vec()) + .is_err() + { + self.sink.emit(errors::unknown_type( + "invalid canonical nominal type arguments", + span, + )); + return TypeInterner::ERROR; + } self.monomorphized_structs.insert(cache_key, ty); // Build substitution map: type param name → concrete TypeId. @@ -13085,13 +17494,6 @@ fn comptime_type_variants_binding(expr: &Expr) -> Option<&TypeExpr> { type_args.first() } -fn direct_reflected_loop_owner_type(expr: &Expr) -> Option<&TypeExpr> { - comptime_type_fields_binding(expr) - .or_else(|| comptime_type_variants_binding(expr)) - .or_else(|| comptime_type_machine_states_binding(expr)) - .or_else(|| comptime_type_machine_fields_binding(expr)) -} - fn comptime_type_variant_value_binding(expr: &Expr) -> Option<&TypeExpr> { let Expr::GenericCall(callee, type_args, args, _) = expr else { return None; @@ -13327,6 +17729,7 @@ mod tests { scope_table: self.scope_table, resolutions: self.resolutions, namespace_aliases: HashMap::new(), + resource_kernels: jett_resolve::ResolvedResourceKernels::default(), diagnostics: Vec::new(), } } @@ -13375,429 +17778,4651 @@ mod tests { .collect() } - #[test] - fn inferred_generic_calls_record_concrete_direct_and_pipeline_types() { - let source = r#"function first[T](view items: list[T]) returns optional[T]: - return none -function direct() returns optional[int64]: - list[int64] items = list(1) - return first(items) -function piped() returns optional[int64]: - list[int64] items = list(2) - return items - into first() + const NATIVE_JSON_QUALIFIED_STATE_SCHEMA: &str = r#"namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +type Cached = Session at cached +struct Snapshot: + current: Ready + history: list[Ready] "#; - let result = check_source_result(source); - let errors = result - .diagnostics - .iter() - .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) - .collect::>(); - assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); - let direct_text = "first(items)"; - let direct_start = source.find(direct_text).expect("direct call should exist") as u32; - let direct_span = Span::new( - FileId::new(0), - direct_start, - direct_start + direct_text.len() as u32, + #[test] + fn native_json_qualified_state_selectors_follow_the_public_payload_boundary() { + let parsed = parse(NATIVE_JSON_QUALIFIED_STATE_SCHEMA, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolve = jett_resolve::resolve(&parsed.module); + assert!( + resolve + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + resolve.diagnostics ); - let pipeline_text = "items\n into first()"; - let pipeline_start = source - .rfind(pipeline_text) - .expect("pipeline expression should exist") as u32; - let pipeline_span = Span::new( - FileId::new(0), - pipeline_start, - pipeline_start + pipeline_text.len() as u32, + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + checker.check_module(&parsed.module); + assert!( + checker + .sink + .diagnostics() + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checker.sink.diagnostics() + ); + let ready = checker.named_types["app.Ready"]; + let cached = checker.named_types["app.Cached"]; + let bare = checker.named_types["app.Session"]; + let snapshot = checker.named_types["app.Snapshot"]; + let ready_list = checker.interner.intern(Type::List(ready)); + let ready_optional = checker.interner.intern(Type::Optional(ready)); + let ready_map = checker + .interner + .intern(Type::Map(TypeInterner::STRING, ready)); + let ready_result = checker + .interner + .intern(Type::Result(ready, TypeInterner::STRING)); + let bare_list = checker.interner.intern(Type::List(bare)); + for (ty, expected) in [ + (ready, true), + (snapshot, true), + (ready_list, true), + (ready_optional, true), + (ready_map, true), + (ready_result, true), + (cached, false), + (bare, false), + (bare_list, false), + ] { + assert_eq!( + checker.native_json_source_supported(ty, &mut HashSet::new()), + expected, + "serializer for {}", + checker.type_name(ty) + ); + assert_eq!( + checker.native_json_parse_source_supported(ty), + expected, + "parser for {}", + checker.type_name(ty) + ); + } + assert!(checker.json_non_string_map_key_types(ready).is_empty()); + assert!(checker.json_unsupported_parse_types(ready).is_empty()); + assert!(checker.json_unsupported_serialize_types(ready).is_empty()); + assert_eq!(checker.json_non_string_map_key_types(bare), ["int64"]); + assert_eq!(checker.json_non_string_map_key_types(cached), ["int64"]); + assert_eq!( + checker + .interner + .resolve_machine(match checker.interner.resolve(bare) { + Type::Machine(machine) => *machine, + _ => panic!("checked machine owner"), + }) + .states + .len(), + 2, + "qualification does not filter declared machine metadata" ); + } - for span in [direct_span, pipeline_span] { - let ty = result.type_map[&span]; + #[test] + fn native_json_serializers_keep_inferred_uninhabited_slots_in_source_targets() { + for serializer in ["serialize", "serialize_public"] { + // These minimal trusted declarations isolate source-facade facts. + // Linked driver gates must exercise the actual reflected bodies. + let source = format!( + r#"namespace json +export function serialize[T](view value: T) returns string: + return "" +export function serialize_public[T](view value: T) returns string: + return "" +namespace app +function direct[T](view values: list[T]) returns string: + return json.{serializer}[list[T]](view values) +function piped[T](view values: list[T]) returns string: + return view values into json.{serializer}[list[T]]() +function main() returns nothing: + string inferred = direct(view list()) + string pipeline = piped(view list()) + string concrete = direct[int64](view list()) + return nothing +"# + ); + let parsed = parse(&source, FileId::new(STDLIB_FILE_ID_START)); assert!( - matches!( - result.interner.resolve(ty), - Type::Optional(inner) if *inner == TypeInterner::INT64 - ), - "expected optional[int64], got {:?}", - result.interner.resolve(ty) + parsed.errors.is_empty(), + "{serializer}: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{serializer}: {:?}", + resolved.diagnostics + ); + let checked = check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{serializer}: {:?}", + checked.diagnostics + ); + let mut targets = Vec::new(); + for instance in &checked.generic_function_instantiations { + let name = resolved.scope_table.def(instance.definition).name.as_str(); + if !matches!(name, "app.direct" | "app.piped") { + continue; + } + let [element] = instance.concrete_args.as_slice() else { + panic!("one inferred or declared element argument"); + }; + assert!(matches!( + *element, + TypeInterner::NEVER | TypeInterner::INT64 + )); + let calls = instance + .generic_calls + .values() + .filter(|call| { + resolved.scope_table.def(call.definition).name + == format!("json.{serializer}") + }) + .collect::>(); + let [call] = calls.as_slice() else { + panic!("{name}[{element:?}] selects exactly one checked JSON source facade"); + }; + let [target] = call.concrete_args.as_slice() else { + panic!("one exact JSON target"); + }; + assert!( + matches!(checked.interner.resolve(*target), Type::List(inner) if inner == element) + ); + assert_eq!(instance.parameter_types, [*target]); + assert_eq!(instance.return_type, TypeInterner::STRING); + assert!(instance.intrinsic_ids.values().any(|id| { + *id == if serializer == "serialize" { + IntrinsicId::JsonSerialize + } else { + IntrinsicId::JsonSerializePublic + } + })); + targets.push((name, *element)); + } + assert_eq!(targets.len(), 3, "{serializer}: {targets:?}"); + assert!(targets.contains(&("app.direct", TypeInterner::NEVER))); + assert!(targets.contains(&("app.piped", TypeInterner::NEVER))); + assert!(targets.contains(&("app.direct", TypeInterner::INT64))); + + let unsupported = format!( + "namespace app\nfunction encode(view values: list[function() returns int64]) returns string:\n return json.{serializer}[list[function() returns int64]](view values)\n" + ); + let errors = check_source_errors(&unsupported); + assert!( + errors.iter().any(|error| error.code.code() == 347), + "{errors:?}" + ); + + // An empty iteration still cannot fabricate an inhabited Never. + let invalid_body = format!( + "namespace app\nfunction encode[T](view values: list[T]) returns string:\n for item in view values:\n T fabricated = 7\n return json.{serializer}[list[T]](view values)\nfunction main() returns nothing:\n string outcome = encode(view list())\n return nothing\n" + ); + let errors = check_source_errors(&invalid_body); + assert!( + errors.iter().any(|error| error.message.contains("")), + "empty source bodies remain checked: {errors:?}" ); } } - // --------------------------------------------------------------- - // Test: simple function with parameters and return type - // --------------------------------------------------------------- + #[test] + fn qualified_machine_state_loops_check_all_declared_field_bodies_and_aliases() { + let source = r#"namespace app +type Count = int64 +type Entries = map[int64, int64] +type BackupCount = int64 +machine Session: + states: + empty + ready(count: Count) + cached(entries: Entries, backup: BackupCount) + transitions: + empty to ready + ready to cached +type Ready = Session at ready +type Empty = Session at empty +function all_states() returns nothing: + for state in type.machine_states[Session at ready](): + for field in state.fields: + comptime type All = field.type_info: + string name = type.name[All]() + return nothing +function all_empty_target_states() returns nothing: + for state in type.machine_states[Session at empty](): + for field in state.fields: + comptime type AllEmpty = field.type_info: + string name = type.name[AllEmpty]() + return nothing +function selected(view value: Ready) returns nothing: + for field in type.machine_state_value[Session at ready](view value).fields: + comptime type Selected = field.type_info: + string name = type.name[Selected]() + return nothing +function selected_empty(view value: Empty) returns nothing: + for field in type.machine_state_value[Session at empty](view value).fields: + comptime type SelectedEmpty = field.type_info: + string name = type.name[SelectedEmpty]() + return nothing +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + let bindings_for = |name: &str| { + let prefix = format!("comptime type {name} ="); + result + .comptime_type_bindings + .iter() + .find_map(|(span, bindings)| { + source[span.start as usize..span.end as usize] + .starts_with(&prefix) + .then_some(bindings) + }) + .expect("checked source binding") + }; + for name in ["All", "AllEmpty"] { + let bindings = bindings_for(name); + assert_eq!(bindings.len(), 3, "{name}"); + assert_eq!( + bindings + .iter() + .map(|binding| binding.reflection.type_name.as_str()) + .collect::>(), + ["app.Count", "app.Entries", "app.BackupCount"], + "{name}: source aliases follow declared field order" + ); + assert_eq!(bindings[0].bound_type, TypeInterner::INT64); + assert!(matches!( + result.interner.resolve(bindings[1].bound_type), + Type::Map(key, value) + if *key == TypeInterner::INT64 && *value == TypeInterner::INT64 + )); + assert_eq!(bindings[2].bound_type, TypeInterner::INT64); + for (index, binding) in bindings.iter().enumerate() { + assert_eq!( + binding.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + assert_eq!( + binding + .body + .intrinsic_type_arguments + .values() + .collect::>(), + vec![&vec![binding.bound_type]], + "{name}: each alias has its own checked type.name body" + ); + } + } + let selected = bindings_for("Selected"); + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].reflection.type_name, "app.Count"); + assert_eq!(selected[0].bound_type, TypeInterner::INT64); + assert!(bindings_for("SelectedEmpty").is_empty()); + } #[test] - fn simple_function_params_and_return() { - // function add(a: int64, b: int64) returns int64: - // return a + b - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - let ref_a_span = sp(10, 11); - let ref_b_span = sp(12, 13); - let binop_span = sp(10, 13); - let ret_span = sp(8, 13); - let body_span = sp(8, 14); - let func_span = sp(0, 14); + fn native_json_parsers_keep_inferred_uninhabited_source_targets() { + for parser in ["parse", "parse_exact"] { + for (shape, params, carrier, seed, expected_args) in [ + ("list", "T", "list[T]", "list()", vec![TypeInterner::NEVER]), + ( + "optional", + "T", + "optional[T]", + "none", + vec![TypeInterner::NEVER], + ), + ( + "missing_error", + "Value, Failure", + "result[Value, Failure]", + "ok(7)", + vec![TypeInterner::INT64, TypeInterner::NEVER], + ), + ( + "missing_success", + "Value, Failure", + "result[Value, Failure]", + "fail(\"seed\")", + vec![TypeInterner::NEVER, TypeInterner::STRING], + ), + ] { + // Trusted facades isolate checked target identity; the + // actual stdlib/link gates must prove occupied Never decoding fails. + let source = format!( + r#"namespace json +export function parse[T](raw: string) returns result[T, string]: + return fail(raw) +export function parse_exact[T](raw: string) returns result[T, string]: + return fail(raw) +namespace app +function direct[{params}](view shape: {carrier}, raw: string) returns string: + {carrier} payload = json.{parser}[{carrier}](raw) handle error: + return error + return "selected" +function piped[{params}](view shape: {carrier}, raw: string) returns string: + {carrier} payload = raw into json.{parser}[{carrier}]() handle error: + return error + return "selected" +function main() returns nothing: + string direct_value = direct(view {seed}, "null") + string pipeline_value = piped(view {seed}, "null") + return nothing +"# + ); + let parsed = parse(&source, FileId::new(STDLIB_FILE_ID_START)); + assert!( + parsed.errors.is_empty(), + "{parser}, {shape}: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved.diagnostics.iter().all(|diagnostic| { + diagnostic.severity != jett_diagnostics::Severity::Error + }), + "{parser}, {shape}: {:?}", + resolved.diagnostics + ); + let checked = check(&parsed.module, &resolved); + assert!( + checked.diagnostics.iter().all(|diagnostic| { + diagnostic.severity != jett_diagnostics::Severity::Error + }), + "{parser}, {shape}: {:?}", + checked.diagnostics + ); + let mut names = Vec::new(); + for instance in &checked.generic_function_instantiations { + let name = resolved.scope_table.def(instance.definition).name.as_str(); + if !matches!(name, "app.direct" | "app.piped") { + continue; + } + assert_eq!( + instance.concrete_args, expected_args, + "{parser}, {shape}, {name}" + ); + let calls = instance + .generic_calls + .values() + .filter(|call| { + resolved.scope_table.def(call.definition).name + == format!("json.{parser}") + }) + .collect::>(); + let [call] = calls.as_slice() else { + panic!("one exact parser source target: {parser}, {shape}, {name}"); + }; + let [target] = call.concrete_args.as_slice() else { + panic!("one parser type argument"); + }; + let children = match checked.interner.resolve(*target) { + Type::List(inner) | Type::Optional(inner) => vec![*inner], + Type::Result(ok, error) => vec![*ok, *error], + _ => panic!("exact inferred carrier target"), + }; + assert_eq!(children, expected_args, "{parser}, {shape}, {name}"); + assert_eq!(instance.parameter_types, [*target, TypeInterner::STRING]); + assert_eq!(instance.return_type, TypeInterner::STRING); + assert!(instance.intrinsic_ids.values().any(|id| { + *id == if parser == "parse" { + IntrinsicId::JsonParse + } else { + IntrinsicId::JsonParseExact + } + })); + names.push(name); + } + names.sort_unstable(); + assert_eq!(names, ["app.direct", "app.piped"], "{parser}, {shape}"); + } + } + } - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("add", fn_name_span); - let a_def_id = env.def_param("a", param_a_span); - let b_def_id = env.def_param("b", param_b_span); - env.reference(ref_a_span, a_def_id); - env.reference(ref_b_span, b_def_id); - // Also reference fn name for self-registration - env.reference(fn_name_span, fn_def_id); + #[test] + fn native_json_parser_pipelines_keep_checked_source_targets_and_owned_inputs() { + for parser in ["parse", "parse_exact"] { + // Minimal trusted facades isolate checker target selection. Actual + // stdlib execution is covered by the linked driver fixtures. + let source = format!( + r#"namespace json +export function parse[T](raw: string) returns result[T, string]: + return fail(raw) +export function parse_exact[T](raw: string) returns result[T, string]: + return fail(raw) +namespace app +machine Session: + states: + ready(count: int64) + cached(entries: map[int64, int64]) + transitions: + ready to cached +type Ready = Session at ready +function direct(raw: string) returns result[Ready, string]: + return json.{parser}[Ready](raw) +function piped(raw: string) returns result[Ready, string]: + return raw into json.{parser}[Ready]() +function primitive(raw: string) returns result[int64, string]: + return raw into json.{parser}[int64]() +function nested(raw: string) returns result[list[Ready], string]: + return raw into json.{parser}[list[Ready]]() +function preserve(raw: string) returns string: + Ready value = clone raw into json.{parser}[Ready]() handle error: + return raw + return raw +"# + ); + let file_id = FileId::new(STDLIB_FILE_ID_START); + let parsed = parse(&source, file_id); + assert!(parsed.errors.is_empty(), "{parser}: {:?}", parsed.errors); + let resolve = jett_resolve::resolve(&parsed.module); + assert!( + resolve + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{parser}: {:?}", + resolve.diagnostics + ); + let checked = check(&parsed.module, &resolve); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{parser}: {:?}", + checked.diagnostics + ); + let call_span = |name: &str| { + let function = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == name => Some(function), + _ => None, + }) + .expect("source function"); + let Stmt::Return(returned) = &function.body.stmts[0] else { + panic!("source function returns its checked call"); + }; + match returned.value.as_ref().expect("returned expression") { + Expr::GenericCall(_, _, _, span) => *span, + Expr::Pipeline(_, steps, _) => { + assert_eq!(steps.len(), 1); + steps[0].span + } + _ => panic!("direct or pipeline JSON call"), + } + }; + let direct = &checked.generic_calls[&call_span("direct")]; + let piped = &checked.generic_calls[&call_span("piped")]; + assert_eq!(piped, direct, "same concrete source target for {parser}"); + assert_eq!( + resolve.scope_table.def(piped.definition).name, + format!("json.{parser}") + ); + let primitive = &checked.generic_calls[&call_span("primitive")]; + assert_eq!(primitive.definition, direct.definition); + assert_eq!(primitive.concrete_args, [TypeInterner::INT64]); + let nested = &checked.generic_calls[&call_span("nested")]; + assert_eq!(nested.definition, direct.definition); + assert!(matches!( + checked.interner.resolve(nested.concrete_args[0]), + Type::List(inner) if direct.concrete_args == [*inner] + )); + for function in ["piped", "primitive", "nested"] { + let span = call_span(function); + assert_eq!( + checked.intrinsic_ids[&span], + if parser == "parse" { + IntrinsicId::JsonParse + } else { + IntrinsicId::JsonParseExact + } + ); + } + } + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("add", fn_name_span), - type_params: vec![], - params: vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("int64", sp(106, 111))), - span: param_b_span, - }, - ], - return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), - body: Block { - stmts: vec![Stmt::Return(ReturnStmt { - value: Some(Expr::Binary( - Box::new(Expr::Ident(ident("a", ref_a_span))), - BinOp::Add, - Box::new(Expr::Ident(ident("b", ref_b_span))), - binop_span, - )), - span: ret_span, - })], - span: body_span, - }, - exported: false, - span: func_span, - })], - span: sp(0, 14), + fn binding_mode_named( + modes: &HashMap, + source: &str, + name: &str, + ) -> (Span, CheckedBindingMode) { + let mut matching = modes + .iter() + .filter(|(span, _)| &source[span.start as usize..span.end as usize] == name); + let (&span, &mode) = matching.next().expect("binding mode should be recorded"); + assert!( + matching.next().is_none(), + "binding name must be unique in this body" + ); + (span, mode) + } + + #[test] + fn checked_binding_modes_borrowed_sum_projections_keep_exact_original_sources() { + let source = r#"function inspect(view optional_source: optional[list[int64]], view result_source: result[list[int64], string]) returns nothing: + list[int64] optional_alias = view((view optional_source) handle: + return nothing + ) + list[int64] result_alias = view((view result_source) handle error: + return nothing + ) + return nothing +"#; + let parsed = parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + let checked = check(&parsed.module, &resolved); + assert!( + resolved + .diagnostics + .iter() + .chain(&checked.diagnostics) + .all(|error| { error.severity != jett_diagnostics::Severity::Error }), + "{:?}: {:?}", + resolved.diagnostics, + checked.diagnostics + ); + for (alias, backing) in [ + ("optional_alias", "optional_source"), + ("result_alias", "result_source"), + ] { + let (_, mode) = binding_mode_named(&checked.binding_modes, source, alias); + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(definition), + } = mode + else { + panic!("expected stable borrowed sum projection: {mode:?}"); + }; + assert_eq!(resolved.scope_table.def(definition).name, backing); + assert_eq!(resolved.scope_table.def(definition).kind, DefKind::Param); + } + } + + #[test] + fn checked_binding_modes_keep_immediate_sources_and_owned_copies() { + let source = r#"struct Holder: + items: list[int64] +function temporary_holder() returns Holder: + return Holder(items: list(1)) +function inspect(view values: list[int64], view holder: Holder) returns nothing: + list[int64] borrowed = view values + list[int64] forwarded = ((borrowed)) + list[int64] copied = clone forwarded + list[int64] projected = holder.items + list[int64] projected_view = view holder.items + list[int64] temporary = view list(1) + list[int64] temporary_projection = view temporary_holder().items + return nothing +"#; + let parsed = parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + resolved.diagnostics + ); + let checked = check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + assert_eq!(checked.binding_modes.len(), 7); + let (borrowed_span, borrowed) = + binding_mode_named(&checked.binding_modes, source, "borrowed"); + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(parameter), + } = borrowed + else { + panic!("expected a direct binding view, got {borrowed:?}"); + }; + assert_eq!(resolved.scope_table.def(parameter).name, "values"); + assert_eq!(resolved.scope_table.def(parameter).kind, DefKind::Param); + let (_, forwarded) = binding_mode_named(&checked.binding_modes, source, "forwarded"); + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(immediate_source), + } = forwarded + else { + panic!("expected a forwarded binding view, got {forwarded:?}"); + }; + assert_eq!( + resolved.scope_table.def(immediate_source).span, + borrowed_span + ); + assert_ne!(immediate_source, parameter); + for name in ["copied", "projected"] { + assert_eq!( + binding_mode_named(&checked.binding_modes, source, name).1, + CheckedBindingMode::Owned, + "{name}" + ); + } + let CheckedBindingMode::View { + source: CheckedViewSource::Binding(projected_root), + } = binding_mode_named(&checked.binding_modes, source, "projected_view").1 + else { + panic!("expected a stable projected binding view"); }; + assert_eq!(resolved.scope_table.def(projected_root).name, "holder"); + assert_eq!( + resolved.scope_table.def(projected_root).kind, + DefKind::Param + ); + for name in ["temporary", "temporary_projection"] { + assert_eq!( + binding_mode_named(&checked.binding_modes, source, name).1, + CheckedBindingMode::View { + source: CheckedViewSource::Other + }, + "{name}" + ); + } + } + + #[test] + fn projected_local_view_aliases_remain_ineligible_for_closure_capture() { + let errors = check_source_errors( + r#"struct Packet: + data: list[int64] +function inspect(view source: Packet) returns nothing: + list[int64] borrowed = view source.data + function() returns list[int64] callback = function() returns list[int64]: return clone borrowed + return nothing +"#, + ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 402); + assert!(errors[0].message.contains("borrowed")); + } + + #[test] + fn checked_binding_modes_use_actual_copy_types_and_transparent_sources() { + for (parameter, output, initializer, owned) in [ + ("int64", "int64", "view source", true), + ("string", "string", "view source", true), + ("Count", "int64", "coarsen (view source)", true), + ("secret[int64]", "int64", "declassify source", true), + ("Numbers", "list[int64]", "coarsen source", false), + ("Numbers", "list[int64]", "coarsen (view source)", false), + ("Numbers", "list[int64]", "coarsen clone source", true), + ( + "secret[list[int64]]", + "list[int64]", + "declassify (view source)", + false, + ), + ( + "secret[list[int64]]", + "list[int64]", + "declassify clone source", + true, + ), + ] { + let source = format!( + "type Count = int64 where true\ntype Numbers = list[int64] where true\nfunction inspect(view source: {parameter}) returns nothing:\n {output} local = {initializer}\n return nothing\n" + ); + let checked = check_source_result(&source); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{source}\n{:?}", + checked.diagnostics + ); + let (_, mode) = binding_mode_named(&checked.binding_modes, &source, "local"); + if owned { + assert_eq!(mode, CheckedBindingMode::Owned, "{source}"); + } else { + assert!( + matches!( + mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(_) + } + ), + "{source}\n{mode:?}" + ); + } + } + } + + #[test] + fn checked_binding_modes_are_distinct_for_generic_copy_and_borrow_bodies() { + let source = r#"function inspect[T](view source: T) returns nothing: + T borrowed = view source + T forwarded = borrowed + T copied = clone forwarded + return nothing +function main() returns nothing: + int64 number = 7 + string text = "seven" + list[int64] values = list(7) + inspect[int64](view number) + inspect[list[int64]](view values) + inspect[string](view text) + return nothing +"#; + let checked = check_source_result(source); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + checked.diagnostics + ); + assert_eq!(checked.generic_function_instantiations.len(), 3); + assert_eq!(checked.binding_modes.len(), 3); + let mut borrowed_count = 0; + for body in &checked.generic_function_instantiations { + assert_eq!(body.binding_modes.len(), 3); + let copies = + crate::ownership::is_implicitly_copyable(&checked.interner, body.concrete_args[0]); + for name in ["borrowed", "forwarded"] { + let (span, mode) = binding_mode_named(&body.binding_modes, source, name); + assert_eq!(body.type_map[&span], body.concrete_args[0]); + assert_eq!(mode == CheckedBindingMode::Owned, copies); + if !copies { + assert!(matches!( + mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(_) + } + )); + borrowed_count += 1; + } + assert!(!checked.binding_modes.contains_key(&span)); + } + assert_eq!( + binding_mode_named(&body.binding_modes, source, "copied").1, + CheckedBindingMode::Owned + ); + } + assert_eq!(borrowed_count, 2); + } + + #[test] + fn checked_binding_modes_survive_recursive_reflected_body_snapshots() { + for (generic, owner, arguments) in [("", "Sample", ""), ("[T]", "T", "[Sample]")] { + let source = format!( + r#"struct Sample: + count: int64 + items: list[int64] +function inspect{generic}(view value: {owner}) returns nothing: + for field in type.fields[{owner}](): + comptime type Field = field.type_info: + Field original = type.field_value[{owner}, Field](view value, view field) + Field borrowed = view original + comptime type Again = type.info[Field](): + Again forwarded = borrowed + return nothing +function main() returns nothing: + Sample sample = Sample(count: 7, items: list(7)) + inspect{arguments}(view sample) + return nothing +"# + ); + let checked = check_source_result(&source); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{source}\n{:?}", + checked.diagnostics + ); + let bodies = if generic.is_empty() { + &checked.comptime_type_bindings + } else { + &checked.generic_function_instantiations[0].comptime_type_bindings + }; + let expansions = bodies + .values() + .find(|expansions| expansions.len() == 2) + .expect("each reflected field must retain a concrete body"); + for expansion in expansions { + let copies = crate::ownership::is_implicitly_copyable( + &checked.interner, + expansion.bound_type, + ); + let (span, mode) = + binding_mode_named(&expansion.body.binding_modes, &source, "borrowed"); + assert_eq!(expansion.body.type_map[&span], expansion.bound_type); + assert_eq!(mode == CheckedBindingMode::Owned, copies); + assert_eq!( + binding_mode_named(&expansion.body.binding_modes, &source, "original").1, + CheckedBindingMode::Owned + ); + let nested = expansion + .body + .comptime_type_bindings + .values() + .next() + .expect("nested comptime expansion must survive its parent snapshot"); + assert_eq!(nested.len(), 1); + assert_eq!(nested[0].bound_type, expansion.bound_type); + let (nested_span, nested_mode) = + binding_mode_named(&nested[0].body.binding_modes, &source, "forwarded"); + assert_eq!(nested[0].body.type_map[&nested_span], expansion.bound_type); + assert_eq!(nested_mode == CheckedBindingMode::Owned, copies); + assert!(!nested[0].body.binding_modes.contains_key(&span)); + } + } + } + + #[test] + fn generic_body_checks_do_not_capture_the_callers_closure_scope() { + let source = r#"namespace test +function duplicate[T](view value: T) returns T: + return clone value +function make() returns function(view list[int64]) returns list[int64]: + return function(view items: list[int64]) returns list[int64]: return duplicate[list[int64]](view items) +"#; + assert!(check_source_errors(source).is_empty()); + let captured = r#"namespace test +function duplicate[T](view value: T) returns T: + return clone value +function make() returns function(view list[int64]) returns list[int64]: + list[int64] outer = list(1) + return function(view items: list[int64]) returns list[int64]: + list[int64] copied = duplicate[list[int64]](view items) + return clone outer +"#; + let errors = check_source_errors(captured); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 402); + assert!(errors[0].message.contains("outer")); + } + + #[test] + fn function_expression_calls_enforce_capability_purity() { + for call in [ + "(effect)(view authority.stdout)", + "(callback)(view authority.stdout)", + "factory()(view authority.stdout)", + "holder.callback(view authority.stdout)", + "(holder.callback)(view authority.stdout)", + "authority.stdout into view callback", + ] { + let source = format!( + r#"namespace test +struct Authority: + stdout: Stdout +struct Holder: + callback: function(view Stdout) returns int64 +function effect(view stdout: Stdout) returns int64: + return 1 +function factory() returns function(view Stdout) returns int64: + return effect +function invoke(view authority: Authority, callback: function(view Stdout) returns int64, view holder: Holder) returns int64: + return {call} +"# + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 500, "{call}: {errors:?}"); + let authorized = source.replace( + "function invoke(view authority: Authority,", + "function invoke(view stdout: Stdout, view authority: Authority,", + ); + assert!(check_source_errors(&authorized).is_empty(), "{call}"); + let comptime = authorized.replace( + &format!("return {call}"), + &format!("return comptime ({call})"), + ); + let errors = check_source_errors(&comptime); + assert!( + errors.iter().any(|error| error.code.code() == 504), + "{call}: {errors:?}" + ); + } + } + + #[test] + fn function_expression_calls_preserve_lifted_secret_return_types() { + for call in [ + "factory()(value)", + "holder.callback(value)", + "(holder.callback)(value)", + "make_holder().callback(value)", + "value into (factory())", + "(function(input: int64) returns int64: return input + 1)(value)", + ] { + let source = format!( + r#"namespace test +struct Holder: + callback: function(int64) returns int64 +function increment(input: int64) returns int64: + return input + 1 +function factory() returns function(int64) returns int64: + return increment +function make_holder() returns Holder: + return Holder(callback: increment) +function main() returns nothing: + secret[int64] value = 4 + Holder holder = make_holder() + secret[int64] transformed = {call} + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{call}: {errors:?}"); + let start = source.rfind(call).unwrap() as u32; + let span = Span::new(FileId::new(0), start, start + call.len() as u32); + assert!( + matches!(result.interner.resolve(result.type_map[&span]), Type::Secret(inner) if *inner == TypeInterner::INT64), + "{call}" + ); + let public = source.replace("secret[int64] transformed", "int64 transformed"); + let errors = check_source_errors(&public); + assert!( + errors.iter().any(|error| error.code.code() == 311), + "{call}: {errors:?}" + ); + } + } + + #[test] + fn inline_function_bodies_obey_their_own_capability_signature() { + let prefix = r#"namespace test +struct Authority: + stdout: Stdout +function effect(view stdout: Stdout) returns int64: + return 1 +"#; + for body in [ + "return effect(view argument.stdout)", + "return (effect)(view argument.stdout)", + ] { + let source = format!( + "{prefix}function make() returns function(view Authority) returns int64:\n return function(view argument: Authority) returns int64: {body}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}: {errors:?}"); + assert_eq!(errors[0].code.code(), 500); + assert!(errors[0].message.contains("")); + } + + for comptime in ["", "comptime "] { + let closure = format!( + "{comptime}function(view stdout: Stdout) returns int64: return effect(view stdout)" + ); + for declaration in [ + format!( + "function make() returns function(view Stdout) returns int64:\n return {closure}\n" + ), + format!( + "verify creation:\n function(view Stdout) returns int64 callback = {closure}\n assert true\n" + ), + ] { + let source = format!("{prefix}{declaration}"); + let errors = check_source_errors(&source); + assert!(errors.is_empty(), "{source}: {errors:?}"); + } + } + } + + #[test] + fn inline_function_invocation_restores_the_surrounding_effect_context() { + let source = r#"namespace test +struct Authority: + stdout: Stdout +function effect(view stdout: Stdout) returns int64: + return 1 +function invoke(view authority: Authority) returns int64: + return (function(view stdout: Stdout) returns int64: return effect(view stdout))(view authority.stdout) +"#; + let errors = check_source_errors(source); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 500); + assert!(errors[0].message.contains("invoke"), "{errors:?}"); + assert!(!errors[0].message.contains("")); + let authorized = source.replace( + "function invoke(view authority", + "function invoke(view permission: Stdout, view authority", + ); + assert!(check_source_errors(&authorized).is_empty()); + let comptime = authorized.replace("return (function", "return comptime (function"); + let errors = check_source_errors(&comptime); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 504); + let verify = r#"namespace test +function effect(view stdout: Stdout) returns int64: + return 1 +verify invocation: + function(view Stdout) returns int64 callback = function(view stdout: Stdout) returns int64: return effect(view stdout) + callback() +"#; + let errors = check_source_errors(verify); + assert!( + errors.iter().any(|error| error.code.code() == 501), + "{errors:?}" + ); + } + + #[test] + fn callback_binding_names_do_not_inherit_namespace_declaration_policy() { + for call in ["effect(4)", "(effect)(4)", "4 into effect"] { + let source = format!( + "namespace test\nfunction effect(view stdout: Stdout) returns int64:\n return 1\nfunction invoke(effect: function(int64) returns int64) returns int64:\n return {call}\n" + ); + let errors = check_source_errors(&source); + assert!(errors.is_empty(), "{source}: {errors:?}"); + } + } + + #[test] + fn parenthesized_declarations_preserve_named_argument_bindings() { + let source = r#"namespace test +function subtract(left: int64, right: int64) returns int64: + return left - right +function main() returns int64: + return (subtract)(right: 3, left: 10) +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + let call_offset = source.find("(subtract)(right:").unwrap() as u32; + let order = result + .call_argument_orders + .iter() + .find_map(|(span, order)| (span.start == call_offset).then_some(&order.source_indices)); + assert_eq!(order, Some(&vec![1, 0])); + } + + #[test] + fn function_expression_calls_reject_unbound_named_arguments() { + for call in [ + "callback(right: 3, left: 10)", + "(callback)(right: 3, left: 10)", + "factory()(right: 3, left: 10)", + "holder.callback(unknown: 3, unknown: 10)", + "(function(left: int64, right: int64) returns int64: return left - right)(right: 3, left: 10)", + ] { + let source = format!( + r#"namespace test +struct Holder: + callback: function(int64, int64) returns int64 +function subtract(left: int64, right: int64) returns int64: + return left - right +function factory() returns function(int64, int64) returns int64: + return subtract +function invoke(callback: function(int64, int64) returns int64, view holder: Holder) returns int64: + return {call} +"# + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 2, "{call}: {errors:?}"); + assert!( + errors.iter().all(|error| error.code.code() == 368), + "{call}: {errors:?}" + ); + } + } + + #[test] + fn enum_constructor_arguments_preserve_declared_payload_names() { + for (prefix, owner, setup, imports) in [ + ("", "Choice", "", ""), + ("namespace test\n", "Choice", "", ""), + ( + "namespace models\n", + "models.Choice", + "namespace app\n", + " use models\n", + ), + ( + "namespace models\n", + "m.Choice", + "namespace app\n", + " use models as m\n", + ), + ] { + for parenthesized in [false, true] { + let callee = format!("{owner}.item"); + let callee = if parenthesized { + format!("({callee})") + } else { + callee + }; + let source = format!( + "{prefix}export enum Choice:\n item(first: int64, second: string, third: bool)\n{setup}function make() returns nothing:\n{imports} {owner} value = {callee}(third: true, first: 4, second: \"ok\")\n return nothing\n" + ); + let result = check_source_result(&source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{source}: {:?}", + result.diagnostics + ); + assert!( + result + .call_argument_orders + .values() + .any(|order| order.source_indices == vec![1, 2, 0]), + "{source}: {:?}", + result.call_argument_orders + ); + } + } + } + + #[test] + fn enum_constructor_arguments_reject_unknown_and_duplicate_payload_names() { + for callee in ["Choice.item", "(Choice.item)"] { + for (arguments, expected_code) in + [("first: 4, other: 5", 368), ("first: 4, first: 5", 369)] + { + let source = format!( + "namespace test\nenum Choice:\n item(first: int64, second: int64)\nfunction make() returns Choice:\n return {callee}({arguments})\n" + ); + let errors = check_source_errors(&source); + assert!( + errors + .iter() + .any(|error| error.code.code() == expected_code), + "{source}: {errors:?}" + ); + } + } + } + + #[test] + fn explicit_comptime_preserves_contextual_sum_types() { + let source = r#"namespace test +function main() returns nothing: + result[int64, string] success = comptime ok(13) + result[int64, string] failure = comptime fail("bad") + optional[int64] absent = comptime none + return nothing +"#; + let checked = check_source_result(source); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + checked.diagnostics + ); + for (text, expected) in [ + ("comptime ok(13)", "result"), + ("comptime fail(\"bad\")", "result"), + ("comptime none", "optional"), + ] { + let start = source.find(text).expect("checked expression") as u32; + let span = Span::new(FileId::new(0), start, start + text.len() as u32); + let ty = checked.type_map[&span]; + match (expected, checked.interner.resolve(ty)) { + ("result", Type::Result(ok, error)) => { + assert_eq!(*ok, TypeInterner::INT64); + assert_eq!(*error, TypeInterner::STRING); + } + ("optional", Type::Optional(inner)) => { + assert_eq!(*inner, TypeInterner::INT64); + } + (_, actual) => panic!("unexpected contextual type: {actual:?}"), + } + } + } + + #[test] + fn checked_builtin_calls_record_one_closed_intrinsic_identity() { + let source = r#"function intrinsic_calls(value: int64) returns int64: + println("audit") + for item in range(0): + println("{item}") + return value into math.abs + +function reflected_name[T]() returns string: + return type.name[T]() + +function main() returns string: + return reflected_name[int64]() +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + + let recorded = result + .intrinsic_ids + .values() + .chain( + result + .generic_function_instantiations + .iter() + .flat_map(|instantiation| instantiation.intrinsic_ids.values()), + ) + .copied() + .collect::>(); + assert_eq!( + recorded, + HashSet::from([ + IntrinsicId::MathAbs, + IntrinsicId::Println, + IntrinsicId::Range, + IntrinsicId::TypeName, + ]) + ); + for intrinsic in recorded { + assert_eq!( + IntrinsicId::from_canonical_name(intrinsic.canonical_name()), + Some(intrinsic) + ); + assert_eq!( + IntrinsicId::ALL + .iter() + .filter(|candidate| candidate.canonical_name() == intrinsic.canonical_name()) + .count(), + 1 + ); + } + } + + #[test] + fn compiler_reflection_enum_variants_have_concrete_expression_types() { + let source = r#"function matches(kind: TypeKind, primitive: TypePrimitive) returns bool: + return kind == TypeKind.alias_type and primitive == TypePrimitive.int8_type +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); + + for expected_name in ["TypeKind", "TypePrimitive"] { + let expression = if expected_name == "TypeKind" { + "TypeKind.alias_type" + } else { + "TypePrimitive.int8_type" + }; + let start = source.find(expression).unwrap() as u32; + let span = Span::new(FileId::new(0), start, start + expression.len() as u32); + let ty = *result + .type_map + .get(&span) + .expect("reflection enum variant should be typed"); + let Type::Enum(enum_id) = *result.interner.resolve(ty) else { + panic!("reflection enum variant should have an enum type"); + }; + assert_eq!(result.interner.resolve_enum(enum_id).name, expected_name); + } + } + + #[test] + fn inferred_generic_calls_record_concrete_direct_and_pipeline_types() { + let source = r#"function first[T](view items: list[T]) returns optional[T]: + return none +function direct() returns optional[int64]: + list[int64] items = list(1) + return first(items) +function piped() returns optional[int64]: + list[int64] items = list(2) + return items + into first() +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + + let direct_text = "first(items)"; + let direct_start = source.find(direct_text).expect("direct call should exist") as u32; + let direct_span = Span::new( + FileId::new(0), + direct_start, + direct_start + direct_text.len() as u32, + ); + let pipeline_text = "items\n into first()"; + let pipeline_start = source + .rfind(pipeline_text) + .expect("pipeline expression should exist") as u32; + let pipeline_span = Span::new( + FileId::new(0), + pipeline_start, + pipeline_start + pipeline_text.len() as u32, + ); + + for span in [direct_span, pipeline_span] { + let ty = result.type_map[&span]; + assert!( + matches!( + result.interner.resolve(ty), + Type::Optional(inner) if *inner == TypeInterner::INT64 + ), + "expected optional[int64], got {:?}", + result.interner.resolve(ty) + ); + } + } + + // --------------------------------------------------------------- + // Test: simple function with parameters and return type + // --------------------------------------------------------------- + + #[test] + fn simple_function_params_and_return() { + // function add(a: int64, b: int64) returns int64: + // return a + b + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + let ref_a_span = sp(10, 11); + let ref_b_span = sp(12, 13); + let binop_span = sp(10, 13); + let ret_span = sp(8, 13); + let body_span = sp(8, 14); + let func_span = sp(0, 14); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("add", fn_name_span); + let a_def_id = env.def_param("a", param_a_span); + let b_def_id = env.def_param("b", param_b_span); + env.reference(ref_a_span, a_def_id); + env.reference(ref_b_span, b_def_id); + // Also reference fn name for self-registration + env.reference(fn_name_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("add", fn_name_span), + type_params: vec![], + params: vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("int64", sp(106, 111))), + span: param_b_span, + }, + ], + return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), + body: Block { + stmts: vec![Stmt::Return(ReturnStmt { + value: Some(Expr::Binary( + Box::new(Expr::Ident(ident("a", ref_a_span))), + BinOp::Add, + Box::new(Expr::Ident(ident("b", ref_b_span))), + binop_span, + )), + span: ret_span, + })], + span: body_span, + }, + exported: false, + span: func_span, + })], + span: sp(0, 14), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + // No errors expected. + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + + // The binary expression should be typed as int64. + assert_eq!(result.type_map[&binop_span], TypeInterner::INT64); + } + + // --------------------------------------------------------------- + // Test: type mismatch error (int64 + string) + // --------------------------------------------------------------- + + #[test] + fn type_mismatch_int_plus_string() { + // a: int64, b: string → a + b should emit an error + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + let ref_a_span = sp(10, 11); + let ref_b_span = sp(12, 13); + let binop_span = sp(10, 13); + let body_span = sp(8, 14); + let func_span = sp(0, 14); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("bad", fn_name_span); + let a_def_id = env.def_param("a", param_a_span); + let b_def_id = env.def_param("b", param_b_span); + env.reference(ref_a_span, a_def_id); + env.reference(ref_b_span, b_def_id); + env.reference(fn_name_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("bad", fn_name_span), + type_params: vec![], + params: vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("string", sp(106, 112))), + span: param_b_span, + }, + ], + return_type: Some(TypeExpr::Named(ident("nothing", sp(113, 120)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Binary( + Box::new(Expr::Ident(ident("a", ref_a_span))), + BinOp::Add, + Box::new(Expr::Ident(ident("b", ref_b_span))), + binop_span, + ), + span: binop_span, + })], + span: body_span, + }, + exported: false, + span: func_span, + })], + span: sp(0, 14), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 301); // binary_op_mismatch + assert!(errors[0].message.contains("int64")); + assert!(errors[0].message.contains("string")); + } + + // --------------------------------------------------------------- + // Test: binary operator type checking (arithmetic, comparison, logic) + // --------------------------------------------------------------- + + #[test] + fn binary_operators_arithmetic_returns_same_type() { + // 10 + 20 → int64 + let binop_span = sp(0, 5); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Binary( + Box::new(Expr::IntLiteral(10, sp(0, 2))), + BinOp::Add, + Box::new(Expr::IntLiteral(20, sp(3, 5))), + binop_span, + ), + span: binop_span, + })], + span: sp(0, 5), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + assert!( + result + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "unexpected errors: {:?}", + result.diagnostics + ); + assert_eq!(result.type_map[&binop_span], TypeInterner::INT64); + } + + #[test] + fn binary_operators_comparison_returns_bool() { + // 10 < 20 → bool + let binop_span = sp(0, 5); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Binary( + Box::new(Expr::IntLiteral(10, sp(0, 2))), + BinOp::Lt, + Box::new(Expr::IntLiteral(20, sp(3, 5))), + binop_span, + ), + span: binop_span, + })], + span: sp(0, 5), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + assert!( + result + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "unexpected errors: {:?}", + result.diagnostics + ); + assert_eq!(result.type_map[&binop_span], TypeInterner::BOOL); + } + + #[test] + fn binary_operators_logic_requires_bool() { + // true && false → bool (ok) + let binop_span = sp(0, 10); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Binary( + Box::new(Expr::BoolLiteral(true, sp(0, 4))), + BinOp::And, + Box::new(Expr::BoolLiteral(false, sp(5, 10))), + binop_span, + ), + span: binop_span, + })], + span: sp(0, 10), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + assert!( + result + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "unexpected errors: {:?}", + result.diagnostics + ); + assert_eq!(result.type_map[&binop_span], TypeInterner::BOOL); + } + + #[test] + fn binary_operators_logic_error_on_non_bool() { + // 42 && true → error + let binop_span = sp(0, 10); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Binary( + Box::new(Expr::IntLiteral(42, sp(0, 2))), + BinOp::And, + Box::new(Expr::BoolLiteral(true, sp(5, 10))), + binop_span, + ), + span: binop_span, + })], + span: sp(0, 10), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].code.code(), 301); + } + + // --------------------------------------------------------------- + // Test: variable declaration type matching + // --------------------------------------------------------------- + + #[test] + fn var_decl_type_match_ok() { + // int64 x = 42 + let var_name_span = sp(6, 7); + let var_span = sp(0, 10); + + let mut env = TestEnv::new(); + env.def_var("x", var_name_span); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::VarDecl(VarDecl { + mutable: false, + ty: TypeExpr::Named(ident("int64", sp(0, 5))), + name: ident("x", var_name_span), + value: Expr::IntLiteral(42, sp(8, 10)), + span: var_span, + })], + span: sp(0, 10), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn var_decl_type_mismatch() { + // string x = 42 → error E0311 + let var_name_span = sp(7, 8); + let var_span = sp(0, 12); + + let mut env = TestEnv::new(); + env.def_var("x", var_name_span); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::VarDecl(VarDecl { + mutable: false, + ty: TypeExpr::Named(ident("string", sp(0, 6))), + name: ident("x", var_name_span), + value: Expr::IntLiteral(42, sp(9, 11)), + span: var_span, + })], + span: sp(0, 12), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 311); + assert!(errors[0].message.contains("string")); + assert!(errors[0].message.contains("int64")); + } + + // --------------------------------------------------------------- + // Test: function call argument type checking + // --------------------------------------------------------------- + + #[test] + fn function_call_correct_args() { + // function add(a: int64, b: int64) returns int64 + // add(1, 2) → no error, result is int64 + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + let call_ref_span = sp(20, 23); + let call_span = sp(20, 30); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("add", fn_name_span); + env.def_param("a", param_a_span); + env.def_param("b", param_b_span); + env.reference(call_ref_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("add", fn_name_span), + type_params: vec![], + params: vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("int64", sp(106, 111))), + span: param_b_span, + }, + ], + return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("add", call_ref_span))), + vec![ + CallArg { + name: None, + value: Expr::IntLiteral(1, sp(24, 25)), + span: sp(24, 25), + }, + CallArg { + name: None, + value: Expr::IntLiteral(2, sp(27, 28)), + span: sp(27, 28), + }, + ], + call_span, + ), + span: call_span, + })], + span: sp(20, 30), + }, + exported: false, + span: sp(0, 30), + })], + span: sp(0, 30), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert_eq!(result.type_map[&call_span], TypeInterner::INT64); + } + + #[test] + fn function_call_wrong_arg_type() { + // function add(a: int64, b: int64) returns int64 + // add(1, "hello") → error E0304 + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + let call_ref_span = sp(20, 23); + let call_span = sp(20, 35); + let bad_arg_span = sp(27, 34); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("add", fn_name_span); + env.def_param("a", param_a_span); + env.def_param("b", param_b_span); + env.reference(call_ref_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("add", fn_name_span), + type_params: vec![], + params: vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("int64", sp(106, 111))), + span: param_b_span, + }, + ], + return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("add", call_ref_span))), + vec![ + CallArg { + name: None, + value: Expr::IntLiteral(1, sp(24, 25)), + span: sp(24, 25), + }, + CallArg { + name: None, + value: Expr::StringLiteral("hello".to_string(), bad_arg_span), + span: bad_arg_span, + }, + ], + call_span, + ), + span: call_span, + })], + span: sp(20, 35), + }, + exported: false, + span: sp(0, 35), + })], + span: sp(0, 35), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 304); // argument_type_mismatch + assert!(errors[0].message.contains("int64")); + assert!(errors[0].message.contains("string")); + } + + #[test] + fn function_call_wrong_arg_count() { + // function add(a: int64, b: int64) returns int64 + // add(1) → error E0303 + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + let call_ref_span = sp(20, 23); + let call_span = sp(20, 28); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("add", fn_name_span); + env.def_param("a", param_a_span); + env.def_param("b", param_b_span); + env.reference(call_ref_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("add", fn_name_span), + type_params: vec![], + params: vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("int64", sp(106, 111))), + span: param_b_span, + }, + ], + return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("add", call_ref_span))), + vec![CallArg { + name: None, + value: Expr::IntLiteral(1, sp(24, 25)), + span: sp(24, 25), + }], + call_span, + ), + span: call_span, + })], + span: sp(20, 28), + }, + exported: false, + span: sp(0, 28), + })], + span: sp(0, 28), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 303); // argument_count_mismatch + } + + // --------------------------------------------------------------- + // Test: if condition must be bool + // --------------------------------------------------------------- + + #[test] + fn if_condition_must_be_bool() { + // if 42: → error E0306 + let cond_span = sp(3, 5); + let if_span = sp(0, 10); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::If(IfStmt { + condition: Expr::IntLiteral(42, cond_span), + then_block: Block { + stmts: vec![], + span: sp(6, 10), + }, + else_ifs: vec![], + else_block: None, + span: if_span, + })], + span: sp(0, 10), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 306); // condition_not_bool + assert!(errors[0].message.contains("int64")); + } + + #[test] + fn if_condition_bool_ok() { + // if true: → no error + let cond_span = sp(3, 7); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::If(IfStmt { + condition: Expr::BoolLiteral(true, cond_span), + then_block: Block { + stmts: vec![], + span: sp(8, 10), + }, + else_ifs: vec![], + else_block: None, + span: sp(0, 10), + })], + span: sp(0, 10), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + // --------------------------------------------------------------- + // Test: return type checking + // --------------------------------------------------------------- + + #[test] + fn return_type_mismatch() { + // function foo() returns int64: + // return "hello" + // → error E0305 + let fn_name_span = sp(0, 3); + let ret_span = sp(10, 25); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("foo", fn_name_span); + env.reference(fn_name_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("foo", fn_name_span), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("int64", sp(100, 105)))), + body: Block { + stmts: vec![Stmt::Return(ReturnStmt { + value: Some(Expr::StringLiteral("hello".to_string(), sp(17, 24))), + span: ret_span, + })], + span: sp(10, 25), + }, + exported: false, + span: sp(0, 25), + })], + span: sp(0, 25), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 305); // return_type_mismatch + assert!(errors[0].message.contains("int64")); + assert!(errors[0].message.contains("string")); + } + + #[test] + fn return_type_correct() { + // function foo() returns int64: + // return 42 + let fn_name_span = sp(0, 3); + + let mut env = TestEnv::new(); + let fn_def_id = env.def_func("foo", fn_name_span); + env.reference(fn_name_span, fn_def_id); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("foo", fn_name_span), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("int64", sp(100, 105)))), + body: Block { + stmts: vec![Stmt::Return(ReturnStmt { + value: Some(Expr::IntLiteral(42, sp(17, 19))), + span: sp(10, 19), + })], + span: sp(10, 19), + }, + exported: false, + span: sp(0, 19), + })], + span: sp(0, 19), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + // --------------------------------------------------------------- + // Test: unary operators + // --------------------------------------------------------------- + + #[test] + fn unary_not_requires_bool() { + // not 42 → error + let unary_span = sp(0, 6); + + let env = TestEnv::new(); + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Unary( + UnaryOp::Not, + Box::new(Expr::IntLiteral(42, sp(4, 6))), + unary_span, + ), + span: unary_span, + })], + span: sp(0, 6), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].code.code(), 302); // unary_op_mismatch + } + + // --------------------------------------------------------------- + // Test: for loop iterable check + // --------------------------------------------------------------- + + #[test] + fn for_loop_requires_list() { + // for x in 42: → error E0307 + let var_span = sp(4, 5); + let iterable_span = sp(9, 11); + let for_span = sp(0, 15); + + let mut env = TestEnv::new(); + env.def_var("x", var_span); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![Stmt::For(ForStmt { + variable: ident("x", var_span), + value_variable: None, + view: false, + iterable: Expr::IntLiteral(42, iterable_span), + body: Block { + stmts: vec![], + span: sp(12, 15), + }, + span: for_span, + })], + span: sp(0, 15), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 307); + } + + // --------------------------------------------------------------- + // Test: assignment type mismatch + // --------------------------------------------------------------- + + #[test] + fn assignment_type_mismatch() { + // int64 x = 42 + // x = "hello" → error E0312 + let var_name_span = sp(6, 7); + let var_span = sp(0, 10); + let ref_x_span = sp(15, 16); + let assign_span = sp(15, 26); + + let mut env = TestEnv::new(); + let x_def = env.def_var("x", var_name_span); + env.reference(ref_x_span, x_def); + + let module = Module { + items: vec![Item::Function(FunctionDef { + name: ident("test", sp(50, 54)), + type_params: vec![], + params: vec![], + return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), + body: Block { + stmts: vec![ + Stmt::VarDecl(VarDecl { + mutable: true, + ty: TypeExpr::Named(ident("int64", sp(0, 5))), + name: ident("x", var_name_span), + value: Expr::IntLiteral(42, sp(8, 10)), + span: var_span, + }), + Stmt::Assign(AssignStmt { + target: Expr::Ident(ident("x", ref_x_span)), + value: Expr::StringLiteral("hello".to_string(), sp(19, 26)), + span: assign_span, + }), + ], + span: sp(0, 26), + }, + exported: false, + span: sp(50, 62), + })], + span: sp(0, 62), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); + assert_eq!(errors[0].code.code(), 312); // assign_type_mismatch + } + + // --------------------------------------------------------------- + // Test: resolve_type_expr for generic types + // --------------------------------------------------------------- + + #[test] + fn resolve_generic_types() { + // Verify that type expressions like list[int64], result[string, int64] resolve correctly. + let env = TestEnv::new(); + let resolve = env.into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + + let list_int = checker.resolve_type_expr(&TypeExpr::Generic( + ident("list", sp(0, 4)), + vec![TypeExpr::Named(ident("int64", sp(5, 10)))], + sp(0, 11), + )); + assert_eq!( + *checker.interner.resolve(list_int), + Type::List(TypeInterner::INT64) + ); + + let result_type = checker.resolve_type_expr(&TypeExpr::Generic( + ident("result", sp(0, 6)), + vec![ + TypeExpr::Named(ident("string", sp(7, 13))), + TypeExpr::Named(ident("int64", sp(15, 20))), + ], + sp(0, 21), + )); + assert_eq!( + *checker.interner.resolve(result_type), + Type::Result(TypeInterner::STRING, TypeInterner::INT64) + ); + } + + // --------------------------------------------------------------- + // Test: capability-based purity enforcement + // --------------------------------------------------------------- + + /// Helper: create a minimal function definition. + fn make_function(name: &str, name_span: Span, params: Vec, body: Block) -> FunctionDef { + FunctionDef { + name: ident(name, name_span), + type_params: vec![], + params, + return_type: Some(TypeExpr::Named(ident("nothing", sp(200, 207)))), + body, + exported: false, + span: Span::new(name_span.file, name_span.start, name_span.start + 50), + } + } + + #[test] + fn pure_function_calling_pure_function_ok() { + // function helper() returns nothing: + // return nothing + // function caller() returns nothing: + // helper() + let helper_name_span = sp(0, 6); + let caller_name_span = sp(100, 106); + let call_ref_span = sp(110, 116); + let call_span = sp(110, 118); + + let mut env = TestEnv::new(); + let helper_def = env.def_func("helper", helper_name_span); + let _caller_def = env.def_func("caller", caller_name_span); + env.reference(helper_name_span, helper_def); + env.reference(caller_name_span, _caller_def); + env.reference(call_ref_span, helper_def); + + let module = Module { + items: vec![ + Item::Function(make_function( + "helper", + helper_name_span, + vec![], + Block { + stmts: vec![], + span: sp(10, 20), + }, + )), + Item::Function(make_function( + "caller", + caller_name_span, + vec![], + Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("helper", call_ref_span))), + vec![], + call_span, + ), + span: call_span, + })], + span: sp(107, 120), + }, + )), + ], + span: sp(0, 200), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn pure_function_calling_impure_function_error() { + // function writer(view out: Stdout) returns nothing: + // return nothing + // function caller() returns nothing: + // writer() ← E0500 + let writer_name_span = sp(0, 6); + let writer_param_span = sp(7, 10); + let caller_name_span = sp(100, 106); + let call_ref_span = sp(110, 116); + let call_span = sp(110, 118); + + let mut env = TestEnv::new(); + let writer_def = env.def_func("writer", writer_name_span); + let _caller_def = env.def_func("caller", caller_name_span); + env.def_param("out", writer_param_span); + env.reference(writer_name_span, writer_def); + env.reference(caller_name_span, _caller_def); + env.reference(call_ref_span, writer_def); + + let module = Module { + items: vec![ + Item::Function(make_function( + "writer", + writer_name_span, + vec![Param { + view: true, + mutable: false, + name: ident("out", writer_param_span), + ty: TypeExpr::View( + Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), + sp(7, 17), + ), + span: writer_param_span, + }], + Block { + stmts: vec![], + span: sp(20, 30), + }, + )), + Item::Function(make_function( + "caller", + caller_name_span, + vec![], + Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("writer", call_ref_span))), + vec![], + call_span, + ), + span: call_span, + })], + span: sp(107, 120), + }, + )), + ], + span: sp(0, 200), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + // Expect E0500 (pure calls impure) and E0303 (arg count mismatch — caller + // passes 0 args but writer expects 1). We only assert E0500 exists. + let purity_errors: Vec<_> = errors.iter().filter(|d| d.code.code() == 500).collect(); + assert_eq!( + purity_errors.len(), + 1, + "expected 1 purity error (E0500), got: {:?}", + purity_errors + ); + assert!(purity_errors[0].message.contains("caller")); + assert!(purity_errors[0].message.contains("writer")); + } + + #[test] + fn impure_function_calling_impure_function_ok() { + // function writer(view out: Stdout) returns nothing: + // return nothing + // function caller(view out: Stdout) returns nothing: + // writer() ← ok, caller is also impure + let writer_name_span = sp(0, 6); + let writer_param_span = sp(7, 10); + let caller_name_span = sp(100, 106); + let caller_param_span = sp(107, 110); + let call_ref_span = sp(150, 156); + let call_span = sp(150, 158); + + let mut env = TestEnv::new(); + let writer_def = env.def_func("writer", writer_name_span); + let _caller_def = env.def_func("caller", caller_name_span); + env.def_param("out", writer_param_span); + env.def_param("out2", caller_param_span); + env.reference(writer_name_span, writer_def); + env.reference(caller_name_span, _caller_def); + env.reference(call_ref_span, writer_def); + + let module = Module { + items: vec![ + Item::Function(make_function( + "writer", + writer_name_span, + vec![Param { + view: true, + mutable: false, + name: ident("out", writer_param_span), + ty: TypeExpr::View( + Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), + sp(7, 17), + ), + span: writer_param_span, + }], + Block { + stmts: vec![], + span: sp(20, 30), + }, + )), + Item::Function(make_function( + "caller", + caller_name_span, + vec![Param { + view: true, + mutable: false, + name: ident("out2", caller_param_span), + ty: TypeExpr::View( + Box::new(TypeExpr::Named(ident("Stdout", sp(111, 117)))), + sp(107, 117), + ), + span: caller_param_span, + }], + Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("writer", call_ref_span))), + vec![], + call_span, + ), + span: call_span, + })], + span: sp(140, 160), + }, + )), + ], + span: sp(0, 300), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + // No purity errors expected (E0500). + let purity_errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.code.code() == 500) + .collect(); + assert!( + purity_errors.is_empty(), + "unexpected purity errors: {:?}", + purity_errors + ); + } + + #[test] + fn function_with_stdout_param_is_impure() { + // function printer(view out: Stdout) returns nothing + // This is a unit-level check: the purity map marks this function as impure. + let fn_name_span = sp(0, 7); + let param_span = sp(8, 11); + + let mut env = TestEnv::new(); + let fn_def = env.def_func("printer", fn_name_span); + env.def_param("out", param_span); + env.reference(fn_name_span, fn_def); + + let func = make_function( + "printer", + fn_name_span, + vec![Param { + view: true, + mutable: false, + name: ident("out", param_span), + ty: TypeExpr::View( + Box::new(TypeExpr::Named(ident("Stdout", sp(12, 18)))), + sp(8, 18), + ), + span: param_span, + }], + Block { + stmts: vec![], + span: sp(20, 30), + }, + ); + + let module = Module { + items: vec![Item::Function(func)], + span: sp(0, 100), + }; + + let resolve = env.into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + checker.check_module(&module); + + // The purity map should mark "printer" as impure. + assert_eq!( + checker.purity_map.get("printer").copied(), + Some(false), + "function with Stdout param should be impure" + ); + } + + #[test] + fn accepted_capability_parameters_have_distinct_concrete_types() { + let result = check_source_result( + r#"function main(stdout: Stdout, stderr: Stderr, stdin: Stdin, filesystem: Filesystem, network: Network, clock: Clock, random: Random, process: Process, environment: Environment, log: Log, graphics: Graphics) returns nothing: + return nothing +"#, + ); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "unexpected errors: {errors:?}"); + + let expected = CapabilityKind::ALL.map(TypeInterner::capability).to_vec(); + let checked_params = result + .definition_types + .values() + .find_map(|type_id| match result.interner.resolve(*type_id) { + Type::Function { params, .. } if params.len() == expected.len() => { + Some(params.clone()) + } + _ => None, + }) + .expect("checked main function type"); + + assert_eq!(checked_params, expected); + assert!( + checked_params + .iter() + .all(|type_id| *type_id != TypeInterner::ERROR) + ); + } + + #[test] + fn distinct_capabilities_do_not_unify_at_builtin_calls() { + let errors = check_source_errors( + r#"function misuse(view err: Stderr) returns nothing: + Stdout.write(view err, "wrong authority") + return nothing +"#, + ); + + assert!( + errors + .iter() + .any(|diagnostic| diagnostic.code.code() == 304), + "expected capability argument mismatch, got: {errors:?}" + ); + } + + #[test] + fn function_without_capability_params_is_pure() { + // function add(a: int64, b: int64) returns nothing + let fn_name_span = sp(0, 3); + let param_a_span = sp(4, 5); + let param_b_span = sp(6, 7); + + let mut env = TestEnv::new(); + let fn_def = env.def_func("add", fn_name_span); + env.def_param("a", param_a_span); + env.def_param("b", param_b_span); + env.reference(fn_name_span, fn_def); + + let func = make_function( + "add", + fn_name_span, + vec![ + Param { + view: false, + mutable: false, + name: ident("a", param_a_span), + ty: TypeExpr::Named(ident("int64", sp(100, 105))), + span: param_a_span, + }, + Param { + view: false, + mutable: false, + name: ident("b", param_b_span), + ty: TypeExpr::Named(ident("int64", sp(106, 111))), + span: param_b_span, + }, + ], + Block { + stmts: vec![], + span: sp(10, 20), + }, + ); + + let module = Module { + items: vec![Item::Function(func)], + span: sp(0, 100), + }; + + let resolve = env.into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + checker.check_module(&module); + + assert_eq!( + checker.purity_map.get("add").copied(), + Some(true), + "function without capability params should be pure" + ); + } + + #[test] + fn verify_block_calling_impure_function_error() { + // function writer(view out: Stdout) returns nothing: + // return nothing + // verify test_writer: + // writer() ← E0501 + let writer_name_span = sp(0, 6); + let writer_param_span = sp(7, 10); + let call_ref_span = sp(110, 116); + let call_span = sp(110, 118); + let verify_span = sp(100, 130); + + let mut env = TestEnv::new(); + let writer_def = env.def_func("writer", writer_name_span); + env.def_param("out", writer_param_span); + env.reference(writer_name_span, writer_def); + env.reference(call_ref_span, writer_def); + + let module = Module { + items: vec![ + Item::Function(make_function( + "writer", + writer_name_span, + vec![Param { + view: true, + mutable: false, + name: ident("out", writer_param_span), + ty: TypeExpr::View( + Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), + sp(7, 17), + ), + span: writer_param_span, + }], + Block { + stmts: vec![], + span: sp(20, 30), + }, + )), + Item::Verify(VerifyBlock { + name: ident("test_writer", sp(100, 111)), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("writer", call_ref_span))), + vec![], + call_span, + ), + span: call_span, + })], + span: sp(112, 130), + }, + span: verify_span, + }), + ], + span: sp(0, 200), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let verify_errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.code.code() == 501) + .collect(); + assert_eq!( + verify_errors.len(), + 1, + "expected 1 verify purity error (E0501), got: {:?}", + verify_errors + ); + assert!(verify_errors[0].message.contains("test_writer")); + assert!(verify_errors[0].message.contains("writer")); + } + + #[test] + fn verify_block_calling_pure_function_ok() { + // function helper() returns nothing: + // return nothing + // verify test_helper: + // helper() ← ok + let helper_name_span = sp(0, 6); + let call_ref_span = sp(110, 116); + let call_span = sp(110, 118); + let verify_span = sp(100, 130); + + let mut env = TestEnv::new(); + let helper_def = env.def_func("helper", helper_name_span); + env.reference(helper_name_span, helper_def); + env.reference(call_ref_span, helper_def); + + let module = Module { + items: vec![ + Item::Function(make_function( + "helper", + helper_name_span, + vec![], + Block { + stmts: vec![], + span: sp(10, 20), + }, + )), + Item::Verify(VerifyBlock { + name: ident("test_helper", sp(100, 111)), + body: Block { + stmts: vec![Stmt::Expr(ExprStmt { + expr: Expr::Call( + Box::new(Expr::Ident(ident("helper", call_ref_span))), + vec![], + call_span, + ), + span: call_span, + })], + span: sp(112, 130), + }, + span: verify_span, + }), + ], + span: sp(0, 200), + }; + + let resolve = env.into_resolve_result(); + let result = check(&module, &resolve); + + let purity_errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.code.code() == 500 || d.code.code() == 501) + .collect(); + assert!( + purity_errors.is_empty(), + "unexpected purity errors: {:?}", + purity_errors + ); + } + + #[test] + fn mutual_block_allows_mutual_recursion() { + let result = check_source_result( + "\ +mutual: + function is_even(n: int64) returns bool + function is_odd(n: int64) returns bool + +function is_even(n: int64) returns bool: + if n == 0: + return true + return is_odd(n - 1) + +function is_odd(n: int64) returns bool: + if n == 0: + return false + return is_even(n - 1) +", + ); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn mutual_block_missing_definition_reports_error() { + let errors = check_source_errors( + "\ +mutual: + function is_even(n: int64) returns bool + +function main() returns nothing: + return nothing +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 325), + "expected E0325, got: {:?}", + errors + ); + } + + #[test] + fn mutual_block_signature_mismatch_reports_error() { + let errors = check_source_errors( + "\ +mutual: + function is_even(n: int64) returns bool + +function is_even(value: string) returns bool: + return true +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 326), + "expected E0326, got: {:?}", + errors + ); + } + + #[test] + fn user_defined_struct_constructor_and_field_access_typecheck_cleanly() { + let result = check_source_result( + "\ +struct Point: + x: int64 + y: int64 + +function sum(view point: Point) returns int64: + return point.x + point.y + +function main() returns int64: + Point point = Point(x: 1, y: 2) + return sum(view point) +", + ); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn struct_method_call_typechecks_cleanly() { + let result = check_source_result( + "\ +struct Point: + x: int64 + y: int64 + + function total(view self: Point) returns int64: + return self.x + self.y + +function main() returns int64: + Point point = Point(x: 1, y: 2) + return Point.total(view point) +", + ); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn named_function_arguments_record_parameter_order() { + let result = check_source_result( + "\ +function subtract(left: int64, right: int64) returns int64: + return left - right + +function main() returns int64: + return subtract(right: 2, left: 7) +", + ); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let order = result + .call_argument_orders + .values() + .next() + .expect("named call must export its parameter order"); + assert_eq!(order.source_indices, vec![1, 0]); + } + + #[test] + fn named_function_arguments_reject_unknown_and_duplicate_parameters() { + let unknown = check_source_errors( + "\ +function subtract(left: int64, right: int64) returns int64: + return left - right +function main() returns int64: + return subtract(left: 7, other: 2) +", + ); + assert!( + unknown + .iter() + .any(|diagnostic| diagnostic.code.code() == 368), + "expected E0368, got: {:?}", + unknown + ); + + let duplicate = check_source_errors( + "\ +function subtract(left: int64, right: int64) returns int64: + return left - right +function main() returns int64: + return subtract(left: 7, left: 2) +", + ); + assert!( + duplicate + .iter() + .any(|diagnostic| diagnostic.code.code() == 369), + "expected E0369, got: {:?}", + duplicate + ); + } + + #[test] + fn pipeline_arguments_and_method_targets_are_checked_in_parameter_order() { + let result = check_source_result( + "\ +struct Score: + value: int64 + function add(view self: Score, bonus: int64) returns int64: + return self.value + bonus +function choose[T](first: T, second: T, third: T) returns T: + return first +function main() returns int64: + Score score = Score(value: 7) + int64 chosen = 1 into choose[int64](third: 3, second: 2) + return score into view Score.add(bonus: chosen) +", + ); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert!( + result + .call_argument_orders + .values() + .any(|order| order.source_indices == vec![0, 2, 1]) + ); + assert_eq!(result.method_calls.len(), 1); + } + + #[test] + fn struct_constructor_missing_field_reports_error() { + let errors = check_source_errors( + "\ +struct Point: + x: int64 + y: int64 + +function main() returns Point: + return Point(x: 1) +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 321), + "expected E0321, got: {:?}", + errors + ); + } + + #[test] + fn unknown_struct_field_reports_error() { + let errors = check_source_errors( + "\ +struct Point: + x: int64 + y: int64 + +function main(view point: Point) returns int64: + return point.z +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 319), + "expected E0319, got: {:?}", + errors + ); + } + + #[test] + fn generic_type_argument_arity_still_checks_value_expressions() { + let errors = check_source_errors( + "\ +function identity[T](value: T) returns T: + return value + +function main() returns nothing: + identity[int64, string](true + \"bad\") +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 301), + "expected nested binary expression diagnostic, got: {:?}", + errors + ); + } + + #[test] + fn enum_variant_construction_and_match_typecheck_cleanly() { + let result = check_source_result( + "\ +enum Shape: + circle(radius: int64) + rect(width: int64, height: int64) + +function area(shape: Shape) returns int64: + match shape: + circle(radius): + return radius * radius + rect(width, height): + return width * height + +function main() returns int64: + Shape shape = Shape.circle(3) + return area(shape) +", + ); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn enum_variant_construction_checks_surplus_argument_expressions() { + let errors = check_source_errors( + "\ +enum Shape: + point(x: int64, y: int64) + +function main() returns nothing: + Shape shape = Shape.point(1, 2, true + \"bad\") +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 303), + "expected the enum arity diagnostic, got: {:?}", + errors + ); + assert!( + errors.iter().any(|d| d.code.code() == 301), + "expected surplus argument expression to be type-checked, got: {:?}", + errors + ); + } + + #[test] + fn non_exhaustive_enum_match_reports_error() { + let errors = check_source_errors( + "\ +enum Color: + red + blue + +function describe(color: Color) returns int64: + match color: + red: + return 1 +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 324), + "expected E0324, got: {:?}", + errors + ); + } + + #[test] + fn enum_pattern_binding_count_mismatch_reports_error() { + let errors = check_source_errors( + "\ +enum Shape: + rect(width: int64, height: int64) + +function area(shape: Shape) returns int64: + match shape: + rect(width): + return width +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 323), + "expected E0323, got: {:?}", + errors + ); + } + + #[test] + fn reflection_metadata_owner_tables_are_type_id_backed() { + let result = check_source_result( + "\ +namespace models + +export struct User: + name: string + score: int64 + +namespace packets + +export enum Protocol: + tcp = 6 + udp = 17 + +export bitfield Header: + version: 4 bits + protocol: 8 bits as Protocol +", + ); + let metadata = result.reflection_metadata; + + let user_id = metadata + .type_id_for_name("models.User") + .expect("models.User should have a canonical TypeId"); + let user_fields = metadata + .get_type_fields_for_id(user_id) + .expect("struct fields should be keyed by owner TypeId"); + assert_eq!(user_fields[0].name, "name"); + assert_eq!( + metadata + .get_type_fields("models.User") + .expect("legacy field lookup should bridge through TypeId")[1] + .name, + "score" + ); + + let header_id = metadata + .type_id_for_name("packets.Header") + .expect("packets.Header should have a canonical TypeId"); + let header = metadata + .get_bitfield_for_id(header_id) + .expect("bitfield metadata should be keyed by owner TypeId"); + assert_eq!(header.fields[1].name, "protocol"); + assert_eq!( + metadata + .get_bitfield("packets.Header") + .expect("legacy bitfield lookup should bridge through TypeId") + .fields[0] + .width, + 4 + ); + + let protocol_id = metadata + .type_id_for_name("packets.Protocol") + .expect("packets.Protocol should have a canonical TypeId"); + let variants = metadata + .get_type_variants_for_id(protocol_id) + .expect("enum variants should be keyed by owner TypeId"); + assert_eq!(variants[0].name, "tcp"); + assert_eq!( + metadata + .get_type_variants("packets.Protocol") + .expect("legacy variant lookup should bridge through TypeId")[1] + .discriminant, + 17 + ); + } + + #[test] + fn reflection_operands_preserve_generic_aliases_without_replacing_canonical_metadata() { + let result = check_source_result( + r#" +type Label = string +struct Box[T]: + item: T +function builder[T]() returns TypeConstruction: + comptime type Owner = type.info[Box[T]](): + return type.construct_start[Owner]() +function main() returns nothing: + TypeConstruction value = builder[Label]() + return nothing +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + let metadata = &result.reflection_metadata; + let owner = metadata + .source_type_id_for_name("Box[Label]") + .expect("checked alias owner"); + let source = metadata + .get_source_type_info("Box[Label]") + .expect("source reflection operand"); + assert_eq!(source.args[0].kind, "alias"); + assert_eq!(source.args[0].type_name, "Label"); + let canonical = metadata + .get_type_info_for_id(owner) + .expect("canonical owner metadata"); + assert_eq!(canonical.args[0].kind, "primitive"); + assert_eq!(canonical.args[0].type_name, "string"); + assert_eq!( + metadata.get_type_fields_for_id(owner).unwrap()[0].type_name, + "string" + ); + } + + #[test] + fn nominal_argument_ids_preserve_aliases_unused_parameters_and_canonical_owners() { + let result = check_source_result( + r#" +namespace models +export type Positive = int64 where value > 0 +export type Copy = Positive +export struct Element: + value: Positive +export struct Holder[T]: + value: T +export struct Marker[T]: + value: int64 +namespace alternate +export type Positive = int64 where value < 0 +namespace app +function inspect(view element: models.Element, view used: models.Holder[models.Positive], view unused: models.Marker[models.Positive], view plain: models.Marker[int64], view shadow: models.Marker[alternate.Positive]) returns nothing: + return nothing +function alias_metadata() returns TypeInfo: + use models as m + return type.info[m.Marker[m.Copy]]() +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + let types = &result.interner; + let metadata = &result.reflection_metadata; + let positive = metadata.type_id_for_name("models.Positive").unwrap(); + let other = metadata.type_id_for_name("alternate.Positive").unwrap(); + assert_ne!(positive, other); + for (name, expected) in [ + ("models.Element", Vec::new()), + ("models.Holder[models.Positive]", vec![positive]), + ("models.Marker[models.Positive]", vec![positive]), + ("models.Marker[int64]", vec![TypeInterner::INT64]), + ("models.Marker[alternate.Positive]", vec![other]), + ] { + let ty = metadata.type_id_for_name(name).expect(name); + assert_eq!( + types.nominal_type_arguments(ty), + expected.as_slice(), + "{name}" + ); + } + let marker = metadata + .type_id_for_name("models.Marker[models.Positive]") + .unwrap(); + let Type::Struct(id) = types.resolve(marker) else { + panic!("marker"); + }; + assert_eq!(types.resolve_struct(*id).fields[0].1, TypeInterner::INT64); + let alias = result + .intrinsic_reflection_arguments + .values() + .flatten() + .find(|info| info.type_name == "models.Marker[models.Copy]") + .expect("source alias metadata"); + assert_eq!(alias.args[0].type_name, "models.Copy"); + assert!( + result + .intrinsic_type_arguments + .values() + .any(|args| args.as_slice() == [marker]) + ); + } + + #[test] + fn reflection_metadata_generic_type_info_args_are_type_id_backed() { + let result = check_source_result( + "\ +namespace accounts + +export struct Box[T]: + value: T + +namespace audit + +export struct Box[T]: + value: T + +namespace app + +function use_boxes() returns string: + use accounts as a + use audit as au + a.Box[int64] account = a.Box[int64](value: 1) + au.Box[int64] audit_box = au.Box[int64](value: 2) + TypeInfo account_info = type.info[a.Box[int64]]() + TypeInfo audit_info = type.info[au.Box[int64]]() + return \"{account_info.type_name}:{audit_info.type_name}:{account.value}:{audit_box.value}\" +", + ); + let metadata = result.reflection_metadata; + + let account_info = metadata + .get_type_info("accounts.Box[int64]") + .expect("accounts.Box[int64] should have canonical type info"); + assert_eq!(account_info.args[0].type_name, "int64"); + + let audit_info = metadata + .get_type_info("audit.Box[int64]") + .expect("audit.Box[int64] should have canonical type info"); + assert_eq!(audit_info.args[0].type_name, "int64"); + + assert!( + metadata.get_type_info("Box[int64]").is_none(), + "ambiguous generic leaf metadata should not be published" + ); + } + + #[test] + fn resolved_namespaced_type_lookup_does_not_fall_back_to_root_leaf() { + let type_span = sp(0, 4); + let mut env = TestEnv::new(); + let namespaced_type = + env.scope_table + .new_def("models.User".to_string(), DefKind::Struct, type_span); + env.reference(type_span, namespaced_type); + let resolve = env.into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + checker.register_named_type(None, "User", TypeInterner::INT64); + + assert_eq!( + checker.resolve_named_type("User", type_span), + TypeInterner::ERROR + ); + + let generic_span = sp(10, 14); + let mut env = TestEnv::new(); + let namespaced_generic = + env.scope_table + .new_def("models.Box".to_string(), DefKind::Struct, generic_span); + env.reference(generic_span, namespaced_generic); + let resolve = env.into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + checker.generic_struct_templates.insert( + "Box".to_string(), + ast::StructDef { + name: ident("Box", generic_span), + type_params: vec![ident("T", generic_span)], + fields: Vec::new(), + methods: Vec::new(), + exported: false, + span: generic_span, + }, + ); + + assert_eq!( + checker.resolve_generic_type( + "Box", + &[TypeExpr::Named(ident("int64", sp(15, 20)))], + generic_span, + ), + TypeInterner::ERROR + ); + } + + #[test] + fn raw_json_facade_signature_prefers_trusted_stdlib_declaration() { + let result = check_source_result_with_file_id( + "\ +namespace json + +export enum JsonTree: + null + +export function kind(view value: JsonTree) returns int64: + return 1 + +namespace app + +function main() returns int64: + json.JsonTree tree = json.JsonTree.null + return json.kind(view tree) +", + FileId::new(STDLIB_FILE_ID_START), + ); + + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn raw_json_facade_uses_ordinary_source_signature_without_trusted_stdlib() { + let errors = check_source_errors( + "\ +namespace json + +export enum JsonTree: + null + +export function kind(view value: JsonTree) returns int64: + return 1 + +namespace app + +function main() returns string: + use json + json.JsonTree tree = json.JsonTree.null + return json.kind(view tree) +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 305), + "expected E0305, got: {:?}", + errors + ); + } + + #[test] + fn whole_sum_interface_compatibility_precedes_payload_handling() { + let errors = check_source_errors( + r#" +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "integer" +implement Named for result[int64, string]: + function name(view self: result[int64, string]) returns string: + return "result" +implement Named for optional[int64]: + function name(view self: optional[int64]) returns string: + return "optional" +function consume(value: Named) returns nothing: + return nothing +function main() returns nothing: + result[int64, string] outcome = ok(1) + optional[int64] maybe = some(2) + Named first = clone outcome + Named second = clone maybe + consume(outcome) + consume(maybe) + consume(first) + consume(second) + return nothing +"#, + ); + assert!(errors.is_empty(), "{errors:?}"); + } + + #[test] + fn interface_payload_membership_does_not_implicitly_unwrap_sums() { + let errors = check_source_errors( + r#" +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "integer" +function consume(value: Named) returns nothing: + return nothing +function main() returns nothing: + result[int64, string] outcome = ok(1) + optional[int64] maybe = some(2) + consume(outcome) + consume(maybe) + return nothing +"#, + ); + assert!( + errors.iter().any(|error| error.code.code() == 316), + "{errors:?}" + ); + assert!( + errors.iter().any(|error| error.code.code() == 317), + "{errors:?}" + ); + } + + #[test] + fn result_handle_requires_error_keyword() { + let errors = check_source_errors( + "\ +function main() returns int64: + int64 parsed = int64.from_string(\"42\") handle: + default 0 + return parsed +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 316), + "expected E0316, got: {:?}", + errors + ); + } + + #[test] + fn optional_handle_rejects_error_keyword() { + let errors = check_source_errors( + "\ +function main() returns int64: + int64 value = some(1) handle error: + default 0 + return value +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 317), + "expected E0317, got: {:?}", + errors + ); + } + + #[test] + fn handle_block_requires_explicit_terminator() { + let errors = check_source_errors( + "\ +function main() returns int64: + int64 parsed = int64.from_string(\"oops\") handle error: + int64 fallback = 0 + return parsed +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 318), + "expected E0318, got: {:?}", + errors + ); + } + + #[test] + fn handle_default_must_match_unwrapped_type() { + let errors = check_source_errors( + "\ +function main() returns int64: + int64 parsed = int64.from_string(\"oops\") handle error: + default \"bad\" + return parsed +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 300), + "expected E0300, got: {:?}", + errors + ); + } + + #[test] + fn handle_with_builtin_result_type_checks_cleanly() { + let errors = check_source_errors( + "\ +function main() returns int64: + int64 parsed = int64.from_string(\"42\") handle error: + default 0 + return parsed +", + ); + + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn interface_implement_call_typechecks_cleanly() { + let result = check_source_result( + "\ +interface Speaker: + function speak(view self: Speaker) returns string - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +struct Dog: + name: string + +implement Speaker for Dog: + function speak(view self: Dog) returns string: + return self.name + +function main() returns string: + Dog dog = Dog(name: \"woof\") + return Speaker.speak(view dog) +", + ); - // No errors expected. let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } - // The binary expression should be typed as int64. - assert_eq!(result.type_map[&binop_span], TypeInterner::INT64); + #[test] + fn erased_interface_calls_keep_checked_slots_across_generic_specialization() { + let result = check_source_result( + "\ +interface Speaker: + function speak(view self: Speaker) returns string +struct Dog: + name: string +implement Speaker for Dog: + function speak(view self: Dog) returns string: + return self.name +function describe(view value: Speaker) returns string: + return Speaker.speak(view value) +function repeat[T](view value: Speaker, ignored: T) returns string: + return Speaker.speak(view value) +function main() returns nothing: + Dog dog = Dog(name: \"woof\") + println(describe(view dog)) + println(repeat[int64](view dog, 1)) + println(repeat[string](view dog, \"x\")) + return nothing +", + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + assert_eq!(result.interface_calls.len(), 1); + assert!(result.method_values.is_empty()); + let call = result.interface_calls.values().next().unwrap(); + let Type::Interface(id) = result.interner.resolve(call.interface_type) else { + panic!("dispatch owner must be a checked interface"); + }; + assert_eq!( + result.interner.resolve_interface(*id).methods[call.method_index].name, + "speak" + ); + let specialized = result + .generic_function_instantiations + .iter() + .filter(|body| !body.interface_calls.is_empty()) + .collect::>(); + assert_eq!(specialized.len(), 2); + for body in specialized { + assert_eq!(body.interface_calls.len(), 1); + assert_eq!(body.interface_calls.values().next(), Some(call)); + assert!(body.method_values.is_empty()); + } } - // --------------------------------------------------------------- - // Test: type mismatch error (int64 + string) - // --------------------------------------------------------------- + #[test] + fn namespaced_interface_alias_call_records_concrete_method_target() { + let result = check_source_result( + "\ +namespace contracts +export interface Named: + function name(view self: contracts.Named) returns string +namespace models +export struct User: + name: string +implement contracts.Named for models.User: + function name(view self: models.User) returns string: + return self.name +namespace app +function describe() returns string: + use contracts as c + use models as m + m.User user = m.User(name: \"Ada\") + return c.Named.name(view user) +", + ); + + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + assert_eq!( + result.method_calls.len(), + 1, + "the alias-qualified interface call must retain its concrete implementation target" + ); + } #[test] - fn type_mismatch_int_plus_string() { - // a: int64, b: string → a + b should emit an error - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - let ref_a_span = sp(10, 11); - let ref_b_span = sp(12, 13); - let binop_span = sp(10, 13); - let body_span = sp(8, 14); - let func_span = sp(0, 14); + fn pending_sum_literals_retain_expected_payload_types() { + let source = r#"function values() returns nothing: + result[int64, string] success = run ok(17) + result[int64, string] failure = run fail("bad") + optional[int64] missing = run run none + list[int8] empty = run list() + int8 narrow = run 127 + return nothing +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + for (expression, expected) in [ + ("run ok(17)", "result[int64, string]"), + ("run fail(\"bad\")", "result[int64, string]"), + ("run run none", "optional[int64]"), + ("run list()", "list[int8]"), + ("run 127", "int8"), + ] { + let start = source.find(expression).unwrap(); + let span = Span::new( + FileId::new(0), + start as u32, + (start + expression.len()) as u32, + ); + assert_eq!(result.interner.type_name(result.type_map[&span]), expected); + } + } - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("bad", fn_name_span); - let a_def_id = env.def_param("a", param_a_span); - let b_def_id = env.def_param("b", param_b_span); - env.reference(ref_a_span, a_def_id); - env.reference(ref_b_span, b_def_id); - env.reference(fn_name_span, fn_def_id); + #[test] + fn collection_alias_methods_export_call_and_value_targets() { + let source = r#"namespace app +interface Named: + function name(view self: Named) returns string +type Numbers = list[int64] +implement Named for Numbers: + function name(view self: Numbers) returns string: + return "numbers" +function read(view values: Numbers) returns string: + return Numbers.name(view values) +function reader() returns function(view Numbers) returns string: + return Numbers.name +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + assert_eq!(result.method_calls.len(), 1, "{:?}", result.method_calls); + assert_eq!(result.method_values.len(), 2, "{:?}", result.method_values); + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("bad", fn_name_span), - type_params: vec![], - params: vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("string", sp(106, 112))), - span: param_b_span, - }, - ], - return_type: Some(TypeExpr::Named(ident("nothing", sp(113, 120)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Binary( - Box::new(Expr::Ident(ident("a", ref_a_span))), - BinOp::Add, - Box::new(Expr::Ident(ident("b", ref_b_span))), - binop_span, - ), - span: binop_span, - })], - span: body_span, - }, - exported: false, - span: func_span, - })], - span: sp(0, 14), + #[test] + fn concrete_method_values_export_source_targets_and_view_signatures() { + let source = r#"namespace models +export interface Reader: + function read(view self: Reader) returns int64 +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +implement Reader for Point: + function read(view self: Point) returns int64: + return self.value +namespace app +function reader() returns function(view models.Point) returns int64: + use models as m + return (m.Point.read) +function invoke() returns int64: + use models as m + function(view m.Point) returns int64 callback = m.Point.amount + m.Point point = m.Point(value: 7) + return callback(view point) +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", + result.diagnostics + ); + assert_eq!(result.method_values.len(), 2); + for (expression_span, target) in &result.method_values { + let CheckedMethodValue::Source { source_span } = target else { + panic!("concrete method value must select a source body"); + }; + let expression = &source[expression_span.start as usize..expression_span.end as usize]; + let definition = result + .method_definitions + .iter() + .find(|method| method.source_span == *source_span) + .expect("method value must identify its checked source definition"); + assert_eq!(definition.owner_name, "models.Point"); + assert_eq!( + definition.interface_name.as_deref(), + match expression { + "m.Point.read" => Some("models.Reader"), + "m.Point.amount" => None, + other => panic!("unexpected method expression: {other}"), + } + ); + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(result.type_map[expression_span]) + else { + panic!("method value must have a function type"); + }; + assert_eq!(params, &vec![definition.owner_type]); + assert_eq!(view_params, &vec![true]); + assert_eq!(*return_type, TypeInterner::INT64); + } + } + + #[test] + fn interface_method_values_export_callback_only_slots_and_exact_signatures() { + let source = r#"namespace app +interface Reader: + function read(view self: Reader) returns int64 + function consume(self: Reader, flag: bool) returns string +function reader() returns function(view Reader) returns int64: + return Reader.read +function consumer() returns function(Reader, bool) returns string: + return Reader.consume +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + result.diagnostics + ); + assert!(result.interface_calls.is_empty()); + assert!(result.method_definitions.is_empty()); + assert!(result.method_value_definitions.is_empty()); + assert_eq!(result.method_values.len(), 2); + for (span, target) in &result.method_values { + let CheckedMethodValue::Interface { + interface_type, + method_index, + } = target + else { + panic!("interface method value must identify its dispatch slot"); + }; + let Type::Interface(id) = result.interner.resolve(*interface_type) else { + panic!("slot owner must remain the exact interface type"); + }; + let interface = result.interner.resolve_interface(*id); + assert_eq!(interface.name, "app.Reader"); + let method = &interface.methods[*method_index]; + let expression = &source[span.start as usize..span.end as usize]; + let (index, params, views, output) = match expression { + "Reader.read" => (0, vec![*interface_type], vec![true], TypeInterner::INT64), + "Reader.consume" => ( + 1, + vec![*interface_type, TypeInterner::BOOL], + vec![false, false], + TypeInterner::STRING, + ), + other => panic!("unexpected method expression: {other}"), + }; + assert_eq!(*method_index, index); + assert_eq!(expression.rsplit('.').next().unwrap(), method.name); + assert_eq!( + result.interner.resolve(result.type_map[span]), + &Type::Function { + params, + view_params: views, + return_type: output, + } + ); + } + } + + #[test] + fn interface_method_values_share_alias_and_namespace_slots() { + let result = check_source_result( + r#"namespace contracts +export interface Reader: + function read(view self: contracts.Reader) returns int64 +namespace app +type Access = contracts.Reader +function callbacks() returns nothing: + use contracts + use contracts as c + function(view contracts.Reader) returns int64 qualified = contracts.Reader.read + function(view c.Reader) returns int64 imported = (c.Reader.read) + function(view Access) returns int64 aliased = Access.read + return nothing +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + result.diagnostics + ); + assert_eq!(result.method_values.len(), 3); + let expected = result.method_values.values().next().unwrap(); + let CheckedMethodValue::Interface { + interface_type, + method_index, + } = expected + else { + panic!("aliases must resolve to an interface slot"); }; + assert_eq!( + result.interner.type_name(*interface_type), + "contracts.Reader" + ); + assert_eq!(*method_index, 0); + for (span, target) in &result.method_values { + assert_eq!(target, expected); + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(result.type_map[span]) + else { + panic!("method value must keep its declared function signature"); + }; + assert_eq!(params, &vec![*interface_type]); + assert_eq!(view_params, &vec![true]); + assert_eq!(*return_type, TypeInterner::INT64); + } + assert!(result.method_value_definitions.is_empty()); + } - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + #[test] + fn interface_method_values_are_distinct_from_direct_call_targets() { + let source = r#"interface Reader: + function read(view self: Reader) returns int64 +struct Point: + value: int64 +implement Reader for Point: + function read(view self: Point) returns int64: + return self.value +function observed(view value: Reader) returns int64: + function(view Reader) returns int64 callback = Reader.read + return Reader.read(view value) + callback(view value) +function main() returns int64: + Point point = Point(value: 7) + return Reader.read(view point) + observed(view point) +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + result.diagnostics + ); + assert_eq!(result.interface_calls.len(), 1); + assert_eq!(result.method_calls.len(), 1); + assert_eq!(result.method_values.len(), 1); + let (span, target) = result.method_values.iter().next().unwrap(); + assert!(matches!( + target, + CheckedMethodValue::Interface { + method_index: 0, + .. + } + )); + let value_start = source.find("Reader.read\n").unwrap(); + assert_eq!(span.start as usize, value_start); + assert_eq!(span.end as usize, value_start + "Reader.read".len()); + assert_eq!(result.method_value_definitions.len(), 1); + assert!( + result + .method_value_definitions + .contains(&result.method_definitions[0].source_span) + ); + } + + #[test] + fn interface_method_values_survive_generic_body_instantiation() { + let result = check_source_result( + r#"namespace sample +interface Reader: + function read(view self: Reader) returns int64 +function reader[T](value: T) returns function(view Reader) returns int64: + return Reader.read +function main() returns nothing: + function(view Reader) returns int64 first = reader[int64](1) + function(view Reader) returns int64 second = reader[string]("two") + return nothing +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + result.diagnostics + ); + assert_eq!(result.generic_function_instantiations.len(), 2); + let mut expected = None; + for instance in &result.generic_function_instantiations { + assert_eq!(instance.method_values.len(), 1); + let (span, target) = instance.method_values.iter().next().unwrap(); + let CheckedMethodValue::Interface { + interface_type, + method_index, + } = target + else { + panic!("concrete generic body must retain its interface slot"); + }; + assert_eq!(result.interner.type_name(*interface_type), "sample.Reader"); + assert_eq!(*method_index, 0); + assert!(instance.type_map.contains_key(span)); + if let Some(expected) = expected { + assert_eq!(*target, expected); + } else { + expected = Some(*target); + } + assert!(instance.interface_calls.is_empty()); + } + assert!(result.method_value_definitions.is_empty()); + } - let errors: Vec<_> = result - .diagnostics + #[test] + fn interface_method_values_survive_comptime_body_fact_snapshots() { + let result = check_source_result( + r#"namespace sample +interface Reader: + function read(view self: Reader) returns int64 +struct Pair: + name: string + count: int64 +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + function(view Reader) returns int64 callback = Reader.read + string reflected_name = type.name[Field]() + return nothing +function main() returns nothing: + Pair pair = Pair(name: "Ada", count: 42) + inspect[Pair](view pair) +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "{:?}", + result.diagnostics + ); + let inspect = result + .generic_function_instantiations .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 301); // binary_op_mismatch - assert!(errors[0].message.contains("int64")); - assert!(errors[0].message.contains("string")); + .find(|instance| !instance.comptime_type_bindings.is_empty()) + .unwrap(); + let bindings = inspect.comptime_type_bindings.values().next().unwrap(); + assert_eq!(bindings.len(), 2); + for binding in bindings { + assert_eq!(binding.body.method_values.len(), 1); + let (span, target) = binding.body.method_values.iter().next().unwrap(); + let CheckedMethodValue::Interface { + interface_type, + method_index, + } = target + else { + panic!("reflected body facts must retain their interface slot"); + }; + assert_eq!(result.interner.type_name(*interface_type), "sample.Reader"); + assert_eq!(*method_index, 0); + assert!(binding.body.type_map.contains_key(span)); + assert!(binding.body.interface_calls.is_empty()); + } + assert!(result.method_value_definitions.is_empty()); } - // --------------------------------------------------------------- - // Test: binary operator type checking (arithmetic, comparison, logic) - // --------------------------------------------------------------- - #[test] - fn binary_operators_arithmetic_returns_same_type() { - // 10 + 20 → int64 - let binop_span = sp(0, 5); - - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Binary( - Box::new(Expr::IntLiteral(10, sp(0, 2))), - BinOp::Add, - Box::new(Expr::IntLiteral(20, sp(3, 5))), - binop_span, - ), - span: binop_span, - })], - span: sp(0, 5), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; + fn interface_method_values_keep_existing_signature_and_member_rejections() { + let prefix = "interface Reader:\n function read(view self: Reader) returns int64\n"; + for declaration in [ + "function(Reader) returns int64 callback = Reader.read", + "function(view Reader, int64) returns int64 callback = Reader.read", + "function(view Reader) returns string callback = Reader.read", + ] { + let source = format!( + "{prefix}function callbacks() returns nothing:\n {declaration}\n return nothing\n" + ); + let errors = check_source_errors(&source); + assert!( + errors.iter().any(|error| error.code.code() == 311), + "{declaration}: {errors:?}" + ); + } + let source = format!( + "{prefix}function callbacks() returns nothing:\n function(view Reader) returns int64 callback = Reader.missing\n return nothing\n" + ); + let result = check_source_result(&source); + assert!( + result + .diagnostics + .iter() + .any(|error| error.code.code() == 329) + ); + assert!(result.method_values.is_empty()); + } - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + #[test] + fn callback_parameters_are_contravariant_and_returns_are_covariant() { + let prefix = r#"interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return self.label +function general(view item: Named) returns User: + return User(label: Named.name(view item)) +function narrow(view item: User) returns Named: + return clone item +"#; + let safe = format!( + "{prefix}function main() returns nothing:\n function(view User) returns Named callback = general\n return nothing\n" + ); + assert!( + check_source_errors(&safe).is_empty(), + "{:?}", + check_source_errors(&safe) + ); + for declaration in [ + "function(view Named) returns Named callback = narrow", + "function(view User) returns User callback = narrow", + "function(User) returns Named callback = general", + ] { + let source = format!( + "{prefix}function main() returns nothing:\n {declaration}\n return nothing\n" + ); + let errors = check_source_errors(&source); + assert!( + errors.iter().any(|error| error.code.code() == 311), + "{declaration}: {errors:?}" + ); + } + } + #[test] + fn concrete_method_values_survive_generic_body_instantiation() { + let result = check_source_result( + r#"namespace sample +struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +function reader[T](value: T) returns function(view Point) returns int64: + return Point.amount +function main() returns int64: + function(view Point) returns int64 first = reader[int64](1) + function(view Point) returns int64 second = reader[string]("two") + Point point = Point(value: 7) + return first(view point) + second(view point) +"#, + ); assert!( result .diagnostics .iter() - .all(|d| d.severity != jett_diagnostics::Severity::Error), - "unexpected errors: {:?}", + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", result.diagnostics ); - assert_eq!(result.type_map[&binop_span], TypeInterner::INT64); + assert_eq!(result.generic_function_instantiations.len(), 2); + let method = &result.method_definitions[0]; + for instance in &result.generic_function_instantiations { + assert_eq!(instance.method_values.len(), 1); + let (expression_span, target) = instance.method_values.iter().next().unwrap(); + assert_eq!( + target, + &CheckedMethodValue::Source { + source_span: method.source_span, + } + ); + assert!(instance.type_map.contains_key(expression_span)); + } } #[test] - fn binary_operators_comparison_returns_bool() { - // 10 < 20 → bool - let binop_span = sp(0, 5); - - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Binary( - Box::new(Expr::IntLiteral(10, sp(0, 2))), - BinOp::Lt, - Box::new(Expr::IntLiteral(20, sp(3, 5))), - binop_span, - ), - span: binop_span, - })], - span: sp(0, 5), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; - - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + fn concrete_method_returning_a_function_is_not_a_struct_constructor() { + for (prefix, call) in [ + ("", "Point.offsetter"), + ("namespace app\n", "models.Point.offsetter"), + ("namespace app\n", "alias.Point.offsetter"), + ] { + let source = format!( + r#"namespace models +export struct Point: + value: int64 + function offsetter(view self: Point) returns function(int64) returns int64: + int64 base = self.value + return function(input: int64) returns int64: return input + base +{prefix}function invoke() returns int64: + use models + use models as alias + models.Point point = models.Point(value: 7) + function(int64) returns int64 callback = {call}(view point) + return callback(3) +"# + ); + let result = check_source_result(&source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{source}: {:?}", + result.diagnostics + ); + assert_eq!(result.struct_constructions.len(), 1); + assert_eq!(result.method_calls.len(), 1); + } + } + #[test] + fn concrete_method_values_select_the_same_inherent_body_as_the_signature() { + let result = check_source_result( + r#"namespace sample +interface Reader: + function read(view self: Reader) returns int64 +struct Point: + value: int64 + function read(view self: Point) returns int64: + return self.value +implement Reader for Point: + function read(view self: Point) returns int64: + return self.value + 10 +function main() returns int64: + function(view Point) returns int64 callback = Point.read + Point point = Point(value: 7) + return callback(view point) + Point.read(view point) + Reader.read(view point) +"#, + ); assert!( result .diagnostics .iter() - .all(|d| d.severity != jett_diagnostics::Severity::Error), - "unexpected errors: {:?}", + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", result.diagnostics ); - assert_eq!(result.type_map[&binop_span], TypeInterner::BOOL); + let source_span = result + .method_values + .values() + .find_map(|target| match target { + CheckedMethodValue::Source { source_span } => Some(*source_span), + CheckedMethodValue::Interface { .. } => None, + }) + .unwrap(); + let definition = result + .method_definitions + .iter() + .find(|method| method.source_span == source_span) + .unwrap(); + assert!(definition.interface_name.is_none()); + assert!( + result + .method_value_definitions + .contains(&definition.source_span) + ); + assert!(!result.method_definitions.iter().any(|method| { + method.interface_name.is_some() + && result + .method_value_definitions + .contains(&method.source_span) + })); + let mut targets = result + .method_calls + .values() + .map(|target| { + result + .method_definitions + .iter() + .find(|method| method.source_span == target.source_span) + .unwrap() + .interface_name + .is_some() + }) + .collect::>(); + targets.sort_unstable(); + assert_eq!(targets, vec![false, true]); } #[test] - fn binary_operators_logic_requires_bool() { - // true && false → bool (ok) - let binop_span = sp(0, 10); - - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Binary( - Box::new(Expr::BoolLiteral(true, sp(0, 4))), - BinOp::And, - Box::new(Expr::BoolLiteral(false, sp(5, 10))), - binop_span, - ), - span: binop_span, - })], - span: sp(0, 10), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; - - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); - + fn concrete_method_values_survive_comptime_body_fact_snapshots() { + let result = check_source_result( + r#"namespace sample +struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +struct Pair: + name: string + count: int64 +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + function(view Point) returns int64 callback = Point.amount + string reflected_name = type.name[Field]() + return nothing +function main() returns nothing: + Pair pair = Pair(name: "Ada", count: 42) + inspect[Pair](view pair) +"#, + ); assert!( result .diagnostics .iter() - .all(|d| d.severity != jett_diagnostics::Severity::Error), - "unexpected errors: {:?}", + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "{:?}", result.diagnostics ); - assert_eq!(result.type_map[&binop_span], TypeInterner::BOOL); + let inspect = result + .generic_function_instantiations + .iter() + .find(|instance| !instance.comptime_type_bindings.is_empty()) + .unwrap(); + let bindings = inspect.comptime_type_bindings.values().next().unwrap(); + assert_eq!(bindings.len(), 2); + for binding in bindings { + assert_eq!(binding.body.method_values.len(), 1); + let (expression_span, target) = binding.body.method_values.iter().next().unwrap(); + assert_eq!( + target, + &CheckedMethodValue::Source { + source_span: result.method_definitions[0].source_span, + } + ); + assert!(binding.body.type_map.contains_key(expression_span)); + } + } + + #[test] + fn implement_block_missing_method_reports_error() { + let errors = check_source_errors( + "\ +interface Speaker: + function speak(view self: Speaker) returns string + function growl(view self: Speaker) returns string + +struct Dog: + name: string + +implement Speaker for Dog: + function speak(view self: Dog) returns string: + return self.name +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 331), + "expected E0331, got: {:?}", + errors + ); + } + + #[test] + fn implement_block_signature_mismatch_reports_error() { + let errors = check_source_errors( + "\ +interface Speaker: + function speak(view self: Speaker) returns string + +struct Dog: + name: string + +implement Speaker for Dog: + function speak(self: Dog) returns int64: + return 1 +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 330), + "expected E0330, got: {:?}", + errors + ); } - #[test] - fn binary_operators_logic_error_on_non_bool() { - // 42 && true → error - let binop_span = sp(0, 10); + #[test] + fn string_interpolation_accepts_user_defined_displayable_types() { + let result = check_source_result( + "\ +interface Displayable: + function display(view self: Displayable) returns string + +struct User: + name: string - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Binary( - Box::new(Expr::IntLiteral(42, sp(0, 2))), - BinOp::And, - Box::new(Expr::BoolLiteral(true, sp(5, 10))), - binop_span, - ), - span: binop_span, - })], - span: sp(0, 10), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; +implement Displayable for User: + function display(view self: User) returns string: + return self.name - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns string: + User user = User(name: \"Ada\") + return \"user: {user}\" +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1); - assert_eq!(errors[0].code.code(), 301); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: variable declaration type matching - // --------------------------------------------------------------- - #[test] - fn var_decl_type_match_ok() { - // int64 x = 42 - let var_name_span = sp(6, 7); - let var_span = sp(0, 10); - - let mut env = TestEnv::new(); - env.def_var("x", var_name_span); - - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::VarDecl(VarDecl { - mutable: false, - ty: TypeExpr::Named(ident("int64", sp(0, 5))), - name: ident("x", var_name_span), - value: Expr::IntLiteral(42, sp(8, 10)), - span: var_span, - })], - span: sp(0, 10), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; + fn secret_value_can_be_declassified() { + let result = check_source_result( + "\ +function reveal(key: secret[string]) returns string: + return declassify key - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns string: + secret[string] api_key = \"abc\" + return reveal(api_key) +", + ); let errors: Vec<_> = result .diagnostics @@ -13808,121 +22433,108 @@ function piped() returns optional[int64]: } #[test] - fn var_decl_type_mismatch() { - // string x = 42 → error E0311 - let var_name_span = sp(7, 8); - let var_span = sp(0, 12); + fn declassify_requires_secret_type() { + let errors = check_source_errors( + "\ +function main() returns string: + return declassify \"abc\" +", + ); - let mut env = TestEnv::new(); - env.def_var("x", var_name_span); + assert!( + errors.iter().any(|d| d.code.code() == 601), + "expected E0601, got: {:?}", + errors + ); + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::VarDecl(VarDecl { - mutable: false, - ty: TypeExpr::Named(ident("string", sp(0, 6))), - name: ident("x", var_name_span), - value: Expr::IntLiteral(42, sp(9, 11)), - span: var_span, - })], - span: sp(0, 12), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; + #[test] + fn stdout_write_rejects_secret_values() { + let errors = check_source_errors( + "\ +function main(stdout: Stdout) returns nothing: + secret[string] api_key = \"abc\" + Stdout.write(view stdout, api_key) +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 600), + "expected E0600, got: {:?}", + errors + ); + } + + #[test] + fn string_interpolation_rejects_secret_values() { + let errors = check_source_errors( + "\ +function main() returns string: + secret[string] api_key = \"abc\" + return \"api key: {api_key}\" +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 600), + "expected E0600, got: {:?}", + errors + ); + } + + #[test] + fn pure_call_with_secret_argument_returns_secret() { + let result = check_source_result( + "\ +function echo(value: string) returns string: + return value + +function main() returns nothing: + secret[string] api_key = \"abc\" + secret[string] echoed = echo(api_key) + return nothing +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 311); - assert!(errors[0].message.contains("string")); - assert!(errors[0].message.contains("int64")); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: function call argument type checking - // --------------------------------------------------------------- - #[test] - fn function_call_correct_args() { - // function add(a: int64, b: int64) returns int64 - // add(1, 2) → no error, result is int64 - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - let call_ref_span = sp(20, 23); - let call_span = sp(20, 30); - - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("add", fn_name_span); - env.def_param("a", param_a_span); - env.def_param("b", param_b_span); - env.reference(call_ref_span, fn_def_id); + fn pure_call_with_secret_argument_cannot_be_assigned_to_public_type() { + let errors = check_source_errors( + "\ +function echo(value: string) returns string: + return value - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("add", fn_name_span), - type_params: vec![], - params: vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("int64", sp(106, 111))), - span: param_b_span, - }, - ], - return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("add", call_ref_span))), - vec![ - CallArg { - name: None, - value: Expr::IntLiteral(1, sp(24, 25)), - span: sp(24, 25), - }, - CallArg { - name: None, - value: Expr::IntLiteral(2, sp(27, 28)), - span: sp(27, 28), - }, - ], - call_span, - ), - span: call_span, - })], - span: sp(20, 30), - }, - exported: false, - span: sp(0, 30), - })], - span: sp(0, 30), - }; +function main() returns nothing: + secret[string] api_key = \"abc\" + string echoed = echo(api_key) + return nothing +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 311), + "expected E0311, got: {:?}", + errors + ); + } + + #[test] + fn secret_redact_returns_public_string() { + let result = check_source_result( + "\ +function main() returns string: + secret[string] api_key = \"abc\" + string masked = secret.redact(api_key) + return masked +", + ); let errors: Vec<_> = result .diagnostics @@ -13930,241 +22542,233 @@ function piped() returns optional[int64]: .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); assert!(errors.is_empty(), "unexpected errors: {:?}", errors); - assert_eq!(result.type_map[&call_span], TypeInterner::INT64); } #[test] - fn function_call_wrong_arg_type() { - // function add(a: int64, b: int64) returns int64 - // add(1, "hello") → error E0304 - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - let call_ref_span = sp(20, 23); - let call_span = sp(20, 35); - let bad_arg_span = sp(27, 34); + fn secret_compare_returns_bool() { + let result = check_source_result( + "\ +function main() returns bool: + secret[string] stored = \"abc\" + secret[string] computed = \"abc\" + return secret.compare(stored, computed) +", + ); - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("add", fn_name_span); - env.def_param("a", param_a_span); - env.def_param("b", param_b_span); - env.reference(call_ref_span, fn_def_id); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("add", fn_name_span), - type_params: vec![], - params: vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("int64", sp(106, 111))), - span: param_b_span, - }, - ], - return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("add", call_ref_span))), - vec![ - CallArg { - name: None, - value: Expr::IntLiteral(1, sp(24, 25)), - span: sp(24, 25), - }, - CallArg { - name: None, - value: Expr::StringLiteral("hello".to_string(), bad_arg_span), - span: bad_arg_span, - }, - ], - call_span, - ), - span: call_span, - })], - span: sp(20, 35), - }, - exported: false, - span: sp(0, 35), - })], - span: sp(0, 35), - }; + #[test] + fn secret_compare_requires_matching_secret_types() { + let errors = check_source_errors( + "\ +function main() returns bool: + secret[string] stored = \"abc\" + secret[bytes] computed = bytes.from_string(\"abc\") + return secret.compare(stored, computed) +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 304), + "expected E0304, got: {:?}", + errors + ); + } + + #[test] + fn secret_compare_rejects_unsupported_payloads() { + let errors = check_source_errors( + "\ +function main() returns bool: + secret[int64] stored = 1 + secret[int64] computed = 1 + return secret.compare(stored, computed) +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 604), + "expected E0604, got: {:?}", + errors + ); + } + + #[test] + fn secret_redact_requires_secret_argument() { + let errors = check_source_errors( + "\ +function main() returns string: + return secret.redact(\"abc\") +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 602), + "expected E0602, got: {:?}", + errors + ); + } + + #[test] + fn field_access_on_secret_struct_stays_secret() { + let result = check_source_result( + "\ +struct User: + name: string + +function main() returns nothing: + secret[User] user = User(name: \"Ada\") + secret[string] name = user.name + return nothing +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 304); // argument_type_mismatch - assert!(errors[0].message.contains("int64")); - assert!(errors[0].message.contains("string")); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } #[test] - fn function_call_wrong_arg_count() { - // function add(a: int64, b: int64) returns int64 - // add(1) → error E0303 - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - let call_ref_span = sp(20, 23); - let call_span = sp(20, 28); + fn mixed_secret_and_public_list_becomes_secret_element_list() { + let result = check_source_result( + "\ +function main() returns nothing: + secret[string] api_key = \"abc\" + list[secret[string]] items = list(\"prefix\", api_key, \"suffix\") + return nothing +", + ); - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("add", fn_name_span); - env.def_param("a", param_a_span); - env.def_param("b", param_b_span); - env.reference(call_ref_span, fn_def_id); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("add", fn_name_span), - type_params: vec![], - params: vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("int64", sp(106, 111))), - span: param_b_span, - }, - ], - return_type: Some(TypeExpr::Named(ident("int64", sp(112, 117)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("add", call_ref_span))), - vec![CallArg { - name: None, - value: Expr::IntLiteral(1, sp(24, 25)), - span: sp(24, 25), - }], - call_span, - ), - span: call_span, - })], - span: sp(20, 28), - }, - exported: false, - span: sp(0, 28), - })], - span: sp(0, 28), - }; + #[test] + fn string_join_rejects_list_of_secret_strings() { + let errors = check_source_errors( + "\ +function combine_strings(items: list[string], separator: string) returns string: + return separator - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns string: + secret[string] api_key = \"abc\" + list[secret[string]] items = list(\"prefix\", api_key, \"suffix\") + return combine_strings(items, \"-\") +", + ); - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 303); // argument_count_mismatch + assert!( + errors + .iter() + .any(|d| d.code.code() == 304 || d.code.code() == 305), + "expected argument/return type mismatch, got: {:?}", + errors + ); } - // --------------------------------------------------------------- - // Test: if condition must be bool - // --------------------------------------------------------------- + #[test] + fn filesystem_write_file_rejects_secret_string() { + let errors = check_source_errors( + "\ +function main(fs: Filesystem) returns nothing: + secret[string] api_key = \"abc\" + Filesystem.write_file(view fs, \"secret.txt\", api_key) handle error: + return nothing + return nothing +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 600), + "expected E0600, got: {:?}", + errors + ); + } #[test] - fn if_condition_must_be_bool() { - // if 42: → error E0306 - let cond_span = sp(3, 5); - let if_span = sp(0, 10); + fn json_serialize_blocks_struct_with_secret_fields() { + let errors = check_source_errors( + "\ +struct User: + id: string + api_key: secret[string] - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::If(IfStmt { - condition: Expr::IntLiteral(42, cond_span), - then_block: Block { - stmts: vec![], - span: sp(6, 10), - }, - else_ifs: vec![], - else_block: None, - span: if_span, - })], - span: sp(0, 10), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; +function main(view user: User) returns string: + return json.serialize[User](view user) +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 603), + "expected E0603, got: {:?}", + errors + ); + } + + #[test] + fn json_serialize_public_allows_struct_with_secret_fields() { + let result = check_source_result( + "\ +struct User: + id: string + api_key: secret[string] + +function main(view user: User) returns string: + return json.serialize_public[User](view user) +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 306); // condition_not_bool - assert!(errors[0].message.contains("int64")); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } #[test] - fn if_condition_bool_ok() { - // if true: → no error - let cond_span = sp(3, 7); + fn json_serialize_public_rejects_secret_wrapped_value() { + let errors = check_source_errors( + "\ +struct User: + id: string - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::If(IfStmt { - condition: Expr::BoolLiteral(true, cond_span), - then_block: Block { - stmts: vec![], - span: sp(8, 10), - }, - else_ifs: vec![], - else_block: None, - span: sp(0, 10), - })], - span: sp(0, 10), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; +function main() returns string: + secret[User] user = User(id: \"1\") + return json.serialize_public[User](user) +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 600), + "expected E0600, got: {:?}", + errors + ); + } + + #[test] + fn filesystem_read_file_returns_result_string() { + let result = check_source_result( + "\ +function main(fs: Filesystem) returns string: + string raw = Filesystem.read_file(view fs, \"config.json\") handle error: + default \"\" + return raw +", + ); let errors: Vec<_> = result .diagnostics @@ -14174,86 +22778,64 @@ function piped() returns optional[int64]: assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: return type checking - // --------------------------------------------------------------- - #[test] - fn return_type_mismatch() { - // function foo() returns int64: - // return "hello" - // → error E0305 - let fn_name_span = sp(0, 3); - let ret_span = sp(10, 25); - - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("foo", fn_name_span); - env.reference(fn_name_span, fn_def_id); - - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("foo", fn_name_span), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("int64", sp(100, 105)))), - body: Block { - stmts: vec![Stmt::Return(ReturnStmt { - value: Some(Expr::StringLiteral("hello".to_string(), sp(17, 24))), - span: ret_span, - })], - span: sp(10, 25), - }, - exported: false, - span: sp(0, 25), - })], - span: sp(0, 25), - }; + fn pure_function_lifts_secret_argument() { + let result = check_source_result( + "\ +function upper(value: string) returns string: + return value - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns secret[string]: + secret[string] api_key = \"abc\" + return upper(api_key) +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 305); // return_type_mismatch - assert!(errors[0].message.contains("int64")); - assert!(errors[0].message.contains("string")); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } #[test] - fn return_type_correct() { - // function foo() returns int64: - // return 42 - let fn_name_span = sp(0, 3); + fn impure_function_rejects_secret_argument_without_secret_param() { + let errors = check_source_errors( + "\ +function emit(view stdout: Stdout, value: string) returns nothing: + Stdout.write(view stdout, value) + return nothing - let mut env = TestEnv::new(); - let fn_def_id = env.def_func("foo", fn_name_span); - env.reference(fn_name_span, fn_def_id); +function main(stdout: Stdout) returns nothing: + secret[string] api_key = \"abc\" + emit(view stdout, api_key) + return nothing +", + ); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("foo", fn_name_span), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("int64", sp(100, 105)))), - body: Block { - stmts: vec![Stmt::Return(ReturnStmt { - value: Some(Expr::IntLiteral(42, sp(17, 19))), - span: sp(10, 19), - })], - span: sp(10, 19), - }, - exported: false, - span: sp(0, 19), - })], - span: sp(0, 19), - }; + assert!( + errors.iter().any(|d| d.code.code() == 600), + "expected E0600, got: {:?}", + errors + ); + } + + #[test] + fn impure_function_accepts_explicit_secret_param() { + let result = check_source_result( + "\ +function send_secret(view stdout: Stdout, value: secret[string]) returns nothing: + string redacted = secret.redact(value) + Stdout.write(view stdout, redacted) + return nothing - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main(stdout: Stdout) returns nothing: + secret[string] api_key = \"abc\" + send_secret(view stdout, api_key) + return nothing +", + ); let errors: Vec<_> = result .diagnostics @@ -14263,264 +22845,164 @@ function piped() returns optional[int64]: assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: unary operators - // --------------------------------------------------------------- - #[test] - fn unary_not_requires_bool() { - // not 42 → error - let unary_span = sp(0, 6); + fn refinement_type_assignment_requires_handle_error() { + let errors = check_source_errors( + "\ +type Port = int64 where value >= 1 && value <= 65535 - let env = TestEnv::new(); - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Unary( - UnaryOp::Not, - Box::new(Expr::IntLiteral(42, sp(4, 6))), - unary_span, - ), - span: unary_span, - })], - span: sp(0, 6), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; +function main() returns nothing: + Port port = 8080 + return nothing +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 333), + "expected E0333, got: {:?}", + errors + ); + } + + #[test] + fn refinement_type_assignment_with_handle_and_coarsen_typechecks() { + let result = check_source_result( + "\ +type Port = int64 where value >= 1 && value <= 65535 + +function main() returns int64: + Port port = 8080 handle error: + return 80 + return coarsen port +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1); - assert_eq!(errors[0].code.code(), 302); // unary_op_mismatch + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: for loop iterable check - // --------------------------------------------------------------- - #[test] - fn for_loop_requires_list() { - // for x in 42: → error E0307 - let var_span = sp(4, 5); - let iterable_span = sp(9, 11); - let for_span = sp(0, 15); - - let mut env = TestEnv::new(); - env.def_var("x", var_span); + fn simple_type_alias_behaves_like_base_type() { + let result = check_source_result( + "\ +type UserId = int64 - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![Stmt::For(ForStmt { - variable: ident("x", var_span), - value_variable: None, - view: false, - iterable: Expr::IntLiteral(42, iterable_span), - body: Block { - stmts: vec![], - span: sp(12, 15), - }, - span: for_span, - })], - span: sp(0, 15), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; +function id(value: UserId) returns UserId: + return value - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns int64: + UserId user_id = 42 + return id(user_id) +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 307); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: assignment type mismatch - // --------------------------------------------------------------- - #[test] - fn assignment_type_mismatch() { - // int64 x = 42 - // x = "hello" → error E0312 - let var_name_span = sp(6, 7); - let var_span = sp(0, 10); - let ref_x_span = sp(15, 16); - let assign_span = sp(15, 26); + fn bare_json_value_is_unknown() { + let errors = check_source_errors( + "\ +function main() returns JsonValue: + return nothing +", + ); - let mut env = TestEnv::new(); - let x_def = env.def_var("x", var_name_span); - env.reference(ref_x_span, x_def); + assert!( + errors + .iter() + .any(|diag| diag.code.code() == 309 && diag.message.contains("JsonValue")), + "expected bare JsonValue to be unknown, got {errors:?}" + ); + } - let module = Module { - items: vec![Item::Function(FunctionDef { - name: ident("test", sp(50, 54)), - type_params: vec![], - params: vec![], - return_type: Some(TypeExpr::Named(ident("nothing", sp(55, 62)))), - body: Block { - stmts: vec![ - Stmt::VarDecl(VarDecl { - mutable: true, - ty: TypeExpr::Named(ident("int64", sp(0, 5))), - name: ident("x", var_name_span), - value: Expr::IntLiteral(42, sp(8, 10)), - span: var_span, - }), - Stmt::Assign(AssignStmt { - target: Expr::Ident(ident("x", ref_x_span)), - value: Expr::StringLiteral("hello".to_string(), sp(19, 26)), - span: assign_span, - }), - ], - span: sp(0, 26), - }, - exported: false, - span: sp(50, 62), - })], - span: sp(0, 62), - }; + #[test] + fn coarsen_can_target_refinement_ancestors() { + let result = check_source_result( + "\ +type NonEmpty = string where value != \"\" +type Password = NonEmpty where value != \"short\" - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function main() returns string: + Password password = \"hunter42!\" handle error: + return \"\" + NonEmpty non_empty = coarsen password + return coarsen non_empty +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert_eq!(errors.len(), 1, "expected 1 error, got: {:?}", errors); - assert_eq!(errors[0].code.code(), 312); // assign_type_mismatch + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } - // --------------------------------------------------------------- - // Test: resolve_type_expr for generic types - // --------------------------------------------------------------- - #[test] - fn resolve_generic_types() { - // Verify that type expressions like list[int64], result[string, int64] resolve correctly. - let env = TestEnv::new(); - let resolve = env.into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + fn refinement_constraint_must_return_bool() { + let errors = check_source_errors( + "\ +type Broken = int64 where 42 - let list_int = checker.resolve_type_expr(&TypeExpr::Generic( - ident("list", sp(0, 4)), - vec![TypeExpr::Named(ident("int64", sp(5, 10)))], - sp(0, 11), - )); - assert_eq!( - *checker.interner.resolve(list_int), - Type::List(TypeInterner::INT64) +function main() returns nothing: + return nothing +", ); - let result_type = checker.resolve_type_expr(&TypeExpr::Generic( - ident("result", sp(0, 6)), - vec![ - TypeExpr::Named(ident("string", sp(7, 13))), - TypeExpr::Named(ident("int64", sp(15, 20))), - ], - sp(0, 21), - )); - assert_eq!( - *checker.interner.resolve(result_type), - Type::Result(TypeInterner::STRING, TypeInterner::INT64) + assert!( + errors.iter().any(|d| d.code.code() == 335), + "expected E0335, got: {:?}", + errors ); } - // --------------------------------------------------------------- - // Test: capability-based purity enforcement - // --------------------------------------------------------------- - - /// Helper: create a minimal function definition. - fn make_function(name: &str, name_span: Span, params: Vec, body: Block) -> FunctionDef { - FunctionDef { - name: ident(name, name_span), - type_params: vec![], - params, - return_type: Some(TypeExpr::Named(ident("nothing", sp(200, 207)))), - body, - exported: false, - span: Span::new(name_span.file, name_span.start, name_span.start + 50), - } - } - #[test] - fn pure_function_calling_pure_function_ok() { - // function helper() returns nothing: - // return nothing - // function caller() returns nothing: - // helper() - let helper_name_span = sp(0, 6); - let caller_name_span = sp(100, 106); - let call_ref_span = sp(110, 116); - let call_span = sp(110, 118); + fn struct_constructor_with_refinement_field_requires_handle() { + let errors = check_source_errors( + "\ +type Age = int64 where value >= 0 && value < 150 - let mut env = TestEnv::new(); - let helper_def = env.def_func("helper", helper_name_span); - let _caller_def = env.def_func("caller", caller_name_span); - env.reference(helper_name_span, helper_def); - env.reference(caller_name_span, _caller_def); - env.reference(call_ref_span, helper_def); +struct User: + age: Age - let module = Module { - items: vec![ - Item::Function(make_function( - "helper", - helper_name_span, - vec![], - Block { - stmts: vec![], - span: sp(10, 20), - }, - )), - Item::Function(make_function( - "caller", - caller_name_span, - vec![], - Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("helper", call_ref_span))), - vec![], - call_span, - ), - span: call_span, - })], - span: sp(107, 120), - }, - )), - ], - span: sp(0, 200), - }; +function main() returns nothing: + User user = User(age: 42) + return nothing +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 316), + "expected E0316, got: {:?}", + errors + ); + } + + #[test] + fn struct_constructor_with_refinement_field_handle_typechecks() { + let result = check_source_result( + "\ +type Age = int64 where value >= 0 && value < 150 + +struct User: + age: Age + +function main() returns nothing: + User user = User(age: 42) handle error: + return nothing + return nothing +", + ); let errors: Vec<_> = result .diagnostics @@ -14531,428 +23013,293 @@ function piped() returns optional[int64]: } #[test] - fn pure_function_calling_impure_function_error() { - // function writer(view out: Stdout) returns nothing: - // return nothing - // function caller() returns nothing: - // writer() ← E0500 - let writer_name_span = sp(0, 6); - let writer_param_span = sp(7, 10); - let caller_name_span = sp(100, 106); - let call_ref_span = sp(110, 116); - let call_span = sp(110, 118); - - let mut env = TestEnv::new(); - let writer_def = env.def_func("writer", writer_name_span); - let _caller_def = env.def_func("caller", caller_name_span); - env.def_param("out", writer_param_span); - env.reference(writer_name_span, writer_def); - env.reference(caller_name_span, _caller_def); - env.reference(call_ref_span, writer_def); - - let module = Module { - items: vec![ - Item::Function(make_function( - "writer", - writer_name_span, - vec![Param { - view: true, - mutable: false, - name: ident("out", writer_param_span), - ty: TypeExpr::View( - Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), - sp(7, 17), - ), - span: writer_param_span, - }], - Block { - stmts: vec![], - span: sp(20, 30), - }, - )), - Item::Function(make_function( - "caller", - caller_name_span, - vec![], - Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("writer", call_ref_span))), - vec![], - call_span, - ), - span: call_span, - })], - span: sp(107, 120), - }, - )), - ], - span: sp(0, 200), - }; + fn refined_return_type_accepts_base_expression() { + let result = check_source_result( + "\ +type Percentage = float64 where value >= 0.0 && value <= 100.0 - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); +function calculate_grade(score: int64, total: int64) returns Percentage: + float64 score_f = float64.from_int64(score) handle error: + default 0.0 + float64 total_f = float64.from_int64(total) handle error: + default 1.0 + return score_f / total_f * 100.0 +", + ); let errors: Vec<_> = result .diagnostics .iter() .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - // Expect E0500 (pure calls impure) and E0303 (arg count mismatch — caller - // passes 0 args but writer expects 1). We only assert E0500 exists. - let purity_errors: Vec<_> = errors.iter().filter(|d| d.code.code() == 500).collect(); - assert_eq!( - purity_errors.len(), - 1, - "expected 1 purity error (E0500), got: {:?}", - purity_errors + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } + + #[test] + fn refined_return_type_still_requires_handle_for_result_values() { + let errors = check_source_errors( + "\ +type Port = int64 where value >= 1 && value <= 65535 + +function parse_port(raw: string) returns Port: + return int64.from_string(raw) +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 316), + "expected E0316, got: {:?}", + errors ); - assert!(purity_errors[0].message.contains("caller")); - assert!(purity_errors[0].message.contains("writer")); } #[test] - fn impure_function_calling_impure_function_ok() { - // function writer(view out: Stdout) returns nothing: - // return nothing - // function caller(view out: Stdout) returns nothing: - // writer() ← ok, caller is also impure - let writer_name_span = sp(0, 6); - let writer_param_span = sp(7, 10); - let caller_name_span = sp(100, 106); - let caller_param_span = sp(107, 110); - let call_ref_span = sp(150, 156); - let call_span = sp(150, 158); + fn function_call_into_refinement_parameter_reports_boundary_error() { + let errors = check_source_errors( + "\ +type Password = string where string.char_count(value) > 8 - let mut env = TestEnv::new(); - let writer_def = env.def_func("writer", writer_name_span); - let _caller_def = env.def_func("caller", caller_name_span); - env.def_param("out", writer_param_span); - env.def_param("out2", caller_param_span); - env.reference(writer_name_span, writer_def); - env.reference(caller_name_span, _caller_def); - env.reference(call_ref_span, writer_def); +function create_user(password: Password) returns nothing: + return nothing - let module = Module { - items: vec![ - Item::Function(make_function( - "writer", - writer_name_span, - vec![Param { - view: true, - mutable: false, - name: ident("out", writer_param_span), - ty: TypeExpr::View( - Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), - sp(7, 17), - ), - span: writer_param_span, - }], - Block { - stmts: vec![], - span: sp(20, 30), - }, - )), - Item::Function(make_function( - "caller", - caller_name_span, - vec![Param { - view: true, - mutable: false, - name: ident("out2", caller_param_span), - ty: TypeExpr::View( - Box::new(TypeExpr::Named(ident("Stdout", sp(111, 117)))), - sp(107, 117), - ), - span: caller_param_span, - }], - Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("writer", call_ref_span))), - vec![], - call_span, - ), - span: call_span, - })], - span: sp(140, 160), - }, - )), - ], - span: sp(0, 300), - }; +function main() returns nothing: + create_user(\"hunter42!\") + return nothing +", + ); - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + assert!( + errors.iter().any(|d| d.code.code() == 333), + "expected E0333, got: {:?}", + errors + ); + } - // No purity errors expected (E0500). - let purity_errors: Vec<_> = result + #[test] + fn bitfield_constructor_with_literals_typechecks() { + let result = check_source_result( + "\ +bitfield TcpFlags: + syn: 1 bit + ack: 1 bit + +function main() returns int64: + TcpFlags flags = TcpFlags(syn: 0, ack: 1) + return flags.ack +", + ); + + let errors: Vec<_> = result .diagnostics .iter() - .filter(|d| d.code.code() == 500) + .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert!( - purity_errors.is_empty(), - "unexpected purity errors: {:?}", - purity_errors - ); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } #[test] - fn function_with_stdout_param_is_impure() { - // function printer(view out: Stdout) returns nothing - // This is a unit-level check: the purity map marks this function as impure. - let fn_name_span = sp(0, 7); - let param_span = sp(8, 11); + fn bitfield_constructor_with_dynamic_int_requires_handle() { + let errors = check_source_errors( + "\ +bitfield TcpFlags: + syn: 1 bit + ack: 1 bit - let mut env = TestEnv::new(); - let fn_def = env.def_func("printer", fn_name_span); - env.def_param("out", param_span); - env.reference(fn_name_span, fn_def); +function main(bit: int64) returns nothing: + TcpFlags flags = TcpFlags(syn: bit, ack: 0) + return nothing +", + ); - let func = make_function( - "printer", - fn_name_span, - vec![Param { - view: true, - mutable: false, - name: ident("out", param_span), - ty: TypeExpr::View( - Box::new(TypeExpr::Named(ident("Stdout", sp(12, 18)))), - sp(8, 18), - ), - span: param_span, - }], - Block { - stmts: vec![], - span: sp(20, 30), - }, + assert!( + errors.iter().any(|d| d.code.code() == 316), + "expected E0316, got: {:?}", + errors ); + } - let module = Module { - items: vec![Item::Function(func)], - span: sp(0, 100), - }; + #[test] + fn bitfield_literal_out_of_range_reports_error() { + let errors = check_source_errors( + "\ +bitfield ColorChannel: + red: 8 bits + green: 8 bits - let resolve = env.into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); - checker.check_module(&module); +function main() returns nothing: + ColorChannel color = ColorChannel(red: 300, green: 1) + return nothing +", + ); - // The purity map should mark "printer" as impure. - assert_eq!( - checker.purity_map.get("printer").copied(), - Some(false), - "function with Stdout param should be impure" + assert!( + errors.iter().any(|d| d.code.code() == 337), + "expected E0337, got: {:?}", + errors ); } #[test] - fn function_without_capability_params_is_pure() { - // function add(a: int64, b: int64) returns nothing - let fn_name_span = sp(0, 3); - let param_a_span = sp(4, 5); - let param_b_span = sp(6, 7); - - let mut env = TestEnv::new(); - let fn_def = env.def_func("add", fn_name_span); - env.def_param("a", param_a_span); - env.def_param("b", param_b_span); - env.reference(fn_name_span, fn_def); + fn bitfield_payload_must_be_list_of_uint8() { + let errors = check_source_errors( + "\ +bitfield Packet: + header: 8 bits + payload: bytes +", + ); - let func = make_function( - "add", - fn_name_span, - vec![ - Param { - view: false, - mutable: false, - name: ident("a", param_a_span), - ty: TypeExpr::Named(ident("int64", sp(100, 105))), - span: param_a_span, - }, - Param { - view: false, - mutable: false, - name: ident("b", param_b_span), - ty: TypeExpr::Named(ident("int64", sp(106, 111))), - span: param_b_span, - }, - ], - Block { - stmts: vec![], - span: sp(10, 20), - }, + assert!( + errors.iter().any(|d| d.code.code() == 336), + "expected E0336, got: {:?}", + errors ); + } - let module = Module { - items: vec![Item::Function(func)], - span: sp(0, 100), - }; + #[test] + fn bitfield_enum_annotation_rejects_negative_discriminant() { + let errors = check_source_errors( + "\ +enum SignedProtocol: + invalid = -1 + tcp = 6 - let resolve = env.into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); - checker.check_module(&module); +bitfield Header: + protocol: 8 bits as SignedProtocol - assert_eq!( - checker.purity_map.get("add").copied(), - Some(true), - "function without capability params should be pure" +function main() returns nothing: + return nothing +", + ); + + assert!( + errors.iter().any(|d| d.code.code() == 336), + "expected E0336, got: {:?}", + errors ); } #[test] - fn verify_block_calling_impure_function_error() { - // function writer(view out: Stdout) returns nothing: - // return nothing - // verify test_writer: - // writer() ← E0501 - let writer_name_span = sp(0, 6); - let writer_param_span = sp(7, 10); - let call_ref_span = sp(110, 116); - let call_span = sp(110, 118); - let verify_span = sp(100, 130); + fn bitfield_binary_roundtrip_typechecks() { + let result = check_source_result( + "\ +bitfield network IpHeader: + version: 4 bits + header_length: 4 bits + total_length: 16 bits - let mut env = TestEnv::new(); - let writer_def = env.def_func("writer", writer_name_span); - env.def_param("out", writer_param_span); - env.reference(writer_name_span, writer_def); - env.reference(call_ref_span, writer_def); +function main() returns int64: + IpHeader header = IpHeader(version: 4, header_length: 5, total_length: 500) + bytes raw = IpHeader.to_bytes(header) + IpHeader decoded = IpHeader.from_bytes(raw) handle error: + return 0 + return decoded.total_length +", + ); - let module = Module { - items: vec![ - Item::Function(make_function( - "writer", - writer_name_span, - vec![Param { - view: true, - mutable: false, - name: ident("out", writer_param_span), - ty: TypeExpr::View( - Box::new(TypeExpr::Named(ident("Stdout", sp(11, 17)))), - sp(7, 17), - ), - span: writer_param_span, - }], - Block { - stmts: vec![], - span: sp(20, 30), - }, - )), - Item::Verify(VerifyBlock { - name: ident("test_writer", sp(100, 111)), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("writer", call_ref_span))), - vec![], - call_span, - ), - span: call_span, - })], - span: sp(112, 130), - }, - span: verify_span, - }), - ], - span: sp(0, 200), - }; + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + } - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + #[test] + fn namespaced_bitfield_alias_calls_record_closed_intrinsic_identities() { + let result = check_source_result( + "\ +namespace packet +export bitfield Header: + version: 8 bits +namespace app +function roundtrip() returns int64: + use packet as p + p.Header header = p.Header(version: 4) + bytes raw = p.Header.to_bytes(header) + p.Header decoded = p.Header.from_bytes(raw) handle error: + return 0 + return decoded.version +", + ); - let verify_errors: Vec<_> = result + let errors = result .diagnostics .iter() - .filter(|d| d.code.code() == 501) - .collect(); + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + let intrinsic_ids = result + .intrinsic_ids + .values() + .copied() + .collect::>(); assert_eq!( - verify_errors.len(), - 1, - "expected 1 verify purity error (E0501), got: {:?}", - verify_errors + intrinsic_ids, + HashSet::from([IntrinsicId::BitfieldFromBytes, IntrinsicId::BitfieldToBytes,]) ); - assert!(verify_errors[0].message.contains("test_writer")); - assert!(verify_errors[0].message.contains("writer")); } #[test] - fn verify_block_calling_pure_function_ok() { - // function helper() returns nothing: - // return nothing - // verify test_helper: - // helper() ← ok - let helper_name_span = sp(0, 6); - let call_ref_span = sp(110, 116); - let call_span = sp(110, 118); - let verify_span = sp(100, 130); + fn bitfield_from_bytes_requires_handle() { + let errors = check_source_errors( + "\ +bitfield network IpHeader: + version: 4 bits + header_length: 4 bits + total_length: 16 bits - let mut env = TestEnv::new(); - let helper_def = env.def_func("helper", helper_name_span); - env.reference(helper_name_span, helper_def); - env.reference(call_ref_span, helper_def); +function main() returns nothing: + bytes raw = IpHeader.to_bytes(IpHeader(version: 4, header_length: 5, total_length: 500)) + IpHeader decoded = IpHeader.from_bytes(raw) + return nothing +", + ); - let module = Module { - items: vec![ - Item::Function(make_function( - "helper", - helper_name_span, - vec![], - Block { - stmts: vec![], - span: sp(10, 20), - }, - )), - Item::Verify(VerifyBlock { - name: ident("test_helper", sp(100, 111)), - body: Block { - stmts: vec![Stmt::Expr(ExprStmt { - expr: Expr::Call( - Box::new(Expr::Ident(ident("helper", call_ref_span))), - vec![], - call_span, - ), - span: call_span, - })], - span: sp(112, 130), - }, - span: verify_span, - }), - ], - span: sp(0, 200), - }; + assert!( + errors.iter().any(|d| d.code.code() == 316), + "expected E0316, got: {:?}", + errors + ); + } - let resolve = env.into_resolve_result(); - let result = check(&module, &resolve); + #[test] + fn trace_statement_reads_variable_without_error() { + let result = check_source_result( + "\ +function main() returns nothing: + int64 total = 42 + trace total +", + ); - let purity_errors: Vec<_> = result + let errors: Vec<_> = result .diagnostics .iter() - .filter(|d| d.code.code() == 500 || d.code.code() == 501) + .filter(|d| d.severity == jett_diagnostics::Severity::Error) .collect(); - assert!( - purity_errors.is_empty(), - "unexpected purity errors: {:?}", - purity_errors - ); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } #[test] - fn mutual_block_allows_mutual_recursion() { + fn enum_with_explicit_discriminants_typechecks() { let result = check_source_result( "\ -mutual: - function is_even(n: int64) returns bool - function is_odd(n: int64) returns bool +enum IpProtocol: + icmp = 1 + tcp = 6 + udp = 17 -function is_even(n: int64) returns bool: - if n == 0: - return true - return is_odd(n - 1) +bitfield network IpHeader: + protocol: 8 bits as IpProtocol -function is_odd(n: int64) returns bool: - if n == 0: - return false - return is_even(n - 1) +function main() returns nothing: + IpHeader header = IpHeader(protocol: IpProtocol.tcp) + bytes raw = IpHeader.to_bytes(header) + IpHeader decoded = IpHeader.from_bytes(raw) handle error: + return nothing + IpProtocol protocol = decoded.protocol + return nothing ", ); @@ -14965,11 +23312,12 @@ function is_odd(n: int64) returns bool: } #[test] - fn mutual_block_missing_definition_reports_error() { + fn duplicate_enum_discriminant_reports_error() { let errors = check_source_errors( "\ -mutual: - function is_even(n: int64) returns bool +enum Bad: + first = 1 + second = 1 function main() returns nothing: return nothing @@ -14977,1761 +23325,5068 @@ function main() returns nothing: ); assert!( - errors.iter().any(|d| d.code.code() == 325), - "expected E0325, got: {:?}", + errors.iter().any(|d| d.code.code() == 339), + "expected E0339, got: {:?}", errors ); } #[test] - fn mutual_block_signature_mismatch_reports_error() { + fn enum_discriminant_requires_unit_variant() { let errors = check_source_errors( "\ -mutual: - function is_even(n: int64) returns bool +enum Bad: + named(value: int64) = 1 -function is_even(value: string) returns bool: - return true +function main() returns nothing: + return nothing ", ); assert!( - errors.iter().any(|d| d.code.code() == 326), - "expected E0326, got: {:?}", + errors.iter().any(|d| d.code.code() == 338), + "expected E0338, got: {:?}", errors ); } + fn check_resource_payload_source_in_profile(source: &str, release: bool) -> CheckResult { + let parsed = parse(source, FileId::new(STDLIB_FILE_ID_START)); + assert!( + parsed.errors.is_empty(), + "parse errors: {:?}", + parsed.errors + ); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| { diagnostic.severity != jett_diagnostics::Severity::Error }), + "resolve errors: {:?}", + resolved.diagnostics + ); + assert!(resolved.resource_kernels.is_empty()); + check_with_options(&parsed.module, &resolved, CheckOptions { release }) + } + + #[test] + fn resource_views_cannot_enter_owning_constructor_payloads() { + let prefix = + "namespace audit\nexport resource FileHandle\nstruct Holder:\n token: FileHandle\n"; + for body in [ + "function reject(view token: FileHandle) returns optional[FileHandle]:\n return some(view token)\n", + "function reject(view token: FileHandle) returns list[FileHandle]:\n return list(view token)\n", + "function reject(view token: FileHandle) returns Holder:\n return Holder(token: view token)\n", + "function reject(view token: FileHandle) returns result[FileHandle, string]:\n return ok(view token)\n", + "function reject(view token: FileHandle) returns result[string, FileHandle]:\n return fail(view token)\n", + "function reject(view token: FileHandle) returns optional[FileHandle]:\n FileHandle alias = view token\n FileHandle forwarded = alias\n return some(forwarded)\n", + "function reject(view holder: Holder) returns list[Holder]:\n Holder alias = view holder\n return list(alias)\n", + "function reject(view token: FileHandle) returns list[FileHandle]:\n FileHandle alias = view token\n return list(alias)\n", + "function reject(view token: FileHandle) returns Holder:\n FileHandle alias = view token\n return Holder(token: alias)\n", + "function reject(first: FileHandle, view second: FileHandle) returns list[FileHandle]:\n return list(first, view second)\n", + "function reject(view token: FileHandle) returns optional[FileHandle]:\n return some((view token))\n", + ] { + let source = format!("{prefix}{body}"); + for release in [false, true] { + let result = check_resource_payload_source_in_profile(&source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "release={release}, {body}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("resource view"), "{errors:?}"); + } + } + } + + #[test] + fn resource_view_payload_guards_validate_inferred_unused_and_generic_bodies() { + let prefix = + "namespace audit\nexport resource FileHandle\nstruct Holder:\n token: FileHandle\n"; + for expression in [ + "some(view token)", + "list(view token)", + "ok(view token)", + "fail(view token)", + "Holder(token: view token)", + "view some(view token)", + "view list(view token)", + "view Holder(token: view token)", + ] { + let view_argument = if expression.starts_with("view ") { + expression.to_owned() + } else { + format!("view {expression}") + }; + let source = format!( + "{prefix}function observe[T](view value: T) returns nothing:\n return nothing\nfunction unused(view token: FileHandle) returns nothing:\n observe({view_argument})\n return nothing\n" + ); + for release in [false, true] { + let result = check_resource_payload_source_in_profile(&source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "{expression}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + } + } + let generic = r#"namespace audit +export resource FileHandle +function wrap[T](view token: T) returns optional[T]: + T alias = view token + return some(alias) +function unused(view token: FileHandle) returns nothing: + optional[FileHandle] payload = wrap[FileHandle](view token) + return nothing +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(generic, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + let start = usize::try_from(errors[0].span.start).expect("source byte offset"); + let end = usize::try_from(errors[0].span.end).expect("source byte offset"); + assert_eq!(generic.get(start..end), Some("alias")); + } + } + + #[test] + fn owned_resource_payloads_views_absence_and_nonresource_constructors_remain_valid() { + let source = r#"namespace audit +export resource FileHandle +struct Holder: + token: FileHandle + count: int64 +struct Phantom[T]: + count: int64 +function phantom(view item: Phantom[FileHandle]) returns optional[Phantom[FileHandle]]: + return some(view item) +function own_optional(token: FileHandle) returns optional[FileHandle]: + return some(token) +function own_list(token: FileHandle) returns list[FileHandle]: + return list(token) +function own_result(token: FileHandle) returns result[FileHandle, string]: + return ok(token) +function own_failure(token: FileHandle) returns result[string, FileHandle]: + return fail(token) +function own_holder(token: FileHandle) returns Holder: + return Holder(token: token, count: 7) +function borrow(view holder: Holder, view token: FileHandle) returns nothing: + FileHandle alias = view token + FileHandle forwarded = alias + trace forwarded + FileHandle field_alias = view holder.token + trace field_alias + int64 count = holder.count + optional[FileHandle] absent = none + list[FileHandle] empty = list() + return nothing +function scalar(view count: int64) returns optional[int64]: + return some(view count) +function unchanged(view values: list[int64]) returns optional[list[int64]]: + return some(view values) +function type_name() returns string: + return type.name[FileHandle]() +function type_kind() returns bool: + return type.kind_tag[FileHandle]() == TypeKind.resource_type +function type_fields() returns list[TypeField]: + return type.fields[FileHandle]() +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "release={release}: {errors:?}"); + } + } + + #[test] + fn resource_roots_reject_value_reflection_without_banning_aggregate_metadata() { + let forbidden = r#"namespace audit +export resource FileHandle +function opaque_variant(view token: FileHandle) returns TypeVariant: + return type.variant_value[FileHandle](view token) +function opaque_state(view token: FileHandle) returns TypeMachineState: + return type.machine_state_value[FileHandle](view token) +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(forbidden, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 300) + ); + assert!( + errors.iter().all(|diagnostic| { + diagnostic.message.contains("non-resource value for type.") + && diagnostic.message.contains("audit.FileHandle") + }), + "{errors:?}" + ); + } + let permitted = r#"namespace audit +export resource FileHandle +enum Slot: + empty + occupied(token: FileHandle) +function aggregate_metadata(view slot: Slot) returns TypeVariant: + return type.variant_value[Slot](view slot) +function resource_metadata() returns string: + return type.name[FileHandle]() +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(permitted, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "{errors:?}"); + } + } + + #[test] + fn resource_root_print_refusal_keeps_release_policy_separate() { + let source = r#"namespace audit +export resource FileHandle +function forbidden_output(view token: FileHandle) returns nothing: + print(view token) + println(view token) + return nothing +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), if release { 4 } else { 2 }, "{errors:?}"); + assert_eq!( + errors + .iter() + .filter(|diagnostic| { + diagnostic.code.code() == 300 + && diagnostic.message.contains("ordinary printing") + }) + .count(), + 2, + "{errors:?}" + ); + assert_eq!( + errors + .iter() + .filter(|diagnostic| { diagnostic.code.code() == 362 }) + .count(), + if release { 2 } else { 0 }, + "{errors:?}" + ); + } + } + + #[test] + fn resource_root_print_guard_does_not_select_nested_or_absent_rendering() { + let source = r#"namespace audit +export resource FileHandle +struct Holder: + token: FileHandle +struct Hidden: + token: secret[FileHandle] +function nested(view holder: Holder, view hidden: Hidden) returns nothing: + println(view holder) + println(view hidden) + return nothing +function absence() returns nothing: + optional[FileHandle] missing = none + list[FileHandle] empty = list() + println(view missing) + println(view empty) + return nothing +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), if release { 4 } else { 0 }, "{errors:?}"); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 362) + ); + } + } + + #[test] + fn resource_map_views_cannot_become_owned_values_in_either_typing_route() { + let prefix = "namespace audit\nexport resource FileHandle\n"; + for body in [ + "function reject(view token: FileHandle) returns map[string, FileHandle]:\n return map(\"one\": view token)\n", + "function reject(view token: FileHandle) returns map[string, FileHandle]:\n FileHandle alias = view token\n return map(\"one\": alias)\n", + "function reject(first: FileHandle, view token: FileHandle) returns map[string, FileHandle]:\n return map(\"one\": first, \"two\": view token)\n", + "function inspect[T](view value: T) returns nothing:\n return nothing\nfunction reject(view token: FileHandle) returns nothing:\n inspect(view map(\"one\": view token))\n return nothing\n", + ] { + let source = format!("{prefix}{body}"); + for release in [false, true] { + let result = check_resource_payload_source_in_profile(&source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "release={release}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("resource view"), "{errors:?}"); + } + } + } + #[test] - fn user_defined_struct_constructor_and_field_access_typecheck_cleanly() { - let result = check_source_result( - "\ -struct Point: - x: int64 - y: int64 - -function sum(view point: Point) returns int64: - return point.x + point.y - -function main() returns int64: - Point point = Point(x: 1, y: 2) - return sum(view point) -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn resource_machine_payloads_and_viewed_owned_sources_are_rejected() { + let prefix = r#"namespace audit +export resource FileHandle +machine Envelope: + states: + empty + holding(token: FileHandle) + transitions: + empty to holding + holding to empty +"#; + for (body, expected_code) in [ + ( + "function reject(view token: FileHandle) returns Envelope at holding:\n return Envelope(holding, view token)\n", + 401, + ), + ( + "function reject(view token: FileHandle) returns Envelope at holding:\n FileHandle alias = view token\n return Envelope(holding, alias)\n", + 401, + ), + ( + "function reject(view token: FileHandle) returns Envelope at holding:\n return Envelope.transition(Envelope(empty), holding, view token)\n", + 401, + ), + ( + "function reject(view token: FileHandle) returns Envelope at holding:\n FileHandle alias = view token\n return Envelope.transition(Envelope(empty), holding, alias)\n", + 401, + ), + ( + "function reject(view source: Envelope at holding) returns Envelope at empty:\n return Envelope.transition(view source, empty)\n", + 375, + ), + ( + "function reject(view source: Envelope at holding) returns Envelope at empty:\n return Envelope.transition(source, empty)\n", + 401, + ), + ( + "function reject(view source: Envelope at holding) returns Envelope at empty:\n Envelope at holding alias = view source\n return Envelope.transition(alias, empty)\n", + 401, + ), + ] { + let source = format!("{prefix}{body}"); + for release in [false, true] { + let result = check_resource_payload_source_in_profile(&source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "release={release}, {body}: {errors:?}"); + assert_eq!(errors[0].code.code(), expected_code, "{errors:?}"); + } + } } #[test] - fn struct_method_call_typechecks_cleanly() { - let result = check_source_result( - "\ -struct Point: - x: int64 - y: int64 - - function total(view self: Point) returns int64: - return self.x + self.y + fn resource_owned_map_machine_and_existing_enum_boundaries_are_preserved() { + let source = r#"namespace audit +export resource FileHandle +enum Packet: + empty + holding(token: FileHandle) +machine Envelope: + states: + empty + holding(token: FileHandle) + transitions: + empty to holding + holding to empty +function map_owned(token: FileHandle) returns map[string, FileHandle]: + return map("one": token) +function map_absent() returns map[string, FileHandle]: + return map() +function machine_owned(token: FileHandle) returns Envelope at holding: + return Envelope(holding, token) +function machine_absent() returns Envelope at empty: + return Envelope(empty) +function transition_owned(source: Envelope at holding) returns Envelope at empty: + return Envelope.transition(source, empty) +function target_owned(source: Envelope at empty, token: FileHandle) returns Envelope at holding: + return Envelope.transition(source, holding, token) +function unchanged(view source: Envelope at empty, token: FileHandle) returns Envelope at holding: + return Envelope.transition(view source, holding, token) +function enum_owned(token: FileHandle) returns Packet: + return Packet.holding(token) +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert!(errors.is_empty(), "release={release}: {errors:?}"); + } + let prefix = "namespace audit\nexport resource FileHandle\nenum Packet:\n empty\n holding(token: FileHandle)\n"; + for (body, expected_code) in [ + ( + "function reject(view token: FileHandle) returns Packet:\n return Packet.holding(view token)\n", + 375, + ), + ( + "function reject(view token: FileHandle) returns Packet:\n FileHandle alias = view token\n return Packet.holding(alias)\n", + 401, + ), + ] { + let source = format!("{prefix}{body}"); + for release in [false, true] { + let result = check_resource_payload_source_in_profile(&source, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), expected_code, "{errors:?}"); + } + } + } -function main() returns int64: - Point point = Point(x: 1, y: 2) - return Point.total(view point) -", - ); + #[test] + fn resource_payload_diagnostics_replace_only_the_exact_legacy_counterpart() { + let bare = r#"namespace audit +export resource FileHandle +struct Holder: + token: FileHandle +function reject(view holder: Holder) returns list[Holder]: + return list(holder) +"#; + let independent = r#"namespace audit +export resource FileHandle +function consume(token: FileHandle) returns nothing: + return nothing +function reject(view viewed: FileHandle, owned: FileHandle) returns nothing: + some(viewed) + consume(owned) + consume(owned) + return nothing +"#; + for release in [false, true] { + let result = check_resource_payload_source_in_profile(bare, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 401); + assert!(errors[0].message.contains("resource view"), "{errors:?}"); - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let result = check_resource_payload_source_in_profile(independent, release); + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect(); + assert_eq!(errors.len(), 3, "{errors:?}"); + assert_eq!( + errors + .iter() + .filter(|diagnostic| diagnostic.code.code() == 342) + .count(), + 1, + "{errors:?}" + ); + assert_eq!( + errors + .iter() + .filter(|diagnostic| { + diagnostic.code.code() == 401 + && diagnostic.message.contains("resource view") + }) + .count(), + 1, + "{errors:?}" + ); + assert_eq!( + errors + .iter() + .filter(|diagnostic| { diagnostic.code.code() == 400 }) + .count(), + 1, + "{errors:?}" + ); + } } #[test] - fn named_function_arguments_record_parameter_order() { - let result = check_source_result( - "\ -function subtract(left: int64, right: int64) returns int64: - return left - right - -function main() returns int64: - return subtract(right: 2, left: 7) -", + fn stdlib_resource_is_nominal_and_reflected_as_resource() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\n", + FileId::new(STDLIB_FILE_ID_START), ); let errors: Vec<_> = result .diagnostics .iter() .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); - let order = result - .call_argument_orders - .values() - .next() - .expect("named call must export its parameter order"); - assert_eq!(order.source_indices, vec![1, 0]); - } - - #[test] - fn named_function_arguments_reject_unknown_and_duplicate_parameters() { - let unknown = check_source_errors( - "\ -function subtract(left: int64, right: int64) returns int64: - return left - right -function main() returns int64: - return subtract(left: 7, other: 2) -", - ); - assert!( - unknown - .iter() - .any(|diagnostic| diagnostic.code.code() == 368), - "expected E0368, got: {:?}", - unknown - ); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); - let duplicate = check_source_errors( - "\ -function subtract(left: int64, right: int64) returns int64: - return left - right -function main() returns int64: - return subtract(left: 7, left: 2) -", - ); + let resource_type = result + .interner + .type_ids() + .find(|type_id| { + matches!( + result.interner.resolve(*type_id), + Type::Resource(name) if name == "io.FileHandle" + ) + }) + .expect("resource should have a distinct nominal semantic type"); + let info = result + .reflection_metadata + .get_type_info_for_id(resource_type) + .expect("resource reflection metadata should be present"); + assert_eq!(info.type_name, "io.FileHandle"); + assert_eq!(info.kind, "resource"); assert!( - duplicate - .iter() - .any(|diagnostic| diagnostic.code.code() == 369), - "expected E0369, got: {:?}", - duplicate + result + .reflection_metadata + .get_type_fields_for_id(resource_type) + .is_none(), + "resources must not expose aggregate fields" ); } #[test] - fn pipeline_arguments_and_method_targets_are_checked_in_parameter_order() { - let result = check_source_result( - "\ -struct Score: - value: int64 - function add(view self: Score, bonus: int64) returns int64: - return self.value + bonus -function choose[T](first: T, second: T, third: T) returns T: - return first -function main() returns int64: - Score score = Score(value: 7) - int64 chosen = 1 into choose[int64](third: 3, second: 2) - return score into view Score.add(bonus: chosen) -", + fn stdlib_resource_kind_tag_is_a_checked_enum_variant() { + let result = check_source_result_with_file_id( + r#"namespace io +export resource FileHandle +type HandleAlias = FileHandle +function resource_kind_matches() returns bool: + return type.kind_tag[FileHandle]() == TypeKind.resource_type +function resource_info_matches() returns bool: + return type.info[FileHandle]().kind_tag == TypeKind.resource_type +function alias_kind_matches() returns bool: + return type.kind_tag[HandleAlias]() == TypeKind.alias_type +"#, + FileId::new(STDLIB_FILE_ID_START), ); let errors: Vec<_> = result .diagnostics .iter() .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); - assert!( - result - .call_argument_orders - .values() - .any(|order| order.source_indices == vec![0, 2, 1]) - ); - assert_eq!(result.method_calls.len(), 1); + assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); } #[test] - fn struct_constructor_missing_field_reports_error() { - let errors = check_source_errors( - "\ -struct Point: - x: int64 - y: int64 - -function main() returns Point: - return Point(x: 1) -", + fn stdlib_resource_clone_is_rejected() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\nfunction duplicate(value: FileHandle) returns FileHandle:\n return clone value\n", + FileId::new(STDLIB_FILE_ID_START), ); + assert!(result.diagnostics.iter().any(|diagnostic| { + diagnostic.code.code() == 364 + && diagnostic.severity == jett_diagnostics::Severity::Error + })); + } - assert!( - errors.iter().any(|d| d.code.code() == 321), - "expected E0321, got: {:?}", - errors + #[test] + fn stdlib_resource_nested_clone_is_rejected() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\nfunction duplicate(value: optional[FileHandle]) returns optional[FileHandle]:\n return clone value\n", + FileId::new(STDLIB_FILE_ID_START), ); + assert!(result.diagnostics.iter().any(|diagnostic| { + diagnostic.code.code() == 364 + && diagnostic.severity == jett_diagnostics::Severity::Error + })); } #[test] - fn unknown_struct_field_reports_error() { - let errors = check_source_errors( - "\ -struct Point: - x: int64 - y: int64 - -function main(view point: Point) returns int64: - return point.z -", + fn stdlib_resource_comparison_is_rejected() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\nfunction same(view left: FileHandle, view right: FileHandle) returns bool:\n return left == right\n", + FileId::new(STDLIB_FILE_ID_START), ); + assert!(result.diagnostics.iter().any(|diagnostic| { + diagnostic.code.code() == 365 + && diagnostic.severity == jett_diagnostics::Severity::Error + })); + } - assert!( - errors.iter().any(|d| d.code.code() == 319), - "expected E0319, got: {:?}", - errors + #[test] + fn stdlib_resource_interface_implementation_is_rejected() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\ninterface Marker:\n function mark(view self: Marker) returns bool\nimplement Marker for FileHandle:\n function mark(view self: FileHandle) returns bool:\n return true\n", + FileId::new(STDLIB_FILE_ID_START), ); + assert!(result.diagnostics.iter().any(|diagnostic| { + diagnostic.code.code() == 366 + && diagnostic.severity == jett_diagnostics::Severity::Error + })); } #[test] - fn generic_type_argument_arity_still_checks_value_expressions() { - let errors = check_source_errors( - "\ -function identity[T](value: T) returns T: - return value - -function main() returns nothing: - identity[int64, string](true + \"bad\") -", + fn stdlib_resource_refinement_is_rejected() { + let result = check_source_result_with_file_id( + "namespace io\nexport resource FileHandle\ntype OpenHandle = FileHandle where true\n", + FileId::new(STDLIB_FILE_ID_START), ); + assert!(result.diagnostics.iter().any(|diagnostic| { + diagnostic.code.code() == 367 + && diagnostic.severity == jett_diagnostics::Severity::Error + })); + } + #[test] + fn generic_instantiation_manifest_is_ordered_deduplicated_and_nested() { + let result = check_source_result( + "function inner[T](value: T) returns T:\n return value\nfunction outer[T](value: T) returns T:\n return inner[T](value)\nfunction main() returns int64:\n int64 first = outer[int64](1)\n int64 second = outer[int64](2)\n return first + second\n", + ); assert!( - errors.iter().any(|d| d.code.code() == 301), - "expected nested binary expression diagnostic, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + assert_eq!(result.generic_function_instantiations.len(), 2); + let outer = &result.generic_function_instantiations[0]; + let inner = &result.generic_function_instantiations[1]; + assert_eq!(outer.concrete_args, vec![TypeInterner::INT64]); + assert_eq!(inner.concrete_args, vec![TypeInterner::INT64]); + assert_eq!(outer.parameter_types, vec![TypeInterner::INT64]); + assert_eq!(outer.return_type, TypeInterner::INT64); + assert!(!outer.type_map.is_empty()); + assert_eq!(outer.generic_calls.len(), 1); + let nested_call = outer.generic_calls.values().next().unwrap(); + assert_eq!(nested_call.definition, inner.definition); + assert_eq!(nested_call.concrete_args, vec![TypeInterner::INT64]); + assert_eq!(result.generic_calls.len(), 2); } #[test] - fn enum_variant_construction_and_match_typecheck_cleanly() { + fn generic_closure_factories_export_concrete_instantiations() { let result = check_source_result( - "\ -enum Shape: - circle(radius: int64) - rect(width: int64, height: int64) - -function area(shape: Shape) returns int64: - match shape: - circle(radius): - return radius * radius - rect(width, height): - return width * height - + r#"function make_constant[T](value: T) returns function() returns T: + return function() returns T: return value +function make_identity[T]() returns function(T) returns T: + return function(value: T) returns T: return value +function make_optional[T]() returns function(T) returns optional[T]: + return function(value: T) returns optional[T]: return some(value) function main() returns int64: - Shape shape = Shape.circle(3) - return area(shape) -", + function() returns int64 constant = make_constant[int64](9) + function(int64) returns int64 identity = make_identity[int64]() + function(int64) returns optional[int64] wrap = make_optional[int64]() + optional[int64] wrapped = wrap(3) + return constant() + identity(4) +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + assert_eq!(result.generic_calls.len(), 3); + assert_eq!(result.generic_function_instantiations.len(), 3); + for call in result.generic_calls.values() { + let instance = result + .generic_function_instantiations + .iter() + .find(|instance| { + instance.definition == call.definition + && instance.concrete_args == call.concrete_args + && instance.specialization == call.specialization + }) + .expect("every factory call needs a checked concrete body"); + assert_eq!(instance.concrete_args, [TypeInterner::INT64]); + let Type::Function { + params, + return_type, + .. + } = result.interner.resolve(instance.return_type) + else { + panic!("factory must return a concrete function type"); + }; + assert!(params.iter().all(|param| *param == TypeInterner::INT64)); + assert!( + *return_type == TypeInterner::INT64 + || matches!( + result.interner.resolve(*return_type), + Type::Optional(inner) if *inner == TypeInterner::INT64 + ) + ); + assert!( + instance + .type_map + .values() + .any(|ty| *ty == instance.return_type) + ); + } + } - let errors: Vec<_> = result - .diagnostics + #[test] + fn generic_closure_reflection_detection_uses_the_body() { + let source = r#"function make_identity[T]() returns function(T) returns T: + return function(value: T) returns T: return value +function make_name[T]() returns function() returns string: + return function() returns string: return type.name[T]() +function make_nested_name[T]() returns function() returns function() returns string: + return function() returns function() returns string: return function() returns string: return type.name[list[T]]() +"#; + let parsed = parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolve = jett_resolve::resolve(&parsed.module); + let checker = TypeChecker::new(&resolve, CheckOptions::default()); + let reflection = parsed + .module + .items .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .filter_map(|item| match item { + Item::Function(function) => { + Some(checker.generic_function_uses_type_param_reflection(function)) + } + _ => None, + }) + .collect::>(); + assert_eq!(reflection, [false, true, true]); } #[test] - fn enum_variant_construction_checks_surplus_argument_expressions() { + fn generic_closure_factory_rejects_move_only_capture() { let errors = check_source_errors( - "\ -enum Shape: - point(x: int64, y: int64) - + r#"function make_constant[T](value: T) returns function() returns T: + return function() returns T: return value function main() returns nothing: - Shape shape = Shape.point(1, 2, true + \"bad\") -", + function() returns list[int64] callback = make_constant[list[int64]](list(1)) +"#, ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 402); + assert!(errors[0].message.contains("value")); + } - assert!( - errors.iter().any(|d| d.code.code() == 303), - "expected the enum arity diagnostic, got: {:?}", - errors + #[test] + fn generic_instantiations_export_checker_owned_static_selections() { + let result = check_source_result( + r#"struct User: + value: int64 +function classify[T]() returns string: + if type.kind_tag[T]() == TypeKind.list_type: + return "list" + else if type.kind_tag[T]() == TypeKind.primitive_type: + return "primitive" + else: + return "other" +function maybe_list[T]() returns string: + if type.kind_tag[T]() == TypeKind.list_type: + return "list" + return "none" +function classify_match[T]() returns string: + match type.kind_tag[T](): + primitive_type: + return "primitive" + other: + return "other" +function main() returns nothing: + string list_label = classify[list[int64]]() + string primitive_label = classify[int64]() + string other_label = classify[User]() + string absent_label = maybe_list[string]() + string match_label = classify_match[int64]() +"#, ); assert!( - errors.iter().any(|d| d.code.code() == 301), - "expected surplus argument expression to be type-checked, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + + let selections = result + .generic_function_instantiations + .iter() + .flat_map(|instantiation| instantiation.static_selections.values().copied()) + .collect::>(); + assert!(selections.contains(&CheckedStaticSelection::IfThen)); + assert!(selections.contains(&CheckedStaticSelection::IfElseIf(0))); + assert!(selections.contains(&CheckedStaticSelection::IfElse)); + assert!(selections.contains(&CheckedStaticSelection::IfNoBranch)); + assert!(selections.contains(&CheckedStaticSelection::MatchArm(0))); } #[test] - fn non_exhaustive_enum_match_reports_error() { - let errors = check_source_errors( - "\ -enum Color: - red - blue - -function describe(color: Color) returns int64: - match color: - red: - return 1 -", + fn generic_type_name_literal_equality_selects_a_checked_branch() { + let result = check_source_result( + r#"function classify[T]() returns string: + if "a" == "b": + return "unrelated" + if type.name[T]() == "int64": + return "integer" + else: + return "other" +function main() returns nothing: + string integer = classify[int64]() + string other = classify[string]() +"#, ); - assert!( - errors.iter().any(|d| d.code.code() == 324), - "expected E0324, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + let selections = result + .generic_function_instantiations + .iter() + .flat_map(|instantiation| instantiation.static_selections.values().copied()) + .collect::>(); + assert!(selections.contains(&CheckedStaticSelection::IfThen)); + assert!(selections.contains(&CheckedStaticSelection::IfElse)); + assert_eq!(selections.len(), 2); } #[test] - fn enum_pattern_binding_count_mismatch_reports_error() { - let errors = check_source_errors( - "\ -enum Shape: - rect(width: int64, height: int64) - -function area(shape: Shape) returns int64: - match shape: - rect(width): - return width -", + fn generic_has_secret_selects_a_checked_branch() { + let result = check_source_result( + r#"function classify[T]() returns string: + if type.has_secret[T](): + return "secret" + else: + return "public" +function main() returns nothing: + string secret_label = classify[secret[string]]() + string public_label = classify[string]() +"#, ); - assert!( - errors.iter().any(|d| d.code.code() == 323), - "expected E0323, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + let selections = result + .generic_function_instantiations + .iter() + .flat_map(|instantiation| instantiation.static_selections.values().copied()) + .collect::>(); + assert!(selections.contains(&CheckedStaticSelection::IfThen)); + assert!(selections.contains(&CheckedStaticSelection::IfElse)); } #[test] - fn reflection_metadata_owner_tables_are_type_id_backed() { + fn alias_and_underlying_type_keep_distinct_branch_and_match_specializations() { let result = check_source_result( - "\ -namespace models - -export struct User: - name: string - score: int64 - -namespace packets - -export enum Protocol: - tcp = 6 - udp = 17 - -export bitfield Header: - version: 4 bits - protocol: 8 bits as Protocol -", - ); - let metadata = result.reflection_metadata; - - let user_id = metadata - .type_id_for_name("models.User") - .expect("models.User should have a canonical TypeId"); - let user_fields = metadata - .get_type_fields_for_id(user_id) - .expect("struct fields should be keyed by owner TypeId"); - assert_eq!(user_fields[0].name, "name"); - assert_eq!( - metadata - .get_type_fields("models.User") - .expect("legacy field lookup should bridge through TypeId")[1] - .name, - "score" + r#"type Names = list[string] +function classify_branch[T]() returns string: + if type.kind_tag[T]() == TypeKind.alias_type: + return "alias" + else: + return "list" +function classify_match[T]() returns string: + match type.kind_tag[T](): + alias_type: + return "alias" + other: + return "list" +function main() returns nothing: + string branch_alias = classify_branch[Names]() + string branch_list = classify_branch[list[string]]() + string match_alias = classify_match[Names]() + string match_list = classify_match[list[string]]() +"#, ); - - let header_id = metadata - .type_id_for_name("packets.Header") - .expect("packets.Header should have a canonical TypeId"); - let header = metadata - .get_bitfield_for_id(header_id) - .expect("bitfield metadata should be keyed by owner TypeId"); - assert_eq!(header.fields[1].name, "protocol"); - assert_eq!( - metadata - .get_bitfield("packets.Header") - .expect("legacy bitfield lookup should bridge through TypeId") - .fields[0] - .width, - 4 + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); - let protocol_id = metadata - .type_id_for_name("packets.Protocol") - .expect("packets.Protocol should have a canonical TypeId"); - let variants = metadata - .get_type_variants_for_id(protocol_id) - .expect("enum variants should be keyed by owner TypeId"); - assert_eq!(variants[0].name, "tcp"); - assert_eq!( - metadata - .get_type_variants("packets.Protocol") - .expect("legacy variant lookup should bridge through TypeId")[1] - .discriminant, - 17 - ); + let mut by_definition: HashMap> = + HashMap::new(); + for instantiation in &result.generic_function_instantiations { + by_definition + .entry(instantiation.definition) + .or_default() + .push(instantiation); + } + assert_eq!(by_definition.len(), 2); + for instantiations in by_definition.values() { + assert_eq!(instantiations.len(), 2); + assert_eq!( + instantiations[0].concrete_args, + instantiations[1].concrete_args + ); + assert_ne!( + instantiations[0].specialization, + instantiations[1].specialization + ); + let kind_tags = instantiations + .iter() + .map(|instantiation| instantiation.specialization.type_argument_kinds[0].as_str()) + .collect::>(); + assert_eq!(kind_tags, HashSet::from(["alias_type", "list_type"])); + assert_ne!( + instantiations[0].static_selections, + instantiations[1].static_selections + ); + } + assert_eq!(result.generic_calls.len(), 4); + assert!(result.generic_calls.values().all(|call| { + result + .generic_function_instantiations + .iter() + .any(|instantiation| { + instantiation.definition == call.definition + && instantiation.concrete_args == call.concrete_args + && instantiation.specialization == call.specialization + }) + })); } #[test] - fn reflection_metadata_generic_type_info_args_are_type_id_backed() { + fn comptime_type_argument_alias_keeps_its_kind_in_generic_calls() { let result = check_source_result( - "\ -namespace accounts - -export struct Box[T]: - value: T - -namespace audit - -export struct Box[T]: - value: T - -namespace app - -function use_boxes() returns string: - use accounts as a - use audit as au - a.Box[int64] account = a.Box[int64](value: 1) - au.Box[int64] audit_box = au.Box[int64](value: 2) - TypeInfo account_info = type.info[a.Box[int64]]() - TypeInfo audit_info = type.info[au.Box[int64]]() - return \"{account_info.type_name}:{audit_info.type_name}:{account.value}:{audit_box.value}\" -", + r#"type Names = list[string] +function classify[T]() returns string: + if type.kind_tag[T]() == TypeKind.alias_type: + return "alias" + return "other" +function inspect[T]() returns string: + comptime type Element = type.arg[T](0): + return classify[Element]() +function main() returns nothing: + string label = inspect[optional[Names]]() +"#, ); - let metadata = result.reflection_metadata; - - let account_info = metadata - .get_type_info("accounts.Box[int64]") - .expect("accounts.Box[int64] should have canonical type info"); - assert_eq!(account_info.args[0].type_name, "int64"); - - let audit_info = metadata - .get_type_info("audit.Box[int64]") - .expect("audit.Box[int64] should have canonical type info"); - assert_eq!(audit_info.args[0].type_name, "int64"); - assert!( - metadata.get_type_info("Box[int64]").is_none(), - "ambiguous generic leaf metadata should not be published" + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); + assert!( + result + .generic_function_instantiations + .iter() + .any(|instance| { + instance + .specialization + .type_argument_reflections + .first() + .is_some_and(|info| info.type_name == "Names") + && instance.specialization.type_argument_kinds == ["alias_type"] + }) ); } #[test] - fn resolved_namespaced_type_lookup_does_not_fall_back_to_root_leaf() { - let type_span = sp(0, 4); - let mut env = TestEnv::new(); - let namespaced_type = - env.scope_table - .new_def("models.User".to_string(), DefKind::Struct, type_span); - env.reference(type_span, namespaced_type); - let resolve = env.into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); - checker.register_named_type(None, "User", TypeInterner::INT64); - - assert_eq!( - checker.resolve_named_type("User", type_span), - TypeInterner::ERROR + fn differing_static_selections_conflict_for_one_canonical_instantiation() { + let span = sp(10, 20); + let mut selections = HashMap::from([(span, CheckedStaticSelection::IfThen)]); + let conflicts = merge_static_selections( + &mut selections, + HashMap::from([(span, CheckedStaticSelection::IfElse)]), ); - let generic_span = sp(10, 14); - let mut env = TestEnv::new(); - let namespaced_generic = - env.scope_table - .new_def("models.Box".to_string(), DefKind::Struct, generic_span); - env.reference(generic_span, namespaced_generic); - let resolve = env.into_resolve_result(); - let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); - checker.generic_struct_templates.insert( - "Box".to_string(), - ast::StructDef { - name: ident("Box", generic_span), - type_params: vec![ident("T", generic_span)], - fields: Vec::new(), - methods: Vec::new(), - exported: false, - span: generic_span, - }, + assert_eq!(conflicts, vec![span]); + assert_eq!(selections.get(&span), Some(&CheckedStaticSelection::IfThen)); + } + + #[test] + fn runtime_type_name_generic_exports_manifest_and_intrinsic_operand() { + let result = check_source_result( + r#"function describe[T]() returns string: + return type.name[T]() +function main() returns string: + return describe[int64]() +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + let [instantiation] = result.generic_function_instantiations.as_slice() else { + panic!("describe[int64] should have one checked manifest entry"); + }; + assert_eq!(instantiation.concrete_args, vec![TypeInterner::INT64]); + assert_eq!(instantiation.intrinsic_type_arguments.len(), 1); assert_eq!( - checker.resolve_generic_type( - "Box", - &[TypeExpr::Named(ident("int64", sp(15, 20)))], - generic_span, - ), - TypeInterner::ERROR + instantiation + .intrinsic_type_arguments + .values() + .next() + .unwrap(), + &vec![TypeInterner::INT64] ); } #[test] - fn raw_json_facade_signature_prefers_trusted_stdlib_declaration() { - let result = check_source_result_with_file_id( - "\ -namespace json - -export enum JsonTree: - null - -export function kind(view value: JsonTree) returns int64: - return 1 + fn runtime_field_reflection_exports_concrete_intrinsic_operands() { + let result = check_source_result( + r#"struct User: + name: string +function first_string_field[T](view value: T) returns string: + list[TypeField] fields = type.fields[T]() + mutable string found = "missing" + for field in fields: + if field.type_name == "string": + string item = type.field_value[T, string](view value, view field) + found = item + return found +function main() returns string: + User user = User(name: "Ada") + return first_string_field[User](view user) +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); -namespace app + let [instantiation] = result.generic_function_instantiations.as_slice() else { + panic!("first_string_field[User] should have one checked manifest entry"); + }; + let owner = instantiation.concrete_args[0]; + let mut operands = instantiation + .intrinsic_type_arguments + .values() + .cloned() + .collect::>(); + operands.sort_by_key(Vec::len); + assert_eq!( + operands, + vec![vec![owner], vec![owner, TypeInterner::STRING]] + ); + } -function main() returns int64: - json.JsonTree tree = json.JsonTree.null - return json.kind(view tree) -", - FileId::new(STDLIB_FILE_ID_START), + #[test] + fn comptime_field_bindings_export_distinct_recursive_body_facts() { + let result = check_source_result( + r#"struct User: + name: string + age: int64 + active: bool +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + string reflected_name = type.name[Field]() + return nothing +function main() returns nothing: + User user = User(name: "Ada", age: 42, active: true) + inspect[User](view user) +"#, + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); - let errors: Vec<_> = result - .diagnostics + let inspect = result + .generic_function_instantiations .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .find(|instantiation| !instantiation.comptime_type_bindings.is_empty()) + .expect("inspect[User] should export its comptime field expansions"); + let bindings = inspect + .comptime_type_bindings + .values() + .next() + .expect("comptime binding should be keyed by its statement span"); + assert_eq!(bindings.len(), 3); + assert_eq!( + bindings + .iter() + .map(|binding| binding.selection) + .collect::>(), + vec![ + CheckedComptimeTypeSelection::ReflectedIteration(0), + CheckedComptimeTypeSelection::ReflectedIteration(1), + CheckedComptimeTypeSelection::ReflectedIteration(2), + ] + ); + assert_eq!( + bindings + .iter() + .map(|binding| binding.bound_type) + .collect::>(), + vec![ + TypeInterner::STRING, + TypeInterner::INT64, + TypeInterner::BOOL, + ] + ); + for binding in bindings { + assert_eq!(binding.body.intrinsic_type_arguments.len(), 1); + assert_eq!( + binding + .body + .intrinsic_type_arguments + .values() + .next() + .unwrap(), + &vec![binding.bound_type] + ); + } } #[test] - fn raw_json_facade_uses_ordinary_source_signature_without_trusted_stdlib() { - let errors = check_source_errors( - "\ -namespace json - -export enum JsonTree: - null - -export function kind(view value: JsonTree) returns int64: - return 1 - -namespace app - -function main() returns string: - use json - json.JsonTree tree = json.JsonTree.null - return json.kind(view tree) -", + fn unconstrained_empty_list_inference_uses_uninhabited_type_not_error() { + let result = check_source_result( + r#"function is_empty[T](view items: list[T]) returns bool: + return true +function main() returns bool: + return is_empty(list()) +"#, ); - assert!( - errors.iter().any(|d| d.code.code() == 305), - "expected E0305, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics ); + let [instantiation] = result.generic_function_instantiations.as_slice() else { + panic!("is_empty[] should have one checked manifest entry"); + }; + assert_eq!(instantiation.concrete_args, vec![TypeInterner::NEVER]); + assert_ne!(instantiation.concrete_args[0], TypeInterner::ERROR); } #[test] - fn result_handle_requires_error_keyword() { - let errors = check_source_errors( - "\ -function main() returns int64: - int64 parsed = int64.from_string(\"42\") handle: - default 0 - return parsed -", + fn expected_result_specializes_empty_generic_argument_without_overriding_values() { + let source = r#"function wrap[T](items: list[T]) returns list[list[T]]: + return list(items) +function main() returns list[list[int64]]: + list[list[int64]] wrapped = wrap(list()) + return wrapped +"#; + let result = check_source_result(source); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); + let [instantiation] = result.generic_function_instantiations.as_slice() else { + panic!("wrap[int64] should have one checked manifest entry"); + }; + assert_eq!(instantiation.concrete_args, vec![TypeInterner::INT64]); + let start = source.rfind("list()").expect("empty argument") as u32; + let argument_type = result + .type_map + .get(&Span::new(FileId::new(0), start, start + 6)) + .copied() + .expect("contextual argument type"); + assert_eq!( + result.interner.resolve(argument_type), + &Type::List(TypeInterner::INT64) ); + let mismatch = check_source_result( + r#"function wrap[T](items: list[T]) returns list[list[T]]: + return list(items) +function main() returns nothing: + list[list[string]] wrapped = wrap(list(1)) +"#, + ); assert!( - errors.iter().any(|d| d.code.code() == 316), - "expected E0316, got: {:?}", - errors + mismatch + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error), + "value-bearing int64 inference must not be replaced by string context" + ); + + let unknown = check_source_result( + r#"function empty[T]() returns list[T]: + return list() +function main() returns list[int64]: + return empty() +"#, + ); + assert!( + unknown + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error), + "result context must not infer a type argument absent from all inputs" ); } #[test] - fn optional_handle_rejects_error_keyword() { + fn rejects_mutable_namespace_values_but_preserves_local_mutability() { let errors = check_source_errors( - "\ -function main() returns int64: - int64 value = some(1) handle error: - default 0 - return value -", + "namespace first\nmutable int64 count = 0\nfunction read_count() returns int64:\n return count\nnamespace second\nmutable string label = \"global\"\nfunction read_label() returns string:\n return label\n", ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!(errors.iter().all(|error| error.code.code() == 377)); + assert!(errors[0].message.contains("count")); + assert!(errors[1].message.contains("label")); - assert!( - errors.iter().any(|d| d.code.code() == 317), - "expected E0317, got: {:?}", - errors + let errors = check_source_errors( + "namespace app\nint64 answer = 42\nfunction read_answer() returns int64:\n mutable int64 local = answer\n local = local + 1\n return local\n", ); + assert!(errors.is_empty(), "{errors:?}"); + } + + #[test] + fn namespace_constants_reject_rebinding_and_projected_writes() { + let source = r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +int64 answer = 42 +Record record = comptime Record(child: Inner(value: 1)) +function example() returns nothing: + answer = 43 + answer = answer + 1 + (answer) = 44 + record.child.value = 2 +"#; + let errors = check_source_errors(source); + assert_eq!(errors.len(), 4, "{errors:?}"); + let targets = errors + .iter() + .map(|diagnostic| { + assert_eq!(diagnostic.code.code(), 404, "{diagnostic:?}"); + &source[diagnostic.span.start as usize..diagnostic.span.end as usize] + }) + .collect::>(); + assert_eq!(targets, ["answer", "answer", "answer", "record"]); } #[test] - fn handle_block_requires_explicit_terminator() { + fn namespace_constants_reject_rebinding_in_methods_and_inline_functions() { let errors = check_source_errors( - "\ -function main() returns int64: - int64 parsed = int64.from_string(\"oops\") handle error: - int64 fallback = 0 - return parsed -", + r#"namespace app +int64 answer = 42 +struct Record: + value: int64 + function change(view self: Record) returns nothing: + answer = 43 +function callback() returns function() returns nothing: + return function() returns nothing: + answer = answer + 1 +"#, ); - + assert_eq!(errors.len(), 2, "{errors:?}"); assert!( - errors.iter().any(|d| d.code.code() == 318), - "expected E0318, got: {:?}", errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 404) ); } #[test] - fn handle_default_must_match_unwrapped_type() { + fn namespace_constants_reject_rebinding_in_actor_and_test_bodies() { let errors = check_source_errors( - "\ -function main() returns int64: - int64 parsed = int64.from_string(\"oops\") handle error: - default \"bad\" - return parsed -", + r#"namespace app +int64 answer = 42 +actor Counter: + mutable int64 count = 0 + receive change: + answer = 43 + count = count + 1 +verify immutable_constant: + answer = answer + 1 +property immutable_reads: + given value: int64 + answer = value +"#, ); - + assert_eq!(errors.len(), 3, "{errors:?}"); assert!( - errors.iter().any(|d| d.code.code() == 300), - "expected E0300, got: {:?}", errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 404) ); } #[test] - fn handle_with_builtin_result_type_checks_cleanly() { + fn namespace_constants_preserve_mutable_local_and_actor_bindings() { let errors = check_source_errors( - "\ -function main() returns int64: - int64 parsed = int64.from_string(\"42\") handle error: - default 0 - return parsed -", + r#"namespace app +int64 answer = 42 +function local() returns nothing: + mutable int64 answer = 1 + answer = answer + 1 +actor Counter: + mutable int64 answer = 0 + receive change: + answer = answer + 1 +property local_binding: + given answer: int64 + assert answer == answer +namespace other +int64 answer = 7 +function local() returns nothing: + mutable int64 answer = 1 + answer = answer + 1 +"#, ); - - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert!(errors.is_empty(), "{errors:?}"); } #[test] - fn interface_implement_call_typechecks_cleanly() { - let result = check_source_result( - "\ -interface Speaker: - function speak(view self: Speaker) returns string - -struct Dog: - name: string - -implement Speaker for Dog: - function speak(view self: Dog) returns string: - return self.name - -function main() returns string: - Dog dog = Dog(name: \"woof\") - return Speaker.speak(view dog) -", + fn projected_assignment_rejects_immutable_and_view_bindings() { + let errors = check_source_errors( + r#"struct Inner: + value: int64 +struct Record: + child: Inner +function immutable(item: Record) returns nothing: + item.child.value = 2 +function borrowed(view mutable item: Record) returns nothing: + item.child.value = 2 +function annotated(mutable item: view Record) returns nothing: + item.child.value = 2 +function local(view item: Record) returns nothing: + Record fixed = clone view item + fixed.child.value = 2 + mutable Record viewed_local = (view item) + viewed_local.child.value = 2 + mutable view Record typed_local = clone view item + typed_local.child.value = 2 +"#, ); - - let errors: Vec<_> = result - .diagnostics + let codes = errors .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .map(|diagnostic| diagnostic.code.code()) + .collect::>(); + assert_eq!(codes, [404, 401, 401, 404, 401, 401], "{errors:?}"); + assert!( + errors + .iter() + .filter(|diagnostic| diagnostic.code.code() == 401) + .all(|diagnostic| diagnostic.message.contains("views are read-only")) + ); } #[test] - fn implement_block_missing_method_reports_error() { + fn projected_assignment_rejects_temporary_and_transformed_places() { let errors = check_source_errors( - "\ -interface Speaker: - function speak(view self: Speaker) returns string - function growl(view self: Speaker) returns string - -struct Dog: - name: string - -implement Speaker for Dog: - function speak(view self: Dog) returns string: - return self.name -", + r#"struct Inner: + value: int64 +struct Record: + child: Inner +type Wrapped = Record where true +function make() returns Record: + return Record(child: Inner(value: 1)) +function temporary(view item: Record) returns nothing: + make().child.value = 2 + Record(child: Inner(value: 1)).child.value = 2 + (clone view item).child.value = 2 + (view item).child.value = 2 +function transformed(mutable item: Wrapped) returns nothing: + (coarsen item).child.value = 2 +function transformed_view(view mutable item: Wrapped) returns nothing: + (coarsen item).child.value = 2 +function borrowed_initializer(view item: Wrapped) returns nothing: + mutable Record borrowed = coarsen (view item) + borrowed.child.value = 2 +"#, ); - + let codes = errors + .iter() + .map(|diagnostic| diagnostic.code.code()) + .collect::>(); + assert_eq!(codes, [404, 404, 404, 401, 404, 401, 401], "{errors:?}"); assert!( - errors.iter().any(|d| d.code.code() == 331), - "expected E0331, got: {:?}", errors + .iter() + .filter(|diagnostic| diagnostic.code.code() == 404) + .all(|diagnostic| diagnostic.message.contains("owned mutable binding")) ); } #[test] - fn implement_block_signature_mismatch_reports_error() { + fn projected_assignment_preserves_owned_roots_and_field_copies() { + // These bodies pin only the selected assignment-root prerequisites. + // Execution of projected updates remains an unresolved language rule. let errors = check_source_errors( - "\ -interface Speaker: - function speak(view self: Speaker) returns string - -struct Dog: - name: string - -implement Speaker for Dog: - function speak(self: Dog) returns int64: - return 1 -", + r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +function make() returns Record: + return Record(child: Inner(value: 1)) +function owned(mutable item: Record) returns nothing: + item.child.value = 2 + (item.child).value = 3 +function copied(view item: Record) returns int64: + mutable Inner first = item.child + first.value = 2 + mutable Inner second = (view item).child + second.value = 3 + mutable Inner third = clone view item.child + third.value = 4 + mutable Record fourth = clone view item + fourth.child.value = 5 + int64 read = make().child.value + return read + item.child.value +function callback() returns function(Record) returns nothing: + return function(mutable item: Record) returns nothing: + item.child.value = 2 +actor Store: + mutable app.Record item = app.Record(child: app.Inner(value: 1)) + receive replace(mutable supplied: app.Record): + item.child.value = 2 + supplied.child.value = 3 +"#, ); + assert!(errors.is_empty(), "{errors:?}"); + } - assert!( - errors.iter().any(|d| d.code.code() == 330), - "expected E0330, got: {:?}", - errors + #[test] + fn projected_assignment_uses_lexical_declaration_identity() { + let source = r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +function example() returns nothing: + if true: + mutable Record item = Record(child: Inner(value: 1)) + item.child.value = 2 + Record item = Record(child: Inner(value: 1)) + item.child.value = 3 +function owned(mutable item: Record) returns nothing: + item.child.value = 4 +function readonly(view item: Record) returns nothing: + item.child.value = 5 +namespace other +struct Record: + value: int64 +function example(mutable item: Record) returns nothing: + item.value = 6 +"#; + let errors = check_source_errors(source); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert_eq!(errors[0].code.code(), 404, "{errors:?}"); + assert_eq!(errors[1].code.code(), 401, "{errors:?}"); + assert_eq!( + &source[errors[0].span.start as usize..errors[0].span.end as usize], + "item" ); } #[test] - fn string_interpolation_accepts_user_defined_displayable_types() { - let result = check_source_result( - "\ -interface Displayable: - function display(view self: Displayable) returns string - -struct User: - name: string - -implement Displayable for User: - function display(view self: User) returns string: - return self.name - -function main() returns string: - User user = User(name: \"Ada\") - return \"user: {user}\" -", + fn projected_assignment_preserves_view_facts_in_actor_aliases() { + let errors = check_source_errors( + r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +actor Store: + receive borrowed(view item: app.Record): + mutable app.Record alias = item + alias.child.value = 2 +"#, ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("views are read-only")); + } - let errors: Vec<_> = result - .diagnostics + #[test] + fn owned_rebinding_rejects_move_only_views_without_changing_root_facts() { + let source = r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +function make() returns Record: + return Record(child: Inner(value: 1)) +function direct(view source: Record) returns nothing: + mutable Record alias = make() + alias = view source + alias.child.value = 9 +function conditional(view source: Record) returns nothing: + mutable Record alias = make() + if true: + alias = view source + alias.child.value = 9 +function known_alias(view source: Record) returns nothing: + Record borrowed = source + mutable Record alias = make() + alias = borrowed + alias.child.value = 9 +actor Store: + mutable app.Record saved = make() + receive explicit(view source: app.Record): + saved = view source + saved.child.value = 9 + receive bare(view source: app.Record): + saved = source + saved.child.value = 9 +"#; + let errors = check_source_errors(source); + assert_eq!(errors.len(), 5, "{errors:?}"); + for diagnostic in &errors { + assert_eq!(diagnostic.code.code(), 401, "{diagnostic:?}"); + assert_eq!( + diagnostic.message, + "cannot rebind an owned value from a view; clone the value instead" + ); + } + let values = errors .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .map(|diagnostic| &source[diagnostic.span.start as usize..diagnostic.span.end as usize]) + .collect::>(); + assert_eq!( + values, + [ + "view source", + "view source", + "borrowed", + "view source", + "source" + ] + ); } #[test] - fn secret_value_can_be_declassified() { - let result = check_source_result( - "\ -function reveal(key: secret[string]) returns string: - return declassify key - -function main() returns string: - secret[string] api_key = \"abc\" - return reveal(api_key) -", + fn owned_rebinding_preserves_clones_scalars_and_field_copies() { + let errors = check_source_errors( + r#"namespace app +struct Inner: + value: int64 +struct Record: + child: Inner +function make() returns Record: + return Record(child: Inner(value: 1)) +function copies(view source: Record, view number: int64) returns nothing: + mutable Record alias = make() + alias = clone view source + alias.child.value = 2 + mutable Inner copied = make().child + copied = source.child + copied = (view source).child + copied = clone view source.child + copied.value = 3 + mutable int64 scalar = 0 + scalar = view number + scalar = number +actor Store: + mutable app.Record saved = make() + receive replace(view source: app.Record): + saved = clone view source + saved.child.value = 4 +"#, ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert!(errors.is_empty(), "{errors:?}"); } #[test] - fn declassify_requires_secret_type() { + fn owned_rebinding_rejects_views_with_parenthesized_identifier_targets() { let errors = check_source_errors( - "\ -function main() returns string: - return declassify \"abc\" -", + r#"struct Record: + value: int64 +function replace(view source: Record) returns nothing: + mutable Record alias = Record(value: 1) + (alias) = view source + ((alias)) = view source +"#, ); + assert_eq!(errors.len(), 2, "{errors:?}"); + for diagnostic in &errors { + assert_eq!(diagnostic.code.code(), 401, "{diagnostic:?}"); + assert_eq!( + diagnostic.message, + "cannot rebind an owned value from a view; clone the value instead" + ); + } + } + #[test] + fn owned_rebinding_preserves_immutable_target_diagnostics() { + let errors = check_source_errors( + r#"namespace app +struct Record: + value: int64 +Record constant = comptime Record(value: 1) +function local(view source: Record) returns nothing: + Record fixed = Record(value: 1) + fixed = view source + constant = view source +"#, + ); + assert_eq!(errors.len(), 2, "{errors:?}"); assert!( - errors.iter().any(|d| d.code.code() == 601), - "expected E0601, got: {:?}", errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 404) ); } #[test] - fn stdout_write_rejects_secret_values() { + fn written_view_return_annotations_reject_headers_and_callable_results() { + for (source, count) in [ + ( + "function copy(view values: list[int64]) returns view list[int64]:\n return clone values\n", + 1, + ), + ( + "function unused[T](value: T) returns view T:\n return value\n", + 1, + ), + ( + "mutual:\n function value(number: int64) returns view int64\nfunction value(number: int64) returns view int64:\n return number\n", + 2, + ), + ( + "struct Item:\n value: int64\n function read(view self: Item) returns view int64:\n return self.value\n", + 1, + ), + ( + "interface Reader:\n function read(view self: Reader) returns view int64\nstruct Item:\n value: int64\nimplement Reader for Item:\n function read(view self: Item) returns view int64:\n return self.value\n", + 2, + ), + ( + "function callback() returns nothing:\n function() returns int64 value = function() returns view int64:\n return 1\n", + 1, + ), + ( + "function callback() returns nothing:\n mutable function() returns view int64 selected = function() returns int64:\n return 1\n", + 1, + ), + ( + "function unused[T](value: T, callback: function() returns view T) returns nothing:\n return nothing\n", + 1, + ), + ( + "struct Unused[T]:\n callback: function() returns view T\n", + 1, + ), + ( + "function factory() returns function() returns view int64:\n return function() returns int64:\n return 1\n", + 1, + ), + ] { + let errors = check_source_errors(source); + assert_eq!(errors.len(), count, "{source}\n{errors:?}"); + for error in errors { + assert_eq!(error.code.code(), 401, "{error:?}"); + assert_eq!( + error.message, + "function return type cannot be a view; return an owned value instead" + ); + assert!( + source[error.span.start as usize..error.span.end as usize].starts_with("view ") + ); + } + } + } + + #[test] + fn written_view_return_annotations_are_reported_once_per_source_span() { let errors = check_source_errors( - "\ -function main(stdout: Stdout) returns nothing: - secret[string] api_key = \"abc\" - Stdout.write(view stdout, api_key) -", + r#"function identity[T](value: T) returns view T: + return value +function main() returns nothing: + int64 first = identity[int64](1) + int64 second = identity[int64](2) + string third = identity[string]("three") +"#, ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert!(errors[0].message.contains("return type cannot be a view")); - assert!( - errors.iter().any(|d| d.code.code() == 600), - "expected E0600, got: {:?}", - errors + let errors = check_source_errors( + "function escape(view values: list[int64]) returns view list[int64]:\n return values\n", ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!(errors[0].message.contains("return type cannot be a view")); + assert!(errors[1].message.contains("cannot return a view")); + assert_ne!(errors[0].span, errors[1].span); } #[test] - fn string_interpolation_rejects_secret_values() { - let errors = check_source_errors( - "\ -function main() returns string: - secret[string] api_key = \"abc\" - return \"api key: {api_key}\" -", - ); + fn owned_returns_reject_known_views_and_forwarded_aliases() { + for (parameter, returned) in [ + ("view values: list[int64]", "values"), + ("values: view list[int64]", "values"), + ("view values: list[int64]", "((values))"), + ("view values: list[int64]", "view values"), + ("view values: list[int64]", "(view values)"), + ("view values: list[int64]", "forwarded"), + ] { + let source = format!( + "function escape({parameter}) returns list[int64]:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n return {returned}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert_eq!( + errors[0].message, + "cannot return a view; clone the value or take ownership instead" + ); + assert_eq!( + &source[errors[0].span.start as usize..errors[0].span.end as usize], + returned + ); + } + } + + #[test] + fn owned_returns_check_inline_and_instantiated_generic_bodies() { + for source in [ + r#"function make() returns function(view list[int64]) returns list[int64]: + return function(view values: list[int64]) returns list[int64]: + return values +"#, + r#"function escape[T](view value: T) returns T: + return value +function main() returns nothing: + list[int64] source = list(7) + list[int64] escaped = escape[list[int64]](view source) +"#, + ] { + let errors = check_source_errors(source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("cannot return a view")); + } + } + + #[test] + fn owned_call_boundaries_reject_views_across_call_forms_once() { + let prefix = r#"function consume(values: list[int64]) returns nothing: + return nothing +function generic[T](values: T) returns nothing: + return nothing +"#; + for call in [ + "consume(values)", + "consume((values))", + "consume(forwarded)", + "(consume)(forwarded)", + "generic[list[int64]](forwarded)", + "generic(forwarded)", + "callback(forwarded)", + ] { + let source = format!( + "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); + assert_eq!( + errors[0].message, + "cannot pass a view to an owned parameter; clone the value or take ownership instead" + ); + } - assert!( - errors.iter().any(|d| d.code.code() == 600), - "expected E0600, got: {:?}", - errors + let errors = check_source_errors( + r#"struct Item: + value: int64 + function consume(self: Item) returns nothing: + return nothing +function reject(view source: Item) returns nothing: + Item borrowed = source + Item.consume(borrowed) +"#, ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{errors:?}"); + assert!(errors[0].message.contains("owned parameter")); } #[test] - fn pure_call_with_secret_argument_returns_secret() { - let result = check_source_result( - "\ -function echo(value: string) returns string: - return value - -function main() returns nothing: - secret[string] api_key = \"abc\" - secret[string] echoed = echo(api_key) + fn owned_view_call_diagnostics_do_not_hide_other_view_errors() { + let errors = check_source_errors( + r#"struct Record: + value: int64 +function consume(item: Record) returns nothing: return nothing -", +function reject(view item: Record) returns nothing: + consume(item) + item.value = 2 +"#, ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!(errors[0].message.contains("owned parameter")); + assert!(errors[1].message.contains("views are read-only")); + assert!(errors.iter().all(|error| error.code.code() == 401)); + } - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + #[test] + fn owned_pipeline_boundaries_check_initial_values_and_extra_arguments() { + let prefix = r#"function consume(values: list[int64]) returns nothing: + return nothing +function second(number: int64, values: list[int64]) returns nothing: + return nothing +function generic[T](values: T) returns nothing: + return nothing +function generic_second[T](number: int64, values: T) returns nothing: + return nothing +"#; + for call in [ + "values into consume()", + "borrowed into consume()", + "borrowed into generic[list[int64]]()", + "borrowed into generic()", + "borrowed into callback()", + "1 into second(borrowed)", + "1 into generic_second[list[int64]](borrowed)", + "1 into generic_second(borrowed)", + ] { + let source = format!( + "{prefix}function reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n list[int64] borrowed = view values\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 401, "{call}: {errors:?}"); + assert!(errors[0].message.contains("owned parameter")); + let reported = &source[errors[0].span.start as usize..errors[0].span.end as usize]; + assert!(matches!(reported, "values" | "borrowed"), "{reported}"); + } } #[test] - fn pure_call_with_secret_argument_cannot_be_assigned_to_public_type() { - let errors = check_source_errors( - "\ -function echo(value: string) returns string: - return value + fn owned_view_boundaries_preserve_explicit_view_call_diagnostics() { + for call in [ + "consume(view values)", + "consume((view values))", + "generic[list[int64]](view values)", + "generic(view values)", + "callback(view values)", + "values into view consume()", + "(view values) into consume()", + "values into view generic[list[int64]]()", + "values into view generic()", + ] { + let source = format!( + "function consume(values: list[int64]) returns nothing:\n return nothing\nfunction generic[T](values: T) returns nothing:\n return nothing\nfunction reject(view values: list[int64], callback: function(list[int64]) returns nothing) returns nothing:\n {call}\n" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{call}: {errors:?}"); + assert_eq!(errors[0].code.code(), 375, "{call}: {errors:?}"); + } + } -function main() returns nothing: - secret[string] api_key = \"abc\" - string echoed = echo(api_key) + #[test] + fn owned_view_boundaries_follow_transparent_wrappers_but_not_type_errors() { + let prefix = r#"type Numbers = list[int64] where true +function consume(values: list[int64]) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 311), - "expected E0311, got: {:?}", - errors - ); +"#; + for (parameter, value, explicit_view) in [ + ("view values: Numbers", "coarsen values", false), + ("view values: Numbers", "coarsen (view values)", true), + ( + "view values: secret[list[int64]]", + "declassify values", + false, + ), + ( + "view values: secret[list[int64]]", + "declassify (view values)", + true, + ), + ] { + for (body, expected_code) in [ + (format!(" return {value}\n"), 401), + ( + format!(" consume({value})\n return list()\n"), + if explicit_view { 375 } else { 401 }, + ), + ] { + let source = + format!("{prefix}function reject({parameter}) returns list[int64]:\n{body}"); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert_eq!(errors[0].code.code(), expected_code, "{errors:?}"); + } + } + for body in [ + " return declassify values\n", + " consume(declassify values)\n return list()\n", + ] { + let source = format!( + "{prefix}function invalid(view values: list[int64]) returns list[int64]:\n{body}" + ); + let errors = check_source_errors(&source); + assert_eq!(errors.len(), 1, "{source}\n{errors:?}"); + assert!(errors[0].message.contains("declassify"), "{errors:?}"); + assert_ne!(errors[0].code.code(), 401, "{errors:?}"); + } } #[test] - fn secret_redact_returns_public_string() { - let result = check_source_result( - "\ -function main() returns string: - secret[string] api_key = \"abc\" - string masked = secret.redact(api_key) - return masked -", + fn owned_view_boundaries_apply_to_actor_verify_and_property_bodies() { + let errors = check_source_errors( + r#"namespace app +function consume(values: list[int64]) returns int64: + return 1 +actor Store: + receive borrowed(view values: list[int64]): + list[int64] alias = view values + int64 count = consume(alias) + trace count +verify borrowed_alias: + list[int64] source = list(7) + list[int64] alias = view source + list[int64] forwarded = alias + assert consume(forwarded) == 1 +property borrowed_property_alias: + given source: list[int64] + list[int64] alias = view source + list[int64] forwarded = alias + assert consume(forwarded) == 1 +"#, ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert_eq!(errors.len(), 1, "{errors:?}"); + for error in errors { + assert_eq!(error.code.code(), 401, "{error:?}"); + assert!(error.message.contains("owned parameter")); + } } #[test] - fn secret_compare_returns_bool() { - let result = check_source_result( - "\ -function main() returns bool: - secret[string] stored = \"abc\" - secret[string] computed = \"abc\" - return secret.compare(stored, computed) -", + fn owned_view_boundaries_preserve_copies_clones_fields_and_call_results() { + let errors = check_source_errors( + r#"struct Record: + values: list[int64] +function consume(values: list[int64]) returns nothing: + return nothing +function copy(view values: list[int64]) returns list[int64]: + list[int64] borrowed = view values + list[int64] forwarded = borrowed + return clone forwarded +function scalar(view value: int64) returns int64: + int64 copied = value + return copied +function text(view value: string) returns string: + return value +function field(view record: Record) returns list[int64]: + return record.values +function copies(view values: list[int64], view record: Record) returns nothing: + consume(clone values) + consume(clone (view values)) + consume(record.values) + consume(copy(view values)) + values into view copy() into consume() + if true: + list[int64] alias = view values + consume(clone alias) + list[int64] alias = list(7) + consume(alias) +"#, ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + assert!(errors.is_empty(), "{errors:?}"); } #[test] - fn secret_compare_requires_matching_secret_types() { + fn owned_view_boundaries_preserve_copyable_transformation_results() { let errors = check_source_errors( - "\ -function main() returns bool: - secret[string] stored = \"abc\" - secret[bytes] computed = bytes.from_string(\"abc\") - return secret.compare(stored, computed) -", + r#"type Count = int64 where true +function consume(value: int64) returns nothing: + return nothing +function refined(view value: Count) returns int64: + return coarsen value +function classified(view value: secret[int64]) returns int64: + return declassify value +function copies(view count: Count, view hidden: secret[int64]) returns nothing: + consume(coarsen count) + consume(declassify hidden) +"#, ); + assert!(errors.is_empty(), "{errors:?}"); - assert!( - errors.iter().any(|d| d.code.code() == 304), - "expected E0304, got: {:?}", - errors + let errors = check_source_errors( + "function explicit(view value: int64) returns int64:\n return view value\n", ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert!(errors[0].message.contains("cannot return a view")); } #[test] - fn secret_compare_rejects_unsupported_payloads() { - let errors = check_source_errors( - "\ -function main() returns bool: - secret[int64] stored = 1 - secret[int64] computed = 1 - return secret.compare(stored, computed) -", - ); - + fn handle_default_is_rechecked_with_its_concrete_success_type() { + let source = r#"function fallback(value: optional[list[int64]]) returns list[int64]: + return value handle: + default list() +"#; + let result = check_source_result(source); assert!( - errors.iter().any(|d| d.code.code() == 604), - "expected E0604, got: {:?}", - errors + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "unexpected diagnostics: {:?}", + result.diagnostics + ); + let start = source.rfind("list()").expect("default list") as u32; + let span = Span::new(FileId::new(0), start, start + 6); + let default_ty = *result + .type_map + .get(&span) + .expect("default list should retain its contextual type"); + assert_eq!( + result.interner.resolve(default_ty), + &Type::List(TypeInterner::INT64) ); } #[test] - fn secret_redact_requires_secret_argument() { - let errors = check_source_errors( - "\ -function main() returns string: - return secret.redact(\"abc\") -", + fn never_assignment_compatibility_is_directional_through_value_shapes() { + let resolve = TestEnv::new().into_resolve_result(); + let mut checker = TypeChecker::new(&resolve, CheckOptions::default()); + let absent = [ + Type::Never, + Type::List(TypeInterner::NEVER), + Type::Set(TypeInterner::NEVER), + Type::Optional(TypeInterner::NEVER), + Type::Secret(TypeInterner::NEVER), + Type::Map(TypeInterner::STRING, TypeInterner::NEVER), + Type::Result(TypeInterner::NEVER, TypeInterner::STRING), + Type::Result(TypeInterner::STRING, TypeInterner::NEVER), + ]; + let inhabited = [ + Type::Int64, + Type::List(TypeInterner::INT64), + Type::Set(TypeInterner::INT64), + Type::Optional(TypeInterner::INT64), + Type::Secret(TypeInterner::INT64), + Type::Map(TypeInterner::STRING, TypeInterner::INT64), + Type::Result(TypeInterner::INT64, TypeInterner::STRING), + Type::Result(TypeInterner::STRING, TypeInterner::INT64), + ]; + for (absent, inhabited) in absent.into_iter().zip(inhabited) { + let absent = checker.interner.intern(absent); + let inhabited = checker.interner.intern(inhabited); + assert!(!checker.types_compatible(absent, inhabited)); + assert!(checker.types_compatible(inhabited, absent)); + } + let bottom_input = checker.interner.intern(Type::Function { + params: vec![TypeInterner::NEVER], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + let concrete = checker.interner.intern(Type::Function { + params: vec![TypeInterner::INT64], + view_params: vec![false], + return_type: TypeInterner::INT64, + }); + assert!(checker.types_compatible(bottom_input, concrete)); + assert!(!checker.types_compatible(concrete, bottom_input)); + assert_eq!( + checker.merge_inferred_never(bottom_input, concrete, false), + Some(bottom_input) ); - - assert!( - errors.iter().any(|d| d.code.code() == 602), - "expected E0602, got: {:?}", - errors + assert_eq!( + checker.merge_inferred_never(TypeInterner::INT8, TypeInterner::INT64, false), + None + ); + let secret = checker.interner.intern(Type::Secret(TypeInterner::NEVER)); + assert_eq!( + checker.merge_inferred_never(secret, TypeInterner::INT64, false), + None ); } #[test] - fn field_access_on_secret_struct_stays_secret() { - let result = check_source_result( - "\ -struct User: - name: string + fn generic_never_places_reject_concrete_payloads_at_every_construction_boundary() { + let cases = [ + ("list[T]", " T item = 7\n return list(item)", 311), + ( + "list[T]", + " T item = none handle:\n default 7\n return list(item)", + 300, + ), + ( + "list[T]", + " mutable T item = none handle: return list()\n item = 7\n return list(item)", + 312, + ), + ("T", " return 7", 305), + ("list[T]", " return list(7)", 300), + ("list[T]", " return append[T](list(), 7)", 300), + ("optional[T]", " return some(7)", 300), + ("result[T, string]", " return ok(7)", 300), + ("result[string, T]", " return fail(7)", 300), + ("map[string, T]", " return map(\"key\": 7)", 300), + ( + "nothing", + " for item in items:\n T forged = 7\n trace forged\n return nothing", + 311, + ), + ]; + for (result_type, body, expected_code) in cases { + for comptime in ["", "comptime "] { + let source = format!( + "function append[T](values: list[T], value: T) returns list[T]:\n return list(value)\nfunction manufacture[T](view items: list[T]) returns {result_type}:\n{body}\nfunction main() returns nothing:\n println({comptime}manufacture(list()))\n return nothing\n" + ); + let errors = check_source_errors(&source); + assert!( + errors + .iter() + .any(|error| error.code.code() == expected_code), + "{source}\n{errors:?}" + ); + assert!( + errors.iter().any(|error| error.message.contains("")), + "{source}\n{errors:?}" + ); + } + } + } + + #[test] + fn inferred_literals_fill_only_never_slots_in_both_element_orders() { + let cases = [ + ("list(none, some(7))", "list[optional[int64]]"), + ("list(some(7), none)", "list[optional[int64]]"), + ("list(ok(7), fail(\"bad\"))", "list[result[int64, string]]"), + ("list(fail(\"bad\"), ok(7))", "list[result[int64, string]]"), + ("list(list(), list(7))", "list[list[int64]]"), + ("list(list(7), list())", "list[list[int64]]"), + ( + "list(some(list()), some(list(7)))", + "list[optional[list[int64]]]", + ), + ( + "list(some(list(7)), some(list()))", + "list[optional[list[int64]]]", + ), + ( + "map(\"a\": none, \"b\": some(7))", + "map[string, optional[int64]]", + ), + ( + "map(\"a\": some(7), \"b\": none)", + "map[string, optional[int64]]", + ), + ("list(map(), map(\"key\": 7))", "list[map[string, int64]]"), + ("list(map(\"key\": 7), map())", "list[map[string, int64]]"), + ("list(none, some(small))", "list[optional[int8]]"), + ("list(some(small), none)", "list[optional[int8]]"), + ]; + for (literal, expected) in cases { + let map_binding = if literal.starts_with("map(") { + "key, value" + } else { + "value" + }; + let source = format!( + "type Tiny = int8\nfunction main() returns nothing:\n Tiny small = 7\n for {map_binding} in {literal}:\n trace value\n return nothing\n" + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(literal).unwrap() as u32; + let span = sp(start, start + literal.len() as u32); + let ty = result.type_map[&span]; + assert_eq!(result.interner.type_name(ty), expected, "{literal}"); + } + } + #[test] + fn contextual_absent_defaults_and_generic_returns_keep_concrete_success_types() { + let source = r#"function identity[T](values: list[T]) returns list[T]: + return values +function fallback[T](view items: list[T], value: T) returns T: + return none handle: + default value function main() returns nothing: - secret[User] user = User(name: \"Ada\") - secret[string] name = user.name + int64 absent = none handle: + default 7 + int64 failure = fail("bad") handle error: + trace error + default 8 + list[int64] empty = identity(list()) + int64 generic = fallback(list(), 9) + int64 baked = comptime (none handle: default 10) + trace absent + trace failure + trace empty + trace generic + trace baked return nothing -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); +"#; + let errors = check_source_errors(source); + assert!(errors.is_empty(), "{errors:?}"); } #[test] - fn mixed_secret_and_public_list_becomes_secret_element_list() { - let result = check_source_result( - "\ + fn inferred_callback_return_sums_use_a_concrete_common_signature() { + for callbacks in [ + "list(make_absent(list()), present)", + "list(present, make_absent(list()))", + ] { + let source = format!( + r#"function make_absent[T](view items: list[T]) returns function() returns optional[T]: + return function() returns optional[T]: return none +function present() returns optional[int64]: + return some(7) function main() returns nothing: - secret[string] api_key = \"abc\" - list[secret[string]] items = list(\"prefix\", api_key, \"suffix\") + for callback in {callbacks}: + optional[int64] value = callback() + trace value return nothing -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(callbacks).unwrap() as u32; + let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; + assert_eq!( + result.interner.type_name(ty), + "list[function() returns optional[int64]]" + ); + } } #[test] - fn string_join_rejects_list_of_secret_strings() { - let errors = check_source_errors( - "\ -function combine_strings(items: list[string], separator: string) returns string: - return separator - -function main() returns string: - secret[string] api_key = \"abc\" - list[secret[string]] items = list(\"prefix\", api_key, \"suffix\") - return combine_strings(items, \"-\") -", - ); - - assert!( - errors + fn mapped_callback_inference_uses_joined_absent_and_present_elements() { + for values in ["list(none, some(7))", "list(some(7), none)"] { + let source = format!( + r#"function append[T](values: list[T], value: T) returns list[T]: + return list(value) +function mapped[A, B](values: list[A], callback: function(A) returns B) returns list[B]: + mutable list[B] output = list() + for value in values: + output = append[B](output, callback(value)) + return output +function unwrap(value: optional[int64]) returns int64: + return value handle: default 0 +function main() returns nothing: + for value in mapped({values}, unwrap): + trace value + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics .iter() - .any(|d| d.code.code() == 304 || d.code.code() == 305), - "expected argument/return type mismatch, got: {:?}", - errors - ); + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = format!("mapped({values}, unwrap)"); + let start = source.find(&call).unwrap() as u32; + let ty = result.type_map[&sp(start, start + call.len() as u32)]; + assert_eq!(result.interner.type_name(ty), "list[int64]"); + } } #[test] - fn filesystem_write_file_rejects_secret_string() { + fn filling_inferred_never_does_not_merge_existing_primitive_widths() { let errors = check_source_errors( - "\ -function main(fs: Filesystem) returns nothing: - secret[string] api_key = \"abc\" - Filesystem.write_file(view fs, \"secret.txt\", api_key) handle error: - return nothing + r#"type Tiny = int8 +function main() returns nothing: + Tiny small = 7 + for value in list(none, some(small), some(7)): + trace value return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 600), - "expected E0600, got: {:?}", - errors +"#, ); + assert_eq!(errors.len(), 1, "{errors:?}"); + assert_eq!(errors[0].code.code(), 300); + assert!(errors[0].message.contains("optional[int8]")); + assert!(errors[0].message.contains("optional[int64]")); } #[test] - fn json_serialize_blocks_struct_with_secret_fields() { - let errors = check_source_errors( - "\ -struct User: - id: string - api_key: secret[string] + fn reverse_literal_compatibility_cannot_hide_a_never_callable_input() { + for callbacks in ["list(broad, narrow(list()))", "list(narrow(list()), broad)"] { + let source = format!( + r#"function broad(value: optional[int64]) returns int64: + return 0 +function narrow[T](view items: list[T]) returns function(optional[T]) returns secret[int64]: + return function(value: optional[T]) returns secret[int64]: return 7 +function main() returns nothing: + for callback in {callbacks}: + trace callback + return nothing +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + if callbacks.starts_with("list(broad,") { + assert!( + errors.iter().any(|error| error.code.code() == 300), + "{source}\n{errors:?}" + ); + } else { + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(callbacks).unwrap() as u32; + let ty = result.type_map[&sp(start, start + callbacks.len() as u32)]; + assert_eq!( + result.interner.type_name(ty), + "list[function(optional[]) returns secret[int64]]" + ); + } + } + } -function main(view user: User) returns string: - return json.serialize[User](view user) -", - ); + #[test] + fn repeated_generic_arguments_fill_nested_never_slots_in_both_orders() { + for (first, second, expected) in [ + ("none", "some(7)", "optional[int64]"), + ("ok(7)", "fail(\"bad\")", "result[int64, string]"), + ("list()", "list(\"ready\")", "list[string]"), + ("map()", "map(\"key\": 7)", "map[string, int64]"), + ( + "some(list())", + "some(list(\"ready\"))", + "optional[list[string]]", + ), + ] { + for (first, second) in [(first, second), (second, first)] { + let call = format!("second({first}, {second})"); + let source = format!( + "function second[T](first: T, last: T) returns T:\n return last\nfunction main() returns nothing:\n println({call})\n return nothing\n" + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let start = source.find(&call).unwrap() as u32; + let ty = result.type_map[&sp(start, start + call.len() as u32)]; + assert_eq!(result.interner.type_name(ty), expected); + } + } + } + #[test] + fn generic_return_context_fills_nested_empty_and_absent_slots() { assert!( - errors.iter().any(|d| d.code.code() == 603), - "expected E0603, got: {:?}", - errors + check_source_errors( + r#"function identity[T](value: T) returns T: + return value +function main() returns nothing: + list[string] values = identity(list()) + optional[list[string]] absent = identity(none) + result[list[string], string] success = identity(ok(list())) + trace values + trace absent + trace success + return nothing +"#, + ) + .is_empty() ); } #[test] - fn json_serialize_public_allows_struct_with_secret_fields() { - let result = check_source_result( - "\ -struct User: - id: string - api_key: secret[string] - -function main(view user: User) returns string: - return json.serialize_public[User](view user) -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn inferred_generic_witnesses_peel_root_aliases_and_preserve_nested_aliases() { + for (parameter, setup, argument, expected_name, expected_kind) in [ + ( + "T", + " Count value = 7\n", + "value", + "int64", + "primitive_type", + ), + ( + "result[T, string]", + " result[Count, string] value = ok(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "list[T]", + " list[Count] value = list(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "result[T, string]", + " Wrapped value = ok(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "function() returns T", + "", + "count_callback", + "app.Count", + "alias_type", + ), + ( + "T", + "", + "count_callback", + "function() returns app.Count", + "function_type", + ), + ( + "function(view T) returns T", + "", + "view_callback", + "app.Count", + "alias_type", + ), + ] { + let source = format!( + r#"namespace app +type Count = int64 +type Wrapped = result[Count, string] +function count_callback() returns Count: + return 7 +function view_callback(view value: Count) returns Count: + return value +function choose[T](input: {parameter}) returns string: + return type.name[T]() +function main() returns string: +{setup} return choose({argument}) +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = result + .generic_calls + .values() + .next() + .expect("inferred choose call"); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, expected_name, + "{source}" + ); + assert_eq!( + call.specialization.type_argument_kinds, + [expected_kind], + "{source}" + ); + assert_eq!( + call.concrete_args[0], + if parameter == "T" && argument == "count_callback" { + result.generic_function_instantiations[0].parameter_types[0] + } else { + TypeInterner::INT64 + } + ); + } } #[test] - fn json_serialize_public_rejects_secret_wrapped_value() { - let errors = check_source_errors( - "\ -struct User: - id: string + fn inferred_alias_witnesses_are_unchanged_by_view_and_parentheses() { + for (parameter, setup, value, expected_name, expected_kind) in [ + ( + "T", + " Count value = 7\n", + "value", + "int64", + "primitive_type", + ), + ( + "optional[T]", + " optional[Count] value = some(7)\n", + "value", + "app.Count", + "alias_type", + ), + ( + "T", + " list[Count] value = list(7)\n", + "value", + "list[app.Count]", + "list_type", + ), + ( + "function() returns T", + "", + "count_callback", + "app.Count", + "alias_type", + ), + ] { + for argument in [ + value.to_string(), + format!("({value})"), + format!("view {value}"), + format!("(view ({value}))"), + ] { + let source = format!( + r#"namespace app +type Count = int64 +function count_callback() returns Count: + return 7 +function choose[T](view input: {parameter}) returns string: + return type.name[T]() +function main() returns string: +{setup} return choose({argument}) +"# + ); + let result = check_source_result(&source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{source}\n{errors:?}"); + let call = result + .generic_calls + .values() + .next() + .expect("inferred choose call"); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, expected_name, + "{source}" + ); + assert_eq!( + call.specialization.type_argument_kinds, + [expected_kind], + "{source}" + ); + } + } + } + #[test] + fn deep_inferred_alias_witnesses_have_distinct_checked_specializations() { + let source = r#"namespace app +type Count = int64 +function inspect[T](input: T) returns string: + return type.name[T]() function main() returns string: - secret[User] user = User(id: \"1\") - return json.serialize_public[User](user) -", + optional[Count] counted = some(7) + optional[int64] plain = some(7) + string first = inspect(counted) + string second = inspect(plain) + return "{first}:{second}" +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let [counted, plain] = result.generic_function_instantiations.as_slice() else { + panic!("expected distinct source-aware instances"); + }; + assert_eq!(counted.concrete_args, plain.concrete_args); + assert_eq!( + counted.specialization.type_argument_kinds, + plain.specialization.type_argument_kinds ); - - assert!( - errors.iter().any(|d| d.code.code() == 600), - "expected E0600, got: {:?}", - errors + assert_eq!( + counted.specialization.type_argument_reflections[0].type_name, + "optional[app.Count]" + ); + assert_eq!( + plain.specialization.type_argument_reflections[0].type_name, + "optional[int64]" ); + assert_ne!(counted.specialization, plain.specialization); } #[test] - fn filesystem_read_file_returns_result_string() { - let result = check_source_result( - "\ -function main(fs: Filesystem) returns string: - string raw = Filesystem.read_file(view fs, \"config.json\") handle error: - default \"\" - return raw -", - ); - - let errors: Vec<_> = result + fn inferred_alias_witnesses_follow_declaration_argument_order() { + let source = r#"namespace app +type Count = int64 +type Other = int64 +function choose[T](first: list[T], second: list[T]) returns string: + return type.name[T]() +function main() returns string: + list[Count] counted = list(7) + list[Other] others = list(8) + return choose(second: others, first: counted) +"#; + let result = check_source_result(source); + let errors = result .diagnostics .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let call = result.generic_calls.values().next().unwrap(); + assert_eq!( + call.specialization.type_argument_reflections[0].type_name, + "app.Count" + ); + assert_eq!(call.specialization.type_argument_kinds, ["alias_type"]); } #[test] - fn pure_function_lifts_secret_argument() { - let result = check_source_result( - "\ -function upper(value: string) returns string: - return value - -function main() returns secret[string]: - secret[string] api_key = \"abc\" - return upper(api_key) -", - ); - - let errors: Vec<_> = result + fn inferred_never_witness_merging_preserves_known_alias_siblings() { + let source = r#"namespace app +type Count = int64 +function describe[T](first: T, second: T) returns string: + return type.name[T]() +function outer[E](view empty: list[E]) returns string: + result[Count, E] partial = ok(7) + result[int64, string] full = fail("bad") + return describe(partial, full) +function main() returns string: + return outer(list()) +"#; + let result = check_source_result(source); + let errors = result .diagnostics .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let instance = result + .generic_function_instantiations + .iter() + .find(|instance| { + instance.specialization.type_argument_reflections[0].type_name + == "result[app.Count, string]" + }) + .expect("filled result with preserved Count witness"); + assert_eq!(instance.specialization.type_argument_kinds, ["result_type"]); + let Type::Result(ok, error) = result.interner.resolve(instance.concrete_args[0]) else { + panic!("expected result"); + }; + assert_eq!((*ok, *error), (TypeInterner::INT64, TypeInterner::STRING)); } #[test] - fn impure_function_rejects_secret_argument_without_secret_param() { - let errors = check_source_errors( - "\ -function emit(view stdout: Stdout, value: string) returns nothing: - Stdout.write(view stdout, value) - return nothing - -function main(stdout: Stdout) returns nothing: - secret[string] api_key = \"abc\" - emit(view stdout, api_key) - return nothing -", + fn inferred_alias_kind_and_info_select_the_same_reflection_branch() { + let source = r#"namespace app +type Count = int64 +function inspect[T](input: list[T]) returns string: + if type.kind_tag[T]() == TypeKind.alias_type: + return type.name[T]() + else: + return "primitive" +function main() returns string: + list[Count] values = list(7) + return inspect(values) +"#; + let result = check_source_result(source); + let errors = result + .diagnostics + .iter() + .filter(|error| error.severity == jett_diagnostics::Severity::Error) + .collect::>(); + assert!(errors.is_empty(), "{errors:?}"); + let instance = result.generic_function_instantiations.first().unwrap(); + assert_eq!(instance.specialization.type_argument_kinds, ["alias_type"]); + assert_eq!( + instance.specialization.type_argument_reflections[0].type_name, + "app.Count" ); - assert!( - errors.iter().any(|d| d.code.code() == 600), - "expected E0600, got: {:?}", - errors + instance + .static_selections + .values() + .any(|selection| *selection == CheckedStaticSelection::IfThen) ); } +} + +#[cfg(test)] +mod resource_hook_span_tests { + use super::*; + use jett_common::{STDLIB_FILE_ID_START, SourceOrigin}; + use jett_resolve::{ResourceKernelSpec, resolve_with_resource_kernels}; + use jett_types::ResourceKernelRecipe; #[test] - fn impure_function_accepts_explicit_secret_param() { - let result = check_source_result( - "\ -function send_secret(view stdout: Stdout, value: secret[string]) returns nothing: - string redacted = secret.redact(value) - Stdout.write(view stdout, redacted) - return nothing + fn synthetic_resource_functions_do_not_replace_source_declaration_fallback() { + let file = FileId::new(STDLIB_FILE_ID_START); + let parsed = jett_parser::parse("namespace resource_probe\nresource TestHandle\n", file); + assert!(parsed.errors.is_empty()); + let Item::Resource(resource) = &parsed.module.items[1] else { + panic!("missing resource") + }; + let specs = [ResourceKernelSpec { + resource_declaration: resource.name.span, + member: "kernel_close".into(), + recipe: ResourceKernelRecipe::Finalize, + }]; + let resolved = resolve_with_resource_kernels( + &parsed.module, + &HashMap::from([(file, SourceOrigin::Stdlib)]), + &specs, + ) + .unwrap(); + let resource_definition = resolved.resolutions[&resource.name.span]; + let kernel_definition = resolved.resource_kernels.iter().next().unwrap().definition; + assert_eq!( + resolved.scope_table.def(resource_definition).span, + resolved.scope_table.def(kernel_definition).span + ); + let checker = TypeChecker::new(&resolved, CheckOptions::default()); + assert_eq!( + checker.decl_defs.get(&resource.name.span), + Some(&resource_definition) + ); + } +} -function main(stdout: Stdout) returns nothing: - secret[string] api_key = \"abc\" - send_secret(view stdout, api_key) - return nothing -", +#[cfg(test)] +mod caller_ownership_source_tests { + use super::*; + use jett_common::STDLIB_FILE_ID_START; + + fn checked(source: &str, release: bool, stdlib: bool) -> CheckResult { + let parsed = jett_parser::parse( + source, + FileId::new(if stdlib { STDLIB_FILE_ID_START } else { 0 }), + ); + assert!(parsed.errors.is_empty(), "parse: {:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), + "resolve: {:?}", + resolved.diagnostics ); + check_with_options(&parsed.module, &resolved, CheckOptions { release }) + } - let errors: Vec<_> = result + fn errors(checked: &CheckResult) -> Vec<&Diagnostic> { + checked .diagnostics .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + .collect() } - #[test] - fn refinement_type_assignment_requires_handle_error() { - let errors = check_source_errors( - "\ -type Port = int64 where value >= 1 && value <= 65535 + const OBSERVER: &str = + "namespace app\nfunction inspect(view values: list[int64]) returns int64:\n return 7\n"; -function main() returns nothing: - Port port = 8080 - return nothing -", - ); + #[test] + fn caller_facts_bare_view_formals_consume_exact_source_bindings() { + for invocation in [ + "inspect(values)", + "app.inspect(values)", + "(inspect)(values)", + ] { + let source = format!( + "{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 number = {invocation}\n return values\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{invocation}: {failures:?}"); + assert_eq!(failures[0].code.code(), 400); + let packet = result + .call_ownership + .values() + .next() + .expect("checked source call"); + let [argument] = packet.arguments.as_slice() else { + panic!("one source operand"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::Bare); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + let CheckedCallerOrigin::Binding(binding) = &argument.origin else { + panic!("exact binding origin"); + }; + assert_eq!(binding.ty, argument.actual_type); + assert_eq!(binding.mode, CheckedBindingMode::Owned); + assert!( + result + .binding_facts + .values() + .any(|fact| fact.definition == binding.definition + && fact.declaration_span == binding.declaration_span) + ); + } + } + } - assert!( - errors.iter().any(|d| d.code.code() == 333), - "expected E0333, got: {:?}", - errors + #[test] + fn caller_facts_written_views_retain_owners_but_cannot_supply_owned_formals() { + let source = format!( + "{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 number = inspect(view values)\n return values\n" ); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let argument = &result.call_ownership.values().next().unwrap().arguments[0]; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + let result = checked( + "function consume(values: list[int64]) returns nothing:\n return nothing\nfunction test(view values: list[int64]) returns nothing:\n consume(view values)\n", + release, + false, + ); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 375); + } } #[test] - fn refinement_type_assignment_with_handle_and_coarsen_typechecks() { - let result = check_source_result( - "\ -type Port = int64 where value >= 1 && value <= 65535 - -function main() returns int64: - Port port = 8080 handle error: - return 80 - return coarsen port -", - ); + fn caller_facts_known_borrowed_bare_arguments_have_one_precise_error() { + for body in [ + " int64 number = inspect(values)\n", + " list[int64] borrowed = view values\n list[int64] forwarded = borrowed\n int64 number = inspect(forwarded)\n", + ] { + let source = format!( + "{OBSERVER}function test(view values: list[int64]) returns nothing:\n{body} return nothing\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 401); + assert!(failures[0].message.contains("borrowed value bare")); + assert!(result.binding_facts.values().any(|fact| matches!( + fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Other + } + ))); + } + } + } - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + #[test] + fn caller_facts_named_calls_join_formals_without_reordering_sources() { + let source = "function choose(view first: list[int64], view second: list[int64]) returns int64:\n return 1\nfunction test(left: list[int64], right: list[int64]) returns int64:\n return choose(second: right, first: left)\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + assert!( + packet + .arguments + .iter() + .all(|argument| argument.effect == CheckedCallerEffect::RelinquishOwned) + ); + let source_text = packet + .arguments + .iter() + .map(|argument| { + &source[argument.source_span.start as usize..argument.source_span.end as usize] + }) + .collect::>(); + assert!( + source_text[0].contains("right") && source_text[1].contains("left"), + "{source_text:?}" + ); + } } #[test] - fn simple_type_alias_behaves_like_base_type() { - let result = check_source_result( - "\ -type UserId = int64 + fn caller_facts_pipeline_source_zero_keeps_its_written_view_witness() { + for (step, expected) in [ + ("into inspect()", CheckedCallerEffect::RelinquishOwned), + ("into view inspect()", CheckedCallerEffect::RetainBorrow), + ] { + let source = format!( + "{OBSERVER}function test(values: list[int64]) returns int64:\n return values {step}\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!(packet.arguments[0].source_index, 0); + assert_eq!(packet.arguments[0].parameter_index, 0); + assert_eq!(packet.arguments[0].effect, expected); + assert_eq!( + packet.arguments[0].syntax == CheckedCallerSyntax::WrittenView, + expected == CheckedCallerEffect::RetainBorrow + ); + } + } + } -function id(value: UserId) returns UserId: - return value + #[test] + fn caller_facts_generic_bodies_keep_concrete_types_and_lexical_contexts() { + let source = "function terminal[T](view value: T) returns int64:\n return 7\nfunction inspect[T](view value: T) returns int64:\n return terminal[T](view value)\nfunction ordinary(values: list[int64]) returns int64:\n int64 scalar = inspect[int64](3)\n return inspect[list[int64]](values)\nverify observations:\n list[int64] values = list(7)\n assert inspect[list[int64]](values) == 7\n assert inspect[list[int64]](values) == 7\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let observed = result + .call_ownership + .values() + .filter(|packet| packet.context == CheckedOwnershipContext::Verify) + .collect::>(); + assert_eq!(observed.len(), 2); + assert!(observed.iter().all(|packet| packet.arguments[0].effect + == CheckedCallerEffect::ObserveData + && packet.arguments[0].syntax == CheckedCallerSyntax::Bare)); + let inner = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.call_ownership.values()) + .collect::>(); + assert_eq!(inner.len(), 2); + assert!( + inner + .iter() + .all(|packet| packet.context == CheckedOwnershipContext::Ordinary) + ); + assert!(inner.iter().any(|packet| packet.arguments[0].effect + == CheckedCallerEffect::Copy + && packet.arguments[0].actual_type == TypeInterner::INT64)); + assert!(inner.iter().any(|packet| packet.arguments[0].effect + == CheckedCallerEffect::RetainBorrow + && matches!( + result.interner.resolve(packet.arguments[0].actual_type), + Type::List(_) + ))); + } + } -function main() returns int64: - UserId user_id = 42 - return id(user_id) -", - ); + #[test] + fn caller_facts_reflected_body_packets_do_not_flatten_field_types() { + let source = "struct Sample:\n count: int64\n values: list[int64]\nfunction probe[T](view value: T) returns nothing:\n return nothing\nfunction inspect[T](view value: T) returns nothing:\n for field in type.fields[T]():\n comptime type Field = field.type_info:\n Field original = type.field_value[T, Field](view value, view field)\n probe[Field](view original)\n return nothing\nfunction main() returns nothing:\n Sample sample = Sample(count: 1, values: list(2))\n inspect[Sample](view sample)\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let bodies = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(bodies.len(), 2); + let arguments = bodies + .iter() + .flat_map(|body| body.body.call_ownership.values()) + .filter(|packet| matches!(packet.target, CheckedInvocationTarget::Generic(_))) + .map(|packet| &packet.arguments[0]) + .collect::>(); + assert_eq!(arguments.len(), 2); + assert!( + arguments + .iter() + .any(|argument| argument.actual_type == TypeInterner::INT64 + && argument.effect == CheckedCallerEffect::Copy) + ); + assert!(arguments.iter().any(|argument| matches!( + result.interner.resolve(argument.actual_type), + Type::List(_) + ) && argument.effect + == CheckedCallerEffect::RetainBorrow)); + assert!( + bodies + .iter() + .all(|body| !body.body.binding_facts.is_empty()) + ); + } + } - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + #[test] + fn caller_facts_resource_parameters_use_same_source_rule_without_runtime_authority() { + let prefix = "namespace audit\nresource Token\nfunction observe(view token: Token) returns int64:\n return 1\n"; + for release in [false, true] { + let result = checked( + &format!( + "{prefix}function last(token: Token) returns int64:\n return observe(token)\n" + ), + release, + true, + ); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert!(result.resource_hooks.is_empty()); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RelinquishOwned + ); + let result = checked( + &format!( + "{prefix}function reject(token: Token) returns Token:\n int64 number = observe(token)\n return token\n" + ), + release, + true, + ); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 400); + let result = checked( + &format!( + "{prefix}function retain(token: Token) returns Token:\n int64 number = observe(view token)\n return token\n" + ), + release, + true, + ); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + } } #[test] - fn bare_json_value_is_unknown() { - let errors = check_source_errors( - "\ -function main() returns JsonValue: - return nothing -", + fn caller_facts_closed_variadic_print_records_each_source_occurrence() { + let source = "function sample(values: list[int64]) returns list[int64]:\n println(1, view values, \"tail\")\n return values\n"; + let result = checked(source, false, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result.call_ownership.values().next().unwrap(); + assert_eq!( + packet.target, + CheckedInvocationTarget::Intrinsic(IntrinsicId::Println) ); - + let CheckedInvocationShape::Intrinsic { operands, .. } = &packet.shape else { + panic!("closed variadic shape"); + }; + assert_eq!(operands.len(), 3); assert!( - errors + operands .iter() - .any(|diag| diag.code.code() == 309 && diag.message.contains("JsonValue")), - "expected bare JsonValue to be unknown, got {errors:?}" + .all(|role| matches!(role, CheckedIntrinsicOperandRole::PrintArgument { .. })) + ); + assert_eq!( + packet + .arguments + .iter() + .map(|argument| argument.effect) + .collect::>(), + [ + CheckedCallerEffect::Copy, + CheckedCallerEffect::RetainBorrow, + CheckedCallerEffect::Copy + ] ); } + #[test] + fn caller_facts_owned_mode_preserves_run_pending_initialization() { + let source = "function main() returns nothing:\n int64 task = run 7\n cancel task\n return nothing\n"; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + } + } + #[test] + fn caller_facts_observation_context_does_not_supply_owned_intrinsic_access() { + let source = "verify invalid_source_access:\n int64 count = 3\n list[int64] generated = range(view count)\n assert true\n"; + for release in [false, true] { + let result = checked(source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{failures:?}"); + assert_eq!(failures[0].code.code(), 375); + } + } #[test] - fn coarsen_can_target_refinement_ancestors() { - let result = check_source_result( - "\ -type NonEmpty = string where value != \"\" -type Password = NonEmpty where value != \"short\" + fn caller_facts_scoped_calls_keep_only_exact_referenced_capture_roots() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function probe[T](view value: T) returns nothing: + return nothing +function probe_count(value: int64) returns int64: + return value +function inspect[T](view value: T, size: int64, spare: int64) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + probe[T](view value) + probe[list[int64]](view value.values) + int64 count = probe_count(value.count) + function() returns int64 read = function() returns int64: + return probe_count(size) + int64 captured = read() + return nothing +function main() returns nothing: + Sample sample = Sample(count: 1, values: list(2)) + inspect[Sample](view sample, 3, 4) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let bodies = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(bodies.len(), 2); + for body in bodies { + let facts = &body.body.binding_facts; + assert!(facts.values().any(|fact| &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "size")); + assert!(!facts.values().any(|fact| &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "spare")); + let mut direct = false; + let mut projected = false; + let mut copied = false; + for packet in body.body.call_ownership.values() { + for argument in &packet.arguments { + match &argument.origin { + CheckedCallerOrigin::Binding(fact) => { + assert_eq!(facts.get(&fact.declaration_span), Some(fact)); + direct = true; + } + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(definition), + } => { + assert!(facts.values().any(|fact| fact.definition == *definition)); + projected = true; + } + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(definition), + } => { + assert!(facts.values().any(|fact| fact.definition == *definition)); + copied = true; + } + _ => {} + } + } + } + assert!(direct && projected && copied); + } + } + } -function main() returns string: - Password password = \"hunter42!\" handle error: - return \"\" - NonEmpty non_empty = coarsen password - return coarsen non_empty -", - ); + #[test] + fn scoped_local_views_retain_exact_transitive_outer_binding_facts() { + let source = r#"namespace app +struct Packet: + values: list[int64] +function inspect(mutable source: Packet, spare: Packet) returns nothing: + Packet borrowed = view source + Packet forwarded = borrowed + comptime type Outer = type.info[int64](): + comptime type Inner = type.info[string](): + list[int64] alias = view forwarded.values + trace alias + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let outer = result + .comptime_type_bindings + .values() + .flatten() + .find(|body| !body.body.comptime_type_bindings.is_empty()) + .unwrap(); + let inner = outer + .body + .comptime_type_bindings + .values() + .flatten() + .next() + .unwrap(); + for body in [&outer.body, &inner.body] { + let fact = |name| { + body.binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .copied() + .expect("retained exact binding dependency") + }; + let owner = fact("source"); + let borrowed = fact("borrowed"); + let forwarded = fact("forwarded"); + let alias = fact("alias"); + assert!(owner.mutable); + assert_eq!(owner.mode, CheckedBindingMode::Owned); + for (view, dependency) in + [(borrowed, owner), (forwarded, borrowed), (alias, forwarded)] + { + assert!(!view.mutable); + assert_eq!( + view.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(dependency.definition) + } + ); + assert_eq!( + body.binding_facts.get(&dependency.declaration_span), + Some(&dependency) + ); + } + assert!(!body.binding_facts.values().any(|fact| &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "spare")); + } + } + } - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + #[test] + fn scoped_local_views_keep_generic_copy_and_reflected_view_contexts_distinct() { + let source = r#"namespace app +struct Metadata: + first: int64 + second: string +struct Holder[T]: + value: T +function inspect[T](mutable source: Holder[T], spare: Holder[T]) returns nothing: + for field in type.fields[Metadata](): + comptime type Field = field.type_info: + T alias = view source.value + trace alias + return nothing +function main() returns nothing: + inspect[int64](Holder[int64](value: 7), Holder[int64](value: 8)) + inspect[list[int64]](Holder[list[int64]](value: list(1)), Holder[list[int64]](value: list(2))) + return nothing +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error) + ); + for release in [false, true] { + let result = check_with_options(&parsed.module, &resolved, CheckOptions { release }); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.generic_function_instantiations.len(), 2); + let mut copies = 0; + let mut views = 0; + for instance in &result.generic_function_instantiations { + let bodies = instance + .comptime_type_bindings + .values() + .flatten() + .collect::>(); + assert_eq!(bodies.len(), 2); + for (index, body) in bodies.iter().enumerate() { + assert_eq!( + body.selection, + CheckedComptimeTypeSelection::ReflectedIteration(index) + ); + let fact = |name| { + body.body + .binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .copied() + }; + let alias = fact("alias").unwrap(); + assert!(!alias.mutable); + assert_eq!(alias.ty, instance.concrete_args[0]); + assert!(fact("spare").is_none()); + if alias.ty == TypeInterner::INT64 { + copies += 1; + assert_eq!(alias.mode, CheckedBindingMode::Owned); + assert!(fact("source").is_none()); + } else { + views += 1; + assert!( + matches!(result.interner.resolve(alias.ty), Type::List(inner) if *inner == TypeInterner::INT64) + ); + let owner = fact("source").unwrap(); + assert!(owner.mutable); + assert_eq!(owner.ty, instance.parameter_types[0]); + assert_eq!(owner.mode, CheckedBindingMode::Owned); + assert_eq!( + alias.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(owner.definition) + } + ); + assert_eq!( + body.body.binding_facts.get(&owner.declaration_span), + instance.binding_facts.get(&owner.declaration_span) + ); + } + } + } + assert_eq!((copies, views), (2, 2)); + let copy_context = result + .generic_function_instantiations + .iter() + .find(|instance| instance.concrete_args == [TypeInterner::INT64]) + .unwrap(); + let view_context = result + .generic_function_instantiations + .iter() + .find(|instance| instance.concrete_args != [TypeInterner::INT64]) + .unwrap(); + assert_ne!( + copy_context.parameter_types[0], + view_context.parameter_types[0] + ); + let copy_body = ©_context + .comptime_type_bindings + .values() + .flatten() + .next() + .unwrap() + .body; + let view_body = &view_context + .comptime_type_bindings + .values() + .flatten() + .next() + .unwrap() + .body; + let alias = view_body + .binding_facts + .values() + .find(|fact| { + &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == "alias" + }) + .copied() + .unwrap(); + let mut checker = TypeChecker::new(&resolved, CheckOptions { release }); + // Same Source spans and DefIds in another generic context do not + // authorize filling a selected copy body's absent view capture. + checker.binding_facts = view_context.binding_facts.clone(); + let mut retained = HashMap::from([(alias.declaration_span, alias)]); + checker.retain_local_view_dependencies(©_body.binding_facts, &mut retained); + assert_eq!(retained, HashMap::from([(alias.declaration_span, alias)])); + } } #[test] - fn refinement_constraint_must_return_bool() { - let errors = check_source_errors( - "\ -type Broken = int64 where 42 + fn scoped_local_view_dependency_retention_never_falls_back_or_invents_ownership() { + let source = r#"namespace app +function inspect(mutable source: list[int64], spare: list[int64]) returns nothing: + comptime type Inner = type.info[int64](): + list[int64] alias = view source + trace alias + return nothing +"#; + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error) + ); + let result = check(&parsed.module, &resolved); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let body = &result + .comptime_type_bindings + .values() + .flatten() + .next() + .unwrap() + .body; + let named = |name| { + result + .binding_facts + .values() + .find(|fact| { + &source + [fact.declaration_span.start as usize..fact.declaration_span.end as usize] + == name + }) + .copied() + .unwrap() + }; + let owner = named("source"); + let spare = named("spare"); + let alias = named("alias"); + let mut checker = TypeChecker::new(&resolved, CheckOptions::default()); + // The valid flat root must not rescue an absent selected dependency. + checker.binding_facts = result.binding_facts.clone(); + for invalid in ["missing", "definition", "declaration", "foreign"] { + let mut selected = body.binding_facts.clone(); + selected.remove(&owner.declaration_span); + if invalid != "missing" { + let mut changed = owner; + match invalid { + "definition" => changed.definition = spare.definition, + "declaration" => changed.declaration_span = spare.declaration_span, + "foreign" => changed.definition = DefId::new(u32::MAX), + _ => unreachable!(), + } + selected.insert(owner.declaration_span, changed); + } + let mut retained = HashMap::from([(alias.declaration_span, alias)]); + checker.retain_local_view_dependencies(&selected, &mut retained); + assert_eq!( + retained, + HashMap::from([(alias.declaration_span, alias)]), + "{invalid}" + ); + } + let foreign_alias = CheckedBindingFact { + mode: CheckedBindingMode::View { + source: CheckedViewSource::Binding(DefId::new(u32::MAX)), + }, + ..alias + }; + let mut retained = HashMap::from([(foreign_alias.declaration_span, foreign_alias)]); + checker.retain_local_view_dependencies(&body.binding_facts, &mut retained); + assert_eq!(retained.len(), 1); + for mode in [ + CheckedBindingMode::View { + source: CheckedViewSource::Other, + }, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(alias.definition), + }, + ] { + let mut selected = body.binding_facts.clone(); + let changed = CheckedBindingFact { mode, ..owner }; + selected.insert(owner.declaration_span, changed); + let mut retained = HashMap::from([(alias.declaration_span, alias)]); + checker.retain_local_view_dependencies(&selected, &mut retained); + assert_eq!(retained.get(&owner.declaration_span), Some(&changed)); + assert_eq!(retained.get(&alias.declaration_span), Some(&alias)); + assert_eq!(retained.len(), 2); + } + } -function main() returns nothing: + #[test] + fn caller_facts_named_descriptors_and_baked_constants_are_producers() { + let source = r#"namespace app +int64 answer = 7 +function identity(value: int64) returns int64: + return value +function accept(callback: function(int64) returns int64, count: int64) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 335), - "expected E0335, got: {:?}", - errors - ); +function main() returns nothing: + accept(identity, answer) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("one source call"); + assert_eq!(packet.arguments.len(), 2); + assert!( + packet + .arguments + .iter() + .all(|argument| argument.origin == CheckedCallerOrigin::OwnedExpression) + ); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + assert_eq!(packet.arguments[1].effect, CheckedCallerEffect::Copy); + } } #[test] - fn struct_constructor_with_refinement_field_requires_handle() { - let errors = check_source_errors( - "\ -type Age = int64 where value >= 0 && value < 150 - -struct User: - age: Age - -function main() returns nothing: - User user = User(age: 42) + fn caller_facts_local_descriptors_keep_their_exact_binding_identity() { + let source = r#"namespace app +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 316), - "expected E0316, got: {:?}", - errors - ); +function main() returns nothing: + function(int64) returns int64 stored = identity + inspect(view stored) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let argument = &result + .call_ownership + .values() + .next() + .expect("one source call") + .arguments[0]; + let CheckedCallerOrigin::Binding(fact) = &argument.origin else { + panic!("local descriptor binding"); + }; + assert_eq!( + &source[fact.declaration_span.start as usize..fact.declaration_span.end as usize], + "stored" + ); + assert_eq!(result.binding_facts.get(&fact.declaration_span), Some(fact)); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + } } #[test] - fn struct_constructor_with_refinement_field_handle_typechecks() { - let result = check_source_result( - "\ -type Age = int64 where value >= 0 && value < 150 + fn caller_facts_pipeline_sources_keep_each_preceding_output_span() { + let source = r#"namespace app +function identity(view values: list[int64]) returns list[int64]: + return clone values +function main(values: list[int64]) returns list[int64]: + return values into view identity() into identity() +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 2); + assert_eq!( + &source[calls[0].1.arguments[0].source_span.start as usize + ..calls[0].1.arguments[0].source_span.end as usize], + "values" + ); + assert_eq!( + calls[0].1.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!(calls[1].1.arguments[0].source_span, *calls[0].0); + assert_eq!( + calls[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert_eq!( + calls[1].1.arguments[0].effect, + CheckedCallerEffect::RelinquishOwned + ); + } + } + #[test] + fn caller_facts_interface_qualified_concrete_calls_use_the_selected_body_signature() { + let source = r#"namespace app +interface Scored: + function score(view self: Scored, bonus: int64) returns int64 struct User: - age: Age - -function main() returns nothing: - User user = User(age: 42) handle error: - return nothing - return nothing -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + points: int64 +implement Scored for User: + function score(view self: User, bonus: int64) returns int64: + return self.points + bonus +function main() returns int64: + User user = User(points: 7) + return Scored.score(bonus: 5, self: view user) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("selected method source call"); + let CheckedInvocationTarget::Method(call) = &packet.target else { + panic!("concrete source method"); + }; + let definition = result + .method_definitions + .iter() + .find(|definition| definition.source_span == call.source_span) + .unwrap(); + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!(params, &definition.parameter_types); + assert_eq!(*return_type, definition.return_type); + assert_eq!(view_params, &[true, false]); + assert_eq!(params[0], definition.owner_type); + assert!(matches!( + result.interner.resolve(params[0]), + Type::Struct(_) + )); + assert_eq!( + packet + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + assert_eq!(packet.arguments[1].callee_access, CheckedCalleeAccess::View); + } } #[test] - fn refined_return_type_accepts_base_expression() { - let result = check_source_result( - "\ -type Percentage = float64 where value >= 0.0 && value <= 100.0 - -function calculate_grade(score: int64, total: int64) returns Percentage: - float64 score_f = float64.from_int64(score) handle error: - default 0.0 - float64 total_f = float64.from_int64(total) handle error: - default 1.0 - return score_f / total_f * 100.0 -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn caller_facts_inherent_calls_keep_registered_source_formal_modes() { + let source = r#"namespace app +struct User: + points: int64 + function score(view self: User, view values: list[int64]) returns int64: + return self.points +function main() returns int64: + User user = User(points: 7) + list[int64] values = list(5) + return User.score(view user, values) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .next() + .expect("selected inherent source call"); + let CheckedInvocationTarget::Method(call) = &packet.target else { + panic!("source method"); + }; + let definition = result + .method_definitions + .iter() + .find(|definition| definition.source_span == call.source_span) + .unwrap(); + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!(params, &definition.parameter_types); + assert_eq!(*return_type, definition.return_type); + assert_eq!(view_params, &[true, true]); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + packet.arguments[1].effect, + CheckedCallerEffect::RelinquishOwned + ); + } } #[test] - fn refined_return_type_still_requires_handle_for_result_values() { - let errors = check_source_errors( - "\ -type Port = int64 where value >= 1 && value <= 65535 - -function parse_port(raw: string) returns Port: - return int64.from_string(raw) -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 316), - "expected E0316, got: {:?}", - errors - ); + fn caller_facts_erased_calls_keep_the_selected_interface_slot_signature() { + let source = r#"namespace app +interface Scored: + function score(view self: Scored, bonus: int64) returns int64 +struct User: + points: int64 +implement Scored for User: + function score(view self: User, bonus: int64) returns int64: + return self.points + bonus +function inspect(view item: Scored) returns int64: + return Scored.score(view item, 5) +function main() returns int64: + User user = User(points: 7) + return inspect(view user) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let packet = result + .call_ownership + .values() + .find(|packet| matches!(packet.target, CheckedInvocationTarget::Interface(_))) + .expect("selected interface source call"); + let CheckedInvocationTarget::Interface(call) = &packet.target else { + unreachable!(); + }; + let Type::Interface(id) = result.interner.resolve(call.interface_type) else { + panic!("interface owner"); + }; + let slot = &result.interner.resolve_interface(*id).methods[call.method_index]; + let CheckedInvocationShape::Function { signature_type } = &packet.shape else { + panic!("function shape"); + }; + let Type::Function { + params, + view_params, + return_type, + } = result.interner.resolve(*signature_type) + else { + panic!("function signature"); + }; + assert_eq!( + params, + &slot.params.iter().map(|(_, ty, _)| *ty).collect::>() + ); + assert_eq!( + view_params, + &slot + .params + .iter() + .map(|(_, _, view)| *view) + .collect::>() + ); + assert_eq!(*return_type, slot.return_type); + assert_eq!(params[0], call.interface_type); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + } } #[test] - fn function_call_into_refinement_parameter_reports_boundary_error() { - let errors = check_source_errors( - "\ -type Password = string where string.char_count(value) > 8 - -function create_user(password: Password) returns nothing: + fn caller_facts_source_actual_types_survive_binding_field_and_producer_promotion() { + let source = r#"namespace app +struct Holder: + values: list[int64] +function inspect(view values: secret[list[int64]]) returns nothing: return nothing - +function produce() returns list[int64]: + return list(7) function main() returns nothing: - create_user(\"hunter42!\") - return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 333), - "expected E0333, got: {:?}", - errors - ); + list[int64] values = list(7) + inspect(view values) + inspect(view produce()) + Holder holder = Holder(values: list(8)) + inspect(view holder.values) + secret[list[int64]] classified = clone values + inspect(view classified) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result + .call_ownership + .iter() + .filter(|(_, packet)| packet.arguments.len() == 1) + .collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 4); + for (index, (_, packet)) in calls.iter().enumerate() { + let argument = &packet.arguments[0]; + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + assert!(matches!( + result.interner.resolve(argument.parameter_type), + Type::Secret(_) + )); + if index < 3 { + assert!(matches!( + result.interner.resolve(argument.actual_type), + Type::List(_) + )); + assert_ne!(argument.actual_type, argument.parameter_type); + } else { + assert_eq!(argument.actual_type, argument.parameter_type); + } + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + } + let CheckedCallerOrigin::Binding(binding) = calls[0].1.arguments[0].origin else { + panic!("ordinary binding"); + }; + assert_eq!(binding.ty, calls[0].1.arguments[0].actual_type); + assert_eq!( + calls[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert!(matches!( + calls[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { .. } + )); + } } #[test] - fn bitfield_constructor_with_literals_typechecks() { - let result = check_source_result( - "\ -bitfield TcpFlags: - syn: 1 bit - ack: 1 bit - -function main() returns int64: - TcpFlags flags = TcpFlags(syn: 0, ack: 1) - return flags.ack -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn caller_facts_source_actual_types_keep_contextual_width_and_container_inference() { + let source = r#"namespace app +function scalar(view value: secret[int8]) returns nothing: + return nothing +function sequence(view values: secret[list[int8]]) returns nothing: + return nothing +function optional_value(view value: secret[optional[int8]]) returns nothing: + return nothing +function result_value(view value: secret[result[int8, string]]) returns nothing: + return nothing +function mapped(view values: secret[map[string, int8]]) returns nothing: + return nothing +function main() returns nothing: + scalar(7) + sequence(list()) + optional_value(some(7)) + optional_value(none) + result_value(ok(7)) + result_value(fail("error")) + mapped(map("one": 7)) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 7); + let expected = [ + "scalar", + "sequence", + "optional_value", + "optional_value", + "result_value", + "result_value", + "mapped", + ]; + for ((_, packet), name) in calls.iter().zip(expected) { + let [argument] = packet.arguments.as_slice() else { + panic!("one source occurrence"); + }; + let Type::Secret(inner) = result.interner.resolve(argument.parameter_type) else { + panic!("outer Secret formal for {name}"); + }; + assert_eq!(argument.actual_type, *inner, "{name}"); + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(inner) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + } + assert_eq!(calls[0].1.arguments[0].actual_type, TypeInterner::INT8); + assert!( + matches!(result.interner.resolve(calls[1].1.arguments[0].actual_type), Type::List(inner) if *inner == TypeInterner::INT8) + ); + } } #[test] - fn bitfield_constructor_with_dynamic_int_requires_handle() { - let errors = check_source_errors( - "\ -bitfield TcpFlags: - syn: 1 bit - ack: 1 bit - -function main(bit: int64) returns nothing: - TcpFlags flags = TcpFlags(syn: bit, ack: 0) + fn caller_facts_source_actual_types_are_concrete_in_generic_and_reflected_frames() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function observe[T](view value: secret[T]) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 316), - "expected E0316, got: {:?}", - errors - ); +function forward[T](view value: T) returns nothing: + observe[T](view value) +function reflected[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + Field original = type.field_value[T, Field](view value, view field) + observe[Field](view original) +function main() returns nothing: + list[int64] values = list(7) + forward[int64](1) + forward[list[int64]](view values) + Sample sample = Sample(count: 2, values: list(3)) + reflected[Sample](view sample) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let direct = result + .generic_function_instantiations + .iter() + .filter(|body| body.comptime_type_bindings.is_empty()) + .flat_map(|body| { + body.call_ownership + .values() + .filter(|packet| { + matches!(packet.target, CheckedInvocationTarget::Generic(_)) + }) + .map(move |packet| (body, packet)) + }) + .collect::>(); + // Reflected calls live only in the two exact scoped body snapshots. + assert_eq!(direct.len(), 2); + for (body, packet) in direct { + let argument = &packet.arguments[0]; + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + body.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + } + let scoped = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(scoped.len(), 2); + let mut actuals = Vec::new(); + for scope in scoped { + let packet = scope + .body + .call_ownership + .values() + .find(|packet| matches!(packet.target, CheckedInvocationTarget::Generic(_))) + .unwrap(); + let argument = &packet.arguments[0]; + assert_eq!( + scope.body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + scope.body.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + actuals.push(argument.actual_type); + } + assert!(actuals.contains(&TypeInterner::INT64)); + assert!( + actuals + .iter() + .any(|ty| matches!(result.interner.resolve(*ty), Type::List(_))) + ); + } } #[test] - fn bitfield_literal_out_of_range_reports_error() { - let errors = check_source_errors( - "\ -bitfield ColorChannel: - red: 8 bits - green: 8 bits - -function main() returns nothing: - ColorChannel color = ColorChannel(red: 300, green: 1) + fn caller_facts_source_actual_types_preserve_secret_children_and_binary_taint() { + let source = r#"namespace app +function sequence(view values: secret[list[secret[int8]]]) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 337), - "expected E0337, got: {:?}", - errors - ); +function optional_value(view value: secret[optional[secret[int8]]]) returns nothing: + return nothing +function result_value(view value: secret[result[secret[int8], string]]) returns nothing: + return nothing +function scalar(view value: secret[int8]) returns nothing: + return nothing +function main() returns nothing: + secret[int8] sequence_child = 7 + secret[int8] optional_child = 8 + secret[int8] result_child = 9 + secret[int8] hidden = 10 + sequence(list(sequence_child)) + optional_value(some(optional_child)) + result_value(ok(result_child)) + scalar(1 + 2) + scalar(hidden + 2) + scalar(2 + hidden) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 6); + for (_, packet) in &calls { + let [argument] = packet.arguments.as_slice() else { + panic!("one source occurrence"); + }; + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + result.type_map.get(&argument.source_span), + Some(&argument.parameter_type) + ); + } + let raw_payload = |index: usize| { + let actual = calls[index].1.arguments[0].actual_type; + let payload = match result.interner.resolve(actual) { + Type::List(payload) | Type::Optional(payload) | Type::Result(payload, _) => { + *payload + } + other => panic!("inner container or sum, got {other:?}"), + }; + assert!( + matches!(result.interner.resolve(payload), Type::Secret(inner) if *inner == TypeInterner::INT8) + ); + let Type::Secret(expected_inner) = result + .interner + .resolve(calls[index].1.arguments[0].parameter_type) + else { + panic!("outer Secret formal"); + }; + assert_eq!(*expected_inner, actual); + }; + for index in 0..3 { + raw_payload(index); + } + assert_eq!(calls[3].1.arguments[0].actual_type, TypeInterner::INT8); + for (_, packet) in &calls[4..] { + let argument = &packet.arguments[0]; + assert_eq!(argument.actual_type, argument.parameter_type); + assert!( + matches!(result.interner.resolve(argument.actual_type), Type::Secret(inner) if *inner == TypeInterner::INT8) + ); + } + } } #[test] - fn bitfield_payload_must_be_list_of_uint8() { - let errors = check_source_errors( - "\ -bitfield Packet: - header: 8 bits - payload: bytes -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 336), - "expected E0336, got: {:?}", - errors - ); + fn caller_facts_pipeline_inputs_survive_indirect_field_callee_spans() { + let source = r#"namespace app +enum Choice: + chosen(value: int64) + other(value: int64) +function choose(value: int64) returns Choice: + return Choice.chosen(value) +function increment(value: int64) returns int64: + return value + 1 +struct ChoiceHolder: + item: function(int64) returns Choice +struct NumberHolder: + item: function(int64) returns int64 +function direct() returns Choice: + ChoiceHolder holder = ChoiceHolder(item: choose) + return 4 into holder.item +function chained() returns int64: + NumberHolder holder = NumberHolder(item: increment) + return 4 into holder.item into holder.item +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut inputs = result.pipeline_step_input_types.iter().collect::>(); + inputs.sort_by_key(|(span, _)| span.start); + assert_eq!(inputs.len(), 3); + for (&span, &input) in inputs.iter().copied() { + assert_eq!(input, TypeInterner::INT64); + assert!(matches!( + result.interner.resolve(result.type_map[&span]), + Type::Function { .. } + )); + let packet = &result.call_ownership[&span]; + assert!(matches!( + packet.target, + CheckedInvocationTarget::Indirect(_) + )); + assert_eq!(packet.arguments[0].actual_type, input); + assert_eq!(packet.arguments[0].parameter_type, input); + assert_eq!( + result.source_type_map[&span], + result.pipeline_step_call_types[&span] + ); + } + assert!(matches!( + result.interner.resolve(result.source_type_map[inputs[0].0]), + Type::Enum(_) + )); + } } #[test] - fn bitfield_enum_annotation_rejects_negative_discriminant() { - let errors = check_source_errors( - "\ -enum SignedProtocol: - invalid = -1 - tcp = 6 - -bitfield Header: - protocol: 8 bits as SignedProtocol - -function main() returns nothing: - return nothing -", + fn caller_facts_pipeline_inputs_distinguish_handled_output_and_reject_bad_inputs() { + let prefix = r#"namespace app +function parse(value: string) returns result[int64, string]: + return fail("bad") +function increment(value: int64) returns int64: + return value + 1 +struct Holder: + parse: function(string) returns result[int64, string] + finish: function(int64) returns int64 +"#; + let source = format!( + "{prefix}function main() returns int64:\n Holder holder = Holder(parse: parse, finish: increment)\n return \"x\"\n into holder.parse handle error:\n default 7\n into holder.finish\n" ); - - assert!( - errors.iter().any(|d| d.code.code() == 336), - "expected E0336, got: {:?}", - errors + let invalid = format!( + "{prefix}function invalid() returns result[int64, string]:\n Holder holder = Holder(parse: parse, finish: increment)\n return 4 into holder.parse\n" ); + for release in [false, true] { + let result = checked(&source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut inputs = result.pipeline_step_input_types.iter().collect::>(); + inputs.sort_by_key(|(span, _)| span.start); + assert_eq!(inputs.len(), 2); + assert_eq!(*inputs[0].1, TypeInterner::STRING); + assert_eq!(*inputs[1].1, TypeInterner::INT64); + let first = *inputs[0].0; + assert!( + matches!(result.interner.resolve(result.pipeline_step_call_types[&first]), Type::Result(value, error) if *value == TypeInterner::INT64 && *error == TypeInterner::STRING) + ); + assert_eq!(result.source_type_map[&first], TypeInterner::INT64); + assert_eq!( + result.call_ownership[inputs[1].0].arguments[0].source_span, + first + ); + // The handle extends the first step span; only the following + // unhandled field target shares its complete occurrence span. + assert_eq!(result.type_map[&first], TypeInterner::STRING); + assert!(matches!( + result.interner.resolve(result.type_map[inputs[1].0]), + Type::Function { .. } + )); + let rejected = checked(&invalid, release, false); + assert!( + !errors(&rejected).is_empty(), + "invalid indirect input was admitted" + ); + assert_eq!( + rejected + .pipeline_step_input_types + .values() + .copied() + .collect::>(), + vec![TypeInterner::INT64] + ); + assert!( + errors(&rejected) + .iter() + .any(|diagnostic| diagnostic.message.contains("string") + && diagnostic.message.contains("int64")) + ); + } } #[test] - fn bitfield_binary_roundtrip_typechecks() { - let result = check_source_result( - "\ -bitfield network IpHeader: - version: 4 bits - header_length: 4 bits - total_length: 16 bits - -function main() returns int64: - IpHeader header = IpHeader(version: 4, header_length: 5, total_length: 500) - bytes raw = IpHeader.to_bytes(header) - IpHeader decoded = IpHeader.from_bytes(raw) handle error: - return 0 - return decoded.total_length -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn caller_facts_pipeline_inputs_stay_in_exact_generic_and_reflected_frames() { + let source = r#"namespace app +struct Sample: + count: int64 + values: list[int64] +function scalar(value: int64) returns int64: + return value +function text(value: string) returns string: + return value +function apply[T](value: T, mapper: function(T) returns T) returns T: + return value into mapper +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + Field original = type.field_value[T, Field](view value, view field) + function(Field) returns Field mapper = function(item: Field) returns Field: + return item + Field copied = original into mapper + return nothing +function main() returns nothing: + int64 count = apply[int64](7, scalar) + string label = apply[string]("x", text) + Sample sample = Sample(count: 1, values: list(2)) + inspect[Sample](view sample) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert!(result.pipeline_step_input_types.is_empty()); + let direct = result + .generic_function_instantiations + .iter() + .filter(|body| body.comptime_type_bindings.is_empty()) + .flat_map(|body| { + body.pipeline_step_input_types + .iter() + .map(move |(span, input)| (body, span, input)) + }) + .collect::>(); + assert_eq!(direct.len(), 2); + let mut direct_types = Vec::new(); + for (body, span, input) in direct { + let packet = &body.call_ownership[span]; + assert_eq!(packet.arguments[0].actual_type, *input); + assert!(matches!( + result.interner.resolve(body.type_map[span]), + Type::Function { .. } + )); + assert!(!result.pipeline_step_input_types.contains_key(span)); + direct_types.push(*input); + } + assert!(direct_types.contains(&TypeInterner::INT64)); + assert!(direct_types.contains(&TypeInterner::STRING)); + let scopes = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.comptime_type_bindings.values()) + .flatten() + .collect::>(); + assert_eq!(scopes.len(), 2); + let mut scoped_types = Vec::new(); + for scope in scopes { + assert_eq!(scope.body.pipeline_step_input_types.len(), 1); + let (&span, &input) = scope.body.pipeline_step_input_types.iter().next().unwrap(); + assert_eq!( + scope.body.call_ownership[&span].arguments[0].actual_type, + input + ); + assert!(matches!( + result.interner.resolve(scope.body.type_map[&span]), + Type::Function { .. } + )); + scoped_types.push(input); + } + assert!(scoped_types.contains(&TypeInterner::INT64)); + assert!(scoped_types.iter().any(|input| matches!(result.interner.resolve(*input), Type::List(element) if *element == TypeInterner::INT64))); + } } #[test] - fn bitfield_from_bytes_requires_handle() { - let errors = check_source_errors( - "\ -bitfield network IpHeader: - version: 4 bits - header_length: 4 bits - total_length: 16 bits - -function main() returns nothing: - bytes raw = IpHeader.to_bytes(IpHeader(version: 4, header_length: 5, total_length: 500)) - IpHeader decoded = IpHeader.from_bytes(raw) + fn caller_facts_transformed_explicit_views_retain_borrows_across_call_forms() { + let source = r#"namespace app +type Numbers = list[int64] where true +function inspect(view values: list[int64]) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 316), - "expected E0316, got: {:?}", - errors - ); +function generic[T](view value: T) returns nothing: + return nothing +function retained(view numbers: Numbers, view hidden: secret[list[int64]], callback: function(view list[int64]) returns nothing) returns nothing: + inspect(coarsen (view numbers)) + generic[list[int64]](declassify (view hidden)) + callback((coarsen ((view numbers)))) + (declassify (view hidden)) into inspect + list[int64] first_copy = clone (coarsen numbers) + list[int64] second_copy = clone (declassify hidden) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.call_ownership.len(), 4); + for packet in result.call_ownership.values() { + let [argument] = packet.arguments.as_slice() else { + panic!("one source argument"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert!(matches!(argument.origin, CheckedCallerOrigin::Binding(_))); + assert!( + matches!(result.interner.resolve(argument.actual_type), Type::List(element) if *element == TypeInterner::INT64) + ); + } + } } #[test] - fn trace_statement_reads_variable_without_error() { - let result = check_source_result( - "\ -function main() returns nothing: - int64 total = 42 - trace total -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + fn caller_facts_transformed_views_keep_explicit_and_bare_diagnostics_distinct() { + let prefix = r#"namespace app +type Numbers = list[int64] where true +function consume(values: list[int64]) returns nothing: + return nothing +function inspect(view values: list[int64]) returns nothing: + return nothing +"#; + for (parameter, call, expected_code) in [ + ( + "view values: Numbers", + "consume(coarsen (view values))", + 375, + ), + ( + "view values: secret[list[int64]]", + "consume(declassify (view values))", + 375, + ), + ("view values: Numbers", "consume(coarsen values)", 401), + ( + "view values: secret[list[int64]]", + "consume(declassify values)", + 401, + ), + ("view values: Numbers", "inspect(coarsen values)", 401), + ( + "view values: secret[list[int64]]", + "inspect(declassify values)", + 401, + ), + ] { + let source = format!( + "{prefix}function rejected({parameter}) returns nothing:\n {call}\n return nothing\n" + ); + for release in [false, true] { + let result = checked(&source, release, false); + let failures = errors(&result); + assert_eq!(failures.len(), 1, "{source}: {failures:?}"); + assert_eq!( + failures[0].code.code(), + expected_code, + "{source}: {failures:?}" + ); + } + } } #[test] - fn enum_with_explicit_discriminants_typechecks() { - let result = check_source_result( - "\ -enum IpProtocol: - icmp = 1 - tcp = 6 - udp = 17 - -bitfield network IpHeader: - protocol: 8 bits as IpProtocol - -function main() returns nothing: - IpHeader header = IpHeader(protocol: IpProtocol.tcp) - bytes raw = IpHeader.to_bytes(header) - IpHeader decoded = IpHeader.from_bytes(raw) handle error: - return nothing - IpProtocol protocol = decoded.protocol + fn caller_facts_transparent_view_syntax_stops_at_clones_and_copied_fields() { + let source = r#"namespace app +type Numbers = list[int64] where true +struct Holder: + values: list[int64] + numbers: Numbers + hidden: secret[list[int64]] +function consume(values: list[int64]) returns nothing: return nothing -", - ); - - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|d| d.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {:?}", errors); +function acquired(view numbers: Numbers, view hidden: secret[list[int64]], view holder: Holder) returns nothing: + consume(coarsen (clone (view numbers))) + consume(declassify (clone (view hidden))) + consume((view holder).values) + consume(coarsen ((view holder).numbers)) + consume(declassify ((view holder).hidden)) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 5); + for (index, (_, packet)) in packets.iter().enumerate() { + let [argument] = packet.arguments.as_slice() else { + panic!("one source argument"); + }; + assert_eq!(argument.syntax, CheckedCallerSyntax::Bare); + assert_eq!(argument.callee_access, CheckedCalleeAccess::Owned); + assert_eq!(argument.effect, CheckedCallerEffect::TransferOwned); + if index < 2 { + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + } else { + assert!(matches!( + argument.origin, + CheckedCallerOrigin::OwnedFieldCopy { .. } + )); + } + } + } } #[test] - fn duplicate_enum_discriminant_reports_error() { - let errors = check_source_errors( - "\ -enum Bad: - first = 1 - second = 1 + fn caller_facts_invalid_calls_preserve_dependency_ownership_diagnostics() { + let take = "function take(values: list[int64]) returns int64:\n return 1\n"; + let cases = [ + (format!("{OBSERVER}function test(values: list[int64]) returns list[int64]:\n int64 ignored = inspect[int64](values)\n return values\n"), vec![309], false), + (format!("{OBSERVER}{take}function test(values: list[int64]) returns list[int64]:\n int64 ignored = inspect[int64](take(values))\n return values\n"), vec![309, 400], false), + (format!("{OBSERVER}{take}function test(view values: list[int64]) returns int64:\n return inspect[int64](take(values))\n"), vec![309, 401], false), + (format!("{OBSERVER}{take}function test(values: list[int64]) returns int64:\n int64 consumed = take(values)\n return inspect[int64](values)\n"), vec![309, 400], false), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex[string](values)\n".to_string(), vec![309], true), + ("namespace app\nfunction test() returns string:\n return bytes.__to_hex[string](1)\n".to_string(), vec![304, 309], true), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex(values)\n".to_string(), vec![401], true), + ("namespace app\nfunction test(view values: bytes) returns string:\n return bytes.__to_hex(view values)\n".to_string(), vec![], true), + ]; + for (source, expected, stdlib) in cases { + for release in [false, true] { + let result = checked(&source, release, stdlib); + let mut codes = errors(&result) + .iter() + .map(|error| error.code.code()) + .collect::>(); + codes.sort_unstable(); + assert_eq!( + codes, expected, + "release={release}: {:?}", + result.diagnostics + ); + } + } + } -function main() returns nothing: - return nothing -", - ); + #[test] + fn caller_facts_qualified_descriptors_keep_source_order_and_written_views() { + let source = r#"namespace app +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64, marker: int64) returns int64: + return marker +function exercise() returns int64: + int64 direct = inspect(marker: 1, callback: app.identity) + int64 retained = inspect(view app.identity, 2) + return app.identity into view inspect(3) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + for (index, (_, packet)) in packets.iter().enumerate() { + let argument = packet + .arguments + .iter() + .find(|argument| argument.parameter_index == 0) + .expect("descriptor formal"); + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!( + argument.effect, + if index == 0 { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::RetainBorrow + } + ); + assert_eq!( + argument.syntax, + if index == 0 { + CheckedCallerSyntax::Bare + } else { + CheckedCallerSyntax::WrittenView + } + ); + } + assert_eq!( + packets[0] + .1 + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + } + } - assert!( - errors.iter().any(|d| d.code.code() == 339), - "expected E0339, got: {:?}", - errors - ); + #[test] + fn caller_facts_method_descriptor_producers_use_exact_concrete_frames() { + let source = r#"namespace models +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value +namespace app +function inspect(view callback: function(view models.Point) returns int64) returns nothing: + return nothing +function forward[T](seed: T) returns nothing: + use models as m + inspect(m.Point.amount) + return nothing +function exercise() returns nothing: + use models as m + inspect(view m.Point.amount) + forward[int64](1) + forward[string]("two") + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let root = result + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + matches!( + result.interner.resolve(argument.actual_type), + Type::Function { .. } + ) + }) + }) + .expect("root method descriptor call"); + assert_eq!( + root.arguments[0].origin, + CheckedCallerOrigin::OwnedExpression + ); + assert_eq!(root.arguments[0].effect, CheckedCallerEffect::RetainBorrow); + assert_eq!(result.generic_function_instantiations.len(), 2); + for body in &result.generic_function_instantiations { + assert_eq!(body.method_values.len(), 1); + assert_eq!(body.call_ownership.len(), 1); + let argument = &body.call_ownership.values().next().unwrap().arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert!(body.method_values.contains_key(&argument.source_span)); + assert!(!result.method_values.contains_key(&argument.source_span)); + } + } } #[test] - fn enum_discriminant_requires_unit_variant() { - let errors = check_source_errors( - "\ -enum Bad: - named(value: int64) = 1 - -function main() returns nothing: + fn caller_facts_function_value_fields_and_locals_remain_value_origins() { + let source = r#"namespace app +struct Holder: + callback: function(int64) returns int64 +function identity(value: int64) returns int64: + return value +function inspect(view callback: function(int64) returns int64) returns nothing: return nothing -", - ); - - assert!( - errors.iter().any(|d| d.code.code() == 338), - "expected E0338, got: {:?}", - errors - ); +function exercise(view holder: Holder) returns nothing: + function(int64) returns int64 stored = identity + inspect(view stored) + inspect(holder.callback) + inspect(view holder.callback) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + assert!(matches!( + packets[0].1.arguments[0].origin, + CheckedCallerOrigin::Binding(_) + )); + assert!(matches!( + packets[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(_) + } + )); + assert!(matches!( + packets[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(_) + } + )); + } } #[test] - fn stdlib_resource_is_nominal_and_reflected_as_resource() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\n", - FileId::new(STDLIB_FILE_ID_START), - ); - let errors: Vec<_> = result - .diagnostics - .iter() - .filter(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) - .collect(); - assert!(errors.is_empty(), "unexpected errors: {errors:#?}"); + fn caller_facts_generic_mutual_forwarding_joins_resolved_definition_not_body_span() { + let source = r#"namespace sample +mutual: + function leaf[T](view value: T) returns int64 + function forward[T](view value: T) returns int64 +function leaf[T](view value: T) returns int64: + return 7 +function forward[T](view value: T) returns int64: + return leaf[T](view value) +function exercise(values: list[int64]) returns int64: + int64 retained = forward[list[int64]](view values) + return forward[list[int64]](values) + retained +"#; + for release in [false, true] { + let file = FileId::new(0); + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|d| d.severity != jett_diagnostics::Severity::Error), + "{:?}", + resolved.diagnostics + ); + let result = check_with_options(&parsed.module, &resolved, CheckOptions { release }); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let forward = parsed + .module + .items + .iter() + .find_map(|item| match item { + Item::Function(function) if function.name.name == "forward" => Some(function), + _ => None, + }) + .expect("actual mutual function body"); + let definition = resolved.resolutions[&forward.name.span]; + assert_ne!(resolved.scope_table.def(definition).span, forward.name.span); + let mut root = result.call_ownership.iter().collect::>(); + root.sort_by_key(|(span, _)| span.start); + assert_eq!(root.len(), 2, "both selected source calls require packets"); + for (index, (_, packet)) in root.iter().enumerate() { + let CheckedInvocationTarget::Generic(target) = &packet.target else { + panic!("exact generic source target"); + }; + assert_eq!(target.definition, definition); + assert_eq!( + packet.arguments[0].syntax, + if index == 0 { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + } + ); + assert_eq!(packet.arguments[0].callee_access, CheckedCalleeAccess::View); + assert_eq!( + packet.arguments[0].effect, + if index == 0 { + CheckedCallerEffect::RetainBorrow + } else { + CheckedCallerEffect::RelinquishOwned + } + ); + assert_eq!( + result.source_type_map.get(&packet.arguments[0].source_span), + Some(&packet.arguments[0].actual_type) + ); + } + let body = result + .generic_function_instantiations + .iter() + .find(|body| body.definition == definition) + .expect("concrete forward body"); + assert_eq!(body.call_ownership.len(), 1); + let nested = body.call_ownership.values().next().unwrap(); + let argument = &nested.arguments[0]; + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!( + !result + .call_ownership + .contains_key(body.call_ownership.keys().next().unwrap()) + ); + } + } - let resource_type = result - .interner - .type_ids() - .find(|type_id| { - matches!( - result.interner.resolve(*type_id), - Type::Resource(name) if name == "io.FileHandle" - ) - }) - .expect("resource should have a distinct nominal semantic type"); - let info = result - .reflection_metadata - .get_type_info_for_id(resource_type) - .expect("resource reflection metadata should be present"); - assert_eq!(info.type_name, "io.FileHandle"); - assert_eq!(info.kind, "resource"); - assert!( - result - .reflection_metadata - .get_type_fields_for_id(resource_type) - .is_none(), - "resources must not expose aggregate fields" - ); + #[test] + fn caller_facts_mutual_generic_same_leaf_names_keep_exact_modes_and_targets() { + let source = r#"namespace left +mutual: + export function inspect[T](view value: T) returns int64 +export function inspect[T](view value: T) returns int64: + return 1 +namespace right +mutual: + export function inspect[T](value: T) returns int64 +export function inspect[T](value: T) returns int64: + return 2 +namespace app +function exercise(first: list[int64], second: list[int64]) returns int64: + use left + use right + int64 observed = left.inspect[list[int64]](view first) + return right.inspect[list[int64]](second) + observed +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut calls = result.call_ownership.iter().collect::>(); + calls.sort_by_key(|(span, _)| span.start); + assert_eq!(calls.len(), 2); + let CheckedInvocationTarget::Generic(left) = &calls[0].1.target else { + panic!("left"); + }; + let CheckedInvocationTarget::Generic(right) = &calls[1].1.target else { + panic!("right"); + }; + assert_ne!(left.definition, right.definition); + assert_eq!( + calls[0].1.arguments[0].callee_access, + CheckedCalleeAccess::View + ); + assert_eq!( + calls[0].1.arguments[0].effect, + CheckedCallerEffect::RetainBorrow + ); + assert_eq!( + calls[1].1.arguments[0].callee_access, + CheckedCalleeAccess::Owned + ); + assert_eq!( + calls[1].1.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); + } } #[test] - fn stdlib_resource_clone_is_rejected() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\nfunction duplicate(value: FileHandle) returns FileHandle:\n return clone value\n", - FileId::new(STDLIB_FILE_ID_START), - ); - assert!(result.diagnostics.iter().any(|diagnostic| { - diagnostic.code.code() == 364 - && diagnostic.severity == jett_diagnostics::Severity::Error - })); + fn caller_facts_machine_state_forwarded_views_preserve_immediate_bindings() { + let source = r#"namespace app +machine Packet: + states: + ready(values: list[int64]) +struct Envelope: + packet: Packet at ready +function inspect(view packet: Packet at ready) returns nothing: + return nothing +function exercise(view envelope: Envelope) returns nothing: + Packet at ready borrowed = view envelope.packet + Packet at ready forwarded = borrowed + inspect(view forwarded) + inspect(view borrowed) + inspect(view envelope.packet) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let fact = |name| { + result + .binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .expect("exact checked binding") + }; + let borrowed = fact("borrowed"); + let forwarded = fact("forwarded"); + assert_eq!( + forwarded.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(borrowed.definition), + } + ); + assert_eq!(forwarded.ty, borrowed.ty); + assert!(matches!( + result.interner.resolve(forwarded.ty), + Type::MachineState { .. } + )); + assert_eq!(result.call_ownership.len(), 3); + assert!( + result + .call_ownership + .values() + .all(|packet| packet.arguments[0].effect == CheckedCallerEffect::RetainBorrow) + ); + } } #[test] - fn stdlib_resource_nested_clone_is_rejected() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\nfunction duplicate(value: optional[FileHandle]) returns optional[FileHandle]:\n return clone value\n", - FileId::new(STDLIB_FILE_ID_START), - ); - assert!(result.diagnostics.iter().any(|diagnostic| { - diagnostic.code.code() == 364 - && diagnostic.severity == jett_diagnostics::Severity::Error - })); + fn caller_facts_machine_view_forwarding_stays_in_each_concrete_generic_body() { + let source = r#"namespace app +machine Packet: + states: + ready(values: list[int64]) +struct Envelope[T]: + packet: Packet at ready + extra: T +function inspect(view packet: Packet at ready) returns nothing: + return nothing +function forward[T](view envelope: Envelope[T]) returns nothing: + Packet at ready borrowed = view envelope.packet + Packet at ready forwarded = borrowed + inspect(view forwarded) + inspect(view envelope.packet) + return nothing +function exercise(first: Envelope[int64], second: Envelope[list[int64]]) returns nothing: + forward[int64](view first) + forward[list[int64]](view second) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.generic_function_instantiations.len(), 2); + for body in &result.generic_function_instantiations { + let fact = |name| { + body.binding_facts + .values() + .find(|fact| { + &source[fact.declaration_span.start as usize + ..fact.declaration_span.end as usize] + == name + }) + .expect("concrete body binding") + }; + let borrowed = fact("borrowed"); + let forwarded = fact("forwarded"); + assert_eq!( + forwarded.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(borrowed.definition), + } + ); + assert_eq!(body.call_ownership.len(), 2); + assert!( + body.call_ownership + .values() + .all(|packet| packet.arguments[0].effect + == CheckedCallerEffect::RetainBorrow) + ); + } + } } #[test] - fn stdlib_resource_comparison_is_rejected() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\nfunction same(view left: FileHandle, view right: FileHandle) returns bool:\n return left == right\n", - FileId::new(STDLIB_FILE_ID_START), - ); - assert!(result.diagnostics.iter().any(|diagnostic| { - diagnostic.code.code() == 365 - && diagnostic.severity == jett_diagnostics::Severity::Error - })); + fn caller_facts_machine_forwarding_does_not_relax_owned_erasure_or_bare_views() { + let prefix = "namespace app\nmachine Packet:\n states:\n ready(values: list[int64])\nfunction inspect(view packet: Packet at ready) returns nothing:\n return nothing\n"; + let cases = [ + ( + "function exercise(packet: Packet at ready) returns nothing:\n Packet erased = packet\n inspect(view packet)\n return nothing\n", + vec![400], + ), + ( + "function exercise(packet: Packet at ready) returns nothing:\n Packet erased = clone packet\n inspect(view packet)\n return nothing\n", + vec![], + ), + ( + "function exercise(view packet: Packet at ready) returns nothing:\n inspect(packet)\n return nothing\n", + vec![401], + ), + ( + "function exercise(view packet: Packet at ready) returns Packet at ready:\n Packet at ready forwarded = packet\n return forwarded\n", + vec![401], + ), + ]; + for (body, expected) in cases { + for release in [false, true] { + let result = checked(&format!("{prefix}{body}"), release, false); + let codes = errors(&result) + .iter() + .map(|error| error.code.code()) + .collect::>(); + assert_eq!(codes, expected, "{body}: {:?}", result.diagnostics); + } + } } #[test] - fn stdlib_resource_interface_implementation_is_rejected() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\ninterface Marker:\n function mark(view self: Marker) returns bool\nimplement Marker for FileHandle:\n function mark(view self: FileHandle) returns bool:\n return true\n", - FileId::new(STDLIB_FILE_ID_START), - ); - assert!(result.diagnostics.iter().any(|diagnostic| { - diagnostic.code.code() == 366 - && diagnostic.severity == jett_diagnostics::Severity::Error - })); + fn caller_facts_qualified_unit_enums_keep_named_order_and_pipeline_syntax() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +function inspect(view choice: models.Choice, marker: int64) returns int64: + return marker +function exercise() returns int64: + use models as m + int64 first = inspect(marker: 1, choice: m.Choice.empty) + int64 second = inspect(view m.Choice.empty, 2) + return m.Choice.empty into view inspect(3) +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 3); + for (index, (_, packet)) in packets.iter().enumerate() { + let argument = packet + .arguments + .iter() + .find(|argument| argument.parameter_index == 0) + .expect("unit enum actual"); + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.callee_access, CheckedCalleeAccess::View); + assert_eq!( + argument.effect, + if index == 0 { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::RetainBorrow + } + ); + assert_eq!( + argument.syntax, + if index == 0 { + CheckedCallerSyntax::Bare + } else { + CheckedCallerSyntax::WrittenView + } + ); + assert_eq!( + result.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + let Type::Enum(id) = result.interner.resolve(argument.actual_type) else { + panic!("exact nominal enum source type"); + }; + assert_eq!(result.interner.resolve_enum(*id).name, "models.Choice"); + } + assert_eq!( + packets[0] + .1 + .arguments + .iter() + .map(|argument| (argument.source_index, argument.parameter_index)) + .collect::>(), + [(0, 1), (1, 0)] + ); + } } #[test] - fn stdlib_resource_refinement_is_rejected() { - let result = check_source_result_with_file_id( - "namespace io\nexport resource FileHandle\ntype OpenHandle = FileHandle where true\n", - FileId::new(STDLIB_FILE_ID_START), - ); - assert!(result.diagnostics.iter().any(|diagnostic| { - diagnostic.code.code() == 367 - && diagnostic.severity == jett_diagnostics::Severity::Error - })); + fn caller_facts_qualified_unit_enums_use_concrete_and_reflected_source_maps() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct First: + count: int64 + text: string +struct Second: + count: int64 +function inspect(view choice: models.Choice) returns nothing: + return nothing +function forward[T](view seed: T) returns nothing: + use models + inspect(models.Choice.empty) + for field in type.fields[T](): + comptime type Field = field.type_info: + inspect(view models.Choice.empty) + return nothing +function exercise(first: First, second: Second) returns nothing: + forward[First](view first) + forward[Second](view second) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + assert_eq!(result.generic_function_instantiations.len(), 2); + let mut scoped_count = 0; + for body in &result.generic_function_instantiations { + let packet = body + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + &source[argument.source_span.start as usize + ..argument.source_span.end as usize] + == "models.Choice.empty" + }) + }) + .expect("outside reflected scope"); + let argument = &packet.arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.effect, CheckedCallerEffect::RelinquishOwned); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + for scoped in body.comptime_type_bindings.values().flatten() { + scoped_count += 1; + let packet = scoped + .body + .call_ownership + .values() + .find(|packet| { + packet.arguments.first().is_some_and(|argument| { + source[argument.source_span.start as usize + ..argument.source_span.end as usize] + .contains("models.Choice.empty") + }) + }) + .expect("reflected scope packet"); + let argument = &packet.arguments[0]; + assert_eq!(argument.origin, CheckedCallerOrigin::OwnedExpression); + assert_eq!(argument.syntax, CheckedCallerSyntax::WrittenView); + assert_eq!(argument.effect, CheckedCallerEffect::RetainBorrow); + assert_eq!( + scoped.body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert_eq!( + body.source_type_map.get(&argument.source_span), + Some(&argument.actual_type) + ); + assert!(!result.source_type_map.contains_key(&argument.source_span)); + } + } + assert_eq!( + scoped_count, 3, + "one exact body per declared reflected field" + ); + } } #[test] - fn generic_instantiation_manifest_is_ordered_deduplicated_and_nested() { - let result = check_source_result( - "function inner[T](value: T) returns T:\n return value\nfunction outer[T](value: T) returns T:\n return inner[T](value)\nfunction main() returns int64:\n int64 first = outer[int64](1)\n int64 second = outer[int64](2)\n return first + second\n", - ); - assert!( - result - .diagnostics - .iter() - .all(|diagnostic| diagnostic.severity != jett_diagnostics::Severity::Error), - "unexpected diagnostics: {:?}", - result.diagnostics - ); - assert_eq!(result.generic_function_instantiations.len(), 2); - let outer = &result.generic_function_instantiations[0]; - let inner = &result.generic_function_instantiations[1]; - assert_eq!(outer.concrete_args, vec![TypeInterner::INT64]); - assert_eq!(inner.concrete_args, vec![TypeInterner::INT64]); - assert_eq!(outer.parameter_types, vec![TypeInterner::INT64]); - assert_eq!(outer.return_type, TypeInterner::INT64); - assert!(!outer.type_map.is_empty()); - assert_eq!(outer.generic_calls.len(), 1); - let nested_call = outer.generic_calls.values().next().unwrap(); - assert_eq!(nested_call.definition, inner.definition); - assert_eq!(nested_call.concrete_args, vec![TypeInterner::INT64]); - assert_eq!(result.generic_calls.len(), 2); + fn caller_facts_enum_valued_bindings_and_real_fields_remain_value_origins() { + let source = r#"namespace models +export enum Choice: + empty + full(value: int64) +namespace app +struct Holder: + choice: models.Choice +function inspect(view choice: models.Choice, marker: int64) returns nothing: + return nothing +function exercise(view holder: Holder, view stored: models.Choice) returns nothing: + use models + inspect(view stored, 1) + inspect(holder.choice, 2) + inspect(view holder.choice, 3) + inspect(Holder(choice: models.Choice.empty).choice, 4) + return nothing +"#; + for release in [false, true] { + let result = checked(source, release, false); + assert!(errors(&result).is_empty(), "{:?}", result.diagnostics); + let mut packets = result.call_ownership.iter().collect::>(); + packets.sort_by_key(|(span, _)| span.start); + assert_eq!(packets.len(), 4); + assert!(matches!( + packets[0].1.arguments[0].origin, + CheckedCallerOrigin::Binding(_) + )); + assert!(matches!( + packets[1].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Binding(_) + } + )); + assert!(matches!( + packets[2].1.arguments[0].origin, + CheckedCallerOrigin::BorrowedProjection { + source: CheckedViewSource::Binding(_) + } + )); + assert_eq!( + packets[3].1.arguments[0].origin, + CheckedCallerOrigin::OwnedFieldCopy { + parent: CheckedViewSource::Other, + } + ); + assert!( + packets + .iter() + .all(|(_, packet)| packet.arguments[0].actual_type + == packets[0].1.arguments[0].actual_type) + ); + } } } diff --git a/crates/jett_typecheck/src/checker/actor_messages.rs b/crates/jett_typecheck/src/checker/actor_messages.rs new file mode 100644 index 00000000..66d1b395 --- /dev/null +++ b/crates/jett_typecheck/src/checker/actor_messages.rs @@ -0,0 +1,160 @@ +use super::*; +use crate::actor_messages::{ + CheckedActorHandlerTarget, CheckedActorMessageCall, CheckedActorMessageKind, +}; +use jett_types::ActorId; + +#[cfg(test)] +#[path = "actor_messages/tests.rs"] +mod tests; + +impl TypeChecker<'_> { + pub(super) fn record_actor_message_call( + &mut self, + outer_span: Span, + kind: CheckedActorMessageKind, + inner: &Expr, + receiver: &Expr, + receiver_type: TypeId, + actor: ActorId, + handler_index: usize, + response_type: TypeId, + args: &[ast::CallArg], + ) { + let Some(&actor_definition) = self.actor_definitions.get(&actor) else { + return; + }; + if self.resolve.scope_table.def(actor_definition).kind != DefKind::Actor + || self.type_env.get(&actor_definition) != Some(&receiver_type) + || self.interner.resolve(receiver_type) != &Type::Actor(actor) + { + return; + } + let target = CheckedActorHandlerTarget { + actor_definition, + actor, + handler_index, + }; + let Some(message) = self + .interner + .resolve_actor(actor) + .messages + .get(handler_index) + .cloned() + else { + return; + }; + let Some(modes) = self.actor_message_modes.get(&target).cloned() else { + return; + }; + let (original_receiver, original_handler, original_args) = match inner { + Expr::Call(callee, arguments, _) => match callee.as_ref() { + Expr::FieldAccess(base, handler, _) => { + (base.as_ref(), handler, arguments.as_slice()) + } + _ => return, + }, + Expr::FieldAccess(base, handler, _) => (base.as_ref(), handler, &[][..]), + _ => return, + }; + if !std::ptr::eq(original_receiver, receiver) + || original_handler.name != message.name + || response_type != message.responds + || args.len() != original_args.len() + || args + .iter() + .zip(original_args) + .any(|(argument, original)| !std::ptr::eq(argument, original)) + || message.params.len() != args.len() + || modes.len() != message.params.len() + { + return; + } + let order = match self.active_generic_instantiations.last() { + Some(active) => active.call_argument_orders.get(&inner.span()), + None => self.call_argument_orders.get(&inner.span()), + }; + let Some(order) = order.cloned() else { + return; + }; + if order.source_indices.len() != args.len() { + return; + } + let mut inverse = vec![None; args.len()]; + for (parameter_index, &source_index) in order.source_indices.iter().enumerate() { + let Some(slot) = inverse.get_mut(source_index) else { + return; + }; + if slot.replace(parameter_index).is_some() { + return; + } + } + let mut arguments = Vec::with_capacity(args.len()); + for (source_index, argument) in args.iter().enumerate() { + let (Some(parameter_index), Some(actual_type)) = ( + inverse[source_index], + self.checked_expression_type(argument.value.span()), + ) else { + return; + }; + let parameter_type = message.params[parameter_index].1; + if actual_type == TypeInterner::ERROR || parameter_type == TypeInterner::ERROR { + return; + } + let written_view = Self::is_explicit_view(&argument.value); + let copyable = crate::ownership::is_implicitly_copyable(&self.interner, actual_type); + // A message crosses an actor boundary even when the receiver gives + // its private incoming owner read-only access inside the handler. + // Test observation does not turn a borrowed caller into that owner. + if written_view || (!copyable && self.initializer_is_view(&argument.value)) { + self.sink.emit(Diagnostic::error( + 401, + "cannot send a view to an actor; transfer ownership or clone the value", + argument.value.span(), + )); + } + let callee_access = if modes[parameter_index] { + CheckedCalleeAccess::View + } else { + CheckedCalleeAccess::Owned + }; + arguments.push(CheckedArgumentOwnership { + source_span: argument.value.span(), + source_index, + parameter_index, + actual_type, + parameter_type, + syntax: if written_view { + CheckedCallerSyntax::WrittenView + } else { + CheckedCallerSyntax::Bare + }, + origin: self.caller_origin(Some(&argument.value), written_view), + callee_access, + effect: if copyable { + CheckedCallerEffect::Copy + } else if callee_access == CheckedCalleeAccess::View { + CheckedCallerEffect::RelinquishOwned + } else { + CheckedCallerEffect::TransferOwned + }, + }); + } + let packet = CheckedActorMessageCall::new( + self.actor_check_session.clone(), + target, + kind, + self.ownership_context, + outer_span, + receiver.span(), + receiver_type, + response_type, + arguments, + ); + if let Some(active) = self.active_generic_instantiations.last_mut() { + active.actor_message_calls.insert(outer_span, packet); + } else { + self.actor_message_calls.insert(outer_span, packet); + } + } +} diff --git a/crates/jett_typecheck/src/checker/actor_messages/tests.rs b/crates/jett_typecheck/src/checker/actor_messages/tests.rs new file mode 100644 index 00000000..0cf7067e --- /dev/null +++ b/crates/jett_typecheck/src/checker/actor_messages/tests.rs @@ -0,0 +1,336 @@ +use super::*; +use jett_diagnostics::Severity; + +fn checked(source: &str, release: bool) -> (CheckResult, ResolveResult) { + let parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + resolved.diagnostics, + ); + let result = check_with_options(&parsed.module, &resolved, CheckOptions { release }); + (result, resolved) +} + +fn assert_accepted(result: &CheckResult) { + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + result.diagnostics, + ); +} + +#[test] +fn actor_message_arguments_keep_lexical_moves_and_named_parameter_join() { + let source = r#"namespace app +actor Store: + receive put(first: list[int64], second: list[int64]): + trace first + trace second +function main() returns nothing: + Store worker = spawn Store() + list[int64] one = list(1) + list[int64] two = list(2) + send worker.put(second: two, first: one) +"#; + for release in [false, true] { + let (result, resolved) = checked(source, release); + assert_accepted(&result); + let packets = result.actor_message_calls.values().collect::>(); + let [packet] = packets.as_slice() else { + panic!("one complete actor message"); + }; + assert_eq!(packet.kind, CheckedActorMessageKind::Send); + assert_eq!(packet.target.handler_index, 0); + assert_eq!( + resolved + .scope_table + .def(packet.target.actor_definition) + .kind, + DefKind::Actor + ); + assert_eq!( + resolved + .scope_table + .def(packet.target.actor_definition) + .name, + "app.Store" + ); + assert_eq!(packet.arguments.len(), 2); + for (source_index, parameter_index, name) in [(0, 1, "two"), (1, 0, "one")] { + let argument = &packet.arguments[source_index]; + assert_eq!(argument.source_index, source_index); + assert_eq!(argument.parameter_index, parameter_index); + assert_eq!(argument.effect, CheckedCallerEffect::TransferOwned); + let CheckedCallerOrigin::Binding(binding) = &argument.origin else { + panic!("an exact caller binding"); + }; + assert_eq!(resolved.scope_table.def(binding.definition).name, name); + } + assert!( + result.call_ownership.is_empty(), + "a message is not a function call" + ); + } +} + +#[test] +fn identically_shaped_actors_keep_distinct_nominal_message_targets() { + let source = r#"namespace app +actor First: + receive get responds int64: + respond 1 +actor Second: + receive get responds int64: + respond 2 +function main() returns int64: + First first = spawn First() + Second second = spawn Second() + int64 left = ask first.get + int64 right = ask second.get + return left + right +"#; + let (result, _) = checked(source, false); + assert_accepted(&result); + let packets = result.actor_message_calls.values().collect::>(); + assert_eq!(packets.len(), 2); + assert_ne!( + packets[0].target.actor_definition, + packets[1].target.actor_definition + ); + assert_ne!(packets[0].target.actor, packets[1].target.actor); + assert_eq!( + packets[0].target.handler_index, + packets[1].target.handler_index + ); + for packet in packets { + assert_eq!(packet.kind, CheckedActorMessageKind::Ask); + assert_eq!(packet.response_type, TypeInterner::INT64); + assert!(packet.arguments.is_empty()); + } +} + +#[test] +fn actor_view_formal_receives_a_transferred_owner_without_a_borrowed_sender() { + let source = r#"namespace app +actor Sink: + receive inspect(view values: list[int64]): + trace values +function main() returns nothing: + Sink sink = spawn Sink() + list[int64] values = list(1) + send sink.inspect(values) +"#; + let (result, _) = checked(source, false); + assert_accepted(&result); + let packet = result.actor_message_calls.values().next().unwrap(); + assert_eq!(packet.arguments[0].callee_access, CheckedCalleeAccess::View); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::RelinquishOwned + ); + assert_eq!(packet.arguments[0].syntax, CheckedCallerSyntax::Bare); +} + +#[test] +fn actor_message_facts_remain_in_the_concrete_generic_body() { + let source = r#"namespace app +actor Store: + receive put(value: list[int64]): + trace value +function relay[T](worker: Store, value: T) returns nothing: + send worker.put(value) +function main() returns nothing: + Store worker = spawn Store() + relay(worker, list(1)) +"#; + let (result, _) = checked(source, false); + assert_accepted(&result); + assert!(result.actor_message_calls.is_empty()); + assert_eq!(result.generic_function_instantiations.len(), 1); + let body = &result.generic_function_instantiations[0]; + assert_eq!(body.actor_message_calls.len(), 1); + let packet = body.actor_message_calls.values().next().unwrap(); + assert_eq!( + packet.arguments[0].actual_type, + packet.arguments[0].parameter_type + ); + assert_eq!( + packet.arguments[0].effect, + CheckedCallerEffect::TransferOwned + ); +} + +#[test] +fn actor_boundary_rejects_borrowed_message_data_in_ordinary_and_observed_bodies() { + let header = + "namespace app\nactor Store:\n receive put(value: list[int64]):\n trace value\n"; + for body in [ + "function invoke(view values: list[int64]) returns nothing:\n Store worker = spawn Store()\n send worker.put(values)\n", + "verify message:\n list[int64] values = list(1)\n list[int64] alias = view values\n Store worker = spawn Store()\n send worker.put(alias)\n", + ] { + let (result, _) = checked(&format!("{header}{body}"), false); + assert!( + result.diagnostics.iter().any(|diagnostic| { + diagnostic.severity == Severity::Error + && diagnostic.code.code() == 401 + && diagnostic + .message + .contains("cannot send a view to an actor") + }), + "{:?}", + result.diagnostics + ); + assert!( + result + .actor_handlers + .values() + .all(|handler| !handler.ownership_validated()) + ); + } +} + +#[test] +fn editing_private_handler_body_or_observed_exits_retires_the_ownership_receipt() { + let source = "namespace app\nactor Store:\n receive ping:\n int64 number = 1\n trace number\n"; + let (result, _) = checked(source, false); + assert_accepted(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(handler.ownership_validated()); + let mut changed_body = handler.clone(); + changed_body.body_mut().actor_message_calls.clear(); + assert!(!changed_body.ownership_validated()); + let mut changed_exit = handler.clone(); + changed_exit.record_exits(Vec::new()); + assert!(!changed_exit.ownership_validated()); +} + +#[test] +fn foreign_same_shaped_handler_and_message_facts_cannot_replace_the_current_session() { + let source = "namespace app\nactor Store:\n receive put(value: list[int64]):\n trace value\nfunction main() returns nothing:\n Store worker = spawn Store()\n send worker.put(list(1))\n"; + let (first, _) = checked(source, false); + let (mut second, _) = checked(source, false); + assert_accepted(&first); + assert_accepted(&second); + let target = *first.actor_handlers.keys().next().unwrap(); + assert!(first.actor_handler(target).is_some()); + assert!(second.actor_handler(target).is_some()); + second + .actor_handlers + .insert(target, first.actor_handlers[&target].clone()); + assert!(second.actor_handler(target).is_none()); + let (span, packet) = first.actor_message_calls.iter().next().unwrap(); + assert!(first.validates_actor_message_at( + packet, + crate::CheckedActorMessageRoot::Ordinary, + &[], + *span + )); + second.actor_message_calls.insert(*span, packet.clone()); + assert!(!second.validates_actor_message_at( + packet, + crate::CheckedActorMessageRoot::Ordinary, + &[], + *span + )); + let mut changed = packet.clone(); + changed.arguments[0].effect = CheckedCallerEffect::Copy; + assert!(!first.validates_actor_message(&changed)); + assert!(!first.validates_actor_message_at( + packet, + crate::CheckedActorMessageRoot::Ordinary, + &[], + packet.receiver_span + )); +} + +#[test] +fn same_span_generic_message_transplants_do_not_gain_the_other_body_context() { + let source = r#"namespace app +actor Store: + receive put(value: list[int64]): + trace value +function relay[T](worker: Store, tag: T, payload: list[int64]) returns nothing: + trace tag + send worker.put(payload) +function main() returns nothing: + Store worker = spawn Store() + relay(clone worker, 1, list(1)) + relay(worker, "second", list(2)) +"#; + let (mut result, _) = checked(source, false); + assert_accepted(&result); + assert_eq!(result.generic_function_instantiations.len(), 2); + let (span, packet) = result.generic_function_instantiations[0] + .actor_message_calls + .iter() + .next() + .map(|(span, call)| (*span, call.clone())) + .unwrap(); + let other = result.generic_function_instantiations[1].actor_message_calls[&span].clone(); + assert_eq!(packet.target, other.target); + assert_eq!(packet.arguments, other.arguments); + assert!(result.validates_actor_message_at( + &packet, + crate::CheckedActorMessageRoot::Generic(0), + &[], + span + )); + assert!(result.validates_actor_message_at( + &other, + crate::CheckedActorMessageRoot::Generic(1), + &[], + span + )); + result.generic_function_instantiations[1] + .actor_message_calls + .insert(span, packet.clone()); + assert!(!result.validates_actor_message_at( + &packet, + crate::CheckedActorMessageRoot::Generic(1), + &[], + span + )); + assert!(!result.validates_actor_message_at( + &packet, + crate::CheckedActorMessageRoot::Generic(0), + &[(span, 0)], + span + )); +} + +#[test] +fn actor_receipt_rejects_changed_original_body_with_all_source_spans_retained() { + let source = "namespace app\nactor Store:\n receive ping:\n int64 number = 1\n trace number\n"; + let (result, _) = checked(source, false); + assert_accepted(&result); + let handler = result.actor_handlers.values().next().unwrap(); + let mut parsed = jett_parser::parse(source, FileId::new(0)); + assert!(parsed.errors.is_empty()); + let actor = parsed + .module + .items + .iter_mut() + .find_map(|item| match item { + Item::Actor(actor) => Some(actor), + _ => None, + }) + .unwrap(); + assert!(handler.matches_source_actor(actor)); + let original_span = actor.handlers[0].body.span; + actor.handlers[0].body.stmts.reverse(); + assert_eq!(actor.handlers[0].body.span, original_span); + assert!( + handler.ownership_validated(), + "the original receipt remains intact" + ); + assert!(!handler.matches_source_actor(actor)); +} diff --git a/crates/jett_typecheck/src/checker/assignment_place_tests.rs b/crates/jett_typecheck/src/checker/assignment_place_tests.rs new file mode 100644 index 00000000..9dca5792 --- /dev/null +++ b/crates/jett_typecheck/src/checker/assignment_place_tests.rs @@ -0,0 +1,295 @@ +//! Checked assignment-place and ownership controls. + +use super::{CheckOptions, CheckResult, check_with_options}; +use crate::{ + CheckedAssignmentPlaceError, CheckedFieldOwner, CheckedFieldUpdateResourceRequirement, + validate_assignment_place_shape, +}; +use jett_common::{FileId, STDLIB_FILE_ID_START}; +use jett_diagnostics::Severity; +use jett_types::Type; + +const DECLARATIONS: &str = "namespace app\nstruct Inner:\n value: int64\nstruct Record:\n inner: Inner\n label: string\nenum Choice:\n wrapped(record: Record)\nfunction make() returns Record:\n return Record(inner: Inner(value: 1), label: \"before\")\n"; + +mod actor_handler_tests; +mod metadata_tests; +mod ownership_tests; + +fn checked(source: &str, file: FileId) -> CheckResult { + let parsed = jett_parser::parse(source, file); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + resolved.diagnostics + ); + check_with_options(&parsed.module, &resolved, CheckOptions { release: false }) +} + +fn ordinary(source: &str) -> CheckResult { + checked(source, FileId::new(0)) +} + +fn assert_no_errors(result: &CheckResult) { + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + result.diagnostics + ); +} + +#[test] +fn original_nested_integer_and_string_bodies_export_closed_places() { + // Same two bodies as projected_assignment.rs, with separate metadata oracles. + let source = format!( + "{DECLARATIONS}function local() returns nothing:\n mutable Record record = make()\n (record).inner.value = 9\nfunction parameter(mutable record: Record) returns nothing:\n record.label = \"after\"\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 2); + let mut depths = result + .assignment_places + .values() + .map(|place| { + validate_assignment_place_shape(place, &result.interner).unwrap(); + assert_eq!(place.root.ty, place.root_type); + assert_eq!( + place.resource, + CheckedFieldUpdateResourceRequirement::Ordinary + ); + place.projections.len() + }) + .collect::>(); + depths.sort_unstable(); + assert_eq!(depths, [1, 2]); +} + +#[test] +fn concrete_generic_assignments_do_not_share_a_span_only_map() { + let result = ordinary( + "namespace app\nstruct Slot[T]:\n item: T\nfunction replace[T](mutable slot: Slot[T], next: T) returns nothing:\n slot.item = next\nfunction callers() returns nothing:\n replace[int64](Slot[int64](item: 1), 2)\n replace[string](Slot[string](item: \"before\"), \"after\")\n", + ); + assert_no_errors(&result); + let mut places = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.assignment_places.values()) + .collect::>(); + places.sort_by_key(|place| place.root.ty.index()); + assert_eq!(places.len(), 2); + assert_eq!(places[0].assignment_span, places[1].assignment_span); + assert_ne!(places[0].root.ty, places[1].root.ty); + assert_ne!( + places[0].projections[0].field_type, + places[1].projections[0].field_type + ); + for place in places { + validate_assignment_place_shape(place, &result.interner).unwrap(); + } +} + +#[test] +fn scoped_assignment_keeps_an_outer_root_in_its_binding_facts() { + let source = format!( + "{DECLARATIONS}function update(mutable record: Record) returns nothing:\n comptime type Bound = type.info[int64]():\n record.inner.value = 9\n" + ); + let result = checked(&source, FileId::new(STDLIB_FILE_ID_START)); + assert_no_errors(&result); + let scopes = result + .comptime_type_bindings + .values() + .flatten() + .collect::>(); + assert_eq!(scopes.len(), 1); + let body = &scopes[0].body; + assert_eq!(body.assignment_places.len(), 1); + let place = body.assignment_places.values().next().unwrap(); + assert_eq!( + body.binding_facts.get(&place.root.declaration_span), + Some(&place.root) + ); + validate_assignment_place_shape(place, &result.interner).unwrap(); +} + +#[test] +fn narrowed_machine_keeps_declared_storage_and_current_state_separate() { + let result = ordinary( + "namespace app\nmachine Session:\n states:\n guest\n logged_in(user_id: string)\n transitions:\n guest to logged_in\nfunction rename(mutable session: Session) returns nothing:\n if session at logged_in:\n session.user_id = \"after\"\n", + ); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + let place = result.assignment_places.values().next().unwrap(); + assert!(matches!( + result.interner.resolve(place.root.ty), + Type::Machine(_) + )); + let Type::MachineState { machine, state } = result.interner.resolve(place.root_type) else { + panic!("missing exact narrowed state"); + }; + assert_eq!( + place.projections[0].owner, + CheckedFieldOwner::MachineState { + machine: *machine, + state: *state + } + ); + validate_assignment_place_shape(place, &result.interner).unwrap(); +} + +#[test] +fn secret_owner_retains_stored_type_and_qualified_access_type() { + let source = format!( + "{DECLARATIONS}function update(mutable record: secret[Record]) returns nothing:\n record.inner.value = 9\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap(); + assert_eq!(place.projections.len(), 2); + for projection in &place.projections { + assert!(matches!( + result.interner.resolve(projection.owner_type), + Type::Secret(_) + )); + assert!( + matches!(result.interner.resolve(projection.field_type), Type::Secret(inner) + if *inner == projection.declared_field_type) + ); + } + validate_assignment_place_shape(place, &result.interner).unwrap(); +} + +#[test] +fn scalar_field_inside_resource_owner_still_requires_custody() { + let result = checked( + "namespace app\nresource Handle\nstruct Holder:\n token: Handle\n count: int64\nfunction change(mutable holder: Holder) returns nothing:\n holder.count = 9\n", + FileId::new(STDLIB_FILE_ID_START), + ); + assert_no_errors(&result); + assert!(result.resource_hooks.is_empty()); + let place = result.assignment_places.values().next().unwrap(); + assert_eq!( + place.resource, + CheckedFieldUpdateResourceRequirement::RequiresCustody + ); + validate_assignment_place_shape(place, &result.interner).unwrap(); + let mut corrupted = place.clone(); + corrupted.resource = CheckedFieldUpdateResourceRequirement::Ordinary; + assert_eq!( + validate_assignment_place_shape(&corrupted, &result.interner), + Err(CheckedAssignmentPlaceError::ResourceClassification) + ); +} + +#[test] +fn exact_layout_rejects_changed_ordinals_types_and_root_source() { + let source = format!( + "{DECLARATIONS}function update(mutable record: Record) returns nothing:\n record.inner.value = 9\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap(); + let mut ordinal = place.clone(); + ordinal.projections[1].field_index = usize::MAX; + assert_eq!( + validate_assignment_place_shape(&ordinal, &result.interner), + Err(CheckedAssignmentPlaceError::FieldOrdinal) + ); + let mut field = place.clone(); + field.projections[1].declared_field_type = jett_types::TypeInterner::STRING; + assert_eq!( + validate_assignment_place_shape(&field, &result.interner), + Err(CheckedAssignmentPlaceError::FieldType) + ); + let mut source = place.clone(); + source.root_use_span = source.root.declaration_span; + assert_eq!( + validate_assignment_place_shape(&source, &result.interner), + Err(CheckedAssignmentPlaceError::SourceAssociation) + ); +} + +#[test] +fn shape_export_does_not_override_use_after_move_diagnostics() { + let source = format!( + "{DECLARATIONS}function consume(record: Record) returns int64:\n return record.inner.value\nfunction invalid() returns nothing:\n mutable Record record = make()\n int64 ignored = consume(record)\n record.inner.value = 9\n" + ); + let result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 400), + "{:?}", + result.diagnostics + ); + // A shape fact can exist in a failed CheckResult. Consumers must retain + // whole-program success and independent live-root validation prerequisites. + assert_eq!(result.assignment_places.len(), 1); +} + +#[test] +fn enclosing_refinements_bitfields_and_view_roots_export_no_place() { + for body in [ + "function invalid(mutable record: Wrapped) returns nothing:\n (coarsen record).inner.value = 9\n", + "function invalid(view mutable record: Record) returns nothing:\n record.inner.value = 9\n", + "bitfield Bits:\n count: 4 bits\nfunction invalid(mutable flags: Bits) returns nothing:\n flags.count = 3\n", + ] { + let source = format!("{DECLARATIONS}type Wrapped = Record where true\n{body}"); + let result = ordinary(&source); + assert!(result.assignment_places.is_empty()); + } +} + +#[test] +fn refined_and_bitfield_owners_receive_static_policy_diagnostics() { + for body in [ + "function invalid(mutable record: Wrapped) returns nothing:\n record.inner.value = 9\n", + "struct RefinedChild:\n inner: Wrapped\nfunction invalid(mutable record: RefinedChild) returns nothing:\n record.inner.inner.value = 9\n", + "bitfield Bits:\n count: 4 bits\nfunction invalid(mutable flags: Bits) returns nothing:\n flags.count = 3\n", + ] { + let source = format!("{DECLARATIONS}type Wrapped = Record where true\n{body}"); + let result = ordinary(&source); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{:?}", result.diagnostics); + assert_eq!(errors[0].code.code(), 404); + assert!(result.assignment_places.is_empty()); + } +} + +#[test] +fn complete_refined_and_bitfield_terminal_fields_remain_replaceable() { + let source = "namespace app\ntype Positive = int64 where value > 0\nbitfield Bits:\n count: 4 bits\nstruct Envelope:\n count: Positive\n flags: Bits\nfunction replace(mutable envelope: Envelope, count: Positive, flags: Bits) returns nothing:\n envelope.count = count\n envelope.flags = flags\n"; + let result = ordinary(source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 2); + for place in result.assignment_places.values() { + assert_eq!(place.projections.len(), 1); + validate_assignment_place_shape(place, &result.interner).unwrap(); + } +} + +#[test] +fn whole_root_reassignment_clears_prior_nonzero_fact_after_rhs() { + let result = ordinary( + "namespace app\nfunction invalid(mutable divisor: int64) returns int64:\n if divisor != 0:\n divisor = 0\n return 8 / divisor\n return 0\n", + ); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "{:?}", + result.diagnostics + ); +} diff --git a/crates/jett_typecheck/src/checker/assignment_place_tests/actor_handler_tests.rs b/crates/jett_typecheck/src/checker/assignment_place_tests/actor_handler_tests.rs new file mode 100644 index 00000000..0cd57281 --- /dev/null +++ b/crates/jett_typecheck/src/checker/assignment_place_tests/actor_handler_tests.rs @@ -0,0 +1,369 @@ +//! Actor entry identity, real exits, persistent loans and loop ownership. +use super::{assert_no_errors, ordinary}; +use crate::{ + CheckedActorBindingRole, CheckedActorHandlerExitKind, CheckedBindingMode, CheckedViewSource, +}; +use jett_diagnostics::Severity; + +const TYPES: &str = "namespace app\nstruct Inner:\n value: int64\n"; + +fn has_error(result: &super::CheckResult, code: u16) -> bool { + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error && diagnostic.code.code() == code) +} + +#[test] +fn actor_owned_field_paths_have_private_handler_context_and_exact_roles() { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive replace(value: int64):\n inner.value = value\n receive read responds int64:\n respond inner.value\n" + )); + assert_no_errors(&result); + assert_eq!(result.actor_handlers.len(), 2); + for handler in result.actor_handlers.values() { + assert!(handler.ownership_validated()); + assert_eq!( + handler + .bindings() + .iter() + .filter(|binding| matches!(binding.role, CheckedActorBindingRole::State { .. })) + .count(), + 1 + ); + assert!(!handler.exits().is_empty()); + assert!( + handler + .body() + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + } + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} + +#[test] +fn actor_message_cannot_shadow_an_existing_state_binding() { + let source = format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive replace(mutable inner: Inner):\n inner.value = 9\n" + ); + let parsed = jett_parser::parse(&source, jett_common::FileId::new(0)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error + && diagnostic.code.code() == 201), + "{:?}", + resolved.diagnostics + ); +} + +#[test] +fn actor_move_then_restore_is_allowed_but_unrestored_actual_exits_reject() { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive replace responds Inner:\n Inner old = inner\n inner = Inner(value: 9)\n respond old\n" + )); + assert_no_errors(&result); + assert!( + result + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); + for body in [ + " respond inner\n", + " Inner old = inner\n", + " Inner old = inner\n if true:\n respond 1\n inner = Inner(value: 9)\n respond 2\n", + ] { + let response = if body == " respond inner\n" { + " responds Inner" + } else if body.contains("respond") { + " responds int64" + } else { + "" + }; + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive invalid{response}:\n{body}" + )); + assert!(has_error(&result, 400), "{:?}", result.diagnostics); + assert!( + result + .actor_handlers + .values() + .all(|handler| !handler.ownership_validated()) + ); + } +} + +#[test] +fn actor_early_respond_abandons_rhs_commit_and_validates_captures() { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update responds int64:\n optional[int64] absent = none\n inner.value = absent handle:\n respond inner.value\n respond inner.value\n" + )); + assert_no_errors(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(handler.ownership_validated()); + assert_eq!( + handler + .exits() + .iter() + .filter(|exit| exit.kind == CheckedActorHandlerExitKind::Respond) + .count(), + 2 + ); + assert!( + handler + .body() + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + let invalid = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update responds Inner:\n optional[int64] absent = none\n inner.value = absent handle:\n respond inner\n respond Inner(value: 9)\n" + )); + assert!(has_error(&invalid, 400), "{:?}", invalid.diagnostics); +} + +#[test] +fn actor_break_and_continue_keep_real_state_and_ignore_unreachable_restores() { + for jump in ["break", "continue"] { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n for item in list(1, 2):\n Inner old = inner\n {jump}\n inner = Inner(value: 9)\n" + )); + assert!(has_error(&result, 400), "{jump}: {:?}", result.diagnostics); + assert!( + result + .actor_handlers + .values() + .all(|handler| !handler.ownership_validated()) + ); + } + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n for item in list(1, 2):\n Inner old = inner\n inner = Inner(value: item)\n continue\n" + )); + assert_no_errors(&result); + assert!( + result + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); + let restored_after_break = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n for item in list(1, 2):\n Inner old = inner\n break\n inner = Inner(value: 9)\n" + )); + assert_no_errors(&restored_after_break); + assert!( + restored_after_break + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); +} + +#[test] +fn actor_loop_backedge_rechecks_availability_before_repeated_reads() { + for statement in [ + " for item in list(1, 2):\n Inner old = inner\n inner = Inner(value: 9)\n", + " while true:\n Inner old = inner\n inner = Inner(value: 9)\n", + ] { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n{statement}" + )); + assert!(has_error(&result, 400), "{:?}", result.diagnostics); + } +} + +#[test] +fn actor_persistent_view_preserves_prior_state_source_and_owner_loan() { + let prefix = format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n Inner alias = view inner\n" + ); + let result = ordinary(&format!( + "{prefix} receive read responds int64:\n respond alias.value\n" + )); + assert_no_errors(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(handler.ownership_validated()); + let owner = handler + .bindings() + .iter() + .find(|binding| binding.role == CheckedActorBindingRole::State { field: 0 }) + .unwrap(); + let alias = handler + .bindings() + .iter() + .find(|binding| binding.role == CheckedActorBindingRole::State { field: 1 }) + .unwrap(); + assert_eq!( + alias.fact.mode, + CheckedBindingMode::View { + source: CheckedViewSource::Binding(owner.fact.definition), + } + ); + for statement in [ + "inner.value = 9", + "inner = Inner(value: 9)", + "Inner old = inner", + ] { + let result = ordinary(&format!( + "{prefix} receive invalid:\n {statement}\n" + )); + assert!( + has_error(&result, 401), + "{statement}: {:?}", + result.diagnostics + ); + } +} + +#[test] +fn actor_response_views_reject_including_written_copy_views() { + for (response, value) in [("Inner", "view inner"), ("int64", "view inner.value")] { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive invalid responds {response}:\n respond {value}\n" + )); + assert!(has_error(&result, 401), "{:?}", result.diagnostics); + } + let owned_copy = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive read responds int64:\n respond inner.value\n" + )); + assert_no_errors(&owned_copy); + assert!( + owned_copy + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); +} + +#[test] +fn actor_return_and_missing_reply_stay_unsealed_without_new_source_ban() { + for body in [ + " return 1\n", + " int64 observed = inner.value\n", + ] { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive future responds int64:\n{body}" + )); + assert_no_errors(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(!handler.ownership_validated()); + assert_eq!(handler.exits().len(), 1); + } +} + +#[test] +fn actor_transparent_copy_replies_observe_borrowed_and_persistent_carriers() { + for (declaration, carrier, expression) in [ + ("", "secret[int64]", "declassify hidden"), + ("type Count = int64 where true\n", "Count", "coarsen hidden"), + ] { + let borrowed = ordinary(&format!( + "{TYPES}{declaration}actor Store:\n receive read(view hidden: {carrier}) responds int64:\n respond {expression}\n" + )); + assert_no_errors(&borrowed); + assert!( + borrowed + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); + if !declaration.is_empty() { + continue; + } + let persistent = ordinary(&format!( + "{TYPES}{declaration}actor Store:\n {carrier} hidden = 1\n receive read responds int64:\n respond {expression}\n" + )); + assert_no_errors(&persistent); + assert!( + persistent + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); + } +} + +#[test] +fn actor_no_response_void_returns_have_checked_exit_receipts() { + for operand in ["", " nothing", " finish()"] { + let result = ordinary(&format!( + "{TYPES}function finish() returns nothing:\n return nothing\nactor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n inner.value = 9\n return{operand}\n" + )); + assert_no_errors(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(handler.ownership_validated()); + assert_eq!(handler.exits().len(), 1); + assert_eq!(handler.exits()[0].kind, CheckedActorHandlerExitKind::Return); + assert!( + handler + .body() + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + } + let restored = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n Inner old = inner\n inner = Inner(value: 9)\n return nothing\n" + )); + assert_no_errors(&restored); + assert!( + restored + .actor_handlers + .values() + .all(|handler| handler.ownership_validated()) + ); +} + +#[test] +fn actor_void_return_still_checks_actual_operand_effects_and_persistent_state() { + for body in [ + " Inner old = inner\n return nothing\n", + " return discard(inner)\n", + " optional[int64] missing = none\n inner.value = missing handle:\n return discard(inner)\n", + ] { + let result = ordinary(&format!( + "{TYPES}function discard(value: Inner) returns nothing:\n return nothing\nactor Store:\n mutable Inner inner = Inner(value: 1)\n receive update:\n{body}" + )); + assert!(has_error(&result, 400), "{:?}", result.diagnostics); + assert!(!has_error(&result, 404), "{:?}", result.diagnostics); + assert!( + result + .actor_handlers + .values() + .all(|handler| !handler.ownership_validated()) + ); + } +} + +#[test] +fn actor_void_receipt_does_not_admit_values_views_or_responding_returns() { + for (header, body) in [ + ("receive future:", "return 1"), + ("receive future:", "return view nothing"), + ("receive future responds int64:", "return 1"), + ("receive future responds nothing:", "return nothing"), + ( + "receive future responds nothing:", + "int64 observed = inner.value", + ), + ] { + let result = ordinary(&format!( + "{TYPES}actor Store:\n mutable Inner inner = Inner(value: 1)\n {header}\n {body}\n" + )); + assert_no_errors(&result); + let handler = result.actor_handlers.values().next().unwrap(); + assert!(!handler.ownership_validated()); + assert_eq!(handler.exits().len(), 1); + } +} diff --git a/crates/jett_typecheck/src/checker/assignment_place_tests/metadata_tests.rs b/crates/jett_typecheck/src/checker/assignment_place_tests/metadata_tests.rs new file mode 100644 index 00000000..9ee317cb --- /dev/null +++ b/crates/jett_typecheck/src/checker/assignment_place_tests/metadata_tests.rs @@ -0,0 +1,185 @@ +//! Compiler metadata identity and field-replacement authority controls. + +use super::{assert_no_errors, ordinary}; +use crate::{CheckedAssignmentPlaceError, checked_field_owner}; +use jett_diagnostics::Severity; + +#[test] +fn each_exact_builtin_metadata_owner_rejects_projected_mutation() { + for (owner, field, value) in [ + ("TypeInfo", "type_name", "\"forged\""), + ("TypeField", "index", "1"), + ("TypeBitfield", "network_order", "true"), + ("TypeBitfieldField", "width", "1"), + ("TypeMachine", "states", "metadata.states"), + ("TypeMachineState", "index", "1"), + ("TypeMachineTransition", "source_index", "1"), + ("TypeVariant", "index", "1"), + ] { + let source = format!( + "namespace app\nfunction invalid(mutable metadata: {owner}) returns nothing:\n metadata.{field} = {value}\n" + ); + let result = ordinary(&source); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{source}: {:?}", result.diagnostics); + assert_eq!(errors[0].code.code(), 404); + assert!( + errors[0] + .message + .contains("compiler-produced reflection metadata") + ); + assert!(result.assignment_places.is_empty()); + } +} + +#[test] +fn aliases_secret_qualification_and_user_holders_preserve_metadata_barrier() { + for body in [ + "function invalid(mutable field: FieldAlias) returns nothing:\n field.index = 1\n", + "function invalid(mutable field: secret[FieldAlias]) returns nothing:\n field.index = 1\n", + "function invalid(mutable holder: Holder) returns nothing:\n holder.field.index = 1\n", + ] { + let source = format!( + "namespace app\ntype FieldAlias = TypeField\nstruct Holder:\n field: FieldAlias\n{body}" + ); + let result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 404 + && diagnostic + .message + .contains("compiler-produced reflection metadata")), + "{source}: {:?}", + result.diagnostics + ); + assert!(result.assignment_places.is_empty()); + } +} + +#[test] +fn replacing_a_complete_genuine_metadata_field_does_not_enter_it() { + let result = ordinary( + "namespace app\nstruct Holder:\n field: TypeField\nfunction replace(mutable holder: Holder, incoming: TypeField) returns nothing:\n holder.field = incoming\n", + ); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + let place = result.assignment_places.values().next().unwrap(); + assert_eq!(place.projections.len(), 1); + assert!(place.ownership_validated()); + let terminal = place.projections[0].declared_field_type; + assert_eq!( + checked_field_owner(&result.interner, terminal), + Err(CheckedAssignmentPlaceError::CompilerMetadataOwner) + ); +} + +#[test] +fn unrelated_user_lookalike_type_names_remain_ordinary_nominal_owners() { + let result = ordinary( + "namespace app\nstruct TypeField:\n index: int64\nfunction make() returns app.TypeField:\n return app.TypeField(index: 0)\nfunction update(mutable field: app.TypeField) returns app.TypeField:\n field.index = 1\n return field\nfunction main() returns int64:\n app.TypeField field = update(make())\n return field.index\n", + ); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + let place = result.assignment_places.values().next().unwrap(); + assert!(checked_field_owner(&result.interner, place.root_type).is_ok()); + assert!(place.ownership_validated()); +} + +#[test] +fn canonical_metadata_marker_survives_snapshot_revalidation() { + let result = ordinary( + "namespace app\nstruct Holder:\n field: TypeField\nfunction replace(mutable holder: Holder, incoming: TypeField) returns nothing:\n holder.field = incoming\n", + ); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap(); + let snapshot = result.interner.snapshot(); + assert_eq!( + checked_field_owner(&snapshot, place.projections[0].declared_field_type), + Err(CheckedAssignmentPlaceError::CompilerMetadataOwner) + ); + crate::validate_assignment_place_shape(place, &snapshot).unwrap(); +} + +#[test] +fn canonical_builtin_constructors_and_aliases_keep_the_existing_refusal() { + for owner in [ + "TypeInfo", + "TypeField", + "TypeBitfield", + "TypeBitfieldField", + "TypeMachine", + "TypeMachineState", + "TypeMachineTransition", + "TypeVariant", + ] { + for constructor in [owner.to_owned(), "Alias".to_owned()] { + let source = format!( + "namespace app\ntype Alias = {owner}\nfunction invalid() returns nothing:\n {owner} metadata = {constructor}()\n" + ); + let result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 354), + "{source}: {:?}", + result.diagnostics + ); + } + } +} + +#[test] +fn a_genuine_reflected_field_still_cannot_be_edited_after_construction_gate() { + let result = ordinary( + "namespace app\nstruct Record:\n first: int64\n second: int64\nfunction invalid() returns nothing:\n list[TypeField] fields = type.fields[Record]()\n mutable TypeField field = list.get[TypeField](view fields, 0) handle:\n return nothing\n field.index = 1\n", + ); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 404 + && diagnostic + .message + .contains("compiler-produced reflection metadata")), + "{:?}", + result.diagnostics + ); + assert!(result.assignment_places.is_empty()); +} + +#[test] +fn extending_a_sealed_holder_path_into_metadata_fails_independent_validation() { + use crate::{CheckedFieldOwner, CheckedFieldProjection}; + use jett_types::{Type, TypeInterner}; + + let result = ordinary( + "namespace app\nstruct Holder:\n field: TypeField\nfunction replace(mutable holder: Holder, incoming: TypeField) returns nothing:\n holder.field = incoming\n", + ); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap(); + let metadata = place.projections[0].declared_field_type; + let Type::Struct(owner) = result.interner.resolve(metadata) else { + panic!("canonical metadata is a nominal struct"); + }; + let mut forged = place.clone(); + forged.projections.push(CheckedFieldProjection { + source_span: place.projections[0].source_span, + owner_type: metadata, + owner: CheckedFieldOwner::Struct(*owner), + field_index: 0, + declared_field_type: TypeInterner::INT64, + field_type: TypeInterner::INT64, + }); + assert!(!forged.ownership_validated()); + assert_eq!( + crate::validate_assignment_place_shape(&forged, &result.interner), + Err(CheckedAssignmentPlaceError::CompilerMetadataOwner) + ); +} diff --git a/crates/jett_typecheck/src/checker/assignment_place_tests/ownership_tests.rs b/crates/jett_typecheck/src/checker/assignment_place_tests/ownership_tests.rs new file mode 100644 index 00000000..1af7f53b --- /dev/null +++ b/crates/jett_typecheck/src/checker/assignment_place_tests/ownership_tests.rs @@ -0,0 +1,395 @@ +//! Ownership and dependent-loan controls for checked field replacements. + +use super::{DECLARATIONS, assert_no_errors, ordinary}; +use jett_diagnostics::Severity; + +#[test] +fn temporary_call_view_and_terminal_return_admit_the_exact_place() { + let source = format!( + "{DECLARATIONS}function next(view record: Record) returns int64:\n return record.inner.value + 1\nfunction update(mutable record: Record) returns Record:\n record.inner.value = next(view record)\n return record\nfunction early(mutable record: Record) returns Record:\n optional[int64] absent = none\n record.inner.value = absent handle:\n return record\n return record\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 2); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); +} + +#[test] +fn rhs_consumption_and_rebinding_that_reach_commit_are_rejected() { + for body in [ + "function invalid(mutable record: Record) returns nothing:\n record.inner.value = consume(record)\n", + "function invalid(mutable record: Record) returns nothing:\n optional[int64] absent = none\n record.inner.value = absent handle:\n ((record)) = make()\n default 7\n", + ] { + let source = format!( + "{DECLARATIONS}function consume(record: Record) returns int64:\n return record.inner.value\n{body}" + ); + let result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 404 + && diagnostic + .message + .contains("while evaluating its field replacement")), + "{:?}", + result.diagnostics + ); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); + } +} + +#[test] +fn dependent_whole_and_field_aliases_do_not_authorize_root_replacement() { + for alias in [ + "Record alias = view record", + "Inner alias = view record.inner", + ] { + let source = format!( + "{DECLARATIONS}function invalid(mutable record: Record) returns nothing:\n {alias}\n record.inner.value = 9\n trace alias\n" + ); + let result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 401 + && diagnostic.message.contains("dependent local view")), + "{:?}", + result.diagnostics + ); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); + } +} + +#[test] +fn independent_scalar_and_string_copies_do_not_create_root_loans() { + let source = format!( + "{DECLARATIONS}function update(mutable record: Record) returns string:\n int64 copied = record.inner.value\n string label = record.label\n record.inner.value = copied + 1\n record.label = \"after\"\n return label\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 2); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); +} + +#[test] +fn verify_property_and_inline_owned_roots_receive_real_admission() { + let source = format!( + "{DECLARATIONS}function factory() returns function(Record) returns Record:\n return function(mutable record: Record) returns Record:\n record.inner.value = 9\n return record\nverify updated:\n mutable Record record = make()\n record.inner.value = 9\n assert record.inner.value == 9\nproperty updated_from_given:\n given original: Record\n mutable Record record = clone original\n record.inner.value = 9\n assert record.inner.value == 9\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 3); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); +} + +#[test] +fn any_source_error_prevents_private_admission_even_after_diagnostic_editing() { + let source = format!( + "{DECLARATIONS}function valid(mutable record: Record) returns nothing:\n record.inner.value = 9\nfunction invalid() returns nothing:\n Record record = make()\n record.inner.value = 9\n" + ); + let mut result = ordinary(&source); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error) + ); + result.diagnostics.clear(); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} + +#[test] +fn actor_observation_supplies_no_assignment_ownership_authority() { + let source = format!( + "{DECLARATIONS}actor Store:\n mutable app.Record record = make()\n receive replace:\n record.inner.value = 9\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} + +#[test] +fn private_admission_is_distinct_in_same_span_concrete_generic_contexts() { + let result = ordinary( + "namespace app\nstruct Slot[T]:\n item: T\nfunction replace[T](mutable slot: Slot[T], next: T) returns nothing:\n slot.item = next\nfunction callers() returns nothing:\n replace[int64](Slot[int64](item: 1), 2)\n replace[string](Slot[string](item: \"before\"), \"after\")\n", + ); + assert_no_errors(&result); + let places = result + .generic_function_instantiations + .iter() + .flat_map(|body| body.assignment_places.values()) + .collect::>(); + assert_eq!(places.len(), 2); + assert_eq!(places[0].assignment_span, places[1].assignment_span); + assert_ne!(places[0].root.ty, places[1].root.ty); + assert!(places.iter().all(|place| place.ownership_validated())); +} + +#[test] +fn an_unvisited_same_span_context_cannot_inherit_a_private_stamp() { + use crate::assignment_places::{ + AssignmentContext, AssignmentContextRoot, AssignmentSite, seal_assignment_ownership, + }; + use std::collections::{HashMap, HashSet}; + + let source = format!( + "{DECLARATIONS}function update(mutable record: Record) returns nothing:\n record.inner.value = 9\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap().clone(); + let span = place.assignment_span; + let first = AssignmentContext { + root: AssignmentContextRoot::Generic(0), + scopes: Vec::new(), + }; + let second = AssignmentContext { + root: AssignmentContextRoot::Generic(1), + scopes: Vec::new(), + }; + let validated = HashSet::from([AssignmentSite { + context: first.clone(), + span, + }]); + let mut first_places = HashMap::from([(span, place.clone())]); + let mut second_places = HashMap::from([(span, place)]); + seal_assignment_ownership(&mut first_places, &mut HashMap::new(), &first, &validated); + seal_assignment_ownership(&mut second_places, &mut HashMap::new(), &second, &validated); + assert!(first_places[&span].ownership_validated()); + assert!(!second_places[&span].ownership_validated()); +} + +#[test] +fn changing_a_same_typed_sibling_ordinal_invalidates_private_admission() { + let result = ordinary( + "namespace app\nstruct Pair:\n first: int64\n second: int64\nfunction update(mutable pair: Pair) returns nothing:\n pair.first = 9\n", + ); + assert_no_errors(&result); + let place = result.assignment_places.values().next().unwrap(); + assert!(place.ownership_validated()); + let mut changed = place.clone(); + changed.projections[0].field_index = 1; + crate::validate_assignment_place_shape(&changed, &result.interner).unwrap(); + assert!(!changed.ownership_validated()); + assert!(place.ownership_validated()); +} + +#[test] +fn active_view_iteration_borrows_the_enclosing_owned_root() { + let result = ordinary( + "namespace app\nstruct Group:\n items: list[int64]\n count: int64\nfunction invalid(mutable group: Group) returns nothing:\n for item in view group.items:\n group.count = item\n", + ); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 401 + && diagnostic.message.contains("dependent local view")), + "{:?}", + result.diagnostics + ); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} + +#[test] +fn completed_view_iteration_does_not_leave_a_phantom_loop_loan() { + let result = ordinary( + "namespace app\nstruct Group:\n items: list[int64]\n count: int64\nfunction update(mutable group: Group) returns nothing:\n mutable int64 total = 0\n for item in view group.items:\n total = total + item\n group.count = total\n", + ); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); +} + +#[test] +fn borrowed_match_payload_retains_the_enclosing_owned_root() { + let result = ordinary( + "namespace app\nstruct Inner:\n count: int64\nenum Choice:\n item(data: Inner)\nstruct Envelope:\n choice: Choice\n count: int64\nfunction invalid(mutable envelope: Envelope) returns nothing:\n match view envelope.choice:\n item(data):\n envelope.count = 9\n trace data\n", + ); + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 401 + && diagnostic.message.contains("dependent local view")), + "{:?}", + result.diagnostics + ); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} + +#[test] +fn transparent_borrowed_matches_retain_the_enclosing_owned_root() { + for (declaration, choice, scrutinee) in [ + ("", "Choice", "(view envelope.choice)"), + ("", "secret[Choice]", "declassify(view envelope.choice)"), + ( + "type RefinedChoice = Choice where true\n", + "RefinedChoice", + "coarsen(view envelope.choice)", + ), + ] { + let source = format!( + "namespace app\nstruct Inner:\n count: int64\nenum Choice:\n item(data: Inner)\n{declaration}struct Envelope:\n choice: {choice}\n count: int64\nfunction invalid(mutable envelope: Envelope) returns nothing:\n match {scrutinee}:\n item(data):\n envelope.count = 9\n trace data\n" + ); + let result = ordinary(&source); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(!errors.is_empty(), "{source}"); + assert!( + errors.iter().all(|diagnostic| diagnostic.code.code() == 401 + && diagnostic.message.contains("dependent local view")), + "{source}\n{errors:?}" + ); + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); + } +} + +#[test] +fn transparent_borrowed_match_copyable_payloads_remain_independent() { + for payload in ["int64", "string"] { + for (declaration, choice, scrutinee) in [ + ("", "Choice", "(view envelope.choice)"), + ("", "secret[Choice]", "declassify(view envelope.choice)"), + ( + "type RefinedChoice = Choice where true\n", + "RefinedChoice", + "coarsen(view envelope.choice)", + ), + ] { + let source = format!( + "namespace app\nenum Choice:\n item(data: {payload})\n{declaration}struct Envelope:\n choice: {choice}\n count: int64\nfunction update(mutable envelope: Envelope) returns nothing:\n match {scrutinee}:\n item(data):\n envelope.count = 9\n trace data\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + } + } +} + +#[test] +fn cloning_a_transparent_match_borrow_keeps_the_new_payload_independent() { + for (declaration, choice, scrutinee) in [ + ("", "Choice", "(view envelope.choice)"), + ("", "secret[Choice]", "declassify(view envelope.choice)"), + ( + "type RefinedChoice = Choice where true\n", + "RefinedChoice", + "coarsen(view envelope.choice)", + ), + ] { + let source = format!( + "namespace app\nstruct Inner:\n count: int64\nenum Choice:\n item(data: Inner)\n{declaration}struct Envelope:\n choice: {choice}\n count: int64\nfunction update(mutable envelope: Envelope) returns nothing:\n match clone ({scrutinee}):\n item(data):\n envelope.count = 9\n trace data\n" + ); + let result = ordinary(&source); + assert_no_errors(&result); + assert_eq!(result.assignment_places.len(), 1); + assert!( + result + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + } +} + +#[test] +fn pending_owned_child_is_transferred_without_becoming_a_source_rejection() { + let declarations = "namespace app\nstruct Inner:\n value: int64\nstruct Holder:\n child: Inner\nfunction make_inner() returns Inner:\n return Inner(value: 1)\n"; + let accepted = ordinary(&format!( + "{declarations}function update(mutable holder: Holder) returns Holder:\n Inner incoming = run make_inner()\n holder.child = incoming\n return holder\n" + )); + assert_no_errors(&accepted); + assert_eq!(accepted.assignment_places.len(), 1); + assert!( + accepted + .assignment_places + .values() + .all(|place| place.ownership_validated()) + ); + let rejected = ordinary(&format!( + "{declarations}function invalid(mutable holder: Holder) returns nothing:\n Inner incoming = run make_inner()\n holder.child = incoming\n trace incoming\n" + )); + assert!( + rejected + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 400), + "{:?}", + rejected.diagnostics + ); + assert!( + rejected + .assignment_places + .values() + .all(|place| !place.ownership_validated()) + ); +} diff --git a/crates/jett_typecheck/src/checker/graphics_audit.rs b/crates/jett_typecheck/src/checker/graphics_audit.rs index 2cc342e3..8439a5b4 100644 --- a/crates/jett_typecheck/src/checker/graphics_audit.rs +++ b/crates/jett_typecheck/src/checker/graphics_audit.rs @@ -29,6 +29,7 @@ impl TypeChecker<'_> { } match self.interner.resolve(ty) { Type::Error + | Type::Capability(_) | Type::TypeConstruction | Type::Function { .. } | Type::Resource(_) @@ -86,7 +87,8 @@ impl TypeChecker<'_> { | Type::String | Type::Bool | Type::Bytes - | Type::Nothing => {} + | Type::Nothing + | Type::Never => {} } } false @@ -183,7 +185,9 @@ impl TypeChecker<'_> { Expr::Ident(_) | Expr::FieldAccess(_, _, _) => { self.audit_graphics_reference(expr, None); if inline - && self.graphics_audit_type(expr.span()) == Some(TypeInterner::ERROR) + && self.graphics_audit_type(expr.span()).is_some_and(|ty| { + matches!(self.interner.resolve(ty), Type::Capability(_) | Type::Error) + }) && !self.graphics_rejected_capture_spans.contains(&expr.span()) { self.sink.emit(errors::graphics_contract( @@ -287,32 +291,29 @@ impl TypeChecker<'_> { } fn audit_graphics_equality(&mut self, operand: &Expr) { - let Some(mut owner) = self.graphics_audit_type(operand.span()) else { + let Some(owner) = self.graphics_audit_type(operand.span()) else { return; }; // Equality on a runtime struct calls Equatable.equals even though the // source contains no Call node. Use this concrete instantiation's // operand type, not the last type recorded for the generic source span. - while let Type::Secret(inner) | Type::Refinement { base: inner, .. } = - self.interner.resolve(owner) - { - owner = *inner; - } - if !matches!(self.interner.resolve(owner), Type::Struct(_)) { - return; - } let Some(&interface) = self.named_types.get("Equatable") else { return; }; - if let Some(&index) = - self.interface_method_definitions - .get(&(interface, owner, "equals".to_string())) - && let Some(function) = self - .graphics_method_definitions - .get(&self.method_definitions[index].source_span) - .cloned() - { - self.audit_graphics_function(&function, None); + for owner in self.aggregate_equality_owners(owner) { + if !matches!(self.interner.resolve(owner), Type::Struct(_)) { + continue; + } + if let Some(&index) = + self.interface_method_definitions + .get(&(interface, owner, "equals".to_string())) + && let Some(function) = self + .graphics_method_definitions + .get(&self.method_definitions[index].source_span) + .cloned() + { + self.audit_graphics_function(&function, None); + } } } @@ -329,7 +330,12 @@ impl TypeChecker<'_> { } if let Some(ty) = self.graphics_audit_type(callee.span()) && let Type::Function { params, .. } = self.interner.resolve(ty) - && params.contains(&TypeInterner::ERROR) + && params.iter().any(|param| { + matches!( + self.interner.resolve(*param), + Type::Capability(_) | Type::Error + ) + }) { self.sink.emit(errors::graphics_contract( "callbacks cannot call function values with opaque capability parameters", @@ -391,7 +397,8 @@ impl TypeChecker<'_> { .enumerate() .filter_map(|(index, instantiation)| { (instantiation.definition == call.definition - && instantiation.concrete_args == call.concrete_args) + && instantiation.concrete_args == call.concrete_args + && instantiation.specialization == call.specialization) .then_some(index) }) .collect::>(); diff --git a/crates/jett_typecheck/src/checker/list_sum_domain_tests.rs b/crates/jett_typecheck/src/checker/list_sum_domain_tests.rs new file mode 100644 index 00000000..ff0b1693 --- /dev/null +++ b/crates/jett_typecheck/src/checker/list_sum_domain_tests.rs @@ -0,0 +1,107 @@ +use super::{CheckOptions, CheckResult, check_with_options}; +use jett_common::{FileId, STDLIB_FILE_ID_START}; +use jett_diagnostics::Severity; + +fn checked(source: &str, release: bool) -> CheckResult { + let parsed = jett_parser::parse(source, FileId::new(STDLIB_FILE_ID_START)); + assert!(parsed.errors.is_empty(), "{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{:?}", + resolved.diagnostics, + ); + check_with_options(&parsed.module, &resolved, CheckOptions { release }) +} + +#[test] +fn list_sum_kernel_accepts_all_numeric_primitives_and_transparent_alias_chains() { + for primitive in [ + "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", + "float64", + ] { + for element in [primitive, "Number", "Alias"] { + let source = format!( + "namespace app\ntype Number = {primitive}\ntype Alias = Number\nfunction total(view values: list[{element}]) returns {element}:\n return list.__sum[{element}](view values)\n" + ); + for release in [false, true] { + let checked = checked(&source, release); + assert!( + checked + .diagnostics + .iter() + .all(|error| error.severity != Severity::Error), + "{primitive}/{element}/{release}: {:?}", + checked.diagnostics, + ); + } + } + } +} + +#[test] +fn list_sum_kernel_rejects_nonnumeric_refinement_and_secret_elements() { + let declarations = "namespace app\ntype Positive = int64 where value > 0\ntype PositiveAlias = Positive\ntype Hidden = secret[int64]\nstruct Record:\n value: int64\nenum Choice:\n empty\ninterface Named:\n function name(view self: Named) returns string\n"; + for element in [ + "bool", + "string", + "nothing", + "bytes", + "list[int64]", + "map[int64, int64]", + "set[int64]", + "optional[int64]", + "result[int64, string]", + "Positive", + "PositiveAlias", + "secret[int64]", + "secret[secret[int64]]", + "Hidden", + "secret[Positive]", + "Record", + "Choice", + "Named", + "function() returns int64", + ] { + let source = format!( + "{declarations}function total(view values: list[{element}]) returns {element}:\n return list.__sum[{element}](view values)\n" + ); + for release in [false, true] { + let checked = checked(&source, release); + let errors = checked + .diagnostics + .iter() + .filter(|error| error.severity == Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{element}/{release}: {errors:?}"); + assert_eq!(errors[0].code.code(), 300, "{element}: {errors:?}"); + assert!(errors[0].message.contains("numeric primitive for list.sum")); + } + } +} + +#[test] +fn list_sum_kernel_requires_its_selected_type_instead_of_a_wildcard() { + for invocation in [ + "list.__sum(view values)", + "list.__sum[int64, bool](view values)", + ] { + let source = format!( + "namespace app\nfunction total(view values: list[bool]) returns bool:\n return {invocation}\n" + ); + for release in [false, true] { + let checked = checked(&source, release); + let errors = checked + .diagnostics + .iter() + .filter(|error| error.severity == Severity::Error) + .collect::>(); + assert_eq!(errors.len(), 1, "{invocation}/{release}: {errors:?}"); + assert_eq!(errors[0].code.code(), 309, "{invocation}: {errors:?}"); + assert!(errors[0].message.contains("expected 1 type argument")); + } + } +} diff --git a/crates/jett_typecheck/src/checker/nonzero_facts.rs b/crates/jett_typecheck/src/checker/nonzero_facts.rs new file mode 100644 index 00000000..c9fb52c4 --- /dev/null +++ b/crates/jett_typecheck/src/checker/nonzero_facts.rs @@ -0,0 +1,317 @@ +//! Local nonzero facts for exact bindings and finite ordinary field places. +//! These are observed guard facts, never mutation or ownership authority. + +use super::*; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) enum NonzeroFact { + Binding(DefId), + Field { + root: DefId, + root_type: TypeId, + projections: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct NonzeroProjection { + owner: NonzeroOwner, + owner_type: TypeId, + ordinal: usize, + declared_type: TypeId, + access_type: TypeId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum NonzeroOwner { + Struct(StructId), + MachineState(MachineId, MachineStateId), +} + +impl NonzeroFact { + fn root(&self) -> DefId { + match self { + Self::Binding(root) | Self::Field { root, .. } => *root, + } + } +} + +impl TypeChecker<'_> { + pub(super) fn nonzero_fact(&self, expression: &Expr) -> Option { + match expression { + Expr::Ident(name) => self.ident_def_id(name).map(NonzeroFact::Binding), + Expr::Paren(inner, _) => self.nonzero_fact(inner), + Expr::FieldAccess(_, _, _) => { + let name = Self::assignment_root(expression)?; + let root = self.ident_def_id(name)?; + if !matches!( + self.resolve.scope_table.def(root).kind, + DefKind::Variable | DefKind::Param + ) { + return None; + } + let root_type = self.checked_expression_type(name.span)?; + let mut checked = Vec::new(); + if !self.collect_assignment_projections(expression, &mut checked) + || checked.is_empty() + { + return None; + } + let projections = checked + .into_iter() + .map(|projection| NonzeroProjection { + owner: match projection.owner { + crate::CheckedFieldOwner::Struct(id) => NonzeroOwner::Struct(id), + crate::CheckedFieldOwner::MachineState { machine, state } => { + NonzeroOwner::MachineState(machine, state) + } + }, + owner_type: projection.owner_type, + ordinal: projection.field_index, + declared_type: projection.declared_field_type, + access_type: projection.field_type, + }) + .collect(); + Some(NonzeroFact::Field { + root, + root_type, + projections, + }) + } + _ => None, + } + } + + pub(super) fn invalidate_nonzero_root(&mut self, root: DefId) { + for scope in &mut self.nonzero_fact_scopes { + scope.retain(|fact| fact.root() != root); + } + } + + fn invalidate_nonzero_roots(&mut self, roots: &HashSet) { + for scope in &mut self.nonzero_fact_scopes { + scope.retain(|fact| !roots.contains(&fact.root())); + } + } + + pub(super) fn nonzero_defs_from_conditions<'b>( + &self, + conditions: impl IntoIterator, + ) -> HashSet { + let mut facts = HashSet::new(); + let mut modified = HashSet::new(); + for (condition, truth) in conditions { + self.collect_nonzero_defs(condition, truth, &mut facts); + self.nonzero_expression_writes(condition, &mut modified); + } + // A later operand/else-if may have changed an earlier tested place. + // Refuse to remint that fact from the condition's original syntax. + facts.retain(|fact| !modified.contains(&fact.root())); + facts + } + + fn collect_nonzero_defs( + &self, + condition: &Expr, + truth: bool, + facts: &mut HashSet, + ) { + match condition { + Expr::Paren(inner, _) => self.collect_nonzero_defs(inner, truth, facts), + Expr::Unary(UnaryOp::Not, inner, _) => self.collect_nonzero_defs(inner, !truth, facts), + Expr::Binary(lhs, BinOp::And, rhs, _) if truth => { + self.collect_nonzero_defs(lhs, true, facts); + self.collect_nonzero_defs(rhs, true, facts); + } + Expr::Binary(lhs, BinOp::Or, rhs, _) if !truth => { + self.collect_nonzero_defs(lhs, false, facts); + self.collect_nonzero_defs(rhs, false, facts); + } + Expr::Binary(lhs, op @ (BinOp::Eq | BinOp::NotEq), rhs, _) => { + if matches!(op, BinOp::NotEq) != truth { + return; + } + let tested = if Self::is_zero_integer_literal(rhs) { + Some(lhs.as_ref()) + } else if Self::is_zero_integer_literal(lhs) { + Some(rhs.as_ref()) + } else { + None + }; + if let Some(fact) = tested.and_then(|value| self.nonzero_fact(value)) { + facts.insert(fact); + } + } + _ => {} + } + } + + fn is_zero_integer_literal(expr: &Expr) -> bool { + match expr { + Expr::IntLiteral(value, _) => *value == 0, + Expr::Unary(UnaryOp::Neg, inner, _) | Expr::Paren(inner, _) => { + Self::is_zero_integer_literal(inner) + } + _ => false, + } + } + + pub(super) fn invalidate_nonzero_loop_facts(&mut self, body: &Block, condition: Option<&Expr>) { + let mut modified = HashSet::new(); + self.nonzero_block_writes(body, &mut modified); + if let Some(condition) = condition { + self.nonzero_expression_writes(condition, &mut modified); + } + // The checker visits a loop body once; inherited observations must also + // be valid at a backedge. A fresh per-iteration guard may prove them anew. + self.invalidate_nonzero_roots(&modified); + } + + fn nonzero_block_writes(&self, body: &Block, roots: &mut HashSet) { + for statement in &body.stmts { + match statement { + Stmt::VarDecl(value) => self.nonzero_expression_writes(&value.value, roots), + Stmt::Assign(value) => { + if let Some(root) = Self::assignment_root(&value.target) + .and_then(|name| self.ident_def_id(name)) + { + roots.insert(root); + } + self.nonzero_expression_writes(&value.target, roots); + self.nonzero_expression_writes(&value.value, roots); + } + Stmt::Return(value) => { + if let Some(value) = &value.value { + self.nonzero_expression_writes(value, roots); + } + } + Stmt::Respond(value) => self.nonzero_expression_writes(&value.value, roots), + Stmt::ComptimeTypeBind(value) => { + self.nonzero_expression_writes(&value.value, roots); + self.nonzero_block_writes(&value.body, roots); + } + Stmt::If(value) => { + self.nonzero_expression_writes(&value.condition, roots); + self.nonzero_block_writes(&value.then_block, roots); + for (condition, body) in &value.else_ifs { + self.nonzero_expression_writes(condition, roots); + self.nonzero_block_writes(body, roots); + } + if let Some(body) = &value.else_block { + self.nonzero_block_writes(body, roots); + } + } + Stmt::For(value) => { + self.nonzero_expression_writes(&value.iterable, roots); + self.nonzero_block_writes(&value.body, roots); + } + Stmt::While(value) => { + self.nonzero_expression_writes(&value.condition, roots); + self.nonzero_block_writes(&value.body, roots); + } + Stmt::Match(value) => { + self.nonzero_expression_writes(&value.expr, roots); + for arm in &value.arms { + self.nonzero_block_writes(&arm.body, roots); + } + } + Stmt::Expr(value) => self.nonzero_expression_writes(&value.expr, roots), + Stmt::Assert(value) => { + self.nonzero_expression_writes(&value.condition, roots); + if let Some(message) = &value.message { + self.nonzero_expression_writes(message, roots); + } + } + Stmt::Breakpoint(value) => { + if let Some(condition) = &value.condition { + self.nonzero_expression_writes(condition, roots); + } + } + Stmt::Use(_) | Stmt::Trace(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + } + } + } + + fn nonzero_expression_writes(&self, value: &Expr, roots: &mut HashSet) { + match value { + Expr::Handle(target, _, body, _) => { + self.nonzero_expression_writes(target, roots); + self.nonzero_block_writes(body, roots); + } + Expr::Binary(left, _, right, _) => { + self.nonzero_expression_writes(left, roots); + self.nonzero_expression_writes(right, roots); + } + Expr::Unary(_, value, _) + | Expr::FieldAccess(value, _, _) + | Expr::Paren(value, _) + | Expr::View(value, _) + | Expr::Comptime(value, _) + | Expr::Ok(value, _) + | Expr::Fail(value, _) + | Expr::Some(value, _) + | Expr::Default(value, _) + | Expr::Declassify(value, _) + | Expr::Coarsen(value, _) + | Expr::At(value, _, _) + | Expr::Spawn(value, _) + | Expr::Send(value, _) + | Expr::Ask(value, _) + | Expr::Clone(value, _) + | Expr::Run(value, _) + | Expr::Join(value, _) + | Expr::Cancel(value, _) => self.nonzero_expression_writes(value, roots), + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + self.nonzero_expression_writes(callee, roots); + for arg in args { + self.nonzero_expression_writes(&arg.value, roots); + } + } + Expr::ListConstruct(values, _) => { + for value in values { + self.nonzero_expression_writes(value, roots); + } + } + Expr::MapConstruct(entries, _) => { + for (key, value) in entries { + self.nonzero_expression_writes(key, roots); + self.nonzero_expression_writes(value, roots); + } + } + Expr::StringInterpolation(parts, _) => { + for part in parts { + if let StringPart::Expr(value) = part { + self.nonzero_expression_writes(value, roots); + } + } + } + Expr::Pipeline(value, steps, _) => { + self.nonzero_expression_writes(value, roots); + for step in steps { + self.nonzero_expression_writes(&step.function, roots); + for arg in &step.extra_args { + self.nonzero_expression_writes(&arg.value, roots); + } + if let Some(handle) = &step.handle { + self.nonzero_block_writes(&handle.body, roots); + } + } + } + // Defining a closure does not execute its body. Captures keep the + // existing copy-only policy; this scan introduces no alias effects. + Expr::InlineFn(_, _, _, _) + | Expr::IntLiteral(_, _) + | Expr::FloatLiteral(_, _) + | Expr::StringLiteral(_, _) + | Expr::BoolLiteral(_, _) + | Expr::Nothing(_) + | Expr::Ident(_) + | Expr::None(_) + | Expr::EnumVariant(_, _, _) + | Expr::Error(_) => {} + } + } +} diff --git a/crates/jett_typecheck/src/checker/nonzero_facts/tests.rs b/crates/jett_typecheck/src/checker/nonzero_facts/tests.rs new file mode 100644 index 00000000..8b0c3cf1 --- /dev/null +++ b/crates/jett_typecheck/src/checker/nonzero_facts/tests.rs @@ -0,0 +1,178 @@ +use crate::{CheckOptions, CheckResult, check_with_options}; +use jett_common::FileId; +use jett_diagnostics::Severity; + +const RECORDS: &str = "namespace app\nstruct Inner:\n divisor: int64\nstruct Record:\n inner: Inner\n divisor: int64\n sibling: int64\n"; + +fn checked(body: &str) -> CheckResult { + let source = format!("{RECORDS}{body}"); + let parsed = jett_parser::parse(&source, FileId::new(0)); + assert!(parsed.errors.is_empty(), "{source}\n{:?}", parsed.errors); + let resolved = jett_resolve::resolve(&parsed.module); + assert!( + resolved + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{source}\n{:?}", + resolved.diagnostics + ); + check_with_options(&parsed.module, &resolved, CheckOptions { release: false }) +} + +fn assert_valid(body: &str) { + let result = checked(body); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{body}\n{:?}", + result.diagnostics + ); +} + +fn assert_nonzero_error(body: &str) { + let result = checked(body); + let errors = result + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .collect::>(); + assert!(!errors.is_empty(), "{body}"); + assert!( + errors + .iter() + .all(|diagnostic| diagnostic.code.code() == 361), + "{body}\n{errors:?}" + ); +} + +#[test] +fn visible_guards_prove_exact_readonly_and_nested_fields() { + for body in [ + " if record.divisor != 0:\n return 8 / record.divisor\n return 0\n", + " if 0 != (record.inner).divisor:\n return 8 / record.inner.divisor\n return 0\n", + " if record.divisor == 0:\n return 0\n return 8 / record.divisor\n", + " if record.divisor == 0:\n return 0\n else:\n return 8 / record.divisor\n", + ] { + assert_valid(&format!( + "function guarded(view record: Record) returns int64:\n{body}" + )); + } +} + +#[test] +fn any_same_root_write_invalidates_nested_field_facts_after_the_rhs() { + for write in [ + "record.inner.divisor = 0", + "record.sibling = 2", + "record.inner = Inner(divisor: 0)", + "record = Record(inner: Inner(divisor: 0), divisor: 0, sibling: 0)", + ] { + assert_nonzero_error(&format!( + "function invalid(mutable record: Record) returns int64:\n if record.inner.divisor != 0:\n int64 before = 8 / record.inner.divisor\n {write}\n return 8 / record.inner.divisor\n return 0\n" + )); + } + assert_valid( + "function replace(mutable record: Record) returns int64:\n if record.divisor != 0:\n record.divisor = 8 / record.divisor\n return 0\n", + ); + assert_nonzero_error( + "function invalid(mutable record: Record) returns int64:\n if record.divisor != 0:\n record.divisor = 8 / record.divisor\n return 8 / record.divisor\n return 0\n", + ); +} + +#[test] +fn field_facts_keep_alias_nominals_root_bindings_and_siblings_distinct() { + assert_valid( + "type Alias = Record\nfunction guarded(view record: Alias, mutable other: Record) returns int64:\n if record.inner.divisor != 0:\n other.inner.divisor = 0\n return 8 / record.inner.divisor\n return 0\n", + ); + assert_valid( + "function guarded(view record: Record) returns int64:\n Record alias = view record\n if alias.divisor != 0:\n return 8 / alias.divisor\n return 0\n", + ); + for divisor in ["other.divisor", "record.sibling"] { + assert_nonzero_error(&format!( + "function invalid(view record: Record, view other: Record) returns int64:\n if record.divisor != 0:\n return 8 / {divisor}\n return 0\n" + )); + } +} + +#[test] +fn loop_backedges_kill_inherited_field_and_scalar_facts() { + for (parameters, divisor, write) in [ + ( + "mutable record: Record, keep: bool", + "record.divisor", + "record.divisor = 0", + ), + ( + "mutable divisor: int64, keep: bool", + "divisor", + "divisor = 0", + ), + ] { + for header in ["for item in list(1, 2)", "while keep"] { + assert_nonzero_error(&format!( + "function invalid({parameters}) returns int64:\n if {divisor} != 0:\n {header}:\n int64 quotient = 8 / {divisor}\n {write}\n return 0\n" + )); + } + } + assert_valid( + "function guarded(mutable record: Record) returns int64:\n while record.divisor != 0:\n int64 quotient = 8 / record.divisor\n record.divisor = 0\n return 0\n", + ); + assert_valid( + "function guarded(mutable divisor: int64) returns int64:\n for item in list(1, 2):\n if divisor != 0:\n int64 quotient = 8 / divisor\n divisor = 0\n return 0\n", + ); +} + +#[test] +fn condition_handler_writes_cannot_remint_field_or_scalar_facts() { + for (parameters, divisor, write) in [ + ( + "mutable record: Record", + "record.divisor", + "record.divisor = 0", + ), + ("mutable divisor: int64", "divisor", "divisor = 0"), + ] { + assert_nonzero_error(&format!( + "function invalid({parameters}) returns int64:\n optional[bool] absent = none\n if {divisor} != 0 and (absent handle:\n {write}\n default true\n ):\n return 8 / {divisor}\n return 0\n" + )); + assert_nonzero_error(&format!( + "function invalid({parameters}) returns int64:\n optional[bool] absent = none\n if {divisor} == 0:\n return 0\n else if (absent handle:\n {write}\n default true\n ):\n return 8 / {divisor}\n return 0\n" + )); + } +} + +#[test] +fn guarded_field_keys_remain_concrete_inside_generic_bodies() { + assert_valid( + "struct GenericRecord[T]:\n divisor: int64\n item: T\nfunction guarded[T](view record: GenericRecord[T]) returns int64:\n if record.divisor != 0:\n return 8 / record.divisor\n return 0\nfunction main() returns int64:\n int64 first = guarded[int64](view GenericRecord[int64](divisor: 2, item: 1))\n return first + guarded[string](view GenericRecord[string](divisor: 2, item: \"two\"))\n", + ); +} + +#[test] +fn exact_machine_state_field_guards_use_the_checked_owner() { + assert_valid( + "machine Counter:\n states:\n ready(divisor: int64)\nfunction guarded(view counter: Counter at ready) returns int64:\n if counter.divisor != 0:\n return 8 / counter.divisor\n return 0\n", + ); +} + +#[test] +fn a_conditional_write_and_repeated_condition_cannot_keep_inherited_facts() { + assert_nonzero_error( + "function invalid(mutable record: Record, replace: bool) returns int64:\n if record.divisor != 0:\n if replace:\n record.divisor = 0\n return 8 / record.divisor\n return 0\n", + ); + for (parameters, divisor, write) in [ + ( + "mutable record: Record", + "record.divisor", + "record.divisor = 0", + ), + ("mutable divisor: int64", "divisor", "divisor = 0"), + ] { + assert_nonzero_error(&format!( + "function invalid({parameters}) returns int64:\n optional[bool] absent = none\n if {divisor} != 0:\n while 8 / {divisor} != 0 and (absent handle:\n {write}\n default true\n ):\n int64 marker = 1\n return 0\n" + )); + } +} diff --git a/crates/jett_typecheck/src/checker/resource_copy_tests.rs b/crates/jett_typecheck/src/checker/resource_copy_tests.rs new file mode 100644 index 00000000..ae653d6a --- /dev/null +++ b/crates/jett_typecheck/src/checker/resource_copy_tests.rs @@ -0,0 +1,280 @@ +use super::{CheckOptions, CheckResult, check_with_options}; +use jett_common::{FileId, STDLIB_FILE_ID_START}; +use jett_diagnostics::Severity; +use jett_parser::{ + ast::{Item, Module}, + parse, +}; +use jett_resolve::resolve; +use jett_types::TypeInterner; + +const PREFIX: &str = "namespace resource_copy\nexport resource Handle\ntype HandleAlias = Handle\nstruct Holder:\n token: Handle\n count: int64\nstruct Envelope:\n child: Holder\n label: string\nstruct Slot[T]:\n item: T\n count: int64\nstruct Phantom[T]:\n count: int64\n"; + +fn parsed(source: &str, file: FileId) -> Module { + let parsed = parse(source, file); + assert!( + !parsed + .errors + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "parse errors: {:?}", + parsed.errors, + ); + parsed.module +} + +fn checked_module(module: &Module, release: bool) -> CheckResult { + let resolved = resolve(module); + assert!( + !resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "resolution errors: {:?}", + resolved.diagnostics, + ); + assert!(resolved.resource_kernels.is_empty()); + let checked = check_with_options(module, &resolved, CheckOptions { release }); + assert!(checked.resource_hooks.is_empty()); + checked +} + +fn checked_source(body: &str, release: bool) -> CheckResult { + let source = format!("{PREFIX}{body}"); + let module = parsed(&source, FileId::new(STDLIB_FILE_ID_START)); + checked_module(&module, release) +} + +fn assert_codes(checked: &CheckResult, expected: &[u16]) { + let mut actual = checked + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity == Severity::Error) + .map(|diagnostic| diagnostic.code.code()) + .collect::>(); + actual.sort_unstable(); + let mut expected = expected.to_vec(); + expected.sort_unstable(); + assert_eq!(actual, expected, "diagnostics: {:?}", checked.diagnostics); +} + +#[test] +fn resource_copy_owned_field_acquisition_rejects_borrowed_and_retained_owned_parents() { + let bodies = [ + "function copy_from_view(view holder: Holder) returns Handle:\n return holder.token\n", + "function receive_both(token: Handle, holder: Holder) returns nothing:\n return nothing\nfunction duplicate_parent(holder: Holder) returns nothing:\n Handle copied = holder.token\n receive_both(copied, holder)\n return nothing\n", + "function alias_copy(view holder: Holder) returns HandleAlias:\n return holder.token\n", + ]; + for release in [false, true] { + for body in bodies { + assert_codes(&checked_source(body, release), &[364]); + } + } +} + +#[test] +fn resource_copy_nested_projection_checks_only_the_terminal_endpoint() { + let denied = [ + "function copy_nested(view envelope: Envelope) returns Handle:\n return envelope.child.token\n", + "function copy_aggregate(view envelope: Envelope) returns Holder:\n return envelope.child\n", + "function copy_optional(view slot: Slot[optional[Handle]]) returns optional[Handle]:\n return slot.item\n", + ]; + let allowed = "function inspect(view token: Handle) returns nothing:\n return nothing\nfunction count(view envelope: Envelope) returns int64:\n return envelope.child.count\nfunction label(view envelope: Envelope) returns string:\n return envelope.label\nfunction observe(view envelope: Envelope) returns nothing:\n Holder child = view envelope.child\n Handle token = view child.token\n Handle forwarded = token\n inspect(view forwarded)\n return nothing\n"; + for release in [false, true] { + for body in denied { + assert_codes(&checked_source(body, release), &[364]); + } + let checked = checked_source(allowed, release); + assert_codes(&checked, &[]); + assert!( + checked + .binding_modes + .values() + .any(|mode| matches!(mode, super::CheckedBindingMode::View { .. })) + ); + } +} + +#[test] +fn resource_copy_explicit_views_whole_moves_and_assignment_addresses_stay_valid() { + let body = "function inspect(view token: Handle) returns nothing:\n return nothing\nfunction move_handle(token: Handle) returns Handle:\n return token\nfunction move_holder(holder: Holder) returns Holder:\n return holder\nfunction owned_payload(token: Handle) returns Holder:\n return Holder(token: token, count: 1)\nfunction borrow(view holder: Holder) returns nothing:\n Handle alias = view (holder.token)\n Handle forwarded = alias\n inspect(view forwarded)\n return nothing\nfunction replace(holder: Holder, token: Handle) returns Holder:\n mutable Holder changed = holder\n changed.token = token\n return changed\nfunction update_count(envelope: Envelope) returns Envelope:\n mutable Envelope changed = envelope\n changed.child.count = 2\n return changed\n"; + for release in [false, true] { + assert_codes(&checked_source(body, release), &[]); + } +} + +#[test] +fn resource_copy_projection_modes_do_not_escape_into_owning_calls_or_wrappers() { + let bodies = [ + "function wrapped(view holder: Holder) returns nothing:\n optional[Handle] borrowed = view some(holder.token)\n return nothing\n", + "function wrap(token: Handle) returns Holder:\n return Holder(token: token, count: 1)\nfunction wrapped(view holder: Holder) returns nothing:\n Holder borrowed = view wrap(holder.token)\n return nothing\n", + "function wrapped(view holder: Holder) returns nothing:\n list[Handle] borrowed = view list(holder.token)\n return nothing\n", + ]; + for release in [false, true] { + for body in bodies { + assert_codes(&checked_source(body, release), &[364]); + } + } +} + +#[test] +fn resource_copy_explicit_clone_has_one_refusal_and_keeps_safe_nested_fields() { + let denied = [ + "function cloned(view envelope: Envelope) returns Holder:\n return clone envelope.child\n", + "function cloned(view envelope: Envelope) returns Handle:\n return clone view envelope.child.token\n", + ]; + let allowed = "function cloned_count(view envelope: Envelope) returns int64:\n return clone envelope.child.count\n"; + for release in [false, true] { + for body in denied { + assert_codes(&checked_source(body, release), &[364]); + } + assert_codes(&checked_source(allowed, release), &[]); + } +} + +#[test] +fn resource_copy_generic_field_specializations_use_concrete_data_not_phantom_arguments() { + let denied = "function extract[T](view slot: Slot[T]) returns T:\n return slot.item\nfunction copied(view slot: Slot[Handle]) returns Handle:\n return extract[Handle](view slot)\n"; + let allowed = "function extract[T](view slot: Slot[T]) returns T:\n return slot.item\nfunction copied(view slot: Slot[int64]) returns int64:\n return extract[int64](view slot)\nfunction cloned_phantom(view slot: Slot[Phantom[Handle]]) returns Phantom[Handle]:\n return clone slot.item\n"; + for release in [false, true] { + assert_codes(&checked_source(denied, release), &[364]); + let checked = checked_source(allowed, release); + assert_codes(&checked, &[]); + assert!( + checked + .generic_function_instantiations + .iter() + .any(|body| body.concrete_args == [TypeInterner::INT64]) + ); + } +} + +#[test] +fn resource_copy_list_clone_getter_rejects_recursive_elements_and_preserves_exact_states() { + let denied = [ + "function copied(view items: list[Handle]) returns optional[Handle]:\n return list.__get_clone[Handle](view items, 0)\n", + "function copied(view items: list[Holder]) returns optional[Holder]:\n return list.__get_clone[Holder](view items, 0)\n", + "function copied(view items: list[list[optional[Handle]]]) returns optional[list[optional[Handle]]]:\n return list.__get_clone[list[optional[Handle]]](view items, 0)\n", + "function copied(view items: list[Handle]) returns optional[Handle]:\n return items into view list.__get_clone[Handle](0)\n", + ]; + let allowed = "machine Session:\n states:\n ready(count: int64)\n holding(token: Handle)\n transitions:\n ready to holding\nfunction copied(view items: list[Phantom[Handle]]) returns optional[Phantom[Handle]]:\n return list.__get_clone[Phantom[Handle]](view items, 0)\nfunction selected(view items: list[Session at ready]) returns optional[Session at ready]:\n return list.__get_clone[Session at ready](view items, 0)\n"; + for release in [false, true] { + for body in denied { + assert_codes(&checked_source(body, release), &[364]); + } + assert_codes(&checked_source(allowed, release), &[]); + } +} + +#[test] +fn resource_copy_reflected_getters_check_returned_data_and_allow_unrelated_owner_resources() { + let types = "struct NestedHolder:\n values: list[optional[Handle]]\nenum Event:\n held(token: Handle, count: int64)\n nested(values: list[optional[Handle]])\nmachine Session:\n states:\n held(token: Handle, count: int64)\n nested(values: list[optional[Handle]])\n empty\n transitions:\n held to empty\n nested to empty\n"; + let cases = [ + ("type.field_value", "Holder", "NestedHolder"), + ("type.variant_field_value", "Event", "Event"), + ( + "type.machine_field_value", + "Session at held", + "Session at nested", + ), + ]; + for release in [false, true] { + for (getter, owner, nested_owner) in cases { + for (owner, returned) in [(owner, "Handle"), (nested_owner, "list[optional[Handle]]")] { + let body = format!( + "{types}function copied(view owner: {owner}, view field: TypeField) returns {returned}:\n return {getter}[{owner}, {returned}](view owner, view field)\n" + ); + assert_codes(&checked_source(&body, release), &[364]); + } + let direct = format!( + "{types}function copied(view owner: {owner}, view field: TypeField) returns int64:\n return {getter}[{owner}, int64](view owner, view field)\n" + ); + assert_codes(&checked_source(&direct, release), &[]); + let piped = format!( + "{types}function copied(view owner: {owner}, view field: TypeField) returns Handle:\n return owner into view {getter}[{owner}, Handle](view field)\n" + ); + assert_codes(&checked_source(&piped, release), &[364]); + } + } +} + +#[test] +fn resource_copy_reflected_field_getters_refuse_exact_resource_root_and_alias() { + for release in [false, true] { + for getter in [ + "type.field_value", + "type.variant_field_value", + "type.machine_field_value", + ] { + for resource in ["Handle", "HandleAlias"] { + let body = format!( + "function invalid_root(view owner: {resource}, view field: TypeField) returns int64:\n return {getter}[{resource}, int64](view owner, view field)\n" + ); + let checked = checked_source(&body, release); + assert_codes(&checked, &[300]); + assert!(checked.diagnostics.iter().any(|diagnostic| { + diagnostic.severity == Severity::Error + && diagnostic + .message + .contains(&format!("non-resource value for {getter}")) + })); + } + } + } +} + +fn public_get_module(project: &str) -> Module { + let resource = "namespace copy_provider\nexport resource Handle\n"; + let mut module = parsed(resource, FileId::new(STDLIB_FILE_ID_START)); + // Parse the real shipped facade, retaining its original body and source spans. + let mut list = parsed( + include_str!("../../../../stdlib/list.jett"), + FileId::new(STDLIB_FILE_ID_START + 1), + ); + list.items.retain(|item| { + matches!(item, Item::Namespace(_)) + || matches!(item, Item::Function(function) if function.name.name == "get") + }); + assert_eq!( + list.items.len(), + 2, + "expected namespace and actual public get" + ); + module.items.extend(list.items); + module.items.extend(parsed(project, FileId::new(0)).items); + module +} + +#[test] +fn resource_copy_public_list_get_checks_the_real_facade_concrete_resource_specialization() { + let project = "namespace caller\nfunction copied(view items: list[copy_provider.Handle]) returns optional[copy_provider.Handle]:\n return list.get[copy_provider.Handle](view items, 0)\n"; + let module = public_get_module(project); + for release in [false, true] { + let checked = checked_module(&module, release); + assert_codes(&checked, &[364]); + let diagnostic = checked + .diagnostics + .iter() + .find(|diagnostic| diagnostic.severity == Severity::Error) + .unwrap(); + assert_eq!(diagnostic.span.file, FileId::new(STDLIB_FILE_ID_START + 1)); + assert!(diagnostic.message.contains("copy_provider.Handle")); + } +} + +#[test] +fn resource_copy_public_list_get_preserves_non_resource_and_phantom_specializations() { + let project = "namespace caller\nstruct Holder:\n token: copy_provider.Handle\n count: int64\nstruct Phantom[T]:\n count: int64\nfunction copied(view items: list[int64]) returns optional[int64]:\n return list.get[int64](view items, 0)\nfunction phantom(view items: list[Phantom[copy_provider.Handle]]) returns optional[Phantom[copy_provider.Handle]]:\n return list.get[Phantom[copy_provider.Handle]](view items, 0)\nfunction count(view holder: Holder) returns int64:\n return holder.count\n"; + let module = public_get_module(project); + for release in [false, true] { + let checked = checked_module(&module, release); + assert_codes(&checked, &[]); + assert!(checked.generic_function_instantiations.iter().any(|body| { + body.concrete_args == [TypeInterner::INT64] + && body + .intrinsic_type_arguments + .values() + .any(|arguments| arguments == &[TypeInterner::INT64]) + })); + } +} diff --git a/crates/jett_typecheck/src/errors.rs b/crates/jett_typecheck/src/errors.rs index 642e9845..ecef383d 100644 --- a/crates/jett_typecheck/src/errors.rs +++ b/crates/jett_typecheck/src/errors.rs @@ -947,6 +947,66 @@ pub fn unspecialized_generic_function_value(name: &str, span: Span) -> Diagnosti ) } +/// E0374: One canonical generic instantiation cannot have two static bodies. +pub fn conflicting_generic_static_selection(span: Span) -> Diagnostic { + Diagnostic::error( + 374, + "generic instantiation has conflicting compile-time control-flow selections".to_string(), + span, + ) +} + +/// E0375: A borrowed argument cannot satisfy an owning function parameter. +pub fn view_argument_requires_view_parameter(span: Span) -> Diagnostic { + Diagnostic::error( + 375, + "`view` argument requires a `view` function parameter".to_string(), + span, + ) +} + +/// E0376: Collections and built-in sums have no direct equality operation. +pub fn direct_collection_equality(type_name: &str, op: &str, span: Span) -> Diagnostic { + Diagnostic::error( + 376, + format!( + "operator `{op}` is not defined for `{type_name}`; compare its contents explicitly" + ), + span, + ) +} + +/// E0376: Erasing a concrete type does not provide a dynamic equality contract. +pub fn erased_interface_equality(type_name: &str, op: &str, span: Span) -> Diagnostic { + Diagnostic::error( + 376, + format!( + "operator `{op}` is not defined for interface-typed values of `{type_name}`; compare before erasure or call an explicit comparison method" + ), + span, + ) +} + +/// E0377: Namespace-level declarations cannot introduce mutable global state. +pub fn mutable_global(name: &str, span: Span) -> Diagnostic { + Diagnostic::error( + 377, + format!( + "namespace-level binding `{name}` cannot be mutable; global declarations must be immutable compile-time constants" + ), + span, + ) +} + +/// E0378: Required value computation in a constant must be explicit. +pub fn constant_initializer(span: Span) -> Diagnostic { + Diagnostic::error( + 378, + "global constant initializer must use a literal, an earlier constant, or `comptime expression` for required evaluation", + span, + ) +} + /// E0800: Function body exceeds the statement count limit. pub fn function_statement_limit( function_name: &str, diff --git a/crates/jett_typecheck/src/lib.rs b/crates/jett_typecheck/src/lib.rs index dc473074..e0086ffd 100644 --- a/crates/jett_typecheck/src/lib.rs +++ b/crates/jett_typecheck/src/lib.rs @@ -1,13 +1,51 @@ // Type checking for the Jett compiler. +pub mod actor_messages; +pub use actor_messages::{ + CheckedActorBindingRole, CheckedActorHandler, CheckedActorHandlerBinding, + CheckedActorHandlerExit, CheckedActorHandlerExitKind, CheckedActorHandlerTarget, + CheckedActorMessageCall, CheckedActorMessageKind, CheckedActorMessageRoot, +}; +pub mod assignment_places; +pub use assignment_places::{ + CheckedAssignmentPlace, CheckedAssignmentPlaceError, CheckedFieldOwner, CheckedFieldProjection, + CheckedFieldUpdateResourceRequirement, checked_field_owner, field_update_resource_requirement, + validate_assignment_place_shape, +}; +pub mod caller_ownership; pub mod capability; pub mod checker; pub mod complexity; pub mod errors; pub mod ownership; +mod reflected_return; +pub mod resource_hooks; +mod resource_program; + +pub use reflected_return::{ + CheckedReflectedCallablePlan, ProducedRefinementRequirement, ProducedRefinementRequirementPlan, + ProducedRefinementUnsupported, produced_refinement_requirement, +}; pub use checker::{ - CheckOptions, CheckResult, CheckedCallArgumentOrder, CheckedGenericCall, - CheckedGenericFunctionInstantiation, CheckedMethodCall, CheckedMethodDefinition, - CheckedStructConstruction, check, check_with_options, + CheckOptions, CheckResult, CheckedBindingMode, CheckedBodyFacts, CheckedCallArgumentOrder, + CheckedComptimeTypeBinding, CheckedComptimeTypeSelection, CheckedGenericCall, + CheckedGenericFunctionInstantiation, CheckedGenericSpecialization, CheckedInterfaceCall, + CheckedMethodCall, CheckedMethodDefinition, CheckedMethodValue, CheckedStaticSelection, + CheckedStructConstruction, CheckedViewSource, check, check_with_options, + check_with_resource_kernels, +}; + +pub use resource_hooks::{CheckedResourceHook, ResourceHookError, validate_resource_hooks}; + +#[cfg(test)] +mod resource_hook_tests; + +pub use resource_program::{CheckedResourceProgram, ResourceProgramError}; + +pub use caller_ownership::{ + CheckedArgumentOwnership, CheckedBindingFact, CheckedCallOwnership, CheckedCalleeAccess, + CheckedCallerEffect, CheckedCallerOrigin, CheckedCallerSyntax, CheckedIntrinsicOperandRole, + CheckedInvocationShape, CheckedInvocationTarget, CheckedOwnershipContext, + intrinsic_operand_access, }; diff --git a/crates/jett_typecheck/src/ownership.rs b/crates/jett_typecheck/src/ownership.rs index 9609f260..a94ad596 100644 --- a/crates/jett_typecheck/src/ownership.rs +++ b/crates/jett_typecheck/src/ownership.rs @@ -1,9 +1,22 @@ use std::collections::{HashMap, HashSet}; -use jett_common::{Span, is_json_implicit_view_facade}; +use crate::assignment_places::{AssignmentContext, AssignmentContextRoot, AssignmentSite}; +use crate::{CheckedAssignmentPlace, CheckedViewSource}; +mod actor_handlers; +mod field_updates; +use crate::{CheckedActorHandler, CheckedActorHandlerTarget, CheckedActorMessageCall}; +use actor_handlers::{ActorFlow, ActorHandlerReport}; + +use crate::{ + CheckedBindingFact, CheckedBindingMode, CheckedBodyFacts, CheckedCallOwnership, + CheckedCallerEffect, CheckedComptimeTypeBinding, CheckedGenericFunctionInstantiation, + CheckedStaticSelection, +}; +use jett_common::Span; use jett_diagnostics::Diagnostic; use jett_parser::ast::{self, Block, CallArg, Expr, FunctionDef, Item, Module, Stmt, StringPart}; -use jett_types::{Type, TypeId, TypeInterner}; +use jett_resolve::{resolver::ResolveResult, scope::DefId}; +use jett_types::{CapabilityKind, Type, TypeId, TypeInterner}; // --------------------------------------------------------------------------- // Ownership state @@ -29,15 +42,19 @@ pub enum OwnershipState { // --------------------------------------------------------------------------- /// Information about a tracked variable. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] struct VarInfo { state: OwnershipState, /// Whether the variable was declared `mutable`. mutable: bool, /// The type of the variable (used to determine whether it is implicitly copyable). type_id: TypeId, + /// Exact machine-state values are owned even when widened to a bare machine. + state_qualified_machine: bool, /// The span where the variable was consumed (for "previously consumed here" labels). consumed_span: Option, + debug_consumed: bool, + borrowed_from: Option, } // --------------------------------------------------------------------------- @@ -55,7 +72,7 @@ fn use_after_move(name: &str, use_span: Span, consumed_span: Span) -> Diagnostic } /// E0401: Cannot consume a view parameter — view parameters are read-only. -fn cannot_consume_view(name: &str, span: Span) -> Diagnostic { +pub(crate) fn cannot_consume_view(name: &str, span: Span) -> Diagnostic { Diagnostic::error( 401, format!("cannot consume `{name}` because it is a view parameter"), @@ -63,6 +80,51 @@ fn cannot_consume_view(name: &str, span: Span) -> Diagnostic { ) } +/// E0401: An owned result cannot retain a borrow from this call. +pub(crate) fn cannot_return_view(span: Span) -> Diagnostic { + Diagnostic::error( + 401, + "cannot return a view; clone the value or take ownership instead", + span, + ) +} + +/// E0401: Function result annotations cannot declare escaping borrows. +pub(crate) fn cannot_declare_view_return(span: Span) -> Diagnostic { + Diagnostic::error( + 401, + "function return type cannot be a view; return an owned value instead", + span, + ) +} + +/// E0401: An owned parameter cannot receive a known borrowed value. +pub(crate) fn cannot_pass_view_to_owned_parameter(span: Span) -> Diagnostic { + Diagnostic::error( + 401, + "cannot pass a view to an owned parameter; clone the value or take ownership instead", + span, + ) +} + +/// E0401: Views are read-only, including when the binding is mutable. +pub(crate) fn cannot_assign_through_view(span: Span) -> Diagnostic { + Diagnostic::error( + 401, + "cannot assign through a view because views are read-only", + span, + ) +} + +/// E0401: Rebinding an owner requires ownership of the replacement value. +pub(crate) fn cannot_rebind_from_view(span: Span) -> Diagnostic { + Diagnostic::error( + 401, + "cannot rebind an owned value from a view; clone the value instead", + span, + ) +} + /// E0403: `cancel` requires a pending variable produced by `run`. fn task_control_requires_pending(operation: &str, span: Span) -> Diagnostic { Diagnostic::error( @@ -72,6 +134,24 @@ fn task_control_requires_pending(operation: &str, span: Span) -> Diagnostic { ) } +/// E0404: Rebinding requires an explicitly mutable local. +pub(crate) fn cannot_rebind_immutable(name: &str, span: Span) -> Diagnostic { + Diagnostic::error( + 404, + format!("cannot reassign `{name}` because it is not mutable"), + span, + ) +} + +/// E0404: A temporary value cannot provide an owned mutable assignment root. +pub(crate) fn cannot_assign_to_temporary(span: Span) -> Diagnostic { + Diagnostic::error( + 404, + "projected assignment requires an owned mutable binding", + span, + ) +} + /// E0402: Closures may capture only implicitly copyable values. pub(crate) fn cannot_capture_move_only(name: &str, span: Span) -> Diagnostic { Diagnostic::error( @@ -81,7 +161,7 @@ pub(crate) fn cannot_capture_move_only(name: &str, span: Span) -> Diagnostic { ) } -pub(crate) fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) -> bool { +pub fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) -> bool { matches!( interner.resolve(type_id), Type::Int8 @@ -97,6 +177,7 @@ pub(crate) fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) - | Type::String | Type::Bool | Type::Nothing + | Type::Never | Type::Error ) } @@ -105,6 +186,50 @@ pub(crate) fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) - // Ownership checker // --------------------------------------------------------------------------- +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum BindingKey { + Definition(DefId), + // Used only by the standalone AST test helper; production never joins names. + Legacy(String), + Unresolved(Span), +} + +#[derive(Clone, Copy)] +struct OwnershipBodyFacts<'a> { + binding_facts: &'a HashMap, + assignment_places: &'a HashMap, + call_ownership: &'a HashMap, + actor_message_calls: &'a HashMap, + type_map: &'a HashMap, + static_selections: Option<&'a HashMap>, + comptime_type_bindings: &'a HashMap>, +} + +impl<'a> OwnershipBodyFacts<'a> { + fn scoped(body: &'a CheckedBodyFacts) -> Self { + Self { + binding_facts: &body.binding_facts, + assignment_places: &body.assignment_places, + call_ownership: &body.call_ownership, + actor_message_calls: &body.actor_message_calls, + type_map: &body.type_map, + static_selections: Some(&body.static_selections), + comptime_type_bindings: &body.comptime_type_bindings, + } + } + fn generic(body: &'a CheckedGenericFunctionInstantiation) -> Self { + Self { + binding_facts: &body.binding_facts, + assignment_places: &body.assignment_places, + call_ownership: &body.call_ownership, + actor_message_calls: &body.actor_message_calls, + type_map: &body.type_map, + static_selections: Some(&body.static_selections), + comptime_type_bindings: &body.comptime_type_bindings, + } + } +} + /// Performs ownership analysis (linear type checking) on a type-checked module. /// /// This is Phase C from the architecture: tracking move/view/consume states for @@ -112,13 +237,23 @@ pub(crate) fn is_implicitly_copyable(interner: &TypeInterner, type_id: TypeId) - /// to the diagnostic list. pub struct OwnershipChecker<'a> { /// Maps variable names to their ownership info within the current scope. - states: HashMap, + states: HashMap, /// Collected diagnostics. diagnostics: Vec, + breakpoint_exclusions: HashMap>, + debug_copyable: HashMap, /// The type interner, needed to check whether a type is implicitly copyable. interner: &'a TypeInterner, - /// Source-defined functions whose first parameter is declared as a view. - source_first_argument_views: HashSet, + resolve: Option<&'a ResolveResult>, + body_facts: Option>, + generic_bodies: &'a [CheckedGenericFunctionInstantiation], + actor_handlers: Option<&'a HashMap>, + actor_reports: HashMap, + actor_flow: Option, + assignment_context: AssignmentContext, + validated_field_updates: HashSet, + field_update_reservations: Vec, + field_update_reservation_errors: HashSet<(BindingKey, Span)>, } impl<'a> OwnershipChecker<'a> { @@ -126,17 +261,235 @@ impl<'a> OwnershipChecker<'a> { Self { states: HashMap::new(), diagnostics: Vec::new(), + breakpoint_exclusions: HashMap::new(), + debug_copyable: HashMap::new(), interner, - source_first_argument_views: HashSet::new(), + resolve: None, + body_facts: None, + generic_bodies: &[], + actor_handlers: None, + actor_reports: HashMap::new(), + actor_flow: None, + assignment_context: AssignmentContext::default(), + validated_field_updates: HashSet::new(), + field_update_reservations: Vec::new(), + field_update_reservation_errors: HashSet::new(), + } + } + + pub(crate) fn with_checked_facts( + mut self, + resolve: &'a ResolveResult, + binding_facts: &'a HashMap, + assignment_places: &'a HashMap, + call_ownership: &'a HashMap, + actor_message_calls: &'a HashMap, + type_map: &'a HashMap, + comptime_type_bindings: &'a HashMap>, + generic_bodies: &'a [CheckedGenericFunctionInstantiation], + actor_handlers: &'a HashMap, + ) -> Self { + self.resolve = Some(resolve); + self.body_facts = Some(OwnershipBodyFacts { + binding_facts, + assignment_places, + call_ownership, + actor_message_calls, + type_map, + static_selections: None, + comptime_type_bindings, + }); + self.generic_bodies = generic_bodies; + self.actor_handlers = Some(actor_handlers); + self + } + + fn binding_key(&self, name: &str, span: Span) -> BindingKey { + match self.resolve { + Some(resolve) => resolve + .resolutions + .get(&span) + .copied() + .or_else(|| { + resolve + .scope_table + .definitions + .iter() + .find(|definition| definition.span == span) + .map(|definition| definition.id) + }) + .map(BindingKey::Definition) + .unwrap_or(BindingKey::Unresolved(span)), + None => BindingKey::Legacy(name.to_string()), + } + } + + fn binding_label(&self, key: &BindingKey) -> Option { + match key { + BindingKey::Definition(definition) => self + .resolve + .map(|resolve| resolve.scope_table.def(*definition).name.clone()), + BindingKey::Legacy(name) => Some(name.clone()), + BindingKey::Unresolved(_) => None, + } + } + + fn binding_fact(&self, name: &ast::Ident) -> Option { + self.body_facts + .and_then(|facts| facts.binding_facts.get(&name.span)) + .copied() + } + + fn binding_type(&self, name: &ast::Ident, syntax: &ast::TypeExpr) -> TypeId { + self.binding_fact(name) + .map(|fact| fact.ty) + .unwrap_or_else(|| { + if self.resolve.is_some() { + TypeInterner::ERROR + } else { + self.resolve_type_for_ownership(syntax) + } + }) + } + + fn binding_state(&self, name: &ast::Ident) -> Option { + self.binding_fact(name).map(|fact| match fact.mode { + CheckedBindingMode::Owned => OwnershipState::Owned, + CheckedBindingMode::View { .. } => OwnershipState::Viewed, + }) + } + + fn register_checked_binding(&mut self, name: &ast::Ident) { + if let Some(fact) = self.binding_fact(name) { + self.states.insert( + BindingKey::Definition(fact.definition), + VarInfo { + state: self.binding_state(name).unwrap_or(OwnershipState::Owned), + mutable: fact.mutable, + type_id: fact.ty, + state_qualified_machine: matches!( + self.interner.resolve(fact.ty), + Type::MachineState { .. } + ), + consumed_span: None, + debug_consumed: false, + borrowed_from: self.binding_borrow_source(name), + }, + ); + } + } + + fn check_argument_effect( + &mut self, + expression: &Expr, + span: Span, + effect: CheckedCallerEffect, + ) { + match effect { + CheckedCallerEffect::Copy + | CheckedCallerEffect::RetainBorrow + | CheckedCallerEffect::ObserveData => self.check_expr_ownership(expression), + CheckedCallerEffect::TransferOwned | CheckedCallerEffect::RelinquishOwned => { + self.consume_expr(expression, span) + } + } + } + + fn check_scoped_type_body(&mut self, binding: &ast::ComptimeTypeBindStmt) { + if self.actor_flow.is_some() { + self.check_actor_scoped_type_body(binding); + return; + } + let Some(bodies) = self + .body_facts + .and_then(|facts| facts.comptime_type_bindings.get(&binding.span)) + else { + // Failed checking has no concrete handoff. Do not guess scoped types + // or walk a body the checker intentionally did not select. + if self.resolve.is_none() { + self.check_block(&binding.body); + } + return; + }; + let baseline = self.states.clone(); + let mut outcomes = Vec::new(); + for (index, body) in bodies.iter().enumerate() { + let saved_context = self.assignment_context.clone(); + self.assignment_context = saved_context.scoped(binding.span, index); + let saved_facts = self + .body_facts + .replace(OwnershipBodyFacts::scoped(&body.body)); + self.states = baseline.clone(); + self.check_block(&binding.body); + if Self::block_can_fall_through(&binding.body) { + outcomes.push(self.states.clone()); + } + self.body_facts = saved_facts; + self.assignment_context = saved_context; + } + self.states = self.merge_fallthrough_states(&baseline, &outcomes); + } + + pub fn with_debug_types(mut self, types: impl IntoIterator) -> Self { + for (span, ty) in types { + let copyable = self.is_copyable(ty); + self.debug_copyable + .entry(span) + .and_modify(|all| *all &= copyable) + .or_insert(copyable); } + self } /// Run ownership analysis on a module and return collected diagnostics. - pub fn check_module(mut self, module: &Module) -> Vec { - self.collect_source_first_argument_views(module); + pub fn check_module(self, module: &Module) -> Vec { + self.check_module_with_debug(module).0 + } + + pub fn check_module_with_debug( + self, + module: &Module, + ) -> (Vec, HashMap>) { + let (diagnostics, exclusions, _, _) = self.check_module_with_field_updates(module); + (diagnostics, exclusions) + } + + pub(crate) fn check_module_with_field_updates( + mut self, + module: &Module, + ) -> ( + Vec, + HashMap>, + HashSet, + HashMap, + ) { for item in &module.items { match item { - Item::Function(func) => self.check_function(func), + Item::Function(func) => { + if self.resolve.is_none() || func.type_params.is_empty() { + self.check_function(func); + } else { + let bodies = self.generic_bodies; + for (index, body) in bodies.iter().enumerate() { + if self.resolve.is_some_and(|resolve| { + resolve.scope_table.def(body.definition).span == func.name.span + }) { + let saved = + self.body_facts.replace(OwnershipBodyFacts::generic(body)); + let context = std::mem::replace( + &mut self.assignment_context, + AssignmentContext { + root: AssignmentContextRoot::Generic(index), + scopes: Vec::new(), + }, + ); + self.check_function(func); + self.assignment_context = context; + self.body_facts = saved; + } + } + } + } Item::Implement(block) => { for method in &block.methods { self.check_function(method); @@ -147,54 +500,89 @@ impl<'a> OwnershipChecker<'a> { self.check_function(method); } } + Item::Actor(actor) => { + if self.resolve.is_some() { + self.check_actor_handlers(actor); + } else { + self.observe_actor_breakpoints(actor); + } + } Item::VarDecl(decl) => self.check_var_decl(decl), + Item::Verify(verify) if self.resolve.is_some() => { + let saved = self.states.clone(); + self.check_block(&verify.body); + self.states = saved; + } + Item::Property(property) if self.resolve.is_some() => { + let saved = self.states.clone(); + for given in &property.givens { + self.register_checked_binding(&given.name); + } + self.check_block(&property.body); + self.states = saved; + } _ => {} } } - self.diagnostics + ( + self.diagnostics, + self.breakpoint_exclusions, + self.validated_field_updates, + self.actor_reports, + ) } - fn collect_source_first_argument_views(&mut self, module: &Module) { - let mut current_file = None; - let mut current_namespace: Option<&str> = None; - for item in &module.items { - let item_file = match item { - Item::Namespace(value) => value.span.file, - Item::Function(value) => value.span.file, - Item::Mutual(value) => value.span.file, - Item::Interface(value) => value.span.file, - Item::Implement(value) => value.span.file, - Item::Struct(value) => value.span.file, - Item::Bitfield(value) => value.span.file, - Item::Enum(value) => value.span.file, - Item::Machine(value) => value.span.file, - Item::Actor(value) => value.span.file, - Item::VarDecl(value) => value.span.file, - Item::Verify(value) => value.span.file, - Item::Property(value) => value.span.file, - Item::Resource(value) => value.span.file, - Item::TypeAlias(value) => value.span.file, - }; - if current_file.is_some_and(|file| file != item_file) { - current_namespace = None; + fn observe_actor_breakpoints(&mut self, actor: &ast::ActorDef) { + // Actor validity is checked separately. Reuse this flow solely for + // observations, with state and message bindings local to each handler. + let saved = std::mem::take(&mut self.states); + let diagnostics_len = self.diagnostics.len(); + let validated = self.validated_field_updates.clone(); + for handler in &actor.handlers { + self.states.clear(); + for field in &actor.state_fields { + self.states.insert( + self.binding_key(&field.name.name, field.name.span), + VarInfo { + state: OwnershipState::Owned, + mutable: field.mutable, + type_id: self.binding_type(&field.name, &field.ty), + state_qualified_machine: matches!( + field.ty, + ast::TypeExpr::StateQualified(_, _, _) + ), + consumed_span: None, + debug_consumed: false, + borrowed_from: None, + }, + ); } - current_file = Some(item_file); - - match item { - Item::Namespace(namespace) => { - current_namespace = Some(&namespace.name.name); - } - Item::Function(function) - if function.params.first().is_some_and(|param| param.view) => - { - let name = current_namespace - .map(|namespace| format!("{namespace}.{}", function.name.name)) - .unwrap_or_else(|| function.name.name.clone()); - self.source_first_argument_views.insert(name); - } - _ => {} + for param in actor.capability_params.iter().chain(&handler.params) { + self.states.insert( + self.binding_key(¶m.name.name, param.name.span), + VarInfo { + state: self.binding_state(¶m.name).unwrap_or(if param.view { + OwnershipState::Viewed + } else { + OwnershipState::Owned + }), + mutable: param.mutable, + type_id: self.binding_type(¶m.name, ¶m.ty), + state_qualified_machine: matches!( + param.ty, + ast::TypeExpr::StateQualified(_, _, _) + ), + consumed_span: None, + debug_consumed: false, + borrowed_from: None, + }, + ); } + self.check_block(&handler.body); } + self.diagnostics.truncate(diagnostics_len); + self.validated_field_updates = validated; + self.states = saved; } // ------------------------------------------------------------------ @@ -217,19 +605,25 @@ impl<'a> OwnershipChecker<'a> { // Register parameters. for param in &func.params { - let type_id = self.resolve_type_for_ownership(¶m.ty); + let type_id = self.binding_type(¶m.name, ¶m.ty); let state = if param.view { OwnershipState::Viewed } else { OwnershipState::Owned }; self.states.insert( - param.name.name.clone(), + self.binding_key(¶m.name.name, param.name.span), VarInfo { - state, + state: self.binding_state(¶m.name).unwrap_or(state), mutable: param.mutable, type_id, + state_qualified_machine: matches!( + param.ty, + ast::TypeExpr::StateQualified(_, _, _) + ), consumed_span: None, + debug_consumed: false, + borrowed_from: None, }, ); } @@ -246,6 +640,9 @@ impl<'a> OwnershipChecker<'a> { fn check_block(&mut self, block: &Block) { for stmt in &block.stmts { + if !self.actor_reachable() { + break; + } self.check_stmt(stmt); if Self::stmt_definitely_exits(stmt) { break; @@ -262,7 +659,7 @@ impl<'a> OwnershipChecker<'a> { Stmt::VarDecl(decl) => self.check_var_decl(decl), Stmt::Assign(assign) => self.check_assign(assign), Stmt::Return(ret) => self.check_return(ret), - Stmt::ComptimeTypeBind(bind) => self.check_block(&bind.body), + Stmt::ComptimeTypeBind(bind) => self.check_scoped_type_body(bind), Stmt::If(if_stmt) => self.check_if(if_stmt), Stmt::For(for_stmt) => self.check_for(for_stmt), Stmt::While(while_stmt) => self.check_while(while_stmt), @@ -274,9 +671,15 @@ impl<'a> OwnershipChecker<'a> { Stmt::Breakpoint(breakpoint_stmt) => self.check_breakpoint(breakpoint_stmt), Stmt::Match(match_stmt) => self.check_match(match_stmt), Stmt::Respond(resp) => { - self.check_expr_ownership(&resp.value); + if self.actor_flow.is_some() { + self.check_actor_respond(resp); + } else { + self.check_expr_ownership(&resp.value); + } } - Stmt::Use(_) | Stmt::Break(_) | Stmt::Continue(_) => {} + Stmt::Break(span) => self.check_actor_loop_jump(*span, false), + Stmt::Continue(span) => self.check_actor_loop_jump(*span, true), + Stmt::Use(_) => {} } } @@ -289,37 +692,136 @@ impl<'a> OwnershipChecker<'a> { if let Some(condition) = &breakpoint_stmt.condition { self.check_expr_ownership(condition); } + self.breakpoint_exclusions.insert( + breakpoint_stmt.span, + self.states + .iter() + .filter(|(_, info)| { + info.debug_consumed + || matches!( + info.state, + OwnershipState::Consumed | OwnershipState::Uninitialized + ) + }) + .filter_map(|(key, _)| self.binding_label(key)) + .collect(), + ); + } + + // Preserve the existing source-validation policy while recording moves + // that native value lowering performs at binding and destructuring sites. + fn record_debug_move(&mut self, expression: &Expr) { + match expression { + Expr::Ident(ident) => { + let info = self.states.get(&self.binding_key(&ident.name, ident.span)); + let copyable = self + .body_facts + .and_then(|facts| facts.type_map.get(&ident.span)) + .map(|&ty| self.is_copyable(ty)) + .or_else(|| self.debug_copyable.get(&ident.span).copied()) + .unwrap_or_else(|| info.is_none_or(|info| self.is_copyable(info.type_id))); + if !copyable && info.is_none_or(|info| info.state != OwnershipState::Viewed) { + let key = self.binding_key(&ident.name, ident.span); + if self.reject_actor_loan_change(&key, ident.span) { + return; + } + if self.reject_reserved_field_update_change(&key, ident.span) { + return; + } + self.states + .entry(self.binding_key(&ident.name, ident.span)) + .or_insert(VarInfo { + state: OwnershipState::Owned, + mutable: false, + type_id: TypeInterner::ERROR, + state_qualified_machine: false, + consumed_span: None, + debug_consumed: false, + borrowed_from: None, + }) + .debug_consumed = true; + } + } + Expr::Paren(inner, _) => self.record_debug_move(inner), + _ => {} + } } fn check_var_decl(&mut self, decl: &ast::VarDecl) { let initial_state = self.initial_task_state(&decl.value); - // Check the initializer expression for ownership violations. - self.check_expr_ownership(&decl.value); + // Erasing an exact machine state into another owned local transfers + // its value. Reusing the source requires an explicit clone. + let destination_is_view = self.binding_state(&decl.name) == Some(OwnershipState::Viewed); + if !destination_is_view + && let Expr::Ident(source) = &decl.value + && self + .states + .get(&self.binding_key(&source.name, source.span)) + .is_some_and(|info| info.state_qualified_machine) + { + self.consume_expr(&decl.value, decl.value.span()); + } else { + self.check_expr_ownership(&decl.value); + } + if !self.actor_reachable() { + return; + } + self.record_debug_move(&decl.value); - let type_id = self.resolve_type_for_ownership(&decl.ty); + let type_id = self.binding_type(&decl.name, &decl.ty); self.states.insert( - decl.name.name.clone(), + self.binding_key(&decl.name.name, decl.name.span), VarInfo { - state: initial_state, + state: if self.binding_state(&decl.name) == Some(OwnershipState::Viewed) { + OwnershipState::Viewed + } else { + initial_state + }, mutable: decl.mutable, type_id, + state_qualified_machine: matches!(decl.ty, ast::TypeExpr::StateQualified(_, _, _)), consumed_span: None, + debug_consumed: false, + borrowed_from: self.binding_borrow_source(&decl.name), }, ); } fn check_assign(&mut self, assign: &ast::AssignStmt) { + if let Some(place) = self + .body_facts + .and_then(|facts| facts.assignment_places.get(&assign.span)) + .cloned() + { + self.check_field_update_assignment(assign, &place); + return; + } + if let Some(ident) = Self::field_assignment_root(&assign.target) { + let key = self.binding_key(&ident.name, ident.span); + self.reject_reserved_field_update_change(&key, ident.span); + self.reject_actor_loan_change(&key, ident.span); + } let initial_state = self.initial_task_state(&assign.value); - // Check the value expression. self.check_expr_ownership(&assign.value); + if !self.actor_reachable() { + return; + } + self.record_debug_move(&assign.value); // If the target is an identifier, check if it's mutable and handle rebinding. if let Expr::Ident(ident) = &assign.target { - if let Some(info) = self.states.get_mut(&ident.name) { + if let Some(info) = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + { if info.mutable { // Rebinding may start a fresh pending task. info.state = initial_state; info.consumed_span = None; + info.debug_consumed = false; + } else { + self.diagnostics + .push(cannot_rebind_immutable(&ident.name, ident.span)); } } } else { @@ -328,8 +830,20 @@ impl<'a> OwnershipChecker<'a> { } fn check_return(&mut self, ret: &ast::ReturnStmt) { + if self.actor_flow.is_some() { + self.check_actor_return(ret); + return; + } if let Some(expr) = &ret.value { - self.check_expr_ownership(expr); + if self.field_update_reservations.is_empty() { + self.check_expr_ownership(expr); + } else { + // A terminal return abandons every enclosing field commit. + // Existing handle flow excludes this state from fallthrough. + let reservations = std::mem::take(&mut self.field_update_reservations); + self.consume_expr(expr, expr.span()); + self.field_update_reservations = reservations; + } } } @@ -340,7 +854,7 @@ impl<'a> OwnershipChecker<'a> { Expr::Ident(ident) if self .states - .get(&ident.name) + .get(&self.binding_key(&ident.name, ident.span)) .is_some_and(|info| info.state == OwnershipState::Pending) => { OwnershipState::Pending @@ -350,7 +864,35 @@ impl<'a> OwnershipChecker<'a> { } fn check_if(&mut self, if_stmt: &ast::IfStmt) { + if self.actor_flow.is_some() { + self.check_actor_if(if_stmt); + return; + } self.check_expr_ownership(&if_stmt.condition); + if let Some(selection) = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&if_stmt.span)) + .copied() + { + match selection { + CheckedStaticSelection::IfThen => self.check_block(&if_stmt.then_block), + CheckedStaticSelection::IfElseIf(index) => { + if let Some((condition, body)) = if_stmt.else_ifs.get(index) { + self.check_expr_ownership(condition); + self.check_block(body); + } + } + CheckedStaticSelection::IfElse => { + if let Some(body) = &if_stmt.else_block { + self.check_block(body); + } + } + CheckedStaticSelection::IfNoBranch => {} + CheckedStaticSelection::MatchArm(_) => {} + } + return; + } let mut condition_state = self.states.clone(); let mut fallthrough_states = Vec::new(); @@ -383,9 +925,9 @@ impl<'a> OwnershipChecker<'a> { fn check_block_from_state( &mut self, - state: &HashMap, + state: &HashMap, block: &Block, - ) -> HashMap { + ) -> HashMap { self.states = state.clone(); self.check_block(block); self.states.clone() @@ -393,9 +935,9 @@ impl<'a> OwnershipChecker<'a> { fn merge_fallthrough_states( &self, - baseline: &HashMap, - branches: &[HashMap], - ) -> HashMap { + baseline: &HashMap, + branches: &[HashMap], + ) -> HashMap { if branches.is_empty() { return baseline.clone(); } @@ -434,6 +976,9 @@ impl<'a> OwnershipChecker<'a> { if branches.iter().any(|branch| !branch.contains_key(name)) { result = baseline_info.clone(); } + result.debug_consumed |= branches + .iter() + .any(|branch| branch.get(name).is_some_and(|info| info.debug_consumed)); merged.insert(name.clone(), result); } merged @@ -465,6 +1010,10 @@ impl<'a> OwnershipChecker<'a> { } fn check_for(&mut self, for_stmt: &ast::ForStmt) { + if self.actor_flow.is_some() { + self.check_actor_for(for_stmt); + return; + } // Check the iterable expression. // If `view` is used, the iterable stays owned; otherwise it is consumed. if for_stmt.view { @@ -482,7 +1031,7 @@ impl<'a> OwnershipChecker<'a> { // We don't know the exact type here without the type map, so we use ERROR // as a placeholder — it will be treated as copyable for ownership purposes. self.states.insert( - for_stmt.variable.name.clone(), + self.binding_key(&for_stmt.variable.name, for_stmt.variable.span), VarInfo { state: if for_stmt.view { OwnershipState::Viewed @@ -490,17 +1039,35 @@ impl<'a> OwnershipChecker<'a> { OwnershipState::Owned }, mutable: false, - type_id: TypeInterner::ERROR, // Element type not tracked here + type_id: self + .binding_fact(&for_stmt.variable) + .map(|fact| fact.ty) + .unwrap_or(TypeInterner::ERROR), + state_qualified_machine: false, consumed_span: None, + debug_consumed: false, + borrowed_from: if for_stmt.view { + self.field_loan_root(&for_stmt.iterable) + } else { + self.binding_borrow_source(&for_stmt.variable) + }, }, ); + if let Some(name) = &for_stmt.value_variable { + self.register_checked_binding(name); + } self.check_block(&for_stmt.body); let mut body_state = self.states.clone(); - if let Some(outer) = entry_state.get(&for_stmt.variable.name) { - body_state.insert(for_stmt.variable.name.clone(), outer.clone()); + if let Some(outer) = + entry_state.get(&self.binding_key(&for_stmt.variable.name, for_stmt.variable.span)) + { + body_state.insert( + self.binding_key(&for_stmt.variable.name, for_stmt.variable.span), + outer.clone(), + ); } else { - body_state.remove(&for_stmt.variable.name); + body_state.remove(&self.binding_key(&for_stmt.variable.name, for_stmt.variable.span)); } let mut fallthrough_states = vec![entry_state.clone()]; @@ -511,6 +1078,10 @@ impl<'a> OwnershipChecker<'a> { } fn check_while(&mut self, while_stmt: &ast::WhileStmt) { + if self.actor_flow.is_some() { + self.check_actor_while(while_stmt); + return; + } self.check_expr_ownership(&while_stmt.condition); let entry_state = self.states.clone(); let body_state = self.check_block_from_state(&entry_state, &while_stmt.body); @@ -529,17 +1100,47 @@ impl<'a> OwnershipChecker<'a> { } fn check_match(&mut self, match_stmt: &ast::MatchStmt) { + if self.actor_flow.is_some() { + self.check_actor_match(match_stmt); + return; + } self.check_expr_ownership(&match_stmt.expr); + self.record_debug_move(&match_stmt.expr); let baseline = self.states.clone(); let mut fallthrough_states = Vec::new(); - - for arm in &match_stmt.arms { - let arm_state = self.check_block_from_state(&baseline, &arm.body); + let loan_root = self.field_loan_root(&match_stmt.expr); + + let selected = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&match_stmt.span)) + .and_then(|choice| match choice { + CheckedStaticSelection::MatchArm(index) => Some(*index), + _ => None, + }); + for (index, arm) in match_stmt.arms.iter().enumerate() { + if selected.is_some_and(|selected| selected != index) { + continue; + } + self.states = baseline.clone(); + if let ast::Pattern::Variant(_, bindings) = &arm.pattern { + for binding in bindings { + self.register_checked_binding(binding); + let key = self.binding_key(&binding.name, binding.span); + if let Some(info) = self.states.get_mut(&key) + && info.state == OwnershipState::Viewed + && info.borrowed_from.is_none() + { + info.borrowed_from = loan_root.clone(); + } + } + } + self.check_block(&arm.body); + let arm_state = self.states.clone(); if Self::block_can_fall_through(&arm.body) { fallthrough_states.push(arm_state); } } - self.states = self.merge_fallthrough_states(&baseline, &fallthrough_states); } @@ -551,18 +1152,37 @@ impl<'a> OwnershipChecker<'a> { /// traversal that verifies variable states are valid but does NOT consume /// anything (consumption happens explicitly through `consume_expr`). fn check_expr_ownership(&mut self, expr: &Expr) { + if !self.actor_reachable() { + return; + } match expr { + Expr::Binary(lhs, ast::BinOp::And | ast::BinOp::Or, rhs, _) + if self.actor_flow.is_some() => + { + self.check_expr_ownership(lhs); + if !self.actor_reachable() { + return; + } + let baseline = self.states.clone(); + self.check_expr_ownership(rhs); + let mut outcomes = vec![baseline.clone()]; + if self.actor_reachable() { + outcomes.push(self.states.clone()); + } + self.states = self.merge_fallthrough_states(&baseline, &outcomes); + self.set_actor_reachable(true); + } Expr::Ident(ident) => { // Reading a variable: check if it has been consumed. - if let Some(info) = self.states.get(&ident.name) { - if info.state == OwnershipState::Consumed { - if let Some(consumed_span) = info.consumed_span { - self.diagnostics.push(use_after_move( - &ident.name, - ident.span, - consumed_span, - )); - } + if let Some(info) = self.states.get(&self.binding_key(&ident.name, ident.span)) { + if info.state == OwnershipState::Consumed + || (self.actor_flow.is_some() && info.debug_consumed) + { + self.diagnostics.push(use_after_move( + &ident.name, + ident.span, + info.consumed_span.unwrap_or(ident.span), + )); } } } @@ -604,10 +1224,18 @@ impl<'a> OwnershipChecker<'a> { self.consume_expr(value, value.span()); } } - Expr::Handle(target, _, body, _) => { + Expr::Handle(target, error_name, body, _) => { + if self.actor_flow.is_some() { + self.check_actor_handle(target, error_name.as_ref(), body); + return; + } self.check_expr_ownership(target); + self.record_debug_move(target); let success_state = self.states.clone(); - let handler_state = self.check_block_from_state(&success_state, body); + if let Some(name) = error_name { + self.register_checked_binding(name); + } + let handler_state = self.check_block_from_state(&self.states.clone(), body); let mut fallthrough_states = vec![success_state.clone()]; if Self::block_can_fall_through(body) { fallthrough_states.push(handler_state); @@ -615,7 +1243,7 @@ impl<'a> OwnershipChecker<'a> { self.states = self.merge_fallthrough_states(&success_state, &fallthrough_states); } Expr::Ok(inner, _) | Expr::Fail(inner, _) | Expr::Some(inner, _) => { - self.check_expr_ownership(inner); + self.consume_expr(inner, inner.span()); } Expr::Default(inner, _) => { self.check_expr_ownership(inner); @@ -634,25 +1262,54 @@ impl<'a> OwnershipChecker<'a> { self.check_expr_ownership(inner); } Expr::Pipeline(initial, steps, _) => { - if let Some(first_step) = steps.first() { - let (callee, _, piped_as_view) = Self::pipeline_step_call_parts(first_step); - if piped_as_view || self.first_argument_is_implicit_view(callee) { - self.check_expr_ownership(initial); - } else { - self.consume_expr(initial, initial.span()); - } - } else { + if steps.is_empty() { self.check_expr_ownership(initial); } - - for step in steps { - let (callee, extra_args, _) = Self::pipeline_step_call_parts(step); + for (step_index, step) in steps.iter().enumerate() { + let (callee, extra_args, piped_as_view) = Self::pipeline_step_call_parts(step); + if let Some(packet) = self + .body_facts + .and_then(|facts| facts.call_ownership.get(&step.span)) + { + for argument in &packet.arguments { + if argument.source_index == 0 { + if step_index == 0 { + self.check_argument_effect( + initial, + initial.span(), + argument.effect, + ); + } + } else if let Some(source) = extra_args.get(argument.source_index - 1) { + self.check_argument_effect( + &source.value, + source.span, + argument.effect, + ); + } + } + } else { + if step_index == 0 { + if piped_as_view { + self.check_expr_ownership(initial); + } else { + self.consume_expr(initial, initial.span()); + } + } + self.check_call_arguments_ownership(callee, extra_args, 1); + } self.check_expr_ownership(callee); - self.check_call_arguments_ownership(callee, extra_args, 1); if let Some(handle) = &step.handle { + if self.actor_flow.is_some() { + self.check_actor_handle_body(handle.error_name.as_ref(), &handle.body); + continue; + } let success_state = self.states.clone(); + if let Some(name) = &handle.error_name { + self.register_checked_binding(name); + } let handler_state = - self.check_block_from_state(&success_state, &handle.body); + self.check_block_from_state(&self.states.clone(), &handle.body); let mut fallthrough_states = vec![success_state.clone()]; if Self::block_can_fall_through(&handle.body) { fallthrough_states.push(handler_state); @@ -665,37 +1322,44 @@ impl<'a> OwnershipChecker<'a> { Expr::At(inner, _, _) => { self.check_expr_ownership(inner); } - Expr::Spawn(inner, _) - | Expr::Send(inner, _) - | Expr::Ask(inner, _) - | Expr::Clone(inner, _) - | Expr::Run(inner, _) => { + Expr::Send(inner, span) | Expr::Ask(inner, span) => { + self.check_actor_message_ownership(inner, *span); + } + Expr::Spawn(inner, _) | Expr::Clone(inner, _) | Expr::Run(inner, _) => { self.check_expr_ownership(inner); } Expr::Join(inner, span) => self.check_task_control("join", inner, *span, true), Expr::Cancel(inner, span) => self.check_task_control("cancel", inner, *span, false), Expr::InlineFn(params, _, body, _) => { + let actor_flow = self.actor_flow.take(); let saved = std::mem::take(&mut self.states); for param in params { - let type_id = self.resolve_type_for_ownership(¶m.ty); + let type_id = self.binding_type(¶m.name, ¶m.ty); self.states.insert( - param.name.name.clone(), + self.binding_key(¶m.name.name, param.name.span), VarInfo { - state: if param.view { + state: self.binding_state(¶m.name).unwrap_or(if param.view { OwnershipState::Viewed } else { OwnershipState::Owned - }, + }), mutable: param.mutable, type_id, + state_qualified_machine: matches!( + param.ty, + ast::TypeExpr::StateQualified(_, _, _) + ), consumed_span: None, + debug_consumed: false, + borrowed_from: None, }, ); } self.check_block(body); self.states = saved; + self.actor_flow = actor_flow; } // Literals and other leaves have no ownership effects. Expr::IntLiteral(_, _) @@ -724,11 +1388,14 @@ impl<'a> OwnershipChecker<'a> { // Only cancellation requires a still-pending task. if resolves_task { if let Expr::Ident(ident) = operand - && let Some(info) = self.states.get(&ident.name) + && let Some(info) = self.states.get(&self.binding_key(&ident.name, ident.span)) && info.state == OwnershipState::Pending { let copyable = self.is_copyable(info.type_id); - let info = self.states.get_mut(&ident.name).unwrap(); + let info = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + .unwrap(); info.state = if copyable { OwnershipState::Owned } else { @@ -746,7 +1413,10 @@ impl<'a> OwnershipChecker<'a> { .push(task_control_requires_pending(operation, span)); return; }; - let Some(info) = self.states.get_mut(&ident.name) else { + let Some(info) = self + .states + .get_mut(&self.binding_key(&ident.name, ident.span)) + else { self.diagnostics .push(task_control_requires_pending(operation, span)); return; @@ -764,7 +1434,9 @@ impl<'a> OwnershipChecker<'a> { _ => (&step.function, false), }; match function { - Expr::GenericCall(callee, _, args, _) => (callee, args, piped_as_view), + Expr::Call(callee, args, _) | Expr::GenericCall(callee, _, args, _) => { + (callee, args, piped_as_view) + } _ => (function, &step.extra_args, piped_as_view), } } @@ -774,10 +1446,36 @@ impl<'a> OwnershipChecker<'a> { /// Arguments passed with `view` (i.e., `Expr::View(inner)`) do not consume /// the inner value. Arguments passed without `view` consume the value if it /// is a non-copyable variable. - fn check_call_ownership(&mut self, callee: &Expr, args: &[CallArg], _span: Span) { - // Check the callee itself (e.g., reading a function variable). - self.check_expr_ownership(callee); + fn check_call_ownership(&mut self, callee: &Expr, args: &[CallArg], span: Span) { + if self + .body_facts + .is_some_and(|facts| facts.type_map.get(&span) == Some(&TypeInterner::ERROR)) + { + // An invalid invocation has no selected ownership handoff. Keep + // checking its evaluated dependencies, including valid nested + // calls, without consuming bare values on behalf of this call. + for argument in args { + self.check_expr_ownership(&argument.value); + } + self.check_expr_ownership(callee); + return; + } + if let Some(packet) = self + .body_facts + .and_then(|facts| facts.call_ownership.get(&span)) + { + for argument in &packet.arguments { + if let Some(source) = args.get(argument.source_index) { + self.check_argument_effect(&source.value, source.span, argument.effect); + } + } + self.check_expr_ownership(callee); + return; + } + // Source evaluates arguments before selecting a function value. An + // argument may consume an owner that the callee expression then reads. self.check_call_arguments_ownership(callee, args, 0); + self.check_expr_ownership(callee); } fn check_call_arguments_ownership( @@ -786,19 +1484,15 @@ impl<'a> OwnershipChecker<'a> { args: &[CallArg], position_offset: usize, ) { - let first_arg_is_view = self.first_argument_is_implicit_view(callee); + let _ = (callee, position_offset); for (index, arg) in args.iter().enumerate() { - let argument_position = position_offset + index; + let _ = index; match &arg.value { Expr::View(inner, _) => { // `view x` — the argument is passed as a view; no consumption. self.check_expr_ownership(inner); } - _ if first_arg_is_view && argument_position == 0 => { - // First argument to a collection builtin is implicitly viewed. - self.check_expr_ownership(&arg.value); - } _ => { // Non-view argument — consumes the value. self.consume_expr(&arg.value, arg.span); @@ -807,37 +1501,30 @@ impl<'a> OwnershipChecker<'a> { } } - fn first_argument_is_implicit_view(&self, callee: &Expr) -> bool { - // Source-owned stdlib views are collected from their declarations. - let collection_view_builtins: &[&str] = &["math.average", "math.median"]; - Self::dotted_name_str(callee) - .as_deref() - .map(|n| { - self.source_first_argument_views.contains(n) - || collection_view_builtins.contains(&n) - || is_json_implicit_view_facade(n) - }) - .unwrap_or(false) - } - - fn dotted_name_str(expr: &Expr) -> Option { - match expr { - Expr::Ident(ident) => Some(ident.name.clone()), - Expr::FieldAccess(inner, field, _) => { - let prefix = Self::dotted_name_str(inner)?; - Some(format!("{prefix}.{}", field.name)) - } - _ => None, - } - } - /// Consume an expression. If the expression is a simple identifier for a /// non-copyable variable, mark it as consumed. If it was already consumed, /// emit a use-after-move error. fn consume_expr(&mut self, expr: &Expr, span: Span) { + if !self.actor_reachable() { + return; + } + if self + .body_facts + .is_some_and(|facts| facts.type_map.get(&expr.span()) == Some(&TypeInterner::ERROR)) + { + // A failed typed expression cannot acquire its child's ownership. + // Still read its dependencies so independent use-after-move errors + // are not hidden by recovery. + self.check_expr_ownership(expr); + return; + } match expr { Expr::Ident(ident) => { self.consume_variable(&ident.name, ident.span, span); + self.record_debug_move(expr); + } + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + self.consume_expr(inner, span) } Expr::View(inner, _) => { // `view x` at the expression level — does not consume. @@ -853,11 +1540,41 @@ impl<'a> OwnershipChecker<'a> { /// Consume a named variable. Handles all the ownership state transitions /// and error reporting. fn consume_variable(&mut self, name: &str, name_span: Span, consume_span: Span) { - if let Some(info) = self.states.get(name) { + let key = self.binding_key(name, name_span); + if self.reject_actor_loan_change(&key, name_span) { + return; + } + if self + .states + .get(&key) + .is_some_and(|info| !self.is_copyable(info.type_id)) + && self.reject_reserved_field_update_change(&key, name_span) + { + return; + } + if !self.field_update_reservations.is_empty() && self.has_dependent_field_alias(&key) { + self.diagnostics.push(Diagnostic::error( + 401, + format!( + "cannot transfer `{name}` into a field while a dependent local view is retained" + ), + name_span, + )); + return; + } + if let Some(info) = self.states.get(&key) { // Implicitly copyable values are never consumed. if self.is_copyable(info.type_id) { return; } + if !self.field_update_reservations.is_empty() && info.debug_consumed { + self.diagnostics.push(use_after_move( + name, + name_span, + info.consumed_span.unwrap_or(consume_span), + )); + return; + } match info.state { OwnershipState::Consumed => { @@ -873,7 +1590,17 @@ impl<'a> OwnershipChecker<'a> { } OwnershipState::Owned => { // Consume it. - let info = self.states.get_mut(name).unwrap(); + let info = self + .states + .get_mut(&self.binding_key(name, name_span)) + .unwrap(); + info.state = OwnershipState::Consumed; + info.consumed_span = Some(consume_span); + } + OwnershipState::Pending if !self.field_update_reservations.is_empty() => { + // Storing an admitted pending owned child transfers its + // wrapper once; it does not inspect or implicitly join it. + let info = self.states.get_mut(&key).unwrap(); info.state = OwnershipState::Consumed; info.consumed_span = Some(consume_span); } @@ -909,8 +1636,11 @@ impl<'a> OwnershipChecker<'a> { "nothing" => TypeInterner::NOTHING, "TypeConstruction" => TypeInterner::TYPE_CONSTRUCTION, "TypeKind" | "TypePrimitive" => TypeInterner::INT64, - // Any other named type is a struct/enum — not copyable. - _ => TypeInterner::BYTES, // Use BYTES as a non-copyable stand-in + // Capabilities retain nominal identity; all remaining named types + // use a non-copyable stand-in in this simplified ownership pass. + name => CapabilityKind::from_name(name) + .map(TypeInterner::capability) + .unwrap_or(TypeInterner::BYTES), }, ast::TypeExpr::Generic(_, _, _) => { // Generic types (list[T], map[K,V], etc.) are handled by @@ -983,6 +1713,18 @@ mod tests { .collect() } + #[test] + fn capability_types_are_move_only() { + let interner = TypeInterner::new(); + + for kind in CapabilityKind::ALL { + assert!(!is_implicitly_copyable( + &interner, + TypeInterner::capability(kind) + )); + } + } + // --------------------------------------------------------------- // Test: Variable consumed after passing to function // --------------------------------------------------------------- diff --git a/crates/jett_typecheck/src/ownership/actor_handlers.rs b/crates/jett_typecheck/src/ownership/actor_handlers.rs new file mode 100644 index 00000000..15cdc06d --- /dev/null +++ b/crates/jett_typecheck/src/ownership/actor_handlers.rs @@ -0,0 +1,697 @@ +//! Accepting actor flow. Exact checked captures remain obligations at normal +//! handler exits; observation-only breakpoint traversal never grants a receipt. +use super::*; +use crate::{ + CheckedActorBindingRole, CheckedActorHandlerBinding, CheckedActorHandlerExit, + CheckedActorHandlerExitKind, +}; + +#[derive(Debug)] +pub(crate) struct ActorHandlerReport { + pub exits: Vec, + pub validated: bool, +} + +#[derive(Default)] +struct ActorLoopFlow { + breaks: Vec>, + continues: Vec>, +} + +pub(super) struct ActorFlow { + reachable: bool, + response_type: TypeId, + responding: bool, + persistent: Vec, + exits: Vec, + supported: bool, + loops: Vec, +} + +impl<'a> OwnershipChecker<'a> { + pub(super) fn reject_actor_loan_change(&mut self, key: &BindingKey, span: Span) -> bool { + if self.actor_flow.is_none() + || !self.has_dependent_field_alias(key) + || self + .states + .get(key) + .is_none_or(|info| self.is_copyable(info.type_id)) + { + return false; + } + self.diagnostics.push(Diagnostic::error( + 401, + "cannot move or replace an actor owner while a dependent view is retained", + span, + )); + true + } + + pub(super) fn actor_reachable(&self) -> bool { + self.actor_flow.as_ref().is_none_or(|flow| flow.reachable) + } + + pub(super) fn set_actor_reachable(&mut self, reachable: bool) { + if let Some(flow) = &mut self.actor_flow { + flow.reachable = reachable; + } + } + + fn actor_header_matches( + &self, + actor: &ast::ActorDef, + handler_index: usize, + checked: &CheckedActorHandler, + ) -> bool { + let target = checked.target(); + let Some(resolve) = self.resolve else { + return false; + }; + let Some(handler) = actor.handlers.get(handler_index) else { + return false; + }; + let canonical = self.interner.resolve_actor(target.actor); + if resolve.scope_table.def(target.actor_definition).span != actor.name.span + || target.handler_index != handler_index + || checked.body_span() != handler.body.span + || canonical.capability_params.len() != actor.capability_params.len() + || canonical.state_fields.len() != actor.state_fields.len() + || canonical.messages.len() != actor.handlers.len() + || checked.response_type() != canonical.messages[handler_index].responds + || checked.bindings().len() + != actor.capability_params.len() + actor.state_fields.len() + handler.params.len() + { + return false; + } + let mut expected = Vec::new(); + for (parameter, (param, (_, ty))) in actor + .capability_params + .iter() + .zip(&canonical.capability_params) + .enumerate() + { + expected.push(( + CheckedActorBindingRole::Capability { parameter }, + ¶m.name, + *ty, + )); + } + for (field, (decl, (_, ty))) in actor + .state_fields + .iter() + .zip(&canonical.state_fields) + .enumerate() + { + expected.push((CheckedActorBindingRole::State { field }, &decl.name, *ty)); + } + for (parameter, (param, (_, ty))) in handler + .params + .iter() + .zip(&canonical.messages[handler_index].params) + .enumerate() + { + expected.push(( + CheckedActorBindingRole::Message { parameter }, + ¶m.name, + *ty, + )); + } + checked + .bindings() + .iter() + .zip(expected) + .all(|(binding, (role, name, ty))| { + binding.role == role + && binding.fact.ty == ty + && binding.fact.ty != TypeInterner::ERROR + && binding.fact.declaration_span == name.span + && self.binding_key(&name.name, name.span) + == BindingKey::Definition(binding.fact.definition) + && checked.body().binding_facts.get(&name.span) == Some(&binding.fact) + }) + } + + pub(super) fn check_actor_handlers(&mut self, actor: &ast::ActorDef) { + let Some(handlers) = self.actor_handlers else { + return; + }; + let saved_states = std::mem::take(&mut self.states); + let saved_context = self.assignment_context.clone(); + let saved_facts = self.body_facts; + // Check actual initializer acquisition in resolver order, preserving a + // persistent view's immediate source instead of inventing owned seeds. + for param in &actor.capability_params { + self.register_checked_binding(¶m.name); + } + for field in &actor.state_fields { + self.check_var_decl(field); + } + let initial_state = self.states.clone(); + for (handler_index, handler) in actor.handlers.iter().enumerate() { + let Some(checked) = handlers.values().find(|checked| { + checked.target().handler_index == handler_index + && checked.body_span() == handler.body.span + && self.resolve.is_some_and(|resolve| { + resolve + .scope_table + .def(checked.target().actor_definition) + .span + == actor.name.span + }) + }) else { + continue; + }; + if !self.actor_header_matches(actor, handler_index, checked) { + continue; + } + let target = checked.target(); + self.body_facts = Some(OwnershipBodyFacts::scoped(checked.body())); + self.assignment_context = AssignmentContext { + root: AssignmentContextRoot::ActorHandler(target), + scopes: Vec::new(), + }; + let persistent = checked + .bindings() + .iter() + .copied() + .filter(|binding| !matches!(binding.role, CheckedActorBindingRole::Message { .. })) + .collect::>(); + self.states = initial_state + .iter() + .filter(|(key, _)| { + persistent + .iter() + .any(|binding| **key == BindingKey::Definition(binding.fact.definition)) + }) + .map(|(key, info)| (key.clone(), info.clone())) + .collect(); + for param in &handler.params { + self.register_checked_binding(¶m.name); + } + let diagnostics_before = self.diagnostics.len(); + self.actor_flow = Some(ActorFlow { + reachable: true, + response_type: checked.response_type(), + responding: handler.responds.is_some(), + persistent, + exits: Vec::new(), + supported: true, + loops: Vec::new(), + }); + // The initial state must itself be available. This also verifies + // persistent aliases against exact previously installed captures. + self.validate_actor_captures(handler.body.span); + self.check_block(&handler.body); + if self.actor_reachable() { + self.record_actor_exit(handler.body.span, CheckedActorHandlerExitKind::Fallthrough); + if handler.responds.is_some() || checked.response_type() != TypeInterner::NOTHING { + self.actor_flow.as_mut().unwrap().supported = false; + } + } + let mut flow = self.actor_flow.take().unwrap(); + flow.exits.sort_by_key(|exit| { + ( + exit.span.file.index(), + exit.span.start, + exit.span.end, + match exit.kind { + CheckedActorHandlerExitKind::Fallthrough => 0, + CheckedActorHandlerExitKind::Respond => 1, + CheckedActorHandlerExitKind::Return => 2, + }, + ) + }); + flow.exits.dedup(); + self.actor_reports.insert( + target, + ActorHandlerReport { + exits: flow.exits, + validated: flow.supported && self.diagnostics.len() == diagnostics_before, + }, + ); + } + self.states = saved_states; + self.assignment_context = saved_context; + self.body_facts = saved_facts; + } + + fn validate_actor_captures(&mut self, span: Span) { + let persistent = self.actor_flow.as_ref().unwrap().persistent.clone(); + for binding in persistent { + let key = BindingKey::Definition(binding.fact.definition); + let expected_source = match binding.fact.mode { + CheckedBindingMode::View { + source: CheckedViewSource::Binding(source), + } => Some(BindingKey::Definition(source)), + _ => None, + }; + let valid = self.states.get(&key).is_some_and(|info| { + info.type_id == binding.fact.ty + && info.mutable == binding.fact.mutable + && !info.debug_consumed + && match binding.fact.mode { + CheckedBindingMode::Owned => { + matches!(info.state, OwnershipState::Owned | OwnershipState::Pending) + && info.borrowed_from.is_none() + } + CheckedBindingMode::View { .. } => { + info.state == OwnershipState::Viewed + && info.borrowed_from == expected_source + } + } + }); + if !valid { + let name = self.binding_label(&key).unwrap_or_else(|| "capture".into()); + self.diagnostics.push( + Diagnostic::error( + 400, + format!( + "actor capture `{name}` must retain its owned state at handler exit" + ), + span, + ) + .with_label( + binding.fact.declaration_span, + "persistent actor capture declared here", + ), + ); + } + if matches!(binding.role, CheckedActorBindingRole::State { .. }) + && matches!(binding.fact.mode, CheckedBindingMode::View { .. }) + && expected_source.is_none() + { + // Existing temporary-view validity is not redefined here. + // Without an exact persistent dependency there is no receipt. + self.actor_flow.as_mut().unwrap().supported = false; + } + if let Some(source) = expected_source { + if !self.states.get(&source).is_some_and(|info| { + !info.debug_consumed + && matches!( + info.state, + OwnershipState::Owned + | OwnershipState::Pending + | OwnershipState::Viewed + ) + }) { + self.diagnostics.push(Diagnostic::error( + 401, + "persistent actor view lost its exact backing capture", + span, + )); + } + } + } + } + + fn record_actor_exit(&mut self, span: Span, kind: CheckedActorHandlerExitKind) { + self.validate_actor_captures(span); + self.actor_flow + .as_mut() + .unwrap() + .exits + .push(CheckedActorHandlerExit { span, kind }); + } + + pub(super) fn check_actor_respond(&mut self, response: &ast::RespondStmt) { + // This terminal path abandons any enclosing staged field replacement. + let reservations = std::mem::take(&mut self.field_update_reservations); + let result_type = self + .body_facts + .and_then(|facts| facts.type_map.get(&response.value.span())) + .copied(); + if result_type.is_some_and(|ty| self.is_copyable(ty)) { + self.check_expr_ownership(&response.value); + } else { + self.consume_expr(&response.value, response.value.span()); + } + self.field_update_reservations = reservations; + if !self.actor_reachable() { + return; + } + self.record_actor_exit(response.span, CheckedActorHandlerExitKind::Respond); + self.set_actor_reachable(false); + } + + fn actor_return_has_explicit_view(value: &Expr) -> bool { + match value { + Expr::Paren(inner, _) | Expr::Coarsen(inner, _) | Expr::Declassify(inner, _) => { + Self::actor_return_has_explicit_view(inner) + } + Expr::View(_, _) => true, + _ => false, + } + } + + pub(super) fn check_actor_return(&mut self, ret: &ast::ReturnStmt) { + let flow = self.actor_flow.as_ref().unwrap(); + let void_exit = !flow.responding + && flow.response_type == TypeInterner::NOTHING + && ret.value.as_ref().is_none_or(|value| { + !Self::actor_return_has_explicit_view(value) + && self + .body_facts + .and_then(|facts| facts.type_map.get(&value.span())) + == Some(&TypeInterner::NOTHING) + }); + // Return exits the handler, so its real operand is evaluated without + // committing or reserving any interrupted outer field replacement. + let reservations = std::mem::take(&mut self.field_update_reservations); + if let Some(value) = &ret.value { + // An admitted nothing result is Copy. Its expression still applies + // every selected call acquisition before persistent exit checks. + self.check_expr_ownership(value); + } + self.field_update_reservations = reservations; + if !self.actor_reachable() { + return; + } + self.record_actor_exit(ret.span, CheckedActorHandlerExitKind::Return); + self.actor_flow.as_mut().unwrap().supported &= void_exit; + self.set_actor_reachable(false); + } + + pub(super) fn check_actor_loop_jump(&mut self, _span: Span, continuing: bool) { + let state = self.states.clone(); + let Some(flow) = &mut self.actor_flow else { + return; + }; + let Some(loop_flow) = flow.loops.last_mut() else { + flow.supported = false; + return; + }; + if continuing { + loop_flow.continues.push(state); + } else { + loop_flow.breaks.push(state); + } + flow.reachable = false; + } + + fn actor_branch( + &mut self, + state: &HashMap, + body: &Block, + ) -> Option> { + self.states = state.clone(); + self.set_actor_reachable(true); + self.check_block(body); + self.actor_reachable().then(|| self.states.clone()) + } + + fn join_actor_branches( + &mut self, + baseline: &HashMap, + outcomes: &[HashMap], + ) { + self.states = self.merge_fallthrough_states(baseline, outcomes); + self.set_actor_reachable(!outcomes.is_empty()); + } + + pub(super) fn check_actor_if(&mut self, statement: &ast::IfStmt) { + self.check_expr_ownership(&statement.condition); + if !self.actor_reachable() { + return; + } + let selection = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&statement.span)) + .copied(); + if let Some(selection) = selection { + match selection { + CheckedStaticSelection::IfThen => self.check_block(&statement.then_block), + CheckedStaticSelection::IfElseIf(index) => { + if let Some((condition, body)) = statement.else_ifs.get(index) { + self.check_expr_ownership(condition); + if self.actor_reachable() { + self.check_block(body); + } + } + } + CheckedStaticSelection::IfElse => { + if let Some(body) = &statement.else_block { + self.check_block(body); + } + } + CheckedStaticSelection::IfNoBranch => {} + CheckedStaticSelection::MatchArm(_) => { + self.actor_flow.as_mut().unwrap().supported = false + } + } + return; + } + let mut prefix = self.states.clone(); + let mut outcomes = Vec::new(); + if let Some(state) = self.actor_branch(&prefix, &statement.then_block) { + outcomes.push(state); + } + let mut prefix_reachable = true; + for (condition, body) in &statement.else_ifs { + self.states = prefix.clone(); + self.set_actor_reachable(true); + self.check_expr_ownership(condition); + if !self.actor_reachable() { + prefix_reachable = false; + break; + } + prefix = self.states.clone(); + if let Some(state) = self.actor_branch(&prefix, body) { + outcomes.push(state); + } + } + if prefix_reachable { + if let Some(body) = &statement.else_block { + if let Some(state) = self.actor_branch(&prefix, body) { + outcomes.push(state); + } + } else { + outcomes.push(prefix.clone()); + } + } + self.join_actor_branches(&prefix, &outcomes); + } + + pub(super) fn check_actor_match(&mut self, statement: &ast::MatchStmt) { + self.check_expr_ownership(&statement.expr); + if !self.actor_reachable() { + return; + } + self.record_debug_move(&statement.expr); + let baseline = self.states.clone(); + let source = self.field_loan_root(&statement.expr); + let selected = self + .body_facts + .and_then(|facts| facts.static_selections) + .and_then(|choices| choices.get(&statement.span)) + .and_then(|choice| { + if let CheckedStaticSelection::MatchArm(index) = choice { + Some(*index) + } else { + None + } + }); + let mut outcomes = Vec::new(); + for (index, arm) in statement.arms.iter().enumerate() { + if selected.is_some_and(|selected| selected != index) { + continue; + } + self.states = baseline.clone(); + self.set_actor_reachable(true); + if let ast::Pattern::Variant(_, bindings) = &arm.pattern { + for name in bindings { + self.register_checked_binding(name); + let key = self.binding_key(&name.name, name.span); + if let Some(info) = self.states.get_mut(&key) + && info.state == OwnershipState::Viewed + && info.borrowed_from.is_none() + { + info.borrowed_from = source.clone(); + } + } + } + self.check_block(&arm.body); + if self.actor_reachable() { + outcomes.push(self.states.clone()); + } + } + self.join_actor_branches(&baseline, &outcomes); + } + + pub(super) fn check_actor_scoped_type_body(&mut self, statement: &ast::ComptimeTypeBindStmt) { + let Some(bodies) = self + .body_facts + .and_then(|facts| facts.comptime_type_bindings.get(&statement.span)) + else { + self.actor_flow.as_mut().unwrap().supported = false; + return; + }; + let baseline = self.states.clone(); + let mut outcomes = Vec::new(); + for (index, body) in bodies.iter().enumerate() { + let saved_context = self.assignment_context.clone(); + self.assignment_context = saved_context.scoped(statement.span, index); + let saved_facts = self + .body_facts + .replace(OwnershipBodyFacts::scoped(&body.body)); + if let Some(state) = self.actor_branch(&baseline, &statement.body) { + outcomes.push(state); + } + self.body_facts = saved_facts; + self.assignment_context = saved_context; + } + self.join_actor_branches(&baseline, &outcomes); + } + + pub(super) fn check_actor_handle( + &mut self, + target: &Expr, + error: Option<&ast::Ident>, + body: &Block, + ) { + self.check_expr_ownership(target); + if !self.actor_reachable() { + return; + } + self.record_debug_move(target); + self.check_actor_handle_body(error, body); + } + + pub(super) fn check_actor_handle_body(&mut self, error: Option<&ast::Ident>, body: &Block) { + if !self.actor_reachable() { + return; + } + let success = self.states.clone(); + if let Some(name) = error { + self.register_checked_binding(name); + } + self.check_block(body); + let mut outcomes = vec![success.clone()]; + if self.actor_reachable() { + outcomes.push(self.states.clone()); + } + self.join_actor_branches(&success, &outcomes); + } + + pub(super) fn check_actor_for(&mut self, statement: &ast::ForStmt) { + if statement.view { + self.check_expr_ownership(&statement.iterable); + } else { + self.consume_expr(&statement.iterable, statement.iterable.span()); + } + if !self.actor_reachable() { + return; + } + let entry = self.states.clone(); + let mut header = entry.clone(); + let mut exits = vec![entry.clone()]; // the iterable may be empty + // The join only loses availability or a pending guarantee. At most one + // loss per entry binding is needed to reach the finite fixed point. + for _ in 0..entry.len().saturating_mul(2).saturating_add(3) { + self.states = header.clone(); + self.set_actor_reachable(true); + self.register_checked_binding(&statement.variable); + let source = if statement.view { + self.field_loan_root(&statement.iterable) + } else { + self.binding_borrow_source(&statement.variable) + }; + let key = self.binding_key(&statement.variable.name, statement.variable.span); + if let Some(info) = self.states.get_mut(&key) { + info.borrowed_from = source; + } else { + self.actor_flow.as_mut().unwrap().supported = false; + } + if let Some(name) = &statement.value_variable { + self.register_checked_binding(name); + } + self.actor_flow + .as_mut() + .unwrap() + .loops + .push(ActorLoopFlow::default()); + self.check_block(&statement.body); + let loop_flow = self.actor_flow.as_mut().unwrap().loops.pop().unwrap(); + exits.extend(loop_flow.breaks); + let mut backedges = vec![entry.clone()]; + backedges.extend(loop_flow.continues); + if self.actor_reachable() { + backedges.push(self.states.clone()); + } + let next = self.merge_fallthrough_states(&entry, &backedges); + exits.push(next.clone()); + if next == header { + self.join_actor_branches(&entry, &exits); + return; + } + header = next; + } + self.actor_flow.as_mut().unwrap().supported = false; + self.join_actor_branches(&entry, &exits); + } + + pub(super) fn check_actor_while(&mut self, statement: &ast::WhileStmt) { + let entry = self.states.clone(); + let mut header = entry.clone(); + let mut exits = Vec::new(); + for _ in 0..entry.len().saturating_mul(2).saturating_add(3) { + self.states = header.clone(); + self.set_actor_reachable(true); + self.check_expr_ownership(&statement.condition); + if !self.actor_reachable() { + self.join_actor_branches(&entry, &exits); + return; + } + exits.push(self.states.clone()); // condition may be false + self.actor_flow + .as_mut() + .unwrap() + .loops + .push(ActorLoopFlow::default()); + self.check_block(&statement.body); + let loop_flow = self.actor_flow.as_mut().unwrap().loops.pop().unwrap(); + exits.extend(loop_flow.breaks); + let mut backedges = vec![entry.clone()]; + backedges.extend(loop_flow.continues); + if self.actor_reachable() { + backedges.push(self.states.clone()); + } + let next = self.merge_fallthrough_states(&entry, &backedges); + if next == header { + self.join_actor_branches(&entry, &exits); + return; + } + header = next; + } + self.actor_flow.as_mut().unwrap().supported = false; + self.join_actor_branches(&entry, &exits); + } + + pub(super) fn check_actor_message_ownership(&mut self, inner: &Expr, span: Span) { + let Some(packet) = self + .body_facts + .and_then(|facts| facts.actor_message_calls.get(&span)) + else { + // Failed checking has no actor transfer packet. + self.check_expr_ownership(inner); + if let Some(flow) = &mut self.actor_flow { + flow.supported = false; + } + return; + }; + let (receiver, args) = match inner { + Expr::Call(callee, args, _) => match callee.as_ref() { + Expr::FieldAccess(receiver, _, _) => (receiver.as_ref(), args.as_slice()), + _ => return, + }, + Expr::FieldAccess(receiver, _, _) => (receiver.as_ref(), &[][..]), + _ => return, + }; + self.check_expr_ownership(receiver); + for argument in &packet.arguments { + if let Some(source) = args.get(argument.source_index) { + self.check_argument_effect(&source.value, source.span, argument.effect); + } + } + } +} diff --git a/crates/jett_typecheck/src/ownership/field_updates.rs b/crates/jett_typecheck/src/ownership/field_updates.rs new file mode 100644 index 00000000..938e487b --- /dev/null +++ b/crates/jett_typecheck/src/ownership/field_updates.rs @@ -0,0 +1,185 @@ +//! Actual checked storage and reservations for Source field replacements. +//! Local alias expiration retains the existing bounded backend policy; this +//! does not introduce general last-use alias support or runtime loan tokens. + +use super::*; + +impl OwnershipChecker<'_> { + pub(super) fn field_loan_root(&self, expression: &Expr) -> Option { + match expression { + Expr::View(inner, _) + | Expr::Paren(inner, _) + | Expr::Coarsen(inner, _) + | Expr::Declassify(inner, _) + | Expr::FieldAccess(inner, _, _) => self.field_loan_root(inner), + Expr::Ident(root) => { + let definition = self.resolve?.resolutions.get(&root.span)?; + Some(BindingKey::Definition(*definition)) + } + // Clones, calls and other owning results start independent storage. + _ => None, + } + } + + pub(super) fn field_assignment_root(target: &Expr) -> Option<&ast::Ident> { + match target { + Expr::Ident(ident) => Some(ident), + Expr::FieldAccess(inner, _, _) | Expr::Paren(inner, _) => { + Self::field_assignment_root(inner) + } + _ => None, + } + } + + pub(super) fn binding_borrow_source(&self, name: &ast::Ident) -> Option { + match self.binding_fact(name)?.mode { + CheckedBindingMode::View { + source: CheckedViewSource::Binding(definition), + } => Some(BindingKey::Definition(definition)), + CheckedBindingMode::Owned + | CheckedBindingMode::View { + source: CheckedViewSource::Other, + } => None, + } + } + + pub(super) fn reject_reserved_field_update_change( + &mut self, + key: &BindingKey, + span: Span, + ) -> bool { + if !self.field_update_reservations.contains(key) { + return false; + } + if self + .field_update_reservation_errors + .insert((key.clone(), span)) + { + let name = self.binding_label(key).unwrap_or_else(|| "owner".into()); + self.diagnostics.push(Diagnostic::error( + 404, + format!("cannot consume or rebind `{name}` while evaluating its field replacement"), + span, + )); + } + true + } + + fn alias_depends_on(&self, alias: &BindingKey, root: &BindingKey) -> bool { + let mut current = alias; + for _ in 0..self.states.len() { + let Some(source) = self + .states + .get(current) + .and_then(|info| info.borrowed_from.as_ref()) + else { + return false; + }; + if source == root { + return true; + } + current = source; + } + false + } + + pub(super) fn has_dependent_field_alias(&self, root: &BindingKey) -> bool { + self.states.iter().any(|(key, alias)| { + alias.state == OwnershipState::Viewed && self.alias_depends_on(key, root) + }) + } + + fn check_field_update_live_root(&mut self, place: &CheckedAssignmentPlace) -> bool { + let root = BindingKey::Definition(place.root.definition); + let Some(info) = self.states.get(&root) else { + self.diagnostics.push(Diagnostic::error( + 404, + "field replacement requires an initialized live owned binding", + place.root_use_span, + )); + return false; + }; + let name = self.binding_label(&root).unwrap_or_else(|| "owner".into()); + if info.state == OwnershipState::Consumed || info.debug_consumed { + self.diagnostics.push(use_after_move( + &name, + place.root_use_span, + info.consumed_span.unwrap_or(place.root.declaration_span), + )); + return false; + } + if info.state == OwnershipState::Viewed { + self.diagnostics + .push(cannot_assign_through_view(place.root_use_span)); + return false; + } + if !info.mutable + || info.type_id != place.root.ty + || info.state == OwnershipState::Uninitialized + { + self.diagnostics.push(Diagnostic::error( + 404, + "field replacement requires the exact initialized mutable owned binding", + place.root_use_span, + )); + return false; + } + // Pending is runtime inspection state. It does not manufacture payload + // readiness here and is deliberately not a blanket Source rejection. + if self.has_dependent_field_alias(&root) { + self.diagnostics.push(Diagnostic::error( + 401, + format!( + "cannot replace a field of `{name}` while a dependent local view is retained" + ), + place.root_use_span, + )); + return false; + } + true + } + + pub(super) fn check_field_update_assignment( + &mut self, + assign: &ast::AssignStmt, + place: &CheckedAssignmentPlace, + ) { + let diagnostics_before = self.diagnostics.len(); + let declaration_matches = self + .body_facts + .and_then(|facts| facts.binding_facts.get(&place.root.declaration_span)) + == Some(&place.root); + let source_matches = self.resolve.is_some_and(|resolve| { + resolve.resolutions.get(&place.root_use_span) == Some(&place.root.definition) + }) && place.assignment_span == assign.span + && declaration_matches; + if !source_matches || crate::validate_assignment_place_shape(place, self.interner).is_err() + { + self.diagnostics.push(Diagnostic::error( + 404, + "field replacement lacks its exact checked Source place", + assign.span, + )); + } + let root = BindingKey::Definition(place.root.definition); + self.reject_reserved_field_update_change(&root, place.root_use_span); + let root_was_live = self.check_field_update_live_root(place); + self.field_update_reservations.push(root); + // This is a transfer into a staged owning field, never an implicit + // Source clone. Written field reads retain their existing copy rules. + self.consume_expr(&assign.value, assign.value.span()); + self.field_update_reservations.pop(); + if !self.actor_reachable() { + return; + } + if root_was_live { + self.check_field_update_live_root(place); + } + if self.diagnostics.len() == diagnostics_before { + self.validated_field_updates.insert(AssignmentSite { + context: self.assignment_context.clone(), + span: assign.span, + }); + } + } +} diff --git a/crates/jett_typecheck/src/reflected_return.rs b/crates/jett_typecheck/src/reflected_return.rs new file mode 100644 index 00000000..5d6c7974 --- /dev/null +++ b/crates/jett_typecheck/src/reflected_return.rs @@ -0,0 +1,359 @@ +//! Type-only admission for a value produced by a reflected callable invocation. +//! No value, callback or predicate is inspected while deriving this plan. + +use std::collections::HashSet; + +use jett_types::{Type, TypeId, TypeInterner}; + +use crate::CheckResult; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProducedRefinementUnsupported { + Nested, + Secret, + CallableOrNominal, +} + +impl ProducedRefinementUnsupported { + pub fn message(self) -> &'static str { + match self { + Self::Nested => { + "reflected field refinement: native nested refinement validation is not implemented" + } + Self::Secret => { + "reflected field refinement: cannot establish new invariants beneath secret" + } + Self::CallableOrNominal => { + "reflected field refinement: unsupported callable or nominal refinement conversion" + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProducedRefinementRequirement { + Exact, + Predicates(Vec), + Unsupported(ProducedRefinementUnsupported), + Refine { + base: Box, + predicates: Vec, + }, + List(Box), + Set(Box), + Map { + key: Box, + value: Box, + }, + Optional(Box), + Result { + ok: Box, + error: Box, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProducedRefinementRequirementPlan { + pub source_type: TypeId, + pub requested_type: TypeId, + pub requirement: ProducedRefinementRequirement, +} + +/// Constructor-only evidence from one checked type relation. Its caller must +/// additionally retain that exact immutable checked session. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedReflectedCallablePlan { + source_type: TypeId, + requested_type: TypeId, + source_return: TypeId, + requested_return: TypeId, + return_requirement: ProducedRefinementRequirement, + source_signature: Type, + requested_signature: Type, +} + +impl CheckedReflectedCallablePlan { + pub fn source_type(&self) -> TypeId { + self.source_type + } + pub fn requested_type(&self) -> TypeId { + self.requested_type + } + pub fn source_return(&self) -> TypeId { + self.source_return + } + pub fn requested_return(&self) -> TypeId { + self.requested_return + } + pub fn return_requirement(&self) -> &ProducedRefinementRequirement { + &self.return_requirement + } + + pub fn validate(&self, checked: &CheckResult) -> bool { + checked + .reflected_callable_plan(self.source_type, self.requested_type) + .as_ref() + == Some(self) + } + + pub fn validate_types(&self, types: &TypeInterner) -> bool { + (self.source_type.index() as usize) < types.len() + && (self.requested_type.index() as usize) < types.len() + && types.resolve(self.source_type) == &self.source_signature + && types.resolve(self.requested_type) == &self.requested_signature + && produced_refinement_requirement(types, self.source_return, self.requested_return) + .as_ref() + == Some(&self.return_requirement) + } +} + +impl CheckResult { + pub fn reflected_callable_plan( + &self, + actual: TypeId, + requested: TypeId, + ) -> Option { + if !self.ordinary_callable_parameters_compatible(actual, requested) { + return None; + } + let Type::Function { + return_type: source_return, + .. + } = self.interner.resolve(actual) + else { + return None; + }; + let Type::Function { + return_type: requested_return, + .. + } = self.interner.resolve(requested) + else { + return None; + }; + let return_requirement = + produced_refinement_requirement(&self.interner, *source_return, *requested_return)?; + let node = ProducedRefinementRequirementPlan { + source_type: *source_return, + requested_type: *requested_return, + requirement: return_requirement.clone(), + }; + if !supported_node(self, &node) { + return None; + } + Some(CheckedReflectedCallablePlan { + source_type: actual, + requested_type: requested, + source_return: *source_return, + requested_return: *requested_return, + return_requirement, + source_signature: self.interner.resolve(actual).clone(), + requested_signature: self.interner.resolve(requested).clone(), + }) + } +} + +fn supported_node(checked: &CheckResult, node: &ProducedRefinementRequirementPlan) -> bool { + use ProducedRefinementRequirement as R; + let Some((source_base, source_chain)) = refinement_chain(&checked.interner, node.source_type) + else { + return false; + }; + let Some((requested_base, _)) = refinement_chain(&checked.interner, node.requested_type) else { + return false; + }; + match &node.requirement { + R::Exact => { + node.source_type == node.requested_type + || source_chain.contains(&node.requested_type) + || checked.ordinary_types_compatible(node.requested_type, node.source_type) + || checked.ordinary_types_compatible(node.requested_type, source_base) + } + R::Predicates(_) => checked.ordinary_types_compatible(requested_base, source_base), + R::Unsupported(_) => false, + R::Refine { base, .. } => supported_node(checked, base), + R::List(child) | R::Set(child) | R::Optional(child) => supported_node(checked, child), + R::Map { key, value } => supported_node(checked, key) && supported_node(checked, value), + R::Result { ok, error } => supported_node(checked, ok) && supported_node(checked, error), + } +} + +fn contains_refinement(types: &TypeInterner, ty: TypeId, seen: &mut HashSet) -> bool { + if ty.index() as usize >= types.len() { + return true; + } + if !seen.insert(ty) { + return false; + } + let children = match types.resolve(ty) { + Type::Refinement { .. } => return true, + Type::Secret(inner) | Type::List(inner) | Type::Set(inner) | Type::Optional(inner) => { + vec![*inner] + } + Type::Result(a, b) | Type::Map(a, b) => vec![*a, *b], + Type::Function { + params, + return_type, + .. + } => params + .iter() + .copied() + .chain(std::iter::once(*return_type)) + .collect(), + Type::Struct(_) + | Type::Enum(_) + | Type::Machine(_) + | Type::MachineState { .. } + | Type::Bitfield(_) + | Type::Interface(_) + | Type::Actor(_) => types.nominal_type_arguments(ty).to_vec(), + _ => Vec::new(), + }; + children + .into_iter() + .any(|child| contains_refinement(types, child, seen)) +} + +pub(crate) fn refinement_chain( + types: &TypeInterner, + mut ty: TypeId, +) -> Option<(TypeId, Vec)> { + let mut chain = Vec::new(); + for _ in 0..types.len() { + if ty.index() as usize >= types.len() { + return None; + } + match types.resolve(ty) { + Type::Refinement { base, .. } => { + chain.push(ty); + ty = *base; + } + _ => { + chain.reverse(); + return Some((ty, chain)); + } + } + } + None +} + +pub fn produced_refinement_requirement( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, +) -> Option { + requirement_node(types, actual, requested, types.len()).map(|node| node.requirement) +} + +fn requirement_node( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + if budget == 0 + || actual.index() as usize >= types.len() + || requested.index() as usize >= types.len() + { + return None; + } + Some(ProducedRefinementRequirementPlan { + source_type: actual, + requested_type: requested, + requirement: requirement_action(types, actual, requested, budget - 1)?, + }) +} + +fn requirement_action( + types: &TypeInterner, + actual: TypeId, + requested: TypeId, + budget: usize, +) -> Option { + use ProducedRefinementRequirement as R; + if actual == requested || !contains_refinement(types, requested, &mut HashSet::new()) { + return Some(R::Exact); + } + let (actual_base, actual_chain) = refinement_chain(types, actual)?; + if actual_chain.contains(&requested) { + return Some(R::Exact); + } + if actual_base == requested + && !matches!( + types.resolve(requested), + Type::List(_) | Type::Set(_) | Type::Map(..) | Type::Optional(_) | Type::Result(..) + ) + { + return Some(R::Exact); + } + let (base, mut chain) = refinement_chain(types, requested)?; + if !chain.is_empty() { + if let Some(index) = chain.iter().rposition(|ty| actual_chain.contains(ty)) { + chain.drain(..=index); + return Some(R::Predicates(chain)); + } + let child = requirement_node(types, actual, base, budget)?; + return Some(match child.requirement { + R::Exact => R::Predicates(chain), + R::Unsupported(reason) => R::Unsupported(reason), + _ => R::Refine { + base: Box::new(child), + predicates: chain, + }, + }); + } + let child = |source, target| requirement_node(types, source, target, budget).map(Box::new); + Some( + match (types.resolve(actual_base), types.resolve(requested)) { + (Type::List(a), Type::List(b)) => R::List(child(*a, *b)?), + (Type::Set(a), Type::Set(b)) => R::Set(child(*a, *b)?), + (Type::Map(ak, av), Type::Map(bk, bv)) => R::Map { + key: child(*ak, *bk)?, + value: child(*av, *bv)?, + }, + (Type::Optional(a), Type::Optional(b)) => R::Optional(child(*a, *b)?), + (Type::Result(a, b), Type::Result(c, d)) => R::Result { + ok: child(*a, *c)?, + error: child(*b, *d)?, + }, + (_, Type::Secret(_)) => R::Unsupported(ProducedRefinementUnsupported::Secret), + _ => R::Unsupported(ProducedRefinementUnsupported::CallableOrNominal), + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn return_requirements_keep_exact_prefix_and_new_suffix_separate() { + let mut types = TypeInterner::new(); + let positive = types.intern(Type::Refinement { + name: "app.Positive".into(), + base: TypeInterner::INT64, + }); + let bounded = types.intern(Type::Refinement { + name: "app.Bounded".into(), + base: positive, + }); + assert_eq!( + produced_refinement_requirement(&types, TypeInterner::INT64, positive), + Some(ProducedRefinementRequirement::Predicates(vec![positive])) + ); + assert_eq!( + produced_refinement_requirement(&types, positive, bounded), + Some(ProducedRefinementRequirement::Predicates(vec![bounded])) + ); + assert_eq!( + produced_refinement_requirement(&types, bounded, positive), + Some(ProducedRefinementRequirement::Exact) + ); + let secret = types.intern(Type::Secret(positive)); + assert_eq!( + produced_refinement_requirement(&types, TypeInterner::INT64, secret), + Some(ProducedRefinementRequirement::Unsupported( + ProducedRefinementUnsupported::Secret + )) + ); + } +} diff --git a/crates/jett_typecheck/src/resource_hook_tests.rs b/crates/jett_typecheck/src/resource_hook_tests.rs new file mode 100644 index 00000000..edd978b5 --- /dev/null +++ b/crates/jett_typecheck/src/resource_hook_tests.rs @@ -0,0 +1,406 @@ +use std::collections::HashMap; + +use jett_common::{FileId, STDLIB_FILE_ID_START, SourceOrigin}; +use jett_diagnostics::Severity; +use jett_parser::{ + ast::{Item, Module}, + parse, +}; +use jett_resolve::{ + DefId, ResolveResult, ResourceKernelSpec, resolve, resolve_with_resource_kernels, +}; +use jett_types::{ResourceHookKind, ResourceKernelRecipe, Type, TypeInterner}; + +use crate::{ + CheckOptions, CheckResult, ResourceHookError, check, check_with_resource_kernels, + validate_resource_hooks, +}; + +const SOURCE: &str = "namespace resource_probe\nresource TestHandle\ntype HandleAlias = TestHandle\nfunction checked_calls(view net: Network) returns nothing:\n TestHandle first = kernel_create(view net, 1) handle error:\n return nothing\n int64 observed = kernel_borrow(view net, view first) handle error:\n return nothing\n kernel_close(first)\n return nothing\nfunction alias_calls(view net: Network) returns nothing:\n HandleAlias first = kernel_create(view net, 2) handle error:\n return nothing\n int64 observed = kernel_borrow(view net, view first) handle error:\n return nothing\n kernel_close(first)\n return nothing\n"; + +fn fixture(source: &str) -> (Module, ResolveResult) { + let file = FileId::new(STDLIB_FILE_ID_START); + let parsed = parse(source, file); + assert!( + !parsed + .errors + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "parse errors: {:?}", + parsed.errors + ); + let resource = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource) + } else { + None + } + }) + .unwrap(); + let span = resource.name.span; + let specs = [ + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_create".into(), + recipe: ResourceKernelRecipe::NetworkFactory, + }, + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_borrow".into(), + recipe: ResourceKernelRecipe::NetworkBorrow, + }, + ResourceKernelSpec { + resource_declaration: span, + member: "kernel_close".into(), + recipe: ResourceKernelRecipe::Finalize, + }, + ]; + let resolved = resolve_with_resource_kernels( + &parsed.module, + &HashMap::from([(file, SourceOrigin::Stdlib)]), + &specs, + ) + .unwrap(); + assert!( + !resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "resolution errors: {:?}", + resolved.diagnostics + ); + (parsed.module, resolved) +} + +fn accepted() -> (Module, ResolveResult, CheckResult) { + let (module, resolved) = fixture(SOURCE); + let checked = check_with_resource_kernels(&module, &resolved, CheckOptions::default()).unwrap(); + assert!( + !checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error), + "checking errors: {:?}", + checked.diagnostics + ); + (module, resolved, checked) +} + +#[test] +fn checked_resource_kernel_calls_use_exact_declarations_modes_and_alias_identity() { + let (module, resolved, checked) = accepted(); + validate_resource_hooks(&module, &resolved, &checked).unwrap(); + assert_eq!(checked.resource_hooks.len(), 3); + let resource = checked + .resource_hooks + .values() + .next() + .unwrap() + .resource_type; + assert!( + matches!(checked.interner.resolve(resource), Type::Resource(name) if name == "resource_probe.TestHandle") + ); + let Item::Resource(declaration) = &module.items[1] else { + panic!("missing resource") + }; + let definition = resolved.resolutions[&declaration.name.span]; + assert_eq!( + checked.definition_types[&definition], resource, + "synthetic source attribution must not overwrite the resource span join" + ); + assert_eq!( + checked + .reflection_metadata + .get_type_info_for_id(resource) + .unwrap() + .kind, + "resource" + ); + assert!( + checked + .reflection_metadata + .get_type_fields_for_id(resource) + .is_none() + ); + for entry in resolved.resource_kernels.iter() { + let hook = &checked.resource_hooks[&entry.definition]; + assert_eq!(hook.definition, entry.definition); + assert_eq!(hook.resource_definition, definition); + assert_eq!(hook.resource_type, resource); + assert_eq!(hook.kind, entry.recipe.kind()); + let Type::Function { + params, + view_params, + return_type, + } = checked.interner.resolve(hook.function_type) + else { + panic!("not a function") + }; + match entry.recipe { + ResourceKernelRecipe::NetworkFactory => { + assert_eq!(params, &[TypeInterner::NETWORK, TypeInterner::INT64]); + assert_eq!(view_params, &[true, false]); + assert!( + matches!(checked.interner.resolve(*return_type), Type::Result(value, error) + if *value == resource && *error == TypeInterner::STRING) + ); + } + ResourceKernelRecipe::NetworkBorrow => { + assert_eq!(params, &[TypeInterner::NETWORK, resource]); + assert_eq!(view_params, &[true, true]); + assert!( + matches!(checked.interner.resolve(*return_type), Type::Result(value, error) + if *value == TypeInterner::INT64 && *error == TypeInterner::STRING) + ); + } + ResourceKernelRecipe::Finalize => { + assert_eq!(params, &[resource]); + assert_eq!(view_params, &[false]); + assert_eq!(*return_type, TypeInterner::NOTHING); + } + } + } + // These call type facts are produced by normal source checking, not a + // private helper bypass. The alias function uses the same Resource ID. + assert!( + checked + .type_map + .values() + .filter(|ty| **ty == resource) + .count() + >= 4 + ); + assert!(checked.intrinsic_ids.is_empty()); +} + +#[test] +fn resource_catalog_requires_explicit_checker_entry_and_empty_catalog_has_no_authority() { + let (module, resolved) = fixture(SOURCE); + let legacy = check(&module, &resolved); + assert!(legacy.diagnostics.iter().any(|diagnostic| diagnostic.code.code() == 0 && diagnostic.severity == Severity::Error)); + assert!(legacy.resource_hooks.is_empty()); + let ordinary = resolve(&module); + assert!( + ordinary + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 200 + && diagnostic.severity == Severity::Error) + ); + let checked = check(&module, &ordinary); + assert!(checked.resource_hooks.is_empty()); +} + +#[test] +fn checked_resource_calls_preserve_ordinary_argument_type_and_owned_view_rejection() { + for (source, expected) in [ + ( + SOURCE.replace( + "kernel_create(view net, 1)", + "kernel_create(view net, true)", + ), + 304, + ), + ( + SOURCE.replace("kernel_close(first)", "kernel_close(view first)"), + 375, + ), + ] { + let (module, resolved) = fixture(&source); + let checked = + check_with_resource_kernels(&module, &resolved, CheckOptions::default()).unwrap(); + assert!( + checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == expected + && diagnostic.severity == Severity::Error), + "expected E{expected}, got {:?}", + checked.diagnostics + ); + } +} + +#[test] +fn checked_resource_hook_metadata_rejects_unused_identity_corruption() { + for corruption in 0..5 { + let (module, resolved, mut checked) = accepted(); + let definition = resolved.resource_kernels.iter().last().unwrap().definition; + let hook = checked.resource_hooks.get_mut(&definition).unwrap(); + match corruption { + 0 => hook.definition = DefId::new(u32::MAX), + 1 => hook.resource_definition = definition, + 2 => hook.resource_type = TypeInterner::INT64, + 3 => hook.kind = ResourceHookKind::Construct, + _ => hook.function_type = TypeInterner::NOTHING, + } + assert!(validate_resource_hooks(&module, &resolved, &checked).is_err()); + } + let (module, resolved, mut checked) = accepted(); + checked.resource_hooks.clear(); + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::UnexpectedCheckedHooks) + ); +} + +#[test] +fn checked_resource_hook_function_metadata_requires_complete_exact_shape() { + for corruption in 0..7 { + let (module, resolved, mut checked) = accepted(); + let entry = resolved + .resource_kernels + .iter() + .find(|entry| entry.recipe == ResourceKernelRecipe::NetworkBorrow) + .unwrap(); + let original = checked.definition_types[&entry.definition]; + let Type::Function { + mut params, + mut view_params, + mut return_type, + } = checked.interner.resolve(original).clone() + else { + panic!("not a function") + }; + match corruption { + 0 => params[1] = TypeInterner::INT64, + 1 => { + params.pop(); + } + 2 => view_params[1] = false, + 3 => { + view_params.pop(); + } + 4 => return_type = TypeInterner::INT64, + 5 => { + return_type = checked + .interner + .intern(Type::Result(TypeInterner::INT64, TypeInterner::BOOL)) + } + _ => params[0] = TypeInterner::FILESYSTEM, + } + let malformed = checked.interner.intern(Type::Function { + params, + view_params, + return_type, + }); + checked.definition_types.insert(entry.definition, malformed); + checked + .resource_hooks + .get_mut(&entry.definition) + .unwrap() + .function_type = malformed; + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::InvalidFunctionShape(entry.definition)) + ); + } + let (module, resolved, mut checked) = accepted(); + let definition = resolved.resource_kernels.iter().next().unwrap().definition; + checked.definition_types.remove(&definition); + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::MissingFunctionType(definition)) + ); + let (module, resolved, mut checked) = accepted(); + let resource = resolved + .resource_kernels + .iter() + .next() + .unwrap() + .resource_definition; + checked + .definition_types + .insert(resource, TypeInterner::INT64); + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::InvalidResourceType(resource)) + ); + + let (module, resolved, mut checked) = accepted(); + let entry = resolved + .resource_kernels + .iter() + .find(|entry| entry.recipe == ResourceKernelRecipe::NetworkBorrow) + .unwrap(); + let mut foreign = TypeInterner::new(); + let mut foreign_id = TypeInterner::INT64; + for _ in 0..checked.interner.len() + 1 { + foreign_id = foreign.intern(Type::List(foreign_id)); + } + assert!(foreign_id.index() as usize >= checked.interner.len()); + let malformed = checked.interner.intern(Type::Function { + params: vec![ + TypeInterner::NETWORK, + checked.resource_hooks[&entry.definition].resource_type, + ], + view_params: vec![true, true], + return_type: foreign_id, + }); + checked.definition_types.insert(entry.definition, malformed); + checked + .resource_hooks + .get_mut(&entry.definition) + .unwrap() + .function_type = malformed; + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::InvalidFunctionShape(entry.definition)) + ); +} + +#[test] +fn privileged_resource_entry_rejects_unused_resolver_corruption_and_checks_source_bodies() { + let (module, mut resolved) = fixture("namespace resource_probe\nresource TestHandle\n"); + let definition = resolved.resource_kernels.iter().last().unwrap().definition; + resolved.scope_table.definitions[definition.index() as usize].id = DefId::new(u32::MAX); + assert!(matches!( + check_with_resource_kernels(&module, &resolved, CheckOptions::default()), + Err(ResourceHookError::Resolution(_)) + )); + + let (module, resolved) = fixture(&SOURCE.replace( + " kernel_close(first)", + " int64 broken = true\n kernel_close(first)", + )); + let checked = check_with_resource_kernels(&module, &resolved, CheckOptions::default()).unwrap(); + assert!( + checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.code() == 311 + && diagnostic.severity == Severity::Error), + "source bodies must remain normally checked: {:?}", + checked.diagnostics + ); + assert_eq!( + validate_resource_hooks(&module, &resolved, &checked), + Err(ResourceHookError::SourceCheckingFailed), + "diagnostic-bearing checker results must not pass the public phase gate" + ); +} + +#[test] +fn unrelated_same_named_source_function_does_not_acquire_hook_identity() { + let file = FileId::new(0); + let parsed = parse( + "function kernel_close(value: int64) returns nothing:\n return nothing\nfunction main() returns nothing:\n kernel_close(1)\n return nothing\n", + file, + ); + assert!(parsed.errors.is_empty()); + let resolved = resolve(&parsed.module); + let checked = check(&parsed.module, &resolved); + assert!( + checked + .diagnostics + .iter() + .all(|diagnostic| diagnostic.severity != Severity::Error), + "{:?}", + checked.diagnostics + ); + assert!(checked.resource_hooks.is_empty()); +} diff --git a/crates/jett_typecheck/src/resource_hooks.rs b/crates/jett_typecheck/src/resource_hooks.rs new file mode 100644 index 00000000..616e6a8c --- /dev/null +++ b/crates/jett_typecheck/src/resource_hooks.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; +use std::fmt; + +use jett_parser::ast::Module; +use jett_resolve::{DefId, ResolveResult, ResourceKernelError, validate_resource_kernels}; +use jett_types::{ResourceHookKind, Type, TypeId}; + +use crate::checker::CheckResult; + +/// Immutable checked identity. A matching callable name never grants authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedResourceHook { + pub definition: DefId, + pub resource_definition: DefId, + pub resource_type: TypeId, + pub kind: ResourceHookKind, + pub function_type: TypeId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceHookError { + Resolution(ResourceKernelError), + SourceResolutionFailed, + SourceCheckingFailed, + MissingResourceType(DefId), + InvalidResourceType(DefId), + MissingFunctionType(DefId), + InvalidFunctionShape(DefId), + InvalidCheckedIdentity(DefId), + UnexpectedCheckedHooks, +} + +impl From for ResourceHookError { + fn from(error: ResourceKernelError) -> Self { + Self::Resolution(error) + } +} + +impl fmt::Display for ResourceHookError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "invalid checked resource hook metadata: {self:?}" + ) + } +} + +impl std::error::Error for ResourceHookError {} + +/// Validate all facts, including unused hooks, before a subsequent phase uses them. +pub fn validate_resource_hooks( + module: &Module, + resolved: &ResolveResult, + checked: &CheckResult, +) -> Result<(), ResourceHookError> { + validate_resource_hook_identities(module, resolved, checked)?; + if checked + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + { + return Err(ResourceHookError::SourceCheckingFailed); + } + Ok(()) +} + +/// Check exact declaration/type joins while ordinary source diagnostics stay inspectable. +/// This helper is not an execution-phase acceptance gate. +pub(crate) fn validate_resource_hook_identities( + module: &Module, + resolved: &ResolveResult, + checked: &CheckResult, +) -> Result<(), ResourceHookError> { + validate_resource_kernels(module, resolved)?; + if resolved + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == jett_diagnostics::Severity::Error) + { + return Err(ResourceHookError::SourceResolutionFailed); + } + let mut expected = HashMap::new(); + for entry in resolved.resource_kernels.iter() { + let resource = *checked + .definition_types + .get(&entry.resource_definition) + .ok_or(ResourceHookError::MissingResourceType( + entry.resource_definition, + ))?; + if resource.index() as usize >= checked.interner.len() { + return Err(ResourceHookError::InvalidResourceType( + entry.resource_definition, + )); + } + let declaration = + &resolved.scope_table.definitions[entry.resource_definition.index() as usize]; + if !matches!(checked.interner.resolve(resource), Type::Resource(name) if name == &declaration.name) + { + return Err(ResourceHookError::InvalidResourceType( + entry.resource_definition, + )); + } + let function_type = *checked + .definition_types + .get(&entry.definition) + .ok_or(ResourceHookError::MissingFunctionType(entry.definition))?; + if !entry + .recipe + .matches_signature(&checked.interner, resource, function_type) + { + return Err(ResourceHookError::InvalidFunctionShape(entry.definition)); + } + expected.insert( + entry.definition, + CheckedResourceHook { + definition: entry.definition, + resource_definition: entry.resource_definition, + resource_type: resource, + kind: entry.recipe.kind(), + function_type, + }, + ); + } + if expected.len() != checked.resource_hooks.len() { + return Err(ResourceHookError::UnexpectedCheckedHooks); + } + for (definition, hook) in expected { + if checked.resource_hooks.get(&definition) != Some(&hook) { + return Err(ResourceHookError::InvalidCheckedIdentity(definition)); + } + } + Ok(()) +} diff --git a/crates/jett_typecheck/src/resource_program.rs b/crates/jett_typecheck/src/resource_program.rs new file mode 100644 index 00000000..cbdb65fc --- /dev/null +++ b/crates/jett_typecheck/src/resource_program.rs @@ -0,0 +1,650 @@ +//! Retain one internally derived compiler session before live Resource integration. + +use std::collections::HashMap; +use std::fmt; + +use jett_common::{FileId, SourceOrigin}; +use jett_diagnostics::{Diagnostic, Severity}; +use jett_parser::{ParseResult, ast::Module}; +use jett_resolve::{ + ResolveResult, ResourceKernelError, ResourceKernelSpec, resolve_with_resource_kernels, +}; + +use crate::{ + CheckOptions, CheckResult, ResourceHookError, check_with_resource_kernels, + validate_resource_hooks, +}; + +/// One owned parsed program and the exact resolution/checking session derived from it. +/// +/// Immutable observations do not grant runtime provider authority. There is no +/// constructor from independent checked parts, mutation or ownership extraction. +#[derive(Debug)] +pub struct CheckedResourceProgram { + module: Module, + resolved: ResolveResult, + checked: CheckResult, + source_origins: HashMap, + parse_diagnostics: Vec, +} + +/// A rejected source phase or invalid compiler-owned Resource association. +#[derive(Debug)] +pub enum ResourceProgramError { + ParseDiagnostics(Vec), + ResolveDiagnostics(Vec), + CheckDiagnostics(Vec), + ResolveMetadata { + source: ResourceKernelError, + diagnostics: Vec, + }, + CheckMetadata { + source: ResourceHookError, + diagnostics: Vec, + }, +} + +impl ResourceProgramError { + /// Observations through the failed phase, in original source-phase order. + pub fn diagnostics(&self) -> Option<&[Diagnostic]> { + match self { + Self::ParseDiagnostics(diagnostics) + | Self::ResolveDiagnostics(diagnostics) + | Self::CheckDiagnostics(diagnostics) + | Self::ResolveMetadata { diagnostics, .. } + | Self::CheckMetadata { diagnostics, .. } => Some(diagnostics), + } + } +} + +impl fmt::Display for ResourceProgramError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseDiagnostics(_) => formatter.write_str("resource program has parser errors"), + Self::ResolveDiagnostics(_) => { + formatter.write_str("resource program has resolver errors") + } + Self::CheckDiagnostics(_) => formatter.write_str("resource program has checker errors"), + Self::ResolveMetadata { source, .. } => write!(formatter, "{source}"), + Self::CheckMetadata { source, .. } => write!(formatter, "{source}"), + } + } +} + +impl std::error::Error for ResourceProgramError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ResolveMetadata { source, .. } => Some(source), + Self::CheckMetadata { source, .. } => Some(source), + Self::ParseDiagnostics(_) | Self::ResolveDiagnostics(_) | Self::CheckDiagnostics(_) => { + None + } + } + } +} + +impl CheckedResourceProgram { + /// Resolve and check this owned parse with explicit trusted compiler inputs. + /// Ordinary production callers supply an empty kernel catalog. + pub fn prepare( + parsed: ParseResult, + source_origins: HashMap, + kernels: &[ResourceKernelSpec], + options: CheckOptions, + ) -> Result { + let ParseResult { + module, + errors: parse_diagnostics, + } = parsed; + if has_errors(&parse_diagnostics) { + return Err(ResourceProgramError::ParseDiagnostics(parse_diagnostics)); + } + let resolved = + resolve_with_resource_kernels(&module, &source_origins, kernels).map_err(|source| { + ResourceProgramError::ResolveMetadata { + source, + diagnostics: observed_diagnostics(&parse_diagnostics, &[], &[]), + } + })?; + if has_errors(&resolved.diagnostics) { + return Err(ResourceProgramError::ResolveDiagnostics( + observed_diagnostics(&parse_diagnostics, &resolved.diagnostics, &[]), + )); + } + let checked = + check_with_resource_kernels(&module, &resolved, options).map_err(|source| { + ResourceProgramError::CheckMetadata { + source, + diagnostics: observed_diagnostics( + &parse_diagnostics, + &resolved.diagnostics, + &[], + ), + } + })?; + if has_errors(&checked.diagnostics) { + return Err(ResourceProgramError::CheckDiagnostics( + observed_diagnostics( + &parse_diagnostics, + &resolved.diagnostics, + &checked.diagnostics, + ), + )); + } + validate_resource_hooks(&module, &resolved, &checked).map_err(|source| { + ResourceProgramError::CheckMetadata { + source, + diagnostics: observed_diagnostics( + &parse_diagnostics, + &resolved.diagnostics, + &checked.diagnostics, + ), + } + })?; + Ok(Self { + module, + resolved, + checked, + source_origins, + parse_diagnostics, + }) + } + + pub fn module(&self) -> &Module { + &self.module + } + + pub fn resolved(&self) -> &ResolveResult { + &self.resolved + } + + pub fn checked(&self) -> &CheckResult { + &self.checked + } + + pub fn source_origins(&self) -> &HashMap { + &self.source_origins + } + + pub fn parse_diagnostics(&self) -> &[Diagnostic] { + &self.parse_diagnostics + } +} + +fn observed_diagnostics( + parsed: &[Diagnostic], + resolved: &[Diagnostic], + checked: &[Diagnostic], +) -> Vec { + parsed + .iter() + .chain(resolved) + .chain(checked) + .cloned() + .collect() +} + +fn has_errors(diagnostics: &[Diagnostic]) -> bool { + diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Severity::Error) +} + +#[cfg(test)] +mod tests { + use super::*; + use jett_common::{STDLIB_FILE_ID_START, Span}; + use jett_parser::{ast::Item, parse}; + use jett_types::{ResourceKernelRecipe, Type, TypeInterner}; + + const SOURCE: &str = "namespace resource_probe\nresource TestHandle\ntype HandleAlias = TestHandle\nfunction checked_calls(view net: Network) returns nothing:\n TestHandle first = kernel_create(view net, 1) handle error:\n return nothing\n int64 observed = kernel_borrow(view net, view first) handle error:\n return nothing\n kernel_close(first)\n return nothing\n"; + + fn inputs( + source: &str, + ) -> ( + ParseResult, + HashMap, + Vec, + ) { + let file = FileId::new(STDLIB_FILE_ID_START); + let parsed = parse(source, file); + assert!(!has_errors(&parsed.errors), "{:?}", parsed.errors); + let span = parsed + .module + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource.name.span) + } else { + None + } + }) + .expect("source Resource declaration"); + let kernels = [ + ("kernel_create", ResourceKernelRecipe::NetworkFactory), + ("kernel_borrow", ResourceKernelRecipe::NetworkBorrow), + ("kernel_close", ResourceKernelRecipe::Finalize), + ] + .into_iter() + .map(|(member, recipe)| ResourceKernelSpec { + resource_declaration: span, + member: member.into(), + recipe, + }) + .collect(); + ( + parsed, + HashMap::from([(file, SourceOrigin::Stdlib)]), + kernels, + ) + } + + #[test] + fn retained_resource_program_preserves_exact_private_calls_and_modes_in_both_profiles() { + for release in [false, true] { + let (parsed, origins, kernels) = inputs(SOURCE); + let program = CheckedResourceProgram::prepare( + parsed, + origins, + &kernels, + CheckOptions { release }, + ) + .unwrap(); + assert_eq!(program.checked().release, release); + assert_eq!(program.checked().resource_hooks.len(), 3); + validate_resource_hooks(program.module(), program.resolved(), program.checked()) + .unwrap(); + let declaration = program + .module() + .items + .iter() + .find_map(|item| { + if let Item::Resource(resource) = item { + Some(resource) + } else { + None + } + }) + .unwrap(); + let definition = program.resolved().resolutions[&declaration.name.span]; + let resource_type = program.checked().definition_types[&definition]; + assert!( + matches!(program.checked().interner.resolve(resource_type), Type::Resource(name) if name == "resource_probe.TestHandle") + ); + for hook in program.checked().resource_hooks.values() { + assert_eq!(hook.resource_definition, definition); + assert_eq!(hook.resource_type, resource_type); + assert_eq!( + program.checked().definition_types[&hook.definition], + hook.function_type + ); + } + } + } + + #[test] + fn retained_resource_program_rejects_each_source_error_phase_with_original_diagnostics() { + let file = FileId::new(0); + let malformed = parse("function broken(\n", file); + let original = malformed + .errors + .iter() + .find(|diagnostic| diagnostic.severity == Severity::Error) + .unwrap() + .clone(); + let error = CheckedResourceProgram::prepare( + malformed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap_err(); + assert!(matches!(&error, ResourceProgramError::ParseDiagnostics(_))); + assert!( + error + .diagnostics() + .unwrap() + .iter() + .any(|diagnostic| diagnostic.code == original.code + && diagnostic.span == original.span + && diagnostic.message == original.message) + ); + for (source, phase) in [ + ( + "function inspect() returns nothing:\n missing()\n return nothing\n", + "resolve", + ), + ( + "function inspect() returns nothing:\n int64 broken = true\n return nothing\n", + "check", + ), + ] { + let parsed = parse(source, file); + assert!(!has_errors(&parsed.errors)); + let error = CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap_err(); + assert!( + match (&error, phase) { + (ResourceProgramError::ResolveDiagnostics(_), "resolve") => true, + (ResourceProgramError::CheckDiagnostics(_), "check") => true, + _ => false, + }, + "{error:?}" + ); + assert!(has_errors(error.diagnostics().unwrap())); + if phase == "check" { + assert!( + error + .diagnostics() + .unwrap() + .iter() + .any(|diagnostic| diagnostic.code.code() == 311) + ); + } + } + } + + #[test] + fn retained_resource_program_keeps_warnings_and_empty_catalog_without_hook_authority() { + let file = FileId::new(0); + let mut parsed = parse( + "function kernel_close(value: int64) returns int64:\n return value\n", + file, + ); + let warning = Diagnostic::warning(1, "retained parser observation", Span::new(file, 0, 8)); + parsed.errors.push(warning.clone()); + let program = CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(); + assert_eq!(program.parse_diagnostics().len(), 1); + assert_eq!(program.parse_diagnostics()[0].span, warning.span); + assert_eq!(program.parse_diagnostics()[0].message, warning.message); + assert_eq!(program.parse_diagnostics()[0].severity, Severity::Warning); + assert!(program.resolved().resource_kernels.is_empty()); + assert!(program.checked().resource_hooks.is_empty()); + assert_eq!( + program.source_origins().get(&file), + Some(&SourceOrigin::Project) + ); + } + + #[test] + fn retained_resource_program_keeps_release_policy_at_preparation() { + let file = FileId::new(0); + let source = + "function main() returns nothing:\n println(\"visible\")\n return nothing\n"; + let debug = CheckedResourceProgram::prepare( + parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(); + assert!(!debug.checked().release); + let error = CheckedResourceProgram::prepare( + parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions { release: true }, + ) + .unwrap_err(); + assert!(matches!(&error, ResourceProgramError::CheckDiagnostics(_))); + assert!( + error + .diagnostics() + .unwrap() + .iter() + .any(|diagnostic| diagnostic.code.code() == 362) + ); + } + + #[test] + fn retained_resource_program_validates_unused_catalog_and_rejects_untrusted_origin() { + let source = "namespace resource_probe\nresource TestHandle\n"; + let (parsed, origins, kernels) = inputs(source); + let program = + CheckedResourceProgram::prepare(parsed, origins, &kernels, CheckOptions::default()) + .unwrap(); + assert_eq!( + program.checked().resource_hooks.len(), + 3, + "unused hooks remain fully checked" + ); + let (parsed, mut origins, kernels) = inputs(source); + origins.insert(FileId::new(STDLIB_FILE_ID_START), SourceOrigin::Project); + let error = + CheckedResourceProgram::prepare(parsed, origins, &kernels, CheckOptions::default()) + .unwrap_err(); + assert!(matches!( + error, + ResourceProgramError::ResolveMetadata { + source: ResourceKernelError::UntrustedOrigin(_), + .. + } + )); + let (parsed, origins, mut kernels) = inputs(source); + kernels.push(kernels[0].clone()); + let error = + CheckedResourceProgram::prepare(parsed, origins, &kernels, CheckOptions::default()) + .unwrap_err(); + assert!(matches!( + error, + ResourceProgramError::ResolveMetadata { + source: ResourceKernelError::DuplicateCatalogEntry(_), + .. + } + )); + } + + #[test] + fn retained_resource_program_preserves_generic_scoped_and_named_argument_facts() { + let file = FileId::new(0); + let source = r#"struct User: + name: string + age: int64 +function inspect[T](view value: T) returns nothing: + for field in type.fields[T](): + comptime type Field = field.type_info: + string reflected_name = type.name[Field]() + return nothing +function order(first: int64, second: int64) returns int64: + return first - second +function main() returns int64: + User user = User(name: "Ada", age: 42) + inspect[User](view user) + return order(second: 2, first: 1) +"#; + let program = CheckedResourceProgram::prepare( + parse(source, file), + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(); + let body = program + .checked() + .generic_function_instantiations + .iter() + .find(|body| !body.comptime_type_bindings.is_empty()) + .expect("retained reflected generic body"); + let bindings = body.comptime_type_bindings.values().next().unwrap(); + assert_eq!(bindings.len(), 2); + assert_eq!(bindings[0].bound_type, TypeInterner::STRING); + assert_eq!(bindings[1].bound_type, TypeInterner::INT64); + assert!( + program + .checked() + .call_argument_orders + .values() + .any(|order| order.source_indices == vec![1, 0]) + ); + assert!(!program.checked().type_map.is_empty()); + assert!(!body.type_map.is_empty()); + } + + #[test] + fn retained_resource_program_is_independent_of_mutated_caller_input_copies() { + let (mut parsed, mut origins, mut kernels) = inputs(SOURCE); + let owned_copy = ParseResult { + module: parsed.module.clone(), + errors: parsed.errors.clone(), + }; + let program = CheckedResourceProgram::prepare( + owned_copy, + origins.clone(), + &kernels, + CheckOptions::default(), + ) + .unwrap(); + parsed.module.items.clear(); + parsed.errors.push(Diagnostic::error( + 1, + "later caller mutation", + Span::new(FileId::new(0), 0, 0), + )); + origins.clear(); + kernels[0].member = "different_member".into(); + kernels.clear(); + assert!(!program.module().items.is_empty()); + assert!(program.parse_diagnostics().is_empty()); + assert_eq!( + program + .source_origins() + .get(&FileId::new(STDLIB_FILE_ID_START)), + Some(&SourceOrigin::Stdlib) + ); + assert_eq!(program.checked().resource_hooks.len(), 3); + validate_resource_hooks(program.module(), program.resolved(), program.checked()).unwrap(); + } + + #[test] + fn retained_resource_program_keeps_prior_warnings_before_source_failures() { + let file = FileId::new(0); + let warning = Diagnostic::warning(1, "injected parser observation", Span::new(file, 0, 8)); + for (source, check_failure) in [ + ( + "function inspect() returns nothing:\n missing()\n return nothing\n", + false, + ), + ( + "function inspect() returns nothing:\n int64 retained_warning = 1\n int64 _broken = true\n return nothing\n", + true, + ), + ] { + let mut parsed = parse(source, file); + assert!(!has_errors(&parsed.errors), "{:?}", parsed.errors); + parsed.errors.push(warning.clone()); + // These observations come from a real source resolution, not a fabricated warning. + let expected_resolved = resolve_with_resource_kernels( + &parsed.module, + &HashMap::from([(file, SourceOrigin::Project)]), + &[], + ) + .unwrap(); + let error = CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap_err(); + let diagnostics = error.diagnostics().unwrap(); + assert_eq!(diagnostics[0].code, warning.code); + assert_eq!(diagnostics[0].span, warning.span); + assert_eq!(diagnostics[0].message, warning.message); + for (actual, expected) in diagnostics[1..].iter().zip(&expected_resolved.diagnostics) { + assert_eq!(actual.code, expected.code); + assert_eq!(actual.span, expected.span); + assert_eq!(actual.message, expected.message); + assert_eq!(actual.severity, expected.severity); + } + if check_failure { + assert!(matches!(&error, ResourceProgramError::CheckDiagnostics(_))); + assert_eq!(expected_resolved.diagnostics.len(), 1); + assert_eq!(diagnostics[1].code.code(), 202); + assert_eq!( + diagnostics[1].message, + "unused variable: `retained_warning`" + ); + assert_eq!(diagnostics.len(), 3); + assert_eq!(diagnostics[2].code.code(), 311); + } else { + assert!(matches!( + &error, + ResourceProgramError::ResolveDiagnostics(_) + )); + assert_eq!(diagnostics.len(), 1 + expected_resolved.diagnostics.len()); + } + } + } + + #[test] + fn retained_resource_program_preserves_observations_and_typed_metadata_causes() { + use std::error::Error; + let source = "namespace resource_probe\nresource TestHandle\n"; + for with_warning in [false, true] { + let (mut parsed, mut origins, kernels) = inputs(source); + let file = FileId::new(STDLIB_FILE_ID_START); + let warning = + Diagnostic::warning(1, "injected parser observation", Span::new(file, 0, 8)); + if with_warning { + parsed.errors.push(warning.clone()); + } + origins.insert(file, SourceOrigin::Project); + let error = + CheckedResourceProgram::prepare(parsed, origins, &kernels, CheckOptions::default()) + .unwrap_err(); + let diagnostics = error + .diagnostics() + .expect("metadata failures also retain observations"); + assert_eq!(diagnostics.len(), usize::from(with_warning)); + if with_warning { + assert_eq!(diagnostics[0].code, warning.code); + assert_eq!(diagnostics[0].span, warning.span); + assert_eq!(diagnostics[0].message, warning.message); + } + let ResourceProgramError::ResolveMetadata { source, .. } = &error else { + panic!("wrong metadata phase: {error:?}"); + }; + assert_eq!(error.to_string(), source.to_string()); + assert_eq!(error.source().unwrap().to_string(), source.to_string()); + } + } + + #[test] + fn retained_resource_program_keeps_real_resolver_warnings_on_success() { + let file = FileId::new(0); + let mut parsed = parse( + "function inspect() returns nothing:\n int64 retained_warning = 1\n return nothing\n", + file, + ); + let warning = Diagnostic::warning(1, "injected parser observation", Span::new(file, 0, 8)); + parsed.errors.push(warning.clone()); + let program = CheckedResourceProgram::prepare( + parsed, + HashMap::from([(file, SourceOrigin::Project)]), + &[], + CheckOptions::default(), + ) + .unwrap(); + assert_eq!(program.parse_diagnostics().len(), 1); + assert_eq!(program.parse_diagnostics()[0].message, warning.message); + assert_eq!(program.resolved().diagnostics.len(), 1); + assert_eq!(program.resolved().diagnostics[0].code.code(), 202); + assert_eq!( + program.resolved().diagnostics[0].message, + "unused variable: `retained_warning`" + ); + assert!(program.checked().diagnostics.is_empty()); + } +} diff --git a/crates/jett_types/src/interner.rs b/crates/jett_types/src/interner.rs index 0a3aed42..f1170d6b 100644 --- a/crates/jett_types/src/interner.rs +++ b/crates/jett_types/src/interner.rs @@ -1,10 +1,18 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::defs::{ ActorDef, ActorId, BitfieldDef, BitfieldId, EnumDef, EnumId, InterfaceDef, InterfaceId, MachineDef, MachineId, StructDef, StructId, }; -use crate::types::{Type, TypeId}; +use crate::types::{CapabilityKind, Type, TypeId}; + +/// Invalid registration of canonical arguments for a nominal type instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NominalTypeArgumentsError { + InvalidOwner(TypeId), + InvalidArgument(TypeId), + ConflictingArguments(TypeId), +} /// Type interner that deduplicates types and provides stable [`TypeId`] handles. /// @@ -18,6 +26,8 @@ pub struct TypeInterner { types: Vec, /// User-defined struct definitions. structs: Vec, + /// Exact builtin metadata owners, immutable under Source operations. + compiler_metadata_structs: HashSet, /// User-defined bitfield definitions. bitfields: Vec, /// User-defined enum definitions. @@ -28,6 +38,9 @@ pub struct TypeInterner { actors: Vec, /// User-defined state machine definitions. machines: Vec, + /// Checker-selected arguments for concrete nominal generic instances. + /// Ordinary nominal definitions have no arguments, regardless of fields. + nominal_type_arguments: HashMap>, } // Constant TypeId values for every primitive type. @@ -49,44 +62,103 @@ impl TypeInterner { pub const NOTHING: TypeId = TypeId(13); pub const ERROR: TypeId = TypeId(14); pub const TYPE_CONSTRUCTION: TypeId = TypeId(15); + pub const NEVER: TypeId = TypeId(16); + pub const STDOUT: TypeId = TypeId(17); + pub const STDERR: TypeId = TypeId(18); + pub const STDIN: TypeId = TypeId(19); + pub const FILESYSTEM: TypeId = TypeId(20); + pub const NETWORK: TypeId = TypeId(21); + pub const CLOCK: TypeId = TypeId(22); + pub const RANDOM: TypeId = TypeId(23); + pub const PROCESS: TypeId = TypeId(24); + pub const ENVIRONMENT: TypeId = TypeId(25); + pub const LOG: TypeId = TypeId(26); + pub const GRAPHICS: TypeId = TypeId(27); + + pub const fn capability(kind: CapabilityKind) -> TypeId { + match kind { + CapabilityKind::Stdout => Self::STDOUT, + CapabilityKind::Stderr => Self::STDERR, + CapabilityKind::Stdin => Self::STDIN, + CapabilityKind::Filesystem => Self::FILESYSTEM, + CapabilityKind::Network => Self::NETWORK, + CapabilityKind::Clock => Self::CLOCK, + CapabilityKind::Random => Self::RANDOM, + CapabilityKind::Process => Self::PROCESS, + CapabilityKind::Environment => Self::ENVIRONMENT, + CapabilityKind::Log => Self::LOG, + CapabilityKind::Graphics => Self::GRAPHICS, + } + } } impl TypeInterner { - /// Creates a new interner with all primitive types pre-registered. + /// Copy all type metadata into an independent owned table with the same IDs. + /// This preserves lookup meanings, not checked Source or witness authority. + pub fn snapshot(&self) -> Self { + Self { + map: self.map.clone(), + types: self.types.clone(), + structs: self.structs.clone(), + compiler_metadata_structs: self.compiler_metadata_structs.clone(), + bitfields: self.bitfields.clone(), + enums: self.enums.clone(), + interfaces: self.interfaces.clone(), + actors: self.actors.clone(), + machines: self.machines.clone(), + nominal_type_arguments: self.nominal_type_arguments.clone(), + } + } + + /// Creates a new interner with all compiler-known leaf types pre-registered. pub fn new() -> Self { - let primitives = vec![ - Type::Int8, // 0 - Type::Int16, // 1 - Type::Int32, // 2 - Type::Int64, // 3 - Type::Uint8, // 4 - Type::Uint16, // 5 - Type::Uint32, // 6 - Type::Uint64, // 7 - Type::Float32, // 8 - Type::Float64, // 9 - Type::String, // 10 - Type::Bool, // 11 - Type::Bytes, // 12 - Type::Nothing, // 13 - Type::Error, // 14 - Type::TypeConstruction, // 15 + let builtins = vec![ + Type::Int8, // 0 + Type::Int16, // 1 + Type::Int32, // 2 + Type::Int64, // 3 + Type::Uint8, // 4 + Type::Uint16, // 5 + Type::Uint32, // 6 + Type::Uint64, // 7 + Type::Float32, // 8 + Type::Float64, // 9 + Type::String, // 10 + Type::Bool, // 11 + Type::Bytes, // 12 + Type::Nothing, // 13 + Type::Error, // 14 + Type::TypeConstruction, // 15 + Type::Never, // 16 + Type::Capability(CapabilityKind::Stdout), // 17 + Type::Capability(CapabilityKind::Stderr), // 18 + Type::Capability(CapabilityKind::Stdin), // 19 + Type::Capability(CapabilityKind::Filesystem), // 20 + Type::Capability(CapabilityKind::Network), // 21 + Type::Capability(CapabilityKind::Clock), // 22 + Type::Capability(CapabilityKind::Random), // 23 + Type::Capability(CapabilityKind::Process), // 24 + Type::Capability(CapabilityKind::Environment), // 25 + Type::Capability(CapabilityKind::Log), // 26 + Type::Capability(CapabilityKind::Graphics), // 27 ]; - let mut map = HashMap::with_capacity(primitives.len()); - for (i, ty) in primitives.iter().enumerate() { + let mut map = HashMap::with_capacity(builtins.len()); + for (i, ty) in builtins.iter().enumerate() { map.insert(ty.clone(), TypeId(i as u32)); } Self { - types: primitives, + types: builtins, map, structs: Vec::new(), + compiler_metadata_structs: HashSet::new(), bitfields: Vec::new(), enums: Vec::new(), interfaces: Vec::new(), actors: Vec::new(), machines: Vec::new(), + nominal_type_arguments: HashMap::new(), } } @@ -102,6 +174,45 @@ impl TypeInterner { id } + /// Record the canonical arguments selected when the checker instantiates a + /// generic nominal declaration. Structs are the only current generic + /// nominal kind. Repeating the same facts is harmless; they cannot change. + pub fn register_nominal_type_arguments( + &mut self, + owner: TypeId, + arguments: Vec, + ) -> Result<(), NominalTypeArgumentsError> { + if !matches!( + self.types.get(owner.index() as usize), + Some(Type::Struct(_)) + ) { + return Err(NominalTypeArgumentsError::InvalidOwner(owner)); + } + if let Some(argument) = arguments + .iter() + .find(|argument| argument.index() as usize >= self.types.len()) + { + return Err(NominalTypeArgumentsError::InvalidArgument(*argument)); + } + if let Some(existing) = self.nominal_type_arguments.get(&owner) { + return if existing == &arguments { + Ok(()) + } else { + Err(NominalTypeArgumentsError::ConflictingArguments(owner)) + }; + } + self.nominal_type_arguments.insert(owner, arguments); + Ok(()) + } + + /// Borrow the canonical arguments of a nominal generic instance. Plain + /// nominal definitions have no arguments; their fields are not arguments. + pub fn nominal_type_arguments(&self, owner: TypeId) -> &[TypeId] { + self.nominal_type_arguments + .get(&owner) + .map_or(&[], Vec::as_slice) + } + /// Look up a type by its [`TypeId`]. /// /// # Panics @@ -116,6 +227,24 @@ impl TypeInterner { (0..self.types.len()).map(|index| TypeId(index as u32)) } + /// Whether this nominal header addresses an existing definition and state. + /// This is bounds validation only, never Source or ownership authority. + pub fn contains_nominal_definition(&self, ty: &Type) -> bool { + match ty { + Type::Struct(id) => (id.index() as usize) < self.structs.len(), + Type::Bitfield(id) => (id.index() as usize) < self.bitfields.len(), + Type::Enum(id) => (id.index() as usize) < self.enums.len(), + Type::Interface(id) => (id.index() as usize) < self.interfaces.len(), + Type::Actor(id) => (id.index() as usize) < self.actors.len(), + Type::Machine(id) => (id.index() as usize) < self.machines.len(), + Type::MachineState { machine, state } => self + .machines + .get(machine.index() as usize) + .is_some_and(|definition| (state.index() as usize) < definition.states.len()), + _ => true, + } + } + /// Register a new struct definition and return its [`StructId`]. pub fn add_struct(&mut self, def: StructDef) -> StructId { let id = StructId(self.structs.len() as u32); @@ -123,6 +252,20 @@ impl TypeInterner { id } + /// Allocate a compiler-produced metadata owner with a permanent exact ID. + /// Only compiler builtin registration uses this operation; ordinary Source + /// structs and user lookalikes continue through `add_struct`. + pub fn add_compiler_metadata_struct(&mut self, definition: StructDef) -> StructId { + let id = self.add_struct(definition); + self.compiler_metadata_structs.insert(id); + id + } + + /// This is identity policy, independent of names, aliases and field shape. + pub fn is_compiler_metadata_struct(&self, id: StructId) -> bool { + self.compiler_metadata_structs.contains(&id) + } + /// Look up a struct definition by its [`StructId`]. /// /// # Panics @@ -244,6 +387,8 @@ impl TypeInterner { Type::Bytes => "bytes".to_string(), Type::Nothing => "nothing".to_string(), Type::TypeConstruction => "TypeConstruction".to_string(), + Type::Never => "".to_string(), + Type::Capability(kind) => kind.name().to_string(), Type::List(inner) => format!("list[{}]", self.type_name(*inner)), Type::Map(k, v) => format!("map[{}, {}]", self.type_name(*k), self.type_name(*v)), Type::Set(inner) => format!("set[{}]", self.type_name(*inner)), @@ -269,9 +414,17 @@ impl TypeInterner { Type::Refinement { name, .. } => name.clone(), Type::Function { params, + view_params, return_type, } => { - let params: Vec = params.iter().map(|p| self.type_name(*p)).collect(); + let params: Vec = params + .iter() + .zip(view_params) + .map(|(param, view)| { + let name = self.type_name(*param); + if *view { format!("view {name}") } else { name } + }) + .collect(); format!( "function({}) returns {}", params.join(", "), @@ -304,6 +457,133 @@ mod tests { use super::*; use crate::defs::{MachineDef, MachineStateDef, MachineTransitionDef, VariantDef}; + #[test] + fn snapshot_preserves_nominal_refinement_instantiation_lookups_and_is_independent() { + let mut original = TypeInterner::new(); + let positive = original.intern(Type::Refinement { + name: "models.Positive".into(), + base: TypeInterner::INT64, + }); + let record = original.add_struct(StructDef { + name: "models.Record[models.Positive]".into(), + fields: vec![("value".into(), positive)], + methods: Vec::new(), + }); + let record_type = original.intern(Type::Struct(record)); + original + .register_nominal_type_arguments(record_type, vec![positive]) + .unwrap(); + let bits = original.add_bitfield(BitfieldDef { + name: "models.Bits".into(), + network_order: true, + fields: vec![crate::defs::BitfieldFieldDef { + name: "flag".into(), + ty: TypeInterner::UINT8, + kind: crate::defs::BitfieldFieldKind::Bits { width: 8 }, + }], + }); + original.intern(Type::Bitfield(bits)); + let choice = original.add_enum(EnumDef { + name: "models.Choice".into(), + variants: vec![VariantDef { + name: "value".into(), + fields: vec![("record".into(), record_type)], + discriminant: 3, + }], + }); + original.intern(Type::Enum(choice)); + let interface = original.add_interface(InterfaceDef { + name: "models.Reader".into(), + methods: vec![crate::defs::FunctionSig { + name: "read".into(), + params: vec![("value".into(), positive, true)], + return_type: record_type, + is_pure: true, + }], + }); + original.intern(Type::Interface(interface)); + let actor = original.add_actor(ActorDef { + name: "models.Worker".into(), + capability_params: vec![("network".into(), TypeInterner::NETWORK)], + state_fields: vec![("record".into(), record_type)], + messages: vec![crate::defs::ActorMessageDef { + name: "read".into(), + params: vec![("value".into(), positive)], + responds: record_type, + }], + }); + original.intern(Type::Actor(actor)); + let machine = original.add_machine(MachineDef { + name: "models.Session".into(), + states: vec![MachineStateDef { + name: "ready".into(), + fields: vec![("value".into(), positive)], + }], + transitions: vec![MachineTransitionDef { + from: crate::defs::MachineStateId::new(0), + to: crate::defs::MachineStateId::new(0), + }], + }); + original.intern(Type::Machine(machine)); + original.intern(Type::MachineState { + machine, + state: crate::defs::MachineStateId::new(0), + }); + let callable = original.intern(Type::Function { + params: vec![record_type], + view_params: vec![true], + return_type: positive, + }); + + let mut snapshot = original.snapshot(); + assert_eq!(snapshot.len(), original.len()); + for ty in original.type_ids() { + assert_eq!(snapshot.resolve(ty), original.resolve(ty)); + assert_eq!(snapshot.intern(original.resolve(ty).clone()), ty); + assert_eq!( + snapshot.nominal_type_arguments(ty), + original.nominal_type_arguments(ty) + ); + assert!(snapshot.contains_nominal_definition(original.resolve(ty))); + } + assert_eq!( + snapshot.resolve_struct(record), + original.resolve_struct(record) + ); + assert_eq!( + snapshot.resolve_bitfield(bits), + original.resolve_bitfield(bits) + ); + assert_eq!(snapshot.resolve_enum(choice), original.resolve_enum(choice)); + assert_eq!( + snapshot.resolve_interface(interface), + original.resolve_interface(interface) + ); + assert_eq!(snapshot.resolve_actor(actor), original.resolve_actor(actor)); + assert_eq!( + snapshot.resolve_machine(machine), + original.resolve_machine(machine) + ); + assert_eq!(snapshot.type_name(callable), original.type_name(callable)); + assert_eq!(snapshot.nominal_type_arguments(record_type), &[positive]); + + let original_length = original.len(); + let added = snapshot.intern(Type::List(TypeInterner::BOOL)); + assert_eq!(added.index() as usize, original_length); + assert_eq!(original.len(), original_length); + let mut changed = snapshot.resolve_struct(record).clone(); + changed.fields[0].1 = TypeInterner::STRING; + snapshot.update_struct(record, changed); + assert_eq!(original.resolve_struct(record).fields[0].1, positive); + let mut changed = original.resolve_machine(machine).clone(); + changed.states[0].fields[0].1 = TypeInterner::INT64; + original.update_machine(machine, changed); + assert_eq!( + snapshot.resolve_machine(machine).states[0].fields[0].1, + positive + ); + } + // -- Primitive constant IDs ----------------------------------------------- #[test] @@ -328,6 +608,23 @@ mod tests { *interner.resolve(TypeInterner::TYPE_CONSTRUCTION), Type::TypeConstruction ); + assert_eq!(TypeInterner::NEVER.index(), 16); + assert_eq!(*interner.resolve(TypeInterner::NEVER), Type::Never); + } + + #[test] + fn capability_type_ids_are_stable_and_distinct() { + let interner = TypeInterner::new(); + let ids = CapabilityKind::ALL.map(TypeInterner::capability); + + assert_eq!( + ids.map(TypeId::index), + [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27] + ); + for (kind, id) in CapabilityKind::ALL.into_iter().zip(ids) { + assert_eq!(*interner.resolve(id), Type::Capability(kind)); + assert_eq!(interner.type_name(id), kind.name()); + } } // -- Basic interning ------------------------------------------------------ @@ -429,10 +726,12 @@ mod tests { let mut interner = TypeInterner::new(); let fn_type = interner.intern(Type::Function { params: vec![TypeInterner::INT64, TypeInterner::STRING], + view_params: vec![false, false], return_type: TypeInterner::BOOL, }); let fn_type_again = interner.intern(Type::Function { params: vec![TypeInterner::INT64, TypeInterner::STRING], + view_params: vec![false, false], return_type: TypeInterner::BOOL, }); assert_eq!(fn_type, fn_type_again); @@ -440,9 +739,21 @@ mod tests { // Different param order -> different type let fn_different = interner.intern(Type::Function { params: vec![TypeInterner::STRING, TypeInterner::INT64], + view_params: vec![false, false], return_type: TypeInterner::BOOL, }); assert_ne!(fn_type, fn_different); + + let fn_view = interner.intern(Type::Function { + params: vec![TypeInterner::INT64, TypeInterner::STRING], + view_params: vec![false, true], + return_type: TypeInterner::BOOL, + }); + assert_ne!(fn_type, fn_view); + assert_eq!( + interner.type_name(fn_view), + "function(int64, view string) returns bool" + ); } // -- Struct and enum definitions ------------------------------------------ @@ -472,6 +783,84 @@ mod tests { assert_eq!(*interner.resolve(struct_type_id), Type::Struct(sid)); } + #[test] + fn nominal_arguments_are_explicit_immutable_and_independent_of_fields() { + let mut interner = TypeInterner::new(); + let positive = interner.intern(Type::Refinement { + name: "models.Positive".into(), + base: TypeInterner::INT64, + }); + let element = interner.add_struct(StructDef { + name: "models.Element".into(), + fields: vec![("value".into(), positive)], + methods: Vec::new(), + }); + let element = interner.intern(Type::Struct(element)); + let marker = interner.add_struct(StructDef { + name: "models.Marker[models.Positive]".into(), + fields: vec![("value".into(), TypeInterner::INT64)], + methods: Vec::new(), + }); + let marker = interner.intern(Type::Struct(marker)); + assert!(interner.nominal_type_arguments(element).is_empty()); + assert!(interner.nominal_type_arguments(marker).is_empty()); + interner + .register_nominal_type_arguments(marker, vec![positive]) + .unwrap(); + interner + .register_nominal_type_arguments(marker, vec![positive]) + .unwrap(); + assert_eq!(interner.nominal_type_arguments(marker), [positive]); + assert!(interner.nominal_type_arguments(element).is_empty()); + assert_eq!( + interner.register_nominal_type_arguments(marker, vec![TypeInterner::STRING]), + Err(NominalTypeArgumentsError::ConflictingArguments(marker)) + ); + assert_eq!(interner.nominal_type_arguments(marker), [positive]); + let Type::Struct(id) = interner.resolve(marker) else { + panic!("marker"); + }; + assert_eq!( + interner.resolve_struct(*id).fields[0].1, + TypeInterner::INT64 + ); + } + + #[test] + fn nominal_argument_registration_rejects_unknown_ids_without_mutation() { + let mut interner = TypeInterner::new(); + let marker = interner.add_struct(StructDef { + name: "Marker".into(), + fields: Vec::new(), + methods: Vec::new(), + }); + let marker = interner.intern(Type::Struct(marker)); + let unknown = TypeId(interner.len() as u32); + for owner in [unknown, TypeInterner::INT64] { + assert_eq!( + interner.register_nominal_type_arguments(owner, vec![TypeInterner::INT64]), + Err(NominalTypeArgumentsError::InvalidOwner(owner)) + ); + assert!(interner.nominal_type_arguments(owner).is_empty()); + } + assert_eq!( + interner.register_nominal_type_arguments(marker, vec![unknown]), + Err(NominalTypeArgumentsError::InvalidArgument(unknown)) + ); + assert!(interner.nominal_type_arguments(marker).is_empty()); + interner + .register_nominal_type_arguments(marker, vec![TypeInterner::INT64]) + .unwrap(); + assert_eq!( + interner.register_nominal_type_arguments(marker, vec![unknown]), + Err(NominalTypeArgumentsError::InvalidArgument(unknown)) + ); + assert_eq!( + interner.nominal_type_arguments(marker), + [TypeInterner::INT64] + ); + } + #[test] fn bitfield_definition_storage() { let mut interner = TypeInterner::new(); @@ -622,9 +1011,9 @@ mod tests { #[test] fn interner_len_includes_primitives() { let interner = TypeInterner::new(); - // 16 built-in leaves: int8..uint64 (8) + float32/64 (2) + - // string, bool, bytes, nothing, error, TypeConstruction (6) - assert_eq!(interner.len(), 16); + // 16 existing built-in leaves, the internal bottom type, and the + // closed set of 11 nominal capabilities. + assert_eq!(interner.len(), 28); } #[test] @@ -638,3 +1027,6 @@ mod tests { assert_eq!(interner.len(), before + 1); } } + +#[cfg(test)] +mod compiler_metadata_tests; diff --git a/crates/jett_types/src/interner/compiler_metadata_tests.rs b/crates/jett_types/src/interner/compiler_metadata_tests.rs new file mode 100644 index 00000000..c935d1ca --- /dev/null +++ b/crates/jett_types/src/interner/compiler_metadata_tests.rs @@ -0,0 +1,41 @@ +//! Candidate identity tests; not executed in this preparation phase. + +use super::*; + +fn record(name: &str) -> StructDef { + StructDef { + name: name.to_owned(), + fields: vec![("index".to_owned(), TypeInterner::INT64)], + methods: Vec::new(), + } +} + +#[test] +fn metadata_classification_uses_allocation_identity_not_spelling_or_shape() { + let mut types = TypeInterner::new(); + let builtin = types.add_compiler_metadata_struct(record("TypeField")); + let lookalike = types.add_struct(record("TypeField")); + assert_ne!(builtin, lookalike); + assert!(types.is_compiler_metadata_struct(builtin)); + assert!(!types.is_compiler_metadata_struct(lookalike)); + assert_eq!( + types.resolve_struct(builtin), + types.resolve_struct(lookalike) + ); +} + +#[test] +fn snapshots_preserve_exact_metadata_markers_and_independent_table_growth() { + let mut original = TypeInterner::new(); + let builtin = original.add_compiler_metadata_struct(record("TypeField")); + let lookalike = original.add_struct(record("TypeField")); + let mut snapshot = original.snapshot(); + assert!(snapshot.is_compiler_metadata_struct(builtin)); + assert!(!snapshot.is_compiler_metadata_struct(lookalike)); + snapshot.update_struct(builtin, record("RenamedByCompilerTransform")); + assert!(snapshot.is_compiler_metadata_struct(builtin)); + assert_eq!(original.resolve_struct(builtin).name, "TypeField"); + let new_user = snapshot.add_struct(record("TypeField")); + assert!(!snapshot.is_compiler_metadata_struct(new_user)); + assert!(!original.is_compiler_metadata_struct(new_user)); +} diff --git a/crates/jett_types/src/lib.rs b/crates/jett_types/src/lib.rs index ebfd869d..7013829a 100644 --- a/crates/jett_types/src/lib.rs +++ b/crates/jett_types/src/lib.rs @@ -3,6 +3,7 @@ mod defs; mod interner; mod reflection; +mod resource_hooks; mod types; pub use defs::{ @@ -10,10 +11,12 @@ pub use defs::{ BitfieldId, EnumDef, EnumId, FunctionSig, InterfaceDef, InterfaceId, MachineDef, MachineId, MachineStateDef, MachineStateId, MachineTransitionDef, StructDef, StructId, VariantDef, }; -pub use interner::TypeInterner; +pub use interner::{NominalTypeArgumentsError, TypeInterner}; pub use reflection::{ ReflectionBitfieldFieldInfo, ReflectionBitfieldInfo, ReflectionFieldInfo, ReflectionMachineInfo, ReflectionMachineStateInfo, ReflectionMachineTransitionInfo, ReflectionMetadata, ReflectionTypeInfo, ReflectionVariantInfo, }; -pub use types::{Type, TypeId}; +pub use types::{CapabilityKind, Type, TypeId}; + +pub use resource_hooks::{ResourceHookKind, ResourceKernelRecipe}; diff --git a/crates/jett_types/src/reflection.rs b/crates/jett_types/src/reflection.rs index 8e40e4c9..25e4b896 100644 --- a/crates/jett_types/src/reflection.rs +++ b/crates/jett_types/src/reflection.rs @@ -6,6 +6,8 @@ use crate::TypeId; /// comptime reflection builtins. #[derive(Debug, Clone, Default)] pub struct ReflectionMetadata { + source_type_ids: HashMap, + source_type_infos: HashMap, type_ids_by_name: HashMap, type_infos_by_id: HashMap, type_fields_by_id: HashMap>, @@ -65,6 +67,27 @@ impl ReflectionMetadata { self.type_infos_by_id.get(&type_id) } + /// Source-visible metadata without replacing an alias by another name + /// sharing its canonical type ID. + pub fn get_source_type_info(&self, type_name: &str) -> Option<&ReflectionTypeInfo> { + self.source_type_infos + .get(type_name) + .or_else(|| self.type_infos.get(type_name)) + } + + /// Retain a checked reflection operand without changing canonical lookup. + pub fn insert_source_type_info(&mut self, ty: TypeId, info: ReflectionTypeInfo) { + self.source_type_ids.insert(info.type_name.clone(), ty); + self.source_type_infos.insert(info.type_name.clone(), info); + } + + pub fn source_type_id_for_name(&self, type_name: &str) -> Option { + self.source_type_ids + .get(type_name) + .copied() + .or_else(|| self.type_id_for_name(type_name)) + } + pub fn get_type_info(&self, type_name: &str) -> Option<&ReflectionTypeInfo> { if let Some(type_id) = self.type_id_for_name(type_name) { return self.get_type_info_for_id(type_id); @@ -210,7 +233,7 @@ impl ReflectionMetadata { } /// Canonical metadata for `TypeInfo`. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ReflectionTypeInfo { pub type_name: String, pub kind: String, @@ -220,6 +243,67 @@ pub struct ReflectionTypeInfo { } impl ReflectionTypeInfo { + /// Structural value identity for compiler-produced `TypeInfo` values. + /// Source aliases are transparent; reflection-specialized dispatch uses + /// `reflection_identity` instead. + pub fn canonical_identity(&self) -> String { + self.identity(false) + } + + /// Identity for bodies specialized by source-visible reflection facts. + pub fn reflection_identity(&self) -> String { + self.identity(true) + } + + fn identity(&self, preserve_aliases: bool) -> String { + if !preserve_aliases && self.kind == "alias" && self.args.len() == 1 { + return self.args[0].identity(false); + } + let name = match self.kind.as_str() { + "list" | "set" | "map" | "optional" | "result" | "secret" | "function" => "", + "struct" if !self.args.is_empty() => self + .type_name + .split_once('[') + .map_or(self.type_name.as_str(), |(base, _)| base), + _ => &self.type_name, + }; + let mut identity = format!( + "{}:{}{}:{}{}:{}", + self.kind.len(), + self.kind, + name.len(), + name, + u8::from(self.has_secret), + self.args.len(), + ); + for argument in &self.args { + identity.push_str(&argument.identity(preserve_aliases)); + } + identity + } + + pub fn kind_tag_variant(kind: &str) -> &'static str { + match kind { + "primitive" => "primitive_type", + "alias" => "alias_type", + "refinement" => "refinement_type", + "struct" => "struct_type", + "bitfield" => "bitfield_type", + "enum" => "enum_type", + "list" => "list_type", + "set" => "set_type", + "map" => "map_type", + "optional" => "optional_type", + "result" => "result_type", + "secret" => "secret_type", + "function" => "function_type", + "machine" => "machine_type", + "machine_state" => "machine_state_type", + "resource" => "resource_type", + _ => "unknown_type", + } + } + pub fn new( type_name: impl Into, kind: impl Into, @@ -449,6 +533,28 @@ mod tests { ) } + #[test] + fn canonical_identity_unwraps_aliases_inside_generic_types() { + let string = info("string", "primitive"); + let alias = + ReflectionTypeInfo::new("app.Label", "alias", None, false, vec![string.clone()]); + let canonical = ReflectionTypeInfo::new("list[string]", "list", None, false, vec![string]); + let with_alias = + ReflectionTypeInfo::new("list[app.Label]", "list", None, false, vec![alias]); + assert_eq!( + canonical.canonical_identity(), + with_alias.canonical_identity() + ); + assert_ne!( + canonical.reflection_identity(), + with_alias.reflection_identity() + ); + assert_ne!( + info("app.First", "struct").canonical_identity(), + info("app.Second", "struct").canonical_identity(), + ); + } + #[test] fn type_id_lookup_preserves_legacy_string_lookup() { let type_id = TypeId(42); diff --git a/crates/jett_types/src/resource_hooks.rs b/crates/jett_types/src/resource_hooks.rs new file mode 100644 index 00000000..7291ae3a --- /dev/null +++ b/crates/jett_types/src/resource_hooks.rs @@ -0,0 +1,110 @@ +use crate::{Type, TypeId, TypeInterner}; + +/// Closed backend-neutral role of a checked private resource declaration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ResourceHookKind { + Construct, + BorrowOperation, + Close, +} + +/// Compiler catalog signature recipes, not source names or public provider APIs. +/// +/// No production catalog is installed yet. These three recipes establish the +/// checked identity substrate using deterministic compiler test declarations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ResourceKernelRecipe { + NetworkFactory, + NetworkBorrow, + Finalize, +} + +impl ResourceKernelRecipe { + pub const fn kind(self) -> ResourceHookKind { + match self { + Self::NetworkFactory => ResourceHookKind::Construct, + Self::NetworkBorrow => ResourceHookKind::BorrowOperation, + Self::Finalize => ResourceHookKind::Close, + } + } + + pub fn parameter_names(self) -> &'static [&'static str] { + match self { + Self::NetworkFactory => &["net", "label"], + Self::NetworkBorrow => &["net", "resource"], + Self::Finalize => &["resource"], + } + } + + /// Derive a signature entirely in the associated checker's interner. + pub fn intern_signature(self, types: &mut TypeInterner, resource: TypeId) -> Option { + if resource.index() as usize >= types.len() + || !matches!(types.resolve(resource), Type::Resource(_)) + { + return None; + } + let (params, view_params, return_type) = match self { + Self::NetworkFactory => ( + vec![TypeInterner::NETWORK, TypeInterner::INT64], + vec![true, false], + types.intern(Type::Result(resource, TypeInterner::STRING)), + ), + Self::NetworkBorrow => ( + vec![TypeInterner::NETWORK, resource], + vec![true, true], + types.intern(Type::Result(TypeInterner::INT64, TypeInterner::STRING)), + ), + Self::Finalize => (vec![resource], vec![false], TypeInterner::NOTHING), + }; + Some(types.intern(Type::Function { + params, + view_params, + return_type, + })) + } + + /// Exact shape/mode check; carrier equality or a matching name is insufficient. + pub fn matches_signature( + self, + types: &TypeInterner, + resource: TypeId, + function: TypeId, + ) -> bool { + if resource.index() as usize >= types.len() + || function.index() as usize >= types.len() + || !matches!(types.resolve(resource), Type::Resource(_)) + { + return false; + } + let Type::Function { + params, + view_params, + return_type, + } = types.resolve(function) + else { + return false; + }; + if return_type.index() as usize >= types.len() { + return false; + } + match self { + Self::NetworkFactory => { + params.as_slice() == [TypeInterner::NETWORK, TypeInterner::INT64] + && view_params.as_slice() == [true, false] + && matches!(types.resolve(*return_type), Type::Result(value, error) + if *value == resource && *error == TypeInterner::STRING) + } + Self::NetworkBorrow => { + params.as_slice() == [TypeInterner::NETWORK, resource] + && view_params.as_slice() == [true, true] + && matches!(types.resolve(*return_type), Type::Result(value, error) + if *value == TypeInterner::INT64 && *error == TypeInterner::STRING) + } + Self::Finalize => { + params.as_slice() == [resource] + && view_params.as_slice() == [false] + && *return_type == TypeInterner::NOTHING + } + } + } +} diff --git a/crates/jett_types/src/types.rs b/crates/jett_types/src/types.rs index a7dbaa3c..3ca96dd1 100644 --- a/crates/jett_types/src/types.rs +++ b/crates/jett_types/src/types.rs @@ -1,5 +1,73 @@ use crate::defs::{ActorId, BitfieldId, EnumId, InterfaceId, MachineId, MachineStateId, StructId}; +/// One compiler-known capability type. +/// +/// Capabilities are a closed nominal set: equal names identify equal authority, +/// while different variants must never unify merely because both are opaque. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CapabilityKind { + Stdout, + Stderr, + Stdin, + Filesystem, + Network, + Clock, + Random, + Process, + Environment, + Log, + Graphics, +} + +impl CapabilityKind { + pub const ALL: [Self; 11] = [ + Self::Stdout, + Self::Stderr, + Self::Stdin, + Self::Filesystem, + Self::Network, + Self::Clock, + Self::Random, + Self::Process, + Self::Environment, + Self::Log, + Self::Graphics, + ]; + + pub const fn name(self) -> &'static str { + match self { + Self::Stdout => "Stdout", + Self::Stderr => "Stderr", + Self::Stdin => "Stdin", + Self::Filesystem => "Filesystem", + Self::Network => "Network", + Self::Clock => "Clock", + Self::Random => "Random", + Self::Process => "Process", + Self::Environment => "Environment", + Self::Log => "Log", + Self::Graphics => "Graphics", + } + } + + pub fn from_name(name: &str) -> Option { + match name { + "Stdout" => Some(Self::Stdout), + "Stderr" => Some(Self::Stderr), + "Stdin" => Some(Self::Stdin), + "Filesystem" => Some(Self::Filesystem), + "Network" => Some(Self::Network), + "Clock" => Some(Self::Clock), + "Random" => Some(Self::Random), + "Process" => Some(Self::Process), + "Environment" => Some(Self::Environment), + "Log" => Some(Self::Log), + "Graphics" => Some(Self::Graphics), + _ => None, + } + } +} + /// A unique handle to an interned type. Cheap to copy and compare. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct TypeId(pub(crate) u32); @@ -38,6 +106,10 @@ pub enum Type { Bytes, Nothing, TypeConstruction, + /// Compiler-internal uninhabited bottom type. + Never, + /// An opaque runtime-provided authority with nominal identity. + Capability(CapabilityKind), // -- Generic built-in types ---------------------------------------------- /// `list[T]` @@ -75,9 +147,11 @@ pub enum Type { }, // -- Function type ------------------------------------------------------- - /// `function(T, U) returns V` + /// `function(T, view U) returns V`. Parameter ownership modes are part + /// of the function value's checked signature. Function { params: Vec, + view_params: Vec, return_type: TypeId, }, diff --git a/docs/README.md b/docs/README.md index 7657e82a..e28b147b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ canonical project overview: These notes describe live implementation and architecture staging: +- [Native code generation parity](completed/native_codegen/native_codegen_parity_plan.md) - [Frontend syntax tree staging](active/frontend_syntax_tree_staging.md) - [Ownership and value semantics](active/ownership_value_semantics.md) - [JSON stdlib extraction plan](active/stdlib_json_extraction_plan.md) diff --git a/docs/active/graphics_game_contract.md b/docs/active/graphics_game_contract.md index 900be163..d7d2c6a8 100644 --- a/docs/active/graphics_game_contract.md +++ b/docs/active/graphics_game_contract.md @@ -33,6 +33,11 @@ apply to direct and pipeline calls, with explicit or inferred generic arguments. Callback call-graph auditing waits until module typechecking has resolved method dispatch, so legal `mutual` declaration order cannot conceal an effectful helper. +The borrowed display must be ready when entering the graphics kernel. A pending +`Graphics` capability forwarded through a helper is a terminal runtime error +before configuration validation or provider access. A partial join remains +pending; complete joins restore the existing authority without an implicit join. + `State` is concrete data: primitive values, structs, enums, and supported collections containing only concrete data. Function values, capabilities, resources, actors, and interfaces are rejected recursively, including behind @@ -80,8 +85,9 @@ its caller thread instead of spawning and joining an interpreter worker. Library callers must use the main thread; the host returns a failure before window creation if they do not. Scripted graphics and non-graphics runs retain the ordinary worker-thread path. This first API has no animation tick, frame-clock -promise, audio, mouse input, texture loading, asynchronous callbacks, or native -executable output. Nested sessions are rejected. +promise, audio, mouse input, texture loading, or asynchronous callbacks. Native +executables use the same synchronous session contract. Nested sessions are +rejected. ## Rendering and validation @@ -98,9 +104,18 @@ scenes contain at most 10,000 rectangles and 1,024 text items. Each text item contains at most 16,384 UTF-8 bytes. Window titles contain at most 256 UTF-8 bytes and no NUL. Validate before opening a window or presenting a scene, and use checked coordinate arithmetic. Invalid input and host creation/presentation failures -return `fail(string)`. Callback runtime errors remain interpreter errors; both +return `fail(string)`. Callback runtime errors remain terminal errors; both error routes still close the host window. +Decode the entire configuration or scene before validating its domain bounds. +Records and their fields must be ready, including strings, Scene list containers, +and each rectangle/text element. Pending values cause terminal shape errors in +declaration order; they are never implicitly joined. Scene decoding checks its +background, all rectangles, then all text. No window opens for an invalid +configuration or initial scene, and no invalid later scene is presented. +Callback state may itself be pending and retains its depth across callbacks; +that does not permit pending configuration or scene data. + ## Verification Check invalid dimensions/colors/geometry and clipped rendering without a display. diff --git a/docs/active/hir_lowering_plan.md b/docs/active/hir_lowering_plan.md index b234257e..8535d650 100644 --- a/docs/active/hir_lowering_plan.md +++ b/docs/active/hir_lowering_plan.md @@ -18,13 +18,17 @@ stored in an in-memory HIR program, but canonical declaration identity is: ```text DeclarationId = SourceOrigin + CanonicalNamespace + CanonicalName + DeclarationKind -FunctionIdentity = DeclarationId + ConcreteTypeArguments +FunctionIdentity = DeclarationId + ConcreteTypeArguments + CheckedSpecializationFacts ``` The current in-memory `FunctionIdentity` uses concrete `TypeId` arguments and -is stable only inside one checked program. Persistent caches, linkage names, -and serialized HIR must replace raw `TypeId` values with canonical structural -type identities; they may not persist interner indices. +checker-owned reflection-visible specialization facts; it is stable only +inside one checked program. The specialization facts distinguish bodies such +as an alias and its underlying type even when their concrete `TypeId` +arguments are equal. Persistent caches, linkage names, and serialized HIR must +replace raw `TypeId` values with canonical structural type identities and +encode the specialization discriminator deterministically; they may not +persist interner indices. `SourceOrigin` is supplied explicitly for every source file. HIR never infers authority from `FileId`, path spelling, namespace spelling, or a reserved @@ -58,13 +62,17 @@ redefine HIR language policy. The next typechecker handoff is an ordered checked-instantiation manifest. Each entry identifies the generic declaration by session-local `DefId`, lists -canonical concrete `TypeId` arguments, resolved parameter and return types, -and preserves per-instantiation expression-type facts for body lowering. - -The canonical HIR identity is declaration plus concrete type arguments only. -Call-site reflection facts can authorize conservative checking but do not -create alternate function identities. Facts derived directly from a concrete -type may specialize HIR; arbitrary caller values remain runtime values. +canonical concrete `TypeId` arguments, checker-owned reflection-visible +specialization facts, and resolved parameter and return types. It also +preserves per-instantiation expression types and compile-time control-flow +selections for body lowering. + +Canonical HIR identity includes the declaration, concrete type arguments, and +the checked specialization facts whenever source-visible kind or propagated +reflection facts select a different concrete body. This prevents an alias and +its underlying type, or two calls with different checked reflection facts, +from merging after type interning. Arbitrary unknown or runtime values remain +runtime values and never enter function identity. Discovery begins at non-generic roots and accepted explicit or inferred generic calls. It is deterministic, deduplicates repeated instantiations, and diff --git a/docs/active/llm_language_benchmark.md b/docs/active/llm_language_benchmark.md index 44d70afe..1c53c5fd 100644 --- a/docs/active/llm_language_benchmark.md +++ b/docs/active/llm_language_benchmark.md @@ -59,6 +59,16 @@ API cost, latency, tool invocations, repair count, source lines/bytes, a simple branch-count complexity proxy, and grader runtime. Runtime is never folded into LLM correctness. +Future repair-loop measurements should report the full attempt, from initial +generation through diagnostics and any permitted repair to final validation. +Record total elapsed time, cumulative tokens, tool invocations, and repair +count, alongside time spent checking code. Keep failed and budget-exhausted +attempts in the results so efficiency is not reported only for successes. +Compare treatments under the same task, model, context, and repair budgets; +compiler speed alone does not measure agent repair efficiency. These are +reporting requirements for future runs, not measurements retroactively inferred +from historical campaigns. + Every result records the task version, language adapter version, prompt hash, model identifier, requested reasoning level, track, repetition, toolchain versions, git revision, and raw response identifier. Cost is derived from a diff --git a/docs/active/native_remaining_gaps.md b/docs/active/native_remaining_gaps.md new file mode 100644 index 00000000..037b860b --- /dev/null +++ b/docs/active/native_remaining_gaps.md @@ -0,0 +1,57 @@ +# Native follow-ups and language decisions for review + +Native parity for this landing means the five fixture gates in +`tests/native_parity.json` passing on Windows MSVC at one committed revision. +The items below are follow-ups, not additional landing gates. Whole-language +parity and percentage estimates are retired. No work from this list starts here. + +## Open native follow-ups from the coverage matrix + +- Results/optionals: retain an alternate owned outer `default` for a borrowed payload until its lexical scope exits; fixture: `tests/native/borrowed_sum_outer_default.jett` (proposed). +- Results/refinements: compare a refinement constructor with a handled operand nested inside a binary expression; fixture: `tests/native/refined_handle_nested_operand.jett` (proposed). +- Projected collection views: preserve a borrowed field across an interface conversion and a later argument's handled failure; fixture: `tests/native/projected_view_interface_conversion.jett` (proposed). +- Reflection: compare wrong-owner and wrong-field-type diagnostics for a generic aggregate's reflected field read; fixture: `tests/native/reflected_aggregate_mismatch.jett` (proposed). +- Erased interfaces: compare exact-owner rejection and pending-error precedence for a primitive-backed refinement argument; fixture: `tests/native/interface_same_owner_pending_refinement.jett` (proposed). +- Resource sums: execute the existing absent optional self-reseat witness natively against its interpreter result; fixture: `crates/jett_mir/src/resource_ownership/fixtures/direct_optional_self_reseat.jett`. +- Resource sums: execute the existing failure-result self-reseat witness natively against its interpreter result; fixture: `crates/jett_mir/src/resource_ownership/fixtures/direct_result_self_reseat.jett`. + +## Language rules selected or newly diagnosed since 2026-09-17 + +- `7b59b820`: verify/property checks establish their executed examples, not universally quantified correctness. +- `239f7242`: float32 rounds at each expression boundary; display widens the rounded value to the existing float64 carrier. +- `2dfe769e`: some/ok/fail consume owned payloads; keeping the original requires an explicit clone. +- `0bb6023c`: callable parameter view modes are part of its type; an explicit view cannot satisfy an owned parameter (E0375). +- `64b76d1f`: expected result context may specialize inferred never collection elements, but cannot invent absent generic parameters. +- `b89b2222`: function-valued calls evaluate arguments before the callee; anonymous callable types have no named-argument labels. +- `b89b2222`: creating a capability-accepting closure may be pure; invoking it still requires its declared capabilities. +- `c615002b`: callable parameters are contravariant, results covariant, and view modes exact. +- `4edc480a`: a generic method owner requires concrete type arguments; a bare generic owner is diagnosed. +- `769dbd37`: refinement predicates use their declaration context and fully coarsened input, not caller aliases or bindings. +- `6e787229`: interpolation selects Displayable for the expression's checked type and evaluates the receiver once. +- `2450e9b4`: one-line breakpoints show the current function's lexical bindings rather than flattened caller frames. +- `cf67fc2d`: typed secret debug values and reflection diagnostics redact secret payloads recursively. +- `7dffa420`: direct bytes/list/map/set/optional/result equality is rejected, including wrappers and generic/comptime uses (E0376). +- `12368e14`: enum payload structs require their exact explicit two-view Equatable.equals; no structural fallback. +- `68f5bbb1`: namespace constants are baked values with no startup initializer or global runtime storage. +- `dd010020`: mutable namespace declarations are rejected (E0377). +- `9554c637`: constant calculations require explicit comptime (E0378); unsupported constant value types fail conservatively (E9001). +- `3a70864c`: handling a refinement over a sum validates the whole sum; its validation error is a string. +- `aee75f32`: known move-only views cannot escape into owned returns/parameters, and returns-view annotations are rejected (E0401). +- `f0ec8030`: projected writes reject immutable/temporary roots (E0404) and view roots (E0401). +- `b7180bd9`: Resource-bearing getters/fields cannot manufacture owned copies or clones; owning constructors require owned payloads. +- `b1cf8943`: compiler debug events are separate from application stdout; release print/println are rejected (E0362). +- `1dde961d`: same-type actor equality compares instance identity; clones alias the instance and pending depth remains distinct. +- `7ff46cc7`: erased-interface concrete arguments must match the receiver's exact checked owner before conversion or method entry. +- `24f1b0bb`: reflected callable return refinement adapters validate lazily after one source call, without implicit joins. +- `9113f395`: an explicit comptime actor graph is an immutable template instantiated freshly per runtime evaluation, preserving internal sharing. +- `10e9c7e5`: interface-typed equality, including implicit enum traversal reaching interfaces, is rejected (E0376); explicit domain comparison remains available. +- `b612399e`: list.sum accepts numeric primitives and transparent aliases only; refinements, secrets and nonnumeric types are rejected. +- `132f9d98`: finite field replacement requires a live mutable owned record/exact-state machine and no dependent loan; bitfields and enclosing refinements require reconstruction. +- `132f9d98`: compiler reflection metadata is inspect-and-pass; projected writes cannot modify its builtin fields. +- `132f9d98`: matching a viewed enum preserves borrowed move-only payloads; coarsen/declassify do not create ownership. +- `a26523cc`: actor handlers preserve owned state at normal exits; Handle-ending respond is terminal and borrowed replies cannot escape. +- `f865e3f1`: a temporary-backed local view retains its whole producer until the original lexical scope exits, without last-use expiry or implicit cloning. +- `099571f8`: a persistent move-only alias blocks owner changes even after its last read; mutable owned backing grants no mutable view. +- `21245731`: Resource replacement stages the RHS before old cleanup; absent sums remain live owners and live shell/payload loans block replacement. +- `5d7bb8a0`: pure Verify/Property bodies may use Resource-typed values with no live Resource, without gaining capabilities. +- `7f7ca39b`: exact same-local direct Resource sum reseating retains occupancy and loans and adds no transfer, finalizer or clone. diff --git a/docs/active/native_value_abi.md b/docs/active/native_value_abi.md new file mode 100644 index 00000000..fefa9a1f --- /dev/null +++ b/docs/active/native_value_abi.md @@ -0,0 +1,1119 @@ +# Native value ABI and ownership slice + +This is an implementation contract, not a full-parity claim. + +Sum task joins retain the checked outer type boundary. An exact result remains +the result of the join; a secret-qualified or nominally refined result becomes +the payload of an outer result. `SumTaskJoin` clones the sum and removes one +outer pending layer without changing its inner tag, error type or payload depth. +The existing `SumNew` operation takes ownership of that joined clone only after +successful construction of the additional result. No ABI signature or ownership +slot policy changes. +An explicit-view Join borrows the input for the clone operation. Its owned +result can escape the call while the original owner's depth remains unchanged. + +Contextually secret struct construction uses an exact nominal inner +`StructConstruct` followed by the existing qualification coercion. Only checked +struct constructors qualify; validating constructors retain their exact +`result[Struct, string]`, predicate chains and validation flag inside the +qualification boundary. Field conversions, source evaluation order, +pending depth, and ownership are preserved without a runtime ABI change. +Predicate errors retain the existing captured, prefixed result failure; field +evaluation errors remain terminal and bypass result handlers. + +Secret qualification around checked bitfield and machine constructors uses the +existing coercion and value ABI. Inner construction retains the exact bitfield +or machine-state identity; validating bitfields retain the exact +`result[Bitfield, string]` output and existing width-validation cleanup. The +qualification coercion transfers that owned value without changing its result +tag, pending depth, field evaluation order, or ownership. Strict constructor +verification remains unchanged. + +Direct-secret aggregate field reads use the exact unqualified nominal layout +and the checker's qualified result TypeId. Ready scalar and owned field paths, +pending-child depth, and machine-state indexing reuse the existing runtime ABI. +Owner readiness still uses the original checked receiver's debug graph, so a +pending secret receiver remains redacted. That separate observation-policy gap +with raw reference diagnostics is not closed by qualified-field admission. + +Secret-wrapped enum constructors retain an unwrapped nominal enum target and a +separately checked secret expression type. Equality uses the value representation +for its root layout, while debug observation keeps the redacted root. This also +preserves exact explicit struct methods in wrapped enum payloads and does not +permit nested secret payload graphs. Mixed public/secret binary operands retain +the checker's inner type compatibility and secret result taint. Clone/view +context conversions may add outer secrecy; neither those conversions nor a +fabricated binary result may declassify. Runtime leaf signatures are unchanged. + +The additive `jett_rt_v1_float_remainder(context, f64, f64) -> f64` leaf implements +floating `modulo` using the same `%` operation as the interpreter. It allocates +no owned values, preserves the context's first-error handling, and treats NaN +results as ordinary floating values. Codegen promotes binary32 operands and +demotes the result explicitly; both source widths retain the existing pending +operand checks before entering this leaf. + +Aggregate debug printing reuses `DebugAppendAggregate` with an empty label and +an owned temporary text buffer. Its value input stays borrowed, so printing +does not transfer a viewed aggregate or builder into the runtime. Debug layout +tag 18, `Uninhabited`, is additive: empty containers and inactive sum branches +may refer to it, but formatting or comparing an actual payload through it fails. +Existing tag numbers remain unchanged. Secret nodes retain conservative +redaction pending the hidden-secret debug-print policy decision. +The aggregate formatter uses a work stack rather than recursive calls and a +fixed depth ceiling. Active-path identities reject cyclic schemas/payloads; +shared finite subvalues remain valid. Dynamic layouts are cached by descriptor +bytes during each observation so cycles crossing interface layouts are detected. +Formatting failure leaves the caller's text buffer unchanged. This changes no +ABI tag or leaf signature and does not extend the aggregate equality contract. + +Immutable UTF-8 strings use nonzero u64 context-associated handles. Zero is an +uninitialized slot, never a language string. Each owning slot holds one reference. +Literal creation, concatenation and formatting create one reference; retain +creates another; release removes it immediately and destroys the last reference. +Borrowed inputs are valid only for a call. Handles cannot cross contexts and are +never reused. No interpreter Value, source, AST or HIR enters the runtime. +Native breakpoint leaves write the interpreter-compatible debug line to stderr +only when the checked boolean condition is true. HIR supplies the scoped source +binding snapshot. The backend emits a type graph for aggregate bindings, and +the runtime formats live list, set, map, sum, struct, bitfield, enum, and machine handles +recursively without consuming them. The graph uses node indexes so recursive +nominal types have finite metadata. Remaining special value kinds need separate +formatting. The leaf does not expose a +debugger pause or evaluate arbitrary values. +Stdout authority is a separate context-bound token, supplied only for checked +Stdout entry parameters and passed explicitly to printing leaf operations. +Stderr, Stdin, Filesystem, Network, Process, and Log also receive distinct, +context-bound tokens when declared on the checked entry. Their currently +implemented value operations (moves, views, direct/indirect calls, debug output, +and sequential run/join/cancel) follow the interpreter's opaque capability +behavior. These grants do not implement the pending I/O providers or authorize +any other capability's leaves. Repeated grants in one context retain identity; +tokens differ across capability kinds and contexts. +Clock authority is likewise a distinct context-bound token. The native entry +grants it only for checked Clock parameters; `Clock.__now` validates the token +and samples the wall clock through the same pre-epoch flooring and int64 +millisecond-range kernel as the interpreter. The native launcher can opt into +a per-context scripted provider through `JETT_NATIVE_TEST_CLOCK_SCRIPT_V1` for +deterministic parity runs. An empty script means an exhausted provider; absent +configuration keeps the production wall clock. Native Clock reads match the +interpreter's sample conversion, unavailability, and exhaustion failures. +Random authority is a separate context-bound token. Its provider is seeded once +from OS entropy when the native entry grants Random, or configured before entry +from `JETT_NATIVE_TEST_RANDOM_SCRIPT_V1` for deterministic parity runs. The +bounded-integer, unit-float, and boolean leaves share their provider logic with +the interpreter, including rejection sampling and invalid/exhausted script +failures. Collection algorithms remain in `stdlib/random.jett`. +`uuid.new` has a separate owned-string leaf matching the interpreter's current +UUID v4 behavior. Both call a shared formatter after sampling OS entropy; the +native leaf reports entropy failure through the runtime status and owns the +resulting string in the current context. The broader UUID generation policy is +still tracked separately from this implementation slice. +Environment authority is another context-bound token. The shared runtime +captures immutable user arguments and environment entries before entering +`main`; a test-only `JETT_NATIVE_TEST_ENVIRONMENT_SNAPSHOT_V1` channel can +replace that snapshot without changing the host process. `Environment.__args` +returns a fresh owned string list. `Environment.__get` returns an owned +`result[optional[string], string]`, preserving missing, invalid-name, and +invalid-Unicode outcomes. The two native Environment fixtures pass, while +platform-raw capture details remain to be audited against the design note. + +Generated functions and all source control flow remain Cranelift machine code. +Copyable string arguments are borrowed by the native call ABI; callee owning +locals retain them. String expression results are owned. Results transfer one +reference to the caller. Assignment evaluates its RHS before releasing its old +slot. Full-expression temporaries have zero-initialized frame slots; all are +released at expression completion and on terminal failure. MIR liveness and +initialization analysis defines local cleanup points, including loop backedges +and early exits. Zero slots permit conditional initialization without releasing +an uninitialized value. The initial string slice did not establish move-only ownership. Later sections +describe bytes, sums, lists and user structs; resource ownership remains unproven. + +Internal native `nothing` values use an unowned u64 pending depth: zero is plain +`nothing`, and each sequential `run` adds one. Depth is preserved through native +parameters, returns, locals, closure captures, aggregate payloads, and explicit +`comptime` materialization. `NothingRun` checks depth overflow; `NothingJoin` +returns one owned result handle, unwrapping one level or returning the owned +error string `task was cancelled` for depth zero. `NothingFormat` returns one +owned string matching the interpreter's nested `pending(...)` rendering. +`NothingEqual` matches direct unit equality and reports the interpreter's +terminal error if either operand is pending; aggregate payload equality compares +depth instead. Trace and breakpoint formatting preserve the same depth. +Ordinary unit literals and effect results remain depth zero. The exported +`jett_aot_v1_entry(context) -> u32` status ABI is unchanged; its internal call to +`main` discards the unit scalar. These leaves model the current sequential +interpreter: `cancel` yields plain `nothing` without changing the pending +operand. They do not establish asynchronous scheduling or cancellation +checkpoints. + +Numeric and boolean values now carry a separate u64 pending depth alongside +their existing width-correct bits in native locals and Jett function signatures. +Direct and indirect calls, returns, and ordinary closure captures forward both +parts. Sequential `run` increments depth with the checked unit-task leaf; +`join` removes one level and places any remaining depth beside the scalar +payload in its result handle. Taking a result restores both parts, including +after cloning the result. Directly primitive-typed trace and breakpoint +bindings format the nested pending wrappers. Graphics callbacks supply the +depth component required by the same function ABI. Struct and enum payload +fields store scalar depth beside their bits; field access and recursive debug +formatting restore it. Primitive lists use a sparse per-element depth map, +preserved by cloning, insertion, removal, iteration, and stable sorting. +`list.get` carries element depth through its result payload. Differential +fixtures cover these boundaries, including nested pending wrappers. Reflected +field reads restore pending depth from the validated struct, enum, or machine +field slot; differential fixtures cover primitive reflected values. Pending +scalar values in unexamined intrinsic operands need further parity work. +Scalar string interpolation now formats pending depth together with the value +bits, including nested pending values and all numeric and boolean widths. +Primitive JSON serialization uses that formatting for numbers and booleans. +A checked source helper serializes nonliteral `nothing` after evaluating its +argument, including pending `nothing`. +Direct unary and binary operators now reject pending primitive operands with the +interpreter's error text after evaluating the operands in source order. +Boolean `and` and `or` still skip an unevaluated right operand when the plain +left value short-circuits; a pending left value evaluates the right operand +before reporting the error. `if`/`while` branch conditions, breakpoint +conditions, and assertion conditions reject pending booleans with their +respective interpreter diagnostics. Pending primitive operands passed into +other intrinsics remain to be audited. +Numeric aggregate kernels inspect both the list's own pending depth and its +per-element depth map before reading scalar bits. `list.sum` distinguishes a +pending first element from a later pending element, matching the interpreter's +two current error paths; `math.average` and `math.median` report their current +numeric-list errors for pending containers or elements. +Average and median accept every primitive numeric width and transparent alias. +Their native decoder validates narrow bits, sign-extends signed integers, and +widens binary32 to binary64 before the existing shared algorithms, retaining +the exact `float64` result and empty/pending failure contracts. This does not +admit nominal refinements or secret element wrappers. +List intrinsic argument checks reject a pending list container before the +operation, and reject pending int64 indexes for get, insert, remove, indexed +sort, and swap with the interpreter's operation-specific errors. Pending scalar +elements remain valid inputs to append and insert, and retain their depth. +Native `range` checks all supplied integer operands before generating values. +`bytes.get`, `bytes.slice`, `string.slice`, and `string.repeat` reject pending +indexes or counts and pending receivers with their current interpreter errors. +Math leaves use a shared pending-scalar check with the checked intrinsic name +and interpreter's type-error wording before operating on numeric payload bits. +Numeric conversion and `string.from_*` leaves use the same scalar check. A +pending-handle check protects numeric string parsing and `bytes.from_string`, +`bytes.from_hex`, `bytes.to_string`, and `bytes.to_hex` before their kernels +inspect the underlying plain string or byte buffer. +The same check rejects pending receivers and arguments for string text +intrinsics and `bytes.length`/`bytes.concat` with interpreter-matching errors. +`string.join` additionally rejects a plain list containing a pending string +element, reporting the wrapped element as the interpreter does. +Encoding and CSV parser hooks reject pending input strings, while base64 +encoding and crypto hooks reject pending byte inputs. CSV stringify rejects a +pending outer list or row and formats pending fields before quoting them. +`run` and `join` dispatch through the representation type for secret and +refinement wrappers, preserving task depth in secret strings and bytes. +Primitive sets likewise keep a sparse per-element pending-depth map. Add, +remove, contains, clone, recursive debug formatting, and scalar iteration use +that depth; string set membership compares both text and the string handle's +pending depth. Set intrinsics reject a pending set container. Native `for` +checks the container before fetching length or elements and reports the +interpreter's shared iterable error for pending lists, strings, maps, and sets. +Map entries store pending depth beside primitive key and value bits. Literal +construction, insert/update, lookup result payloads, removal, cloning, +`from_lists`, recursive debug formatting, and key/value iteration preserve it. +String map keys compare both text and their handle's pending depth. Public map +intrinsics reject pending map containers, while `map.from_lists` rejects +pending input lists with the interpreter's current errors. +`TypeConstruction` builder fields also store scalar pending depth, so builder +debug output, cloning, and finished struct, enum, and machine values preserve +it. Bitfield width validation rejects pending numeric fields with the +interpreter's current error. + +Owned native strings also carry a pending depth in the runtime registry. +`StringRun` and `StringTaskJoin` create independent handles, so wrapping a +cloned string does not change its aliases. Trace, aggregate debug output, +`print`/`println`, and ordinary string interpolation render nested `pending(...)` +wrappers; direct string equality and inequality report the interpreter's runtime +error while aggregate equality compares both text and pending depth. Owned bytes +handles keep the same depth in a sparse runtime registry: `BytesRun` and +`BytesTaskJoin` create independent byte handles, cloning preserves depth, and +trace and aggregate equality observe the wrapper. Lists, sets, and maps carry +pending depth in their native handles. Their task leaves clone the collection, +preserve nested owned values, and change one depth level; debug output and +aggregate equality observe the depth. Other runtime operations on pending +values, along with other value kinds, still need the general pending-task +representation. + +Implicitly selected display methods have an additional `DisplayResultCheck` +boundary. This borrowed I64-handle-to-I32-status leaf rejects pending results +with their exact formatted diagnostic and adds no handle or cleanup obligation. +The returned string keeps its existing owner, and direct method calls retain +ordinary pending-string behavior. + +Native record handles preserve pending depth for structs, enums, bitfields, +and machines. `RecordRun` and `RecordTaskJoin` deep-clone fields before changing +the outer depth; tracing preserves nested wrappers and state-qualified machine +binding labels. Direct enum equality rejects pending operands with the +interpreter's runtime message, while aggregate payload comparison observes +pending depth. + +Optional and result handles also preserve pending depth. Their task leaves +clone owned payloads before changing the outer depth, and native debug and +aggregate equality inspect the wrapper. Joining a pending result returns its +existing success or failure branch, as in the interpreter's sequential task +simulation; joining a pending optional returns a result containing the optional. +MIR snapshots a cloneable linear local before `run`, so the original optional, +result, list, map, or record remains readable after the pending value is made. +The task leaf then gives the pending value its own handle and depth; this is +covered by linked interpreter/native traces of both values. + +Function descriptors use owned record handles, so task `run` and `join` retain +pending depth while cloning captured environments. Debug formatting applies +that depth to named and captured function labels, including aggregate elements. +Indirect calls check the descriptor after evaluating arguments and reject a +still-pending function with the interpreter's runtime error. + +`TypeConstruction` builders use the same record-handle pending depth. Task +`run` and `join` clone their fields and builder metadata; debug formatting +preserves nested wrappers. `construct_put` and `construct_finish` reject a +still-pending builder with the interpreter's source-value error before +performing builder validation or construction. +MIR also treats a builder as snapshotable: `run` of a local leaves the original +builder usable, repeated handles can clone an optional builder payload, and a +view argument remains tied to its pre-handler builder when a later argument +rebinds the caller's local. + +Actor identities also carry pending depth in their registered record. Task +`run` and `join` change that depth without allocating a second actor identity; +debug output retains the actor ordinal. `send` and `ask` reject a pending actor +before evaluating message arguments, matching the interpreter's actor-value +check. + +Actor state records now store primitive pending depth alongside scalar bits. +Handler entry restores that depth from each state field, and every writeback +replaces bits and depth together. Differential fixtures cover pending state +initialization and updates for integer, boolean, and floating-point fields. + +Capability task values use separate scalar handles mapped to the granted +authority token and a pending depth. `run` allocates a new handle and consumes +an earlier task handle; `join` returns a result containing the authority token +or a handle with one less pending level. Joining a plain Stdout, Clock, or +Random value returns the interpreter's current cancellation error, while a +plain Environment or Graphics value joins successfully. Debug formatting uses +the interpreter's current `nothing` display for the first three capabilities +and the named capability display for Environment and Graphics, with pending +wrappers at each depth. The original granted token remains the authority for +capability operations after the final join. +Clock, Random, and Environment intrinsics reject a pending capability before +using its authority, with their interpreter-specific errors. Environment +lookup also rejects a pending key before reading the launch snapshot. +`Stdout.write` follows the interpreter's current behavior: it accepts a +pending Stdout capability and writes pending string arguments with their +visible `pending(...)` wrappers. +`secret.compare` rejects pending string or byte operands before comparing +their plain buffers; `secret.redact` still returns the fixed redaction text. +Bitfield byte conversion rejects a pending record or input bytes. Payload +encoding checks a pending `list[uint8]` or element before copying byte values +and reports the interpreter's field-specific error. + +Native string search shares one linear grapheme-boundary scanner with split. +`string.index_of` returns an optional int64 handle with a non-owning index +payload, including index zero for an empty needle. `string.count` returns the +number of non-overlapping matches and zero for an empty needle. The scanner +rejects byte matches that begin or end inside a grapheme, matching the +interpreter's string contract. + +The first terminal failure of the current execution lifetime is separate from +Jett result.fail. Ordinary one-entry contexts keep their legacy session channel. +Independently authenticated Resource entries retain separate attempt-owned +status, dynamic message and diagnostic-prefix records. Nested compiled calls +share the active record; exact completion seals it. A new entry constructor may +select a fresh record only after predecessor, session-poison and complete +custody/storage checks, preserving every old observation without resetting it. +Fallible leaf and compiled calls are followed by a failure edge, which releases +live temporaries/locals before returning a zero placeholder. Callers must inspect +failure before using that placeholder. The entry wrapper returns failure status +and the launcher renders the message before context destruction. Cleanup does +not overwrite the first failure. +Context destruction checks that all owning values have been released; it must +not silently hide compiler leaks by acting as a program-long value arena. + +An actor is a runtime-owned state record rather than an ordinary frame owner. +`ActorRegister` transfers a fully initialized native struct to the context's +actor registry. `ActorReplace` transfers a new state field and releases the +previous owned field. Ordinary `DropValue` rejects a registered actor. Context +destruction releases registered actor records before checking for leaked frame +owners, including any nested owned state. Checked actor constructors evaluate +capability arguments and state initializers in source order, then register the +complete record. `send` and `ask` call the checked handler identity with the +actor record as their hidden environment. Handlers copy or clone captured +fields into local owners, write mutable state back through `ActorReplace` on +return or response, and release their local copies. A discarded `send` response +is still owned and cleaned up. Linked native/interpreter fixtures cover scalar +and owned string state, named arguments, duplicate actor names across +namespaces, and actor responses. + +`Graphics` now has a distinct context-bound entry authority token, and native +function signatures can carry that token through checked `view` parameters. +The Graphics kernel checks capability pending depth before configuration +validation, using the existing pending-handle leaf. A forwarded still-pending +authority reports the reference terminal capability error; every required join +must complete before a session can use the original token. +The native ABI now validates checked `graphics.Config` and `graphics.Scene` +records and exposes open, present, next-key, and close leaves. The session stays +on its creating thread; context destruction closes an unfinished session before +checking value ownership. Domain errors return owned Jett result values, while +terminal callback errors remain separate runtime failures. Native +`graphics.__run` validates the config and initial scene, opens a session, +presents each rendered scene, and dispatches checked update and render +callbacks until close. It tracks the state owner across updates and closes +the session on handled domain failures; context destruction closes it after +a terminal callback failure. Named callbacks, including render's `view` +parameter, now match the interpreter in scripted fixtures. Inline Graphics +callbacks with `view` parameters extract to checked function values before +MIR lowering, preserving their borrow mode. +Primitive Graphics state retains pending depth across the initial callback, +update returns, and subsequent callbacks. Scripted differential fixtures +cover nested pending integer, boolean, and floating-point state. +Graphics decoding rejects pending record owners and fields before payload +access, including pending strings, Scene list containers, and record elements. +It follows reference order: Config title/width/height; Scene background, all +rectangles, then all text. Complete shape decoding precedes domain validation, +so a later pending field still fails before an earlier ready out-of-range value. +These are terminal runtime failures, not result failures or implicit joins. +The emitter reads dimensions only after configuration validation succeeds. +Existing leaf signatures, callback state metadata, and session cleanup remain +unchanged. +For deterministic parity runs, the launcher configures a context-local Graphics +event queue from `JETT_NATIVE_TEST_GRAPHICS_SCRIPT_V1` before entry. The runtime +and interpreter share the JSON event grammar (key, close, host error); no +window is created by configuration or authority grant. Scripted sessions +consume that queue and validate rendered scenes without opening a window. +After a successful native entry, the launcher invokes a context-local check for +unconsumed scripted Random, Clock, and Graphics inputs in that order. It records +the interpreter's exact terminal message and fails the entry; an earlier runtime +failure skips the check. Context destruction and owned-value cleanup still run +once on either path. + +Runtime exports use fixed-width scalar parameters and typed leaf operations; +no universal operation/name dispatcher. Panics are contained at each C boundary. +The existing ABI v1 lifecycle remains compatible; added leaf signatures are +shared by the runtime and emitter. Layout/tag tests pin the added contract. + +Temporary capacity is a structural MIR bound on emitter ownership operations, +not a multiplier on string-typed expressions. Literals, string-local retains, +string-call results and string-producing intrinsics each own one slot. +Interpolation owns an initial empty string, then one concatenation per segment +and one formatting slot per scalar segment (text segments own a literal). +Print owns its empty accumulator, argument formatting/concatenations, two slots +per separator, and two for a newline. Child costs accumulate recursively until +full-expression cleanup; both short-circuit branches get distinct frame slots. +The maximum full-expression cost determines the frame capacity. + +## Float32 oracle correction + +The design defines float32 as 32-bit floating point, with IEEE arithmetic. +Retaining parsed f64 precision in the interpreter contradicts that contract: +`(value + 1.0) - 1.0` at float32 must round the intermediate addition, not +just the final result. Native already emits typed f32 instructions. The +interpreter therefore must normalize float32 at its existing checked-expression +and declared-type boundaries (including literal, assignment, argument, return, +and explicit comptime evaluation), just as it already normalizes narrow integers. +The internal Value::Float64 f64 carrier may represent a widened f32, but cannot +retain extra precision for a checked float32 expression. Alias/refinement base +type resolution remains centralized in the existing normalization boundary. + +Two possible formatting changes were considered: dedicated shortest-f32 decimal +formatting, or retaining the current shared f64-carrier display. This fix retains +the existing display: normalize the value first, then format the exactly widened +f32 with the same f64 conversion used by native and interpreter. Thus float32 +0.1 displays as 0.10000000149011612, while float64 0.1 still displays as 0.1. +This does not introduce a new shortest-f32 formatting policy, disable formatting, +or treat the old unnormalized f64 oracle as evidence of correct float32 behavior. +Regression tests compare actual executable bytes to the corrected checked oracle +and pin formatting-dependent string equality/control flow, including comptime. + + +`print` and `println` retain their existing language diagnostic-output permission: +their dedicated leaf receives the explicit runtime context, not a fabricated +Stdout token. `Stdout.write` requires the separate granted token and rejects a +token from another context. Ordinary user code cannot construct that token. + +Typed numeric leaves preserve fixed-width wrapping, IEEE values, and exact +library error text. Integer division syntax remains governed by the frontend +nonzero proof; leaf errors such as invalid `math.clamp` bounds are terminal. +Checked `float64.from_int64` and `int64.from_float64` return ordinary result +values. They accept only exactly representable inputs, including the signed +minimum boundary, and return the interpreter's error strings for precision +loss, fractional values, non-finite values, or overflow. + +Cleanup evidence is executable: destruction rejects a nonempty value registry, +and launcher tests prove that a deliberately leaked owner overrides entry failure +with exit 72. An expected runtime failure counts in the exhaustive audit only when +its output/message match and exit 71 establishes successful checked destruction. +Resource finalizer instrumentation must be extended before move-only resource +families can use this gate. + +## Linear bytes places (phase 3) + +CopyValuePlan retains its copy-only public guard. MoveValuePlan additionally +proves definite availability over the MIR CFG, with intersection at joins and +loop backedges, ordered operand evaluation, and call-bounded view loans. It +rejects consuming a view, escaping a view into an owner, moving a borrowed +argument during later argument evaluation, and reading a moved place. It does +not yet accept aggregates, sums, resources, or hidden handler control flow. + +Bytes use distinct context-associated handles backed by owned mutable Vec +storage, not UTF-8 strings and not reference-counted owners. Explicit clone +allocates independent storage. A local move clears its materialized source +slot and creates one owning expression slot. Zero is the drop flag for an +uninitialized/moved slot. Assignment evaluates the RHS before dropping the old +owner. Owned call arguments transfer only after every argument is evaluated; +temporary owners remain available to failure cleanup until that point. Views +borrow a local or an owning temporary through the call only. Callees own owned +parameters and never destroy view parameters. Owned returns detach their slot +before frame cleanup. Leaf byte kernels borrow their inputs for the call; +compiled stdlib bodies implement language-level consumption and control flow. + +Function types retain the checked view mode of every parameter. Native +indirect calls borrow view arguments until the call returns and transfer only +owned arguments; the borrowed callee parameter never destroys the caller's +value. A call-site `view` to an owned parameter is rejected before codegen. +Immutable local view aliases retain their immediate backing local in HIR/MIR. +Native storage excludes these aliases from owning slots and retains the root +owner through transitive alias reads. This includes qualified strings and +function descriptors as well as supported linear carriers. Handler extraction +uses checked formal view modes and stages independent snapshots when needed; +it resolves an indirect callback after its arguments. Mutable and temporary +alias origins, allocating conversions, and later root consumption or rebinding +remain explicit implementation limits, described in +[the local alias note](../completed/native_codegen/native_local_view_aliases.md). +Function-valued callee expressions use the same descriptor ABI as named local +callbacks. Argument temporaries remain owned while a returned, projected, or +handled callee expression is evaluated; a callee failure or handler return +cleans them up before any transfer to the selected function. Native differential +fixtures cover successful calls and terminal callee failure with owned list +arguments. + +Function descriptors use three initialized fields: a borrowed code address at +index 0, an owned capture-environment handle (or borrowed zero) at index 1, and +an owned debug-label string at index 2. The label is derived from checked source +metadata: `function(namespace.name)` for a named function and +`function(parameter, names)` for an inline function, excluding synthetic capture +parameters. HIR and MIR preserve this distinction explicitly even when a +capture-free inline function uses the same call representation as a named one. +The runtime debug-layout format appends the zero-payload `Function` tag (13) +without changing earlier tag values. Its reader borrows the descriptor and +label; it never calls the code address or exposes the capture environment. +Ordinary clone/drop and failure cleanup own the label through the descriptor. +The current host representation retains this label on every function value; +eliding unused debug metadata is a later optimization. + +Bytes new/from_string/slice/concat, explicit clone, length and to_hex execute +natively. Concat creates real checked-capacity byte storage. No byte mutation +syntax is added. Byte temporaries add one structural slot per allocating call, +intrinsic, explicit clone, or local move; local views add none. Existing string +structural costs are unchanged. Cleanup accepts both owned families, including +terminal failure, and byte creation/destruction counters must balance in +addition to both registries being empty. Runtime tests separately prove byte +storage independence, exact destruction and byte-only leak detection. This is +not evidence for resource finalizers or full native parity. + +Encoding leaves borrow their byte or string input and return an owned string +or an owned `result` with an owned byte/string payload. Native and interpreter +call the same base64, strict hex, URL, and form kernels, including validation +precedence and UTF-8 errors. Failed sum allocation drops the newly created +payload before propagating terminal failure. + +CSV leaves parse strict records into nested owned string lists or header maps, +and stringify checked nested string lists. The interpreter and native runtime +share the parser, quoting, and header-validation rules. Native construction +releases partial rows, fields, maps, and result payloads if an allocation +boundary fails; the runtime fault-injection test checks each boundary. + +Secret and refinement wrappers retain their underlying native representation. +The checked base-to-secret lift is recorded in expression types before MIR +lowering, so local storage, calls, and aggregate fields agree on the wrapper. +Native redaction returns the fixed `***` string after evaluating its operand. +Native secret comparison accepts the checked string and bytes payload shapes, +returns false on a public length mismatch, and uses constant-time byte +comparison for equal lengths. Wrapper-aware ownership retains and releases +strings and transfers or clones owned bytes and aggregates as their base types. +Explicit `declassify` transfers the same underlying value after the verifier +checks its source `secret[T]` and destination `T`. +Explicit `coarsen` likewise transfers the underlying value after the verifier +walks the checked refinement ancestry to the requested base type. It creates +no runtime wrapper and does not bypass the separate fallible validation needed +to construct a refinement. MIR snapshots a cloneable owned local before either +transparent conversion, preserving the original refinement or secret value +for a later read while the converted result owns its copy. Linked differential +fixtures cover reuse after `coarsen` and repeated `declassify` of a secret list. +When the conversion is used as a borrowed argument, MIR and native emission +preserve the underlying view instead of creating or consuming an owner. Linked +debug/release coverage checks local and projected bytes, direct and indirect +calls, later-argument handlers, source removal, and terminal argument failure +with successful cleanup. +Private crypto leaves borrow checked bytes and +return owned digests; the interpreter and native runtime use +the same SHA-256, SHA-512, MD5, and HMAC-SHA-256 kernels behind public `.jett` +wrappers. The HMAC result retains its secret type in MIR and native ownership. + +At a base-value refinement `handle error` boundary, HIR supplies one checked +predicate function per refinement ancestor. MIR calls them in base-first order +with a clone of the candidate, branches on each result, and transfers the +candidate into the refined output only after all predicates pass. A false +result creates the interpreter's type-specific error string and enters the +source failure block. When the candidate is a cloneable owned local, MIR +snapshots it before validation so both success and handled failure can still +read the original source. + +Direct local and struct-field validation reuses the original checked source +refinement, skipping its established ancestor prefix while preserving remaining +predicate order, pending depth, ownership, and the constructor's result shape. +Reference execution captures that proof before evaluation and normalization; +raw API inputs and changed property candidates retain their existing validation +entry points. Generic fields and result wrapping follow the constructor's selected +type arguments and declaration namespace. This repair adds no runtime ABI +operation or representation. Checked named calls and returns likewise transport +original expression proofs in the reference engine, including reordered named +arguments, generic/reflected bodies, NamedFunction values, and source pipelines. +Raw calls disable proof reuse throughout nested bodies and callbacks; checked +program and zero-argument verify entries use an explicit bridge with forced +input normalization. Changed property candidates and forced validators retain +their separate validation boundaries. Builder finish also remains forced. +Reference inline closures retain their declared return annotations and validate +parameters and results inside their captured context before exposing a typed +call result. Fresh private sum fallback results also validate before checked +proof reuse; refined summation remains outside native admission. Native checks +for newly refined root returns now use the existing staged refinement machinery. +Checked concrete function and inline signatures select the destination. Exact +sources skip predicates; ancestor sources check only their missing suffix. The +candidate is evaluated once, and the original string error reaches the existing +borrowed `RuntimeFailMessage` leaf before owned locals are unwound. The private +HIR `RuntimeFailureMessage` boundary requires exact string input and nothing +output; it introduces no source spelling or ABI operation. Outer secret or +container return conversion admission is not broadened. +Reference reflected reads also establish new requested invariants in ready +builtin containers before exposing checked result proof. Exact declared schemas +preserve pending values; unsupported changed pending, secret, callable, or nominal +generic conversions fail before exposing a result. Requested-type admission and +secret observation remain open policy questions. Canonical generic, captured, +reflected, and checked type identities survive caller import shadowing. These +reference safeguards add no runtime operation or representation and do not certify +equivalent native reflected producer validation. + +Native reflected root-refinement reads now use complete HIR plans for every +declared field slot. Exact and established ancestor reads reuse proof; ancestor +promotion checks its new suffix, while an unrefined or sibling source checks the +requested chain in base-first order. MIR stages source and selector once, runs +the original getter's metadata, owner, member, index, requested-type and pending +checks first, then validates only the selected clone. Predicate identities and +TypeField layouts are verified before descriptor use. Dynamic predicate errors +use the existing borrowed failure leaf and cleanup path; no ABI operation is +added. New native nested-container invariants remain an implementation gap. +Changed secret, callable and nominal generic proofs retain conservative +value-free refusals without broadening requested-type admission. +Direct and piped getters append the same checked field metadata in declaration, +variant, or state order after the original source/selector evaluation order. +Narrowed machine owners keep their checked identity and base metadata spelling. +The admitted one-operand observers `type.variant_value`, +`type.machine_state_value`, and `type.arg` now share direct/pipeline metadata +expansion too. Their checked output type selects metadata carrier types after +the original operand runs once. Existing pending/index checks and independently +owned metadata clones use the same runtime leaves, with no ABI addition. + +## Result and optional ownership and handlers + +Native sums have separate context-associated, move-only handles. Their internal +records contain a genuine discriminant (0: fail/none, 1: ok/some), a 64-bit +payload carrier, and an owned-payload flag. Fixed-width scalars preserve bits; +float32/64 are bitcast, never numerically cast into the carrier. Nothing/none +have no owned payload. Owning payloads transfer into the record only on +successful construction. Sum destruction recursively drops the active payload +only; explicit clone recursively clones bytes/sums and retains copyable strings. +Taking a payload checks the expected tag, destroys the outer record, and +transfers the payload to a new initialized place without destroying it. A failed +tag check leaves the record available to terminal cleanup. Sum allocation and +destruction counters are checked independently from string and byte registries. + +Statement-root result/optional handles now lower to SumTag, ordinary Branch, +SumTake, and explicit continuation blocks. Handler default yields to that +continuation; return exits the enclosing function; break/continue retain their +enclosing loop edges. Failure branches without an error binding drop the sum +normally. Terminal runtime failure still bypasses these language-level branches. +The move fixed point covers the extracted edges and transferred payload places. +Emitted SumTag statements call `SumHandleTag` with the source's checked debug +layout. The leaf rejects outer pending depth before extraction, preserving the +sum and its owned payload for cleanup. Ready sums return their tag without +parsing the layout; pending payloads inside a ready sum remain valid. On failure, +typed observation produces the reference pending-value diagnostic for public +values while retaining native secret redaction. The separate raw SumTag leaf +continues to serve internal runtime protocols. +Nested payloads work. Result/optional handlers inside direct-call arguments +also lower through `view` wrappers; preceding owned arguments are staged in +checked evaluation order before the handler's CFG branch. A native differential +fixture checks present/default branches and named-argument side-effect order. +Handling a cloneable local result or optional snapshots that local before +SumTag and SumTake, because the source remains available to later handles. +This includes view parameters and sums containing owned lists. Direct and +indirect calls can stage a projected view before a later handler rebinds its +parent; the callee reads the original projected value. +The extracted CFG also covers many nested expression and refinement forms; +unexamined handlers and sums containing noncloneable resources still require +parity work. + +All original bytes_operations fixture functions execute in a supplemental +native main, including binary conversion errors and borrowed observer aliases. +int64/uint64/float64 string parsing produces ordinary tagged result data; the +three original math MIN runtime contracts now execute using that path. Full +fixture denominators and unsupported families remain unchanged. + +Cleanup-operation failure is recorded separately from the first terminal error. +Even after all registries empty, a double drop forces context destruction to +fail and launcher exit 72; the original runtime failure diagnostic is retained. +Named argument tests distinguish lexical evaluation from parameter order and +prove nested views end before a later legitimate move, while same-call active +loans reject conflicting moves. + +## Initial list layouts and compiled iteration + +Native list handles own a homogeneous vector of payload carriers plus the +checked element ownership category. List construction owns its partially built +list before evaluating any elements. Append mutates uniquely owned storage, +transferring the element and returning the same owning list on success; failed +append leaves both input owners with the caller. Get/first/last return genuine +optional data and clone the selected element. Explicit list clone recursively +clones owned elements. Lists have separate creation/destruction counters and +leak checks, including lists with no owning elements. Scalar, string, bytes, +sum and nested-list elements are supported; named structs/maps/sets are not. + +Insert-at transfers its element into the uniquely owned list after a valid +index and successful capacity reservation. Remove-at drops the removed owned +element exactly once and returns the same list handle. Invalid indices leave +the input list owned by the caller. Terminal failures preserve the signed invalid +index in the context-owned message, matching the interpreter's error text. + +New/length/append/insert/remove/get and primitive sort are typed native leaves. +Sort reorders the uniquely owned list in place, comparing signed and unsigned widths, +floating-point values, bools, and owned strings with interpreter semantics. +Sort carrier selection follows nested refinements over these primitives while +retaining the identical checked `list[T]` input and result; it preserves existing +element payloads and ownership. Secret and aggregate element carriers remain +rejected. Summation retains its separate primitive-only admission rule. +First/last/is_empty/reverse/repeat and math.sum run their actual compiled Jett +bodies. list.sum uses a typed leaf for all numeric primitives, including their +transparent aliases: integers wrap at the checked width, floating addition +rounds at that width after each step, and empty lists return typed numeric zero. +The remaining summation type boundary is recorded in +[the open contract](../open_design/list_sum_type_contract.md). Contextual conversion +of an empty `list[never]` to an owned element layout remains guarded. No +name-dispatch or loop interpreter is introduced. + +Before native validation, a MIR sequence pass materializes supported iterables +in a preheader exactly once, creates a cursor and length, and replaces ForEach +with ordinary Branch, SequenceGet, increment, and existing exit/backedge CFG. +Direct string iteration uses typed scalar-count and scalar-at leaves, so each +loop item matches the interpreter's Unicode scalar `for` semantics. This is +distinct from `string.chars`, which returns grapheme clusters. The string +source remains owned by its frame slot while each yielded scalar is a new +owned string handle. +Consuming iteration transfers scalar/string and move-only elements from initialized +element places. Each list slot has an independent initialized flag; a take clears +only that slot after bounds/initialization checks. Partial-list destruction visits +only remaining initialized slots, including early return/break and terminal failure. +Borrowed iteration currently yields copyable scalar/string elements only; projected +move-only views remain rejected until projection lifetimes are implemented. A borrowed container has an explicit token from preheader to +exit; ownership analysis unions active tokens across edges and preserves nested +loans of the same owner. Moving or overwriting an actively iterated owner is +rejected; moving after the exit is valid. Return/break/continue use ordinary +frame cleanup and loop exit edges. Exact effects prove iterable evaluation once. + +Sequence preheaders are selected from CFG predecessors reachable from function entry +without crossing the loop header, not from numeric block order. The lowering +requires a unique unconditional entry edge; ambiguous shapes remain rejected. +Regression coverage relocates the preheader after the loop, re-enters loops from +an outer loop, checks empty inputs, nested loans and handled loop exits, and +unpacks float32/int8/uint8 elements. Test-only allocation fault injection confirms +partial nested list clone releases its cloned prefix without consuming sources. + +The typed int64 range leaf builds owned scalar list storage with checked capacity +and checked progression at signed boundaries. One/two-argument defaults are emitted +constants, after source arguments are evaluated in their checked order. Zero step +and oversized output are terminal failures (not ordinary result.fail), matching +interpreter diagnostics and unwinding live bytes/sum/list owners in callers. + +An unconstrained `list()` may have the uninhabited element type `never`. Its +native handle has no owned elements and supports empty-list length and emptiness +checks. The backend still rejects a value-producing `never` projection and a +conversion into another list element layout until it can update nested owner +metadata without leaking later inserted values. + +String chars/words/lines/split are typed segmentation leaves returning genuinely +owned list[string] storage. Partial construction releases every inserted string +and the list on allocation failure. Join borrows its native list input for the +leaf; source-level consumption still moves the list into a tracked caller temporary, +which is destroyed at full-expression cleanup. Grapheme split requires both ends +of every delimiter match to be grapheme boundaries, including overlapping rejected +byte matches. Empty delimiters preserve endpoint empty strings. CR/LF/CRLF line +segmentation matches the interpreter. split_max, reverse and list.skip remain +compiled Jett control flow, not runtime implementations of their source bodies. + +Primitive sets have distinct context-associated owner handles. Integer and bool +elements compare their checked scalar bits; string elements compare UTF-8 content +while retaining separate owner references. Add preserves insertion order, drops a +consumed duplicate string, and leaves both inputs owned by the caller if capacity +allocation fails. Remove drops only the removed owner. Clone retains each string +and rolls back partial construction on failure. Native sequence lowering uses +initialized element slots for consuming iteration, so an early exit drops only +the elements still in the set. Borrowed iteration clones strings without moving +the source set. Primitive-backed refinement elements use their checked integer, +bool, or string representation, including the string ownership flag. The +native/interpreter fixtures cover these paths, including duplicate content and +cleanup at context destruction. + +Native maps use distinct ordered owner handles with exact key and value +ownership flags. Literal construction appends every source entry, including +duplicate keys; `map.insert` replaces the first matching value and preserves +the earlier key, while `map.remove` removes every matching entry. String keys +compare by content. `map.from_lists` zips to the shorter input, replaces +duplicate keys, and consumes both lists only after a successful result. +Lookup clones owned values into an optional sum. Explicit map clone recursively +clones owned entries and releases a partial prefix on failure. Consuming +iteration transfers each key and value separately; an interrupted iteration +drops only fields still owned by the map. Borrowed iteration clones supported +scalar and string fields. Primitive-backed refinement keys use the same key +bits and string ownership as their underlying type. Native/interpreter execution +fixtures and runtime cleanup tests cover these paths. Projected move-only +views and callback-bearing map helpers remain guarded. + +Checked generic JSON calls for `json.JsonTree` lower to trusted +`json.parse_raw` or `json.serialize_raw` source functions. The exact-parse +path is equivalent for this raw representation because its source validator +accepts every valid `JsonTree` shape. String, bool, and numeric serialization +construct a checked `JsonTree` variant and call the same source serializer; +string payloads are cloned before the tree takes ownership. A literal +`nothing` constructs the null variant. Checked `json.parse` and `json.parse_exact` +calls for `string`, `bool`, `int64`, `uint64`, `float64`, `bytes`, and `nothing` +lower to private stdlib source decoders after compiler policy checks. Lists, +sets of supported hashable primitives, string-keyed maps, optionals, and results +composed recursively of these leaves use +checked source decoders as well. They parse through `JsonTree` and its strict +accessors, preserving source error messages and ownership. A linked differential +fixture checks successful values, nested cleanup, and element/key error paths. +Narrow numeric leaves and named aggregates still require +native parse lowering. + +Concrete structs, lists, string-keyed maps, optionals, and results composed of +supported primitive values use specialized checked stdlib serialization. The +public policy gate still runs before the source body. Borrowed bytes and +containers are cloned before consuming serialization paths; linked +native/interpreter fixtures check output and +owner cleanup. Float32 fields and other aggregate JSON shapes remain pending. + +The remaining string intrinsics now have typed native leaves. Replace reuses +the grapheme split matcher and checks output capacity before assembly; empty +needles preserve the interpreter's leading, between-grapheme and trailing +replacement positions. Slugify and first-grapheme case changes match the +interpreter's Unicode behavior. Existing run-pass string fixtures now emit +native objects, and dedicated linked fixtures compare observable output with +the interpreter. + +## Native user struct ownership (phase 4) + +User structs, including concrete generic instances, use distinct context-bound +move-only handles. A record has fixed indexed field slots, each with a payload +carrier, ownership category and initialized flag. The compiler validates field +count, exact field types, owner type and lexical evaluation permutation against +the checked type table. Native code owns the empty record before evaluating +fields, initializes slots in source order, and transfers each field owner only +after the typed initialization leaf succeeds. Partial-construction failure drops +only initialized fields and the still-owned expression temporaries. + +Scalar field bits use the existing exact payload packing (including float32, +float64 and narrow integers). Field access implicitly borrows its parent. +Scalar fields copy and string fields retain. An owned field read clones the +selected move-only value; an explicit projected view remains a bounded borrow. +A nested projected view keeps its root owner live. Call loans prevent moving that root during evaluation of a +later argument. Explicit clone recursively copies the selected owned field or +whole record, and leaves the source intact. A failed deep clone destroys its +initialized cloned prefix. Whole-record calls, returns, rebindings and cleanup +use the same materialized owner slots and transfer rules as bytes/sums/lists. +Struct owners also work inside sums and consuming lists. The runtime checks +record creation/destruction counts independently, even for empty records. + +The checker records the exact Equatable.equals method identity for struct +comparison operators in per-body facts. HIR lowers equality to that direct +compiled call with its two view parameters; inequality negates its result. +The implicit `EquatableResult` boundary checks returned pending depth before +inequality negation or later expressions. It preserves the exact checked bool +type and any leading secret qualification, borrows the existing scalar result, +and uses `RejectPendingScalars` with `Equatable.equals must return bool`. +Explicit method calls keep ordinary boolean task behavior. +Native primitive comparison rejects raw struct operands. No structural equality, +handle equality or method-name dispatcher is used. + +Enums use the same owned record handle with a dense variant tag in slot zero. +Payloads occupy subsequent typed slots. Construction owns the partial record +before evaluating payloads; a matched arm transfers bound fields out, then +drops the remaining record. Clone and cleanup recursively handle owned +payloads, including nested enums. Switch validation rejects duplicate, invalid +and non-exhaustive arms. Native/interpreter differential fixtures cover unit, +scalar, string and recursive owned payloads, multiple bindings and an `other` +arm. Equality borrows both enum operands, compares dense tags, then compares +the selected variant's scalar payloads. Integer and boolean payloads compare +their value bits, floating-point payloads use IEEE equality, and strings compare +their text. A linked differential fixture also covers a user-struct payload +with owned list fields through construction, borrowed matching, clone, trace, +list storage, and cleanup. User structs inside aggregate payloads now call their +exact checked `Equatable.equals` methods. Opaque struct nodes yield through an +owned comparison cursor to generated direct calls outside the runtime context +lock. Nested comparisons use independent cursors; temporary borrowed shells +unwrap matching pending structs without traversing ignored fields. Success, +false results, and method failures retire every cursor and shell. The four +additive enum equality leaves and their `JQ` version-1 descriptor are documented +in `docs/completed/enum_payload_struct_equality.md`. +Supported aggregate payloads use an explicit comparison stack instead of the +former 128-level recursion guard. Active `(node, left, right)` paths reject +malformed cycles; repeated children on separate paths remain valid. Child +validation is deferred until its comparison executes, preserving short-circuit +failure precedence. Pointer identity is never an equality shortcut, so nested +NaN, signed-zero, pending-depth, and collection-order behavior stays intact. +This changes no ABI signature and does not add direct collection comparisons. + +`TypeInfoMatches` and `TypeInfoReflectionMatches` construct their existing +length-prefixed identity strings with a work stack. Canonical matching still +erases aliases; reflection matching preserves them. The previous 64-level +metadata cutoff is removed for finite trees. Active metadata handles detect +cycles without rejecting shared argument subtrees, and malformed alias arity, +secret flags, and missing arguments retain their existing errors. + +Bitfield values use the same typed record storage for fields, including owned +payload fields. Native construction, field projection, clone and cleanup match +the interpreter in a dedicated fixture. `to_bytes` writes checked field widths, +network or native byte order, enum discriminants and trailing `list[uint8]` +payloads to owned bytes. `from_bytes` decodes checked widths, enum tags and +trailing payloads into owned records; invalid bytes return matching error strings. +Native/interpreter fixtures cover roundtrips and decode failures, while runtime +allocation-boundary tests cover partial-owner cleanup. Dynamic field-width +validation still requires bitfield-specific native lowering. + +Machine values also use owned records: slot zero stores the dense state tag, +followed by that state's payload fields. Construction initializes an empty +record before evaluating payloads; a transition consumes its source and builds +the declared target state. State tests borrow the record. Reading a field of a +state-qualified machine projects the corresponding payload slot, and narrowing +a bare machine local checks its tag before projection. Clone and destruction +recursively handle owned payloads. The dedicated native/interpreter fixture +covers construction, transition, two- and three-state branches, payload reads, +and bare-machine narrowing; the runtime guard test rejects a mismatched tag +while preserving cleanup. + +This is not full aggregate parity. Refinement-validating constructors, +field-place assignment, borrowed iteration yielding compound views, +payload-enum equality for aggregate fields, and bitfield width validation remain +unsupported. +Composite explicit comptime constants still require baking. + +Borrowed native list and map iteration over move-only elements materializes an +owned deep clone for each loop binding; the source collection retains its +owner through the loop. A switch over a borrowed enum likewise matches an +owned clone, preserving the caller's value and existing payload-transfer +semantics. An explicit `view` passed to an owned direct-call parameter is +cloned at that call boundary. MIR ownership analysis and temporary capacity +account for the clone. An ordinary owned read of a move-only struct field also +deep-clones that field, matching the interpreter's field-copy behavior; a +projected view keeps the parent loan active instead. +Iteration over a list, set, or map projected through struct fields also borrows +the root struct for the loop and clones each binding. The projected collection +is never placed in an owning temporary, and the parent remains available after +the loop. This does not permit an owning assignment from the projected field. +Native `type.variant_value[T]` borrows the enum, reads its runtime tag, and +selects a compiler-built `TypeVariant` from checked variant metadata. The +selected record is deep-cloned into an owned result before temporary metadata +records are released; no payload field is read or moved by this selection. +Native `type.field_value[T, U]` and `type.variant_field_value[T, U]` borrow the +source value and reflected `TypeField`. Hidden compiler-built field snapshots +identify candidates with the checked result representation and secret flag. +Runtime validation compares the selected candidate's index, owner type, owner +member, field name, and type name with the supplied metadata before reading +the value slot. Linear results are deep-cloned, strings are retained, and +scalar results are unpacked, so the source stays owned by its caller. When a +`TypeField` owner mismatches, native reports the actual and expected type or +member labels before candidate selection, including empty active members. +A valid field with an incompatible requested +type reports both checked type names. Other invalid metadata still produces a +static terminal error; exact interpreter diagnostics and alias-equivalence +behavior remain parity work. +Native `type.machine_state_value[T]` uses the borrowed machine tag to select +and clone a checked `TypeMachineState` snapshot. `type.machine_field_value[T, U]` +borrows both the machine and `TypeField`, selects a checked field snapshot by +state tag and field index, and validates it before reading the payload slot. +The result is cloned or retained when it owns storage. Wrong-state owner +metadata and incompatible requested types report the interpreter's specific +errors; other metadata errors still need exact diagnostic parity. +The reflected field-read leaves reject a pending `TypeField` before inspecting +its owner or selecting a checked field. The construction leaves do the same +for `construct_put` and reject pending `TypeVariant` or `TypeMachineState` +builder-start metadata. Source-derived debug layouts format these errors with +the interpreter's full pending value while retaining borrowed metadata. +The selection and field-read leaves also reject pending source enum, machine, +struct, and bitfield records before returning reflected data. For payload +fields, a pending `TypeField` is diagnosed first, then a pending source value, +then an owner mismatch; struct field reads retain their checked field/type +validation before checking the source value. Secret-bearing owners use recursive +redaction in these diagnostics. Debug tag `Redacted` (16) has no payload and +formats `[redacted]` without accessing hidden handles or pending depth. It is +not an equality layout and never changes ownership or declassifies a value. +Public pending text equal to the literal `[redacted]` still retains its pending +wrappers; redaction is determined by the layout, not the resulting string. +Static machine layout, state, and transition reflection materializes checked +metadata as ordinary owned structs and lists, including nested `TypeField` +records. State-qualified machine types expose their machine's full layout; +alias and non-machine total probes produce empty values. +Native `type.arg` checks pending and negative indexes and reports an +out-of-range index with the reflected source type name. It validates the index +before cloning the selected checked `TypeInfo` record. +Native `TypeConstruction` is a move-only handle backed by a partially filled +owned record and checked field layout metadata. The HIR handoff keeps +each field's source type spelling alongside its canonical value type, so +alias-typed fields validate against reflected metadata without weakening the +provided value check. `construct_put` borrows +the reflected `TypeField`, consumes the builder and value, and returns an owned +result. A handled validation failure drops both consumed owners; a successful +put transfers the builder through the result. `construct_finish` checks the +owner and every required field, then transfers the completed record into its +result or drops the incomplete builder on a handled error. Deep cloning copies +both populated fields and builder metadata. Context destruction checks that +no builder metadata or owned fields remain. Concrete structs without +refinement-validating construction, bitfields with supported integer or +unit-enum bit fields, and enums without refinement-validating payload fields +use this path. Enum start validates the borrowed `TypeVariant` and its payload +field metadata, then creates an owned tagged record. Put validates each +selected variant field, and finish reports a handled missing-payload error. +Bitfield finish checks widths and reports handled errors for out-of-range +values. Machine start validates the borrowed `TypeMachineState` and selected +state fields. A state-qualified target accepts the checked state builder, then +finish reports a handled error if the selected state differs from its target. +Machine put and finish preserve the same owned-builder cleanup contract. +The checked `.jett` JSON decoder now reaches that builder for supported +concrete structs and bare or state-qualified machines with supported state +fields. An empty machine state performs no reflected field dispatch before +finishing its builder. It selects the struct, machine, or alias branch before +unrelated generic decoder bodies, so direct aliases decode through their base struct +and alias-typed fields retain their checked metadata names. +The checked source serializer now reaches supported bare and state-qualified +machines, including nested collections and public projection that omits secret +state fields. Assigning an exact machine-state local to a bare machine local +transfers ownership; an explicit clone preserves the source for later use. +The decoder now also reaches supported `secret[T]` values and secret-bearing +record and machine fields. MIR verification accepts the checked promotion of +an inner local to its secret wrapper while retaining the type-checker's +prohibition on implicit secret exposure. Public record serialization omits +direct secret fields. Raw `secret[json.JsonTree]` uses a dedicated trusted +`.jett` parser with the same open-shape exact-parse policy as `JsonTree`. +Supported top-level enums call checked generic `.jett` parse and serialize +hooks. Payload decoding constructs the selected variant inside its reflected +variant loop; raw `JsonTree` keeps its dedicated wire path. Nested enum source +dispatch remains pending. +Reflected `comptime type` dispatch borrows its runtime `TypeInfo` selector. +The runtime leaf derives a recursive structural identity, unwrapping aliases; +native branches compare it with the checker-owned identity of each specialized +arm. The selector remains live until the selected edge releases expression +temporaries, and only that arm executes. +`type.arg[T](index)` carries checked `TypeInfo` snapshots for each source-aware +type argument. Native code validates the runtime index, selects one snapshot, +and returns an owned clone. Out-of-range errors are terminal, but their exact +diagnostic still needs interpreter parity. + +Function values use an owned native descriptor with a code address and an +environment handle. Named and capture-free inline functions have an empty +environment. Inline functions that read enclosing locals extract to checked +HIR/MIR functions with leading capture parameters; creation copies those +locals into an owned environment. Internal native functions receive a hidden +environment argument after the runtime context, while the exported entry +wrapper retains its one-context-argument ABI. Indirect calls read both fields +from the descriptor and pass the environment to the checked target signature. +Function values copy by deep-cloning their descriptors and captured values; +aggregate fields own and release them. Source argument order, runtime-failure +checks, and owned result handling match direct calls. View-parameter function +values retain their checked borrow modes, including capability arguments. +Ownership planning reserves an independent result temporary for every indirect +call returning an owned aggregate or interface, as well as the callee descriptor +and argument temporaries. Linked debug/release tests cover collections built +from multiple owned callbacks, both live and baked interface facade results, +and cleanup after a later callback fails. +The `list.sort_by` callback now runs in `.jett` over +one precomputed key per item, then performs stable swaps of parallel key and +item lists. This avoids invoking Jett callbacks while a runtime collection +lock is held. The current implementation uses quadratic comparisons and swaps; +larger-list performance remains a follow-up. + +Indexed-row list sorting uses a stable runtime sort over borrowed row keys and +keeps the owning outer list intact until validation succeeds. Sortedness checks +borrow the list. Both select the checked carrier for every numeric width, +`bool`, and `string`, including primitive-backed refinements. Pending strings, +scalar keys, and rows compare as equal without observing their payloads, while +their owning handles and pending depths remain intact. Floating sortedness uses +IEEE `<=`: an adjacent ready NaN makes the check false; ordinary sorting retains +the interpreter's equal-comparison fallback for NaN. `list.group_by` now invokes +its named key callback in `.jett` and builds the map through trusted collection +intrinsics. Its current map lookup +clones accumulated groups, so larger-input performance remains a follow-up. + +Borrowed sequence tokens now end on each CFG edge leaving the loop region, +including handler default edges that bypass the ordinary loop exit. Split edges +end only that loop token, preserving outer loans. Nested iterable element IDs +are checked before sequence preprocessing resolves them. + +Native object emission also lowers checked `verify` and `property` blocks to +callable test symbols. Property `given` bindings are typed parameters, not +values synthesized by the object emitter. Test-body functions preserve the +source policy that ordinary owned values can be read repeatedly: an owned +native read deep-clones the value while a view borrows it. The default `assert` +failure records the static terminal message `assertion failed`. A custom +interpolated assertion message is evaluated only on the failed branch and +copied into context-owned terminal storage. The existing v1 result still +returns static message bytes; `jett_rt_v1_value_failure_copy` queries and copies +the exact failure text into caller-owned storage before context destruction. +The same path carries signed invalid indices from `list.insert_at` and +`list.remove_at`, matching the interpreter's terminal error text. +The launcher uses that copy for its error line, and the normal owned-value +cleanup epilogue still runs. Production program objects still select +only the checked source `main`. A separate native verify build synthesizes one +entry that calls the primary file's top-level verify functions in declaration +order, stopping on terminal failure. A separate property suite embeds the +existing deterministic `given` samples as typed HIR constructions and calls +each checked property body 100 times. The interpreter chooses inputs, while +the emitted code executes the property body. Compiler-owned HIR marks each case +with its property name and one-based trial number before constructing inputs, +then clears the context after the body returns. `PropertyCaseSet` copies this +diagnostic prefix into runtime context storage; `PropertyCaseClear` removes it. +Neither operation retains a Jett value. Terminal failure copies prepend this +context without changing caught predicate errors or the static v1 result. +The driver performs native counterexample shrinking by regenerating checked +input constructions and invoking fresh native processes through the same +launcher. It shares the interpreter's bounded shrink search, preserves the +original suite output, and accepts failed candidates only after successful +runtime cleanup. A standalone suite executable does not itself run this search. + +Evidence includes existing generic_struct and explicit_struct_equality fixture +bodies executed with supplemental mains, nested owners and early returns, +partial construction terminal cleanup (exit 71), runtime clone fault boundaries, +malformed MIR rejection, and one generated object executed with four distinct +process inputs through a test-only scalar input adapter. Allocation tests cover +returned leaf failures, not allocator abort, OS termination or resource finalizers. + + +## Callback conversion descriptors + +Nested interface-compatible container conversions carry a finite compiler-owned +conversion tree. Its callback leaf (tag 7) contains an eight-byte linked function +address. Object emission clears the placeholder function ID and writes a native +function relocation at the recorded byte offset. The host linker resolves it; +the runtime never interprets a function ID or looks up source declarations. +Descriptor parsing uses an explicit parent stack in place of the former +128-level recursion cutoff. Its wire encoding, result branch order, ownership +flags, and leaf validation remain unchanged. A descriptor must consume exactly +the supplied bytes before any value conversion begins. + +Each converted callback owns an adapter descriptor retaining its original +function, captures, debug label, and pending depth. Container allocation failure +releases every completed adapter and partial owner, while the source stays valid. +The checked HIR descriptor retains source/target signatures and the generated +function ID so reachability and MIR signature validation cover all relocated +bodies, including adapters nested inside higher-order callback parameters. diff --git a/docs/active/reference_refinement_proof_plan.md b/docs/active/reference_refinement_proof_plan.md new file mode 100644 index 00000000..3302f244 --- /dev/null +++ b/docs/active/reference_refinement_proof_plan.md @@ -0,0 +1,45 @@ +# Reference refinement proof transport + +An established refinement may be reused through an unchanged checked value. +A checked result annotation alone cannot establish that invariant for fresh +data. Runtime `Typed` labels also cannot prove a replacement payload: raw calls +and property shrinking can preserve the label while changing the data. + +The current repair transports actual source-expression facts through named and +inline calls, parameters, returns, reordered arguments, and pipelines. Raw +invocation trees and forced validators disable that reuse transitively. Checked +program and zero-argument verify entries have explicit trusted entry bridges; +entry annotations do not synthesize argument proofs. + +Three producer defects must be closed before committing that optimization: +private list sum fallback values, inline closures whose return annotations were +discarded, and reflected fields requested under unproved refinements. Reflection +must establish new requested invariants before publishing reusable result facts, +including refinements inside admitted builtin containers. New invariants beneath +secret qualifiers require a protected observer contract; conservatively refuse +that proof rather than run a public predicate on a hidden payload. +Use the actual declared field schema for exact and ancestor reuse; inspect only +active sum payloads and preserve collection order, pending depth, and source +ownership. Raw and metadata-free execution retains its old consuming boundaries. +Exact schemas preserve pending values. A changed nested invariant under a pending +container fails without an implicit join. Changed callable and nominal generic +requests introducing refinements also fail until conversion contracts are +selected. These refusals do not claim complete reflected-type support. +This is enforcement of existing value invariants, not a choice of which reflected +requests or list sum domains should be admitted. Those admission questions remain +in `docs/open_design/reflected_field_request_compatibility.md` and +`docs/open_design/list_sum_type_contract.md`. + +Canonical type owners must also remain stable after resolution. Written aliases +resolve in their lexical context once; canonical checked, captured, generic, and +reflected owners must not be resolved again through a caller's shadowing import. +Keep body imports available and isolate only canonical normalization, validation, +and selection operations. Written view wrappers affect borrowing metadata, while +their underlying refinement still requires validation at raw boundaries. + +Verification includes focused interpreter controls, real frontend-admitted +probes, linked debug/release calls after source removal, comptime and native +verify/property bodies, changed-candidate shrinking, and final workspace and +supported-host distribution gates. Native validation of newly refined returns +is a separate implementation slice; supplemental tests do not change the fixed +acceptance inventory denominator. diff --git a/docs/architecture.md b/docs/architecture.md index 0126c742..33a0ec6b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -304,6 +304,13 @@ retained separately where formatting and source tooling need them. - **Pratt parsing** (precedence climbing) for expressions — handles arithmetic operators, comparisons, boolean operators, and `modulo` with correct precedence. - **Error recovery:** On a parse error, the parser skips to the next `Dedent` at the current level or `Newline`, emitting an error node. This allows reporting multiple errors per file. +Generic-call and typed-local recognition scan balanced type delimiters without +consuming tokens. The logical line and actual token stream bound the scan; +there are no fixed token budgets for nested arguments or callable annotations. +Callable return prefixes are scanned iteratively. Recognition still requires +the matching generic `]` immediately before the call's `(`, or a complete local +type followed by its binding name and `=`; normal parsing validates the type. + ### Direct AST now, CST later Initial Jett versions deliberately parse directly into the AST while language @@ -448,8 +455,10 @@ ResolveResult { - **No unused imports** — every `use` must be referenced. - **No unused variables** — every variable declaration must be referenced. - **Inline-only imports** — `use` statements are only allowed inside functions/blocks, never at file level. Within a function or nested block, `use` must appear before any other code. Executable access to another project or vendored namespace requires an active local import; same-namespace access and canonical qualified types in declaration signatures do not. Compiler-provided standard namespaces remain available by canonical qualification under the fixed prelude and module contract. +- **Bare declarations before standard module prefixes** — a bare reference to a declared same-namespace function, such as `math(4)`, resolves to that function. A dotted standard call such as `math.abs(-4)` keeps its module prefix. Resolver facts carry this distinction into HIR. - **Duplicate namespace detection** — two project/dependency files declaring the same namespace is an error. Compiler-shipped stdlib files have a narrow fragment exception so one stdlib namespace can be split across several implementation files; duplicate declarations inside that namespace still fail normally. -- **Global constants** — registered as top-level declarations (global mutable variables are forbidden). Their initializers may use literals and same-namespace declarations, but project or vendored declarations from another namespace are rejected with `E0211`; compiler-provided standard declarations follow the fixed stdlib namespace and prelude policy. +- **Global constants** — registered as top-level declarations; the checker reports E0377 at every mutable namespace-level binding, without rejecting local mutability. Initializers admit literals (including signed numeric spellings), earlier same-namespace constant references, parentheses around those forms, or explicit `comptime expression`. Required calculations, including operators, interpolation, and calls, must be explicit; ordinary calculations report E0378 even with known operands. Project or vendored declarations from another namespace are rejected with `E0211`; compiler-provided standard declarations follow the fixed stdlib namespace and prelude policy. + - **Canonical type names** — every struct, enum, interface, machine, actor, bitfield, alias, and refinement declaration is validated before registration. Names must begin with an ASCII uppercase letter and otherwise contain only @@ -461,6 +470,25 @@ ResolveResult { - **Parent namespace aggregation** — `use net.http` imports all child namespaces (`net.http.server`, `net.http.client`) when `net.http` itself is not a declared namespace but its children are. Accessing child items uses the last segment: `server.listen(...)`, `client.get(...)`. - **Namespace exports** — namespaced declarations are private to their declaring namespace by default. `export` marks public API declarations, but executable code outside the namespace must first import it locally and then use the import's bound name or alias; exported names are not inserted into the global flat scope. +Namespace constants are materialized in declaration order during compilation, +under their declaring namespace. The compiler retains values by declaration +span in the shared checked-value table; reference execution, verify/property +drivers, and native lowering receive that same table. HIR reads carry a +`Constant` marker naming the checked declaration, which the native materializer +replaces before MIR lowering. No source initializer executes at startup and no +mutable global runtime storage is introduced. The implemented subset covers +fixed-width integers, floats, booleans, strings, `nothing`, and transparent +primitive aliases. Evaluation errors, unsupported constant types, and hidden +pending primitive values report E9001 before execution or output publication. +The [constant contract](open_design/global_constant_execution.md) retains the +unresolved ownership rules for other types. + +A constant `trace` lowers through a scoped compiler-owned temporary with the +declaration's debug type spelling. That temporary is absent from subsequent +breakpoint snapshots and release lowering removes the observation. Constant +reads inside generic bodies and inline callbacks keep declaration identity +instead of becoming closure captures or caller-dependent ambient bindings. + --- ## Phase 6: Type Checking (`jett_typecheck`) @@ -488,10 +516,15 @@ Walk all type declarations and build the type registry: - **User-defined types:** structs, enums, machines, actors, bitfields, interfaces, type aliases (including refinement types). - **Compiler-shipped resource types:** nominal `resource Name` declarations with no source representation or constructor. They are move-only, carry one - trusted cleanup obligation, and expose only name plus `resource_type` in - type-level reflection. See the - [opaque runtime resource contract](completed/opaque_runtime_resource_contract.md). -- **Function types:** `function(T) returns U`. + trusted cleanup obligation, and expose only the nominal name plus + `TypeKind.resource_type` in type-level reflection. Their primitive tag is + absent and their field, variant and machine-state metadata is empty. The + builtin enum schema and shared reflection mapper use the same resource tag; + existing enum indices and ordinary alias tags are preserved. See the + [opaque runtime resource contract](completed/opaque_runtime_resource_contract.md) + and [type-only implementation evidence](completed/native_codegen/native_resource_kind_tags.md). +- **Function types:** `function(T) returns U`, with each parameter's owned or + `view` mode included in type identity and checked for function values. - **Capability types:** `Filesystem`, `Network`, `Stdout`, `Stderr`, `Stdin`, `Clock`, `Random`, `Process`, `Environment`, `Foreign`, `Log`. Random sampling uses the explicit `view Random` API, injected per-runtime generator state, and non-cryptographic contract defined in the [Random capability and entropy contract](completed/random_capability_entropy_contract.md). The interpreter-backed [`Environment` contract](open_design/environment_argv_capability_contract.md) uses source-owned `Environment.get` and `Environment.args` over one immutable injected launch snapshot; ambient `os.env`/`os.args` are removed. `Foreign` guards the generated native C boundary specified by the [C FFI contract](open_design/c_ffi_binding_contract.md). `Log` authorizes the independent structured application-log channel defined by the [structured logging contract](completed/structured_logging_contract.md). Property tests may create only the typed test capabilities admitted by the [capability mocking contract](completed/capability_mocking_test_harness_contract.md); this does not open the capability set or add production constructors. - **Secret wrapper:** `secret[T]`. - **State-qualified types:** `Machine at state`. @@ -539,8 +572,14 @@ Bottom-up type checking of every expression: primitive type, such as a small integer literal inside a `uint64` argument or `list[uint64]` element, the expression type map records the checked type rather than the literal's default carrier. +- **Empty generic arguments:** an empty collection initially infers `never`. + For a direct inferred generic call whose arguments account for every type + parameter, the expected result shape refines only `never` parameters before + the arguments are rechecked against the resulting concrete signature. + Value-derived types are not overridden, and a result-only parameter remains + uninferable. - **Refinement type assignments:** wrapping a base type in a refinement type is fallible → must have `handle error:`. -- **Handle blocks:** verify that `handle error:` is used on `result[T, E]` and `handle:` on `optional[T]`. Verify handle blocks end with `return` or `default`. The `default` keyword inside a handle block is part of the `HandleExpr` structure — it provides a fallback value and resumes normal execution. +- **Handle blocks:** verify that `handle error:` is used on `result[T, E]` and `handle:` on `optional[T]`. Verify handle blocks end with `return` or `default`, or with `respond` inside a responding actor handler. The `default` keyword inside a handle block is part of the `HandleExpr` structure — it provides a fallback value and resumes normal execution. - **Coarsen expressions:** `coarsen value` converts a refinement type to an ancestor type. The target type is determined by the variable declaration's type annotation on the left side. The type checker walks the refinement chain to verify the target is a valid ancestor. - **Match exhaustiveness:** verify all enum variants are covered. - **Constrained generics:** For `function sort[T implements Orderable](...)`, verify that type arguments at call sites implement the required interfaces. Multiple constraints use `and` (e.g., `T implements Orderable and Displayable`). Unconstrained `T` can only be stored and passed around — no operations. @@ -554,6 +593,27 @@ Bottom-up type checking of every expression: implementation validation. `!=` negates `equals` rather than adding a second customization point. Missing implementations report E0358. Enums retain variant-and-payload equality. + Nested user-struct payloads require the same explicit method, including through + containers, machines, and refinements. The frontend validates the recursive type + graph; HIR/MIR retain exact owner-to-function targets. The interpreter calls + source methods, while native cursors yield to generated calls outside the + runtime context lock. Struct fields remain opaque at this boundary. See + [the implementation note](completed/enum_payload_struct_equality.md). +- **Direct collection equality:** `==` and `!=` on bytes, lists, maps, sets, + optionals, and results report E0376 in the frontend. Classification walks + secret and refinement wrappers; transparent aliases already share their base + type. Concrete generic bodies and explicit comptime expressions use the same + check before evaluation or lowering. Enum payload comparison retains its + separate aggregate path. +- **Erased interface equality:** `==` and `!=` on interface-typed values + report E0376 before evaluation or lowering, including `Equatable`, transparent + aliases, nested refinement/secret wrappers, concrete generic bodies and inline + functions. The enum equality traversal rejects interface leaves through its + recursive aggregate graph, while stopping at an explicit user-struct equality + method. A struct method may intentionally ignore an interface-valued field. + Existing direct collection diagnostics, concrete equality dispatch and actor + identity are unchanged. Explicit domain comparison calls use ordinary method + dispatch, ownership and secrecy checks; no dynamic equality vtable is added. - **Primitive collection hashing boundary:** Type formation for `map[K, V]` and `set[T]` accepts signed/unsigned integers, `string`, `bool`, and refinements backed by those types. Other types report E0359 before @@ -588,13 +648,17 @@ This sub-phase tracks the ownership state of every variable through the control - A variable can only be used once unless it is a `view` or has an explicitly copyable type. Numeric primitives, `bool`, `nothing`, and immutable `string` are implicitly copyable; the primitive `bytes` type is move-only. -- After a variable is passed to a non-`view` parameter, it becomes `Consumed`. +- A bare move-only owned argument becomes `Consumed` even when its formal + parameter is `view`. Written `view` retains the caller owner only at a View + formal; an Owned formal requires an owned value or an explicit clone. - Using a `Consumed` variable is a compile error. - `view` parameters can read but not consume. - `view` values cannot be returned, stored in structs, or sent to actors. - For types that support duplication, `clone` creates an owned duplicate from an owned or viewed value. Clone support is type-specific rather than universal. -- `mutable` variables can be rebound after their value is consumed. +- Assigning to an immutable local is rejected during ownership analysis + (`E0404`). A `mutable` local can be rebound after its value is consumed. + - **For loops:** `for item in items` consumes `items`; `for item in view items` borrows `items`. - **Run/join:** `run` marks a value as pending; it cannot be used until `join`ed. - **No orphaned tasks:** every `run` must have a matching `join` or `cancel` before the function returns. @@ -610,6 +674,328 @@ This sub-phase tracks the ownership state of every variable through the control - **View propagation:** Views propagate through field access and collection element access. `view list[T]` element access yields `view T`, not an owned copy. `clone` is required to get an owned value from a view. - **Closure capture analysis:** Anonymous functions may capture only implicitly copyable values from the enclosing scope. Each capture is copied into the closure. Capturing a move-only value is a compile error; it must be passed explicitly as a parameter. +Projected field assignment implements the selected local replacement contract for ordinary records and exact-state machine values in checked reference, required-comptime and native execution. Checked actor-handler updates to ordinary owned state retain original handler identity and persistent-slot ownership through reference and native execution, with bounded MSVC acceptance recorded in the native audit. Resource-bearing updates remain a separate obligation. An exact resolved live mutable owned binding may supply a finite checked ordinary-record field path. An existing exact machine-state qualification or checked narrowing may supply that state's fields without changing the state or tag. No mutable reference, caller mutation or structural owner inference is introduced. See [owned mutable field updates](completed/native_codegen/owned_mutable_field_updates.md). + +Checked field places retain the original binding, concrete/scoped body and exact +field identities through HIR and MIR. Preflight precedes RHS staging; commit +revalidates the place and transfers a complete replacement before retiring the +old field. The ordinary `StructReplaceOccupied` runtime leaf validates an occupied +slot atomically and returns the displaced bits without performing cleanup. Its +caller retains RHS ownership until success and handles terminal cleanup through +the existing drop plan. Actor roots use the separate ordinary +`ActorReplaceOccupied` leaf, which validates a live occupied actor slot and returns +displaced bits without cleanup. After success, the caller clears the transferred +staging owner, then retires the displaced owner through the checked cleanup plan. +Neither ordinary leaf grants Resource custody. + +Nonzero facts for field reads use the exact root and checked finite field path. +A write invalidates affected root facts after evaluating its RHS. Loop backedges +and writes inside condition prefixes cannot reuse a stale guard. Borrowed enum +match payloads carry their exact arm/tag/field origin through caller validation +and canonical MIR remapping. Transparent `coarsen` and `declassify` views retain +an independent native match snapshot before destructive payload extraction; +owned matches retain ordinary owned transfers. + +The checker retains E0404 for immutable/temporary roots and E0401 for views, including mutable view parameters and view-derived aliases. Actual consumed/uninitialized/loaned roots must be rejected by ownership/liveness analysis, not inferred from a mutable flag. Paths through enclosing nominal refinements require whole-value reconstruction and validation; bitfields retain constructor-and-rebind updates. Exact field types and their existing refinement conversion rules remain authoritative. + +Compiler-produced reflection metadata is not an ordinary mutable user record. +Field paths cannot traverse the canonical builtin metadata owners: TypeInfo, +TypeField, TypeBitfield, TypeBitfieldField, TypeMachine, TypeMachineState, +TypeMachineTransition and TypeVariant. The type table records their exact +identities when creating the builtins and preserves that identity through clones +and canonical remaps; checked-place construction and revalidation use that +registry, never spelling or structural shape. Whole genuine metadata values can +still replace fields in user holders. This maintains the existing constructor +and reflected-observer authority boundary. + +The checked place and every ancestor's readiness/state are established before evaluating the RHS once. Existing runtime pending-inspection failures are preserved without an implicit join or reading pending payloads. Precommit RHS/validation/allocation failure preserves the old field and retires temporaries; successful replacement retires the displaced field once and preserves sibling ownership, exact metadata, task depth and logical clone independence. Cleanup failures keep established failure precedence. Typed HIR/MIR place, move/loan and drop plans must make those edges explicit before emission. Resource custody and Graph/original-Source authentication cannot be bypassed by the ordinary update route. + +The checker rejects known move-only views at owned return boundaries and +ordinary bare argument boundaries with E0401, including calls whose formal is +View. It reuses declaration facts for view parameters, local views, and forwarded +aliases, retaining the borrow through parentheses, `coarsen`, and `declassify` +when the checked result is move-only. Ordinary, generic, indirect, pipeline and +actor calls obey the same boundary. Lexical verify, property and breakpoint +expressions retain Rule25's ordinary-data observation behavior only through a +validated copy, borrow or owning snapshot. Capability and Resource authority +cannot be acquired through observation. Explicit written view arguments to Owned +formals retain E0375; implicit-copy results, valid clones, ordinary owned field +copies without Resource data, and owned call results remain valid. +Resource-bearing field acquisition requires a nonowning view or a genuine owned +transfer. + +A rejected typed invocation has no ownership handoff. Ownership recovery reads +its dependencies in source order, preserving independent nested-call effects and +use-after-move errors. Invalid builtin type arguments retain the operand shape for +checking but have an ERROR result and cannot select an acquisition packet. + +Caller ownership facts retain the resolved target, raw source occurrence type, +formal access, original written syntax, exact binding identity, and lexical +argument order in each ordinary, concrete generic, and reflected body. Pipeline +inputs have a separate checked input fact from the step's callee and output; +sharing a syntax span does not merge these roles. HIR seals source facts before +physical conversions and retains generated metadata operands separately. A +physical View wrapper cannot turn a bare transfer into caller retention. + +MIR records finite owning, copying, observation and loan stages while preserving +the original source certificate. A relinquished View input first moves its full +endpoint into one owning temporary at its lexical position, then borrows that +temporary through the call. Compiler-generated calls seal their exact operation, +result, target and argument order through one fallible factory. Their stages +retain an immutable acquisition witness and a once-only private storage +association. Private generated-producer shapes preserve the exact operation, +target, typed occurrence, ordered fields and backing identities. Restoring a +materialized local requires its unique dominating owning initializer and the +same immutable producer shape. Ordinary snapshots require recursive ordinary-data +proof; a callable signature or opaque carrier cannot grant cloning authority. +MIR joins the packet to its exact initializer, Begin/End loan and typed original +occurrence before pruning, move planning or native eligibility. +Observation snapshots can precede existing interface conversion only when the +sealed raw source is ordinary data with an independently supported clone. MIR +clones that exact backing once and restores the existing conversion spine; +an erased Interface signature does not grant cloning authority. + +Intrinsic operands normalize their closed physical View roles before the common +ordered caller staging check. That check distinguishes relinquished owners from +retained borrows without using a collection's clone support as transfer authority. + +Extracted breakpoint conditions retain private constructor-owned lexical regions. +The ownership walker rejoins their exact statements, terminators, CFG boundaries +and final endpoints before selecting observation context. Canonical sequence and +uninhabited-sum preparation update these records through checked edits and remaps; +copied, moved or forged markers cannot grant observation context. Archived source +conditions do not keep removed blocks or locals alive. + +Compiler-generated refinement handlers retain their ordered exact predicate +identities, candidate backing, error-empty tests, selecting branches and shared +failure entry. One validated output and every owning default must reach the use +through that checked graph. Transparent default conversions cannot acquire a +borrowed loop binder. Canonical compaction remaps the private proof without +turning it into an execution read or cleanup owner. + +View-loop binders carry a private typed element/key/value proof only inside their +exact lexical loop body. MIR retains the original checked loop inventory while +canonical sequence preparation records the exact borrowed source, cursor, length, +binders and body/exit graph. Prepared records cannot manufacture a missing original +loop, authorize a binder outside its body or acquire its borrowed storage. Dense +compaction remaps these identities without treating proof metadata as execution +reads or cleanup owners. + +Canonical simplification of a checked uninhabited sum arm carries private +preparation metadata across compaction. Distinct absent-success and present-success +records preserve their exact source, output, runtime tag and selected edge. An +arbitrary Goto or default cannot replace the checked sum edge, and a retained +successful payload still requires its exact extraction and owning source. + +Expression lowering descends through existing containers and conversions when a +nested checked call requires owning staging, even if it contains no handler. +Short-circuit boolean operands retain their selected control-flow arm and lexical +evaluation order. These facts do not supply Resource runtime layout, providers, +async or actor execution. + +Directly written return-view annotations report E0401 by unique annotation +span, including unused generic headers, interface/inherent/implementation +methods, inline functions, and callable result types. This declaration check +does not expand named aliases or prewalk unused generic bodies. It adds no +lifetime or projected-place policy. General call-produced borrow provenance +remains a separate gap. + +Checked binding facts distinguish owned copies from borrowed local aliases by +declaration identity in each ordinary, generic, and reflected body. HIR records +an alias's immediate backing `LocalId`; every alias slot in the finite chain +remains immutable. Its terminal may be an immutable owner or view parameter, or +a mutable owned local/owned parameter authenticated by the exact active checked +binding fact. Mutable view-parameter backing remains unsupported. This bounded +mutable-owner extension has focused MSVC acceptance recorded in the local-view +note. A direct initializer preserves the same carrier and interface erasure. A projected initializer uses its existing +finite typed `Field` nodes to prove each ordinary-struct, bitfield or exact +declared `MachineState` owner, canonical field index, intermediate type, endpoint type, +and terminating local identity. A qualified-machine step resolves the exact +machine/state definition before indexing that state's fields. A stale state, +foreign owner, invalid index, or different endpoint cannot supply a proof. +Every machine step must preserve its exact state, and the terminating local +must match its actual declared source type. A flow-narrowed bare backing slot +is outside this bounded path contract. The +whole owner's type can differ from the selected field type; the typed path, +not carrier equality with the whole owner, establishes that dependency. + +The shared projected-view proof accepts a base whose type is the exact owner, +or one direct `Secret(owner_type)` for a declared Struct, Bitfield or +MachineState. Declaration/state/index lookup remains exact. For a secret base, +`nothing` and already-secret fields retain their declared TypeId; a bounded +walk of a declared refinement's base chain detects secrecy without erasing its +nominal identity. Every other field expression must be exactly +`Secret(declared_field_type)`. This is separate from the existing final local +annotation's outer-secret promotion. The original secret receiver stays in the +initializer, preserving existing pending guards. No arbitrary wrapper peeling, +declassification, carrier retagging, hidden owner or runtime change is added. + +Declaration-backed endpoint types retain their checked qualification and +existing representation checks. No field-index path is duplicated in alias +metadata, and no runtime ABI, hidden owner or implicit allocation is introduced +for these paths. Each such borrowed initializer must pass the typed proof before +optimization can remove unused locals or unreachable blocks. Generated snapshots and captured copies remain owned; +origins from another body cannot survive as borrowed captures. MIR retains and +remaps the backing chain, expands alias reads into whole-owner liveness, and +excludes aliases from cleanup slots. Pending checks remain at each field access. +Bitfield steps resolve their exact declaration and read the selected +`BitfieldFieldDef.ty` from borrowed metadata, preserving the declared semantic +type without copying a field table. Their canonical FieldId is zero-based +and needs no physical tag offset. The canonical machine FieldId is also +zero-based within its declared payload; native emission alone adds the +existing machine tag-slot offset. Neither path duplicates physical indexes +in alias metadata. Borrowed linear +field reads keep the existing handle, while ordinary owned reads clone it. +Concrete generic contexts preserve their checked endpoint types and binding +modes: implicitly copyable endpoints remain copies, while move-only aliases +retain their validated source dependency. +Borrowed matches clone their scrutinee before taking payloads, and callbacks +borrow their descriptor while it is invoked. Explicit clones still acquire +independent ownership. +MIR preserves validated borrowed initializers through transparent `coarsen` and +`declassify` wrappers. Those bindings keep their backing handle; the owning +snapshots used for ordinary value expressions must not replace their origin. +Copy-owned alias expressions acquire retained string or cloned descriptor +temporaries before entering containers; borrowed argument lowering reads their +existing handles. Plain string view expressions retain ordinary copy behavior. + +Ordinary handled optional/result projections authenticate the explicit outer +View, inner View of an exact stable sum binding, +concrete endpoint type and terminal Return continuation. HIR retains the +checked binding identity. The bounded mutable-owned extension records a +private receipt for every canonical declaration, including final primitive/String +copies. It joins the original mutable Owned binding fact, alias fact, initializer, +concrete scope path and complete real function snapshot; View parameters and +borrowed match/iteration binders gain no owning authority. MIR consumes that +receipt before admitting a mutable backing and rejoins it against its immutable +original HIR function. MIR seals the original declaration, current local +headers, tag observation, selecting branch, success projection, absent path +and final initializer before pruning or ownership planning. Generated sum and +success-payload locals remain nonowning; a move-only final alias has the same +backing dependency and no cleanup slot. + +The checked final ownership mode distinguishes a borrowed payload from an +implicitly copyable primitive or String destination. The copy destination may +be mutable, and String receives an owning retained copy. After it is +initialized, constructor-only projection intermediates leave the borrow +inventory, permitting a later ordinary shell transfer or unwrap. The Result +Fail companion likewise owns one String copy. A nonconsuming runtime payload +read preserves the shell and selected payload identity; pending scalar depth +and opaque container metadata remain intact. Outer pending sums fail at Handle +observation before either extraction or failure continuation. + +This bounded ordinary proof does not grant Resource custody or prove the +payload identity hidden by erased interfaces, callables, actors or capability +carriers. Alternate outer Default payload lifetimes, mutable View-parameter +roots, missing or inconsistent selected owner facts, temporary-backed Handle +aliases and general loan expiry remain outside it. Mutable-owner admission +changes neither generated nonowning slots nor copied-payload loan retirement. +The initial sum snapshot follows genuine callable sealing; later private sealing +reports exact old/new callable tokens for replay against the protected body. +Reflected source normalization omits only the independently validated sum proof +inventory; the sum snapshot retains exact callable identity. Baked/materialized +callable composition remains explicit unverified debt. See the +[bounded mutable-owned sum acceptance](completed/native_codegen/native_mutable_borrowed_sum_backings.md) +and the +[ordinary borrowed-sum implementation](completed/native_codegen/native_ordinary_borrowed_sum_codegen.md). + +Native ownership planning conservatively refuses owner consumption or rebinding +after a persistent move-only source alias may have been created on a reachable +path. Final copied primitive/String bindings retain the ordinary ownership +behavior described above. This does not select general source loan expiry. +Mutable source alias chains, temporary-backed source aliases beyond the bounded +ordinary function-body slice below, allocating conversions, flow-narrowed +bare-machine origins, and field paths without the checked declared-owner proof +remain explicit native limitations. Read-only +aliases grant no write authority, and retained dependent loans block changes to +their owners, including after the last alias read. Selected owned-local field +updates follow their checked ownership and loan requirements. The producer +follows retained local-view binding dependencies through only +the selected concrete fact map, resolving each exact DefId and declaration span +with bounded cycle detection. Original types, modes and mutable flags remain +unchanged. HIR still requires the exact root join; missing or inconsistent facts +cannot fall back to flat global maps. Focused MSVC acceptance is recorded in the +local-view note. Borrowed-sum Handle aliases keep their separate checked backing +receipt and [bounded mutable-owned acceptance](completed/native_codegen/native_mutable_borrowed_sum_backings.md). +Owner transitions after source alias creation follow the same +conservative owner-change refusal; see +[the bounded implementation](completed/native_codegen/native_local_view_aliases.md) and +[the stable projected-view contract](completed/native_codegen/native_stable_projected_local_views.md). + +The selected temporary-backed local-alias contract requires one compiler-owned +whole producer result, evaluated once at the original initializer and retained +to the declaration's original lexical scope exit. Immutable aliases and their +forwarding chain remain nonowning. A private checked association must join +the original body/declaration, owned producer and concrete type, finite typed +projection, generated backing initializer, lexical scope and every cleanup +edge. `CheckedViewSource::Other`, matching types or synthesized LocalIds alone +are not proof; generated storage receives no invented Source DefId. Scope +identity must survive extraction and remapping before pruning. Last-use +liveness cannot retire this backing early. Each executed loop declaration +has its own owner; exits retire only crossed scopes, after acquiring any +owned Return/Respond/Default result. Aliases receive no destructor slots. +Copyable endpoints, readiness/error order and existing escape/loan rules stay +unchanged. No actor/Resource authority or call-view End extension follows. +The bounded implementation recognizes original ordinary nongeneric function-body +declarations whose checked direct named producer returns a finite ordinary +struct, projected through exact typed struct fields to bytes or a list. Its +native owner proof admits finite primitive/string/bytes/list/struct data; other +owner shapes remain outside this slice. Checked reference transport currently +supports bytes; list transport remains pending. +The original Other fact stays intact. Admission additionally requires an exact +immutable original `VarDecl` with a written `view` initializer and its canonical +variable fact. Viewed pattern and iteration binders retain their existing +borrowed paths without temporary owner authority. Complete nested declaration +lookup preserves refusals for genuine written temporaries at raw entries and in +unsupported regions. A private SourceCall receipt joins the original checked +declaration, producer, whole-owner header, projection and +scope; single-use canonical capture and the immutable archive's expected +inventory precede current-marker validation. Exact conversion and peer replay +cannot infer this authority from matching names, spans or layout. Reusing the +archive grants no Resource custody or execution family. + +MIR validates its immutable HIR association and complete current graph before +pruning or planning. It authenticates either the direct whole-result acquisition +or its exact staged call/End/move chain, then pins the initialized owning slot +through the original function-body cleanup edges independently of alias reads. +Aliases remain nonowning. Existing result emission acquires the owned return +before dropping occupied slots. Checked reference activations retain whole +Values in vacant/live/retired storage with private nonowning tickets and named +argument frames, and bind acquired output to the exact completed activation. +Original root definitions declared through `mutual:` use the immutable +resolver's definition-name-span edge to the existing declaration DefId. The +lookup remains unique and registration retains complete typed AST, canonical +name, namespace and signature checks; temporary receipts additionally retain +the physical root, original call and activation. A later body is not required +to reuse its earlier declaration span. Interface method dispatch stays outside +this ordinary-root activation family. No Runtime helper, ABI, implicit clone or +fabricated Source DefId is added. + +Focused compiler/reference controls and core libraries pass. A genuine checked +Source control covers a factory's staged temporary argument before a later +Handle; genuine nonidentity local compaction is separately covered. Synthetic +disconnected-block controls are refused by the public graph guard/pruner and +exercise only isolated exact temporary replay back to the original graph; they +are not Source-generated pruning evidence. The two bytes temporary controls +pass four paired Source-deleted native executions (Source Debug/Runtime Debug +and Source Release/Runtime Release). The complete refreshed regression matrix +passes 23 tests/46 executions with all Source/Runtime/fixture guards intact. +These receipts do not measure native allocation/destruction timing, which +remains pending. Nested declarations, loops, actor/Resource/generic/reflected +contexts and types, and other producer families +still owe implementation and cleanup evidence without new checker bans. The +full selected contract, unchanged Source09 and remaining gates are recorded in +[the temporary local-view note](completed/native_codegen/native_temporary_local_views.md). + +The bounded [List reference transport](completed/native_codegen/native_temporary_list_views.md) +extends the same private owner/ticket adapter through exact retained generic +instance facts. Canonical mutual body/DefId joins and actual written-VarDecl +classification remain mandatory; pattern and iteration borrows keep their +existing paths. The complete matrix passes 24 tests and 48 paired native +Debug/Release executions under its fresh matching gate. It includes the focused +List pair, with no additional distinct count. Physical cleanup and broader +lifetime gates remain open. Runtime helpers and ABI are unchanged. + **Implementation strategy:** Abstract interpretation over the control flow graph. At each program point, maintain a mapping from variable → ownership state. At control flow joins (if/else merge points, loop entries), states must be compatible: ``` @@ -663,10 +1049,11 @@ Track which capabilities flow through the program: through `test.mock.environment`. See the [Environment and argument capability contract](open_design/environment_argv_capability_contract.md) and implementation issue [#170](https://github.com/vycdev/jett/issues/170). -- **Scripted provider seams are exact.** Successful host-side Random and Clock - conformance runs reject unconsumed samples instead of accepting a script whose - expected operations were only partially observed. Runtime failures remain - primary and are not replaced by this successful-run cleanup check. +- **Scripted provider seams are exact.** Successful interpreter and native + Random, Clock, and Graphics conformance runs reject unconsumed inputs instead + of accepting a script whose expected operations were only partially observed. + Runtime failures remain primary and are not replaced by this successful-run + check. - **Application logging is a capability operation.** The source-owned `log.emit` and level wrappers borrow `view Log`. Their public types, wrappers, private trusted-kernel boundary, capability propagation, and secret-output @@ -677,12 +1064,30 @@ Track which capabilities flow through the program: - **`trace` and `breakpoint` are capability-exempt** — they produce output/open connections without requiring a `Stdout` or `Network` capability. They are compiler keywords with special treatment, compiled out in release mode. - **`print` and `println` are compiler-owned debug builtins, not ordinary I/O.** They remain secret-output boundaries and require no `Stdout` capability. The - current interpreter shares its stdout path with `Stdout.write`; a distinct - debug-event channel is pending. Mode-aware checking rejects them in release - builds with E0362. Future backends must either route them through a - debug diagnostic channel or reject them; they must never silently lower to - ambient process stdout. Verify/comptime entrypoints may allow them only when - debug text is isolated from protocol output. See the + interpreter keeps one canonical ordered buffer of typed debug events with + exact text. Accepted Print/Println calls retain caller argument type metadata + before evaluation and recursively redact hidden secrets with `[redacted]`. + Retained concrete identities and builder field types drive observation; + the reference fallback for missing composite/field metadata redacts the + subtree without inventing an owner contract. Native malformed-schema guards + remain errors. Print rendering does not invoke user methods or inspect + captures, and emits one complete event after all arguments succeed. This + selection leaves pending-sum diagnostics separate. The accompanying direct-field + diagnostic repair retains checked receiver types; integrated verification is + pending. Native typed schemas and redaction checks remain intact. Runtime + entrypoints emit + those bytes to stderr; only `Stdout.write` + enters application stdout. The native DebugPrint leaf shares the diagnostic + stderr transport used by trace and breakpoint without changing the C ABI. + Driver results retain comptime and frontend verification observations apart + from runtime events, including observations preceding a failure. Baked values + contain no event replay. A separate attempted-expression/context ledger prevents + duplicate failed comptime recovery without caching failed values. Property + shrinking remains private, including replay errors. Agent output + serializes explicit phase and kind with escaped exact text; it never infers + event kinds from user text. Mode-aware checking still rejects print builtins + in release builds with E0362. Future backends must provide this diagnostic + channel or reject the calls. See the [decided policy](open_design/print_debug_builtin_policy.md). **Implementation:** For each function, compute the set of semantic capabilities @@ -704,12 +1109,12 @@ Track `secret[T]` values through the program: payloads. Strings compare as UTF-8 bytes. The interpreter treats length as observable, rejects length mismatches directly, and passes equal-length payloads to a vetted constant-time byte comparison with no content-dependent - early exit. Unsupported payloads are rejected by the checker. Future - HIR/MIR/native lowering must use a vetted - constant-time primitive or otherwise preserve this no-short-circuit contract; - [#33](https://github.com/vycdev/jett/issues/33) defines the boundary, while - [#20](https://github.com/vycdev/jett/issues/20) and - [#22](https://github.com/vycdev/jett/issues/22) own future lowering. + early exit. Unsupported payloads are rejected by the checker. HIR/MIR/native + lowering must preserve this no-short-circuit contract; the Cranelift native + path uses the same constant-time byte primitive for supported string and + byte secrets. [#33](https://github.com/vycdev/jett/issues/33) defines the + boundary, while [#20](https://github.com/vycdev/jett/issues/20) and + [#22](https://github.com/vycdev/jett/issues/22) cover remaining native work. - `json.serialize` on a struct with secret fields is a compile error -> use `json.serialize_public`. Public JSON serialization omits secret-bearing record fields; it may descend through containers to project nested records, but rejects secret wrappers and secret-bearing enums when their secret data cannot be projected away through record fields. A future explicit full-serialization path can require a declassification token. - **Secret refinement types:** For `type ApiKey = secret[string] where string.char_count(value) == 40`, the `where` clause operates on the inner `string` value — the constraint function implicitly receives the unwrapped value for validation purposes only. @@ -732,6 +1137,10 @@ For each `machine` type: when an API intentionally erases precise state. Parameter, return, and local annotation boundaries are authoritative: once typed as bare `Machine`, exact state is forgotten and is not recovered from construction or caller history. + Native HIR retains that declared bare type for local storage while constructor + and assignment expressions keep their precise state, owner, and source span. + Ordinary owned rebinding therefore accepts another valid state of the same + machine and retires the old payload through the existing cleanup plan. A positive `if value at state:` guard narrows that local variable back to `Machine at state` for the guarded branch, exposing state payload fields and legal transitions there. A bare `Machine` value cannot satisfy a @@ -744,6 +1153,8 @@ For each `machine` type: - A narrowed local keeps the exact state type for the guarded branch. Assigning a different state to that same local is rejected rather than silently widening the branch fact in place. + Native assignment verification checks the target's exact state alongside the + underlying storage type, including when the backing slot is a bare machine. - If the guarded value is a bare machine and an `if` / `else if` chain excludes all but one declared state, the final `else` branch narrows to that single remaining state. For `if not (value at state):`, the immediate `else` branch @@ -797,7 +1208,8 @@ concrete method bodies; struct construction records canonical field order and refinement validation; struct field access uses dense field IDs; list/map constructors preserve lexical element order; and pipelines become nested checked calls with the piped value as synthetic argument 1. Result, optional, -and refinement-boundary handles are explicit typed HIR operations; their +and refinement-boundary handles are explicit typed HIR operations; owned +`ok`, `fail`, and `some` payloads transfer ownership into the wrapper. Their failure blocks distinguish local `default` fallback from function `return`, and step-local handles wrap only their intermediate pipeline call. Enum construction carries a dense checked variant ID and payloads; exhaustive @@ -807,16 +1219,433 @@ arguments and explicit lexical evaluation order, core expressions, returns, bran loops, `for`, assertions/debug controls, string interpolation, comptime markers, explicit declassification/coarsening, state tests, and task-control markers are also covered. Bitfield and state-machine construction, transitions, -and fields carry explicit checked types plus dense field/state IDs. Remaining -compiler-owned calls carry canonical intrinsic identity, typed arguments, and -lexical evaluation order after type checking has authorized them. Inline -functions and indirect calls retain explicit parameter/local identity; -comptime type-bind scopes erase to checked HIR scopes; actor spawn/send/ask -carry typed operands and message identity. Actor receive handlers are -deterministic HIR functions whose locals preserve checked capability, state, -and message bindings; actor construction, persistent state layout, scheduling, -and dispatch remain actor-runtime work. Remaining source constructs are staged -by the [initial HIR lowering plan](active/hir_lowering_plan.md). +and fields carry explicit checked types plus dense field/state IDs. Each HIR +local keeps its declared binding type separate from its checked runtime type, +so state narrowing does not change the debug label. Native `coarsen` verifies +a checked refinement ancestor and transfers its base +representation, including ownership of aggregate payloads, without a wrapper. +MIR copy and move planning propagate the enclosing argument's borrow mode +through coarsening, declassification, and validated-refinement expressions. +Native argument emission follows that same mode through transparent wrappers, +including local and field projections, rather than loading an owned argument +from a view-only binding. Direct and indirect calls retain ordinary cleanup +when a later argument fails. + +For source `list(...)`, `map(...)`, `some(...)`, `none`, `ok(...)`, and +`fail(...)` literals whose checked type has outer `Secret` wrappers, HIR builds +the constructor at the exact inner container type, then uses the existing +`InterfaceCoerce` to restore the original checked TypeId and source span. It +peels only leading secret qualifiers and requires the matching constructor +shape; nominal refinements and arbitrary producer types remain untouched. +Payload interface boxing, function adapters, and inferred `never` conversions +still run against that exact inner type. Native constructor validation remains +strict, and the outer qualification preserves the existing representation, +evaluation order, pending metadata, and ownership. +Source `Call` and `GenericCall` expressions with a checked struct-construction +fact use the same split when the constructor does not validate refinements and +the peeled checked TypeId exactly matches its nominal `struct_type`. Field +conversions and lexical evaluation order remain inside that constructor. Calls +to ordinary producers, reflected builders, mismatched nominal types, and +refinement-validating result constructors retain their existing metadata. +For a checked validating struct call, HIR also separates leading secret wrappers +from an exact `result[Struct, string]` output. The nominal success type, +validation flag, per-field predicate chains, and source evaluation order remain +inside the constructor. MIR extracts predicate control flow within the existing +qualification coercion. An empty predicate chain keeps the validating result +shape; predicate failures retain ordinary result-data handling. +Resolved bitfield and machine declaration calls use the same qualification +boundary. HIR removes only leading `Secret` wrappers to identify an exact +bitfield value or `result[Bitfield, string]`, retaining `validates_widths` and +the declaration's nominal TypeId. Machine construction retains the exact +`MachineState`, validates its machine and source-selected state identity, and +preserves its payload order. The outer checked type and span are restored by +`InterfaceCoerce` after the unchanged constructor. Ordinary producer calls, +nominal refinements, reflected builders, and machine transitions do not acquire +constructor metadata through this path. +HIR local allocation also retains the checked declaration TypeId when it has +outer `Secret` qualification, including repeated wrappers. A less-qualified +initializer keeps its checked producer type; the existing `Let` coercion adds +the declared qualification. This does not extend contextual literal inference, +and `declassify` continues to remove one outer secret layer at a time. + +Native field validation follows the checker's direct-secret receiver rule for +structs, bitfields, and state-qualified machines. A receiver is either the exact +nominal owner or exactly one `Secret` around that owner. The declared field +TypeId remains exact for public receivers, `nothing`, and already-secret fields +(including secret-backed refinements); other secret receiver fields require +exactly `Secret(field TypeId)`. Emission retains the original receiver type for +pending diagnostics and ownership, uses the existing machine-state field offset, +and preserves field depth and owned-copy behavior. Nested-secret or refinement +receivers, mismatched nominal owners, invalid indexes, and forged result types +remain rejected. Direct-field receiver failures retain the original checked +receiver type in both execution paths. The reference renderer observes the +original value before consuming its wrappers and applies the existing typed +secret redaction, matching native pending receiver diagnostics. Unsupported +`handle` receivers use the same typed formatter before wrapper consumption; +pipeline steps retain their full result type independently of the handler's +error-child type. Neither diagnostic joins pending values, declassifies payloads, +or changes the failure point. Pending-sum controls have bounded reference and native acceptance; other terminal paths retain separate audit obligations. + +For refinement boundaries, HIR lowers each checked predicate to a +compiler-owned pure function. MIR calls unvalidated ancestor predicates in +base-first order and branches to the source `handle error` block on a false +result; successful construction transfers the original value through an +explicit validated-refinement expression. An already-refined input skips its +validated ancestors. A secret-backed input is unwrapped only for the checked +predicate call. Direct struct constructors use the same predicate path for +refined fields. Predicate runtime failures remain a future slice. +Predicate functions retain their declaration namespace. Interpreter validation +uses the same context: caller locals, namespace aliases, generic arguments, +scoped type bindings, and per-function checked maps are isolated until validation +returns. Predicate `value` has the fully coarsened base type with the outer +secret wrapper removed, matching the checker and native predicate parameter. +Inherited interface refinements therefore dispatch through that base inside +their predicates. Both successful and rejected boundaries restore caller context. +Interpreter local and direct struct-field boundaries capture the original +checked source type before evaluating the expression or normalizing its value. +Exact established refinements skip validation; ancestor inputs skip only the +validated prefix. Result and optional handling supplies the checked success +payload type only when extracting that payload. Explicit coarsening and sibling +refinements cannot reuse discarded or unrelated predicates. This proof path +does not use runtime `Typed` labels; metadata-free calls and property-shrink +candidates retain their existing validation entry points. +Direct generic struct construction resolves actual type arguments in caller +context, maps them to the struct's own parameters, and resolves declared fields +in declaration context. The selected concrete field types determine fallible +result wrapping, independently of the caller's generic bindings and imports. +Checked named calls, method calls, NamedFunction values, and source pipelines +transport each argument's original source proof alongside its value. Named +argument permutation moves both together. Return signals retain the checked +return expression type until the named function boundary; a declared return +type is never a proof of that expression. Public/raw call entry points disable +refinement proof reuse throughout nested execution, including checked bodies, +callbacks, and forced refinement validators. This prevents forged refined +aggregate children from gaining proof through their declared field or element +types. Checked program and zero-argument verify entries use an explicit bridge; +their inputs still undergo forced normalization without invented source proofs. +Inline closures retain their declared return annotation in the captured lexical +context. Parameter and return normalization and refinement validation run before +that context is restored; actual return-expression facts supply the same proof +as named calls. Host-generated callbacks retain their separate forced-call path. +Native returns into a root refinement validate the original checked expression +before representation coercion. The selected concrete function signature supplies +the destination, including generic methods and scoped inline functions. Exact +sources retain their proof; ancestor sources skip their established predicate +prefix. MIR stages the candidate once and uses its existing refinement checks. +A compiler-owned `RuntimeFailureMessage` borrows the captured string error and +terminates through the existing runtime failure leaf, preserving cleanup and +the first predicate error. It has no source spelling and adds no ABI operation. +Fresh private sum fallback results validate their selected actual type before +checked source execution can reuse it as a proof. Raw and metadata-free calls +keep the existing forced facade return boundary. This does not select a broader +summation domain or change primitive arithmetic. +Reference and native reflected field producers establish newly requested +refinements before the result type becomes reusable proof. Already admitted +requests use recursive producer trees for `list`, `set`, `map`, `optional`, and +`result`, including root refinements over those wrappers. A complete structural +pass over occupied payloads precedes every new predicate. Collection order is +preserved, with each map key before its value and child validation before an +enclosing root predicate. Empty collections, absent optional payloads, and +inactive result arms have no child checks. Pending changed wrappers fail +readiness before predicates; primitive pending children retain the ordinary +predicate-error boundary. Neither pass joins a value. + +Actual checked declared schemas supply exact or ancestor proof; runtime labels +do not. Requested named ancestors skip their established predicates, and the +nearest established common named ancestor supplies a root branch's proven +prefix. Exact whole schemas preserve pending depth. Child proof reuse does not +make a changed outer wrapper exact: a named root over `list[Positive]` requested +as `list[Positive]` still needs a ready list, despite zero child predicates. +Requests containing no refinements skip producer validation. The root-ancestor +lookup never obtains facts from an outer secret qualifier, collection, or +nominal payload field. + +TypeInterner retains immutable canonical argument TypeIds for instantiated +generic structs. The checker registers the selected arguments before resolving +recursive fields or installing the instance cache; repeated registration must +agree and invalid IDs or conflicting arguments fail registration. Producer +refinement detection follows these argument edges, including unused parameters, +and never substitutes ordinary struct, enum, machine, or bitfield payload +fields for generic arguments. Transparent aliases share canonical IDs while +source reflection retains their written identities. Closed HIR type validation +also visits unused argument edges, so an unresolved error type cannot hide in a +phantom parameter. This metadata does not select a nominal conversion. + +Reference reflected-name normalization tracks only the active alias-expansion +path. Completed sibling arguments leave that path, so repeated aliases and +refinements compare independently while actual recursive re-entry remains bounded. +Native reads carry complete per-declared-slot typed validation trees in HIR. +Each node retains its actual declared source TypeId and requested TypeId; +validators reconstruct the canonical action, occupied wrapper schema, and +unproven refinement suffix. Predicate targets must be the exact canonical +refinement declarations with their selected base and input types. A new root's +base node retains the original actual root TypeId so erasure cannot manufacture +an exact-schema exemption. + +Direct and piped getters share the canonical hidden field-metadata tail, +appended after the original source and selector operands. Narrowed machines keep +their checked state identity while metadata uses the complete machine layout. +The one-operand observers `type.variant_value`, `type.machine_state_value`, and +`type.arg` likewise share their direct/pipeline metadata expansion. User operands +run first; the checked pipeline output type selects each hidden metadata value's +type, because the pipeline step span describes its input. Variant ordinals and +discriminants remain distinct, and narrowed machine observations retain all base +states. Existing runtime observation and cleanup leaves consume the same metadata. + +MIR stages the original operands once and completes the raw getter's existing +selector and compatibility checks before dispatching the selected slot's tree. +The getter produces an independent candidate; both passes leave it unchanged. +Sequence projections clone private child snapshots, and occupied sum projections +take payloads only from private sum clones. The first failure borrows its captured +string through `RuntimeFailureMessage` before cleanup, preserving the original +predicate error. Ownership, reachability, and backend verification cover the +expanded control flow. The internal raw-read marker is emitted after consuming +a checked HIR plan; leftover or missing markers fail MIR validation. + +`ReflectedContainerReady` is a compiler-owned typed MIR statement with a +canonical diagnostic and an exact builtin wrapper contract. Codegen borrows its +handle and extends dispatch tags of the existing `RejectPendingHandle` leaf for +sets, maps, and sums; no runtime symbol or signature changes. Readiness inspects +only that wrapper's pending depth, preserving source identity and child depths. +Raw carrier compatibility recurses through the five builtin shapes, matching +secrecy at every layer and retaining exact nominal, callable, and primitive +identities. Existing root interface conversion remains separate; nested erased +interface adapters and changed secret, callable, or nominal generic proofs keep +their conservative refusals. Occupied unsupported proof nodes fail during +preflight, while inactive branches do not run proof checks. The raw selector's +compatibility relation still runs first. These safeguards do not select the +broader [reflected requested-type admission rule](open_design/reflected_field_request_compatibility.md). +Written type aliases resolve once in lexical context. Resolved checked owners, +installed generic arguments, captured bindings, and reflected metadata retain +their canonical declaration identities through selection and normalization. +Canonical operations isolate caller aliases; function bodies keep their imports. +Written view wrappers retain borrowing metadata while their underlying refinement +still validates at raw boundaries. +Explicit comptime HIR retains its original source span for evaluated-value lookup, +independently of an enclosing expression span widened by parentheses. +Interpolation selects the exact checked `Displayable.display` owner before +using built-in primitive formatting. Interpreter selection uses the expression's +checked type as well, preserving an interface's own display implementation over +its concrete payload. Implementation registration resolves the interface's +declaration namespace independently of the implementation's namespace. +HIR marks implicitly selected display results with an exact string-to-string +`DisplayResult` boundary. MIR retains that boundary inside staged interpolation +segment initializers, so a pending result fails before later handlers or +effects. Native code borrows the returned string for the readiness check and +retains its existing owning slot for ordinary use or failure cleanup. Explicit +method calls and ordinary pending string formatting remain unchanged. +When a pending interface receiver cannot dispatch, the interpreter and generated +dispatcher both name the registered interface method with its canonical namespace. +Remaining compiler-owned calls carry a closed `IntrinsicId`, typed arguments, and +lexical evaluation order after type checking has authorized them. The shared +registry is the only source-spelling-to-intrinsic boundary; HIR, MIR, +comptime/runtime dispatch, and native codegen consume the checked ID and use +exhaustive matches rather than rediscovering policy from names. Inline +functions and indirect calls retain explicit parameter/local identity. An +indirect call evaluates arguments in lexical source order before resolving its +function-valued expression, including a returned function or projected field. +Argument handlers can change callback selection or return before the callee is +evaluated. HIR preserves checked constructor, source function, and intrinsic +identities through parentheses; other checked function expressions become +`IndirectCall`. Checked declaration parameter names order named arguments; +anonymous function values reject argument labels. Ownership analysis follows +the same evaluation order, and capability signatures enforce call purity for +every callee shape. Function types carry parameter `view` modes through HIR and +MIR; native indirect calls borrow view arguments through the call and transfer +only owned arguments. MIR lowering receives the checked type interner; existing +handler snapshots remain restricted to recursively cloneable value types. +Original explicit views of exact checked local/field origins instead use typed +`BeginCallView` and `EndCallView` statements when snapshots cannot preserve the +borrow. The initializer is evaluated eagerly, keeping checked field and pending +receiver guards before later handlers. Direct views add no readiness check. +The internal local is immutable and non-owning; its terminal owner may be +mutable, and an ordinary owned temporary root evaluates once into a separate +owning slot. Ownership/liveness analysis protects the owner through the complete +operation, then kills only the internal loan. The operation result is stored +before End; aborted returns/responds end abandoned loans before their owned +operand, while break/continue respect the target loop's scope depth. Original +metadata and every Begin/End path are validated before pruning; source alias +names cannot authorize scope expiry. The emitter allocates no destructor slot +for the internal alias and End emits no runtime ownership action. Existing +source alias loans remain persistent. For an exact source-checked mutable plain +`TypeConstruction` root, constructor-owned records now preserve a captured +generation across later assignments. The original call, Source binding fact, +full local header and assignments remain sealed independently of current dense +IDs. Exact CFG and loan checks authorize a private escrow: the replacement RHS +is acquired before the first old generation moves there; subsequent replacement +owners and the captured generation retain separate cleanup. Canonical preparation +validates and remaps current records without turning archival IDs into reads. +Broader simultaneous cohorts and other owner kinds remain separate work; no +source clone, owner pin or runtime ABI permission is introduced. + +A handled owned producer under written `view` is materialized once through its +original checked endpoint conversion and exact owning Let/Begin/End proof. A +fresh validated `CallerAcquisitions` map associates only that exact interface +boxing occurrence with its proved concrete View input. Move analysis reads that +input borrowed while keeping the produced box owned; unrelated View initializers +retain their escape refusal. Runtime boxing may copy its payload, so this is not +a zero-runtime-copy guarantee. The original Source/Handle and produced-result CFG +checks remain mandatory. See the +[scoped implementation contract](completed/native_codegen/native_scoped_call_view_staging.md). +Interpreter function and closure invocations expose their own lexical scopes +and the initial global environment, excluding unrelated caller locals and +namespace aliases. Closures retain the namespace aliases visible at creation. +Concrete source method values carry a separate checker-selected source-body +identity through ordinary, generic, and reflected comptime body facts. HIR +resolves that identity to the registered method function instead of treating +the type namespace as a runtime field. The receiver stays an explicit callback +parameter, with its checked ownership mode. Interpreter registration retains +the declaration namespace separately from the qualified method name, so method +bodies resolve their lexical helpers consistently for direct and indirect calls. +Qualified interface method values carry the checked interface type and method +slot through those same fact maps. HIR collects slots used only as callbacks, +including generic and nested reflected bodies, and resolves them to the existing +zero-capture dispatcher. Parameter types and view modes remain exact; the +explicit receiver selects an implementation at invocation. Direct qualified +calls retain their selected call target without an unused callback fact. +Interpreter registration retains declared slots even without implementations, +and transparent interface aliases produce the canonical named callback. +Those aliases resolve their targets in declaration scope; caller namespace +imports and generic type bindings cannot redirect the selected interface. +Explicit compile-time callback values are materialized from the generated +dispatcher's reserved identity and canonical debug name, rather than a concrete +implementation body. Creating a callback does not invoke it or capture authority. +Declared interface slots take precedence over concrete methods with a colliding +qualified display name. The interpreter cannot fall back to that concrete body +after failed dynamic selection, and compile-time materialization excludes the +colliding source candidate while retaining its distinct implementation function. +Generic body checking similarly saves and restores the triggering closure's capture +scope so a generic callee's parameters cannot be mistaken for captures. +Generic annotations on an inline function use ordinary type substitution; +they do not by themselves make its enclosing function reflection-dependent or +remove its concrete instantiation from native lowering. +HIR/MIR function debug metadata retains named source identities and inline +origin separately from native symbol identity. Function descriptors retain an +owned display label alongside the code address and capture environment, so +trace and breakpoint rendering can borrow function values inside recursive +aggregate debug layouts without invoking callbacks or inspecting captures. +Native actor registration assigns a separate debug ordinal in spawn order. +Recursive debug layouts read that ordinal from a validated live actor handle +and render `actor#N` without exposing the native allocation identity or state. +For a non-short-circuit binary expression with a nested handler, the left value +is captured before lowering the right; owned cloneable operands are cloned so +the handler cannot change the value compared by a later enum equality. +MIR also extracts handlers from field-access receivers, state-test operands, +and `clone`, `coarsen`, and `declassify` operands. It evaluates handled `if` +and `while` conditions in their respective control-flow blocks, so a loop +rechecks the condition on every iteration, and evaluates handled `match` +scrutinees before selecting an arm. +Handlers in `for` iterables are extracted before the loop header. Sequence lowering +then materializes the iterable once on the resulting preheader path, so a loop +backedge never re-evaluates the source expression. +For a checked list with an uninhabited element or map with an uninhabited key +or value, HIR omits lowering the body after source checking. Native sequence +preparation retains iterable materialization and the ordinary length/pending +check, ends any iteration loan, and routes directly to the exit without reading +an element. It compacts unreachable blocks and unused locals only in functions +changed by this lowering, preserving dense IDs and retained debug metadata. +Eager concrete generic specializations with a bare `never` parameter or result +are not implicit native project roots. Retained calls require ordinary callable +verification; a retained function descriptor is distinct from invoking its body. +An exact non-capture `never` input permits descriptor-only representation, with +normal capture storage, cloning, pending depth, labels and cleanup. Original +typed metadata must be checked before preparation prunes any body or local, +while native control-flow and ownership checks follow normal preparation. +Original local and debug types are checked before unused enclosing-frame slots +are compacted across all original blocks and typed references. This removes +transferred inline-body slots without changing parent control flow or erasing +real parameters, bindings, captures, observations or loan dependencies. +Original type validation distinguishes checked metadata from native value +eligibility. An exact `never` function result can appear in original metadata, +including nested container and nominal types, without proving descriptor-only +invocation. This preserves closures in absent optional/result arms until normal +arm preparation removes their creation and calls. Function view-schema lengths +and every inhabited child type are still validated before compaction. Prepared +runtime classification remains strict: a surviving return-only `never` function +is unsupported, and no `never` capture environment or carrier is emitted. +The descriptor retains a nonzero internal trap entry with no `never` parameter +or return carrier; no valid call reaches it and no original body effect is +emitted. Created `never` capture environments, mismatched signatures and +surviving impossible calls remain invalid. A return-only `never` signature does not grant this proof. + +Descriptor metadata validation also follows exact function, closure and adapter +value targets inside the proven uncallable body. It checks each target identity, +signature, view mode and capture-local type before a separate visited FunctionId +set prevents repeated body traversal. Every followed original body is checked; +this traversal does not add native callable roots or emit an inner environment. +A latent zero-argument closure may describe an owned `never` capture from the +outer input. Actual closure creation retains the ordinary copyable capture +restriction and rejects uninhabited environment values. + +This body phase uses recursive OriginalMetadata classification for composite +types and recognizes only exact `never` or one direct `secret[never]` as absent +value metadata. Function-descriptor classification may omit that exact result +shape only with an exact `never` input; original return-only type metadata is +not runtime authority. Literal fabrication, malformed nested TypeIds/view +schemas and surviving callable uses still fail. Arbitrary wrapper/refinement +peeling, opaque Resource layouts and hidden-secret observation policy are +unchanged. The existing descriptor trap ABI and native reachability need no +new carrier or runtime operation. + +Inhabited containers and +absent sum arms containing `never` retain their normal representations. +Native handler preparation also recognizes an exact final `SumTag` followed +by a branch on its checked boolean local. An optional with a `never` payload +can only take absence; a result with exactly one `never` payload can only take +the other arm. The pass retains sum observation, input evaluation, and pending +diagnostics, then bypasses the impossible extraction and compacts only changed +functions. It leaves other graph shapes and results with two uninhabited arms +unchanged. Snapshot cloneability ignores absent `never` sum children while +retaining the ordinary policy for inhabited payloads; bare `never` still has +no runtime carrier or clone operation. +Generated inline functions are reached through retained descriptors and calls, +rather than treated as independent source-declaration roots. Native preparation +compacts their copied local tables even when their own graph contains no erased +loop or handler arm, preserving capture/parameter order and function identity. +This removes unused parent bindings without changing named-function admission. +The frontend's directional compatibility and collection-literal common-type +inference establish these uninhabited proofs: concrete values cannot populate +a `never` place, while mixed empty/absent members refine only their compatible +uninhabited slots. Handler defaults, declarations, calls, constructors, and +returns retain ordinary assignment checks before any native arm is removed. +Contextual sum handling keeps the producer's original local or call signature. +Inhabited nested containers receive an explicit conversion before extraction; +empty collections are rebuilt with the target ownership flags, and nested +function values retain their checked adapters. Owning Handle snapshots clone the +underlying local before conversion so owned and borrowed sources stay readable. +A certified ordinary borrowed-payload initializer instead preserves the exact +backing and nonowning success projection described above. +Inferred generic reflection peels transparent aliases only at each argument's +root and preserves nested alias witnesses. Repeated compatible witnesses fill +only missing `never` slots; concrete alias identity follows declaration parameter +order. The checker and reference evaluator retain the same reflection names and +kinds, including calls whose empty input comes through another generic local. +Assertion and breakpoint conditions also extract handlers before their MIR +statements execute. Assertion messages remain on the failure-only path. +Handled machine transitions snapshot their source and payloads in lexical +order before the new state is constructed. An earlier value cannot change when +a later payload handler runs. +`run` and `join` also extract handlers from their operands before the task +operation, preserving the pending representation for `nothing` and other +values. +Actor spawn arguments use their checked lexical evaluation order when handlers +are extracted. Actor messages capture the target first and then their ordered +arguments, so a later handler cannot change an earlier message value. +Actor handler execution retains a private checked session, original actor syntax, +canonical actor type and handler ordinal. Exact capability, state and message +roles join checked declaration facts to physical locals; persistent state is +validated at real exits. Send/Ask facts retain their original outer expression, +receiver, lexical argument acquisition and checked body/scoped context. Copies +from another session, handler or scoped occurrence cannot grant this authority. +The [actor field-update note](completed/native_codegen/actor_handler_field_updates.md) records this +checked contract, bounded MSVC acceptance and remaining obligations. +Comptime type-bind scopes erase to checked HIR scopes; actor spawn/send/ask +carry typed operands and message identity. Generated actor receive bodies retain +the original handler receipt and exact capability, state and message slots through +HIR, MIR, construction and synchronous dispatch. Generated function identities do +not replace original handler authority. Real asynchronous scheduling, authenticated +Resource transport and wider frozen-graph compositions remain separate obligations. +Remaining source constructs are staged by the [initial HIR lowering plan](active/hir_lowering_plan.md). The initial implemented `jett_mir` boundary accepts only HIR that passes the HIR structural validator. It lowers top-level `if`, `while`, exhaustive @@ -836,9 +1665,63 @@ The HIR is the first representation where generic functions are fully expanded. Resolver `DefId` and `TypeId` values remain session-local join keys. An in-memory HIR function identity is the compiler-supplied source origin, canonical namespace, canonical declaration name and kind, plus concrete type -arguments. Persistent artifacts replace raw type interner IDs with canonical -structural type identities. Lowering never infers source authority from -`FileId` ranges or trusted-looking names. +arguments and the checker-owned reflection-visible specialization facts. +Persistent artifacts replace raw type interner IDs with canonical structural +type identities and encode those specialization facts deterministically so an +alias-specialized body cannot collide with its underlying-type body. Lowering +never infers source authority from `FileId` ranges or trusted-looking names. +Checked reflection operands also carry source-aware names, kinds, secret flags, +and nested arguments into HIR and MIR. Scalar reflection and recursive +`TypeInfo` construction use those facts instead of reconstructing aliases from +canonical `TypeId`. +The checker restores both the canonical type and source kind inside a +`comptime type` binding. Generic calls made from that scope therefore select +the alias branch before the underlying struct or collection branch; nested +`type.arg` bindings resolve the aliased base while retaining its source-aware +reflection metadata. +`type.fields` lowers checked field snapshots into ordinary `TypeField` values +only for struct and bitfield kinds; an alias remains a distinct total probe. +The ordinary checked interpreter authenticates the original field intrinsic +call or pipeline step before selecting the canonical struct or bitfield owner. +Field metadata and the construction lifecycle use that same canonical owner +when its generic arguments contain transparent aliases; source-visible names +and `TypeInfo` still retain those aliases. The ordinary cursor carries no +Resource custody or refinement proof authority, and its canonical field table +must exactly match the retained checked program. +`type.bitfield_layout` and `type.bitfield_fields` likewise lower checked +bitfield layout snapshots into `TypeBitfield` and `TypeBitfieldField` values; +other kinds produce an empty layout, including source aliases. +`type.variants` lowers checked enum variant snapshots and their payload-field +metadata into `TypeVariant` values; non-enum kinds, including aliases, produce +an empty list. +For a concrete enum, `type.variant_value` carries those same checked snapshots +as hidden HIR operands. Native code reads the enum tag, selects the matching +snapshot, and returns an owned clone without consuming the enum or its payload. +`type.field_value` and `type.variant_field_value` likewise carry checked +`TypeField` snapshots as hidden operands. Native code selects a candidate by +field index and, for enums, the active variant tag; a runtime leaf validates +the supplied metadata against that candidate before reading the borrowed +source. The result receives its own ownership where required. Native mismatch +diagnostics and alias-equivalence checks still need interpreter parity. +`type.machine_state_value` uses the same snapshot handoff for checked machine +states, selecting the active state by tag and returning an owned clone. +`type.machine_field_value` selects a checked `TypeField` by active state tag +and field index, validates the supplied metadata, then copies the payload +from the borrowed machine. Exact mismatch diagnostics remain pending. +Static `type.machine_layout`, `type.machine_states`, and +`type.machine_transitions` lower checked state and edge snapshots into ordinary +`TypeMachine`, `TypeMachineState`, and `TypeMachineTransition` values. Alias and +non-machine probes produce empty layouts and lists. +Reflected `comptime type` loops carry one checker-specialized MIR arm per +concrete bound type. HIR records a canonical identity from checked `TypeInfo` +metadata for each arm; native code compares the borrowed runtime `TypeInfo` +recursively before selecting exactly one arm. Source aliases are transparent +to dispatch identity while their source-aware names remain available inside +the selected body. +`type.arg[T](index)` carries checked, source-aware `TypeInfo` snapshots for the +type arguments of `T` as hidden HIR operands. Native code validates the runtime +index and returns an owned clone of the selected snapshot. Exact out-of-range +diagnostic parity remains pending. Interface-implementation method declarations additionally include both the concrete owner and canonical interface in their identity. @@ -854,6 +1737,48 @@ concrete owner and canonical interface in their identity. move-only and follows the same view/consume rules as other owned storage. 7. **Comptime reflection lowering:** Preserve enough type metadata for comptime code to inspect `type.name[T]()`, `type.kind[T]()`, `type.has_secret[T]()`, `type.fields[T]()`, bitfield layout metadata, state-machine state/transition metadata, active machine states, and reflected active-state payload fields. JSON serialization is expressible in terms of these reflection primitives rather than as format-specific HIR magic. Struct, bitfield, enum, and state-machine deserialization use the explicit `TypeConstruction` builder family to build `T` from parsed field values; that builder is the sole canonical source form, with no parallel construction-block syntax. +Native JSON lowering currently bridges checked `JsonTree` calls to trusted raw +stdlib functions. For primitive serialization it constructs a checked +`JsonTree` variant and uses the same source serializer. Primitive parsing for +`string`, `bool`, `int64`, `uint64`, `float64`, `bytes`, and `nothing` calls +private stdlib decoders over that tree. This is an interim lowering path while +other concrete generic JSON bodies are staged for native specialization. +Structs, lists, string-keyed maps, optionals, and results composed of supported +primitive values now run the checked reflected stdlib serializer. The +compiler-owned public policy gates still run first. +Those gates already admit inferred uninhabited slots. Serializer source +eligibility must retain `Type::Never` in an otherwise supported empty or +absent carrier, without choosing a default element type or introducing a +runtime Never representation. The same recursive checks still reject every +unsupported inhabited slot; runtime emptiness is not a policy exception. +The frontend checks the selected reflected source bodies before HIR/MIR omit +impossible payload operations. Existing uninhabited preparation retains +carrier evaluation, length or sum-tag observation, pending checks, and cleanup +before normal native verification, independently of optimization. +The selector repair, executed focused gates and remaining release gates are +recorded in +[the inferred JSON slot note](completed/native_codegen/native_json_uninhabited_slots.md). +The separate parser eligibility repair retains `Type::Never` in otherwise +supported inferred carriers. Checked reflection facts select the existing +unknown-kind decoder, whose return type is inhabited `result[Never, string]`: +it returns the ordinary unsupported-reflected-type error and must remain +reachable. It does not construct Never, choose a default payload type, or +replace the checked stdlib body with a native generic fallback. The ordinary +array index loop still calls this decoder for occupied input; optional and +result decoding skip only genuinely unoccupied payloads. Raw parsing, exact +validation, field/index/arm prefixes, pending checks and cleanup retain their +order. Existing uninhabited sum preparation may remove an impossible success +extraction only after retaining the decoder call and sum observation. Checked +TypeInfo/TypeKind argument facts select source branches; ordinary pure helper +folding supplies no source-validity proof. Public JSON/key filters and inhabited +unsupported targets are unchanged. Characterization and pending acceptance +are recorded in [the parser contract](completed/native_codegen/native_json_uninhabited_parse_slots.md). +The checker also records a source parser facade for `json.parse[T]` and +`json.parse_exact[T]` pipeline steps from the checked result's success type. +It uses the existing direct-call facade mechanism after public JSON policy +and ordinary argument checks; HIR retains pipeline argument order, ownership, +and step-local handlers. + --- ## Phase 8: Mid-Level IR (`jett_mir`) @@ -971,11 +1896,63 @@ primitive interface dispatch. The same normalization enforces the declared `int8`/`int16`/`int32` and `uint8`/`uint16`/`uint32` ranges after expressions and at typed assignment, parameter, and return boundaries; values outside the checked primitive range stop interpretation with a deterministic diagnostic. +The same checked-expression and declared-type normalization rounds float32 +literals and arithmetic results to f32 before widening into the internal +`Value::Float64` carrier. Both runtime and explicit comptime evaluation use +this boundary, so intermediate arithmetic cannot silently retain f64 precision. +Native formatting and interpreter display both format that exactly widened value. +The explicit pure `float32.from_float64` intrinsic applies this same rounding +at runtime and in comptime; native codegen emits a scalar f64-to-f32 demotion. The driver additionally evaluates every explicit `comptime expression` after type checking and stores the resulting value by source span. Runtime interpretation consumes that stored value instead of evaluating the expression again. Explicit expressions are evaluated in an empty lexical environment, so runtime parameters and locals cannot leak into required compile-time work. +The collector carries visible block-local `use` aliases as name bindings for +each expression, including shadowing in nested blocks. Each evaluation has an +isolated alias scope, so aliases cannot leak between baked expressions. + +A native verify audit entry is constructed from the retained checked primary +file, selecting exact original Verify identities and their current protected +bodies in source order. For an authenticated ordinary archive, the private HIR +factory first checks the exact checked session, original type meanings and +whole current function table. It appends only generated zero-argument Verify +calls with `nothing` results and validates each call's ownership and target +signature before renewing the existing reflected and Graph transitions. +Original Source, constructor recipes, required values and ABI metadata remain +unchanged. No caller-provided function or target list can renew that custody; +edited existing bodies or edited appended suites fail the next conversion +gate. Ordinary lowering without a protected archive retains its existing +suite path and cannot gain Resource or Graph authority. + +The successor Resource suite transition retains the complete protected current +table and immutable original Source separately. Its private factory appends only +the exact checked Verify or Property wrapper, retains the successful required +cache and opaque generated-case plan, and issues an Arc-backed receipt for each +whole-wrapper preorder call occurrence. Identical sampled values do not merge +trial identity. MIR carries a distinct boxed native-suite invocation role; it +never manufactures a Source call certificate or admits Generated Resource calls +by type alone. Canonical conversion preserves the private recorded-output receipt +and validates the unchanged-before archive before joining its Graph, reflected and +temporary peer transitions. Its transaction publishes only after sealing and all +final HIR/backend-type checks. The first connected +activation slice has ordinary generated arguments and a Nothing result; all +transitive Resource aggregate helpers retain their existing Source/carrier plans. +Zero provider acquisition and custody still require observed native evidence. +See the [candidate protocol and acceptance boundary](completed/native_codegen/native_resource_suites.md). + +The Builder's contextual extraction predicate also recognizes exact retained +direct Source calls in the Resource execution family whose arguments and result +have ordinary endpoints. It propagates through existing eager/ordered operand +containers with their original eligibility checks, stages preceding values in +checked source order, and puts a lazy boolean RHS region only in its selected +branch. Each normalized region authenticates the complete original tuple before +extraction and retains ordered actual/site/header associations through its +Begin/Stage/Invoke/End sequence; failed construction restores its checkpoint. +This changes neither the suite wrapper's flat private protocol nor the separate +carrier custody proof. Resource-bearing endpoint staging, indirect/hook timing, +refinement-handler exits and broader aggregate contexts remain independent +obligations. No Runtime helper, ABI, provider grant or Source rule is added. ### What Runs at Comptime @@ -987,6 +1964,10 @@ runtime parameters and locals cannot leak into required compile-time work. 3. **`comptime` expressions** — `if comptime is_numeric[T]()` branches are resolved, dead branches are eliminated. 4. **Refinement type constraints on literals** — `Port p = 80` validates `80 >= 1 && 80 <= 65535` at compile time. 5. **Bitfield literal validation** — `ColorChannel(red: 300, ...)` catches the out-of-range value at compile time. +6. **Namespace constants** — literal values and earlier constant references are + materialized in declaration order for both execution backends, even when + unused. Every calculation in an initializer requires an explicit `comptime` + site; declaring a constant does not make evaluation implicit. ### Comptime Type Reflection @@ -996,12 +1977,31 @@ The comptime interpreter supports basic type-level reflection for generic type p These are built-in operations of the comptime interpreter that query the compiler's type table. They enable `if comptime` branching on type properties. +Every concrete generic invocation must have a checked body, including reflection +inside interpolation, call arguments, and closures. Reflection placement never +allows ordinary type checking to be skipped. A `comptime type` binding must be +valid during compilation even behind an ordinary runtime condition: for example, +`type.arg[string](0)` is invalid regardless of the condition's runtime value. +Valid bindings under unknown runtime guards are checked normally in every +branch. Only supported reflection specialization may exclude an unreachable +branch. + Generic specialization uses only reflection facts that remain structurally tied to the reflected type: direct `TypeKind` / `TypePrimitive` comparisons, immutable locals carrying those tags, typed helper parameters receiving them from the same generic instantiation, and matching arms. Arbitrary tags supplied by callers are not facts about `T`. The checker can use visible facts to determine branch reachability and validate casts for a concrete instantiation. +Known selections are recorded inside inline functions and are independent of +where other reflection expressions occur in the body. When no selection is +known, the checker checks all branches, including their scoped type bindings; +there is no separate placement restriction on otherwise valid bindings. +Native reflected-type dispatch serializes `TypeInfo` identities using an +explicit traversal stack. Finite argument and alias trees have no fixed runtime +depth cutoff; active-path checks reject cyclic metadata while permitting shared +children. Reflection dispatch retains alias identities, and canonical identity +matching keeps its existing alias erasure. Both use the same identity encoding +as before, without runtime source lookup or interpretation. Predicate calls that return `bool` and reflection comparisons copied into arbitrary `bool` locals do not carry type evidence. This conservative boundary prevents facts from being detached from their generic parameter or from hiding @@ -1026,9 +2026,12 @@ The compiler never constructs runtime capability values for comptime; file, network, clock, randomness, environment, process, foreign access, and application logging are all excluded. -Only explicit `comptime expression` sites require build-time value evaluation. -Ordinary pure calls may be constant-folded later, but that optimization cannot -affect diagnostics or source acceptance. +Required compile-time value evaluation always uses explicit `comptime expression` +sites. Literal spellings and earlier constant references denote existing values +without executing a calculation. Namespace declarations do not introduce an +implicit evaluation boundary for operators, interpolation, or calls. Ordinary +pure expressions outside explicit sites may be constant-folded later, but that +optimization cannot affect diagnostics or source acceptance. --- @@ -1046,7 +2049,7 @@ These optimizations exploit Jett's linear type system — the compiler has **per #### Ownership-Aware Optimizations -1. **In-place reuse (consume-transform pattern):** When a value is consumed and immediately transformed (`x = transform(x)`), the compiler detects that the old value has no other references and mutates the underlying memory in-place. This turns `list.append(old_list, item)` into a genuine in-place buffer append (no copy), and `struct.with_field(old_struct, new_value)` into a field write on the existing allocation. This is the single highest-impact Jett-specific optimization — it turns what looks like immutable functional code into zero-copy imperative code. +1. **In-place reuse (consume-transform pattern):** A MIR optimization may reuse uniquely owned storage for a consume-and-return transform (`x = transform(x)`) or the canonical owned-local replacement `record.field = value`. A checked field update must preserve ancestor/place preflight, one RHS evaluation, replacement ownership, displaced-value cleanup and failure order. Shared backing must retain the selected clone/view and reconstruction rules. This is an optimization opportunity, not a claim that every update reuses its allocation or that list append is always zero-copy. 2. **Move coalescing:** When a value is moved through a chain of single-use variables (`a` → `b` → `c` → `f(c)`), the intermediate names are eliminated and the value stays in the same memory location. No intermediate allocations or copies. @@ -1247,7 +2250,14 @@ LLVM is the compilation bottleneck. Strategies to minimize its impact: 3. **ThinLTO for release:** ThinLTO imports callee summaries into each module, enabling cross-module inlining and devirtualization without merging everything into one module. Achieves ~90-95% of full LTO's performance at much lower compile time. Each module is re-optimized in parallel. -4. **Future: Cranelift for debug builds.** Cranelift compiles ~5-10x faster than LLVM O0 by avoiding LLVM's inherent overhead (IR construction, verifier, pass infrastructure). Code runs ~20-30% slower, acceptable for debug. Supports x86-64, AArch64, RISC-V. This would be a `jett_codegen_cranelift` crate following the same MIR → IR interface. +4. **Cranelift AOT backend.** The initial production path uses the + `jett_codegen_cranelift` crate to turn validated MIR into a host object. + Every emitted Jett function receives a backend-private runtime-context + pointer before its source parameters, and direct calls forward that exact + pointer. The exported C-ABI entry wrapper accepts the context from the + launcher and forwards it to the compiler-selected Jett entry function. + LLVM remains the later optimizing backend and must share the same validated + MIR, runtime ABI, and conformance suite. #### Linking @@ -1255,7 +2265,14 @@ LLVM is the compilation bottleneck. Strategies to minimize its impact: |---|---|---| | Linux | `mold` (~5-10x faster than `lld`) | `lld` (handles LTO) | | macOS | `lld` or Apple's linker | `lld` or Apple's linker | -| Windows | `lld-link` | `lld-link` | +| Windows | MSVC `link.exe` (initial host path) | MSVC `link.exe` (initial host path) | + +The Windows host linker consumes an exact checked program-entry identity and a +typed launcher-bundle contract (target, runtime ABI, static CRT mode, and +ordered native libraries). The driver discovers both the MSVC linker and +Windows SDK, invokes the linker without a shell under a bounded timeout, stages +all intermediates in a secure directory on the destination volume, and only +atomically replaces the requested executable after a successful link. Additional linking optimizations: - **Split DWARF** (`-gsplit-dwarf`): Moves debug info into separate `.dwo` files that the linker does not process. Reduces link time by 30-50% for debug builds with large debug info. The debugger reads `.dwo` files directly. @@ -1299,7 +2316,7 @@ Uses the `inkwell` crate for safe Rust bindings to the LLVM C API. | `trace` | Conditional instrumentation code (compiled out in release) | | `breakpoint` | Conditional pause + IPC server (compiled out in release). Supports optional condition expression (`breakpoint expr`) — only pauses when condition is true | | Bitfields | Packed integer types with shift/mask accessors | -| `function(T) returns U` | Function pointer. Closures capturing immutable values use a fat pointer: `{ fn_ptr, env_ptr }` | +| `function(T) returns U` | Owned function descriptor containing a code pointer, an environment handle, and a source-derived debug label. Captured inline functions copy enclosing values into the environment; native calls pass it as a hidden argument after the runtime context. | | Capabilities | Regular struct parameters — no special runtime representation | #### Platform-Specific Capability Lowering @@ -1388,7 +2405,8 @@ values preserve their lexical namespace, so calls from the stdlib graphics loop resolve application helpers in the defining module. The callback audit includes implicit `Equatable.equals` dispatch from struct -`==` and `!=` expressions. It selects the method using the operand type in the +`==` and `!=` expressions and recursively nested enum payloads. It selects +methods using the operand type in the current checked generic instantiation, without auditing unrelated instantiations. Bare generic templates are rejected as function values before interpretation; @@ -1409,6 +2427,18 @@ focus flag in the exactly pinned minifb 0.28.0 backend before reconciling held k the adapter must be rechecked when that dependency changes. Focus/repeat unit tests exercise both backend conventions without requiring a native window. +Native graphics uses the existing typed validation and session leaves. Kernel +entry rejects pending Graphics authority through the existing capability depth +check after evaluating arguments and before decoding configuration or using the +provider. Complete joins preserve the original context-bound token. Configuration +and scene decoders reject pending owners, fields, strings, list +containers, and list elements before reading their payloads. Decoding follows +the interpreter's field order and precedes domain validation; terminal shape +errors remain distinct from handled domain failures. The emitter reads window +dimensions only after complete configuration validation. Callback state keeps +its pending metadata. Context destruction closes an unfinished native session +before checking value ownership, including after a terminal decoding failure. + Native macOS graphics keeps interpretation on the driver's caller thread, which must be the process main thread for AppKit. A worker-thread window request from a library caller returns a host failure before entering AppKit. The CLI therefore @@ -1433,7 +2463,7 @@ that compatibility case does not suppress errors from evaluated expressions. The driver injects authority only when runtime `main` requests `Graphics`. Comptime, verify/property, untrusted private-kernel calls, and nested sessions -cannot open a window. Native compilation, textures, audio, animation timing, and +cannot open a window. Textures, audio, animation timing, and source-visible window ownership remain separate work. See the [graphics contract](active/graphics_game_contract.md). @@ -1445,7 +2475,78 @@ generic parameters, user destructor, or source-visible carrier. The checked program binds private trusted operations by stdlib-origin declaration identity, not by matching a public qualified-name string. -The interpreter stores each live resource in a run-context registry and gives +The identity prerequisite uses an empty-by-default compiler catalog associated +with an actual parsed Resource declaration. A nonempty privileged catalog +requires both the reserved stdlib FileId and loader `SourceOrigin::Stdlib`. +Resolution inserts real private Function definitions through normal namespace, +duplicate and declaration-order handling. Synthetic diagnostic spans do not +replace the resource's source-span binding. Closed checker-session recipes bind +complete `Type::Function` parameter types, view/owned modes and return types by +those exact DefIds; validators check all entries, including unused ones. + +Explicit fallible resolver/checker entries publish the checked facts. Legacy +checker entries reject nonempty catalogs with internal diagnostic code 0 instead +of bypassing validation; normal empty-catalog and source diagnostic behavior +remain unchanged. No production hook, provider dispatch or source carrier is +installed. The implementation and focused evidence are in +[the resource-hook identity note](completed/native_codegen/native_resource_hook_identity.md). + +`CheckedResourceProgram::prepare` consumes the whole `ParseResult` and explicit +loader origins, then resolves and checks the owned AST internally with the +existing private compiler catalog. Its Module, ResolveResult, CheckResult/type +interner, origins and parser observations have only shared immutable access. +There is no constructor from independent checked parts or mutable/into-parts +access. Error diagnostics from each source phase prevent a sealed result; +warnings remain inspectable. Public `validate_resource_hooks` additionally +refuses checker errors with `SourceCheckingFailed`, while a crate-private +identity/shape helper lets the diagnostic checker keep returning repair facts. +The full generic/scoped facts, parameter permutations and binding modes remain +owned by that exact session. This validates the frontend through checking; +explicit comptime and verification keep their separate evaluation gates. +The reference driver keeps a private preparation result containing the public +BuildResult observations, an Arc of that sealed program and the primary FileId0 +main's saved span/namespace. Public BuildResult/build_file signatures and fields +are unchanged. Required evaluation uses the original merged AST and checked +scoped/generic facts once. Runtime emits retained frontend observations, gates +build errors and missing snapshots before provider setup, then registers that +exact merged module once; registration resets namespace at each item FileId. +No post-preparation source read, parse or support rediscovery occurs. Existing +macOS graphics thread selection still reads/parses before preparation, so the +entire public run path is not claimed to parse once. + +ResourceProgramError keeps all observations through the failed phase in parse, +resolve and check order. Metadata variants retain typed source errors plus the +earlier observations; diagnostics() keeps its optional-slice signature and +returns Some even for an empty metadata prefix. Display and Error::source expose +the original metadata cause. Successful session records stay owned without +repeat checking; earlier prefixes are cloned only on failure. See +[the reference handoff](completed/native_codegen/native_prepared_reference_driver.md). This does +not install Resource providers or callable authority; other driver/backend +preparation paths and runtime eligibility remain separate work. + +Before source execution, the checker separates owning field copies from +borrowed place projections. Only Field and the existing transparent place +wrappers propagate that projection context; calls and owning constructors check +their payloads normally. Recursive Resource-bearing field and cloning-getter +results report E0364. A known Resource-bearing view cannot become an owned +Some/Ok/Fail, list/map value, struct or machine payload (E0401), even when the +completed constructor is itself viewed. Exact-state Resource-bearing transition +sources reuse E0375/E0401 owned-argument checks. Ordinary owned data, empty +containers, safe field views and non-Resource endpoints keep their existing +admission. Generic phantom arguments do not count as stored Resource data. + +The payload guard diagnoses each source occurrence once and records only the +exact identifier's legacy ownership counterpart for diagnostic deduplication; +ordinary ownership traversal and unrelated diagnostics remain. Exact Resource +roots also reject ordinary print/println and value-reflection operations with +E0300. Type-only metadata, aggregate metadata and trace/breakpoint ownership +summaries remain separate. Nested ordinary printing, including hidden Secret, +known-absent and omitted values, has not been settled by this guard. The +[static ownership note](completed/native_codegen/native_resource_ownership_boundaries.md) records +executed checker evidence and remaining live-resource prerequisites. + +Under the selected execution contract, the interpreter stores each live +resource in a run-context registry and gives source execution an internal key containing context, nominal type, slot, and generation identities. Operations validate every component before provider dispatch. Finalization detaches pending work, runs one infallible trusted @@ -1462,8 +2563,121 @@ finalizers, and rejected insertions release their consumed provider payloads. Suppressed panic payloads are dropped under unwind protection; if their destructors panic too, only that secondary payload is deliberately forgotten to bound cleanup while retaining the first failure for propagation. -Interpreter trusted-hook dispatch and source-level -scope/drop integration remain later stages of the resource contract. +The private reference interpreter now carries Resource and hook-descriptor +values bound to an Arc of the successful checked program. The physical carrier +may be copied internally without creating ownership; the separate non-cloneable +holder ticket records custody and acquisition order. Checked argument packets +supply source order, formal permutation and caller effects independently of +callee View/Owned access. Nested checked calls keep those envelopes through +ordinary function entry/return rather than unwrapping a Resource into an +ordinary Value argument. + +Generated absent For/Match binders use a private original-control proof keyed to +the current checked attempt and active body. It retains each original binder, +ordinal, resolver definition, concrete TypeId and immutable ownership mode; Match +also retains the exact selected arm and its payload cardinality. Complete proof +revalidation and the existing recursive absence-shape validator precede binding +publication. Resource-bearing children enter the existing declaration slot as +ordinary evaluated values with empty custody; the strict missing-slot identifier +guard remains intact. Original view loops borrow their retained parent binding +through the existing transport, and generated scope retirement leaves unrelated +live owner tickets intact. No Resource owner/grant, runtime leaf, ABI or native +aggregate permission is added. The full comptime replay passes 554 tests, +including the original 28-wrapper/120-constructor controls and 60 focused entry +observations. Native aggregate emission remains a separate gate. See the +[generated-binding contract](completed/native_codegen/native_resource_absent_generated_bindings.md). + +An executable source function rejoins a unique retained AST declaration and its +exact resolver definition. Ordinary declarations use their actual DefInfo span; +mutual definitions additionally rejoin the actual-body resolution and retained +forward declaration. Namespace and header checks remain exact. Runtime +registration clones identify the original checked body and cannot replace it. +Generic selection remains an independent concrete-body proof. + +The ordered ledger covers source scopes, operation temporaries, partial actuals, +owned storage/returns and occupied result payloads. Explicit close and reverse +scope/failure cleanup retire each owning ticket once; views keep a live checked +backing and never gain a ticket. Cleanup completes before context teardown. +Protected cleanup continues after a finalizer panic. A cleanup failure takes +precedence over entry failure; clean cleanup preserves the ordinary entry error, +and source result.fail remains data. The implemented synchronous reference +slice does not prove task/actor cancellation or all aggregate operations. + +Hook descriptors grant no invocation authority. A reached exact hook must have +ReferenceRuntime purpose, matching program/type/signature/caller facts and +runtime capability provenance. Explicit comptime, constants, verify and property +workers cannot execute it; purpose is restored after property evaluation. +The production catalog remains empty, its installed provider is Disabled, and +scripted provider installation is compiler-test-only. All 83 Resource-focused +reference tests pass, including both-profile Source lifecycles, exact absent +aggregate layouts, enum-constructor dispatch and required-worker controls. +Direct scoped/generic/required regions retain original body identity and complete +concrete intrinsic metadata. Pipelines preserve original steps and lexical actual +order; mutable assignment retains declaration slots and replaces owners only +after RHS evaluation. Repeated entry restores metadata and body cursors. +Ordinary refinement predicates, occupied aggregate custody, broader captured/ +reflected/worker coverage and production providers remain pending. Checked HIR +lowering now retains an Arc-bound Resource manifest, exact hook descriptors and +original Source invocation packets; MIR preserves that same manifest and lexical +argument order. Whole-program validation checks unused hooks, nominal types and +complete signatures. Raw lowering has an empty manifest and cannot authorize a +Resource layout. Ordinary staging, acquisition, copy, move and native emission +refuse Resource operations until the separate CFG ownership/drop plan and ABI +exist. Five focused Source handoff groups and all 522 HIR/MIR/codegen/object +emission tests pass; this does not establish native Resource execution. See +[the current reference record](completed/native_codegen/native_resource_hook_identity.md#reference-lifecycle-execution). + +A separate private `jett_runtime::resource_custody` core now keeps program/kind/ +owner-slot identity, non-cloneable owner and loan tokens, and scope/operation/ +return frames. Moves advance holder generations; copied physical carriers +cannot adopt an owner. Protected reverse cleanup continues after a finalizer +panic and records Resource cleanup failures separately from ordinary native +first-error state. A private native registration module now decodes bounded +owned little-endian layout tables and validates every row, including unused +hooks, complete signatures, occupied paths and operation references, before +issuing fresh nominal kinds. Its single installation holder binds the registry +context and rejects second installation, shutdown and live registry entries. +Frame signature consistency follows the containing function independently of +instruction site. A separate physical acquisition preflight checks/reserves +storage before a future provider effect; core and opaque-table preflights remain +mandatory. Registration itself creates no owner, loan, payload or provider. +The private native context adapter now uses the existing stationary context +lease and registry for exact installed layout, attempt, frame, owner, loan, +prepared hook and sum records. Ordinary and Resource lookup IDs share one checked +monotonic allocator; lookup bits alone cannot supply custody. Moves consume the +old carrier only after successful core transfer, and refused transfers restore +it. Test-only provider installation stages its paired Network publication after +fallible preflight. Production state remains absent and providers disabled. + +Completion performs protected reverse cleanup before retaining outcome messages +and counts. The caller's observed body status remains unchanged; ordinary first +error, Resource body faults, host panic and cleanup failures determine the +separate selected outcome. A legal ordinary FailureTake cannot erase a Resource +fault. Ordinary borrow Result carriers and their error strings use the existing +recursive native destruction path. Complete v2 Source rows retain lexical/formal +permutations, exact owned argument holders, resident parent loans, callee Scope +and provisional Return frames, and the original caller result destination. +Source entry validates every owned transfer and reserves the whole acquisition +log before moving a prefix. Return publication requires the completed Scope and +current Return activation; failed Scope or Return cleanup cancels publication +and takes priority over body failure. Absent and failed sum shells move as whole +carriers without recovering a Resource owner. Historical v1 metadata cannot +activate these Source transitions. + +All 32 exact Resource leaves use the existing authenticated context; entry +getters return its actual root Scope and paired Network carrier. Output checks +precede effects and reject known context/output overlaps. The formatted runtime +package passes 204 unit and 13 integration tests, including nine Source controls, +three v2 registration controls, two batch-transfer controls and all fifteen +previous adapter controls. The exact root-outcome getter reads the body status +saved only after root Scope retirement in the same Runtime attempt; it neither +resets errors nor substitutes cleanup status. Five controls pin the original +status, active/stale/foreign refusal and body zero with cleanup panic. Ordinary +failure-companion validation checks every selected nested sum payload's ownership +bit before drop. These are runtime host/C-boundary checks. Dedicated +compiler body emission and ordinary companion cleanup, the matched archive, +linked Source lifecycle execution and production providers remain pending. See +[the Source runtime record](completed/native_codegen/native_resource_hook_identity.md#native-source-runtime-and-custody-leaves). Move dataflow transfers one cleanup obligation; views never own cleanup. Scope exit, return, handled failure, cancellation, dropped actor messages, and runtime @@ -1524,6 +2738,14 @@ lowering. Native code adapts values to the same operation model instead of defining a second debugger wire schema. See the [breakpoint protocol record](completed/breakpoint_pause_inspection_protocol.md) for the lifecycle, authorization, envelopes, examples, and verification slices. +HIR now records the visible source bindings at each breakpoint and carries the +snapshot through MIR. The native backend emits the compatibility debug line +for empty and multi-binding snapshots, including supported aggregate values, +after evaluating the optional condition. Cross-function binding scope and +interactive pausing remain pending. +Failed native assertions keep their interpolated message in context-owned +storage; the launcher copies it before destroying the context. The v1 result +record continues to expose only process-lifetime static message bytes. ### How the Compiler Uses the Runtime @@ -1758,9 +2980,10 @@ Crypto has reached that boundary for its implemented initial surface. Public `crypto.sha256`, `crypto.sha512`, legacy-only `crypto.md5`, and key-first binary `crypto.hmac_sha256` declarations live in `stdlib/crypto.jett`; source wrappers convert exact UTF-8 through `bytes` and -format raw fixed-size digests as lowercase hex. Only private trusted compression -kernels remain in the interpreter, and project code cannot call them. HMAC -borrows a secret byte key and byte message and returns a secret 32-byte tag; +format raw fixed-size digests as lowercase hex. Private trusted compression +kernels are shared by the interpreter and typed native runtime leaves; project +code cannot call them. HMAC borrows a secret byte key and byte message and +returns a secret 32-byte tag; HMAC-SHA-512 remains reserved and undiscoverable. Exact taint, security, and backend obligations are defined by the [Crypto hashing and security contract](completed/crypto_hashing_security_contract.md). @@ -2078,6 +3301,12 @@ Since Jett has one canonical form for everything, the formatter is relatively si The `jett test` command runs both `verify` blocks (at compile time, via the comptime interpreter) and `property` blocks (at test time, via the fuzzer). +Both mechanisms evaluate assertions for concrete inputs. A passing `verify` +block establishes its assertions for the executed examples; a passing property +run establishes them for the generated cases only. Neither is a universally +quantified proof. Static guarantees such as ownership, capability requirements, +and exhaustive enum matching are enforced separately by the typechecker. + ### Fuzzer Architecture 1. **Type-aware generation:** For each `given` parameter, generate random values based on the type: @@ -2268,7 +3497,7 @@ Each crate has its own unit tests: ### Integration Tests (`tests/`) -- **`compile_pass/`** — Jett programs that should compile without errors. Tests run `jett build` and assert exit code 0. +- **`compile_pass/`** — Jett programs that should compile without errors. Tests run `jett build --check` and assert exit code 0. - **`compile_fail/`** — Jett programs with intentional errors. Each test file declares its expected error-code multiset with comment annotations such as `# ERROR: E0601`; repeated diagnostics use the compact form `# ERROR: E0601 x3`. The harness rejects missing or malformed annotations, unexpected codes, and incorrect duplicate counts without depending on diagnostic order. Message text is not currently contractual. - **`run_pass/`** — Jett programs that should compile and execute successfully. Verify/property fixtures assert internally, and stdout-producing runtime fixtures can be pinned through the driver's captured-stdout test helper. - **`snapshots/`** — Source fixtures for the current direct AST snapshots tracked by [#162](https://github.com/vycdev/jett/issues/162); the committed `insta` outputs use the parser crate's conventional snapshot directory. HIR, MIR, and LLVM IR snapshots remain deferred until those representations exist. @@ -2549,11 +3778,1786 @@ call, type, and handle diagnostics instead of getting a parallel error family. |---|---| | E0000 | Driver and file/project discovery errors | | E0200–E0212 | Name resolution errors and warnings (undefined, duplicate, namespace visibility, `export root`, type naming) | -| E0300–E0373 | Type and language policy errors: calls, generic arity and function values, handles, interfaces, refinements, bitfields, JSON policy, state machines, reflection metadata, pipeline boundaries, collection hashing, sequence policy, arithmetic safety, graphics policy, and release debug-print policy | -| E0400–E0401 | Ownership errors (use-after-move, consuming a view) | +| E0300–E0378 | Type and language policy errors: calls, generic arity and function values, handles, interfaces, refinements, bitfields, JSON policy, state machines, reflection metadata, pipeline boundaries, collection hashing and equality, sequence policy, arithmetic safety, graphics policy, release debug-print policy, mutable global rejection, and required explicit constant evaluation | +| E0400–E0404 | Ownership errors (use-after-move, consuming or writing through a view, move-only closure captures, pending task control, immutable rebinding, and temporary assignment roots) | | E0500–E0503 | Capability and purity errors (impure calls and capability-parameter ownership) | | E0600–E0603 | Secret errors (secret exposure, invalid declassification/helper use, secret-containing output) | | E0700 | Actor response errors (`respond` outside a handler with `responds`) | | E0800–E0802 | Complexity limit errors (too many statements, too much nesting, too much cyclomatic complexity) | | E0999–E1000 | Lexer/parser diagnostics surfaced by the parser | | E9000 | Comptime verify failures | +| E9001 | Required compile-time evaluation failures, unsupported namespace constant types, and invalid primitive constant value shapes | + +### Native Linux executable seed + +The Cranelift scalar AOT path can link on the exact x86-64 Linux GNU host as +well as the existing Windows MSVC target. An explicit, target-matched launcher +bundle supplies ABI version, CRT mode, archive and system libraries. Linux uses +`cc` or a literal `JETT_NATIVE_CC` executable path, without shell interpolation; +Windows retains MSVC SDK discovery and the static-CRT contract. Both publish +only a successfully linked executable. The Linux scalar seed runs under an +automated deadline without source-tree or compiler environment dependencies. +Full language/runtime parity and clean Windows execution are still release +gates, not claims made by this seed. See `completed/native_codegen/native_codegen_parity_plan.md`. + +Explicit `comptime` results are materialized into typed HIR before MIR lowering, +retaining their checked type and span. The shared native value materializer +constructs supported aggregates from the evaluated value, including collections, +sums, structs, bitfields, enums, machines, and bytes. It resolves named function +values by checked identity and capture-free inline functions by source body span +and signature. Captured inline functions resolve by the same checked span and +signature; their evaluated capture values become typed caller-local bindings +that feed the existing closure environment path. +The checker passes the surrounding expected type through `comptime` for contextual +sum values. Ordinary pure calls remain runtime calls. Missing or unsupported +baked values fail before codegen; the original source computation is never +emitted as a fallback. Native regressions execute baked `math.factorial(5)` +after removing its source file +and compare supported composite and closure results with the interpreter. + +Namespace primitive constants use the same evaluated-value materializer through +their declaration-keyed HIR `Constant` reads. HIR baking completes before MIR, +including reads inside test bodies, generic specializations, inline callbacks, +and constant trace temporaries. Native validation rejects any remaining marker. +This handoff does not authorize move-only namespace constants: their read, +sharing, lifetime, and authority rules remain unresolved, and compilation +conservatively reports E9001 rather than introducing implicit clones or storage. + + +### Native string and numeric execution slice + +The current Cranelift subset also executes immutable strings, interpolation, +print/println, checked Stdout entry parameters, and typed numeric runtime kernels. +String handles have explicit retain/release ownership and are destroyed at last +release. MIR definite-initialization and liveness facts drive local and temporary +cleanup through branches, loops, overwrites, returns, and terminal failures. + +Floating-point `modulo` emits the typed `FloatRemainder` runtime leaf after the +ordinary pending-operand checks. Its binary64 operands and result use the same +remainder operation as the interpreter. Binary32 operands widen exactly for the +call, and the result rounds back to binary32 at the checked expression boundary. +NaN and infinity outcomes remain floating values rather than runtime failures. + +`list.sum[T]` uses the checked numeric primitive width in both execution paths. +The native `ListSumPrimitive` leaf returns packed value bits for typed unpacking; +fixed-width integers wrap and floating sums round at each addition, including +binary32. Empty lists return zero of the declared numeric type. The list remains +borrowed, and outer pending lists or pending elements retain their terminal +failure checks. The checker admits only exact numeric primitive element types; +transparent aliases already resolve to the same canonical type. The policy is +attached to the closed `IntrinsicId::ListSum` signature, so public source-facade +specializations, inferred and pipeline calls, concrete function wrappers, and +trusted private-kernel calls share it. The private kernel requires its single +explicit type operand; public inference still selects that operand through the +source facade. The policy does not recursively coarsen refinements +or strip secret wrappers. An unsupported domain produces E0300 before required +values, frontend test bodies, or native publication run. The existing generic +source signature introduces no public addition/identity interface. See the +[summation decision](open_design/list_sum_type_contract.md). + +`list.sort[T]` selects its runtime carrier through refinement layers over +numeric primitives, `string`, or `bool`, including nested refinements and +transparent aliases. Admission still requires the identical checked `list[T]` +input and result. The existing runtime reorder preserves payload bits, pending +metadata, and owned string handles; it neither coarsens the public element type +nor synthesizes values requiring predicate evaluation. Secret and aggregate +carriers remain ineligible. Sort and sum use separate carrier admission, so +sorting a refined list does not admit it to the numeric-primitive summation domain. +Indexed sorting and sortedness checks use the same numeric width selection. +They retain pending strings, selected pending keys, and pending rows as equal +comparison cases without reading their payloads. Sortedness uses IEEE `<=` for +ready floating pairs, so NaN makes an adjacent comparison false; sorting retains +its stable equal-comparison fallback for unordered floating values. + +Native `math.average[T]` and `math.median[T]` admit every checked primitive +numeric list carrier and require an exact `float64` result. Runtime conversion +sign-extends narrow signed values, zero-extends unsigned values, and widens +already-rounded binary32 values to binary64 before the shared aggregate +algorithm. It validates carrier bits and checks outer pending depth, empty +input, and pending elements before decoding values. The kernel leaves list +storage and ownership intact; the source wrapper retains its consuming input +contract. Refinements, secret wrappers, and nonnumeric types remain rejected, +independently of sort's refinement support and the open summation domain. + +Native aggregate consumers check the owner's outer pending depth before public +field reads, machine state tests/transitions, enum matches, and each ancestor in +a projected sequence path. `StructPendingAccessCheck` leaves ready owners and +their children untouched. Machine transition checks precede payload evaluation. +Raw `StructField` remains available for internal reconstruction and reflection. +Enum matches read numeric/bool payload depth before taking the field and retain +that depth in the binding's scalar task carrier. +Pending diagnostics use the typed debug graph and preserve its secret redaction. +An unavailable graph uses a conservative `[redacted]` rendering without reading +children; that fallback does not establish exact public diagnostic parity for +unsupported layouts or settle the open hidden-secret observation policy. + +Non-release `print` and `println` validate aggregate arguments through the native +debug type graph and reuse `DebugAppendAggregate` to format a borrowed value into +an owned temporary string. Primitive task formatting retains its existing path. +MIR argument modes preserve explicit views (also through coarsening and +declassification) without changing consumption of ordinary print arguments. +Handler extraction, ownership planning, and emission use the same argument rule. +Formatting starts only after every argument has evaluated successfully. +An additive `Uninhabited` debug node describes never-typed children of empty +containers and absent sums; attempting to format an actual value at that node +fails without reading a payload. Native and interpreter accepted Print/Println +formatting now use typed redaction for hidden secrets. This selected print +policy leaves trace behavior unchanged and pending-sum diagnostics separate. +The accompanying direct-field diagnostic repair applies typed secret redaction; +integrated verification remains pending until its focused and linked tests run. +Typed aggregate formatting traverses values with an explicit work stack, so +finite recursive values have no formatter-specific nesting cutoff. An active +path tracks layout, node, and payload identity to reject malformed cycles while +allowing shared children on separate paths. Dynamic interface and builder +layouts are cached for the traversal; aliases are resolved with a schema-sized +cycle check. Secret nodes short-circuit before inspecting payloads or pending +depth, and the caller receives text only after the whole formatting succeeds. + +Terminal runtime failure is a context-local first error, not `result.fail` data. +Compiled calls test that channel before using a return value; failure edges release +frame owners and propagate it to the launcher. Context destruction checks for leaked +native owners, and cleanup failure overrides entry failure. UTF-8 string kernels +use the same extended-grapheme segmentation dependency as the interpreter. This +initial handle representation is not the proposed inline/SSO optimization. +Payload enum equality compares the selected variant's integer, boolean, +floating-point, and string fields through the native value ABI. Supported +aggregate payloads use the typed layout graph, including recursive enums, +collections, sums, machines, and bitfields. Its explicit comparison stack +preserves left-to-right short-circuit evaluation and rejects malformed cycles +without imposing a fixed nesting cutoff on finite values. Equal handles do not +bypass payload comparison: a nested NaN still compares unequal. Comparison +against a known unit variant checks only the variant tag. User-struct payloads +invoke their exact explicit `Equatable.equals` implementation. Erased-interface +leaves are rejected by the frontend before this comparison path. Concrete actor +identity retains its separate checked contract; neither rule creates a dynamic +equality operation for erased payloads. + +Internal native `nothing` parameters, returns, locals, and aggregate payloads +carry an unowned u64 pending depth. Sequential `run` increments that depth and +`join` unwraps one level or returns a cancellation error for plain `nothing`. +Closure captures, explicit `comptime`, debug formatting, and equality preserve +the interpreter's current behavior. The public entry ABI still returns runtime +status. Sequential cancellation leaves the operand unchanged; asynchronous +scheduling and capability cancellation checkpoints remain unimplemented. + +The portable `native_conformance` integration suite executes the 30 manifest +entry points, 25 runtime contracts, and complete verify/property inventories on +both supported hosts. Entry and runtime-contract gates compare exact stdout, +stderr, and terminal status with the interpreter, using the manifest's scripted +providers and requiring successful context cleanup. The `native_parity` example +discovers those bodies from parsed source and includes their execution counts +alongside lowering, object, main, and runtime-contract results. Its successful +fixture-gate result is distinct from a complete native release audit. +All native builder modes retain frontend property failure diagnostics, including +shrunk counterexamples and the property-name source span, before any native +emission or output publication. Generated suite HIR sets compiler-owned property +name and one-based trial context before materializing each case, then clears it +after the call. The runtime copies that metadata independently of Jett value +handles. Only terminal failure copies prepend it; errors caught during +refinement checking retain their original text. Normal failure cleanup still +runs. The native property driver retains the exact checked case/function +identities and uses shorter native prefixes to locate the first failing case +without parsing debug output. It confirms an isolated replay, then reuses the +interpreter's ordered 50-step shrink search with a native execution predicate. +Narrow integer pools carry their exact primitive identity in `Value::Typed`. +The shared shrinker preserves that identity and rejects out-of-range candidates +before either execution backend runs them. These tags survive list/set/map, +sum, enum, and struct payload shrinking; integer carriers at their full 64-bit +width keep their existing representation and search order. Refined generated +values likewise retain their canonical type names. `float32` pools carry a typed, +binary32-rounded payload in the interpreter's `Float64` carrier; every shrink +candidate is rounded back to binary32 before replay or counterexample formatting. +Generic record pools retain `concrete_type` using the interpreter's canonical +type formatter, including transparent alias normalization and declaration-ordered +type arguments. Replacing a field during shrinking preserves this metadata; +otherwise erasure of a nested record loses its checked implementation owner. +The interpreter recursively checks these tags before executing a shrink candidate. Native candidate +materialization resolves the exact checked predicate functions and emits the +existing refinement handlers, including inherited and nested predicates. A +rejected candidate clears its diagnostic context and returns without calling +the property; normal failure cleanup releases partially constructed inputs. +The initial suite and prefix replay still use the already validated generator +inputs, avoiding additional predicate effects in normal suite output. +Every replay clones the retained checked HIR, replaces only compiler-owned +input construction, completes typed conversions, validates MIR, and compiles +and links a fresh attempt. It does not reload source or execute property bodies +in the interpreter. Every attempt has a fresh process/context and a deadline. +Only exits 0 and 71 count as completed attempts; other exits, compilation/link +errors, timeouts, and non-reproducible failures abort the runner. Initial suite +stdout/stderr remain separate from replay output and the shrunk counterexample. +Native link and execution capture retains parent-owned append-only temporary +files and gives each child an owned duplicate handle. Each readback uses explicit +offsets bounded by one sampled file length; append-only writes preserve earlier +bytes when Windows positioned reads move the shared cursor. Capture errors retain +their stream, creation/duplication/snapshot stage and original I/O source. The +existing polling deadline and child kill/reap protocol do not wait for EOF from +descendants that inherited output handles. +The CLI validates explicit build targets before loading source, accepting only +the current supported host. Native `build` invokes the driver object and link stages and publishes an +executable atomically. `build --check` retains frontend-only validation. The +installed layout is `bin/jett`, `lib/jett/stdlib`, and +`lib/jett/runtime///launcher.json` plus its archive. +Runtime selection prefers `--runtime-bundle`, then `JETT_NATIVE_RUNTIME_BUNDLE`, +then the installed manifest; validation checks compiler version, target, ABI, +CRT, profile, archive filename, and ordered native libraries before linking. +Stdlib lookup prefers `JETT_STDLIB_DIR`, the installed sibling directory, then +the development tree. Packaging builds both runtime profiles outside the CLI; +the compiler never invokes Cargo. Default artifacts use +`target///` (`.exe` on Windows), overridable with `-o`. +The native driver accepts `BuildOptions` for artifact generation. Release mode +applies frontend debug-print restrictions, discards trace/breakpoint HIR nodes +before MIR expands their expressions, and enables Cranelift speed optimization. +Breakpoint conditions therefore have no release execution or output. +Native verify and property suite lowering, object emission, and executable +building accept the same `BuildOptions`. Their existing entry points retain +default debug behavior; options variants apply release source policy and +optimized codegen. Runtime archives remain explicit caller inputs, validated +against their selected profile when loaded from a manifest. + +All eleven checked capability types have native entry grants and value +representations. Stderr, Stdin, Filesystem, Network, Process, and Log use +context-bound opaque tokens for their implemented move/view/call/task/debug +behavior; this does not implement their pending I/O providers. Provider leaves +continue to validate the exact authority granted to their context. + +One-line breakpoint snapshots use the current lexical function frame in both +execution paths. The interpreter applies its name-lookup scope floor when +collecting bindings and preserves closure capture type metadata; native code +uses the checked HIR snapshot. Caller bindings are never merged into a callee's +snapshot. Nested-scope locals disappear when their scope exits. The compiler +also exports breakpoint exclusions from ownership flow, including transfers at +binding and destructuring sites. HIR filters snapshots with those exclusions; +the runtime interpreter, comptime evaluator, and verify/property runners receive +the same map. Mutable assignment restores availability and branch joins require +availability on every continuing path. Actor handlers establish and restore their +own lexical frame, including on failure. + +Interface-typed runtime parameters, returns, stored values, and mixed collection +literals use typed native boxes and compiler-generated dispatch functions. The +checker records interface slots separately from concrete method bodies, including +generic and reflected body facts. Interface values remain move-only, with explicit +cloning, borrowing, pending depth, and recursive secret redaction preserved. +Existing lists, maps, optionals, and results convert recursively across compatible +interface boundaries while preserving ownership and pending depth. +The runtime parses the finite compiler-owned conversion descriptor with a parent +stack, retaining list/optional/map child order and both result branches. A checked +conversion is not rejected because its descriptor exceeds 128 nested nodes, +including when the source container is empty. Truncated descriptors, invalid +flags, null callback addresses, and trailing bytes still fail before conversion. +The checker compares function parameter types contravariantly, return types +covariantly, and view modes exactly. Generated native adapters retain source +callbacks, preserving captures, debug names, and pending depth, including callbacks +inside existing lists, maps, optionals, and results. Container descriptors use +linked addresses of checked generated adapters. Comptime callbacks materialize +their checked source signatures before conversion to the requested type. A final +HIR conversion pass reuses existing adapters and lowers newly materialized +conversions before ownership analysis. Canonical completion runs on an unpublished +Program clone. A private boxed receipt records the one actual coercer execution +and its complete before/after tables, retaining occurrence-specific reflected +callable identities. Actor, mutable-sum and temporary-view proof refreshers replay +that output only for the exact complete input and the same borrowed type arena. +They still validate their own original receipts and exact current operations. +All peer views and final HIR/backend-type checks must succeed before publication. +Every optional private archive Arc participates in identity comparison, including +hidden views on an archive without original Source data. The receipt is temporary +and cannot be stored in its own output graph; see +[canonical value conversions](completed/native_codegen/native_canonical_value_conversions.md). +Generic closures carry their lexical type +bindings, ordered enclosing type arguments, and alias reflection metadata so +materialization can select the checked body even when signatures coincide. +Named calls resolve explicit type arguments in the caller before isolating the +callee's type bindings. Both normal returns and failures restore the caller. +The driver retains expression maps by generic declaration and checked +specialization, including alias reflection and reflection-valued parameters. +An invocation selects its checked function context; a closure retains the whole +context through shared ownership. Native materialization matches reflection +parameter facts alongside concrete type arguments and alias metadata. HIR +inline functions retain their enclosing scoped type binding chain through +extraction, including unconditional and reflected-loop bindings. Each binding +retains both its canonical type and its source-visible reflection metadata. +Evaluated closures preserve the same chain; native materialization matches it +and native symbols encode structural types and alias-sensitive reflection. +Reflected dispatch uses alias-preserving identity through the dedicated +`TypeInfoReflectionMatches` leaf; the alias-transparent `TypeInfoMatches` ABI +operation remains available. `TypeInfo.args` loops retain each argument's +reflection metadata, including arguments reached through fields or other +argument loops. Generic argument selection can retain reflection from the +lexical scope or the interpreter's existing type reflection representation +when a nested source spelling lacks its own global metadata entry. +Named calls isolate the chain and restore it on success or failure. +The driver also exports recursive scoped expression maps from each checked +`comptime type` expansion. Execution selects the bound type's scope, and +closures retain that scope alongside their checked function. Scoped entries +are removed from enclosing flat maps, and nested binding maps remain under +their lexical parent. Conflicting matching scope maps fail explicitly. +Generic body entries are excluded from the global fallback map. Compiler-owned +facades may execute interpreter source instances without a checked map, while +conflicting matching maps fail explicitly. The +metadata is boxed to avoid inflating every interpreter value and recursive frame. +Actor and closure construction use separate evaluator helpers so their temporary +state does not enlarge every recursive expression frame. The nested JSON machine +conformance test exercises this path under the existing runtime stack limit. +Other reflection specialization keys and parity edges remain open; see +`completed/native_codegen/native_interface_values.md`. +Explicit comptime interface values materialize from unambiguous checked concrete +payload types; secret fields and pending wrappers remain part of the typed value. +Construction builders materialize through the existing typed start/put +intrinsics using checked reflection layouts. The checker exports source-aware +reflection operands from ordinary, generic, and scoped bodies separately from +canonical metadata, retaining names such as `Box[Label]` without changing the +canonical `Box[string]` layout. Shared reflection-value encoders produce field, +variant, and state metadata. Builder payloads use their storage types and defer +refinement and width validation to runtime finish; no source computation is +re-executed to recreate a builder. +Primitive interface values retain their checked concrete identity even when +runtime storage shares a wider carrier. Integer widths, `float32`, and nominal +primitive refinements dispatch to their own implementations through collections, +callbacks, reflected fields, and explicit comptime evaluation. Transparent +aliases use their canonical base identity. Arithmetic and formatting inspect the +payload without losing identity when the value is stored or cloned. +Collection and sum interface payloads retain their full type arguments, including +interface arguments qualified by namespace. Converting a concrete collection +changes its outer checked identity to the destination type while retaining the +concrete identities of its elements. List and record refinements likewise keep +their nominal implementation identity. Type aliases used for concrete method +calls or method values resolve within their declaration namespace. +Function interface values use the same retained type metadata, with the full +checked signature as their concrete owner. Calling one unwraps its payload; +dispatch and comptime materialization use its exposed signature. Callback +conversion replaces that signature without replacing the evaluated source body +or closure context, allowing native materialization to reconstruct the source +and its required adapter. Type-name parsing preserves nested function signatures +and parameter view modes before interpreting generic type arguments. +Function-backed refinements retain their nominal owner even when alias-base +classification returns the simple carrier `function`. Coarsening replaces that +owner with the base signature while preserving the source body and captures. +Explicit comptime evaluation runs on a scoped worker with the same 8 MiB stack +budget as reference execution, independent of the compiler caller's thread. +The reference evaluator dispatches ordinary and generic calls before its general +expression match. This keeps aggregate expression temporaries off the active +stack across nested source calls; checked result normalization and control-flow +signals still pass through the same central boundary. +Modules without explicit expressions skip worker creation. Worker creation +failures become compilation diagnostics; evaluation never moves to runtime. +Explicit values are keyed by source span and checked type context, including +generic arguments, reflection specialization, and lexical type-binding chains. +HIR snapshots lexical bindings at each explicit expression, so native baking +and interpreter execution select the same value after generic and inline +function lowering. Evaluation supplies checked type bindings without runtime +locals or parameters. Nested explicit expressions in baked inline function +bodies are collected independently; a missing checked value is an error. +Machine owners retain checked state qualification, including flow narrowing +inside state guards. Native materialization constructs the exact state and binds +it at the requested bare machine type when widening is required, so subsequent +erasure and refinement validation see the correct owner. That generated constant +local is cloned at each use so repeated evaluation does not consume its seed. +Enum, machine, and +bitfield refinements retain nominal identity without changing their payloads. +Actor references retain their declared owner through interface erasure; message +dispatch reads the underlying actor ID. Construction-builder and capability +refinements likewise retain their nominal implementation owner. MIR handler +extraction traverses validated refinement wrappers, including reconstruction +operations nested inside baked builders. +An interface-backed refinement retains two identities: its nominal method owner +and the underlying erased value's concrete owner. Interface conversion boxes or +unboxes that refinement even when both sides share the interface handle layout. +MIR treats extraction as a borrow of the outer box. Interpreter metadata and +comptime materialization preserve the inner identity so explicit coarsening can +restore the requested ancestor without losing the concrete base implementation. +The checker accepts a compatible whole sum before considering missing payload +handling. Interpreter refinement handles use the checked source type to +distinguish a sum from an interface whose concrete payload happens to be a sum. +For an exact base/ancestor of the expected refinement, whole-value refinement +validation takes precedence over sum extraction. Checked HIR keeps the matching +handle kind and a string error local. Native sum extraction checks outer pending +depth before reading the tag; a pending sum produces the reference handle error +without taking its payload, including inside a caught refinement predicate. +Predicates observe the base interface identity; already validated pending +refinements retain their invariant without rerunning a predicate on the task. +Constructor fields with pending predicate chains keep their checked ancestor +type through interface conversion; MIR adds the refined type after validation. +Interpreter boundary normalization likewise attaches nominal identity only +after successful validation, including for pending constructor inputs. +Machine state field type references pass through the same resolver traversal as +struct and enum fields, retaining declaration-site namespaces, visibility checks, +and declaration order before checked layouts and reflection metadata are built. +Reflected interface fields and lists retain their erased owners through record, +enum, and machine builders, including baked builders and completed values. +For an admitted reflected read of an interface-backed refinement as an erased +interface, native extraction selects conversion metadata from the actual field +type. After the existing owner, member, requested-type, and pending checks, +conversion borrows and clones the stored value into one owned result, retaining +the refinement's method identity rather than reusing its base interface box. +Ordinary and reflected boxing use the same owner rule: matching outer secret +qualifiers are removed when choosing the concrete implementation, while nominal +refinements and additional source secret layers remain intact. A secret source +converted to an unqualified interface retains its explicitly secret-owned +implementation and redacted layout. Destination secrecy still redacts the outer +value; concrete layouts retain recursive redaction for nested secret payloads. +These conversions preserve pending depth, source ownership, and failure cleanup. +They do not broaden reflected requested-type admission, whose remaining contract +is recorded in [reflected field request compatibility](open_design/reflected_field_request_compatibility.md). +Expected type context also passes through `run`: for example, initializing +`result[int64, string]` with `run ok(17)` retains both declared result payload +types in the checked body used by native lowering. +Generic structs retain their concrete type arguments across interface erasure, +including reflected construction and comptime evaluation. Typed observations +therefore redact secret generic fields while keeping the base struct display name. +Interface implementations use the full instantiated struct identity, so +`Box[int64]` and `Box[string]` dispatch independently. Transparent type aliases +retain that identity for concrete method calls and method values. A bare +generic owner such as `Box.name` is rejected for missing type arguments; name +a concrete instantiation with a type alias before accessing its methods. +Actor constructor parameters are retained before state initializers run; an +initializer consuming its working parameter does not consume the actor's capture. + +The full native parity gate is still incomplete: the current object count is +recorded in `completed/native_codegen/native_codegen_parity_plan.md`, alongside 30/30 main +outcomes and 25/25 runtime contracts, with 182/182 typed lowering. See +`active/native_value_abi.md` for ABI ownership and failure contracts and +`completed/native_codegen/native_codegen_parity_plan.md` for the remaining gates. In particular, +move-only value/drop elaboration, handlers, aggregates, collections, callbacks, +remaining reflection/JSON shapes, actors and full task scheduling, other capabilities, and clean Windows release +verification are not established by this slice. +Native JSON parsing now checks and emits the stdlib decoder for concrete +structs, lists, sets of primitive-backed hashable types, string-keyed maps, +optionals, results, supported `secret[T]` wrappers, and bare or +state-qualified machines recursively composed of supported leaves. +Native JSON eligibility checks every state's payload for a bare machine, +but only the exact selected payload for a state-qualified machine. An +unrelated state's non-JSON map key or other unsupported field cannot block +a valid qualified source specialization. Declared state metadata remains +complete; this eligibility boundary does not filter public reflection. +Loops over `type.machine_states[Machine at state]()` expose all declared +states. The checker therefore records field-type and field-level source-alias +candidates from all states for nested `state.fields` loops. Top-level aggregate +aliases remain non-matching probes with empty metadata; this does not add +automatic aggregate alias unwrapping. Direct loops over +`type.machine_state_value[T](view value).fields` retain the selected payload +boundary. This distinction keeps checked reflected dispatch complete without +broadening JSON admission. +The private fallback decoder selects raw-tree copying through the direct +condition `type.name[T]() == "json.JsonTree"`, matching the named decoder. +The existing checked static selection excludes that body when T is an +unrelated inactive callable, interface, or actor payload. An ordinary +`json_reflected_raw_type(view info)` call cannot supply the same checked +fact: pure-call folding never permits an incompatible generic return body +to escape typechecking. Alias, refinement, and secret dispatch continue +through their existing typed paths; public JSON eligibility is unchanged. +The private reflected map decoder uses an alias-aware closed key guard. +Only an exact string key reaches its typed string-map construction body; +other keys return an unsupported-key failure. Completing all-state checked +candidates exposed the old incompatible return of a string map as a +non-string-keyed T. No carrier conversion substitutes for a typed branch. +For malformed qualified input naming another state's unsupported map, +the guard can fail before the eventual builder state mismatch. Required +field lookup and exact pre-decoding validation retain their order. Public +non-string map requests still fail E0343; builder behavior is unchanged. +Machine parsing uses the reflected state builder; an empty state has no field +specializations, and HIR lowers its checked empty field loop as an empty scope. +Builder finish still rejects a state that differs from the qualified target. +Raw `secret[json.JsonTree]` uses a dedicated trusted parser; other unsupported +field shapes remain on the generic intrinsic path. Set elements may be narrow +integers or primitive-backed refinements, with their ordinary range and +refinement checks applied before insertion. Top-level +refinements with a supported base use checked source parsing and serialization; +signed `int8`/`int16`/`int32` and unsigned `uint16`/`uint32` JSON values now +use checked source decoding, both directly and inside supported aggregates. +The decoder checks the `int64` range before constructing the narrow value +through bounded arithmetic. `float32` JSON decoding uses a checked source +call to `float32.from_float64` after reading a JSON number as `float64`. +Checked source serialization formats the rounded `float32` value through its +existing string display path, including when it is a record field. +Record fields with supported refinements now decode through a checked source +conversion before entering the native reflected builder. The builder accepts +the exact refined type; direct struct constructors now lower checked field +predicates into MIR branches and emit a success or failure result. Inputs with +an intermediate refinement skip its already-validated predicates, and +secret-backed inputs are unwrapped for validation. Exact refined fields retain +their validated invariant. Top-level +enums with supported payload fields, including nested raw `json.JsonTree`, use +dedicated checked native parse and serialize hooks. Raw trees retain their JSON +wire behavior and are cloned when a decoder returns a borrowed tree. Pipeline +calls to `json.serialize` and `json.serialize_public` select the same checked +source serializer for supported structured types as direct calls. Parser +pipelines select the declared `json.parse` or `json.parse_exact` source facade +using their checked result payload and the existing recursive parse +eligibility predicate. This includes ordinary primitive parser pipelines; +the policy, argument metadata, and input ownership remain unchanged. Supported +sets with primitive-backed elements use that serializer directly or as record +fields; iteration clones the viewed set before consuming its elements. +Supported bitfields use dedicated checked parse, exact-parse, and serialization hooks +that dispatch unit-enum fields separately. Payload `list[uint8]` decoding uses +a bounded source conversion after JSON integer range validation. Nested enums +and bitfields composed of supported fields now use checked source parsing and +serialization inside records and collections. Generic branch specialization +selects the raw `json.JsonTree` wire path by checked `type.name[T]()` equality +with a string literal. Native +source serialization supports bare and state-qualified machines with supported +state fields; public serialization omits direct secret fields in records and +machines. Generic `type.has_secret[T]()` branches are checked static selections; +the serializer uses them to omit whole fields containing secret collections. +The trusted JSON record decoder passes already validated field values to the +native builder, whose runtime checks exact field metadata and canonical type. +General base-to-refinement builder inputs remain blocked until native finish +can run the predicate. Validated struct construction reserves separate +temporary owners for its record and result wrapper. A trusted concrete bytes +serializer emits the canonical hex JSON +string. Records with `json.JsonTree` fields use the checked +source serializer, cloning borrowed raw trees before encoding them. Native MIR +verification accepts checked inner-value promotion to a +secret local without allowing an implicit secret-to-public conversion. +Widening a state-qualified machine local to a bare machine consumes the source +unless it is cloned. +Direct and field-level aliases take the source alias branch before +construction. A self-recursive struct with supported fields remains eligible +for source JSON serialization and parsing: the eligibility walk treats a +revisited struct as an already checked edge, and checked function +specializations form a finite recursive call graph. Refinement-validating +construction remains outside this native path. +The public compiler policy still runs before the source decoder. +The interpreter handoff omits checked expression types at generic source spans +whose concrete instantiations disagree, so a later unsigned instantiation +cannot coerce an earlier signed call's values. + + +### Native user struct slice + +Checked concrete struct layouts now reach native fixed field-slot records with +explicit initialization and recursive ownership. MIR plans moves, call-bounded +projected views, clone temporaries and drop slots; codegen emits construction, +projection and all user control flow. Typed runtime leaves only manage storage. +Native sequence loops can borrow a checked struct-field collection or string directly: +MIR records the root owner and field path, keeps the root loan active through +the loop, and clones each loop binding without owning the projected value. +An owned field read clones the selected value, including nested move-only +payloads, while a field view borrows the parent through the call boundary. +The checker exports exact Equatable method identities for direct comparisons +and typed owner-to-method metadata for aggregate payloads. HIR turns direct +comparisons into exact method calls with an `EquatableResult` readiness +boundary; native aggregate traversal yields custom leaves to compiled method +calls and checks their result depth. Both reject pending method results before +inequality negation. MIR preserves the direct boundary through handler staging, +and codegen uses the existing pending-scalar check without an owning temporary +or new ABI operation. Explicit calls remain ordinary boolean calls. +Outer `secret` wrappers retain nominal enum constructor targets and taint the +comparison's boolean result. Mixed public/secret operands compare their checked +inner types without treating different nominal types as interchangeable. +Equality graphs inspect the outer wrapped value's representation while debug +graphs retain redaction; nested secret payload restrictions are unchanged. +Verified clone/view conversions may add outer secrecy but cannot remove it, +and native binary validation rejects handoffs that lose a secret result. +See `active/native_value_abi.md` for the supported +ownership subset and remaining projected-place/refinement/enum boundaries. +Native reflected construction now uses the same explicit `TypeConstruction` +builder source form for concrete structs, supported bitfields, enums, and +machines. The checked type arguments select a field layout, including source +alias spellings for field metadata, while runtime leaves own partially filled +records, validate `TypeField` metadata, and return handled duplicate/missing-field +errors. +Native struct, enum, and machine builders accept base values for refined +fields and invoke checked predicates at completion, after ordinary builder +errors. Enum and machine completion validates only the selected variant or +state. This handles nested refinements and mixed refined and ordinary fields +sharing the same base type, including both bare and state-qualified machine +targets. Trusted JSON decoders also bind each payload's exact reflected type +before insertion. +Native debug layouts recognize `TypeConstruction` handles. Builder metadata +also carries checked debug layouts for each field and records successful put +order, so trace and breakpoint render partial struct, bitfield, enum, and +machine builders in the same order as the interpreter without consuming them. +The zero-payload native debug tag `Redacted` (16) renders a secret without +reading its bits or handle. Alias nodes preserve this behavior, including field +pending depth. Interpreter formatting walks typed values and reflected builder +fields recursively, and typed reflection failures use the same formatter. +The checker exports declared debug type names separately from canonical TypeIds, +per generic instantiation and reflected body expansion. HIR/MIR retain those +labels, including closure captures. Interpreter loop, match, and handler bindings +retain type metadata; generic handlers and pipelines use the active invocation's +types when a single source span has multiple checked instantiations. HIR exposes +new locals only after lowering their initializers. +When a generic record helper is instantiated for a non-record reflection kind, +native code creates an empty builder that preserves the interpreter's handled +`type.construct_put` and `type.construct_finish` errors. It does not infer a +record layout from the canonical `TypeId` of a source alias. +Builder results transfer ownership through put and finish; abandoned builders +drop their populated fields. Bitfield finish also validates field widths; enum +and machine start validate selected member and payload metadata. State-qualified +machine finish checks the selected state. Structs requiring refinement +validation remain native parity work. +Earlier native coverage figures above describe historical slices, not the current +release gate. The exhaustive checkpoint is recorded in the active parity plan. + +Generic task joins preserve the instantiated error type in handler observations, +including when one source body is called with both secret and public errors. +The checked outer operand type selects whether a join preserves an exact result +or creates an outer `result[Operand, string]`. Secret and nominal refinement +boundaries remain intact; transparent aliases retain their canonical result +type. Reference evaluation and Join type reconstruction use the active checked +generic/reflected facts before declared-type fallback. Native sum joins clone +the owner while removing one outer pending layer, then transfer that clone into +the additional result when the checked output differs from the operand type. +Payload pending depth and inner failure data remain unchanged. +The native move planner borrows only explicitly viewed Join operands for that +copy operation and ends the temporary loan before returning the owning result. +Ordinary Join operands retain their consumption checks, including view-parameter +rejection without an explicit `view`. + +### Source-authenticated Resource ownership analysis + +Successful checked HIR lowering captures an immutable archive of the complete +original functions, equality map and type meanings, including nominal definitions +and generic arguments. MIR authenticates it before constructor capture. A private +per-function witness records the initially emitted graph and exact original/current +Source call associations. Full Source operands, target, access, lexical context and +evaluation order remain independently checked. Canonical block/local compaction +remaps current records; other unsupported rewrites roll back transactionally. +Nominal bounds are checked before archive getters, and malformed type metadata +is rejected before dependent custody queries. + +`ResourceOwnershipPlan` separately analyzes scope/operation/provisional-return +frames, exact local/expression/argument holders, incoming views, bounded loans, +moves, occupied Optional/Result arms and cleanup obligations. A relinquished +Resource actual has no ordinary caller owning-operand claim. The dynamic runtime +acquisition log must determine reverse cleanup order; local-number order is not +cleanup order. The plan's entry block is distinct from the driver-selected program +entry function. The initial plain/Optional/Result analysis does not yet implement +the execution ABI. A fresh plan now supplies the emitted native layout described +below. Direct handled actuals use the constructor-owned call regions described +below; complete aggregate/scoped/reflected custody and concurrency remain required. + +The formatted affected gate passes 535 HIR/MIR/codegen/object-emission tests, +32 type-library tests and the MIR doctest. Twelve focused ownership groups and +the missing-nominal-table regression pass. These are compiler checks, not linked +native Resource execution. See the [ownership handoff record](completed/native_codegen/native_resource_hook_identity.md#source-authenticated-ownership-handoff). + +### Bounded recursive lowering frames on Windows + +Heavy HIR statement, equality and inline-function branches and MIR expression +and per-actual branches now execute in private non-inlined helpers. Their +existing Source checks, context restoration, lexical argument order, whole-call +preflight and rollback remain unchanged. Resource call capture remains at the +original expression-lowering entry. This reduces unrelated Debug branch locals +in recursive callers without changing Source limits, worker stacks or launcher +ordering. The original Windows CLI capture regression and all nine capture +checks pass; compiler/object and driver/fixture gates pass. Measured MIR +lower_value frames decrease from 169,448 to 15,272 bytes. See the +[repair receipt](completed/native_codegen/native_resource_hook_identity.md#windows-lowering-stack-repair) +for individual frame measurements and exact verification scopes. Native +Resource Source execution remains pending its connected ABI and cleanup. + +### Resource execution family and object layout + +After authenticating the entire original checked HIR archive, MIR derives a +private execution family from Resource functions and their direct Source caller +predecessors. Resource-free wrappers receive their own sealed constructor +witnesses and fresh Scope and invocation proofs. Current graph edits cannot +recompute or expand that authority. Direct calls with handled actuals use typed +Begin/Stage/Invoke/End statements. Their private witness retains the original +call, lexical/formal permutation, generated endpoint headers, complete current +graph and exact completion sites. Fresh CFG analysis tracks the active operation +stack and arrived argument prefix across handler edges. Abandonment retires that +prefix before the original Return operand; normal completion follows exactly one +reached invocation. + +The reference interpreter records each callable body's operation floor and +Return destination separately from its lexical Scopes. Checked Source functions +retain the exact caller destination; ordinary closure and named-callable bodies +have no Resource publication destination. Return retires only newer Operations, +innermost first, while interleaved Scope owners and loans remain live. Private +one-shot receipts bind a canceled Operation to its exact parent and permit +completion only while propagating Return or error, never ordinary successful +output adoption. Cleanup failure drains the eligible prefix before preventing +the original Return operand. + +Native Source status checks validate a terminal completion record tied to the +exact caller Operation, immutable Source tuple and retired callee Scope/Return +frames. That record keeps the original body status separate from cleanup +failure. An aborted call propagates its recorded failure without permitting +output publication; incomplete or unrelated calls cannot acquire a completion +record. Clean output still requires a running attempt with no latched failure. + +Each native operation-frame slot stores its dynamic frame and prepared Source +call in sixteen bytes. Begin prepares the complete Source tuple without actual +effects. Stage evaluates once, activates its exact transfer or loan, and records +one lexical actual. Invoke consumes arrived endpoints and checks Source status +before publishing its result. End clears prepared storage and retires only the +proved prefix. Prepared borrowed-formal metadata remains complete even when no +invocation is reached, using the existing wire-v2 rows and runtime ABI. + +The fresh custody plan also records the exact incoming execution frame at every +reachable current statement and terminator. Sum tag reads, temporary transfers +and selected extraction use that frame; their Local storage may remain in the +ancestor Scope. This projection has no span, type or holder fallback, and public +graph edits still fail the original/current witness checks. + +The code generator projects one fresh whole-program ownership plan into bounded, +deterministic v2 layout bytes. Rows preserve exact containing-function signatures, +parameter access and evaluation order, caller result destinations, provisional +Return storage, absent sums and ordinary failure companions. Return publication +uses the callee's completed root Scope while retaining the distinct Return slot. +The immutable object accessor exposes that layout and the actual driver-selected +entry function, signature and root Scope. It emits no Source body or provider. +Dedicated body calls to the runtime ABI, a matched archive and linked lifecycle +execution remain required. + +ResourceCompanionPlan borrows one fresh ResourceOwnershipPlan and selects only +its exact immutable current function and types. Its private context retains the +complete Source caller validation and ordinary type/child, initialization, +liveness, alias, loan and move checks. Ordinary ownership storage excludes +Resource carriers; their custody remains in the independent CFG plan. The plan +exposes readonly ordinary storage and complete Source acquisition facts with the +same immutable program lifetime. Public ordinary CopyValuePlan, MoveValuePlan +and caller admission keep their Resource-pending refusals. The CFG validator +associates roles with exact current expression occurrences; clones or same-span, +same-type expressions cannot select those roles. This is a compiler prerequisite, +not native body or runtime authority. Three focused both-profile groups and all +546 HIR/MIR/codegen/object checks plus the MIR doctest pass. + +### Direct Resource emission and private matched archives + +The direct Resource emitter validates the original checked graph before native +preparation, derives a fresh immutable custody/layout plan and retains the full +ordinary companion/acquisition analyses. Only functions in that plan receive +the internal Scope argument. Ordinary functions retain their previous CFG and +storage: Resource failure blocks and Return cleanup slots are allocated only +for selected Resource functions. The C entry reads the authenticated original +root body outcome after retirement; Scope cleanup has its separate channel. + +One double-cfg `jett_runtime` staticlib owns the compiler-test main, private +installation and read-only observations. It carries the actual wire-v2 codec +and 32 Resource leaves. The matching receipt records actual profile, source, +archive, native-library order, CRT fingerprints and complete symbol/import +inspection. The driver report decoder preserves original outcome fields and +pre-destruction counts; missing observations refuse acceptance. Ordinary +runtime/test builds exclude the private modules. Actual HOST controls and five +shared reference/native Source scenarios pass in both private archive profiles; +the native programs run after deleting their Project and synthetic Stdlib files. +Exact outcomes/events and zero obligations are observed before teardown. The +private link uses its actual measured library ordering; the reviewed bcrypt/ +advapi32 prefix difference does not alter production launcher validation. +This is a local compiler-test distribution seam, with production providers, +supported-host distribution and broader Resource execution still pending. + +The selected normal-startup prerequisite is generated-object owned: the Resource +C wrapper obtains its own immutable manifest, authenticates and installs the +exact bytes/entry in the normal runtime, begins a Runtime attempt, and completes +that attempt on body/refusal returns. Ordinary objects keep their entry ABI and +the shared launcher has no optional manifest dependency. The normal launcher +observes retained outcomes/messages and completes a Generated-owned Running +attempt after a recoverable entry panic before its single context destruction. +The double-cfg scripted private runner retains ownership of its existing +attempts. Disabled installation supplies no Network authority or production I/O +credit. See [the generated startup prerequisite](completed/native_codegen/native_resource_generated_startup.md) +for the exact handoff, first-error channels and current bounded validation. The +restored source passes 68 Python helper controls, eight Runtime startup controls, +four codegen controls, the 11 existing launcher lifecycle controls and 2,371 +workspace library tests. Fresh callable regression gates retain 48 executions +against the changed normal launcher. The 16 fresh private Resource gates retain +754 executions and 900 entries. The complete local startup80 gate passes all +48 normal and 32 private controls across Runtime Debug/Release; the private +controls retain 40 decoded entries, including four derived borrowed probes +classified separately. Linked manifests are wire2; occupied Source33 wire3 is +object-only in this startup packet. Normal links use the ordered library vector +measured from private v6, without a separately measured normal compiler note. +Hosts must exclusively own entry and finish for the context; generated unwind +has no invocation or thread token, and a rejected concurrent or recursive caller +must not unwind its owner. The built-in launcher uses a fresh context for one +entry. Full workspace/native conformance, exact CI, GNU, installed distribution, +actor-graph import, public catalog, typed TCP transport, mixed entry capabilities +and real providers remain required. No concurrent-host ABI or provider acceptance +is implied. + +### Resource replacement preflight + +The native replacement leaf preflights its exact old and evaluated replacement owners against the registered destination before removing handles or invoking finalizers. Core custody checks both current generations and runtime frames, exclusive destination occupancy, absent loans, nominal kind and reserved transfer capacity. The existing transfer rechecks custody after old cleanup; no reusable transfer permission is exposed. Cleanup failure still retires the independently staged replacement and keeps its original failure priority. Four focused replacement tests and all 220 runtime checks pass. The Source projection is described below; aggregate replacement remains required. + +### Source Resource replacement emission + +The fresh MIR custody constructor seals the evaluated RHS slot into Replace, alongside its original mutable destination and old occupancy. The layout projects these exact distinct plain-owner slots to existing tag 12; equal nominal shape and the containing Scope are checked independently. Assignment emits the composite replacement once and publishes its new handle only after Status0, without replaying descriptive Drop/Transfer rows. Constructor-authenticated plain occupied Resource Local-to-same-Local reseating has no runtime row; vacant assignment uses its exact Transfer. Public Source/witness, ordinary companion and current activation checks stay mandatory. Eleven original Source scenarios pass the real reference and generated native programs in both profiles, with Source deleted before native launch and all native obligations retired before destroy. Conditional/sum, aggregate and projected replacement remain separate transport work. + +The selected direct-sum replacement contract extends that composite operation to distinct live `optional[R]` and `result[R, E]` shells with an exact registered direct-sum shape. It retains the original mutable owned Local, complete nominal Resource and failure shapes, containing Scope, current activation and exact Replace/Drop(old)/Transfer correspondence. Static Empty/Occupied/Conditional facts do not turn an absent shell into vacant storage or substitute for the actual authenticated payload. Preflight checks both shell loans, occupied token identities and generations, exact ordinary failure companions, and reserved handle/transfer capacity before effects. Old cleanup precedes destination publication; the independently owned RHS is cleaned once if old cleanup fails. Successful publication consumes the old and RHS handles and clears the RHS slot only after Status0. + +Both-absent sum replacement may execute in an otherwise eligible pure purpose without Resource acquisition, occupied transfer or an added capability. Some/Ok tokens retain the existing Runtime-purpose custody gates. Failure companions use the existing typed ordinary validation/drop protocol; explicit ordinary clones remain legal and replacement adds no hidden retain or clone. This contract reuses the existing Replace signature and tag without new public ABI or provider authority. Native acceptance of the extension, same-local sum reseating, aggregate/projected/actor-state replacement and wider failure carriers remain separate obligations; none is credited by this documentation change. + +The selected same-local custody whitelist admits an occupied plain Resource, or a direct `ResourceShape::Optional`/`ResourceShape::Result` shell with `Empty`, `Occupied` or `Conditional` occupancy. The original evaluated RHS and mutable destination must be the same owned Local, with exact Source, complete nominal/failure type, slot and containing Scope correspondence. Current activation and loan checks remain mandatory. Reseating preserves the existing sum shell and ordinary failure companion without a Runtime row, clone or ABI change; ordinary lexical cleanup still applies. Other carriers gain no native admission. The [active reseating note](completed/native_codegen/native_resource_same_local_reseating.md) records the original red baselines and the passing core compiler checkpoint. The existing `SelfRebind` remains the exact MIR marker without an added Replace/Transfer/Drop at that site. Linked native execution and occupied/unknown/companion/loan evidence remain pending. + +### Named Resource function values + +Checked named-indirect body selection retains the original Indirect invocation +and exact argument packet. A private proof joins the original callee occurrence, +immutable local initializer/alias chain, signature and retained declaration under +the current checked program/body attempt. Namespace-qualified producers require +the whole expression span to resolve to that Function definition and exact +retained header/signature; instance fields and method adapters carry no such +namespace authority. The reached function value and runtime +registration must identify that declaration; matching a signature or supplying a +public function name cannot select a replacement body. Execution uses the +retained Source body and the existing Scope, argument envelopes and Return floor. + +MIR captures the original root-scope immutable producer and aliases, authenticates +current headers/initializers after local remaps, and derives target facts through +the current CFG. An independent current-use check refuses escapes even if a +private current-body witness is copied and resealed. Exact current producer, +value and reached invocation occurrences receive readonly projections. Ordinary +companion storage admits only those proven descriptor expressions and locals; +public type-only Resource signature refusals and compiler-hook descriptor +handling remain unchanged. + +The native callee is evaluated once after the original actuals. Callable checks, +exact selected code-address equality and environment-zero validation precede +SourceEnter. Refusal reaches ordinary failure cleanup and retires the acquired +prefix. Accepted calls use the selected Resource family's context/environment/ +Scope/actual ABI, followed by SourceStatus; an ordinary indirect call cannot +supply that Scope. The descriptor has its ordinary owner lifetime and does not +create another Resource owner or finalizer. Runtime ABI and wire-v2 are unchanged. + +Thirty-two shared Source scenarios pass in both reference and linked native +profiles, including exact same-signature target selection, immutable aliases, +construction failure, terminal body failure, finalizer failure and combined +body/cleanup failure. Native Source is absent at launch and obligations are zero +before teardown. This is local MSVC private-archive acceptance; returned hook, +captured/scoped/generic and handled/pipeline callback forms and +GNU private-archive Source execution remain separate required work. + + +### Returned Resource hook descriptor transport + +Exact hook Return summaries are captured from the retained checked Source archive. +Current producer/alias/Return occurrences, headers and CFG state are joined to +that archive, with an independent body/entry/header snapshot checked before +canonical remaps. Readonly getters select only exact current opaque metadata +occurrences. A preceding ordinary branch can preserve a single root Return; +nested returns, captures and dynamic alternatives do not inherit that proof. + +Returned hook shape 10 and indirect invocation tag 15 select the exact hook and +signature plus a separate Acquire, Borrow or Close row at the same sealed frame. +Actual staging precedes the once-only callee evaluation and HookPrepare. Source +results are read only after SourceStatus completes Scope retirement. Exact opaque +metadata avoids ordinary Function cloning/ownership storage; it creates no +Resource owner or finalizer. Existing runtime wire-v2 and custody leaves are +unchanged. Authenticated Resource function roots seed typed dependency traversal, +so ordinary helpers receive normal declarations, verification and their existing +context/environment ABI. + +All 46 shared Source scenarios pass in both reference and Source-absent linked +native profiles (92 executions each), with zero obligations before teardown. +Nine focused MIR groups and 242 MIR library tests, seven focused codegen groups, +100 codegen library tests, 63 object tests and all 92 driver-library tests pass. +This is local MSVC private-archive acceptance. Broader implemented callback, +aggregate and reflection combinations, GNU private-archive Source execution and +coherent-revision release gates remain in current parity scope. Production +provider and asynchronous scheduler integration are unimplemented extension work. +See the +[returned descriptor note](completed/native_codegen/native_returned_resource_hooks.md). + +Required Resource materialization uses an opaque value proof produced from +jett_comptime's private program-bound checked cache. HIR consumes that proof +through an acyclic compiler-internal dependency, derives the admitted replacement +itself and records an authenticated execution view while retaining the immutable +original ResourceSourceArchive. MIR checks the original-to-materialized occurrence, +function identity, specialization and same-manifest hook relation through CFG +transforms. Public mirrored cache data or a fresh snapshot of mutated HIR cannot +supply this authority. + +The driver exposes checked required evaluation and baking seams using one +CheckedResourceProgram. Runtime Resource operations are preserved during baking; +only explicit required values are replaced. Compile-time-only helper classification +must be authenticated from the original checked call graph and exported/runtime +roots. It cannot be inferred from a callable signature or used to erase original +body, header or type integrity checks. + + +Required-value rows also seal ordinary helpers' current MIR bodies. Proof capture +is distinct from runtime custody classification: it cannot add Resource frames, +executable Resource roots or hidden Resource ABI parameters to an ordinary helper. +Both syntactic arms of an exact sum-tag guard are planned; the emitted runtime +branch and extraction checks retain actual absence/occupancy. Typed sum headers +and normalized operation nodes remain visible to disconnected-custody checks. +The current shared checkpoint passes 61 scenarios in both reference and Source- +absent native profiles (122 runs each) against the unchanged matched MSVC archives. + +### Borrowed Resource sum integration + +The next Resource family retains the checked concrete body's exact handled-view +binding origin through HIR and a private MIR projection region. Generated sum and +payload locals are nonowning. Fresh typed tag edges distinguish Some/Ok projections +from None/Fail observations; ordinary consuming SumTake remains a separate role. +Wire-v2 registration adds conditional source formals and explicit sum borrow, +observe, project, failure-read and end operations. Five selected status/out C leaves +extend the Resource leaf inventory from 32 to 37 without changing public record ABI. +Absent shells receive live-loan guards despite having no occupied Resource token. + +The expanded 138-scenario corpus passes 276 reference and 276 Source-deleted +native runs in Debug/Release against matched measured MSVC private runtime +archives. Original declaration keys include exact lexical paths; current headers, +backings, guards and constructor graphs retain independent seals. Certified +Fallthrough/Return/Break/Continue records end child loans before parent loans and +clear nonowning aliases before joins/backedges. Return operands run first; lexical +retirement does not complete RootScope or consume its backing. Recursive declaration +inventory alone does not admit For/Match/reflected bodies or unemitted declarations. +Alternate outer Default lifetimes remain pending. Ordinary borrowed data sums +follow their [separate implementation](completed/native_codegen/native_ordinary_borrowed_sum_codegen.md); +this borrowed-sum slice does not itself extend Resource For/Match/reflected-body +admission. See +[the borrowed-sum contract](completed/native_codegen/native_resource_borrowed_sums.md) and +[the verified nested contract](completed/native_codegen/native_resource_nested_sums.md). + +A separate [Resource control-flow proof](completed/native_codegen/native_resource_control_flow.md) +authenticates consuming Resource-free list ForEach and ordinary Resource-free enum +Switch. Sequence preparation reconstructs the exact three temporary headers, +source/cursor/length initialization, get/increment prefix and original CFG before +transporting current borrowed, lexical, call and descriptor proofs. Strict joins +and final raw ForEach refusal remain unchanged. The focused local MSVC packet +passes 30 cases / 60 Source-deleted executions across seven Sources, including +consecutive and nested loops. Full 191/382 reference and Source-deleted MSVC native +execution pass. At committed control-flow revision +`f4fa59da6656b1424e7689b78f7035975b14cc14`, the coherent workspace passes 3,454 +tests with zero failures and three ignored tests across 92 result groups; all +3,659 frozen inputs remain unchanged. The receipt is +`target/native-control-flow-f4fa-workspace-evidence/after-evidence.json`. +[Exact-revision CI run 37744039455](https://github.com/vycdev/jett/actions/runs/37744039455) +also completes all four Windows/Linux compiler and installed-toolchain jobs +successfully, recorded in the adjacent `ci-final.json`. These accepted gates +belong to that control-flow revision. The latest reflected-field bridge still +requires its own coherent-workspace and exact-revision CI gates. Excluded erased +carriers and other iterator forms need separate compiler transport proofs under existing +Source semantics. Source06's separate reference repair passes strict Resource execution, including +exceptional exits and same-grant reentry. A separate direct-field HIR/MIR ordinal +bridge now retains the exact checked scopes and passes its bounded native gate, +including clean same-grant reentry. Original Source06 exceptional native +single-entry and same-grant reentry acceptance, plus the bounded derived control +matrix, are recorded below. Broader lifetime combinations remain obligations. + +The [checked reflected-field reference bridge](completed/native_codegen/native_resource_reflected_fields.md) +retains the original For/body, parent attempt, resolver binder and ordered owner +metadata. Each field ordinal selects its exact checker-owned scope and full +reflection; runtime metadata alone grants no body authority. Calls isolate and +restore item selectors, and normal/control/failure exits restore the entry frame. +Retained `type.fields` facts also support pure required evaluation without a +provider. Direct nominal-field native acceptance is recorded below; broader +checked Resource reflected loops remain pending. + +### Checked Resource reflected-field ordinal transport + +`ResourceSourceArchive` now captures constructor-only correspondence for direct +nominal-struct `TypeFields` loops in the root body of exact non-generic, +noncapturing functions in its `CheckedResourceProgram`. HIR preserves +every checked `ReflectedIteration(ordinal)` expansion instead of coalescing equal +type/reflection arms in this path. The archive retains exact original For and +binding objects, unique resolver declarations, owner TypeId and full source-aware +reflection, ordered field snapshots, checked body facts, HIR headers and original +loop/dispatch/arm Scope paths. Its opaque proof cannot be created from public HIR +or caller-supplied metadata. The ordinary `lower_program` entry lacks this archive +and retains its current coalescing behavior. Checking specializes type-level +reflection facts while field-index/name branches inspect the current runtime +binder. Independent ordinary positive controls pass 30 Source-deleted native +main/verify/property executions across five forms without a production change. +Those bounded controls establish no ordinary coalescing defect and do not prove +broader checked Resource reflection transport. + +MIR consumes the exact archived dispatch occurrence. For each ordinal it derives +the original immutable `TypeField` binder's index-equality guard and a one-arm +`ReflectedTypeDispatch` with the full source-aware TypeInfo identity. Equal-TypeId +fields consequently retain separate guards and body targets while the existing +native dispatch uniqueness checks remain intact. Private original/current seals +revalidate guard order, dispatches, body and refusing targets, field headers, +archive membership and original lexical paths. Canonical sequence normalization, +pruning and local remaps update only current sites and headers. Arm-specific +subtree lookup preserves the exact nested handler paths; independent borrowed +projection and lexical retirement validation keep local membership and custody +joins unchanged. + +An ordinary function inside this checked Resource program can receive a proof-only +MIR witness for its own archived loop. Execution-family classification remains +ordinary: the witness adds no Resource plan, runtime Scope, provider permission or +custody. Its ordinary map, string, view and uninhabited sequence lowering uses the +existing finite `SequenceEdit` validator, retaining original headers and function +identity and refreshing only the current graph and independent body seal. +Resource execution functions retain their separate consuming-list reconstruction. +No runtime leaf, ABI or wire-v2 row is added. + +The integrated affected-phase gate passes 1,202 checks with all 3,679 frozen inputs +unchanged. The subsequent focused native gate passes 16 Source-deleted executables +and 24 entries across four cases, including clean same-provider/grant reentry. +The unchanged predecessor corpus separately passes all 191 cases / 382 +Source-deleted Debug/Release executions. Both native gates retain all 3,686 frozen +inputs, nine matched MSVC runtime files and the frozen test executable unchanged. +Exact receipts and the independent 30-execution ordinary positive control are in +the [field-loop note](completed/native_codegen/native_resource_reflected_fields.md). +Generic/alias owners, bitfields, variants, machines, `TypeInfo.args` and nested +producers still require checked Resource compiler transport proofs and Source +evidence. The original Source06 additionally passes all eight exceptional scripts +through 16 reference observations, 16 objects and 16 Source-deleted native single +entries in both checking profiles, with exact completion and pre-teardown retirement. + +The original nine-script matrix also passes 18 Source-deleted same-grant native +executables / 36 entries in Debug/Release: every original script followed by READY +on one retained Session/provider/paired Network grant. The private launcher +completes and observes the first attempt before requesting the next existing +`entry_begin`. All eight Resource counts and ordinary value storage are zero +before teardown; the runtime independently validates current ordinary +first-error/cleanup state, installed entry, attempt budget and absence of active +or residual custody. Completed Resource cleanup failure is retained historical +evidence and permits the declared next entry after these checks. No error reset +or failure-taking API is introduced. Both immutable completion/diagnostic records, +continuous ordered events and original first nonzero process exit remain exact; +the combined script is exhausted before one Session destruction. + +Fresh measured MSVC Debug/Release archives use compiled receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Actual reference/native session 30992, terminal `22c716`, passes both Rust +harnesses with exit zero and retains all 3,722 inputs and nine runtime files +unchanged in the tested candidate based on parent `b5c55ac7`, recorded in +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` +(SHA-256 `f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`). +The earlier zero-test filter attempt remains explicitly unaccepted in `after-v2.json`; +each corrected manifest filter executed one real test. Runtime ABI 1, Source wire 2 +and private script/report protocol 1 remain unchanged. This is local MSVC original +Source06 acceptance; the separate derived control acceptance follows. + +The unchanged derived Sources7/8/9 now additionally pass the historical ten-script +matrix: three handled Returns, two Break paths, two Continue paths and three +scalar Handle Default paths. Borrow-failure handlers in Sources7/8 return before +Break/Continue; the executed signal, rather than the fixture family name, defines +these counts. A separate Source9 READY baseline was verified without changing +Source bytes. Break keeps total17 and retires its selected scoped-type body; +Continue keeps total34 and skips the statement after that body; scalar Default +supplies primitive17, resumes the selected body and preserves both field visits +and total34. The outer optional owner is retained until the existing consuming +close or handled Return. + +Actual session 18249, terminal `e1bcf6`, passes all five one-test gates with exit +zero: 22 single-entry reference calls, 40 same-grant reference calls, 22 objects, +22 Source-deleted single-entry executables / 22 entries and 20 same-grant reentry +executables / 40 entries, for **42 native executables / 62 entries**. Each original +script is followed by its own Source's READY on the same Session/provider/paired +Network grant. Strict completion/diagnostic/event records, all eight zero +Resource counts and empty ordinary storage before teardown, combined-script +exhaustion and one destruction remain exact. Explicit-close cleanup failures +retain body255 / cleanup255 / Cleanup channel / first exit71 after READY; existing +entry-begin checks and immutable first observations are unchanged, with no reset. +`target/native-reflected-derived-control-native-evidence/after-v1.json` +(SHA-256 `f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`) +retains all 3,725 frozen inputs and nine measured v2 runtime files unchanged in +the tested candidate based on parent `f4c63a0f`, using the MSVC receipt pin above. +Prior original Source06 and 191/382 receipts remain separate. This gate adds no +GNU carrier/fresh-v3 native execution acceptance. The dedicated Return successor +is recorded below; broader lifetimes and coherent-revision gates remain separate. + +Dedicated additive Sources10/11 now pass twelve shared cases for genuine first- +and second-field successful Return, handled first/later borrow failure and cleanup +combinations. Three equal-int64 fields and terminal sentinels verify skipped +subsequent fields/statements. Source10's implicit Return retires its scoped loan +and outer optional owner; cleanup failure preserves original body0/cleanup255. +Source11 returns a distinct owned Resource892 while retiring aliased outer891. +Successful outer cleanup gates publication and exact caller Borrow892(29)/close; +helper cleanup failure suppresses caller use, retires891 then provisional892 and +propagates root body255/cleanup255. The helper Source follows a normal Return +path; the report observes the entry-root completion and retirement events, not a +separate nested callee completion. Cleanup channel, exact diagnostic and first +exit71 remain immutable after same-Source READY. + +Actual sessions 64018 (`0b2a26`) and 47358 (`dcfb82`) pass all five one-test gates: +72 reference calls, 24 objects, 24 Source-deleted single executables/24 entries +and 24 same-grant reentry executables/48 entries, for **48 native executables / +72 entries**. All eight native custody counts and ordinary storage retire before +one destruction; continuous ordered events, per-entry boundaries, script +exhaustion and both exact completions remain preserved on the same +Session/provider/paired Network grant. No reset or failure-latch clearing occurs. +`target/native-reflected-body-return-native-evidence/after-v2.json` +(SHA-256 `9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`) +retains all 3,730 frozen inputs and nine measured v2 runtime files unchanged at +tested parent `91105876e5dbbbe9bbaa1d19bc2408ec9a274255`, using the unchanged +MSVC receipt pin above. Sources7/8/9 and their historical ten scripts remain exact. + +The Return operand continues to precede certified lexical retirement; the +successful owned-return case uses a survivor without the outer owner's resident +lease. Same-owner live-lease Return, broader body/Default lifetimes, generic/nested +reflected producers, aggregate custody, fresh-v3 native evidence and coherent- +workspace/exact-revision CI gates remain separate full-goal obligations. The +rough 87% estimate and fixed 207/182 inventory denominators remain unchanged. + + +### Typed Resource aggregate carrier transport + +A separate private carrier plan captures the complete authenticated original type +graph and exact constructor/projection/current-CFG associations. Its slots reuse +Scope/Operation/Return frames while preserving individual leaf custody separately. +Runtime roots bind installed shape, activation, slot and generation; typed paths +and bounded leases validate live projections. Dedicated v3 graph/slot/Source roles +retain strict accepted v1/v2 decoding. An explicit parent-bound sum-loan adapter +bridges selected optional/result children without consuming borrowed backing. +Ordinary ownership fields cannot contain Resource-table handles. Bounded v14/v15 +object/native absence acceptance and current post-repair fresh MSVC-v3 +regression pass as recorded below; broader lifetime/purpose acceptance remains +required. See the +[typed aggregate plan](completed/native_codegen/native_resource_aggregate_carriers.md). + +### Authenticated ordinary native entry Scope + +The entry-aware MIR validator authenticates the complete current/original graph +before canonical preparation and returns a separate readonly entry Scope plan. +It adds no execution-family function, owner, loan or Source-call authority. Initial +checked Resource-archive lowering captures proof-only metadata and an independent +body seal for closed Network-only/Nothing candidates, including genuinely pure +entries. Missing, foreign, transplanted or resealed mutable bodies refuse; raw +HIR/default archive paths cannot mint this proof. + +Codegen serializes the selected wrapper root and Complete row separately from +Source-family signature/frame maps and includes the exact entry as a reachability +root. The wrapper validates the installed tuple, obtains the existing Network +grant, calls the ordinary two-hidden-parameter body, reads its retained Status, +attempts exact Complete without replacing the original outcome, and observes +EntryOutcome. Existing Resource-family bodies keep their calling convention. +Runtime ABI, layout wire and provider APIs remain unchanged by this entry bridge. +Current compiler proof and bounded earlier native checkpoints are recorded below +and in [the ordinary entry plan](completed/native_codegen/native_resource_ordinary_entry.md). + +### Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +### Current workspace and wider-language observations + +The current v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +Source13's repeated ordinary error, occupied resources, wider whole-language +parity and exact-revision CI remain pending. The rough 87% estimate and fixed +207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +### Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +### Remaining acceptance + +The current whole-workspace gate passes at v20, and all eleven fresh matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. Exact-revision CI remains +pending. Earlier accepted GNU Source-prefix and Return documentation and wire-v2 +receipts retain their independent historical scope; only the measured v21 gate +supplies current MSVC-v3 regression credit. No GNU runtime-archive acceptance is +inferred from these local MSVC results. + +Source13 repeated-error lifetime, occupied aggregate transport/replacement, +broader Resource-valued Default and generic/nested reflected lifetime compositions +remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + + +### Ordinary terminal-error lifetime for repeated checked entries + +The selected Resource entry lifetime keeps ordinary first-error state in a +context-owned private record issued with the exact installed entry, purpose, +RootScope and monotonic attempt identity. Nested ordinary/Source calls share +that record. Exact entry completion retires Resource custody and verifies the +ordinary owner/counter predicate before sealing status, dynamic diagnostic and +prefix. Existing completed Resource diagnostics and original body status remain +independent from selected cleanup. A failed report leaves a non-reenterable +tombstone; a failed retirement leaves the Completing attempt observable. + +The next constructor authenticates the exact sealed predecessor, fresh ordinal, +session failure/ordinary cleanup poison, installed layout and all original +custody/storage guards before publishing another record under the same context +lease. No reset/take-error API authorizes this transition. Existing checked +refinement recovery applies only within its active evaluation and cannot consume +a sealed terminal record. Historical Resource cleanup failure retains the +accepted original Source06 behavior; persistent ordinary cleanup poison remains +fatal even after empty tables. The private launcher requests declared entries, +retains the first nonzero process exit and every completion, and destroys one +Session. Legacy ordinary context failure behavior remains unchanged. + +The managed ordinary runtime passes 307 measured checks with fresh matched +MSVC archives in measurements-02. The new fourteen-Source same-grant gate passes +in Debug/Release: 28 Source-deleted executables / 56 entries, including both +original control13 Bounds messages, body1/cleanup0/Body and retirement before +one destruction. The accepted combined twelve-gate scope is 604 executables / +728 entries: the authenticated first 28/56 plus eleven freshly rerun predecessor +gates at 576/672. The fresh whole workspace passes 3,614 checks, zero failures +and 14 ignored tests across 66 nonzero / 92 total groups. Independent v24 receipt +verification preserves all 3,763 Source inputs, nine fresh/nine historical runtime +artifacts, HEAD/inventory, both native binaries and fresh02 provenance. +See the [ordinary error lifetime note](completed/native_codegen/native_resource_ordinary_error_reentry.md) +for exact sessions and hashes. Earlier Source13-pending statements describe +historical checkpoints superseded by this bounded acceptance. No reset, +independent nested callee observation, GNU archive credit or feature-denominator +change is introduced. The separate normal-main linked baseline is complete: +16 observations (12 exit0 / four exit71) from eight exact opaque objects reused +with Debug/Release runtime artifact profiles, with fifteen raw-probe Sources +preserved. It supplies no desired Source oracle, ordinary public-Main context- +retirement observations or feature credit. Occupied custody, broader lifetimes, +wider ordinary parity and fixes, and the successor's exact CI remain open; the +rough estimate stays 87%. + +### Native actor identity comparison + +Validated MIR retains exact nominal actor operand TypeIds and the checked +aggregate layout. The direct comparison leaf validates both opaque IDs in the +current context actor registry and requires complete live backing records. +The recursive Actor layout node compares base instance identity and pending +depth. Actor state stays behind its existing message interface. + +Pending handles use separate zero-field records with the base instance's debug +ordinal. `run` and `join` retain shared state without changing other aliases' +depths. Context retirement releases the base state once. The reference evaluator +uses the same live identity rule for direct and recursive comparison. + +The local actor suite passes on Debug and Release runtime archives after Source +deletion, including production output with Jett optimization enabled. See the +[actor identity verification note](completed/native_codegen/actor_identity_equality.md) for scope +and remaining native codegen acceptance work. + +### Checked occupied Resource struct constructors + +MIR retains authenticated Begin/Stage/Commit/End constructor operations and +their exact original HIR fields, written permutation, scope and companion +completion roles. Original/current body checks run before pruning; public +node edits cannot mint the retained constructor proof. The carrier plan and +codegen validator join each operation to the same checked family. + +Runtime ownership transfers transactionally between slot and carrier holders. +A unified acquisition ledger retires ownership before invoking finalizers, +including partial constructor cleanup and same-context reentry. Fresh local +MSVC Debug/Release ABI1/wire3/static-CRT acceptance on 2026-10-12 passes 754 +Source-deleted native executions / 900 entries across all sixteen Resource gates: +the original twelve predecessor gates, two occupied gates and two additional +pipeline/control-flow subset gates. Executed Rust gates decode each completion and check +the exact Source event, message, status and exit oracles, eight zero custody counts +and empty ordinary storage before one destruction. Compiler and Source inputs, +measured archives, compiled gate and dep-info guards pass after execution. The +current integration also passes all 2,314 workspace library tests, including +308 runtime, 577 interpreter, 317 MIR and 120 code generator unit tests, plus +six occupied-struct object emissions. Private +double-cfg archive measurement is separate from ordinary runtime unit execution. +Owning strings/nested occupied carriers, broader aggregate families, public Driver +provider admission, GNU and installed-distribution acceptance remain pending. See [the occupied struct checkpoint](completed/native_codegen/native_resource_occupied_structs.md). + +### Selected erased-interface concrete argument validation + +The dispatch boundary must bind a selected concrete method to the receiver's +retained checked implementation owner, then validate every concrete argument +requirement before concrete normalization and body entry. Runtime values retain +nominal and generic identities through erasure; identical field layouts cannot +substitute for the checked type. This is validation only and does not authorize +cloning, bypass pending failures, or remove native unboxing/handle guards. +Generated dispatch preflights already evaluated formals with `InterfaceValidate` +before normalization. Its exact retained TypeIds include nominal refinements, +generic arguments and widths. The reference path uses the selected declaration +namespace before installing callee scopes. The leaf grants no ownership and does +not allocate native value storage; descriptor parsing may allocate Rust storage. +Local MSVC Debug/Release Source-deleted gates pass, including optimized production +matching/mismatched cases. See +[exact concrete argument verification](completed/native_codegen/native_erased_same_owner_arguments.md) +for scope, helper-level limits and remaining platform acceptance. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and coherent-revision acceptance +of the generated Verify/Property transitions below remain full-goal obligations. +No Source exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. + +### Generated callback-suite proof continuity + +Checked Verify and Property execution now retain the original test-body owner +when constructing a closure witness. The owner is joined to the same retained +checked program, original block and namespace; active capture ownership and +Property purpose restore on both ordinary errors and panic. Public test-file +execution installs that same checked program instead of a metadata-only +projection. + +The HIR observation proof separately joins an immutable owned CallbackHolder +constructor or alias to its exact original binding, callable producer, captures +and reflected read. Matching types, spans or an edited public body cannot mint +that association. Existing full Source, current-body and CFG seals remain +mandatory; this proof grants no runtime Resource or Scope authority. + +The ordinary reflected-callable Property path now uses an opaque checked-case +plan from the original successful evaluator/cache snapshot. Its canonical HIR +factory builds the ordered 100-trial wrapper and records the exact protected +transition. Replay reconstructs from the private pre-suite graph, checks the +retained plan and original trial identities, and validates changed shrink inputs. +It cannot renew proof around a caller-appended or edited wrapper. Exact sampled +inputs use full typed snapshots rather than ordinary Value equality. Verify's +existing canonical factory remains unchanged. + +The Driver checked suite route retains the same original program and successful +required-value view. Canonical wrapper installation updates only the authenticated +suite transition while preserving prior functions and private reflected/graph +seals. Native shrinking starts from that retained base instead of adopting +publicly mutated HIR. The private test harness injects failures only after fresh +MIR lowering and before ordinary validation/codegen; it refreshes no proof. + +All three permanent Driver controls now pass for original named/captured +Sources19/20: both reference reports record 100 Property trials, six standalone +purpose objects emit, and six separately emitted Source-deleted objects pass +twelve normal MSVC Debug/Release launches. Current compiler/Source, dependency +and reused measured-archive guards remain unchanged. Exact outputs/status and +executable reaping/removal are checked; ordinary context-retirement counters are +unavailable. Both six-test Comptime/HIR groups, all eight native Property +controls and all 1,664 affected compiler-library tests pass. See the +[bounded audit](completed/native_codegen/native_acceptance_audit.md#generated-reflected-callback-suites-bounded-msvc-acceptance); +this adds no full-workspace, CI, GNU or installed-distribution acceptance. + +### Checked actor graph template import + +Successful required evaluation retains a program-owned immutable actor graph +template, joined to its private checked required-value identity. A type-aware +work queue and graph-wide memo preserve shared references, distinct seed nodes, +evaluated state, pending depth and admitted captures. Each runtime evaluation +imports fresh context-owned instances; ordinary aliases after binding share +those imported instances. Initializers, handlers, source predicates and compiler +observations are not replayed. + +Graph HIR reconstruction derives from the original checked program. Independent +original/current MIR seals authenticate native reserve/fill/commit/abort staging +before publication. Failed staging retires partial owners and registrations while +preserving the first error and cleanup poison. The checked actor constructor +remains the sole raw allocation boundary. Exact nominal and generic identities, +Owner preflight, Resource constructor/incoming-frame checks and Callback +plan/capture authentication remain separate requirements. + +An aggregate graph producer transported through Resource constructor staging +retains its complete protected lifecycle and exact Commit output. Generated +output headers remain separate from original Source headers. Planning and native +ordinary word representation select a current physical graph producer, endpoint +or complete call edge; they grant no type-wide aggregate admission or Resource +custody. Ordinary graph heap retain/drop and prefix cleanup remain in codegen. +When the Resource manifest has no kinds, the ordinary emitter still derives a +fresh graph storage plan before and after canonical preparation. Verification and +function emission consume that plan without a Resource layout or hidden Scope. +Canonical collection iteration records only the exact generated iterator owner +initialized from the protected original iterable. It remains separate from +original Source headers; current validation checks its initializer, header and +sequence operations before selecting ordinary storage. +See [the constructor transport note](completed/native_codegen/native_graph_constructor_transport.md). + +One private immutable `OriginalGraphAnalysis` table reconstructs all owner rows +for an exact checked execution archive. Execution closure classification, +constructor capture and typed graph validation share that table. Selection +checks exact archive identity, preserved type meanings and the complete original +owner; current body/header, constructor, transport and pointer checks remain +independent. Closure reconstruction recomputes membership using the retained +table. This adds no global cache or type-wide admission. A completed private +analysis proving no original rows, together with empty current metadata, may +return only a negative storage answer. It grants no proof or custody exemption; +positive/current records retain full validation. Original archive/type checks +and independent closure reconstruction remain mandatory. + +Ordinary checked namespace initializers use the same retained original program +and selected-purpose preparation as collected explicit expressions. The two +passes share the existing private checked attempt key, suppressing duplicate +evaluation of successful and failed initializers without filtering observations. +Baked reads remain silent; no runtime globals, Resource transport or provider +are introduced. Every retained checked ordinary archive also carries required +replacement through the private transition, regardless of graph/callable-plan +presence. Exact original archive, owner/context proof, typed occurrence, +original locals and generated binding checks precede canonical seal renewal. +An admitted TypeConstruction replacement must match the complete typed +transcript from the sole evaluated-builder producer with no added locals or +bindings. This repair grants no wider builder-domain admission. + +Ordinary actor/interface wrappers with exact original entry-Scope capture and +no execution or graph records retain the existing finite ordinary sequence +transition. That route independently excludes constructor/seal/call/region, +borrowed-sum and lexical-custody records, checks the whole canonical edit and +before/current witness, preserves original headers and refuses new custody. +It retains wrapper authentication without installing a new Scope or grant. +Dedicated graph sequence and real Resource execution paths remain separate. + +Graph Resource hook descriptors carry metadata bound to the same context, +installed program, layout, descriptor operation, hook and signature. The new +`jett_rt_v1_resource_graph_descriptor_box` export grants no Scope, Network +authority or Resource custody. The composed Resource inventory is 56 production +exports: 37 original ordinary leaves, one graph descriptor leaf, 13 carrier +leaves and five startup leaves; the eight private hooks remain separate. +Generated startup ownership and its test-only borrowed-attempt sentinel remain +unchanged. Completion releases Resource companions before retiring actor state +and unfinished imports for the exact Running ordinary attempt. Retirement +precedes emptiness checks and terminal sealing; first diagnostics remain intact +and cleanup poison prevents successful reentry. Later context destruction +observes the drained actor table. + +Existing Source admission excludes move-only Actor closure captures with E0402. +Unchanged admitted primitive captures retain their existing witness, and reflected +callables retain their checked plan and authenticated source. No unchecked cache, +witness reset or new capture admission is selected. This is a sequential import +contract. Existing sequential task/actor behavior remains in native parity +scope. Absent asynchronous scheduling and production-provider APIs are separate +future feature work, not prerequisites for current reference parity. Focused local checks and 32 linked MSVC executions passed; broader +native and supported-host acceptance remain pending. +See [the graph note](completed/native_codegen/native_comptime_actor_graph.md). + + +### Occupied Resource primitive companion transport + +The selected extension shares one checked primitive admission predicate between +constructor planning and physical companion validation. It preserves canonical +alias TypeIds, each original constructor endpoint and field ordinal, written +evaluation order, and the existing acquisition and cleanup transcript. uint64 +requires `Value::Uint64`; float32 physical validation requires the exact f64 +widening of binary32. Checked expression normalization is the separate rounding +boundary and restores every pending wrapper before physical validation. + +Integer, floating-point and bool carrier children retain pending depth +separately from payload bits. Nothing keeps its existing scalar-word depth +encoding: codegen sends zero payload plus depth into the carrier and reconstructs +the depth word on observation. String handles retain their existing depth +through clone, observation and retirement. Single-word observation refuses +pending carrier scalar data; the generated path uses the depth-preserving pair. +`jett_rt_v1_resource_carrier_child_scalar_task` and +`jett_rt_v1_resource_carrier_observe_scalar_task` carry payload and depth without +encoding Resource ownership or capability authority. Their observation outputs +are committed only after handle, operation and output validation succeeds. +All existing leaf signatures, generated startup ownership and graph descriptor +authentication remain unchanged. ABI1/wire3 remains unchanged. The selected +catalog has 58 production exports: 38 ordinary, 15 carrier and five startup; +the eight private hooks remain separate. Source completion preserves an +ordinary body failure only when the returned nonzero status exactly matches the +recorded ordinary status. Independent Resource errors and cleanup failures retain +their existing priority. The private test API copies each selected diagnostic +channel directly; production failure-copy behavior is unchanged. + +The original all-target check, 46 focused compiler tests and 82 measurement-helper +controls passed. After the ordinary source-completion repair, the four compiler +libraries pass 1,322 tests and Runtime passes 366. Those unit checks precede the +private report-copy repair and two-file native oracle correction. Fresh normal +and private MSVC Debug/Release archive measurements retain the 58-export catalog, +static CRT and unchanged ABI1/wire3. All eighteen corrected Resource gates pass +846 Source-deleted executions and 1,038 entries; Startup80 also passes with exact +current source guards. Earlier Graph32 and selected normal regressions retain +their accepted scope before the isolated oracle correction, with no production +or selected-test behavior change. This supplies bounded local MSVC acceptance; +full workspace, inventory, GNU and installed-distribution acceptance remain +separate requirements. See +[primitive companion work](completed/native_codegen/native_resource_primitive_companions.md). + + +### Ordinary diagnostic propagation across Source scopes + +Classification follows the recorded ordinary or Resource failure, not whether +the failing expression is in a root or nested Source scope. The exact installed +scope, operation and current ordinary status remain authenticated. Matching +ordinary propagation does not add `NativeBodyFailure::Resource`; a wrong nonzero +status or genuine protocol error still does. Cleanup priority and immutable +completed attempts remain unchanged. The private test API copies the requested channel's +own bytes; production failure-copy selection is unchanged. + +The v10/v24 Source13 observations of an additional Resource refusal are retained +as historical behavior, not the current ordinary-only message contract. Their +receipt counts remain bound to those revisions. Correcting only the stale +message oracle supplies no execution credit; the affected exact native gates +still require fresh acceptance. See the +[ordinary terminal-error lifetime note](completed/native_codegen/native_resource_ordinary_error_reentry.md). + +### Original required regions for inline bodies and refinement predicates + +Required-value preparation follows explicit private region steps into retained +inline bodies. The ordinary executable walker continues to stop at callable +boundaries. Each step identifies the exact physical original inline node, keeps +its enclosing declaration and concrete generic instance, and interleaves scoped +selections in lexical order. These references are admitted only for explicit +required evaluation; they cannot become runtime Resource entry authority. + +A refinement required region names the exact retained alias item, checked +declaration definition and physical constraint expression. The existing checker +still proves each explicit subexpression closed and pure. The original attempt +key and read-only required proof include this region rather than selecting it by +source spelling or nominal type alone. + +HIR records the canonical extraction association from an original inline node +to its extracted function and enclosing original owner. Required materialization +joins that private record, the retained checked program, exact original HIR +header and expression, and complete generic and scoped context. Refinement +materialization likewise joins the original checked predicate declaration and +HIR function. Both routes retain original/current archive authentication and the +existing value matcher. Prepared payloads and format checks provide no native +execution credit; focused forgery controls and exact-head CI must pass first. + +### Actor-owned required-value provenance + +Closed pure explicit `comptime` remains valid in actor initializers and receive +bodies. The required worker retains original actor owner/handler ordinals and +checked handler scope facts as metadata; runtime ActorHandler Resource entry +remains forbidden by its existing purpose gate. Frontend cache proofs do not +authorize native body edits or renew actor snapshots. The separate checked HIR +required-bake receipt described below joins exact actor owners and renews full +snapshots. A separate actual-sealer receipt authenticates exact token edits and +renewed execution tables. The original-Lowerer initial actor path below may +authenticate its bounded snapshot delta and source epoch; later unrepresented +actor-changing compositions still require their own path. A plan moved into a +generated adapter need not change the handler snapshot. The frontend evidence remains recorded in +[the actor required-value note](completed/native_codegen/native_actor_required_values.md). + +Retained ExplicitComptime and NamespaceConstant workers also validate lazy +closure environments before creating a function value. The complete original +parameters, return annotation and body must match the same checked program. +External Variable/Param references must exist in the actual permitted lexical +captures; own formals and body locals remain deferred. Exact namespace value +declarations, named functions and types retain their registry roles, but a +same-name constant cannot satisfy an actor field or handler parameter. Nested +lazy bodies keep the same closedness requirement. This enforces the existing +closed-expression rule without granting runtime actor entry, Resource custody, +HIR baking or native snapshot renewal. + +### Checked actor required-value bake transitions + +Ordinary required-value baking retains an ephemeral receipt of its actual fully +validated input/output, successful cache identity and type arena. Actor handler +and constructor view snapshots advance only from that receipt while preserving +original checker receipts, roles and projections. Peer execution tables capture +the renewed table at the same private transaction boundary. The sole actual +callable sealer retains complete before/output Programs, checked session, borrowed +type arena, source epoch and exact token-slot edits. The already-materialized +baked view authenticates its current table and private cache identity before +renewing from that retained output, preserving mutable-sum replay and validating +full actor snapshots unchanged. MIR retains complete archive equality. This +already-materialized baked renewal does not admit actor-body-changing deltas. +The separate original-Lowerer initial actor path below authenticates only its +represented delta; later source-lineage, actor-owned Graph and Resource-hook- +bearing obligations remain. +Ordinary native output does not establish physical lifetime or same-context +reentry; see the [checked actor bake note](completed/native_codegen/native_actor_required_bake.md). + +### Initial actor callable-sealing source epochs + +The original Lowerer may renew complete actor snapshots from the sole actual +CheckedCallableSealing output, retaining original receipt/role/view metadata. +A private finite epoch stores raw predecessor sources and the validated final +table before attaching its own edge. Source comparison restores only existing +protected stages to that exact checkpoint and retains full actor metadata. +The same actual token receipt remains authoritative; no duplicate token producer +is introduced. Actual Weak expiry controls cover the finite compiler graph, not +native physical actor/callback lifetime or same-context reentry. A fresh original +construction may also freeze unchanged temporary canonical producers through the +separate exact transition below. Unrepresented later actor/checkpoint/Graph/Resource +compositions remain pending; see the +[initial actor sealing note](completed/native_codegen/native_actor_callable_sealing.md). + +### Initial actor and retained temporary-owner finalization + +The consuming Lowerer owns a non-Clone mint-time inventory of exact temporary +proof Arcs, bound to its original module/check/type arena. It closes registration +before canonical passes and never publishes a pending registry or Program. The +ordinary no-actor/no-delta path retains its existing one-time late finalizer. + +For an actual initial actor callable delta, the sole actual sealer receipt may +authenticate a separate open-to-closed temporary-cell transition. Every minted +marker must match the complete retained input/output inventory and original +owner/alias facts. Candidates come only from the actual receipt's BEFORE owner +expressions, after clearing only their own outer marker; complete canonical +operands must equal the token output. A hidden temporary argument proof visitor +joins each predecessor to an exact earlier declaration in the same body. Repeated +uses remain valid; absent, foreign, self or forward dependencies refuse. Bound +callable payloads cannot import a sealed or Graph epoch. + +Preparation writes no cells. An opaque checkpoint derived from the retained +actor stage and the actual initial sum producer authenticates the complete final +peer table. All dependency, open-cell and attachment checks precede the first +write. The producer consumes the fresh registry, fills OnceLocks once with its +retained BEFORE values, then captures the existing pre-attachment actor epoch. +Only its opaque completion may skip the late setter. Arc-based Program equality +alone is not rollback or proof of a cell-state transition. + +The unchanged checker-valid Packet/list composition passes the new transition +controls and the broader 2,553-test local regression checkpoint. Its fresh +compile-only gate is accepted. One unchanged Source emits two objects and passes +two Source-deleted Debug/Release native processes, each with observed:5 plus LF, +empty stderr and exit zero. These bounded output and cleanup observations do not +measure native physical actor ownership or same-context reentry. A canonical producer +changed by sealing still needs a separate immutable current-canonical proof and +HIR/MIR consumer design. No marker removal, current-table recapture, checker +restriction, arbitrary historical ancestry or runtime authority is introduced. +The 35 focused HIR controls are included in that broader total; no native +execution credit is inferred from compiler tests. Earlier Resource observations +retain their committed checkpoint scope. See the +[temporary finalization note](completed/native_codegen/native_actor_temporary_owner_finalization.md). + +### Private Resource observation protocol 2 + +The recorder under `cfg(all(test, jett_resource_native_test_archive))` adds +private observation only. Production Runtime ABI 1, Resource layout wire 3, the 58 +production and 8 private C exports, NativeResourceCounts/ArchiveInfo layouts of +72/32 bytes and +receipt/normal-launcher manifest version 1 remain unchanged. The private +ArchiveInfo.test_protocol and JTRRP002 report use version 2; host controls use +JTRHC002. These versions select separate contracts. + +Five cumulative counters retain hook ingress, its acquire subset, factory +prepare, factory commit and actual provider dispatch independently. Successful +Property Set/Clear and Resource-ABI calls entered with installed Property +context retain actual history and supported installed int64 bits. Calls without +that context, ordinary ABI calls and other installed value forms remain +unmeasured. Strict decoding preserves optional values and known sticky +incompleteness; Source17 acceptance requires complete history and no incomplete +flags. Observation failure preserves Source error selection. + +The implementation across 16 Rust paths passes 407 focused local checks. Six +fresh MSVC Debug/Release archives have been measured and the fresh compiler gate +is accepted. Archive measurement and compile-only acceptance grant zero Source +executions. The matched native ingress control gate passes 32 categorized +processes; Source17 passes eight processes with measured complete private +history, installed int64 bits and five independent zero acquisition counters. +Normal-launcher telemetry remains unmeasured. Per-kind ordinary storage, private artifact retainers, normal +custody/reentry and physical lifetime remain open; see the +[private observation checkpoint](completed/native_codegen/native_resource_private_observations.md). diff --git a/docs/completed/breakpoint_call_stack_binding_scope.md b/docs/completed/breakpoint_call_stack_binding_scope.md new file mode 100644 index 00000000..0ca072a4 --- /dev/null +++ b/docs/completed/breakpoint_call_stack_binding_scope.md @@ -0,0 +1,32 @@ +# Breakpoint bindings across function calls + +Decision: the one-line debug snapshot contains only bindings visible in the +current lexical function frame, including its parameters, captured bindings, +and active nested scopes. Source-level shadowing remains prohibited. Caller locals +belong to separate frames and are not flattened into this snapshot. The +separate interactive debugger protocol may expose explicitly identified stack +frames; that does not change the one-line snapshot's lexical scope. + +Implementation must use the same lexical scope boundary as ordinary name +lookup in the interpreter and the checked HIR binding snapshot in native code. +Differential tests cover direct and indirect nested calls, nested-scope exit, +same-named bindings in separate functions, returning to a caller, and captured +closure bindings. + +## Original gap + +The interpreter's one-line `breakpoint` snapshot currently walks every active +scope in `Interpreter::hit_breakpoint`. At a breakpoint in a callee, this can +include caller locals that the callee cannot name in Jett source. A native +breakpoint uses the checked HIR binding snapshot for its current function, so +it reports only the callee's visible bindings. The difference is reproducible +by calling a function containing `breakpoint true` after declaring a local in +`main`. + +The original language and protocol question was whether a breakpoint inspects only +the current lexical frame, or exposes a call-stack view with separately named +frames. Flattening all frames into one binding map loses shadowed caller values +and gives caller locals the appearance of being in scope in the callee. The +decision above preserves the checked HIR snapshot and brings interpreter +snapshots into agreement. `tests/native/breakpoint_lexical_frames.jett` pins +direct and indirect calls, captures, nested scopes, and return to the caller. diff --git a/docs/completed/breakpoint_consumed_bindings.md b/docs/completed/breakpoint_consumed_bindings.md new file mode 100644 index 00000000..1c64b192 --- /dev/null +++ b/docs/completed/breakpoint_consumed_bindings.md @@ -0,0 +1,22 @@ +# Consumed bindings in breakpoint snapshots + +Decision: omit bindings that have already been consumed by a move. This applies +to the current function's visible bindings, including captures, and avoids +reading values whose ownership has transferred elsewhere. A mutable binding +becomes observable again after assignment initializes it. Pending values are +still observable while owned; running or joining a task follows ordinary +consumption rules. Public trace remains a checked read of its explicitly named +binding. + +The compiler records binding availability at each breakpoint and supplies the +same exclusions to HIR and the interpreter, including comptime and test runners. +Transfers at binding and destructuring sites affect observation without widening +source ownership permissions. Branch joins omit bindings unavailable on any +continuing path. Mutable reassignment restores observation. Copyable aliases +use checked expression types, and actor handlers establish a separate lexical +frame for state, capability, message, and local bindings. + +`tests/native/breakpoint_consumed_bindings.jett` compares linked native and +interpreter output for transfers, calls, branch joins, loops, handled failures, +reassignment, copyable aliases, and actor handlers. Existing actor observation +expectations omit a handle already transferred into a list. diff --git a/docs/completed/direct_collection_equality.md b/docs/completed/direct_collection_equality.md new file mode 100644 index 00000000..a926bca8 --- /dev/null +++ b/docs/completed/direct_collection_equality.md @@ -0,0 +1,19 @@ +# Direct equality of collections and sums + +Settled on 2026-09-30: reject direct `==` and `!=` on bytes, lists, maps, sets, +optionals, and results at compile time. The former checker accepted these +expressions, while the interpreter failed at runtime and native codegen rejected +them internally. These unsupported comparisons now report E0376 before comptime +evaluation or backend lowering. + +Classification follows transparent aliases, refinement ancestry, and secret +wrappers. The same rule applies to concrete generic instantiations and explicit +comptime expressions, including absent sums and empty collections. It does not +introduce structural equality, change collection-key eligibility, or change the +separate existing collection comparison path within enum payloads. Struct, +actor, and erased-interface comparison contracts remain separate. + +Four compile-fail fixtures cover both operators, all six families, aliases, +inherited refinements, secrets, concrete generics, and explicit comptime values. +The native publication regression checks every family and operator in debug and +release, requires the frontend diagnostic, and preserves existing artifacts. diff --git a/docs/completed/enum_payload_struct_equality.md b/docs/completed/enum_payload_struct_equality.md new file mode 100644 index 00000000..9f9673b9 --- /dev/null +++ b/docs/completed/enum_payload_struct_equality.md @@ -0,0 +1,63 @@ +# Explicit struct equality inside enum payloads + +Status: implemented on 2026-09-30 following the user's explicit language +decision. Frontend, interpreter/comptime, and linked native execution enforce +this contract. This closes this equality gate, not the complete native audit. + +## Contract + +Enum comparison preserves variant/state/branch, length, collection order, pending +depth, IEEE floating-point behavior, and left-to-right short-circuiting. Nested +user structs require exact explicit two-view `Equatable.equals` methods. The +frontend walks the type graph with cycle detection and reports E0358 for a +missing implementation, including through containers, machines, and refinements. +Traversal stops at each custom struct method: ignored fields neither gain an +equality requirement nor receive structural comparison. Inequality negates the +complete result. Direct collection/sum equality still reports E0376; ordinary +bitfields retain their existing structural path. Actor and erased-interface +equality decisions remain separate. + +## Compiler and runtime handoff + +Checked owner types and exact body identities become typed HIR/MIR function +targets. Backend validation requires two exact struct view parameters, bool +return, and no captures. Reachability retains implicit dependency/stdlib methods. +Traversal order determines descriptors and generated branches, preserving +deterministic object emission. The graphics callback effect audit follows these +implicit methods through nested payloads and concrete generic comparisons. + +The interpreter uses an explicit work stack and source method dispatch. Native +code owns resumable comparison cursors through additive `EnumEqualityStart`, +`EnumEqualityNext`, `EnumEqualityArgument`, and `EnumEqualityAnswer` leaves. +The `JQ` version-1 descriptor maps opaque record nodes to generated call branches. +Leaves release the runtime context lock before generated code invokes methods. +Nested comparisons use independent cursors. Matching pending structs receive +temporary zero-depth view shells borrowing their original fields; ignored fields +are neither cloned nor traversed. Success, inequality, allocation failure, and +method failure retire cursors and shells. Pending method results fail with +`Equatable.equals must return bool`. + +## Verification + +- Missing methods fail for both operators, concrete generic bodies, and closed + required comptime comparisons. Graphics rejects effects hidden in nested + enum equality methods. +- `tests/native/enum_struct_equality.jett` covers custom identity distinct from + field equality, ignored structs/functions, concrete generic owners, namespace + aliases, nested method comparisons, lists, maps, both result branches, + optionals, machines, refinements, pending depth, and 160-level recursive enums. + Required comptime evaluation follows the same explicit method contract. +- Linked debug/release artifacts execute after source removal. Both operators + preserve ordered operand evaluation, false-prefix and length/tag short-circuiting, + method failures, and pending bool result failures. Terminal status/output + agrees with the interpreter and context cleanup succeeds. +- Runtime tests cover independent nested cursors, opaque/malformed ignored + fields, partial view allocation rollback, pending result cleanup, and 4,096 + recursive levels. Existing cycle, IEEE, shared-child, and malformed suffix + tests remain green. +- Object tests retain implicit stdlib method reachability, deterministic bytes, + checked target signatures, and no structural fallback when metadata is missing. + Compiler phase suites and all 589 frontend fixtures pass. + +Full native inventory and platform CI remain separate acceptance gates in +`docs/completed/native_codegen/native_acceptance_audit.md`. diff --git a/docs/completed/native_codegen/README.md b/docs/completed/native_codegen/README.md new file mode 100644 index 00000000..7d465976 --- /dev/null +++ b/docs/completed/native_codegen/README.md @@ -0,0 +1,35 @@ +# Native codegen campaign handoff + +Jett parses and resolves Source, checks types and ownership, evaluates explicit +comptime expressions, lowers checked values through HIR and MIR, and emits +native objects with Cranelift. The native linker combines those objects with a +target-matched runtime launcher bundle; execution uses the checked native ABI. + +Native parity means all five fixture gates in `tests/native_parity.json` pass +at one commit on Windows MSVC (`x86_64-pc-windows-msvc`): lowering, host object +emission, inventory execution, verify suites, and property suites. + +The verified landing SHA is `dd0d415238e29743780e6372a126dbda40f9845d`. +Its five gates passed (1, 1, 2, 1 and 1 tests respectively); the workspace passed +4,260 tests, failed 0 and ignored 26 in 2h 48m 56.51s. That run selected test +optimization level 1 through the environment. `Cargo.toml` now selects it with +`[profile.test]`, retaining the default debug assertions and overflow checks. + +The final closeout SHA and plain-command verification result are recorded in +[draft PR #281](https://github.com/vycdev/jett/pull/281). Use `git rev-parse HEAD` +to identify this checkout's revision. GitHub CI remains disabled; the PR remains +a draft for review. + +Rerun the five gates and workspace with no build environment overrides: + +```text +cargo test --locked -p jett_driver --test backend_lowering run_pass_backend_lowering_gaps_are_explicit_and_monotonic +cargo test --locked -p jett_driver --test native_parity_manifest native_parity_object_emit_obligations_emit_host_objects +cargo test --locked -p jett_driver --test native_conformance inventory_execution +cargo test --locked -p jett_driver --test native_conformance native_verify_suites_execute_for_all_run_pass_fixtures +cargo test --locked -p jett_driver --test native_conformance native_property_suites_execute_for_all_run_pass_fixtures +cargo test --locked --workspace --no-fail-fast +``` + +[Remaining native gaps and language-rule review](../../active/native_remaining_gaps.md) +are follow-up work. The notes in this folder preserve campaign history. diff --git a/docs/completed/native_codegen/actor_handler_field_updates.md b/docs/completed/native_codegen/actor_handler_field_updates.md new file mode 100644 index 00000000..afaae2b8 --- /dev/null +++ b/docs/completed/native_codegen/actor_handler_field_updates.md @@ -0,0 +1,85 @@ +# Checked actor handler field updates + +Status: implemented with bounded MSVC acceptance, 2026-10-15. This note records +the actor-context contract under [owned mutable field updates](owned_mutable_field_updates.md). +The [native audit](native_acceptance_audit.md#checked-actor-handler-field-updates-bounded-msvc-acceptance) +retains the exact execution matrix and its remaining limits. + +The existing actor rules already select the behavior: an actor owns its private +state, a message transfers its owned arguments, and a response transfers an owned +result. A mutable owned state binding may update a checked record field using +the same place, readiness, reservation and replacement rules as an ordinary +mutable local. An actor handle never exposes remote state as a writable place. + +## Checked identity and persistent bindings + +A handler is identified by the retained actor declaration, its canonical actor +type and its original handler ordinal. It has no invented function definition. +The original Send or Ask expression, exact argument acquisition and order, +response type, and caller context must join that handler. Handler body facts are +selected under that handler's own context, including nested checked scopes. + +Capabilities and state occupy the actor's captured scope. Message parameters and +handler locals occupy a child scope, matching the resolver. The existing +no-shadowing rule rejects a message parameter that reuses a state +name. Physical state publication still joins exact captured slots instead of +reconstructing ownership from an innermost name lookup. State, capability and +message roles retain their exact +resolved declaration, type, access mode and mutability. + +An accepting ownership traversal must retain diagnostics and check persistent +capture availability at each real normal exit, after acquiring the response. +The previous breakpoint observer is not an ownership admission proof. An early +Respond inside a field RHS abandons the field commit and preserves the same +persistent state obligation. Copy values use their normal copy acquisition; +move-only replies require ownership, and borrowed replies cannot escape. + +A persistent state view, if established by an admitted initializer, must retain +its actual checked backing owner and loan dependency. It cannot be installed as +owned data or as an unbound view. Initializer order, scope and provenance must +be preserved by both backends; this note introduces no broader view admission. + +## Terminal and unresolved response exits + +A handler whose original declaration has no `responds` may terminate with a +checked nothing-valued Return. Its canonical response type must be nothing; the +operand must be absent or have the exact checked nothing type and cannot escape +a view. Actual operand effects run before persistent capture validation. A Return +inside a field RHS abandons that field commit, and a successful handler exit +publishes the retained captures. Source16 exercises a nothing-valued helper after +state updates, an early Return inside a field Handle, and independent instances. + +The design spells replies with Respond. The current checker also observes Return +and fallthrough in responding handlers, including `responds nothing`, without a +checked response protocol. This is an existing gap, not authorization to give Return a new meaning or to +add a blanket Source ban. Keep these exits explicit and withhold an actor +execution receipt when its response/persistent-state contract is unproven. +Their complete language and backend handling remains an open obligation. + +## Publication and verification + +Native publication must establish an occupied, live actor field before replacing +it. The separate ordinary `ActorReplaceOccupied` no-cleanup swap transfers the +incoming field and returns the displaced value. The caller clears the transferred +staging slot before separately retiring the displaced owner. A failure before the swap leaves ownership unchanged; a +failure during displaced cleanup must not restore a second incoming owner. +The ordinary struct replacement leaf remains unable to mutate an actor base. + +The unchanged original24 and actor Source03 execute alongside repeated owning +updates, independent instances, ordinary helpers, persistent direct and projected +views, terminal Respond, owning replies and exact nothing returns from +nonresponding handlers. Separate checker/HIR/MIR/reference controls cover invalid +identity, role and context joins, persistent capture availability and loans. + +Eleven native tests pass with twenty-four Source-deleted executions and forty-eight +publication refusals. Refusals and runtime symbol measurements supply no execution +credit. Main and suite controls pair Source Debug/default with Runtime Debug and +Source Release/optimized with Runtime Release. The original nested receiver keeps +Debug Source policy against both runtime profiles. This is not the full profile +cross-product. Existing original initializer/capture Graph regressions also pass +with Source removed, using the Debug runtime and both Source options; they do not +grant live Resource custody or general handler Resource authority. + +Resource custody, wider frozen-graph and generic/reflected compositions, real +asynchronous scheduling and full workspace/platform acceptance remain separate +obligations. The unresolved response exits above remain open. diff --git a/docs/completed/native_codegen/actor_identity_equality.md b/docs/completed/native_codegen/actor_identity_equality.md new file mode 100644 index 00000000..443a1db0 --- /dev/null +++ b/docs/completed/native_codegen/actor_identity_equality.md @@ -0,0 +1,38 @@ +# Native actor identity equality + +The reference evaluator and native backend implement live actor identity +comparison. `==` and `!=` require the same exact checked nominal actor type. +Cloning preserves instance identity, while separate spawns remain unequal even +when their state matches. `!=` negates the identity comparison. + +Both runtime IDs must belong to the current actor registry and have complete, +live backing records. Equality compares identity without reading actor state. +Enum payload traversal applies this rule through supported nested containers. +An explicit struct `Equatable.equals` method still controls struct comparison. + +Each pending handle records its own depth and refers to the base actor instance. +`run` creates an alias; `join` reaches the same ready instance. Wrapping one +handle leaves ready aliases unchanged. Nested enum comparison checks identity +and pending depth. Direct comparison of pending actors retains the existing +unsupported binary-operation error. + +## Verification + +Local MSVC static-CRT verification on 2026-10-12 passes the complete runtime +(301), interpreter (564), and code generator (118) unit suites, plus three actor +object and nominal-type rejection tests. The actor native suite runs nine +executables per runtime profile after deleting their Source: seven original +and supplemental debug-policy cases, and a production-output case with both +Jett optimization settings. Debug and Release runtime profiles both pass. + +The production case observes direct identity, nested list/optional enum +payloads, pending aliases and depths, state shared after joining, and explicit +struct equality that ignores its actor field. Runtime tests additionally cover +foreign and stale IDs, incomplete backing records, pending allocation failure, +and single state retirement. + +The six saved fixture Sources remain unchanged. These supplemental `tests/native` +controls leave the fixed `tests/native_parity.json` inventory at 207 fixtures +and 182 object obligations. Full predecessor, GNU, and installed-distribution +acceptance remain part of the native codegen completion work. Escaping comptime +actor graph import is tracked separately. diff --git a/docs/completed/native_codegen/hidden_secret_debug_print.md b/docs/completed/native_codegen/hidden_secret_debug_print.md new file mode 100644 index 00000000..909a8781 --- /dev/null +++ b/docs/completed/native_codegen/hidden_secret_debug_print.md @@ -0,0 +1,94 @@ +# Selected accepted debug print redaction + +Status: implemented and verified in the integrated compiler. The native schema +and source admission policy are unchanged. + +The interpreter uses typed recursive redaction for values already accepted by non-release +`print` and `println`. The alternative of blanket static rejection of interfaces +and construction builders would remove accepted public programs. Accepted debug +printing instead follows the existing trace/reflection token, `[redacted]`, for +secret-qualified subtrees. Renderers must inspect the qualifier before reading +the payload or any pending wrappers inside it. A public string equal to the +marker remains public text. No user getter, Displayable method or callback runs +while rendering. + +The original checked argument type belongs to the caller. Capture it before any +argument evaluation, nested generic/scoped/reflected execution or ownership +transfer, then retain its association with the actual value. Interface values +use retained concrete nominal/generic identity; construction builders use each +stored field type. Missing composite/field metadata must produce a conservative +redacted subtree, never a raw payload fallback. Do not invent a concrete type +from matching fields or attach a destination type to erase the caller's secret +qualifier. Opaque callback and actor rendering preserves existing labels without +reading captures or actor state. + +Arguments are evaluated once in source order before one complete print event. +`view` remains nonowning, ordinary arguments retain their existing consumption, +argument separators remain spaces, print adds no terminator and println adds +one newline. Diagnostic stderr/debug event transport remains unchanged. + +Direct secret and refinement-wrapped secret Source restrictions, Resource-root +restrictions and Source release E0362 remain unchanged. Debug/Release runtime +archive profiles do not alter Source release policy. The native typed schema and +redaction guards remain independent safeguards and are not weakened to match +historical reference disclosure. + +The Print/Println repair covers accepted debug observations. The accompanying +direct-field diagnostic successor applies existing secret non-disclosure to the +demonstrated pending secret receiver failure; both runtime profiles pass. +Pending-sum extraction and pipeline failure paths remain separate parity work in +docs/open_design/debug_print_hidden_secrets.md. No secret declassification or +additional source admission is introduced by either repair. + +Original probe 07_hidden_secret_debug_print and control +15_public_erased_debug_print are preserved byte-for-byte. The historical +sentinel-secret disclosure is a defect observation, never the future oracle. +Expected selected debug text is app.Hidden(token: [redacted]) followed by one +newline; the public control retains app.Public(token: sentinel-public) followed +by one newline. Reference events and Source-deleted linked diagnostic output match these exact +bytes in both runtime profiles. +## Integrated verification + +All 645 Comptime tests pass, including nine focused renderer controls. The +five native print controls pass: nine source programs produce 18 Source-deleted +executions across the normal Debug and Release runtime archives. The cases +cover hidden and public interface fields, generic owners, nested sums, builders, +pending collections, argument failure, owned/view routes and exact invocation +order. Source release and direct-secret rejection controls remain enforced. + +All seven existing secret-owner controls pass in both profiles, including the +original pending secret receiver failure. Eight adjacent native debug and +pending-aggregate controls also pass. Exact stdout, stderr, failure status and +available cleanup checks remain asserted by the tests. No runtime ABI or native +renderer change is part of this reference repair. + +The authentic reflected-callable test uses checked named and closure adapters, +verifies printing emits no callback or refinement-predicate events, then invokes +them explicitly. Its closure captures a copyable int64: Jett rejects move-only +secret captures. A separate host-value test covers defensive opaque rendering +of a synthetic secret capture without claiming source admission. + +Logs: `target/native/secret-observation-comptime-full-v2.log`, +`secret-observation-native-both-v2.log`, `secret-observation-owner-views-v1.log` +and `secret-observation-adjacent-v1.log`. These focused results do not replace +full-workspace or supported-host release acceptance. + +The existing raw-host public view test now supplies its known list[string] +contract. Its exact public text oracle is unchanged. Source argument facts are +captured from actual checked caller metadata before evaluation; no production +missing fact is synthesized to match that host-only setup. A separate control +requires conservative redaction when field/container metadata is unavailable. +Host-only renderer controls exercise defensive missing-metadata and secret-set +carriers; they do not claim those host values are accepted Jett Source. The +native renderer's malformed-schema/handle guards remain errors, not newly +redacted success. Exact Source output tests must prove checked facts exist for +public subtrees; conservative reference fallback alone is no parity credit. + +The new pipeline/view fixture uses a named `emit(view value: Named)` wrapper +whose body calls `println(view value)`, followed by an owned `println(borrowed)`. +It preserves the two selected exact events. Both direct builtin pipeline +spellings, `borrowed into view println` and `borrowed into view println()`, +failed actual checking with existing E0313. Their original bytes and failure +logs are retained in the execution packet as before-policy observations. Direct +builtin pipeline admission is a separate feature-contract audit follow-up for +the full native goal; this change neither broadens it nor claims it covered. diff --git a/docs/completed/native_codegen/native_acceptance_audit.md b/docs/completed/native_codegen/native_acceptance_audit.md new file mode 100644 index 00000000..5f03e7e9 --- /dev/null +++ b/docs/completed/native_codegen/native_acceptance_audit.md @@ -0,0 +1,4165 @@ +# Native parity acceptance audit + +Status: consolidated Windows MSVC (`x86_64-pc-windows-msvc`) landing. Native +parity means the five fixture gates in `tests/native_parity.json`; the final +`codex/native-codegen` SHA and six-command result are recorded in its draft PR. +Whole-language parity and percentage tracking below are retired. Linux, +installed-package, relocation and source-deleted gates are outside this landing. + +## Inventory gates + +`tests/native_parity.json` contains 207 fixtures: 182 run-pass files and 25 +runtime-contract files. The inventory validator compares it with the filesystem, +rejects duplicate paths and malformed rows, and checks entry-point obligations. +The verify/property gates discover test bodies from parsed source independently +of each row's lowering marker. + +| Requirement | Denominator | Executable evidence | +| --- | ---: | --- | +| Checked HIR and MIR for every run-pass fixture | 182 | `backend_lowering::run_pass_backend_lowering_gaps_are_explicit_and_monotonic` lowers every discovered file and rejects any failure. | +| Nonempty native objects, including test-only roots | 182 | `native_parity_manifest::native_parity_object_emit_obligations_emit_host_objects` uses native test lowering and emits every inventory obligation. | +| Entry-point execution, exact output, and cleanup | 30 | `native_conformance::inventory_execution::native_inventory_main_execution_matches_interpreter` executes all manifest entries: 29 successful outcomes and one expected Graphics callback failure. | +| Runtime contract execution, exact output, and cleanup | 25 | `native_conformance::inventory_execution::native_inventory_runtime_contracts_match_interpreter` executes 17 wrapping-success cases and eight expected failures. | +| All checked verify bodies | 155 fixtures | `native_conformance::native_verify_suites_execute_for_all_run_pass_fixtures` discovers and executes every fixture with verify blocks. | +| Deterministic property trials | 3 fixtures | `native_conformance::native_property_suites_execute_for_all_run_pass_fixtures` executes the generated cases in native suites; the HIR suite builder requires 100 trials per property. | + +The two inventory execution gates share provider-input decoding with the +standalone `native_parity` report tool. Clock, Random, Environment, and Graphics +inputs come from the manifest. Failure comparison requires exit 71 and exact +stderr; a cleanup failure overriding entry failure with exit 72 cannot pass. +The native launcher has a separate regression for that precedence. + +The larger `tests/native/` corpus supplements these denominators. It is not a +replacement inventory or proof that every combination of language features has +been audited. Likewise, a fixture's reference in Rust is not proof of execution. + +## Structural and distribution gates + +| Requirement | Evidence that must pass at the release revision | +| --- | --- | +| Canonical identities and closed compiler handoff | Driver origin/entry tests, HIR backend type validation, generic and reflected-body regressions, and codegen tests for symbols based on structural types rather than session-local IDs. | +| Explicit control flow, evaluation order, ownership, and failure cleanup | MIR validation and ownership suites; linked handler, projected-view, temporary-owner, callback, actor, refinement, and terminal-failure regressions; native allocation-failure rollback tests. | +| Versioned C ABI and exact resource retirement | Runtime ABI version/layout tests, mismatch rejection, cleanup probes and panic containment; native launcher tests for successful and failing entry cleanup. | +| Deterministic objects, target selection, linking, and atomic publication | Codegen deterministic-object and unsupported-target tests; native driver linker/runtime validation and output-preservation tests. | +| Required comptime evaluation stays at compile time | Explicit-comptime and namespace-constant evaluation/materialization tests, per-instantiation context regressions, primitive value-shape rejection, and rejection of unbaked markers by codegen. No runtime initializer or interpreter fallback is an acceptance substitute. | +| Supported-host distribution | Local Linux GNU and Windows MSVC workspace/build/package gates, followed by clean installed checks with no checkout. Both runtime profiles must work after relocation and source removal. Historical workflow results retain their own revision and scope; GitHub CI is disabled at the user's request. | +| Complete current workspace | `cargo test --locked --workspace --no-fail-fast` or equivalent test and doc-test invocations against a coherent revision. Rebuilding dependency artifacts during a run can invalidate later rustdoc inputs; such failures require a fresh doc-test run. | +| Stable documentation matches the implementation | Review `docs/design.md`, `docs/architecture.md`, the parity plan, and the notes below against the final implementation and verified gate results. | + +The tested CI toolchain is Rust 1.97.1. Cross-compilation, future providers, and +asynchronous scheduling must not be inferred from host packaging or the current +sequential interpreter/native task model. Their implementation status remains +separate from parity for existing accepted behavior. + +At the user's request to save GitHub Actions minutes, the sole native workflow +(`native.yml`, workflow ID `369899180`) is `disabled_manually`. At the recorded +check there were no queued or running jobs. GitHub CI is not an acceptance gate +for the current work and must remain disabled. No current-revision workflow +acceptance is claimed; equivalent local workspace, +supported-host, installed-distribution and complete-corpus coverage still apply +to the full goal. Earlier workflow links and CI requirements below describe +historical receipts and their original scope, not a request to re-enable CI. +The primary checkout's local failure audit, +`native-codegen-recovery-2026-10-09/ci-disabled-failure-audit-v1/audit.json`, records +existing PR-merge failures; their local repair and replacement acceptance remain +pending. + +## Selected semantics and remaining acceptance + +The resolved decisions below distinguish selected contracts from remaining work. Recorded +implementation evidence retains its revision and scope; remaining implementation, +coherent-workspace and supported-host gates still prevent a full parity claim. + +- [Comptime actor graph materialization](native_comptime_actor_graph.md) implements the selected frozen-graph contract. Current bounded MSVC acceptance passes 32 Source-deleted executions; wider admitted compositions and coherent-revision release gates remain required. Actor closure capture remains rejected with E0402. +- [Erased interface equality](../../open_design/interface_value_equality.md): **selected and implemented.** Interface-typed `==` and `!=` report E0376 before evaluation or lowering; concrete equality and explicit domain comparisons remain available. Recorded frontend, publication-refusal and Source-deleted positive-comparison controls cover this contract. Final-revision workspace and platform acceptance remain separate. +- [Secrets hidden in debug print values](hidden_secret_debug_print.md): **selected and implemented.** Accepted non-release Print/Println values use recursive typed redaction for secrets hidden by interfaces, aggregates and builders. Direct-secret and Source release restrictions remain unchanged. The recorded nine-program/eighteen-execution gate is bounded acceptance; pending-sum and pipeline terminal diagnostics have separate bounded acceptance below. +- [Owned mutable field updates](owned_mutable_field_updates.md): **selected contract.** Checked updates through live mutable owners retain the selected ownership, readiness, replacement and Resource-custody rules. Native implementation and execution acceptance are tracked separately from policy selection; frontend publication refusals do not supply execution evidence. +- [List summation types](../../open_design/list_sum_type_contract.md): **selected and implemented.** `list.sum` and trusted `list.__sum` admit exactly the ten numeric primitives and their transparent aliases. Refinements, secret wrappers and nonnumeric elements report E0300 during checking, including empty and explicit-comptime calls. Checked width, typed zero, borrowing and pending behavior remain unchanged. Recorded focused controls retain their scope; final-revision acceptance is separate. +- [Reflected field request compatibility](../../open_design/reflected_field_request_compatibility.md): + current checks admit some refinement/base requests with matching carriers and + secrecy. Preserving the actual owner on an admitted interface read does not + select the broader requested-type contract or authorize arbitrary nominal casts. + +[Exact concrete-owner preflight](native_erased_same_owner_arguments.md) now +implements the selected runtime contract in reference direct/named-callback +calls and generated native interface dispatch. It checks receiver-selected +checked identities before argument normalization or body entry, preserving +nominal types, generic arguments, primitive widths, ordinary view/owned modes +and earlier argument failures. Native validation observes existing values +without cloning or transferring ownership; existing unboxing guards remain. + +The restored integration's normal Debug and Release runtimes each pass thirteen +original Source-deleted cases and four production-output executions spanning +matching/mismatched owners and both Jett optimization settings: thirty-four +executions overall. All 2,331 workspace library tests pass, including six runtime +and eleven focused reference controls; twenty-seven broader interface tests pass. +Full coherent native conformance, GNU, installed and exact-revision CI acceptance +remain pending. Native Source coverage for refinement and pending combinations remains +incomplete. Checker substitution remains at whole interface parameters; +`list[Pairable]` retains its declared erased child contract without recursive +Self substitution. Erased-interface equality is separately rejected by the +frontend under the selected explicit-comparison contract. These bounded gates +leave the rough 87% estimate and fixed inventory +denominators unchanged. + +[Actor identity equality](actor_identity_equality.md) implements the selected +live-instance contract in the reference evaluator and native backend. Direct +comparisons require the same exact checked nominal actor type; recursive enum +payload comparison checks identity and per-handle pending depth without reading +state. The predecessor MSVC static-CRT checkpoint on 2026-10-12 passed nine +Source-deleted executables per Debug/Release runtime profile, including +production output with both Jett optimization settings. That checkpoint did not +establish escaping comptime actor graph import. Current bounded graph import +acceptance is recorded below and in the active graph note; full predecessor, +GNU, installed-distribution and coherent-revision acceptance remain open. This bounded acceptance does not change the rough 87% planning estimate, +the fixed inventory denominators or the whole-language completion requirement. + +The [inferred uninhabited JSON parser repair](native_json_uninhabited_parse_slots.md) +is accepted at predecessor `08f9f7e7`: its 19 Source tests, frozen full workspace +and all four supported-host jobs passed. Each later revision still requires its +own coherent release gates. + +The [interface audit](native_interface_values.md) also retains the remaining +facade, refinement-composition, and comptime combinations that need scrutiny. +Passing its growing regression suite does not remove those audit obligations. + +## Selected boundaries and unimplemented extensions + +Namespace constants have a selected current domain in `docs/design.md`: +implicitly copyable primitive values and transparent aliases are baked once by +the shared required evaluator; aggregates, nominal refinements, resources, +actors, function values and erased payloads report E9001. This is a frontend +boundary for both execution paths, not missing positive native support. +`native_global_constants_match_interpreter_without_source_in_both_profiles` +executes the admitted values and suites after Source removal, while driver +constant tests pin aggregate and hidden-pending rejection before publication. +Revalidate those gates at the final revision. The +[broader constant ownership note](../../open_design/global_constant_execution.md) +records a possible extension and does not authorize runtime globals. + +Current task/actor execution is sequential: eager `run`, one-level `join`, +no-op `cancel`, and synchronous actor dispatch. Existing run/join/cancel, +actor/message ownership and scalar actor-comptime Source gates remain parity +obligations. `docs/architecture.md` explicitly marks asynchronous scheduling +and capability cancellation checkpoints unimplemented; the reference path also +rejects Resource task/message transport and has no corresponding production +provider APIs. Introducing those absent APIs or a scheduler is separate feature +work, not a prerequisite for parity with the existing interpreter. Implemented +portions of partial features, including occupied Resource custody, still require +the complete original-Source evidence specified by the accepted plan. + +The current private Resource checkpoint is 138 shared scenarios /276 native runs +in debug and release on MSVC, with Source absent at launch and zero obligations +before teardown. The nested-view packet passes all 276 independent reference runs +and all 276 native executions, including branch joins, scoped bodies, loops and +Break/Continue. The approximate feature estimate advances from 85% to 86%; it is +not measured code coverage or a changed inventory denominator. See the +[nested lifetime contract](native_resource_nested_sums.md). Archive provenance, +GNU private Source execution, and the final four supported-host workflow jobs +remain separate gates. + +The [ordinary borrowed-sum implementation](native_ordinary_borrowed_sum_codegen.md) +now passes 15 reference Sources and 30 Source-deleted native debug/release +executions, including later handled-argument failures and mutable copied +primitive/String destinations. The fresh runtime also re-passes all 138 Resource +scenarios /276 native executions. Full affected MIR/codegen/object/CFG, runtime +and driver-library checks pass; coherent full workspace and current platform +acceptance remain required. The current rough feature estimate is about 87%, +with the fixed inventory denominators unchanged and the full 100% objective open. + +[Direct collection and sum equality](../direct_collection_equality.md) +is settled: both operators now report E0376 before comptime evaluation or native +lowering, including wrapped and concretely instantiated types. Frontend fixture +coverage and native debug/release publication checks pin this contract. + +[User structs nested inside enum payloads](../enum_payload_struct_equality.md) +now invoke exact explicit `Equatable.equals` implementations. Recursive frontend +validation, interpreter/comptime dispatch, and typed native cursor calls are +implemented. Linked debug/release coverage includes generic owners, containers, +recursive values, pending depth, method failures, and source removal. Compiler +phase suites and all 589 frontend fixtures pass. This closes this equality gate; +full inventory and platform CI are still independent acceptance obligations. + +Normal native builds run frontend verify/property checks before emission; a +source-level failed property cannot be silently +bypassed to claim native failure coverage. +`native_builds_preserve_shrunk_property_diagnostics_and_existing_artifacts` +pins that public pre-emission contract: a property fails on its fourth generated +trial, shrinks both scalar and list inputs, and retains the exact E9000 message +and property-name span in ordinary program, verify-suite, and property-suite +builds. All three preserve an existing output artifact. The comparison also +checks the structured `test_file` result, including the trial count. +This verifies frontend diagnostic handoff; it does not establish native shrinking. +`native::property_tests::native_property_failure_identifies_the_trial_and_cleans_owned_values` +then changes a checked assertion only in test-owned MIR to exercise a failure +that the frontend did not encounter. Both debug and optimized linked suites +report the second property's name, trial 4, and original assertion text, with +successful owned-value cleanup. A failure injected after a successful suite +checks that the compiled calls cleared their context. Runtime ABI tests cover +context clearing, repeated/undersized failure copies, and caught predicate +errors. This supplies +case-specific diagnostics without skipping frontend validation or disclosing +argument values. + +The native driver now retains checked input and function identities for replay +and shares the interpreter's bounded shrink search. The property inventory gate +and standalone parity report both use this driver. The regression +`native_property_runner_shrinks_native_failures_from_the_checked_session` injects +a checked-HIR assertion failure after frontend validation, then verifies native +prefix search, isolated replay, and scalar/list shrinking in debug and optimized +modes. It matches the interpreter's counterexample after deleting the source; +original suite output stays separate from replay output. Additional regressions +reject timeouts and an initial MIR fault that checked-HIR replay cannot reproduce. +Candidate generation/order and backend-error propagation are covered by the +shared shrink-search tests. The integer-domain regression covers both signed +and unsigned narrow pool boundaries and checks metadata through nested shrink +candidates. `native_property_shrinking_preserves_narrow_boundaries_and_nested_inputs` +compares debug and optimized native shrinking of an `int8` minimum with the +interpreter, including generic record, list, and map inputs. It prevents an +out-of-range positive candidate from replacing the signed minimum. +Refinement identities now survive candidate generation too. The interpreter +checks retained tags recursively, and native replay emits the existing checked +predicate handlers before calling the property. The regression +`native_property_shrinking_checks_refinement_chains_and_nested_predicates` +compares debug and optimized counterexamples for a namespaced refinement chain, +a generic record with a refined field, and a refined nonempty list whose elements +are refined. It also rejects a candidate whose predicate raises a runtime error, +without replacing the original property failure or failing cleanup. The direct +`Positive` regression keeps its counterexample at one rather than zero, and unit +tests cover nested container and sum shapes. Further failing-input combinations +remain part of the semantic audit; successful inventory counts alone do not +prove every shrink candidate can be replayed. + +`native_property_shrinking_reports_the_executed_float32_input` pins binary32 +counterexample fidelity: a generated `3.14` input is stored and reported as its +actual rounded value, `3.140000104904175`. Debug and optimized native replay agree +with the interpreter after source removal. Pool and shrink tests also check +binary32 identity, negative zero, finite limits, and subnormal rounding; binary64 +values keep their original precision. + +`native_property_shrinking_preserves_generated_generic_interface_owners` checks +canonical instantiated identities in generated records, including a transparent +type argument alias and distinct implementations reached through interface lists. +Its failure shrinks from three records to two records with zero-valued fields; +losing the owner during field shrinking would instead introduce a dispatch error +and wrongly accept a singleton. Native debug and optimized replay must match the +interpreter's exact counterexample after source removal. + +The rejection-path audit found and addressed floating `modulo`, which was +accepted by the checker and interpreter but rejected by native binary validation. +`native_float_remainder_matches_interpreter_in_both_profiles` compares both +widths, signs, signed zero, NaN/infinity/zero-divisor cases, generic functions, +joined tasks, and explicit comptime results after source removal. Its pending +operand companion requires matching diagnostics and exit 71 with owned locals +on both operand sides and both profiles. The runtime leaf test also covers +subnormal values and verifies that NaN results do not set runtime failure. + +The aggregate debug-print gate runs public values from five debug fixture +families through `print`/`println` and compares complete native/interpreter +output after source removal. It covers empty never-typed containers, absent +sums, explicit views, pending values, concrete interface owners, and borrowed +arguments spanning handlers. A terminal argument failure must produce no partial +call output and exit 71 after cleanup. Runtime layout tests reject a fabricated +inhabitant of an uninhabited debug node. These tests do not resolve the separate +hidden-secret print policy above. +The deep-value gate compares trace, print, breakpoint, and erased-interface +observation of 160-level recursive values, including pending nodes and secret +leaves, after deleting source. Runtime tests format 4,096-level values and shared +children, reject alias and payload cycles (including dynamic interface layouts), +and preserve output-buffer contents on failed formatting. This removes the +formatter's former 128-level cutoff. A separate deep-enum equality gate compares +debug and release binaries after source removal, including 160-level equal and +unequal payloads, different chain lengths, pending wrappers, NaN, and signed zero. +Runtime equality tests cover 4,096 levels, shared children, malformed cycles, +and short-circuit ordering before invalid later fields. These gates retain the +existing equality type restrictions and do not resolve the open policies above. +The deep-reflection dispatch gate checks 70 nested list arguments both directly +and through a named alias, comparing exact reflected names/kinds in debug and +release binaries after source removal. Runtime identity tests cover 4,096 alias +levels with alias preservation and erasure, shared metadata, cycles, and +malformed alias arguments. These replace the former 64-level identity cutoff. +The deep-interface conversion gate compiles 130 nested list types in debug and +release, removes source, and checks the converted empty container against the +interpreter. Runtime tests additionally convert populated 130-level lists, +inspect the boxed leaf's identity and payload, and inject allocation failures +at outer, middle, and innermost construction boundaries. Every failed conversion +preserves its source and retires partial owners. Truncated and extra descriptor +bytes are rejected before conversion. + +The standalone report intentionally keeps `complete` false while these release +obligations are unresolved, even when every fixture count is full. The historical +85% planning estimate is not a measured code-coverage or completion result. + +The refined-small-value interface gate executes `nothing`, inherited unit +refinements, booleans, and bytes in runtime and comptime lists, with nested +pending unit joins. It also checks borrowed coarsening/declassification through +direct calls, callbacks, projected fields, and later-argument handlers. Both +debug and release artifacts run after source removal. The companion failure +gate covers direct/indirect calls and both transparent wrappers, requiring the +interpreter's exact terminal error, no partial output, and exit 71 rather than +cleanup failure. MIR and codegen suites validate the shared ownership handoff. + +The facade-context audit found a missing ownership-planner case: indirect calls +returning move-only values did not reserve their result temporary. A list built +from several callbacks returning interfaces failed native emission after +exhausting the planned slots. MIR now counts each owned indirect result, while +retaining separate slots for callee descriptors and argument evaluation. +`native_indirect_owned_results_match_interpreter_in_both_profiles` checks records, +enums, bitfields, machines, lists, sets, maps, bytes, optional/result values, and +construction builders. A third callback failure must retire earlier results and +the partially built collection, match the interpreter's error and output, and +exit 71. Both profiles execute after source removal. + +`native_interface_math_facade_contexts_match_interpreter_in_both_profiles` checks +all int64/float64 forms of the overloaded abs/min/max facades, secret taint from +either argument, generic wrappers, captured public-value callbacks, wholly baked +results, and comptime-created callbacks invoked at runtime. It verifies concrete +owner dispatch through interface collections and exact output in debug/release +after source removal. These probes strengthen the facade/comptime audit. The +separate erased-interface equality contract rejects implicit operators at compile +time and retains explicit domain comparison calls. Current concrete-owner +preflight acceptance is recorded above. + +The refined-sum audit fixes direct optional/result refinement boundaries to +validate their whole declared base before considering extraction. Its linked +matrix preserves erased payload owners across inheritance, fields, callbacks, +pending joins, and runtime/comptime values. Failure/absence cases and integer +result errors pin the independent string refinement-error contract. +The pending-candidate regression also exposed unguarded native sum extraction; +native tag reads now reject outer pending depth before payload transfer. +Debug/release terminal-error tests cover both sum families and branches, nested +pending depth, and an owned record payload after source removal. Runtime tests +verify that failed checks preserve owners and ready sums with pending payloads +remain extractable. Hidden-secret diagnostic observation remains subject to the +unresolved observation policy; native formatting retains its existing redaction. + +The wrapped-enum regression closes accepted secret constructor and equality +paths through unit and narrow variants, collection and exact struct payloads, +generic calls, projected fields named like variants, inherited refinements, +secret-backed refinements, nested pending joins, and explicit comptime values. +Debug and release artifacts execute after source removal and match the reference +output. Adjacent mixed public/secret arithmetic and logic preserve result taint; +malformed handoff tests reject lost secrecy and missing exact payload methods. +Debug layout tests retain root redaction and reject nested secret equality graphs. +These checks do not settle secret terminal-diagnostic observation policy. + +The namespace-constant handoff materializes literal values and earlier constant +references in declaration order, including unused declarations. All required +calculations, including operators, interpolation, and calls, require explicit +`comptime`; ordinary calculation initializers report E0378. E0377 still forbids +mutable globals. A shared declaration-keyed table supplies the reference, +verify/property, and native drivers. HIR `Constant` reads are baked before MIR +rather than emitted as startup work. Primitive shape checks reject hidden pending +values with E9001. +Debug constant traces retain declared type spelling through a scoped observation +temporary; that temporary does not enter later breakpoint snapshots and release +lowering removes the trace. + +The public-driver regressions in `global_constants.rs` cover literal-only +programs, namespace isolation across files, generic and inline reads, +verify/property contexts, required explicit calculations, initializer evaluation, +unused aggregate and pending rejection, and preservation of diagnostics and +existing native outputs. +`native_global_constants_match_interpreter_without_source_in_both_profiles` +provides the linked primitive-constant obligation, including test bodies and +source removal. These regressions must pass against the final revision alongside +the inventory and platform gates; they do not resolve move-only constant ownership +or establish full native completion. + +Local checks for this slice pass: compiler-phase suites, all 593 frontend +fixtures, all 182 run-pass backend-lowering cases, ten public constant-driver +regressions, and linked constant programs and verify/property suites after +source removal. Both program profiles are covered. The final revision still +requires the independent workspace, inventory, and platform distribution gates. + +The primitive-list sum audit found that native admission accepted only `int64`, +while the reference path also lost narrow-width wrapping and returned an integer +zero for empty floating lists. Both paths now use the checked numeric primitive +width, preserving per-addition binary32 rounding and IEEE infinity/NaN behavior. +Runtime and interpreter regressions cover widths, empty values, and pending +element failures. `native_primitive_list_sums_match_interpreter_in_both_profiles` +adds linked program and verify/property execution after source removal. These +supplemental cases do not change the inventory denominator or establish full +native acceptance. + +The pending-aggregate audit found unchecked public field/tag reads and projected +sequence ancestors, plus lost scalar payload depth during enum matching. Native +consumers now reject outer pending owners before observation or transition +payload execution, while raw internal projections remain usable. Scalar enum +bindings retain their pending depth through match and one-level joins. +`native_pending_aggregate_access_fails_before_observation_in_both_profiles` +pins ten exact public reference failures, debug task traces, and successful +failure cleanup. `native_ready_and_joined_aggregate_access_preserves_owners_in_both_profiles` +covers repeated borrowed enum-list matches, ready/projected owners, and fully +joined records, machines, enums, and scalar payloads. Its `int8`, `float32`, +`bool`, and `nothing` enum payloads pin depth-two, depth-one, and ready traces +through successive joins. Both profiles run after source removal. Runtime +checks also preserve nested pending children and typed +redaction. Hidden-secret diagnostic observation remains open; missing debug +layouts use conservative redaction rather than proving public output parity. + +The projected-assignment prerequisite handoff rejects immutable, temporary, and +known view roots before execution in functions, inline bodies, actor handlers, +comptime requests, and verify/property bodies. It also rejects rebinding an +owned mutable local from a move-only view, including parenthesized targets, +without changing declaration facts across branches. Owned mutable field updates +still need the open execution contract above. The known borrowed return and +owned argument checks below extend this prerequisite; named view-type aliases +and general call-produced borrow provenance remain separate ownership work. + +`projected_assignment.rs` pins frontend diagnostics, rejection before evaluation, +and preservation of existing native outputs for both profiles and test suites. +`native_projected_reads_and_reconstructed_owners_match_interpreter_in_both_profiles` +executes reads, owned copies, and construction/rebinding after source removal, +including verify and property suites. Local validation passes all 179 +typechecker tests, all 594 frontend fixtures, all 182 run-pass lowering cases, +four public driver regressions, and five linked projected-value regressions. +These checks establish the prerequisite handoff, not support for field updates +or full native parity. + +The native list-ordering audit found three additional accepted-domain gaps: +primitive-backed refinement sorts failed admission, narrow numeric sortedness +and indexed sorting selected no comparison carrier, and pending strings/rows +could be ordered by their hidden payload. Sort and comparison carrier selection +now follows refinement bases at every numeric width while retaining the exact +checked list type. Summation stays primitive-only. Pending strings, scalar keys, +and rows compare as equal before payload access; ready floating sortedness uses +IEEE `<=`, preserving empty/singleton and pending-pair behavior for NaN. + +`native_list_sort_refinements_and_ordering_match_interpreter_in_both_profiles` +executes both ordering fixtures in debug and release and runs their native +verify/property suites after source removal. It includes local explicit-comptime +refined numeric/string lists, nominal method dispatch, numeric limits and +rounding, duplicates, empty/singleton cases, NaN/infinity, indexed payload +markers, and pending depth through joins. The separate outer-pending regression +matches the public failure and exit 71 in both profiles. All 66 codegen tests, +122 runtime unit tests and its integration suites pass, as do the existing +pending-list and primitive-sum linked regressions. This closes these ordering +mismatches without changing the inventory denominator or resolving the open +summation domain. + +The native math-aggregate audit found that `math.average` and `math.median` +accepted all ten primitive numeric list types in the checker and reference path, +but native admission handled only the three wider carriers. Both native APIs +now admit the complete checked primitive domain and transparent aliases, retain +their exact `float64` result, and validate carrier bits before decoding. Signed +narrow values extend with their sign and binary32 values widen after rounding; +the existing binary64 aggregate algorithms and input ownership remain unchanged. +Refinements, secret wrappers, and nonnumeric elements remain rejected. + +`native_math_aggregate_primitives_match_interpreter_in_both_profiles` compares +all widths, aliases, ordinary generic calls, explicit-comptime results, extrema, +large unsigned values, binary32 precision, and IEEE behavior. Program binaries +in debug/release and native verify/property suites run after source removal. +Its failure companion pins 20 typed-empty cases and six outer/first/later pending +cases against exact interpreter output, diagnostics, and exit 71 in both profiles. +All 68 codegen tests, 126 runtime unit tests and its integration suites pass, +alongside the existing linked pending-math and extreme-value regressions. +These supplemental regressions do not change the inventory denominator or +settle the separate summation and other open policy gates. + +The uninhabited-iteration audit found accepted loops over inferred empty lists +and maps failing native admission on their `never` bindings. HIR now omits +lowering these checked bodies while retaining iterable expressions and reserved +generic identities. Native sequence preparation preserves one-time evaluation, +pending and projected-ancestor checks, and loan cleanup, then bypasses extraction. +Only changed functions have unreachable blocks and unused locals compacted; +retained spans, debug types, parameters, captures, and function identities remain +intact. Dead concrete generic specializations with bare `never` signatures are +excluded only from implicit project roots. Retained references still reach them +and cannot acquire a fabricated runtime carrier. + +`native_empty_collection_iteration_matches_interpreter_in_both_profiles` pins +ordinary generic calls, dead named/inline callbacks and debug events, borrowed +and consuming loops, once-only argument effects, nested collections, contextual +concrete empty values, joined inputs, and explicit-comptime results. Debug/release +programs and native verify/property suites execute after source removal. Four +runtime/comptime pending-list/map cases retain exact output and errors with exit +71; a public-driver companion rejects invalid arithmetic, interpolation, and +unresolved calls in dead bodies before lowering. Malformed MIR controls preserve +invalid IDs and inline metadata for validation rather than hiding them in pruning. +All 69 HIR tests, 32 MIR unit tests and its integration test, 71 codegen tests, +and eight backend-lowering regressions covering all 182 run-pass cases pass. +These checks retain the inventory denominator and do not establish full native +acceptance or settle the remaining open policies. + +The uninhabited-handler audit found optional/result handlers attempting to +extract impossible payloads, and generated callbacks retaining unused parent +locals whose types have no native carrier. Native preparation selects the sole +inhabited arm only for an exact checked `SumTag` branch. It retains source +evaluation, snapshots, and pending validation, then compacts changed graphs. +Explicitly generated inline functions also compact unused parent locals and +become reachable through retained calls and descriptors. Original structural +validation protects pruned graphs; global debug labels remain validated even +for omitted functions. + +The proof depends on directional frontend compatibility: concrete values cannot +populate `never` through defaults, declarations, calls, constructors, returns, +or required comptime evaluation. Mixed collections and repeated generic +arguments fill compatible absent slots in either order without promoting +numeric widths, erasing nominal types, or weakening callable variance and view +modes. Contextual handlers preserve producer signatures and convert inhabited +nested payloads explicitly; empty owned containers receive their target +ownership flags. Cloneable local sums snapshot their underlying source before +conversion, preserving owned and borrowed reuse. + +Generic reflection now retains declared nested alias witnesses through +identifiers, explicit views, parentheses, and concrete named/inline callback +signatures. Root transparent aliases still peel, and repeated concrete witnesses +follow declaration parameter order, including named arguments. The reference +evaluator fills nested `never` slots using the same variance and source-witness +rules. Field, returned-call, and clone-expression witness recovery remains a +separate metadata audit; this handoff does not claim those expression forms. + +`native_uninhabited_sum_arms_match_interpreter_in_both_profiles` covers absent +handlers, empty reverse, dead and surviving callbacks, once-only effects, +contextual interfaces and function values, owned/view source reuse, nested +pending joins, mixed collection inference, alias reflection, and required +comptime values. Debug/release programs and native verify/property suites run +after source removal. Its pending companion covers eight runtime/comptime +sum/reverse failures with exact output and exit 71 in both profiles. Public +driver controls reject invalid dead arms and manufactured `never` values before +execution or native publication, retaining existing output files. These +supplemental cases do not change the 207-obligation inventory denominator. + +Local validation passes all 195 checker tests, 325 reference/comptime tests, +74 HIR tests, 46 MIR unit tests and its integration test, and 73 codegen tests. +All 594 frontend fixtures and eight backend-lowering regressions covering all +182 run-pass files pass. All 182 inventory object obligations emit nonempty +native objects, with deterministic repeat-emission controls passing. The four +new native regressions and eleven adjacent native tests pass, including both +profiles and native verify/property execution after source removal. Formatting +and whitespace checks pass. Complete workspace, inventory execution, and +platform distribution results for the final revision remain independent gates; +this handoff does not establish full native parity. + +The reflected-field owner audit found that an admitted read of an +interface-backed refinement as its base interface reused the base box and lost +the refinement's method implementation. Native record, enum, and machine reads +now select conversion metadata from the actual declared field type and run the +conversion after the existing metadata, requested-type, and pending checks. +Conversion borrows and clones the source, preserves pending depth, and produces +one owned result. +Ordinary and reflected secret-qualified destinations choose the underlying +nominal owner after removing only matching outer secret qualifiers. Unqualified +interface destinations still select explicit secret-owned implementations; +additional source qualifiers and nested secret payload redaction remain intact. + +All 78 codegen tests pass, including five unit controls pinning owner selection, +unchanged admission, and nested redaction. Both new native conformance tests pass. +`native_reflected_interface_field_owners_match_interpreter_in_both_profiles` +covers direct/reflected dispatch, exact and ancestor reads, runtime/comptime +values, source reuse, pending joins, qualified secrets, and native verify/property +suites after source removal. +`native_reflected_interface_field_failures_match_interpreter_in_both_profiles` +checks requested-type, active-member, and pending-metadata failures in both +profiles. All 19 adjacent interface conformance tests, three deep-conversion and +reflection tests, and eight pending-reflection tests pass, including existing +explicit secret-owner implementations. +These supplemental cases leave the inventory denominator unchanged and do not +settle the open reflected admission contract or establish full native parity. + +The qualified interface callback audit found that `Reader.read` checked as a +function value but became a non-function in the reference evaluator and lacked +a native dispatcher target. Checked method values now distinguish concrete +source bodies from interface slots. Callback-only slots are collected through +ordinary, generic, and nested reflected bodies and resolve to the existing +dispatcher with exact parameter types and view modes. Declared slots remain +available without an implementation, and creating a callback executes no body +or authority. Runtime receivers still select the existing implementation. +Transparent aliases resolve in declaration scope, including when a caller's +namespace import or generic parameter has the same name as the target. + +An implementation of another interface for an interface type can share the +declared method's qualified display name. The declaration's slot takes +precedence during reference lookup, invocation, and explicit compile-time +callback materialization; failure to select an implementation cannot fall back +to the colliding source body. The source implementation retains its distinct +native identity. + +All 201 checker, 332 reference/comptime, 77 HIR, 47 MIR, and 594 frontend fixture +tests pass. The two +new native conformance tests pass in debug and release after source removal, +including compiled verify/property suites, aliases, multiple owners and slots, +explicit receiver reuse, callback adapters, generic and compile-time values, +capability parameters, argument-before-callee effects, canonical debug names, +name collisions, and pending-receiver failures. All 13 adjacent linked function +value tests and 19 adjacent interface tests pass. Complete inventory, workspace, +and host distribution gates +remain independent obligations for the final revision; the supplemental +callback fixture does not change the inventory denominator or settle the +concrete-owner argument policy for erased calls. + +The borrowed-boundary audit found known move-only views that could be returned +or passed to owned parameters through local aliases and unchecked body +contexts. The checker now reports E0401 for these boundaries across ordinary, +generic, indirect, and pipeline calls, preserving view facts through +parentheses, coarsening, and declassification. Explicit written view arguments +retain E0375. Direct return-view annotations also report E0401, including unused +generic headers and callable results; the check does not expand named aliases +or prewalk unused generic bodies. + +`view_owned_returns.jett`, `view_owned_arguments.jett`, +`view_explicit_owned_modes.jett`, and `view_return_annotations.jett` pin these +diagnostics. +`view_escapes::borrowed_values_cannot_escape_before_program_or_suite_execution` +checks rejection before runtime, test suites, and explicit comptime evaluation. +`view_escapes::borrowed_value_escapes_preserve_existing_native_publications` +checks existing program outputs in both profiles and compiled test suites. +`native_borrowed_return_clone_controls_match_interpreter_in_both_profiles` uses +`borrowed_return_clone_controls.jett` for source removal, exact output, compiled +verify/property suites, cloned returns and owned arguments, scalar/string +copies, owned field copies, cloned coarsening/declassification, and closed +comptime. + +Focused validation passes all 213 checker, 332 reference/comptime, 77 HIR, +47 MIR, 78 codegen, and 598 frontend fixture tests. All eight backend-lowering +tests pass, including the 182-fixture run-pass obligation. Both new public driver +regressions and the linked clone-control regression pass, including debug and +release after source removal and compiled verify/property suites. + +These supplemental regressions do not change the inventory denominator or +establish full view support. At that checkpoint native local view-alias lowering +remained unsupported; the frontend checks added no lifetime or projection policy. + +The direct contextual secret-constructor gap is resolved for source +`list(...)`, `map(...)`, `some(...)`, `none`, `ok(...)`, and `fail(...)` with +checked outer `Secret` qualification. HIR builds the exact inner container, +then restores the original checked TypeId and span with the existing +`InterfaceCoerce`. Payload interface, function, and inferred `never` +conversions remain unchanged; nominal refinements and arbitrary producers are +untouched. This correction adds no policy and keeps strict native constructor +shape checks. +Declared outer-secret local types are also retained, including repeated +wrappers, while less-qualified initializers keep their checked producer types +and use the existing initialization coercion. This closes the local/read +metadata mismatch without extending contextual inference or changing the +one-layer `declassify` rule. + +Focused constructor-normalization HIR validation has passed. +`native_contextual_secret_constructor_failures_preserve_order_and_cleanup` +passes five payload-failure cases in debug and release after source removal, +preserving lexical effects, the terminal failure, and cleanup. +`native_contextual_secret_constructors_match_interpreter_in_both_profiles` is +the success regression for `contextual_secret_constructors.jett`. It passes +in debug and release after source removal with all nine stdout lines matching +the interpreter, root-secret debug redaction, and release trace omission. The +compiled verify suite and 100 property trials also pass. All 82 HIR, 47 MIR, +332 reference/comptime, and 78 codegen tests pass with the declared-local +correction. All eight backend-lowering tests also pass, including the 182-file +run-pass obligation. Complete workspace, inventory, and host distribution gates +remain independent obligations for the final revision. + +Direct and forwarded immutable local view aliases now have a bounded native +handoff. Checked binding modes retain immediate resolved origins separately in +ordinary, generic, and reflected bodies. HIR validates immutable backing chains +and allocation-free carrier-preserving initializers. MIR remaps and retains +origins, expands every alias read into backing-owner liveness, and excludes +aliases from cleanup slots. Owner consumption or rebinding after reachable +alias creation remains a conservative native implementation error; this does +not settle source loan expiry or temporary lifetime rules. The remaining domain +is recorded in [the local alias note](native_local_view_aliases.md). + +Formal view modes survive direct and indirect handler staging. Copy-owned +string and callback observations acquire independent snapshots where needed; +ordinary clones and implicitly copied results acquire owning temporaries before +entering containers. Borrowed arguments retain their existing backing handle. +Plain string view expressions keep their existing copy behavior. Borrowed +enum matches snapshot the scrutinee before payload extraction. + +`local_view_aliases.jett` and its two linked tests pass in debug and release +after source removal. The success test pins fourteen stdout lines and four +debug traces, including secret redaction and release trace omission. It covers +list, bytes, string, struct, interface, enum, and function aliases; generic and +reflected bodies; sibling scopes, loops, pending values, clone independence, +handled later arguments, and container copies that outlive their helper frame. +The compiled verify suite and 100 property trials pass. The terminal-failure +test preserves lexical output and the exact error with exit 71 in both profiles, +proving successful checked cleanup rather than a cleanup-error override. + +All 217 checker, 89 HIR, 59 MIR, 332 reference/comptime, and 78 codegen tests +pass, as do all 598 frontend fixtures and eight backend-lowering tests, including +the 182-file run-pass obligation. Thirteen adjacent function-value tests, nine +handler tests, and three pending-string tests also pass. These supplemental +fixtures do not change the inventory denominator or establish full workspace, +inventory execution, or supported-host distribution parity for this revision. + +Strict native graphics decoding now preserves the reference distinction between +terminal shape errors and handled domain failures. The reproduced `width: run 0` +case previously read zero as a ready dimension and returned a handled failure; +it now reports the exact terminal Config width error before rendering or provider +use. The decoder checks pending record owners, field metadata, strings, list +containers, and record elements before payload access, in reference field order. +Complete decoding precedes domain bounds, and window dimensions are read only +after successful Config validation. Existing ABI leaves and callback state +metadata remain unchanged. + +Four linked regressions in `native_conformance/pending_graphics.rs` pass in debug +and release after source removal. Their 53-case matrix covers Config, Scene, +Color, Rect, and Text roots and fields; Scene list containers/elements; nested +pending depth, partial joins, closed comptime values, competing errors, and ready +domain failures. Initial and later render failures preserve lexical stdout and +the exact terminal error with exit 71, including unused scripted input. The fully +joined session control retains pending State across callbacks, pins three debug +traces and release omission, and passes compiled verify and 100 property trials. + +Seven focused runtime regressions additionally check all 38 pending metadata +sites at depths one and two, malformed payload guards, source preservation, +one-layer joins, provider ordering, and unfinished-session cleanup. All 145 +runtime tests, 78 codegen tests, nine linked graphics tests, and fourteen graphics +frontend/reference integration tests pass. These supplemental cases leave the +inventory denominator unchanged; full workspace, inventory execution, and +supported-host distribution gates remain independent final-revision obligations. + +Forwarded pending Graphics authority now fails at kernel entry before Config +decoding or domain validation. The source-admitted helper probe previously +returned a handled width-zero failure and continued execution; it now preserves +the reference terminal `graphics.__run expects a Graphics capability` error. +The emitter reuses the existing capability pending-depth leaf after lexical +argument evaluation. It neither consumes nor implicitly joins authority and +adds no ABI operation or callback policy. + +Three linked regressions in `native_conformance/graphics_authority.rs` pass in +both profiles after source removal. The minimal probe and eleven companion +cases pin nested depth, partial joins, capability-before-Config error precedence, +ready/final-joined authority, and explicit-view reuse. Terminal outcomes require +exact stdout/stderr and exit 71. A separate successful control opens and closes +two scripted sessions with the same fully joined authority, proving that its +context-bound identity remains usable at the provider boundary. All twelve +linked graphics tests and 78 codegen tests pass with the guard. Supplemental +coverage leaves the inventory denominator unchanged and does not replace the +workspace, inventory, or supported-host distribution gates for this revision. + +The locked Windows workspace run at `22fcbf13` passed all inventory gates: +182 HIR/MIR fixtures, 182 native objects, 30 entry cases, 25 runtime contracts, +155 verify fixtures, and three property fixtures with 100 trials each. Its +native conformance target reported 259 passes and three failures; all other +workspace targets and doc-tests passed. The three failures exposed borrowed +`coarsen` initializers that MIR had replaced with owned snapshots, invalidating +their checked backing origins. + +MIR now preserves only local alias initializers accepted by the shared typed +origin validator. Borrowed `coarsen`, `declassify`, and forwarded aliases retain +their backing handles; ordinary owned expressions still acquire snapshots. +Malformed clone, handler, and different-source initializers remain rejected. +The earlier projected-local admission error is pinned directly, separately from +source escape, conflicting borrow, and moved-owner errors. A portable debug and +release regression verifies rejection before launcher lookup and preserves an +existing executable. + +All 62 MIR tests, 78 codegen tests, 21 linked interface tests, both public +view-escape tests, and four local alias regressions pass with this repair. +The linked nonstring qualification regression runs in both profiles after +source removal, checks independent returned clones and unchanged backing +owners, and requires the exact later terminal failure with successful cleanup. +The Linux-only projection-loan test +expectation is updated but requires Linux validation. This repair does not +change the inventory denominator or extend native view origins; full workspace +and supported-host distribution results must still be established for the +final revision. + +The implicit display-result gap is resolved at its interpolation boundary. +The reproduced method returning `run "shown"` previously printed +`value:pending(shown)` natively while the reference interpreter stopped after +`before` with `Displayable.display returned pending(shown) instead of string`. +HIR now carries `DisplayResult` around only the implicitly selected call. +MIR preserves it inside staged segment initializers, and codegen checks its +already owned string through a borrowed runtime leaf before later segments. +Exact-string validation, reachability, ownership planning, pruning, and native +constant baking retain the marker without adding an owning slot. + +Three linked regressions in `native_conformance/display_results.rs` cover the +minimal failure, six terminal cases, and successful controls in both profiles +after source removal. Terminal cases pin nested depth, Unicode and empty text, +partial joins, primitive and erased-interface owners, method-failure precedence, +once-only debug output, suppressed later handlers, exact stdout/stderr, and +exit 71 with cleanup. Success controls pin ordinary pending-string formatting +from explicit calls, fully joined implicit and generic display, namespaced +implementations, receiver reuse, closed comptime, compiled verify, and 100 +property trials. + +All 91 HIR, 64 MIR, 80 codegen, 151 runtime, and 332 reference/comptime tests +pass. Five focused runtime tests and the independent C ABI regression preserve +string handles and reference counts, pending layers, exact dynamic failures, +first-error status, and terminal cleanup. Supplemental cases leave the +inventory denominator unchanged; complete workspace and supported-host +distribution validation remain final-revision obligations. + +The locked Windows workspace run at `7cdf24b1` passed every workspace target +and doc-test. Native conformance reported 267 passes with no failures. All +inventory gates passed: 182 HIR/MIR fixtures, 182 native objects, 30 entry +cases, 25 runtime contracts, 155 verify fixtures, and three property fixtures +with 100 trials each. Source remained frozen throughout the run. Linux-only +tests and both supported hosts' clean package jobs still require CI results for +the final revision; a local Windows pass does not substitute for those gates. +The [supported-host run for `7cdf24b1`](https://github.com/vycdev/jett/actions/runs/36872780921) +has since completed successfully: both Linux and Windows workspace/build/package +jobs and both clean installed-package jobs passed. That result proves the prior +checkpoint's distribution gates, not those of a later compiler revision. + +Additional source probes at that checkpoint found two gaps outside the fixed inventory. A direct +struct `Equatable.equals` returning `run true` is accepted by the checker but +the reference interpreter reports `Equatable.equals must return bool`. Native +`==` instead prints `pending(true)` and exits successfully; native `!=` reports +`'not' requires a boolean operand` with exit 71. Secret-qualified operands show +the same equality-result gap when the secret is initialized from a ready local. +The enum payload callback already checks result depth; the direct operator +call needed its own preserved result boundary before inequality negation. + +The direct equality-result gap is now resolved. HIR wraps only implicitly +selected struct equality calls in `EquatableResult`; MIR preserves that boundary +through operand and later-handler staging. Codegen checks pending depth with the +existing `RejectPendingScalars` leaf before using the boolean or negating it. +The child's exact checked boolean type, including leading secret qualification, +is retained. Explicit method calls remain ordinary task-producing calls. No +runtime ABI operation or owned temporary is added, and malformed boolean, +qualification, and nominal-refinement metadata remains rejected. + +All 93 HIR, 66 MIR, 82 codegen, and 332 reference/comptime tests pass. Three +linked regressions in `native_conformance/equality_results.rs` cover both +operators in a minimal reproduction, 16 terminal matrix cases, and successful +controls. Debug and release binaries run after source removal. Failures pin +true and false payloads at depth one and two, partial joins, exact method-error +precedence, left-to-right effects, once-only traces, suppressed later handlers +and short-circuit effects, secret-qualified operands, exact stderr, and exit 71 +with successful cleanup. Successful controls cover ready and fully joined +implicit equality, ordinary explicit pending calls, unchanged owners, +namespaced generic and reflected comparisons, closed comptime, compiled verify, +and 100 property trials. Six adjacent linked enum regressions also pass. +Supplemental cases do not change the inventory denominator; the prior locked +workspace result remains specific to `7cdf24b1`, and the final revision still +requires workspace and supported-host distribution validation. + +The separate direct `secret[Item]` constructor admission gap is also resolved. +Its checked construction previously retained the outer secret type without an +inner constructor/qualification split, and native verification correctly +rejected the inconsistent metadata. HIR now normalizes only source calls with +their checked struct-construction fact and an exact, nonvalidating nominal +target. Leading secret qualifiers are restored through the existing coercion +after construction; field conversions, source order, pending depth, and owner +identity remain intact. Validating result constructors, nominal refinements, +ordinary producers, and reflected builder calls are preserved unchanged. + +All 96 HIR, 66 MIR, and 83 codegen tests pass. Three focused HIR tests cover +direct/local/generic/specialized and parenthesized constructors, nested +qualification and `run`, interface/function/refinement payloads, handlers, +source construction provenance, unchanged producers and validating results, +and strict normalization controls. The object regression independently rejects +raw secret construction, a qualified inner constructor, a different nominal +target with the same fields, and invalid refinement-validation metadata. + +Two linked regressions in `native_conformance/secret_struct_constructors.rs` +pass in both profiles after source removal. Successful controls pin eight +handwritten output lines and root-secret trace redaction, narrow fields, +generic and nested constructors, function and inline returns, full joins, +closed comptime, refinement-backed interface dispatch, callback adaptation, +independent owned field copies, unchanged public producers, compiled verify, +and 100 property trials. Three terminal field cases pin reversed named-field +evaluation order, the exact earlier error, suppressed later effects, and exit +71 cleanup. Both existing linked contextual collection/sum constructor +regressions also pass. Neither repair changes the inventory denominator or authorizes a +broader equality or constructor policy. Final-revision workspace and +supported-host distribution validation remain required. + +Direct field reads from secret-qualified aggregates now pass native admission. +A minimal ready `Item` promoted to `secret[Item]` previously printed `7` in the +reference backend, while native verification rejected `hidden.value` with +`field owner mismatch`. The verifier now mirrors the existing checker rule: +one direct secret wrapper around the exact struct, bitfield, or machine-state +owner; unchanged `nothing` and already-secret field types, including nominal +secret-backed refinements; otherwise exactly one wrapper around the declared +field TypeId. Nominal owners, indexes, and result qualification remain strict. +Emission, pending checks, machine-state indexing, and field ownership paths +are unchanged; no ABI operation is added. + +All 84 codegen tests pass. The new object regression rejects eight malformed +field contracts, including erased secrecy or nominal identity, added or doubled +secrecy, qualified `nothing`, different owners, invalid indexes, and nested +secret receivers. Three linked regressions in `native_conformance/secret_fields.rs` +pass in both profiles after source removal. Successful controls pin eight +handwritten output lines, narrow numbers and Unicode strings, lists, refined +interface owners and captured callbacks, already-secret fields, `nothing`, +secret-backed nominal fields, bitfield and state-qualified machine reads, +generic/reflected controls, owned field copies surviving their helper scope, +independent list mutation, nested child depth and joins, closed comptime, +compiled verify, and 100 property trials. Pending-child failures preserve exact +depth, first-error precedence, and exit 71 cleanup. + +Pending secret receiver errors intentionally retain native typed redaction. +Depth-one, depth-two, and partially joined receiver regressions pin the raw +reference error separately from native `[redacted]`, with exact output and +cleanup in both profiles. Those tests do not claim diagnostic parity or resolve +the [hidden-secret observation policy](../../open_design/debug_print_hidden_secrets.md). + +A separate accepted-source probe still exposes direct contextual secret +bitfield construction: `secret[Header] hidden = Header(value: 7)` runs and +prints `7` in the reference backend, but HIR reports `checked bitfield +construction has an invalid type`. Public construction followed by secret +qualification works, as the linked field suite demonstrates. This is an +implementation gap outside the fixed inventory; width-validating constructors +and machine construction need their own evidence before extending that repair. +Final-revision workspace and supported-host distribution gates remain open. + +The locked Windows workspace run at `65c6ba68` passed every workspace target +and doc-test with source frozen throughout. All 275 native conformance tests +passed, including the eight new linked equality, secret-struct-construction, +and secret-field regressions. The independent inventory gates also passed: +182 HIR/MIR fixtures, 182 native objects, 30 entry cases, 25 runtime contracts, +155 verify fixtures, and three property fixtures with 100 trials each. +This checkpoint does not certify a later revision or Linux-only execution; +the [supported-host run for this revision](https://github.com/vycdev/jett/actions/runs/36884693314) +has since passed both Linux and Windows workspace/build/package jobs and both +clean installed-package jobs. This certifies that checkpoint's distribution, +not a later compiler revision. + +The contextual secret bitfield and exact-state machine constructor gap is now +resolved. HIR peels only leading secret wrappers while selecting an already +resolved declaration, retaining the exact nominal bitfield or machine/state +identity. Plain bitfields retain their value output; validating bitfields retain +`result[Bitfield, string]` and their width-validation flag. The existing coercion +restores the original checked secret type after construction. Field conversions, +lexical evaluation order, pending depth, result tags, and ownership remain intact; +native verification and the runtime ABI are unchanged. + +All 99 HIR, 66 MIR, 85 codegen, and 598 frontend fixture tests pass. Three new +linked regressions cover literal and dynamic bitfields, successful and failed +width validation, exact machine states, nested qualification, parentheses, +generic bodies, namespace and destination aliases, returns, inline callbacks, +full joins, closed comptime, unchanged public producers, owned payload copies, +machine interface/callback payloads, compiled verify, and 100 property trials. +Seven handwritten output lines pin the successful entry behavior. Debug and +release binaries execute after source removal. Reversed named arguments and +handled machine payloads preserve source order; terminal field failures suppress +later effects and result handlers, retain the exact first error, and exit 71 with +successful cleanup. The object test rejects 19 malformed nominal, result, +validation-flag, state-index, payload, and qualification contracts. Three adjacent +bitfield regressions, machine-transition and baked-machine regressions, and both +contextual collection/sum constructor regressions also pass. + +The contextual secret validating struct constructor gap is resolved. HIR moves +leading secret qualification outside the exact `result[Struct, string]` +constructor while preserving its checked construction fact, nominal success +type, validation flag, predicate chains and lexical field order. MIR's generated +sums now retain their ordinary result type inside the qualification coercion. +Empty predicate chains keep the constructor's result shape; ordinary producers +and nonmatching nominal or result types are unchanged. + +The HIR and strict object regressions pass, including malformed outer/inner +qualification, different nominal success types, wrong error or success types, +validation flags and erased result shape. Three linked regressions in +`native_conformance/secret_validating_struct_constructors.rs` pass in both +profiles after source removal. Eight handwritten successful output lines cover +generic/nested/parenthesized constructors, returned and inline values, public +producers, closed comptime, full joins and independent string/list copies. +Compiled verify and 100 property trials pass. Predicate failures retain captured +result data and source order; field evaluation failures remain terminal, precede +later predicates/handlers and exit 71 with successful cleanup. + +Qualified result joins now preserve their checked outer boundary in both +backends. Exact results retain their type; secret-qualified and nominally +refined results receive an additional result around the joined payload. The +interpreter also reconstructs Join types from the active checked facts before +declared fallback, so refinement handlers extract that outer result correctly. +The native move planner admits only explicit-view borrowing for Join's owned +copy and preserves ordinary view-consumption and escape rejections. + +All 339 interpreter, 69 MIR, 100 HIR, 86 codegen and 598 frontend tests pass. +Three linked regressions in `native_conformance/qualified_result_tasks.rs` pass in both +profiles after source removal. Handwritten Unicode output and typed debug +observations pin exact/qualified/refined/aliased results, generic and reflected +bodies, nested secrecy, closed comptime, one-layer joins, independent list +copies, explicit-view owner reuse, pending scalar children and nonstring inner +errors. Compiled verify and 100 property trials pass. The inner failure handler +remains separate from the outer Join handler; its later terminal failure retains +the first error, effect order and exit 71 cleanup. No runtime leaf signature, +verifier type rule or fixture denominator changes. + +The direct local and struct-field predicate reuse repair follows the selected +design: interpreter validation captures the original checked source type before +evaluation or primitive normalization. Exact refinements reuse their invariant, +and ancestor promotion runs only the remaining base-first predicates. Handled +optional/result success payloads retain their checked proof; whole nominal sum +refinements remain distinct. Coarsening supplies its selected output proof, and +a sibling sharing the carrier proves nothing. Forced validation of changed +property candidates and metadata-free calls remains on its existing path. + +The same audit exposed generic constructor type selection: reference execution +returned a plain struct for `Box[Positive]`, while a caller's unrelated +`T = Positive` made `Box[bool]` validate `true` as Positive. Constructor arguments +now resolve in caller context and map to the declaration's own parameters. +Field resolution, validation flags, normalization, and retained identities use +the declaration context; caller imports cannot redirect canonical field names. +The selected concrete fields determine the existing checked result contract, +including an empty predicate chain. Caller context is restored after success, +captured predicate failure, or terminal construction failure. + +All 350 interpreter tests pass, including eleven new proof, normalization, +context, and generic-constructor controls. Four linked regressions in +`native_conformance/refinement_reuse.rs` pass in debug and release after source +removal. Handwritten stdout and trace expectations pin exact/local/field reuse, +ancestor suffix order, explicit coarsening, owned string/list copies, one-layer +pending joins, captured predicate errors, and rejection of pending inputs to new +predicates. Concrete generic result shapes, closed comptime, compiled verify, +and 100 property trials also pass. These additions retain the fixed inventory +denominator and require final-revision workspace and distribution validation. +The existing native shrink regression for refinement chains and nested +predicates also passes in both profiles, preserving validation of changed +candidates and caught predicate errors. + +Checked named function parameters and exact returns now retain their original +expression proofs. Arguments and proofs are captured before evaluation and +permuted together for named calls and source pipelines. Actual return-expression +facts travel with return signals through nested handlers; destination annotations +and runtime labels never supply that proof. Raw call entry points still force +parameter and return validation, even with a checked body map installed. Proof +reuse stays disabled throughout their nested invocation tree, including calls, +locals, constructors, callbacks, and forced validators. Four additional unit +controls reject forged refined struct children and list elements and check mode +restoration after success and failure. Checked program and zero-argument legacy +verify entries use an explicit bridge without synthesizing argument proofs. +Reflected builder finish retains its documented validation of provided values. + +Seven new interpreter tests and four linked regressions in +`native_conformance/refinement_calls.rs` pass. Debug and release programs run +after source removal with handwritten output and trace expectations for scalar, +string/list, method, NamedFunction, generic/reflected, pipeline, named-order, +handler/default, and pending-depth cases. Pure closed comptime, compiled verify, +100 property trials, and forced builder completion also pass. Inline descriptors +now retain their declared return annotation in captured lexical context. +Seven additional interpreter tests cover invalid fresh inline results before +local, argument and return consumers; exact and ancestor proofs; owned Unicode +strings and lists; pending depth; generic/reflected and namespace alias scope +restoration; primitive normalization; and forced raw callbacks. Host-generated +callbacks retain their separate forced-call path; these tests do not establish +proof reuse for that scope. + +The proof audit found that the private list sum fallback can produce raw zero +under a checked Positive result annotation. Source execution now forces +validation of the fresh result against its actual intrinsic type argument before +that annotation becomes reusable proof. Actual arguments resolve before nested +evaluation. Four unit tests cover invalid/valid results, pending and mixed-type +errors, second-generic selection, primitive aliases, ordinary and pipeline +consumers, raw/no-metadata behavior, and partial checked maps. A driver control +pins empty Positive summation's reference constraint failure and native codegen +refusal in both profiles, preserving an existing publication before launcher +lookup. This repairs proof safety without selecting the unresolved refined-sum +domain or widening native admission. + +Ten reflected producer unit tests cover root refinements and admitted ready +builtin wrappers across record, enum, and machine fields. Shape preflight finishes +before predicates; only occupied arms and collection members are checked, in +their existing order. Declared schemas preserve exact invariants and skip proven +ancestor prefixes. Exact roots retain nominal identity and pending depth without +rechecking. Changed nested pending, secret, callable, and nominal generic requests +fail conservatively before publishing proof; diagnostics do not render values. +Raw and metadata-free field readers preserve their existing consuming boundaries. +Five frontend-admitted source probes now reject zero-as-Positive before optional, +list, declassify, callable, or nominal-field consumers. This reference safeguard +does not select broader reflected admission or certify native producer predicates. + +Fourteen additional boundary and canonical-type unit tests preserve written view +validation, resolved primitive widths, generic parameter identities, captured +closure metadata, reflected bindings, inferred call/debug types, and caller +imports on success and failure. Written aliases resolve once; installed and checked +types keep their canonical owners. Fresh CLI runtime and closed comptime probes +pass under colliding imports, including enum and machine payload loops and a +closure fixture containing Unicode strings and owned lists. All 396 interpreter +tests and all 598 frontend fixtures pass. The four linked call regressions and the +refined-sum refusal control also pass against this combined source revision. +Final workspace and supported-host distribution validation remain required. + +Reference reflected reads now also reuse a stronger declared refinement as proof +of its requested root ancestor. The lookup follows only the actual declaration's +root chain, preserves normalized result identity and pending depths, and never +uses a runtime label or descends through an outer qualifier or child schema. +Three regressions cover all three getters, both metadata modes, transparent +aliases, owned Unicode values, source preservation, pending depths 0/1/2, and +raw, disabled, unrelated-fact, and forged-label controls. All 399 interpreter +tests pass. A fresh CLI success probe matches the independent scalar, owned, +pending, operand-order, and predicate-trace oracle, including five reciprocal +ancestor reads with no extra predicate. Native execution remains a separate +producer-validation obligation. + +Native returns now establish newly requested root refinements before exposing +their checked result type. Returning `-1` under Positive, or a Positive value of +7 under High constrained above 10, rejects with the reference error. The original +checked expression supplies source proof before coercion: exact returns skip +predicates, and ancestor returns check only their missing suffix. Concrete +generic/method signatures select destinations; nested inline targets restore the +enclosing function, and reflected body facts keep the selected return target. +MIR stages the candidate once, preserves pending depths, and forwards the first +predicate failure through a private exact-string `RuntimeFailureMessage` boundary +and the existing borrowed runtime failure leaf. Owned candidates and error strings +retain their cleanup slots. No source spelling, ABI operation, or broader outer +secret/container return admission is added. + +Eleven focused HIR, MIR, and object-emission tests and all 214 HIR/MIR/codegen +unit tests pass. Three linked return regressions and two inline regressions pass +in debug and release after source removal, with handwritten scalar/ancestor, +owned Unicode/string/list, generic/method/reflected, alias capture, pending, +predicate-error, operand-precedence, and exit-71 cleanup expectations. Closed +comptime, compiled verify suites, and 100-trial property suites also pass. +These five supplemental tests bring native conformance to 298 tests without +changing the fixed acceptance inventory. Full workspace and supported-host +distribution checks still need to certify the final revision. Return proof reuse +cannot replace validation at a reflected producer. + +Native reflected root-refinement producers now validate their selected cloned +field after the original getter completes its selector and payload checks. Exact +and stronger declared sources reuse established proof; ancestor promotions +evaluate only their missing suffix, and sibling or base sources check the whole +requested chain. HIR retains complete per-slot plans and exact canonical +predicate identities. MIR consumes those plans before exposing its internal +raw-read marker, preserving owned cleanup, pending depths, source identity, and +the first original error. Requested admission and outer secret/callable/nominal +conversion rules are unchanged. + +Fourteen focused HIR, MIR, and object tests, all 224 HIR/MIR/codegen library tests, +and all 55 object-emission tests pass. Three linked regressions pass through both +direct and piped getter forms in debug and release after source removal, +including all three getters, dynamic selectors, thirteen terminal failure cases, +owned Unicode/string/list values, unchanged sources, pending depths 1/2, +operand staging, closed comptime, verify, and 100-trial property execution. +The pipeline helper shares the direct getter's metadata expansion, preserving +source/selector order, complete slot plans, bitfields, and narrowed machine owners. +Native conformance now has 301 supplemental tests; the fixed inventory denominator +is unchanged. At that checkpoint, new native nested-container invariants were +implementation gaps; the recursive builtin checkpoint below closes that bounded +producer work. The other admitted one-operand reflection pipeline forms +are covered by the later checkpoint below. +Final workspace and supported-host distribution gates still require verification. + +The locked Windows workspace run at `52bd4dcb` passed every target and doc-test +with tracked source frozen throughout. All 284 native conformance tests passed, +including the six new qualified-result and secret validating-constructor tests. +The independent gates passed for 182 HIR/MIR fixtures, 182 native objects, +30 entry cases, 25 runtime contracts, 155 verify fixtures, and three property +fixtures with 100 trials each. The +[supported-host run for that revision](https://github.com/vycdev/jett/actions/runs/36898786909) +passed both workspace/build/package jobs and both clean installed jobs. +This checkpoint does not certify the later refinement +reuse repair or its final workspace and distribution obligations. + +The locked Windows workspace run at `a57d69a6` also passed every target and +doc-test with source frozen throughout. All 288 native conformance tests passed, +including the four direct refinement reuse regressions. Its independent inventory +gates passed for 182 HIR/MIR fixtures, 182 native objects, 30 entry cases, +25 runtime contracts, 155 verify fixtures, and three property fixtures with +100 trials each. The +[supported-host run for this revision](https://github.com/vycdev/jett/actions/runs/36905939310) +passed both workspace/build/package jobs and both clean installed jobs. This +certifies that checkpoint, not the later +call-proof or native-return changes. + +The frozen workspace run at `8ad7b1e4` and the supported-host runs at +[`768fdb15`](https://github.com/vycdev/jett/actions/runs/36923339203) and +[`8ad7b1e4`](https://github.com/vycdev/jett/actions/runs/36970926217) failed with a +reference runtime stack overflow during native conformance. Sequential isolation +identified nested machine JSON parsing. The source call path is finite: a +temporary diagnostic stack increase completed the case, and unoptimized Windows +assembly showed the general expression evaluator reserving 79,328 bytes per +expression. Ordinary and generic calls now dispatch before that large match, +retaining central checked-result normalization and signal propagation. The +production stack budget remains 8 MiB. All six adjacent nested JSON conformance +tests and all 396 interpreter tests pass with the repair. Complete workspace and +supported-host distribution validation still need to certify the repaired +revision; the failed runs do not establish those gates. + +The locked Windows workspace run at `f8e70722` passed every target and doc-test +with tracked source frozen throughout. All 399 interpreter tests, 598 frontend +fixtures, and 301 native conformance tests passed. The independent inventory +gates passed for 182 HIR/MIR fixtures, 182 native objects, 30 entry cases, +25 runtime contracts, 155 verify fixtures, and three property fixtures with +100 trials each. This checkpoint includes the call-frame stack repair, declared +ancestor proof reuse, native return validation, and direct/piped reflected root +producer validation. It does not close the remaining nested-container or other +reflection pipeline gaps, or the unresolved language policies. + +The [supported-host run for the stack repair at `57e0b003`](https://github.com/vycdev/jett/actions/runs/36974175496) +passed both workspace/build/package jobs and both clean installed jobs. The +[run for `f8e70722`](https://github.com/vycdev/jett/actions/runs/36975813263) +passed both workspace/build/package jobs and the Linux clean installed job. +Its Windows clean installed job failed when the native linker exceeded its +60-second deadline on the first relocated debug build, without linker output. +This does not establish the fourth distribution gate. The +[run for `cca40a2e`](https://github.com/vycdev/jett/actions/runs/36980991837) +is live in both platform workspace checks; its eventual installed-package result +must determine whether the linker timeout recurs. The earlier repair's green +distribution evidence does not certify these later changes. + +The admitted one-operand observers `type.variant_value`, +`type.machine_state_value`, and `type.arg` now append their existing checked +metadata through a shared direct/pipeline HIR helper. Pipeline result signatures +select metadata types; the step span describes the input. Source operands run +first and once. Variant ordinals remain distinct from discriminants, and narrowed +machines retain the complete base layout. Admission, runtime ABI, pending/index +checks, and ownership semantics are unchanged. + +Five focused HIR/object tests, all 227 HIR/MIR/codegen library tests, and all +57 object-emission tests pass. Three linked regressions pass through direct and +pipeline forms in debug and release after source removal, covering exact metadata, +independent field ownership, operand/consumer order, nine terminal errors, closed +comptime, compiled verify, and 100-trial property execution. The fixtures use +ordinary return factories to keep a separate whole-machine reassignment defect +outside these observer tests. Native conformance now has 304 supplemental tests; +the fixed inventory denominator is unchanged. Full workspace and supported-host +distribution validation remain obligations for this later revision. + +Reference reflected-type comparison now removes completed aliases/refinements +from its active expansion path. Repeated map/result arguments therefore normalize +independently, while actual recursive re-entry remains bounded. Four regressions +cover all three selectors, map key/value and occupied-arm order, exact pending +depths 0/1/2, changed-pending refusal, unchanged source, selector precedence, +declaration namespace, cycles, and protected conversion refusals. All 403 +interpreter tests and all 598 frontend fixtures pass. Fresh CLI map and result +controls pass through all three getters with the expected predicate traces and +unchanged sources. This reference repair does not widen the native raw getter's +currently narrower nested-type compatibility gate. + +Native whole-machine local storage now retains the checked bare machine +annotation instead of the initializer's precise state. Owned mutable locals can +therefore be rebound to another valid state of the same nominal machine, as the +existing annotation-erasure rule selects. Producer types, owner identities, and +source spans remain precise. Assignment verification retains the storage/owner +check and additionally enforces an exact checked state target when a visible +guard narrows a full-machine slot; foreign owners and other states still fail. + +Two HIR and two object regressions pass, along with all 229 HIR/MIR/codegen +library tests and all 59 object-emission tests. The linked regression passes in +debug and release after source removal, including owned replacement, move and +clone independence, self-clone, 32 repeated cleanup cycles, same-state guarded +replacement, pending depths 2/1, closed comptime, compiled verify and 100-trial +property execution. Fresh reference CLI output and traces match the independent +oracle. This closes the separate whole-machine reassignment defect found while +isolating the observer pipeline fixtures. Native conformance now has 305 +supplemental tests; the fixed inventory denominator is unchanged. Full workspace +and supported-host distribution gates remain required for this later revision. + +The admitted alias-as-constructor spelling also remains an unresolved source +contract; see [type alias constructor calls](../../open_design/type_alias_constructor_calls.md). +Supplemental cases do not change the inventory denominator. Full workspace and +supported-host distribution validation remain obligations for the final revision. + +The frozen `b733296a` workspace, whose compiled source is unchanged from +`cca40a2e`, passed `cargo test --locked --workspace --no-fail-fast`, including +all doc-tests. This verifies 403 interpreter tests, 598 frontend fixtures, +305 supplemental native conformance tests, all 182 native object obligations, +and the complete verify/property inventory. The log is +`target/native-workspace-b733296a.log`; later changes need their own verification. + +Native verify/property suite APIs now forward explicit build options through +lowering, emission, and executable building. Existing default wrappers retain +debug behavior. Three focused regressions pass: default and explicit-debug +objects match; debug/release suites execute with matching runtime manifests +after source removal, with 100 property trials and release traces omitted; +release debug printing reports E0362 before publication and preserves existing +output; and emitted release objects match optimized codegen while differing +from unoptimized controls. Both runtime manifest profiles reject a mismatched +selection. Native conformance now contains 308 supplemental tests. This repairs +suite mode selection; it does not close the remaining language-policy gaps. + +Deep reflected requests exposed parser lookahead ceilings before either backend +could check the requested type. Generic calls and typed locals now recognize +balanced type syntax across the complete logical line, with iterative callable +return prefixes and no fixed 20/40/60-token cutoff. All 92 parser tests and all +598 frontend fixtures pass. The five new parser regressions cover direct and +pipeline requests, nested callable arguments and locals, source spans, +non-consuming recognition, missing EOF sentinels, and malformed-input recovery. +This repairs recognition of existing syntax; recursive reflected-field native +execution remains a separate obligation. + +### Recursive builtin reflected producers + +Native reflected field reads now establish newly requested refinements inside +the five already admitted builtin wrappers: list, set, map, optional, and result. +Typed HIR trees retain actual and requested types, canonical predicate identities, +and proven prefixes. MIR completes all occupied structural checks before any new +predicate, then checks values in collection order with each map key before its +value. Both passes preserve the independent candidate and original source. +Exact schemas, named ancestors, and shared root prefixes reuse declared proof; +changed wrappers retain readiness even when their child predicates are skipped. + +Canonical generic argument TypeIds distinguish nominal fields from arguments. +An ordinary Element containing a Positive field does not request new invariants +through list[Element]. Used or unused Positive generic arguments do retain +wrapper readiness, including transparent aliases. Closed backend validation +visits unused argument edges and rejects malformed or forged plans. + +All ten new linked groups pass over 96 source cases. They cover every getter, +direct and pipeline calls, debug/release runtime profiles, source deletion, +ready/empty/inactive wrappers, recursive combinations, Unicode ownership, +operand order, exact and ancestor pending exceptions, first-failure order, +selector precedence, terminal-handler bypass, and compiled verify/property +suites with 100 trials. Fresh reference admission and literal output/trace +oracles agree. The logs are `target/native-recursive-linked-all.log` and +`target/native-recursive-linked-nominal.log`. + +All 494 types/typechecker/HIR/MIR/codegen library tests, 141 runtime tests, +62 object-emission tests, and 598 frontend fixtures pass. Native conformance +contains 318 supplemental tests; the fixed inventory denominator remains 207. +This closes the bounded recursive builtin producer gap, without selecting +broader requested-type admission, changed nominal/callable/interface casts, +or protected secret observation. Final frozen workspace and supported-host +distribution gates remain required for this revision. + +### Selected debug-event channel isolation + +The decided print/println diagnostic channel is implemented across reference +and native execution. One typed interpreter buffer preserves Trace, Breakpoint, +Print, and Println events with exact text; no newline is inserted between events. +Native DebugPrint emits the same staged bytes to stderr, apart from application +Stdout. Native differential gates compare only runtime events, excluding actual +frontend/comptime observations and private replay. + +Build, lowering, native artifact, test, and run outcomes retain preceding events +on success and failure. Compiler captures keep phase and test/file association; +baked values do not replay events and property shrinking remains private. CLI +agent rows encode known phase and kind and escape exact text instead of +classifying prefixes or leaking raw worker output. Human tools emit captured +debug bytes to diagnostic stderr before later errors. + +Failed comptime recovery records actual span/context attempts separately from +baked values, so repeated checked contexts and namespace initializer markers do +not duplicate failures or events. Failed values remain unavailable. Native +property replay errors retain the original suite streams when that attempt +returned captured process output and suppress private replay runtime streams in +the public error chain. Initial execution errors and timeouts retain their +existing typed error contract rather than inventing an original-suite record; +actual linker diagnostics remain available. + +Focused verification passes: 416 comptime tests, 83 driver library tests, +144 runtime tests, all 50 CLI unit/command tests, and all five new linked native +transport tests. The latter cover exact event boundaries and kind controls, +frontend/runtime separation, failed argument ownership cleanup, partial-print +terminal error adjacency, source deletion, compiled verify/property suites, +release E0362 plus argument checking, and preservation of an existing output. +The final focused logs are `target/native-debug-isolation-comptime-final.log`, +`target/native-debug-isolation-driver-final.log`, +`target/native-debug-isolation-cli-final.log`, and +`target/native-debug-isolation-native-transport-final.log`; the unchanged runtime +gate is in `target/native-debug-isolation-library-driver-check.log`. + +The frozen `b1cf8943` workspace passed +`cargo test --locked --workspace --no-fail-fast`, including all doc-tests, +323 supplemental native conformance tests, all 182 native object obligations, +598 frontend fixtures, and the complete fixed inventory gates. The log is +`target/native-workspace-b1cf8943.log`. This verifies that frozen revision; +the later JSON changes require their own workspace acceptance. + +The prior `3bc23e6a` revision passed its frozen workspace and doc-tests in +`target/native-workspace-3bc23e6a.log` and all four supported-host jobs in +[workflow run 36995400983](https://github.com/vycdev/jett/actions/runs/36995400983). +The `b1cf8943` supported-host +[workflow run 37002829148](https://github.com/vycdev/jett/actions/runs/37002829148) +passed all four Linux/Windows build/package and installed-package jobs. +The installed-package jobs cover clean relocation, execution after source +removal, and both debug/release profiles. This accepts the frozen debug +isolation revision on its supported hosts; later JSON source changes still +require their own workspace and supported-host acceptance. +The broad estimate remains about 85%; the fixed inventory denominator remains +207. Hidden-secret policy and other semantic obligations remain independent. + +### Qualified machine JSON and parser pipeline dispatch + +Native JSON source eligibility now matches the existing public admission +boundary: a state-qualified machine visits only its selected payload, while a +bare machine still visits every declared state. The unrelated state in the +regression schema contains an integer-keyed map; qualified ready-state JSON is +admitted, but bare and cached-state requests retain E0343. Public policy, +all-state reflection metadata, secret projection, and qualified builder-finish +checks are unchanged. + +Nested `state.fields` loops originating in `type.machine_states[T]()` receive +checked field-type and source-alias candidates from every declared state, +including canonical `Machine at state` requests. Source aliases in these +candidates belong to field TypeInfo; top-level aggregate aliases remain +non-matching probes with empty metadata. Runtime state metadata remains complete, +so an unselected field must not reach an unmatched reflected dispatch arm. +Direct active-state field loops retain their selected-payload candidates. +This compiler handoff repair is distinct from selected-state JSON eligibility. + +The private fallback decoder now uses the direct canonical condition +`type.name[T]() == "json.JsonTree"` for raw-tree copying, matching the +named decoder's existing checked static selection. An ordinary pure metadata +helper call had left the raw return body typechecked under unrelated T, +exposing E0311 for inactive callable, interface, and actor fields. Pure-call +folding does not control source validity. This narrow source repair keeps +the raw body out of those specializations without changing public JSON +eligibility, reflection metadata, or the existing typed wrapper paths. + +Completing those candidates exposed a private map decoder branch that returned +`map[string, Val]` for a non-string-keyed T. An alias-aware closed key guard now +limits typed string-map construction to exact string keys. Public non-string +map targets still fail E0343. A malformed qualified envelope naming another +state's unsupported map payload instead returns an unsupported-key failure +when its field decoder is reached, before the eventual qualified builder +mismatch. Required-field lookup and exact validation retain their order. +This bounded diagnostic-precedence tradeoff is recorded in the +[machine JSON contract](../../open_design/state_machine_json_contract.md); +it introduces no key cast, dummy value, early state gate, or global builder +change. Fresh reference characterization pins the actual first wrapped +error for empty and nonempty cached-state maps as +`cached.entries: JSON object maps require string keys, got int64`. Lenient +numeric entries reach the same key refusal; exact numeric entries retain +the earlier `entries: expected object, got number` validation error. Missing +entries still report `missing required field 'entries' for app.Session at ready.cached`. +The expanded direct/pipeline controls compare these exact error prefixes, +continued successful parsing, retained owners, and complete reference/native +streams. All 22 groups passed (323 filtered) in 175.92 seconds in +`target/native-json-qualified-driver-final.log`. + +Parser pipelines now record the declared `json.parse` or `json.parse_exact` +source facade from their checked `result[T, string]` payload using the existing +direct-call handoff mechanism and recursive eligibility predicate. Argument +checking, input ownership, pipeline order, step-local handlers, and intrinsic +identity are unchanged. An independent int64 pipeline witness exposes this +dispatch defect without requiring machine qualification. + +Fresh pre-change CLI witnesses checked and ran successfully through the +reference path while native lowering refused the generic JSON intrinsic. Bare +machine controls reported E0343. These characterization results identify the +defects; they are not validation of the new implementation or regression suite. + +The qualified-JSON repair suite contains 23 independent tests and 32 Jett fixtures. +It is intended to cover all four JSON operations in direct and pipeline forms, +qualified aliases in lists, wrong-state handling, lenient and exact extra fields, +borrowed-value preservation, primitive parser input clone/reread, debug and +release profiles, source deletion, output publication controls, and pure +selected-state serialization through explicit comptime, verify, and 100-trial +property suites. Additional controls exercise exact string-key aliases and +independent all-state reflection with field aliases, canonical ready/empty +qualifiers, and direct selected-field loops. Runtime fixtures use +capability-backed Stdout, with debug and frontend observations checked +separately. Missing entries in the wrong-state payload are now covered; +unknown or missing state tags, missing payloads, other missing required +fields, secrecy, and broader wrapper combinations remain outside this +suite's claim. One new group over three inactive callable/interface/actor +payloads checks all four direct JSON APIs for a valid selected ready state, +both runtime profiles, retained source owners, and source deletion. Those +cases do not admit the unsupported payloads themselves as JSON targets or +claim malformed-input behavior for them. + +Recorded phase checks pass: 40 codegen and 416 comptime library tests in +`target/native-json-qualified-compiler-phases.log`, plus 125 HIR, 79 MIR, +and 221 typechecker tests in `target/native-json-qualified-remaining-phases.log`. +The driver library batch reported 82 passes and one temporary capture-file +creation PermissionDenied failure; the exact failed test passed its isolated +retry (one pass, 82 filtered) in +`target/native-json-qualified-driver-permission-retry.log`. The original +batch is not described as an uninterrupted passing run. + +At this qualified-JSON checkpoint, the compiled supplemental corpus contains +346 tests, confirmed +by the final JSON family gate's 39 passes and 307 filtered in +`target/native-json-qualified-json-family-final.log` (438.34 seconds). +That gate includes all 23 new groups on the final source and 16 existing JSON +regressions; it is not 346 executed passes. The earlier 22-group checkpoint +and additional group's pass remain recorded separately in +`target/native-json-qualified-driver-final.log` and +`target/native-json-qualified-unselected-payloads-final.log`. +All 598 frontend fixtures pass on the repaired source +in `target/native-json-qualified-frontend-fixtures.log` (20.66 seconds). +The frozen `23ac34c4` complete workspace passes in +`target/native-workspace-23ac34c4.log`, including all 346 native conformance +tests (838.03 seconds), all 182 native object obligations, all 598 frontend +fixtures (22.88 seconds), and doc-tests. Its full driver library batch also +passes all 83 tests, separately from the earlier temporary-file retry. +The head and worktree remained unchanged throughout that run. Supported-host +acceptance for this revision remains pending in +[workflow run 37013565734](https://github.com/vycdev/jett/actions/runs/37013565734): +the Windows build/package job has passed; Linux and clean installed-package +acceptance are not yet complete. Earlier phase results do not validate later +source additions. +This slice supplements the unchanged 207-fixture inventory; the +broad estimate remains about 85%. + +### Qualified machine JSON inside optional, result, and map wrappers + +An additional 24 linked tests cover `optional[Session at ready]`, both arms of +`result[Session at ready, Session at ready]`, and `map[string, Session at ready]`. +Each wrapper crosses all four public JSON APIs with direct and pipeline calls. +The machine's unrelated cached state contains an unsupported integer-keyed map. +The controls exercise occupied and absent optionals, both result arms, populated +and empty maps, and retained original owners. Three additional bare-machine +controls keep E0343 for an empty optional, an inactive result arm, and an empty +map; runtime emptiness does not exempt a target from public type policy. + +All 27 new Jett fixtures passed frontend/reference preflight in +`target/native-json-qualified-wrapper-preflight/results.json`: the 24 valid +cases produced exact literal stdout and empty stderr, and the three invalid +cases reported E0343. All 24 linked tests then passed (346 filtered) in +`target/native-json-qualified-wrapper-linked.log` (190.12 seconds), using both +matching runtime profiles after source removal. The extended policy group +passes (one test, 369 filtered) in `target/native-json-qualified-wrapper-policy.log` +(7.93 seconds), preserving existing output artifacts in both profiles. +All 27 source-format checks and Rust formatting checks pass. + +This test-only extension registers 370 supplemental native tests. The frozen +346-test workspace result and these focused passes remain separate evidence; +a complete 370-test workspace and supported-host run have not yet been accepted. +Wrong-state wrapper errors, deeper wrapper combinations, aliases, secrets, and +pending values retain their independent audit obligations. The broad estimate +remains about 85%, and the fixed inventory remains 207 fixtures. + +### Stable ordinary-struct field local-view checkpoint + +The typed borrowed initializer now proves ordinary-struct field paths rather +than requiring the selected endpoint to equal its whole owner's type. Exact +source identity, field types, nominal refinement qualification, initializer +coverage, and original-function validation before native compaction remain +required. Alias storage is non-owning; dependent reads retain the real owner. +Copied inline-function tables discard origins belonging to another body. + +`target/native-stable-projected-view-final-phases.log` passes all 896 library +tests across codegen/comptime/HIR/MIR/typechecking. The final native alias gate +passes all 12 tests (366 filtered, 11.78 seconds) in +`target/native-stable-projected-view-final-linked.log`: eight linked cases, +mutable/temporary publication boundaries, and three existing broad alias tests. +Both runtime profiles execute after source deletion with exact output/status, +independent clone ownership, and later-failure cleanup. The initial unused-type +negative-test lookup was corrected to the actual type interner before the final +passing phase batch; it did not require a compiler behavior change. + +The final driver/frontend batch passes all 83 driver library tests and all 598 +frontend fixtures in `target/native-stable-projected-view-final-driver-frontend.log`. +The stale capture test now uses the existing mutable-root HIR boundary. The +Linux-only projection-loan gate now checks owner consumption after creating a +valid field alias; its zero-test Windows target is not an executed pass. Linux +CI remains required, and the matching Windows CLI source independently refuses +the owner change before archive lookup. + +This ordinary-struct checkpoint registered 378 native tests. Its later frozen +a0bf12df workspace passed all local gates, while supported-host acceptance +retains its separate revision-specific obligation. A qualified machine-field +alias was source-valid but native-refused before the exact-state follow-up +below. Neither this checkpoint nor the fixed inventory establishes full- +language parity; the planning estimate stays about 85% and denominator 207. + +### Exact qualified-machine field local-view focused acceptance + +The exact declared MachineState owner lookup is integrated with unchanged +source/nominal proof, persistent loans, non-owning slots and runtime ABI. +Malformed unused state/index/endpoint/root metadata and concrete generic +contexts pass the compiler gate: 261 tests (41 codegen, 134 HIR, 86 MIR) in +`target/native-machine-projected-view-phases.log`. All 83 driver library tests +pass in `target/native-machine-projected-view-driver-frontend.log`; all 598 +frontend fixtures pass in `target/native-machine-projected-view-frontend.log` +under the corrected `fixture_suite` target. + +The 25-test local-alias batch passes (366 filtered, 17.72 seconds) in +`target/native-machine-projected-view-pending-linked.log`; the typed-endpoint +case passes separately (one test, 391 filtered, 2.80 seconds) in +`target/native-machine-projected-view-typed-linked.log`. These are 26 distinct +executed tests, not one combined batch. Linked cases use matching runtime +profiles and execute after source removal, preserving original owners, clone independence, exact +pending/intermediate errors, depth-two traces and explicit joins, terminal +status/cleanup. Separate boundary cases prove before-publication owner-change +refusal without looking up the archive. Generic endpoint +modes and nominal/secret field identities are pinned by the final case. All 15 +machine Jett source-format checks and Rust formatting checks pass. Initial +source-only preflight and the corrected E0308 fixture handler are recorded in +[the scoped note](native_local_view_aliases.md#exact-state-qualified-machine-field-follow-up). + +Bare flow-narrowed machine origins, mutable backing chains, temporary roots, +projected writes, allocating conversions and owner consume/rebind/transition +after reachable alias creation remain outside this bounded native support. +The actual owner-change group reaches the existing typed ownership refusal +before archive lookup and preserves the output sentinel in both profiles. +No source loan-expiry or temporary-lifetime contract is selected. + +The previous frozen a0bf12df workspace passed with unchanged head/worktree +and exit zero at 16:01:32 UTC in `target/native-workspace-a0bf12df.log`: all 378 +native tests (785.23 seconds), all 182 object obligations (491.87 seconds), 598 +frontend fixtures (20.05 seconds), 83 driver library tests (75.05 seconds), and +the remaining workspace/doc-tests. That prior result does not validate later +machine source/test additions. + +The earlier a0f0c982 workflow +[37019114837](https://github.com/vycdev/jett/actions/runs/37019114837) passed all +four jobs. The a0bf12df workflow +[37027835546](https://github.com/vycdev/jett/actions/runs/37027835546) also passes +all four Linux GNU/Windows MSVC build/package and clean installed-package jobs. +These accepted predecessor revisions do not establish new-source platform +acceptance. + +### Current frozen revision and inferred JSON follow-up + +The exact-state machine repair is committed as `774b13e7`. Its complete frozen +workspace passes with unchanged source and exit zero at 17:12:39 UTC in +`target/native-workspace-774b13e7.log`, including all 392 native tests (858.92 +seconds), all 182 object obligations (the four-test object manifest gate passes +in 505.90 seconds), the remaining workspace targets and doc-tests. Its +[workflow 37035818971](https://github.com/vycdev/jett/actions/runs/37035818971) +now passes all four supported-host jobs. Broader owner/endpoint +combinations and independent language surfaces remain audit obligations. + +The applied inferred JSON serializer repair passes 14 new linked/publication +tests (8.09 seconds), 948 crossed compiler library tests, 83 driver library +tests, all 598 frontend fixtures, and 17 Jett format checks. Both profiles pin +source-deleted execution, pending failure/cleanup, pure/comptime/verify/property, +public policy and owner rereads. Its actual captures, exact logs and boundaries +are centralized in [the scoped note](native_json_uninhabited_slots.md). +The serializer revision `05494ed4` passes its complete frozen workspace with +unchanged source, clean start/end and exit zero at 18:03:56 UTC in +`target/native-workspace-05494ed4.log` and its JSON summary: all 406 native tests +(873.42 seconds), all 182 object obligations (four-test manifest gate, 508.07 +seconds), 83 driver library tests (65.72 seconds), all 598 frontend fixtures +(20.69 seconds), remaining workspace targets and doc-tests. Its +[workflow 37041625704](https://github.com/vycdev/jett/actions/runs/37041625704) +passes all four Linux GNU/Windows MSVC build/package and clean installed-package +jobs. These accepted serializer results do not validate subsequent parser or +bitfield changes. + +The applied parser source-selector repair passes all 19 direct, pipeline and +recursive linked tests together (14.08 seconds, 406 filtered), pinning actual +valid, occupied-slot and malformed-input lines in both profiles after source +removal. The metadata unit passes once; the crossed library gate passes 949 +tests, followed by 83 driver library tests, all 598 frontend fixtures and all +19 new source format checks. +The parser revision `08f9f7e7` passes its complete frozen workspace with +unchanged head, clean start/end and exit zero at +`2026-10-02T19:49:44.2598850Z`, recorded in +`target/native-workspace-08f9f7e7.log` and its JSON summary: all 425 native +tests (878.50 seconds), all 182 object obligations (four-test manifest gate, +506.38 seconds), 83 driver library tests (70.43 seconds), all 598 frontend +fixtures (21.95 seconds), remaining workspace targets and doc-tests. Its +[workflow 37053539878](https://github.com/vycdev/jett/actions/runs/37053539878) +passes all four Linux GNU/Windows MSVC build/package and relocated installed- +package jobs (last completion `2026-10-02T20:19:29Z`). This accepted predecessor +does not validate the later bitfield source changes. Exact parser behavior +and focused logs are in [the parser contract](native_json_uninhabited_parse_slots.md). +The objective remains whole-language native parity; the about-85% planning +estimate and fixed 207-fixture denominator do not measure completion of +these focused slices. + + +### Bitfield field local views: focused acceptance + +The declaration-backed Bitfield Field step now preserves exact semantic field +type, canonical zero-based identity and the existing immutable root/path proof. +No MIR, runtime ABI, payload schema or lifetime policy changes are required. +Both focused HIR tests pass (134 filtered) in +`target/native-bitfield-local-view-hir.log`. All seven new linked cases pass +together in 3.89 seconds (425 filtered) in +`target/native-bitfield-local-view-linked.log`, using matching profiles and +source removal with exact application bytes, status, stderr and typed traces. +Pending whole-owner access fails before the after-binding marker with status +71; cleanup status 72 cannot pass. Explicit payload-copy joins preserve the +original depth-two payload, and release omits trace events. + +The crossed compiler gate passes 951 library tests (41 codegen, 416 comptime, +136 HIR, 86 MIR, 48 resolve and 224 typecheck) in +`target/native-bitfield-local-view-phases.log`. All 83 driver library tests pass +(86.96 seconds) in `target/native-bitfield-local-view-driver.log`, and all 598 +frontend fixtures pass (21.88 seconds) in +`target/native-bitfield-local-view-frontend.log`. All seven new source-format +checks pass in `target/native-bitfield-local-view-format.json`; Rust formatting +and diff checks pass. + +The bitfield checkpoint registers 432 supplemental tests. The later clean +`cb617ecf` workspace below includes this repair and passes all 182 object +obligations. Supported-host jobs for both owner-view revisions now pass, as +recorded below. Initial source characterization, +exact qualification/ownership limits and required gates are in +[the bitfield contract](native_bitfield_projected_local_views.md). The accepted +08f9f7e7 result above is predecessor evidence, not new-head acceptance. The +about-85% coarse estimate and fixed 207 inventory remain unchanged. + + +### Direct secret aggregate local views: focused acceptance and open observation + +The selected exact one-Secret owner/endpoint proof is integrated. It retains +declared nothing/already-secret nominal exceptions, root/index/type anchoring, +immutable loans, non-owning storage and existing pending guards. Three focused +HIR tests pass (136 filtered, 0.00 seconds) in +`target/native-secret-owner-local-view-hir.log`. The warning-interrupted first +invocation produced no test result and is not acceptance evidence. + +All seven linked cases pass together (2.82 seconds, 432 filtered) in +`target/native-secret-owner-local-view-linked.log`, with matching profiles and +source-deleted execution. Six pin successful outcome parity. The pending case +separately pins the raw reference message and native `[redacted]` error, matching +`before:secret\n`, status 71 and cleanup. This preserves the already recorded +[open observation boundary](../../open_design/debug_print_hidden_secrets.md); +it neither weakens native redaction nor claims diagnostic parity. +The initial six-pass/one-failure attempt (4.56 seconds) is retained in +`target/native-secret-owner-local-view-linked-initial-mismatch.log`. Its mistaken +shared error oracle was corrected to the existing separate-backend contract; +no source policy or interpreter behavior was changed. + +All 954 crossed compiler library tests pass (41 codegen, 416 comptime, 139 HIR, +86 MIR, 48 resolve, 224 typecheck) in +`target/native-secret-owner-local-view-phases.log`. +All 83 driver library tests pass (66.62 seconds) in +`target/native-secret-owner-local-view-driver.log`; all 598 frontend fixtures +pass (22.51 seconds) in `target/native-secret-owner-local-view-frontend.log`. +All seven source-format checks, Rust formatting and diff checks pass in +`target/native-secret-owner-local-view-format.json`. + +The corpus registers 439 supplemental tests. The exact `cb617ecf` workspace +passes with clean start/end and unchanged head at +`2026-10-02T20:55:57.0941072Z`, recorded in +`target/native-workspace-cb617ecf.log` and its JSON summary. All 439 native +conformance tests pass (905.04 seconds), all 182 object obligations pass in the +four-test manifest gate (516.91 seconds), and all 83 driver library tests +(91.44 seconds), 598 frontend fixtures (19.08 seconds), remaining workspace +targets and doc-tests pass. The wrapper reports exit zero and 92 successful +result groups. This accepts both owner-view repairs at that exact source +revision; it does not settle the preserved observation-policy boundary. +The documentation-only `194ce065` revision has identical compiler/runtime/test +sources and passes all four jobs in +[workflow 37064035604](https://github.com/vycdev/jett/actions/runs/37064035604): +Windows MSVC static-CRT and Linux GNU build/package jobs, followed by both +relocated installed-package jobs (last completion `2026-10-02T22:10:29Z`). +The standalone `cb617ecf` run 37060490263 was superseded and cancelled before +starting any job; that cancelled run is not acceptance evidence. The bitfield +`5d919052` revision separately passes all four jobs in +[workflow 37058389987](https://github.com/vycdev/jett/actions/runs/37058389987) +(last completion `2026-10-02T21:16:06Z`). Exact selected rules and actual seven-source +characterization are in +[the secret-owner contract](native_secret_owner_projected_local_views.md). +The about-85% estimate, fixed 207 inventory and whole-language 100% objective +remain unchanged; the independent MIR staging repair is not applied. + + +### Uninhabited-input callback descriptors: focused acceptance + +An inferred empty-list callback remains an inhabited function descriptor even +when a non-capture input cannot exist. Native emission now retains its exact +captures, label, cloning, pending state and cleanup with a nonzero trap entry; +it creates no Never argument/result carrier and emits no original body effect. +Original descriptor metadata and all original local/debug types are checked +before preparation. Named enclosing functions compact only unused frame slots +across all original blocks and typed dependencies; surviving callable Never +parameters, locals and invocations remain invalid. + +All 966 compiler library tests pass (52 codegen, 416 comptime, 139 HIR, 87 MIR, +48 resolve and 224 typecheck) in `target/native-uninhabited-callback-phases.log`. +All nine linked regressions pass together (3.65 seconds, 439 filtered) in +`target/native-uninhabited-callback-linked.log`, using matching profiles and +source deletion. Direct/returned/captured descriptors, owned cloning, pending +joins, Never-result identity, loop/equality bodies, invoked integer controls and +terminal failure cleanup retain exact outcomes. Nine Jett source-format checks +and Rust formatting pass. The corpus registers 448 supplemental cases. +All 83 driver library tests pass (69.44 seconds) in +`target/native-uninhabited-callback-driver.log`; all 598 frontend fixtures pass +(24.60 seconds) in `target/native-uninhabited-callback-frontend.log`. + +The first isolated unit attempts failed before MIR because their loader does +not load the stdlib facade. Declared source helpers corrected those test inputs; +the driver cases retain full stdlib evaluation. The actual factory diagnostic +then exposed unused child formals in the enclosing frame table. Initial logs, +pre-change captures, exact bounds and remaining gates are in +[the descriptor contract](native_uninhabited_callback_descriptors.md). +The subsequently repaired `90d82701` full workspace/object and supported-host +results are recorded below. The earlier 439-case predecessor alone could not +supply that proof. The whole-language objective remains 100%, the fixed +inventory remains 207, and the broad planning estimate stays about 85%. + +## Original function metadata in absent sum arms + +The clean `d14b830b` full native suite reports 446 passes and two existing +sum-arm failures (941.02 seconds). Original preflight rejects a closure's exact +Never result before checked impossible-arm preparation can remove the closure. +The remaining object/workspace targets were stopped after that complete native +suite failed; `target/native-workspace-d14b830b.json` records exit -1, unchanged +clean head and the stop reason. No complete-workspace or object acceptance is +claimed for this attempt. + +OriginalMetadata type validation now permits that exact function-result +metadata recursively while preserving view-schema and inhabited child checks. +NativeValue classification remains strict, and a return-only Never signature +still grants no descriptor authority or runtime capture/value representation. +The three new codegen units pin all three absent optional/result arms, omitted +closure symbols, nested metadata validity, malformed child/result/view schemas +in both local and debug types, and surviving return-only rejection. + +All 969 compiler library tests pass in +`target/native-original-function-metadata-phases.log`. The two previously failing +linked tests pass (48.51 seconds) in +`target/native-original-function-metadata-linked-sum-arms.log`, retaining both +profiles, pending diagnostics, source deletion and native verify/property +execution. All nine callback cases pass again (3.82 seconds) in +`target/native-original-function-metadata-linked-callbacks.log`. Exact repaired +head `90d82701` subsequently passes its clean complete workspace and all 182 +object obligations. Its unchanged-head log and JSON are +`target/native-workspace-90d82701.log` and `.json`: 448 native cases pass in +944.48 seconds; the four-test object manifest gate passes in 553.77 seconds; +all 83 driver library tests, 598 frontend fixtures, remaining targets and +doc-tests pass. Exit zero and 92 successful groups complete at +`2026-10-03T08:31:51.9056689Z`. Exact-head supported-host workflow 37108099164 +passes all four Windows/Linux build/package and relocated installed jobs, last +completing `2026-10-03T09:03:23Z`. The corpus is 448, the fixed inventory 207, and the +planning estimate about 85%. + +## Internal call-view stages across handlers + +Original explicit borrowed arguments use typed MIR Begin/End scopes when the +existing cloneable snapshots cannot preserve the borrow. Exact checked local +and field origins, pending receiver order, normal owned temporary storage and +post-operation owner use are covered; source alias lifetime rules and ownership +changes during later operands remain independent. The focused execution record +and complete acceptance obligations are in +[the call-view contract](native_scoped_call_view_staging.md). This is additional +native implementation coverage, not whole-language completion or a denominator +change. + +## Latent nested and secret-result callback metadata + +The isolated follow-up preserves exact Never-input descriptor authority while +checking latent nested function bodies and direct Secret[Never] result metadata. +Validated target/signature/mode/capture edges precede cycle-safe body traversal; +no nested environment, native callable root, runtime carrier or ABI is created. +Live return-only callbacks, created Never captures, phantom values, malformed +metadata and opaque Resource layouts remain rejected. Exact selected proof and +centralized execution evidence are in +[the descriptor note](native_uninhabited_callback_descriptors.md#latent-nested-closures-and-direct-secret-results). + +All 63 codegen tests and 27 linked regressions pass with unchanged source hashes. +The broader compiler run passes 1052 tests (989 library units plus 63 integration +tests); its wrapper's incorrect expected-count guard is not a compiler failure. +The 83 driver, 598 frontend and eight backend-lowering gates also pass with +unchanged sources. Exact clean `72237c4a` subsequently passes its complete +464-case workspace, all 182 object obligations and 92 successful result groups; +its supported-host workflow remains in progress. Independently, `55027914` +passes all four supported-host jobs as well as its complete local workspace. +Exact-head acceptance records are centralized in the descriptor note above. +The fixed inventory remains 207, object obligations 182, broad estimate about +85%, and whole-language goal 100%. + +## Resource type-only kind tags + +The checked builtin enum schema and shared reflection mapper now agree on the +accepted opaque `resource_type` tag. Three regressions cover source comparisons, +reference type metadata with empty shapes, and a native object reached through +an ordinary project caller. The affected 800 compiler tests and 598 frontend +fixtures pass with unchanged source hashes. This adds no live Resource carrier, +provider, source constructor or cleanup behavior. Clean `616b51b9` subsequently +passes its full workspace; supported-host acceptance for that head remains +separate. Exact evidence and remaining +lifecycle prerequisites are in [the active note](native_resource_kind_tags.md). + +## Checked private resource-hook identities + +The identity-only prerequisite creates associated private Function DefIds through +ordinary resource declaration resolution and checks complete function types and +ownership modes against the exact nominal Resource definition. Stdlib FileId +and independent loader origin, ordinary privacy/order/duplicates and unused +metadata validation are required. Production catalogs stay empty; no source +constructor, provider operation, runtime Resource value or native carrier is +created. See [the selected contract and scope](native_resource_hook_identity.md). + +The isolated final focused run passes six resolver, seven checker and one +span-collision test with ten unchanged Rust source hashes. Exact results/logs +are retained in `target/native-resource-hook-focused-after-diagnostic.json` and +its referenced per-gate logs. The earlier missing-`mut` compile failure and +source-body diagnostic expectation correction are not successful gates. The +broader 1037 compiler library tests, 63 codegen object integration tests, 83 +driver tests, 598 frontend fixtures and eight backend-lowering tests also pass +with unchanged sources in `target/native-resource-hook-phases.json`. The +subsequent clean `616b51b9` full workspace passes, as recorded below; its +supported-host run remains in progress. No reference or native Resource +lifecycle was executed. This is checked identity proof, not a +resource-provider parity row closure or inventory/percentage increase. + + +## Resource static ownership boundaries + +The accepted local predecessor is exact clean `616b51b9`: its frozen full +workspace exits zero with all 92 result groups, 464 supplemental native cases +(940.26 seconds), all 182 object obligations (four manifest groups, 534.07 +seconds), remaining workspace targets and doc tests passing. Start/end HEAD and +cleanliness agree in `target/native-workspace-616b51b9.json`; the run ends at +`2026-10-03T12:11:54.8924860Z`. The older `62de6505` supported-host workflow +[37118139645](https://github.com/vycdev/jett/actions/runs/37118139645) succeeds in +all four Windows/Linux build and installed relocation jobs. The exact +`616b51b9` workflow 37119573097 is still in progress; predecessor platform +acceptance does not transfer to this head or the new guards. + +The new static guards refuse recursive Resource-copy acquisitions (E0364), +known Resource views stored in owning payloads (E0401), and exact Resource-root +printing/value reflection (E0300). Exact Resource-bearing machine transition +sources reuse existing owned-argument checks. Safe views, unrelated copyable +fields, actual owned payload transfers, empty carriers and type-only metadata +remain admitted. Nested printing, hidden-Secret observation and other ownership +paths remain separate work; no runtime/provider/native Resource value is enabled. + +All 36 focused groups pass, including 21 new source groups, in +`target/native-resource-source-guards-focused-after-diagnostic.json`. The original +20 sources and additional 14 payload sources all parse/resolve and are checked +in both profiles with stable inputs. Their reports show the intended refusals +and retained controls; these probes do not evaluate comptime/property bodies, +install providers or execute native Resource code. See the +[complete static scope and source evidence](native_resource_ownership_boundaries.md). + +The broader wrapper passes 1058 library units (254 typechecker units), 63 +codegen object integration tests (1.11 seconds), 83 driver tests (69.79 seconds), +598 frontend fixtures (22.13 seconds) and eight backend-lowering tests. All three +source hashes remain unchanged; `target/native-resource-source-guards-phases.json` +ends with exit zero at `2026-10-03T12:28:58.9045108Z`. New complete-workspace and +supported-host acceptance remain pending. This prerequisite adds no linked +Resource case or object obligation: the about-85% estimate, fixed 207 inventory, +182 obligations, 464 supplemental cases and whole-language 100% objective stay +unchanged. + + +## Immutable checked Resource program handoff + +Exact clean `b7180bd9` completes its frozen full workspace with exit zero and +all 92 successful result groups. It passes 464 supplemental native cases +(958.70 seconds), all 182 object obligations (four-test manifest gate, 650.63 +seconds), remaining workspace targets and doc tests. Start/end HEAD and +cleanliness match in `target/native-workspace-b7180bd9.json`; the actual session +1952 is terminal with exit zero and ends `2026-10-03T13:18:31.4769949Z`. +Its supported-host workflow +[37123835396](https://github.com/vycdev/jett/actions/runs/37123835396) now succeeds +in all four Windows/Linux build and installed relocation jobs. The last Windows +installed job ends `2026-10-03T14:14:19Z`; the exact-head receipt is retained in +`target/native-platform-b7180bd9.json`. Earlier in-progress observations are +superseded by that result, without transferring acceptance to exact94. + +Exact predecessor `616b51b9` now also has supported-host acceptance: workflow +[37119573097](https://github.com/vycdev/jett/actions/runs/37119573097) succeeds in +both Windows/Linux build and installed relocated-package jobs. The final +Windows installed job completes `2026-10-03T13:14:20Z`. This supersedes that +run's earlier in-progress observations; it does not validate the new handoff. + +`CheckedResourceProgram::prepare` owns its ParseResult and derives resolution +and checking internally with explicit loader origins/private catalog input. +Only immutable source/resolver/checker/interner observations are exposed. Error +diagnostics from each source phase prevent sealing, and complete unused hook +identity/signature checks still apply. Public hook validation adds independent +checker-error refusal while ordinary checking keeps inspectable source repair +diagnostics. No private operation executes or Resource carrier is installed. + +The root-owned focused gate passes all **43 Resource groups**, including seven +new source-driven snapshot groups and the strengthened invalid-body phase-gate +assertion. The broader **454 library tests** pass: 139 HIR, 54 resolver and 261 +typechecker. Five compiler hashes remain unchanged in +`target/native-resource-checked-program-focused.json` and +`target/native-resource-checked-program-phases.json`, both exit zero. The +[scope and evidence note](native_resource_checked_program.md) preserves the +pre-Rust contract and separates this handoff from driver retention, reached-hook +runtime eligibility, call ownership, cleanup and native Resource admission. + +Full-workspace and supported-host validation for this new change remain pending. +The fixed 207 inventory, 182 object obligations and 464 supplemental native +cases are unchanged; the about-85% estimate is still coarse planning and the +whole-language native-codegen objective remains 100%. + + +## Retained reference preparation + +The isolated reference follow-up over exact `94bb6b2e` retains a private +`Arc` and primary FileId0 main span/namespace alongside +unchanged public BuildResult observations. Required evaluation uses that same +merged AST and checked facts. Runtime gates compilation and missing-snapshot +failures before providers, registers the saved module once and performs no +post-preparation read/parse/rediscovery. The macOS preliminary graphics-thread +read remains scoped outside this guarantee. Through-failure diagnostics preserve +earlier phase warnings and typed metadata causes. This enables no live Resource, +provider, callable descriptor authority or native ABI. See +[the selected contract and scope](native_prepared_reference_driver.md). + +All **46 focused Resource groups** and **four new driver groups** pass in +`target/native-prepared-reference-driver-focused.json`, ending +`2026-10-03T14:12:41.4871736Z` with exit zero and both formatted source hashes +unchanged. These include three new warning/cause groups and four driver controls +for deleted or mutated sources, primary selection despite a support main, +once-only frontend/runtime observations, and failure before provider setup. +All **544 driver/HIR/resolver/typechecker tests** also pass: 87 driver, 139 HIR, +54 resolver and 264 typechecker. `target/native-prepared-reference-driver-phases.json` +ends with exit zero at `2026-10-03T14:15:36.7702211Z`; both source hashes remain +unchanged. All **eight backend-lowering regressions** (198.73 seconds), including +the fixed inventory lowering check, and **598 frontend fixtures** (24.41 seconds) +pass in `target/native-prepared-reference-driver-frontend.json`, ending +`2026-10-03T14:20:34.8384468Z` with exit zero and the same two source hashes. +New full-workspace and supported-host acceptance remain pending. + +Exact94's frozen full workspace is **not accepted**: it ends with Cargo exit101, +91 successful result groups and stable clean HEAD in +`target/native-workspace-94bb6b2e.json` at `2026-10-03T14:10:25.5278499Z`. +Its native suite passes 463 cases and fails one NamedTempFile.reopen operation +with PermissionDenied. All four object-manifest tests pass (562.05 seconds), +but that does not make the workspace successful. The affected exact94 native +case passes its separate rerun (one passed, 463 filtered, 7.98 seconds) under +elevated execution; this is a changed permission context, not acceptance of the +failed whole-workspace run. The original failure remains evidence. + +The last accepted local predecessor remains `b7180bd9`, with its 464 native +cases, 182 object obligations and 92 result groups recorded above. The same b718 head also has all-four supported-host acceptance, recorded above; +616 remains historical. These receipts do not transfer to exact94 or this +follow-up. The exact94 workflow 37126837754 is still in progress. No linked native case or +object obligation is added: fixed 207 inventory/182 obligations, 464 supplemental cases, about-85% planning +and the whole-language 100% goal remain unchanged. + +## Typed caller ownership integration + +The isolated caller-ownership change over `a9a6cd63` implements Rule24 caller +disposition and Rule25 ordinary-data observation. Sealed source-call facts rejoin +exact declarations, argument permutations and raw/physical types through HIR, +MIR and native validation. Generated calls, temporary staging, viewed loops and +handled defaults carry separate checked proofs. The +[implementation contract](native_call_ownership_facts.md) records the scope. + +The earlier formatted caller-core checkpoint passed 304 checker, 188 HIR, +196 MIR, 70 backend and 63 object-emission tests, plus the MIR doctest. Focused +handled-producer and builder-generation checks passed 66 MIR acquisition groups, +seven generation groups, three native emission groups and both original native +regressions. All seven failures of the historical 472/479 run subsequently +passed their individual replays. + +The complete Windows native suite now passes **480/480** at exact committed +`a63f891016fac7dc50fffdde33be8b0ce45c655f`. Root terminal `06b7f8` and +`target/native480-after-owner-generation/receipt.json` record zero failures and +stable HEAD/tracked sources, ending `2026-10-04T14:20:12.987780+00:00` +(2320.39 seconds). Resource candidate changes are absent from that run. +This is complete native-corpus acceptance for that caller head, not whole-language +completion or new current full-workspace/platform acceptance. Fixed lowering +remains 182 fixtures; a separate earlier stable recovery scan passed all 182. +Current 182-object, workspace and supported-host receipts remain required. + +The accepted unchanged a9 baseline passed 92 workspace result groups, 464 native +cases and all 182 object obligations. Workflow 37130142896 passed all four +Windows/Linux build and installed jobs. Those historical receipts do not transfer +to the Resource candidate. The fixed inventory remains 207 with 182 object +obligations and the rough feature estimate stays 85%. Async/actor lifetimes, +broader caller-generation/capture/reflection cases and Resource backend/provider +execution remain in the full 100% objective. + +## Resource reference lifecycle execution + +The private reference Resource implementation over a63 passes all **36 focused +tests** in `target/native-resource-after-checked-call-relay-focused.log` +(root terminal `923450`; zero failures, 418 filtered, 0.07 seconds). +The suite includes exact ordinary/mutual declaration identity, retained source +registration, real both-profile Source construct/borrow/move/close and reverse +cleanup, occupied returns, indirect close, later lexical actual failure, +ordinary closure return7, exact terminal list-kernel failure and mixed cleanup +failure precedence. Provider events and zero live custody/registry entries are +asserted before teardown. Closed descriptor/absence and required-worker purpose +controls do not invoke a runtime provider. The +[detailed reference scope](native_resource_hook_identity.md#reference-lifecycle-execution) +keeps runtime eligibility, custody and catalog authority separate. + +These are private reference tests with a compiler-test-only scripted provider, +not native Resource execution. Production catalogs/providers remain disabled; +no shipped Resource API or native hook/carrier/drop path is admitted. Whole +scoped/reflected/indirect/worker and aggregate ownership breadth, real providers, +async/actor cancellation and late-completion paths remain pending. Broad library, +whole-workspace and supported-host acceptance of the Resource candidate are +also pending. The 36 tests add no native inventory row and do not change the +480-case caller receipt, 207/182 fixed denominators or about-85% estimate. + +## Checked Resource reference transport follow-up + +The later scoped/required/generic, pipeline and mutable-assignment transport +passes **83/83** focused reference groups (`61a351`) in +`target/native-resource-concrete-intrinsic-pipeline-syntax-fixed-focused.log`. +Exact original region/body/step/statement proofs survive nested entry and restore +metadata/cursors. Concrete intrinsic metadata preserves generic substitutions +and alias names; copied source packets and foreign contexts remain refused. +The complete formatted gate passes **1,186/1,186** (501 Comptime, 87 driver, 598 frontend; `4ba85c`) in `target/native-resource-transport-formatted-property-fixed-broad.log`. Pipeline actuals retain lexical order, and assignment replacement retains the +previous owner until RHS succeeds and cleanup/publication complete. + +The prior 36/43 receipts remain historical. The foundation `7157f564` +[supported-host run](https://github.com/vycdev/jett/actions/runs/37217759157) passed +all four jobs and excludes this follow-up and custody core `92421063`. These reference +controls do not execute native Resource values. The 480-case native caller receipt, +fixed 207/182 inventory and approximate 85% estimate keep their separate scopes; +full providers, aggregates, capture/reflection/refinement breadth and +async/actor/cancellation native execution remain whole-goal requirements. + +## Checked Resource HIR/MIR handoff + +The original checked Resource manifest and exact Source hook calls now survive +HIR-to-MIR lowering, including unused hook validation and lexical argument +order. The five focused Source groups pass (`852dfc`). The formatted complete +phase gate passes **522/522**: 191 HIR, 198 MIR, 70 codegen and 63 object-emission +tests (`961b04`, `target/native-resource-manifest-metadata-fixed-phase.log`). +Earlier foreign nested-type rejection is pinned at manifest validation, with +the lower native type gate still tested independently. + +This is checked metadata preservation. Native Resource custody operations, +ownership/drop CFG, registration/ABI and linked Source execution remain pending. +These phase receipts do not extend the separate 480-case native caller receipt +or foundation-only platform acceptance. The fixed 207/182 inventory, approximate +85% feature estimate and whole-language 100% objective remain unchanged. + +## Native Resource registration protocol + +The bounded private runtime metadata issuer now validates every layout row and +issues context-bound nominal kind/slot metadata without creating a provider, +payload, grant, owner or loan. Exact guarded application and independent +byte/API review precede the initial **9/9** registration replay (`f753aa`). A +separate containing-function signature consistency correction adds an unused +Return-frame refusal control. The formatted full runtime passes **167/167**, +including ten registration and thirteen core custody controls (`56e7ac`, +`target/native-resource-registration-formatted-library.log`). + +These are runtime Rust protocol tests. The new metadata issuer has no connected +native Source ABI or provider, and the compiler still refuses Resource execution +without the dedicated ownership/drop plan. They do not enlarge the separate +480-case native caller acceptance, fixed 207/182 inventory or approximate 85% +estimate. Matching native Resource linked execution in both profiles, then full +providers, aggregates and concurrency breadth, remain required. + +## Source-authenticated ownership handoff + +The original checked HIR/type archive and constructor-emitted MIR witness now +support a separate `ResourceOwnershipPlan`. It records exact scopes, operation +and provisional-return frames, owner holders, incoming views, bounded loans, +transfers, occupied sum extraction and cleanup obligations. Original/current +Source call associations preserve bare owned Resource-to-view custody without +creating ordinary owning operand claims. Copied queries, changed call sites, +operands, Source certificates, evaluation order and nominal type meanings are +refused. Unnamed Resource-bearing intermediate expressions retain their witness. +Canonical block/local compaction retains exact current associations. + +All **12 focused ownership groups** pass (`155203`, +`target/native-resource-ownership-associated-focused.log`). The Source-derived +missing nominal-table regression passes (`fe7b0a`), returning the precise error +without a panic. The original nominal fixture retains both field-corruption +oracles after copying the complete original nominal tables and selecting its +actual Source Holder type. The malformed-type phase controls exposed a validation +ordering bug; valid type/invocation metadata is now required before custody +queries, preserving the exact manifest diagnostic and independent scalar refusals. + +The complete formatted affected gate passes **535/535** (192 HIR, 210 MIR, +70 codegen, 63 object-emission), plus 32 type-library tests and one MIR doctest +(`3c6a8a`, `target/native-resource-ownership-valid-metadata-phase.log`). Earlier +failed runs remain diagnostic evidence. This is initial compiler ownership +analysis: connected native frames, provider/ABI operations, operational cleanup, +handled actual normalization, complete aggregates/reflection and linked binaries +executed after source deletion remain required. The separate 480-case caller +receipt, fixed 207/182 inventory and approximate 85% estimate are unchanged. + +The broader driver/frontend gate also passes **693/693**: 87 driver library, +8 backend-lowering tests (including all 182 run-pass lowering obligations) and +598 frontend fixtures (`450ea4`, +`target/native-resource-ownership-driver-frontend.log`). The previous registration +commit's Windows CI job failed its CLI native-setup capture test with a main-thread +stack overflow; that separate platform regression is under investigation and is +not represented as current ownership or whole-workspace acceptance. + +## Native context adapter and custody operations + +The private runtime adapter connects the existing authenticated stationary +context lease, bounded layout issuer, registry and custody core. Exact current +attempt, frame, owner, loan, prepared hook and Resource sum records retain the +non-cloneable core tokens. Ordinary and Resource lookup IDs share one checked +monotonic allocator. Installation publishes a scripted provider and paired +ordinary Network grant transactionally after preflight; that provider is test +only. Production state starts absent with disabled providers and no new exported +Resource leaf. + +Real core operations now cover root entry, Operation frames, direct/descriptor +hooks, acquisition, moves, bounded borrowing, close/drop, occupied sums, staged +replacement and protected reverse cleanup. Completion preserves the observed +body status verbatim and separately selects cleanup, host panic and current +ordinary/Resource body failure. Ordinary FailureTake is neither invoked nor +reset by the adapter. Refused transfers retain the old owner; a cleanup fault +cannot publish success. Borrow domain Fail stays ordinary Result data. + +Initial compilation found three private API/field-name integration errors. The +first runnable focused check passed 13/14 and exposed string-only cleanup being +used for an ordinary borrow Result. The bridge now uses the existing recursive +native destructor, and an additional real-provider BorrowFail control checks +sum/string retirement, duplicate-drop refusal, clean completion and exact events. +All **15/15 focused groups** pass (`fd8577`, +`target/native-resource-state-adapter-companion-fixed-focused.log`). After +formatting, the full runtime package passes **182 unit + 13 integration tests** +(`96d995`, `target/native-resource-state-adapter-formatted-runtime.log`). The +earlier compile/failure logs remain diagnostic evidence. + +These are runtime host-protocol controls, not linked Source execution. Exact +Source child Scope/Return activation, resident incoming views, connected C leaves +and emission, operational compiler cleanup, the matched archive and linked +source-deleted binaries in both profiles remain required. The adapter-head Windows +CLI setup-capture stack overflow is recorded separately; the later helper +repair below supersedes that pending local regression. Broader +aggregate/reflection/refinement, provider and concurrency coverage remains part +of the full goal. Native caller 480/480, the fixed 207/182 inventory and the +approximate **85%** feature estimate retain their separate scopes. + +## Windows lowering stack repair + +The unchanged CLI native-setup capture test reproduced a main-thread stack +overflow at Resource ownership and adapter heads. The adapter-head Windows CI +also completed the native conformance suite with **480/480 passing** before the +workflow failed its separate CLI capture target (`b32d80`, workflow +`37244085637`). This is current adapter-head supported native execution evidence; +it still includes no native Resource lifecycle case. + +The repair extracts the existing heavy HIR statement/equality/inline-function +branches and MIR expression/per-actual branches into private helpers. Original +Source facts, Resource call capture, preflight, lexical order, shared call-view +scope, rollback, spans, diagnostics and context restoration remain at the same +operations. Source fixtures, assertions, snapshots, launcher order and stack +sizes are unchanged. Exact guarded application reconstructs both files and all +39 hunks (`3a8820`); the independent reconstruction record agrees. + +The original regression now passes **1/1** (`285533`), and the formatted full +CLI capture target passes **9/9** (`508636`). The formatted phase gate passes +**535/535** HIR/MIR/codegen/object checks plus 32 type tests and the MIR doctest +(`f8fe67`). The broader gate passes **693/693**: 87 driver library, 8 lowering +checks including all 182 run-pass fixtures, and 598 frontend fixtures (`647ede`, +`target/native-windows-stack-decomposition-driver-fixtures.log`). An initial +command used the nonexistent test target frontend; no driver test ran in that +attempt. The corrected fixture_suite command supplies the actual receipt. + +Actual Debug assembly measurement (`779571`, +`target/native-windows-stack-decomposition-frame-measurements.json`) records +MIR lower_value decreasing from 169,448 to 15,272 bytes and the ordered-call +loop from 73,736 to 4,504 bytes. Its separate per-actual helper uses 70,104 bytes. +HIR lower_expression decreases from 117,624 to 106,200 bytes and lower_statement +from 108,552 to 100,216 bytes. These are individual function-frame measurements, +not a whole-program stack limit or proof of every recursion shape. + +The local platform regression is repaired. Whole-head supported-host CI and +connected Resource Source ABI/emission/cleanup and linked lifecycle execution +remain required. The fixed 207/182 inventory and approximate 85% coverage +estimate remain unchanged; runtime/metadata checks do not supply the missing +Resource native execution. + +## Resource execution family and layout prerequisite + +Resource-free original direct Source callers now belong to the authenticated +Resource execution family. Fresh whole-program ownership plans project exact +formal facts, sum shell roles, Scope/Return distinctions and the actual +driver-selected entry into deterministic immutable v2 layout metadata. The +readonly object accessor emits no Source body or provider execution. + +Eight focused groups pass in both compiler profiles. The formatted compiler +gate passes **543/543** plus the MIR doctest (`fc3f02`); the final Return +projection passes all **74 codegen and 63 object checks** (`45ddfe`). The driver +gate passes **693/693** (`b9de51`). Corrections preserve the existing owned +runtime-capability entry rule and private constructor authority. See the +[implementation receipt](native_resource_hook_identity.md#resource-execution-family-and-emitted-layout). + +Native Resource Source execution remains unaccepted until dedicated body/ABI +emission and cleanup run in linked binaries with Source inputs removed. The +native **480/480** result remains the separate adapter-head Windows test receipt; +it contains no admitted Resource lifecycle case. The approximate **85%** feature +coverage estimate remains unchanged. Remaining aggregate, captured/reflected, +refinement, provider and concurrency cases stay within the full goal. + +## Native Source runtime transition gate + +At the Source runtime successor, all nine focused Source controls pass, as do +three v2 registration, two all-or-nothing transfer and fifteen retained adapter +controls. The repaired formatted runtime package passes 196 unit and thirteen +integration tests (`fd04d3`). One historical malformed-header assertion was +updated because v2 is now supported; unsupported versions remain refused. + +This supplies host/runtime and C-boundary evidence only. It does not add a +Source-native lifecycle fixture or close the Resource execution row. Generated +bodies, ordinary companion cleanup, matched archive and Source-deleted linked +runs remain pending; whole-language parity remains the goal and the broad +estimate stays about 85%. See the [runtime handoff](native_resource_hook_identity.md#native-source-runtime-and-custody-leaves). + +The final Source runtime successor additionally passes three causal ordinary +companion ownership regressions and five exact root-outcome controls. The final +formatted runtime gate is **204 unit plus thirteen integration tests** +(`716d31`). The malformed companion controls were observed failing before the +fix (`bf61fa`) and passing after it (`5af1b8`). Root-outcome evidence (`4f200a`) +preserves original body zero independently of cleanup failure and rejects +active/stale/foreign or wrong-purpose observations before effects. These remain +host/C protocol checks; generated Source-native lifecycle admission is pending. + +## Resource ordinary companion compiler gate + +The fresh-plan companion analysis passes three both-profile Source groups and +**546 HIR/MIR/codegen/object checks plus the MIR doctest** (`f48b08`). It preserves +ordinary initialization/liveness/alias/move/type/Source call checks and excludes +Resource endpoints from ordinary owned storage. Current expression identity +selects already validated roles; public ordinary Resource refusals remain. +Readonly acquisitions borrow the same fresh immutable program. This compiler +gate does not establish emitted Resource bodies or linked lifecycle execution; +the fixed inventory and approximate 85% feature estimate are unchanged. + +## Direct Resource body and matched archive checkpoint + +The direct Resource emitter's five original-Source object/ABI groups pass in +both compiler profiles. The formatted codegen/object/HIR/MIR gate passes +551 checks plus the MIR doctest; a detected ordinary CFG regression is repaired +by allocating Resource failure blocks and cleanup slots only in the selected +family. Ordinary runtime checking passes 217 tests. Four exact report encoder +controls and six actual driver decoder controls also pass. + +Both private Windows MSVC runtime profiles are compiled and inspected with +the actual static-CRT flags, native-library vector and discovered SDK/linker. +Each archive contains exactly one main, eight private test exports and 32 +Resource leaves. The production archive excludes private main/test exports. +The HOST C ABI controls pass with each measured archive, including malformed +input, foreign/stale context, repeated entry and output-capacity refusals. These +handwritten controls add no Source coverage. Their actual primary report shows +zero obligations before destroy and exact Constructed/Finalized events. + +The real reference gate and native gate each pass five original Source scenarios +under both profiles: connected ownership/borrow/return/close, factory failure, +later-actual ordinary failure, implicit drop and finalizer panic. Native binaries +run after both Project and synthetic Stdlib directories are deleted. The strict +report checks original body/cleanup outcomes, messages, events, exhausted script, +ordinary storage and every Resource obligation before exactly one destroy. +These are ten native executions in one conformance group, separate from the +earlier 480-case corpus. The first local Windows compiler-test slice is accepted; +production provider and supported-host distribution acceptance remain pending. + +The actual broader driver gate passes 693 tests, including all 182 lowering +fixtures. Indirect/aborted/Replace and broader Resource semantics remain within +the full goal. The fixed 207/182 inventory and rough 85% feature estimate retain +their separate scopes. The private measured library order begins bcrypt then +advapi32; the production canonical vector reverses those first two. The reviewed +private prerequisite retains actual measured ordering and changes no production +launcher validation or installed manifest. + +### Plain Resource replacement pre-effect validation + +At the runtime successor to cc06aa1f, `native-resource-replacement-preflight-focused.log` passes four replacement groups and `native-resource-replacement-preflight-runtime.log` passes 220 runtime checks. Exact loan, consumed-handle and private generation-exhaustion controls preserve both owners before any old finalizer, then verify valid once-only cleanup. No wire or C ABI change is made. This evidence does not establish Source-native mutable replacement, aggregate replacement, or current supported-host acceptance. The earlier 486 native regression result and ten Source runs remain evidence for cc06aa1f. Rebuilt matched archives and Source replacements are required next; overall tracking remains about 85%. + +### Source plain Resource replacement acceptance + +The formatted Source replacement successor to `2b9442b1` passes 558 compiler/object/HIR/MIR checks plus one MIR doctest, and all 606 frontend/backend-lowering fixture checks. `native-resource-replacement-final-reference.log` and `native-resource-replacement-final-native.log` each pass eleven shared original Source scenarios in both profiles. The native launch deletes Project and synthetic Stdlib Source, observes exact events/body/cleanup/messages/scripts, retires every Resource/ordinary obligation before one destroy, and preserves body 255/cleanup 255 for executed old-finalizer failure. Exact self-reseat emits no finalizer/transfer, while failed RHS evaluation leaves old available to its handler. The fixed 207 inventory/182 obligations and broad 85% estimate are unchanged. + +The new measured archive receipt `54a38248` preserves exact CRT/library/symbol provenance; both HOST C ABI controls pass. The broad driver-library attempt is 86 passed/1 failed due to CaptureLinkerOutput PermissionDenied during pre-spawn capture setup; the exact isolated property replay passes. The error alone does not identify capture creation versus second-handle opening. This is not a clean full driver-library result. The previous 486 native regression result remains specific to `cc06aa1f` until the complete suite is rerun on the new commit. Conditional/aggregate/projected replacement, other Resource execution and supported-host production distribution remain unproven. + + +### Exact native Resource replacement regression + +The clean pushed `cda86ac7` Source replacement commit passes the complete default +native gate: 486 tests, zero failures and one ignored Resource group, in 1355.37 +seconds. All 2099 recorded Rust/Jett/Cargo inputs remain exact after termination. +The ignored group was separately executed in all 22 native and 22 real-reference +scenario/profile runs. The earlier 86/87 driver-library result remains a recorded +capture setup failure; its isolated pass alone did not make that broad gate clean. + +### Retained native output capture + +The driver retains append-only capture files, gives children owned duplicate +handles, and reads bounded snapshots with explicit offsets. It no longer opens +the second file object through ReOpenFile. Private error context identifies +creation, duplication or snapshot while retaining the original I/O source. +The original PermissionDenied log proves a pre-spawn setup failure, but does +not distinguish creation from reopening or establish an external cause. + +After correcting the authored Windows test commands' quoting without changing +their stream/status/deadline oracles, all 12 capture checks and three command +checks pass. The unchanged property replay regression passes twice in isolation, +then the complete driver library passes all 92 tests (152.85 seconds). Existing +Source deletion, both optimize modes, 104 trials, trial-4 sentinel, counterexample +and original-stream requirements remain intact. Child polling/kill/reap and +deadline behavior are unchanged. This verification repair does not increase the +about-85% feature estimate. Full native and supported-host acceptance of the next +combined Source implementation remain separate gates. + +## Checked Source call staging and active-frame sums (2026-10-05) + +Direct Resource calls with handled actuals now lower to constructor-owned typed +Begin/Stage/Invoke/End regions. The sealed witness keeps the complete original +Source tuple and current graph; fresh CFG analysis proves lexical argument order, +arrived-prefix custody, exact endpoints and retirement before a handler Return +operand. Complete prepared Source and borrowed-formal metadata exists even when +Invoke is skipped. Public graph edits cannot recreate that authority. + +A genuine first-factory failure initially returned native protocol refusal after +ConstructionFailed, while the reference returned cleanly. The repair separates +Scope storage from active execution: a private exact-site projection supplies the +current frame for sum observations, transfers and extraction. Runtime top-frame, +slot ancestry and original Source checks remain strict. Both first-factory +success and failure now execute with their original outcomes. + +Eighteen shared Source cases pass in both reference and native profiles (36 +executions each). Native launch follows Source deletion; exact body, cleanup, +channel, messages, events, script consumption and zero obligations are observed +before teardown. Seven additions cover minimal and original-helper abandonment, +successful invocation, retained written views, first-factory failure and success, +and cleanup panic. The formatted compiler/object gate passes 573 checks plus the +MIR doctest; 606 frontend/lowering fixture checks, all four inventory gates +(including 182 host objects), and the complete 92-test driver library pass. +The final formatted Source replay also passes all 36 reference and 36 native +executions. The full current native regression and supported-host jobs remain +separate release gates. + +Whole-language coverage remains about 85%. Indirect/refinement/aggregate calls, +static region selection, broader exits and projections, production providers and +shared scheduling remain part of the full objective. These local Source results +do not establish complete native parity. + +### Return-prefix and completed failure propagation (2026-10-06) + +The shared original Source gate now passes 22 scenarios in both reference and +native profiles (44 executions each). Later handled argument failure retires +both temporary view owners in reverse lexical acquisition order. A handler +Return retires its earlier temporary owner before its original helper borrows +an independent retained owner. A default continues to one invocation; actual +cleanup panic prevents the Return helper and still cleans the retained owner. +All native programs launch after Source deletion and require exact channels, +messages, events, script consumption and zero obligations before one destroy. + +Two causal mismatches were repaired without changing those oracles. The reference +Return operand originally ran before pending-operation retirement; ordinary +callables then exposed an inherited caller-floor regression. Each callable now +has its own boundary. Native cleanup already retired both owners, but SourceStatus +converted the callee's recorded status 255 into generic refusal 1. A private +terminal completion record preserves original body and actual cleanup separately, +while exact caller/Source identity and retired Scope/Return guards remain strict. + +All 508 reference-library, 92 driver-library and 222 runtime checks pass. Six +Return-prefix controls cover floor/Scope/receipt/refusal/panic behavior; eleven +native Source controls include exact body 71, clean body 0 with real cleanup +panic, wrong activation/tuple and absent completion authority. Fresh measured +single-runtime archives in both profiles retain one main, eight private exports, +32 Resource leaves, static CRT, ABI 1 and layout wire 2. The source-deleted gate +passes against those archives; production providers remain disabled. + +The parent a30ca97e source tree has all four supported-host jobs green, with +full workspace and 486 default native tests on each host. Its local full run's +26 deadline failures subsequently passed unchanged deadlines in 25 plus one +replays; that does not replace a clean complete local run. Full successor +workspace/platform release acceptance remains distinct from the local gates. +The full 100% objective is active, with the coarse estimate unchanged at 85%. + + +### Required Resource values and exact ordinary helper bodies (2026-10-06) + +The latest local shared Source checkpoint is 61 scenarios and 122 executions per +backend in debug/release. It supersedes the earlier 22/32/46-scenario checkpoints +above. Required evaluation runs once through a provider-free checked cache before +runtime registration; the native path retains original Source provenance through +materialization, production value conversion, MIR validation, object emission, +linking and Source-deleted launch. Exact events, script consumption, failure +channels and zero obligations before teardown remain mandatory. + +All nine required-value MIR controls and 251 MIR library tests pass. The genuine +scalar-helper baseline accepted a changed required value and canonical pruning; +current body-only proof capture rejects both without changing that helper's +ordinary calling convention. Typed sum headers and normalized operation nodes +also participate in disconnected-custody detection. Exact guarded absence still +creates no Resource payload owner or finalizer at runtime. + +These results are local MSVC private-archive acceptance. GNU private execution, +full current workspace/default-native/platform gates and the remaining semantic +families remain separate requirements. Whole-language tracking remains about 85%; +the full 100% objective is active. + +The complete affected codegen suite passes 100 library and 63 object tests; +all 93 driver-library tests also pass. Final Rust/Jett formatting and whitespace +checks pass, and all 2,168 frozen source/config inputs revalidate unchanged. + +### Verified borrowed optional/result Resource payloads (2026-10-07) + +Sources 45..56 add twelve scenarios covering written/bare None/Some/Fail/Ok, +named-argument prefix abort, and handled borrow-domain failure. The unchanged +baseline passed all 73 shared scenarios / 146 reference executions but refused +Source45 at HIR stable-origin admission. The complete corpus now passes 146 +Source-deleted native executions against freshly measured Debug/Release MSVC +private runtime archives, with exact events and zero obligations before teardown. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three focused +MIR groups and six runtime groups cover original-body custody, guarded projections, +conditional shell lifetimes, failure companions, argument aborts and forgery. +The full codegen suite passes 102 library and 63 object tests, including unchanged +prior invalid-MIR controls. Archives contain 37 Resource leaves, eight private +exports and one main; independent source/symbol/CRT receipts remain mandatory. + +Admission is limited to root-body Resource aliases. Nested/scoped aliases, +ordinary non-Resource borrowed sums and alternate Default payload lifetimes remain +pending. The rough full-language estimate stays about 85%, and the full 100% +native-codegen goal, GNU Resource gate and broader language/platform requirements +remain open. See [the integration contract](native_resource_borrowed_sums.md). + +All 521 reference-evaluator and 93 driver-library tests pass. The existing +Source33 required-value assertion now verifies its exact two original proofs, +including scalar 761 and the unique checked hook; other fixtures retain exact +one-value counts. This separately fixes the sole failure observed on both hosts +at 17d6c841. Rust and all twelve new Jett Source format checks pass, and the +2,188 frozen source/config inputs revalidate. The final MIR-only formatting change +is exactly one error-return line wrap and passes all 254 MIR tests again. + + +### Verified nested Resource lexical lifetimes (2026-10-07) + +Sources57..95 extend the original shared corpus to 138 scenarios. The Source-only +baseline passed all 276 reference executions but refused native Source57 at exact +constructor-scope admission. Private declaration/exit records and ordered native +loan retirement now pass all 276 Source-deleted MSVC executions in both profiles, +including owned backing reuse, sibling identities, borrow failures, Break and +Continue. All 2,234 frozen inputs revalidate; runtime bytes and the measured +37-leaf ABI1/wire2 archives remain unchanged. + +All 261 MIR, 103 codegen library, 63 object, 521 reference-evaluator and 93 driver +library tests pass. Six public object baselines and eighteen forged-exit refusals +bind codegen admission to exact private records. The rough feature estimate now +tracks about 86%, with unchanged fixed 207/182 inventory denominators. The whole +100% objective stays active. Ordinary borrowed list sums have a separate genuine +12-Source reference baseline and 24 native unsupported-handler refusals; their +implementation is not included here. Updated coherent-revision workspace/platform +and private GNU execution remain independent gates. + + +### Accepted ordinary borrowed-sum checkpoint (2026-10-08) + +The ordinary borrowed-sum implementation is accepted at clean committed HEAD +`2c9f9aa8f2cd481c1d99ce6340ddf63688b2657a`. Its coherent locked workspace passes +**3,446 tests across 92 result groups**, with **0 failed and 1 ignored**, +including **491 default native-conformance tests** and the **182-object inventory +gate**. All **3,637 tracked inputs remain unchanged** after execution, and the +worktree remains clean. The receipt is +`target/native-ordinary-2c9-workspace-evidence/after-evidence.json`, SHA-256 +`07fb0df1d059420c2705111a00da6d56b820e5e168eb37fc757c3fdadbe2dd1a`; its workspace +log SHA-256 is +`42657fc3b9ca894ee3a3a2d9b4997c1af044e15520f5b2031e6aeb01bf7b2e4f`. + +The exact-revision [CI run 37673804359](https://github.com/vycdev/jett/actions/runs/37673804359) +passes all four jobs: Windows MSVC and Linux GNU build/conformance, plus both +relocated installed-package jobs. This closes the coherent-workspace and +supported-host workflow requirements for this accepted ordinary-sum revision. +It does not establish the later expanded Resource pipeline test packet or a +complete language/distribution gate. + +The [ordinary borrowed-sum family](native_ordinary_borrowed_sum_codegen.md) +advances the broad planning estimate to **about 87%**. That judgment remains +separate from the fixed **207-fixture / 182-run-pass inventory**; neither +denominator changes. Other ordinary-carrier, Resource, reflection and semantic +obligations remain open. + +### Source-only Resource pipeline checkpoint (2026-10-08) + +The [pipeline packet](native_resource_pipelines.md) has measured **23 cases / 46 +Debug-and-Release attempts** through each distinct local gate: checked Source +reference execution, Cranelift object emission, and linked native execution with +Project and synthetic Stdlib Source files absent at launch. The reference focus +is included in the accepted **161-case / 322-profile-execution reference +corpus**, with **522 reference tests passing**. The reference observation tuple +proves exact events and zero outstanding owners/registry entries; it does not +measure script consumption. Native reports additionally require zero remaining +scripted inputs and zero obligations before teardown. + +`target/native-resource-pipeline-source-evidence/acceptance-v2.json` records all +**3,648 frozen repository inputs unchanged**; both matched local MSVC archives +and six nested receipts separately revalidate. Provider-panic retirement and cleanup-error precedence now +pass through genuine Source pipeline fixture 20: body/cleanup/channel and exit +are respectively **1/0/2, 73** and **1/255/3, 71**, with exact runtime-operation +and cleanup-error messages. These are tests of existing behavior, with no +language or ABI change. + +The preserved full **161-case / 322-execution linked native corpus passes** +(1256.05 seconds), with all frozen inputs and measured archives unchanged. +Its separate receipt is `full-acceptance-v2.json` in the same evidence directory. +Object emission is not execution, and these matched local MSVC runs do not prove +GNU or other platform acceptance of the expanded packet. Broader Resource +`For`/`Match`, reflected bodies and ordinary families remain separate work. This +checkpoint leaves the **207/182** inventory and **about 87%** estimate unchanged. + +### Expanded Resource For/Match transport (2026-10-08) + +The [control-flow successor](native_resource_control_flow.md) preserves custody +through consuming Resource-free list ForEach and ordinary Resource-free enum +Switch, without a language or ABI change. Constructor-owned reconstruction +authenticates the three temporary headers, source/cursor/length initialization, +extraction/increment prefix and original CFG; independent original/current +borrowed, lexical, call and descriptor proofs remain mandatory. Strict joins and +final raw ForEach refusal are unchanged. Historical v2 acceptance remains +22 cases / 44 Source-deleted MSVC executions, three focused MIR groups and +450 affected phase checks. + +V3's seven accepted Source files 01..05/07/08 cover consecutive and nested loops +as well as Fallthrough/Break/Continue and both unit-enum arms. All 30 cases / +60 focused Source-deleted MSVC executions pass in Debug/Release (204.35 seconds), +with exact outcomes/events/channels/messages, script exhaustion and zero ordinary/ +Resource custody before teardown. Reference zero-owner/registry observations do +not measure that full native tuple. The receipt +`target/native-resource-control-flow-source-evidence/focused-acceptance-v3.json`, +SHA-256 `7f41c389daa268942afe8a645085daac18d746f553f3be32e93d14f798fc30ea`, records +all 3,658 frozen inputs and both measured archives plus six nested receipts +unchanged at HEAD `50ad73a0693c502d1fa05720091b80a4ab623104`. Four focused MIR +groups pass; 89 Resource reference groups include all 191 cases / 382 profile +executions, and final object preflight separately emits 60 objects. + +The clean base `50ad` workspace independently passes 3,448 tests with zero +failures across 92 result groups. That is separate from the modified successor's +coherent-revision gate. Full 191/382 Source-deleted MSVC native execution now +passes in 1,288.83 seconds. `full-acceptance-v3.json` records actual exit zero, +all 3,658 frozen inputs, both measured archives, six nested receipts and the +executable unchanged; its SHA-256 is +`08bcf865bfed8a9b49f262a6c611336260cd0446ba8517139c71ab10627fb806`. +Successor workspace and exact-revision CI acceptance remain pending. Source06's +separate GNU reference repair passes authority, recursion and pure control-flow +tests plus 74 strict Resource entry attempts, including same-grant reentry after +failures; `after-v3.json` records all 3,662 inputs unchanged. Its native +HIR/MIR/codegen ordinal bridge remains pending, and no GNU native acceptance +follows from those reference results. +Erased carriers, borrowed/projected/Never-element or other iterators and broader +For/Match/reflected combinations remain compiler proof obligations under existing +Source semantics. The full goal stays active at the rough 87% planning estimate; +fixed 207/182 inventory denominators remain unchanged. + +### Checked reflected-field reference repair (2026-10-08) + +[Direct field selection](native_resource_reflected_fields.md) now passes all +541 comptime tests, including 19 added authority/recursion/control/metadata and +Resource tests. Strict original Source06/derived controls pass 74 entry attempts +in both checking profiles, including same-provider/grant reentry after every +selected failure. `after-v5.json` records all 3,664 inputs unchanged at the +complete crate gate. This checkpoint proves checked reference behavior; its +receipt alone establishes no native reflected Resource emission or Source-deleted +execution. Accepted native successor gates are recorded below; the full goal +remains active. + +### Checked Resource reflected-field native ordinal bridge (2026-10-08) + +The [direct-field bridge](native_resource_reflected_fields.md) now carries exact +nominal-struct TypeFields ordinal/body correspondence through checked Resource +HIR and MIR. An immutable original archive retains the checked program, original +For/binding and resolver identity, concrete owner and ordered full metadata, +checker-selected body and original lexical path. Every ordinal receives its own +original-field-index guard and one-arm TypeInfo dispatch, preserving distinct +bodies for fields with equal type and reflection while retaining native dispatch +uniqueness. Independent original/current seals reject changed metadata, guards, +targets, paths and public HIR before runtime effects. No runtime leaf, ABI or +wire-v2 row changes. + +Ordinary helpers inside the checked Resource program receive proof-only witnesses +and keep ordinary execution classification and sequence preparation. They gain +no Resource plan, hidden Scope parameter, provider permission or custody. The +ordinary `lower_program` entry retains its existing equal type/reflection +coalescing. Checking specializes type-level facts while field-index/name branches +use the current runtime binder; existing native fixtures already distinguish +same-type fields. Independent ordinary positive controls now pass 30 +Source-deleted native executions without a production change. Their bounded +parity establishes no ordinary coalescing defect or required production repair; +broader checked Resource reflection transport remains a separate obligation. + +The complete affected-phase run passes **1,202 checks**: **542 comptime, 203 HIR, +286 MIR, 107 Cranelift codegen, 63 object and one CFG check**, with zero failed or +ignored tests across ten result groups. Integrated controls include canonical view +iteration and an inferred empty list, exact ordinal guard/body validation and +full ownership rejection of redirected or stale selections. The receipt +`target/native-resource-reflected-field-native-bridge-evidence/after-v4.json` +records actual session **4116**, terminal **1335da**, exit **0**, and all **3,679 +frozen inputs unchanged** at parent HEAD +`f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`. Its SHA-256 is +`a043e2b9b4b1b1e908f4037941c0e6ca5113309e08fe167979a046fbdea963ad` and it explicitly records `native_execution_accepted: false`. + +This earlier compiler phase gate is distinct from the native execution acceptance +recorded below. Generic/alias owners, bitfields, variants, machines, TypeInfo.args, +nested producers and broader reflected combinations retain their compiler proof +and Source-evidence obligations. This boundary adds no policy ban. The full goal +remains active at the rough **87%** estimate with fixed **207/182** denominators. + +#### Native execution acceptance + +Four focused Sources retain the original Source06 bytes and separately test +distinct field-index bodies, distinct field-name bodies and a proof-only ordinary +helper. They pass **16 Source-deleted native executions / 24 entries** in +Debug/Release: eight single-entry executables and eight clean two-entry +executables, with the same Session/provider/grant retained across both entries. +Each entry checks exact outcomes, ordered events and zero ordinary/Resource +obligations before teardown. The combined script is exhausted before one Session +destruction. +`target/native-resource-reflected-field-native-bridge-evidence/native-acceptance-v5.json` +records actual session **29429**, terminal **9cfc8d**, exit **0**; its SHA-256 is +`0b05489e3b97201ba0aee4642c2f157e81d5f9684e818f347eb403f775cda10a`. + +The unchanged predecessor corpus independently passes **191 cases / 382 +Source-deleted native executions** in Debug/Release in **1,273.64 seconds**, with +all strict reports accepted. +`target/native-resource-reflected-field-native-bridge-evidence/full-native-acceptance-v5.json` +records actual session **49641**, terminal **1d2132**, exit **0**; its SHA-256 is +`31d6b59bebc1e11c18b9dfb6102b5f5486f28dfd69d1439fce15498aa4050db9`. Both gates retain all +**3,686 frozen inputs** and **nine runtime files** unchanged, including the matched +measured MSVC archives and their build/CRT/native-static-libs receipts. The frozen +test executable SHA-256 remains +`76fe2a90106ee8ede00eb86758cefbaa4cefb42d6b818e90a06337b029aba39d`. +These receipts establish the bounded direct nominal-field native bridge and clean +reentry. Original Source06's separate exceptional acceptance is recorded below; +derived native control acceptance is recorded below; broader aggregate custody +remains a separate obligation. + +#### Independent ordinary reflection positive control + +Three Source families produce five forms covering nominal/generic and alias +fields, bitfields, variant/state fields, type arguments, nested fields and getter +pipelines. Main, verify and property suites in both checking profiles pass +**30 Source-deleted native executions**, matching reference stdout/empty debug +events and explicit-comptime stdout. +`target/native-ordinary-reflected-ordinal-control-evidence/after-v1.json` records +actual session **21527**, terminal **6befe6**, exit **0**, and all **3,703 frozen +inputs unchanged**; its SHA-256 is +`4a282285082fc411b4d8714a890b13c7146d81970533522469eb5848d4a0177e`. The receipt explicitly records +`production_changed: false` and no established ordinary equal-type coalescing +defect. These ordinary controls do not establish Resource transport for their +broader reflection families. The full goal remains active at the rough **87%** +estimate; fixed **207/182** inventory denominators remain unchanged. +#### Original reflected Source06 exceptional single-entry acceptance + +The eight original failure/cleanup scripts pass in both checking profiles: +**16 reference observations / 16 object attempts / 16 Source-deleted native +executions**, with unchanged exact 929-byte Source06. Strict completion channels, +messages, event order, script exhaustion and zero obligations before one teardown +all pass. The existing four READY cases and 191-case catalog are retained unchanged. +`target/native-reflected-exceptional-native-evidence/after-v1.json` records actual +session **2180**, terminal **aa0907**, three command exits **0**, and all **3,720 +frozen inputs / nine runtime files unchanged** at `2be038c7`; its SHA-256 is +`38662e59e4dfebfbbde4236ee0a8d6b2cc647b6a9554ea8afb9f2b1119bf3085`. +Original Source06 exceptional same-grant reentry is accepted separately below. +The bounded derived native control matrix is accepted separately below. +This single-entry evidence adds no aggregate transport acceptance and leaves the +full goal, rough **87%** estimate and fixed **207/182** inventory intact. + +#### Original reflected Source06 exceptional same-grant reentry acceptance + +All **nine original scripts followed by READY** pass in both profiles on the same +Session/provider/paired Network grant: **18 Source-deleted native executables / +36 entries**, plus **36 actual shared reference calls**. Both per-entry completion +tuples, exact diagnostic bytes, event boundaries and continuous ordered events are +retained. Each entry observes zero owners/loans/frames/registry entries, +owner/loan/frame handles and provisional returns, plus empty ordinary storage, +before teardown. The combined script is exhausted and the Session is destroyed +once. Exceptional first completions keep their original nonzero process exit +after clean READY, including cleanup-wins handled Return body 0 / cleanup 255 / +Cleanup channel / exit 71. + +The private launcher completes and copies real observations before continuing +after retired Resource cleanup failure with current ordinary status zero. The +existing next `entry_begin` independently enforces installed-entry/budget, +ordinary first-error/cleanup and active/live-custody checks. No failure latch is +cleared, no provider/grant is reinstalled and no reset/failure-taking API is added. +The immutable first failure remains observable after the clean second entry. + +Fresh MSVC Debug/Release archives were built in actual session **23542**, terminal +**e09148**, exit **0**, and independently measured at +`target/native-reflected-exceptional-reentry-archives/measurements-01/receipt.json`; +the compiled receipt pin is +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Runtime ABI 1, Source wire 2 and script/report protocol 1 remain unchanged. + +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` records actual +session **30992**, terminal **22c716**, both Rust harness exits **0**, and all +**3,722 frozen inputs / nine runtime files unchanged** in the tested candidate +based on parent `b5c55ac7`; its SHA-256 is +`f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`. +The corrected exact manifest filters each executed **one real test**. Earlier +session **52970**, terminal **e8c3b4**, selected zero tests; preserved `after-v2.json` +explicitly records zero executions and **nonacceptance**. Only the corrected real +gates supply this acceptance. + +The exact 929-byte original Source06 and prior single-entry/four READY/191-case +evidence remain unchanged. This is bounded local MSVC original-script reentry +acceptance; the bounded derived matrix and dedicated Return successor are +accepted below. Broader Default lifetimes, fresh-archive predecessor compatibility, +coherent-workspace/exact-revision CI gates and broader +aggregate Resource transport remain required. The full goal stays active at the +rough **87%** estimate and fixed **207/182** inventory denominators. + +#### Derived reflected control single-entry and same-grant native acceptance + +The exact existing Sources7/8/9 and historical ten scripts pass both profiles. +Actual control paths are **three handled Returns, two Break paths, two Continue +paths and three scalar Handle Default paths**. The failure handlers return before +the named Break/Continue statements. A separate eleventh Source9 READY baseline +was checked before use as that same Source's reentry tail; the original ten rows +and all three Source byte sequences are unchanged. + +Break preserves total17 and selected-body retirement; Continue preserves total34 +and skips the statement after its scoped-type body; scalar Default substitutes +17, resumes the selected body and preserves both field visits and total34. +The outer optional Resource owner is retained +until the existing close or handled Return. These are exact selected-body/scalar +Default lifetimes, not general Resource-valued Default or arbitrary lifetime +composition evidence. + +The accepted gates are **22 single-entry reference calls + 40 same-grant reference +calls = 62**, **22 object attempts**, **22 Source-deleted single-entry executables / +22 entries**, and **20 same-grant reentry executables / 40 entries**, totaling +**42 native executables / 62 entries**. Each historical first script uses its own +Source's READY on one retained Session/provider/paired Network grant. Exact +completion tuples/messages, continuous events and entry boundaries, zero all +eight Resource counts and empty ordinary storage before teardown, script +exhaustion and one clean destruction all pass. The three explicit-close cleanup +cases preserve body255 / cleanup255 / Cleanup channel / exit71 and exact +`native Resource cleanup failed`, including the immutable original completion +and first nonzero process exit after READY. Existing entry-begin checks remain +authoritative; no reset, latch clearing or provider/grant reinstallation occurs. + +`target/native-reflected-derived-control-native-evidence/after-v1.json` records +actual session **18249**, terminal **e1bcf6**, all five command exits **0**, and +**one real test selected by each filter**; its SHA-256 is +`f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`. +All **3,725 frozen inputs / nine runtime files** remain unchanged in the tested +candidate based on parent `f4c63a0f`. The measured MSVC Debug/Release archives use +the unchanged compiled receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Original Source06's nine-script matrix and earlier single-entry/four READY/ +ordinary-control/191-case evidence retain their original counts, receipts and +archive pins. This is local MSVC derived-control acceptance; GNU carrier/fresh-v3 +native execution is separate. The dedicated Return successor is recorded below. +Broader body/Default lifetimes, aggregate transport, fresh-archive predecessor, +coherent-workspace and exact-revision CI remain required. No Source policy ban is +added; the rough **87%** estimate and fixed **207/182** denominators stay unchanged. + +#### Dedicated reflected body and owned Resource Return acceptance + +The additive Sources10/11 and twelve shared rows pass in both profiles. Each +retains three equal-int64 fields and the statements following its conditional +Return and loop. Finite provider scripts/sentinels establish true successful +first/second-field Return and skipped later fields/statements, independently of +the original Sources7/8/9 ten-script matrix, which remains unchanged. + +Source10 returns nothing with scoped projection/outer owner retirement. Its two +implicit-cleanup cases retain **body0 / cleanup255 / Cleanup / exit71**, empty +ordinary message and exact `native Resource cleanup failed`. Source11 returns +owned survivor892 while outer891 retains the selected body's alias; successful +outer cleanup precedes publication and exact caller Borrow892(29)/close. The +successful events place F891 before B892. Its two cleanup cases suppress +publication/consumer borrowing, retire891 then provisional892, and retain root +**body255 / cleanup255 / Cleanup / exit71**. The helper Source follows a normal +Return path; the report directly observes the entry-root completion and exact +retirement/publication-suppression events, without reading a nested callee +completion. +Clean/handled-domain Return cases retain body0/cleanup0/clean/empty messages/exit0. + +Actual reference/object session **64018**, terminal **0b2a26**, passes three +one-test filters with exits0: **24 single reference calls + 48 same-grant calls = +72**, and **24 final objects**. Actual linked session **47358**, terminal +**dcfb82**, passes two one-test filters with exits0: **24 Source-deleted single +executables / 24 entries** and **24 same-grant reentry executables / 48 entries**, +for **48 native executables / 72 entries**. READY retains the same byte-identical +Source, Session/provider/paired Network grant. Increasing attempt IDs, immutable +first completion/diagnostics/first nonzero exit, continuous events and boundaries, +all eight zero Resource counts and empty ordinary storage before teardown, +combined-script exhaustion and one destruction pass. No reset or latch clearing +is introduced. + +`target/native-reflected-body-return-native-evidence/after-v1.json` records the +reference/object checkpoint. Linked acceptance is recorded in +`target/native-reflected-body-return-native-evidence/after-v2.json`, SHA-256 +`9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`. +All **3,730 frozen inputs / nine measured v2 runtime files** remain unchanged at +tested parent `91105876e5dbbbe9bbaa1d19bc2408ec9a274255`, using the unchanged +measured MSVC Debug/Release receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +This local MSVC gate gives no fresh-v3/GNU carrier or successor-workspace/CI +credit; predecessor receipts retain their original counts and archive pins. + +Owned operands still precede certified lexical retirement. A same-owner move +while its resident lease remains live is not covered by the distinct-survivor +success. That lifetime seam, broader Resource-valued Default/body compositions, +generic/nested reflection, aggregate live custody, fresh-archive predecessor +compatibility and coherent-workspace/exact-revision CI remain full-goal +obligations under unchanged Source semantics. The rough87% estimate and fixed +207/182 inventory denominators remain unchanged. + + +#### Current comptime actor graph MSVC acceptance + +Actor handles retained by an admitted pure `comptime` expression now form an +immutable graph template. Each evaluation imports fresh actors while preserving +aliases, sharing, evaluated state and authenticated callback/hook metadata. +Graph producer and iterator proofs preserve list/map behavior. Ordinary required +replacements preserve their checked archives; ordinary actor/interface wrappers +retain original entry metadata through canonical sequence iteration. These paths +keep ordinary heap ownership without Resource custody or capability authority. +Entry completion retires its actors, and required constants use one checked +attempt. The negative graph fast path leaves positive admission proofs intact. + +The current four affected libraries pass **793 tests across four groups**, with +zero failures or ignored tests: codegen 133, Driver 95, HIR 220 and MIR 345. The actual +log is `target/native/graph-affected-libraries-current-v6.log`, SHA256 +`6b16b29fa296b6955587a123ad9c206544ff80c660f0876467481c8a0a84436d`. +The terminal source-freeze record binds all 947 unchanged compiler/Source inputs: +`native-codegen-recovery-2026-10-09/graph-affected-libraries-acceptance-v6.json`, +SHA256 `58e25727552b941da02784f323039011fe0515c45b5735ccd4007ed5fd2f3a3f`. +A pinned predecessor command contributes **1,673 passing tests across 18 other +package result groups**, excluding its codegen, Driver and HIR rows. That log's +SHA256 is `7d27eecec6261eb2d28d6fc0d945ca9fcf587672aa78c69839cb2a07e3b693e6`. +These historical results remain separate. The mixed 2,466 count does not establish +a fresh full workspace library or integration run. + +Two focused HIR controls and one MIR control pass the required-value and ordinary +wrapper-sequence repairs, including foreign/pre/post-edit and builder transcript +refusals, five iterator/header/body/Source alterations and real Resource-owner/ +direct-call refusals. All three original interface native regression tests pass +with Source present. Their unchanged-input receipt is +`target/native/interface-archive-native-controls-v2/after.json`, SHA256 +`04b66bd7b53c564f6983ff35799d8aeb0dd517b64aa3034e41b1466167d1619b`. +These tests provide no Source-deleted execution credit. + +The current normal MSVC Debug/Release bundles pass **32 Source-deleted graph +executions: 24 public and eight protected hook cases**. Original ordinary fixtures +and the adapted callback use Debug Source. The production Stdout callback and both +hook Sources cover both Jett optimization choices. Independent reference/stream +oracles, object hashes, successful terminal/reap records and absent Source/case +executables pass with all 947 compiler/Source inputs and used-launcher guards +unchanged. The receipt is `target/native/actor-graph-native-acceptance-v6/after.json`, +SHA256 `ecbba9f7539f250d8c48be4ee9ef9256e918bf95ab2d1731c39ee10460366d76`. + +Normal v4 and private v8 Runtime/Launcher archives retain the 56-export production +Resource catalog, static CRT and separate private hooks. Actual compiler native +library notes and the canonical manifest vector remain distinct records; linked +acceptance establishes the selected vector. Archive receipt pins are +`e7f818891a707382ae9066bed5f26079a3da93dda0aa7d29124abe7e21e56a9d` +and `f40be801d5f609b0010a747ebc3a428d5ccc10e4e20989c83c3555dbde7595b9`. + +The current bounded startup matrix passes **80 Source-deleted executions**: 48 normal and 32 private, with 40 decoded private entries, 60 successful exits and 20 expected exit71 refusals. Source, terminal reaping, original case oracles and artifact guards pass; no case executables remain. Source33 wire3 is object-only and supplies no linked execution credit. The receipt is `target/native/startup80-current-graph-v3/after.json`, SHA256 `467f5b21d331f568543cdadec9390a05038d305e70b16d56976786a9219a4596`. + +The current interface compatibility subset passes **25 native tests** with unchanged inputs: all 21 original `native_interface_` tests plus four generic, deep-conversion and reflected-interface controls. Its receipt is `target/native/graph-interface-regressions-current-compiler3/after.json`, SHA256 `766080cb513147f87c056a9452457f6ba07ff40443a16a05f33310b953731607`. This selection does not establish all normal callback, Owner or Actor regression acceptance; their earlier 17-test result retains its predecessor revision. The current Source-deleted Graph gate independently covers its joint Graph/callback cases. + +All **16 current Resource gates pass**, covering **754 Source-deleted executions and 900 entries** on the freshly compiled compiler 3 test artifact. The whole lifecycle gate reports 382 executions and 246 observed builds. The occupied two, predecessor twelve and clean-reentry two gates retain exact selectors, current compile-time archive pins and unchanged compiler/Source guards. The counts receipt is `target/native/resource-msvc-v8-current-compiler3/current-gate-counts.json`, SHA256 `4d9409982e937fa19dcdfa9c1c8a7cb9467569988b5df9050005888152ce813e`. + +Broader normal native regressions and a complete workspace run remain pending. Predecessor receipts retain their original revision and scope. The +published helper revision 0eb3ce5f passed four CI jobs, which does not establish +acceptance for the current Graph revision. Exact-head supported-host CI, GNU and +clean installed distribution remain separate obligations. The rough **87%** +estimate and fixed **207/182** inventory are unchanged. Existing sequential +reference parity remains required; absent scheduler/provider APIs remain separate +feature work. + + +#### Occupied Resource primitive companion MSVC acceptance + +The occupied constructor path now carries every implicitly copyable primitive +and transparent alias with exact checked schema and written argument order. +uint64 retains its unsigned carrier, float32 normalization remains separate +from physical validation, and integer/float/bool pending depth remains separate +from payload bits. Nothing retains its scalar-word depth representation across +the carrier boundary; strings retain depth in their handles. No new Source +admission, Resource custody or authority is introduced. Original Sources33..35 +and their support remain unchanged. + +The original all-target check, formatting checks, 82 measurement-helper controls +and eleven focused groups/46 tests passed. After the Scope completion repair, +actual Cargo runs pass 1,322 compiler tests (Comptime 620, HIR 221, MIR 347, +Codegen 134), and Runtime passes 366. These unit runs precede the private report +copier and two-file oracle correction. Only those three exact guarded files +differ afterward. Broad receipt: `native-codegen-recovery-2026-10-09/scalar-v9/current-broad-cargo-acceptance.json`, SHA-256 `f4b2217d6ed8228de432922f3ee90fc066d20ed4dbb40c420dbc79794289ce9c`. +These groups do not establish a fresh whole-workspace test run. + +The correction changes six expected Resource-message fields for ordinary-only +Bounds failures and introduces no root/nested special case. Original Source, +reference messages, body/cleanup statuses, process exits, events, custody and +reentry assertions remain unchanged. Oracle correction: +`native-codegen-recovery-2026-10-09/scalar-v9/ordinary-diagnostic-oracle-correction/manifest.json`, SHA-256 `aafc67f2455050bc0b15bfc535a859fc4a01662ef8e6291e8200c5efed328d44`. + +Fresh normal and private MSVC Debug/Release measurements retain 58 production +exports (38 ordinary, 15 carrier, five startup), eight separate private hooks, +ABI1/wire3 and static CRT. Receipt pins: +2cdef1b1c334389e624407ceb819246a9ea61737539f1943d1d167ec95470fd6 and +46bbe5fd5116b9093afa374f15c64e70f8747cd5cba0670ff607281d90df3738. +The fresh corrected Resource gate binds exact current Source and dep-info. +All sixteen predecessor gates pass 754 Source-deleted executions/900 entries; +both new scalar gates pass 92 executions/138 entries, totaling 846 executions +and 1,038 entries. Scalar receipt: `target/native/scalar-resource18-oracle-corrected-v4/scalar-gate-acceptance.json`, SHA-256 `902e3f6ec6cb946c646b4fa12917ba365ccecfa7eedbcf43b9b83ec9d4dda2b6`. +Its reentry counts derive from guarded 23-case/two-profile loops and internally +decoded two-entry reports; individual per-child counts are not logged. +Predecessor counts: `target/native/scalar-resource18-oracle-corrected-v4/current-gate-counts.json`, SHA-256 `adf33cbab4544509dc66dce176493fcbab18bcfc22f0713ca007cb33636be991`. + +Before the isolated two-file oracle correction, Graph acceptance passed 32 +Source-deleted executions (24 public, eight hooks), and selected Callback, Owner, +Actor and interface regressions passed 42 tests. Only those two exact +expected-message tables differ from their source snapshots. Production and +selected test behavior are unchanged. These retain bounded prior evidence, +not a fresh run of the corrected test binary. Receipts: +`target/native/actor-graph-current-scalar-v2/after.json`, SHA-256 `11dc2e66b2cffa23cb502111307e3b40d544d67456e37b7613194d1a1a13700e` and `target/native/scalar-normal-regressions-v2/after.json`, SHA-256 `314ef80985d15d79e130747a7b1cd973db802498f152f2630cbd03151b3dba77`. +The current guarded Startup rerun passes 80 executions (48 normal, 32 private). +Earlier failed attempts supply no acceptance. Startup: +`target/native/startup80-current-scalar-v4/after.json`, SHA-256 `09c79ae568c54ca818f37aa928b3f3857653826aba21565b62f506299b03dcaa`. +Source33 wire3 remains object-only and gains no linked execution credit. + +All scopes remain bounded local MSVC evidence. Full coherent-workspace and +fixed-inventory conformance, exact-revision CI, GNU and clean installed +distribution remain required. Known deterministic CI failures remain a separate +repair obligation; this slice claims no exact-revision CI acceptance. Other +incomplete language parity compositions remain full-goal obligations. The rough +87% estimate and fixed 207/182 inventory denominators remain unchanged. + +#### Canonical native Verify-suite construction + +The native audit now appends a suite through an authenticated HIR factory when +required values retain a protected checked table. It preserves all existing +functions and selects only original zero-argument Verify bodies in source order. +Foreign archives, edited old bodies, extra table entries, and changed generated +calls or headers cannot renew the protected table. Source syntax, compile-time +eligibility, Resource authority and ABI remain unchanged. + +Focused checks pass nine tests: four ordinary/Graph factory controls, one Driver +control over the four original failing fixtures in both checking modes, and +four existing Graph/reflected forgery controls. Receipt: +`target/native/verify-factory-focused-v3/focused-results.json`, SHA-256 +`9b3e7afeb67e12afad4bb9f5474a8b51036f9df2193d6ef6e4496a4bae880cb6`. +The four previously failing fixtures pass sixteen Source-deleted native +executions across both checking modes and both normal MSVC runtime profiles. +Receipt `target/native/verify-four-current-v1/after.json`, SHA-256 +`bcad338942b98cd538e6d435cb110cbaa83d6a1d925528dac7f4e577e4a19bbd`. + +The unchanged complete native Verify audit passes 155/155 fixtures, with the +Debug runtime, default Jett source mode and original Sources present. This is +separate from the sixteen Source-deleted executions above. Receipt: +`target/native/verify-fixture155-current-v1/after.json`, SHA-256 +`cbf5d51341bcce173027afb9181ab07b57c4045b022c2b3a605b118c09c5227e`. +Current source, dependency and archive guards remain unchanged in both native +runs; measured Runtime sources and artifacts are reused without modification. +These results resolve the local Verify-suite regression. The two required-owner +CI failures, exact-head supported-host/installed acceptance and broader language +parity remain open. The rough 87% estimate and fixed 207/182 denominators remain +unchanged; this does not complete the native-codegen goal. + + +#### Original required regions inside inline bodies and refinement predicates + +Required evaluation now selects exact original inline-body regions and checked +refinement predicates while retaining the enclosing declaration and complete +generic/scoped context. The executable walker still stops at callable boundaries. +Canonical HIR extraction and predicate declaration joins retain private archive, +header and operand authentication. Canonical resolver names and original HIR +alias leaves are checked separately. Copied syntax or matching spelling grants +no proof, capability or runtime Resource permission. Closed and pure eligibility +and once-per-original-attempt evaluation remain unchanged. + +Focused checks pass 20 tests across 17 actual invocations, with no failures or ignored tests, including meaningful foreign, +clone, owner, context, header, operand, alias-name and runtime-purpose refusals. +Receipt: `target/native/required-owner-focused-v2/focused-results.json`, SHA-256 `14494e22ef31e941ea97e87fb2a06dee503e9daf929279bd633150aae0d5f96e`. +Affected-library breadth passes 1,334 complete library tests (Cranelift 134, Comptime 624, HIR 229 and MIR 347), plus 25 selected Driver tests (library 14, backend lowering 7, generic function values 2 and named arguments 2), all with no failures or ignored tests. +Receipt: `target/native/required-owner-broad-v3/broad-results.json`, SHA-256 `4241e0b95302f388f2d748f2c8cbb6599262541d66b1af72723ed479c82e5797`. +The broad run's final guard authenticates 2,099 current Source/fixture inputs +and 68 unchanged Runtime artifacts. Its library run completed successfully in +v2; v3 resumed only the Driver batches after independently checking unchanged +inputs. The later test-only restoration extension passes one focused control: +`native-codegen-recovery-2026-10-09/scalar-v9/required-owner-restoration-acceptance.json`, SHA-256 `a6ba8bf827ff749e400c7460ef7cf5d39399c541b4f1c77236fc06dc19f691e0`. +It checks same-program failure, full metadata restoration and successful reentry. +This changes no production code. The earlier 20-test and native receipts precede +that test-only extension and retain their own snapshot; the final breadth +includes it. The two native fixtures and their expected streams remain exact. + +The two unchanged original contextual-comptime and refinement-declaration gates +pass two Debug native main executions and two interpreter mains with Source +present. Exact stdout and rendered debug-event stderr match their reference +oracles. Receipt: `target/native/required-owner-native2-v2/after.json`, SHA-256 `a8e1014f3c7f79d10bff2b0077f353a9357d69f74b5486681d3f90f3ce9f225d`. +The gates supply no Release execution, Verify/Property suite or Source-deleted +lifecycle credit. Measured Runtime archives are reused unchanged; neither archive +guards nor compilation supply execution credit for other profiles or platforms. + +This closes the local RequiredOwner regression; published Graph's earlier E9001 +results remain historical. Full coherent-workspace/inventory conformance, +exact-revision supported-host CI, GNU and clean installed distribution remain +pending. The separate VerifySuite and capture-owner results retain their existing +scope. The rough 87% estimate and fixed 207/182 denominators remain unchanged. + +#### Generated reflected callback suites: bounded MSVC acceptance + +The current successor retains checked Verify/Property closure owners and restores +capture/purpose metadata on error and panic. HIR authenticates immutable +CallbackHolder constructors/aliases and their exact callable read. The public +reference test path retains the checked program. Property sampling now produces +an opaque same-program/successful-cache witness; the canonical HIR wrapper and +replay constructor preserve original trial/function order and protected tables. +Changed shrink inputs are validated before body entry. Public raw wrapper edits, +lookalike snapshots and matching signatures supply no authority. + +The permanent Driver leaf preserves byte-exact Sources19/20. Its three nonignored +tests require the actual two-block/100-trial reference reports, six ordinary +Program/Verify/Property objects and twelve Source-deleted normal MSVC +Debug/Release launches. The existing normal bundle selector remains the CI path; +supplied measured manifests are available for the controlled local gate. Each +executable is reaped and removed immediately. No private Resource Main is linked, +and no ordinary context-retirement counters are fabricated. + +All three exact permanent tests now pass. Each original Source's reference +report passes both blocks and records 100 Property trials. The object-only gate +emits six Program/Verify/Property objects; the linked gate separately emits six +objects, deletes each Source before linking/launch and passes twelve actual +normal MSVC Debug/Release executions. Exact exit0, empty stdout, Program stderr +`5\n`, empty suite stderr, object/entry/target identities and executable reaping +and removal are checked. Source checking retains release=false independently of +the runtime profile. Receipt: `target/native/callback-current-normal-v2/after.json`, +SHA-256 `0bb9987813fd2bf7735badf637edfe1882c8966f024156c1df535643f827611a`. +The fresh current compiler gate is +`target/native/callback-current-compile-v4/gate-acceptance.json`, SHA-256 +`25dde5c3c4b66ce083e98aacf530090e77ee06311dc0b3129d0d0c6ebd13fbcf`. +Complete compiler/Source, dependency and selected artifact guards remain +unchanged. Measured normal58 Debug/Release archives are reused unchanged, not +remeasured. Ordinary public launchers provide no context-retirement observation. + +Separately, 20 public reference/test-loading controls pass. Both corrected +six-test Comptime/HIR groups pass without changing original Sources19/20. All +eight native Property controls pass, including counterexample shrinking through +refinements, generic interface owners and float32 inputs, source-absent replay, +unreproducible failures and timeout handling. The timeout fault uses a canonical +internal MIR block and still passes ordinary validation before execution. + +The five affected compiler libraries pass all 1,664 tests: Codegen 134, +Comptime 636, HIR 241, MIR 347 and Typecheck 306. Logs are +`target/native/callback-interface-libraries-v1.log`, +`target/native/callback-property-replay-other-v1.log` (seven controls) and +`target/native/callback-property-replay-focused-v3.log` (timeout/replay control). +Initial failed attempts retain their historical scope; the corrected controls +now pass. + +The test-only Property harness applies faults to freshly lowered MIR for both +initial execution and replay, then runs the unchanged validators and codegen. +It does not reseal edited HIR or grant production authority. Existing shrink, +timeout and initial-MIR-only replay controls retain their original oracles. + +Final evidence must bind the corrected test inputs, current compiler/dependencies +and unchanged measured normal58 archives. Existing receipts retain their own +revision and scope. Full workspace/inventory conformance, exact-revision CI, +GNU and installed-distribution acceptance remain separate. The rough 87% +estimate, fixed 207 fixtures/182 object obligations and full native-codegen goal +are unchanged. + +#### Owned mutable field updates: bounded MSVC acceptance + +Ordinary record and exact-state machine writes now use one checked place from +frontend ownership through reference/required evaluation, HIR, MIR and native +preflight/commit. The ordinary occupied-slot leaf transfers a staged replacement +atomically and leaves displaced-value cleanup to the caller. Exact builtin +reflection metadata stays read-only internally. Nonzero field guards retain +exact paths and lose stale facts after writes, across loop backedges and inside +condition prefixes. Borrowed enum match payloads retain exact arm origins; +transparent borrowed coarsening/declassification takes an independent native +snapshot before payload extraction. Owned matches retain owned transfer. + +All seven affected libraries pass 2,162 tests: Codegen 135, Comptime 659, HIR 246, +MIR 355, Runtime 378, Typecheck 354 and Types 35. All 97 Driver library controls +pass across two invocations: the broad run passed 96, and its sole missing-runtime- +manifest Graph test passed after supplying the measured bundle. All eight backend +lowering tests pass, including every one of the 182 run-pass fixtures; all ten +field reference/frontend tests pass. The measurement helper passes 93 controls. +Full formatting and whitespace checks pass. Logs below refer to the primary +checkout's shared `target/native`: +`field-update-libraries-full-v5.log`, `field-driver-broad-v1.log`, +`field-driver-graph-configured-v1.log`, and +`field-driver-lowering-reference-v2.log`. The corrected Python result is +`native-codegen-recovery-2026-10-09/field-runtime-measurement-prepare-v1/measurement-controls-corrected.log`. + +All four permanent native field tests pass: 18 positive mains in both paired +profiles (36 executions), three expected pending failures in both profiles (six), +and Verify/Property suites in both profiles (four): **46 actual executions**. +Each compiled temporary Source is removed before launch. Exact success output, +exit 71 and exact failure streams, early RHS/preflight order, owning-field clone +independence, generic/inline/reflected bodies, and repeated borrowed enum owners +are checked. Eighteen frontend publication refusals pass separately; they supply +no execution credit. Source Debug/default and Release/optimized are paired with +normal Runtime Debug and Release respectively, not a full profile cross-product. + +The fresh compiler gate is `field-gate-compile-v2/gate-acceptance.json`, SHA-256 +`179bd113686808bd0d5ae2c1a6f0ffbd5e0d673912fbeb0ef861c4fa5f271143`. +`field-native-execution-v3/accepted-counts.json`, SHA-256 +`db72c9958a614156347fea8c099fc4c4cdb8013d5304d5d3a3c888853df09c9a`, +records the passing counts and exact test-harness/log/guard pins. Before/after +checks retain 1,012 compiler Source inputs, 68 measured runtime artifacts and all +32 fixture hashes. The guarded set includes the unexecuted actor-future fixture. +The measured normal/private v2 archives contain the new ordinary leaf; Resource +exports remain 58, private hooks eight, and ABI version one. Public ordinary +launchers supply no private Resource-retirement observation. Earlier failed +attempts retain their own scope and supply no linked acceptance. + +This earlier field gate did not execute actor handler updates. The bounded +checked-actor successor below records their later identity/state acceptance; +Resource-bearing replacement and wider unarchived reflected/generic compositions +remain separate obligations. The following typed pending-sum section records its +separate reference repair and native acceptance. Full coherent workspace, +fixed-inventory execution/object gates, exact-revision supported-host CI, GNU and +clean installed-package acceptance remain separate. Earlier workflow +38685454101 passed all four jobs at its own head; it does not validate this +change. The fixed 207-fixture/182-object denominators and full native-codegen goal +remain unchanged. + +#### Typed pending-sum terminal diagnostics + +Direct `handle` and pipeline failures now render the original wrapped receiver +through the existing typed formatter before consumption. Direct checked target +types are retained before evaluation; full pipeline result types remain separate +from ready-handler error payload types. Public pending depth stays visible, +secret subtrees are redacted, and earlier producer failures/once-only effects, +ready sum extraction and error typing retain their behavior. No native renderer, +runtime ABI or Source admission change is part of this reference repair. + +All 659 Comptime tests pass. Six permanent native controls cover nine programs +and pass 18 Source-deleted executions against both normal Runtime profiles, +with default Source policy. Three original leak witnesses remain byte-exact. +Additional checked Sources cover parenthesized named-callable views, named +view-parameter pipelines and retained view-parameter callbacks. Exact stdout, +stderr and exit status are asserted. Existing public borrowed-sum controls pass +both tests over twelve Sources and 24 Source-deleted native executions, including +unchanged public pending some/none/ok/fail messages and owning/view behavior. + +The fresh compiler gate is `target/native/pending-sum-gate-compile-v2/gate-acceptance.json`, +SHA-256 `4cb64bde30e531c6808e2be372e3aff262118fb9cb9b3280a5d5a4bb21f55c65`. +The secret matrix summary is `pending-sum-native-execution-v2/execution-summary.json`, +SHA-256 `95d9d1ba48a58799cbb2284013832a0c5320c9dc874240f2a5858b61f083129d`; +the public matrix summary is `pending-sum-public-regressions-v1/execution-summary.json`, +SHA-256 `65ec51c31cf7d268a6e35570e21fb0ea787492c42102b08e1cc274df39b58541`. +Both run under the primary checkout's shared `target/native`; their permanent +passing test loops establish the respective execution counts. Before/after +guards preserve 1,019 compiler Source inputs and 68 reused measured runtime +artifacts, with separate six-secret/twelve-public fixture hashes. No private +Resource retirement observation is claimed by these ordinary launchers. + +Full coherent workspace/inventory execution, other remaining language +compositions and exact-revision supported-host/installed acceptance remain open. +The fixed 207/182 denominators and full native-codegen goal are unchanged. + + +#### Checked actor handler field updates: bounded MSVC acceptance + +Original actor syntax, canonical actor type and handler ordinal retain one private +checked identity through original Send/Ask joins, handler execution and native +publication. Exact capability/state/message roles retain declarations, access +modes and physical slots. An accepting ownership traversal checks persistent +captures at every real normal exit after acquiring the response. Direct/projected +persistent views retain their actual backing owner and loan path. Early Respond +abandons an unfinished field commit; owning Handle replies are acquired before +publication. Nonresponding Return is sealed only for the original no-response +declaration, canonical nothing, an absent/exact-nothing operand and no view escape. +Responding Return and fallthrough, including `responds nothing`, remain unsealed. +Verify/Property factories retain the exact ordinary checked archive and protected +function table even without reflected callable plans. No Resource authority or +function-body exemption follows from these transitions. + +All eleven permanent native tests pass: **24 Source-deleted executions**, comprising +nine main programs in paired Source/runtime profiles (eighteen), Verify/Property +suites in both paired profiles (four), and the original nested receiver against +both runtime profiles (two). Main and suite controls pair Source Debug/default +with Runtime Debug and Source Release/optimized with Runtime Release. The original +receiver keeps Debug Source policy because it uses println. Exact stdout/stderr, +exit status and Source absence are checked; this is not the full profile +cross-product. **48 publication refusals** pass separately: eight invalid Sources +across Main, Verify and Property roots with both Source options, rejected during +checking before archive lookup while preserving the sentinel publication. +Refusals supply no process-execution credit. + +The fresh compiler gate is `target/native/actor-field-gate-compile-v2/gate-acceptance.json`, +SHA-256 `dff2c66924781008aa15566c025eac6286ef8837df45abe0e4465dd2aa5d9033`. +The execution summary is `actor-field-native-execution-v2/execution-summary.json`, +SHA-256 `3e8a2da1d475964aee5aec4e8d810bc5686f6f56f0b56aea19f6bfb0e8ccb41f`. +These paths use the primary checkout's shared `target/native`. Before/after guards +preserve 1,035 compiler inputs, 68 runtime artifacts and eighteen fixture hashes: +sixteen actor fixtures, unchanged original24, and the original nested receiver. +Actor Source03 is unchanged and also supplies the Verify/Property bodies. Source16 +pins nothing-valued helper returns after state updates, abandoned field commits, +and independent instances. The failed predecessor matrix remains historical and +grants no accepted execution count. + +Fresh normal/private MSVC Debug/Release measurements bind the new ordinary +`ActorReplaceOccupied` leaf alongside `StructReplaceOccupied`. The normal receipt +is `actor-field-normal-msvc-v1/receipt.json`, SHA-256 +`2b2b9cd529c451c216072dc2b563253149f409bd3ef9ea867a1a86da1c637069`; +the private receipt is `actor-field-private-msvc-v1/receipt.json`, SHA-256 +`35db08fbe46f339dad36731e24998497fdd77b459b0a283ed236cd124638db60`. +Resource exports remain 58, private hooks eight and ABI version one. Ordinary +launchers supply no private Resource-retirement observation. Measurements alone +supply no Source execution credit. + +All 21 non-Driver library targets pass 2,572 tests; the Driver library passes all +97 controls in a separate invocation. The seven core affected libraries total +2,211 tests: Codegen 135, Comptime 663, HIR 251, MIR 362, Runtime 389, Typecheck 376 and +Types 35. Resolve also passes all 56. All ten actor reference/frontend tests and all +eight backend-lowering tests pass, including all 182 run-pass fixtures. Original +checked initializer/capture Graph controls additionally pass four Source-deleted +executions using the Debug runtime with both Source options; original Source and +Support are removed before launch. Their hook descriptors retain exact checked +program identity, without live custody or handler Resource entry. Logs: +`actor-workspace-libs-except-driver-v4.log`, SHA-256 +`63d63df60d1431db6a9e8a4be1f9b9d5895263a7a1a7b00258e089d8383448f0`, +and `actor-driver-libs-reference-lowering-v2.log`, SHA-256 +`69e73107d884c4dc29adc5dd6e681bc5426da23a14e746668a445f2348b4a78f`. +The unchanged measurement helper passes 98 controls; formatting and whitespace +checks pass. These independent library runs are not the full workspace/inventory +execution gate. + +Resource-bearing replacement, wider generic/reflected and frozen-graph +compositions, real asynchronous scheduling, complete fixed-inventory execution, +exact-revision GNU/MSVC CI and clean installed-distribution acceptance remain +separate obligations. Responding-handler Return/fallthrough remain identified in +the actor note. The fixed 207-fixture/182-object denominators and full native-codegen +goal are unchanged. + +#### Mutable owned local-view backing: bounded MSVC acceptance + +An immutable local alias chain may now end at a mutable owned local or owned +parameter when the exact original DefId-to-LocalId mapping, declaration, +concrete type, mutable flag and active checked Owned fact agree. Alias slots +remain immutable. Typed initializer/field proofs, readiness, backing liveness +and non-owning storage remain mandatory; names, absent alias metadata and flat +global facts supply no ownership authority. Persistent may-created aliases keep +blocking owner changes across CFG joins, including after the last read. + +The unchanged original Source08 and observable nested bytes/list Source11 pass +four Source-deleted executions in paired Source/runtime Debug and Release. +Forwarding, borrowed calls, explicit clones and ordinary owned-field copies +preserve the original owner and independent copied values. All 753 affected +library tests pass: HIR 254, MIR 364 and Codegen 135. The log is +`target/native/mutable-view-hir-mir-codegen-v3.log`, SHA-256 +`856819c888d339a80aed7c8680648bee7713cf3ce56a2b69655204b3d3133983`. + +The full selected native matrix passes 31 tests and **64 Source-deleted +executions**: the complete local-view module, two exact actor persistent-view +controls and the three-Source ordinary borrowed-sum supplemental packet. The +four new-fixture executions are included in that total. Fifty-six executions +pair Source Debug/default with Runtime Debug and Source Release/optimized with +Runtime Release. Eight older local-view executions use only the Debug harness +runtime, including default Verify/Property suites. Overall runtime counts are +Debug 36 and Release 28; this is not a full Source/runtime cross-product. +Six existing publication-boundary checks pass separately: two temporary-origin +and four machine-owner-change refusals. They supply no process-execution credit +and do not turn admitted Source limitations into language bans. + +Fresh compiler gate: `target/native/mutable-view-gate-compile-v1/gate-acceptance.json`, +SHA-256 `b5a9edea952a73a8f945a867fe57e09325361a36c5e4cdc20a0adbdea30c8b0c`. +Execution summary: `target/native/mutable-view-native-execution-v1/execution-summary.json`, +SHA-256 `581f6f92ce8ec279d7886017c05155a0fac207d3c4d50b4167b02cfa521e2600`. +Before/after guards retain 1,036 compiler inputs, 68 reused runtime artifacts and +32 fixture hashes unchanged. Runtime source/ABI and measured bundles are +unchanged; no runtime rebuild or additional Resource authority is part of this +slice. + +Temporary roots, mutable alias slots, mutable view-parameter backing, missing +reflected outer-owner facts and borrowed-sum Handle aliases requiring immutable +backing remain separate implementation/proof limits. This change selects no +lifetime extension, loan expiry or write-through-alias rule. Complete Resource +transport, full coherent workspace/inventory execution and exact-revision +supported-host/installed acceptance remain open. The fixed 207-fixture/182-object +denominators and full 100% native-codegen goal are unchanged. + +#### Reflected local-view backing facts: bounded MSVC acceptance + +The checked producer retains the transitive binding dependencies of actual +local-view facts from their supplied concrete body map. Every binding DefId +joins its exact declaration and matching fact; forwarding and nested reflected +snapshots preserve original types, ownership modes and mutable flags. A visited +DefId set bounds malformed cycles. No Owned mode, global fallback, unrelated +enclosing capture or new session authority is synthesized. Missing or +inconsistent selected facts still refuse admission even when another enclosing +or concrete-instance map contains a valid fact. Existing HIR original-local and +typed-initializer joins, MIR persistent loans, liveness and cleanup remain +authoritative. + +All five affected libraries pass **1,797 tests**: Typecheck 379, Comptime 663, +HIR 256, MIR 364 and Codegen 135. The three producer and two HIR focused controls +also pass. Source12 formatting, full Rust formatting and whitespace checks pass. +Library log: `target/native/reflected-mutable-view-core-libraries-v1.log`, +SHA-256 `62f7c3649a3c21f8308c149e2735d08f082cdc439d3bd3a679351a78e1d4dcbd`. + +Source12 passes reference checks and two Source-deleted executions covering a +direct scoped mutable root, generic repeated reflected fields and nested +forwarded aliases. Those executions are included in the passing selected matrix: +32 native tests and **66 Source-deleted executions**, with six separate +publication-boundary checks. Fifty-eight executions pair Source Debug/default +with Runtime Debug and Source Release/optimized with Runtime Release; eight +older controls use only the Debug harness runtime. Overall runtime counts are +Debug 37 and Release 29, not a full profile cross-product. Refusals supply no +execution credit and do not create new Source bans. + +Fresh compiler gate: +`target/native/reflected-mutable-view-gate-compile-v1/gate-acceptance.json`, +SHA-256 `f7bfc37055da8f4ea9d659d369be1df137561bc18f2d12bed34bf940ed596a8d`. +Native summary: +`target/native/reflected-mutable-view-native-execution-v1/execution-summary.json`, +SHA-256 `abebafba9097acd0e366a9dde03102cbe48380485c8d51bdcc35dd01b44bec38`. +Before/after guards preserve 1,037 compiler inputs, 68 reused runtime artifacts +and 33 fixture hashes unchanged. First-slice 753/31/64 evidence retains its +original revision and scope. Runtime/helper/ABI and measured archives are +unchanged. + +Absent/mismatched selected root facts, mutable alias slots, mutable +view-parameter backing, temporary roots and mutable borrowed-sum Handle backing +retain their separate proof/implementation limits. No lifetime extension, loan +expiry, Resource authority or flat-map fallback is introduced. Full coherent +workspace/inventory, complete Resource transport and exact-revision +supported-host/installed acceptance remain open. The fixed +207-fixture/182-object denominators and full 100% native-codegen goal are unchanged. + +#### Mutable owned borrowed-sum backing: bounded MSVC acceptance + +The canonical explicitly viewed Handle admits a mutable root through its +original checked Owned local/parameter receipt. Exact declaration, root and alias +facts, initializer, concrete context and complete real function remain joined. +MIR rejoins the immutable original HIR record before independent current CFG, +tag/arm, field and remapping checks. Canonical conversion and private exact +callable-token transitions cannot reseal an arbitrary current tree. Generated +alias slots remain immutable and nonowning. Persistent move-only aliases keep +blocking owner changes after their last read; primitive/String copies retain +ordinary ownership and survive shell replacement. The detailed +[mutable-sum note](native_mutable_borrowed_sum_backings.md) records the proof and +Source controls. + +All five affected compiler libraries pass **1,807 tests**: Typecheck 379, +Comptime 663, HIR 261, MIR 369 and Codegen 135. The log is +`target/native/mutable-sum-core-libraries-v1.log`, SHA-256 +`6ab23ff17459a6000209349df28c39fcb77dcd903288974836eeb6a815f0a895`. +Focused HIR and corrected MIR groups each pass five tests, counted within that +total; full Rust formatting passes. This is not a full workspace/Driver gate. + +The selected native matrix passes **10 tests** and **46 Source-deleted +executions**: five additive mutable-sum programs contribute ten, the unchanged +ordinary packet 24, the supplemental packet six, and exact actor direct-view, +actor projected-view and reflected mutable-owned-view controls two each. +Every run pairs Source Debug/default with normal Runtime Debug or Source +Release/optimized with normal Runtime Release: 23 Debug and 23 Release +executions, not a full profile cross-product. Exact output, status, Source +absence and terminal-failure oracles pass. There are **zero publication +refusals** in this selection; expected runtime failures supply execution evidence. +The normal-runtime controls establish output/status/order parity, not new +private cleanup-counter observations, complete physical cleanup counts or +full Resource-corpus acceptance. + +Fresh compiler gate: +`target/native/mutable-sum-gate-compile-v1/gate-acceptance.json`, SHA-256 +`4e2cd94e7ccc2cb23ae22b14a65e66a547d4136e148cf7f2708294cfb147c819`. +Native summary: +`target/native/mutable-sum-native-execution-v2/execution-summary.json`, SHA-256 +`df43d3141cbef95c2c68402cb1fff94e78f5aa753c50d9ab523ad241d45ea7b4`. +Before/after guards preserve 1,046 compiler/source inputs, 68 reused runtime +artifacts and 50 fixture hashes; those fixtures are not the 23-program execution +denominator. Runtime/helper/ABI and measured bundles are unchanged. The +interrupted v1 attempt supplies no completed-matrix or additional acceptance +credit. Earlier 753/31/64 and 1,797/32/66 local-view results retain their own scope. + +Temporary-backed Handle roots, mutable view-parameter roots, borrowed binders, +absent/mismatched selected facts, alternate outer Default payload lifetimes, +scoped copied-Handle contexts without genuine selected source facts, +erased/capability/actor/Resource carriers and +baked/materialized callable composition retain their separate proof or +implementation limits. The lexical whole-owner temporary-backing contract at +`f865e3f1` remains unchanged and pending implementation and cleanup gates, +including unchanged Source09. No last-use expiry, implicit clone, invented +Source identity or wider escape is added. Full coherent local workspace and +inventory execution, complete Resource transport, wider actor/task compositions, +asynchronous scheduling and local supported-host/installed acceptance remain +open. GitHub CI remains disabled at the user's request; this bounded receipt +neither requires re-enabling it nor completes the local full-coverage goal. +The fixed 207-fixture/182-object denominators and full native-codegen goal are +unchanged. + +#### Private HIR storage: Windows CLI stack regression + +Two private retained values now use owned heap storage: the complete field-update +Source target and the complete Source-call certificate. The constructor values, +immutable target accessor, full target/certificate comparisons and controlled +remaps remain the same. Derived cloning and equality still operate on complete +pointees, retaining existing checked identity and nonce behavior. This changes +compiler storage/allocation cost; it adds no Source rule, proof authority, +Runtime helper or ABI change. + +The unchanged Windows CLI control +`native_setup_failure_keeps_the_object_builds_captured_frontend_events` +first reproduces the child stack overflow (exit 3221225725), then passes with +both storage changes. All **nine CLI capture tests** pass without changing their +fixtures, stack settings or object-build/manifest-lookup order. Full affected +libraries pass **765 tests**: HIR 261, MIR 369 and Codegen 135. Logs under +`target/native/`: + +- `local-regression-cli-stack-before-v1.log`, SHA-256 + `7abe5336663c42d415bb9d9c10657c26c4da24a3d08f4ee9e95aed816319f5f7`. +- `local-regression-cli-stack-both-box-v1.log`, SHA-256 + `fa7912c02f2e1274ae6ce558ebbfd3bab20bc051ce2eb3a2dc0ce44c0208b7e0`. +- `local-regression-cli-capture-all-v1.log`, SHA-256 + `e34111882036d853626fafd6d1cd667c565c8ad0d65ba49ab3282f7d36cdf016`. +- `local-regression-box-hir-mir-codegen-v1.log`, SHA-256 + `32552a35ddb6b487efc23d765200e2b763e3422908bcea8ab628afdf0a266535`. + +The same Rust layout probe records Expression **944 to 592 bytes**, Statement +**3,104 to 1,456**, FieldUpdatePlace **1,200 to 256**, SourceCallOwnership +**760 to 400** and CallOwnership **760 to 416**. Before/after HIR archives are +pinned respectively to SHA-256 +`cf0ff42e7a679478c82775246c4ae3510c489206419abf356685a590fb072f2c` and +`1c8e96896d5cddf34c12e10de1a13301c223a5544c9473535f16a2fc84f2f41b`. +Probe outputs and the after-archive receipt are in +`native-codegen-recovery-2026-10-09/ci-disabled-failure-audit-v1/`. +These layout measurements are not post-change frame measurements or a proof of +a universal stack bound. + +This local regression receipt supplies no new Source-deleted executions or +full workspace, inventory, GNU/installed-distribution or Resource acceptance. +The remaining coverage obligations and fixed 207-fixture/182-object denominators +are unchanged. GitHub CI remains disabled at the user's request. + +#### Frontend regression expectations after owned-view support + +The Driver HIR-failure capture test now uses a nested temporary field view in +place of the supported mutable-owned local view. It retains the exact ordered +comptime/Verify observations, absent BuildResult, HIR diagnostic prefix, current +stable-local-origin diagnostic and error source chain. The nested temporary +case records a current HIR implementation limit; it does not prohibit valid +Source or change the selected temporary-backing lifetime contract. + +The readonly-root fixture changes only its expected diagnostic distribution: +E0404 x13 and E0401 x10 still require all 23 errors. A payload matched from a +viewed enum retains its borrowed origin, so its field write reports E0401. This +matches the existing isolated borrowed-match control and the design's read-only +view rule. No compiler policy or fixture body changes. + +Focused Driver capture and compile-fail fixture tests each pass. All four +projected-assignment integration tests and the full **603-test fixture suite** +pass. The fixture suite supplies reference/frontend evidence, not full native +execution acceptance. Logs under `target/native/`: + +- `local-regression-driver-hir-capture-v1.log`, SHA-256 + `792dc6b6ab7b7f06285025c4c3532f65384c0ca9de433fcc41681d159dc06465`. +- `local-regression-projected-fixture-v1.log`, SHA-256 + `db6da58cb0332c43b32039a2908a29869710feef1e4515c5c0c1b9d990b74081`. +- `local-regression-projected-assignment-v1.log`, SHA-256 + `be4be8393b246b8ffae2f7bff72e636c6fedd521b37325ff9bcffd3ca7373d1d`. +- `local-regression-fixture-suite-v1.log`, SHA-256 + `df7168797468b39cc14403ab1398d69600faab17af85d3ba5c01fd333dbbee1b`. + +This is regression maintenance, with no new native execution or full +workspace/inventory acceptance. Coverage denominators and remaining obligations +are unchanged. GitHub CI remains disabled at the user's request. + +#### Pending actor send error parity + +The checked reference actor-message path again includes the rejected value in +its error, matching the existing unchecked interpreter and native Runtime. +The native conformance control requires the exact reference message before +comparing the complete native output and diagnostic stream. Runtime code and +the pending actor fixture are unchanged. + +`native_pending_actor_send_matches_interpreter_error` passes locally with the +supplied normal Debug Runtime bundle: one reference failure oracle and one +Source-present native failure execution, with exact output/status/error parity. +This is not a Source-deleted run or paired Debug/Release acceptance. The log is +`target/native/local-regression-pending-actor-native-v1.log`, SHA-256 +`2d1afb32c7f1a7e3da61b1ed0fc450af3f8c48280b5d5a147d48ea99563df694`. +All 68 reused Runtime artifact hashes remain unchanged. + +All 663 Comptime library tests also passed with this diagnostic repair in +`target/native/local-regression-driver-comptime-libraries-v1.log`, SHA-256 +`3aaebe06f8079f0e68e24ba40fa20c3aaa4d62d5fcb717b954c2b9b22d74853f`. +That combined invocation had a separate Driver test failure caused by its +missing required bundle environment; the successful focused rerun does not +turn that invocation into a fully passing Driver gate. Full workspace/native +acceptance remains open, and GitHub CI stays disabled. + +#### Canonical field owners in checked ordinary reflection + +A fresh diagnostic-only probe confirms that the required +`comptime reflected_generic_secret()` call in `interface_values.jett` failed at +the existing complete reflected-loop item comparison. The ordinary producer +stamped the source spelling `Box[Token]`, while the immutable checked loop +required canonical `Box[secret[string]]`. The probe changed only error context +and was removed before product verification; its log is +`target/native/local-regression-interface-probe-check-v1.log`, SHA-256 +`09fa4c39b0963f3036fabb4f21253cf05aa738f6eaefc2a46f861e88d2062280`. + +The interface fixture explicitly clones the actor's retained `initial` value +when initializing its mutable current item. Both actor bindings remain usable, +preserving the intended original/current behavior under move-only ownership. +This Source repair removes the earlier moved-value error without changing the +ownership rule or the fixture's observation oracles. + +The ordinary field adapter now validates its original call or physical pipeline +step and checked body before selecting canonical struct/bitfield owner data. +The complete canonical field table must match the retained checked program. +Field enumeration, field access and construction start/put/finish use the same +owner; later type arguments and existing metadata comparisons remain intact. +Source-visible alias names and nested `TypeInfo` remain distinct. Top-level +aliases and refinements keep empty field probes and empty checked loop schemas. +Reflection-only programs receive the ordinary checked context independently of +actors, field writes or Resources. No purity marker, Source ban, refinement +proof, Resource cursor authority, Runtime helper or ABI change is introduced. + +Three new controls cover actorless closed comptime and reference construction, +nested alias metadata, empty outer alias/refinement probes, and rejection of +copied same-span callees, type-argument slices and pipeline steps. The focused +selector passes **four tests**, including one existing control. All affected Comptime and Driver +libraries and the frontend/reference fixture suite pass **1,366 tests**: +Comptime 666, Driver 97 and fixtures 603. The four focused tests are included in +the Comptime total. Logs under `target/native/`: + +- `canonical-reflection-focused-v1.log`, SHA-256 + `f4c6d14f7ff235a07db77ccfcdacf80b656e12fd026fdba5036f1d4564651f3c`. +- `canonical-reflection-comptime-fixtures-v1.log`, SHA-256 + `d446e34c9b35b65ec564eecd874bc91ab91981bf4db46ca1e948b1e4d62c97ee`. + +All **nine CLI capture tests** also pass. A fresh default-profile CLI +`build --check --agent` of the actual interface fixture passes without E9001, +and its formatter check passes. The Release check is not accepted: the existing +fixture's debug-only `println` calls produce 58 E0362 errors. The fixture was +not changed to obtain Release acceptance; the actorless pure regression above +separately passes Debug and Release checking. Logs under `target/native/`: + +- `canonical-reflection-cli-capture-v1.log`, SHA-256 + `28f36c537a97ca32e44ef361d34b3f85b39f8c6525967ee74a3151ea5b2618a4`. +- `canonical-reflection-interface-check-debug-v1.log`, SHA-256 + `58b8a099048311576253ba2181377581d34273bf274a6e52c54ccf8671ec719a`. +- `canonical-reflection-interface-check-release-v1.log`, SHA-256 + `cba8bacb65d80059410fb5a3143e964b64a06e4872c177558af1addb087b0328`. +- `canonical-reflection-interface-format-v1.log`, SHA-256 + `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. + +`native_interface_values_match_interpreter` passes with one Source-present +native execution, default/Debug Source and the normal Debug Runtime. It requires +successful execution and exact reference/native stdout and diagnostic-stream +parity. The existing trace oracles retain redaction and the visible generic +interface value while rejecting hidden-token and `reflection-failed` trace +text. This is neither a Source-deleted run nor paired Debug/Release execution. +The log is `target/native/canonical-reflection-interface-native-v1.log`, SHA-256 +`ad3b588d3e58c2b6c4a67ced4e1a585ee7281cc12bbe1a3996c568d8b576a00b`. + +This receipt supplies no new Source-deleted execution, complete native corpus, +full workspace/doc-test, GNU/installed-distribution or complete Resource +acceptance. The fixed 207-fixture/182-object denominators and remaining native +coverage obligations are unchanged. GitHub CI remains disabled at the user's +request. + + +#### Temporary local backing: bounded MSVC acceptance + +The selected full lexical-backing contract now has an initial ordinary +function-body implementation. The alias declaration and direct named factory +are original nongeneric ordinary functions; the whole returned finite struct +backs an exact typed bytes/list field path. The native whole-owner proof +admits finite primitive/string/bytes/list/struct data; other wrapper, sum, +collection, recursive and runtime-authority shapes remain outside this slice. +HIR/MIR cover those endpoints, while +the checked reference adapter currently supports bytes only. List reference +transport, nested declaration scopes/loops, actor/Resource/generic/reflected +contexts and broader producer families remain owed. None becomes a checker ban +or a narrower completion denominator. + +Private original-declaration/producer/scope receipts and a single-use canonical +capture join the retained checked archive. Its immutable expected inventory +prevents current marker/function erasure from hiding a temporary obligation +while the archive is retained. MIR checks the complete graph and exact owning +acquisition before pruning, then pins the whole owner through lexical cleanup +independently of reads. Alias and forwarded slots remain nonowning. The checked +reference path uses whole-owner activations, payload-free tickets and exact +acquired-output packets. Existing native result acquisition precedes cleanup; +no Runtime helper or ABI changes are introduced. + +The actual checked Source staged-argument test establishes factory acquisition +after its temporary call loan and later handled argument, followed by the loan +End and whole-result move into the persistent owner. Genuine nonidentity local +compaction also passes. Synthetic disconnected-block preimages are refused by +the public Resource graph guard and pruner. Isolated private temporary replay +can restore their original graph and unchanged independent receipts, supplying +no Source-generated unreachable-block or public-pruning acceptance. + +The initial focused gate passes **27 tests**: reference 7, HIR 10 and MIR 10. +One added ordinary-entry regression also passes. The reference adapter gives +activations only to original ordinary root declarations, preserves interface +method dispatch outside that receipt family, and refuses edited claimed roots +or methods attempting to import a temporary frame. The mutual lookup also +joins a later original definition through its immutable +resolver name-span edge to the same declaration DefId, preserving exact unique +root identity, full typed AST, canonical name, namespace, signature and +activation. Two additional controls pass for mutual producer/caller/borrowed +callee execution in Debug and Release checking and adversarial edited facts +refused before acquisition. The full current core gate passes **1,842 library +tests**: typechecker 379, Comptime 678, HIR 271, MIR 379 and codegen 135, +including the written-declaration classification controls below. All compiler +focused controls are included in that library total. +Logs under `target/native/`: + +- `temporary-local-view-focused-v4.log`, SHA-256 + `cd3f32de06fed2008516fe2e3297f1e9a8310b5cfe01accba68caee4390b7251`. +- `temporary-ordinary-entry-focused-v1.log`, SHA-256 + `92da6acdfeaa20c57522508a9d12194d30fff5707da15b416338d70cd58cada3`. +- `temporary-mutual-entry-focused-v1.log`, SHA-256 + `02ba9c2b11b282641211dcc89aed18d2a34db2d882ca3c8d26492e245ea8200f`. +- `temporary-local-view-core-libraries-v4.log`, SHA-256 + `e1fa74d244fc54b25e3044a349fe321d25c94198694c17e05cae4de872ab13b9`. + +The earlier full fixture attempt failed with **546 passes and 57 failures**. +The diagnostic joined `json.json_tree_parse` to its original definition in +stdlib file 10016 while DefId 299 retained the `mutual:` declaration span in +file 10015. Requiring those spans to match incorrectly refused genuine checked +Source. The shared immutable resolver join repairs this without weakening +registration or inventing a new source identity. The failed log is +`temporary-local-view-fixtures-v1.log`, SHA-256 +`8fe22530ee635e56f2ef5b82ade5766dc2444d9b7307d4fe03e9ddf4b4b9e5bd`. +The successor fixture attempt fails with **549 passes and 54 failures** and +`temporary local view body needs its original checked entry or caller`. Its +log is `temporary-local-view-fixtures-v2.log`, SHA-256 +`34eb9d0f7e4d6a4bc4d0ec0f15f66494b99f792aaf3ab1abffbebc9bf037b0bb`. +The written-declaration classification repair requires an authentic immutable +original `VarDecl` with a written `view` initializer and its canonical variable +definition/name/span and checked `Other` fact. Existing viewed pattern and +iteration binders keep their ordinary borrowed paths and mint no temporary +owner authority. Complete nested AST lookup retains true temporary raw-entry +and unsupported-region refusals. All **12 reference call controls** pass, +including two new classification controls and the mutual controls, in +`temporary-written-view-focused-v1.log`, SHA-256 +`47be7144f0e2a90ea9879053d4cadf00613d300e7ec9957ee8ca7f21cbd9a33b`. +The real JSON namespace smoke passes one test in +`temporary-written-view-namespace-smoke-v1.log`, SHA-256 +`105f1d0a3bd723d623e3c57f568b18bdac2b79e1a29140fd76af403fbfde1ef5`. +The full successor fixture gate passes **603 tests** in +`temporary-local-view-fixtures-v3.log`, SHA-256 +`da41cfcf837bc381f0cfecd23f11e210f9c6f48c5f9f9f9926c20b9ee59c5d65`. +That fixture selection includes the namespace smoke. All **nine CLI capture +tests** pass in `temporary-local-view-cli-capture-v1.log`, SHA-256 +`317b28bbd05dbfff3f68519d86c2cb52bd7cc80066a67a342cf2f48efca00745`. +The successful successors do not turn either earlier failed invocation into a +passing gate. + +Original Source09 remains byte-exact SHA-256 +`c0f288b659bdf84bf87fb7f46576eb7cadd3b440c000f726b5b1c099ccfd83af`. +Its driver control now expects success. The new formatted Source13 covers +producer-once output, later direct/forwarded reads, an unviewed owned sibling +and explicit-clone independence. Source13 is unchanged SHA-256 +`48391e6a0fa7390f6d36cb190c8e4100d835218ca317232a64ec8ec228cd59aa`. + +The fresh MSVC gate passes compile-only acceptance, with zero Source execution +credit. The subsequent complete selection passes **23 tests and 46 paired +Source-deleted native executions**: Source Debug/Runtime Debug and Source +Release/Runtime Release, 23 per Runtime profile. No full profile cross-product, +legacy Debug-only executions or publication refusals are counted. The two +new bytes temporary controls account for four executions; the rest are eight +stable projected-view, eight scoped-call, two mutable-owner, one reflected +mutable-view and two actor persistent-view regression tests. Their passing +regressions grant no temporary-owner acceptance in actor or reflected contexts. +The helpers remove the written Source before executing the binaries and assert +exact reference/native stdout, stderr, status and debug observations. All +1,056 compiler/Source, 68 reused Runtime, 64 fixture and 182 original run-pass +input guards remain unchanged. + +- `temporary-local-view-gate-compile-v2/gate-acceptance.json`, SHA-256 + `fb39b49856ad89bbdfb5777008a1b9365b6ca4d95a04940a89e5285be4eb6891`. +- `temporary-local-view-native-execution-v2/execution-summary.json`, SHA-256 + `314b6e92a7cd0235bbf92292e49df9e92ebb86490de290982ae23c6865876c0a`. + +Both pin the actual gate image SHA-256 +`a1f9a700a5219d91bbbf784a5768dc9ec87558a78a72ecd3ae7a9be7a8e28a14`. +The earlier matrix attempt failed, with zero accepted execution credit in +`temporary-local-view-native-execution-v1/execution-summary.json`, SHA-256 +`dcc869793d07c776035c7a95e88c122e8e310b04298f569606b8a7a7c97d38c8`. +Its partial observations are not added to the successful successor. + +Source09 empty output and Source13 producer/read/clone markers establish the +selected output behavior, not physical owner lifetime or absence of hidden +cloning. Native mid-scope allocation/destruction timing, +full connected lifecycle/failure traces, earlier-owner cleanup ordering and the +remaining full-contract gates are still open in +[the temporary local-view note](native_temporary_local_views.md). + +This bounded receipt does not establish full workspace/doc-test, complete native +corpus, GNU/installed-distribution, complete Resource or whole-language native +acceptance. The fixed 207-fixture/182-object denominators and remaining coverage +obligations are unchanged. GitHub CI remains disabled at the user's request. + +#### Bounded temporary List transport — 2026-10-16 + +The [List transport note](native_temporary_list_views.md) records accepted local +MSVC behavior for exact retained generic observer instances and List endpoints. +It preserves canonical mutual DefId/body joins, registered Source authentication, +the immutable written-VarDecl classifier, all prior 23 selected tests and original +Source09/13. Existing enum/iteration borrows gain no temporary-owner authority. + +Five new List and twelve existing reference controls pass together: **17 passed / +0 failed** in temporary-list-transport-focused-v1.log, SHA-256 +cac01d28dac98fada049d521bd80b9139d842d10e9495ebb4861998d7dabd90c. +After a warning-only Rust or-pattern parentheses cleanup, selected successor +checks pass **1847 compiler library tests** (135 + 683 + 271 + 379 + 379), +**603 fixtures** and **nine CLI capture tests**, all with zero failures: + +- temporary-list-transport-core-v1.log, SHA-256 + d86f8177789a9aed444317787a49a81854a0e5cd1ca3eb7101ea2d9dde8ebd7b. +- temporary-list-transport-fixtures-v1.log, SHA-256 + d7d77d9c9b9641516890253923c3d822b8b2af2727a532e92f11cf40c1e6594a. +- temporary-list-transport-cli-v1.log, SHA-256 + 56c1b715305b34e36cbb7bf0f537b8ae1e94a510668201b59935adce20db6033. + +The first All matrix fails exit 101: authored Source14 incorrectly uses +match/some/none optional patterns, so its checked-reference parse fails before +List native build/execution. That aggregate accepts **zero** executions, despite +two earlier retained selectors passing; partial results are not added to the +successor. All guards pass in the failed attempt. Its summary is +temporary-list-view-native-execution-v1/execution-summary.json, SHA-256 +55751b44050f635c90b0972c39f0f26938ee91171a5fc74448da7d82387be711. +Its List diagnostic log is SHA-256 +907fdd85438cc9b59e77c39d1957e175c00bfc323720e4885b22556fb8ae5550. + +The fixture-only canonical bare-handle repair preserves numeric/String producer, +alias, mutual generic, list/bounds/clone observations and fixed expected stdout. +Formatted Source14 is ae81376d38a94f9890146138609ee5df641cf49bca9e15530464a50fea6d1c99. +The frontend receipt in temporary-list-fixture-handle-repair-v1/frontend-receipt.json +is SHA-256 c8eb735fb7d852c9a3388ba556548f96b1a865df8d07aa9165a98cb4519a45f0: +format and Debug/Release checks exit zero; both frontend checks have zero errors +and warnings. The initial format-only and compile-v1 observations did not prove +fixture validity and remain bound to their earlier Source inventory. + +The matching fresh compile-v2 gate passes compile-only acceptance; its own +Source-execution credit is zero. The focused repaired List selector then passes +**one test/two paired Source-deleted executions**. The complete successor passes +**24 tests/48 paired Source-deleted executions**, split **24 Source Debug/Runtime +Debug** and **24 Source Release/Runtime Release**. The new List control accounts +for one test/two executions within those totals. There are no publication +refusals, legacy Debug-only runs or full profile cross-product claims. All 16 +selection log hashes and exact zero-failure terminal counts were independently +verified. The full 48 include the earlier focused 2; the distinct accepted packet +total is 48, not 50. The historical byte 23/46 receipt is not added either. + +- temporary-list-view-gate-compile-v2/gate-acceptance.json, SHA-256 + 40ad0d06d7a5585659b3e39311d60363cbd39848b2384bd5fe9d4760dd4c2968. +- temporary-list-view-native-focused-v2/execution-summary.json, SHA-256 + ae05fa796b52584804f5004fbd3315a0839a5963fc5a92760dec15b49a4e0411. +- temporary-list-view-native-execution-v2/execution-summary.json, SHA-256 + a2b39a53402f88709493b2b022d97d6d8ce342c8b46b62252ce7007d7cd1ee84. + +The compiled and executed gate image is SHA-256 +76f8a7020fb82fd53d573324f5a38730df68261581add438025243798f3a0c82. +All 1058 compiler/Source, 68 reused Runtime, 65 fixture and 182 original run-pass +guards remain unchanged. The fixture inventory adds only Source14 to the prior +64. All six count/launcher sources, original Source09/13, selectors and fixed +expected stdout remain; the ready-v2 runner changes only the fixture pin. + +This bounded receipt supplies no physical destruction/no-hidden-clone proof, +actor/Resource temporary authority, full connected lifetime contract, +Resource/Graph integration, suite-factory, full native inventory/corpus, +workspace/doc-test, installed-host or whole-goal acceptance. The fixed 207/182 +denominators and rough planning estimate remain unchanged. CI remains disabled. + +#### Canonical value conversions: bounded compiler and native regression acceptance — 2026-10-16 + +The [canonical conversion note](native_canonical_value_conversions.md) records +a private receipt around completion's single actual canonical coercer execution. It +retains complete immutable before/after Programs and the same borrowed type +arena, then replays the actual occurrence-specific callable tokens and adapters. +It does not independently remint expected output or accept a caller-provided +current table. Actor, mutable-sum and temporary peers retain original-proof +checks and exact current-operation comparisons. Graph, baked and temporary +joins see the same unchanged-before archive. Completion publishes its local +candidate only after all peer, callable-sealing, HIR and backend-type checks +succeed. No Runtime, ABI, provider, custody or Source semantics change. + +The first positive attempt fails 0/1 before conversion because its authored +main-body asserts are invalid Source (E0340). The fixture-only repair replaces +them with typed call/coarsen locals for both required occurrences. All Rust +proof assertions remain intact, including exact distinct sealed plans, one +genuine temporary owner, unchanged original Source inventory and repeated +completion. The corrected positive passes one test in Debug and Release +checking; all **nine focused receipt controls** and the existing **one genuine +Graph/baked/temporary composition control** pass. The positive is included in +the nine, not added to them. Synthetic hidden-archive and NaN comparisons +remain private comparison evidence, without Source or native execution credit. + +- canonical-conversion-compound-positive-v1.log (failed), SHA-256 + f4891373dff5eacdf2c9a1e175f8eb67f6eff0ca6f33324bcd812334142f1c99. +- canonical-conversion-compound-positive-v2.log, SHA-256 + fa986958329d859de65978f79ec9ea14a815a77c66fa0533f890b1023f01abca. +- canonical-conversion-focused-v1.log, SHA-256 + 38c3a8d6d82b51e86113dfb633525d6242c1531d6200c67f31f71a04e5ddaf5c. +- canonical-conversion-graph-baked-temporary-v1.log, SHA-256 + fb2129d0a82a5e1c62b7dcd2c13859a9add435fad6e18e83bca3604d79d08f06. + +Broader successors pass **1,856 compiler library tests** (Codegen 135, Comptime +683, HIR 280, MIR 379 and Typecheck 379), **603 frontend/reference fixtures** +and **nine CLI capture tests**, all with zero failures. Focused compiler +controls are included in the HIR total. Exact logs under target/native/: + +- canonical-conversion-core-v1.log, SHA-256 + b712b29d7f69f505131282b590218c5242e3af5ede42494b01717b45b14691a0. +- canonical-conversion-fixtures-v1.log, SHA-256 + 89dd139d83b8821d845158c2a74fd644270155f87cee02da2c54c1c182c5f94f. +- canonical-conversion-cli-v1.log, SHA-256 + 45cafe79af2294857dbb443e83c7b4db81e728c3cfa0e7ae4ae821b8d26ef279. + +The outer sequence reports command-input-too-long warnings while repeating VS +setup on its fourth/fifth wrapper calls. The actual Cargo gates and overall +sequence exit zero using the initialized environment. This is not fresh +installed-host evidence. + +The fresh MSVC compile-only gate then passes with zero Source execution credit. +The current-image existing-view regression selection passes **24 tests and 48 +paired Source-deleted native executions**: 24 Source Debug/Runtime Debug and +24 Source Release/Runtime Release. No publication refusals, legacy Debug-only +runs or full profile cross-product are counted. All 16 pinned selection logs +and exact zero-failure terminal test counts pass independently. The helpers +remove the written Source before native execution and require exact reference/ +native stdout, stderr, status and debug observations. All 1,060 compiler/Source, +68 reused Runtime, 65 fixture and 182 original run-pass guards remain unchanged. + +- canonical-conversion-native-gate-v1/gate-acceptance.json, SHA-256 + 41ca89ea180268d5df296355dc45fded45c98bcd0676cfa1d86fc896cd94f637. +- canonical-conversion-native-views-v1/execution-summary.json, SHA-256 + fda6e2118a9a00409a45ea53429f490b1010242523877d9feb305f70e2097394. + +Both pin the compiled and executed gate image SHA-256 +b37a2366042a3e0326825156eb151e72c3026cc3d593516400b78f5cb1fc6695. +The existing bytes/List temporary, stable, scoped-call, mutable-owner, reflected +and actor persistent-view regressions grant current-compiler regression +acceptance. They do not execute the new compound required-callable or Source17 +Verify/Property suite witnesses; those native obligations remain unproven. +Historical byte/List matrices are not added to this distinct 48-execution total. + +Archive identity now compares all four current optional Arcs even without +original data. The separate unapplied suite transition must preserve the +transaction, add native_suites and required_cache to both exhaustive patterns +and comparisons, and keep every peer on the unchanged before archive. Source17 +still requires actual suite Verify/Property order, distinct ordinal occurrences +for equal sampled values, its configured trials, Source-deleted Debug/Release +executions, measured zero provider acquisitions/live custody, cleanup and +same-context reentry. None is inferred from a closed Source or absent Network +parameters. Exact actor-handler/actor-state-view callable-sealing transitions +and nested callback-return identity coverage also remain unproven. The draft +actor witness first needs valid checked Source; a predicted mismatch is not an +observed failure. + +This bounded receipt does not complete full Resource/Graph suite integration, +physical temporary destruction/no-hidden-clone proof, the selected lifetime +contract, the full native inventory/corpus, workspace/doc-tests, installed-host +or whole-goal acceptance. The fixed 207-fixture/182-object denominators and rough +planning estimate remain unchanged. GitHub CI remains disabled. + +#### Private Resource observation v2: local checks and archive checkpoint — 2026-10-16 + +The double-private recorder and strict v2 decoder are integrated across 16 Rust +paths. Production ABI 1/layout 3, 58 production and 8 private C exports, +Counts 72/ArchiveInfo 32 bytes and receipt/normal-launcher manifest version 1 are +unchanged. Private test protocol 2/JTRRP002 and JTRHC002 add bounded observation, +not Source/custody/provider authority. Five acquisition-pipeline stages stay +separate; successful Property history and supported installed int64 bits remain +actual runtime observations only when captured by a matched native run. See the +[observation checkpoint](native_resource_private_observations.md). + +**407 focused local checks pass**: 389 Runtime library, 12 report decoder, one +Source17 history oracle and five host decoder controls. Six actual MSVC archives +are measured in matched Debug/Release profiles: normal Runtime+Launcher and +private Runtime. Their exact target/native/ receipt pins are: + +- private-observation-v2-normal-msvc-v1/receipt.json: + 4e41eca939e70f4c1ea162639b3f714510726a118a200d38bb89bddc6d575e8c. +- private-observation-v2-private-msvc-v1/receipt.json: + d19cc38b1d38a6f84783ad0fd1dc219cea722d88ce5e1db8ddba2b025d64dbe0. +- private-observation-v2-measurement-paired-v1-terminal.json: + 411f53b3f27b98962ed900c336bd1a7c89f88c7836c7ccebdccd3b53ee6aa52f. + +Both measurement processes exit zero and are reaped. Source/config/helper/tool +maps and 68 historical artifact guards remain unchanged. The actual terminal +records 7.4653 GiB maximum cache usage, within its 8 GiB limit. This is a bounded +measurement observation, not a claim that the cache ceiling was maintained +through earlier historical runs. Archive measurement grants zero Source +executions. The Source17 normal pin is updated only from its actual receipt; +its Source is unchanged. + +The verifier retains the actual receipt bytes. Its successor checks physical +linker identity/digest without rewriting recorded argv and authenticates the +known terminal duplicate-liveness serialization defect through the exact genuine +preflight receipt. Four real command baselines and 12 negative controls pass. +The full compiler-before guard passes. The accepted fresh compiler gate now +binds 1,089 compiler/Source, 69 fresh Runtime, 33 measurement and 68 historical +artifact inputs plus 27 helper entries and 182 original run-pass inputs. + +At this checkpoint the fresh compiler gate is **accepted**, with zero Source +execution credit. Native ingress and whole Source17 v2 now have separate bounded +**accepted** execution receipts. Historical protocol 1 Suite/View/lifecycle receipts retain +their original scope and are not promoted to current-v2 coverage. Actor 8827 +separately accepted two unchanged Sources/four Source-deleted Debug/Release +processes before this Runtime change; its required-value summary +f9f2e093ac3806fd48c24f426b5eace86ef745697fe46f494ed8e44dbe2aaa5d and +direct-getter summary cb01bf437b69d6906e526035e955050d906c654cbe356ac7e7065c3aef591fdd +remain historical and grant no fresh v2 regression credit. Per-kind ordinary +storage, private artifact retainers, normal custody/reentry, physical lifetime, +whole inventory/workspace/installed-host acceptance and the full native goal +remain open. No coverage percentage is inferred. GitHub CI remains disabled. + +The accepted compile-only gate is +private-observation-v2-gate-compile-v3/gate-acceptance.json, SHA-256 +3d05e4499c87c4d606ca515ca1ab2c7c9ece96331749bb564b62dfe017f78cc3; +its inner terminal is 55d5c2889f7fa5fae6e19a668c3cee6f07e71c9a7bdfd35bfc75de219691b3e6 +and outer private-observation-v2-compile-supervisor-v4/terminal.json is +2276b0b86a8469365642ce61179a323573a013e1b255cedcaab8c474a75866a5. +The image is 40,563,712 bytes, SHA-256 +7a626568c675d99fe8464843efe69735232d9e0b8fb0d786853193801fde6205. +Both terminals complete with exit zero, reaped quiet owned processes, disposed +root handle, released leases and no errors. The outer watcher observes a maximum +7.5453 GiB within its 8 GiB limit. Five pure PowerShell and eight pure Python +identity controls pass separately from the 407 local tests; none grants Source +execution credit. + +The first gate attempt remains unaccepted after a recycled-child-PID monitor +refusal following successful Cargo compilation. The second keeps unchanged +guards and releases leases but refuses the cached test image. Both failed +outputs remain preserved. For the accepted third gate only the test target +timestamp changes; Source bytes remain 1eb3316afe08153c48f4da8e9e33cc233ac30896d6b3695947f5e45d0c4b7e75 +and Source17 remains unchanged. No failed-attempt observations are promoted to +accepted native execution. + +The fresh native ingress gate accepts 32 actual processes: eight Source +processes/10 report-header entries, 22 host ABI controls and two pure writer +controls. Four linked artifact rows supply no execution credit, and the Source +processes cover two distinct original Source hashes. Acceptance is +native-bounded-ingress-execution-v3/execution-acceptance.json, SHA-256 +56c746359c5fcd7ab73399ed98f64de5fe80d048d94f2d4a33b0706d09c773fb; +its catalogue is 4347e7b4a69a43b43466954538f07dad69a99c83aee5a0d127eabb97d231ae39. + +Whole unchanged Source17 accepts eight processes/four objects/eight private +entries across paired Debug/Release normal and private launchers. Both private +Property profiles observe two 100-trial entries and 6,404 history records each; +Verify observes four history records per private profile. Every private +incomplete flag is zero, all five separate acquisition counters are measured +zero, and same-context reentry passes. All eight statuses are zero; Source/ +objects are deleted, checked-program Weak/input-Arc roots released, and reaped +executables/PDBs removed. Exact installed int64 bits are checked by the Rust +oracle against original hashed bytes, without JavaScript-number rewriting. +Acceptance is native-bounded-source17-execution-v3/execution-acceptance.json, +SHA-256 a59a3d4cadf23a05d30b7ecceb034f501dd40d02f404570f33309d787192087b; +the harness summary is 271caf5fbd93f9fd4733185188ad270b344f2dcebba0f126ed93df984ffdb5e6. + +The accepted v4 runner uses 15-second helper startup, 5-second CIM operations, +a 30-second single cleanup deadline and 3,600-second active deadline. The two +preserved prelaunch refusals—physical-tool PATH and helper startup—grant no +Source execution credit. Normal telemetry remains null and unmeasured. The +per-kind ordinary storage, private retainer, normal custody/reentry, physical +lifetime and full native acceptance obligations remain open; full_acceptance +stays false and GitHub CI stays disabled. Historical protocol 1 limitations above +are not rewritten or promoted. + +#### Original actor and retained temporary-owner finalization: compiler checkpoint + +The consuming Lowerer now retains its exact mint-time temporary-proof inventory +through a separate initial actor transition. The actual sealer's BEFORE values, +finite earlier-declaration dependencies, open cells and complete actor/sum +checkpoint are validated before the first OnceLock write. The unchanged 970-byte +Packet/list witness is +a1f3fa32e14828daebb472f316608683b303187eba63b987edbab56d381a2f0f. +No Source restriction, marker erasure or current-table recapture grants coverage. +See the [finalization note](native_actor_temporary_owner_finalization.md). + +All 35 focused HIR controls pass. Broad local regression passes **2,553 distinct +tests**: Codegen 135, Comptime 691, HIR 311, MIR 384, Typecheck 379, 603 frontend/ +reference fixtures and 50 CLI tests. Focused controls are included in HIR 311. +The three logs under target/native/ are: + +- actor-temporary-owner-core-regression-v1.log, SHA-256 + 0745179c292cc76b55510000b8530bd17073909042a09f57d51d8af913f5c11b. +- actor-temporary-owner-fixture-regression-v1.log, SHA-256 + 66e11f2cff69c9394ca1aa9e36167af3662d654a2148267ba6038ddf9f4d4e49. +- actor-temporary-owner-cli-regression-v1.log, SHA-256 + a077a1eb56e2638311665196eb3e980f9ce548fe61a6927fa2ec5924dd030b14. + +The fresh compile-only gate is accepted, with 1,093 compiler/Source inputs and +image SHA-256 c308ff075c6dc904684867a3854e6f9d82961c1e417a01a05dd899dbe138237d. +Gate acceptance is private-observation-v2-gate-compile-v4/gate-acceptance.json, +SHA-256 290a8b403a76aec6b10d7e1e0b5e27cce268a78cc6600a717761c82914498160. +Compilation grants zero native execution credit. The separate native selector +passes one Rust test and accepts one unchanged Source/two objects/two native +processes across paired Debug and Release. Both produce exact observed:5 plus LF, +empty stderr and exit zero. The written Source is deleted and host object +dropped before executable-only launch; children are reaped, executables and +temporary directories removed. The outer process is quiet/reaped, its root +handle closed and lease released, with no failures. + +Acceptance is +native-bounded-actor-temporary-owner-execution-v1/execution-acceptance.json, +SHA-256 076ac84a3717a56fafa9a5450edc96003a2ec3035e50c4fd10ef3eada9fe78b4. +Its 5,863-byte schema-v1 native-evidence/actor-temporary-owner.json is +2b19eea41a33e6a94d89050443cad25062e1802e3a76159b7243c982525bbe34; +the actual process receipt is +9782953688918c25d1d42adc161145ba03d9a7fd21934e0b79227d2f44446fb6. +The intentionally incomplete prerelease terminal is not the acceptance leaf. Historical actor 8827 and Resource e916 observations retain their own +revision, runtime and execution scope; neither is relabeled as current Source17 +regression or added to the new actor execution count. Physical actor/callback/queue +ownership, same-context reentry, changed canonical producers, broader proof +transport and the full native goal remain open. CI remains disabled. + +#### Direct Resource optional/result replacement: local checkpoint + +Distinct live owned optional/result Resource shells now use the existing +composite Replace contract: stage the RHS once, validate both owners and exact +failure companions, retire old before publication, and retire staged RHS once +if old cleanup fails. Typed failure packing/extraction preserves scalar versus +owned representations. Sealed nested lexical owner records supply exact Drop/ +clear behavior through ordinary exits and canonical remapping. Existing Source +policy, capabilities, ABI 1/layout 3 and private report protocol 2 remain unchanged. +See the [Resource sum replacement note](native_resource_sum_replacement.md). + +The frozen local receipt passes **3,038 distinct checks**: 409 Runtime, 1,976 +compiler/phase, 603 frontend/reference fixtures and 50 CLI tests. Its SHA-256 is +325751dfe2bf1335c12114667a7a21017bdacb6a449d82abcc5e72991510405f. +Six fresh normal Runtime/Launcher and private Runtime Debug/Release archives are +measured. Their normal/private/paired receipt hashes are respectively +be980ae18006737304ae69af4d254f78cad15c51fe8e6226c6a2b9ca9cf4b6b4, +454be0bf810ecc500e362fd99de37e02a909ca3abfc4d89c92afb06e8d1cfc50 and +b725c561901d53bc2fd27b5ae6f5cf07265051be498efc6e4b7a7b1a283395a9. +Measurement supplies zero Source execution credit. + +The fresh compile-only gate is accepted with 1,111 compiler/Source inputs, +image 03a6ffebbcd1b5a56d986d61bddbaec3ebf164c7af9c480d068df19e2ba4924d, +and gate receipt f8e0623b59056bf3a6da1b5f94c4bcd54948a83df67863bbfd8c516cfb719be3. +Quiet/reaped completion, unchanged guards and released leases supply zero +Source/host execution credit. The separate object-emission control passes all +six original Sources in Debug/Release and adds no linked Source execution. +Single entry accepts 38 child rows/38 attempts/12 fresh programs; retained- +Session reentry accepts 38 child rows/76 attempts/38 fresh programs. Across both +matrices that is **76 native children, 114 attempts and 50 fresh programs**, with +a union of six distinct Sources. Each matrix observes 22 child exits zero and +16 expected status 71, empty streams, zero incomplete report flags, retired +logical Resources and empty aggregate ordinary storage. Written Source/support +is absent before launch; single-entry compilation artifacts remain for reuse, +whereas reentry closes and removes its complete temporary directory. These are +private observations and do not establish checked-program Weak expiry or +per-kind physical lifetime. Exact acceptance pins are in the linked note. +Preserved SDK-preflight and shared-harness import failures receive no accepted +credit. Prior actor/Runtime receipts retain their +historical scope and do not renew current Source17 coverage. Physical/per-kind +retainer observations, normal custody/reentry, wider replacement/transport and +full workspace/inventory/installed-host acceptance remain open. Full acceptance +stays false; rough tracking is unchanged and CI remains disabled. + +#### Pure replacement suite execution and current Source17 regression + +This follow-up closes the earlier pure-suite execution obligation; previous +checkpoint counts retain their scope. + +Two ordinary preparation controls pass (two native selectors ignored), and the +v4 compiler-only gate is accepted with 1,111 inputs. The six measured archives +are reused, not remeasured. Separate pure-suite execution accepts one unchanged +Source, eight native processes, four emitted object groups/four distinct hashes +and eight private entries. Private Property records 400 actual trial calls, +with 2,404 history records/profile; Verify records four/profile. Flags and all +five measured counters are zero, with matched private same-context reentry. +Normal custody/reentry remains null and unmeasured. All child statuses are zero, +streams empty; Source/object deletion, checked-root/input-Arc release and child/ +executable/PDB cleanup are observed. Exact receipts and remaining limits are in +the [Resource sum note](native_resource_sum_replacement.md). Separate current +Source17 regression also accepts eight processes/four object groups/eight +private entries/400 private Property trial calls, with 6,404 Property history +records/profile and four Verify records/profile. The same v4 gate and reused +archives bind both runs. These counts remain separate from the earlier +replacement checkpoint and historical Source17 receipts. Physical/per-kind +retainers, normal custody/reentry and full acceptance stay open; rough tracking +and disabled CI are unchanged. + +#### Ordinary corpus attempt: unaccepted supervision terminal + +The accepted current inventory remains 602 registered tests: 576 ordinary and +26 opt-in, with 1,191 fixture-input hashes. Its two listing processes execute +zero test bodies. The subsequent ordinary attempt ends without a complete +libtest result or accepted corpus credit. Its partial stdout has 43 ok status +lines and 3 ignored lines, no failed status line and no terminal summary; stderr +is empty. These partial observations are not a completed pass count or a +measured native Source/child/object count. + +Supervision refuses a CIM observation of an owned link.exe whose ExecutablePath +is null (RuntimeError: expected JSON string). The final cleanup probe also +collides with the existing probe-00045.json artifact. At the frozen terminal, +the root process is reaped and its handle closed, but owned-tree quiet is +unconfirmed and the shared lease is unreleased; complete=false. Before/after +input guards have no failures, and final cache 8,395,662,274 bytes remains below +the 8,589,934,592-byte cap. This is an unaccepted monitoring/cleanup result. + +Under target/native/native-bounded-ordinary-execution-v1/, execution-terminal.json +is e367039f70c81efef89603fc37e3084323b907fa930ca4f4fce491de36f220d0; +test-process.json is +f93bf7c70e11376ccfce3478dc649b0be3a3b2543667b5145689fe19a3016749. +The later recovery is accepted under +ordinary-native-unaccepted-cleanup-v1/recovery-acceptance.json, SHA-256 +0fdfdc10cbecd5e30f79669301e2d14222cf2518592a5e45600453669efa28fc. +Two independent CIM snapshots are empty, their helpers are reaped with handles +closed, and the exact held lease is released. Recovery terminates no processes +and executes no Sources. It does not convert the earlier run into acceptance. + +The v13 supervisor controls pass six positive and 25 negative in-process cases, +with zero child processes, native API calls or filesystem mutations. Their +actual result is native-adapter-v13-root-control-run-v1/result.json, SHA-256 +f6ac4118b88bc888868dbc9742e76ceffc21d67fb55459885fbd7272fca4b900. +These controls grant no ordinary corpus or Source execution credit. Successor +ordinary execution remains pending. The prior accepted +replacement/pure/Source17 scopes and the fixed 182 manifest, remaining opt-ins, +full workspace and installed-host obligations are unchanged. Full/physical +acceptance remains false; rough tracking is unchanged and CI stays disabled. diff --git a/docs/completed/native_codegen/native_actor_callable_sealing.md b/docs/completed/native_codegen/native_actor_callable_sealing.md new file mode 100644 index 00000000..8a8f0a48 --- /dev/null +++ b/docs/completed/native_codegen/native_actor_callable_sealing.md @@ -0,0 +1,210 @@ +# Initial actor reflected callable sealing + +This bounded transition handles initial checked lowering when a real reflected +field getter occurs directly in an actor handler or constructor. Actor snapshots +are already sealed when the actual callable producer replaces bound plan tokens. +Their renewal must derive from that same actual output, preserving all checked +actor receipts, target ordinals, role facts and persistent-view projections. + +The sole CheckedCallableSealing receipt remains the actual token authority. Its +complete before/output Programs, archive identities, checked Arc, borrowed type +arena, raw source epoch and recorded optional getter/adapter edits are unchanged. +The native baked.current renewal also remains unchanged. An ordinary getter's +real empty adapter slot is recorded by that shared producer as None to Some of +the same sealed getter token; the actor transition does not reconstruct tokens. + +A private original-Lowerer context may derive an opaque actor renewal from the +retained output. It authenticates the entire token output, full original actor +inventories and snapshots, exact checked Arc/type arena and complete empty initial +archive before changing snapshot fields. An empty archive alone supplies no +admission. A normal later actor-changing seal refuses until its producer path is +represented. With no actor delta, the shared existing peer flow stays unchanged. + +An actual actor delta with temporary-owner markers may proceed only through the +separate original-construction, BEFORE-value-only finalization path. It retains +every marker and complete Source certificate, proves an exact finite earlier- +declaration dependency closure, and fills each original cell once before the +actor epoch's retained after table exists. Changed canonical producers and later +unrepresented compositions still refuse as compiler gaps, never checker/source- +validity rules. No combined Source cycle is claimed from structural reasoning. +The separate unchanged Packet projection control now requires genuine lowering +and full archive/backend/epoch validation; its new native proof is a separate +measurement. See [the temporary finalization note](native_actor_temporary_owner_finalization.md). + +The protected actor stage must remain exact after existing initial peers. The +original finalizer runs the existing initial sum sealer once on that retained +stage and validates full actor/sum receipts. One private epoch then retains the +raw predecessor source Arc and complete final table before attaching its edge to +changed entries/views. The stored tables contain no new edge, and sealed plans +refer only to the raw predecessor. This forms a finite immutable graph; actual +Weak expiry controls remain required rather than inferring lifetime from exit. + +Ordinary Seal.sources retains the same raw pre-seal Arc as the actual receipt; +existing outer callable canonicalization happens during comparison. Source +matching retains full actor metadata. If ordinary exact matching does not suffice, +it uses only existing protected original/baked/Graph/required restoration to reach +a represented checkpoint, authenticates current actor/sum/archive gates and the +exact source epoch/checked identities, then compares the complete restored table +to that edge's private actual checkpoint. Only after that proof may a non-executable +comparison view restore historical actor metadata for affected entries. Outer +operations, headers, siblings, functions and peer inventories remain exact. + +The view cannot grant Source-call, Generated, Resource, provider or actor-entry +permissions. It never overwrites historical sources with a current table, erases +actor roles or searches arbitrary ancestors. Graph conversion_sources remains its +separate private recipe. Later actor edges, unsupported appended-function epochs +or canonical/bake/Graph/Resource compositions without an exact represented path +continue to refuse explicitly. + +The genuine direct-handler Source stays byte-identical with SHA-256 +`a90ef2641c1a3937e02718368f699d4cf2c51d3f2b47ee5d15579a450aca4a92`. +Its fallback returns 7 and actual callback returns 5. The separately measured +native checkpoint below returns `observed:5\n`, empty program stderr and exit +zero. The separate regression +first requires genuine checked initial lowering, actual handler/getter/adapter +identities, full HIR/backend/archive validation and idempotent completion in both +checking modes. Six actor controls add epoch identity, whole-table and foreign +attachment refusal, actual Weak expiry, and transactional context/arena/later-entry +refusal, plus the separate unchanged Packet Source for temporary-owner composition. +They reuse the shared receipt's existing four producer controls. + +Root measured the unchanged-compiler RED: Source checking/preparation succeeds, +then genuine initial lowering refuses the stale full actor Snapshot with +"actor handler changed its sealed original native body" at Span 219..568. The +first Debug checking iteration stops the loop; Release RED was not separately +measured. Evidence is target/native/actor-initial-sealing-red-v1.log (SHA-256 +3b9392ea75b384dfa2ce38e65c31d0fb5b434ac6c1175e3e2709ad6e35d5ce2b). + +## Accepted initial-epoch checkpoint + +The evidence below belongs to committed checkpoint +`8827c57321e6e0d74cee6fd30dd21a7f11940d8c`, before the bounded temporary- +finalization extension. It is preserved as measured evidence for that initial +repair. Its then-refused Packet control and native counts do not establish the +new temporary path; fresh extension gates are recorded separately. + +The focused gates pass **18 HIR tests**, each exercising Debug and Release +checking modes: one genuine direct-handler regression, six actor epoch controls, +seven prior required-bake controls and four shared actual-sealer receipt controls. +The direct regression checks genuine original lowering, exact handler/getter/ +adapter identities, full HIR/backend/archive validation and idempotent completion. +The six controls include actual Weak expiry of the compiler epoch/source/checked +graph. That is a compiler graph lifetime proof, not a measurement of native +physical actor/callback ownership or same-context reentry. The checker-valid +Packet/list temporary composition is tested only for its explicit unsupported +HIR transition; that refusal earns no coverage credit. + +Focused logs under `target/native/` are: + +- `actor-initial-sealing-regression-v1.log`, SHA-256 + `c264ed43b905cb606783b369f910269398afb90461807ff06d77fce0b94fdf71`. +- `actor-initial-sealing-epoch-controls-v1.log`, SHA-256 + `fb5eb720c858ba2d06bc6ace711c976cbfe58194cf08cce224a9eb6551a38a5c`. +- `actor-initial-sealing-required-regression-v1.log`, SHA-256 + `cbf5d9f7c106a5b1d7d157b9f8eee9b2594b864cf44bb5e83d0db7bb5f1290ec`. +- `actor-initial-sealing-shared-receipt-v1.log`, SHA-256 + `7dcd33452384791ddcf90e8fda918098725e81c9a180d9f29a2c4b528f9c91a5`. + +All 1,086 current compiler/Source hashes match +`native-codegen-recovery-2026-10-09/actor-initial-sealing-current-source-inputs-v1.json`, +SHA-256 `29cabb7a126c5b5c6a88fef9fc92f40aa212266ded574455690ae04a4a3b6241`. +The fresh broader gates pass **2,546 distinct Rust tests**: 1,893 compiler-library +tests (Codegen 135, Comptime 691, HIR 304, MIR 384 and Typecheck 379), 603 +frontend/reference fixtures and all 50 CLI tests. The 18 focused controls are +included in HIR 304. Current logs under `target/native/` are: + +- `actor-initial-sealing-core-v1.log`, SHA-256 + `df6b93899fae5a20eeb458695c19704b1c45ae89f1505a7cea44c1f9a0bc34ed`. +- `actor-initial-sealing-fixtures-v1.log`, SHA-256 + `4f4b9f2c97125cc94b3f95ddfe2aec86ba67225f707c09ea52c79695732aaf12`. +- `actor-initial-sealing-cli-v1.log`, SHA-256 + `c1ab1bd14776d0b453b983c16099049673c0a520f0fb45cc3f82ff5e40d9241c`. + +The fresh gate and both authoritative native selectors pass as recorded below. +An earlier Required attempt completed its Rust/native checks, but the outer +PowerShell5 terminal receipt publication failed. Its authoritative marker stays +incomplete and grants **zero accepted observations**; a staged passed receipt +is not acceptance. The failure record is +`native-codegen-recovery-2026-10-09/actor-initial-required-runner-publication-failure-v1.json`, +SHA-256 `5fb78b897f0f9c68a1875edd9b97985f9c6db6f13fc02e2ca475f519140eea22`. +The corrected runner published the fresh accepted results below. Neither that +incomplete attempt nor the historical required-comptime checkpoint is added to +the current execution count. + +## Historical paired ordinary native output checkpoint + +Two independent selectors pass one Rust test each, separately from the 2,546 +regression tests above. The unchanged required witness +`ce01b25e20a4955067e87fbc7389f1957b21fd10de6e76575e337f2f98111665` +and unchanged direct getter +`a90ef2641c1a3937e02718368f699d4cf2c51d3f2b47ee5d15579a450aca4a92` +each emit two objects and execute two native processes. Current acceptance is +**two Sources, four objects and four native processes**, without adding the +historical checkpoint or the incomplete attempt. Each Source pairs Debug with +normal Runtime Debug and Release with normal Runtime Release. All four outputs +are exactly `observed:5\n`, with empty program stderr and exit zero through the +public object/link path. The direct getter's distinct fallback value remains 7. + +For both selectors, isolated entry Source is deleted and host object/reference +values are dropped before launch. Native children are reaped; executables and +temporary directories are removed afterward. All 1,086 Source and 68 reused +Runtime before/after guards pass, with no Runtime rebuild or retained raw +binary/object copies. Raw stream capture and all four exact log checks pass. +The observed-process runner reaps and disposes its root handle, reports no +timeout, monitor refusal or cleanup failure/actions, leaves no observed owned +process live and releases its lease. + +Receipts under `target/native/` are: + +- `actor-initial-sealing-native-gate-v1/gate-acceptance.json`, SHA-256 + `b262ed230d008e48542023de76dca92d3223fb4ab86f81f446b1496a55133f9b`. +- `actor-initial-required-native-execution-v2/execution-summary.json`, SHA-256 + `f9f2e093ac3806fd48c24f426b5eace86ef745697fe46f494ed8e44dbe2aaa5d`; + its `observations.json` is + `a8570fe7ec099020857d051e4e1ec9a1aa81a114c35f8e216118cd5320d18bf3`. +- `actor-initial-direct-getter-native-execution-v1/execution-summary.json`, SHA-256 + `cb01bf437b69d6906e526035e955050d906c654cbe356ac7e7065c3aef591fdd`; + its `observations.json` is + `485272b8288e9838458b468647be15c4bce67d384586e538fe9997d03f9f71af`. + +The executed fresh compiler image is SHA-256 +`e8f90a3db844cc299ad88159c2cdca8ead1509f6ac36f27f4c17efa40c135a16`. +These are bounded ordinary output and process/file cleanup observations. Compiler +Weak expiry and process cleanup do not measure native physical actor/callback/ +queue ownership or same-context reentry. Every row retains +`physical_lifetime_measured=false`, `reentry_measured=false` and +`full_native_acceptance=false`. + +The accepted required-bake checkpoint `44ebf1348b66c244164dd6034ed9bdb495ce03c5` +retains its [2,539-test and paired required-witness native evidence](native_actor_required_bake.md). +Its older observations are not relabeled as the direct getter's evidence. The +full native-codegen goal remains open. The new bounded temporary-owner +composition passes the current HIR/regression and paired native checkpoint +below. Changed temporary canonical producers, later actor +edges and broader Graph/Resource/checkpoint transport remain separate obligations. + +## Temporary-owner extension: current compiler checkpoint + +The original Packet/list witness remains 970 bytes with SHA-256 +a1f3fa32e14828daebb472f316608683b303187eba63b987edbab56d381a2f0f. +Its separate exact initial-finalization path passes 35 focused HIR controls and +the broader 2,553 distinct tests: Codegen 135, Comptime 691, HIR 311, MIR 384, +Typecheck 379, 603 frontend/reference fixtures and 50 CLI tests. The 35 focused +controls are included in HIR 311 and are not added again. Exact logs and the +transition boundary are recorded in the +[temporary finalization note](native_actor_temporary_owner_finalization.md). + +The fresh compile-only gate is accepted with zero native execution credit. +The separate native selector passes one Rust test and records one unchanged +Source, two objects and two native processes, paired Source/Runtime Debug and +Release. Both outputs are exactly observed:5 plus LF, with empty stderr and exit +zero. Source is deleted and the host object dropped before executable-only +launch; children are reaped, executables and temporary directories removed. +The authoritative execution receipt is +076ac84a3717a56fafa9a5450edc96003a2ec3035e50c4fd10ef3eada9fe78b4; +its schema-v1 observation file is +2b19eea41a33e6a94d89050443cad25062e1802e3a76159b7243c982525bbe34. +The earlier initial actor and committed Resource observation receipts retain +their historical scope. These tests do not rerun Source17 or measure physical +actor/callback ownership or same-context reentry; full native acceptance stays +false. diff --git a/docs/completed/native_codegen/native_actor_required_bake.md b/docs/completed/native_codegen/native_actor_required_bake.md new file mode 100644 index 00000000..7e34516d --- /dev/null +++ b/docs/completed/native_codegen/native_actor_required_bake.md @@ -0,0 +1,201 @@ +# Checked actor required-value baking + +This is a bounded HIR transition after actor-owned required cache proofs exist. +It preserves the language rule that explicit `comptime` is closed and pure. +The private checked ordinary producer authenticates its complete input/archive, +constructs each typed value once, matches every result against its private cache +proof, preserves all original locals, and authenticates every added binding. +Only then does it mint a temporary receipt retaining the actual before/after +Programs, exact successful cache, checked session and borrowed type arena. + +The actor transition authenticates the whole produced output against that +receipt. It preserves original ActorHandlerEntry receipts/roles and constructor +view inventories, and renews complete snapshots from the receipt-retained output. +Constructor view receipts, facts, locals, backing roots and initializer projections +stay original. Handler role view projections are not reconstructed. The renewed +table is installed in Graph/reflected/temporary peers only at that fixed private +boundary; no public edited table or Generated role can mint replacement authority. + +Actor required owner matching joins the exact checked actor DefId/ActorId, +handler or field ordinal, original declaration/body spans, namespace/origin, +response or constructor type, and immutable original HIR operand/header. Equal +values, labels and spans do not substitute for original checked provenance. + +Complete snapshot bodies compare exact floating-point bits, including NaN +payloads and signed zero, using the existing HIR block comparison. + +## Actual callable sealing and the current execution table + +The sole actual callable sealer now retains a private ephemeral receipt of its +complete input/output Programs, checked Source session, borrowed type arena and +source epoch. It replays only the exact recorded getter/adapter token slots, +identified by function, preorder expression and field/adapter position. Bound to +sealed pairs retain their exact admission, return plan, occurrence, materialized +source, checked reference Arc and Graph-source lineage. Initial adapter insertion +must have its unique actual paired getter. An expected table or caller-owned +edit vector cannot create the receipt. + +The existing baked view authenticates its complete current input and exact +checked/cache/property/original identities before replaying that retained real +output. It preserves the existing mutable-sum replay and validates full actor +snapshots before and after. It then retains the actual sealed table required by +MIR; MIR's complete archive equality remains unchanged. This already-materialized +baked renewal does not admit an actor-body-changing delta. The separate +[original-Lowerer initial actor transition](native_actor_callable_sealing.md) +now authenticates that bounded initial delta from the same actual sealer receipt +and a private exact epoch; later actor-changing compositions remain separate. +A callable plan moved into a generated adapter can also change the full execution +table while leaving the handler snapshot unchanged. + +The four focused receipt controls cover materialized Some-to-Some renewal, +idempotence, foreign checked/cache/arena/archive identities, previous/current +token or occurrence splices, complete unrelated output edits and transactional +refusal. The separate bounded original-Lowerer initial-getter path is recorded +in [the initial actor sealing note](native_actor_callable_sealing.md). Later +actor-changing, valid temporary-owner and broader Graph/source-lineage contexts +retain their separate obligations. + +At checkpoint `44ebf1348b66c244164dd6034ed9bdb495ce03c5`, the focused gates +passed **11 HIR tests**, each exercising Debug and +Release checking modes: seven actor required-bake controls and four actual +sealing-receipt controls. The whole unchanged witness control now completes +actual value conversions/sealing, validates HIR/backend output and enforces the +strict full execution-table equality required by MIR. The actor controls also +cover equal-valued occurrences, constructor persistent views, exact +receipt/cache/arena replay, complete input/output edits, transactional late +refusal and NaN/signed-zero bits. Historical checkpoint logs under `target/native/` are: + +- `actor-required-sealed-focused-v1.log`, SHA-256 + `c7a954b62fb632a263d81bd31f2a2e440017e12c56cdf3ea825f0455527cf7e5`. +- `actor-required-sealing-receipt-focused-v2.log`, SHA-256 + `5b85981dfdbb8f8cfc1266d7505ec648875c7eb73ecc8e4cb732b552c3032d0c`. + +An earlier receipt-test filename filter selected zero tests and supplies no +acceptance evidence. The broader successor gates and separately measured native +output checkpoint below pass within their stated scope. + +The initial gate passed six controls and failed the arena-negative test because +`TypeInterner` is not Clone: cloning its borrowed reference retained the same +arena. The test now uses an independently checked same-Source arena. This was a +test repair, with no production change for that failure. The failed log is +`target/native/actor-required-bake-focused-v1.log`, SHA-256 +`5c9505545c7704cb3cf14118ff7bd9dd8f76b0e8a27e17c0dc8bc74c2ca1b553`, +and receives no accepted-gate credit. + +That accepted checkpoint passed **2,539 distinct Rust tests**: 1,886 compiler-library +tests (Codegen 135, Comptime 691, HIR 297, MIR 384 and Typecheck 379), 603 +frontend/reference fixtures and all 50 CLI tests. The 11 focused controls are +included in HIR 297. Historical checkpoint logs under `target/native/` are: + +- `actor-required-sealed-core-v1.log`, SHA-256 + `7c335abafed1644f9207db8c01bf577316afbb198c87dda58c78e137aebaedfc`. +- `actor-required-sealed-fixtures-v2.log`, SHA-256 + `91a63d3ecf3b365d02d71a61e8b19009002b6b5f497fb494b67da514ea9c1dc2`. +- `actor-required-sealed-cli-v1.log`, SHA-256 + `45bde9c8f4c0132b4689115af273529664cd77111a00a8a143f8d4f5a431d957`. + +The first fixture invocation named a nonexistent test target and ran no tests; +it supplies no acceptance or compiler-failure evidence. At that checkpoint, +all 1,081 compiler/Source input hashes matched +`native-codegen-recovery-2026-10-09/actor-required-sealed-current-source-inputs-v1.json`, +SHA-256 `bd9dd1206287a75f93ff746bc833cf003a111bd579d8842f1bbab53edeed197c`. + +The first fresh native gate compiled, but its actual unchanged-witness test +failed during Debug object emission: MIR reported `Resource lowering differs +from its original checked HIR archive`. No object or native process was produced +and no observations file exists; this attempt receives **zero native execution +credit**. The existing reference-output assertion completed before that failure. +The gate receipt is +`target/native/actor-required-bake-native-gate-v1/gate-acceptance.json`, SHA-256 +`7308274ed4aa96745fe58ab664b178f030a83b4d5a1ef3dadc96572d14cb5228`. +The execution summary is +`target/native/actor-required-bake-native-execution-v1/execution-summary.json`, +SHA-256 `fda2927016246aaf492d252d77710e8ee0213e0741c6117119ad19dcdc967a61`; +its failed tests.log is SHA-256 +`f7f73426842e8a8910eb4228a0090408b23df994cff8b1b7874c0715b0cc489a`. +That failed attempt's 1,079 Source and 68 Runtime before/after guards passed +unchanged. It prompted the actual-sealer repair above. The later accepted +output checkpoint below supplies separate successor evidence. + +The existing whole-witness HIR control was then strengthened to complete actual +value conversion/sealing, validate HIR/backend output and require the strict full +execution-table equality used by MIR. It first failed because the retained +current execution table kept stale sealing tokens. Its RED log is +`target/native/actor-required-execution-archive-red-v1.log`, SHA-256 +`9282eef9c67c067e6823d0bc58dc55839ebc96895c953e28a339792f3cbc50df`. +The earlier seven-control/2,520-test receipts and 1,079-input map predate that +strengthening and cannot establish acceptance of the later production repair. +The current focused control and fresh broader successor gates are green after +actual-sealer receipt renewal. The native successor attestation below is separate. + +The next Rust native test passed with two actual paired output observations, but +its outer v2 attestation remained incomplete because PowerShell formatting +wrapped the harness summary. Its summary keeps accepted observations at zero; +the two raw successful outputs are bounded facts, not promoted acceptance. +The fresh v3 raw-process runner attestation below supplies acceptance for its own +two executions; the earlier raw outcomes are not added to that count. + +The HIR unit controls alone supply no native execution credit. The accepted frontend checkpoint +`2e0ebb0e9e27d11eee0e68611fc726803c79a65a` validates retained lazy closure +environments before creating function values. Required expressions cannot retain +unavailable actor-state or handler-parameter captures, including nested bodies or +same-name namespace constants. This snapshot transition preserves that gate. +That frontend checkpoint's CLI checks passed for the unchanged whole witness +and closed-capture control in both checking modes; all four free-capture controls +refused with E9001 in both checking modes. Its frontend-only evidence is +`native-codegen-recovery-2026-10-09/actor-required-frontend-cli-v3/check-result.json` +(SHA-256 `e6c1c28dbb746559d8f39c265a09a3a1995c3e9d5fbc9069214c6efe022170c1`). + +## Accepted required-witness native output checkpoint + +The original whole witness remains byte-identical with Source SHA-256 +`ce01b25e20a4955067e87fbc7389f1957b21fd10de6e76575e337f2f98111665`. +A fresh accepted v3 native selector passes **one Rust test**, separately from the +2,539 compiler/fixture/CLI tests above. It emits **two objects and runs two +processes**, pairing Source Debug with the existing normal Runtime Debug bundle +and Source Release with normal Runtime Release. Each process returns exactly +`observed:5\n`, empty program stderr and exit zero through the public object/link +path. This is one genuine complete Source, not a reduced actor or altered callback. + +The isolated entry Source is removed before launch, and the emitted host value +and reference result are dropped. The launch directory contains only its +executable. Both children are reaped; executables and temporary directories are +removed afterward. No per-case raw binary/object copies are retained. All 1,081 +Source and 68 reused Runtime before/after guards pass, with no Runtime rebuild. +The outer Rust test is reaped without timeout and uses raw stream capture; all +four exact log checks pass. + +Receipts under `target/native/` are: + +- `actor-required-sealed-native-gate-v2/gate-acceptance.json`, SHA-256 + `5fa7b7be9040cfeb47ec916998fc85ce1b8be5443238830a9d0d7a0ccdfb8170`. +- `actor-required-sealed-native-execution-v3/execution-summary.json`, SHA-256 + `2777801e8a42d77750755aa8cf1d57fd557507b6b61fb9c39438a41545e0d627`. +- `actor-required-sealed-native-execution-v3/observations.json`, SHA-256 + `3adeff2c8a254704150c0951107fb7be4c4fb61e9b8377008202cce570011207`. + +The executed fresh compiler image is SHA-256 +`db795907492bf9ca9a219f5d794bec247270fdf496d76e7c51a29e78dc934114`. +These are accepted ordinary output observations. File deletion and host-value +drops do not measure physical Runtime ownership, actor queues, callback +finalizers or same-context reentry. Both rows retain +`physical_lifetime_measured=false`, `reentry_measured=false` and +`full_native_acceptance=false`; the full native-codegen goal remains open. + +The bounded original-Lowerer initial sealing/snapshot epoch now has its separate +[checkpoint](native_actor_callable_sealing.md). Remaining independent obligations +include later actor-body-changing sealing, valid temporary-owner composition, +unrepresented Seal.sources checkpoints, actor-owned Graph producer transitions +and Resource-hook-bearing actor required execution. A callback plan that moves into a generated adapter +need not change its handler snapshot; no unconditional sealing failure follows. +Physical lifetime, actor queues, callback ownership/finalizers and same-context +reentry remain separate from ordinary native output. Do not erase actor snapshots, +change Source, disable Resource transport or infer broader native coverage from +this bounded HIR transition. + +A later [initial actor sealing checkpoint](native_actor_callable_sealing.md) +records fresh Required and Direct native selectors separately. The historical +2,539-test/two-process facts above remain bound to `44ebf134`; they are not added +to the later native count. The bounded original-Lowerer delta does not resolve +valid temporary-owner, later actor/Graph/Resource or native physical lifetime +obligations. diff --git a/docs/completed/native_codegen/native_actor_required_values.md b/docs/completed/native_codegen/native_actor_required_values.md new file mode 100644 index 00000000..87aa36df --- /dev/null +++ b/docs/completed/native_codegen/native_actor_required_values.md @@ -0,0 +1,137 @@ +# Actor-owned required values + +The language already permits a closed, pure explicit `comptime` expression in an +actor initializer or receive body. This work repairs compiler provenance; it does +not select a new language rule or permit runtime actor storage in a closed value. + +The unchanged responding-handler witness is +`crates/jett_comptime/src/resource_execution/fixtures/actor_handler_required_callable.jett`. +Its Source SHA-256 is +`ce01b25e20a4955067e87fbc7389f1957b21fd10de6e76575e337f2f98111665`. +Before this frontend change, Debug and Release CLI checks reached required-value +discovery and reported E9001, `resource execution has no exact checked body`, at +its `comptime stored()` expression. The receive body is retained checked Source; +the worker simply did not enumerate actor owners. + +The first change projects metadata for the exact original actor initializer or +handler occurrence. It retains the checked program Arc, actor DefId/ActorId, +handler ordinal, physical original expression, complete selected lexical scopes +and result TypeId. An actor handler has no generic-function projection. Its root +and scoped type-name projections come from the checked handler body, and cached +proofs preserve the original actor owner instead of pretending it is a function. + +This required-metadata route does not install a Resource execution cursor. +`validate_executable_cursor` and `install_function_body` continue to reject an +ActorHandler root with `WrongPurpose`. Copied AST nodes, foreign sessions, +changed owners/ordinals, mismatched scope paths and runtime entry remain refused. +The new controls also retain closed-worker rejection of free actor state and +typechecker rejection of capability-requiring calls in required expressions. +Frontend controls evaluate the complete unchanged witness in both checking modes, +but do not execute its runtime main. + +## Required-worker closure environments + +Closedness also applies when required evaluation constructs a lazy inline +function without invoking its body. Before constructing that value, retained +ExplicitComptime and NamespaceConstant workers match the full original +parameters, return annotation and body in the same checked program. They inspect +its resolved Variable/Param identities: external lexical bindings must exist in +the actual captured interpreter scopes. Own formals and body-local declarations +remain deferred, and exact namespace values, named functions and type +declarations keep their existing registry roles. A same-name namespace constant +cannot supply a missing actor state field or handler parameter. Nested lazy +bodies cannot hide an unavailable outer capture. + +This generic worker check preserves pure local/parameter captures, own lambda +formals and locals, exact named/constant references and genuine nested returned +closures. It does not call the constants-fallback variable lookup, install a +runtime ActorHandler cursor, mint Resource custody or renew a native snapshot. +It enforces the existing closed-expression rule without selecting new Source +behavior. + +## Accepted frontend checkpoint + +The successor focused gate passes **eight Rust tests**, each exercising Debug +and Release checking modes. It includes the complete unchanged responding +handler, distinct initializer owners, exact metadata refusals, closedness and +capability rejection, checked lexical type scopes, unavailable returned/nested +captures and retained pure captures. The focused log is +`target/native/actor-required-frontend-focused-v2.log`, SHA-256 +`1ccd5c4983869f82ee69ab396a7036280459278926f781fe9cc095920ba1da81`. +At that frontend checkpoint, broader gates passed **1,615 distinct Rust tests**: 691 Comptime, +286 HIR, 603 frontend/reference fixtures, 26 CLI unit tests and nine CLI capture +tests. The eight focused controls are included in the Comptime total. The CLI +build succeeds and its Cargo JSON identifies the exact executable checked below. + +That checkpoint's rebuilt CLI passed **12 expected checks** across Debug and Release +checking modes: the unchanged original witness and the pure-capture control +each exit zero; all four unavailable-capture controls each exit one with E9001. +The original witness has no errors and retains one unused-variable warning. +These were frontend checks; runtime main was not executed. At that checkpoint, +the exact 1,075 compiler/Source input hashes were guarded unchanged. + +Frontend checkpoint logs under `target/native/` are: + +- `actor-required-frontend-core-v2.log`, SHA-256 + `f6c053d6f676397918d50e6bb86b9b100a05850b5ca9da6b85699210f58cbc59`. +- `actor-required-frontend-fixtures-v2.log`, SHA-256 + `a48c70eea4da541ca856acb98f3e19584d4dda964208a352acea07b8bcda9a6a`. +- `actor-required-frontend-cli-v2.log`, SHA-256 + `b2fe9d9afea22edd9fd695310a70b7da9ba93a3928b5eed8d1ad6c871020f43c`. +- `actor-required-frontend-cli-build-v2.log`, SHA-256 + `6547c594cf5de096cf1ce9e3707bd4c08b65feebd3f1ad9b27de918ff27c6d66`. + +The frontend CLI receipt is +`native-codegen-recovery-2026-10-09/actor-required-frontend-cli-v3/check-result.json`, +SHA-256 `e6c1c28dbb746559d8f39c265a09a3a1995c3e9d5fbc9069214c6efe022170c1`. +It pins executable SHA-256 +`01e5e6cac3ef94c89ac465c354c0673a8a55adfa3e2cfcf9a7e07f617bf061ae` +and all 12 Source/log hashes. The compiler-input map is SHA-256 +`bc346d6b796710cf17c6c9064b12b1c34bc0c4896d95a5417507ff79bbfc48f3`. + +The genuine returned-capture negative control first failed with zero diagnostics +where one was required; its log is +`target/native/actor-required-free-capture-red-v1.log`, SHA-256 +`224ac709a668bc9c1591d648d4d9866d725234c574f35f8d9fb2e37dbe2246eb`. +Exact-current-image v2 checks also confirmed that defect. Earlier direct CLI v1 +checks used a stale image and supply no current acceptance or refusal evidence. +These discovery runs receive no successor acceptance credit. + +Frontend cache construction and checking alone supplied no HIR or native actor +acceptance. The separate [checked actor required-bake transition](native_actor_required_bake.md) +now joins exact actor owners and renews complete snapshots from the actual +checked materialization receipt. The HIR owner matcher no longer declines an +actor owner merely because its metadata kind is ActorInitializer or ActorHandler. +The later actual-sealer receipt also renews the complete baked execution table +from its exact token edits while validating unchanged full actor snapshots. It +does not itself admit actor-body-changing sealing deltas. A separate +[original-Lowerer initial transition](native_actor_callable_sealing.md) now renews +that bounded initial snapshot using the same actual receipt and an exact private +epoch. The original frontend evidence above remains a historical checkpoint. + +The remaining boundaries are independent: + +1. The bounded original-Lowerer initial snapshot transition is separate from + later actor-body-changing sealing. Later and unrepresented checkpoints still + require their exact receipt/source-lineage path and every canonical, temporary + and peer proof. The checker-valid Packet/list temporary-owner composition + remains an unsupported HIR/native gap in the full goal, with no coverage + credit for refusal. A plan moved into a generated adapter need not change + its handler snapshot. +2. Actor-owned Graph producer transitions remain separate from ordinary checked + required baking. +3. In a retained program with Resource hooks, actor-owned required evaluation + still reaches the unchanged runtime cursor gate. That requires a separate + exact occurrence-bound execution route. Temporarily disabling transport would + bypass transitive absence/custody handling and is not an acceptable fallback. +4. The [historical paired ordinary native checkpoint](native_actor_required_bake.md) + records two Source-deleted executions of the whole unchanged required witness with + `observed:5\n`, empty program stderr and exit zero in matched Debug/Release. + Physical lifetime, queue/callback ownership and same-context reentry still + need their own observations; ordinary output alone cannot establish them or + complete the native-codegen goal. + +The later [initial actor sealing checkpoint](native_actor_callable_sealing.md) +records fresh Required and Direct native selectors and their exact provenance. +Those bounded outputs do not close the temporary-owner, later proof-composition +or physical lifetime/reentry obligations above. diff --git a/docs/completed/native_codegen/native_actor_temporary_owner_finalization.md b/docs/completed/native_codegen/native_actor_temporary_owner_finalization.md new file mode 100644 index 00000000..6f4725b2 --- /dev/null +++ b/docs/completed/native_codegen/native_actor_temporary_owner_finalization.md @@ -0,0 +1,165 @@ +# Original actor and retained temporary-owner finalization + +This is a bounded compiler transition for the existing checker-valid Packet/list +composition with an initial actor reflected getter. Source policy is unchanged. +The original direct and required-comptime Sources remain byte-identical. The full +combined Packet Source remains the existing TEMPORARY_SOURCE control, SHA-256 +`a1f3fa32e14828daebb472f316608683b303187eba63b987edbab56d381a2f0f`. + +The actual callable sealer remains the only token producer. Its complete input, +output, exact checked Arc, borrowed arena, raw source epoch and Getter/Adapter +occurrence receipt are unchanged. A separate private producer accounts for the +Original.canonical_producer OnceLocks; table equality compares marker Arc identity +and cannot by itself authenticate those cells' open-to-closed state. + +The consuming Lowerer registers each exact proof at mint before body and argument +clones, then closes registration before canonical passes. This non-Clone, non- +Default registry is bound to the original module/check addresses and arena. It is +not reconstructed from a caller Program or checked Arc. A by-value fresh wrapper +owns both registry and unpublished Program. The clone-based ordinary wrappers get +no capability. No public API returns pending markers or the registry. Failure +drops the private construction; cloning alone supplies no rollback for shared +OnceLocks. Ordinary/no-actor/no-delta behavior keeps the existing late setter. + +On an actual actor snapshot delta with markers, preparation consumes that fresh +registry and authenticates the sole sealer's real input/output. It joins every +minted P to exact checked Source function/root/statement facts, unique owner/alias +initialization, complete locals and rewritten projection. Each cell must be open. +Canonical candidates are cloned only from the retained BEFORE owner and normalized +by removing only its own exact outer marker. All canonical operands, Source call +certificates, order, spans, types, nested plan identities and foreign argument +proofs remain intact. The normalized OUTPUT owner must equal that candidate, +including callable Arc identity and float bits. A changed canonical producer +refuses until its separate current-canonical transition is represented. + +Proof closure visits expression-owned markers and the hidden +SourceArgumentWitness.temporary_origin in all four ownership-bearing expression +variants. Each predecessor must be the exact registered earlier declaration in +the same function/body and checked session; legitimate repeated references are +deduplicated. Self, forward, absent, foreign and conflicting declaration rows +refuse. Initial retained payloads cannot import sealed/materialized callable or +Graph epochs. This establishes a finite earlier-proof dependency graph. It adds +no Source or Generated call, runtime effect, Resource custody or entry authority. + +Preparation writes nothing. The actual actor finalizer derives an opaque complete +checkpoint from its retained actor stage and the real initial sum producer. That +checkpoint verifies the full current table, not only temporary rows. Exact +source/checked/arena, dependency, OPEN-state and affected attachment preflight +checks all pass before the first cell write. Commit consumes preparation and +fills each OnceLock once with the retained BEFORE value. Only afterward does +the existing actor epoch capture its complete pre-attachment after table and +attach the edge. Only an opaque completion from this producer can skip the +original late setter; already-filled cells alone are never an admission. + +The raw source Arc may temporarily retain open cells inside this unpublished +construction. Final P payloads cannot point back to it: they come from the old +input, equal the actual token output and own only a finite earlier-P graph, with +no sealed current epoch. The final actor edge owns its raw predecessor and a +complete table that does not contain the edge. Current/historical full actor +comparison gates are unchanged. No combined Arc cycle has been reproduced; +actual Weak expiry controls verify this new bounded graph rather than treating +process exit as proof. + +Seven new controls cover real BEFORE/OPEN observation and final filled states, +idempotent completion, foreign context/arena/inventory/table refusal before +writes, exact source Arc and complete sibling/function drift, hidden earlier +proofs with repeated argument uses and invalid edges, rejection of real sealed +output payloads as BEFORE replacements, actual archive/Program clone lifetime, +and a separate actor/interface-coerced temporary producer. The unchanged combined +control also requires full HIR/backend/archive/epoch validation and real E/S Weak +expiry. It changes its expected result only after real lowering succeeds; no +Source or selected-value oracle is rewritten. Test-only observers borrow actual +producer receipts and checkpoints; they mint no positive proof or capability. + +## Verification boundary + +The implementation is applied and formatted. All 35 focused HIR controls pass, +including seven new initial-finalization controls and the existing actor, +required-bake, sealer and temporary-view regressions. The broad local checkpoint +passes **2,553 distinct tests**: 1,900 compiler-library tests (Codegen 135, +Comptime 691, HIR 311, MIR 384 and Typecheck 379), 603 frontend/reference fixtures +and 50 CLI tests. The 35 focused controls are included in HIR 311, not added to +that total. Exact broad logs under target/native/ are: + +| Log | SHA-256 | +| --- | --- | +| actor-temporary-owner-core-regression-v1.log | 0745179c292cc76b55510000b8530bd17073909042a09f57d51d8af913f5c11b | +| actor-temporary-owner-fixture-regression-v1.log | 66e11f2cff69c9394ca1aa9e36167af3662d654a2148267ba6038ddf9f4d4e49 | +| actor-temporary-owner-cli-regression-v1.log | a077a1eb56e2638311665196eb3e980f9ce548fe61a6927fa2ec5924dd030b14 | + +The fresh compile-only gate is accepted with zero Source execution credit. +The separate native selector passes one Rust test and accepts **one unchanged +Source, two objects and two native processes**. The 970-byte Packet witness pairs +Source/Runtime Debug and Release. Both actual outputs are exactly observed:5 +plus LF, with empty stderr and zero exit. These measurements grant the bounded +output/cleanup acceptance below, without adding historical executions or +inferring physical lifetime. + +Prior actor checkpoint 8827c57321e6e0d74cee6fd30dd21a7f11940d8c retains its +18 focused/2,546 broad tests and two-Source/four-object/four-process native scope. +The Resource observation results committed at +e9162a5d5aabc6cc5c1386d7c3366d4abc80baf5 also retain their original counts and +receipts. Neither historical checkpoint is promoted to current actor or Source17 +regression acceptance by the tests above. Native physical actor/callback/queue +ownership, same-context reentry and full native acceptance remain unmeasured. + +## Fresh compiler gate + +The accepted gate binds 1,093 compiler/Source inputs. Its image is 40,657,920 bytes +with SHA-256 c308ff075c6dc904684867a3854e6f9d82961c1e417a01a05dd899dbe138237d. +The inner and outer terminals complete successfully: exit zero, reaped quiet +owned processes, disposed root handle, released leases and no errors. Receipt +paths below are relative to target/native/: + +| Receipt | SHA-256 | +| --- | --- | +| private-observation-v2-gate-compile-v4/gate-acceptance.json | 290a8b403a76aec6b10d7e1e0b5e27cce268a78cc6600a717761c82914498160 | +| private-observation-v2-gate-compile-v4/terminal.json | 4612c72c6116b744f7836726098dbe22c45cc176078665716b6d92668f88af97 | +| private-observation-v2-compile-supervisor-v5/terminal.json | 32b46311bdfd5959ef93f7f05c20677f91030fc6c0172ca6ad562663c7220508 | + +This is actual compilation, with zero Source or host ABI execution credit. Its +separate actual native execution receipt is recorded below. + +## Paired ordinary native acceptance + +The current selector passes one Rust test in 199.77 seconds, separately from the +2,553 regression tests. Its one unchanged Source emits two objects and executes +two processes, one matched Debug pair and one matched Release pair. These are +not two Sources or a full Source/Runtime profile cross-product. + +Both rows require exact stdout bytes observed:5 followed by LF, empty program +stderr and exit zero. The written Source is removed and the host object dropped +before launch; the isolated directory contains only the executable. Native +children are reaped, executables and temporary directories removed. The outer +process is quiet/reaped, its root handle closed and its lease released. The +authoritative acceptance leaf is complete with zero failures. + +Receipt paths below are relative to +target/native/native-bounded-actor-temporary-owner-execution-v1/: + +| Receipt | SHA-256 | +| --- | --- | +| execution-acceptance.json | 076ac84a3717a56fafa9a5450edc96003a2ec3035e50c4fd10ef3eada9fe78b4 | +| test-process.json | 9782953688918c25d1d42adc161145ba03d9a7fd21934e0b79227d2f44446fb6 | +| native-evidence/actor-temporary-owner.json | 2b19eea41a33e6a94d89050443cad25062e1802e3a76159b7243c982525bbe34 | +| native-evidence-catalogue.json | 56dee1eb862cf6c4d541bb5b7d86eace86623271af16a0581200dfc7473c4e07 | + +The schema-v1 observation file is 5,863 bytes and retains two actual rows. The +prerelease execution-terminal.json remains intentionally complete=false and +lease_released=false; its digest is +c5ef3ad06831febba147b872821bd77586151189d04171c973d86868b2fa435e. +The later acceptance leaf proves successful release and is authoritative. +Every observation retains physical_lifetime_measured=false, +reentry_measured=false and full_native_acceptance=false. Process/file cleanup +and compiler Weak expiry do not establish native actor/callback/queue lifetime. + +## Remaining obligations + +A producer whose operands gain callable tokens needs a separate immutable current- +canonical proof derived from actual receipt slots, including None-to-Some. That +would require a validated HIR consumer API used by MIR, whose current temporary +path directly checks matches_owner_initializer. Do not refill old cells, replace +P identities, strip markers, recapture current tables, or broaden historical +ancestor/Graph/Resource admission. Such compositions remain part of the 100% +goal, alongside later actor epochs and unrepresented proof transport. No language +design change is made, so docs/design.md remains unchanged. diff --git a/docs/completed/native_codegen/native_bitfield_projected_local_views.md b/docs/completed/native_codegen/native_bitfield_projected_local_views.md new file mode 100644 index 00000000..2001dddd --- /dev/null +++ b/docs/completed/native_codegen/native_bitfield_projected_local_views.md @@ -0,0 +1,92 @@ +# Native bitfield projected local views + +## Selected contract + +Rule Set 24 admits readonly views of immutable local data. Existing bitfield +field access exposes its declared semantic type, including the final +`list[uint8]` payload on a byte boundary. The bounded native repair extends only +the typed initializer proof to a declaration-backed Bitfield Field step. + +Resolve the exact bitfield and canonical zero-based FieldId, then compare the +borrowed `BitfieldFieldDef.ty` exactly. Preserve the checked base/owner type, +intermediate and endpoint types, terminating LocalId/type and immutable immediate +backing chain. Existing nominal/secrecy qualification, transparent conversions, +cycle checks and successful-initializer ledger remain required, including for +unused/orphan metadata before optimization. No parallel field table is allocated +or copied. Ordinary structs, bitfields and exact-state machine steps can compose +through the existing finite Field tree. + +MIR retains its persistent whole-owner loans, liveness and non-owning slots. +Existing native emission uses zero-based bitfield indexes and checks pending +receivers at each Field access. Borrowed linear reads retain their handle; +numeric fields retain implicit copying, ordinary owned field reads retain copies +and explicit clones acquire independent ownership. The existing payload schema, +runtime carrier and ABI do not change. No implicit clone/join, hidden owner, +mutable/temporary view, alias escape, projected write, owner-change permission or +loan-expiry policy is selected. This note does not claim a later owner domain or +MIR repair. + +## Actual pre-change characterization + +The first two sources in `target/native-next-after-inferred-json/results.json` +pass two format checks, four frontend profile checks and two ordinary default- +profile reference runs with empty debug observations. Exact application outputs +are `2:3:2:4007ff` and `3:2:4007ff`, with no LF. Both borrowed-alias AOT profiles +refuse the typed HIR projection before archive lookup; both owning-copy controls +reach unavailable-archive lookup. All four existing output sentinels survive. + +Three sources in `target/native-bitfield-projected-view-extra-witness/results.json` +pass three format checks, six frontend profile checks and three default-profile +reference/agent runs with exact two-line output and empty stderr/debug captures. +They cover a view parameter, mixed Envelope/Packet paths and an empty payload, +with forwarding, independently grown copies and original owner rereads. Their +six prior AOT attempts refuse the borrowed proof and preserve six sentinels. + +The two sources in `target/native-bitfield-projected-view-pending-witness/results.json` +pass two format checks and four frontend profile checks. One default-profile +reference/agent run fails at whole pending Packet access after `before:whole\n`, +before its after-binding marker. The other succeeds with +`before:payload\nitems:3:2\n` and five exact ordered Trace events: two cloned +payloads go from depth two to one while the original stays at depth two. The +four prior AOT attempts refuse the borrowed proof and preserve four sentinels. +These prior refusals and reference runs are distinct from new native execution. + +## Focused acceptance and remaining gates + +Both new HIR tests pass (134 filtered, 0.00 seconds) in +`target/native-bitfield-local-view-hir.log`. They pin ordinary-struct/bitfield +composition, view parameters, forwarding and owned controls, plus unused forged +owner/index/endpoint/root and orphan metadata with otherwise valid structure. + +All seven linked cases pass together (425 filtered, 3.89 seconds) in +`target/native-bitfield-local-view-linked.log`. Both matching-profile builds +complete before source deletion. Executions require exact application stdout, +status, stderr, typed runtime events and empty frontend/artifact observations. +The whole pending owner reports status 71 and its exact field-access error; +status 72 cannot pass as cleanup success. Payload-copy success preserves five +ordered debug Trace records and empty release stderr, with original depth-two +payload retained. Silent successes require status zero and empty observations. +The crossed library gate passes 951 tests (41 codegen, 416 comptime, 136 HIR, +86 MIR, 48 resolve and 224 typecheck) in +`target/native-bitfield-local-view-phases.log`. All 83 driver library tests pass +(86.96 seconds) in `target/native-bitfield-local-view-driver.log`; all 598 +frontend fixtures pass (21.88 seconds) in +`target/native-bitfield-local-view-frontend.log`. The rebuilt CLI passes all seven +fixture-format checks in `target/native-bitfield-local-view-format.json`, and +Rust formatting and diff checks pass. + +The bitfield checkpoint registers 432 supplemental tests. The later exact +clean `cb617ecf` workspace includes this repair and passes all 439 native tests +(905.04 seconds), all 182 object obligations (516.91 seconds), remaining +workspace targets and doc-tests. Its unchanged-head, clean-start/end result +finishes at `2026-10-02T20:55:57.0941072Z` with exit zero; logs and metadata are +`target/native-workspace-cb617ecf.log` and its JSON summary. Supported-host +acceptance also passes: all four jobs at `5d919052` in +[workflow 37058389987](https://github.com/vycdev/jett/actions/runs/37058389987) +and at the later documentation-only `194ce065` revision in +[workflow 37064035604](https://github.com/vycdev/jett/actions/runs/37064035604). +The accepted clean 08f9f7e7 parser predecessor is recorded separately +in [the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). +It cannot substitute for the new source gates. Whole-language native parity +remains the 100% objective, tracked with the unchanged about-85% estimate and +fixed 207 inventory. diff --git a/docs/completed/native_codegen/native_call_ownership_facts.md b/docs/completed/native_codegen/native_call_ownership_facts.md new file mode 100644 index 00000000..681ea594 --- /dev/null +++ b/docs/completed/native_codegen/native_call_ownership_facts.md @@ -0,0 +1,423 @@ +# Typed caller ownership: selected implementation contract + +Status: caller repairs are committed at +`a63f891016fac7dc50fffdde33be8b0ce45c655f`. The complete Windows native suite +passes 480/480 with stable HEAD/tracked sources. Current full-workspace, +182-object inventory and supported-host acceptance remain separate obligations. +The integration began over `a9a6cd63db33fb29fb10f4b32607b3f82ab431bc`. An earlier formatted repair checkpoint +passed all 304 checker, 188 HIR, 181 MIR, 67 backend and 63 object-emission tests, +plus the MIR doctest. These counts describe that compiler-core gate. + +A stable Windows native snapshot completed 472 of 479 supplemental test groups +successfully, with seven failures. Five failures subsequently passed focused +native replays: the nested breakpoint condition, primitive and refined interface +identity, contextual secret constructors in both profiles, and the 64-fixture +scalar/owned-collection group. The collection fixture corrections add explicit +views to set/map observers and preserve their output oracles. All 64 sources also +pass source checking. Ten breakpoint-context and three intrinsic owning-staging +regression groups pass. These native replays preceded the formatting-only pass; +the compiler-core gate above follows it. The later complete 480-test receipt +below supersedes the historical 472/479 result. + +The bounded builder-generation and handled-producer repairs are now implemented. +The actual focused gate passes all 66 MIR caller acquisition groups and all seven +generation groups; three native emission groups and the original builder native +regression also pass. A new converted handled-view linked fixture passes in both +profiles after source deletion, observing exact payload lengths one and two. +All seven failures from the historical 479-group run now pass individual replays. +The fresh complete Windows native suite now passes all 480 tests at exact a63, +with zero failures and stable HEAD/tracked sources, in +`target/native480-after-owner-generation/receipt.json` (root terminal `06b7f8`), +ending `2026-10-04T14:20:12.987780+00:00`. Full workspace, current fixed-inventory +and supported-host acceptance remain pending. The historical focused results +remain distinct from that new complete-suite count. + +Earlier integration receipts passed 598 frontend fixtures, 15 native caller groups +with source deletion and exact debug/release streams, and both property replays +that failed the earlier driver run. Their scope remains historical. The unchanged +exact-a9 baseline passed 464 native cases and all 182 object obligations; workflow +37130142896 passed all four Windows/Ubuntu build and installed jobs. Those baseline +receipts do not validate this candidate. + +This implements the existing Rule24 contract rather than a new source ownership +rule. No Resource carrier, provider, ticket runtime or finalizer acceptance follows +from the caller/native receipt. A separate private reference Resource slice now +passes 36 focused tests, recorded in +[the resource-hook note](native_resource_hook_identity.md#reference-lifecycle-execution); +production providers and native Resource support remain disabled. +The full 100% native-codegen objective and rough 85% feature estimate remain open. + +## Existing authority and actual proof + +Rule10 (`docs/design.md:2091-2095`) and Rule24 (`:5794-5848`, especially the +four-row table at5821) determine caller ownership independently of callee access. +Written `view` keeps a caller owner; bare owned data is relinquished even when +the formal is View. Explicit view to Owned is E0375. A known move-only borrowed +binding cannot supply ownership; retain conservative E0401 for its bare input, +including a formal View destination. Exact implicitly copyable values copy. +Field reads borrow the parent and acquire the endpoint by the already selected +copy operation; Resource-bearing field copies remain prohibited. A view of a +producer needs a temporary backing owner, rather than an existing binding to +retain. Root flattening, general owner-changing overlap, alias expiry and projected +write policies remain unselected. The bounded builder-generation follow-up below +implements existing admitted source behavior. + +I independently read the immutable root-executed exact94 report with SHA +`a9dbc168c298b84d75a88c15442849ede9cac182bc572af0deff713651041e04`. +All10 ordinary sources have matching debug/release check outcomes (20checks), +no timeout/schema failure, and the five admitted default-profile reference runs +have exit0, empty stderr and debug0. Cases02/03/07/08 are admitted retention +holes; cases04/05/06/09/10 already give sole E0400. Case01's explicit views are +admitted. Case07 proves a source-first Owned destination is retained because +legacy analysis ignores the actual formal-to-source permutation. These are +historical exact94 observations, not current-a9 ordinary CLI re-execution. + +Root also actually built and executed the fixed checking-only a9v2 probe; +root-execution-proof-attempt2.json reports build0/execution0 and stable guards. +The separate actual results SHA is +`a18000c88557644514b6b64b30c495d4796de26192a18a50c9198ed34d966e89`. +All12 synthetic checks (six sources x debug/release) agree between profiles: +Resource01/03/04/06 are accepted;02/05 are rejected by the Check phase with +sole E0400 `` `owner` was consumed and cannot be used again ``. Accepted03's +actual formal-to-source permutation is [1,0]. Accepted snapshots retain exact +private Construct [View Network,Owned int64], BorrowOperation [View Network, +View Resource], and Close (the Finalize recipe) [Owned Resource] shapes. Formal modes and +permutations are present, but a caller retention handoff table is explicitly +unavailable. Thus the qualified/mixed/pipeline holes affect actual checked +Resource signatures, while the indirect/private-bare controls already reject. +Resource06 preserves explicit borrows then an owned Close boundary. + +The probe has no provider installation, interpreter/native invocation or +application stdout. Its historical hard-coded compiler_provenance string says +build proof pending; the separate root execution proof supplies actual build +and linkage provenance, without modifying that saved result. No runtime/native +cleanup result is inferred from reference value copies or synthetic preparation. + +## Schema + +Put the closed checker types in a new `jett_typecheck/src/caller_ownership.rs`. +The following describes concrete fields/variants, not applied Rust: + +- `CheckedCallerSyntax::{Bare, WrittenView}` records original spelling. Written + view is recovered only through Paren and already checked carrier-preserving + Coarsen/Declassify wrappers; it is never inferred from the formal or inserted + HIR/MIR View. The existing E0375 source-boundary check remains authoritative. +- `CheckedCalleeAccess::{Owned, View}` records the exact destination access. +- `CheckedCallerEffect::{Copy, RetainBorrow, TransferOwned, RelinquishOwned, + ObserveData}` records the separately validated caller disposition. ObserveData + is available only in the selected lexical test/debug context below. +- `CheckedBindingFact { definition: DefId, declaration_span: Span, ty: TypeId, + mode: CheckedBindingMode, mutable: bool }` anchors a binding to the same + resolution session and concrete body. Parameter facts include their actual + concrete types and View modes. Forwarded alias modes retain the existing + immediate `CheckedViewSource`; they are not replaced by the final owner. +- `CheckedCallerOrigin` is `Binding(CheckedBindingFact)`, + `BorrowedProjection { source: CheckedViewSource }`, + `OwnedFieldCopy { parent: CheckedViewSource }`, or `OwnedExpression`. + BorrowedProjection applies only to a checked explicit view of a field path. + A plain field is OwnedFieldCopy. Clone and owned calls/constructors are + OwnedExpression, including explicit views of their produced result. The + original typed field expression proves every owner/index/endpoint; no second + vector of mutable field indices is added. Source Other means no stable + original binding and cannot be upgraded to a retained caller owner. +- `CheckedArgumentOwnership { source_span: Span, source_index: usize, + parameter_index: usize, actual_type: TypeId, parameter_type: TypeId, + syntax: CheckedCallerSyntax, origin: CheckedCallerOrigin, + callee_access: CheckedCalleeAccess, effect: CheckedCallerEffect }`. +- `CheckedInvocationTarget` is an exact resolved source DefId, the existing + `CheckedGenericCall`, existing `CheckedMethodCall` or `CheckedInterfaceCall`, + an indirect Function TypeId, or closed IntrinsicId. No name/namespace/raw + spelling is an authority. +- `CheckedInvocationShape` is `Function { signature_type: TypeId }` for the + exact substituted source/indirect Function type, or `Intrinsic { intrinsic, + result_type: TypeId, operands: Vec }`. The closed + operand roles validate source arity/variadic shape and allowed source access, + rather than fabricating a source Function DefId or function arity. + Print/Println have one checked PrintArgument role per variadic source actual + and identity source order; the current checker has no ordinary signature or + call-order fact for them. Their permitted written views remain physically + borrowed and bare linear arguments remain consuming. Metadata appended by + HIR has a distinct compiler-added role, never a missing source actual. +- `CheckedCallOwnership { target, shape: CheckedInvocationShape, + context: CheckedOwnershipContext, arguments: Vec }` + stores arguments in lexical SOURCE order. `CheckedOwnershipContext` is + Ordinary, Verify, Property, or BreakpointExpression. Invocation syntax and + observing context are distinct facts; Trace is its existing statement. + +The effect is checked from the complete tuple rather than accepted as a free +Retain flag. Ordinary noncopyable Binding/Owned + Bare yields TransferOwned to +Owned, RelinquishOwned to View. WrittenView + View yields RetainBorrow, whether +its backing is an owned binding, known borrow, projected borrow or an owned +producer temporary. Known borrowed Binding + Bare is E0401 before an effect is +accepted. A new owned endpoint/producer follows the same callee access but has +no original binding to invalidate. Copyable type proof yields Copy and does not +invent authority for a Resource/capability. WrittenView + Owned remains E0375. +A temporary's RetainBorrow means protect its one acquired backing through this +operation, not retain a non-existent caller binding after it. + +The schema contains no public ticket ID, registry key, Resource runtime value, +Finalize permission, raw DefId foreign-session token or synthetic carrier. + +## Checker ownership contexts and integration + +Add `call_ownership: HashMap` and +`binding_facts: HashMap` to CheckResult, +CheckedGenericFunctionInstantiation, CheckedBodyFacts, TypeChecker and +ActiveGenericInstantiation. Binding maps use declaration-name Span to fit +existing capture/clear operations, while each value carries the exact DefId. +A scoped body's ownership walker inherits the exact incoming lexical binding +state/context; local facts are extracted by declaration span. Captured incoming +bindings use their parent concrete context, not a top-level definition_types +entry overwritten/restored by a different generic instantiation. + +Ownership context is lexical, not inherited from whoever triggered checking. +Use a private current-context stack/save-restore field for recording: entering +an ordinary named/generic/inline function body selects Ordinary, even when its +instantiation was requested from a verify/property/breakpoint expression. +Entering the actual verify/property body or breakpoint expression selects its +observation context; nested reflected scopes inherit that exact lexical owner. +Do not derive new caller effects solely from ambient in_verify_block/ +in_property_block flags. Existing generic checking already resets those flags +(checker.rs:7227-7228); follow that boundary and restore it for the new ownership +context too. check_function_impl itself has no equivalent reset. The caller site may +ObserveData while the checked callee body still consumes its Owned formals. +Add a source test instantiating the same generic body from a test block and an +ordinary function, proving that only the lexical test call has ObserveData. + +Recording routes to the last active generic frame or root, matching +record_call_argument_order/record_binding_mode. Wire initialization, completion, +clone/swap, checked_body_facts and clear_checked_body_facts. Nested +CheckedComptimeTypeBinding owns its own complete facts. Inline parameters, +locals, handler/match/loop bindings and incoming captures need exact facts +where they enter the existing checked environment. CheckResult.definition_types +is useful for ordinary declarations, but cannot substitute for per-instance +local facts after generic type_env restoration. + +After argument binding/type checking, record each accepted source invocation +once, using the same target/signature and formal-to-source permutation. This +covers ordinary direct, explicit/inferred generic, source method/interface, +indirect and pipeline call routes. Error recovery still checks malformed/excess +arguments normally; it must not manufacture a successful ownership fact. +Pipeline source0 is an explicit virtual source occurrence: first step uses the +original input origin; later steps use the previous evaluated producer result. +`into view` supplies WrittenView for source0. Extras retain their own syntax. + +Replace source-name first-View retention in ownership.rs with a borrowed +`OwnershipFacts` context over the same Module/ResolveResult/checker maps. +Use DefId-keyed state and exact declaration facts for typed invocation effects; +names are diagnostic labels only. Walk actual arguments in lexical source +order, looking up destination mode by each checked parameter_index. This +consumes a bare owner before any later actual can reread it, even when named +arguments were checked in formal order. Preserve the existing args-before- +indirect-callee evaluation order. Transparent wrappers recurse to the proven +binding; a field copy observes the parent without moving the whole parent. + +Ordinary bodies use root facts. Generic templates are checked at each concrete +instantiation (checker.rs:5748-5793,5843-5848); analyze each selected concrete +body with its own parameter types/facts, not the legacy AST `bytes` stand-in. +Nested scoped/reflected bodies push their own frame and consume the existing +CheckedStaticSelection/CheckedComptimeTypeSelection. Do not re-evaluate type +reflection or visit deliberately unchecked branches. Existing flow join/loop +conservatism is retained; this work grants no new borrow lifetime. Any legacy +untyped helper retained for AST unit tests must be separate from production +checked analysis and must not authorize source-name retention. + +Do not flatten generic maps by Span. finish_check's current flattened +with_debug_types copyability conjunction is observation compatibility only; it +is not caller authority. Concrete breakpoint/consumption observations should +stay with their owning frame, with any compatibility summary conservative and +never used to decide caller ownership. Keep existing exact span/code/message +suppression when a typed E0401 replaces its legacy counterpart, preserving +unrelated ownership diagnostics. + +## Math, JSON and debug are separate audits + +1. `stdlib/math.jett:28-31` declares Owned list parameters for average/median. + Rule24 therefore consumes a bare owning list. Their private compiler + intrinsic may physically read the operand; that does not retain the caller. + ownership.rs:1030-1041's literal math names lack independent selected + retention authority. The design's1648-1649 example reuses scores without + written view/clone although both formals are Owned; record/repair that stale + example during aligned documentation rather than adding an exception. + Likewise design5770/5807 forwards a borrowed data binding bare into the + SOURCE list.length wrapper. IntrinsicId::ListLength is list.__length; + from_callable_name does not classify list.length as that intrinsic, and the + real wrapper itself forwards with explicit view (stdlib/list.jett:11-12). + Correct those stale examples to written view. They are not independent + authority for implicitly forwarding into ordinary source wrappers. +2. `stdlib/json/90_public_api.jett:16-64` declares View for raw queries and + serializers, Owned copyable string for parsers. design3990 and6033/7057 + require explicit caller view for retained data and pipelines. Remove + is_json_implicit_view_facade as ownership authority for source calls. Keep + the public JSON policy/type/source handoff identities intact. When HIR + substitutes a trusted source facade/helper, preserve the ORIGINAL caller + disposition; the new implementation callee cannot retroactively retain it. +3. design6455-6475 explicitly allows ordinary-data observation in verify, + property and breakpoint expressions; represent that lexical context. + Authority-bearing capability/Resource input cannot gain an owner or clone + from it. Keep existing Resource observation/printing guards. Trace retains + its selected non-destructive statement semantics. Print/Println remain + closed variadic compiler operations: bare linear actuals do not gain a new + retention exception; explicit view physically borrows. Release checking, + debug event ordering and unresolved hidden-Secret rendering stay unchanged. + ObserveData preserves source availability only with an independently + validated ordinary-data acquisition: copyable inputs copy; a supported + cloneable data input to Owned gets one checked snapshot owner, and to View + is borrowed. It never changes an Owned formal into a borrowed physical + parameter. A function descriptor also needs its existing capture-ownership + validation; its Function signature alone does not prove authority-free + captures. Where supported snapshot acquisition is not proved, retain the + current lowering refusal rather than expanding cloning authority. This + slice changes neither absent/type-only controls nor the unsettled nested + hidden observation domain. Existing can_snapshot_view is a physical + supported-data predicate, not standalone source permission. + +For compiler-owned operations, enumerate callee access by closed IntrinsicId +and exact source-operand role, with evidence from the existing selected syntax +and checker. MIR intrinsic_borrows is a PHYSICAL access map, not sufficient +proof of caller retention. No blanket IntrinsicId exemption is allowed. Pure +scalar/metadata operands remain copyable; constructor payload transfer and +already selected explicit borrowed operands keep their operation-specific +rules. A source Resource hook uses its exact checked declaration/signature and +ordinary Rule24, not this intrinsic route. Unproven operation-specific source +syntax is a separate prerequisite to implementing that entry, not a fallback +Retain fact. + +## HIR, MIR and supported ordinary native lowering + +Attach a required typed call-ownership packet to HIR Call/IndirectCall/Intrinsic +(or one shared invocation operand structure used by those variants). Lower +source facts into formal argument order using the existing checked +source_indices and retain evaluation_order as the inverse lexical permutation. +Convert origin DefId to current-function LocalId through the existing local +join. Keep signature/target and original occurrence separate from the physical +operand. The packet retains actual source input type and expected parameter +type separately: an already checked interface/coarsen/secret conversion may +change the lowered operand type, so validate the existing typed conversion tree +instead of demanding an incorrect raw actual-equals-parameter equality. +Validate count, unique complete source/formal permutation, TypeIds, exact +mode/signature, valid immediate local source and typed field path before +any pruning, including dead/unused source calls. + +Compiler-generated calls use explicit generated-operation metadata with fully +validated argument acquisition/access; they do not use missing/None to retain +arbitrary source inputs. Helper dispatch, equality/refinement adapters and JSON +handoffs keep the original source packet when representing that invocation. +Extra reflected metadata operands are identified as generated copyable metadata +in a derived LOWERED operand shape, without changing the original source +argument count/permutation. Any generated linear operand still requires its +own checked acquisition. This +is not a caller-name or all-generated-call escape hatch. + +MIR preserves the packet through handlers, inline extraction, scoped views, +compaction and every LocalId remap. An inserted View for formal callee access +never changes WrittenView or RelinquishOwned into RetainBorrow. Existing +explicit source aliases remain persistent; compiler call-view scopes retain +their existing bounded lifetime. Original source roots are provenance, not +implicit liveness loans created from bare inputs. + +For supported ordinary linear data, lower RelinquishOwned-to-View by moving +the evaluated owned input once into an internal owning temporary at that +source-order position, then lending that temporary to the call. The original +binding is unavailable to later actuals/continuations. The temporary remains +owned across later-argument failure/early return and the complete call, then +normal owned cleanup releases it. No clone acquires authority. Physical View +access stays separate from ownership acquisition. Existing explicit +view-of-producer handling uses its one backing acquisition and no original +caller retention. At the initial HIR join, validate WrittenView against the original AST actual +and keep that source witness distinct from subsequently inserted physical +View. Later MIR validation checks the preserved packet/staging relation rather +than rediscovering caller syntax from the current operand wrapper. Validate +forged caller packets before MoveValuePlan and native eligibility, not after +successful optimizer pruning. + +This supported-data path is a caller ownership/ordinary cleanup proof only. +Type::Resource still has no native scalar/layout/runtime carrier or drop ledger; +this slice must not enable it in is_linear/scalar_kind/emission. Future live +Resource requires typed hook dispatch/effects, sealed dynamic identity, exact +owning tickets, partial-argument reverse cleanup and Resource finalization on +every exit. The new packet will be a prerequisite, not that implementation. + +## Bounded implementation files and proof + +Likely files: new typecheck/caller_ownership.rs; checker.rs exports/context wiring; +ownership.rs typed flow; HIR lib.rs plus call validation/helper consumers; +MIR lib.rs, handlers.rs, move_values.rs, sequences/prune.rs and generated/inline +remap consumers; ordinary native call emit/verification where acquisition must +survive physical borrowing. Resource runtime/providers are excluded. Update +aligned design/architecture and active Resource notes after root selects scope. + +Required tests are source-driven and run both frontend profiles: all20 old +characterizations are motivations, not expected legacy compatibility. Only01 +keeps caller owners;02/03/07/08 now reject the invalid reread, and04/05/06/09/10 +retain E0400. Add valid last-use versions, transparent wrappers, named mixed +permutation, direct/alias/indirect/pipeline forms, first versus later pipeline +producer, known-view aliases/parameters (exact E0401), copyable scalars, field +copy versus projected view, owned clone/result and explicit producer temporary. + +Pin exact caller packets for generic copyable versus linear instantiations and +nested reflected/scoped bodies sharing Span. Source order must catch duplicate +owner use across permuted actuals. Independently pin lexical observation context +and math/JSON source signatures without bare-retention whitelist. Use the six +exact synthetic hook sources with the checking-only catalog harness: all12 +pre-change outcomes are now actual root evidence. After repair, Resource01/03/04 +must reject their invalid rereads,02/05 remain E0400, and06 remains admitted. +Add valid last-use counterparts proving RelinquishOwned without requiring +Finalize execution. No provider executes. + +Original HIR/MIR malformed metadata tests must reject missing/count/permutation, +wrong signature/mode/type, forged source origin, alias claimed Owned and inserted +physical View claimed WrittenView, including unused calls before pruning. Native +supported-data controls cover both profiles, source deletion, exact stdout/debug +streams, partial-argument failure and balanced cleanup. Registration or a passing +metadata test is not live Resource or whole-language coverage acceptance. + +At selection, no implementation or tests have run for this change. The full100% objective, +about85% broad estimate, fixed207 inventory/182 object obligations and464 +supplemental native cases remain unchanged. + +## Root selection receipt + +Selected source contract SHA256: 963a045fbe05e40aa59c1ba6a6729a92fa6ccf83b9883b9dd4fcb8f09622ba17. +Manifest SHA256: 2b7bc6319cb79e0bb6d97d904bebf0820bb764d71b57be59b79234cd094b5e2a. +All 31 input guards and four artifact guards were checked before this write. +The selected behavior follows existing Rules 10 and 24. This note precedes +tracked Rust changes; the complete checker/HIR/MIR/ordinary cleanup scope above +remains required. The overall objective remains whole-language native parity. + +## Exact producer and capture handoff + +Root selected the additive caller-origin handoff before its Rust implementation at 2026-10-03T17:28:13.4850053Z (contract SHA256 dda43905e81f5d74ce939a9e1c3ad26098e1f06c42079a84b4f9f2fd21035665). Capture facts retain exact declaration DefIds in their concrete frame and are included by in-body source call occurrences even when their declarations are outside the body span. Resolved named functions, compiler-baked namespace constants and static namespace/type members are produced expressions; they cannot supply nonexistent caller-local roots. Local function-valued variables retain Binding provenance. No signatures alone prove closure capture cloning, no root/generic maps are flattened and no Resource authority is added. The selected handoff is implemented in this candidate; the header records current verification and remaining acceptance gates. + + +## Captured builder generations and handled conversion inputs + +The generation path is restricted to an original checked written view of an +owned mutable plain `TypeConstruction` binding. Its complete original call, +Source identifier fact, full declaration header and assignments remain sealed; +only current loan/root/RHS/site identities undergo verified canonical remaps. +The replacement RHS is evaluated before the captured old generation enters +private escrow. The current replacement and captured backing have separate, +once-only cleanup on completion and abandoned call paths. No builder clone, +public carrier, source ownership rule or ABI permission is introduced. Broader +simultaneous cohorts and other owner kinds remain unaccepted breadth. + +The handled-conversion path preserves original OwnedExpression/WrittenView/ +RetainBorrow authority, its private Handle witness, exact owning storage, +Begin/End lifetime and produced-result CFG. Fresh caller acquisitions associate +only a fully validated interface boxing occurrence with its actual concrete View +input. Move analysis borrows that input; the boxing result is independently owned +and the runtime may copy its payload. Other same-type/span occurrences grant no +permission, and this does not authorize a general View-to-owner escape or a +zero-runtime-copy claim. + +Root's actual focused receipts pass 66 MIR acquisition groups (2722a1), seven +generation groups (5f71c0), three native emission groups (76179f), the original +builder native regression (f9b4a4), and the new converted linked fixture (161f8b). +The last fixture observes payload lengths one and two, matches both profiles and +runs after source deletion. +The generation gate's earlier six-of-seven result was an invalid total-local-count +test assertion; its separate amendment checks the exact unused seed removal, +dense current IDs and immutable archives. Full acceptance remains pending. +Resource lifecycle/providers/native cleanup, concurrency breadth and the full +100% objective remain whole-goal work, with the broad estimate still about 85%. diff --git a/docs/completed/native_codegen/native_canonical_value_conversions.md b/docs/completed/native_codegen/native_canonical_value_conversions.md new file mode 100644 index 00000000..422289fa --- /dev/null +++ b/docs/completed/native_codegen/native_canonical_value_conversions.md @@ -0,0 +1,125 @@ +# Canonical value conversion proof identity + +Status: implemented with local compiler and native view-regression acceptance +on 2026-10-16. Nine focused controls, 1,856 core-library tests, 603 fixtures and +nine CLI capture tests pass. The current-image native regression passes 24 tests +and 48 paired Source-deleted executions. Native execution of the new compound +required-callable and suite witnesses remains unproven. GitHub CI stays disabled. + +## Construction and replay + +Canonical value coercion can resolve and bind freshly materialized reflected +callable plans. Independent calls to the coercer create distinct private Arc +identities even when every Source expression and checked type is the same. +Therefore an independently recomputed table cannot authenticate the one actual +canonical construction used by a temporary, actor or mutable-sum proof. + +Within value-conversion completion, the private receipt surrounds its single +actual coercer execution. +It retains boxed complete before/after Programs and borrows the live TypeInterner. +Replay requires the same arena address, complete ordered function table with +float-bit equality, Resource manifest, equality methods and complete private +archive identity. It returns the recorded canonical output, preserving all new +occurrence tokens and adapter bodies. Callers supply no output table or callback. + +Each peer still validates its original proof and compares every actual operation, +header and callable identity. Existing independent actor and mutable-sum metadata +checks remain in force. Graph, baked reflected values and temporary views are +joined only after seeing the same unchanged-before archive. Final callable +sealing retains its separate exact old/new correspondence. + +Completion mutates a local Program candidate and publishes it only after all +peer joins, HIR validation and backend-type validation succeed. The ephemeral +receipt is never stored in HIR or an archive, so its snapshots cannot refer back +to it. No runtime layout, ABI, provider, custody or Source semantics change. + +Archive equality compares all private optional Arc identities even if the +original Source data is absent. The current base has data, execution, reflected +and Graph identities. The separate native-suite packet must add its native_suites +and required_cache fields to this same complete comparison when merged; neither +identity may disappear behind an empty-original fast path. + +## Local compiler verification and remaining gates + +All nine private controls pass. They cover a genuine closed +required reflected callable twice beside an ordinary temporary owner, repeated +completion, exact recorded bound tokens, independent coercer output refusal, +foreign arenas/plans/archives, edited headers and bodies, dropped/duplicated/ +reordered input and output occurrences/functions, hidden Arc identities, signed +zero and NaN payload bits, failed publication and receipt lifetime without a cycle. +The synthetic malformed-archive and NaN controls test private comparisons only; +they grant no Source acceptance or native execution credit. + +The genuine compound positive passes in Debug and Release checking modes. Its +first authored Source used `assert` in main, which reports E0340 because asserts +are confined to Verify/Property blocks. The fixture repair uses typed call and +`coarsen` locals for both required callable occurrences. Every Rust proof +assertion remains intact; no producer, occurrence, archive, repeated completion +or sealing check was removed. The failed first attempt has zero accepted credit. +The corrected positive is included in the complete nine-control gate. + +The existing genuine Graph/baked/temporary composition control also passes one +test in both checking modes. The full successor core gate passes **1,856 tests**: +codegen 135, Comptime 683, HIR 280, MIR 379 and typechecker 379. The focused +controls are included in HIR's total. The full frontend/reference fixture gate +passes **603 tests**, and the unchanged Windows CLI capture suite passes **nine +tests** under its existing stack limit. Logs under `target/native/`: + +- `canonical-conversion-compound-positive-v1.log` (failed), SHA-256 + `f4891373dff5eacdf2c9a1e175f8eb67f6eff0ca6f33324bcd812334142f1c99`. +- `canonical-conversion-compound-positive-v2.log`, SHA-256 + `fa986958329d859de65978f79ec9ea14a815a77c66fa0533f890b1023f01abca`. +- `canonical-conversion-focused-v1.log`, SHA-256 + `38c3a8d6d82b51e86113dfb633525d6242c1531d6200c67f31f71a04e5ddaf5c`. +- `canonical-conversion-graph-baked-temporary-v1.log`, SHA-256 + `fb2129d0a82a5e1c62b7dcd2c13859a9add435fad6e18e83bca3604d79d08f06`. +- `canonical-conversion-core-v1.log`, SHA-256 + `b712b29d7f69f505131282b590218c5242e3af5ede42494b01717b45b14691a0`. +- `canonical-conversion-fixtures-v1.log`, SHA-256 + `89dd139d83b8821d845158c2a74fd644270155f87cee02da2c54c1c182c5f94f`. +- `canonical-conversion-cli-v1.log`, SHA-256 + `45cafe79af2294857dbb443e83c7b4db81e728c3cfa0e7ae4ae821b8d26ef279`. + +## Current-image native view regressions + +The fresh MSVC gate passes compile-only acceptance, with zero Source execution +credit. Its subsequent existing-view regression selection passes **24 tests and +48 paired Source-deleted native executions**: 24 Source Debug/Runtime Debug +and 24 Source Release/Runtime Release. No publication refusals, legacy +Debug-only runs or full profile cross-product are counted. All 16 selection +logs have their pinned hashes and exact zero-failure test counts. The written +Source is removed before the binaries execute, with exact reference/native +stdout, stderr, status and debug-observation checks. All 1,060 compiler/Source, +68 reused Runtime, 65 fixture and 182 original run-pass input guards remain +unchanged. Receipts under `target/native/`: + +- `canonical-conversion-native-gate-v1/gate-acceptance.json`, SHA-256 + `41ca89ea180268d5df296355dc45fded45c98bcd0676cfa1d86fc896cd94f637`. +- `canonical-conversion-native-views-v1/execution-summary.json`, SHA-256 + `fda6e2118a9a00409a45ea53429f490b1010242523877d9feb305f70e2097394`. + +Both pin the compiled and executed gate image to SHA-256 +`b37a2366042a3e0326825156eb151e72c3026cc3d593516400b78f5cb1fc6695`. +The selected bytes/List temporary, stable, scoped-call, mutable-owner, reflected +and actor persistent-view controls provide current-compiler regression +acceptance. They do not execute the new compound required-callable or Source17 +Verify/Property suite witnesses. Those native obligations remain unproven. +Historical byte/List matrices are not added to this distinct 48-execution total. + +The separate native-suite transition and its Source17 compound control are still +unapplied. They require fresh required-callable composition and exact Verify/ +Property occurrence order after these packets merge. Linked Source-deleted +Debug/Release runs, provider/custody counters, cleanup and reentry remain actual +execution obligations. Equal sampled Property values must keep distinct ordinal +occurrences; Source17's configured trial count is not a universal suite count. +No provider/custody result may be inferred from its lack of Network parameters. + +A required callable in an actor handler, or a constructor carrying actor-state +views, still needs genuine checked coverage of the exact bound-to-sealed token +transition. The ordinary compound positive does not establish it. The historical +actor witness is unexecuted and must first use a valid Source observation in +place of its main-body assert; a predicted snapshot mismatch is not a measured +failure. Nested callback-return proof identities also retain their own coverage +obligation. Preserve complete snapshots instead of weakening them to admit a +current body. Full temporary lifetimes and the 100% native-codegen goal remain +incomplete. diff --git a/docs/completed/native_codegen/native_codegen_parity_plan.md b/docs/completed/native_codegen/native_codegen_parity_plan.md new file mode 100644 index 00000000..265b98bd --- /dev/null +++ b/docs/completed/native_codegen/native_codegen_parity_plan.md @@ -0,0 +1,2686 @@ +# Native Code Generation Parity Plan + +Status: consolidated Windows MSVC (`x86_64-pc-windows-msvc`) landing. Native +parity means the five fixture gates in `tests/native_parity.json`; the final +`codex/native-codegen` SHA and six-command result are recorded in its draft PR. +Whole-language parity and percentage tracking below are retired. Linux, +installed-package, relocation and source-deleted gates are outside this landing. + +This plan defines when Jett may claim native-code parity with the current +interpreter-backed language. It fixes the initial backend, the prerequisite IR +contracts, and measurable acceptance gates. Native execution must implement +accepted Jett semantics; it may not become a second source-language policy +layer or silently delegate runtime behavior to the interpreter. + +## Scope and Baseline + +The current fixture inventory establishes five separate denominators: + +| Obligation | Denominator | Acceptance condition | +| --- | ---: | --- | +| Native lowering | 182 | Every `tests/run_pass/*.jett` fixture reaches validated HIR and MIR and is accepted by native object generation. | +| `main` execution | 30 | Every run-pass fixture that declares `main` links and runs on the supported host with its interpreter-equivalent expected outcome and observable behavior. One scripted graphics fixture intentionally returns a runtime error. | +| `verify` execution | 155 | Every run-pass fixture with top-level `verify` blocks links a native suite that executes each body in declaration order and exits successfully. | +| `property` execution | 3 | Every run-pass fixture with top-level `property` blocks links a native suite that executes each body for 100 deterministic trials chosen by the existing generator. | +| Runtime contracts | 25 | Every `tests/runtime_fail/*.jett` fixture links and matches its interpreter contract: 17 wrapping-success cases and 8 runtime-failure cases, including failure class, message contract, and cleanup behavior where applicable. | + +These numbers are denominators, not a sample or a percentage target. Fixtures +added before parity is declared extend the applicable denominator. A fixture +may leave it only when the corresponding language or runtime feature is +explicitly marked unimplemented, with the reason recorded in the same change. + +Run-pass files without `main`, including verification and property fixtures, +count toward the 182-fixture lowering obligation. They do not enter the +30-fixture `main` execution denominator. The native verification suite now +links and executes every checked top-level `verify` body in each of 155 +fixtures as a separate execution gate. A native property suite uses the +interpreter's established deterministic `given` pools as test inputs, then +executes the checked property bodies as native code. Native terminal failures identify the property and trial number. The native +driver also replays and shrinks failed cases using the shared bounded search; +it returns the original suite output and shrunk inputs separately. + +Compile-fail fixtures remain frontend contracts. Native work must not change +their diagnostics, but rejected programs do not enter a backend denominator. +Required comptime evaluation remains a compile-time operation; a generated +program may not fall back to the interpreter for runtime semantics. + +The only language or runtime feature exclusions are features that the current +project records explicitly as unimplemented. Implemented portions of partial +features remain in scope, even when finishing their native representation is +difficult. Host-only output, optimization level, and debug-information quality +are backend staging limits rather than semantic feature exclusions. + +## Backend Strategy + +The first production path is a host ahead-of-time object backend using +Cranelift. The Cranelift crates are pinned exactly to `0.132.3`. Native CI tests +the compiler and packages with Rust `1.97.1`: + +```toml +cranelift-codegen = { version = "=0.132.3", default-features = false, features = ["std", "unwind", "x86", "arm64"] } +cranelift-frontend = { version = "=0.132.3", default-features = false, features = ["std"] } +cranelift-module = { version = "=0.132.3", default-features = false, features = ["std"] } +cranelift-object = { version = "=0.132.3", default-features = false } +target-lexicon = "=0.13.3" +``` + +The selected feature set covers the first x86-64 host and the intended AArch64 +host follow-up without enabling unrelated ISA backends. The implementation +change will record the same exact versions and features in `Cargo.toml`; this +plan does not itself add dependencies. + +Cranelift produces a target object, not an in-memory-only JIT result. Jett then +links that object with a target-matched Jett runtime and the host system +libraries to produce the executable. This keeps ordinary builds independent of +an installed LLVM development package and gives the first backend a bounded, +portable Rust dependency surface. + +LLVM remains the later optimizing backend. It must consume the same validated +MIR, layouts, intrinsic identities, runtime ABI, and conformance suite rather +than reimplementing language decisions. LLVM is not on the critical path for +the first full-parity claim. Adding it later must not weaken the Cranelift +denominators or make backend selection observable in program semantics. + +A C transpiler may be useful as a diagnostic oracle, but it is not the accepted +production path. Direct instruction and object emission is also out of scope; +Jett will not independently implement instruction selection, register +allocation, unwind metadata, and each platform ABI. + +## Host-Only First Slice + +The first slice supports only the compiler host target, initially +`x86_64-pc-windows-msvc`. An omitted target selects that host. An explicit +target is accepted only when it is exactly the supported host target. + +Any other target must produce an explicit unsupported-target diagnostic before +object emission or linking. Jett must never ignore `--target`, substitute host +layouts for a requested target, or emit an object that the selected linker +cannot consume. Target validation covers the Cranelift ISA, pointer width, +data layout, calling convention, object format, runtime library, linker, and +host SDK. Cross-compilation is added only after all of those inputs are +target-specific and tested. + +The CLI now validates an explicit `build --target` before reading source and +accepts only the current supported native host (Linux GNU or Windows MSVC on +x86-64). The CLI emits native artifacts using validated installed runtime +manifests; `build --check` retains frontend-only validation. Clean supported-host +package verification remains a release gate. + +## Required Compiler Handoff + +Native code generation begins only after the checked-program, HIR, and MIR +gates below pass. A backend adapter may reject a genuinely unsupported target; +it may not recover missing semantic facts by inspecting source spellings or +re-running frontend policy. + +### Checked-program gate + +The driver must publish one coherent checked-program artifact that keeps the +facts needed by every downstream phase together. At minimum it carries: + +- explicit `SourceOrigin` and canonical declaration and function identities; +- deterministic concrete generic and method call targets; +- function parameters, locals, checked expression and definition types, and + the type interner or a closed canonical type table; +- aggregate, enum, machine, bitfield, recursive-value, and resource layout + inputs; +- trusted-stdlib and compiler-intrinsic identities as checked identities, not + source-name tests; and +- reflection, capability, and required comptime results used by runtime + lowering. + +Session-local IDs may join facts inside one checked program. Persistent +artifacts and symbols use canonical identities and structural types, never raw +`FileId`, `DefId`, or `TypeId` indices. The gate fails on an unresolved call, +missing type, missing origin, or ambiguous trusted operation. + +### HIR identity gate + +Validated HIR must represent every accepted runtime construct and preserve the +checked identity of every direct call, method, intrinsic, resource operation, +and trusted hook. It makes lexical evaluation order and concrete generic +instantiation explicit. It does not retain an AST fallback that a backend must +reinterpret. + +The 182-fixture lowering denominator first passes through this gate. Any +unsupported-HIR error for one of those fixtures is a parity failure, not an +allowed skip. + +### MIR control-flow and ownership gate + +Validated MIR is the sole semantic input to native code generation. Before it +is backend-ready, it must make all branches, loops, handlers, failure paths, +calls, and returns explicit basic-block control flow. Operands and locals have +complete types and layouts, and each runtime operation has a typed intrinsic or +function identity. + +Definitive ownership dataflow and drop elaboration also belong here. The MIR +validator must prove moves, copies, and views follow the checked value category +and that every owned value and resource is cleaned up exactly once on normal, +early-return, handled-failure, and runtime-failure edges. Code generation is +blocked when an ownership state, cleanup edge, or layout remains implicit. + +## Versioned Native Runtime ABI + +The native runtime is packaged behind a versioned C ABI. It is an internal +compiler/runtime contract, but version mismatches must fail deterministically +rather than rely on Rust layout or symbol compatibility. + +- Exported runtime symbols carry an ABI version namespace or are accompanied by + a required ABI-version symbol. Objects and runtime libraries record the same + version. +- The boundary uses fixed-width integers, C-compatible records, pointers with + explicit lengths, opaque handles, status codes, and out parameters. +- Rust `String`, `Vec`, `Result`, trait objects, closures, `Any`, enum layout, + and other Rust-ABI values never cross the boundary. +- Ownership is explicit for every pointer and handle: borrowed inputs have a + bounded lifetime, transferred inputs identify the new owner, and created + values have one matching retain, release, or drop contract. +- Runtime context is passed explicitly for capabilities, allocation, resource + registries, scheduling, diagnostics, and other per-program state. Native + code must not acquire undeclared ambient authority. +- The Cranelift ABI realizes that rule as one backend-private, pointer-sized + first parameter on every emitted Jett function. It is absent from source, + HIR, and MIR arity; direct Jett calls forward the caller's exact pointer, + and the exported `jett_aot_v1_entry(void *context)` wrapper passes its input + to the checked program entry without reconstructing or inspecting it. +- Panics and Rust unwinding may not cross the ABI. Runtime operations translate + failure to the stable status and error-value contract; native cleanup paths + remain responsible for owned Jett values. If entry execution and final + runtime-context destruction both fail, the cleanup/infrastructure failure is + the terminal process outcome, while diagnostics retain both failures. +- Host layout tests pin every shared record's size, alignment, field offsets, + tag values, and calling convention. Backend and runtime use one canonical + definition of those layouts. + +The compiler maintains one typed intrinsic registry shared by type checking, +the interpreter mapping, HIR/MIR lowering, and runtime ABI adapters. The +closed `IntrinsicId` set is the sole acceptance boundary for compiler-owned +call spellings. Checked call sites carry that ID into HIR and MIR; downstream +dispatchers match it exhaustively so adding an intrinsic requires an explicit +decision in every execution path. Source names remain only diagnostic and +symbol metadata. This prevents duplicated string dispatch and preserves +trusted operation identity through codegen. + +## Artifacts, Linking, and Packaging + +The host AOT pipeline is: + +```text +checked program -> validated HIR -> validated MIR -> host object + -> host linker + versioned runtime -> executable +``` + +The driver carries the primary source file's checked `main` as an exact +`FunctionId` in its backend-lowering result. Object emission and native test +harnesses consume that identity directly; they never rediscover the entry by +scanning source names or native symbols. + +The implementation must provide all of the following before the pipeline is a +supported `jett build` result: + +- deterministic native symbol mangling from canonical function identities; +- a host object with the correct target triple, object format, relocations, + unwind information, and profile settings; +- a small entry shim that initializes runtime context, passes process inputs + through declared capabilities, invokes Jett `main`, renders failures, and + performs final cleanup; +- a target- and profile-matched runtime static library, its ABI metadata, and + all required host system-library declarations; +- explicit linker discovery or configuration, safe argument construction, + host SDK validation, and actionable diagnostics that identify a missing + linker, SDK, runtime archive, or system library; +- an artifact layout that separates target and profile outputs, publishes only + successful final files, and removes incomplete temporary products; and +- redistribution notices and runtime artifacts needed to run the executable on + a clean supported host without the compiler source tree. + +On the initial Windows host, the linker contract must be tested with the +documented MSVC-compatible linker and Windows SDK/UCRT inputs. Finding Clang or +LLVM utilities on one developer machine is not sufficient packaging. Clean-host +CI must exercise compiler build, object emission, runtime archive selection, +linking, and execution. + +## Staged Acceptance Gates + +Progress is monotonic: a later gate includes all preceding checks, and a +backend feature is not complete while its fixture is skipped or delegated to +the runtime interpreter. + +1. **Baseline and harness:** freeze the fixture denominators, classify every + fixture by required constructs and runtime hooks, and add differential + harness support without changing current interpreter expectations. +2. **Checked-program handoff:** publish and validate the closed semantic + artifact, canonical identities, concrete calls, layouts, and trusted + intrinsic identities required downstream. +3. **Complete HIR lowering:** all 182 run-pass fixtures lower to validated HIR + with no source-form or name-based backend fallback. +4. **Backend-ready MIR:** those same 182 fixtures lower to validated CFGs with + explicit evaluation, failure, ownership, and cleanup behavior. +5. **Object and link slice:** Cranelift emits deterministic host objects for a + scalar/control-flow seed, links them with the versioned runtime, and rejects + unsupported targets and missing toolchain inputs explicitly. +6. **Runtime surface expansion:** add typed ABI operations and layouts by + feature family while continuously re-running every already-supported + fixture. Strings, aggregates, collections, generics, results and optionals, + reflection and JSON, capabilities and resources, and concurrency/runtime + services remain on the matrix until their current behavior is covered. +7. **Successful execution parity:** all 30 `main` fixtures compile, link, and + execute natively with interpreter-equivalent stdout, stderr, result, visible + capability effects, and cleanup behavior. +8. **Runtime-contract parity:** all 25 runtime-contract fixtures compile and + link. The 17 wrapping-success cases return their accepted values; the 8 + failure cases fail through the native runtime with the accepted message + contract and release every live owned value and resource exactly once. +9. **Full parity release gate:** all fixture denominators are complete with no + skips other than explicitly unimplemented features; the full Cargo suite and + clean-host native suite pass; artifacts run without the source tree; and the + stable design, architecture, and progress documents are updated to describe + the proven implementation. + +The parity report must publish counts as `passed / denominator` for each +obligation. A single percentage would hide the difference between code +that merely lowers, code that executes successfully, and code that preserves +failure semantics. + +The portable `native_conformance` integration suite runs on both supported +x86-64 hosts (Linux GNU and Windows MSVC). It includes manifest-driven execution +of all 30 entry-point fixtures and 25 runtime-contract fixtures, with exact +stdout/stderr and terminal status comparison against the interpreter. Provider +scripts come from the inventory, using the same decoders as the standalone +report tool. Successful runs and entry-failure status 71 both require successful +runtime-context cleanup; cleanup failure cannot count as a matching entry error. +The suite also includes the exhaustive verify and property execution gates, +plus failure probes that prevent a failed +source assertion in a later body from publishing a native executable. +The property diagnostic regression also requires the exact shrunk counterexample, +diagnostic code, source span, and preservation of existing output artifacts for +program, verify-suite, and property-suite builds. This is the required frontend +validation path. Native driver replay and shrinking are verified separately +with a backend-injected failure after successful frontend validation. +The `native_parity` example also discovers top-level verify/property bodies +from parsed source and reports their fixture and body denominators. Its exit +status and `fixture_gates_complete` field certify those fixture gates only; +`complete` remains false while the separately listed release gates are open. +Its object gate uses native test lowering, which roots checked verify/property +bodies as well as program functions; ordinary program-only lowering can produce +an empty object for a valid test-only fixture and is not the inventory gate. + +The [acceptance audit](native_acceptance_audit.md) maps these requirements to +their executable gates and separates them from unresolved semantic contracts. + +## Current Coverage Matrix + +The 2026-09-29 semantic audit found an additional native gap: interface-typed +runtime values and dynamic interface dispatch. Concrete interface method calls +already worked; typed erased boxes and generated dispatch now also cover direct +parameters, returns, stored values, mixed literals, and nested whole-container +conversions. Safe function signature adapters and runtime/baked identity now +cover primitive widths, collections, functions, machine states, and the tested +nominal refinements. The remaining interface audit and cross-platform release +gates are tracked in [native interface values](native_interface_values.md). +Passing the existing fixture denominators alone does not close these gates. + +[Exact concrete-owner preflight](native_erased_same_owner_arguments.md) now +validates erased direct/named-callback calls in the reference evaluator and +generated native dispatch before argument normalization or implementation body +entry. Matching owners preserve their view/owned modes; mismatches report +`interface dispatch concrete argument owner mismatch`. Retained nominal and +generic identities and primitive widths govern validation, with earlier argument +failure precedence and existing native unboxing guards preserved. + +The restored integration's normal Debug and Release runtimes each pass ten Owner +tests: thirteen original Source-deleted cases plus four production executions for +matching/mismatched owners with both Jett optimization settings, thirty-four +executions overall. All 2,331 workspace library tests pass, including six runtime +and eleven focused reference controls; twenty-seven broader interface tests pass. +Full coherent native, GNU, installed and exact-revision CI acceptance remain pending; +refinement and pending native Source combinations still need coverage. This +change preserves the checker's whole-parameter substitution: declared erased +children such as `list[Pairable]` retain their contract, and recursive Self +substitution is not introduced. The separate erased-interface equality contract +rejects implicit operators at compile time and retains explicit domain comparison +calls; see [the decision](../../open_design/interface_value_equality.md). +These bounded gates do not change the rough 87% planning estimate, fixed +207-fixture/182-object inventory or whole-language completion requirement. + +Explicit comptime values now retain each checked generic instantiation and +lexical reflected type-binding chain, including nested callback bodies. The +context fix and its differential regression coverage are recorded in +[native comptime contexts](native_comptime_context.md). + +Refinement predicates now evaluate in their declaration context, isolated from +caller aliases, locals, and generic/scoped type bindings. Their input has the +same fully coarsened base type in both execution paths. The regression also +pins original comptime source identity through parentheses; see +[refinement declaration contexts](native_refinement_context.md). + +`temporary_projected_views` extends the borrowed-field audit to owners returned +as temporary values. It covers nested list, set, map, Unicode string, and record +fields; direct and indirect calls whose later argument runs a fallback handler; +early returns from that handler; and loop `break`/`continue` cleanup. Exact output +pins single receiver evaluation and ordering, and the native launcher must exit +successfully without cleanup diagnostics. +`temporary_projected_view_failure` requires the original runtime error and +successful cleanup when a later indirect-call argument fails after borrowing +the nested temporary field; both the temporary owner and the failing argument's +owned collection must be released. + +`tests/native_parity.json` is the machine-checked fixture inventory. The table +below records implementation coverage; a row is complete only when its native +object, linked execution, and differential behavior gates all pass. Typed +lowering alone never changes an execution row to complete. + +For planning, the broad native-language coverage estimate is **about 87%**. +Update it as concrete language families gain linked native coverage. This is a +judgment based on the matrix, while fixture counts remain measured +pass/denominator gates. Earlier 80%, 85% and 86% checkpoints below are history. +The latest increase follows the verified +[ordinary borrowed-sum family](native_ordinary_borrowed_sum_codegen.md). + +| Surface | Validated HIR/MIR | Cranelift object | Linked native behavior | +| --- | --- | --- | --- | +| Fixed-width integers, floats, booleans, and `nothing` | covered | scalar expressions, direct calls, branches, and loops covered | linked native/interpreter scalar, conversion, numeric aggregate, and debug-value fixtures cover representative widths and floating-point edges; a width-by-operation arithmetic edge matrix now covers all integer and float widths, while exhaustive operand combinations remain pending | +| Strings and bytes | covered | all current string intrinsics, direct and projected Unicode-scalar string iteration, separately owned bytes storage, encoding leaves, and checked user-defined `Displayable.display` calls in interpolation | string search/replace, Unicode case changes, scalar `for` loops through nested borrowed fields, byte and encoding fixture functions, nested cleanup, moves/views/clones, and explicit interface display with a same-named inherent method covered | +| Structs, enums, bitfields, machines, and refinements | covered | concrete user structs and enums with typed payloads; bitfield construction and fields, including result-wrapped width validation; machine construction, transitions, state tests and state fields; transparent secret/refinement representation, `coarsen`, checked refinement predicates for base, intermediate-refined, and secret-backed inputs, and result-wrapped direct and reflected struct construction with validated fields | struct moves/views/clones, nested owners, explicit equality and failure cleanup; enum construction, payload transfer, clone, match and cleanup, plus scalar and supported aggregate payload-enum equality; bitfield field ownership, clone, byte roundtrip, and direct/reflected-builder width validation; machine state narrowing, transitions and owned payload cleanup; native/interpreter predicate success and false-result diagnostics for integer, owned-string, and secret-backed refinements, including base values inserted into reflected struct, enum, and machine builders; enum payloads containing user structs dispatch through exact explicit Equatable.equals; broader refinement boundaries remain pending | +| Lists, maps, and sets | covered | scalar/string/bytes/sum/nested lists, primitive and indexed-row list sorting, sortedness checks and sets, and primitive-backed refinement keys and set elements | compiled list access, insert/remove, reverse/repeat, scalar iteration and sort; `list.filter`, `map`, `flat_map`, `find`, `all`, `any`, `count`, `reduce`, `sort_by`, and `group_by` with named, inline, and captured callbacks; `map.filter`, `map_values`, and `for_each` callbacks; indexed-row sorting for interpreter-supported key types; set insert/remove/membership/clone and iteration; map literals, insert/remove/lookup/from_lists/clone and key-value iteration; representative `zip`, `chunk`, `enumerate`, `flatten`, set/list conversions, and map entries; borrowed iteration through nested struct-field and machine-state field collection paths, including owned string map values and Unicode string fields in a machine; refinement string and integer keys and set elements; contextual generic empty lists for struct, enum, and nested-list elements plus borrowed generic aggregate access covered; other projected views and unexercised generic collection shapes pending | +| Numeric aggregates | covered | `math.average` and `math.median` for all ten fixed-width integer and floating-point element types | native/interpreter differential fixtures cover every primitive width and overflow-safe floating-point extremes; empty-list and pending-operand error contracts covered | +| CSV | covered | checked parse, parse-with-header, and stringify leaves over owned lists and maps | strict quoting, CRLF, header values/errors, and nested allocation cleanup match interpreter | +| Crypto | covered | private SHA-256, SHA-512, MD5, and HMAC-SHA-256 byte kernels | native differential fixture covers public text digests, binary HMAC, long keys, secret comparison, and explicit declassification | +| Secret values | covered | transparent scalar and owned representations; redaction and string/bytes comparison | native differential fixture covers equal, unequal, length-mismatched and Unicode strings, bytes, redaction, and aggregate ownership | +| Results, optionals, and `handle` control flow | explicit CFG for statement-root, direct-call, indirect-call, supported intrinsic-argument, unary, cloneable binary, short-circuit boolean, collection, value-constructor, string-interpolation, machine-transition, task `run`/`join`, actor-spawn, actor-message, field-receiver, state-test, clone, coarsen, declassify, condition, match-scrutinee, for-iterable, assertion-condition, and breakpoint-condition handlers | genuine tags, owned payloads, selected extraction and certified nonconsuming handled payload views | nested sums, defaults, early returns, loop exits, terminal bypass, ordered call arguments and constructor fields, scalar, string, and enum evaluation order, short-circuit fallback skipping, handled field/state projections in `if`, `while`, and `match`, one-time for-iterable evaluation, handled debug conditions, ordered transition source/payload defaults, handled task values including pending `nothing`, actor source/argument order, and interpolated success/failure paths with ordered segment evaluation covered; stable ordinary handled-view list payloads, copied primitive/String destinations, later argument failures and pending metadata now have 15-Source/30-native coverage; other carriers, outer Default lifetimes, nested-expression and refinement handlers remain pending | +| Function values, closures, and indirect calls | covered; inline bodies extract to checked functions with explicit capture parameters and ownership modes | owned descriptors carry code addresses, copied capture environments, and source-derived debug labels; indirect calls pass the environment after the runtime context and borrow view parameters; parenthesized, returned, projected, inline, and pipeline callee expressions retain checked call order | named, capture-free, and captured callbacks passed, returned, copied through aggregates, and invoked through indirect calls, including view and capability parameters; callee handlers and terminal failures clean already evaluated owned arguments; explicit `comptime` materializes named, capture-free, and captured inline function values from closed pure expressions, including capability-accepting callbacks whose bodies execute only with runtime authority | +| Compiler intrinsics and reflection | covered with checked operands, source-aware reflection metadata, and closed `IntrinsicId` identities | `type.name`, `type.kind`, `type.has_secret`, `type.kind_tag`, `type.primitive_tag`, recursively constructed `type.info`, checked `type.arg`, struct/bitfield, enum, and machine metadata lists and layouts, active enum variant and machine state metadata, reflected field values, and checked reflected-type dispatch covered; other aggregate reflection pending | direct and generic scalar reflection, nested `TypeInfo`, indexed type arguments, struct/bitfield, enum, and machine metadata, active enum and machine state selection, reflected field values, and alias-aware `comptime type` dispatch match the interpreter on positive cases; alias probes remain empty as required; mismatch diagnostics and other aggregate reflection pending | +| Explicit `comptime` values | checked closed pure expressions and contextual expected types covered | evaluated scalar and supported composite values materialize as typed HIR; captured closure values gain typed caller-local bindings; original source bodies are not emitted | linked native/interpreter fixtures cover nested collections, sums, structs, bitfields, enums, machines, bytes, contextual `ok`/`fail`/`none`, named functions, capture-free inline functions, captured closures, and pending `nothing` values, including captured and list-contained values; runtime authority remains unsupported | +| Capabilities and runtime resources | nominal checked types and static Resource copy/payload guards covered | entry grants for all eleven capability types; capability task handles retain authority and pending depth through `run`/`join`; provider effects remain limited to implemented families | Stdout output, Clock/Random sampling, and immutable Environment launch snapshots covered; native/interpreter fixtures compare plain and pending Stdout and Environment debug values, nested capability joins, Stdout cancellation, and a joined Stdout write; successful native runs reject unconsumed scripted Random, Clock, and Graphics inputs; other providers/resources pending | +| Actors and structured concurrency | covered | sequential `run`/`join`/`cancel` values and result propagation; pending depth for `nothing` persists through calls, captures, and aggregates, while owned strings, bytes, lists, sets, maps, optionals, results, structs, enums, bitfields, machines, function descriptors, and `TypeConstruction` builders retain pending depth through runtime handles; actor identities and capability authority retain depth through separate scalar handles; actor constructors, registration, handler dispatch, and state writeback covered; all fixed-width integer, floating-point, and boolean task carriers retain pending depth | native/interpreter differential probes cover successful and failed string tasks, nested pending `nothing` and owned values including optional and result branches, captured functions, builders, actor identities, and capabilities, bare-unit and plain Stdout join failure, current sequential cancellation behavior, pending-value debug output, string and enum direct equality errors, pending function-call, builder-use, and actor-message errors, and aggregate enum-payload equality, direct ready-handle identity and recursive actor identity/pending-depth comparison, actor allocation and owned-state cleanup, message ordering, state mutation, and responses; asynchronous scheduling and cancellation checkpoints remain pending | +| JSON and trusted stdlib hooks | covered | checked `JsonTree` calls use trusted raw stdlib functions; supported structs, machines, collections including sets of primitive-backed elements, and top-level refinements specialize the checked source serializer, including public omission of direct secret fields; supported top-level enums use dedicated checked source hooks; primitive parse calls use private source decoders, including bounded `int8`/`int16`/`int32`, `uint16`/`uint32`, and `float32` construction; concrete structs, bare and state-qualified machines, supported secret wrappers, top-level refinements over supported bases, lists, sets of primitive-backed hashable types, string-keyed maps, optionals, and results specialize the checked source parser; other JSON shapes remain pending | native/interpreter fixtures cover raw trees, primitive and structured serialization, enum unit and payload values, machine state envelopes and public secret omission, primitive and structured parsing, top-level refinement parsing and serialization, secret-bearing records and machines, exact validation, renamed fields, aliases, nested collections including lists/maps of enums and enum payload structs, result branches, errors, and owned cleanup; other concrete JSON types pending | +| Trace, breakpoint, assert, and failure reporting | covered | primitive, capability, function, actor, `TypeConstruction`, and recursive list, set, map, optional, result, struct, bitfield, enum, and machine values trace; zero- and multi-binding breakpoints over those types; default and interpolated custom-message `assert` in test bodies; other special debug values pending | native debug lines match interpreter stderr for scalar and aggregate fixtures, including empty and recursive values, bitfield numeric/enum/payload fields, false conditions, Unicode strings, bytes, `nothing`, plain/pending capabilities, and an out-of-scope local; named, inline, captured, and comptime callbacks inside aggregates, with subsequent calls and failure cleanup; actor ordinals and partially filled struct, bitfield, enum, and machine builders match; custom assertion codegen, context-owned error text, launcher copy-out, and passing verify/property suites have separate tests; lexical breakpoint frames, nested-scope exit, and typed closure captures covered; recursive secret redaction covered, including inferred bindings, generic errors, and reflection failures | + +Task ownership planning follows the representation beneath secret and +refinement wrappers. Both `run` and `join` reserve the owned intermediates emitted +for those payloads; erased-interface joins reserve their extracted owner too. +Contextually secret byte tasks previously exceeded the temporary-slot bound +before the expected pending-operand diagnostic could execute. + +Numeric and boolean task values now retain pending depth through native locals, +direct and indirect function calls, closure captures, and result payloads. +Linked differential coverage spans `int64`, `uint64`, `float32`, `float64`, and +`bool`, including nested joins and cloned results. Scalar pending depth now +survives struct and enum fields plus primitive list construction, cloning, +insertion, removal, iteration, access, and sorting. Native unary and binary +operators plus boolean branch and breakpoint conditions now reject pending +primitive values with interpreter diagnostics; short-circuit operators retain +the interpreter's right-operand evaluation order. Pending scalar values in +unexamined intrinsic operands remain open. +Numeric list aggregates now reject pending containers and elements before +operating on scalar payload bits, with differential cases for `list.sum`, +`math.average`, and `math.median`. +Public list intrinsics reject pending list containers, and index-taking list +intrinsics reject pending integer indexes with interpreter-matching errors. +Differential cases cover length, append, get, insert, remove, sort, indexed +sort, sortedness, and both swap indexes. +`range`, `bytes.get`, `bytes.slice`, `string.slice`, and `string.repeat` now +reject pending integer operands with interpreter diagnostics. The byte and +string operations also reject a pending receiver. Other intrinsic consumers +of pending values remain open. +Scalar math intrinsics now reject pending numeric arguments before invoking +native kernels, with differential cases spanning every argument-bearing scalar +math intrinsic and each argument position used by the multi-argument forms. +Numeric conversions and `string.from_*` now reject pending primitive operands +with interpreter type errors. Numeric string parsers and byte/text conversion +intrinsics reject pending string or bytes handles before parsing or formatting. +Primitive set elements now preserve pending depth through membership, updates, +cloning, debug output, and iteration. Pending set containers fail at public set +intrinsics. Native `for` rejects pending list, string, map, and set containers +before iteration, matching the interpreter's shared iterable error. Pending +primitive map keys and values now retain pending depth through literals, +updates, lookup, removal, cloning, `from_lists`, and iteration. Pending map +containers fail at public map intrinsics, and pending input lists fail at +`map.from_lists`. Other intrinsic consumers of pending primitives remain open. +Reflected construction retains pending scalar field depth through builder puts, +cloning, trace output, and finished struct, enum, and machine values. Pending +numeric bitfield fields produce the interpreter's validation error. + +Capability task handles preserve pending nesting independently of the original +authority token. Joining a task restores the authority needed by the capability +operation; joining plain Stdout follows the interpreter's current cancellation +result. Stdout, Clock, and Random debug values use the interpreter's current +`nothing` representation, while Environment and Graphics retain their named +capability representation. This closes a handle-shaped task gap while the +remaining task semantics above stay open. + +Actor handler capability and state snapshots are explicit leading HIR/MIR +parameters, and `respond` participates in native ownership analysis. Checked +actor state initializers lower into constructor functions that source spawns +reference directly. Typed runtime leaves register actor-owned state records, +replace owned fields, and clean them at context destruction. Native actor +allocation, state writeback, and message dispatch pass linked differential +fixtures; true asynchronous scheduling remains a separate task. +Actor state now preserves pending primitive depth across constructor +initialization, handler entry, state updates, responses, and joins. Linked +interpreter/native fixtures cover `int64`, `bool`, and `float64` state, including +trace output and repeated handler calls. This closes the identified actor +state propagation gap while broader task scheduling remains open. + +The current fixture gates are: + +| Obligation | Passing | Denominator | Evidence | +| --- | ---: | ---: | --- | +| Typed backend lowering | 182 | 182 | `run_pass_backend_lowering_gaps_are_explicit_and_monotonic` | +| Native object generation | 182 | 182 | `native_parity_object_emit_obligations_emit_host_objects`; every run-pass fixture emits reachable native code | +| Successful/expected `main` execution | 30 | 30 | Windows MSVC production linking and exact interpreter stdout/debug-output comparison, including scripted Clock, Random, Environment, and Graphics inputs | +| Native `verify` execution | 155 | 155 | `native_verify_suites_execute_for_all_run_pass_fixtures`; 507 top-level bodies execute across the 155 fixtures | +| Native `property` execution | 3 | 3 | `native_property_suites_execute_for_all_run_pass_fixtures`; 18 top-level bodies each execute 100 generated trials | +| Runtime contracts | 25 | 25 | exhaustive runtime-contract probe, including scripted Clock and Random failures; matched behavior and checked native-value cleanup | + +These counts track fixture gates, not a weighted percentage of Jett syntax or +runtime semantics: fixtures differ in size, overlap, and coverage. The object +gate is currently 182/182 (100%), a useful progress measure rather than a +claim that the same fraction of the language has native support. Native +property-body execution is measured separately from the object and verify gates. + +`tests/native/arithmetic_matrix.jett` adds linked differential checks across +all eight fixed-width integer types and both floating-point widths. It checks +integer wraparound, minimum divided and reduced by negative one, unsigned +division/remainder, float arithmetic, signed zero, infinities, and NaN +comparisons through runtime function calls. The 81-test Windows native suite +includes this case; the matrix is an edge sample, not exhaustive over all +operand values. +`tests/native/json_nested_enum.jett` adds linked differential coverage for +lists and string-keyed maps of enums, including a list of enum values whose +payload contains a user struct. Exact parsing and serialization agree with +the interpreter. The 82-test Windows native suite now includes both added +fixtures; fixture gate denominators remain unchanged. +`tests/native/projected_machine_sequences.jett` compares list, set, and map +iteration through a state-qualified machine's fields with the interpreter, +including repeated calls on a borrowed machine parameter. The 83-test Windows +native suite includes this projected-view case. +`tests/native/generic_empty_collections.jett` adds linked differential +coverage for generic empty lists specialized to structs, enums, and nested +lists, and for borrowed generic `list.first` calls returning owned aggregate +elements. The 84-test Windows native suite includes this case. +`tests/native/displayable_interpolation.jett` checks that interpolation of a +user-defined value calls its explicit `Displayable.display` implementation in +both the interpreter and native executable. A same-named inherent method +returns different text, so the fixture also checks exact interface dispatch. +The 85-test Windows native suite includes this case. +`tests/native/handled_interpolation.jett` covers a `handle` inside an +interpolated expression. MIR now extracts those handlers and saves interpolated +values in source order before a later fallback can run. The linked fixture +checks successful and fallback paths plus observable segment evaluation order; +the 86-test Windows native suite includes it. +`tests/native/json_nested_bitfield.jett` adds a linked differential case for +bitfield decoding and serialization inside a struct. It covers an enum-backed +field, byte payload, and nested field-path errors for invalid enum and byte +values. The 87-test Windows native suite includes this case. +`tests/native/projected_string_iteration.jett` covers repeated Unicode-scalar +iteration through a string field of a borrowed struct. The linked +`tests/native/generic_map_aggregates.jett` specializes generic empty maps and +lookups for struct, enum, and nested-list values. The 89-test Windows native +suite includes both collection cases. +The 90-test Windows native suite also checks that successful linked programs +reject unconsumed scripted Random, Clock, and Graphics inputs with the same +message and retained output as the interpreter. +`tests/native/nested_borrowed_collections.jett` iterates a map of owned structs, +list, set, and Unicode string through a nested borrowed struct field twice. +Projected strings now use the same MIR root loan and field path as projected +collections, and the native validator admits string loans. The 91-test linked +suite compares output and successful cleanup with the interpreter. + +As of 2026-09-26, the working estimate for overall native language coverage is +**about 80%**. This is a deliberately coarse progress marker, reviewed in +five-percentage-point steps against the coverage matrix above, not a computed +ratio or a release gate. The fixture counts remain the reproducible measures. +View-parameter function values, pending `nothing` tasks, and general +function-expression calls gained native execution, closing known gaps without +yet supporting the next five-point step of the estimate. It stays below full +parity while known semantic gaps remain in +ownership and nested handlers, capability/resource providers, asynchronous +task behavior, JSON shapes, reflection, and special-value diagnostics. + +Pending `nothing` values now retain their nesting as an unowned native scalar. +Each `run` adds a level; `join` unwraps one level, while joining plain `nothing` +returns `fail("task was cancelled")`. Calls, branches, closures, lists, maps, +optionals, results, structs, enums, machines, and explicit `comptime` preserve +that value. Native output and terminal comparison errors match the interpreter. +The current sequential `cancel` leaves its operand unchanged and returns plain +`nothing`; this parity slice does not implement the designed concurrent +cancellation checkpoints. Dedicated native fixtures cover these behaviors +outside the fixed 182-fixture object denominator. +Validation for this slice passed the full 53-test Windows native execution +suite and the unchanged 182/182 object gate, plus focused runtime, backend, and +driver unit tests. + +Function parameter ownership now remains part of checked function type +identity through native emission. Differential coverage includes borrowed and +consumed inputs, mixed borrowed/owned arguments, captured closures, explicit +comptime function values, generic forwarding, pipelines, and views evaluated +before handlers. Generic calls and pipelines reject a view passed to an owned +parameter at the frontend; affected JSON map observers explicitly clone their +owned-input API arguments instead of relying on an implicit native clone. +General function-expression calls now lower and execute through parenthesized +callbacks, immediately invoked returned functions, function-valued fields, and +pipeline targets. They retain the existing indirect-call order: evaluate +arguments in source order, then evaluate the callee expression, then invoke the +selected function value. +Argument handlers may therefore change which callback is selected, and an early +return from an argument must skip callee evaluation. Constructor, declared +function, and compiler-intrinsic identities retain their checked dispatch. +Function parameter ownership, purity, and capability rules apply to expression +callees exactly as to existing function values; no new call syntax is introduced. +The frontend must check ownership in that same argument-before-callee order, +including parenthesized owners, and must enforce capability purity for all +callee shapes. Named arguments require checked declaration parameter names. +Parentheses around a declared function preserve those names; an anonymous +function type supplies no names, so labels on such calls must be rejected +instead of silently interpreted as positional arguments. This conservative +boundary avoids adding parameter names to function type identity or deriving +them from whichever implementation happens to be stored at runtime. +Inline function bodies follow the same signature-based purity rule as declared +functions. Checking a closure body uses its own function context; constructing +a closure does not execute that body or inherit the caller's comptime authority. +Pure expression calls retain secret input taint on their return values. +Enum constructor payloads carry checked parameter order and a separate source +evaluation permutation through HIR, MIR, and native emission. Reordered named +payloads, handlers, early returns, pipelines, and baked enum values preserve +both their field layout and lexical effects. A function-valued field whose name +matches a variant of its result enum still invokes the callback. +Mixed named and positional arguments fill the next unbound parameter in both +execution paths. Interpreter calls isolate the callee's lexical locals and +namespace aliases, including aliases retained by returned closures. +Validation passed all 56 Windows native execution tests and the unchanged +182/182 object gate. After the final argument-binding and lexical-scope fixes, +all 296 interpreter tests, 578 frontend fixtures, 10 Graphics frontend tests, +and the three dedicated native expression-call/enum tests passed again. +The new native fixtures extend focused coverage without changing the fixed +run-pass object denominator. +Function-valued trace and breakpoint bindings now render the interpreter's +source-derived labels. Named callbacks retain their canonical source name; +inline callbacks retain their source parameter names independently of capture +count, generic specialization, and native symbol identity. The owned label +travels with the descriptor through clones and nested lists, maps, optionals, +results, records, recursive enums, and machines. Debug reads never invoke the +callback or expose captured values. Native fixtures cover repeated tracing, +subsequent invocation, conditional breakpoints, terminal-failure cleanup, and +callback traces in `verify` bodies and 100 generated `property` trials. +Generic closure factories also now receive concrete native instantiations: +ordinary generic signature annotations no longer trigger reflection-only +handling, and HIR parameters come from each checked concrete function signature. +Integer and string specializations retain their own parameter/capture types. +The function-debug slice passed the full 58-test Windows native suite and +182/182 object gate, plus 578 frontend fixtures, 153 typechecker tests, 59 HIR +tests, 26 MIR tests, 54 backend tests, 90 runtime tests, and 61 driver unit tests. +Generic factories still reject move-only captures with E0402. Overall coverage +remains at the coarse 80% estimate while the matrix's remaining gaps are open. + +Concrete source method values now pass through checker-selected body identities +into HIR and native function descriptors. This covers struct methods and +interface implementations stored, returned, cloned, projected from aggregates, +invoked through generic factories, baked with explicit `comptime`, and traced. +Receivers remain explicit first parameters with their checked `view` modes. +Where an inherent method and interface implementation share a name, concrete +method values use the inherent body, while interface-qualified calls use the +implementation. Interpreter method calls now retain their declaration namespace +so lexical helper references agree with native execution. The differential +`tests/native/method_function_values.jett` fixture exercises these paths in +`main`, `verify`, and 100 property trials. The broad native percentage remains +the coarse 80% estimate pending further coverage-matrix closure. +Explicit `comptime` extraction now retains the visible block-local `use` +aliases for each closed expression. An inner alias may shadow an outer one; +evaluation isolates each expression so the outer binding is restored afterward. +`tests/native/comptime_namespace_aliases.jett` compares baked named and returned +callbacks through both scopes with the interpreter. This removes the previous +local-alias frontend gap without admitting runtime locals to comptime. +Actor handles now participate in native trace and breakpoint layouts, including +handles nested in lists. Registration preserves the interpreter's `actor#N` +spawn ordinal independently of native allocation IDs; debug validates each +handle and never reads actor state. `tests/native/debug_actor_values.jett` +compares both debug lines and subsequent message sends with the interpreter. +Other special debug values and cross-function breakpoint scope remain open; +overall language coverage stays at the coarse 80% estimate. +`tests/native/debug_type_construction.jett` compares empty and partially filled +builders for struct, bitfield, enum, and machine owners, including a struct +whose fields are supplied out of declaration order. Native builder metadata +stores checked field debug layouts and successful insertion order; tracing +does not consume the builder. The secret-field output policy is recorded in +`docs/completed/secret_debug_output.md`. The 62-test Windows native suite, +182-fixture typed-lowering gate, and backend/runtime unit suites pass after +this change. +The linked `tests/native/collection_callbacks.jett` fixture now covers +higher-order list and map helpers, including an inline predicate, a captured +sort key, and an observable `map.for_each` callback. The linked +`tests/native/collection_shapes.jett` fixture covers generic pair/indexed +records, chunk/zip/flatten/enumerate, set/list conversion, and map entries. +Both compare exact output with the interpreter. These checks replace the broad +"callback helpers and collection shape conversions pending" label with the +remaining unexercised generic shapes and projected views; the overall coarse +80% estimate is unchanged. +Reflected enum and machine builders now accept already validated exact +refinement field values. The native JSON source selector uses this for enum +payloads and machine states, including a machine with a refined field beside +an ordinary field. `tests/native/json_refined_aggregates.jett` checks +primitive-backed and struct-backed refinements, valid parsing, predicate +failures, and direct reflected construction against the +interpreter. Trusted JSON decoders bind each inserted `Field` from checked +reflection metadata. Ordinary struct builders now accept base values for +refined fields and invoke checked predicates at finish, including nested +refinements and mixed refined and ordinary fields of the same base type. +Enum builders now also accept base payload values and validate the selected +variant's checked predicates at finish. Machine builders now accept base +payload values for bare and state-qualified targets, checking only the active +state's predicates at finish. The overall coarse estimate stays 80%. +At this checkpoint, the 68-test Windows native suite, 182/182 native object +gate, 182/182 typed-lowering gate, and focused HIR/MIR/Cranelift suites pass. + +Nested supported enum and bitfield fields now select checked JSON source hooks +inside records and collections. Generic `type.name[T]()` equality with a +literal gives raw `json.JsonTree` its distinct checked branch, while reflected +names expand imported namespace aliases. A linked native/interpreter fixture +covers nested parsing, serialization, and namespace-qualified names. The +exhaustive gate gained `json_reflection_bridge_parity.jett`, +`json_shape_matrix.jett`, `json_tree_reflection_parse_wrapper.jett`, and +`namespace_use_alias.jett` without losing an earlier object pass; +`json_runtime_reflection_metadata.jett` also gained its `main` behavior match. +Checked generic `type.has_secret[T]()` branches now omit whole secret-bearing +record fields before native serialization, including fields holding collections +of secrets. The trusted reflected decoder inserts already validated refinement +values; native builder insertion checks exact field metadata and type. Native +ownership planning reserves separate temporaries for validated records and +their result wrappers. Linked native/interpreter fixtures cover the nested +serializer and decoder, including error paths. Struct builder insertion of +base values into refinement fields now runs checked predicates at finish; +the same holds for enum and machine payloads, with validation restricted to +the selected variant or state. +The exhaustive audit gained `json_reflection_nested_decoder.jett` and +`json_reflection_nested_serializer.jett`, without losing an earlier pass. +Captured inline functions now extract to checked functions with explicit +capture parameters. An owned descriptor carries both the code address and a +copied environment; indirect calls pass the environment through a hidden +argument. Linked native/interpreter fixtures cover nested closures, loop +captures, higher-order callbacks, and captured strings and narrow/floating +numbers. The 207-row audit gained `captured_local_function_name.jett`, +`closures.jett`, and `closures_advanced.jett`, and `main` execution gained the +first of those, without losing an earlier pass. Before this step, function +values had moved to owned descriptors, with a linked fixture covering copies +through struct fields, lists, and maps; that audit held at 159/182. +Checked source JSON parsing now accepts sets of narrow integers and +primitive-backed refinements as well as the earlier `string`, `bool`, `int64`, +and `uint64` cases. A linked native/interpreter fixture covers duplicate +elimination, boundary values, indexed range and refinement errors, exact +parsing, and a set inside a record. The 207-row object audit remains 159/182 +with no lost passes; this fixture is outside that fixed gate. +Checked source JSON serialization now handles sets of primitive-backed +elements, including refinements, directly, through pipelines, and as record +fields. The serializer clones a viewed set before iterating it. A linked +native/interpreter fixture covers integer bounds, a string refinement, and +all three call shapes. The 207-row object audit remains 159/182 with no lost +passes; this additional differential fixture sits outside that fixed gate. +Checked source JSON decoding now accepts `float32` directly and within +supported aggregates by reading a JSON number as `float64` and explicitly +rounding to binary32. Checked source serialization accepts `float32` as well. +The pure `float32.from_float64` conversion works at runtime and in explicit +`comptime` expressions. A linked native/interpreter fixture covers those +paths and shape errors. The 207-row audit gained +`json_parse_exact_primitive_edges.jett` and `json_sized_primitives.jett` +without losing an earlier object pass. The denominator remains 182 because +the added differential fixture is outside the exhaustive run-pass gate. +Checked stdlib JSON decoders now construct `int8`, `int16`, `int32`, `uint16`, +and `uint32` from range-validated `int64` values, including inside supported +aggregates. A linked native/interpreter fixture covers bounds, malformed +shapes, and a record with a `list[uint16]`; its output also matches the +pre-change interpreter. At that narrow-integer checkpoint, the exhaustive +audit kept the same 157 object passes, and both affected fixtures reached +their later `float32` parse blocker. +Direct struct constructors lower checked base-value field predicates into MIR +branches before creating their required `result[Struct, string]`. Native +execution matches the interpreter for successful string and integer fields, +ordered nested predicates, handled failures, and a constructor nested in a +call. Inputs already validated as an intermediate refinement skip their +ancestor predicates; secret-backed integer and owned-string inputs pass their +underlying values to the checked predicate. The same rule applies at explicit +refinement `handle` boundaries. A linked native/interpreter fixture covers +their successful and rejected paths. Exact refined fields retain the success +path. At that checkpoint the 207-row audit remained 157/182 with no regressions; the earlier exact-field change gained +`type_construction_builder.jett`. +The ownership checker now treats `ok`, `fail`, and `some` payloads as moves. +The secret-bearing JSON policy fixture explicitly clones a payload used in +both result arms and now emits a native object; the 207-row audit gained that +one object gate without losing any previous pass. +Self-recursive structs with a finite base and supported fields now specialize +the source JSON serializer, parser, and exact validator. The eligibility walk +accepts a revisited struct without skipping checks of its other fields, and +the existing checked specialization cache keeps the generated call graph +finite. A linked native/interpreter test round-trips a nested `Node` through +`json.parse_exact`; `recursive_owned_values.jett` joins the object gate. +Generic record helpers instantiated for aliases and other non-record kinds now +emit an empty native builder that preserves handled construction failures. +The checker also keeps the reflected source kind in scoped `comptime type` +bindings, so alias JSON validation reaches the aliased base and its renamed +fields. Linked native/interpreter fixtures cover direct alias-builder failures +and nested alias JSON parsing. `json_parse_exact.jett` and +`reflection_type_id_duplicate_aliases.jett` join the object gate. +Machine `TypeConstruction` now validates checked state and payload metadata, +builds the selected tagged record, and checks state-qualified targets at +finish. This adds `type_construction_machine.jett` to the object gate; a linked +fixture checks construction and handled owner, member, and missing-field errors. +Enum `TypeConstruction` now validates the checked `TypeVariant` and payload +field metadata, builds the selected tag and fields, and returns handled +owner, member, and missing-field errors. This adds native objects for +`type_construction_enum.jett` and `json_tree_reflection_construction.jett`. +Bitfield `TypeConstruction` now uses the native builder with checked field +metadata and width validation at finish, adding +`type_construction_bitfield.jett` to the object gate. A linked fixture compares +successful reconstruction and handled width errors with the interpreter. +Typed payload enum equality compares integer, boolean, floating-point, and +string fields for the selected variant, adding native objects for +`namespace_dotted_qualified_types.jett` and `namespace_qualified_types.jett`. +Aggregate payload comparison now uses a recursive type descriptor for lists, +sets, maps, optionals, results, bytes, bitfields, machines, and nested enums. +`tests/native/enum_aggregate_equality.jett` compares matching and differing +values with the interpreter, including collection order, recursive payloads, +signed zero, and NaN. Payloads containing user structs now invoke exact checked +`Equatable.equals` methods through resumable cursors and compiled calls. The +frontend rejects missing methods and retains opaque method boundaries. See +`docs/completed/enum_payload_struct_equality.md` for implementation and linked +debug/release coverage. +[Actor identity equality](actor_identity_equality.md) now implements live +instance identity in the reference evaluator and native backend. Exact checked +nominal actor types must match; clones preserve identity and separate spawns +remain unequal even with identical state. Recursive enum payload comparison +checks identity and per-handle pending depth without reading state, while direct +pending comparisons retain their unsupported binary-operation error. Local +MSVC static-CRT Debug/Release acceptance on 2026-10-12 passes nine Source-deleted +executables per runtime profile, including production output with both Jett +optimization settings. Escaping comptime actor graph import, full predecessor, +GNU, installed-distribution and coherent-revision acceptance remain open. This +closes the selected actor equality gate without changing the rough 87% estimate, +fixed 207-fixture/182-object inventory or whole-language completion requirement. +Active `type.variant_value` selection matches native/interpreter output for +payload and empty variants. Reflected `type.field_value` and +`type.variant_field_value` now read checked struct, bitfield, and enum payload +fields in dedicated native/interpreter fixtures, including owned and secret +payloads. Metadata mismatches fail safely, but their exact diagnostics and +alias-equivalence behavior still need interpreter parity. +Owned struct-field reads deep-clone the selected value while projected views +remain borrowed. This adds `generic_reflection_branch_specialization.jett` and +`generic_reflection_match_specialization.jett` to the object gate; other affected +reflection fixtures advance to later unsupported intrinsics. +Reflected field reads add object gates for `bitfield_uint64_reflection.jett`, +`json_reflection_flat_serializer.jett`, `type_reflection.jett`, and +`json_tree_reflection_variant_metadata.jett`. The latter now emits an object. +Native machine state selection and reflected state-field reads match dedicated +native/interpreter cases for general and state-qualified values. They move +`json_parse_machine_envelope.jett` to the unsupported `json.parse` gate and +`type_info_reflection.jett` to reflected type dispatch. Checked dispatch now +matches the runtime `TypeInfo` to one specialized body through canonical type +identity, including source aliases nested inside generic types. That adds +`namespace_comptime_reflection_aliases.jett` and `type_info_reflection.jett` to +the object gate. Checked `type.arg` selection then adds +`comptime_type_bind.jett` to the object gate. The native/interpreter fixture +covers nested generic arguments and alias bases; out-of-range native failures +still need exact interpreter diagnostic parity. +Checked machine layouts, state lists, and transition lists also lower to +ordinary native values. The native/interpreter fixture covers state-qualified +metadata and the empty total probe for non-machine and alias types. + +The counts come from the exhaustive 207-row `native_parity` probe, which +attempts every fixture regardless of staged `object_emit` labels and returns +failure until all denominators pass. The original manifest pins 29 nonempty, +code-bearing object gates; the exhaustive Windows MSVC probe also attempts every +unmarked row and now proves 117. Four string/comptime fixtures began emitting +objects after the remaining string intrinsics were implemented; the payload +enum and unit-equality slice adds `enum_advanced.jett`, and bitfield value +support adds `namespace_exports_syntax.jett`; byte decoding adds three bitfield +roundtrip objects, two of which also execute native `main`; `trace_basic.jett` +adds one object and native `main`; primitive list sort adds +`list_sort_uint64.jett`; primitive sets add `set_empty_helpers.jett`; maps add +`map_from_lists_duplicate_keys.jett`, `map_merge_helpers.jett`, +`map_operations.jett`, and `map_set_source_surface.jett`. +Scoped breakpoint bindings add `breakpoint_basic.jett` to both native object +and linked `main` gates. Later primitive debug-value formatting adds +multi-binding breakpoints. A recursive type graph now formats aggregate +bindings for trace and breakpoint; bitfields and special values remain open. +Compiler-owned predicate functions and CFG lowering for base-value refinement +boundaries add `integer_nonzero_proofs.jett` to the native object gate. The +native/interpreter fixture covers ordered ancestor predicates, false-result +messages, owned-string cleanup, and successful transfers. The remaining +refinement fixtures advanced to JSON intrinsics or primitive-backed sets. +Predicate runtime failures remain pending; the later slices above add +secret-backed and already-refined inputs plus direct struct construction. +Primitive-backed refinement collection keys and elements, plus normalized +boolean lookup keys, add `primitive_collection_hash_types.jett` to native +object emission. A native/interpreter fixture covers string-refinement sets +and maps, integer-refinement sets, and boolean set/map lookup and removal. +Checked raw-tree JSON bridging adds `json_parse_error_parity.jett`, +`json_parse_success_parity.jett`, and `json_tree_value.jett` to object +emission. Primitive serialization uses the same trusted raw serializer in +native/interpreter execution fixtures. Private source decoders now handle +checked scalar parses for `string`, `bool`, `int64`, `uint64`, `float64`, +`bytes`, and `nothing`, including both public parse entrypoints. A dedicated +native/interpreter fixture checks values and shape/range errors. This moves +`json_parse_exact_primitive_edges.jett` to its then-later narrow-numeric parse +blocker, but did not add an object gate. The checked integer decoders described +above leave `float32` and other JSON shapes pending. +Lists, string-keyed maps, optionals, and results whose leaves are `string`, `bool`, +`int64`, `uint64`, `float64`, `bytes`, or `nothing` now instantiate the checked +stdlib decoder for both public parse spellings. A linked differential fixture +covers nested lists, numeric lists, optional null/present values, map bytes, +indexed list errors, and map value errors. Another linked fixture covers result +success/failure branches, nested results, exact parsing, and malformed envelopes. +At that checkpoint, named aggregates and narrow numeric leaves retained their +existing lowering. Later source decoders cover supported aggregates and narrow +integers; `float32` and other JSON shapes still block the affected fixtures. +Sets of `string`, `bool`, `int64`, or `uint64` now use the checked decoder, +including exact parsing, duplicate elimination, membership, and indexed +element errors in a linked differential fixture. The interpreter handoff drops +ambiguous checked expression types from generic source spans instead of using +the last specialization's type for earlier calls. A portable run-pass check +mixes signed and unsigned set parsing to guard this behavior. The exhaustive +object count stays at 118/182; `json_parse_collection_edges.jett` now reaches +its named-struct result payload blocker. +Native `TypeConstruction` now carries an owned struct builder with checked +field layout metadata. `construct_put` transfers the builder and field value, +returns handled metadata and duplicate-field errors, and `construct_finish` +returns a constructed struct or a handled missing-field or owner error. A linked +native/interpreter fixture covers ordinary and generic structs, alias-typed +fields, successful construction, error messages, builder cloning, and cleanup. +The object gate adds `json_reflection_flat_decoder.jett` and +`reflection_type_id_duplicate_construction.jett`, reaching 120/182. +Refinement-validating construction remains a later native slice; the verifier +retains that explicit unsupported boundary. +The checked source JSON decoder now specializes concrete structs and aliases +before its general reflection fallback. The recursive native parse gate accepts +structs whose fields are already supported, including nested structs and +collections, while retaining the refinement and recursive-type boundaries at +that checkpoint. +Linked native/interpreter cases cover renamed fields, nested and list values, +alias targets and alias-typed fields, handled shape errors, and exact unknown +fields. Seven more run-pass fixtures emit native objects, reaching 127/182; +enum, bitfield, machine, secret, refinement, and narrow-numeric parsing still +need their own checked native paths. +Checked integer/float conversion adds `conversions.jett`. +Native empty `list[never]` length/emptiness support adds `list_operations.jett`. +Borrowed enum matching, explicit view-to-owner cloning at direct calls, and +borrowed list/map iteration over move-only elements add nine raw JSON tree +objects. `json_tree_parse_runtime.jett` also links and matches interpreter +behavior. Generic JSON reflection intrinsics remain guarded; this does not +establish typed JSON parity. +Shared interpreter/native encoding kernels add `encoding.jett` object emission; +a dedicated native main checks base64, hex, URL, and form encoding and their +error precedence against the interpreter. +Shared strict CSV parsing and header validation add `csv_operations.jett` and +`csv_escaping_helpers.jett` objects. A dedicated native main checks parsed +values, canonical quoting, malformed input, and header errors; allocation +fault tests verify nested list/map rollback. +Transparent secret/refinement representation and constant-time native secret +comparison add `secret_compare_boundary.jett` object emission; the two secret +integer wrapping fixtures now pass their runtime contracts. The remaining +runtime-contract gaps at that point required Clock and Random capability providers. +Explicit `coarsen` now carries the checked base representation through native +codegen, including owned string refinements and multi-step refinement ancestry. +Constructing and validating new refinement values at a `handle error` boundary +remains pending, so this slice does not change the fixture gate counts. +Shared private crypto kernels add `crypto.jett` and `use_imports.jett` object +emission. Their native leaves keep public wrappers in `.jett`, with a dedicated +main checking digest bytes, long-key HMAC, and explicit secret declassification. +Nested result/optional handlers in direct-call arguments add +`uint64_checked_expression_runtime_main.jett` object emission and native main +execution. Differential mains cover both handler branches and named-argument +evaluation order; arbitrary expression nesting and refinement handlers remain +open. +Handlers inside unary and plain-scalar binary expressions now extract into MIR +before native ownership validation. Boolean `and` and `or` use explicit MIR +branches so a right-side handler runs only when needed. The linked +`tests/native/nested_handle_binary.jett` fixture checks both handler outcomes, +left-value capture before a fallback mutation, unary negation, a binary value +inside a call argument, and short-circuit skipping. Handlers inside list and +map literals, struct, bitfield, enum, and machine constructors, and +optional/result wrappers now extract into MIR. Constructor fields are +evaluated into owned locals in source order before aggregate construction. +`tests/native/nested_handle_constructors.jett` compares native stdout and +debug lines with the interpreter for present and missing values, including a +handled map key, named-field order, checked bitfield width, and owned string +payloads. Indirect function-value calls with owned arguments now extract +argument handlers into MIR in lexical order before reading the callee. +`tests/native/nested_handle_indirect_call.jett` checks both handler branches +and a fallback that rebinds the mutable callback; +native emission also evaluates ordinary indirect-call arguments before the +callee to match the interpreter. Direct view arguments of cloneable values now +snapshot into MIR locals before a later handler can mutate their source. MIR +lowering uses the checked type interner and rejects resource-bearing aggregates +at this boundary. Supported intrinsic arguments also extract handlers in +lexical order; `tests/native/nested_handle_indirect_view.jett` and +`tests/native/nested_handle_intrinsic.jett` compare both paths with the +interpreter. `tests/native/nested_handle_indirect_aggregate_view.jett` checks +that a list view captures the old value before a fallback replaces its source. +`tests/native/nested_handle_borrowed_stdlib_call.jett` checks the same rule for +a source stdlib call with a borrowed list parameter and handled index. +`tests/native/nested_handle_enum_binary.jett` checks that enum equality clones +its left operand before a right-side handler mutates the source, including a +constructed enum and copyable string equality. Borrowed aggregate compiler +`tests/native/nested_handle_refined_binary.jett` compares primitive-backed +integer and string refinements through the same path. Borrowed aggregate compiler +intrinsics, authority-bearing direct views, other refined binary operand +combinations, other nested forms, and source syntax for a handler inside +interpolation remain open. +Field receivers and state tests now extract nested result/optional handlers +before native ownership validation. `tests/native/nested_field_handle.jett` +covers owned and nested fields, defaults, and early-return failures; +`tests/native/nested_state_handle.jett` covers handled state checks in `if` +and `while`, including repeated condition evaluation. Handled `match` +scrutinees and `clone`, `coarsen`, and `declassify` operands have separate +native/interpreter fixtures. `tests/native/nested_for_handle.jett` covers a +handled list iterable with success, default, and early-return paths and +observes one source call per loop. `tests/native/nested_debug_condition_handle.jett` +covers handled assertion and breakpoint conditions, including a default that +reaches a passing assertion. `tests/native/nested_transition_handle.jett` +compares a handled machine transition source and payload with the interpreter +on success and default paths, including their observable evaluation order and +an earlier payload captured before a later handler mutates its source local. +Terminal `return` handlers nested in transition operands are accepted by the +checker but currently fail in the interpreter with `return` cannot escape +expression evaluation, so that parity case remains open. +`tests/native/nested_task_handle.jett` +compares handled `run` and `join` operands with the interpreter for string and +pending `nothing` values, including success and default paths. +`tests/native/nested_actor_spawn_handle.jett` +and `tests/native/nested_actor_message_handle.jett` cover handled actor +constructor, target, `send`, and `ask` expressions; their output checks +lexical argument order and an earlier argument preserved across a later +handler mutation. The 80-test Windows native suite exercises these paths; +the coarse language-coverage estimate remains 80% while other gaps remain +open. +Enforcing immutable-local rebinding in the frontend and correcting affected +fixtures adds native objects for `string_iteration.jett`, `set_operations.jett`, +and `uint64_checked_expression_runtime_types.jett`. The last also passes a +supplemental native/interpreter dispatch execution check. Fixture membership +and denominators are unchanged. Unit and payload enums also pass dedicated +native/interpreter differential fixtures, including recursive owned payloads. +Native `math.average` and `math.median` over integer and floating-point lists +add `math_extra.jett` object emission. They share the interpreter's floating-point +reduction kernels and pass differential execution for ordinary and extreme values; +the empty-list runtime error is checked directly. The fixture has no `main`, so +the tracked native-main count is unchanged. +Direct string `for` iteration now lowers through the MIR sequence pass and +uses Unicode scalar count/index leaves, matching the interpreter rather than +the grapheme-cluster `string.chars` API. This adds `loops.jett` object emission; +dedicated Windows and Linux native/interpreter fixtures exercise a joined emoji, +empty input, and early `break`. +Production Clock authority and wall-clock sampling add native objects for +`clock_production.jett` and `clock_scripted.jett`. `clock_production.jett` +also passes native `main` execution, and a dedicated executable checks a +mixed Clock/Stdout entry. Clock conversion shares the interpreter's pre-epoch +flooring and range checks. The launcher now accepts an explicit test-only +sample script. The parity probe supplies the same samples to the interpreter +and native runtime for `clock_scripted.jett`, including pre-epoch timestamps, +and an empty script for the exhausted-provider runtime failure. Successful +native entries now reject unconsumed scripted Random, Clock, and Graphics +inputs in the interpreter's priority order, while earlier runtime failures +remain primary. A linked native/interpreter test checks the exact terminal +message, exit status, and retained stdout for each provider. +Random now uses one context-bound provider shared with the interpreter. Native +entry grants initialize OS entropy once, while the parity probe can inject +normalized bounded, unit-float, and boolean samples. The two Random runtime +failure fixtures check invalid and exhausted scripts with exact messages and +clean native teardown. `stdlib/random.jett` retains the public collection +algorithms; native `list.__swap` supports the scripted shuffle path. +Environment now freezes the same launch snapshot type in the shared runtime. +Native entry grants a context-bound token, and the `Environment.__args` and +`Environment.__get` leaves return independently owned lists and the exact +result/optional shape. The injected snapshot fixture covers duplicate names, +empty arguments, missing values, invalid names, and invalid Unicode values; +`time_and_os.jett` covers a production entry. Raw platform capture details +specified in the Environment design note still need a separate backend audit. +Unit-enum equality borrows its operands while comparing tags, so comparisons +of an enum field do not move that field out of its aggregate. The bitfield +roundtrip fixture now emits an object and matches interpreter execution; the +native bitfield fixture compares the decoded enum field twice before using its +parent again. General owning projection of move-only fields remains guarded. +Native arithmetic now accepts a nonzero refinement as the right operand of +integer division or modulo when its base matches the left operand. This is +covered by an object-emission test and the now-emitting +`integer_nonzero_proofs.jett` fixture. +Fixed-width bitfield values, collection payload ownership and byte roundtrips +also pass a dedicated native/interpreter fixture, including network order, +native order, enum discriminants, trailing payload bytes, decode errors and +allocation cleanup. Dynamic field-width validation remains outside this slice. +An unconstrained empty `list[never]` has a native empty-list layout. Expected +result types now refine an argument-inferred `never` before specializing a +generic call, so `list_shape_helpers.jett` emits an object; a dedicated native +main executes the resulting nested-list ownership path. Other collection +shape conversions still need explicit ownership metadata conversion. +Machine values use owned tagged records. Checked constructors, transitions, +state tests and state-qualified field projection now emit native objects for +eight machine run-pass fixtures, including namespace branches and negative +narrowing. A dedicated native/interpreter fixture exercises transitions, +borrowed field access and three-state branch narrowing. A runtime guard checks +the tag before treating a bare machine local as a narrowed state. JSON machine +fixtures remain blocked by generic JSON operations. +The `uuid.new` intrinsic now shares UUID v4 formatting and OS entropy handling +between interpreter and native runtime. This adds the `string_chars.jett` +object gate; a dedicated native executable checks its version, variant, +separators, and length without comparing random identifiers across runs. +Named function values and indirect calls unlock `list_higher_order.jett` after +`list.sort_by` moves its callback evaluation into `.jett`. Indexed row sorting, +sortedness checks, and source-level `list.group_by` then unlock +`list_source_surface.jett`. Dedicated native/interpreter programs check stable +sorting, callback results, large unsigned values, floating-point and boolean +keys, missing indices, and grouping. +Capture-free inline functions now extract to ordinary checked functions and +use the same native function-address path. This adds object gates for +`generic_function_value_wrappers.jett`, `inline_functions.jett`, +`list_map_extra.jett`, and `map_advanced.jett`. A linked differential fixture +covers inline list callbacks, returned callbacks, zero-argument functions, +and nested capture-free functions. At that checkpoint, closures reading +enclosing locals still awaited native environments. +Checked `list.insert_at` and `list.remove_at` leaves transfer list and element +ownership on success, including owned bytes elements. An indirect callback +returning a string now receives a planned owning temporary. Together these add +`list_extras.jett` to the object gate; linked differential fixtures cover valid +insert/remove positions and string-returning `list.map`. Invalid-index failures +now preserve the signed index in the context-owned terminal message. Dedicated +native/interpreter fixtures compare the exact error text for insertion past the +end and removal at a negative index; runtime leaf tests cover both index signs +for both operations while checking that the list remains unchanged. + +Checked public JSON calls for supported structs, lists, string-keyed maps, +optionals, and results now instantiate the reflected `.jett` serializer after +the compiler-owned policy gate. The serializer selects type-kind and primitive +branches while checking each concrete instantiation. Borrowed containers are +cloned before consuming iteration. +Nested generic checks preserve the outer parameter types; HIR derives result +handler error bindings from the checked result type. A linked differential +fixture covers both public spellings, renamed and escaped fields, all supported +scalar categories, nested collections, optional/result branches, and cleanup. +Float32 fields, enums, bitfields, secrets, raw-tree fields, and other JSON +shapes still need this source path or another checked native lowering. +Bare and state-qualified machines whose state fields recursively use the +supported parser leaves now instantiate the reflected `.jett` decoder. A +linked native/interpreter fixture covers populated and empty states, exact +validation, a missing field, and a state-qualified mismatch. Empty reflected +field loops lower without a body. Supported bare and state-qualified machines +also instantiate the reflected source serializer; a linked fixture covers +renamed payload fields, nested machine collections, and public omission of +secret payload fields. Widening a state-qualified machine local to a bare +machine consumes that local; reuse requires an explicit clone. These changes +add `json_namespace_duplicate_machine_envelope.jett` and +`json_serialize_machine_envelope.jett` to the object gate. The exhaustive +Windows gate was 135/182 emitted objects. The checked decoder now accepts +supported `secret[T]` values and secret-bearing struct and machine fields. A +linked fixture checks direct secret parsing, closed-shape parsing, public +omission of record and machine secrets, and handled exact-parse errors. Native +MIR verification permits only the checked promotion from an inner local to its +`secret` wrapper. This adds `json_parse_machine_envelope.jett` and +`json_stdlib_bridge_delegation.jett` to the object gate. That exhaustive +Windows gate reached 137/182 emitted objects. A dedicated trusted `.jett` raw +parser now returns `secret[json.JsonTree]` for both public parse spellings; +`parse_exact` retains the raw tree's open-shape policy. The linked fixture +checks successful and malformed input, and +`json_parse_exact_secret_edges.jett` joins the object gate. The current +exhaustive Windows gate reached 138/182 emitted objects. Supported top-level +enums now use dedicated checked `.jett` parse and serialize hooks, keeping the +raw `json.JsonTree` wire path separate. The enum payload decoder keeps its +reflected construction inside the checked variant loop. A linked fixture +covers unit and payload variants, nested exact validation, unknown variants, +and public serialization. This adds `json_enum_shapes.jett`, +`json_result_shape_diagnostics.jett`, and +`reflection_type_id_duplicate_enum_payloads.jett` to the object gate. The +Windows gate reached 141/182 emitted objects. Checked source parsing and +serialization now accept refinements over supported bases. The reflected +decoder selects the refinement branch before its generic fallback, and native +HIR supplies the checked string type for refinement error bindings. A linked +interpreter/native fixture covers valid and rejected scalar values, exact +record parsing, and serialization. `json_refinement_exact_serialize_edges.jett` +adds one object; the Windows gate reached 142/182 emitted objects. Checked +source serialization now handles `bytes` through a trusted hex-string hook and +records containing raw `json.JsonTree` fields through an owned clone. A linked +interpreter/native fixture covers both public serialization spellings and raw +tree payloads. `json_serialize_public.jett` joins the object gate; the Windows +gate reached 143/182 emitted objects. Supported bitfields now serialize through +a checked source hook that dispatches unit-enum fields separately from ordinary +fields. A linked fixture covers enum-backed bits, wide integers, and list +payloads. `json_serialize.jett` joins the object gate; the Windows gate reached +144/182 emitted objects. Dedicated checked bitfield parse and exact-parse +hooks now decode ordinary fields and unit-enum annotations through reflected +construction. A bounded source decoder converts checked JSON integers to +`uint8` for bitfield payload lists and supported other JSON shapes. Linked +native/interpreter cases cover valid payloads, missing fields, enum shape +errors, range errors, and exact unknown-field errors. +`json_bitfield_shapes.jett`, `namespace_duplicate_leaf_types.jett`, and +`reflection_type_id_duplicate_named_owners.jett` join the object gate. The +current exhaustive Windows gate is 147/182 emitted objects, 23/30 main +outcomes, 25/25 runtime contracts, and 182/182 typed lowerings. Enum payloads +containing raw JSON, other narrow numeric shapes, refinement-validating struct +fields, and other JSON shapes remain. +Checked source JSON parsing now accepts raw `json.JsonTree` values nested in +supported containers and enum payloads. The raw decoder clones a borrowed tree +when returning an owned value. A mixed-payload enum can compare with a known +unit variant by tag without interpreting its aggregate payload. A linked +native/interpreter fixture covers raw enum payload parsing, exact validation, +serialization, and unit comparisons. `json_enum_bitfield_exact_edges.jett` and +`json_parse_collection_edges.jett` join the object gate. The current exhaustive +Windows checkpoint is 149/182 objects, 23/30 main outcomes, 25/25 runtime +contracts, and 182/182 typed lowerings. +Pipeline calls to `json.serialize` and `json.serialize_public` now select the +same checked source serializer as direct calls for supported structured types. +A linked native/interpreter fixture covers both forms, and `pipeline_into.jett` +joins the object gate. That exhaustive Windows checkpoint was 150/182 +objects, 23/30 main outcomes, 25/25 runtime contracts, and 182/182 typed +lowerings. +Supported records with refinement fields now use the checked source JSON +decoder. It validates each field as its exact refinement type before insertion +into the reflected native builder. Native verification rejects builder inputs +that could pass an unvalidated base value into a direct refinement field; +accepting those values requires predicate execution at builder finish. A linked +fixture covers valid and rejected JSON, exact unknown-field errors, and reflected +reconstruction. `json_parse.jett` and `json_parse_refinement_valid.jett` join +the object gate. That exhaustive Windows checkpoint was 152/182 objects, +23/30 main outcomes, 25/25 runtime contracts, and 182/182 typed lowerings. + +Verification-only empty objects do not count. Native +execution tests additionally assert computed output, not only process success. + +Terminal failures count only after behavior and cleanup match. ABI context +destruction now rejects unreleased string owners; launcher exit 71 requires +successful destruction, while leaks override it with exit 72. Dedicated launcher +tests exercise both paths and nested native-call tests exercise temporary/local +cleanup. This is evidence for the current scalar/string runtime, not a claim +that unsupported resource families already have finalizer/effect instrumentation. + +## Linux GNU executable harness + +The production linker also accepts exactly `x86_64-unknown-linux-gnu` when it +is the compiler host. `NativeLauncherBundle::linux_gnu_v1` specifies the Rust +static launcher archive, ABI v1, dynamic GNU CRT, and ordered system libraries. +`cc` (or the literal executable path in `JETT_NATIVE_CC`) links the Cranelift +object without a shell. Cross-target bundles are rejected before linking. +Windows MSVC retains its static CRT, SDK discovery, library order, and atomic +publication contract. A Windows MSVC host test now builds its matching launcher +archive, links native executables, and compares scalar entry, stdout, owned +bytes, and generic struct fixtures with interpreter output. This is a host-local +execution gate; the full parity denominators still require the exhaustive audit. + +`cargo test -p jett_driver --test native_execution` builds the target-matched +launcher archive and executes the scalar-entry fixture in an empty directory +with an empty environment and a deadline. The executable needs no compiler +source at runtime. This is one executable seed, not full native parity or a +clean-distribution packaging claim. All remaining fixture obligations remain. + +Explicit `comptime` values are materialized into typed HIR before MIR lowering, +retaining the checked type and span. The native property-case value materializer +also reconstructs supported composite values: lists, maps, sets, sums, structs, +bitfields, enums, machines, and bytes. The checker propagates contextual expected +types through `comptime`, so a closed `ok` or `none` can adopt the declared sum +type. Ordinary pure calls remain runtime calls. Missing or unsupported baked +values fail before codegen rather than executing their source computation. Native +regressions execute baked `math.factorial(5)` after removing its source file and +compare `tests/native/comptime_composites.jett` with the interpreter. Other +Runtime authority remains outside this materialization boundary. Checked named +functions resolve by exact namespace, name, and signature; inline functions +resolve by their source body span and signature. Evaluated closure captures are +materialized into typed caller-local bindings and copied through the ordinary +native closure environment path. `tests/native/comptime_function_values.jett` +and `tests/native/comptime_captured_functions.jett` compare direct, returned, +and list-contained function values, including captured strings, with the interpreter. + + +## Native runtime-value slice + +`active/native_value_abi.md` defines immutable context-associated string handles, +borrowed call inputs, owned results, terminal failure transport, and the separate +Stdout token. `jett_mir::copy_values::CopyValuePlan` computes definite initialization +and liveness over CFG backedges. Cranelift consumes those facts to release dead +locals, overwritten values, full-expression temporaries, and all frame owners on +return or terminal failure. Runtime allocation maps remove each string at its last +release; context destruction is a leak check, not a program-long value arena. + +Ordinary Jett calls, branches, loops, argument evaluation, and string interpolation +are emitted code. Typed runtime leaves implement string storage, formatting, +grapheme slicing/counting, selected Unicode operations, stdout, and numeric kernels. +Native `string.index_of` and `string.count` search only at grapheme boundaries; +the former returns an owned optional value and the latter counts non-overlapping +matches. Both share the native split scanner. The same interpreter differential +fixture is wired into the Linux GNU and Windows MSVC execution suites. +They consume no interpreter Value, AST, HIR, or source operation names. Exact checked +IntrinsicId and concrete numeric type arguments select the native leaf signature. + +MoveValuePlan now models linear bytes, selected sum payloads, list and user struct owners, with +call-bounded loans, active iteration loans and initialized owning slots. Full +ownership remains open for capabilities beyond Stdout, resources, other aggregate kinds, +closures and tasks. Nested-expression handlers and move-only iteration projections +remain pending. Unsupported types/forms remain rejected +rather than being made nominally supported by the copyable-string plan. CLI +packaging, other capability providers, and clean Windows MSVC execution also remain +release gates. + +Phase 4 additionally executes both existing generic-struct and explicit-equality +fixture bodies with supplemental mains. Namespace interface mains now match exact +stdout natively. Struct fields retain their checked layouts and implicit-view +semantics; Equatable comparisons carry exact checker-selected method identities +into compiled calls. One emitted object is tested with four real process inputs. +The full workspace checkpoint passed 1763 tests; complete remains false. Enum +payloads, refinement-validating construction and projected-owner escapes remain +continuation work. See `../../active/native_value_abi.md` for the precise supported boundary. + +The September 2026 actor slice gives spawn expressions a checked constructor +identity and message expressions a checked handler identity in HIR and MIR. +Native constructors allocate context-owned actor state; handlers receive the +actor as their hidden environment, evaluate ordered arguments, write mutable +state back, and return or discard responses with owned-value cleanup. Native +execution tests compare actor state mutation, string ownership, named arguments, +numeric contexts, and duplicate namespace names with the interpreter. The +earlier exhaustive Windows audit reported 174/182 emitted objects (95.6%), 27/30 +linked `main` outcomes, 25/25 runtime contracts, and 182/182 typed lowerings. +That percentage tracked fixture object coverage, not overall language coverage. +At that checkpoint, the eight remaining object failures were four Graphics +fixtures and four verify-only fixtures with no reachable code symbols; the +three remaining linked-main failures were scripted Graphics fixtures. + +The Graphics parity rows carry the same deterministic key and close events +used by interpreter graphics tests. The native launcher configures those +events before program entry, and the audit obtains its interpreter oracle from +the scripted provider. The runtime exposes typed Graphics session leaves with +scripted open/present/key/close lifecycle, domain failures, and context cleanup. +The compiler emits the private `graphics.__run` callback loop and tracks source +state ownership across updates. The three named-callback Graphics mains now +link and match interpreter output or terminal callback failure. + +The September 2026 exhaustive Windows object audit first reached 177/182 +emitted objects (97.3%), 30/30 linked `main` outcomes, 25/25 runtime +contracts, and 182/182 typed lowerings. Inline Graphics callbacks with `view` +parameters then raised object coverage to 178/182 (97.8%). Those percentages +measure the fixture set, not overall language coverage. + +Ordinary object emission now includes checked `verify` and `property` bodies as +native functions, so the four test-only fixtures emit actual body symbols +rather than empty objects. A property `given` is a typed function parameter; +generation and shrinking stay with the test runner. Native `assert` has a +terminal failure path, and test-body reads clone ordinary owned values to +preserve Jett's relaxed test-block ownership policy. The full 182-fixture +test-body audit now emits 182/182 objects (100%). The regular object gate +checks all 182 rows, with deterministic byte checks on representative objects. +The native verify runner now synthesizes a source-file suite entry that calls +only exact top-level `verify` bodies in declaration order. It runs the 155 +verify-bearing fixtures through the production launcher and linker. Extracted +inline callbacks remain reachable through their parent bodies, rather than +becoming independent zero-argument tests. The native property runner embeds +the existing generator's typed cases into a separate suite entry. All 18 +property bodies across three run-pass fixtures execute 100 native trials each; +terminal failures now identify the property and one-based trial number. A +backend-injected assertion failure tests the fourth trial of the second +property through linked debug and optimized objects, including cleanup of owned +inputs. The native suite driver now locates and replays failed cases, then +uses the existing shrink search with native execution for every candidate. +The scalar/list regression matches the interpreter counterexample in debug and +optimized modes after source removal. Broader failing-input combinations still +need the same semantic audit as the rest of native codegen. + +Direct bitfield constructors now route checked dynamic widths through the +existing reflected construction validator, preserving the interpreter's +result and error text. Primitive trace and breakpoint statements now assemble +one debug line from live native bindings. The differential +`tests/native/debug_primitives.jett` fixture covers every fixed-width integer +category, both float widths, booleans, UTF-8 strings, bytes, `nothing`, and +sorted multi-binding breakpoints. `tests/native/debug_aggregates.jett` covers +nested and empty collections, optional and result branches, structs, recursive +enums, machine states, and direct/decoded bitfields against interpreter output. +Custom assertion messages now evaluate on the failed branch and cross the +runtime boundary through a copy-out failure API, preserving the static v1 +result contract. `tests/native/assert_messages.jett` checks that a passing +assertion skips an otherwise failing message expression and compiles in native +verify and property suites; a failing message is exercised by backend, runtime, +and launcher tests. Special debug values and cross-function breakpoint binding +scope remain open. + +The pending-value parity pass now checks string text operations and +`bytes.length`/`bytes.concat` before their runtime kernels read a wrapped +receiver or argument. `string.join` also diagnoses a pending string inside an +otherwise plain list. Interpreter/native differential fixtures cover unary, +multi-argument, list, and bytes cases. This closes those identified behavior +gaps; the coarse overall language-coverage estimate remains 80% until the +remaining semantic matrix is audited. + +Encoding, CSV, and crypto trusted hooks now validate pending inputs against +the interpreter's operation-specific errors. CSV stringify also formats a +pending string field and rejects a pending row. Secret string and byte task +values now use the underlying runtime representation for `run` and `join`, +including contextual promotion of a `run` byte value to `secret[bytes]`. +Differential fixtures cover those paths; the coarse coverage estimate stays +at 80% while broader semantic gaps remain. + +Capability intrinsic parity now distinguishes the interpreter's current +pending-value rules: Clock, Random, and Environment operations reject a +pending authority; Environment lookup rejects a pending key; `Stdout.write` +accepts a pending authority and renders pending text. Linked differential +fixtures cover each case. The coarse overall estimate remains 80% while +unresolved language and runtime gaps remain. + +Secret comparison and bitfield byte conversion now reject pending operands +with interpreter-matching diagnostics. Bitfield payload encoding detects a +pending `list[uint8]` container or element before writing bytes, while +`secret.redact` retains its unconditional redaction behavior. Linked +differential fixtures cover these cases. The coarse estimate remains 80%. + +As of 2026-09-27, the working estimate is **about 85%**. The pending-value +passes above closed identified gaps in strings, bytes, encoding, CSV, crypto, +capabilities, secrets, bitfields, and actor state. This is a five-point planning +step, not a fixture-derived percentage. Projected views and generic collection +shapes, nested handlers, aggregate reflection, runtime providers and +scheduling, concrete JSON shapes, and special-value diagnostics remain +incomplete. + +Graphics callback state now carries primitive pending depth from the initial +value through each update result and render call. Scripted interpreter/native +fixtures cover nested pending `int64`, `bool`, and `float64` state; the coverage +estimate remains 85% while the broader gaps above remain. + +Reflected `type.field_value`, `type.variant_field_value`, and +`type.machine_field_value` now restore primitive pending depth from the selected +field after metadata validation. Linked differential fixtures cover nested +integer, boolean, and floating-point struct fields plus enum and machine +payloads. Other aggregate reflection and mismatch diagnostics remain open. + +Linked reflected-read coverage now also includes pending owned strings, lists, +maps, sets, optionals, and results in struct fields, enum payloads, and machine +state fields. The fixtures check each copied value's debug shape and +successful `join` after reflection; other aggregate shapes and mismatch +diagnostics remain open. + +MIR handler extraction now keeps a noncopyable view of an immutable local at +the call site while hoisting the later handled argument into control flow. +This permits a `view Stdout` before a handled optional in direct calls, +indirect calls, and the `Stdout.write` intrinsic without copying capability +authority. A linked fixture compares all three forms with the interpreter. + +Native `type.arg` now checks pending, negative, and out-of-range indexes with +interpreter-matching errors before selecting checked `TypeInfo` metadata. +Differential fixtures include nested pending depth and alias-typed owners; +other reflected-metadata mismatch diagnostics remain open. + +Pending `int8`, `int16`, `int32`, `uint8`, `uint16`, and `uint32` now have +linked differential coverage through nested tasks, joins, traces, and aggregate +fields. Explicitly handled refinement conversions also preserve predicate +evaluation errors and reject pending boolean predicate results with the +interpreter's message. Direct struct construction and reflected struct, enum, +and machine builder finish paths now share that error handling. Differential +fixtures cover pending predicate results in all four shapes and a predicate +evaluation failure during reflected struct finish. Other refinement boundaries +still need coverage. The current broad planning estimate remains about 85%; +these fixtures do not establish a new five-point step. + +Reflected struct, enum, and machine field reads now report the interpreter's +specific owner-mismatch error when the metadata index selects a checked +candidate field but the `TypeField` belongs to another type, variant, or state. +Linked differential fixtures cover all three owner forms; invalid-index and +field-name diagnostics still need parity work. + +Reflected field reads now validate a checked field candidate before checking +whether its type can satisfy the requested result type. This preserves owner +error precedence and gives interpreter-matching requested-type errors for +struct, enum, and machine fields. A foreign owner with an incompatible +requested type is covered by the same differential gate. + +Native reflected reads now check the `TypeField` owner before searching for a +candidate field. Differential fixtures cover foreign indexes beyond the active +struct, enum variant, or machine state's fields, plus empty active variants and +states. The interpreter's owner error is retained even when no candidate +exists; forged metadata and other invalid-index diagnostics remain open. + +`tests/native/json_nested_machine_parse.jett` adds linked differential +coverage for exact JSON parsing of bare machines in lists and string-keyed +maps, state-qualified machines in records and lists, and nested missing-field, +unknown-state, extra-field, and state-mismatch errors. All match the +interpreter; other concrete JSON shapes remain open, so the broad estimate +stays at about 85%. + +Native value validation and ownership analysis now treat a contextually +inferred `never` arm as uninhabited rather than an unsupported runtime type. +Linked fixtures cover pending `some`, `none`, `ok`, and `fail` values carried +through reflected struct, enum, and machine fields, plus pending empty lists +and maps embedded in a struct. The interpreter and native paths agree on +debug output, joins, and cleanup. Other semantic gaps keep the broad estimate +at about 85%. + +Pending reflection metadata now follows the interpreter at native use sites. +`TypeInfo` and `TypeField` survive `run`, `join`, trace, and subsequent valid +reflection. Unjoined `TypeField` values fail with the interpreter's diagnostic +for struct, enum, and machine field reads and `type.construct_put`; +unjoined `TypeVariant` and `TypeMachineState` values return the corresponding +builder-start error. Linked differential fixtures exercise all of these paths. + +Native reflected variant/state selection and struct, bitfield, enum, and machine +field reads now reject pending source values with the interpreter's full value +diagnostic when their checked debug layout is available. Secret-bearing pending +owners still use a conservative native error pending the secret-debug policy. +Field-read validation preserves metadata-before-value precedence; +for enum and machine payloads, a pending source value precedes a foreign field +owner error. Differential fixtures include state-qualified machines, foreign +fields, and both pending metadata and a pending source value. + +Linked JSON differential probes now cover nested secret wrappers in lists, +maps, results, and optionals, including an exact-parse error inside a secret +list. A recursive enum also roundtrips through serialize and parse. These +cases match the interpreter and expand evidence for concrete JSON shapes; +they do not close the remaining JSON row or change the broad 85% estimate. + +Native string interpolation now renders pending scalar operands with their +nested `pending(...)` wrappers, matching the interpreter for representative +signed, unsigned, boolean, and floating-point values. Primitive JSON +serialization preserves that text for numeric and boolean inputs; a checked +source helper also handles nonliteral `nothing` without dropping argument +evaluation. Pending JSON parse and string-serialization inputs retain the +interpreter's errors. Linked differential fixtures cover the successful and +failing paths. Other intrinsic operands and JSON shapes remain open, so the +broad estimate remains about 85%. + +Cloneable local results and optionals now stay available after a native +`handle` extraction. MIR snapshots a local sum before reading its tag and +taking a payload. Linked differential cases reuse empty and present optionals, +results, owned list payloads, and a view parameter. They also cover direct and +indirect calls that stage a struct-field collection view before a later +handler rebinds the parent, plus nested projected map/list iteration. Other +handler and collection shapes remain open; the coarse estimate remains 85%. + +Native `run` now snapshots cloneable local aggregate inputs before creating +pending task values. The interpreter leaves the original local readable; +native code previously moved it and rejected a later trace. Differential +fixtures cover optional, result, list, map, and struct locals, comparing both +original and pending debug output. Repeated handles on projected optional and +result struct fields also match the interpreter. Other noncloneable task inputs +and unexamined control-flow shapes remain open at the coarse 85% estimate. + +`TypeConstruction` builders are now included among cloneable MIR snapshots. +The interpreter leaves a local builder readable after `run`, while native +lowering previously moved it. Linked differential coverage checks the original +and pending builder, repeated handles on an optional builder, and a borrowed +builder call staged before a later handler rebinds its caller local. Other +noncloneable runtime values and task semantics remain open at about 85%. + +Native `coarsen` and `declassify` now snapshot cloneable owned locals before +transferring the transparent representation. Differential fixtures reuse a +refined list after `coarsen` and declassify a secret list twice, matching the +interpreter's local-read behavior. Noncloneable conversion inputs and the +matrix gaps above remain open; the broad estimate stays about 85%. + +Linked native/interpreter coverage now exercises an enum payload containing a +user struct with an owned list field: construction, borrowed match, clone, +trace, list storage, and cleanup agree. Equality for user-struct payloads now +invokes exact explicit methods, including through containers, generic owners, +refinements, and machines. Linked debug/release and interpreter/comptime coverage +is recorded in `docs/completed/enum_payload_struct_equality.md`. This closes the +nested-struct equality gate without claiming completion of the full native audit. + +Native refinement validation now snapshots a cloneable owned local before +testing its predicate, leaving the source readable after success and inside +the handled failure branch. Linked differential fixtures cover a list +refinement with both a nonempty and an empty source, plus a nested optional +handler that supplies a refinement candidate. Other unexamined refinement +sources and expression shapes remain open at about 85%. + +Resolver facts now retain a bare source-function call when its name also +spells a standard module prefix. Linked native/interpreter coverage calls +same-namespace `validate` and `math` functions alongside `math.abs`, checking +that native HIR selects the source functions for bare calls and the standard +module for the dotted call. The broad estimate remains about 85%. + +Linked differential failures now cover pending lower and upper arguments to +public `random.int64`. Its source wrapper compares the bounds before reaching +the private bounded-random intrinsic; native and interpreter both reject that +comparison with the same pending-value diagnostic. Pending capability inputs +were already covered separately. The broad estimate remains about 85%. + +Repeated `join` of copyable local string and `int64` task values now has linked +native/interpreter coverage, including a later trace of the still-pending +local. The checker rejects repeating `join` on an owned list task after its +first consumption, so the test stays within the language's ownership rule. +This narrows task-value coverage without changing the broad estimate. + +Native entry grants now also cover opaque Stderr, Stdin, Filesystem, Network, +Process, and Log values. These capability families have no interpreter I/O +provider, but their accepted value behavior remains part of parity. The +portable conformance suite compares moves, explicit views, indirect calls, +nested task wrapping/joins, trace output, and plain-capability join failure for +all six families. Runtime tests require distinct authority across kinds and +contexts. Implementing their provider effects remains separate work. + + +### Recursive secret observations and declared debug types + +Trace and one-line breakpoint output now use the decided `[redacted]` marker +through secret aliases/refinements and nested collections, records, recursive +enums, machines, and all reflected builder kinds. Pending secret payload depth +is hidden. Typed reflection failures use the same redaction. Native layouts do +not inspect hidden payload handles, and redacted layouts cannot be used for +equality. Public text equal to the marker retains ordinary pending formatting. +Declared debug labels survive canonical alias interning, concrete generic +specialization, reflected body expansions, and closure capture lowering. +Interpreter loop, match, and handler bindings now carry type metadata; generic +handler/pipeline failures use active invocation types. Binding initialization +precedes visibility in a breakpoint. Linked differential coverage exercises +these observations plus seven pending-owner/builder failure paths. + +Consumed bindings are omitted from breakpoint snapshots through a shared +compiler-owned exclusion map. Differential coverage includes local transfers, +call arguments, loop elements, handled results, branch joins, mutable +reinitialization, copyable aliases, and actor handlers. Actor snapshots now +exclude caller locals and restore the caller frame after handler completion or +failure. This closes the consumed-binding observation decision; it does not +establish the remaining full-release gates. + + +### Native release policy + +`build_host_executable_with_options` and its program-object counterpart now +apply the same release checker policy as frontend builds. HIR discards complete +trace/breakpoint observations before MIR lowering, including breakpoint condition +effects, while retaining validation of their checked source. Cranelift uses its +speed optimization setting for release objects. The linked release differential +fixture proves debug-condition output is absent, application capability output +remains, debug builds match the interpreter, and forbidden debug printing cannot +publish a release artifact. Runtime bundle selection and CLI packaging are +implemented below; clean supported-host verification remains a release gate. + +### Packaged CLI artifact layout + +The native CLI uses an explicit versioned launcher manifest, either supplied +with `--runtime-bundle` (or `JETT_NATIVE_RUNTIME_BUNDLE`) or installed at +`lib/jett/runtime///launcher.json` beside the package's +`bin/jett`. Each manifest records the compiler package version, native ABI, +target, profile, CRT mode, archive filename, and ordered native link libraries. +The loader validates those inputs against this compiler before linking. + +A package also carries `lib/jett/stdlib`; discovery prefers an explicit +`JETT_STDLIB_DIR`, then the installed sibling directory, then the development +source tree. Native `build` publishes `target///` by default +(with `.exe` on Windows), with `-o` for an explicit output. `build --check` +retains frontend validation without requiring a launcher or entry point. +Packaging builds both runtime profiles and keeps compilation outside the CLI; +the installed compiler never starts Cargo to obtain its runtime. + + +Local Linux package smoke checks pass from an unrelated working directory: +installed-stdlib discovery, linked debug/release output after source removal, +agent artifact reporting, default output paths, target/profile rejection, and +preservation of an existing artifact after failed builds. The packaging tool +includes dependency notices and a standalone smoke script for clean consumers. +These local results do not substitute for the separate clean Windows/Linux jobs. + + +### Native property shrinking implementation + +The driver retains the deterministic cases and their exact checked function +identities in the same lowering session. After a full native suite fails with +entry status 71 and successful cleanup, it can identify the earliest failing +case by compiling and running shorter prefixes, then confirm that case alone. +This avoids parsing human-readable trace or failure output as a control protocol. +Each replay and shrink candidate runs in a new process and runtime context. +The existing ordered, 50-step shrink search is shared with the interpreter; +only the failure predicate differs. Compiler, linker, process, timeout, and +cleanup failures abort shrinking rather than becoming candidate failures. +Replay recompiles only compiler-owned input constructions against the retained +checked HIR, with no source rechecking or runtime interpreter fallback. This +initial implementation favors an exact existing contract over caching candidate +objects. Normal source validation remains mandatory. A failure that cannot be +reproduced in isolation must be reported, never converted to a passing suite. + + +### Shrink candidate integer ranges + +A generated input and every accepted shrink candidate must remain representable +by the declared `given` type. The audit found that an `int8` minimum input could +shrink to 128: the interpreter treated the resulting range error as another +property failure, while native replay correctly rejected the invalid literal. +Narrow integer pools now retain concrete `Value::Typed` identity +through containers and record fields. The shared shrinker preserves that +identity and filters candidates against its primitive range. Candidate ordering +and the 50-step bound stay unchanged. Plain `int64` and `uint64` carriers already +encode their ranges. The refinement handling below extends this invariant to +user predicates. + + +### Refinement-preserving shrink candidates + +Property generation must retain the canonical refinement identity after filtering +its initial pool. Shrinking may simplify that payload, but an invalid predicate +result is not a property failure. The interpreter validates retained tags +recursively before binding a candidate. Native candidate replay constructs +checked refinement handlers that call the already-lowered predicate functions; +a rejected candidate returns successfully without entering the property body. +Original generated trials and prefix replay keep their existing trusted input +path, so validation does not add observable predicate calls to normal suites. +Predicate chains use declaration identities and the existing handler ordering, +including refinements inside aggregates. No property body or native predicate +falls back to the interpreter. + +### Binary32 property input fidelity + +The shared property pool previously stored `3.14` as a binary64 value even for +`float32` givens. Execution rounded it at a typed boundary, but counterexample +formatting reported the original carrier instead of the executed input. +Generated binary32 values now retain their primitive identity and rounded +payload. Shrinking preserves both, including when halving a subnormal requires +another binary32 rounding. The binary64 pool and candidate order remain intact. +The native regression compares the fourth-trial failure and exact rounded +counterexample in debug and optimized modes after removing the source. + +### Generated generic record identity + +Property-generated generic records must carry the same instantiated owner as +source-constructed records. The former pool retained only the base display name: +a `list[Boxed[int8]]` passed as `list[Named]` could not dispatch `Named.name`, even +though a source-constructed list passed. Generation now retains canonical generic +arguments, and field shrinking preserves that identity. The regression covers +two implementations, a transparent argument alias, list erasure, and shrinking +that keeps two simplified records as the smallest failure. Debug and optimized +native replay must match the interpreter after the source is removed. + +### Floating remainder + +The rejection-path audit found that the checker and interpreter accept floating +`modulo`, but native binary validation admitted only integer operands. Native +validation and emission now cover both float widths through a typed binary64 +remainder leaf, with explicit binary32 widening and result rounding. This uses +the established interpreter operation, including signed zero, NaN, infinity, +and zero-divisor behavior. Pending operands still fail before arithmetic and +follow ordinary owned-value cleanup. Linked tests compare debug and optimized +execution, generic calls, explicit comptime results, and pending tasks. + +### Public aggregate debug printing + +`print` and `println` previously accepted aggregate arguments in the frontend +but failed native signature validation. Native printing now uses the existing +typed debug formatter for public containers, records, enums, machines, bitfields, +function values, actors, capabilities, builders, and erased values. The shared +ownership rule preserves explicit views while consuming ordinary arguments. +Empty literal containers and absent sums have an explicit uninhabited layout +child. Differential tests cover five debug fixture families, argument evaluation +order, borrowed values across handlers, and cleanup without partial output after +a later argument fails. Release rejection remains unchanged. Hidden-secret +printing is a separate unresolved policy, recorded in +`../../open_design/debug_print_hidden_secrets.md`. + +### Direct collection comparison audit + +Direct equality of bytes, lists, maps, sets, optionals, and results previously +reached runtime errors in the interpreter and internal build errors in native +code. The user selected compile-time rejection on 2026-09-30. Both operators +now report E0376 before evaluation or lowering, including aliases, refinements, +secrets, generic instantiations, and explicit comptime expressions. Native +debug/release builds preserve existing artifacts on rejection. The settled +contract is recorded in +[direct collection equality](../direct_collection_equality.md). + +### Deep recursive debug values + +A finite 160-level enum printed in the interpreter but failed native formatting +with `invalid native trace label`. Typed native formatting now uses an explicit +work stack and active-path cycle detection instead of its 128-level recursion +limit. The linked regression covers pending recursive values, erased owners, +repeated children, trace, print, and secret-bearing breakpoint snapshots. +Runtime tests cover 4,096 levels and malformed cyclic schemas/payloads without +recursive traversal or partial output. Existing aggregate equality remains a +separate audit obligation. + +### Deep supported enum equality + +Comparing a 160-level recursive enum with itself returned `true` in the +interpreter and an invalid-struct runtime error natively. The existing supported +aggregate equality path now uses an explicit stack with active-path cycle +checks. Child comparisons retain their order and stop at the first unequal +payload, including before malformed later fields. Linked debug/release tests +cover deep values, unequal leaves and lengths, pending nodes, NaN, and signed +zero after source removal. Runtime tests cover 4,096 levels and malformed cycles. +This does not add equality for user structs, actors, interfaces, or direct +collection operands; their recorded policy decisions remain open. + +### Deep reflected type identities + +A scoped reflected binding for a field with 70 nested list arguments succeeded +in the interpreter and failed native dispatch with invalid `TypeInfo`. Runtime +identity construction now uses an explicit stack instead of a 64-level recursion +limit, retaining the existing identity encoding and alias preservation/erasure +modes. Linked debug/release tests cover both direct and aliased deep fields; +runtime tests cover 4,096 alias levels, shared subtrees, cycles, and malformed +arguments. This changes no language binding or specialization rule. + +### Deep interface conversion descriptors + +An empty list with 130 nested concrete element types converted successfully to +the corresponding interface types in the interpreter but failed native descriptor +parsing. Parsing now uses an explicit parent stack instead of a 128-level cutoff, +with the same encoding and validation. Linked debug/release tests cover the +accepted empty-container source; runtime tests cover populated deep conversions, +the boxed leaf identity, allocation-failure rollback, truncation, and extra bytes. + +### Borrowed transparent values and small refinement owners + +The saved transparent-borrow fix now has linked debug/release gates. Refined +`nothing`, `bool`, and `bytes` values retain their interface owners at runtime +and comptime, including inherited unit refinements and nested pending joins. +Coarsening and declassification preserve views through direct and indirect +calls, projected fields, repeated reads, and later-argument handlers. Both +profiles execute after deleting source. Companion failures retain the exact +interpreter error and exit 71 after cleanup when a later argument fails. This +closes the unfinished fixture verification from the saved commit; the open +semantic decisions and supported-host release gates remain in the acceptance +audit. + +### Native suite build modes + +Verify/property suite lowering, object emission, and executable APIs now accept +explicit `BuildOptions`; their existing wrappers retain default debug behavior. +Release options apply source policy, remove trace/breakpoint execution, and +enable optimized codegen. Three focused regressions pass in both runtime +profiles after source removal, including 100 property trials, default-object +compatibility, optimized-object comparison, manifest profile rejection, and +E0362 publication rollback. Supplemental native conformance now has 308 tests. +The fixed inventory denominator and broad 85% estimate are unchanged. + +Before this API change, the frozen `b733296a` workspace (compiled source +`cca40a2e`) passed the complete locked workspace run and doc-tests, including +all 305 then-current native conformance tests and every inventory gate. Later +source changes and supported-host distribution retain their independent gates. + +### Deep type syntax recognition + +Generic calls and typed local declarations no longer stop recognizing valid +types after fixed 20/40/60-token lookahead budgets. Balanced delimiters are +scanned within the actual logical line, and callable return prefixes are +iterative. Five focused regressions cover complete direct/pipeline requested +types, callable and local type trees, non-consuming bounded recognition, and +malformed-input recovery. All 92 parser tests and 598 frontend fixtures pass. +This preserves the existing grammar and does not claim completion of recursive +reflected-field execution or change the broad 85% estimate. + +### Recursive builtin reflected producers + +Already admitted list, set, map, optional, and result requests now carry recursive +typed producer plans from HIR through MIR. Structural preflight over every +occupied child completes before new predicates; child checks precede enclosing +root checks, and map keys precede values. Declared exact/ancestor proofs and +shared root prefixes skip established checks. Changed wrappers still require +readiness when their children reuse proof, without joining pending values. +Private projections preserve the candidate and original source through success, +failure, and cleanup. + +Generic nominal argument facts come from canonical checker TypeIds, including +unused parameters and aliases. Ordinary nominal payload fields are excluded +from requested-refinement detection, and backend validation visits unused args. +All ten new linked groups pass across 96 cases, every getter/direct/pipeline +form, both runtime profiles, source deletion, terminal errors, and compiled +verify/property suites with 100 trials. Library, object, runtime, and frontend +checks pass; supplemental conformance now contains 318 tests. The fixed inventory +and broad 85% estimate are unchanged. Broader reflected admission, open semantic +rules, and final workspace/distribution acceptance retain their separate gates. + +### Selected debug-event isolation + +Print/println now join trace and breakpoint in a typed, ordered compiler debug +capture. Application stdout remains capability-backed; native debug transport +uses exact stderr bytes, including partial print fragments and terminal-error +adjacency. Driver and CLI retain actual comptime, frontend verification, and +runtime observations separately, escape agent rows, and preserve preceding +events on failure. Comptime baked values contain no events, and property +shrinking does not duplicate original observations. + +Recovery tracks attempted comptime span/context pairs without caching failed +values. Native replay failures preserve the completed original suite's streams +separately from private replay output; initial process errors retain their +existing typed contract. + +Release E0362 and current value-formatting/secrecy restrictions remain unchanged. +The independent hidden-secret print choice is still open. Its dedicated phase, +transport, and protocol gates now pass: 416 comptime, 83 driver library, 144 +runtime, 50 CLI, and five new linked native transport tests. This closes the +selected channel implementation obligation. The conformance suite now contains +323 tests. The frozen `b1cf8943` complete workspace and doc-tests passed, +including all 182 object obligations and 598 frontend fixtures; the log is +`target/native-workspace-b1cf8943.log`. Its supported-host workflow +37002829148 passed all four Linux/Windows build/package and installed-package +jobs, including clean relocation, source removal, and both profiles. +The prior `3bc23e6a` revision +passed all four supported-host jobs in workflow run 36995400983. Later source +changes retain their own workspace and supported-host acceptance gates. +The fixed inventory denominator and broad 85% estimate are unchanged. + +### Qualified machine JSON and parser pipelines + +The checker now selects native JSON source bodies from only the chosen payload +of a state-qualified machine. A bare machine still requires every state's +payload to be JSON-compatible, and the invalid selected integer-map payload +retains public E0343 refusal. All-state metadata and qualified construction +checks remain unchanged. This repairs a concrete eligibility defect where an unrelated +integer-keyed map state blocked valid ready-state requests. + +Nested `state.fields` loops over the complete `type.machine_states[T]()` +metadata now check candidates for all declared fields and their source aliases +when the request uses a canonical machine or state-qualified type. Field-level +alias identities are preserved; top-level aggregate aliases remain +non-matching probes with empty metadata. +Direct active-state field loops remain selected. JSON eligibility and complete +reflected iteration therefore use separate, existing payload boundaries. + +The completed candidate set exposed the private map decoder's incompatible +string-map return for a non-string-keyed T. The selected source repair rejects +unsupported key types in a closed, alias-aware branch before checking that +construction body. Public non-string map requests remain E0343 errors; valid +string-keyed maps are unchanged. Malformed other-state payloads can now report +the private unsupported-key error before a final qualified-builder mismatch. +Required-field and exact-validation precedence, full metadata, and the builder +lifecycle remain unchanged. The bounded tradeoff is captured in the machine +JSON contract before guard integration. Fresh reference characterization +records the unsupported-key error with the `cached.entries` prefix, while +exact numeric payloads retain the earlier expected-object validation error. +Expanded literal controls pin these messages and missing-field precedence +within the earlier passing 22-test linked gate. The later inactive-payload +decoder group also passes separately. The final-source JSON family gate +passes all 39 tests, including the 23 new groups; complete workspace/platform +acceptance remains pending. + +The private fallback raw decoder now uses the visible canonical condition +`type.name[T]() == "json.JsonTree"` to select its typed copying body. +Existing checked static selection keeps that body out of inactive callable, +interface, and actor specializations. An ordinary pure metadata helper does +not authorize branch skipping through folding; source validity stays +independent of optimization. Public JSON admission and wrapper paths remain +unchanged. + +`json.parse` and `json.parse_exact` pipeline steps also record a checked source +facade from their result payload through the existing direct-call handoff +mechanism. Fresh primitive pipeline witnesses establish that the missing +handoff was an independent defect. Ordinary JSON policy, argument evaluation, +ownership, and step-local handling are unchanged. + +The qualified-JSON repair suite has 23 tests over 32 Jett fixtures, covering +direct/pipeline operations, qualified list aliases, handled state/extra-field +errors, input preservation, both profiles, source removal, publication policy, +pure comptime/verify/property execution, exact string-key aliases, and an +independent all-state reflection control with canonical ready/empty qualifiers. +All 22 earlier groups passed (323 filtered) in +`target/native-json-qualified-driver-final.log` before the final fallback +guard. The additional group over three inactive callable/interface/actor +fixtures passes all four direct JSON APIs for the selected ready payload in +both runtime profiles after source removal (one pass, 345 filtered) in +`target/native-json-qualified-unselected-payloads-final.log`. The compiled +native corpus at that checkpoint contains 346 tests. These separate focused +runs do not establish 346 executed passes. The final-source JSON family gate passes all +39 tests (307 filtered), including all 23 new groups and 16 existing JSON +regressions, in `target/native-json-qualified-json-family-final.log` +(438.34 seconds). + +Recorded phase checks pass: 40 codegen, 416 comptime, 125 HIR, 79 MIR, and +221 typechecker tests. The driver batch's 82 passes plus one temporary capture +PermissionDenied failure are followed by a passing isolated retry of that +exact test; this is distinct from a clean full-batch result. All 598 frontend +fixtures pass on the repaired source in `target/native-json-qualified-frontend-fixtures.log` +(20.66 seconds). The frozen `23ac34c4` complete workspace subsequently passes +in `target/native-workspace-23ac34c4.log`, including all 346 native tests, +182 object obligations, 598 frontend fixtures, all 83 driver library tests, +and doc-tests, with unchanged head and worktree. Workflow 37013565734 has +passed its Windows build/package job; Linux and clean installed-package +acceptance remain pending. Later test additions retain their own acceptance +gates. The JSON row retains its other concrete-shape obligations; +the broad estimate remains about +85%, and the fixed inventory remains 207 fixtures with 182 object obligations. + +### Qualified machine JSON wrapper coverage + +Twenty-four additional native tests cross optional, result, and string-keyed map +wrappers with all four public JSON APIs and direct/pipeline forms. They select +the valid ready payload while another state contains a non-string-keyed map. +Occupied/absent optionals, both repeated-type result arguments and arms, +populated/empty maps, and retained owners are covered. Three additional empty +or inactive bare-machine targets retain compile-time E0343 and preserve output +publication in both profiles. + +All 27 fixture preflights pass in +`target/native-json-qualified-wrapper-preflight/results.json`. All 24 new linked +tests pass (346 filtered, 190.12 seconds) in +`target/native-json-qualified-wrapper-linked.log`, comparing exact reference +output in both matching runtime profiles after source deletion. The extended +policy group passes (one test, 369 filtered, 7.93 seconds) in +`target/native-json-qualified-wrapper-policy.log`. Source and Rust formatting +checks pass. No production code or public admission rule changes in this slice. + +The supplemental corpus now registers 370 tests. This is not a complete frozen +370-test workspace or supported-host acceptance result; the accepted local +workspace checkpoint above contains 346. Wrong-state wrapper diagnostics, +deeper combinations, aliases, secrets, pending values, and independent language +contracts remain open audit work. The broad 85% estimate and 207-fixture +inventory denominator remain unchanged. + +### Stable ordinary-struct field local views + +Immutable local/view-parameter field aliases now preserve the checked immediate +binding origin. Their existing typed initializer proves each ordinary-struct +owner, field, endpoint, and source-local identity before native compaction. +Owner liveness and non-owning cleanup storage remain unchanged. Conversion +proofs preserve nominal refinements, allowing only existing explicit ancestors, +exact secret declassification, and harmless outer secret promotion. + +At the ordinary-struct checkpoint, all 896 compiler-phase library tests and +12 local-alias driver tests passed on its integrated source. The latter include +eight new linked cases with +matching debug/release archives, source removal, independent clones, and later +argument-failure cleanup; the corpus registers 378 tests. Exact logs and scope +are in [the local-view note](native_local_view_aliases.md). These focused gates +do not replace complete frozen workspace and supported-host acceptance. + +A qualified-machine field alias passed frontend/reference execution but failed +the native origin proof before the exact-state follow-up below. Mutable/temporary +roots and unresolved lifetime decisions also remain separate obligations. The +broad estimate stays about 85%; the fixed inventory remains 207 fixtures. + +### Exact qualified-machine field local views: focused acceptance + +Exact declared MachineState field steps now preserve checked immediate origin, +state/source identity, endpoint and nominal qualification through the existing +initializer tree. Persistent loans, non-owning slots, pending checks and tag +offsets remain the existing MIR/emitter contract. Source policy and ABI do not +change; bare flow-narrowed/mutable/temporary origins and owner changes remain +outside bounded support. + +Current focused library gates pass 261 tests (41 codegen, 134 HIR, 86 MIR), +and the driver/frontend gate passes 83 library tests and 598 fixtures. The +local-alias gate passes 25 tests (366 filtered, 17.72 seconds), with one +separate typed-endpoint pass (391 filtered, 2.80 seconds): 26 unique executed +cases, not a combined batch. Linked cases execute in both profiles after +source removal; rejection controls preserve publication before archive lookup. +Exact pending owner/intermediate errors and endpoint clone/two-join traces, +owner-change publication refusal, generic copy/borrow facts and nominal/secret +field identities are pinned. Exact logs and initial source-only preflight +distinctions are recorded in +[the local-view note](native_local_view_aliases.md#exact-state-qualified-machine-field-follow-up). + +The accepted a0bf12df workspace and all four supported-host jobs are recorded +in [the acceptance audit](native_acceptance_audit.md#exact-qualified-machine-field-local-view-focused-acceptance). +The `774b13e7` complete frozen workspace passes, including all 392 native +tests and 182 object obligations; all four supported-host jobs also pass. +Exact logs and acceptance boundaries are in the audit. This accepted +local revision predates the inferred JSON repair below. + +At that machine checkpoint the corpus registered 392 tests. The estimate +remains about 85%; broader view/endpoint shapes, open semantics and independent language rows remain +audit work toward the unchanged whole-language objective. + +### Inferred uninhabited JSON serializer slots: focused acceptance + +Public JSON policy and actual reference execution admit inferred empty lists, +absent optionals, one-sided results, string-keyed empty maps, and nested +record/collection carriers. The characterized native selector omitted their +Never slot and retained an unsupported generic intrinsic. The applied repair +selects the existing checked source serializer without changing inference, +public type policy, or runtime representation. Parse eligibility remains its +own characterization and implementation obligation. + +Actual pre-change captures, the pending-carrier failure boundary, and the +executed linked/compiler gates are in +[the scoped contract](native_json_uninhabited_slots.md). Source-body checking, +carrier evaluation and cleanup cannot be replaced by optimizer folding or an +empty-value exemption for unsupported inhabited targets. These evidence and +implementation states do not raise the broad estimate or change the fixed +inventory; the full 100% language objective remains intact. + +The further actual pure/comptime/reference-suite and public-policy controls are +recorded in that note along with the separate projected-owner reread witness. +All 14 new driver tests pass together, covering the original carriers, pure +native suites, public projection, grouped negative publication controls and +owner rereads. The crossed compiler gate passes 948 library tests, followed +by 83 driver library tests, all 598 frontend fixtures and 17 Jett format checks. +The serializer revision `05494ed4` passes its complete 406-test frozen native +corpus, 182 object obligations, remaining workspace/doc-tests and all four +supported-host jobs. Exact logs and counts are centralized in +[the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). + +### Inferred uninhabited JSON parser slots: separate selected repair + +The existing checked decoder returns an error when raw input tries to populate +an inferred Never slot; empty/absent carriers and supported one-sided result +arms still decode. The bounded source-selector repair preserves that slot and +retains the error-producing result[Never, string] specialization, ordinary raw +parse/exact validation and field/index/arm prefixes. It changes no public JSON +domain, inferred default, body-validity rule, runtime representation or ABI. + +Actual pre-change source characterization covers eight direct and eight pipeline +sources across four inferred shapes and both parse APIs, plus three recursive +carrier sources with their actual field/key/index failure prefixes. All 19 +linked cases now pass together in both profiles after source removal; the +crossed compiler gate passes 949 tests, followed by 83 driver library tests, +all 598 frontend fixtures and all 19 new source format checks. The corpus +registers 425 supplemental tests, separate from this executed focused gate. +The parser revision 08f9f7e7 subsequently passes its complete frozen +425-test workspace and all 182 object obligations. Its supported-host workflow +also passes all four jobs. Exact accepted-head evidence is +centralized in [the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). The scoped +evidence and acceptance requirements are in +[the parser contract](native_json_uninhabited_parse_slots.md). This follow-up +does not increase the broad about-85% estimate, change the fixed 207 inventory, +or narrow the whole-language 100% objective. + + +### Bitfield field local views: bounded follow-up + +The existing readonly field rule now has a declaration-backed Bitfield step in +the native initializer proof. Exact field identity/type, source/owner anchoring, +qualification, immutable loans and non-owning cleanup remain unchanged. Existing +numeric copies, owned field copies, explicit clones and pending checks retain +their behavior; no new ABI, payload shape or lifetime rule is introduced. + +Both focused HIR tests and all seven new linked cases pass. The latter execute +both matching runtime profiles after source deletion and preserve exact output, +owner rereads, empty payloads, pending-owner failure and ordered copy-join traces. +The crossed compiler gate passes 951 library tests, followed by all 83 driver +library tests, 598 frontend fixtures and seven source-format checks. The corpus +registers 432 tests at its checkpoint. The later clean `cb617ecf` full workspace +includes the repair and passes 439 native tests and all 182 object obligations. +Supported-host acceptance passes at `5d919052` and the later documentation-only +`194ce065` revision. Required proof, pre-change captures and exact executed logs are +in [the bitfield contract](native_bitfield_projected_local_views.md) and +[the acceptance audit](native_acceptance_audit.md#bitfield-field-local-views-focused-acceptance). +The broad estimate stays about 85%, the fixed inventory stays 207, and the +whole-language objective remains 100%. + + +### Direct secret aggregate local views: bounded focused acceptance + +The declared-owner proof now preserves the already selected one-direct-Secret +receiver rule and exact endpoint qualification. Nothing and already-secret +nominal fields remain exact; other fields gain one Secret layer. Existing root, +immutable-loan, pending and non-owning cleanup contracts are unchanged. + +Three focused HIR tests, all 954 crossed compiler library tests and all seven +linked cases pass. Six cases pin successful outcome parity; the pending receiver +pins raw reference error and existing native redaction separately under the +[open observation policy](../../open_design/debug_print_hidden_secrets.md), with +matching partial stdout/status and preserved cleanup. It does not establish +diagnostic parity or select new policy. All 83 driver library tests, 598 frontend +fixtures and seven source-format checks pass. The exact clean `cb617ecf` +workspace passes all 439 native tests, all 182 object obligations, remaining +workspace targets and doc-tests, with unchanged head and exit zero at +`2026-10-02T20:55:57.0941072Z`. The documentation-only `194ce065` revision +passes all four supported-host build/package and relocated installed jobs in +[workflow 37064035604](https://github.com/vycdev/jett/actions/runs/37064035604), +with identical compiler/runtime/test sources. Exact +proof, captures and required resolution are in +[the secret-owner contract](native_secret_owner_projected_local_views.md) and +[the acceptance audit](native_acceptance_audit.md#direct-secret-aggregate-local-views-focused-acceptance-and-open-observation). +This changes neither hidden-secret observation policy nor the independent +unapplied MIR handler staging. Whole-language 100% remains the objective, +with about-85% tracking and the fixed 207 inventory unchanged. + + +### Inferred uninhabited callback inputs: descriptor storage and invocation + +Native codegen now distinguishes retaining a callback descriptor from invoking +its original body. An exact non-capture Never input permits normal owned +descriptor storage, captures, cloning, pending state and cleanup through a +nonzero internal trap entry, with no Never scalar or return payload. Original +typed metadata is checked before preparation; unused enclosing-frame slots from +inline extraction compact without deleting parent blocks or typed dependencies. +Surviving callable Never inputs and forged values remain rejected. + +All 966 compiler library tests and all nine source-deleted linked cases pass. +The latter exercise both matching profiles with exact success/failure outcomes, +captured ownership, pending joins, real integer calls, ordinary loop bodies and +equality inside uncallable bodies. Nine source-format checks and Rust formatting +pass, as do all 83 driver library tests and 598 frontend fixtures. +The corpus registers 448 supplemental cases at this checkpoint; the repaired +`90d82701` complete-workspace/object and supported-host result below subsequently +accepts that source scope. +Pre-change characterization, initial failures and selected proof boundaries are +in [the descriptor contract](native_uninhabited_callback_descriptors.md) and +[the acceptance audit](native_acceptance_audit.md#uninhabited-input-callback-descriptors-focused-acceptance). +This does not complete all callback-body operations or the independent borrowed +handler staging. Whole-language 100% remains the objective, tracked at about 85% +with the fixed 207-fixture inventory. + +### Original function metadata repair after the full callback run + +The exact clean `d14b830b` native suite completes with 446 passes and two +sum-arm failures. Original preflight demands a native result carrier for a +closure that checked impossible optional/result arm preparation later removes. +The remaining workspace/object targets were stopped after this complete native +suite failed; its log and terminal metadata retain the unchanged clean head, +446/2 outcome and explicit stop reason. It is not full acceptance. + +The recursive OriginalMetadata/NativeValue type-validation distinction preserves +the exact Never result as original metadata, without admitting a surviving +return-only callback or Never capture. All 969 compiler library tests, both +previously failing linked sum-arm tests and all nine callback tests pass. Three +new codegen units cover absent-arm closure removal and strict nested metadata +and live-value negatives. See +[the regression evidence](native_uninhabited_callback_descriptors.md#full-run-regression-and-original-function-metadata-repair). +The exact clean repaired revision `90d82701` subsequently passes the complete +workspace with an unchanged head: all 448 native tests (944.48 seconds), all +182 object obligations in the four-test manifest gate (553.77 seconds), all +83 driver library tests and 598 frontend fixtures, remaining targets and +doc-tests. The wrapper exits zero with 92 successful result groups at +`2026-10-03T08:31:51.9056689Z`; exact log/metadata are +`target/native-workspace-90d82701.log` and `.json`. Its supported-host workflow +[37108099164](https://github.com/vycdev/jett/actions/runs/37108099164) passes all +four Windows/Linux build/package and relocated installed jobs, last completing +`2026-10-03T09:03:23Z`. Corpus 448, inventory +207, about-85% tracking and the whole-language 100% objective remain unchanged. + +### Internal call-view scopes across handlers + +Original explicit view arguments now have eager typed Begin/End stages for +noncloneable exact local and field origins. A protected terminal mutable owner +may be rebound after the operation; an owned temporary evaluates once into +ordinary owning storage. Complete results precede End, and aborted owned +returns/loop exits abandon only their operation's internal loans. Persistent +source aliases and same-operation owner-change safety remain separate. +See [the implementation and focused gates](native_scoped_call_view_staging.md). +Whole-language coverage remains about 85%, with the fixed 207-fixture inventory +and 100% native-language objective unchanged. + +### Latent nested bodies and direct secret callback results + +Exact Never-input callbacks now preserve source-valid nested closure metadata +and one direct Secret[Never] result without demanding a runtime carrier. Typed +function/closure/adapter edges are checked before a separate cycle-safe original +body traversal; this does not add native reachability or relax live callable, +capture, nominal, Resource or hidden-secret policy guards. The source remains +checked before any body pruning. No runtime or ABI change is required. + +The isolated follow-up passes 63 codegen tests, all 27 linked regressions and +1052 tests in the broader compiler run; the count-guard wrapper discrepancy is +recorded separately from passing tests. All 83 driver, 598 frontend and eight +backend-lowering tests also pass. Exact clean `72237c4a` subsequently passes its +complete 464-case workspace, all 182 object obligations and 92 successful result +groups; its supported-host workflow remains in progress. The preceding +`55027914` staging revision passes all four supported-host jobs as well as its +complete workspace. See +[the centralized proof and gates](native_uninhabited_callback_descriptors.md#latent-nested-closures-and-direct-secret-results). +This bounded repair changes neither the fixed 207 inventory/182 object +obligations nor the about-85% estimate and whole-language 100% objective. + +### Opaque resource type-only metadata + +The builtin `TypeKind` enum now admits the already selected `resource_type` +variant, and the shared reflection mapper returns the same tag. Existing enum +indices, alias tags and empty opaque shapes are preserved. Source checking, +reference metadata and native object emission are tested without a Resource +value. The affected 800 compiler tests and 598 frontend fixtures pass; the +subsequent clean `616b51b9` full workspace also passes. Supported-host acceptance +for that head remains separate. See +[the contract and executed evidence](native_resource_kind_tags.md). Live resource +hooks, carriers, provider authority and exactly-once cleanup remain open. + +### Checked private resource-hook identity prerequisite + +Compiler-supplied private resource declarations now retain exact Resource and +Function DefIds, independent stdlib origin evidence, normal privacy/order and +complete checked function ownership modes. Explicit fallible compiler entries +validate all facts; default production resolution has an empty catalog. No +public provider spelling, live carrier, resource hook dispatch or native drop +admission is introduced. The selected pre-Rust contract and implementation are +in [the active identity note](native_resource_hook_identity.md). + +All 14 focused resolver/checker/span tests pass with stable source hashes in +`target/native-resource-hook-focused-after-diagnostic.json`. The initial helper +mutability compile error and corrected source-body E0311 assertion remain +separate failure evidence. The broader 1037 compiler library tests, 63 codegen +object integration tests, 83 driver tests, 598 frontend fixtures and eight +backend-lowering tests also pass with unchanged sources. Clean `616b51b9` +subsequently passes its full workspace; its supported-host workflow remains +in progress. Reference provider installation, ordered source cleanup, +HIR/MIR transfer/drop proofs and native lifecycle execution still remain. This +prerequisite changes neither the fixed 207 inventory/182 object obligations nor +the 464-case supplemental native corpus, about-85% estimate or full 100% goal. + + +### Static Resource ownership prerequisites + +Resource-bearing owned field copies and cloning collection/reflection getters +now report E0364. Known borrowed Resource data cannot be acquired by owning +optional/result, list/map, struct or machine payloads (E0401); a Resource-bearing +exact-state transition source retains the existing owned-argument boundary. +Exact Resource-root printing and value reflection report E0300. Safe explicit +views, unrelated copyable fields, real owned transfers, empty carriers and +type-only metadata remain admitted. Nested printing policy is unchanged. + +All 36 focused resource groups pass, including 21 new source groups. The 34 +frozen synthetic-stdlib sources are rechecked in debug and release: selected +copy/acquisition/root-observation gaps are refused and owned/view/empty controls +are retained. The broader 1058 library units, 63 codegen object integration +checks, 83 driver tests, 598 frontend fixtures and eight backend-lowering tests +also pass with three unchanged source hashes. Details and the separate accepted +`616b51b9` predecessor record are in the +[ownership note](native_resource_ownership_boundaries.md) and +[acceptance audit](native_acceptance_audit.md#resource-static-ownership-boundaries). +This is checker-stage safety evidence. Resource carriers, fake/real providers, +source cleanup and native lifecycle execution remain required. The new full +workspace and supported-host gates are pending; 85%, 207 fixtures, 182 object +obligations and the 464-case supplemental corpus are unchanged. + + +### Immutable checked Resource program prerequisite + +The exact clean static-guard head `b7180bd9` now passes its full workspace: +all 92 result groups, 464 supplemental native cases and 182 object obligations. +Its supported-host workflow 37123835396 also passes all four Windows/Linux build +and installed relocation jobs. The earlier `616b51b9` four-job result remains +historical. These are separate commit-specific receipts in the acceptance audit. + +The next compiler handoff retains one owned parsed Module, ResolveResult, +CheckResult/interner and explicit loader origins, derived internally and exposed +only through shared references. Parser, resolver and checker errors prevent +sealing; public hook validation rejects checker errors independently while +ordinary checking retains source repair diagnostics. All 43 focused Resource +groups pass, including seven new snapshot groups; all 454 resolver/typechecker/ +HIR library tests pass with five unchanged compiler source hashes. The +[checked-program note](native_resource_checked_program.md) records exact scope. +This supplies no provider, runtime Resource, cleanup or native ABI. Private +reference-driver retention is now implemented in the following bounded +follow-up. Program-bound invocation eligibility and exact caller ownership +handoff remain required; a formal View mode alone does not recover caller +retention. Full-workspace and supported-host gates remain commit-specific. +The about-85% estimate, fixed 207 inventory/182 object obligations, 464 native +cases and whole-language 100% objective remain unchanged. + + +### Retained reference preparation + +A private driver result now retains the exact sealed checked Module and primary +FileId0 main span/namespace through reference execution. Public BuildResult is +unchanged. Runtime uses one merged registration and performs no source read, +parse or module rediscovery after preparation. The earlier macOS graphics +thread-selection read remains outside this bounded repair. Phase errors retain +all preceding warnings in order, and metadata errors retain their typed cause. +Required comptime and verification still run once and gate execution before +providers. No Resource value, provider, hook dispatch or native ABI is enabled. + +All 46 focused Resource groups and four new driver groups pass with stable +source hashes. All 544 driver/HIR/resolver/typechecker tests also pass with those +hashes unchanged. All 598 frontend fixtures and eight backend-lowering +regressions also pass. New full-workspace/platform acceptance remains pending. The exact94 predecessor +workspace failed after a NamedTempFile.reopen PermissionDenied; its affected +native case passes an isolated rerun, not whole-workspace acceptance. See +[the reference contract and proof](native_prepared_reference_driver.md) and +[centralized acceptance context](native_acceptance_audit.md#retained-reference-preparation). +The about-85% estimate, fixed 207 inventory/182 obligations, 464 supplemental +native cases and whole-language 100% goal are unchanged. + +### Typed caller ownership integration + +The caller disposition selected by Rule24 now survives exact typed source, +HIR, MIR and native handoffs. Separate sealed proofs cover generated calls, +temporary staging, viewed loops and handled defaults; Rule25 data snapshots +preserve existing interface conversion. An earlier formatted integration +checkpoint passed 293 checker, 598 frontend, 168 HIR, 147 MIR, 67 backend and +63 object tests. At that checkpoint all 15 linked caller groups passed in both +profiles after source deletion, and the two failed driver property replays +passed independently. + +The current formatted compiler-core gate passes 304 checker, 188 HIR, 196 MIR, +70 backend and 63 object-emission tests, plus the MIR doctest. These current +counts remain separate from full-workspace, complete-native and supported-host +acceptance. Per-pass signature projection reuse preserves all validation +boundaries and exact diagnostics. + +The bounded handled-conversion follow-up now passes all 66 MIR acquisition +groups and a new linked payload-reading fixture in both profiles after source +deletion. Captured mutable plain `TypeConstruction` generations pass seven MIR +groups, three native emission groups and the original builder native regression. +All seven failures of the historical 479-group run now pass individual replays. +These paths preserve exact private Source/CFG authority and add no source clone +or ABI permission; runtime interface boxing may copy its payload. Broader +simultaneous cohorts and other owner kinds remain open. + +The [contract](native_call_ownership_facts.md) and +[acceptance record](native_acceptance_audit.md#typed-caller-ownership-integration) +separate these focused results from the later complete Windows native receipt: +480/480 pass at exact committed `a63f891016fac7dc50fffdde33be8b0ce45c655f`, +with stable HEAD/tracked sources in +`target/native480-after-owner-generation/receipt.json`, ending +`2026-10-04T14:20:12.987780+00:00`. Resource changes are absent. Full-workspace, +current 182-object inventory and supported-host acceptance remain pending. The a9 predecessor is accepted locally +and on all four supported-host jobs. The fixed 207 inventory, about 85% tracking +and 100% whole-language goal remain unchanged. A separate private reference Resource lifecycle now passes 36 focused tests, +including actual Source cleanup and required-worker controls. Production +provider dispatch and native ownership/cleanup remain required. + + +### Private reference Resource lifecycle + +The exact checked program now supports private reference hook descriptors, +runtime-purpose invocation gates, non-cloneable owning tickets and ordered +custody through Source calls, scopes, temporaries and occupied results. +Ordinary/mutual declarations rejoin their retained executable AST and exact +resolver metadata; registration clones cannot replace checked bodies. Caller +source order and formal permutation remain separate. Explicit close and reverse +cleanup retire owners before context teardown. Cleanup failure overrides entry +failure; clean cleanup preserves an ordinary entry error and source result.fail +remains data. The public production catalog and provider remain disabled. + +Root terminal `923450` passes all 36 Resource-focused reference tests in +`target/native-resource-after-checked-call-relay-focused.log`, including real +both-profile Source lifecycles, failed later actuals, indirect close, terminal +failure and required-worker/purpose controls. This is separate from the committed +caller native480 receipt and adds no native inventory case. Broad library, +full-workspace and supported-host acceptance of this Resource candidate remain +pending. See [the exact scope](native_resource_hook_identity.md#reference-lifecycle-execution). +Whole scoped/reflected/indirect/worker and aggregate ownership breadth, real +providers, native hook/carrier/drop integration and task/actor cancellation remain +required. The about-85% estimate, fixed 207/182 inventory and 100% goal are unchanged. + +### Source plain Resource replacement + +Native assignment now executes exact checked plain Resource replacement, self-reseat and vacant-after-close transfer. Six new shared scenarios extend the original five; all 11 pass in both real-reference profiles and as generated native programs after Source deletion. Runtime preflight preserves invalid inputs before finalization, and failure tests distinguish an ordinary RHS error from old-finalizer cleanup failure. The formatted compiler gate passes 558 checks plus the MIR doctest and606 frontend/lowering checks pass. One temporary capture failure in the broad driver-library run passes isolated replay; clean full-library, complete native regression and supported-host results remain separate gates. Conditional/aggregate/projected replacement and broader Resource/provider/async/reflection work remain required. The coarse estimate remains about 85%, with fixed 207 fixtures and 182 object obligations unchanged. + +### Handled Resource call progress (2026-10-05) + +The local native Resource gate now includes eighteen shared original Source +scenarios in both profiles (36 native and 36 reference executions). Direct Result +handlers inside call arguments preserve lexical evaluation, prepared formals, +acquired-prefix cleanup, retained views, clean skipped invocations and observed +cleanup failure. Nested first-factory success/failure also distinguish Scope +storage from the current execution frame. Every native case runs after Source +deletion and observes zero obligations before destroy. + +The formatted affected compiler/object gate passes 573 checks plus the MIR +doctest, with 606 frontend/lowering fixture checks, all four fixed inventory +gates (182 emitted host objects), and the complete 92-test driver library. +The final formatted Source replay passes all 36 native and 36 reference runs. +This advances the Resource row without closing its remaining indirect, aggregate, refinement and broader +control-flow obligations. The broad estimate remains about 85%; full current +native/workspace and supported-host release gates remain separate requirements. + +### Callable cleanup boundaries (2026-10-06) + +Three new original Source programs extend the shared lifecycle gate from 18 to +22 scenarios. Both reference and native profiles pass all 44 executions each; +native launch follows Source deletion and observes exact reports and zero +obligations before teardown. The additions cover reversed lexical/formal order +with two temporary view owners, a Return helper using an independent owner, +a continuing default, and cleanup panic suppressing the Return helper. + +The reference evaluator now retires the pending call prefix before evaluating +the original Return operand. Ordinary closure and named-callable bodies have +their own operation floor and no inherited Resource return destination. Native +Source joins retain authenticated terminal failure outcomes, keeping original +body status separate from cleanup instead of replacing status 255 with 1. + +The affected gates pass all 508 reference-library tests, all 92 driver-library +tests, and all 222 runtime tests (209 unit and 13 integration). Six private +Return-prefix controls and eleven focused native Source controls are included +in those totals. Fresh matched debug/release archives contain one main, eight +private exports and all 32 Resource leaves each, with the unchanged ABI/wire. + +Parent a30ca97e's exact source tree passed the complete workspace on GNU and +MSVC, including 486 default native tests per host and both source-absent installed +package jobs. The local full run had 26 deadline failures; all 26 subsequently +passed at the same deadlines, in a 25-case replay and an isolated inventory +replay. This is not a clean full local run. The successor's complete workspace +and supported-host checks remain separate release gates. Overall tracking stays +about 85%; indirect/aggregate/refinement Resource calls, broader projections and +reflection, production providers and shared scheduling remain in the full goal. + +### Named Resource function values, 2026-10-06 + +Exact named non-generic, capability-free callbacks now retain their original +Indirect Source authority through immutable root-scope locals and aliases. A +private original/current producer/use proof selects the retained body; native +code and empty-environment checks precede the existing hidden-Scope Source +bridge. Ordinary descriptor storage is admitted only for exact proved values, +with public signature refusals and raw hook descriptor handling unchanged. + +The shared Resource corpus increases from 22 to 28 scenarios. All 56 genuine reference +and 56 linked native executions pass in both profiles, including distinct +same-signature body selection and separate body/cleanup failure propagation. +The complete reference library passes 511 tests, MIR 233, native codegen 93 and +object emission 63. Runtime sources/archives are unchanged; native Source is +removed before launch and obligations retire before teardown. The complete +driver library passes 92 tests; ordinary callable regression groups pass +13 function, one captured-closure, one collection-callback and 16 caller-ownership +tests. Parent a5e33b1f passed the full workspace and 486 default native tests on +both GNU and MSVC, plus both Source-absent installed package jobs. Its tested +merge has the identical source tree. Fresh supported-host acceptance of this +named-callback successor remains a separate gate. +Namespace-member/returned/captured/scoped/generic/handled/pipeline forms, +aggregate/refinement Resource paths and production provider/concurrency work +remain required. The coarse 85% estimate, 207-fixture/182-object inventory and +whole-language 100% goal remain unchanged. + +### Namespace-qualified Resource callbacks, 2026-10-06 + +The interpreter now joins a qualified function value to its exact full-span +resolved declaration and retained checked header/signature. Instance fields, +method adapters, generic functions and compiler hooks cannot mint named-body +proof. Original expression identity, alias chain, body attempt and indirect +argument tuple remain authenticated. + +Two new Source programs and four shared oracles grow the Resource corpus from +28 to 32 scenarios. All 64 reference and 64 Source-absent linked native executions +pass across debug and release, including alias close, handled factory failure +and cleanup panic. Seven callback-proof tests and the complete 515-test reference +library pass; the complete driver library passes 92 tests. Fresh supported-host +acceptance remains a separate gate. Runtime sources and matched archives are +unchanged. + +Returned Resource hook callbacks are the next executed baseline. MIR currently +loses exact hook identity across a factory return, and native invocation still +refuses the descriptor route. Existing runtime descriptor validation and frame +staging provide a compatible ABI path; exact return provenance, opaque carrier +transport and emitted descriptor invocation rows remain to implement. Dynamic, +captured/scoped/generic/handled/pipeline/required callbacks, aggregates, providers +and concurrency remain required. Overall tracking remains about 85%. + + +### Returned compiler-hook callbacks, 2026-10-06 + +Exact returned Factory, Borrow and Close descriptors now pass all 46 shared +Resource scenarios in both reference and Source-absent linked native profiles: +92 executions each, including stored/alias/relay/unused metadata, immediate +callees, observable argument-before-callee order and factory/actual/callee/ +finalizer failure cleanup. Immutable return provenance and independent current +body/entry/header proofs survive canonical remaps. Copied/resealed pruning +regressions are refused before evidence can be erased. Returned metadata is +installation-owned; only exact proved occurrences bypass ordinary Function +storage. Existing wire-v2/runtime leaves and matched archives remain unchanged. + +Nine focused MIR groups and 242 MIR library tests pass. Seven focused codegen +groups, all 100 codegen library tests and 63 object tests pass. Authenticated +Resource roots now seed typed reachability before walking ordinary dependencies; +pure helpers retain their normal ABI. All 92 driver-library tests pass; fresh +supported-host gates remain pending. See +[the contract and remaining forms](native_returned_resource_hooks.md). +Required/comptime materialization, returned named callbacks, dynamic/captured/ +scoped/generic/handled/pipeline forms, nested returns, aggregates and production +providers/concurrency remain required. The coarse estimate remains about 85%, +the fixed inventory remains 207 fixtures/182 objects, and the whole-language +100% goal is active. + +### Required Resource descriptors: candidate extension + +The shared original Source corpus has 60 candidate scenarios (46 previously +accepted plus fourteen new cases across eleven Source files). The reference +oracle now runs the real provider-free required worker, installs its same-program +checked cache before runtime registration, and refuses missing or failed required +evaluation. All 120 debug/release reference executions pass. The initial native +baseline fails on the first new comptime descriptor with pending nested Resource +evaluation requiring exact Source-order staging. + +Implementation in progress uses opaque private checked required proofs and an +authenticated materialized HIR view that preserves original Source. New candidate +cases cover Factory/Borrow/Close, factory and cleanup failure, aliases, relays, +unused storage, immediate invocation, absence, pure conditional selection and +generic/scoped selectors. They do not count as accepted native execution until +the same cases pass both profiles with source absent, exact event/status oracles +and zero obligations before teardown. The whole-language estimate remains about +85%, and the full completion obligations remain active. + + +### Verified required Resource values and ordinary helper seals (2026-10-06) + +The shared original Source corpus now contains 61 scenarios. All 122 reference +and 122 source-absent native executions pass in both profiles against the +unchanged matched MSVC compiler-test archives. Required capture and materialization +use the actual driver seams; acceptance also includes production HIR value +conversion and general MIR validation before native object emission. + +Opaque private-cache proofs retain the exact checked program, original owner, +required region, specialization and scoped selection. The original archive is +immutable. Every required row seals its current body, including ordinary runtime +helpers that retain their normal ABI and receive no Resource execution authority. +Exact guarded sum arms are both planned; runtime absence still skips extraction +and finalization. Disconnected Resource headers and normalized operation nodes +remain custody obligations. + +Nine focused MIR controls, 251 MIR library tests and the driver foreign-context +control pass. Full native coverage remains active: qualified/complex required +transport, broader callbacks/aggregates/views/reflection, production providers, +concurrency, GNU private-archive execution and final coherent workspace/platform +acceptance are still required. The rough tracking estimate remains about 85%. + +The complete affected codegen suite passes 100 library and 63 object tests; +all 93 driver-library tests also pass. Final Rust/Jett formatting and whitespace +checks pass, and all 2,168 frozen source/config inputs revalidate unchanged. + +### Verified borrowed optional/result Resource payloads (2026-10-07) + +Sources 45..56 add twelve scenarios covering written/bare None/Some/Fail/Ok, +named-argument prefix abort, and handled borrow-domain failure. The unchanged +baseline passed all 73 shared scenarios / 146 reference executions but refused +Source45 at HIR stable-origin admission. The complete corpus now passes 146 +Source-deleted native executions against freshly measured Debug/Release MSVC +private runtime archives, with exact events and zero obligations before teardown. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three focused +MIR groups and six runtime groups cover original-body custody, guarded projections, +conditional shell lifetimes, failure companions, argument aborts and forgery. +The full codegen suite passes 102 library and 63 object tests, including unchanged +prior invalid-MIR controls. Archives contain 37 Resource leaves, eight private +exports and one main; independent source/symbol/CRT receipts remain mandatory. + +Admission is limited to root-body Resource aliases. Nested/scoped aliases, +ordinary non-Resource borrowed sums and alternate Default payload lifetimes remain +pending. The rough full-language estimate stays about 85%, and the full 100% +native-codegen goal, GNU Resource gate and broader language/platform requirements +remain open. See [the integration contract](native_resource_borrowed_sums.md). + +All 521 reference-evaluator and 93 driver-library tests pass. The existing +Source33 required-value assertion now verifies its exact two original proofs, +including scalar 761 and the unique checked hook; other fixtures retain exact +one-value counts. This separately fixes the sole failure observed on both hosts +at 17d6c841. Rust and all twelve new Jett Source format checks pass, and the +2,188 frozen source/config inputs revalidate. The final MIR-only formatting change +is exactly one error-return line wrap and passes all 254 MIR tests again. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and coherent-revision acceptance +of the generated Verify/Property transitions below remain full-goal obligations. +No Source exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. + +### Generated callback-suite proof continuity + +Checked Verify and Property execution now retain the original test-body owner +when constructing a closure witness. The owner is joined to the same retained +checked program, original block and namespace; active capture ownership and +Property purpose restore on both ordinary errors and panic. Public test-file +execution installs that same checked program instead of a metadata-only +projection. + +The HIR observation proof separately joins an immutable owned CallbackHolder +constructor or alias to its exact original binding, callable producer, captures +and reflected read. Matching types, spans or an edited public body cannot mint +that association. Existing full Source, current-body and CFG seals remain +mandatory; this proof grants no runtime Resource or Scope authority. + +The ordinary reflected-callable Property path now uses an opaque checked-case +plan from the original successful evaluator/cache snapshot. Its canonical HIR +factory builds the ordered 100-trial wrapper and records the exact protected +transition. Replay reconstructs from the private pre-suite graph, checks the +retained plan and original trial identities, and validates changed shrink inputs. +It cannot renew proof around a caller-appended or edited wrapper. Exact sampled +inputs use full typed snapshots rather than ordinary Value equality. Verify's +existing canonical factory remains unchanged. + +Permanent Driver coverage adds the unchanged named and captured Sources19/20 +under the existing reflected-callable module. The strict reference gate requires +two passing blocks and the actual 100-trial Property report per Source. Separate +gates require all six Program/Verify/Property objects and twelve Source-deleted +executions using normal Debug/Release launchers; executables are reaped and +removed one at a time. Object hashes, raw streams and actual outputs may be +retained; ordinary public launchers expose no context-retirement counters. + +All three permanent controls now pass on a fresh current compiler gate. Each +reference report records both passing blocks and 100 Property trials; six +standalone objects and six separately emitted Source-deleted linked objects pass +twelve actual normal MSVC Debug/Release executions with unchanged inputs and +measured archives. Exact output/status, object identity and executable reaping +are checked. The public launcher supplies no context-retirement observation. +See the [bounded acceptance receipt](native_acceptance_audit.md#generated-reflected-callback-suites-bounded-msvc-acceptance). + +Both six-test Comptime/HIR groups, all eight native Property controls and all +1,664 tests in the five affected compiler libraries pass. Original fixtures and +ordinary validation remain intact. +Full coherent-workspace and platform acceptance are separate gates. The rough +87% estimate and fixed 207/182 inventory stay unchanged. diff --git a/docs/completed/native_codegen/native_comptime_actor_graph.md b/docs/completed/native_codegen/native_comptime_actor_graph.md new file mode 100644 index 00000000..f87d5cec --- /dev/null +++ b/docs/completed/native_codegen/native_comptime_actor_graph.md @@ -0,0 +1,201 @@ +# Native materialization of comptime actor graph templates + +Status: implemented in the current combined compiler. Focused local checks and +32 linked MSVC executions passed; broader completion gates remain pending. + +Pure closed required expressions freeze their reachable actors, typed state and +admitted captures at successful completion. The baked cache holds an immutable +template. Every runtime evaluation of the expression imports a fresh graph; +call and loop activations do not share mutable template state. Once a result is +bound, ordinary reads and aliases retain the same live instances. A graph-wide +memo preserves sharing and keeps distinct seed nodes distinct. + +Export/import covers actors inside admitted typed/refined/interface wrappers, +aggregate values, pending carriers, builders and callback captures. Checked +nominal identities and existing pending behavior remain intact. A type-aware +work queue and visited-node memo avoid treating nested actor IDs as independent +scalars. Import does not execute initializers, handlers, source predicates, the +original expression or compile-time debug events. Runtime capabilities and +Resource custody cannot be cloned into a template. Resource hook descriptors +retain their separate private checked association and gain no authority from a +graph packet. + +Current raw native allocation remains confined to the exact checked actor +constructor. The snapshot path carries a privately derived reconstruction plan; +it cannot admit arbitrary raw ActorSpawn allocation. A two-phase unpublished +import reserves fresh identities, populates checked fields, validates completion +and then publishes the root. Any failure retires all partial owners and +registrations. Existing context-local liveness and global nonreused native actor +identities remain unchanged. + +Existing Source admission remains unchanged. Actor closure capture is move-only +and rejected with E0402. The saved rejection control remains required; the graph +contract covers admitted captures. Unchanged primitive closure captures retain +their original typed context and capture witnesses. Reflected callable values +must retain their checked plan and authenticated source through reconstruction +and lowering. Neither copying an obsolete witness after changing its captures +nor clearing/resetting a witness can authorize a graph import. This integration +introduces no Actor-capture resealing policy. + +The current compiler derives HIR reconstruction from the checked program and +retains independent original/current MIR seals, native JA1 carrier schemas and +private reserve/fill/commit/abort staging. Owner preflight, Callback plans and +capture witnesses, Resource constructor/incoming-frame checks and occupied +carrier cleanup remain separately authenticated. Graph and callable witnesses +are renewed together against the same original/current program transition; +exact leaf association selects the adapted callback. Required-value helper +relocation preserves reflected-callable authentication and materialization. + +The protected constructor producer mapping and ordinary storage planning, +including graph programs with no Resource kinds, are recorded in +[Graph constructor transport](native_graph_constructor_transport.md). + +The existing generated Resource startup is reused without a parallel installer +or Runtime-purpose issuer. Graph hook metadata needs exactly one additional +normal export, `jett_rt_v1_resource_graph_descriptor_box`; the composed Resource +inventory is 56 production symbols and eight separate private hooks. This proof +provides no Scope, Network grant or live Resource owner. The normal provider +remains Disabled. Descriptor graph startup supplies no production I/O credit. + +Protected collection iteration keeps graph storage tied to its original +producer. Canonical sequence preparation records only the exact generated +iterator owner initialized once from that iterable, separately from original +Source headers. Current validation checks its initializer, header, sequence +element/value selection and consuming behavior. Altering a same-typed local or +iterator operation grants no storage proof. + +Generated Resource completion releases Resource companions before retiring +actor state and unfinished graph imports belonging to the exact Running +ordinary attempt. Retirement precedes emptiness checks and terminal sealing. +First body diagnostics survive; cleanup failure retains priority and prevents +clean reentry. Context destruction observes the already drained actor table. + +## Current local validation + +The current `cargo test --offline --locked -q --lib --no-fail-fast -p jett_hir -p jett_mir -p jett_codegen_cranelift -p jett_driver --target x86_64-pc-windows-msvc -- --test-threads=1` run passed all 793 tests across 4 result groups, with zero failures and ignored tests. This current command covers HIR, MIR, codegen and Driver. The separately pinned predecessor run contributes 1673 passing tests across 18 other package result groups. Those results remain historical evidence; the combined count does not establish a fresh full workspace library run. Complete workspace integration/native conformance, exact-revision supported-host CI, GNU and installed distribution remain open. + +The current interface compatibility selection passed 25 native tests with unchanged compiler and artifact guards: 21 original interface-prefix tests and four generic, deep-conversion and reflected-interface controls. This selection is a subset of normal native conformance. Earlier ordinary callback, Owner and Actor regression results retain their predecessor revision and are not counted as current compatibility acceptance. + +The current sixteen original private Resource gates passed 754 Source-deleted native executions and 900 observed entries, using the fresh compiled receipt pin and unchanged compiler/source guards. The bounded startup matrix passed all 80 selected executions with Source absent, unchanged inputs and no retained case executables. + +The preceding compiler passed MIR 344, graph metadata 13, focused constant 23 and +the exact latent-callback gate with two Source-deleted Debug/Release executions. +Its broad21/2119 and separate MIR 344 runs covered 2463 workspace library tests, +with no workspace integration credit. Runtime 361, HIR 218, Python 74, map object +emission and constructor controls are earlier local observations. The ordinary +required-replacement and wrapper-sequence repairs passed their focused controls +and the current linked acceptance recorded below. No speedup ratio +or newly verified builder domain is claimed. + +Normal MSVC Runtime and Launcher archives were measured in Debug and Release +with 56 production Resource exports, static CRT and no private exports. The +receipt retains actual ordered normal native-library notes. The canonical +launcher-manifest vector differs in the order of its first two libraries; both +vectors are retained separately, and linked acceptance must establish the +selected manifest vector. + +Fresh normal MSVC linked acceptance passed 32 Source-deleted executions: 12 +public and four protected hook executions with each Debug/Release Runtime +archive. Public coverage comprises nine original ordinary fixtures, an adapted +callback and a production Stdout callback. Original and adapted debug cases used +Debug Source; the production callback and both hook Sources covered both Jett +optimization choices. Exact unique rows, independent reference/stream oracles, +successful terminal/reap/timeout records, absent Source and object hashes passed +with unchanged compiler and used-launcher guards. Each case executable was +removed after reaping. Actual compiler native-library notes and the canonical +manifest vector remain separate records; the linked executions establish the +selected manifest vector. Admission/map collectors provide no acceptance credit. + +The older failures of original hook Sources07/08 occurred before graph +construction in a compiler lacking generated Resource startup. They remain +historical observations, not current Source rejection oracles. The original +aggregate success case and body/header/pointer/resealed-parent forgery controls +remain required. + +Integration/native conformance, exact-revision supported-host CI, GNU and +installed distribution remain open. Wider original probes, +post-comptime state mutations, identity collisions, repeated activation, +sharing/distinct nodes, admitted nested carriers/captures, nominal dispatch, +failure cleanup and no replay remain obligations wherever current evidence has +not established them. Release Source print rejection remains separate from the +Debug/Release runtime archive profiles. The rough 87% estimate and fixed +207-fixture/182-object inventory are unchanged; this slice does not establish the +full 100% native-codegen goal. + +This implementation covers the existing sequential evaluator and supplies no +asynchronous scheduling or production-provider credit. Existing run/join/cancel, +actor/message ownership and other implemented reference behavior remain native +parity obligations. Adding the absent asynchronous scheduler or production +provider APIs is separate future feature work, not a prerequisite for parity +with the existing interpreter. + +## Original graph analysis batching + +`OriginalGraphAnalysis` reconstructs all original owner rows once for an exact +immutable checked execution archive. Its private constructor validates archived +type meanings and dense function IDs, rebuilds factories and accounts for unknown +call/message edges with the existing bounded fixed point and exact row comparison. +Non-convergence produces no admission rows. Each selection checks exact archive +identity, archived types and the complete original owner; equal Source text or a +TypeId cannot substitute for those checks. + +The Resource execution closure retains this private immutable table and shares +it with constructor capture and typed graph validation. Reconstructed closures +reuse the same table while independently recomputing their function and handler +sets. Closure equality also checks the table's exact retained archive identity. +Current MIR body/header, constructor, root shape, transport and pointer-membership +checks remain mandatory. The table grants no runtime Scope or callable authority +and is never stored in a global or cross-session cache. + +A completed analysis also records whether it contains any original graph rows. +Only that private result for the exact archive, together with empty current +metadata, permits a negative storage answer without repeating graph-specific +validation. It never yields a storage proof or removes a custody function. +Original archive/type authentication and independent closure reconstruction remain +mandatory; positive or malformed current records retain full validation. + +## Required observation capture + +Ordinary retained checked programs prepare namespace constant initializers through +the same original-program and purpose checks as collected explicit expressions. +Both passes select the same private checked attempt key, so successful and failed +initializers are evaluated once and retain their actual observations once. Baked +runtime reads stay silent. This repair adds no runtime initializer, global storage, +Resource transport, provider or custody and changes no constant admission rule. + +## Authenticated ordinary required replacements + +Every retained checked ordinary archive now carries required replacements through +the private authenticated transition, even when no graph or reflected callable +plan selected the owner. The original archive is checked before replacement; +empty Resource kinds, the unique original owner/context proof, typed Source site, +unchanged original locals and exact generated bindings remain required. Canonical +graph/callable conversion renews the existing seals at that fixed boundary. Raw +baking cannot silently change a retained actor archive. + +For an admitted `TypeConstruction` value, the matcher uses the sole existing +evaluated-builder producer and compares its complete canonical typed transcript, +including schema/owner, payload, field order, pending layers and failure operations. +This path requires zero added locals and bindings. It introduces no broader +builder-domain admission or runtime fallback. The two focused HIR controls passed primitive replacement, foreign/pre/post-edit +refusal and altered builder-transcript checks. The three original interface +native regression tests passed with Source present and unchanged inputs; these +results provide no Source-deleted execution credit. + +## Ordinary wrapper sequence continuity + +An ordinary actor/interface wrapper may retain authenticated Source entry-Scope +metadata while owning no Resource execution records. Its collection loop must +still use the existing finite ordinary sequence transition. The wrapper route +requires exact original entry-Scope capture and excludes graph metadata; the +ordinary reflected-field route keeps its existing qualification. + +The transition independently refuses execution, constructor/seal, call, region, +borrowed-sum and lexical-custody records. It checks the exact before/current +witness, canonical whole-body edit, original function/local headers and absence +of new custody. It may retain the authenticated wrapper but cannot install a new +Scope, provider, owner or proof. Dedicated graph sequence transport and actual +Resource execution routes remain unchanged. The MIR control passed the list[Named] Actor loop, five iterator/header/body/Source +alterations and real Resource-owner/direct-call predecessor refusals. The three +original interface native regression tests passed with Source present. Fresh +complete graph, Resource and startup proofs remain separate requirements. diff --git a/docs/completed/native_codegen/native_comptime_context.md b/docs/completed/native_codegen/native_comptime_context.md new file mode 100644 index 00000000..200b2b76 --- /dev/null +++ b/docs/completed/native_codegen/native_comptime_context.md @@ -0,0 +1,43 @@ +# Explicit comptime values in checked type contexts + +Explicit comptime expressions in a generic body are evaluated for each checked +instantiation. For example, `comptime type.name[T]()` must produce `int64` in +`label[int64]()` and `string` in `label[string]()`. A source span alone cannot +identify the result. The existing global span cache instead produces `T` for +both calls, contradicting the documented generic specialization contract. + +The evaluation identity must retain the checked generic arguments and +reflection-visible specialization, plus the complete lexical `comptime type` +binding chain. Source aliases that share a canonical type must remain distinct +when reflection distinguishes them. Inline functions inherit their enclosing +type context, while ordinary calls isolate the caller's bindings. + +Evaluation receives type bindings and checked expression facts, never runtime +parameters, locals, or captures. A generic expression referencing a runtime +parameter therefore still fails compilation. Only checked generic instances +are evaluated; an uninstantiated template supplies no concrete value. + +The interpreter and native constant materializer must select the same context. +HIR retains lexical type bindings at the explicit expression, including scoped +branches whose enclosing function has no generic arguments. Missing evaluated +values are compilation failures, never permission to execute source comptime +computations at runtime. Nested explicit expressions evaluated while building +an outer constant use that evaluation's type context. + +Validation must distinguish multiple generic instantiations and aliases at one +source span, lexical reflected bindings, nested binding chains, inline functions, +and forbidden runtime dependencies. Existing closed value, reflection, callback, +builder, and fixture suites remain regression gates. + +The implementation now keys evaluated values by that complete context and +selects them identically in the interpreter and native HIR baker. +`tests/native/contextual_comptime_values.jett` covers distinct generic arguments, +aliases, reflected record fields, nested lexical bindings, live and baked +callbacks, checked type guards, runtime guards, nested explicit expressions, +generic forwarding, and isolation from caller type arguments. A compile-fail +fixture rejects a generic runtime parameter used as a comptime value. + +Validation passed: the new linked differential fixture, 451 compiler-phase +tests, all 583 frontend fixtures, 11 existing native comptime tests, 11 native +interface tests, and workspace documentation tests. These checks establish this +context fix; they do not close the remaining native parity release gates. diff --git a/docs/completed/native_codegen/native_display_context.md b/docs/completed/native_codegen/native_display_context.md new file mode 100644 index 00000000..7517d586 --- /dev/null +++ b/docs/completed/native_codegen/native_display_context.md @@ -0,0 +1,41 @@ +# Display interpolation uses the checked owner + +String interpolation selects the exact `Displayable.display` implementation for +the expression's checked type. If that type is an interface, its implementation +receives the erased value and may dispatch through the original interface. +Selecting the concrete payload's display method instead changes this explicit +contract. Native HIR selects the checked owner; the interpreter now does the +same instead of selecting only the payload's runtime owner. + +Implementation registration must retain the interface's declaration namespace, +which can differ from the namespace containing its implementation. In particular, +a root `Displayable` implemented by a namespaced type remains that root interface. +This is ordinary declaration identity, not a new method search rule. + +Differential coverage includes namespaced implementations, an erased +primitive and record with distinct concrete display behavior, projected and +returned interface values, generic and scoped bindings, and closed comptime +formatting. Interpolation evaluates each expression once and keeps view operands +available afterward. + +An explicit checked display implementation also takes precedence over the +built-in formatter for primitive types. Both the interpreter and native HIR +select the exact method before their primitive fallback. + +Pending interface receivers still fail dispatch. Their diagnostic names the +registered interface method, including its declaration namespace, consistently +with generated dispatchers. The failure regression evaluates an earlier owned +interpolation segment before a temporary pending interface receiver containing +a secret-bearing record. Both paths preserve the earlier output, omit the +secret, and terminate with the same error; native cleanup must release all +partially evaluated owners. + +Implicit display results have a separate exact-string readiness boundary. +The reference interpreter rejects a pending string returned by the selected +method with `Displayable.display returned pending(...) instead of string`. +HIR retains this distinction with `DisplayResult` around only the implicit +call; direct method calls and ordinary pending-string interpolation are +unaffected. MIR keeps the check in each staged segment initializer, before +later handlers or effects. Native checking borrows the returned handle without +joining or consuming it, leaving its existing owning slot available for use +or terminal cleanup. diff --git a/docs/completed/native_codegen/native_erased_same_owner_arguments.md b/docs/completed/native_codegen/native_erased_same_owner_arguments.md new file mode 100644 index 00000000..2269be00 --- /dev/null +++ b/docs/completed/native_codegen/native_erased_same_owner_arguments.md @@ -0,0 +1,72 @@ +# Exact concrete arguments for erased interface dispatch + +Erased interface calls validate additional concrete argument requirements against +the receiver-selected implementation before conversion or method body entry. +Different nominal types remain different even when their fields have identical +layouts. The selected runtime contract was recorded before implementation on +2026-10-08; this does not define equality operators on erased interfaces. + +The original wrong-owner Source05 now reports +`interface dispatch concrete argument owner mismatch` before its Left body. +Original matching-owner Source12 retains `left:one:two`. The fifteen original +parity Sources remain unchanged; the two required fixtures restored with this +slice retain their exact recovered bytes. + +## Implementation + +Reference direct and named-callback calls preflight already evaluated arguments +before installing callee scopes, selecting body metadata or normalizing values. +The selected declaration namespace and type substitutions preserve nominal +refinements, full generic arguments and primitive widths. + +Native dispatch borrows already evaluated formal locals and checks every required +finite conversion descriptor before conversion. The additive `InterfaceValidate` +leaf checks retained exact TypeIds and occupied container/sum arms without cloning, +changing native value storage or transferring custody. Descriptor parsing can +allocate Rust `Vec`/`Box` storage. Existing unboxing and malformed-handle checks +remain mandatory; validation failure follows the existing cleanup path. + +Pending receivers preserve canonical undefined-dispatch failure. Direct pending +identity can be checked without joining or reading fields; unavailable containers +provide no child-owner facts. Earlier argument failures keep their message and +output order. Matching view and owning argument modes remain unchanged. + +## Current integration verification + +On 2026-10-12 the restored Actor/Resource/Owner integration passes all 2,331 +workspace library tests, including eleven focused reference and six native runtime +preflight controls. Broader native interface regressions pass. Rust formatting and +whitespace checks pass. + +Normal MSVC static-CRT Debug and Release launcher archives each pass ten native +Owner tests. The original nine tests execute thirteen Source-deleted cases per +runtime profile with unoptimized Jett objects. An additional production `Stdout` +test executes matching and mismatched owners with both Jett optimization settings: +four more executions per profile, seventeen per profile and thirty-four overall. +Its release-policy builds preserve successful production output or the exact +owner-mismatch error and exit without requiring a debug-output exception. + +All original Source output, body-entry witness, evaluation-order, pending failure +precedence, expected statuses and cleanup assertions remain operation-specific. +The tests compile before removing the Source and launch fresh native processes. +Normal launcher builds use current runtime sources, including the additive leaf. + +## Remaining scope + +The checker substitutes the selected owner when an entire interface parameter is +the interface type. It does not recursively substitute Self inside +`list[Pairable]`; that declared erased-child contract remains unchanged. Nested +source coverage uses a concrete collection owner such as `list[optional[Left]]`. +Helper-level recursive checks do not establish unsupported Source signatures. + +Refinement and pending-value preflight controls are reference/runtime tests; +the current native Source suite does not establish their full linked coverage. +The optimized production control covers the original matching/mismatched pair, +not every nested/generic witness. GNU, installed-distribution and complete +coherent-revision native parity remain open. This checkpoint does not establish +normal Main Resource cleanup counters, a public Resource provider or full-goal +completion. The rough 87% estimate and fixed inventory remain unchanged. + +Historical isolated 3,640-test and twenty-six-execution records describe earlier +candidate work. Their target receipts were discarded during disk cleanup; those +records are not acceptance evidence for this integration. diff --git a/docs/completed/native_codegen/native_graph_constructor_transport.md b/docs/completed/native_codegen/native_graph_constructor_transport.md new file mode 100644 index 00000000..b71f3fc5 --- /dev/null +++ b/docs/completed/native_codegen/native_graph_constructor_transport.md @@ -0,0 +1,94 @@ +# Protected Graph constructor producer transport + +The admitted Graph aggregate fixture constructs a plain Bundle containing an Actor and a checked function descriptor. Current Source lowering stages its StructConstruct through the existing authenticated ResourceConstructor Begin/Stage/Commit/End lifecycle. The Graph metadata seal previously expected the unlowered Let expression, so it rejected the successful Source transition before its tampering controls could run. + +MIR must transport this producer through the immutable constructor occurrence seal. An exact original Graph value row selects a constructor by its unique original expression and ID. Its complete current lifecycle, lexical field order, field endpoints, generated headers, exact Commit output and completions remain checked by ResourceConstructorRegion::current. Forward reconstruction additionally maps each original field through the canonical Source-local/import transition and compares the recorded endpoint. It substitutes only that protected Commit output; no constructor exemption or type/signature-based storage proof is introduced. + +Generated Commit headers receive a separate private constructor-output collection. They never enter the original Source header collection or original/current local map. Typed reconstruction derives the output collection from the protected original rows and immutable constructor seal, checks it exactly, and compares each current expression against a forward reconstruction. The Graph body snapshot and existing constructor seal remain independent. Canonical local remaps update output headers and canonical block remaps retain the existing lifecycle and independent snapshot. + +Fresh Resource planning selects ordinary storage independently for each protected expression, local, formal and return. Unproved values retain the existing Resource shape and carrier route. Physical metadata constructor nodes retain their authenticated Source lifecycle while Begin/Commit/End create no carrier runtime operations or provisional custody frames; Stage still traverses the actual endpoint and refuses any acquired Resource custody. Descriptor producers and their exact instruction-to-operation rows remain evaluated and planned. The aggregate regression checks that the resulting plan has no carrier family, owner slots, loans or constructor/return frames and that its Descriptor rows survive. + +This is an implementation repair for an already admitted Graph expression, with no language or capability-policy change. Storage selection still requires current pointer identity. Invocation, Resource custody, Scope and Network authority remain separate proofs. The original aggregate success fixture and all body/header/pointer/resealed-parent tampering controls remain required. Native verification/emission must consume this exact producer-backed metadata distinction at each authenticated constructor occurrence; merely suppressing carrier-plan requirements is insufficient. + +## Exact companion endpoint preflight + +The ordinary companion's type-only preflight still rejects an unsupported aggregate +custody type. An exact protected Graph return, physical local header, or physical +expression instead supplies the typed ordinary storage proof for that endpoint. +The three preflight call sites select that proof independently; no TypeId cache or +type-wide admission is added. Expressions still pass the current-function pointer +gate and retain all child traversal, reads, moves, and liveness accounting. Graph +HookBox and owning RootPublish producers also reserve their ordinary owned +temporary. An absent producer/output proof invalidates storage selection, and +ordinary planning without the fresh companion context remains refused. + +## Native ordinary word qualification + +The first actual native object test rejects function 10's `app.Bundle` result in `Rows::populate`: the fresh Source plan correctly classifies its rooted Graph value as Ordinary, but the strict native Resource shape table has no ordinary aggregate shape. The same distinction applies to exact Graph formals and their original/current Source actual edges. + +The existing native protocol represents an ordinary Source argument/result as one 64-bit word. `NativeShape::Integer { bits: 64, signed: false }` validates that ordinary word without creating an owner slot, loan, Scope, descriptor grant, or Resource custody. Graph heap ownership remains in ordinary codegen, including the aggregate builder's prefix cleanup and the destination local's existing retain/drop protocol. `HookDescriptor` is inappropriate because it represents a scalar checked Resource descriptor; a Graph hook box is an owned ordinary value. `Carrier` is inappropriate because it represents Resource custody. + +Codegen encodes an opaque ordinary Graph native handle as that existing word shape only after selecting exact protected `GraphStorageProof` at the physical expression, return/formal or complete Source actual edge. The native storage classifier must also confirm a supported opaque handle. Primitive Graph values retain their primitive shape. General `shape(TypeId)` remains unchanged and rejects unsupported aggregate/callable types. The proof-qualified word is deduplicated by wire bytes but never inserted into the type-wide shape cache. An unrelated value or formal sharing the same TypeId therefore gains no qualification. Source rows still check the complete fresh caller/callee tuple and require an Ordinary operand; the exact edge retains the complete original/current call and original formal/actual. + +## Graph storage without Resource kinds + +The ordinary emitter derives a fresh checked graph storage plan whenever the +program contains validated graph storage, even if its Resource manifest has no +kinds. It derives and verifies the plan before canonical preparation, then derives +it again for the prepared MIR. Ordinary function emission receives that plan with +no Resource layout, hidden Scope or custody operations. A program with no graph +storage continues through ordinary verification. A changed body or local header +cannot authorize storage through an earlier plan. + +The unchanged sharing fixture exercises this route. Its current native Debug +execution passed with Source absent and unchanged compiler/launcher guards. +The complete 32-execution graph gate and current broad tests passed. Wider +native conformance and supported-host acceptance remain pending. + +## Protected collection iteration + +Sequence preparation materializes an iterable once before its loop backedge. +For protected graph collections it records only the exact generated iterator +owner initialized from the original/current protected producer. This generated +header remains separate from original Source headers and canonical local maps. +Validation requires the unique exact initializer and unchanged generated header, +then checks prepared sequence element/value selection, consuming behavior and +operation order. An unrelated same-typed collection or altered iterator cannot +inherit storage qualification. Existing ordinary loop ownership remains in force. + +The unchanged map-sharing fixture emits objects with both Jett optimization +choices in predecessor checks. Those checks rejected ten producer/iterator +alterations and passed 342 MIR tests. The current repaired compiler passed +all 345 MIR library tests and the separate 32-execution linked graph gate. +These object and validation results do not themselves establish linked execution. + +## Ordinary required-value archive continuity + +Required replacements in any retained checked ordinary program use the same +private original-owner/context transition, even without a graph or callable plan. +Original archive, Source-site and local/binding checks must complete before the +canonical graph/callable seals advance. A raw baked expression is insufficient +to authorize a changed retained actor archive. Admitted TypeConstruction values +are compared against the sole evaluated-builder producer's complete typed +transcript with zero added locals or bindings. Fresh regression acceptance for +this repair remains separate from predecessor collection and graph results. + +## Ordinary wrapper sequence continuity + +An ordinary actor/interface wrapper may retain authenticated Source entry-Scope +metadata while owning no Resource execution records. Its collection loop must +still use the existing finite ordinary sequence transition. The wrapper route +requires exact original entry-Scope capture and excludes graph metadata; the +ordinary reflected-field route keeps its existing qualification. + +The transition independently refuses execution, constructor/seal, call, region, +borrowed-sum and lexical-custody records. It checks the exact before/current +witness, canonical whole-body edit, original function/local headers and absence +of new custody. It may retain the authenticated wrapper but cannot install a new +Scope, provider, owner or proof. Dedicated graph sequence transport and actual +Resource execution routes remain unchanged. The MIR control passed the list[Named] Actor loop, five iterator/header/body/Source +alterations and real Resource-owner/direct-call predecessor refusals. The three +original interface native regression tests passed with Source present. Fresh +complete graph, Resource and startup proofs remain separate requirements. + +This repairs native representation of already admitted Source. It adds no Source capture admission, Runtime ABI/export, descriptor authority, custody exemption, or language rule. diff --git a/docs/completed/native_codegen/native_interface_values.md b/docs/completed/native_codegen/native_interface_values.md new file mode 100644 index 00000000..3aaf74f1 --- /dev/null +++ b/docs/completed/native_codegen/native_interface_values.md @@ -0,0 +1,548 @@ +# Native interface values + +The full native parity audit found an accepted runtime surface absent from the +fixture inventory: a concrete implementation can be stored in an interface +binding, returned as an interface, passed to an interface parameter, and mixed +with other implementations in a `list[Interface]`. The interpreter dispatches +an interface-qualified method using the concrete runtime value. The current +native path originally handled concrete interface calls but rejected these dynamic +calls. The first implementation stage below now supports direct erased values. +This is an implementation gap, not a proposed language feature or exclusion. + +## Existing behavior to preserve + +An interface `Named` with `name(view self: Named) returns string`, implemented +by `User` and `Group`, accepts a function `show(view item: Named)` whose body +calls `Named.name(view item)`. A function returning `Named` may select either +implementation, and a `list[Named]` may contain both. The interpreter prints +the concrete record in a trace while labeling the binding `Named`. + +No new source spelling, cast, implementation search, or interpreter fallback +belongs in the native backend. The checker remains the authority for interface +membership and method signatures. Exact checked implementation bodies supply +native dispatch targets, including when an inherent method shares a name. + +## Implementation handoff + +1. Distinguish a concrete checked method call from an interface dispatch slot + in the checker handoff, including generic and reflected specializations. +2. Preserve interface coercions explicitly through HIR and MIR at declarations, + assignments, arguments, returns, aggregates, collection elements, and sums. + The existing compatibility relation also admits nested container and function + types; those boundaries must be covered, rather than supporting parameters + alone. +3. Use a context-owned erased value carrying the concrete payload, its checked + type identity, ownership, pending depth, and typed debug layout. Dispatch + selects compiler-generated targets from that identity. Native code must not + rediscover implementations by source spelling at runtime. +4. Preserve the current move/view behavior of interface bindings and release + every owner on overwrites, failures, and normal exits. Debug observations must + format the concrete payload with recursive secret redaction. +5. Add linked differential coverage for multiple implementations, parameters, + returns, stored values, collections, callbacks, clone/view behavior, pending + values, and cleanup. Only then close this release gate. + +Clean host packaging checks run independently while this semantic gap is open. + +## Implemented foundation and remaining work + +HIR inserts interface coercions at typed boundaries and synthesizes dispatchers +from exact checked implementation records. Native boxes own a cloned concrete +payload plus its type identity and debug layout; pending depth belongs to the +outer box. Clones own independent payloads, and allocation failure releases +partially initialized boxes. The interpreter now uses concrete nominal field +types when debugging a value declared as an interface, so secret fields remain +redacted on both paths. + +Linked differential coverage includes primitive and nominal payloads, mixed list +literals, record/enum/machine fields, actor state and messages, parameters and +returns, explicit clones, nested pending values, typed callbacks, generic bodies, +consumed-binding snapshots and reassignment, and terminal dispatch failure. +Existing list/map/optional/result values convert through compiler-described trees, +including nested containers, pending payloads, empty values, and later insertion. +Allocation-failure tests verify cleanup of partial lists, maps, sums, and boxes; +malformed conversion descriptors fail before touching source storage. +Explicit comptime values with unambiguous checked primitive or nominal payloads +materialize as typed boxes, including mixed lists, secret fields, and pending +wrappers. Generic structs retain their instantiated identity for typed observation +and comptime materialization; other ambiguous erased payloads still fail closed. + +Generic struct values must retain their instantiated type separately from their +base display name. Interface erasure must not discard secret type arguments: +`Box[secret[string]]` still redacts its field when observed as `Named`, including +after cloning, pending tasks, and explicit comptime evaluation. Construction +resolves the current invocation's type arguments rather than consulting a shared +expression-span map, which can describe a different generic instantiation. +This metadata supplies typed observation and comptime materialization; it does +not introduce structural equality or change the printed struct name. + +This does **not** close the full interface gate. Still required: + +- Verify the selected rejection of equality operators on erased interface + values and the retained explicit domain-comparison path in coherent-revision + native gates. See + [interface value equality](../../open_design/interface_value_equality.md). +- Resolve erased calls whose non-receiver arguments require the implementation's + concrete owner. See + [same-owner arguments](../../open_design/interface_same_owner_arguments.md). +- Audit interpreter source instances of compiler-owned facades that use + different native helpers and lack checked source bodies. +- Audit remaining comptime combinations; reflected interface fields and lists + now have record, enum, and machine round-trip coverage. +- Resolve actor handles escaping comptime evaluation; a saved interpreter actor + ID currently has no corresponding runtime instance. See + [comptime actor values](../../open_design/comptime_actor_values.md). +- Audit refinement compositions beyond the covered primitive, list, map, set, + optional, result, record, enum, machine, bitfield, function, builder, and + capability bases. + +Generic struct interface dispatch uses the concrete instantiated owner retained +on the value. Implementations for `Box[int64]`, `Box[string]`, and +`Box[secret[string]]` must coexist without overwriting each other. Namespace +qualification and transparent aliases normalize before matching. Concrete method +aliases use the fully instantiated owner, including through a transparent type +alias such as `type IntBox = Box[int64]`. An inherent method keeps its own source +identity. A bare generic owner such as `Box.name` has no instantiated type and must +receive the same missing-type-arguments diagnostic as other incomplete generic +types. Use a concrete type alias for a method call or value; inferring the owner +from a receiver would introduce a new inference rule. The checker must reject +this source before HIR lowering instead of silently returning an error type. Pending interface receivers retain the +existing failure behavior until joined. + +Comptime callback materialization must reconstruct the evaluated source function +with its own checked signature before adapting it to the expression's result +type. Re-labeling the source descriptor with the target signature is invalid. +Exact signature selection remains authoritative for specialized candidates; when +erasure leaves only one source candidate, materialize that candidate and retain +the conversion explicitly. Ambiguous source identity must fail closed rather +than pick an arbitrary specialization. Generated materialization conversions +must run through the same HIR adapter pass before ownership lowering, reusing +existing adapters and processing newly generated adapter bodies. + +Actor constructor parity follows the interpreter's existing two environments: +constructor parameters remain captured by the actor, while state initializers +receive independent working values. Native lowering must establish retained +copies before evaluating initializers, so an initializer may consume its working +binding without invalidating the later generated actor allocation. This is +compiler-owned capture storage, not implicit source-level copying of interfaces. + +## Callback compatibility decision + +The checker previously compared function parameter types covariantly: it accepted +`function(User)` as `function(Named)`. Calling the resulting value with another +implementation can expose interpreter behavior for a value that violates the +callee's checked nominal type. The user selected safe callback substitution: +compare parameters contravariantly and returns covariantly. Reject unsafe +parameter widening and accept safe narrowing. Native adapters now preserve +borrowed/owned arguments, covariant returns, captured closures, stored callbacks, +higher-order callback parameters, debug names, and pending depth. A context-owned +adapter descriptor retains the original function and invokes a generated typed +body; no interpreter or source-name lookup participates in native execution. + +Whole-container callback conversions use the same generated signature adapters. +HIR retains each required source/target signature and its generated function ID +on the conversion. Reachability and verification include these functions. The +native conversion descriptor contains link-time function-address relocations; +runtime conversion wraps each callback while preserving captures, debug labels, +pending state, and allocation-failure cleanup. Nested list/map/optional/result +conversions do not perform runtime type inference or implementation lookup. + +Linked differential coverage now exercises container callbacks with captures, +contravariant parameters, covariant returns, secret lifting, pending functions +and containers, empty insertion, and adapters whose own parameters need nested +container conversion. Runtime tests walk every allocation failure through list, +map, and optional callback conversion; object tests reject missing or mismatched +adapter metadata and require native data relocations to reachable adapter bodies. + +Comptime callback materialization now retains an unambiguous source function's +checked signature and applies explicit adapters afterward. Linked regressions +cover named choices, captured closures, callbacks in lists and records, pending +callbacks, covariant returns, and secret parameters/results. Completing generated +conversions is idempotent and shares already emitted adapters, avoiding duplicate +native symbols when comptime evaluation recreates an existing conversion. + +Generic closures must capture their lexical type bindings and the enclosing +function's concrete type arguments, including reflection-visible alias metadata. +The source body span and callable signature alone do not identify a specialization: +two instances may differ only in capture types or a type-dependent operation. +Calling a closure restores its captured type context, and materialization matches +that context against checked HIR identity before considering signature adapters. + +Generic closure regressions now cover multiple concrete instantiations sharing a +body and callable signature, aliases of an existing type, captured values, empty +capture environments with type-dependent calls, nested factories, and calls from +a different generic context. Interpreter restoration also runs after argument +normalization failure. Boxing the captured type context and separating actor and +closure construction helpers preserve the stack budget for recursive JSON fixtures. + +Named function invocation resolves explicit type arguments in the caller, then +executes in the callee's own type-parameter scope. A non-generic function sees +its declaration's nominal types even when a generic caller uses the same type +parameter name. Closures created by that callee capture only its lexical type +bindings. Return and argument failures must restore the caller's type scope. + +Interpreter arithmetic normalization must use the checked expression map for the +active generic specialization. The driver must retain those maps and their type +argument, alias-reflection, and reflection-parameter identities instead of +collapsing all instantiations into one span map. A named call selects its checked +map before evaluation; a closure retains that map with its lexical context. +Calls and failures restore the caller's map. Matching must reject conflicting +checked identities rather than select whichever specialization was registered +last. Reflected loop body expansions still require their own scoped selection. + +The `generic_integer_wrapping` linked regression now covers signed and unsigned +closure arithmetic, named generic calls, and explicit comptime results. Checked +maps survive closure capture; argument failures restore the caller's map. +Generic spans are absent from the global fallback, including spans checked for +only one concrete instance. Compiler-owned facade instances without matching +source facts retain source interpretation without borrowing another instance's +map. Conflicting matching maps are rejected in either registration order. + +Comptime closures retain the complete selected checked function context through +shared ownership. Native materialization compares reflection-parameter facts as +well as type arguments and alias metadata. The linked +`comptime_reflection_callbacks` fixture distinguishes closures sharing one body, +signature, and type arguments using `TypeKind`, `TypePrimitive`, and both kind +and primitive facts from `TypeInfo` parameters. Captured ordinary values survive +selection, and moving a reflection parameter before closure creation is valid. + +Every concrete generic invocation is now checked regardless of where reflection +expressions occur. Interpolation, nested call arguments, and runtime or baked +closures retain checked bodies for native lowering. The user selected a +compile-time error for invalid `comptime type` bindings behind runtime guards; +`generic_reflection_runtime_guard_deferral` is now a compile-fail fixture. +`generic_reflection_body_type_error` rejects a mismatched generic return type, +and `generic_reflection_expressions` compares interpreter and native output. + +Reflected-loop callbacks now carry their lexical `comptime type` binding chain +through inline HIR identity, evaluated closures, and native symbol encoding. +Previously a list of baked callbacks returning `type.name[Field]()` for different +record fields failed native materialization as ambiguous. Named calls start +their own lexical chain; nested closures retain their defining chain. The +`comptime_reflected_callbacks` differential fixture covers runtime and baked +callbacks, repeated field types, a nested direct binding inside a generic +reflected loop, and callbacks that produce another callback. + +Scoped expression facts now follow the checker's recursive `comptime type` +body snapshots. Each binding selects facts for its concrete type; nested +bindings remain under their parent, and flat fallback maps exclude their +expression spans. Closures retain the selected scope through shared ownership, +while named calls isolate it and restore it on return or failure. Conflicting +matching scope facts fail rather than choosing an expansion by registration +order. `reflected_integer_wrapping` checks signed and unsigned field widths +through ordinary and generic functions, nested bindings, and runtime or baked +callbacks, including repeated field types. + +The scoped alias audit found and fixed native dispatch merging `Label` (an +alias of `string`) with `string`: it printed `alias`, `alias` instead of +`alias`, `primitive`. Checked bindings now export their source-visible +reflection metadata. Scoped expression selection, callback materialization, +native symbols, and runtime dispatch preserve that metadata. The dedicated +alias-preserving reflection match leaf keeps the earlier alias-transparent +identity operation available. Argument-loop facts retain alias metadata from +direct `TypeInfo` sources, fields, and other argument loops. +`reflected_alias_callbacks` exercises runtime and baked callbacks, repeated +aliases, distinct aliases of the same base, nested container aliases, direct +`type.arg` bindings, and nested argument sources. + +The alias regression also covers a generic callee receiving a scoped +`list[Small]` argument where `Small` aliases `int8`. Nested source spellings +may have no global reflection-table entry, so argument selection now retains +metadata from the lexical scope or from the interpreter's existing type +reflection representation. This prevents a missing callee context from +evaluating `127 + 1` as 128 instead of -128. + +Settled reflection guard policy: a valid `comptime type` binding is allowed +under an unknown runtime guard when every branch typechecks. An invalid binding +still fails at compile time, regardless of the runtime condition. Unknown tags, +predicate calls, and detached boolean locals do not provide type evidence. +The former `generic_reflection_guarded_unknown_fact` and +`generic_reflection_match_unknown_fact` negative fixtures become compile-pass +fixtures; runtime coverage exercises both outcomes of each guard. + +Known reflection branch selection also applies inside closures and after +interpolation or other reflection expressions. The checker no longer uses a +whole-body placement classifier to decide whether these facts are available. +`closure_reflection_guards` compares runtime and baked callbacks with scalar +and list instantiations. `closure_reflection_boolean_boundaries` keeps +predicate-call and detached-bool casts rejected inside callbacks. The +`generic_reflection_unknown_guard_invalid_binding` negative fixture checks +invalid lookups in unknown match arms and closures, complementing the existing +runtime-if regression. + +Primitive interface identity implementation: the interpreter must retain the +checked concrete type when a primitive carrier would erase it (integer widths, +float32, and nominal refinements). This is value metadata, preserved by cloning, +containers, pending results, captures, and comptime constants. Primitive +operations inspect the payload; interface dispatch and comptime materialization +inspect the retained identity. Transparent aliases canonicalize to their base, +and an interface-typed boundary must preserve the incoming concrete identity. +Typed debug output must keep recursive secret redaction. This restores the +already checked dispatch contract without changing source typing or arithmetic. + +The primitive identity regression now covers every signed and unsigned width, +float32 versus float64, nominal integer/string refinements, and a transparent +primitive alias in a namespace. Runtime and baked lists, literal element types, +callbacks, cloning, pending values, list/optional/map conversions, and reflected +interface fields retain the checked identity. Native materialization resolves +that retained name to a unique checked type instead of guessing from the carrier. +Primitive operations borrow the payload; container insertion and cloning keep +the whole value. The broader fixture suite caught and pinned byte-list consumers +that also need to inspect their element payloads. + +Collection identity implementation follows the same checked contract as the +primitive fix: implementation owners use the full canonical type, and erased +collections retain that identity. Concrete collection conversions retag the +outer value for the destination type while preserving element identities; +interface boundaries preserve the incoming identity. Register concrete owners +explicitly so unrelated runtime collections need no extra wrapper. Collection +operations and sum/aggregate projections inspect payload storage without changing +stored element values. Non-primitive refinements with interface implementations +must retain their nominal identity by the same mechanism. + +`interface_collection_identity` now covers distinct list/map/set/optional/result +implementations, empty and failed sums, list and record refinements, nested +collection conversion, typed insertion, loops, sorting, alias methods and method +values, reflected collection fields, callbacks, cloning, pending values, and +baked values. Secret element types survive erasure and remain redacted. + +The expanded regression exposed two checker handoff gaps. A namespace-local +alias method must resolve its canonical owner before recording its source +method target; otherwise valid calls and method values reach HIR without their +checked target. A `run` initializer must preserve its expected type context; +otherwise `run ok(17)` leaves a local with `result[int64, ]` while later +uses carry the declared `result[int64, string]`. Dedicated checker tests pin +both behaviors, including nested pending values, empty containers, and narrow +integer literals. Interpreter registration also retains interface declarations +for namespace qualification inside canonical collection type arguments. + +Function interface identity follows the checked function signature, including +parameter view modes and the result type. Runtime and baked callbacks must retain +that signature across erasure without losing their evaluated source declaration, +captures, or generic context. A checked function conversion changes the exposed +signature while preserving the source body used by the adapter. Calling the +value unwraps identity metadata and invokes that body; interface dispatch uses +the retained signature. Canonical type parsing must preserve nested function and +container arguments rather than treating a whole signature as an identifier. + +`interface_function_identity` covers distinct scalar widths, string and list +returns, owned and view parameters, nested function parameters, named functions, +captured closures, and generic closure specializations. Runtime and baked mixed +interface lists, cloning, pending functions, reflected function fields, and +contravariant parameter/covariant return adapters retain the exposed signature +and evaluated source body. Its native stdout and trace output match the +interpreter, including explicit comptime erasure after a callback conversion. + +Machine interface identity must distinguish the checked bare machine type from +each state-qualified type when both have implementations. A value checked as +`Session at active` dispatches to that implementation; converting it to `Session` +uses the bare implementation. Keep the actual state and fields in the payload, +and preserve the exposed owner through erasure and explicit comptime evaluation. +The interpreter retains the qualification so a state-qualified value selects +its own implementation. +Checked flow narrowing inside a state guard must also supply the exposed state +type even when the binding was declared with the bare machine type. Native +materialization must construct the actual state and then bind it at the requested +bare type before wrapping a refinement or interface; returning a state-typed +constructor in place of the requested bare value changes dispatch and violates +the refinement base contract. + +`interface_nominal_identity` covers bare machines and distinct state-qualified +implementations, aliases, transitions, guarded narrowing, pending values, +reflected state-qualified fields, and enum/machine/bitfield refinements. Runtime +and baked mixed interface lists retain the same dispatch and recursive secret +redaction. Materialized bare machines use an explicit typed local after state +construction, preserving both the requested owner and validated refinement base. + +Explicit comptime construction builders are accepted pure values. Native +materialization must recreate the evaluated owner, selected variant or state, +provided field order, and stored payloads from checked reflection metadata. It +must retain incomplete builders and defer refinement/width validation until the +same runtime finish boundary as an ordinary builder. Reconstruction may use the +existing typed builder intrinsics, but must not execute the source computation +or invent layouts from source spellings. Nested, pending, and erased builders +must retain the same ownership and recursive secret redaction contracts. + +`comptime_builders` covers empty and partially filled records, generic and +alias-bearing owners, nested builders, variant/state builders, bitfields, +cloning, lists, stored values, pending and erased builders, and provided fields +in a different order from the declaration. Missing-field, refinement, and width +errors remain deferred to runtime finish. Source reflection operands are +exported separately from canonical metadata; equivalent aliases compare by +their checked type ID without discarding the builder's source owner name. + +Generated locals holding baked bare-machine values are constant seeds. Each +evaluation clones its seed so an expression inside a runtime loop creates an +independent owned value on every iteration. Moving the seed consumed it after +the first iteration and incorrectly failed native ownership validation. + +The facade identity audit includes JSON parsing and round trips through numeric, +collection, refinement, generic record, enum, bitfield, and machine helpers, plus +the compiler-owned math facades. Its nested explicit comptime calls expose a +caller-stack dependency: native compilation on a normal Rust test thread can +overflow even when the same program builds from the CLI. Explicit evaluation +must use the existing 8 MiB interpreter stack budget on its own scoped worker, +without increasing that budget or moving any evaluation to runtime. Modules +without explicit comptime expressions need no evaluation worker. + +`interface_facade_identity` now matches native stdout and recursive secret +redaction for runtime and baked JSON parser results, enum/bitfield/machine JSON +round trips, and math facade results. It passes from the ordinary native test +thread after explicit evaluation adopts the shared interpreter stack budget. + +The `interface_math_facade_contexts` probe covers both allowed numeric owners +for every abs/min/max facade, secret taint from either operand, generic wrappers, +captured public-value callbacks, entirely baked collections, and baked callbacks +called at runtime. All preserve integer/fraction/secret interface dispatch in +linked debug and release artifacts after source removal. This probe exposed an +independent MIR capacity error: owned indirect-call results were omitted from +temporary planning. The planner now reserves each result in addition to the +callee descriptor and its arguments. A companion matrix covers the other owned +return families and terminal failure after earlier callbacks produced owners. + +`interface_refined_sums` audits optional/result refinements containing erased +interface payloads. Direct whole-sum validation, inherited nominal owners, +field construction, function return adapters, pending joins, and ordinary/baked +values retain the inner concrete implementation. Absent and rejected failure +branches exercise validation independently from payload extraction. Refinement +errors keep their string type when the base result's errors are integers. + +Opaque carriers must also retain their checked concrete owners when erased to +interfaces. Distinct actor declarations and refinements of `TypeConstruction` +already dispatch independently in native code. Interpreter metadata must preserve +those owners while actor messages and reflected construction operate on the +underlying handle or builder payload. Converting a refined builder to its base +selects the base implementation; erasure preserves the exposed refinement. +Capability-backed refinements use the same checked owner rule. Explicit comptime +builders nested inside a validated refinement still need their reconstruction +handlers extracted into MIR control flow; the validated wrapper must not hide +those handlers from lowering. + +`interface_opaque_identity` exercises distinct actor declarations, cloned and +stored actor references, messages after annotation, pending actors and refined +builders, runtime and baked refined builder erasure, and capability refinements. +Builder completion explicitly coarsens the refinement as required by the checked +intrinsic signature. MIR extracts reconstruction handlers through +`RefinementValidated` so baked refined builders remain ordinary native values. + +Machine state field types follow ordinary declaration-site name resolution, +including namespace qualification, visibility, and declaration order. The +resolver must visit them just as it visits struct and enum fields; otherwise +an interface field accepted at the root fails inside a namespace before native +lowering. Reflected fields preserve erased owners through builder round trips, +including stored lists, secrets, and explicit comptime construction. + +`interface_reflected_fields` checks direct interface fields and interface lists +in records, enum payloads, and machine states. It compares ordinary round trips, +fully baked results, and baked builders completed at runtime; all preserve +integer-width dispatch and recursively redact secret elements. Resolver tests +also pin nested field type qualification and reject private or forward field +type references. + +Function-backed refinements retain their nominal interface owner just like +collection-backed refinements. Alias-base classification can report the simple +carrier name `function`; that carrier must be treated as concrete, while the +value metadata keeps the full exposed refinement name. Coarsening restores the +base function signature and its implementation without changing the callable +body or captures. + +`interface_refined_containers` covers nominal map, set, optional, result, and +function owners, including both sum branches, named and captured functions, +cloning, pending callbacks, and coarsening back to the base function signature. +Runtime and baked heterogeneous interface lists match in calls and traces. + +`interface_facade_results` extends the facade audit to erased result values on +success and failure paths. Typed JSON parsing covers numeric widths, refinement +failure, secret-bearing records, field errors, and exact unknown-field checks; +raw JSON access covers malformed input, wrong-kind errors, missing items, and +successful absent optionals. Runtime and baked lists retain their full result +owner even when the active payload is only an error string, and traces preserve +recursive secret redaction. + +`reflected_opaque_fields` checks plain and doubly pending actor/capability fields +through typed field reads and builder reconstruction. Messages through rebuilt +actor references still update the original actor, and joined capabilities retain +the authority required by their provider. Exact stdout and debug comparisons +also require reconstruction to succeed; failures cannot return the original +record as an unnoticed fallback. + +The refinement audit found that an interface-backed refinement loses its +nominal method owner. `type Selected = Named where true` with its own `Named` +implementation dispatches as the underlying concrete value after erasure; +the interpreter also loses the owner on direct calls. The established nominal +refinement rule requires preserving both layers: exposed `Selected` dispatches +to its implementation, while explicit `coarsen` restores the original erased +`Named` value and that value's concrete implementation. + +Interface-backed refinements use the interface handle as their underlying +representation. Erasing one must box that handle with the refinement identity; +dispatch must unbox it before entering the refinement method. Equal underlying +representations alone cannot justify omitting those conversions. Interpreter +type metadata must retain the underlying concrete identity instead of flattening +both layers, including through comptime values, collections, and pending tasks. + +The sum-value regression also exposes a checker ordering problem: when both a +sum and its payload implement the expected interface, the checker requests a +payload `handle` before checking whether the whole sum already satisfies the +destination. Existing whole-value compatibility must take precedence. Erasing +that value preserves the sum and its implementation; only explicit `handle` +extracts its active payload. + +`interface_refined_interfaces` now covers direct and erased dispatch, intermediate +and fully coarsened ancestors, narrow primitive and secret-bearing record +payloads, sums whose payloads also implement the interface, whole-container +conversions, signature adapters, nested pending tasks, and runtime/baked values. +Its predicate calls the base interface implementation, and rejection cases +exercise both ordinary values and an unvalidated pending constructor field. + +Fresh constructor inputs keep their ancestor type until MIR runs the checked +predicate chain. Interpreter normalization similarly attaches the destination +identity after validation. Already validated pending values retain their +invariant; a new pending value cannot acquire that invariant from a type label. + +Validation for this change passed 616 compiler-phase tests, all 583 frontend +fixtures, 30 native interface/comptime/refinement tests, and workspace +documentation tests. Actor handles escaping comptime and the remaining semantic +audit still prevent a full native parity claim. + +`interface_refined_actors` covers actor-backed refinements and inherited +refinements with distinct interface implementations. Cloning, coarsening, direct +and reflected record storage, and nested pending handles preserve the original +actor's mutable state. Erased pending handles retain the exposed refinement +for dispatch after joining, while a fresh pending actor still fails a rejected +refinement constructor. A scalar-returning computation uses a refined actor +internally at runtime and comptime; no actor handle escapes evaluation. +Exact stdout and traces match, including actor identity through reconstruction. + +`interface_display_contexts` checks the exact display contract of interface +values against their concrete records, including namespaced implementations, +generic and reflected scoped types, fields, returned values, and comptime +formatting. A side-effecting receiver is evaluated once, view operands remain +usable, and explicit primitive display implementations override the default +formatter. See [display contexts](native_display_context.md). + +`interface_method_returns` checks dynamic methods returning interface values, +interface lists, and captured callbacks returning interfaces. Distinct string, +`int64`, and `int8` implementations retain their method owners, including +wrapping narrow arithmetic inside a returned collection. Callback lists created +at runtime and comptime preserve captures after the source receiver's scope +ends. The accepted method signatures explicitly return the interface types; +substituting concrete return types is rejected by the existing checker. + +Property generation uses the same concrete generic owner metadata as ordinary +record construction. A generated `list[Boxed[Tiny]]`, where `Tiny` aliases `int8`, +must dispatch through the `Boxed[int8]` implementation after interface erasure. +Shrinking a field preserves the record's instantiated identity. The +`property_generic_owners` fixture and linked replay test distinguish narrow and +string owners and require shrinking to preserve successful dispatch before the +property's intended length assertion fails. + +`interface_refined_small_values` completes linked coverage for `nothing`, +inherited unit refinements, boolean refinements, and byte-backed refinements. +Runtime and baked heterogeneous lists select each nominal implementation; +doubly pending unit values preserve their refinement through interface joins. +Coarsening and declassification retain borrowed byte storage through direct +calls, callbacks, field projections, and handlers in later arguments. Debug and +release binaries match the interpreter after source removal. A companion gate +checks terminal failure in a later argument for both call forms and wrapper +kinds, requiring exit 71 and exact diagnostics after owned-value cleanup. diff --git a/docs/completed/native_codegen/native_json_uninhabited_parse_slots.md b/docs/completed/native_codegen/native_json_uninhabited_parse_slots.md new file mode 100644 index 00000000..374c8bb2 --- /dev/null +++ b/docs/completed/native_codegen/native_json_uninhabited_parse_slots.md @@ -0,0 +1,145 @@ +# Native JSON parsing for inferred uninhabited slots + +This is a selected implementation gap under existing public JSON policy. +Unconstrained empty collections and absent sum slots retain their inferred +Never type. Actual reference execution already decodes admitted empty/absent +carriers and returns handled errors when raw input attempts to occupy a Never +slot. The characterized native parser selector omitted that slot and retained +an unsupported generic intrinsic. The applied source-selector repair passes +the focused compiler and 19-source linked gates below; coherent final +workspace/platform acceptance remains pending. + +The accepted serializer revision and predecessor workspace/platform evidence +are centralized in +[the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). +They do not validate this parser repair. + +## Bounded existing contract + +Preserve the inferred slot and use the checked trusted source decoder for an +otherwise supported target. An actual empty array or absent optional needs no +Never payload; a one-sided result can decode its inhabited arm. Occupied Never +requests return the existing unsupported-reflected-type error with the actual +index, field or result-arm prefix. Raw syntax, shape checks and parse_exact +validation retain their ordinary order. Handling this error does not make a +Never value constructible. + +There is no default element type, source Never spelling, cast, retag, fake +runtime representation, native generic JSON fallback or public-domain change. +Every inhabited slot keeps its ordinary recursive eligibility. Unsupported +functions, actors, interfaces, capabilities, resources and builders remain +rejected; Set element and string Map key checks remain unchanged. Runtime +emptiness never exempts an unsupported inhabited target. + +## Checked source and reachability proof + +Never reflection has unknown_type, no primitive tag and canonical name +``. The selected decoder's closed TypeInfo/TypeKind argument facts skip +the primitive, alias, refinement, secret and named-aggregate branches. The +existing canonical JsonTree name guard also skips its incompatible success +body. The final unknown-kind branch returns only +`fail("unsupported reflected JSON type: ")`; it starts no construction +builder and manufactures no successful payload. + +These are checked specialization facts, not optimizer folding. Ordinary pure +helpers and unchecked runtime conditions do not make an ill-typed source body +valid. The checker source-target unit uses trusted fail(raw) facades only to +pin direct/pipeline target metadata, concrete inferred slots and closed parse +identities; it does not prove the real decoder executes. + +The real decoder has JsonTree input and inhabited result[Never, string] output. +It remains a normal reachable callee. Native validation already supports the +inhabited string error arm; the existing uninhabited sum pass can remove only +impossible success extraction after retaining the decoder call and sum +observation. A nonempty raw array loops over inhabited int64 indexes and still +calls the Never decoder. Optional and result decoding skip only an unoccupied +arm. Syntax/shape/tag checks, pending errors, prefix construction, input +evaluation and owned cleanup must remain observable independently of optimizer +settings. This requires no new runtime carrier or ABI. + +## Actual pre-change characterization + +Eight direct-call sources are recorded in +`target/native-json-inferred-parse-slots-witness/results.json`; eight pipeline +counterparts are recorded in +`target/native-json-inferred-parse-pipeline-witness/results.json`. Each family +passes eight format checks, all 16 frontend profile checks, and eight reference +runs with exact application stdout and empty stderr/debug observations. An +unused generic inference anchor warning is separate from checking success. +Each family has 16 AOT generic parse/parse_exact refusals before runtime archive +lookup; all 16 existing output sentinels remain intact. None is a linked native +pass. + +The four shapes are empty list, absent optional, result with no inferred error +type and result with no inferred success type. Both APIs/forms retain the +literal occupied-slot errors: + +| Slot | Handled decoder error | +| --- | --- | +| First list element | `0: unsupported reflected JSON type: ` | +| Optional payload | `unsupported reflected JSON type: ` | +| Missing result error type | `fail: unsupported reflected JSON type: ` | +| Missing result success type | `ok: unsupported reflected JSON type: ` | + +Valid empty/absent inputs and supported result arms succeed. Malformed inputs +retain unterminated-array or exactly-one-result-key errors, as pinned in the +individual source captures. The lenient/exact outputs agree for these selected +inputs; this is not a blanket equivalence claim for the two APIs. + +Three further string-keyed map, generic record and nested-list sources are +characterized in `target/native-json-inferred-parse-recursive-witness/results.json`. +All three format checks, six frontend profile checks, three reference/agent +runs pass with exact handled errors and empty stderr/debug observations. Their +six AOT generic parser refusals occur before unavailable-archive lookup and +preserve all six sentinels. Actual occupied-slot prefixes are respectively +`x:`, `items: 0:` and `0: 0:` before the same unsupported-reflected-type error; +valid empty map/record fields or nested empty arrays succeed. Malformed inputs +retain their recorded unterminated-object/array errors. + +The runtime source gate has 19 actually characterized sources. +That source count is separate from registered Rust groups and executed native +tests; no linked pass follows from these refusals. + +## Focused acceptance and remaining gates + +The initial eight direct groups passed together in 9.14 seconds (406 filtered) +in `target/native-json-inferred-parse-direct-linked.log`. After integrating the +eight pipeline and three recursive groups, all 19 pass together in 14.08 seconds +(406 filtered) in `target/native-json-inferred-parse-linked.log`. The helper +builds both profiles with matching launchers, deletes source only +after both builds, and then requires process status zero, every exact literal +line and empty stderr/runtime/frontend/artifact observations. Errors are handled +within the program; success status must not conceal a skipped Never decoder. + +The focused checked-target unit passes once (223 filtered) in +`target/native-json-inferred-parse-focused-checker-applied.log`. The crossed +library gate passes 949 tests (41 codegen, 416 comptime, 134 HIR, 86 MIR, +48 resolve and 224 typecheck) in `target/native-json-inferred-parse-phases.log`. +The driver gate passes all 83 library tests in 72.25 seconds and all 598 frontend +fixtures pass in 23.41 seconds, recorded in +`target/native-json-inferred-parse-driver.log` and +`target/native-json-inferred-parse-frontend.log`. These linked gates exercise +the real trusted stdlib decoder, separately from the metadata unit's stub. + +All 19 new Jett format checks pass in the structured +`target/native-json-inferred-parse-format.log`; Rust format and diff checks also +pass. The corpus now registers 425 supplemental tests; this is separate from +the executed 19-test focused gate. The coherent 08f9f7e7 parser workspace +subsequently passes all 425 native tests, 182 object obligations and remaining +workspace/doc-tests with unchanged source and clean start/end. Exact logs and +counts are centralized in [the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). +Its supported-host workflow also passes all four build/package and relocated +installed-package jobs. This accepted parser predecessor does not establish +later bitfield or secret-owner workspace/platform acceptance. + +The initial focused-unit log selected zero tests and is not accepted evidence; +the applied-unit log above records the actual pass. The source application +guard also detected a serializer-unit formatting-only baseline difference; +reviewed current hashes and patch context checks resolved it before application. +Neither event changes the selected parser behavior or validates an extra test. + +The bitfield payload alias candidate is an independent later repair. This slice +does not settle open actor/interface/debug/lifetime/global-constant/list.sum or +reflected-request policies, implement future providers, or change the fixed +207 inventory. The objective remains whole-language 100% native parity, tracked +with the unchanged about-85% coarse estimate. diff --git a/docs/completed/native_codegen/native_json_uninhabited_slots.md b/docs/completed/native_codegen/native_json_uninhabited_slots.md new file mode 100644 index 00000000..d954a368 --- /dev/null +++ b/docs/completed/native_codegen/native_json_uninhabited_slots.md @@ -0,0 +1,157 @@ +# Native JSON for inferred uninhabited slots + +This is a selected implementation gap, not an unresolved JSON type-domain +decision. Public JSON policy already admits the uninhabited type inferred for +an unconstrained empty collection or absent sum slot. The reference serializer +executes its checked trusted stdlib body. The characterized native selector +omitted Type::Never and left otherwise admitted targets on unsupported generic +JSON intrinsics. The applied serializer repair passes its compiler, linked, +ownership and public-policy gates below. The accepted `774b13e7` complete +workspace predates this repair. The `05494ed4` serializer revision now passes its +complete frozen workspace and all four supported-host jobs, centralized in +[the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). + +## Bounded contract + +Preserve the inferred Never slot and select the existing source serializer for +an otherwise supported carrier. This includes empty lists, absent optionals, +one-sided results, empty string-keyed maps, and such slots inside nested records +or collections. Every other slot retains its ordinary recursive eligibility and +public policy. Runtime emptiness does not permit unsupported functions, actors, +interfaces, capabilities, resources, TypeConstruction, non-string map keys, or +secret data outside the existing public projection rule. + +Do not choose a default element type, introduce an inhabited Never value or +source spelling, add a fake runtime representation, or substitute a generic +native JSON implementation for the checked stdlib body. Parser source selection +is a separate follow-up: empty and nonempty inputs, exact validation and failure +order require their own characterization. No parse selector change is selected +by this serializer repair. + +Generic source bodies remain checked before HIR omits impossible list/map loop +bodies. Existing MIR sequence preparation retains carrier evaluation and the +length/pending check before bypassing an impossible element read. Existing sum +preparation retains the sum observation and pending check before bypassing an +impossible extraction. Bare Never specializations are not implicit project +callable roots, but retained calls or function references still require ordinary +verification. Inhabited carriers are verified normally. These preparations run +before native verification and independently of optimizer settings; ordinary +pure-call folding cannot make an ill-typed source body valid. + +## Actual pre-change characterization + +The original three-source family is recorded in +`target/native-next-after-qualified-machine/results.json`: six frontend profile +checks and three format checks pass; all three reference runs succeed with +application stdout `[]\n` and empty stderr. The inferred serialize and +serialize_public calls have four native AOT refusals across both profiles, +each reporting the unsupported generic serializer before archive lookup. The +explicit list[int64] control instead reaches the deliberately missing runtime +bundle in both profiles. Those missing-bundle controls are not linked passes. + +The seven additional slot witnesses are recorded in +`target/native-json-inferred-slots-witness/results.json`: all 14 frontend profile +checks and seven format checks pass. Six reference runs succeed with empty +stderr/debug events and these exact application bytes: + +| Carrier | Application stdout | +| --- | --- | +| Absent optional | `null\n` | +| Result with absent error type | `{"ok":7}\n` | +| Result with absent success type | `{"fail":"bad"}\n` | +| String-keyed empty map | `{}\n` | +| Generic record containing an empty list | `{"items":[]}\n` | +| Nested empty list | `[[]]\n` | + +The seventh reference run evaluates a depth-two pending empty list. It preserves +application stdout `before\n`, emits no debug event, and fails with +`runtime error: for loop requires a list, string, map, or set value`; it never +writes the later JSON line. All 14 additional AOT attempts still fail at the +unsupported generic serialize or serialize_public intrinsic. No linked native +success or pending-error parity is proved by these pre-change refusals. + +### Pure source and public-policy controls + +Six additional fixtures are characterized in +`target/native-json-inferred-slots-extra-witness/results.json`. All six format +checks pass. The two positive fixtures pass four frontend profile checks and +reference execution with empty stderr/debug events: the pure direct/pipeline +facades produce `[]:[]:[]:[]\n`, and public projection of a record's secret field +produces `{"items":[]}\n`. The pure fixture also passes its reference test run: +one verify block and one property with 100 trials, two of two passed, no debug +observations. Explicit comptime evaluation remains closed and pure. + +The four negative fixtures fail all eight frontend profile checks with one +error apiece. Two unused-variable warnings in the ill-typed-body fixture are +separate from its sole E0311 error: + +| Control | Sole error | +| --- | --- | +| Ordinary serialization of an empty function-valued list | E0347 | +| Public serialization of an empty function-valued list | E0347 | +| Public serialization of an empty secret-wrapped element list | E0603 | +| Fabricating int64 as inferred Never inside an empty loop | E0311 | + +All 12 AOT attempts preserve an existing output sentinel. The positive four +still stop at unsupported generic serializers before archive lookup; the +negative eight stop at their typed frontend policy/source error. These are +pre-change controls, not linked native passes or new public exemptions. + +The separate owner-reread witness in +`target/native-json-inferred-slots-owner-witness/results.json` passes its format +check, both frontend profile checks, and reference execution with exact +`[]:[]:{"items":[]}:0:0:0\n`, empty stderr and no debug events. It borrows a +projected list from Envelope[T], calls both serializers, serializes an owned +whole-record clone, and rereads the alias, owner field and original list. Both +native profiles still refuse the generic serializer before archive lookup and +preserve their existing output sentinels. Ownership parity for this repaired +native path is established by the linked owner-reread gate below. + +## Focused acceptance and remaining release gates + +The production change adds `Type::Never` to serializer eligibility only; +parser and public policy remain unchanged. Its checker regression pins inferred Never rather +than a default primitive, direct/pipeline source targets, a concrete list[int64] +control, closed intrinsic identity, unsupported inhabited function targets, +and rejection of an ill-typed body inside an empty loop. + +The focused checker regression passes in +`target/native-json-inferred-slots-focused-checker.log`. All 14 new driver +tests pass together in 8.09 seconds (392 filtered) in +`target/native-json-inferred-slots-linked.log`: the ten original carrier controls, +combined pure main/verify/property suites, public projection, grouped typed +negative publication controls, and owner rereads. The 13 runtime sources build +with matching debug/release archives and execute after source deletion. Exact +application bytes and empty debug/frontend observations are pinned. The pending +case exits 71 with exactly `before\n` and the recorded terminal message plus LF; +an exit-72 cleanup override cannot pass. The pure verify and 100-trial property +binaries also execute after source deletion in both profiles with empty output. +The four-source negative group retains its sole error code and existing output +sentinel before missing-archive lookup in both profiles. + +The crossed library gate passes 948 tests: codegen 41, comptime 416, HIR 134, +MIR 86, resolve 48 and typecheck 223, recorded in +`target/native-json-inferred-slots-phases.log`. The initial invocation used the +nonexistent package name `jett_codegen` and executed no tests; the actual +`jett_codegen_cranelift` package was then tested successfully. That failed +invocation remains in `target/native-json-inferred-slots-phases-package-error.log`. +Driver library checks pass 83 tests in 66.79 seconds in +`target/native-json-inferred-slots-driver-frontend.log`; all 598 frontend +fixtures pass in 21.73 seconds in `target/native-json-inferred-slots-frontend.log`. +All 17 byte-identical characterized Jett sources pass formatting, along with +Rust formatting and whitespace checks, in `target/native-json-inferred-slots-format.log`. + +The accepted `05494ed4` revision registers and executes all 406 supplemental +native tests, along with the complete workspace and all four supported-host +jobs. The audit centralizes its exact results; these accepted serializer gates +do not validate later source changes. The separately characterized parser gap +and its pending repair are recorded in +[the parser contract](native_json_uninhabited_parse_slots.md). + +The independent current machine checkpoint and predecessor workspace/platform +evidence are in +[the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). +The whole-language objective remains 100%, the broad planning estimate remains +about 85%, and the fixed inventory remains 207 fixtures. This slice does not +settle the open actor/interface/debug/lifetime/global-constant/list.sum or +reflected-request policies, or implement future reference providers. diff --git a/docs/completed/native_codegen/native_local_view_aliases.md b/docs/completed/native_codegen/native_local_view_aliases.md new file mode 100644 index 00000000..c79d5e00 --- /dev/null +++ b/docs/completed/native_codegen/native_local_view_aliases.md @@ -0,0 +1,392 @@ +# Native local view aliases + +Local aliases of views are an admitted source form with a bounded native +implementation. This note records its handoff and remaining limits. The +[temporary-backing decision](native_temporary_local_views.md) selects a later +lexical lifetime contract; its implementation and cleanup gates are pending. +Historical receipts below do not establish that extension or complete native coverage. + +## Settled behavior and current gap + +[Rule Set 24](../../design.md#rule-set-24-read-only-views-solving-the-memory-borrowing-problem) +makes views read-only borrows: they can be read, passed as views, and explicitly +cloned into ownership. They cannot be consumed or escape their scope. For example: + +```jett +function duplicate(view source: list[int64]) returns list[int64]: + list[int64] borrowed = view source + list[int64] forwarded = borrowed + return clone forwarded +``` + +Previously the checker tracked these declarations by resolved identity without +transporting their borrow mode into HIR. MIR analyzed `Let` as an owning +expression and gave ordinary aggregate locals owning drop slots. Allowing the +initializer alone would transfer or drop a borrowed handle incorrectly. +Implicitly copyable results, explicit clones, and ordinary owned field copies +retain their existing behavior. + +## Immutable local alias domain + +The bounded implementation admits direct and forwarded immutable aliases rooted +in a stable owned local/owned parameter or immutable view parameter, used for +borrowed reads, view calls, and explicit clones. A mutable owned backing requires +its exact active checked ownership fact. The mutable-owner extension has bounded +reference and Source-deleted MSVC acceptance recorded below. +Preserve the checked type, including secret and refinement identity. Do not infer +view-type semantics from named aliases or turn arbitrary call results into views. + +Carrier-preserving `coarsen` and `declassify` bindings retain their checked backing +chain through MIR lowering. Only a destination with a valid borrowed origin and +a validated transparent initializer takes this path. Ordinary owning expressions +keep their existing snapshots; an explicit `clone`, handler, or allocating +conversion cannot be reinterpreted as an alias initializer. + +1. Export typed binding facts through ordinary, generic, and reflected comptime + body handoffs. Facts must belong to the concrete instantiation: the same + generic source span can describe a scalar copy or an aggregate borrow. +2. Represent borrowed local bindings and their source owners explicitly in HIR + and MIR. Forwarded aliases retain the original owner dependency. Preserve + these facts through handler extraction, generated functions, and ID remapping. +3. Carry persistent loan dependencies across statements and CFG edges. Extend + owner liveness for alias reads, validate owner availability, and preserve + borrowing in matches, sum handling, iteration, and debug observations. A view + binding must never become an owning move merely because its name is local. +4. Give borrowed handles non-owning native storage. Only actual owners enter + drop planning; normal exits, branches, loops, and terminal failures must clean + them exactly once after dependent reads. Existing borrowed-handle operations + may suffice, but runtime requirements must be verified rather than assumed. + +Validate alias chains, lexical shadowing, generic specializations, nested control +flow, pending values, clone independence, debug reads, and failure cleanup with +linked reference/native comparisons and ownership-plan regressions. + +MIR retains a may-created alias set across CFG joins. Once a reachable path has +created an alias, consuming or rebinding its root produces an explicit native +implementation error, including after the last alias read. This conservative +boundary avoids choosing lexical or last-use loan expiry. Alias reads keep +their immediate origins and ultimate owner live; aliases never receive owning +drop slots. Function descriptor aliases follow the same rule even though native +function storage otherwise permits copied descriptors. + +Handler staging follows exact formal view modes for direct and indirect calls. +It snapshots a borrowed argument before a later handler and observes a callback +after its arguments. Qualified string and descriptor snapshots acquire their +own retained or cloned handles. Ordinary copy-owned alias expressions also +acquire ownership before entering a list, sum, or record, while borrowed +arguments keep raw handles. Plain string view expressions preserve existing +copy semantics in owning contexts. + +## Stable immutable ordinary-struct field paths + +The [typed stable-field contract](native_stable_projected_local_views.md) records +this bounded extension before compiler integration. Rule Set 24 already permits +readonly field views, forwarded aliases, and explicit clones. In this original +checkpoint, a field of an immutable owner is a stable selected payload even +though its type differs from the whole owner's type; incidental reference cloning +is not its ownership +contract. This extension transports and validates the existing borrow rather +than selecting a new lifetime rule. + +An immutable local or immutable view parameter may back a finite field path. +The existing immediate binding dependency remains the source of owner liveness; +the initializer's typed `Field` nodes prove the path. Each step checks the exact +ordinary-struct owner, canonical field index, declared intermediate/endpoint +type, and terminating source-local identity. Forwarding from a projected alias +keeps that immediate alias dependency and its original owner chain. The checked +endpoint retains its type, including secret/refinement qualifications and +existing interface erasure. Bytes and lists are concrete witnesses, not an +endpoint allowlist; other endpoint operations retain their existing admission. + +Only borrowed field access initializes this non-owning storage. Ordinary field +copies and explicit clones remain independently owned. No implicit clone, new +runtime symbol, hidden temporary owner, or duplicate path metadata is needed. +The whole owner stays initialized/live for all dependent aliases, including +across CFG edges, while aliases receive no owning cleanup slot. Existing field +access preserves pending checks on every ancestor and endpoint. Transparent +`coarsen`/`declassify` bindings retain the existing carrier/erasure restrictions. + +Removing the old whole-owner/endpoint equality check requires typed validation +of every alias initializer before optimization or compaction, including unused +locals and unreachable blocks. Source facts must belong to their concrete +ordinary, generic, reflected, or generated body; copied origins from another +function cannot become borrowed captures. Invalid fields, types, unavailable +roots, or broken local chains remain handoff failures. + +The initial source package has six successful candidates/controls, one terminal +argument-failure cleanup case, and two frontend-valid boundaries. Its reference +records are `target/native-stable-projected-view-preflight/results.json`; the +five projected candidates failed native HIR before this repair, independently +of the two already supported ownership controls. + +An additional frontend/reference-checked fixture covers a borrowed struct field +followed by its own bytes field, scalar versus move-only `Carrier[T]` bodies, +and explicit `declassify`/`coarsen` operations on secret/refinement field aliases. +Its exact three-line source oracle passes in +`target/native-stable-projected-view-driver-draft/10_typed_endpoints.preflight.json`. +After integration and nominal-proof review, all 12 local-alias driver tests pass +(366 filtered, 11.78 seconds) in +`target/native-stable-projected-view-final-linked.log`. This includes eight +linked cases with matching debug/release runtime archives and source removal, +the mutable/temporary publication-sentinel group, and the three existing broad +alias tests. Successful clones retain independent ownership; later argument +failure preserves exact output, status 71, and cleanup. + +All 896 compiler-phase library tests pass in +`target/native-stable-projected-view-final-phases.log`: 41 codegen, 416 comptime, +131 HIR, 86 MIR, and 222 typechecker tests. The nominal guard additionally rejects +invented or discarded refinement proofs while retaining explicit existing +ancestors, exact secret declassification, and outer secret promotion. An initial +negative-test lookup used the exported definition map for an unused type; the +corrected test reads its actual interned type and passes in the final batch. + +The final driver/frontend batch passes all 83 driver library tests (79.46 +seconds) and all 598 frontend fixtures (22.80 seconds) in +`target/native-stable-projected-view-final-driver-frontend.log`. The diagnostic +capture regression previously used a now-supported stable field alias as its +HIR failure trigger; it now uses the already excluded mutable-root case while +preserving exact comptime/verify observations and diagnostic transport. +The Linux-only projection-loan regression now tests real owner consumption +after alias creation instead of rejecting the alias declaration itself. Its +Windows target discovers zero tests, so Linux CI still must execute that gate; +the same source has an actual Windows native-object refusal in +`target/native-stable-projected-view-owner-conflict.json`. + +The ordinary-struct checkpoint registered 378 native tests; its subsequent +frozen a0bf12df workspace passed all local gates. Focused results and the +later workspace acceptance remain separate evidence. Before the exact-state +follow-up below, a machine-field alias was frontend/reference-valid but +failed the struct-only proof, as characterized in +`target/native-next-after-projected-view/root_machine_field_alias.preflight.json`. + +## Exact state-qualified machine field follow-up + +Status: integrated; focused compiler and linked gates pass. New-source full +workspace and supported-host acceptance remain pending. Rule Set 24 and the +existing exact-state payload rule already permit the read-only source form. + +The initializer's existing Field proof now accepts a declaration-backed +MachineState owner. It resolves the exact machine/state and payload fields, +then checks the zero-based FieldId, exact endpoint, base-owner type and actual +terminating local identity/type. Struct-to-machine and machine-to-struct paths +compose through the same finite tree. Every initializer, including unused ones, +still requires proof before compaction. Nominal refinement, outer-secret +promotion, exact Coarsen ancestors and direct-Secret Declassify remain unchanged. + +MIR reuses persistent whole-owner loans, liveness, non-owning storage and cleanup. +Native emission keeps the existing machine tag-slot offset outside canonical +FieldId metadata and checks pending owners at each field access. Borrowed linear +reads retain their handle; owning copies and explicit clones acquire independent +ownership. No ABI, hidden owner, implicit clone/join or lifetime policy changes. + +The initial nine-case characterization in +`target/native-machine-projected-view-preflight/results.json` recorded 18 +successful frontend checks, eight reference successes and one terminal failure. +Six alias cases and two owner-change probes had 16 native refusals at the old +struct-only proof; the owning-copy control was not native-built in that batch. + +After integration, three focused new HIR tests pass. The current library gate +passes 261 tests: 41 codegen, 134 HIR and 86 MIR in +`target/native-machine-projected-view-phases.log`. It includes exact malformed +unused state/index/endpoint/root, bare/foreign owner and orphan controls, plus +concrete generic contexts. This is not a new complete workspace/library claim. +All 83 driver library tests pass (66.89 seconds) in +`target/native-machine-projected-view-driver-frontend.log`. The corrected +`fixture_suite` invocation passes all 598 frontend fixtures (22.15 seconds) +in `target/native-machine-projected-view-frontend.log`; the earlier command +used the nonexistent `fixtures` target and executed no frontend tests. + +The expanded local-alias gate passes 25 tests (366 filtered, 17.72 seconds) in +`target/native-machine-projected-view-pending-linked.log`. It includes the +existing ordinary-struct controls, seven exact-state cases and the consume/ +transition publication-sentinel group, plus five pending-machine cases. +Those pending cases preserve whole-owner and intermediate-owner field errors, +depth-two endpoint/owner aliases, explicit clone/two-join copies and unchanged +originals. Ordered runtime Trace events, application stdout, stderr and status +0/71 match in both runtime profiles after source removal; release omits traces +while retaining terminal errors. Earlier pending frontend/reference preflight +is recorded separately in `target/native-machine-projected-view-pending-draft/results.json`. + +The typed-endpoint case passes separately (one test, 391 filtered, 2.80 seconds) +in `target/native-machine-projected-view-typed-linked.log`, using matching +archives and source removal with exact application stdout and empty debug +events. `Envelope[int64]`, `Envelope[bytes]` and `Envelope[list[int64]]` +instances retain scalar-copy versus aggregate-borrow facts. Exact machine fields +retain `NarrowValues`/`Numbers` and `secret[list[int64]]` identities through +explicit borrowed Coarsen/Declassify; +grown clones leave reread originals unchanged. Its source preflight is +`target/native-machine-projected-view-typed-endpoints-draft/preflight.json`. +An initial handler around an already-proven Packet constructor reported E0308; +removing that invalid handler repaired only the fixture, without compiler changes. + +These are 26 distinct executed local-alias tests across the 25-test batch and +the separate typed-endpoint pass, not one executed 26-test batch. The supplemental +native corpus registers 392 tests. All 15 machine Jett source-format checks and +Rust formatting checks pass. Broader endpoint/owner combinations retain their +independent obligations; the examples do not impose an endpoint allowlist. + +At this machine checkpoint, bare flow-narrowed origins, mutable backing chains, +temporary roots, projected writes, allocating conversions and owner +consume/rebind/transition after reachable alias creation were outside the bounded +native support. The mutable-owned-backing follow-up below has separate bounded +acceptance; writes with dependent aliases remain refused. +The actual owner-change group reaches the existing typed ownership refusal +before archive lookup and preserves the output sentinel in both profiles. +That checkpoint selected no source loan-expiry or temporary-lifetime contract. + +The accepted a0bf12df frozen workspace and all four supported-host jobs are +centralized in +[the acceptance audit](native_acceptance_audit.md#exact-qualified-machine-field-local-view-focused-acceptance). +The `774b13e7` complete frozen workspace now passes; its supported-host workflow +remains in progress. The audit records exact final logs and counts. This local +acceptance does not validate later inferred JSON changes or establish full-language parity. The about-85% +planning estimate and fixed 207-fixture denominator remain unchanged. + + +## Bitfield field follow-up + +The exact declared Bitfield owner now participates in the existing finite Field +initializer proof. Canonical zero-based identity and declared field type are +checked alongside the unchanged source, owner and endpoint proof. Struct-to- +bitfield paths, whole-bitfield aliases, view parameters and forwarding retain +the same immutable backing chain. Pending field checks, numeric copies, owned +field copies, explicit clones and non-owning alias cleanup remain unchanged. + +Both focused HIR tests pass, including unused forged owner/index/endpoint/root +and orphan metadata. All seven linked cases pass together (3.89 seconds, 425 +filtered), executing matching debug/release profiles after source removal. +These include exact no-LF output, owner rereads, empty payloads, whole pending +owner failure and five ordered payload-copy traces. The current corpus registers +432 tests. The crossed compiler gate passes 951 library tests, followed by all +83 driver library tests, 598 frontend fixtures and seven source-format checks. +New full-workspace and supported-host acceptance remain pending. + +The accepted 08f9f7e7 parser predecessor and the bounded bitfield evidence are +recorded in [the acceptance audit](native_acceptance_audit.md#bitfield-field-local-views-focused-acceptance) +and [the bitfield contract](native_bitfield_projected_local_views.md). This +repair selects no mutable or temporary view, owner-change, capture-escape, +projected-write or loan-expiry policy and makes no claim for later repairs. + + +## Direct secret aggregate owner follow-up + +The same typed initializer proof now retains one direct Secret layer on an +exact declared Struct, Bitfield or MachineState receiver. Nothing and an +already-secret nominal field keep their declared type; every other endpoint +gains exactly one Secret layer. Field identity/type, checked root, immutable +loans, non-owning slots and pending access remain unchanged. Explicit borrowed +declassification/coarsening and owned clones preserve their established roles. +No hidden-secret debugger or general lifetime policy is selected. + +The three focused HIR tests pass (136 filtered), followed by all 954 crossed +compiler library tests. All seven linked cases pass in 2.82 seconds: six pin +full successful outcome parity, while the pending receiver separately pins its +raw reference message and existing native redaction with matching partial stdout +and status 71. This follows the already recorded open observation boundary; +it does not claim seven diagnostic-parity cases. All 83 driver library tests, +598 frontend fixtures and seven source-format checks pass. New +439-test workspace/platform gates remain pending. +Exact captures, selected proof and current evidence are in +[the secret-owner contract](native_secret_owner_projected_local_views.md). +The independent projected interface/handler repair remains unapplied. + + +## Boundaries requiring a contract + +The selected [projected assignment contract](../../open_design/projected_field_assignment.md) +rejects writes through views or with outstanding dependent loans. Declaration +facts alone are not complete borrow-provenance analysis. Keep these remaining +lifetime questions explicit before extending the first domain: + +- Rebinding or consuming an owner while an alias exists, including whether loan + expiry for source validity is lexical or based on last use. +- Rebinding a mutable alias itself; current declaration facts remain read-only + and do not define a general lifetime transition. +- Allocating conversions and temporary origins without an exact checked owned + producer proof. The selected ordinary-temporary contract below does not + manufacture ownership or conversion authority for those cases. + +Some such forms are already admitted by the frontend. Acceptance and incidental +interpreter cloning do not settle their ownership contract. Keep these forms +visible as unimplemented native cases; do not silently redefine their lifetime +rules, permit writes through aliases, or count the first domain as complete view +support. + +## Lexical temporary backing: selected, not implemented + +An immutable local view of an ordinary owned temporary retains the whole +exact producer result in compiler-owned storage until the alias declaration's +original lexical scope exits. The producer evaluates once; immutable +forwarding shares its nonowning dependency. No last-use expiry, implicit clone +or fake Source DefId is selected. Whole-owner cleanup occurs once on each +scope exit after any owned exit result is acquired. Copyable endpoints and +existing escape/loan rules retain their behavior. This does not alter internal +call-view End or grant actor/Resource custody. + +The [temporary local-view note](native_temporary_local_views.md) records the +tradeoff, exact checked initializer/hidden-owner proof and pending gates. +Original `local_view_aliases/09_temporary_root_boundary.jett` remains unchanged +and currently native-unsupported. Its unused alias and empty output cannot +prove backing lifetime or cleanup; used-alias and scope-exit controls are +required. No implementation or acceptance credit is claimed by this decision. + + +## Mutable owned backing follow-up + +The finite alias chain now permits a mutable terminal only when its original +DefId/LocalId, resolver declaration, concrete type, mutable flag and selected +`CheckedBindingMode::Owned` agree. Every alias/intermediate slot remains +immutable. View parameters and borrowed match/iteration binders gain no owning +authority. Original typed initializers, finite field proofs, readiness, backing +liveness and non-owning storage remain unchanged; there is no runtime ABI change. + +Persistent may-created aliases continue to block owner consumption, rebinding, +field writes and state changes across CFG joins, including after the last read. +Temporary roots, mutable alias slots, mutable view-parameter backing and capture +escape retain their implementation limits. Absent or mismatched selected +reflected root facts remain refusal cases. No flat checker-fact fallback, +lifetime extension or loan expiry is added. +Borrowed-sum Handle aliases retain their separate checked backing receipt; the +[mutable-owned sum extension](native_mutable_borrowed_sum_backings.md) has its own +bounded library/native acceptance, separate from these local-view receipts. + +Original Source08 is preserved as an executable empty-output control; new +Source11 observes nested bytes/list projections, forwarding, borrowed calls, +explicit clone independence and ordinary field-copy independence. Both pass in +paired Source/runtime Debug and Release: four executions with Source absent. +Typed forgery, consistent mutable View-parameter, persistent-loan, liveness and +actual LocalId-remapping controls also pass. The affected HIR/MIR/Codegen suites +pass 753 tests. The full selected matrix passes 31 native tests, 64 Source-deleted +executions and six separate publication-boundary checks; it includes eight older +Debug-runtime-only executions rather than a full profile cross-product. Exact +counts, receipts and limits are recorded in the +[native audit](native_acceptance_audit.md#mutable-owned-local-view-backing-bounded-msvc-acceptance). +Historical receipts above retain their original scope. The fixed 207/182 +inventory and full native-codegen goal are unchanged. + + +## Reflected outer-owner fact transport + +The producer closes the binding dependencies of actual retained +local-view facts through only their supplied concrete checked map. Each +`View { source: Binding(definition) }` resolves its exact declaration and matching +fact; forwarded aliases retain the transitive chain. A visited DefId set bounds +malformed cycles. Modes, concrete types and mutable flags are preserved, with no +`Owned` synthesis, global fallback or unrelated enclosing capture import. + +Existing HIR original LocalId/DefId and typed initializer joins, MIR persistent +loans, liveness and cleanup stay authoritative. Absent or inconsistent selected +root facts remain refusal cases even if an enclosing or another concrete map has +a valid fact. Public identical Copy metadata gains no new session brand. + +Generic copy/view, repeated/nested reflected snapshots, transitive dependencies, +spare exclusion and missing/mismatched/cross-context controls accompany Source12. +It passes reference checks and two Source-deleted executions in paired +Source/runtime Debug and Release. All 1,797 affected library tests pass. The full +selected native matrix passes 32 tests, 66 Source-deleted executions and six +separate publication-boundary checks; exact evidence is recorded in the +[native audit](native_acceptance_audit.md#reflected-local-view-backing-facts-bounded-msvc-acceptance). +The accepted first-slice counts above remain specific to that revision. No +Runtime/ABI/helper or lifetime-policy change is made. Mutable borrowed-sum Handle +backing has separate [bounded private-proof acceptance](native_mutable_borrowed_sum_backings.md); +these existing local-view receipts retain their own revision and scope. diff --git a/docs/completed/native_codegen/native_mutable_borrowed_sum_backings.md b/docs/completed/native_codegen/native_mutable_borrowed_sum_backings.md new file mode 100644 index 00000000..c9d6b91f --- /dev/null +++ b/docs/completed/native_codegen/native_mutable_borrowed_sum_backings.md @@ -0,0 +1,160 @@ +# Mutable owned borrowed-sum backing + +## Status and selected boundary + +The bounded mutable-owned ordinary borrowed-sum extension has focused MSVC +acceptance recorded below. Existing immutable-backing, Resource, actor and +local-view receipts retain their original scope. No native coverage estimate +or historical fixture denominator advances here. + +A canonical declaration views a Handle whose target is an explicit View of one +stable original sum binding. A mutable backing must be a genuinely checked Owned +local or owned parameter. An immutable alias cannot authorize owner changes: +persistent move-only payload aliases continue blocking whole-owner consumption, +rebinding, field replacement and state changes, including after the last read. +Generated source/output slots remain immutable and nonowning. + +Implicitly copyable primitive/String destinations keep their checked Owned mode +and no persistent alias origin. A String acquires its ordinary retained copy. +Only the two constructor-authenticated intermediates retire at its final Let; +later source replacement or owning unwrap must leave the copied value valid. +The Result failure companion continues acquiring its one owned String copy. + +## Original proof and canonical transitions + +The opaque HIR receipt is produced at every genuine canonical local declaration, +including copied destinations that bypass local alias-origin metadata. It joins +the exact active root DefId/declaration/type/mutability/Owned fact, alias fact, +original initializer, real function identity and concrete required-scope/type +binding path. View parameters and borrowed match/iteration binders are excluded +by their original role evidence. Missing source facts cannot fall back to flat +or enclosing maps. Absence of view_source, matching names or public headers is +not ownership. The per-lowering token is not an independent check-session brand. + +Initial sealing follows canonical inline extraction, conversion and genuine +reflected callable sealing. It pins the unique actual Let and complete physical +function. Later conversion authenticates the protected before tree and exact +canonical replay. Reflected source normalization excludes only the independent +sum inventory to avoid circular snapshots. The genuine private callable pass +supplies exact old/new callable tokens; sum renewal replays only those typed +changes and compares the complete resulting body before updating its snapshot. +Callable identity stays exact. No public current-tree reseal is introduced. + +MIR admission consumes that receipt and retains it in the original declaration +and projection row. Current validation rejoins the immutable original HIR record +before the independent CFG/header/tag/arm/field checks. Existing exact sequence, +block and LocalId remaps remain mandatory. Public missing, foreign or mutated +receipts cannot gain authority from a refreshed current-tree seal. + +## Preserved semantics and explicit debt + +Outer pending sums fail at the existing typed tag observation before extraction +or failure continuation. Ready payload pending metadata, Return-only absence/fail +continuations, nested scalar Defaults and error/cleanup order remain unchanged. +Alternate outer Default payload lifetimes, temporary-backed Handle roots, mutable +View parameters, borrowed binder roots, erased/capability/actor/Resource carriers +and general alias expiry are not admitted by this extension. Scoped copied-Handle +contexts still need a genuine selected source fact; no capture fallback is added. +Baked/materialized callable composition remains unverified implementation debt; +the ordinary same-function callable control does not establish it. + +The selected [temporary local-view lifetime contract](native_temporary_local_views.md) +is separate and remains pending implementation. This stable binding receipt needs +a genuine Source DefId and cannot authorize its hidden temporary owner. No runtime, +ABI, helper, measured archive or Resource/actor custody permission changes here. + +## Authored Source and proof controls + +The five additive native cases contribute ten profile-matched Source-deleted +executions to the selected matrix below: + +- Source03 and Source15 differ from their preserved originals only in two backing + declarations each. Scoped/forwarded move-only aliases and mutable copied final + bindings retain their exact original output. +- Source13 and Source14 differ only in their one backing declaration. The original + RuntimeFailure oracle checks named-argument order and terminal-failure behavior + after the borrow in both profiles. +- Source16 keeps primitive/String copies across old-shell replacement and then + observes both the retained old copy and new owning payload. + +HIR controls cover selected Owned facts for both destination classes, changed or +foreign receipts, missing origins, duplicate declarations, denied resealing and +canonical conversion. A genuine same-function reflected callable composition +lowers and completes conversions twice while retaining exact callable identity. +MIR controls cover fresh proof admission, original rows/current headers, immutable +nonowning slots, copied-intermediate retirement, persistent owner-change refusal +and actual nonidentity LocalId compaction through a captured callback. + +## Bounded MSVC acceptance + +All five affected compiler library targets pass **1,807 tests**: Typecheck 379, +Comptime 663, HIR 261, MIR 369 and Codegen 135. The log is +`target/native/mutable-sum-core-libraries-v1.log`, SHA-256 +`6ab23ff17459a6000209349df28c39fcb77dcd903288974836eeb6a815f0a895`. +The focused HIR and corrected MIR groups each pass five tests; these are reruns +within the library total. Full Rust formatting passes. These five targets are +an affected-library gate, not the full workspace, Driver or fixed inventory. + +The selected native matrix passes **10 tests** and **46 Source-deleted +executions**, covering 23 distinct programs: + +| Selection | Tests | Executions | Runtime Debug | Runtime Release | +| --- | ---: | ---: | ---: | ---: | +| Five additive mutable-sum Sources | 5 | 10 | 5 | 5 | +| Unchanged ordinary borrowed-sum packet | 1 | 24 | 12 | 12 | +| Unchanged supplemental borrowed-sum packet | 1 | 6 | 3 | 3 | +| Exact actor direct persistent-view control | 1 | 2 | 1 | 1 | +| Exact actor projected persistent-view control | 1 | 2 | 1 | 1 | +| Exact reflected mutable-owned view control | 1 | 2 | 1 | 1 | +| Total | 10 | 46 | 23 | 23 | + +Every execution pairs Source Debug/default with normal Runtime Debug or Source +Release/optimized with normal Runtime Release. This is not the full profile +cross-product. The authenticated harness checks Source absence, exact stdout, +stderr and exit status, including the original terminal-failure oracles. The +selected matrix has **zero publication refusals**; runtime failures are executed +failure oracles and supply no publication-refusal count. The five additive +Sources contribute ten executions, rather than 46 new programs. + +Fresh compiler gate: +`target/native/mutable-sum-gate-compile-v1/gate-acceptance.json`, SHA-256 +`4e2cd94e7ccc2cb23ae22b14a65e66a547d4136e148cf7f2708294cfb147c819`. +It pins the Debug test executable `native_conformance-3b1b799f1cd09233.exe`, +SHA-256 `165d50c4c149bb12d1a59e4549401d2960cda242855636ece26fd49d94197624`. +Execution summary: +`target/native/mutable-sum-native-execution-v2/execution-summary.json`, SHA-256 +`df43d3141cbef95c2c68402cb1fff94e78f5aa753c50d9ab523ad241d45ea7b4`. +These paths use the primary checkout's shared `target/native`. The interrupted +v1 attempt is not a completed matrix and supplies no additional acceptance. + +Before/after guards preserve **1,046 compiler/source inputs**, **68 reused +runtime artifacts** and all **50 fixture hashes**. Guarded fixtures are not the +executed-program denominator. The frozen runner is +`native-codegen-recovery-2026-10-09/mutable-sum-execution-v1/run-execution.ps1`, +SHA-256 `e681256ae305bc6af5d56f4593e0623243876cfbf3b4b3ed71deacd9bced3f75`. +Its exact count/launcher inputs and all six passing selections are bound by the +summary. Runtime/helper/ABI and measured bundles remain unchanged. The reused +normal receipt SHA-256 is +`2b2b9cd529c451c216072dc2b563253149f409bd3ef9ea867a1a86da1c637069`; +the guarded private receipt SHA-256 is +`35db08fbe46f339dad36731e24998497fdd77b459b0a283ed236cd124638db60`. +Guarding that private bundle grants no private-launcher execution credit. + +These normal-runtime controls establish output/status/order parity. They supply +no new private cleanup-counter observations, complete physical cleanup counts +or full Resource-corpus acceptance. Earlier 753/31/64 and 1,797/32/66 local-view +receipts remain specific to their own revisions and selections. + +Temporary-backed Handle roots, mutable View-parameter roots, borrowed binders, +absent or mismatched selected facts, alternate outer Default payload lifetimes, +scoped copied-Handle contexts lacking genuine selected source facts and +baked/materialized callable composition retain their separate proof or +implementation limits. The lexical whole-owner temporary-backing contract +selected at `f865e3f1` is unchanged and still awaits implementation and cleanup +gates, including unchanged Source09. This stable binding receipt cannot +authorize a hidden temporary owner. No last-use expiry, implicit clone, invented +Source identity, wider escape or operation-bounded call-stage lifetime is added. +Complete Resource transport, wider actor/task compositions, asynchronous +scheduling, full coherent workspace/inventory execution and exact-revision local +supported-host/installed acceptance remain open. The fixed 207-fixture/182-object +denominators and full native-codegen goal are unchanged. diff --git a/docs/completed/native_codegen/native_named_resource_callbacks.md b/docs/completed/native_codegen/native_named_resource_callbacks.md new file mode 100644 index 00000000..51e9120b --- /dev/null +++ b/docs/completed/native_codegen/native_named_resource_callbacks.md @@ -0,0 +1,79 @@ +# Named Resource callbacks + +## Selected implementation contract + +The next native parity milestone is an immutable function value initialized from +one original named, non-generic, capability-free function. Passing an owned +Resource to that value is existing Jett syntax. This work changes backend support; +it introduces no source-language restriction or new callable syntax. + +The original checked invocation stays indirect. A separate private proof selects +the exact retained declaration from the original callable initializer and its +current use. Signature equality, a public function name or an edited MIR pointer +cannot select another body. Immutable alias chains preserve exact provenance; +ambiguous joins, reassignment and escapes require their own proved transport. + +Reference execution must use the retained checked body and existing argument +custody envelopes, Return destination and operation floor. Native execution must +validate the selected code pointer and empty environment before entering the +existing Resource Scope bridge. Ordinary callable invocation omits that hidden +Scope and cannot be used for a Resource family target. Refusal must clean any +already acquired argument prefix without entering or publishing the wrong body. + +## Original Source baseline, 2026-10-06 + +The shared `18_indirect_named_close.jett` parses, resolves and checks. Its named +`close_selected` owns `resource_probe.TestHandle`, calls the checked close wrapper, +and is bound to immutable `dispose` before construction of token 701. + +The genuine reference gate reaches the first new scenario and fails with +`Resource source binding has no live cleanup custody`. The native gate reaches it +and fails MIR validation with `indirect custody call lacks an exact descriptor +producer`. These are executed failures, not accepted behavior. Baseline logs and +Source snapshots are retained in +`target/native-resource-indirect-named-source-integration/`. + +## Required acceptance + +The shared oracles add successful named close, handled construction failure, +ordinary target-body failure, close finalizer failure, and combined body/cleanup +failure. Original body status must survive cleanup failure, while cleanup +diagnostics take precedence. +A same-signature `wrong_target` performs an ordinary terminal failure; a separate +legal Source selects it and must execute that failure while retiring the Resource. +Private target-swap controls must refuse substituting that body into the checked +close program. This distinguishes exact body selection from matching signatures. + +Both profiles must pass genuine reference and linked native execution, including +native execution after deleting the project and synthetic stdlib Source. Provider +events, body versus cleanup status, diagnostics, zero remaining obligations and +exactly-once teardown remain shared acceptance requirements. Existing 22 scenarios +and ordinary callable regressions stay required. Runtime ABI changes are not +planned; any runtime change requires newly built, measured matching archives. + +## First reference gate + +The new three private named-body tests pass, and the complete reference library +passes 511 tests. The shared gate executes all 28 scenarios in both profiles, +including exact target selection, immutable aliases and combined body/cleanup +failure: 56 reference executions with zero live provider obligations before +teardown. The corrected complete MIR suite passes 233 tests; native codegen passes 93 +unit tests and 63 object-emission tests. The linked native gate passes all 28 +scenarios in both profiles (56 executions), with Source absent and zero live +obligations before teardown. Existing hook descriptor storage remains unchanged; +the earlier broader companion predicate was rejected by its regression test. +Runtime sources and both measured archives still match receipt `bf0c7805`. +The complete driver library passes 92 tests. Ordinary callable regression groups +pass 13 function tests, one captured-closure test, one collection-callback test +and 16 caller-ownership tests. Fresh supported-host acceptance of this successor +remains a separate gate. + +## Remaining full-goal work + +Qualified namespace members are covered by the subsequent +[namespace callback proof](native_namespace_resource_callbacks.md). Returned +compiler hook descriptors, dynamic and captured callables, scoped and generic +bodies, reflected/pipeline/handled invocation and occupied aggregate +Resource paths remain required. Reference fixture07 returns a compiler hook +descriptor and does not prove named Source callback support. This milestone does +not count those separate invocation forms as covered. diff --git a/docs/completed/native_codegen/native_namespace_resource_callbacks.md b/docs/completed/native_codegen/native_namespace_resource_callbacks.md new file mode 100644 index 00000000..5dc58f1e --- /dev/null +++ b/docs/completed/native_codegen/native_namespace_resource_callbacks.md @@ -0,0 +1,39 @@ +# Namespace-qualified Resource callbacks + +A namespace-qualified function value must select its exact resolved declaration +and retained checked body, just like an unqualified named function. The original +call remains indirect. Namespace members resolve at the whole expression span; +field spelling or a matching function signature cannot grant that authority. +Instance fields, method adapters and compiler hooks need their own transport and +must not enter this named-body path merely because they use dotted syntax. + +The genuine shared Source programs initialize an immutable callback from +`resource_probe.close`, then call it directly or through an immutable alias. +The shared corpus grows from 28 to 32 scenarios: successful close, handled factory +failure, finalizer failure and alias close. Both profiles require exact events, +separate body/cleanup reports, Source-absent native launch and zero obligations +before teardown. The baseline reference gate fails on the first qualified +callback with `resource value lost its checked payload custody`. The actual +native gate passes all 64 executions. A corrected test filter selects the one +shared Resource group; an earlier zero-test invocation is excluded. Runtime +sources and measured archives are unchanged. + +This closes one existing callable provenance seam. Returned hook descriptors, +dynamic/captured/scoped/generic/handled/pipeline callbacks, aggregate Resource +ownership, production providers and scheduling remain in the full goal. + +## Verified reference and native execution + +Reference proof now admits the original qualified producer only through its +full-span resolved Function definition, unique retained non-generic header and +exact checked Function signature. The original expression occurrence, body +attempt, immutable alias chain and indirect invocation packet remain checked. +Member spelling and runtime function names cannot supply body authority. + +All seven named-callable proof tests and all 515 reference-library tests pass, +including all 64 shared Resource executions. Genuine instance-field, method-value, +generic and compiler-hook controls cannot mint named-body proof. Copied, foreign, +same-signature target, signature and producer-as-invocation substitutions fail. +The final linked native gate passes all 64 executions with Source absent and zero +obligations before teardown. The complete driver library also passes 92 tests. +Fresh supported-host acceptance remains a separate gate. The broad native-language estimate stays about 85%. diff --git a/docs/completed/native_codegen/native_ordinary_borrowed_sum_codegen.md b/docs/completed/native_codegen/native_ordinary_borrowed_sum_codegen.md new file mode 100644 index 00000000..e5b3280c --- /dev/null +++ b/docs/completed/native_codegen/native_ordinary_borrowed_sum_codegen.md @@ -0,0 +1,133 @@ +# Native ordinary borrowed optional/result payloads + +The native-codegen goal remains active. This change implements the existing +ordinary handled-view family; it does not establish whole-language parity. +The [Source-only baseline](native_ordinary_borrowed_sums.md) retains its original +12 reference passes and 24 native unsupported-handler refusals as history. + +## Contract and implementation + +The canonical initializer is an explicit outer `view` of a Handle targeting an +explicit `view` of one stable sum binding. Immutable owners and View parameters +retain the measured predecessor proof. A bounded extension admits a mutable +owned local or owned parameter only through its private original checked receipt. +The absent +continuation ends with Return. A Some/Ok move-only payload keeps its exact checked +type and backing dependency without a clone, sum transfer, or owning cleanup +slot. Forwarded, nested and loop aliases preserve that dependency. Explicit +`clone` still acquires independent ownership. + +Implicitly copyable final destinations keep ordinary copy semantics. Primitive +values are copied; String owns its retained copy. A copied destination may be +mutable and rebound. After initialization, generated projection intermediates +leave the borrow inventory, allowing a later ordinary transfer, owning unwrap +or replacement of the original mutable sum. This does not expire a persistent move-only alias or permit +changing its backing owner. A Result Fail String companion likewise owns one +retained copy for its handler. + +MIR seals the original declaration, current headers, backing identity, tag +observation, selecting branch, success/failure projections and exact final +initializer. Canonical remapping authenticates both the prior and resulting +graphs; exact no-op transitions preserve proof identity. Missing, copied, +foreign, resealed or disconnected witnesses cannot authorize payload views. +Native admission validates this proof before consuming sum lowering. + +The nonconsuming `jett_rt_v1_sum_payload_borrow` leaf returns the selected payload +bits without allocation, clone, retain, adoption or mutation. Generated source +and success payload locals remain nonowning. Only the checked copy destination +acquires ordinary ownership. Scalar pending depth and opaque container metadata +are preserved. Outer pending sums fail before extraction or continuation; +ready sums retain later payload-observer readiness checks. + +This bounded proof grants no Resource custody. Alternate outer Default payload +lifetimes, mutable View-parameter roots, unproven owned origins, temporary-backed +Handle aliases, general loan expiry +and payload identity hidden by erased interfaces, callables, actors or capability +carriers remain separate work. Nested scalar Handle defaults retain their +existing semantics. + +## Measured acceptance + +The coherent candidate based on `c453297d` passes: + +| Gate | Result | +| --- | --- | +| Original frozen list-payload packet | 12 checked reference Sources; 24 Source-deleted debug/release native executions | +| Separate supplemental packet | 3 checked reference Sources; 6 Source-deleted debug/release native executions | +| MIR and native codegen | 276 MIR, 107 codegen, 63 object and 1 CFG checks | +| Runtime | 223 checks, including 8 nonconsuming payload controls | +| Driver library | 93 checks | +| Resource regression with fresh matched runtime archives | 138 scenarios; 276 Source-deleted debug/release native executions | + +The final combined Source gate passes all four Rust groups in 79.12 seconds. +It compares complete status/stdout/stderr and requires empty debug streams. +The Sources cover Some/None and Ok/Fail, stable owned/View-parameter roots, +forwarding, nested aliases, real loops, clones, named argument-prefix Return and +terminal failure, outer pending sums and pending payloads. Supplemental cases +add a later argument Handle Default failure after a staged alias, plus mutable +copied integer/String bindings followed by original owning unwrap. They preserve +the original Sources, runner and baseline note; their parent registration is +an additive change, separately recorded from the frozen baseline artifact. + +The final compiler/runtime/Source snapshot contains 2257 inputs, independently +revalidated after execution. Its manifest SHA256 is +`20c4c6b5ea49b94f16ed8f68b6db4aa913909227cbfce8571e403d78077e8a94`. +Evidence is retained under `target/native-ordinary-borrowed-sum-candidate-evidence/` +in `acceptance-v7.json`, `driver-all-packets-v7.log`, `mir-codegen-full-v5.log` +and `fresh-runtime-resource-acceptance.json`. + +Fresh Resource archive receipt SHA256 is +`29dd83c7f3874d7fe4b4fd3a5784786a5b31ee45337e9f40f133da9f33316d42`. +The receipt is pinned at test compilation; runtime environment chooses only its +path. The 138-scenario gate passes in 761.14 seconds with Source absent at launch, +exact reference events/status/messages and zero obligations before teardown. +Both archives, all nested receipts, the frozen test executable and current +compiler/runtime/Source inputs are rehashed after the run. Subsequent changes to +compiled inputs are limited to supplemental tests, their formatting, and a +codegen test symbol lookup; documentation changes do not affect that gate. + +Reproduce the public gates from the repository: + +```text +cargo test --locked -p jett_mir -p jett_codegen_cranelift +cargo test --locked -p jett_runtime +cargo test --locked -p jett_driver --lib +cargo test --locked -p jett_driver --test native_conformance ordinary_borrowed_sums -- --nocapture --test-threads=1 +``` + +The private Resource gate additionally needs the independently measured archive +receipt and its compile-time pin. See [archive measurement](native_resource_gnu_gate.md) +and the [Resource runner](../../../crates/jett_driver/tests/native_conformance/resource_execution.rs). + +## Remaining release gates + +The estimate advances from about 86% to about 87% as this concrete family gains +linked native coverage. It is a planning estimate, not measured code coverage; +the fixed 207-fixture/182-object inventory denominators do not change. +The clean committed `2c9f9aa8` workspace passes 3,446 tests across 92 result +groups, with every one of 3,637 tracked inputs unchanged. Its [exact-revision +workflow](https://github.com/vycdev/jett/actions/runs/37673804359) passes all four +supported-host jobs, including private GNU Source execution. These gates are +accepted for this compiler/runtime revision. Later Source packets retain their +own complete-corpus and platform requirements. The complete 100% objective and +all other semantic, ordinary-carrier, Resource and reflection obligations remain +open in the [acceptance audit](native_acceptance_audit.md). + +## Mutable owned backing: bounded successor acceptance + +The [mutable-owned sum extension](native_mutable_borrowed_sum_backings.md) adds +an opaque original checked receipt before MIR admission for both borrowed and +copied final destinations. Exact selected root/alias facts, original initializer, +concrete context and full function identity remain mandatory. The original MIR +row, current CFG, tag/arm association and canonical remaps retain independent +checks. Persistent aliases still block consumption, rebinding, field writes and +state changes after their last read; copied values keep their ordinary ownership. +The coordinated callable transition retains exact tokens and no public reseal. + +The successor note records all 1,807 affected-library tests and a selected +ten-test native matrix with 46 Source-deleted profile-matched executions. The +five additive Sources contribute ten executions, including unchanged-oracle +named-argument terminal failures. The normal-runtime controls establish exact +output/status/order parity; they supply no new private cleanup-counter or full +Resource-corpus acceptance. The measured tables above remain predecessor +evidence; no coverage estimate or inventory changes. diff --git a/docs/completed/native_codegen/native_ordinary_borrowed_sums.md b/docs/completed/native_codegen/native_ordinary_borrowed_sums.md new file mode 100644 index 00000000..eb8d5e9c --- /dev/null +++ b/docs/completed/native_codegen/native_ordinary_borrowed_sums.md @@ -0,0 +1,143 @@ +# Ordinary borrowed optional/result payloads + +## Source-only baseline + +This packet isolates the existing public `optional[list[int64]]` and +`result[list[int64], string]` language family. It changes no compiler, runtime, +stdlib, language policy, or production representation. The baseline production +revision is `35bef5ea63cc8c28eb0c45edcf15fcd827a8f3b9`, in the separate +`codex/ordinary-borrowed-sums` worktree. + +The canonical payload alias is an explicit outer View of a Handle whose target +is an explicit View of one stable immutable sum local or View parameter: + +```jett +list[int64] alias = view((view source) handle: + return nothing +) +``` + +A Result uses `handle error:`. The checked alias retains the exact backing +binding. The occupied arm exposes a nonowning payload; the absent arm ends with +Return and creates no payload alias. A nested expression-level Handle can have +its own scalar Default. This packet does not select an alternate outer Default +payload lifetime, owner mutation, owner consumption after alias creation, or +loan expiry. + +## Frozen packet scope + +The 12 independent Sources are in +`crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums/`. + +| Source | Concrete behavior | Reference outcome | +|---|---|---| +| `01_optional_branches.jett` | Some/None across written and bare owned actuals to a View sum formal; repeated occupied aliases; nested scalar Handle Default inside the None continuation | success, exact branch markers, lengths and sums | +| `02_result_branches.jett` | Ok/Fail across written and bare owned actuals; repeated occupied aliases and exact string failure payload | success, exact branch markers, lengths and sums | +| `03_owned_scoped_forwarded.jett` | Immutable owned sum roots, forwarded/nested/loop aliases, explicit independent clones and repeated backing reads; scalar/list generic instances | success, exact sums and generic reachability markers | +| `04_named_prefix_return.jett` | Both kinds; original alias View actual evaluated before a later handler Return, with lexical named order differing from formal order; retained caller roots read again | success, exact argument-order and return markers; consuming body absent | +| `05_optional_terminal_prefix.jett` | Optional alias View actual staged before a later terminal argument failure; other owners retained | exact terminal failure after the occupied alias prefix | +| `06_result_terminal_prefix.jett` | Result counterpart of Source05 | exact terminal failure after the occupied alias prefix | +| `07_outer_pending_some.jett` | Twice-pending Some sum | Handle fails before handler or alias observation | +| `08_outer_pending_none.jett` | Twice-pending None sum | same boundary, no failure continuation | +| `09_outer_pending_ok.jett` | Twice-pending Ok sum | same boundary | +| `10_outer_pending_fail.jett` | Twice-pending Fail sum with string companion | same boundary | +| `11_pending_optional_payload.jett` | Ready Some containing a twice-pending list; alias forwarding succeeds | payload observer fails after `afteralias` | +| `12_pending_result_payload.jett` | Ready Ok counterpart of Source11 | same boundary | + +Sources01/02 pass owned sum bindings bare only at their last use. An existing +borrowed payload alias must be passed with explicit `view`; an initial draft's +bare alias call was correctly rejected by E0401 and repaired before freezing. +Source03's generic helper deliberately returns a reachability marker: its +generic rows establish alias initializer admission, not observation of generic +payload contents. Concrete list payloads are observed separately by length and +numeric sum. Identical Source outputs alone do not prove absence of an implicit +clone; implementation acceptance must also preserve the checked nonowning origin +and ownership facts. + +## Actual reference evidence + +The focused reference test is +`ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes`, in +`crates/jett_driver/tests/native_conformance/ordinary_borrowed_sums.rs`. +Every Source first passes `build_file_with_options`, then executes through +`run_file_capture_outcome`. The final formatted packet passed all 12 cases: +four success outcomes and eight exact runtime failures. The runner checks the +whole stdout, exact terminal message, and empty runtime/frontend debug streams. + +Sources05/06 end with +`runtime error: list.__remove_at: index -1 out of bounds`. Sources07-10 preserve +the exact nested pending Some/None/Ok/Fail rendering in the existing Handle +error. Sources11/12 end with +`runtime error: list.__sum: argument must be a list`, after `afteralias\n`. +The Rust packet lists every exact stdout/error expectation. + +All 12 Sources were formatted with the built Jett CLI, then passed +`jett format --check`. The Rust module was formatted with +`rustfmt --edition 2024 --config skip_children=true`. + +## Native baseline and acceptance gate + +The focused native test is +`ordinary_borrowed_sums_native_packet_matches_reference_without_source`. +For each genuine Source, it repeats the checked reference run, attempts native +object emission with both `BuildOptions { release: false }` and +`BuildOptions { release: true }`, and records every refusal. Object preflight +reaches the actual native frontend/MIR/verifier before building a runtime +archive. A refused object is a failing parity result, not an expected passing +assertion or an ignored test. + +If emission succeeds, the same gate builds a profile-matched linked executable, +deletes its temporary Source, asserts Source absence, executes the bounded +binary, and compares its status, stdout and stderr to the reference. Temporary +Sources are deleted even when object emission refuses. Baseline refusals produce +no binary, so Source-independent binary execution and matching remain unproven +until this gate passes. + +The final native baseline reached all 24 profile attempts against unchanged +production bytes: all 12 Sources refused in both profiles. Every actual refusal +reports +`NativeBuildError::Codegen` containing +`CodegenError::UnsupportedMir { construct: "failure handler", ... }`. +The native command exited 101 with 24 refusals after 34.44 seconds. No native +object or binary was produced and no native binary was executed. The gate remains +failing; no MIR/codegen/runtime fix is included here. + +## Evidence storage and reproduction + +Only this Cargo target was used: + +```text +C:/Users/Vycto/Documents/GitHub/jett/target/native-ordinary-borrowed-sum-baseline-build +``` + +Its `baseline-evidence/` directory contains the raw final reference/native +process logs, exact copied Source/Rust/documentation bytes, and SHA-256 manifests. +The production manifest covers all crate `src/` files, crate manifests/build +scripts, stdlib files, and root Cargo manifest/lockfile: 332 files. Its initial +SHA-256 is +`842a746b3a70128be7c4de351e2e97a8b07154ea013426d250e859f31e88ee9d`. +The final production manifest matches it byte-for-byte. + +From the isolated worktree, with `CARGO_TARGET_DIR` set to the path above: + +```text +cargo test -q -p jett_driver --test native_conformance ordinary_borrowed_sums_reference_packet_matches_exact_source_outcomes -- --nocapture +cargo test -q -p jett_driver --test native_conformance ordinary_borrowed_sums_native_packet_matches_reference_without_source -- --nocapture +``` + +An initial sandboxed run could compile the driver test but could not scan this +worktree's stdlib because directory access was denied. The final authorized +worktree runs used the same dedicated target with expanded filesystem access; +that environment failure is not language or native parity evidence. Existing +compiler warnings are retained in the logs. No existing primary-worktree paths, +commits, pushes, PRs, or native coverage denominators were changed. + +## Mutable-owned successor acceptance + +This frozen Source-only baseline, its original fixture bytes and its refusal +counts remain historical evidence. The separate +[mutable-owned sum extension](native_mutable_borrowed_sum_backings.md) retains +the original binding/Handle proof while adding exact checked mutable Owned-root +receipts. Its new variants and acceptance gates do not rewrite this baseline. +The successor note records bounded library and paired-profile Source-deleted +MSVC acceptance, with the original packet retained as an unchanged control. diff --git a/docs/completed/native_codegen/native_prepared_reference_driver.md b/docs/completed/native_codegen/native_prepared_reference_driver.md new file mode 100644 index 00000000..2d0a82b0 --- /dev/null +++ b/docs/completed/native_codegen/native_prepared_reference_driver.md @@ -0,0 +1,112 @@ +# Retained checked-program reference execution + +Status: implemented retained reference driver boundary. The original isolated +follow-up was over `94bb6b2e021181ec283489c9a02f29a09497fbc7`. +The contract was selected before +tracked Rust changes. All 46 focused Resource groups and four new driver groups +pass with unchanged source hashes. The broader 544 driver/HIR/resolver/typechecker +tests, 598 frontend fixtures and eight backend-lowering regressions also pass. +New full-workspace and supported-host acceptance for this follow-up remain +pending; exact94's failed full run is not +accepted by a later single-case rerun. + +The public BuildResult/build_file API remains unchanged. A private +PreparedReference retains its ordinary BuildResult observations plus an +`Option>` and the primary FileId0 main's exact saved +span/namespace. Build-file preparation parses/discovers once, preserves actual +loader origins, and uses CheckedResourceProgram::prepare internally with an +empty production catalog. The full checked session remains sealed. Comptime and +verify run once from that saved merged Module and original scoped/generic facts. + +run_file_inner delegates to private preparation then run_prepared_reference. +Runtime first emits retained frontend observations and refuses build.has_errors. +A successful build lacking a retained snapshot is an internal handoff failure, +never permission to reparse or initialize a provider. The runtime entry is the +saved FileId0 main span in the exact checked Module, not the first support-file +main. Interpreter.register_module resets namespace when item FileId changes +(interpreter.rs1330-1339); registering the merged module once preserves support, +project and entry order. There is no second filesystem read, parse or module +rediscovery after preparation. The unchanged macOS graphics-thread selection +still reads/parses before preparation; the entire public run path is not claimed +to parse once. Existing default argument, explicit environment, +Clock/Random/Graphics installation, output/debug capture and terminal sample +checks retain their order and behavior. No Resource provider, descriptor, +ResourceValue, ownership ledger, native ABI or execution effect is introduced. + +Selected through-failure diagnostics adjustment: + +- Parse/Resolve/CheckDiagnostics retain observations through the failed phase in + original parse -> resolve -> check order, without duplicate phase vectors. +- ResolveMetadata and CheckMetadata become { source, diagnostics } variants. + Their typed error is preserved by Display and std::error::Error::source. +- `diagnostics()` retains `Option<&[Diagnostic]>` compatibility and returns Some + even when a metadata failure observed zero diagnostics. +- Clone an earlier diagnostic prefix only on failure where needed. Successful + snapshot records remain owned in their original phase structures; do not run + resolution/checking again to recover warnings. +- Driver keeps discovery/parse observations in the exact prepared ParseResult. + Metadata rejection appends one operation-appropriate internal error while + retaining prior warnings. Successful preparation concatenates each retained + phase once before required evaluation. Error-bearing builds may retain the + checked snapshot internally for attribution, but runtime must gate the build + error before execution or provider initialization. + +Tests must use a real source unused-binding warning before checker E0311, +parser-warning injection into a real ParseResult with attribution stated, +warning retention before typed catalog errors and source-derived resolver +failures. Pin order/span/code/message and warning-only success. Driver controls +prepare source once then mutate/delete entry/support files, preserve exact +primary entry and per-file namespace reset with a support main, preserve actual +comptime/verify/runtime observations once, refuse a failed frontend before any +provider setup, and reject a corrupted successful handoff missing its snapshot. +No test needs a live Resource or native archive. The focused gates below are +executed; new full-workspace/platform acceptance remains pending. + +Driver build_source and backend lowering are outside this bounded preparation +repair. Their existing paths are not claimed unified. Runtime-only Resource +invocation eligibility was separate at this checkpoint; the later private +reference implementation and its remaining breadth are recorded below. +At this earlier driver checkpoint the whole-language 100% objective, about-85% +estimate, fixed 207 inventory/182 object obligations and 464 supplemental cases +were unchanged. The later caller native receipt passes 480 cases, separately +from the Resource reference tests. + + +## Verification and remaining boundaries + +The original target source draft is `e304c625` with manifest `51e6aaaf`; its +contract is `8d0e6fc5`. Root applied and formatted the two files in isolation. +`target/native-prepared-reference-driver-focused.json` passes 46 Resource and +four driver groups, with exit zero and both formatted source hashes unchanged, +ending `2026-10-03T14:12:41.4871736Z`. Three new Resource groups pin ordered +warning prefixes and typed metadata causes. Four driver groups preserve the +saved source/primary entry, ordered observations and pre-provider failure gates. +The parser-warning control injects a Diagnostic into a real ParseResult and +does not claim that the parser currently emits warnings. + +The broader driver/HIR/resolver/typechecker wrapper passes all 544 tests +(87/139/54/264) with both source hashes unchanged in +`target/native-prepared-reference-driver-phases.json`, ending +`2026-10-03T14:15:36.7702211Z` with exit zero. All eight backend-lowering regressions +(198.73 seconds), including the fixed inventory lowering check, and 598 frontend +fixtures (24.41 seconds) pass with those hashes unchanged in +`target/native-prepared-reference-driver-frontend.json`, ending +`2026-10-03T14:20:34.8384468Z` with exit zero. New full-workspace/platform gates +remain pending. The exact94 full workspace failed one +native NamedTempFile.reopen operation with PermissionDenied; all 182 object obligations and +463 other native cases pass. The affected case's elevated single-case rerun +passes, but neither run is full94 acceptance. The accepted b718 local and all-four supported-host receipts, plus the +historical `616b51b9` platform result, are centralized in +[the acceptance audit](native_acceptance_audit.md#retained-reference-preparation). +This follow-up adds no native inventory case and changes no coverage metric. + +The later [reference Resource lifecycle](native_resource_hook_identity.md#reference-lifecycle-execution) +implements program-bound callable identity, runtime-only hook eligibility, +checked caller effects, carriers and ordered cleanup under a compiler-test-only +scripted provider. Its 36 focused tests are separate from this driver receipt. +Broader scoped/reflected/indirect/worker coverage, production providers and +native lifecycle remain prerequisites. The sealed snapshot and +private reference bridge alone grant none of that authority. build_source and +backend lowering keep their existing independent paths. A formal View mode is +not proof of the caller's retained owner; no new generic borrowing policy is +selected here. diff --git a/docs/completed/native_codegen/native_refinement_context.md b/docs/completed/native_codegen/native_refinement_context.md new file mode 100644 index 00000000..4826f2dd --- /dev/null +++ b/docs/completed/native_codegen/native_refinement_context.md @@ -0,0 +1,60 @@ +# Refinement predicate declaration contexts + +A refinement predicate is checked where its type is declared. Its private helper +functions, interfaces, and type names must retain that declaration context when +the value crosses a boundary elsewhere. Native predicate functions already +have their declaration namespace. The interpreter previously evaluated the +predicate in the caller's namespace, so an exported refinement using +`Named.name(view value)` failed when validated by another namespace. + +Predicate evaluation now isolates caller locals, block-local `use` aliases, +generic arguments, scoped type bindings, and checked function maps. The only +runtime binding supplied to the predicate is its typed base `value`. Its checked +declaration facts and explicit comptime constants remain available. The base +type resolves in that same declaration context before evaluating inherited +constraints. The caller's complete context is restored on success and failure. + +The checker gives `value` the fully coarsened base type (and removes the outer +secret wrapper for predicate evaluation). Binding an intermediate refinement +instead changes interface dispatch inside an inherited predicate. The +interpreter uses the same predicate input type as the checker and native +predicate function. + +Differential regressions cover private helper lookup, interface-backed +refinements, inherited predicates, caller shadowing, generic and lexical type +bindings, explicit comptime predicates, and caller execution after a rejected +boundary. This implements ordinary lexical scope; it adds no dynamic name +lookup or caller capture to the language. + +The regression also includes a parenthesized explicit comptime expression. +HIR preserves the original comptime span as its evaluated-value lookup key +separately from the enclosing expression span, which parentheses may widen. + +## Refinements of whole sum values + +A refinement predicate receives its declared base value. A refinement whose base +is `optional[Named]` or `result[Named, string]` therefore validates the whole sum, +including its absent or failure branch. Refined struct-field construction already +implements this boundary. Direct bindings previously routed a `handle error:` +to sum extraction first, so the checker rejected the same base value and the +interpreter/HIR selected the wrong operation. The boundary now follows the +declared refinement base consistently. + +Direct refinement boundaries must take precedence when the checked source is an +exact ancestor/base accepted by the refinement. An outer result or optional +whose payload is the desired value still uses ordinary sum extraction; this does +not implicitly combine extraction and validation or introduce another spelling. +The error binding for refinement validation is always a string, independently +of a result base's own error type. Linked tests must cover both branches, +inherited refinements, interface payload owners, callbacks, pending values, and +runtime/comptime construction. `interface_refined_sums` supplies that coverage +in debug/release executables after source removal, including rejection of a +fresh pending input and ordinary extraction of already refined payloads. + +The pending input exposed a separate extraction bug: native MIR sum tag reads +did not check outer pending depth. `SumHandleTag` now checks before returning the +tag and formats the pending value with its checked layout. It preserves the sum +and owned payloads on failure. A ready sum may still contain a pending payload; +only the outer depth prevents extraction. `pending_sum_handle_failure` checks +present/absent optionals, both result branches, nested depth, a record payload, +exact terminal diagnostics, and cleanup in both profiles. diff --git a/docs/completed/native_codegen/native_required_owner_regions.md b/docs/completed/native_codegen/native_required_owner_regions.md new file mode 100644 index 00000000..40e659e5 --- /dev/null +++ b/docs/completed/native_codegen/native_required_owner_regions.md @@ -0,0 +1,67 @@ +# Original required regions inside inline bodies and refinement predicates + +Status: local acceptance recorded below; exact-revision supported-host and +installed acceptance remain open. + +Published Graph checkpoint `9113f395039afe41752be98f6fba55588af01ae6` +failed the same two required-evaluation checks on Windows MSVC and GNU. The +contextual comptime reference oracle reported three E9001 errors; the refinement +declaration oracle reported one. Those were checked materialization failures, +not timeouts or native Runtime mismatches. The local repair is verified only by +the receipts below; it does not establish new GNU execution or successor CI. + +The repair adds private required-region paths into exact retained inline bodies +and an exact checked refinement-predicate origin. It keeps the enclosing +declaration, concrete generic instance and ordered scoped selections. These +paths are restricted to explicit required evaluation and grant no runtime +Resource or capability permission. The existing closed and pure checks remain +authoritative, and the ordinary executable walker still stops at callable +boundaries. + +Required-value proofs reconstruct the same original occurrence and context. +Private HIR extraction records join an original inline node to its canonical +extracted function and enclosing owner. Refinement proofs join the exact checked +declaration and original predicate HIR. Resolver names use the canonical +namespace and leaf; HIR declaration identity retains the original alias leaf. +Both must match their respective retained metadata. DefId, kind, declaration +span, namespace, origin, constraint, original header and operand checks remain +required. Full original/current archive checks, the value matcher and +once-per-original-attempt evaluation remain in place. + +The original contextual and refinement fixtures and expected interpreter +streams remain unchanged. Focused controls cover physical clones, foreign +checked programs, wrong enclosing owners, generic and scoped selections, +callable-boundary ancestry, changed extracted headers and operands, wrong +refinement declarations and alias names, runtime-purpose refusals, and metadata +restoration before reentry. + +Focused acceptance: 20 tests across 17 actual invocations, with no failures or ignored tests. Receipt: +`target/native/required-owner-focused-v2/focused-results.json`, SHA-256 `14494e22ef31e941ea97e87fb2a06dee503e9daf929279bd633150aae0d5f96e`. +Broad affected-library acceptance: 1,334 complete library tests (Cranelift 134, Comptime 624, HIR 229 and MIR 347), plus 25 selected Driver tests (library 14, backend lowering 7, generic function values 2 and named arguments 2), all with no failures or ignored tests. Receipt: +`target/native/required-owner-broad-v3/broad-results.json`, SHA-256 `4241e0b95302f388f2d748f2c8cbb6599262541d66b1af72723ed479c82e5797`. +The broad run's final guard authenticates 2,099 current Source/fixture inputs +and 68 unchanged Runtime artifacts. Its library run completed successfully in +v2; v3 resumed only the Driver batches after independently checking unchanged +inputs. The later test-only restoration extension passes one focused control: +`native-codegen-recovery-2026-10-09/scalar-v9/required-owner-restoration-acceptance.json`, SHA-256 `a6ba8bf827ff749e400c7460ef7cf5d39399c541b4f1c77236fc06dc19f691e0`. +It checks same-program failure, full metadata restoration and successful reentry. +This changes no production code. The earlier 20-test and native receipts precede +that test-only extension and retain their own snapshot; the final breadth +includes it. The two native fixtures and their expected streams remain exact. + +Both unchanged original selectors, contextual comptime and refinement +declaration contexts, pass two Debug native main executions and two interpreter +main executions with Source present. Receipt: +`target/native/required-owner-native2-v2/after.json`, SHA-256 `a8e1014f3c7f79d10bff2b0077f353a9357d69f74b5486681d3f90f3ce9f225d`. +Their original stdout and full rendered debug-event stderr match the reference. +These gates request no Release source check, execute no Release runtime, and +invoke no Verify/Property suite. They supply no Source-deleted lifecycle credit. +The existing measured Runtime archives are reused unchanged; guarding the +Release bundle does not establish Release execution. + +These results close the bounded local RequiredOwner regression. Full workspace +and fixed-inventory conformance, exact-head MSVC/GNU and clean installed +distribution remain separate obligations. The generated VerifySuite transition +and Verify/Property capture-owner repairs retain their own evidence scope. +The rough 87% estimate and fixed 207/182 inventory denominators remain unchanged; +this does not complete the native-codegen goal. diff --git a/docs/completed/native_codegen/native_required_resource_hooks.md b/docs/completed/native_codegen/native_required_resource_hooks.md new file mode 100644 index 00000000..622a0429 --- /dev/null +++ b/docs/completed/native_codegen/native_required_resource_hooks.md @@ -0,0 +1,68 @@ +# Required Resource hook materialization + +This change connects explicitly required, closed pure comptime evaluation to +native Resource hook values. It does not change function eligibility: every +semantically pure function remains eligible, including project and stdlib +functions, generic and scoped selections, arbitrary pure control flow, aliases +and relays. A runtime capability or provider is never available to that worker. + +The shared reference oracle must evaluate required expressions using the same +CheckedResourceProgram identity before registering runtime code or installing a +provider. A failed evaluation or absent exact cache entry is an error; runtime +evaluation of the comptime body is not an acceptable oracle. + +The existing public span/context cache is compatibility materialization data, +not Resource authority. An opaque proof is minted only from the private +program-bound checked required cache. It retains the original required-region +key, owner, outer expression occurrence and complete concrete specialization and +scoped selection. The same checked program and manifest must authenticate the +selected hook. A public mirror insertion, foreign program with identical Source, +copied or resealed expression, changed context, stale hook or missing evaluation +cannot mint that proof. + +HIR consumes that opaque proof through an acyclic dependency on jett_comptime. +This precise compiler-internal coupling avoids a publicly forgeable tuple of +program, span and DefId. HIR preserves its immutable original ResourceSourceArchive +and records an authenticated materialized view of the original Comptime +occurrence. It must not overwrite the original Source, reseal a replacement as +original, or pretend required evaluation was a runtime call. MIR verifies that +mapping at the exact current occurrence and through canonical CFG/local remaps. + +The current native value transport supports exact primitive literals, absent +optional Resource sums, and exact compiler hook descriptors. Qualified and +complex required values remain transport work; this does not restrict which +semantically pure functions may be evaluated. Required scalar rows also seal +ordinary runtime helpers without granting those helpers Resource frames or a +Resource calling convention. + +Runtime Resource operations and ordinary descriptor values are preserved while +the baker walks the program. Only required values are materialized. Runtime +reachability must not force compile-time-only pure selector bodies through a +narrow returned-hook allowlist. Such a restriction would contradict existing +pure-function eligibility rather than implement native coverage. + +The shared corpus grows from 46 to 61 scenarios using twelve new original +Source files. All 122 real-reference and 122 source-absent native runs pass in +debug and release against the unchanged matched MSVC archives. They cover all +three recipes, factory/domain/cleanup failure, alias, relay, unused storage, +immediate invocation, absent Resource sum, pure branch selection, generic/scoped +pure selection, and an ordinary runtime helper whose own required scalar value +keeps its normal calling convention. Reports require exact event order, outcome +channels, complete script consumption and zero custody before teardown. + +Nine focused MIR controls and all 251 MIR library tests pass. Genuine pre-fix +controls exposed acceptance of a changed required scalar helper and omission of +disconnected Resource extraction headers; independent body sealing and typed +header detection now reject both. Exact sum-tag branches plan both syntactic +arms while the unchanged runtime branch selects absence or occupancy. Ordinary +non-Resource sums do not gain custody classification. + +The complete codegen suite passes 100 library and 63 object-emission tests, and +all 93 driver-library tests pass. Rust/Jett formatting and diff checks pass; +all 2,168 frozen source/config inputs remain exact. The complete current +workspace, default native regression and supported-host jobs remain release gates. + +Full native coverage remains active. Returned named/dynamic/captured callbacks, +broader Resource aggregates and views, production providers, concurrency, GNU +private execution, and coherent final workspace/distribution acceptance remain +separate outstanding requirements. diff --git a/docs/completed/native_codegen/native_resource_absent_generated_bindings.md b/docs/completed/native_codegen/native_resource_absent_generated_bindings.md new file mode 100644 index 00000000..b921d152 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_absent_generated_bindings.md @@ -0,0 +1,139 @@ +# Checked absent For and Match bindings + +The reference repair passes all 554 comptime tests, including the original +Source17 replay and nine focused tests with 60 Source entry observations. +Coherent-revision workspace checks remain separate; these reference results +establish no native aggregate acceptance. + +The reference path preserves existing checked Source semantics for generated +list elements, map values and enum payloads whose types can contain Resource but +whose selected values are recursively inactive. It adds no Source syntax or +language permission and does not transport occupied aggregate custody. + +## Original control and binding proof + +The proof joins the exact original For or Match statement to its current checked +attempt and active body. It retains binder object identity and ordinal, resolver +definition, declaration span, concrete TypeId, immutable binding mode and exact +expression facts. Match additionally retains its selected arm, variant and +payload cardinality, respecting concrete/scoped static selection and the first +matching Source arm. Equal spans, copied controls/binders, another checked +program or a changed body cannot substitute for that correspondence. + +Preparation validates the complete selected parent. Every generated child is +checked against its exact retained type by the existing recursive absence-shape +validator before publication. Nested occupied ordinary fields, keys, elements, +variant/state payloads and nominal/concrete headers remain checked; physical live +Resource carriers and grants remain refused. A nominal refinement tag cannot +replace its ordinary predicate proof. + +Resource-bearing children enter the existing declaration binding slot as ordinary +evaluated values with empty custody. No owner ticket, provider grant or aggregate +custody path is minted. The strict missing-slot identifier guard remains intact. +View For loops authenticate the original control and use the existing transport +to retain their parent binding; move-only elements/values keep their checked view +mode while implicitly copyable map keys keep their ordinary owned mode. +Generated scopes retire on ordinary completion and selected control/error exits. +Unrelated live owners retain their existing tickets and normal cleanup; admission +does not require global owner or registry counts to be zero. + +## Retained predecessor evidence + +The unchanged Source17 baseline admits all fourteen wrappers in both checking +profiles. At parent `f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`, 22 of its 28 +reference attempts match their exact oracles. Six attempts in `02_absent_tokens`, +`04_absent_map` and `10_absent_choice` fail at ReferenceCall with +`Resource source binding has no live cleanup custody`. This is a failed baseline, +not an accepted generated-binding implementation. The retained receipt is +`target/native-resource-absent-aggregate-baseline-evidence/after-v1-wrapper.json`. + +The independent original gate passes 120 constructor calls across twelve +constructors, five execution purposes and both profiles, plus the selected +required/verify/property controls. All 3,696 frozen baseline inputs remain +unchanged. Its receipt is +`target/native-resource-absent-aggregate-baseline-evidence/after-v1-all.json`. +The original 3,600-byte Source17 prefix remains exact, SHA-256 +`c8e74848356a5b3d09ab6659318b1e1292718d34443f7f37222bafd6f3eed0ff`. +Existing constructor/refinement/required-value/verify/property bodies and exact +ordinary Source13 error oracles are preserved. + +## Successor reference acceptance + +The integrated candidate passes **nine focused tests**, zero failed, covering +exact original-control/binder/ordinal/body/type facts, recursive parent/child +shape rejection, occupied/grant rejection and existing empty-custody publication. +Its **60 Source entry observations** comprise **56 unchanged original-wrapper +entries** (fourteen Sources, both profiles, repeated entry) and **four genuine +view/Continue/Break/parent-reuse entries**. Exact success and ordinary Source13 +error oracles, empty provider/debug observations and full entry restoration pass. +The unrelated live-owner control keeps the owner live across generated-binding +publication and retires it exactly once through its existing outer cleanup. + +`target/native-resource-absent-generated-binding-repair-evidence/after-v3.json` +records actual session **23693**, terminal **5bc44f**, exit **0**, at committed +parent `c14189d9d95b09a7613651ccf3c2dade2c308994`. Its SHA-256 is +`b4d8582032012cb05999eab2c5527fe4bc881253e339872be1205ce73cfc12d8`. +The matching before-input receipt is `inputs-v3.json` in the same directory, +SHA-256 `f1c18f376491a429d6de6e24270ee957b1b0851db016d171a306ab087ee3b59d`. +All **3,709 frozen inputs** remain unchanged through the focused gate. The +fourteen original wrappers and the exact original Source17 prefix remain byte +preserved. The receipt explicitly records `native_execution_accepted: false`. + +The complete `jett_comptime` gate independently passes **554 tests**, zero failed +or ignored, at the same committed parent. Its separate original wrapper replay +records **28 attempts / 28 matched / zero failed**. The original **120 constructor +calls** pass across five execution purposes and both profiles. Namespace constant +17, required primitive 34, original verify blocks and **100 property iterations +per profile** also pass. These totals remain distinct from the 60 focused entry +observations, which run again inside the complete crate gate. +`target/native-resource-absent-generated-binding-repair-evidence/full-comptime-after-v3.json` +records actual session **22882**, terminal **e0c10b**, exit **0**; its SHA-256 is +`73f109efba6c69cf72f92abb9d8bd5cd5f1eeda4318b61f57ea1b0bf86eee883`. All **3,709 frozen inputs** +remain unchanged, the original Source17 prefix stays exact, and +`native_execution_accepted` remains false. + +| Gate | Actual outcome | +|---|---| +| Original-control/binder/shape and custody checks | Accepted in the nine-test focused gate | +| Unchanged fourteen wrappers in both profiles, repeated entry | 56 exact entry observations accepted | +| Genuine view/Continue/Break/parent-reuse control | Four exact entry observations accepted | +| Unrelated live-owner retention and exact cleanup | Accepted focused control | +| Separate original 28-wrapper baseline replay | 28 matched / zero failed in the full crate gate | +| Original 120 constructor calls and required/verify/property controls | Accepted full crate replay | +| Full comptime crate | 554 passed / zero failed / zero ignored | +| Coherent-revision workspace checks | Separate gate still pending | + +The earlier failed Rust/source-policy diagnostics remain retained as predecessor +evidence. Original Sources and exact oracles remain unchanged; failed compilation +or execution attempts are not counted as successful reference gates. + +## Independent native obligation + +All 28 baseline native attempts pass MIR lowering and structural validation, then +fail at public ObjectEmission with `CodegenError::InvalidMir`: +`pending ResourceOwnershipPlan: occupied Resource layout needs its dedicated aggregate or qualification transport`. +No object or linked native execution is accepted by that baseline or by this +reference repair. The original aggregate returns and refined contract remain +required compiler transport work; removing exports or pruning those declarations +would not satisfy the gate. Occupied aggregate custody and broader For/Match +Resource paths remain required. The full native-codegen goal remains active at +the rough 87% planning estimate with unchanged inventory denominators. + +## Final formatted fixture + +Canonical formatting removed five appended blank lines while keeping the original +3,600-byte Source17 prefix exact. The focused nine tests / 60 entry observations +pass again on those final bytes. All 3,709 inputs remain unchanged during that +gate; production files remain exact from the complete 554-test replay above. +The receipt is +`target/native-resource-absent-generated-binding-repair-evidence/after-v4.json` +(SHA-256 `630e5f3c47037efadbdab9a3deec49b7560a1ca8c2b5150a43aa10d049e19d72`), actual session 29722, terminal `bfe7a5`, exit zero. + +## Integration with native reflection bridge + +The combined candidate retains both reflected-field positive controls and the +aggregate baseline module. Its full comptime gate passes 556 tests, zero failed +or ignored, with all 3,720 inputs unchanged during execution. The sole merge +conflict appended both original test-module registrations. The receipt is +`target/native-resource-absent-reference-pr-integration-evidence/after-v1.json` +(SHA-256 `e29fb085aeded7a534ea01a603bc974b468bcf3a952bcc4618270b60c2831854`), actual session 72978, terminal `24c775`, exit zero. diff --git a/docs/completed/native_codegen/native_resource_aggregate_carriers.md b/docs/completed/native_codegen/native_resource_aggregate_carriers.md new file mode 100644 index 00000000..3333d9f9 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_aggregate_carriers.md @@ -0,0 +1,245 @@ +# Typed Resource aggregate transport + +Status: typed absence transport, current compiler proof checks, the v20 whole- +workspace gate and all eleven v21 MSVC-v3 native regression gates pass. This +work preserves the existing Source +language and full native parity goal, with no aggregate, refinement, machine or +generic Source exclusion. + +The whole original absent aggregate Source contains twelve exported constructors, +record/generic/enum/machine/qualified declarations, an unused refined view contract, +required controls, verify and property100. Fourteen wrappers preserve its exact +prefix. The historical pre-carrier baseline passed MIR lowering and public MIR +validation but failed ObjectEmission in both profiles because the single-leaf +ownership/layout model could not represent those declarations. The bounded +object/native checkpoints below supersede that historical failure. + +The selected replacement is a private typed carrier plan with a complete original +type graph and opaque runtime roots. Constructor-issued roots retain nominal owner, +concrete generic arguments, ordered fields/payloads, selected variant/state, +qualification and checked refinement proof boundary. Root generation and typed +paths identify each projection; equality of physical layout or type spelling is +not authority. Full original HIR/type/Source and current MIR/CFG witnesses are +validated before the plan is issued. Existing single-leaf custody remains separate. + +Zero live leaves is the first connected implementation. Empty collections, each +`none` shell, result failure data and inactive enum/machine payloads preserve their +complete selected data. A type's potential Resource paths are not removed by +actual absence. Latent occupied view formals and refinement metadata remain in +their original interfaces. Unimplemented live tree operations refuse before +effects while the same root/path interface awaits individual leaf custody support. +Copied physical Resource or grant handles never receive that authority. + +Wire v3 adds typed graph, carrier slot/constructor/projection and Source carrier +roles. Strict accepted v1/v2 decoding and all old tag meanings remain. Fixed-width +status/opaque-u64 C ABI v1 records remain selected; actual new leaf symbol/layout +checks are required. Ordinary ownership fields cannot contain Resource-table sum +or owner handles. Typed observations route exact carrier lengths, keys, tags, +fields and sequence extraction through dedicated leaves. + +An explicit borrowed-sum adapter retains carrier parent/path/generation and lease +while presenting selected optional/result children to the existing sum-loan +protocol. Owned extraction transfers once. Original call lexical staging, abandoned +prefix retirement before Return operands, Default joins, body/cleanup separation, +provisional Return publication and one-shot retirement remain unchanged. + +Bounded whole-Source17 object/native gates and current fresh-v3 regression pass +as recorded below, including the unchanged 191-case and reflected Return gates. +Broader lifetime/purpose acceptance remains required. + +Legitimate occupied trees, multiple kinds and dynamic paths, aggregate replacement, +machine transitions, refinement construction, recursive nominals, other reflected +families and derived or aggregate exceptional native reentry remain full-goal +follow-ons. Original Source06 single entries and original-then-READY same-grant +reentry were accepted at f4c63a0f and now pass fresh matched-v3 regression at +v21. Earlier wire-2 and bounded v15 absence receipts remain separate historical +evidence; current post-repair regression is established by v21 below. Existing checker/reference semantics determine live +replacement and destruction order; record any unresolved tradeoff before +implementing new behavior in Rust. + +## Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +## Current workspace and wider-language observations + +The earlier v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +At v21, Source13's repeated ordinary error remained pending; the v24 +same-grant gate below closes that bounded obligation. Occupied resources, wider +whole-language parity and the successor's exact-revision CI remain pending. +The rough 87% estimate and fixed 207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +## Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +## Remaining acceptance + +The earlier whole-workspace gate passes at v20, and all eleven matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. These remain independent +historical receipts; current ordinary-error regression and workspace acceptance +are recorded at v24 below. Earlier accepted GNU Source-prefix and Return +documentation and wire-v2 receipts retain their independent historical scope. +The successor's exact-revision CI remains pending. No GNU runtime-archive +acceptance is inferred from these local MSVC results. + +Occupied aggregate transport/replacement, broader Resource-valued Default +and generic/nested reflected lifetime compositions remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + +## Accepted ordinary-error successor checkpoint (v24) + +The changed ordinary runtime passes its measured **307-check** suite with fresh +matched MSVC `measurements-02` archives. The accepted twelve-gate native scope +covers **604 Source-deleted executables / 728 entries**: the new all-fourteen +same-grant gate contributes **28/56** from session **62286**, including both +Source13 Bounds errors and original body1/cleanup0/Body completions; session +**76385**, terminal **d30da4**, passes the eleven predecessor gates with +**576/672**. The latter includes full **191-case/382-execution** regression from +**113 distinct Sources**, carrier, reflected body, owned Return and original +Source06. No separate nested callee completion is inferred from Return root +observations. + +The fresh whole workspace passes **3,614 checks, zero failures and 14 ignored +tests**, across **66 nonzero / 92 total groups**, at actual session **75810**, +terminal **0f5a7b**. Independent verification at **58261 / 75fabb** authenticates +all **3,763 source inputs / nine fresh and nine historical runtime artifacts**, +HEAD/inventory, both native binaries and fresh02 provenance/log hashes unchanged. +The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +The v20/v21 receipts above retain their accepted historical scope; current +ordinary-error regression is established independently by v24. Earlier +Source13-pending statements describe their original checkpoint and are now +superseded for this bounded same-grant obligation. See the +[ordinary terminal-error note](native_resource_ordinary_error_reentry.md) for +the exact archive, binary and per-attempt messages/retirement evidence. + +Local MSVC results provide no GNU runtime-archive or full platform acceptance. +The separate normal-main linked baseline is complete: sixteen observations, +twelve exit0 and four exit71, from eight exact opaque objects reused with +Debug/Release runtime artifact profiles; all fifteen raw-probe Sources remain +preserved. This is baseline collection without a desired Source oracle, new +feature acceptance or ordinary public-Main context-retirement observations. +Broader ordinary parity and fixes remain open. Occupied custody/replacement, broader Default and +generic/nested reflected lifetimes, wider language parity and the successor's +exact-revision CI remain open. The rough **87%** estimate, fixed **207/182** +inventories and full 100% goal do not change. + +## Occupied named struct checkpoint + +Fresh local MSVC static-CRT acceptance on 2026-10-12 passes the three valid +occupied-struct entry Sources on both measured runtime profiles. The two gates +cover partial prefix retirement, reversed named arguments, cleanup failure and +same-context reentry: 44 native executions/66 entries. The original twelve +predecessor gates pass 604/728, and additional pipeline/control-flow subset +gates pass 106/106 against the same archives. All sixteen Resource gates pass +754 executions/900 entries overall. All 2,314 workspace library tests and the +six-object gate also pass. + +The discarded historical raw-artifact packet is not evidence for this integration. +Current Rust gates check decoded reports and exact Source oracles; Source and +compiled-artifact guards pass. Broader occupied collection, recursive/generic, +projection/replacement, reflected and captured lifetime compositions, GNU and +installed-distribution acceptance remain required. The rough 87% estimate and +fixed 207/182 inventories stay unchanged. See the +[occupied struct note](native_resource_occupied_structs.md) for scope and limits. diff --git a/docs/completed/native_codegen/native_resource_borrowed_sums.md b/docs/completed/native_codegen/native_resource_borrowed_sums.md new file mode 100644 index 00000000..79b6d1b8 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_borrowed_sums.md @@ -0,0 +1,100 @@ +# Native borrowed Resource sums + +The full native-codegen parity goal remains active. This note records the verified local +implementation family after required value materialization at 17d6c841. +It implements the existing view rules; it adds no ownership or lifetime syntax. + +## Source and evidence + +The shared conformance corpus contains 73 scenarios. Sources 45 through 56 add +written and bare View actuals for None, Some, Fail and Ok; two named-argument +aborts after an earlier borrowed sum; and two handled borrow-domain failures. +All 146 reference executions passed in both profiles before production changes. +The original native baseline first refuses Source45 at HIR lowering, File0 +186..236: a handled view is not yet recognized as a stable local origin. +The expanded corpus now passes all 146 Source-deleted native executions against +freshly measured Debug/Release MSVC runtime archives. Exact events, consumed +scripts, failure channels and zero ordinary/Resource obligations before teardown +remain mandatory. This supersedes the earlier 61-scenario / 122-run checkpoint. + +## Existing source contract + +An immutable typed declaration whose initializer explicitly views the handled +success payload may borrow from an explicitly viewed optional/result binding. +Its exact Optional or Result kind, success payload type and original binding +identity remain checked. The failure branch in this family exits the function. +An alternate Default, including one in a conditional before a final Return, +needs its own backing/lifetime proof and cannot inherit the original sum origin. +Defaults belonging to nested handlers are distinct from the outer continuation. +Temporary producers cannot acquire stable alias identity by annotation alone. + +Written View actuals keep the caller's sum and sole occupied Resource owner. +A bare owned actual into a View formal transfers the entire sum into owning +argument storage; the callee only borrows that holder. Caller source argument +order governs evaluation even when named formal order differs. A later argument +failure skips the callee, ends all prefix leases, and drops each acquired holder +once. None and Fail carry no occupied Resource token or finalizer. + +## Proof across compiler phases + +Checked binding metadata retains the exact concrete body's resolved backing +DefId. HIR verifies the complete typed handled-view initializer against that +immutable local chain, without changing general assignment-root recognition. +The private checked Source archive supplies authority; public metadata or a +matching span/type never grants Resource execution permission. + +MIR retains the original function, declaration, handled expression and target, +sum and endpoint types, failure block, source/alias headers, and concrete scope. +Canonical lowering introduces nonowning sum and payload headers authenticated +against that original occurrence. A fresh SumTag and its exact Branch select +Some/Ok before projection. Borrowed extraction never performs an owning SumTake +or manufactures an owner slot. Altered tag sources, arms, headers, current bodies, +foreign archives and truly disconnected extractions must fail independent proof, +including before canonical pruning or remapping. + +## Native execution + +Conditional sum views need a distinct runtime representation from direct +Resource loans. Occupied payload views are bounded child loans; absence creates +no occupied loan. Fail's ordinary string is observed/copied without consuming the +caller shell. All handles remain tied to the exact attempt, frame, registered +nominal shape and payload path. The original shell cannot move, be replaced, be +taken or be destroyed while any shell/payload view is live, including absent +shells that have no Resource token to enforce that restriction. + +Children retire before their parent shell view; source call retirement precedes +argument-holder cleanup. Registration rows must authenticate conditional shape, +loan source, selected path, operation and frame rather than infer permission +from a handle tag. Any runtime schema or implementation change requires fresh +Debug/Release single-runtime archives, symbol/source measurements and a pinned +receipt before the complete Source corpus may count as native acceptance. + +## Verification and remaining work + +The complete reference suite passes 521 tests, including all 146 current Source +executions; all 93 driver-library tests pass. A separate test correction verifies +Source33's exact scalar and hook proofs and fixes the previous commit's sole +Ubuntu/Windows CI failure. Rust and all twelve new Source formatting checks pass. +All 2,188 compiler/source/config inputs revalidate; the final MIR formatting change +is an exact error-return line wrap, followed by another complete 254-test pass. + +All 305 checker, 203 HIR, 254 MIR and 215 runtime library tests pass. Three +focused MIR groups verify actual Source profiles, altered headers/guards/bodies, +foreign or missing proofs, disconnected extraction and exact loan ordering. Six +runtime groups cover conditional shell custody, failure companions, child loans, +argument-prefix aborts, forged registration and C output preflight. All 102 +codegen library and 63 object tests pass, including unchanged prior forgery +controls. Fresh custody validation precedes local-view queries that consume its +proof, preserving forged-graph rejection as invalid MIR. + +Both freshly measured archives contain 37 Resource leaves, eight private test +exports and one main, with static CRT and the actual ordered native library list. +Their independent receipt is pinned in the native harness. Neither this private +MSVC gate nor earlier platform CI proves current full workspace/GNU acceptance. +Broader aggregates, qualified values, callbacks, reflection, production providers, +asynchronous execution and complete platform acceptance remain in the full plan. + +The first constructor inventory is limited to root-body Resource aliases. +Nested/scoped Resource alias declarations, ordinary non-Resource borrowed sums, +and alternate Default payload lifetimes remain separate unverified native paths. +Checker/HIR acceptance alone does not establish native execution for these paths. diff --git a/docs/completed/native_codegen/native_resource_checked_program.md b/docs/completed/native_codegen/native_resource_checked_program.md new file mode 100644 index 00000000..33764da5 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_checked_program.md @@ -0,0 +1,123 @@ +# Immutable checked resource program handoff + +Status: implemented compiler handoff prerequisite. Its original isolated step +over `b7180bd9` passed 43 focused Resource groups and the resolver/typechecker/HIR +suites. The later private reference lifecycle now passes 36 focused tests, as +recorded below; broader acceptance of that Resource candidate remains pending. +The accepted authority is the opaque runtime resource contract in +`docs/completed/opaque_runtime_resource_contract.md`. + +The handoff must own the parsed merged program and perform its own resolution +and checking. It must not accept an independently assembled Module, +ResolveResult and CheckResult as proof that those values describe one program. +The constructor consumes ParseResult, explicit loader SourceOrigins and the +existing private Rust resource catalog. Parser, resolver and checker Error +diagnostics each prevent publication of a successful snapshot. All hook +identities and complete signature modes are validated, including unused hooks. +Warnings remain observations and do not become errors. + +The sealed snapshot keeps private Module, ResolveResult, CheckResult/interner +and SourceOrigins fields. Shared immutable access preserves generic and scoped +body facts, call permutations, binding modes, reflection and exact definitions +from that one check. There is no mutable or into-parts access and no constructor +from previously checked parts. Compiler input remains trusted Rust loader input; +this does not authenticate malicious Rust code or grant provider authority. + +Ordinary checking must still return source diagnostics for invalid bodies. +Separate crate-private catalog identity/shape checking from the public phase +handoff validator. The latter additionally rejects checker Error diagnostics; +checking with a valid catalog can return an Error-bearing CheckResult for +repair, but that result cannot pass the successful-program handoff. + +This step enables no live Resource, provider, cleanup ledger or native carrier. +The production catalog remains empty. The private reference driver now retains +the sealed checked program instead of reparsing after checking; its isolated +follow-up and commit-specific acceptance are recorded in +[the reference preparation note](native_prepared_reference_driver.md). +Raw DefId/TypeId integers are local to +the retained program, not proof that a value came from another session. Runtime +callable identity, ownership retention and execution effects must be tied to +that snapshot before dispatch. Resource-only finalizer eligibility and current +source-checker/lowering view-retention differences remain open prerequisites; +no capability signature, source purity rule or implicit borrowing rule is +changed by this handoff. + +Source-driven gates must cover real accepted private calls, parser/resolver/ +checker rejection, release policy, warnings and empty catalogs, unused hooks, +origin/catalog refusals, preservation of concrete generic and call-order facts, +and independent mutation of original input copies after preparation. These are +the selected pre-Rust test matrix; executed results are recorded below. +Whole-language native coverage remains +about 85%; the fixed inventory is 207, with 182 object obligations and 464 +supplemental native conformance cases. + + +## Implemented boundary and executed evidence + +`CheckedResourceProgram::prepare` stores one owned parse/resolve/check session, +with immutable accessors for the original module, full resolver/checker facts, +loader origins and parser diagnostics. The reference follow-up extends +`ResourceProgramError` to retain observations through the failed phase, including +preceding parse/resolve warnings. Structured metadata variants keep their typed +cause and observations; diagnostics() returns Some even for an empty prefix, +and Error::source preserves the cause. Warning-only parse +observations are retained; the warning test injects a compiler diagnostic into a +real ParseResult and does not claim that the parser emitted that warning. + +The public hook validator refuses an Error-bearing CheckResult with +`SourceCheckingFailed`. The diagnostic checker uses a separate crate-private +identity validator, so normal E0311 source-body failures remain inspectable and +cannot be reclassified as valid execution inputs. Existing malformed/unused +hook identity, complete mode and foreign TypeId checks remain intact. + +The frozen pre-format source draft is patch `1d247656` and manifest `b6306c34` +under `target/native-resource-checked-program-draft/`; root applies and formats +it in isolation. All **43 focused Resource groups**, including seven new +handoff groups and a strengthened existing invalid-body assertion, pass in +`target/native-resource-checked-program-focused.json`, ending +`2026-10-03T13:23:01.1489478Z`. The broader **454 library tests** (139 HIR, 54 +resolver and 261 typechecker) pass in +`target/native-resource-checked-program-phases.json`, ending +`2026-10-03T13:24:25.9374022Z`. All five compiler source hashes remain unchanged +through both gates. These are compiler-source tests, not live Resource execution. + +The accepted predecessor is exact clean `b7180bd9`. Its full workspace ends at +`2026-10-03T13:18:31.4769949Z` with all 92 result groups, 464 native conformance +cases (958.70 seconds) and all 182 object obligations (four-test manifest gate, +650.63 seconds). Its supported-host workflow +[37123835396](https://github.com/vycdev/jett/actions/runs/37123835396) now passes +all four Windows/Linux build and installed relocation jobs, finishing +`2026-10-03T14:14:19Z`. The earlier exact `616b51b9` workflow +[37119573097](https://github.com/vycdev/jett/actions/runs/37119573097) passes all +four Windows/Linux build and installed relocation jobs, finishing +`2026-10-03T13:14:20Z`. That result does not validate this new handoff. + +The snapshot proves parsing, resolution, checking and hook shape only. It does +not bypass explicit comptime or verification failures, retain baked runtime +authority or authenticate a foreign raw DefId. The separate reference bridge +repairs post-preparation rereading without granting live Resource authority. +Typed caller ownership now carries the existing Rule24 effects separately from +callee access. The later private reference lifecycle adds program-bound hook +descriptors, reached-hook purpose checks and one-owner custody ledgers; its +36-test acceptance is recorded in +[the resource-hook note](native_resource_hook_identity.md#reference-lifecycle-execution). +The successful snapshot alone grants none of those execution rights. Broader +scoped/reflected/indirect/worker cases, production providers and native drop +elaboration remain required. + + +## Retained reference integration + +The isolated follow-up over `94bb6b2e` keeps public BuildResult unchanged while +retaining the exact checked program and primary FileId0 entry privately. Its +focused gate passes all 46 Resource groups and four new driver groups with both +source hashes unchanged. The tests retain earlier warnings/metadata causes, +run the original primary/support program after source deletion or mutation, +keep comptime/verify/runtime observations once, and refuse failed or missing +handoffs before provider setup. All 544 driver/HIR/resolver/typechecker tests, 598 frontend +fixtures and eight backend-lowering regressions also pass with stable source +hashes. New full-workspace/platform acceptance for this follow-up remains +pending. The detailed receipt and predecessor failure +context are centralized in +[the reference note](native_prepared_reference_driver.md#verification-and-remaining-boundaries) +and [acceptance audit](native_acceptance_audit.md#retained-reference-preparation). diff --git a/docs/completed/native_codegen/native_resource_control_flow.md b/docs/completed/native_codegen/native_resource_control_flow.md new file mode 100644 index 00000000..2f7a5843 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_control_flow.md @@ -0,0 +1,155 @@ +# Native Resource control-flow transport + +The native-codegen goal remains active. This successor preserves existing +Resource custody through consuming Resource-free list iteration and ordinary +Resource-free enum selection. It adds no Source syntax, ownership rule, provider +authority or runtime ABI. The evidence below is bounded local MSVC acceptance; +the complete successor native corpus passes; coherent-revision gates remain pending. + +## Genuine Source baseline and expansion + +The original five Sources01..05 place the established owned Optional/Result +backing, written handled payload view and Resource observation inside genuine +`For` and `Match` bodies. Sources01..03 cover two-iteration Fallthrough, Break +and Continue; Sources04..05 select each arm of an ordinary exhaustive unit enum. +Each backing is reused by an owning extraction and explicit close after the +lexical boundary. Their 22 script rows also cover construction failure, +first/later borrow failure and explicit-close cleanup panic. No While or +direct-call substitute establishes these Source forms. + +With unchanged production code, all 22 cases passed 44 real-reference executions +in Debug/Release, while all 44 final native codegen attempts refused emission. +That independent historical baseline is recorded in +`target/native-resource-control-flow-source-evidence/baseline-after-v1.json`. +The first v2 repair then passed all 22 cases / 44 Source-deleted MSVC executions; +Sources07/08 were still unverified drafts at that checkpoint. + +V3 includes those consecutive and nested-loop Sources07/08 and eight additional +rows. The accepted focused packet now contains seven Source files and 30 cases / +60 profile executions. It preserves the accepted 161-case / 322-execution +predecessor as an exact Case-row prefix and retains its Support bytes. The +complete successor contains 191 cases; its full 382 reference executions have +passed, and its full 382 Source-deleted MSVC native executions now pass. + +Reference observations check exact outcomes/events and zero owners/registry +entries before context teardown. That observation tuple does not measure script +exhaustion or every native custody counter, and reference success does not +establish linked native success. + +## Constructor-owned transport + +Fresh Resource CFG analysis admits only a structurally proven Resource-free +consuming `list` operand with one exact ordinary element binder, or an ordinary +Resource-free enum discriminant with distinct checked variant payload binders +and the original exhaustive edges. Resource operations in their bodies retain +their independent custody plan; the iterable, binder and discriminant do not +create a Resource owner or loan. + +Sequence preparation validates the complete before-witness, then reconstructs +the finite canonical edit itself. It appends exactly three temporary headers in +cursor, length, source order; initializes source, cursor zero and length at the +unique original preheader; prefixes the body with consuming element extraction +then cursor increment; and replaces `ForEach` with `cursor < length` using its +original body and exit edges. Exact types, spans, header metadata, statement +order and the entire resulting graph must match. Added blocks, redirected edges +or an otherwise convincing edit transcript cannot grant authority. + +Only after that equality check may the two-statement body prefix advance current +borrowed-projection, lexical-exit and normalized-call sites. Independent +original/current association seals and descriptor-body checks remain required. +The archival declaration, initializer, backing and Source facts stay distinct +from current sites. Consecutive or nested normalization must validate each +finite transition; a preceding successful edit grants no general rewrite +authority. Duplicated, missing, moved, retargeted or independently resealed +projections/exits fail, including before canonical pruning. Preparation without +another edit preserves every witness. + +Existing strict joins are unchanged: operation stacks, arrived prefixes, +retirement obligations and leases must agree, and a moved/uninitialized versus +live owner still requires its own conditional cleanup proof. Certified lexical +exits end child payload loans before parent shell loans and clear only their +nonowning aliases, preserving the backing for its later owning use. Final native +codegen still refuses a raw, unprepared `ForEach`. + +## Distinct acceptance gates + +The historical v2 receipt, +`target/native-resource-control-flow-source-evidence/acceptance-v2.json`, records +22 cases / 44 Source-deleted MSVC executions in Debug/Release (228.02 seconds), +three focused MIR groups and 450 affected phase checks: 107 codegen library, +63 object, 279 MIR library and one CFG test. Its compiled receipt-pin forgery +control passes. All 3,656 frozen inputs, its recorded base HEAD, both measured +archives and six nested receipts revalidate unchanged. Those phase totals remain +historical v2 evidence, rather than a newly measured v3 phase gate. + +The v3 receipt, +`target/native-resource-control-flow-source-evidence/focused-acceptance-v3.json`, +records 30 cases / 60 Source-deleted MSVC executions in both profiles +(204.35 seconds). Native reports require exact outcomes, channels, messages and +events, exact script exhaustion, and zero ordinary and Resource custody +obligations before teardown. These are separate from reference observations. +All 3,658 frozen inputs and both measured archives plus six nested receipts +revalidate unchanged at recorded HEAD +`50ad73a0693c502d1fa05720091b80a4ab623104`. The receipt SHA-256 is +`7f41c389daa268942afe8a645085daac18d746f553f3be32e93d14f798fc30ea`; +its executable SHA-256 is +`10922730f3f84fa314635f90f8e88ab984aa4212c68b8c0141630a53510367d5`. +The recorded HEAD alone does not establish a later committed candidate gate. + +Four focused MIR groups pass. The separate reference gate passes 89 Resource +groups, including the complete 191-case / 382-profile-execution reference corpus +and the 30-case / 60-execution control-flow focus. Final object preflight +separately emits all 60 control-flow objects. These results are recorded in +`reference-after-v3.json` and `object-after-v3.json` in the same evidence +directory; object emission alone does not prove linked execution. + +The separate full v3 receipt, +`target/native-resource-control-flow-source-evidence/full-acceptance-v3.json`, +records all 191 cases / 382 Source-deleted MSVC executions in both profiles +(1,288.83 seconds), with the same strict native report assertions. Actual session +17187 completed with exit zero. All 3,658 frozen inputs, the recorded HEAD, both +runtime archives, six nested receipts and the frozen executable revalidate +unchanged. The full receipt SHA-256 is +`08bcf865bfed8a9b49f262a6c611336260cd0446ba8517139c71ab10627fb806`. +This full corpus includes the preserved predecessor and the new seven-Source +control-flow packet; it does not include reflected Source06. +The clean base revision `50ad73a0693c502d1fa05720091b80a4ab623104` independently +passes 3,448 workspace tests across 92 result groups, with zero failures and two +ignored tests. Its 3,649 tracked inputs remain unchanged and its worktree stays +clean, as recorded in +`target/native-pipeline-50ad-workspace-evidence/after-evidence.json`. That accepted +base gate does not establish the modified successor's coherent-workspace or +exact-revision CI acceptance. + +## Remaining obligations + +The structural no-Resource proof does not cover erased interface, function, +actor, capability or `TypeConstruction` carriers. Borrowed/projected iteration, +`Never` elements and other iterator forms need their exact compiler transport +proofs. These are outstanding implementation obligations for existing Source +semantics, not new language-policy rejections. The measured Match Sources use +unit variants; payload-variant compositions need independent Source evidence. +Broader owning declarations, alternate Default lifetimes, aggregate custody, +reflected bodies and other For/Match combinations remain in the full goal. + +Source06's historical metadata-selected reflected-field baseline genuinely +reports `resource execution has no exact checked body` (`MissingCheckedBody`), +after construction and before the intended observations; cleanup still retires +its owner. Its diagnostic probe deliberately fails and does not provide an +acceptance oracle. The separate reference repair in the GNU worktree passes the original 11 +proof tests, recursive runtime and required-evaluation tests, and pure selected +Return/Break/Continue/Default controls. Its strict Resource tests also pass all +74 entry attempts across both profiles: nine original Source06 script controls, +same-provider/grant reentry after each, and ten explicit control-flow adaptations. +Exact outcomes/events, full entry-context restoration, zero owners/registry and +empty debug observations are checked before interpreter teardown. These results +are recorded in `target/native-resource-reflected-field-reference-evidence/after-v2.json` +and `after-v3.json`; all 3,662 inputs of the latter gate remain unchanged. +The native HIR/MIR/codegen ordinal bridge remains pending. This reference result +establishes no GNU native acceptance and does not add Source06 to the seven-Source +control-flow packet. + +The complete 191-case / 382-execution Source-deleted MSVC native successor passes. The successor's coherent workspace and +exact-revision CI remain pending, and no GNU or other host acceptance follows +from the focused MSVC receipt. The coarse planning estimate stays about 87%, +with the fixed 207-fixture / 182-run-pass inventory unchanged. diff --git a/docs/completed/native_codegen/native_resource_generated_startup.md b/docs/completed/native_codegen/native_resource_generated_startup.md new file mode 100644 index 00000000..431465cf --- /dev/null +++ b/docs/completed/native_codegen/native_resource_generated_startup.md @@ -0,0 +1,231 @@ +# Generated Resource object startup prerequisite + +This restored bounded implementation connects the selected Resource ownership +contract to the normal launcher. It changes no public Source policy and supplies +no production I/O provider. Authoring was based on published GNU f4/v25. The +preceding Actor/Resource/Owner/reuse/Callback compiler passed local MSVC focused +controls, private Resource gates and the complete bounded 80-row startup matrix. +Startup80v2, Resource16 compiler2 and Native32v5 are predecessor compiler +proofs after the ordinary required-value HIR repair. Normal v4 and private v8 +Runtime archives remain current; current compiler Resource and startup gates passed after the retained-archive +and ordinary wrapper-sequence repairs. The four affected libraries passed; +other library results remain separately pinned predecessor evidence. Wrapper-only entry-Scope authentication does not qualify Resource +execution: ordinary sequence transport separately excludes graph and custody +records and retains exact original/current headers and the finite body edit. +Native conformance, exact-revision CI, GNU and installed distribution remain +current parity obligations. Absent production-provider APIs are separate future +feature work. Earlier +isolated results below are historical handoff claims; they do not validate the +current restored compiler or its archives. + +The normal `jett_aot_v1_entry(context) -> u32` ABI is preserved. A Resource object +calls its own immutable `jett_aot_resource_v1_manifest` accessor into a distinct +32-byte stack record, then calls `jett_rt_v1_resource_program_begin`. Ordinary +objects do not reference either operation. The shared launcher never imports an +optional manifest symbol. + +Begin validates every known input/output/context region before any effect. Under +the same authenticated stationary-context lease, the existing installation +issuer decodes the checked layout, validates the exact entry and retains an owned +byte-for-byte copy before provider/grant publication. Generated ownership fixes +purpose to Runtime. Different wire bytes/entries and active reentry refuse; exact +reentry after completed retirement uses the existing new-attempt constructor. +No error channel is reset, overwritten or transferred to legacy session state. + +The normal provider remains Disabled. Its entry installation receives no paired +Network grant, and actual Network/resource use refuses. This is startup plumbing, +not a real creation/use/drop or production I/O acceptance receipt. The exact +Network-only/Nothing entry boundary is retained for this prerequisite; supporting +mixed ordinary capabilities such as Stdout is still a required subsequent step. + +After successful begin, every generated body/refusal return goes through normal +completion once. Resource companions first release their borrowed actor references; +the runtime then retires actor state and unfinished graph imports for the exact +Running ordinary attempt, before emptiness checks and terminal sealing. Wrong +or sealed attempt owners cannot retire that state. Actor release failure retains +the first ordinary diagnostic, marks cleanup poison and blocks clean reentry. +Later context destruction observes the actor table already drained. The core retains the original body status, first ordinary and +Resource diagnostics, first cleanup status and independent selected kind. A +separate private terminal-status field covers recorded first errors even when the +original body status is zero, without changing the existing completion record +ABI. Cleanup retains priority. Report-allocation failure retains a tombstone and +blocks new attempts; failed retirement remains observable and is never retried as +though it had succeeded. + +The normal launcher reads the retained Resource outcome and copies its selected +diagnostic while the context is alive. Ordinary contexts keep the previous value +failure path. After a recoverable entry panic, the launcher completes only a +Generated-owned Running attempt, before its existing single context destruction. +The returned selected kind distinguishes host panic from cleanup failure even +when both statuses are 255. An existing Completing attempt is refused, never +restarted. Ordinary contexts and externally managed private attempts are no-ops. + +Only `cfg(all(test, jett_resource_native_test_archive))` admits an exact already +Running Runtime attempt owned by the scripted private installer. Begin returns a +zero borrowed-attempt sentinel solely for this generated-wrapper compatibility +branch; the private runner remains responsible for completion and retained +telemetry. Production cannot adopt externally owned custody. Explicit comptime, +verify and property attempts cannot pass this Runtime-only handoff. + +The initial focused controls cover exact reentry/history, active/wrong-context +refusal, swapped layouts, Disabled/no-Network installation, output/context +overlap, reserved metadata, original ordinary first-error copying, Resource first +error/cleanup priority, recoverable panic retirement and non-runtime refusal. +These are host controls, not linked normal-archive acceptance. Required validation +also includes byte-for-byte predecessor private receipts, a normal Resource object +against ordinary debug/release launcher archives, ordinary non-Resource objects, +original body/cleanup outcomes and copied messages, source deletion and context +retirement observations. Those initial host controls supply no native execution or percentage credit. +Current linked observations are recorded below. The current sixteen original private Resource gates passed 754 Source-deleted native executions and 900 observed entries, using the fresh compiled receipt pin and unchanged compiler/source guards. The bounded startup matrix passed all 80 selected executions with Source absent, unchanged inputs and no retained case executables. + +The standalone archive contract retains 37 ordinary leaves, 13 carrier leaves +and eight private hooks, and adds exactly five normal startup exports from the +unconditional canonical `program_entry` child module. Each startup declaration +must remain an unmangled unsafe C function with its exact context, manifest, +attempt, status and output-pointer signature. The standalone Resource inventory +is 55 production symbols plus eight private symbols. Later graph or scalar +composition requires its own exact inventory and fresh authenticated receipts. +The startup source guard runs before tool discovery/output and again after the +source snapshot; receipt authentication checks the same source contract. Python +controls exercise missing/duplicate/foreign/conditional/mangled declarations, +every argument and return type, relocated names, exact text symbol definitions +and mutation between source guards. Restoration preserves the current MSVC +measurement and receipt checks while adding the recovered startup controls. +The current restoration passed all 68 Python test methods; the actual log is +`target/native/startup-python-controls.log`. The saved v2 note reported 30 Python controls passing on Windows Python +3.12 and two initial sandbox permission errors. Those historical observations +supply no current Rust runtime, native or distribution acceptance. + +The saved v2 note reported eight normal Runtime `program_entry` controls and +11 existing launcher lifecycle controls passing with direct stable Cargo/rustc, +offline/locked, explicit MSVC/static CRT, incremental zero, DEBUG zero and two +jobs. Its reported 3766-file source and 60-file Runtime guards describe that +historical isolated tree. Current restored-source controls are recorded below. +The 11 launcher controls cover existing lifecycle paths; they do not +establish linked generated Resource object acceptance. The separate ninth +Runtime handoff control is admitted only by +`cfg(all(test, jett_resource_native_test_archive))`. + +The recovered v3 successor extracts the unchanged entry instruction construction +into a private typed builder used by normal object definition and focused +controls. The controls verify the actual generated IR, startup output separation, +context forwarding, exact ABI, manifest/begin success and refusal edges, the +single completion gate and exact terminal status, private zero-sentinel branch, +and real object relocations owned solely by the generated C entry. Both codegen +optimization modes also require an ordinary object with the same entry and no +Resource startup/manifest reference. These are compiler host controls; linked +normal/debug/release distribution, source deletion and real providers remain +unaccepted. + +The saved v3 note reported four focused codegen controls passing under the same +serial bounded MSVC configuration, including both Source release policies and +both object codegen optimization selections. It also reported the entry +instruction-construction body matching v2 after newline normalization (8630 +characters, SHA-256 +`83e5961d37f88d963374f58588276e998b2f88738d7c0ecea9228756a8f65a68`). +Those historical comparisons and passes do not authenticate current codegen. +The fresh restored-source results below are separate. + +Current restoration passed all 68 Python archive-helper controls, eight normal +Runtime startup controls, four codegen IR/object controls and the 11 existing +launcher lifecycle controls. The workspace library check passed 2,371 tests +across 22 result groups, with zero failures and zero ignored tests. These results +are recorded in `target/native/startup-python-controls.log`, +`startup-runtime-controls.log`, `startup-codegen-controls.log`, +`startup-launcher-controls.log` and `startup-workspace-libraries.log`. Four fresh +callable Driver gates also pass against the changed normal launcher: 48 native +executions, recorded in `target/native/startup-callback-regressions.log`. This is +a compatibility result, with no controlled performance comparison. Host controls +and library tests are distinct from linked native execution. + +Fresh Debug/Release private archives use the current MSVC/static-CRT compiler +inputs, retain exactly 55 Resource exports plus eight private hooks and one main, +and are bound by `target/native/resource-msvc-v6-startup/receipt.json` SHA-256 +`f2675af94be6a5ff32e10157eaadf605ae3374ce9417a0a97f02286e69f42721`. +All 16 existing Resource gates pass against that compiled receipt pin: 754 +executions and 900 entries, counted from the actual v6 logs by +`native-codegen-recovery-2026-10-09/resource-gate-reuse/v6-gate-counts.json`. +This revalidates the existing private scripted-provider scope after startup; +it supplies no real-provider acceptance. + +Generated `program_unwind` selects the context's current Running attempt and +carries no invocation or thread token. Hosts must exclusively own entry and +finish for that context. A caller rejected for concurrent or recursive entry +must not unwind the owner's attempt. The unsafe call requires entry control to +have returned. The built-in launcher creates a fresh context for one entry; +these controls establish no concurrent-host ABI coverage. + +The recovered preparation packet described an 80-execution plan and performed +no builds or native runs. Its current local MSVC successor has now accepted every +one of the 80 exact case IDs: 48 normal controls and 32 private controls, 40 per +Runtime archive profile. All Source checking is Debug (`source_check_release=false`). +Resource objects cover both Jett optimization selections; the four ordinary +controls retain their supported unoptimized selection. The runner and all guards +pass; actual native exits are 60 zero and 20 expected 71 refusal/failure outcomes. +All native terminal receipts are reaped without timeout, ephemeral Sources and +case executables are removed, and the 902 compiler/Source inputs plus protected +artifacts match their afterguards. + +The 32 private controls contain exactly 40 decoded entries. Twenty-eight +unchanged-object controls account for 32 entries; four separately classified +derived borrowed-probe controls account for eight. The two unique derived +objects preserve code/data bytes, relocations, section and symbol metadata, +changing only the entry symbol name. Each derived execution retains a 48-byte +probe containing two rows of six zero u32 fields. Normal manual C controls are +counted as executions, without inventing a Source-entry total from API calls. + +The full result is +`native-codegen-recovery-2026-10-09/startup-linked-current-runner-v1/full80-v6-observations-2/after.json`, +SHA-256 `403d51214936e6c297aafe741a95fc584c825726910421d36481658e43459f1b`. +This closes the local bounded startup80 gate. The earlier private-six result is +a subset and is not added to the 80 executions. Normal Runtime-only and launcher +archives retain their measured compilation, source, COFF and symbol observations. +Their actual links use the exact ordered 14-token static-CRT library vector +measured from fresh private v6, including repeated `kernel32.lib` and +`/defaultlib:libcmt`; no separate normal compiler-emitted +`--print=native-static-libs` note was measured. This narrower vector provenance +is retained explicitly. Full coherent-workspace/native conformance, exact CI, +GNU, installed distribution, public providers and actor-graph import remain open. + +Actual object/manifest observations resolve the earlier wire3 assumption. The +76 linked Resource-object observations contain wire2 layouts; 36 runtime C +manifest checks observe wire2 at execution. The four ordinary controls contain +no Resource manifest. `actual-manifest-wires.json` in the current full80 packet +records the exact observed wire bytes. A separate occupied Source 33 object +contains wire3, but that supplemental object-only observation receives no linked +startup execution credit. The Runtime's current wire constant remains 3 with its +existing legacy wire1/wire2 acceptance; these observations require no schema +change or new provider authority. + +The full production objective still requires the public driver/catalog envelope, +closed typed TCP hooks, source-owned APIs, protected Endpoint construction, +Network restriction records, actual stream/listener payloads, readiness/deadline +and cancellation behavior, task/actor/message custody, supported-host loopback +and installed/relocated distribution. The socket behavior contract is selected; +Endpoint public representation and malformed dynamic `Network.allow` failure +policy need focused notes before implementation. Disabled cannot satisfy any of +those production provider requirements. + +## Graph follow-on candidate + +The current standalone startup inventory above remains its own 55-export +contract. The next actor graph composition adds exactly +`jett_rt_v1_resource_graph_descriptor_box`, bringing the composed Resource +inventory to 56 production exports while retaining the original 50 leaves, five +startup exports and eight separately gated private hooks. Its private checked +proof binds descriptor metadata to the exact context/program/layout and +operation/hook/signature; it supplies no Scope, Network grant or Resource owner. + +The generated entry's manifest, Runtime-only attempt ownership, single completion +gate, retained first-error/cleanup observations and private borrowed-attempt +handoff remain unchanged. The graph addition requires fresh source guards, +normal/private Debug/Release archives, exact 56-plus-eight inventories and +source-deleted linked execution. Existing Startup or Resource receipts do not +authenticate the changed graph compiler or archives. The current MSVC and GNU +measurement checks, actual compiler/linker/CRT evidence, immutable receipt pins +and compile-time pin freshness checks must remain intact. + +The recovered graph measurement successor adds six Python controls to the current +68-method suite, for a prospective 74. These are parser/receipt controls, not +graph native acceptance. Current combined graph execution remains pending. See +[the graph integration note](native_comptime_actor_graph.md). diff --git a/docs/completed/native_codegen/native_resource_gnu_gate.md b/docs/completed/native_codegen/native_resource_gnu_gate.md new file mode 100644 index 00000000..10fec567 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_gnu_gate.md @@ -0,0 +1,82 @@ +# Native Resource GNU acceptance gate + +This change prepares a private GNU Resource acceptance gate. Full GNU Rust +archive builds and Source execution remain unverified until the new CI steps pass. +The existing production package and Source-absent installed jobs remain required. + +## Trust contract + +`tools/measure_resource_native_archives.py prepare` builds fresh Debug and Release +archives on the exact `x86_64-unknown-linux-gnu` host. Cargo receives +`--cfg test --cfg jett_resource_native_test_archive` only after `cargo rustc --` +for the selected runtime package. Private archive target directories, the gate's +compiler target directory, and production package artifacts stay separate. + +The helper measures unchanged runtime/Cargo inputs, compiler and linker identity, +actual ordered `native-static-libs` arguments including duplicates, complete +symbol inventories, Cargo fingerprints, and ELF observations. Each archive must +contain the exact 37 Resource leaves, eight private test exports, and one +`main`. A separate C probe observes the dynamic GNU CRT and `-no-pie` linker +contract; that probe does not establish Resource Source execution. + +Rustup proxy invocation paths are preserved while executable bytes are measured. +Compiler overrides and wrappers are rejected, Cargo uses the measured compiler, +and actual verbose commands must match its identity. Cargo output is forced +uncolored through `CARGO_TERM_COLOR=never` and `--color never`, so command and +library observations are parsed without discarding unexpected escape bytes. +Raw fingerprint bytes and all provenance records are retained for review. + +The separate `run` command receives the independently measured receipt digest +and embeds it in the compiled test binary. Runtime environment supplies only +the receipt path; it cannot replace the expected digest. The existing local MSVC +receipt pin remains the fallback for compilation without a CI pin. The harness +rehashes runtime/archive and measured linker bytes before each link, and the +helper revalidates archives, nested provenance, and Source/compiler inputs before +recording acceptance. + +The ignored lifecycle test executes the complete shared Source corpus in both +profiles. Every execution deletes its primary and synthetic stdlib Source before +launch, then checks exact events, messages, statuses, consumed script, and zero +obligations before teardown. CI retains these measurements and execution evidence +separately from production packages. + +## Invocation + +On the supported GNU host, use a fresh output directory and a fresh gate target: + +```text +python tools/measure_resource_native_archives.py prepare --output target/native-resource-gnu +python tools/measure_resource_native_archives.py run --receipt RECEIPT_PATH --expected-sha256 MEASURED_DIGEST --target-dir target/native-resource-gnu-gate +``` + +Preparation reports its receipt path and digest. CI obtains them through the +preparation step's outputs; execution must not derive its own expected pin from +an arbitrary runtime receipt. + +## Actual verification + +At parent `d1ffd08c`, the revised harness passes all 73 accepted Source scenarios +in Debug and Release on MSVC: **146 Source-deleted executions**, in 533.06 seconds +(`bbaf50`, `target/gnu-private-resource-msvc-accepted-source-regression.log` in +the main workspace). The runtime-environment forged-pin refusal test also passes. +The final helper-only color change does not alter this Rust harness. + +Fifteen focused helper groups pass on Windows and the existing Ubuntu WSL host. +Python syntax, Rust formatting, and diff whitespace checks pass. Real GNU +`cc`/`ar`/`nm` controls accept the exact stub symbol inventory, reject duplicate +definitions, and observe dynamic CRT linking through `readelf`; their evidence is +retained under `target/gnu-private-resource-host-tools-real/` in the main workspace. +Those C controls are not Rust Resource archive or Source acceptance. + +A later narrow typing repair gives JSON list/dict reads precise readonly types +while preserving their exact runtime predicates and diagnostics, and replaces one +untyped test mock with a typed function. Strict Pyright 1.1.405 in Python 3.12 +mode independently selects both helper/test files and reports zero errors, +warnings, or information messages (`3b711f`); all fifteen helper groups also pass +after the repair (`0696a3`). The installed benchmark checker requires an explicit +typeshed path and relative include paths to select those two files. + +No accessible GNU Rust toolchain was available for local archive verification. +**Local full GNU Rust Source execution remains unverified.** Broader language, +provider, concurrency, and coherent-revision platform requirements remain in the +native parity plan. diff --git a/docs/completed/native_codegen/native_resource_hook_identity.md b/docs/completed/native_codegen/native_resource_hook_identity.md new file mode 100644 index 00000000..2ee60106 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_hook_identity.md @@ -0,0 +1,743 @@ +# Checked private opaque resource hook identities + +Status: the identity prerequisite is implemented. Its original focused receipt +passes all 14 tests, and that earlier compiler/object/driver/frontend/lowering +checkpoint passes. The subsequent clean `616b51b9` workspace and all four +supported-host jobs also pass. These are historical identity/static-guard +receipts, not broader acceptance of the later reference lifecycle. +The private reference follow-up now passes 83 Resource-focused tests; its latest +scope is recorded below. The production catalog stays empty. Whole-language native +coverage remains about 85%, with the fixed 207-fixture inventory and 100% +objective unchanged. The [acceptance audit](native_acceptance_audit.md) +keeps local, reference and platform evidence separate. + +## Selected boundary before Rust + +The [opaque resource contract](../opaque_runtime_resource_contract.md) +requires an owned resource to come from an associated private trusted operation. +Authority follows loader origin, resolved declaration identity and complete +checked ownership modes. A callable name or matching runtime layout cannot +select that operation. The pre-Rust identity contract is frozen in +`target/native-opaque-resource-hook-draft/contract.md` (SHA `8dbd1458`); the +separate API selection is in `api-note.md` (SHA `f9daa540`). The earlier +[type-only metadata correction](native_resource_kind_tags.md) remains a +separate prerequisite. + +This implementation establishes that identity/type boundary without choosing +production kernel names or public provider signatures. It uses compiler Rust +catalog input, not new Jett syntax, source configuration, environment variables +or command-line options. Ordinary production resolution supplies an empty +catalog. The specimen declarations used below belong only to the compiler test +harness and are not installed in the stdlib. + +## Exact declaration and type facts + +A catalog entry identifies the actual parsed resource name span and a closed +signature recipe. Resolution requires the existing successful Resource `DefId`, +its namespace, both a reserved stdlib FileId and independent loader +`SourceOrigin::Stdlib` evidence. It creates an associated private Function +`DefId` immediately after that resource through ordinary namespaced insertion. +Duplicate declarations, private access and top-to-bottom order still use normal +resolution rules. A private-looking name, a forged numeric FileId, an alias or +a same-named ordinary type cannot substitute for the resource declaration. + +The compiler-created declaration is a real definition-table entry, not an AST +function with a fake body. Every actual source function body remains checked. +Synthetic diagnostic attribution may share the resource name span, but the +checker excludes the synthetic Function IDs from its source-span fallback map; +the resource's own declaration binding remains intact. + +The checker derives each recipe inside its own type interner and binds the +resulting complete `Type::Function` to the exact Function `DefId`. Checked facts +retain that definition, the associated resource definition and type, the closed +hook kind, and the function type. Validation checks every parameter, complete +view/owned mode vector and result type, including unused entries. It does not +use the trusted function-signature tuple that omits view modes, a display name, +externally supplied TypeIds or the name-based intrinsic classifier. + +The three closed recipes exercise Construct, BorrowOperation and Close facts: + +| Test-only recipe | Parameters in order | Exact return | +| --- | --- | --- | +| NetworkFactory | `view Network`, owned `int64` script label | `result[R, string]` | +| NetworkBorrow | `view Network`, `view R` | `result[int64, string]` | +| Finalize | owned `R` | `nothing` | + +Here `R` is the exact associated nominal Resource type. Transparent aliases +retain the ordinary canonical type identity. These are test signatures, not +new socket APIs or universal resource signatures. The logical script label is +not a resource key, provider descriptor or source-visible carrier. + +## Explicit compiler entry and validation + +`resolve_with_resource_kernels` and `check_with_resource_kernels` are fallible +compiler Rust entry points. Invalid catalog/proof metadata produces a typed +error without publishing partial checked authority. Independent +`validate_resource_kernels` and `validate_resource_hooks` entry points check all +joins, origin evidence and complete shapes before another phase uses the facts. +The checked validator also refuses a resolver result containing Error +diagnostics; a later phase cannot salvage an order/privacy/duplicate failure. + +Legacy `check` and `check_with_options` retain default empty-catalog behavior. +They reject a nonempty privileged catalog with a bounded internal compiler +diagnostic, code 0, and no checked hook facts. Source argument, ownership, +visibility, order and body diagnostics retain their ordinary paths and codes. +No new source-policy diagnostic or body-validity exemption is introduced. + +Resource remains distinct from Capability. Semantic purity and capability +eligibility do not change: the two capability-bearing specimen hooks use the +existing rules, while the resource-only finalizer is syntactically pure. A +future runtime provider installation must be explicit; purity, debug emission +and ambient stdlib caller flags cannot grant provider authority. + +## Executed focused evidence + +The candidate is applied on the clean `62de6505` metadata parent in an +isolated worktree. The frozen source candidate is `c38adcec`; it remains a +historical artifact. The first checker compile attempt exposes a missing +`mut` on the `finish_check` helper's checker parameter. After that repair, the +source-body test exposes an incorrect expected E0300: ordinary checking emits +E0311 for the invalid variable initializer. Only that assertion is corrected. +Both initial failures remain separate evidence, rather than successful gates. + +The final focused run passes six resolver tests, seven checker tests and the +synthetic-span collision test. These parsed-source and malformed-metadata tests +cover exact IDs/modes, aliases, default absence of authority, stdlib origin +spoofing, private access, declaration order, source/catalog duplicates, ordinary +source body checking, corrupted unused joins and foreign TypeIds. All ten tested +Rust source hashes are stable across the successful run. Exact results and +per-gate logs are in: + +- `target/native-resource-hook-focused-after-diagnostic.json`. +- `target/native-resource-hook-resolver-focused-after-diagnostic.log`. +- `target/native-resource-hook-checker-focused-after-diagnostic.log`. +- `target/native-resource-hook-span-focused-after-diagnostic.log`. + +The broader wrapper passes all **1037 compiler library tests**: 63 codegen, +417 comptime, 139 HIR, 99 MIR, 54 resolver, 233 typechecker and 32 types units. +All **63 codegen object integration tests** pass (1.23 seconds), all **83 driver +tests** pass (77.95 seconds), all **598 frontend fixtures** pass (23.63 seconds), +and all **eight backend-lowering tests** pass (170.93 seconds), including the +182-file lowering inventory. The wrapper exits zero with all ten Rust source +hashes unchanged at `2026-10-03T11:18:33.3848153Z` in +`target/native-resource-hook-phases.json` and its five referenced logs. Rust +formatting and diff checks pass. Clean `616b51b9` subsequently passes its complete +workspace; supported-host acceptance for that head remains pending. The exact +predecessor record is in the acceptance audit above. This slice adds no +supplemental linked Resource case, object obligation or completed language row. + +## Required lifecycle and backend followup + +The [static ownership guards](native_resource_ownership_boundaries.md) now close +selected field/getter copies, known-view payload acquisition and root-value +observation gaps. They do not establish source execution or settle the remaining +nested printing and borrow provenance boundaries. + +The checked map is not a provider installation. The private reference +interpreter now has the carrier, ordered custody ledger and internal scripted +provider described in [the reference record](#reference-lifecycle-execution). +Execution retains the same successful checked source/declaration snapshot; +re-reading a same-named function and attaching earlier DefIds by name/span +remains invalid. Production catalogs and provider installation stay disabled. + +One owned value carries one cleanup obligation; a copied internal key or +ordinary Rust value clone cannot create another owner. The ordered acquisition +ledger must cover scopes, expression temporaries, partial arguments, owned +storage/return transfers and occupied aggregate arms. Cleanup must run in +reverse acquisition order on fallthrough, returns, handled/terminal failure and +loop exits, including fallible paths that bypass today's normal scope pop. +Explicit close uses the same finalizer and suppresses the later owner drop. +The existing runtime registry's context teardown is not proof of source-scope +cleanup. + +HIR must retain the checked hook/resource identity. Typed MIR transfer/drop +elaboration must prove exactly-once cleanup and no borrowed drop before native +Resource carriers or provider operations become admissible. Native refusal of +unsupported Resource values stays conservative. Actor/message transfer, +pending work, cancellation, late completions, capability provenance and real +provider/socket adapters remain required parts of the full goal. Neither +implicit Resource cloning nor runtime pinning is selected here. + + +## Successful checked-program handoff + +The later [immutable checked-program boundary](native_resource_checked_program.md) +owns its parse/resolve/check session and refuses parser, resolver and checker +errors before publication. Public hook validation now also refuses checker Error +diagnostics; diagnostic checking retains the separate identity-only helper so +ordinary source failures stay inspectable. This closes the successful-source +proof gap without installing a provider or changing the Close signature, source +purity rule or Resource value representation. Driver retention and reached-hook +runtime eligibility remain separate prerequisites. + + +## Reference lifecycle execution + +The reference implementation over committed caller head `a63f8910` retains an +Arc of the successful CheckedResourceProgram. Physical Resource carriers and +hook descriptors keep that program identity and exact nominal type/hook facts. +The non-cloneable holder ticket is the owner; cloning an internal carrier or +ordinary Value cannot duplicate custody. Caller packets select source-order +argument effects before formal permutation, so View access alone cannot retain +an owner passed bare. Ordinary checked calls relay evaluated ownership envelopes +through entry and return. Generic body selection remains exact; broader scoped +and reflected cursor/capture cases are not claimed complete. + +Executable registration rejoins the unique retained Item::Function and exact +DefInfo. Ordinary declarations have no name-span resolution entry; their actual +DefInfo span supplies the declaration join. Actual mutual bodies additionally +rejoin the authoritative resolution and the retained forward declaration. +Namespace/header checks stay exact, and the retained checked body executes even +if a host registration clone's body changes. A raw name or matching signature +cannot install a hook or replace executable Source authority. + +The ledger tracks source scopes, operation temporaries, partial arguments, +owned returns/storage and occupied result payloads. Views carry checked live +backing without ownership. Explicit close retires the owner once. Reverse-order +cleanup on normal/early return, handled/terminal failure and failed later actuals +is observed before context teardown. The later-named-argument witness preserves +source order `[token, marker, net]` and formal permutation `[1, 0, 2]`: exactly +one Borrow501 proves the first actual ran, and a second would expose erroneous +callee entry after the marker failed. + +Cleanup continues through older owners after a finalizer panic. Cleanup failure +overrides entry failure; successful cleanup preserves an ordinary entry error. +Ordinary source result.fail remains a protocol value. The terminal Source +fixture reaches the actual checked synthetic-Stdlib list kernel and pins +`list.__remove_at: index -1 out of bounds`. The ordinary closure fixture returns +Int64(7) to its Rust caller while the Resource/grant remain unreferenced by the +closure. Both controls assert cleanup/provider observations before teardown. + +Descriptor creation does not execute a provider. Reached Construct, Borrow and +Close hooks require ReferenceRuntime purpose and exact checked target/signature, +caller facts, nominal type, context and authority provenance. Close remains +semantically pure at the source signature boundary; required evaluation cannot +invoke it. Closed descriptors and absent values remain usable without a provider +where already checked. Property evaluation restores its prior purpose on ordinary +success, failure/shrinking and unsupported sample-pool returns. + +Actual root receipt `923450` passes all **36 Resource-focused reference tests** +(36 passed, zero failed, 418 filtered; 0.07 seconds) in +`target/native-resource-after-checked-call-relay-focused.log`. Seven declaration +identity/registration groups, real both-profile Source lifecycles, terminal and +mixed-failure cleanup, and required-worker/purpose controls are included. +Earlier API, parser and declaration-identity failures remain preserved in their +original logs; they are not passing receipts. Root receipt `14ee9c` also passes +all **454 comptime library tests and 87 driver library tests** (541 total) in +`target/native-resource-reference-libraries.log`. The complete frontend fixture +suite then passes **598/598** in +`target/native-resource-reference-fixture-suite.log`. These checks cover the same +unformatted Resource reference candidate, before later pipeline/assignment, +absence-shape and scoped-entry changes. Full-workspace and supported-host +acceptance of this Resource candidate remain pending. + +The required reference work still includes exact scoped/reflected/captured and +worker body contexts, empty Resource-containing aggregate validation, pipeline +input and argument custody, mutable binding replacement, repeated checked-entry +restoration, and occupied aggregate transport/iteration. The existing 36-test +receipt does not cover these gaps. Native providers and MIR cleanup remain +required after reference lifecycle parity. + +The typed absence follow-up repairs exact unoccupied list/map, nominal +struct/generic-struct, enum and machine layouts. It checks every occupied +ordinary field, element, key, variant and state against the checked schema; +absence never grants an owner or provider right. An enum variant constructor +is joined through its exact resolved owner, checked callable signature and +variant field tuple before any actual evaluation. Actuals are evaluated once +in lexical order and only then permuted into fields. Occupied Resource enum +and other aggregate custody transport remains required. + +The expanded focused replay passes **43/43** (`e78a71`), including five new +aggregate/required-worker groups and two both-profile pipeline/assignment +frontend-prerequisite groups. The latter two are source-checking evidence, +not pipeline or assignment runtime acceptance. Current broader receipt +`fa9a70` passes **461 comptime library tests, 87 driver library tests and +598 frontend fixtures** (1146 total) in +`target/native-resource-after-absence-enum-broad.log`. Rust formatting and all +22 Jett fixture format checks pass. The original prerequisite E0378 failure +and enum dispatch failure are retained in their logs, superseded by these +actual passing results. + +Ordinary refinement fields inside an otherwise Resource-containing absent +aggregate still need exact predicate validation; a nominal type tag alone is +not proof. Constrained aliases whose base contains Resource retain the existing +E0367 rejection. The stronger original-body/scoped/reusable-entry layer remains +an unaccepted intermediate until collected required-expression workers and +remaining contexts retain their positive controls. Pipeline/assignment source +validity does not implement custody transport, and production/native providers +and async/actor lifetimes remain required. + +The separate committed caller suite passes **480/480 native tests** at exact +`a63f891016fac7dc50fffdde33be8b0ce45c655f`; see +`target/native480-after-owner-generation/receipt.json`, ending +`2026-10-04T14:20:12.987780+00:00` with stable HEAD/tracked sources. That corpus +contains no Resource candidate and grants no native Resource/provider acceptance. +Its exact supported-host workflow +[37206485724](https://github.com/vycdev/jett/actions/runs/37206485724) passes both +Windows/Linux locked workspace builds and both installed relocation jobs, +ending `2026-10-04T14:51:19Z`. The installed jobs have no source checkout; +`target/native-platform-a63f8910.json` retains the authoritative four-job receipt. +That result validates the caller commit and does not transfer to this Resource +reference candidate. +The 36 reference tests add no native inventory case. The fixed inventory stays +207 fixtures/182 object obligations and the broad feature estimate stays about +85%, with the whole-language 100% objective unchanged. + +Production hook catalogs remain empty; no Resource declaration/provider is +installed in the stdlib. Scripted provider installation is compiler-test-only. +Whole scoped/reflected/indirect/worker breadth, other aggregate ownership paths, +real providers, task/actor cancellation and late completion remain required. +At that reference checkpoint, HIR hook identity/operation lowering, Resource-linear +MIR transfer/drop proofs and native carriers/providers were still missing. Nested ordinary printing, +hidden Secret, known absence and omitted rendering policy is unchanged. + +## Native custody core protocol + +The private native runtime core is implemented independently of reference AST +transport. Program/kind/owner-slot references keep one constructor-owned +registration identity. Non-cloneable owner and loan tokens rejoin the registry's +context, nominal kind, key generation and authority provenance. Transfers +advance holder generations without minting an owner from a copied carrier. +Chronological scope/operation/return frames retire loans before reverse owner +cleanup. A protected finalizer panic does not stop cleanup of later owners. + +Resource completion retains cleanup-first precedence in a separate channel; +ordinary native first-error behavior is unchanged. The controls cover failed +publication cleanup, active-loan move refusal, stale/foreign/wrong-generation +tokens, repeated entries, counter exhaustion, cleanup continuation and every +entry/cleanup outcome pair. They assert owner, loan, frame and registry counts +before context teardown. Dropping a copied physical carrier or token is not +source-scope cleanup. + +Root's actual focused receipt `7b0e8f` passes **13/13** protocol tests in +`target/native-resource-runtime-custody-core-first-focused.log`. After runtime +formatting, `9e9957` passes **157/157** complete runtime library tests in +`target/native-resource-runtime-custody-core-formatted-library.log`. +At the92421063 checkpoint, registration was test-only. Its later private +metadata implementation is recorded below. Native ABI, Resource CFG ownership/ +drop plans, linked source-deleted lifecycle execution and real providers +remain required. This protocol core adds no native inventory +case; the broad feature estimate remains **85%** and the whole-language objective +remains 100%. + +## Checked required, scoped, pipeline and assignment transport + +The expanded reference transport preserves the immutable checked program and +original function, generic/scoped body, namespace initializer, explicit comptime, +verify or property region. Entry selection and metadata installation are +transactional; normal return, failure and re-entry restore the previous body +cursor and complete concrete type/reflection facts. A cloned AST or another +program/context cannot replace the original source occurrence. + +Pipelines stage actuals in source order using original step identities before +formal permutation. Mutable assignment keeps its declaration slot after a move +or Close, evaluates RHS while the previous owner remains observable, then retires +and publishes owners exactly once. Reverse cleanup still completes after an +eligible finalizer panic. Generic intrinsic arguments use the exact selected +body's concrete type IDs and complete reflection records, preserving alias names; +no ambient metadata or unresolved source type name supplies that proof. + +Root receipt `61a351` passes **83/83** focused tests in +`target/native-resource-concrete-intrinsic-pipeline-syntax-fixed-focused.log`. +This includes all 12 pipeline and seven assignment groups, the preserved generic +Source27 oracle and four new both-profile metadata/corruption groups. The two +test helper API corrections and canonical `into` fixture spelling preserve all +expected values and refusal oracles. The complete formatted reference/driver/frontend gate also passes all **1,186 tests** (501 Comptime, 87 driver, 598 frontend; `4ba85c`) in `target/native-resource-transport-formatted-property-fixed-broad.log`. Rust formatting and all eight new Jett +fixture format checks pass (`658c63`). Earlier failed gates remain in target logs. + +The foundation `7157f564` supported-host run +[37217759157](https://github.com/vycdev/jett/actions/runs/37217759157) completed all +four Windows/Linux workspace, package and relocated installed-runtime jobs. +That receipt excludes these later transport changes and custody core `92421063`. +Ordinary refinement children, complete capture/reflection/worker contexts, +occupied aggregates and real providers remain required. This is reference +execution; it adds no native inventory case or native Resource admission. + +## Checked HIR/MIR Resource manifest handoff + +The checked lowering entry accepts the original immutable +`Arc`. Its private manifest retains every nominal +Resource declaration and trusted hook, including unused entries, and validates +exact declaration origin, type identity, closed recipe and complete signature. +Program-bound kind and hook references cannot be minted by raw lowering or a +matching name/signature. HIR descriptor and invocation nodes preserve the exact +original Source call facts, operands and lexical argument order; MIR retains the +same manifest through its visitors and remaps. + +This is a metadata handoff. Resource actuals that require unsupported eager +staging fail transactionally. Ordinary acquisitions, copy/move/drop plans, +native constants and emission retain explicit pending Resource refusals. +Descriptor retention does not issue runtime storage, a provider or ownership. + +The focused original Source gate passes all five groups (`852dfc`). After +formatting and the reviewed diagnostic successor, the complete phase gate passes +**522/522**: 191 HIR, 198 MIR, 70 codegen and 63 object-emission tests +(`961b04`, `target/native-resource-manifest-metadata-fixed-phase.log`). Foreign +nested type IDs are now rejected by recursive manifest validation before the +ordinary native type gate; the controls pin that exact diagnostic and separately +retain lower-level malformed-type refusal coverage. All other Source fixtures, +corruption inputs and expected values are unchanged. + +Resource CFG ownership/drop plans, native registration/ABI, real providers and +linked source-deleted Resource execution remain required. These checks add no +native execution case and do not increase the approximate 85% coverage estimate. + +## Bounded native registration and nominal issuance + +The private runtime registration path now owns and validates the complete +`JTRSC001` layout blob. A 56-byte header, explicit little-endian tags, bounded +section sizes and dense ordinals reject malformed/truncated/trailing data. +Every unused hook, canonical signature/shape, occupied path, frame and operation +reference is checked before the sole production-capable issuer creates fresh +nonzero nominal kinds and the complete slot-kind projection. One context-bound +installation holder rejects repeated installation, foreign context, shutdown +and nonempty registry state. Metadata agreement cannot create Source authority, +a provider grant, a resident loan or ownership from carrier bits. + +Scope, Operation and Return signatures all describe the containing Source +function. Signature agreement is checked across that function's frames even at +different instruction sites; hook and callee signatures remain separate. The +new refusal control also corrupts an unused Return frame. Physical acquisition +preflight checks slot bounds, generation/creation exhaustion and storage +capacity before a later provider runs. The core preparation and future opaque +token-table preflights remain separate mandatory checks. + +The exact six-file/eight-hunk packet and eleven inputs passed Root guarded +application (`58a435`) after independent byte/API review. The initial nine +registration groups pass (`f753aa`). After the selected frame-signature +strengthening and formatting, the complete runtime passes **167/167** +(`56e7ac`, `target/native-resource-registration-formatted-library.log`), including +ten registration and the existing thirteen custody protocol controls. + +The context/ABI adapter, typed grants, provider installation, compiler ownership +and cleanup CFG, occupied sums and linked source-deleted execution are still +required. These are runtime metadata/protocol tests; they add no native Source +execution case and keep the approximate coverage estimate at 85%. Full aggregate, +capture/reflection/refinement, real-provider and async/actor/cancellation breadth +remain part of the 100% objective. + +## Source-authenticated ownership handoff + +The original checked HIR/type archive and constructor-emitted MIR witness now +support a separate `ResourceOwnershipPlan`. It records exact scopes, operation +and provisional-return frames, owner holders, incoming views, bounded loans, +transfers, occupied sum extraction and cleanup obligations. Original/current +Source call associations preserve bare owned Resource-to-view custody without +creating ordinary owning operand claims. Copied queries, changed call sites, +operands, Source certificates, evaluation order and nominal type meanings are +refused. Unnamed Resource-bearing intermediate expressions retain their witness. +Canonical block/local compaction retains exact current associations. + +All **12 focused ownership groups** pass (`155203`, +`target/native-resource-ownership-associated-focused.log`). The Source-derived +missing nominal-table regression passes (`fe7b0a`), returning the precise error +without a panic. The original nominal fixture retains both field-corruption +oracles after copying the complete original nominal tables and selecting its +actual Source Holder type. The malformed-type phase controls exposed a validation +ordering bug; valid type/invocation metadata is now required before custody +queries, preserving the exact manifest diagnostic and independent scalar refusals. + +The complete formatted affected gate passes **535/535** (192 HIR, 210 MIR, +70 codegen, 63 object-emission), plus 32 type-library tests and one MIR doctest +(`3c6a8a`, `target/native-resource-ownership-valid-metadata-phase.log`). Earlier +failed runs remain diagnostic evidence. This is initial compiler ownership +analysis: connected native frames, provider/ABI operations, operational cleanup, +handled actual normalization, complete aggregates/reflection and linked binaries +executed after source deletion remain required. The separate 480-case caller +receipt, fixed 207/182 inventory and approximate 85% estimate are unchanged. + +The broader driver/frontend gate also passes **693/693**: 87 driver library, +8 backend-lowering tests (including all 182 run-pass lowering obligations) and +598 frontend fixtures (`450ea4`, +`target/native-resource-ownership-driver-frontend.log`). The previous registration +commit's Windows CI job failed its CLI native-setup capture test with a main-thread +stack overflow; that separate platform regression is under investigation and is +not represented as current ownership or whole-workspace acceptance. + +## Native context adapter and custody operations + +The private runtime adapter connects the existing authenticated stationary +context lease, bounded layout issuer, registry and custody core. Exact current +attempt, frame, owner, loan, prepared hook and Resource sum records retain the +non-cloneable core tokens. Ordinary and Resource lookup IDs share one checked +monotonic allocator. Installation publishes a scripted provider and paired +ordinary Network grant transactionally after preflight; that provider is test +only. Production state starts absent with disabled providers and no new exported +Resource leaf. + +Real core operations now cover root entry, Operation frames, direct/descriptor +hooks, acquisition, moves, bounded borrowing, close/drop, occupied sums, staged +replacement and protected reverse cleanup. Completion preserves the observed +body status verbatim and separately selects cleanup, host panic and current +ordinary/Resource body failure. Ordinary FailureTake is neither invoked nor +reset by the adapter. Refused transfers retain the old owner; a cleanup fault +cannot publish success. Borrow domain Fail stays ordinary Result data. + +Initial compilation found three private API/field-name integration errors. The +first runnable focused check passed 13/14 and exposed string-only cleanup being +used for an ordinary borrow Result. The bridge now uses the existing recursive +native destructor, and an additional real-provider BorrowFail control checks +sum/string retirement, duplicate-drop refusal, clean completion and exact events. +All **15/15 focused groups** pass (`fd8577`, +`target/native-resource-state-adapter-companion-fixed-focused.log`). After +formatting, the full runtime package passes **182 unit + 13 integration tests** +(`96d995`, `target/native-resource-state-adapter-formatted-runtime.log`). The +earlier compile/failure logs remain diagnostic evidence. + +These are runtime host-protocol controls, not linked Source execution. Exact +Source child Scope/Return activation, resident incoming views, connected C leaves +and emission, operational compiler cleanup, the matched archive and linked +source-deleted binaries in both profiles remain required. The adapter-head Windows +CLI setup-capture stack overflow is recorded separately; the later helper +repair below supersedes that pending local regression. Broader +aggregate/reflection/refinement, provider and concurrency coverage remains part +of the full goal. Native caller 480/480, the fixed 207/182 inventory and the +approximate **85%** feature estimate retain their separate scopes. + +## Windows lowering stack repair + +The unchanged CLI native-setup capture test reproduced a main-thread stack +overflow at Resource ownership and adapter heads. The adapter-head Windows CI +also completed the native conformance suite with **480/480 passing** before the +workflow failed its separate CLI capture target (`b32d80`, workflow +`37244085637`). This is current adapter-head supported native execution evidence; +it still includes no native Resource lifecycle case. + +The repair extracts the existing heavy HIR statement/equality/inline-function +branches and MIR expression/per-actual branches into private helpers. Original +Source facts, Resource call capture, preflight, lexical order, shared call-view +scope, rollback, spans, diagnostics and context restoration remain at the same +operations. Source fixtures, assertions, snapshots, launcher order and stack +sizes are unchanged. Exact guarded application reconstructs both files and all +39 hunks (`3a8820`); the independent reconstruction record agrees. + +The original regression now passes **1/1** (`285533`), and the formatted full +CLI capture target passes **9/9** (`508636`). The formatted phase gate passes +**535/535** HIR/MIR/codegen/object checks plus 32 type tests and the MIR doctest +(`f8fe67`). The broader gate passes **693/693**: 87 driver library, 8 lowering +checks including all 182 run-pass fixtures, and 598 frontend fixtures (`647ede`, +`target/native-windows-stack-decomposition-driver-fixtures.log`). An initial +command used the nonexistent test target frontend; no driver test ran in that +attempt. The corrected fixture_suite command supplies the actual receipt. + +Actual Debug assembly measurement (`779571`, +`target/native-windows-stack-decomposition-frame-measurements.json`) records +MIR lower_value decreasing from 169,448 to 15,272 bytes and the ordered-call +loop from 73,736 to 4,504 bytes. Its separate per-actual helper uses 70,104 bytes. +HIR lower_expression decreases from 117,624 to 106,200 bytes and lower_statement +from 108,552 to 100,216 bytes. These are individual function-frame measurements, +not a whole-program stack limit or proof of every recursion shape. + +The local platform regression is repaired. Whole-head supported-host CI and +connected Resource Source ABI/emission/cleanup and linked lifecycle execution +remain required. The fixed 207/182 inventory and approximate 85% coverage +estimate remain unchanged; runtime/metadata checks do not supply the missing +Resource native execution. + +## Resource execution family and emitted layout + +The compiler now captures the Resource execution family only after validating +the complete original checked HIR archive. Resource-free direct Source caller +predecessors receive sealed constructor witnesses and fresh Scope/invocation +plans. Edited current graphs cannot remove or recapture those witnesses. +Exact formal projections retain actual and parameter types, access, spelling, +effects and lexical evaluation order. Absent Optional and failed Result shells +receive exact holders; an ordinary failure companion is extracted once. + +One fresh whole-program plan now projects deterministic bounded v2 layout bytes +and the exact driver-selected entry tuple. Every manifest hook remains checked, +including unused descriptors. The immutable object accessor emits metadata only. +Return transfer executes under the callee root Scope while retaining its private +Return destination; publication uses the same completed Scope and resolves the +active caller destination. These are distinct from the original recorded Return +storage and site facts. + +The original producer packet reconstructs 11 paths and 33 hunks (`4319e3`). +Actual compilation exposed a missing FunctionId ordering implementation and a +private-module import; the fixes use stable dense IDs and supported readonly MIR +reexports. New fixtures were corrected to the existing owned-capability entry +rule after E0503. No language or checker rule was relaxed. Two narrow writer +successors preserve Return storage while projecting its active Scope (`25bf9c`, +`f36175`). Independent source/API review found no additional concrete blocker. + +All eight focused groups pass in both compiler profiles (`1cba25`, `242535`). +The formatted compiler/object gate passes **543/543** plus the MIR doctest +(`fc3f02`). After the final Return projection, all **74 codegen and 63 object +checks** pass again (`45ddfe`). The driver gate passes **693/693**: 87 library, +8 lowering and 598 frontend fixture checks (`b9de51`). These are Source, compiler +and metadata checks. Dedicated body/ABI emission, ordinary companion cleanup, +matched runtime archive and Source-deleted linked lifecycle runs remain required. + +## Native Source runtime and custody leaves + +The runtime validates complete v2 Source invocation rows and implements the +32 exact Resource custody leaves. Formal permutation, lexical evaluation order, +owned parameter slots, resident parent loans, callee Scope/Return roles and the +original caller result destination are checked before execution. Version 1 +remains historical metadata and cannot activate the new Source transitions. + +Source entry preflights all owner generations, borrows, destination holders and +capacity before transferring any owner. Resident views keep one parent loan. +Return publication joins the completed callee Scope, current provisional Return +activation and exact caller destination. Scope/Return cleanup failure cancels +publication and outranks the separately retained observed body status. None and +Fail shells move whole; ordinary Fail companions use ordinary recursive drop. +C output alignment, overflow and known overlaps are refused before effects. +Production state and providers remain disabled. + +The guarded runtime packet applied 16 paths and 61 hunks (`4788d3`). Its first +nine Source controls passed (`ea7d1f`). The formatted focused gate passed three +v2 registration, two batch-transfer and all fifteen previous adapter controls +(`29d4b6`). The first full suite exposed an obsolete test that expected version 2 +to be an invalid header. The correction tests unsupported versions 0, the next +version and u32::MAX; no decoder refusal was weakened. The repaired formatted +runtime suite passes **196 unit and 13 integration tests** (`fd04d3`). + +These checks execute real private runtime transitions and C-boundary controls, +not generated Jett Source bodies. Dedicated native body emission, ordinary +companion cleanup, a matched archive and Source-deleted linked lifecycle runs +remain required. Real providers, occupied aggregates, captured/reflected +contexts and concurrency retain their full-goal obligations. The approximate +85% estimate and fixed inventory are unchanged. + +Independent byte review found eight unrelated Unicode test literals changed by +encoding conversion in the authored packet; Root restored the original HEAD +bytes exactly and reran the runtime suite (`e45fe3`). Independent semantic review +also found that ordinary nested sums could carry the wrong physical payload +ownership bit. Three causal controls failed before the repair (`bf61fa`), then +passed with exact immediate Optional/Result ownership validation (`5af1b8`), +preserving malformed carriers and unrelated live strings on refusal. + +A selected pre-Rust root-outcome seam preserves the original body status after +exact root Scope retirement. Its readonly C lookup joins the same installed +entry tuple and current Runtime attempt, refusing active, stale, child, foreign +or worker-purpose roots without latching a body fault. Output refusal precedes +state effects. Body zero stays zero when cleanup panics; host completion retains +its independent cleanup-first channel. The successor applied five paths and +eleven hunks (`f54e7e`). One authored cleanup control incorrectly left its owner +in an Operation holder; the actual failure (`5763d2`) led to moving it through +real Source Return publication into the root holder. All five controls then +passed (`4f200a`). The final formatted runtime suite passes **204 unit and 13 +integration tests** (`716d31`). Source-native execution remains pending. + +The final reference Resource regression filter also passes **67/67** (`fd2bdb`). +Independent current source/API reviews confirm the malformed ordinary-sum fix +and exact readonly root-outcome boundary. Their finite scope does not establish +multi-owned Source actual or recursive activation coverage or native execution. + +## Fresh-plan ordinary companion analysis + +ResourceCompanionPlan borrows the fresh custody plan, exact immutable current +function and types. It retains ordinary storage/type/child, initialization, +liveness, alias, loan and move analysis and complete original Source acquisition +validation. Resource carriers do not enter ordinary owned storage. Readonly +ordinary storage and CallerAcquisitions retain the same current program lifetime; +public ordinary Copy/Move/Caller gates keep their Resource-pending refusals. +Exact expression occurrence roles are captured by the fresh CFG validator; +identical cloned expressions do not select operations. + +The guarded prerequisite applied seven paths and 24 hunks (`7bca1d`). Actual +compilation exposed use of the public caller admission gate inside the private +companion path (`12afac`). The repair uses the same complete internal Source +validator only in the already authenticated companion context, retaining the +ordinary public gate. The constructor also stores its readonly Source acquisition +result for dedicated body emission. All three both-profile Source groups pass +(`bbbf1d`). The formatted gate passes **546 checks**: 74 codegen, 63 object, +192 HIR and 217 MIR tests, plus the MIR doctest (`f48b08`). Independent current +source/API review confirms the finite ordinary-analysis boundary. Dedicated +body emission and linked native Resource lifecycle execution remain pending; +whole-language coverage remains the about-85% estimate. + +## Direct body and matched archive checkpoint + +Direct original-Source body emission now compiles the selected Scope ABI, +lexical Source/Hook actual staging, resident-view transport, typed sums, +ordinary failure companions and provisional Return publication. Five focused +groups check both compiler profiles. The full formatted compiler gate passes +551 checks plus the MIR doctest after restricting Resource cleanup storage and +blocks to the selected family. Exact indirect descriptors, eagerly aborted +Resource acquisition CFGs and mutable Replace rows remain explicit successor +work; they are part of the full goal. + +The actual double-cfg debug and release runtime archives compile on Windows +MSVC with coherent `+crt-static` dependency flags. Complete symbol inspection +finds exactly one main, eight private observation/host exports and all 32 +production Resource leaves in each. A separate production archive has no +private main or test exports; the ordinary runtime suite passes 217 checks. +The measured vectors retain their original order and duplication, including +`/defaultlib:libcmt`. These are local Rust 1.93.1 checks; supported-host CI uses +its separately pinned toolchain and remains a distinct gate. + +Four encoder controls run against exact current record declarations and the +real report module; six driver decoder controls pass. They preserve unknown +versus empty observations and original body/cleanup channels. Actual HOST C ABI +negative controls pass with both measured profiles; these handwritten controls +remain separate from Source conformance. The real reference and native gates +each pass five shared original Source scenarios under both profiles, covering +connected lifecycle, factory failure, partial actual failure, implicit drop and +finalizer panic. Native execution runs after deleting Project and synthetic +Stdlib Source directories and independently verifies outcomes, exact events, +script exhaustion and zero Resource/ordinary obligations before one destroy. +This accepts the first local compiler-test slice. It does not remove provider, +aggregate, reflection, refinement, task, actor, concurrency or supported-host +distribution obligations. The broader driver gate also passes 693 checks. + +## Replacement refusal before finalization + +The native plain-owner replacement protocol now validates both owners and reserves generation/acquisition capacity before old cleanup. New causal controls cover a loan on either owner, a consumed stale RHS handle and deterministic RHS generation exhaustion; each refusal preserves live owners and finalizer events, followed by a valid replacement or transfer. The unchanged success/finalizer-panic baseline also passes. The formatted focused gate passes four tests, and the full runtime gate passes 220 checks (207 unit and 13 integration). These are runtime/HOST controls. Source Replace producer/writer/emitter and aggregate replacement remain required; the 85% estimate is unchanged. Earlier matched archives and ten Source-native executions apply to cc06aa1f, before this runtime change. Fresh private archives and Source acceptance must be measured separately. + +## Source mutable replacement checkpoint + +The original Source replacement gate failed at the missing evaluated-RHS layout field before this compiler change. MIR now seals that field, the writer emits exact existing tag 12, and native assignment handles live replacement, exact self-reseat and vacant-after-close transfer separately. Seven new both-profile compiler groups cover endpoints, Source/header mutation refusals and object routes. Actual compilation required one missing test-only ExpressionKind import; canonical formatting changed no Source behavior. The final compiler/object/HIR/MIR gate passes 558 checks plus the MIR doctest, and 606 frontend/lowering fixture checks pass. All eleven shared original Source cases now pass both real reference and Source-deleted native execution under debug and release (22 executions each). Six added cases cover live replacement/use, self-reseat, close/rebind, handled RHS failure borrowing old, ordinary RHS failure and old-finalizer panic cleaning both owners. + +The fresh runtime archives have measured receipt `54a38248`; both HOST C ABI profiles pass. The complete driver-library run passes 86/87, with one pre-spawn temporary capture PermissionDenied; that exact property replay passes in isolation. This is recorded separately from Source acceptance, and no clean complete driver-library or current full-native regression pass is inferred. Whole-language coverage remains about 85%; indirect/handled/aggregate/worker/provider/concurrency and supported-host obligations remain required. + +## Constructor-owned handled Source calls (2026-10-05) + +MIR now preserves handled direct calls through typed Begin/Stage/Invoke/End +regions with private original/current witnesses. Fresh flow joins the exact +operation stack, arrived lexical prefix, loans and owners. A handler Return ends +its acquired prefix before lowering its original operand; clean abandonment +creates no body error and performs no target invocation. Prepared formals and +loan parameters are available without scanning a future reached Invoke. + +Native frame storage retains the dynamic operation and prepared call separately. +Staging evaluates once and registers exact Source actuals; invocation uses arrived +endpoints and checks Source status before publishing its output. Retirement uses +the fresh prefix plan. Local sum storage can belong to an ancestor Scope while +its instructions execute under the active Operation; a private validated current +site projection supplies that execution frame. Wire-v2 and runtime checks stay +unchanged. + +All eighteen shared Sources pass both real reference and Source-deleted native +profiles, with exact outcomes/events and zero obligations before destroy (36 +executions each). The formatted affected gate passes 573 compiler/object checks +and the MIR doctest, plus 606 frontend/lowering fixture checks, all four fixed +inventory gates (182 host objects), and the complete 92-test driver library. +The final formatted Source replay also passes all 36 reference and 36 native +executions. Broader Resource transport and the final full-goal release gates +remain required. + +## Callable boundaries and failed native joins (2026-10-06) + +The shared Source lifecycle gate now has 22 scenarios, all passing reference and +source-deleted native execution in debug/release (44 each). Three new programs +exercise reversed temporary-owner ordering, an independent Return helper and a +continuing default. A fourth scenario reuses the helper program with real cleanup +panic and proves the helper is not evaluated while both owners are retired. + +Reference Return cancels only newer operations within the current callable's +floor. Ordinary closure/named fallback bodies have no Resource publication +destination. Exact one-shot cancellation receipts cannot authorize normal +success or output adoption. Native SourceStatus validates a terminal completion +record tied to the caller operation, immutable Source row and physically retired +callee Scope/Return. Nonzero original body 71 stays 71; body 0 plus actual cleanup +panic remains original 0 internally and carries cleanup 255 to its caller. + +The full affected suites pass 508 reference-library, 92 driver-library and 222 +runtime checks. Fresh measured debug/release archives and all 44 native runs pass +with unchanged ABI 1, wire 2, one main, eight private exports and 32 Resource +leaves per archive. Exact parent a30ca97e workspace/default-native and all four +supported-host distribution jobs pass; successor release gates remain separate. +These results advance lifecycle coverage while the broader 85% estimate and +full language/provider/concurrency objective remain unchanged. diff --git a/docs/completed/native_codegen/native_resource_kind_tags.md b/docs/completed/native_codegen/native_resource_kind_tags.md new file mode 100644 index 00000000..f089705b --- /dev/null +++ b/docs/completed/native_codegen/native_resource_kind_tags.md @@ -0,0 +1,66 @@ +# Opaque resource type-only reflection tags + +Status: the affected compiler/frontend gates and subsequent clean `616b51b9` +full workspace pass. That head's supported-host run remains in progress; local +acceptance and predecessor platform evidence are centralized in the +[acceptance audit](native_acceptance_audit.md#resource-static-ownership-boundaries). +The broad coverage estimate remains about 85%. + +## Selected contract and correction + +The [opaque resource contract](../opaque_runtime_resource_contract.md) +already selects a nominal type name plus `TypeKind.resource_type`, no primitive +tag and empty field, variant and machine-state metadata. Type aliases retain +their existing `alias_type` tag. The pre-Rust correction was captured in +`target/native-resource-kind-tag-contract.md`. + +The checker already selected `resource_type` for a resource type, but the builtin +enum schema omitted that variant, so source comparisons failed with E0319. +The shared reflection mapper returned `unknown_type` for the same resource. +The correction appends the missing variant after all existing enum members, +preserving their indices, and maps the shared `resource` metadata kind to it. + +These two table entries do not admit resource values, source constructors, +cloning, reflected construction, serialization, runtime providers or native +resource carriers. Live ownership and finalization remain separate work. + +## Executed regressions + +The initial focused gate reproduces all three failures in +`target/native-resource-kind-before.log`: source/native E0319 and the reference +`unknown_type` mismatch. The final source-derived regressions check: + +- Stdlib resource comparisons through both `type.kind_tag` and `type.info`, + with ordinary alias-tag behavior preserved. +- Checked reference metadata returning the resource tag, absent primitive tag + and empty field, variant and machine-state lists. +- A real project caller reaching three synthetic stdlib functions through + normal checking, HIR/MIR lowering and nonempty native object emission, with + all three function symbols retained and no resource value created. + +The native fixture's initial private-only form emitted no retained functions; +its attempted explicit-comptime form was also inappropriate for the raw-HIR +test helper. Those failures remain in `target/native-resource-kind-after.log` +and `target/native-resource-kind-object-after.log`. The final reachable +project/stdlib fixture passes in `target/native-resource-kind-object-final.log`. +No production reachability or comptime workaround was added. + +The broader wrapper passes **800 compiler tests**: 63 codegen library units, +63 codegen object integration tests, 417 comptime units, 225 typecheck units +and 32 types units. All **598 frontend fixtures** also pass (22.31 seconds). +Rust formatting passes. All four tested Rust source hashes remain unchanged; +the wrapper exits zero at `2026-10-03T10:44:38.0454370Z` in +`target/native-resource-kind-phases.json`, with exact logs +`target/native-resource-kind-libraries.log` and +`target/native-resource-kind-frontend.log`. + +## Remaining native resource work + +Type-only reflection and [checked hook identities](native_resource_hook_identity.md) +are implemented prerequisites. The selected static copy/acquisition guards are +recorded in the [ownership note](native_resource_ownership_boundaries.md). Real +owned carriers, provider authority, other ownership paths, ordered cleanup, +explicit close, error propagation, actor transfer and task cancellation still +need checked implementations and reference/native execution evidence. This +correction changes neither the 207-fixture inventory, its 182 object obligations +nor the 464-case supplemental native corpus. diff --git a/docs/completed/native_codegen/native_resource_nested_sums.md b/docs/completed/native_codegen/native_resource_nested_sums.md new file mode 100644 index 00000000..d3b7a67c --- /dev/null +++ b/docs/completed/native_codegen/native_resource_nested_sums.md @@ -0,0 +1,111 @@ +# Nested borrowed Resource sums + +The native-codegen goal remains active, at an approximate 86% feature +checkpoint after the native execution gate. This successor implements established lexical view lifetimes; it +must not infer ownership authority from a matching expression, span or type. +No new source spelling or runtime Resource ABI is proposed. + +## Genuine initial baseline + +The accepted predecessor is d1ffd08c433023f65aa18f227b0d344e559f728c: +73 shared scenarios, with 146 real-reference and 146 Source-deleted MSVC native +executions in debug and release. The original sources and complete production +inputs were frozen before the next probe. + +Source57 declares an explicitly viewed handled Optional Resource payload inside +an `if enabled` body, then returns the borrowed observation. With unchanged +production code, all 74 shared scenarios pass 148 real-reference executions. +Native lowering rejects the new original source in MIR, at File0 14..387: +`pending ResourceOwnershipPlan: nested Resource declarations need exact constructor Scope records`. +That genuine rejection establishes the declaration-inventory gap; it is not new +native acceptance. The terminating branch alone cannot establish joining-branch +or loop cleanup. + +## Planned original Source packet + +Sources58 through95 exercise written/bare actuals, Optional Some/None and Result +Ok/Fail, and aliases inside joining `if`, scoped type bodies and two-iteration +`while` bodies. Additional controls retain an owned backing sum in the same +function and unwrap/close it after the lexical boundary, distinguish same-named +aliases backed by separate locals in sibling blocks, and handle an inner borrow +failure without changing the outer payload handler. Owned loop controls also +exercise Break and Continue before using the original backing afterward. + +Every occupied source has a separate construction-failure script against the +same bytes. Later-construction failures preserve exact prior finalization order. +The frozen packet contains 138 scenarios / 276 executions per backend. All +276 genuine reference executions pass with unchanged production code. The +expanded native baseline again refuses Source57 at the same MIR span and +constructor-scope diagnostic. All 2,227 source/config inputs revalidate after +both actual runs, and the accepted 73 Case rows remain exact. Source authoring +or reference success does not increase accepted native coverage. + +## Required private proof + +Use one exhaustive original-body declaration inventory over lexical blocks and +handler failure bodies, without entering a separate inline callable body. Each +borrowed alias key retains the original function/archive, declaration LocalId, +immutable local headers, complete initializer, exact backing local and lexical +path. Match the complete expected key set bijectively; an equal row count or an +expression/span lookup is insufficient. Canonical remaps may update current +headers and sites only after validating the independent original/current body +seal. They cannot rewrite the original occurrence or manufacture authority. + +Only exact authenticated borrowed alias declarations may cross the current +nested-declaration refusal. Nested owning declarations and unrelated view locals +retain their own conservative admission rules. Child payload loans end before +parent shell loans, and generated alias/source/output state is retired at the +actual lexical exit before a branch join or loop backedge. Runtime reissue at +the same static row must receive fresh dynamic loan handles. + +Authenticate Fallthrough, Return, Break and Continue independently. A Return +operand that observes its alias must be evaluated before that alias retires; +abandoned argument-prefix cleanup retains its existing separate ordering. +For/Match/reflected bodies and declarations after a terminal Return remain +full-goal obligations unless actual original Source and constructor exit proofs +cover them. A recursive visitor alone cannot claim execution support or erase an +unemitted occurrence as permission. + +## Acceptance + +Freeze the full Source-only packet and production hashes, pass every real +reference execution, then implement the constructor/MIR/codegen handoff. Require +negative controls for sibling/backing swaps, duplicate/missing keys, altered +headers/initializers/guards/exit membership, foreign archives, copied or +independently resealed/pruned bodies, and disconnected extraction. Run all shared +sources in both native profiles with exact scripts, outcomes and event ordering, +Source absent at launch, and zero ordinary/Resource obligations before teardown. + +The existing runtime can end and reissue a loan at the same static operation; +logical lexical retirement alone requires no ABI/schema change. Runtime changes +would instead require fresh independently measured archives and a new pinned +receipt. Inactive loan history grows across iterations even when live obligations +are zero; this existing storage behavior is separate from lexical correctness. +GNU private execution and current coherent workspace/package/installed gates +remain independent acceptance requirements. + + +## Verified native execution + +The frozen candidate passes all 138 shared scenarios in Debug and Release: +276 Source-deleted MSVC native executions in 968.37 seconds, with exact scripts, +channels, messages and finalization events, and zero ordinary/Resource obligations +before teardown. All 2,234 compiler/Source/config/gate inputs revalidate after the +actual run; the original packet and runtime bytes remain unchanged. The initial +276 reference executions remain the independent expected-outcome baseline. + +All 261 MIR tests pass. New controls cover original declaration identity, sibling +backings, guards, copied/missing/altered exits and exact owning-backing reuse after +retirement. Two earlier disconnected-custody mutations remain intact and now fail +at the stronger graph seal before the final CFG scan. Six genuine public object +baselines and eighteen copied/retargeted/unwitnessed graph controls pass across +both profiles; no forged graph emits an object. All 103 codegen library, 63 object, +521 reference-evaluator and 93 driver-library tests pass. Workspace formatting +and the frozen Source packet's Jett formatting also pass. + +This closes the tested If/scoped/While/Break/Continue family. For/Match/reflection, +unemitted declarations, ordinary data views and alternate Default lifetimes stay +open. GNU private execution uses the separate measured gate. The four supported +host jobs passed at predecessor d1ffd08c; updated coherent-revision workspace and +platform acceptance is still required. The approximate 86% feature estimate is a +planning measure, not measured code coverage or whole-language parity. diff --git a/docs/completed/native_codegen/native_resource_occupied_structs.md b/docs/completed/native_codegen/native_resource_occupied_structs.md new file mode 100644 index 00000000..229bb2ce --- /dev/null +++ b/docs/completed/native_codegen/native_resource_occupied_structs.md @@ -0,0 +1,97 @@ +# Occupied Resource struct constructors + +Checked named construction preserves written field evaluation order and the +original nominal schema. Begin/Stage/Commit/End carry a private constructor +witness through MIR and codegen. Original/current body and operation checks +reject forged public nodes before pruning. Slot and carrier migration keep one +cleanup obligation per owned Resource. Normal and abandoned-prefix cleanup +retire owners before finalizers, in reverse acquisition order. + +Sources33..35 cover ordinary scope drop, reversed named argument order, and +abandoned construction after a Resource has staged. Sources30..32 preserve the +historical invalid view-capability entry and its E0503 refusal. The shared support +Sources and original Source17 remain unchanged. + +## Resource predecessor verification + +At Resource commit `c141de22`, the integration includes the published actor +identity change. Local +MSVC static-CRT checks on 2026-10-12 pass all 308 runtime, 577 interpreter, 317 +MIR, and 120 code generator unit tests. The three-Source object gate emits all +six objects with both codegen optimization settings. Focused tests validate +constructor provenance, lexical staging, copied-node and forged-header refusal, +exact owner migration, stale generations, atomic late refusal, and cleanup that +continues after finalizer panic. + +Fresh linked acceptance passes on 2026-10-12 against independently measured +private Debug/Release MSVC static-CRT archives with both cfgs and ABI1/wire3. +The occupied retirement gate passes 22 executions/22 entries; same-grant reentry +passes 22/44. The original twelve predecessor gates pass 604/728 against the +same archives. Additional pipeline/control-flow subset gates pass 106/106, +so all sixteen Resource gates pass 754 native executions/900 entry completions. Each +gate removes its project and synthetic stdlib Sources before execution and checks +exact script events, failure precedence, statuses, messages, process exits, eight +zero custody counts and empty ordinary storage before one teardown. + +The MSVC measurement helper passes all 60 Python controls. It measures actual +compiler/linker commands, ordered native libraries, complete COFF members and +exact runtime exports. Cargo reports a freshly compiled gate; its dep-info +contains the independently supplied receipt pin exactly once. Runtime receives +only the receipt path. Source, archive, compiled gate and dep-info guards pass +after execution. The local predecessor collector was repaired to recognize its +two actual summary formats, then validated the completed logs without rerunning +the native tests. Accounting distinguishes the original twelve-gate corpus from +the subset and occupied gates. All 2,314 workspace library tests pass. This +evidence does not include an independent second raw-report +decoder or retain thousands of per-case executable copies. + +## Combined revision and executable reuse + +Fresh verification on 2026-10-12 includes Actor, Resource and Owner commit +`7ff46cc7` plus the executable-reuse harness change. All sixteen private MSVC +gates pass again: 754 Source-deleted executions and 900 entry completions. +The newly measured Debug/Release ABI1/wire3 static-CRT runtimes include the +additive Owner validation leaf. Receipt SHA-256 is +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +Fresh gate compilation, source/archive/tool/binary/dep-info guards and the +runtime-environment pin refusal control pass. The combined implementation +passes all 2,331 workspace library tests; this is not full native conformance. + +Consecutive script cases with identical project bytes reuse one completed +program within the exact runtime profile. Every case still runs in a fresh +process and working directory, after Source removal and binary-hash validation, +with its own script, report and unchanged oracle. Compiler-session proofs and +runtime state are never cached. The 191-case gate retains 382 executions while +reducing emitted/linked programs from 382 to 246. Occupied retirement retains +22 executions while reducing builds from 22 to 6. Reentry gates are unchanged. + +An additional evidence-enabled occupied run passes all 22 cases. Independent +file checks verify each executable, object, original script, report, exit and +empty capture by size and SHA-256. Compact records retain report/script bytes +and six distinct executable hashes; temporary per-case executable copies were +pruned after verification. These checks validate report framing, not a second +full event decoder. Source absence is asserted by the passing guarded harness. +All wider language, provider and platform obligations below remain open. + +## Historical checkpoint + +The isolated v16 checkpoint recorded 22 executables/22 entries for occupied +scope retirement and 22/44 for same-grant reentry. Together with its twelve +predecessor gates, it recorded 648 executables/794 entries on private MSVC +archives. Those historical target receipts were discarded during disk cleanup; +they do not establish acceptance of the restored integration. + +## Remaining scope + +The current eleven script cases do not observe completed fields, stage owning +strings or nested occupied carriers, or exercise occupied constructor allocation +refusal. Pair factory failures happen before construction; abandoned-prefix +coverage uses a later ordinary marker handler return. These are remaining +native completion obligations, along with projection, extraction, replacement, +reflection, recursive/generic aggregates, collections, captured lifetimes, +broader Default compositions, and GNU/installed-distribution acceptance. + +Private scripted execution does not establish public Driver admission or a +normal-runtime Resource provider. Backend refusals remain incomplete coverage. +The fixed inventory is 207 fixtures/182 object obligations; overall native +coverage is not complete. diff --git a/docs/completed/native_codegen/native_resource_ordinary_entry.md b/docs/completed/native_codegen/native_resource_ordinary_entry.md new file mode 100644 index 00000000..86d0cd02 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_ordinary_entry.md @@ -0,0 +1,347 @@ +# Ordinary entry in a Resource object + +The ordinary-entry bridge has bounded Source17 object/native acceptance and a +current compiler proof checkpoint. It changes entry metadata and wrapper emission, +preserving Jett source semantics and ordinary function calling conventions. +The earlier v20 whole-workspace gate and all eleven v21 MSVC-v3 native +regression gates pass; exact-revision CI remains pending. + +## Historical failure and selected repair + +Root's v7 gate compiled the candidate and passed all fourteen MIR controls and +all forty-four runtime controls. The first thirteen original Source17 wrappers +emit actual objects before wrapper14 fails. The reported error is `callee has +no fresh execution-family signature`. Root's v7 terminal is `6b7069`, session +15179; its unchanged-3742-input receipt is +`d57fca527e65a7008a8fce54a1c0293cdc984a90bddd01b2c1485b2a4bedead6`. + +`emit_for_triple` selects the Resource emitter when the checked manifest is +nonempty. `EmittedResourceLayout::from_program` validates the selected current +FunctionId and its original Source header, populates execution-family rows, and +then calls `rows.function_signature(entry)` before checking +`plan.function(entry)`. Thus the reported error is the selected entry lookup. +`source_row` first checks `plan.function(target)` and would report a different +error for a missing Source callee. + +For wrapper14, `original_source(entry)` succeeded. This entry has an initially +captured proof-only witness. Its required values and original/current graph are +authenticated, but it does not need runtime Resource custody. +`has_execution_records` and `validate_resource_ownership` correctly keep it out +of `plan.functions()`. `absent_required_controls` likewise remains ordinary. + +## Decision + +Keep the existing execution-family classification. Introduce a separate private +entry Scope record for the exact driver-selected ordinary entry. The native +program wrapper owns this Scope and its completion. The Jett main and ordinary +helpers keep their ordinary context/environment ABI. + +Appending the ordinary main to `plan.functions()` is unsuitable: signature and +translation currently use presence in that collection to add a third Scope +parameter and select `ResourceEmission`. Falling back to the ordinary object +emitter is also unsuitable: it omits the installed private archive entry, +authenticated root tuple, and retirement contract. + +## Authenticated MIR API + +A readonly `ResourceEntryScopePlan` with private fields and no public +constructor. It exposes the exact selected `FunctionId`, complete identity and +header, one root `ResourceFrame`, and one `ResourceOperation` whose role is +`Complete`. This frame and completion describe wrapper execution. They do not +claim that a Source branch or Source Return executed a Resource operation. + +The entry-aware API is: + +```rust +pub fn validate_resource_ownership_for_entry<'p>( + program: &'p Program, + types: &'p TypeInterner, + entry: FunctionId, +) -> Result, Vec>; + +impl ResourceOwnershipPlan<'_> { + pub fn entry_scope(&self) -> Option<&ResourceEntryScopePlan>; +} +``` + +The existing validator and `function`/`functions` getters retain their meaning. +The entry-aware validator first runs complete current call and witness +validation. If the selected entry is already an execution-family function, it +uses the existing family plan without adding a bridge. Otherwise it mints only +the separate entry record under all of these conditions: + +- The selected ID denotes the exact current function in the dense table. +- The selected function has its own initially captured Source witness. Another + function's archive is not sufficient authority to reconstruct its MIR body. +- Its original/materialized archive, manifest, interner, identity, complete + current graph and local/parameter headers pass the existing private checks. +- It is not required-only and has no runtime execution-family classification. +- The exact original/current native root header is closed, capture-free, + Network-only (including zero parameters), and returns Nothing, matching the + currently supported Resource entry header. + +The record borrows the immutable validated Program through the plan lifetime. +It adds no owner slot, loan, carrier slot, callable proof, Source invocation, +descriptor authority, provisional return or execution-family member. The +original required-value proof remains responsible for authenticating the +materialized values and body through canonical preparation. + +At initial authenticated checked lowering, capture a proof-only witness for +every capture-free, Nothing-returning, Network-only function (including zero +parameters) in an authentic Resource Source archive. This uses identity and +header, not a `main` name. Require `source.manifest().is_some()` and preserve the +complete original/current archive join. An independent full-body graph seal is +mandatory for this proof-only class, including genuinely pure bodies with no +required-value row. Missing-witness validation derives eligibility from the +exact immutable execution-archive function ID and header. A public header edit +cannot remove the original function from this mandatory-witness class. + +This metadata condition does not alter `runtime_custody_needed`, which remains +the execution-family classifier. Raw HIR/default archive and ordinary +non-Resource paths remain unchanged. Public mutable MIR is never sealed +retrospectively. The entry-aware constructor selects only the exact passed ID +from these authenticated candidates, and still rejects required-only selection. + +## Codegen projection and wrapper + +`resource_execution::emit` validates the original graph with the entry-aware API +before preparation. `EmittedResourceLayout::from_program` repeats that fresh +entry-aware proof after canonical preparation. It serializes a bridge signature, +root Scope frame and Complete row separately from execution-family rows. The +root has only the Root parent. All ordinals are produced from the private typed +record, not from source names or a public map override. + +`plan.function(main)` and `plan.function(absent_required_controls)` remain None. +Both declarations and body translations therefore keep the ordinary two hidden +parameters and ordinary ownership/caller proofs. Entry metadata presence in the +layout's Source-family signature, frame and operation maps exclude bridge rows. +`source_row` retains its mandatory family-plan check. A legitimate ordinary call +to main elsewhere continues to use the ordinary ABI. + +The bridge-aware wrapper performs this exact sequence: + +1. Obtain the installed entry Scope through the existing EntryScope leaf. +2. Validate the compiled function/signature/Scope tuple with ScopeValidate; + ordinary main has no Resource prologue to perform this validation for it. +3. Obtain Network arguments through the existing EntryNetwork checks. +4. Call ordinary main with context, zero environment and ordinary parameters; + do not append the Scope argument or select ResourceEmission for its body. +5. Read the existing NativeLeaf::Status after the body. Do not take, reset or + replace the original body status. +6. Attempt the exact bridge Complete row through ScopeComplete, passing that + original status. ScopeComplete is the existing cleanup leaf. Its selected + error can differ from a nonzero original body status, so do not return that + selected error prematurely before observing the retired root outcome. +7. Read EntryOutcome with the compiled tuple and return its exact original body + status. If retirement or tuple validation failed, propagate the authentic + refusal; do not invent a success or a replacement body status. + +Existing Resource-family entries continue through their current three-parameter +body and completion path. No C ABI, layout wire version, runtime leaf, report +format or provider behavior changes are needed. + +Reachability must explicitly include the selected bridge entry as a native root +without making it an execution-family member. Required-only exclusion and all +ordinary transitive dependency/type checks remain intact. Keep all fourteen +wrappers, twelve original exports, refinements and shared Source fixtures. + +## Required controls + +- Actual original wrapper14 emits and runs in both profiles, with the correct + required primitive values, body status and zero-before-destroy obligations. +- All first thirteen original wrappers and the complete export/refinement + inventories continue to pass; do not substitute simplified Sources. +- Typed plan and emitted signature controls prove main and the ordinary helper + remain ordinary, while only the selected wrapper has the bridge Scope and + Complete leaf calls. +- A genuine checked pure entry with no Resource operation, comptime expression + or required-value row receives its own initial proof-only witness and bridge. + An equivalent raw/default-archive HIR path receives no Resource entry proof. +- A genuine ordinary internal call to the selected main remains valid with its + ordinary ABI; bridge metadata must not give it Source-family authority. +- Missing, foreign or copied/resealed entry witness/body/header substitutions + fail the current private proof checks, before canonical preparation can erase + the mutation. +- A required-only selected ID, wrong current ID, changed original Network/Nothing + header, capture or stale canonical graph refuses bridge construction. +- An ordinary body failure preserves its original status and message/channel, + still attempts exact root retirement, and completes with no live obligations. +- Missing installation, wrong installed tuple and an invalid completion ordinal + refuse without manufacturing Source activation or status. + +Root owns production integration, remaining execution gates and final receipts. +The measured bounded checkpoints and current proof repair are recorded below. + +The initial independent body seal uses existing certified canonical transports. +Consuming ordinary list iteration is covered by a genuine positive control. +Borrowed-view iteration and other unsupported composed transports remain +pending unless their own exact constructor transition is implemented and proved; +this bridge adds no general reseal of a changed body. + +## Bounded object/native acceptance and current compiler checkpoint + +All fourteen whole-Source17 wrappers emitted **28/28** genuine host objects in +both profiles at v14, with zero refusals. The full original 3,600-byte Source +prefix, twelve exports, unused refined contract, required controls, verify and +property100 remain intact and in scope. This main-wrapper gate does not establish +every original purpose or live aggregate value. + +target/native-resource-carrier-compile-evidence/after-v14.json (SHA-256 +43c3a3f1fc2fa6d1e4c6cbd7cc7e0bad4f0d560dce0685e6a8fcb2b411aee2dc) +records actual session 94028, terminal f7b2aa, exit zero. + +The separate v15 gate passes **28 Source-deleted single-entry executables / +28 entries**, including Source13's exact ordinary bounds error, and **26 clean +same-grant reentry executables / 52 entries**: **54 native executables / +80 entries**. Exact completion/message/event reports, all eight zero Resource +custody counts and empty ordinary storage before one teardown, event boundaries +and script exhaustion pass. Source13's repeated ordinary-error call on the same +grant remains pending; thirteen clean Sources in both profiles do not replace +that lifetime obligation. + +target/native-resource-carrier-compile-evidence/after-v15.json (SHA-256 +b20c4d9427b11676e7207e5691171c64a861666e0f728b8aa24dba631123154b) +records actual session 93295, terminal b1a472, exit zero. Both v14/v15 retain +all **3,752 frozen inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The measured v15 runtime receipt pin is +74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. + +The current post-refinement-repair compiler gate is separate from v15 native +acceptance. At v19, **17 focused entry controls (13 MIR + four codegen)** pass, +including four new raw/refinement archive controls. Full package gates pass +**314 MIR checks (313 library + one doctest)** and **181 Cranelift checks +(118 library + 63 object)**, with zero failures or ignored tests. + +target/native-resource-carrier-compile-evidence/after-v19.json (SHA-256 +b549b995be46093f29dbf45314b47f1eea3b09c9bfc20cebf068e477bfaa8f8b) +records actual session 65346, terminal 26ed19, exit zero and all **3,757 frozen +inputs / nine runtime files unchanged** at the same parent. Earlier v3 runtime +gates passed **275 checks at v6** and **44 focused carrier checks at v7**. These +are historical revision-specific results, not current whole-runtime or workspace +success. + +## Current workspace and wider-language observations + +The earlier v20 whole-workspace gate passes **3,582 checks, zero failures and +13 ignored tests**, across **66 real selected groups / 92 total result groups**. +Actual session **75431**, terminal **1f1c8b**, exits zero for +`cargo test --offline -q --workspace --no-fail-fast`. +target/native-resource-carrier-compile-evidence/after-v20.json (SHA-256 +fb59285ab91f6d15ce57ab59ecf7cc5c00187cdf4c323de04b5bbafe4a78e7d2) +records all **3,757 source inputs / nine runtime files unchanged** at parent +91e3706b770124cbddca952e5fe6b558abc170be. The inputs-v20 manifest SHA-256 is +58dd6f590a1bf2fd63f04d0ec0abdd15aa71452e9e00b1ff36b1feb29ab3612a; +the workspace log SHA-256 is +f610439bbbcb334d88de90c8b8ebf4178f1015be564cc7caf63bba2b5d6ed8da. +This is measured workspace success, not fresh ignored-native acceptance. + +All **11 fresh-v3 native gates now pass**, each selected as a real one-test gate. +Actual session **41872**, terminal **3c7b60**, exits zero with **576 Source-deleted +native executions / 672 entry calls**, across Debug and Release. This includes +the unchanged **191-case / 382-execution corpus drawn from 113 distinct Sources**, +carrier controls, reflected-body controls and owned-Return regression. The v20 +workspace's ignored-test count is historical; v21 independently executes the +eleven native gates rather than treating ignored tests as acceptance. + +target/native-resource-carrier-compile-evidence/after-v21.json (SHA-256 +38f67805c93e66e273d29de84c1cfc3351d70c0235bf656ea661244474582fa2) +records all **3,757 source inputs / nine runtime files, HEAD and inventory +unchanged** at parent 91e3706b770124cbddca952e5fe6b558abc170be. The frozen native +binary SHA-256 remains +110ba3b7bf23e63677ac7afd13a83fc2fa98aeccebc553e8df4f294fb5bc872f. +Matched MSVC v3 archive provenance passes before and after execution with receipt +pin 74a811660ee1772e0374e04579dd51234e8ff38191b942335bedaab5b4e4b6cd. +The successful runner report SHA-256 is +699f2364769ccdd489c7322832f2410690129d8f687006239e3dc6f0b6e82ae6. +This gate establishes no GNU runtime-archive acceptance. + +Owned-Return cleanup failure retains caller/entry-root **body255 / cleanup255**; +the observations establish retirement and root completion, not a separate nested +callee completion. The accepted historical Return semantics and exact Source +prefixes remain intact. Earlier prospective fresh-v3, full-191/382 and reflected- +Return gate statements in retained historical sections are superseded by v21; +broader body/Default, generic/reflected lifetimes and occupied custody remain open. + +At v21, Source13's repeated ordinary error remained pending; the v24 +same-grant gate below closes that bounded obligation. Occupied resources, wider +whole-language parity and the successor's exact-revision CI remain pending. +The rough 87% estimate and fixed 207/182 inventory do not advance. + +The separate 15-case wider-language probe is an **observation baseline, not +acceptance**. Actual session **77834**, terminal **ee797f**, leaves compiler +inputs unchanged and records eight emitted objects, six object refusals, one +earlier backend failure and three reference failures. Frontend/driver build +success does not establish linked parity. Its authoritative receipt is +target/native-language-parity-probe-observations-v1/root-after-evidence.json +(SHA-256 434cba150528d36e018d89b9dbe3acb05adf0309ac6a5aeb47a8ea1b906b4c3d). + +## Raw refinement proof boundary + +Pure refinement predicates lowered through raw/default HIR without an +authenticated checked Resource archive stay ordinary through canonical +preparation. Declaration kind or spelling alone supplies no Resource witness or +entry Scope. Raw bodies with actual Resource custody retain the independent +mandatory capture/refusal path. + +Authenticated predicate proof requirements derive the original declaration kind +from the immutable archived function. Removing a witness and changing the current +predicate kind cannot escape that requirement. Constructor seals reject missing +or foreign proof, modified predicate body and changed header. The repair preserves +ordinary raw refinement behavior while retaining authenticated original authority. + +## Remaining acceptance + +The earlier whole-workspace gate passes at v20, and all eleven matched- +MSVC-v3 native gates pass at v21, including full **191-case / 382-execution**, +carrier, reflected-body and owned-Return regression. These remain independent +historical receipts; current ordinary-error regression and workspace acceptance +are recorded at v24 below. Earlier accepted GNU Source-prefix and Return +documentation and wire-v2 receipts retain their independent historical scope. +The successor's exact-revision CI remains pending. No GNU runtime-archive +acceptance is inferred from these local MSVC results. + +Occupied aggregate transport/replacement, broader Resource-valued Default +and generic/nested reflected lifetime compositions remain full-goal obligations. Whole Source/export/refinement and verify/property100 +purposes remain required. No refusal counts as successful Source execution, no +Source policy exclusion is introduced, and the full native parity goal stays +active. The rough 87% estimate and fixed 207/182 inventory do not advance. + +## Accepted ordinary-error successor checkpoint (v24) + +The changed ordinary runtime passes its measured **307-check** suite with fresh +matched MSVC `measurements-02` archives. The accepted twelve-gate native scope +covers **604 Source-deleted executables / 728 entries**: the new all-fourteen +same-grant gate contributes **28/56** from session **62286**, including both +Source13 Bounds errors and original body1/cleanup0/Body completions; session +**76385**, terminal **d30da4**, passes the eleven predecessor gates with +**576/672**. The latter includes full **191-case/382-execution** regression from +**113 distinct Sources**, carrier, reflected body, owned Return and original +Source06. No separate nested callee completion is inferred from Return root +observations. + +The fresh whole workspace passes **3,614 checks, zero failures and 14 ignored +tests**, across **66 nonzero / 92 total groups**, at actual session **75810**, +terminal **0f5a7b**. Independent verification at **58261 / 75fabb** authenticates +all **3,763 source inputs / nine fresh and nine historical runtime artifacts**, +HEAD/inventory, both native binaries and fresh02 provenance/log hashes unchanged. +The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +The v20/v21 receipts above retain their accepted historical scope; current +ordinary-error regression is established independently by v24. Earlier +Source13-pending statements describe their original checkpoint and are now +superseded for this bounded same-grant obligation. See the +[ordinary terminal-error note](native_resource_ordinary_error_reentry.md) for +the exact archive, binary and per-attempt messages/retirement evidence. + +Local MSVC results provide no GNU runtime-archive or full platform acceptance. +The separate normal-main linked baseline is complete: sixteen observations, +twelve exit0 and four exit71, from eight exact opaque objects reused with +Debug/Release runtime artifact profiles; all fifteen raw-probe Sources remain +preserved. This is baseline collection without a desired Source oracle, new +feature acceptance or ordinary public-Main context-retirement observations. +Broader ordinary parity and fixes remain open. Occupied custody/replacement, broader Default and +generic/nested reflected lifetimes, wider language parity and the successor's +exact-revision CI remain open. The rough **87%** estimate, fixed **207/182** +inventories and full 100% goal do not change. diff --git a/docs/completed/native_codegen/native_resource_ordinary_error_reentry.md b/docs/completed/native_codegen/native_resource_ordinary_error_reentry.md new file mode 100644 index 00000000..84a57cf5 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_ordinary_error_reentry.md @@ -0,0 +1,212 @@ +# Ordinary terminal-error lifetime for checked Resource entries + +Status: the historical v24 ordinary-error same-grant checkpoint passed in both +native checking profiles. That twelve-gate scope covered **604 +Source-deleted executables / 728 entries**, and that workspace passed +**3,614 checks / zero failures / 14 ignored tests**. Source13 retained both +original Bounds outcomes without error reset. Broader full-language acceptance +remains required; the rough feature estimate stays **87%**. + +The full 100% native-codegen goal includes existing repeated-entry semantics. +Original Source17's fourteen unchanged wrappers are executed twice in each +checking profile by `absent_generated_unchanged_source17_wrappers_repeat_exact_outcomes_and_restore_entry`: +56 reference entries on one interpreter/provider/grant per Source/profile. Source13 +returns `list.__remove_at: index -1 out of bounds` twice, with empty effects, +zero custody/registry, restored entry context and empty debug events. The full +original Source17 prefix, constructor returns, namespace baked value, explicit +required controls, verify and property100 remain present. + +Historical v10 wire-v3 Source13 single-entry execution passes in Debug and +Release with exact tuple `(1, 0, Body, exit 71)`, signed bounds ordinary bytes, +`native Resource protocol refused` Resource message, no events, all eight Resource +counts and ordinary storage zero before successful destruction. Root session +22590 / terminal b71964 receipt +`target/native-resource-carrier-compile-evidence/after-v10.json` SHA-256 +`a4d03928a4992b8049c0bfe7dc1712de9ca632765db18af4cea63d961695c305` +retains 3,745 inputs and nine archive files unchanged. At that historical +checkpoint the whole-family strict gates exit101 on the separate ordinary-entry +bridge case; this receipt is not whole-family or repeated-error acceptance. + +## Selected implementation + +Use context-owned immutable attempt records for terminal ordinary status, +dynamic message and diagnostic prefix, issued by the exact Resource entry +constructor. Nested calls share the running channel. Legacy session failure and +persistent ordinary cleanup poison remain sticky; historical Resource cleanup +failure keeps its separate accepted reentry behavior. Exact completion/retirement +seals the channel, retains original body and cleanup observations and creates no +error reset. Only an authenticated next constructor can select a fresh channel +after exact predecessor/ordinal, installation, budget and all custody/storage +checks. A report refusal/tombstone or partial cleanup cannot authorize reentry. + +Existing refinement capture remains within a valid active evaluation. It cannot +take a sealed terminal error or provide reentry authority. The private launcher +requests the declared next entry after complete observation; runtime guards decide +admission, and the first nonzero process exit and every completion survive. +No Source syntax, provider API, runtime-global rule or new recovery construct is +introduced. ABI1 and current layout/report protocols need no new export for this +internal lifetime, subject to genuine verification. + +## Current measured runtime and archive checkpoint + +The ordinary-error successor is now present in the managed private worktree. +Actual session **3890**, terminal **56ec08**, exits zero with **307 runtime +checks / zero failures** and genuine matching Debug/Release MSVC archives in +`measurements-02`. The measured receipt is +`target/native-resource-carrier-compile-evidence/after-v23-measurements02.json` +(SHA-256 a429ef699cfe5804fce2755f53fdb9869c4dc7d7a393ce00ec18fb7fc0db91d3). It records **3,763 source inputs**, nine new runtime +artifacts and nine historical artifacts unchanged; `measurements-01` remains +unchanged. Capture pin +`6dea1dd7a2b26e366a8b76d80fe0f403f8e5694baab41870659f6f54f1cce707` +and Verify pin +`535b996c1b697694514cbfb92c81094cc4c8a9fc11b630a7c5877c477b11e033` +authenticate fresh archive receipt +`9c6ada6414341f3d2755e6e896f1b50d42a4db133363d0c066057665551171ef`. + +The native harness build separately passes at actual session **93714**, terminal +**3a2dc1**, with binary SHA-256 +`9a6a84c4ef0643cb4a2760c29d0b532ffde43d934cead0056e72ae39381d4301`. +The v24 freeze SHA-256 is +`1eb489a0eed0f294dc7111609e019995b5f82c09c7be46745792d51aae68b578`: +3,763 source inputs, nine fresh runtime artifacts and nine historical artifacts. +Neither runtime tests nor binary construction alone establishes Source execution. + +## Accepted all-fourteen gate and predecessor regression + +The new +`native_resource_absent_aggregate_all_original_sources_reenter_with_same_provider_grant` +gate passes in actual session **62286**: **28 Source-deleted native executables / 56 entry calls**, across +Debug and Release. Each unchanged original Source17 wrapper runs twice on one +retained Session/provider/grant. Its completed process record is +`target/native-resource-ordinary-error-native-v24/run-01/absent_aggregate_all14_error_reentry.process.json` +(SHA-256 e69c14092fac1d1f64d44b453acab559ed962e234da2498778741e2862084c0a); stdout/stderr SHA-256 are +`4e8e36bfd4ff70e92064961a992771fffdd19dcc1b2cb08c1a4ad5cb0a9d3a84` +and `862e8121741ea9ee81462e498e7686fb07e93d2278ed7fba20d1ae20be70290a`. + +At that historical v24 checkpoint, Source13 retained **both** original +`list.__remove_at: index -1 out of bounds` ordinary messages and +`native Resource protocol refused` Resource messages. +Both attempts retained **body1 / cleanup0 / Body**, with first process exit **71**, +monotonic attempt identities, zero eight Resource custody counts, empty ordinary +storage before destruction, no events, exhausted script and one successful +Session destruction. The unchanged full Source prefix, constructor returns, +namespace baked value, explicit required controls, verify and property100 remain +in scope. This records an actual gate result, not merely the earlier clean +thirteen-row gate or a build. + +The accepted combined **twelve-gate** scope is **604 Source-deleted +executables / 728 entries**. It joins this authenticated new **28/56** gate with +eleven freshly rerun predecessor gates: actual session **76385**, terminal +**d30da4**, exits zero with **576 executables / 672 entries**. The latter includes +the unchanged **191-case / 382-execution corpus from 113 distinct Sources**, +reflected-body, owned-Return, carrier and original Source06 regression. The first +gate is reused only as its authenticated 28/56 result; all 604/728 executions are +not attributed to the resumed session. + +The fresh whole workspace independently passes **3,614 checks, zero failures +and 14 ignored tests**, across **66 nonzero / 92 total result groups**. Actual +session **75810**, terminal **0f5a7b**, exits zero. These are measured counts, +not projections from v20 or credit for ignored tests. The twelve native gates +were separately selected and actually executed. + +Independent Root verification, actual session **58261**, terminal **75fabb**, +exits zero and authenticates all **3,763 source inputs / nine fresh and nine +historical runtime artifacts**, HEAD/inventory, both native binaries, fresh02 +provenance and process/log hashes unchanged. The authoritative receipt is +`target/native-resource-carrier-compile-evidence/after-v24-native12-workspace.json` +(SHA-256 +`4dabdff125cefac5891d6b026cb0b67ec14448db67915755116f014698ea6aa2`). +Its native runner success SHA-256 is +`5737084cdeea8bc8fb5be172ab0788051c19800344f89d093b5142c98c13decb`; +workspace stdout/stderr SHA-256 are +`101f3e11d9726dba3b3ada6f8c7b4472d32e245c694e18dc8c2ee2a3eaea98ed` +and `7825efe2c018844c19f4842027808208f236b7750314f5beb85f6d4acc247cea`. +The first gate's independent source/artifact authentication is retained in +`after-v24-disk-recovery.json` (SHA-256 +`77387a2d98b1e4363dd19abf534cb00020a8c8f562c67842af47b8a17b95d4c7`). +The measured successor is at parent +`202cf02066b9e84c8994d88a184879454d5c11e3`; this does not claim exact-revision +CI for the not-yet-published runtime changes. + +Broader ordinary normal-main archives separately prepare successfully at actual +session **26531**, terminal **421094**, with receipt SHA-256 +`781bb358a5965b87b0b75fa591352f50f69b638453fbddcd9d0d5ecee558c371`. +The full normal-main linked baseline is now collected at actual session +**44746**, terminal **a66d7d**, exit **0**: **16 observations**, comprising +**12 exit0 / four exit71**, from **eight exact opaque objects** reused with +Debug/Release runtime artifact profiles. All **15 original raw-probe Sources** +remain preserved, and isolated Sources were deleted before linking. Source +checking used release=false and codegen optimization=false; the two artifact +profiles do not establish separate Source-checking or codegen profiles. + +Independent evidence is recorded in +`target/native-resource-carrier-compile-evidence/after-v24-ordinary-linked-baseline.json` +(SHA-256 +`c22d671cad677ae97d68c82034d97437e65d0538d12640d7a1dd47efb6c71e84`), +with raw linked report SHA-256 +`131bf7f52e1a8c7c58d24041db7e62637ee0021ded140c11f39e0e79d6a6f18a`. +Original Source, archive, object, executable and log hashes authenticate the +collected baseline. Ordinary public Main artifacts expose process exits and +diagnostics without context-retirement observations. This collects actual raw +behavior without a desired Source oracle or new language feature acceptance; +broader ordinary parity and fixes remain open. Historical emission hashes still +do not authenticate the fourteen changed current Source paths. + +Earlier v20 workspace and v21 eleven-gate receipts keep their historical scope. +The v24 receipt above separately establishes regression acceptance for the +changed ordinary runtime. +Owned Return's caller/entry-root body255/cleanup255 does not prove an independent +nested callee completion. Local matched MSVC archives confer no GNU runtime- +archive or complete platform credit. Occupied aggregate custody/replacement, +broader Resource-valued Default, generic/nested reflected lifetimes, wider +language parity and the successor's exact-revision CI remain open. The rough **87%** feature +estimate, fixed **207/182** inventories and full 100% goal stay unchanged. + +## Deterministic reservation refusals + +The managed runtime successor includes private `cfg(test)` one-shot +faults immediately before five native-protocol fallible growth reservations: entry +active-frame storage, immutable completion history, ordinary attempt channels, +and Resource/ordinary diagnostic copies. Two additional core frame/active-stack +reservations prove that a failure after ordinary preparation publishes no channel +or custody. Non-growing reservations do not consume a fault; ordinary-channel +proof refusals precede its allocation hook. Resource frame/history preflight stays +in its existing earlier order. Diagnostic faults occur after bounded-length +validation and real retirement; report refusal seals a retained tombstone whose +exact first ordinary failure and completion cannot authorize another entry. +Sealing/history publication itself remains covered by storage reserved before +entry publication. These deterministic controls belong to the measured +307-check runtime suite; +they do not independently establish Source execution or allocator behavior +outside the selected hooks. Fresh measurements-02 is separate from unchanged +historical measurements-01. + + +## Ordinary-only channel clarification + +The checked entry has one ordinary first-error channel shared by root and nested +calls. An exact propagated ordinary failure keeps its original ordinary bytes +without creating a second Resource refusal. Independently recorded Resource +errors and cleanup failures remain observable with their existing priority. +Generated Scope completion still authenticates the returned status; a mismatched +or unattributed nonzero Source body status remains a protocol error. The observed +entry body status and the selected Body report channel do not by themselves +imply Resource bytes. + +The v10 and v24 records above really observed an additional +`native Resource protocol refused` message for Source13. Those observations, +sessions, hashes and counts remain historical facts. The extra message is +superseded as the current ordinary-only oracle; it is not a root/nested semantic +distinction. The existing Runtime completion control already distinguishes an +ordinary assertion failure with an empty Resource message from a real wrong-hook +Resource refusal. + +Only the native expected Resource bytes for ordinary-only failures are corrected. +Original Source, reference Bounds bytes, body1/cleanup0/Body, first exit71, +events, custody/storage, exact retirement, script exhaustion and same-grant +reentry checks remain unchanged. Corrected affected native gates now pass in +bounded MSVC acceptance: all eighteen Resource gates cover 846 Source-deleted +executions and 1,038 entries. See the current scalar checkpoint in the +[native acceptance audit](native_acceptance_audit.md). Old passing receipts do +not prove the corrected message oracle. Broader workspace, CI, GNU and installed +acceptance remain open; these bounded results do not complete the full goal. diff --git a/docs/completed/native_codegen/native_resource_ownership_boundaries.md b/docs/completed/native_codegen/native_resource_ownership_boundaries.md new file mode 100644 index 00000000..27195d45 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_ownership_boundaries.md @@ -0,0 +1,129 @@ +# Static opaque Resource ownership boundaries + +Status: the selected checker guards pass 36 focused groups, including 21 new +source groups, and the broader compiler/object/driver/frontend/lowering gates. +All 34 frozen source witnesses are rechecked in debug and release with stable +inputs. These are static prerequisites for live Resources. No runtime carrier, +provider installation, source-scope cleanup or native Resource operation is +introduced. Exact clean `b7180bd9` subsequently passes the full workspace; its +supported-host run remains in progress. The accepted local and platform status are in the +[acceptance audit](native_acceptance_audit.md#resource-static-ownership-boundaries). +The broad estimate remains about 85%; 207 inventory fixtures, 182 object +obligations, 464 supplemental cases and the whole-language 100% goal are unchanged. + +## Selected source contract + +The [opaque resource contract](../opaque_runtime_resource_contract.md) +requires one owner and one cleanup obligation. Resource views cannot acquire +ownership by copying or be hidden in an owned payload. This does not ban owning +aggregates containing Resource data, safe nonowning field inspection or type-only +metadata. Production hook catalogs remain empty. The pre-Rust boundaries and +subsequent narrow diagnostic amendment are captured in: + +- `target/native-resource-copy-guards-draft/contract.md` and `getter-owner-note.md`. +- `target/native-resource-view-payload-guards-draft/contract.md`. +- Its additive `map-machine-contract.md` and `diagnostic-dedup-contract.md`. +- `target/native-resource-source-guards-root-diagnostic-amendment.md`. + +## Implemented static boundaries + +| Operation | Checked boundary and preserved behavior | +| --- | --- | +| Plain field read used as an owning value | E0364 if the selected returned endpoint recursively stores Resource data. Explicit field views and unrelated copyable fields remain legal, including in Resource-bearing owners. | +| Clone and cloning collection/reflection getter | Existing recursive no-clone rule also covers `list.__get_clone` and owned field-value getters. Cloning a parent/endpoint cannot acquire another Resource owner. Consuming `map.get` is not converted into a clone or blanket banned. | +| Owning Some/Ok/Fail, list, map value or struct payload | E0401 for an explicit or otherwise known Resource-bearing view, in inferred and contextual checking. Actual owned transfers, None, empty containers, non-Resource views and phantom generic arguments without stored Resource data retain current admission. | +| Machine constructor or transition target payload | The same typed child acquisition guard applies. Real owned target data, empty states and ordinary type/arity checking remain intact. Enum written/aliased views retain their existing E0375/E0401 checks. | +| Machine transition source | Only a valid exact selected state whose data contains Resource invokes the existing owned-source gate: E0375 for a written view and E0401 for a known viewed alias. Actual owned sources and Resource-free selected states retain current behavior. | +| Exact Resource-root value reflection | E0300 for variant/state value reflection and all three owned field-value getters. Type-only Resource metadata and aggregate variant/state metadata remain available. | +| Exact Resource-root print/println | E0300 with ordinary-print wording. Release mode also retains E0362. Nested printing and hidden-Secret, known-absent or omitted rendering remain unchanged and unresolved. | + +The recursive data predicate follows checked concrete fields and payloads, +including the exact selected MachineState. Unused nominal generic arguments do +not create stored Resource data. This is not a generic contains-Resource ban. + +Owning versus borrowed field projection is explicit checker context. Only the +existing place path and transparent wrappers carry borrowed projection context; +a surrounding view does not authorize borrowed data inside a constructor. +Calls and owning payloads perform their normal child checks. Known-view facts +come from explicit View and existing viewed bindings/aliases, with the existing +parenthesis/coarsen/declassify handling; no general call-result borrow provenance +or view-expiry rule is introduced. + +The payload diagnostic is emitted once per diagnosed source span, after typed +Resource-data and known-view checks. The guard records only that occurrence's +exact identifier legacy ownership counterpart. Existing ownership traversal +continues and retains unrelated E0400 and unhandled-value diagnostics. This +prevents duplicate E0401 without suppressing other errors or source checking. + +## Executed source and compiler evidence + +The first focused run passes 33 of 36 groups. Its three failures concern duplicate +inferred-map payload diagnostics and test sources that also require ordinary +unhandled-value diagnostics. The diagnostic-only/source-test correction above +preserves those policies. The initial result is retained in +`target/native-resource-source-guards-focused-initial.json`; it is not acceptance. +The earlier in-progress composition hash guard also refused to compose before a +final manifest existed; `target/native-resource-source-guards-composition-guard-failure.md` +records that no tracked mutation occurred. + +The final focused run passes **36 groups**, including **21 new source groups**, +with all three source hashes unchanged in +`target/native-resource-source-guards-focused-after-diagnostic.json` and its log. +Both profiles cover copied fields/getters, inferred/contextual payloads, +forwarded and generic viewed aliases, bare-view diagnostic deduplication, +independent errors, exact-state transitions, root observation and preserved +owned/empty/non-Resource controls. + +The frozen 20-source probe and additional 14-source payload probe all parse and +resolve under synthetic `SourceOrigin::Stdlib`, reserved FileId 10000 and an empty +resource-hook catalog. Their debug/release checks produce these actual results: + +| Frozen source family | Observed outcome in both checker profiles | +| --- | --- | +| Original 04 through 06 and 08: field/reflected/getter copies | E0364. | +| Original 09 through 11: borrowed optional/list/struct payloads | E0401. | +| Additional 01 through 03: contextual/inferred/aliased map values | E0401. | +| Additional 06 through 08: machine constructor/transition target payloads | E0401. | +| Additional 09: viewed Resource-bearing transition source | E0375. | +| Original 07: root variant/state value reflection | E0300 for each operation. | +| Original 02: direct/root ordinary printing | E0300 per call; release additionally retains E0362. | +| Original 01 and 14 through 16/20; additional 10 through 14 | Resource tags, true owned/view transfers, type-only/absence and checker-stage comptime/property controls pass. No value evaluation or sample generation is claimed. | +| Original 12/13/17 through 19; additional 04/05 | Existing recursive-clone, view-return, interpolation/declassify, construction, JSON and enum-view refusals remain. | +| Original 03: nested printing | Still admitted in debug; release rejects printing with E0362. This open boundary is not hidden by the root guard. | + +Exact reports are `target/native-resource-source-guards-original-witnesses-report.json` +and `target/native-resource-payload-audit-616b51b9-v2/report-after-source-guards.json`. +Their proof wrappers retain stable fixture/compiler inputs. All 34 source checks +are characterization only: no interpreter evaluation, comptime execution, +property generation, Resource provider or native Resource execution occurs. +The initial payload audit's malformed inferred-map source is retained separately; +the corrected frozen v2 source is the one checked above. + +The broader gate passes **1058 library units** (63 codegen, 417 comptime, 139 HIR, +99 MIR, 54 resolver, 254 typechecker and 32 types), **63 codegen object integration +tests** (1.11 seconds), **83 driver tests** (69.79 seconds), **598 frontend fixtures** +(22.13 seconds) and **eight backend-lowering tests**. All three source hashes +remain unchanged. `target/native-resource-source-guards-phases.json` exits zero +at `2026-10-03T12:28:58.9045108Z` and names each exact gate log. This affected-phase +acceptance is distinct from a new complete frozen workspace and supported-host +run, both still pending. + +## Required work before live providers + +These guards do not implement a Value/interpreter Resource carrier, runtime hook +installation, HIR/MIR cleanup ownership or native ABI/drop support. Deterministic +fake-provider execution must prove actual moves, scopes, temporaries, failed +arguments, aggregate arms, error paths and exactly-once finalization before real +provider operations can be exposed. Consuming collection extraction must transfer +an owner and clean remaining elements rather than duplicate an internal key. + +Other acquisition/escape paths and general call-produced borrow provenance still +need audit. Nested ordinary Resource printing requires an observation boundary +for occupied data, hidden Secret, known absence and omitted values; this slice +does not select that policy. Trace/breakpoint type-and-ownership-availability +summaries are distinct from ordinary printing and expose no registry key, slot, +generation or provider state. The later private [reference lifecycle](native_resource_hook_identity.md#reference-lifecycle-execution) +now has carrier/custody and scripted-provider Source tests; those 36 passing +reference groups are separate from the static checking evidence above. Production +providers, opaque native layouts and native Resource values remain unsupported. Actor/message transfer, pending work, cancellation, +late completion and real provider authority remain independent lifecycle work. diff --git a/docs/completed/native_codegen/native_resource_pipelines.md b/docs/completed/native_codegen/native_resource_pipelines.md new file mode 100644 index 00000000..11e39086 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_pipelines.md @@ -0,0 +1,49 @@ +# Source-only native Resource pipeline evidence + +This records checked Source pipeline coverage for existing Resource behavior. + +The expanded pipeline packet has measured local acceptance for **23 cases in +Debug and Release**. Its frozen-input receipt, +`target/native-resource-pipeline-source-evidence/acceptance-v2.json`, records +worktree HEAD `5d01097e5330d5aaa894c5fc92c786378f0e105a` and all **3,648 inputs +unchanged**; both measured archives and six nested receipts separately revalidate. +The input +snapshot identifies this expanded test packet; the recorded HEAD alone does not +establish acceptance of a later committed revision. Its measured +archive-receipt SHA-256 is +`29dd83c7f3874d7fe4b4fd3a5784786a5b31ee45337e9f40f133da9f33316d42`. + +| Gate | Recorded evidence | Limit | +| --- | --- | --- | +| Checked Source reference execution | 46 pipeline attempts, included in the full 161-case / 322-profile-execution reference corpus; 522 reference tests pass. Exact handwritten outcomes and event order; zero outstanding owners and zero registry entries before teardown. | The reference observation tuple does not measure unconsumed script entries. Full reference acceptance does not prove full linked native acceptance. | +| Cranelift object preflight | 46 emitted objects through the checked Source/HIR/MIR path | Object emission does not establish linked execution. | +| Linked local native execution | 46 Debug/Release executions with Project and synthetic Stdlib Source files deleted before launch; exact status, messages and events; zero obligations before teardown and no remaining scripted inputs | Uses matched local Windows MSVC archives; does not establish GNU or other platform acceptance. | + +The packet covers construction/move/close pipelines, written-view +retention, bare-owner view operations, named-argument abort ordering, domain +failure stopping the next step, and an indirect close descriptor. Its failure +rows check construction and borrow errors plus finalizer panic and retirement +before teardown. These are genuine checked Source programs; the scripted +provider selects outcomes for their runtime operations. + +The two test-only additions now have measured Source reference and local native +acceptance: + +| Accepted pipeline row | Measured existing behavior | +| --- | --- | +| `pipeline_bare_owner_provider_panic` | Provider panic retires the owner; native body/cleanup/channel `1/0/2`, exit `73`, message `runtime operation panicked`. | +| `pipeline_bare_owner_provider_panic_cleanup_wins` | Finalizer panic takes precedence over the provider panic; native body/cleanup/channel `1/255/3`, exit `71`, message `native Resource cleanup failed`. | + +The earlier 21-case / 42-execution receipt remains historical evidence. The +preserved complete gate passes **161 Resource cases / 322 Source-deleted native +executions** in both profiles (1256.05 seconds). Its receipt is +`target/native-resource-pipeline-source-evidence/full-acceptance-v2.json`; all +3,648 frozen inputs, both measured archives and six nested receipts revalidate +unchanged. No GNU or other platform acceptance of the expanded test packet is +claimed here. + +Broader Resource `For`/`Match`, reflected bodies and ordinary borrowed-data +families remain separate obligations. This supplemental packet leaves the fixed +`tests/native_parity.json` inventory at **207 fixtures / 182 run-pass lowering +obligations**, and leaves the broad planning estimate at **about 87%**. It does +not close the complete native-language or distribution gate. diff --git a/docs/completed/native_codegen/native_resource_primitive_companions.md b/docs/completed/native_codegen/native_resource_primitive_companions.md new file mode 100644 index 00000000..46089f40 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_primitive_companions.md @@ -0,0 +1,115 @@ +# Occupied Resource struct primitive companions + +Status: implemented with bounded local MSVC acceptance. Full-language and +supported-host acceptance remain open. + +The selected v9 extension connects the existing implicitly copyable primitive +surface to occupied Resource struct construction: all eight integer types, +float32/float64, immutable string, bool and nothing. Transparent aliases retain +their canonical checked TypeId and source reflection metadata. No new language +spelling or admission rule is selected. + +The original checked declaration, exact field relation, written argument order +and private constructor operation proof remain authoritative. Physical +validation checks integer width and range, a distinct `Value::Uint64` carrier, +and float32 as the exact f64 widening of binary32, preserving signed zero, +infinities and NaN behavior. It never rounds an untrusted supplied carrier into +acceptance. The ordinary checked expression path applies the declared +primitive's existing normalization rules first; that rounding boundary remains +separate from physical validation. + +Every `run` layer remains transported data. Integer, floating-point and bool +carrier children store depth separately from payload bits. Nothing preserves +its existing scalar-word depth encoding: codegen sends zero payload plus depth +into the carrier and reconstructs the depth word on observation. String handles +preserve their existing depth through clone, observation and retirement. +Construction does not imply a join or readiness. Single-word observation refuses +pending carrier scalar data; the generated path uses the depth-preserving pair. +The two additive authenticated leaves, +`jett_rt_v1_resource_carrier_child_scalar_task` and +`jett_rt_v1_resource_carrier_observe_scalar_task`, retain all existing signatures +and ABI1/wire3. The composed catalog becomes 58 production exports: 38 ordinary, +15 carrier and five startup. The eight private hooks remain unchanged. Depth +grants no Resource ownership, provider authority or session proof. + +The extension preserves the current Callback, Owner, Actor graph and generated +startup contracts, including exact original/current archive transitions and +canonical collection iteration. New scalar Run/Join traversal remains tied to +its original checked constructor endpoint and qualifier; it does not bypass +Graph transport or general Resource custody validation. Driver artifact reuse +is bound to the exact source, support bytes, profile and runtime archive. It +caches completed artifacts, never live contexts, authority or session witnesses. + +Original Sources33..35 and their support remain unchanged. New Sources36..44 +cover primitive and alias fields, width boundaries, binary32 normalization, +distinct float64/uint64, pending transport and inspection, prior ordinary +failure, and the primitive pending matrix. A uniquely named trusted support +namespace is appended only for the new collectors. The authored 23-case table +includes clean completion, failed factories, cleanup panic and expected exit71. +It supplies normal and same-provider-grant reentry controls in both profiles. +The accepted execution counts and their evidence scopes are recorded below. + +## Verification and remaining acceptance + +The original v9 all-target Cargo check, Rust formatting checks and 82 +measurement-helper controls passed. The initial eleven focused compiler groups +passed 46 tests, including both real reference-oracle controls over all 23 cases +and both checked source modes, plus Driver object emission in both Jett +optimization settings. Object emission alone supplies no linked execution. + +After the ordinary source-completion repair, actual Cargo runs pass all four +affected compiler libraries: 1,322 tests (Comptime 620, HIR 221, MIR 347 and +Codegen 134). The Runtime library passes 366 tests, including exact ordinary +status, independent Resource refusal and cleanup controls. These unit runs +precede the private report-copy repair and two-file native oracle correction. +Only those three exact guarded files differ afterward. The unit groups do not +constitute a full workspace test run. + +Resource source completion now retains the ordinary diagnostic when the +returned nonzero body status exactly matches that recorded ordinary failure. A +wrong nonzero status, independent Resource failure or real cleanup panic retains +its existing refusal or cleanup channel. The private test observation API copies +the selected channel directly, preserving an empty Resource message for an +ordinary-only failure. Production failure-copy behavior is unchanged. + +Six stale native expected Resource-message fields are corrected across two test +files: five ordinary Bounds cases and absent Source13. Their original Source, +reference message, body/cleanup status, selected channel, process exit, events, +custody and reentry assertions remain unchanged. Historical duplicated refusal +bytes are explicitly superseded; no root/nested special case is introduced. + +Fresh normal and private MSVC Debug/Release archive measurements pass the +58-export catalog, COFF, static CRT, exact C signatures, source/fingerprint and +ordered native-library guards. The corrected native gate is freshly compiled +against its exact private receipt pin and dep-info. All sixteen corrected +predecessor Resource gates pass 754 Source-deleted executions and 900 entries. +Both new scalar gates pass 92 Source-deleted executions and 138 entries: 46 normal +executions complete 46 entries, and 46 same-grant reentry executions complete 92 +entries. Reentry counts come from successful guarded Rust loops and their +per-child decoded report assertions; they are not individual per-child log +counters. Existing scripts, streams, reports, statuses, custody and ordinary-empty +checks remain required. The current Resource and Startup receipts bind the +complete corrected source. + +Earlier Graph acceptance passed 32 Source-deleted executions, and selected normal +Callback, Owner, Actor and interface regressions passed 42 tests before the isolated +oracle correction. Their source differs only in those two exact expected-message +tables. The unchanged selected tests and production code retain that bounded +evidence; it is not a fresh execution of the corrected test binary. The fresh +guarded Startup rerun passes its complete 80-execution matrix. Earlier failed +native attempts supply no acceptance. These results do not constitute a fresh +full workspace or complete language conformance run. + +Full workspace/inventory, exact-revision supported-host CI, GNU and clean +installed-distribution acceptance remain pending. The known deterministic CI +failures remain a separate repair obligation; this slice claims no exact-revision +CI acceptance. Archive measurement and object emission alone supply no runtime +result. Earlier donor and predecessor results retain their original scope and +revision. + +Generic and recursive Resource aggregates, refinements, secret qualifications, +collections, projection/extraction/replacement, reflection, captures and +platform breadth remain full native-codegen obligations. This extension adds +no Source exclusion. The bounded results above do not establish full-language +coverage. The rough full-goal estimate remains 87% until broader acceptance +supports a change. diff --git a/docs/completed/native_codegen/native_resource_private_observations.md b/docs/completed/native_codegen/native_resource_private_observations.md new file mode 100644 index 00000000..f1bba716 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_private_observations.md @@ -0,0 +1,189 @@ +# Private Resource Runtime observations + +Status: the implementation across 16 Rust paths passes 407 focused local checks +and six fresh MSVC archives have been measured. The fresh compiler gate is +accepted. The bounded native ingress and whole-Source17 gates now pass. Archive +measurement and compile-only acceptance grant zero new Source executions. Historical reports retain their original protocol and limits. + +| Contract | Value | +| --- | --- | +| Production Runtime ABI | 1 | +| Current Resource layout wire | 3 | +| Production Resource/private C exports | 58 / 8, unchanged signatures | +| NativeResourceCounts / ArchiveInfo | 72 / 32 bytes | +| Receipt / normal launcher manifest version | 1 / 1 | +| Private ArchiveInfo.test_protocol | 2 | +| Private Source report | JTRRP002, numeric version 2 | +| Private host-control envelope | JTRHC002 | + +The double-private recorder measures five separate cumulative pipeline stages: +all HookPrepare ingress, acquire-HookPrepare ingress as a subset, FactoryPrepare +ingress, FactoryCommit ingress and actual provider.construct dispatch. A live +authenticated context is required for attribution. Refused outputs/handles or +early purpose/descriptor/provider checks do not erase already observed ingress. +Unknown/dead context is unobservable rather than an attributed zero. These +stages are not summed into a fictitious acquisition count. + +Successful Property mutations retain exact optional context bytes. For Resource- +ABI calls entered with an installed Property context, validated prologues and +first real completion publication retain exact call/scope and checked target +identities, original body versus selected failure status and actual installed +int64 parameter bits. Genuine Begin/End use the original attempt owner; counters/ +history stay cumulative across reentry and observation. Nested calls entered +with Property context remain raw history. Calls entered without that context, +ordinary ABI calls and non-int64 parameter values are explicitly unobserved. + +The matched private launcher copies an owned Rust snapshot through the existing +authenticated state contract. It adds no C export or pointer-layout field and +does not retain Jett values, consume provider script, clear failure channels or +grant Source/custody/provider authority. Strict report decoding rejects old +versions, unknown flags, malformed kinds/lengths and broken attempt/call ordering. +Known sticky incompleteness is preserved in reports and excludes Source17 +acceptance. None and Some(empty) remain distinct. Bounds/allocation failures +never fabricate known-zero telemetry or change Source error selection. + +Generic decoding retains legal context overwrites and redundant clears; it does +not select a new Source rule. Source17's separate oracle compares its actual +runtime schedule and installed argument bits to the retained checked plan; +static labels remain expected-only inputs. Each independent +acquisition stage must be measured rather than inferred from provider events. +Archive measurement alone establishes no native history; the accepted runs +below establish only their reported observations. + +Individual ordinary storage-kind counts, private artifact-retainer counts, +normal-launcher custody/reentry and native physical lifetime remain unresolved. +This instrumentation does not by itself grant full native acceptance. + +Ingress controls must keep original-Source entries, host ABI controls and pure +recorder/writer controls separate. Mode 11 has no Session or Source-entry credit. +Mode 12 deliberately expects a BYTE_LIMIT observation fault while preserving real +diagnostic/status/cleanup; that control cannot certify Source17 telemetry +completeness. Source17 requires every sticky incomplete flag to be clear. + +## Local checks and measured archives + +The 407 focused checks comprise 389 normal-cfg Runtime library tests, 12 strict +report decoder controls, one Source17 wrong-value/order/incomplete-call oracle +control and five host decoder controls. These local tests do not execute Jett +Source through the matched private launcher. Their logs under target/native/ are: + +| Log | SHA-256 | +| --- | --- | +| private-observation-v2-runtime-unit-v1.log | bd2b734d74ce3e05d7ca4532bca33d2ca1afe02e0f1caaad27d44245011c5d87 | +| private-observation-v2-report-controls-v1.log | 5ad4ac199ef13dec8fa76d6df08815f8ad98e43042b1aa34d9e995a23f3fbf80 | +| private-observation-v2-source17-oracle-v1.log | a5cca540afe1c11a8c3c7e75a6287ee0232f94e368101d7a792ffd9e56feeecd | +| private-observation-v2-host-decoder-v1.log | 5882ae7b4cc7f69c64df48b8d1c69976b1a1b39e96252da8594c87d69d56871b | + +The paired measurement completes with both child processes exiting zero, reaped +owned children and a released lease. All 77 recorded Runtime input entries, +config/helper maps, actual tool maps and 68 historical artifact guards remain +unchanged. Four normal rows cover Runtime+Launcher +Debug/Release; two private rows cover Runtime Debug/Release. Exact receipt paths +are relative to target/native/: + +| Receipt | SHA-256 | +| --- | --- | +| private-observation-v2-normal-msvc-v1/receipt.json | 4e41eca939e70f4c1ea162639b3f714510726a118a200d38bb89bddc6d575e8c | +| private-observation-v2-private-msvc-v1/receipt.json | d19cc38b1d38a6f84783ad0fd1dc219cea722d88ce5e1db8ddba2b025d64dbe0 | +| private-observation-v2-measurement-paired-v1-terminal.json | 411f53b3f27b98962ed900c336bd1a7c89f88c7836c7ccebdccd3b53ee6aa52f | + +The actual Source17 normal-receipt literal is refreshed from that measured +receipt. Its harness hash is +201788c70e0ecb6dc3e475970606e7e3a747e28af00181541b131888a65da26c; +the whole original Source remains +c8e74848356a5b3d09ab6659318b1e1292718d34443f7f37222bafd6f3eed0ff. +The 16-path formatted map d8da07ef518ed74c09fb9f27b5dc4947ceb9101f450508f653f22fdc60dc0e50 +precedes that one literal replacement; the actual replacement is recorded +separately, rather than rewriting the earlier map. + +The repaired verifier authenticates the actual linker invocation by physical +file identity and digest while preserving strict recorded command spelling. Its +exact pinned measurement join uses the genuine preflight's observed empty +liveness list; the terminal's duplicate empty-object serialization is preserved +as a known serialization defect and supplies no liveness observation. The guard +controls pass four real argument baselines and 12 refusal controls. These checks +do not replace native execution. + +The native ingress and Source17 v2 history/counter gates now have accepted +execution receipts joining the compiled gate, exact archive/source/tool inputs, +raw reports and authoritative execution terminals. Actual categories and +observations follow below. Per-kind physical observations and full_acceptance +remain open. GitHub CI remains disabled. + +## Accepted compiler gate + +The fresh MSVC native-conformance gate compiles successfully and its inner and +outer terminals are accepted. The retained gate image is 40,563,712 bytes with +SHA-256 7a626568c675d99fe8464843efe69735232d9e0b8fb0d786853193801fde6205. +Exact receipt paths are relative to target/native/: + +| Receipt | SHA-256 | +| --- | --- | +| private-observation-v2-gate-compile-v3/gate-acceptance.json | 3d05e4499c87c4d606ca515ca1ab2c7c9ece96331749bb564b62dfe017f78cc3 | +| private-observation-v2-gate-compile-v3/terminal.json | 55d5c2889f7fa5fae6e19a668c3cee6f07e71c9a7bdfd35bfc75de219691b3e6 | +| private-observation-v2-compile-supervisor-v4/terminal.json | 2276b0b86a8469365642ce61179a323573a013e1b255cedcaab8c474a75866a5 | + +The compiler process exits zero and is reaped, its root handle is disposed, +owned processes are quiet and both leases are released. The accepted guards bind +1,089 compiler/Source inputs, 69 fresh Runtime artifacts, 68 historical artifacts, +33 measurement artifacts, 27 helper entries and 182 original run-pass inputs. +The outer watcher observes a maximum 7.5453 GiB within the 8 GiB limit. These +receipts explicitly record zero Source and zero host ABI executions. + +Two earlier attempts remain preserved and unaccepted. The first watcher refuses +a recycled descendant PID after Cargo's successful compile and terminates the +producer; the second preserves its guards and releases its leases but refuses +a cached test image. The accepted third gate requires a fresh compiler +invocation after updating only the test target timestamp; its Source bytes +remain 1eb3316afe08153c48f4da8e9e33cc233ac30896d6b3695947f5e45d0c4b7e75. +The original Source17 fixture remains unchanged. Five pure PowerShell and eight +pure Python identity controls also pass; they are separate support checks, not +additional Jett Source executions or part of the 407 local-test total. + +## Bounded native observation acceptance + +The ingress gate passes 32 actual processes: eight original-Source processes with +10 actual entries read from report headers, 22 host ABI controls and two pure +recorder/writer controls. Its four linked artifact rows are build evidence, not +executions. There are two distinct original Source hashes. Source processes, +host ABI controls and pure writer controls retain separate credit categories. + +Whole unchanged Source17 passes eight processes/four objects across Debug and +Release with matched normal and private launchers. The four private processes +enter twice each on the same context, giving eight private entries. For each +profile, the private Property process records two actual 100-trial entries and +6,404 cumulative history records; the private Verify process records four +history records. All private incomplete flags are zero. Each of the five +independent acquisition counters is measured zero, rather than inferred from +empty provider events. Successful Property context/call history and actual +installed int64 bits pass the retained Rust oracle. Exact bits stay in original +hashed raw reports and JSON bytes; no JavaScript-number rewrite supplies proof. + +All eight Source17 statuses are zero. Written Source and objects are deleted +before launch; checked-program Weak and input-Arc roots are released. Each child +is reaped and its executable/PDB removed. Normal-launcher custody, reentry, +acquisition and history telemetry remains null and unmeasured. Per-kind ordinary +storage and private artifact-retainer counts beyond the stated root releases +remain open, as do physical lifetime and full_acceptance. + +Exact accepted receipt paths are relative to target/native/: + +| Receipt | SHA-256 | +| --- | --- | +| native-bounded-ingress-execution-v3/execution-acceptance.json | 56c746359c5fcd7ab73399ed98f64de5fe80d048d94f2d4a33b0706d09c773fb | +| native-bounded-ingress-execution-v3/native-evidence-catalogue.json | 4347e7b4a69a43b43466954538f07dad69a99c83aee5a0d127eabb97d231ae39 | +| native-bounded-source17-execution-v3/execution-acceptance.json | a59a3d4cadf23a05d30b7ecceb034f501dd40d02f404570f33309d787192087b | +| native-bounded-source17-execution-v3/native-evidence/summary.json | 271caf5fbd93f9fd4733185188ad270b344f2dcebba0f126ed93df984ffdb5e6 | + +The Root rollups under +private-observation-v2-root-native-runs-v3/ingress/accepted-observations.json +(c1d7a8a8d103c23b7d239bbf833d98d37b22bd04ce043c9baf86c02579291dee) +and source17/accepted-observations.json +(510b1fced511bd5817f3df2e9cfd2af04edf3c223520ab528453524f04feaf93) +retain the authenticated categories and original report hashes. + +The accepted v4 runner configures a 15-second helper-startup allowance, 5-second +CIM operation timeout, 30-second single cleanup deadline and 3,600-second active +deadline. Two earlier prelaunch refusals remain preserved: missing physical-tool +PATH and insufficient five-second helper startup. Neither receives Source +execution credit. GitHub CI remains disabled. diff --git a/docs/completed/native_codegen/native_resource_reflected_fields.md b/docs/completed/native_codegen/native_resource_reflected_fields.md new file mode 100644 index 00000000..bf79bc30 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_reflected_fields.md @@ -0,0 +1,379 @@ +# Checked reflected-field execution and native ordinal transport + +The checked reference interpreter now enters the exact original body selected +by a direct `for field in type.fields[T]()` loop and its +`comptime type Element = field.type_info` binding. This repairs existing checked +Source execution; it adds no language syntax, provider or runtime capability. +The checked Resource lowering path now retains a direct nominal-struct field +ordinal bridge through HIR and MIR. Four focused Source cases pass 16 linked +native executions and 24 entry attempts; the unchanged 191-case corpus also +passes all 382 Source-deleted Debug/Release executions. Original Source06 also +passes 18 exceptional same-grant executables / 36 entry attempts. Derived +Sources7/8/9 separately pass 42 native executables / 62 entries. Dedicated +Sources10/11 pass 48 native executables / 72 entries, recorded separately below. + +## Retained selection proof + +A private prepared loop retains its exact checked program, original For/body, +parent attempt, resolver binder definition, concrete owner and ordered field +snapshot. Each item carries that proof and its ordinal. Before entering the +selected scope, execution validates the actual item against the retained full +field metadata, rejoins the original binding and lexical path, and selects the +checker-owned `ReflectedIteration(ordinal)` body with its exact TypeId and +source-visible reflection. Names, equal canonical types and lookalike metadata +cannot identify an executable body. Preparation does not change the cursor. + +The checked `type.fields` intrinsic reads retained owner metadata. An ambient +reflection table cannot supply the proof or redirect the selected body. Equal +`int64` fields retain distinct ordinals; an alias field additionally retains +its source-visible name and kind. Struct, bitfield and concrete generic owners +use the same boundary. Existing shape probes for primitives, enums and opaque +resources return empty field metadata without enabling a provider. + +Named and recursive calls isolate their active item selectors and restore the +caller's selectors and aliases. Scope entry retires on ordinary completion, +Return, Break, Continue, Handle Default and unwinding. Entry cleanup restores +its original full context even when a provider or finalizer panics; the existing +cleanup-precedence rule remains intact. Closed pure required evaluation uses +the same checked selection without receiving a runtime provider or grant. + +## Genuine acceptance + +The reference checkpoint passed all 541 `jett_comptime` tests. The candidate's 3,664 frozen inputs and +recorded parent HEAD remain unchanged at that gate, as recorded in +`target/native-resource-reflected-field-reference-evidence/after-v5.json` +(receipt SHA-256 +`5c3104bee1dd58acb19c861f17dd61c39aacbd879ad59856ea80dd3b507b7966`). +This includes 19 added tests for original-node/parent/program proof boundaries, +corrupt ordinals and metadata, distinct equal-type fields, aliases, genuine +same-body recursion, required evaluation, selected control signals, and +heterogeneous/bitfield/concrete-generic metadata. + +The strict Resource runner executes unchanged Source06 plus three explicit +control-flow adaptations in both checking profiles. Its 74 entry attempts +cover nine original scripts, same-provider/grant reentry after every script, +and ten derived script controls. Exact provider/finalizer panic text and +cleanup-wins outcomes, ordered events, zero owners/registry, empty debug events +and full entry-context restoration are checked before interpreter teardown. +The original Source06 bytes remain exact; derived Break, Continue and primitive +Result Default bodies are named separately. Reference observations do not +independently count every native loan or expose remaining script length. + +Reproduce the crate gate with: + +```text +cargo test --offline --locked -p jett_comptime +``` + +## Native compiler ordinal bridge + +The new native producer is the direct `for field in type.fields[T]()` loop +over a nominal struct at the root of an exact non-generic, noncapturing function +in an authenticated `CheckedResourceProgram`, with a direct +`comptime type Element = field.type_info` binding in that loop. It +retains every checker-selected `ReflectedIteration(ordinal)` body, including +fields with equal canonical types and equal source-visible type reflection. +The ordinary `lower_program` entry retains its existing equal-type/reflection +coalescing. Checking specializes type, name, kind and primitive facts, while +`field.index` and `field.name` branches inspect the current runtime field binder. +Existing native `type_construction_enum` and `comptime_reflected_callbacks` +fixtures already distinguish same-type fields. A separate ordinary positive +control passes 30 Source-deleted native executions without a production change. +This covers five Source forms across nominal/generic fields, aliases, bitfields, +variant/state fields, type arguments, nested fields and getter pipelines. It +establishes parity for those controls; no ordinary coalescing defect is established. + +The immutable `ResourceSourceArchive` captures the exact original checked +program, declaration, For and binding objects, resolver identities, concrete +owner TypeId and source-aware owner reflection, ordered complete field table, +checked body facts, HIR field headers and original lexical paths. Only the +successful checked constructor can create this archive membership. Public HIR +changes, source spans, field names, an equal TypeId or lookalike metadata cannot +replace the original selection proof. + +MIR derives an ordinal guard from that archive: the original immutable field +binder's `TypeField.index` must equal the selected ordinal. Each guard leads to +a one-arm `ReflectedTypeDispatch` which checks the source-aware TypeInfo identity +before entering that ordinal's exact body. Two `int64` fields can therefore +select different field-index- or field-name-dependent bodies. Each native +TypeInfo dispatch retains the existing unique-identity rule. Metadata stays +ordinary storage and grants no executable authority by itself. + +Each arm carries its original lexical path. Nested handler lookup remains +inside that arm's archived subtree, preventing equal source spans in distinct +specializations from redirecting a Scope. Existing borrowed-projection and +lexical-retirement seals retain distinct local/site membership, child-before- +parent loan endings and Return cleanup rules. Private MIR records seal each +guard, dispatch, body target, refusing target and current field header against +both the original archive and the constructor graph. Canonical sequence edits, +block deletion and local remaps transport only current sites and headers; +original Source paths stay fixed. Reordered equal-type ordinals, redirected +guards or body targets, changed metadata, stale paths and edited original HIR +fail validation before provider or runtime effects. + +An ordinary helper inside the same checked Resource program may retain its own +exact field-loop proof. Its MIR witness supplies only that proof: runtime +classification stays ordinary, with no Resource execution plan, hidden Scope +parameter, provider permission or custody authority. Existing ordinary sequence +preparation still handles its map, string, view and uninhabited loops through +the private finite `SequenceEdit` validator. It preserves function identities +and original headers, forbids added custody, and refreshes only the current +graph and independent body seal. Resource execution functions retain their +separate reconstructed consuming-list transition. + +No Source rule, runtime leaf, ABI or wire-v2 record changes in this bridge. + +## Affected compiler phase gate + +The integrated candidate passes 1,202 checks: 542 comptime, 203 HIR, 286 MIR, +107 Cranelift codegen, 63 object and one CFG check. The ten result groups report +zero failed or ignored tests. Focused controls cover equal-type ordinal/body +selection, field-index/name-dependent bodies, altered original HIR and metadata, +reordered guards, redirected dispatch/body targets, stale Scope paths, proof-only +helpers, and their retained ordinary sequence preparation. The canonical view +control is `for observed in view values:`; the uninhabited control uses the +inferred empty `for impossible in list():` form. + +`target/native-resource-reflected-field-native-bridge-evidence/after-v4.json` +records actual session 4116, terminal `1335da`, exit zero and all 3,679 frozen +inputs unchanged at parent HEAD `f9fa84a6f05b2886a83e09fad4ae3b6dcb22c74a`. +The receipt explicitly records `native_execution_accepted: false`. These +phase checks do not prove linked native execution, matched runtime archives, +Source absence at launch, script exhaustion or zero native obligations. + +## Native execution acceptance + +The four focused cases are the byte-preserved original Source06, distinct +field-index bodies, distinct field-name bodies and a proof-only ordinary helper. +Both checking profiles pass eight single-entry executables and eight clean +two-entry executables: **16 Source-deleted native executions / 24 entry attempts**. +The clean two-entry cases retain the same Session, provider and grant. Each entry +has the exact outcome, ordered events and zero ordinary/Resource obligations +before teardown. The combined script is exhausted before one Session destruction. +The focused receipt is +`target/native-resource-reflected-field-native-bridge-evidence/native-acceptance-v5.json` +(SHA-256 `0b05489e3b97201ba0aee4642c2f157e81d5f9684e818f347eb403f775cda10a`), actual session 29429, +terminal `9cfc8d`, exit zero. + +The unchanged predecessor corpus separately passes **191 cases / 382 +Source-deleted native executions** in Debug/Release in 1,273.64 seconds. All +strict reports pass. The full receipt is +`target/native-resource-reflected-field-native-bridge-evidence/full-native-acceptance-v5.json` +(SHA-256 `31d6b59bebc1e11c18b9dfb6102b5f5486f28dfd69d1439fce15498aa4050db9`), actual session 49641, +terminal `1d2132`, exit zero. Both execution gates keep all **3,686 frozen inputs** +and **nine runtime files** unchanged, including the matched measured MSVC +Debug/Release archives and their build/CRT/native-static-libs receipts. The frozen +test executable SHA-256 is +`76fe2a90106ee8ede00eb86758cefbaa4cefb42d6b818e90a06337b029aba39d`. +The phase receipt above remains a distinct earlier compiler gate. + +## Ordinary reflection positive control + +The independent ordinary control passes three Source families in five forms, +each with main, verify and property suite executables in both checking profiles: +**30 Source-deleted native executions**. Reference stdout and empty debug-event +oracles, explicit-comptime stdout and native suite outcomes all agree. Its receipt +`target/native-ordinary-reflected-ordinal-control-evidence/after-v1.json` +(SHA-256 `4a282285082fc411b4d8714a890b13c7146d81970533522469eb5848d4a0177e`) records actual session 21527, +terminal `6befe6`, exit zero and all 3,703 frozen inputs unchanged. It explicitly +records `production_changed: false` and no established ordinary equal-type +coalescing defect. These ordinary controls do not establish Resource transport +for those broader reflection families. + +## Original reflected failure single entries + +The byte-identical 929-byte Source06 now passes all eight original exceptional +scripts in both checking profiles: 16 reference observations, 16 emitted objects +and **16 Source-deleted native executions**. The cases cover handled construction +and borrow failures, provider panic, explicit close cleanup failure and both +provider/domain failure with cleanup failure. Exact completion channels, messages, +ordered events, script exhaustion, zero ordinary/Resource obligations before +teardown and one Session destruction agree with the strict native oracles. + +`target/native-reflected-exceptional-native-evidence/after-v1.json` +(SHA-256 `38662e59e4dfebfbbde4236ee0a8d6b2cc647b6a9554ea8afb9f2b1119bf3085`) +records actual session 2180, terminal `aa0907`, all three commands exiting zero, +and all 3,720 frozen inputs plus nine measured runtime files unchanged at +`2be038c77f084395e8638beaaf2617fe86b49730`. Only additive test leaves are involved; +compiler/runtime code, four READY controls and the 191-case catalog are unchanged. +These single entries do not establish exceptional same-grant reentry. + +## Original reflected exceptional same-grant reentry + +All nine original Source06 scripts now execute followed by exact READY on the +same Session, provider and paired Network grant in both checking profiles: +**18 Source-deleted native executables / 36 entry attempts**. The shared checked +reference matrix separately passes 36 real calls. This includes all three +cleanup-failure cases as well as the original handled Return and provider-panic +paths. The exact 929-byte Source06 and original scripts remain unchanged. + +The private launcher first completes the actual attempt and copies its original +body status, cleanup status, selected channel, exact ordinary/Resource message +bytes, event boundary and live counts. Every entry has zero owners, loans, frames, +registry entries, owner/loan/frame handles and provisional returns, plus empty +ordinary value storage, before teardown. After these checks, completed Resource +cleanup failure permits the declared next entry when current ordinary status is +zero. Existing `entry_begin` independently validates the installed entry, attempt +budget, ordinary first-error/cleanup state and absence of active or residual custody. +It creates fresh attempt/root identities on the retained Session/provider/grant. +No failure latch is cleared and no reset or failure-taking API is invoked. + +Each original completion and diagnostic remains immutable in the report. Events +retain one continuous sequence with exact per-entry boundaries; the combined +script is exhausted and the Session is destroyed once. The first nonzero process +exit remains 71 or 73 after clean READY. Handled-domain Return with failed cleanup +retains original body 0, cleanup 255, Cleanup channel and exit 71. These are exact +observed exceptional outcomes; the second entry does not replace the first record. + +Fresh independently measured MSVC Debug/Release archives were built in actual +session 23542, terminal `e09148`, exit zero. Their receipt is +`target/native-reflected-exceptional-reentry-archives/measurements-01/receipt.json` +with compiled pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +The private launcher continuation changed; runtime ABI 1, Source wire 2 and +script/report protocol 1 remain unchanged. The archive build alone is separate +from the subsequent actual Source execution. + +`target/native-reflected-exceptional-reentry-evidence/after-v3.json` +(SHA-256 `f3dc34c6a13079c6e0be04716d37af9a69580a7b99b859a4ebd5301376e3aa8b`) +records actual session 30992, terminal `22c716`, both Rust harnesses exiting zero, +and all **3,722 frozen inputs / nine runtime files unchanged** in the tested +candidate based on parent `b5c55ac7ed9ec559fbae6616de74fff507bff042`. The exact +manifest filters each ran one real test. Earlier session 52970 selected zero +tests; preserved `after-v2.json` +explicitly records zero executions and nonacceptance. The earlier single-entry, +four READY and 191/382 receipts remain separate evidence with their original +archive pins. This gate establishes the original exceptional same-grant matrix; +fresh-archive predecessor, coherent-workspace and exact-revision CI gates remain +separate obligations. + +## Derived reflected control native acceptance + +The unchanged derived Sources7/8/9 now pass their historical ten-script matrix +in both checking profiles. The executed signals are **three handled Returns, +two Break paths, two Continue paths and three scalar Handle Default paths**. +The first-borrow failure in Source7 and first/later-borrow failures in Source8 +return from their handlers before Break or Continue; they are counted as Return. +A separate eleventh Source9 READY baseline was added and verified, without +changing Source bytes or the original ten scripts. + +Break preserves the total17 guard and retires the selected scoped-type body +before the consuming close. Continue preserves both field visits and total34, +and skips the statement after its scoped-type body. Scalar Default substitutes +17 for the failed primitive borrow, resumes the selected body and preserves both +field visits and total34. These exact controls exercise selected body retirement +with the outer optional owner retained until its existing close or handled Return. +They do not +establish arbitrary Resource-valued Default or other body/Default lifetime +compositions. + +The five actual gates pass **22 single-entry reference calls, 40 same-grant +reference calls and 22 emitted objects**, followed by **22 Source-deleted +single-entry executables / 22 entries** and **20 same-grant reentry executables / +40 entries**: **42 native executables / 62 entries** and **62 reference calls**. +Every historical first script is followed by its own unchanged Source's READY +on the same Session/provider/paired Network grant; the separately verified +Source9 READY supplies its two-borrow tail. Exact completion tuples, diagnostic +bytes, continuous events and per-entry boundaries, zero ordinary/all eight +Resource obligations before teardown, combined-script exhaustion and one clean +Session destruction all pass. The three explicit-close cleanup cases retain +body 255 / cleanup 255 / Cleanup channel / exit 71 and exact +`native Resource cleanup failed`; their original completion and first nonzero +process exit remain unchanged after clean READY. Existing `entry_begin` checks +remain authoritative; no reset, failure-latch clearing or provider/grant +reinstallation is introduced. + +`target/native-reflected-derived-control-native-evidence/after-v1.json` +(SHA-256 `f9cfe12b7a2013fd63ac0e73672a1c4fbee617e01a366e5747a1ab17a1e6da24`) +records actual session **18249**, terminal **e1bcf6**, all five commands exiting +zero with **one real test each**, and all **3,725 frozen inputs / nine runtime +files unchanged** in the tested candidate based on parent `f4c63a0f`. It uses +the unchanged measured MSVC Debug/Release receipt pin +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +The earlier original Source06 nine-script, four READY, ordinary-control and +191/382 receipts remain separate evidence. No GNU carrier/fresh-v3 native +execution acceptance follows from this local MSVC gate. + +## Dedicated reflected body and owned Resource Return acceptance + +Sources10/11 are separately named additive controls; the original Sources7/8/9, +historical ten scripts and scalar-Default READY baseline remain byte-identical. +Twelve new shared cases execute genuine first- and second-field successful +Returns, handled first/later borrow failures and four cleanup combinations in +both checking profiles. Each Source retains three equal-int64 reflected fields, +its statements after the Return condition and its statements after the loop. +Finite provider scripts and terminal sentinels verify that Return skips those +statements and all later fields. + +Source10 returns `nothing` from the selected scoped-type body while an outer +optional Resource owner remains live. Its exact borrowed projection ends and the +outer owner retires during Scope completion. Both cleanup cases retain original +body **0**, cleanup **255**, Cleanup channel, empty ordinary message, exact +`native Resource cleanup failed` and first process exit **71**. This differs from +the historical derived explicit-close cases' body255/cleanup255. + +Source11's helper returns an actual distinct, unborrowed Resource owner892 while +aliasing outer owner891. Successful body retirement and outer cleanup precede +publication; the caller's exact Borrow892(29) and consuming close establish the +returned owner's identity and custody. Its ordered events place Finalized891 +before caller Borrowed892. Both helper cleanup cases suppress publication and +caller borrowing, retire891 then the provisional892, and retain root body **255**, +cleanup **255**, Cleanup channel and exit **71**. The helper Source follows a +normal Return path before cleanup failure propagates. The directly observed +tuple belongs to the entry root; the report does not separately read a nested +callee completion. Clean success and +handled failures remain body0/cleanup0 with empty messages and exit0. + +Actual session **64018**, terminal **0b2a26**, passes two one-test reference groups +and one final object group: **24 single-entry reference calls + 48 same-grant +reference calls = 72**, and **24 objects**. Actual session **47358**, terminal +**dcfb82**, passes both one-test linked groups: **24 Source-deleted single-entry +executables / 24 entries** plus **24 same-grant reentry executables / 48 entries**, +for **48 native executables / 72 entries**. Every first script is followed by its +same byte-identical Source's READY on the same Session/provider/paired Network +grant. Increasing nonzero attempt identities, immutable first completion/messages +and first nonzero exit, continuous events/entry boundaries, all eight zero native +Resource counts and empty ordinary storage before teardown, combined-script +exhaustion and one clean destruction all pass. No reset, latch clearing or grant +replacement is introduced. + +`target/native-reflected-body-return-native-evidence/after-v1.json` records the +reference/object gate. The linked receipt is +`target/native-reflected-body-return-native-evidence/after-v2.json` +(SHA-256 `9250a5d37375cd66ba212e1cfcaa489ef0b55571dba0efa72dc59a6d306baa18`). +Both gates retain all **3,730 frozen inputs / nine measured v2 runtime files** +unchanged in the tested candidate based on parent +`91105876e5dbbbe9bbaa1d19bc2408ec9a274255`. The unchanged measured MSVC +Debug/Release receipt pin is +`96721717326f7d6f96dd2bb98dc1d6e919f7119235e33e103e5ac89bd6f29d0a`. +Earlier original/derived/191-case receipts retain their counts and archive pins. +This is bounded local MSVC Return acceptance; it adds no fresh-v3, GNU carrier, +coherent-workspace or exact-revision CI acceptance. + +## Remaining native and reflection work + +The producer breadth is deliberately limited to the exact direct nominal-struct +case in the checked Resource program. Generic and alias owners, bitfield field +families, reflected variants, machines, `TypeInfo.args`, nested reflected +producers and broader function compositions remain compiler implementation and +Source-evidence obligations in the checked Resource path. The ordinary controls +above establish their own bounded parity and leave ordinary `lower_program` +unchanged. Original Source06 exceptional same-grant reentry is accepted above. +The derived historical control matrix and dedicated body/owned Resource Return +controls are accepted above. Same-owner Return while its resident lease remains +live, broader body/Default lifetimes and broad aggregate Resource custody remain +separate obligations. Return operands still precede certified lexical retirement; +the accepted owned-return control uses a distinct unborrowed survivor. This +boundary adds no +compiler-policy ban on existing Source semantics. + +The full native-codegen goal remains active at the rough 87% planning estimate. +The fixed 207-fixture / 182-run-pass inventory denominators do not change. + +The ordinary lazy-callback successor also preserves original Verify/Property +capture owners, immutable CallbackHolder observations and canonical generated +Property wrappers. Its bounded ordinary-callback acceptance is tracked in +[the callback coverage plan](native_codegen_parity_plan.md#generated-callback-suite-proof-continuity) +and [the audit](native_acceptance_audit.md#generated-reflected-callback-suites-bounded-msvc-acceptance). +That work adds no Resource ordinal, Scope, provider or lifetime acceptance to +this reflected Resource checkpoint. diff --git a/docs/completed/native_codegen/native_resource_same_local_reseating.md b/docs/completed/native_codegen/native_resource_same_local_reseating.md new file mode 100644 index 00000000..18bfd88f --- /dev/null +++ b/docs/completed/native_codegen/native_resource_same_local_reseating.md @@ -0,0 +1,80 @@ +# Native same-local Resource sum reseating + +This selected compiler-only extension admits `token = token` for the exact same +mutable owned Resource local. Existing occupied plain Resource reseating stays +valid. Direct `optional[R]` and `result[R, E]` shells are selected for every +checked Empty/Occupied/Conditional occupancy, including live `none` and `fail` +shells. A result failure retains its exact ordinary error companion. + +Admission must preserve the original Source identity, complete nominal Resource +and failure types, local slot, containing lexical scope, current activation and +existing loan checks. Reseating itself creates no runtime replacement, transfer, +finalizer or implicit clone. Scope cleanup still retires the live owner and any +ordinary companion once. Generic, aggregate and projected carriers are outside +this selected whitelist. No Runtime, public ABI or report-protocol change is +selected. Source eligibility and capability restrictions remain unchanged. + +## Original witnesses and recorded baseline + +| Original fixture | Bytes | SHA-256 | +| --- | ---: | --- | +| direct_optional_self_reseat.jett (`none`) | 272 | 73482d983e29bffc61e2f6829c26f8f4514e10f2560bd0481cebe0f887271c5a | +| direct_result_self_reseat.jett (`fail("kept")`) | 282 | 4d4357096c172d7a4ac4cebe53dd2451b0f97ee600d6fb5525714f07e22756ae | + +Both fixtures remain unchanged under crates/jett_mir/src/resource_ownership/fixtures/. +One existing plain-Resource control passes. One reference control passes both +original Sources across its two profiles. Those are one Rust control each, not +new native execution counts. The split MIR baseline fails two tests and the +object-emission baseline fails two tests at the exact plain-only reseat refusal: +`self-rebind needs the exact occupied plain Local reseat`. + +The initial combined MIR failure reaches only the optional witness in Debug. +The later split tests establish both original witnesses at their first Debug +refusal; none of these red records grants a Release-attempt or emitted-object +pass. Log paths below are relative to target/native/: + +| Baseline log | SHA-256 | +| --- | --- | +| resource-sum-self-reseat-plain-baseline-v1.log | dacea5d0cc069102479595389dabe5f40536ed3eeb01f5866e8b3a1673f3ebf9 | +| resource-sum-self-reseat-reference-baseline-v1.log | a794c672934dccea06b97b964628cae1b7a2eeff61e6fd3ec5bea612215d31da | +| resource-sum-self-reseat-mir-red-v1.log | 91631222a54076acc220c7bbec2bf0aea3b9eb72d1cf2ee5070821ba4888d256 | +| resource-sum-self-reseat-mir-red-v2.log | ffc72cb8a00cdcaf7e7d1caf2e7fd6d311b424de0e2191b959692cb3d2e789c0 | +| resource-sum-self-reseat-object-red-v1.log | efddf873675880989451780e4a90f0a6671949bb00dfc6f5dc5398c7038c9010 | + +## Core compiler checkpoint + +The actual MIR flow now admits the selected optional/result occupancies through +its existing exact same-Local branch. Both unchanged original MIR controls and +both object-emission controls pass, with each helper completing Debug and +Release. The MIR controls retain one exact SelfRebind and no Replace, Transfer +or Drop at that assignment site. This is object-emission evidence with **zero +linked native execution credit**. It does not by itself establish occupied or +conditional execution, observable failure-companion preservation or loan refusal. + +Broad local regression passes **1,291 distinct checks**: 142 Codegen library, +66 Codegen integration, 694 Comptime library, 388 MIR library and one MIR +integration. The focused green checks are included, not added again. Current +logs/receipt pins are: + +- MIR green: f18b1257baed6f55bfebcd745e26adc56ec4e11d9915e91faa2f9de28d6dabe6. +- Object green: 3339d1dd9c25ab27ae5482ca68431a92c4edf0a96508e4c4b1dfab9156b0f21d. +- resource-sum-self-reseat-core-regression-v1.log: + 26ae9be8603af75a518c9aaad9eb212748a017e71ea58350feec633729400add. +- resource-sum-self-reseat-mir-repair-v1/local-verification.json: + 81fb340662dccb6c8d2d78d610e7d8cd4c7b69832f878656c3cbf9b695d45115. + +## Acceptance still required + +Linked native execution, occupied/conditional positive cases, unknown/forged +same-owner refusals, observable companion preservation and live-loan refusal +evidence remain **pending**. Their separate follow-up controls are outside this +core checkpoint. Generic, aggregate and projected carrier admission remains +conservative. No current fresh compiler gate or full ordinary corpus is accepted +for these changed compiler inputs. Earlier replacement/suite receipts retain +their own scope. Existing native inventory and workspace validation remain pending for these +compiler inputs. Additional lifetime instrumentation and further platform +certification are separate follow-up work and do not expand the frozen +implementation coverage goal. Historical broader acceptance flags remain false; +CI stays disabled. + +See [the design](../../design.md) and [architecture](../../architecture.md). diff --git a/docs/completed/native_codegen/native_resource_suites.md b/docs/completed/native_codegen/native_resource_suites.md new file mode 100644 index 00000000..b6076349 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_suites.md @@ -0,0 +1,301 @@ +# Native pure suites over absent Resource aggregates + +Status: local compiler gates, the whole Source17 native available-observation +gate, current-view regressions and the original Resource lifecycle matrix pass +as recorded below. Complete provider/context instrumentation and full +Suite/native-goal acceptance remain unproven. + +The unchanged Source17 fixture is +`crates/jett_comptime/src/resource_execution/fixtures/17_absent_aggregate_shapes.jett`. +It declares twelve exported constructors with Resource-bearing aggregate types, +an unused refined view contract, explicit required expressions, one verify body, +and one property with 100 generated int64 inputs. Existing checking/reference +semantics admit this pure Source. The previous native suite factories rejected +any nonempty ResourceManifest. The private transition addresses that compiler +proof/transport gap; no new +Source syntax, purity permission, Resource layout exclusion, or provider behavior +is selected here. + +## Exact private transition + +Only the existing public checked suite factories derive the wrapper from the +retained immutable checked entry file. A private constructor first authenticates +the complete current function table, unchanged manifest/equality/type meanings, +original checked-program identity, every existing required-value and Graph or +reflected/temporary proof. It appends exactly its locally constructed canonical +wrapper. A retained transition records that complete before program, canonical +wrapper, purpose, entry file, exact checked session, and complete current table. +The original archive.functions() table stays immutable; it is never recaptured +or replaced by the generated wrapper. + +Resource required materialization retains the opaque successful required-cache +identity. Property construction joins CheckedPropertyCases to that same checked +session and cache. Preserve every original property's checked trial count and +order; Source17 uses the existing default of 100 generated trials. This protocol +adds no fixed-count Source rule. Replay continues to start from the private pre-suite program, preserving +its original trial identities and checked changed-input validation. + +The transition issues one opaque suite-call receipt per canonical wrapper +invocation, with a one-based preorder expression ordinal across the whole +original wrapper. This includes PropertyCaseContext and argument expressions; +identical argument values in different trials do not collapse their identity. +Each receipt retains the whole suite origin and exact invocation tuple. A public +GeneratedOperation::NativeSuite packet, equal spelling, copied current function, +or caller-owned case vector cannot mint the receipt. Later canonical conversion +may renew a complete current view only after its complete input is authenticated. +All peer Graph, reflected, mutable-sum, actor and temporary transitions must first +validate the same unchanged-before archive before their renewed views are joined. + +## Native activation + +The Resource execution closure accepts an exact sealed suite edge in addition to +existing Source-certified edges. This is a distinct private native-suite bridge, +not a fabricated SourceCallOwnership and not a general Generated exception. +MIR retains exact original-wrapper statement/expression correspondence into its +current CFG. A separate InvokeNativeSuite operation retains that receipt, exact +callee/formals, argument order and ordinary operands. The first connected slice +has only ordinary generated arguments and a Nothing result, matching Source17. +Broader Resource-valued generated inputs remain a required transport follow-on; +no checked Source validity rule is narrowed. + +The emitter validates and activates the same Resource entry/call Scope protocol +through that separate role. Pure suite wrappers have zero Network parameters; +zero provider acquisition must also be observed through actual runtime counters. +Potential Resource paths stay +in the full original type graph even when selected values contain no live leaves. +All transitive helper calls keep their original Source certificates, typed carrier +plans and cleanup. No hook, provider grant or occupied Resource custody is minted. + +## Contextual extraction inside checked bodies + +A direct helper call can belong to the retained Resource execution family even +when every actual and the result have ordinary types. Endpoint types alone do +not prove that the call may stay embedded inside an ordinary expression. The +Builder now includes such exact calls in its contextual extraction predicate: +the retained execution closure must recognize the original direct call, the +Source packet must retain its original actuals, and all endpoints admitted by +this added ordinary path must be free of pending Resource transport. Matching +names, a nonempty manifest or an ordinary return type cannot supply this proof. + +Existing eager operand and ordered-container lowering propagates that predicate +through its established traversal. It stages earlier values before lowering a +later nested call, preserves the checked argument/field permutation and invokes +the helper at its original occurrence. Lists, map entry pairs, record fields, +interpolations and ordinary call/intrinsic operands reuse their existing +ordinary-staging eligibility checks; this is not a blanket promise for every +aggregate shape. Ordinary owning staging still refuses Resource-bearing values +instead of acquiring custody or cloning authority. + +Boolean `&&` and `||` retain short-circuit behavior. If the right operand needs +extraction, its call region is built in the selected right-hand CFG branch; it +is not moved ahead of the left test or into the bypass branch. The direct-call +region authenticates the complete unique original tuple before extraction, +then retains Begin/ordered Stage/Invoke/End associations, generated headers and +current sites. A failed region construction restores the Builder checkpoint. +Existing lexical-exit and cleanup proofs remain required. + +This transport concerns calls inside original checked bodies. The canonical +generated suite wrapper still has its separate exact flat context/call protocol +and private suite-call receipts. It gains no arbitrary nonflat wrapper, Source +certificate or callable authority from this extraction. Resource-bearing actuals +and results keep their separate carrier transport obligations. Indirect and +resource-hook invocation timing, refinement-handler exit proofs and wider +aggregate/constructor contexts remain pending without new Source prohibitions. + +## Required candidate controls + +Use the byte-exact original Source17, including all exports, unused contract, +required expressions, verify and property100. Verify and Property native gates +must execute after Source removal against both measured Runtime profiles and +observe zero provider acquisitions, exact expected results, zero live custody, +normal cleanup and same-context reentry. Factory and MIR/codegen negative controls +must reject foreign checked/cache/case identities; omitted/reordered/extra trials; +mutated original or generated bodies/headers; manufactured Generated wrappers; +missing/reused/foreign receipts; duplicate-expression trial swaps; erased archive +or manifest; and detached current CFG sites. The ordinary and Graph/reflected +suite corpus must remain accepted. A temporary-only manifest-empty archive with +an ordinary temporary producer plus independent verify/property body must append +without Graph, preserve producer markers, and pass canonical conversion/validation. + +Expected HIR Property labels and an empty provider event list are not complete +runtime measurements. Total acquisition attempts, successful runtime Property +set/call/clear history and any unobserved live storage/artifact kinds still need +actual instrumentation; no zero-filled or inferred fields can close them. + +These are acceptance obligations, not results. No CI should run; the repository +workflow remains disabled at the user's instruction. Root owns all local builds +and measured artifact execution under the shared disk budget. + +## Candidate composition checkpoint + +The packet retains independent Source17 and temporary-only suite controls, plus +the fresh required reflected callable / temporary producer / generated suite +control. Its main-level Source assertion is repaired to a typed int64 call/coarsen +binding, retaining the same genuine required occurrence and invocation; Rust +ownership, sealing and exact table assertions are unchanged. + +Canonical completion now uses the prerequisite single actual coercer receipt. +Actor, mutable-sum and temporary proof refreshers replay the recorded complete +output against the exact preimage and same borrowed TypeInterner. The Suite +successor preserves that transaction and all six private archive Arc identities, +including native_suites and required_cache even without original data. Its +post-conversion callable sealing still replays exact bound-to-sealed pairs. + +Root reports the separate canonical genuine-callable/temporary positive passing +in Debug and Release checking modes (one test; log +canonical-conversion-compound-positive-v2.log SHA256 +fa986958329d859de65978f79ec9ea14a815a77c66fa0533f890b1023f01abca). +That prerequisite alone does not establish Suite acceptance. The current local +compiler gates below cover the Suite implementation and cross-phase +regressions. The Source17 native checkpoint below supplies available measured +outcomes; complete counter/history evidence remains required. + +## Historical suite checkpoint and measurement limits + +This section retains the earlier protocol1 receipts and their exact compiler, +Runtime and observation limits. They are historical acceptance; the protocol2 +successor below has its own fresh bounded native acceptance. + +Those successor gates pass **1,867 compiler-library tests**: Codegen 135, +Comptime 683, HIR 286, MIR 384 and Typecheck 379. The full frontend/reference +fixture suite passes **603 tests**, and the unchanged Windows CLI capture suite +passes **nine tests**. Focused compiler controls are included in the library +totals. These current results do not turn earlier failed or intermediate runs +into accepted gates. Logs under `target/native/`: + +- `native-suite-core-v3.log`, SHA-256 + `0fdbab0eb7bdad51b212d44b100a17a942d98088b323267bf0d8dcf51f8b860e`. +- `native-suite-fixtures-v1.log`, SHA-256 + `015e7efada19cde3712e76e1b3b14b16bd95a7887c77d953806940c3f61e3823`. +- `native-suite-cli-v1.log`, SHA-256 + `92e961e08879c451aa1b0c41e18187460a00b66d511090cc1704f6ad10290a3e`. + +The whole unchanged Source17 canonical Verify and Property entries pass the +then-compiled native available-observation gate: **four objects and eight processes** +across paired Source/Runtime Debug and Release with both matched normal and +private launchers. Written project/support Source and objects are removed +before launch; the reaped executable and temporary PDB are retired afterward. +The four private processes each enter twice on the same retained context, for +**eight private attempts**. Every attempt records all eight live Resource counts +as zero before destruction, aggregate ordinary storage empty, clean body and +cleanup status, and empty messages. Each private process records one successful +context destruction and successful report writing. Normal launchers supply +status/stdout/stderr and artifact retirement, not custody/reentry measurements. +Exact checked-program and caller input-Arc Weak roots expire before native +launch; no individual opaque sampled-cache Weak observation is claimed. + +All eight observation files retain **full_acceptance=false**. The expected +checked 100-trial argument plan and Property context labels are preserved, +explicitly marked as expected-plan evidence, not a runtime history. The +unchanged private report records provider events rather than all attempted +acquisitions: rejections before event recording would not appear. All private +observations have empty recorded events, but total acquisition-attempt fields +remain null and their measured flag remains false. Successful runtime Property +set/call/clear history is also null. Normal-launcher custody/reentry, per-kind +ordinary storage and opaque artifact-retainer observations remain unavailable. +None is replaced by a zero-filled value, static HIR labels or absence of Network +parameters. Full acceptance stays open. + +The Source17 gate keeps all 1,067 compiler/Source inputs, 68 reused Runtime +artifacts and 182 original run-pass inputs guarded and unchanged. Its compile- +only receipt grants zero executions; the Source17 selector passes one test and +records the eight native processes separately. Receipts under target/native/: + +- native-suite-native-gate-v1/gate-acceptance.json, SHA-256 + 551f40f16b61c95b5394addf029df894fbee182e350ed137a703730b3fd6c81f. +- native-suite-source17-v1/execution-summary.json, SHA-256 + fa4fdf2f41eb35d7c153dba0c382d2aebe795ea4c3c10a6d1a5843c51c06fff4. +- native-suite-source17-v1/source17-observations/summary.json, SHA-256 + 292614e0b797ee01355cdee57aa0783b94488ce3df0461c702b01cbb89f269ad. + +The compiled and executed gate image is SHA-256 +b7c1391ce3548701be12487541e40552a3c2c2735b4799e4c111a39122dcc3be. +The same historical existing-view regression passes **24 tests and 48 paired +Source-deleted executions**, 24 Source Debug/Runtime Debug and 24 Source +Release/Runtime Release, with zero publication refusals or legacy Debug-only +runs. All 16 selection-log hashes and exact zero-failure counts pass. The same +1,067 compiler/Source and 68 Runtime guards, its 65 fixture pins and 182 original +run-pass inputs remain unchanged. Its receipt is +native-suite-native-views-v1/execution-summary.json, SHA-256 +383af6e155679d65f7c9dea7865093b052072edf4e9becc66c71747005a85b38. +These existing-view regressions do not supply missing Suite instrumentation or +new compound required-callable/Suite native acceptance. + +The original Resource lifecycle selector separately passes one Rust test covering +**191 cases and 382 Source-deleted executions**, 191 per matched Debug and +Release profile, across 113 distinct original Sources. Adjacent build reuse +produces **123 emitted/linked programs per profile**; the distinct Source count +is not the build count. Existing in-process reference, lifecycle and cleanup +assertions remain unchanged, and the fresh aggregate log records the terminal +counts. Per-case raw reports, binaries and objects are not retained. All 1,067 +compiler/Source inputs, 68 reused Runtime artifacts and 182 original run-pass +inputs pass their before/after guards. Its execution summary is +native-suite-original-lifecycle-v1/execution-summary.json, SHA-256 +afe8a9428828ed0f167cff88d730674d0700d57d76df3b65b35a20a290e95b91; +its OriginalLifecycle.log is SHA-256 +a41ceb2e3ec171a09f8532b32e1ebe43e68a58c475d64cd712fc2845347805bf. +This is the exact original Resource lifecycle matrix, not the full ordinary +native corpus. It adds no duplicate pipeline/control-flow selector credit and +does not close Source17 instrumentation or the full native-codegen goal. + +During the CLI batch the native cache was observed at **8.0950 GiB**, exceeding +the 8 GiB budget. Guarded retirement of literal old test-image and PDB paths +reduced it first to 7.9106 GiB and then **7.4105 GiB** before the fresh native +gate. All Runtime guards remained intact. The ceiling was not continuously +maintained. The two completed retirement receipts under +`native-codegen-recovery-2026-10-09/` are: + +- `native-suite-pre-native-retirement-v1/retirement-result.json`, SHA-256 + `4b47d614668c602021f1d6361a59ba3c82a1e0a6550149e46ade7cda03ffff8c`. +- `native-suite-native-headroom-retirement-v1/retirement-result.json`, SHA-256 + `85326e1653a1d2d0c92e608e92cd201d4321838dde0a92410e200b5fa6dad871`. + +## Private observation v2 successor + +The 16-path observation implementation passes **407 focused local checks**: +389 Runtime library tests, 12 report decoder controls, one Source17 history-oracle +control and five host decoder controls. Six fresh MSVC archives are measured: +normal Runtime and Launcher plus private Runtime, each in Debug and Release. +The normal receipt is 4e41eca939e70f4c1ea162639b3f714510726a118a200d38bb89bddc6d575e8c; +the private receipt is d19cc38b1d38a6f84783ad0fd1dc219cea722d88ce5e1db8ddba2b025d64dbe0. +Measurement grants zero Source executions. The Source17 harness now pins the +actual normal receipt; its unchanged original Source remains +c8e74848356a5b3d09ab6659318b1e1292718d34443f7f37222bafd6f3eed0ff. + +The fresh compiler gate is accepted at +private-observation-v2-gate-compile-v3/gate-acceptance.json, SHA-256 +3d05e4499c87c4d606ca515ca1ab2c7c9ece96331749bb564b62dfe017f78cc3. +Its compile-only acceptance grants zero Source executions. The fresh native +ingress and whole-Source17 protocol 2 gates now pass separately. Ingress records +32 processes: eight Source processes with 10 actual report-header entries, 22 +host ABI controls and two pure writer controls. Four linked artifact rows are +not executions; the Source processes use two distinct original Source hashes. + +Source17 records eight processes/four objects across paired Debug/Release and +normal/private launchers, with eight private entries. Each private Property +profile records two 100-trial entries and 6,404 cumulative history records; each +private Verify profile records four history records. All private incomplete +flags are zero, all five separate acquisition counters are measured zero and +same-context reentry passes. Normal telemetry remains null. These fresh results +close the corresponding v2 available-observation gaps; the protocol 1 nulls above +retain their historical meaning. Per-kind storage/retainers, normal custody/ +reentry, physical lifetime and full_acceptance remain open. The +[private observation note](native_resource_private_observations.md) records the +protocol boundary, exact evidence and outstanding physical measurements. + +## Actor and wider transport obligations + +Actor checkpoint 8827c57321e6e0d74cee6fd30dd21a7f11940d8c authenticates +bounded full-snapshot renewal from the sole actual callable sealer and a private +original-Lowerer source epoch. Its unchanged required-value and direct-getter +Sources pass four Source-deleted native processes across Debug and Release. +Those receipts precede the Runtime observation change and remain historical; +they supply no current-v2 regression or physical lifetime/reentry credit. See +the [initial actor sealing note](native_actor_callable_sealing.md). The valid +Packet/list temporary-owner composition and broader actor/checkpoint/Graph/ +Resource obligations remain open. This is separate from actorless Source17. + +Broader generated Resource/callback/refinement inputs, nonflat wrapper CFG shapes, +physical private artifact lifetime, full ordinary suite inventory and wider +Resource transport remain part of the full native-codegen goal. No Source ban or +weaker caller-owned proof is introduced to avoid them. diff --git a/docs/completed/native_codegen/native_resource_sum_replacement.md b/docs/completed/native_codegen/native_resource_sum_replacement.md new file mode 100644 index 00000000..478074d6 --- /dev/null +++ b/docs/completed/native_codegen/native_resource_sum_replacement.md @@ -0,0 +1,176 @@ +# Native direct Resource sum replacement + +The current implementation extends whole-binding assignment to distinct owned +optional[R] and result[R,E] shells with an exact checked Resource/failure shape. +The original 204-byte witness remains +23f997a76378f0e805805f7cc3a57b168e00863121bd3e54ab2b93f28366d4ec. +Design and architecture already record the selected contract; this note tracks +its bounded implementation and verification. + +The RHS is evaluated once and remains independently owned through preflight and +old-value cleanup. Successful cleanup precedes destination publication. RHS +failure preserves old when it has not consumed it; old cleanup failure retires +the staged RHS once and keeps established error priority. Live None/Fail shells +are owned values with real loans, and Fail retains its ordinary error companion. +Both-absent replacement can run in an otherwise eligible pure purpose; occupied +Resource operations retain their existing capability/purpose restrictions. + +Two related compiler repairs preserve this behavior. Typed failure construction +uses existing payload_bits packing and extraction uses typed unpack_payload; +primitive bits no longer become ordinary owning handles, while String/compound +ownership remains intact. Nested owned declarations retain exact original +Source scope/path/header/initializer and sealed current-site records. Exact +lexical exits drop live owners once, clear moved/dead slots and retain pending +Return ownership until transfer. General resealing grants no new owner facts. + +These changes reuse the existing Replace operation and wire contract. Production +ABI 1/layout 3, 58 production and 8 private Resource exports, private report +protocol 2 and receipt version 1 remain unchanged. No Source restriction, new +capability policy or hidden clone is introduced. Primitive-failure controls keep +their actual zero MIR owning-temporary bound. + +## Local checks and archive measurements + +The frozen local receipt records **3,038 distinct checks**: 409 Runtime tests, +1,976 compiler/phase checks, 603 frontend/reference fixtures and 50 CLI tests. +The receipt is resource-sum-replacement-root-verification-v1/local-verification.json, +SHA-256 325751dfe2bf1335c12114667a7a21017bdacb6a449d82abcc5e72991510405f. +It retains exact inputs and logs; focused subsets are not added again. + +Six fresh MSVC archives are measured: normal Runtime+Launcher Debug/Release and +private Runtime Debug/Release. The paired run completes with reaped children, +released lease, unchanged 80 Runtime inputs and genuine empty before/after +liveness arrays. Its maximum observed cache is 8,060,836,909 bytes within its +8 GiB limit. These measurements grant zero Source executions. Receipt paths +below are relative to target/native/: + +| Receipt | SHA-256 | +| --- | --- | +| resource-sum-replacement-normal-msvc-v1/receipt.json | be980ae18006737304ae69af4d254f78cad15c51fe8e6226c6a2b9ca9cf4b6b4 | +| resource-sum-replacement-private-msvc-v1/receipt.json | 454be0bf810ecc500e362fd99de37e02a909ca3abfc4d89c92afb06e8d1cfc50 | +| resource-sum-replacement-measurement-paired-v1-terminal.json | b725c561901d53bc2fd27b5ae6f5cf07265051be498efc6e4b7a7b1a283395a9 | + +## Native acceptance boundary + +The fresh compile-only gate is **accepted** with 1,111 compiler/Source inputs. +The 40,875,520-byte compiler image is +03a6ffebbcd1b5a56d986d61bddbaec3ebf164c7af9c480d068df19e2ba4924d. +Gate acceptance is resource-sum-replacement-gate-compile-v3/gate-acceptance.json, +SHA-256 f8e0623b59056bf3a6da1b5f94c4bcd54948a83df67863bbfd8c516cfb719be3. +Inner and outer terminals complete with unchanged guards, quiet/reaped children +and released leases. Compilation supplies zero Source/host execution credit. + +Ordinary-object preflight is accepted: its named Rust control passes all six +original Source endpoints in Debug and Release. It supplies no linked Source +execution credit. The separate single-entry matrix accepts **38 child rows and +38 attempts**, using **12 fresh programs across six distinct Sources** in paired +Debug/Release. All exact Rust oracles pass; actual child exits are 22 zero and +16 expected cleanup-failure status 71, with empty stdout/stderr. Every observed +attempt records retired logical Resources and empty aggregate ordinary storage; +private reports have zero incomplete flags. The five actual acquisition/dispatch counters include nonzero +measurements and are retained at their entry boundaries. + +All written Source/support files are absent before launch and each fresh +execution working directory is closed and absent afterward. Single-entry +compilation directories, objects and executables are retained for matrix reuse. +This is not prelaunch object deletion or a checked-program Weak-expiry test. +The adapter itself completes quietly with reaped processes, a closed root handle, +released lease and no guard failures. + +The retained-Session matrix also accepts **38 child rows and 76 attempts**, +with **38 fresh programs across the same six Sources** in Debug/Release. Each +child processes its selected first attempt then the ready attempt in the same +retained Session. Distinct attempt IDs and exact first-end/second-begin counters +retain the boundary observations. All attempts record retired logical Resources +and empty aggregate ordinary storage with zero incomplete flags. The 38 child +exits again match 22 zero and 16 expected status 71; child streams are empty. +Written Source/support is absent before launch, and each complete compilation/ +execution temporary directory is closed and absent afterward. Neither matrix +measures checked-program Weak expiry or per-kind physical storage. + +A pre-Cargo SDK-environment refusal and a later shared-test-harness import failure +remain preserved and unaccepted. The subsequent repair changes only the Rust +import path; original Jett Sources and oracles remain unchanged. + +Accepted execution leaves below are relative to target/native/. Their separate +process receipts and complete raw-file catalogues are frozen with the checkpoint. + +| Phase | Acceptance SHA-256 | +| --- | --- | +| native-bounded-resource-sum-object-preflight-execution-v1 | c5689a99986adfe4746d6f9b347784b1fb46ed123bf330444915f9709cca3744 | +| native-bounded-resource-sum-single-execution-v1 | eb7aa76b517cd0b2075b5086c4b85f9e439121c72bf8f7dc9533bf97d2e54bf5 | +| native-bounded-resource-sum-reentry-execution-v1 | 1c99637b0fe029179a18a81fff393da14f1f34394bf5393def1f29c04422943c | + +The two execution matrices total **76 native children, 114 attempts and 50 fresh +programs**, covering a union of **six distinct Sources**. Object preflight is +separate and is not added to these program or execution counts. The frozen +combined rollup is resource-sum-replacement-root-native-run-v1/accepted-observations.json, +SHA-256 25cdbc3dbbc2bf62b2ab20c9868dfb5efedc1de02707aee1938e0f1f7f3c1660. + +The unchanged pure_replacement_suites.jett fixture passes required evaluation +and native Verify/Property object emission. Its actual suite execution remains +a separate open obligation. + +Earlier actor 268ec7a9 and Runtime e916 observations retain their own compiler, +archive and execution scope. They are not promoted to current Resource sum or +Source17 acceptance. Physical storage/lifetime, per-kind private retainers, +normal-launcher custody/reentry, wider carriers, same-local sum reseating, +aggregate/projected/actor-state replacement and complete workspace/inventory/ +installed-host acceptance remain open. Full acceptance remains false; existing +rough tracking and fixed denominators are unchanged. GitHub CI stays disabled. + +## Pure-suite execution and Source17 regression checkpoint + +This follow-up closes the earlier pure-suite execution obligation. The preceding +checkpoint prose and counts retain their original execution scope. + +The shared harness passes **two ordinary controls**, with two native selectors +ignored by that command. The preparation log is +target/native/pure-replacement-source17-harness-preparation-v1.log, SHA-256 +0dd64dfbd2619ad2142dfe4ec53e17a0eb4cf147a722ca47a66efeacd0f24e44. +The actual formatted harness is +3c912d029957d97426822bb1d333d210dca6415e140b61bff77e1bf0e21ba6f0; +both original Jett fixture byte sequences remain unchanged. + +The v4 compiler-only gate is accepted with 1,111 compiler/Source inputs. Its +40,891,392-byte image is +3a0275e71bda3f4bb80af2b9584b829b22bd80374b652a9b97f6cd1ff4260173; +gate acceptance is +1e082661879eea5ea76ace2690cd9eb9fb49d74f7d246b0803d6b1c56881433b. +It grants zero Source/host execution credit. The same six measured archives and +receipt pins above are reused; no archive remeasurement occurred. + +The separate unchanged pure-replacement Source 0801c8… accepts **eight native +processes**, four emitted object groups/four distinct object hashes, and eight +measured private entries across Verify/Property and Debug/Release with normal/ +private Runtime links. The private Property runs record **400 actual trial +calls**: two 100-trial entries per profile, with 2,404 history records/profile; +private Verify records four history records/profile. Private incomplete flags +and all five separately measured acquisition/dispatch counters are zero, and +matched private launchers observe same-context reentry. Normal custody/reentry +telemetry remains null and unmeasured. + +All eight children return zero with empty streams. Written Source and object +are deleted before launch, the checked-program Weak root and required-value +input Arc are released, and children are reaped with executable/PDB removal. +These are bounded observations; individual opaque cache/private retainers and +per-kind physical lifetime remain open. The outer adapter is quiet/reaped with +its handle closed, lease released and no guard failures. The accepted leaf is +native-bounded-pure-replacement-suites-execution-v1/execution-acceptance.json, +SHA-256 4e3fb063933aa117145a4098f979d34024a3c1e01931bcd287339509d0f449d2. + +The separate unchanged Source17 c8e748… regression accepts **eight native +processes**, four object groups/four distinct hashes and eight private entries +under the same v4 compiler and reused archives. Private Property again records +400 actual trial calls, with **6,404 history records/profile**; Verify records +four/profile. Private flags/five counters are zero and matched same-context +reentry is observed. All child statuses/streams and release/cleanup observations +match the pure run; normal custody/reentry remains unmeasured. Its accepted leaf +is native-bounded-source17-execution-v4/execution-acceptance.json, SHA-256 +984da1c565b2f126fda2241de56fb4dabcb98b6e0d6ce177b8ce297c856bbb5a. + +These are two separate current suite observations. They do not enlarge or +relabel the preceding replacement checkpoint’s execution counts. Per-kind +physical storage, opaque/private retainers and normal-launcher custody/reentry +remain open. Full acceptance is false, rough tracking is unchanged and GitHub +CI stays disabled. diff --git a/docs/completed/native_codegen/native_returned_resource_hooks.md b/docs/completed/native_codegen/native_returned_resource_hooks.md new file mode 100644 index 00000000..ad839187 --- /dev/null +++ b/docs/completed/native_codegen/native_returned_resource_hooks.md @@ -0,0 +1,62 @@ +# Returned Resource hook descriptors + +Native lowering now supports exact returned compiler-hook descriptors for the +Factory, Borrow and Close recipes. Immutable bindings, aliases, root Return +relays, storage without invocation and immediately invoked returned callees +retain the original checked hook. These are installation-owned metadata, with +no Resource owner, finalizer or ordinary code/environment Function allocation. +A matching public function type never selects a hook or grants provider authority. + +MIR captures the original return and producer family from the retained +ResourceSourceArchive, then rechecks exact headers, original/current Source +calls, aliases and Return occurrences against the current CFG. An independent +body/entry/header snapshot protects callee effects and selected actual order. +Canonical block/local transforms authenticate that snapshot before consuming +remaps; missing retained targets fail without erasing evidence. Fresh CFG facts +invalidate changed locals and joins. Readonly descriptor getters select exact +current occurrences; copied expressions do not receive that authority. + +A preceding ordinary branch with no nested Return can preserve one exact root +Return. Nested or conditional descriptor returns, mutable/dynamic producers, +captures, generic/scoped bodies and descriptor escapes remain pending. The +current rule does not infer an identity from the callable signature. + +Native wire shape 10 records the exact returned hook. Wire tag 15 joins the +original indirect invocation to its hook, signature and separate physical +Acquire, Borrow or Close target at the exact operation frame. The emitter opens +the operation, evaluates and stages original actuals, evaluates the opaque +callee once last, and only then prepares the descriptor invocation. Existing +runtime installation, hook, signature, target and frame checks remain intact. +Raw returned metadata is adopted only after SourceStatus authenticates completed +Scope retirement. Exact opaque occurrences bypass ordinary Function clone and +ownership storage; ordinary named callbacks retain their separate lifetime. + +Authenticated Resource function roots seed the existing typed reachability +traversal before dependencies are walked. An ordinary pure helper reached from +an otherwise unused descriptor accessor is declared and verified normally; it +retains the ordinary context/environment ABI and receives no hidden Scope or +carrier exemption. + +The initial six-case baseline passed 76 reference executions across 38 scenarios +but failed native MIR with `indirect custody call lacks an exact descriptor +producer`. The expanded corpus has 46 scenarios, with all 92 reference and 92 +Source-absent linked native executions passing across debug and release. Stored +and immediate callbacks cover all three recipes, aliases, relays, unused +metadata, factory failure, argument-before-callee effects, failed later actual, +failed callee and finalizer failure. Every execution checks zero obligations +before context teardown. + +A review regression reproduced a copied/resealed branch followed by canonical +pruning being accepted before the repair. Nine focused MIR groups and all 242 +MIR library tests now pass, including block/local pruning and missing-target +controls. Seven focused codegen groups, all 100 codegen library tests and 63 +object tests and all 92 driver-library tests pass. Runtime sources and the +matched private archives remain unchanged. + +This is local MSVC private-archive acceptance. Required/comptime materialization, +returned named callbacks, dynamic/captured/scoped/generic/handled/pipeline forms, +nested return transport, broader aggregates, production providers and +concurrency remain required for the full native-codegen goal. GNU private-archive +Source execution and final-revision workspace/distribution gates remain separate +obligations. The coarse whole-language estimate remains about 85%; this corpus +is execution evidence rather than measured whole-language coverage. diff --git a/docs/completed/native_codegen/native_scoped_call_view_staging.md b/docs/completed/native_codegen/native_scoped_call_view_staging.md new file mode 100644 index 00000000..3a9f2106 --- /dev/null +++ b/docs/completed/native_codegen/native_scoped_call_view_staging.md @@ -0,0 +1,141 @@ +# Native internal call-view stages + +This is a bounded native implementation of Rule 24's existing explicit, +read-only call borrow. It does not change persistent source local-alias lifetimes. +The selected contract was captured before Rust in the internal stage contract +and the field/direct-origin amendments: + +- `target/native-projected-interface-view-staging-owner-change-witness/scoped_stage_contract.md` +- `target/native-scoped-call-view-origin-witness/contract.md` +- `target/native-scoped-call-view-value-origin-witness/contract.md` + +The source authority remains `docs/design.md` Rule 24, particularly the explicit +call-site `view`, call-bounded lifetime and subsequent owner-rebinding examples +(lines 5755–5775, 5893–5915 and 5939–5942). The zero-copy contract protects +backing storage while a view exists (5990–5995). Architecture's existing handler +snapshots remain bounded by recursive cloneability (1199–1202); explicit interface +cloning does not authorize a new implicit erased-authority snapshot. + +The implementation stages the original checked view eagerly before a later +handled operand. Typed MIR `BeginCallView` and `EndCallView` identify the internal +loan; synthesized local names grant no authority. Exact local identity and +endpoint types, checked field owners/indices and transparent qualifiers remain +validated before pruning, including unused and unreachable metadata. Direct +views keep their pending metadata and add no readiness guard. Projected fields +retain every eager receiver guard, so a pending owner fails before a later +handler can emit its marker. + +Safe terminal mutable owners remain protected through the consuming operation +and may be rebound afterward. Original owned temporary producers evaluate once +into normal owning storage; the internal alias owns no destructor slot and +does not clone or rebox an interface. Ordinary owner cleanup remains separate +from End. Normal completion materializes the operation's owned/copy result +before End. Aborted handler returns end abandoned stages before evaluating an +owned return operand. Break/continue end only stages belonging to operations +abandoned by the targeted loop exit. Source-declared aliases retain their +existing persistent origin and owner-loan checks. + +The source argument's original explicit view and checked borrowed position are +both required. Formal-mode wrappers cannot hide a bare argument's ownership +transfer. Direct/indirect calls, existing borrowed intrinsic positions and +complete reflected read/validation operations use consuming-operation bounds. +Owning aggregate/collection/actor/transition paths retain their existing staging +eligibility; this change does not store borrowed views in owned data. No runtime +ABI, reference count, owner pin, resource clone or escaping-view permission is +introduced. + +The admitted owner-change path now includes an exact source-checked mutable +plain `TypeConstruction` binding captured by written `view`. Its current binding +and captured generation remain separate through later replacement RHS evaluation, +assignment and complete/aborted call cleanup. Private constructor-owned original +Source/local-header/assignment witnesses and exact CFG/loan joins authorize the +internal generation escrow. Archival identities do not become runtime locals or +liveness roots, and canonical preparation validates every current remap. + +Written views of handled owned producers preserve their exact original conversion +and normal owning backing. For interface boxing, an occurrence-specific validated +association permits the concrete input read while the new box remains owned. +Runtime boxing may copy that payload; no zero-runtime-copy guarantee, source +clone permission or ABI is added. Ending a loan alone does not preserve destroyed +backing. Broader simultaneous cohorts and other owner kinds, persistent source +alias owner changes, arbitrary erased-authority snapshots and whole-language +parity remain open. + +Root applied the guarded 90d82701 candidate only in the isolated managed staging +worktree. Root repaired one Rust mutable-walk diagnostic with a typed recursive +root replacement, repaired the loop fixture's required default path, and aligned +the persistent-alias test with direct internal staging while retaining its +owner-loan rejection. The resulting focused gates actually passed **99 MIR +library tests and 57 codegen tests**, including the object-emission units. This +is not the independent full object inventory or a full workspace acceptance. +The primary 90d82701 full-workspace audit remains a separate checkpoint. + +All nine linked regressions pass from unchanged, root-characterized LF +sources. They pin eager field ordering, pending first-error, the declared Named +alias control, post-call owner consumption, early-return ownership, mutable field +and direct owners, and owned field/direct temporary evaluation once. Each test +checks the literal reference outcome and empty frontend/runtime events, builds +both profiles with matching runtime archives, deletes source before executing +bounded native binaries, and compares exact stdout, stderr and terminal status. +The pending failure expects native status 71 and the canonical runtime message; +normal CLI's reporting prefix is not part of that runtime message. The executed +gate passes nine tests in 44.95 seconds (448 filtered) with all 21 compiler/test +source hashes unchanged, recorded in `target/native-scoped-call-view-linked.log` +and `.json`. + +All 983 crossed compiler library tests pass (57 codegen, 416 comptime, 139 HIR, +99 MIR, 48 resolve, 224 typecheck) in `target/native-scoped-call-view-phases.log`. +All 83 driver library tests pass in 70.22 seconds in +`target/native-scoped-call-view-driver.log`. The actual frontend/lowering gate +passes eight backend tests (154.20 seconds), 598 frontend fixtures (23.94 seconds) +and two view-escape tests (8.73 seconds) in +`target/native-scoped-call-view-frontend-and-lowering.log`. The separate +`native-scoped-call-view-frontend.log` records an initial wrong test-target name; +that invocation is not a fixture acceptance result. Rust formatting, nine source +format checks and diff checks pass. + +The preceding exact clean `90d82701` revision passes its complete 448-test +workspace, all 182 object obligations and 92 result groups. Its supported-host +[workflow 37108099164](https://github.com/vycdev/jett/actions/runs/37108099164) +passes all four Windows/Linux build/package and relocated installed jobs, last +completing `2026-10-03T09:03:23Z`. Those results accept the preceding callback +repair, not this later staging implementation. The exact clean `55027914` +staging commit now passes its complete 461-case workspace, all 182 object +obligations, remaining targets and doc-tests with 92 successful groups and an +unchanged head. Its terminal record is centralized in +[the descriptor note](native_uninhabited_callback_descriptors.md#accepted-predecessor-and-separate-follow-up). +Supported-host workflow 37112967414 also passes all four Windows/Linux build/ +package and relocated installed jobs, last completing `2026-10-03T10:23:08Z`. +No full surface completion, denominator change, or new coarse coverage percentage +is claimed by this note. + +## Expanded consuming-operation acceptance + +Four additional root-characterized sources extend the same internal loan to an +indirect interface-field call, a source call borrowing Stdout, a complete +reflected field getter plus Positive producer validation, and an owned temporary +whose later handler fails independently in list.remove_at. The last case keeps +ordinary bytes/list slots live, evaluates its original producer once, suppresses +callee/after markers, and retains the exact list.__remove_at diagnostic and +native status 71 instead of cleanup status 72. The capability case uses the +source formal's checked View mode; StdoutWrite's intrinsic mask is unchanged. + +All four selected LF sources pass formatting and frontend checking in both +profiles. Actual default-profile reference/agent captures have empty debug +events; the success outputs and independent failure are literal test oracles. +The original reflected-source formatting failure and exact original capture +remain in `target/native-scoped-call-view-consumer-witness`; root formatted a +separate selected copy and recaptured all four sources in +`target/native-scoped-call-view-consumer-formatted-witness`. No execution oracle +was inferred from a formatting change. + +The final combined gate passes **all 13 linked tests in 25.95 seconds** (448 +filtered), with all 25 compiler/test source hashes unchanged, in +`target/native-scoped-call-view-linked13.log` and `.json`. Both matching runtime +profiles build before source deletion, then bounded launches compare exact +status/stdout/stderr and empty unintended observations. All 13 tracked source +format checks pass in `target/native-scoped-call-view-source-format13.json`. +Rust formatting and diff checks pass. The supplemental corpus now registers +461 cases. Complete local workspace and 182-object acceptance at `55027914` +and all four supported-host jobs are recorded above. The fixed inventory is +still 207 and the broad planning estimate remains about 85%. diff --git a/docs/completed/native_codegen/native_secret_owner_projected_local_views.md b/docs/completed/native_codegen/native_secret_owner_projected_local_views.md new file mode 100644 index 00000000..54a29142 --- /dev/null +++ b/docs/completed/native_codegen/native_secret_owner_projected_local_views.md @@ -0,0 +1,136 @@ +# Native directly secret-qualified aggregate local views + +## Selected typed proof + +Design's secret field rule and Rule Set 24 already admit readonly projected +aliases from directly secret-qualified aggregates. The bounded HIR repair +preserves the actual checked receiver and accepts precisely an exact declared +Struct, Bitfield or MachineState owner, optionally carrying one direct +`Secret(owner_type)` layer. State and canonical FieldId lookup remain exact. + +For a secret receiver, a declared `nothing` or already-secret field retains +its exact TypeId, including nominal refinements whose declared base chain reaches +Secret. All other field expressions must be exactly one +`Secret(declared_field_type)`. Finite checked metadata traversal detects the +exception without substituting the field's nominal identity. Final local secret +promotion is an independent existing annotation rule. Multiple owner layers, +implicit declassification, sibling/base refinement substitution and arbitrary +carrier equality do not supply the Field proof. + +Keep exact source LocalId/type, base owner, intermediate/endpoint types, +immutable preceding origin chain, cycle checks and validated-initializer ledger. +Unused or orphan aliases cannot bypass proof before compaction. Explicit +borrowed Coarsen/Declassify preserve their exact selected conversion and backing +owner; explicit clones and ordinary owned fields remain independent owners. +The same generic field can require a secret borrowed binding but produce an +implicitly copyable scalar after explicit declassification. + +MIR persistent loans, whole-owner liveness, non-owning slots and cleanup are +unchanged. The native field access retains the secret base and existing pending +checks. No allocation, hidden owner, implicit clone/join, ABI change, mutable or +temporary view, capture escape, projected mutation, owner-change permission or +loan-expiry policy is added. The independent interface handler staging draft +is unapplied; this proof does not resolve its scoped internal loan problem. +This field-view proof introduces no debugger observation policy. Direct-field +terminal receiver errors apply the existing secret non-disclosure contract; +the diagnostic successor below preserves the checked receiver type. + +## Actual pre-change source evidence + +`target/native-secret-owner-projected-view-witness/results.json` (SHA256 +`904a9430ebbd75af1a5d52eaa478c194a12556c49535f9ece88cf3cc26f27aeb`) +records all seven sources. Seven formatter checks and 14 frontend profile +checks pass. Ordinary default-profile reference/agent captures record six +successful sources and one pending-receiver failure, all with no debug events. + +Direct and nested/forwarded struct aliases, the declassified owned control, +secret bitfield payload, owned exact-state machine and machine view parameter +preserve literal application lines and original owner rereads. Every visible +data observation uses existing explicit declassification. Their prior AOT +attempts preserve all 14 output sentinels: 12 alias attempts fail first at HIR +owner validation, while the two owning-copy controls reach unavailable archive +lookup. These are predecessor boundaries, not repaired native execution. + +The pending nested struct prints only `before:secret\n`, then the reference +reports its existing pending Packet field-access error. It reaches neither the +after-binding marker nor a later observation. The original reference error +contains the underlying aggregate. Its normal CLI `error: ` reporting prefix +is separate from `RunFailure.message`. Native retains its existing checked +`[redacted]` receiver rendering. This exact difference is already recorded in +[the open observation note](../../open_design/debug_print_hidden_secrets.md); +qualified-field admission preserved native redaction. Those original raw +reference observations remain historical; the direct-field diagnostic successor +below applies existing non-disclosure to this same checked source. + +## Historical acceptance and remaining gates + +The formatted/rebased HIR proof adds three tests: nested/forwarded exact owner +and declared qualification exceptions; concrete generic scalar/linear binding +facts and explicit clones; fifteen malformed qualification/owner/index/root +and unused/orphan proof controls. All three pass (136 filtered, 0.00 seconds) +in `target/native-secret-owner-local-view-hir.log`. The first warning-interrupted +invocation produced no test result and is not an executed gate. + +All seven linked tests pass together (2.82 seconds, 432 filtered) in +`target/native-secret-owner-local-view-linked.log`. Both matching-profile +executables build before source removal and require exact stdout, status, +stderr and empty runtime/frontend/artifact observations. Six successful cases +pin full outcome parity. The pending case requires the existing raw reference +message separately from native `runtime error: field access is not supported on [redacted]`, +with matching partial stdout and native status 71. Cleanup status 72 cannot +substitute for that status. This records the open diagnostic boundary, not +seven diagnostic-parity cases or a new secrecy rule. + +The initial attempt's six passes and one failed shared-error comparison +(4.56 seconds) are retained in +`target/native-secret-owner-local-view-linked-initial-mismatch.log`. Correcting +the test to the already documented backend-specific error contract leaves +source policy, native redaction and interpreter behavior unchanged. +All 954 crossed library tests pass (41 codegen, 416 comptime, 139 HIR, 86 MIR, +48 resolve, 224 typecheck) in `target/native-secret-owner-local-view-phases.log`. +All 83 driver library tests pass (66.62 seconds) in +`target/native-secret-owner-local-view-driver.log`; all 598 frontend fixtures +pass (22.51 seconds) in `target/native-secret-owner-local-view-frontend.log`. +All seven source-format checks, Rust formatting and diff checks pass in +`target/native-secret-owner-local-view-format.json`. + +The supplemental corpus registers 439 tests. The exact clean `cb617ecf` +workspace passes all 439 native tests (905.04 seconds), all 182 object +obligations (516.91 seconds), 83 driver library tests (91.44 seconds), all 598 +frontend fixtures (19.08 seconds), remaining workspace targets and doc-tests. +The unchanged-head, clean-start/end wrapper finishes at +`2026-10-02T20:55:57.0941072Z` with exit zero and 92 successful result groups, +recorded in `target/native-workspace-cb617ecf.log` and its JSON summary. +Supported-host acceptance passes at documentation-only `194ce065`, whose +compiler/runtime/test sources are identical: all four build/package and +relocated installed jobs succeed in +[workflow 37064035604](https://github.com/vycdev/jett/actions/runs/37064035604) +(last completion `2026-10-02T22:10:29Z`). The standalone `cb617ecf` workflow +was superseded and cancelled without starting jobs; it is not acceptance. +The prior accepted +08f9f7e7 platform and bitfield focused gates are centralized in +[the acceptance audit](native_acceptance_audit.md#current-frozen-revision-and-inferred-json-follow-up). +The predecessor platform result does not validate this secret revision. +Whole-language native parity +remains the 100% objective, tracked with about-85% and the fixed 207 inventory. + +## Direct-field diagnostic successor + +Unsupported direct-field receivers now render from the original value and its +checked receiver type before wrapper consumption. This makes the existing +pending secret Packet failure redact the entire receiver, including pending +depth and nested bytes, in both execution paths. Successful extraction, receiver +evaluation, failure location, partial stdout and cleanup order stay unchanged. +No implicit declassification, join, new field admission or debugger policy is +introduced. Raw direct-handle and pipeline fallbacks are outside this repair; +no authenticated secret-bearing failing source for them was established. + +The original `07_pending_secret_struct_receiver.jett` bytes remain unchanged. +Its existing native gate now requires one exact shared reference/native message, +`runtime error: field access is not supported on [redacted]`, before-only stdout, +status 71 and both Source-deleted profile executions. Historical receipts above +retain their original mismatch scope. Integrated verification passes all seven +secret-owner controls (14 Source-deleted profile executions), all 645 Comptime +tests and eight adjacent native debug/pending controls. The focused execution +log is `target/native/secret-observation-owner-views-v1.log`; these results do +not replace full-workspace or supported-host acceptance. diff --git a/docs/completed/native_codegen/native_stable_projected_local_views.md b/docs/completed/native_codegen/native_stable_projected_local_views.md new file mode 100644 index 00000000..e299f333 --- /dev/null +++ b/docs/completed/native_codegen/native_stable_projected_local_views.md @@ -0,0 +1,121 @@ +# Typed stable projected local views + +Rule Set 24 already permits readonly, non-owning field views. Native support +must preserve the exact checked field type and the lifetime of its backing +owner. Alias slots remain immutable; a mutable terminal must be an exact checked +owned local or owned parameter, while view-parameter backing stays immutable. +The mutable-owner extension below has focused MSVC acceptance. The characterized +`view packet.data` case passes checking and reference execution, but native +lowering lost its stable binding origin before this repair. + +Keep `CheckedViewSource::Binding` and `Local.view_source` as the immediate +binding dependency. The typed initializer's existing ordinary-struct `Field` +nodes provide the canonical finite projection path; do not duplicate field +indices in local metadata. Validate the actual source-local type, every field +owner and index, intermediate and endpoint types, and the terminating local ID. +Carrier-preserving wrappers retain the existing representation checks. +They also preserve nominal qualification: only existing outer secret promotion +may be implicit. `coarsen` follows the declared refinement ancestor chain and +`declassify` removes the exact outer secret layer. A view or empty conversion +cannot introduce, remove, or substitute a refinement. + +The whole owner's type can differ from the borrowed endpoint's type. Removing +that old equality check requires proving every borrowed initializer, including +unused locals and unreachable MIR blocks, before optimization can remove it. +Generated functions must discard copied origins that belong to another body; +borrowed captures retain their existing rejection. + +Existing persistent owner dependencies, liveness, and non-owning alias storage +apply to projected aliases. Ordinary field copies and explicit clones remain +owned. Bytes and lists are executable witnesses, not an endpoint allowlist. +There is no runtime ABI change or implicit allocation. + +Temporary roots, mutable alias slots, mutable view-parameter backing and owner +changes with dependent aliases retain their implementation boundaries. Absent or +mismatched selected reflected root facts and borrowed-sum Handle backing keep +separate proof limits. The mutable-owned-backing extension selected no lifetime extension or +loan expiry. The later [temporary-backing contract](native_temporary_local_views.md) +selects lexical whole-owner retention; its implementation remains pending. +General loan expiry and other unresolved boundaries stay in the local-view note. + +Verification must cover direct and nested paths, forwarded aliases, view +parameters, exact checked contexts, clone independence, owner liveness, and +success/failure cleanup with linked debug/release native execution. + +The ordinary-struct focused checkpoint passed all 896 compiler-phase library +tests and 12 local-alias native tests; the later frozen a0bf12df workspace +passed all local gates at its 378-test corpus. The +[exact-state follow-up](native_local_view_aliases.md#exact-state-qualified-machine-field-follow-up) +now extends the same proof to declared MachineState payloads. Its current +261 HIR/MIR/codegen library tests pass, as do 26 distinct local-alias tests +across a 25-test batch and a separate typed-endpoint test. Exact scope/logs +are in that note. The new 392-test frozen workspace and supported-host +acceptance remain required. This preserves exact state/nominal qualification +and current owner-change boundaries. At that checkpoint, bare flow-narrowed, +mutable or temporary origins and broader projected owner/endpoint combinations +were outside the bounded proof. Those historical counts do not validate the +mutable-owned-backing follow-up. + + +## Mutable owned backing follow-up + +The extension authenticates a mutable terminal through its exact original +DefId-to-LocalId mapping and active concrete owned binding fact. It does not +infer ownership from absent alias metadata, a name, or flat global facts. +Direct and projected aliases retain their immutable immediate dependency chain; +all field-owner/index/endpoint, nominal qualification and initializer checks +remain mandatory, including unused and unreachable aliases. Existing persistent +loan and liveness planning still blocks every owner change while a reachable +alias may exist, even after its last read. + +The unchanged original unused Source08 and observable nested bytes/list +forwarding and copy-independence Source11 pass reference checks and four +Source-deleted executions in paired Source/runtime Debug and Release. The +[native audit](native_acceptance_audit.md#mutable-owned-local-view-backing-bounded-msvc-acceptance) +records the full selected matrix: 31 tests, 64 executions and six separate +publication-boundary checks, alongside 753 affected library tests. +Mutable View-parameter roots and temporary-backed aliases remain unsupported +native compositions; they are not new Source bans. Mutable owned borrowed-sum +Handle backing has separate [bounded receipt acceptance](native_mutable_borrowed_sum_backings.md), +which do not infer ownership from absent alias metadata. Absent or mismatched selected reflected root facts remain refusal cases. +No runtime ABI, implicit clone, +lifetime extension or loan-expiry change is made. The current bounded acceptance +is separate from the historical receipts above and does not complete native parity. + + +## Reflected outer-owner fact transport + +The producer retains only exact transitive dependencies of its +actual local-view facts from the selected concrete body map. It follows the +original binding DefId to its declaration and copies the matching fact without +changing ownership mode, type or mutability. Missing/mismatched facts and cycles +remain conservative; no flat-map fallback or ownership inference is added. + +HIR and MIR admission, finite field proof, immutable alias slots, persistent +loans, backing liveness and cleanup guards are unchanged. Source12 targets direct, +generic repeated reflected and nested alias forwarding contexts. Genuine scoped +fact tests distinguish scalar copies from aggregate views and refuse a corrupted +child map despite a valid enclosing or other concrete-instance fact. + +Source12 passes reference checks and two Source-deleted executions in paired +Source/runtime Debug and Release. The +[native audit](native_acceptance_audit.md#reflected-local-view-backing-facts-bounded-msvc-acceptance) +records the 1,797-test affected-library result and the complete selected matrix: +32 tests, 66 executions and six separate publication-boundary checks. Earlier +first-slice receipts retain their own scope. No lifetime, borrowed-sum, +Resource, Runtime ABI or archive change is included. + + +## Temporary backing decision + +The selected [temporary local-view contract](native_temporary_local_views.md) +retains an ordinary owned temporary's whole exact result, evaluated once, in +compiler-owned storage through the alias declaration's original lexical scope. +The finite typed projection and immutable forwarding remain nonowning; no +last-use expiry, implicit clone or invented Source DefId is allowed. Cleanup +must retire the backing exactly once on each scope exit, after acquiring any +owned exit result, while preserving copyable endpoints, pending/error order +and escape/loan rules. Operation-bounded call staging stays separate and no +actor/Resource authority follows. Implementation, unchanged Source09 execution +and reference/native cleanup gates are pending. None of the historical or +current stable-owner receipts above establish temporary-backed execution. diff --git a/docs/completed/native_codegen/native_temporary_list_views.md b/docs/completed/native_codegen/native_temporary_list_views.md new file mode 100644 index 00000000..72d19717 --- /dev/null +++ b/docs/completed/native_codegen/native_temporary_list_views.md @@ -0,0 +1,181 @@ +# Temporary List views: bounded acceptance + +The bounded List transport slice passes local MSVC acceptance on 2026-10-16: +17 focused reference controls, selected compiler-library/fixture/CLI regressions, +and 24 tests / 48 paired Source-deleted native executions. The new List selector +accounts for one test and two executions within that complete matrix. The earlier +focused List pair overlaps the full matrix; it does not increase the distinct +accepted total beyond 48. Physical cleanup and the full connected lifetime +contract remain open. + +The selected [lexical temporary-view contract](native_temporary_local_views.md) +remains unchanged. An ordinary producer result stays owned until its original +function scope exits; aliases and named view parameters carry nonowning tickets. +This slice extends the checked reference adapter through List endpoints already +represented by HIR/MIR proof. Its bounds add no checker restriction or language +spelling and do not narrow the complete lifetime rule. + +## Transport and authentication + +The producer is an ordinary nongeneric named function returning a finite ordinary +struct. An exact checked finite field path selects a List endpoint. The complete +owner, including an unviewed sibling, remains in activation-owned vacant/live/ +retired storage. Alias publication and forwarding use payload-free Nothing +placeholders and private exact live tickets. Explicit Source clone produces a +whole owned List copy. + +Named observers may be ordinary or generic. A receipt identifies the exact +retained checked generic instance by immutable program and instance index, +including definition, concrete arguments and full specialization. Calls retain +original callee/actual storage, the full ownership packet, parameter permutation, +concrete signature, view modes and selected body facts. Registered Source is +authenticated before independent argument effects. Runtime type projection and +Source-aware type arguments rejoin the exact retained instance; missing facts +do not fall back to global generic body maps. + +Both private frame boundaries select checked type projection by the exact +DefId-owned declaration span and rejoin it to that original instance. A later +mutual body-name span is a different key. Canonical mutual declarations retain +their original DefId through the immutable resolver edge and full registered +Source authentication; neither boundary guesses from a name or body span. + +Temporary-owner admission preserves the accepted classifier: an exact immutable +original VarDecl with a written view initializer, canonical variable/name/span +and authentic checked Other-view fact. Existing viewed enum patterns and +iteration binders keep their ordinary borrowed paths and gain no temporary +owner authority. Nested true temporary declarations retain raw-entry and +unsupported-region refusals. + +The observer adapter borrows the original List for length, sum in the already +specified numeric domain, and get of checked implicitly copyable primitive or +String elements. Get preserves negative/out-of-range optional results and may +copy one copyable element; it does not copy the backing List. Move-only get and +Handle composition remain separate work, without changing Source list.get. +Runtime helpers and ABI are unchanged by this slice. + +## Compiler and fixture verification + +All 17 focused reference controls pass: five new List controls and twelve +existing call controls. The List controls retain real shipped list.jett bodies +and cover numeric/String endpoints, explicit/inferred generic observers, nested +stdlib calls, same-template specialization and foreign-program identity, +changed-specialization refusal, copied get results and bounds. The genuine user +mutual observer checks distinct declaration/body spans and the exact DefId edge. + +The focused log is target/native/temporary-list-transport-focused-v1.log, +SHA-256 cac01d28dac98fada049d521bd80b9139d842d10e9495ebb4861998d7dabd90c. +The terminal reports 17 passed / 0 failed. Root then removed only unnecessary +Rust or-pattern parentheses; retained before/after images are in +temporary-list-pattern-cleanup-v1. Successor selected regressions all pass: + +| Check | Tests | Log under target/native | SHA-256 | +| --- | ---: | --- | --- | +| Compiler libraries | 1847 (135 + 683 + 271 + 379 + 379) | temporary-list-transport-core-v1.log | d86f8177789a9aed444317787a49a81854a0e5cd1ca3eb7101ea2d9dde8ebd7b | +| Fixtures | 603 | temporary-list-transport-fixtures-v1.log | d7d77d9c9b9641516890253923c3d822b8b2af2727a532e92f11cf40c1e6594a | +| CLI capture | 9 | temporary-list-transport-cli-v1.log | 56c1b715305b34e36cbb7bf0f537b8ae1e94a510668201b59935adce20db6033 | + +Each terminal result has zero failures; Root reports complete sequence exit zero. +These selections do not establish full-workspace or whole-native-corpus coverage. + +The first full matrix attempt fails with exit 101 because the authored Source14 +uses unsupported match/some/none optional patterns. Its List selector fails +checked-reference parsing before List native building or execution. The failed +aggregate accepts zero executions, despite two earlier retained selectors +passing; their partial observations are never added to a successor receipt. +All Source/Runtime/fixture/original-run-pass guards pass in that failed attempt. + +- Failed summary: target/native/temporary-list-view-native-execution-v1/execution-summary.json, + SHA-256 55751b44050f635c90b0972c39f0f26938ee91171a5fc74448da7d82387be711. +- Failed List log: temporary-list.log in the same directory, + SHA-256 907fdd85438cc9b59e77c39d1957e175c00bfc323720e4885b22556fb8ae5550. + +The fixture-only repair adds number_text(optional[int64]) and +name_text(optional[string]) with canonical bare handle, replacing four optional +text observations. Producer-once numeric/String scenarios, forwarding, mutual +explicit/inferred count, sum/is_empty, get at 0/-1/3, clone/append independence, +the test selector and fixed expected stdout are retained. Compiler files and +native test counts do not change. + +The formatted repaired Source14 is SHA-256 +ae81376d38a94f9890146138609ee5df641cf49bca9e15530464a50fea6d1c99. +Its frontend receipt in temporary-list-fixture-handle-repair-v1/frontend-receipt.json +is SHA-256 c8eb735fb7d852c9a3388ba556548f96b1a865df8d07aa9165a98cb4519a45f0: +format and Debug/Release build --agent --check all exit zero; each frontend +check reports zero errors and zero warnings. The failed before image is +4723d2929ee5d45f93980ca670e495c2a92e1f6a956c78dcce09baacbec00656. +The initial format-only result and compile-v1 gate never established Source14 +validity. That historical gate remains bound to its earlier Source inventory. + +## Accepted paired native matrix + +The fresh matching MSVC compile-v2 gate passes compile-only acceptance with zero +Source-execution credit of its own. Its receipt is +target/native/temporary-list-view-gate-compile-v2/gate-acceptance.json, SHA-256 +40ad0d06d7a5585659b3e39311d60363cbd39848b2384bd5fe9d4760dd4c2968. +The compiled gate image is SHA-256 +76f8a7020fb82fd53d573324f5a38730df68261581add438025243798f3a0c82. + +The focused repaired List selector passes one test/two paired Source-deleted +executions in target/native/temporary-list-view-native-focused-v2/execution-summary.json, +SHA-256 ae05fa796b52584804f5004fbd3315a0839a5963fc5a92760dec15b49a4e0411. +Its selected log is SHA-256 +958d5430766ec664c44de2811514f94fcc14c15e28aaef5607f80429b12572b9. + +The complete successor passes all 24 tests and 48 paired Source-deleted +executions, with 24 Source Debug/Runtime Debug and 24 Source Release/Runtime +Release executions. There are zero publication refusals and zero legacy +Debug-only runs; no full profile cross-product is claimed. The terminal receipt +is target/native/temporary-list-view-native-execution-v2/execution-summary.json, +SHA-256 a2b39a53402f88709493b2b022d97d6d8ce342c8b46b62252ce7007d7cd1ee84. +All sixteen selected log hashes and exact passing terminal test counts were +independently checked against that receipt. + +| Accepted selection | Tests | Debug/Debug | Release/Release | Source-deleted executions | +| --- | ---: | ---: | ---: | ---: | +| Retained selections, freshly rerun | 23 | 23 | 23 | 46 | +| New List selector | 1 | 1 | 1 | 2 | +| Complete matrix | 24 | 24 | 24 | 48 | + +The 48 already include the earlier focused List pair. The distinct accepted +packet total is 48, not 50. The committed byte checkpoint at 291ccbb0 retains +its own historical 23/46 receipt, and is not added to the refreshed List matrix. +The retained controls cover two bytes temporary tests, eight stable projected +views, eight scoped calls, two mutable owners, one reflected mutable view and +two actor persistent views. Their regressions grant no temporary-owner authority +in reflected or actor contexts. + +The exact new selector is +local_view_aliases::native_temporary_local_list_views_match_checked_reference_without_source. +It calls run_case once. That helper obtains the genuine checked reference +outcome, compares it with fixed expected stdout, builds each matching normal +Runtime profile, deletes copied Source, then compares native status/stdout/stderr +and applicable Debug events. The repaired fixture observes one producer call per +scenario, numeric and String Lists, alias/generic reads, copyable get/bounds and +explicit clone independence. Its success oracle requires no stderr or Debug +events. These observations establish selected behavior, not physical lifetime. + +All 1058 compiler/Source, 68 reused Runtime artifact, 65 fixture and 182 original +run-pass guards remain unchanged before/after execution. All six count/launcher +sources are retained; Source14 alone grows the fixture inventory from 64 to 65. +The ready-v2 runner changes only its Source14 pin from the first ready runner; +all prior 23 tests, selectors and expected stdout stay intact. + +- Repaired Source14: ae81376d38a94f9890146138609ee5df641cf49bca9e15530464a50fea6d1c99. +- local_view_aliases.rs: 23687b0f4d91e310645059f4170c358090e82cb8ffd627075c835016745c5a1d. +- Unchanged Source09: c0f288b659bdf84bf87fb7f46576eb7cadd3b440c000f726b5b1c099ccfd83af. +- Unchanged Source13: 48391e6a0fa7390f6d36cb190c8e4100d835218ca317232a64ec8ec228cd59aa. + +## Remaining contract + +Output and logical audit events do not prove physical owner destruction timing +or absence of hidden allocation/cloning. Private measured cleanup counters, +whole-owner/sibling lifetime, earlier-owner cleanup order and full connected +failure traces remain required. Nested regions, loops, abrupt exits, +general/generic producers, reflected bodies, actor/Resource owners and move-only +get/Handle results retain their implementation and acceptance obligations. + +This receipt does not establish complete Resource/Graph integration, +suite-factory, full native inventory/corpus, workspace/doc-tests, +GNU/installed-distribution or whole-language native acceptance. The fixed +207-fixture/182-object denominators and rough planning estimate are unchanged. +GitHub CI remains disabled at the user's request. diff --git a/docs/completed/native_codegen/native_temporary_local_views.md b/docs/completed/native_codegen/native_temporary_local_views.md new file mode 100644 index 00000000..3a83071a --- /dev/null +++ b/docs/completed/native_codegen/native_temporary_local_views.md @@ -0,0 +1,292 @@ +# Lexical backing for temporary local views + +Status: selected language contract, 2026-10-15. A bounded ordinary function-body +implementation has MSVC acceptance on 2026-10-16. All +1,842 core-library tests, 603 frontend/reference fixtures and nine CLI capture +tests pass. The native selection passes 23 tests and 46 paired Source-deleted +executions. Physical cleanup measurements and broader-context acceptance remain +pending. The full selected contract below is unchanged; this implementation +slice does not narrow its remaining obligations. + +## Selected rule and tradeoff + +An admitted immutable source local initialized by a view of an ordinary owned +temporary retains the whole exact producer result in compiler-owned storage. +Evaluate the producer once at its original initializer position and retain that +owner until the declaration's original lexical scope exits. Immutable forwarding +creates nonowning aliases to the same backing; it does not duplicate the owner +or extend the originating scope. Even an unused alias or one read only early in +the scope keeps that backing until scope exit. + +Scope-based freeing in [Rule Set 10](../../design.md) and Rule 24's lexical escape +bound support this choice. Previously, the local-view note left both temporary +extension and last-use expiry unresolved. Lexical retention keeps cleanup local +and explicit without introducing last-use loan analysis. It may retain unused +owned storage longer than a last-use scheme. That is the selected tradeoff; +keeping only a projected field or cloning it would change the ownership model. + +The whole owner retires exactly once on normal scope completion or an exit by +Return, Respond, Default, Break, Continue or terminal failure. Acquire any owned +exit result, including an explicit clone, before retiring backing it may read. +End dependent aliases before dropping the owner. Loop iterations create separate +backing activations; backedges retire that iteration's owners, and other exits +retire only crossed scopes. Unselected branches create no owner. Existing cleanup +ordering and primary-error/cleanup precedence remain authoritative. + +No last-use expiry, implicit clone, runtime owner pin or fake Source DefId is +introduced. Copyable endpoints retain their ordinary copy behavior. Existing +escape and dependent-loan rules, typed field paths, pending checks and failure +order remain unchanged. Views gain no permission to return, capture, enter an +owning aggregate, cross threads or outlive their originating scope. + +This local rule does not grant actor-state lifetime or Resource custody. Mutable +alias transitions, allocating conversions without an exact owned-producer proof, +borrowed-sum alternate backing and other current implementation limits remain +separate. It adds no Source ban to compensate for missing native support. + +## Checked initializer and generated owner proof + +`CheckedViewSource::Other` records an unstable expression origin; it is not an +ownership or lifetime certificate. Lowering must construct one private checked +association joining: + +- The authentic program/body and concrete ordinary, generic or reflected + context, original alias declaration, immutable binding type and lexical scope. +- The original written View and exact owned producer occurrence, whole-owner + type, checked target/construction facts, conversions and argument order. +- Every finite projection step's checked owner, field identity and endpoint + type, preserving secret/refinement/interface identity and established machine + state qualification without inferring authority from physical layout. +- One dominating generated owning initializer and the nonowning alias chain. + The original whole result moves there once; it is not copied or re-evaluated. +- The source scope and each dynamic activation, current HIR/MIR headers and + remaps, and every normal/abrupt cleanup edge. Validate these before pruning, + including unused aliases and unreachable metadata. + +A matching name, span, type or LocalId cannot substitute for this proof. +Generated storage has private compiler identity, not an invented resolved Source +declaration. Synthetic call/Handle extraction scopes must neither shorten nor +extend its lifetime. Alias reads retain the whole owner's dependency, but +last-use liveness cannot release it early. Only the hidden owner has an owning +cleanup slot. Failed initialization must distinguish vacant, acquired and retired +storage and must not publish an uninitialized alias. + +Reference execution must retain the checked scope-owned obligation; incidental +Value cloning is not lifetime evidence. Native emission uses ordinary owning +storage for the whole producer and nonowning alias storage. Pending ancestors +still fail at the original projection before later effects; no implicit join or +metadata erasure is introduced. Exact checked proofs remain required for every +admitted executable body context. + +## Separate operation-bounded call views + +[Internal call-view staging](native_scoped_call_view_staging.md) keeps its own +temporary through one complete consuming operation. Its BeginCallView/EndCallView +rules are unchanged. Ending an inner call loan cannot retire a persistent local +alias's backing. The new local rule cannot lengthen an existing call stage, and +a call-stage End cannot be used as this source local's scope-exit cleanup proof. +Shared root/projection validation is useful only with distinct lifetime proofs. + +## Bounded ordinary function-body implementation + +The first native implementation recognizes an immutable alias declaration in +the original body of an ordinary nongeneric named function. Its producer is an +exact checked direct call to an ordinary nongeneric named function returning a +finite ordinary struct. The existing typed finite struct-field path selects a +bytes or list endpoint. HIR and MIR cover both endpoint kinds; the checked +reference transport currently supports bytes endpoints only. Reference list +transport remains owed. The native whole-owner proof currently admits finite +primitive/string/bytes/list/struct data; other wrapper, sum, collection, +recursive and runtime-authority owner shapes remain outside this accepted +slice. Nested declaration scopes, loops, generic/reflected bodies and types, +actor handlers, Resource owners and other producer families remain +implementation gaps, not new checker prohibitions. + +HIR preserves the original `CheckedViewSource::Other` fact. It gives the whole +owner private generated storage without a Source DefId and associates the exact +original declaration, producer occurrence, typed projection and function-body +scope with an opaque receipt on the Source call. Canonical producer capture is +single use, after the original canonical passes. The immutable checked archive +supplies the expected inventory before current markers are inspected. Exact +conversion and peer-callable replay preserves that inventory; erasing a current +marker or current function cannot remove its obligation while the archive is +retained. Reusing this archive grants no Resource custody or execution family. +Stripping the entire archive together with every private marker remains the +existing public IR boundary; the controls make no stronger claim. + +MIR retains a separate immutable HIR association and seals complete function +headers and the current graph. It validates one complete acquisition before +alias publication, either a direct call or an authenticated staged call followed +by the existing call-loan Ends and an exact owning move. Definite initialization +precedes a read-independent lifetime pin. That pin keeps the whole owner live +through the original function body's cleanup edges, including after the last +alias read and for an unused alias. The alias has no owning slot. Existing +result emission acquires the complete owned result before ordinary terminal +cleanup; no emitter, Runtime helper or ABI change is needed for this slice. + +Temporary admission requires an actual immutable original `VarDecl` with a +written `view` initializer, its canonical variable definition/name/span and +checked `Other` origin. Viewed enum patterns and loop binders may share that +origin fact, but keep their existing borrowed execution paths and gain no +temporary owner authority. Complete nested AST lookup still detects genuine +written temporary declarations before raw or unsupported-region execution; +those checked-entry and nested-region refusals remain in place. + +Reference execution retains the whole Value in checked activation-owned +vacant/live/retired storage. Payload-free private tickets and nonowning argument +frames access that owner instead of cloning a view into public Value storage. +Original root functions declared through `mutual:` retain the declaration's +DefId. Their later immutable definition is joined through its original resolver +name-span edge to that same DefId, without equating the body span to the earlier +declaration span. Registration still requires the unique original definition, +complete typed AST, canonical name, namespace and checked signature; temporary +receipts also retain the exact physical root, call and activation checks. Method +dispatch remains outside this ordinary-root activation family. + +Explicit clones acquire an independent owned result. An exact successful-output +packet carries the acquired return across scope cleanup, bound to its original +function and activation. The private tests cover these storage and protocol +edges; they do not constitute a complete connected lifecycle trace or native +allocation/destruction observation. + +One genuine checked Source control passes a temporary bytes actual to the +factory before a later handled argument. MIR requires the first call loan +before the sum observation, invokes the factory once, ends that call loan, then +moves the whole Packet into its persistent lexical owner. Its returned explicit +clone retains that owner in the exit plan. A separate genuine child-local +compaction test covers a nonidentity local remap. Disconnected-block fixtures +are synthetic private replay controls: the public Resource graph guard and +pruner refuse their artificial preimage. Only isolated exact temporary replay +restores the original graph and unchanged independent receipts. These tests +grant no genuine Source-generated unreachable-block or public-pruning credit. + +The initial **27 focused tests** pass: reference 7, HIR 10 and MIR 10. A further +ordinary-entry regression passes after repairing interface-method dispatch in +the reference adapter. Only genuine original root declarations receive an +ordinary activation; unrelated methods keep their established dispatch, while +edited claimed roots and ineligible bodies requiring a temporary frame refuse. +Two additional mutual-declaration controls pass with Debug and Release checked +Source. They cover original producer, caller and borrowed callee definitions, +one whole-owner acquisition/retirement, and refusals for edited body/span/header/ +namespace/name facts before acquisition. The full current core-library gate +passes **1,842 tests**: typechecker 379, Comptime 678, HIR 271, MIR 379 and +codegen 135, including the two written-declaration classification controls +below. All compiler focused controls are included in that total. Exact logs +under `target/native/` are `temporary-local-view-focused-v4.log`, SHA-256 +`cd3f32de06fed2008516fe2e3297f1e9a8310b5cfe01accba68caee4390b7251`, +`temporary-ordinary-entry-focused-v1.log`, SHA-256 +`92da6acdfeaa20c57522508a9d12194d30fff5707da15b416338d70cd58cada3`, +`temporary-mutual-entry-focused-v1.log`, SHA-256 +`02ba9c2b11b282641211dcc89aed18d2a34db2d882ca3c8d26492e245ea8200f`, +and `temporary-local-view-core-libraries-v4.log`, SHA-256 +`e1fa74d244fc54b25e3044a349fe321d25c94198694c17e05cae4de872ab13b9`. +The earlier full fixture attempt failed with 546 passes and 57 failures because +the adapter confused mutual declaration and definition spans. Its log is +`temporary-local-view-fixtures-v1.log`, SHA-256 +`8fe22530ee635e56f2ef5b82ade5766dc2444d9b7307d4fe03e9ddf4b4b9e5bd`. +The successor fixture attempt also fails, with 549 passes and 54 failures: +`temporary-local-view-fixtures-v2.log`, SHA-256 +`34eb9d0f7e4d6a4bc4d0ec0f15f66494b99f792aaf3ab1abffbebc9bf037b0bb`. +The written-declaration classification repair corrects the false admission of +existing viewed pattern and iteration binders. All 12 reference call controls +pass, including two new classification controls and the mutual controls: +`temporary-written-view-focused-v1.log`, SHA-256 +`47be7144f0e2a90ea9879053d4cadf00613d300e7ec9957ee8ca7f21cbd9a33b`. +The real JSON namespace smoke test also passes: +`temporary-written-view-namespace-smoke-v1.log`, SHA-256 +`105f1d0a3bd723d623e3c57f568b18bdac2b79e1a29140fd76af403fbfde1ef5`. +The full successor fixture gate passes **603 tests** in +`temporary-local-view-fixtures-v3.log`, SHA-256 +`da41cfcf837bc381f0cfecd23f11e210f9c6f48c5f9f9f9926c20b9ee59c5d65`; +the namespace smoke is included in that fixture selection. All **nine CLI +capture tests** pass in `temporary-local-view-cli-capture-v1.log`, SHA-256 +`317b28bbd05dbfff3f68519d86c2cb52bd7cc80066a67a342cf2f48efca00745`. +These successful successor gates do not convert earlier failed invocations +into acceptance. Other failed or intermediate runs are not accepted gates. +GitHub CI remains disabled at the user's request; these are local results. + +## Bounded native execution and remaining cleanup gates + +`crates/jett_driver/tests/native_conformance/local_view_aliases/09_temporary_root_boundary.jett` +remains byte-exact SHA-256 +`c0f288b659bdf84bf87fb7f46576eb7cadd3b440c000f726b5b1c099ccfd83af`. +It declares `bytes borrowed = view make_packet().data` and never reads the +alias. The selected backing is the whole returned Packet and its owned bytes +until main's lexical scope exits. Expected stdout/stderr and debug events are +empty. Its driver control now requires successful execution rather than the +previous native refusal. The new formatted Source13 includes producer-once +markers, a later forwarded read, an unviewed sibling and explicit-clone +independence. Source13 remains SHA-256 +`48391e6a0fa7390f6d36cb190c8e4100d835218ca317232a64ec8ec228cd59aa`. +Both controls pass checked reference and paired Source Debug/Runtime Debug and +Source Release/Runtime Release native execution after deleting the written +Source file: **two tests, four native executions**. These are paired profiles, +not the full Source/Runtime cross-product. The exact expected stdout, stderr, +status and debug observations pass; Source09 retains empty output. + +The complete refreshed regression selection passes **23 tests and 46 paired +Source-deleted executions**, 23 per Runtime profile, with zero publication +refusals and zero legacy Debug-only executions. It includes the two temporary +controls plus eight stable projected-view, eight scoped-call, two mutable-owner, +one reflected mutable-view and two actor persistent-view controls. Those actor, +reflected and stable regression controls do not establish temporary owners in +those contexts. All 1,056 compiler/Source inputs, 68 reused Runtime artifacts, +64 fixture inputs and 182 original run-pass inputs remain guarded and unchanged. + +The fresh compile-only gate has zero Source executions; the subsequent matrix +provides the execution credit. Receipts under `target/native/` are: + +- `temporary-local-view-gate-compile-v2/gate-acceptance.json`, SHA-256 + `fb39b49856ad89bbdfb5777008a1b9365b6ca4d95a04940a89e5285be4eb6891`. +- `temporary-local-view-native-execution-v2/execution-summary.json`, SHA-256 + `314b6e92a7cd0235bbf92292e49df9e92ebb86490de290982ae23c6865876c0a`. + +Both receipts pin the actual compiled gate image to SHA-256 +`a1f9a700a5219d91bbbf784a5768dc9ec87558a78a72ecd3ae7a9be7a8e28a14`. +The earlier matrix attempt failed and receives no accepted execution credit; +its partial observations are not added to the successful successor. +Empty-output success and producer markers alone do not prove physical lifetime, +sibling destruction timing or absence of hidden cloning. + +Remaining acceptance work for the full contract includes: + +- Preserve Source09 and Source13 as the accepted bytes baseline. Add equivalent + joined checked-reference and paired Source-deleted native coverage for list + endpoints and the remaining whole-owner shapes. +- Exercise nested scopes, skipped branches and loop fallthrough/Continue/Break, + retaining backing after the last read until its original scope exits. +- Acquire an owned `return clone borrowed` or reply before backing retirement; + preserve negative consumption/capture/escape controls and early Handle exits. +- Preserve pending-root and later-failure order, exact terminal diagnostics and + cleanup of partial producers, whole owners, siblings and unrelated earlier + values. Retire each owner once on success and failure without changing error + precedence. +- Extend genuine checked reference and paired Source-deleted native Debug/Release + execution to the remaining contexts. Inspect ownership/drop plans and measure + existing private allocation/destruction counters: producer output cannot prove absence of + hidden cloning. Require no leaked owners after teardown. +- Use authentic proof mutations and runtime fault injection for wrong context, + producer/type/field/scope, stale remaps, missing/duplicate initialization and + early/missing/double cleanup. Refusals grant no execution credit. Preserve the + existing call-stage temporary controls and their independent End behavior. + +The [local-view note](native_local_view_aliases.md) and +[stable projected-view note](native_stable_projected_local_views.md) retain their +bounded implementation status and historical receipts. The bounded +implementation, passing compiler controls and bounded MSVC execution do not +complete the selected temporary-backed contract or whole-language native parity. + +## Accepted bounded List transport + +The [List transport acceptance note](native_temporary_list_views.md) extends the +same lexical whole-owner rule with exact generic observer receipts and List +endpoints. Mutual-body and written-VarDecl authentication, original Source09/13 +and existing byte controls are preserved. Seventeen reference controls and +selected compiler-library/fixture/CLI checks pass (1847/603/9 tests). + +The repaired Source14 passes the focused one-test/two-execution native check and +the complete 24-test/48-execution paired Source-deleted matrix under its fresh +matching gate. The 48 include the focused pair; no additional distinct count is +added. The first authored fixture parse failure retains zero aggregate credit. +All Source/Runtime/fixture/original-input guards pass in the accepted successor. +Physical cleanup and the broader lifetime contract remain open. diff --git a/docs/completed/native_codegen/native_uninhabited_callback_descriptors.md b/docs/completed/native_codegen/native_uninhabited_callback_descriptors.md new file mode 100644 index 00000000..e4058c6f --- /dev/null +++ b/docs/completed/native_codegen/native_uninhabited_callback_descriptors.md @@ -0,0 +1,317 @@ +# Native descriptors with an uninhabited invocation input + +Status: `72237c4a` passes its complete clean 464-case workspace and all 182 +object obligations. Its supported-host workflow remains in progress. The +preceding `55027914` call-view revision passes both its complete workspace and +all four supported-host jobs. The later +[resource kind-tag correction](native_resource_kind_tags.md) has separate +focused acceptance. The native parity objective remains whole-language coverage. + +## Existing source behavior + +An unconstrained empty list supplies the internal uninhabited element type. +An enclosing generic can still create an inline callback whose parameter has +that type, return it, store it, explicitly clone it, mark it pending or drop it. +Creating a callback does not execute its body. Source `list.map` evaluates its +input and mapper normally and returns a real empty output list without ever +producing an element or invoking the callback. Source bodies receive normal +checking before empty-loop lowering. + +The callback descriptor is an inhabited owned value. Its invocation input has +no value and no scalar carrier. An exact checked non-capture parameter equal to +the internal Never TypeId grants descriptor-only emission authority. Captures, +generic markers, source names, display labels and a return-only Never signature +do not grant it. The result may also be Never when an invocation is already +impossible, including one direct secret qualification; no result carrier or +default payload is created. + +## Representation and validation contract + +Preserve original FunctionIds, signatures, modes and capture facts. Original +typed metadata is validated before pruning; ordinary native control-flow and +ownership verification follows normal sequence/handler preparation. Valid +high-level loop bodies must not be rejected by running a prepared-native +ownership checker before that lowering. Malformed unused types, locals, +references, signatures and captures must not be laundered by pruning. + +Original type shape and prepared native value eligibility are separate phases. +An exact Never function result remains valid original metadata even without a +Never input. Existing checked optional/result arms can contain such a closure, +including an impossible capture, whose creation and invocation disappear only +after arm preparation proves them absent. Recursive container and nominal +metadata keeps this distinction; malformed view schemas, foreign child TypeIds +and recovery results still fail before pruning. A surviving return-only Never +function still fails native classification, and an actual descriptor cannot +store a Never capture. Latent owned-capture metadata inside an already +uncallable body is checked separately and creates no environment. This does +not broaden descriptor-only authority. + +Inline extraction shares a dense local table with its enclosing function. The +actual prepared factory retains the extracted child's unused Never formal in +that table, even though the parent has no statement or parameter using it. +Native preparation compacts only unused parent slots, across every original +block and all typed references, after original local and debug type validation. +It retains every real parameter, binding, capture, debug observation and loan +dependency; it does not remove parent blocks or grant a Never runtime slot. +Every surviving callable Never parameter/local/use still fails. The captured +factory MIR and failed first integration gates are retained under +`target/native-uninhabited-callback-local-diagnostic.log`, +`target/native-uninhabited-callback-codegen-rerun.log` and +`target/native-uninhabited-callback-linked-initial.log`. + +A descriptor edge retains storage identity without making the original body or +its arbitrary callees callable. Retained direct or indirect invocations with a +bare Never input remain invalid. Capture evaluation, initialization, exact +types, copyability, lexical order, owned environments, pending depth, labels, +cloning and cleanup keep their existing contracts. + +The existing descriptor layout requires a nonzero code address even for +observation. A descriptor-only identity uses an internal trap entry with only +runtime context and environment parameters and no return. It accepts no Never +scalar, emits no original body effect and returns no invented value. No valid +checked call reaches it. This adds neither a runtime ABI leaf nor a source +exception, implicit authority clone or interpreter fallback. + +## Actual pre-repair characterization + +Root characterized nine exact LF sources using the existing built CLI before +compiler integration. All nine current source-format checks and eighteen +frontend profile checks pass. Eight reference successes match exact expected +stdout and empty stderr; the independent terminal-cleanup source fails after +`before:0:1:5\n` with `runtime error: list.__remove_at: index -1 out of bounds`. +All agent captures have zero debug events. Sixteen native probes refuse bare +Never representation; the two integer-control probes reach object generation +and then the deliberately unavailable runtime bundle. All eighteen output +sentinels survive. These are characterization results, not linked acceptance. + +| Case | Reference application stdout | +| --- | --- | +| Direct inline descriptor through an empty map | `direct:0:-1\n` | +| Returned captured descriptor through an empty map | `captured:0:-1\n` | +| Integer callback bodies actually invoked | `direct:2:7:captured:3:3\n` | +| Descriptor-list cloning and getter ownership | `owners:0:1:1\n` | +| Depth-two pending descriptor through an empty map, then two joins | `pending:0:0\n` | +| Live descriptor/list/bytes cleanup at an independent terminal error | `before:0:1:5\n` | +| Both callback input and result uninhabited | `identity:0\n` | +| A normal loop body with absent and really invoked callbacks | `loop:0:-1:1:3\n` | +| Equality inside absent and really invoked callback bodies | `equality:0:false:1:true\n` | + +Captures and source hashes are under +`target/native-next-after-owner-views`, +`target/native-uninhabited-callback-controls-witness`, +`target/native-uninhabited-callback-never-result-witness` and +`target/native-uninhabited-callback-loop-body-witness` and +`target/native-uninhabited-callback-binary-witness`. +The loop source's initial wrong mutable-binding spelling failed with E1000; +that source and its captures are retained under `initial-invalid-mutable`. +It is not evidence of a native gap. The canonical corrected source supplies +the current accepted characterization above. + +The equality witness passes ordinary source checking and preserves its Binary +operation in HIR. Descriptor metadata checking must accept exact Never operands +for equality and inequality with a bool result without requiring a scalar +carrier. This permission applies only to an already proven uncallable body; +phantom literals, mismatched operands/results and callable Never inputs retain +their rejection. It does not select arithmetic or ordering behavior for Never. + +The initial linked baseline compiles the first eight driver cases and reports +one passing integer control and seven failures at the existing Never boundary +(3.24 seconds, 439 predecessor cases filtered out). The baseline is preserved +in `target/native-uninhabited-callback-linked-baseline.log`. It proves the +regressions reproduce the gap, not that the repair passes. + +## Required acceptance + +Meaningful compiler units must preserve typed descriptor storage, actual integer +callable behavior and the original-MIR negative contracts. Invalid IDs, wrong +signatures/view modes, Never captures, stale capture counts/types, false callable +edges and return-only shortcuts must still fail. Source body validity must +remain independent of empty-loop emission. + +Linked cases require both matching runtime profiles, both builds before source +deletion, exact stdout/stderr/status, empty unintended observations and normal +owned-value cleanup. The independent failure must exit 71, never cleanup status +72. Existing full-stdlib, compiler/driver, frontend, workspace/object and +supported-host gates remain separate obligations for the actual repair head. + +The accepted predecessor source `cb617ecf` passes a complete clean 439-test +workspace and all 182 object obligations. The documentation-only `194ce065` +revision passes all four supported-host build/package and relocated installed +jobs. Those results do not accept the current callback repair. +The fixed inventory remains 207 and the broad estimate remains about 85%. + +## Focused acceptance + +All 966 crossed compiler library tests pass in +`target/native-uninhabited-callback-phases.log`: 52 codegen, 416 comptime, +139 HIR, 87 MIR, 48 resolve and 224 typecheck. The new units check descriptor +entry/signature/storage and original loop/equality/capture metadata, enclosing +foreign local/debug types and surviving Never reads/initializers, plus named +control-flow, ABI order and disconnected-block dependencies during compaction. + +All nine linked cases pass together (3.65 seconds, 439 filtered) in +`target/native-uninhabited-callback-linked.log`. Both matching runtime profiles +build before source deletion; exact application bytes, status, observations and +cleanup pass. The independent owned-list failure retains status 71 and its +original error; cleanup status 72 cannot pass. Nine source-format checks pass in +`target/native-uninhabited-callback-source-format.json`; Rust formatting passes. +The supplemental corpus now registers 448 cases. Registration is separate from +the accepted predecessor's complete 439-case execution. + +All 83 driver library tests pass (69.44 seconds) in +`target/native-uninhabited-callback-driver.log`, followed by all 598 frontend +fixtures (24.60 seconds) in `target/native-uninhabited-callback-frontend.log`. + +Initial isolated unit sources incorrectly called the unloaded `list.length` +facade. They were corrected to a declared pure consuming helper, while the +driver cases keep full-stdlib calls. The initial codegen and MIR logs are +preserved; they are not acceptance results. The subsequent actual factory-MIR +diagnostic exposed the transferred parent slots and selected the compaction +repair before the passing gates above. Ordinary callable body checks remain +independent of descriptor-only metadata; unrelated unsupported latent operations +are not claimed complete by these nine cases. + +## Full-run regression and original function metadata repair + +The exact clean `d14b830b` full native suite finishes with 446 passes and two +failures (941.02 seconds) in `target/native-workspace-d14b830b.log`. Both existing +sum-arm regressions fail with `type is unsupported in function value +result (Never)`: original preflight demands a runtime representation for a +`function() returns Never` closure before sum preparation removes its impossible +arm. This is distinct from named local compaction. After the complete native +suite reported these failures, root stopped the remaining object/workspace +targets; the wrapper records terminal exit -1, unchanged clean head and the +reason in `target/native-workspace-d14b830b.json`. That run grants no complete +workspace or 182-object acceptance. + +The repair adds a recursive typed OriginalMetadata/NativeValue distinction. +Only exact Never function results gain the original-metadata exception; +native scalar classification, descriptor proof and capture eligibility remain +strict. Three new codegen units cover absent optional success, result success +and result error closures, omitted closure symbols/environments, valid nested +metadata, malformed nested local/debug types and live return-only rejection. + +All 969 compiler library tests pass in +`target/native-original-function-metadata-phases.log`: 55 codegen, 416 comptime, +139 HIR, 87 MIR, 48 resolve and 224 typecheck. Both previously failing linked +tests pass (48.51 seconds, 446 filtered) in +`target/native-original-function-metadata-linked-sum-arms.log`; all nine callback +tests pass again (3.82 seconds, 439 filtered) in +`target/native-original-function-metadata-linked-callbacks.log`. The existing +linked tests retain both runtime profiles, source deletion, pending-error order, +comptime observations and native verify/property execution. The corpus remains +448 cases. The exact clean `90d82701` complete workspace subsequently passes +all 448 native cases (944.48 seconds), all 182 object obligations (553.77 seconds), +remaining targets and doc-tests. The wrapper reports unchanged clean start/end, +exit zero and 92 successful groups at `2026-10-03T08:31:51.9056689Z` in +`target/native-workspace-90d82701.log` and `.json`. Workflow 37108099164 remains +the independent supported-host gate for that revision and passes all four build/ +package and relocated installed jobs, last completing `2026-10-03T09:03:23Z`. +The about-85% estimate, +fixed 207 inventory and whole-language objective remain unchanged. + +## Latent nested closures and direct secret results + +The selected pre-Rust contract is +`target/native-uninhabited-latent-callback-draft/contract.md` (SHA `422abde8`). +The exact non-capture Never input remains the sole descriptor-only authority. +The retained outer descriptor may contain a nested zero-argument closure that +captures this absent input; creating or calling that inner closure is only +latent source behavior. A direct Secret[Never] result of the same uncallable +callback similarly needs no result carrier. Concrete instantiations retain +ordinary closure environments, invocation and explicit declassification. + +The verifier follows validated FunctionRef, ClosureRef and function-adapter +edges from the already proven metadata root. Exact target identity, signature, +view/capture modes and capture-local types are checked before a visited +FunctionId set prevents cycles and repeated body validation. Every followed +original body is checked without granting native reachability or a frame. +Composite bodies use OriginalMetadata classification. Only exact Never and +one direct Secret[Never] gain the absent-value metadata treatment; no arbitrary +qualification/refinement is peeled. Actual created captures, live return-only +callbacks, callable Never edges, phantom literals, malformed nested metadata +and unsupported opaque Resource layouts retain their existing rejection. +The source policy, native trap ABI, runtime and hidden-secret rules are unchanged. + +### Characterization and focused execution + +Three exact LF sources have actual default-profile reference/agent status zero, +empty stderr/debug observations and literal stdout: + +| Case | Application stdout | +| --- | --- | +| Nested closure in an absent callback plus invoked integer control | `nested:0:1:7\n` | +| Cloned/stored secret-result descriptor plus explicit integer declassification | `secret:1:7:1\n` | +| Concrete nested/secret-only control | `control:1:7:9\n` | + +Before the repair, all three source-format and six frontend profile checks +passed. The first two sources refused Never function-result representation in +both native profiles; the concrete control reached the deliberately unavailable +runtime bundle. All six output sentinels survived. Fresh characterization +with the isolated repaired CLI repeats all three format/default reference/agent +and six frontend checks, preserves exact source hashes/output, and now reaches +object generation and the unavailable bundle for all six native probes. These +probes are object-admission evidence, not linked execution. Exact captures and +CLI provenance are in `target/native-uninhabited-nested-secret-callback-witness` +and `target/native-uninhabited-latent-callback-after`. + +All **63 codegen library tests pass** (0.30 seconds) in +`target/native-latent-callback-codegen.log`. Six new source-derived units pin +latent symbol absence with retained outer/concrete symbols, direct secret result +descriptor classification, original-body phantom result rejection, exact owned +capture edges, malformed nested local/debug types and live return-only rejection. +The combined linked wrapper passes all **27 tests**: three new cases (6.95 +seconds), nine existing descriptor cases (18.01 seconds), two sum-arm cases +(87.04 seconds) and thirteen scoped call-view cases (25.69 seconds). Both matching +runtime profiles build before source deletion, then compare exact status/output +and empty unintended observations. The wrapper exits zero with all seven formatted +compiler/test source hashes unchanged at `2026-10-03T09:53:15.5452645Z` in +`target/native-latent-callback-linked.json` and its four logs. The three new Jett +source-format checks pass. The isolated corpus registers 464 supplemental tests; +registration is not complete-workspace acceptance. + +The broader compiler run passes **989 library units**: 63 codegen, 416 comptime, +139 HIR, 99 MIR, 48 resolve and 224 typecheck. It also passes 62 codegen object +integration tests and one MIR integration test, for **1052 actual tests** with +Cargo exit zero. Its wrapper +exits one solely because the expected count incorrectly excluded the 63 +integration tests; all seven source hashes remain unchanged. Preserve that +wrapper/log as a count-guard failure, not a compiler failure or full-workspace +acceptance. Its actual passing tests do not require repetition. + +The corrected remaining-gates wrapper passes all 83 driver tests (89.77 +seconds), 598 frontend fixtures (24.61 seconds) and eight backend-lowering +tests (168.01 seconds). It exits zero with the same seven source hashes unchanged +at `2026-10-03T10:01:25.0163025Z` in +`target/native-latent-callback-remaining-phases.json` and its three logs. Rust +formatting checks pass. The earlier compiler count-guard record remains +`target/native-latent-callback-phases.json` with the actual passing Cargo log +`target/native-latent-callback-libraries.log`; it is not silently replaced. + +The exact clean committed follow-up `72237c4a` subsequently passes the complete +workspace: 464 native cases (954.38 seconds), all 182 object obligations in the +four-test manifest gate (542.22 seconds), 83 driver tests (72.45 seconds), 598 +frontend fixtures (22.91 seconds), remaining targets and doc-tests. The wrapper +exits zero with 92 successful groups, clean start/end and unchanged exact head at +`2026-10-03T10:40:27.4609702Z` in `target/native-workspace-72237c4a.log` and +`.json`. Supported-host +[workflow 37115479516](https://github.com/vycdev/jett/actions/runs/37115479516) +is still in progress; that platform acceptance remains independently required. + +### Accepted predecessor and separate follow-up + +The accepted `90d82701` complete 448-case workspace, 182 object obligations, +92 result groups and all four supported-host jobs above remain historical +exact-head evidence. Main `55027914` now passes its complete clean workspace: +461 native cases (915.93 seconds), the four-test manifest covering all 182 object +obligations (534.62 seconds), 83 driver tests (78.52 seconds), 598 frontend +fixtures (22.20 seconds), remaining targets and doc-tests. The wrapper exits +zero with 92 successful groups, clean start/end and unchanged exact head at +`2026-10-03T09:56:47.1797084Z` in `target/native-workspace-55027914.log` and +`.json`. Supported-host +[workflow 37112967414](https://github.com/vycdev/jett/actions/runs/37112967414) +also passes all four Windows/Linux build/package and relocated installed jobs, +last completing `2026-10-03T10:23:08Z`. This accepts the staging revision on those +hosts; the later `72237c4a` platform run remains separate. The whole-language +objective stays 100%, broad tracking stays about 85%, and the fixed 207 +inventory/182 object obligations are unchanged. diff --git a/docs/completed/native_codegen/owned_mutable_field_updates.md b/docs/completed/native_codegen/owned_mutable_field_updates.md new file mode 100644 index 00000000..f4c14dc5 --- /dev/null +++ b/docs/completed/native_codegen/owned_mutable_field_updates.md @@ -0,0 +1,125 @@ +# Owned mutable field updates + +Status: selected contract, 2026-10-15. The user delegated the choice to fit Jett's +philosophy; checked local field updates are the selected rule. Implementation and +verified acceptance are tracked in the [native acceptance audit](native_acceptance_audit.md). +This contract note grants no execution evidence. + +Jett is imperative, immutable by default, with explicit local `mutable` bindings. +An update to one field should identify that logical change without reconstructing +every unrelated field. `record.field = value` is the sole field-write spelling; +there is no mutable reference, setter hook, structural cast or alternate builder. +Its value semantics are replacement within one owned local, conceptually consuming +the old root and rebinding the updated root. A consuming function can update its +own mutable owned parameter and return it; it cannot mutate the caller's view. + +## Admitted places and invariants + +An assignment place is an exact resolved live mutable owned binding followed by a +finite checked field path. Parentheses do not create a different place. Ordinary +struct paths retain exact nominal/generic owner and field identities. A machine +field is eligible only when existing state qualification or checked narrowing +establishes its exact state and field. The state and tag are unchanged: an update +is not a transition. A nested record or exact-state machine is still part of the +same root ownership obligation. Mutable actor state may use the same rule only +through its existing owned handler binding, never through a remote actor handle. + +Compiler-produced reflection metadata remains inspect-and-pass data. A field +path must not enter the canonical builtin TypeInfo, TypeField, TypeBitfield, +TypeBitfieldField, TypeMachine, TypeMachineState, TypeMachineTransition or +TypeVariant owner to alter its fields. Capture these exact builtin identities +when their type definitions are created; preserve the marker through type-table +cloning and canonical remapping, and revalidate it in every checked path. Aliases +and secret qualification do not erase this boundary. A user record may replace +a complete metadata field with another genuine metadata value; unrelated user +structs with similar names remain ordinary values. + +This preserves the existing compiler-produced metadata contract and constructor +gate. Allowing edits to a genuine metadata record would permit manufacturing +different metadata after that gate. Separate mutable-observer provenance would +add another authority mechanism; the selected local user-value update rule does +not introduce it. This authority preservation was selected before its Rust +implementation on 2026-10-15; it is not an execution or bypass claim. + +Immutable bindings, views and view-derived aliases, temporary/call-produced +targets, uninitialized or consumed roots, and owners with outstanding dependent +views cannot be written. A `mutable` flag is not ownership proof. Aliases and +generic/scoped contexts must be resolved before deciding whether the place is +owned; qualification must not be peeled to manufacture permission. No caller +mutation or escaping mutable borrow is introduced. + +Do not traverse through an enclosing nominal refinement to change its fields. +Reconstruct and validate the complete refined value, then replace that value. +This applies to a refined root and a refined intermediate owner, including +aliases to either. A field whose own type is a refinement may be replaced as one +whole value under the existing checked refinement-conversion/handling rules; +its proof may not be assumed from the base carrier. Unrelated sibling refinements +remain intact. Updates must invalidate obsolete value/proof facts for the changed +place and affected root; no stale nonzero, narrowing or reflected-owner fact can +authorize a later operation. + +Bitfields retain constructor-and-rebind updates. No bitfield projected write, +implicit mask/truncation or new width-validation rule is introduced. Enum payload +updates use ordinary owned bindings obtained through existing match semantics; +this contract adds no direct variant selector or state transition spelling. + +## Readiness, order and replacement + +Static ownership/place violations are compile errors. Pending depth is runtime +data as well as checked task state: existing admitted transport stays admitted. +Updating a pending root or intermediate owner uses the existing typed pending- +inspection refusal; no implicit join, payload observation or new blanket Source +rejection is added. Existing static pending-use restrictions remain intact. +Once an ancestor is ready, an untouched or newly supplied child may retain its +own pending depth under the existing transport rules. + +Resolve the fixed place and check ancestor readiness/state before evaluating the +RHS. Evaluate the RHS exactly once using existing Source order and type, view, +move and refinement rules. It may read the old root through a temporary view that +ends before replacement, but cannot consume or overwrite the root needed for +commit. The root identity/generation and path must still be valid at commit. +An RHS failure or early handler exit changes no target field and retires every +newly acquired temporary exactly once. Allocation/validation that can fail must +finish before old storage is displaced. + +On success, transfer the RHS into the selected field and retire the displaced +field exactly once. Preserve every other field, exact concrete/generic identity, +state, secret qualification and task depth without implicit Source cloning, +declassification, predicate replay or Resource authority. No partially moved +root is visible. Existing explicitly cloned values remain logically independent; +shared physical storage must use safe reconstruction or copy-on-write rather +than expose writes through an old clone or view. + +If displaced cleanup itself fails, retain the established terminal cleanup/error +precedence and retire remaining/new obligations once; do not pretend the old +value was restored or publish an unvalidated replacement. Resource-bearing +updates require their authenticated custody/loan/replacement protocol. Ordinary +record support cannot bypass that protocol; incomplete Resource transport stays +an implementation obligation, not a new semantic exclusion. + +Ordinary structs and exact-state machine owners have bounded checked reference, +required-comptime and native acceptance, including named/generic, inline and +reflected-iteration bodies. Checked actor-handler updates to ordinary owned state +have separate bounded reference and MSVC native acceptance under original handler +identity and persistent-state ownership. See the [actor handoff](actor_handler_field_updates.md) +for its exact controls and limits. Authenticated Resource-bearing replacement, +wider reflected/generic and frozen-graph compositions, real asynchronous +scheduling, and full workspace/platform acceptance remain separate obligations. +The native audit keeps the executed matrices distinct. + +## Verification handoff + +Start from `crates/jett_driver/tests/projected_assignment.rs` and +`tests/native/projected_read_controls.jett`. Preserve original rejection and +read/reconstruction witnesses. Add genuine normally checked execution controls +for local/owned-parameter nested scalar and owning fields, clone independence, +failure-before-commit, exact machine state, actor handler state, refinement and +bitfield rejection, and consumed/borrowed/temporary roots. Include pending root, +intermediate owner and stored-child cases without joining implicitly. Check stale +nonzero/refinement facts, views, generic/alias context, branch/loop lifetimes and +exact cleanup. Host-built carriers do not establish Source acceptance. + +Reference, required comptime, native main and applicable verify/property roots +must consume the same checked place contract. Record actual Source-deleted linked +Debug/Release results, Source policy and optimization separately. Full affected +phase, workspace and supported-host acceptance remain independent gates. diff --git a/docs/completed/secret_debug_output.md b/docs/completed/secret_debug_output.md new file mode 100644 index 00000000..41a5b970 --- /dev/null +++ b/docs/completed/secret_debug_output.md @@ -0,0 +1,33 @@ +# Secret values in trace and breakpoint output + +Decision: trace and one-line breakpoint snapshots render each `secret[T]` +value as the fixed marker `[redacted]`. The rule applies through aliases and +refinements and recursively inside collections, sums, structs, enums, machines, +and reflected construction builders. Public fields retain their normal debug +representation. The marker is independent of the secret payload, including +its pending depth; a pending wrapper outside a secret retains its ordinary +debug representation. Formatting never consumes or declassifies a value. + +This decision does not relax the interactive debugger's metadata-only +inspection policy for secret-bearing values, nor the print/serialization +boundaries. Runtime diagnostics that render typed values must use the same +redaction when native reflection reports them. + +## Implementation and evidence + +Interpreter debug formatting uses binding and reflected field types rather than +untyped `Value` formatting. Inferred loop, match, and handler bindings retain +metadata; concrete generic invocations supply types for ambiguous source spans. +Native debug layouts use a zero-payload Redacted node and do not dereference the +secret payload. Builder metadata carries the same layouts. Checked aliases +survive in binding labels separately from runtime canonical type identity. + +`tests/native/debug_secret_values.jett` compares interpreter/native output for +scalar, pending, alias/refinement, generic, collection, recursive aggregate, +closure, inferred-binding, pipeline, and builder cases. Seven secret-bearing +pending reflected owner/builder failure fixtures compare error output. Runtime +tests prove redaction ignores invalid hidden handles and large pending depths, +does not confer equality, and does not mistake public marker text for a secret. + +Generic task joins preserve the instantiated error type in handler observations, +including when one source body is called with both secret and public errors. diff --git a/docs/design.md b/docs/design.md index 26b61fe0..84f39d0a 100644 --- a/docs/design.md +++ b/docs/design.md @@ -59,7 +59,22 @@ semantic AST, resolver facts, and typechecker facts. HIR does not reinterpret source spellings: it carries resolved declaration targets, checked types, explicit ownership operations, capabilities, trusted source origin, and source provenance. Generic function identity is the canonical declaration plus its -concrete type arguments. +concrete type arguments and the checker-owned reflection-visible +specialization facts. The latter keep source aliases and passed reflection +facts distinct when canonical type interning intentionally gives them the same +`TypeId`. +Scoped types bound by `comptime type` keep their source reflection kind as well +as their canonical type. A nested generic call or static `type.kind_tag` branch +must still see an alias as an alias, even when its underlying `TypeId` is a +struct or collection. Inline function identities also carry their enclosing +`comptime type` binding chain, so callbacks sharing a source body and signature +remain distinct when reflected field types differ. Nested closures retain that +lexical chain, while named function calls start their own. Expression types +inside each binding belong to its concrete expansion, including when captured +by a closure; another field's expansion cannot determine arithmetic width. Scoped identity +also retains alias-visible reflection metadata: an alias and its base may +share a value type while selecting different reflected bodies. This applies +to field loops, `TypeInfo.args` loops, and nested argument metadata. Named call arguments reach HIR only as a checked permutation into canonical parameter order. Struct construction likewise carries canonical field order @@ -78,8 +93,10 @@ uses dense variant identities, and exhaustive `match` arms bind payload locals explicitly rather than preserving source patterns. Accepted compiler-owned calls become typed intrinsic HIR nodes only after the -typechecker authorizes them; HIR records canonical identity and evaluation -order without reapplying trusted-source policy. Inline functions, indirect +typechecker authorizes them; HIR records a closed `IntrinsicId` and evaluation +order without reapplying trusted-source policy. That ID is shared by MIR, +comptime/runtime dispatch, and native codegen, whose exhaustive matches make +new compiler-owned operations an explicit cross-phase decision. Inline functions, indirect calls, bitfields, state machines, and actor operations likewise use explicit backend-neutral nodes rather than retaining source AST fragments. @@ -216,6 +233,12 @@ There must be **no spooky action at a distance**. A variable must never be silen - Semantic program side effects must be declared in the function signature via **capability parameters** (see Rule Set 16). If a function writes to a file, it receives a `Filesystem` capability. If it accesses the network, it receives a `Network` capability. The signature is the contract. Compiler-owned debug observations such as `trace`, `breakpoint`, `print`, and `println` are non-release tooling instrumentation, not semantic I/O. - All inputs to a function come through its parameters or an anonymous function's explicit capture environment. No global ambient reads and no thread-local storage. Anonymous functions may capture only implicitly copyable values from the enclosing scope; each captured value is copied into the closure. Move-only values must be passed explicitly as parameters. This allows patterns like `list.find(users, function(u: User) returns bool: return u.id == target_id)` when `target_id` is copyable. +Supported namespace constant values are materialized during compilation and +baked into the binary. Reading one substitutes its checked immutable value; it does not +add an ambient runtime input, startup initializer, or mutable global storage. +The supported primitive subset and remaining ownership questions are recorded +in the [global constant contract](open_design/global_constant_execution.md). + **Example — side effects are declared, not hidden:** ``` @@ -515,8 +538,85 @@ User user = User(name: name, email: email, age: age) handle error: return fail("invalid user data: {error}") ``` +A field that already has its refinement type carries the established invariant +into the struct constructor. The constructor still has the same fallible result +type and `handle` requirement. + +An unchanged refined value also retains its invariant when bound to a local or +copied from a field. Promotion from an established ancestor checks only the +remaining descendant constraints, in base-first order. Explicit `coarsen` +removes the discarded proof; another refinement with the same underlying +representation supplies no proof. Validation uses the checked source type, +including the success payload type when a result or optional is handled. +Runtime labels alone cannot establish an invariant for replaced payloads. + +Checked function parameters and returns retain the same established invariant. +A return entering a new refinement must validate its unproven constraints; +the function's return annotation alone does not prove the returned expression. + +A reflected field read must also establish newly requested refinements before +its result type supplies proof. For already admitted requests, +`type.field_value`, `type.variant_field_value`, and `type.machine_field_value` +use the same rule in direct calls and pipelines. Existing owner, member, field, +requested-type, and pending-selector checks finish before producer validation. + +New invariants may occur recursively inside `list`, `set`, `map`, `optional`, +and `result`. The read completes structural preflight over all occupied +payloads before running any new predicate. Changed wrappers that request +refinements must be ready; absent optional payloads, inactive result arms, and +empty collections supply no child to validate. Predicate checks follow +collection order, checking each map key before its value, and establish child +invariants before an enclosing root refinement. The first structural or +predicate error stops the read. Primitive pending children reach their ordinary +predicate boundary rather than acquiring an earlier container-readiness error. +The read never implicitly joins a pending value and preserves its original +source; validation operates on an independent candidate. + +Only the field's actual checked declared schema supplies an established proof. +Exact schemas and requested named ancestors retain pending depth without +rerunning their predicates. Promotion or a sibling root branch with an +established common named ancestor checks only its unproven suffix. A changed +builtin wrapper still requires readiness even when every child has an exact or +ancestor proof: a named root over `list[Positive]` read as `list[Positive]` is +such a changed wrapper. Requests without a refinement do not acquire this +producer-validation traversal. Runtime labels and a requested annotation alone +cannot establish an invariant. + +A nominal type's ordinary payload fields are not its generic arguments. For +example, a named `Element` with a `Positive` field does not make +`list[Element]` a request for a new nested producer refinement. An actual +`Holder[Positive]` or unused `Marker[Positive]` argument does; transparent +aliases retain the same canonical argument proof. Each requested generic +argument compares independently, so repeating an alias or refinement does not +itself imply recursive expansion. + +These checks preserve the separate requested-type admission contract. They do +not select arbitrary nominal or callable casts, nested erased-interface +adapters, or new proofs across secret qualifiers. Existing exact declared +proofs remain separate from those changed-conversion refusals; broader +admission remains in +[reflected field request compatibility](open_design/reflected_field_request_compatibility.md). + +Generic struct fields use the constructor's selected concrete type arguments, +resolved in the caller. Field declarations then resolve in the struct's own +namespace. A caller's unrelated generic parameter or namespace alias cannot +change a field's refinement or the constructor's result type. + **Refinement type constraints must be self-contained.** The `where` clause can only reference `value` (the value being constrained) and call pure functions with literal or constant arguments. Constraints cannot take external parameters — there is no `type Password[min: int64] = string where string.char_count(value) > min`. This keeps `[]` unambiguous: it always means generics, never parameterized constraints. +Constraint names resolve in the refinement's declaration namespace, including +private pure helpers and type names. Caller locals, namespace aliases, generic +arguments, and scoped type bindings cannot change their meaning. The constraint's +`value` has the fully coarsened base type, with an outer `secret` wrapper removed +for validation; inherited interface refinements dispatch through that base. +Validation preserves the caller's context whether the constraint passes or fails. + +If a refinement's base is an optional or result, its `handle error:` validates +the whole sum, including an absent or failure branch. It does not extract the +payload. An outer optional/result containing an already refined value still +uses ordinary sum extraction. Refinement-validation errors are strings even +when the base result has a different error type. + **For parameterized validation, use functions.** If validation rules depend on runtime values (e.g., a minimum password length from config), write a regular function that returns `result[T, string]`: ``` @@ -548,6 +648,31 @@ NonEmpty ne = coarsen password # Password → NonEmpty string raw = coarsen password # Password → string (skips to base) ``` +The same nominal rule applies when a refinement's base is an interface. If the +refinement explicitly implements that interface, passing the refined value to +an interface parameter retains the refinement's implementation. Coarsening it +to the base interface restores the underlying value's concrete implementation; +coarsening to an intermediate refinement retains that ancestor's implementation. +When a `result` or `optional` and its payload both implement a destination +interface, conversion preserves the whole sum and selects its implementation. +Extracting the payload still requires explicit `handle`. + +Ordinary conversion to a secret-qualified interface and an admitted reflected +field read preserve the underlying nominal method owner. For example, +`secret[Selected]` converted to `secret[Named]` dispatches through `Selected` +after explicit declassification. Matching outer secret qualifiers protect the +destination; they do not name its implementation. An unqualified interface +destination instead retains an explicitly secret-owned implementation, such as +`Named` implemented for `secret[string]`. Additional source secret layers and +nominal refinements keep their own identities, and nested secret payloads remain +redacted. The broader requested-type admission rule for reflected fields remains +unresolved; see [reflected field request compatibility](open_design/reflected_field_request_compatibility.md). + +Coarsening or declassifying a value passed as a view preserves that borrow. +The callee observes the underlying payload without consuming the refinement or +secret owner. This also applies to borrowed fields and function-value calls; +the original value remains available after the call. + If you need the base type multiple times in one function, assign it to a local variable once: ``` @@ -819,7 +944,7 @@ Config config = read_config(view fs, "app.conf") handle error: The `handle error:` block executes only when the result is `fail`. If the result is `ok`, the coarsened value is bound to the variable on the left (`config`). The error variable is always available inside the block. -**Every handle block must end with either `return` or `default`** — there is no implicit value. This rule applies to both `handle error:` (for `result`) and bare `handle:` (for `optional`). A handle block that does neither is a compile error: +**Every handle block must end with `return` or `default`; inside a responding actor handler, it may instead end with `respond`** — there is no implicit value. This rule applies to both `handle error:` (for `result`) and bare `handle:` (for `optional`). A handle block that does neither is a compile error: ``` # result — COMPILE ERROR: @@ -1088,14 +1213,14 @@ A mutable variable that gets reassigned on lines 5, 12, and 23 requires the LLM Immutable variables eliminate this entirely. `trimmed_name` has one value, forever, from the line it is defined. There is no timeline to track. The LLM's attention head links the name to one definition and one value — done. -**Mutability is local only — no mutable references.** There is no way for a function to modify the caller's data. When a value is passed to a function, it is either consumed (moved) or borrowed read-only via `view`. There is no `param: mutable T` — no mutable references exist in the language. If a function needs to transform a value, it takes ownership, transforms it, and returns the new value. The caller rebinds: +**Mutability is local only: no mutable references.** A function cannot modify a caller's data through a view. Values are consumed or borrowed read-only; no mutable reference escapes. A consuming transform returns its new value for the caller to bind: ``` mutable int64 x = 5 x = transform(x) # transform consumes x, returns new value, x is rebound ``` -This guarantees that reading a mutable variable's current value never requires looking at function implementations. The only place `x` can change is at explicit rebinding statements in the same scope. +An owned mutable local may also replace a checked field with `record.field = value`, including finite nested paths. This is an explicit local owner update, not mutation through a shared reference. Whole-name and field assignments are the only statements that can change that local value; calls receiving views cannot do so. See [owned mutable field updates](completed/native_codegen/owned_mutable_field_updates.md). ### Rule Set 7: Syntactically Enforced Modularity (Chunking) @@ -1339,6 +1464,16 @@ heterogeneous wildcard-list results. `list.flatten[T]` accepts `list[list[T]]`, and `list.sort_by_index[T]` accepts and returns `list[list[T]]`, so their source signatures state their actual shapes. +`list.sort[T]` accepts numeric, `string`, and `bool` elements, including +transparent aliases and refinements over those primitives. It consumes and +returns the same `list[T]` type: sorting reorders existing values without +coarsening their public type or creating new values that require refinement +validation. Native carrier selection follows refinement bases while retaining +the checked list identity. This does not extend summation to refinements. +`list.is_sorted[T]` and `list.sort_by_index[T]` compare numeric keys at their +checked width. Floating sortedness follows IEEE `<=`, so an adjacent NaN is not +ordered; sorting preserves the interpreter's equal-comparison behavior for NaN. + The complete public byte-buffer surface is compiler-shipped source in `stdlib/bytes.jett`. A `bytes` value is move-only and has no implicit aliasing: read-only operations (`length`, `get`, `slice`, `to_string`, and `to_hex`) @@ -1527,6 +1662,14 @@ kernels. `math.abs`, `math.min`, and `math.max` retain only their narrow compiler-owned `int64`/`float64` type-policy gate, not public runtime dispatch or general overloading. +`math.average[T]` and `math.median[T]` accept all ten primitive numeric types, +including transparent aliases, and return `float64`. They consume their list +input and convert its checked values to binary64 before the existing aggregate +algorithm; binary32 inputs widen after their declared-width rounding. Empty and +pending inputs retain the current kernel failures. Nominal refinements and secret +wrappers are not numeric specializations of these APIs. This does not broaden +the separate `abs`/`min`/`max` type policy. + **Hashing and encoding — no application dependencies:** > The stable text-digest API, algorithm classifications, reserved HMAC shape, @@ -1798,7 +1941,9 @@ accepting or returning bare `UserAuth`, the value must be guarded again before it can flow back into a `UserAuth at logged_in` parameter. This erasure is permanent and signature-directed: a function declared `returns UserAuth` returns a bare value even when its implementation returned `UserAuth at -logged_in`; the compiler does not preserve hidden caller provenance. Guards +logged_in`; the compiler does not preserve hidden caller provenance. A mutable +local explicitly declared as bare `UserAuth` keeps that full type when rebound +to another valid value of the same machine. Guards over arbitrary paths such as `holder.session at logged_in` are state tests only; narrowing is permanently limited to bare local variables, so paths do not narrow later field accesses. @@ -1947,6 +2092,10 @@ Garbage collection is too slow for native-speed code. C-style manual memory (`ma When a variable is passed into a function, it is **consumed** (moved) and immediately becomes invalid in the current scope. If the LLM tries to use it again on the next line, the compiler rejects it. If the LLM wants to keep it, it must explicitly clone. +Wrapping an owned value with `ok(value)`, `fail(value)`, or `some(value)` also +consumes it. Use `clone value` inside the wrapper when the original must remain +available. + ``` function send_message(view net: Network, connection: Connection, payload: Payload) returns nothing: Network.send(view net, connection, payload) @@ -1990,7 +2139,7 @@ The `clone` keyword is explicit and visible. The LLM (and any reader) can see ex **Auto-view for field access:** -Field access on a value implicitly creates a view of the parent. `self.x` is equivalent to `(view self).x`. Accessing `user.name` does NOT consume `user` — it reads the field through an implicit view. Only passing the entire value to a function (as an owned parameter) consumes it. +Reading a field implicitly views its parent: `self.x` reads through `(view self).x` and does not consume `self`. A field read grants no write authority. A selected field assignment instead requires a live mutable owned root and the checked replacement rules below; it neither creates a mutable view nor permits changing a caller's borrowed data. This means code like the following is valid: @@ -2008,7 +2157,11 @@ primitive: `bytes` is primitive but move-only, as are compound owned values. **Rebinding semantics for mutable variables:** -The `mutable` keyword allows a variable name to be rebound after its previous value is consumed. This is not mutation — it is consume-and-rebind: +The `mutable` keyword permits a local name to receive a replacement value. Whole-name assignment consumes/rebinds the old value; a checked field assignment performs the corresponding replacement within that same owned root. Both preserve single ownership. Immutable names and views remain non-writable, and an enclosing nominal refinement must be reconstructed and validated as a whole. + +For whole-name Resource replacement from a distinct owned value, evaluate the RHS once and keep it owned while cleaning up the previous value. Publish the replacement only after successful old-value cleanup. An RHS failure preserves the previous value when the RHS has not itself consumed it. If old cleanup fails, retire the staged RHS exactly once, preserve the established cleanup/error priority, and execute no later statement. This whole-name order is separate from the selected field-update publication rule. + +A direct `optional[R]` or `result[R, E]`, where `R` is an opaque resource, remains a live owned sum when it contains `none` or `fail`. Absence is not an uninitialized or moved binding; `fail` also owns its ordinary error value. A live shell or payload view prevents replacement even when the shell holds no Resource token. Replacement validates and moves the exact ordinary error shape without an implicit clone; an explicitly cloned ordinary value remains valid. A pure replacement whose old and incoming sums are both absent requires no new capability or Runtime-purpose permission. Occupied Resource operations keep their existing capability restrictions, including in required evaluation, Verify and Property bodies. ``` mutable int64 total = 0 @@ -2056,6 +2209,147 @@ source declares a nominal runtime-owned handle as `resource Name`, for example is move-only, cannot be cloned, compared, serialized, or reflected as a value, and cannot be disguised as a fieldless struct. +Type-only reflection exposes the nominal name and `TypeKind.resource_type`. +A resource has no primitive tag; its field, variant and machine-state metadata +are empty. This metadata does not expose a resource value or constructor. A +named type alias retains the ordinary `alias_type` reflection tag. + +Private trusted resource operations bind to the exact associated resource and +Function declaration identities, loader-owned stdlib origin, and complete +checked parameter ownership modes and result type. Ordinary privacy and +declaration order still apply; a matching name or type cannot grant authority. +The production catalog remains empty and no public provider API is exposed. +The private reference interpreter can execute the exact checked compiler-test +hooks with an internal scripted provider. This does not enable a shipped +Resource API or native Resource execution. Known absence is admitted only +through the exact checked aggregate shape; it grants no cleanup obligation or +provider authority. Ordinary refinement predicates remain required even when +another field is an absent Resource container. The checked boundary and current +reference evidence are recorded in the +[active resource-hook note](completed/native_codegen/native_resource_hook_identity.md). + +Generated For element/map-value and Match payload bindings can retain a checked +inactive Resource-bearing shape without acquiring cleanup ownership. Reference +execution authenticates the original control, selected binder/ordinal, active +body and exact type/mode facts, then validates the complete selected parent and +child before installing an empty-custody binding. View iteration retains its +original parent binding; unrelated live owners keep their existing tickets. +This supplies no occupied aggregate custody, provider authority, refinement +predicate shortcut or native aggregate execution. All 554 comptime tests pass, +including the unchanged Source17 replay and 60 focused entry observations. +The bounded contract and +acceptance record are in the +[generated-binding note](completed/native_codegen/native_resource_absent_generated_bindings.md). + +The compiler can retain one immutable checked resource program by owning the +parsed source and deriving its resolution and checking internally. Parse, +resolution and checking errors prevent that handoff, including errors unrelated +to an otherwise valid private hook. Complete hook identities and signatures are +validated even when unused. Ordinary checking still returns source diagnostics +for repair. This compiler proof does not install a provider or authorize a live +resource operation; runtime execution must retain the same checked program. +Reference execution retains that same immutable merged program and the primary +source main's exact span and namespace through interpretation. It does not +reread or rediscover modules after preparation. Earlier warnings remain ordered +observations when a later source phase rejects the program. The public build +result stays unchanged, and required comptime/verification failures still +prevent execution. This is a compiler handoff, not live Resource authority. +The [checked-program handoff](completed/native_codegen/native_resource_checked_program.md) and +[reference preparation note](completed/native_codegen/native_prepared_reference_driver.md) record +the bounded implementation and remaining runtime prerequisites. + +The checker refuses copying a Resource-bearing field into ownership, including +through borrowed parents, and refuses cloning collection/reflection getters +whose returned data contains a Resource. Explicit field views and reads of +unrelated copyable fields remain valid. Owning optional/result, list/map, struct +and machine payloads must receive owned Resource data, not a known view hidden +inside a constructor. For a Resource-bearing exact machine state, its transition +source also obeys the existing owned-argument boundary. These static guards +preserve owned transfers, empty containers and type-only metadata; they do not +install a runtime carrier or provider. Exact Resource roots cannot be ordinarily +printed or reflected as a value. Nested printing remains a separate boundary, +including hidden secrets, known absence and omitted values. See the +[implemented ownership boundaries](completed/native_codegen/native_resource_ownership_boundaries.md). + +Reference Resource execution retains the same successful checked program, +exact declaration and concrete body facts through argument evaluation. Caller +ownership follows Rule24 independently of callee access. An opaque physical +carrier is not an owner: a separate non-cloneable ticket transfers the one +cleanup obligation through scopes, temporaries, calls and occupied results. +The implemented reference tests cover reverse cleanup, explicit close, owned +returns and failure paths before context teardown. Descriptor creation is data; +a reached hook requires runtime purpose and matching provider authority. +Required comptime, verify and property execution do not acquire that authority. +Direct scoped bodies, concrete generic type metadata, pipelines and mutable +assignment retain their exact original checked body, step or statement facts. +Repeated entry restores its metadata and body cursor before another invocation. +Broader captured/reflected/worker contexts, ordinary refinement predicates, +occupied aggregate custody and native drop elaboration remain required; the +bounded tests do not establish those complete domains. + +Checked Resource lowering now preserves the original immutable program's +manifest, nominal kinds and exact hook signatures through HIR and MIR. Typed +descriptor and invocation nodes retain original caller facts and lexical actual +order. Raw lowering cannot create those identities, and all manifest entries, +including unused hooks, are validated. A separate Source-authenticated ownership analysis now records scope, operation +and provisional-return frames, exact owner slots, loans, transfers, occupied sum +arms and cleanup obligations. It retains the original checked HIR and complete +original type meanings; edited public HIR cannot recreate that authority. An +original bare owned Resource actual passed to a view formal belongs to an owning +argument holder and a bounded loan, rather than an ordinary acquisition record. +This analysis is compiler metadata. Native execution still requires the emitted +layout, connected custody ABI and cleanup on every control-flow edge. +The compiler now derives the native execution family from that authenticated +original program, including resource-free direct callers of Resource functions. +It projects the fresh ownership plan into immutable layout bytes and the exact +driver-selected entry tuple. These projections provide consistency metadata; +they do not grant runtime authority or enable native Resource body execution. +A separate ordinary companion plan borrows that fresh custody proof and its +exact current function. It retains complete Source acquisitions, ordinary type, +initialization, liveness, alias and move checks while excluding Resource carriers +from ordinary ownership storage. Exact current expression identity selects +already validated custody roles; identical cloned expressions cannot select them. +Public ordinary move/copy/caller planning still refuses Resource authority. + +Native lowering also retains exact returned compiler-hook descriptors through +immutable bindings, aliases and root Return relays. Immediately invoked returned +callees execute once after the original actuals. Installation-owned hook +metadata carries no Resource cleanup obligation and is distinct from the +ordinary named Function object. Its exact original/current producer, body, +header and Return proof selects the hook; a public callable type or matching +signature cannot do so. Canonical remaps authenticate an independent body proof +before pruning, and runtime invocation still validates installation, hook, +signature and exact physical operation frame. Pure helper dependencies retain +their ordinary ABI. The local native/reference corpus passes all 46 scenarios +in both profiles; dynamic, captured, scoped/generic, required/comptime and +returned named callbacks remain separate work. See the +[returned hook implementation](completed/native_codegen/native_returned_resource_hooks.md). + +The private native runtime custody core separately tracks non-cloneable owned +and borrowed tokens, checked frame destinations and reverse acquisition +cleanup. Its protocol tests cover generation/provenance refusal and +cleanup after failure or panic. A private bounded registration path validates +all layout rows before issuing context-bound nominal kinds and slot metadata. +Registration installs no payload, provider, grant or owning token. A separate +private context adapter now connects that registration to the custody core with +exact frame, owner, loan, sum and prepared-operation records. Scripted providers +and a paired Network grant are available only to compiler tests; production +state remains absent and providers disabled. Completion cleans obligations +before recording the outcome, preserving observed body status and separate +ordinary, Resource body and cleanup errors. Ordinary Result companions use the +existing recursive destruction path. The private runtime now validates complete +v2 Source-call metadata and exports exact custody leaves for child Scope entry, +owned actual transfer, resident views, provisional Return publication and absent +or failed sum carriers. It checks every owned actual and reserves the callee +acquisition log before any transfer. Return cancellation cleanup takes priority +over body failure. The generated entry can read the original body status from +its exact retired root Scope while the same Runtime attempt remains active; +this read does not reset errors or replace the separate cleanup outcome. +Ordinary optional/result companions validate each selected payload's physical +ownership before recursive destruction. These runtime transitions still require dedicated compiler +body emission, ordinary companion cleanup, a matched runtime archive and linked +Source execution; production providers remain disabled. + Moving a resource transfers its single cleanup obligation. A `view` borrows it without taking cleanup responsibility. An explicit close consumes the owner; otherwise the compiler invokes the same trusted finalizer exactly once when the @@ -2140,6 +2434,17 @@ function main(stdout: Stdout) returns nothing: - `ask` delivers a message and waits for a response. Used when the sender needs a value back. - Because variables are linear (Rule Set 10), when a value is sent to an actor, it is consumed in the sender's scope. **No two threads can ever hold the same mutable data.** Race conditions are structurally impossible. +A receive handler keeps its captured state and capabilities available at every +normal exit. Moving state into a helper is valid when its owned replacement is +installed before the handler exits. A response may copy a Copy value or transfer +an owned message/local value; a borrowed value cannot escape as a reply. Mutable +owned state may use the same checked field-update rule as an ordinary owned +local. The no-shadowing rule also applies to message parameters and state names. +A `respond` at the end of a Handle block exits the receive handler; it does not +supply a fallback value or commit an interrupted field assignment. The current +implementation and unresolved response-exit cases are tracked in +[checked actor handler field updates](completed/native_codegen/actor_handler_field_updates.md). + **How actors receive capabilities:** Actors receive capabilities at spawn time. The capability is moved (or cloned) into the actor and becomes part of the actor's private state. The actor can then use the capability in its receive handlers without threading it through messages. @@ -2239,6 +2544,14 @@ function fetch_all_data(view net: Network) returns result[DashboardData, HttpErr - Every `run` must have a matching `join` or `cancel` before the enclosing function returns. If the LLM forgets one, the compiler rejects the code. - No orphaned tasks. No background processes silently running after the function ends. +The checked outer type selects the result of `join`. An exact `result[T, E]` +retains that result type, including through a transparent alias. Other operand +types produce `result[Operand, string]`; this includes secret-qualified results +and nominal refinements of results. Their inner result remains payload data for +its own handler. Each join removes one outer pending layer and preserves pending +depth inside the payload. `join view value` produces an independent joined +value while preserving the original owner's pending depth and availability. + **Cancellation through capabilities:** `cancel` requires a still-pending task variable; a value that has already been joined on any live branch cannot be cancelled. Unlike cancellation, `join` also accepts resolved values. `cancel` sets a cancellation flag on a task. The task is not killed immediately — instead, its next capability checkpoint terminates the pending task with a `CancelledError` before the operation takes effect. This task-control failure is surfaced by `join`; it is separate from the interrupted function's declared `result[T, E]` error type. No cancellation tokens or manual flag checking are needed — the capability system provides natural cancellation checkpoints: @@ -2347,6 +2660,15 @@ syntax, or preprocessor. **One function form, two execution times.** Required compile-time value evaluation is always explicit and has the canonical form `comptime expression`. The expression must be closed: it may use literals, types, and pure functions, but it cannot depend on a runtime parameter or local. +Block-local `use` aliases may name those functions and types; an inner block may +shadow an alias without changing a later expression in the outer block. +It receives the same expected type as an ordinary expression in that position, +so `comptime ok(value)` and `comptime none` can use a declared sum type's context. +Inside a generic body, evaluation uses each checked instantiation's type +arguments and lexical `comptime type` bindings. Source aliases remain visible +to reflection even when their canonical types coincide. A type parameter is +available during evaluation; a runtime value parameter or closure capture is +not. An uninstantiated generic template has no concrete value to evaluate. Compilation fails if the compiler cannot produce the value. An ordinary pure call remains a runtime call in source semantics. The optimizer may fold it when safe, but that invisible optimization cannot introduce diagnostics, suppress @@ -2707,7 +3029,7 @@ Jett borrows from Zig's approach: **tests are written directly below the functio #### 1. Co-located `verify` Blocks -Every function can have a `verify` block immediately after its definition. The `verify` block contains test cases that are **contracts** — they are not optional quality checks, they are compiler-enforced proofs that the function behaves correctly. +Every function can have a `verify` block immediately after its definition. The `verify` block contains test cases whose assertions are checked during compilation. Adding a block is optional; passing its checks is required once it is present. These checks establish the asserted behavior for the executed cases, not correctness for every possible input. **Basic example:** @@ -2738,6 +3060,13 @@ The word `test` implies something optional — something you run separately, may `verify` blocks are not regular tests that run at runtime. They are executed by the **comptime engine** (Rule Set 10) during compilation. If any assertion in a `verify` block fails, the program **does not compile**. +The native verification audit additionally executes the primary file's checked +verify bodies through one generated suite in declaration order. Its entry +contains only zero-argument calls returning `nothing`; it does not change +compile-time eligibility or admit additional source bodies. When required +values retain a protected checked table, suite construction preserves every +existing body and renews custody only for that canonical appended entry. + **What happens during compilation:** ``` @@ -2758,8 +3087,8 @@ verify add_positive: The binary is **never emitted** if a `verify` block fails. This means: -- Every function with a `verify` block is **proven correct** (for the tested inputs) before the program ever runs. -- Bugs are caught at compile time, not at runtime, not in CI, not in production. +- Every assertion in a `verify` block has passed for its executed inputs before the program ever runs. +- Failures exposed by these assertions are caught during compilation. - The LLM gets immediate feedback: write function → write verify → compile → if verify fails, fix → compile again. **Comptime verification limitations:** @@ -2773,7 +3102,7 @@ propagate through the call graph. For impure functions, Jett provides #### 3. The Full Pattern: Function → Verify → Next Function -The idiomatic Jett file follows a strict rhythm: define, verify, define, verify. Each function and its proof live together as a unit. +The idiomatic Jett file follows a strict rhythm: define, verify, define, verify. Each function and its example checks live together as a unit. ``` function celsius_to_fahrenheit(c: float64) returns float64: @@ -2801,11 +3130,11 @@ verify is_boiling: assert is_boiling(200.0) == true ``` -Each function is immediately followed by its contract. When the LLM generates `celsius_to_fahrenheit`, it writes the verify block while the formula `c * 1.8 + 32.0` is still the most recent thing in its context. By the time it moves on to `fahrenheit_to_celsius`, the previous function is fully verified and can be trusted. +Each function is immediately followed by its checks. When the LLM generates `celsius_to_fahrenheit`, it writes the verify block while the formula `c * 1.8 + 32.0` is still the most recent thing in its context. After a successful build, the assertions for that function have passed; behavior outside those assertions and inputs remains untested. #### 4. Verify Blocks and Refinement Types -`verify` blocks work with refinement types (Rule Set 3) to create a powerful proof chain: +`verify` blocks complement refinement types (Rule Set 3) by checking examples within the type's constraints: ``` type Percentage = float64 where value >= 0.0 && value <= 100.0 @@ -2824,7 +3153,7 @@ verify calculate_grade: assert calculate_grade(1, 3) == 33.33 within 0.01 ``` -The return type `Percentage` guarantees the result is between 0 and 100. The `verify` block proves specific input/output pairs. Together, the type system and the verification contracts provide two layers of correctness: the type constrains the range, the verify proves specific behaviors. +The return type `Percentage` constrains returned values to the range from 0 to 100. The `verify` block checks specific input/output pairs. The type's range constraint and these example checks establish different facts; neither alone establishes that every result matches the intended grade calculation. **Float comparison with `== ... within`:** @@ -2850,7 +3179,7 @@ The LLM generates code → the compiler runs verify blocks → if they fail, the **3. Tests are contracts, not afterthoughts.** -Verify blocks are optional — but when present, they are compiler-enforced. If a verify block exists, the function is proven correct (for the tested inputs) before the binary exists. The natural workflow encourages writing them: generate function, generate verify, move on. +Verify blocks are optional — but when present, their assertions must pass before the binary exists. A passing block establishes only its asserted behavior for the executed cases. The natural workflow encourages writing them: generate function, generate verify, compile, move on. **4. The LLM never writes a test for code it can't see.** @@ -3008,6 +3337,38 @@ struct User: The `secret[string]` type is not just a label — it is a distinct type that the compiler enforces differently from `string`. A `secret[string]` **cannot** be used anywhere a `string` is expected. +An expected outer `secret` type can also qualify contextual `list(...)`, +`map(...)`, `some(...)`, `none`, `ok(...)`, and `fail(...)` literals. The +constructor keeps its exact inner container and payload types before receiving +the checked outer qualification. Nested secrets, nominal refinements, and +producer identities remain intact; interface, function, and inferred `never` +payload conversions keep their existing rules. This lowering correction adds +no language policy and does not relax native constructor shape checks. + +Checked struct constructors can likewise receive outer secret qualification. +The constructor retains its exact nominal struct type before qualification, +including generic arguments and field conversions. Constructors that validate +refinement fields keep their existing `result[Struct, string]` contract. +Outer secret qualification can wrap that result without handling it or skipping +its refinement predicates. Already-refined fields still produce the checked +result shape even when they need no additional predicate evaluation. + +Checked bitfield and machine constructors also retain their exact inner type +before outer secret qualification. A literal bitfield constructor produces its +nominal bitfield value; one requiring width validation still produces +`result[Bitfield, string]`, including when that result is secret. Qualification +does not skip width checks or consume the result. A machine constructor retains +its exact machine and selected state, with payloads evaluated in source order. + +Field reads from a directly secret-qualified struct, bitfield, or state-qualified +machine preserve the exact nominal field type and add outer secrecy. `nothing` +remains `nothing`; an already-secret field, including a nominal refinement over +a secret value, retains its declared type without another wrapper. + +Local bindings retain their declared outer secret qualification when an +initializer has fewer secret layers. Each `declassify` removes one outer layer; +the initializer keeps its checked type and existing contextual inference. + **What the compiler rejects:** ``` @@ -3106,10 +3467,12 @@ Payload length is observable under this initial contract, so a length mismatch may return `false` immediately. Equal-length payloads use a vetted constant-time byte comparison with no content-dependent early exit. This gives constant work for fixed public lengths; it does not claim to hide length. -Future HIR, MIR, and native backends must preserve those semantics with a -vetted constant-time primitive or lowering that cannot be changed into -content-short-circuiting equality by optimization. This boundary was resolved -by [#33](https://github.com/vycdev/jett/issues/33); native lowering remains part +HIR, MIR, and native backends must preserve those semantics with a vetted +constant-time primitive or lowering that cannot be changed into +content-short-circuiting equality by optimization. The Cranelift native path +uses the same constant-time byte primitive as the interpreter for supported +string and byte secrets. This boundary was resolved by +[#33](https://github.com/vycdev/jett/issues/33); full native parity remains part of [#20](https://github.com/vycdev/jett/issues/20) and [#22](https://github.com/vycdev/jett/issues/22). @@ -3367,15 +3730,51 @@ not a second output API. They may emit diagnostic text without a `Stdout` capability. They remain secret-output boundaries and their text cannot be read by Jett code or treated as a semantic program result. -The current interpreter shares their path with `Stdout.write`; separating debug -events is pending. When the release/backend boundary is implemented, release -builds must reject `print` and `println` with guidance to use `Stdout.write`; -they must not silently strip calls or lower them to ambient process stdout. -Future native or bytecode backends may support the calls only through an -explicit debug diagnostic channel, and must otherwise reject them. `verify` -and comptime execution may use the helpers only when tooling isolates their -debug events from protocol output. Production output always requires -`Stdout`, while structured debugging should prefer `trace` or `breakpoint`. +Non-release native debug printing supports the same public aggregate and opaque +values as the interpreter, including empty containers and absent sums. Explicit +`view` arguments remain borrowed; ordinary arguments retain their consumption +rules. All arguments are evaluated before the call emits text, so a failing +later argument cannot leave partial output from that call. Accepted non-release +`print` and `println` recursively redact hidden secret subtrees as `[redacted]` +using the retained concrete type and builder field metadata. The reference +fallback for missing composite or field metadata redacts that subtree rather +than exposing raw payloads; it does not prove checked metadata propagation is +complete. Native malformed-schema guards remain errors. Rendering +invokes no user getter, Displayable method, or callback and inspects no captures +or actor state. Direct secret argument restrictions and Source release E0362 +remain unchanged. Pending-value terminal diagnostics remain separately open in +`open_design/debug_print_hidden_secrets.md`. +Finite recursive values retain their full typed debug representation; nesting +past a fixed implementation depth must not turn valid observation into a runtime +error. This also applies to trace and breakpoint snapshots, preserving pending +wrappers, field order, and recursive secret redaction. + +Tooling captures `print`, `println`, `trace`, and `breakpoint` in one ordered +debug-event stream, separate from capability-backed application stdout. Each +event retains its known kind and exact text: `print` adds no terminator, +`println` adds one newline, and embedded newlines remain part of the original +event. Human runtime entrypoints and native executables emit debug bytes to +diagnostic stderr. Agent entrypoints escape event text in structured rows; +printed text resembling a trace or protocol header does not change its kind. + +Compiler observations retain their phase: explicit comptime evaluation, +frontend verification, or runtime execution. Comptime events stay separate from +baked values, so reading a baked value does not replay its observations. +Verification captures each block and its actual property trials; shrinking and +private replay do not become additional observations of the original failure. +Successful and failed tools retain events already produced before a later +failure. They do not rerun evaluation to recover those events. + +Comptime error recovery also records attempted expression/context pairs apart +from successful baked values. Repeated checked contexts and namespace initializer +markers therefore do not repeat a failed evaluation, its diagnostic, or its debug +events. A failed value remains unavailable; distinct contexts still evaluate. + +Release builds reject `print` and `println` with E0362 and guidance to use +`Stdout.write`; calls are never silently stripped. Future backends must provide +an explicit debug diagnostic channel or reject the calls. Production output +always requires `Stdout`, while structured debugging should prefer `trace` or +`breakpoint`. This exception does not grant user code an output capability. A capability-free function remains free of semantic I/O: debug observations are @@ -3545,7 +3944,7 @@ function start_server(view net: Network, view stdout: Stdout, port: int64) retur This code does not contain a single OS-specific reference. No `#ifdef`, no `cfg!()`, no `#[target_os]`. The LLM writes against the `Network` capability interface. The compiler does the rest. -**What the compiler does at build time:** +**Planned cross-platform provider mapping (not the current host-only implementation):** ``` jett build server.jett --target linux-x86_64 @@ -3637,6 +4036,11 @@ If a genuinely platform-specific behavior is needed (rare, and only for advanced #### Build Targets +The following aliases and cross-compilation examples describe the roadmap. +The implemented CLI accepts only its exact host triple: +`x86_64-unknown-linux-gnu` or `x86_64-pc-windows-msvc`. Other targets, +comma-separated targets, and shorthand aliases are rejected. + ``` # Build for the current platform: jett build server.jett @@ -3713,6 +4117,38 @@ The compiler makes every JSON-data struct compatible with `json.serialize[Type]( The LLM does not write parsing functions. The LLM does not import a serialization library. The compiler sees the struct definition and generates everything. For fields that need custom naming (e.g., mapping to camelCase APIs), Jett uses an inline `serialize` keyword on the field itself (see Custom Field Naming below) — not a separate annotation syntax. +A self-recursive JSON-data struct uses the same `json.serialize`, `json.parse`, +and `json.parse_exact` contracts as a non-recursive struct. Its finite base +value ends runtime traversal; the source decoder and serializer may call their +own checked specialization again for a nested value. + +Typed JSON parser calls also use ordinary pipeline semantics: +`raw into json.parse[T]()` and `raw into json.parse_exact[T]()` apply the +same target type, compiler-owned JSON policy, and step-local error handling +as their direct-call forms. + +JSON policy preserves the uninhabited type inferred for unconstrained empty +collections or absent sum slots. An admitted empty list, absent optional, +one-sided result, or string-keyed map does not need a default element or +payload type merely to serialize it. The ordinary rules still check every +inhabited slot, including nested fields and secret projection; an empty value +does not exempt function, actor, interface, capability, resource, or other +unsupported targets from public JSON policy. There is no inhabited `never` +value or new source spelling. Generic source bodies remain checked before +impossible iteration or sum extraction is omitted. Carrier evaluation, +ownership, pending-state errors and cleanup remain observable even when no +payload can be read. + +Typed parsing preserves those inferred slots too. An empty array, absent +optional, or supported arm of a one-sided result may decode into its otherwise +admitted carrier. An input that tries to populate an uninhabited slot returns +the existing reflected-decoder error, including its field, index or result-arm +prefix; it cannot manufacture a Never payload. Raw JSON syntax, shape checks +and parse_exact validation keep their ordinary order. The error-producing +decoder still executes even though its success type is uninhabited. Checked +source specialization supplies that behavior independently of optimization; +ordinary pure-call folding cannot make an ill-typed decoder body valid. + **Using auto-generated serialization:** ``` @@ -3805,6 +4241,33 @@ projection. `json.parse[Machine]` accepts any declared state in the envelope, while `json.parse[Machine at state]` requires the envelope to carry that exact state. +JSON admission for `Machine at state` checks that state's payload. A +non-JSON payload in another state does not block an otherwise valid +state-qualified request. A bare `Machine` request checks every declared +state because its value or parsed envelope may select any of them. +Qualification does not remove states from reflection metadata. Aggregate +probes still inspect the top-level kind: a top-level alias is non-matching +and exposes empty metadata. Field-level `TypeInfo` preserves its source +alias identity. + +Private source decoders use a visible canonical reflection condition, +`type.name[T]() == "json.JsonTree"`, to select raw-tree copying. This +checked type condition keeps the raw return body out of unrelated field +specializations, including callable, interface, and actor payloads in an +unselected state. An ordinary pure helper call does not authorize skipping +typechecking by being folded; optimizations cannot change source validity. +This source repair does not admit those payloads as public JSON targets. + +Public JSON object maps require string keys, including transparent aliases +that resolve to string; non-string map targets are rejected before +evaluation. If a malformed qualified-machine document names another +declared state's non-string map payload, the private decoder reports an +unsupported-key failure when it reaches that field. It does not convert +keys before the later qualified-state construction check. Required-field +lookup still runs first, and exact parsing retains its separate validation +before decoding. The canonical envelope and valid target behavior are +unchanged. + The serialized shape includes the state tag: ``` @@ -4723,6 +5186,12 @@ duplicate declarations inside the merged namespace are still compile errors. This exception is not available to project files or vendored dependencies, and project code still cannot reopen a stdlib namespace such as `json`. +A bare call to a declared same-namespace function resolves to that function +even when its name is also a standard module prefix, such as `math` or +`validate`. Dotted calls such as `math.abs(-4)` still resolve through the +standard module. The module name itself cannot be redeclared as a project +namespace. + **Third-party namespace collisions:** If a vendored library declares `namespace auth` and your project also has `namespace auth`, this is a compile error. Since dependencies are vendored source files, you own the copy — rename the namespace in the vendored file (e.g. to `namespace authlib.auth`), then `use authlib.auth as auth` at the call site. Library authors should use prefixed namespaces (e.g. `namespace mylib.auth` instead of just `namespace auth`) to minimize collisions. #### Namespace Visibility And Exports @@ -5281,7 +5750,7 @@ function make_color(r: int64, g: int64, b: int64) returns result[ColorChannel, s return ok(color) ``` -There is no field-level mutation — bitfields follow the same ownership rules as structs (Rule Set 6). To change a field, construct a new bitfield value. +Bitfields retain constructor-and-rebind updates: to change a field, construct a new bitfield value and apply its existing width and refinement checks. The owned mutable field-update contract for ordinary records does not introduce bitfield projected writes or implicit masking/truncation. This is consistent with refinement types (Rule Set 3) — compile-time values are checked at compile time, runtime values are checked at runtime and require error handling. @@ -5315,11 +5784,20 @@ A `list[uint8]` field captures everything after the fixed-size fields as a raw b |--------------------------|----------------------------| | `(value >> 4) & 0x0F` | `header.version` (4-bit field) | | `value \| (1 << 5)` | `TcpFlags(..., ack: 1, ...)` — construct a new TcpFlags with the modified field | -| `value & ~(0xFF << 8)` | Direct field assignment — compiler handles masking | +| `value & ~(0xFF << 8)` | Construct a replacement bitfield value with checked field widths | | `htons(port)` / `ntohs(port)` | Compiler handles byte order based on `network` modifier | | `memcpy(&header, buffer, sizeof(header))` | `Header.from_bytes(buffer)` | | `0x1F`, `0b00011111`, `31` | A field width: `field: 5 bits` | +Bitfield updates use construction and whole-value rebinding. For ordinary records, the selected [owned mutable field-update contract](completed/native_codegen/owned_mutable_field_updates.md) permits checked finite field paths rooted in a live mutable owned binding. Immutable/temporary/view roots remain rejected, as do paths through an enclosing nominal refinement. Checked reference, required-comptime and native execution implement ordinary-record and exact-state machine updates under that contract. Checked actor-handler updates to ordinary owned state retain original handler identity and persistent-state ownership, with bounded MSVC acceptance recorded in the native audit. Resource-bearing updates remain a separate obligation. + +Compiler-produced reflection metadata retains its inspect-and-pass contract. +Projected writes cannot enter the exact builtin metadata record identities to +change their fields, including through an alias, secret qualification or a user +holder. A user holder may replace a complete metadata field with another genuine +metadata value. This preserves the compiler's construction/authority boundary; +ordinary user records with similar names remain writable under the local rule. + No hex literals, no binary literals, no shift operators, no mask operators. The LLM works entirely in base-10 integers and field names. #### Byte Order Annotation @@ -5418,7 +5896,7 @@ Jett introduces one concept: the **view**. A view is a read-only, non-owning ref ``` function count_items(view data: list[Item]) returns int64: - return list.length[Item](data) + return list.length[Item](view data) function total_price(view items: list[Item]) returns float64: mutable float64 sum = 0.0 @@ -5455,7 +5933,7 @@ The `view` keyword appears in **both** declarations and call sites. The function ``` function count(view data: list[int64]) returns int64: - return list.length[int64](data) + return list.length[int64](view data) # Keep the value — view at call site: int64 len = count(view items) @@ -5493,6 +5971,187 @@ function bad_consume(view data: list[int64]) returns list[int64]: A view is a borrow — the caller still owns the data. Any operation that would consume (move) the value is a compile error. The function can read the view and pass it as `view` to other functions, but cannot take ownership. +Returning a known move-only view as an owned value, or passing it to an owned +parameter, reports E0401. This includes view parameters, explicit local views, +forwarded aliases, parentheses, and move-only results of `coarsen` or +`declassify`. Ordinary, generic, indirect, and pipeline calls obey the same +boundary. An explicitly written `view` argument to an owned parameter retains +E0375. Actual implicitly copyable results, valid explicit clones, ordinary owned +field copies that do not contain Resource data, and owned call results remain +valid. A Resource-bearing field cannot be acquired by copying its owner or view; +use an explicit nonowning field view or a genuine owned transfer instead. + +Field assignment through a known view reports E0401 even when its binding is marked `mutable`; an immutable or temporary root reports E0404. Checked finite field paths through a live mutable owned record, or an already exact-state-qualified machine owner, use the selected [local replacement contract](completed/native_codegen/owned_mutable_field_updates.md). Outstanding dependent views, consumed roots, paths through enclosing nominal refinements and bitfield writes are rejected. An update preserves exact owner/state/field metadata and does not change a machine's state. Pending ancestors retain the existing typed runtime inspection refusal before RHS effects; no implicit join or blanket new pending-transport Source restriction is introduced. Ordinary-record, exact-state machine and checked actor-handler updates to ordinary owned state have bounded reference and native acceptance. Actor updates retain original handler identity and persistent-state ownership; Resource-bearing updates remain open. See the [acceptance audit](completed/native_codegen/native_acceptance_audit.md#checked-actor-handler-field-updates-bounded-msvc-acceptance). +Move-only enum payloads extracted by matching a view retain their borrowed, +read-only origin. Forwarded payload aliases, parentheses, explicit `coarsen` +and `declassify` do not acquire ownership. Matching an owned enum retains owned +payload transfer; implicitly copyable payloads retain ordinary copying. +Rebinding an owned mutable value from a known move-only view reports E0401; +use an explicit clone to acquire ownership. Copyable values retain implicit +copying. + +Native lowering keeps direct and forwarded local aliases immutable. A stable +owned local or owned parameter may back the alias, including a mutable owner +when its exact owned fact is available in the active checked context; an +immutable view parameter remains a valid backing. Mutable view-parameter +backing remains a native implementation limit. This bounded mutable-owner +extension has focused MSVC acceptance recorded in the local-view note. Finite +ordinary-struct, bitfield and exactly state-qualified machine field paths retain +the same typed origin proof. +Each field step preserves its checked owner, +field identity, and endpoint type. The endpoint's existing secret, refinement, +or interface identity is not erased to establish the path. Bytes and lists are +examples of this rule, not an endpoint-type allowlist. + +The same read-only view rule permits a finite field path through a declared +`Machine at state` payload. Its native initializer proof must resolve the +exact machine and declared state, then preserve the canonical field identity +and endpoint type. Ordinary structs and state-qualified machines can be +intermediate owners in the same path. Every machine step must already carry +that exact state type in the checked path, and the terminating local must +preserve its actual declared type. This bounded proof does not recover state +qualification from a flow-narrowed bare backing slot. +The bounded native proof continues to refuse transition, consumption, or +rebinding of an owner after a reachable alias has been created. Nominal +refinement and secrecy qualification retain the same proof requirements +as ordinary-struct views. + +A declared bitfield participates in the same read-only path rule. Its native +proof resolves the exact bitfield declaration and canonical zero-based field +identity, then preserves the declared semantic field type. This includes the +existing final `list[uint8]` payload; numeric fields retain their implicit-copy +behavior. Ordinary structs, bitfields and exact-state machines can be +intermediate owners. This adds no payload shape, hidden owner, implicit clone +or lifetime rule; see the [bitfield contract](completed/native_codegen/native_bitfield_projected_local_views.md). + +The native path proof also retains the existing directly secret-qualified +aggregate field rule. A receiver can carry one direct `secret[Owner]` layer, +where Owner is the exact declared struct, bitfield or machine state. Its field +keeps the declared type when it is `nothing` or already secret, including a +nominal refinement over secret data; other fields gain exactly one outer secret +layer. The actual qualified receiver remains in the path. No multiple-layer +owner peeling, implicit declassification or nominal substitution establishes a +view. Explicit borrowed `declassify` and `coarsen` retain their existing proof +and owner dependency; see the [secret-owner contract](completed/native_codegen/native_secret_owner_projected_local_views.md). + +The original owner remains live for dependent reads; projected and forwarded +aliases are non-owning and carry no cleanup obligation. Explicit clones acquire +independent ownership, and ordinary owned field initializers retain their copy +behavior. Carrier-preserving `coarsen` and `declassify` aliases retain the same +backing owner and existing representation checks. Temporary-backed source local +aliases beyond the bounded ordinary function-body slice below, mutable alias +chains, allocating conversions, and owner consumption or rebinding after source +alias creation remain native implementation limits. A +read-only alias grants no write authority, and a retained dependent loan blocks +changes to its owner, including after its last alias read. Selected field +updates to a live mutable owned local without a conflicting loan follow the +checked replacement contract. The producer retains the +transitive binding dependencies of a reflected body's actual local aliases from +that concrete checked map. It preserves each original fact and adds no owning +authority. Absent or inconsistent root facts still fail native admission; flat +global facts cannot supply a missing proof. Focused MSVC acceptance is recorded +in the local-view note. Borrowed-sum Handle aliases retain their separate +checked backing receipt described below. These implementation limits do not establish +general loan expiry, change the checker rules above, or claim every projected +owner kind is supported; see the +[local view implementation note](completed/native_codegen/native_local_view_aliases.md) and +[typed stable-field contract](completed/native_codegen/native_stable_projected_local_views.md). + +For an immutable local alias of an ordinary owned temporary, the selected +lifetime rule evaluates the whole exact producer once and retains it in +compiler-owned storage until the declaration's original lexical scope exits. +Immutable forwarding borrows that same backing. There is no last-use expiry, +implicit clone or fabricated Source binding identity. Aliases own no cleanup; +the whole backing retires once on every scope exit, after any owned return, +reply or Default result has been acquired. Copyable endpoints keep their +existing copy behavior. Escape and dependent-loan rules, checked field types, +pending checks and failure order are unchanged. This contract does not grant +actor-state or Resource custody and does not extend operation-bounded call +loans. A bounded implementation retains such backing for original ordinary +nongeneric function-body declarations with a checked direct named producer +returning a finite ordinary struct and an exact typed bytes/list field path. +Checked reference transport currently supports bytes; list transport and +broader scopes and producer families remain pending. Private original-program +proofs, generated owning storage and read-independent MIR lifetime planning +implement that slice without granting Source ownership to an alias. Local +compiler/reference controls and paired Source-deleted native Debug/Release +execution pass for unchanged Source09 and the new Source13. Physical cleanup +measurements and broader-context gates remain pending. These bounds add no +checker bans and do not narrow the selected lifetime rule; see the +[temporary local-view contract](completed/native_codegen/native_temporary_local_views.md). + +The bounded [temporary List transport](completed/native_codegen/native_temporary_list_views.md) +uses that same lexical whole-owner rule and existing list operations. Its +checked reference adapter retains exact generic observer instances, nonowning +List tickets, copyable element observations and explicit whole-List cloning. +Focused reference and selected compiler checks pass. The repaired List fixture +and all retained selections pass the 24-test/48-execution paired native matrix. +Physical cleanup and broader lifetime acceptance remain open. This adds no +language spelling or checker restriction. + +An original explicit call-site view may be evaluated eagerly before a later +handled argument, then released when the complete consuming operation returns. +Native lowering retains its exact checked local or finite field path through +an internal non-owning call scope. A terminal mutable owner stays protected +during that scope and may be rebound afterward. An ordinary owned temporary +producer evaluates once into owning storage, which backs the internal view; +this introduces no implicit clone of an interface or resource. Direct views +preserve pending metadata, while field reads retain their eager receiver checks. +The operation's result is materialized before the internal scope ends. An +aborted handler ends abandoned scopes before returning an owned operand, and +loop exits abandon only scopes inside the exited operation. Persistent source +aliases retain the limits above. Native staging now preserves the captured +generation of an exact source-checked mutable plain `TypeConstruction` binding +when a later argument assigns its replacement. The current binding and captured +backing remain separate, without a new source clone or ABI permission. Broader +simultaneous cohorts and other owner kinds still require reconciliation. + +A written view of an original handled owned producer also keeps its exact +checked conversion and temporary backing. An interface boxing conversion may +read that proved concrete input borrowed while producing an independently owned +box; its runtime implementation may copy the payload. These bounded paths make +no zero-runtime-copy claim; see the +[call-view implementation](completed/native_codegen/native_scoped_call_view_staging.md). + +An explicitly viewed handled optional/result payload follows the same +borrowing rule when it retains one stable sum binding. Immutable owners and +view parameters retain their existing native proof. The bounded mutable-owned +extension admits an original mutable local or owned parameter through a private +receipt joining its exact selected Owned binding fact, declaration, initializer +and concrete body context. A mutable view parameter or absent origin is not an +owned root. The canonical initializer has an explicit outer `view` around a +Handle whose target is an explicit `view` of that binding. A Some/Ok move-only +payload is a read-only, nonowning alias with the exact checked payload type; +it is not an implicit clone or transfer out of the sum. Forwarded aliases +retain the same backing dependency. An explicit `clone` acquires independent +ownership. + +A final implicitly copyable value keeps its ordinary copy semantics instead +of becoming a persistent borrow. Primitive values are copied; a `string` +destination owns its retained copy. A copied destination may be declared +`mutable` and rebound. Once that destination is initialized, the generated +projection views no longer prevent a later ordinary owned transfer or unwrap +of the original sum. This does not end a live move-only payload alias or +permit changing its backing owner. + +The bounded native ordinary-sum proof requires a terminal Return on the +None/Fail continuation. In `result[T, string]`, the Fail companion owns one +String copy for that handler. Outer pending sums fail before extraction or +continuation execution; ready sums retain their payload's pending metadata for +later observers. Nested expression-level Handles can retain their own scalar +Default, but an alternate outer Default payload needs its own backing-lifetime +contract. Mutable view-parameter roots, missing or inconsistent selected owned +facts, temporary-backed Handle aliases, loan expiry and erased Resource-bearing +payload identity remain outside this proof. Copy destinations keep their Owned +mode and can outlive a later shell replacement; move-only aliases retain the +existing persistent owner-change prohibition, including after their last read. +The [mutable-owned sum note](completed/native_codegen/native_mutable_borrowed_sum_backings.md) records +its bounded MSVC acceptance and remaining proof and implementation limits. +The [ordinary borrowed-sum implementation](completed/native_codegen/native_ordinary_borrowed_sum_codegen.md) +is separate from the +[Resource borrowed-sum contract](completed/native_codegen/native_resource_borrowed_sums.md) and +[verified nested Resource contract](completed/native_codegen/native_resource_nested_sums.md). + **Rule 2: A view cannot be sent to another thread.** ``` @@ -5522,6 +6181,12 @@ mutable view list[int64] stored_view A view cannot be returned, cannot be stored in a struct field, cannot be assigned to a variable in an outer scope, and cannot be captured in a closure. It exists for exactly one purpose: reading data within the current function, then disappearing. This is why no lifetime annotations are needed — the lifetime is always "this function call, no more." +A directly written `returns view T` annotation reports E0401 even when the body +returns an owned clone. The restriction covers declared, inline, interface, +inherent, and implementation functions, and callable result annotations. An +unused generic header is subject to the same declaration check. Return an owned +type and use `clone` when duplicating a view's move-only value. + #### Why No Lifetime Annotations In Rust, lifetime annotations exist to tell the compiler how long a reference must be valid when the answer is not obvious from the code structure. Jett's views eliminate the ambiguity entirely: @@ -5774,8 +6439,13 @@ The LLM receives the **minimal failing input** — the simplest case that breaks `verify` and `property` are complementary: -- **`verify`** — specific input/output pairs, executed at compile time (comptime). Proves the function is correct for known examples. Fast, deterministic, zero overhead. -- **`property`** — invariant declarations, executed by the fuzzer at test time. Proves the function is correct for thousands of unknown examples. Finds the edge cases the LLM didn't imagine. +- **`verify`** — explicit example assertions, executed at compile time (comptime). Checks the asserted behavior for those cases, with no production runtime overhead. +- **`property`** — properties checked against generated inputs by the fuzzer at test time. Can find counterexamples beyond the hand-written examples; passing a finite run does not prove the property for every input. + +These execution-based checks are distinct from static guarantees enforced by +the typechecker, such as ownership, capability requirements, and exhaustive +enum matching. Neither `verify` nor `property` supplies a universally quantified +proof of application behavior. ``` function clamp(value: int64, low: int64, high: int64) returns int64: @@ -5802,7 +6472,48 @@ property clamp: assert result == value ``` -The `verify` block proves 5 specific cases at compile time. The `property` block proves the invariants hold for 10,000 random `(value, low, high)` triples — including integer boundaries, negative numbers, extreme ranges, and invalid combinations like `low > high` that the LLM would never think to test. +The `verify` block checks 5 specific cases at compile time. The `property` block checks its assertions against 10,000 generated `(value, low, high)` triples. Generation can exercise integer boundaries, negative numbers, extreme ranges, and invalid combinations like `low > high`; a passing run establishes no guarantee about untested triples. + +Pure Verify and Property bodies may use Resource-typed aggregate values whose +selected contents contain no live Resource. Potential Resource paths remain in +the original checked type graph; their absence does not grant a provider or make +an impure function eligible. The native implementation must retain the same +checked program, required cache and generated case identities through its +canonical suite entry. The [native Resource suite protocol](completed/native_codegen/native_resource_suites.md) +records the compiler transport and outstanding acceptance for this case. + +Inside these original checked bodies, an ordinary-typed direct call can still +belong to the exact Resource execution family. Native extraction must preserve +its original checked call and argument/field order through ordinary expression +containers. Boolean `&&` and `||` keep their right operand on the selected +short-circuit branch. This does not give ordinary staging Resource custody or +implement wider indirect, refinement-handler or Resource-bearing aggregate +native transport. + +The current native property-suite implementation reuses the interpreter's 100 +deterministic trials per property. A terminal failure in an emitted suite +identifies the property name and one-based trial number before the original +runtime error. This diagnostic metadata contains no generated argument values. +Normal builds still reject failing properties during frontend validation, with +the frontend's shrunk counterexample. Shrinking a failure encountered only in +native execution is performed by the native suite driver: it replays the failing +case and evaluates the existing bounded shrink search using fresh native +processes. A standalone suite executable reports the original failure; the +driver returns the shrunk counterexample separately. Backend build errors, +timeouts, unsuccessful cleanup, and unreproducible failures remain runner +errors, not passing properties or accepted shrink candidates. Generated narrow +integers retain their declared width through aggregates and shrinking. A shrink +candidate outside that integer range is discarded before property execution; +an invalid input must not become a counterexample through its range error. +Generated `float32` values and their shrink candidates retain binary32 precision +and type identity. Counterexamples describe the rounded value actually executed, +including when the value occurs inside an aggregate or refinement. +Generated generic records retain their instantiated type through containers, +interface erasure, and shrinking, just as records constructed by source code do. +Refinement identities also survive shrinking, including inside aggregates. +Candidates that fail a refinement predicate, or whose predicate evaluation fails, +are rejected before executing the property. Native candidate validation calls +the compiled predicates; it does not interpret their bodies. #### Property Tests with Capability Mocks @@ -5979,7 +6690,7 @@ The fuzzer shrinks failing cases to the simplest reproduction. `sort_list(list(1 **5. Catches hallucinated logic that verify blocks miss.** -A `verify` block with 5 hand-picked examples might pass even if the function is completely wrong for edge cases. A `property` block with 10,000 random inputs will almost certainly catch it. The combination of both — `verify` for compile-time proof of known cases, `property` for test-time proof of unknown cases — provides the strongest correctness guarantee an LLM-generated function can have. +A `verify` block with 5 hand-picked examples might pass while the function fails on other inputs. A `property` block can expose additional failures, but detection depends on the assertions, input generator, and iteration budget. Combining example checks with generated checks increases test coverage without proving correctness for all inputs. #### Implicit Views in Test and Debug Contexts @@ -6144,7 +6855,7 @@ The trace data is part of the agent TOON payload. The LLM receives it directly i 5. Fixes the function. 6. Removes the `trace` statement. -The compiler can optionally warn about `trace` statements left in code during release builds. `trace` statements are compiled out entirely in `--release` mode — they produce no runtime overhead and no output in production. +The compiler can optionally warn about `trace` statements left in code during release builds. `trace` statements are compiled out entirely in `--release` mode — they produce no runtime overhead and no output in production. Native release artifact generation also discards breakpoint conditions before executable control flow is lowered. #### Combining Trace with Property Testing @@ -6311,6 +7022,12 @@ examples. #### Conditional Breakpoints +The one-line `breakpoint hit` snapshot reports the current function's lexical +bindings: parameters, closure captures, and active nested scopes. Same-named +bindings in distinct functions remain separate. It does not flatten caller frames into that +snapshot. The interactive protocol addresses other frames explicitly by frame +identity. Captured bindings retain their declared types in debug output. + `breakpoint` optionally takes a condition expression. It only pauses when the condition is true: ``` @@ -6577,7 +7294,7 @@ Jett keeps its symbol set **as small as possible**. Symbols are only used where | `:` | Type annotations, block starts | | `"` | Strings | -The `view` keyword is explicit at both call sites and declarations. When passing a value to a function that declares a `view` parameter, the caller must write `view`: `process(view data)`. In pipelines: `data into view json.serialize[Type]()`. +The `view` keyword appears at declarations and call sites. To retain an owned move-only value, or forward a borrowed value to a `view` parameter, write `view`: `process(view data)`. In pipelines: `data into view json.serialize[Type]()`. Rule24 also permits a bare owned last-use argument to a `view` parameter; the caller relinquishes that owner after the call. **Replaced by keywords:** @@ -6664,7 +7381,11 @@ string DEFAULT_HOST = "localhost" float64 PI = 3.14159265358979 ``` -Global mutable variables are forbidden (Rule Set 2). Global constants are allowed because they never change — they are baked into the binary at compile time. +Global mutable variables are forbidden (Rule Set 2). The frontend reports E0377 +at each mutable namespace-level binding before either backend lowers or executes +it. Local mutable bindings remain allowed. Assigning to a namespace constant, +including through a projected field, reports E0404. Global constants are allowed because +they never change — they are baked into the binary at compile time. Global constant initializers are namespace-local: they may use literals and declarations from their own namespace, but they cannot reference project or @@ -6673,6 +7394,32 @@ and global constants cannot create an implicit cross-namespace dependency. Compiler-provided standard declarations retain the canonical qualified and fixed-prelude availability selected by the module contract. +An initializer may use a literal, including a signed numeric spelling, an earlier +same-namespace constant reference, parentheses around those forms, or an explicit +`comptime expression`. Literal spellings and constant references already denote +values; they do not execute a calculation. All required calculations, including +operators, interpolation, and function calls, must use `comptime`; an ordinary +calculation in a constant initializer reports E0378 even when all operands are +known. For example, write `int64 doubled = comptime (base * 2)`, while +`int64 copied = base` directly reads the earlier value. Any pure function remains +eligible for an explicit evaluation, including a function that reads earlier +constants. Evaluation failure reports E9001 during compilation, even when the +constant is unused, before runtime execution or native publication. + +Both execution backends receive the same checked constant values. The current +supported types are fixed-width integers, `float32`, `float64`, `bool`, `string`, +`nothing`, and transparent aliases of those types. A primitive declaration must +evaluate to its actual primitive value shape; a hidden pending task cannot be +baked as a primitive constant. Other constant types, including move-only +aggregates, nominal refinements, resources, actors, function values, and erased +payloads, conservatively report E9001 while their ownership contract remains +unresolved. This restriction does not limit ordinary local values or explicit +`comptime` results outside namespace constant declarations. + +`trace` may observe a supported constant in debug builds and retains its declared +type spelling. The compiler's observation temporary does not appear in a later +breakpoint's lexical scope. Release builds remove the trace as usual. + ### Functions ``` @@ -6689,7 +7436,12 @@ Every function always has a `returns` clause — functions that produce no value **Every code path must end with an explicit `return` — except `returns nothing` functions.** A function that `returns int64` must have `return ` on every code path. If any path is missing a return, the compiler rejects it. The one exception: functions that `returns nothing` may omit the final `return nothing` — the function implicitly returns when execution reaches the end. Early `return nothing` is still allowed for exiting mid-function. -Named arguments work in both struct construction AND function calls. Any parameter can be passed by name for clarity. This allows `GuiCapability.create_text_field(gui, label, width: 200, height: 30)` — mixing positional and named arguments in a single call. +Named arguments work in struct and enum construction and in calls to declared +functions. Any parameter can be passed by name for clarity. This allows +`GuiCapability.create_text_field(gui, label, width: 200, height: 30)` to mix +positional and named arguments in a single call. A positional argument fills +the first parameter not yet supplied; a pipeline input reserves the first +parameter before the remaining arguments are assigned. Argument expressions are always evaluated once in lexical left-to-right source order. Named arguments change which parameter receives a value, not when the @@ -6712,6 +7464,12 @@ Note: `else if condition:` is the construct for chaining conditionals. It is not ### Loops +An inferred empty list or map can retain an uninhabited `never` element, key, +or value type. Iterating it evaluates the collection expression once and runs +no body. The body still undergoes ordinary source checking, and an outer pending +collection still reports the normal iteration failure until it is joined. +This does not create a value of `never` or make it a source-declared type. + ``` function process_items(view stdout: Stdout, items: list[string]) returns nothing: for item in items: @@ -6881,6 +7639,18 @@ string msg = "user: {user}" # COMPILE ERROR: User does not impleme **Compiler-stdlib coupling:** This is one of a small number of places where the compiler has special knowledge of a standard library interface. String interpolation depends on `Displayable`, just as `handle error:` depends on the built-in `result` type and `handle:` depends on `optional`. These are intentional, well-defined couplings — not a general implicit conversion system. Outside of string interpolation, converting to string requires an explicit `string.from_int64()` or `string.from_float64()` call. +Interpolation selects the implementation for the expression's checked type. +An implementation for an interface receives its erased value; the concrete +payload's implementation does not replace that contract. An explicit display +implementation takes precedence over built-in primitive formatting. Each +interpolated expression is evaluated once, and display borrows its value. +An implicitly selected `display` call must return a ready string. A pending +string result fails at that interpolation segment before later segments are +evaluated; it is not implicitly joined. This result check belongs to +interpolation. An explicit method call retains ordinary string task behavior. +Failed dispatch through a pending interface value reports the registered +interface method's name, including its declaration namespace. + **Literal braces:** Use `{{` and `}}` for literal `{` and `}` characters: ``` @@ -6933,9 +7703,43 @@ of that same method. There is no separate `not_equals` method and no derive or field-based fallback. This makes identity choices visible at the type rather than silently changing when a field is added. A struct without an explicit, exact `Equatable` implementation gets a compile error for either operator. +The implicitly invoked method must produce a ready boolean. A pending result +reports `Equatable.equals must return bool` before inequality negation or later +expressions, without implicitly joining it. Explicit calls to `equals` retain +ordinary boolean task behavior, and secret-qualified operands retain the +comparison result's checked secrecy. Enums retain their existing variant-and-payload equality. Equality does not grant collection-key eligibility. +User structs nested inside enum payloads invoke each +struct's exact explicit `Equatable.equals` method, including through nested +containers. The frontend reports E0358 when a required method is missing, and +comparison stops at each custom struct method without inspecting ignored fields. +Both interpreted/comptime and native execution enforce this boundary; `!=` +negates the complete result. See +[the enum equality implementation](completed/enum_payload_struct_equality.md). +Direct `==` and `!=` are rejected for `bytes`, `list`, `map`, `set`, `optional`, +and `result`, including aliases, refinements, and secret wrappers over those +types. Compare their contents explicitly. This restriction is checked in +generic instantiations and explicit comptime expressions too; it does not change +the separate comparisons of collections nested inside enum payloads. + +**Interface equality is explicit through domain operations.** `==` and `!=` +are rejected after values have an interface type, even `Equatable`. This includes +transparent aliases, refinements and secret wrappers, concrete generic bodies, +inline functions and explicit comptime expressions. Compare before erasure or +call an explicit domain comparison function or interface method. Erasure never +adds a dynamic equality operation or a handle-identity fallback. +An enum's implicit payload comparison is also rejected if it would reach an +interface value through nested containers or other aggregate payloads. The check +stops at an explicit user-struct `Equatable.equals` method, which can choose its +own fields and ignore interface-valued data. Explicit calls retain the ordinary +ownership and secrecy rules. See [the interface equality decision](open_design/interface_value_equality.md). + +Finite recursive enum payloads use the same comparisons at every depth. A +fixed backend traversal limit must not reject an otherwise supported comparison. +Payload comparison retains its short-circuit order, pending-depth distinctions, +and IEEE floating behavior, including unequal NaNs and equal signed zeros. **Collection hashing stays primitive-only.** Jett does not expose a custom `Hashable` interface yet. `map[K, V]` accepts only integer, `string`, or `bool` @@ -7013,7 +7817,18 @@ width, including signed minimum divided by `-1`. Integer division and modulo are accepted only when the divisor is statically proven nonzero by its refinement, a nonzero literal or immutable binding, or a visible equality guard. Floating-point arithmetic follows IEEE behavior, including infinity and -NaN for division by zero. Arithmetic therefore does not create runtime +NaN for division by zero. Floating-point `modulo` uses a truncated-quotient +remainder with the dividend's sign, including negative zero. A zero divisor, +infinite dividend, or NaN operand produces NaN; a finite dividend modulo infinity +returns that dividend. A float32 expression rounds to 32-bit precision at +each expression result, including literals and intermediate arithmetic, in both +runtime and explicit comptime evaluation. Its current string conversion formats +that value exactly widened to the shared float64 display carrier; it does not +restore discarded precision or promise shortest-float32 decimal output. +`float32.from_float64(value)` explicitly rounds a `float64` to binary32 using +the same boundary rule. It is pure, has no handled failure, and preserves IEEE +NaN and infinity behavior; the result is eligible for `comptime` evaluation. +Arithmetic therefore does not create runtime exceptions or handled errors. Refinement types remain the way to constrain an application to a mathematically non-overflowing domain. @@ -7035,6 +7850,14 @@ function serve(view net: Network, handler: Handler) returns nothing: int64 doubled = apply(5, function(x: int64) returns int64: return x * 2) ``` +Parameter ownership is part of a function signature: `function(view list[int64]) returns int64` +and `function(list[int64]) returns int64` are different types. +A named or inline function value must match each parameter's `view` mode as +well as its type and return type. Calls through a function value follow the +same call-site rule as direct calls: an explicit `view` keeps the caller's +value, and passing a `view` to an owned parameter is rejected. This check also +applies to generic calls and pipeline inputs or additional arguments. + Anonymous functions may capture only implicitly copyable values from the enclosing scope. Each capture is copied into the closure. Move-only values must be passed explicitly as parameters instead of being captured. @@ -7049,6 +7872,103 @@ function(int64) returns int64 add5 = make_adder(5) int64 result = add5(10) # result == 15 ``` +Concrete source methods also form function values with the existing +`Type.method` spelling, including methods supplied by an interface +implementation. The receiver remains the explicit first parameter; storing a +method does not bind an instance. Parameter types and `view` modes remain part +of the callback's type. Method bodies use their declaring namespace when +invoked through a callback, including callbacks produced by `comptime`. + +An interface-qualified method value, such as `Reader.read`, denotes the same +dynamic dispatch as an interface-qualified call. Its receiver remains an +explicit parameter with the interface type and declared `view` mode; invoking +the callback selects the receiver's concrete implementation. Creating or storing +the callback does not select an implementation or execute its body. The named +callback retains the interface declaration's canonical namespace and method name. +The declared slot owns that qualified spelling even if an implementation of +another interface for the interface type has a method with the same name. + +A call through a function-valued expression evaluates arguments in lexical +source order, then evaluates the callee to select the function. This includes +parenthesized callbacks, returned functions, and function-valued fields. If an +argument's handled failure reassigns a mutable callback or its containing +record, the call uses its new value. An argument's early return skips callee +evaluation. Ownership checking uses the same order; parentheses preserve moves +and views. Capability-bearing signatures remain impure for every callee form. +Inline function bodies follow the same signature-based purity rule as declared +functions. Creating a closure does not execute its body: a pure or `comptime` +expression may create a callback that accepts a capability when invoked later. +The body is checked in its own function context and cannot acquire authority +from the surrounding expression. + +A concretely instantiated callback may accept the internal uninhabited element +type inferred from an empty collection. The callback itself is still a value: +creation, storage, returns, explicit cloning, pending state and cleanup preserve +its ordinary captures and metadata without invoking it. No checked invocation +can supply the uninhabited input. Its result may also be uninhabited without +creating a result value. Mapping an empty collection evaluates its inputs and +checks the source body normally, then runs no callback body and manufactures no +element. This does not add a source spelling or runtime carrier for `never`. + +The already uncallable callback body may contain a nested zero-argument closure +that captures its input and returns it. That nested body still receives normal +checking, but its environment is never created and its call never executes in +this specialization. The same source with an inhabited input follows ordinary +closure creation and invocation. A directly secret-qualified uninhabited result +likewise creates no result value when an exact uninhabited input already makes +invocation impossible. This grants neither a surviving return-only callback a +callable representation nor an uninhabited capture a runtime environment. + +A concrete specialization may also contain a closure returning the internal +uninhabited type inside a provably absent optional or result arm. Its source +body still receives normal checking. Native preparation may omit both creation +and invocation after selecting the inhabited arm. The original function type +remains valid metadata; it does not grant a surviving closure a callable native +representation or permit an uninhabited capture to be stored at runtime. + +Named arguments use parameter names from a checked function or constructor +declaration, and parentheses preserve that declaration metadata. Anonymous +function types contain parameter types and ownership modes but no names; calls +through such values require positional arguments. Unknown or duplicate labels +are rejected rather than silently treated as positional arguments. + +Debug snapshots render a named function value as `function(namespace.name)` +and an inline function as `function(parameter, names)`, using its source +parameters. This is value metadata, independent of the function's type and +argument-label rules. Captured values and native addresses are not rendered. +Tracing or inspecting a callback does not invoke or consume it, including when +the callback is nested in a collection or record. +Actor handles appear in debug snapshots as `actor#N`, where `N` follows spawn +order within the run. A trace or breakpoint does not expose actor state or +consume the handle. +An in-progress `TypeConstruction` builder appears with its owner, selected +variant or machine state, and the fields successfully supplied so far. Fields +appear in insertion order. Inspecting the builder does not finish or consume it. +Trace and one-line breakpoint snapshots render `secret[T]` as `[redacted]`, +including through aliases and refinements and inside collections, sums, records, +enums, machines, and builders. Secret payloads and their pending depth never +appear; public fields and pending wrappers outside a secret retain their usual +format. Formatting does not consume, finish, or declassify values. Typed runtime +reflection diagnostics, unsupported direct-field receivers, and pending sum +receivers use the same rule. Direct-field and `handle` failures retain the checked +receiver type before removing value wrappers; redaction does not join pending +values or change failure order. A pipeline handler retains its step's result type +separately from the error payload type used by a ready failure handler. +The interactive debugger's existing +metadata-only secret inspection policy remains in force. See +`docs/completed/secret_debug_output.md`. +Debug binding labels preserve declared aliases, including concrete generic +arguments and closure captures. Inferred loop, match, and handler bindings carry +their checked types. A binding is not visible inside its own initializer. Breakpoint snapshots omit +consumed bindings; assignment to a mutable binding makes it visible again. +At a branch merge, a binding must remain available on every live path to appear. +Actor handlers have their own lexical frame, containing state, capabilities, +message parameters, and handler locals rather than caller locals. +Reflected construction accepts a field value with its exact refinement type +after that value has passed the refinement predicate. A supplied base value +still requires the predicate at builder completion; the exact refined type +does not remove that requirement for base values. + ### Explicit Typing Every variable declaration requires a type annotation. There is no type inference for variable declarations. @@ -7065,6 +7985,36 @@ Generics use `[T]` (square brackets) rather than `` — avoids ambiguity with Generic type parameters may be inferred when argument types uniquely determine every parameter, including direct and pipeline calls to source-owned stdlib functions such as `list.length(items)`. Calls with no inferable value argument, an ambiguous result-only parameter, or an explicit reflection target write the type arguments. When type arguments are written, their count must exactly match the callable's generic arity. +Nested type arguments and callable annotations have no fixed token lookahead +limit. Direct calls, pipeline calls, and typed local declarations recognize the +complete type on its logical line; malformed delimiters remain parse errors. + +An empty collection initially contributes the uninhabited element type `never`. +When arguments have supplied every generic parameter, an expected result type +may refine an inferred `never` to a concrete type. It does not introduce a +parameter absent from all arguments or replace a type inferred from an actual +element. For example, `list[list[int64]] chunks = list.chunk(list(), 3)` +specializes `chunk` for `int64`. + +Mixed collection literals and repeated generic arguments fill compatible `never` +slots from their concrete members in either order. For example, +`list(none, some(7))` has an optional integer element type, and mixed `ok`/`fail` +values retain both result payload types. This does not promote numeric widths or +erase nominal types. Assignment is directional: an uninhabited value can flow +into a contextual type, but an inhabited value cannot satisfy `never`, including +through a handler default. +Generic argument inference unwraps a transparent alias at the root of an +argument, while aliases nested in containers or callable signatures retain their +reflection identity through explicit views and parentheses. Equal concrete +witnesses keep the first alias in declaration parameter order; filling an absent +`never` slot does not replace concrete siblings. + +An inferred absent optional or result arm can also retain `never`. That arm +has no payload value. Handling still observes the sum and rejects an outer +pending value before selecting its possible arm; source checking applies to +both arms. An absent arm contributes no payload to clone, so a cloneable local +sum retains the ordinary observation and reuse contract. + A bare generic function template is not a concrete function value. To pass one as a callback, use a concrete named wrapper or inline function containing an ordinary generic call. Contextual specialization of generic function values is @@ -7352,13 +8302,26 @@ Native macOS windows and their synchronous callbacks execute on the process main thread. Embedding callers must invoke native graphics there; an off-main-thread window request returns a host failure without entering AppKit. -The interpreter supports `graphics.run[State]` for a synchronous native window -session. `main` requests a `Graphics` capability, and the public source-owned -wrapper borrows it with `view`. Pure update and render functions receive only -ordinary state and keyboard values; rendering returns a `graphics.Scene` of RGB -rectangles and bitmap text. The runtime owns the window inside the call and -closes it on normal return, host failure, or callback failure. No source-visible -window handle or resource is exposed. +The interpreter and native executables support `graphics.run[State]` for a +synchronous native window session. `main` requests a `Graphics` capability, and +the public source-owned wrapper borrows it with `view`. Pure update and render +functions receive only ordinary state and keyboard values; rendering returns a +`graphics.Scene` of RGB rectangles and bitmap text. The runtime owns the window +inside the call and closes it on normal return, host failure, or callback failure. +No source-visible window handle or resource is exposed. + +The display capability must be ready on entry. A pending `Graphics` capability, +including one forwarded through a helper, causes a terminal runtime error before +configuration decoding or provider access. Joining every pending level restores +the existing authority; graphics never joins it implicitly. + +Configuration and scene decoding requires ready values. A pending record, field, +list, or list element is a terminal runtime shape error; decoding never joins it +implicitly. Fields are decoded in declaration order, with rectangles before text, +before checking domain bounds or opening/presenting a window. Invalid ready +dimensions, colors, and geometry instead return a handled `fail(string)`. +Pending callback state remains valid data and retains its depth until explicitly +joined; rendering must produce a ready scene. The initial callback boundary accepts directly named functions (including qualified namespace references) or inline functions. Update owns its two @@ -7402,13 +8365,35 @@ External dependencies live in the `deps/` directory as vendored `.jett` files tr ## Generic Reflection Specialization Facts +Every concrete generic invocation must have a checked body, including reflection +inside interpolation, call arguments, and closures. Reflection placement never +allows ordinary type checking to be skipped. A `comptime type` binding must be +valid during compilation even behind an ordinary runtime condition: for example, +`type.arg[string](0)` is invalid regardless of the condition's runtime value. +Valid bindings are allowed behind unknown runtime guards when all branches +typecheck. An unknown guard does not itself make a binding invalid or provide +evidence for a cast. Only supported reflection specialization may exclude an +unreachable branch. + +Reflected type bindings preserve supported finite nested type arguments and +alias identities at runtime. Native dispatch must not reject a checked type +solely because its metadata exceeds a backend traversal depth limit. + +Direct calls and admitted pipeline forms of `type.variant_value`, +`type.machine_state_value`, and `type.arg` return the same checked metadata. +Their source operand is evaluated once before observation. Pipeline results use +the observer's checked output type and retain the ordinary pending and index +checks, source ownership, and complete owner layout. + Generic casts may be justified only by reflection evidence that stays visibly tied to the reflected type. Direct `TypeKind` / `TypePrimitive` comparisons, immutable locals carrying those tags, typed helper parameters receiving them from the same generic instantiation, and matching arms can specialize a branch for that instantiation. Arbitrary caller-supplied tags are not facts about `T`. -A predicate call returning `bool` does not create such a fact, -and copying a reflection comparison into an arbitrary `bool` local discards +The same trusted facts apply inside inline functions and after reflection +expressions used in interpolation or ordinary arguments; expression placement +does not discard a known fact. A predicate call returning `bool` does not +create such a fact, and copying a reflection comparison into an arbitrary `bool` local discards the evidence. This conservative rule keeps type proofs local for agents and prevents broad @@ -7440,3 +8425,647 @@ the completed [reflection predicate fact contract](completed/reflection_predicat 3. A compressed reference file (`skill.md`) should be created for LLM agents — containing compiler usage, syntax overview, code rules, common patterns, and short code examples. This is the file an LLM loads into its context before writing Jett code. Should be done after the design document is stable. 4. TOON (`toon.serialize()`, `toon.parse()`) could be exposed as a standard library module for user code, alongside JSON. This would let Jett programs produce TOON output for LLM consumption — not just the compiler. To be considered after core stdlib is stable. 5. Fast compilation is an implementation goal: incremental compilation, caching, and minimal rebuilds. Target should be sub-second recompilation for typical changes. Slow compilation breaks the LLM compile-test-fix feedback cycle. + +### Native Linux executable seed + +Status: [native-codegen campaign handoff](completed/native_codegen/README.md). + +The Cranelift scalar AOT path can link on the exact x86-64 Linux GNU host as +well as the existing Windows MSVC target. An explicit, target-matched launcher +bundle supplies ABI version, CRT mode, archive and system libraries. Linux uses +`cc` or a literal `JETT_NATIVE_CC` executable path, without shell interpolation; +Windows retains MSVC SDK discovery and the static-CRT contract. Both publish +only a successfully linked executable. The Linux scalar seed runs under an +automated deadline without source-tree or compiler environment dependencies. +Full language/runtime parity and clean Windows execution are still release +gates, not claims made by this seed. See `completed/native_codegen/native_codegen_parity_plan.md`. + +Explicit `comptime` results are imported into typed HIR before MIR lowering, +retaining their checked type and span. Supported namespace constant reads use +the same materializer and have no runtime initializer. Ordinary pure calls +outside required evaluation remain runtime calls. The backend rejects unresolved +`Comptime` and namespace `Constant` markers instead of emitting their source +computation. Other namespace constant types report E9001 under the current +supported domain; a separate extension would require a read and ownership +contract. Current parity validates admitted baked values and shared frontend +rejections. A native regression executes baked `math.factorial(5)` after removing +its source file. + +Native verify and property suites honor the selected debug or release build +mode, including release debug-print restrictions and omission of trace and +breakpoint execution. Selecting a release suite also selects optimized native +codegen; the runtime bundle must match the caller's selected profile. + + +### Native string and numeric execution slice + +The current Cranelift subset also executes immutable strings, interpolation, +print/println, checked Stdout entry parameters, and typed numeric runtime kernels. +String handles have explicit retain/release ownership and are destroyed at last +release. MIR definite-initialization and liveness facts drive local and temporary +cleanup through branches, loops, overwrites, returns, and terminal failures. + +Terminal runtime failure is the first error of the current execution lifetime, +not `result.fail` data. Ordinary entries retain the context-local channel. Each +independently declared checked Resource entry owns a privately authenticated +attempt channel; nested calls share it. Exact completion seals its status and +original diagnostic. Only the entry constructor may select a fresh attempt after +retirement, while all prior completion/error records and session poison remain. +Compiled calls test the current channel before using a return value; failure edges +release frame owners and propagate it to the launcher. Context destruction checks +for leaked native owners, and cleanup failure overrides entry failure. UTF-8 string kernels +use the same extended-grapheme segmentation dependency as the interpreter. This +initial handle representation is not the proposed inline/SSO optimization. + +The current native sequential task implementation preserves nested pending +`nothing` values through calls, captures, aggregates, and explicit `comptime`. +Joining unwraps one pending level; joining plain `nothing` returns +`fail("task was cancelled")`, matching the interpreter. Output and direct +comparison failures also match. This implementation parity does not establish +the concurrent scheduling and cancellation checkpoints described in Rule Set 10; +the current sequential `cancel` leaves its pending operand unchanged. + +`list.sum[T]` follows the selected arithmetic rules for every numeric primitive: +integer sums wrap at the declared width, and floating sums use that width's IEEE +addition at each step. An empty list returns numeric zero of its declared type. +The operation borrows the list and preserves existing errors for pending lists +or elements. The element type must be an exact numeric primitive or a transparent +alias of one. The checker rejects nominal refinements, secret wrappers, and +nonnumeric elements before runtime or explicit compile-time evaluation; neither +an empty list nor generic inference changes this domain. Refinement invariants +need not contain zero or survive wrapping addition, and summation never silently +coarsens a refinement or declassifies a secret. No public addition/identity +interface is introduced. The [summation decision](open_design/list_sum_type_contract.md) +records this boundary and its rationale. + +The native sequential task path rejects field reads, machine state tests and +transitions, and enum matches while the aggregate owner remains pending. A +projected sequence read checks each enclosing owner. Ready owners may contain +pending children; extracting a child preserves its depth, and each `join` still +unwraps exactly one level. A failed transition checks its source before running +target payload expressions. Typed diagnostics retain native secret redaction; +hidden-secret terminal observation remains a separate open policy. + +Native entry grants cover all eleven checked capability types for the value +operations already implemented by the interpreter, including direct and +indirect calls, views, debug output, and task wrapping. Stderr, Stdin, +Filesystem, Network, Process, and Log remain opaque tokens without implemented +I/O providers. Their tokens cannot be substituted for another capability's +authority or used in another runtime context. + +Native conformance runs the same exhaustive verify/property suite gates on +Linux GNU and Windows MSVC. The standalone fixture report includes their +separate execution denominators; passing fixture gates alone does not certify +the remaining full-language and distribution release gates. +An explicit `jett build --target` must equal the supported compiler host; +unsupported targets are rejected before source loading. Native `build` emits +an executable at `target///` (`.exe` on Windows), or +at `-o `. `build --check` validates without an entry point or runtime bundle. +Release builds reject debug printing and remove trace/breakpoint execution, +including condition effects, while enabling native optimization. + +Installed packages contain the compiler, stdlib, and both runtime profiles. +`--runtime-bundle` or `JETT_NATIVE_RUNTIME_BUNDLE` selects an explicit launcher +manifest; otherwise it comes from `lib/jett/runtime//` relative +to the installation prefix. The manifest must match compiler version, target, +ABI, CRT, profile, and required system libraries. The compiler never invokes +Cargo and preserves existing output on a failed build. Stdlib lookup prefers +`JETT_STDLIB_DIR`, then installed `lib/jett/stdlib`, then the development tree. + +Interface-typed runtime parameters, returns, stored values, and mixed collection +literals use typed native boxes and compiler-generated dispatch functions. The +checker records interface slots separately from concrete method bodies, including +generic and reflected body facts. Interface values remain move-only, with explicit +cloning, borrowing, pending depth, and recursive secret redaction preserved. +Existing lists, maps, optionals, and results convert recursively across compatible +interface boundaries while preserving ownership and pending depth. +Supported finite nesting retains that conversion behavior for empty and populated +containers; a native descriptor-depth cutoff must not reject a checked conversion. +Function parameters are contravariant and returns are covariant, with exact view +modes: a callback accepting every `Named` can accept `User` inputs, while one +accepting only `User` cannot serve a caller allowed to pass any `Named`. +Native signature adapters preserve the source callback's captures, debug name, +and pending depth, including callbacks inside existing lists, maps, optionals, +and results. Container descriptors use linked addresses of checked generated +adapters. Comptime callbacks materialize their checked source signatures before +converting to the requested callback type; captures and pending state survive. +Generic closures retain their enclosing type arguments and alias reflection +metadata, and invocation restores their captured type bindings. Named function +calls resolve explicit type arguments in the caller, then execute using only the +callee's own type bindings, so a generic caller cannot shadow its nominal types. +Each checked generic body keeps its own expression types. Closures retain this +map so fixed-width arithmetic uses the invoked specialization, including during +explicit comptime evaluation. Reflection-valued parameters also remain part +of a comptime closure's specialization identity, so callbacks with identical +signatures and type arguments still select their own checked bodies. Other reflection +specialization keys and parity edges remain open; see +`completed/native_codegen/native_interface_values.md` for the remaining gates. +Explicit comptime interface values materialize from unambiguous checked concrete +payload types; secret fields and pending wrappers remain part of the typed value. +Explicit comptime construction builders preserve their owner, selected variant +or state, and provided fields. Programs can clone, store, erase, join, and finish +them at runtime. Baking a partially filled builder does not validate or finish +it: missing fields, refinement predicates, and bitfield widths retain their +ordinary `type.construct_finish` checks. +Each runtime evaluation of a baked owned value yields an independent value, +including when the expression appears inside a loop. +Primitive interface values retain their checked concrete identity even when +runtime storage shares a wider carrier. Integer widths, `float32`, and nominal +primitive refinements dispatch to their own implementations through collections, +callbacks, reflected fields, and explicit comptime evaluation. Transparent +aliases use their canonical base identity. Arithmetic and formatting inspect the +payload without losing identity when the value is stored or cloned. +Collection and sum interface payloads retain their full type arguments, including +interface arguments qualified by namespace. Converting a concrete collection +changes its outer checked identity to the destination type while retaining the +concrete identities of its elements. List and record refinements likewise keep +their nominal implementation identity. Type aliases used for concrete method +calls or method values resolve within their declaration namespace. +Function interface values retain their checked signature, including parameter +view modes and result types. A safe callback conversion changes this exposed +signature while preserving the original function body, captures, and generic +context. Interface dispatch uses the exposed signature; invoking the callback +still executes its source body, including after explicit comptime evaluation. +Machine interface values likewise distinguish the bare machine from its checked +state-qualified types, including inside state guards. Widening to the bare type +changes the implementation selected at an interface boundary. Enum, machine, +and bitfield refinements retain their nominal implementation across erasure and +explicit comptime evaluation. +Expected type context also passes through `run`: for example, initializing +`result[int64, string]` with `run ok(17)` retains both declared result payload +types in the checked body used by native lowering. +Generic structs retain their concrete type arguments across interface erasure, +including reflected construction and comptime evaluation. Typed observations +therefore redact secret generic fields while keeping the base struct display name. +Interface implementations use the full instantiated struct identity, so +`Box[int64]` and `Box[string]` dispatch independently. Transparent type aliases +retain that identity for concrete method calls and method values. A bare +generic owner such as `Box.name` is rejected for missing type arguments; name +a concrete instantiation with a type alias before accessing its methods. +Actor constructor parameters are retained before state initializers run; an +initializer consuming its working parameter does not consume the actor's capture. + +The full native parity gate is still incomplete: the current object count is +recorded in `completed/native_codegen/native_codegen_parity_plan.md`, alongside 30/30 main +outcomes and 25/25 runtime contracts, with 182/182 typed lowering. See +`active/native_value_abi.md` for ABI ownership and failure contracts and +`completed/native_codegen/native_codegen_parity_plan.md` for the remaining gates. In particular, +remaining reflection/JSON shapes, actors and full task scheduling, other capabilities, and clean +Windows release verification are not established by this slice. +The native JSON source path handles top-level refinements over supported bases, +including exact parse validation and serialization. Supported refined record +fields decode and validate before entering the reflected native builder. Direct +struct construction validates refined fields from base, already-refined, and +secret-backed inputs. +Native serialization of `bytes` uses the trusted hex-string source hook, and +supported records containing raw `json.JsonTree` fields retain raw JSON wire +values through the checked source serializer. Supported enum payloads and +collections can also contain raw `json.JsonTree` values in the checked native +source parser. A borrowed raw tree is cloned when returned as a decoded value. +Native comparison against a known unit enum variant checks its tag even when +other variants have aggregate payloads. +Pipeline calls to `json.serialize` and `json.serialize_public` use the checked +native source path for the same supported structured types as direct calls. +Supported bitfields parse and serialize through checked source hooks that handle +unit-enum fields and ordinary payload fields. Native JSON decoding of `uint8`, +`int8`/`int16`/`int32`, and `uint16`/`uint32` checks the range before a bounded +source conversion, including when these values occur in supported aggregates. +Supported nested enum and bitfield fields use the same checked source hooks. +Within a generic function, equality between `type.name[T]()` and a string +literal is a checked static branch selection, preserving the distinct raw +`json.JsonTree` representation. Ordinary string comparisons retain runtime +semantics. +`type.has_secret[T]()` also selects a checked static branch in generic code. +The public JSON serializer uses it inside reflected field bindings to omit +entire secret-bearing fields, including fields whose contents are collections +of secrets. The checked decoder may put an already validated refinement value +into a record field; native struct, enum, and machine builders accept base +values for refinement fields and run their checked predicates at finish. +Enum and machine builders validate only the selected variant or state. +`float32` JSON decoding uses the same checked source path: it reads a `float64` +JSON number and explicitly rounds with `float32.from_float64`. +Checked source JSON serialization formats that rounded value using the +language's existing `float32` string display behavior. +Native `json.serialize` and `json.serialize_public` use the checked source +serializer for sets of primitive-backed elements, including refinements, both +at the top level and inside supported records. The serializer emits a JSON +array by iterating a clone of the viewed set. +Native `json.parse` and `json.parse_exact` likewise decode sets of any +primitive-backed hashable element, including narrow integers and refinements. +Each element is checked before insertion, and duplicate values collapse under +the set's existing equality semantics. + +Generic task joins preserve the instantiated error type in handler observations, +including when one source body is called with both secret and public errors. + +The native backend now emits direct checked Resource calls with an internal +Scope parameter, exact lexical acquisition, provisional Return publication and +ordinary companion cleanup. Ordinary callable signatures and Source rules stay +the same. Direct Result handlers inside call arguments use constructor-owned +Begin/Stage/Invoke/End regions. Arguments stage in lexical source order, and a +handler Return retires only the acquired prefix before evaluating its original +return operand. A skipped invocation preserves the handled Source outcome; +cleanup failure retains its own observed failure channel. Temporary sum storage +and the active execution frame are checked separately. +The reference evaluator records a separate operation floor for each callable +body. A Return retires only operations opened within that body, preserving +interleaved Scope owners and loans. Ordinary callable bodies cannot inherit +a Resource return destination from their caller. Cleanup failure prevents +evaluation of the original Return operand. +Native call joins propagate the callee's completed failure status without +replacing it with a protocol refusal. The callee's original body outcome +stays separate from failure during cleanup. +Native function values initialized from exact named, non-generic, +capability-free declarations now support Resource calls through root-scope +immutable locals and immutable aliases, including namespace-qualified +functions selected by their full-expression resolver identity. Their original +invocation remains +indirect. The backend verifies the selected code address and empty environment, +then supplies the selected body's internal Scope through the checked Source +bridge. Descriptor storage remains ordinary; the Resource argument retains its +independent custody and cleanup. Exact returned compiler-hook descriptors and +required pure comptime hook materialization now retain their checked authority +through aliases, root Return relays and immediate invocation. Their independent +original-body proofs survive canonical pruning and reject altered or missing +occurrences. Captured/scoped/generic runtime callables, handled indirect calls, +nested descriptor returns and conditional/aggregate replacement still require +separate implementations and Source evidence. +Compiler-test Resource providers are confined to a single runtime archive built +with both `test` and `jett_resource_native_test_archive`; production providers +remain disabled. Archive compilation and object emission alone do not establish +linked Source execution. The shared original Source corpus now contains 138 +scenarios passing in both MSVC native profiles against their real reference +outcomes: 276 native runs with Source files absent at launch and zero live +obligations before teardown. Nested handled-view aliases now support terminating +and joining branches, scoped type bodies, and While/Break/Continue boundaries. +Exact declaration identity and certified child-before-parent lexical loan ends +preserve backing ownership; Return operands run before retirement. Broader +For/Match forms, reflected bodies, alternate Default lifetimes and native Resource +semantics remain separate work. Ordinary borrowed data sums have their +[separate implementation](completed/native_codegen/native_ordinary_borrowed_sum_codegen.md). +This local test slice keeps production Resource providers disabled. See the +[nested borrowed-sum contract](completed/native_codegen/native_resource_nested_sums.md). + +The separate [Resource control-flow transport](completed/native_codegen/native_resource_control_flow.md) +now preserves checked custody through consuming Resource-free list iteration and +ordinary Resource-free enum selection. Exact constructor-owned normalization and +independent original/current proofs keep backing ownership and strict joins intact. +Its seven-Source, 30-case / 60-execution local MSVC Source-deleted gate passes, +including consecutive and nested loops. The full 191-case / 382-execution MSVC native successor also passes. +Its coherent-workspace and exact-revision CI gates remain pending. Erased carriers, other iterator forms +and reflected bodies retain their compiler proof obligations under existing Source +semantics; the separate Source06 reference repair establishes no native admission. + +Native plain Resource replacement validates both exact live owners, active runtime holders, loans, nominal kinds, generation capacity and acquisition storage before retiring the old value. An invalid replacement leaves both owners intact. The native compiler consumes this check for exact plain-owner replacement. + +Native plain Resource assignment now retains the evaluated RHS while the old owner remains live. Successful replacement retires old and installs RHS once; failed RHS evaluation preserves old for its handler or scope cleanup. Exact plain occupied Resource self-assignment moves and reseats the same local without a runtime transfer or finalizer. Assignment after close transfers into the vacant local. Conditional, aggregate and projected native replacement remain pending. + +The selected same-local extension admits direct `optional[R]` and `result[R, E]` shells at every checked occupancy. The evaluated RHS and mutable destination must identify the exact same owned local, with the original Source, complete nominal Resource and failure type, local slot, containing scope, current activation and existing loan checks preserved. A live absent shell remains owned. Reseating itself adds no runtime replacement, transfer, finalizer or implicit clone; ordinary scope cleanup still applies. Generic, aggregate and projected carriers receive no new admission. The core MIR admission and object-emission checks now pass for the original direct-sum witnesses. Linked native execution and additional occupancy/refusal evidence remain separate obligations; see [the active reseating note](completed/native_codegen/native_resource_same_local_reseating.md). + +Native required Resource values retain their compile-time authority separately +from the public span/context cache. Only a successful required evaluation in the +same checked program can select its exact hook. Materialization preserves the +original comptime expression and its full concrete generic and scoped context. +Pure branch selection, aliases and relays do not introduce a separate eligibility +rule; no runtime capability or provider enters the required worker. + +Checked reference execution now preserves exact field ordinals and original +scope bodies in direct `type.fields` loops, including equal-type fields, aliases, +recursive calls and Resource cleanup through early exits. Closed pure required +evaluation uses the same boundary without runtime authority. + +The checked Resource native lowering path now retains distinct field ordinals +for direct nominal-struct `type.fields` loops and their direct `field.type_info` +type bindings. An immutable original Source archive joins each ordinal to its +exact checker-selected body and lexical Scope path. MIR selects that body with +the original field binder's index guard followed by a one-arm TypeInfo dispatch, +so equal-type fields keep distinct bodies without weakening TypeInfo identity +checks. Runtime metadata alone cannot select an executable body. + +Ordinary helpers in the same checked Resource program may retain this proof +without gaining Resource custody, provider authority or a hidden Scope ABI. +Their ordinary map, string, view and uninhabited sequence preparation stays +available. This bridge adds no Source rule, runtime leaf, ABI or wire-v2 change. +The affected compiler phase gate passes 1,202 checks. Four focused cases pass +16 Source-deleted native executions / 24 entries, including clean same-grant +reentry; the unchanged 191-case corpus passes all 382 Debug/Release executions. +All 3,686 frozen inputs and nine matched runtime files remain unchanged. +Ordinary `lower_program` coalescing stays unchanged: independent positive controls +pass 30 Source-deleted main/verify/property executions without a production change, +and establish no ordinary coalescing defect. Generic and alias owners, bitfields, +variants, machines, type arguments and nested producers remain implementation +obligations in the checked Resource path. Original Source06 additionally passes +16 exceptional Source-deleted single entries and 18 same-grant native executables / +36 entries in both checking profiles. Its original completions and first nonzero +process exits remain exact after READY on the retained Session/provider/grant. +Explicit derived Return/Break/Continue/Default native controls and broader aggregate +custody remain separate acceptance obligations under existing Source semantics. See the +[reflected-field execution contract](completed/native_codegen/native_resource_reflected_fields.md). + + +Typed Resource aggregate transport is being implemented through a separate +constructor-owned carrier plan. Actual empty collections and inactive payloads +retain their complete nominal, generic, variant/state and qualification identity; +absence cannot erase a type's potential Resource paths or prove a refinement. +Dedicated carrier observations and a parent-bound borrowed-sum adapter preserve +existing Source ownership and cleanup. Wire v3 retains strict v1/v2 compatibility. +Bounded typed absence transport and its v21 local MSVC regression have actual +native acceptance; the ordinary-error successor's twelve-gate regression also +passes at v24. Live aggregate custody and replacement remain follow-ons. See the +[typed aggregate plan](completed/native_codegen/native_resource_aggregate_carriers.md). + +### Native ordinary entries in Resource programs + +A program may declare Resource APIs while its selected entry and ordinary helpers +perform no Resource custody operations. Native compilation preserves their +ordinary calling convention. The generated program wrapper owns a separately +validated cleanup Scope for the exact selected closed, capture-free, +Network-only/Nothing entry, including an entry with no parameters. Initial checked +lowering retains its own original Source provenance and full-body graph proof; +mutable MIR cannot manufacture this authority afterward. This native metadata +changes neither Source execution-family membership nor purity/comptime rules. +See [the ordinary entry plan](completed/native_codegen/native_resource_ordinary_entry.md) for the +bounded implementation and pending acceptance gates. + +The selected normal startup prerequisite makes the generated Resource object +install its exact immutable layout and own the Runtime begin/complete lifecycle. +It preserves the ordinary entry ABI, retained first-error/cleanup outcomes and +Runtime-only authority. The normal provider remains Disabled and grants no +Network authority; actual TCP/OS providers, public driver/catalog wiring, mixed +entry capabilities and shipped-host execution remain required. The separate +[generated startup note](completed/native_codegen/native_resource_generated_startup.md) records the +internal ABI choice, private-runner compatibility and current bounded validation. +Current local MSVC controls pass all 68 Python helper, eight Runtime startup, +four codegen and 11 existing launcher lifecycle controls, plus 2,371 workspace +library tests. Four fresh callable Driver gates retain 48 executions against the +changed normal launcher. The 16 fresh private Resource gates retain 754 executions +and 900 entries. The complete local startup80 gate accepts 48 normal and 32 +private executions; the private reports retain 40 entries, with four derived +borrowed probes classified separately. Actual linked Resource manifests are +wire2; supplemental occupied Source33 wire3 is object-only. Normal links use the +private-v6 measured ordered static-CRT vector; a separate normal compiler-emitted +vector was not measured. Hosts must exclusively own entry and finish for a +context; generated unwind has no invocation or thread token, and a rejected +concurrent or recursive caller must not unwind its owner. The built-in launcher +creates a fresh context for one entry. Full workspace/native conformance, exact +CI, GNU, installed distribution, public providers and actor-graph import remain +open. No concurrent-host ABI or real-provider acceptance is established. + +The selected ordinary-error reentry repair keeps sealed attempt observations +separate from legacy session failures and persistent ordinary cleanup poison. +It introduces no error reset, test recovery API or recovery inside a failed Source +function. The managed runtime and fresh matched MSVC archives pass their measured +checkpoint. The full fourteen-Source same-grant gate passes in Debug/Release, +retaining both control13 signed Bounds errors, body1/cleanup0/Body and exact +retirement before destruction. Its authenticated 28 executables/56 entries and +eleven fresh predecessor gates at 576/672 establish a combined twelve-gate +604-executable/728-entry scope. The fresh whole workspace passes 3,614 checks, +zero failures and 14 ignored tests; independent v24 input/artifact verification +passes. These are bounded local MSVC results. The separate normal-main linked +baseline is complete: sixteen observations, twelve exit0 and four exit71, from +eight exact opaque objects reused with Debug/Release runtime artifact profiles; +all fifteen raw-probe Sources remain preserved. Baseline collection supplies no +desired Source oracle, ordinary public-Main context-retirement observations or +feature credit. Occupied aggregates, broader lifetimes, wider ordinary parity +and fixes, and exact successor CI remain obligations. See the +[ordinary terminal-error lifetime note](completed/native_codegen/native_resource_ordinary_error_reentry.md). + +### Actor handle identity equality + +Actor `==` and `!=` compare live instance identity for the same checked nominal +actor type in its owning runtime context. An explicit clone aliases the same +actor; distinct spawns compare unequal even when state is identical. `!=` +negates the same comparison. Enum payload traversal applies this rule through +supported nested containers. Explicit user-struct `Equatable.equals` remains +its own exact two-view boundary. + +The actor registry separates base instance identity from pending handle depth. +`run` creates a registered pending alias that shares state, and `join` reaches +the same base instance at ready depth. Ready cloned aliases remain ready when +another handle is wrapped. Recursive actor payload equality compares instance +identity and per-handle pending depth. Direct pending comparisons retain their +unsupported binary-operation error. Context retirement releases actor state +once. See [the implementation and verification note](completed/native_codegen/actor_identity_equality.md). + +### Occupied Resource struct constructor checkpoint + +The checked Resource path now stages named struct fields in their written +evaluation order, then commits their owned tokens to one constructor carrier. +Abandoning a partially initialized constructor retires its reached fields; +normal scope exit retires committed fields in reverse acquisition order. +Cleanup failure remains visible, and a later entry uses the existing sealed +completion transition after the prior entry has retired its custody. + +Fresh local MSVC static-CRT Debug/Release acceptance on 2026-10-12 covers the +three valid entry Sources for scope drop, reversed named arguments and abandoned +prefix: 44 Source-deleted native executions / 66 entries. The original twelve-gate +predecessor corpus passes 604 executions / 728 entries, and additional pipeline +and control-flow subset gates pass 106 / 106 against the same independently +measured private archives. All sixteen gates pass 754 executions / 900 entries. Recursive and generic aggregates, collections, +projection, extraction, replacement, reflection and captured aggregate lifetimes +remain full native-codegen obligations. This checkpoint adds no Source policy +exclusion. See the [occupied struct note](completed/native_codegen/native_resource_occupied_structs.md). + +### Selected erased-interface concrete argument contract + +An erased call remains admissible when its interface method requires additional +arguments of the receiver's concrete implementation type. Dynamic dispatch must +validate those arguments' retained exact checked concrete identities before +converting arguments or entering the concrete method. Structurally identical +nominal types are not interchangeable. Validation preserves pending failure +precedence, ordinary view semantics and argument cleanup, and supplies no clone +or ownership authority. The reference evaluator and native backend implement this +contract. Local MSVC Debug/Release gates pass, including optimized production +matching/mismatched calls after Source deletion. See +[exact concrete argument verification](completed/native_codegen/native_erased_same_owner_arguments.md) +for measured scope and remaining work. Interface-typed equality operators are +rejected by the frontend; explicit domain comparison methods remain available. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +On 2026-10-12 the managed integration passes all eighteen original Sources, +including the byte-exact wider/exact probes, named/captured callbacks, lazy reads, +repeated calls, invalid results, selector precedence, optional results and +Nothing/refined-unit pending transport. Their thirty-six Source-deleted native +executions pass against normal MSVC Debug/Release Main archives, with the +original release=false Source/debug policy and unoptimized objects preserved. +Three additive canonical Stdout controls pass six objects and twelve executions +across both Jett optimization settings and both runtime profiles: lazy repeated +calls preserve the source descriptor; invalid returns suppress the consumer; +wrong selector ownership refuses before callback body output. The four Driver +gates compare exact stdout, stderr and exit status. The ordinary public launcher +does not expose context-retirement counters. + +The current fourteen focused reference controls preserve every original thirteen +test body and add a factory/primitive-call origin-restoration control. Original +closure body and typed capture/context witnesses are constructed at the trusted +capture site. Admission and invocation compare against retained witnesses; +mutating the body, checked context, signature, ordinal, Source owner or predicate +cannot mint proof. Ordinary required constants/refinements and already-proved +ancestor transport retain their existing path. + +Capture-origin preparation now skips an unused checked-root projection only when +the exhaustive named-body scan finds no possible inline capture origin. Inline +setup stays inside its function-value arm; InlineFn, Comptime, Error and +ComptimeTypeBind remain conservative positive shapes. Reflected authentication +and pending capture snapshots remain unchanged. One isolated, unchanged native +reflection control takes 295.06 seconds before and 64.86 seconds after this +change; this measures that gate's wall time rather than general native throughput. + +After this performance change, all 2,359 workspace library tests across 22 result +groups pass, together with the four callable Driver gates (48 native executions), +27 broader interface, 10 erased-owner and nine actor native regression tests. +Fresh current compiler/source/binary/dep-info guards also accept all sixteen +private Resource gates: 754 Source-deleted executions and 900 entry completions. +Only the unchanged, independently measured v4 Debug/Release ABI1/wire3 static-CRT +runtime archives are reused, with compiled receipt pin +`bc2ad54743380441f074d8d5d212048ecf440669817c0cb69ce2fd0810ba1c95`. +The fresh post-change gate and complete source guard supply current acceptance; +old v4 execution records do not. Each Rust gate checks its existing exact +completion, script/event, message and cleanup oracles before one teardown. + +This is bounded local MSVC integration acceptance. Full coherent-workspace and +native conformance gates, exact-revision supported-host CI, GNU and installed +distribution remain pending. New nominal/secret proof establishment, protected +secret observation, broader generic/reflected forms and authenticated generated +verify/property-suite transitions remain full-goal obligations. No Source +exclusion or broad language-completion claim follows: the rough 87% estimate, +fixed 207-fixture/182-object inventory and full 100% goal remain unchanged. + +### Selected explicit comptime actor graph templates + +A pure closed explicit `comptime` result may retain actor handles. The compiler +freezes the reachable actor graph at successful required-expression completion +as an immutable template. Each runtime evaluation of that explicit expression +instantiates a fresh graph, including each call or loop activation. Ordinary +reads and aliases of the resulting bound value retain those imported actor +instances. Sharing within the exported root graph is preserved, while distinct +seed actors remain distinct. + +Import restores the evaluated state and typed captures without rerunning source +initializers, handlers, the original expression, source predicates or compiler +observations. It introduces no process-global mutable actor state and clones no +capability authority or Resource custody. Exact nominal actor, interface, +refinement and callback identities remain checked. Unpublished partial imports +must retire completely before any root or actor becomes observable. This +contract describes the current sequential evaluator; future asynchronous +scheduling requires a separate design. + +Existing Source admission remains unchanged. A closure cannot capture a move-only +Actor; the existing E0402 rejection remains required. Admitted primitive closure +captures retain their original typed context and capture witnesses. Reflected +callable adapters retain their checked plan and authenticated source; graph +transport cannot clear, reset or manufacture those proofs. No Actor-capture +resealing policy is introduced by this integration. + +The current compiler implements this contract alongside Actor identity, Resource +ownership, Owner preflight, Callback authentication and generated startup. +Protected graph producers retain their exact constructor lifecycle and ordinary +heap ownership without gaining Resource custody or capability authority. Graph +programs with no Resource kinds retain this checked ordinary storage plan without +a Resource layout or hidden Scope. Collection iteration preserves each exact +producer's storage proof. Generated Resource completion retires its actors +after companions release references and before terminal sealing, retaining +first-error and cleanup priority. Focused checks and 32 linked MSVC executions +passed; broader native and supported-host acceptance remain pending. +Namespace initializers and explicit-expression collection retain one checked +evaluation attempt per original occurrence, including failures. Observations +are retained once and baked reads do not replay them. This creates no runtime +initializer or global storage and changes no constant admission rule. +Ordinary required replacements preserve the retained checked archive through +its authenticated original-owner/context transition. Admitted TypeConstruction +values retain the sole evaluated-builder producer and its complete typed +transcript; this repair adds no new builder domain or fallback. +Ordinary actor/interface wrappers also retain their authenticated original +entry-Scope metadata through canonical collection iteration without gaining +Resource custody. Graph transport and real Resource execution still require +their separate exact proofs. +See [the graph integration note](completed/native_codegen/native_comptime_actor_graph.md). + + +### Occupied Resource struct primitive companions + +The selected companion extension follows the existing implicitly copyable +primitive surface: all eight fixed-width integer types, float32/float64, string, +bool and nothing, including transparent aliases with the same canonical checked +type. The original nominal declaration, exact field schema, written argument +order and authenticated constructor lifecycle remain authoritative. Resource +custody and reverse cleanup retain their existing rules. + +Construction and field transport retain every pending layer. They neither join +a task nor make a stored field ready. Ordinary checked expression normalization +applies the declared primitive's arithmetic and float32 rounding rules before +transport; physical validation then checks the exact carrier without rounding +an untrusted supplied value into acceptance. In particular, uint64 uses its +distinct unsigned carrier, and float32 stores an exact widening of binary32. +Focused checks and bounded linked MSVC acceptance pass against fresh +58-export runtimes, including both new scalar gates and all sixteen predecessor +Resource gates. Full inventory, workspace and supported-host acceptance remain +pending; see +[primitive companion work](completed/native_codegen/native_resource_primitive_companions.md). + + +### Ordinary failure classification through Resource scopes + +Root and nested calls share the checked entry's ordinary first-error channel. +An exact propagated ordinary failure retains its ordinary diagnostic and does +not manufacture a second Resource refusal. Real Resource errors and cleanup +failures retain their separate observations and priority. Generated Scope +completion still refuses a mismatched or unattributed nonzero Source body status; +independently observed entry body status remains distinct from the recorded +failure channel. +The selected Body report channel does not require a nonempty Resource message. + +The earlier ordinary-error reentry checkpoints preserve their historical +sessions, hashes and counts. Their duplicated `native Resource protocol refused` +bytes for ordinary-only Bounds failures are superseded as the current message +oracle. This correction changes no Source, ordinary diagnostic, body or cleanup +status, process exit, custody, retirement or reentry requirement. Corrected +native gates must pass before new acceptance is recorded. See the +[ordinary terminal-error lifetime note](completed/native_codegen/native_resource_ordinary_error_reentry.md). + +### Required expressions in inline bodies and refinement predicates + +Explicit `comptime` expressions keep the same closed, pure eligibility rules +inside an inline function or a refinement predicate as elsewhere. Every pure +function remains eligible; capabilities are unavailable during required +compile-time evaluation. These positions add no runtime Resource permission. + +The compiler must retain the exact original declaration and lexical generic and +scoped type context for each required occurrence. An inline body has its own +original syntax region within its enclosing declaration. A refinement predicate +belongs to the exact checked refinement declaration. Copied syntax, coincident +source spans, a different enclosing declaration or another compiler session +cannot substitute for that association. A successful or failed occurrence is +still evaluated once for each selected original context. + +### Native conversion proof identity + +Completing generated HIR conversions retains each actual canonical constructor's +private proof identity, including distinct occurrences with the same values. +Independent proof refreshers may replay that exact construction after checking +the complete input and original ownership proofs. A type spelling, copied token, +or mutable current table grants no replacement authority. Failed completion +publishes no partially converted Program or renewed archive. These compiler +invariants change no Source admission, equality, purity, ownership or comptime +rule. Verification remains recorded separately in the +[canonical conversion note](completed/native_codegen/native_canonical_value_conversions.md). diff --git a/docs/open_design/actor_equality.md b/docs/open_design/actor_equality.md new file mode 100644 index 00000000..c3e9d5f4 --- /dev/null +++ b/docs/open_design/actor_equality.md @@ -0,0 +1,30 @@ +# Actor handle equality + +Decision status: live instance identity implemented in the reference evaluator and native backend; local Debug/Release native acceptance passes. See [the active selected contract](../completed/native_codegen/actor_identity_equality.md). The observations and alternatives below record the prior unresolved state. + +## Historical divergence and alternatives + +Before this implementation, the checker accepted `first == first` for actor handles, but the +interpreter rejected it at runtime as an unsupported binary operation. An enum +whose payload is an actor handle was also accepted; the interpreter's recursive +enum equality compared the actor IDs, while native aggregate enum equality +rejected that payload type. These paths needed one language rule before native +codegen can claim parity. + +Two coherent choices are: + +- Give actor handles identity equality. Direct `==` and `!=`, and actor values + nested in enums and collections, compare the same live actor identity. This + requires aligning the interpreter's direct operation with its existing + recursive comparison and extending native aggregate equality. +- Reject actor equality, directly and when an actor occurs inside a compared + enum or collection. This requires checker diagnostics for the nested forms + and removal of the interpreter's incidental recursive comparison. + +The decision should preserve user-defined struct equality's separate exact +`Equatable.equals` rule. Differential tests should cover the same cloned actor +handle, distinct spawned actors, nested enum and list payloads, and `!=`. + +## Selected contract + +Actor equality now selects live instance identity. Exact checked nominal actor types must match; runtime IDs must belong to the owning context. Cloning preserves identity, distinct spawns remain unequal even with equal state, and != negates the same comparison. State is never structurally compared. See [the selected implementation note](../completed/native_codegen/actor_identity_equality.md). Escaping comptime actor graph import remains a separate contract. diff --git a/docs/open_design/comptime_actor_values.md b/docs/open_design/comptime_actor_values.md new file mode 100644 index 00000000..9743a5a8 --- /dev/null +++ b/docs/open_design/comptime_actor_values.md @@ -0,0 +1,66 @@ +# Actor handles escaping explicit comptime evaluation + +Before graph integration, a closed `comptime spawn Counter()` passed checking +and evaluation but saved only an actor ID. Its interpreter instance and state +were discarded. Reference execution failed on the first message with +`unknown actor instance #0`, and native materialization could not reconstruct +the actor from that ID. These are historical pre-graph observations. + +Pure code can use a capability-free actor entirely during evaluation and return +an ordinary value. At that checkpoint, the unresolved boundary was a handle +escaping in the result, including inside an aggregate, pending value, +interface, or admitted capture. The selected contract below preserves the +evaluated graph without expanding Source admission. + +The alternatives considered were: + +- Reject escaping actor handles during compilation, consistently in reference + execution and native builds, while permitting internal actor use whose final + result contains no actor handles. +- Preserve and materialize the evaluated actor graph. This needs defined + identity, shared-reference, state, capture, and repeated-evaluation semantics; + copying an integer ID or rerunning the original spawn is insufficient. + +Do not silently execute the original comptime expression at runtime. The chosen +contract must cover direct and nested escaping values and an actor used entirely +inside an expression that returns a scalar. + +`tests/native/comptime_actor_computation.jett` verifies the independent supported +case: a capability-free actor is spawned, updated, and queried entirely inside +a pure function. Its scalar result agrees between ordinary native execution +and explicit comptime materialization. This does not decide escaping handles. + +## Selected graph contract and current bounded acceptance + +The observations and alternatives above record the pre-graph implementation. +The selected direction preserves pure closed results as immutable frozen actor +graph templates, taken at successful required-expression completion. Every +runtime evaluation imports a fresh graph, including repeated calls and loop +activations. Bound aliases share the imported instances; a graph-wide memo +preserves sharing and keeps distinct seed actors distinct. + +Import restores evaluated state and admitted typed captures without replaying +initializers, handlers, the original expression, source predicates or compiler +observations. Exact actor, interface, refinement and callback identities and +private checked required-value joins remain intact. It creates no global mutable +actor state and clones no runtime capability authority or Resource custody. +Unpublished partial imports retire completely before publication. The scalar +`tests/native/comptime_actor_computation.jett` control remains independent. + +This choice preserves current Source admission. Actor closure capture remains +move-only and rejected with E0402; admitted primitive captures retain their +existing capture/context witness. Reflected callable transport preserves its +checked plan and authenticated source. No witness reset or new Actor-capture +resealing policy is introduced. Future admission of such captures would require +a separate checked transformation design before implementation. + +The contract is integrated for the current sequential evaluator. Current local +MSVC acceptance passes 32 Source-deleted graph executions across the normal +Debug/Release runtime profiles with unchanged compiler and launcher guards. +The receipt is `target/native/actor-graph-native-acceptance-v6/after.json`, SHA256 +`ecbba9f7539f250d8c48be4ee9ef9256e918bf95ab2d1731c39ee10460366d76`. +These bounded results do not establish whole-language native parity. Wider +admitted graph compositions, complete workspace and exact-revision supported-host +CI, GNU and installed-distribution acceptance retain their separate obligations. +Future asynchronous mailbox/task freezing remains a separate design. See +[the graph integration note](../completed/native_codegen/native_comptime_actor_graph.md). diff --git a/docs/open_design/debug_print_hidden_secrets.md b/docs/open_design/debug_print_hidden_secrets.md new file mode 100644 index 00000000..d6dc94d7 --- /dev/null +++ b/docs/open_design/debug_print_hidden_secrets.md @@ -0,0 +1,86 @@ +# Secrets hidden from debug observations + +## Decided: accepted debug Print/Println values use typed redaction + +Non-release `print` and `println` recursively redact secrets hidden by otherwise +accepted interface, refinement, aggregate, or reflected builder values. This +retains the existing public-value admission policy. Rejecting every interface or +builder would remove accepted public programs without proving non-disclosure. + +The canonical marker is `[redacted]`, shared with typed trace/reflection output +and the native debug schema. Rendering visits the secret qualifier before its +payload and inner pending wrappers. Public text equal to the marker remains +public. Concrete nominal/generic identities and stored builder field types +supply the observation graph; structural field similarity never supplies a +missing owner or field contract. An interpreter observation with missing +composite/field metadata produces a conservative redacted subtree instead of +exposing raw payloads. Such fallback is not evidence that checked metadata +propagation is complete. Native malformed schema/handle guards remain errors; +this fallback does not convert them into redacted successes. + +Checked caller argument types are retained before argument evaluation, including +nested generic/scoped/reflected calls and ownership transfer. Each type remains +paired with its actual value. Values are evaluated once in source order; the +call emits one complete event only after every argument succeeds. `view`, owned +argument consumption, spacing, newline and diagnostic stream behavior remain +unchanged. Formatting invokes no getter, Displayable method or callback and does +not inspect closure captures or actor state. + +Direct secret arguments remain compile-time errors, as do the existing Resource +root restrictions. Source release checking retains E0362 for both functions. +Debug/Release runtime archive profiles do not authorize Source release calls. +Native typed redaction and concrete schema checks remain independent defenses. + +The historical reference interpreter rendered a raw secret field from a record +erased to an interface, while the native formatter already used typed redaction. +That disclosure is a defect observation, not a parity oracle. Compare exact +public and secret-bearing Source output through real reference execution and +Source-deleted linked executables; object emission alone does not close the gap. + +## Pending-value terminal diagnostics + +Print/Println rendering and terminal failure output are separate observation +paths. The direct-field diagnostic successor below applies the existing secret +non-disclosure contract to its demonstrated receiver failure. Other terminal +paths retain their existing native typed guards and need separate parity evidence. + +Three original checked pending-sum probes demonstrated a reference disclosure: +`01_pending_secret_error.jett`, `02_pending_optional_secret_field.jett`, and +`03_pipeline_pending_secret_error.jett`. They reached the terminal failure after +only `before\n` stdout and no debug events, exposing the secret child through +raw `Value` display. Their original Source bytes remain unchanged. + +Direct target types are now captured before evaluation, and pipeline steps carry +their full result type separately from the handler error-child type. Unsupported +sum receivers render from their original wrapped values before consumption, +preserving public pending depth and redacting only secret subtrees. Ready sums, +handler error typing, earlier target failures and once-only effects retain their +behavior; diagnostics neither join nor declassify values. + +All 659 Comptime tests pass. Six permanent controls cover nine programs and pass +18 Source-deleted native executions against normal Debug and Release runtimes. +They include the original three leak witnesses, ready/error-order controls, a +parenthesized view of a named callable, a named view-parameter pipeline, and a +retained view-parameter callback. Both existing public borrowed-sum controls pass +all twelve source cases and 24 Source-deleted native executions with exact public +messages. Source policy stays at its default independently of runtime profile +for the new secret controls. These bounded results do not establish every other +terminal diagnostic or full workspace/platform acceptance; see the +[native audit](../completed/native_codegen/native_acceptance_audit.md#typed-pending-sum-terminal-diagnostics). + +Direct field reads from a pending `secret[Struct]` previously exposed raw +reference data after stripping the typed wrapper, while native field readiness +rendered `[redacted]`. The direct-field diagnostic successor retains the checked +receiver type and renders the original wrapped value before consuming it, applying +the same redaction without joining pending values or changing failure order. +The original `07_pending_secret_struct_receiver.jett` source is retained; its gate +requires one shared redacted failure message and passes with both normal runtime +profiles after Source deletion. All seven secret-owner controls and eight adjacent +native debug/pending controls pass. This repair does not establish parity for the +separate `handle` or pipeline failure paths. + +The accepted-print controls compare exact reference events and native output for +public and secret-bearing erased records, generic fields, incomplete builders, +nested containers and pending payloads. Nine programs pass 18 Source-deleted +executions across Debug and Release runtime profiles. Public text equal to +`[redacted]` remains ordinary public text. diff --git a/docs/open_design/environment_argv_capability_contract.md b/docs/open_design/environment_argv_capability_contract.md index ec5efcfd..72b31366 100644 --- a/docs/open_design/environment_argv_capability_contract.md +++ b/docs/open_design/environment_argv_capability_contract.md @@ -3,10 +3,11 @@ Status: selected design from [#94](https://github.com/vycdev/jett/issues/94); interpreter-backed implementation is tracked by [#170](https://github.com/vycdev/jett/issues/170). -The interpreter-backed launch snapshot, capability enforcement, source-owned -public surface, private kernels, deterministic injection, and ambient-builtin -migration diagnostics are implemented. Later backend preservation in slice 4 -remains pending. +The shared launch snapshot, capability enforcement, source-owned public +surface, private kernels, deterministic injection, and ambient-builtin +migration diagnostics are implemented. Native codegen now executes the +production and injected Environment fixtures. Platform-raw capture and broader +backend preservation still need verification. ## Context diff --git a/docs/open_design/global_constant_execution.md b/docs/open_design/global_constant_execution.md new file mode 100644 index 00000000..d06bfa49 --- /dev/null +++ b/docs/open_design/global_constant_execution.md @@ -0,0 +1,69 @@ +# Global constant execution + +The design selects immutable compile-time constants baked into the binary, +subject to declaration order and namespace isolation. Runtime initialization +and mutable global storage are forbidden. E0377 rejects mutable namespace-level +declarations; local mutable bindings remain unaffected. + +## Implemented contract + +The frontend accepts literals (including signed numeric spellings), earlier +same-namespace constant references, parentheses around these forms, or explicit +`comptime expression`. All required calculations, including operators, +interpolation, and calls, use that canonical explicit spelling. An ordinary +calculation initializer reports E0378 even when its operands are known. Every +semantically pure function remains eligible at an explicit site. Ordinary pure +expressions outside required evaluation keep their runtime behavior, and optional +optimization cannot change source validity. + +A literal spelling intrinsically denotes a value, and a constant reference reads +an earlier value. Recognizing a negative numeric literal is part of its spelling, +not a general authorization to evaluate unary expressions. Computing `base * 2`, +`not enabled`, `-base`, or an interpolated string executes a calculation. Making +the destination immutable does not authorize that execution implicitly. Write +`int64 doubled = comptime (base * 2)`; a direct copy may use `int64 copied = base`. +This distinction preserves the unconditional explicit-evaluation rule in +`docs/design.md` and does not add an implicit operator or function boundary. + +Compilation materializes permitted namespace constants in declaration order, +evaluating calculations only at explicit sites, including unused declarations. +Their checked values are retained by declaration +span and supplied to reference execution and verify/property drivers. Native HIR +uses declaration-keyed `Constant` reads and replaces them with typed baked values +before MIR lowering. Both backends therefore receive the same immutable value, +without a startup initializer or a mutable global store. Generic and inline +function bodies retain the constant declaration identity rather than capturing +ambient caller state. + +The supported subset is fixed-width integers, `float32`, `float64`, `bool`, +`string`, `nothing`, and transparent aliases of those primitives. Evaluated +values must match their declared primitive shape and width. A `run` expression +can retain hidden pending state despite having a primitive checked type; such a +value reports E9001 instead of entering the baked-value table. Evaluation errors +also report E9001 before execution or native publication. + +Debug `trace` reads supported constants with their declared type spelling. +Native lowering uses a scoped observation temporary that does not enter later +breakpoint snapshots; release builds remove the trace. + +## Remaining ownership policy + +Move-only constant values still need a precise read and ownership rule before +execution support is extended to them. Do not implicitly clone a move-only +constant on every read, silently consume shared storage, or introduce mutable +references. Opaque resources, actors, pending tasks, and erased payloads require +separate scrutiny for hidden state or authority. Callable constants must retain +declaration context and their permitted captures. + +Other constant types, including aggregates and nominal refinements, resources, +actor/task values, function values, and erased payloads, conservatively report +E9001. This is an implementation boundary while ownership remains unresolved, +not a decision to narrow the language's constant contract permanently. Local +values and supported explicit `comptime` results outside namespace declarations +keep their existing behavior. + +Extending namespace constants beyond the current supported domain requires +selecting this ownership policy first. Current native acceptance validates the +admitted baked values and conservative frontend rejections, together with the +final revision's independent inventory, workspace and distribution gates. The +primitive handoff alone does not establish complete native parity. diff --git a/docs/open_design/interface_same_owner_arguments.md b/docs/open_design/interface_same_owner_arguments.md new file mode 100644 index 00000000..385b90a2 --- /dev/null +++ b/docs/open_design/interface_same_owner_arguments.md @@ -0,0 +1,63 @@ +# Concrete argument requirements in erased interface calls + +An interface method may mention its interface type in more than one parameter: + +```jett +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +``` + +The implementation contract substitutes the concrete owner for those parameters. +Thus `implement Pairable for Left` declares both parameters as `Left`. This is +also the shape of the standard `Equatable.equals` implementation contract. + +The checker nevertheless accepts a dynamic call with two `Pairable` operands +whose concrete owners differ. A probe with `Left(name: "one")` and +`Right(name: "two")` enters the `Left` implementation in the interpreter and +prints `left:one:two`, despite the second argument's nominal type. Native code +checks the concrete owner when unboxing and fails with the internal diagnostic +`invalid native struct handle or field`. + +Before selection, the language tradeoff was whether to: + +- Permit erased calls and validate every concrete argument requirement at + runtime, reporting a typed dispatch error before entering the implementation; + or +- Reject erased calls whose implementation requires another argument of the + receiver's concrete type, unless a checked static fact establishes that owner. + +Do not reinterpret a different nominal struct as the selected owner's layout, +remove native unboxing checks, or add structural compatibility. The chosen rule +must cover view and owned parameters, nested occurrences of the owner type, +pending arguments, matching owners, and consistent cleanup/error precedence. +This is independent of the decision about equality operators on interfaces. + +## Selected runtime contract: exact concrete arguments + +The native-codegen goal selects runtime validation rather than banning an erased +call whose static interface signature contains another occurrence of the owner. +Selection was recorded before implementation on 2026-10-08. Source accepted by +the checker remains accepted; an erased interface is not a proof that two values +have the same nominal implementation owner. + +Resolve the receiver's exact checked concrete type and selected method first. +Before converting any argument to that concrete signature or entering the method +body, validate each concrete argument requirement against the argument's retained +checked concrete identity. Different nominal structs remain different even when +their fields have identical names and representations. This also applies to +checked generic arguments, refinements, primitive widths, and nested occurrences +of the owner in parameter types. Matching owners retain ordinary view semantics; +validation grants neither cloning nor ownership transfer. + +A mismatch fails dispatch before the implementation body. It follows existing +runtime failure propagation and argument cleanup, with no user-defined body +execution. Pending receiver/argument failures retain their existing evaluation +and error precedence; validation must not inspect an unavailable payload or +replace an earlier failure. Native unboxing and handle checks remain mandatory. +This decision does not define equality operators on erased interfaces. + +The reference/native implementation and local MSVC Debug/Release gates are +recorded in [exact concrete argument verification](../completed/native_codegen/native_erased_same_owner_arguments.md). +That bounded acceptance includes optimized production matching/mismatched calls; +full refinement/pending Source, GNU, installed and coherent native parity remain +open. No recursive Self substitution or interface equality rule is introduced. \ No newline at end of file diff --git a/docs/open_design/interface_value_equality.md b/docs/open_design/interface_value_equality.md new file mode 100644 index 00000000..8122cb2b --- /dev/null +++ b/docs/open_design/interface_value_equality.md @@ -0,0 +1,48 @@ +# Equality of erased interface values + +## Selected contract + +`==` and `!=` are rejected at compile time when an operand's comparison type +is an interface, including the `Equatable` interface itself. Transparent aliases, +refinements and secret wrappers cannot restore an equality operation after +erasure. Concrete generic instantiations, inline function bodies and explicit +comptime expressions use the same rule. The diagnostic is E0376. + +An enum's implicit payload comparison also cannot reach an interface value, +including through containers, machines and refinement wrappers. The recursive +check stops at a user struct's explicit `Equatable.equals` method: that method +owns the comparison and may deliberately ignore an interface-typed field. + +Compare concrete values before erasure, or call an explicit domain comparison +function or interface method. Such a method can expose and compare an intended +primitive key; unrelated implementations may share that domain contract without +acquiring a general dynamic equality operation. Ordinary secrecy and ownership +rules still apply to these calls and their results. + +## Rationale + +Jett keeps policy explicit and locally visible. An interface says which +operations its implementation supplies; erasing a concrete type must not add a +comparison whose behavior depends on a hidden payload. Dynamic equality would +need additional rules for different concrete types, unsupported payloads, +secrets, pending values and method dispatch. Rejecting the operator provides one +checked rule without introducing a second equality protocol. + +This preserves exact concrete `Equatable.equals` dispatch, the rule that `!=` +negates that method, and actor identity comparison. There is no handle-address +comparison or structural fallback for user structs. Direct collection equality +continues to report E0376 with its existing contents-comparison diagnostic. + +## Previous behavior + +The checker accepted same-interface operands without an equality contract. +Native verification rejected the binary expression, while the interpreter +discarded concrete metadata and attempted the ordinary payload operation. Two +erased integers could compare successfully; unrelated payloads failed at +runtime. That accidental behavior is replaced by the compile-time rule above. + +The decision is recorded here to preserve the original design discussion; +this document no longer describes an open policy choice. Driver fixtures cover +the rejected source forms and positive explicit comparisons, and native +conformance checks frontend rejection before artifact publication plus linked +positive comparisons with the source removed. diff --git a/docs/open_design/list_sum_type_contract.md b/docs/open_design/list_sum_type_contract.md new file mode 100644 index 00000000..7e2e7530 --- /dev/null +++ b/docs/open_design/list_sum_type_contract.md @@ -0,0 +1,145 @@ +# List summation type contract + +## Selected domain + +The selected domain is exact numeric primitives and transparent aliases of those +primitives only. +Nominal refinements, secret wrappers, and nonnumeric types are rejected during +checking, including empty lists, inferred/generic specializations, pipelines, +concrete function wrappers, runtime code, and explicit `comptime` code. The same +rule applies to public `list.sum` and the private trusted `list.__sum` kernel. + +This keeps the result locally predictable: it has the selected primitive width +and a known zero identity. A refinement is not necessarily closed under wrapping +addition and may exclude zero. Secret arithmetic needs an explicit secrecy +contract; admitting it by unwrapping its type would silently cross that boundary. +There is no public addition/identity interface and no implicit coarsening or +declassification. Callers can explicitly convert values using existing language +operations when their intended numeric domain permits it. + +The checker owns this policy at the closed intrinsic signature. It checks each +concrete source-facade body rather than recognizing public call spellings, so +ordinary generic specialization cannot bypass it. Existing primitive-width, +typed-zero, pending-input, and borrowing behavior remains unchanged. +The private kernel requires one explicit type operand, already supplied by the +public facade, so an omitted operand cannot become an unchecked wildcard. + +The probes below record the pre-decision mismatch, not current accepted behavior. + +## Existing numeric behavior and historical boundary + +The numeric-primitive behavior is selected in [the language design](../design.md) +and [the architecture](../architecture.md): `list.sum[T]` borrows its list, +integer addition wraps at the checked primitive width, floating addition rounds +at that width after each step, and empty numeric lists return zero of that type. +The implementation preserves the existing pending-container and pending-element +errors. Transparent aliases normalize to their primitive target and use the same +contract. The linked `list_sum_primitives` regression includes an `int8` alias, +all ten primitive numeric widths, typed empty zero, interface erasure, runtime +and explicit comptime values, and verify/property bodies. + +The original numeric implementation left nominal refinements and nonnumeric types +without a summation contract. It selected no public addition/identity interface, +refinement validation result, or secret-wrapper admission. + +The public source signature in `stdlib/list.jett` remains unconstrained: + +```jett +export function sum[T](view items: list[T]) returns T: + return list.__sum[T](view items) +``` + +Before this decision, the private kernel signature in +`crates/jett_typecheck/src/checker.rs` accepted `list[T]` and returned `T` without +checking its numeric domain. Native admission +in `crates/jett_codegen_cranelift/src/values.rs` accepts only an exact numeric +primitive element and an identical result type. The interpreter's selected +primitive path uses canonical alias expansion; before this decision its older +fallback remained reachable for other checked types. That fallback chooses an +integer or floating carrier from the first element and returns `Int64(0)` for any +empty list. + +## Reproduced accepted gaps + +These probes were run with a freshly built CLI at `8fdfa027`, after the primitive +sum and pending-aggregate fixes. Each direct runtime program passed frontend +checking. Except for the alias control, native emission rejected them with +`invalid native list signature for list.__sum` before an executable is produced. + +| Checked element type and input | Reference outcome | Native outcome | +| --- | --- | --- | +| Transparent `uint64` alias, maximum value plus 2 | `1` | Same linked output | +| Transparent `float32` alias, empty list and 16777216 + 1 - 16777216 | `0` and `0` | Same linked output | +| `Positive = int64 where value > 0`, values 1 and 2 | Refined total 3 | Rejected at native admission | +| The same `Positive`, empty list | Terminal refinement constraint failure | Rejected at native admission | +| `SmallPositive = int8 where value > 0`, values 127 and 1 | Terminal `int8 value 128 is outside range -128..127` | Rejected at native admission | +| `bool`, one true element | Terminal `list.__sum: list elements must be int64 or float64` | Rejected at native admission | +| `bool`, empty list | Integer zero; trace says `bool = 0` | Rejected at native admission | +| `string`, empty list | Integer zero; trace says `string = 0` | Rejected at native admission | +| Struct with an int64 field, empty list | Integer zero; later field access reports `field access is not supported on 0` | Rejected at native admission | +| `optional[int64]`, empty list | Integer zero; later handling reports `handle block requires a result or optional value, got 0` | Rejected at native admission | +| `secret[int64]`, values 1 and 2 | Explicitly declassified total 3 | Rejected at native admission | +| `secret[int64]`, empty list | Explicitly declassified zero | Rejected at native admission | + +The bool gap has a small standalone reproducer: + +```jett +namespace app +function main() returns nothing: + list[bool] values = list() + bool total = list.sum[bool](view values) + trace total + println(total) +``` + +Its reference output is `trace total: bool = 0` followed by `0`. This is a wrong +runtime value shape, not evidence that bool summation or a bool zero is selected. +Moving the call into a closed pure function and invoking it with `comptime` +still prints `0` in the reference path. Native HIR materialization rejects it: +`cannot materialize generated Int64(0) as checked Bool`. Required compile-time +evaluation therefore does not close the malformed-value gap. + +A numeric refinement can already produce a successful reference result: + +```jett +namespace app +type Positive = int64 where value > 0 +function main() returns nothing: + Positive first = 1 handle error: + return nothing + Positive second = 2 handle error: + return nothing + list[Positive] values = list(first, second) + Positive total = list.sum[Positive](view values) + println(coarsen total) +``` + +This prints `3`; an empty list fails the refinement predicate at the return +boundary. A closed pure helper computing and coarsening that total also returns +3 with explicit `comptime`, while its native build still rejects the unsupported +kernel specialization. Neither success establishes a contract for arbitrary +refinements, empty refined lists, or invariant-breaking overflow. + +## Deferred extensions + +The selected domain closes the source-admission gap. Any future extension requires +a separate design decision; the historical fallback is not its specification: + +- If nominal numeric refinements are admitted, how the result preserves or + validates their invariant, what return type carries validation failure, and + what happens when the empty-list zero violates the predicate. Primitive-width + wrapping alone cannot prove a refinement is closed under addition. +- Whether secret-wrapped numeric types are admitted, and how their selected + arithmetic width and result secrecy are retained across runtime/comptime + boundaries. +- Whether any nonnumeric summation is intended. A general extension would need + an explicit addition and identity contract; the current unconstrained `T` + signature does not provide one. + +The implementation must not infer these choices from the fallback's successful +zero or from the primitive carrier used by a refinement. In particular, applying +`primitive_base_type_name` to all summation types would silently erase refinement +and secret boundaries while leaving the return/invariant questions unresolved. +The numeric-only frontend admission makes the old fallback unreachable from these +checked source specializations. Native admission remains conservative; a rejected +specialization is not an implemented numeric or refinement conversion. diff --git a/docs/open_design/print_debug_builtin_policy.md b/docs/open_design/print_debug_builtin_policy.md index 244decad..7c7d7f23 100644 --- a/docs/open_design/print_debug_builtin_policy.md +++ b/docs/open_design/print_debug_builtin_policy.md @@ -1,8 +1,8 @@ # Print Debug Builtin Policy Status: decided by [#8](https://github.com/vycdev/jett/issues/8). The language -policy and release-mode diagnostics are implemented; dedicated debug-event -isolation and backend conformance remain pending. +policy, release-mode diagnostics, and dedicated debug-event channel are +implemented. Hidden-secret observation remains a separate open policy. `print` and `println` remain a stable compatibility surface as compiler-owned, debug-only builtins. There is no scheduled removal, but they are not ordinary @@ -10,18 +10,38 @@ output APIs and must never become an implicit spelling of `Stdout.write`. ## Current Implementation -The checker currently accepts arbitrary non-secret value arguments, returns -`nothing`, and does not classify either builtin as capability-requiring. The -interpreter formats arguments with spaces, adds a newline only for `println`, -and uses the same stdout path as `Stdout.write`. Depending on the entrypoint, -that path is either written directly to process stdout or captured into the -combined run output. - -Those shared-channel details are transitional, not the stable contract. -`--release` now reaches the checker and rejects both builtins with E0362. Verify -execution does not yet isolate debug text from agent output. The remaining -requirements below apply when those debug-event and backend boundaries are -added. +The checker accepts checked non-secret value arguments, returns `nothing`, +and does not classify either builtin as capability-requiring. Formatting and +ownership remain unchanged. Arguments finish evaluation before one complete +event is emitted; failure in a later argument publishes no portion of that call. + +The interpreter records explicit Trace, Breakpoint, Print, and Println events in +one ordered buffer. Event text contains exactly the operation's intended bytes, +including partial prints and embedded CR/LF. Silent compilation and capture +workers never write those bytes to process output. Human runtime entrypoints and +the native DebugPrint leaf emit them to diagnostic stderr. Capability-backed +`Stdout.write` retains application stdout. + +Driver captures preserve comptime, frontend verification, and runtime phases. +Comptime observations are separate from baked values. Verification retains +actual block/trial events, including first failure; shrinking and private replay +stay isolated. Tools retain preceding observations on later build, lowering, +linking, or runtime failure without rerunning workers. Agent renderers escape +exact text with an explicit phase and kind; a printed trace prefix or protocol +header is ordinary print text. + +Failed comptime recovery records actual attempted expression/context pairs +without inserting a baked value. Duplicate checked contexts and namespace +initializer markers do not repeat the error or its observations. Native property +replay failures retain the original suite streams separately from the actionable +replay cause; private runtime replay streams do not become public observations. +This retention applies when the original attempt returned captured process +output. An initial execution error or timeout retains its existing typed error +contract; it does not invent a completed original-suite capture record. + +`--release` still rejects both names with E0362, checks their arguments, and +preserves existing output artifacts. This channel change does not select the +separate policy for secrets hidden by interfaces or builders. ## Decision @@ -69,10 +89,11 @@ function emit(view stdout: Stdout, message: string) returns nothing: ## Compatibility And Migration -The current interpreter behavior remains valid while the dedicated debug-event -channel is pending. In particular, `tests/run_pass/stdlib_loading.jett` may keep -its no-capability `println` fallback until the loader smoke test has a dedicated -diagnostic assertion. +`tests/run_pass/stdlib_loading.jett` keeps its no-capability `println` failure +fallback as a debug event. Programs and tools that previously captured print +text as application stdout must now read the diagnostic event capture. Native +transport uses exact stderr bytes, and agent mode represents events in escaped +structured rows. Production examples and APIs must use `Stdout.write`. Code that needs structured debug facts should prefer `trace` or `breakpoint`; unstructured `print` calls @@ -80,30 +101,22 @@ are for short-lived inspection and simple fixtures only. Future mode diagnostics must say that directly instead of suggesting an implicit capability or silently changing the call's meaning. -## Conformance Work - -Existing coverage includes `stdout_output_can_be_captured` in -`crates/jett_comptime/src/interpreter.rs` for spacing, newline, and current -shared-channel order, -`tests/compile_fail/secret_print.jett` for bare secrets, and -`tests/compile_fail/refined_secret_exposure.jett` for refinement-wrapped -secrets. The stdlib fallback lives in `tests/run_pass/stdlib_loading.jett`. - -The remaining policy coverage is: - -1. Add a driver fixture for multiple `print` / `println` calls and their - relative captured order. -2. Add verify/comptime coverage once their debug events are isolated from plain - and agent protocol output. -3. Release compile-fail coverage for both names is implemented by - `tests/compile_fail/release_debug_print.jett`; it also proves rejected call - arguments are still typechecked. -4. Add backend conformance tests proving that unsupported debug builds reject - the calls and that supported debug builds use a diagnostic channel rather - than application `Stdout`. -5. Add agent-output coverage that distinguishes captured debug events from - capability-backed application output. - -Until those phases exist, the conservative implementation rule is: do not add -more capability-free output APIs, do not advertise `print` or `println` as -logging, and do not lower either name as ordinary native I/O. +## Conformance + +Interpreter and driver tests cover exact spacing and terminators, empty events, +partial and multiline text, Unicode, explicit kinds, and application/debug +separation. A failed later argument retains preceding events while publishing +none of the failed call. Dedicated native transport cases compare exact stderr +with the ordered runtime event capture after source deletion, including event +and terminal-error adjacency, ownership cleanup, and compiled suites. + +Comptime and verification capture tests cover successful and failed evaluation, +baked-value reads, property generation and trial order, and private shrinking. +CLI renderer tests preserve phase and kind on success and failure and escape +printed text resembling protocol headers. Existing direct-secret rejection and +release E0362 gates remain separate, including typechecking rejected arguments +and preserving existing artifacts. + +The remaining hidden-secret observation choice is recorded in +[debug_print_hidden_secrets.md](debug_print_hidden_secrets.md). No additional +capability-free output APIs or implicit application I/O are introduced here. diff --git a/docs/open_design/projected_field_assignment.md b/docs/open_design/projected_field_assignment.md new file mode 100644 index 00000000..b01c0d76 --- /dev/null +++ b/docs/open_design/projected_field_assignment.md @@ -0,0 +1,23 @@ +# Projected field assignment: selected local owner updates + +Status: decided on 2026-10-15 after the user delegated language choices to fit Jett's philosophy. Checked updates through owned mutable values are selected. Implementation and verified acceptance are tracked in the [native acceptance audit](../completed/native_codegen/native_acceptance_audit.md); this selection alone is not evidence that either backend executes projected writes. + +Jett's imperative/local-mutability and small logical diff goals favor `record.field = value` for an exact live mutable owner, rather than rebuilding every unrelated constructor field for a one-field repair. Immutable defaults, explicit consuming transforms and read-only views remain. The full selected contract and implementation obligations live in [owned mutable field updates](../completed/native_codegen/owned_mutable_field_updates.md). + +The eligible place is a finite checked field path with exact resolved binding, nominal/generic owners and field types. Existing state qualification or checked narrowing may supply an exact machine field; no state transition is implied. No view, mutable reference, temporary, consumed owner or outstanding dependent loan authorizes a write. No qualifier peeling or structural field match supplies ownership proof. + +An enclosing nominal refinement is replaced and validated as a whole instead of edited internally. A complete refined field may be replaced under its existing checked conversion rules. Bitfields retain their constructor-and-rebind contract, including declared width validation; the former table entry proposing direct masked assignment is superseded. + +The existing compiler-produced reflection metadata authority is preserved. +Projected writes do not enter canonical builtin metadata owners to edit their +fields. Capture exact builtin identities at creation and retain them through +type-table cloning/remapping; aliases, qualification and user holders cannot +remove the boundary. Whole genuine metadata fields remain replaceable in user +holders, and user types with similar names remain ordinary records. This +selection precedes implementation and introduces no new observer-provenance API. + +Preflight the place and ancestor readiness/state before the RHS. Existing pending inspection errors remain runtime behavior for admitted pending transport; no implicit join or payload read is added. Evaluate the RHS once, preserve the old field on precommit failure, and transfer/retire replacement ownership exactly once. Preserve untouched fields, metadata, task depth and logical clone independence. Resource-bearing paths need their authenticated custody protocol and cannot use an ordinary update as a bypass. + +Historical baseline: the owned mutable integer/String probe passed checking, while reference execution reported `only simple variable assignment is supported in comptime` and MIR rejected a nonlocal assignment without materialized ownership. Existing immutable/view/temporary rejection and projected-read/reconstruction controls remain authoritative. Their retained Source bytes and historical receipts must not be rewritten to turn this decision into a pass. + +Ordinary-record and exact-state machine updates now have checked reference, required-comptime and bounded Source-absent native acceptance. Actor handler state, Resource-bearing replacement and wider reflected/generic compositions remain implementation work. Full coherent workspace, inventory and supported-host acceptance remain separate gates; the audit records their actual revision and scope. diff --git a/docs/open_design/reflected_field_request_compatibility.md b/docs/open_design/reflected_field_request_compatibility.md new file mode 100644 index 00000000..b0fb8f87 --- /dev/null +++ b/docs/open_design/reflected_field_request_compatibility.md @@ -0,0 +1,185 @@ +# Reflected field request compatibility + +Reflected field reads validate the metadata owner, member, field index, and +requested type before reading a payload. The design describes the requested +type as matching the field type. Both execution paths currently also admit +some refinement/base requests when their underlying representation and secrecy +agree. That broader compatibility rule still needs a precise language contract; +it does not establish arbitrary nominal casts or implicit coarsening. + +A concrete accepted case exposed a separate native dispatch defect. If a record, +enum, or machine stores `Selected = Named where true`, and `Selected` has its own `Named` +implementation, reading the field as `Named` retains `Selected` dispatch in the +reference evaluator. Native reflection previously read the underlying interface +handle as though it were already the requested erased value, so it dispatched +through the refinement's base payload instead. Ordinary checked conversion of +`Selected` to `Named` already preserves the refinement owner. + +For a request admitted by the existing reflection checks, native extraction +must preserve the actual declared field owner when converting it to an erased +interface. The field's representation alone cannot identify its method owner. +This follows the established nominal dispatch rule and does not broaden which +requests pass the metadata/type checks. Exact-type reads, explicit `coarsen`, +pending validation, source ownership, and failure order retain their contracts. + +The same conversion must respect the directional secret qualification rule. +For `secret[Selected]` to `secret[Named]`, the shared outer qualifier protects +the destination while `Selected` remains the concrete method owner. Peel source +qualifiers only while peeling destination qualifiers, and stop at a nominal +refinement. A secret source converted to an unqualified interface retains its +secret owner, allowing an explicit implementation for `secret[T]`; surplus +source qualifiers and secret fields retain their redacted debug layouts. + +The remaining admission question is whether reflected requests require exact +semantic types, allow a defined refinement/base relation, or use another checked +conversion relation. In particular, equal runtime carriers do not prove that +unrelated nominal refinements satisfy one another's invariants. Keep this +question distinct from preserving identity for the currently admitted interface +read. + +## Requested invariants are not producer proof + +The frontend currently accepts each request below for a field whose stored +integer is `0`, where `Positive = int64 where value > 0`. Before establishing +the requested invariants at the reflected producer, reference probes returned +`0` through a checked `Positive` consumer in all five cases. A checked call's +declared result type therefore cannot, by itself, prove the cloned payload. + +| Actual declared field | Requested type | Later exposure | +| --- | --- | --- | +| `optional[int64]` | `optional[Positive]` | Handle the occupied success arm | +| `list[int64]` | `list[Positive]` | Read an element | +| `secret[int64]` | `secret[Positive]` | Explicitly declassify the result | +| `function() returns int64` | `function() returns Positive` | Invoke the callback | +| `Box[int64]` | `Box[Positive]` | Read `Box.value` | + +For currently admitted ready builtin wrappers, a bounded producer safeguard +can establish newly requested leaf refinements before publishing checked proof. +Use actual declared schemas to retain exact invariants or skip an established +ancestor prefix. Visit occupied optional/result arms and collection members in +their existing order, preserve the source, and leave inactive arms alone. +Runtime type labels are not evidence for a replaced payload. + +Repeated alias or refinement arguments normalize independently within this +existing relation. The reference comparator now tracks only the active expansion +path; a completed sibling is not a recursive re-entry. This repairs repeated +map/result slot comparison without selecting broader admission or converting +nominal owners. Native raw carrier compatibility now follows the five admitted +builtin shapes, retaining matching secrecy and exact primitive, nominal, and +callable identities. Checked producer trees establish their new refinements +after complete occupied-shape preflight. This implements the bounded ready +builtin safeguard without selecting broader requested-type admission. + +Pending containers require a separate conversion contract. An exact +actual/requested schema preserves pending values unchanged. Establishing a +changed nested invariant must not implicitly join the container or inspect its +pending payload; refuse unsupported proof before exposing a trusted result. +This does not select broader requested-type admission or pending conversion +semantics. + +New invariants beneath `Secret` also require a protected observation contract. +Running `Positive`'s public predicate during a request for `secret[Positive]` +can trace its publicly typed input before the source's explicit `declassify`. +The existing rule allowing validation of a declared secret-backed refinement +does not select that new reflected conversion. Conservatively refuse this proof +establishment without rendering the value, while retaining exact qualified +reads. Keep the hidden-observer policy in +[debug_print_hidden_secrets.md](debug_print_hidden_secrets.md) open. + +Changed callable results now have the selected lazy return-validation adapter +contract below. A descriptor alone still cannot prove a future result. Changed +nominal generic instances need a separate selected conversion contract; walking +builtin wrappers does not validate arbitrary nominal fields. Their unsupported +proof establishment remains an explicit feature gap, not a claim of complete +reflected read coverage. + +The same observer concern applies to a successful summation fallback whose +selected type is one or more secret qualifiers around a refinement. Refusing +that unsupported proof does not define the still-open summation domain, add +container-valued summation, or change existing arithmetic and shape errors. + +### Selected lazy reflected callable return validation + +A checked reflected callable request may establish supported new return +refinements through an owned lazy adapter. Exact owner, member, field ordinal, +pending-selector and declared-schema checks precede construction. The adapter +retains the original stored signature, source body, captures, debug identity +and pending state; constructing or reading it invokes neither callback nor +predicate. Requested parameter, view and capability contracts must satisfy the +existing ordinary callable adapter relation. A descriptor or requested type +annotation grants no proof, effect authority or nominal conversion. + +Each invocation evaluates its actuals once, invokes the retained source once +under the original signature, and preserves the source's ordinary validation. +Only after the source result exists does the supported typed produced-refinement +conversion establish the unproved requested return suffix. Failed validation +uses the existing terminal refinement-boundary error and publishes no trusted +target result. Exact established prefixes are reused. The adapter never joins +implicitly or inspects pending payload; a pending result requiring a new check +reaches the existing pending-inspection refusal before predicates or consumers, +while exact already-proved pending transport remains unchanged. + +The selected contract now has an isolated checked/reference/HIR/MIR/native +prototype. Eighteen genuine Sources include the two byte-exact original wider +and exact callback probes, named and captured callbacks, lazy reads, repeated +calls, invalid results, selector precedence, recursive optional results and +ready, pending, exact and ancestor Nothing/refined-unit returns. All eighteen +reference entries and host objects pass; all thirty-six Source-deleted linked +executions pass against freshly built normal MSVC Debug/Release Main archives. +Source checking uses release=false independently of runtime artifact profile. +The native assertions compare exact stdout, stderr and exit status. The public +ordinary launcher does not expose context-retirement counters. + +Current isolated authentication gates also pass thirteen reference, five HIR +and six MIR tests. Original closure body and typed capture/context witnesses +are constructed at the trusted capture site. Admission and invocation compare +against those retained witnesses; mutating a same-span body, checked context, +signature, field ordinal, Source owner or predicate cannot mint a replacement +proof. Ordinary required constants and refinements retain their original +checking and materialization path; ancestor return transport uses the already +proved type relation without a new pending inspection or predicate. + +Those isolated results retain their original candidate and scope. The later +managed eighteen-Source callback integration is recorded in the +[native coverage plan](../completed/native_codegen/native_codegen_parity_plan.md#selected-lazy-reflected-callable-return-validation). +The generated-suite implementation below has separate bounded local acceptance; +its focused/replay and coherent-revision gates are not yet complete. New +nominal/secret proof establishment, protected secret observation, +generic and other reflected forms remain full-goal obligations. No Source +exclusion, fixture denominator change or broad completion claim follows. + +### Generated callback-suite proof continuity + +Checked Verify and Property execution now retain the original test-body owner +when constructing a closure witness. The owner is joined to the same retained +checked program, original block and namespace; active capture ownership and +Property purpose restore on both ordinary errors and panic. Public test-file +execution installs that same checked program instead of a metadata-only +projection. + +The HIR observation proof separately joins an immutable owned CallbackHolder +constructor or alias to its exact original binding, callable producer, captures +and reflected read. Matching types, spans or an edited public body cannot mint +that association. Existing full Source, current-body and CFG seals remain +mandatory; this proof grants no runtime Resource or Scope authority. + +The ordinary reflected-callable Property path now uses an opaque checked-case +plan from the original successful evaluator/cache snapshot. Its canonical HIR +factory builds the ordered 100-trial wrapper and records the exact protected +transition. Replay reconstructs from the private pre-suite graph, checks the +retained plan and original trial identities, and validates changed shrink inputs. +It cannot renew proof around a caller-appended or edited wrapper. Exact sampled +inputs use full typed snapshots rather than ordinary Value equality. Verify's +existing canonical factory remains unchanged. + +These are implementation and authentication changes under the existing lazy +return-validation contract, not new reflected request admission. Original +Sources19/20 and their permanent oracles remain exact. All three controls pass: +both reference reports record 100 Property trials, six standalone objects emit, +and six separately emitted Source-deleted objects pass twelve normal MSVC +Debug/Release launches. The [bounded audit](../completed/native_codegen/native_acceptance_audit.md#generated-reflected-callback-suites-bounded-msvc-acceptance) +records exact current compiler/Source and reused-archive guards. No ordinary +context-retirement observation is available. Both six-test Comptime/HIR groups, +all eight native Property controls and all 1,664 affected compiler-library tests +pass. Full workspace/platform acceptance remains pending. The rough 87% estimate +and fixed 207/182 inventory do not advance. diff --git a/docs/open_design/state_machine_json_contract.md b/docs/open_design/state_machine_json_contract.md index e0cb7335..9991bdb0 100644 --- a/docs/open_design/state_machine_json_contract.md +++ b/docs/open_design/state_machine_json_contract.md @@ -12,10 +12,12 @@ builder path followed by `type.construct_put` and `type.construct_finish`. - `json.serialize` and `json.serialize_public` accept bare `Machine` and `Machine at state` targets, including nested machine fields inside structs - and containers, when all serialized payload fields are JSON-compatible. + and containers. Bare targets require every state's serialized payload to + be JSON-compatible; a qualified target checks only its selected payload. - `json.parse` and `json.parse_exact` accept bare `Machine` and `Machine at state` targets, including nested machine fields inside structs - and containers, when all payload fields are JSON-compatible. + and containers. Bare targets require every state's payload to be + JSON-compatible; a qualified target checks only its selected payload. - The wire envelope has exactly two canonical keys, `state` and `payload`. Serializers do not emit a machine type tag, and exact parsing rejects extra envelope keys such as `type`. @@ -84,6 +86,69 @@ envelope shape. The malformed-envelope cases are pinned in Missing optional payload fields should use the same `none` default as struct decoding. Missing required payload fields should fail. +## Private Map Decoder Boundary + +JSON object map keys are exact strings, including transparent aliases that +resolve to string. Non-string map targets, including integer-keyed maps in a +bare machine or the selected state, retain frontend E0343 rejection. This +public domain does not change when a qualified request has an unrelated state +with a non-JSON payload. + +`type.machine_states[Machine at state]()` still exposes all declared states. +Top-level aliases remain non-matching aggregate probes with empty metadata; +the source aliases preserved below are field-level TypeInfo identities. +Nested `state.fields` loops must therefore have checked type and source-alias +candidates for every declared field. Direct active-state field loops retain +their selected payload candidates. Omitting other-state candidates masked the +old private map decoder's incompatible return: it constructed +`map[string, Val]` even when its actual T had non-string keys. + +The selected repair uses a closed, alias-aware key guard before the private +decoder's typed string-map body. Unsupported keys produce a failure rather than +a key cast or a dummy value. Valid public string-map parsing and the canonical +wire envelope are unchanged. + +This intentionally changes error precedence for malformed qualified input +that names another declared state's unsupported map payload. If field decoding +is reached, the private unsupported-key error precedes map construction and +the eventual qualified-builder state mismatch. Pre-change reference +characterization showed empty and nonempty map objects reaching that final +state mismatch; a numeric payload reached the map's expected-object error. +Lenient parsing now rejects unsupported keys before that object-kind check. +Missing required-field lookup still runs before this decoder, and exact parsing +retains its separate object/unknown-field validation before decoding. Fresh +reference characterization of the regression Session records +`cached.entries: JSON object maps require string keys, got int64` for empty +and nonempty other-state map objects. Lenient numeric payloads reach the +same key guard; exact numeric payloads first report +`entries: expected object, got number`. Missing entries still fail the +required-field lookup. All 22 linked qualified-JSON groups passed in +`target/native-json-qualified-driver-final.log`, including these literal +first-error controls in direct and pipeline forms. The later inactive-payload +decoder group also passed separately. The final-source JSON family gate passes +all 39 tests, including the 23 new groups, in +`target/native-json-qualified-json-family-final.log` (438.34 seconds). +The frozen `23ac34c4` complete workspace and doc-tests also pass in +`target/native-workspace-23ac34c4.log`, including all 346 native tests and +182 object obligations. Supported-host acceptance for that revision and +acceptance for later test additions remain independent gates. + +This is a bounded private type-correctness repair selected before guard +integration. It does not add an early machine-state gate, filter public +metadata, change global builder behavior, coerce carriers, or admit non-string +public map targets. Migration annotations and broader JSON questions remain +separate. + +The private fallback decoder also uses the visible canonical condition +`type.name[T]() == "json.JsonTree"` before raw-tree copying. This reuses +checked static selection, keeping its typed raw return body out of unrelated +inactive callable, interface, and actor field specializations. Ordinary +pure predicate calls are not folded to determine source validity or skip +typechecking. One additional group over those three inactive payloads passes +all four direct JSON APIs for the valid selected state in both runtime +profiles after source removal. It does not broaden the public JSON domain +or establish malformed-input behavior for those unsupported payloads. + ## Serialize Semantics `json.serialize[Machine]` and `json.serialize[Machine at state]` emit the same diff --git a/docs/open_design/type_alias_constructor_calls.md b/docs/open_design/type_alias_constructor_calls.md new file mode 100644 index 00000000..e35e6ad5 --- /dev/null +++ b/docs/open_design/type_alias_constructor_calls.md @@ -0,0 +1,40 @@ +# Type aliases used as constructor callees + +The design permits transparent destination type aliases, namespace aliases, +and alias owners for method lookup. It does not select whether a transparent +type alias can also spell a constructor call. + +The frontend currently accepts this bitfield example: + +```jett +namespace app +bitfield Header: + value: 8 bits +type Alias = Header +function main(stdout: Stdout) returns nothing: + Alias header = Alias(value: 7) + Stdout.write(view stdout, "{header.value}\n") +``` + +Reference execution reports an undefined function for the alias callee; native +lowering has no checked constructor argument order for it. Construction through +`Header(...)` followed by an `Alias` destination is a separate, supported case. +Reflected `TypeConstruction` metadata also does not decide this call spelling. + +Two policies need comparison before changing either backend: + +- Permit an alias callee and resolve it to the exact underlying declaration. + Argument order, generic arguments, validation/result shape, nominal identity, + and source provenance must come from that selected declaration. +- Require the declaration's constructor spelling and reject an alias callee + during checking. Destination aliases and alias method lookup remain valid. + +Nominal refinements require their own boundary and must not be treated as +transparent aliases merely because they share a runtime layout. An accepted +transparent-alias policy would also need an explicit scope for struct, +bitfield, enum and machine constructors, including generic and qualified forms. + +Until the call policy is selected, do not infer constructor permission from +destination-type compatibility or add backend name rewriting for arbitrary +aliases. This is an unresolved source contract beyond the fixed native fixture +inventory, not evidence of complete native parity. diff --git a/stdlib/json/00_types_and_decls.jett b/stdlib/json/00_types_and_decls.jett index a72cee92..ccc71ef4 100644 --- a/stdlib/json/00_types_and_decls.jett +++ b/stdlib/json/00_types_and_decls.jett @@ -112,7 +112,7 @@ function json_reflected_raw_type(view info: TypeInfo) returns bool: if base.type_name == info.type_name: return false return json_reflected_raw_type(view base) -function json_type_primitive_is_signed_integer(primitive: TypePrimitive) returns bool: +function json_type_primitive_is_signed_integer(view primitive: TypePrimitive) returns bool: if primitive == TypePrimitive.int8_type: return true if primitive == TypePrimitive.int16_type: @@ -120,16 +120,16 @@ function json_type_primitive_is_signed_integer(primitive: TypePrimitive) returns if primitive == TypePrimitive.int32_type: return true return primitive == TypePrimitive.int64_type -function json_type_primitive_is_narrow_unsigned_integer(primitive: TypePrimitive) returns bool: +function json_type_primitive_is_narrow_unsigned_integer(view primitive: TypePrimitive) returns bool: if primitive == TypePrimitive.uint8_type: return true if primitive == TypePrimitive.uint16_type: return true return primitive == TypePrimitive.uint32_type -function json_type_primitive_uses_int64_json_integer(primitive: TypePrimitive) returns bool: - if json_type_primitive_is_signed_integer(primitive): +function json_type_primitive_uses_int64_json_integer(view primitive: TypePrimitive) returns bool: + if json_type_primitive_is_signed_integer(view primitive): return true - return json_type_primitive_is_narrow_unsigned_integer(primitive) + return json_type_primitive_is_narrow_unsigned_integer(view primitive) mutual: export function json_tree_parse(raw: string) returns result[JsonTree, string] function json_tree_parse_span(view input: bytes, start: int64, end: int64) returns result[JsonTree, string] diff --git a/stdlib/json/10_tree_core.jett b/stdlib/json/10_tree_core.jett index 06ffd798..12941d69 100644 --- a/stdlib/json/10_tree_core.jett +++ b/stdlib/json/10_tree_core.jett @@ -77,7 +77,7 @@ function json_tree_is_object(view value: JsonTree) returns bool: function json_tree_field(view value: JsonTree, key: string) returns optional[JsonTree]: match value: object_value(fields): - return map.get[string, JsonTree](view fields, key) + return map.get[string, JsonTree](clone fields, key) other: return none function json_tree_index(view value: JsonTree, index: int64) returns optional[JsonTree]: @@ -89,7 +89,7 @@ function json_tree_index(view value: JsonTree, index: int64) returns optional[Js function json_tree_object_field(view value: JsonTree, key: string) returns result[optional[JsonTree], string]: match value: object_value(fields): - return ok(map.get[string, JsonTree](view fields, key)) + return ok(map.get[string, JsonTree](clone fields, key)) other: return fail(json_tree_kind_error(view value, "object")) function json_tree_array_index(view value: JsonTree, index: int64) returns result[optional[JsonTree], string]: @@ -119,7 +119,7 @@ function json_tree_array_length(view value: JsonTree) returns result[int64, stri function json_tree_object_keys(view value: JsonTree) returns result[list[string], string]: match value: object_value(fields): - return ok(map.keys[string, JsonTree](view fields)) + return ok(map.keys[string, JsonTree](clone fields)) other: return fail(json_tree_kind_error(view value, "object")) function json_tree_as_string(view value: JsonTree) returns result[string, string]: diff --git a/stdlib/json/20_tree_parse_containers.jett b/stdlib/json/20_tree_parse_containers.jett index e081475a..a72fba61 100644 --- a/stdlib/json/20_tree_parse_containers.jett +++ b/stdlib/json/20_tree_parse_containers.jett @@ -87,7 +87,7 @@ function json_tree_top_level_segments(view input: bytes, start: int64, end: int6 segments = list.append[JsonTreeSegment](segments, JsonTreeSegment(start: segment_start, end: index)) segment_start = index + 1 else: - scan = step.scan + scan = clone step.scan index = index + 1 nothing checked = json_tree_finish_scan(view scan) handle error: return fail(error) @@ -99,7 +99,7 @@ function json_tree_insert_object_segment(view input: bytes, fields: map[string, return fail(error) if map.has[string, JsonTree](view fields, field.key) == true: return fail("duplicate object field '{field.key}'") - return ok(map.insert[string, JsonTree](fields, field.key, field.value)) + return ok(map.insert[string, JsonTree](fields, field.key, clone field.value)) function json_tree_parse_object_field_span(view input: bytes, start: int64, end: int64) returns result[JsonTreeObjectField, string]: int64 colon = json_tree_find_top_level_colon(view input, start, end) handle error: return fail(error) @@ -124,7 +124,7 @@ function json_tree_find_top_level_colon(view input: bytes, start: int64, end: in return fail(error) if step.separated == true: return ok(index) - scan = step.scan + scan = clone step.scan index = index + 1 return fail("expected object field separator") function json_tree_top_level_separator_step(view scan: JsonTreeScan, byte: int64, separator: int64) returns result[JsonTreeTopLevelSeparatorStep, string]: @@ -150,21 +150,21 @@ function json_tree_segment_value_scan_start() returns JsonTreeSegmentValueScan: return JsonTreeSegmentValueScan(tree_scan: json_tree_scan_start(), started: false, scalar: false, complete: false) function json_tree_segment_value_scan_step(view scan: JsonTreeSegmentValueScan, byte: int64) returns result[JsonTreeSegmentValueScan, string]: if scan.complete == true: - return ok(JsonTreeSegmentValueScan(tree_scan: scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: scan.complete)) + return ok(JsonTreeSegmentValueScan(tree_scan: clone scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: scan.complete)) if scan.started == false: return json_tree_segment_value_scan_start_value(view scan, byte) return json_tree_segment_value_scan_continue(view scan, byte) function json_tree_segment_value_scan_start_value(view scan: JsonTreeSegmentValueScan, byte: int64) returns result[JsonTreeSegmentValueScan, string]: if json_tree_is_whitespace_byte(byte) == true: - return ok(JsonTreeSegmentValueScan(tree_scan: scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: scan.complete)) + return ok(JsonTreeSegmentValueScan(tree_scan: clone scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: scan.complete)) bool scalar = byte != 34 and byte != 91 and byte != 123 JsonTreeScan tree_scan = json_tree_scan_step(view scan.tree_scan, byte) handle error: return fail(error) return ok(JsonTreeSegmentValueScan(tree_scan: tree_scan, started: true, scalar: scalar, complete: false)) function json_tree_segment_value_scan_continue(view scan: JsonTreeSegmentValueScan, byte: int64) returns result[JsonTreeSegmentValueScan, string]: if scan.tree_scan.in_string == false and scan.tree_scan.depth == 0 and scan.scalar == true and json_tree_is_whitespace_byte(byte) == true: - return ok(JsonTreeSegmentValueScan(tree_scan: scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: true)) - JsonTreeScan before = scan.tree_scan + return ok(JsonTreeSegmentValueScan(tree_scan: clone scan.tree_scan, started: scan.started, scalar: scan.scalar, complete: true)) + JsonTreeScan before = clone scan.tree_scan JsonTreeScan tree_scan = json_tree_scan_step(view scan.tree_scan, byte) handle error: return fail(error) bool complete = json_tree_segment_value_finished(scan.scalar, view before, view tree_scan) diff --git a/stdlib/json/60_reflect_serialize.jett b/stdlib/json/60_reflect_serialize.jett index 4ec53cf4..0b9bd280 100644 --- a/stdlib/json/60_reflect_serialize.jett +++ b/stdlib/json/60_reflect_serialize.jett @@ -1,6 +1,12 @@ namespace json mutual: function json_serialize_public_reflected[T](view value: T) returns string + function json_serialize_public_checked_primitive[T](view value: T) returns string + function json_serialize_public_checked_numeric[T](view value: T) returns string + function json_serialize_public_checked_signed_numeric[T](view value: T) returns string + function json_serialize_public_checked_unsigned_numeric[T](view value: T) returns string + function json_serialize_public_checked_wide_numeric[T](view value: T) returns string + function json_serialize_public_other_reflected[T](view value: T, view info: TypeInfo) returns string function json_serialize_public_raw_reflected[T](view value: T) returns string function json_serialize_public_primitive[T](view value: T, primitive: TypePrimitive) returns string function json_serialize_public_integer_primitive[T](view value: T, primitive: TypePrimitive) returns optional[string] @@ -20,26 +26,119 @@ mutual: function json_serialize_public_record_reflected[T](view value: T) returns string function json_serialize_public_reflected[T](view value: T) returns string: TypeInfo info = type.info[T]() + TypeKind kind = info.kind_tag + if kind == TypeKind.struct_type: + return json_serialize_public_record_reflected[T](view value) + else if kind == TypeKind.enum_type: + return json_serialize_public_enum_reflected[T](view value) + else if kind == TypeKind.bitfield_type: + return json_serialize_public_record_reflected[T](view value) + else if kind == TypeKind.primitive_type: + return json_serialize_public_checked_primitive[T](view value) + else if kind == TypeKind.list_type: + return json_serialize_public_list_reflected[T](view value) + else if kind == TypeKind.map_type: + return json_serialize_public_map_reflected[T](view value) + else if kind == TypeKind.optional_type: + return json_serialize_public_optional_reflected[T](view value) + else if kind == TypeKind.result_type: + return json_serialize_public_result_reflected[T](view value) + else if kind == TypeKind.machine_type: + return json_serialize_public_machine_reflected[T](view value) + else: + return json_serialize_public_other_reflected[T](view value, view info) +function json_serialize_public_checked_primitive[T](view value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.string_type: + string item = value + return json_stdlib_quote(item) + else if primitive == TypePrimitive.bool_type: + bool item = value + return "{item}" + else if primitive == TypePrimitive.bytes_type: + bytes item = clone value + return json_stdlib_quote("0x{bytes.to_hex(item)}") + else if primitive == TypePrimitive.nothing_type: + return "null" + else: + return json_serialize_public_checked_numeric[T](view value) +function json_serialize_public_checked_numeric[T](view value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.int8_type or primitive == TypePrimitive.int16_type or primitive == TypePrimitive.int32_type or primitive == TypePrimitive.int64_type: + return json_serialize_public_checked_signed_numeric[T](view value) + else if primitive == TypePrimitive.uint8_type or primitive == TypePrimitive.uint16_type or primitive == TypePrimitive.uint32_type: + return json_serialize_public_checked_unsigned_numeric[T](view value) + else: + return json_serialize_public_checked_wide_numeric[T](view value) +function json_serialize_public_checked_signed_numeric[T](view value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.int8_type: + int8 item = value + return "{item}" + else if primitive == TypePrimitive.int16_type: + int16 item = value + return "{item}" + else if primitive == TypePrimitive.int32_type: + int32 item = value + return "{item}" + else if primitive == TypePrimitive.int64_type: + int64 item = value + return "{item}" + else: + return "null" +function json_serialize_public_checked_unsigned_numeric[T](view value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.uint8_type: + uint8 item = value + return "{item}" + else if primitive == TypePrimitive.uint16_type: + uint16 item = value + return "{item}" + else if primitive == TypePrimitive.uint32_type: + uint32 item = value + return "{item}" + else: + return "null" +function json_serialize_public_checked_wide_numeric[T](view value: T) returns string: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.uint64_type: + uint64 item = value + return string.from_uint64(item) + else if primitive == TypePrimitive.float32_type: + float32 item = value + return "{item}" + else if primitive == TypePrimitive.float64_type: + float64 item = value + return "{item}" + else: + return "null" +function json_serialize_public_other_reflected[T](view value: T, view info: TypeInfo) returns string: TypeKind kind = info.kind_tag TypePrimitive primitive = info.primitive_tag handle: default TypePrimitive.unknown_type - if json_reflected_raw_type(view info): - return json_serialize_public_raw_reflected[T](view value) + if kind == TypeKind.enum_type: + if json_reflected_raw_type(view info): + return json_serialize_public_raw_reflected[T](view value) if primitive != TypePrimitive.unknown_type: return json_serialize_public_primitive[T](view value, primitive) if kind == TypeKind.alias_type: comptime type Base = type.arg[T](0): - Base item = value + Base item = clone value return json_serialize_public_reflected[Base](view item) if kind == TypeKind.refinement_type: comptime type Base = type.arg[T](0): - Base item = coarsen value + Base item = coarsen(clone value) return json_serialize_public_reflected[Base](view item) if kind == TypeKind.enum_type: return json_serialize_public_enum_reflected[T](view value) return json_serialize_public_non_enum_reflected[T](view value, view info) function json_serialize_public_raw_reflected[T](view value: T) returns string: - JsonTree item = value + JsonTree item = clone value return json_tree_serialize(view item) function json_serialize_public_primitive[T](view value: T, primitive: TypePrimitive) returns string: if primitive == TypePrimitive.string_type: @@ -62,7 +161,7 @@ function json_serialize_public_primitive[T](view value: T, primitive: TypePrimit return json_serialize_public_bytes_value[T](view value) return "null" function json_serialize_public_integer_primitive[T](view value: T, primitive: TypePrimitive) returns optional[string]: - if json_type_primitive_uses_int64_json_integer(primitive): + if json_type_primitive_uses_int64_json_integer(view primitive): return some(json_serialize_public_integer_value[T](view value)) if primitive == TypePrimitive.uint64_type: return some(json_serialize_public_uint64_value[T](view value)) @@ -88,6 +187,8 @@ function json_serialize_public_bytes_value[T](view value: T) returns string: bytes item = value return json_stdlib_quote("0x{bytes.to_hex(item)}") function json_serialize_public_enum_reflected[T](view value: T) returns string: + if type.name[T]() == "json.JsonTree": + return json_serialize_public_raw_reflected[T](view value) TypeVariant selected = type.variant_value[T](view value) for variant in type.variants[T](): if variant.name != selected.name: @@ -124,21 +225,21 @@ function json_serialize_public_non_enum_reflected[T](view value: T, view info: T return json_serialize_public_record_reflected[T](view value) function json_serialize_public_list_reflected[T](view value: T) returns string: comptime type Elem = type.arg[T](0): - list[Elem] items = value + list[Elem] items = clone value mutable list[string] parts = list.new[string]() for item in items: parts = list.append[string](parts, json_serialize_public_reflected[Elem](view item)) return json_stdlib_array_from_parts(parts) function json_serialize_public_set_reflected[T](view value: T) returns string: comptime type Elem = type.arg[T](0): - set[Elem] items = value + set[Elem] items = clone value mutable list[string] parts = list.new[string]() for item in items: parts = list.append[string](parts, json_serialize_public_reflected[Elem](view item)) return json_stdlib_array_from_parts(parts) function json_serialize_public_map_reflected[T](view value: T) returns string: comptime type Val = type.arg[T](1): - map[string, Val] entries = value + map[string, Val] entries = clone value mutable list[string] parts = list.new[string]() for key, item in entries: string encoded = json_serialize_public_reflected[Val](view item) @@ -146,18 +247,18 @@ function json_serialize_public_map_reflected[T](view value: T) returns string: return json_stdlib_object_from_parts(parts) function json_serialize_public_optional_reflected[T](view value: T) returns string: comptime type Inner = type.arg[T](0): - Inner item = value handle: + Inner item = clone value handle: return "null" return json_serialize_public_reflected[Inner](view item) function json_serialize_public_result_reflected[T](view value: T) returns string: comptime type Ok = type.arg[T](0): - Ok item = value handle error: - comptime type Err = type.arg[T](1): + comptime type Err = type.arg[T](1): + Ok item = clone value handle error: Err failure = error string encoded = json_serialize_public_reflected[Err](view failure) return json_stdlib_single_entry_object("fail", encoded) - string encoded = json_serialize_public_reflected[Ok](view item) - return json_stdlib_single_entry_object("ok", encoded) + string encoded = json_serialize_public_reflected[Ok](view item) + return json_stdlib_single_entry_object("ok", encoded) function json_serialize_public_machine_reflected[T](view value: T) returns string: TypeMachineState state = type.machine_state_value[T](view value) mutable list[string] payload_fields = list.new[string]() @@ -165,9 +266,10 @@ function json_serialize_public_machine_reflected[T](view value: T) returns strin if field.has_secret == true: continue comptime type Field = field.type_info: - Field item = type.machine_field_value[T, Field](view value, view field) - string encoded = json_serialize_public_reflected[Field](view item) - payload_fields = list.append[string](payload_fields, json_stdlib_object_entry(field.serialize_name, encoded)) + if not type.has_secret[Field](): + Field item = type.machine_field_value[T, Field](view value, view field) + string encoded = json_serialize_public_reflected[Field](view item) + payload_fields = list.append[string](payload_fields, json_stdlib_object_entry(field.serialize_name, encoded)) string payload = json_stdlib_object_from_parts(payload_fields) mutable list[string] envelope = list.new[string]() envelope = list.append[string](envelope, json_stdlib_object_entry("state", json_stdlib_quote(state.name))) @@ -179,7 +281,8 @@ function json_serialize_public_record_reflected[T](view value: T) returns string if field.has_secret == true: continue comptime type Field = field.type_info: - Field item = type.field_value[T, Field](view value, view field) - string encoded = json_serialize_public_reflected[Field](view item) - fields = list.append[string](fields, json_stdlib_object_entry(field.serialize_name, encoded)) + if not type.has_secret[Field](): + Field item = type.field_value[T, Field](view value, view field) + string encoded = json_serialize_public_reflected[Field](view item) + fields = list.append[string](fields, json_stdlib_object_entry(field.serialize_name, encoded)) return json_stdlib_object_from_parts(fields) diff --git a/stdlib/json/70_reflect_decode.jett b/stdlib/json/70_reflect_decode.jett index fb754ebf..2e124d77 100644 --- a/stdlib/json/70_reflect_decode.jett +++ b/stdlib/json/70_reflect_decode.jett @@ -1,6 +1,28 @@ namespace json mutual: function json_decode_tree_reflected[T](view raw: JsonTree) returns result[T, string] + function json_decode_tree_named_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string] + function json_decode_tree_terminal_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string] + function json_decode_tree_checked_primitive[T](view raw: JsonTree) returns result[T, string] + function json_decode_tree_checked_other_primitive[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string] + function json_decode_tree_checked_numeric[T](view raw: JsonTree) returns result[T, string] + function json_decode_tree_checked_narrow_numeric[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string] + function json_decode_tree_checked_unsigned_numeric[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string] + function json_decode_tree_uint8_reflected(view raw: JsonTree) returns result[uint8, string] + function json_uint8_from_bounded_int64(value: int64) returns uint8 + function json_decode_tree_bounded_int64(view raw: JsonTree, minimum: int64, maximum: int64, name: string) returns result[int64, string] + function json_decode_tree_int8_reflected(view raw: JsonTree) returns result[int8, string] + function json_decode_tree_int16_reflected(view raw: JsonTree) returns result[int16, string] + function json_decode_tree_int32_reflected(view raw: JsonTree) returns result[int32, string] + function json_decode_tree_uint16_reflected(view raw: JsonTree) returns result[uint16, string] + function json_decode_tree_uint32_reflected(view raw: JsonTree) returns result[uint32, string] + function json_decode_tree_float32_reflected(view raw: JsonTree) returns result[float32, string] + function json_int8_from_bounded_int64(value: int64) returns int8 + function json_int16_from_bounded_int64(value: int64) returns int16 + function json_int32_from_bounded_int64(value: int64) returns int32 + function json_uint16_from_bounded_int64(value: int64) returns uint16 + function json_uint32_from_bounded_int64(value: int64) returns uint32 + function json_decode_tree_other_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string] function json_decode_tree_raw_reflected[T](view raw: JsonTree) returns result[T, string] function json_decode_tree_alias_reflected[T](view raw: JsonTree) returns result[T, string] function json_decode_tree_refinement_reflected[T](view raw: JsonTree) returns result[T, string] @@ -27,6 +49,7 @@ mutual: function json_decode_tree_list_reflected[T](view raw: JsonTree) returns result[T, string] function json_decode_tree_set_reflected[T](view raw: JsonTree) returns result[T, string] function json_decode_tree_array_items_reflected[Elem](view raw: JsonTree) returns result[list[Elem], string] + function json_decode_tree_string_map_reflected[Val](view raw: JsonTree) returns result[map[string, Val], string] function json_decode_tree_map_reflected[T](view raw: JsonTree) returns result[T, string] function json_decode_tree_optional_reflected[T](view raw: JsonTree) returns result[T, string] function json_tree_result_payload(view raw: JsonTree) returns result[JsonTreeObjectField, string] @@ -35,20 +58,269 @@ mutual: function json_decode_tree_enum_string_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string] function json_decode_tree_enum_object_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string] function json_decode_tree_enum_payload_reflected[T](view payload: JsonTree, type_name: string, variant_name: string, payload_count: int64) returns result[T, string] - function json_decode_tree_enum_payload_variant_reflected[T](view payload: JsonTree, type_name: string, variant: TypeVariant, payload_count: int64) returns result[T, string] function json_tree_machine_state_name(view raw: JsonTree) returns result[string, string] function json_tree_machine_payload_tree(view raw: JsonTree) returns result[JsonTree, string] + function json_tree_machine_parts(view raw: JsonTree) returns result[JsonTreeObjectField, string] function json_decode_tree_machine_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string] - function json_decode_tree_machine_payload_reflected[T](view payload: JsonTree, type_name: string, state: TypeMachineState) returns result[T, string] function json_tree_missing_field_reflected(view field: TypeField, owner_name: string) returns result[JsonTree, string] function json_tree_record_field_value(view raw: JsonTree, view field: TypeField, owner_name: string) returns result[JsonTree, string] function json_decode_tree_record_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string] function json_decode_tree_reflected[T](view raw: JsonTree) returns result[T, string]: TypeInfo info = type.info[T]() + TypeKind kind = info.kind_tag + if kind == TypeKind.list_type: + return json_decode_tree_list_reflected[T](view raw) + else if kind == TypeKind.set_type: + return json_decode_tree_set_reflected[T](view raw) + else if kind == TypeKind.map_type: + return json_decode_tree_map_reflected[T](view raw) + else if kind == TypeKind.optional_type: + return json_decode_tree_optional_reflected[T](view raw) + else if kind == TypeKind.result_type: + return json_decode_tree_result_reflected[T](view raw) + else if kind == TypeKind.struct_type: + return json_decode_tree_record_reflected[T](view raw, info.type_name) + else: + return json_decode_tree_named_reflected[T](view raw, view info) +function json_decode_tree_named_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string]: + if type.name[T]() == "json.JsonTree": + return json_decode_tree_raw_reflected[T](view raw) + TypeKind kind = info.kind_tag + if kind == TypeKind.enum_type: + return json_decode_tree_enum_reflected[T](view raw, info.type_name) + else if kind == TypeKind.bitfield_type: + return json_decode_tree_record_reflected[T](view raw, info.type_name) + else if kind == TypeKind.refinement_type: + return json_decode_tree_refinement_reflected[T](view raw) + else if kind == TypeKind.machine_type: + return json_decode_tree_machine_reflected[T](view raw, info.type_name) + else if kind == TypeKind.machine_state_type: + return json_decode_tree_machine_reflected[T](view raw, info.type_name) + else: + return json_decode_tree_terminal_reflected[T](view raw, view info) +function json_decode_tree_terminal_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string]: + TypeKind kind = type.kind_tag[T]() + if kind == TypeKind.alias_type: + return json_decode_tree_alias_reflected[T](view raw) + else if kind == TypeKind.secret_type: + return json_decode_tree_secret_reflected[T](view raw) + else if kind == TypeKind.primitive_type: + return json_decode_tree_checked_primitive[T](view raw) + else: + return json_decode_tree_other_reflected[T](view raw, view info) +function json_decode_tree_checked_primitive[T](view raw: JsonTree) returns result[T, string]: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.string_type: + string item = json_tree_as_string(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.bool_type: + bool item = json_tree_as_bool(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else: + return json_decode_tree_checked_other_primitive[T](view raw, primitive) +function json_decode_tree_checked_other_primitive[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string]: + if primitive == TypePrimitive.bytes_type: + string raw_hex = json_tree_as_string(view raw) handle error: + return fail(error) + bytes item = bytes.from_hex(raw_hex) handle error: + return fail("expected hex bytes string") + T typed = item + return ok(typed) + else if primitive == TypePrimitive.nothing_type: + if json_tree_is_null(view raw) == false: + return fail("expected null, got {json_tree_kind(view raw)}") + T typed = nothing + return ok(typed) + else: + return json_decode_tree_checked_numeric[T](view raw) +function json_decode_tree_checked_numeric[T](view raw: JsonTree) returns result[T, string]: + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.int64_type: + int64 item = json_tree_as_int64(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.uint64_type: + uint64 item = json_tree_as_uint64(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.float64_type: + float64 item = json_tree_as_float64(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.uint8_type: + uint8 item = json_decode_tree_uint8_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else: + return json_decode_tree_checked_narrow_numeric[T](view raw, primitive) +function json_decode_tree_checked_narrow_numeric[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string]: + if primitive == TypePrimitive.int8_type: + int8 item = json_decode_tree_int8_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.int16_type: + int16 item = json_decode_tree_int16_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.int32_type: + int32 item = json_decode_tree_int32_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.float32_type: + float32 item = json_decode_tree_float32_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else: + return json_decode_tree_checked_unsigned_numeric[T](view raw, primitive) +function json_decode_tree_checked_unsigned_numeric[T](view raw: JsonTree, primitive: TypePrimitive) returns result[T, string]: + if primitive == TypePrimitive.uint16_type: + uint16 item = json_decode_tree_uint16_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else if primitive == TypePrimitive.uint32_type: + uint32 item = json_decode_tree_uint32_reflected(view raw) handle error: + return fail(error) + T typed = item + return ok(typed) + else: + return json_decode_tree_primitive_reflected[T](view raw, primitive) +function json_decode_tree_uint8_reflected(view raw: JsonTree) returns result[uint8, string]: + int64 item = json_tree_as_int64(view raw) handle error: + return fail(error) + if item < 0 or item > 255: + return fail("expected uint8 in range 0..255") + return ok(json_uint8_from_bounded_int64(item)) +function json_uint8_from_bounded_int64(value: int64) returns uint8: + mutable int64 remaining = value + mutable int64 place = 128 + mutable uint8 place_value = 128 + mutable uint8 converted = 0 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_decode_tree_bounded_int64(view raw: JsonTree, minimum: int64, maximum: int64, name: string) returns result[int64, string]: + int64 item = json_tree_as_int64(view raw) handle error: + return fail(error) + if item < minimum or item > maximum: + return fail("expected {name} in range {minimum}..{maximum}") + return ok(item) +function json_decode_tree_int8_reflected(view raw: JsonTree) returns result[int8, string]: + int64 item = json_decode_tree_bounded_int64(view raw, -128, 127, "int8") handle error: + return fail(error) + return ok(json_int8_from_bounded_int64(item)) +function json_decode_tree_int16_reflected(view raw: JsonTree) returns result[int16, string]: + int64 item = json_decode_tree_bounded_int64(view raw, -32768, 32767, "int16") handle error: + return fail(error) + return ok(json_int16_from_bounded_int64(item)) +function json_decode_tree_int32_reflected(view raw: JsonTree) returns result[int32, string]: + int64 item = json_decode_tree_bounded_int64(view raw, -2147483648, 2147483647, "int32") handle error: + return fail(error) + return ok(json_int32_from_bounded_int64(item)) +function json_decode_tree_uint16_reflected(view raw: JsonTree) returns result[uint16, string]: + int64 item = json_decode_tree_bounded_int64(view raw, 0, 65535, "uint16") handle error: + return fail(error) + return ok(json_uint16_from_bounded_int64(item)) +function json_decode_tree_uint32_reflected(view raw: JsonTree) returns result[uint32, string]: + int64 item = json_decode_tree_bounded_int64(view raw, 0, 4294967295, "uint32") handle error: + return fail(error) + return ok(json_uint32_from_bounded_int64(item)) +function json_decode_tree_float32_reflected(view raw: JsonTree) returns result[float32, string]: + float64 item = json_tree_as_float64(view raw) handle error: + return fail(error) + return ok(float32.from_float64(item)) +function json_int8_from_bounded_int64(value: int64) returns int8: + mutable int64 remaining = value + 128 + mutable int8 converted = -128 + if remaining >= 128: + remaining = remaining - 128 + converted = 0 + mutable int64 place = 64 + mutable int8 place_value = 64 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_int16_from_bounded_int64(value: int64) returns int16: + mutable int64 remaining = value + 32768 + mutable int16 converted = -32768 + if remaining >= 32768: + remaining = remaining - 32768 + converted = 0 + mutable int64 place = 16384 + mutable int16 place_value = 16384 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_int32_from_bounded_int64(value: int64) returns int32: + mutable int64 remaining = value + 2147483648 + mutable int32 converted = -2147483648 + if remaining >= 2147483648: + remaining = remaining - 2147483648 + converted = 0 + mutable int64 place = 1073741824 + mutable int32 place_value = 1073741824 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_uint16_from_bounded_int64(value: int64) returns uint16: + mutable int64 remaining = value + mutable int64 place = 32768 + mutable uint16 place_value = 32768 + mutable uint16 converted = 0 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_uint32_from_bounded_int64(value: int64) returns uint32: + mutable int64 remaining = value + mutable int64 place = 2147483648 + mutable uint32 place_value = 2147483648 + mutable uint32 converted = 0 + while place > 0: + if remaining >= place: + remaining = remaining - place + converted = converted + place_value + place = place / 2 + place_value = place_value / 2 + return converted +function json_decode_tree_other_reflected[T](view raw: JsonTree, view info: TypeInfo) returns result[T, string]: TypeKind kind = info.kind_tag TypePrimitive primitive = info.primitive_tag handle: default TypePrimitive.unknown_type - if json_reflected_raw_type(view info): + if type.name[T]() == "json.JsonTree": return json_decode_tree_raw_reflected[T](view raw) if primitive != TypePrimitive.unknown_type: return json_decode_tree_primitive_reflected[T](view raw, primitive) @@ -60,7 +332,7 @@ function json_decode_tree_reflected[T](view raw: JsonTree) returns result[T, str return json_decode_tree_secret_reflected[T](view raw) return json_decode_tree_structured_reflected[T](view raw, view info) function json_decode_tree_raw_reflected[T](view raw: JsonTree) returns result[T, string]: - T item = raw + T item = clone raw return ok(item) function json_decode_tree_alias_reflected[T](view raw: JsonTree) returns result[T, string]: comptime type Base = type.arg[T](0): @@ -96,7 +368,7 @@ function json_decode_tree_primitive_reflected[T](view raw: JsonTree, primitive: return ok(decoded_float) return ok(decoded_integer) function json_decode_tree_integer_primitive_reflected[T](view raw: JsonTree, primitive: TypePrimitive) returns result[optional[T], string]: - if json_type_primitive_uses_int64_json_integer(primitive): + if json_type_primitive_uses_int64_json_integer(view primitive): return json_decode_tree_integer_some_reflected[T](view raw, primitive) if primitive == TypePrimitive.uint64_type: return json_decode_tree_uint64_some_reflected[T](view raw) @@ -258,18 +530,31 @@ function json_decode_tree_array_items_reflected[Elem](view raw: JsonTree) return return fail("{index}: {error}") items = list.append[Elem](items, item) return ok(items) -function json_decode_tree_map_reflected[T](view raw: JsonTree) returns result[T, string]: - comptime type Val = type.arg[T](1): - list[string] keys = json_tree_object_keys(view raw) handle error: +function json_decode_tree_string_map_reflected[Val](view raw: JsonTree) returns result[map[string, Val], string]: + list[string] keys = json_tree_object_keys(view raw) handle error: + return fail(error) + mutable map[string, Val] entries = map.new[string, Val]() + for key in keys: + JsonTree raw_item = json_tree_require_field(view raw, key) handle error: return fail(error) - mutable map[string, Val] entries = map.new[string, Val]() - for key in keys: - JsonTree raw_item = json_tree_require_field(view raw, key) handle error: - return fail(error) - Val item = json_decode_tree_reflected[Val](view raw_item) handle error: - return fail("{key}: {error}") - entries = map.insert[string, Val](entries, key, item) - return ok(entries) + Val item = json_decode_tree_reflected[Val](view raw_item) handle error: + return fail("{key}: {error}") + entries = map.insert[string, Val](entries, key, item) + return ok(entries) +function json_decode_tree_map_reflected[T](view raw: JsonTree) returns result[T, string]: + comptime type Key = type.arg[T](0): + if type.kind_tag[Key]() == TypeKind.alias_type: + comptime type Base = type.arg[Key](0): + comptime type Val = type.arg[T](1): + return json_decode_tree_map_reflected[map[Base, Val]](view raw) + else: + TypePrimitive primitive = type.primitive_tag[Key]() handle: + default TypePrimitive.unknown_type + if primitive == TypePrimitive.string_type: + comptime type Val = type.arg[T](1): + return json_decode_tree_string_map_reflected[Val](view raw) + else: + return fail("JSON object maps require string keys, got {type.name[Key]()}") function json_decode_tree_optional_reflected[T](view raw: JsonTree) returns result[T, string]: if json_tree_is_null(view raw) == true: return ok(none) @@ -282,7 +567,7 @@ function json_tree_result_payload(view raw: JsonTree) returns result[JsonTreeObj return fail("expected result object, got {json_tree_kind(view raw)}") list[string] keys = json_tree_object_keys(view raw) handle error: return fail(error) - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected result object with exactly one key") string key = list.get[string](keys, 0) handle: return fail("expected result object with exactly one key") @@ -294,17 +579,17 @@ function json_tree_result_payload(view raw: JsonTree) returns result[JsonTreeObj return fail("expected result object with key 'ok' or 'fail'") return ok(JsonTreeObjectField(key: key, value: value)) function json_decode_tree_result_reflected[T](view raw: JsonTree) returns result[T, string]: - JsonTreeObjectField payload = json_tree_result_payload(view raw) handle error: - return fail(error) - if payload.key == "ok": - comptime type Ok = type.arg[T](0): - Ok item = json_decode_tree_reflected[Ok](view payload.value) handle error: - return fail("ok: {error}") - return ok(ok(item)) comptime type Err = type.arg[T](1): - Err failure = json_decode_tree_reflected[Err](view payload.value) handle error: - return fail("fail: {error}") - return ok(fail(failure)) + comptime type Ok = type.arg[T](0): + JsonTreeObjectField payload = json_tree_result_payload(view raw) handle error: + return fail(error) + if payload.key == "ok": + Ok item = json_decode_tree_reflected[Ok](view payload.value) handle error: + return fail("ok: {error}") + return ok(ok(item)) + Err failure = json_decode_tree_reflected[Err](view payload.value) handle error: + return fail("fail: {error}") + return ok(fail(failure)) function json_decode_tree_enum_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string]: if json_tree_is_string(view raw) == true: return json_decode_tree_enum_string_reflected[T](view raw, type_name) @@ -325,7 +610,7 @@ function json_decode_tree_enum_string_reflected[T](view raw: JsonTree, type_name function json_decode_tree_enum_object_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string]: list[string] keys = json_tree_object_keys(view raw) handle error: return fail("expected enum string or object, got {json_tree_kind(view raw)}") - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected enum object with exactly one variant key") string variant_name = list.get[string](keys, 0) handle: return fail("expected enum object with exactly one variant key") @@ -338,25 +623,23 @@ function json_decode_tree_enum_payload_reflected[T](view payload: JsonTree, type for variant in type.variants[T](): if variant.name != variant_name: continue - return json_decode_tree_enum_payload_variant_reflected[T](view payload, type_name, variant, payload_count) + int64 expected_count = list.length[TypeField](variant.fields) + if payload_count != expected_count: + return fail("enum variant '{type_name}.{variant.name}' expects {expected_count} payload field(s), got {payload_count}") + mutable TypeConstruction builder = type.construct_variant_start[T](view variant) handle error: + return fail(error) + mutable int64 index = 0 + for field in variant.fields: + comptime type Field = field.type_info: + JsonTree raw_item = json_tree_require_index(view payload, index) handle error: + return fail("missing enum payload item {index}") + Field decoded = json_decode_tree_reflected[Field](view raw_item) handle error: + return fail("{variant.name}.{field.name}: {error}") + builder = type.construct_put[T, Field](builder, view field, decoded) handle error: + return fail(error) + index = index + 1 + return type.construct_finish[T](builder) return fail("unknown enum variant '{type_name}.{variant_name}'") -function json_decode_tree_enum_payload_variant_reflected[T](view payload: JsonTree, type_name: string, variant: TypeVariant, payload_count: int64) returns result[T, string]: - int64 expected_count = list.length[TypeField](variant.fields) - if payload_count != expected_count: - return fail("enum variant '{type_name}.{variant.name}' expects {expected_count} payload field(s), got {payload_count}") - mutable TypeConstruction builder = type.construct_variant_start[T](view variant) handle error: - return fail(error) - mutable int64 index = 0 - for field in variant.fields: - comptime type Field = field.type_info: - JsonTree raw_item = json_tree_require_index(view payload, index) handle error: - return fail("missing enum payload item {index}") - Field decoded = json_decode_tree_reflected[Field](view raw_item) handle error: - return fail("{variant.name}.{field.name}: {error}") - builder = type.construct_put[T, Field](builder, view field, decoded) handle error: - return fail(error) - index = index + 1 - return type.construct_finish[T](builder) function json_tree_machine_state_name(view raw: JsonTree) returns result[string, string]: JsonTree raw_state = json_tree_require_field(view raw, "state") handle error: return fail(error) @@ -369,31 +652,35 @@ function json_tree_machine_payload_tree(view raw: JsonTree) returns result[JsonT if json_tree_is_object(view payload) == false: return fail("expected machine payload object, got {json_tree_kind(view payload)}") return ok(payload) -function json_decode_tree_machine_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string]: +function json_tree_machine_parts(view raw: JsonTree) returns result[JsonTreeObjectField, string]: if json_tree_is_object(view raw) == false: return fail("expected machine envelope object, got {json_tree_kind(view raw)}") string state_name = json_tree_machine_state_name(view raw) handle error: return fail(error) JsonTree payload = json_tree_machine_payload_tree(view raw) handle error: return fail(error) + return ok(JsonTreeObjectField(key: state_name, value: payload)) +function json_decode_tree_machine_reflected[T](view raw: JsonTree, type_name: string) returns result[T, string]: + JsonTreeObjectField parts = json_tree_machine_parts(view raw) handle error: + return fail(error) + string state_name = parts.key + JsonTree payload = parts.value for state in type.machine_states[T](): - if state.name == state_name: - return json_decode_tree_machine_payload_reflected[T](view payload, type_name, state) + if state.name != state_name: + continue + mutable TypeConstruction builder = type.construct_machine_start[T](view state) handle error: + return fail(error) + string owner_name = "{type_name}.{state_name}" + for field in state.fields: + comptime type Field = field.type_info: + JsonTree raw_field = json_tree_record_field_value(view payload, view field, owner_name) handle error: + return fail(error) + Field decoded = json_decode_tree_reflected[Field](view raw_field) handle error: + return fail("{state_name}.{field.serialize_name}: {error}") + builder = type.construct_put[T, Field](builder, view field, decoded) handle error: + return fail(error) + return type.construct_finish[T](builder) return fail("unknown machine state '{type_name}.{state_name}'") -function json_decode_tree_machine_payload_reflected[T](view payload: JsonTree, type_name: string, state: TypeMachineState) returns result[T, string]: - mutable TypeConstruction builder = type.construct_machine_start[T](view state) handle error: - return fail(error) - string state_name = state.name - string owner_name = "{type_name}.{state_name}" - for field in state.fields: - comptime type Field = field.type_info: - JsonTree raw_field = json_tree_record_field_value(view payload, view field, owner_name) handle error: - return fail(error) - Field decoded = json_decode_tree_reflected[Field](view raw_field) handle error: - return fail("{state_name}.{field.serialize_name}: {error}") - builder = type.construct_put[T, Field](builder, view field, decoded) handle error: - return fail(error) - return type.construct_finish[T](builder) function json_tree_missing_field_reflected(view field: TypeField, owner_name: string) returns result[JsonTree, string]: if field.type_info.kind_tag == TypeKind.optional_type: return ok(JsonTree.null) diff --git a/stdlib/json/80_reflect_validate_exact.jett b/stdlib/json/80_reflect_validate_exact.jett index 91389b83..8d38784f 100644 --- a/stdlib/json/80_reflect_validate_exact.jett +++ b/stdlib/json/80_reflect_validate_exact.jett @@ -129,23 +129,23 @@ function json_validate_exact_optional_reflected[T](view raw: JsonTree) returns r comptime type Inner = type.arg[T](0): return json_validate_exact_reflected[Inner](view raw) function json_validate_exact_result_reflected[T](view raw: JsonTree) returns result[nothing, string]: - JsonTreeObjectField payload = json_tree_result_payload(view raw) handle error: - return fail(error) - if payload.key == "ok": + comptime type Err = type.arg[T](1): comptime type Ok = type.arg[T](0): - nothing checked = json_validate_exact_reflected[Ok](view payload.value) handle error: - return fail("ok: {error}") + JsonTreeObjectField payload = json_tree_result_payload(view raw) handle error: + return fail(error) + if payload.key == "ok": + nothing checked = json_validate_exact_reflected[Ok](view payload.value) handle error: + return fail("ok: {error}") + return ok(nothing) + nothing checked = json_validate_exact_reflected[Err](view payload.value) handle error: + return fail("fail: {error}") return ok(nothing) - comptime type Err = type.arg[T](1): - nothing checked = json_validate_exact_reflected[Err](view payload.value) handle error: - return fail("fail: {error}") - return ok(nothing) function json_validate_exact_enum_reflected[T](view raw: JsonTree, type_name: string) returns result[nothing, string]: if json_tree_is_string(view raw) == true: return json_validate_exact_enum_string_reflected[T](view raw, type_name) list[string] keys = json_tree_object_keys(view raw) handle error: return fail("expected enum string or object, got {json_tree_kind(view raw)}") - if list.length[string](keys) != 1: + if list.length[string](view keys) != 1: return fail("expected enum object with exactly one variant key") string variant_name = list.get[string](keys, 0) handle: return fail("expected enum object with exactly one variant key") diff --git a/stdlib/json/90_public_api.jett b/stdlib/json/90_public_api.jett index 888f2cf9..75e1aa7c 100644 --- a/stdlib/json/90_public_api.jett +++ b/stdlib/json/90_public_api.jett @@ -63,3 +63,110 @@ export function serialize[T](view value: T) returns string: return json_serialize_reflected[T](view value) export function serialize_public[T](view value: T) returns string: return json_serialize_public_reflected[T](view value) +function json_parse_native_string(raw: string) returns result[string, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_tree_as_string(view tree) +function json_parse_native_bool(raw: string) returns result[bool, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_tree_as_bool(view tree) +function json_parse_native_int64(raw: string) returns result[int64, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_tree_as_int64(view tree) +function json_parse_native_uint8(raw: string) returns result[uint8, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_decode_tree_uint8_reflected(view tree) +function json_parse_native_uint64(raw: string) returns result[uint64, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_tree_as_uint64(view tree) +function json_parse_native_float64(raw: string) returns result[float64, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_tree_as_float64(view tree) +function json_parse_native_bytes(raw: string) returns result[bytes, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + string raw_hex = json_tree_as_string(view tree) handle error: + return fail(error) + bytes item = bytes.from_hex(raw_hex) handle error: + return fail("expected hex bytes string") + return ok(item) +function json_serialize_native_bytes(view value: bytes) returns string: + bytes item = clone value + return json_stdlib_quote("0x{bytes.to_hex(item)}") +function json_serialize_native_nothing(view value: nothing) returns string: + return "null" +function json_parse_native_nothing(raw: string) returns result[nothing, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + if json_tree_is_null(view tree) == false: + return fail("expected null, got {json_tree_kind(view tree)}") + return ok(nothing) +function json_parse_native_secret_tree(raw: string) returns result[secret[JsonTree], string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + secret[JsonTree] secret_tree = tree + return ok(secret_tree) +function json_parse_native_enum[T](raw: string) returns result[T, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + TypeInfo info = type.info[T]() + return json_decode_tree_enum_reflected[T](view tree, info.type_name) +function json_parse_exact_native_enum[T](raw: string) returns result[T, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + TypeInfo info = type.info[T]() + nothing checked = json_validate_exact_enum_reflected[T](view tree, info.type_name) handle error: + return fail(error) + return json_decode_tree_enum_reflected[T](view tree, info.type_name) +function json_serialize_native_enum[T](view value: T) returns string: + return json_serialize_public_enum_reflected[T](view value) +function json_serialize_native_bitfield_field[Field](view item: Field) returns string: + if type.kind_tag[Field]() == TypeKind.enum_type: + return json_serialize_native_enum[Field](view item) + else: + return json_serialize_public_reflected[Field](view item) +function json_serialize_native_bitfield[T](view value: T) returns string: + mutable list[string] fields = list.new[string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + Field item = type.field_value[T, Field](view value, view field) + string encoded = json_serialize_native_bitfield_field[Field](view item) + fields = list.append[string](fields, json_stdlib_object_entry(field.serialize_name, encoded)) + return json_stdlib_object_from_parts(fields) +function json_decode_tree_native_bitfield_enum[Field](view raw: JsonTree) returns result[Field, string]: + TypeInfo info = type.info[Field]() + return json_decode_tree_enum_reflected[Field](view raw, info.type_name) +function json_decode_tree_native_bitfield_field[Field](view raw: JsonTree) returns result[Field, string]: + if type.kind_tag[Field]() == TypeKind.enum_type: + return json_decode_tree_native_bitfield_enum[Field](view raw) + else: + return json_decode_tree_reflected[Field](view raw) +function json_decode_tree_native_bitfield[T](view raw: JsonTree) returns result[T, string]: + TypeInfo info = type.info[T]() + if json_tree_is_object(view raw) == false: + return fail("expected object, got {json_tree_kind(view raw)}") + mutable TypeConstruction builder = type.construct_start[T]() + for field in type.fields[T](): + comptime type Field = field.type_info: + JsonTree raw_field = json_tree_record_field_value(view raw, view field, info.type_name) handle error: + return fail(error) + Field decoded = json_decode_tree_native_bitfield_field[Field](view raw_field) handle error: + return fail("{field.serialize_name}: {error}") + builder = type.construct_put[T, Field](builder, view field, decoded) handle error: + return fail(error) + return type.construct_finish[T](builder) +function json_parse_native_bitfield[T](raw: string) returns result[T, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + return json_decode_tree_native_bitfield[T](view tree) +function json_parse_exact_native_bitfield[T](raw: string) returns result[T, string]: + JsonTree tree = parse_raw(raw) handle error: + return fail(error) + nothing checked = json_validate_exact_reflected[T](view tree) handle error: + return fail(error) + return json_decode_tree_native_bitfield[T](view tree) diff --git a/stdlib/list.jett b/stdlib/list.jett index 5de599c8..a5311525 100644 --- a/stdlib/list.jett +++ b/stdlib/list.jett @@ -133,7 +133,25 @@ export function find[T](view items: list[T], predicate: function(T) returns bool return some(clone item) return none export function sort_by[T](items: list[T], key_function: function(T) returns int64) returns list[T]: - return list.__sort_by[T](items, key_function) + mutable list[int64] keys = list.new[int64]() + for item in view items: + keys = list.append[int64](keys, key_function(clone item)) + mutable list[T] sorted = items + int64 length = list.length[int64](view keys) + mutable int64 pass = 0 + while pass < length: + mutable int64 index = 1 + while index < length: + int64 left = list.get[int64](view keys, index - 1) handle: + default 0 + int64 right = list.get[int64](view keys, index) handle: + default 0 + if left > right: + keys = list.swap[int64](keys, index - 1, index) + sorted = list.swap[T](sorted, index - 1, index) + index = index + 1 + pass = pass + 1 + return sorted export function all[T](view items: list[T], predicate: function(T) returns bool) returns bool: for item in view items: if not predicate(clone item): @@ -153,7 +171,14 @@ export function count[T](view items: list[T], predicate: function(T) returns boo export function sum[T](view items: list[T]) returns T: return list.__sum[T](view items) export function group_by[T](items: list[T], key_function: function(T) returns string) returns map[string, list[T]]: - return list.__group_by[T](items, key_function) + mutable map[string, list[T]] groups = map.__new[string, list[T]]() + for item in items: + string key = key_function(clone item) + mutable list[T] bucket = map.__get[string, list[T]](view groups, view key) handle: + default list.new[T]() + bucket = list.append[T](bucket, item) + groups = map.__insert[string, list[T]](groups, key, bucket) + return groups export function reduce[T, U](items: list[T], initial: U, reducer: function(U, T) returns U) returns U: mutable U accumulator = initial for item in items: diff --git a/tests/compile_fail/closure_capture_move_only.jett b/tests/compile_fail/closure_capture_move_only.jett index 694a4af3..627a3aa9 100644 --- a/tests/compile_fail/closure_capture_move_only.jett +++ b/tests/compile_fail/closure_capture_move_only.jett @@ -1,4 +1,4 @@ -# ERROR: E0402 x2 +# ERROR: E0402 namespace app function make_counter(items: list[int64]) returns function() returns int64: diff --git a/tests/compile_fail/closure_reflection_boolean_boundaries.jett b/tests/compile_fail/closure_reflection_boolean_boundaries.jett new file mode 100644 index 00000000..f93e7d5f --- /dev/null +++ b/tests/compile_fail/closure_reflection_boolean_boundaries.jett @@ -0,0 +1,21 @@ +# ERROR: E0311 +# ERROR: E0311 +function is_list(kind: TypeKind) returns bool: + return kind == TypeKind.list_type +function predicate[T]() returns function(T) returns int64: + return function(value: T) returns int64: + if is_list(type.kind_tag[T]()): + list[int64] items = value + return list.length[int64](view items) + return 0 +function detached[T]() returns function(T) returns int64: + return function(value: T) returns int64: + bool check = type.kind_tag[T]() == TypeKind.list_type + if check: + list[int64] items = value + return list.length[int64](view items) + return 0 +function main() returns nothing: + function(int64) returns int64 first = predicate[int64]() + function(int64) returns int64 second = detached[int64]() + return nothing diff --git a/tests/compile_fail/comptime_generic_requires_closed_value.jett b/tests/compile_fail/comptime_generic_requires_closed_value.jett new file mode 100644 index 00000000..b07da614 --- /dev/null +++ b/tests/compile_fail/comptime_generic_requires_closed_value.jett @@ -0,0 +1,7 @@ +# ERROR: E9001 +namespace app +function invalid[T](value: T) returns T: + return comptime value +function main() returns nothing: + println(invalid[int64](7)) + return nothing diff --git a/tests/compile_fail/comptime_type_bind_requires_direct_field_loop.jett b/tests/compile_fail/comptime_type_bind_requires_direct_field_loop.jett index d9d708a0..c83bc13b 100644 --- a/tests/compile_fail/comptime_type_bind_requires_direct_field_loop.jett +++ b/tests/compile_fail/comptime_type_bind_requires_direct_field_loop.jett @@ -7,7 +7,7 @@ struct User: function bad() returns string: list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: comptime type Field = field.type_info: found = type.name[Field]() diff --git a/tests/compile_fail/comptime_type_bind_requires_direct_machine_state_field_loop.jett b/tests/compile_fail/comptime_type_bind_requires_direct_machine_state_field_loop.jett index 5df7cd12..72628361 100644 --- a/tests/compile_fail/comptime_type_bind_requires_direct_machine_state_field_loop.jett +++ b/tests/compile_fail/comptime_type_bind_requires_direct_machine_state_field_loop.jett @@ -9,7 +9,7 @@ machine Session: guest to logged_in function bad(view session: Session at logged_in) returns string: TypeMachineState state = type.machine_state_value[Session at logged_in](view session) - string found = "missing" + mutable string found = "missing" for field in state.fields: comptime type Field = field.type_info: found = type.name[Field]() diff --git a/tests/compile_fail/comptime_type_bind_requires_direct_type_info.jett b/tests/compile_fail/comptime_type_bind_requires_direct_type_info.jett index ff1ed5c5..f13a03da 100644 --- a/tests/compile_fail/comptime_type_bind_requires_direct_type_info.jett +++ b/tests/compile_fail/comptime_type_bind_requires_direct_type_info.jett @@ -4,7 +4,7 @@ namespace test function bad() returns string: TypeInfo info = type.info[int64]() - string found = "missing" + mutable string found = "missing" comptime type Bound = info: found = type.name[Bound]() return found diff --git a/tests/compile_fail/comptime_type_bind_requires_direct_variant_field_loop.jett b/tests/compile_fail/comptime_type_bind_requires_direct_variant_field_loop.jett index 495b4ddc..935d741e 100644 --- a/tests/compile_fail/comptime_type_bind_requires_direct_variant_field_loop.jett +++ b/tests/compile_fail/comptime_type_bind_requires_direct_variant_field_loop.jett @@ -8,7 +8,7 @@ enum Shape: function bad() returns string: Shape shape = Shape.circle(2.5) TypeVariant variant = type.variant_value[Shape](view shape) - string found = "missing" + mutable string found = "missing" for field in variant.fields: comptime type Field = field.type_info: found = type.name[Field]() diff --git a/tests/compile_fail/comptime_type_bind_requires_literal_type_arg_index.jett b/tests/compile_fail/comptime_type_bind_requires_literal_type_arg_index.jett index c52f0583..74aa244b 100644 --- a/tests/compile_fail/comptime_type_bind_requires_literal_type_arg_index.jett +++ b/tests/compile_fail/comptime_type_bind_requires_literal_type_arg_index.jett @@ -3,7 +3,7 @@ namespace test function bad(index: int64) returns string: - string found = "missing" + mutable string found = "missing" comptime type Arg = type.arg[list[string]](index): found = type.name[Arg]() return found diff --git a/tests/compile_fail/comptime_type_bind_requires_trusted_args_loop.jett b/tests/compile_fail/comptime_type_bind_requires_trusted_args_loop.jett index 072ea2d9..a2aad18e 100644 --- a/tests/compile_fail/comptime_type_bind_requires_trusted_args_loop.jett +++ b/tests/compile_fail/comptime_type_bind_requires_trusted_args_loop.jett @@ -4,7 +4,7 @@ namespace test function bad() returns string: TypeInfo info = type.info[list[int64]]() - string found = "missing" + mutable string found = "missing" for arg in info.args: comptime type Arg = arg: found = type.name[Arg]() diff --git a/tests/compile_fail/comptime_type_bind_type_arg_index_out_of_range.jett b/tests/compile_fail/comptime_type_bind_type_arg_index_out_of_range.jett index 6e970db9..aaa78ed3 100644 --- a/tests/compile_fail/comptime_type_bind_type_arg_index_out_of_range.jett +++ b/tests/compile_fail/comptime_type_bind_type_arg_index_out_of_range.jett @@ -3,7 +3,7 @@ # ERROR: E0345 namespace test function bad() returns string: - string found = "missing" + mutable string found = "missing" comptime type Arg = type.arg[list[string]](18446744073709551615): found = type.name[Arg]() return found diff --git a/tests/compile_fail/direct_collection_equality.jett b/tests/compile_fail/direct_collection_equality.jett new file mode 100644 index 00000000..6f34f383 --- /dev/null +++ b/tests/compile_fail/direct_collection_equality.jett @@ -0,0 +1,26 @@ +# ERROR: E0376 x12 +namespace app +function bytes_equal(view left: bytes, view right: bytes) returns bool: + return left == right +function bytes_unequal(view left: bytes, view right: bytes) returns bool: + return left != right +function lists_equal(view left: list[int64], view right: list[int64]) returns bool: + return left == right +function lists_unequal(view left: list[int64], view right: list[int64]) returns bool: + return left != right +function maps_equal(view left: map[string, int64], view right: map[string, int64]) returns bool: + return left == right +function maps_unequal(view left: map[string, int64], view right: map[string, int64]) returns bool: + return left != right +function sets_equal(view left: set[int64], view right: set[int64]) returns bool: + return left == right +function sets_unequal(view left: set[int64], view right: set[int64]) returns bool: + return left != right +function optionals_equal(view left: optional[int64], view right: optional[int64]) returns bool: + return left == right +function optionals_unequal(view left: optional[int64], view right: optional[int64]) returns bool: + return left != right +function results_equal(view left: result[int64, string], view right: result[int64, string]) returns bool: + return left == right +function results_unequal(view left: result[int64, string], view right: result[int64, string]) returns bool: + return left != right diff --git a/tests/compile_fail/direct_collection_equality_comptime.jett b/tests/compile_fail/direct_collection_equality_comptime.jett new file mode 100644 index 00000000..825effaf --- /dev/null +++ b/tests/compile_fail/direct_collection_equality_comptime.jett @@ -0,0 +1,7 @@ +# ERROR: E0376 x4 +namespace app +function main() returns nothing: + bool first = comptime(list(1) == list(1)) + bool second = comptime(ok(1) != ok(1)) + bool third = comptime(none == none) + bool fourth = comptime(bytes.from_string("a") != bytes.from_string("a")) diff --git a/tests/compile_fail/direct_collection_equality_generic.jett b/tests/compile_fail/direct_collection_equality_generic.jett new file mode 100644 index 00000000..e73f8203 --- /dev/null +++ b/tests/compile_fail/direct_collection_equality_generic.jett @@ -0,0 +1,11 @@ +# ERROR: E0376 x2 +namespace app +function equal[T](view left: T, view right: T) returns bool: + return left == right +function unequal[T](view left: T, view right: T) returns bool: + return left != right +function main() returns nothing: + list[int64] values = list(1) + optional[string] absent = none + bool first = equal[list[int64]](view values, view values) + bool second = unequal[optional[string]](view absent, view absent) diff --git a/tests/compile_fail/direct_collection_equality_wrappers.jett b/tests/compile_fail/direct_collection_equality_wrappers.jett new file mode 100644 index 00000000..c7620965 --- /dev/null +++ b/tests/compile_fail/direct_collection_equality_wrappers.jett @@ -0,0 +1,14 @@ +# ERROR: E0376 x4 +namespace app +type Values = list[int64] +type Selected = Values where true +type Again = Selected where true +type Hidden = secret[list[int64]] where true +function aliased(view left: Values, view right: Values) returns bool: + return left == right +function refined(view left: Again, view right: Again) returns bool: + return left != right +function hidden(view left: secret[list[int64]], view right: secret[list[int64]]) returns secret[bool]: + return left == right +function refined_hidden(view left: Hidden, view right: Hidden) returns secret[bool]: + return left != right diff --git a/tests/compile_fail/enum_struct_equality_requires_equatable.jett b/tests/compile_fail/enum_struct_equality_requires_equatable.jett new file mode 100644 index 00000000..71705e85 --- /dev/null +++ b/tests/compile_fail/enum_struct_equality_requires_equatable.jett @@ -0,0 +1,16 @@ +# ERROR: E0358 x4 +struct User: + id: int64 +enum Item: + empty + nested(value: optional[list[User]]) +function equal(view left: Item, view right: Item) returns bool: + return left == right +function different(view left: Item, view right: Item) returns bool: + return left != right +function generic[T](view left: T, view right: T) returns bool: + return left == right +function specialized(view left: Item, view right: Item) returns bool: + return generic[Item](view left, view right) +function baking() returns bool: + return comptime(Item.empty == Item.empty) diff --git a/tests/compile_fail/function_expression_ownership.jett b/tests/compile_fail/function_expression_ownership.jett new file mode 100644 index 00000000..13e68e56 --- /dev/null +++ b/tests/compile_fail/function_expression_ownership.jett @@ -0,0 +1,51 @@ +# ERROR: E0400 x6 +# ERROR: E0401 +namespace test +function identity(value: int64) returns int64: + return value +function choose(view items: list[int64]) returns function(int64) returns int64: + return identity +function consume(items: list[int64]) returns int64: + return list.length[int64](view items) +function generic_consume[T](items: list[T]) returns int64: + return list.length[T](view items) +struct Holder: + callback: function(int64) returns int64 +function consume_holder(holder: Holder) returns int64: + return 7 +function pair(left: int64, right: int64) returns int64: + return left + right +struct PairHolder: + callback: function(int64, int64) returns int64 +function consume_pair_holder(holder: PairHolder) returns int64: + return 8 +function callee_uses_consumed_argument() returns nothing: + list[int64] items = list(1, 2) + println(choose(view items)(consume(items))) + return nothing +function field_uses_consumed_owner() returns nothing: + Holder holder = Holder(callback: identity) + println(holder.callback(consume_holder(holder))) + return nothing +function parentheses_preserve_moves() returns nothing: + list[int64] items = list(1, 2) + println((consume)(((items)))) + println(list.length[int64](view items)) + return nothing +function generic_parentheses_preserve_moves() returns nothing: + list[int64] items = list(1, 2) + println(generic_consume[int64]((items))) + println(list.length[int64](view items)) + return nothing +function pipeline_uses_consumed_owner() returns nothing: + PairHolder holder = PairHolder(callback: pair) + println(1 into holder.callback(consume_pair_holder(holder))) + return nothing +function parentheses_preserve_view_ownership(view items: list[int64]) returns nothing: + println((consume)((items))) + return nothing +function pipeline_call_extra_is_consumed() returns bool: + list[int64] items = list(1) + list[int64] pool = list(1) + bool contained = items into view list.all_elements_in(pool) + return list.is_empty[int64](view pool) diff --git a/tests/compile_fail/function_expression_policy.jett b/tests/compile_fail/function_expression_policy.jett new file mode 100644 index 00000000..cdfeb952 --- /dev/null +++ b/tests/compile_fail/function_expression_policy.jett @@ -0,0 +1,50 @@ +# ERROR: E0500 x4 +# ERROR: E0368 x2 +# ERROR: E0375 +# ERROR: E0305 x2 +namespace test +struct Authority: + stdout: Stdout +function write(view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "effect") + return nothing +function select_write() returns function(view Stdout) returns nothing: + return write +struct EffectHolder: + callback: function(view Stdout) returns nothing +function parenthesized_effect(view authority: Authority) returns nothing: + (write)(view authority.stdout) + return nothing +function returned_effect(view authority: Authority) returns nothing: + select_write()(view authority.stdout) + return nothing +function field_effect(view authority: Authority, holder: EffectHolder) returns nothing: + holder.callback(view authority.stdout) + return nothing +function hidden_inline_effect(view authority: Authority) returns nothing: + function(view Authority) returns nothing callback = function(view argument: Authority) returns nothing: + write(view argument.stdout) + return nothing + callback(view authority) + return nothing +function increment(value: int64) returns int64: + return value + 1 +struct Holder: + callback: function(int64) returns int64 +function unknown_field_parameter() returns int64: + Holder holder = Holder(callback: increment) + return holder.callback(value: 3) +function unknown_local_parameter(callback: function(int64) returns int64) returns int64: + return(callback)(value: 4) +function take(items: list[int64]) returns int64: + return list.length[int64](view items) +function select_take() returns function(list[int64]) returns int64: + return take +function returned_owned_parameter(view items: list[int64]) returns int64: + return select_take()(view items) +function select_increment() returns function(int64) returns int64: + return increment +function secret_return_cannot_escape(value: secret[int64]) returns int64: + return select_increment()(value) +function secret_pipeline_cannot_escape(value: secret[int64]) returns int64: + return value into(select_increment()) diff --git a/tests/compile_fail/function_view_modes.jett b/tests/compile_fail/function_view_modes.jett new file mode 100644 index 00000000..87d41b7d --- /dev/null +++ b/tests/compile_fail/function_view_modes.jett @@ -0,0 +1,49 @@ +# ERROR: E0311 x2 +# ERROR: E0375 x28 +namespace test +function take(items: list[int64]) returns int64: + return list.length[int64](view items) +function count(view items: list[int64]) returns int64: + return list.length[int64](view items) +function pair(amount: int64, items: list[int64]) returns int64: + return amount + list.length[int64](view items) +function generic_take[T](items: list[T]) returns int64: + return list.length[T](view items) +function generic_pair[T](amount: int64, items: list[T]) returns int64: + return amount + list.length[T](view items) +function main() returns nothing: + list[int64] items = list(1, 2) + function(view list[int64]) returns int64 callback = count + println(take(view items)) + function(list[int64]) returns int64 wrong = count + println(wrong(view items)) + function(view list[int64]) returns int64 wrong_view = take + println(generic_take[int64](view items)) + println(generic_take(view items)) + println(generic_pair[int64](items: view items, amount: 1)) + println(generic_pair(items: view items, amount: 1)) + println(items into view take) + println(1 into pair(view items)) + function(list[int64]) returns int64 owned_callback = take + println(items into view owned_callback) + function(int64, list[int64]) returns int64 owned_pair = pair + println(1 into owned_pair(view items)) + println(items into view generic_take[int64]()) + println(items into view generic_take()) + println(1 into generic_pair[int64](view items)) + println(1 into generic_pair(view items)) + println(take((view items))) + println(owned_callback(((view items)))) + println(generic_take[int64]((view items))) + println(generic_take((view items))) + println(generic_pair[int64](items:(view items), amount: 1)) + println(generic_pair(items:(view items), amount: 1)) + println(1 into pair((view items))) + println(1 into owned_pair((view items))) + println(1 into generic_pair[int64]((view items))) + println(1 into generic_pair((view items))) + println((view items) into take) + println(((view items)) into owned_callback) + println((view items) into generic_take[int64]()) + println((view items) into generic_take()) + return nothing diff --git a/tests/compile_fail/generic_method_owner.jett b/tests/compile_fail/generic_method_owner.jett new file mode 100644 index 00000000..b7136759 --- /dev/null +++ b/tests/compile_fail/generic_method_owner.jett @@ -0,0 +1,22 @@ +# ERROR: E0309 x4 +namespace records +export interface Named: + function name(view self: Named) returns string +export struct Box[T]: + item: T +implement Named for Box[int64]: + function name(view self: Box[int64]) returns string: + return "integer" +function local(view value: Box[int64]) returns string: + return Box.name(view value) +namespace app +function qualified(view value: records.Box[int64]) returns string: + use records + return records.Box.name(view value) +function aliased(view value: records.Box[int64]) returns string: + use records as r + return r.Box.name(view value) +function stored(view value: records.Box[int64]) returns string: + use records + function(view records.Box[int64]) returns string callback = records.Box.name + return callback(view value) diff --git a/tests/compile_fail/generic_reflection_body_type_error.jett b/tests/compile_fail/generic_reflection_body_type_error.jett new file mode 100644 index 00000000..929e4075 --- /dev/null +++ b/tests/compile_fail/generic_reflection_body_type_error.jett @@ -0,0 +1,6 @@ +# ERROR: E0305 +function broken[T]() returns int64: + return "type:{type.name[T]()}" +function main() returns nothing: + println(broken[int64]()) + return nothing diff --git a/tests/compile_fail/generic_reflection_helper_kind_fact_cache.jett b/tests/compile_fail/generic_reflection_helper_kind_fact_cache.jett index bad7a6f0..07a0fdbd 100644 --- a/tests/compile_fail/generic_reflection_helper_kind_fact_cache.jett +++ b/tests/compile_fail/generic_reflection_helper_kind_fact_cache.jett @@ -4,8 +4,7 @@ function helper_cached_kind[T](value: T, kind: TypeKind) returns int64: int64 item = value return item return 0 - function main() returns nothing: - int64 first = helper_cached_kind[string]("Ada", TypeKind.string_type) + int64 first = helper_cached_kind[string]("Ada", TypeKind.primitive_type) int64 second = helper_cached_kind[string]("Ada", TypeKind.list_type) return nothing diff --git a/tests/compile_pass/generic_reflection_runtime_guard_deferral.jett b/tests/compile_fail/generic_reflection_runtime_guard_deferral.jett similarity index 94% rename from tests/compile_pass/generic_reflection_runtime_guard_deferral.jett rename to tests/compile_fail/generic_reflection_runtime_guard_deferral.jett index c8c8589a..0a7bfc47 100644 --- a/tests/compile_pass/generic_reflection_runtime_guard_deferral.jett +++ b/tests/compile_fail/generic_reflection_runtime_guard_deferral.jett @@ -1,9 +1,9 @@ +# ERROR: E0345 function runtime_guarded_type_arg[T](value: T, flag: bool) returns int64: if flag: comptime type Elem = type.arg[T](0): return 1 return 0 - function main() returns nothing: int64 value = runtime_guarded_type_arg[string]("Ada", false) return nothing diff --git a/tests/compile_fail/generic_reflection_unknown_guard_invalid_binding.jett b/tests/compile_fail/generic_reflection_unknown_guard_invalid_binding.jett new file mode 100644 index 00000000..e22e6c02 --- /dev/null +++ b/tests/compile_fail/generic_reflection_unknown_guard_invalid_binding.jett @@ -0,0 +1,22 @@ +# ERROR: E0345 +# ERROR: E0345 +function invalid_match[T](view kind: TypeKind) returns int64: + match kind: + list_type: + comptime type Elem = type.arg[T](0): + return 1 + other: + return 0 +function invalid_callback[T](flag: bool) returns function() returns int64: + return function() returns int64: + if flag: + comptime type Elem = type.arg[T](0): + return 1 + return 0 +function main() returns nothing: + TypeKind kind = TypeKind.primitive_type + int64 count = invalid_match[string](view kind) + function() returns int64 callback = invalid_callback[string](false) + println(count) + println(callback()) + return nothing diff --git a/tests/compile_fail/global_constant_initializer.jett b/tests/compile_fail/global_constant_initializer.jett new file mode 100644 index 00000000..82775898 --- /dev/null +++ b/tests/compile_fail/global_constant_initializer.jett @@ -0,0 +1,12 @@ +# ERROR: E0378 x5 +namespace app +function compute() returns int64: + return 42 +int64 ordinary = compute() +string interpolated = "answer:{compute()}" +int64 calculated = 40 + 2 +bool inverted = not true +string literal_interpolation = "answer:{42}" +int64 explicit_value = comptime compute() +function main() returns nothing: + return nothing diff --git a/tests/compile_fail/graphics_callback_capability_alias.jett b/tests/compile_fail/graphics_callback_capability_alias.jett index 2b9a352c..d589ca3f 100644 --- a/tests/compile_fail/graphics_callback_capability_alias.jett +++ b/tests/compile_fail/graphics_callback_capability_alias.jett @@ -1,3 +1,5 @@ +# ERROR: E0300 +# The Graphics alias is not the int64 State required by render. # ERROR: E0372 namespace app type Display = Graphics diff --git a/tests/compile_fail/graphics_callback_capture_capability.jett b/tests/compile_fail/graphics_callback_capture_capability.jett index 0a3dbda6..02999310 100644 --- a/tests/compile_fail/graphics_callback_capture_capability.jett +++ b/tests/compile_fail/graphics_callback_capture_capability.jett @@ -5,4 +5,7 @@ function render(view state: int64) returns graphics.Scene: return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) function main(display: Graphics) returns result[nothing, string]: graphics.Config config = graphics.Config(title: "No callback authority", width: 320, height: 240) - return graphics.run[int64](view display, config, 0, function(state: int64, key: graphics.Key) returns int64: return display, render) + return graphics.run[int64](view display, config, 0, function(state: int64, key: graphics.Key) returns int64: + Graphics captured = display + return state + , render) diff --git a/tests/compile_fail/graphics_callback_policy.jett b/tests/compile_fail/graphics_callback_policy.jett index ae5a22a7..67e6f98b 100644 --- a/tests/compile_fail/graphics_callback_policy.jett +++ b/tests/compile_fail/graphics_callback_policy.jett @@ -1,4 +1,6 @@ # ERROR: E0372 x3 +# ERROR: E0300 +# impure_update also has Graphics where the update signature requires Key. namespace app function update(state: int64, key: graphics.Key) returns int64: return state diff --git a/tests/compile_fail/graphics_enum_equality_effect.jett b/tests/compile_fail/graphics_enum_equality_effect.jett new file mode 100644 index 00000000..fb9b39d2 --- /dev/null +++ b/tests/compile_fail/graphics_enum_equality_effect.jett @@ -0,0 +1,28 @@ +# ERROR: E0500 +# ERROR: E0372 x2 +namespace app +function effect(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "effect inside enum equality callback") + return 0 +struct Number: + value: int64 +implement Equatable for Number: + function equals(view self: Number, view other: Number) returns bool: + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 + return perform() == 0 +enum Boxed: + number(value: optional[list[Number]]) +function update(state: int64, key: graphics.Key) returns int64: + Boxed left = Boxed.number(some(list(Number(value: state)))) + Boxed right = Boxed.number(some(list(Number(value: state)))) + if left == right: + return state + 1 + return state +function render(view state: int64) returns graphics.Scene: + graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function main(display: Graphics) returns result[nothing, string]: + graphics.Config config = graphics.Config(title: "Callback review", width: 32, height: 32) + return graphics.run[int64](view display, config, 0, update, render) diff --git a/tests/compile_fail/graphics_equality_effect.jett b/tests/compile_fail/graphics_equality_effect.jett index 9e14c006..6b68f657 100644 --- a/tests/compile_fail/graphics_equality_effect.jett +++ b/tests/compile_fail/graphics_equality_effect.jett @@ -1,4 +1,5 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside equality callback") @@ -7,7 +8,9 @@ struct Number: value: int64 implement Equatable for Number: function equals(view self: Number, view other: Number) returns bool: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() == 0 function update(state: int64, key: graphics.Key) returns int64: Number left = Number(value: state) diff --git a/tests/compile_fail/graphics_fake_authority.jett b/tests/compile_fail/graphics_fake_authority.jett index 1d484069..29cb3d92 100644 --- a/tests/compile_fail/graphics_fake_authority.jett +++ b/tests/compile_fail/graphics_fake_authority.jett @@ -1,3 +1,5 @@ +# ERROR: E0300 +# The forged string is not a Graphics capability as well as not an explicit view. # ERROR: E0372 namespace app function update(state: int64, key: graphics.Key) returns int64: diff --git a/tests/compile_fail/graphics_generic_method_effect.jett b/tests/compile_fail/graphics_generic_method_effect.jett index c8716a1d..6721b324 100644 --- a/tests/compile_fail/graphics_generic_method_effect.jett +++ b/tests/compile_fail/graphics_generic_method_effect.jett @@ -1,4 +1,5 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside callback") @@ -11,7 +12,9 @@ struct Pure: value: int64 implement Evaluator for Effectful: function evaluate(view self: Effectful) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() implement Evaluator for Pure: function evaluate(view self: Pure) returns int64: diff --git a/tests/compile_fail/graphics_inline_callback_effect.jett b/tests/compile_fail/graphics_inline_callback_effect.jett index 6f283745..4c38c4ad 100644 --- a/tests/compile_fail/graphics_inline_callback_effect.jett +++ b/tests/compile_fail/graphics_inline_callback_effect.jett @@ -1,4 +1,5 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view display: Graphics) returns int64: return 0 @@ -7,4 +8,7 @@ function render(view state: int64) returns graphics.Scene: return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) function main(display: Graphics) returns result[nothing, string]: graphics.Config config = graphics.Config(title: "No callback effects", width: 320, height: 240) - return graphics.run[int64](view display, config, 0, function(state: int64, key: graphics.Key) returns int64: return effect("fake authority"), render) + return graphics.run[int64](view display, config, 0, function(state: int64, key: graphics.Key) returns int64: + function(view Graphics) returns int64 emit = function(view authority: Graphics) returns int64: return effect(view authority) + return state + , render) diff --git a/tests/compile_fail/graphics_inline_method_effect.jett b/tests/compile_fail/graphics_inline_method_effect.jett index 1aba9648..86551c0b 100644 --- a/tests/compile_fail/graphics_inline_method_effect.jett +++ b/tests/compile_fail/graphics_inline_method_effect.jett @@ -1,4 +1,5 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside callback") @@ -9,7 +10,9 @@ struct Number: value: int64 implement Evaluator for Number: function evaluate(view self: Number) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: Number) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_inline_transitive_effect.jett b/tests/compile_fail/graphics_inline_transitive_effect.jett index 6e10fca4..e4a519bc 100644 --- a/tests/compile_fail/graphics_inline_transitive_effect.jett +++ b/tests/compile_fail/graphics_inline_transitive_effect.jett @@ -1,10 +1,13 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside supposedly pure callback") return 0 function helper() returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: int64) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_mutual_method_effect.jett b/tests/compile_fail/graphics_mutual_method_effect.jett index 723c4c58..21e90efe 100644 --- a/tests/compile_fail/graphics_mutual_method_effect.jett +++ b/tests/compile_fail/graphics_mutual_method_effect.jett @@ -1,4 +1,5 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside callback") @@ -9,7 +10,9 @@ struct Number: value: int64 implement Evaluator for Number: function evaluate(view self: Number) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: int64) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_named_callback_nested_effect.jett b/tests/compile_fail/graphics_named_callback_nested_effect.jett index d7cd30d6..747b6a77 100644 --- a/tests/compile_fail/graphics_named_callback_nested_effect.jett +++ b/tests/compile_fail/graphics_named_callback_nested_effect.jett @@ -1,10 +1,13 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside supposedly pure callback") return 0 function update(state: int64, key: graphics.Key) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: int64) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_opaque_data.jett b/tests/compile_fail/graphics_opaque_data.jett index c690d3c4..e3a971bb 100644 --- a/tests/compile_fail/graphics_opaque_data.jett +++ b/tests/compile_fail/graphics_opaque_data.jett @@ -1,3 +1,5 @@ +# ERROR: E0300 +# The forbidden Graphics value is also not a graphics.Config. # ERROR: E0372 namespace app function update(state: int64, key: graphics.Key) returns int64: diff --git a/tests/compile_fail/graphics_pipeline_authority.jett b/tests/compile_fail/graphics_pipeline_authority.jett index bd9df3c7..99563c78 100644 --- a/tests/compile_fail/graphics_pipeline_authority.jett +++ b/tests/compile_fail/graphics_pipeline_authority.jett @@ -1,3 +1,5 @@ +# ERROR: E0300 +# The piped string is not a Graphics capability as well as not an explicit view. # ERROR: E0372 namespace app function update(state: int64, key: graphics.Key) returns int64: diff --git a/tests/compile_fail/graphics_pipeline_callback_effect.jett b/tests/compile_fail/graphics_pipeline_callback_effect.jett index eaccca9b..4ce37228 100644 --- a/tests/compile_fail/graphics_pipeline_callback_effect.jett +++ b/tests/compile_fail/graphics_pipeline_callback_effect.jett @@ -1,10 +1,13 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside callback") return 0 function update(state: int64, key: graphics.Key) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: int64) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_pipeline_inferred_callback_effect.jett b/tests/compile_fail/graphics_pipeline_inferred_callback_effect.jett index 0a87c1b3..d21efbe0 100644 --- a/tests/compile_fail/graphics_pipeline_inferred_callback_effect.jett +++ b/tests/compile_fail/graphics_pipeline_inferred_callback_effect.jett @@ -1,10 +1,13 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside callback") return 0 function update(state: int64, key: graphics.Key) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function render(view state: int64) returns graphics.Scene: graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) diff --git a/tests/compile_fail/graphics_state_callback_authority.jett b/tests/compile_fail/graphics_state_callback_authority.jett index fd471b57..be5273b8 100644 --- a/tests/compile_fail/graphics_state_callback_authority.jett +++ b/tests/compile_fail/graphics_state_callback_authority.jett @@ -1,4 +1,8 @@ +# ERROR: E0402 +# The state closure also attempts to capture the move-only Stdout capability. # ERROR: E0372 +# ERROR: E0500 +# Its no-capability inline signature cannot call the impure effect function. namespace app struct State: perform: function() returns int64 diff --git a/tests/compile_fail/graphics_transitive_callback_effect.jett b/tests/compile_fail/graphics_transitive_callback_effect.jett index ff802a2b..20b15aa0 100644 --- a/tests/compile_fail/graphics_transitive_callback_effect.jett +++ b/tests/compile_fail/graphics_transitive_callback_effect.jett @@ -1,10 +1,13 @@ # ERROR: E0500 +# ERROR: E0372 x2 namespace app function effect(view stdout: Stdout) returns int64: Stdout.write(view stdout, "effect inside supposedly pure callback") return 0 function helper() returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() function update(state: int64, key: graphics.Key) returns int64: return helper() diff --git a/tests/compile_fail/immutable_reassignment.jett b/tests/compile_fail/immutable_reassignment.jett new file mode 100644 index 00000000..9d7c982f --- /dev/null +++ b/tests/compile_fail/immutable_reassignment.jett @@ -0,0 +1,5 @@ +# ERROR: E0404 +function main() returns nothing: + int64 value = 1 + value = 2 + return nothing diff --git a/tests/compile_fail/interface_equality.jett b/tests/compile_fail/interface_equality.jett new file mode 100644 index 00000000..c6ea533e --- /dev/null +++ b/tests/compile_fail/interface_equality.jett @@ -0,0 +1,22 @@ +# ERROR: E0376 x7 +namespace app +interface Named: + function name(view self: Named) returns string +type Alias = Named +type Selected = Alias where true +type Again = Selected where true +type Hidden = secret[Named] where true +function equal(view left: Named, view right: Named) returns bool: + return left == right +function unequal(view left: Named, view right: Named) returns bool: + return left != right +function aliased(view left: Alias, view right: Alias) returns bool: + return left == right +function refined(view left: Again, view right: Again) returns bool: + return left != right +function hidden(view left: secret[Named], view right: secret[Named]) returns secret[bool]: + return left == right +function refined_hidden(view left: Hidden, view right: Hidden) returns secret[bool]: + return left != right +function erased_equatable(view left: Equatable, view right: Equatable) returns bool: + return left == right diff --git a/tests/compile_fail/interface_equality_comptime.jett b/tests/compile_fail/interface_equality_comptime.jett new file mode 100644 index 00000000..6eeead13 --- /dev/null +++ b/tests/compile_fail/interface_equality_comptime.jett @@ -0,0 +1,13 @@ +# ERROR: E0376 x2 +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return string.from_int64(self) +function erased() returns Named: + return 7 +function equal() returns bool: + return comptime(erased() == erased()) +function unequal() returns bool: + return comptime(erased() != erased()) diff --git a/tests/compile_fail/interface_equality_enum_payload.jett b/tests/compile_fail/interface_equality_enum_payload.jett new file mode 100644 index 00000000..5b104e5d --- /dev/null +++ b/tests/compile_fail/interface_equality_enum_payload.jett @@ -0,0 +1,18 @@ +# ERROR: E0376 x4 +namespace app +interface Named: + function name(view self: Named) returns string +type Selected = secret[Named] where true +enum Entry: + empty + nested(value: optional[list[Selected]]) +function equal(view left: Entry, view right: Entry) returns bool: + return left == right +function unequal(view left: Entry, view right: Entry) returns bool: + return left != right +function generic[T](view left: T, view right: T) returns bool: + return left == right +function selected(view left: Entry, view right: Entry) returns bool: + return generic[Entry](view left, view right) +function baking() returns bool: + return comptime(Entry.empty == Entry.empty) diff --git a/tests/compile_fail/interface_equality_generic.jett b/tests/compile_fail/interface_equality_generic.jett new file mode 100644 index 00000000..454997a3 --- /dev/null +++ b/tests/compile_fail/interface_equality_generic.jett @@ -0,0 +1,16 @@ +# ERROR: E0376 x3 +namespace app +interface Named: + function name(view self: Named) returns string +function equal[T](view left: T, view right: T) returns bool: + return left == right +function unequal[T](view left: T, view right: T) returns bool: + return left != right +function selected(view left: Named, view right: Named) returns bool: + bool same = equal[Named](view left, view right) + bool different = unequal[Named](view left, view right) + return same or different +function indirect(view left: Named, view right: Named) returns bool: + function(view Named, view Named) returns bool compare = function(view first: Named, view second: Named) returns bool: + return first == second + return compare(view left, view right) diff --git a/tests/compile_fail/mutable_global.jett b/tests/compile_fail/mutable_global.jett new file mode 100644 index 00000000..534ed89b --- /dev/null +++ b/tests/compile_fail/mutable_global.jett @@ -0,0 +1,8 @@ +# ERROR: E0377 x2 +namespace app +mutable int64 count = 0 +mutable string label = "global" +function main(stdout: Stdout) returns nothing: + mutable int64 local = 0 + local = 1 + Stdout.write(view stdout, "{count}:{label}:{local}") diff --git a/tests/compile_fail/ownership_machine_state_widening_use_after_move.jett b/tests/compile_fail/ownership_machine_state_widening_use_after_move.jett new file mode 100644 index 00000000..b5f34656 --- /dev/null +++ b/tests/compile_fail/ownership_machine_state_widening_use_after_move.jett @@ -0,0 +1,12 @@ +# ERROR: E0400 +namespace test +machine Session: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +function reuse_after_state_erasure() returns string: + Session at logged_in active = Session(logged_in, "Ada") + Session erased = active + return active.user_id diff --git a/tests/compile_fail/ownership_result_optional_wrappers_move_payloads.jett b/tests/compile_fail/ownership_result_optional_wrappers_move_payloads.jett new file mode 100644 index 00000000..93d8b1af --- /dev/null +++ b/tests/compile_fail/ownership_result_optional_wrappers_move_payloads.jett @@ -0,0 +1,16 @@ +# ERROR: E0400 x3 +namespace test +struct Payload: + value: string +function some_moves_payload() returns int64: + Payload payload = Payload(value: "some") + optional[Payload] wrapped = some(payload) + return string.char_count(payload.value) +function ok_moves_payload() returns int64: + Payload payload = Payload(value: "ok") + result[Payload, string] wrapped = ok(payload) + return string.char_count(payload.value) + 1 +function fail_moves_payload() returns int64: + Payload payload = Payload(value: "fail") + result[string, Payload] wrapped = fail(payload) + return string.char_count(payload.value) + 1 diff --git a/tests/compile_fail/projected_readonly_roots.jett b/tests/compile_fail/projected_readonly_roots.jett new file mode 100644 index 00000000..9f89db1d --- /dev/null +++ b/tests/compile_fail/projected_readonly_roots.jett @@ -0,0 +1,82 @@ +# ERROR: E0404 x13 +# ERROR: E0401 x10 +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +enum Choice: + wrapped(record: Record) +function make() returns Record: + return Record(inner: Inner(value: 1), label: "before") +function immutable_local() returns nothing: + Record record = make() + record.label = "after" + (record).inner.value = 9 +function owned_parameter(record: Record) returns nothing: + record.inner.value = 9 +function view_parameter(view record: Record) returns nothing: + record.inner.value = 9 +function mutable_view_parameter(view mutable record: Record) returns nothing: + record.inner.value = 9 +function owned_rebind_from_view(view source: Record) returns nothing: + mutable Record alias = make() + alias = view source +function parenthesized_owned_rebind_from_view(view source: Record) returns nothing: + mutable Record alias = make() + ((alias)) = view source +function temporary_roots() returns nothing: + Record(inner: Inner(value: 1), label: "before").inner.value = 9 + make().inner.value = 9 + Record record = make() + (clone record).inner.value = 9 +function explicit_view() returns nothing: + mutable Record record = make() + (view record).inner.value = 9 +function owned_loop() returns nothing: + list[Record] records = list(make()) + for record in records: + record.inner.value = 9 +function borrowed_loop() returns nothing: + mutable list[Record] records = list(make()) + for record in view records: + record.inner.value = 9 +function owned_match(choice: Choice) returns nothing: + match choice: + wrapped(record): + record.inner.value = 9 +function borrowed_match(view choice: Choice) returns nothing: + match choice: + wrapped(record): + record.inner.value = 9 +function expired_binding() returns nothing: + if false: + mutable Record record = make() + trace record + Record record = make() + record.inner.value = 9 +function inline_view() returns nothing: + function(view Record) returns nothing callback = function(view record: Record) returns nothing: + record.inner.value = 9 + Record source = make() + callback(view source) +actor Store: + app.Record record = make() + receive replace: + record.inner.value = 9 + receive owned(replacement: Record): + replacement.inner.value = 9 + receive borrowed(view mutable replacement: Record): + replacement.inner.value = 9 + receive rebind(view source: Record): + mutable Record alias = make() + alias = view source +verify readonly_local: + Record record = make() + record.inner.value = 9 + assert true +property readonly_given: + given record: Record + record.inner.value = 9 + assert true diff --git a/tests/compile_fail/random_capability_elements.jett b/tests/compile_fail/random_capability_elements.jett index 162da943..cb52ece3 100644 --- a/tests/compile_fail/random_capability_elements.jett +++ b/tests/compile_fail/random_capability_elements.jett @@ -3,7 +3,7 @@ namespace app function authority_selection_is_forbidden(view rng: Random, view items: list[Random]) returns nothing: random.choice[Random](view rng, view items) handle: return nothing - list[Random] shuffled = random.shuffle[Random](view rng, view items) + mutable list[Random] shuffled = random.shuffle[Random](view rng, view items) random.choice(view rng, view items) handle: return nothing shuffled = random.shuffle(view rng, view items) diff --git a/tests/compile_fail/refinement_sum_requires_handle.jett b/tests/compile_fail/refinement_sum_requires_handle.jett new file mode 100644 index 00000000..20e96a41 --- /dev/null +++ b/tests/compile_fail/refinement_sum_requires_handle.jett @@ -0,0 +1,9 @@ +# ERROR: E0333 x2 +type Choice = optional[int64] where true +type Outcome = result[int64, string] where true +function main() returns nothing: + optional[int64] maybe = some(1) + result[int64, string] source = ok(2) + Choice choice = maybe + Outcome outcome = source + return nothing diff --git a/tests/compile_fail/refinement_sum_requires_handle_error.jett b/tests/compile_fail/refinement_sum_requires_handle_error.jett new file mode 100644 index 00000000..d91d8daf --- /dev/null +++ b/tests/compile_fail/refinement_sum_requires_handle_error.jett @@ -0,0 +1,11 @@ +# ERROR: E0333 x2 +type Choice = optional[int64] where true +type Outcome = result[int64, string] where true +function main() returns nothing: + optional[int64] maybe = some(1) + result[int64, string] source = ok(2) + Choice choice = maybe handle: + return nothing + Outcome outcome = source handle: + return nothing + return nothing diff --git a/tests/compile_fail/state_machine_narrowed_assignment_to_other_state.jett b/tests/compile_fail/state_machine_narrowed_assignment_to_other_state.jett index 03bd215e..a9976514 100644 --- a/tests/compile_fail/state_machine_narrowed_assignment_to_other_state.jett +++ b/tests/compile_fail/state_machine_narrowed_assignment_to_other_state.jett @@ -1,5 +1,6 @@ # ERROR: E0312 # ERROR: E0400 +# ERROR: E0404 machine Session: states: guest diff --git a/tests/compile_fail/type_construct_put_wrong_arity.jett b/tests/compile_fail/type_construct_put_wrong_arity.jett index ff83acf9..c0b86288 100644 --- a/tests/compile_fail/type_construct_put_wrong_arity.jett +++ b/tests/compile_fail/type_construct_put_wrong_arity.jett @@ -3,7 +3,7 @@ namespace test struct User: id: string function bad_construct_put_arity() returns nothing: - TypeConstruction builder = type.construct_start[User]() + mutable TypeConstruction builder = type.construct_start[User]() TypeField field = list.get[TypeField](type.fields[User](), 0) handle: return nothing builder = type.construct_put[User](builder, view field, "u1") handle error: diff --git a/tests/compile_fail/type_construct_put_wrong_field_arg.jett b/tests/compile_fail/type_construct_put_wrong_field_arg.jett index 7dbecc7d..c858d3bd 100644 --- a/tests/compile_fail/type_construct_put_wrong_field_arg.jett +++ b/tests/compile_fail/type_construct_put_wrong_field_arg.jett @@ -3,7 +3,7 @@ namespace test struct User: id: string function bad_construct_put_field_arg() returns nothing: - TypeConstruction builder = type.construct_start[User]() + mutable TypeConstruction builder = type.construct_start[User]() builder = type.construct_put[User, string](builder, "id", "u1") handle error: return nothing return nothing diff --git a/tests/compile_fail/type_construct_put_wrong_value_type.jett b/tests/compile_fail/type_construct_put_wrong_value_type.jett index f0e29ec3..6dd42533 100644 --- a/tests/compile_fail/type_construct_put_wrong_value_type.jett +++ b/tests/compile_fail/type_construct_put_wrong_value_type.jett @@ -5,7 +5,7 @@ struct User: id: string function bad_construct_put_value_type() returns nothing: - TypeConstruction builder = type.construct_start[User]() + mutable TypeConstruction builder = type.construct_start[User]() TypeField field = list.get[TypeField](type.fields[User](), 0) handle: return nothing builder = type.construct_put[User, int64](builder, view field, "wrong") handle error: diff --git a/tests/compile_fail/type_field_value_mismatched_metadata.jett b/tests/compile_fail/type_field_value_mismatched_metadata.jett index 1ddcce38..3cc572a5 100644 --- a/tests/compile_fail/type_field_value_mismatched_metadata.jett +++ b/tests/compile_fail/type_field_value_mismatched_metadata.jett @@ -10,7 +10,7 @@ struct Other: function bad_field_value_metadata() returns string: User user = User(id: "u1") list[TypeField] fields = type.fields[Other]() - string found = "missing" + mutable string found = "missing" for field in fields: found = type.field_value[User, string](view user, view field) return found diff --git a/tests/compile_fail/type_field_value_non_struct_owner.jett b/tests/compile_fail/type_field_value_non_struct_owner.jett index 14644aa2..25d80fd2 100644 --- a/tests/compile_fail/type_field_value_non_struct_owner.jett +++ b/tests/compile_fail/type_field_value_non_struct_owner.jett @@ -6,7 +6,7 @@ struct User: function bad_field_value_non_struct_owner() returns string: list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: found = type.field_value[int64, string](42, view field) return found diff --git a/tests/compile_fail/type_field_value_wrong_arity.jett b/tests/compile_fail/type_field_value_wrong_arity.jett index 71c89775..8c9b74b0 100644 --- a/tests/compile_fail/type_field_value_wrong_arity.jett +++ b/tests/compile_fail/type_field_value_wrong_arity.jett @@ -8,7 +8,7 @@ struct User: function bad_field_value_arity() returns string: User user = User(id: "u1") list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: found = type.field_value[User](view user, view field) return found diff --git a/tests/compile_fail/type_field_value_wrong_requested_type.jett b/tests/compile_fail/type_field_value_wrong_requested_type.jett index fd5632f6..43664213 100644 --- a/tests/compile_fail/type_field_value_wrong_requested_type.jett +++ b/tests/compile_fail/type_field_value_wrong_requested_type.jett @@ -8,7 +8,7 @@ struct User: function bad_field_value_requested_type() returns int64: User user = User(id: "u1", age: 42) list[TypeField] fields = type.fields[User]() - int64 found = 0 + mutable int64 found = 0 for field in fields: if field.name == "id": found = type.field_value[User, int64](view user, view field) diff --git a/tests/compile_fail/type_field_value_wrong_value_arg.jett b/tests/compile_fail/type_field_value_wrong_value_arg.jett index c2b663b7..0f0af83e 100644 --- a/tests/compile_fail/type_field_value_wrong_value_arg.jett +++ b/tests/compile_fail/type_field_value_wrong_value_arg.jett @@ -6,7 +6,7 @@ struct User: function bad_field_value_value_arg() returns string: list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: found = type.field_value[User, string](42, view field) return found diff --git a/tests/compile_fail/type_machine_field_value_wrong_arity.jett b/tests/compile_fail/type_machine_field_value_wrong_arity.jett index 96e66512..97047075 100644 --- a/tests/compile_fail/type_machine_field_value_wrong_arity.jett +++ b/tests/compile_fail/type_machine_field_value_wrong_arity.jett @@ -8,7 +8,7 @@ machine Session: logged_in to logged_in function bad_machine_field_value_arity(view session: Session at logged_in) returns string: TypeMachineState state = type.machine_state_value[Session at logged_in](view session) - string found = "missing" + mutable string found = "missing" for field in state.fields: found = type.machine_field_value[Session at logged_in](view session, view field) return found diff --git a/tests/compile_fail/type_machine_field_value_wrong_requested_type.jett b/tests/compile_fail/type_machine_field_value_wrong_requested_type.jett index f3eadeec..311bc89f 100644 --- a/tests/compile_fail/type_machine_field_value_wrong_requested_type.jett +++ b/tests/compile_fail/type_machine_field_value_wrong_requested_type.jett @@ -8,7 +8,7 @@ machine Session: function bad_machine_field_value_requested_type() returns int64: Session at logged_in session = Session(logged_in, "user-1") TypeMachineState state = type.machine_state_value[Session at logged_in](view session) - int64 found = 0 + mutable int64 found = 0 for field in state.fields: found = type.machine_field_value[Session at logged_in, int64](view session, view field) return found diff --git a/tests/compile_fail/type_variant_field_value_wrong_arity.jett b/tests/compile_fail/type_variant_field_value_wrong_arity.jett index de991f7f..24707b5d 100644 --- a/tests/compile_fail/type_variant_field_value_wrong_arity.jett +++ b/tests/compile_fail/type_variant_field_value_wrong_arity.jett @@ -7,7 +7,7 @@ enum Shape: function bad_variant_field_value_arity() returns string: Shape shape = Shape.circle(2.5) - string found = "missing" + mutable string found = "missing" for field in type.variant_value[Shape](view shape).fields: found = type.variant_field_value[Shape](view shape, view field) return found diff --git a/tests/compile_fail/type_variant_field_value_wrong_requested_type.jett b/tests/compile_fail/type_variant_field_value_wrong_requested_type.jett index d018a91e..123d0157 100644 --- a/tests/compile_fail/type_variant_field_value_wrong_requested_type.jett +++ b/tests/compile_fail/type_variant_field_value_wrong_requested_type.jett @@ -6,7 +6,7 @@ enum Shape: function bad_variant_field_value_requested_type() returns int64: Shape shape = Shape.circle(2.5) - int64 found = 0 + mutable int64 found = 0 for field in type.variant_value[Shape](view shape).fields: found = type.variant_field_value[Shape, int64](view shape, view field) return found diff --git a/tests/compile_fail/view_explicit_owned_modes.jett b/tests/compile_fail/view_explicit_owned_modes.jett new file mode 100644 index 00000000..9ba7a633 --- /dev/null +++ b/tests/compile_fail/view_explicit_owned_modes.jett @@ -0,0 +1,17 @@ +# ERROR: E0375 x6 +namespace app +function consume(values: list[int64]) returns int64: + return list.length[int64](view values) +function generic_consume[T](values: list[T]) returns int64: + return list.length[T](view values) +function consume_more(amount: int64, values: list[int64]) returns int64: + return amount + list.length[int64](view values) +function explicit_modes(view values: list[int64]) returns nothing: + println(consume(view values)) + function(list[int64]) returns int64 callback = consume + println(callback((view values))) + println(generic_consume[int64](view values)) + println(values into view consume) + println((view values) into consume) + println(1 into consume_more(view values)) + return nothing diff --git a/tests/compile_fail/view_owned_arguments.jett b/tests/compile_fail/view_owned_arguments.jett new file mode 100644 index 00000000..b580e92c --- /dev/null +++ b/tests/compile_fail/view_owned_arguments.jett @@ -0,0 +1,42 @@ +# ERROR: E0401 x9 +namespace app +function consume(values: list[int64]) returns int64: + return list.length[int64](view values) +function generic_consume[T](values: list[T]) returns int64: + return list.length[T](view values) +function consume_more(amount: int64, values: list[int64]) returns int64: + return amount + list.length[int64](view values) +function ordinary(view values: list[int64]) returns int64: + return consume(values) +function indirect(view values: list[int64]) returns int64: + function(list[int64]) returns int64 callback = consume + return callback(values) +function explicit_generic(view values: list[int64]) returns int64: + return generic_consume[int64](values) +function inferred_generic(view values: list[int64]) returns int64: + return generic_consume(values) +function pipeline_initial(view values: list[int64]) returns int64: + return values into consume +function pipeline_extra(view values: list[int64]) returns int64: + return 1 into consume_more(values) +function local_alias(view values: list[int64]) returns int64: + list[int64] borrowed = view values + return consume(borrowed) +function forwarded_alias(view values: list[int64]) returns int64: + list[int64] borrowed = view values + list[int64] forwarded = borrowed + return(consume)(((forwarded))) +actor Store: + receive use_borrowed(view values: list[int64]): + int64 count = consume(values) + trace count +verify borrowed_local: + list[int64] source = list(7) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + assert consume(forwarded) == 1 +property borrowed_given: + given source: list[int64] + list[int64] borrowed = view source + list[int64] forwarded = borrowed + assert consume(forwarded) >= 0 diff --git a/tests/compile_fail/view_owned_returns.jett b/tests/compile_fail/view_owned_returns.jett new file mode 100644 index 00000000..6a203189 --- /dev/null +++ b/tests/compile_fail/view_owned_returns.jett @@ -0,0 +1,19 @@ +# ERROR: E0401 x6 +namespace app +function return_parameter(view values: list[int64]) returns list[int64]: + return values +function return_parenthesized(view values: list[int64]) returns list[int64]: + return((values)) +function return_mutable_parameter(view mutable values: list[int64]) returns list[int64]: + return values +function return_explicit_view(view values: list[int64]) returns list[int64]: + return view values +function return_forwarded_alias(view values: list[int64]) returns list[int64]: + list[int64] borrowed = view values + list[int64] forwarded = borrowed + return forwarded +function inline_return() returns nothing: + function(view list[int64]) returns list[int64] callback = function(view values: list[int64]) returns list[int64]: + return values + trace callback + return nothing diff --git a/tests/compile_fail/view_return_annotations.jett b/tests/compile_fail/view_return_annotations.jett new file mode 100644 index 00000000..98098a0a --- /dev/null +++ b/tests/compile_fail/view_return_annotations.jett @@ -0,0 +1,15 @@ +# ERROR: E0401 x4 +namespace app +function copy_values(view values: list[int64]) returns list[int64]: + return clone values +function written_return(view values: list[int64]) returns view list[int64]: + return clone values +function unused_generic[T](view value: T) returns view T: + return clone value +function inline_annotation() returns nothing: + function(view list[int64]) returns list[int64] callback = function(view values: list[int64]) returns view list[int64]: + return clone values + trace callback + return nothing +function callable_annotation() returns function(view list[int64]) returns view list[int64]: + return copy_values diff --git a/tests/compile_fail/generic_reflection_guarded_unknown_fact.jett b/tests/compile_pass/generic_reflection_guarded_unknown_fact.jett similarity index 95% rename from tests/compile_fail/generic_reflection_guarded_unknown_fact.jett rename to tests/compile_pass/generic_reflection_guarded_unknown_fact.jett index a7052c90..87306769 100644 --- a/tests/compile_fail/generic_reflection_guarded_unknown_fact.jett +++ b/tests/compile_pass/generic_reflection_guarded_unknown_fact.jett @@ -1,9 +1,7 @@ -# ERROR: E0345 function helper_requires_known_kind[T](value: T, kind: TypeKind) returns int64: if kind == TypeKind.list_type: comptime type Elem = type.arg[T](0): return 1 return 0 - function caller(value: list[int64], kind: TypeKind) returns int64: return helper_requires_known_kind[list[int64]](value, kind) diff --git a/tests/compile_fail/generic_reflection_match_unknown_fact.jett b/tests/compile_pass/generic_reflection_match_unknown_fact.jett similarity index 96% rename from tests/compile_fail/generic_reflection_match_unknown_fact.jett rename to tests/compile_pass/generic_reflection_match_unknown_fact.jett index b08cc3c4..aeda0b36 100644 --- a/tests/compile_fail/generic_reflection_match_unknown_fact.jett +++ b/tests/compile_pass/generic_reflection_match_unknown_fact.jett @@ -1,4 +1,3 @@ -# ERROR: E0345 function helper_match_requires_known_kind[T](value: T, kind: TypeKind) returns int64: match kind: list_type: diff --git a/tests/compile_pass/graphics_equality_generic_isolation.jett b/tests/compile_pass/graphics_equality_generic_isolation.jett index f8dd77ef..a2b92a9b 100644 --- a/tests/compile_pass/graphics_equality_generic_isolation.jett +++ b/tests/compile_pass/graphics_equality_generic_isolation.jett @@ -8,7 +8,9 @@ struct Pure: value: int64 implement Equatable for Effectful: function equals(view self: Effectful, view other: Effectful) returns bool: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() == 0 implement Equatable for Pure: function equals(view self: Pure, view other: Pure) returns bool: diff --git a/tests/compile_pass/graphics_generic_method_isolation.jett b/tests/compile_pass/graphics_generic_method_isolation.jett index 9b31f1c6..015cd3eb 100644 --- a/tests/compile_pass/graphics_generic_method_isolation.jett +++ b/tests/compile_pass/graphics_generic_method_isolation.jett @@ -10,7 +10,9 @@ struct Pure: value: int64 implement Evaluator for Effectful: function evaluate(view self: Effectful) returns int64: - function() returns int64 perform = function() returns int64: return effect("fake stdout") + function() returns int64 perform = function() returns int64: + function(view Stdout) returns int64 emit = function(view stdout: Stdout) returns int64: return effect(view stdout) + return 0 return perform() implement Evaluator for Pure: function evaluate(view self: Pure) returns int64: diff --git a/tests/native/actor_direct_identity.jett b/tests/native/actor_direct_identity.jett new file mode 100644 index 00000000..1c7f4cae --- /dev/null +++ b/tests/native/actor_direct_identity.jett @@ -0,0 +1,18 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +type CounterAlias = Counter +function same(view first: Counter, view second: Counter) returns bool: + return first == second +function main() returns nothing: + Counter worker = spawn Counter() + CounterAlias alias = clone worker + Counter distinct = spawn Counter() + println(worker == alias, worker != alias, worker == distinct, worker != distinct) + send worker.add(9) + println(same(view worker, view alias), same(view worker, view distinct)) + return nothing diff --git a/tests/native/actor_enum_equality.jett b/tests/native/actor_enum_equality.jett new file mode 100644 index 00000000..dc67dcb5 --- /dev/null +++ b/tests/native/actor_enum_equality.jett @@ -0,0 +1,13 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +function main() returns nothing: + Counter worker = spawn Counter() + Holder first = Holder.held(clone worker) + Holder second = Holder.held(clone worker) + println(first == second, first != second) + return nothing diff --git a/tests/native/actor_field_updates/01_nested_owners_independent_actors.jett b/tests/native/actor_field_updates/01_nested_owners_independent_actors.jett new file mode 100644 index 00000000..a2322f65 --- /dev/null +++ b/tests/native/actor_field_updates/01_nested_owners_independent_actors.jett @@ -0,0 +1,33 @@ +namespace app +struct Inner: + values: list[int64] + label: string +struct State: + inner: Inner + stamp: int64 +actor Store(seed: int64): + mutable State state = State(inner: Inner(values: list(seed), label: "start"), stamp: seed) + receive replace(values: list[int64], label: string): + state.inner.values = values + state.inner.label = label + receive append(value: int64): + state.inner.values = list.append[int64](clone state.inner.values, value) + state.inner.label = "{state.inner.label}!" + receive read responds string: + respond "{list.length(view state.inner.values)}:{list.sum[int64](view state.inner.values)}:{state.inner.label}:{state.stamp}" +function main(stdout: Stdout) returns nothing: + Store first = spawn Store(seed: 2) + Store second = spawn Store(seed: 9) + list[int64] supplied = list(3, 4) + string caption = "left" + send first.replace(clone supplied, caption) + send first.append(5) + send first.append(6) + string changed = ask first.read + string independent = ask second.read + Stdout.write(view stdout, "first:{changed}|{independent}|sender:{list.length(view supplied)}:{list.sum[int64](view supplied)}:{caption}\n") + send second.replace(supplied, "right") + string retained = ask first.read + string replaced = ask second.read + Stdout.write(view stdout, "after:{retained}|{replaced}\n") + return nothing diff --git a/tests/native/actor_field_updates/02_shadowed_message_and_ordinary_helper.jett b/tests/native/actor_field_updates/02_shadowed_message_and_ordinary_helper.jett new file mode 100644 index 00000000..73ce60f6 --- /dev/null +++ b/tests/native/actor_field_updates/02_shadowed_message_and_ordinary_helper.jett @@ -0,0 +1,29 @@ +namespace app +struct Inner: + value: int64 +struct State: + inner: Inner + label: string +function update(mutable item: State, value: int64) returns State: + item.inner.value = value + item.label = "helper" + return item +actor Store: + mutable State state = State(inner: Inner(value: 1), label: "initial") + receive shadow(mutable state: State) responds int64: + state.inner.value = state.inner.value + 10 + respond state.inner.value + receive change(value: int64): + state = update(state, value) + receive read responds string: + respond "{state.inner.value}:{state.label}" +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + int64 first_reply = ask store.shadow(State(inner: Inner(value: 100), label: "message")) + string before = ask store.read + send store.change(9) + string changed = ask store.read + int64 second_reply = ask store.shadow(State(inner: Inner(value: 200), label: "other")) + string retained = ask store.read + Stdout.write(view stdout, "shadow:{first_reply}|{before}|{changed}|{second_reply}|{retained}\n") + return nothing diff --git a/tests/native/actor_field_updates/03_early_respond_preserves_old_field.jett b/tests/native/actor_field_updates/03_early_respond_preserves_old_field.jett new file mode 100644 index 00000000..eceac008 --- /dev/null +++ b/tests/native/actor_field_updates/03_early_respond_preserves_old_field.jett @@ -0,0 +1,33 @@ +namespace app +struct Inner: + value: int64 +actor Store: + mutable Inner inner = Inner(value: 4) + receive set(value: int64): + inner.value = value + receive replace(candidate: optional[int64]) responds int64: + inner.value = candidate handle: + respond inner.value + default 999 + respond inner.value + receive read responds int64: + respond inner.value +function round_trip(value: int64) returns int64: + Store store = spawn Store() + send store.set(value) + int64 echoed = ask store.replace(none) + return echoed +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + int64 first = ask store.replace(none) + int64 before = ask store.read + int64 changed = ask store.replace(some(9)) + int64 second = ask store.replace(none) + int64 after = ask store.read + Stdout.write(view stdout, "early:{first}:{before}:{changed}:{second}:{after}\n") + return nothing +verify actor_early_response: + assert round_trip(17) == 17 +property actor_early_response_trials: + given value: int64 + assert round_trip(value) == value diff --git a/tests/native/actor_field_updates/04_consumed_message_root_rejected.jett b/tests/native/actor_field_updates/04_consumed_message_root_rejected.jett new file mode 100644 index 00000000..d21f7acb --- /dev/null +++ b/tests/native/actor_field_updates/04_consumed_message_root_rejected.jett @@ -0,0 +1,13 @@ +namespace app +struct Packet: + value: int64 +function consume(packet: Packet) returns int64: + return packet.value +actor Store: + receive change(mutable supplied: Packet): + int64 consumed = consume(supplied) + supplied.value = consumed + 1 +function main() returns nothing: + Store store = spawn Store() + send store.change(Packet(value: 1)) + return nothing diff --git a/tests/native/actor_field_updates/05_persistent_state_not_restored_rejected.jett b/tests/native/actor_field_updates/05_persistent_state_not_restored_rejected.jett new file mode 100644 index 00000000..7407a0bf --- /dev/null +++ b/tests/native/actor_field_updates/05_persistent_state_not_restored_rejected.jett @@ -0,0 +1,13 @@ +namespace app +struct Packet: + value: int64 +function consume(packet: Packet) returns int64: + return packet.value +actor Store: + mutable Packet saved = Packet(value: 1) + receive drain: + int64 consumed = consume(saved) +function main() returns nothing: + Store store = spawn Store() + send store.drain + return nothing diff --git a/tests/native/actor_field_updates/06_view_message_root_rejected.jett b/tests/native/actor_field_updates/06_view_message_root_rejected.jett new file mode 100644 index 00000000..fbbf6fee --- /dev/null +++ b/tests/native/actor_field_updates/06_view_message_root_rejected.jett @@ -0,0 +1,8 @@ +namespace app +struct Packet: + value: int64 +actor Store: + receive change(view mutable supplied: Packet): + supplied.value = 9 +function main() returns nothing: + return nothing diff --git a/tests/native/actor_field_updates/07_immutable_state_root_rejected.jett b/tests/native/actor_field_updates/07_immutable_state_root_rejected.jett new file mode 100644 index 00000000..671a65e9 --- /dev/null +++ b/tests/native/actor_field_updates/07_immutable_state_root_rejected.jett @@ -0,0 +1,11 @@ +namespace app +struct Packet: + value: int64 +actor Store: + Packet saved = Packet(value: 1) + receive change: + saved.value = 9 +function main() returns nothing: + Store store = spawn Store() + send store.change + return nothing diff --git a/tests/native/actor_field_updates/08_live_whole_state_loan_rejected.jett b/tests/native/actor_field_updates/08_live_whole_state_loan_rejected.jett new file mode 100644 index 00000000..bbefeb89 --- /dev/null +++ b/tests/native/actor_field_updates/08_live_whole_state_loan_rejected.jett @@ -0,0 +1,15 @@ +namespace app +struct Inner: + value: int64 +struct State: + inner: Inner +actor Store: + mutable State state = State(inner: Inner(value: 1)) + receive change responds int64: + State alias = view state + state.inner.value = 9 + respond alias.inner.value +function main() returns nothing: + Store store = spawn Store() + int64 forbidden = ask store.change + return nothing diff --git a/tests/native/actor_field_updates/09_live_state_field_loan_rejected.jett b/tests/native/actor_field_updates/09_live_state_field_loan_rejected.jett new file mode 100644 index 00000000..29f85e45 --- /dev/null +++ b/tests/native/actor_field_updates/09_live_state_field_loan_rejected.jett @@ -0,0 +1,15 @@ +namespace app +struct Inner: + value: int64 +struct State: + inner: Inner +actor Store: + mutable State state = State(inner: Inner(value: 1)) + receive change responds int64: + Inner alias = view state.inner + state.inner.value = 9 + respond alias.value +function main() returns nothing: + Store store = spawn Store() + int64 forbidden = ask store.change + return nothing diff --git a/tests/native/actor_field_updates/10_nonshadowing_message_and_helper.jett b/tests/native/actor_field_updates/10_nonshadowing_message_and_helper.jett new file mode 100644 index 00000000..a2509939 --- /dev/null +++ b/tests/native/actor_field_updates/10_nonshadowing_message_and_helper.jett @@ -0,0 +1,29 @@ +namespace app +struct Inner: + value: int64 +struct State: + inner: Inner + label: string +function update(mutable item: State, value: int64) returns State: + item.inner.value = value + item.label = "helper" + return item +actor Store: + mutable State state = State(inner: Inner(value: 1), label: "initial") + receive shadow(mutable incoming_state: State) responds int64: + incoming_state.inner.value = incoming_state.inner.value + 10 + respond incoming_state.inner.value + receive change(value: int64): + state = update(state, value) + receive read responds string: + respond "{state.inner.value}:{state.label}" +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + int64 first_reply = ask store.shadow(State(inner: Inner(value: 100), label: "message")) + string before = ask store.read + send store.change(9) + string changed = ask store.read + int64 second_reply = ask store.shadow(State(inner: Inner(value: 200), label: "other")) + string retained = ask store.read + Stdout.write(view stdout, "message:{first_reply}|{before}|{changed}|{second_reply}|{retained}\n") + return nothing diff --git a/tests/native/actor_field_updates/11_persistent_aliases_and_independent_field.jett b/tests/native/actor_field_updates/11_persistent_aliases_and_independent_field.jett new file mode 100644 index 00000000..a8f85bdd --- /dev/null +++ b/tests/native/actor_field_updates/11_persistent_aliases_and_independent_field.jett @@ -0,0 +1,25 @@ +namespace app +struct Inner: + value: int64 +actor Store(seed: int64): + mutable list[int64] owned = list(seed, seed + 1) + list[int64] immediate = view owned + list[int64] transitive = view immediate + mutable Inner inner = Inner(value: seed) + receive advance(amount: int64): + inner.value = inner.value + amount + list.sum[int64](view transitive) + receive read responds string: + respond "{list.length(view immediate)}:{list.sum[int64](view immediate)}:{list.length(view transitive)}:{list.sum[int64](view transitive)}:{inner.value}" +function main(stdout: Stdout) returns nothing: + Store first = spawn Store(seed: 2) + Store second = spawn Store(seed: 10) + string first_before = ask first.read + string second_before = ask second.read + Stdout.write(view stdout, "before:{first_before}|{second_before}\n") + send first.advance(3) + send first.advance(4) + send second.advance(1) + string first_after = ask first.read + string second_after = ask second.read + Stdout.write(view stdout, "after:{first_after}|{second_after}\n") + return nothing diff --git a/tests/native/actor_field_updates/12_persistent_alias_owner_replacement_rejected.jett b/tests/native/actor_field_updates/12_persistent_alias_owner_replacement_rejected.jett new file mode 100644 index 00000000..8ca8d486 --- /dev/null +++ b/tests/native/actor_field_updates/12_persistent_alias_owner_replacement_rejected.jett @@ -0,0 +1,18 @@ +namespace app +struct Inner: + value: int64 +actor Store(seed: int64): + mutable list[int64] owned = list(seed, seed + 1) + list[int64] immediate = view owned + list[int64] transitive = view immediate + mutable Inner inner = Inner(value: seed) + receive replace(values: list[int64]): + owned = values + inner.value = inner.value + 1 + receive read responds string: + respond "{list.length(view immediate)}:{list.sum[int64](view transitive)}:{inner.value}" +function main() returns nothing: + Store store = spawn Store(seed: 2) + send store.replace(list(9, 10)) + string forbidden = ask store.read + return nothing diff --git a/tests/native/actor_field_updates/13_respond_is_terminal_handle_exit.jett b/tests/native/actor_field_updates/13_respond_is_terminal_handle_exit.jett new file mode 100644 index 00000000..e2f4ace9 --- /dev/null +++ b/tests/native/actor_field_updates/13_respond_is_terminal_handle_exit.jett @@ -0,0 +1,32 @@ +namespace app +struct Inner: + value: int64 +actor Store: + mutable Inner inner = Inner(value: 4) + receive set(value: int64): + inner.value = value + receive replace(candidate: optional[int64]) responds int64: + inner.value = candidate handle: + respond inner.value + respond inner.value + receive read responds int64: + respond inner.value +function round_trip(value: int64) returns int64: + Store store = spawn Store() + send store.set(value) + int64 echoed = ask store.replace(none) + return echoed +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + int64 first = ask store.replace(none) + int64 before = ask store.read + int64 changed = ask store.replace(some(9)) + int64 second = ask store.replace(none) + int64 after = ask store.read + Stdout.write(view stdout, "early:{first}:{before}:{changed}:{second}:{after}\n") + return nothing +verify actor_early_response: + assert round_trip(17) == 17 +property actor_early_response_trials: + given value: int64 + assert round_trip(value) == value diff --git a/tests/native/actor_field_updates/14_projected_persistent_alias.jett b/tests/native/actor_field_updates/14_projected_persistent_alias.jett new file mode 100644 index 00000000..2ce82049 --- /dev/null +++ b/tests/native/actor_field_updates/14_projected_persistent_alias.jett @@ -0,0 +1,40 @@ +namespace app +struct Inner: + value: int64 + label: string +struct Outer: + prefix: int64 + inner: Inner + suffix: string +struct Envelope: + marker: string + outer: Outer + stamp: int64 +struct Progress: + total: int64 +function make(seed: int64) returns Envelope: + Inner leaf = Inner(value: seed, label: "inner-{seed}") + Outer middle = Outer(prefix: seed + 100, inner: leaf, suffix: "outer-{seed}") + return Envelope(marker: "envelope-{seed}", outer: middle, stamp: seed + 1000) +actor Store(seed: int64): + Envelope envelope = make(seed) + Inner projected = view envelope.outer.inner + Inner transitive = view projected + mutable Progress progress = Progress(total: seed) + receive advance(amount: int64): + progress.total = progress.total + amount + transitive.value + receive read responds string: + respond "{envelope.marker}:{envelope.outer.prefix}:{envelope.outer.inner.value}:{envelope.outer.inner.label}:{envelope.outer.suffix}:{envelope.stamp}|{projected.value}:{projected.label}|{transitive.value}:{transitive.label}|{progress.total}" +function main(stdout: Stdout) returns nothing: + Store first = spawn Store(seed: 2) + Store second = spawn Store(seed: 7) + string first_before = ask first.read + string second_before = ask second.read + Stdout.write(view stdout, "before-first:{first_before}\nbefore-second:{second_before}\n") + send first.advance(3) + send first.advance(4) + send second.advance(1) + string first_after = ask first.read + string second_after = ask second.read + Stdout.write(view stdout, "after-first:{first_after}\nafter-second:{second_after}\n") + return nothing diff --git a/tests/native/actor_field_updates/15_owning_handle_reply.jett b/tests/native/actor_field_updates/15_owning_handle_reply.jett new file mode 100644 index 00000000..3cbbc736 --- /dev/null +++ b/tests/native/actor_field_updates/15_owning_handle_reply.jett @@ -0,0 +1,22 @@ +namespace app +struct Inner: + value: int64 +function maybe_values(valid: bool, value: int64) returns result[list[int64], string]: + if valid: + return ok(list(value, value + 1)) + return fail("unavailable") +actor Store: + mutable Inner inner = Inner(value: 1) + receive snapshot(valid: bool) responds list[int64]: + inner.value = inner.value + 1 + respond maybe_values(valid, inner.value) handle error: + default list(9) + receive read responds int64: + respond inner.value +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + list[int64] success = ask store.snapshot(true) + list[int64] fallback = ask store.snapshot(false) + int64 stored = ask store.read + Stdout.write(view stdout, "reply:{list.length[int64](view success)}:{list.sum[int64](view success)}|{list.length[int64](view fallback)}:{list.sum[int64](view fallback)}|{stored}\n") + return nothing diff --git a/tests/native/actor_field_updates/16_nonresponding_return_preserves_state.jett b/tests/native/actor_field_updates/16_nonresponding_return_preserves_state.jett new file mode 100644 index 00000000..d0b18bd1 --- /dev/null +++ b/tests/native/actor_field_updates/16_nonresponding_return_preserves_state.jett @@ -0,0 +1,33 @@ +namespace app +struct Inner: + value: int64 +function finish() returns nothing: + return nothing +actor Store: + mutable Inner inner = Inner(value: 1) + mutable string label = "initial" + receive replace(value: int64): + inner.value = value + label = "changed" + return finish() + label = "unreachable" + receive guarded(candidate: optional[int64]): + inner.value = candidate handle: + return nothing + default 999 + label = "selected" + return nothing + receive read responds string: + respond "{inner.value}:{label}" +function main(stdout: Stdout) returns nothing: + Store first = spawn Store() + Store second = spawn Store() + send first.replace(7) + string committed = ask first.read + send first.guarded(none) + string retained = ask first.read + send first.guarded(some(9)) + string selected = ask first.read + string independent = ask second.read + Stdout.write(view stdout, "void-return:{committed}|{retained}|{selected}|{independent}\n") + return nothing diff --git a/tests/native/actor_messages.jett b/tests/native/actor_messages.jett new file mode 100644 index 00000000..238a6e7c --- /dev/null +++ b/tests/native/actor_messages.jett @@ -0,0 +1,18 @@ +namespace test +actor Counter(label: string): + mutable int64 count = 0 + mutable string title = "{label}!" + receive increment(amount: int64): + count = count + amount + receive rename(text: string): + title = text + receive read responds string: + respond "{title}: {count}" +function main() returns nothing: + Counter counter = spawn Counter(label: "hits") + send counter.increment(2) + send counter.increment(3) + send counter.rename("hits") + send counter.read + string reading = ask counter.read + println(reading) diff --git a/tests/native/actor_nested_identity.jett b/tests/native/actor_nested_identity.jett new file mode 100644 index 00000000..697a1036 --- /dev/null +++ b/tests/native/actor_nested_identity.jett @@ -0,0 +1,36 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +enum Leaf: + held(worker: Counter) + vacant +enum Tree: + leaf(worker: Counter) + children(values: list[Tree]) +enum Bundle: + grouped(values: list[Leaf], worker: optional[Counter]) +struct Keyed: + key: int64 + worker: Counter +implement Equatable for Keyed: + function equals(view self: Keyed, view other: Keyed) returns bool: + return self.key == other.key +enum Custom: + keyed(value: Keyed) +function main() returns nothing: + Counter worker = spawn Counter() + Counter other = spawn Counter() + Bundle first = Bundle.grouped(list(Leaf.held(clone worker)), some(clone worker)) + Bundle alias = clone first + Bundle different = Bundle.grouped(list(Leaf.held(clone other)), some(clone other)) + println(first == alias, first != alias, first == different, first != different) + Tree original = Tree.children(list(Tree.leaf(clone worker))) + Tree repeated = Tree.children(list(Tree.leaf(clone worker))) + Tree distinct = Tree.children(list(Tree.leaf(clone other))) + println(original == repeated, original != distinct) + Custom left = Custom.keyed(Keyed(key: 7, worker: clone worker)) + Custom right = Custom.keyed(Keyed(key: 7, worker: clone other)) + println(left == right, left != right) + return nothing diff --git a/tests/native/actor_optimized_identity.jett b/tests/native/actor_optimized_identity.jett new file mode 100644 index 00000000..f13d1383 --- /dev/null +++ b/tests/native/actor_optimized_identity.jett @@ -0,0 +1,46 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +enum Bundle: + grouped(values: list[Holder], worker: optional[Counter]) +struct Keyed: + key: int64 + worker: Counter +implement Equatable for Keyed: + function equals(view self: Keyed, view other: Keyed) returns bool: + return self.key == other.key +enum Custom: + keyed(value: Keyed) +function main(stdout: Stdout) returns nothing: + Counter worker = spawn Counter() + Counter alias = clone worker + Counter other = spawn Counter() + Stdout.write(view stdout, "{worker == alias} {worker != alias} {worker == other} {worker != other}\n") + Bundle first = Bundle.grouped(list(Holder.held(clone worker)), some(clone worker)) + Bundle repeated = clone first + Bundle different = Bundle.grouped(list(Holder.held(clone other)), some(clone other)) + Stdout.write(view stdout, "{first == repeated} {first != repeated} {first == different} {first != different}\n") + Counter pending = run clone worker + Counter also_pending = run clone worker + Counter twice = run clone pending + Holder pending_first = Holder.held(clone pending) + Holder pending_repeated = Holder.held(clone also_pending) + Holder deeper = Holder.held(clone twice) + Holder ready = Holder.held(clone worker) + Stdout.write(view stdout, "{pending_first == pending_repeated} {pending_first != pending_repeated} {pending_first == deeper} {pending_first != deeper} {pending_first == ready} {pending_first != ready}\n") + Counter once = join twice handle error: + return Stdout.write(view stdout, "unexpected first join: {error}\n") + Counter joined = join once handle error: + return Stdout.write(view stdout, "unexpected second join: {error}\n") + send joined.add(9) + Stdout.write(view stdout, "{worker == joined} {worker != joined} {ask worker.read} {ask joined.read}\n") + Custom left = Custom.keyed(Keyed(key: 7, worker: clone worker)) + Custom right = Custom.keyed(Keyed(key: 7, worker: clone other)) + Stdout.write(view stdout, "{left == right} {left != right}\n") + return nothing diff --git a/tests/native/actor_pending_alias_baseline.jett b/tests/native/actor_pending_alias_baseline.jett new file mode 100644 index 00000000..be8f5d2d --- /dev/null +++ b/tests/native/actor_pending_alias_baseline.jett @@ -0,0 +1,13 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = spawn Counter() + Counter pending = run clone worker + println(view worker, view pending) + Counter ready = join pending handle error: + return println(error) + println(view worker, view ready) + return nothing diff --git a/tests/native/actor_pending_alias_identity.jett b/tests/native/actor_pending_alias_identity.jett new file mode 100644 index 00000000..55ed9fb2 --- /dev/null +++ b/tests/native/actor_pending_alias_identity.jett @@ -0,0 +1,28 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +enum Holder: + held(worker: Counter) +function main() returns nothing: + Counter worker = spawn Counter() + Counter alias = clone worker + Counter pending = run clone worker + Counter also_pending = run clone worker + Counter twice = run clone pending + println(worker == alias, worker != alias) + Holder first = Holder.held(clone pending) + Holder repeated = Holder.held(clone also_pending) + Holder deeper = Holder.held(clone twice) + Holder ready = Holder.held(clone worker) + println(first == repeated, first != repeated, first == deeper, first != deeper, first == ready, first != ready) + Counter once = join twice handle error: + return println(error) + Counter joined = join once handle error: + return println(error) + send joined.add(9) + println(worker == joined, worker != joined, ask worker.read, ask joined.read) + return nothing diff --git a/tests/native/actor_pending_identity.jett b/tests/native/actor_pending_identity.jett new file mode 100644 index 00000000..0efb3926 --- /dev/null +++ b/tests/native/actor_pending_identity.jett @@ -0,0 +1,10 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive read responds int64: + respond count +function main() returns nothing: + Counter worker = spawn Counter() + Counter pending = run clone worker + println(pending == pending) + return nothing diff --git a/tests/native/actor_spawn.jett b/tests/native/actor_spawn.jett new file mode 100644 index 00000000..968780ce --- /dev/null +++ b/tests/native/actor_spawn.jett @@ -0,0 +1,9 @@ +namespace test +actor Holder(seed: string): + mutable string label = "{seed}!" + receive noop: + return nothing +function main() returns nothing: + Holder first = spawn Holder(seed: "alpha") + Holder second = spawn Holder(seed: "beta") + println("spawned") diff --git a/tests/native/alias_construction.jett b/tests/native/alias_construction.jett new file mode 100644 index 00000000..d4b65e0b --- /dev/null +++ b/tests/native/alias_construction.jett @@ -0,0 +1,22 @@ +namespace models +export struct Record: + id: string +export type RecordAlias = Record +namespace app +function alias_put_error() returns string: + use models + mutable TypeConstruction builder = type.construct_start[models.RecordAlias]() + TypeField field = list.get[TypeField](type.fields[models.Record](), 0) handle: + return "missing base field" + builder = type.construct_put[models.RecordAlias, string](builder, view field, "x") handle error: + return error + return "unexpected put success" +function alias_finish_error() returns string: + use models + TypeConstruction builder = type.construct_start[models.RecordAlias]() + models.RecordAlias item = type.construct_finish[models.RecordAlias](builder) handle error: + return error + return item.id +function main() returns nothing: + println(alias_put_error()) + println(alias_finish_error()) diff --git a/tests/native/arithmetic_matrix.jett b/tests/native/arithmetic_matrix.jett new file mode 100644 index 00000000..b773d528 --- /dev/null +++ b/tests/native/arithmetic_matrix.jett @@ -0,0 +1,62 @@ +namespace app +function signed8(value: int8) returns nothing: + int8 minimum = -128 + int8 negative_one = -1 + println("i8", value + 1, minimum - 1, value * 2, minimum / negative_one, minimum modulo negative_one, value > minimum) + return nothing +function signed16(value: int16) returns nothing: + int16 minimum = -32768 + int16 negative_one = -1 + println("i16", value + 1, minimum - 1, value * 2, minimum / negative_one, minimum modulo negative_one, value > minimum) + return nothing +function signed32(value: int32) returns nothing: + int32 minimum = -2147483648 + int32 negative_one = -1 + println("i32", value + 1, minimum - 1, value * 2, minimum / negative_one, minimum modulo negative_one, value > minimum) + return nothing +function signed64(value: int64) returns nothing: + int64 minimum = -9223372036854775808 + println("i64", value + 1, minimum - 1, value * 2, minimum / -1, minimum modulo -1, value > minimum) + return nothing +function unsigned8(value: uint8) returns nothing: + uint8 zero = 0 + println("u8", value + 1, zero - 1, value * 2, value / 2, value modulo 2, value > zero) + return nothing +function unsigned16(value: uint16) returns nothing: + uint16 zero = 0 + println("u16", value + 1, zero - 1, value * 2, value / 2, value modulo 2, value > zero) + return nothing +function unsigned32(value: uint32) returns nothing: + uint32 zero = 0 + println("u32", value + 1, zero - 1, value * 2, value / 2, value modulo 2, value > zero) + return nothing +function unsigned64(value: uint64) returns nothing: + uint64 zero = 0 + println("u64", value + 1, zero - 1, value * 2, value / 2, value modulo 2, value > zero) + return nothing +function float32_edges(zero: float32) returns nothing: + float32 left = 1.25 + float32 right = -2.5 + float32 infinity = 1.0 / zero + float32 not_a_number = zero / zero + println("f32", left + right, left - right, left * right, left / right, infinity > 1.0, not_a_number != not_a_number, not_a_number == not_a_number, -zero == zero, 1.0 / -zero < 0.0) + return nothing +function float64_edges(zero: float64) returns nothing: + float64 left = 1.25 + float64 right = -2.5 + float64 infinity = 1.0 / zero + float64 not_a_number = zero / zero + println("f64", left + right, left - right, left * right, left / right, infinity > 1.0, not_a_number != not_a_number, not_a_number == not_a_number, -zero == zero, 1.0 / -zero < 0.0) + return nothing +function main() returns nothing: + signed8(127) + signed16(32767) + signed32(2147483647) + signed64(9223372036854775807) + unsigned8(255) + unsigned16(65535) + unsigned32(4294967295) + unsigned64(18446744073709551615) + float32_edges(0.0) + float64_edges(0.0) + return nothing diff --git a/tests/native/assert_messages.jett b/tests/native/assert_messages.jett new file mode 100644 index 00000000..24c99a6f --- /dev/null +++ b/tests/native/assert_messages.jett @@ -0,0 +1,11 @@ +namespace test +function answer() returns int64: + return 41 +function explosive_message() returns string: + return string.repeat("overflow", 9223372036854775807) +verify answer: + assert true "this message must not be evaluated: {explosive_message()}" + assert answer() == 41 "expected 42, got {answer()}" +property same_value: + given value: int64 + assert value == value "expected {value} to equal itself" diff --git a/tests/native/bitfield_reflection.jett b/tests/native/bitfield_reflection.jett new file mode 100644 index 00000000..5809417d --- /dev/null +++ b/tests/native/bitfield_reflection.jett @@ -0,0 +1,20 @@ +namespace app +enum Protocol: + tcp = 6 + udp = 17 +bitfield network Packet: + version: 4 bits + reserved: 4 bits + protocol: 8 bits as Protocol + payload: list[uint8] +type PacketAlias = Packet +function main() returns nothing: + TypeBitfield layout = type.bitfield_layout[Packet]() + println(layout.network_order) + list[TypeBitfieldField] fields = type.bitfield_fields[Packet]() + for field in fields: + println(field.index, field.name, field.shape, field.shape_tag == TypeBitfieldFieldShape.payload_field, field.width, field.type_info.type_name) + list[TypeBitfieldField] aliases = type.bitfield_fields[PacketAlias]() + println(list.length[TypeBitfieldField](view aliases)) + TypeBitfield other = type.bitfield_layout[int64]() + println(other.network_order) diff --git a/tests/native/bitfield_values.jett b/tests/native/bitfield_values.jett new file mode 100644 index 00000000..55f3d954 --- /dev/null +++ b/tests/native/bitfield_values.jett @@ -0,0 +1,75 @@ +namespace app +bitfield Header: + version: 4 bits + length: 8 bits +enum Protocol: + tcp = 6 + udp = 17 +bitfield network NetworkHeader: + version: 4 bits + header_length: 4 bits + protocol: 8 bits as Protocol +bitfield WideNativeHeader: + sequence: 64 bits +bitfield network WideNetworkHeader: + sequence: 64 bits +bitfield Packet: + kind: 8 bits + payload: list[uint8] +function decode_header_error(raw: bytes) returns string: + Header decoded = Header.from_bytes(raw) handle error: + return error + return "ok" +function decode_protocol_error(raw: bytes) returns string: + NetworkHeader decoded = NetworkHeader.from_bytes(raw) handle error: + return error + return "ok" +function main() returns nothing: + Header original = Header(version: 4, length: 12) + Header duplicate = clone original + println(original.version, duplicate.length) + bytes header_raw = Header.to_bytes(original) + println(bytes.to_hex(view header_raw)) + Header header_decoded = Header.from_bytes(header_raw) handle error: + return nothing + println(header_decoded.version, header_decoded.length) + NetworkHeader network = NetworkHeader(version: 4, header_length: 5, protocol: Protocol.tcp) + bytes network_raw = NetworkHeader.to_bytes(network) + println(bytes.to_hex(view network_raw)) + NetworkHeader network_decoded = NetworkHeader.from_bytes(network_raw) handle error: + return nothing + println(network_decoded.protocol == Protocol.tcp, network_decoded.protocol != Protocol.udp) + println(network_decoded.header_length, bytes.to_hex(NetworkHeader.to_bytes(network_decoded))) + WideNativeHeader wide = WideNativeHeader(sequence: 72623859790382856) + bytes wide_raw = WideNativeHeader.to_bytes(wide) + println(bytes.to_hex(view wide_raw)) + WideNativeHeader wide_decoded = WideNativeHeader.from_bytes(wide_raw) handle error: + return nothing + println(string.from_uint64(wide_decoded.sequence)) + WideNetworkHeader network_wide = WideNetworkHeader(sequence: 18446744073709551615) + bytes network_wide_raw = WideNetworkHeader.to_bytes(network_wide) + println(bytes.to_hex(view network_wide_raw)) + WideNetworkHeader network_wide_decoded = WideNetworkHeader.from_bytes(network_wide_raw) handle error: + return nothing + println(string.from_uint64(network_wide_decoded.sequence)) + mutable list[uint8] payload = list.new[uint8]() + payload = list.append[uint8](payload, 7) + payload = list.append[uint8](payload, 127) + payload = list.append[uint8](payload, 255) + Packet packet = Packet(kind: 3, payload: payload) + Packet copied = clone packet + println(packet.kind, list.length(view copied.payload)) + bytes packet_raw = Packet.to_bytes(copied) + println(bytes.to_hex(view packet_raw)) + Packet packet_decoded = Packet.from_bytes(packet_raw) handle error: + return nothing + println(packet_decoded.kind, list.length(view packet_decoded.payload)) + bytes too_short = bytes.from_hex("40") handle error: + return nothing + println(decode_header_error(too_short)) + bytes extra_byte = bytes.from_hex("40c0ff") handle error: + return nothing + println(decode_header_error(extra_byte)) + bytes unknown_protocol = bytes.from_hex("4512") handle error: + return nothing + println(decode_protocol_error(unknown_protocol)) diff --git a/tests/native/borrowed_return_clone_controls.jett b/tests/native/borrowed_return_clone_controls.jett new file mode 100644 index 00000000..19ddad2f --- /dev/null +++ b/tests/native/borrowed_return_clone_controls.jett @@ -0,0 +1,86 @@ +namespace app +struct Record: + values: list[int64] + label: string +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 +function copy_values[T](view values: list[T]) returns list[T]: + return clone values +function copy_record(view source: Record) returns Record: + return clone source +function copy_field(view source: Record) returns list[int64]: + return source.values +function copy_scalar(view value: int64) returns int64: + return value +function copy_text(view text: string) returns string: + return text +function coarsen_copy(view source: NonEmptyNumbers) returns list[int64]: + return coarsen clone source +function reveal_copy(view source: secret[list[int64]]) returns list[int64]: + return declassify clone source +function consume(values: list[int64]) returns int64: + return list.length[int64](view values) +function generic_consume[T](values: list[T]) returns int64: + return list.length[T](view values) +function consume_more(amount: int64, values: list[int64]) returns int64: + return amount + list.length[int64](view values) +function first(view values: list[int64]) returns int64: + return list.get[int64](view values, 0) handle: + default 0 +function owned_paths(view source: list[int64]) returns string: + int64 ordinary = consume(clone source) + function(list[int64]) returns int64 callback = consume + int64 indirect = callback(clone source) + int64 generic = generic_consume[int64](clone source) + int64 pipeline =(clone source) into consume + int64 extra = 1 into consume_more(clone source) + return "{ordinary}:{indirect}:{generic}:{pipeline}:{extra}:{list.length(view source)}" +function copy_report() returns string: + Record source = Record(values: list(3, 5, 7), label: "before") + Record copied_record = copy_record(view source) + mutable list[int64] copied_values = copy_values[int64](view source.values) + copied_values = list.append[int64](copied_values, 9) + list[int64] copied_field = copy_field(view source) + return "{list.length(view source.values)}:{list.length(view copied_values)}:{list.length(view copied_record.values)}:{list.length(view copied_field)}:{source.label}" +function scalar_report() returns string: + int64 number = 7 + string text = "before" + return "{copy_scalar(view number)}:{copy_text(view text)}" +function wrapped_report() returns string: + NonEmptyNumbers refined = list(5, 7) handle error: + return error + list[int64] coarse = coarsen_copy(view refined) + list[int64] again_coarse = coarsen_copy(view refined) + list[int64] public_source = list(5, 7) + secret[list[int64]] hidden = public_source + list[int64] public = reveal_copy(view hidden) + list[int64] again = reveal_copy(view hidden) + return "{list.length(view coarse)}:{list.length(view again_coarse)}:{list.length(view public)}:{list.length(view again)}" +function baked_report() returns string: + list[int64] baked = comptime copy_values[int64](view list(2, 3, 5)) + string paths = comptime owned_paths(view list(2, 3, 5)) + int64 number = comptime copy_scalar(view 7) + string text = comptime copy_text(view "seed") + return "{list.length(view baked)}:{paths}:{number}:{text}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "copies:{copy_report()}\n") + list[int64] source = list(2, 3, 5) + Stdout.write(view stdout, "owned:{owned_paths(view source)}\n") + Stdout.write(view stdout, "scalars:{scalar_report()}\n") + Stdout.write(view stdout, "wrapped:{wrapped_report()}\n") + Stdout.write(view stdout, "baked:{baked_report()}\n") +verify borrowed_copies: + assert copy_report() == "3:4:3:3:before" + assert owned_paths(view list(2, 3, 5)) == "3:3:3:3:4:3" + assert scalar_report() == "7:before" + assert wrapped_report() == "2:2:2:2" + assert baked_report() == "3:3:3:3:3:4:3:7:seed" +property copied_source_survives: + given seed: int64 + list[int64] source = list(seed, seed + 1, seed + 2) + list[int64] copied = copy_values[int64](view source) + assert first(view copied) == seed + assert list.length(view source) == 3 + assert owned_paths(view source) == "3:3:3:3:4:3" + assert copy_scalar(view seed) == seed + assert copy_text(view "seed") == "seed" + assert wrapped_report() == "2:2:2:2" diff --git a/tests/native/breakpoint_consumed_bindings.jett b/tests/native/breakpoint_consumed_bindings.jett new file mode 100644 index 00000000..4ac2d0ab --- /dev/null +++ b/tests/native/breakpoint_consumed_bindings.jett @@ -0,0 +1,49 @@ +namespace app +type Count = uint8 +struct Packet: + data: bytes +function take(value: bytes) returns nothing: + breakpoint + return nothing +actor Worker: + receive consume(value: bytes): + breakpoint + take(value) + breakpoint + +function branch(flag: bool) returns nothing: + bytes item = bytes.from_string("branch") + if flag: + take(item) + breakpoint + return nothing +function main() returns nothing: + bytes initial = bytes.from_string("first") + bytes moved = initial + breakpoint + take(moved) + breakpoint + mutable bytes reused = bytes.from_string("old") + take(reused) + breakpoint + reused = bytes.from_string("new") + breakpoint + list[bytes] items = list(bytes.from_string("item")) + for item in items: + breakpoint + take(item) + breakpoint + breakpoint + result[bytes, string] failed = fail("failure") + bytes absent = failed handle error: + breakpoint + default bytes.new() + breakpoint + branch(true) + branch(false) + Worker worker = spawn Worker() + send worker.consume(bytes.from_string("actor")) + Count original_count = 7 + Count copied_count = original_count + breakpoint + return nothing diff --git a/tests/native/breakpoint_int64.jett b/tests/native/breakpoint_int64.jett new file mode 100644 index 00000000..137a683c --- /dev/null +++ b/tests/native/breakpoint_int64.jett @@ -0,0 +1,7 @@ +function main() returns nothing: + breakpoint true + if true: + int64 count = 9 + breakpoint false + breakpoint count > 0 + breakpoint true diff --git a/tests/native/breakpoint_lexical_frames.jett b/tests/native/breakpoint_lexical_frames.jett new file mode 100644 index 00000000..e306fa87 --- /dev/null +++ b/tests/native/breakpoint_lexical_frames.jett @@ -0,0 +1,33 @@ +namespace test + +function leaf(value: int64) returns nothing: + breakpoint + if value > 0: + string nested = "leaf" + breakpoint + breakpoint + return nothing + +function invoke(callback: function(int64) returns nothing) returns nothing: + string caller = "indirect caller" + callback(2) + breakpoint + return nothing + +function make_snapshot(seed: uint8) returns function(int64) returns nothing: + string label = "captured" + return function(value: int64) returns nothing: + breakpoint + leaf(value) + breakpoint + return nothing + +function main() returns nothing: + string caller = "main caller" + int64 value = 99 + leaf(1) + invoke(leaf) + function(int64) returns nothing snapshot = make_snapshot(7) + snapshot(3) + breakpoint + return nothing diff --git a/tests/native/bytes_ownership.jett b/tests/native/bytes_ownership.jett new file mode 100644 index 00000000..91460c1b --- /dev/null +++ b/tests/native/bytes_ownership.jett @@ -0,0 +1,15 @@ +function combine(first: bytes, second: bytes) returns bytes: + return bytes.concat(first, second) +function observe(view value: bytes) returns int64: + return bytes.length(view value) +function main() returns nothing: + bytes original = bytes.from_string("Hi") + bytes duplicate = clone original + bytes joined = combine(duplicate, bytes.from_string("!")) + println(observe(view original), bytes.to_hex(view joined)) + mutable bytes current = bytes.new() + mutable int64 index = 0 + while index < 3: + current = bytes.concat(current, bytes.from_string("x")) + index = index + 1 + println(bytes.to_hex(view current), bytes.length(view original)) diff --git a/tests/native/captured_closure_values.jett b/tests/native/captured_closure_values.jett new file mode 100644 index 00000000..8ade2ca9 --- /dev/null +++ b/tests/native/captured_closure_values.jett @@ -0,0 +1,12 @@ +namespace app +function make_label(prefix: string) returns function(int64) returns string: + return function(value: int64) returns string: return "{prefix}:{value}" +function make_scale(factor: float32) returns function(float32) returns float32: + return function(value: float32) returns float32: return value * factor +function make_offset(delta: int8) returns function(int8) returns int8: + return function(value: int8) returns int8: return value + delta +function main() returns nothing: + function(int64) returns string label = make_label("item") + function(float32) returns float32 scale = make_scale(3.0) + function(int8) returns int8 offset = make_offset(-5) + println(label(7), scale(1.5), offset(12)) diff --git a/tests/native/clock_production.jett b/tests/native/clock_production.jett new file mode 100644 index 00000000..b761eff5 --- /dev/null +++ b/tests/native/clock_production.jett @@ -0,0 +1,8 @@ +namespace app + +function main(clock: Clock, stdout: Stdout) returns nothing: + time.Timestamp now = Clock.now(view clock) + if time.to_unix_milliseconds(now) == 0: + Stdout.write(view stdout, "zero") + else: + Stdout.write(view stdout, "clock-ok") diff --git a/tests/native/closure_reflection_guards.jett b/tests/native/closure_reflection_guards.jett new file mode 100644 index 00000000..8292ce1e --- /dev/null +++ b/tests/native/closure_reflection_guards.jett @@ -0,0 +1,67 @@ +function make_count[T]() returns function(T) returns int64: + return function(value: T) returns int64: + string label = "type:{type.name[T]()}" + if type.kind_tag[T]() == TypeKind.list_type: + list[int64] items = value + return list.length[int64](view items) + return 0 +function make_match[T]() returns function(T) returns int64: + return function(value: T) returns int64: + TypeKind kind = type.kind_tag[T]() + match kind: + list_type: + list[int64] items = value + return list.length[int64](view items) + other: + return 0 +function summarized[T](value: T) returns string: + string label = "type:{type.name[T]()}" + if type.kind_tag[T]() == TypeKind.list_type: + list[int64] items = value + return "{label}:{list.length[int64](view items)}" + return "{label}:0" +function unknown_if[T](value: T, view kind: TypeKind) returns int64: + if kind == TypeKind.list_type: + comptime type Elem = type.arg[T](0): + list[Elem] items = value + return list.length[Elem](view items) + return 0 +function unknown_match[T](value: T, view kind: TypeKind) returns int64: + match kind: + list_type: + comptime type Elem = type.arg[T](0): + list[Elem] items = value + return list.length[Elem](view items) + other: + return 0 +function unknown_callback[T](flag: bool) returns function(T) returns int64: + return function(value: T) returns int64: + if flag: + comptime type Elem = type.arg[T](0): + list[Elem] items = value + return list.length[Elem](view items) + return 0 +function exercise_unknown(kind: TypeKind) returns nothing: + println(unknown_if[list[int64]](list(1, 2), view kind)) + println(unknown_match[list[int64]](list(1, 2, 3), view kind)) + function(list[int64]) returns int64 callback = unknown_callback[list[int64]](kind == TypeKind.list_type) + println(callback(list(1, 2, 3, 4))) + return nothing +function main() returns nothing: + function(int64) returns int64 scalar = make_count[int64]() + function(list[int64]) returns int64 sequence = make_count[list[int64]]() + println(scalar(7)) + println(sequence(list(1, 2))) + function(int64) returns int64 baked_scalar = comptime make_match[int64]() + function(list[int64]) returns int64 baked_sequence = comptime make_match[list[int64]]() + println(baked_scalar(7)) + println(baked_sequence(list(1, 2, 3))) + println(summarized[int64](9)) + println(summarized[list[int64]](list(4, 5))) + exercise_unknown(TypeKind.list_type) + exercise_unknown(TypeKind.primitive_type) + function(list[int64]) returns int64 baked_true = comptime unknown_callback[list[int64]](true) + function(list[int64]) returns int64 baked_false = comptime unknown_callback[list[int64]](false) + println(baked_true(list(8, 9))) + println(baked_false(list(8, 9))) + return nothing diff --git a/tests/native/coarsen_local_refinement_reuse.jett b/tests/native/coarsen_local_refinement_reuse.jett new file mode 100644 index 00000000..4539aade --- /dev/null +++ b/tests/native/coarsen_local_refinement_reuse.jett @@ -0,0 +1,10 @@ +namespace app +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 +function main() returns nothing: + NonEmptyNumbers checked = list(2, 3) handle error: + println(error) + return nothing + list[int64] raw = coarsen checked + trace checked + trace raw + return nothing diff --git a/tests/native/collection_callbacks.jett b/tests/native/collection_callbacks.jett new file mode 100644 index 00000000..0eb3adb4 --- /dev/null +++ b/tests/native/collection_callbacks.jett @@ -0,0 +1,49 @@ +namespace app +function double(value: int64) returns int64: + return value * 2 +function expand(value: int64) returns list[int64]: + return list(value, value + 10) +function add(total: int64, value: int64) returns int64: + return total + value +function parity(value: int64) returns string: + return string.from_int64(value modulo 2) +function observe(key: string, value: int64) returns int64: + trace value + return value +function list_callbacks() returns string: + list[int64] kept = list.filter[int64](list(1, 2, 3, 4), function(value: int64) returns bool: return value modulo 2 == 0) + list[int64] doubled = list.map[int64, int64](kept, double) + list[int64] expanded = list.flat_map[int64, int64](doubled, expand) + int64 total = list.reduce[int64, int64](clone expanded, 0, add) + optional[int64] found = list.find[int64](view expanded, function(value: int64) returns bool: return value > 10) + int64 first_big = found handle: + default 0 + bool all_positive = list.all[int64](view expanded, function(value: int64) returns bool: return value > 0) + bool any_large = list.any[int64](view expanded, function(value: int64) returns bool: return value > 15) + int64 count = list.count[int64](view expanded, function(value: int64) returns bool: return value < 10) + return "{total}:{first_big}:{all_positive}:{any_large}:{count}" +function map_callbacks() returns string: + map[string, int64] scores = map("ada": 2, "lin": -1, "sam": 4) + map[string, int64] positive = map.filter[string, int64](scores, function(key: string, value: int64) returns bool: return value > 0) + map[string, string] labels = map.map_values[string, int64, string](positive, function(value: int64) returns string: return "score-{value}") + string ada = map.get_or[string, string](clone labels, "ada", "missing") + string sam = map.get_or[string, string](labels, "sam", "missing") + return "{ada}:{sam}" +function ordering_callbacks() returns string: + int64 offset = 5 + list[int64] ordered = list.sort_by[int64](list(3, 1, 2), function(value: int64) returns int64: return value + offset) + int64 first = list.get[int64](view ordered, 0) handle: + default 0 + map[string, list[int64]] groups = list.group_by[int64](list(1, 2, 3, 4), parity) + list[int64] odd = map.get_or[string, list[int64]](groups, "1", list.new[int64]()) + return "{first}:{list.length[int64](view odd)}" +function for_each_callback() returns nothing: + map[string, int64] scores = map("ada": 2) + map.for_each[string, int64, int64](scores, observe) + return nothing +function main() returns nothing: + println(list_callbacks()) + println(map_callbacks()) + println(ordering_callbacks()) + for_each_callback() + return nothing diff --git a/tests/native/collection_shapes.jett b/tests/native/collection_shapes.jett new file mode 100644 index 00000000..d417938e --- /dev/null +++ b/tests/native/collection_shapes.jett @@ -0,0 +1,31 @@ +namespace app +function list_shapes() returns string: + list[list.Pair[int64, string]] pairs = list.zip[int64, string](list(1, 2, 3), list("one", "two")) + list.Pair[int64, string] first = list.first[list.Pair[int64, string]](view pairs) handle: + return "missing pair" + list[list[int64]] chunks = list.chunk[int64](list(1, 2, 3, 4, 5), 2) + list[int64] tail = list.get[list[int64]](view chunks, 2) handle: + return "missing chunk" + list[list.Indexed[string]] indexed = list.enumerate[string](list("a", "b")) + list.Indexed[string] second = list.get[list.Indexed[string]](view indexed, 1) handle: + return "missing index" + list[int64] flattened = list.flatten[int64](list(list(1, 2), list(3))) + return "{list.length[list.Pair[int64, string]](view pairs)}:{first.first}:{first.second}:{list.length[int64](view tail)}:{second.index}:{second.value}:{list.length[int64](view flattened)}" +function set_shapes() returns string: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 3) + values = set.add[int64](values, 1) + list[int64] ordered = list.from_set[int64](clone values) + list[int64] alternate = set.to_list[int64](values) + return "{list.length[int64](view ordered)}:{list.length[int64](view alternate)}" +function map_shapes() returns string: + map[string, int64] values = map("a": 1, "b": 2) + list[map.Entry[string, int64]] entries = map.entries[string, int64](values) + map.Entry[string, int64] first = list.first[map.Entry[string, int64]](view entries) handle: + return "missing entry" + return "{list.length[map.Entry[string, int64]](view entries)}:{first.key}:{first.value}" +function main() returns nothing: + println(list_shapes()) + println(set_shapes()) + println(map_shapes()) + return nothing diff --git a/tests/native/comptime_actor_computation.jett b/tests/native/comptime_actor_computation.jett new file mode 100644 index 00000000..a0d9b57f --- /dev/null +++ b/tests/native/comptime_actor_computation.jett @@ -0,0 +1,17 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +function calculate() returns int64: + Counter counter = spawn Counter() + send counter.add(4) + return ask counter.read +function main() returns nothing: + int64 live = calculate() + int64 baked = comptime calculate() + println("live:{live}") + println("baked:{baked}") + return nothing diff --git a/tests/native/comptime_builders.jett b/tests/native/comptime_builders.jett new file mode 100644 index 00000000..947e9927 --- /dev/null +++ b/tests/native/comptime_builders.jett @@ -0,0 +1,178 @@ +namespace app +type Positive = int64 where value > 0 +struct Record: + count: Positive + token: secret[string] + label: string +function partial(number: int64) returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Record]() + for field in type.fields[Record](): + if field.name == "count": + builder = type.construct_put[Record, int64](builder, view field, number) handle error: + return type.construct_start[Record]() + if field.name == "token": + secret[string] token = "hidden-baked-builder" + builder = type.construct_put[Record, secret[string]](builder, view field, token) handle error: + return type.construct_start[Record]() + return builder +function finish_record(builder: TypeConstruction) returns string: + Record value = type.construct_finish[Record](builder) handle error: + return error + return "record:{coarsen value.count}:{value.label}" +function complete(builder: TypeConstruction) returns TypeConstruction: + mutable TypeConstruction current = builder + for field in type.fields[Record](): + if field.name == "label": + current = type.construct_put[Record, string](current, view field, "runtime") handle error: + return type.construct_start[Record]() + return current +enum Event: + idle + named(token: secret[string]) +function event_builder() returns TypeConstruction: + TypeVariant variant = list.get[TypeVariant](type.variants[Event](), 1) handle: + return type.construct_start[Record]() + mutable TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return type.construct_start[Record]() + for field in variant.fields: + secret[string] token = "hidden-baked-builder" + builder = type.construct_put[Event, secret[string]](builder, view field, token) handle error: + return type.construct_start[Record]() + return builder +machine Session: + states: + empty + active(count: int64) + transitions: + empty to active +function state_builder() returns TypeConstruction: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return type.construct_start[Record]() + mutable TypeConstruction builder = type.construct_machine_start[Session at active](view state) handle error: + return type.construct_start[Record]() + for field in state.fields: + builder = type.construct_put[Session at active, int64](builder, view field, 7) handle error: + return type.construct_start[Record]() + return builder +bitfield Header: + kind: 8 bits +function header_builder(number: int64) returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Header]() + for field in type.fields[Header](): + builder = type.construct_put[Header, int64](builder, view field, number) handle error: + return type.construct_start[Header]() + return builder +function finish_header(builder: TypeConstruction) returns string: + Header value = type.construct_finish[Header](builder) handle error: + return error + return "header:{value.kind}" +interface Named: + function name(view self: Named) returns string +implement Named for TypeConstruction: + function name(view self: TypeConstruction) returns string: + return "builder" +struct Holder: + builder: TypeConstruction +struct Box[T]: + item: T +function boxed[T](value: T) returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Box[T]]() + for field in type.fields[Box[T]](): + builder = type.construct_put[Box[T], T](builder, view field, value) handle error: + return type.construct_start[Box[T]]() + return builder +type Label = string +type DeclaredLabel = string +struct AliasRecord: + label: DeclaredLabel +function alias_builder() returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[AliasRecord]() + for field in type.fields[AliasRecord](): + builder = type.construct_put[AliasRecord, string](builder, view field, "declared-alias") handle error: + return type.construct_start[AliasRecord]() + return builder +function nested_builder() returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + builder = type.construct_put[Holder, TypeConstruction](builder, view field, partial(12)) handle error: + return type.construct_start[Holder]() + return builder +function reordered() returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Record]() + TypeField token_field = list.get[TypeField](type.fields[Record](), 1) handle: + return type.construct_start[Record]() + secret[string] token = "hidden-baked-builder" + builder = type.construct_put[Record, secret[string]](builder, view token_field, token) handle error: + return type.construct_start[Record]() + TypeField count_field = list.get[TypeField](type.fields[Record](), 0) handle: + return type.construct_start[Record]() + return type.construct_put[Record, int64](builder, view count_field, 14) handle error: + return type.construct_start[Record]() +function additional_builders() returns nothing: + TypeConstruction small = comptime boxed[int8](127) + Box[int8] small_value = type.construct_finish[Box[int8]](small) handle error: + return nothing + println(small_value.item) + TypeConstruction alias = comptime boxed[Label]("alias") + trace alias + Box[Label] alias_value = type.construct_finish[Box[Label]](alias) handle error: + return nothing + println(alias_value.item) + TypeConstruction declared_alias = comptime alias_builder() + AliasRecord declared = type.construct_finish[AliasRecord](declared_alias) handle error: + return nothing + println(declared.label) + TypeConstruction nested = comptime nested_builder() + trace nested + Holder holder = type.construct_finish[Holder](nested) handle error: + return nothing + println(finish_record(complete(holder.builder))) + TypeConstruction ordered = comptime reordered() + trace ordered + println(finish_record(complete(ordered))) + return nothing +function records() returns nothing: + TypeConstruction empty = comptime type.construct_start[Record]() + trace empty + println(finish_record(empty)) + TypeConstruction partial_value = comptime partial(5) + trace partial_value + TypeConstruction copied = clone partial_value + println(finish_record(complete(copied))) + println(finish_record(partial_value)) + TypeConstruction invalid = comptime partial(0) + println(finish_record(complete(invalid))) + Holder stored = comptime Holder(builder: partial(8)) + trace stored + println(finish_record(complete(stored.builder))) + list[TypeConstruction] items = comptime list(partial(1), partial(2)) + for value in items: + println(finish_record(complete(value))) + TypeConstruction later = comptime run partial(9) + trace later + TypeConstruction ready = join later handle error: + return nothing + println(finish_record(complete(ready))) + Named erased = comptime partial(6) + trace erased + println(Named.name(view erased)) + return nothing +function main() returns nothing: + records() + additional_builders() + TypeConstruction event = comptime event_builder() + trace event + Event built_event = type.construct_finish[Event](event) handle error: + return nothing + trace built_event + TypeConstruction state = comptime state_builder() + trace state + Session at active built_state = type.construct_finish[Session at active](state) handle error: + return nothing + println(built_state.count) + TypeConstruction header = comptime header_builder(3) + trace header + println(finish_header(header)) + TypeConstruction invalid_header = comptime header_builder(256) + println(finish_header(invalid_header)) + return nothing diff --git a/tests/native/comptime_captured_functions.jett b/tests/native/comptime_captured_functions.jett new file mode 100644 index 00000000..7f93d213 --- /dev/null +++ b/tests/native/comptime_captured_functions.jett @@ -0,0 +1,34 @@ +namespace test +function make_shift() returns function(int64) returns int64: + int64 offset = 5 + return function(value: int64) returns int64: return value + offset +function make_pair() returns function(int64) returns int64: + int64 unused = 100 + int64 first = 3 + int64 second = 4 + return function(value: int64) returns int64: return value + first + second +function make_label(prefix: string) returns function(int64) returns string: + return function(value: int64) returns string: return "{prefix}:{value}" +function make_plain() returns function(int64) returns int64: + int64 unused = 100 + return function(value: int64) returns int64: return value + 2 +verify baked_function_values: + function(int64) returns int64 inline = comptime function(value: int64) returns int64: return value + 1 + assert inline(3) == 4 + function(int64) returns int64 captured = comptime make_shift() + assert captured(3) == 8 +function main() returns nothing: + function(int64) returns int64 shifted = comptime make_shift() + println(shifted(7)) + function(int64) returns int64 pair = comptime make_pair() + println(pair(7)) + function(int64) returns string label = comptime make_label("item") + println(label(7)) + list[function(int64) returns string] labels = comptime list(make_label("a"), make_label("b")) + optional[function(int64) returns string] last = list.get[function(int64) returns string](view labels, 1) + function(int64) returns string selected = last handle: + default label + println(selected(7)) + function(int64) returns int64 plain = comptime make_plain() + println(plain(7)) + return nothing diff --git a/tests/native/comptime_composites.jett b/tests/native/comptime_composites.jett new file mode 100644 index 00000000..f3cda473 --- /dev/null +++ b/tests/native/comptime_composites.jett @@ -0,0 +1,73 @@ +namespace test +struct Pair: + first: int64 + second: string +enum Choice: + empty + number(value: int64) +bitfield Header: + version: 4 bits + length: 8 bits +machine Session: + states: + guest + active(user: string) + transitions: + guest to active +function baked_success() returns result[int64, string]: + return ok(13) +function main() returns nothing: + uint64 wide = comptime 18446744073709551615 + println(string.from_uint64(wide)) + list[int64] baked = comptime list(3, 5, 8) + int64 first = list.get[int64](view baked, 0) handle: + default 0 + println(first, list.length[int64](view baked)) + list[list[int64]] nested = comptime list(list(1), list(2, 3)) + println(list.length[list[int64]](view nested)) + map[string, int64] mapped = comptime map("a": 7, "b": 9) + println(map.get_or[string, int64](clone mapped, "b", 0)) + set[int64] selected = comptime set.add[int64](set.add[int64](set.new[int64](), 3), 5) + println(set.contains[int64](view selected, 5)) + optional[int64] present = comptime some(11) + int64 unwrapped = present handle: + default 0 + println(unwrapped) + optional[int64] absent = comptime none + int64 absent_value = absent handle: + default 12 + println(absent_value) + result[int64, string] success = comptime ok(13) + int64 result_value = success handle error: + default 0 + println(result_value) + result[int64, string] from_function = comptime baked_success() + int64 from_function_value = from_function handle error: + default 0 + println(from_function_value) + result[int64, string] direct_success = ok(21) + int64 direct_value = direct_success handle error: + default 0 + println(direct_value) + result[int64, string] failure = comptime fail("bad") + int64 failure_value = failure handle error: + println(error) + default 0 + println(failure_value) + Pair pair = comptime Pair(first: 17, second: "hi") + println(pair.first, pair.second) + Header header = comptime Header(version: 4, length: 12) + println(header.version, header.length) + Session at active session = comptime Session(active, "Lin") + println(session.user) + Session broad = comptime Session(guest) + println(broad at guest) + bytes raw = comptime bytes.from_string("Hi") + println(bytes.to_hex(view raw)) + Choice choice = comptime Choice.number(19) + match choice: + empty: + println(0) + number(value): + println(value) + return nothing diff --git a/tests/native/comptime_function_values.jett b/tests/native/comptime_function_values.jett new file mode 100644 index 00000000..11c2fc98 --- /dev/null +++ b/tests/native/comptime_function_values.jett @@ -0,0 +1,26 @@ +namespace test +function increment(value: int64) returns int64: + return value + 1 +function double(value: int64) returns int64: + return value * 2 +function choose(increment_first: bool) returns function(int64) returns int64: + if increment_first: + return increment + return double +function make_shift() returns function(int64) returns int64: + return function(value: int64) returns int64: return value + 4 +function main() returns nothing: + function(int64) returns int64 baked = comptime increment + println(baked(7)) + function(int64) returns int64 chosen = comptime choose(false) + println(chosen(7)) + list[function(int64) returns int64] callbacks = comptime list(increment, double) + optional[function(int64) returns int64] last = list.get[function(int64) returns int64](view callbacks, 1) + function(int64) returns int64 selected = last handle: + default increment + println(selected(7)) + function(int64) returns int64 inline = comptime function(value: int64) returns int64: return value + 3 + println(inline(7)) + function(int64) returns int64 from_call = comptime make_shift() + println(from_call(7)) + return nothing diff --git a/tests/native/comptime_generic_closures.jett b/tests/native/comptime_generic_closures.jett new file mode 100644 index 00000000..90b40e25 --- /dev/null +++ b/tests/native/comptime_generic_closures.jett @@ -0,0 +1,37 @@ +type Label = string +function make_label[T](prefix: T) returns function(int64) returns string: + return function(value: int64) returns string: return "{prefix}:{value}" +function identity[T](value: T) returns T: + return value +function make_echo[T]() returns function(T) returns T: + return function(value: T) returns T: return identity[T](value) +function invoke_in_other_type_scope[T](ignored: T, callback: function(int64) returns int64) returns int64: + return callback(29) +function make_nested[T](prefix: T) returns function() returns function(int64) returns string: + return function() returns function(int64) returns string: return make_label[T](prefix) +function reflected_name[T]() returns string: + return type.name[T]() +function make_name[T]() returns function() returns string: + return function() returns string: return reflected_name[T]() +function main() returns nothing: + function(int64) returns string numbered = comptime make_label[int64](7) + function(int64) returns string named = comptime make_label[string]("name") + function(int64) returns string aliased = comptime make_label[Label]("alias") + println(numbered(2)) + println(named(3)) + println(aliased(4)) + function(int64) returns int64 numeric_echo = comptime make_echo[int64]() + function(string) returns string text_echo = comptime make_echo[string]() + println(numeric_echo(17)) + println(text_echo("kept")) + println(invoke_in_other_type_scope[string]("caller", numeric_echo)) + function() returns function(int64) returns string nested_factory = comptime make_nested[string]("nested") + function(int64) returns string nested = nested_factory() + println(nested(5)) + function(int64) returns string baked_nested = comptime make_nested[int64](23)() + println(baked_nested(6)) + function() returns string numeric_name = comptime make_name[int64]() + function() returns string alias_name = comptime make_name[Label]() + println(numeric_name()) + println(alias_name()) + return nothing diff --git a/tests/native/comptime_machine_reuse.jett b/tests/native/comptime_machine_reuse.jett new file mode 100644 index 00000000..36723ec6 --- /dev/null +++ b/tests/native/comptime_machine_reuse.jett @@ -0,0 +1,38 @@ +namespace app +machine Session: + states: + active(count: int64) + transitions: + active to active +function make() returns Session: + return Session(active, 7) +struct Holder: + session: Session +function builder() returns TypeConstruction: + mutable TypeConstruction value = type.construct_start[Holder]() + for field in type.fields[Holder](): + value = type.construct_put[Holder, Session](value, view field, make()) handle error: + return type.construct_start[Holder]() + return value +function describe(view value: Session) returns string: + if value at active: + return "active:{value.count}" + return "unexpected" +interface Named: + function name(view self: Named) returns string +implement Named for Session: + function name(view self: Session) returns string: + return describe(view self) +function main() returns nothing: + for index in range(0, 3): + Session value = comptime make() + println(describe(view value)) + Holder holder = comptime Holder(session: make()) + println(describe(view holder.session)) + Named erased = comptime make() + println(Named.name(view erased)) + TypeConstruction stored = comptime builder() + Holder built = type.construct_finish[Holder](stored) handle error: + return nothing + println(describe(view built.session)) + return nothing diff --git a/tests/native/comptime_namespace_aliases.jett b/tests/native/comptime_namespace_aliases.jett new file mode 100644 index 00000000..30c0e0e1 --- /dev/null +++ b/tests/native/comptime_namespace_aliases.jett @@ -0,0 +1,30 @@ +namespace alpha +export function increment(value: int64) returns int64: + return value + 1 +export function choose() returns function(int64) returns int64: + return increment +namespace beta +export function increment(value: int64) returns int64: + return value + 100 +namespace app +verify scoped_alias_bake: + use alpha as selected + function(int64) returns int64 callback = comptime selected.increment + assert callback(3) == 4 +property scoped_alias_trials: + given input: int64 + use beta as selected + function(int64) returns int64 callback = comptime selected.increment + assert callback(input) == input + 100 +function main() returns nothing: + use alpha as selected + function(int64) returns int64 first = comptime selected.increment + function(int64) returns int64 chosen = comptime selected.choose() + println(first(5), chosen(5)) + if true: + use beta as selected + function(int64) returns int64 shadowed = comptime selected.increment + println(shadowed(5)) + function(int64) returns int64 restored = comptime selected.increment + println(restored(5)) + return nothing diff --git a/tests/native/comptime_reflected_callbacks.jett b/tests/native/comptime_reflected_callbacks.jett new file mode 100644 index 00000000..99cb00ac --- /dev/null +++ b/tests/native/comptime_reflected_callbacks.jett @@ -0,0 +1,36 @@ +struct Record: + number: int64 + text: string + repeated: int64 +function plain() returns function() returns string: + return function() returns string: return "plain" +function make() returns list[function() returns string]: + mutable list[function() returns string] callbacks = list.new[function() returns string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + string label = field.name + function() returns string unrelated = plain() + callbacks = list.append[function() returns string](callbacks, unrelated) + callbacks = list.append[function() returns string](callbacks, function() returns string: return "{label}:{type.name[Field]()}") + return callbacks +function make_nested[T]() returns list[function() returns function() returns string]: + mutable list[function() returns function() returns string] callbacks = list.new[function() returns function() returns string]() + for field in type.fields[T](): + comptime type Outer = field.type_info: + comptime type Inner = type.info[bool](): + callbacks = list.append[function() returns function() returns string](callbacks, function() returns function() returns string: + return function() returns string: return "{type.name[Outer]()}:{type.name[Inner]()}" + ) + return callbacks +function print_callbacks(callbacks: list[function() returns string]) returns nothing: + for callback in callbacks: + println(callback()) + return nothing +function main() returns nothing: + print_callbacks(make()) + print_callbacks(comptime make()) + list[function() returns function() returns string] nested = comptime make_nested[Record]() + for factory in nested: + function() returns string callback = factory() + println(callback()) + return nothing diff --git a/tests/native/comptime_reflection_callbacks.jett b/tests/native/comptime_reflection_callbacks.jett new file mode 100644 index 00000000..1c5eb20e --- /dev/null +++ b/tests/native/comptime_reflection_callbacks.jett @@ -0,0 +1,35 @@ +function kind_label(kind: TypeKind) returns string: + if kind == TypeKind.list_type: + return "list" + return "alias" +function type_label[T]() returns string: + return type.name[T]() +function make[T](kind: TypeKind) returns function() returns string: + string label = kind_label(kind) + return function() returns string: return "{label}:{type_label[T]()}" +function primitive_label(primitive: TypePrimitive) returns string: + if primitive == TypePrimitive.string_type: + return "text" + return "integer" +function make_primitive[T](primitive: TypePrimitive) returns function() returns string: + string label = primitive_label(primitive) + return function() returns string: return "{label}:{type_label[T]()}" +function make_info[T](view info: TypeInfo) returns function() returns string: + string label = info.type_name + return function() returns string: return "{label}:{type_label[T]()}" +function main() returns nothing: + function() returns string listed = comptime make[int64](TypeKind.list_type) + function() returns string aliased = comptime make[int64](TypeKind.alias_type) + println(listed()) + println(aliased()) + function() returns string textual = comptime make_primitive[int64](TypePrimitive.string_type) + function() returns string integer = comptime make_primitive[int64](TypePrimitive.int64_type) + function() returns string int_info = comptime make_info[int64](view type.info[int64]()) + function() returns string string_info = comptime make_info[int64](view type.info[string]()) + function() returns string list_info = comptime make_info[int64](view type.info[list[int64]]()) + println(textual()) + println(integer()) + println(int_info()) + println(string_info()) + println(list_info()) + return nothing diff --git a/tests/native/contextual_comptime_values.jett b/tests/native/contextual_comptime_values.jett new file mode 100644 index 00000000..5f8efea3 --- /dev/null +++ b/tests/native/contextual_comptime_values.jett @@ -0,0 +1,74 @@ +namespace app +type Label = string +struct T: + count: int64 +function label[T]() returns string: + return ((comptime "{type.name[T]()}:{type.kind[T]()}")) +function forward[T]() returns string: + return comptime label[T]() +function callback[T]() returns function() returns string: + return function() returns string: return comptime type.name[T]() +function nested[T]() returns string: + comptime type Outer = type.info[list[T]](): + comptime type Inner = type.arg[Outer](0): + return (comptime type.name[Inner]()) +function scoped_callback[T]() returns function() returns string: + comptime type Bound = type.info[T](): + return function() returns string: return comptime type.name[Bound]() +function shape[T]() returns string: + if type.kind_tag[T]() == TypeKind.list_type: + comptime type Element = type.arg[T](0): + return comptime type.name[Element]() + return comptime type.name[T]() +function guarded[T](flag: bool) returns string: + if flag: + return comptime type.name[T]() + return "off" +function global_label() returns string: + return comptime type.name[T]() +function invoke[T]() returns string: + return global_label() +function empty[T]() returns list[T]: + return comptime list.new[T]() +struct Record: + count: int8 + label: Label + text: string +function fields() returns nothing: + for field in type.fields[Record](): + comptime type Field = field.type_info: + println(comptime type.name[Field]()) + comptime type Wrapped = type.info[list[Field]](): + println(comptime type.name[Wrapped]()) + return nothing +function main() returns nothing: + println(label[int64]()) + println(label[string]()) + println(label[Label]()) + println(nested[int8]()) + println(nested[Label]()) + function() returns string small = callback[int8]() + function() returns string text = callback[string]() + function() returns string alias = comptime callback[Label]() + println(small()) + println(text()) + println(alias()) + println(list.length[int8](empty[int8]())) + println(list.length[string](empty[string]())) + fields() + function() returns string scoped = scoped_callback[int8]() + function() returns string scoped_baked = comptime scoped_callback[Label]() + println(scoped()) + println(scoped_baked()) + println(shape[list[int8]]()) + println(shape[string]()) + println(guarded[int8](true)) + println(guarded[string](false)) + println(invoke[int64]()) + println(invoke[string]()) + println(global_label()) + function() returns string direct = comptime function() returns string: return comptime type.name[int16]() + println(direct()) + println(forward[string]()) + println(forward[Label]()) + return nothing diff --git a/tests/native/contextual_generic_empty_list.jett b/tests/native/contextual_generic_empty_list.jett new file mode 100644 index 00000000..5cb8646d --- /dev/null +++ b/tests/native/contextual_generic_empty_list.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + list[list[int64]] chunks = list.chunk(list(), 3) + list[list[int64]] filled = list.append[list[int64]](chunks, list(7, 8)) + println(list.length(filled)) diff --git a/tests/native/contextual_secret_constructors.jett b/tests/native/contextual_secret_constructors.jett new file mode 100644 index 00000000..eee53dbf --- /dev/null +++ b/tests/native/contextual_secret_constructors.jett @@ -0,0 +1,194 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +struct Counter: + value: int8 +implement Named for Counter: + function name(view self: Counter) returns string: + return "record:{self.value}" +type Selected = Named where true +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +type Tiny = int8 where value != 0 +function selected(value: int8) returns Selected: + Named base = value + Selected chosen = base handle error: + return selected(value) + return chosen +function increment(value: int8) returns int8: + return value + 1 +function item(view values: list[int8], index: int64) returns int8: + return list.get[int8](view values, index) handle: + default 0 +function map_item(view values: map[int8, int8], key: int8) returns int8: + return map.get[int8, int8](clone values, key) handle: + default 0 +function describe_optional(view candidate: optional[int8]) returns string: + int8 found = clone candidate handle: + return "none" + return "some:{found}" +function describe_outcome(view outcome: result[int8, int8]) returns string: + int8 found = clone outcome handle error: + return "fail:{error}" + return "ok:{found}" +function names(view values: list[Named]) returns string: + mutable string text = "" + for value in view values: + text = "{text}{Named.name(view value)};" + return text +function optional_name(view candidate: optional[Named]) returns string: + Named found = clone candidate handle: + return "none" + return "some:{Named.name(view found)}" +function outcome_name(view outcome: result[Named, Named]) returns string: + Named found = clone outcome handle error: + return "fail:{Named.name(view error)}" + return "ok:{Named.name(view found)}" +function map_name(view values: map[string, Named], key: string) returns string: + Named found = map.get[string, Named](clone values, key) handle: + return "none" + return Named.name(view found) +function list_report() returns string: + secret[list[int8]] hidden = list(-128, 127, -1) + secret[list[int8]] empty = list() + list[int8] values = declassify clone hidden + list[int8] absent = declassify clone empty + return "list={list.length(view values)}:{item(view values, 0)}:{item(view values, 1)}:{item(view values, 2)};empty={list.length(view absent)}" +function map_report() returns string: + secret[map[int8, int8]] hidden = map(-128: 127, 7: -8) + secret[map[int8, int8]] empty = map() + map[int8, int8] values = declassify clone hidden + map[int8, int8] absent = declassify clone empty + return "map={map.length(view values)}:{map_item(view values, -128)}:{map_item(view values, 7)};empty={map.length(view absent)}" +function sum_report() returns string: + secret[optional[int8]] present = some(-128) + secret[optional[int8]] absent = none + secret[result[int8, int8]] success = ok(127) + secret[result[int8, int8]] failure = fail(-128) + optional[int8] yes = declassify clone present + optional[int8] no = declassify clone absent + result[int8, int8] good = declassify clone success + result[int8, int8] bad = declassify clone failure + return "{describe_optional(view yes)}:{describe_optional(view no)}:{describe_outcome(view good)}:{describe_outcome(view bad)}" +function nested_report() returns string: + int8 first = 7 + int8 last = -8 + secret[secret[list[int8]]] twice = list(first, last) + secret[list[int8]] once = declassify clone twice + list[int8] values = declassify clone once + secret[list[secret[int8]]] hidden_children = list(7, -8) + list[secret[int8]] children = declassify clone hidden_children + mutable string text = "" + for child in view children: + int8 public = declassify child + text = "{text}{public}," + return "twice={list.length(view values)}:{item(view values, 0)}:{item(view values, 1)};children={text}" +function owner_report() returns string: + secret[list[Named]] hidden = list(selected(7), Counter(value: 8)) + secret[optional[Named]] maybe = some(selected(9)) + secret[result[Named, Named]] success = ok(Counter(value: 10)) + secret[result[Named, Named]] failure = fail(selected(11)) + secret[map[string, Named]] hidden_map = map("selected": selected(12), "record": Counter(value: 13)) + list[Named] values = declassify clone hidden + optional[Named] candidate = declassify clone maybe + result[Named, Named] good = declassify clone success + result[Named, Named] bad = declassify clone failure + map[string, Named] mapping = declassify clone hidden_map + return "{names(view values)}|{optional_name(view candidate)}|{outcome_name(view good)}|{outcome_name(view bad)}|{map_name(view mapping, "selected")}:{map_name(view mapping, "record")}" +function child_report() returns string: + int8 first_seed = 7 + int8 last_seed = -8 + Tiny first = first_seed handle error: + return error + Tiny last = last_seed handle error: + return error + secret[list[Tiny]] hidden = list(first, last) + list[Tiny] values = declassify clone hidden + mutable string text = "" + for value in view values: + int8 public = coarsen value + text = "{text}{public}," + int8 offset = 3 + function(int8) returns int8 captured = function(value: int8) returns int8: return value + offset + secret[list[function(int8) returns int8]] callbacks = list(increment, captured) + list[function(int8) returns int8] public_callbacks = declassify clone callbacks + function(int8) returns int8 named = list.get[function(int8) returns int8](view public_callbacks, 0) handle: + return "missing named" + function(int8) returns int8 closure = list.get[function(int8) returns int8](view public_callbacks, 1) handle: + return "missing closure" + return "refined={text};callbacks={named(127)}:{closure(4)}" +function hidden_pair[T](first: T, last: T) returns secret[list[T]]: + return list(first, last) +function hidden_empty[T]() returns secret[list[T]]: + return list() +function hidden_some[T](value: T) returns secret[optional[T]]: + return some(value) +function hidden_failure[T, E](problem: E) returns secret[result[T, E]]: + return fail(problem) +function context_report() returns string: + secret[list[int8]] hidden = hidden_pair[int8](-8, 7) + secret[list[int8]] empty = hidden_empty[int8]() + secret[optional[int8]] maybe = hidden_some[int8](-128) + secret[result[int8, int8]] failure = hidden_failure[int8, int8](-8) + function() returns secret[map[int8, int8]] mapping = function() returns secret[map[int8, int8]]: return map(7: -8) + function(int8) returns secret[optional[int8]] wrap = function(value: int8) returns secret[optional[int8]]: return some(value) + list[int8] values = declassify clone hidden + list[int8] absent = declassify clone empty + optional[int8] candidate = declassify clone maybe + result[int8, int8] outcome = declassify clone failure + map[int8, int8] inline_map = declassify mapping() + optional[int8] inline_some = declassify wrap(127) + return "list={list.length(view values)}:{item(view values, 0)}:{item(view values, 1)};empty={list.length(view absent)};{describe_optional(view candidate)};{describe_outcome(view outcome)};map={map_item(view inline_map, 7)};inline={describe_optional(view inline_some)}" +function baked_report() returns string: + secret[list[int8]] hidden = comptime list(-128, 127) + secret[optional[int8]] maybe = comptime hidden_some[int8](-8) + secret[list[Named]] hidden_owners = comptime list(selected(7), Counter(value: 8)) + list[int8] values = declassify clone hidden + optional[int8] candidate = declassify clone maybe + list[Named] owners = declassify clone hidden_owners + return "list={list.length(view values)}:{item(view values, 0)}:{item(view values, 1)};{describe_optional(view candidate)};owners={names(view owners)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "lists:{list_report()}\n") + Stdout.write(view stdout, "maps:{map_report()}\n") + Stdout.write(view stdout, "sums:{sum_report()}\n") + Stdout.write(view stdout, "nested:{nested_report()}\n") + Stdout.write(view stdout, "owners:{owner_report()}\n") + Stdout.write(view stdout, "children:{child_report()}\n") + Stdout.write(view stdout, "contexts:{context_report()}\n") + Stdout.write(view stdout, "baked:{baked_report()}\n") + secret[list[int8]] observed = list(7, -8) + trace observed + list[int8] visible = declassify clone observed + Stdout.write(view stdout, "observed:{list.length(view visible)}:{item(view visible, 0)}:{item(view visible, 1)}\n") +verify contextual_secret_values: + assert list_report() == "list=3:-128:127:-1;empty=0" + assert map_report() == "map=2:127:-8;empty=0" + assert sum_report() == "some:-128:none:ok:127:fail:-128" + assert nested_report() == "twice=2:7:-8;children=7,-8," + assert owner_report() == "selected:small:7;record:8;|some:selected:small:9|ok:record:10|fail:selected:small:11|selected:small:12:record:13" + assert child_report() == "refined=7,-8,;callbacks=-128:7" + assert context_report() == "list=2:-8:7;empty=0;some:-128;fail:-8;map=-8;inline=some:127" + assert baked_report() == "list=2:-128:127;some:-8;owners=selected:small:7;record:8;" +property secret_constructor_trials: + given seed: int8 + secret[list[int8]] hidden = hidden_pair[int8](seed, seed + 1) + list[int8] values = declassify clone hidden + assert item(view values, 0) == seed + assert item(view values, 1) == seed + 1 + secret[map[int8, int8]] hidden_map = map(1: seed) + map[int8, int8] mapping = declassify clone hidden_map + assert map_item(view mapping, 1) == seed + secret[optional[int8]] present = some(seed) + int8 found = declassify clone present handle: + default 0 + assert found == seed + secret[result[int8, int8]] failure = fail(seed) + int8 recovered = declassify clone failure handle error: + default error + assert recovered == seed + assert child_report() == "refined=7,-8,;callbacks=-128:7" diff --git a/tests/native/crypto.jett b/tests/native/crypto.jett new file mode 100644 index 00000000..c9ae13f8 --- /dev/null +++ b/tests/native/crypto.jett @@ -0,0 +1,20 @@ +namespace app +function main() returns nothing: + println(crypto.sha256("abc"), crypto.sha512("abc"), crypto.md5("abc")) + println(crypto.sha256(""), crypto.sha512("Jett 🚀"), crypto.md5("")) + secret[bytes] key = bytes.from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") handle error: + return nothing + bytes message = bytes.from_string("Hi There") + secret[bytes] tag = crypto.hmac_sha256(view key, view message) + secret[bytes] expected = bytes.from_hex("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") handle error: + return nothing + println(secret.compare(clone tag, expected), secret.redact(clone tag)) + bytes public_tag = declassify tag + println(bytes.to_hex(view public_tag)) + secret[bytes] long_key = bytes.from_hex(string.repeat("aa", 131)) handle error: + return nothing + bytes long_message = bytes.from_string("Test Using Larger Than Block-Size Key - Hash Key First") + secret[bytes] long_tag = crypto.hmac_sha256(view long_key, view long_message) + secret[bytes] long_expected = bytes.from_hex("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54") handle error: + return nothing + println(secret.compare(long_tag, long_expected)) diff --git a/tests/native/csv.jett b/tests/native/csv.jett new file mode 100644 index 00000000..dc9174a3 --- /dev/null +++ b/tests/native/csv.jett @@ -0,0 +1,20 @@ +namespace app +function parsed(value: string) returns string: + list[list[string]] rows = csv.parse(value) handle error: + return error + return csv.stringify(rows) +function header_name(value: string) returns string: + list[map[string, string]] rows = csv.parse_with_header(value) handle error: + return error + map[string, string] first = list.get[map[string, string]](rows, 0) handle: + default map("missing": "row") + optional[string] name = map.get(first, "name") + return name handle: + default "missing name" +function main() returns nothing: + println(parsed("a,b\r\n1,\"two, words\"\r\n")) + println(parsed("\"say \"\"hello\"\"\",x")) + println(parsed("a\rb"), parsed("\"unterminated"), parsed("a\"b")) + println(header_name("name,age\nalice,30")) + println(header_name("name,name\nalice,smith")) + println(header_name("name,age\nalice")) diff --git a/tests/native/debug_actor_values.jett b/tests/native/debug_actor_values.jett new file mode 100644 index 00000000..9d046410 --- /dev/null +++ b/tests/native/debug_actor_values.jett @@ -0,0 +1,17 @@ +namespace test +actor Holder(seed: string): + mutable string label = "{seed}!" + receive noop: + return nothing +function main() returns nothing: + Holder first = spawn Holder(seed: "alpha") + Holder second = spawn Holder(seed: "beta") + Holder third = spawn Holder(seed: "gamma") + list[Holder] active = list(third) + trace first + trace second + trace active + breakpoint true + send first.noop + send second.noop + return nothing diff --git a/tests/native/debug_aggregates.jett b/tests/native/debug_aggregates.jett new file mode 100644 index 00000000..6ca7e164 --- /dev/null +++ b/tests/native/debug_aggregates.jett @@ -0,0 +1,75 @@ +namespace test +struct Payload: + title: string + numbers: list[int16] + maybe: optional[string] + outcome: result[int64, string] +enum Message: + empty + text(value: string) + nested(inner: Message) +machine Session: + states: + guest + active(user: string) + transitions: + guest to active +enum Protocol: + tcp = 6 + udp = 17 +bitfield network Packet: + version: 4 bits + flags: 4 bits + protocol: 8 bits as Protocol + payload: list[uint8] +bitfield Wide: + sequence: 64 bits +struct PacketBox: + packet: Packet +function make_packet() returns Packet: + mutable list[uint8] body = list.new[uint8]() + body = list.append[uint8](body, 7) + body = list.append[uint8](body, 255) + return Packet(version: 4, flags: 0, protocol: Protocol.tcp, payload: body) +function roundtrip_packet(view packet: Packet) returns Packet: + bytes encoded = Packet.to_bytes(clone packet) + Packet decoded = Packet.from_bytes(encoded) handle error: + return clone packet + return decoded +function trace_bitfields() returns nothing: + Packet packet = make_packet() + Packet decoded = roundtrip_packet(view packet) + Wide wide = Wide(sequence: 18446744073709551615) + PacketBox wrapped = PacketBox(packet: clone packet) + trace packet + trace decoded + trace wide + trace wrapped + breakpoint true + return nothing +function main() returns nothing: + trace_bitfields() + list[optional[int64]] choices = list(some(3), none, some(-2)) + mutable set[string] tags = set.new[string]() + tags = set.add[string](tags, "alpha") + tags = set.add[string](tags, "beta") + map[string, list[int64]] groups = map("one": list(1, 2), "other": list(0)) + list[int64] empty = list.new[int64]() + Payload payload = Payload(title: "Ada", numbers: list(2, -3), maybe: some("ready"), outcome: fail("retry")) + Message message = Message.nested(Message.text("hello")) + Message unit = Message.empty + Session session = Session(active, "Lin") + Session guest = Session(guest) + result[int64, string] success = ok(42) + trace choices + trace tags + trace groups + trace empty + trace payload + trace message + trace unit + trace session + trace guest + trace success + breakpoint true + return nothing diff --git a/tests/native/debug_deep_values.jett b/tests/native/debug_deep_values.jett new file mode 100644 index 00000000..52e71307 --- /dev/null +++ b/tests/native/debug_deep_values.jett @@ -0,0 +1,34 @@ +namespace app +interface Named: + function name(view self: Named) returns string +enum Chain: + end + next(value: Chain) +implement Named for Chain: + function name(view self: Chain) returns string: + return "chain" +enum Hidden: + token(value: secret[string]) + next(value: Hidden) +function secret_values() returns nothing: + mutable Hidden hidden = Hidden.token("deep-secret-token") + mutable int64 index = 0 + while index < 160: + hidden = Hidden.next(run hidden) + index = index + 1 + trace hidden + breakpoint +function main() returns nothing: + mutable Chain chain = Chain.end + mutable int64 index = 0 + while index < 160: + chain = Chain.next(run chain) + index = index + 1 + trace chain + println(view chain) + list[Chain] repeated = list(clone chain, clone chain) + trace repeated + Named erased = chain + trace erased + println(view erased) + secret_values() diff --git a/tests/native/debug_functions.jett b/tests/native/debug_functions.jett new file mode 100644 index 00000000..8d16814e --- /dev/null +++ b/tests/native/debug_functions.jett @@ -0,0 +1,105 @@ +namespace callback_library +export function increment(value: int64) returns int64: + return value + 1 +namespace test +struct CallbackHolder: + callback: function(int64) returns int64 +enum CallbackTree: + empty + leaf(callback: function(int64) returns int64) + branch(left: CallbackTree, right: CallbackTree) +machine CallbackState: + states: + idle + active(callback: function(int64) returns int64) + transitions: + idle to active +function double(value: int64) returns int64: + return value * 2 +function make_offset(offset: int64) returns function(int64) returns int64: + return function(input: int64) returns int64: return input + offset +function make_label(prefix: string) returns function(int64) returns string: + return function(number: int64) returns string: return "{prefix}:{number}" +function make_constant[T](value: T) returns function() returns T: + return function() returns T: return value +function make_replacer[T](value: T) returns function(T) returns T: + return function(replacement: T) returns T: return value +function count(view items: list[int64]) returns int64: + return list.length[int64](view items) +function uninvoked(value: int64) returns int64: + println("unexpected callback invocation") + return value +verify function_debug_values: + function(int64) returns int64 verify_named = double + function(int64) returns int64 verify_captured = comptime make_offset(11) + trace verify_named + trace verify_captured + assert verify_named(2) == 4 + assert verify_captured(2) == 13 +property function_debug_trials: + given input: int64 + function(int64) returns int64 property_callback = double + trace property_callback + assert property_callback(input) == input * 2 +function main() returns nothing: + use callback_library as callbacks + function(int64) returns int64 named = callbacks.increment + function(int64) returns int64 inline = function(argument: int64) returns int64: return argument + 3 + function(int64) returns int64 captured = make_offset(7) + function(int64) returns string label = make_label("DO_NOT_RENDER_CAPTURE") + function() returns int64 zero = function() returns int64: return 5 + function(int64, int64) returns int64 pair = function(left: int64, right: int64) returns int64: return left + right + function(view list[int64]) returns int64 observer = count + function() returns int64 generic = make_constant[int64](9) + function(int64) returns int64 generic_int = make_replacer[int64](12) + function(string) returns string generic_string = make_replacer[string]("kept") + function(int64) returns int64 baked_named = comptime double + function(int64) returns int64 baked_inline = comptime function(source: int64) returns int64: return source + 4 + function(int64) returns int64 baked_captured = comptime make_offset(8) + function(int64) returns int64 untouched = uninvoked + function(int64) returns int64 copied = clone captured + CallbackHolder holder = CallbackHolder(callback: clone captured) + list[function(int64) returns int64] many = list(clone named, clone inline, clone captured) + list[list[function(int64) returns int64]] nested = list(clone many) + map[string, function(int64) returns int64] indexed = map("named": clone named, "captured": clone captured) + optional[function(int64) returns int64] present = some(clone captured) + optional[function(int64) returns int64] absent = none + result[function(int64) returns int64, string] success = ok(clone named) + result[int64, function(int64) returns int64] failure = fail(clone inline) + CallbackTree tree = CallbackTree.branch(CallbackTree.leaf(clone captured), CallbackTree.empty) + CallbackState state = CallbackState(active, clone inline) + trace named + trace named + trace inline + trace captured + trace label + trace zero + trace pair + trace observer + trace generic + trace generic_int + trace generic_string + trace baked_named + trace baked_inline + trace baked_captured + trace untouched + trace copied + trace holder + trace many + trace nested + trace indexed + trace present + trace absent + trace success + trace failure + trace tree + trace state + breakpoint false + breakpoint true + function(int64) returns int64 selected = list.get[function(int64) returns int64](view many, 2) handle: + default named + println(named(2), inline(2), captured(2), zero(), pair(2, 3), generic()) + println(observer(list(1, 2, 3)), copied(2), holder.callback(2), selected(2)) + println(baked_named(2), baked_inline(2), baked_captured(2)) + println(generic_int(0), generic_string("unused")) + return nothing diff --git a/tests/native/debug_functions_failure.jett b/tests/native/debug_functions_failure.jett new file mode 100644 index 00000000..f7e472e6 --- /dev/null +++ b/tests/native/debug_functions_failure.jett @@ -0,0 +1,16 @@ +namespace test +function increment(value: int64) returns int64: + return value + 1 +function make_label(prefix: string) returns function(int64) returns string: + return function(number: int64) returns string: return "{prefix}:{number}" +function main() returns nothing: + function(int64) returns int64 named = increment + function(int64) returns string captured = make_label("hidden") + list[function(int64) returns string] callbacks = list(clone captured) + trace named + trace captured + trace callbacks + breakpoint true + list[int64] invalid = list.remove_at[int64](list(1), 5) + println(list.length[int64](view invalid)) + return nothing diff --git a/tests/native/debug_primitives.jett b/tests/native/debug_primitives.jett new file mode 100644 index 00000000..7acd0983 --- /dev/null +++ b/tests/native/debug_primitives.jett @@ -0,0 +1,36 @@ +namespace test +function main() returns nothing: + int8 tiny = -8 + int16 short = -32768 + int32 medium = -2147483648 + int64 large = -1234567890123 + uint8 byte = 255 + uint16 count = 65535 + uint32 wide = 4294967295 + uint64 maximum = 18446744073709551615 + float32 fraction = 1.25 + float32 rounded = 0.1 + float64 precise = -2.5 + bool ready = true + string label = "café" + nothing empty = nothing + bytes raw = bytes.from_hex("00ff") handle error: + return nothing + trace tiny + trace short + trace medium + trace large + trace byte + trace count + trace wide + trace maximum + trace fraction + trace rounded + trace precise + trace ready + trace label + trace empty + trace raw + breakpoint ready + breakpoint false + return nothing diff --git a/tests/native/debug_print_values.jett b/tests/native/debug_print_values.jett new file mode 100644 index 00000000..e0e2f643 --- /dev/null +++ b/tests/native/debug_print_values.jett @@ -0,0 +1,39 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +implement Named for Boxed[int8]: + function name(view self: Boxed[int8]) returns string: + return "narrow" +function inspect(view value: Named) returns nothing: + println(view value) + println(Named.name(view value)) +function argument(value: int64) returns list[int64]: + print("argument:", value, ";") + return list(value) +function handled(view values: list[result[int64, string]]) returns nothing: + optional[int64] missing = none + println(view values, missing handle: + default 42 + ) + println(view values) +function projected() returns nothing: + optional[int64] missing = none + println(view Boxed[list[int64]](value: list(3, 4)).value, missing handle: + default 9 + ) +function main(stdout: Stdout) returns nothing: + println(none, list(), map(), ok(3), fail("no"), some(none)) + println(bytes.from_string("hi"), set.new[string]()) + list[result[int64, string]] values = list(ok(7), fail("retry")) + println(view values, view values) + list[result[int64, string]] pending = run run clone values + println(view pending) + println(view stdout) + inspect(Boxed[int8](value: 5)) + println(argument(1), argument(2)) + handled(view values) + projected() + print(view values) + println() diff --git a/tests/native/debug_secret_values.jett b/tests/native/debug_secret_values.jett new file mode 100644 index 00000000..d8ada3d4 --- /dev/null +++ b/tests/native/debug_secret_values.jett @@ -0,0 +1,193 @@ +namespace app + +type HiddenText = secret[string] +type PublicCount = uint8 +struct Box[T]: + value: T +type HiddenPositive = secret[int64] where value > 0 +struct Record: + label: string + token: HiddenText + numbers: list[secret[int64]] +enum Payload: + empty + item(value: secret[string]) + branch(next: Payload) +machine Session: + states: + active(token: secret[string]) + closed + transitions: + active to closed +struct ActiveSession: + session: Session at active +struct Vault: + token: secret[int64] + +function scalar_values() returns nothing: + HiddenText token = "hidden-token" + secret[int64] pending = run run 4321 + secret[bytes] data = bytes.from_string("hidden-bytes") + secret[int64] input = 7654 + HiddenPositive positive = input handle error: + return nothing + trace token + trace pending + trace data + trace positive + breakpoint + println(secret.redact(token)) + return nothing + +function collection_values() returns nothing: + secret[string] token = "hidden-collection" + list[secret[int64]] numbers = list(run 4321, 7654) + map[string, secret[string]] entries = map("public-key": clone token) + optional[secret[string]] maybe = some(clone token) + result[secret[string], string] success = ok(clone token) + result[string, secret[string]] failure = fail(token) + trace numbers + trace entries + trace maybe + trace success + trace failure + list[string] public_markers = list(run "[redacted]") + trace public_markers + return nothing + +function aggregate_values() returns nothing: + Record record = Record(label: "public-label", token: "hidden-record", numbers: list(4321)) + Payload payload = Payload.branch(Payload.item("hidden-enum")) + Session session = Session(active, "hidden-machine") + trace record + trace payload + trace session + Box[HiddenText] boxed = Box[HiddenText](value: "hidden-box") + trace boxed + ActiveSession narrowed = ActiveSession(session: Session(active, "hidden-narrowed")) + trace narrowed + breakpoint + return nothing + +function builder_values() returns nothing: + TypeField field = list.get[TypeField](type.fields[Vault](), 0) handle: + return nothing + secret[int64] token = run 4321 + TypeConstruction builder = type.construct_start[Vault]() + TypeConstruction filled = type.construct_put[Vault, secret[int64]](builder, view field, token) handle error: + return nothing + trace filled + list[TypeConstruction] builders = list(clone filled) + trace builders + Vault built = type.construct_finish[Vault](filled) handle error: + return nothing + trace built + return nothing + +function debug_generic[T](value: T) returns nothing: + T local = value + trace local + breakpoint + return nothing + +function debug_alias_capture(value: PublicCount) returns function() returns nothing: + return function() returns nothing: + trace value + breakpoint + return nothing + +function debug_member_builders() returns nothing: + TypeVariant variant = list.get[TypeVariant](type.variants[Payload](), 1) handle: + return nothing + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + TypeConstruction builder = type.construct_variant_start[Payload](view variant) handle error: + return nothing + TypeConstruction filled = type.construct_put[Payload, secret[string]](builder, view field, "hidden-variant-builder") handle error: + return nothing + trace filled + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 0) handle: + return nothing + TypeField token = list.get[TypeField](state.fields, 0) handle: + return nothing + TypeConstruction session_builder = type.construct_machine_start[Session](view state) handle error: + return nothing + TypeConstruction active = type.construct_put[Session, secret[string]](session_builder, view token, "hidden-state-builder") handle error: + return nothing + trace active + return nothing + +function debug_inferred_bindings() returns nothing: + list[secret[string]] tokens = list("hidden-loop") + for item in clone tokens: + trace item + breakpoint + map[string, secret[string]] entries = map("public": "hidden-map-loop") + for key, token in clone entries: + trace key + trace token + Payload payload = Payload.item("hidden-match") + match clone payload: + item(token): + trace token + breakpoint + branch(next): + trace next + empty: + return nothing + result[int64, secret[string]] failed = fail("hidden-error") + int64 unused = clone failed handle error: + trace error + breakpoint + return nothing + return nothing + +function debug_generic_loop[T](values: list[T]) returns nothing: + for item in values: + trace item + return nothing + +function debug_generic_error[T](failed: result[int64, T]) returns nothing: + int64 unused = clone failed handle error: + trace error + breakpoint + return nothing + return nothing + +function error_identity[T](value: result[int64, T]) returns result[int64, T]: + return value + +function debug_generic_pipeline[T](failed: result[int64, T]) returns nothing: + int64 unused = clone failed into error_identity[T]() handle error: + trace error + return nothing + return nothing + +function debug_generic_join[T](failed: result[int64, T]) returns nothing: + result[int64, T] pending = run failed + int64 unused = join pending handle error: + trace error + breakpoint + return nothing + return nothing + +function main() returns nothing: + scalar_values() + collection_values() + aggregate_values() + builder_values() + debug_generic[HiddenText]("hidden-generic") + debug_generic[PublicCount](7) + function() returns nothing captured = debug_alias_capture(8) + captured() + debug_member_builders() + debug_inferred_bindings() + debug_generic_loop[secret[string]](list("hidden-generic-loop")) + debug_generic_loop[int64](list(9)) + debug_generic_error[secret[string]](fail("hidden-generic-error")) + debug_generic_error[string](fail("public-error")) + debug_generic_pipeline[secret[string]](fail("hidden-pipeline-error")) + debug_generic_pipeline[string](fail("public-pipeline-error")) + debug_generic_join[secret[string]](fail("hidden-join-error")) + debug_generic_join[string](fail("public-join-error")) + return nothing diff --git a/tests/native/debug_type_construction.jett b/tests/native/debug_type_construction.jett new file mode 100644 index 00000000..52a68343 --- /dev/null +++ b/tests/native/debug_type_construction.jett @@ -0,0 +1,97 @@ +namespace app +struct User: + id: string + age: int64 +enum Shape: + circle(radius: float64) + rect(width: float64, height: float64) +bitfield Header: + version: 4 bits +machine Session: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +function build_user() returns string: + mutable TypeConstruction builder = type.construct_start[User]() + trace builder + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view User(id: "Ada", age: 7), view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + trace builder + User built = type.construct_finish[User](builder) handle error: + return error + mutable TypeConstruction reversed = type.construct_start[User]() + TypeField age = list.get[TypeField](type.fields[User](), 1) handle: + return "missing age" + TypeField id = list.get[TypeField](type.fields[User](), 0) handle: + return "missing id" + reversed = type.construct_put[User, int64](reversed, view age, 9) handle error: + return error + reversed = type.construct_put[User, string](reversed, view id, "Lin") handle error: + return error + trace reversed + User reordered = type.construct_finish[User](reversed) handle error: + return error + if reordered.id != "Lin": + return "bad reordered user" + return "{built.id}:{built.age}" +function debug_bitfield() returns nothing: + mutable TypeConstruction bits = type.construct_start[Header]() + TypeField version = list.get[TypeField](type.fields[Header](), 0) handle: + return nothing + bits = type.construct_put[Header, int64](bits, view version, 4) handle error: + return nothing + trace bits + Header built_bits = type.construct_finish[Header](bits) handle error: + return nothing + println(built_bits.version) + return nothing +function debug_shape() returns nothing: + TypeVariant variant = list.get[TypeVariant](type.variants[Shape](), 1) handle: + return nothing + mutable TypeConstruction shape = type.construct_variant_start[Shape](view variant) handle error: + return nothing + trace shape + TypeField width_field = list.get[TypeField](variant.fields, 0) handle: + return nothing + shape = type.construct_put[Shape, float64](shape, view width_field, 3.0) handle error: + return nothing + trace shape + TypeField height_field = list.get[TypeField](variant.fields, 1) handle: + return nothing + shape = type.construct_put[Shape, float64](shape, view height_field, 4.0) handle error: + return nothing + Shape built_shape = type.construct_finish[Shape](shape) handle error: + return nothing + match built_shape: + rect(width, height): + println("{width}:{height}") + circle(radius): + return nothing + return nothing +function debug_machine() returns nothing: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return nothing + mutable TypeConstruction session = type.construct_machine_start[Session](view state) handle error: + return nothing + trace session + TypeField user_id = list.get[TypeField](state.fields, 0) handle: + return nothing + session = type.construct_put[Session, string](session, view user_id, "Ada") handle error: + return nothing + trace session + Session built_session = type.construct_finish[Session](session) handle error: + return nothing + TypeMachineState active = type.machine_state_value[Session](view built_session) + println(active.name) + return nothing +function main() returns nothing: + println(build_user()) + debug_bitfield() + debug_shape() + debug_machine() + return nothing diff --git a/tests/native/declassify_local_secret_reuse.jett b/tests/native/declassify_local_secret_reuse.jett new file mode 100644 index 00000000..4db7a0a1 --- /dev/null +++ b/tests/native/declassify_local_secret_reuse.jett @@ -0,0 +1,9 @@ +namespace app +function main() returns nothing: + secret[list[int64]] hidden = json.parse[secret[list[int64]]]("[5,7]") handle error: + println(error) + return nothing + list[int64] first = declassify hidden + list[int64] second = declassify hidden + println(list.length[int64](view first), list.length[int64](view second)) + return nothing diff --git a/tests/native/displayable_interpolation.jett b/tests/native/displayable_interpolation.jett new file mode 100644 index 00000000..fb366f16 --- /dev/null +++ b/tests/native/displayable_interpolation.jett @@ -0,0 +1,14 @@ +interface Displayable: + function display(view self: Displayable) returns string +struct User: + name: string + function display(view self: User) returns string: + return "inherent:{self.name}" +implement Displayable for User: + function display(view self: User) returns string: + return "user:{self.name}" +function main() returns nothing: + User ada = User(name: "Ada") + println("Hello {ada}!") + println("Result {User(name: "Grace")}") + return nothing diff --git a/tests/native/empty_collection_iteration.jett b/tests/native/empty_collection_iteration.jett new file mode 100644 index 00000000..7a978300 --- /dev/null +++ b/tests/native/empty_collection_iteration.jett @@ -0,0 +1,154 @@ +namespace app +function identity[T](value: T) returns T: + return value +function pass[T](values: list[T]) returns list[T]: + return values +function count[T](values: list[T]) returns int64: + mutable int64 total = 0 + for value in values: + total = total + 1 + return total +function dead_references() returns int64: + for value in list(): + int64 copied = identity(value) + function() returns int64 callback = function() returns int64: return value + int64 captured = callback() + trace value + trace copied + trace captured + breakpoint + return copied + captured + for value in comptime list(): + trace value + breakpoint + return 7 +function direct_empty_lists(view stdout: Stdout) returns nothing: + for value in list(): + Stdout.write(view stdout, "unexpected direct body\n") + for value in view list(): + Stdout.write(view stdout, "unexpected borrowed body\n") + for value in clone list(): + Stdout.write(view stdout, "unexpected cloned body\n") + for value in pass(list()): + Stdout.write(view stdout, "unexpected generic body\n") + Stdout.write(view stdout, "direct:done\n") +function source[T](values: list[T], view stdout: Stdout) returns list[T]: + Stdout.write(view stdout, "source\n") + return values +function argument(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "argument\n") + return 0 +function effectful_empty_lists(view stdout: Stdout) returns nothing: + for value in source(list(), view stdout): + Stdout.write(view stdout, "unexpected source body\n") + for value in list.take(list(), argument(view stdout)): + Stdout.write(view stdout, "unexpected take body\n") + Stdout.write(view stdout, "effects:done\n") +function nested_counts() returns string: + mutable int64 outer = 0 + mutable int64 inner_count = 0 + for inner in list(list(), list()): + outer = outer + 1 + for value in inner: + inner_count = inner_count + 1 + return "{outer}:{inner_count}" +function borrowed_nested_counts() returns string: + mutable int64 outer = 0 + mutable int64 inner_count = 0 + for inner in view list(list(), list()): + outer = outer + 1 + for value in view inner: + inner_count = inner_count + 1 + return "{outer}:{inner_count}" +function inferred_map_count() returns int64: + mutable int64 total = 0 + for key, value in map(): + trace key + trace value + total = total + 1 + for key, value in view map(): + trace key + trace value + total = total + 1 + return total +function contextual_counts() returns string: + list[int64] values = list() + map[string, int64] entries = map() + mutable int64 total = 0 + for value in view values: + total = total + 1 + for key, value in view entries: + total = total + 1 + int64 list_size = list.length[int64](view values) + int64 map_size = map.length[string, int64](view entries) + int64 copied_count = count[int64](clone values) + int64 owned_count = count[int64](values) + for key, value in entries: + total = total + 1 + return "{total}:{list_size}:{map_size}:{copied_count}:{owned_count}" +function nothing_callback(value: int64) returns nothing: + trace value + return nothing +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "number" +function named_callback(value: int64) returns Named: + trace value + return value +function mapped_empty_count() returns int64: + mutable int64 total = 0 + for value in list.map(list(), nothing_callback): + trace value + total = total + 1 + for value in list.map(list(), named_callback): + string label = Named.name(view value) + trace label + total = total + 1 + return total +function join_list[T](values: list[T]) returns list[T]: + list[T] once = join values handle error: return list() + list[T] ready = join once handle error: return list() + return ready +function joined_empty_count() returns int64: + mutable int64 total = 0 + for value in join_list(run run list()): + trace value + total = total + 1 + map[string, int64] entries = run run map() + map[string, int64] once = join entries handle error: return -1 + map[string, int64] ready = join once handle error: return -2 + for key, value in ready: + trace key + trace value + total = total + 1 + return total +function main(stdout: Stdout) returns nothing: + direct_empty_lists(view stdout) + effectful_empty_lists(view stdout) + int64 baked = comptime count(list()) + int64 baked_references = comptime dead_references() + Stdout.write(view stdout, "generic:{count(list())}:{baked}:live:{count(list(10, 20, 30))}\n") + Stdout.write(view stdout, "dead:{dead_references()}:{baked_references}\n") + Stdout.write(view stdout, "nested:{nested_counts()}:borrowed:{borrowed_nested_counts()}\n") + Stdout.write(view stdout, "maps:{inferred_map_count()}:context:{contextual_counts()}\n") + Stdout.write(view stdout, "mapped:{mapped_empty_count()}:joined:{joined_empty_count()}\n") +verify empty_iteration: + assert count(list()) == 0 + assert count(list(10, 20, 30)) == 3 + int64 baked = comptime count(list()) + assert baked == 0 + assert dead_references() == 7 + assert nested_counts() == "2:0" + assert borrowed_nested_counts() == "2:0" + assert inferred_map_count() == 0 + assert contextual_counts() == "0:0:0:0:0" + assert mapped_empty_count() == 0 + assert joined_empty_count() == 0 +property empty_iteration_trials: + given seed: int64 + assert seed + count(list()) == seed + assert dead_references() == 7 + assert nested_counts() == "2:0" + assert inferred_map_count() == 0 diff --git a/tests/native/empty_never_list.jett b/tests/native/empty_never_list.jett new file mode 100644 index 00000000..d02b5fec --- /dev/null +++ b/tests/native/empty_never_list.jett @@ -0,0 +1,3 @@ +namespace app +function main() returns nothing: + println(list.is_empty(list()), list.length(list())) diff --git a/tests/native/encoding.jett b/tests/native/encoding.jett new file mode 100644 index 00000000..c80b182b --- /dev/null +++ b/tests/native/encoding.jett @@ -0,0 +1,23 @@ +namespace app +function base64_decoded(value: string) returns string: + bytes raw = encoding.base64_decode(value) handle error: + return error + return bytes.to_hex(view raw) +function hex_decoded(value: string) returns string: + bytes raw = encoding.hex_decode(value) handle error: + return error + return bytes.to_hex(view raw) +function url_decoded(value: string) returns string: + return encoding.url_decode(value) handle error: + default error +function form_decoded(value: string) returns string: + return encoding.form_decode(value) handle error: + default error +function main() returns nothing: + bytes raw = bytes.from_hex("0080ff") handle error: + return + println(encoding.base64_encode(view raw), bytes.to_hex(view raw)) + println(base64_decoded("AID/"), base64_decoded("AB=="), base64_decoded("!"), base64_decoded("!===")) + println(hex_decoded("00aBFF"), hex_decoded("z"), hex_decoded("00zz")) + println(encoding.url_encode("a b+c"), url_decoded("a%20b%2bc"), url_decoded("%FF%GG"), url_decoded("%FF")) + println(encoding.form_encode("a b+c~"), form_decoded("a+b%2bc"), form_decoded("%GG")) diff --git a/tests/native/enum_aggregate_equality.jett b/tests/native/enum_aggregate_equality.jett new file mode 100644 index 00000000..78c9e86b --- /dev/null +++ b/tests/native/enum_aggregate_equality.jett @@ -0,0 +1,57 @@ +namespace test +bitfield Header: + version: 4 bits + length: 8 bits +machine Session: + states: + guest + active(user: string) + transitions: + guest to active +enum Bundle: + empty + numbers(values: list[int64]) + tags(values: set[string]) + mapping(values: map[string, optional[int64]]) + maybe(value: optional[list[int64]]) + outcome(value: result[list[int64], string]) + raw(value: bytes) + header(value: Header) + session(value: Session) + fractions(values: list[float64]) + nested(inner: Bundle) +function make_tags(first: string, second: string) returns set[string]: + mutable set[string] values = set.new[string]() + values = set.add[string](values, first) + values = set.add[string](values, second) + return values +function main() returns nothing: + Bundle first = Bundle.numbers(list(1, 2)) + Bundle same = Bundle.numbers(list(1, 2)) + Bundle changed = Bundle.numbers(list(2, 1)) + println(first == same, first != changed, first == Bundle.empty) + println(first == changed) + println(Bundle.tags(make_tags("a", "b")) == Bundle.tags(make_tags("a", "b"))) + println(Bundle.tags(make_tags("a", "b")) == Bundle.tags(make_tags("b", "a"))) + map[string, optional[int64]] entries = map("a": some(1), "b": none) + map[string, optional[int64]] matching = map("a": some(1), "b": none) + println(Bundle.mapping(entries) == Bundle.mapping(matching)) + println(Bundle.mapping(map("a": some(1))) == Bundle.mapping(map("a": some(2)))) + println(Bundle.maybe(some(list(3))) == Bundle.maybe(some(list(3)))) + println(Bundle.maybe(none) == Bundle.maybe(some(list(3)))) + println(Bundle.maybe(some(list(3))) == Bundle.maybe(some(list(4)))) + println(Bundle.outcome(ok(list(5))) == Bundle.outcome(ok(list(5)))) + println(Bundle.outcome(fail("bad")) != Bundle.outcome(ok(list(5)))) + println(Bundle.outcome(fail("bad")) == Bundle.outcome(fail("other"))) + println(Bundle.raw(bytes.from_string("Hi")) == Bundle.raw(bytes.from_string("Hi"))) + println(Bundle.raw(bytes.from_string("Hi")) == Bundle.raw(bytes.from_string("Ho"))) + println(Bundle.header(Header(version: 4, length: 12)) == Bundle.header(Header(version: 4, length: 12))) + println(Bundle.header(Header(version: 4, length: 12)) == Bundle.header(Header(version: 4, length: 13))) + println(Bundle.session(Session(active, "Lin")) == Bundle.session(Session(active, "Lin"))) + println(Bundle.session(Session(active, "Lin")) == Bundle.session(Session(active, "Ada"))) + float64 nan = 0.0 / 0.0 + println(Bundle.fractions(list(-0.0)) == Bundle.fractions(list(0.0))) + println(Bundle.fractions(list(nan)) == Bundle.fractions(list(nan))) + println(Bundle.nested(Bundle.numbers(list(7))) == Bundle.nested(Bundle.numbers(list(7)))) + println(Bundle.nested(Bundle.numbers(list(7))) == Bundle.nested(Bundle.numbers(list(8)))) + return nothing diff --git a/tests/native/enum_deep_equality.jett b/tests/native/enum_deep_equality.jett new file mode 100644 index 00000000..6c58d96f --- /dev/null +++ b/tests/native/enum_deep_equality.jett @@ -0,0 +1,23 @@ +namespace app +enum Chain: + leaf(value: float64) + next(value: Chain) +function chain(value: float64, depth: int64, pending: bool) returns Chain: + mutable Chain current = Chain.leaf(value) + mutable int64 index = 0 + while index < depth: + if pending: + current = Chain.next(run current) + else: + current = Chain.next(current) + index = index + 1 + return current +function main(stdout: Stdout) returns nothing: + Chain first = chain(0.0, 160, false) + Chain same = chain(-0.0, 160, false) + Chain changed = chain(1.0, 160, false) + Chain shorter = chain(0.0, 159, false) + Chain nan = chain(0.0 / 0.0, 160, false) + Chain pending = chain(0.0, 160, true) + Chain other = chain(0.0, 160, true) + Stdout.write(view stdout, "{first == same} {first != changed} {first == shorter} {nan == nan} {pending == other} {first == pending}\n") diff --git a/tests/native/enum_struct_equality.jett b/tests/native/enum_struct_equality.jett new file mode 100644 index 00000000..7edf8bb4 --- /dev/null +++ b/tests/native/enum_struct_equality.jett @@ -0,0 +1,120 @@ +namespace models +struct Ignored: + value: int64 +export struct User: + id: int64 + ignored: Ignored + callback: function(int64) returns int64 +implement Equatable for User: + function equals(view self: User, view other: User) returns bool: + return self.id == other.id +function identity(value: int64) returns int64: + return value +export function user(id: int64, ignored: int64) returns User: + return User(id: id, ignored: Ignored(value: ignored), callback: identity) +export struct Box[T]: + id: int64 + ignored: T +implement Equatable for Box[Ignored]: + function equals(view self: Box[Ignored], view other: Box[Ignored]) returns bool: + return self.id == other.id +export function box(id: int64, ignored: int64) returns Box[Ignored]: + return Box[Ignored](id: id, ignored: Ignored(value: ignored)) +export type Trusted = User where value.id >= 0 +export machine Session: + states: + guest + active(user: User) + transitions: + guest to active +export enum Item: + empty + user(value: User) + users(values: list[User]) + mapping(values: map[string, optional[User]]) + maybe(value: optional[User]) + outcome(value: result[User, User]) + pair(first: User, second: User) + boxed(value: Box[Ignored]) + trusted(value: Trusted) + session(value: Session) + nested(value: Item) +export type Selected = Item where true +export struct Wrapper: + item: Item +implement Equatable for Wrapper: + function equals(view self: Wrapper, view other: Wrapper) returns bool: + return self.item == other.item +export enum Outer: + wrapper(value: Wrapper) +export function compare[T](view left: T, view right: T) returns bool: + return left == right +export function baked() returns bool: + return Item.user(user(1, 2)) == Item.user(user(1, 3)) +namespace app +function basic() returns bool: + use models as selected + selected.Item left = selected.Item.user(selected.user(1, 2)) + selected.Item right = selected.Item.user(selected.user(1, 3)) + return left == right && !(left != right) && selected.compare[selected.Item](view left, view right) +function collections() returns bool: + use models as selected + selected.Item left = selected.Item.users(list(selected.user(1, 2), selected.user(3, 4))) + selected.Item right = selected.Item.users(list(selected.user(1, 8), selected.user(3, 9))) + selected.Item changed = selected.Item.users(list(selected.user(1, 2), selected.user(4, 4))) + return left == right && left != changed +function sums() returns bool: + use models as selected + selected.Item first = selected.Item.maybe(some(selected.user(1, 2))) + selected.Item same = selected.Item.maybe(some(selected.user(1, 3))) + selected.Item success = selected.Item.outcome(ok(selected.user(1, 4))) + selected.Item matching = selected.Item.outcome(ok(selected.user(1, 5))) + selected.Item failure = selected.Item.outcome(fail(selected.user(2, 6))) + selected.Item other_failure = selected.Item.outcome(fail(selected.user(2, 7))) + return first == same && success == matching && failure == other_failure && success != failure +function mappings() returns bool: + use models as selected + selected.Item first = selected.Item.mapping(map("key": some(selected.user(1, 2)))) + selected.Item same = selected.Item.mapping(map("key": some(selected.user(1, 3)))) + selected.Item changed = selected.Item.mapping(map("other": some(selected.user(1, 3)))) + return first == same && first != changed +function nested() returns bool: + use models as selected + selected.Outer first = selected.Outer.wrapper(selected.Wrapper(item: selected.Item.nested(selected.Item.user(selected.user(1, 2))))) + selected.Outer same = selected.Outer.wrapper(selected.Wrapper(item: selected.Item.nested(selected.Item.user(selected.user(1, 3))))) + return first == same +function deep() returns bool: + use models as selected + mutable selected.Item first = selected.Item.user(selected.user(1, 2)) + mutable selected.Item same = selected.Item.user(selected.user(1, 3)) + mutable int64 depth = 0 + while depth < 160: + first = selected.Item.nested(first) + same = selected.Item.nested(same) + depth = depth + 1 + return first == same && !(first != same) +function pending() returns bool: + use models as selected + selected.Item first = selected.Item.user(run selected.user(1, 2)) + selected.Item same = selected.Item.user(run selected.user(1, 3)) + selected.Item deeper = selected.Item.user(run run selected.user(1, 3)) + selected.Item plain = selected.Item.user(selected.user(1, 3)) + return first == same && first != deeper && first != plain +function wrappers() returns bool: + use models as selected + selected.Trusted first = selected.user(1, 2) handle error: return false + selected.Trusted same = selected.user(1, 3) handle error: return false + selected.Selected left = selected.Item.trusted(first) handle error: return false + selected.Selected right = selected.Item.trusted(same) handle error: return false + return left == right +function nominal() returns bool: + use models as selected + selected.Item first = selected.Item.boxed(selected.box(1, 2)) + selected.Item same = selected.Item.boxed(selected.box(1, 3)) + selected.Item session = selected.Item.session(selected.Session(active, selected.user(1, 2))) + selected.Item matching = selected.Item.session(selected.Session(active, selected.user(1, 3))) + return first == same && session == matching +function main(stdout: Stdout) returns nothing: + use models as selected + bool baked = comptime selected.baked() + Stdout.write(view stdout, "{basic()} {collections()} {sums()} {mappings()} {nested()} {deep()} {pending()} {wrappers()} {nominal()} {baked}\n") diff --git a/tests/native/enum_struct_equality_failure.jett b/tests/native/enum_struct_equality_failure.jett new file mode 100644 index 00000000..892cd18e --- /dev/null +++ b/tests/native/enum_struct_equality_failure.jett @@ -0,0 +1,29 @@ +namespace app +struct Fragile: + id: int64 + label: string +implement Equatable for Fragile: + function equals(view self: Fragile, view other: Fragile) returns bool: + if self.id == -1: + string rejected = string.repeat("ab", 9223372036854775807) + return string.char_count(rejected) == 0 + return self.id == other.id +enum Probe: + empty + pair(first: Fragile, second: Fragile) + users(values: list[Fragile]) +function value(id: int64) returns Fragile: + return Fragile(id: id, label: "owned") +function prefix_checks() returns bool: + Probe first = Probe.pair(value(1), value(-1)) + Probe different = Probe.pair(value(2), value(-1)) + Probe longer = Probe.users(list(value(-1), value(-1))) + Probe shorter = Probe.users(list(value(-1))) + return first != different && longer != shorter && first != Probe.empty +function operand(view stdout: Stdout, label: string) returns Probe: + Stdout.write(view stdout, label) + return Probe.pair(value(1), run value(-1)) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{prefix_checks()}\n") + bool failed = operand(view stdout, "left\n") == operand(view stdout, "right\n") + Stdout.write(view stdout, "{failed}") diff --git a/tests/native/enum_struct_payload.jett b/tests/native/enum_struct_payload.jett new file mode 100644 index 00000000..d05cead6 --- /dev/null +++ b/tests/native/enum_struct_payload.jett @@ -0,0 +1,23 @@ +namespace app +struct Packet: + label: string + values: list[int64] +enum Message: + empty + packet(value: Packet) +function describe(view message: Message) returns string: + match message: + empty: + return "empty" + packet(value): + return "{value.label}:{list.length[int64](view value.values)}" +function main() returns nothing: + Message original = Message.packet(Packet(label: "first", values: list(2, 3))) + trace original + println(describe(view original)) + Message copied = clone original + println(describe(view copied), describe(view original)) + list[Message] messages = list(original, copied, Message.empty) + for message in view messages: + println(describe(view message)) + return nothing diff --git a/tests/native/erased_same_owner/05_erased_other_owner.jett b/tests/native/erased_same_owner/05_erased_other_owner.jett new file mode 100644 index 00000000..e6cad1bf --- /dev/null +++ b/tests/native/erased_same_owner/05_erased_other_owner.jett @@ -0,0 +1,18 @@ +namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +struct Right: + name: string +implement Pairable for Left: + function combine(view self: Left, view other: Left) returns string: + return "left:{self.name}:{other.name}" +implement Pairable for Right: + function combine(view self: Right, view other: Right) returns string: + return "right:{self.name}:{other.name}" +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Right(name: "two") + println(Pairable.combine(view first, view second)) + return nothing diff --git a/tests/native/erased_same_owner/12_erased_matching_owner.jett b/tests/native/erased_same_owner/12_erased_matching_owner.jett new file mode 100644 index 00000000..4d461004 --- /dev/null +++ b/tests/native/erased_same_owner/12_erased_matching_owner.jett @@ -0,0 +1,13 @@ +namespace app +interface Pairable: + function combine(view self: Pairable, view other: Pairable) returns string +struct Left: + name: string +implement Pairable for Left: + function combine(view self: Left, view other: Left) returns string: + return "left:{self.name}:{other.name}" +function main() returns nothing: + Pairable first = Left(name: "one") + Pairable second = Left(name: "two") + println(Pairable.combine(view first, view second)) + return nothing diff --git a/tests/native/field_update_reference/01_called_baselines.jett b/tests/native/field_update_reference/01_called_baselines.jett new file mode 100644 index 00000000..24f9ebc6 --- /dev/null +++ b/tests/native/field_update_reference/01_called_baselines.jett @@ -0,0 +1,43 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function make() returns Record: + return Record(inner: Inner(value: 1), label: "before") +function local() returns nothing: + mutable Record record = make() + (record).inner.value = 9 +function parameter(mutable record: Record) returns nothing: + record.label = "after" +function next_value(view record: Record) returns int64: + return record.inner.value + 8 +function updated(mutable record: Record) returns Record: + record.inner.value = next_value(view record) + record.label = "after" + return record +function main(stdout: Stdout) returns nothing: + local() + parameter(make()) + Record original = make() + Record changed = updated(clone original) + Stdout.write(view stdout, "original:{original.inner.value}:{original.label}\n") + Stdout.write(view stdout, "changed:{changed.inner.value}:{changed.label}\n") +verify called_updates: + local() + parameter(make()) + Record original = make() + Record changed = updated(clone original) + assert original.inner.value == 1 + assert original.label == "before" + assert changed.inner.value == 9 + assert changed.label == "after" +property owned_clone_is_independent: + given seed: int64 + Record original = Record(inner: Inner(value: seed), label: "before") + Record changed = updated(clone original) + assert original.inner.value == seed + assert original.label == "before" + assert changed.inner.value == seed + 8 + assert changed.label == "after" diff --git a/tests/native/field_update_reference/02_owning_clone_fields.jett b/tests/native/field_update_reference/02_owning_clone_fields.jett new file mode 100644 index 00000000..e4c33392 --- /dev/null +++ b/tests/native/field_update_reference/02_owning_clone_fields.jett @@ -0,0 +1,25 @@ +namespace app +struct Owned: + values: list[int64] + raw: bytes + hidden: secret[string] + label: string +function make() returns Owned: + secret[string] hidden = "private-sibling" + return Owned(values: list(1, 2), raw: bytes.from_string("a"), hidden: hidden, label: "before") +function update(mutable owned: Owned) returns Owned: + owned.values = list(3, 4, 5) + owned.raw = bytes.from_string("xyz") + owned.label = "after" + return owned +function report() returns string: + Owned original = make() + Owned changed = update(clone original) + string original_hidden = declassify original.hidden + string changed_hidden = declassify changed.hidden + bool secret_preserved = original_hidden == changed_hidden + return "{list.length(view original.values)}:{bytes.length(view original.raw)}:{original.label}:{list.length(view changed.values)}:{bytes.length(view changed.raw)}:{changed.label}:{secret_preserved}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report()}\n") +verify owning_replacements: + assert report() == "2:1:before:3:3:after:true" diff --git a/tests/native/field_update_reference/03_rhs_order_and_early_return.jett b/tests/native/field_update_reference/03_rhs_order_and_early_return.jett new file mode 100644 index 00000000..fc04996c --- /dev/null +++ b/tests/native/field_update_reference/03_rhs_order_and_early_return.jett @@ -0,0 +1,29 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function make() returns Record: + return Record(inner: Inner(value: 1), label: "before") +function replacement(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "rhs\n") + return 9 +function successful(view stdout: Stdout) returns Record: + mutable Record record = make() + record.inner.value = replacement(view stdout) + return record +function handled_abort(mutable record: Record) returns Record: + optional[int64] absent = none + record.inner.value = absent handle: + return record + return record +function main(stdout: Stdout) returns nothing: + Record changed = successful(view stdout) + Stdout.write(view stdout, "changed:{changed.inner.value}:{changed.label}\n") + Record unchanged = handled_abort(make()) + Stdout.write(view stdout, "unchanged:{unchanged.inner.value}:{unchanged.label}\n") +verify abort_before_commit: + Record unchanged = handled_abort(make()) + assert unchanged.inner.value == 1 + assert unchanged.label == "before" diff --git a/tests/native/field_update_reference/04_exact_machine_state.jett b/tests/native/field_update_reference/04_exact_machine_state.jett new file mode 100644 index 00000000..4ea2f81b --- /dev/null +++ b/tests/native/field_update_reference/04_exact_machine_state.jett @@ -0,0 +1,21 @@ +namespace app +struct Inner: + value: int64 +machine Session: + states: + closed + open(inner: Inner, label: string) + transitions: + closed to open +function update_session(mutable session: Session at open) returns Session at open: + session.inner.value = 8 + session.label = "after" + return session +function machine_report() returns string: + Session at open changed = update_session(Session(open, Inner(value: 1), "before")) + bool preserved_state = changed at open + return "{changed.inner.value}:{changed.label}:{preserved_state}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "machine:{machine_report()}\n") +verify machine_keeps_state: + assert machine_report() == "8:after:true" diff --git a/tests/native/field_update_reference/05_pending_root_preflight.jett b/tests/native/field_update_reference/05_pending_root_preflight.jett new file mode 100644 index 00000000..203f4c29 --- /dev/null +++ b/tests/native/field_update_reference/05_pending_root_preflight.jett @@ -0,0 +1,16 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function make() returns Record: + return Record(inner: Inner(value: 1), label: "before") +function replacement(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "rhs-must-not-run\n") + return 9 +function main(stdout: Stdout) returns nothing: + mutable Record record = run run make() + Stdout.write(view stdout, "before:root\n") + record.inner.value = replacement(view stdout) + Stdout.write(view stdout, "after-must-not-run\n") diff --git a/tests/native/field_update_reference/06_pending_intermediate_preflight.jett b/tests/native/field_update_reference/06_pending_intermediate_preflight.jett new file mode 100644 index 00000000..81e85e7a --- /dev/null +++ b/tests/native/field_update_reference/06_pending_intermediate_preflight.jett @@ -0,0 +1,14 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function replacement(view stdout: Stdout) returns int64: + Stdout.write(view stdout, "rhs-must-not-run\n") + return 9 +function main(stdout: Stdout) returns nothing: + mutable Record record = Record(inner: run run Inner(value: 1), label: "before") + Stdout.write(view stdout, "before:inner\n") + record.inner.value = replacement(view stdout) + Stdout.write(view stdout, "after-must-not-run\n") diff --git a/tests/native/field_update_reference/07_pending_children_transport.jett b/tests/native/field_update_reference/07_pending_children_transport.jett new file mode 100644 index 00000000..04b9041c --- /dev/null +++ b/tests/native/field_update_reference/07_pending_children_transport.jett @@ -0,0 +1,22 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + pending: int64 + label: string +function main(stdout: Stdout) returns nothing: + mutable Record record = Record(inner: Inner(value: 1), pending: run run 17, label: "before") + record.inner.value = 9 + record.label = "after" + int64 once = join record.pending handle error: + return nothing + int64 ready = join once handle error: + return nothing + record.inner = run run Inner(value: 5) + record.label = "again" + Inner inner_once = join record.inner handle error: + return nothing + Inner inner_ready = join inner_once handle error: + return nothing + Stdout.write(view stdout, "children:{ready}:{inner_ready.value}:{record.label}\n") diff --git a/tests/native/field_update_reference/08_stored_child_depth_refusal.jett b/tests/native/field_update_reference/08_stored_child_depth_refusal.jett new file mode 100644 index 00000000..f8865c4c --- /dev/null +++ b/tests/native/field_update_reference/08_stored_child_depth_refusal.jett @@ -0,0 +1,12 @@ +namespace app +struct Record: + pending: int64 + label: string +function main(stdout: Stdout) returns nothing: + mutable Record record = Record(pending: run run 17, label: "before") + record.label = "after" + int64 once = join record.pending handle error: + return nothing + Stdout.write(view stdout, "before:remaining-depth\n") + int64 forbidden = once + 1 + Stdout.write(view stdout, "after-must-not-run:{forbidden}\n") diff --git a/tests/native/field_update_reference/09_generic_alias.jett b/tests/native/field_update_reference/09_generic_alias.jett new file mode 100644 index 00000000..e2e740b0 --- /dev/null +++ b/tests/native/field_update_reference/09_generic_alias.jett @@ -0,0 +1,17 @@ +namespace app +struct Boxed[T]: + value: T + label: string +type NumberBox = Boxed[int64] +function update[T](mutable boxed: Boxed[T], replacement: T) returns Boxed[T]: + boxed.value = replacement + return boxed +function report() returns string: + NumberBox original = Boxed[int64](value: 1, label: "number") + NumberBox changed = update[int64](clone original, 9) + Boxed[string] text = update[string](Boxed[string](value: "old", label: "text"), "new") + return "{original.value}:{changed.value}:{changed.label}:{text.value}:{text.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report()}\n") +verify generic_place_selection: + assert report() == "1:9:number:new:text" diff --git a/tests/native/field_update_reference/10_refined_field_replacement.jett b/tests/native/field_update_reference/10_refined_field_replacement.jett new file mode 100644 index 00000000..d78868ff --- /dev/null +++ b/tests/native/field_update_reference/10_refined_field_replacement.jett @@ -0,0 +1,18 @@ +namespace app +type NonZero = int64 where value != 0 +struct Record: + divisor: NonZero + label: string +function report() returns string: + NonZero initial = 2 handle error: + return error + NonZero replacement = 5 handle error: + return error + mutable Record record = Record(divisor: initial, label: "stable") handle error: + return error + record.divisor = replacement + return "{10 / record.divisor}:{record.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report()}\n") +verify whole_refined_field: + assert report() == "2:stable" diff --git a/tests/native/field_update_reference/11_refined_root_rejected.jett b/tests/native/field_update_reference/11_refined_root_rejected.jett new file mode 100644 index 00000000..9248f877 --- /dev/null +++ b/tests/native/field_update_reference/11_refined_root_rejected.jett @@ -0,0 +1,11 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner +type ValidRecord = Record where value.inner.value > 0 +type Selected = ValidRecord +function main() returns nothing: + mutable Selected record = Record(inner: Inner(value: 1)) handle error: + return nothing + record.inner.value = 0 diff --git a/tests/native/field_update_reference/12_refined_intermediate_rejected.jett b/tests/native/field_update_reference/12_refined_intermediate_rejected.jett new file mode 100644 index 00000000..dc8a61c0 --- /dev/null +++ b/tests/native/field_update_reference/12_refined_intermediate_rejected.jett @@ -0,0 +1,13 @@ +namespace app +struct Inner: + value: int64 +type ValidInner = Inner where value.value > 0 +type Selected = ValidInner +struct Record: + inner: Selected +function main() returns nothing: + Selected initial = Inner(value: 1) handle error: + return nothing + mutable Record record = Record(inner: initial) handle error: + return nothing + record.inner.value = 0 diff --git a/tests/native/field_update_reference/13_bitfield_rejected.jett b/tests/native/field_update_reference/13_bitfield_rejected.jett new file mode 100644 index 00000000..471fdb63 --- /dev/null +++ b/tests/native/field_update_reference/13_bitfield_rejected.jett @@ -0,0 +1,6 @@ +namespace app +bitfield network Header: + value: 4 bits +function main() returns nothing: + mutable Header header = Header(value: 1) + header.value = 2 diff --git a/tests/native/field_update_reference/14_consumed_root_rejected.jett b/tests/native/field_update_reference/14_consumed_root_rejected.jett new file mode 100644 index 00000000..e98a6169 --- /dev/null +++ b/tests/native/field_update_reference/14_consumed_root_rejected.jett @@ -0,0 +1,9 @@ +namespace app +struct Record: + value: int64 +function consume(record: Record) returns int64: + return record.value +function main() returns nothing: + mutable Record record = Record(value: 1) + int64 consumed = consume(record) + record.value = consumed diff --git a/tests/native/field_update_reference/15_whole_loan_rejected.jett b/tests/native/field_update_reference/15_whole_loan_rejected.jett new file mode 100644 index 00000000..9a2edcb9 --- /dev/null +++ b/tests/native/field_update_reference/15_whole_loan_rejected.jett @@ -0,0 +1,12 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner +function invalid() returns int64: + mutable Record record = Record(inner: Inner(value: 1)) + Record alias = view record + record.inner.value = 9 + return alias.inner.value +function main() returns nothing: + int64 forbidden = invalid() diff --git a/tests/native/field_update_reference/16_field_loan_rejected.jett b/tests/native/field_update_reference/16_field_loan_rejected.jett new file mode 100644 index 00000000..2fa44405 --- /dev/null +++ b/tests/native/field_update_reference/16_field_loan_rejected.jett @@ -0,0 +1,12 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner +function invalid() returns int64: + mutable Record record = Record(inner: Inner(value: 1)) + Inner alias = view record.inner + record.inner.value = 9 + return alias.value +function main() returns nothing: + int64 forbidden = invalid() diff --git a/tests/native/field_update_reference/17_rhs_root_consumption_rejected.jett b/tests/native/field_update_reference/17_rhs_root_consumption_rejected.jett new file mode 100644 index 00000000..5082621a --- /dev/null +++ b/tests/native/field_update_reference/17_rhs_root_consumption_rejected.jett @@ -0,0 +1,8 @@ +namespace app +struct Record: + value: int64 +function consume(record: Record) returns int64: + return record.value + 1 +function main() returns nothing: + mutable Record record = Record(value: 1) + record.value = consume(record) diff --git a/tests/native/field_update_reference/18_stale_nonzero_rejected.jett b/tests/native/field_update_reference/18_stale_nonzero_rejected.jett new file mode 100644 index 00000000..f05706fb --- /dev/null +++ b/tests/native/field_update_reference/18_stale_nonzero_rejected.jett @@ -0,0 +1,10 @@ +namespace app +struct Record: + divisor: int64 +function invalid(mutable record: Record) returns int64: + if record.divisor != 0: + record.divisor = 0 + return 8 / record.divisor + return 0 +function main() returns nothing: + int64 forbidden = invalid(Record(divisor: 2)) diff --git a/tests/native/field_update_reference/19_view_alias_rejected.jett b/tests/native/field_update_reference/19_view_alias_rejected.jett new file mode 100644 index 00000000..8bcd14e9 --- /dev/null +++ b/tests/native/field_update_reference/19_view_alias_rejected.jett @@ -0,0 +1,8 @@ +namespace app +struct Record: + value: int64 +function invalid(view source: Record) returns nothing: + mutable Record alias = view source + alias.value = 9 +function main() returns nothing: + invalid(view Record(value: 1)) diff --git a/tests/native/field_update_reference/20_unknown_machine_state_rejected.jett b/tests/native/field_update_reference/20_unknown_machine_state_rejected.jett new file mode 100644 index 00000000..685679ef --- /dev/null +++ b/tests/native/field_update_reference/20_unknown_machine_state_rejected.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + closed + open(value: int64) + transitions: + closed to open +function invalid(mutable session: Session) returns nothing: + session.value = 9 +function main() returns nothing: + invalid(Session(closed)) diff --git a/tests/native/field_update_reference/21_guarded_field_control.jett b/tests/native/field_update_reference/21_guarded_field_control.jett new file mode 100644 index 00000000..588b69cf --- /dev/null +++ b/tests/native/field_update_reference/21_guarded_field_control.jett @@ -0,0 +1,10 @@ +namespace app +struct Record: + divisor: int64 +function guarded(view record: Record) returns int64: + if record.divisor != 0: + return 8 / record.divisor + return 0 +function main(stdout: Stdout) returns nothing: + Record record = Record(divisor: 2) + Stdout.write(view stdout, "guarded:{guarded(view record)}\n") diff --git a/tests/native/field_update_reference/22_branch_loop_lifetimes.jett b/tests/native/field_update_reference/22_branch_loop_lifetimes.jett new file mode 100644 index 00000000..1f58b98b --- /dev/null +++ b/tests/native/field_update_reference/22_branch_loop_lifetimes.jett @@ -0,0 +1,33 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function update(mutable record: Record, take_first: bool) returns Record: + if take_first: + record.inner.value = 1 + else: + record.inner.value = 2 + if take_first: + record.label = "first" + else: + record.label = "second" + record.inner.value = 3 + mutable int64 step = 0 + while step < 3: + step = step + 1 + if step == 1: + continue + record.inner.value = record.inner.value + step + if step == 2: + break + return record +function report(take_first: bool) returns string: + Record changed = update(Record(inner: Inner(value: 1), label: "before"), take_first) + return "{changed.inner.value}:{changed.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{report(true)}:{report(false)}\n") +verify branch_loop_places: + assert report(true) == "5:first" + assert report(false) == "5:second" diff --git a/tests/native/field_update_reference/23_required_comptime.jett b/tests/native/field_update_reference/23_required_comptime.jett new file mode 100644 index 00000000..79688773 --- /dev/null +++ b/tests/native/field_update_reference/23_required_comptime.jett @@ -0,0 +1,16 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function nested_value() returns int64: + mutable Record record = Record(inner: Inner(value: 1), label: "before") + record.inner.value = 9 + return record.inner.value +function main(stdout: Stdout) returns nothing: + int64 baked = comptime nested_value() + Stdout.write(view stdout, "baked:{baked}\n") +verify required_field_update: + int64 baked = comptime nested_value() + assert baked == 9 diff --git a/tests/native/field_update_reference/24_actor_handler_future.jett b/tests/native/field_update_reference/24_actor_handler_future.jett new file mode 100644 index 00000000..0b2e09d0 --- /dev/null +++ b/tests/native/field_update_reference/24_actor_handler_future.jett @@ -0,0 +1,15 @@ +namespace app +struct Inner: + value: int64 +actor Store: + mutable Inner inner = Inner(value: 1) + receive replace(value: int64): + inner.value = value + receive read responds int64: + respond inner.value +function main(stdout: Stdout) returns nothing: + Store store = spawn Store() + int64 before = ask store.read + send store.replace(9) + int64 after = ask store.read + Stdout.write(view stdout, "actor:{before}:{after}\n") diff --git a/tests/native/field_update_reference/25_secret_owned_root.jett b/tests/native/field_update_reference/25_secret_owned_root.jett new file mode 100644 index 00000000..5d78d0d7 --- /dev/null +++ b/tests/native/field_update_reference/25_secret_owned_root.jett @@ -0,0 +1,25 @@ +namespace app +struct Inner: + value: int64 +struct Record: + number: int64 + text: string + inner: Inner +function summary(view hidden: secret[Record]) returns string: + int64 number = declassify hidden.number + string text = declassify hidden.text + int64 nested = declassify hidden.inner.value + return "{number}:{text}:{nested}" +function update(mutable hidden: secret[Record]) returns secret[Record]: + hidden.number = 9 + hidden.text = "after" + hidden.inner.value = 8 + return hidden +function report() returns string: + secret[Record] original = Record(number: 1, text: "before", inner: Inner(value: 2)) + secret[Record] changed = update(clone original) + return "{summary(view original)}|{summary(view changed)}|{summary(view original)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "secret:{report()}\n") +verify secret_owned_place: + assert report() == "1:before:2|9:after:8|1:before:2" diff --git a/tests/native/field_update_reference/26_for_loop_update.jett b/tests/native/field_update_reference/26_for_loop_update.jett new file mode 100644 index 00000000..69341c06 --- /dev/null +++ b/tests/native/field_update_reference/26_for_loop_update.jett @@ -0,0 +1,17 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + items: list[int64] + label: string +function report() returns string: + mutable Record record = Record(inner: Inner(value: 0), items: list(0), label: "stable") + for step in list(1, 2, 3): + record.inner.value = record.inner.value + step + record.items = list(step) + return "{record.inner.value}:{list.length(view record.items)}:{record.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "loop:{report()}\n") +verify for_places: + assert report() == "6:1:stable" diff --git a/tests/native/field_update_reference/27_nested_handler_update.jett b/tests/native/field_update_reference/27_nested_handler_update.jett new file mode 100644 index 00000000..566ec5cb --- /dev/null +++ b/tests/native/field_update_reference/27_nested_handler_update.jett @@ -0,0 +1,18 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +function report() returns string: + mutable Record outer = Record(inner: Inner(value: 1), label: "outer") + optional[int64] absent = none + outer.inner.value = absent handle: + mutable Record inner = Record(inner: Inner(value: 2), label: "stable") + inner.inner.value = 7 + return "{outer.inner.value}:{inner.inner.value}:{inner.label}" + return "committed-must-not-run" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "handler:{report()}\n") +verify nested_handler_places: + assert report() == "1:7:stable" diff --git a/tests/native/field_update_reference/28_metadata_holder_replacement.jett b/tests/native/field_update_reference/28_metadata_holder_replacement.jett new file mode 100644 index 00000000..1ce4d112 --- /dev/null +++ b/tests/native/field_update_reference/28_metadata_holder_replacement.jett @@ -0,0 +1,12 @@ +namespace app +struct Holder: + selected: TypeInfo + label: string +function report() returns string: + mutable Holder holder = Holder(selected: type.info[int64](), label: "stable") + holder.selected = type.info[string]() + return "{holder.selected.type_name}:{holder.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "metadata:{report()}\n") +verify terminal_metadata_replacement: + assert report() == "string:stable" diff --git a/tests/native/field_update_reference/29_inline_owned_parameter.jett b/tests/native/field_update_reference/29_inline_owned_parameter.jett new file mode 100644 index 00000000..dcfe2124 --- /dev/null +++ b/tests/native/field_update_reference/29_inline_owned_parameter.jett @@ -0,0 +1,16 @@ +namespace app +struct Record: + value: int64 + label: string +function report() returns string: + function(Record) returns Record change = function(mutable owned: Record) returns Record: + owned.value = 7 + owned.label = "changed" + return owned + Record original = Record(value: 1, label: "stable") + Record changed = change(clone original) + return "{original.value}:{original.label}|{changed.value}:{changed.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "inline:{report()}\n") +verify inline_owned_parameter: + assert report() == "1:stable|7:changed" diff --git a/tests/native/field_update_reference/30_returned_inline_owned_parameter.jett b/tests/native/field_update_reference/30_returned_inline_owned_parameter.jett new file mode 100644 index 00000000..1a5e01e8 --- /dev/null +++ b/tests/native/field_update_reference/30_returned_inline_owned_parameter.jett @@ -0,0 +1,17 @@ +namespace app +struct Record: + value: int64 + label: string +function factory() returns function(Record) returns Record: + return function(mutable owned: Record) returns Record: + owned.value = 8 + return owned +function report() returns string: + function(Record) returns Record change = factory() + Record original = Record(value: 1, label: "stable") + Record changed = change(clone original) + return "{original.value}:{changed.value}:{changed.label}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "returned:{report()}\n") +verify returned_inline_owned_parameter: + assert report() == "1:8:stable" diff --git a/tests/native/field_update_reference/31_reflected_iteration_update.jett b/tests/native/field_update_reference/31_reflected_iteration_update.jett new file mode 100644 index 00000000..fb616ec3 --- /dev/null +++ b/tests/native/field_update_reference/31_reflected_iteration_update.jett @@ -0,0 +1,24 @@ +namespace app +struct Mixed: + first: int64 + second: string + third: int64 +struct Record: + count: int64 + label: string + items: list[int64] + sibling: string +function report() returns string: + mutable Record record = Record(count: 0, label: "", items: list(0), sibling: "stable") + for field in type.fields[Mixed](): + comptime type Field = field.type_info: + record.count = record.count + field.index + 1 + string name = type.name[Field]() + record.label = "{record.label}{name}|" + record.items = list(field.index) + int64 length = list.length[int64](view record.items) + return "{record.count}:{record.label}:{length}:{record.sibling}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "reflected:{report()}\n") +verify reflected_iteration_update: + assert report() == "6:int64|string|int64|:1:stable" diff --git a/tests/native/field_update_reference/32_borrowed_match_owner_preservation.jett b/tests/native/field_update_reference/32_borrowed_match_owner_preservation.jett new file mode 100644 index 00000000..7a623b8a --- /dev/null +++ b/tests/native/field_update_reference/32_borrowed_match_owner_preservation.jett @@ -0,0 +1,48 @@ +namespace app +struct Payload: + values: list[int64] +enum Choice: + item(data: Payload) + empty +type ValidChoice = Choice where true +function observe(view data: Payload) returns int64: + return list.sum[int64](view data.values) +function direct(view choice: Choice) returns int64: + match choice: + item(data): + return observe(view data) + empty: + return 0 +function aliased(view choice: Choice) returns int64: + Choice retained = view choice + match retained: + item(data): + Payload forwarded = view data + return observe(view forwarded) + empty: + return 0 +function unqualified(view choice: secret[Choice]) returns int64: + match declassify(view choice): + item(data): + return observe(view data) + empty: + return 0 +function coarsened(view choice: ValidChoice) returns int64: + match coarsen(view choice): + item(data): + return observe(view data) + empty: + return 0 +function main(stdout: Stdout) returns nothing: + Choice original = Choice.item(Payload(values: list(1, 2, 3))) + secret[Choice] protected = clone original + ValidChoice validated = clone original handle error: + return nothing + int64 first = direct(view original) + int64 second = aliased(view original) + int64 third = unqualified(view protected) + int64 fourth = coarsened(view validated) + int64 repeated_secret = unqualified(view protected) + int64 repeated_refined = coarsened(view validated) + int64 repeated_original = direct(view original) + Stdout.write(view stdout, "matches:{first}:{second}:{third}:{fourth}:{repeated_secret}:{repeated_refined}:{repeated_original}\n") diff --git a/tests/native/float_remainder.jett b/tests/native/float_remainder.jett new file mode 100644 index 00000000..60205a02 --- /dev/null +++ b/tests/native/float_remainder.jett @@ -0,0 +1,42 @@ +namespace app +function remainder[T](left: T, right: T) returns T: + return left modulo right +function finite32(view stdout: Stdout, left: float32, right: float32) returns nothing: + Stdout.write(view stdout, "{remainder[float32](left, right)} {remainder[float32](-left, right)} {remainder[float32](left, -right)} {remainder[float32](-left, -right)}\n") + float32 pending = run remainder[float32](left, right) + float32 ready = join pending handle error: + return nothing + Stdout.write(view stdout, "{ready}\n") +function finite64(view stdout: Stdout, left: float64, right: float64) returns nothing: + Stdout.write(view stdout, "{remainder[float64](left, right)} {remainder[float64](-left, right)} {remainder[float64](left, -right)} {remainder[float64](-left, -right)}\n") + float64 pending = run remainder[float64](left, right) + float64 ready = join pending handle error: + return nothing + Stdout.write(view stdout, "{ready}\n") +function edges32(view stdout: Stdout, zero: float32) returns nothing: + float32 infinity = 1.0 / zero + float32 nan = zero / zero + float32 zero_divisor = remainder[float32](5.5, zero) + float32 infinite_dividend = remainder[float32](infinity, 2.0) + float32 nan_dividend = remainder[float32](nan, 2.0) + float32 nan_divisor = remainder[float32](2.0, nan) + float32 negative_zero = remainder[float32](-zero, 2.0) + Stdout.write(view stdout, "{zero_divisor != zero_divisor} {infinite_dividend != infinite_dividend} {nan_dividend != nan_dividend} {nan_divisor != nan_divisor} {1.0 / negative_zero < 0.0} {remainder[float32](5.5, infinity) == 5.5}\n") +function edges64(view stdout: Stdout, zero: float64) returns nothing: + float64 infinity = 1.0 / zero + float64 nan = zero / zero + float64 zero_divisor = remainder[float64](5.5, zero) + float64 infinite_dividend = remainder[float64](infinity, 2.0) + float64 nan_dividend = remainder[float64](nan, 2.0) + float64 nan_divisor = remainder[float64](2.0, nan) + float64 negative_zero = remainder[float64](-zero, 2.0) + Stdout.write(view stdout, "{zero_divisor != zero_divisor} {infinite_dividend != infinite_dividend} {nan_dividend != nan_dividend} {nan_divisor != nan_divisor} {1.0 / negative_zero < 0.0} {remainder[float64](5.5, infinity) == 5.5}\n") +function main(stdout: Stdout) returns nothing: + finite32(view stdout, 5.5, 2.0) + finite64(view stdout, 5.5, 2.0) + edges32(view stdout, 0.0) + edges64(view stdout, 0.0) + float32 baked32 = comptime remainder[float32](5.5, 2.0) + float64 baked64 = comptime remainder[float64](-5.5, 2.0) + float32 rounded = comptime remainder[float32](5.3, 0.2) + Stdout.write(view stdout, "{baked32} {baked64}\n{remainder[float32](5.3, 0.2) == rounded}\n") diff --git a/tests/native/function_descriptor_ownership.jett b/tests/native/function_descriptor_ownership.jett new file mode 100644 index 00000000..3dbc3ae1 --- /dev/null +++ b/tests/native/function_descriptor_ownership.jett @@ -0,0 +1,24 @@ +namespace app +struct Callbacks: + primary: function(int64) returns int64 + secondary: function(int64) returns int64 +function increment(value: int64) returns int64: + return value + 1 +function double(value: int64) returns int64: + return value * 2 +function choose(first: bool) returns function(int64) returns int64: + if first: + return increment + return double +function main() returns nothing: + Callbacks callbacks = Callbacks(primary: choose(true), secondary: choose(false)) + function(int64) returns int64 selected = callbacks.primary + function(int64) returns int64 fallback = callbacks.secondary + println(selected(4), fallback(4)) + list[function(int64) returns int64] many = list(increment, double) + function(int64) returns int64 from_list = list.get[function(int64) returns int64](view many, 1) handle: + default increment + println(from_list(5)) + map[string, function(int64) returns int64] named = map("inc": increment, "double": double) + function(int64) returns int64 from_map = map.get_or[string, function(int64) returns int64](named, "double", increment) + println(from_map(6)) diff --git a/tests/native/function_expression_callee_failure.jett b/tests/native/function_expression_callee_failure.jett new file mode 100644 index 00000000..4e59758e --- /dev/null +++ b/tests/native/function_expression_callee_failure.jett @@ -0,0 +1,10 @@ +namespace test +function count(items: list[string]) returns int64: + return list.length[string](view items) +function failing_factory() returns function(list[string]) returns int64: + list[int64] invalid = list.remove_at[int64](list(1), -1) + println(list.length[int64](view invalid)) + return count +function main() returns nothing: + println(failing_factory()(list("owned", "before", "callee"))) + return nothing diff --git a/tests/native/function_expression_calls.jett b/tests/native/function_expression_calls.jett new file mode 100644 index 00000000..a714f4ad --- /dev/null +++ b/tests/native/function_expression_calls.jett @@ -0,0 +1,171 @@ +namespace alpha +export function value() returns int64: + return 1 +export function read_value() returns int64: + return alpha.value() +export function make_reader() returns function() returns int64: + use alpha as selected + return function() returns int64: return selected.value() +export function make_baked_reader() returns function() returns int64: + return comptime alpha.make_reader() +namespace beta +export function value() returns int64: + return 9 +namespace test +struct CallbackHolder: + callback: function(int64) returns int64 +struct ViewHolder: + callback: function(view list[int64]) returns int64 +struct WriterHolder: + callback: function(view Stdout, string) returns nothing +enum PairValue: + pair(left: int64, right: int64) +enum Choice: + item(value: int64) + empty +struct ChoiceMaker: + item: function(int64) returns Choice +function increment(value: int64) returns int64: + return value + 1 +function double(value: int64) returns int64: + return value * 2 +function subtract(left: int64, right: int64) returns int64: + return left - right +function generic_identity[T](value: T) returns T: + return value +function make_choice(value: int64) returns Choice: + return Choice.item(value + 1) +function check_variant_name_field() returns int64: + ChoiceMaker holder = ChoiceMaker(item: make_choice) + Choice selected = holder.item(4) + match selected: + item(value): + return value + empty: + return 0 +function make_offset(delta: int64) returns function(int64) returns int64: + return function(value: int64) returns int64: return value + delta +function make_holder(delta: int64) returns CallbackHolder: + return CallbackHolder(callback: make_offset(delta)) +function make_count(delta: int64) returns function(view list[int64]) returns int64: + return function(view items: list[int64]) returns int64: return list.length[int64](view items) + delta +function write(view stdout: Stdout, text: string) returns nothing: + Stdout.write(view stdout, text) + return nothing +function make_writer() returns function(view Stdout, string) returns nothing: + return write +function make_inline_writer() returns function(view Stdout, string) returns nothing: + return function(view stdout: Stdout, text: string) returns nothing: + Stdout.write(view stdout, text) + return nothing +function check_effects(view stdout: Stdout) returns nothing: + function(view Stdout, string) returns nothing callback = write + (callback)(view stdout, "parenthesized effect\n") + make_writer()(view stdout, "returned effect\n") + WriterHolder holder = WriterHolder(callback: write) + holder.callback(view stdout, "field effect\n") + make_inline_writer()(view stdout, "inline effect\n") + function(view Stdout, string) returns nothing baked = comptime make_inline_writer() + (baked)(view stdout, "baked effect\n") + return nothing +function argument_with_output() returns int64: + println("argument") + return 4 +function callee_with_output() returns function(int64) returns int64: + println("callee") + return increment +function skip_callee() returns int64: + optional[int64] missing = none + return callee_with_output()(missing handle: + return 7 + ) +function skip_call_after_owned_arguments() returns int64: + optional[function(list[string]) returns int64] missing = none + return(missing handle: + return 11 + )(list("temporary", "owned")) +function check_named_enum() returns int64: + PairValue value =(PairValue.pair)(right: 4, left: 9) + match value: + pair(left, right): + return left - right +function check_selection() returns nothing: + mutable function(int64) returns int64 selected = increment + optional[int64] missing = none + int64 changed =(selected)(missing handle: + selected = double + default 4 + ) + println(changed) + mutable CallbackHolder holder = CallbackHolder(callback: increment) + optional[int64] no_value = none + int64 changed_field = holder.callback(no_value handle: + holder = CallbackHolder(callback: double) + default 6 + ) + println(changed_field) + optional[function(int64) returns int64] no_callback = none + int64 handled_callee =(no_callback handle: + default increment + )(argument_with_output()) + println(handled_callee) + return nothing +function check_views() returns nothing: + list[int64] items = list(1, 2, 3) + function(view list[int64]) returns int64 callback = make_count(10) + println((callback)(view items)) + println(make_count(20)(view items)) + ViewHolder holder = ViewHolder(callback: make_count(30)) + println(holder.callback(view items)) + println((callback)(((view items)))) + println(make_count(40)((clone items))) + println(list.length[int64](view items)) + return nothing +function check_secret_results() returns nothing: + secret[int64] value = 4 + secret[int64] direct = increment(clone value) + secret[int64] returned = make_offset(10)(clone value) + secret[int64] projected = make_holder(20).callback(clone value) + secret[int64] piped = value into(make_offset(30)) + println(declassify direct, declassify returned, declassify projected, declassify piped) + return nothing +function check_namespaces() returns nothing: + use alpha as original + use beta as alpha + println(original.read_value()) + function() returns int64 reader = original.read_value + println((reader)()) + println(original.make_reader()()) + function() returns int64 baked = original.make_baked_reader() + println((baked)()) + println(alpha.value()) + return nothing +function main(stdout: Stdout) returns nothing: + function(int64) returns int64 callback = increment + println((callback)(4)) + println((increment)(4)) + println(((subtract))(right: 3, left: 10)) + println((generic_identity)[int64](8)) + println((generic_identity)(9)) + println(make_offset(10)(4)) + CallbackHolder holder = make_holder(20) + println(holder.callback(4)) + println((holder.callback)(5)) + println(4 into holder.callback) + println(4 into(make_offset(70))) + println(make_holder(30).callback(4)) + println((function(value: int64) returns int64: return value + 40)(4)) + int64 baked = comptime make_offset(50)(4) + int64 baked_field = comptime make_holder(60).callback(4) + println(baked, baked_field) + println(callee_with_output()(argument_with_output())) + println(skip_callee()) + println(skip_call_after_owned_arguments()) + println(check_named_enum()) + println(check_variant_name_field()) + check_selection() + check_views() + check_secret_results() + check_namespaces() + check_effects(view stdout) + return nothing diff --git a/tests/native/function_values.jett b/tests/native/function_values.jett new file mode 100644 index 00000000..48d92241 --- /dev/null +++ b/tests/native/function_values.jett @@ -0,0 +1,39 @@ +namespace app +struct Entry: + key: int64 + label: string +function entry_key(entry: Entry) returns int64: + return entry.key +function increment(value: int64) returns int64: + return value + 1 +function double(value: int64) returns int64: + return value * 2 +function negate(value: int64) returns int64: + return 0 - value +function apply(callback: function(int64) returns int64, value: int64) returns int64: + return callback(value) +function choose(increment_first: bool) returns function(int64) returns int64: + if increment_first: + return increment + return double +function main() returns nothing: + function(int64) returns int64 chosen = choose(true) + list[int64] mapped = list.map[int64, int64](list(1, 2, 3), increment) + optional[int64] last = list.get[int64](mapped, 2) + int64 final_value = last handle: + default 0 + list[int64] sorted = list.sort_by[int64](list(1, 2, 3), negate) + optional[int64] first = list.get[int64](sorted, 0) + int64 first_value = first handle: + default 0 + list[Entry] stable = list.sort_by[Entry](list(Entry(key: 1, label: "a"), Entry(key: 1, label: "b"), Entry(key: 0, label: "z")), entry_key) + optional[Entry] first_entry = list.get[Entry](view stable, 0) + Entry earliest = first_entry handle: + default Entry(key: 0, label: "missing") + optional[Entry] second_entry = list.get[Entry](view stable, 1) + Entry next = second_entry handle: + default Entry(key: 0, label: "missing") + optional[Entry] third_entry = list.get[Entry](view stable, 2) + Entry final = third_entry handle: + default Entry(key: 0, label: "missing") + println(apply(chosen, 7), apply(choose(false), 7), final_value, first_value, earliest.label, next.label, final.label) diff --git a/tests/native/generic_empty_collections.jett b/tests/native/generic_empty_collections.jett new file mode 100644 index 00000000..c6b8cac3 --- /dev/null +++ b/tests/native/generic_empty_collections.jett @@ -0,0 +1,24 @@ +namespace app +struct Record: + label: string +enum Choice: + named(value: string) + empty +function length[T](items: list[T]) returns int64: + return list.length[T](items) +function empty[T]() returns list[T]: + return list() +function first[T](view items: list[T]) returns optional[T]: + return list.first[T](view items) +function main() returns nothing: + println(length[Record](list()), length[Choice](list()), length[list[string]](list())) + println(list.length[Record](empty[Record]()), list.length[Choice](empty[Choice]())) + list[Record] records = list(Record(label: "Ada")) + list[Choice] choices = list(Choice.named("Lin")) + println(length[Record](clone records), length[Choice](clone choices)) + Record record = first[Record](view records) handle: + default Record(label: "missing") + Choice choice = first[Choice](view choices) handle: + default Choice.empty + println(record.label, choice == Choice.named("Lin")) + return nothing diff --git a/tests/native/generic_integer_wrapping.jett b/tests/native/generic_integer_wrapping.jett new file mode 100644 index 00000000..5a0e9b1a --- /dev/null +++ b/tests/native/generic_integer_wrapping.jett @@ -0,0 +1,22 @@ +function bump[T](value: T) returns T: + return value + 1 +function make_bump[T]() returns function(T) returns T: + return function(value: T) returns T: return value + 1 +function main() returns nothing: + function(int8) returns int8 narrow = make_bump[int8]() + function(int64) returns int64 wide = make_bump[int64]() + function(uint8) returns uint8 small_unsigned = make_bump[uint8]() + function(uint64) returns uint64 large_unsigned = make_bump[uint64]() + println(narrow(127)) + println(wide(127)) + println(small_unsigned(255)) + println(large_unsigned(18446744073709551615)) + println(bump[int8](127)) + println(bump[int64](127)) + function(int8) returns int8 baked_narrow = comptime make_bump[int8]() + function(int64) returns int64 baked_wide = comptime make_bump[int64]() + println(baked_narrow(127)) + println(baked_wide(127)) + println(comptime bump[int8](127)) + println(comptime bump[int64](127)) + return nothing diff --git a/tests/native/generic_interface_identity.jett b/tests/native/generic_interface_identity.jett new file mode 100644 index 00000000..5bb71243 --- /dev/null +++ b/tests/native/generic_interface_identity.jett @@ -0,0 +1,38 @@ +namespace records +export interface Named: + function name(view self: Named) returns string +export struct Box[T]: + item: T +type Token = secret[string] +export type IntBox = Box[int64] +implement Named for Box[int64]: + function name(view self: Box[int64]) returns string: + return "integer:{self.item}" +implement Named for Box[string]: + function name(view self: Box[string]) returns string: + return "text:{self.item}" +implement Named for Box[Token]: + function name(view self: Box[Token]) returns string: + return "secret" +export function show(view value: Named) returns string: + return Named.name(view value) +export function hidden() returns Named: + return Box[Token](item: "generic-hidden-token") +namespace app +function main() returns nothing: + use records + records.Named number = records.Box[int64](item: 17) + records.Named text = records.Box[string](item: "hello") + records.Named hidden = records.hidden() + println(records.show(view number)) + println(records.show(view text)) + println(records.show(view hidden)) + records.Named baked_number = comptime records.Box[int64](item: 23) + records.Named baked_hidden = comptime records.hidden() + println(records.show(view baked_number)) + println(records.show(view baked_hidden)) + records.Box[int64] concrete = records.Box[int64](item: 31) + println(records.IntBox.name(view concrete)) + function(view records.Box[int64]) returns string callback = records.IntBox.name + println(callback(view concrete)) + return nothing diff --git a/tests/native/generic_lexical_type_scope.jett b/tests/native/generic_lexical_type_scope.jett new file mode 100644 index 00000000..8fd9709d --- /dev/null +++ b/tests/native/generic_lexical_type_scope.jett @@ -0,0 +1,25 @@ +struct T: + number: int64 +function global_name() returns string: + return type.name[T]() +function global_closure() returns function() returns string: + return function() returns string: return type.name[T]() +function invoke[T]() returns string: + return global_name() +function make[T]() returns function() returns string: + return global_closure() +function explicit_name[T]() returns string: + return type.name[T]() +function invoke_explicit[T]() returns string: + return explicit_name[T]() +function main() returns nothing: + println(global_name()) + println(invoke[int64]()) + println(comptime invoke[string]()) + function() returns string baked = comptime make[int64]() + function() returns string live = make[string]() + println(baked()) + println(live()) + println(invoke_explicit[int64]()) + println(invoke_explicit[string]()) + return nothing diff --git a/tests/native/generic_map_aggregates.jett b/tests/native/generic_map_aggregates.jett new file mode 100644 index 00000000..fe9f3b08 --- /dev/null +++ b/tests/native/generic_map_aggregates.jett @@ -0,0 +1,23 @@ +namespace app +struct Record: + label: string +enum Choice: + named(value: string) + empty +function empty[T]() returns map[string, T]: + return map.new[string, T]() +function first[T](entries: map[string, T]) returns optional[T]: + return map.get[string, T](entries, "one") +function main() returns nothing: + println(map.length[string, Record](empty[Record]()), map.length[string, Choice](empty[Choice]())) + map[string, Record] records = map("one": Record(label: "Ada")) + map[string, Choice] choices = map("one": Choice.named("Lin")) + map[string, list[string]] lists = map("one": list("a", "b")) + Record record = first[Record](records) handle: + default Record(label: "missing") + Choice choice = first[Choice](choices) handle: + default Choice.empty + list[string] values = first[list[string]](lists) handle: + default list() + println(record.label, choice == Choice.named("Lin"), list.length[string](values)) + return nothing diff --git a/tests/native/generic_reflection_expressions.jett b/tests/native/generic_reflection_expressions.jett new file mode 100644 index 00000000..a985daac --- /dev/null +++ b/tests/native/generic_reflection_expressions.jett @@ -0,0 +1,16 @@ +function decorate(value: string) returns string: + return "<{value}>" +function describe[T]() returns string: + return "type:{type.name[T]()}" +function nested_argument[T]() returns string: + return decorate(type.name[T]()) +function make[T](prefix: string) returns function() returns string: + return function() returns string: return "{prefix}:{type.name[T]()}" +function main() returns nothing: + println(describe[int64]()) + println(nested_argument[string]()) + function() returns string live = make[int64]("live") + function() returns string baked = comptime make[string]("baked") + println(live()) + println(baked()) + return nothing diff --git a/tests/native/global_constants.jett b/tests/native/global_constants.jett new file mode 100644 index 00000000..d027d5c5 --- /dev/null +++ b/tests/native/global_constants.jett @@ -0,0 +1,44 @@ +namespace app +type Count = int8 +Count minimum = -128 +uint64 maximum = 18446744073709551615 +float32 rounded = 3.14 +string greeting = "hello" +bool enabled = true +nothing unit = nothing +int64 base = 40 +int64 answer = comptime(base + 2) +string label = comptime "{greeting}:{answer}" +function calculate() returns int64: + return answer + 1 +int64 baked = comptime calculate() +function describe() returns string: + bool unit_equal = unit == nothing + return "{minimum}:{maximum}:{rounded}:{label}:{enabled}:{unit_equal}:{baked}" +function callback() returns function() returns string: + return function() returns string: + return describe() +function generic_answer[T](value: T) returns int64: + return answer +function main(stdout: Stdout) returns nothing: + trace minimum + trace label + trace unit + string baked_description = comptime describe() + function() returns string describe_later = callback() + function() returns string label_later = function() returns string: return label + Stdout.write(view stdout, "{describe()}\n{describe_later()}\n{baked_description}\n") + Stdout.write(view stdout, "{label_later()}:{generic_answer[bool](true)}:{generic_answer[string]("ignored")}\n") +verify baked_constants: + trace answer + breakpoint + assert answer == 42 + assert baked == 43 + assert unit == nothing + assert label == "hello:42" +property constant_reads: + given value: int64 + trace greeting + assert value + answer - answer == value + assert generic_answer[int64](value) == 42 + assert label == "hello:42" diff --git a/tests/native/graphics_authority.jett b/tests/native/graphics_authority.jett new file mode 100644 index 00000000..194c1cb1 --- /dev/null +++ b/tests/native/graphics_authority.jett @@ -0,0 +1,3 @@ +namespace test +function main(display: Graphics) returns nothing: + println("graphics authority granted") diff --git a/tests/native/graphics_pending_scalar_state.jett b/tests/native/graphics_pending_scalar_state.jett new file mode 100644 index 00000000..6f200170 --- /dev/null +++ b/tests/native/graphics_pending_scalar_state.jett @@ -0,0 +1,15 @@ +namespace app +function update(state: int64, key: graphics.Key) returns int64: + trace state + return run state +function render(view state: int64) returns graphics.Scene: + trace state + graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function main(display: Graphics, stdout: Stdout) returns nothing: + graphics.Config config = graphics.Config(title: "Pending state", width: 32, height: 32) + int64 initial = run 5 + graphics.run[int64](view display, config, initial, update, render) handle error: + Stdout.write(view stdout, error) + return nothing + Stdout.write(view stdout, "closed\n") diff --git a/tests/native/graphics_pending_scalar_state_types.jett b/tests/native/graphics_pending_scalar_state_types.jett new file mode 100644 index 00000000..3e23fc80 --- /dev/null +++ b/tests/native/graphics_pending_scalar_state_types.jett @@ -0,0 +1,27 @@ +namespace app +function update_flag(state: bool, key: graphics.Key) returns bool: + trace state + return run state +function render_flag(view state: bool) returns graphics.Scene: + trace state + graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function update_ratio(state: float64, key: graphics.Key) returns float64: + trace state + return run state +function render_ratio(view state: float64) returns graphics.Scene: + trace state + graphics.Color color = graphics.Color(red: 0, green: 0, blue: 0) + return graphics.Scene(background: color, rectangles: list.new[graphics.Rect](), texts: list.new[graphics.Text]()) +function main(display: Graphics, stdout: Stdout) returns nothing: + graphics.Config config = graphics.Config(title: "Pending flag", width: 32, height: 32) + bool flag = run true + graphics.run[bool](view display, config, flag, update_flag, render_flag) handle error: + Stdout.write(view stdout, error) + return nothing + graphics.Config second = graphics.Config(title: "Pending ratio", width: 32, height: 32) + float64 ratio = run 1.25 + graphics.run[float64](view display, second, ratio, update_ratio, render_ratio) handle error: + Stdout.write(view stdout, error) + return nothing + Stdout.write(view stdout, "closed twice\n") diff --git a/tests/native/handled_interpolation.jett b/tests/native/handled_interpolation.jett new file mode 100644 index 00000000..4692d381 --- /dev/null +++ b/tests/native/handled_interpolation.jett @@ -0,0 +1,15 @@ +namespace app +function maybe(flag: bool) returns optional[int64]: + if flag: + return some(3) + return none +function label(value: string) returns string: + println(value) + return value +function fallback() returns int64: + println("fallback") + return 7 +function main() returns nothing: + println("value {maybe(true) handle: default fallback()}") + println("ordered {label("first")} {maybe(false) handle: default fallback()} {label("last")}") + return nothing diff --git a/tests/native/indirect_owned_result_failure.jett b/tests/native/indirect_owned_result_failure.jett new file mode 100644 index 00000000..14cbd852 --- /dev/null +++ b/tests/native/indirect_owned_result_failure.jett @@ -0,0 +1,14 @@ +namespace app +struct Record: + label: string +function item(view stdout: Stdout, index: int64) returns Record: + Stdout.write(view stdout, "{index};") + if index == 3: + string rejected = string.repeat("ab", 9223372036854775807) + return Record(label: rejected) + return Record(label: "item:{index}") +function collect[T](factory: function(view Stdout, int64) returns T, view stdout: Stdout) returns list[T]: + return list(factory(view stdout, 1), factory(view stdout, 2), factory(view stdout, 3), factory(view stdout, 4)) +function main(stdout: Stdout) returns nothing: + list[Record] values = collect[Record](item, view stdout) + Stdout.write(view stdout, "{list.length(view values)}") diff --git a/tests/native/indirect_owned_results.jett b/tests/native/indirect_owned_results.jett new file mode 100644 index 00000000..b83c4b1c --- /dev/null +++ b/tests/native/indirect_owned_results.jett @@ -0,0 +1,75 @@ +namespace app +struct Record: + label: string +enum Event: + empty + named(label: string) +bitfield Header: + code: 8 bits +machine Session: + states: + active(label: string) + transitions: + active to active +function record() returns Record: + return Record(label: "record") +function event() returns Event: + return Event.named("event") +function header() returns Header: + return Header(code: 7) +function session() returns Session: + return Session(active, "session") +function sequence() returns list[string]: + return list("sequence") +function members() returns set[string]: + return set.add[string](set.new[string](), "member") +function mapping() returns map[string, string]: + return map("key": "value") +function raw() returns bytes: + return bytes.from_string("raw") +function maybe() returns optional[string]: + return some("maybe") +function outcome() returns result[string, string]: + return ok("outcome") +function builder() returns TypeConstruction: + return type.construct_start[Record]() +function collect[T](factory: function() returns T) returns list[T]: + return list(factory(), factory(), factory(), factory()) +function verify_records() returns bool: + list[Record] values = collect[Record](record) + for value in values: + if value.label != "record": + return false + return true +function verify_events() returns bool: + list[Event] values = collect[Event](event) + for value in values: + match value: + named(label): + if label != "event": + return false + empty: + return false + return true +function verify_sums() returns bool: + list[optional[string]] maybe_values = collect[optional[string]](maybe) + list[result[string, string]] outcomes = collect[result[string, string]](outcome) + for value in maybe_values: + string text = value handle: return false + if text != "maybe": + return false + for value in outcomes: + string text = value handle error: return false + if text != "outcome": + return false + return true +function main(stdout: Stdout) returns nothing: + list[Header] headers = collect[Header](header) + list[Session] sessions = collect[Session](session) + list[list[string]] sequences = collect[list[string]](sequence) + list[set[string]] sets = collect[set[string]](members) + list[map[string, string]] maps = collect[map[string, string]](mapping) + list[bytes] bytes_values = collect[bytes](raw) + list[TypeConstruction] builders = collect[TypeConstruction](builder) + Stdout.write(view stdout, "{verify_records()} {verify_events()} {verify_sums()}\n") + Stdout.write(view stdout, "{list.length(view headers)} {list.length(view sessions)} {list.length(view sequences)} {list.length(view sets)} {list.length(view maps)} {list.length(view bytes_values)} {list.length(view builders)}\n") diff --git a/tests/native/inline_functions.jett b/tests/native/inline_functions.jett new file mode 100644 index 00000000..0c1d762b --- /dev/null +++ b/tests/native/inline_functions.jett @@ -0,0 +1,24 @@ +namespace app +function triple() returns function(int64) returns int64: + return function(value: int64) returns int64: return value * 3 +function main() returns nothing: + list[int64] selected = list.filter[int64](list(1, 2, 3, 4), function(value: int64) returns bool: return value > 2) + println(list.length[int64](selected)) + function(int64) returns int64 mapper = function(value: int64) returns int64: + int64 product = value * 2 + return product + list[int64] doubled = list.map[int64, int64](list(2, 4), mapper) + int64 last = list.get[int64](doubled, 1) handle: + default 0 + println(last) + list[string] labels = list.map[int64, string](list(1, 2), function(value: int64) returns string: return "v{value}") + string label = list.get[string](view labels, 1) handle: + default "missing" + println(label) + function(int64) returns int64 callback = triple() + println(callback(5)) + function() returns int64 seven = function() returns int64: return 7 + println(seven()) + function() returns function() returns int64 make = function() returns function() returns int64: return function() returns int64: return 9 + function() returns int64 nested = make() + println(nested()) diff --git a/tests/native/interface_collection_identity.jett b/tests/native/interface_collection_identity.jett new file mode 100644 index 00000000..f04ba7db --- /dev/null +++ b/tests/native/interface_collection_identity.jett @@ -0,0 +1,172 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Numbers = list[int64] +type NonEmpty = list[int64] where list.length[int64](view value) > 0 +implement Named for Numbers: + function name(view self: Numbers) returns string: + return "numbers:{list.length[int64](view self)}" +implement Named for list[string]: + function name(view self: list[string]) returns string: + return "words:{list.length[string](view self)}" +implement Named for list[secret[string]]: + function name(view self: list[secret[string]]) returns string: + return "secrets:{list.length[secret[string]](view self)}" +implement Named for NonEmpty: + function name(view self: NonEmpty) returns string: + list[int64] items = coarsen self + return "nonempty:{list.length[int64](view items)}" +implement Named for map[string, int64]: + function name(view self: map[string, int64]) returns string: + return "number-map:{map.length[string, int64](view self)}" +implement Named for map[string, string]: + function name(view self: map[string, string]) returns string: + return "word-map:{map.length[string, string](view self)}" +implement Named for set[int64]: + function name(view self: set[int64]) returns string: + return "number-set:{set.length[int64](view self)}" +implement Named for set[string]: + function name(view self: set[string]) returns string: + return "word-set:{set.length[string](view self)}" +implement Named for optional[int64]: + function name(view self: optional[int64]) returns string: + int64 found = clone self handle: + return "no-number" + return "maybe-number:{found}" +implement Named for optional[string]: + function name(view self: optional[string]) returns string: + string found = clone self handle: + return "no-word" + return "maybe-word:{found}" +implement Named for result[int64, string]: + function name(view self: result[int64, string]) returns string: + int64 found = clone self handle error: + return "number-fail:{error}" + return "number-ok:{found}" +implement Named for result[string, int64]: + function name(view self: result[string, int64]) returns string: + string found = clone self handle error: + return "word-fail:{error}" + return "word-ok:{found}" +implement Named for list[Named]: + function name(view self: list[Named]) returns string: + return "interfaces:{list.length[Named](view self)}" +struct Record: + count: int64 +type Counted = Record where value.count > 0 +implement Named for Record: + function name(view self: Record) returns string: + return "record:{self.count}" +implement Named for Counted: + function name(view self: Counted) returns string: + Record plain = coarsen self + return "counted:{plain.count}" +function show(view value: Named) returns string: + return Named.name(view value) +function mixed() returns list[Named]: + list[int64] numbers = list(1, 2) + list[string] words = list("a", "b") + list[secret[string]] hidden = list("hidden-collection-secret") + NonEmpty checked = list(3, 4, 5) handle error: + return list() + map[string, int64] number_map = map("a": 1) + map[string, string] word_map = map("a": "b", "c": "d") + set[int64] number_set = set.add[int64](set.new[int64](), 3) + set[string] word_set = set.add[string](set.new[string](), "word") + optional[int64] number_some = some(7) + optional[string] word_some = some("hello") + optional[int64] number_none = none + optional[string] word_none = none + result[int64, string] number_ok = ok(9) + result[string, int64] word_ok = ok("yes") + result[int64, string] number_fail = fail("bad") + result[string, int64] word_fail = fail(11) + return list(numbers, words, hidden, checked, number_map, word_map, number_set, word_set, number_some, word_some, number_none, word_none, number_ok, word_ok, number_fail, word_fail) +function numbers() returns Named: + return list(1, 2) +function callback() returns function() returns Named: + return function() returns Named: + return numbers() +struct Holder: + item: Named +function converted() returns list[Named]: + list[list[int64]] source = list(list(1), list(2, 3)) + list[Named] erased = source + return list.append[Named](erased, list("last")) +function counted() returns Named: + Counted value = Record(count: 4) handle error: + return Record(count: 0) + return value +struct CollectionHolder: + items: list[int64] +function reflected() returns Named: + CollectionHolder source = CollectionHolder(items: list(5, 6, 7)) + mutable TypeConstruction builder = type.construct_start[CollectionHolder]() + for field in type.fields[CollectionHolder](): + comptime type Field = field.type_info: + Field value = type.field_value[CollectionHolder, Field](view source, view field) + builder = type.construct_put[CollectionHolder, Field](builder, view field, value) handle error: + return Record(count: 0) + CollectionHolder built = type.construct_finish[CollectionHolder](builder) handle error: + return Record(count: 0) + return built.items +function concrete_operations() returns nothing: + list[int64] values = list.append[int64](list(2, 1), 3) + list[int64] sorted = list.sort[int64](values) + mutable int64 total = 0 + for value in sorted: + total = total + value + println(total) + map[string, int64] entries = map.insert[string, int64](map("a": 1), "b", 2) + int64 mapped = map.get[string, int64](entries, "b") handle: + return nothing + println(mapped) + set[int64] items = set.add[int64](set.new[int64](), 9) + for value in items: + println(value) + result[int64, string] later = run ok(17) + int64 found = join later handle error: + return nothing + println(found) + return nothing +function main() returns nothing: + for value in mixed(): + println(show(view value)) + list[Named] baked = comptime mixed() + trace baked + for value in baked: + println(show(view value)) + Named direct = numbers() + Named copied = clone direct + println(show(view copied)) + Holder holder = Holder(item: numbers()) + println(show(view holder.item)) + function() returns Named live = callback() + function() returns Named frozen = comptime callback() + println(show(view live())) + println(show(view frozen())) + Named pending = run numbers() + Named ready = join pending handle error: + return nothing + println(show(view ready)) + list[Named] converted_values = converted() + println(show(view converted_values)) + for value in converted_values: + println(show(view value)) + Named converted_baked = comptime converted() + println(show(view converted_baked)) + Named nominal = counted() + Named nominal_baked = comptime counted() + println(show(view nominal)) + println(show(view nominal_baked)) + Named reflected_value = reflected() + Named reflected_baked = comptime reflected() + println(show(view reflected_value)) + println(show(view reflected_baked)) + list[int64] alias_value = list(1, 2, 3, 4) + println(Numbers.name(view alias_value)) + function(view Numbers) returns string reader = Numbers.name + println(reader(view alias_value)) + concrete_operations() + trace holder + return nothing diff --git a/tests/native/interface_display_contexts.jett b/tests/native/interface_display_contexts.jett new file mode 100644 index 00000000..b765e0a8 --- /dev/null +++ b/tests/native/interface_display_contexts.jett @@ -0,0 +1,68 @@ +interface Displayable: + function display(view self: Displayable) returns string +namespace domain +export interface Named: + function name(view self: Named) returns string +export struct Record: + name: string +implement Named for int64: + function name(view self: int64) returns string: + return "number:{self}" +implement Named for Record: + function name(view self: Record) returns string: + return "record:{self.name}" +implement Displayable for Record: + function display(view self: Record) returns string: + return "concrete:{self.name}" +implement Displayable for Named: + function display(view self: Named) returns string: + return "erased:{Named.name(view self)}" +implement Displayable for bool: + function display(view self: bool) returns string: + if self: + return "enabled" + return "disabled" +export function make() returns Named: + return Record(name: "returned") +export function baked() returns string: + Named value = make() + return "{value}" +namespace app +struct Holder: + value: domain.Named +function format[T](view value: T) returns string: + return "{value}" +function marked() returns domain.Named: + use domain + println("evaluated") + return domain.make() +function scoped() returns nothing: + use domain + comptime type Bound = type.info[domain.Named](): + Bound value = domain.make() + println("scoped:{value}") + return nothing +function main() returns nothing: + use domain + domain.Named number = 7 + domain.Record record = domain.Record(name: "Ada") + domain.Named erased = clone record + println("{number}") + println("{record}") + println("{erased}") + println(format[domain.Record](view record)) + println(format[domain.Named](view number)) + println(format[domain.Named](view erased)) + Holder stored = Holder(value: erased) + println("field:{stored.value}") + println("call:{domain.make()}") + println("once:{marked()}") + println(domain.baked()) + string baked = comptime domain.baked() + println(baked) + scoped() + println(domain.Named.name(view number), record.name) + println("{true}") + string flag = comptime "{false}" + println(flag) + return nothing diff --git a/tests/native/interface_display_failure.jett b/tests/native/interface_display_failure.jett new file mode 100644 index 00000000..c2602445 --- /dev/null +++ b/tests/native/interface_display_failure.jett @@ -0,0 +1,24 @@ +interface Displayable: + function display(view self: Displayable) returns string +namespace app +interface Named: + function name(view self: Named) returns string +struct Record: + label: string + hidden: secret[string] +implement Named for Record: + function name(view self: Record) returns string: + return self.label +implement Displayable for Named: + function display(view self: Named) returns string: + return Named.name(view self) +function make() returns Named: + Record source = Record(label: "visible", hidden: "hidden-display-failure") + Named value = source + return run value +function prefix() returns string: + println("prefix") + return "allocated-prefix" +function main() returns nothing: + println("{prefix()}:{make()}:unreached") + return nothing diff --git a/tests/native/interface_explicit_comparison.jett b/tests/native/interface_explicit_comparison.jett new file mode 100644 index 00000000..57b78360 --- /dev/null +++ b/tests/native/interface_explicit_comparison.jett @@ -0,0 +1,45 @@ +namespace app +interface Keyed: + function key(view self: Keyed) returns string +implement Keyed for int64: + function key(view self: int64) returns string: + return string.from_int64(self) +implement Keyed for string: + function key(view self: string) returns string: + return self +struct Record: + id: string + ignored: Keyed +implement Equatable for Record: + function equals(view self: Record, view other: Record) returns bool: + return self.id == other.id +enum Entry: + record(value: Record) +function same_key(view left: Keyed, view right: Keyed) returns bool: + function(view Keyed) returns string key = Keyed.key + return key(view left) == key(view right) +function concrete_comparison() returns bool: + Record left = Record(id: "7", ignored: 7) + Record right = Record(id: "7", ignored: "different payload") + bool same = left == right + bool different = left != right + return same and not different and left.id == "7" and right.id == "7" +function nested_comparison() returns bool: + Entry left = Entry.record(Record(id: "7", ignored: 7)) + Entry right = Entry.record(Record(id: "7", ignored: "different payload")) + return left == right +function main(stdout: Stdout) returns nothing: + Keyed left = 7 + Keyed right = "7" + Keyed other = "8" + Stdout.write(view stdout, "{same_key(view left, view right)} {same_key(view left, view other)}\n") + Stdout.write(view stdout, "{concrete_comparison()} {nested_comparison()}\n") + Stdout.write(view stdout, "{Keyed.key(view left)} {Keyed.key(view right)} {Keyed.key(view other)}\n") +verify concrete_equality_and_explicit_domain_comparison: + Keyed left = 7 + Keyed right = "7" + assert same_key(view left, view right) + assert Keyed.key(view left) == "7" + assert Keyed.key(view right) == "7" + assert concrete_comparison() + assert nested_comparison() diff --git a/tests/native/interface_facade_identity.jett b/tests/native/interface_facade_identity.jett new file mode 100644 index 00000000..d447e450 --- /dev/null +++ b/tests/native/interface_facade_identity.jett @@ -0,0 +1,92 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +implement Named for int64: + function name(view self: int64) returns string: + return "wide:{self}" +implement Named for float32: + function name(view self: float32) returns string: + return "float32:{self}" +implement Named for float64: + function name(view self: float64) returns string: + return "float64:{self}" +implement Named for list[int8]: + function name(view self: list[int8]) returns string: + return "small-list:{list.length[int8](view self)}" +implement Named for list[int64]: + function name(view self: list[int64]) returns string: + return "wide-list:{list.length[int64](view self)}" +type Positive = int8 where value > 0 +implement Named for Positive: + function name(view self: Positive) returns string: + return "positive:{coarsen self}" +struct Box[T]: + item: T +implement Named for Box[int8]: + function name(view self: Box[int8]) returns string: + return "small-box:{self.item}" +implement Named for Box[secret[string]]: + function name(view self: Box[secret[string]]) returns string: + return "secret-box" +enum Event: + idle + named(count: int8) +implement Named for Event: + function name(view self: Event) returns string: + match self: + idle: + return "idle" + named(count): + return "event:{count}" +bitfield Header: + count: 8 bits +implement Named for Header: + function name(view self: Header) returns string: + return "header:{self.count}" +machine Session: + states: + active(count: int8) + transitions: + active to active +implement Named for Session: + function name(view self: Session) returns string: + if self at active: + return "session:{self.count}" + return "unexpected" +implement Named for Session at active: + function name(view self: Session at active) returns string: + return "active:{self.count}" +function parsed[T](text: string) returns Named: + T value = json.parse[T](text) handle error: + return 0 + return value +function roundtrip[T](value: T) returns Named: + string text = json.serialize[T](view value) + return parsed[T](text) +function mixed() returns list[Named]: + Session bare = Session(active, 17) + return list(parsed[int8]("7"), parsed[int64]("7"), parsed[float32]("1.5"), parsed[float64]("1.5"), parsed[list[int8]]("[1,2]"), parsed[list[int64]]("[1,2]"), parsed[Positive]("9"), parsed[Box[int8]]("{{\"item\":11}}"), parsed[Box[secret[string]]]("{{\"item\":\"hidden-json-interface\"}}"), roundtrip[Event](Event.named(13)), roundtrip[Header](Header(count: 15)), roundtrip[Session](bare), roundtrip[Session at active](Session(active, 19))) +function math_values() returns list[Named]: + return list(math.abs(-3), math.abs(-3.5), math.min(5, 6), math.max(5.0, 6.0)) +function main() returns nothing: + list[Named] live = mixed() + trace live + for value in live: + println(Named.name(view value)) + list[Named] baked = comptime mixed() + trace baked + for value in baked: + println(Named.name(view value)) + list[int8] elements = json.parse[list[int8]]("[21,23]") handle error: + return nothing + for element in elements: + println(Named.name(view element)) + for value in math_values(): + println(Named.name(view value)) + list[Named] math_baked = comptime math_values() + for value in math_baked: + println(Named.name(view value)) + return nothing diff --git a/tests/native/interface_facade_results.jett b/tests/native/interface_facade_results.jett new file mode 100644 index 00000000..78155a52 --- /dev/null +++ b/tests/native/interface_facade_results.jett @@ -0,0 +1,58 @@ +namespace app +interface Named: + function name(view self: Named) returns string +function describe[T](view source: result[T, string]) returns string: + T value = clone source handle error: + return "fail:{error}" + return "ok" +implement Named for result[int8, string]: + function name(view self: result[int8, string]) returns string: + return "small:{describe[int8](view self)}" +implement Named for result[int64, string]: + function name(view self: result[int64, string]) returns string: + return "wide:{describe[int64](view self)}" +implement Named for result[float32, string]: + function name(view self: result[float32, string]) returns string: + return "float32:{describe[float32](view self)}" +type Positive = int8 where value > 0 +implement Named for result[Positive, string]: + function name(view self: result[Positive, string]) returns string: + return "positive:{describe[Positive](view self)}" +struct Record: + count: int8 + token: secret[string] +implement Named for result[Record, string]: + function name(view self: result[Record, string]) returns string: + return "record:{describe[Record](view self)}" +implement Named for result[json.JsonTree, string]: + function name(view self: result[json.JsonTree, string]) returns string: + return "tree:{describe[json.JsonTree](view self)}" +implement Named for result[optional[json.JsonTree], string]: + function name(view self: result[optional[json.JsonTree], string]) returns string: + return "maybe-tree:{describe[optional[json.JsonTree]](view self)}" +function typed_results() returns list[Named]: + return list(json.parse[int8]("7"), json.parse[int8]("128"), json.parse[int64]("128"), json.parse[int64]("null"), json.parse[float32]("1.5"), json.parse[float32]("false"), json.parse[Positive]("9"), json.parse[Positive]("0"), json.parse[Record]("{{\"count\":11,\"token\":\"hidden-facade-result\"}}"), json.parse[Record]("{{\"count\":128,\"token\":\"hidden-facade-result\"}}"), json.parse_exact[Record]("{{\"count\":7,\"token\":\"hidden-facade-result\",\"extra\":1}}")) +function raw_results() returns list[Named]: + json.JsonTree text = json.parse_raw("\"wrong\"") handle error: + return list() + json.JsonTree empty = json.parse_raw("[]") handle error: + return list() + return list(json.parse_raw("["), json.as_int64(view text), json.require_index(view empty, 0), json.array_index(view empty, 0), json.object_field(view empty, "missing")) +function main() returns nothing: + list[Named] live = typed_results() + trace live + for item in live: + println(Named.name(view item)) + list[Named] baked = comptime typed_results() + trace baked + for item in baked: + println(Named.name(view item)) + list[Named] raw = raw_results() + trace raw + for item in raw: + println(Named.name(view item)) + list[Named] baked_raw = comptime raw_results() + trace baked_raw + for item in baked_raw: + println(Named.name(view item)) + return nothing diff --git a/tests/native/interface_function_identity.jett b/tests/native/interface_function_identity.jett new file mode 100644 index 00000000..e0bccb38 --- /dev/null +++ b/tests/native/interface_function_identity.jett @@ -0,0 +1,155 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Reader = function() returns int64 +type Words = function() returns string +type Items = function() returns list[int64] +type Borrowed = function(view string) returns string +type Owned = function(string) returns string +type Nested = function(function() returns int64) returns int64 +type Small = function() returns int8 +implement Named for Reader: + function name(view self: Reader) returns string: + return "reader:{self()}" +implement Named for Words: + function name(view self: Words) returns string: + return "words:{self()}" +implement Named for Items: + function name(view self: Items) returns string: + list[int64] values = self() + return "items:{list.length[int64](view values)}" +implement Named for Borrowed: + function name(view self: Borrowed) returns string: + string argument = "borrowed" + return self(view argument) +implement Named for Owned: + function name(view self: Owned) returns string: + return self("owned") +function seven() returns int64: + return 7 +implement Named for Nested: + function name(view self: Nested) returns string: + return "nested:{self(seven)}" +implement Named for Small: + function name(view self: Small) returns string: + return "small:{self()}" +function captured[T](value: T) returns function() returns T: + return function() returns T: + return clone value +function read() returns Reader: + int64 captured = 11 + return function() returns int64: + return captured +function words() returns string: + return "hello" +function items() returns list[int64]: + return list(1, 2, 3) +function borrowed(view value: string) returns string: + return clone value +function owned(value: string) returns string: + return value +function nested(value: Reader) returns int64: + return value() + 1 +function mixed() returns list[Named]: + Small small = captured[int8](17) + Reader wide = captured[int64](19) + return list(seven, read(), words, items, borrowed, owned, nested, small, wide) +function show(view value: Named) returns string: + return Named.name(view value) +struct Holder: + callback: Reader +function reflected() returns Named: + Holder source = Holder(callback: read()) + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view source, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return seven + Holder built = type.construct_finish[Holder](builder) handle error: + return seven + return built.callback +struct Label: + text: string +implement Named for Label: + function name(view self: Label) returns string: + return self.text +type Broad = function(view Named) returns string +type Narrow = function(view Label) returns string +implement Named for Broad: + function name(view self: Broad) returns string: + Label value = Label(text: "broad") + return self(view value) +implement Named for Narrow: + function name(view self: Narrow) returns string: + Label value = Label(text: "narrow") + return self(view value) +function broad() returns Broad: + int64 captured = 13 + return function(view value: Named) returns string: + return "{Named.name(view value)}:{captured}" +function narrowed() returns Narrow: + return broad() +type LabelFactory = function() returns Label +type NamedFactory = function() returns Named +implement Named for LabelFactory: + function name(view self: LabelFactory) returns string: + Label value = self() + return "label-factory:{value.text}" +implement Named for NamedFactory: + function name(view self: NamedFactory) returns string: + Named value = self() + return "named-factory:{Named.name(view value)}" +function label_factory() returns LabelFactory: + return function() returns Label: + return Label(text: "made") +function erased_factory() returns NamedFactory: + return label_factory() +function factories() returns nothing: + LabelFactory original = label_factory() + NamedFactory converted = erased_factory() + NamedFactory baked = comptime erased_factory() + println(show(view original)) + println(show(view converted)) + println(show(view baked)) + Named erased = comptime erased_factory() + println(show(view erased)) + Named called = baked() + println(show(view called)) + return nothing +function main() returns nothing: + for value in mixed(): + println(show(view value)) + list[Named] baked = comptime mixed() + trace baked + for value in baked: + println(show(view value)) + Named direct = read() + Named copied = clone direct + println(show(view copied)) + Reader live = read() + Reader frozen = comptime read() + println(live()) + println(frozen()) + Reader later = run read() + Reader ready = join later handle error: + return nothing + println(show(view ready)) + Named reflected_value = reflected() + Named reflected_baked = comptime reflected() + println(show(view reflected_value)) + println(show(view reflected_baked)) + Broad wide = broad() + Narrow narrow = narrowed() + Narrow baked_narrow = comptime narrowed() + println(show(view wide)) + println(show(view narrow)) + println(show(view baked_narrow)) + Label label = Label(text: "called") + println(narrow(view label)) + println(baked_narrow(view label)) + Named erased_narrow = comptime narrowed() + println(show(view erased_narrow)) + trace erased_narrow + factories() + return nothing diff --git a/tests/native/interface_math_facade_contexts.jett b/tests/native/interface_math_facade_contexts.jett new file mode 100644 index 00000000..fb8d9b86 --- /dev/null +++ b/tests/native/interface_math_facade_contexts.jett @@ -0,0 +1,54 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "integer:{self}" +implement Named for float64: + function name(view self: float64) returns string: + return "fraction:{self}" +implement Named for secret[int64]: + function name(view self: secret[int64]) returns string: + return "hidden-integer" +implement Named for secret[float64]: + function name(view self: secret[float64]) returns string: + return "hidden-fraction" +function primitive_facades() returns list[Named]: + return list(math.abs(-3), math.abs(-1.25), math.min(1, 2), math.min(1.25, 2.5), math.max(1, 2), math.max(1.25, 2.5)) +function secret_facades() returns list[Named]: + secret[int64] integer = -4 + secret[float64] fraction = -1.25 + return list(math.abs(clone integer), math.abs(clone fraction), math.min(clone integer, 1), math.min(clone fraction, 1.25), math.max(1, clone integer), math.max(1.25, clone fraction)) +function maximum[T](first: T, second: T) returns Named: + return math.max(first, second) +function factory[T](value: T) returns function() returns Named: + return function() returns Named: + return math.max(value, value) +function closures() returns list[Named]: + function() returns Named first = factory[int64](3) + function() returns Named second = factory[float64](1.25) + return list(first(), second(), maximum[secret[int64]](4, 4), maximum[secret[float64]](1.25, 1.25)) +function baked_closures() returns list[Named]: + function() returns Named first = comptime factory[int64](3) + function() returns Named second = comptime factory[float64](1.25) + return list(first(), second(), maximum[secret[int64]](4, 4), maximum[secret[float64]](1.25, 1.25)) +function describe(view values: list[Named]) returns string: + mutable string output = "" + for value in view values: + output = "{output}{Named.name(view value)};" + return "{output}\n" +function main(stdout: Stdout) returns nothing: + list[Named] primitive = primitive_facades() + list[Named] hidden = secret_facades() + list[Named] callbacks = closures() + list[Named] baked_primitive = comptime primitive_facades() + list[Named] baked_hidden = comptime secret_facades() + list[Named] baked_callbacks = comptime closures() + list[Named] materialized = baked_closures() + Stdout.write(view stdout, describe(view primitive)) + Stdout.write(view stdout, describe(view hidden)) + Stdout.write(view stdout, describe(view callbacks)) + Stdout.write(view stdout, describe(view baked_primitive)) + Stdout.write(view stdout, describe(view baked_hidden)) + Stdout.write(view stdout, describe(view baked_callbacks)) + Stdout.write(view stdout, describe(view materialized)) diff --git a/tests/native/interface_method_returns.jett b/tests/native/interface_method_returns.jett new file mode 100644 index 00000000..be23bcb7 --- /dev/null +++ b/tests/native/interface_method_returns.jett @@ -0,0 +1,63 @@ +namespace app +interface Named: + function name(view self: Named) returns string + function copy(view self: Named) returns Named + function copies(view self: Named) returns list[Named] + function producer(view self: Named) returns function() returns Named +implement Named for int64: + function name(view self: int64) returns string: + return "number:{self}" + function copy(view self: int64) returns Named: + return self + function copies(view self: int64) returns list[Named]: + return list(self, self + 1) + function producer(view self: int64) returns function() returns Named: + return function() returns Named: return self +implement Named for string: + function name(view self: string) returns string: + return "text:{self}" + function copy(view self: string) returns Named: + return self + function copies(view self: string) returns list[Named]: + return list(self, "{self}!") + function producer(view self: string) returns function() returns Named: + return function() returns Named: return self +implement Named for int8: + function name(view self: int8) returns string: + return "tiny:{self}" + function copy(view self: int8) returns Named: + return self + function copies(view self: int8) returns list[Named]: + return list(self, self + 1) + function producer(view self: int8) returns function() returns Named: + return function() returns Named: return self +function callbacks() returns list[function() returns Named]: + Named number = 13 + Named text = "baked" + int8 small = 127 + Named tiny = small + return list(Named.producer(view number), Named.producer(view text), Named.producer(view tiny)) +function show_callbacks(values: list[function() returns Named]) returns nothing: + for callback in values: + Named produced = callback() + println(Named.name(view produced)) + return nothing +function exercise() returns nothing: + int8 small = 127 + list[Named] values = list(7, "hello", small) + for value in values: + Named copied = Named.copy(view value) + println(Named.name(view copied)) + list[Named] many = Named.copies(view value) + for item in many: + println(Named.name(view item)) + function() returns Named callback = Named.producer(view value) + Named produced = callback() + println(Named.name(view produced)) + return nothing +function main() returns nothing: + exercise() + show_callbacks(callbacks()) + list[function() returns Named] baked = comptime callbacks() + show_callbacks(baked) + return nothing diff --git a/tests/native/interface_method_values.jett b/tests/native/interface_method_values.jett new file mode 100644 index 00000000..2ff12f7a --- /dev/null +++ b/tests/native/interface_method_values.jett @@ -0,0 +1,163 @@ +namespace callback_library +export interface Reader: + function read(view self: Reader) returns int64 + function offset(view self: Reader, delta: int64) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 + function offset(view self: int64, delta: int64) returns int64: + return self + 1 + delta +implement Reader for int8: + function read(view self: int8) returns int64: + return 107 + function offset(view self: int8, delta: int64) returns int64: + return 107 + delta +export struct Counter: + value: int64 + function read(view self: Counter) returns int64: + return self.value + 500 +implement Reader for Counter: + function read(view self: Counter) returns int64: + return self.value * 2 + function offset(view self: Counter, delta: int64) returns int64: + return self.value * 2 + delta +export type Selected = Reader where true +implement Reader for Selected: + function read(view self: Selected) returns int64: + Reader base = coarsen clone self + function(view Reader) returns int64 callback = Reader.read + return callback(view base) + 1000 + function offset(view self: Selected, delta: int64) returns int64: + Reader base = coarsen clone self + function(view Reader, int64) returns int64 callback = Reader.offset + return callback(view base, delta) + 1000 +export type Readable = Reader +export struct Holder: + callback: function(view Reader) returns int64 +export interface Reporter: + function emit(view self: Reporter, view stdout: Stdout) returns nothing +implement Reporter for Counter: + function emit(view self: Counter, view stdout: Stdout) returns nothing: + Stdout.write(view stdout, "capability:{self.value}\n") +export interface Absent: + function read(view self: Absent) returns int64 +export function choose() returns function(view Reader) returns int64: + return Reader.read +export function generic_choose[T](unused: T) returns function(view Reader) returns int64: + return Reader.read +export function make_holder() returns Holder: + return Holder(callback: Reader.read) +export function closed_read() returns int64: + Reader source = 7 + function(view Reader) returns int64 callback = Reader.read + return callback(view source) +export function seed() returns Selected: + Reader source = 7 + Selected selected = source handle error: + return seed() + return selected +export function apply[T](callback: function(view T) returns int64, view source: T) returns int64: + return callback(view source) +namespace collision_library +export interface Reader: + function read(view self: Reader) returns int64 +export interface Other: + function read(view self: Other) returns int64 +implement Reader for int64: + function read(view self: int64) returns int64: + return self + 1 +implement Other for Reader: + function read(view self: Reader) returns int64: + return 99 +namespace app +function receiver_effect(view stdout: Stdout) returns callback_library.Reader: + Stdout.write(view stdout, "receiver\n") + return 7 +function factory_effect(view stdout: Stdout) returns function(view callback_library.Reader) returns int64: + use callback_library + Stdout.write(view stdout, "factory\n") + return callback_library.Reader.read +function alias_choice[Reader](unused: Reader) returns function(view callback_library.Reader) returns int64: + use callback_library + return callback_library.Readable.read +function main(stdout: Stdout) returns nothing: + use callback_library + use callback_library as library + use collision_library + use collision_library as collision + function(view library.Readable) returns int64 callback = library.Readable.read + function(view library.Reader, int64) returns int64 offset = library.Reader.offset + library.Reader wide = 7 + int8 small = 7 + library.Reader tiny = small + library.Counter concrete = library.Counter(value: 9) + library.Reader record = clone concrete + library.Selected selected = library.seed() + library.Reader refined = clone selected + library.Reader base = coarsen selected + Stdout.write(view stdout, "owners:{callback(view wide)}:{callback(view tiny)}:{callback(view record)}:{callback(view refined)}\n") + Stdout.write(view stdout, "slots:{offset(view wide, 5)}:{offset(view tiny, 5)}:{offset(view record, 5)}:{offset(view refined, 5)}\n") + Stdout.write(view stdout, "coarsen:{callback(view base)}\n") + function(view library.Reader) returns int64 returned = library.choose() + function(view library.Reader) returns int64 generic_int = library.generic_choose[int64](0) + function(view library.Reader) returns int64 generic_string = library.generic_choose[string]("unused") + function(view library.Reader) returns int64 copied = clone callback + library.Holder holder = library.Holder(callback: clone callback) + list[function(view library.Reader) returns int64] callbacks = list(clone callback) + function(view library.Reader) returns int64 listed = list.get[function(view library.Reader) returns int64](view callbacks, 0) handle: + default library.Reader.read + optional[function(view library.Reader) returns int64] absent = none + function(view library.Reader) returns int64 fallback = absent handle: + default library.Reader.read + Stdout.write(view stdout, "routes:{(library.Reader.read)(view wide)}:{returned(view wide)}:{generic_int(view wide)}:{generic_string(view wide)}:{copied(view wide)}:{holder.callback(view wide)}:{listed(view wide)}:{fallback(view wide)}\n") + function(view library.Counter) returns int64 narrow = library.Reader.read + Stdout.write(view stdout, "adapted:{narrow(view concrete)}:{library.apply[library.Counter](library.Reader.read, view concrete)}\n") + function(view library.Reader) returns int64 shadowed = alias_choice[int64](0) + Stdout.write(view stdout, "alias:{shadowed(view record)}\n") + function(view library.Reader) returns int64 baked = comptime callback_library.Reader.read + function(view library.Reader) returns int64 baked_returned = comptime callback_library.choose() + library.Holder baked_holder = comptime callback_library.make_holder() + list[function(view library.Reader) returns int64] baked_list = comptime list(callback_library.Reader.read) + function(view library.Reader) returns int64 baked_listed = list.get[function(view library.Reader) returns int64](view baked_list, 0) handle: + default library.Reader.read + int64 baked_call = comptime callback_library.closed_read() + Stdout.write(view stdout, "baked:{baked(view wide)}:{baked_returned(view wide)}:{baked_holder.callback(view wide)}:{baked_listed(view wide)}:{baked_call}\n") + function(view library.Absent) returns int64 unimplemented = library.Absent.read + trace callback + trace offset + trace baked + trace unimplemented + int64 ordered =(factory_effect(view stdout))(receiver_effect(view stdout)) + Stdout.write(view stdout, "ordered:{ordered}\n") + function(view library.Reporter, view Stdout) returns nothing emit = comptime callback_library.Reporter.emit + library.Reporter reporter = clone concrete + emit(view reporter, view stdout) + Stdout.write(view stdout, "reuse:{concrete.value}:{callback(view wide)}\n") + collision.Reader collision_receiver = 7 + function(view collision.Reader) returns int64 collision_callback = collision.Reader.read + function(view collision.Reader) returns int64 collision_baked = comptime collision_library.Reader.read + Stdout.write(view stdout, "collision:{collision_callback(view collision_receiver)}:{collision_baked(view collision_receiver)}:{collision.Reader.read(view collision_receiver)}\n") + return nothing +verify interface_callbacks: + use callback_library + use callback_library as library + library.Counter source = library.Counter(value: 9) + library.Reader erased = clone source + function(view library.Reader) returns int64 callback = library.Reader.read + function(view library.Reader) returns int64 baked = comptime callback_library.choose() + assert callback(view erased) == 18 + assert baked(view erased) == 18 + assert source.value == 9 + assert library.Counter.read(view source) == 509 + library.Holder holder = comptime callback_library.make_holder() + assert holder.callback(view erased) == 18 + assert callback(view erased) == 18 +property interface_callback_trials: + given input: int64 + use callback_library as library + library.Counter source = library.Counter(value: input) + library.Reader erased = clone source + function(view library.Reader) returns int64 callback = library.Reader.read + assert callback(view erased) == input * 2 + assert source.value == input + assert callback(view erased) == input * 2 diff --git a/tests/native/interface_nominal_identity.jett b/tests/native/interface_nominal_identity.jett new file mode 100644 index 00000000..2727b2b5 --- /dev/null +++ b/tests/native/interface_nominal_identity.jett @@ -0,0 +1,129 @@ +namespace app +interface Named: + function name(view self: Named) returns string +machine Session: + states: + empty + active(count: int64, token: secret[string]) + transitions: + empty to active +implement Named for Session: + function name(view self: Session) returns string: + if self at active: + return "session:{self.count}" + return "empty" +type ActiveState = Session at active +implement Named for ActiveState: + function name(view self: ActiveState) returns string: + return "active:{self.count}" +implement Named for Session at empty: + function name(view self: Session at empty) returns string: + return "empty-state" +type Active = Session where value at active +implement Named for Active: + function name(view self: Active) returns string: + Session plain = coarsen self + if plain at active: + return "refined-session:{plain.count}" + return "unexpected-empty" +enum Event: + idle + named(label: string, token: secret[string]) +function is_named(view value: Event) returns bool: + match value: + idle: + return false + named(label, token): + return true +type Tagged = Event where is_named(view value) +implement Named for Event: + function name(view self: Event) returns string: + match self: + idle: + return "idle" + named(label, token): + return "event:{label}" +implement Named for Tagged: + function name(view self: Tagged) returns string: + Event plain = coarsen self + match plain: + idle: + return "unexpected-idle" + named(label, token): + return "tagged:{label}" +bitfield Header: + kind: 8 bits +type NonzeroHeader = Header where value.kind > 0 +implement Named for Header: + function name(view self: Header) returns string: + return "header:{self.kind}" +implement Named for NonzeroHeader: + function name(view self: NonzeroHeader) returns string: + Header plain = coarsen self + return "nonzero-header:{plain.kind}" +function make() returns Session at active: + return Session(active, 7, "hidden-nominal-secret") +function refined_session() returns Named: + Session source = make() + Active value = source handle error: + return Event.idle + return value +function refined_event() returns Named: + Tagged value = Event.named("chosen", "hidden-nominal-secret") handle error: + return Event.idle + return value +function refined_header() returns Named: + NonzeroHeader value = Header(kind: 3) handle error: + return Event.idle + return value +function mixed() returns list[Named]: + Session plain = make() + return list(make(), plain, Session(empty), refined_session(), Event.idle, Event.named("normal", "hidden-nominal-secret"), refined_event(), Header(kind: 2), refined_header()) +function show(view value: Named) returns string: + return Named.name(view value) +struct Holder: + value: ActiveState +function reflected() returns Named: + Holder source = Holder(value: make()) + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view source, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return Event.idle + Holder built = type.construct_finish[Holder](builder) handle error: + return Event.idle + return built.value +function states() returns nothing: + Session at empty initial = Session(empty) + println(show(view initial)) + Session at active changed = Session.transition(initial, active, 9, "hidden-nominal-secret") + println(show(view changed)) + Session bare = changed + println(show(view bare)) + if bare at active: + println(show(view bare)) + Session at active later = run make() + Session at active ready = join later handle error: + return nothing + println(show(view ready)) + Named erased = ready + Named copied = clone erased + println(show(view copied)) + trace copied + Named reflected_value = reflected() + Named reflected_baked = comptime reflected() + println(show(view reflected_value)) + println(show(view reflected_baked)) + return nothing +function main() returns nothing: + list[Named] live = mixed() + trace live + for value in live: + println(show(view value)) + list[Named] baked = comptime mixed() + trace baked + for value in baked: + println(show(view value)) + states() + return nothing diff --git a/tests/native/interface_opaque_identity.jett b/tests/native/interface_opaque_identity.jett new file mode 100644 index 00000000..9c6e4d75 --- /dev/null +++ b/tests/native/interface_opaque_identity.jett @@ -0,0 +1,124 @@ +namespace app +interface Named: + function name(view self: Named) returns string +actor First: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +actor Second: + receive read responds int64: + respond 2 +implement Named for First: + function name(view self: First) returns string: + return "first" +implement Named for Second: + function name(view self: Second) returns string: + return "second" +struct ActorHolder: + worker: First +function actors() returns nothing: + First first = spawn First() + First copied = clone first + send copied.add(4) + println(Named.name(view first)) + println(ask first.read) + ActorHolder holder = ActorHolder(worker: copied) + println(Named.name(view holder.worker)) + println(ask holder.worker.read) + First later = run holder.worker + First ready = join later handle error: + return nothing + println(Named.name(view ready)) + send ready.add(2) + println(ask ready.read) + Second second = spawn Second() + println(ask second.read) + list[Named] mixed = list(first, second, ready) + for item in mixed: + println(Named.name(view item)) + return nothing +struct Record: + count: int64 +type CheckedBuilder = TypeConstruction where true +implement Named for TypeConstruction: + function name(view self: TypeConstruction) returns string: + return "builder" +implement Named for CheckedBuilder: + function name(view self: CheckedBuilder) returns string: + return "checked-builder" +struct BuilderHolder: + builder: CheckedBuilder +function builder(number: int64) returns optional[CheckedBuilder]: + TypeConstruction empty = type.construct_start[Record]() + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return none + TypeConstruction filled = type.construct_put[Record, int64](empty, view field, number) handle error: + return none + CheckedBuilder checked = filled handle error: + return none + return some(checked) +function finish(builder: CheckedBuilder) returns string: + Record value = type.construct_finish[Record](coarsen builder) handle error: + return error + return "record:{value.count}" +function erased_builder() returns Named: + CheckedBuilder checked = builder(15) handle: + return type.construct_start[Record]() + return checked +function mixed_builders() returns list[Named]: + return list(erased_builder(), type.construct_start[Record]()) +function builders() returns nothing: + CheckedBuilder runtime = builder(7) handle: + return nothing + CheckedBuilder baked = comptime builder(9) handle: + return nothing + println(Named.name(view runtime)) + println(Named.name(view baked)) + println(finish(clone runtime)) + println(finish(clone baked)) + TypeConstruction base = coarsen clone baked + println(Named.name(view base)) + BuilderHolder stored = BuilderHolder(builder: clone baked) handle error: + return nothing + println(Named.name(view stored.builder)) + println(finish(stored.builder)) + CheckedBuilder later = run clone baked + CheckedBuilder ready = join later handle error: + return nothing + println(Named.name(view ready)) + println(finish(ready)) + list[Named] mixed = list(runtime, baked, base) + for item in mixed: + println(Named.name(view item)) + list[Named] baked_mixed = comptime mixed_builders() + for item in baked_mixed: + println(Named.name(view item)) + return nothing +type Output = Stdout where true +implement Named for Stdout: + function name(view self: Stdout) returns string: + return "stdout" +implement Named for Output: + function name(view self: Output) returns string: + return "output" +function capabilities(view stdout: Stdout) returns nothing: + Output output = clone stdout handle error: + return nothing + println(Named.name(view output)) + Output later = run clone output + Output ready = join later handle error: + return nothing + println(Named.name(view ready)) + Stdout base = coarsen clone ready + Stdout.write(view base, "ready") + list[Named] mixed = list(output, ready, base) + for item in mixed: + println(Named.name(view item)) + return nothing +function main(stdout: Stdout) returns nothing: + actors() + builders() + capabilities(view stdout) + return nothing diff --git a/tests/native/interface_pending_failure.jett b/tests/native/interface_pending_failure.jett new file mode 100644 index 00000000..26f6e958 --- /dev/null +++ b/tests/native/interface_pending_failure.jett @@ -0,0 +1,12 @@ +interface Named: + function name(view self: Named) returns string +struct User: + label: string +implement Named for User: + function name(view self: User) returns string: + return self.label +function main() returns nothing: + Named item = run User(label: "owned payload") + trace item + println(Named.name(view item)) + return nothing diff --git a/tests/native/interface_primitive_identity.jett b/tests/native/interface_primitive_identity.jett new file mode 100644 index 00000000..8f428766 --- /dev/null +++ b/tests/native/interface_primitive_identity.jett @@ -0,0 +1,138 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Small = int8 +type Positive = int64 where value > 0 +type NonEmpty = string where value != "" +implement Named for Small: + function name(view self: Small) returns string: + return "int8:{self}" +implement Named for int64: + function name(view self: int64) returns string: + return "int64:{self}" +implement Named for uint8: + function name(view self: uint8) returns string: + return "uint8:{self}" +implement Named for uint64: + function name(view self: uint64) returns string: + return "uint64:{self}" +implement Named for float32: + function name(view self: float32) returns string: + return "float32" +implement Named for float64: + function name(view self: float64) returns string: + return "float64" +implement Named for int16: + function name(view self: int16) returns string: + return "int16:{self}" +implement Named for int32: + function name(view self: int32) returns string: + return "int32:{self}" +implement Named for uint16: + function name(view self: uint16) returns string: + return "uint16:{self}" +implement Named for uint32: + function name(view self: uint32) returns string: + return "uint32:{self}" +implement Named for Positive: + function name(view self: Positive) returns string: + return "positive:{coarsen self}" +implement Named for NonEmpty: + function name(view self: NonEmpty) returns string: + return "nonempty:{coarsen self}" +implement Named for string: + function name(view self: string) returns string: + return "string:{self}" +function show(view value: Named) returns string: + return Named.name(view value) +function narrow() returns Named: + int8 value = 7 + return value +function refined() returns Named: + Positive value = 7 handle error: + return "invalid" + return value +function callback() returns function() returns Named: + Small value = 8 + return function() returns Named: + return value +struct Holder: + value: Named +function mixed() returns list[Named]: + int8 small = 7 + int64 large = 7 + uint8 small_unsigned = 9 + uint64 large_unsigned = 9 + float32 narrow_float = 1.0 + float64 wide_float = 1.0 + int16 medium_signed = 15 + int32 larger_signed = 31 + uint16 medium_unsigned = 16 + uint32 larger_unsigned = 32 + Positive positive = 7 handle error: + return list() + NonEmpty nonempty = "yes" handle error: + return list() + string plain = "yes" + return list(small, large, small_unsigned, large_unsigned, narrow_float, wide_float, medium_signed, larger_signed, medium_unsigned, larger_unsigned, positive, nonempty, plain) +function literal_sequence() returns list[Named]: + list[int8] values = list(12, 13) + return values +function reflected() returns Named: + Holder original = Holder(value: narrow()) + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view original, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return "put failed" + Holder built = type.construct_finish[Holder](builder) handle error: + return "finish failed" + return built.value +function main() returns nothing: + for value in mixed(): + println(show(view value)) + list[Named] baked = comptime mixed() + for value in baked: + println(show(view value)) + Named direct = narrow() + Named copied = clone direct + println(show(view copied)) + Named baked_narrow = comptime narrow() + println(show(view baked_narrow)) + Named baked_refined = comptime refined() + println(show(view baked_refined)) + function() returns Named live_callback = callback() + function() returns Named baked_callback = comptime callback() + println(show(view live_callback())) + println(show(view baked_callback())) + Holder stored = Holder(value: narrow()) + println(show(view stored.value)) + Named pending = run narrow() + Named ready = join pending handle error: + return nothing + println(show(view ready)) + int8 small = 11 + list[int8] source = list(small) + list[Named] converted = source + list[Named] appended = list.append[Named](converted, small) + for value in appended: + println(show(view value)) + optional[int8] maybe = some(small) + optional[Named] erased = maybe + Named found = erased handle: + return nothing + println(show(view found)) + map[string, Named] entries = map("key": small) + Named mapped = map.get[string, Named](entries, "key") handle: + return nothing + println(show(view mapped)) + list[Named] literals = comptime literal_sequence() + for value in literals: + println(show(view value)) + Named reflected_value = reflected() + Named reflected_baked = comptime reflected() + println(show(view reflected_value)) + println(show(view reflected_baked)) + trace stored + return nothing diff --git a/tests/native/interface_refined_actors.jett b/tests/native/interface_refined_actors.jett new file mode 100644 index 00000000..3c0d7a74 --- /dev/null +++ b/tests/native/interface_refined_actors.jett @@ -0,0 +1,94 @@ +namespace app +interface Named: + function name(view self: Named) returns string +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +type Selected = Counter where true +type Again = Selected where true +implement Named for Counter: + function name(view self: Counter) returns string: + return "counter" +implement Named for Selected: + function name(view self: Selected) returns string: + return "selected" +implement Named for Again: + function name(view self: Again) returns string: + return "again" +struct Holder: + worker: Selected +function copy_holder(source: Holder) returns result[Holder, string]: + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view source, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return fail(error) + return type.construct_finish[Holder](builder) +function inspect(worker: Selected) returns int64: + Counter base = coarsen worker + send base.add(2) + return ask base.read +function internal() returns int64: + Counter base = spawn Counter() + Selected selected = base handle error: + return -1 + return inspect(selected) +function erased_tasks(worker: Selected) returns nothing: + Named erased = worker + Named later = run run erased + trace later + Named inner = join later handle error: + return println(error) + Named ready = join inner handle error: + return println(error) + println(Named.name(view ready)) + return nothing +type Rejected = Counter where false +struct RejectedHolder: + worker: Rejected +function reject_pending() returns nothing: + Counter pending = run spawn Counter() + RejectedHolder holder = RejectedHolder(worker: pending) handle error: + return println(error) + trace holder + return nothing +function main() returns nothing: + Counter base = spawn Counter() + Selected selected = clone base handle error: + return println(error) + Again again = clone selected handle error: + return println(error) + println(Named.name(view base), Named.name(view selected), Named.name(view again)) + Holder holder = Holder(worker: clone selected) handle error: + return println(error) + println(inspect(holder.worker)) + println(ask base.read) + Again later = run clone again + Again ready = join later handle error: + return println(error) + Selected parent = coarsen ready + println(Named.name(view parent), inspect(parent)) + list[Named] mixed = list(clone base, clone selected, again) + for item in mixed: + println(Named.name(view item)) + Selected pending = run run clone selected + Holder pending_holder = Holder(worker: pending) handle error: + return println(error) + Holder copied = copy_holder(pending_holder) handle error: + return println(error) + trace copied + Selected inner = join copied.worker handle error: + return println(error) + Selected restored = join inner handle error: + return println(error) + println(Named.name(view restored)) + println(inspect(restored)) + println(ask base.read) + erased_tasks(clone selected) + reject_pending() + println(internal(), comptime internal()) + return nothing diff --git a/tests/native/interface_refined_containers.jett b/tests/native/interface_refined_containers.jett new file mode 100644 index 00000000..486a6563 --- /dev/null +++ b/tests/native/interface_refined_containers.jett @@ -0,0 +1,70 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Mapping = map[string, int8] where true +type Members = set[int8] where true +type Maybe = optional[int8] where true +type Outcome = result[int8, string] where true +type Callback = function(int8) returns int8 where true +implement Named for Mapping: + function name(view self: Mapping) returns string: + map[string, int8] plain = coarsen self + return "mapping:{map.length[string, int8](view plain)}" +implement Named for Members: + function name(view self: Members) returns string: + set[int8] plain = coarsen self + return "members:{set.length[int8](view plain)}" +implement Named for Maybe: + function name(view self: Maybe) returns string: + int8 plain = coarsen self handle: + return "none" + return "some:{plain}" +implement Named for Outcome: + function name(view self: Outcome) returns string: + int8 plain = coarsen self handle error: + return "fail:{error}" + return "ok:{plain}" +implement Named for Callback: + function name(view self: Callback) returns string: + function(int8) returns int8 plain = coarsen self + return "callback:{plain(4)}" +implement Named for function(int8) returns int8: + function name(view self: function(int8) returns int8) returns string: + return "function:{self(4)}" +function increment(value: int8) returns int8: + return value + 1 +struct Values: + mapping: Mapping + members: Members + present: Maybe + absent: Maybe + success: Outcome + failure: Outcome + callback: Callback + captured: Callback +function mixed() returns list[Named]: + map[string, int8] source_map = map("a": 7) + set[int8] source_set = set.add[int8](set.new[int8](), 8) + optional[int8] source_present = some(9) + optional[int8] source_absent = none + result[int8, string] source_success = ok(11) + result[int8, string] source_failure = fail("problem") + int8 offset = 3 + function(int8) returns int8 closure = function(value: int8) returns int8: return value + offset + Values values = Values(mapping: source_map, members: source_set, present: source_present, absent: source_absent, success: source_success, failure: source_failure, callback: increment, captured: closure) handle error: + return list() + Callback later = run clone values.captured + Callback ready = join later handle error: + return list() + function(int8) returns int8 base = coarsen clone values.callback + return list(values.mapping, values.members, values.present, values.absent, values.success, values.failure, values.callback, values.captured, ready, base) +function main() returns nothing: + list[Named] live = mixed() + trace live + for item in live: + println(Named.name(view item)) + list[Named] baked = comptime mixed() + trace baked + for item in baked: + println(Named.name(view item)) + return nothing diff --git a/tests/native/interface_refined_interfaces.jett b/tests/native/interface_refined_interfaces.jett new file mode 100644 index 00000000..8ebf6f40 --- /dev/null +++ b/tests/native/interface_refined_interfaces.jett @@ -0,0 +1,162 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for string: + function name(view self: string) returns string: + return "text:{self}" +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +struct Record: + label: string + token: secret[string] +implement Named for Record: + function name(view self: Record) returns string: + return "record:{self.label}" +type Outcome = result[int8, string] +implement Named for Outcome: + function name(view self: Outcome) returns string: + int8 found = self handle error: + return "result-fail:{error}" + return "result-ok:{found}" +type Maybe = optional[int8] +implement Named for Maybe: + function name(view self: Maybe) returns string: + int8 found = self handle: + return "optional-none" + return "optional-some:{found}" +struct Empty: + code: int64 +implement Named for Empty: + function name(view self: Empty) returns string: + return "" +type Selected = Named where Named.name(view value) != "" +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +type Again = Selected where true +implement Named for Again: + function name(view self: Again) returns string: + Selected parent = coarsen clone self + return "again:{Named.name(view parent)}" +function select(value: Named) returns result[Selected, string]: + Selected selected = value handle error: + return fail(error) + return ok(selected) +function mixed() returns result[list[Named], string]: + Selected text = select("plain") handle error: + return fail(error) + int8 number = 7 + Selected small = select(number) handle error: + return fail(error) + Selected record = select(Record(label: "kept", token: "hidden-refined-interface")) handle error: + return fail(error) + Again again = clone text handle error: + return fail(error) + Named base = coarsen clone again + Selected parent = coarsen clone again + Outcome success = ok(9) + Outcome failure = fail("bad") + Maybe absent = none + Selected selected_success = select(success) handle error: + return fail(error) + Selected selected_failure = select(failure) handle error: + return fail(error) + Selected selected_absent = select(absent) handle error: + return fail(error) + return ok(list(text, small, record, again, base, parent, selected_success, selected_failure, selected_absent)) +function show(view value: Named) returns string: + return Named.name(view value) +struct Stored: + value: Selected +function constructed() returns result[Stored, string]: + Named source = "fresh" + return Stored(value: source) +type Rejected = Named where false +struct RejectedField: + value: Rejected +function pending_rejection() returns nothing: + Named pending = run "unchecked" + RejectedField accepted = RejectedField(value: pending) handle error: + return println(error) + trace accepted + println("unexpected pending acceptance") + return nothing +function collections(view original: Selected) returns nothing: + list[Selected] source = list(clone original) + list[Named] erased = source + for value in erased: + println(show(view value)) + map[string, Selected] entries = map("key": clone original) + map[string, Named] converted = entries + Named mapped = map.get[string, Named](converted, "key") handle: + return nothing + println(show(view mapped)) + optional[Selected] maybe = some(clone original) + optional[Named] converted_maybe = maybe + Named found = converted_maybe handle: + return nothing + println(show(view found)) + Stored stored = Stored(value: clone original) handle error: + return println(error) + println(show(view stored.value)) + trace stored + return nothing +function selected_callback() returns result[Selected, string]: + return select("callback") +function callbacks() returns nothing: + function() returns result[Named, string] live = selected_callback + function() returns result[Named, string] baked = comptime selected_callback + Named first = live() handle error: + return println(error) + Named second = baked() handle error: + return println(error) + println(show(view first)) + println(show(view second)) + return nothing +function tasks(view original: Selected) returns nothing: + Selected once = run clone original + Selected twice = run once + Named erased = twice + trace erased + Named still_pending = join erased handle error: + return println(error) + trace still_pending + Named ready = join still_pending handle error: + return println(error) + println(show(view ready)) + return nothing +function rejection(value: result[Selected, string]) returns nothing: + Selected accepted = value handle error: + return println(error) + println("unexpected:{show(view accepted)}") + return nothing +function main() returns nothing: + list[Named] live = mixed() handle error: + return println(error) + list[Named] baked = comptime mixed() handle error: + return println(error) + trace live + trace baked + for value in live: + println(show(view value)) + for value in baked: + println(show(view value)) + Selected original = select("direct") handle error: + return println(error) + println(Named.name(view original)) + println(show(view original)) + collections(view original) + callbacks() + tasks(view original) + rejection(select(Empty(code: 0))) + rejection(comptime select(Empty(code: 0))) + Stored fresh = constructed() handle error: + return println(error) + Stored baked_fresh = comptime constructed() handle error: + return println(error) + println(show(view fresh.value)) + println(show(view baked_fresh.value)) + pending_rejection() + return nothing diff --git a/tests/native/interface_refined_small_values.jett b/tests/native/interface_refined_small_values.jett new file mode 100644 index 00000000..ac283f7b --- /dev/null +++ b/tests/native/interface_refined_small_values.jett @@ -0,0 +1,85 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Unit = nothing where true +type Again = Unit where true +type Enabled = bool where value +type Data = bytes where bytes.length(value) > 0 +implement Named for nothing: + function name(view self: nothing) returns string: + return "nothing" +implement Named for Unit: + function name(view self: Unit) returns string: + return "unit" +implement Named for Again: + function name(view self: Again) returns string: + return "again" +implement Named for bool: + function name(view self: bool) returns string: + return "bool:{self}" +implement Named for Enabled: + function name(view self: Enabled) returns string: + return "enabled:{coarsen self}" +implement Named for bytes: + function name(view self: bytes) returns string: + return "bytes:{bytes.to_hex(view self)}" +implement Named for Data: + function name(view self: Data) returns string: + string text = bytes.to_hex(view coarsen self) + int64 length = bytes.length(view coarsen self) + if length == 0: + return "empty" + return "data:{text}" +struct Packet: + data: Data + hidden: secret[bytes] +function hex(view data: bytes) returns string: + return bytes.to_hex(view data) +function sized(view data: bytes, length: int64) returns string: + return "{bytes.to_hex(view data)}:{length}" +function borrowed() returns string: + Packet packet = Packet(data: bytes.from_string("ok"), hidden: bytes.from_string("sh")) handle error: + return error + function(view bytes) returns string reader = hex + string first = hex(view coarsen packet.data) + string second = reader(view coarsen packet.data) + string third = hex(view declassify packet.hidden) + string fourth = reader(view declassify packet.hidden) + optional[int64] absent = none + string fifth = sized(view declassify packet.hidden, absent handle: + default bytes.length(coarsen packet.data) + ) + Data copied = clone packet.data + return "{first}:{second}:{third}:{fourth}:{fifth}:{Named.name(view copied)}:{bytes.length(coarsen packet.data)}:{bytes.length(declassify packet.hidden)}" +function values() returns list[Named]: + Unit unit = nothing handle error: + return list() + Again again = unit handle error: + return list() + Enabled enabled = true handle error: + return list() + Data data = bytes.from_string("ok") handle error: + return list() + return list(nothing, unit, again, false, enabled, bytes.from_string("ok"), data) +function restored() returns list[Named]: + Unit unit = nothing handle error: + return list() + Unit pending = run run unit + Named erased = pending + Named inner = join erased handle error: + return list() + Named ready = join inner handle error: + return list() + return list(ready) +function main(stdout: Stdout) returns nothing: + for value in values(): + Stdout.write(view stdout, "{Named.name(view value)}\n") + for value in comptime values(): + Stdout.write(view stdout, "{Named.name(view value)}\n") + for value in restored(): + Stdout.write(view stdout, "{Named.name(view value)}\n") + for value in comptime restored(): + Stdout.write(view stdout, "{Named.name(view value)}\n") + Stdout.write(view stdout, "{borrowed()}\n") + string baked = comptime borrowed() + Stdout.write(view stdout, "{baked}\n") diff --git a/tests/native/interface_refined_sums.jett b/tests/native/interface_refined_sums.jett new file mode 100644 index 00000000..c39c3192 --- /dev/null +++ b/tests/native/interface_refined_sums.jett @@ -0,0 +1,115 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +implement Named for string: + function name(view self: string) returns string: + return self +function accepted_maybe(view source: optional[Named]) returns bool: + Named found = clone source handle: return false + return Named.name(view found) != "reject" +function accepted_outcome(view source: result[Named, string]) returns bool: + Named found = clone source handle error: return error == "kept" + return Named.name(view found) != "reject" +type Choice = optional[Named] where accepted_maybe(view value) +type Again = Choice where true +type Outcome = result[Named, string] where accepted_outcome(view value) +implement Named for Choice: + function name(view self: Choice) returns string: + optional[Named] base = coarsen clone self + Named found = base handle: return "unexpected absent" + return "choice:{Named.name(view found)}" +implement Named for Again: + function name(view self: Again) returns string: + Choice parent = coarsen clone self + return "again:{Named.name(view parent)}" +implement Named for Outcome: + function name(view self: Outcome) returns string: + result[Named, string] base = coarsen clone self + Named found = base handle error: return "outcome-fail:{error}" + return "outcome:{Named.name(view found)}" +struct Stored: + choice: Choice + outcome: Outcome +struct ChoiceHolder: + choice: Choice +function values() returns result[list[Named], string]: + int8 small = 7 + optional[Named] input = some(small) + Choice choice = input handle error: return fail(error) + Again again = clone choice handle error: return fail(error) + result[Named, string] success = ok("text") + Outcome outcome = success handle error: return fail(error) + result[Named, string] failure = fail("kept") + Outcome kept = failure handle error: return fail(error) + Stored stored = Stored(choice: clone choice, outcome: clone outcome) handle error: return fail(error) + return ok(list(choice, again, outcome, kept, stored.choice, stored.outcome)) +function rejected() returns string: + optional[Named] input = some("reject") + Choice invalid = input handle error: return "choice rejected" + return "unexpected choice" +function rejected_outcome() returns string: + result[Named, string] input = fail("bad") + Outcome invalid = input handle error: return "outcome rejected" + return "unexpected outcome" +function show(view source: list[Named]) returns string: + mutable string text = "" + for item in view source: + text = "{text}{Named.name(view item)};" + return text +function choice_factory() returns result[Choice, string]: + int8 small = 11 + optional[Named] input = some(small) + ChoiceHolder holder = ChoiceHolder(choice: input) handle error: return fail(error) + return ok(holder.choice) +function callbacks() returns string: + function() returns result[Named, string] live = choice_factory + function() returns result[Named, string] baked = comptime choice_factory + Named first = live() handle error: return error + Named second = baked() handle error: return error + return "{Named.name(view first)};{Named.name(view second)};" +function tasks() returns string: + Choice ready = choice_factory() handle error: return error + Choice pending = run ready + Named erased = pending + Named joined = join erased handle error: return error + return Named.name(view joined) +function absent_rejected() returns string: + optional[Named] input = none + Choice invalid = input handle error: return "absent rejected" + return "unexpected absent" +function pending_rejected() returns string: + optional[Named] input = some("text") + optional[Named] pending = run input + Choice invalid = pending handle error: return "pending rejected" + return "unexpected pending" +function extracted() returns string: + Choice choice = choice_factory() handle error: return error + result[Choice, int64] outer = ok(choice) + Choice found = outer handle error: return "unexpected outer:{error}" + optional[Choice] maybe = some(found) + Choice final = maybe handle: return "unexpected missing" + return Named.name(view final) +function accepted_integer_outcome(view source: result[int8, int64]) returns bool: + int8 found = clone source handle error: return error == 17 + return found > 0 +type IntegerOutcome = result[int8, int64] where accepted_integer_outcome(view value) +function integer_failure(view source: IntegerOutcome) returns int64: + int8 found = coarsen clone source handle error: return error + return 0 +function typed_errors() returns string: + result[int8, int64] source = fail(17) + IntegerOutcome valid = source handle error: return "unexpected typed failure" + result[int8, int64] rejected = fail(19) + IntegerOutcome invalid = rejected handle error: + return "{integer_failure(view valid)}:{string.length(error) > 0}" + return "unexpected typed acceptance" +function main(stdout: Stdout) returns nothing: + list[Named] live = values() handle error: return Stdout.write(view stdout, error) + list[Named] baked = comptime values() handle error: return Stdout.write(view stdout, error) + Stdout.write(view stdout, "{show(view live)}\n{show(view baked)}\n") + Stdout.write(view stdout, "{rejected()}\n{comptime rejected()}\n{rejected_outcome()}\n{comptime rejected_outcome()}\n") + Stdout.write(view stdout, "{callbacks()}\n{tasks()}\n") + Stdout.write(view stdout, "{absent_rejected()}\n{comptime absent_rejected()}\n{pending_rejected()}\n{extracted()}\n{typed_errors()}\n{comptime typed_errors()}\n") diff --git a/tests/native/interface_reflected_fields.jett b/tests/native/interface_reflected_fields.jett new file mode 100644 index 00000000..262e7375 --- /dev/null +++ b/tests/native/interface_reflected_fields.jett @@ -0,0 +1,130 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +implement Named for int64: + function name(view self: int64) returns string: + return "wide:{self}" +implement Named for secret[string]: + function name(view self: secret[string]) returns string: + return "secret" +struct Holder: + item: Named + items: list[Named] +function seed() returns Holder: + int8 small = 7 + secret[string] token = "hidden-reflected-interface" + list[Named] items = list(small, 9, token) + return Holder(item: small, items: items) +function record_builder(source: Holder) returns TypeConstruction: + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view source, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return type.construct_start[Holder]() + return builder +function finish_record(builder: TypeConstruction) returns Holder: + return type.construct_finish[Holder](builder) handle error: + return Holder(item: 0, items: list()) +function show_record(source: Holder) returns nothing: + trace source + println(Named.name(view source.item)) + for item in source.items: + println(Named.name(view item)) + return nothing +enum Event: + idle + active(item: Named, items: list[Named]) +function event_seed() returns Event: + Holder source = seed() + return Event.active(source.item, source.items) +function event_builder(source: Event) returns TypeConstruction: + for variant in type.variants[Event](): + if variant.name != "active": + continue + mutable TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return type.construct_start[Holder]() + for field in variant.fields: + comptime type Field = field.type_info: + Field value = type.variant_field_value[Event, Field](view source, view field) + builder = type.construct_put[Event, Field](builder, view field, value) handle error: + return type.construct_start[Holder]() + return builder + return type.construct_start[Holder]() +function finish_event(builder: TypeConstruction) returns Event: + return type.construct_finish[Event](builder) handle error: + return Event.idle +function show_event(source: Event) returns nothing: + trace source + match source: + idle: + println("idle") + active(item, items): + println(Named.name(view item)) + for value in items: + println(Named.name(view value)) + return nothing +machine Session: + states: + empty + active(item: Named, items: list[Named]) + transitions: + empty to active +function session_seed() returns Session: + Holder source = seed() + return Session(active, source.item, source.items) +function session_builder(source: Session) returns TypeConstruction: + for state in type.machine_states[Session](): + if state.name != "active": + continue + mutable TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return type.construct_start[Holder]() + for field in state.fields: + comptime type Field = field.type_info: + Field value = type.machine_field_value[Session, Field](view source, view field) + builder = type.construct_put[Session, Field](builder, view field, value) handle error: + return type.construct_start[Holder]() + return builder + return type.construct_start[Holder]() +function finish_session(builder: TypeConstruction) returns Session: + return type.construct_finish[Session](builder) handle error: + return Session(empty) +function show_session(source: Session) returns nothing: + trace source + if source at active: + println(Named.name(view source.item)) + for item in source.items: + println(Named.name(view item)) + return nothing +function records() returns nothing: + show_record(finish_record(record_builder(seed()))) + Holder baked = comptime finish_record(record_builder(seed())) + show_record(baked) + TypeConstruction builder = comptime record_builder(seed()) + trace builder + show_record(finish_record(builder)) + return nothing +function events() returns nothing: + show_event(finish_event(event_builder(event_seed()))) + Event baked = comptime finish_event(event_builder(event_seed())) + show_event(baked) + TypeConstruction builder = comptime event_builder(event_seed()) + trace builder + show_event(finish_event(builder)) + return nothing +function sessions() returns nothing: + show_session(finish_session(session_builder(session_seed()))) + Session baked = comptime finish_session(session_builder(session_seed())) + show_session(baked) + TypeConstruction builder = comptime session_builder(session_seed()) + trace builder + show_session(finish_session(builder)) + return nothing +function main() returns nothing: + records() + events() + sessions() + return nothing diff --git a/tests/native/interface_values.jett b/tests/native/interface_values.jett new file mode 100644 index 00000000..626261fc --- /dev/null +++ b/tests/native/interface_values.jett @@ -0,0 +1,456 @@ +interface Named: + function name(view self: Named) returns string +struct User: + label: string +struct Group: + label: string +implement Named for User: + function name(view self: User) returns string: + return "user:{self.label}" +implement Named for Group: + function name(view self: Group) returns string: + return "group:{self.label}" +implement Named for int64: + function name(view self: int64) returns string: + return "number:{self}" +implement Named for string: + function name(view self: string) returns string: + return "text:{self}" +implement Named for nothing: + function name(view self: nothing) returns string: + return "nothing" +function show(view item: Named) returns string: + return Named.name(view item) +function select(flag: bool) returns Named: + if flag: + return User(label: "Ada") + return Group(label: "team") +struct Holder: + item: Named +enum Choice: + item(value: Named) + empty +struct Vault: + token: secret[string] +implement Named for Vault: + function name(view self: Vault) returns string: + return "vault" +function secret_values() returns nothing: + Named vault = Vault(token: "hidden-interface-token") + trace vault + println(show(view vault)) + Named later = run vault + trace later + secret[Named] hidden = later + trace hidden + return nothing +machine Session: + states: + opened(item: Named) + closed(item: Named) + transitions: + opened to closed +actor Catalog(initial: Named): + mutable Named item = clone initial + receive replace(value: Named): + item = value + receive read responds string: + respond show(view item) + receive original responds string: + respond show(view initial) +function state_values() returns nothing: + Session at opened started = Session(opened, User(label: "session")) + println(show(view started.item)) + Session at closed ended = Session.transition(started, closed, Group(label: "closed")) + println(show(view ended.item)) + Catalog catalog = spawn Catalog(initial: User(label: "actor")) + println(ask catalog.read) + send catalog.replace(Group(label: "message")) + println(ask catalog.read) + println(ask catalog.original) + return nothing +function primitive_values() returns nothing: + Named empty = run nothing + trace empty + Named joined_empty = join empty handle error: + return nothing + println(show(view joined_empty)) + Named number = 42 + Named text = "hello" + println(show(view number)) + println(show(view text)) + Named pending = run 17 + trace pending + Named ready = join pending handle error: + return nothing + println(show(view ready)) + string source = run "later" + Named erased = source + trace source + trace erased + Named joined = join erased handle error: + return nothing + println(show(view joined)) + return nothing +function stored_values() returns nothing: + Holder holder = Holder(item: User(label: "stored")) + println(show(view holder.item)) + Holder copied = clone holder + println(show(view copied.item)) + Choice choice = Choice.item(Group(label: "enum")) + match choice: + item(value): + println(show(view value)) + empty: + println("empty") + Named original = User(label: "copy") + Named copied_item = clone original + println(show(view original)) + println(show(view copied_item)) + return nothing +function pending_values() returns nothing: + Named pending = run run User(label: "pending") + trace pending + Named once = join pending handle error: + return nothing + trace once + Named ready = join once handle error: + return nothing + println(show(view ready)) + Named erased = Group(label: "erased") + Named later = run erased + trace later + Named joined = join later handle error: + return nothing + println(show(view joined)) + return nothing +function generic_show[T](view item: Named, marker: T) returns string: + return Named.name(view item) +function callback_values() returns nothing: + Named item = User(label: "callback") + function(view Named) returns string reader = show + println(reader(view item)) + println(generic_show[int64](view item, 1)) + return nothing +function moved_values() returns nothing: + mutable Named original = User(label: "before move") + Named moved = original + breakpoint + original = Group(label: "restored") + breakpoint + println(show(view moved)) + println(show(view original)) + return nothing +function collection_conversions() returns nothing: + list[User] users = list(User(label: "converted-list")) + list[Named] items = users + for item in items: + println(show(view item)) + optional[Group] group = some(Group(label: "converted-optional")) + optional[Named] maybe = group + Named found = maybe handle: + return nothing + println(show(view found)) + result[User, Group] success = ok(User(label: "converted-result")) + result[Named, Named] converted = success + Named value = converted handle error: + return nothing + println(show(view value)) + map[string, User] records = map("key": User(label: "converted-map")) + map[string, Named] mapped = records + trace mapped + return nothing +function nested_collection_conversions() returns nothing: + list[list[User]] records = list(list(User(label: "nested"))) + list[list[Named]] converted = records + trace converted + list[User] pending = run list(User(label: "pending-list")) + list[Named] erased = pending + trace erased + list[Named] ready = join erased handle error: + return nothing + for item in ready: + println(show(view item)) + result[User, Group] failed = fail(Group(label: "converted-error")) + result[Named, Named] failure = failed + trace failure + optional[User] missing = none + optional[Named] empty = missing + trace empty + return nothing +function empty_names() returns list[Named]: + return list() +function empty_map() returns map[string, Named]: + return map() +function empty_conversions() returns nothing: + list[Named] empty = empty_names() + list[Named] filled = list.append[Named](empty, User(label: "after-empty")) + trace filled + map[string, Named] entries = empty_map() + map[string, Named] inserted = map.insert[string, Named](entries, "key", Group(label: "after-empty-map")) + trace inserted + return nothing +function static_items() returns list[Named]: + return list(User(label: "static-user"), Group(label: "static-group")) +function static_pending() returns Named: + return run User(label: "static-pending") +function static_vault() returns Named: + return Vault(token: "hidden-interface-token") +function comptime_values() returns nothing: + Named vault = comptime static_vault() + trace vault + Named selected = comptime select(true) + println(show(view selected)) + list[Named] items = comptime static_items() + for item in items: + println(show(view item)) + Named pending = comptime static_pending() + trace pending + Named ready = join pending handle error: + return nothing + println(show(view ready)) + return nothing +function adapted_return(view item: Named) returns User: + return User(label: Named.name(view item)) +struct ReaderHolder: + reader: function(view User) returns string +function make_reader(prefix: string) returns function(view Named) returns string: + return function(view item: Named) returns string: return "{prefix}:{Named.name(view item)}" +function invoke_reader(reader: function(view User) returns string) returns string: + User input = User(label: "higher-order") + return reader(view input) +function stored_adapters() returns nothing: + ReaderHolder holder = ReaderHolder(reader: show) + User input = User(label: "stored-adapter") + println(holder.reader(view input)) + function(view User) returns string captured = make_reader("captured") + println(captured(view input)) + trace captured + function(function(view Named) returns string) returns string higher = invoke_reader + println(higher(show)) + return nothing +function adapter_values() returns nothing: + function(view User) returns string reader = show + function(view User) returns Named transformer = adapted_return + User input = User(label: "adapted") + println(reader(view input)) + Named transformed = transformer(view input) + println(show(view transformed)) + trace reader + trace transformer + function(view Named) returns string original = run show + function(view User) returns string pending = original + trace pending + function(view User) returns string ready = join pending handle error: + return nothing + println(ready(view input)) + return nothing +function secret_callback(value: secret[int64]) returns secret[int64]: + return value +function secret_adapter() returns nothing: + function(int64) returns secret[int64] callback = secret_callback + secret[int64] value = callback(42) + trace value + return nothing +type Reader = function(view Named) returns string +type UserReader = function(view User) returns string +function callback_list_conversions() returns nothing: + list[Reader] source = list(show, make_reader("list-closure")) + list[UserReader] converted = source + User input = User(label: "container") + trace converted + for callback in converted: + println(callback(view input)) + list[optional[Reader]] nested_source = list(some(make_reader("nested")), none) + list[optional[UserReader]] nested = nested_source + for maybe in nested: + UserReader callback = maybe handle: + default show + println(callback(view input)) + list[Reader] pending_source = run list(show) + list[UserReader] pending = pending_source + trace pending + list[UserReader] ready = join pending handle error: + return nothing + for callback in ready: + println(callback(view input)) + list[Reader] no_callbacks = list() + list[UserReader] empty = no_callbacks + list[UserReader] filled = list.append[UserReader](empty, show) + trace filled + return nothing +function callback_sum_conversions() returns nothing: + User input = User(label: "sums") + optional[Reader] source = some(make_reader("optional")) + optional[UserReader] maybe = source + UserReader optional_callback = maybe handle: + return nothing + println(optional_callback(view input)) + result[Reader, Reader] success = ok(show) + result[UserReader, UserReader] converted = success + UserReader ok_callback = converted handle error: + return nothing + println(ok_callback(view input)) + result[Reader, Reader] failed = fail(make_reader("error")) + result[UserReader, UserReader] failure = failed + UserReader failed_callback = failure handle error: + println(error(view input)) + return nothing + println(failed_callback(view input)) + return nothing +function callback_map_conversions() returns nothing: + map[string, Reader] source = map("key": make_reader("map")) + map[string, UserReader] converted = source + trace converted + UserReader callback = map.get[string, UserReader](converted, "key") handle: + return nothing + User input = User(label: "lookup") + println(callback(view input)) + return nothing +function callback_pending_conversions() returns nothing: + optional[Reader] source = some(run make_reader("pending-callback")) + optional[UserReader] converted = source + trace converted + UserReader pending = converted handle: + return nothing + UserReader callback = join pending handle error: + return nothing + User input = User(label: "joined") + println(callback(view input)) + list[function(view Named) returns User] factories = list(adapted_return) + list[function(view User) returns Named] adapted = factories + for factory in adapted: + Named produced = factory(view input) + println(show(view produced)) + list[function(secret[int64]) returns secret[int64]] secret_source = list(secret_callback) + list[function(int64) returns secret[int64]] secret_adapted = secret_source + for secret_function in secret_adapted: + secret[int64] secret_value = secret_function(41) + trace secret_value + return nothing +function first_reader(items: list[UserReader]) returns string: + User input = User(label: "higher-container") + for callback in items: + return callback(view input) + return "empty" +function callback_nested_adapters() returns nothing: + function(list[Reader]) returns string adapted = first_reader + println(adapted(list(make_reader("higher")))) + return nothing +function static_reader(named: bool) returns UserReader: + if named: + return show + return make_reader("baked-capture") +function static_callback_list() returns list[UserReader]: + list[Reader] readers = list(show, make_reader("baked-list")) + return readers +function static_callback_holder() returns ReaderHolder: + return ReaderHolder(reader: make_reader("baked-holder")) +function static_pending_reader() returns UserReader: + return run show +function static_factory() returns function(view User) returns Named: + return adapted_return +function static_secret_callback() returns function(int64) returns secret[int64]: + return secret_callback +function comptime_callback_adapters() returns nothing: + User input = User(label: "comptime-adapter") + UserReader named = comptime static_reader(true) + UserReader captured = comptime static_reader(false) + println(named(view input)) + println(captured(view input)) + trace captured + list[UserReader] readers = comptime static_callback_list() + for callback in readers: + println(callback(view input)) + ReaderHolder holder = comptime static_callback_holder() + println(holder.reader(view input)) + UserReader pending = comptime static_pending_reader() + trace pending + UserReader ready = join pending handle error: + return nothing + println(ready(view input)) + function(view User) returns Named factory = comptime static_factory() + Named produced = factory(view input) + println(show(view produced)) + function(int64) returns secret[int64] hidden = comptime static_secret_callback() + secret[int64] hidden_value = hidden(73) + trace hidden_value + comptime static_reader(true) + return nothing +type Token = secret[string] +struct Box[T]: + item: T +implement Named for Box[Token]: + function name(view self: Box[Token]) returns string: + return "box" +function wrap[T](value: T) returns Box[T]: + return Box[T](item: value) +function generic_secret() returns Named: + return wrap[Token]("hidden-generic-interface") +function reflected_generic_secret() returns Named: + Box[Token] source = wrap[Token]("hidden-generic-interface") + mutable TypeConstruction builder = type.construct_start[Box[Token]]() + for field in type.fields[Box[Token]](): + comptime type Field = field.type_info: + Field value = type.field_value[Box[Token], Field](view source, view field) + builder = type.construct_put[Box[Token], Field](builder, view field, value) handle error: + return Group(label: "reflection-failed") + Box[Token] built = type.construct_finish[Box[Token]](builder) handle error: + return Group(label: "reflection-failed") + return built +function generic_values() returns nothing: + Box[int64] public = wrap[int64](19) + trace public + Named value = generic_secret() + trace value + println(show(view value)) + Named copied = clone value + trace copied + Named pending = run copied + trace pending + Named ready = join pending handle error: + return nothing + trace ready + Named baked = comptime generic_secret() + trace baked + println(show(view baked)) + list[Named] items = comptime list(generic_secret(), generic_secret()) + trace items + Named reflected = reflected_generic_secret() + trace reflected + Named reflected_baked = comptime reflected_generic_secret() + trace reflected_baked + Box[string] plain = wrap[string]("visible-generic-interface") + trace plain + return nothing +function main() returns nothing: + comptime_callback_adapters() + callback_list_conversions() + callback_sum_conversions() + callback_map_conversions() + callback_pending_conversions() + callback_nested_adapters() + generic_values() + secret_adapter() + stored_adapters() + adapter_values() + comptime_values() + empty_conversions() + collection_conversions() + nested_collection_conversions() + moved_values() + callback_values() + primitive_values() + state_values() + secret_values() + stored_values() + pending_values() + Named first = select(true) + Named second = select(false) + println(show(view first)) + println(show(view second)) + trace first + list[Named] items = list(User(label: "Grace"), Group(label: "crew")) + for item in items: + println(show(view item)) + return nothing diff --git a/tests/native/join_local_reuse.jett b/tests/native/join_local_reuse.jett new file mode 100644 index 00000000..481b30f2 --- /dev/null +++ b/tests/native/join_local_reuse.jett @@ -0,0 +1,21 @@ +namespace app +function main() returns nothing: + string pending = run "Ada" + string first = join pending handle error: + println(error) + return nothing + string second = join pending handle error: + println(error) + return nothing + trace pending + println(first, second) + int64 pending_number = run 7 + int64 first_number = join pending_number handle error: + println(error) + return nothing + int64 second_number = join pending_number handle error: + println(error) + return nothing + trace pending_number + println(first_number, second_number) + return nothing diff --git a/tests/native/json_aggregate_parse.jett b/tests/native/json_aggregate_parse.jett new file mode 100644 index 00000000..1e4d9f21 --- /dev/null +++ b/tests/native/json_aggregate_parse.jett @@ -0,0 +1,67 @@ +namespace app +function parse_list_error() returns string: + list[string] values = json.parse[list[string]]("[\"ok\",7]") handle error: + return error + return "unexpected {list.length[string](view values)}" +function parse_map_bytes() returns string: + map[string, optional[bytes]] entries = json.parse[map[string, optional[bytes]]]("{{\"blob\":\"0x4869\",\"missing\":null}}") handle error: + return error + optional[optional[bytes]] maybe_entry = map.get[string, optional[bytes]](clone entries, "blob") + optional[bytes] maybe_blob = maybe_entry handle: + return "missing blob" + bytes blob = maybe_blob handle: + return "none blob" + bool has_missing = map.has[string, optional[bytes]](entries, "missing") + return "{bytes.length(blob)}:{has_missing}" +function parse_more() returns nothing: + list[bool] flags = json.parse[list[bool]]("[true,false]") handle error: + return + println("{list.length[bool](view flags)}") + list[int64] numbers = json.parse[list[int64]]("[1,-2]") handle error: + return + println("{list.length[int64](view numbers)}") + println(parse_map_bytes()) + optional[int64] present = json.parse[optional[int64]]("7") handle error: + return + int64 present_value = present handle: + default 0 + println("{present_value}") +function parse_numeric_lists() returns nothing: + list[uint64] unsigned = json.parse[list[uint64]]("[0,42]") handle error: + return + uint64 unsigned_value = list.get[uint64](view unsigned, 1) handle: + default 0 + println("{unsigned_value}") + list[float64] fractions = json.parse[list[float64]]("[1.5,-2.25]") handle error: + return + float64 fraction_value = list.get[float64](view fractions, 1) handle: + default 0.0 + println("{fraction_value}") + list[list[string]] nested = json.parse[list[list[string]]]("[[\"a\"],[\"b\",\"c\"]]") handle error: + return + println("{list.length[list[string]](view nested)}") + optional[int64] absent = json.parse[optional[int64]]("null") handle error: + return + int64 absent_value = absent handle: + default -1 + println("{absent_value}") +function parse_map_error() returns string: + map[string, int64] entries = json.parse[map[string, int64]]("{{\"bad\":true}}") handle error: + return error + bool has_bad = map.has[string, int64](entries, "bad") + return "unexpected {has_bad}" +function main() returns nothing: + list[string] names = json.parse[list[string]]("[\"Ada\",\"Lin\"]") handle error: + return + string first = list.get[string](view names, 0) handle: + default "missing" + string last = list.get[string](view names, 1) handle: + default "missing" + println("{first}:{last}:{list.length[string](view names)}") + list[string] exact = json.parse_exact[list[string]]("[\"exact\"]") handle error: + return + println(list.length[string](view exact)) + println(parse_list_error()) + parse_more() + parse_numeric_lists() + println(parse_map_error()) diff --git a/tests/native/json_bitfield_source.jett b/tests/native/json_bitfield_source.jett new file mode 100644 index 00000000..f0098b53 --- /dev/null +++ b/tests/native/json_bitfield_source.jett @@ -0,0 +1,82 @@ +namespace app +enum Protocol: + tcp = 6 + udp = 17 +bitfield Header: + version: 4 bits + protocol: 8 bits as Protocol +bitfield WideHeader: + sequence: 64 bits +bitfield Packet: + kind: 8 bits + payload: list[uint8] +function parse_header() returns string: + Header header = json.parse[Header]("{{\"version\":4,\"protocol\":\"tcp\"}}") handle error: + return error + return json.serialize[Header](view header) +function parse_header_exact() returns string: + Header header = json.parse_exact[Header]("{{\"version\":5,\"protocol\":\"udp\"}}") handle error: + return error + return json.serialize_public[Header](view header) +function parse_wide_header() returns string: + WideHeader header = json.parse[WideHeader]("{{\"sequence\":18446744073709551615}}") handle error: + return error + return json.serialize[WideHeader](view header) +function parse_header_missing_field() returns string: + Header header = json.parse[Header]("{{\"version\":4}}") handle error: + return error + return json.serialize[Header](view header) +function parse_header_numeric_enum() returns string: + Header header = json.parse[Header]("{{\"version\":4,\"protocol\":6}}") handle error: + return error + return json.serialize[Header](view header) +function parse_wide_extra_field() returns string: + WideHeader header = json.parse_exact[WideHeader]("{{\"sequence\":1,\"extra\":2}}") handle error: + return error + return json.serialize[WideHeader](view header) +function parse_packet() returns string: + Packet packet = json.parse[Packet]("{{\"kind\":42,\"payload\":[1,127,255]}}") handle error: + return error + return json.serialize[Packet](view packet) +function parse_packet_range_error() returns string: + Packet packet = json.parse[Packet]("{{\"kind\":42,\"payload\":[256]}}") handle error: + return error + return json.serialize[Packet](view packet) +function parse_packet_exact_extra_field() returns string: + Packet packet = json.parse_exact[Packet]("{{\"kind\":42,\"payload\":[1],\"extra\":true}}") handle error: + return error + return json.serialize[Packet](view packet) +function parse_uint8() returns string: + uint8 value = json.parse[uint8]("255") handle error: + return error + return "{value}" +function parse_uint8_list() returns string: + list[uint8] values = json.parse[list[uint8]]("[0,128,255]") handle error: + return error + uint8 first = list.get[uint8](view values, 0) handle: + return "missing" + uint8 second = list.get[uint8](view values, 1) handle: + return "missing" + uint8 third = list.get[uint8](view values, 2) handle: + return "missing" + return "{first}:{second}:{third}" +function parse_uint8_range_error() returns string: + uint8 value = json.parse[uint8]("256") handle error: + return error + return "{value}" +function main() returns nothing: + Header header = Header(version: 4, protocol: Protocol.tcp) + println(json.serialize[Header](view header)) + println(json.serialize_public[Header](view header)) + WideHeader wide = WideHeader(sequence: 18446744073709551615) + println(json.serialize[WideHeader](view wide)) + mutable list[uint8] payload = list.new[uint8]() + payload = list.append[uint8](payload, 1) + payload = list.append[uint8](payload, 127) + payload = list.append[uint8](payload, 255) + Packet packet = Packet(kind: 42, payload: payload) + println(json.serialize_public[Packet](view packet)) + println(parse_header(), parse_header_exact(), parse_wide_header()) + println(parse_header_missing_field(), parse_header_numeric_enum(), parse_wide_extra_field()) + println(parse_packet(), parse_packet_range_error(), parse_packet_exact_extra_field()) + println(parse_uint8(), parse_uint8_list(), parse_uint8_range_error()) diff --git a/tests/native/json_bytes_raw_source.jett b/tests/native/json_bytes_raw_source.jett new file mode 100644 index 00000000..9f481b8d --- /dev/null +++ b/tests/native/json_bytes_raw_source.jett @@ -0,0 +1,22 @@ +namespace test +struct RawPayload: + tree: json.JsonTree + label: string +function serialize_bytes() returns string: + bytes value = bytes.from_string("Hi") + return json.serialize[bytes](view value) +function serialize_public_bytes() returns string: + bytes value = bytes.from_string("Hi") + return json.serialize_public[bytes](view value) +function serialize_raw_field() returns string: + json.JsonTree tree = json.parse_raw("{{\"name\":\"Ada\"}}") handle error: + return error + RawPayload payload = RawPayload(tree: tree, label: "ok") + return json.serialize[RawPayload](view payload) +function serialize_public_raw_field() returns string: + json.JsonTree tree = json.parse_raw("[1,true,null]") handle error: + return error + RawPayload payload = RawPayload(tree: tree, label: "ok") + return json.serialize_public[RawPayload](view payload) +function main() returns nothing: + println(serialize_bytes(), serialize_public_bytes(), serialize_raw_field(), serialize_public_raw_field()) diff --git a/tests/native/json_enum_raw_source.jett b/tests/native/json_enum_raw_source.jett new file mode 100644 index 00000000..8242faf5 --- /dev/null +++ b/tests/native/json_enum_raw_source.jett @@ -0,0 +1,36 @@ +namespace app +enum Shape: + raw(payload: json.JsonTree) + circle(radius: float64) + empty +function parse_raw_payload() returns string: + Shape shape = json.parse_exact[Shape]("{{\"raw\":[{{\"extra\":\"kept\"}}]}}") handle error: + return error + match shape: + raw(payload): + json.JsonTree extra = json.require_field(view payload, "extra") handle error: + return error + return json.as_string(view extra) handle error: + default error + circle(radius): + return "{radius}" + empty: + return "empty" +function compare_raw_to_unit() returns string: + Shape shape = json.parse[Shape]("{{\"raw\":[[1,true,null]]}}") handle error: + return error + return "{shape == Shape.empty}:{shape != Shape.empty}:{Shape.empty == shape}:{Shape.empty != shape}:{json.serialize[Shape](view shape)}" +function compare_empty_to_unit() returns string: + Shape shape = json.parse_exact[Shape]("\"empty\"") handle error: + return error + return "{shape == Shape.empty}:{shape != Shape.empty}:{Shape.empty == shape}:{Shape.empty != shape}" +function serialize_raw_payload() returns string: + json.JsonTree payload = json.parse_raw("{{\"nested\":[1,2]}}") handle error: + return error + Shape shape = Shape.raw(payload) + return json.serialize_public[Shape](view shape) +function main() returns nothing: + println(parse_raw_payload()) + println(compare_raw_to_unit()) + println(compare_empty_to_unit()) + println(serialize_raw_payload()) diff --git a/tests/native/json_enum_source.jett b/tests/native/json_enum_source.jett new file mode 100644 index 00000000..42d03f77 --- /dev/null +++ b/tests/native/json_enum_source.jett @@ -0,0 +1,47 @@ +namespace app +enum Direction: + north + south +enum Shape: + point(x: int64, y: int64) + empty +struct Profile: + name: string +enum Event: + recorded(profile: Profile) + idle +function parse_unit() returns string: + Direction direction = json.parse[Direction]("\"north\"") handle error: + return error + return json.serialize[Direction](view direction) +function parse_payload() returns string: + Shape shape = json.parse[Shape]("{{\"point\":[3,4]}}") handle error: + return error + return json.serialize[Shape](view shape) +function serialize_payload() returns string: + Shape shape = Shape.point(5, 6) + return json.serialize_public[Shape](view shape) +function parse_bad_payload() returns string: + Shape shape = json.parse_exact[Shape]("{{\"point\":[3]}}") handle error: + return error + return json.serialize[Shape](view shape) +function parse_unknown() returns string: + Direction direction = json.parse[Direction]("\"east\"") handle error: + return error + return json.serialize[Direction](view direction) +function parse_exact_nested() returns string: + Event event = json.parse_exact[Event]("{{\"recorded\":[{{\"name\":\"Ada\"}}]}}") handle error: + return error + return json.serialize[Event](view event) +function parse_exact_nested_extra() returns string: + Event event = json.parse_exact[Event]("{{\"recorded\":[{{\"name\":\"Ada\",\"extra\":1}}]}}") handle error: + return error + return json.serialize[Event](view event) +function main() returns nothing: + println(parse_unit()) + println(parse_payload()) + println(serialize_payload()) + println(parse_bad_payload()) + println(parse_unknown()) + println(parse_exact_nested()) + println(parse_exact_nested_extra()) diff --git a/tests/native/json_float32.jett b/tests/native/json_float32.jett new file mode 100644 index 00000000..635d7986 --- /dev/null +++ b/tests/native/json_float32.jett @@ -0,0 +1,27 @@ +namespace test +struct Reading: + value: float32 +function parse_value(raw: string) returns string: + float32 value = json.parse_exact[float32](raw) handle error: + return error + return "{value}" +function parse_reading(raw: string) returns string: + Reading reading = json.parse[Reading](raw) handle error: + return error + return "{reading.value}" +function roundtrip_reading(raw: string) returns string: + Reading reading = json.parse_exact[Reading](raw) handle error: + return error + return json.serialize[Reading](view reading) +function conversion_summary() returns string: + float32 runtime = float32.from_float64(0.1) + float32 baked = comptime float32.from_float64(2.5) + return "{runtime}:{baked}" +function main() returns nothing: + println(parse_value("2.5")) + println(parse_value("0.1")) + println(parse_value("\"2.5\"")) + println(parse_reading("{{\"value\":2.5}}")) + println(parse_reading("{{\"value\":\"2.5\"}}")) + println(roundtrip_reading("{{\"value\":2.5}}")) + println(conversion_summary()) diff --git a/tests/native/json_generic_parse_primitives.jett b/tests/native/json_generic_parse_primitives.jett new file mode 100644 index 00000000..fa0dd2f6 --- /dev/null +++ b/tests/native/json_generic_parse_primitives.jett @@ -0,0 +1,44 @@ +namespace app +function parsed_string(raw: string) returns string: + string value = json.parse[string](raw) handle error: + return "error:{error}" + return "ok:{value}" +function parsed_bool(raw: string) returns string: + bool value = json.parse_exact[bool](raw) handle error: + return "error:{error}" + return "ok:{value}" +function parsed_int(raw: string) returns string: + int64 value = json.parse[int64](raw) handle error: + return "error:{error}" + return "ok:{value}" +function parsed_uint(raw: string) returns string: + uint64 value = json.parse_exact[uint64](raw) handle error: + return "error:{error}" + return "ok:{string.from_uint64(value)}" +function parsed_float(raw: string) returns string: + float64 value = json.parse[float64](raw) handle error: + return "error:{error}" + return "ok:{value}" +function parsed_bytes(raw: string) returns string: + bytes value = json.parse_exact[bytes](raw) handle error: + return "error:{error}" + return "ok:{bytes.to_hex(value)}" +function parsed_nothing(raw: string) returns string: + nothing value = json.parse[nothing](raw) handle error: + return "error:{error}" + return "ok" +function main() returns nothing: + println(parsed_string("\"Ada\\n€\"")) + println(parsed_string("42")) + println(parsed_bool("true")) + println(parsed_bool("0")) + println(parsed_int("-9223372036854775808")) + println(parsed_int("18446744073709551615")) + println(parsed_uint("18446744073709551615")) + println(parsed_uint("-1")) + println(parsed_float("-2.5")) + println(parsed_float("\"wrong\"")) + println(parsed_bytes("\"0x4869\"")) + println(parsed_bytes("\"bad\"")) + println(parsed_nothing("null")) + println(parsed_nothing("false")) diff --git a/tests/native/json_generic_primitives.jett b/tests/native/json_generic_primitives.jett new file mode 100644 index 00000000..b11c2d32 --- /dev/null +++ b/tests/native/json_generic_primitives.jett @@ -0,0 +1,19 @@ +namespace app +function main() returns nothing: + int8 small = -128 + uint8 byte = 255 + uint64 large = 18446744073709551615 + float32 narrow = -2.5 + float64 wide = 3.125 + float64 not_a_number = 0.0 / 0.0 + float64 infinity = 1.0 / 0.0 + println(json.serialize[int8](small)) + println(json.serialize_public[uint8](byte)) + println(json.serialize[uint64](large)) + println(json.serialize[float32](narrow)) + println(json.serialize_public[float64](wide)) + println(json.serialize[float64](not_a_number)) + println(json.serialize[float64](infinity)) + println(json.serialize[bool](true)) + println(json.serialize[string]("quote \" slash \\ line\n€")) + println(json.serialize[nothing](nothing)) diff --git a/tests/native/json_generic_raw_bridge.jett b/tests/native/json_generic_raw_bridge.jett new file mode 100644 index 00000000..dd2a82e2 --- /dev/null +++ b/tests/native/json_generic_raw_bridge.jett @@ -0,0 +1,18 @@ +namespace app +function summarize(raw: string) returns string: + json.JsonTree parsed = json.parse[json.JsonTree](raw) handle error: + return "parse:{error}" + json.JsonTree exact = json.parse_exact[json.JsonTree](raw) handle error: + return "exact:{error}" + string regular = json.serialize[json.JsonTree](view parsed) + string public = json.serialize_public[json.JsonTree](view exact) + return "{regular}|{public}|{json.serialize_raw(view parsed)}" +function exact_error(raw: string) returns string: + json.JsonTree parsed = json.parse_exact[json.JsonTree](raw) handle error: + return error + return json.serialize_raw(view parsed) +function main() returns nothing: + println(summarize("{{\"name\":\"Ada\",\"items\":[1,true,null]}}")) + println(summarize("\"line\\nnext\"")) + println(summarize("[1,")) + println(exact_error("[1,")) diff --git a/tests/native/json_machine_parse.jett b/tests/native/json_machine_parse.jett new file mode 100644 index 00000000..6ad1f2cb --- /dev/null +++ b/tests/native/json_machine_parse.jett @@ -0,0 +1,47 @@ +namespace app +machine Session: + states: + guest + logged_in(user_id: string, nickname: optional[string]) + transitions: + guest to logged_in +function parse_logged_in() returns string: + Session session = json.parse[Session]("{{\"state\":\"logged_in\",\"payload\":{{\"user_id\":\"Ada\"}}}}") handle error: + return error + if session at logged_in: + string nickname = session.nickname handle: + default "none" + return "{session.user_id}:{nickname}" + return "wrong state" +function parse_guest() returns string: + Session session = json.parse[Session]("{{\"state\":\"guest\",\"payload\":{{}}}}") handle error: + return error + TypeMachineState state = type.machine_state_value[Session](view session) + return state.name +function parse_state_qualified() returns string: + Session at logged_in session = json.parse[Session at logged_in]("{{\"state\":\"logged_in\",\"payload\":{{\"user_id\":\"Lin\",\"nickname\":\"L\"}}}}") handle error: + return error + string nickname = session.nickname handle: + default "none" + return "{session.user_id}:{nickname}" +function parse_state_mismatch() returns string: + Session at logged_in session = json.parse[Session at logged_in]("{{\"state\":\"guest\",\"payload\":{{}}}}") handle error: + return error + return session.user_id +function parse_exact_extra() returns string: + Session session = json.parse_exact[Session]("{{\"state\":\"guest\",\"payload\":{{}},\"extra\":1}}") handle error: + return error + TypeMachineState state = type.machine_state_value[Session](view session) + return state.name +function parse_missing_field() returns string: + Session session = json.parse[Session]("{{\"state\":\"logged_in\",\"payload\":{{}}}}") handle error: + return error + TypeMachineState state = type.machine_state_value[Session](view session) + return state.name +function main() returns nothing: + println(parse_logged_in()) + println(parse_guest()) + println(parse_state_qualified()) + println(parse_state_mismatch()) + println(parse_exact_extra()) + println(parse_missing_field()) diff --git a/tests/native/json_machine_serialize.jett b/tests/native/json_machine_serialize.jett new file mode 100644 index 00000000..314b4e7c --- /dev/null +++ b/tests/native/json_machine_serialize.jett @@ -0,0 +1,35 @@ +namespace app +machine Session: + states: + guest + logged_in(user_id: string, display_name: string serialize "displayName") + transitions: + guest to logged_in +machine TokenLifecycle: + states: + issued(token: secret[string], public_id: string serialize "publicId") + revoked + transitions: + issued to revoked +struct SessionBox: + current: Session at logged_in + history: list[Session] +function serialize_bare() returns string: + Session session = Session.transition(Session(guest), logged_in, "user-1", "Ada") + return json.serialize[Session](view session) +function serialize_state() returns string: + Session at logged_in session = Session(logged_in, "user-2", "Lin") + return json.serialize[Session at logged_in](view session) +function serialize_nested() returns string: + Session at logged_in active = Session.transition(Session(guest), logged_in, "user-3", "Bea") + Session logged = clone active + SessionBox box = SessionBox(current: active, history: list(Session(guest), logged)) + return json.serialize[SessionBox](view box) +function serialize_public_secret() returns string: + TokenLifecycle token = TokenLifecycle(issued, "secret-token", "tok-1") + return json.serialize_public[TokenLifecycle](view token) +function main() returns nothing: + println(serialize_bare()) + println(serialize_state()) + println(serialize_nested()) + println(serialize_public_secret()) diff --git a/tests/native/json_narrow_integers.jett b/tests/native/json_narrow_integers.jett new file mode 100644 index 00000000..fd35dd81 --- /dev/null +++ b/tests/native/json_narrow_integers.jett @@ -0,0 +1,52 @@ +namespace test +struct NarrowBundle: + low: int8 + high: uint32 + values: list[uint16] +function parse_int8(raw: string) returns string: + int8 value = json.parse_exact[int8](raw) handle error: + return error + return "{value}" +function parse_int16(raw: string) returns string: + int16 value = json.parse[int16](raw) handle error: + return error + return "{value}" +function parse_int32(raw: string) returns string: + int32 value = json.parse_exact[int32](raw) handle error: + return error + return "{value}" +function parse_uint16(raw: string) returns string: + uint16 value = json.parse_exact[uint16](raw) handle error: + return error + return "{value}" +function parse_uint32(raw: string) returns string: + uint32 value = json.parse[uint32](raw) handle error: + return error + return "{value}" +function parse_bundle(raw: string) returns string: + NarrowBundle bundle = json.parse_exact[NarrowBundle](raw) handle error: + return error + uint16 last = list.get[uint16](view bundle.values, 1) handle: + default 0 + return "{bundle.low}:{bundle.high}:{last}" +function main() returns nothing: + println(parse_int8("-128")) + println(parse_int8("127")) + println(parse_int8("128")) + println(parse_int8("9223372036854775808")) + println(parse_int8("2.5")) + println(parse_int16("-32768")) + println(parse_int16("32767")) + println(parse_int16("32768")) + println(parse_int32("-2147483648")) + println(parse_int32("2147483647")) + println(parse_int32("2147483648")) + println(parse_uint16("0")) + println(parse_uint16("65535")) + println(parse_uint16("65536")) + println(parse_uint16("\"9\"")) + println(parse_uint32("0")) + println(parse_uint32("4294967295")) + println(parse_uint32("4294967296")) + println(parse_bundle("{{\"low\":-5,\"high\":4294967295,\"values\":[1,65535]}}")) + println(parse_bundle("{{\"low\":128,\"high\":9,\"values\":[1,2]}}")) diff --git a/tests/native/json_nested_bitfield.jett b/tests/native/json_nested_bitfield.jett new file mode 100644 index 00000000..e84d53f0 --- /dev/null +++ b/tests/native/json_nested_bitfield.jett @@ -0,0 +1,29 @@ +namespace app +enum Protocol: + tcp = 6 + udp = 17 +bitfield Header: + version: 4 bits + flags: 4 bits + protocol: 8 bits as Protocol + payload: list[uint8] +struct Envelope: + title: string + header: Header +function roundtrip() returns string: + Envelope value = json.parse_exact[Envelope]("{{\"title\":\"packet\",\"header\":{{\"version\":4,\"flags\":2,\"protocol\":\"tcp\",\"payload\":[1,255]}}}}") handle error: + return error + return json.serialize[Envelope](view value) +function bad_protocol() returns string: + Envelope value = json.parse_exact[Envelope]("{{\"title\":\"packet\",\"header\":{{\"version\":4,\"flags\":2,\"protocol\":6,\"payload\":[1,255]}}}}") handle error: + return error + return json.serialize[Envelope](view value) +function bad_payload() returns string: + Envelope value = json.parse_exact[Envelope]("{{\"title\":\"packet\",\"header\":{{\"version\":4,\"flags\":2,\"protocol\":\"tcp\",\"payload\":[256]}}}}") handle error: + return error + return json.serialize[Envelope](view value) +function main() returns nothing: + println(roundtrip()) + println(bad_protocol()) + println(bad_payload()) + return nothing diff --git a/tests/native/json_nested_enum.jett b/tests/native/json_nested_enum.jett new file mode 100644 index 00000000..0007e5e4 --- /dev/null +++ b/tests/native/json_nested_enum.jett @@ -0,0 +1,34 @@ +namespace app +enum Direction: + north + south +enum Shape: + point(x: int64, y: int64) + empty +struct Profile: + name: string +enum Event: + recorded(profile: Profile) + idle +function parse_directions() returns string: + list[Direction] values = json.parse_exact[list[Direction]]("[\"north\",\"south\"]") handle error: + return error + return json.serialize[list[Direction]](view values) +function parse_shapes() returns string: + list[Shape] values = json.parse_exact[list[Shape]]("[{{\"point\":[1,2]}},{{\"empty\":[]}}]") handle error: + return error + return json.serialize[list[Shape]](view values) +function parse_mapping() returns string: + map[string, Direction] values = json.parse_exact[map[string, Direction]]("{{\"a\":\"north\",\"b\":\"south\"}}") handle error: + return error + return json.serialize[map[string, Direction]](view values) +function parse_records() returns string: + list[Event] values = json.parse_exact[list[Event]]("[{{\"recorded\":[{{\"name\":\"Ada\"}}]},\"idle\"]") handle error: + return error + return json.serialize[list[Event]](view values) +function main() returns nothing: + println(parse_directions()) + println(parse_shapes()) + println(parse_mapping()) + println(parse_records()) + return nothing diff --git a/tests/native/json_nested_machine_parse.jett b/tests/native/json_nested_machine_parse.jett new file mode 100644 index 00000000..6f81d443 --- /dev/null +++ b/tests/native/json_nested_machine_parse.jett @@ -0,0 +1,45 @@ +namespace app +machine Session: + states: + guest + active(user_id: string) + transitions: + guest to active +struct SessionBox: + sessions: list[Session] + by_name: map[string, Session] +struct QualifiedBox: + current: Session at active + history: list[Session at active] +function parse_box() returns string: + SessionBox value = json.parse_exact[SessionBox]("{{\"sessions\":[{{\"state\":\"guest\",\"payload\":{{}}}},{{\"state\":\"active\",\"payload\":{{\"user_id\":\"Ada\"}}}}],\"by_name\":{{\"first\":{{\"state\":\"active\",\"payload\":{{\"user_id\":\"Lin\"}}}}}}}}") handle error: + return error + return json.serialize[SessionBox](view value) +function missing_payload_field() returns string: + SessionBox value = json.parse_exact[SessionBox]("{{\"sessions\":[{{\"state\":\"active\",\"payload\":{{}}}}],\"by_name\":{{}}}}") handle error: + return error + return json.serialize[SessionBox](view value) +function unknown_state() returns string: + SessionBox value = json.parse_exact[SessionBox]("{{\"sessions\":[{{\"state\":\"offline\",\"payload\":{{}}}}],\"by_name\":{{}}}}") handle error: + return error + return json.serialize[SessionBox](view value) +function extra_payload_field() returns string: + SessionBox value = json.parse_exact[SessionBox]("{{\"sessions\":[{{\"state\":\"active\",\"payload\":{{\"user_id\":\"Ada\",\"extra\":1}}}}],\"by_name\":{{}}}}") handle error: + return error + return json.serialize[SessionBox](view value) +function parse_qualified() returns string: + QualifiedBox value = json.parse_exact[QualifiedBox]("{{\"current\":{{\"state\":\"active\",\"payload\":{{\"user_id\":\"Ada\"}}}},\"history\":[{{\"state\":\"active\",\"payload\":{{\"user_id\":\"Lin\"}}}}]}}") handle error: + return error + return json.serialize[QualifiedBox](view value) +function mismatched_qualified_state() returns string: + QualifiedBox value = json.parse_exact[QualifiedBox]("{{\"current\":{{\"state\":\"guest\",\"payload\":{{}}}},\"history\":[]}}") handle error: + return error + return json.serialize[QualifiedBox](view value) +function main() returns nothing: + println(parse_box()) + println(missing_payload_field()) + println(unknown_state()) + println(extra_payload_field()) + println(parse_qualified()) + println(mismatched_qualified_state()) + return nothing diff --git a/tests/native/json_nested_secret_probe.jett b/tests/native/json_nested_secret_probe.jett new file mode 100644 index 00000000..ee409f1a --- /dev/null +++ b/tests/native/json_nested_secret_probe.jett @@ -0,0 +1,52 @@ +namespace app +function parse_secret_list() returns string: + secret[list[int64]] value = json.parse[secret[list[int64]]]("[1,2,3]") handle error: + return error + list[int64] opened = declassify value + return "{list.length[int64](view opened)}" +function parse_list_of_secrets() returns string: + list[secret[string]] values = json.parse[list[secret[string]]]("[\"Ada\",\"Lin\"]") handle error: + return error + secret[string] first = list.get[secret[string]](view values, 0) handle: + return "missing" + return declassify first +function parse_secret_bytes_list() returns string: + list[secret[bytes]] values = json.parse[list[secret[bytes]]]("[\"4142\",\"00\"]") handle error: + return error + secret[bytes] first = list.get[secret[bytes]](view values, 0) handle: + return "missing" + bytes opened = declassify first + return bytes.to_hex(view opened) +function parse_map_of_secret_lists() returns string: + map[string, secret[list[int64]]] values = json.parse[map[string, secret[list[int64]]]]("{{\"first\":[1,2]}}") handle error: + return error + secret[list[int64]] first = map.get[string, secret[list[int64]]](values, "first") handle: + return "missing" + list[int64] opened = declassify first + return "{list.length[int64](view opened)}" +function parse_result_secret() returns string: + result[secret[string], secret[bytes]] value = json.parse[result[secret[string], secret[bytes]]]("{{\"ok\":\"Ada\"}}") handle error: + return error + secret[string] opened = value handle error: + return "failure" + return declassify opened +function parse_optional_secret_float() returns string: + optional[secret[float32]] value = json.parse[optional[secret[float32]]]("1.25") handle error: + return error + secret[float32] opened = value handle: + return "missing" + float32 number = declassify opened + return "{number}" +function parse_exact_secret_error() returns string: + map[string, secret[list[int64]]] value = json.parse_exact[map[string, secret[list[int64]]]]("{{\"first\":[1,\"bad\"]}}") handle error: + return error + return "unexpected {map.length[string, secret[list[int64]]](view value)}" +function main() returns nothing: + println(parse_secret_list()) + println(parse_list_of_secrets()) + println(parse_secret_bytes_list()) + println(parse_map_of_secret_lists()) + println(parse_result_secret()) + println(parse_optional_secret_float()) + println(parse_exact_secret_error()) + return nothing diff --git a/tests/native/json_pipeline_source.jett b/tests/native/json_pipeline_source.jett new file mode 100644 index 00000000..8da169de --- /dev/null +++ b/tests/native/json_pipeline_source.jett @@ -0,0 +1,12 @@ +namespace app +struct User: + name: string +function serialize_pipeline() returns string: + User user = User(name: "Ada") + return user into view json.serialize[User]() +function serialize_public_pipeline() returns string: + User user = User(name: "Grace") + return user into view json.serialize_public[User]() +function main() returns nothing: + println(serialize_pipeline()) + println(serialize_public_pipeline()) diff --git a/tests/native/json_recursive_enum_probe.jett b/tests/native/json_recursive_enum_probe.jett new file mode 100644 index 00000000..f0579fcc --- /dev/null +++ b/tests/native/json_recursive_enum_probe.jett @@ -0,0 +1,18 @@ +namespace app +enum IntList: + empty + item(value: int64, next: IntList) +function total(value: IntList) returns int64: + match value: + empty: + return 0 + item(head, tail): + return head + total(tail) +function main() returns nothing: + IntList original = IntList.item(2, IntList.item(3, IntList.empty)) + string raw = json.serialize[IntList](view original) + IntList decoded = json.parse[IntList](raw) handle error: + println(error) + return nothing + println(total(decoded)) + return nothing diff --git a/tests/native/json_refined_aggregates.jett b/tests/native/json_refined_aggregates.jett new file mode 100644 index 00000000..0c7446a1 --- /dev/null +++ b/tests/native/json_refined_aggregates.jett @@ -0,0 +1,114 @@ +namespace app +type NonEmpty = string where value != "" +struct Profile: + name: string +type NamedProfile = Profile where value.name != "" +enum Event: + idle + named(name: app.NonEmpty) +enum MixedEvent: + idle + named(name: app.NonEmpty, note: string) +machine Session: + states: + guest + active(name: app.NonEmpty) + transitions: + guest to active +machine MixedSession: + states: + guest + active(name: app.NonEmpty, note: string) + transitions: + guest to active +machine ProfileSession: + states: + guest + active(profile: app.NamedProfile) + transitions: + guest to active +function parse_valid() returns string: + Session at active session = json.parse[Session at active]("{{\"state\":\"active\",\"payload\":{{\"name\":\"Ada\"}}}}") handle error: + return error + return coarsen session.name +function parse_invalid() returns string: + Session session = json.parse[Session]("{{\"state\":\"active\",\"payload\":{{\"name\":\"\"}}}}") handle error: + return error + return "unexpected success" +function parse_enum_valid() returns string: + Event event = json.parse[Event]("{{\"named\":[\"Lin\"]}}") handle error: + return error + match event: + idle: + return "idle" + named(name): + return coarsen name +function parse_enum_invalid() returns string: + Event event = json.parse[Event]("{{\"named\":[\"\"]}}") handle error: + return error + return "unexpected success" +function parse_mixed_machine() returns string: + MixedSession at active session = json.parse[MixedSession at active]("{{\"state\":\"active\",\"payload\":{{\"name\":\"Ada\",\"note\":\"ready\"}}}}") handle error: + return error + return "{coarsen session.name}:{session.note}" +function parse_mixed_enum() returns string: + MixedEvent event = json.parse[MixedEvent]("{{\"named\":[\"Lin\",\"ready\"]}}") handle error: + return error + match event: + idle: + return "idle" + named(name, note): + return "{coarsen name}:{note}" +function parse_profile_machine() returns string: + ProfileSession at active session = json.parse[ProfileSession at active]("{{\"state\":\"active\",\"payload\":{{\"profile\":{{\"name\":\"Tao\"}}}}}}") handle error: + return error + Profile profile = coarsen session.profile + return profile.name +function reject_profile_machine() returns string: + ProfileSession session = json.parse[ProfileSession]("{{\"state\":\"active\",\"payload\":{{\"profile\":{{\"name\":\"\"}}}}}}") handle error: + return error + return "unexpected success" +function construct_machine_exact() returns string: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return "missing state" + mutable TypeConstruction builder = type.construct_machine_start[Session at active](view state) handle error: + return error + TypeField field = list.get[TypeField](state.fields, 0) handle: + return "missing field" + NonEmpty name = "Mira" handle error: + return error + builder = type.construct_put[Session at active, NonEmpty](builder, view field, name) handle error: + return error + Session at active session = type.construct_finish[Session at active](builder) handle error: + return error + return coarsen session.name +function construct_enum_exact() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Event](), 1) handle: + return "missing variant" + mutable TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return error + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return "missing field" + NonEmpty name = "Nia" handle error: + return error + builder = type.construct_put[Event, NonEmpty](builder, view field, name) handle error: + return error + Event event = type.construct_finish[Event](builder) handle error: + return error + match event: + idle: + return "idle" + named(value): + return coarsen value +function main() returns nothing: + println(parse_valid()) + println(parse_invalid()) + println(parse_enum_valid()) + println(parse_enum_invalid()) + println(parse_mixed_machine()) + println(parse_mixed_enum()) + println(parse_profile_machine()) + println(reject_profile_machine()) + println(construct_machine_exact()) + println(construct_enum_exact()) + return nothing diff --git a/tests/native/json_refined_record_source.jett b/tests/native/json_refined_record_source.jett new file mode 100644 index 00000000..25bd268a --- /dev/null +++ b/tests/native/json_refined_record_source.jett @@ -0,0 +1,33 @@ +namespace app +type NonEmpty = string where string.char_count(value) > 0 +struct User: + name: NonEmpty +function parse_valid() returns string: + User user = json.parse[User]("{{\"name\":\"Ada\"}}") handle error: + return error + return coarsen user.name +function parse_invalid() returns string: + User user = json.parse[User]("{{\"name\":\"\"}}") handle error: + return error + return coarsen user.name +function parse_exact_unknown() returns string: + User user = json.parse_exact[User]("{{\"name\":\"Ada\",\"extra\":1}}") handle error: + return error + return coarsen user.name +function rebuild_refined() returns string: + User source = json.parse[User]("{{\"name\":\"Grace\"}}") handle error: + return error + mutable TypeConstruction builder = type.construct_start[User]() + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + User built = type.construct_finish[User](builder) handle error: + return error + return coarsen built.name +function main() returns nothing: + println(parse_valid()) + println(parse_invalid()) + println(parse_exact_unknown()) + println(rebuild_refined()) diff --git a/tests/native/json_refinement_source.jett b/tests/native/json_refinement_source.jett new file mode 100644 index 00000000..847ae1f3 --- /dev/null +++ b/tests/native/json_refinement_source.jett @@ -0,0 +1,33 @@ +namespace test +type NonEmpty = string where string.char_count(value) > 0 +struct Profile: + name: string +type Named = Profile where string.char_count(value.name) > 0 +function parse_scalar() returns string: + NonEmpty value = json.parse[NonEmpty]("\"Ada\"") handle error: + return error + return coarsen value +function reject_empty_scalar() returns string: + NonEmpty value = json.parse[NonEmpty]("\"\"") handle error: + return error + return coarsen value +function parse_exact_record() returns string: + Named value = json.parse_exact[Named]("{{\"name\":\"Ada\"}}") handle error: + return error + Profile raw = coarsen value + return raw.name +function reject_unknown_record_field() returns string: + Named value = json.parse_exact[Named]("{{\"name\":\"Ada\",\"extra\":1}}") handle error: + return error + Profile raw = coarsen value + return raw.name +function serialize_scalar() returns string: + NonEmpty value = "Ada" handle error: + return error + return json.serialize[NonEmpty](view value) +function serialize_record() returns string: + Named value = json.parse_exact[Named]("{{\"name\":\"Ada\"}}") handle error: + return error + return json.serialize[Named](view value) +function main() returns nothing: + println(parse_scalar(), reject_empty_scalar(), parse_exact_record(), reject_unknown_record_field(), serialize_scalar(), serialize_record()) diff --git a/tests/native/json_result_parse.jett b/tests/native/json_result_parse.jett new file mode 100644 index 00000000..d0a36706 --- /dev/null +++ b/tests/native/json_result_parse.jett @@ -0,0 +1,56 @@ +namespace app +function parse_ok() returns string: + result[int64, string] parsed = json.parse[result[int64, string]]("{{\"ok\":7}}") handle error: + return error + int64 value = parsed handle error: + return error + return "{value}" +function parse_fail() returns string: + result[int64, string] parsed = json.parse[result[int64, string]]("{{\"fail\":\"bad\"}}") handle error: + return error + int64 value = parsed handle error: + return error + return "unexpected {value}" +function parse_exact_ok() returns string: + result[optional[int64], string] parsed = json.parse_exact[result[optional[int64], string]]("{{\"ok\":null}}") handle error: + return error + optional[int64] maybe = parsed handle error: + return error + int64 value = maybe handle: + default -1 + return "{value}" +function parse_exact_fail() returns string: + result[int64, string] parsed = json.parse_exact[result[int64, string]]("{{\"fail\":\"exact\"}}") handle error: + return error + int64 value = parsed handle error: + return error + return "unexpected {value}" +function parse_nested() returns string: + list[result[int64, string]] values = json.parse[list[result[int64, string]]]("[{{\"ok\":1}},{{\"fail\":\"x\"}}]") handle error: + return error + return "{list.length[result[int64, string]](view values)}" +function parse_exact_nested() returns string: + map[string, result[int64, string]] values = json.parse_exact[map[string, result[int64, string]]]("{{\"first\":{{\"ok\":1}}}}") handle error: + return error + return "{map.length[string, result[int64, string]](view values)}" +function parse_multiple_keys_error() returns string: + result[int64, string] value = json.parse[result[int64, string]]("{{\"ok\":1,\"fail\":\"bad\"}}") handle error: + return error + int64 item = value handle error: + return error + return "unexpected {item}" +function parse_exact_payload_error() returns string: + result[int64, string] value = json.parse_exact[result[int64, string]]("{{\"ok\":true}}") handle error: + return error + int64 item = value handle error: + return error + return "unexpected {item}" +function main() returns nothing: + println(parse_ok()) + println(parse_fail()) + println(parse_exact_ok()) + println(parse_exact_fail()) + println(parse_nested()) + println(parse_exact_nested()) + println(parse_multiple_keys_error()) + println(parse_exact_payload_error()) diff --git a/tests/native/json_secret_source.jett b/tests/native/json_secret_source.jett new file mode 100644 index 00000000..760d4a31 --- /dev/null +++ b/tests/native/json_secret_source.jett @@ -0,0 +1,51 @@ +namespace app +struct Credential: + name: string + token: secret[string] +machine TokenLifecycle: + states: + issued(token: secret[string], public_id: string serialize "publicId") + revoked + transitions: + issued to revoked +function parse_secret() returns string: + secret[string] token = json.parse[secret[string]]("\"key\"") handle error: + return error + return secret.redact(token) +function parse_record() returns string: + Credential credential = json.parse[Credential]("{{\"name\":\"svc\",\"token\":\"key\"}}") handle error: + return error + return "{credential.name}:{secret.redact(credential.token)}:{json.serialize_public[Credential](view credential)}" +function parse_exact_extra() returns string: + Credential credential = json.parse_exact[Credential]("{{\"name\":\"svc\",\"token\":\"key\",\"extra\":true}}") handle error: + return error + return credential.name +function parse_exact_secret_record() returns string: + secret[Credential] credential = json.parse_exact[secret[Credential]]("{{\"name\":\"svc\",\"token\":\"key\"}}") handle error: + return error + return secret.redact(credential) +function parse_raw_secret() returns string: + secret[json.JsonTree] tree = json.parse[secret[json.JsonTree]]("{{\"open\":true}}") handle error: + return error + return secret.redact(tree) +function parse_exact_raw_secret() returns string: + secret[json.JsonTree] tree = json.parse_exact[secret[json.JsonTree]]("{{\"open\":true,\"nested\":{{\"kept\":1}}}}") handle error: + return error + return secret.redact(tree) +function parse_raw_secret_error() returns string: + secret[json.JsonTree] tree = json.parse_exact[secret[json.JsonTree]]("[1,") handle error: + return error + return secret.redact(tree) +function parse_machine() returns string: + TokenLifecycle token = json.parse[TokenLifecycle]("{{\"state\":\"issued\",\"payload\":{{\"token\":\"key\",\"publicId\":\"tok-1\"}}}}") handle error: + return error + return json.serialize_public[TokenLifecycle](view token) +function main() returns nothing: + println(parse_secret()) + println(parse_record()) + println(parse_exact_extra()) + println(parse_exact_secret_record()) + println(parse_raw_secret()) + println(parse_exact_raw_secret()) + println(parse_raw_secret_error()) + println(parse_machine()) diff --git a/tests/native/json_set_parse.jett b/tests/native/json_set_parse.jett new file mode 100644 index 00000000..114d1b2f --- /dev/null +++ b/tests/native/json_set_parse.jett @@ -0,0 +1,38 @@ +namespace app +function parse_int_set() returns string: + set[int64] values = json.parse[set[int64]]("[1,1,2]") handle error: + return error + int64 length = set.length[int64](values) + set[int64] values_for_one = json.parse[set[int64]]("[1,1,2]") handle error: + return error + bool has_one = set.contains[int64](values_for_one, 1) + set[int64] values_for_two = json.parse[set[int64]]("[1,1,2]") handle error: + return error + bool has_two = set.contains[int64](values_for_two, 2) + return "{length}:{has_one}:{has_two}" +function parse_string_set_exact() returns string: + set[string] values = json.parse_exact[set[string]]("[\"Ada\",\"Ada\",\"Lin\"]") handle error: + return error + int64 length = set.length[string](values) + set[string] for_lin = json.parse_exact[set[string]]("[\"Ada\",\"Ada\",\"Lin\"]") handle error: + return error + bool has_lin = set.contains[string](for_lin, "Lin") + return "{length}:{has_lin}" +function parse_bool_set() returns string: + set[bool] values = json.parse[set[bool]]("[true,false,true]") handle error: + return error + return "{set.length[bool](values)}" +function parse_uint_set() returns string: + set[uint64] values = json.parse[set[uint64]]("[0,42,42]") handle error: + return error + return "{set.length[uint64](values)}" +function parse_set_error() returns string: + set[int64] values = json.parse_exact[set[int64]]("[1,true]") handle error: + return error + return "unexpected {set.length[int64](values)}" +function main() returns nothing: + println(parse_int_set()) + println(parse_string_set_exact()) + println(parse_bool_set()) + println(parse_uint_set()) + println(parse_set_error()) diff --git a/tests/native/json_set_parse_extended.jett b/tests/native/json_set_parse_extended.jett new file mode 100644 index 00000000..d8be45dc --- /dev/null +++ b/tests/native/json_set_parse_extended.jett @@ -0,0 +1,39 @@ +namespace test +type Tag = string where string.char_count(value) > 0 +struct Bag: + values: set[int16] +function parse_int8_set() returns string: + set[int8] values = json.parse[set[int8]]("[-128,127,127]") handle error: + return error + int64 count = set.length[int8](clone values) + bool has_minimum = set.contains[int8](view values, -128) + return "{count}:{has_minimum}" +function parse_uint32_set() returns string: + set[uint32] values = json.parse_exact[set[uint32]]("[0,4294967295,4294967295]") handle error: + return error + int64 count = set.length[uint32](clone values) + bool has_maximum = set.contains[uint32](view values, 4294967295) + return "{count}:{has_maximum}" +function parse_tag_set() returns string: + set[Tag] values = json.parse_exact[set[Tag]]("[\"ready\",\"ready\",\"go\"]") handle error: + return error + return json.serialize[set[Tag]](view values) +function parse_bad_tag() returns string: + set[Tag] values = json.parse[set[Tag]]("[\"ok\",\"\"]") handle error: + return error + return "unexpected {set.length[Tag](values)}" +function parse_bad_int8() returns string: + set[int8] values = json.parse[set[int8]]("[-129]") handle error: + return error + return "unexpected {set.length[int8](values)}" +function parse_bag() returns string: + Bag bag = json.parse_exact[Bag]("{{\"values\":[-32768,32767,-32768]}}") handle error: + return error + return json.serialize[Bag](view bag) +function main() returns nothing: + println(parse_int8_set()) + println(parse_uint32_set()) + println(parse_tag_set()) + println(parse_bad_tag()) + println(parse_bad_int8()) + println(parse_bag()) diff --git a/tests/native/json_set_serialize.jett b/tests/native/json_set_serialize.jett new file mode 100644 index 00000000..d46b8eed --- /dev/null +++ b/tests/native/json_set_serialize.jett @@ -0,0 +1,30 @@ +namespace test +type Tag = string where string.char_count(value) > 0 +struct Bag: + values: set[int16] +function serialize_int_set() returns string: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 3) + values = set.add[int64](values, 1) + return json.serialize[set[int64]](view values) +function serialize_int_set_pipeline() returns string: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 8) + return values into view json.serialize[set[int64]]() +function serialize_tag_set() returns string: + Tag tag = "ready" handle error: + return error + mutable set[Tag] values = set.new[Tag]() + values = set.add[Tag](values, tag) + return json.serialize_public[set[Tag]](view values) +function serialize_bag() returns string: + mutable set[int16] values = set.new[int16]() + values = set.add[int16](values, -32768) + values = set.add[int16](values, 32767) + Bag bag = Bag(values: values) + return json.serialize[Bag](view bag) +function main() returns nothing: + println(serialize_int_set()) + println(serialize_int_set_pipeline()) + println(serialize_tag_set()) + println(serialize_bag()) diff --git a/tests/native/json_struct_parse.jett b/tests/native/json_struct_parse.jett new file mode 100644 index 00000000..73ee7a02 --- /dev/null +++ b/tests/native/json_struct_parse.jett @@ -0,0 +1,54 @@ +namespace app +struct User: + id: string serialize "userId" + age: int64 +type UserAlias = User +struct Envelope: + user: User + tags: list[string] +type Identifier = string +struct AliasUser: + id: Identifier +function parse_user() returns string: + User user = json.parse[User]("{{\"userId\":\"Ada\",\"age\":37}}") handle error: + return error + return "{user.id}:{user.age}" +function parse_alias_target() returns string: + UserAlias user = json.parse[UserAlias]("{{\"userId\":\"Alias\",\"age\":8}}") handle error: + return error + return "{user.id}:{user.age}" +function parse_nested() returns string: + Envelope envelope = json.parse[Envelope]("{{\"user\":{{\"userId\":\"Lin\",\"age\":4}},\"tags\":[\"x\",\"y\"]}}") handle error: + return error + return "{envelope.user.id}:{list.length[string](view envelope.tags)}" +function parse_list() returns string: + list[User] users = json.parse[list[User]]("[{{\"userId\":\"A\",\"age\":1}},{{\"userId\":\"B\",\"age\":2}}]") handle error: + return error + User second = list.get[User](users, 1) handle: + return "missing" + return "{second.id}:{second.age}" +function parse_alias_field() returns string: + AliasUser user = json.parse[AliasUser]("{{\"id\":\"alias\"}}") handle error: + return error + return user.id +function missing_field() returns string: + User user = json.parse[User]("{{\"userId\":\"Ada\"}}") handle error: + return error + return user.id +function wrong_field_type() returns string: + User user = json.parse[User]("{{\"userId\":\"Ada\",\"age\":true}}") handle error: + return error + return user.id +function exact_unknown_field() returns string: + User user = json.parse_exact[User]("{{\"userId\":\"Ada\",\"age\":37,\"extra\":1}}") handle error: + return error + return user.id +function main() returns nothing: + println(parse_user()) + println(parse_alias_target()) + println(parse_nested()) + println(parse_list()) + println(parse_alias_field()) + println(missing_field()) + println(wrong_field_type()) + println(exact_unknown_field()) diff --git a/tests/native/json_struct_serialize.jett b/tests/native/json_struct_serialize.jett new file mode 100644 index 00000000..0d7dd4d1 --- /dev/null +++ b/tests/native/json_struct_serialize.jett @@ -0,0 +1,36 @@ +namespace app +struct Profile: + display_name: string serialize "displayName" + score: int64 +struct User: + id: string + profile: Profile + tags: list[string] + scores: map[string, int64] + maybe_name: optional[string] + missing: optional[string] + outcome: result[int64, string] + failed: result[int64, string] +struct ScalarKinds: + small: int8 + narrow: uint16 + wide: uint64 + ratio: float64 + enabled: bool + payload: bytes + empty: nothing +function main() returns nothing: + Profile profile = Profile(display_name: "Ada \"L\"", score: -42) + User user = User(id: "u1", profile: profile, tags: list("writer", "coder"), scores: map("wins": 3, "losses": 1), maybe_name: some("ally"), missing: none, outcome: ok(7), failed: fail("bad")) + println(json.serialize[User](view user)) + println(json.serialize_public[User](view user)) + list[string] direct_tags = list("direct", "values") + println(json.serialize[list[string]](view direct_tags)) + map[string, int64] direct_scores = map("one": 1, "two": 2) + println(json.serialize_public[map[string, int64]](view direct_scores)) + optional[string] direct_optional = none + println(json.serialize[optional[string]](view direct_optional)) + result[int64, string] direct_result = fail("missing") + println(json.serialize_public[result[int64, string]](view direct_result)) + ScalarKinds kinds = ScalarKinds(small: -8, narrow: 65535, wide: 18446744073709551615, ratio: 2.5, enabled: true, payload: bytes.from_string("Hi"), empty: nothing) + println(json.serialize[ScalarKinds](view kinds)) diff --git a/tests/native/list_insert_invalid_index.jett b/tests/native/list_insert_invalid_index.jett new file mode 100644 index 00000000..7cdd24fd --- /dev/null +++ b/tests/native/list_insert_invalid_index.jett @@ -0,0 +1,5 @@ +namespace test +function main() returns nothing: + list[int64] values = list.insert_at[int64](list(1), 2, 3) + println(list.length[int64](view values)) + return nothing diff --git a/tests/native/list_insert_remove.jett b/tests/native/list_insert_remove.jett new file mode 100644 index 00000000..a00bf6e9 --- /dev/null +++ b/tests/native/list_insert_remove.jett @@ -0,0 +1,31 @@ +namespace app +function numbers() returns string: + mutable list[int64] values = list.insert_at[int64](list(2, 4), 0, 1) + values = list.insert_at[int64](values, 2, 3) + values = list.insert_at[int64](values, 4, 5) + values = list.remove_at[int64](values, 1) + int64 first = list.get[int64](view values, 0) handle: + default -1 + int64 middle = list.get[int64](view values, 1) handle: + default -1 + int64 last = list.get[int64](view values, 3) handle: + default -1 + return "{first}:{middle}:{last}:{list.length[int64](view values)}" +function words() returns string: + mutable list[string] values = list.insert_at[string](list("b", "c"), 0, "a") + values = list.remove_at[string](values, 1) + string first = list.get[string](view values, 0) handle: + default "missing" + string last = list.get[string](view values, 1) handle: + default "missing" + return "{first}:{last}" +function binary() returns string: + mutable list[bytes] values = list.insert_at[bytes](list(bytes.from_string("A"), bytes.from_string("B")), 1, bytes.from_string("X")) + values = list.remove_at[bytes](values, 0) + bytes first = list.get[bytes](view values, 0) handle: + default bytes.new() + return bytes.to_hex(view first) +function main() returns nothing: + println(numbers()) + println(words()) + println(binary()) diff --git a/tests/native/list_ordering_controls.jett b/tests/native/list_ordering_controls.jett new file mode 100644 index 00000000..b5ed9970 --- /dev/null +++ b/tests/native/list_ordering_controls.jett @@ -0,0 +1,145 @@ +namespace app +function signed_report() returns string: + list[int8] descending = list(7, -2) + bool before = list.is_sorted[int8](view descending) + list[list[int8]] rows = list(list(7, 1), list(-2, 2)) + list[list[int8]] ordered = list.sort_by_index[int8](rows, 0) + mutable string markers = "" + for row in view ordered: + int8 marker = list.get[int8](view row, 1) handle: default 0 + markers = "{markers}{marker}," + return "int8:{before}:{markers}" +function unsigned_report() returns string: + list[uint8] descending = list(255, 2) + bool before = list.is_sorted[uint8](view descending) + list[list[uint8]] rows = list(list(255, 1), list(2, 2)) + list[list[uint8]] ordered = list.sort_by_index[uint8](rows, 0) + mutable string markers = "" + for row in view ordered: + uint8 marker = list.get[uint8](view row, 1) handle: default 0 + markers = "{markers}{marker}," + return "uint8:{before}:{markers}" +function decimal_report() returns string: + list[float32] descending = list(3.5, -2.25) + bool before = list.is_sorted[float32](view descending) + list[list[float32]] rows = list(list(3.5, 1.0), list(-2.25, 2.0)) + list[list[float32]] ordered = list.sort_by_index[float32](rows, 0) + mutable string markers = "" + for row in view ordered: + float32 marker = list.get[float32](view row, 1) handle: default 0.0 + markers = "{markers}{marker}," + return "float32:{before}:{markers}" +function wide_unsigned_report() returns string: + list[uint64] descending = list(18446744073709551615, 9223372036854775808) + bool before = list.is_sorted[uint64](view descending) + list[list[uint64]] rows = list(list(18446744073709551615, 1), list(9223372036854775808, 2)) + list[list[uint64]] ordered = list.sort_by_index[uint64](rows, 0) + mutable string markers = "" + for row in view ordered: + uint64 marker = list.get[uint64](view row, 1) handle: default 0 + markers = "{markers}{marker}," + return "uint64:{before}:{markers}" +function float32_report() returns string: + float32 zero = 0.0 + float32 one = 1.0 + float32 nan = zero / zero + float32 positive = one / zero + float32 negative = -positive + list[float32] nan_left = list(nan, one) + list[float32] nan_right = list(one, nan) + list[float32] empty = list() + list[float32] singleton = list(nan) + list[float32] infinities = list(negative, zero, positive) + list[float32] reversed = list(positive, negative) + bool left = list.is_sorted[float32](view nan_left) + bool right = list.is_sorted[float32](view nan_right) + bool no_pair = list.is_sorted[float32](view empty) + bool one_value = list.is_sorted[float32](view singleton) + bool ready = list.is_sorted[float32](view infinities) + bool backwards = list.is_sorted[float32](view reversed) + list[float32] ordered = list.sort[float32](nan_left) + float32 first = list.first[float32](view ordered) handle: default 0.0 + float32 last = list.last[float32](view ordered) handle: default 0.0 + return "f32:nan:{left}:{right}:{no_pair}:{one_value};infinity:{ready}:{backwards};sort:{first != first}:{last == one}" +function float64_report() returns string: + float64 zero = 0.0 + float64 one = 1.0 + float64 nan = zero / zero + float64 positive = one / zero + float64 negative = -positive + list[float64] nan_left = list(nan, one) + list[float64] nan_right = list(one, nan) + list[float64] empty = list() + list[float64] singleton = list(nan) + list[float64] infinities = list(negative, zero, positive) + list[float64] reversed = list(positive, negative) + bool left = list.is_sorted[float64](view nan_left) + bool right = list.is_sorted[float64](view nan_right) + bool no_pair = list.is_sorted[float64](view empty) + bool one_value = list.is_sorted[float64](view singleton) + bool ready = list.is_sorted[float64](view infinities) + bool backwards = list.is_sorted[float64](view reversed) + list[float64] ordered = list.sort[float64](nan_left) + float64 first = list.first[float64](view ordered) handle: default 0.0 + float64 last = list.last[float64](view ordered) handle: default 0.0 + return "f64:nan:{left}:{right}:{no_pair}:{one_value};infinity:{ready}:{backwards};sort:{first != first}:{last == one}" +function pending_string_rows(view stdout: Stdout) returns nothing: + list[list[string]] rows = list(list(run run "zebra", "first"), list("apple", "second")) + list[list[string]] ordered = list.sort_by_index[string](rows, 0) + trace ordered + list[string] head = list.first[list[string]](view ordered) handle: return nothing + string key = list.first[string](view head) handle: return nothing + string once = join key handle error: return nothing + string joined = join once handle error: return nothing + string marker = list.last[string](view head) handle: return nothing + list[string] tail = list.last[list[string]](view ordered) handle: return nothing + string last = list.last[string](view tail) handle: return nothing + Stdout.write(view stdout, "index-string:{marker}:{last}:{joined}\n") +function pending_scalar_rows(view stdout: Stdout) returns nothing: + list[list[int64]] rows = list(list(run 2, 11), list(1, 22)) + list[list[int64]] ordered = list.sort_by_index[int64](rows, 0) + trace ordered + list[int64] head = list.first[list[int64]](view ordered) handle: return nothing + int64 key = list.first[int64](view head) handle: return nothing + int64 joined = join key handle error: return nothing + int64 marker = list.last[int64](view head) handle: return nothing + list[int64] tail = list.last[list[int64]](view ordered) handle: return nothing + int64 last = list.last[int64](view tail) handle: return nothing + Stdout.write(view stdout, "index-scalar:{marker}:{last}:{joined}\n") +function pending_outer_rows(view stdout: Stdout) returns nothing: + list[list[string]] rows = list(run run list("zebra", "first"), list("apple", "second")) + list[list[string]] ordered = list.sort_by_index[string](rows, 0) + trace ordered + list[string] head = list.first[list[string]](view ordered) handle: return nothing + list[string] once = join head handle error: return nothing + list[string] joined = join once handle error: return nothing + string key = list.first[string](view joined) handle: return nothing + string marker = list.last[string](view joined) handle: return nothing + list[string] tail = list.last[list[string]](view ordered) handle: return nothing + string last = list.last[string](view tail) handle: return nothing + Stdout.write(view stdout, "index-row:{marker}:{last}:{key}\n") +function ascending_signed(first: int8, second: int8) returns bool: + list[int8] ordered = list.sort[int8](list(first, second)) + return list.is_sorted[int8](view ordered) +function ascending_unsigned(first: uint8, second: uint8) returns bool: + list[uint8] ordered = list.sort[uint8](list(first, second)) + return list.is_sorted[uint8](view ordered) +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "{signed_report()}\n{unsigned_report()}\n{decimal_report()}\n{wide_unsigned_report()}\n{float32_report()}\n{float64_report()}\n") + pending_string_rows(view stdout) + pending_scalar_rows(view stdout) + pending_outer_rows(view stdout) +verify ordering: + assert signed_report() == "int8:false:2,1," + assert unsigned_report() == "uint8:false:2,1," + assert decimal_report() == "float32:false:2,1," + assert wide_unsigned_report() == "uint64:false:2,1," + assert float32_report() == "f32:nan:false:false:true:true;infinity:true:false;sort:true:true" + assert float64_report() == "f64:nan:false:false:true:true;infinity:true:false;sort:true:true" +property ordered_pairs: + given signed_first: int8 + given signed_second: int8 + given unsigned_first: uint8 + given unsigned_second: uint8 + assert ascending_signed(signed_first, signed_second) + assert ascending_unsigned(unsigned_first, unsigned_second) diff --git a/tests/native/list_remove_invalid_index.jett b/tests/native/list_remove_invalid_index.jett new file mode 100644 index 00000000..efbdd4dc --- /dev/null +++ b/tests/native/list_remove_invalid_index.jett @@ -0,0 +1,5 @@ +namespace test +function main() returns nothing: + list[int64] values = list.remove_at[int64](list(1), -1) + println(list.length[int64](view values)) + return nothing diff --git a/tests/native/list_sort.jett b/tests/native/list_sort.jett new file mode 100644 index 00000000..18c368bb --- /dev/null +++ b/tests/native/list_sort.jett @@ -0,0 +1,31 @@ +namespace app +function main() returns nothing: + list[int8] signed = list(-1, 3, -4, 2) + list[int8] signed_sorted = list.sort[int8](signed) + int8 signed_first = list.first[int8](view signed_sorted) handle: + default 0 + int8 signed_last = list.last[int8](view signed_sorted) handle: + default 0 + println(signed_first, signed_last) + list[uint64] unsigned = list(18446744073709551615, 2, 9223372036854775808) + list[uint64] unsigned_sorted = list.sort[uint64](unsigned) + uint64 unsigned_first = list.first[uint64](view unsigned_sorted) handle: + default 0 + uint64 unsigned_last = list.last[uint64](view unsigned_sorted) handle: + default 0 + println(unsigned_first, unsigned_last) + list[float64] decimals = list(1.5, -2.25, 0.0) + list[float64] decimals_sorted = list.sort[float64](decimals) + float64 decimal_first = list.first[float64](view decimals_sorted) handle: + default 0.0 + println(decimal_first) + list[string] words = list("zebra", "apple", "éclair") + list[string] words_sorted = list.sort[string](words) + string word_first = list.first[string](view words_sorted) handle: + default "" + println(word_first) + list[bool] flags = list(true, false, true) + list[bool] flags_sorted = list.sort[bool](flags) + bool flag_first = list.first[bool](view flags_sorted) handle: + default true + println(flag_first) diff --git a/tests/native/list_sort_refinements.jett b/tests/native/list_sort_refinements.jett new file mode 100644 index 00000000..daa6fa0b --- /dev/null +++ b/tests/native/list_sort_refinements.jett @@ -0,0 +1,234 @@ +namespace app +type Narrow = int8 where true +type Unsigned = uint64 where true +type Decimal = float32 where true +type Integer = int64 where true +type Word = string where string.char_count(value) > 0 +type Flag = bool where true +type Nested = Narrow where true +type Alias = Nested +type RawAlias = int64 +interface Named: + function name(view self: Named) returns string +implement Named for Narrow: + function name(view self: Narrow) returns string: + return "narrow:{coarsen self}" +implement Named for Nested: + function name(view self: Nested) returns string: + Narrow parent = coarsen self + return "nested:{coarsen parent}" +struct Words: + items: list[Word] +function sorted_values[T](items: list[T]) returns list[T]: + return list.sort[T](items) +function narrow_values() returns list[Narrow]: + int8 high_base = 127 + int8 low_base = -128 + int8 zero_base = 0 + Narrow high = high_base handle error: return list() + Narrow low = low_base handle error: return list() + Narrow zero = zero_base handle error: return list() + return list(high, clone low, zero, low) +function unsigned_values() returns list[Unsigned]: + uint64 high_base = 18446744073709551615 + uint64 middle_base = 9223372036854775808 + uint64 low_base = 2 + Unsigned high = high_base handle error: return list() + Unsigned middle = middle_base handle error: return list() + Unsigned low = low_base handle error: return list() + return list(clone high, low, middle, high) +function decimal_values() returns list[Decimal]: + float32 rounded_base = 16777217.0 + float32 high_base = 16777216.0 + float32 low_base = -2.25 + float32 zero_base = 0.0 + Decimal rounded = rounded_base handle error: return list() + Decimal high = high_base handle error: return list() + Decimal low = low_base handle error: return list() + Decimal zero = zero_base handle error: return list() + return list(rounded, low, high, zero) +function integer_values() returns list[Integer]: + int64 high_base = 9223372036854775807 + int64 low_base = -9223372036854775808 + int64 middle_base = 2 + Integer high = high_base handle error: return list() + Integer low = low_base handle error: return list() + Integer middle = middle_base handle error: return list() + return list(high, clone low, middle, low) +function word_values() returns list[Word]: + Word high = "zebra" handle error: return list() + Word low = "apple" handle error: return list() + Word middle = "eclair" handle error: return list() + return list(high, clone low, middle, low) +function flag_values() returns list[Flag]: + Flag yes = true handle error: return list() + Flag no = false handle error: return list() + return list(clone yes, clone no, no, yes) +function nested_values() returns list[Alias]: + int8 first_raw = 2 + int8 second_raw = -3 + Narrow first_base = first_raw handle error: return list() + Narrow second_base = second_raw handle error: return list() + Alias first = first_base handle error: return list() + Alias second = second_base handle error: return list() + return list(first, clone second, second) +function narrow_report() returns string: + list[Narrow] items = sorted_values[Narrow](narrow_values()) + mutable string text = "" + for value in view items: + int8 raw = coarsen value + text = "{text}{raw}," + Narrow first = list.first[Narrow](view items) handle: return "empty" + return "{text}|{Named.name(view first)}" +function unsigned_report() returns string: + list[Unsigned] items = sorted_values[Unsigned](unsigned_values()) + mutable string text = "" + for value in view items: + uint64 raw = coarsen value + text = "{text}{raw}," + return text +function decimal_report() returns string: + list[Decimal] items = sorted_values[Decimal](decimal_values()) + mutable string text = "" + for value in view items: + float32 raw = coarsen value + text = "{text}{raw}," + return text +function integer_report() returns string: + list[Integer] items = sorted_values[Integer](integer_values()) + mutable string text = "" + for value in view items: + int64 raw = coarsen value + text = "{text}{raw}," + return text +function word_report() returns string: + Words words = Words(items: word_values()) + list[Word] items = sorted_values[Word](words.items) + mutable string text = "" + for value in view items: + string raw = coarsen value + text = "{text}{raw}," + return "{text}|source:{list.length[Word](view words.items)}" +function flag_report() returns string: + list[Flag] items = sorted_values[Flag](flag_values()) + mutable string text = "" + for value in view items: + bool raw = coarsen value + text = "{text}{raw}," + return text +function nested_report() returns string: + list[Alias] items = sorted_values[Alias](nested_values()) + mutable string text = "" + for value in view items: + Narrow parent = coarsen value + int8 raw = coarsen parent + text = "{text}{raw}," + Alias first = list.first[Alias](view items) handle: return "empty" + return "{text}|{Named.name(view first)}" +function alias_report() returns string: + list[RawAlias] items = sorted_values[RawAlias](list(3, -2, 3)) + mutable string text = "" + for value in view items: + text = "{text}{value}," + return text +function refined_sortedness() returns string: + list[Narrow] original = narrow_values() + bool before = list.is_sorted[Narrow](view original) + list[Narrow] ordered = sorted_values[Narrow](original) + bool after = list.is_sorted[Narrow](view ordered) + return "refined:{before}:{after}" +function edge_report() returns string: + list[Narrow] empty = sorted_values[Narrow](list()) + list[Word] empty_words = sorted_values[Word](list()) + int8 raw_base = 5 + Narrow base = raw_base handle error: return "rejected" + Alias only = base handle error: return "rejected" + list[Alias] singleton = sorted_values[Alias](list(only)) + Alias first = list.first[Alias](view singleton) handle: return "empty" + Narrow parent = coarsen first + int8 raw = coarsen parent + Word word = "solo" handle error: return "rejected" + list[Word] single_word = sorted_values[Word](list(word)) + Word found = list.first[Word](view single_word) handle: return "empty" + return "empty:{list.length[Narrow](view empty)}:{list.length[Word](view empty_words)};single:{raw}:{coarsen found}" +function nan_sortedness() returns bool: + float32 zero = 0.0 + float32 one = 1.0 + float32 nan = zero / zero + list[float32] values = list(nan, one) + return list.is_sorted[float32](view values) +function baked_report() returns string: + list[Narrow] baked = comptime sorted_values[Narrow](narrow_values()) + Narrow first = list.first[Narrow](view baked) handle: return "empty" + Narrow last = list.last[Narrow](view baked) handle: return "empty" + list[Word] words = comptime sorted_values[Word](word_values()) + Word first_word = list.first[Word](view words) handle: return "empty" + Word last_word = list.last[Word](view words) handle: return "empty" + bool nan_sorted = comptime nan_sortedness() + return "baked:{Named.name(view first)}:{coarsen last}:{coarsen first_word}:{coarsen last_word}:{nan_sorted}" +function ordered_pair(first: Narrow, second: Narrow) returns bool: + list[Narrow] items = sorted_values[Narrow](list(first, second)) + mutable int8 previous = -128 + for value in view items: + int8 raw = coarsen value + if raw < previous: + return false + previous = raw + return list.length[Narrow](view items) == 2 +function pending_word_control(view stdout: Stdout) returns nothing: + Word high = "zebra" handle error: return nothing + Word low = "apple" handle error: return nothing + list[Word] items = sorted_values[Word](list(run run high, low)) + Word head = list.first[Word](view items) handle: return nothing + trace head + Word once = join head handle error: return nothing + trace once + Word joined = join once handle error: return nothing + trace joined + Word tail = list.last[Word](view items) handle: return nothing + Stdout.write(view stdout, "pending:{coarsen joined}:{coarsen tail}\n") + list[Word] ready = sorted_values[Word](list(joined, tail)) + Word first = list.first[Word](view ready) handle: return nothing + Word last = list.last[Word](view ready) handle: return nothing + Stdout.write(view stdout, "ready:{coarsen first}:{coarsen last}\n") +function pending_string_control(view stdout: Stdout) returns nothing: + list[string] items = sorted_values[string](list(run "zebra", "apple")) + bool pending_sorted = list.is_sorted[string](view items) + string head = list.first[string](view items) handle: return nothing + trace head + string joined = join head handle error: return nothing + trace joined + string tail = list.last[string](view items) handle: return nothing + Stdout.write(view stdout, "plain-pending:{joined}:{tail}\n") + list[string] unsorted = list(joined, tail) + bool before = list.is_sorted[string](view unsorted) + list[string] ready = sorted_values[string](unsorted) + bool after = list.is_sorted[string](view ready) + string first = list.first[string](view ready) handle: return nothing + string last = list.last[string](view ready) handle: return nothing + Stdout.write(view stdout, "plain-ready:{first}:{last};sorted:{pending_sorted}:{before}:{after}\n") +function pending_outer_failure(view stdout: Stdout) returns nothing: + list[Narrow] values = run run narrow_values() + Stdout.write(view stdout, "before\n") + list[Narrow] items = sorted_values[Narrow](values) + Stdout.write(view stdout, "{list.length[Narrow](view items)}\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "narrow:{narrow_report()}\nunsigned:{unsigned_report()}\ndecimal:{decimal_report()}\ninteger:{integer_report()}\nword:{word_report()}\nflag:{flag_report()}\nnested:{nested_report()}\nalias:{alias_report()}\n{edge_report()}\n{refined_sortedness()}\n{baked_report()}\n") + pending_word_control(view stdout) + pending_string_control(view stdout) +verify refined_sorts: + assert narrow_report() == "-128,-128,0,127,|narrow:-128" + assert unsigned_report() == "2,9223372036854775808,18446744073709551615,18446744073709551615," + assert decimal_report() == "-2.25,0,16777216,16777216," + assert integer_report() == "-9223372036854775808,-9223372036854775808,2,9223372036854775807," + assert word_report() == "apple,apple,eclair,zebra,|source:4" + assert flag_report() == "false,false,true,true," + assert nested_report() == "-3,-3,2,|nested:-3" + assert alias_report() == "-2,3,3," + assert edge_report() == "empty:0:0;single:5:solo" + assert refined_sortedness() == "refined:false:true" + assert baked_report() == "baked:narrow:-128:127:apple:zebra:false" +property refined_pairs: + given first: Narrow + given second: Narrow + assert ordered_pair(first, second) diff --git a/tests/native/list_source_values.jett b/tests/native/list_source_values.jett new file mode 100644 index 00000000..2242d125 --- /dev/null +++ b/tests/native/list_source_values.jett @@ -0,0 +1,50 @@ +namespace app +function parity(value: int64) returns string: + if value modulo 2 == 0: + return "even" + return "odd" +function sorted_name() returns string: + list[list[string]] names = list.sort_by_index[string](list(list("b", "2"), list("a", "1")), 0) + list[string] first_name_row = list.get[list[string]](view names, 0) handle: + default list() + string first_name = list.get[string](view first_name_row, 0) handle: + default "missing" + return first_name +function sorted_uint64() returns uint64: + list[list[uint64]] numbers = list.sort_by_index[uint64](list(list(18446744073709551615), list(2), list(9223372036854775808)), 0) + list[uint64] first_number_row = list.get[list[uint64]](view numbers, 0) handle: + default list() + uint64 first_number = list.get[uint64](view first_number_row, 0) handle: + default 0 + return first_number +function sorted_float64() returns float64: + list[list[float64]] floats = list.sort_by_index[float64](list(list(3.5), list(-1.0)), 0) + list[float64] first_float_row = list.get[list[float64]](view floats, 0) handle: + default list() + float64 first_float = list.get[float64](view first_float_row, 0) handle: + default 0.0 + return first_float +function sorted_bool() returns bool: + list[list[bool]] flags = list.sort_by_index[bool](list(list(true), list(false)), 0) + list[bool] first_flag_row = list.get[list[bool]](view flags, 0) handle: + default list() + bool first_flag = list.get[bool](view first_flag_row, 0) handle: + default true + return first_flag +function missing_key_stays_first() returns bool: + list[list[string]] missing = list.sort_by_index[string](list(list("a"), list("b")), -1) + list[string] first_missing_row = list.get[list[string]](view missing, 0) handle: + default list() + string first_missing = list.get[string](view first_missing_row, 0) handle: + default "missing" + return first_missing == "a" +function even_group_size() returns int64: + map[string, list[int64]] groups = list.group_by[int64](list(1, 2, 3, 4), parity) + list[int64] evens = map.get[string, list[int64]](groups, "even") handle: + default list() + return list.length[int64](view evens) +function main() returns nothing: + list[string] words = list("a", "b") + list[uint64] unsigneds = list(2, 18446744073709551615) + list[bool] booleans = list(true, false) + println(sorted_name(), sorted_uint64() == 2, sorted_float64() == -1.0, not sorted_bool(), missing_key_stays_first(), even_group_size(), list.is_sorted[string](view words), list.is_sorted[uint64](view unsigneds), not list.is_sorted[bool](view booleans)) diff --git a/tests/native/list_sum_primitives.jett b/tests/native/list_sum_primitives.jett new file mode 100644 index 00000000..e36b043a --- /dev/null +++ b/tests/native/list_sum_primitives.jett @@ -0,0 +1,84 @@ +namespace app +interface Named: + function name(view self: Named) returns string +type Small = int8 +implement Named for int8: + function name(view self: int8) returns string: + return "int8:{self}" +implement Named for int16: + function name(view self: int16) returns string: + return "int16:{self}" +implement Named for int32: + function name(view self: int32) returns string: + return "int32:{self}" +implement Named for int64: + function name(view self: int64) returns string: + return "int64:{self}" +implement Named for uint8: + function name(view self: uint8) returns string: + return "uint8:{self}" +implement Named for uint16: + function name(view self: uint16) returns string: + return "uint16:{self}" +implement Named for uint32: + function name(view self: uint32) returns string: + return "uint32:{self}" +implement Named for uint64: + function name(view self: uint64) returns string: + return "uint64:{self}" +implement Named for float32: + function name(view self: float32) returns string: + return "float32:{self}" +implement Named for float64: + function name(view self: float64) returns string: + return "float64:{self}" +function total[T](values: list[T]) returns T: + return list.sum[T](view values) +function totals() returns list[Named]: + Small small = total[Small](list(127, 1)) + int16 medium = total[int16](list(32767, 1)) + int32 larger = total[int32](list(2147483647, 1)) + int64 largest = total[int64](list(9223372036854775807, 1)) + uint8 unsigned_small = total[uint8](list(255, 2)) + uint16 unsigned_medium = total[uint16](list(65535, 2)) + uint32 unsigned_larger = total[uint32](list(4294967295, 2)) + uint64 unsigned_largest = total[uint64](list(18446744073709551615, 2)) + float32 narrow = total[float32](list(16777216.0, 1.0, -16777216.0)) + float64 wide = total[float64](list(16777216.0, 1.0, -16777216.0)) + return list(small, medium, larger, largest, unsigned_small, unsigned_medium, unsigned_larger, unsigned_largest, narrow, wide) +function empty_totals() returns list[Named]: + int8 small = total[int8](list()) + int16 medium = total[int16](list()) + int32 larger = total[int32](list()) + int64 largest = total[int64](list()) + uint8 unsigned_small = total[uint8](list()) + uint16 unsigned_medium = total[uint16](list()) + uint32 unsigned_larger = total[uint32](list()) + uint64 unsigned_largest = total[uint64](list()) + float32 narrow = total[float32](list()) + float64 wide = total[float64](list()) + return list(small, medium, larger, largest, unsigned_small, unsigned_medium, unsigned_larger, unsigned_largest, narrow, wide) +function describe(values: list[Named]) returns string: + mutable string text = "" + for value in values: + text = "{text}{Named.name(view value)};" + return text +function main(stdout: Stdout) returns nothing: + list[Named] live = totals() + list[Named] baked = comptime totals() + list[Named] empty_live = empty_totals() + list[Named] empty_baked = comptime empty_totals() + Stdout.write(view stdout, "{describe(live)}\n{describe(baked)}\n{describe(empty_live)}\n{describe(empty_baked)}\n") +verify primitive_sums: + string expected = "int8:-128;int16:-32768;int32:-2147483648;int64:-9223372036854775808;uint8:1;uint16:1;uint32:1;uint64:1;float32:0;float64:1;" + assert describe(totals()) == expected + list[Named] baked = comptime totals() + assert describe(baked) == expected + string empty_expected = "int8:0;int16:0;int32:0;int64:0;uint8:0;uint16:0;uint32:0;uint64:0;float32:0;float64:0;" + assert describe(empty_totals()) == empty_expected + list[Named] empty_baked = comptime empty_totals() + assert describe(empty_baked) == empty_expected +property primitive_sum_trials: + given value: int64 + assert total[int64](list(value, 1)) == value + 1 + assert total[int64](list(value, -1)) == value - 1 diff --git a/tests/native/local_view_aliases.jett b/tests/native/local_view_aliases.jett new file mode 100644 index 00000000..844a62af --- /dev/null +++ b/tests/native/local_view_aliases.jett @@ -0,0 +1,235 @@ +namespace app +struct Record: + values: list[int64] + label: string +type Label = string where true +enum Choice: + selected(values: list[int64]) + empty +interface Named: + function name(view self: Named) returns string +implement Named for Record: + function name(view self: Record) returns string: + return self.label +function total(view values: list[int64]) returns int64: + mutable int64 amount = 0 + for number in view values: + amount = amount + number + return amount +function increment(value: int64) returns int64: + return value + 1 +function cloned_alias[T](view source: T) returns T: + T borrowed = view source + T forwarded = borrowed + return clone forwarded +function parameter_report(view source: list[int64]) returns string: + list[int64] borrowed = view source + list[int64] forwarded = borrowed + list[int64] copied = clone forwarded + return "{total(view forwarded)}:{total(view copied)}:{list.length(view source)}" +function list_report() returns string: + list[int64] source = list(2, 3, 5) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + mutable list[int64] copied = clone forwarded + copied = list.append[int64](copied, 7) + return "{total(view forwarded)}:{total(view copied)}:{list.length(view source)}:{parameter_report(view source)}" +function bytes_report() returns string: + bytes source = bytes.from_string("AB") + bytes borrowed = view source + bytes forwarded = borrowed + bytes copied = clone forwarded + return "{bytes.to_hex(view forwarded)}:{bytes.to_hex(view copied)}:{bytes.length(view source)}" +function string_report() returns string: + secret[string] source = "secret" + secret[string] borrowed = view source + secret[string] forwarded = borrowed + string copied = declassify clone forwarded + string public_copy = declassify forwarded + Label label = "refined" handle error: + return error + Label label_alias = view label + Label label_forwarded = label_alias + Label label_clone = clone label_forwarded + return "{copied}:{public_copy}:{declassify source}:{coarsen label_forwarded}:{coarsen label_clone}:{coarsen label}" +function record_report() returns string: + Record source = Record(values: list(7, 11), label: "record") + Record borrowed = view source + Record forwarded = borrowed + Record copied = clone forwarded + return "{forwarded.label}:{total(view forwarded.values)}:{total(view copied.values)}:{total(view source.values)}" +function interface_report() returns string: + Named source = Record(values: list(7), label: "named") + Named borrowed = view source + Named forwarded = borrowed + Named copied = clone forwarded + return "{Named.name(view forwarded)}:{Named.name(view copied)}:{Named.name(view source)}" +function function_report() returns string: + int64 offset = 3 + function(int64) returns int64 source = function(value: int64) returns int64: return value + offset + function(int64) returns int64 borrowed = view source + function(int64) returns int64 forwarded = borrowed + function(int64) returns int64 copied = clone forwarded + function(int64) returns int64 named = increment + function(int64) returns int64 named_alias = view named + return "{forwarded(7)}:{copied(8)}:{source(9)}:{named_alias(10)}:{named(11)}" +function generic_report() returns string: + int64 number = 7 + int64 scalar = cloned_alias[int64](view number) + list[int64] source = list(2, 3) + list[int64] copied = cloned_alias[list[int64]](view source) + return "{scalar}:{number}:{total(view copied)}:{total(view source)}" +function choice_total(view source: Choice) returns int64: + match view source: + selected(values): + return total(view values) + empty: + return 0 +function sum_report() returns string: + Choice source = Choice.selected(list(2, 3)) + Choice borrowed = view source + Choice forwarded = borrowed + mutable int64 amount = 0 + match forwarded: + selected(values): + amount = total(view values) + empty: + amount = -1 + return "{amount}:{choice_total(view source)}:{choice_total(view forwarded)}" +function selected_length(view source: list[int64], marker: int64) returns int64: + return list.length(view source) + marker +function selected_secret(view source: secret[string], marker: int64) returns string: + return "{declassify source}:{marker}" +function selected_label(view source: Label, marker: int64) returns string: + return "{coarsen source}:{marker}" +function handler_report() returns string: + list[int64] source = list(7, 11) + list[int64] borrowed = view source + function(view list[int64], int64) returns int64 callback = selected_length + function(view list[int64], int64) returns int64 callback_alias = view callback + int64 direct = selected_length(view borrowed,(none handle: default 1)) + list[int64] indirect_borrowed = view source + int64 indirect = callback_alias(view indirect_borrowed,(none handle: default 2)) + secret[string] hidden = "secret" + secret[string] hidden_alias = view hidden + string explicit = selected_secret(view hidden_alias,(none handle: default 3)) + secret[string] implicit_alias = view hidden + string implicit = selected_secret(view implicit_alias,(none handle: default 4)) + Label label = "refined" handle error: + return error + Label label_alias = view label + string refined = selected_label(view label_alias,(none handle: default 5)) + return "{direct}:{indirect}:{explicit}:{implicit}:{refined}" +function copied_callbacks(view source: function(int64) returns int64) returns list[function(int64) returns int64]: + function(int64) returns int64 borrowed = view source + return list(clone borrowed) +function copied_secret_text(view source: secret[string]) returns optional[string]: + secret[string] borrowed = view source + return some(declassify clone borrowed) +function copied_texts(view source: string) returns list[string]: + return list(view source, clone view source) +function copied_text_sum(view source: string) returns optional[string]: + return some(view source) +function collection_report() returns string: + int64 offset = 3 + function(int64) returns int64 source = function(value: int64) returns int64: return value + offset + list[function(int64) returns int64] callbacks = copied_callbacks(view source) + function(int64) returns int64 callback = list.get[function(int64) returns int64](view callbacks, 0) handle: default increment + string hidden = copied_secret_text("secret") handle: default "missing" + list[string] texts = copied_texts("owned") + string text = list.get[string](view texts, 1) handle: default "missing" + string sum = copied_text_sum("sum") handle: default "missing" + return "{callback(7)}:{source(8)}:{hidden}:{text}:{sum}" +function scope_report(view source: list[int64]) returns int64: + mutable int64 amount = 0 + if true: + list[int64] borrowed = view source + list[int64] forwarded = borrowed + amount = total(view forwarded) + else: + list[int64] borrowed = view source + amount = total(view borrowed) + for number in view source: + list[int64] borrowed = view source + amount = amount + number + list.length(view borrowed) + list[int64] borrowed = list(13) + return amount + total(view borrowed) +function reflected_report[T](view source: T) returns string: + mutable string text = "" + for field in type.fields[T](): + T borrowed = view source + T forwarded = borrowed + T copied = clone forwarded + text = "{text}{field.name}:{type.name[T]()};" + return text +function reflection_report() returns string: + Record source = Record(values: list(7), label: "reflection") + string fields = reflected_report[Record](view source) + return "{fields}{source.label}:{total(view source.values)}" +function pending_report() returns string: + list[int64] source = run list(7, 11) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + list[int64] ready = join clone forwarded handle error: + return error + list[int64] again = join clone source handle error: + return error + return "{total(view ready)}:{total(view again)}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "lists:{list_report()}\n") + Stdout.write(view stdout, "bytes:{bytes_report()}\n") + Stdout.write(view stdout, "strings:{string_report()}\n") + Stdout.write(view stdout, "records:{record_report()}\n") + Stdout.write(view stdout, "interfaces:{interface_report()}\n") + Stdout.write(view stdout, "functions:{function_report()}\n") + Stdout.write(view stdout, "generics:{generic_report()}\n") + Stdout.write(view stdout, "sums:{sum_report()}\n") + Stdout.write(view stdout, "handlers:{handler_report()}\n") + Stdout.write(view stdout, "collections:{collection_report()}\n") + list[int64] scope_source = list(2, 3) + Stdout.write(view stdout, "scopes:{scope_report(view scope_source)}:{total(view scope_source)}\n") + Stdout.write(view stdout, "reflection:{reflection_report()}\n") + Stdout.write(view stdout, "pending:{pending_report()}\n") + list[int64] source = list(2, 3, 5) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + trace source + trace borrowed + trace forwarded + secret[string] hidden = "hidden" + secret[string] hidden_alias = view hidden + trace hidden_alias + Stdout.write(view stdout, "debug-root:{total(view source)}:{total(view forwarded)}\n") +verify local_alias_values: + assert list_report() == "10:17:3:10:10:3" + assert bytes_report() == "4142:4142:2" + assert string_report() == "secret:secret:secret:refined:refined:refined" + assert record_report() == "record:18:18:18" + assert interface_report() == "named:named:named" + assert function_report() == "10:11:12:11:12" + assert generic_report() == "7:7:5:5" + assert sum_report() == "5:5:5" + assert handler_report() == "3:4:secret:3:secret:4:refined:5" + assert collection_report() == "10:11:secret:owned:sum" + assert scope_report(view list(2, 3)) == 27 + assert reflection_report() == "values:app.Record;label:app.Record;reflection:7" + assert pending_report() == "18:18" +property local_alias_trials: + given seed: int64 + list[int64] source = list(seed, seed + 1) + list[int64] borrowed = view source + list[int64] forwarded = borrowed + list[int64] copied = cloned_alias[list[int64]](view forwarded) + assert total(view forwarded) == seed +(seed + 1) + assert total(view copied) == total(view source) + assert list.length(view source) == 2 + assert cloned_alias[int64](view seed) == seed + assert bytes_report() == "4142:4142:2" + assert string_report() == "secret:secret:secret:refined:refined:refined" + assert sum_report() == "5:5:5" + assert handler_report() == "3:4:secret:3:secret:4:refined:5" + assert collection_report() == "10:11:secret:owned:sum" + assert record_report() == "record:18:18:18" + assert interface_report() == "named:named:named" + assert function_report() == "10:11:12:11:12" + assert reflection_report() == "values:app.Record;label:app.Record;reflection:7" diff --git a/tests/native/machine_values.jett b/tests/native/machine_values.jett new file mode 100644 index 00000000..b8254cb2 --- /dev/null +++ b/tests/native/machine_values.jett @@ -0,0 +1,70 @@ +namespace app +machine Session: + states: + guest + logged_in(user_id: string) + transitions: + guest to logged_in +type SessionAlias = Session +function describe(view session: Session) returns string: + if session at logged_in: + return session.user_id + return "guest" +function login_if_guest(session: Session) returns Session: + if session at guest: + return Session.transition(session, logged_in, "Lin") + return session +machine Review: + states: + draft(reason: string) + approved(user_id: string) + rejected(reason: string) + transitions: + draft to approved + approved to rejected +function review_label(review: Review) returns string: + if review at approved: + return review.user_id + else if not(review at draft): + return review.reason + else: + return review.reason +function reflect_session(view session: Session) returns nothing: + TypeMachineState state = type.machine_state_value[Session](view session) + println(state.index, state.owner_type, state.name, state.has_secret, list.length[TypeField](state.fields)) + for field in state.fields: + println(field.index, field.owner_type, field.name, field.type_name) + if field.name == "user_id": + string copied = type.machine_field_value[Session, string](view session, view field) + println(copied) +function reflect_logged(view session: Session at logged_in) returns nothing: + TypeMachineState state = type.machine_state_value[Session at logged_in](view session) + println(state.index, state.owner_type, state.name) + for field in state.fields: + if field.name == "user_id": + string copied = type.machine_field_value[Session at logged_in, string](view session, view field) + println(copied) +function reflect_layout() returns nothing: + TypeMachine layout = type.machine_layout[Session]() + println(list.length[TypeMachineState](layout.states), list.length[TypeMachineTransition](layout.edges)) + for state in type.machine_states[Session at logged_in](): + println(state.index, state.owner_type, state.name, state.has_secret, list.length[TypeField](state.fields)) + for edge in type.machine_transitions[Session](): + println(edge.index, edge.source_index, edge.source, edge.target_index, edge.target) + println(list.length[TypeMachineState](type.machine_states[SessionAlias]())) + println(list.length[TypeMachineTransition](type.machine_transitions[int64]())) + println(list.length[TypeMachineState](type.machine_layout[int64]().states)) +function main() returns nothing: + reflect_layout() + Session at guest start = Session(guest) + println(start at guest) + reflect_session(view start) + Session at logged_in logged = Session.transition(start, logged_in, "Ada") + println(logged at logged_in, logged.user_id) + reflect_session(view logged) + reflect_logged(view logged) + Session session = login_if_guest(Session(guest)) + println(describe(view session), session at logged_in) + println(review_label(Review(approved, "Lin"))) + println(review_label(Review(draft, "pending"))) + println(review_label(Review(rejected, "policy"))) diff --git a/tests/native/map_values.jett b/tests/native/map_values.jett new file mode 100644 index 00000000..082c5e74 --- /dev/null +++ b/tests/native/map_values.jett @@ -0,0 +1,26 @@ +namespace app +function main() returns nothing: + mutable map[string, int64] scores = map("ada": 1, "bob": 2, "ada": 3) + println(map.length[string, int64](view scores), map.has[string, int64](view scores, "ada")) + println(map.get_or[string, int64](clone scores, "ada", 0)) + for key, value in clone scores: + println(key, value) + scores = map.insert[string, int64](scores, "bob", 7) + println(map.get_or[string, int64](clone scores, "bob", 0)) + scores = map.remove[string, int64](scores, "ada") + println(map.length[string, int64](view scores), map.has[string, int64](view scores, "ada")) + mutable map[int64, string] names = map.new[int64, string]() + names = map.insert[int64, string](names, 1, "first") + names = map.insert[int64, string](names, 1, "second") + for key, value in view names: + println(key, value) + println(map.get_or[int64, string](clone names, 1, "missing")) + println(map.get_or[int64, string](names, 2, "missing")) + map[string, int64] zipped = map.from_lists[string, int64](list("x", "x", "y"), list(2, 5)) + println(map.length[string, int64](view zipped), map.get_or[string, int64](zipped, "x", 0)) + map[string, int64] merged = map.merge[string, int64](map("left": 1), map("left": 4, "right": 5)) + println(map.length[string, int64](view merged), map.get_or[string, int64](clone merged, "left", 0)) + for key in map.keys[string, int64](clone merged): + println(key) + for value in map.values[string, int64](merged): + println(value) diff --git a/tests/native/math_aggregate.jett b/tests/native/math_aggregate.jett new file mode 100644 index 00000000..d68f177a --- /dev/null +++ b/tests/native/math_aggregate.jett @@ -0,0 +1,10 @@ +namespace app + +function main() returns nothing: + list[int64] signed_average = list(-3, 1, 8) + list[int64] signed_median = list(-3, 1, 8, 11) + list[uint64] unsigned_average = list(2, 4, 6) + list[uint64] unsigned_median = list(2, 4, 6, 8) + list[float64] float_average = list(1.0, 2.0, 3.0) + list[float64] float_median = list(1.0, 2.0, 3.0, 4.0) + println(math.average(signed_average), math.median(signed_median), math.average(unsigned_average), math.median(unsigned_median), math.average(float_average), math.median(float_median)) diff --git a/tests/native/math_aggregate_primitives.jett b/tests/native/math_aggregate_primitives.jett new file mode 100644 index 00000000..51381ea1 --- /dev/null +++ b/tests/native/math_aggregate_primitives.jett @@ -0,0 +1,104 @@ +namespace app +type Small = int8 +type WideUnsigned = uint64 +function summarize[T](values: list[T]) returns string: + float64 average = math.average[T](clone values) + float64 median = math.median[T](values) + return "{average}:{median}" +function signed8_report() returns string: + list[Small] values = list(-128, 127) + return "int8:{summarize[Small](values)}" +function signed16_report() returns string: + list[int16] values = list(-32768, 32767) + return "int16:{summarize[int16](values)}" +function signed32_report() returns string: + list[int32] values = list(-2147483648, 2147483647) + return "int32:{summarize[int32](values)}" +function signed64_report() returns string: + list[int64] values = list(-9223372036854775808, 9223372036854775807) + return "int64:{summarize[int64](values)}" +function unsigned8_report() returns string: + list[uint8] values = list(0, 255) + return "uint8:{summarize[uint8](values)}" +function unsigned16_report() returns string: + list[uint16] values = list(0, 65535) + return "uint16:{summarize[uint16](values)}" +function unsigned32_report() returns string: + list[uint32] values = list(0, 4294967295) + return "uint32:{summarize[uint32](values)}" +function unsigned64_report() returns string: + list[WideUnsigned] values = list(9223372036854775808, 18446744073709551615) + return "uint64:{summarize[WideUnsigned](values)}" +function decimal32_report() returns string: + list[float32] values = list(16777216.0, 16777217.0, -16777216.0) + return "float32:{summarize[float32](values)}" +function decimal64_report() returns string: + list[float64] values = list(16777216.0, 16777217.0, -16777216.0) + return "float64:{summarize[float64](values)}" +function numeric_reports() returns list[string]: + return list(signed8_report(), signed16_report(), signed32_report(), signed64_report(), unsigned8_report(), unsigned16_report(), unsigned32_report(), unsigned64_report(), decimal32_report(), decimal64_report()) +function ieee_summary[T](nan: T, positive: T, negative: T) returns string: + float64 nan_average = math.average[T](list(clone nan, clone positive)) + float64 nan_median = math.median[T](list(nan, clone positive)) + float64 infinite_average = math.average[T](list(clone positive, clone positive)) + float64 infinite_median = math.median[T](list(clone positive, clone positive)) + float64 opposite_average = math.average[T](list(clone positive, clone negative)) + float64 opposite_median = math.median[T](list(positive, negative)) + return "nan:{nan_average != nan_average}:{nan_median != nan_median};infinity:{infinite_average}:{infinite_median};opposite:{opposite_average != opposite_average}:{opposite_median != opposite_median}" +function ieee32_report() returns string: + float32 zero = 0.0 + float32 one = 1.0 + float32 nan = zero / zero + float32 positive = one / zero + return "ieee32:{ieee_summary[float32](nan, positive, -positive)}" +function ieee64_report() returns string: + float64 zero = 0.0 + float64 one = 1.0 + float64 nan = zero / zero + float64 positive = one / zero + return "ieee64:{ieee_summary[float64](nan, positive, -positive)}" +function finite_extremes() returns string: + float64 maximum = float64.from_string("1.7976931348623157e308") handle error: return "rejected" + float64 residual = float64.from_string("3e-200") handle error: return "rejected" + float64 average = math.average[float64](list(maximum, maximum)) + float64 median = math.median[float64](list(maximum, maximum)) + float64 residual_average = math.average[float64](list(maximum, -maximum, residual)) + return "maximum:{average == maximum}:{median == maximum};residual:{residual_average == residual / 3.0}" +function signed_singleton(value: int8) returns bool: + float64 expected = float64.from_string("{value}") handle error: return false + float64 average = math.average[int8](list(value)) + float64 median = math.median[int8](list(value)) + return average == expected and median == expected +function unsigned_singleton(value: uint16) returns bool: + float64 expected = float64.from_string("{value}") handle error: return false + float64 average = math.average[uint16](list(value)) + float64 median = math.median[uint16](list(value)) + return average == expected and median == expected +function main(stdout: Stdout) returns nothing: + list[string] live = numeric_reports() + for report in live: + Stdout.write(view stdout, "runtime:{report}\n") + list[string] baked = comptime numeric_reports() + for report in baked: + Stdout.write(view stdout, "comptime:{report}\n") + string baked_ieee = comptime ieee32_report() + Stdout.write(view stdout, "{ieee32_report()}\n{ieee64_report()}\n{finite_extremes()}\nbaked:{baked_ieee}\n") +verify primitive_aggregates: + assert signed8_report() == "int8:-0.5:-0.5" + assert signed16_report() == "int16:-0.5:-0.5" + assert signed32_report() == "int32:-0.5:-0.5" + assert signed64_report() == "int64:0:0" + assert unsigned8_report() == "uint8:127.5:127.5" + assert unsigned16_report() == "uint16:32767.5:32767.5" + assert unsigned32_report() == "uint32:2147483647.5:2147483647.5" + assert unsigned64_report() == "uint64:13835058055282164000:13835058055282164000" + assert decimal32_report() == "float32:5592405.333333333:16777216" + assert decimal64_report() == "float64:5592405.666666667:16777216" + assert ieee32_report() == "ieee32:nan:true:true;infinity:inf:inf;opposite:true:true" + assert ieee64_report() == "ieee64:nan:true:true;infinity:inf:inf;opposite:true:true" + assert finite_extremes() == "maximum:true:true;residual:true" +property singleton_values: + given signed: int8 + given unsigned: uint16 + assert signed_singleton(signed) + assert unsigned_singleton(unsigned) diff --git a/tests/native/method_function_values.jett b/tests/native/method_function_values.jett new file mode 100644 index 00000000..86f0ec82 --- /dev/null +++ b/tests/native/method_function_values.jett @@ -0,0 +1,95 @@ +namespace method_library +function adjusted(value: int64) returns int64: + return value + 3 +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return adjusted(self.value) + function offsetter(view self: Point) returns function(int64) returns int64: + int64 base = adjusted(self.value) + return function(input: int64) returns int64: return input + base +export interface Reader: + function amount(view self: Reader) returns int64 +implement Reader for Point: + function amount(view self: Point) returns int64: + return adjusted(self.value) + 1000 +export struct Counter: + value: int64 +implement Reader for Counter: + function amount(view self: Counter) returns int64: + return adjusted(self.value) * 2 +export struct Holder: + callback: function(view Point) returns int64 +export function choose() returns function(view Point) returns int64: + return Point.amount +export function generic_choice[T](unused: T) returns function(view Point) returns int64: + return Point.amount +export function make_offset() returns function(int64) returns int64: + Point point = Point(value: 8) + return Point.offsetter(view point) +export function make_holder() returns Holder: + return Holder(callback: Point.amount) +namespace alternate_library +export struct Point: + value: int64 + function amount(view self: Point) returns int64: + return self.value + 100 +namespace test +verify method_callbacks: + use method_library + use method_library as models + models.Point point = models.Point(value: 4) + function(view models.Point) returns int64 callback = models.Point.amount + function(view models.Point) returns int64 baked = comptime method_library.choose() + assert callback(view point) == 7 + assert baked(view point) == 7 +property method_trials: + given input: int64 + use method_library as models + models.Point point = models.Point(value: input) + function(view models.Point) returns int64 callback = models.Point.amount + assert callback(view point) == input + 3 +function main() returns nothing: + use method_library + use method_library as models + use alternate_library as other_models + models.Point point = models.Point(value: 7) + models.Counter counter = models.Counter(value: 9) + other_models.Point other_point = other_models.Point(value: 5) + function(view models.Point) returns int64 callback = models.Point.amount + function(view models.Counter) returns int64 implementation = models.Counter.amount + function(view other_models.Point) returns int64 other_callback = other_models.Point.amount + function(view models.Point) returns int64 returned = models.choose() + function(view models.Point) returns int64 generic_int = models.generic_choice[int64](0) + function(view models.Point) returns int64 generic_string = models.generic_choice[string]("unused") + function(view models.Point) returns int64 copied = clone callback + models.Holder holder = models.Holder(callback: clone callback) + list[function(view models.Point) returns int64] callbacks = list(clone callback, clone returned) + function(view models.Point) returns int64 selected = list.get[function(view models.Point) returns int64](view callbacks, 1) handle: + default callback + function(view models.Point) returns int64 baked = comptime method_library.Point.amount + function(view models.Counter) returns int64 baked_impl = comptime method_library.Counter.amount + function(view models.Point) returns int64 baked_returned = comptime method_library.choose() + function(view models.Point) returns int64 baked_generic = comptime method_library.generic_choice[int64](1) + models.Holder baked_holder = comptime method_library.make_holder() + list[function(view models.Point) returns int64] baked_list = comptime list(method_library.Point.amount) + function(view models.Point) returns int64 baked_selected = list.get[function(view models.Point) returns int64](view baked_list, 0) handle: + default callback + function(int64) returns int64 offset = models.Point.offsetter(view point) + function(int64) returns int64 baked_offset = comptime method_library.make_offset() + trace callback + trace implementation + trace other_callback + trace baked + trace baked_impl + trace baked_holder + trace baked_list + trace offset + trace baked_offset + println(callback(view point), implementation(view counter), other_callback(view other_point)) + println((models.Point.amount)(view point), returned(view point), generic_int(view point), generic_string(view point)) + println(copied(view point), holder.callback(view point), selected(view point)) + println(baked(view point), baked_impl(view counter), baked_returned(view point), baked_generic(view point)) + println(baked_holder.callback(view point), baked_selected(view point), offset(2), baked_offset(2)) + println(models.Reader.amount(view point)) + return nothing diff --git a/tests/native/named_enum_arguments.jett b/tests/native/named_enum_arguments.jett new file mode 100644 index 00000000..9a99db46 --- /dev/null +++ b/tests/native/named_enum_arguments.jett @@ -0,0 +1,54 @@ +namespace test +enum Payload: + packet(label: string, numbers: list[int64], amount: int64) +function numbers(label: string) returns list[int64]: + println(label) + return list(2, 3) +function amount(label: string, value: int64) returns int64: + println(label) + return value +function text(label: string) returns string: + println(label) + return label +function show(value: Payload) returns nothing: + match value: + packet(label, values, count): + println(label, list.length[int64](view values), count) + return nothing +function reordered() returns Payload: + return Payload.packet(amount: amount("amount", 7), label: text("label"), numbers: numbers("numbers")) +function handled() returns Payload: + mutable string label = "before" + optional[int64] missing = none + return(Payload.packet)(numbers: numbers("first"), amount: missing handle: + label = "after" + println("handler") + default 9 + , label: text(label)) +function early_return() returns Payload: + optional[int64] missing = none + return Payload.packet(numbers: numbers("allocated"), amount: missing handle: + return Payload.packet(label: "returned", numbers: list(0), amount: 11) + , label: text("skipped")) +function build_packet(label: string, values: list[int64], count: int64) returns Payload: + return Payload.packet(label, values, count) +function mixed_arguments() returns nothing: + show(Payload.packet(amount: amount("mixed amount", 13), text("mixed label"), numbers("mixed numbers"))) + Payload piped = text("mixed piped") into Payload.packet(amount: amount("mixed piped amount", 14), numbers("mixed piped numbers")) + show(piped) + Payload baked = comptime Payload.packet(amount: 15, "mixed baked", list(4)) + show(baked) + show((build_packet)(count: 16, "mixed function", list(4, 5))) + Payload function_piped = "mixed function piped" into build_packet(count: 17, list(3)) + show(function_piped) + return nothing +function main() returns nothing: + show(reordered()) + show(handled()) + show(early_return()) + Payload piped = text("piped") into Payload.packet(amount: amount("piped amount", 12), numbers: numbers("piped numbers")) + show(piped) + Payload baked = comptime Payload.packet(amount: 8, numbers: list(1, 2, 3), label: "baked") + show(baked) + mixed_arguments() + return nothing diff --git a/tests/native/nested_actor_message_handle.jett b/tests/native/nested_actor_message_handle.jett new file mode 100644 index 00000000..b34e68ea --- /dev/null +++ b/tests/native/nested_actor_message_handle.jett @@ -0,0 +1,42 @@ +namespace app +actor Holder(seed: string): + mutable string label = seed + receive rename(text: string): + label = text + receive read responds string: + respond label + receive combine(first: string, second: string) responds string: + respond "{first}:{second}:{label}" +function maybe_text(valid: bool) returns result[string, string]: + println("text") + if valid: + return ok("Ada") + return fail("missing") +function maybe_holder(valid: bool) returns result[Holder, string]: + if valid: + return ok(spawn Holder(seed: "Ada")) + return fail("missing holder") +function target_value(valid: bool) returns string: + return ask(maybe_holder(valid) handle error: + default spawn Holder(seed: "Fallback") + ).read +function messages(valid: bool) returns nothing: + Holder holder = spawn Holder(seed: "initial") + send holder.rename(maybe_text(valid) handle error: + default "Fallback" + ) + mutable string first = "Before" + string combined = ask holder.combine(first, maybe_text(valid) handle error: + first = "Changed" + default "Fallback" + ) + println(first) + println(combined) + println(ask holder.read) + return nothing +function main() returns nothing: + messages(true) + messages(false) + println(target_value(true)) + println(target_value(false)) + return nothing diff --git a/tests/native/nested_actor_spawn_handle.jett b/tests/native/nested_actor_spawn_handle.jett new file mode 100644 index 00000000..5f91e1d2 --- /dev/null +++ b/tests/native/nested_actor_spawn_handle.jett @@ -0,0 +1,31 @@ +namespace app +actor Holder(seed: string, start: int64): + mutable string label = seed + mutable int64 count = start + receive read responds string: + respond "{label}:{count}" +function maybe_seed(valid: bool) returns result[string, string]: + println("seed") + if valid: + return ok("Ada") + return fail("missing seed") +function maybe_count(valid: bool) returns result[int64, string]: + println("count") + if valid: + return ok(3) + return fail("missing count") +function build(valid: bool) returns nothing: + mutable int64 fallback_count = 7 + Holder holder = spawn Holder(start: maybe_count(valid) handle error: + default fallback_count + , seed: maybe_seed(valid) handle error: + fallback_count = 9 + default "Fallback" + ) + println(fallback_count) + println(ask holder.read) + return nothing +function main() returns nothing: + build(true) + build(false) + return nothing diff --git a/tests/native/nested_borrowed_collections.jett b/tests/native/nested_borrowed_collections.jett new file mode 100644 index 00000000..1ff6bce6 --- /dev/null +++ b/tests/native/nested_borrowed_collections.jett @@ -0,0 +1,35 @@ +namespace app +struct Item: + label: string + score: int64 +struct Store: + rows: map[string, Item] + names: list[string] + tags: set[string] + title: string +struct Envelope: + inner: Store +function inspect(view envelope: Envelope) returns int64: + mutable int64 total = 0 + for key, item in view envelope.inner.rows: + println(key, item.label) + total = total + item.score + for name in view envelope.inner.names: + println(name) + total = total + 1 + for tag in view envelope.inner.tags: + println(tag) + total = total + 1 + for character in view envelope.inner.title: + println(character) + total = total + 1 + return total +function main() returns nothing: + mutable set[string] tags = set.new[string]() + tags = set.add[string](tags, "live") + map[string, Item] rows = map("a": Item(label: "Ada", score: 2), "b": Item(label: "Lin", score: 3)) + Store store = Store(rows: rows, names: list("first", "second"), tags: tags, title: "🚀é") + Envelope value = Envelope(inner: store) + println(inspect(view value), inspect(view value)) + println(list.length[string](view value.inner.names), map.length[string, Item](view value.inner.rows)) + return nothing diff --git a/tests/native/nested_clone_handle.jett b/tests/native/nested_clone_handle.jett new file mode 100644 index 00000000..224d1b64 --- /dev/null +++ b/tests/native/nested_clone_handle.jett @@ -0,0 +1,15 @@ +namespace app +function maybe_items(valid: bool) returns result[list[string], string]: + if valid: + return ok(list("Ada")) + return fail("missing") +function first_item(valid: bool) returns string: + list[string] items = clone(maybe_items(valid) handle error: + default list("fallback") + ) + return list.get[string](items, 0) handle: + return "empty" +function main() returns nothing: + println(first_item(true)) + println(first_item(false)) + return nothing diff --git a/tests/native/nested_coarsen_handle.jett b/tests/native/nested_coarsen_handle.jett new file mode 100644 index 00000000..09dd3b55 --- /dev/null +++ b/tests/native/nested_coarsen_handle.jett @@ -0,0 +1,18 @@ +namespace app +type NonEmpty = string where value != "" +function maybe_name(valid: bool) returns optional[NonEmpty]: + NonEmpty name = "Ada" handle error: + return none + if valid: + return some(name) + return none +function coarsened(valid: bool) returns string: + NonEmpty backup = "Fallback" handle error: + return error + return coarsen(maybe_name(valid) handle: + default backup + ) +function main() returns nothing: + println(coarsened(true)) + println(coarsened(false)) + return nothing diff --git a/tests/native/nested_debug_condition_handle.jett b/tests/native/nested_debug_condition_handle.jett new file mode 100644 index 00000000..e45e0be3 --- /dev/null +++ b/tests/native/nested_debug_condition_handle.jett @@ -0,0 +1,21 @@ +namespace app +function maybe_flag(valid: bool) returns result[bool, string]: + if valid: + return ok(true) + return fail("missing") +function main() returns nothing: + breakpoint(maybe_flag(true) handle error: + default false + ) + breakpoint(maybe_flag(false) handle error: + default false + ) + println("after") + return nothing +verify handled_assertion: + assert(maybe_flag(true) handle error: + default false + ) + assert(maybe_flag(false) handle error: + default true + ) diff --git a/tests/native/nested_declassify_handle.jett b/tests/native/nested_declassify_handle.jett new file mode 100644 index 00000000..d19be2b7 --- /dev/null +++ b/tests/native/nested_declassify_handle.jett @@ -0,0 +1,15 @@ +namespace app +function maybe_secret(valid: bool) returns optional[secret[string]]: + secret[string] value = "Ada" + if valid: + return some(value) + return none +function revealed(valid: bool) returns string: + secret[string] backup = "Fallback" + return declassify(maybe_secret(valid) handle: + default backup + ) +function main() returns nothing: + println(revealed(true)) + println(revealed(false)) + return nothing diff --git a/tests/native/nested_field_handle.jett b/tests/native/nested_field_handle.jett new file mode 100644 index 00000000..9c92bfbe --- /dev/null +++ b/tests/native/nested_field_handle.jett @@ -0,0 +1,43 @@ +namespace app +struct User: + name: string +struct Wrapper: + user: User +function maybe_user(valid: bool) returns result[User, string]: + if valid: + return ok(User(name: "Ada")) + return fail("missing") +function user_name(valid: bool) returns string: + return(maybe_user(valid) handle error: + default User(name: "fallback") + ).name +function user_name_or_error(valid: bool) returns string: + return(maybe_user(valid) handle error: + return error + ).name +function maybe_optional(valid: bool) returns optional[User]: + if valid: + return some(User(name: "Lin")) + return none +function optional_name(valid: bool) returns string: + return(maybe_optional(valid) handle: + default User(name: "optional fallback") + ).name +function maybe_wrapper(valid: bool) returns result[Wrapper, string]: + if valid: + return ok(Wrapper(user: User(name: "Kai"))) + return fail("missing wrapper") +function nested_name(valid: bool) returns string: + return(maybe_wrapper(valid) handle error: + default Wrapper(user: User(name: "nested fallback")) + ).user.name +function main() returns nothing: + println(user_name(true)) + println(user_name(false)) + println(user_name_or_error(true)) + println(user_name_or_error(false)) + println(optional_name(true)) + println(optional_name(false)) + println(nested_name(true)) + println(nested_name(false)) + return nothing diff --git a/tests/native/nested_for_handle.jett b/tests/native/nested_for_handle.jett new file mode 100644 index 00000000..07da8f57 --- /dev/null +++ b/tests/native/nested_for_handle.jett @@ -0,0 +1,23 @@ +namespace app +function maybe_items(valid: bool) returns result[list[string], string]: + println("source") + if valid: + return ok(list("Ada", "Lin")) + return fail("missing") +function print_items(valid: bool) returns nothing: + for item in(maybe_items(valid) handle error: + default list("fallback") + ): + println(item) + return nothing +function print_or_error(valid: bool) returns nothing: + for item in(maybe_items(valid) handle error: + return println(error) + ): + println(item) + return nothing +function main() returns nothing: + print_items(true) + print_items(false) + print_or_error(false) + return nothing diff --git a/tests/native/nested_handle_binary.jett b/tests/native/nested_handle_binary.jett new file mode 100644 index 00000000..81db818f --- /dev/null +++ b/tests/native/nested_handle_binary.jett @@ -0,0 +1,54 @@ +namespace test +function record(label: string, value: int64) returns int64: + println(label) + return value +function calculate(maybe: optional[int64]) returns int64: + return record("left", 8) +(maybe handle: + println("fallback") + default record("right", 4) + ) +function capture_left(maybe: optional[int64]) returns int64: + mutable int64 left = 8 + int64 answer = left +(maybe handle: + left = 100 + default 4 + ) + println(left) + return answer +function from_left(maybe: optional[int64]) returns int64: + return(maybe handle: + default 4 + ) + record("right operand", 6) +function negated(maybe: optional[int64]) returns int64: + return -(maybe handle: + default 4 + ) +function nested_argument(maybe: optional[int64]) returns int64: + return record("outer",(maybe handle: + default 4 + ) + 2) +function logical_and(flag: bool, maybe: optional[bool]) returns bool: + return flag and(maybe handle: + println("and fallback") + default true + ) +function logical_or(flag: bool, maybe: optional[bool]) returns bool: + return flag or(maybe handle: + println("or fallback") + default false + ) +function main() returns nothing: + println(calculate(some(2))) + println(calculate(none)) + println(capture_left(some(2))) + println(capture_left(none)) + println(from_left(none)) + println(negated(none)) + println(nested_argument(none)) + println(logical_and(false, none)) + println(logical_and(true, none)) + println(logical_and(true, some(false))) + println(logical_or(true, none)) + println(logical_or(false, none)) + println(logical_or(false, some(true))) + return nothing diff --git a/tests/native/nested_handle_borrowed_stdlib_call.jett b/tests/native/nested_handle_borrowed_stdlib_call.jett new file mode 100644 index 00000000..65fa0706 --- /dev/null +++ b/tests/native/nested_handle_borrowed_stdlib_call.jett @@ -0,0 +1,15 @@ +namespace test +function first_or_zero(view items: list[int64]) returns int64: + return list.get[int64](view items, 0) handle: + default 0 +function main() returns nothing: + mutable list[int64] items = list(3) + optional[int64] maybe = none + optional[int64] selected = list.get[int64](view items, maybe handle: + items = list(8) + default 0 + ) + int64 old_first = selected handle: + default -1 + println(old_first, first_or_zero(view items)) + return nothing diff --git a/tests/native/nested_handle_capability_view.jett b/tests/native/nested_handle_capability_view.jett new file mode 100644 index 00000000..ab29cf86 --- /dev/null +++ b/tests/native/nested_handle_capability_view.jett @@ -0,0 +1,18 @@ +namespace app +function write(view stdout: Stdout, text: string) returns nothing: + Stdout.write(view stdout, text) +function main(stdout: Stdout) returns nothing: + optional[string] direct_missing = none + write(view stdout, direct_missing handle: + default "direct\n" + ) + optional[string] intrinsic_missing = none + Stdout.write(view stdout, intrinsic_missing handle: + default "intrinsic\n" + ) + function(view Stdout, string) returns nothing callback = write + optional[string] indirect_missing = none + callback(view stdout, indirect_missing handle: + default "indirect\n" + ) + return nothing diff --git a/tests/native/nested_handle_constructors.jett b/tests/native/nested_handle_constructors.jett new file mode 100644 index 00000000..46796850 --- /dev/null +++ b/tests/native/nested_handle_constructors.jett @@ -0,0 +1,73 @@ +namespace test +struct Pair: + label: string + count: int64 +enum Bundle: + packet(label: string, count: int64) +machine Session: + states: + guest + active(label: string, count: int64) + transitions: + guest to active +bitfield Header: + version: 4 bits + length: 8 bits +function text(label: string) returns string: + println(label) + return label +function number(label: string, value: int64) returns int64: + println(label) + return value +function make_map(maybe: optional[int64]) returns map[string, int64]: + optional[string] maybe_key = none + return map("first": number("map first", 3),(maybe_key handle: + println("map key fallback") + default "second" + ): clone maybe handle: + println("map fallback") + default number("map second", 4) + ) +function show(maybe: optional[int64]) returns nothing: + list[int64] numbers = list(number("list first", 1), clone maybe handle: + println("list fallback") + default number("list second", 2) + ) + map[string, int64] entries = make_map(clone maybe) + Pair pair = Pair(count: clone maybe handle: + println("pair fallback") + default number("pair count", 5) + , label: text("pair label")) + Bundle bundle = Bundle.packet(text("enum label"), clone maybe handle: + println("enum fallback") + default number("enum count", 6) + ) + Session session = Session(active, text("machine label"), clone maybe handle: + println("machine fallback") + default number("machine count", 7) + ) + result[Header, string] checked_header = Header(version: clone maybe handle: + println("header fallback") + default 4 + , length: number("header length", 12)) + Header header = checked_header handle error: + return nothing + optional[int64] wrapped = some(clone maybe handle: + default 8 + ) + result[int64, string] outcome = ok(clone maybe handle: + default 10 + ) + trace numbers + trace entries + trace pair + trace bundle + trace session + trace header + trace wrapped + trace outcome + return nothing +function main() returns nothing: + show(some(9)) + show(none) + return nothing diff --git a/tests/native/nested_handle_enum_binary.jett b/tests/native/nested_handle_enum_binary.jett new file mode 100644 index 00000000..670120aa --- /dev/null +++ b/tests/native/nested_handle_enum_binary.jett @@ -0,0 +1,31 @@ +namespace test +enum State: + idle + text(value: string) +function capture_left(maybe: optional[State]) returns bool: + mutable State left = State.text("old") + bool equal = left ==(maybe handle: + left = State.text("new") + default State.text("old") + ) + println(left == State.text("new")) + return equal +function capture_text(maybe: optional[string]) returns bool: + mutable string left = "old" + bool equal = left ==(maybe handle: + left = "new" + default "old" + ) + println(left) + return equal +function constructed_left(maybe: optional[State]) returns bool: + return State.text("old") ==(maybe handle: + default State.text("old") + ) +function main() returns nothing: + println(capture_left(none)) + println(capture_left(some(State.text("old")))) + println(capture_text(none)) + println(capture_text(some("old"))) + println(constructed_left(none)) + return nothing diff --git a/tests/native/nested_handle_indirect_aggregate_view.jett b/tests/native/nested_handle_indirect_aggregate_view.jett new file mode 100644 index 00000000..d32d8a37 --- /dev/null +++ b/tests/native/nested_handle_indirect_aggregate_view.jett @@ -0,0 +1,18 @@ +namespace test +function add_first(view items: list[int64], extra: int64) returns int64: + int64 first = list.get[int64](view items, 0) handle: + default 0 + return first + extra +function first_or_zero(view items: list[int64]) returns int64: + return list.get[int64](view items, 0) handle: + default 0 +function main() returns nothing: + mutable list[int64] items = list(3) + function(view list[int64], int64) returns int64 callback = add_first + optional[int64] maybe = none + int64 answer = callback(view items, maybe handle: + items = list(8) + default 2 + ) + println(answer, first_or_zero(view items)) + return nothing diff --git a/tests/native/nested_handle_indirect_call.jett b/tests/native/nested_handle_indirect_call.jett new file mode 100644 index 00000000..0daa54c6 --- /dev/null +++ b/tests/native/nested_handle_indirect_call.jett @@ -0,0 +1,29 @@ +namespace test +function combine(first: int64, second: int64) returns int64: + return first * 10 + second +function alternate(first: int64, second: int64) returns int64: + return first - second +function choose() returns function(int64, int64) returns int64: + println("choose") + return combine +function record(label: string, value: int64) returns int64: + println(label) + return value +function evaluate(maybe: optional[int64]) returns int64: + function(int64, int64) returns int64 callback = choose() + return callback(record("first", 2), maybe handle: + println("fallback") + default record("second", 4) + ) +function evaluate_mutating(maybe: optional[int64]) returns int64: + mutable function(int64, int64) returns int64 callback = combine + return callback(record("captured first", 5), maybe handle: + callback = alternate + default record("captured second", 6) + ) +function main() returns nothing: + println(evaluate(some(3))) + println(evaluate(none)) + println(evaluate_mutating(some(3))) + println(evaluate_mutating(none)) + return nothing diff --git a/tests/native/nested_handle_indirect_view.jett b/tests/native/nested_handle_indirect_view.jett new file mode 100644 index 00000000..95e56c68 --- /dev/null +++ b/tests/native/nested_handle_indirect_view.jett @@ -0,0 +1,22 @@ +namespace test +function combine(view label: string, value: int64) returns string: + return "{label}:{value}" +function main() returns nothing: + mutable string label = "old" + function(view string, int64) returns string callback = combine + optional[int64] maybe = none + string answer = callback(view label, maybe handle: + label = "new" + default 2 + ) + println(answer) + println(label) + mutable string nested_label = "before" + function(view string, int64) returns string nested_callback = combine + optional[int64] nested_maybe = none + println(nested_callback(view nested_label, nested_maybe handle: + nested_label = "after" + default 3 + )) + println(nested_label) + return nothing diff --git a/tests/native/nested_handle_intrinsic.jett b/tests/native/nested_handle_intrinsic.jett new file mode 100644 index 00000000..aa8a9f78 --- /dev/null +++ b/tests/native/nested_handle_intrinsic.jett @@ -0,0 +1,11 @@ +namespace test +function record(label: string, value: int64) returns int64: + println(label) + return value +function main() returns nothing: + optional[int64] maybe = none + println(record("first", 1), maybe handle: + println("fallback") + default record("second", 2) + ) + return nothing diff --git a/tests/native/nested_handle_projected_view.jett b/tests/native/nested_handle_projected_view.jett new file mode 100644 index 00000000..eacb4928 --- /dev/null +++ b/tests/native/nested_handle_projected_view.jett @@ -0,0 +1,26 @@ +namespace app +struct Store: + items: list[int64] +function first_or_zero(view items: list[int64]) returns int64: + return list.get[int64](view items, 0) handle: + default 0 +function add_first(view items: list[int64], extra: int64) returns int64: + return first_or_zero(view items) + extra +function main() returns nothing: + mutable Store store = Store(items: list(3)) + optional[int64] missing = none + optional[int64] chosen = list.get[int64](view store.items, missing handle: + store = Store(items: list(8)) + default 0 + ) + int64 old = chosen handle: + default -1 + println(old, first_or_zero(view store.items)) + function(view list[int64], int64) returns int64 callback = add_first + optional[int64] missing_again = none + int64 combined = callback(view store.items, missing_again handle: + store = Store(items: list(13)) + default 2 + ) + println(combined, first_or_zero(view store.items)) + return nothing diff --git a/tests/native/nested_handle_refined_binary.jett b/tests/native/nested_handle_refined_binary.jett new file mode 100644 index 00000000..5dad8cc7 --- /dev/null +++ b/tests/native/nested_handle_refined_binary.jett @@ -0,0 +1,39 @@ +namespace test +type Positive = int64 where value > 0 +type NonEmpty = string where value != "" +function capture_left(maybe: optional[Positive]) returns bool: + mutable Positive left = 7 handle error: + return false + Positive replacement = 9 handle error: + return false + Positive fallback = 7 handle error: + return false + bool equal = left ==(maybe handle: + left = replacement + default fallback + ) + println(coarsen left) + return equal +function capture_text(maybe: optional[NonEmpty]) returns bool: + mutable NonEmpty left = "old" handle error: + return false + NonEmpty replacement = "new" handle error: + return false + NonEmpty fallback = "old" handle error: + return false + bool equal = left ==(maybe handle: + left = replacement + default fallback + ) + println(coarsen left) + return equal +function main() returns nothing: + Positive present = 7 handle error: + return + NonEmpty present_text = "old" handle error: + return + println(capture_left(none)) + println(capture_left(some(present))) + println(capture_text(none)) + println(capture_text(some(present_text))) + return nothing diff --git a/tests/native/nested_handler_call.jett b/tests/native/nested_handler_call.jett new file mode 100644 index 00000000..344bcacb --- /dev/null +++ b/tests/native/nested_handler_call.jett @@ -0,0 +1,11 @@ +namespace app +function identity(view value: uint64) returns uint64: + return value +function pick(view values: list[uint64], fallback: uint64) returns uint64: + return identity(view(list.get[uint64](view values, 0) handle: + default fallback + )) +function main() returns nothing: + list[uint64] present = list(42) + list[uint64] empty = list.new[uint64]() + println(pick(view present, 7), pick(view empty, 99)) diff --git a/tests/native/nested_handler_order.jett b/tests/native/nested_handler_order.jett new file mode 100644 index 00000000..c584e0cd --- /dev/null +++ b/tests/native/nested_handler_order.jett @@ -0,0 +1,19 @@ +namespace app +function marked(value: int64) returns int64: + println(value) + return value +function combine(left: int64, right: int64) returns int64: + return left * 10 + right +function pick(view values: list[int64], fallback: int64) returns int64: + return combine(marked(1), list.get[int64](view values, 0) handle: + default marked(fallback) + ) +function pick_named(view values: list[int64], fallback: int64) returns int64: + return combine(right: list.get[int64](view values, 0) handle: + default marked(fallback) + , left: marked(3)) +function main() returns nothing: + list[int64] present = list(2) + list[int64] empty = list.new[int64]() + println(pick(view present, 8), pick(view empty, 9)) + println(pick_named(view present, 4), pick_named(view empty, 5)) diff --git a/tests/native/nested_machine_borrowed_collections.jett b/tests/native/nested_machine_borrowed_collections.jett new file mode 100644 index 00000000..81d845f0 --- /dev/null +++ b/tests/native/nested_machine_borrowed_collections.jett @@ -0,0 +1,20 @@ +namespace app +machine Portfolio: + states: + active(rows: map[string, string], title: string) + closed + transitions: + active to closed +function inspect(view portfolio: Portfolio at active) returns int64: + mutable int64 total = 0 + for key, item in view portfolio.rows: + println(key, item) + total = total + 1 + for character in view portfolio.title: + println(character) + total = total + 1 + return total +function main() returns nothing: + Portfolio at active portfolio = Portfolio(active, map("a": "Ada", "b": "Lin"), "A🚀é") + println(inspect(view portfolio), inspect(view portfolio)) + return nothing diff --git a/tests/native/nested_match_handle.jett b/tests/native/nested_match_handle.jett new file mode 100644 index 00000000..8aaff1c9 --- /dev/null +++ b/tests/native/nested_match_handle.jett @@ -0,0 +1,20 @@ +namespace app +enum Message: + empty + text(value: string) +function maybe_message(valid: bool) returns result[Message, string]: + if valid: + return ok(Message.text("hello")) + return fail("missing") +function describe(valid: bool) returns string: + match(maybe_message(valid) handle error: + default Message.empty + ): + empty: + return "empty" + text(value): + return value +function main() returns nothing: + println(describe(true)) + println(describe(false)) + return nothing diff --git a/tests/native/nested_refinement_handle.jett b/tests/native/nested_refinement_handle.jett new file mode 100644 index 00000000..d50bfa1a --- /dev/null +++ b/tests/native/nested_refinement_handle.jett @@ -0,0 +1,16 @@ +namespace app +type Positive = int64 where value > 0 +function maybe_number(present: bool) returns optional[int64]: + if present: + return some(7) + return none +function checked(present: bool) returns string: + Positive number =(maybe_number(present) handle: + default -1 + ) handle error: + return error + return "{coarsen number}" +function main() returns nothing: + println(checked(true)) + println(checked(false)) + return nothing diff --git a/tests/native/nested_state_handle.jett b/tests/native/nested_state_handle.jett new file mode 100644 index 00000000..c7aba1fa --- /dev/null +++ b/tests/native/nested_state_handle.jett @@ -0,0 +1,29 @@ +namespace app +machine Session: + states: + guest + active(name: string) + transitions: + guest to active +function maybe_session(valid: bool) returns result[Session, string]: + if valid: + return ok(Session(active, "Ada")) + return fail("missing") +function state_name(valid: bool) returns string: + if (maybe_session(valid) handle error: + default Session(guest) + ) at active: + return "active" + return "guest" +function active_count() returns int64: + mutable int64 count = 0 + while (maybe_session(count < 2) handle error: + default Session(guest) + ) at active: + count = count + 1 + return count +function main() returns nothing: + println(state_name(true)) + println(state_name(false)) + println(active_count()) + return nothing diff --git a/tests/native/nested_task_handle.jett b/tests/native/nested_task_handle.jett new file mode 100644 index 00000000..9342239b --- /dev/null +++ b/tests/native/nested_task_handle.jett @@ -0,0 +1,43 @@ +namespace app +function identity(value: string) returns string: + return value +function work() returns nothing: + return nothing +function maybe_text(valid: bool) returns result[string, string]: + if valid: + return ok("Ada") + return fail("missing text") +function maybe_pending(valid: bool) returns result[string, string]: + println("source") + if valid: + return ok(run identity("Lin")) + return fail("missing task") +function maybe_unit(valid: bool) returns result[nothing, string]: + if valid: + return ok(run work()) + return fail("missing unit task") +function run_value(valid: bool) returns string: + string pending = run(maybe_text(valid) handle error: + default "Fallback" + ) + return join pending handle error: + default error +function join_value(valid: bool) returns string: + return join(maybe_pending(valid) handle error: + default run identity("Fallback") + ) handle error: + default error +function join_unit(valid: bool) returns string: + nothing completed = join(maybe_unit(valid) handle error: + default run work() + ) handle error: + return error + return "joined" +function main() returns nothing: + println(run_value(true)) + println(run_value(false)) + println(join_value(true)) + println(join_value(false)) + println(join_unit(true)) + println(join_unit(false)) + return nothing diff --git a/tests/native/nested_transition_handle.jett b/tests/native/nested_transition_handle.jett new file mode 100644 index 00000000..4294c9ed --- /dev/null +++ b/tests/native/nested_transition_handle.jett @@ -0,0 +1,31 @@ +namespace app +machine Session: + states: + guest + active(label: string, name: string) + transitions: + guest to active +function maybe_guest(valid: bool) returns result[Session at guest, string]: + println("source") + if valid: + return ok(Session(guest)) + return fail("missing guest") +function maybe_name(valid: bool) returns result[string, string]: + println("payload") + if valid: + return ok("Ada") + return fail("missing name") +function transition(valid: bool) returns string: + mutable string label = "Before" + Session at active session = Session.transition(maybe_guest(valid) handle error: + default Session(guest) + , active, label, maybe_name(valid) handle error: + label = "Changed" + default "Fallback" + ) + println(label) + return "{session.label}:{session.name}" +function main() returns nothing: + println(transition(true)) + println(transition(false)) + return nothing diff --git a/tests/native/numeric_conversions.jett b/tests/native/numeric_conversions.jett new file mode 100644 index 00000000..004e76d8 --- /dev/null +++ b/tests/native/numeric_conversions.jett @@ -0,0 +1,17 @@ +namespace app +function float_from_int(value: int64) returns string: + float64 converted = float64.from_int64(value) handle error: + return error + return string.from_float64(converted) +function int_from_float(value: float64) returns string: + int64 converted = int64.from_float64(value) handle error: + return error + return string.from_int64(converted) +function main() returns nothing: + println(float_from_int(9007199254740992)) + println(float_from_int(9007199254740993)) + println(float_from_int(9223372036854775807)) + println(int_from_float(42.0)) + println(int_from_float(-9223372036854775808.0)) + println(int_from_float(3.14)) + println(int_from_float(9223372036854775808.0)) diff --git a/tests/native/payload_enum.jett b/tests/native/payload_enum.jett new file mode 100644 index 00000000..4df38036 --- /dev/null +++ b/tests/native/payload_enum.jett @@ -0,0 +1,51 @@ +namespace app +enum Item: + empty + number(value: int64) + text(value: string) + pair(left: string, right: string) + wrapped(inner: Item) +function describe(item: Item) returns string: + match item: + empty: + return "empty" + number(value): + return "number {value}" + text(value): + return value + pair(left, right): + return "{left}/{right}" + wrapped(inner): + return describe(inner) +function classify(item: Item) returns string: + match item: + empty: + return "empty" + other: + return "present" +function borrowed_description(view item: Item) returns string: + match item: + text(value): + return value + pair(left, right): + return "{left}/{right}" + other: + return "other" +function main() returns nothing: + Item original = Item.text("hello") + Item duplicate = clone original + println(describe(original), describe(duplicate)) + println(describe(Item.number(42)), describe(Item.pair("a", "b"))) + println(describe(Item.wrapped(Item.text("nested")))) + println(classify(Item.empty), classify(Item.number(1))) + Item borrowed = Item.pair("read", "again") + println(borrowed_description(view borrowed), borrowed_description(view borrowed)) + println(describe(borrowed)) + list[Item] collection = list(Item.text("first"), Item.text("second")) + for item in view collection: + println(borrowed_description(view item)) + println(list.length[Item](collection)) + map[string, Item] keyed = map("one": Item.text("value")) + for key, item in view keyed: + println(key, borrowed_description(view item)) + println(map.length[string, Item](keyed)) diff --git a/tests/native/payload_enum_equality.jett b/tests/native/payload_enum_equality.jett new file mode 100644 index 00000000..cd447aa8 --- /dev/null +++ b/tests/native/payload_enum_equality.jett @@ -0,0 +1,24 @@ +namespace app +enum Value: + empty + number(value: int64) + tiny(value: uint8) + fraction(value: float64) + small(value: float32) + text(value: string) + pair(first: bool, second: string) +function main() returns nothing: + println(Value.empty == Value.empty) + println(Value.empty == Value.number(0)) + println(Value.number(7) == Value.number(7)) + println(Value.number(7) != Value.number(8)) + println(Value.number(7) == Value.fraction(7.0)) + println(Value.tiny(255) == Value.tiny(255)) + println(Value.fraction(1.5) == Value.fraction(1.5)) + println(Value.fraction(-0.0) == Value.fraction(0.0)) + float64 nan = 0.0 / 0.0 + println(Value.fraction(nan) == Value.fraction(nan)) + println(Value.small(1.5) == Value.small(1.5)) + println(Value.text("a") == Value.text("b")) + println(Value.pair(true, "same") == Value.pair(true, "same")) + println(Value.pair(true, "same") != Value.pair(false, "same")) diff --git a/tests/native/pending_actor_scalar_state.jett b/tests/native/pending_actor_scalar_state.jett new file mode 100644 index 00000000..aa5d7707 --- /dev/null +++ b/tests/native/pending_actor_scalar_state.jett @@ -0,0 +1,17 @@ +namespace app +actor Holder: + mutable int64 value = 0 + receive set(amount: int64): + value = amount + receive read responds int64: + respond value +function main() returns nothing: + Holder holder = spawn Holder() + int64 pending = run 7 + send holder.set(pending) + int64 observed = ask holder.read + trace observed + int64 joined = join observed handle error: + return nothing + println(joined) + return nothing diff --git a/tests/native/pending_actor_scalar_state_types.jett b/tests/native/pending_actor_scalar_state_types.jett new file mode 100644 index 00000000..eaddb169 --- /dev/null +++ b/tests/native/pending_actor_scalar_state_types.jett @@ -0,0 +1,49 @@ +namespace app +actor Holder(seed: int64): + mutable int64 count = seed + mutable bool enabled = false + mutable float64 ratio = 0.0 + receive update(next_count: int64, next_enabled: bool, next_ratio: float64): + count = next_count + enabled = next_enabled + ratio = next_ratio + receive read_count responds int64: + respond count + receive read_enabled responds bool: + respond enabled + receive read_ratio responds float64: + respond ratio +function main() returns nothing: + int64 initial = 5 + int64 pending_initial = run initial + Holder holder = spawn Holder(seed: pending_initial) + int64 initial_count = ask holder.read_count + trace initial_count + int64 joined_initial = join initial_count handle error: + return nothing + bool plain_enabled = true + bool pending_enabled = run plain_enabled + float64 plain_ratio = 1.25 + float64 pending_ratio = run plain_ratio + send holder.update(pending_initial, pending_enabled, pending_ratio) + int64 count = ask holder.read_count + bool enabled = ask holder.read_enabled + float64 ratio = ask holder.read_ratio + trace count + trace enabled + trace ratio + int64 joined_count = join count handle error: + return nothing + bool joined_enabled = join enabled handle error: + return nothing + float64 joined_ratio = join ratio handle error: + return nothing + println(joined_initial, joined_count, joined_enabled, joined_ratio) + send holder.update(9, false, 2.5) + int64 plain_count = ask holder.read_count + bool plain_flag = ask holder.read_enabled + float64 plain_level = ask holder.read_ratio + trace plain_count + trace plain_flag + trace plain_level + return nothing diff --git a/tests/native/pending_actor_send_failure.jett b/tests/native/pending_actor_send_failure.jett new file mode 100644 index 00000000..9a3ff8fa --- /dev/null +++ b/tests/native/pending_actor_send_failure.jett @@ -0,0 +1,8 @@ +namespace app +actor Holder: + receive noop: + return nothing +function main() returns nothing: + Holder pending = run spawn Holder() + send pending.noop + return nothing diff --git a/tests/native/pending_actor_values.jett b/tests/native/pending_actor_values.jett new file mode 100644 index 00000000..75c629d3 --- /dev/null +++ b/tests/native/pending_actor_values.jett @@ -0,0 +1,18 @@ +namespace app +actor Holder(seed: string): + mutable string label = seed + receive noop: + return nothing +function main() returns nothing: + Holder pending = run spawn Holder(seed: "Ada") + trace pending + Holder nested = run pending + trace nested + Holder inner = join nested handle error: + default spawn Holder(seed: "fallback") + trace inner + Holder joined = join inner handle error: + default spawn Holder(seed: "fallback") + trace joined + send joined.noop + return nothing diff --git a/tests/native/pending_aggregate_access.jett b/tests/native/pending_aggregate_access.jett new file mode 100644 index 00000000..e448d9e6 --- /dev/null +++ b/tests/native/pending_aggregate_access.jett @@ -0,0 +1,213 @@ +namespace app +struct Record: + label: string + items: list[int64] +struct Envelope: + inner: Record +machine Session: + states: + guest + active(label: string, number: int64) + transitions: + guest to active +enum Choice: + empty + number(value: int64) + rows(items: list[int64]) +enum NarrowPayload: + wrapped(value: int8) +enum FloatPayload: + wrapped(value: float32) +enum BoolPayload: + wrapped(value: bool) +enum NothingPayload: + wrapped(value: nothing) +function record_label(view record: Record) returns string: + return record.label +function choice_count(view choice: Choice) returns int64: + match choice: + empty: + return 0 + number(value): + return value + rows(items): + return list.length[int64](view items) +function joined_number(choice: Choice) returns int64: + match choice: + empty: + return 0 + number(value): + int64 ready = join value handle error: + default 0 + return ready + rows(items): + return list.length[int64](view items) +function transition_label() returns string: + string marker = "transition payload must not execute" + trace marker + return "Ada" +function direct_record_failure(view stdout: Stdout) returns nothing: + Record record = run Record(label: "Ada", items: list(7)) + Stdout.write(view stdout, "before\n") + string copied = record.label + Stdout.write(view stdout, "{copied}\n") +function nested_record_failure(view stdout: Stdout) returns nothing: + Envelope envelope = Envelope(inner: run run Record(label: "Ada", items: list(7))) + Stdout.write(view stdout, "before\n") + string copied = envelope.inner.label + Stdout.write(view stdout, "{copied}\n") +function cloned_record_failure(view stdout: Stdout) returns nothing: + Record record = run run Record(label: "Ada", items: list(7)) + Record copied = clone record + Stdout.write(view stdout, "before\n") + string label = record_label(view copied) + Stdout.write(view stdout, "{label}\n") +function machine_field_failure(view stdout: Stdout) returns nothing: + Session at active session = run run Session(active, "Ada", 7) + Stdout.write(view stdout, "before\n") + string copied = session.label + Stdout.write(view stdout, "{copied}\n") +function machine_state_failure(view stdout: Stdout) returns nothing: + Session at active session = run run Session(active, "Ada", 7) + Stdout.write(view stdout, "before\n") + bool checked = session at active + Stdout.write(view stdout, "{checked}\n") +function broad_machine_state_failure(view stdout: Stdout) returns nothing: + Session session = run run Session(active, "Ada", 7) + Stdout.write(view stdout, "before\n") + bool checked = session at active + Stdout.write(view stdout, "{checked}\n") +function machine_transition_failure(view stdout: Stdout) returns nothing: + Session at guest session = run run Session(guest) + Stdout.write(view stdout, "before\n") + Session at active changed = Session.transition(session, active, transition_label(), 7) + Stdout.write(view stdout, "{changed.label}\n") +function enum_match_failure(view stdout: Stdout) returns nothing: + Choice choice = run run Choice.rows(list(7)) + Stdout.write(view stdout, "before\n") + match choice: + empty: + Stdout.write(view stdout, "empty\n") + number(value): + Stdout.write(view stdout, "{value}\n") + rows(items): + int64 count = list.length[int64](view items) + Stdout.write(view stdout, "{count}\n") +function enum_payload_failure(view stdout: Stdout) returns nothing: + Choice choice = Choice.number(run run 7) + Stdout.write(view stdout, "before\n") + match choice: + empty: + Stdout.write(view stdout, "empty\n") + number(value): + trace value + int64 joined = join value handle error: + default 0 + trace joined + Stdout.write(view stdout, "{joined + 1}\n") + rows(items): + int64 count = list.length[int64](view items) + Stdout.write(view stdout, "{count}\n") +function projected_iteration_failure(view stdout: Stdout) returns nothing: + Envelope envelope = Envelope(inner: run run Record(label: "Ada", items: list(7))) + Stdout.write(view stdout, "before\n") + for value in view envelope.inner.items: + Stdout.write(view stdout, "{value}\n") +function observe_record(view record: Record, view stdout: Stdout) returns nothing: + int64 count = list.length[int64](view record.items) + Stdout.write(view stdout, "record:{record.label}:{count}\n") +function record_controls(view stdout: Stdout) returns nothing: + Record plain = Record(label: "Ada", items: list(7)) + observe_record(view plain, view stdout) + observe_record(view plain, view stdout) + Record pending = run run clone plain + Record once = join pending handle error: + return nothing + Record joined = join once handle error: + return nothing + observe_record(view joined, view stdout) + Envelope envelope = Envelope(inner: joined) + for value in view envelope.inner.items: + Stdout.write(view stdout, "{value}\n") +function machine_controls(view stdout: Stdout) returns nothing: + Session at guest initial = Session(guest) + Session at active plain = Session.transition(initial, active, "Ada", 7) + bool plain_state = plain at active + Stdout.write(view stdout, "{plain_state}:{plain.label}:{plain.number}\n") + Session at active pending = run run clone plain + Session at active once = join pending handle error: + return nothing + Session at active joined = join once handle error: + return nothing + bool joined_state = joined at active + Stdout.write(view stdout, "{joined_state}:{joined.label}:{joined.number}\n") +function enum_controls(view stdout: Stdout) returns nothing: + Choice plain = Choice.rows(list(7)) + Stdout.write(view stdout, "{choice_count(view plain)}:{choice_count(view plain)}\n") + Choice pending = run run clone plain + Choice once = join pending handle error: + return nothing + Choice joined = join once handle error: + return nothing + Stdout.write(view stdout, "{choice_count(view joined)}:{choice_count(view joined)}\n") + Choice scalar = Choice.number(run 7) + int64 ready = joined_number(scalar) + Stdout.write(view stdout, "{ready}\n") +function narrow_payload_control(view stdout: Stdout) returns nothing: + NarrowPayload choice = NarrowPayload.wrapped(run run 7) + match choice: + wrapped(value): + trace value + int8 once = join value handle error: + return nothing + trace once + int8 ready = join once handle error: + return nothing + trace ready + Stdout.write(view stdout, "int8:{ready + 1}\n") +function float_payload_control(view stdout: Stdout) returns nothing: + FloatPayload choice = FloatPayload.wrapped(run run 1.25) + match choice: + wrapped(value): + trace value + float32 once = join value handle error: + return nothing + trace once + float32 ready = join once handle error: + return nothing + trace ready + Stdout.write(view stdout, "float32:{ready + 0.25}\n") +function bool_payload_control(view stdout: Stdout) returns nothing: + BoolPayload choice = BoolPayload.wrapped(run run true) + match choice: + wrapped(value): + trace value + bool once = join value handle error: + return nothing + trace once + bool ready = join once handle error: + return nothing + trace ready + Stdout.write(view stdout, "bool:{!ready}\n") +function nothing_payload_control(view stdout: Stdout) returns nothing: + NothingPayload choice = NothingPayload.wrapped(run run nothing) + match choice: + wrapped(value): + trace value + nothing once = join value handle error: + return nothing + trace once + nothing ready = join once handle error: + return nothing + trace ready + Stdout.write(view stdout, "nothing joined\n") +function ready_and_joined_controls(view stdout: Stdout) returns nothing: + record_controls(view stdout) + machine_controls(view stdout) + enum_controls(view stdout) + narrow_payload_control(view stdout) + float_payload_control(view stdout) + bool_payload_control(view stdout) + nothing_payload_control(view stdout) +function main(stdout: Stdout) returns nothing: + ready_and_joined_controls(view stdout) diff --git a/tests/native/pending_bitfield_field_owner_failure.jett b/tests/native/pending_bitfield_field_owner_failure.jett new file mode 100644 index 00000000..d91936b9 --- /dev/null +++ b/tests/native/pending_bitfield_field_owner_failure.jett @@ -0,0 +1,10 @@ +namespace app +bitfield Header: + version: 4 bits +function main() returns nothing: + Header header = run Header(version: 3) + TypeField field = list.get[TypeField](type.fields[Header](), 0) handle: + return nothing + int64 copied = type.field_value[Header, int64](view header, view field) + println(copied) + return nothing diff --git a/tests/native/pending_bitfield_from_bytes_failure.jett b/tests/native/pending_bitfield_from_bytes_failure.jett new file mode 100644 index 00000000..a41fc532 --- /dev/null +++ b/tests/native/pending_bitfield_from_bytes_failure.jett @@ -0,0 +1,8 @@ +namespace app +bitfield Header: + version: 4 bits +function main() returns nothing: + bytes encoded = run bytes.from_string("A") + result[Header, string] decoded = Header.from_bytes(encoded) + trace decoded + return nothing diff --git a/tests/native/pending_bitfield_payload_element_failure.jett b/tests/native/pending_bitfield_payload_element_failure.jett new file mode 100644 index 00000000..a9fb1af0 --- /dev/null +++ b/tests/native/pending_bitfield_payload_element_failure.jett @@ -0,0 +1,11 @@ +namespace app +bitfield Packet: + kind: 8 bits + payload: list[uint8] +function main() returns nothing: + uint8 plain = 7 + uint8 value = run plain + Packet packet = Packet(kind: 3, payload: list(value)) + bytes encoded = Packet.to_bytes(packet) + trace encoded + return nothing diff --git a/tests/native/pending_bitfield_payload_list_failure.jett b/tests/native/pending_bitfield_payload_list_failure.jett new file mode 100644 index 00000000..b9690655 --- /dev/null +++ b/tests/native/pending_bitfield_payload_list_failure.jett @@ -0,0 +1,11 @@ +namespace app +bitfield Packet: + kind: 8 bits + payload: list[uint8] +function main() returns nothing: + list[uint8] plain = list(7) + list[uint8] payload = run plain + Packet packet = Packet(kind: 3, payload: payload) + bytes encoded = Packet.to_bytes(packet) + trace encoded + return nothing diff --git a/tests/native/pending_bitfield_to_bytes_failure.jett b/tests/native/pending_bitfield_to_bytes_failure.jett new file mode 100644 index 00000000..6e895357 --- /dev/null +++ b/tests/native/pending_bitfield_to_bytes_failure.jett @@ -0,0 +1,8 @@ +namespace app +bitfield Header: + version: 4 bits +function main() returns nothing: + Header header = run Header(version: 4) + bytes encoded = Header.to_bytes(header) + trace encoded + return nothing diff --git a/tests/native/pending_builder_field.jett b/tests/native/pending_builder_field.jett new file mode 100644 index 00000000..6d39621d --- /dev/null +++ b/tests/native/pending_builder_field.jett @@ -0,0 +1,16 @@ +namespace app +struct Record: + name: string +function main() returns nothing: + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return nothing + TypeField pending_field = run field + mutable TypeConstruction builder = type.construct_start[Record]() + builder = type.construct_put[Record, string](builder, view pending_field, "Ada") handle error: + println(error) + return nothing + Record record = type.construct_finish[Record](builder) handle error: + println(error) + return nothing + println(record.name) + return nothing diff --git a/tests/native/pending_builder_member_metadata.jett b/tests/native/pending_builder_member_metadata.jett new file mode 100644 index 00000000..c5aa11b7 --- /dev/null +++ b/tests/native/pending_builder_member_metadata.jett @@ -0,0 +1,26 @@ +namespace app +enum Message: + named(value: string) +machine Session: + states: + active(name: string) + transitions: + active to active +function variant_error() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Message](), 0) handle: + return "missing variant" + TypeVariant pending_variant = run variant + TypeConstruction builder = type.construct_variant_start[Message](view pending_variant) handle error: + return error + return "unexpected variant success" +function state_error() returns string: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 0) handle: + return "missing state" + TypeMachineState pending_state = run state + TypeConstruction builder = type.construct_machine_start[Session](view pending_state) handle error: + return error + return "unexpected state success" +function main() returns nothing: + println(variant_error()) + println(state_error()) + return nothing diff --git a/tests/native/pending_bytes_concat_first_failure.jett b/tests/native/pending_bytes_concat_first_failure.jett new file mode 100644 index 00000000..53ad7f3e --- /dev/null +++ b/tests/native/pending_bytes_concat_first_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes first = run bytes.from_string("a") + bytes joined = bytes.concat(first, bytes.from_string("b")) + trace joined + return nothing diff --git a/tests/native/pending_bytes_concat_second_failure.jett b/tests/native/pending_bytes_concat_second_failure.jett new file mode 100644 index 00000000..19aaf082 --- /dev/null +++ b/tests/native/pending_bytes_concat_second_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes second = run bytes.from_string("b") + bytes joined = bytes.concat(bytes.from_string("a"), second) + trace joined + return nothing diff --git a/tests/native/pending_bytes_from_hex_failure.jett b/tests/native/pending_bytes_from_hex_failure.jett new file mode 100644 index 00000000..42294114 --- /dev/null +++ b/tests/native/pending_bytes_from_hex_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[bytes, string] data = bytes.from_hex(run "6162") + trace data + return nothing diff --git a/tests/native/pending_bytes_from_string_failure.jett b/tests/native/pending_bytes_from_string_failure.jett new file mode 100644 index 00000000..b4661e0e --- /dev/null +++ b/tests/native/pending_bytes_from_string_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + bytes data = bytes.from_string(run "abc") + trace data + return nothing diff --git a/tests/native/pending_bytes_get_container_failure.jett b/tests/native/pending_bytes_get_container_failure.jett new file mode 100644 index 00000000..f8a64f0e --- /dev/null +++ b/tests/native/pending_bytes_get_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("abc") + optional[int64] value = bytes.get(view data, 0) + trace value + return nothing diff --git a/tests/native/pending_bytes_get_index_failure.jett b/tests/native/pending_bytes_get_index_failure.jett new file mode 100644 index 00000000..6a2583b2 --- /dev/null +++ b/tests/native/pending_bytes_get_index_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = bytes.from_string("abc") + optional[int64] value = bytes.get(view data, run 0) + trace value + return nothing diff --git a/tests/native/pending_bytes_length_failure.jett b/tests/native/pending_bytes_length_failure.jett new file mode 100644 index 00000000..9058d27c --- /dev/null +++ b/tests/native/pending_bytes_length_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("a") + int64 length = bytes.length(view data) + trace length + return nothing diff --git a/tests/native/pending_bytes_slice_container_failure.jett b/tests/native/pending_bytes_slice_container_failure.jett new file mode 100644 index 00000000..f162d47e --- /dev/null +++ b/tests/native/pending_bytes_slice_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("abc") + bytes part = bytes.slice(view data, 0, 2) + trace part + return nothing diff --git a/tests/native/pending_bytes_slice_end_failure.jett b/tests/native/pending_bytes_slice_end_failure.jett new file mode 100644 index 00000000..1b4b0347 --- /dev/null +++ b/tests/native/pending_bytes_slice_end_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = bytes.from_string("abc") + bytes part = bytes.slice(view data, 0, run 2) + trace part + return nothing diff --git a/tests/native/pending_bytes_slice_start_failure.jett b/tests/native/pending_bytes_slice_start_failure.jett new file mode 100644 index 00000000..c2abc509 --- /dev/null +++ b/tests/native/pending_bytes_slice_start_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = bytes.from_string("abc") + bytes part = bytes.slice(view data, run 0, 2) + trace part + return nothing diff --git a/tests/native/pending_bytes_to_hex_failure.jett b/tests/native/pending_bytes_to_hex_failure.jett new file mode 100644 index 00000000..953d42b9 --- /dev/null +++ b/tests/native/pending_bytes_to_hex_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("abc") + string value = bytes.to_hex(view data) + trace value + return nothing diff --git a/tests/native/pending_bytes_to_string_failure.jett b/tests/native/pending_bytes_to_string_failure.jett new file mode 100644 index 00000000..28ffe209 --- /dev/null +++ b/tests/native/pending_bytes_to_string_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("abc") + result[string, string] value = bytes.to_string(view data) + trace value + return nothing diff --git a/tests/native/pending_bytes_values.jett b/tests/native/pending_bytes_values.jett new file mode 100644 index 00000000..7ce0ecd4 --- /dev/null +++ b/tests/native/pending_bytes_values.jett @@ -0,0 +1,29 @@ +namespace app +enum Packet: + payload(value: bytes) + empty +function identity(value: bytes) returns bytes: + return value +function make_pending() returns bytes: + return run identity(bytes.from_string("B")) +function main() returns nothing: + bytes plain = bytes.from_string("A") + bytes pending = run clone plain + bytes nested = run make_pending() + list[bytes] tasks = list(clone pending, clone nested) + trace plain + trace pending + trace nested + trace tasks + println(Packet.payload(clone pending) == Packet.payload(clone pending)) + println(Packet.payload(clone pending) == Packet.payload(clone plain)) + bytes first = join pending handle error: + default bytes.from_string("unexpected") + bytes inner = join nested handle error: + default bytes.from_string("unexpected") + trace first + trace inner + bytes last = join inner handle error: + default bytes.from_string("unexpected") + trace last + return nothing diff --git a/tests/native/pending_capability_cancelled.jett b/tests/native/pending_capability_cancelled.jett new file mode 100644 index 00000000..56bfb928 --- /dev/null +++ b/tests/native/pending_capability_cancelled.jett @@ -0,0 +1,7 @@ +namespace app +function main(stdout: Stdout) returns nothing: + Stdout joined = join stdout handle error: + println(error) + return nothing + trace joined + return nothing diff --git a/tests/native/pending_capability_values.jett b/tests/native/pending_capability_values.jett new file mode 100644 index 00000000..d4732eb9 --- /dev/null +++ b/tests/native/pending_capability_values.jett @@ -0,0 +1,15 @@ +namespace app +function main(stdout: Stdout) returns nothing: + trace stdout + Stdout pending = run stdout + trace pending + Stdout nested = run pending + trace nested + Stdout inner = join nested handle error: + return nothing + trace inner + Stdout joined = join inner handle error: + return nothing + trace joined + Stdout.write(view joined, "ready") + return nothing diff --git a/tests/native/pending_clock_now_failure.jett b/tests/native/pending_clock_now_failure.jett new file mode 100644 index 00000000..5ee22f05 --- /dev/null +++ b/tests/native/pending_clock_now_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(clock: Clock) returns nothing: + Clock pending = run clock + time.Timestamp sampled = Clock.now(view pending) + trace sampled + return nothing diff --git a/tests/native/pending_contextual_empty_collections.jett b/tests/native/pending_contextual_empty_collections.jett new file mode 100644 index 00000000..08c04009 --- /dev/null +++ b/tests/native/pending_contextual_empty_collections.jett @@ -0,0 +1,13 @@ +namespace app +struct EmptyCollections: + items: list[int64] + rows: map[string, int64] +function main() returns nothing: + EmptyCollections value = EmptyCollections(items: run list(), rows: run map()) + trace value + list[int64] items = join value.items handle error: + return nothing + map[string, int64] rows = join value.rows handle error: + return nothing + println(list.length[int64](view items), map.length[string, int64](view rows)) + return nothing diff --git a/tests/native/pending_crypto_hmac_contextual_key_failure.jett b/tests/native/pending_crypto_hmac_contextual_key_failure.jett new file mode 100644 index 00000000..9f3cd8e1 --- /dev/null +++ b/tests/native/pending_crypto_hmac_contextual_key_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + secret[bytes] key = run bytes.from_string("k") + bytes message = bytes.from_string("m") + secret[bytes] tag = crypto.hmac_sha256(view key, view message) + return nothing diff --git a/tests/native/pending_crypto_hmac_key_failure.jett b/tests/native/pending_crypto_hmac_key_failure.jett new file mode 100644 index 00000000..46f1dc19 --- /dev/null +++ b/tests/native/pending_crypto_hmac_key_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + secret[bytes] plain = bytes.from_string("k") + secret[bytes] key = run plain + bytes message = bytes.from_string("m") + secret[bytes] tag = crypto.hmac_sha256(view key, view message) + return nothing diff --git a/tests/native/pending_crypto_hmac_message_failure.jett b/tests/native/pending_crypto_hmac_message_failure.jett new file mode 100644 index 00000000..882efebb --- /dev/null +++ b/tests/native/pending_crypto_hmac_message_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + secret[bytes] key = bytes.from_string("k") + bytes message = run bytes.from_string("m") + secret[bytes] tag = crypto.hmac_sha256(view key, view message) + return nothing diff --git a/tests/native/pending_csv_parse_failure.jett b/tests/native/pending_csv_parse_failure.jett new file mode 100644 index 00000000..be8101c8 --- /dev/null +++ b/tests/native/pending_csv_parse_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string data = run "a,b" + result[list[list[string]], string] parsed = csv.parse(data) + trace parsed + return nothing diff --git a/tests/native/pending_csv_parse_with_header_failure.jett b/tests/native/pending_csv_parse_with_header_failure.jett new file mode 100644 index 00000000..1fa641d1 --- /dev/null +++ b/tests/native/pending_csv_parse_with_header_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string data = run "name\na" + result[list[map[string, string]], string] parsed = csv.parse_with_header(data) + trace parsed + return nothing diff --git a/tests/native/pending_csv_stringify_field.jett b/tests/native/pending_csv_stringify_field.jett new file mode 100644 index 00000000..58ad7214 --- /dev/null +++ b/tests/native/pending_csv_stringify_field.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string field = run "a,b" + string serialized = csv.stringify(list(list(field, "c"))) + trace serialized + return nothing diff --git a/tests/native/pending_csv_stringify_list_failure.jett b/tests/native/pending_csv_stringify_list_failure.jett new file mode 100644 index 00000000..91c62d6e --- /dev/null +++ b/tests/native/pending_csv_stringify_list_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[list[string]] rows = run list(list("a", "b")) + string serialized = csv.stringify(rows) + trace serialized + return nothing diff --git a/tests/native/pending_csv_stringify_row_failure.jett b/tests/native/pending_csv_stringify_row_failure.jett new file mode 100644 index 00000000..a84fba21 --- /dev/null +++ b/tests/native/pending_csv_stringify_row_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[string] row = run list("a", "b") + string serialized = csv.stringify(list(row)) + trace serialized + return nothing diff --git a/tests/native/pending_encoding_base64_decode_failure.jett b/tests/native/pending_encoding_base64_decode_failure.jett new file mode 100644 index 00000000..1ad42fab --- /dev/null +++ b/tests/native/pending_encoding_base64_decode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string encoded = run "YQ==" + result[bytes, string] decoded = encoding.base64_decode(encoded) + trace decoded + return nothing diff --git a/tests/native/pending_encoding_base64_encode_failure.jett b/tests/native/pending_encoding_base64_encode_failure.jett new file mode 100644 index 00000000..63be8b1b --- /dev/null +++ b/tests/native/pending_encoding_base64_encode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bytes data = run bytes.from_string("a") + string encoded = encoding.base64_encode(view data) + trace encoded + return nothing diff --git a/tests/native/pending_encoding_form_decode_failure.jett b/tests/native/pending_encoding_form_decode_failure.jett new file mode 100644 index 00000000..0a148c5b --- /dev/null +++ b/tests/native/pending_encoding_form_decode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string encoded = run "a+b" + result[string, string] decoded = encoding.form_decode(encoded) + trace decoded + return nothing diff --git a/tests/native/pending_encoding_form_encode_failure.jett b/tests/native/pending_encoding_form_encode_failure.jett new file mode 100644 index 00000000..fff0801d --- /dev/null +++ b/tests/native/pending_encoding_form_encode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string data = run "a b" + string encoded = encoding.form_encode(data) + trace encoded + return nothing diff --git a/tests/native/pending_encoding_hex_decode_failure.jett b/tests/native/pending_encoding_hex_decode_failure.jett new file mode 100644 index 00000000..18c0f139 --- /dev/null +++ b/tests/native/pending_encoding_hex_decode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string encoded = run "61" + result[bytes, string] decoded = encoding.hex_decode(encoded) + trace decoded + return nothing diff --git a/tests/native/pending_encoding_url_decode_failure.jett b/tests/native/pending_encoding_url_decode_failure.jett new file mode 100644 index 00000000..916b9546 --- /dev/null +++ b/tests/native/pending_encoding_url_decode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string encoded = run "a%20b" + result[string, string] decoded = encoding.url_decode(encoded) + trace decoded + return nothing diff --git a/tests/native/pending_encoding_url_encode_failure.jett b/tests/native/pending_encoding_url_encode_failure.jett new file mode 100644 index 00000000..64e41fa8 --- /dev/null +++ b/tests/native/pending_encoding_url_encode_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string data = run "a b" + string encoded = encoding.url_encode(data) + trace encoded + return nothing diff --git a/tests/native/pending_enum_equality_failure.jett b/tests/native/pending_enum_equality_failure.jett new file mode 100644 index 00000000..eed54f43 --- /dev/null +++ b/tests/native/pending_enum_equality_failure.jett @@ -0,0 +1,9 @@ +namespace app +enum Message: + text(value: string) + empty +function main() returns nothing: + Message plain = Message.text("Ada") + Message pending = run clone plain + println(pending == plain) + return nothing diff --git a/tests/native/pending_enum_inequality_failure.jett b/tests/native/pending_enum_inequality_failure.jett new file mode 100644 index 00000000..8a0a7e77 --- /dev/null +++ b/tests/native/pending_enum_inequality_failure.jett @@ -0,0 +1,9 @@ +namespace app +enum Message: + text(value: string) + empty +function main() returns nothing: + Message plain = Message.text("Ada") + Message pending = run clone plain + println(pending != plain) + return nothing diff --git a/tests/native/pending_environment_args_failure.jett b/tests/native/pending_environment_args_failure.jett new file mode 100644 index 00000000..c0ae765c --- /dev/null +++ b/tests/native/pending_environment_args_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(env: Environment) returns nothing: + Environment pending = run env + list[string] arguments = Environment.args(view pending) + trace arguments + return nothing diff --git a/tests/native/pending_environment_get_authority_failure.jett b/tests/native/pending_environment_get_authority_failure.jett new file mode 100644 index 00000000..4dd5bb56 --- /dev/null +++ b/tests/native/pending_environment_get_authority_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(env: Environment) returns nothing: + Environment pending = run env + result[optional[string], string] found = Environment.get(view pending, "PATH") + trace found + return nothing diff --git a/tests/native/pending_environment_get_key_failure.jett b/tests/native/pending_environment_get_key_failure.jett new file mode 100644 index 00000000..b803d6cf --- /dev/null +++ b/tests/native/pending_environment_get_key_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(env: Environment) returns nothing: + string key = run "PATH" + result[optional[string], string] found = Environment.get(view env, key) + trace found + return nothing diff --git a/tests/native/pending_environment_values.jett b/tests/native/pending_environment_values.jett new file mode 100644 index 00000000..c79364af --- /dev/null +++ b/tests/native/pending_environment_values.jett @@ -0,0 +1,9 @@ +namespace app +function main(env: Environment) returns nothing: + trace env + Environment pending = run env + trace pending + Environment joined = join pending handle error: + return nothing + trace joined + return nothing diff --git a/tests/native/pending_float32_from_float64_failure.jett b/tests/native/pending_float32_from_float64_failure.jett new file mode 100644 index 00000000..7e38a7e0 --- /dev/null +++ b/tests/native/pending_float32_from_float64_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float32 value = float32.from_float64(run 1.5) + trace value + return nothing diff --git a/tests/native/pending_float64_from_int64_failure.jett b/tests/native/pending_float64_from_int64_failure.jett new file mode 100644 index 00000000..4478c7c8 --- /dev/null +++ b/tests/native/pending_float64_from_int64_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[float64, string] value = float64.from_int64(run 3) + trace value + return nothing diff --git a/tests/native/pending_float64_from_string_failure.jett b/tests/native/pending_float64_from_string_failure.jett new file mode 100644 index 00000000..5f2ce72c --- /dev/null +++ b/tests/native/pending_float64_from_string_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[float64, string] value = float64.from_string(run "3.5") + trace value + return nothing diff --git a/tests/native/pending_function_call_failure.jett b/tests/native/pending_function_call_failure.jett new file mode 100644 index 00000000..863e24e7 --- /dev/null +++ b/tests/native/pending_function_call_failure.jett @@ -0,0 +1,7 @@ +namespace app +function increment(value: int64) returns int64: + return value + 1 +function main() returns nothing: + function(int64) returns int64 pending = run increment + println(pending(4)) + return nothing diff --git a/tests/native/pending_function_values.jett b/tests/native/pending_function_values.jett new file mode 100644 index 00000000..c5219140 --- /dev/null +++ b/tests/native/pending_function_values.jett @@ -0,0 +1,24 @@ +namespace app +function increment(value: int64) returns int64: + return value + 1 +function main() returns nothing: + function(int64) returns int64 plain = increment + function(int64) returns int64 pending = run clone plain + function(int64) returns int64 nested = run clone pending + trace plain + trace pending + trace nested + function(int64) returns int64 joined = join pending handle error: + default increment + trace joined + println(joined(4)) + int64 offset = 7 + function(int64) returns int64 captured = function(value: int64) returns int64: return value + offset + function(int64) returns int64 pending_captured = run clone captured + trace pending_captured + list[function(int64) returns int64] callbacks = list(clone nested, clone pending_captured) + trace callbacks + function(int64) returns int64 joined_captured = join pending_captured handle error: + default increment + println(joined_captured(4)) + return nothing diff --git a/tests/native/pending_int64_from_float64_failure.jett b/tests/native/pending_int64_from_float64_failure.jett new file mode 100644 index 00000000..387aa555 --- /dev/null +++ b/tests/native/pending_int64_from_float64_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[int64, string] value = int64.from_float64(run 3.0) + trace value + return nothing diff --git a/tests/native/pending_int64_from_string_failure.jett b/tests/native/pending_int64_from_string_failure.jett new file mode 100644 index 00000000..ecd99127 --- /dev/null +++ b/tests/native/pending_int64_from_string_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[int64, string] value = int64.from_string(run "3") + trace value + return nothing diff --git a/tests/native/pending_json_parse_input_failure.jett b/tests/native/pending_json_parse_input_failure.jett new file mode 100644 index 00000000..d7d04d41 --- /dev/null +++ b/tests/native/pending_json_parse_input_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string input = run "123" + result[int64, string] parsed = json.parse[int64](input) + trace parsed + return nothing diff --git a/tests/native/pending_json_serialize_string_failure.jett b/tests/native/pending_json_serialize_string_failure.jett new file mode 100644 index 00000000..72f5b0c4 --- /dev/null +++ b/tests/native/pending_json_serialize_string_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string value = run "abc" + string encoded = json.serialize[string](view value) + trace encoded + return nothing diff --git a/tests/native/pending_list_append_container_failure.jett b/tests/native/pending_list_append_container_failure.jett new file mode 100644 index 00000000..6e8e546c --- /dev/null +++ b/tests/native/pending_list_append_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = run list(1) + list[int64] changed = list.append[int64](items, 2) + trace changed + return nothing diff --git a/tests/native/pending_list_get_container_failure.jett b/tests/native/pending_list_get_container_failure.jett new file mode 100644 index 00000000..233c202f --- /dev/null +++ b/tests/native/pending_list_get_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = run list(1) + optional[int64] value = list.get[int64](view items, 0) + trace value + return nothing diff --git a/tests/native/pending_list_get_index_failure.jett b/tests/native/pending_list_get_index_failure.jett new file mode 100644 index 00000000..94a89ca4 --- /dev/null +++ b/tests/native/pending_list_get_index_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = list(1, 2) + optional[int64] value = list.get[int64](view items, run 0) + trace value + return nothing diff --git a/tests/native/pending_list_insert_index_failure.jett b/tests/native/pending_list_insert_index_failure.jett new file mode 100644 index 00000000..29bb01bc --- /dev/null +++ b/tests/native/pending_list_insert_index_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = list(1) + list[int64] changed = list.insert_at[int64](items, run 0, 2) + trace changed + return nothing diff --git a/tests/native/pending_list_is_sorted_container_failure.jett b/tests/native/pending_list_is_sorted_container_failure.jett new file mode 100644 index 00000000..bcb26239 --- /dev/null +++ b/tests/native/pending_list_is_sorted_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = run list(1, 2) + bool sorted = list.is_sorted[int64](view items) + trace sorted + return nothing diff --git a/tests/native/pending_list_iteration_failure.jett b/tests/native/pending_list_iteration_failure.jett new file mode 100644 index 00000000..b48b2f15 --- /dev/null +++ b/tests/native/pending_list_iteration_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = run list(2) + for item in values: + trace item + return nothing diff --git a/tests/native/pending_list_length_container_failure.jett b/tests/native/pending_list_length_container_failure.jett new file mode 100644 index 00000000..5e76144b --- /dev/null +++ b/tests/native/pending_list_length_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = run list(1) + int64 count = list.length[int64](view items) + trace count + return nothing diff --git a/tests/native/pending_list_remove_index_failure.jett b/tests/native/pending_list_remove_index_failure.jett new file mode 100644 index 00000000..bd27b607 --- /dev/null +++ b/tests/native/pending_list_remove_index_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = list(1) + list[int64] changed = list.remove_at[int64](items, run 0) + trace changed + return nothing diff --git a/tests/native/pending_list_sort_container_failure.jett b/tests/native/pending_list_sort_container_failure.jett new file mode 100644 index 00000000..577a15de --- /dev/null +++ b/tests/native/pending_list_sort_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = run list(2, 1) + list[int64] sorted = list.sort[int64](items) + trace sorted + return nothing diff --git a/tests/native/pending_list_sort_index_failure.jett b/tests/native/pending_list_sort_index_failure.jett new file mode 100644 index 00000000..d659a85d --- /dev/null +++ b/tests/native/pending_list_sort_index_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[list[int64]] rows = list(list(2), list(1)) + list[list[int64]] sorted = list.sort_by_index[int64](rows, run 0) + trace sorted + return nothing diff --git a/tests/native/pending_list_sum_container_failure.jett b/tests/native/pending_list_sum_container_failure.jett new file mode 100644 index 00000000..46250873 --- /dev/null +++ b/tests/native/pending_list_sum_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = run list(2, 3) + int64 total = list.sum[int64](view values) + trace total + return nothing diff --git a/tests/native/pending_list_sum_first_failure.jett b/tests/native/pending_list_sum_first_failure.jett new file mode 100644 index 00000000..ed75329b --- /dev/null +++ b/tests/native/pending_list_sum_first_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = list(run 2, 3) + int64 total = list.sum[int64](view values) + trace total + return nothing diff --git a/tests/native/pending_list_sum_later_failure.jett b/tests/native/pending_list_sum_later_failure.jett new file mode 100644 index 00000000..df601b73 --- /dev/null +++ b/tests/native/pending_list_sum_later_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = list(2, run 3) + int64 total = list.sum[int64](view values) + trace total + return nothing diff --git a/tests/native/pending_list_swap_first_failure.jett b/tests/native/pending_list_swap_first_failure.jett new file mode 100644 index 00000000..fa99e22c --- /dev/null +++ b/tests/native/pending_list_swap_first_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = list(1, 2) + list[int64] changed = list.swap[int64](items, run 0, 1) + trace changed + return nothing diff --git a/tests/native/pending_list_swap_second_failure.jett b/tests/native/pending_list_swap_second_failure.jett new file mode 100644 index 00000000..38cbf039 --- /dev/null +++ b/tests/native/pending_list_swap_second_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] items = list(1, 2) + list[int64] changed = list.swap[int64](items, 0, run 1) + trace changed + return nothing diff --git a/tests/native/pending_list_values.jett b/tests/native/pending_list_values.jett new file mode 100644 index 00000000..44d29f28 --- /dev/null +++ b/tests/native/pending_list_values.jett @@ -0,0 +1,29 @@ +namespace app +enum Batch: + numbers(values: list[int64]) + empty +function identity(value: list[int64]) returns list[int64]: + return value +function make_pending() returns list[int64]: + return run identity(list(2, 3)) +function main() returns nothing: + list[int64] plain = list(1) + list[int64] pending = run clone plain + list[int64] nested = run make_pending() + list[list[int64]] tasks = list(clone pending, clone nested) + trace plain + trace pending + trace nested + trace tasks + println(Batch.numbers(clone pending) == Batch.numbers(clone pending)) + println(Batch.numbers(clone pending) == Batch.numbers(clone plain)) + list[int64] first = join pending handle error: + default list(-1) + list[int64] inner = join nested handle error: + default list(-1) + trace first + trace inner + list[int64] last = join inner handle error: + default list(-1) + trace last + return nothing diff --git a/tests/native/pending_machine_field_owner_failure.jett b/tests/native/pending_machine_field_owner_failure.jett new file mode 100644 index 00000000..dd6ac065 --- /dev/null +++ b/tests/native/pending_machine_field_owner_failure.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + active(name: string) + transitions: + active to active +function main() returns nothing: + Session session = Session(active, "Ada") + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + Session pending_session = run Session(active, "Lin") + string copied = type.machine_field_value[Session, string](view pending_session, view field) + println(copied) + return nothing diff --git a/tests/native/pending_machine_foreign_field_precedence.jett b/tests/native/pending_machine_foreign_field_precedence.jett new file mode 100644 index 00000000..b524e664 --- /dev/null +++ b/tests/native/pending_machine_foreign_field_precedence.jett @@ -0,0 +1,16 @@ +namespace app +machine Session: + states: + first(value: int64) + second(value: int64) + transitions: + first to second +function main() returns nothing: + Session session = run Session(first, 7) + TypeMachineState other = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 copied = type.machine_field_value[Session, int64](view session, view foreign) + println(copied) + return nothing diff --git a/tests/native/pending_machine_state_value_failure.jett b/tests/native/pending_machine_state_value_failure.jett new file mode 100644 index 00000000..ed1830f3 --- /dev/null +++ b/tests/native/pending_machine_state_value_failure.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + active(name: string) + transitions: + active to active +function main() returns nothing: + Session session = run Session(active, "Ada") + TypeMachineState state = type.machine_state_value[Session](view session) + println(state.name) + return nothing diff --git a/tests/native/pending_map_entries.jett b/tests/native/pending_map_entries.jett new file mode 100644 index 00000000..2efd1cd4 --- /dev/null +++ b/tests/native/pending_map_entries.jett @@ -0,0 +1,40 @@ +namespace app +function main() returns nothing: + map[int64, int64] entries = map(run 2: run 7, 2: 8) + trace entries + bool has_pending = map.has[int64, int64](view entries, run 2) + bool has_plain = map.has[int64, int64](view entries, 2) + trace has_pending + trace has_plain + int64 found = map.get[int64, int64](clone entries, run 2) handle: + default -1 + trace found + map[int64, int64] copied = clone entries + trace copied + map[int64, int64] kept = map.remove[int64, int64](clone entries, 2) + trace kept + map[int64, int64] removed_pending = map.remove[int64, int64](clone entries, run 2) + trace removed_pending + map[int64, int64] updated = map.insert[int64, int64](clone entries, run 2, run 9) + trace updated + map[int64, int64] added = map.insert[int64, int64](clone entries, 3, run 10) + trace added + mutable list[int64] keys = list() + mutable list[int64] values = list() + for key, value in copied: + keys = list.append[int64](keys, key) + values = list.append[int64](values, value) + trace keys + trace values + map[int64, int64] zipped = map.from_lists[int64, int64](list(run 5), list(run 6)) + trace zipped + map[string, int64] words = map(run "north": run 1, "north": 2) + trace words + bool has_pending_word = map.has[string, int64](view words, run "north") + bool has_plain_word = map.has[string, int64](view words, "north") + trace has_pending_word + trace has_plain_word + map[string, int64] only_pending = map(run "east": 3) + bool missing_plain_word = map.has[string, int64](view only_pending, "east") + trace missing_plain_word + return nothing diff --git a/tests/native/pending_map_from_lists_failure.jett b/tests/native/pending_map_from_lists_failure.jett new file mode 100644 index 00000000..d28401f6 --- /dev/null +++ b/tests/native/pending_map_from_lists_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] keys = run list(2) + map[int64, int64] values = map.from_lists[int64, int64](keys, list(3)) + trace values + return nothing diff --git a/tests/native/pending_map_get_failure.jett b/tests/native/pending_map_get_failure.jett new file mode 100644 index 00000000..89ca2fe8 --- /dev/null +++ b/tests/native/pending_map_get_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + map[int64, int64] values = run map(2: 3) + int64 found = map.get[int64, int64](values, 2) handle: + default -1 + trace found + return nothing diff --git a/tests/native/pending_map_has_failure.jett b/tests/native/pending_map_has_failure.jett new file mode 100644 index 00000000..6d43888a --- /dev/null +++ b/tests/native/pending_map_has_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + map[int64, int64] values = run map(2: 3) + bool has = map.has[int64, int64](view values, 2) + trace has + return nothing diff --git a/tests/native/pending_map_insert_failure.jett b/tests/native/pending_map_insert_failure.jett new file mode 100644 index 00000000..adb1203a --- /dev/null +++ b/tests/native/pending_map_insert_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + map[int64, int64] values = run map(2: 3) + map[int64, int64] changed = map.insert[int64, int64](values, 4, 5) + trace changed + return nothing diff --git a/tests/native/pending_map_iteration_failure.jett b/tests/native/pending_map_iteration_failure.jett new file mode 100644 index 00000000..6271907c --- /dev/null +++ b/tests/native/pending_map_iteration_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + map[string, int64] values = run map("a": 2) + for key, value in values: + trace key + return nothing diff --git a/tests/native/pending_map_length_failure.jett b/tests/native/pending_map_length_failure.jett new file mode 100644 index 00000000..c7659a60 --- /dev/null +++ b/tests/native/pending_map_length_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + map[int64, int64] values = run map(2: 3) + int64 length = map.length[int64, int64](view values) + trace length + return nothing diff --git a/tests/native/pending_map_remove_failure.jett b/tests/native/pending_map_remove_failure.jett new file mode 100644 index 00000000..36a06309 --- /dev/null +++ b/tests/native/pending_map_remove_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + map[int64, int64] values = run map(2: 3) + map[int64, int64] changed = map.remove[int64, int64](values, 2) + trace changed + return nothing diff --git a/tests/native/pending_math_abs_failure.jett b/tests/native/pending_math_abs_failure.jett new file mode 100644 index 00000000..fe23574e --- /dev/null +++ b/tests/native/pending_math_abs_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.abs(run 3) + trace value + return nothing diff --git a/tests/native/pending_math_average_container_failure.jett b/tests/native/pending_math_average_container_failure.jett new file mode 100644 index 00000000..cd4bed23 --- /dev/null +++ b/tests/native/pending_math_average_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = run list(2, 3) + float64 average = math.average(values) + trace average + return nothing diff --git a/tests/native/pending_math_average_failure.jett b/tests/native/pending_math_average_failure.jett new file mode 100644 index 00000000..b6289c5a --- /dev/null +++ b/tests/native/pending_math_average_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[int64] values = list(2, run 3) + float64 average = math.average(values) + trace average + return nothing diff --git a/tests/native/pending_math_ceil_failure.jett b/tests/native/pending_math_ceil_failure.jett new file mode 100644 index 00000000..debcdbed --- /dev/null +++ b/tests/native/pending_math_ceil_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.ceil(run 4.5) + trace value + return nothing diff --git a/tests/native/pending_math_clamp_failure.jett b/tests/native/pending_math_clamp_failure.jett new file mode 100644 index 00000000..178472ee --- /dev/null +++ b/tests/native/pending_math_clamp_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.clamp(2.0, run 0.0, 3.0) + trace value + return nothing diff --git a/tests/native/pending_math_clamp_upper_failure.jett b/tests/native/pending_math_clamp_upper_failure.jett new file mode 100644 index 00000000..2f26d13d --- /dev/null +++ b/tests/native/pending_math_clamp_upper_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.clamp(2.0, 0.0, run 3.0) + trace value + return nothing diff --git a/tests/native/pending_math_cos_failure.jett b/tests/native/pending_math_cos_failure.jett new file mode 100644 index 00000000..c45698e8 --- /dev/null +++ b/tests/native/pending_math_cos_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.cos(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_factorial_failure.jett b/tests/native/pending_math_factorial_failure.jett new file mode 100644 index 00000000..fa6c1504 --- /dev/null +++ b/tests/native/pending_math_factorial_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.factorial(run 4) + trace value + return nothing diff --git a/tests/native/pending_math_floor_failure.jett b/tests/native/pending_math_floor_failure.jett new file mode 100644 index 00000000..c3bf3afb --- /dev/null +++ b/tests/native/pending_math_floor_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.floor(run 4.5) + trace value + return nothing diff --git a/tests/native/pending_math_gcd_failure.jett b/tests/native/pending_math_gcd_failure.jett new file mode 100644 index 00000000..8362b5cf --- /dev/null +++ b/tests/native/pending_math_gcd_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.gcd(8, run 12) + trace value + return nothing diff --git a/tests/native/pending_math_lcm_failure.jett b/tests/native/pending_math_lcm_failure.jett new file mode 100644 index 00000000..a869e233 --- /dev/null +++ b/tests/native/pending_math_lcm_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.lcm(run 8, 12) + trace value + return nothing diff --git a/tests/native/pending_math_log10_failure.jett b/tests/native/pending_math_log10_failure.jett new file mode 100644 index 00000000..73507937 --- /dev/null +++ b/tests/native/pending_math_log10_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.log10(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_log2_failure.jett b/tests/native/pending_math_log2_failure.jett new file mode 100644 index 00000000..d7dd9d3e --- /dev/null +++ b/tests/native/pending_math_log2_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.log2(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_log_failure.jett b/tests/native/pending_math_log_failure.jett new file mode 100644 index 00000000..8ea62479 --- /dev/null +++ b/tests/native/pending_math_log_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.log(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_max_failure.jett b/tests/native/pending_math_max_failure.jett new file mode 100644 index 00000000..7830c050 --- /dev/null +++ b/tests/native/pending_math_max_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.max(2.0, run 3.0) + trace value + return nothing diff --git a/tests/native/pending_math_median_container_failure.jett b/tests/native/pending_math_median_container_failure.jett new file mode 100644 index 00000000..60e96598 --- /dev/null +++ b/tests/native/pending_math_median_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[float64] values = run list(2.0, 3.0) + float64 median = math.median(values) + trace median + return nothing diff --git a/tests/native/pending_math_median_failure.jett b/tests/native/pending_math_median_failure.jett new file mode 100644 index 00000000..7d51fbc8 --- /dev/null +++ b/tests/native/pending_math_median_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[float64] values = list(2.0, run 3.0) + float64 median = math.median(values) + trace median + return nothing diff --git a/tests/native/pending_math_min_failure.jett b/tests/native/pending_math_min_failure.jett new file mode 100644 index 00000000..93b549e9 --- /dev/null +++ b/tests/native/pending_math_min_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.min(3, run 4) + trace value + return nothing diff --git a/tests/native/pending_math_min_first_failure.jett b/tests/native/pending_math_min_first_failure.jett new file mode 100644 index 00000000..badbf204 --- /dev/null +++ b/tests/native/pending_math_min_first_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.min(run 3, 4) + trace value + return nothing diff --git a/tests/native/pending_math_mod_failure.jett b/tests/native/pending_math_mod_failure.jett new file mode 100644 index 00000000..68afb030 --- /dev/null +++ b/tests/native/pending_math_mod_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + int64 value = math.mod(run 7, 3) + trace value + return nothing diff --git a/tests/native/pending_math_pow_exponent_failure.jett b/tests/native/pending_math_pow_exponent_failure.jett new file mode 100644 index 00000000..e231333f --- /dev/null +++ b/tests/native/pending_math_pow_exponent_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.pow(2.0, run 3.0) + trace value + return nothing diff --git a/tests/native/pending_math_pow_failure.jett b/tests/native/pending_math_pow_failure.jett new file mode 100644 index 00000000..a9a7a287 --- /dev/null +++ b/tests/native/pending_math_pow_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.pow(run 2.0, 3.0) + trace value + return nothing diff --git a/tests/native/pending_math_round_failure.jett b/tests/native/pending_math_round_failure.jett new file mode 100644 index 00000000..b306ff3d --- /dev/null +++ b/tests/native/pending_math_round_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.round(run 4.5) + trace value + return nothing diff --git a/tests/native/pending_math_sin_failure.jett b/tests/native/pending_math_sin_failure.jett new file mode 100644 index 00000000..f0b1a591 --- /dev/null +++ b/tests/native/pending_math_sin_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.sin(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_sqrt_failure.jett b/tests/native/pending_math_sqrt_failure.jett new file mode 100644 index 00000000..1fca2139 --- /dev/null +++ b/tests/native/pending_math_sqrt_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.sqrt(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_math_tan_failure.jett b/tests/native/pending_math_tan_failure.jett new file mode 100644 index 00000000..e977b0db --- /dev/null +++ b/tests/native/pending_math_tan_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + float64 value = math.tan(run 4.0) + trace value + return nothing diff --git a/tests/native/pending_narrow_integers.jett b/tests/native/pending_narrow_integers.jett new file mode 100644 index 00000000..0049c4ec --- /dev/null +++ b/tests/native/pending_narrow_integers.jett @@ -0,0 +1,67 @@ +namespace app +struct Narrow: + signed: int16 + unsigned: uint16 +function echo_int8(value: int8) returns int8: + return value +function echo_uint32(value: uint32) returns uint32: + return value +function check_signed() returns nothing: + int8 small = -128 + int8 pending_small = run run echo_int8(small) + trace pending_small + int8 once = join pending_small handle error: + return nothing + trace once + int8 ready = join once handle error: + return nothing + int16 base_medium = -32768 + int32 base_large = -2147483648 + int16 medium = run base_medium + int32 large = run base_large + trace medium + trace large + int16 joined_medium = join medium handle error: + return nothing + int32 joined_large = join large handle error: + return nothing + println(ready, joined_medium, joined_large) +function check_unsigned() returns nothing: + uint8 base_small = 255 + uint16 base_medium = 65535 + uint32 base_large = 4294967295 + uint8 small = run base_small + uint16 medium = run base_medium + uint32 large = run run echo_uint32(base_large) + trace small + trace medium + trace large + uint8 joined_small = join small handle error: + return nothing + uint16 joined_medium = join medium handle error: + return nothing + uint32 once = join large handle error: + return nothing + trace once + uint32 joined_large = join once handle error: + return nothing + println(joined_small, joined_medium, joined_large) +function check_aggregate() returns nothing: + int16 base_signed = -123 + uint16 base_unsigned = 456 + Narrow value = Narrow(signed: run base_signed, unsigned: run base_unsigned) + trace value + int16 signed = value.signed + uint16 unsigned = value.unsigned + trace signed + trace unsigned + int16 joined_signed = join signed handle error: + return nothing + uint16 joined_unsigned = join unsigned handle error: + return nothing + println(joined_signed, joined_unsigned) +function main() returns nothing: + check_signed() + check_unsigned() + check_aggregate() + return nothing diff --git a/tests/native/pending_narrowed_machine_field_owner_failure.jett b/tests/native/pending_narrowed_machine_field_owner_failure.jett new file mode 100644 index 00000000..86283a2a --- /dev/null +++ b/tests/native/pending_narrowed_machine_field_owner_failure.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + active(name: string) + transitions: + active to active +function main() returns nothing: + Session at active session = run Session(active, "Ada") + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 0) handle: + return nothing + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + string copied = type.machine_field_value[Session at active, string](view session, view field) + println(copied) + return nothing diff --git a/tests/native/pending_nothing_equality_failure.jett b/tests/native/pending_nothing_equality_failure.jett new file mode 100644 index 00000000..b55fac06 --- /dev/null +++ b/tests/native/pending_nothing_equality_failure.jett @@ -0,0 +1,8 @@ +namespace test +function work() returns nothing: + return nothing +function main() returns nothing: + nothing pending = run work() + println("before equality") + println(pending == nothing) + return nothing diff --git a/tests/native/pending_nothing_inequality_failure.jett b/tests/native/pending_nothing_inequality_failure.jett new file mode 100644 index 00000000..460a65fb --- /dev/null +++ b/tests/native/pending_nothing_inequality_failure.jett @@ -0,0 +1,10 @@ +namespace test +function work() returns nothing: + return nothing +function make_pending() returns nothing: + return run work() +function main() returns nothing: + nothing nested = run make_pending() + println("before inequality") + println(nothing != nested) + return nothing diff --git a/tests/native/pending_random_bool_failure.jett b/tests/native/pending_random_bool_failure.jett new file mode 100644 index 00000000..525dac30 --- /dev/null +++ b/tests/native/pending_random_bool_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(rng: Random) returns nothing: + Random pending = run rng + bool sampled = random.bool(view pending) + trace sampled + return nothing diff --git a/tests/native/pending_random_float_failure.jett b/tests/native/pending_random_float_failure.jett new file mode 100644 index 00000000..ceed6c29 --- /dev/null +++ b/tests/native/pending_random_float_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(rng: Random) returns nothing: + Random pending = run rng + float64 sampled = random.float64(view pending) + trace sampled + return nothing diff --git a/tests/native/pending_random_int_failure.jett b/tests/native/pending_random_int_failure.jett new file mode 100644 index 00000000..adfe65a1 --- /dev/null +++ b/tests/native/pending_random_int_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(rng: Random) returns nothing: + Random pending = run rng + result[int64, string] sampled = random.int64(view pending, 0, 10) + trace sampled + return nothing diff --git a/tests/native/pending_random_lower_bound_failure.jett b/tests/native/pending_random_lower_bound_failure.jett new file mode 100644 index 00000000..f6192f08 --- /dev/null +++ b/tests/native/pending_random_lower_bound_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(rng: Random) returns nothing: + int64 lower = run 0 + result[int64, string] sampled = random.int64(view rng, lower, 10) + trace sampled + return nothing diff --git a/tests/native/pending_random_upper_bound_failure.jett b/tests/native/pending_random_upper_bound_failure.jett new file mode 100644 index 00000000..c06b1eaf --- /dev/null +++ b/tests/native/pending_random_upper_bound_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main(rng: Random) returns nothing: + int64 upper = run 10 + result[int64, string] sampled = random.int64(view rng, 0, upper) + trace sampled + return nothing diff --git a/tests/native/pending_range_end_failure.jett b/tests/native/pending_range_end_failure.jett new file mode 100644 index 00000000..318fc108 --- /dev/null +++ b/tests/native/pending_range_end_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + list[int64] values = range(run 3) + trace values + return nothing diff --git a/tests/native/pending_range_second_failure.jett b/tests/native/pending_range_second_failure.jett new file mode 100644 index 00000000..d2e0eac6 --- /dev/null +++ b/tests/native/pending_range_second_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + list[int64] values = range(0, run 3) + trace values + return nothing diff --git a/tests/native/pending_range_start_failure.jett b/tests/native/pending_range_start_failure.jett new file mode 100644 index 00000000..d688b9c3 --- /dev/null +++ b/tests/native/pending_range_start_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + list[int64] values = range(run 1, 3) + trace values + return nothing diff --git a/tests/native/pending_range_step_failure.jett b/tests/native/pending_range_step_failure.jett new file mode 100644 index 00000000..66ebc64b --- /dev/null +++ b/tests/native/pending_range_step_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + list[int64] values = range(0, 3, run 1) + trace values + return nothing diff --git a/tests/native/pending_record_values.jett b/tests/native/pending_record_values.jett new file mode 100644 index 00000000..505cbc0e --- /dev/null +++ b/tests/native/pending_record_values.jett @@ -0,0 +1,63 @@ +namespace app +struct User: + name: string +enum Message: + text(value: string) + empty +machine Session: + states: + active(user: string) + closed + transitions: + active to closed +bitfield Header: + code: 8 bits +function pending_user() returns User: + return run User(name: "Lin") +function check_user() returns nothing: + User plain = User(name: "Ada") + User pending = run clone plain + User nested = run pending_user() + trace plain + trace pending + trace nested + User first = join pending handle error: + default User(name: "unexpected") + User inner = join nested handle error: + default User(name: "unexpected") + trace first + trace inner + return nothing +function check_enum() returns nothing: + Message plain = Message.text("Ada") + Message pending = run clone plain + trace plain + trace pending + Message joined = join pending handle error: + default Message.empty + trace joined + return nothing +function check_machine() returns nothing: + Session at active plain = Session(active, "Ada") + Session at active pending = run clone plain + trace plain + trace pending + Session at active joined = join pending handle error: + default Session(active, "unexpected") + trace joined + return nothing +function check_bitfield() returns nothing: + Header plain = Header(code: 7) + Header pending = run clone plain + trace plain + trace pending + Header joined = join pending handle error: + default Header(code: 0) + trace joined + return nothing +function main() returns nothing: + check_user() + check_enum() + check_machine() + check_bitfield() + return nothing diff --git a/tests/native/pending_refined_narrow.jett b/tests/native/pending_refined_narrow.jett new file mode 100644 index 00000000..ca796127 --- /dev/null +++ b/tests/native/pending_refined_narrow.jett @@ -0,0 +1,10 @@ +namespace app +type Positive8 = int8 where value > 0 +function main() returns nothing: + int8 base = 7 + int8 pending = run base + Positive8 refined = pending handle error: + println(error) + return nothing + trace refined + return nothing diff --git a/tests/native/pending_reflected_collections.jett b/tests/native/pending_reflected_collections.jett new file mode 100644 index 00000000..e0930c47 --- /dev/null +++ b/tests/native/pending_reflected_collections.jett @@ -0,0 +1,56 @@ +namespace app +struct Parcel: + rows: map[string, string] + tags: set[string] +enum Message: + entries(rows: map[string, string]) +machine Session: + states: + active(tags: set[string]) + transitions: + active to active +function pending_tags() returns set[string]: + mutable set[string] tags = set.new[string]() + tags = set.add[string](tags, "live") + return run tags +function check_struct() returns nothing: + Parcel parcel = Parcel(rows: run map("a": "Ada"), tags: pending_tags()) + TypeField rows_field = list.get[TypeField](type.fields[Parcel](), 0) handle: + return nothing + map[string, string] rows = type.field_value[Parcel, map[string, string]](view parcel, view rows_field) + trace rows + map[string, string] joined_rows = join rows handle error: + return nothing + println(map.length[string, string](view joined_rows)) + TypeField tags_field = list.get[TypeField](type.fields[Parcel](), 1) handle: + return nothing + set[string] tags = type.field_value[Parcel, set[string]](view parcel, view tags_field) + trace tags + set[string] joined_tags = join tags handle error: + return nothing + println(set.length[string](view joined_tags)) +function check_enum() returns nothing: + Message message = Message.entries(run map("b": "Bea")) + TypeVariant variant = type.variant_value[Message](view message) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + map[string, string] rows = type.variant_field_value[Message, map[string, string]](view message, view field) + trace rows + map[string, string] joined = join rows handle error: + return nothing + println(map.length[string, string](view joined)) +function check_machine() returns nothing: + Session session = Session(active, pending_tags()) + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + set[string] tags = type.machine_field_value[Session, set[string]](view session, view field) + trace tags + set[string] joined = join tags handle error: + return nothing + println(set.length[string](view joined)) +function main() returns nothing: + check_struct() + check_enum() + check_machine() + return nothing diff --git a/tests/native/pending_reflected_construction.jett b/tests/native/pending_reflected_construction.jett new file mode 100644 index 00000000..f32f7ec6 --- /dev/null +++ b/tests/native/pending_reflected_construction.jett @@ -0,0 +1,82 @@ +namespace app +struct User: + age: int64 +enum Message: + number(value: int64) + empty +machine Session: + states: + active(count: int64) + closed + transitions: + active to closed +bitfield Header: + code: 8 bits +function check_struct() returns nothing: + TypeField field = list.get[TypeField](type.fields[User](), 0) handle: + return nothing + TypeConstruction builder = type.construct_start[User]() + TypeConstruction filled = type.construct_put[User, int64](builder, view field, run run 7) handle error: + println(error) + return nothing + trace filled + TypeConstruction copied = clone filled + trace copied + User built = type.construct_finish[User](copied) handle error: + println(error) + return nothing + trace built + return nothing +function check_enum() returns nothing: + TypeVariant variant = list.get[TypeVariant](type.variants[Message](), 0) handle: + return nothing + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + TypeConstruction builder = type.construct_variant_start[Message](view variant) handle error: + println(error) + return nothing + TypeConstruction filled = type.construct_put[Message, int64](builder, view field, run 8) handle error: + println(error) + return nothing + trace filled + Message built = type.construct_finish[Message](filled) handle error: + println(error) + return nothing + trace built + return nothing +function check_machine() returns nothing: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 0) handle: + return nothing + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + println(error) + return nothing + TypeConstruction filled = type.construct_put[Session, int64](builder, view field, run 9) handle error: + println(error) + return nothing + trace filled + Session built = type.construct_finish[Session](filled) handle error: + println(error) + return nothing + trace built + return nothing +function check_bitfield() returns nothing: + TypeField field = list.get[TypeField](type.fields[Header](), 0) handle: + return nothing + TypeConstruction builder = type.construct_start[Header]() + TypeConstruction filled = type.construct_put[Header, int64](builder, view field, run 10) handle error: + println(error) + return nothing + trace filled + Header built = type.construct_finish[Header](filled) handle error: + println(error) + return nothing + trace built + return nothing +function main() returns nothing: + check_struct() + check_enum() + check_machine() + check_bitfield() + return nothing diff --git a/tests/native/pending_reflected_field_failure.jett b/tests/native/pending_reflected_field_failure.jett new file mode 100644 index 00000000..16fc4f50 --- /dev/null +++ b/tests/native/pending_reflected_field_failure.jett @@ -0,0 +1,11 @@ +namespace app +struct Record: + name: string +function main() returns nothing: + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return nothing + TypeField pending_field = run field + Record record = Record(name: "Ada") + string copied = type.field_value[Record, string](view record, view pending_field) + println(copied) + return nothing diff --git a/tests/native/pending_reflected_machine_field_failure.jett b/tests/native/pending_reflected_machine_field_failure.jett new file mode 100644 index 00000000..81b752a6 --- /dev/null +++ b/tests/native/pending_reflected_machine_field_failure.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + active(name: string) + transitions: + active to active +function main() returns nothing: + Session session = Session(active, "Ada") + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + TypeField pending_field = run field + string copied = type.machine_field_value[Session, string](view session, view pending_field) + println(copied) + return nothing diff --git a/tests/native/pending_reflected_owned_fields.jett b/tests/native/pending_reflected_owned_fields.jett new file mode 100644 index 00000000..9cbea768 --- /dev/null +++ b/tests/native/pending_reflected_owned_fields.jett @@ -0,0 +1,54 @@ +namespace app +struct Parcel: + label: string + items: list[int64] +enum Message: + items(values: list[int64]) + empty +machine Session: + states: + active(values: list[int64]) + closed + transitions: + active to closed +function check_struct() returns nothing: + Parcel parcel = Parcel(label: run "ready", items: run list(1, 2)) + TypeField label_field = list.get[TypeField](type.fields[Parcel](), 0) handle: + return nothing + string label = type.field_value[Parcel, string](view parcel, view label_field) + trace label + string joined_label = join label handle error: + return nothing + println(joined_label) + TypeField items_field = list.get[TypeField](type.fields[Parcel](), 1) handle: + return nothing + list[int64] items = type.field_value[Parcel, list[int64]](view parcel, view items_field) + trace items + list[int64] joined_items = join items handle error: + return nothing + println(list.sum[int64](view joined_items)) +function check_enum() returns nothing: + Message message = Message.items(run list(3, 4)) + TypeVariant variant = type.variant_value[Message](view message) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + list[int64] values = type.variant_field_value[Message, list[int64]](view message, view field) + trace values + list[int64] joined = join values handle error: + return nothing + println(list.sum[int64](view joined)) +function check_machine() returns nothing: + Session session = Session(active, run list(5, 6)) + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + list[int64] values = type.machine_field_value[Session, list[int64]](view session, view field) + trace values + list[int64] joined = join values handle error: + return nothing + println(list.sum[int64](view joined)) +function main() returns nothing: + check_struct() + check_enum() + check_machine() + return nothing diff --git a/tests/native/pending_reflected_scalar_fields.jett b/tests/native/pending_reflected_scalar_fields.jett new file mode 100644 index 00000000..6a99df68 --- /dev/null +++ b/tests/native/pending_reflected_scalar_fields.jett @@ -0,0 +1,64 @@ +namespace app +struct User: + age: int64 + active: bool + ratio: float64 +enum Message: + number(value: int64) + empty +machine Session: + states: + active(count: int64) + closed + transitions: + active to closed +function check_struct() returns nothing: + User user = User(age: run run 7, active: run true, ratio: run 1.25) + TypeField field = list.get[TypeField](type.fields[User](), 0) handle: + return nothing + int64 copied = type.field_value[User, int64](view user, view field) + trace copied + int64 once = join copied handle error: + return nothing + int64 twice = join once handle error: + return nothing + println(twice) + TypeField active_field = list.get[TypeField](type.fields[User](), 1) handle: + return nothing + bool active = type.field_value[User, bool](view user, view active_field) + trace active + bool joined_active = join active handle error: + return nothing + println(joined_active) + TypeField ratio_field = list.get[TypeField](type.fields[User](), 2) handle: + return nothing + float64 ratio = type.field_value[User, float64](view user, view ratio_field) + trace ratio + float64 joined_ratio = join ratio handle error: + return nothing + println(joined_ratio) +function check_enum() returns nothing: + Message message = Message.number(run 8) + TypeVariant variant = type.variant_value[Message](view message) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + int64 copied = type.variant_field_value[Message, int64](view message, view field) + trace copied + int64 joined = join copied handle error: + return nothing + println(joined) +function check_machine() returns nothing: + Session session = Session(active, run 9) + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + int64 copied = type.machine_field_value[Session, int64](view session, view field) + trace copied + int64 joined = join copied handle error: + return nothing + println(joined) +function main() returns nothing: + check_struct() + check_enum() + check_machine() + return nothing diff --git a/tests/native/pending_reflected_sums.jett b/tests/native/pending_reflected_sums.jett new file mode 100644 index 00000000..593cbf31 --- /dev/null +++ b/tests/native/pending_reflected_sums.jett @@ -0,0 +1,56 @@ +namespace app +struct Parcel: + maybe: optional[string] + outcome: result[string, string] +enum Message: + outcome(value: result[string, string]) +machine Session: + states: + active(maybe: optional[string]) + transitions: + active to active +function check_struct() returns nothing: + Parcel parcel = Parcel(maybe: run some("Ada"), outcome: run ok("Lin")) + TypeField maybe_field = list.get[TypeField](type.fields[Parcel](), 0) handle: + return nothing + optional[string] maybe = type.field_value[Parcel, optional[string]](view parcel, view maybe_field) + trace maybe + optional[string] joined_maybe = join maybe handle error: + return nothing + string text = joined_maybe handle: + default "missing" + println(text) + TypeField outcome_field = list.get[TypeField](type.fields[Parcel](), 1) handle: + return nothing + result[string, string] outcome = type.field_value[Parcel, result[string, string]](view parcel, view outcome_field) + trace outcome + string result_text = join outcome handle error: + default error + println(result_text) +function check_enum() returns nothing: + Message message = Message.outcome(run fail("missing")) + TypeVariant variant = type.variant_value[Message](view message) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + result[string, string] outcome = type.variant_field_value[Message, result[string, string]](view message, view field) + trace outcome + string text = join outcome handle error: + default error + println(text) +function check_machine() returns nothing: + Session session = Session(active, run none) + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + optional[string] maybe = type.machine_field_value[Session, optional[string]](view session, view field) + trace maybe + optional[string] joined = join maybe handle error: + return nothing + string text = joined handle: + default "absent" + println(text) +function main() returns nothing: + check_struct() + check_enum() + check_machine() + return nothing diff --git a/tests/native/pending_reflected_variant_field_failure.jett b/tests/native/pending_reflected_variant_field_failure.jett new file mode 100644 index 00000000..651d6930 --- /dev/null +++ b/tests/native/pending_reflected_variant_field_failure.jett @@ -0,0 +1,12 @@ +namespace app +enum Message: + named(value: string) +function main() returns nothing: + Message message = Message.named("Ada") + TypeVariant variant = type.variant_value[Message](view message) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + TypeField pending_field = run field + string copied = type.variant_field_value[Message, string](view message, view pending_field) + println(copied) + return nothing diff --git a/tests/native/pending_reflection_metadata.jett b/tests/native/pending_reflection_metadata.jett new file mode 100644 index 00000000..723b5329 --- /dev/null +++ b/tests/native/pending_reflection_metadata.jett @@ -0,0 +1,19 @@ +namespace app +struct Record: + name: string +function main() returns nothing: + TypeInfo info = run type.info[list[int64]]() + trace info + TypeInfo ready_info = join info handle error: + return nothing + println(ready_info.type_name, ready_info.kind) + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return nothing + TypeField pending_field = run field + trace pending_field + TypeField ready_field = join pending_field handle error: + return nothing + Record record = Record(name: "Ada") + string copied = type.field_value[Record, string](view record, view ready_field) + println(ready_field.name, copied) + return nothing diff --git a/tests/native/pending_scalar_add_failure.jett b/tests/native/pending_scalar_add_failure.jett new file mode 100644 index 00000000..86456ec6 --- /dev/null +++ b/tests/native/pending_scalar_add_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + int64 pending = run 7 + int64 bad = pending + 2 + trace bad + return nothing diff --git a/tests/native/pending_scalar_aggregates.jett b/tests/native/pending_scalar_aggregates.jett new file mode 100644 index 00000000..8ed0dac5 --- /dev/null +++ b/tests/native/pending_scalar_aggregates.jett @@ -0,0 +1,37 @@ +namespace app +struct Packet: + value: int64 +enum Choice: + number(value: int64) + empty +function main() returns nothing: + Packet record = Packet(value: run 7) + trace record + int64 field = record.value + trace field + int64 field_joined = join field handle error: + return nothing + trace field_joined + Choice choice = Choice.number(run 9) + trace choice + list[int64] numbers = list(run 2, run 3) + trace numbers + int64 first = list.first[int64](view numbers) handle: + return nothing + trace first + int64 first_joined = join first handle error: + return nothing + trace first_joined + list[int64] changed = list.insert_at[int64](numbers, 1, run 4) + trace changed + mutable list[int64] copied = list() + for number in view changed: + copied = list.append[int64](copied, number) + trace copied + list[int64] removed = list.remove_at[int64](clone changed, 0) + trace removed + bool sorted_already = list.is_sorted[int64](view changed) + trace sorted_already + list[int64] sorted = list.sort[int64](clone changed) + trace sorted + return nothing diff --git a/tests/native/pending_scalar_and_failure.jett b/tests/native/pending_scalar_and_failure.jett new file mode 100644 index 00000000..0563af4d --- /dev/null +++ b/tests/native/pending_scalar_and_failure.jett @@ -0,0 +1,9 @@ +namespace app +function right() returns bool: + println("right") + return true +function main() returns nothing: + bool pending = run false + bool bad = pending and right() + trace bad + return nothing diff --git a/tests/native/pending_scalar_and_right_failure.jett b/tests/native/pending_scalar_and_right_failure.jett new file mode 100644 index 00000000..3f44458c --- /dev/null +++ b/tests/native/pending_scalar_and_right_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + bool bad = true and run false + trace bad + return nothing diff --git a/tests/native/pending_scalar_breakpoint_failure.jett b/tests/native/pending_scalar_breakpoint_failure.jett new file mode 100644 index 00000000..56d0ac0c --- /dev/null +++ b/tests/native/pending_scalar_breakpoint_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + bool pending = run true + breakpoint pending + return nothing diff --git a/tests/native/pending_scalar_comparison_failure.jett b/tests/native/pending_scalar_comparison_failure.jett new file mode 100644 index 00000000..b97457bf --- /dev/null +++ b/tests/native/pending_scalar_comparison_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + bool bad = 2 < run 3 + trace bad + return nothing diff --git a/tests/native/pending_scalar_condition_failure.jett b/tests/native/pending_scalar_condition_failure.jett new file mode 100644 index 00000000..fd045703 --- /dev/null +++ b/tests/native/pending_scalar_condition_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bool pending = run true + if pending: + println("wrong branch") + return nothing diff --git a/tests/native/pending_scalar_float_comparison_failure.jett b/tests/native/pending_scalar_float_comparison_failure.jett new file mode 100644 index 00000000..c9f14e55 --- /dev/null +++ b/tests/native/pending_scalar_float_comparison_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + float32 pending = run float32.from_float64(1.25) + bool bad = pending == float32.from_float64(1.25) + trace bad + return nothing diff --git a/tests/native/pending_scalar_formatting.jett b/tests/native/pending_scalar_formatting.jett new file mode 100644 index 00000000..390e48dc --- /dev/null +++ b/tests/native/pending_scalar_formatting.jett @@ -0,0 +1,29 @@ +namespace app +function main() returns nothing: + int64 value = run 123 + string direct = "{value}" + trace direct + string ready = join direct handle error: + println("direct join: {error}") + return nothing + trace ready + string encoded = json.serialize[int64](value) + trace encoded + int8 small = -128 + string signed_text = "{run run small}" + trace signed_text + uint32 wide = 4294967295 + string unsigned_text = "{run wide}" + trace unsigned_text + bool flag = run true + string bool_text = "{flag}" + trace bool_text + float32 narrow = 1.25 + string float_text = "{run narrow}" + trace float_text + string encoded_bool = json.serialize[bool](flag) + trace encoded_bool + nothing empty = run nothing + string encoded_nothing = json.serialize[nothing](empty) + trace encoded_nothing + return nothing diff --git a/tests/native/pending_scalar_negation_failure.jett b/tests/native/pending_scalar_negation_failure.jett new file mode 100644 index 00000000..475c0951 --- /dev/null +++ b/tests/native/pending_scalar_negation_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + int64 pending = run 7 + int64 bad = -pending + trace bad + return nothing diff --git a/tests/native/pending_scalar_nested_add_failure.jett b/tests/native/pending_scalar_nested_add_failure.jett new file mode 100644 index 00000000..a43ad927 --- /dev/null +++ b/tests/native/pending_scalar_nested_add_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + int64 pending = run run 7 + int64 bad = pending + 2 + trace bad + return nothing diff --git a/tests/native/pending_scalar_not_failure.jett b/tests/native/pending_scalar_not_failure.jett new file mode 100644 index 00000000..f714dfe4 --- /dev/null +++ b/tests/native/pending_scalar_not_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + bool pending = run true + bool bad = not pending + trace bad + return nothing diff --git a/tests/native/pending_scalar_or_failure.jett b/tests/native/pending_scalar_or_failure.jett new file mode 100644 index 00000000..5e744f92 --- /dev/null +++ b/tests/native/pending_scalar_or_failure.jett @@ -0,0 +1,9 @@ +namespace app +function right() returns bool: + println("right") + return false +function main() returns nothing: + bool pending = run true + bool bad = pending or right() + trace bad + return nothing diff --git a/tests/native/pending_scalar_or_right_failure.jett b/tests/native/pending_scalar_or_right_failure.jett new file mode 100644 index 00000000..7e83de23 --- /dev/null +++ b/tests/native/pending_scalar_or_right_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + bool bad = false or run true + trace bad + return nothing diff --git a/tests/native/pending_scalar_short_circuit.jett b/tests/native/pending_scalar_short_circuit.jett new file mode 100644 index 00000000..57c507aa --- /dev/null +++ b/tests/native/pending_scalar_short_circuit.jett @@ -0,0 +1,13 @@ +namespace app +function pending_true() returns bool: + println("wrong and branch") + return run true +function pending_false() returns bool: + println("wrong or branch") + return run false +function main() returns nothing: + bool first = false and pending_true() + bool second = true or pending_false() + trace first + trace second + return nothing diff --git a/tests/native/pending_scalar_values.jett b/tests/native/pending_scalar_values.jett new file mode 100644 index 00000000..d6847193 --- /dev/null +++ b/tests/native/pending_scalar_values.jett @@ -0,0 +1,57 @@ +namespace app +function echo_int(value: int64) returns int64: + return value +function capture_task() returns function() returns int64: + int64 pending = run 23 + return function() returns int64: return pending +function main() returns nothing: + int64 pending = run 17 + trace pending + int64 nested = run echo_int(pending) + trace nested + int64 inner = join nested handle error: + return nothing + trace inner + int64 joined = join inner handle error: + return nothing + trace joined + float64 decimal = run 2.5 + trace decimal + float64 resolved = join decimal handle error: + return nothing + trace resolved + bool flag = run true + trace flag + bool ready = join flag handle error: + return nothing + trace ready + uint64 wide = run 18446744073709551615 + trace wide + uint64 wide_joined = join wide handle error: + return nothing + trace wide_joined + float32 narrow = run float32.from_float64(1.25) + trace narrow + float32 narrow_joined = join narrow handle error: + return nothing + trace narrow_joined + function(int64) returns int64 relay = echo_int + int64 indirect = relay(run 9) + trace indirect + int64 indirect_joined = join indirect handle error: + return nothing + trace indirect_joined + result[int64, string] boxed = ok(run 31) + trace boxed + result[int64, string] copied = clone boxed + trace copied + int64 extracted = boxed handle error: + return nothing + trace extracted + function() returns int64 captured = capture_task() + int64 captured_pending = captured() + trace captured_pending + int64 captured_joined = join captured_pending handle error: + return nothing + trace captured_joined + return nothing diff --git a/tests/native/pending_secret_compare_bytes_first_failure.jett b/tests/native/pending_secret_compare_bytes_first_failure.jett new file mode 100644 index 00000000..f75a6ddb --- /dev/null +++ b/tests/native/pending_secret_compare_bytes_first_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + secret[bytes] first = run bytes.from_string("a") + secret[bytes] second = bytes.from_string("a") + bool equal = secret.compare(first, second) + trace equal + return nothing diff --git a/tests/native/pending_secret_compare_bytes_second_failure.jett b/tests/native/pending_secret_compare_bytes_second_failure.jett new file mode 100644 index 00000000..a10d4556 --- /dev/null +++ b/tests/native/pending_secret_compare_bytes_second_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + secret[bytes] first = bytes.from_string("a") + secret[bytes] second = run bytes.from_string("a") + bool equal = secret.compare(first, second) + trace equal + return nothing diff --git a/tests/native/pending_secret_compare_string_first_failure.jett b/tests/native/pending_secret_compare_string_first_failure.jett new file mode 100644 index 00000000..7a5c18b9 --- /dev/null +++ b/tests/native/pending_secret_compare_string_first_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + secret[string] first = run "a" + secret[string] second = "a" + bool equal = secret.compare(first, second) + trace equal + return nothing diff --git a/tests/native/pending_secret_compare_string_second_failure.jett b/tests/native/pending_secret_compare_string_second_failure.jett new file mode 100644 index 00000000..15d4b4da --- /dev/null +++ b/tests/native/pending_secret_compare_string_second_failure.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + secret[string] first = "a" + secret[string] second = run "a" + bool equal = secret.compare(first, second) + trace equal + return nothing diff --git a/tests/native/pending_secret_redact.jett b/tests/native/pending_secret_redact.jett new file mode 100644 index 00000000..cacbe664 --- /dev/null +++ b/tests/native/pending_secret_redact.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + secret[string] value = run "hidden" + println(secret.redact(value)) + return nothing diff --git a/tests/native/pending_secret_task_join.jett b/tests/native/pending_secret_task_join.jett new file mode 100644 index 00000000..08f6a0be --- /dev/null +++ b/tests/native/pending_secret_task_join.jett @@ -0,0 +1,20 @@ +namespace app +function main() returns nothing: + secret[string] text = "agent" + secret[string] pending_text = run text + secret[string] joined_text = join pending_text handle error: + return nothing + secret[string] expected_text = "agent" + println(secret.compare(joined_text, expected_text)) + secret[string] contextual_text = run "agent" + secret[string] joined_contextual = join contextual_text handle error: + return nothing + secret[string] second_expected = "agent" + println(secret.compare(joined_contextual, second_expected)) + secret[bytes] data = bytes.from_string("key") + secret[bytes] pending_data = run data + secret[bytes] joined_data = join pending_data handle error: + return nothing + secret[bytes] expected_data = bytes.from_string("key") + println(secret.compare(joined_data, expected_data)) + return nothing diff --git a/tests/native/pending_set_add_failure.jett b/tests/native/pending_set_add_failure.jett new file mode 100644 index 00000000..039caf6c --- /dev/null +++ b/tests/native/pending_set_add_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + set[int64] values = run set.new[int64]() + set[int64] changed = set.add[int64](values, 2) + trace changed + return nothing diff --git a/tests/native/pending_set_contains_failure.jett b/tests/native/pending_set_contains_failure.jett new file mode 100644 index 00000000..a7478f13 --- /dev/null +++ b/tests/native/pending_set_contains_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + set[int64] values = run set.new[int64]() + bool contains = set.contains[int64](view values, 2) + trace contains + return nothing diff --git a/tests/native/pending_set_elements.jett b/tests/native/pending_set_elements.jett new file mode 100644 index 00000000..a8da9628 --- /dev/null +++ b/tests/native/pending_set_elements.jett @@ -0,0 +1,29 @@ +namespace app +function main() returns nothing: + set[int64] numbers = set.add[int64](set.new[int64](), run 4) + trace numbers + bool has_pending = set.contains[int64](view numbers, run 4) + bool has_plain = set.contains[int64](view numbers, 4) + trace has_pending + trace has_plain + set[int64] copied = clone numbers + trace copied + set[int64] kept = set.remove[int64](clone numbers, 4) + trace kept + set[int64] both = set.add[int64](clone numbers, 4) + trace both + set[int64] just_plain = set.remove[int64](both, run 4) + trace just_plain + mutable list[int64] iterated = list() + for item in copied: + iterated = list.append[int64](iterated, item) + trace iterated + set[string] words = set.add[string](set.new[string](), run "north") + trace words + bool has_pending_word = set.contains[string](view words, run "north") + bool has_plain_word = set.contains[string](view words, "north") + trace has_pending_word + trace has_plain_word + set[string] both_words = set.add[string](words, "north") + trace both_words + return nothing diff --git a/tests/native/pending_set_iteration_failure.jett b/tests/native/pending_set_iteration_failure.jett new file mode 100644 index 00000000..64920339 --- /dev/null +++ b/tests/native/pending_set_iteration_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + set[int64] values = run set.add[int64](set.new[int64](), 2) + for item in values: + trace item + return nothing diff --git a/tests/native/pending_set_length_failure.jett b/tests/native/pending_set_length_failure.jett new file mode 100644 index 00000000..eed44af2 --- /dev/null +++ b/tests/native/pending_set_length_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + set[int64] values = run set.new[int64]() + int64 length = set.length[int64](view values) + trace length + return nothing diff --git a/tests/native/pending_set_map_values.jett b/tests/native/pending_set_map_values.jett new file mode 100644 index 00000000..0cfb1893 --- /dev/null +++ b/tests/native/pending_set_map_values.jett @@ -0,0 +1,54 @@ +namespace app +enum Collection: + tags(value: set[string]) + entries(value: map[string, string]) + empty +function pending_set() returns set[string]: + mutable set[string] value = set.new[string]() + value = set.add[string](value, "live") + return run value +function pending_map() returns map[string, string]: + return run map("a": "Ada") +function inspect_set() returns nothing: + mutable set[string] plain = set.new[string]() + plain = set.add[string](plain, "live") + set[string] pending = run clone plain + set[string] nested = run pending_set() + trace plain + trace pending + trace nested + println(Collection.tags(clone pending) == Collection.tags(clone pending)) + println(Collection.tags(clone pending) == Collection.tags(clone plain)) + set[string] first = join pending handle error: + default set.new[string]() + set[string] inner = join nested handle error: + default set.new[string]() + trace first + trace inner + set[string] last = join inner handle error: + default set.new[string]() + trace last + return nothing +function inspect_map() returns nothing: + map[string, string] plain = map("a": "Ada") + map[string, string] pending = run clone plain + map[string, string] nested = run pending_map() + trace plain + trace pending + trace nested + println(Collection.entries(clone pending) == Collection.entries(clone pending)) + println(Collection.entries(clone pending) == Collection.entries(clone plain)) + map[string, string] first = join pending handle error: + default map("unexpected": "value") + map[string, string] inner = join nested handle error: + default map("unexpected": "value") + trace first + trace inner + map[string, string] last = join inner handle error: + default map("unexpected": "value") + trace last + return nothing +function main() returns nothing: + inspect_set() + inspect_map() + return nothing diff --git a/tests/native/pending_set_remove_failure.jett b/tests/native/pending_set_remove_failure.jett new file mode 100644 index 00000000..e34e6339 --- /dev/null +++ b/tests/native/pending_set_remove_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + set[int64] values = run set.new[int64]() + set[int64] changed = set.remove[int64](values, 2) + trace changed + return nothing diff --git a/tests/native/pending_stdout_write.jett b/tests/native/pending_stdout_write.jett new file mode 100644 index 00000000..b0aab4ef --- /dev/null +++ b/tests/native/pending_stdout_write.jett @@ -0,0 +1,5 @@ +namespace app +function main(stdout: Stdout) returns nothing: + Stdout pending = run stdout + Stdout.write(view pending, "ready") + return nothing diff --git a/tests/native/pending_stdout_write_text.jett b/tests/native/pending_stdout_write_text.jett new file mode 100644 index 00000000..74417e63 --- /dev/null +++ b/tests/native/pending_stdout_write_text.jett @@ -0,0 +1,5 @@ +namespace app +function main(stdout: Stdout) returns nothing: + string message = run "ready" + Stdout.write(view stdout, message) + return nothing diff --git a/tests/native/pending_string_equality_failure.jett b/tests/native/pending_string_equality_failure.jett new file mode 100644 index 00000000..89bd8aaa --- /dev/null +++ b/tests/native/pending_string_equality_failure.jett @@ -0,0 +1,7 @@ +namespace app +function identity(value: string) returns string: + return value +function main() returns nothing: + string pending = run identity("Ada") + println(pending == "Ada") + return nothing diff --git a/tests/native/pending_string_from_bool_failure.jett b/tests/native/pending_string_from_bool_failure.jett new file mode 100644 index 00000000..d8d76169 --- /dev/null +++ b/tests/native/pending_string_from_bool_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string value = string.from_bool(run true) + trace value + return nothing diff --git a/tests/native/pending_string_from_float64_failure.jett b/tests/native/pending_string_from_float64_failure.jett new file mode 100644 index 00000000..21086482 --- /dev/null +++ b/tests/native/pending_string_from_float64_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string value = string.from_float64(run 1.5) + trace value + return nothing diff --git a/tests/native/pending_string_from_int64_failure.jett b/tests/native/pending_string_from_int64_failure.jett new file mode 100644 index 00000000..0d6bb424 --- /dev/null +++ b/tests/native/pending_string_from_int64_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string value = string.from_int64(run 3) + trace value + return nothing diff --git a/tests/native/pending_string_from_uint64_failure.jett b/tests/native/pending_string_from_uint64_failure.jett new file mode 100644 index 00000000..c8cb65ec --- /dev/null +++ b/tests/native/pending_string_from_uint64_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + uint64 wide = run 18446744073709551615 + string value = string.from_uint64(wide) + trace value + return nothing diff --git a/tests/native/pending_string_inequality_failure.jett b/tests/native/pending_string_inequality_failure.jett new file mode 100644 index 00000000..967f90ba --- /dev/null +++ b/tests/native/pending_string_inequality_failure.jett @@ -0,0 +1,7 @@ +namespace app +function identity(value: string) returns string: + return value +function main() returns nothing: + string pending = run identity("Ada") + println(pending != "Ada") + return nothing diff --git a/tests/native/pending_string_iteration_failure.jett b/tests/native/pending_string_iteration_failure.jett new file mode 100644 index 00000000..68b0706f --- /dev/null +++ b/tests/native/pending_string_iteration_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string value = run "ab" + for character in value: + trace character + return nothing diff --git a/tests/native/pending_string_join_element_failure.jett b/tests/native/pending_string_join_element_failure.jett new file mode 100644 index 00000000..a8924bb6 --- /dev/null +++ b/tests/native/pending_string_join_element_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[string] parts = list(run "a", "b") + string joined = string.join(parts, ",") + trace joined + return nothing diff --git a/tests/native/pending_string_join_list_failure.jett b/tests/native/pending_string_join_list_failure.jett new file mode 100644 index 00000000..7be34f5e --- /dev/null +++ b/tests/native/pending_string_join_list_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + list[string] parts = run list("a", "b") + string joined = string.join(parts, ",") + trace joined + return nothing diff --git a/tests/native/pending_string_join_separator_failure.jett b/tests/native/pending_string_join_separator_failure.jett new file mode 100644 index 00000000..780dc62e --- /dev/null +++ b/tests/native/pending_string_join_separator_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string separator = run "," + string joined = string.join(list("a", "b"), separator) + trace joined + return nothing diff --git a/tests/native/pending_string_repeat_container_failure.jett b/tests/native/pending_string_repeat_container_failure.jett new file mode 100644 index 00000000..de1d16ec --- /dev/null +++ b/tests/native/pending_string_repeat_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string value = run "ab" + string repeated = string.repeat(value, 2) + trace repeated + return nothing diff --git a/tests/native/pending_string_repeat_count_failure.jett b/tests/native/pending_string_repeat_count_failure.jett new file mode 100644 index 00000000..ce7abc63 --- /dev/null +++ b/tests/native/pending_string_repeat_count_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string value = string.repeat("ab", run 2) + trace value + return nothing diff --git a/tests/native/pending_string_replace_third_failure.jett b/tests/native/pending_string_replace_third_failure.jett new file mode 100644 index 00000000..9cf04bc6 --- /dev/null +++ b/tests/native/pending_string_replace_third_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string replacement = run "x" + string changed = string.replace("abc", "b", replacement) + trace changed + return nothing diff --git a/tests/native/pending_string_slice_container_failure.jett b/tests/native/pending_string_slice_container_failure.jett new file mode 100644 index 00000000..87ab3c87 --- /dev/null +++ b/tests/native/pending_string_slice_container_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string value = run "abcd" + string part = string.slice(value, 0, 2) + trace part + return nothing diff --git a/tests/native/pending_string_slice_end_failure.jett b/tests/native/pending_string_slice_end_failure.jett new file mode 100644 index 00000000..021c9ca1 --- /dev/null +++ b/tests/native/pending_string_slice_end_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string part = string.slice("abcd", 0, run 2) + trace part + return nothing diff --git a/tests/native/pending_string_slice_start_failure.jett b/tests/native/pending_string_slice_start_failure.jett new file mode 100644 index 00000000..6d847773 --- /dev/null +++ b/tests/native/pending_string_slice_start_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + string part = string.slice("abcd", run 0, 2) + trace part + return nothing diff --git a/tests/native/pending_string_split_delimiter_failure.jett b/tests/native/pending_string_split_delimiter_failure.jett new file mode 100644 index 00000000..b45cb452 --- /dev/null +++ b/tests/native/pending_string_split_delimiter_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string delimiter = run "," + list[string] parts = string.split("a,b", delimiter) + trace parts + return nothing diff --git a/tests/native/pending_string_upper_failure.jett b/tests/native/pending_string_upper_failure.jett new file mode 100644 index 00000000..efd3da5b --- /dev/null +++ b/tests/native/pending_string_upper_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + string value = run "Ab" + string changed = string.upper(value) + trace changed + return nothing diff --git a/tests/native/pending_string_values.jett b/tests/native/pending_string_values.jett new file mode 100644 index 00000000..8eb35c8d --- /dev/null +++ b/tests/native/pending_string_values.jett @@ -0,0 +1,31 @@ +namespace app +enum TaskLabel: + named(value: string) + empty +function identity(value: string) returns string: + return value +function make_pending() returns string: + return run identity("Lin") +function main() returns nothing: + string plain = "Ada" + string pending = run clone plain + string nested = run make_pending() + list[string] tasks = list(clone pending, clone nested) + trace plain + trace pending + trace nested + trace tasks + println(TaskLabel.named(clone pending) == TaskLabel.named(clone pending)) + println(TaskLabel.named(clone pending) == TaskLabel.named(clone plain)) + println(plain, pending, nested) + println("{pending}:{nested}") + string first = join pending handle error: + default error + string inner = join nested handle error: + default error + trace first + trace inner + string last = join inner handle error: + default error + println(first, last) + return nothing diff --git a/tests/native/pending_struct_field_owner_failure.jett b/tests/native/pending_struct_field_owner_failure.jett new file mode 100644 index 00000000..2ff6ee9a --- /dev/null +++ b/tests/native/pending_struct_field_owner_failure.jett @@ -0,0 +1,10 @@ +namespace app +struct Record: + name: string +function main() returns nothing: + Record record = run Record(name: "Ada") + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return nothing + string copied = type.field_value[Record, string](view record, view field) + println(copied) + return nothing diff --git a/tests/native/pending_sum_handle_failure.jett b/tests/native/pending_sum_handle_failure.jett new file mode 100644 index 00000000..fb77ded6 --- /dev/null +++ b/tests/native/pending_sum_handle_failure.jett @@ -0,0 +1,12 @@ +namespace app +struct Record: + label: string +function main(stdout: Stdout) returns nothing: + bytes owned = bytes.from_string("live owner") + optional[string] source = some("payload") + optional[string] first = run source + optional[string] pending = run first + Stdout.write(view stdout, "before;") + string found = pending handle: + return Stdout.write(view stdout, "unexpected handler") + Stdout.write(view stdout, "unexpected extraction") diff --git a/tests/native/pending_sum_values.jett b/tests/native/pending_sum_values.jett new file mode 100644 index 00000000..ec872683 --- /dev/null +++ b/tests/native/pending_sum_values.jett @@ -0,0 +1,41 @@ +namespace app +enum Envelope: + value(payload: optional[string]) +function check_optional() returns nothing: + optional[string] plain = some("Ada") + optional[string] absent = none + optional[string] pending = run clone plain + optional[string] pending_absent = run clone absent + optional[string] nested = run clone pending + trace plain + trace absent + trace pending + trace pending_absent + trace nested + Envelope plain_envelope = Envelope.value(clone plain) + Envelope pending_envelope = Envelope.value(clone pending) + println(plain_envelope == pending_envelope) + println(pending_envelope == Envelope.value(clone pending)) + return nothing +function check_result() returns nothing: + result[string, string] plain = ok("Ada") + result[string, string] failed = fail("missing") + result[string, string] pending = run clone plain + result[string, string] pending_failure = run clone failed + result[string, string] nested = run clone pending + trace plain + trace failed + trace pending + trace pending_failure + trace nested + string first = join pending handle error: + default error + string failed_value = join pending_failure handle error: + default error + println(first) + println(failed_value) + return nothing +function main() returns nothing: + check_optional() + check_result() + return nothing diff --git a/tests/native/pending_type_arg_failure.jett b/tests/native/pending_type_arg_failure.jett new file mode 100644 index 00000000..738a988a --- /dev/null +++ b/tests/native/pending_type_arg_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + int64 index = run 0 + TypeInfo argument = type.arg[list[int64]](index) + println(argument.type_name) + return nothing diff --git a/tests/native/pending_type_arg_nested_failure.jett b/tests/native/pending_type_arg_nested_failure.jett new file mode 100644 index 00000000..fcb5c577 --- /dev/null +++ b/tests/native/pending_type_arg_nested_failure.jett @@ -0,0 +1,6 @@ +namespace app +function main() returns nothing: + int64 index = run run 0 + TypeInfo argument = type.arg[map[string, int64]](index) + println(argument.type_name) + return nothing diff --git a/tests/native/pending_type_construction.jett b/tests/native/pending_type_construction.jett new file mode 100644 index 00000000..553855a4 --- /dev/null +++ b/tests/native/pending_type_construction.jett @@ -0,0 +1,22 @@ +namespace app +struct User: + name: string +function main() returns nothing: + TypeConstruction pending = run type.construct_start[User]() + trace pending + TypeConstruction nested = run pending + trace nested + TypeConstruction inner = join nested handle error: + default type.construct_start[User]() + trace inner + TypeConstruction builder = join inner handle error: + default type.construct_start[User]() + trace builder + TypeField field = list.get[TypeField](type.fields[User](), 0) handle: + return nothing + TypeConstruction filled = type.construct_put[User, string](builder, view field, "Ada") handle error: + return nothing + User user = type.construct_finish[User](filled) handle error: + return nothing + println(user.name) + return nothing diff --git a/tests/native/pending_type_construction_invalid_use.jett b/tests/native/pending_type_construction_invalid_use.jett new file mode 100644 index 00000000..23ddc5f2 --- /dev/null +++ b/tests/native/pending_type_construction_invalid_use.jett @@ -0,0 +1,16 @@ +namespace app +struct User: + name: string +function finish_pending() returns string: + TypeConstruction builder = type.construct_start[User]() + TypeField field = list.get[TypeField](type.fields[User](), 0) handle: + return "missing field" + TypeConstruction filled = type.construct_put[User, string](builder, view field, "Ada") handle error: + return error + TypeConstruction pending = run filled + User user = type.construct_finish[User](pending) handle error: + return error + return user.name +function main() returns nothing: + println(finish_pending()) + return nothing diff --git a/tests/native/pending_type_construction_put_failure.jett b/tests/native/pending_type_construction_put_failure.jett new file mode 100644 index 00000000..ac2141dd --- /dev/null +++ b/tests/native/pending_type_construction_put_failure.jett @@ -0,0 +1,13 @@ +namespace app +struct User: + name: string +function main() returns nothing: + TypeConstruction pending = run type.construct_start[User]() + TypeField field = list.get[TypeField](type.fields[User](), 0) handle: + return nothing + TypeConstruction filled = type.construct_put[User, string](pending, view field, "Ada") handle error: + return nothing + User user = type.construct_finish[User](filled) handle error: + return nothing + println(user.name) + return nothing diff --git a/tests/native/pending_uint64_from_string_failure.jett b/tests/native/pending_uint64_from_string_failure.jett new file mode 100644 index 00000000..b1d0ad15 --- /dev/null +++ b/tests/native/pending_uint64_from_string_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + result[uint64, string] value = uint64.from_string(run "3") + trace value + return nothing diff --git a/tests/native/pending_variant_both_metadata_failure.jett b/tests/native/pending_variant_both_metadata_failure.jett new file mode 100644 index 00000000..dc2c6582 --- /dev/null +++ b/tests/native/pending_variant_both_metadata_failure.jett @@ -0,0 +1,13 @@ +namespace app +enum Message: + named(value: string) +function main() returns nothing: + Message message = run Message.named("Ada") + TypeVariant variant = list.get[TypeVariant](type.variants[Message](), 0) handle: + return nothing + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + TypeField pending_field = run field + string copied = type.variant_field_value[Message, string](view message, view pending_field) + println(copied) + return nothing diff --git a/tests/native/pending_variant_field_owner_failure.jett b/tests/native/pending_variant_field_owner_failure.jett new file mode 100644 index 00000000..ba3d64b2 --- /dev/null +++ b/tests/native/pending_variant_field_owner_failure.jett @@ -0,0 +1,12 @@ +namespace app +enum Message: + named(value: string) +function main() returns nothing: + TypeVariant variant = list.get[TypeVariant](type.variants[Message](), 0) handle: + return nothing + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + Message pending_message = run Message.named("Ada") + string copied = type.variant_field_value[Message, string](view pending_message, view field) + println(copied) + return nothing diff --git a/tests/native/pending_variant_foreign_field_precedence.jett b/tests/native/pending_variant_foreign_field_precedence.jett new file mode 100644 index 00000000..deee384d --- /dev/null +++ b/tests/native/pending_variant_foreign_field_precedence.jett @@ -0,0 +1,13 @@ +namespace app +enum Message: + first(value: int64) + second(value: int64) +function main() returns nothing: + Message message = run Message.first(7) + TypeVariant other = list.get[TypeVariant](type.variants[Message](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 copied = type.variant_field_value[Message, int64](view message, view foreign) + println(copied) + return nothing diff --git a/tests/native/pending_variant_value_failure.jett b/tests/native/pending_variant_value_failure.jett new file mode 100644 index 00000000..820d01fd --- /dev/null +++ b/tests/native/pending_variant_value_failure.jett @@ -0,0 +1,8 @@ +namespace app +enum Message: + named(value: string) +function main() returns nothing: + Message message = run Message.named("Ada") + TypeVariant variant = type.variant_value[Message](view message) + println(variant.name) + return nothing diff --git a/tests/native/projected_machine_sequences.jett b/tests/native/projected_machine_sequences.jett new file mode 100644 index 00000000..8ab0f540 --- /dev/null +++ b/tests/native/projected_machine_sequences.jett @@ -0,0 +1,29 @@ +namespace app +machine Basket: + states: + ready(items: list[int64], tags: set[int64], scores: map[string, int64]) +function borrowed_total(view basket: Basket at ready) returns int64: + mutable int64 total = 0 + for item in view basket.items: + total = total + item + for tag in view basket.tags: + total = total + tag + for key, score in view basket.scores: + total = total + score + return total +function main() returns nothing: + mutable set[int64] tags = set.new[int64]() + tags = set.add[int64](tags, 3) + tags = set.add[int64](tags, 9) + Basket at ready basket = Basket(ready, list(2, 4), tags, map("a": 5, "b": 7)) + mutable int64 total = 0 + for item in view basket.items: + total = total + item + for tag in view basket.tags: + total = total + tag + for key, score in view basket.scores: + println(key, score) + total = total + score + println(total, list.length[int64](view basket.items), set.length[int64](view basket.tags), map.length[string, int64](view basket.scores)) + println(borrowed_total(view basket), borrowed_total(view basket)) + return nothing diff --git a/tests/native/projected_nested_collections.jett b/tests/native/projected_nested_collections.jett new file mode 100644 index 00000000..24f87e30 --- /dev/null +++ b/tests/native/projected_nested_collections.jett @@ -0,0 +1,26 @@ +namespace app +struct Item: + label: string + score: int64 +struct Store: + batches: map[string, list[Item]] + grid: list[list[Item]] +function inspect(view store: Store) returns int64: + mutable int64 total = 0 + for key, batch in view store.batches: + println(key) + for item in view batch: + println(item.label) + total = total + item.score + for row in view store.grid: + for item in view row: + total = total + item.score + return total + map.length[string, list[Item]](view store.batches) + list.length[list[Item]](view store.grid) +function main() returns nothing: + list[Item] first = list(Item(label: "Ada", score: 2), Item(label: "Lin", score: 3)) + list[Item] second = list(Item(label: "Mia", score: 5)) + map[string, list[Item]] batches = map("first": first, "second": second) + list[list[Item]] grid = list(list(Item(label: "A", score: 7)), list(Item(label: "B", score: 11))) + Store store = Store(batches: batches, grid: grid) + println(inspect(view store), inspect(view store)) + return nothing diff --git a/tests/native/projected_read_controls.jett b/tests/native/projected_read_controls.jett new file mode 100644 index 00000000..7ac5c48c --- /dev/null +++ b/tests/native/projected_read_controls.jett @@ -0,0 +1,60 @@ +namespace app +struct Inner: + value: int64 +struct Record: + inner: Inner + label: string +enum Choice: + wrapped(record: Record) +function make() returns Record: + return Record(inner: Inner(value: 1), label: "before") +function read(view record: Record) returns int64: + return record.inner.value +function copy_and_replace(view source: Record) returns Record: + mutable Record copied = clone source + copied = Record(inner: Inner(value: read(view copied) + 1), label: copied.label) + return copied +function copied_field_reads(view source: Record) returns int64: + mutable Inner direct = source.inner + direct = Inner(value: direct.value + 2) + mutable Inner viewed =(view source).inner + viewed = Inner(value: viewed.value + 3) + return direct.value + viewed.value +function loop_read(view records: list[Record]) returns int64: + mutable int64 total = 0 + for record in view records: + total = total + record.inner.value + return total +function match_read(view choice: Choice) returns int64: + match choice: + wrapped(record): + return record.inner.value +function temporary_read() returns int64: + return make().inner.value +function main(stdout: Stdout) returns nothing: + Record original = make() + Record copied = copy_and_replace(view original) + Stdout.write(view stdout, "original:{read(view original)}\ncopied:{read(view copied)}\noriginal:{read(view original)}\n") + list[Record] records = list(clone original, clone copied) + Choice choice = Choice.wrapped(clone copied) + Stdout.write(view stdout, "loop:{loop_read(view records)}\nmatch:{match_read(view choice)}:{match_read(view choice)}\ntemporary:{temporary_read()}\n") + Stdout.write(view stdout, "fields:{copied_field_reads(view original)}:{read(view original)}\n") +verify read_and_copy: + Record original = make() + Record copied = copy_and_replace(view original) + assert read(view original) == 1 + assert read(view copied) == 2 + list[Record] records = list(clone original, clone copied) + Choice choice = Choice.wrapped(clone copied) + assert loop_read(view records) == 3 + assert match_read(view choice) == 2 + assert temporary_read() == 1 + assert copied_field_reads(view original) == 7 + assert read(view original) == 1 +property preserves_source: + given record: Record + int64 before = read(view record) + Record copied = copy_and_replace(view record) + assert read(view record) == before + assert read(view copied) == before + 1 + assert copied_field_reads(view record) == before * 2 + 5 diff --git a/tests/native/projected_sequences.jett b/tests/native/projected_sequences.jett new file mode 100644 index 00000000..f655b158 --- /dev/null +++ b/tests/native/projected_sequences.jett @@ -0,0 +1,33 @@ +namespace app +struct Collections: + items: list[int64] + tags: set[int64] + scores: map[string, int64] + label: string +struct Nested: + value: Collections +function main() returns nothing: + mutable set[int64] tags = set.new[int64]() + tags = set.add[int64](tags, 3) + tags = set.add[int64](tags, 9) + Collections value = Collections(items: list(2, 4), tags: tags, scores: map("a": 5, "b": 7), label: "alive") + mutable int64 total = 0 + for item in value.items: + total = total + item + for tag in value.tags: + total = total + tag + for key, score in value.scores: + println(key, score) + total = total + score + println(total, value.label, list.length[int64](view value.items), set.length[int64](view value.tags), map.length[string, int64](view value.scores)) + mutable list[int64] copied_items = value.items + copied_items = list.append[int64](copied_items, 6) + mutable set[int64] copied_tags = value.tags + copied_tags = set.add[int64](copied_tags, 12) + mutable map[string, int64] copied_scores = value.scores + copied_scores = map.insert[string, int64](copied_scores, "c", 11) + println(list.length[int64](view copied_items), set.length[int64](view copied_tags), map.length[string, int64](view copied_scores)) + println(list.length[int64](view value.items), set.length[int64](view value.tags), map.length[string, int64](view value.scores)) + Nested nested = Nested(value: value) + list[int64] nested_items = nested.value.items + println(list.length[int64](view nested_items), nested.value.label) diff --git a/tests/native/projected_string_iteration.jett b/tests/native/projected_string_iteration.jett new file mode 100644 index 00000000..5c592c56 --- /dev/null +++ b/tests/native/projected_string_iteration.jett @@ -0,0 +1,12 @@ +namespace app +struct Holder: + text: string +function count(view holder: Holder) returns int64: + mutable int64 total = 0 + for ch in holder.text: + total = total + 1 + return total +function main() returns nothing: + Holder holder = Holder(text: "A🚀é") + println(count(view holder), count(view holder)) + return nothing diff --git a/tests/native/projected_sum_handles.jett b/tests/native/projected_sum_handles.jett new file mode 100644 index 00000000..1ae33c34 --- /dev/null +++ b/tests/native/projected_sum_handles.jett @@ -0,0 +1,18 @@ +namespace app +struct Slot: + payload: optional[list[int64]] + outcome: result[int64, string] +function inspect(view slot: Slot) returns int64: + list[int64] first = slot.payload handle: + default list.new[int64]() + list[int64] second = slot.payload handle: + default list.new[int64]() + int64 left = slot.outcome handle error: + default 0 + int64 right = slot.outcome handle error: + default 0 + return list.length[int64](view first) + list.length[int64](view second) + left + right +function main() returns nothing: + Slot slot = Slot(payload: some(list(2, 3)), outcome: ok(7)) + println(inspect(view slot), inspect(view slot)) + return nothing diff --git a/tests/native/property_generic_owners.jett b/tests/native/property_generic_owners.jett new file mode 100644 index 00000000..1a5e38c9 --- /dev/null +++ b/tests/native/property_generic_owners.jett @@ -0,0 +1,30 @@ +namespace app +interface Named: + function name(view self: Named) returns string +struct Boxed[T]: + value: T +type Tiny = int8 +implement Named for Boxed[Tiny]: + function name(view self: Boxed[Tiny]) returns string: + return "narrow" +implement Named for Boxed[string]: + function name(view self: Boxed[string]) returns string: + return "text" +function named(values: list[Named], expected: string) returns bool: + for value in values: + if Named.name(view value) != expected: + return false + return true +function count(values: list[Boxed[Tiny]]) returns int64: + mutable int64 total = 0 + for value in values: + total = total + 1 + return total +verify constructed_owner: + assert named(list(Boxed[Tiny](value: 7)), "narrow") +property owners: + given records: list[Boxed[Tiny]] + given texts: list[Boxed[string]] + assert named(clone records, "narrow") "narrow owner sentinel" + assert named(texts, "text") "text owner sentinel" + assert count(records) <= 3 "length sentinel" diff --git a/tests/native/refined_builder_base.jett b/tests/native/refined_builder_base.jett new file mode 100644 index 00000000..800b2706 --- /dev/null +++ b/tests/native/refined_builder_base.jett @@ -0,0 +1,29 @@ +namespace app +type NonEmpty = string where value != "" +type Long = NonEmpty where string.char_count(value) >= 3 +struct User: + name: NonEmpty + alias: Long + label: string +function build_user(raw: string, alias: string) returns string: + mutable TypeConstruction builder = type.construct_start[User]() + TypeField name_field = list.get[TypeField](type.fields[User](), 0) handle: + return "missing field" + builder = type.construct_put[User, string](builder, view name_field, raw) handle error: + return error + TypeField alias_field = list.get[TypeField](type.fields[User](), 1) handle: + return "missing field" + builder = type.construct_put[User, string](builder, view alias_field, alias) handle error: + return error + TypeField label_field = list.get[TypeField](type.fields[User](), 2) handle: + return "missing field" + builder = type.construct_put[User, string](builder, view label_field, "account") handle error: + return error + User built = type.construct_finish[User](builder) handle error: + return error + return coarsen built.name +function main() returns nothing: + println(build_user("Ada", "Agent")) + println(build_user("", "Agent")) + println(build_user("Ada", "Al")) + return nothing diff --git a/tests/native/refined_enum_builder_base.jett b/tests/native/refined_enum_builder_base.jett new file mode 100644 index 00000000..d2f57780 --- /dev/null +++ b/tests/native/refined_enum_builder_base.jett @@ -0,0 +1,50 @@ +namespace app +type NonEmpty = string where value != "" +type Long = NonEmpty where string.char_count(value) >= 3 +enum Event: + idle + named(name: NonEmpty, alias: Long, note: string) +function create_event(raw: string, raw_alias: string) returns result[Event, string]: + TypeVariant variant = list.get[TypeVariant](type.variants[Event](), 1) handle: + return fail("missing variant") + mutable TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return fail(error) + TypeField name_field = list.get[TypeField](variant.fields, 0) handle: + return fail("missing field") + builder = type.construct_put[Event, string](builder, view name_field, raw) handle error: + return fail(error) + TypeField alias_field = list.get[TypeField](variant.fields, 1) handle: + return fail("missing field") + builder = type.construct_put[Event, string](builder, view alias_field, raw_alias) handle error: + return fail(error) + TypeField note_field = list.get[TypeField](variant.fields, 2) handle: + return fail("missing field") + builder = type.construct_put[Event, string](builder, view note_field, "ready") handle error: + return fail(error) + return type.construct_finish[Event](builder) +function build_event(raw: string, raw_alias: string) returns string: + Event event = create_event(raw, raw_alias) handle error: + return error + match event: + idle: + return "idle" + named(name, alias, note): + return "{coarsen name}:{coarsen alias}:{note}" +function build_idle() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Event](), 0) handle: + return "missing variant" + TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return error + Event event = type.construct_finish[Event](builder) handle error: + return error + match event: + idle: + return "idle" + named(name, alias, note): + return "unexpected named" +function main() returns nothing: + println(build_idle()) + println(build_event("Ada", "Agent")) + println(build_event("", "Agent")) + println(build_event("Ada", "Al")) + return nothing diff --git a/tests/native/refined_machine_builder_base.jett b/tests/native/refined_machine_builder_base.jett new file mode 100644 index 00000000..43ddd02d --- /dev/null +++ b/tests/native/refined_machine_builder_base.jett @@ -0,0 +1,94 @@ +namespace app +type NonEmpty = string where value != "" +type Long = NonEmpty where string.char_count(value) >= 3 +type Positive = int64 where value > 0 +machine Session: + states: + guest + active(name: app.NonEmpty, alias: app.Long, note: string) + transitions: + guest to active +machine Simple: + states: + empty + filled(code: app.Positive) + transitions: + empty to filled +function create_bare(raw: string, raw_alias: string) returns result[Session, string]: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return fail("missing state") + mutable TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return fail(error) + TypeField name_field = list.get[TypeField](state.fields, 0) handle: + return fail("missing field") + builder = type.construct_put[Session, string](builder, view name_field, raw) handle error: + return fail(error) + TypeField alias_field = list.get[TypeField](state.fields, 1) handle: + return fail("missing field") + builder = type.construct_put[Session, string](builder, view alias_field, raw_alias) handle error: + return fail(error) + TypeField note_field = list.get[TypeField](state.fields, 2) handle: + return fail("missing field") + builder = type.construct_put[Session, string](builder, view note_field, "ready") handle error: + return fail(error) + return type.construct_finish[Session](builder) +function create_qualified(raw: string) returns result[Session at active, string]: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return fail("missing state") + mutable TypeConstruction builder = type.construct_machine_start[Session at active](view state) handle error: + return fail(error) + TypeField name_field = list.get[TypeField](state.fields, 0) handle: + return fail("missing field") + builder = type.construct_put[Session at active, string](builder, view name_field, raw) handle error: + return fail(error) + TypeField alias_field = list.get[TypeField](state.fields, 1) handle: + return fail("missing field") + builder = type.construct_put[Session at active, string](builder, view alias_field, "Agent") handle error: + return fail(error) + TypeField note_field = list.get[TypeField](state.fields, 2) handle: + return fail("missing field") + builder = type.construct_put[Session at active, string](builder, view note_field, "ready") handle error: + return fail(error) + return type.construct_finish[Session at active](builder) +function describe_bare(raw: string, raw_alias: string) returns string: + Session session = create_bare(raw, raw_alias) handle error: + return error + if session at active: + return "{coarsen session.name}:{coarsen session.alias}:{session.note}" + return "guest" +function describe_qualified(raw: string) returns string: + Session at active session = create_qualified(raw) handle error: + return error + return coarsen session.name +function create_simple(raw: int64) returns result[Simple, string]: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Simple](), 1) handle: + return fail("missing state") + mutable TypeConstruction builder = type.construct_machine_start[Simple](view state) handle error: + return fail(error) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return fail("missing field") + builder = type.construct_put[Simple, int64](builder, view field, raw) handle error: + return fail(error) + return type.construct_finish[Simple](builder) +function describe_simple(raw: int64) returns string: + Simple value = create_simple(raw) handle error: + return error + return "filled" +function describe_empty() returns string: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Simple](), 0) handle: + return "missing state" + TypeConstruction builder = type.construct_machine_start[Simple](view state) handle error: + return error + Simple value = type.construct_finish[Simple](builder) handle error: + return error + return "empty" +function main() returns nothing: + println(describe_bare("Ada", "Agent")) + println(describe_bare("", "Agent")) + println(describe_bare("Ada", "Al")) + println(describe_qualified("Nia")) + println(describe_qualified("")) + println(describe_simple(7)) + println(describe_simple(0)) + println(describe_empty()) + return nothing diff --git a/tests/native/refined_struct_base_values.jett b/tests/native/refined_struct_base_values.jett new file mode 100644 index 00000000..202f3ef4 --- /dev/null +++ b/tests/native/refined_struct_base_values.jett @@ -0,0 +1,33 @@ +namespace test +type NonEmpty = string where string.char_count(value) > 0 +type Long = NonEmpty where string.char_count(value) >= 3 +type Positive = int64 where value > 0 +struct User: + name: NonEmpty + alias: Long +struct Counter: + value: Positive +function make(name: string, alias: string) returns string: + User user = User(name: name, alias: alias) handle error: + return error + string first = coarsen user.name + string second = coarsen user.alias + return "{first}:{second}" +function inspect(value: result[User, string]) returns string: + User user = value handle error: + return error + return coarsen user.name +function nested(name: string, alias: string) returns string: + return inspect(User(name: name, alias: alias)) +function count(value: int64) returns string: + Counter counter = Counter(value: value) handle error: + return error + return "{coarsen counter.value}" +function main() returns nothing: + println(make("Ada", "Agent")) + println(make("", "Agent")) + println(make("Ada", "Al")) + println(nested("Ada", "Agent")) + println(nested("", "Agent")) + println(count(7)) + println(count(0)) diff --git a/tests/native/refined_struct_derived_inputs.jett b/tests/native/refined_struct_derived_inputs.jett new file mode 100644 index 00000000..1e3372ef --- /dev/null +++ b/tests/native/refined_struct_derived_inputs.jett @@ -0,0 +1,53 @@ +namespace test +type NonEmpty = string where string.char_count(value) > 0 +type Long = NonEmpty where string.char_count(value) >= 3 +type SecretPositive = secret[int64] where value > 0 +type SecretNonEmpty = secret[string] where string.char_count(value) > 0 +type SecretLong = SecretNonEmpty where string.char_count(value) >= 3 +struct Label: + value: Long +struct SecretCounter: + value: SecretPositive +struct SecretLabel: + value: SecretLong +function derived_label(value: string) returns string: + NonEmpty checked = value handle error: + return error + Label label = Label(value: checked) handle error: + return error + return coarsen label.value +function secret_count(value: int64) returns string: + secret[int64] hidden = value + SecretCounter counter = SecretCounter(value: hidden) handle error: + return error + return "accepted" +function derived_handle(value: string) returns string: + NonEmpty checked = value handle error: + return error + Long longer = checked handle error: + return error + return coarsen longer +function secret_handle(value: int64) returns string: + secret[int64] hidden = value + SecretPositive checked = hidden handle error: + return error + return "accepted" +function secret_label(value: string) returns string: + secret[string] hidden = value + SecretNonEmpty checked = hidden handle error: + return error + SecretLabel label = SecretLabel(value: checked) handle error: + return error + return declassify coarsen label.value +function main() returns nothing: + println(derived_label("Agent")) + println(derived_label("Al")) + println(secret_count(7)) + println(secret_count(0)) + println(derived_handle("Agent")) + println(derived_handle("Al")) + println(secret_handle(7)) + println(secret_handle(0)) + println(secret_label("Agent")) + println(secret_label("Al")) + println(secret_label("")) diff --git a/tests/native/refinement_collections.jett b/tests/native/refinement_collections.jett new file mode 100644 index 00000000..27ebd5ab --- /dev/null +++ b/tests/native/refinement_collections.jett @@ -0,0 +1,48 @@ +namespace app +type UserId = string where value != "" +type Positive = int64 where value > 0 +function main() returns nothing: + UserId first = "ada" handle error: + return + UserId duplicate = "ada" handle error: + return + UserId second = "bob" handle error: + return + mutable set[UserId] ids = set.new[UserId]() + ids = set.add[UserId](ids, first) + ids = set.add[UserId](ids, duplicate) + ids = set.add[UserId](ids, second) + println(set.length[UserId](view ids)) + UserId query = "ada" handle error: + return + println(set.contains[UserId](view ids, query)) + UserId removal = "ada" handle error: + return + ids = set.remove[UserId](ids, removal) + println(set.length[UserId](view ids)) + mutable map[UserId, int64] scores = map.new[UserId, int64]() + UserId map_key = "ada" handle error: + return + scores = map.insert[UserId, int64](scores, map_key, 7) + UserId map_query = "ada" handle error: + return + println(map.has[UserId, int64](view scores, map_query)) + Positive seven = 7 handle error: + return + Positive another_seven = 7 handle error: + return + mutable set[Positive] numbers = set.new[Positive]() + numbers = set.add[Positive](numbers, seven) + numbers = set.add[Positive](numbers, another_seven) + println(set.length[Positive](view numbers)) + mutable set[bool] flags = set.new[bool]() + flags = set.add[bool](flags, true) + println(set.contains[bool](view flags, true)) + flags = set.remove[bool](flags, true) + println(set.length[bool](view flags)) + mutable map[bool, int64] by_flag = map.new[bool, int64]() + by_flag = map.insert[bool, int64](by_flag, true, 9) + println(map.has[bool, int64](view by_flag, true)) + println(map.get_or[bool, int64](clone by_flag, true, 0)) + by_flag = map.remove[bool, int64](by_flag, true) + println(map.length[bool, int64](view by_flag)) diff --git a/tests/native/refinement_declaration_contexts.jett b/tests/native/refinement_declaration_contexts.jett new file mode 100644 index 00000000..6a1f28bd --- /dev/null +++ b/tests/native/refinement_declaration_contexts.jett @@ -0,0 +1,73 @@ +namespace decoy +export function name(view value: string) returns string: + return "decoy:{value}" +namespace domain +function accept(value: int64) returns bool: + return value > 0 +export type Positive = int64 where accept(value) +export type Bigger = Positive where value > 2 +export struct T: + count: int64 +export type CheckedRecord = T where value.count > 0 and (comptime type.name[T]()) == "domain.T" +export interface Named: + function name(view self: Named) returns string +implement Named for string: + function name(view self: string) returns string: + return "text:{self}" +export type Selected = Named where Named.name(view value) == "text:value" +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +export type Again = Selected where Named.name(view value) == "text:value" +implement Named for Again: + function name(view self: Again) returns string: + Selected parent = coarsen clone self + return "again:{Named.name(view parent)}" +export function make() returns result[Selected, string]: + Named source = "value" + Selected selected = source handle error: + return fail(error) + return ok(selected) +namespace app +function interface_values() returns nothing: + use domain + use decoy as Named + domain.Selected live = domain.make() handle error: + return println(error) + domain.Selected baked = comptime domain.make() handle error: + return println(error) + println(domain.Named.name(view live)) + println(domain.Named.name(view baked)) + domain.Again again = clone live handle error: + return println(error) + println(domain.Named.name(view again)) + println(Named.name(view "alias")) + return nothing +function generic[T](number: int64) returns nothing: + use domain + function(int64) returns bool accept = function(value: int64) returns bool: + return value < 0 + comptime type Bound = type.info[list[T]](): + domain.Positive positive = number handle error: + println("rejected:{type.name[T]()}:{type.name[Bound]()}:{error}") + println(accept(1)) + return nothing + println(type.name[T](), type.name[Bound](), coarsen positive, accept(1)) + domain.T source = domain.T(count: coarsen positive) + domain.CheckedRecord checked = source handle error: + return println(error) + domain.T restored = coarsen checked + println(restored.count) + domain.Bigger bigger = number handle error: + println("bigger-rejected:{type.name[T]()}:{type.name[Bound]()}:{error}") + println(accept(1)) + return nothing + println("bigger:{coarsen bigger}:{type.name[T]()}") + return nothing +function main() returns nothing: + interface_values() + generic[string](7) + generic[int8](0) + generic[bool](2) + return nothing diff --git a/tests/native/refinement_local_source_reuse.jett b/tests/native/refinement_local_source_reuse.jett new file mode 100644 index 00000000..7ade38cd --- /dev/null +++ b/tests/native/refinement_local_source_reuse.jett @@ -0,0 +1,15 @@ +namespace app +type NonEmptyNumbers = list[int64] where list.length(view value) > 0 +function inspect_source(source: list[int64]) returns string: + NonEmptyNumbers checked = source handle error: + trace source + return error + trace source + trace checked + return "{list.length[int64](view source)}" +function main() returns nothing: + list[int64] valid = list(2, 3) + list[int64] empty = list() + println(inspect_source(valid)) + println(inspect_source(empty)) + return nothing diff --git a/tests/native/refinement_pending_aggregate_builders.jett b/tests/native/refinement_pending_aggregate_builders.jett new file mode 100644 index 00000000..eeba0e8f --- /dev/null +++ b/tests/native/refinement_pending_aggregate_builders.jett @@ -0,0 +1,67 @@ +namespace app +type Positive = int64 where run(value > 0) +type StrictPositive = int64 where value > 0 +struct Record: + value: Positive +struct PendingRecord: + value: StrictPositive +enum Event: + item(value: Positive) +machine Session: + states: + ready(value: app.Positive) + transitions: + ready to ready +function direct_record() returns string: + Record record = Record(value: 7) handle error: + return error + return "unexpected record" +function reflected_record() returns string: + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return "missing record field" + TypeConstruction builder = type.construct_start[Record]() + TypeConstruction filled = type.construct_put[Record, int64](builder, view field, 7) handle error: + return error + Record record = type.construct_finish[Record](filled) handle error: + return error + return "unexpected reflected record" +function reflected_pending_record() returns string: + TypeField field = list.get[TypeField](type.fields[PendingRecord](), 0) handle: + return "missing pending record field" + TypeConstruction builder = type.construct_start[PendingRecord]() + TypeConstruction filled = type.construct_put[PendingRecord, int64](builder, view field, run 7) handle error: + return error + PendingRecord record = type.construct_finish[PendingRecord](filled) handle error: + return error + return "unexpected pending record" +function reflected_event() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Event](), 0) handle: + return "missing variant" + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return "missing event field" + TypeConstruction builder = type.construct_variant_start[Event](view variant) handle error: + return error + TypeConstruction filled = type.construct_put[Event, int64](builder, view field, 7) handle error: + return error + Event event = type.construct_finish[Event](filled) handle error: + return error + return "unexpected event" +function reflected_session() returns string: + TypeMachineState state = list.get[TypeMachineState](type.machine_states[Session](), 0) handle: + return "missing state" + TypeField field = list.get[TypeField](state.fields, 0) handle: + return "missing session field" + TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + TypeConstruction filled = type.construct_put[Session, int64](builder, view field, 7) handle error: + return error + Session session = type.construct_finish[Session](filled) handle error: + return error + return "unexpected session" +function main() returns nothing: + println(direct_record()) + println(reflected_record()) + println(reflected_pending_record()) + println(reflected_event()) + println(reflected_session()) + return nothing diff --git a/tests/native/refinement_pending_predicate_result.jett b/tests/native/refinement_pending_predicate_result.jett new file mode 100644 index 00000000..27b3baac --- /dev/null +++ b/tests/native/refinement_pending_predicate_result.jett @@ -0,0 +1,9 @@ +namespace app +type Positive = int8 where run(value > 0) +function main() returns nothing: + int8 base = 7 + Positive value = base handle error: + println(error) + return nothing + println(coarsen value) + return nothing diff --git a/tests/native/refinement_validation.jett b/tests/native/refinement_validation.jett new file mode 100644 index 00000000..c7d49ab5 --- /dev/null +++ b/tests/native/refinement_validation.jett @@ -0,0 +1,18 @@ +namespace app +type Positive = int64 where value > 0 +type AboveTen = Positive where value > 10 +type NonEmpty = string where value != "" +function describe(raw: int64) returns string: + AboveTen checked = raw handle error: + return error + return "{coarsen checked}" +function describe_name(raw: string) returns string: + NonEmpty checked = raw handle error: + return error + return coarsen checked +function main() returns nothing: + println(describe(-1)) + println(describe(5)) + println(describe(12)) + println(describe_name("")) + println(describe_name("Ada")) diff --git a/tests/native/reflected_alias_callbacks.jett b/tests/native/reflected_alias_callbacks.jett new file mode 100644 index 00000000..0623da60 --- /dev/null +++ b/tests/native/reflected_alias_callbacks.jett @@ -0,0 +1,73 @@ +type Label = string +type Other = string +type Small = int8 +struct Wrapped: + items: list[Small] +struct Record: + aliased: Label + plain: string + other: Other + repeated: Label + nested_alias: list[Label] + nested_plain: list[string] +function describe[T]() returns string: + return "{type.kind[T]()}:{type.name[T]()}" +function make[T]() returns list[function() returns string]: + mutable list[function() returns string] callbacks = list.new[function() returns string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + callbacks = list.append[function() returns string](callbacks, function() returns string: return describe[Field]()) + return callbacks +function direct() returns function() returns string: + comptime type Element = type.arg[list[Label]](0): + return function() returns string: return describe[Element]() +function arguments() returns string: + mutable list[string] names = list.new[string]() + for arg in type.info[map[Label, string]]().args: + comptime type Arg = arg: + names = list.append[string](names, describe[Arg]()) + return string.join(names, ",") +function nested_arguments() returns string: + mutable list[string] names = list.new[string]() + for arg in type.info[list[Label]]().args: + for base in arg.args: + comptime type Base = base: + names = list.append[string](names, describe[Base]()) + return string.join(names, ",") +function field_arguments() returns string: + mutable list[string] names = list.new[string]() + for field in type.fields[Record](): + for arg in field.type_info.args: + comptime type Arg = arg: + names = list.append[string](names, describe[Arg]()) + return string.join(names, ",") +function nested_width[T]() returns string: + comptime type Item = type.arg[T](0): + Item value = 127 + return "{value + 1}" +function wrapped_width() returns string: + mutable string found = "missing" + for field in type.fields[Wrapped](): + comptime type Field = field.type_info: + found = nested_width[Field]() + return found +function print_callbacks(callbacks: list[function() returns string]) returns nothing: + for callback in callbacks: + println(callback()) + return nothing +function main() returns nothing: + print_callbacks(make[Record]()) + print_callbacks(comptime make[Record]()) + function() returns string first = direct() + function() returns string second = comptime direct() + println(first()) + println(second()) + println(arguments()) + println(comptime arguments()) + println(nested_arguments()) + println(comptime nested_arguments()) + println(field_arguments()) + println(comptime field_arguments()) + println(wrapped_width()) + println(comptime wrapped_width()) + return nothing diff --git a/tests/native/reflected_enum_empty_candidate_owner_failure.jett b/tests/native/reflected_enum_empty_candidate_owner_failure.jett new file mode 100644 index 00000000..37a32f6a --- /dev/null +++ b/tests/native/reflected_enum_empty_candidate_owner_failure.jett @@ -0,0 +1,13 @@ +namespace app +enum Choice: + empty + number(value: int64) +function main() returns nothing: + Choice selected = Choice.empty + TypeVariant other = list.get[TypeVariant](type.variants[Choice](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 value = type.variant_field_value[Choice, int64](view selected, view foreign) + println(value) + return nothing diff --git a/tests/native/reflected_enum_foreign_field_failure.jett b/tests/native/reflected_enum_foreign_field_failure.jett new file mode 100644 index 00000000..986fc1ea --- /dev/null +++ b/tests/native/reflected_enum_foreign_field_failure.jett @@ -0,0 +1,13 @@ +namespace app +enum Choice: + first(value: int64) + second(value: int64) +function main() returns nothing: + Choice selected = Choice.first(7) + TypeVariant other = list.get[TypeVariant](type.variants[Choice](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 value = type.variant_field_value[Choice, int64](view selected, view foreign) + println(value) + return nothing diff --git a/tests/native/reflected_enum_missing_candidate_owner_failure.jett b/tests/native/reflected_enum_missing_candidate_owner_failure.jett new file mode 100644 index 00000000..3cfb396e --- /dev/null +++ b/tests/native/reflected_enum_missing_candidate_owner_failure.jett @@ -0,0 +1,13 @@ +namespace app +enum Choice: + first(value: int64) + second(label: string, value: int64) +function main() returns nothing: + Choice selected = Choice.first(7) + TypeVariant other = list.get[TypeVariant](type.variants[Choice](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 1) handle: + return nothing + int64 value = type.variant_field_value[Choice, int64](view selected, view foreign) + println(value) + return nothing diff --git a/tests/native/reflected_enum_requested_type_failure.jett b/tests/native/reflected_enum_requested_type_failure.jett new file mode 100644 index 00000000..40e1f3de --- /dev/null +++ b/tests/native/reflected_enum_requested_type_failure.jett @@ -0,0 +1,12 @@ +namespace app +enum Choice: + text(value: string) + number(value: int64) +function main() returns nothing: + Choice selected = Choice.text("hello") + TypeVariant variant = type.variant_value[Choice](view selected) + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + int64 wrong = type.variant_field_value[Choice, int64](view selected, view field) + println(wrong) + return nothing diff --git a/tests/native/reflected_integer_wrapping.jett b/tests/native/reflected_integer_wrapping.jett new file mode 100644 index 00000000..12d2b942 --- /dev/null +++ b/tests/native/reflected_integer_wrapping.jett @@ -0,0 +1,50 @@ +struct Widths: + narrow: int8 + wide: int64 + repeated: int8 +struct UnsignedWidths: + narrow: uint8 + wide: uint64 +function direct(view values: Widths) returns string: + mutable list[string] results = list.new[string]() + for field in type.fields[Widths](): + comptime type Field = field.type_info: + Field value = type.field_value[Widths, Field](view values, view field) + Field bumped = value + 1 + results = list.append[string](results, "{bumped}") + return string.join(results, ",") +function generic[T](view values: T) returns string: + mutable list[string] results = list.new[string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + Field value = type.field_value[T, Field](view values, view field) + comptime type Inner = type.info[Field](): + Inner bumped = value + 1 + results = list.append[string](results, "{bumped}") + return string.join(results, ",") +function make[T](view values: T) returns list[function() returns string]: + mutable list[function() returns string] callbacks = list.new[function() returns string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + Field value = type.field_value[T, Field](view values, view field) + comptime type Inner = type.info[Field](): + callbacks = list.append[function() returns string](callbacks, function() returns string: return "{value + 1}") + return callbacks +function print_callbacks(callbacks: list[function() returns string]) returns nothing: + for callback in callbacks: + println(callback()) + return nothing +function main() returns nothing: + Widths values = Widths(narrow: 127, wide: 127, repeated: 127) + UnsignedWidths unsigned_values = UnsignedWidths(narrow: 255, wide: 18446744073709551615) + println(direct(view values)) + println(comptime direct(view Widths(narrow: 127, wide: 127, repeated: 127))) + println(generic[Widths](view values)) + println(comptime generic[Widths](view Widths(narrow: 127, wide: 127, repeated: 127))) + println(generic[UnsignedWidths](view unsigned_values)) + println(comptime generic[UnsignedWidths](view UnsignedWidths(narrow: 255, wide: 18446744073709551615))) + print_callbacks(make[Widths](view values)) + print_callbacks(comptime make[Widths](view Widths(narrow: 127, wide: 127, repeated: 127))) + print_callbacks(make[UnsignedWidths](view unsigned_values)) + print_callbacks(comptime make[UnsignedWidths](view UnsignedWidths(narrow: 255, wide: 18446744073709551615))) + return nothing diff --git a/tests/native/reflected_interface_field_owners.jett b/tests/native/reflected_interface_field_owners.jett new file mode 100644 index 00000000..19b2e5f8 --- /dev/null +++ b/tests/native/reflected_interface_field_owners.jett @@ -0,0 +1,235 @@ +namespace app +interface Named: + function name(view self: Named) returns string +implement Named for int8: + function name(view self: int8) returns string: + return "small:{self}" +type Selected = Named where true +implement Named for Selected: + function name(view self: Selected) returns string: + Named base = coarsen clone self + return "selected:{Named.name(view base)}" +type Nested = Selected where true +implement Named for Nested: + function name(view self: Nested) returns string: + Selected base = coarsen clone self + Named erased = base + return "nested:{Named.name(view erased)}" +function selected_seed(value: int8) returns Selected: + Named base = value + Selected selected = base handle error: + return selected_seed(value) + return selected +function nested_seed(value: int8) returns Nested: + Selected base = selected_seed(value) + Nested nested = base handle error: + return nested_seed(value) + return nested +struct Record: + marker: string + item: Selected + nested: Nested + count: uint64 +struct Hidden: + item: secret[Selected] +enum Event: + idle + active(marker: string, item: Selected, nested: Nested) + mirror(marker: int64, nested: Nested, item: Selected) +machine Session: + states: + idle + active(marker: string, item: Selected, nested: Nested) + mirror(marker: int64, nested: Nested, item: Selected) + transitions: + idle to active + active to mirror +function record_seed(value: int8) returns Record: + return Record(marker: "record", item: selected_seed(value), nested: nested_seed(value), count: 18446744073709551615) handle error: + return record_seed(value) +function event_seed(value: int8, reversed: bool) returns Event: + if reversed: + return Event.mirror(9, nested_seed(value), selected_seed(value)) + return Event.active("event", selected_seed(value), nested_seed(value)) +function session_seed(value: int8, reversed: bool) returns Session: + if reversed: + return Session(mirror, 9, nested_seed(value), selected_seed(value)) + return Session(active, "session", selected_seed(value), nested_seed(value)) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Record](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function selected_names(erased: Named, exact: Selected) returns string: + Named nominal = clone exact + Named base = coarsen exact + return "{Named.name(view erased)}|{Named.name(view nominal)}|{Named.name(view base)}" +function nested_names(erased: Named, ancestor: Selected, exact: Nested) returns string: + Named ancestor_named = ancestor + Named exact_named = clone exact + Selected coarsened = coarsen exact + Named base = coarsened + return "{Named.name(view erased)}|{Named.name(view ancestor_named)}|{Named.name(view exact_named)}|{Named.name(view base)}" +function record_summary(view source: Record) returns string: + TypeField item = record_field(1) + TypeField nested = record_field(2) + Named selected_named = type.field_value[Record, Named](view source, view item) + Selected selected_exact = type.field_value[Record, Selected](view source, view item) + Named nested_named = type.field_value[Record, Named](view source, view nested) + Selected ancestor = type.field_value[Record, Selected](view source, view nested) + Nested nested_exact = type.field_value[Record, Nested](view source, view nested) + return "{selected_names(selected_named, selected_exact)};{nested_names(nested_named, ancestor, nested_exact)};{source.marker}:{source.count}" +function event_summary(view source: Event) returns string: + TypeVariant variant = type.variant_value[Event](view source) + mutable int64 selected_index = 1 + mutable int64 nested_index = 2 + if variant.name == "mirror": + selected_index = 2 + nested_index = 1 + TypeField item = event_field(view source, selected_index) + TypeField nested = event_field(view source, nested_index) + Named selected_named = type.variant_field_value[Event, Named](view source, view item) + Selected selected_exact = type.variant_field_value[Event, Selected](view source, view item) + Named nested_named = type.variant_field_value[Event, Named](view source, view nested) + Selected ancestor = type.variant_field_value[Event, Selected](view source, view nested) + Nested nested_exact = type.variant_field_value[Event, Nested](view source, view nested) + return "{variant.name}:{selected_names(selected_named, selected_exact)};{nested_names(nested_named, ancestor, nested_exact)}" +function session_summary(view source: Session) returns string: + TypeMachineState state = type.machine_state_value[Session](view source) + mutable int64 selected_index = 1 + mutable int64 nested_index = 2 + if state.name == "mirror": + selected_index = 2 + nested_index = 1 + TypeField item = session_field(view source, selected_index) + TypeField nested = session_field(view source, nested_index) + Named selected_named = type.machine_field_value[Session, Named](view source, view item) + Selected selected_exact = type.machine_field_value[Session, Selected](view source, view item) + Named nested_named = type.machine_field_value[Session, Named](view source, view nested) + Selected ancestor = type.machine_field_value[Session, Selected](view source, view nested) + Nested nested_exact = type.machine_field_value[Session, Nested](view source, view nested) + return "{state.name}:{selected_names(selected_named, selected_exact)};{nested_names(nested_named, ancestor, nested_exact)}" +function owned_record(source: Record) returns string: + return "{record_summary(view source)}={record_summary(view source)}" +function direct_summary() returns string: + Named selected = selected_seed(7) + Named nested = nested_seed(7) + return "{Named.name(view selected)}|{Named.name(view nested)}" +function hidden_read(view source: Hidden) returns secret[Named]: + TypeField field = list.get[TypeField](type.fields[Hidden](), 0) handle: + return hidden_read(view source) + return type.field_value[Hidden, secret[Named]](view source, view field) +function hidden_summary(view source: Hidden) returns string: + secret[Named] hidden = hidden_read(view source) + Named exposed = declassify hidden + return Named.name(view exposed) +function secret_controls(view stdout: Stdout) returns nothing: + secret[Selected] hidden = selected_seed(7) + secret[Named] ordinary = clone hidden + trace ordinary + Named direct = declassify ordinary + Stdout.write(view stdout, "secret-direct:{Named.name(view direct)}\n") + Hidden source = Hidden(item: hidden) + secret[Named] reflected = hidden_read(view source) + trace reflected + Named exposed = declassify reflected + Stdout.write(view stdout, "secret-reflected:{Named.name(view exposed)}\n") + Stdout.write(view stdout, "secret-reuse:{hidden_summary(view source)}\n") + return nothing +function pending_record() returns string: + Record source = Record(marker: "pending", item: run run selected_seed(7), nested: nested_seed(7), count: 1) handle error: + return "invalid record" + TypeField field = record_field(1) + Named copied = type.field_value[Record, Named](view source, view field) + trace copied + Named once = join copied handle error: + return "first join failed" + trace once + Named ready = join once handle error: + return "second join failed" + return Named.name(view ready) +function pending_event() returns string: + Event source = Event.mirror(9, run run nested_seed(9), selected_seed(9)) + TypeField field = event_field(view source, 1) + Named copied = type.variant_field_value[Event, Named](view source, view field) + trace copied + Named once = join copied handle error: + return "first join failed" + trace once + Named ready = join once handle error: + return "second join failed" + return Named.name(view ready) +function pending_session() returns string: + Session source = Session(mirror, 9, run run nested_seed(9), selected_seed(9)) + TypeField field = session_field(view source, 1) + Named copied = type.machine_field_value[Session, Named](view source, view field) + trace copied + Named once = join copied handle error: + return "first join failed" + trace once + Named ready = join once handle error: + return "second join failed" + return Named.name(view ready) +function wrong_record_request() returns string: + Record source = record_seed(7) + TypeField field = record_field(1) + int64 value = type.field_value[Record, int64](view source, view field) + return "{value}" +function wrong_event_member() returns string: + Event source = event_seed(7, true) + Event other = event_seed(7, false) + TypeField field = event_field(view other, 1) + Named value = type.variant_field_value[Event, Named](view source, view field) + return Named.name(view value) +function pending_session_metadata() returns string: + Session source = session_seed(7, false) + TypeField field = run run session_field(view source, 1) + Named value = type.machine_field_value[Session, Named](view source, view field) + return Named.name(view value) +function summaries() returns string: + Record record = record_seed(7) + Event event = event_seed(7, false) + Event reversed_event = event_seed(9, true) + Session session = session_seed(7, false) + Session reversed_session = session_seed(9, true) + return "record:{record_summary(view record)}\nevent:{event_summary(view event)}\nmirror-event:{event_summary(view reversed_event)}\nstate:{session_summary(view session)}\nmirror-state:{session_summary(view reversed_session)}\n" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "direct:{direct_summary()}\n") + Stdout.write(view stdout, summaries()) + string baked = comptime summaries() + Stdout.write(view stdout, "baked:\n{baked}") + Record source = record_seed(7) + Stdout.write(view stdout, "reuse:{owned_record(clone source)}={record_summary(view source)}\n") + Stdout.write(view stdout, "pending:{pending_record()}|{pending_event()}|{pending_session()}\n") + secret_controls(view stdout) + return nothing +verify record_owners: + Record source = record_seed(7) + assert record_summary(view source) == "selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7;record:18446744073709551615" + assert owned_record(clone source) == "{record_summary(view source)}={record_summary(view source)}" + assert direct_summary() == "selected:small:7|nested:selected:small:7" + string baked = comptime summaries() + assert baked == summaries() + secret[Selected] selected = selected_seed(7) + secret[Named] erased = clone selected + Named exposed = declassify erased + assert Named.name(view exposed) == "selected:small:7" + Hidden hidden = Hidden(item: selected) + assert hidden_summary(view hidden) == "selected:small:7" + assert hidden_summary(view hidden) == "selected:small:7" +property preserves_member_owners: + given reversed: bool + Event event = event_seed(7, reversed) + Session session = session_seed(7, reversed) + mutable string expected = "active:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7" + if reversed: + expected = "mirror:selected:small:7|selected:small:7|small:7;nested:selected:small:7|selected:small:7|nested:selected:small:7|selected:small:7" + assert event_summary(view event) == expected + assert session_summary(view session) == expected + assert event_summary(view event) == expected diff --git a/tests/native/reflected_iteration_controls/arguments_nested.jett b/tests/native/reflected_iteration_controls/arguments_nested.jett new file mode 100644 index 00000000..1c286a60 --- /dev/null +++ b/tests/native/reflected_iteration_controls/arguments_nested.jett @@ -0,0 +1,55 @@ +namespace app +struct Inner: + first: int64 + second: int64 +struct Outer: + left: Inner + right: Inner +function first[T]() returns string: + return "first:{type.name[T]()}" +function second[T]() returns string: + return "second:{type.name[T]()}" +function is_first(index: int64) returns bool: + return index == 0 +function nested_label[T](name: string, index: int64) returns string: + if is_first(index): + return "{name}:{first[T]()}" + return "{name}:{second[T]()}" +function arguments(high: bool) returns string: + mutable list[string] parts = list.new[string]() + mutable int64 index = 0 + if high: + index = 1 + for arg in type.info[map[int64, int64]]().args: + comptime type Arg = arg: + if is_first(index): + parts = list.append[string](parts, first[Arg]()) + else: + parts = list.append[string](parts, second[Arg]()) + index = index + 1 + return string.join(parts, ";") +function nested_fields() returns string: + mutable list[string] parts = list.new[string]() + for outer in type.fields[Outer](): + comptime type Field = outer.type_info: + for inner in type.fields[Field](): + comptime type Item = inner.type_info: + string name = "{outer.name}.{inner.name}" + parts = list.append[string](parts, nested_label[Item](name, inner.index)) + return string.join(parts, ";") +function report(high: bool) returns string: + return "{arguments(high)}|{nested_fields()}" +function expected(high: bool) returns string: + string nested = "left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64" + if high: + return "second:int64;second:int64|{nested}" + return "first:int64;second:int64|{nested}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_arguments_nested: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_arguments_nested_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/arguments_nested.stdout b/tests/native/reflected_iteration_controls/arguments_nested.stdout new file mode 100644 index 00000000..16dd4c56 --- /dev/null +++ b/tests/native/reflected_iteration_controls/arguments_nested.stdout @@ -0,0 +1,2 @@ +first:int64;second:int64|left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64 +first:int64;second:int64|left.first:first:int64;left.second:second:int64;right.first:first:int64;right.second:second:int64 diff --git a/tests/native/reflected_iteration_controls/members.jett b/tests/native/reflected_iteration_controls/members.jett new file mode 100644 index 00000000..df0d827a --- /dev/null +++ b/tests/native/reflected_iteration_controls/members.jett @@ -0,0 +1,85 @@ +namespace app +bitfield Header: + left: 4 bits + right: 4 bits +enum Event: + empty + first(left: int64, right: int64) + second(left: int64, right: int64) +machine Session: + states: + empty + first(left: int64, right: int64) + second(left: int64, right: int64) + transitions: + empty to first + first to second +function first(value: int64) returns int64: + return value + 10 +function second(value: int64) returns int64: + return value + 100 +function is_right(view field: TypeField) returns bool: + return field.name == "right" +function bitfield_report(high: bool) returns string: + mutable int64 left = 2 + mutable int64 right = 3 + if high: + left = 5 + right = 7 + Header source = Header(left: left, right: right) handle error: + return error + mutable list[string] parts = list.new[string]() + for field in type.fields[Header](): + comptime type Field = field.type_info: + Field value = type.field_value[Header, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function variant_report(seed: int64) returns string: + mutable list[string] parts = list.new[string]() + for variant in type.variants[Event](): + string member = variant.name + mutable int64 total = 0 + for field in variant.fields: + comptime type Field = field.type_info: + Field value = seed + if field.index == 0: + total = total + first(value) + else: + total = total + second(value) + parts = list.append[string](parts, "{member}:{total}") + return string.join(parts, ";") +function machine_report(seed: int64) returns string: + mutable list[string] parts = list.new[string]() + for state in type.machine_states[Session](): + string member = state.name + mutable int64 total = 0 + for field in state.fields: + comptime type Field = field.type_info: + Field value = seed + if is_right(view field): + total = total + second(value) + else: + total = total + first(value) + parts = list.append[string](parts, "{member}:{total}") + return string.join(parts, ";") +function report(high: bool) returns string: + mutable int64 seed = 17 + if high: + seed = 23 + return "{bitfield_report(high)}|{variant_report(seed)}|{machine_report(seed)}" +function expected(high: bool) returns string: + if high: + return "left:15;right:107|empty:0;first:156;second:156|empty:0;first:156;second:156" + return "left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_members: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_member_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/members.stdout b/tests/native/reflected_iteration_controls/members.stdout new file mode 100644 index 00000000..c86a8be1 --- /dev/null +++ b/tests/native/reflected_iteration_controls/members.stdout @@ -0,0 +1,2 @@ +left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144 +left:12;right:103|empty:0;first:144;second:144|empty:0;first:144;second:144 diff --git a/tests/native/reflected_iteration_controls/nominal.jett b/tests/native/reflected_iteration_controls/nominal.jett new file mode 100644 index 00000000..719d8545 --- /dev/null +++ b/tests/native/reflected_iteration_controls/nominal.jett @@ -0,0 +1,81 @@ +namespace app +struct Record: + left: int64 + right: int64 +struct PairOf[T]: + left: T + right: T +type Slot = int64 +struct AliasPair: + left: Slot + right: Slot +function first(value: int64) returns int64: + return value + 10 +function second(value: int64) returns int64: + return value + 100 +function is_right(view field: TypeField) returns bool: + return field.name == "right" +function typed_label[T](name: string) returns string: + return "{name}:{type.name[T]()}" +function by_index(view source: Record) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + Field value = type.field_value[Record, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function by_name(view source: Record) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + Field value = type.field_value[Record, Field](view source, view field) + if is_right(view field): + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function generic_by_index[T](view source: T) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[T](): + comptime type Field = field.type_info: + Field value = type.field_value[T, Field](view source, view field) + if field.index == 0: + parts = list.append[string](parts, "{field.name}:{first(value)}") + else: + parts = list.append[string](parts, "{field.name}:{second(value)}") + return string.join(parts, ";") +function alias_labels() returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[AliasPair](): + comptime type Field = field.type_info: + parts = list.append[string](parts, typed_label[Field](field.name)) + return string.join(parts, ";") +function report(high: bool) returns string: + mutable int64 left = 2 + mutable int64 right = 3 + if high: + left = 5 + right = 7 + Record source = Record(left: left, right: right) + PairOf[int64] generic_source = PairOf[int64](left: left, right: right) + string indexed = by_index(view source) + string named = by_name(view source) + string generic_record = generic_by_index[Record](view source) + string generic_owner = generic_by_index[PairOf[int64]](view generic_source) + return "{indexed}|{named}|{generic_record}|{generic_owner}|{alias_labels()}" +function expected(high: bool) returns string: + if high: + return "left:15;right:107|left:105;right:17|left:15;right:107|left:15;right:107|left:app.Slot;right:app.Slot" + return "left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(false) + Stdout.write(view stdout, "{report(false)}\n{baked}\n") +verify ordinary_reflection_nominal: + assert report(false) == expected(false) + assert report(true) == expected(true) +property ordinary_reflection_nominal_trials: + given high: bool + assert report(high) == expected(high) diff --git a/tests/native/reflected_iteration_controls/nominal.stdout b/tests/native/reflected_iteration_controls/nominal.stdout new file mode 100644 index 00000000..5aab8ad5 --- /dev/null +++ b/tests/native/reflected_iteration_controls/nominal.stdout @@ -0,0 +1,2 @@ +left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot +left:12;right:103|left:102;right:13|left:12;right:103|left:12;right:103|left:app.Slot;right:app.Slot diff --git a/tests/native/reflected_machine_empty_candidate_owner_failure.jett b/tests/native/reflected_machine_empty_candidate_owner_failure.jett new file mode 100644 index 00000000..97c20239 --- /dev/null +++ b/tests/native/reflected_machine_empty_candidate_owner_failure.jett @@ -0,0 +1,16 @@ +namespace app +machine Session: + states: + active + inactive(count: int64) + transitions: + active to inactive +function main() returns nothing: + Session session = Session(active) + TypeMachineState other = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 count = type.machine_field_value[Session, int64](view session, view foreign) + println(count) + return nothing diff --git a/tests/native/reflected_machine_foreign_field_failure.jett b/tests/native/reflected_machine_foreign_field_failure.jett new file mode 100644 index 00000000..1a7e1a9a --- /dev/null +++ b/tests/native/reflected_machine_foreign_field_failure.jett @@ -0,0 +1,16 @@ +namespace app +machine Session: + states: + active(count: int64) + inactive(count: int64) + transitions: + active to inactive +function main() returns nothing: + Session session = Session(active, 7) + TypeMachineState other = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 0) handle: + return nothing + int64 count = type.machine_field_value[Session, int64](view session, view foreign) + println(count) + return nothing diff --git a/tests/native/reflected_machine_missing_candidate_owner_failure.jett b/tests/native/reflected_machine_missing_candidate_owner_failure.jett new file mode 100644 index 00000000..4a836823 --- /dev/null +++ b/tests/native/reflected_machine_missing_candidate_owner_failure.jett @@ -0,0 +1,16 @@ +namespace app +machine Session: + states: + active(count: int64) + inactive(label: string, count: int64) + transitions: + active to inactive +function main() returns nothing: + Session session = Session(active, 7) + TypeMachineState other = list.get[TypeMachineState](type.machine_states[Session](), 1) handle: + return nothing + TypeField foreign = list.get[TypeField](other.fields, 1) handle: + return nothing + int64 count = type.machine_field_value[Session, int64](view session, view foreign) + println(count) + return nothing diff --git a/tests/native/reflected_machine_requested_type_failure.jett b/tests/native/reflected_machine_requested_type_failure.jett new file mode 100644 index 00000000..92b2e0df --- /dev/null +++ b/tests/native/reflected_machine_requested_type_failure.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + active(label: string, count: int64) + closed + transitions: + active to closed +function main() returns nothing: + Session session = Session(active, "ready", 7) + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + int64 wrong = type.machine_field_value[Session, int64](view session, view field) + println(wrong) + return nothing diff --git a/tests/native/reflected_nested/child_ancestor_pending_1.debug b/tests/native/reflected_nested/child_ancestor_pending_1.debug new file mode 100644 index 00000000..8ee32178 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_1.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high diff --git a/tests/native/reflected_nested/child_ancestor_pending_1.jett b/tests/native/reflected_nested/child_ancestor_pending_1.jett new file mode 100644 index 00000000..87cc3955 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_1.jett @@ -0,0 +1,70 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[High] +enum Event: + content(payload: list[High]) +machine Session: + states: + content(payload: list[High]) + empty + transitions: + content to empty +function seed() returns list[High]: + Positive lower = 12 handle error: + return seed() + High higher = lower handle error: + return seed() + return run list(higher) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/child_ancestor_pending_1.stdout b/tests/native/reflected_nested/child_ancestor_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/child_ancestor_pending_2.debug b/tests/native/reflected_nested/child_ancestor_pending_2.debug new file mode 100644 index 00000000..8ee32178 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_2.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high diff --git a/tests/native/reflected_nested/child_ancestor_pending_2.jett b/tests/native/reflected_nested/child_ancestor_pending_2.jett new file mode 100644 index 00000000..7dfe56c5 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_2.jett @@ -0,0 +1,70 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[High] +enum Event: + content(payload: list[High]) +machine Session: + states: + content(payload: list[High]) + empty + transitions: + content to empty +function seed() returns list[High]: + Positive lower = 12 handle error: + return seed() + High higher = lower handle error: + return seed() + return run run list(higher) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/child_ancestor_pending_2.stdout b/tests/native/reflected_nested/child_ancestor_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/child_ancestor_ready.debug b/tests/native/reflected_nested/child_ancestor_ready.debug new file mode 100644 index 00000000..f0a6f3cc --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_ready.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace record: app.Holder = app.Holder(payload: list(12)) +trace event: app.Event = app.Event.content(list(12)) +trace session: app.Session = app.Session@content(list(12)) diff --git a/tests/native/reflected_nested/child_ancestor_ready.jett b/tests/native/reflected_nested/child_ancestor_ready.jett new file mode 100644 index 00000000..ad708594 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_ready.jett @@ -0,0 +1,70 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[High] +enum Event: + content(payload: list[High]) +machine Session: + states: + content(payload: list[High]) + empty + transitions: + content to empty +function seed() returns list[High]: + Positive lower = 12 handle error: + return seed() + High higher = lower handle error: + return seed() + return list(higher) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/child_ancestor_ready.stdout b/tests/native/reflected_nested/child_ancestor_ready.stdout new file mode 100644 index 00000000..99f0b1d2 --- /dev/null +++ b/tests/native/reflected_nested/child_ancestor_ready.stdout @@ -0,0 +1,2 @@ +before +after::12|:12|:12:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/child_exact_pending_1.debug b/tests/native/reflected_nested/child_exact_pending_1.debug new file mode 100644 index 00000000..6cf61459 --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_1.debug @@ -0,0 +1,12 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace items: list[app.High] = pending(list(12)) +trace items: list[app.High] = pending(list(12)) +trace items: list[app.High] = pending(list(12)) +trace record: app.Holder = app.Holder(payload: pending(list(12))) +trace event: app.Event = app.Event.content(pending(list(12))) +trace session: app.Session = app.Session@content(pending(list(12))) diff --git a/tests/native/reflected_nested/child_exact_pending_1.jett b/tests/native/reflected_nested/child_exact_pending_1.jett new file mode 100644 index 00000000..88fcbe19 --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_1.jett @@ -0,0 +1,68 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[High] +enum Event: + content(payload: list[High]) +machine Session: + states: + content(payload: list[High]) + empty + transitions: + content to empty +function seed() returns list[High]: + Positive lower = 12 handle error: + return seed() + High higher = lower handle error: + return seed() + return run list(higher) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[High]: + return type.field_value[Holder, list[High]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[High]: + return type.variant_field_value[Event, list[High]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[High]: + return type.machine_field_value[Session, list[High]](view source, view field) +function summarize(items: list[High]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/child_exact_pending_1.stdout b/tests/native/reflected_nested/child_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/child_exact_pending_2.debug b/tests/native/reflected_nested/child_exact_pending_2.debug new file mode 100644 index 00000000..2cb6b15f --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_2.debug @@ -0,0 +1,12 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace items: list[app.High] = pending(pending(list(12))) +trace items: list[app.High] = pending(pending(list(12))) +trace items: list[app.High] = pending(pending(list(12))) +trace record: app.Holder = app.Holder(payload: pending(pending(list(12)))) +trace event: app.Event = app.Event.content(pending(pending(list(12)))) +trace session: app.Session = app.Session@content(pending(pending(list(12)))) diff --git a/tests/native/reflected_nested/child_exact_pending_2.jett b/tests/native/reflected_nested/child_exact_pending_2.jett new file mode 100644 index 00000000..bfc17b5d --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_2.jett @@ -0,0 +1,68 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[High] +enum Event: + content(payload: list[High]) +machine Session: + states: + content(payload: list[High]) + empty + transitions: + content to empty +function seed() returns list[High]: + Positive lower = 12 handle error: + return seed() + High higher = lower handle error: + return seed() + return run run list(higher) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[High]: + return type.field_value[Holder, list[High]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[High]: + return type.variant_field_value[Event, list[High]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[High]: + return type.machine_field_value[Session, list[High]](view source, view field) +function summarize(items: list[High]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/child_exact_pending_2.stdout b/tests/native/reflected_nested/child_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/child_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/declared_root_ancestor_pending.debug b/tests/native/reflected_nested/declared_root_ancestor_pending.debug new file mode 100644 index 00000000..5a09dab9 --- /dev/null +++ b/tests/native/reflected_nested/declared_root_ancestor_pending.debug @@ -0,0 +1,15 @@ +trace marker: string = positive +trace marker: string = rootlower +trace marker: string = rootupper +trace marker: string = positive +trace marker: string = rootlower +trace marker: string = rootupper +trace marker: string = positive +trace marker: string = rootlower +trace marker: string = rootupper +trace items: app.RootLower = pending(pending(list(7))) +trace items: app.RootLower = pending(pending(list(7))) +trace items: app.RootLower = pending(pending(list(7))) +trace record: app.Holder = app.Holder(payload: pending(pending(list(7)))) +trace event: app.Event = app.Event.content(pending(pending(list(7)))) +trace session: app.Session = app.Session@content(pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/declared_root_ancestor_pending.jett b/tests/native/reflected_nested/declared_root_ancestor_pending.jett new file mode 100644 index 00000000..aa79d1a3 --- /dev/null +++ b/tests/native/reflected_nested/declared_root_ancestor_pending.jett @@ -0,0 +1,80 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function root_lower_check(view values: list[Positive]) returns bool: + string marker = "rootlower" + trace marker + return list.length(view values) > 0 +type RootLower = list[Positive] where root_lower_check(view value) +function root_upper_check(view values: list[Positive]) returns bool: + string marker = "rootupper" + trace marker + return list.length(view values) > 0 +type RootUpper = RootLower where root_upper_check(view value) +struct Holder: + payload: RootUpper +enum Event: + content(payload: RootUpper) +machine Session: + states: + content(payload: RootUpper) + empty + transitions: + content to empty +function seed() returns RootUpper: + Positive value = 7 handle error: + return seed() + RootLower lower = list(value) handle error: + return seed() + RootUpper upper = clone lower handle error: + return seed() + return run run upper +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns RootLower: + return type.field_value[Holder, RootLower](view source, view field) +function read_event(view source: Event, view field: TypeField) returns RootLower: + return type.variant_field_value[Event, RootLower](view source, view field) +function read_session(view source: Session, view field: TypeField) returns RootLower: + return type.machine_field_value[Session, RootLower](view source, view field) +function summarize(items: RootLower) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) handle error: + return error + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/declared_root_ancestor_pending.stdout b/tests/native/reflected_nested/declared_root_ancestor_pending.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/declared_root_ancestor_pending.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/inactive_secret_arm_success.debug b/tests/native/reflected_nested/inactive_secret_arm_success.debug new file mode 100644 index 00000000..1c5e2140 --- /dev/null +++ b/tests/native/reflected_nested/inactive_secret_arm_success.debug @@ -0,0 +1,3 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive diff --git a/tests/native/reflected_nested/inactive_secret_arm_success.jett b/tests/native/reflected_nested/inactive_secret_arm_success.jett new file mode 100644 index 00000000..1268b89d --- /dev/null +++ b/tests/native/reflected_nested/inactive_secret_arm_success.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: result[secret[int64], int64] +enum Event: + content(payload: result[secret[int64], int64]) +machine Session: + states: + content(payload: result[secret[int64], int64]) + empty + transitions: + content to empty +function seed() returns result[secret[int64], int64]: + return fail(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[secret[Positive], Positive]: + return type.field_value[Holder, result[secret[Positive], Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[secret[Positive], Positive]: + return type.variant_field_value[Event, result[secret[Positive], Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[secret[Positive], Positive]: + return type.machine_field_value[Session, result[secret[Positive], Positive]](view source, view field) +function summarize(items: result[secret[Positive], Positive]) returns string: + secret[Positive] hidden = items handle error: + return "fail:{coarsen error}" + return "ok" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/inactive_secret_arm_success.stdout b/tests/native/reflected_nested/inactive_secret_arm_success.stdout new file mode 100644 index 00000000..2dc501e2 --- /dev/null +++ b/tests/native/reflected_nested/inactive_secret_arm_success.stdout @@ -0,0 +1,2 @@ +before +after:fail:7|fail:7|fail:7:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/later_occupied_secret_refusal.debug b/tests/native/reflected_nested/later_occupied_secret_refusal.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/later_occupied_secret_refusal.jett b/tests/native/reflected_nested/later_occupied_secret_refusal.jett new file mode 100644 index 00000000..c43c45b7 --- /dev/null +++ b/tests/native/reflected_nested/later_occupied_secret_refusal.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[result[int64, secret[int64]]] +enum Event: + content(payload: list[result[int64, secret[int64]]]) +machine Session: + states: + content(payload: list[result[int64, secret[int64]]]) + empty + transitions: + content to empty +function seed() returns list[result[int64, secret[int64]]]: + secret[int64] hidden = -913 + return list(ok(7), fail(hidden)) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[result[Positive, secret[Positive]]]: + return type.field_value[Holder, list[result[Positive, secret[Positive]]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[result[Positive, secret[Positive]]]: + return type.variant_field_value[Event, list[result[Positive, secret[Positive]]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[result[Positive, secret[Positive]]]: + return type.machine_field_value[Session, list[result[Positive, secret[Positive]]]](view source, view field) +function summarize(items: list[result[Positive, secret[Positive]]]) returns string: + return "unreachable" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/later_occupied_secret_refusal.stdout b/tests/native/reflected_nested/later_occupied_secret_refusal.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/later_occupied_secret_refusal.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_child_pending.debug b/tests/native/reflected_nested/list_child_pending.debug new file mode 100644 index 00000000..0de05abf --- /dev/null +++ b/tests/native/reflected_nested/list_child_pending.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = positive diff --git a/tests/native/reflected_nested/list_child_pending.jett b/tests/native/reflected_nested/list_child_pending.jett new file mode 100644 index 00000000..c91b4bfb --- /dev/null +++ b/tests/native/reflected_nested/list_child_pending.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return list(7, run 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_child_pending.stdout b/tests/native/reflected_nested/list_child_pending.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_child_pending.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_empty_success.debug b/tests/native/reflected_nested/list_empty_success.debug new file mode 100644 index 00000000..5b079991 --- /dev/null +++ b/tests/native/reflected_nested/list_empty_success.debug @@ -0,0 +1,3 @@ +trace record: app.Holder = app.Holder(payload: list()) +trace event: app.Event = app.Event.content(list()) +trace session: app.Session = app.Session@content(list()) diff --git a/tests/native/reflected_nested/list_empty_success.jett b/tests/native/reflected_nested/list_empty_success.jett new file mode 100644 index 00000000..8adc9f60 --- /dev/null +++ b/tests/native/reflected_nested/list_empty_success.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return list() +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_empty_success.stdout b/tests/native/reflected_nested/list_empty_success.stdout new file mode 100644 index 00000000..3cfe5a3e --- /dev/null +++ b/tests/native/reflected_nested/list_empty_success.stdout @@ -0,0 +1,2 @@ +before +after:||:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/list_exact_pending_1.debug b/tests/native/reflected_nested/list_exact_pending_1.debug new file mode 100644 index 00000000..8a7766d3 --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_1.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: list[app.Positive] = pending(list(7)) +trace items: list[app.Positive] = pending(list(7)) +trace items: list[app.Positive] = pending(list(7)) +trace record: app.Holder = app.Holder(payload: pending(list(7))) +trace event: app.Event = app.Event.content(pending(list(7))) +trace session: app.Session = app.Session@content(pending(list(7))) diff --git a/tests/native/reflected_nested/list_exact_pending_1.jett b/tests/native/reflected_nested/list_exact_pending_1.jett new file mode 100644 index 00000000..3c7d7cef --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_1.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[Positive] +enum Event: + content(payload: list[Positive]) +machine Session: + states: + content(payload: list[Positive]) + empty + transitions: + content to empty +function seed() returns list[Positive]: + Positive value = 7 handle error: + return seed() + return run list(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_exact_pending_1.stdout b/tests/native/reflected_nested/list_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/list_exact_pending_2.debug b/tests/native/reflected_nested/list_exact_pending_2.debug new file mode 100644 index 00000000..eb241551 --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_2.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: list[app.Positive] = pending(pending(list(7))) +trace items: list[app.Positive] = pending(pending(list(7))) +trace items: list[app.Positive] = pending(pending(list(7))) +trace record: app.Holder = app.Holder(payload: pending(pending(list(7)))) +trace event: app.Event = app.Event.content(pending(pending(list(7)))) +trace session: app.Session = app.Session@content(pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/list_exact_pending_2.jett b/tests/native/reflected_nested/list_exact_pending_2.jett new file mode 100644 index 00000000..91957486 --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: list[Positive] +enum Event: + content(payload: list[Positive]) +machine Session: + states: + content(payload: list[Positive]) + empty + transitions: + content to empty +function seed() returns list[Positive]: + Positive value = 7 handle error: + return seed() + return run run list(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_exact_pending_2.stdout b/tests/native/reflected_nested/list_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/list_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/list_later_optional_pending.debug b/tests/native/reflected_nested/list_later_optional_pending.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/list_later_optional_pending.jett b/tests/native/reflected_nested/list_later_optional_pending.jett new file mode 100644 index 00000000..56944d5a --- /dev/null +++ b/tests/native/reflected_nested/list_later_optional_pending.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[optional[int64]] +enum Event: + content(payload: list[optional[int64]]) +machine Session: + states: + content(payload: list[optional[int64]]) + empty + transitions: + content to empty +function seed() returns list[optional[int64]]: + return list(some(7), run some(9)) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[optional[Positive]]: + return type.field_value[Holder, list[optional[Positive]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[optional[Positive]]: + return type.variant_field_value[Event, list[optional[Positive]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[optional[Positive]]: + return type.machine_field_value[Session, list[optional[Positive]]](view source, view field) +function summarize(items: list[optional[Positive]]) returns string: + mutable string output = "" + for item in items: + Positive value = item handle: + return "absent" + output = "{output}:{coarsen value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_later_optional_pending.stdout b/tests/native/reflected_nested/list_later_optional_pending.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_later_optional_pending.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_middle_false.debug b/tests/native/reflected_nested/list_middle_false.debug new file mode 100644 index 00000000..0de05abf --- /dev/null +++ b/tests/native/reflected_nested/list_middle_false.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = positive diff --git a/tests/native/reflected_nested/list_middle_false.jett b/tests/native/reflected_nested/list_middle_false.jett new file mode 100644 index 00000000..d55a2565 --- /dev/null +++ b/tests/native/reflected_nested/list_middle_false.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return list(7, -9, 11) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_middle_false.stdout b/tests/native/reflected_nested/list_middle_false.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_middle_false.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_outer_pending_1.debug b/tests/native/reflected_nested/list_outer_pending_1.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/list_outer_pending_1.jett b/tests/native/reflected_nested/list_outer_pending_1.jett new file mode 100644 index 00000000..ca382f87 --- /dev/null +++ b/tests/native/reflected_nested/list_outer_pending_1.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_outer_pending_1.stdout b/tests/native/reflected_nested/list_outer_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_outer_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_outer_pending_2.debug b/tests/native/reflected_nested/list_outer_pending_2.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/list_outer_pending_2.jett b/tests/native/reflected_nested/list_outer_pending_2.jett new file mode 100644 index 00000000..c18320c7 --- /dev/null +++ b/tests/native/reflected_nested/list_outer_pending_2.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_outer_pending_2.stdout b/tests/native/reflected_nested/list_outer_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_outer_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_owned_text_false.debug b/tests/native/reflected_nested/list_owned_text_false.debug new file mode 100644 index 00000000..633792cb --- /dev/null +++ b/tests/native/reflected_nested/list_owned_text_false.debug @@ -0,0 +1,2 @@ +trace marker: string = text +trace marker: string = text diff --git a/tests/native/reflected_nested/list_owned_text_false.jett b/tests/native/reflected_nested/list_owned_text_false.jett new file mode 100644 index 00000000..73310358 --- /dev/null +++ b/tests/native/reflected_nested/list_owned_text_false.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +struct Holder: + payload: list[string] +enum Event: + content(payload: list[string]) +machine Session: + states: + content(payload: list[string]) + empty + transitions: + content to empty +function seed() returns list[string]: + return list("Agent-λ🙂", "") +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Text]: + return type.field_value[Holder, list[Text]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Text]: + return type.variant_field_value[Event, list[Text]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Text]: + return type.machine_field_value[Session, list[Text]](view source, view field) +function summarize(items: list[Text]) returns string: + mutable string output = "" + for item in items: + string value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_owned_text_false.stdout b/tests/native/reflected_nested/list_owned_text_false.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/list_owned_text_false.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/list_ready_success.debug b/tests/native/reflected_nested/list_ready_success.debug new file mode 100644 index 00000000..b03842ea --- /dev/null +++ b/tests/native/reflected_nested/list_ready_success.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: list(7, 9)) +trace event: app.Event = app.Event.content(list(7, 9)) +trace session: app.Session = app.Session@content(list(7, 9)) diff --git a/tests/native/reflected_nested/list_ready_success.jett b/tests/native/reflected_nested/list_ready_success.jett new file mode 100644 index 00000000..dabfa26e --- /dev/null +++ b/tests/native/reflected_nested/list_ready_success.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/list_ready_success.stdout b/tests/native/reflected_nested/list_ready_success.stdout new file mode 100644 index 00000000..792d3093 --- /dev/null +++ b/tests/native/reflected_nested/list_ready_success.stdout @@ -0,0 +1,2 @@ +before +after::7:9|:7:9|:7:9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/map_empty_success.debug b/tests/native/reflected_nested/map_empty_success.debug new file mode 100644 index 00000000..5d63cd5c --- /dev/null +++ b/tests/native/reflected_nested/map_empty_success.debug @@ -0,0 +1,3 @@ +trace record: app.Holder = app.Holder(payload: map()) +trace event: app.Event = app.Event.content(map()) +trace session: app.Session = app.Session@content(map()) diff --git a/tests/native/reflected_nested/map_empty_success.jett b/tests/native/reflected_nested/map_empty_success.jett new file mode 100644 index 00000000..a8863939 --- /dev/null +++ b/tests/native/reflected_nested/map_empty_success.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return map.new[int64, int64]() +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Holder, map[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function summarize(items: map[int64, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_empty_success.stdout b/tests/native/reflected_nested/map_empty_success.stdout new file mode 100644 index 00000000..3cfe5a3e --- /dev/null +++ b/tests/native/reflected_nested/map_empty_success.stdout @@ -0,0 +1,2 @@ +before +after:||:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/map_exact_pending_1.debug b/tests/native/reflected_nested/map_exact_pending_1.debug new file mode 100644 index 00000000..ca446db2 --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_1.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: map[app.Positive, app.Positive] = pending(map(7: 7)) +trace items: map[app.Positive, app.Positive] = pending(map(7: 7)) +trace items: map[app.Positive, app.Positive] = pending(map(7: 7)) +trace record: app.Holder = app.Holder(payload: pending(map(7: 7))) +trace event: app.Event = app.Event.content(pending(map(7: 7))) +trace session: app.Session = app.Session@content(pending(map(7: 7))) diff --git a/tests/native/reflected_nested/map_exact_pending_1.jett b/tests/native/reflected_nested/map_exact_pending_1.jett new file mode 100644 index 00000000..5e7299d5 --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_1.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: map[Positive, Positive] +enum Event: + content(payload: map[Positive, Positive]) +machine Session: + states: + content(payload: map[Positive, Positive]) + empty + transitions: + content to empty +function seed() returns map[Positive, Positive]: + Positive value = 7 handle error: + return seed() + return run map(clone value: value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[Positive, Positive]: + return type.field_value[Holder, map[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[Positive, Positive]: + return type.variant_field_value[Event, map[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[Positive, Positive]: + return type.machine_field_value[Session, map[Positive, Positive]](view source, view field) +function summarize(items: map[Positive, Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_exact_pending_1.stdout b/tests/native/reflected_nested/map_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/map_exact_pending_2.debug b/tests/native/reflected_nested/map_exact_pending_2.debug new file mode 100644 index 00000000..599d43fc --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_2.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: map[app.Positive, app.Positive] = pending(pending(map(7: 7))) +trace items: map[app.Positive, app.Positive] = pending(pending(map(7: 7))) +trace items: map[app.Positive, app.Positive] = pending(pending(map(7: 7))) +trace record: app.Holder = app.Holder(payload: pending(pending(map(7: 7)))) +trace event: app.Event = app.Event.content(pending(pending(map(7: 7)))) +trace session: app.Session = app.Session@content(pending(pending(map(7: 7)))) diff --git a/tests/native/reflected_nested/map_exact_pending_2.jett b/tests/native/reflected_nested/map_exact_pending_2.jett new file mode 100644 index 00000000..ad61db76 --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: map[Positive, Positive] +enum Event: + content(payload: map[Positive, Positive]) +machine Session: + states: + content(payload: map[Positive, Positive]) + empty + transitions: + content to empty +function seed() returns map[Positive, Positive]: + Positive value = 7 handle error: + return seed() + return run run map(clone value: value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[Positive, Positive]: + return type.field_value[Holder, map[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[Positive, Positive]: + return type.variant_field_value[Event, map[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[Positive, Positive]: + return type.machine_field_value[Session, map[Positive, Positive]](view source, view field) +function summarize(items: map[Positive, Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_exact_pending_2.stdout b/tests/native/reflected_nested/map_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/map_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/map_key_value_order_false.debug b/tests/native/reflected_nested/map_key_value_order_false.debug new file mode 100644 index 00000000..38ad3f0c --- /dev/null +++ b/tests/native/reflected_nested/map_key_value_order_false.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive diff --git a/tests/native/reflected_nested/map_key_value_order_false.jett b/tests/native/reflected_nested/map_key_value_order_false.jett new file mode 100644 index 00000000..bcdd1a46 --- /dev/null +++ b/tests/native/reflected_nested/map_key_value_order_false.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return map(1: 7, 2: -9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[Positive, Positive]: + return type.field_value[Holder, map[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[Positive, Positive]: + return type.variant_field_value[Event, map[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[Positive, Positive]: + return type.machine_field_value[Session, map[Positive, Positive]](view source, view field) +function summarize(items: map[Positive, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = coarsen key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_key_value_order_false.stdout b/tests/native/reflected_nested/map_key_value_order_false.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/map_key_value_order_false.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/map_later_optional_pending.debug b/tests/native/reflected_nested/map_later_optional_pending.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/map_later_optional_pending.jett b/tests/native/reflected_nested/map_later_optional_pending.jett new file mode 100644 index 00000000..c085a786 --- /dev/null +++ b/tests/native/reflected_nested/map_later_optional_pending.jett @@ -0,0 +1,68 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: map[int64, optional[int64]] +enum Event: + content(payload: map[int64, optional[int64]]) +machine Session: + states: + content(payload: map[int64, optional[int64]]) + empty + transitions: + content to empty +function seed() returns map[int64, optional[int64]]: + return map(1: some(7), 2: run some(9)) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[Positive, optional[Positive]]: + return type.field_value[Holder, map[Positive, optional[Positive]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[Positive, optional[Positive]]: + return type.variant_field_value[Event, map[Positive, optional[Positive]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[Positive, optional[Positive]]: + return type.machine_field_value[Session, map[Positive, optional[Positive]]](view source, view field) +function summarize(items: map[Positive, optional[Positive]]) returns string: + mutable string output = "" + for key, item in items: + Positive value = item handle: + return "none" + output = "{output}:{coarsen key}={coarsen value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_later_optional_pending.stdout b/tests/native/reflected_nested/map_later_optional_pending.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/map_later_optional_pending.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/map_outer_pending_1.debug b/tests/native/reflected_nested/map_outer_pending_1.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/map_outer_pending_1.jett b/tests/native/reflected_nested/map_outer_pending_1.jett new file mode 100644 index 00000000..3abcb21f --- /dev/null +++ b/tests/native/reflected_nested/map_outer_pending_1.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return run map(1: 7, 2: 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Holder, map[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function summarize(items: map[int64, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_outer_pending_1.stdout b/tests/native/reflected_nested/map_outer_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/map_outer_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/map_outer_pending_2.debug b/tests/native/reflected_nested/map_outer_pending_2.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/map_outer_pending_2.jett b/tests/native/reflected_nested/map_outer_pending_2.jett new file mode 100644 index 00000000..34b83f17 --- /dev/null +++ b/tests/native/reflected_nested/map_outer_pending_2.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return run run map(1: 7, 2: 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Holder, map[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function summarize(items: map[int64, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_outer_pending_2.stdout b/tests/native/reflected_nested/map_outer_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/map_outer_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/map_ready_success.debug b/tests/native/reflected_nested/map_ready_success.debug new file mode 100644 index 00000000..0c68ad20 --- /dev/null +++ b/tests/native/reflected_nested/map_ready_success.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: map(1: 7, 2: 9)) +trace event: app.Event = app.Event.content(map(1: 7, 2: 9)) +trace session: app.Session = app.Session@content(map(1: 7, 2: 9)) diff --git a/tests/native/reflected_nested/map_ready_success.jett b/tests/native/reflected_nested/map_ready_success.jett new file mode 100644 index 00000000..b2a1aded --- /dev/null +++ b/tests/native/reflected_nested/map_ready_success.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return map(1: 7, 2: 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Holder, map[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function summarize(items: map[int64, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/map_ready_success.stdout b/tests/native/reflected_nested/map_ready_success.stdout new file mode 100644 index 00000000..912322fc --- /dev/null +++ b/tests/native/reflected_nested/map_ready_success.stdout @@ -0,0 +1,2 @@ +before +after::1=7:2=9|:1=7:2=9|:1=7:2=9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/operand_order.debug b/tests/native/reflected_nested/operand_order.debug new file mode 100644 index 00000000..b573ed9e --- /dev/null +++ b/tests/native/reflected_nested/operand_order.debug @@ -0,0 +1,15 @@ +trace marker: string = source +trace marker: string = field +trace marker: string = positive +trace marker: string = positive +trace marker: string = source +trace marker: string = field +trace marker: string = positive +trace marker: string = positive +trace marker: string = source +trace marker: string = field +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: list(7, 9)) +trace event: app.Event = app.Event.content(list(7, 9)) +trace session: app.Session = app.Session@content(list(7, 9)) diff --git a/tests/native/reflected_nested/operand_order.jett b/tests/native/reflected_nested/operand_order.jett new file mode 100644 index 00000000..c5072211 --- /dev/null +++ b/tests/native/reflected_nested/operand_order.jett @@ -0,0 +1,77 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function record_operand(view source: Holder) returns Holder: + string marker = "source" + trace marker + return clone source +function event_operand(view source: Event) returns Event: + string marker = "source" + trace marker + return clone source +function session_operand(view source: Session) returns Session: + string marker = "source" + trace marker + return clone source +function field_operand(view field: TypeField) returns TypeField: + string marker = "field" + trace marker + return clone field +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view record_operand(view source), view field_operand(view field)) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view event_operand(view source), view field_operand(view field)) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view session_operand(view source), view field_operand(view field)) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/operand_order.stdout b/tests/native/reflected_nested/operand_order.stdout new file mode 100644 index 00000000..792d3093 --- /dev/null +++ b/tests/native/reflected_nested/operand_order.stdout @@ -0,0 +1,2 @@ +before +after::7:9|:7:9|:7:9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/optional_exact_pending_1.debug b/tests/native/reflected_nested/optional_exact_pending_1.debug new file mode 100644 index 00000000..bf4fc840 --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_1.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: optional[app.Positive] = pending(some(7)) +trace items: optional[app.Positive] = pending(some(7)) +trace items: optional[app.Positive] = pending(some(7)) +trace record: app.Holder = app.Holder(payload: pending(some(7))) +trace event: app.Event = app.Event.content(pending(some(7))) +trace session: app.Session = app.Session@content(pending(some(7))) diff --git a/tests/native/reflected_nested/optional_exact_pending_1.jett b/tests/native/reflected_nested/optional_exact_pending_1.jett new file mode 100644 index 00000000..950b8f39 --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_1.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: optional[Positive] +enum Event: + content(payload: optional[Positive]) +machine Session: + states: + content(payload: optional[Positive]) + empty + transitions: + content to empty +function seed() returns optional[Positive]: + Positive value = 7 handle error: + return seed() + return run some(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_exact_pending_1.stdout b/tests/native/reflected_nested/optional_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/optional_exact_pending_2.debug b/tests/native/reflected_nested/optional_exact_pending_2.debug new file mode 100644 index 00000000..385a8f19 --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_2.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: optional[app.Positive] = pending(pending(some(7))) +trace items: optional[app.Positive] = pending(pending(some(7))) +trace items: optional[app.Positive] = pending(pending(some(7))) +trace record: app.Holder = app.Holder(payload: pending(pending(some(7)))) +trace event: app.Event = app.Event.content(pending(pending(some(7)))) +trace session: app.Session = app.Session@content(pending(pending(some(7)))) diff --git a/tests/native/reflected_nested/optional_exact_pending_2.jett b/tests/native/reflected_nested/optional_exact_pending_2.jett new file mode 100644 index 00000000..694e7d7d --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: optional[Positive] +enum Event: + content(payload: optional[Positive]) +machine Session: + states: + content(payload: optional[Positive]) + empty + transitions: + content to empty +function seed() returns optional[Positive]: + Positive value = 7 handle error: + return seed() + return run run some(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_exact_pending_2.stdout b/tests/native/reflected_nested/optional_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/optional_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/optional_inner_list_pending.debug b/tests/native/reflected_nested/optional_inner_list_pending.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/optional_inner_list_pending.jett b/tests/native/reflected_nested/optional_inner_list_pending.jett new file mode 100644 index 00000000..787825ae --- /dev/null +++ b/tests/native/reflected_nested/optional_inner_list_pending.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: optional[list[int64]] +enum Event: + content(payload: optional[list[int64]]) +machine Session: + states: + content(payload: optional[list[int64]]) + empty + transitions: + content to empty +function seed() returns optional[list[int64]]: + return some(run list(7, 9)) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[list[Positive]]: + return type.field_value[Holder, optional[list[Positive]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[list[Positive]]: + return type.variant_field_value[Event, optional[list[Positive]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[list[Positive]]: + return type.machine_field_value[Session, optional[list[Positive]]](view source, view field) +function summarize(items: optional[list[Positive]]) returns string: + list[Positive] values = items handle: + return "none" + return "{list.length(view values)}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_inner_list_pending.stdout b/tests/native/reflected_nested/optional_inner_list_pending.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/optional_inner_list_pending.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/optional_none_success.debug b/tests/native/reflected_nested/optional_none_success.debug new file mode 100644 index 00000000..aad948fe --- /dev/null +++ b/tests/native/reflected_nested/optional_none_success.debug @@ -0,0 +1,3 @@ +trace record: app.Holder = app.Holder(payload: none) +trace event: app.Event = app.Event.content(none) +trace session: app.Session = app.Session@content(none) diff --git a/tests/native/reflected_nested/optional_none_success.jett b/tests/native/reflected_nested/optional_none_success.jett new file mode 100644 index 00000000..c01f41b5 --- /dev/null +++ b/tests/native/reflected_nested/optional_none_success.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: optional[int64] +enum Event: + content(payload: optional[int64]) +machine Session: + states: + content(payload: optional[int64]) + empty + transitions: + content to empty +function seed() returns optional[int64]: + return none +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + Positive item = items handle: + return "none" + return "{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_none_success.stdout b/tests/native/reflected_nested/optional_none_success.stdout new file mode 100644 index 00000000..9427a09c --- /dev/null +++ b/tests/native/reflected_nested/optional_none_success.stdout @@ -0,0 +1,2 @@ +before +after:none|none|none:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/optional_outer_pending_1.debug b/tests/native/reflected_nested/optional_outer_pending_1.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/optional_outer_pending_1.jett b/tests/native/reflected_nested/optional_outer_pending_1.jett new file mode 100644 index 00000000..2b0d44da --- /dev/null +++ b/tests/native/reflected_nested/optional_outer_pending_1.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: optional[int64] +enum Event: + content(payload: optional[int64]) +machine Session: + states: + content(payload: optional[int64]) + empty + transitions: + content to empty +function seed() returns optional[int64]: + return run some(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + Positive item = items handle: + return "none" + return "{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_outer_pending_1.stdout b/tests/native/reflected_nested/optional_outer_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/optional_outer_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/optional_outer_pending_2.debug b/tests/native/reflected_nested/optional_outer_pending_2.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/optional_outer_pending_2.jett b/tests/native/reflected_nested/optional_outer_pending_2.jett new file mode 100644 index 00000000..bc4e6c71 --- /dev/null +++ b/tests/native/reflected_nested/optional_outer_pending_2.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: optional[int64] +enum Event: + content(payload: optional[int64]) +machine Session: + states: + content(payload: optional[int64]) + empty + transitions: + content to empty +function seed() returns optional[int64]: + return run run some(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + Positive item = items handle: + return "none" + return "{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_outer_pending_2.stdout b/tests/native/reflected_nested/optional_outer_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/optional_outer_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/optional_present_false.debug b/tests/native/reflected_nested/optional_present_false.debug new file mode 100644 index 00000000..ac461ed2 --- /dev/null +++ b/tests/native/reflected_nested/optional_present_false.debug @@ -0,0 +1 @@ +trace marker: string = positive diff --git a/tests/native/reflected_nested/optional_present_false.jett b/tests/native/reflected_nested/optional_present_false.jett new file mode 100644 index 00000000..09c8cb2d --- /dev/null +++ b/tests/native/reflected_nested/optional_present_false.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: optional[int64] +enum Event: + content(payload: optional[int64]) +machine Session: + states: + content(payload: optional[int64]) + empty + transitions: + content to empty +function seed() returns optional[int64]: + return some(-7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + Positive item = items handle: + return "none" + return "{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_present_false.stdout b/tests/native/reflected_nested/optional_present_false.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/optional_present_false.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/optional_ready_success.debug b/tests/native/reflected_nested/optional_ready_success.debug new file mode 100644 index 00000000..4389ffdc --- /dev/null +++ b/tests/native/reflected_nested/optional_ready_success.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: some(7)) +trace event: app.Event = app.Event.content(some(7)) +trace session: app.Session = app.Session@content(some(7)) diff --git a/tests/native/reflected_nested/optional_ready_success.jett b/tests/native/reflected_nested/optional_ready_success.jett new file mode 100644 index 00000000..d20386ef --- /dev/null +++ b/tests/native/reflected_nested/optional_ready_success.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: optional[int64] +enum Event: + content(payload: optional[int64]) +machine Session: + states: + content(payload: optional[int64]) + empty + transitions: + content to empty +function seed() returns optional[int64]: + return some(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns optional[Positive]: + return type.field_value[Holder, optional[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function summarize(items: optional[Positive]) returns string: + Positive item = items handle: + return "none" + return "{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/optional_ready_success.stdout b/tests/native/reflected_nested/optional_ready_success.stdout new file mode 100644 index 00000000..b16b2829 --- /dev/null +++ b/tests/native/reflected_nested/optional_ready_success.stdout @@ -0,0 +1,2 @@ +before +after:7|7|7:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/owned_proof_prefixes.debug b/tests/native/reflected_nested/owned_proof_prefixes.debug new file mode 100644 index 00000000..f5162153 --- /dev/null +++ b/tests/native/reflected_nested/owned_proof_prefixes.debug @@ -0,0 +1,24 @@ +trace marker: string = text +trace marker: string = long +trace marker: string = siblingtext +trace marker: string = text +trace marker: string = long +trace marker: string = long +trace marker: string = text +trace marker: string = long +trace marker: string = text +trace marker: string = long +trace marker: string = siblingtext +trace marker: string = text +trace marker: string = long +trace marker: string = long +trace marker: string = text +trace marker: string = long +trace marker: string = text +trace marker: string = long +trace marker: string = siblingtext +trace marker: string = text +trace marker: string = long +trace marker: string = long +trace marker: string = text +trace marker: string = long diff --git a/tests/native/reflected_nested/owned_proof_prefixes.jett b/tests/native/reflected_nested/owned_proof_prefixes.jett new file mode 100644 index 00000000..fed7a6c5 --- /dev/null +++ b/tests/native/reflected_nested/owned_proof_prefixes.jett @@ -0,0 +1,142 @@ +namespace app +function positive_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where positive_check(value) +function high_check(value: string) returns bool: + string marker = "long" + trace marker + return string.char_count(value) > 2 +type Long = Text where high_check(value) +function sibling_check(value: string) returns bool: + string marker = "siblingtext" + trace marker + return string.char_count(value) >= 0 +type SiblingText = string where sibling_check(value) +struct Holder: + raw: list[string] + positive: list[Text] + high: list[Long] + sibling: list[SiblingText] +enum Event: + first(raw: list[string], positive: list[Text], high: list[Long], sibling: list[SiblingText]) + mirror(sibling: list[SiblingText], high: list[Long], positive: list[Text], raw: list[string]) +machine Session: + states: + first(raw: list[string], positive: list[Text], high: list[Long], sibling: list[SiblingText]) + mirror(sibling: list[SiblingText], high: list[Long], positive: list[Text], raw: list[string]) + transitions: + first to mirror +function positive_seed(value: string) returns Text: + Text ready = value handle error: + return positive_seed("fallback") + return ready +function high_seed(view value: Text) returns Long: + Long ready = clone value handle error: + return high_seed(view value) + return ready +function sibling_seed(value: string) returns SiblingText: + SiblingText ready = value handle error: + return sibling_seed("fallback") + return ready +function record_seed(value: string) returns Holder: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + return Holder(raw: list(value), positive: list(positive), high: list(high), sibling: list(sibling)) +function event_seed(value: string, reversed: bool) returns Event: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + if reversed: + return Event.mirror(list(sibling), list(high), list(positive), list(value)) + return Event.first(list(value), list(positive), list(high), list(sibling)) +function session_seed(value: string, reversed: bool) returns Session: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + if reversed: + return Session(mirror, list(sibling), list(high), list(positive), list(value)) + return Session(first, list(value), list(positive), list(high), list(sibling)) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Holder](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function record_read(view source: Holder, index: int64) returns list[Long]: + TypeField field = record_field(index) + return type.field_value[Holder, list[Long]](view source, view field) +function event_read(view source: Event, index: int64) returns list[Long]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Long]](view source, view field) +function session_read(view source: Session, index: int64) returns list[Long]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Long]](view source, view field) +function record_weaker(view source: Holder) returns list[Text]: + TypeField field = record_field(2) + return type.field_value[Holder, list[Text]](view source, view field) +function event_weaker(view source: Event, index: int64) returns list[Text]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Text]](view source, view field) +function session_weaker(view source: Session, index: int64) returns list[Text]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Text]](view source, view field) +function summarize(items: list[Long]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function summarize_weaker(items: list[Text]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function record_report(value: string) returns string: + Holder source = record_seed(value) + string raw = summarize(record_read(view source, 0)) + string suffix = summarize(record_read(view source, 1)) + string exact = summarize(record_read(view source, 2)) + string sibling = summarize(record_read(view source, 3)) + string ancestor = summarize_weaker(record_weaker(view source)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function event_report(value: string, reversed: bool) returns string: + Event source = event_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(event_read(view source, index)) + string suffix = summarize(event_read(view source, index + step)) + string exact = summarize(event_read(view source, index + step + step)) + string sibling = summarize(event_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(event_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function session_report(value: string, reversed: bool) returns string: + Session source = session_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(session_read(view source, index)) + string suffix = summarize(session_read(view source, index + step)) + string exact = summarize(session_read(view source, index + step + step)) + string sibling = summarize(session_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(session_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function report(value: string, reversed: bool) returns string: + string record = record_report(clone value) + string event = event_report(clone value, reversed) + string session = session_report(value, reversed) + return "{record}|{event}|{session}" +function main(stdout: Stdout) returns nothing: + string value = report("Agent-λ🙂", true) + Stdout.write(view stdout, "proof:{value}\n") diff --git a/tests/native/reflected_nested/owned_proof_prefixes.stdout b/tests/native/reflected_nested/owned_proof_prefixes.stdout new file mode 100644 index 00000000..d72f35fe --- /dev/null +++ b/tests/native/reflected_nested/owned_proof_prefixes.stdout @@ -0,0 +1 @@ +proof::Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂|:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂|:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂 diff --git a/tests/native/reflected_nested/owned_unicode_independence.debug b/tests/native/reflected_nested/owned_unicode_independence.debug new file mode 100644 index 00000000..b9b95766 --- /dev/null +++ b/tests/native/reflected_nested/owned_unicode_independence.debug @@ -0,0 +1,9 @@ +trace marker: string = text +trace marker: string = text +trace marker: string = text +trace marker: string = text +trace marker: string = text +trace marker: string = text +trace record: app.Holder = app.Holder(payload: list(Agent-λ🙂, next)) +trace event: app.Event = app.Event.content(list(Agent-λ🙂, next)) +trace session: app.Session = app.Session@content(list(Agent-λ🙂, next)) diff --git a/tests/native/reflected_nested/owned_unicode_independence.jett b/tests/native/reflected_nested/owned_unicode_independence.jett new file mode 100644 index 00000000..3b77b998 --- /dev/null +++ b/tests/native/reflected_nested/owned_unicode_independence.jett @@ -0,0 +1,67 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +struct Holder: + payload: list[string] +enum Event: + content(payload: list[string]) +machine Session: + states: + content(payload: list[string]) + empty + transitions: + content to empty +function seed() returns list[string]: + return list("Agent-λ🙂", "next") +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Text]: + return type.field_value[Holder, list[Text]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Text]: + return type.variant_field_value[Event, list[Text]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Text]: + return type.machine_field_value[Session, list[Text]](view source, view field) +function summarize(items: list[Text]) returns string: + list[Text] reversed = list.reverse[Text](items) + mutable string output = "" + for item in reversed: + string value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/owned_unicode_independence.stdout b/tests/native/reflected_nested/owned_unicode_independence.stdout new file mode 100644 index 00000000..b8b9d2ed --- /dev/null +++ b/tests/native/reflected_nested/owned_unicode_independence.stdout @@ -0,0 +1,2 @@ +before +after::next:Agent-λ🙂|:next:Agent-λ🙂|:next:Agent-λ🙂:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/proof_prefixes.debug b/tests/native/reflected_nested/proof_prefixes.debug new file mode 100644 index 00000000..52be04ac --- /dev/null +++ b/tests/native/reflected_nested/proof_prefixes.debug @@ -0,0 +1,24 @@ +trace marker: string = positive +trace marker: string = high +trace marker: string = sibling +trace marker: string = positive +trace marker: string = high +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = sibling +trace marker: string = positive +trace marker: string = high +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = positive +trace marker: string = high +trace marker: string = sibling +trace marker: string = positive +trace marker: string = high +trace marker: string = high +trace marker: string = positive +trace marker: string = high diff --git a/tests/native/reflected_nested/proof_prefixes.jett b/tests/native/reflected_nested/proof_prefixes.jett new file mode 100644 index 00000000..c66ee745 --- /dev/null +++ b/tests/native/reflected_nested/proof_prefixes.jett @@ -0,0 +1,142 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) +function sibling_check(value: int64) returns bool: + string marker = "sibling" + trace marker + return value >= 0 +type Sibling = int64 where sibling_check(value) +struct Holder: + raw: list[int64] + positive: list[Positive] + high: list[High] + sibling: list[Sibling] +enum Event: + first(raw: list[int64], positive: list[Positive], high: list[High], sibling: list[Sibling]) + mirror(sibling: list[Sibling], high: list[High], positive: list[Positive], raw: list[int64]) +machine Session: + states: + first(raw: list[int64], positive: list[Positive], high: list[High], sibling: list[Sibling]) + mirror(sibling: list[Sibling], high: list[High], positive: list[Positive], raw: list[int64]) + transitions: + first to mirror +function positive_seed(value: int64) returns Positive: + Positive ready = value handle error: + return positive_seed(value) + return ready +function high_seed(view value: Positive) returns High: + High ready = clone value handle error: + return high_seed(view value) + return ready +function sibling_seed(value: int64) returns Sibling: + Sibling ready = value handle error: + return sibling_seed(value) + return ready +function record_seed(value: int64) returns Holder: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + return Holder(raw: list(value), positive: list(positive), high: list(high), sibling: list(sibling)) +function event_seed(value: int64, reversed: bool) returns Event: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Event.mirror(list(sibling), list(high), list(positive), list(value)) + return Event.first(list(value), list(positive), list(high), list(sibling)) +function session_seed(value: int64, reversed: bool) returns Session: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Session(mirror, list(sibling), list(high), list(positive), list(value)) + return Session(first, list(value), list(positive), list(high), list(sibling)) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Holder](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function record_read(view source: Holder, index: int64) returns list[High]: + TypeField field = record_field(index) + return type.field_value[Holder, list[High]](view source, view field) +function event_read(view source: Event, index: int64) returns list[High]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[High]](view source, view field) +function session_read(view source: Session, index: int64) returns list[High]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[High]](view source, view field) +function record_weaker(view source: Holder) returns list[Positive]: + TypeField field = record_field(2) + return type.field_value[Holder, list[Positive]](view source, view field) +function event_weaker(view source: Event, index: int64) returns list[Positive]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Positive]](view source, view field) +function session_weaker(view source: Session, index: int64) returns list[Positive]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[High]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function summarize_weaker(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function record_report(value: int64) returns string: + Holder source = record_seed(value) + string raw = summarize(record_read(view source, 0)) + string suffix = summarize(record_read(view source, 1)) + string exact = summarize(record_read(view source, 2)) + string sibling = summarize(record_read(view source, 3)) + string ancestor = summarize_weaker(record_weaker(view source)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function event_report(value: int64, reversed: bool) returns string: + Event source = event_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(event_read(view source, index)) + string suffix = summarize(event_read(view source, index + step)) + string exact = summarize(event_read(view source, index + step + step)) + string sibling = summarize(event_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(event_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function session_report(value: int64, reversed: bool) returns string: + Session source = session_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(session_read(view source, index)) + string suffix = summarize(session_read(view source, index + step)) + string exact = summarize(session_read(view source, index + step + step)) + string sibling = summarize(session_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(session_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function report(value: int64, reversed: bool) returns string: + string record = record_report(value) + string event = event_report(value, reversed) + string session = session_report(value, reversed) + return "{record}|{event}|{session}" +function main(stdout: Stdout) returns nothing: + string value = report(12, true) + Stdout.write(view stdout, "proof:{value}\n") diff --git a/tests/native/reflected_nested/proof_prefixes.stdout b/tests/native/reflected_nested/proof_prefixes.stdout new file mode 100644 index 00000000..2d90f54e --- /dev/null +++ b/tests/native/reflected_nested/proof_prefixes.stdout @@ -0,0 +1 @@ +proof::12/:12/:12/:12/:12|:12/:12/:12/:12/:12|:12/:12/:12/:12/:12 diff --git a/tests/native/reflected_nested/pure_owned_proof_prefixes.debug b/tests/native/reflected_nested/pure_owned_proof_prefixes.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/pure_owned_proof_prefixes.jett b/tests/native/reflected_nested/pure_owned_proof_prefixes.jett new file mode 100644 index 00000000..698f7ee3 --- /dev/null +++ b/tests/native/reflected_nested/pure_owned_proof_prefixes.jett @@ -0,0 +1,151 @@ +namespace app +function positive_check(value: string) returns bool: + string marker = "text" + return string.char_count(value) > 0 +type Text = string where positive_check(value) +function high_check(value: string) returns bool: + string marker = "long" + return string.char_count(value) > 2 +type Long = Text where high_check(value) +function sibling_check(value: string) returns bool: + string marker = "siblingtext" + return string.char_count(value) >= 0 +type SiblingText = string where sibling_check(value) +struct Holder: + raw: list[string] + positive: list[Text] + high: list[Long] + sibling: list[SiblingText] +enum Event: + first(raw: list[string], positive: list[Text], high: list[Long], sibling: list[SiblingText]) + mirror(sibling: list[SiblingText], high: list[Long], positive: list[Text], raw: list[string]) +machine Session: + states: + first(raw: list[string], positive: list[Text], high: list[Long], sibling: list[SiblingText]) + mirror(sibling: list[SiblingText], high: list[Long], positive: list[Text], raw: list[string]) + transitions: + first to mirror +function positive_seed(value: string) returns Text: + Text ready = value handle error: + return positive_seed("fallback") + return ready +function high_seed(view value: Text) returns Long: + Long ready = clone value handle error: + return high_seed(view value) + return ready +function sibling_seed(value: string) returns SiblingText: + SiblingText ready = value handle error: + return sibling_seed("fallback") + return ready +function record_seed(value: string) returns Holder: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + return Holder(raw: list(value), positive: list(positive), high: list(high), sibling: list(sibling)) +function event_seed(value: string, reversed: bool) returns Event: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + if reversed: + return Event.mirror(list(sibling), list(high), list(positive), list(value)) + return Event.first(list(value), list(positive), list(high), list(sibling)) +function session_seed(value: string, reversed: bool) returns Session: + Text positive = positive_seed(clone value) + Long high = high_seed(view positive) + SiblingText sibling = sibling_seed(clone value) + if reversed: + return Session(mirror, list(sibling), list(high), list(positive), list(value)) + return Session(first, list(value), list(positive), list(high), list(sibling)) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Holder](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function record_read(view source: Holder, index: int64) returns list[Long]: + TypeField field = record_field(index) + return type.field_value[Holder, list[Long]](view source, view field) +function event_read(view source: Event, index: int64) returns list[Long]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Long]](view source, view field) +function session_read(view source: Session, index: int64) returns list[Long]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Long]](view source, view field) +function record_weaker(view source: Holder) returns list[Text]: + TypeField field = record_field(2) + return type.field_value[Holder, list[Text]](view source, view field) +function event_weaker(view source: Event, index: int64) returns list[Text]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Text]](view source, view field) +function session_weaker(view source: Session, index: int64) returns list[Text]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Text]](view source, view field) +function summarize(items: list[Long]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function summarize_weaker(items: list[Text]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function record_report(value: string) returns string: + Holder source = record_seed(value) + string raw = summarize(record_read(view source, 0)) + string suffix = summarize(record_read(view source, 1)) + string exact = summarize(record_read(view source, 2)) + string sibling = summarize(record_read(view source, 3)) + string ancestor = summarize_weaker(record_weaker(view source)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function event_report(value: string, reversed: bool) returns string: + Event source = event_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(event_read(view source, index)) + string suffix = summarize(event_read(view source, index + step)) + string exact = summarize(event_read(view source, index + step + step)) + string sibling = summarize(event_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(event_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function session_report(value: string, reversed: bool) returns string: + Session source = session_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(session_read(view source, index)) + string suffix = summarize(session_read(view source, index + step)) + string exact = summarize(session_read(view source, index + step + step)) + string sibling = summarize(session_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(session_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function report(value: string, reversed: bool) returns string: + string record = record_report(clone value) + string event = event_report(clone value, reversed) + string session = session_report(value, reversed) + return "{record}|{event}|{session}" +function expected(value: string) returns string: + string part = ":{value}/:{value}/:{value}/:{value}/:{value}" + return "{part}|{part}|{part}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report("Agent-λ🙂", true) + Stdout.write(view stdout, "pure:{baked}\n") +verify reflected_proofs: + assert report("Agent-λ🙂", true) == expected("Agent-λ🙂") +property reflected_proof_trials: + given other: bool + given reversed: bool + mutable string value = "Agent-λ🙂" + if other: + value = "other-λ🙂" + assert report(clone value, reversed) == expected(value) diff --git a/tests/native/reflected_nested/pure_owned_proof_prefixes.stdout b/tests/native/reflected_nested/pure_owned_proof_prefixes.stdout new file mode 100644 index 00000000..fde21f64 --- /dev/null +++ b/tests/native/reflected_nested/pure_owned_proof_prefixes.stdout @@ -0,0 +1 @@ +pure::Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂|:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂|:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂/:Agent-λ🙂 diff --git a/tests/native/reflected_nested/pure_proof_prefixes.debug b/tests/native/reflected_nested/pure_proof_prefixes.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/pure_proof_prefixes.jett b/tests/native/reflected_nested/pure_proof_prefixes.jett new file mode 100644 index 00000000..e62f59dd --- /dev/null +++ b/tests/native/reflected_nested/pure_proof_prefixes.jett @@ -0,0 +1,151 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + return value > 10 +type High = Positive where high_check(value) +function sibling_check(value: int64) returns bool: + string marker = "sibling" + return value >= 0 +type Sibling = int64 where sibling_check(value) +struct Holder: + raw: list[int64] + positive: list[Positive] + high: list[High] + sibling: list[Sibling] +enum Event: + first(raw: list[int64], positive: list[Positive], high: list[High], sibling: list[Sibling]) + mirror(sibling: list[Sibling], high: list[High], positive: list[Positive], raw: list[int64]) +machine Session: + states: + first(raw: list[int64], positive: list[Positive], high: list[High], sibling: list[Sibling]) + mirror(sibling: list[Sibling], high: list[High], positive: list[Positive], raw: list[int64]) + transitions: + first to mirror +function positive_seed(value: int64) returns Positive: + Positive ready = value handle error: + return positive_seed(value) + return ready +function high_seed(view value: Positive) returns High: + High ready = clone value handle error: + return high_seed(view value) + return ready +function sibling_seed(value: int64) returns Sibling: + Sibling ready = value handle error: + return sibling_seed(value) + return ready +function record_seed(value: int64) returns Holder: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + return Holder(raw: list(value), positive: list(positive), high: list(high), sibling: list(sibling)) +function event_seed(value: int64, reversed: bool) returns Event: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Event.mirror(list(sibling), list(high), list(positive), list(value)) + return Event.first(list(value), list(positive), list(high), list(sibling)) +function session_seed(value: int64, reversed: bool) returns Session: + Positive positive = positive_seed(value) + High high = high_seed(view positive) + Sibling sibling = sibling_seed(value) + if reversed: + return Session(mirror, list(sibling), list(high), list(positive), list(value)) + return Session(first, list(value), list(positive), list(high), list(sibling)) +function record_field(index: int64) returns TypeField: + return list.get[TypeField](type.fields[Holder](), index) handle: + return record_field(0) +function event_field(view source: Event, index: int64) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, index) handle: + return event_field(view source, 0) +function session_field(view source: Session, index: int64) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, index) handle: + return session_field(view source, 0) +function record_read(view source: Holder, index: int64) returns list[High]: + TypeField field = record_field(index) + return type.field_value[Holder, list[High]](view source, view field) +function event_read(view source: Event, index: int64) returns list[High]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[High]](view source, view field) +function session_read(view source: Session, index: int64) returns list[High]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[High]](view source, view field) +function record_weaker(view source: Holder) returns list[Positive]: + TypeField field = record_field(2) + return type.field_value[Holder, list[Positive]](view source, view field) +function event_weaker(view source: Event, index: int64) returns list[Positive]: + TypeField field = event_field(view source, index) + return type.variant_field_value[Event, list[Positive]](view source, view field) +function session_weaker(view source: Session, index: int64) returns list[Positive]: + TypeField field = session_field(view source, index) + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[High]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function summarize_weaker(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + output = "{output}:{coarsen item}" + return output +function record_report(value: int64) returns string: + Holder source = record_seed(value) + string raw = summarize(record_read(view source, 0)) + string suffix = summarize(record_read(view source, 1)) + string exact = summarize(record_read(view source, 2)) + string sibling = summarize(record_read(view source, 3)) + string ancestor = summarize_weaker(record_weaker(view source)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function event_report(value: int64, reversed: bool) returns string: + Event source = event_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(event_read(view source, index)) + string suffix = summarize(event_read(view source, index + step)) + string exact = summarize(event_read(view source, index + step + step)) + string sibling = summarize(event_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(event_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function session_report(value: int64, reversed: bool) returns string: + Session source = session_seed(value, reversed) + mutable int64 step = 1 + mutable int64 index = 0 + if reversed: + step = -1 + index = 3 + string raw = summarize(session_read(view source, index)) + string suffix = summarize(session_read(view source, index + step)) + string exact = summarize(session_read(view source, index + step + step)) + string sibling = summarize(session_read(view source, index + step + step + step)) + string ancestor = summarize_weaker(session_weaker(view source, index + step + step)) + return "{raw}/{suffix}/{exact}/{sibling}/{ancestor}" +function report(value: int64, reversed: bool) returns string: + string record = record_report(value) + string event = event_report(value, reversed) + string session = session_report(value, reversed) + return "{record}|{event}|{session}" +function expected(value: int64) returns string: + string part = ":{value}/:{value}/:{value}/:{value}/:{value}" + return "{part}|{part}|{part}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(12, true) + Stdout.write(view stdout, "pure:{baked}\n") +verify reflected_proofs: + assert report(12, true) == expected(12) +property reflected_proof_trials: + given large: bool + given reversed: bool + mutable int64 value = 12 + if large: + value = 127 + assert report(value, reversed) == expected(value) diff --git a/tests/native/reflected_nested/pure_proof_prefixes.stdout b/tests/native/reflected_nested/pure_proof_prefixes.stdout new file mode 100644 index 00000000..0adca1ad --- /dev/null +++ b/tests/native/reflected_nested/pure_proof_prefixes.stdout @@ -0,0 +1 @@ +pure::12/:12/:12/:12/:12|:12/:12/:12/:12/:12|:12/:12/:12/:12/:12 diff --git a/tests/native/reflected_nested/pure_recursive_wrappers.debug b/tests/native/reflected_nested/pure_recursive_wrappers.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/pure_recursive_wrappers.jett b/tests/native/reflected_nested/pure_recursive_wrappers.jett new file mode 100644 index 00000000..073ed15a --- /dev/null +++ b/tests/native/reflected_nested/pure_recursive_wrappers.jett @@ -0,0 +1,99 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + return value > 0 +type Positive = int64 where positive_check(value) +function text_check(value: string) returns bool: + string marker = "text" + return string.char_count(value) > 0 +type Text = string where text_check(value) +struct Holder: + payload: list[optional[result[map[int64, list[string]], set[int64]]]] +enum Event: + content(payload: list[optional[result[map[int64, list[string]], set[int64]]]]) +machine Session: + states: + content(payload: list[optional[result[map[int64, list[string]], set[int64]]]]) + empty + transitions: + content to empty +function seed(filled: bool) returns list[optional[result[map[int64, list[string]], set[int64]]]]: + if not filled: + return list.new[optional[result[map[int64, list[string]], set[int64]]]]() + mutable set[int64] failed = set.new[int64]() + failed = set.add[int64](failed, 7) + failed = set.add[int64](failed, 9) + return list(none, some(ok(map(1: list("Agent-λ🙂", "next")))), some(fail(failed))) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.field_value[Holder, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.variant_field_value[Event, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.machine_field_value[Session, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function summarize_text(items: list[Text]) returns string: + list[Text] reversed = list.reverse[Text](items) + mutable string output = "" + for item in reversed: + output = "{output}/{coarsen item}" + return output +function summarize_map(items: map[Positive, list[Text]]) returns string: + mutable string output = "ok" + for key, value in items: + string text = summarize_text(value) + output = "{output}:{coarsen key}={text}" + return output +function summarize_set(items: set[Positive]) returns string: + mutable string output = "fail" + for value in items: + output = "{output}:{coarsen value}" + return output +function summarize_result(item: result[map[Positive, list[Text]], set[Positive]]) returns string: + map[Positive, list[Text]] success = item handle error: + return summarize_set(error) + return summarize_map(success) +function summarize_optional(item: optional[result[map[Positive, list[Text]], set[Positive]]]) returns string: + result[map[Positive, list[Text]], set[Positive]] occupied = item handle: + return "none" + return summarize_result(occupied) +function summarize(items: list[optional[result[map[Positive, list[Text]], set[Positive]]]]) returns string: + mutable string output = "" + for item in items: + string part = summarize_optional(item) + output = "{output}[{part}]" + return output +function report(filled: bool) returns string: + Holder record = Holder(payload: seed(filled)) + Event event = Event.content(seed(filled)) + Session session = Session(content, seed(filled)) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + return "{first}|{second}|{third}" +function expected(filled: bool) returns string: + if not filled: + return "||" + string part = "[none][ok:1=/next/Agent-λ🙂][fail:7:9]" + return "{part}|{part}|{part}" +function main(stdout: Stdout) returns nothing: + string baked = comptime report(true) + Stdout.write(view stdout, "pure:{baked}\n") +verify reflected_wrappers: + assert report(true) == expected(true) + assert report(false) == expected(false) +property reflected_wrapper_trials: + given filled: bool + assert report(filled) == expected(filled) diff --git a/tests/native/reflected_nested/pure_recursive_wrappers.stdout b/tests/native/reflected_nested/pure_recursive_wrappers.stdout new file mode 100644 index 00000000..15b80f12 --- /dev/null +++ b/tests/native/reflected_nested/pure_recursive_wrappers.stdout @@ -0,0 +1 @@ +pure:[none][ok:1=/next/Agent-λ🙂][fail:7:9]|[none][ok:1=/next/Agent-λ🙂][fail:7:9]|[none][ok:1=/next/Agent-λ🙂][fail:7:9] diff --git a/tests/native/reflected_nested/recursive_wrappers.debug b/tests/native/reflected_nested/recursive_wrappers.debug new file mode 100644 index 00000000..72ad99ff --- /dev/null +++ b/tests/native/reflected_nested/recursive_wrappers.debug @@ -0,0 +1,18 @@ +trace marker: string = positive +trace marker: string = text +trace marker: string = text +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = text +trace marker: string = text +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = text +trace marker: string = text +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: list(none, some(ok(map(1: list(Agent-λ🙂, next)))), some(fail(set(7, 9))))) +trace event: app.Event = app.Event.content(list(none, some(ok(map(1: list(Agent-λ🙂, next)))), some(fail(set(7, 9))))) +trace session: app.Session = app.Session@content(list(none, some(ok(map(1: list(Agent-λ🙂, next)))), some(fail(set(7, 9))))) diff --git a/tests/native/reflected_nested/recursive_wrappers.jett b/tests/native/reflected_nested/recursive_wrappers.jett new file mode 100644 index 00000000..f7157317 --- /dev/null +++ b/tests/native/reflected_nested/recursive_wrappers.jett @@ -0,0 +1,94 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function text_check(value: string) returns bool: + string marker = "text" + trace marker + return string.char_count(value) > 0 +type Text = string where text_check(value) +struct Holder: + payload: list[optional[result[map[int64, list[string]], set[int64]]]] +enum Event: + content(payload: list[optional[result[map[int64, list[string]], set[int64]]]]) +machine Session: + states: + content(payload: list[optional[result[map[int64, list[string]], set[int64]]]]) + empty + transitions: + content to empty +function seed() returns list[optional[result[map[int64, list[string]], set[int64]]]]: + mutable set[int64] failed = set.new[int64]() + failed = set.add[int64](failed, 7) + failed = set.add[int64](failed, 9) + return list(none, some(ok(map(1: list("Agent-λ🙂", "next")))), some(fail(failed))) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.field_value[Holder, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.variant_field_value[Event, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[optional[result[map[Positive, list[Text]], set[Positive]]]]: + return type.machine_field_value[Session, list[optional[result[map[Positive, list[Text]], set[Positive]]]]](view source, view field) +function summarize_text(items: list[Text]) returns string: + list[Text] reversed = list.reverse[Text](items) + mutable string output = "" + for item in reversed: + output = "{output}/{coarsen item}" + return output +function summarize_map(items: map[Positive, list[Text]]) returns string: + mutable string output = "ok" + for key, value in items: + string text = summarize_text(value) + output = "{output}:{coarsen key}={text}" + return output +function summarize_set(items: set[Positive]) returns string: + mutable string output = "fail" + for value in items: + output = "{output}:{coarsen value}" + return output +function summarize_result(item: result[map[Positive, list[Text]], set[Positive]]) returns string: + map[Positive, list[Text]] success = item handle error: + return summarize_set(error) + return summarize_map(success) +function summarize_optional(item: optional[result[map[Positive, list[Text]], set[Positive]]]) returns string: + result[map[Positive, list[Text]], set[Positive]] occupied = item handle: + return "none" + return summarize_result(occupied) +function summarize(items: list[optional[result[map[Positive, list[Text]], set[Positive]]]]) returns string: + mutable string output = "" + for item in items: + string part = summarize_optional(item) + output = "{output}[{part}]" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/recursive_wrappers.stdout b/tests/native/reflected_nested/recursive_wrappers.stdout new file mode 100644 index 00000000..af40a838 --- /dev/null +++ b/tests/native/reflected_nested/recursive_wrappers.stdout @@ -0,0 +1,2 @@ +before +after:[none][ok:1=/next/Agent-λ🙂][fail:7:9]|[none][ok:1=/next/Agent-λ🙂][fail:7:9]|[none][ok:1=/next/Agent-λ🙂][fail:7:9]:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/repeated_map_refinement_comparator_control.debug b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.debug new file mode 100644 index 00000000..d866bb4f --- /dev/null +++ b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.debug @@ -0,0 +1,15 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: map(1: 7, 2: 9)) +trace event: app.Event = app.Event.content(map(1: 7, 2: 9)) +trace session: app.Session = app.Session@content(map(1: 7, 2: 9)) diff --git a/tests/native/reflected_nested/repeated_map_refinement_comparator_control.jett b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.jett new file mode 100644 index 00000000..6b0ba3b9 --- /dev/null +++ b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: map[int64, int64] +enum Event: + content(payload: map[int64, int64]) +machine Session: + states: + content(payload: map[int64, int64]) + empty + transitions: + content to empty +function seed() returns map[int64, int64]: + return map(1: 7, 2: 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns map[Positive, Positive]: + return type.field_value[Holder, map[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns map[Positive, Positive]: + return type.variant_field_value[Event, map[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns map[Positive, Positive]: + return type.machine_field_value[Session, map[Positive, Positive]](view source, view field) +function summarize(items: map[Positive, Positive]) returns string: + mutable string output = "" + for key, item in items: + int64 public_key = coarsen key + int64 public_item = coarsen item + output = "{output}:{public_key}={public_item}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/repeated_map_refinement_comparator_control.stdout b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.stdout new file mode 100644 index 00000000..912322fc --- /dev/null +++ b/tests/native/reflected_nested/repeated_map_refinement_comparator_control.stdout @@ -0,0 +1,2 @@ +before +after::1=7:2=9|:1=7:2=9|:1=7:2=9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/repeated_result_refinement_comparator_control.debug b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.debug new file mode 100644 index 00000000..3d17e96b --- /dev/null +++ b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: ok(7)) +trace event: app.Event = app.Event.content(ok(7)) +trace session: app.Session = app.Session@content(ok(7)) diff --git a/tests/native/reflected_nested/repeated_result_refinement_comparator_control.jett b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.jett new file mode 100644 index 00000000..0a1d2562 --- /dev/null +++ b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return ok(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, Positive]: + return type.field_value[Holder, result[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, Positive]: + return type.variant_field_value[Event, result[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, Positive]: + return type.machine_field_value[Session, result[Positive, Positive]](view source, view field) +function summarize(items: result[Positive, Positive]) returns string: + Positive item = items handle error: + return "fail:{coarsen error}" + return "ok:{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/repeated_result_refinement_comparator_control.stdout b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.stdout new file mode 100644 index 00000000..c891dee7 --- /dev/null +++ b/tests/native/reflected_nested/repeated_result_refinement_comparator_control.stdout @@ -0,0 +1,2 @@ +before +after:ok:7|ok:7|ok:7:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/result_exact_pending_1.debug b/tests/native/reflected_nested/result_exact_pending_1.debug new file mode 100644 index 00000000..9dbae2e3 --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_1.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: result[app.Positive, app.Positive] = pending(ok(7)) +trace items: result[app.Positive, app.Positive] = pending(ok(7)) +trace items: result[app.Positive, app.Positive] = pending(ok(7)) +trace record: app.Holder = app.Holder(payload: pending(ok(7))) +trace event: app.Event = app.Event.content(pending(ok(7))) +trace session: app.Session = app.Session@content(pending(ok(7))) diff --git a/tests/native/reflected_nested/result_exact_pending_1.jett b/tests/native/reflected_nested/result_exact_pending_1.jett new file mode 100644 index 00000000..d7658351 --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_1.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: result[Positive, Positive] +enum Event: + content(payload: result[Positive, Positive]) +machine Session: + states: + content(payload: result[Positive, Positive]) + empty + transitions: + content to empty +function seed() returns result[Positive, Positive]: + Positive value = 7 handle error: + return seed() + return run ok(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, Positive]: + return type.field_value[Holder, result[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, Positive]: + return type.variant_field_value[Event, result[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, Positive]: + return type.machine_field_value[Session, result[Positive, Positive]](view source, view field) +function summarize(items: result[Positive, Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_exact_pending_1.stdout b/tests/native/reflected_nested/result_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/result_exact_pending_2.debug b/tests/native/reflected_nested/result_exact_pending_2.debug new file mode 100644 index 00000000..6a0df509 --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_2.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: result[app.Positive, app.Positive] = pending(pending(ok(7))) +trace items: result[app.Positive, app.Positive] = pending(pending(ok(7))) +trace items: result[app.Positive, app.Positive] = pending(pending(ok(7))) +trace record: app.Holder = app.Holder(payload: pending(pending(ok(7)))) +trace event: app.Event = app.Event.content(pending(pending(ok(7)))) +trace session: app.Session = app.Session@content(pending(pending(ok(7)))) diff --git a/tests/native/reflected_nested/result_exact_pending_2.jett b/tests/native/reflected_nested/result_exact_pending_2.jett new file mode 100644 index 00000000..29324b6f --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: result[Positive, Positive] +enum Event: + content(payload: result[Positive, Positive]) +machine Session: + states: + content(payload: result[Positive, Positive]) + empty + transitions: + content to empty +function seed() returns result[Positive, Positive]: + Positive value = 7 handle error: + return seed() + return run run ok(value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, Positive]: + return type.field_value[Holder, result[Positive, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, Positive]: + return type.variant_field_value[Event, result[Positive, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, Positive]: + return type.machine_field_value[Session, result[Positive, Positive]](view source, view field) +function summarize(items: result[Positive, Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_exact_pending_2.stdout b/tests/native/reflected_nested/result_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/result_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/result_fail_success.debug b/tests/native/reflected_nested/result_fail_success.debug new file mode 100644 index 00000000..c5169008 --- /dev/null +++ b/tests/native/reflected_nested/result_fail_success.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: fail(9)) +trace event: app.Event = app.Event.content(fail(9)) +trace session: app.Session = app.Session@content(fail(9)) diff --git a/tests/native/reflected_nested/result_fail_success.jett b/tests/native/reflected_nested/result_fail_success.jett new file mode 100644 index 00000000..a23523fe --- /dev/null +++ b/tests/native/reflected_nested/result_fail_success.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return fail(9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[int64, Positive]: + return type.field_value[Holder, result[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[int64, Positive]: + return type.variant_field_value[Event, result[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[int64, Positive]: + return type.machine_field_value[Session, result[int64, Positive]](view source, view field) +function summarize(items: result[int64, Positive]) returns string: + int64 item = items handle error: + return "fail:{coarsen error}" + return "ok:{item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_fail_success.stdout b/tests/native/reflected_nested/result_fail_success.stdout new file mode 100644 index 00000000..d8a8a5f6 --- /dev/null +++ b/tests/native/reflected_nested/result_fail_success.stdout @@ -0,0 +1,2 @@ +before +after:fail:9|fail:9|fail:9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/result_inner_map_pending.debug b/tests/native/reflected_nested/result_inner_map_pending.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/result_inner_map_pending.jett b/tests/native/reflected_nested/result_inner_map_pending.jett new file mode 100644 index 00000000..65929e4f --- /dev/null +++ b/tests/native/reflected_nested/result_inner_map_pending.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: result[map[int64, int64], int64] +enum Event: + content(payload: result[map[int64, int64], int64]) +machine Session: + states: + content(payload: result[map[int64, int64], int64]) + empty + transitions: + content to empty +function seed() returns result[map[int64, int64], int64]: + return ok(run map(1: 7, 2: 9)) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[map[int64, Positive], int64]: + return type.field_value[Holder, result[map[int64, Positive], int64]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[map[int64, Positive], int64]: + return type.variant_field_value[Event, result[map[int64, Positive], int64]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[map[int64, Positive], int64]: + return type.machine_field_value[Session, result[map[int64, Positive], int64]](view source, view field) +function summarize(items: result[map[int64, Positive], int64]) returns string: + map[int64, Positive] values = items handle error: + return "fail:{error}" + return "{map.length(view values)}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_inner_map_pending.stdout b/tests/native/reflected_nested/result_inner_map_pending.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/result_inner_map_pending.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/result_occupied_false.debug b/tests/native/reflected_nested/result_occupied_false.debug new file mode 100644 index 00000000..ac461ed2 --- /dev/null +++ b/tests/native/reflected_nested/result_occupied_false.debug @@ -0,0 +1 @@ +trace marker: string = positive diff --git a/tests/native/reflected_nested/result_occupied_false.jett b/tests/native/reflected_nested/result_occupied_false.jett new file mode 100644 index 00000000..10849f77 --- /dev/null +++ b/tests/native/reflected_nested/result_occupied_false.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return fail(-9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[int64, Positive]: + return type.field_value[Holder, result[int64, Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[int64, Positive]: + return type.variant_field_value[Event, result[int64, Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[int64, Positive]: + return type.machine_field_value[Session, result[int64, Positive]](view source, view field) +function summarize(items: result[int64, Positive]) returns string: + int64 item = items handle error: + return "fail:{coarsen error}" + return "ok:{item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_occupied_false.stdout b/tests/native/reflected_nested/result_occupied_false.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/result_occupied_false.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/result_ok_success.debug b/tests/native/reflected_nested/result_ok_success.debug new file mode 100644 index 00000000..3d17e96b --- /dev/null +++ b/tests/native/reflected_nested/result_ok_success.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: ok(7)) +trace event: app.Event = app.Event.content(ok(7)) +trace session: app.Session = app.Session@content(ok(7)) diff --git a/tests/native/reflected_nested/result_ok_success.jett b/tests/native/reflected_nested/result_ok_success.jett new file mode 100644 index 00000000..ebc543de --- /dev/null +++ b/tests/native/reflected_nested/result_ok_success.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return ok(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Holder, result[Positive, int64]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function summarize(items: result[Positive, int64]) returns string: + Positive item = items handle error: + return "fail:{error}" + return "ok:{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_ok_success.stdout b/tests/native/reflected_nested/result_ok_success.stdout new file mode 100644 index 00000000..c891dee7 --- /dev/null +++ b/tests/native/reflected_nested/result_ok_success.stdout @@ -0,0 +1,2 @@ +before +after:ok:7|ok:7|ok:7:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/result_outer_pending_1.debug b/tests/native/reflected_nested/result_outer_pending_1.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/result_outer_pending_1.jett b/tests/native/reflected_nested/result_outer_pending_1.jett new file mode 100644 index 00000000..c5d45fbd --- /dev/null +++ b/tests/native/reflected_nested/result_outer_pending_1.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return run ok(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Holder, result[Positive, int64]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function summarize(items: result[Positive, int64]) returns string: + Positive item = items handle error: + return "fail:{error}" + return "ok:{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_outer_pending_1.stdout b/tests/native/reflected_nested/result_outer_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/result_outer_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/result_outer_pending_2.debug b/tests/native/reflected_nested/result_outer_pending_2.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/result_outer_pending_2.jett b/tests/native/reflected_nested/result_outer_pending_2.jett new file mode 100644 index 00000000..c478630e --- /dev/null +++ b/tests/native/reflected_nested/result_outer_pending_2.jett @@ -0,0 +1,59 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: result[int64, int64] +enum Event: + content(payload: result[int64, int64]) +machine Session: + states: + content(payload: result[int64, int64]) + empty + transitions: + content to empty +function seed() returns result[int64, int64]: + return run run ok(7) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Holder, result[Positive, int64]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function summarize(items: result[Positive, int64]) returns string: + Positive item = items handle error: + return "fail:{error}" + return "ok:{coarsen item}" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/result_outer_pending_2.stdout b/tests/native/reflected_nested/result_outer_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/result_outer_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_list_depth0.debug b/tests/native/reflected_nested/root_base_list_depth0.debug new file mode 100644 index 00000000..18bb2c24 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth0.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Positive] = list(7) +trace source: app.Record = app.Record(values: list(7)) diff --git a/tests/native/reflected_nested/root_base_list_depth0.jett b/tests/native/reflected_nested/root_base_list_depth0.jett new file mode 100644 index 00000000..ab20796a --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth0.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Positive]: + return type.field_value[Record, list[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Positive]: + return source into view type.field_value[Record, list[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Positive]: + return source into view type.variant_field_value[Event, list[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Positive]: + return source into view type.machine_field_value[Session, list[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_list_depth0.stdout b/tests/native/reflected_nested/root_base_list_depth0.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth0.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_base_list_depth1.debug b/tests/native/reflected_nested/root_base_list_depth1.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth1.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_base_list_depth1.jett b/tests/native/reflected_nested/root_base_list_depth1.jett new file mode 100644 index 00000000..d6627b06 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth1.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Positive]: + return type.field_value[Record, list[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Positive]: + return source into view type.field_value[Record, list[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Positive]: + return source into view type.variant_field_value[Event, list[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Positive]: + return source into view type.machine_field_value[Session, list[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_list_depth1.stdout b/tests/native/reflected_nested/root_base_list_depth1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_list_depth2.debug b/tests/native/reflected_nested/root_base_list_depth2.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_base_list_depth2.jett b/tests/native/reflected_nested/root_base_list_depth2.jett new file mode 100644 index 00000000..c2954d1c --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth2.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Positive]: + return type.field_value[Record, list[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Positive]: + return source into view type.field_value[Record, list[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Positive]: + return source into view type.variant_field_value[Event, list[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Positive]: + return source into view type.machine_field_value[Session, list[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_list_depth2.stdout b/tests/native/reflected_nested/root_base_list_depth2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_list_depth2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_map_depth0.debug b/tests/native/reflected_nested/root_base_map_depth0.debug new file mode 100644 index 00000000..57bde519 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth0.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = root +trace extracted: map[int64, app.Positive] = map(1: 7) +trace source: app.Record = app.Record(values: map(1: 7)) diff --git a/tests/native/reflected_nested/root_base_map_depth0.jett b/tests/native/reflected_nested/root_base_map_depth0.jett new file mode 100644 index 00000000..3359cb03 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth0.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: map[int64, Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = map[int64, Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + map[int64, Positive] base = map(1: value) + Root values = base handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Record, map[int64, Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns map[int64, Positive]: + return source into view type.field_value[Record, map[int64, Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns map[int64, Positive]: + return source into view type.variant_field_value[Event, map[int64, Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns map[int64, Positive]: + return source into view type.machine_field_value[Session, map[int64, Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + map[int64, Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_map_depth0.stdout b/tests/native/reflected_nested/root_base_map_depth0.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth0.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_base_map_depth1.debug b/tests/native/reflected_nested/root_base_map_depth1.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth1.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_map_depth1.jett b/tests/native/reflected_nested/root_base_map_depth1.jett new file mode 100644 index 00000000..c3c1939d --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth1.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: map[int64, Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = map[int64, Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + map[int64, Positive] base = map(1: value) + Root values = base handle error: + return seed() + return run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Record, map[int64, Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns map[int64, Positive]: + return source into view type.field_value[Record, map[int64, Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns map[int64, Positive]: + return source into view type.variant_field_value[Event, map[int64, Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns map[int64, Positive]: + return source into view type.machine_field_value[Session, map[int64, Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + map[int64, Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_map_depth1.stdout b/tests/native/reflected_nested/root_base_map_depth1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_map_depth2.debug b/tests/native/reflected_nested/root_base_map_depth2.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_map_depth2.jett b/tests/native/reflected_nested/root_base_map_depth2.jett new file mode 100644 index 00000000..88c0339e --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth2.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: map[int64, Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = map[int64, Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + map[int64, Positive] base = map(1: value) + Root values = base handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns map[int64, Positive]: + return type.field_value[Record, map[int64, Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns map[int64, Positive]: + return source into view type.field_value[Record, map[int64, Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns map[int64, Positive]: + return type.variant_field_value[Event, map[int64, Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns map[int64, Positive]: + return source into view type.variant_field_value[Event, map[int64, Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns map[int64, Positive]: + return type.machine_field_value[Session, map[int64, Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns map[int64, Positive]: + return source into view type.machine_field_value[Session, map[int64, Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + map[int64, Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_map_depth2.stdout b/tests/native/reflected_nested/root_base_map_depth2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_map_depth2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_optional_depth0.debug b/tests/native/reflected_nested/root_base_optional_depth0.debug new file mode 100644 index 00000000..54f3a2d5 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth0.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = root +trace extracted: optional[app.Positive] = some(7) +trace source: app.Record = app.Record(values: some(7)) diff --git a/tests/native/reflected_nested/root_base_optional_depth0.jett b/tests/native/reflected_nested/root_base_optional_depth0.jett new file mode 100644 index 00000000..a5b0118a --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth0.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: optional[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = optional[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + optional[Positive] base = some(value) + Root values = base handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns optional[Positive]: + return type.field_value[Record, optional[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns optional[Positive]: + return source into view type.field_value[Record, optional[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns optional[Positive]: + return source into view type.variant_field_value[Event, optional[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns optional[Positive]: + return source into view type.machine_field_value[Session, optional[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + optional[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_optional_depth0.stdout b/tests/native/reflected_nested/root_base_optional_depth0.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth0.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_base_optional_depth1.debug b/tests/native/reflected_nested/root_base_optional_depth1.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth1.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_optional_depth1.jett b/tests/native/reflected_nested/root_base_optional_depth1.jett new file mode 100644 index 00000000..3ba012a2 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth1.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: optional[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = optional[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + optional[Positive] base = some(value) + Root values = base handle error: + return seed() + return run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns optional[Positive]: + return type.field_value[Record, optional[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns optional[Positive]: + return source into view type.field_value[Record, optional[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns optional[Positive]: + return source into view type.variant_field_value[Event, optional[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns optional[Positive]: + return source into view type.machine_field_value[Session, optional[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + optional[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_optional_depth1.stdout b/tests/native/reflected_nested/root_base_optional_depth1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_optional_depth2.debug b/tests/native/reflected_nested/root_base_optional_depth2.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_optional_depth2.jett b/tests/native/reflected_nested/root_base_optional_depth2.jett new file mode 100644 index 00000000..8e360945 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth2.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: optional[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = optional[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + optional[Positive] base = some(value) + Root values = base handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns optional[Positive]: + return type.field_value[Record, optional[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns optional[Positive]: + return source into view type.field_value[Record, optional[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns optional[Positive]: + return type.variant_field_value[Event, optional[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns optional[Positive]: + return source into view type.variant_field_value[Event, optional[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns optional[Positive]: + return type.machine_field_value[Session, optional[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns optional[Positive]: + return source into view type.machine_field_value[Session, optional[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + optional[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_optional_depth2.stdout b/tests/native/reflected_nested/root_base_optional_depth2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_optional_depth2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_result_depth0.debug b/tests/native/reflected_nested/root_base_result_depth0.debug new file mode 100644 index 00000000..98963bfa --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth0.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = root +trace extracted: result[app.Positive, int64] = ok(7) +trace source: app.Record = app.Record(values: ok(7)) diff --git a/tests/native/reflected_nested/root_base_result_depth0.jett b/tests/native/reflected_nested/root_base_result_depth0.jett new file mode 100644 index 00000000..cb06e670 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth0.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: result[Positive, int64]) returns bool: + string marker = "root" + trace marker + return true +type Root = result[Positive, int64] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + result[Positive, int64] base = ok(value) + Root values = base handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Record, result[Positive, int64]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns result[Positive, int64]: + return source into view type.field_value[Record, result[Positive, int64]](view field) +function event_direct(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns result[Positive, int64]: + return source into view type.variant_field_value[Event, result[Positive, int64]](view field) +function machine_direct(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns result[Positive, int64]: + return source into view type.machine_field_value[Session, result[Positive, int64]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + result[Positive, int64] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_result_depth0.stdout b/tests/native/reflected_nested/root_base_result_depth0.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth0.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_base_result_depth1.debug b/tests/native/reflected_nested/root_base_result_depth1.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth1.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_result_depth1.jett b/tests/native/reflected_nested/root_base_result_depth1.jett new file mode 100644 index 00000000..fb6b7128 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth1.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: result[Positive, int64]) returns bool: + string marker = "root" + trace marker + return true +type Root = result[Positive, int64] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + result[Positive, int64] base = ok(value) + Root values = base handle error: + return seed() + return run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Record, result[Positive, int64]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns result[Positive, int64]: + return source into view type.field_value[Record, result[Positive, int64]](view field) +function event_direct(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns result[Positive, int64]: + return source into view type.variant_field_value[Event, result[Positive, int64]](view field) +function machine_direct(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns result[Positive, int64]: + return source into view type.machine_field_value[Session, result[Positive, int64]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + result[Positive, int64] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_result_depth1.stdout b/tests/native/reflected_nested/root_base_result_depth1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_result_depth2.debug b/tests/native/reflected_nested/root_base_result_depth2.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_result_depth2.jett b/tests/native/reflected_nested/root_base_result_depth2.jett new file mode 100644 index 00000000..a13c2288 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth2.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: result[Positive, int64]) returns bool: + string marker = "root" + trace marker + return true +type Root = result[Positive, int64] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + result[Positive, int64] base = ok(value) + Root values = base handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns result[Positive, int64]: + return type.field_value[Record, result[Positive, int64]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns result[Positive, int64]: + return source into view type.field_value[Record, result[Positive, int64]](view field) +function event_direct(view source: Event, view field: TypeField) returns result[Positive, int64]: + return type.variant_field_value[Event, result[Positive, int64]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns result[Positive, int64]: + return source into view type.variant_field_value[Event, result[Positive, int64]](view field) +function machine_direct(view source: Session, view field: TypeField) returns result[Positive, int64]: + return type.machine_field_value[Session, result[Positive, int64]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns result[Positive, int64]: + return source into view type.machine_field_value[Session, result[Positive, int64]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + result[Positive, int64] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_result_depth2.stdout b/tests/native/reflected_nested/root_base_result_depth2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_result_depth2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_set_depth0.debug b/tests/native/reflected_nested/root_base_set_depth0.debug new file mode 100644 index 00000000..68756b1d --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth0.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = root +trace extracted: set[app.Positive] = set(7) +trace source: app.Record = app.Record(values: set(7)) diff --git a/tests/native/reflected_nested/root_base_set_depth0.jett b/tests/native/reflected_nested/root_base_set_depth0.jett new file mode 100644 index 00000000..c38ce404 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth0.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: set[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = set[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + mutable set[Positive] base = set.new[Positive]() + base = set.add[Positive](base, value) + Root values = base handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns set[Positive]: + return type.field_value[Record, set[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns set[Positive]: + return source into view type.field_value[Record, set[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns set[Positive]: + return source into view type.variant_field_value[Event, set[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns set[Positive]: + return source into view type.machine_field_value[Session, set[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + set[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_set_depth0.stdout b/tests/native/reflected_nested/root_base_set_depth0.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth0.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_base_set_depth1.debug b/tests/native/reflected_nested/root_base_set_depth1.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth1.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_set_depth1.jett b/tests/native/reflected_nested/root_base_set_depth1.jett new file mode 100644 index 00000000..751863d2 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth1.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: set[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = set[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + mutable set[Positive] base = set.new[Positive]() + base = set.add[Positive](base, value) + Root values = base handle error: + return seed() + return run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns set[Positive]: + return type.field_value[Record, set[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns set[Positive]: + return source into view type.field_value[Record, set[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns set[Positive]: + return source into view type.variant_field_value[Event, set[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns set[Positive]: + return source into view type.machine_field_value[Session, set[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + set[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_set_depth1.stdout b/tests/native/reflected_nested/root_base_set_depth1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_base_set_depth2.debug b/tests/native/reflected_nested/root_base_set_depth2.debug new file mode 100644 index 00000000..4bf2da14 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = root diff --git a/tests/native/reflected_nested/root_base_set_depth2.jett b/tests/native/reflected_nested/root_base_set_depth2.jett new file mode 100644 index 00000000..9ac96527 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth2.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function root_check(view values: set[Positive]) returns bool: + string marker = "root" + trace marker + return true +type Root = set[Positive] where root_check(view value) +struct Record: + values: Root +enum Event: + content(values: Root) +machine Session: + states: + content(values: Root) + empty + transitions: + content to empty +function seed() returns Root: + Positive value = 7 handle error: + return seed() + mutable set[Positive] base = set.new[Positive]() + base = set.add[Positive](base, value) + Root values = base handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns set[Positive]: + return type.field_value[Record, set[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns set[Positive]: + return source into view type.field_value[Record, set[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns set[Positive]: + return source into view type.variant_field_value[Event, set[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns set[Positive]: + return source into view type.machine_field_value[Session, set[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + set[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_base_set_depth2.stdout b/tests/native/reflected_nested/root_base_set_depth2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_base_set_depth2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_generic_exact_pending2.debug b/tests/native/reflected_nested/root_generic_exact_pending2.debug new file mode 100644 index 00000000..007dfc2f --- /dev/null +++ b/tests/native/reflected_nested/root_generic_exact_pending2.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Positive] = pending(pending(list(7))) +trace source: app.Record = app.Record(values: pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/root_generic_exact_pending2.jett b/tests/native/reflected_nested/root_generic_exact_pending2.jett new file mode 100644 index 00000000..8986b31f --- /dev/null +++ b/tests/native/reflected_nested/root_generic_exact_pending2.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: list[Positive] +enum Event: + content(values: list[Positive]) +machine Session: + states: + content(values: list[Positive]) + empty + transitions: + content to empty +function seed() returns list[Positive]: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + list[Positive] base = coarsen values + return run run base +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Positive]: + return type.field_value[Record, list[Positive]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Positive]: + return source into view type.field_value[Record, list[Positive]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Positive]: + return source into view type.variant_field_value[Event, list[Positive]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Positive]: + return source into view type.machine_field_value[Session, list[Positive]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) + TypeField field = record_selector() + list[Positive] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_generic_exact_pending2.stdout b/tests/native/reflected_nested/root_generic_exact_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_generic_exact_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_named_exact_pending2.debug b/tests/native/reflected_nested/root_named_exact_pending2.debug new file mode 100644 index 00000000..a6e9d32d --- /dev/null +++ b/tests/native/reflected_nested/root_named_exact_pending2.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: app.Nonempty = pending(pending(list(7))) +trace source: app.Record = app.Record(values: pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/root_named_exact_pending2.jett b/tests/native/reflected_nested/root_named_exact_pending2.jett new file mode 100644 index 00000000..3a488d25 --- /dev/null +++ b/tests/native/reflected_nested/root_named_exact_pending2.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns Nonempty: + return type.field_value[Record, Nonempty](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns Nonempty: + return source into view type.field_value[Record, Nonempty](view field) +function event_direct(view source: Event, view field: TypeField) returns Nonempty: + return type.variant_field_value[Event, Nonempty](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns Nonempty: + return source into view type.variant_field_value[Event, Nonempty](view field) +function machine_direct(view source: Session, view field: TypeField) returns Nonempty: + return type.machine_field_value[Session, Nonempty](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns Nonempty: + return source into view type.machine_field_value[Session, Nonempty](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + Nonempty extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_named_exact_pending2.stdout b/tests/native/reflected_nested/root_named_exact_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_named_exact_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_new_sibling_pending2.debug b/tests/native/reflected_nested/root_new_sibling_pending2.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_new_sibling_pending2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_new_sibling_pending2.jett b/tests/native/reflected_nested/root_new_sibling_pending2.jett new file mode 100644 index 00000000..c4288a4d --- /dev/null +++ b/tests/native/reflected_nested/root_new_sibling_pending2.jett @@ -0,0 +1,67 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +function sibling_check(view values: list[Positive]) returns bool: + string marker = "sibling" + trace marker + return true +type Sibling = list[Positive] where sibling_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns Sibling: + return type.field_value[Record, Sibling](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns Sibling: + return source into view type.field_value[Record, Sibling](view field) +function event_direct(view source: Event, view field: TypeField) returns Sibling: + return type.variant_field_value[Event, Sibling](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns Sibling: + return source into view type.variant_field_value[Event, Sibling](view field) +function machine_direct(view source: Session, view field: TypeField) returns Sibling: + return type.machine_field_value[Session, Sibling](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns Sibling: + return source into view type.machine_field_value[Session, Sibling](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + Sibling extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_new_sibling_pending2.stdout b/tests/native/reflected_nested/root_new_sibling_pending2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_new_sibling_pending2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_pending2.debug b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_pending2.jett b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.jett new file mode 100644 index 00000000..94041713 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder[T]: + value: T +function nonempty_check(view values: list[Holder[Positive]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Holder[Positive]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Holder[Positive] element = Holder[Positive](value: value) handle error: + return seed() + Nonempty values = list(element) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Holder[Positive]]: + return type.field_value[Record, list[Holder[Positive]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.field_value[Record, list[Holder[Positive]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Holder[Positive]]: + return type.variant_field_value[Event, list[Holder[Positive]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.variant_field_value[Event, list[Holder[Positive]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Holder[Positive]]: + return type.machine_field_value[Session, list[Holder[Positive]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.machine_field_value[Session, list[Holder[Positive]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Holder[Positive]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_pending2.stdout b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_pending2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_ready.debug b/tests/native/reflected_nested/root_nominal_holder_positive_ready.debug new file mode 100644 index 00000000..7e0413e6 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_ready.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Holder[app.Positive]] = list(app.Holder(value: 7)) +trace source: app.Record = app.Record(values: list(app.Holder(value: 7))) diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_ready.jett b/tests/native/reflected_nested/root_nominal_holder_positive_ready.jett new file mode 100644 index 00000000..15812156 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_ready.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder[T]: + value: T +function nonempty_check(view values: list[Holder[Positive]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Holder[Positive]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Holder[Positive] element = Holder[Positive](value: value) handle error: + return seed() + Nonempty values = list(element) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Holder[Positive]]: + return type.field_value[Record, list[Holder[Positive]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.field_value[Record, list[Holder[Positive]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Holder[Positive]]: + return type.variant_field_value[Event, list[Holder[Positive]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.variant_field_value[Event, list[Holder[Positive]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Holder[Positive]]: + return type.machine_field_value[Session, list[Holder[Positive]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Holder[Positive]]: + return source into view type.machine_field_value[Session, list[Holder[Positive]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Holder[Positive]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_holder_positive_ready.stdout b/tests/native/reflected_nested/root_nominal_holder_positive_ready.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_holder_positive_ready.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.debug b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.debug new file mode 100644 index 00000000..a8cca678 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Element] = pending(pending(list(app.Element(value: 7)))) +trace source: app.Record = app.Record(values: pending(pending(list(app.Element(value: 7))))) diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.jett b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.jett new file mode 100644 index 00000000..24eac06c --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Element: + value: Positive +function nonempty_check(view values: list[Element]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Element] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Element element = Element(value: value) handle error: + return seed() + Nonempty values = list(element) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Element]: + return type.field_value[Record, list[Element]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Element]: + return source into view type.field_value[Record, list[Element]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Element]: + return type.variant_field_value[Event, list[Element]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Element]: + return source into view type.variant_field_value[Event, list[Element]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Element]: + return type.machine_field_value[Session, list[Element]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Element]: + return source into view type.machine_field_value[Session, list[Element]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Element] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.stdout b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_ready.debug b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.debug new file mode 100644 index 00000000..d90c7f29 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Element] = list(app.Element(value: 7)) +trace source: app.Record = app.Record(values: list(app.Element(value: 7))) diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_ready.jett b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.jett new file mode 100644 index 00000000..173f4ea0 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Element: + value: Positive +function nonempty_check(view values: list[Element]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Element] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Element element = Element(value: value) handle error: + return seed() + Nonempty values = list(element) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Element]: + return type.field_value[Record, list[Element]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Element]: + return source into view type.field_value[Record, list[Element]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Element]: + return type.variant_field_value[Event, list[Element]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Element]: + return source into view type.variant_field_value[Event, list[Element]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Element]: + return type.machine_field_value[Session, list[Element]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Element]: + return source into view type.machine_field_value[Session, list[Element]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Element] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_ordinary_element_ready.stdout b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_ordinary_element_ready.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_pending2.debug b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_pending2.jett b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.jett new file mode 100644 index 00000000..4902548f --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +type Alias = Positive +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[Alias]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[Alias]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[Alias] element = Unused[Alias](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[Alias]]: + return type.field_value[Record, list[Unused[Alias]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.field_value[Record, list[Unused[Alias]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[Alias]]: + return type.variant_field_value[Event, list[Unused[Alias]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.variant_field_value[Event, list[Unused[Alias]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[Alias]]: + return type.machine_field_value[Session, list[Unused[Alias]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.machine_field_value[Session, list[Unused[Alias]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[Alias]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_pending2.stdout b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_pending2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_ready.debug b/tests/native/reflected_nested/root_nominal_unused_alias_ready.debug new file mode 100644 index 00000000..7c6f134c --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_ready.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Unused[app.Alias]] = list(app.Unused(value: 7)) +trace source: app.Record = app.Record(values: list(app.Unused(value: 7))) diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_ready.jett b/tests/native/reflected_nested/root_nominal_unused_alias_ready.jett new file mode 100644 index 00000000..15a083db --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_ready.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +type Alias = Positive +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[Alias]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[Alias]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[Alias] element = Unused[Alias](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[Alias]]: + return type.field_value[Record, list[Unused[Alias]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.field_value[Record, list[Unused[Alias]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[Alias]]: + return type.variant_field_value[Event, list[Unused[Alias]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.variant_field_value[Event, list[Unused[Alias]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[Alias]]: + return type.machine_field_value[Session, list[Unused[Alias]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[Alias]]: + return source into view type.machine_field_value[Session, list[Unused[Alias]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[Alias]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_alias_ready.stdout b/tests/native/reflected_nested/root_nominal_unused_alias_ready.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_alias_ready.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_pending2.debug b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.debug new file mode 100644 index 00000000..a2fe8ca5 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Unused[int64]] = pending(pending(list(app.Unused(value: 7)))) +trace source: app.Record = app.Record(values: pending(pending(list(app.Unused(value: 7))))) diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_pending2.jett b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.jett new file mode 100644 index 00000000..0cc19fe9 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[int64]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[int64]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[int64] element = Unused[int64](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[int64]]: + return type.field_value[Record, list[Unused[int64]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[int64]]: + return source into view type.field_value[Record, list[Unused[int64]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[int64]]: + return type.variant_field_value[Event, list[Unused[int64]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[int64]]: + return source into view type.variant_field_value[Event, list[Unused[int64]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[int64]]: + return type.machine_field_value[Session, list[Unused[int64]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[int64]]: + return source into view type.machine_field_value[Session, list[Unused[int64]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[int64]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_pending2.stdout b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_ready.debug b/tests/native/reflected_nested/root_nominal_unused_plain_ready.debug new file mode 100644 index 00000000..75aa144e --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_ready.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Unused[int64]] = list(app.Unused(value: 7)) +trace source: app.Record = app.Record(values: list(app.Unused(value: 7))) diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_ready.jett b/tests/native/reflected_nested/root_nominal_unused_plain_ready.jett new file mode 100644 index 00000000..eea2ede1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_ready.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[int64]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[int64]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[int64] element = Unused[int64](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[int64]]: + return type.field_value[Record, list[Unused[int64]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[int64]]: + return source into view type.field_value[Record, list[Unused[int64]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[int64]]: + return type.variant_field_value[Event, list[Unused[int64]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[int64]]: + return source into view type.variant_field_value[Event, list[Unused[int64]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[int64]]: + return type.machine_field_value[Session, list[Unused[int64]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[int64]]: + return source into view type.machine_field_value[Session, list[Unused[int64]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[int64]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_plain_ready.stdout b/tests/native/reflected_nested/root_nominal_unused_plain_ready.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_plain_ready.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_pending2.debug b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.debug new file mode 100644 index 00000000..791d4588 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.debug @@ -0,0 +1,2 @@ +trace marker: string = positive +trace marker: string = nonempty diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_pending2.jett b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.jett new file mode 100644 index 00000000..e6969dc3 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[Positive]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[Positive]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[Positive] element = Unused[Positive](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[Positive]]: + return type.field_value[Record, list[Unused[Positive]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.field_value[Record, list[Unused[Positive]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[Positive]]: + return type.variant_field_value[Event, list[Unused[Positive]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.variant_field_value[Event, list[Unused[Positive]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[Positive]]: + return type.machine_field_value[Session, list[Unused[Positive]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.machine_field_value[Session, list[Unused[Positive]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[Positive]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_pending2.stdout b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_pending2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_ready.debug b/tests/native/reflected_nested/root_nominal_unused_positive_ready.debug new file mode 100644 index 00000000..60ff780b --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_ready.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[app.Unused[app.Positive]] = list(app.Unused(value: 7)) +trace source: app.Record = app.Record(values: list(app.Unused(value: 7))) diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_ready.jett b/tests/native/reflected_nested/root_nominal_unused_positive_ready.jett new file mode 100644 index 00000000..af058d93 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_ready.jett @@ -0,0 +1,65 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Unused[T]: + value: int64 +function nonempty_check(view values: list[Unused[Positive]]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Unused[Positive]] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Unused[Positive] element = Unused[Positive](value: coarsen value) + Nonempty values = list(element) handle error: + return seed() + return values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[Unused[Positive]]: + return type.field_value[Record, list[Unused[Positive]]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.field_value[Record, list[Unused[Positive]]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[Unused[Positive]]: + return type.variant_field_value[Event, list[Unused[Positive]]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.variant_field_value[Event, list[Unused[Positive]]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[Unused[Positive]]: + return type.machine_field_value[Session, list[Unused[Positive]]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[Unused[Positive]]: + return source into view type.machine_field_value[Session, list[Unused[Positive]]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[Unused[Positive]] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_nominal_unused_positive_ready.stdout b/tests/native/reflected_nested/root_nominal_unused_positive_ready.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_nominal_unused_positive_ready.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_shared_named_ancestor_pending2.debug b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.debug new file mode 100644 index 00000000..9a610616 --- /dev/null +++ b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.debug @@ -0,0 +1,6 @@ +trace marker: string = positive +trace marker: string = nonempty +trace marker: string = strong +trace marker: string = sibling +trace extracted: app.Sibling = pending(pending(list(7))) +trace source: app.Record = app.Record(values: pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/root_shared_named_ancestor_pending2.jett b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.jett new file mode 100644 index 00000000..214546d7 --- /dev/null +++ b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.jett @@ -0,0 +1,74 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +function strong_check(view values: list[Positive]) returns bool: + string marker = "strong" + trace marker + return true +type Strong = Nonempty where strong_check(view value) +function sibling_check(view values: list[Positive]) returns bool: + string marker = "sibling" + trace marker + return true +type Sibling = Nonempty where sibling_check(view value) +struct Record: + values: Strong +enum Event: + content(values: Strong) +machine Session: + states: + content(values: Strong) + empty + transitions: + content to empty +function seed() returns Strong: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + Strong stronger = clone values handle error: + return seed() + return run run stronger +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns Sibling: + return type.field_value[Record, Sibling](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns Sibling: + return source into view type.field_value[Record, Sibling](view field) +function event_direct(view source: Event, view field: TypeField) returns Sibling: + return type.variant_field_value[Event, Sibling](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns Sibling: + return source into view type.variant_field_value[Event, Sibling](view field) +function machine_direct(view source: Session, view field: TypeField) returns Sibling: + return type.machine_field_value[Session, Sibling](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns Sibling: + return source into view type.machine_field_value[Session, Sibling](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + Sibling extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_shared_named_ancestor_pending2.stdout b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_shared_named_ancestor_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/root_to_plain_pending2.debug b/tests/native/reflected_nested/root_to_plain_pending2.debug new file mode 100644 index 00000000..13f2e5f5 --- /dev/null +++ b/tests/native/reflected_nested/root_to_plain_pending2.debug @@ -0,0 +1,4 @@ +trace marker: string = positive +trace marker: string = nonempty +trace extracted: list[int64] = pending(pending(list(7))) +trace source: app.Record = app.Record(values: pending(pending(list(7)))) diff --git a/tests/native/reflected_nested/root_to_plain_pending2.jett b/tests/native/reflected_nested/root_to_plain_pending2.jett new file mode 100644 index 00000000..d9346823 --- /dev/null +++ b/tests/native/reflected_nested/root_to_plain_pending2.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function nonempty_check(view values: list[Positive]) returns bool: + string marker = "nonempty" + trace marker + return list.length(view values) > 0 +type Nonempty = list[Positive] where nonempty_check(view value) +struct Record: + values: Nonempty +enum Event: + content(values: Nonempty) +machine Session: + states: + content(values: Nonempty) + empty + transitions: + content to empty +function seed() returns Nonempty: + Positive value = 7 handle error: + return seed() + Nonempty values = list(value) handle error: + return seed() + return run run values +function record_selector() returns TypeField: + return list.get[TypeField](type.fields[Record](), 0) handle: + return record_selector() +function event_selector(view value: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view value) + return list.get[TypeField](variant.fields, 0) handle: + return event_selector(view value) +function machine_selector(view value: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view value) + return list.get[TypeField](state.fields, 0) handle: + return machine_selector(view value) +function record_direct(view source: Record, view field: TypeField) returns list[int64]: + return type.field_value[Record, list[int64]](view source, view field) +function record_pipe(view source: Record, view field: TypeField) returns list[int64]: + return source into view type.field_value[Record, list[int64]](view field) +function event_direct(view source: Event, view field: TypeField) returns list[int64]: + return type.variant_field_value[Event, list[int64]](view source, view field) +function event_pipe(view source: Event, view field: TypeField) returns list[int64]: + return source into view type.variant_field_value[Event, list[int64]](view field) +function machine_direct(view source: Session, view field: TypeField) returns list[int64]: + return type.machine_field_value[Session, list[int64]](view source, view field) +function machine_pipe(view source: Session, view field: TypeField) returns list[int64]: + return source into view type.machine_field_value[Session, list[int64]](view field) +function report(view stdout: Stdout) returns nothing: + Record source = Record(values: seed()) handle error: + Stdout.write(view stdout, error) + return nothing + TypeField field = record_selector() + list[int64] extracted = record_direct(view source, view field) + trace extracted + trace source + Stdout.write(view stdout, "accepted\n") +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "before\n") + report(view stdout) diff --git a/tests/native/reflected_nested/root_to_plain_pending2.stdout b/tests/native/reflected_nested/root_to_plain_pending2.stdout new file mode 100644 index 00000000..ff8e3ca1 --- /dev/null +++ b/tests/native/reflected_nested/root_to_plain_pending2.stdout @@ -0,0 +1,2 @@ +before +accepted diff --git a/tests/native/reflected_nested/selector_wrong_member_event.debug b/tests/native/reflected_nested/selector_wrong_member_event.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_member_event.jett b/tests/native/reflected_nested/selector_wrong_member_event.jett new file mode 100644 index 00000000..ba32eea3 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_member_event.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) + mirror(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + Event prior = Event.mirror(list(7)) + TypeField event_selector = event_field(view prior) + TypeField session_selector = session_field(view session) + string first = summarize(read_event(view event, view event_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_member_event.stdout b/tests/native/reflected_nested/selector_wrong_member_event.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_member_event.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_member_session.debug b/tests/native/reflected_nested/selector_wrong_member_session.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_member_session.jett b/tests/native/reflected_nested/selector_wrong_member_session.jett new file mode 100644 index 00000000..a7e70a62 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_member_session.jett @@ -0,0 +1,63 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + mirror(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + Session prior = Session(mirror, list(7)) + TypeField session_selector = session_field(view prior) + string first = summarize(read_session(view session, view session_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_member_session.stdout b/tests/native/reflected_nested/selector_wrong_member_session.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_member_session.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_owner_event.debug b/tests/native/reflected_nested/selector_wrong_owner_event.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_owner_event.jett b/tests/native/reflected_nested/selector_wrong_owner_event.jett new file mode 100644 index 00000000..1eb8251b --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_event.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Other: + payload: list[int64] +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = other_field() + TypeField session_selector = session_field(view session) + string first = summarize(read_event(view event, view event_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_owner_event.stdout b/tests/native/reflected_nested/selector_wrong_owner_event.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_event.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_owner_record.debug b/tests/native/reflected_nested/selector_wrong_owner_record.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_owner_record.jett b/tests/native/reflected_nested/selector_wrong_owner_record.jett new file mode 100644 index 00000000..2a776cca --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_record.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Other: + payload: list[int64] +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = other_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_owner_record.stdout b/tests/native/reflected_nested/selector_wrong_owner_record.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_record.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_owner_session.debug b/tests/native/reflected_nested/selector_wrong_owner_session.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_owner_session.jett b/tests/native/reflected_nested/selector_wrong_owner_session.jett new file mode 100644 index 00000000..c2a08240 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_session.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Other: + payload: list[int64] +struct Holder: + payload: list[int64] +enum Event: + content(payload: list[int64]) +machine Session: + states: + content(payload: list[int64]) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function other_field() returns TypeField: + return list.get[TypeField](type.fields[Other](), 0) handle: + return other_field() +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = other_field() + string first = summarize(read_session(view session, view session_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_owner_session.stdout b/tests/native/reflected_nested/selector_wrong_owner_session.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_owner_session.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_request_event.debug b/tests/native/reflected_nested/selector_wrong_request_event.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_request_event.jett b/tests/native/reflected_nested/selector_wrong_request_event.jett new file mode 100644 index 00000000..d16a141d --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_event.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] + text: string +enum Event: + content(payload: list[int64], text: string) +machine Session: + states: + content(payload: list[int64], text: string) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 1) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed(), text: "") + Event event = Event.content(seed(), "") + Session session = Session(content, seed(), "") + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_event(view event, view event_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_request_event.stdout b/tests/native/reflected_nested/selector_wrong_request_event.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_event.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_request_record.debug b/tests/native/reflected_nested/selector_wrong_request_record.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_request_record.jett b/tests/native/reflected_nested/selector_wrong_request_record.jett new file mode 100644 index 00000000..07000e35 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_record.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] + text: string +enum Event: + content(payload: list[int64], text: string) +machine Session: + states: + content(payload: list[int64], text: string) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 1) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed(), text: "") + Event event = Event.content(seed(), "") + Session session = Session(content, seed(), "") + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_request_record.stdout b/tests/native/reflected_nested/selector_wrong_request_record.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_record.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/selector_wrong_request_session.debug b/tests/native/reflected_nested/selector_wrong_request_session.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/selector_wrong_request_session.jett b/tests/native/reflected_nested/selector_wrong_request_session.jett new file mode 100644 index 00000000..95652688 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_session.jett @@ -0,0 +1,62 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: list[int64] + text: string +enum Event: + content(payload: list[int64], text: string) +machine Session: + states: + content(payload: list[int64], text: string) + empty + transitions: + content to empty +function seed() returns list[int64]: + return run run list(7, 9) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 1) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns list[Positive]: + return type.field_value[Holder, list[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns list[Positive]: + return type.variant_field_value[Event, list[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns list[Positive]: + return type.machine_field_value[Session, list[Positive]](view source, view field) +function summarize(items: list[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed(), text: "") + Event event = Event.content(seed(), "") + Session session = Session(content, seed(), "") + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_session(view session, view session_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/selector_wrong_request_session.stdout b/tests/native/reflected_nested/selector_wrong_request_session.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/selector_wrong_request_session.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/set_empty_success.debug b/tests/native/reflected_nested/set_empty_success.debug new file mode 100644 index 00000000..96c85bc7 --- /dev/null +++ b/tests/native/reflected_nested/set_empty_success.debug @@ -0,0 +1,3 @@ +trace record: app.Holder = app.Holder(payload: set()) +trace event: app.Event = app.Event.content(set()) +trace session: app.Session = app.Session@content(set()) diff --git a/tests/native/reflected_nested/set_empty_success.jett b/tests/native/reflected_nested/set_empty_success.jett new file mode 100644 index 00000000..7cc94521 --- /dev/null +++ b/tests/native/reflected_nested/set_empty_success.jett @@ -0,0 +1,61 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: set[int64] +enum Event: + content(payload: set[int64]) +machine Session: + states: + content(payload: set[int64]) + empty + transitions: + content to empty +function seed() returns set[int64]: + return set.new[int64]() +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_empty_success.stdout b/tests/native/reflected_nested/set_empty_success.stdout new file mode 100644 index 00000000..3cfe5a3e --- /dev/null +++ b/tests/native/reflected_nested/set_empty_success.stdout @@ -0,0 +1,2 @@ +before +after:||:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/set_exact_pending_1.debug b/tests/native/reflected_nested/set_exact_pending_1.debug new file mode 100644 index 00000000..73c9a549 --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_1.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: set[app.Positive] = pending(set(7)) +trace items: set[app.Positive] = pending(set(7)) +trace items: set[app.Positive] = pending(set(7)) +trace record: app.Holder = app.Holder(payload: pending(set(7))) +trace event: app.Event = app.Event.content(pending(set(7))) +trace session: app.Session = app.Session@content(pending(set(7))) diff --git a/tests/native/reflected_nested/set_exact_pending_1.jett b/tests/native/reflected_nested/set_exact_pending_1.jett new file mode 100644 index 00000000..0205b139 --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_1.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: set[Positive] +enum Event: + content(payload: set[Positive]) +machine Session: + states: + content(payload: set[Positive]) + empty + transitions: + content to empty +function seed() returns set[Positive]: + Positive value = 7 handle error: + return seed() + return run set.add[Positive](set.new[Positive](), value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_exact_pending_1.stdout b/tests/native/reflected_nested/set_exact_pending_1.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_1.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/set_exact_pending_2.debug b/tests/native/reflected_nested/set_exact_pending_2.debug new file mode 100644 index 00000000..43e2c85e --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_2.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace items: set[app.Positive] = pending(pending(set(7))) +trace items: set[app.Positive] = pending(pending(set(7))) +trace items: set[app.Positive] = pending(pending(set(7))) +trace record: app.Holder = app.Holder(payload: pending(pending(set(7)))) +trace event: app.Event = app.Event.content(pending(pending(set(7)))) +trace session: app.Session = app.Session@content(pending(pending(set(7)))) diff --git a/tests/native/reflected_nested/set_exact_pending_2.jett b/tests/native/reflected_nested/set_exact_pending_2.jett new file mode 100644 index 00000000..33c11867 --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_2.jett @@ -0,0 +1,66 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +function high_check(value: int64) returns bool: + string marker = "high" + trace marker + return value > 10 +type High = Positive where high_check(value) + +struct Holder: + payload: set[Positive] +enum Event: + content(payload: set[Positive]) +machine Session: + states: + content(payload: set[Positive]) + empty + transitions: + content to empty +function seed() returns set[Positive]: + Positive value = 7 handle error: + return seed() + return run run set.add[Positive](set.new[Positive](), value) +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + trace items + return "exact" +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_exact_pending_2.stdout b/tests/native/reflected_nested/set_exact_pending_2.stdout new file mode 100644 index 00000000..d91962ea --- /dev/null +++ b/tests/native/reflected_nested/set_exact_pending_2.stdout @@ -0,0 +1,2 @@ +before +after:exact|exact|exact:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_nested/set_outer_pending_1.debug b/tests/native/reflected_nested/set_outer_pending_1.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/set_outer_pending_1.jett b/tests/native/reflected_nested/set_outer_pending_1.jett new file mode 100644 index 00000000..d1cb0328 --- /dev/null +++ b/tests/native/reflected_nested/set_outer_pending_1.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: set[int64] +enum Event: + content(payload: set[int64]) +machine Session: + states: + content(payload: set[int64]) + empty + transitions: + content to empty +function seed() returns set[int64]: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 7) + values = set.add[int64](values, 9) + return run values +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_outer_pending_1.stdout b/tests/native/reflected_nested/set_outer_pending_1.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/set_outer_pending_1.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/set_outer_pending_2.debug b/tests/native/reflected_nested/set_outer_pending_2.debug new file mode 100644 index 00000000..e69de29b diff --git a/tests/native/reflected_nested/set_outer_pending_2.jett b/tests/native/reflected_nested/set_outer_pending_2.jett new file mode 100644 index 00000000..a964ac59 --- /dev/null +++ b/tests/native/reflected_nested/set_outer_pending_2.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: set[int64] +enum Event: + content(payload: set[int64]) +machine Session: + states: + content(payload: set[int64]) + empty + transitions: + content to empty +function seed() returns set[int64]: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 7) + values = set.add[int64](values, 9) + return run run values +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_outer_pending_2.stdout b/tests/native/reflected_nested/set_outer_pending_2.stdout new file mode 100644 index 00000000..90be1f30 --- /dev/null +++ b/tests/native/reflected_nested/set_outer_pending_2.stdout @@ -0,0 +1 @@ +before diff --git a/tests/native/reflected_nested/set_ready_success.debug b/tests/native/reflected_nested/set_ready_success.debug new file mode 100644 index 00000000..401b6942 --- /dev/null +++ b/tests/native/reflected_nested/set_ready_success.debug @@ -0,0 +1,9 @@ +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace marker: string = positive +trace record: app.Holder = app.Holder(payload: set(7, 9)) +trace event: app.Event = app.Event.content(set(7, 9)) +trace session: app.Session = app.Session@content(set(7, 9)) diff --git a/tests/native/reflected_nested/set_ready_success.jett b/tests/native/reflected_nested/set_ready_success.jett new file mode 100644 index 00000000..70b9cde2 --- /dev/null +++ b/tests/native/reflected_nested/set_ready_success.jett @@ -0,0 +1,64 @@ +namespace app +function positive_check(value: int64) returns bool: + string marker = "positive" + trace marker + return value > 0 +type Positive = int64 where positive_check(value) +struct Holder: + payload: set[int64] +enum Event: + content(payload: set[int64]) +machine Session: + states: + content(payload: set[int64]) + empty + transitions: + content to empty +function seed() returns set[int64]: + mutable set[int64] values = set.new[int64]() + values = set.add[int64](values, 7) + values = set.add[int64](values, 9) + return values +function record_field() returns TypeField: + return list.get[TypeField](type.fields[Holder](), 0) handle: + return record_field() +function event_field(view source: Event) returns TypeField: + TypeVariant variant = type.variant_value[Event](view source) + return list.get[TypeField](variant.fields, 0) handle: + return event_field(view source) +function session_field(view source: Session) returns TypeField: + TypeMachineState state = type.machine_state_value[Session](view source) + return list.get[TypeField](state.fields, 0) handle: + return session_field(view source) +function read_record(view source: Holder, view field: TypeField) returns set[Positive]: + return type.field_value[Holder, set[Positive]](view source, view field) +function read_event(view source: Event, view field: TypeField) returns set[Positive]: + return type.variant_field_value[Event, set[Positive]](view source, view field) +function read_session(view source: Session, view field: TypeField) returns set[Positive]: + return type.machine_field_value[Session, set[Positive]](view source, view field) +function summarize(items: set[Positive]) returns string: + mutable string output = "" + for item in items: + int64 value = coarsen item + output = "{output}:{value}" + return output +function report() returns string: + Holder record = Holder(payload: seed()) + Event event = Event.content(seed()) + Session session = Session(content, seed()) + TypeField record_selector = record_field() + TypeField event_selector = event_field(view event) + TypeField session_selector = session_field(view session) + string first = summarize(read_record(view record, view record_selector)) + string second = summarize(read_event(view event, view event_selector)) + string third = summarize(read_session(view session, view session_selector)) + trace record + trace event + trace session + return "{first}|{second}|{third}" +function main(stdout: Stdout) returns nothing: + bytes earlier = bytes.from_string("earlier-owned") + list[string] retained = list("kept", "owned") + Stdout.write(view stdout, "before\n") + string value = report() + Stdout.write(view stdout, "after:{value}:{bytes.to_hex(view earlier)}:{list.length(view retained)}\n") diff --git a/tests/native/reflected_nested/set_ready_success.stdout b/tests/native/reflected_nested/set_ready_success.stdout new file mode 100644 index 00000000..792d3093 --- /dev/null +++ b/tests/native/reflected_nested/set_ready_success.stdout @@ -0,0 +1,2 @@ +before +after::7:9|:7:9|:7:9:6561726c6965722d6f776e6564:2 diff --git a/tests/native/reflected_opaque_fields.jett b/tests/native/reflected_opaque_fields.jett new file mode 100644 index 00000000..32b538aa --- /dev/null +++ b/tests/native/reflected_opaque_fields.jett @@ -0,0 +1,61 @@ +namespace app +actor Counter: + mutable int64 count = 1 + receive add(amount: int64): + count = count + amount + receive read responds int64: + respond count +struct Holder: + worker: Counter + output: Stdout +function copy_holder(source: Holder) returns result[Holder, string]: + mutable TypeConstruction builder = type.construct_start[Holder]() + for field in type.fields[Holder](): + comptime type Field = field.type_info: + Field value = type.field_value[Holder, Field](view source, view field) + builder = type.construct_put[Holder, Field](builder, view field, value) handle error: + return fail(error) + return type.construct_finish[Holder](builder) +function pending(view stdout: Stdout) returns nothing: + Counter worker = run run spawn Counter() + Stdout output = run run clone stdout + Holder source = Holder(worker: worker, output: output) + Holder copied = copy_holder(clone source) handle error: + println(error) + return nothing + trace source + trace copied + Counter inner = join copied.worker handle error: + return nothing + Counter ready = join inner handle error: + return nothing + Stdout inner_output = join copied.output handle error: + return nothing + Stdout ready_output = join inner_output handle error: + return nothing + send ready.add(4) + Counter source_inner = join source.worker handle error: + return nothing + Counter source_ready = join source_inner handle error: + return nothing + int64 count = ask source_ready.read + println("pending:{count}") + Stdout.write(view ready_output, "ready\n") + return nothing +function plain(view stdout: Stdout) returns nothing: + Counter worker = spawn Counter() + Holder source = Holder(worker: worker, output: clone stdout) + Holder copied = copy_holder(clone source) handle error: + println(error) + return nothing + trace source + trace copied + send copied.worker.add(4) + int64 count = ask source.worker.read + println("plain:{count}") + Stdout.write(view copied.output, "ready\n") + return nothing +function main(stdout: Stdout) returns nothing: + plain(view stdout) + pending(view stdout) + return nothing diff --git a/tests/native/reflected_struct_foreign_field_failure.jett b/tests/native/reflected_struct_foreign_field_failure.jett new file mode 100644 index 00000000..c93d7ba9 --- /dev/null +++ b/tests/native/reflected_struct_foreign_field_failure.jett @@ -0,0 +1,12 @@ +namespace app +struct User: + age: int64 +struct OtherUser: + age: int64 +function main() returns nothing: + User user = User(age: 7) + TypeField foreign = list.get[TypeField](type.fields[OtherUser](), 0) handle: + return nothing + int64 age = type.field_value[User, int64](view user, view foreign) + println(age) + return nothing diff --git a/tests/native/reflected_struct_foreign_incompatible_failure.jett b/tests/native/reflected_struct_foreign_incompatible_failure.jett new file mode 100644 index 00000000..a3c6aac1 --- /dev/null +++ b/tests/native/reflected_struct_foreign_incompatible_failure.jett @@ -0,0 +1,13 @@ +namespace app +struct User: + id: string + age: int64 +struct OtherUser: + id: string +function main() returns nothing: + User user = User(id: "u1", age: 7) + TypeField foreign = list.get[TypeField](type.fields[OtherUser](), 0) handle: + return nothing + int64 wrong = type.field_value[User, int64](view user, view foreign) + println(wrong) + return nothing diff --git a/tests/native/reflected_struct_missing_candidate_owner_failure.jett b/tests/native/reflected_struct_missing_candidate_owner_failure.jett new file mode 100644 index 00000000..ccf36b00 --- /dev/null +++ b/tests/native/reflected_struct_missing_candidate_owner_failure.jett @@ -0,0 +1,13 @@ +namespace app +struct User: + age: int64 +struct OtherUser: + id: string + age: int64 +function main() returns nothing: + User user = User(age: 7) + TypeField foreign = list.get[TypeField](type.fields[OtherUser](), 1) handle: + return nothing + int64 age = type.field_value[User, int64](view user, view foreign) + println(age) + return nothing diff --git a/tests/native/reflected_struct_requested_type_failure.jett b/tests/native/reflected_struct_requested_type_failure.jett new file mode 100644 index 00000000..bfeb2752 --- /dev/null +++ b/tests/native/reflected_struct_requested_type_failure.jett @@ -0,0 +1,11 @@ +namespace app +struct User: + id: string + age: int64 +function main() returns nothing: + User user = User(id: "u1", age: 7) + TypeField id = list.get[TypeField](type.fields[User](), 0) handle: + return nothing + int64 wrong = type.field_value[User, int64](view user, view id) + println(wrong) + return nothing diff --git a/tests/native/reflected_type_dispatch.jett b/tests/native/reflected_type_dispatch.jett new file mode 100644 index 00000000..f3f1fede --- /dev/null +++ b/tests/native/reflected_type_dispatch.jett @@ -0,0 +1,20 @@ +namespace app +type Label = string +struct Record: + label: Label + count: int64 + tags: list[Label] +function describe(view value: Record) returns string: + mutable list[string] parts = list.new[string]() + for field in type.fields[Record](): + comptime type Field = field.type_info: + Field copied = type.field_value[Record, Field](view value, view field) + parts = list.append[string](parts, "{field.name}:{type.name[Field]()}") + return string.join(parts, ";") +function main() returns nothing: + Record value = Record(label: "Ada", count: 7, tags: list("a", "b")) + println(describe(view value)) + TypeInfo value_arg = type.arg[map[string, list[int64]]](1) + println(value_arg.type_name) + TypeInfo alias_base = type.arg[Label](0) + println(alias_base.type_name) diff --git a/tests/native/reflection_scalars.jett b/tests/native/reflection_scalars.jett new file mode 100644 index 00000000..4c68ad5c --- /dev/null +++ b/tests/native/reflection_scalars.jett @@ -0,0 +1,7 @@ +namespace app +type MyInt = int64 +type OtherInt = int64 +function describe[T]() returns string: + return type.name[T]() +function main() returns nothing: + println(type.name[MyInt](), type.kind[MyInt](), type.has_secret[MyInt](), describe[MyInt](), describe[OtherInt](), describe[int64](), type.name[secret[string]](), type.kind[secret[string]](), type.has_secret[secret[string]]()) diff --git a/tests/native/release_debug_observations.jett b/tests/native/release_debug_observations.jett new file mode 100644 index 00000000..20013623 --- /dev/null +++ b/tests/native/release_debug_observations.jett @@ -0,0 +1,10 @@ +namespace app +function observe(view stdout: Stdout) returns bool: + Stdout.write(view stdout, "condition evaluated\n") + return true +function main(stdout: Stdout) returns nothing: + int64 value = 7 + trace value + breakpoint observe(view stdout) + Stdout.write(view stdout, "program output\n") + return nothing diff --git a/tests/native/reused_handle_locals.jett b/tests/native/reused_handle_locals.jett new file mode 100644 index 00000000..a875619d --- /dev/null +++ b/tests/native/reused_handle_locals.jett @@ -0,0 +1,34 @@ +namespace app +function inspect_view(view source: optional[list[int64]]) returns int64: + list[int64] first = source handle: + default list.new[int64]() + list[int64] second = source handle: + default list.new[int64]() + return list.length[int64](view first) + list.length[int64](view second) +function main() returns nothing: + optional[int64] missing = none + int64 first = missing handle: + default 1 + int64 second = missing handle: + default 2 + println(first, second) + optional[int64] present = some(7) + int64 third = present handle: + default 0 + int64 fourth = present handle: + default 0 + println(third, fourth) + result[int64, string] outcome = ok(5) + int64 fifth = outcome handle error: + default 0 + int64 sixth = outcome handle error: + default 0 + println(fifth, sixth) + optional[list[int64]] owned = some(list(4, 6)) + println(inspect_view(view owned)) + list[int64] first_list = owned handle: + default list.new[int64]() + list[int64] second_list = owned handle: + default list.new[int64]() + println(list.length[int64](view first_list), list.length[int64](view second_list)) + return nothing diff --git a/tests/native/run_local_aggregate_reuse.jett b/tests/native/run_local_aggregate_reuse.jett new file mode 100644 index 00000000..9ce112c7 --- /dev/null +++ b/tests/native/run_local_aggregate_reuse.jett @@ -0,0 +1,25 @@ +namespace app +struct Holder: + values: list[int64] +function main() returns nothing: + optional[list[int64]] value = some(list(1, 2)) + optional[list[int64]] pending = run value + trace value + trace pending + result[int64, string] outcome = ok(7) + result[int64, string] pending_outcome = run outcome + trace outcome + trace pending_outcome + list[int64] items = list(4, 6) + list[int64] pending_items = run items + trace items + trace pending_items + map[string, int64] scores = map("a": 3) + map[string, int64] pending_scores = run scores + trace scores + trace pending_scores + Holder holder = Holder(values: list(9)) + Holder pending_holder = run holder + trace holder + trace pending_holder + return nothing diff --git a/tests/native/run_local_builder_reuse.jett b/tests/native/run_local_builder_reuse.jett new file mode 100644 index 00000000..3bb549c1 --- /dev/null +++ b/tests/native/run_local_builder_reuse.jett @@ -0,0 +1,30 @@ +namespace app +struct User: + name: string +struct Alternate: + code: int64 +function inspect_builder(view builder: TypeConstruction, extra: int64) returns int64: + trace builder + return extra +function main() returns nothing: + TypeConstruction builder = type.construct_start[User]() + TypeConstruction pending = run builder + trace builder + trace pending + optional[TypeConstruction] maybe = some(type.construct_start[User]()) + TypeConstruction first = maybe handle: + default type.construct_start[User]() + TypeConstruction second = maybe handle: + default type.construct_start[User]() + trace maybe + trace first + trace second + mutable TypeConstruction switchable = type.construct_start[User]() + optional[int64] missing = none + int64 selected = inspect_builder(view switchable, missing handle: + switchable = type.construct_start[Alternate]() + default 7 + ) + println(selected) + trace switchable + return nothing diff --git a/tests/native/secret_pending_builder_finish_failure.jett b/tests/native/secret_pending_builder_finish_failure.jett new file mode 100644 index 00000000..d1e57f30 --- /dev/null +++ b/tests/native/secret_pending_builder_finish_failure.jett @@ -0,0 +1,13 @@ +namespace app +struct Vault: + token: secret[string] +function main() returns nothing: + TypeField field = list.get[TypeField](type.fields[Vault](), 0) handle: + return nothing + TypeConstruction builder = type.construct_start[Vault]() + TypeConstruction filled = type.construct_put[Vault, secret[string]](builder, view field, "hidden-builder") handle error: + return nothing + TypeConstruction pending = run filled + Vault unused = type.construct_finish[Vault](pending) handle error: + return nothing + return nothing diff --git a/tests/native/secret_pending_builder_put_failure.jett b/tests/native/secret_pending_builder_put_failure.jett new file mode 100644 index 00000000..8e5e4efa --- /dev/null +++ b/tests/native/secret_pending_builder_put_failure.jett @@ -0,0 +1,13 @@ +namespace app +struct Vault: + token: secret[string] +function main() returns nothing: + TypeField field = list.get[TypeField](type.fields[Vault](), 0) handle: + return nothing + TypeConstruction builder = type.construct_start[Vault]() + TypeConstruction filled = type.construct_put[Vault, secret[string]](builder, view field, "hidden-builder") handle error: + return nothing + TypeConstruction pending = run filled + TypeConstruction unused = type.construct_put[Vault, secret[string]](pending, view field, "hidden-second") handle error: + return nothing + return nothing diff --git a/tests/native/secret_pending_machine_field_owner_failure.jett b/tests/native/secret_pending_machine_field_owner_failure.jett new file mode 100644 index 00000000..3a132b12 --- /dev/null +++ b/tests/native/secret_pending_machine_field_owner_failure.jett @@ -0,0 +1,15 @@ +namespace app +machine Session: + states: + active(name: secret[string]) + transitions: + active to active +function main() returns nothing: + Session session = Session(active, "hidden-owner") + TypeMachineState state = type.machine_state_value[Session](view session) + TypeField field = list.get[TypeField](state.fields, 0) handle: + return nothing + Session pending_session = run Session(active, "hidden-pending-owner") + secret[string] copied = type.machine_field_value[Session, secret[string]](view pending_session, view field) + println(secret.redact(copied)) + return nothing diff --git a/tests/native/secret_pending_machine_state_value_failure.jett b/tests/native/secret_pending_machine_state_value_failure.jett new file mode 100644 index 00000000..1c55fe45 --- /dev/null +++ b/tests/native/secret_pending_machine_state_value_failure.jett @@ -0,0 +1,11 @@ +namespace app +machine Session: + states: + active(name: secret[string]) + transitions: + active to active +function main() returns nothing: + Session session = run Session(active, "hidden-owner") + TypeMachineState state = type.machine_state_value[Session](view session) + println(state.name) + return nothing diff --git a/tests/native/secret_pending_struct_field_owner_failure.jett b/tests/native/secret_pending_struct_field_owner_failure.jett new file mode 100644 index 00000000..182417e8 --- /dev/null +++ b/tests/native/secret_pending_struct_field_owner_failure.jett @@ -0,0 +1,10 @@ +namespace app +struct Record: + name: secret[string] +function main() returns nothing: + Record record = run Record(name: "hidden-owner") + TypeField field = list.get[TypeField](type.fields[Record](), 0) handle: + return nothing + secret[string] copied = type.field_value[Record, secret[string]](view record, view field) + println(secret.redact(copied)) + return nothing diff --git a/tests/native/secret_pending_variant_field_owner_failure.jett b/tests/native/secret_pending_variant_field_owner_failure.jett new file mode 100644 index 00000000..b3d07659 --- /dev/null +++ b/tests/native/secret_pending_variant_field_owner_failure.jett @@ -0,0 +1,12 @@ +namespace app +enum Message: + named(value: secret[string]) +function main() returns nothing: + TypeVariant variant = list.get[TypeVariant](type.variants[Message](), 0) handle: + return nothing + TypeField field = list.get[TypeField](variant.fields, 0) handle: + return nothing + Message pending_message = run Message.named("hidden-owner") + secret[string] copied = type.variant_field_value[Message, secret[string]](view pending_message, view field) + println(secret.redact(copied)) + return nothing diff --git a/tests/native/secret_pending_variant_value_failure.jett b/tests/native/secret_pending_variant_value_failure.jett new file mode 100644 index 00000000..0f0c9b63 --- /dev/null +++ b/tests/native/secret_pending_variant_value_failure.jett @@ -0,0 +1,8 @@ +namespace app +enum Message: + named(value: secret[string]) +function main() returns nothing: + Message message = run Message.named("hidden-owner") + TypeVariant variant = type.variant_value[Message](view message) + println(variant.name) + return nothing diff --git a/tests/native/secret_values.jett b/tests/native/secret_values.jett new file mode 100644 index 00000000..c7e8c373 --- /dev/null +++ b/tests/native/secret_values.jett @@ -0,0 +1,23 @@ +namespace app +struct Credentials: + label: string + token: secret[string] + key: secret[bytes] +function payload() returns bytes: + return bytes.from_string("payload") +function main() returns nothing: + secret[string] token = "agent-key" + secret[string] same = "agent-key" + secret[string] other = "agent-kex" + println(secret.compare(clone token, same), secret.compare(clone token, other), secret.redact(clone token)) + secret[string] longer = "agent-key-longer" + secret[string] unicode = "Jett 🚀" + secret[string] unicode_same = "Jett 🚀" + println(secret.compare(clone token, longer), secret.compare(unicode, unicode_same)) + secret[bytes] key = payload() + secret[bytes] matching = bytes.from_string("payload") + secret[bytes] different = bytes.from_string("payloae") + println(secret.compare(clone key, matching), secret.compare(clone key, different), secret.redact(clone key)) + Credentials first = Credentials(label: "service", token: token, key: key) + Credentials second = clone first + println(first.label, secret.redact(first.token), secret.redact(second.key)) diff --git a/tests/native/set_values.jett b/tests/native/set_values.jett new file mode 100644 index 00000000..fc57d24d --- /dev/null +++ b/tests/native/set_values.jett @@ -0,0 +1,25 @@ +namespace app +function main() returns nothing: + mutable set[int64] numbers = set.new[int64]() + numbers = set.add[int64](numbers, -2) + numbers = set.add[int64](numbers, 7) + numbers = set.add[int64](numbers, -2) + println(set.length[int64](view numbers), set.contains[int64](view numbers, 7)) + numbers = set.remove[int64](numbers, -2) + println(set.length[int64](view numbers), set.contains[int64](view numbers, -2)) + mutable int64 total = 0 + for item in numbers: + total = total + item + println(total) + mutable set[string] names = set.new[string]() + names = set.add[string](names, "ada") + names = set.add[string](names, "bob") + names = set.add[string](names, "ada") + set[string] copy = clone names + names = set.remove[string](names, "ada") + println(set.length[string](view names), set.contains[string](view names, "ada")) + println(set.length[string](view copy), set.contains[string](view copy, "ada")) + for item in view names: + println(item) + for item in copy: + println(item) diff --git a/tests/native/source_function_builtin_prefix_name.jett b/tests/native/source_function_builtin_prefix_name.jett new file mode 100644 index 00000000..bed58040 --- /dev/null +++ b/tests/native/source_function_builtin_prefix_name.jett @@ -0,0 +1,8 @@ +namespace app +function validate(value: int64) returns int64: + return value + 1 +function math(value: int64) returns int64: + return value + 2 +function main() returns nothing: + println(validate(4), math(4), math.abs(-4)) + return nothing diff --git a/tests/native/string_intrinsics.jett b/tests/native/string_intrinsics.jett new file mode 100644 index 00000000..fec495fb --- /dev/null +++ b/tests/native/string_intrinsics.jett @@ -0,0 +1,8 @@ +namespace app +function main() returns nothing: + println(string.replace("x🙂🙂y", "🙂", "Q")) + println(string.replace("á", "a", "X"), string.replace("á", "á", "X")) + println(string.replace("abc", "", "-")) + println(string.slugify(" Crème brûlée & TOMATO ")) + println(string.to_upper_first("áBC"), string.to_lower_first("ÁBC")) + println(string.to_upper_first(""), string.to_lower_first("")) diff --git a/tests/native/string_scalar_iteration.jett b/tests/native/string_scalar_iteration.jett new file mode 100644 index 00000000..51274412 --- /dev/null +++ b/tests/native/string_scalar_iteration.jett @@ -0,0 +1,13 @@ +namespace app + +function emit_scalars(input: string) returns int64: + mutable int64 count = 0 + for scalar in input: + println(scalar) + count = count + 1 + if count == 4: + break + return count + +function main() returns nothing: + println(emit_scalars("👩‍💻ZQ"), emit_scalars("")) diff --git a/tests/native/string_search.jett b/tests/native/string_search.jett new file mode 100644 index 00000000..9536f644 --- /dev/null +++ b/tests/native/string_search.jett @@ -0,0 +1,10 @@ +namespace app +function index_or_missing(value: string, needle: string) returns int64: + int64 index = string.index_of(value, needle) handle: + default -1 + return index +function main() returns nothing: + println(index_or_missing("x🙂🙂y", "🙂"), string.count("x🙂🙂y", "🙂")) + println(index_or_missing("á", "a"), index_or_missing("á", "á")) + println(index_or_missing("abc", ""), string.count("abc", "")) + println(index_or_missing("aaaa", "aa"), string.count("aaaa", "aa")) diff --git a/tests/native/structured_concurrency.jett b/tests/native/structured_concurrency.jett new file mode 100644 index 00000000..685ec061 --- /dev/null +++ b/tests/native/structured_concurrency.jett @@ -0,0 +1,20 @@ +namespace test +function identity(value: string) returns string: + return value +function successful_task() returns result[string, string]: + return ok("ready") +function failed_task() returns result[string, string]: + return fail("failed") +function main() returns nothing: + string pending = run identity("hello") + string joined = join pending handle error: + default "unexpected" + result[string, string] success = run successful_task() + string successful = join success handle error: + default "unexpected" + result[string, string] failure = run failed_task() + string message = join failure handle error: + default error + string cancelled = run identity("discard") + cancel cancelled + println("{joined}:{successful}:{message}") diff --git a/tests/native/structured_concurrency_nothing.jett b/tests/native/structured_concurrency_nothing.jett new file mode 100644 index 00000000..08203eb8 --- /dev/null +++ b/tests/native/structured_concurrency_nothing.jett @@ -0,0 +1,102 @@ +namespace test +struct TaskBundle: + value: nothing +enum TaskValue: + wrapped(value: nothing) + empty +machine TaskState: + states: + ready(value: nothing) + complete + transitions: + ready to complete +function work() returns nothing: + return nothing +function make_pending() returns nothing: + return run work() +function forward(value: nothing) returns nothing: + return value +function capture(value: nothing) returns function() returns nothing: + return function() returns nothing: return value +function choose_pending(enabled: bool) returns nothing: + mutable nothing chosen = nothing + if enabled: + chosen = run work() + return chosen +function pending_depth(value: nothing) returns int64: + mutable nothing current = value + mutable int64 depth = 0 + while depth < 8: + nothing inner = join current handle error: + return depth + current = inner + depth = depth + 1 + return -1 +function check_collections(pending: nothing, nested: nothing) returns nothing: + list[nothing] tasks = list(nothing, pending, nested) + nothing listed = list.get[nothing](view tasks, 2) handle: + default nothing + map[string, nothing] entries = map("work": pending) + nothing mapped = map.get[string, nothing](clone entries, "work") handle: + default nothing + optional[nothing] maybe = some(pending) + nothing optional_value = maybe handle: + default nothing + result[nothing, string] outcome = ok(nested) + trace outcome + nothing result_value = outcome handle error: + default nothing + println(pending_depth(listed), pending_depth(mapped), pending_depth(optional_value), pending_depth(result_value)) + trace tasks + trace entries + optional[nothing] debug_optional = some(pending) + trace debug_optional + return nothing +function check_records(pending: nothing, nested: nothing) returns nothing: + TaskBundle bundle = TaskBundle(value: nested) + TaskValue wrapped = TaskValue.wrapped(pending) + TaskState state = TaskState(ready, nested) + println(pending_depth(bundle.value)) + match wrapped: + wrapped(value): + println(pending_depth(value)) + empty: + println(-1) + if state at ready: + println(pending_depth(state.value)) + trace bundle + TaskValue debug_wrapped = TaskValue.wrapped(nested) + trace debug_wrapped + trace state + println(TaskValue.wrapped(pending) == TaskValue.wrapped(pending)) + println(TaskValue.wrapped(pending) == TaskValue.wrapped(nothing)) + println(TaskValue.wrapped(pending) == TaskValue.wrapped(nested)) + println(TaskValue.wrapped(pending) != TaskValue.wrapped(nested)) + return nothing +function main() returns nothing: + nothing plain = nothing + nothing pending = run work() + nothing nested = run make_pending() + nothing baked = comptime run make_pending() + function() returns nothing baked_capture = comptime capture(run make_pending()) + list[nothing] baked_tasks = comptime list(nothing, run make_pending()) + nothing baked_listed = list.get[nothing](view baked_tasks, 1) handle: + default nothing + println(pending_depth(plain), pending_depth(pending), pending_depth(nested)) + println(pending_depth(baked)) + println(pending_depth(baked_capture()), pending_depth(baked_listed)) + println(pending_depth(forward(nested))) + function(nothing) returns nothing callback = forward + println(pending_depth(callback(pending))) + function() returns nothing captured = capture(nested) + println(pending_depth(captured())) + println(pending_depth(choose_pending(false)), pending_depth(choose_pending(true))) + cancel pending + println(pending_depth(pending)) + println(plain == nothing, plain != nothing) + println(plain, pending, nested) + trace pending + trace nested + check_collections(pending, nested) + check_records(pending, nested) + return nothing diff --git a/tests/native/temporary_projected_view_failure.jett b/tests/native/temporary_projected_view_failure.jett new file mode 100644 index 00000000..d6d76022 --- /dev/null +++ b/tests/native/temporary_projected_view_failure.jett @@ -0,0 +1,20 @@ +namespace app +struct Store: + names: list[string] +struct Envelope: + inner: Store +function make() returns Envelope: + println("owner") + return Envelope(inner: Store(names: list("owned", "temporary"))) +function first(view names: list[string], extra: string) returns string: + string name = list.get[string](view names, 0) handle: + default "empty" + return "{name}:{extra}" +function failing() returns string: + println("argument") + list[string] invalid = list.remove_at[string](list("other", "owner"), -1) + return "{list.length[string](view invalid)}" +function main() returns nothing: + function(view list[string], string) returns string callback = first + println(callback(view make().inner.names, failing())) + return nothing diff --git a/tests/native/temporary_projected_views.jett b/tests/native/temporary_projected_views.jett new file mode 100644 index 00000000..8cb9ba56 --- /dev/null +++ b/tests/native/temporary_projected_views.jett @@ -0,0 +1,63 @@ +namespace app +struct Item: + label: string + score: int64 +struct Store: + names: list[string] + tags: set[string] + rows: map[string, Item] + title: string +struct Envelope: + inner: Store +function make(label: string) returns Envelope: + println("make:{label}") + mutable set[string] tags = set.new[string]() + tags = set.add[string](tags, "tag") + Store inner = Store(names: list("Ada", "Lin"), tags: tags, rows: map("row": Item(label: "owned", score: 7)), title: "🚀é") + return Envelope(inner: inner) +function first(view names: list[string], extra: string) returns string: + string name = list.get[string](view names, 0) handle: + default "empty" + return "{name}:{extra}:{list.length[string](view names)}" +function missing(label: string) returns optional[string]: + println("missing:{label}") + return none +function early() returns string: + return first(view make("early").inner.names, missing("early") handle: + return "returned" + ) +function inspect(view store: Store) returns int64: + mutable int64 count = 0 + for name in view store.names: + println(name) + count = count + 1 + for key, row in view store.rows: + println(key, row.label) + count = count + row.score + return count + list.length[string](view store.names) +function main() returns nothing: + for name in view make("list").inner.names: + println(name) + for tag in view make("set").inner.tags: + println(tag) + for key, row in view make("map").inner.rows: + println(key, row.label, row.score) + for character in view make("string").inner.title: + println(character) + println(inspect(view make("record").inner)) + println(first(view make("direct").inner.names, missing("direct") handle: + default "fallback" + )) + function(view list[string], string) returns string callback = first + println(callback(view make("indirect").inner.names, missing("indirect") handle: + default "fallback" + )) + println(early()) + for name in view make("break").inner.names: + println(name) + break + for name in view make("continue").inner.names: + if name == "Ada": + continue + println(name) + return nothing diff --git a/tests/native/trace_int64.jett b/tests/native/trace_int64.jett new file mode 100644 index 00000000..95ea0c31 --- /dev/null +++ b/tests/native/trace_int64.jett @@ -0,0 +1,7 @@ +namespace app +function main() returns nothing: + mutable int64 total = -7 + trace total + total = total * -2 + trace total + println("done") diff --git a/tests/native/type_arg_alias_out_of_range_failure.jett b/tests/native/type_arg_alias_out_of_range_failure.jett new file mode 100644 index 00000000..2e4ba9ae --- /dev/null +++ b/tests/native/type_arg_alias_out_of_range_failure.jett @@ -0,0 +1,6 @@ +namespace app +type NumberList = list[int64] +function main() returns nothing: + TypeInfo argument = type.arg[NumberList](1) + println(argument.type_name) + return nothing diff --git a/tests/native/type_arg_negative_failure.jett b/tests/native/type_arg_negative_failure.jett new file mode 100644 index 00000000..06e410d6 --- /dev/null +++ b/tests/native/type_arg_negative_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + TypeInfo argument = type.arg[list[int64]](-1) + println(argument.type_name) + return nothing diff --git a/tests/native/type_arg_out_of_range_failure.jett b/tests/native/type_arg_out_of_range_failure.jett new file mode 100644 index 00000000..5a336070 --- /dev/null +++ b/tests/native/type_arg_out_of_range_failure.jett @@ -0,0 +1,5 @@ +namespace app +function main() returns nothing: + TypeInfo argument = type.arg[map[string, int64]](2) + println(argument.type_name) + return nothing diff --git a/tests/native/type_construction_bitfield.jett b/tests/native/type_construction_bitfield.jett new file mode 100644 index 00000000..de6b7e40 --- /dev/null +++ b/tests/native/type_construction_bitfield.jett @@ -0,0 +1,53 @@ +namespace test +enum IpProtocol: + icmp = 1 + tcp = 6 + udp = 17 +bitfield network Header: + version: 4 bits + header_length: 4 bits + protocol: 8 bits as IpProtocol +bitfield TinyNumber: + number: 4 bits +function clone_header_by_reflection() returns bool: + Header source = Header(version: 4, header_length: 5, protocol: IpProtocol.tcp) + mutable TypeConstruction builder = type.construct_start[Header]() + for field in type.fields[Header](): + comptime type Field = field.type_info: + Field value = type.field_value[Header, Field](view source, view field) + builder = type.construct_put[Header, Field](builder, view field, value) handle error: + return false + Header built = type.construct_finish[Header](builder) handle error: + return false + return built.version == 4 and built.header_length == 5 and built.protocol == IpProtocol.tcp +function bitfield_width_message() returns string: + Header source = Header(version: 4, header_length: 5, protocol: IpProtocol.tcp) + mutable TypeConstruction builder = type.construct_start[Header]() + TypeField version = list.get[TypeField](type.fields[Header](), 0) handle: + return "missing version" + builder = type.construct_put[Header, int64](builder, view version, 16) handle error: + return error + for field in type.fields[Header](): + if field.name != "version": + comptime type Field = field.type_info: + Field value = type.field_value[Header, Field](view source, view field) + builder = type.construct_put[Header, Field](builder, view field, value) handle error: + return error + Header built = type.construct_finish[Header](builder) handle error: + return error + return "{built.version}" +function negative_width_message() returns string: + mutable TypeConstruction builder = type.construct_start[TinyNumber]() + for field in type.fields[TinyNumber](): + builder = type.construct_put[TinyNumber, int64](builder, view field, -1) handle error: + return error + TinyNumber built = type.construct_finish[TinyNumber](builder) handle error: + return error + return "{built.number}" +verify type_construction_bitfield: + assert clone_header_by_reflection() == true + assert string.contains(bitfield_width_message(), "4 bit(s) wide") == true +function main() returns nothing: + println(clone_header_by_reflection()) + println(bitfield_width_message()) + println(negative_width_message()) diff --git a/tests/native/type_construction_enum.jett b/tests/native/type_construction_enum.jett new file mode 100644 index 00000000..5d1fd5c1 --- /dev/null +++ b/tests/native/type_construction_enum.jett @@ -0,0 +1,120 @@ +namespace test +enum Direction: + north + south +enum Shape: + circle(radius: float64) + rect(width: float64, height: float64) +enum OtherShape: + circle(radius: float64) + rect(width: float64, height: float64) +function shape_area(shape: Shape) returns float64: + match shape: + circle(radius): + return radius + rect(width, height): + return width * height +function construct_unit_variant() returns bool: + for variant in type.variants[Direction](): + if variant.name == "south": + TypeConstruction builder = type.construct_variant_start[Direction](view variant) handle error: + return false + Direction built = type.construct_finish[Direction](builder) handle error: + return false + return built == Direction.south + return false +function construct_payload_variant_area() returns float64: + for variant in type.variants[Shape](): + if variant.name != "rect": + continue + mutable TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return -1.0 + for field in variant.fields: + comptime type Field = field.type_info: + mutable Field value = 0.0 + if field.name == "width": + value = 3.0 + if field.name == "height": + value = 4.0 + builder = type.construct_put[Shape, Field](builder, view field, value) handle error: + return -2.0 + Shape built = type.construct_finish[Shape](builder) handle error: + return -3.0 + return shape_area(built) + return -4.0 +function missing_payload_message() returns string: + for variant in type.variants[Shape](): + if variant.name != "rect": + continue + mutable TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return error + for field in variant.fields: + if field.name != "width": + continue + comptime type Field = field.type_info: + Field value = 3.0 + builder = type.construct_put[Shape, Field](builder, view field, value) handle error: + return error + Shape built = type.construct_finish[Shape](builder) handle error: + return error + return "unexpected success" + return "missing variant" +function shape_variant_owner_type() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Shape](), 1) handle: + return "missing variant" + return variant.owner_type +function mismatched_variant_owner_message() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[OtherShape](), 1) handle: + return "missing variant" + TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return error + return "unexpected success" +function mismatched_variant_field_owner_message() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Shape](), 1) handle: + return "missing variant" + mutable TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return error + TypeVariant other_variant = list.get[TypeVariant](type.variants[OtherShape](), 1) handle: + return "missing other variant" + TypeField field = list.get[TypeField](other_variant.fields, 0) handle: + return "missing other field" + builder = type.construct_put[Shape, float64](builder, view field, 3.0) handle error: + return error + return "unexpected success" +function mismatched_variant_field_member_message() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Shape](), 1) handle: + return "missing variant" + mutable TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return error + TypeVariant other_variant = list.get[TypeVariant](type.variants[Shape](), 0) handle: + return "missing other variant" + TypeField field = list.get[TypeField](other_variant.fields, 0) handle: + return "missing other field" + builder = type.construct_put[Shape, float64](builder, view field, 3.0) handle error: + return error + return "unexpected success" +function mismatched_variant_finish_target_message() returns string: + TypeVariant variant = list.get[TypeVariant](type.variants[Shape](), 1) handle: + return "missing variant" + TypeConstruction builder = type.construct_variant_start[Shape](view variant) handle error: + return error + OtherShape built = type.construct_finish[OtherShape](builder) handle error: + return error + return "unexpected success" +verify type_construction_enum: + assert construct_unit_variant() == true + assert construct_payload_variant_area() == 12.0 + assert string.contains(missing_payload_message(), "missing required payload field 'height'") == true + assert shape_variant_owner_type() == "test.Shape" + assert string.contains(mismatched_variant_owner_message(), "belongs to 'test.OtherShape', expected 'test.Shape'") == true + assert string.contains(mismatched_variant_field_owner_message(), "belongs to 'test.OtherShape.rect', expected 'test.Shape.rect'") == true + assert string.contains(mismatched_variant_field_member_message(), "belongs to 'test.Shape.circle', expected 'test.Shape.rect'") == true + assert string.contains(mismatched_variant_finish_target_message(), "builder for 'test.Shape' cannot construct 'test.OtherShape'") == true +function main() returns nothing: + println(construct_unit_variant()) + println(construct_payload_variant_area()) + println(missing_payload_message()) + println(mismatched_variant_owner_message()) + println(mismatched_variant_field_owner_message()) + println(mismatched_variant_field_member_message()) + println(mismatched_variant_finish_target_message()) diff --git a/tests/native/type_construction_machine.jett b/tests/native/type_construction_machine.jett new file mode 100644 index 00000000..99178abd --- /dev/null +++ b/tests/native/type_construction_machine.jett @@ -0,0 +1,143 @@ +namespace test +machine Session: + states: + guest + logged_in(user_id: string, display_name: string) + pending(user_id: string, display_name: string) + transitions: + guest to logged_in + guest to pending +machine OtherSession: + states: + guest + logged_in(user_id: string, display_name: string) + transitions: + guest to logged_in +function logged_in_state() returns optional[TypeMachineState]: + list[TypeMachineState] states = type.machine_states[Session]() + return list.get[TypeMachineState](states, 1) +function construct_logged_in_session() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + mutable TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + TypeField user_field = list.get[TypeField](state.fields, 0) handle: + return "missing-user-field" + TypeField display_field = list.get[TypeField](state.fields, 1) handle: + return "missing-display-field" + builder = type.construct_put[Session, string](builder, view user_field, "user-1") handle error: + return error + builder = type.construct_put[Session, string](builder, view display_field, "Ada") handle error: + return error + Session built = type.construct_finish[Session](builder) handle error: + return error + TypeMachineState active = type.machine_state_value[Session](view built) + string user_id = type.machine_field_value[Session, string](view built, view user_field) + return "{active.name}:{user_id}" +function construct_state_qualified_session() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + mutable TypeConstruction builder = type.construct_machine_start[Session at logged_in](view state) handle error: + return error + TypeField user_field = list.get[TypeField](state.fields, 0) handle: + return "missing-user-field" + TypeField display_field = list.get[TypeField](state.fields, 1) handle: + return "missing-display-field" + builder = type.construct_put[Session at logged_in, string](builder, view user_field, "user-2") handle error: + return error + builder = type.construct_put[Session at logged_in, string](builder, view display_field, "Lin") handle error: + return error + Session at logged_in built = type.construct_finish[Session at logged_in](builder) handle error: + return error + string display_name = type.machine_field_value[Session at logged_in, string](view built, view display_field) + return "logged_in:{display_name}" +function state_qualified_mismatch_error() returns string: + list[TypeMachineState] states = type.machine_states[Session at logged_in]() + TypeMachineState guest = list.get[TypeMachineState](states, 0) handle: + return "missing-state" + TypeConstruction builder = type.construct_machine_start[Session at logged_in](view guest) handle error: + return error + Session at logged_in built = type.construct_finish[Session at logged_in](builder) handle error: + return error + return "unexpected" +function missing_payload_error() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + Session built = type.construct_finish[Session](builder) handle error: + return error + return "unexpected" +function state_field_type_binding() returns string: + mutable string summary = "missing" + for state in type.machine_states[Session](): + for field in state.fields: + comptime type Field = field.type_info: + if field.name == "display_name": + summary = type.name[Field]() + return summary +function session_state_owner_type() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + return state.owner_type +function mismatched_state_owner_message() returns string: + list[TypeMachineState] states = type.machine_states[OtherSession]() + TypeMachineState state = list.get[TypeMachineState](states, 1) handle: + return "missing-state" + TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + return "unexpected" +function mismatched_state_field_owner_message() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + mutable TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + list[TypeMachineState] other_states = type.machine_states[OtherSession]() + TypeMachineState other_state = list.get[TypeMachineState](other_states, 1) handle: + return "missing-other-state" + TypeField field = list.get[TypeField](other_state.fields, 0) handle: + return "missing-other-field" + builder = type.construct_put[Session, string](builder, view field, "user-1") handle error: + return error + return "unexpected" +function mismatched_state_field_member_message() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + mutable TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + list[TypeMachineState] states = type.machine_states[Session]() + TypeMachineState other_state = list.get[TypeMachineState](states, 2) handle: + return "missing-other-state" + TypeField field = list.get[TypeField](other_state.fields, 0) handle: + return "missing-other-field" + builder = type.construct_put[Session, string](builder, view field, "user-1") handle error: + return error + return "unexpected" +function mismatched_machine_finish_target_message() returns string: + TypeMachineState state = logged_in_state() handle: + return "missing-state" + TypeConstruction builder = type.construct_machine_start[Session](view state) handle error: + return error + OtherSession built = type.construct_finish[OtherSession](builder) handle error: + return error + return "unexpected" +verify type_construction_machine: + assert construct_logged_in_session() == "logged_in:user-1" + assert construct_state_qualified_session() == "logged_in:Lin" + assert string.contains(state_qualified_mismatch_error(), "cannot finish state 'guest'") == true + assert string.contains(missing_payload_error(), "missing required payload field 'user_id'") == true + assert state_field_type_binding() == "string" + assert session_state_owner_type() == "test.Session" + assert string.contains(mismatched_state_owner_message(), "belongs to machine 'test.OtherSession', expected 'test.Session'") == true + assert string.contains(mismatched_state_field_owner_message(), "belongs to 'test.OtherSession.logged_in', expected 'test.Session.logged_in'") == true + assert string.contains(mismatched_state_field_member_message(), "belongs to 'test.Session.pending', expected 'test.Session.logged_in'") == true + assert string.contains(mismatched_machine_finish_target_message(), "builder for 'test.Session' cannot construct 'test.OtherSession'") == true +function main() returns nothing: + println(construct_logged_in_session()) + println(construct_state_qualified_session()) + println(state_qualified_mismatch_error()) + println(missing_payload_error()) + println(mismatched_state_owner_message()) + println(mismatched_state_field_owner_message()) + println(mismatched_state_field_member_message()) + println(mismatched_machine_finish_target_message()) diff --git a/tests/native/type_construction_struct.jett b/tests/native/type_construction_struct.jett new file mode 100644 index 00000000..94ed9d39 --- /dev/null +++ b/tests/native/type_construction_struct.jett @@ -0,0 +1,105 @@ +namespace app +struct User: + id: string + age: int64 +struct Box[T]: + value: T + label: string +struct Other: + id: string + age: int64 +type Identifier = string +struct AliasField: + id: Identifier +function clone_user() returns string: + User source = User(id: "u1", age: 42) + mutable TypeConstruction builder = type.construct_start[User]() + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + User built = type.construct_finish[User](builder) handle error: + return error + return "{built.id}:{built.age}" +function clone_box() returns string: + Box[string] source = Box[string](value: "payload", label: "name") + mutable TypeConstruction builder = type.construct_start[Box[string]]() + for field in type.fields[Box[string]](): + comptime type Field = field.type_info: + Field value = type.field_value[Box[string], Field](view source, view field) + builder = type.construct_put[Box[string], Field](builder, view field, value) handle error: + return error + Box[string] built = type.construct_finish[Box[string]](builder) handle error: + return error + return "{built.value}:{built.label}" +function alias_field() returns string: + mutable TypeConstruction builder = type.construct_start[AliasField]() + for field in type.fields[AliasField](): + builder = type.construct_put[AliasField, string](builder, view field, "alias") handle error: + return error + AliasField built = type.construct_finish[AliasField](builder) handle error: + return error + return built.id +function missing_field() returns string: + User source = User(id: "u1", age: 42) + mutable TypeConstruction builder = type.construct_start[User]() + for field in type.fields[User](): + if field.name != "age": + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + User built = type.construct_finish[User](builder) handle error: + return error + return built.id +function duplicate_field() returns string: + User source = User(id: "u1", age: 42) + mutable TypeConstruction builder = type.construct_start[User]() + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + if field.name == "id": + Field duplicate = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, duplicate) handle error: + return error + return "no duplicate" +function mismatched_metadata() returns string: + User source = User(id: "u1", age: 42) + mutable TypeConstruction builder = type.construct_start[Other]() + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[Other, Field](builder, view field, value) handle error: + return error + Other built = type.construct_finish[Other](builder) handle error: + return error + return built.id +function mismatched_finish() returns string: + TypeConstruction builder = type.construct_start[User]() + Other built = type.construct_finish[Other](builder) handle error: + return error + return built.id +function cloned_builder() returns string: + User source = User(id: "cloned", age: 17) + mutable TypeConstruction builder = type.construct_start[User]() + for field in type.fields[User](): + comptime type Field = field.type_info: + Field value = type.field_value[User, Field](view source, view field) + builder = type.construct_put[User, Field](builder, view field, value) handle error: + return error + TypeConstruction copy = clone builder + User built = type.construct_finish[User](copy) handle error: + return error + return "{built.id}:{built.age}" +function main() returns nothing: + println(clone_user()) + println(clone_box()) + println(alias_field()) + println(missing_field()) + println(duplicate_field()) + println(mismatched_metadata()) + println(mismatched_finish()) + println(cloned_builder()) diff --git a/tests/native/type_fields.jett b/tests/native/type_fields.jett new file mode 100644 index 00000000..c40afbfd --- /dev/null +++ b/tests/native/type_fields.jett @@ -0,0 +1,28 @@ +namespace app +struct User: + id: string serialize "userId" + token: secret[string] + tags: list[int64] +type UserAlias = User +function reflected_values() returns nothing: + User user = User(id: "Ada", token: "key", tags: list(2, 4)) + for field in type.fields[app.User](): + if field.name == "id": + string id = type.field_value[app.User, string](view user, view field) + println(id, user.id) + if field.name == "token": + secret[string] token = type.field_value[app.User, secret[string]](view user, view field) + println(secret.redact(token), secret.redact(user.token)) + if field.name == "tags": + mutable list[int64] tags = type.field_value[app.User, list[int64]](view user, view field) + tags = list.append[int64](tags, 6) + println(list.length[int64](view tags), list.length[int64](view user.tags)) +function main() returns nothing: + list[TypeField] fields = type.fields[app.User]() + println("field count", list.length[TypeField](view fields)) + for field in fields: + println(field.owner_type, field.name, field.type_name, field.kind_tag == TypeKind.primitive_type, field.serialize_name, field.type_info.type_name, field.has_secret) + list[TypeField] aliases = type.fields[app.UserAlias]() + for field in aliases: + println(field.owner_type, field.name) + reflected_values() diff --git a/tests/native/type_info.jett b/tests/native/type_info.jett new file mode 100644 index 00000000..8a3ac015 --- /dev/null +++ b/tests/native/type_info.jett @@ -0,0 +1,25 @@ +namespace app +type Alias = int64 +function primitive_tag_matches() returns bool: + TypeInfo info = type.info[int64]() + TypePrimitive primitive = clone info.primitive_tag handle: + return false + return primitive == TypePrimitive.int64_type +function direct_primitive_tag_matches() returns bool: + TypePrimitive primitive = type.primitive_tag[int64]() handle: + return false + return primitive == TypePrimitive.int64_type +function alias_has_no_primitive_tag() returns bool: + TypeInfo info = type.info[Alias]() + TypePrimitive primitive = clone info.primitive_tag handle: + return true + return primitive == TypePrimitive.unknown_type +function nested_arg_name() returns string: + TypeInfo nested = type.info[list[Alias]]() + TypeInfo arg = list.get[TypeInfo](view nested.args, 0) handle: + return "missing" + return arg.type_name +function main() returns nothing: + TypeInfo direct = type.info[Alias]() + TypeInfo nested = type.info[list[Alias]]() + println(direct.type_name, direct.kind, direct.kind_tag == TypeKind.alias_type, direct.has_secret, nested.type_name, list.length[TypeInfo](view nested.args), primitive_tag_matches(), type.kind_tag[Alias]() == TypeKind.alias_type, direct_primitive_tag_matches(), alias_has_no_primitive_tag(), nested_arg_name()) diff --git a/tests/native/type_variants.jett b/tests/native/type_variants.jett new file mode 100644 index 00000000..66753c24 --- /dev/null +++ b/tests/native/type_variants.jett @@ -0,0 +1,51 @@ +namespace app +enum Choice: + token(value: secret[string]) + score(count: int64) + batch(items: list[int64]) + empty +type ChoiceAlias = Choice +function reflected_score() returns nothing: + Choice scored = Choice.score(42) + TypeVariant selected = type.variant_value[Choice](view scored) + println(selected.index, selected.owner_type, selected.name, selected.discriminant, selected.has_secret) + for field in selected.fields: + println(field.name, field.type_name) + if field.name == "count": + int64 count = type.variant_field_value[Choice, int64](view scored, view field) + println(count) + TypeVariant empty = type.variant_value[Choice](Choice.empty) + println(empty.index, empty.name, list.length[TypeField](empty.fields)) +function reflected_token() returns nothing: + Choice token_value = Choice.token("key") + TypeVariant token_variant = type.variant_value[Choice](view token_value) + for field in token_variant.fields: + if field.name == "value": + secret[string] copied = type.variant_field_value[Choice, secret[string]](view token_value, view field) + println(secret.redact(copied), token_variant.name) +function reflected_batch() returns nothing: + Choice batch = Choice.batch(list(3, 5)) + TypeVariant batch_variant = type.variant_value[Choice](view batch) + for field in batch_variant.fields: + if field.name == "items": + mutable list[int64] items = type.variant_field_value[Choice, list[int64]](view batch, view field) + items = list.append[int64](items, 7) + println(batch_variant.name, list.length[int64](view items)) +function main() returns nothing: + list[TypeVariant] variants = type.variants[Choice]() + for variant in variants: + println(variant.index, variant.owner_type, variant.name, variant.discriminant, variant.has_secret) + for field in variant.fields: + string member = clone field.owner_member handle: + default "missing" + println(field.index, field.owner_type, member, field.name, field.type_name, field.type_info.type_name) + for arg in field.type_info.args: + println(arg.type_name) + println(variant.name) + list[TypeVariant] aliases = type.variants[ChoiceAlias]() + println(list.length[TypeVariant](view aliases)) + list[TypeVariant] primitive = type.variants[int64]() + println(list.length[TypeVariant](view primitive)) + reflected_score() + reflected_token() + reflected_batch() diff --git a/tests/native/uninhabited_sum_arms.jett b/tests/native/uninhabited_sum_arms.jett new file mode 100644 index 00000000..25935fd2 --- /dev/null +++ b/tests/native/uninhabited_sum_arms.jett @@ -0,0 +1,355 @@ +namespace app +type Count = int64 +type OtherCount = int64 +function identity[T](value: T) returns T: + return value +function unwrap_optional[T](candidate: optional[T]) returns list[T]: + T item = candidate handle: + return list() + T copied = identity(item) + trace copied + breakpoint + return list(copied) +function unwrap_result[T, E](candidate: result[T, E]) returns list[T]: + T item = candidate handle error: + trace error + return list() + trace item + return list(item) +function collect_errors[T, E](candidate: result[T, E]) returns list[E]: + T item = candidate handle error: + E copied = identity(error) + trace copied + breakpoint + return list(copied) + trace item + return list() +function captured_optional[T](candidate: optional[T]) returns list[T]: + T item = candidate handle: + return list() + function() returns T closure = function() returns T: return item + T copied = closure() + trace copied + breakpoint + return list(copied) +function optional_with_default[T](candidate: optional[T], fallback: list[T]) returns list[T]: + T item = candidate handle: + default list.first[T](view fallback) handle: return list() + trace item + return list(item) +function result_with_default[T, E](candidate: result[T, E], fallback: list[T]) returns list[T]: + T item = candidate handle error: + trace error + default list.first[T](view fallback) handle: return list() + trace item + return list(item) +function reuse_optional[T](candidate: optional[T]) returns int64: + optional[T] saved = clone candidate + list[T] first = optional_with_default(saved, list()) + list[T] second = optional_with_default(candidate, list()) + return list.length[T](view first) + list.length[T](view second) +function reuse_result[T, E](candidate: result[T, E]) returns int64: + result[T, E] saved = clone candidate + list[T] first = unwrap_result(saved) + list[T] second = unwrap_result(candidate) + return list.length[T](view first) + list.length[T](view second) +function reuse_errors[T, E](candidate: result[T, E]) returns int64: + result[T, E] saved = clone candidate + list[E] first = collect_errors(saved) + list[E] second = collect_errors(candidate) + return list.length[E](view first) + list.length[E](view second) +function handler_counts() returns string: + int64 absent = list.length(view unwrap_optional(none)) + int64 failure = list.length(view unwrap_result(fail("bad"))) + int64 success = list.length(view collect_errors(ok(8))) + return "{absent}:{failure}:{success}" +function reuse_counts() returns string: + int64 absent = reuse_optional(none) + int64 failure = reuse_result(fail("reuse")) + int64 success = reuse_result(ok(8)) + int64 errors = reuse_errors(ok(8)) + return "{absent}:{failure}:{success}:{errors}" +function default_counts() returns string: + int64 absent = list.length(view optional_with_default(none, list())) + int64 failure = list.length(view result_with_default(fail("default"), list())) + return "{absent}:{failure}" +function transform_counts() returns string: + int64 reversed = list.length(view list.reverse(list())) + int64 skipped = list.length(view list.skip(list(), 1)) + int64 taken = list.length(view list.take(list(), 1)) + int64 flattened = list.length(view list.flatten(list(list(), list()))) + int64 absent = list.length(view unwrap_optional(list.first(view list()))) + return "{reversed}:{skipped}:{taken}:{flattened}:{absent}" +function optional_source[T](values: list[T], view stdout: Stdout) returns optional[T]: + Stdout.write(view stdout, "optional source\n") + return list.first[T](view values) +function result_source[T](values: list[T], view stdout: Stdout) returns result[T, string]: + Stdout.write(view stdout, "result source\n") + T item = list.first[T](view values) handle: + return fail("absent") + Stdout.write(view stdout, "unexpected success\n") + return ok(item) +function effectful_handlers(view stdout: Stdout) returns nothing: + int64 absent = list.length(view unwrap_optional(optional_source(list(), view stdout))) + Stdout.write(view stdout, "effect optional:{absent}\n") + int64 failure = list.length(view unwrap_result(result_source(list(), view stdout))) + Stdout.write(view stdout, "effect failure:{failure}\n") +function contextual_counts() returns string: + optional[Count] absent = none + result[Count, string] failure = fail("concrete") + result[Count, string] success = ok(8) + int64 absent_count = list.length(view unwrap_optional(absent)) + int64 failure_count = list.length(view unwrap_result(failure)) + int64 error_count = list.length(view collect_errors(success)) + return "{absent_count}:{failure_count}:{error_count}" +interface Named: + function name(view self: Named) returns string +implement Named for int64: + function name(view self: int64) returns string: + return "number:{self}" +function callback() returns int64: + return 9 +function callback_counts() returns string: + Named named = 8 + int64 interface_count = list.length(view collect_errors(ok(named))) + int64 function_count = list.length(view collect_errors(ok(callback))) + int64 captured = list.length(view captured_optional(none)) + return "{interface_count}:{function_count}:{captured}" +function joined_optional[T](candidate: optional[T]) returns list[T]: + T item = candidate handle: + return list() + trace item + T once = join item handle error: return list() + trace once + T ready = join once handle error: return list() + trace ready + return list(ready) +function joined_result[T, E](candidate: result[T, E]) returns list[T]: + T item = candidate handle error: + trace error + return list() + trace item + T once = join item handle error: return list() + trace once + T ready = join once handle error: return list() + trace ready + return list(ready) +function pending_payload_counts() returns string: + int64 absent = list.length(view joined_optional(some(run run 6))) + int64 success = list.length(view joined_result(ok(run run 8))) + return "{absent}:{success}" +function live_after_empty_loop() returns function(int64) returns int64: + for value in list(): + function() returns int64 dead = function() returns int64: return value + int64 copied = dead() + trace copied + breakpoint + int64 offset = 30 + return function(number: int64) returns int64: return offset + number +function live_after_success[T, E](candidate: result[T, E], offset: int64) returns function(int64) returns int64: + T item = candidate handle error: + E captured = identity(error) + function() returns E dead = function() returns E: return captured + E copied = dead() + trace copied + breakpoint + return function(number: int64) returns int64: return number + return function(number: int64) returns int64: return offset + number +function surviving_callbacks() returns string: + function(int64) returns int64 after_loop = live_after_empty_loop() + function(int64) returns int64 after_success = live_after_success(ok(8), 40) + return "{after_loop(7)}:{after_success(9)}" +function present_values[T](candidate: optional[T]) returns list[T]: + T item = candidate handle: return list() + return list(item) +function successful_values[T, E](candidate: result[T, E]) returns list[T]: + T item = candidate handle error: return list() + return list(item) +function collect_optionals[T](candidates: list[optional[T]]) returns list[T]: + mutable list[T] output = list() + for candidate in candidates: + output = list.concat[T](output, present_values[T](candidate)) + return output +function collect_results[T, E](candidates: list[result[T, E]]) returns list[T]: + mutable list[T] output = list() + for candidate in candidates: + output = list.concat[T](output, successful_values[T, E](candidate)) + return output +function values_text[T](values: list[T]) returns string: + mutable string text = "" + for value in values: + text = "{text}{value}," + return text +function mixed_sum_values() returns string: + string absent_first = values_text(collect_optionals(list(none, some(7)))) + string present_first = values_text(collect_optionals(list(some(7), none))) + string success_first = values_text(collect_results(list(ok(8), fail("bad")))) + string failure_first = values_text(collect_results(list(fail("bad"), ok(8)))) + return "{absent_first}:{present_first}:{success_first}:{failure_first}" +function nested_map_values[K, V](maps: list[map[K, V]]) returns list[V]: + mutable list[V] output = list() + for entries in maps: + for key, value in entries: + output = list.append[V](output, value) + return output +function mixed_nested_values() returns string: + string empty_list_first = values_text(list.flatten(list(list(), list(9)))) + string empty_list_last = values_text(list.flatten(list(list(9), list()))) + string empty_map_first = values_text(nested_map_values(list(map(), map("key": 10)))) + string empty_map_last = values_text(nested_map_values(list(map("key": 10), map()))) + return "{empty_list_first}:{empty_list_last}:{empty_map_first}:{empty_map_last}" +function make_absent[T](values: list[T]) returns function() returns optional[T]: + return function() returns optional[T]: return none +function present() returns optional[int64]: + return some(7) +function collect_callbacks[T](callbacks: list[function() returns optional[T]]) returns list[T]: + mutable list[T] output = list() + for callback in callbacks: + output = list.concat[T](output, present_values[T](callback())) + return output +function mixed_callback_values() returns string: + string absent_first = values_text(collect_callbacks(list(make_absent(list()), present))) + string present_first = values_text(collect_callbacks(list(present, make_absent(list())))) + return "{absent_first}:{present_first}" +function decorated_rows[T](rows: list[list[T]], extra: T) returns string: + mutable string text = "" + for row in rows: + list[T] augmented = list.append[T](clone row, clone extra) + list[T] ordered = list.sort[T](augmented) + T first = list.get[T](view ordered, 0) handle: return "missing" + text = "{text}{first}:{list.length[T](view ordered)}:{list.length[T](view row)};" + return text +function mixed_string_rows() returns string: + string empty_first = decorated_rows(list(list(), list("zebra", "apple")), "added") + string empty_last = decorated_rows(list(list("zebra", "apple"), list()), "added") + return "{empty_first}|{empty_last}" +function make_empty[T](view values: list[T]) returns function() returns list[T]: + return function() returns list[T]: return list() +function words() returns list[string]: + return list("zebra", "apple") +function callback_rows[T](callbacks: list[function() returns list[T]]) returns list[list[T]]: + mutable list[list[T]] output = list() + for callback in callbacks: + output = list.append[list[T]](output, callback()) + return output +function mixed_row_callbacks() returns string: + string empty_first = decorated_rows(callback_rows(list(make_empty(view list()), words)), "added") + string empty_last = decorated_rows(callback_rows(list(words, make_empty(view list()))), "added") + return "{empty_first}|{empty_last}" +function defaulted[T](candidate: optional[T]) returns string: + int64 found = candidate handle: default 7 + list[T] original = present_values(candidate) + return "{found}:{list.length[T](view original)}" +function defaulted_view[T](view candidate: optional[T]) returns string: + int64 found = candidate handle: default 7 + list[T] original = present_values(clone candidate) + return "{found}:{list.length[T](view original)}" +function source_optional[T](view values: list[T]) returns optional[T]: + return list.first[T](view values) +function defaulted_call[T](values: list[T]) returns string: + int64 found = source_optional[T](view values) handle: default 7 + return "{found}:{list.length[T](view values)}" +function nested_owned[T](candidate: optional[list[T]]) returns string: + list[string] found = candidate handle: default list("fallback") + list[string] ordered = list.sort[string](list.append[string](found, "added")) + string first = list.get[string](view ordered, 0) handle: return "missing" + list[list[T]] original = present_values(candidate) + list[T] row = list.get[list[T]](view original, 0) handle: default list() + return "{first}:{list.length[string](view ordered)}:{list.length[list[T]](view original)}:{list.length[T](view row)}" +function nested_view[T](view candidate: optional[list[T]]) returns string: + list[string] found = candidate handle: default list("fallback") + list[string] ordered = list.sort[string](list.append[string](found, "added")) + string first = list.get[string](view ordered, 0) handle: return "missing" + list[list[T]] original = present_values(clone candidate) + list[T] row = list.get[list[T]](view original, 0) handle: default list() + return "{first}:{list.length[string](view ordered)}:{list.length[list[T]](view original)}:{list.length[T](view row)}" +function second[T](first: T, second: T) returns T: + return second +function repeated_values() returns string: + string present = values_text(present_values(second(none, some(7)))) + string absent = values_text(present_values(second(some(7), none))) + string words_second = decorated_rows(list(second(list(), list("zebra", "apple"))), "added") + string empty_second = decorated_rows(list(second(list("zebra", "apple"), list())), "added") + string callback_second = decorated_rows(callback_rows(list(second(make_empty(view list()), words))), "added") + string empty_callback_second = decorated_rows(callback_rows(list(second(words, make_empty(view list())))), "added") + return "{present}:{absent}|{words_second}|{empty_second}|{callback_second}|{empty_callback_second}" +function boundary_controls() returns string: + string owned = defaulted(none) + string borrowed = defaulted_view(view none) + string called = defaulted_call(list()) + string nested = nested_owned(some(list())) + string nested_borrowed = nested_view(view some(list())) + return "local={owned};view={borrowed};call={called};nested={nested}|{nested_borrowed};second={repeated_values()}" +function describe[T](view first: T, view last: T) returns string: + return type.name[T]() +function outer[T](view empty: list[T]) returns string: + string empty_first = describe(view empty, view list(7)) + string empty_last = describe(view list(7), view empty) + return "{empty_first}:{empty_last}" +function payload_name[T](view candidate: optional[T]) returns string: + return type.name[T]() +function payload_describe[T](view first: optional[T], view last: optional[T]) returns string: + return type.name[T]() +function reflection_controls() returns string: + Count count = 7 + OtherCount other_count = 8 + optional[Count] named = some(clone count) + optional[OtherCount] other_named = some(clone other_count) + string literal_first = describe(view list(), view list(7)) + string literal_last = describe(view list(7), view list()) + string first_alias = payload_describe(view named, view other_named) + string other_alias = payload_describe(view other_named, view named) + string named_first_alias = payload_describe(last: view other_named, first: view named) + string named_other_alias = payload_describe(last: view named, first: view other_named) + return "outer={outer(view list())};literal={literal_first}:{literal_last};payload={payload_name(view named)};root={describe(view count, view other_count)};aliases={first_alias}:{other_alias}:{named_first_alias}:{named_other_alias}" +function main(stdout: Stdout) returns nothing: + Stdout.write(view stdout, "handlers:{handler_counts()}\n") + Stdout.write(view stdout, "reuse:{reuse_counts()}:default:{default_counts()}\n") + Stdout.write(view stdout, "transforms:{transform_counts()}\n") + effectful_handlers(view stdout) + Stdout.write(view stdout, "context:{contextual_counts()}:callbacks:{callback_counts()}\n") + string baked_handlers = comptime handler_counts() + string baked_reuse = comptime reuse_counts() + string baked_transforms = comptime transform_counts() + Stdout.write(view stdout, "baked:{baked_handlers}:{baked_reuse}:{baked_transforms}\n") + Stdout.write(view stdout, "payloads:{pending_payload_counts()}\n") + Stdout.write(view stdout, "surviving:{surviving_callbacks()}\n") + Stdout.write(view stdout, "mixed-sums:{mixed_sum_values()}\n") + Stdout.write(view stdout, "mixed-nested:{mixed_nested_values()}\n") + Stdout.write(view stdout, "mixed-callbacks:{mixed_callback_values()}\n") + Stdout.write(view stdout, "mixed-rows:{mixed_string_rows()}\n") + Stdout.write(view stdout, "mixed-row-callbacks:{mixed_row_callbacks()}\n") + Stdout.write(view stdout, "boundary:{boundary_controls()}\n") + Stdout.write(view stdout, "reflection:{reflection_controls()}\n") +verify uninhabited_handlers: + assert handler_counts() == "0:0:0" + assert transform_counts() == "0:0:0:0:0" + assert list.length(view captured_optional(none)) == 0 + string baked_handlers = comptime handler_counts() + string baked_reuse = comptime reuse_counts() + assert baked_handlers == "0:0:0" + assert baked_reuse == "0:0:2:0" + assert surviving_callbacks() == "37:49" + assert mixed_sum_values() == "7,:7,:8,:8," + assert mixed_nested_values() == "9,:9,:10,:10," + assert mixed_callback_values() == "7,:7," + assert mixed_string_rows() == "added:1:0;added:3:2;|added:3:2;added:1:0;" + assert mixed_row_callbacks() == "added:1:0;added:3:2;|added:3:2;added:1:0;" + assert boundary_controls() == "local=7:0;view=7:0;call=7:0;nested=added:1:1:0|added:1:1:0;second=7,:|added:3:2;|added:1:0;|added:3:2;|added:1:0;" + assert reflection_controls() == "outer=list[int64]:list[int64];literal=list[int64]:list[int64];payload=app.Count;root=int64;aliases=app.Count:app.OtherCount:app.Count:app.OtherCount" +property uninhabited_handler_trials: + given seed: int64 + assert seed + list.length(view unwrap_optional(none)) == seed + assert list.length(view unwrap_result(fail("bad"))) == 0 + assert list.length(view collect_errors(ok(8))) == 0 + function(int64) returns int64 after_loop = live_after_empty_loop() + function(int64) returns int64 after_success = live_after_success(ok(8), 40) + assert after_loop(seed) == seed + 30 + assert after_success(seed) == seed + 40 + assert mixed_sum_values() == "7,:7,:8,:8," + assert mixed_nested_values() == "9,:9,:10,:10," + assert mixed_callback_values() == "7,:7," + assert mixed_string_rows() == "added:1:0;added:3:2;|added:3:2;added:1:0;" + assert mixed_row_callbacks() == "added:1:0;added:3:2;|added:3:2;added:1:0;" + assert boundary_controls() == "local=7:0;view=7:0;call=7:0;nested=added:1:1:0|added:1:1:0;second=7,:|added:3:2;|added:1:0;|added:3:2;|added:1:0;" + assert reflection_controls() == "outer=list[int64]:list[int64];literal=list[int64]:list[int64];payload=app.Count;root=int64;aliases=app.Count:app.OtherCount:app.Count:app.OtherCount" diff --git a/tests/native/unit_enum.jett b/tests/native/unit_enum.jett new file mode 100644 index 00000000..3cdb5b7d --- /dev/null +++ b/tests/native/unit_enum.jett @@ -0,0 +1,28 @@ +namespace app +enum Direction: + north + south + east + west +function opposite(value: Direction) returns Direction: + match value: + north: + return Direction.south + south: + return Direction.north + east: + return Direction.west + west: + return Direction.east +function label(value: Direction) returns string: + match value: + north: + return "north" + other: + return "other" +function main() returns nothing: + Direction original = Direction.north + Direction duplicate = clone original + println(label(duplicate), label(original)) + println(label(opposite(Direction.south)), label(opposite(Direction.east))) + println(Direction.north == Direction.north, Direction.north != Direction.south) diff --git a/tests/native/uuid_values.jett b/tests/native/uuid_values.jett new file mode 100644 index 00000000..da6efb37 --- /dev/null +++ b/tests/native/uuid_values.jett @@ -0,0 +1,14 @@ +namespace app +function main() returns nothing: + string id = uuid.new() + optional[string] version_char = string.char_at(id, 14) + string version = version_char handle: + default "" + optional[string] variant_char = string.char_at(id, 19) + string variant = variant_char handle: + default "" + optional[string] first_separator = string.char_at(id, 8) + string separator = first_separator handle: + default "" + bool valid_variant = variant == "8" or variant == "9" or variant == "a" or variant == "b" + println(string.char_count(id) == 36, version == "4", separator == "-", valid_variant) diff --git a/tests/native/view_function_values.jett b/tests/native/view_function_values.jett new file mode 100644 index 00000000..5bce9b30 --- /dev/null +++ b/tests/native/view_function_values.jett @@ -0,0 +1,42 @@ +namespace test +function count(view items: list[int64]) returns int64: + return list.length[int64](view items) +function make_offset(delta: int64) returns function(view list[int64]) returns int64: + return function(view items: list[int64]) returns int64: return list.length[int64](view items) + delta +function combine(view items: list[int64], value: int64) returns int64: + return list.length[int64](view items) + value +function combine_extra(view items: list[int64], value: int64) returns int64: + return list.length[int64](view items) + value + 100 +function combine_owned(view items: list[int64], more: list[int64]) returns int64: + return list.length[int64](view items) + list.length[int64](view more) +function apply[T](view items: list[T], callback: function(view list[T]) returns int64) returns int64: + return callback(view items) +function main() returns nothing: + list[int64] items = list(1, 2, 3) + function(view list[int64]) returns int64 callback = count + println(callback(view items)) + println(list.length[int64](view items)) + list[int64] transient = list(4, 5) + println(callback(transient)) + function(view list[int64]) returns int64 captured = make_offset(10) + println(captured(view items)) + function(view list[int64], int64) returns int64 mixed = combine + println(mixed(view items, 7)) + function(view list[int64], list[int64]) returns int64 mixed_owned = combine_owned + println(mixed_owned(view items, list(4, 5))) + println(list.length[int64](view items)) + function(view list[int64]) returns int64 baked = comptime make_offset(20) + println(baked(view items)) + function(view list[int64]) returns int64 baked_named = comptime count + println(baked_named(view items)) + println(items into view callback) + println(apply[int64](view items, count)) + println(apply(view items, captured)) + mutable function(view list[int64], int64) returns int64 selected = combine + optional[int64] no_extra = none + int64 selected_answer = items into view selected(no_extra handle: + selected = combine_extra + default 4 + ) + println(selected_answer) + return nothing diff --git a/tests/native/wrapped_enum_equality.jett b/tests/native/wrapped_enum_equality.jett new file mode 100644 index 00000000..98bde3b2 --- /dev/null +++ b/tests/native/wrapped_enum_equality.jett @@ -0,0 +1,113 @@ +namespace app +struct Entry: + id: int8 + label: string +implement Equatable for Entry: + function equals(view self: Entry, view other: Entry) returns bool: + return self.id == other.id +enum Choice: + empty + number(value: int8) + numbers(values: list[int64]) + entry(value: Entry) +type Selected = Choice where true +type Inherited = Selected where true +type Hidden = secret[Choice] where true +struct Container: + empty: secret[Choice] +enum Simple: + empty + number(value: int8) +function equal[T](view left: secret[T], view right: secret[T]) returns bool: + return declassify(left == right) +function different[T](view left: secret[T], view right: secret[T]) returns bool: + return declassify(left != right) +function mixed_left[T](view left: secret[T], view right: T) returns bool: + return declassify(left == right) +function mixed_right[T](view left: T, view right: secret[T]) returns bool: + return declassify(left != right) +function units() returns bool: + secret[Choice] first = Choice.empty + secret[Choice] same = Choice.empty + Choice different_value = Choice.number(1) + bool direct = declassify(first == Choice.empty) + return direct && equal[Choice](view first, view same) && mixed_right[Choice](view different_value, view first) +function simple() returns bool: + secret[Simple] first = Simple.number(-128) + Simple same = Simple.number(-128) + Simple changed = Simple.number(127) + return mixed_left[Simple](view first, view same) && mixed_right[Simple](view changed, view first) +function numbers() returns bool: + secret[Choice] first = Choice.number(-128) + Choice same = Choice.number(-128) + secret[Choice] changed = Choice.number(127) + return mixed_left[Choice](view first, view same) && different[Choice](view first, view changed) +function collections() returns bool: + secret[Choice] first = Choice.numbers(list(7, 9)) + secret[Choice] same = Choice.numbers(list(7, 9)) + secret[Choice] changed = Choice.numbers(list(7, 10)) + return equal[Choice](view first, view same) && different[Choice](view first, view changed) +function entries() returns bool: + secret[Choice] first = Choice.entry(Entry(id: 7, label: "first")) + secret[Choice] same = Choice.entry(Entry(id: 7, label: "ignored")) + Choice changed = Choice.entry(Entry(id: 8, label: "first")) + return equal[Choice](view first, view same) && mixed_right[Choice](view changed, view first) +function projected() returns bool: + Container holder = Container(empty: Choice.entry(Entry(id: 7, label: "projected"))) + secret[Choice] same = Choice.entry(Entry(id: 7, label: "ignored")) + return equal[Choice](view holder.empty, view same) +function refinements() returns bool: + Inherited first = Choice.entry(Entry(id: 7, label: "first")) handle error: + return false + secret[Inherited] hidden = clone first + Inherited matching = Choice.entry(Entry(id: 7, label: "ignored")) handle error: + return false + secret[Inherited] same = clone matching + return equal[Inherited](view hidden, view same) && mixed_left[Inherited](view hidden, view first) +function secret_refinements() returns bool: + Hidden first = Choice.entry(Entry(id: 7, label: "first")) handle error: + return false + Hidden same = Choice.entry(Entry(id: 7, label: "ignored")) handle error: + return false + secret[bool] matches = first == same + return declassify matches +function pending() returns bool: + secret[Choice] task = run run Choice.entry(Entry(id: 7, label: "pending")) + secret[Choice] inner = join task handle error: + return false + secret[Choice] ready = join inner handle error: + return false + secret[Choice] same = Choice.entry(Entry(id: 7, label: "ignored")) + return equal[Choice](view ready, view same) +function baked_values() returns bool: + secret[Choice] baked = comptime Choice.entry(Entry(id: 7, label: "baked")) + secret[Choice] same = Choice.entry(Entry(id: 7, label: "ignored")) + secret[Simple] number = comptime Simple.number(-128) + Simple matching = Simple.number(-128) + return equal[Choice](view baked, view same) && mixed_left[Simple](view number, view matching) +function primitive_arithmetic() returns bool: + secret[int8] hidden = 7 + int8 plain = 2 + secret[int8] added = hidden + plain + secret[int8] subtracted = plain - hidden + secret[int8] divided = hidden / plain + secret[int8] remainder = hidden modulo plain + secret[bool] ordered = plain < hidden + int8 negative = -5 + return declassify added == 9 && declassify subtracted == negative && declassify divided == 3 && declassify remainder == 1 && declassify ordered +function primitive_logic() returns bool: + secret[bool] hidden = true + bool plain = false + secret[bool] both = hidden && plain + secret[bool] either = plain || hidden + secret[bool] same = hidden == true + return !(declassify both) && declassify either && declassify same +function main(stdout: Stdout) returns nothing: + bool baked_units = comptime units() + bool baked_numbers = comptime numbers() + bool baked_collections = comptime collections() + bool baked_entries = comptime entries() + bool baked_refinements = comptime refinements() + bool baked_secret_refinements = comptime secret_refinements() + Stdout.write(view stdout, "{units()} {simple()} {numbers()} {collections()} {entries()} {projected()} {refinements()} {secret_refinements()} {pending()} {baked_values()} {primitive_arithmetic()} {primitive_logic()}\n") + Stdout.write(view stdout, "{baked_units} {baked_numbers} {baked_collections} {baked_entries} {baked_refinements} {baked_secret_refinements}\n") diff --git a/tests/native_parity.json b/tests/native_parity.json new file mode 100644 index 00000000..99b89824 --- /dev/null +++ b/tests/native_parity.json @@ -0,0 +1,212 @@ +{ + "version": 1, + "fixtures": [ + {"path": "tests/run_pass/actor_counter.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/actor_named_arguments.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/bitfield_payload_roundtrip.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/bitfield_roundtrip.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/bitfield_uint64_reflection.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/bitfield_uint64_roundtrip.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/breakpoint_basic.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/bytes_boundary_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/bytes_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/captured_local_function_name.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/clock_production.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/clock_scripted.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success", "clock_test_samples": [{"wall": {"unix_seconds": "0", "nanoseconds": 0}}, {"wall": {"unix_seconds": "0", "nanoseconds": 0}}, {"wall": {"unix_seconds": "-1", "nanoseconds": 999999999}}, {"wall": {"unix_seconds": "42", "nanoseconds": 123456789}}, {"wall": {"unix_seconds": "40", "nanoseconds": 0}}]}, + {"path": "tests/run_pass/closures.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/closures_advanced.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/comptime_pure_function_boundary.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/comptime_type_bind.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/conversions.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/crypto.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/csv_escaping_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/csv_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/encoding.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/enum_advanced.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/environment_snapshot.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success", "environment_test_snapshot": {"arguments": [{"unicode":"first"},{"unicode":""},{"unicode":"third"}], "entries": [{"name":{"unicode":"PRESENT"},"value":{"unicode":"value"}},{"name":{"unicode":"DUPLICATE"},"value":{"unicode":"first"}},{"name":{"unicode":"DUPLICATE"},"value":{"unicode":"second"}},{"name":{"unicode":"BROKEN"},"value":{"invalid_unicode":true}},{"name":{"invalid_unicode":true},"value":{"unicode":"ignored"}}]}}, + {"path": "tests/run_pass/error_handling.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/escape_sequences.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/explicit_comptime_expression.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/explicit_struct_equality.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/fibonacci.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/generic_function.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/generic_function_value_wrappers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/generic_reflection_branch_specialization.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/generic_reflection_match_specialization.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/generic_struct.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/graphics_callback_runtime_error.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "expected_failure", "graphics_test_events": [{"key":"right"}]}, + {"path": "tests/run_pass/graphics_pipeline_scripted.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success", "graphics_test_events": [{"key":"right"}, "close", {"key":"left"}, "close", {"key":"right"}, "close"]}, + {"path": "tests/run_pass/graphics_scene.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/graphics_scripted.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success", "graphics_test_events": [{"key":"right"}, {"key":"right"}, {"key":"left"}, "close"]}, + {"path": "tests/run_pass/handle_result_optional.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/hello_print.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/inline_functions.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/integer_nonzero_proofs.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/integer_wrapping_and_float_ieee.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_bitfield_shapes.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_enum_bitfield_exact_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_enum_shapes.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_namespace_duplicate_machine_envelope.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_collection_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_error_parity.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_exact.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_exact_container_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_exact_primitive_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_exact_secret_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_machine_envelope.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_raw.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_refinement_valid.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_parse_success_parity.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_public_secret_policy.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_raw_facade_tree_surface.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_raw_strict_accessors.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_raw_tree_parity.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_raw_value_access_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_refinement_exact_serialize_edges.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_bridge_parity.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_flat_decoder.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_flat_serializer.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_nested_decoder.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_nested_serializer.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_reflection_parse_wrapper.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_result_shape_diagnostics.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_roundtrip_user.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_runtime_reflection_metadata.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/json_serialize.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_serialize_machine_envelope.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_serialize_public.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_serialize_public_omits_secret_fields.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_shape_matrix.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_sized_primitives.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_stdlib_bridge_delegation.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_accessors.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_parse_compound.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_parse_runtime.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/json_tree_parse_scalar.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_reflection_construction.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_reflection_parse_wrapper.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_reflection_variant_metadata.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_tree_value.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/json_unknown_fields.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_access_source.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_extras.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_higher_order.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_map_extra.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_shape_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_sort_uint64.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/list_source_surface.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/logical_ops.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/loops.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/map_advanced.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/map_from_lists_duplicate_keys.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/map_merge_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/map_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/map_set_source_surface.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_advanced.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_error_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_extra.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_integer_wrapping.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_sum_source.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/math_trig.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/multi_verify.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/named_argument_runtime_order.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/native_scalar_entry.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/namespace_bitfield_enum_roundtrip.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/namespace_comptime_reflection_aliases.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_dotted_qualified_types.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_duplicate_leaf_actors.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_duplicate_leaf_interfaces.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/namespace_duplicate_leaf_types.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_exports_syntax.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_machine_branch_narrowing.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_qualified_actors.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_qualified_functions.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_qualified_interface_implement.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/namespace_qualified_types.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_runtime_main_context.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/namespace_runtime_verify_context.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_strict_imports.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/namespace_use_alias.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/numeric_literal_contexts.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/pipeline_into.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/primitive_collection_hash_types.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/property_generic_lists.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/property_recursive_structs.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/random_production.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/random_scripted.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success", "random_test_samples": [{"bounded":"0"},{"bounded":"18446744073709551614"},{"unit53":"0"},{"unit53":"9007199254740991"},{"boolean":false},{"boolean":true},{"bounded":"0"},{"bounded":"2"},{"bounded":"0"},{"bounded":"1"},{"bounded":"0"}]}, + {"path": "tests/run_pass/range_step_boundaries.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/recursive_owned_values.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_aliases.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_construction.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_container_args.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_enum_payloads.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_generic_owners.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/reflection_type_id_duplicate_named_owners.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/secret_compare_boundary.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/set_empty_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/set_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/simple.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_branch_narrowing.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_else_if_remaining_narrowing.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_else_narrowing.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_guarded_state_parameter.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_local_state_type.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_negative_narrowing.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/state_machine_runtime_fields.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/stdlib_loading.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/string_chars.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_extra.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_format.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_indic_grapheme.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_interpolation.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/string_iteration.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_layout_helpers.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_lines_mixed_endings.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_operations.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_search.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_split_grapheme_boundaries.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/string_split_max.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/structured_concurrency.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/time_and_os.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/time_values.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/trace_basic.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/type_construction_bitfield.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/type_construction_builder.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/type_construction_enum.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/type_construction_machine.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/type_info_reflection.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/type_reflection.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/uint64_checked_expression_runtime_main.jett", "obligations": ["lower", "object_emit", "main_execute"], "expected_outcome": "success"}, + {"path": "tests/run_pass/uint64_checked_expression_runtime_types.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/use_imports.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/run_pass/verify_test.jett", "obligations": ["lower", "object_emit"], "expected_outcome": "lower_only"}, + {"path": "tests/runtime_fail/clock_provider_failure.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure", "clock_test_samples": []}, + {"path": "tests/runtime_fail/int16_return_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/int32_assignment_underflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/int8_expression_underflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/list_sum_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_abs_int64_min.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_clamp_nan_bound.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure"}, + {"path": "tests/runtime_fail/math_clamp_nan_upper_bound.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure"}, + {"path": "tests/runtime_fail/math_clamp_reversed_float_bounds.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure"}, + {"path": "tests/runtime_fail/math_factorial_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_gcd_int64_min.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_lcm_int64_min.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_lcm_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_mod_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/math_sum_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/random_invalid_test_sample.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure", "random_test_samples": [{"bounded":"0"}]}, + {"path": "tests/runtime_fail/random_provider_exhausted.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure", "random_test_samples": []}, + {"path": "tests/runtime_fail/range_capacity_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure"}, + {"path": "tests/runtime_fail/secret_uint8_alias_expression_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/secret_uint8_expression_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/string_repeat_capacity_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "expected_failure"}, + {"path": "tests/runtime_fail/uint16_parameter_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/uint32_multiplication_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/uint32_nested_expression_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"}, + {"path": "tests/runtime_fail/uint8_expression_overflow.jett", "obligations": ["runtime_contract"], "expected_outcome": "wrapping_success"} + ] +} diff --git a/tests/run_pass/bitfield_roundtrip.jett b/tests/run_pass/bitfield_roundtrip.jett index b6ca78ae..5e96ab28 100644 --- a/tests/run_pass/bitfield_roundtrip.jett +++ b/tests/run_pass/bitfield_roundtrip.jett @@ -2,15 +2,21 @@ enum IpProtocol: icmp = 1 tcp = 6 udp = 17 - bitfield network Header: version: 4 bits header_length: 4 bits protocol: 8 bits as IpProtocol - +function checked_version(value: int64) returns string: + Header header = Header(version: value, header_length: 5, protocol: IpProtocol.tcp) handle error: + return error + return "{header.version}" function main() returns nothing: Header header = Header(version: 4, header_length: 5, protocol: IpProtocol.tcp) bytes raw = Header.to_bytes(header) Header decoded = Header.from_bytes(raw) handle error: return nothing return nothing +verify direct_bitfield_widths: + assert checked_version(4) == "4" + assert checked_version(16) == "bitfield 'Header' field 'version' is 4 bit(s) wide and cannot hold '16'" + assert checked_version(-1) == "bitfield 'Header' field 'version' is 4 bit(s) wide and cannot hold '-1'" diff --git a/tests/run_pass/bytes_operations.jett b/tests/run_pass/bytes_operations.jett index 973a4aa2..57577f1d 100644 --- a/tests/run_pass/bytes_operations.jett +++ b/tests/run_pass/bytes_operations.jett @@ -85,9 +85,9 @@ verify slice_clamp_test: function roundtrip_test() returns string: string original = "Jett language" bytes b = bytes.from_string(original) - string result = bytes.to_string(b) handle error: + string decoded_text = bytes.to_string(b) handle error: default "failed" - return result + return decoded_text verify roundtrip_test: assert roundtrip_test() == "Jett language" # --- hex conversion --- @@ -126,19 +126,19 @@ verify from_hex_odd_message: function concat_empty_test() returns int64: bytes empty = bytes.new() bytes data = bytes.from_string("abc") - bytes result = bytes.concat(empty, data) - return bytes.length(result) + bytes combined = bytes.concat(empty, data) + return bytes.length(combined) verify concat_empty_test: assert concat_empty_test() == 3 # --- ownership contract --- function observer_views_preserve_owner() returns string: bytes original = bytes.from_string("Hi") - int64 length = bytes.length(original) - int64 first = bytes.get(original, 0) handle: + int64 length = bytes.length(view original) + int64 first = bytes.get(view original, 0) handle: default -1 - bytes piece = bytes.slice(original, 0, 1) - string hex = bytes.to_hex(original) - string text = bytes.to_string(original) handle error: + bytes piece = bytes.slice(view original, 0, 1) + string hex = bytes.to_hex(view original) + string text = bytes.to_string(view original) handle error: default error return "{length}:{first}:{bytes.length(piece)}:{hex}:{text}:{bytes.length(original)}" verify observer_views_preserve_owner: diff --git a/tests/run_pass/closures_advanced.jett b/tests/run_pass/closures_advanced.jett index 25bac65e..f7ca33f8 100644 --- a/tests/run_pass/closures_advanced.jett +++ b/tests/run_pass/closures_advanced.jett @@ -54,7 +54,7 @@ verify test_conditional_closures: function sum_with_closure() returns int64: function(int64, int64) returns int64 add = function(a: int64, b: int64) returns int64: return a + b - int64 total = 0 + mutable int64 total = 0 for i in range(1, 6): total = add(total, i) return total @@ -65,7 +65,7 @@ verify test_closure_in_loop: # --- Closure that captures a loop variable --- function accumulate_squares() returns int64: - int64 sum = 0 + mutable int64 sum = 0 for i in range(1, 5): function() returns int64 sq = function() returns int64: return i * i sum = sum + sq() diff --git a/tests/run_pass/comptime_type_bind.jett b/tests/run_pass/comptime_type_bind.jett index d13dde44..5a942da8 100644 --- a/tests/run_pass/comptime_type_bind.jett +++ b/tests/run_pass/comptime_type_bind.jett @@ -14,13 +14,13 @@ type NameList = list[string] type NonEmpty = string where string.char_count(value) > 0 function bound_type_name() returns string: - string found = "missing" + mutable string found = "missing" comptime type Bound = type.info[int64](): found = type.name[Bound]() return found function bound_generic_field_kind() returns string: - string found = "missing" + mutable string found = "missing" comptime type Bound = type.info[Box[string]](): list[TypeField] fields = type.fields[Bound]() for field in fields: @@ -29,7 +29,7 @@ function bound_generic_field_kind() returns string: return found function bound_generic_field_type_name() returns string: - string found = "missing" + mutable string found = "missing" comptime type Bound = type.info[Box[string]](): list[TypeField] fields = type.fields[Bound]() for field in fields: @@ -52,7 +52,7 @@ function bound_reflected_field_types() returns string: function bound_reflected_generic_field_type() returns string: Box[list[int64]] box = Box[list[int64]](value: list(1, 2, 3)) - string found = "missing" + mutable string found = "missing" for field in type.fields[Box[list[int64]]](): comptime type Field = field.type_info: Field field_value = type.field_value[Box[list[int64]], Field](view box, view field) @@ -60,7 +60,7 @@ function bound_reflected_generic_field_type() returns string: return found function bound_direct_arg_type() returns string: - string found = "missing" + mutable string found = "missing" for arg in type.info[list[string]]().args: comptime type Arg = arg: found = type.name[Arg]() @@ -71,25 +71,25 @@ function reflected_arg_type_info_name() returns string: return info.type_name function bound_direct_type_arg() returns string: - string found = "missing" + mutable string found = "missing" comptime type Value = type.arg[map[string, list[int64]]](1): found = type.name[Value]() return found function bound_alias_base_arg() returns string: - string found = "missing" + mutable string found = "missing" comptime type Base = type.arg[NameList](0): found = type.name[Base]() return found function bound_refinement_base_arg() returns string: - string found = "missing" + mutable string found = "missing" comptime type Base = type.arg[NonEmpty](0): found = type.name[Base]() return found function bound_nested_arg_type() returns string: - string found = "missing" + mutable string found = "missing" for outer in type.info[list[optional[string]]]().args: comptime type Outer = outer: for inner in outer.args: @@ -99,7 +99,7 @@ function bound_nested_arg_type() returns string: function bound_field_arg_type() returns string: ListBox box = ListBox(values: list(1, 2, 3)) - string found = "missing" + mutable string found = "missing" for field in type.fields[ListBox](): for arg in field.type_info.args: comptime type Arg = arg: diff --git a/tests/run_pass/csv_escaping_helpers.jett b/tests/run_pass/csv_escaping_helpers.jett index c89d313e..df0753e8 100644 --- a/tests/run_pass/csv_escaping_helpers.jett +++ b/tests/run_pass/csv_escaping_helpers.jett @@ -13,7 +13,7 @@ verify csv_escaped_quote_test: function csv_trailing_crlf_test() returns string: list[list[string]] rows = csv.parse("a,b\r\n1,2\r\n") handle error: return error - list[string] second_row = list.get[list[string]](rows, 1) handle: + list[string] second_row = list.get[list[string]](view rows, 1) handle: default list() return "{list.length(rows)}:{list.length[string](second_row)}" verify csv_trailing_crlf_test: diff --git a/tests/run_pass/csv_operations.jett b/tests/run_pass/csv_operations.jett index a36caa34..795acc4e 100644 --- a/tests/run_pass/csv_operations.jett +++ b/tests/run_pass/csv_operations.jett @@ -56,9 +56,9 @@ verify csv_parse_empty_test: function csv_parse_single_blank_line_test() returns string: list[list[string]] rows = csv.parse("\n") handle error: return error - list[string] blank = list.get[list[string]](rows, 0) handle: + list[string] blank = list.get[list[string]](view rows, 0) handle: return "missing blank record" - string value = list.get[string](blank, 0) handle: + string value = list.get[string](view blank, 0) handle: return "missing empty field" return "{list.length[list[string]](rows)}:{list.length[string](blank)}:{value}" verify csv_parse_single_blank_line_test: @@ -66,9 +66,9 @@ verify csv_parse_single_blank_line_test: function csv_parse_blank_record_test() returns string: list[list[string]] rows = csv.parse("first\n\nthird") handle error: return error - list[string] blank = list.get[list[string]](rows, 1) handle: + list[string] blank = list.get[list[string]](view rows, 1) handle: return "missing blank record" - string value = list.get[string](blank, 0) handle: + string value = list.get[string](view blank, 0) handle: return "missing empty field" return "{list.length[list[string]](rows)}:{list.length[string](blank)}:{value}" verify csv_parse_blank_record_test: @@ -76,9 +76,9 @@ verify csv_parse_blank_record_test: function csv_parse_ragged_rows_test() returns string: list[list[string]] rows = csv.parse("a,b\n1\n2,3,4") handle error: return error - list[string] short = list.get[list[string]](rows, 1) handle: + list[string] short = list.get[list[string]](view rows, 1) handle: return "missing short row" - list[string] long = list.get[list[string]](rows, 2) handle: + list[string] long = list.get[list[string]](view rows, 2) handle: return "missing long row" return "{list.length[list[string]](rows)}:{list.length[string](short)}:{list.length[string](long)}" verify csv_parse_ragged_rows_test: diff --git a/tests/run_pass/json_enum_bitfield_exact_edges.jett b/tests/run_pass/json_enum_bitfield_exact_edges.jett index 9b48a902..adb1155c 100644 --- a/tests/run_pass/json_enum_bitfield_exact_edges.jett +++ b/tests/run_pass/json_enum_bitfield_exact_edges.jett @@ -5,6 +5,9 @@ enum Direction: struct Profile: display_name: string serialize "displayName" note: string +implement Equatable for Profile: + function equals(view self: Profile, view other: Profile) returns bool: + return self.display_name == other.display_name && self.note == other.note enum Shape: circle(radius: float64) profile(profile: Profile) diff --git a/tests/run_pass/json_parse_collection_edges.jett b/tests/run_pass/json_parse_collection_edges.jett index 2585dc7f..6d917657 100644 --- a/tests/run_pass/json_parse_collection_edges.jett +++ b/tests/run_pass/json_parse_collection_edges.jett @@ -13,6 +13,10 @@ function parse_top_level_set_dedupes() returns string: return error bool has_two = set.contains[int64](values_for_two, 2) return "{length}:{has_one}:{has_two}" +function parse_top_level_uint64_set() returns int64: + set[uint64] values = json.parse[set[uint64]]("[0,42,42]") handle error: + return -1 + return set.length[uint64](values) function parse_top_level_map_optional_bytes() returns string: map[string, optional[bytes]] entries = json.parse[map[string, optional[bytes]]]("{{\"blob\":\"0x4869\",\"missing\":null}}") handle error: return error @@ -56,6 +60,7 @@ function parse_result_optional_json_value_null() returns string: return json.kind(view value) verify json_parse_collection_edges: assert parse_top_level_set_dedupes() == "2:true:true" + assert parse_top_level_uint64_set() == 2 assert parse_top_level_map_optional_bytes() == "2:true:0" assert parse_top_level_result_fail_payload() == "Alice:bad" assert parse_result_optional_json_value_object() == "Ada" diff --git a/tests/run_pass/json_parse_exact.jett b/tests/run_pass/json_parse_exact.jett index 85412b8a..88b73f01 100644 --- a/tests/run_pass/json_parse_exact.jett +++ b/tests/run_pass/json_parse_exact.jett @@ -161,7 +161,7 @@ function parse_exact_accepts_legacy_json_value_payload() returns string: string raw = "{{\"answer\":43,\"extra\":{{\"nested\":true}}}}" json.JsonTree tree = json.parse_exact[json.JsonTree](raw) handle error: return error - json.JsonTree answer = json.field(tree, "answer") handle: + json.JsonTree answer = json.field(view tree, "answer") handle: return "missing" int64 value = json.as_int64(answer) handle error: return error @@ -217,7 +217,7 @@ function parse_exact_accepts_json_value_map_payload_extras() returns string: string raw = "{{\"primary\":{{\"extra\":\"kept\"}}}}" map[string, json.JsonTree] entries = json.parse_exact[map[string, json.JsonTree]](raw) handle error: return error - json.JsonTree payload = map.get[string, json.JsonTree](view entries, "primary") handle: + json.JsonTree payload = map.get[string, json.JsonTree](clone entries, "primary") handle: return "missing" json.JsonTree extra = json.require_field(view payload, "extra") handle error: return error diff --git a/tests/run_pass/json_public_secret_policy.jett b/tests/run_pass/json_public_secret_policy.jett index a4a247e5..10068afe 100644 --- a/tests/run_pass/json_public_secret_policy.jett +++ b/tests/run_pass/json_public_secret_policy.jett @@ -54,7 +54,7 @@ function public_projection_optional_record_summary() returns string: function public_projection_result_record_summary() returns string: Credential credential = json.parse[Credential]("{{\"name\":\"svc\",\"token\":\"secret-token\"}}") handle error: return error - result[Credential, Credential] success = ok(credential) + result[Credential, Credential] success = ok(clone credential) result[Credential, Credential] failure = fail(credential) string ok_raw = json.serialize_public[result[Credential, Credential]](view success) string fail_raw = json.serialize_public[result[Credential, Credential]](view failure) diff --git a/tests/run_pass/json_raw_facade_tree_surface.jett b/tests/run_pass/json_raw_facade_tree_surface.jett index 22805133..f812c273 100644 --- a/tests/run_pass/json_raw_facade_tree_surface.jett +++ b/tests/run_pass/json_raw_facade_tree_surface.jett @@ -4,39 +4,39 @@ function raw_tree_facade_root() returns json.JsonTree: return json.parse_raw("{{\"name\":\"Ada\",\"scores\":[3,5],\"active\":true,\"ratio\":2.5,\"missing\":null}}") handle error: default json.JsonTree.string_value(error) function raw_tree_facade_name(view root: json.JsonTree) returns string: - json.JsonTree name = json.field(root, "name") handle: + json.JsonTree name = json.field(view root, "name") handle: return "missing name" return json.as_string(name) handle error: default error function raw_tree_facade_scores(view root: json.JsonTree) returns string: - json.JsonTree scores = json.field(root, "scores") handle: + json.JsonTree scores = json.field(view root, "scores") handle: return "missing scores" - int64 score_count = json.array_length(scores) handle error: + int64 score_count = json.array_length(view scores) handle error: return error - json.JsonTree second = json.index(scores, 1) handle: + json.JsonTree second = json.index(view scores, 1) handle: return "missing score" - int64 second_score = json.as_int64(second) handle error: + int64 second_score = json.as_int64(view second) handle error: return error return "{json.is_array(scores)}:{score_count}:{second_score}:{json.is_number(second)}" function raw_tree_facade_scalars(view root: json.JsonTree) returns string: - json.JsonTree active = json.field(root, "active") handle: + json.JsonTree active = json.field(view root, "active") handle: return "missing active" - bool active_value = json.as_bool(active) handle error: + bool active_value = json.as_bool(view active) handle error: return error - json.JsonTree ratio = json.field(root, "ratio") handle: + json.JsonTree ratio = json.field(view root, "ratio") handle: return "missing ratio" float64 ratio_value = json.as_float64(ratio) handle error: return error - json.JsonTree missing = json.field(root, "missing") handle: + json.JsonTree missing = json.field(view root, "missing") handle: return "missing null" return "{active_value}:{json.is_bool(active)}:{ratio_value}:{json.is_null(missing)}" function raw_tree_facade_root_summary(view root: json.JsonTree) returns string: - string root_kind = json.kind(root) - bool root_is_object = json.is_object(root) - list[string] keys = json.object_keys(root) handle error: + string root_kind = json.kind(view root) + bool root_is_object = json.is_object(view root) + list[string] keys = json.object_keys(view root) handle error: return error - string raw = json.serialize_raw(root) - string root_kind_after_reads = json.kind(root) + string raw = json.serialize_raw(view root) + string root_kind_after_reads = json.kind(view root) bool raw_mentions_name = string.contains(raw, "Ada") return "{root_kind}:{root_is_object}:{list.length[string](view keys)}:{raw_mentions_name}:{root_kind_after_reads}" function raw_tree_facade_summary() returns string: diff --git a/tests/run_pass/json_raw_strict_accessors.jett b/tests/run_pass/json_raw_strict_accessors.jett index 87805c07..db8e51be 100644 --- a/tests/run_pass/json_raw_strict_accessors.jett +++ b/tests/run_pass/json_raw_strict_accessors.jett @@ -149,15 +149,15 @@ function strict_optional_accessors_accept_namespaced_value() returns string: string item_text = json.as_string(view item) handle error: return error return "{field_text}:{item_text}" -function strict_accessors_accept_implicit_view_calls() returns string: +function strict_accessors_retain_with_explicit_view() returns string: json.JsonTree root = strict_root() - json.JsonTree name = json.require_field(root, "name") handle error: + json.JsonTree name = json.require_field(view root, "name") handle error: return error optional[json.JsonTree] maybe_scores = json.object_field(root, "scores") handle error: return error json.JsonTree scores = maybe_scores handle: return "missing scores" - optional[json.JsonTree] maybe_second = json.array_index(scores, 1) handle error: + optional[json.JsonTree] maybe_second = json.array_index(view scores, 1) handle error: return error json.JsonTree second = maybe_second handle: return "missing second" @@ -189,4 +189,4 @@ verify json_raw_strict_accessors: assert strict_accessors_accept_legacy_value() == "Grace" assert strict_accessors_accept_namespaced_value() == "Lin:5" assert strict_optional_accessors_accept_namespaced_value() == "field absent:index absent" - assert strict_accessors_accept_implicit_view_calls() == "Ada:5:5" + assert strict_accessors_retain_with_explicit_view() == "Ada:5:5" diff --git a/tests/run_pass/json_raw_tree_parity.jett b/tests/run_pass/json_raw_tree_parity.jett index f5d00dd0..a09a9dc8 100644 --- a/tests/run_pass/json_raw_tree_parity.jett +++ b/tests/run_pass/json_raw_tree_parity.jett @@ -12,7 +12,7 @@ function raw_tree_object_kind_summary() returns string: return "raw parse: {error}" json.JsonTree tree = json.json_tree_parse(body) handle error: return "tree parse: {error}" - return "{json.kind(raw)}:{json.is_object(raw)}|{json.json_tree_kind(view tree)}:{json.json_tree_is_object(view tree)}" + return "{json.kind(view raw)}:{json.is_object(raw)}|{json.json_tree_kind(view tree)}:{json.json_tree_is_object(view tree)}" function raw_tree_string_field_summary() returns string: string body = "{{\"name\":\"Ada\"}}" json.JsonTree raw = json.parse_raw(body) handle error: @@ -34,7 +34,7 @@ function raw_array_field_summary() returns string: return "raw parse: {error}" json.JsonTree raw_scores = json.field(raw, "scores") handle: return "raw missing scores" - int64 raw_score_count = json.array_length(raw_scores) handle error: + int64 raw_score_count = json.array_length(view raw_scores) handle error: return "raw score length: {error}" json.JsonTree raw_second = json.index(raw_scores, 1) handle: return "raw missing second" @@ -58,9 +58,9 @@ function raw_scalar_field_summary() returns string: string body = "{{\"active\":true,\"missing\":null,\"ratio\":2.5}}" json.JsonTree raw = json.parse_raw(body) handle error: return "raw parse: {error}" - json.JsonTree raw_active = json.field(raw, "active") handle: + json.JsonTree raw_active = json.field(view raw, "active") handle: return "raw missing active" - json.JsonTree raw_missing = json.field(raw, "missing") handle: + json.JsonTree raw_missing = json.field(view raw, "missing") handle: return "raw missing null" json.JsonTree raw_ratio = json.field(raw, "ratio") handle: return "raw missing ratio" diff --git a/tests/run_pass/json_reflection_flat_decoder.jett b/tests/run_pass/json_reflection_flat_decoder.jett index a94ab810..e6a960b1 100644 --- a/tests/run_pass/json_reflection_flat_decoder.jett +++ b/tests/run_pass/json_reflection_flat_decoder.jett @@ -4,20 +4,23 @@ struct FlatUser: age: int64 active: bool function decode_flat_value[T](view raw: json.JsonTree) returns result[T, string]: - string type_name = type.name[T]() - if type_name == "string": - string item = json.as_string(view raw) handle error: - return fail(error) - return ok(item) - if type_name == "int64": - int64 item = json.as_int64(view raw) handle error: - return fail(error) - return ok(item) - if type_name == "bool": - bool item = json.as_bool(view raw) handle error: - return fail(error) - return ok(item) - return fail("unsupported reflected JSON type: {type_name}") + TypePrimitive primitive = type.primitive_tag[T]() handle: + default TypePrimitive.unknown_type + match primitive: + string_type: + string item = json.as_string(view raw) handle error: + return fail(error) + return ok(item) + int64_type: + int64 item = json.as_int64(view raw) handle error: + return fail(error) + return ok(item) + bool_type: + bool item = json.as_bool(view raw) handle error: + return fail(error) + return ok(item) + other: + return fail("unsupported reflected JSON primitive") function decode_flat_struct[T](view raw: json.JsonTree) returns result[T, string]: mutable TypeConstruction builder = type.construct_start[T]() for field in type.fields[T](): diff --git a/tests/run_pass/json_reflection_nested_decoder.jett b/tests/run_pass/json_reflection_nested_decoder.jett index 1cfb3978..765c8f12 100644 --- a/tests/run_pass/json_reflection_nested_decoder.jett +++ b/tests/run_pass/json_reflection_nested_decoder.jett @@ -53,7 +53,7 @@ function decoded_nested_summary() returns string: return error bool south = user.direction == Direction.south bool tcp = user.header.protocol == Protocol.tcp - int64 shape_total = 0 + mutable int64 shape_total = 0 match user.shape: empty: shape_total = -1 diff --git a/tests/run_pass/json_serialize_machine_envelope.jett b/tests/run_pass/json_serialize_machine_envelope.jett index c7cfc24a..984bef25 100644 --- a/tests/run_pass/json_serialize_machine_envelope.jett +++ b/tests/run_pass/json_serialize_machine_envelope.jett @@ -25,7 +25,7 @@ function guest_session_json() returns string: return json.serialize[Session](view session) function nested_session_json() returns string: Session at logged_in active = Session.transition(Session(guest), logged_in, "user-3", "Bea") - Session logged = active + Session logged = clone active SessionBox box = SessionBox(current: active, history: list(Session(guest), logged)) return json.serialize[SessionBox](view box) function public_token_json() returns string: diff --git a/tests/run_pass/json_shape_matrix.jett b/tests/run_pass/json_shape_matrix.jett index eb4ea920..5af9bb5d 100644 --- a/tests/run_pass/json_shape_matrix.jett +++ b/tests/run_pass/json_shape_matrix.jett @@ -22,7 +22,7 @@ function parse_matrix_summary() returns string: Matrix matrix = json.parse[Matrix](raw) handle error: return error float64 ratio = matrix.ratio - int64 percent = 0 + mutable int64 percent = 0 match matrix.state: idle: percent = -1 diff --git a/tests/run_pass/json_tree_reflection_parse_wrapper.jett b/tests/run_pass/json_tree_reflection_parse_wrapper.jett index d42ffc53..4519c5e3 100644 --- a/tests/run_pass/json_tree_reflection_parse_wrapper.jett +++ b/tests/run_pass/json_tree_reflection_parse_wrapper.jett @@ -79,7 +79,7 @@ function tree_reflected_full_summary() returns string: TreeFull item = json.parse[TreeFull]("{{\"header\":{{\"version\":4,\"header_length\":5,\"protocol\":\"tcp\"}},\"shape\":{{\"point\":[3,4]}},\"lucky\":[11,13],\"display\":\"ready\",\"outcome\":{{\"ok\":7}},\"failed\":{{\"fail\":\"bad\"}},\"credential\":{{\"name\":\"svc\",\"token\":\"secret-token\"}},\"data\":\"0x4869\"}}") handle error: return error bool tcp = item.header.protocol == TreeProtocol.tcp - int64 shape_total = 0 + mutable int64 shape_total = 0 match item.shape: empty: shape_total = -1 diff --git a/tests/run_pass/json_tree_reflection_variant_metadata.jett b/tests/run_pass/json_tree_reflection_variant_metadata.jett index 229bb18c..68995204 100644 --- a/tests/run_pass/json_tree_reflection_variant_metadata.jett +++ b/tests/run_pass/json_tree_reflection_variant_metadata.jett @@ -2,7 +2,7 @@ namespace test function json_tree_variant_count() returns int64: return list.length[TypeVariant](type.variants[json.JsonTree]()) function json_tree_variant_names() returns string: - string names = "" + mutable string names = "" for variant in type.variants[json.JsonTree](): names = "{names}{variant.name}|" return names @@ -32,7 +32,7 @@ function json_tree_variant_field_arg_shape(variant_name: string, field_name: str for field in variant.fields: if field.name != field_name: continue - int64 index = 0 + mutable int64 index = 0 for arg in field.type_info.args: if index == target: return "{arg.kind}:{arg.type_name}" diff --git a/tests/run_pass/json_tree_value.jett b/tests/run_pass/json_tree_value.jett index f2fc2ef5..9fb90233 100644 --- a/tests/run_pass/json_tree_value.jett +++ b/tests/run_pass/json_tree_value.jett @@ -23,7 +23,7 @@ function json_tree_roundtrip_through_raw() returns bool: return decoded == "Ada" function json_tree_serialize_does_not_consume_tree() returns string: json.JsonTree tree = sample_tree() - string raw = json.json_tree_serialize(tree) + string raw = json.json_tree_serialize(view tree) string kind = json.json_tree_kind(tree) bool has_name = string.contains(raw, "\"Ada\"") return "{kind}:{has_name}" diff --git a/tests/run_pass/list_access_source.jett b/tests/run_pass/list_access_source.jett index 6c745bb7..94a431ac 100644 --- a/tests/run_pass/list_access_source.jett +++ b/tests/run_pass/list_access_source.jett @@ -1,14 +1,14 @@ namespace test function inferred_first_reuses_list() returns int64: list[int64] items = list(10, 20, 30) - optional[int64] first = list.first(items) + optional[int64] first = list.first(view items) int64 value = first handle: default 0 return value + list.length(items) verify inferred_first_reuses_list: assert inferred_first_reuses_list() == 13 function inferred_first_from_view(view items: list[int64]) returns int64: - optional[int64] first = list.first(items) + optional[int64] first = list.first(view items) return first handle: default 0 function inferred_first_view_parameter() returns int64: @@ -20,7 +20,7 @@ verify inferred_first_view_parameter: function inferred_pipeline_first_reuses_list() returns int64: list[int64] items = list(60, 70) int64 first = items - into list.first() handle: + into view list.first() handle: default 0 return first + list.length(items) verify inferred_pipeline_first_reuses_list: @@ -35,8 +35,8 @@ verify explicit_last_reuses_list: assert explicit_last_reuses_list() == 33 function empty_first_and_last() returns bool: list[int64] items = list() - optional[int64] first = list.first(items) - optional[int64] last = list.last[int64](items) + optional[int64] first = list.first(view items) + optional[int64] last = list.last[int64](view items) int64 first_value = first handle: default 7 int64 last_value = last handle: @@ -47,8 +47,8 @@ verify empty_first_and_last: function is_empty_outcomes_reuse_lists() returns bool: list[int64] empty = list() list[int64] filled = list(1) - bool empty_result = list.is_empty[int64](empty) - bool filled_result = list.is_empty(filled) + bool empty_result = list.is_empty[int64](view empty) + bool filled_result = list.is_empty(view filled) return empty_result and not filled_result and list.length(empty) == 0 and list.length(filled) == 1 verify is_empty_outcomes_reuse_lists: assert is_empty_outcomes_reuse_lists() == true diff --git a/tests/run_pass/loops.jett b/tests/run_pass/loops.jett index b988ebdc..250a119f 100644 --- a/tests/run_pass/loops.jett +++ b/tests/run_pass/loops.jett @@ -3,7 +3,7 @@ namespace app # --- range(end) --- function sum_to(n: int64) returns int64: - int64 total = 0 + mutable int64 total = 0 for i in range(n): total = total + i return total @@ -16,7 +16,7 @@ verify sum_to: # --- range(start, end) --- function sum_range(a: int64, b: int64) returns int64: - int64 total = 0 + mutable int64 total = 0 for i in range(a, b): total = total + i return total @@ -29,7 +29,7 @@ verify sum_range: # --- range(start, end, step) --- function sum_step() returns int64: - int64 total = 0 + mutable int64 total = 0 for i in range(0, 10, 2): total = total + i return total @@ -40,7 +40,7 @@ verify sum_step: # --- for loop over string --- function count_chars(s: string) returns int64: - int64 count = 0 + mutable int64 count = 0 for ch in s: count = count + 1 return count @@ -52,12 +52,12 @@ verify count_chars: # --- while loop --- function factorial(n: int64) returns int64: - int64 result = 1 - int64 i = 2 + mutable int64 product = 1 + mutable int64 i = 2 while i <= n: - result = result * i + product = product * i i = i + 1 - return result + return product verify factorial: assert factorial(5) == 120 @@ -67,7 +67,7 @@ verify factorial: # --- break in for loop --- function first_above(items: list[int64], threshold: int64) returns int64: - int64 found = -1 + mutable int64 found = -1 for item in items: if item > threshold: found = item @@ -81,7 +81,7 @@ verify first_above: # --- for loop over map (key, value) --- function sum_map_values(m: map[string, int64]) returns int64: - int64 total = 0 + mutable int64 total = 0 for key, value in m: total = total + value return total diff --git a/tests/run_pass/namespace_comptime_reflection_aliases.jett b/tests/run_pass/namespace_comptime_reflection_aliases.jett index 8af487f7..49bacec9 100644 --- a/tests/run_pass/namespace_comptime_reflection_aliases.jett +++ b/tests/run_pass/namespace_comptime_reflection_aliases.jett @@ -12,20 +12,20 @@ export machine Session: namespace app function bound_namespaced_alias_base() returns string: use data as d - string found = "missing" + mutable string found = "missing" comptime type Base = type.arg[d.NameList](0): found = type.name[Base]() return found function bound_namespaced_refinement_base() returns string: use data as d - string found = "missing" + mutable string found = "missing" comptime type Base = type.arg[d.NonEmpty](0): found = type.name[Base]() return found function bound_namespaced_generic_field() returns string: use data as d d.Box[list[int64]] box = d.Box[list[int64]](value: list(1, 2, 3)) - string found = "missing" + mutable string found = "missing" for field in type.fields[d.Box[list[int64]]](): comptime type Field = field.type_info: Field value = type.field_value[d.Box[list[int64]], Field](view box, view field) @@ -34,13 +34,13 @@ function bound_namespaced_generic_field() returns string: function bound_namespaced_machine_layout() returns string: use data as d TypeMachine layout = type.machine_layout[d.Session at logged_in]() - string field_type = "missing" + mutable string field_type = "missing" for state in type.machine_states[d.Session at logged_in](): if state.name == "logged_in": for field in state.fields: if field.name == "user_id": field_type = field.type_info.type_name - string edge = "missing" + mutable string edge = "missing" for item in type.machine_transitions[d.Session](): if item.source == "guest": edge = "{item.source}->{item.target}" diff --git a/tests/run_pass/native_scalar_entry.jett b/tests/run_pass/native_scalar_entry.jett new file mode 100644 index 00000000..c0a0fd00 --- /dev/null +++ b/tests/run_pass/native_scalar_entry.jett @@ -0,0 +1,19 @@ +namespace app + +function triangular(limit: int64) returns int64: + mutable int64 index = 0 + mutable int64 total = 0 + while index < limit: + total = total + index + index = index + 1 + return total + +verify triangular: + assert triangular(0) == 0 + assert triangular(10) == 45 + +function main() returns nothing: + mutable int64 computed = triangular(10) + while computed != 45: + computed = computed + return nothing diff --git a/tests/run_pass/range_step_boundaries.jett b/tests/run_pass/range_step_boundaries.jett index 698c140c..8459074b 100644 --- a/tests/run_pass/range_step_boundaries.jett +++ b/tests/run_pass/range_step_boundaries.jett @@ -1,11 +1,11 @@ namespace app function positive_step_boundary() returns int64: - int64 value = 0 + mutable int64 value = 0 for item in range(9223372036854775806, 9223372036854775807, 2): value = item return value function negative_step_boundary() returns int64: - int64 value = 0 + mutable int64 value = 0 for item in range(-9223372036854775807, -9223372036854775808, -2): value = item return value diff --git a/tests/run_pass/recursive_owned_values.jett b/tests/run_pass/recursive_owned_values.jett index 8992ef5f..d9d69c68 100644 --- a/tests/run_pass/recursive_owned_values.jett +++ b/tests/run_pass/recursive_owned_values.jett @@ -29,8 +29,8 @@ function recursive_enum_has_a_finite_base() returns bool: IntList values = IntList.item(1, IntList.item(2, IntList.empty)) return list_total(values) == 3 function recursive_struct_reflection_is_finite() returns bool: - int64 count = 0 - bool found_next = false + mutable int64 count = 0 + mutable bool found_next = false for field in type.fields[Node](): count = count + 1 if field.name == "next": diff --git a/tests/run_pass/reflection_type_id_duplicate_named_owners.jett b/tests/run_pass/reflection_type_id_duplicate_named_owners.jett index ebc66827..2affa398 100644 --- a/tests/run_pass/reflection_type_id_duplicate_named_owners.jett +++ b/tests/run_pass/reflection_type_id_duplicate_named_owners.jett @@ -27,7 +27,7 @@ function accounts_named_owner_summary() returns string: TypeBitfieldField status_field = list.get[TypeBitfieldField](type.bitfield_fields[a.Header](), 1) handle: return "missing account status field" a.Header header = a.Header(version: 4, status: a.Status.active) - bool reflected_active = false + mutable bool reflected_active = false for field in type.fields[a.Header](): if field.name == "status": a.Status reflected = type.field_value[a.Header, a.Status](view header, view field) @@ -48,7 +48,7 @@ function audit_named_owner_summary() returns string: TypeBitfieldField status_field = list.get[TypeBitfieldField](type.bitfield_fields[a.Header](), 1) handle: return "missing audit status field" a.Header header = a.Header(level: 3, status: a.Status.active) - bool reflected_active = false + mutable bool reflected_active = false for field in type.fields[a.Header](): if field.name == "status": a.Status reflected = type.field_value[a.Header, a.Status](view header, view field) diff --git a/tests/run_pass/set_operations.jett b/tests/run_pass/set_operations.jett index 3eff1e65..7ddc08fc 100644 --- a/tests/run_pass/set_operations.jett +++ b/tests/run_pass/set_operations.jett @@ -56,7 +56,7 @@ function test_difference() returns int64: verify test_difference: assert test_difference() == 1 function sum_set(s: set[int64]) returns int64: - int64 total = 0 + mutable int64 total = 0 for item in s: total = total + item return total diff --git a/tests/run_pass/string_iteration.jett b/tests/run_pass/string_iteration.jett index 68e6bcbd..c0bbdaf8 100644 --- a/tests/run_pass/string_iteration.jett +++ b/tests/run_pass/string_iteration.jett @@ -16,7 +16,7 @@ verify chars_first_test: assert chars_first_test() == "h" function chars_for_loop_count() returns int64: - int64 count = 0 + mutable int64 count = 0 for c in string.chars("hi!"): count = count + 1 return count diff --git a/tests/run_pass/type_info_reflection.jett b/tests/run_pass/type_info_reflection.jett index 8b3067fe..60a800e9 100644 --- a/tests/run_pass/type_info_reflection.jett +++ b/tests/run_pass/type_info_reflection.jett @@ -68,8 +68,8 @@ function type_info_has_no_primitive_tag[T]() returns bool: return primitive == TypePrimitive.unknown_type function type_info_arg_primitive_tag_is[T](target: int64, expected: TypePrimitive) returns bool: TypeInfo info = type.info[T]() - bool found = false - int64 index = 0 + mutable bool found = false + mutable int64 index = 0 for arg in info.args: if index == target: TypePrimitive primitive = arg.primitive_tag handle: @@ -82,8 +82,8 @@ function type_info_arg_count[T]() returns int64: return list.length[TypeInfo](info.args) function type_info_arg_kind[T](target: int64) returns string: TypeInfo info = type.info[T]() - string found = "missing" - int64 index = 0 + mutable string found = "missing" + mutable int64 index = 0 for arg in info.args: if index == target: found = arg.kind @@ -91,8 +91,8 @@ function type_info_arg_kind[T](target: int64) returns string: return found function type_info_arg_type_name[T](target: int64) returns string: TypeInfo info = type.info[T]() - string found = "missing" - int64 index = 0 + mutable string found = "missing" + mutable int64 index = 0 for arg in info.args: if index == target: found = arg.type_name @@ -100,11 +100,11 @@ function type_info_arg_type_name[T](target: int64) returns string: return found function type_info_nested_arg_kind[T](outer_target: int64, inner_target: int64) returns string: TypeInfo info = type.info[T]() - string found = "missing" - int64 outer_index = 0 + mutable string found = "missing" + mutable int64 outer_index = 0 for outer in info.args: if outer_index == outer_target: - int64 inner_index = 0 + mutable int64 inner_index = 0 for inner in outer.args: if inner_index == inner_target: found = inner.kind @@ -113,11 +113,11 @@ function type_info_nested_arg_kind[T](outer_target: int64, inner_target: int64) return found function type_info_nested_arg_type_name[T](outer_target: int64, inner_target: int64) returns string: TypeInfo info = type.info[T]() - string found = "missing" - int64 outer_index = 0 + mutable string found = "missing" + mutable int64 outer_index = 0 for outer in info.args: if outer_index == outer_target: - int64 inner_index = 0 + mutable int64 inner_index = 0 for inner in outer.args: if inner_index == inner_target: found = inner.type_name @@ -126,7 +126,7 @@ function type_info_nested_arg_type_name[T](outer_target: int64, inner_target: in return found function complex_field_arg_count(field_name: string) returns int64: list[TypeField] fields = type.fields[Complex]() - int64 found = -1 + mutable int64 found = -1 for field in fields: if field.name == field_name: found = list.length[TypeInfo](field.type_info.args) @@ -136,35 +136,35 @@ function field_count[T]() returns int64: return list.length[TypeField](fields) function field_kind[T](field_name: string) returns string: list[TypeField] fields = type.fields[T]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.kind return found function field_kind_tag_is[T](field_name: string, expected: TypeKind) returns bool: list[TypeField] fields = type.fields[T]() - bool found = false + mutable bool found = false for field in fields: if field.name == field_name: found = field.kind_tag == expected return found function complex_field_arg_kind(field_name: string, target: int64) returns string: list[TypeField] fields = type.fields[Complex]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: - int64 index = 0 + mutable int64 index = 0 for arg in field.type_info.args: if index == target: found = arg.kind index = index + 1 return found function nested_arg_kind_from_info(info: TypeInfo, outer_target: int64, inner_target: int64) returns string: - string found = "missing" - int64 outer_index = 0 + mutable string found = "missing" + mutable int64 outer_index = 0 for outer in info.args: if outer_index == outer_target: - int64 inner_index = 0 + mutable int64 inner_index = 0 for inner in outer.args: if inner_index == inner_target: found = inner.kind @@ -173,31 +173,31 @@ function nested_arg_kind_from_info(info: TypeInfo, outer_target: int64, inner_ta return found function complex_field_nested_arg_kind(field_name: string, outer_target: int64, inner_target: int64) returns string: list[TypeField] fields = type.fields[Complex]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = nested_arg_kind_from_info(field.type_info, outer_target, inner_target) return found function complex_field_type_info_secret(field_name: string) returns bool: list[TypeField] fields = type.fields[Complex]() - bool found = false + mutable bool found = false for field in fields: if field.name == field_name: found = field.type_info.has_secret return found function complex_field_type_info_matches_legacy(field_name: string) returns bool: list[TypeField] fields = type.fields[Complex]() - bool found = false + mutable bool found = false for field in fields: if field.name == field_name: found = field.type_info.type_name == field.type_name and field.type_info.kind == field.kind and field.type_info.has_secret == field.has_secret return found function pair_field_arg_type_name(field_name: string, target: int64) returns string: list[TypeField] fields = type.fields[Pair[list[int64], optional[string]]]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: - int64 index = 0 + mutable int64 index = 0 for arg in field.type_info.args: if index == target: found = arg.type_name @@ -206,58 +206,58 @@ function pair_field_arg_type_name(field_name: string, target: int64) returns str function header_version_by_reflection() returns int64: Header header = Header(version: 4) list[TypeField] fields = type.fields[Header]() - int64 found = -1 + mutable int64 found = -1 for field in fields: if field.name == "version": found = type.field_value[Header, int64](view header, view field) return found function bitfield_field_count[T]() returns int64: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - return list.length[TypeField](fields) + return list.length[TypeBitfieldField](fields) function bitfield_metadata_field_count[T]() returns int64: TypeBitfield info = type.bitfield_layout[T]() - return list.length[TypeField](info.fields) + return list.length[TypeBitfieldField](info.fields) function bitfield_metadata_network_order[T]() returns bool: TypeBitfield info = type.bitfield_layout[T]() return info.network_order function bitfield_field_shape[T](field_name: string) returns string: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.shape return found function bitfield_field_shape_tag_is[T](field_name: string, expected: TypeBitfieldFieldShape) returns bool: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - bool found = false + mutable bool found = false for field in fields: if field.name == field_name: found = field.shape_tag == expected return found function bitfield_field_width[T](field_name: string) returns int64: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - int64 found = -1 + mutable int64 found = -1 for field in fields: if field.name == field_name: found = field.width return found function bitfield_field_type_name[T](field_name: string) returns string: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.type_info.type_name return found function bitfield_field_type_kind[T](field_name: string) returns string: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.type_info.kind return found function bitfield_field_enum_kind[T](field_name: string) returns string: list[TypeBitfieldField] fields = type.bitfield_fields[T]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: TypeInfo info = field.enum_type handle: @@ -272,17 +272,17 @@ function machine_state_count[T]() returns int64: return list.length[TypeMachineState](states) function machine_state_field_count[T](state_name: string) returns int64: list[TypeMachineState] states = type.machine_states[T]() - int64 found = -1 + mutable int64 found = -1 for state in states: if state.name == state_name: found = list.length[TypeField](state.fields) return found function machine_state_field_type_name[T](state_name: string, target: int64) returns string: list[TypeMachineState] states = type.machine_states[T]() - string found = "missing" + mutable string found = "missing" for state in states: if state.name == state_name: - int64 index = 0 + mutable int64 index = 0 for field in state.fields: if index == target: found = field.type_info.type_name @@ -290,7 +290,7 @@ function machine_state_field_type_name[T](state_name: string, target: int64) ret return found function machine_state_has_secret[T](state_name: string) returns bool: list[TypeMachineState] states = type.machine_states[T]() - bool found = false + mutable bool found = false for state in states: if state.name == state_name: found = state.has_secret @@ -300,14 +300,14 @@ function machine_transition_count[T]() returns int64: return list.length[TypeMachineTransition](edges) function machine_transition_target[T](source_name: string) returns string: list[TypeMachineTransition] edges = type.machine_transitions[T]() - string found = "missing" + mutable string found = "missing" for edge in edges: if edge.source == source_name: found = edge.target return found function machine_transition_source_index[T](source_name: string) returns int64: list[TypeMachineTransition] edges = type.machine_transitions[T]() - int64 found = -1 + mutable int64 found = -1 for edge in edges: if edge.source == source_name: found = edge.source_index @@ -320,13 +320,13 @@ function active_machine_state_summary_precise(view session: Session at logged_in return "{state.index}:{state.name}:{list.length[TypeField](state.fields)}:{state.has_secret}" function active_machine_logged_in_user_id(view session: Session at logged_in) returns string: TypeMachineState state = type.machine_state_value[Session at logged_in](view session) - string found = "missing" + mutable string found = "missing" for field in state.fields: if field.name == "user_id": found = type.machine_field_value[Session at logged_in, string](view session, view field) return found function active_machine_logged_in_user_id_direct_loop(view session: Session at logged_in) returns string: - string found = "missing" + mutable string found = "missing" for field in type.machine_state_value[Session at logged_in](view session).fields: comptime type Field = field.type_info: Field item = type.machine_field_value[Session at logged_in, Field](view session, view field) @@ -349,31 +349,31 @@ function variant_count[T]() returns int64: return list.length[TypeVariant](variants) function variant_field_count[T](variant_name: string) returns int64: list[TypeVariant] variants = type.variants[T]() - int64 found = -1 + mutable int64 found = -1 for variant in variants: if variant.name == variant_name: found = list.length[TypeField](variant.fields) return found function variant_discriminant[T](variant_name: string) returns int64: list[TypeVariant] variants = type.variants[T]() - int64 found = -1 + mutable int64 found = -1 for variant in variants: if variant.name == variant_name: found = variant.discriminant return found function variant_has_secret[T](variant_name: string) returns bool: list[TypeVariant] variants = type.variants[T]() - bool found = false + mutable bool found = false for variant in variants: if variant.name == variant_name: found = variant.has_secret return found function variant_field_kind[T](variant_name: string, target: int64) returns string: list[TypeVariant] variants = type.variants[T]() - string found = "missing" + mutable string found = "missing" for variant in variants: if variant.name == variant_name: - int64 index = 0 + mutable int64 index = 0 for field in variant.fields: if index == target: found = field.kind @@ -381,10 +381,10 @@ function variant_field_kind[T](variant_name: string, target: int64) returns stri return found function variant_field_type_name[T](variant_name: string, target: int64) returns string: list[TypeVariant] variants = type.variants[T]() - string found = "missing" + mutable string found = "missing" for variant in variants: if variant.name == variant_name: - int64 index = 0 + mutable int64 index = 0 for field in variant.fields: if index == target: found = field.type_name diff --git a/tests/run_pass/type_reflection.jett b/tests/run_pass/type_reflection.jett index 419541b7..15bcb6f9 100644 --- a/tests/run_pass/type_reflection.jett +++ b/tests/run_pass/type_reflection.jett @@ -22,7 +22,7 @@ function user_field_count() returns int64: function user_field_type(field_name: string) returns string: list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.type_name @@ -30,7 +30,7 @@ function user_field_type(field_name: string) returns string: function user_field_secret(field_name: string) returns bool: list[TypeField] fields = type.fields[User]() - bool found = false + mutable bool found = false for field in fields: if field.name == field_name: found = field.has_secret @@ -38,7 +38,7 @@ function user_field_secret(field_name: string) returns bool: function user_field_serialize_name(field_name: string) returns string: list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.serialize_name @@ -46,7 +46,7 @@ function user_field_serialize_name(field_name: string) returns string: function pair_field_type(field_name: string) returns string: list[TypeField] fields = type.fields[Pair[int64, string]]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.type_name @@ -54,7 +54,7 @@ function pair_field_type(field_name: string) returns string: function api_field_serialize_name(field_name: string) returns string: list[TypeField] fields = type.fields[ApiResponse]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = field.serialize_name @@ -63,7 +63,7 @@ function api_field_serialize_name(field_name: string) returns string: function user_string_field_value(field_name: string) returns string: User user = User(id: "u1", age: 42, api_key: "secret-token") list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == field_name: found = type.field_value[User, string](view user, view field) @@ -72,7 +72,7 @@ function user_string_field_value(field_name: string) returns string: function user_age_field_value() returns int64: User user = User(id: "u1", age: 42, api_key: "secret-token") list[TypeField] fields = type.fields[User]() - int64 found = 0 + mutable int64 found = 0 for field in fields: if field.name == "age": found = type.field_value[User, int64](view user, view field) @@ -81,7 +81,7 @@ function user_age_field_value() returns int64: function user_secret_field_redacted() returns string: User user = User(id: "u1", age: 42, api_key: "secret-token") list[TypeField] fields = type.fields[User]() - string found = "missing" + mutable string found = "missing" for field in fields: if field.name == "api_key": found = secret.redact(type.field_value[User, secret[string]](view user, view field)) @@ -90,7 +90,7 @@ function user_secret_field_redacted() returns string: function pair_first_field_value() returns int64: Pair[int64, string] pair = Pair[int64, string](first: 7, second: "seven") list[TypeField] fields = type.fields[Pair[int64, string]]() - int64 found = 0 + mutable int64 found = 0 for field in fields: if field.name == "first": found = type.field_value[Pair[int64, string], int64](view pair, view field) diff --git a/tests/run_pass/uint64_checked_expression_runtime_types.jett b/tests/run_pass/uint64_checked_expression_runtime_types.jett index fb09df7e..dfe6c18e 100644 --- a/tests/run_pass/uint64_checked_expression_runtime_types.jett +++ b/tests/run_pass/uint64_checked_expression_runtime_types.jett @@ -13,15 +13,15 @@ function constructed_list_uint64_dispatch() returns string: default fallback )) function appended_list_uint64_dispatch() returns string: - list[uint64] values = list.new[uint64]() - values = list.append[uint64](view values, 7) + mutable list[uint64] values = list.new[uint64]() + values = list.append[uint64](values, 7) uint64 fallback = 0 return Show.show(view(list.get[uint64](view values, 0) handle: default fallback )) function map_value_uint64_dispatch() returns string: map[string, uint64] values = map("count": 9) - optional[uint64] maybe_value = map.get[string, uint64](view values, "count") + optional[uint64] maybe_value = map.get[string, uint64](values, "count") uint64 fallback = 0 return Show.show(view(maybe_value handle: default fallback @@ -29,7 +29,7 @@ function map_value_uint64_dispatch() returns string: function set_to_list_uint64_dispatch() returns string: mutable set[uint64] values = set.new[uint64]() values = set.add[uint64](values, 17) - list[uint64] items = set.to_list[uint64](view values) + list[uint64] items = set.to_list[uint64](values) uint64 fallback = 0 return Show.show(view(list.get[uint64](view items, 0) handle: default fallback diff --git a/tests/runtime_fail/math_factorial_overflow.jett b/tests/runtime_fail/math_factorial_overflow.jett index ccfecfa7..9b32c2c8 100644 --- a/tests/runtime_fail/math_factorial_overflow.jett +++ b/tests/runtime_fail/math_factorial_overflow.jett @@ -1,3 +1,3 @@ function main() returns nothing: - int64 result = math.factorial(21) + int64 computed = math.factorial(21) return nothing diff --git a/tests/runtime_fail/math_mod_overflow.jett b/tests/runtime_fail/math_mod_overflow.jett index 1c80e57f..747e6697 100644 --- a/tests/runtime_fail/math_mod_overflow.jett +++ b/tests/runtime_fail/math_mod_overflow.jett @@ -1,4 +1,4 @@ function main() returns nothing: int64 minimum = -9223372036854775807 - 1 - int64 result = math.mod(minimum, -1) + int64 computed = math.mod(minimum, -1) return nothing diff --git a/tools/measure_resource_native_archives.py b/tools/measure_resource_native_archives.py new file mode 100644 index 00000000..d9698502 --- /dev/null +++ b/tools/measure_resource_native_archives.py @@ -0,0 +1,1504 @@ +#!/usr/bin/env python3 +"""Measure fresh private Resource archives, then run their pinned Source gate. + +These archives are compiler-test artifacts, never production launcher packages. +Preparation and gate execution are separate commands and separate processes. +""" +from __future__ import annotations + +import argparse +from collections import Counter +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +import hashlib +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +from typing import TypeAlias, TypeGuard + + +ROOT = Path(__file__).resolve().parents[1] +TARGET = "x86_64-unknown-linux-gnu" +MSVC_TARGET = "x86_64-pc-windows-msvc" +MSVC_RUSTFLAGS = ("-C", "target-feature=+crt-static", "-C", "debuginfo=0") +MSVC_FLAGS_ENV = "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS" +# Raw-dylib import members observed in the supported MSVC standard runtime. +# New DLLs require an independent observation before extending this closed set. +MSVC_IMPORT_DLLS = frozenset({ + "advapi32.dll", "api-ms-win-core-handle-l1-1-0.dll", "api-ms-win-core-synch-l1-2-0.dll", + "bcp47mrm.dll", "bcryptprimitives.dll", "elscore.dll", "gdi32.dll", "icu.dll", + "icuin.dll", "icuuc.dll", "kernel32.dll", "normaliz.dll", "ntdll.dll", + "oleaut32.dll", "user32.dll", "usp10.dll", +}) +OCCUPIED_STRUCT_TESTS = ( + "resource_execution::occupied_struct_harness::" + "native_resource_occupied_struct_scripts_retire_before_teardown", + "resource_execution::occupied_struct_harness::" + "native_resource_occupied_struct_scripts_reenter_with_same_provider_grant", +) +CURRENT_LAYOUT_WIRE = 3 +CARRIER_EXPORTS = frozenset( + "jett_rt_v1_resource_carrier_" + suffix for suffix in ( + "construct_begin", "child", "commit", "transfer", "qualify", "borrow", "observe", + "extract", "adapt", "end", "retire", "reset_iteration", "publish", + "child_scalar_task", "observe_scalar_task", + ) +) +SCALAR_TASK_SIGNATURES: dict[str, tuple[str, ...]] = { + "jett_rt_v1_resource_carrier_child_scalar_task": ( + "*const JettRuntimeContextV1", "u64", "u32", "u64", "u64", + ), + "jett_rt_v1_resource_carrier_observe_scalar_task": ( + "*const JettRuntimeContextV1", "u64", "u32", "u64", "u64", "*mut u64", "*mut u64", + ), +} +SCALAR_TASK_EXPORTS = frozenset(SCALAR_TASK_SIGNATURES) +CFGS = ["test", "jett_resource_native_test_archive"] +STARTUP_SIGNATURES: dict[str, tuple[str, ...]] = { + "jett_rt_v1_resource_program_begin": ( + "*const JettRuntimeContextV1", "*const ObjectManifest", "*mut u64", + "*mut JettRuntimeResultV1", + ), + "jett_rt_v1_resource_program_complete": ( + "*const JettRuntimeContextV1", "u64", "u32", + ), + "jett_rt_v1_resource_program_unwind": ( + "*const JettRuntimeContextV1", "u32", "u32", "*mut u32", + ), + "jett_rt_v1_resource_program_failure": ( + "*const JettRuntimeContextV1", "*mut JettRuntimeResultV1", + ), + "jett_rt_v1_resource_program_failure_copy": ( + "*const JettRuntimeContextV1", "*mut u8", "u64", "*mut u64", + "*mut JettRuntimeResultV1", + ), +} +STARTUP_EXPORTS = frozenset(STARTUP_SIGNATURES) +GRAPH_DESCRIPTOR_EXPORT = "jett_rt_v1_resource_graph_descriptor_box" +GRAPH_DESCRIPTOR_SIGNATURE = ( + "*const JettRuntimeContextV1", "u64", "u32", "u64", "*mut u64", +) +STRUCT_REPLACE_EXPORT = "jett_rt_v1_struct_replace_occupied" +ACTOR_REPLACE_EXPORT = "jett_rt_v1_actor_replace_occupied" +PIN_ENV = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1" +RECEIPT_ENV = "JETT_RESOURCE_NATIVE_TEST_ARCHIVE_RECEIPT_V1" +TEST = ("resource_execution::" + "native_resource_original_source_lifecycle_matches_reference_and_retires_before_teardown") +Json: TypeAlias = str | int | bool | None | list["Json"] | dict[str, "Json"] + + +class MeasurementError(RuntimeError): + """A build or independent observation cannot support an acceptance receipt.""" + + +@dataclass(frozen=True) +class Tools: + cargo: Path + rustc: Path + nm: Path + readelf: Path + cc: Path + + +@dataclass(frozen=True) +class MsvcTools: + cargo: Path + rustc: Path + nm: Path + readobj: Path + linker: Path + + +def windows_arguments(command: str) -> list[str]: + """Decode the Windows CRT argv quoting used by Cargo's verbose command log.""" + arguments: list[str] = [] + index = 0 + while index < len(command): + while index < len(command) and command[index] in " \t": + index += 1 + if index == len(command): + break + value: list[str] = [] + quoted = False + while index < len(command) and (quoted or command[index] not in " \t"): + slashes = 0 + while index < len(command) and command[index] == "\\": + slashes += 1 + index += 1 + if index < len(command) and command[index] == '"': + value.extend("\\" * (slashes // 2)) + if slashes % 2: + value.append('"') + elif quoted and index + 1 < len(command) and command[index + 1] == '"': + value.append('"') + index += 1 + else: + quoted = not quoted + index += 1 + else: + value.extend("\\" * slashes) + if index < len(command) and (quoted or command[index] not in " \t"): + value.append(command[index]) + index += 1 + require(not quoted, "unterminated Windows command quoting") + arguments.append("".join(value)) + return arguments + + +def option_values(arguments: Sequence[str], option: str) -> list[str]: + values: list[str] = [] + for index, argument in enumerate(arguments): + if argument == option: + require(index + 1 < len(arguments), f"missing actual argument for {option}") + values.append(arguments[index + 1]) + elif argument.startswith(option + "="): + values.append(argument[len(option) + 1:]) + elif option == "-C" and argument.startswith("-C") and len(argument) > 2: + values.append(argument[2:]) + return values + + +def msvc_compiler_commands(text: str, compiler: Path) -> list[list[str]]: + commands: list[list[str]] = [] + for line in text.splitlines(): + line = line.strip() + if not line.startswith("Running ") or "--crate-name" not in line: + continue + command = line.removeprefix("Running ") + require(command.startswith("`") and command.endswith("`"), + "actual Windows Cargo command delimiters are missing") + arguments = windows_arguments(command[1:-1]) + require(bool(arguments) and Path(arguments[0]).absolute() == compiler.absolute(), + "Cargo used an unmeasured Windows compiler or compiler wrapper") + commands.append(arguments) + require(bool(commands), "actual Windows compiler invocation is missing") + return commands + + +def checked_msvc_flags(arguments: Sequence[str]) -> None: + codes = option_values(arguments, "-C") + features = [value for value in codes if value.startswith("target-feature=")] + debug = [value for value in codes if value.startswith("debuginfo=")] + require(features == ["target-feature=+crt-static"] and bool(debug) + and all(value == "debuginfo=0" for value in debug), + "actual MSVC runtime command must use static CRT and zero debug information") + require(not any(value.startswith("incremental=") for value in codes), + "incremental private archive compilation is forbidden") + + +def msvc_selected_cfgs(text: str, compiler: Path) -> None: + commands = msvc_compiler_commands(text, compiler) + selected = [arguments for arguments in commands + if any(value in CFGS for value in option_values(arguments, "--cfg"))] + require(len(selected) == 1, "private cfgs must occur only in one selected Windows runtime command") + arguments = selected[0] + require(option_values(arguments, "--crate-name") == ["jett_runtime"], + "private cfg escaped selected runtime package") + cfgs = option_values(arguments, "--cfg") + require([value for value in cfgs if value in CFGS] == CFGS, + "both private cfgs must occur exactly once in the selected runtime command") + require("staticlib" in option_values(arguments, "--crate-type") and "--test" not in arguments, + "private runtime must be a staticlib without the Rust test harness") + require(option_values(arguments, "--target") == [MSVC_TARGET], + "selected runtime command has the wrong MSVC target") + require(option_values(arguments, "--print") == ["native-static-libs"], + "selected runtime must actually print its native library vector") + checked_msvc_flags(arguments) + + +def msvc_library_name(argument: str) -> str: + # Decode only the two observed safe linker forms; retain the raw argv elsewhere. + if argument.lower().startswith("/defaultlib:"): + name = argument[len("/defaultlib:"):] + require(re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.+-]*\.lib|[A-Za-z0-9_][A-Za-z0-9_+-]*", + name, re.IGNORECASE) is not None, + f"unsafe MSVC default library argument: {argument}") + return name if name.lower().endswith(".lib") else name + ".lib" + require(re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.+-]*\.lib", argument) is not None, + f"unexpected MSVC native library argument: {argument}") + return argument + + +def checked_msvc_library(argument: str) -> None: + lower = msvc_library_name(argument).lower() + require("jett_runtime" not in lower and "jett_native_launcher" not in lower, + "a second Rust runtime archive is forbidden") + require(lower not in {"msvcrt.lib", "msvcrtd.lib", "ucrt.lib", "ucrtd.lib", + "vcruntime.lib", "vcruntimed.lib", "libcmtd.lib", + "libucrtd.lib", "libvcruntimed.lib"}, + f"dynamic or debug CRT library observed: {argument}") + + +def checked_msvc_libraries(arguments: Sequence[str]) -> None: + require(bool(arguments), "native-static-libs vector must not be empty") + for argument in arguments: + checked_msvc_library(argument) + require("libcmt.lib" in {msvc_library_name(value).lower() for value in arguments}, + "MSVC static CRT libcmt.lib was not observed") + + +def msvc_native_libraries(text: str) -> list[str]: + lines = text.splitlines() + indices = [index for index, line in enumerate(lines) if "note: native-static-libs:" in line] + require(len(indices) == 1, "exactly one actual native-static-libs observation is required") + index = indices[0] + parts = [lines[index].split("note: native-static-libs:", 1)[1]] + for line in lines[index + 1:]: + if re.match(r"\s+Finished\s", line): + break + if not line.strip() or not line[0].isspace(): + break + # A wrapped vector may contain /defaultlib tokens, never response files or paths. + continuation = windows_arguments(line.strip()) + require(bool(continuation), "empty native library continuation") + for argument in continuation: + checked_msvc_library(argument) + parts.append(line.strip()) + arguments = windows_arguments(" ".join(parts)) + checked_msvc_libraries(arguments) + return arguments + + +def checked_msvc_environment(environment: Mapping[str, str], linker: Path) -> None: + require(bool(environment.get("VCINSTALLDIR")), "MSVC measurement requires an inherited Developer Shell") + require(environment.get("VSCMD_ARG_TGT_ARCH") == "x64", "Developer Shell target architecture must be x64") + require(bool(environment.get("LIB")), "Developer Shell SDK LIB environment is missing") + require(bool(environment.get("PATH")), "Developer Shell PATH environment is missing") + first = shutil.which("link.exe", path=environment.get("PATH")) + require(first is not None and Path(first).absolute() == linker.absolute(), + "measured linker must be the first link.exe in the inherited Developer Shell PATH") + override = environment.get("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER") + require(not override or Path(override).absolute() == linker.absolute(), + "target linker override differs from measured link.exe") + + +def msvc_build_environment() -> dict[str, str]: + environment = os.environ.copy() + for name in ("RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", MSVC_FLAGS_ENV): + require(not environment.get(name, "").strip(), f"MSVC measurement requires empty incoming {name}") + for name in ("RUSTC", "CARGO_BUILD_RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"): + require(not environment.get(name), f"unmeasured compiler wrapper: {name}") + environment.pop(PIN_ENV, None) + environment.pop(RECEIPT_ENV, None) + environment["CARGO_TERM_COLOR"] = "never" + environment[MSVC_FLAGS_ENV] = " ".join(MSVC_RUSTFLAGS) + environment["CARGO_INCREMENTAL"] = "0" + environment["CARGO_PROFILE_DEV_DEBUG"] = "0" + environment["CARGO_PROFILE_RELEASE_DEBUG"] = "0" + return environment + + +def msvc_fingerprint_records(directory: Path, snapshot_directory: Path | None = None, *, + selected_filename: str | None = None) -> list[Json]: + if selected_filename is None: + paths = sorted(directory.glob("jett_runtime-*/lib-jett_runtime.json")) + else: + require(re.fullmatch(r"jett_runtime-[a-f0-9]{16}", selected_filename) is not None, + "selected MSVC fingerprint must have one canonical runtime identity") + paths = [directory / selected_filename / "lib-jett_runtime.json"] + require(bool(paths), "runtime Cargo fingerprint is missing") + records: list[Json] = [] + for path in paths: + require(path.is_file(), f"runtime Cargo fingerprint is missing: {path}") + if snapshot_directory is not None: + path = snapshot_artifact(path, snapshot_directory / path.parent.name / path.name) + value = read_object(path) + require(value.get("rustflags") == list(MSVC_RUSTFLAGS), + "MSVC target fingerprint must contain the exact static CRT and zero-debug flags") + records.append({**artifact(path), "raw_json_utf8": path.read_text(encoding="utf-8")}) + return records + + +def checked_coff_member(text: str) -> list[str]: + files = re.findall(r"^File: (.+)$", text, re.MULTILINE) + formats = re.findall(r"^Format: (.+)$", text, re.MULTILINE) + require(len(files) == len(formats) == 1, "each COFF member needs one file and target format observation") + architectures = re.findall(r"^Arch: (.+)$", text, re.MULTILINE) + sizes = re.findall(r"^AddressSize: (.+)$", text, re.MULTILINE) + machines = re.findall(r"^\s+Machine: (.+)$", text, re.MULTILINE) + directives = re.findall(r"^Directive\(s\):[ \t]*(.*)$", text, re.MULTILINE) + if formats == ["COFF-x86-64"]: + require(architectures == ["x86_64"] and sizes == ["64bit"] + and machines == ["IMAGE_FILE_MACHINE_AMD64 (0x8664)"], + "every ordinary private archive member must have complete COFF AMD64 headers") + return directives + require(formats == ["COFF-import-file-x86-64"], "unknown or foreign private archive member format") + require(files[0].lower() in MSVC_IMPORT_DLLS, + f"private archive import DLL is outside the supported Windows system set: {files[0]}") + require(not architectures and not sizes and not machines and not directives, + "COFF import members must use their observed import-file metadata") + kinds = re.findall(r"^Type: (.+)$", text, re.MULTILINE) + names = re.findall(r"^Name type: (.+)$", text, re.MULTILINE) + symbols = re.findall(r"^Symbol: (.+)$", text, re.MULTILINE) + require(kinds == ["code"] + and names == ["name"] and bool(symbols), + "complete supported AMD64 import-file metadata is required") + return [] + + +def checked_coff_archive(text: str) -> None: + text = "\n".join(text.splitlines()) + blocks = re.split(r"(?=^File: )", text, flags=re.MULTILINE) + require(len(blocks) > 1 and not blocks[0].strip(), "complete COFF member observations are required") + directives: list[str] = [] + ordinary = 0 + for block in blocks[1:]: + directives.extend(checked_coff_member(block)) + ordinary += len(re.findall(r"^Format: COFF-x86-64$", block, re.MULTILINE)) + require(ordinary > 0, "private runtime must contain ordinary AMD64 COFF definitions") + # Rust staticlibs may expose no embedded directives; retain that absence. + libraries: list[str] = [] + for line in directives: + for argument in windows_arguments(line): + lower = argument.lower() + require(not lower.startswith(("/nodefaultlib", "-nodefaultlib")), + "COFF directives suppress measured CRT default libraries") + if lower.startswith(("/defaultlib:", "-defaultlib:")): + library = argument.split(":", 1)[1] + libraries.append(library if library.lower().endswith(".lib") else library + ".lib") + if "runtimelibrary=" in lower: + require(lower.split("runtimelibrary=", 1)[1] == "mt_staticrelease", + "COFF RuntimeLibrary directive must select the release static CRT") + for library in libraries: + checked_msvc_library(library) + +def require(condition: bool, message: str) -> None: + if not condition: + raise MeasurementError(message) + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def artifact(path: Path) -> dict[str, Json]: + require(path.is_file(), f"missing measured file: {path}") + return {"path": str(path.resolve()), "sha256": digest(path)} + + +def reject_reparse_ancestors(path: Path) -> None: + for ancestor in (path, *path.parents): + require(not ancestor.is_symlink(), f"symlink is not a bounded measurement path: {ancestor}") + if os.name == "nt" and ancestor.exists(): + require(ancestor.lstat().st_file_attributes & 0x400 == 0, + f"reparse point is not a bounded measurement path: {ancestor}") + + +def bounded_msvc_build_cache(cache_root: Path, target_dir: Path, output: Path) -> Path: + require(cache_root.is_absolute() and target_dir.is_absolute() and output.is_absolute(), + "explicit MSVC cache, root and evidence paths must be absolute") + for path in (cache_root, target_dir, output): + reject_reparse_ancestors(path) + require(cache_root.is_dir(), "reuse requires an existing bounded cache root") + root = cache_root.resolve(strict=True) + cache = target_dir.resolve() + evidence = output.resolve() + require(cache != root and cache.is_relative_to(root) + and evidence != root and evidence.is_relative_to(root), + "MSVC cache and evidence must remain strict descendants of the explicit root") + require(not cache.is_relative_to(evidence) and not evidence.is_relative_to(cache), + "MSVC cache and immutable evidence must not overlap") + require(not cache.exists() or cache.is_dir(), "MSVC cache must be a directory") + return cache + + +def snapshot_artifact(source: Path, destination: Path) -> Path: + require(source.is_file(), f"snapshot source is missing: {source}") + reject_reparse_ancestors(source) + reject_reparse_ancestors(destination) + require(source.resolve() != destination.resolve() and not destination.exists(), + "immutable snapshot destination must be distinct and absent") + expected = digest(source) + destination.parent.mkdir(parents=True, exist_ok=True) + with source.open("rb") as reader, destination.open("xb") as writer: + shutil.copyfileobj(reader, writer) + require(digest(source) == expected and digest(destination) == expected, + "snapshot source changed or copied bytes differ") + return destination + + +def write_json(path: Path, value: Json) -> None: + with path.open("x", encoding="utf-8", newline="\n") as output: + output.write(json.dumps(value, indent=2, sort_keys=True) + "\n") + + +def json_array(value: object) -> TypeGuard[Sequence[object]]: + return isinstance(value, list) + + +def json_object(value: object) -> TypeGuard[Mapping[object, object]]: + return isinstance(value, dict) + + +def normalize_json(value: object) -> Json: + if value is None or isinstance(value, (str, bool, int)): + return value + if json_array(value): + return [normalize_json(item) for item in value] + if json_object(value): + output: dict[str, Json] = {} + for key, item in value.items(): + require(isinstance(key, str), "JSON object keys must be strings") + if isinstance(key, str): + output[key] = normalize_json(item) + return output + raise MeasurementError("unexpected JSON value in measured record") + + +def read_object(path: Path) -> dict[str, Json]: + raw: object = json.loads(path.read_text(encoding="utf-8")) + value = normalize_json(raw) + require(isinstance(value, dict), f"expected JSON object: {path}") + if isinstance(value, dict): + return value + raise MeasurementError(f"expected JSON object: {path}") + + +def executable(value: str) -> Path: + found = shutil.which(value) + require(found is not None, f"required executable is unavailable: {value}") + if found is None: + raise MeasurementError(f"required executable is unavailable: {value}") + # Rustup proxy dispatch depends on argv[0]; preserve cargo/rustc symlinks. + path = Path(found).absolute() + require(path.is_file(), f"executable must be a regular file: {path}") + return path + + +def logged(argv: list[str], directory: Path, environment: dict[str, str], + log: Path, timeout: int = 3600) -> str: + with log.open("xb") as output: + result = subprocess.run(argv, cwd=directory, env=environment, stdin=subprocess.DEVNULL, + stdout=output, stderr=subprocess.STDOUT, timeout=timeout) + text = log.read_text(encoding="utf-8", errors="replace") + require(result.returncode == 0, f"command exited {result.returncode}; see {log}") + return text + + +def sources(repo: Path, complete: bool = False) -> dict[str, Json]: + paths = {repo / "Cargo.toml", repo / "Cargo.lock", + repo / "crates/jett_runtime/Cargo.toml", + repo / "tools/measure_resource_native_archives.py"} + paths.update((repo / "crates/jett_runtime").rglob("*.rs")) + paths.update((repo / ".cargo").rglob("*.toml")) + if complete: + paths.add(repo / ".github/workflows/native.yml") + for suffix in ("*.rs", "*.jett", "Cargo.toml"): + paths.update((repo / "crates").rglob(suffix)) + paths.update((repo / "stdlib").rglob("*.jett")) + return {str(path.relative_to(repo)): digest(path) for path in sorted(paths)} + + +def build_environment() -> dict[str, str]: + environment = os.environ.copy() + for name in ("RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", + "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS"): + require(not environment.get(name, "").strip(), + f"GNU dynamic-CRT measurement requires empty {name}") + for name in ("RUSTC", "CARGO_BUILD_RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"): + require(not environment.get(name), f"unmeasured compiler wrapper: {name}") + # The selected Cargo command alone supplies the two private cfgs. + environment.pop(PIN_ENV, None) + environment.pop(RECEIPT_ENV, None) + # Cargo command identity and rustc's native-library note are parsed as text. + # Override user/config color preferences instead of discarding escape bytes. + environment["CARGO_TERM_COLOR"] = "never" + return environment + + +def native_libraries(text: str) -> list[str]: + lines = text.splitlines() + indices = [index for index, line in enumerate(lines) if "note: native-static-libs:" in line] + require(len(indices) == 1, "exactly one actual native-static-libs observation is required") + index = indices[0] + parts = [lines[index].split("note: native-static-libs:", 1)[1]] + for line in lines[index + 1:]: + if not re.fullmatch(r"\s+-\S.*", line): + break + parts.append(line.strip()) + arguments = shlex.split(" ".join(parts)) + require(bool(arguments), "native-static-libs vector must not be empty") + for argument in arguments: + lower = argument.lower() + require("jett_runtime" not in lower and "jett_native_launcher" not in lower, + "a second Rust runtime archive is forbidden") + require(argument.startswith("-l") and len(argument) > 2, + f"unexpected GNU native library argument: {argument}") + require("-lc" in arguments and "-lgcc_s" in arguments, + "GNU dynamic CRT libraries were not observed") + return arguments + + +def selected_cfgs(text: str, compiler: Path | None = None) -> None: + commands = [line.strip() for line in text.splitlines() if line.strip().startswith("Running ")] + private = [line for line in commands if "jett_resource_native_test_archive" in line] + require(len(private) == 1, "private cfg must occur in exactly one executed rustc command") + command = private[0] + require("--crate-name jett_runtime " in command, "private cfg escaped selected runtime package") + require("--cfg test " in command and "--cfg jett_resource_native_test_archive" in command, + "both selected-package cfgs must occur in the actual rustc command") + require("--crate-type staticlib" in command, "private runtime staticlib was not built") + if compiler is not None: + checked_compiler_commands(text, compiler) + + +def checked_compiler_commands(text: str, compiler: Path) -> None: + commands = [line.strip() for line in text.splitlines() + if line.strip().startswith("Running ") and "--crate-name " in line] + require(bool(commands), "actual compiler invocation is missing") + for command in commands: + arguments = shlex.split(command.removeprefix("Running ").strip("`")) + require(bool(arguments) and Path(arguments[0]).absolute() == compiler.absolute(), + "Cargo used an unmeasured compiler or compiler wrapper") + + +def startup_exports(directory: Path) -> set[str]: + owner, child = directory.with_suffix(".rs"), directory / "program_entry.rs" + require(owner.is_file() and child.is_file(), "Resource startup module sources are required") + owner_text = owner.read_text(encoding="utf-8") + module_pattern = r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)mod[ \t]+program_entry[ \t]*;' + modules = re.findall(module_pattern, owner_text) + require(len(re.findall(r"\bmod\s+program_entry\b", owner_text)) == 1 and modules == [""], + "Resource startup must have one unconditional canonical child module") + text = child.read_text(encoding="utf-8") + require("#![" not in text, "Resource startup child cannot carry conditional module attributes") + pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'pub\s+unsafe\s+extern\s+"C"\s+fn\s+(jett_rt_v1_resource_[a-z_]+)' + r'\s*\(([^)]*)\)\s*->\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{') + declarations = re.findall(pattern, text) + names = [name for _, name, _, _ in declarations] + require(len(names) == len(set(names)) == 5 and set(names) == STARTUP_EXPORTS, + "five exact Resource startup exports are required") + for attributes, name, parameters, result in declarations: + require(attributes.strip() == "#[unsafe(no_mangle)]", + f"Resource startup export must be unconditional and unmangled: {name}") + types: list[str] = [] + for parameter in parameters.split(","): + if not parameter.strip(): + continue + parts = parameter.split(":", 1) + require(len(parts) == 2, f"invalid Resource startup parameter: {name}") + types.append(" ".join(parts[1].split())) + require(tuple(types) == STARTUP_SIGNATURES[name] and result == "JettRuntimeStatusV1", + f"Resource startup C signature differs: {name}") + return set(names) + + +def graph_descriptor_export(directory: Path) -> set[str]: + owner, child = directory.with_suffix(".rs"), directory / "graph_descriptors.rs" + require(owner.is_file() and child.is_file(), "checked graph descriptor proof module is required") + owner_text = owner.read_text(encoding="utf-8") + module_pattern = r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)mod[ \t]+graph_descriptors[ \t]*;' + require(len(re.findall(r"\bmod\s+graph_descriptors\b", owner_text)) == 1 + and re.findall(module_pattern, owner_text) == [""], + "checked graph descriptor must have one unconditional canonical private module") + proof_text = child.read_text(encoding="utf-8") + require("#![" not in proof_text, "checked graph descriptor proof module cannot be conditional") + proof_pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'pub\(in crate::native_abi\) struct CheckedGraphHookProof\s*\{([^}]*)\}') + proof = re.findall(proof_pattern, proof_text) + require(len(proof) == len(re.findall(r"\bstruct\s+CheckedGraphHookProof\b", proof_text)) == 1 + and proof[0][0].strip() == "#[derive(Clone)]", + "checked graph descriptor proof must retain its unconditional private metadata owner") + fields = [" ".join(field.split()) for field in proof[0][1].split(",") if field.strip()] + require(fields == ["context: ContextRegistryKey", "program: ResourceHandleId", + "layout: Arc", "descriptor: ResourceHandleId", + "descriptor_operation: u32", "hook: u32", "signature: u32"], + "checked graph descriptor proof cannot acquire Scope, grant or owner fields") + pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'pub\s+unsafe\s+extern\s+"C"\s+fn\s+(jett_rt_v1_resource_graph_descriptor_box)' + r'\s*\(([^)]*)\)\s*->\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{') + declarations = re.findall(pattern, (directory / "leaves.rs").read_text(encoding="utf-8")) + require(len(declarations) == 1, "one exact canonical graph descriptor export is required") + attributes, name, parameters, result = declarations[0] + require(name == GRAPH_DESCRIPTOR_EXPORT and attributes.strip() == "#[unsafe(no_mangle)]", + "checked graph descriptor export must be unconditional and unmangled") + types: list[str] = [] + for parameter in parameters.split(","): + if not parameter.strip(): + continue + parts = parameter.split(":", 1) + require(len(parts) == 2, "invalid checked graph descriptor parameter") + types.append(" ".join(parts[1].split())) + require(tuple(types) == GRAPH_DESCRIPTOR_SIGNATURE and result == "JettRuntimeStatusV1", + "checked graph descriptor C signature differs") + return {name} + + +def scalar_task_exports(directory: Path) -> set[str]: + leaves = directory / "leaves.rs" + child = directory / "leaves/carriers.rs" + require(leaves.is_file() and child.is_file(), "scalar carrier leaf sources are required") + owner_text = leaves.read_text(encoding="utf-8") + module_pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'mod[ \t]+carrier_leaves[ \t]*;') + require(len(re.findall(r"\bmod\s+carrier_leaves\b", owner_text)) == 1 + and re.findall(module_pattern, owner_text) == ['#[path = "leaves/carriers.rs"]\n'], + "scalar carrier leaves must retain their unconditional canonical private child module") + text = child.read_text(encoding="utf-8") + require("#![" not in text, "scalar carrier child cannot carry conditional module attributes") + pattern = (r'(?m)^[ \t]*((?:#\[[^\n]*\][ \t]*\n[ \t]*)*)' + r'pub\s+unsafe\s+extern\s+"C"\s+fn\s+(jett_rt_v1_resource_carrier_[a-z_]+)' + r'\s*\(([^)]*)\)\s*->\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{') + declarations = [row for row in re.findall(pattern, text) if row[1] in SCALAR_TASK_EXPORTS] + names = [name for _, name, _, _ in declarations] + require(len(names) == len(set(names)) == 2 and set(names) == SCALAR_TASK_EXPORTS, + "two exact additive scalar-task C exports are required") + for attributes, name, parameters, result in declarations: + require(attributes.strip() == "#[unsafe(no_mangle)]", + f"scalar-task export must be unconditional and unmangled: {name}") + types: list[str] = [] + for parameter in parameters.split(","): + if not parameter.strip(): + continue + parts = parameter.split(":", 1) + require(len(parts) == 2, f"invalid scalar-task parameter: {name}") + types.append(" ".join(parts[1].split())) + require(tuple(types) == SCALAR_TASK_SIGNATURES[name] and result == "JettRuntimeStatusV1", + f"scalar-task C signature differs: {name}") + return set(names) + + +def export_names(repo: Path) -> tuple[set[str], set[str]]: + directory = repo / "crates/jett_runtime/src/native_abi/resource" + pattern = r'pub unsafe extern "C" fn (jett_rt_v1_resource_[a-z_]+)\(' + leaves = re.findall(pattern, (directory / "leaves.rs").read_text(encoding="utf-8")) + carriers: list[str] = re.findall(pattern, (directory / "leaves/carriers.rs").read_text(encoding="utf-8")) + private = re.findall(pattern, (directory / "test_archive_api.rs").read_text(encoding="utf-8")) + graph = graph_descriptor_export(directory) + scalar = scalar_task_exports(directory) + legacy = [name for name in leaves if name not in graph] + require(len(leaves) == len(set(leaves)) == 38 and len(legacy) == len(set(legacy)) == 37, + "frozen Resource leaf contract must contain 37 original names and one exact graph descriptor") + require(not set(leaves) & CARRIER_EXPORTS, "carrier exports must occur only in their declared child module") + require(len(carriers) == len(set(carriers)) == 15 and set(carriers) == set(CARRIER_EXPORTS), + "carrier leaf contract must retain 13 original and two exact scalar-task names") + require(len(private) == len(set(private)) == 8, "frozen private test contract must contain eight names") + require(all(name.startswith("jett_rt_v1_resource_test_") for name in private), + "unexpected private export name") + require(len(set(legacy) | (set(carriers) - scalar)) == 50, + "original Resource contract must retain 50 names") + require(len(set(leaves) | (set(carriers) - scalar)) == 51, + "graph Resource contract must retain exactly 51 original names") + require(len(set(leaves) | set(carriers)) == 53, + "graph and scalar Resource contract must contain exactly 53 names") + startup = startup_exports(directory) + require(not startup & (set(leaves) | set(carriers) | set(private)), + "Resource startup exports belong only to their canonical child module") + production = set(leaves) | set(carriers) | startup + require(len(production) == 58, "fifty-eight production Resource exports are required") + return production, set(private) + + +def checked_layout_wire(repo: Path) -> None: + registration = repo / "crates/jett_runtime/src/resource_custody/registration.rs" + versions = re.findall(r"pub\(crate\) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = ([0-9]+);", + registration.read_text(encoding="utf-8")) + require(versions == [str(CURRENT_LAYOUT_WIRE)], + "fresh carrier archives require the exact current runtime wire version 3") + + +def checked_measurement_source(repo: Path) -> None: + retained = repo / "tools/measure_resource_native_archives.py" + require(retained.is_file() and digest(retained) == digest(Path(__file__)), + "executing measurement helper differs from the measured repository helper") + + +def check_symbols(text: str, leaves: set[str], private: set[str]) -> None: + definitions: Counter[str] = Counter() + production: set[str] = set() + test_exports: set[str] = set() + for line in text.splitlines(): + fields = line.split() + if len(fields) < 3 or len(fields[1]) != 1: + continue + name, kind = fields[:2] + if name == "main" or name.startswith("jett_rt_v1_resource_"): + require(kind == "T", f"expected one global text definition for {name}, got {kind}") + definitions[name] += 1 + if name.startswith("jett_rt_v1_resource_test_"): + test_exports.add(name) + elif name != "main": + production.add(name) + require(production == leaves, "Resource leaf symbol inventory differs from frozen source") + require(test_exports == private, "private test symbol inventory differs from frozen source") + for name in leaves | private | {"main"}: + require(definitions[name] == 1, f"expected exactly one definition of {name}") + + +def checked_struct_replace_leaf(repo: Path) -> None: + text = (repo / "crates/jett_runtime/src/native_abi/values.rs").read_text(encoding="utf-8") + invocations = re.findall(r"(?ms)^leaves! \{\n(.*?)^\}\s*$", text) + require(len(invocations) == 1, "ordinary leaf catalog must retain one canonical invocation") + body = invocations[0] + require("/*" not in body and "*/" not in body, + "ordinary leaf signature observation requires its uncommented canonical invocation") + rows = re.findall(r"(?m)^[ \t]*StructReplaceOccupied,[^\n]*", body) + expected = ( + r"\s*StructReplaceOccupied,\s*jett_rt_v1_struct_replace_occupied,\s*false,\s*\(" + r"value:\s*u64\s*=>\s*I64,\s*index:\s*u64\s*=>\s*I64,\s*" + r"bits:\s*u64\s*=>\s*I64,\s*owned:\s*u32\s*=>\s*I32,\s*" + r"depth:\s*u64\s*=>\s*I64\),\s*u64\s*=>\s*I64,\s*" + ) + require(len(rows) == 1 and re.fullmatch(expected, rows[0]) is not None, + "StructReplaceOccupied must retain its exact ordinary C/ABI signature and cleanup mode") + contexts = re.findall(r'pub unsafe extern "C" fn \$name\(context:\s*([^,]+),\s*\$\(', text) + require(contexts == ["*const JettRuntimeContextV1"], + "ordinary leaf macro must retain its exact leading context pointer") + + +def check_struct_replace_symbols(text: str) -> None: + definitions = [line.split()[1] for line in text.splitlines() + if len(line.split()) >= 3 and line.split()[0] == STRUCT_REPLACE_EXPORT] + require(definitions == ["T"], + "StructReplaceOccupied must have exactly one global text definition") + + +def checked_actor_replace_leaf(repo: Path) -> None: + text = (repo / "crates/jett_runtime/src/native_abi/values.rs").read_text(encoding="utf-8") + macros = re.findall(r"(?ms)^macro_rules! leaves \{\n(.*?)^\}", text) + invocations = re.findall(r"(?ms)^leaves! \{\n(.*?)^\}\s*$", text) + require(len(macros) == len(invocations) == 1, + "ordinary actor leaf must retain one canonical macro and invocation") + macro, body = macros[0], invocations[0] + require(all("/*" not in part and "*/" not in part for part in (macro, body)), + "actor leaf signature observation requires its uncommented canonical catalog") + rows = re.findall(r"(?m)^[ \t]*ActorReplaceOccupied,[^\n]*", body) + expected = ( + r"\s*ActorReplaceOccupied,\s*jett_rt_v1_actor_replace_occupied,\s*false,\s*\(" + r"actor:\s*u64\s*=>\s*I64,\s*index:\s*u64\s*=>\s*I64,\s*" + r"bits:\s*u64\s*=>\s*I64,\s*owned:\s*u32\s*=>\s*I32,\s*" + r"depth:\s*u64\s*=>\s*I64\),\s*u64\s*=>\s*I64,\s*" + ) + require(len(rows) == 1 and re.fullmatch(expected, rows[0]) is not None, + "ActorReplaceOccupied must retain its exact ordinary C/ABI signature and cleanup mode") + contracts = { + "symbol": "pub fn symbol(self) -> &'static str { match self { $( Self::$variant => stringify!($name), )* } }", + "parameters": "pub fn parameters(self) -> &'static [AbiScalar] { match self { $( Self::$variant => &[AbiScalar::Pointer, $( AbiScalar::$abi, )*], )* } }", + "result": "pub fn result(self) -> AbiScalar { match self { $( Self::$variant => AbiScalar::$retabi, )* } }", + } + for name, contract in contracts.items(): + declarations = re.findall(rf"(?m)^[ \t]*pub fn {name}\(self\)[^\n]*", macro) + require([" ".join(row.split()) for row in declarations] == [contract], + f"actor leaf must retain the exact NativeLeaf {name} mapping") + wrappers = re.findall(r'(?m)^[ \t]*pub unsafe extern "C" fn \$name[^\n]*', macro) + require([" ".join(row.split()) for row in wrappers] == [ + 'pub unsafe extern "C" fn $name(context: *const JettRuntimeContextV1, $( $arg: $rust ),*) -> $ret {'], + "actor leaf must retain its exact leading context pointer and generated C signature") + dispatches = re.findall(r"(?m)^[ \t]*leaf\(context,[^\n]*", macro) + require([" ".join(row.split()) for row in dispatches] == ["leaf(context, $cleanup, $body)"], + "actor leaf macro must preserve the selected cleanup mode") + + +def check_actor_replace_symbols(text: str) -> None: + definitions = [line.split()[1] for line in text.splitlines() + if len(line.split()) >= 3 and line.split()[0] == ACTOR_REPLACE_EXPORT] + require(definitions == ["T"], + "ActorReplaceOccupied must have exactly one global text definition") + + +def fingerprint_records(directory: Path) -> list[Json]: + paths = sorted(directory.glob("jett_runtime-*/lib-jett_runtime.json")) + require(bool(paths), "runtime Cargo fingerprint is missing") + records: list[Json] = [] + for path in paths: + value = read_object(path) + require(value.get("rustflags") == [], "GNU target fingerprint has unexpected rustflags") + records.append({**artifact(path), "raw_json_utf8": path.read_text(encoding="utf-8")}) + return records + + +def toolchain(tools: Tools, repo: Path, output: Path, environment: dict[str, str]) -> dict[str, Json]: + observed: dict[str, Json] = {} + for name, path, arguments in ( + ("rustc", tools.rustc, ["-vV"]), ("cargo", tools.cargo, ["-vV"]), + ("nm", tools.nm, ["--version"]), ("readelf", tools.readelf, ["--version"]), + ("cc", tools.cc, ["--version"]), + ): + log = output / f"{name}-version.log" + text = logged([str(path), *arguments], repo, environment, log) + observed[name] = {"invocation_path": str(path), "executable": artifact(path), + "version_observation": artifact(log)} + if name == "rustc": + require(f"host: {TARGET}" in text.splitlines(), "private archives must be built on their GNU host") + target_log = output / "cc-target.log" + target = logged([str(tools.cc), "-dumpmachine"], repo, environment, target_log).strip() + require(re.fullmatch(r"x86_64(?:-[a-z0-9_]+)*-linux-gnu", target) is not None, + f"unexpected C linker target: {target}") + observed["cc_target"] = artifact(target_log) + return observed + + +def dynamic_crt(tools: Tools, repo: Path, output: Path, profile: str, + arguments: list[str], environment: dict[str, str]) -> dict[str, Json]: + source = output / f"{profile}-crt-probe.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + binary = output / f"{profile}-crt-probe" + command = [str(tools.cc), "-no-pie", "-o", str(binary), str(source), *arguments] + link_log = output / f"{profile}-crt-probe-link.log" + logged(command, repo, environment, link_log, timeout=60) + inspection = output / f"{profile}-crt-probe-elf.log" + text = logged([str(tools.readelf), "--file-header", "--program-headers", "--dynamic", str(binary)], + repo, environment, inspection, timeout=60) + require(re.search(r"Type:\s+EXEC\b", text) is not None, "GNU probe must be an ELF executable") + require("INTERP" in text and "(NEEDED)" in text and "libc.so" in text, + "dynamic GNU CRT interpreter/libc dependency was not observed") + return {"source": artifact(source), "binary": artifact(binary), "link_command": list(command), + "link_observation": artifact(link_log), "elf_observation": artifact(inspection), + "scope": "Measured C driver and native-library vector; not Resource Source execution"} + + +def measure_profile(tools: Tools, repo: Path, output: Path, profile: str, + environment: dict[str, str], inputs: dict[str, Json], + versions: dict[str, Json], leaves: set[str], private: set[str]) -> dict[str, Json]: + target_dir = output / f"{profile}-build" + command = [str(tools.cargo), "rustc", "--locked", "--color", "never", "--verbose", "-p", "jett_runtime", "--lib", + "--target", TARGET, "--target-dir", str(target_dir)] + if profile == "release": + command.append("--release") + command.extend(["--", "--cfg", "test", "--cfg", "jett_resource_native_test_archive", + "--print=native-static-libs"]) + build_log = output / f"{profile}-build.log" + text = logged(command, repo, environment, build_log) + require(sources(repo) == inputs, "runtime inputs changed during private archive build") + selected_cfgs(text, tools.rustc) + arguments = native_libraries(text) + directory = target_dir / TARGET / profile + archive = directory / "libjett_runtime.a" + require(archive.is_file() and archive.stat().st_size <= 256 * 1024 * 1024, + "private archive is missing or exceeds the harness limit") + archive_hash = digest(archive) + symbols = output / f"{profile}-symbols.log" + text = logged([str(tools.nm), "--defined-only", "--extern-only", "--format=posix", str(archive)], + repo, environment, symbols) + check_symbols(text, leaves, private) + elf = output / f"{profile}-archive-elf.log" + text = logged([str(tools.readelf), "--file-header", "--symbols", str(archive)], repo, environment, elf) + require("ELF64" in text and "X86-64" in text, "GNU archive target inspection failed") + fingerprints = fingerprint_records(directory / ".fingerprint") + probe = dynamic_crt(tools, repo, output, profile, arguments, environment) + require(sources(repo) == inputs, "runtime inputs changed during archive measurement") + require(digest(archive) == archive_hash, "runtime archive changed during independent measurement") + build = output / f"{profile}-build.json" + write_json(build, {"profile": profile, "target": TARGET, "toolchain": versions, + "runtime_sources": inputs, "workspace_lock": inputs["Cargo.lock"], + "cfgs_selected_package_only": list(CFGS), "actual_build_command": list(command), + "cargo_build_log": artifact(build_log), "archive": artifact(archive), + "measurement_script": artifact(Path(__file__))}) + libraries = output / f"{profile}-native-static-libs.json" + write_json(libraries, {"actual_print_log": artifact(build_log), "arguments": list(arguments), + "order_and_duplication_preserved": True}) + crt = output / f"{profile}-crt.json" + write_json(crt, {"actual_target_scoped_rustflags": [], "cargo_fingerprints": fingerprints, + "native_static_libs": artifact(libraries), "complete_symbol_inventory": artifact(symbols), + "complete_elf_archive_inventory": artifact(elf), "dynamic_crt_probe": probe, + "linker_path": str(tools.cc), "linker_executable": artifact(tools.cc), "mode": "dynamic"}) + return {"profile": profile, "target": TARGET, "archive_path": str(archive), + "archive_sha256": archive_hash, "runtime_abi": 1, "layout_wire": CURRENT_LAYOUT_WIRE, + "private_cfgs": list(CFGS), "crt_mode": "dynamic", "linker_path": str(tools.cc), + "native_library_args": list(arguments), "build_receipt": artifact(build), + "native_static_libs_receipt": artifact(libraries), "crt_receipt": artifact(crt)} + + +def msvc_linker_version(linker: Path, repo: Path, environment: dict[str, str], + log: Path) -> tuple[str, int]: + # MSVC's help command uses 1100 for valid help; build commands remain strict. + with log.open("xb") as output: + result = subprocess.run([str(linker), "/?"], cwd=repo, env=environment, + stdin=subprocess.DEVNULL, stdout=output, + stderr=subprocess.STDOUT, timeout=60) + text = log.read_text(encoding="utf-8", errors="replace") + require(result.returncode in (0, 1100), + f"linker version probe exited {result.returncode}; see {log}") + require(re.search(r"^Microsoft \(R\) Incremental Linker Version [0-9]+(?:\.[0-9]+){3}\s*$", + text, re.MULTILINE) is not None + and re.search(r"^\s*usage: LINK \[options\] \[files\] \[@commandfile\]\s*$", + text, re.MULTILINE) is not None + and re.search(r"^\s*options:\s*$", text, re.MULTILINE) is not None, + "linker version probe must contain the Microsoft version and complete help header") + require(re.search(r"(?:fatal\s+error|error\s+LNK[0-9]+)", text, re.IGNORECASE) is None, + "linker version probe contains an actual linker error") + return text, result.returncode + + +def msvc_toolchain(tools: MsvcTools, repo: Path, output: Path, + environment: dict[str, str]) -> dict[str, Json]: + observed: dict[str, Json] = {} + for name, path, arguments in ( + ("rustc", tools.rustc, ["-vV"]), ("cargo", tools.cargo, ["-vV"]), + ("nm", tools.nm, ["--version"]), ("readobj", tools.readobj, ["--version"]), + ("linker", tools.linker, ["/?"]), + ): + log = output / f"{name}-version.log" + if name == "linker": + text, returncode = msvc_linker_version(path, repo, environment, log) + else: + text = logged([str(path), *arguments], repo, environment, log) + returncode = 0 + observed[name] = {"invocation_path": str(path), "executable": artifact(path), + "version_observation": artifact(log), "returncode": returncode} + if name == "rustc": + require(f"host: {MSVC_TARGET}" in text.splitlines(), + "private MSVC archives must be built on their Windows MSVC host") + elif name in ("nm", "readobj"): + require("LLVM" in text, f"MSVC {name} observations require the LLVM tool") + elif name == "linker": + require("Microsoft" in text and "Linker" in text, + "measured link.exe must be the Microsoft linker") + return observed + + +def checked_msvc_linker_commands(text: str, compiler: Path, linker: Path) -> None: + for arguments in msvc_compiler_commands(text, compiler): + if option_values(arguments, "--target") != [MSVC_TARGET]: + continue + linkers = [value.removeprefix("linker=") for value in option_values(arguments, "-C") + if value.startswith("linker=")] + require(linkers == [str(linker)], "actual Windows compiler target linker differs from measured link.exe") + + +def measure_msvc_profile(tools: MsvcTools, repo: Path, output: Path, profile: str, + environment: dict[str, str], inputs: dict[str, Json], + versions: dict[str, Json], leaves: set[str], private: set[str], *, + build_cache: Path | None = None) -> dict[str, Json]: + target_dir = output / f"{profile}-build" if build_cache is None else build_cache + require(build_cache is not None or not target_dir.exists(), + "each private MSVC profile requires a fresh build directory unless reuse is explicit") + require(sources(repo) == inputs, "runtime inputs changed before private archive build") + command = [str(tools.cargo), "rustc", "--offline", "--locked", "--color", "never", "--verbose", + "-p", "jett_runtime", "--lib", "--target", MSVC_TARGET, "--target-dir", str(target_dir)] + if profile == "release": + command.append("--release") + command.extend(["--", "--cfg", "test", "--cfg", "jett_resource_native_test_archive", + "--print=native-static-libs"]) + build_log = output / f"{profile}-build.log" + text = logged(command, repo, environment, build_log) + require(sources(repo) == inputs, "runtime inputs changed during private archive build") + msvc_selected_cfgs(text, tools.rustc) + checked_msvc_linker_commands(text, tools.rustc, tools.linker) + runtime_commands = [argv for argv in msvc_compiler_commands(text, tools.rustc) + if option_values(argv, "--crate-name") == ["jett_runtime"]] + require(len(runtime_commands) == 1, "one actual selected runtime compiler invocation is required") + suffixes = [value.removeprefix("extra-filename=-") + for value in option_values(runtime_commands[0], "-C") + if value.startswith("extra-filename=-")] + require(len(suffixes) == 1 and re.fullmatch(r"[a-f0-9]{16}", suffixes[0]) is not None, + "actual selected runtime compiler fingerprint identity is missing") + arguments = msvc_native_libraries(text) + directory = target_dir / MSVC_TARGET / profile + archive = directory / "jett_runtime.lib" + require(archive.is_file() and archive.stat().st_size <= 256 * 1024 * 1024, + "private archive is missing or exceeds the harness limit") + emitted_path = archive + if build_cache is not None: + archive = snapshot_artifact(archive, output / "artifacts" / profile / "jett_runtime.lib") + archive_hash = digest(archive) + symbols = output / f"{profile}-symbols.log" + text = logged([str(tools.nm), "--defined-only", "--extern-only", "--format=posix", str(archive)], + repo, environment, symbols) + check_symbols(text, leaves, private) + check_struct_replace_symbols(text) + check_actor_replace_symbols(text) + coff = output / f"{profile}-archive-coff.log" + text = logged([str(tools.readobj), "--file-headers", "--coff-directives", str(archive)], + repo, environment, coff) + checked_coff_archive(text) + directive_count = len(re.findall(r"^Directive\(s\):", text, re.MULTILINE)) + fingerprints = msvc_fingerprint_records( + directory / ".fingerprint", + output / "fingerprints" / profile if build_cache is not None else None, + selected_filename="jett_runtime-" + suffixes[0]) + require(sources(repo) == inputs, "runtime inputs changed during archive measurement") + require(digest(archive) == archive_hash, "runtime archive changed during independent measurement") + checked_msvc_environment(environment, tools.linker) + build = output / f"{profile}-build.json" + write_json(build, {"profile": profile, "target": MSVC_TARGET, "toolchain": versions, + "runtime_sources": inputs, "workspace_lock": inputs["Cargo.lock"], + "cfgs_selected_package_only": list(CFGS), "actual_build_command": list(command), + "cargo_build_log": artifact(build_log), "archive": artifact(archive), + "actual_emitted_archive_path": str(emitted_path), + "actual_emitted_archive_sha256": archive_hash, + "build_environment": {name: environment[name] for name in ( + MSVC_FLAGS_ENV, "CARGO_INCREMENTAL", "CARGO_PROFILE_DEV_DEBUG", + "CARGO_PROFILE_RELEASE_DEBUG")}, + "measurement_script": artifact(Path(__file__))}) + libraries = output / f"{profile}-native-static-libs.json" + write_json(libraries, {"actual_print_log": artifact(build_log), "arguments": list(arguments), + "order_and_duplication_preserved": True}) + crt = output / f"{profile}-crt.json" + write_json(crt, {"actual_target_scoped_rustflags": list(MSVC_RUSTFLAGS), + "cargo_fingerprints": fingerprints, "native_static_libs": artifact(libraries), + "complete_symbol_inventory": artifact(symbols), + "complete_coff_archive_inventory": artifact(coff), + "coff_directive_record_count": directive_count, + "linker_path": str(tools.linker), "linker_executable": artifact(tools.linker), + "sdk_LIB": environment["LIB"], + "developer_shell": {name: environment[name] for name in ("VCINSTALLDIR", "VSCMD_ARG_TGT_ARCH")}, + "mode": "static"}) + return {"profile": profile, "target": MSVC_TARGET, "archive_path": str(archive), + "archive_sha256": archive_hash, "runtime_abi": 1, "layout_wire": CURRENT_LAYOUT_WIRE, + "private_cfgs": list(CFGS), "crt_mode": "static", "linker_path": str(tools.linker), + "native_library_args": list(arguments), "build_receipt": artifact(build), + "native_static_libs_receipt": artifact(libraries), "crt_receipt": artifact(crt)} + + +def prepare_msvc(repo: Path, output: Path, nm: str = "llvm-nm", readobj: str = "llvm-readobj", + linker: str = "link.exe", *, cache_root: Path | None = None, + build_target_dir: Path | None = None) -> tuple[Path, str]: + require(not output.exists(), "preserve immutable measurements: choose a fresh output directory") + require((cache_root is None) == (build_target_dir is None), + "explicit MSVC cache reuse requires both root and target directory") + cache = None + if cache_root is not None and build_target_dir is not None: + cache = bounded_msvc_build_cache(cache_root, build_target_dir, output) + checked_layout_wire(repo) + checked_struct_replace_leaf(repo) + checked_actor_replace_leaf(repo) + leaves, private = export_names(repo) + checked_measurement_source(repo) + environment = msvc_build_environment() + tools = MsvcTools(executable("cargo"), executable("rustc"), executable(nm), + executable(readobj), executable(linker)) + checked_msvc_environment(environment, tools.linker) + # Pin both compiler and linker; Developer Shell LIB remains byte-for-byte inherited. + environment["RUSTC"] = str(tools.rustc) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(tools.linker) + inputs = sources(repo) + checked_layout_wire(repo) + require(export_names(repo) == (leaves, private), "Resource export sources changed before measurement") + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed while establishing archive guards") + output.mkdir(parents=True) + versions = msvc_toolchain(tools, repo, output, environment) + archives: list[Json] = [] + for profile in ("debug", "release"): + archives.append(measure_msvc_profile(tools, repo, output, profile, environment, + inputs, versions, leaves, private, build_cache=cache)) + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed across matched archive profiles") + checked_struct_replace_leaf(repo) + checked_actor_replace_leaf(repo) + receipt = output / "receipt.json" + write_json(receipt, {"version": 1, "scope": "Measured compiler-test MSVC static CRT archives; not Source execution", + "archives": archives}) + return receipt, digest(receipt) + +def prepare(repo: Path, output: Path, cc: str, nm: str, readelf: str, *, + target: str = TARGET, readobj: str = "llvm-readobj", linker: str = "link.exe", + msvc_cache_root: Path | None = None, + msvc_build_target_dir: Path | None = None) -> tuple[Path, str]: + require(target in (TARGET, MSVC_TARGET), "unsupported archive measurement target") + if target == MSVC_TARGET: + return prepare_msvc(repo, output, nm, readobj, linker, cache_root=msvc_cache_root, + build_target_dir=msvc_build_target_dir) + require(msvc_cache_root is None and msvc_build_target_dir is None, + "explicit MSVC cache arguments cannot alter GNU measurement") + require(not output.exists(), "preserve immutable measurements: choose a fresh output directory") + checked_layout_wire(repo) + leaves, private = export_names(repo) + checked_measurement_source(repo) + environment = build_environment() + tools = Tools(executable("cargo"), executable("rustc"), executable(nm), executable(readelf), executable(cc)) + # Pin Cargo to the measured rustup proxy and disable config-provided wrappers. + environment["RUSTC"] = str(tools.rustc) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + inputs = sources(repo) + checked_layout_wire(repo) + require(export_names(repo) == (leaves, private), "Resource export sources changed before measurement") + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed while establishing archive guards") + output.mkdir(parents=True) + versions = toolchain(tools, repo, output, environment) + archives: list[Json] = [] + for profile in ("debug", "release"): + archives.append(measure_profile(tools, repo, output, profile, environment, inputs, + versions, leaves, private)) + checked_measurement_source(repo) + require(sources(repo) == inputs, "runtime inputs changed across matched archive profiles") + receipt = output / "receipt.json" + write_json(receipt, {"version": 1, "scope": "Measured compiler-test GNU archives; not Source execution", + "archives": archives}) + return receipt, digest(receipt) + + +def measured_artifact(record: Json) -> Path: + require(isinstance(record, dict), "measured artifact must be an object") + if not isinstance(record, dict): + raise MeasurementError("measured artifact must be an object") + path, expected = record.get("path"), record.get("sha256") + require(isinstance(path, str) and isinstance(expected, str), "artifact path/hash are required") + if not isinstance(path, str) or not isinstance(expected, str): + raise MeasurementError("artifact path/hash are required") + file = Path(path) + require(file.is_absolute() and file.is_file() and file.stat().st_size <= 256 * 1024 * 1024 + and digest(file) == expected, + f"measured artifact changed: {file}") + return file + + +def verify_embedded_artifacts(value: Json) -> None: + if isinstance(value, dict): + if "path" in value and "sha256" in value: + measured_artifact(value) + raw = value.get("raw_json_utf8") + if isinstance(raw, str): + require(hashlib.sha256(raw.encode("utf-8")).hexdigest() == value["sha256"], + "retained Cargo fingerprint bytes differ from their observation") + for item in value.values(): + verify_embedded_artifacts(item) + elif isinstance(value, list): + for item in value: + verify_embedded_artifacts(item) + + +def checked_msvc_receipt(row: dict[str, Json], build: dict[str, Json], + libraries: dict[str, Json], crt: dict[str, Json], repo: Path) -> None: + require(build.get("target") == MSVC_TARGET and build.get("profile") == row.get("profile"), + "MSVC build provenance target/profile differs from archive row") + require(build.get("archive") == {"path": row.get("archive_path"), "sha256": row.get("archive_sha256")}, + "MSVC build provenance must describe the exact linked runtime archive") + require(build.get("cfgs_selected_package_only") == CFGS, + "MSVC build provenance must preserve the selected-package private cfgs") + require(crt.get("actual_target_scoped_rustflags") == list(MSVC_RUSTFLAGS), + "MSVC CRT receipt must preserve the exact target flags") + require(build.get("build_environment") == { + MSVC_FLAGS_ENV: " ".join(MSVC_RUSTFLAGS), "CARGO_INCREMENTAL": "0", + "CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_PROFILE_RELEASE_DEBUG": "0"}, + "MSVC build must disable incremental compilation and debug information") + path = row.get("linker_path") + require(isinstance(path, str), "measured MSVC linker path is missing") + if not isinstance(path, str): + raise MeasurementError("measured MSVC linker path is missing") + checked_msvc_environment(os.environ, Path(path)) + require(crt.get("sdk_LIB") == os.environ.get("LIB"), + "SDK LIB differs from the inherited measured Developer Shell environment") + require(crt.get("developer_shell") == { + name: os.environ.get(name) for name in ("VCINSTALLDIR", "VSCMD_ARG_TGT_ARCH")}, + "Developer Shell identity differs from measured environment") + toolchain_value = build.get("toolchain") + require(isinstance(toolchain_value, dict), "MSVC measured toolchain is missing") + if not isinstance(toolchain_value, dict): + raise MeasurementError("MSVC measured toolchain is missing") + compiler_record = toolchain_value.get("rustc") + require(isinstance(compiler_record, dict), "MSVC measured compiler is missing") + if not isinstance(compiler_record, dict): + raise MeasurementError("MSVC measured compiler is missing") + compiler_path = compiler_record.get("invocation_path") + require(isinstance(compiler_path, str), "MSVC compiler invocation path is missing") + if not isinstance(compiler_path, str): + raise MeasurementError("MSVC compiler invocation path is missing") + compiler = measured_artifact(compiler_record.get("executable")) + require(Path(compiler_path).absolute() == compiler.absolute(), "measured compiler invocation identity differs") + build_log = measured_artifact(build.get("cargo_build_log")) + text = build_log.read_text(encoding="utf-8") + msvc_selected_cfgs(text, Path(compiler_path)) + checked_msvc_linker_commands(text, Path(compiler_path), Path(path)) + require(libraries.get("actual_print_log") == build.get("cargo_build_log"), + "native library observation must be from the measured runtime build") + observed_libraries = msvc_native_libraries(measured_artifact( + libraries.get("actual_print_log")).read_text(encoding="utf-8")) + require(libraries.get("arguments") == observed_libraries + and row.get("native_library_args") == observed_libraries, + "MSVC native libraries differ from the actual ordered rustc observation") + require(crt.get("native_static_libs") == row.get("native_static_libs_receipt"), + "CRT provenance refers to a different native library measurement") + fingerprints = crt.get("cargo_fingerprints") + require(isinstance(fingerprints, list) and bool(fingerprints), "MSVC Cargo fingerprint provenance is missing") + if not isinstance(fingerprints, list): + raise MeasurementError("MSVC Cargo fingerprint provenance is missing") + for record in fingerprints: + fingerprint = read_object(measured_artifact(record)) + require(fingerprint.get("rustflags") == list(MSVC_RUSTFLAGS), + "MSVC fingerprint contradicts the static CRT flags") + coff = measured_artifact(crt.get("complete_coff_archive_inventory")) + coff_text = coff.read_text(encoding="utf-8") + checked_coff_archive(coff_text) + directive_count = crt.get("coff_directive_record_count") + require(isinstance(directive_count, int) and not isinstance(directive_count, bool) + and directive_count == len(re.findall(r"^Directive\(s\):", coff_text, re.MULTILINE)), + "CRT receipt directive count differs from the complete COFF observation") + symbols = measured_artifact(crt.get("complete_symbol_inventory")) + leaves, private = export_names(repo) + check_symbols(symbols.read_text(encoding="utf-8"), leaves, private) + checked_struct_replace_leaf(repo) + checked_actor_replace_leaf(repo) + check_struct_replace_symbols(symbols.read_text(encoding="utf-8")) + check_actor_replace_symbols(symbols.read_text(encoding="utf-8")) + +def receipt_sources(receipt: Path, expected: str, repo: Path, *, target: str = TARGET) -> None: + require(target in (TARGET, MSVC_TARGET), "unsupported archive receipt target") + mode = "static" if target == MSVC_TARGET else "dynamic" + require(re.fullmatch(r"[0-9a-f]{64}", expected) is not None, "expected SHA-256 must be canonical") + require(receipt.is_file() and receipt.stat().st_size <= 1024 * 1024, + "receipt exceeds the test harness limit") + require(digest(receipt) == expected, "receipt differs from the independently supplied compile-time pin") + checked_layout_wire(repo) + export_names(repo) + checked_measurement_source(repo) + record = read_object(receipt) + require(record.get("version") == 1, "unsupported archive receipt version") + archives = record.get("archives") + require(isinstance(archives, list) and len(archives) == 2, "two measured runtime profiles are required") + if not isinstance(archives, list): + raise MeasurementError("archive rows are required") + profiles: set[str] = set() + for row in archives: + require(isinstance(row, dict), "archive row must be an object") + if not isinstance(row, dict): + raise MeasurementError("archive row must be an object") + profile = row.get("profile") + require(isinstance(profile, str) and profile in ("debug", "release"), "unknown runtime profile") + if isinstance(profile, str): + profiles.add(profile) + require(row.get("target") == target and row.get("runtime_abi") == 1 + and row.get("layout_wire") == CURRENT_LAYOUT_WIRE and row.get("private_cfgs") == CFGS + and row.get("crt_mode") == mode, "archive contract differs from measured gate") + measured_artifact({"path": row.get("archive_path"), "sha256": row.get("archive_sha256")}) + for name in ("build_receipt", "native_static_libs_receipt", "crt_receipt"): + provenance = read_object(measured_artifact(row.get(name))) + verify_embedded_artifacts(provenance) + build = read_object(measured_artifact(row.get("build_receipt"))) + require(build.get("runtime_sources") == sources(repo), "measured runtime source differs from current tree") + require(digest(measured_artifact(build.get("measurement_script"))) == digest(Path(__file__)), + "measured helper differs from the executing helper") + libraries = read_object(measured_artifact(row.get("native_static_libs_receipt"))) + require(row.get("native_library_args") == libraries.get("arguments"), "native library vector changed") + crt = read_object(measured_artifact(row.get("crt_receipt"))) + require(crt.get("mode") == mode and crt.get("linker_path") == row.get("linker_path"), + "linker/CRT identity differs from receipt") + linker = measured_artifact(crt.get("linker_executable")) + path = row.get("linker_path") + require(isinstance(path, str), "measured linker path is missing") + if isinstance(path, str): + require(Path(path).resolve(strict=True) == linker.resolve(strict=True), "measured linker changed") + if target == MSVC_TARGET: + checked_msvc_receipt(row, build, libraries, crt, repo) + require(profiles == {"debug", "release"}, "measured profiles must be unique") + + +def compiled_test(messages: str) -> Path: + executables: set[Path] = set() + for line in messages.splitlines(): + value = normalize_json(json.loads(line)) + if not isinstance(value, dict) or value.get("reason") != "compiler-artifact": + continue + target = value.get("target") + executable_path = value.get("executable") + if isinstance(target, dict) and target.get("name") == "native_conformance" and isinstance(executable_path, str): + executables.add(Path(executable_path)) + require(len(executables) == 1, "one compiled native_conformance executable must be selected") + executable_path = next(iter(executables)) + require(executable_path.is_absolute() and executable_path.is_file(), "compiled gate executable is missing") + return executable_path + + +def checked_msvc_gate_artifact(messages: str, binary: Path, expected: str) -> Path: + matches: list[dict[str, Json]] = [] + for line in messages.splitlines(): + value = normalize_json(json.loads(line)) + if not isinstance(value, dict) or value.get("reason") != "compiler-artifact": + continue + target = value.get("target") + if isinstance(target, dict) and target.get("name") == "native_conformance": + matches.append(value) + require(len(matches) == 1 and matches[0].get("fresh") is False + and matches[0].get("executable") == str(binary), + "MSVC gate artifact must be freshly compiled with the independently supplied pin") + dep_info = binary.with_suffix(".d") + require(dep_info.is_file(), "fresh MSVC gate compiler dep-info is missing") + pins = re.findall(r"^# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1=(.*)$", + dep_info.read_text(encoding="utf-8"), re.MULTILINE) + require(pins == [expected], "MSVC gate dep-info does not prove the exact compile-time receipt pin") + return dep_info + + +def run_msvc_gate(repo: Path, receipt: Path, expected: str, target_dir: Path, *, + suite: str = "original", reuse_target_dir: bool = False) -> None: + require(suite in ("original", "occupied-structs"), "unsupported Resource gate suite") + require(reuse_target_dir or not target_dir.exists(), + "existing MSVC gate targets require explicit --reuse-target-dir") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + environment = msvc_build_environment() + compiler = executable("rustc") + linker = executable("link.exe") + checked_msvc_environment(environment, linker) + environment["RUSTC"] = str(compiler) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(linker) + environment["CARGO_PROFILE_TEST_DEBUG"] = "0" + inputs = sources(repo, complete=True) + output = receipt.parent + compiler_log = output / "gate-rustc-version.log" + version = logged([str(compiler), "-vV"], repo, environment, compiler_log) + require(f"host: {MSVC_TARGET}" in version.splitlines(), "Source gate must compile on its measured MSVC host") + environment[PIN_ENV] = expected + command = [str(executable("cargo")), "test", "--offline", "--locked", "--color", "never", "-p", "jett_driver", + "--test", "native_conformance", "--target", MSVC_TARGET, "--target-dir", str(target_dir), + "--verbose", "--no-run", "--message-format=json"] + messages = output / "gate-compile-messages.jsonl" + errors = output / "gate-compile-stderr.log" + with messages.open("xb") as stdout, errors.open("xb") as stderr: + result = subprocess.run(command, cwd=repo, env=environment, stdin=subprocess.DEVNULL, + stdout=stdout, stderr=stderr, timeout=3600) + require(result.returncode == 0, f"gate compilation failed; see {errors}") + actual_commands = msvc_compiler_commands(errors.read_text(encoding="utf-8"), compiler) + gates = [arguments for arguments in actual_commands + if option_values(arguments, "--crate-name") == ["native_conformance"]] + require(len(gates) == 1 and "--test" in gates[0] + and option_values(gates[0], "--target") == [MSVC_TARGET], + "MSVC gate must have an actual native_conformance test compiler invocation") + checked_msvc_flags(gates[0]) + checked_msvc_linker_commands(errors.read_text(encoding="utf-8"), compiler, linker) + message_text = messages.read_text(encoding="utf-8") + binary = compiled_test(message_text) + dep_info = checked_msvc_gate_artifact(message_text, binary, expected) + binary_hash, dep_info_hash = digest(binary), digest(dep_info) + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate compilation") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + environment.pop(PIN_ENV) + environment[RECEIPT_ENV] = str(receipt) + tests = OCCUPIED_STRUCT_TESTS if suite == "occupied-structs" else (TEST,) + observations: list[Json] = [] + counts: dict[str, Json] = {} + for index, test in enumerate(tests, start=1): + log = output / f"gate-execution-{index}.log" + text = logged([str(binary), test, "--ignored", "--exact", "--test-threads=1", "--nocapture"], + repo, environment, log, timeout=3600) + require(re.search(r"^running 1 test$", text, re.MULTILINE) is not None + and "1 passed; 0 failed; 0 ignored;" in text, + f"exactly one ignored Resource test must execute and pass: {test}") + require(digest(binary) == binary_hash and digest(dep_info) == dep_info_hash, + "compiled MSVC gate or pin dep-info changed during execution") + observations.append({"test": test, "execution": artifact(log)}) + if suite == "original": + summary = re.findall(r"Resource native acceptance: (\d+) cases; (\d+) Source-deleted executions; profiles=debug,release", text) + require(len(summary) == 1, "complete Source execution summary is missing") + count, executions = (int(value) for value in summary[0]) + require(count > 0 and executions == 2 * count, "both archive profiles must execute the complete corpus") + counts = {"cases": count, "source_deleted_executions": executions} + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate execution") + receipt_sources(receipt, expected, repo, target=MSVC_TARGET) + write_json(output / "gate-acceptance.json", { + "scope": ("Occupied struct retirement and clean reentry Source gates on private MSVC archives" + if suite == "occupied-structs" else "Complete shared Source corpus on private MSVC archives"), + "target": MSVC_TARGET, "suite": suite, "profiles": ["debug", "release"], **counts, + "compile_time_receipt_sha256": expected, "receipt": artifact(receipt), + "compiler_and_source_inputs": inputs, "compiler_executable": artifact(compiler), + "compiler_version": artifact(compiler_log), "compile_command": list(command), + "compile_messages": artifact(messages), "compile_stderr": artifact(errors), + "compiled_gate": artifact(binary), "compiler_dep_info": artifact(dep_info), + "gate_executions": observations, "runtime_expected_pin_environment": False}) + +def run_gate(repo: Path, receipt: Path, expected: str, target_dir: Path, *, target: str = TARGET, + suite: str = "original", reuse_target_dir: bool = False) -> None: + require(target in (TARGET, MSVC_TARGET), "unsupported Resource gate target") + if target == MSVC_TARGET: + run_msvc_gate(repo, receipt, expected, target_dir, suite=suite, reuse_target_dir=reuse_target_dir) + return + require(suite == "original" and not reuse_target_dir, "GNU gate requires the original suite and a fresh target") + require(not target_dir.exists(), "compile the receipt pin in a fresh gate target directory") + receipt_sources(receipt, expected, repo) + environment = build_environment() + compiler = executable("rustc") + environment["RUSTC"] = str(compiler) + environment["RUSTC_WRAPPER"] = "" + environment["RUSTC_WORKSPACE_WRAPPER"] = "" + inputs = sources(repo, complete=True) + output = receipt.parent + compiler_log = output / "gate-rustc-version.log" + version = logged([str(compiler), "-vV"], repo, environment, compiler_log) + require(f"host: {TARGET}" in version.splitlines(), "Source gate must compile on its measured GNU host") + environment[PIN_ENV] = expected + command = [str(executable("cargo")), "test", "--locked", "--color", "never", "-p", "jett_driver", "--test", "native_conformance", + "--target", TARGET, "--target-dir", str(target_dir), "--verbose", "--no-run", "--message-format=json"] + messages = output / "gate-compile-messages.jsonl" + errors = output / "gate-compile-stderr.log" + with messages.open("xb") as stdout, errors.open("xb") as stderr: + result = subprocess.run(command, cwd=repo, env=environment, stdin=subprocess.DEVNULL, + stdout=stdout, stderr=stderr, timeout=3600) + require(result.returncode == 0, f"gate compilation failed; see {errors}") + checked_compiler_commands(errors.read_text(encoding="utf-8"), compiler) + binary = compiled_test(messages.read_text(encoding="utf-8")) + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate compilation") + receipt_sources(receipt, expected, repo) + environment.pop(PIN_ENV) + environment[RECEIPT_ENV] = str(receipt) + gate_log = output / "gate-execution.log" + text = logged([str(binary), TEST, "--ignored", "--exact", "--test-threads=1", "--nocapture"], + repo, environment, gate_log, timeout=3600) + require("1 passed; 0 failed; 0 ignored;" in text, "the complete ignored Resource gate did not execute") + summary = re.findall(r"Resource native acceptance: (\d+) cases; (\d+) Source-deleted executions; profiles=debug,release", text) + require(len(summary) == 1, "complete Source execution summary is missing") + count, executions = (int(value) for value in summary[0]) + require(count > 0 and executions == 2 * count, "both archive profiles must execute the complete corpus") + require(sources(repo, complete=True) == inputs, "compiler/Source inputs changed during gate execution") + receipt_sources(receipt, expected, repo) + write_json(output / "gate-acceptance.json", {"scope": "Complete shared Source corpus on private GNU archives", + "target": TARGET, "cases": count, "source_deleted_executions": executions, + "profiles": ["debug", "release"], "compile_time_receipt_sha256": expected, + "receipt": artifact(receipt), "compiler_and_source_inputs": inputs, + "compiler_executable": artifact(compiler), "compiler_version": artifact(compiler_log), + "compile_command": list(command), "compile_messages": artifact(messages), + "compile_stderr": artifact(errors), "compiled_gate": artifact(binary), + "gate_execution": artifact(gate_log), "runtime_expected_pin_environment": False}) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + preparation = commands.add_parser("prepare", help="build and independently measure both private archives") + preparation.add_argument("--repo", type=Path, default=ROOT) + preparation.add_argument("--output", type=Path, required=True) + preparation.add_argument("--cc", default=os.environ.get("JETT_NATIVE_CC", "cc")) + preparation.add_argument("--target", choices=(TARGET, MSVC_TARGET), default=TARGET) + preparation.add_argument("--nm") + preparation.add_argument("--readobj", default="llvm-readobj") + preparation.add_argument("--linker", default="link.exe") + preparation.add_argument("--msvc-cache-root", type=Path, + help="explicit existing root bounding a reusable MSVC Cargo cache") + preparation.add_argument("--msvc-build-target-dir", type=Path, + help="MSVC only: one bounded reusable cache; linked evidence is snapshotted") + preparation.add_argument("--readelf", default="readelf") + preparation.add_argument("--github-output", type=Path) + gate = commands.add_parser("run", help="compile the independently supplied receipt pin and run Source gate") + gate.add_argument("--repo", type=Path, default=ROOT) + gate.add_argument("--receipt", type=Path, required=True) + gate.add_argument("--expected-sha256", required=True) + gate.add_argument("--target-dir", type=Path, required=True) + gate.add_argument("--target", choices=(TARGET, MSVC_TARGET), default=TARGET) + gate.add_argument("--suite", choices=("original", "occupied-structs"), default="original") + gate.add_argument("--reuse-target-dir", action="store_true", + help="MSVC only: reuse a gate target while requiring fresh compilation and dep-info pin evidence") + arguments = parser.parse_args() + if arguments.command == "prepare": + nm = arguments.nm or ("llvm-nm" if arguments.target == MSVC_TARGET else "nm") + measurement_output = (arguments.output if arguments.target == MSVC_TARGET + and arguments.msvc_cache_root is not None else arguments.output.resolve()) + receipt, expected = prepare(arguments.repo.resolve(), measurement_output, arguments.cc, + nm, arguments.readelf, target=arguments.target, + readobj=arguments.readobj, linker=arguments.linker, + msvc_cache_root=arguments.msvc_cache_root, + msvc_build_target_dir=arguments.msvc_build_target_dir) + if arguments.github_output is not None: + require("\n" not in str(receipt) and "\r" not in str(receipt), "receipt path cannot contain a newline") + with arguments.github_output.open("a", encoding="utf-8", newline="\n") as output: + output.write(f"receipt={receipt}\nsha256={expected}\n") + print(json.dumps({"receipt": str(receipt), "sha256": expected, "native_resource_source_execution": False})) + else: + run_gate(arguments.repo.resolve(), arguments.receipt.resolve(), arguments.expected_sha256, + arguments.target_dir.resolve(), target=arguments.target, suite=arguments.suite, + reuse_target_dir=arguments.reuse_target_dir) + + +if __name__ == "__main__": + main() diff --git a/tools/package_native.py b/tools/package_native.py new file mode 100644 index 00000000..aa7e4a65 --- /dev/null +++ b/tools/package_native.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Build a relocatable host-native compiler package with both runtime profiles. + +Usage: python3 tools/package_native.py --output target/package [--compiler-profile debug] +Requires the host Rust toolchain and C linker/SDK. It never cross-compiles. +""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def run(*args: str, env: dict[str, str], capture: bool = False) -> str: + result = subprocess.run(args, cwd=ROOT, env=env, check=True, text=True, + stdout=subprocess.PIPE if capture else None) + return result.stdout if capture else "" + + +def copy_notices(package: Path, metadata: dict, env: dict[str, str]) -> None: + notices = package / "licenses" + notices.mkdir() + entries = [] + for dependency in sorted(metadata["packages"], key=lambda item: (item["name"], item["version"])): + source = Path(dependency["manifest_path"]).parent + files = set(source.glob("LICENSE*")) | set(source.glob("COPYING*")) | set(source.glob("NOTICE*")) + if dependency.get("license_file"): + files.add(source / dependency["license_file"]) + destination = notices / f'{dependency["name"]}-{dependency["version"]}' + for file in sorted(files): + if file.is_file(): + destination.mkdir(exist_ok=True) + shutil.copy2(file, destination / file.name) + entries.append({key: dependency.get(key) for key in ("name", "version", "license", "repository")}) + (notices / "packages.json").write_text(json.dumps(entries, indent=2) + "\n", encoding="utf-8") + rust_docs = Path(run("rustc", "--print", "sysroot", env=env, capture=True).strip()) / "share/doc/rust" + rust_notices = notices / "rust" + rust_notices.mkdir() + for pattern in ("LICENSE*", "COPYRIGHT*"): + for file in rust_docs.glob(pattern): + if file.is_file(): + shutil.copy2(file, rust_notices / file.name) + shutil.copy2(ROOT / "Cargo.lock", package / "Cargo.lock") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--compiler-profile", choices=("debug", "release"), default="release") + args = parser.parse_args() + output = args.output.resolve() + if output.exists(): + parser.error(f"output already exists: {output}; choose a new package directory") + env = os.environ.copy() + host = next(line.removeprefix("host: ") for line in + run("rustc", "-vV", env=env, capture=True).splitlines() if line.startswith("host: ")) + windows = host == "x86_64-pc-windows-msvc" + if not windows and host != "x86_64-unknown-linux-gnu": + parser.error(f"unsupported native packaging host: {host}") + if windows: + # Explicit --target applies these flags to target artifacts, leaving + # host procedural macros and build scripts with their ordinary ABI. + flags = env.get("CARGO_ENCODED_RUSTFLAGS") + if flags is not None: + env["CARGO_ENCODED_RUSTFLAGS"] = flags + "\x1f-Ctarget-feature=+crt-static" + else: + env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -Ctarget-feature=+crt-static" + metadata = json.loads(run("cargo", "metadata", "--locked", "--format-version=1", env=env, capture=True)) + target = Path(metadata["target_directory"]) / host + compiler_flags = ["--release"] if args.compiler_profile == "release" else [] + run("cargo", "build", "--locked", "-p", "jett_cli", "--target", host, *compiler_flags, env=env) + for profile in ("debug", "release"): + flags = ["--release"] if profile == "release" else [] + run("cargo", "build", "--locked", "-p", "jett_native_launcher", "--target", host, *flags, env=env) + output.parent.mkdir(parents=True, exist_ok=True) + # Publish only after all compiler/runtime files and notices are present. + with tempfile.TemporaryDirectory(prefix="jett-package-", dir=output.parent) as staging: + package = Path(staging) / "package" + binary_dir = package / "bin" + binary_dir.mkdir(parents=True) + binary_name = "jett.exe" if windows else "jett" + shutil.copy2(target / args.compiler_profile / binary_name, binary_dir / binary_name) + shutil.copytree(ROOT / "stdlib", package / "lib/jett/stdlib") + archive_name = "jett_native_launcher.lib" if windows else "libjett_native_launcher.a" + for profile in ("debug", "release"): + runtime = package / "lib/jett/runtime" / host / profile + runtime.mkdir(parents=True) + archive = runtime / archive_name + shutil.copy2(target / profile / archive_name, archive) + run("cargo", "run", "--locked", "--quiet", "-p", "jett_driver", "--example", + "native_bundle_manifest", "--target", host, "--", str(archive), profile, env=env) + (package / "tools").mkdir() + shutil.copy2(ROOT / "tools/smoke_native_package.py", package / "tools/smoke_native_package.py") + copy_notices(package, metadata, env) + (package / "README.txt").write_text( + f"Jett native compiler for {host}\n\n" + "Run bin/jett build source.jett, optionally with --release or -o output.\n" + "Build requires the host C linker and SDK; the compiler never invokes Cargo.\n" + "Run bin/jett build --check source.jett for frontend validation only.\n" + "Keep bin and lib together when moving this package.\n" + "Runtime manifests declare the required target, profile, CRT, ABI, and system libraries.\n" + "Licenses and dependency declarations are collected under licenses/.\n", + encoding="utf-8") + package.rename(output) + print(f"Packaged native compiler: {output}") + + +if __name__ == "__main__": + main() diff --git a/tools/smoke_native_package.py b/tools/smoke_native_package.py new file mode 100644 index 00000000..145c9e2a --- /dev/null +++ b/tools/smoke_native_package.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Check a relocated package without Cargo or the Jett source tree.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import subprocess +import tempfile + + +def command(args: list[str | Path], cwd: Path, env: dict[str, str], success: bool = True) -> subprocess.CompletedProcess: + result = subprocess.run([str(arg) for arg in args], cwd=cwd, env=env, capture_output=True, text=True, timeout=120) + if (result.returncode == 0) != success: + raise AssertionError(f"{args}: exit {result.returncode}\n{result.stdout}\n{result.stderr}") + return result + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("package", type=Path) + args = parser.parse_args() + package = args.package.resolve() + compiler = package / "bin" / ("jett.exe" if os.name == "nt" else "jett") + env = os.environ.copy() + env.pop("JETT_STDLIB_DIR", None) + env.pop("JETT_NATIVE_RUNTIME_BUNDLE", None) + # This added namespace proves installed-stdlib discovery won over any + # development checkout that happens to remain on the packaging machine. + probe = package / "lib/jett/stdlib/package_smoke_probe.jett" + if probe.exists(): + raise RuntimeError(f"unexpected existing smoke probe: {probe}") + probe.write_text('namespace package_smoke_probe\nexport function message() returns string:\n return "installed stdlib"\n', encoding="utf-8") + try: + with tempfile.TemporaryDirectory(prefix="jett-package-smoke-") as temporary: + work = Path(temporary) + source = work / "hello.jett" + source_text = ('function main(stdout: Stdout) returns nothing:\n' + ' int64 unused = 42\n' + ' string marker = package_smoke_probe.message()\n' + ' trace marker\n' + ' Stdout.write(view stdout, "{marker}\\n")\n') + for release in (False, True): + source.write_text(source_text, encoding="utf-8") + binary = work / ("release.exe" if release else "debug.exe") + build = [compiler, "build", source, "-o", binary] + if release: + build.append("--release") + compiled = command(build, work, env) + assert "warning[E0202]" in compiled.stderr, compiled + source.unlink() + result = command([binary], work, env) + assert result.stdout == "installed stdlib\n", result + expected_debug = "" if release else "trace marker: string = installed stdlib\n" + assert result.stderr == expected_debug, result + # Failed builds preserve an existing output and expose diagnostics. + source.write_text("function main() returns nothing:\n println(1)\n", encoding="utf-8") + existing = work / "preserved.exe" + existing.write_bytes(b"existing artifact") + failed = command([compiler, "build", source, "--release", "-o", existing], work, env, False) + assert "E0362" in failed.stderr, failed + assert existing.read_bytes() == b"existing artifact" + source.write_text(source_text, encoding="utf-8") + manifests = sorted((package / "lib/jett/runtime").glob("*/debug/launcher.json")) + assert len(manifests) == 1, manifests + manifest = manifests[0] + metadata = json.loads(manifest.read_text(encoding="utf-8")) + # Explicit mismatched runtime profiles must fail before publication. + failed = command([compiler, "build", source, "--release", "--runtime-bundle", manifest, "-o", existing], work, env, False) + assert "profile mismatch" in failed.stderr, failed + assert existing.read_bytes() == b"existing artifact" + # Agent mode identifies the artifact, and default paths separate profiles. + result = command([compiler, "build", source, "--agent"], work, env) + assert "status: ok" in result.stdout and "artifact:" in result.stdout, result + assert "E0202" in result.stdout and "unused" in result.stdout, result + name = "hello.exe" if os.name == "nt" else "hello" + assert (work / "target" / metadata["target"] / "debug" / name).is_file() + command([compiler, "build", source, "--check"], work, env) + failed = command([compiler, "build", "absent.jett", "--target", "wasm32-unknown-unknown"], work, env, False) + assert "unsupported native target" in failed.stderr, failed + finally: + probe.unlink() + print("Native package smoke checks passed (debug, release, relocation, diagnostics, runtime profile, and atomic output).") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_jett_bench_grading.py b/tools/tests/test_jett_bench_grading.py index 262482b8..902b9b42 100644 --- a/tools/tests/test_jett_bench_grading.py +++ b/tools/tests/test_jett_bench_grading.py @@ -15,7 +15,7 @@ def assess(self, first_error: str = "", test_error: str = "") -> tuple[dict, lis (directory / "hidden.jett").write_text("PRIVATE FIXTURE", encoding="utf-8") task = {"id": "phase_check", "adapters": {"jett": { "candidate": "solution.jett", "hidden": "hidden.jett", "grade_mode": "append", - "commands": [["{jett}", "build", "{candidate}"], ["{jett}", "test", "{candidate}"]], + "commands": [["{jett}", "build", "--check", "{candidate}"], ["{jett}", "test", "{candidate}"]], }}} def execute(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: diff --git a/tools/tests/test_measure_resource_native_archives.py b/tools/tests/test_measure_resource_native_archives.py new file mode 100644 index 00000000..f639a570 --- /dev/null +++ b/tools/tests/test_measure_resource_native_archives.py @@ -0,0 +1,2135 @@ +"""Evidence controls for private GNU/MSVC archives and compiled pin handoff.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import tempfile +from typing import BinaryIO +import unittest +from unittest.mock import patch + +from tools import measure_resource_native_archives as measure + + +def copy_resource_export_fixture(root: Path) -> tuple[Path, Path]: + resource = root / "crates/jett_runtime/src/native_abi/resource" + (resource / "leaves").mkdir(parents=True) + for relative in ("leaves.rs", "test_archive_api.rs", "program_entry.rs", "graph_descriptors.rs"): + original = measure.ROOT / "crates/jett_runtime/src/native_abi/resource" / relative + (resource / relative).write_bytes(original.read_bytes()) + owner = resource.with_suffix(".rs") + owner.write_bytes((measure.ROOT / "crates/jett_runtime/src/native_abi/resource.rs").read_bytes()) + carriers = resource / "leaves/carriers.rs" + declarations = [f'pub unsafe extern "C" fn {name}() {{}}' + for name in sorted(measure.CARRIER_EXPORTS - measure.SCALAR_TASK_EXPORTS)] + for name, types in measure.SCALAR_TASK_SIGNATURES.items(): + parameters = "\n".join(f" argument_{index}: {kind}," + for index, kind in enumerate(types)) + declarations.append(f'#[unsafe(no_mangle)]\npub unsafe extern "C" fn {name}(\n' + f"{parameters}\n) -> JettRuntimeStatusV1 {{}}") + carriers.write_text("\n".join(declarations), encoding="utf-8") + return owner, resource / "program_entry.rs" + + +class NativeLibraryTests(unittest.TestCase): + def test_actual_order_and_duplicate_arguments_are_retained(self) -> None: + text = "note: native-static-libs: -lgcc_s -lc -lm -lc\n -lpthread\nFinished build\n" + self.assertEqual(measure.native_libraries(text), + ["-lgcc_s", "-lc", "-lm", "-lc", "-lpthread"]) + + def test_missing_multiple_or_unsafe_observations_are_refused(self) -> None: + values = [ + "", "note: native-static-libs: -lc\n", + "note: native-static-libs: -lgcc_s -lc\nnote: native-static-libs: -lc\n", + "note: native-static-libs: -lgcc_s -lc -ljett_runtime\n", + "note: native-static-libs: -lgcc_s -lc -ljett_native_launcher\n", + "note: native-static-libs: -lgcc_s -lc -static\n", + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.native_libraries(value) + + def test_private_cfgs_belong_to_one_selected_runtime_command(self) -> None: + runtime = ("Running `rustc --crate-name jett_runtime --crate-type rlib " + "--crate-type staticlib --cfg test --cfg jett_resource_native_test_archive " + "--print=native-static-libs`") + measure.selected_cfgs(runtime) + values = [ + runtime.replace("jett_runtime ", "dependency "), + runtime + "\n" + runtime, + runtime.replace("--cfg test ", ""), + runtime.replace("--crate-type staticlib ", ""), + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.selected_cfgs(value) + + def test_executed_compiler_identity_rejects_wrappers_and_other_compilers(self) -> None: + compiler = Path("/measured/rustc").absolute() + command = f"Running `{str(compiler)!r} --crate-name jett_runtime --crate-type staticlib`" + measure.checked_compiler_commands(command, compiler) + for invalid in ("", "Running `\"/foreign/rustc\" --crate-name jett_runtime`", + f"Running `wrapper {str(compiler)!r} --crate-name jett_runtime`"): + with self.subTest(command=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_compiler_commands(invalid, compiler) + + def test_rustup_proxy_invocation_keeps_its_original_name(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + proxy = root / "cargo" + proxy.write_bytes(b"proxy") + with patch.object(measure.shutil, "which", return_value=str(proxy)), \ + patch.object(Path, "resolve", return_value=root / "rustup") as resolve: + self.assertEqual(measure.executable("cargo"), proxy) + resolve.assert_not_called() + + def test_cargo_color_preferences_cannot_change_measured_output(self) -> None: + with patch.dict(os.environ, {"CARGO_TERM_COLOR": "always"}, clear=True): + self.assertEqual(measure.build_environment()["CARGO_TERM_COLOR"], "never") + + def test_global_cfgs_and_wrappers_are_refused(self) -> None: + for name, value in [ + ("RUSTFLAGS", "--cfg test"), + ("CARGO_ENCODED_RUSTFLAGS", "--cfg\x1ftest"), + ("CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS", "-Ctarget-feature=+crt-static"), + ("RUSTC_WRAPPER", "unmeasured-rustc"), + ("RUSTC", "unmeasured-rustc"), + ("CARGO_BUILD_RUSTC", "unmeasured-rustc"), + ]: + with self.subTest(name=name), patch.dict(os.environ, {name: value}, clear=True): + with self.assertRaises(measure.MeasurementError): + measure.build_environment() + with patch.dict(os.environ, {measure.PIN_ENV: "old", measure.RECEIPT_ENV: "old"}, clear=True): + environment = measure.build_environment() + self.assertNotIn(measure.PIN_ENV, environment) + self.assertNotIn(measure.RECEIPT_ENV, environment) + + +class SymbolAndFingerprintTests(unittest.TestCase): + def setUp(self) -> None: + self.leaves, self.private = measure.export_names(measure.ROOT) + self.symbols = "archive[member.o]:\n" + "\n".join( + f"{name} T 0 10" for name in sorted(self.leaves | self.private | {"main"})) + + def test_complete_current_source_export_contract(self) -> None: + self.assertEqual((len(self.leaves), len(self.private)), (58, 8)) + self.assertTrue(measure.CARRIER_EXPORTS <= self.leaves) + self.assertTrue(measure.STARTUP_EXPORTS <= self.leaves) + self.assertEqual(len(self.leaves - measure.STARTUP_EXPORTS), 53) + self.assertEqual(len(self.leaves - measure.STARTUP_EXPORTS - {measure.GRAPH_DESCRIPTOR_EXPORT} + - measure.SCALAR_TASK_EXPORTS), 50) + measure.check_symbols(self.symbols, self.leaves, self.private) + + def test_each_startup_symbol_must_have_one_text_definition(self) -> None: + for name in sorted(measure.STARTUP_EXPORTS): + line = f"{name} T 0 10" + for invalid in (self.symbols.replace(line, ""), self.symbols + "\n" + line, + self.symbols.replace(line, f"{name} D 0 10")): + with self.subTest(name=name, symbols=invalid), self.assertRaises(measure.MeasurementError): + measure.check_symbols(invalid, self.leaves, self.private) + + def test_missing_duplicate_foreign_or_nontext_exports_are_refused(self) -> None: + leaf = sorted(self.leaves)[0] + private = sorted(self.private)[0] + values = [ + self.symbols.replace(f"{leaf} T 0 10", ""), + self.symbols + "\nmain T 0 10", + self.symbols + f"\n{private} T 0 10", + self.symbols + "\njett_rt_v1_resource_foreign T 0 10", + self.symbols + "\njett_rt_v1_resource_test_foreign T 0 10", + self.symbols.replace(f"{leaf} T", f"{leaf} W"), + ] + for value in values: + with self.subTest(value=value), self.assertRaises(measure.MeasurementError): + measure.check_symbols(value, self.leaves, self.private) + + def test_fingerprints_require_exact_gnu_flags_and_existing_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.fingerprint_records(root) + directory = root / "jett_runtime-hash" + directory.mkdir() + record = directory / "lib-jett_runtime.json" + record.write_text('{"rustflags":[]}', encoding="utf-8") + self.assertEqual(len(measure.fingerprint_records(root)), 1) + record.write_text('{"rustflags":["--cfg","test"]}', encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.fingerprint_records(root) + + +class ReceiptAndGateTests(unittest.TestCase): + def fixture(self, root: Path, inputs: dict[str, measure.Json]) -> Path: + copy_resource_export_fixture(root) + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + linker = root / "cc" + linker.write_bytes(b"measured C linker") + log = root / "observation.log" + log.write_bytes(b"independent observation") + archives: list[measure.Json] = [] + for profile in ("debug", "release"): + archive = root / f"{profile}-runtime.a" + archive.write_bytes(profile.encode()) + fingerprint = root / f"{profile}-fingerprint.json" + fingerprint.write_text('{"rustflags":[]}', encoding="utf-8") + build = root / f"{profile}-build.json" + measure.write_json(build, {"runtime_sources": inputs, "archive": measure.artifact(archive), + "measurement_script": measure.artifact(Path(measure.__file__))}) + libraries = root / f"{profile}-libraries.json" + measure.write_json(libraries, {"arguments": ["-lgcc_s", "-lc"], + "actual_print_log": measure.artifact(log)}) + crt = root / f"{profile}-crt.json" + measure.write_json(crt, {"mode": "dynamic", "linker_path": str(linker), + "linker_executable": measure.artifact(linker), "cargo_fingerprints": [ + {**measure.artifact(fingerprint), "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}], + "native_static_libs": measure.artifact(libraries)}) + archives.append({"profile": profile, "target": measure.TARGET, "runtime_abi": 1, + "layout_wire": measure.CURRENT_LAYOUT_WIRE, "private_cfgs": list(measure.CFGS), "crt_mode": "dynamic", + "archive_path": str(archive), "archive_sha256": measure.digest(archive), + "linker_path": str(linker), "native_library_args": ["-lgcc_s", "-lc"], + "build_receipt": measure.artifact(build), "native_static_libs_receipt": measure.artifact(libraries), + "crt_receipt": measure.artifact(crt)}) + receipt = root / "receipt.json" + measure.write_json(receipt, {"version": 1, "archives": archives}) + return receipt + + def test_stale_sources_changed_provenance_and_untrusted_pin_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + pin = measure.digest(receipt) + with patch.object(measure, "sources", return_value=inputs): + measure.receipt_sources(receipt, pin, root) + for invalid in ("", "0" * 64, pin.upper()): + with self.subTest(pin=invalid), self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, invalid, root) + with patch.object(measure, "sources", return_value={"Cargo.lock": "changed"}): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, pin, root) + (root / "debug-build.json").write_text("{}", encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, pin, root) + + def test_archive_linker_and_nested_fingerprint_mutation_are_refused(self) -> None: + for name in ("debug-runtime.a", "cc", "debug-fingerprint.json", "debug-libraries.json"): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + expected = measure.digest(receipt) + (root / name).write_bytes(b"changed after independent measurement") + with patch.object(measure, "sources", return_value=inputs): + with self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, expected, root) + + def test_existing_output_is_never_rebuilt_or_reauthenticated(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.prepare(root, root, "unused-cc", "unused-nm", "unused-readelf") + + def test_compiler_messages_must_select_one_actual_executable(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + first = root / "first" + second = root / "second" + first.write_bytes(b"first") + second.write_bytes(b"second") + def message(path: Path) -> str: + return json.dumps({"reason": "compiler-artifact", + "target": {"name": "native_conformance"}, "executable": str(path)}) + self.assertEqual(measure.compiled_test(message(first)), first) + for invalid in ("", message(root / "absent"), message(first) + "\n" + message(second)): + with self.subTest(value=invalid), self.assertRaises(measure.MeasurementError): + measure.compiled_test(invalid) + + def test_pin_is_compiled_then_removed_for_complete_source_gate(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + expected = measure.digest(receipt) + binary = root / "compiled-gate" + binary.write_bytes(b"compiled test") + compiler = root / "rustc" + compiler.write_bytes(b"measured compiler proxy") + compile_environment: list[dict[str, str]] = [] + runtime_environment: list[dict[str, str]] = [] + def compile_test(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: BinaryIO, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertIn("--no-run", argv) + self.assertIn("--target", argv) + self.assertEqual(argv[argv.index("--color") + 1], "never") + self.assertEqual(env["CARGO_TERM_COLOR"], "never") + self.assertNotIn("--cfg", argv) + self.assertEqual(cwd, root) + self.assertEqual(stdin, subprocess.DEVNULL) + compile_environment.append(dict(env)) + stderr.write(f"Running `{str(compiler)!r} --crate-name jett_driver`\n".encode()) + stdout.write((json.dumps({"reason": "compiler-artifact", + "target": {"name": "native_conformance"}, "executable": str(binary)}) + "\n").encode()) + return subprocess.CompletedProcess(argv, 0) + def execute_test(argv: list[str], directory: Path, environment: dict[str, str], + log: Path, timeout: int = 3600) -> str: + if argv == [str(compiler), "-vV"]: + self.assertNotIn(measure.PIN_ENV, environment) + text = f"host: {measure.TARGET}\n" + log.write_text(text, encoding="utf-8") + return text + self.assertEqual(argv[0], str(binary)) + self.assertEqual(argv[1], measure.TEST) + self.assertIn("--ignored", argv) + self.assertIn("--exact", argv) + self.assertEqual(directory, root) + runtime_environment.append(dict(environment)) + text = ("Resource native acceptance: 73 cases; 146 Source-deleted executions; " + "profiles=debug,release\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n") + log.write_text(text, encoding="utf-8") + return text + def executable_for_test(name: str) -> Path: + return root / name + with patch.object(measure, "sources", return_value=inputs), \ + patch.object(measure, "executable", side_effect=executable_for_test), \ + patch.object(measure.subprocess, "run", side_effect=compile_test), \ + patch.object(measure, "logged", side_effect=execute_test), \ + patch.dict(os.environ, {}, clear=True): + measure.run_gate(root, receipt, expected, root / "fresh-gate-target") + self.assertEqual(compile_environment[0][measure.PIN_ENV], expected) + self.assertEqual(compile_environment[0]["RUSTC"], str(compiler)) + self.assertNotIn(measure.RECEIPT_ENV, compile_environment[0]) + self.assertNotIn(measure.PIN_ENV, runtime_environment[0]) + self.assertEqual(runtime_environment[0][measure.RECEIPT_ENV], str(receipt)) + accepted = measure.read_object(root / "gate-acceptance.json") + self.assertEqual(accepted["source_deleted_executions"], 146) + self.assertEqual(accepted["runtime_expected_pin_environment"], False) + + def test_old_future_or_mixed_receipt_wire_cannot_authenticate_current_sources(self) -> None: + for wires in ((2, 2), (4, 4), (3, 2), (1, 3)): + with self.subTest(wires=wires), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = self.fixture(root, inputs) + record = measure.read_object(receipt) + rows = record["archives"] + self.assertIsInstance(rows, list) + if not isinstance(rows, list): + self.fail("fixture archive rows must be a list") + for row, wire in zip(rows, wires, strict=True): + self.assertIsInstance(row, dict) + if not isinstance(row, dict): + self.fail("fixture archive row must be an object") + row["layout_wire"] = wire + receipt.write_text(json.dumps(record), encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs), \ + self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, measure.digest(receipt), root) + + +class CarrierContractTests(unittest.TestCase): + def fixture(self, root: Path) -> tuple[Path, Path]: + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + copy_resource_export_fixture(root) + resource = root / "crates/jett_runtime/src/native_abi/resource" + carriers = resource / "leaves/carriers.rs" + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + return registration, carriers + + def test_exact_current_wire_and_carrier_source_contract(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + measure.checked_layout_wire(root) + leaves, private = measure.export_names(root) + self.assertEqual((len(leaves), len(private)), (58, 8)) + self.assertTrue(measure.CARRIER_EXPORTS <= leaves) + self.assertEqual(leaves & measure.STARTUP_EXPORTS, measure.STARTUP_EXPORTS) + + def test_old_future_duplicate_or_missing_runtime_wire_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, _ = self.fixture(root) + original = registration.read_text(encoding="utf-8") + for invalid in (original.replace("= 3;", "= 2;"), original.replace("= 3;", "= 4;"), + original + original, ""): + registration.write_text(invalid, encoding="utf-8") + with self.subTest(invalid=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_layout_wire(root) + + def test_missing_duplicate_and_foreign_carrier_names_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, carriers = self.fixture(root) + original = carriers.read_text(encoding="utf-8") + first, *rest = original.splitlines() + for invalid in ("\n".join(rest), original + "\n" + first, + original.replace("carrier_adapt(", "carrier_foreign(")): + carriers.write_text(invalid, encoding="utf-8") + with self.subTest(invalid=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + carriers.write_text(original, encoding="utf-8") + legacy = carriers.parent.parent / "leaves.rs" + original_legacy = legacy.read_text(encoding="utf-8") + first_name = sorted(measure.export_names(root)[0] - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS)[0] + legacy.write_text(original_legacy.replace(first_name + "(", + "jett_rt_v1_resource_carrier_adapt("), encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_stale_wire_or_symbols_refused_before_tools_and_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, carriers = self.fixture(root) + output = root / "fresh-measurement" + original = registration.read_text(encoding="utf-8") + registration.write_text(original.replace("= 3;", "= 2;"), encoding="utf-8") + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + registration.write_text(original, encoding="utf-8") + carriers.write_text("", encoding="utf-8") + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_foreign_executing_helper_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + measure.checked_measurement_source(root) + (root / "tools/measure_resource_native_archives.py").write_bytes(b"foreign helper") + with self.assertRaises(measure.MeasurementError): + measure.checked_measurement_source(root) + + def test_guards_are_bound_to_the_source_snapshot_before_output_or_build(self) -> None: + for changed in ("wire", "exports", "startup_module", "startup_signature", "startup_exports", + "graph_module", "graph_proof", "graph_signature", "graph_exports"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + registration, carriers = self.fixture(root) + output = root / "fresh-measurement" + def snapshot_mutation(repo: Path, complete: bool = False) -> dict[str, measure.Json]: + self.assertEqual(repo, root) + self.assertFalse(complete) + if changed == "wire": + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 2;\n", + encoding="utf-8") + elif changed == "exports": + carriers.write_text("", encoding="utf-8") + elif changed == "startup_module": + owner = carriers.parent.parent.with_suffix(".rs") + owner.write_text(owner.read_text(encoding="utf-8").replace( + "mod program_entry;", "#[cfg(test)]\nmod program_entry;"), encoding="utf-8") + elif changed.startswith("graph_"): + directory = carriers.parent.parent + if changed == "graph_module": + owner = directory.with_suffix(".rs") + owner.write_text(owner.read_text(encoding="utf-8").replace( + "mod graph_descriptors;", "#[cfg(test)]\nmod graph_descriptors;"), encoding="utf-8") + elif changed == "graph_proof": + child = directory / "graph_descriptors.rs" + child.write_text(child.read_text(encoding="utf-8").replace( + "context: ContextRegistryKey,", "scope: ResourceHandleId,"), encoding="utf-8") + else: + leaf = directory / "leaves.rs" + text = leaf.read_text(encoding="utf-8") + if changed == "graph_signature": + text = text.replace("out_box: *mut u64,", "out_box: *mut u32,") + else: + text = text.replace(measure.GRAPH_DESCRIPTOR_EXPORT + "(", + "jett_rt_v1_resource_foreign_graph(") + leaf.write_text(text, encoding="utf-8") + else: + child = carriers.parent.parent / "program_entry.rs" + text = child.read_text(encoding="utf-8") + if changed == "startup_signature": + text = text.replace("out_attempt: *mut u64,", "out_attempt: *mut u32,") + else: + text = text.replace("jett_rt_v1_resource_program_begin(", + "jett_rt_v1_resource_program_foreign(") + child.write_text(text, encoding="utf-8") + return {} + with patch.object(measure, "sources", side_effect=snapshot_mutation), \ + patch.object(measure, "executable", return_value=root / "unused-tool"), \ + patch.object(measure, "build_environment", return_value={}), \ + patch.object(measure, "toolchain") as toolchain, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + toolchain.assert_not_called() + self.assertFalse(output.exists()) + + +class ScalarTaskContractTests(unittest.TestCase): + def test_additive_scalar_contract_preserves_original_and_graph_startup_exports(self) -> None: + leaves, private = measure.export_names(measure.ROOT) + self.assertEqual((len(leaves), len(private)), (58, 8)) + self.assertEqual(len(leaves - measure.SCALAR_TASK_EXPORTS), 56) + self.assertEqual(len(measure.CARRIER_EXPORTS - measure.SCALAR_TASK_EXPORTS), 13) + self.assertEqual(measure.scalar_task_exports( + measure.ROOT / "crates/jett_runtime/src/native_abi/resource"), measure.SCALAR_TASK_EXPORTS) + + def test_scalar_symbols_are_required_once_as_global_text(self) -> None: + leaves, private = measure.export_names(measure.ROOT) + symbols = "\n".join(f"{name} T 0 10" for name in sorted(leaves | private | {"main"})) + for name in sorted(measure.SCALAR_TASK_EXPORTS): + line = f"{name} T 0 10" + for invalid in (symbols.replace(line, ""), symbols + "\n" + line, + symbols.replace(line, f"{name} D 0 10")): + with self.subTest(name=name, symbols=invalid), self.assertRaises(measure.MeasurementError): + measure.check_symbols(invalid, leaves, private) + + def test_scalar_child_module_cannot_be_conditional_public_redirected_or_duplicated(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + owner = startup.parent / "leaves.rs" + original = owner.read_text(encoding="utf-8") + declaration = '#[path = "leaves/carriers.rs"]\nmod carrier_leaves;' + for replacement in ("", declaration + "\n" + declaration, + "#[cfg(test)]\n" + declaration, + declaration.replace("mod carrier_leaves", "pub mod carrier_leaves"), + declaration.replace("leaves/carriers.rs", "other.rs")): + owner.write_text(original.replace(declaration, replacement), encoding="utf-8") + with self.subTest(module=replacement), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_scalar_exports_cannot_be_missing_duplicate_conditional_mangled_or_non_c(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + child = startup.parent / "leaves/carriers.rs" + original = child.read_text(encoding="utf-8") + for name in sorted(measure.SCALAR_TASK_EXPORTS): + prefix = f'pub unsafe extern "C" fn {name}(' + start, end = original.index(prefix), original.index("{", original.index(prefix)) + header = original[start:end] + for invalid in ( + original.replace(prefix, prefix.removeprefix("pub "), 1), + original + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', + original.replace(name + "(", "jett_rt_v1_resource_carrier_foreign_scalar(", 1), + original.replace(prefix, "#[cfg(test)]\n" + prefix, 1), + original[:start].removesuffix("#[unsafe(no_mangle)]\n") + original[start:], + original.replace(prefix, prefix.replace('extern "C"', 'extern "Rust"'), 1), + "#![cfg(test)]\n" + original, + ): + child.write_text(invalid, encoding="utf-8") + with self.subTest(name=name, source=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_every_scalar_argument_and_return_type_has_exact_c_width(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + child = startup.parent / "leaves/carriers.rs" + original = child.read_text(encoding="utf-8") + for name in sorted(measure.SCALAR_TASK_EXPORTS): + start = original.index(f'pub unsafe extern "C" fn {name}(') + end = original.index("{", start) + lines = original[start:end].splitlines(keepends=True) + for index, line in enumerate(lines): + if ": " not in line and "-> JettRuntimeStatusV1" not in line: + continue + changed = list(lines) + changed[index] = (line.split(": ", 1)[0] + ": usize,\n" if ": " in line + else line.replace("JettRuntimeStatusV1", "u32")) + child.write_text(original[:start] + "".join(changed) + original[end:], encoding="utf-8") + with self.subTest(name=name, parameter=index), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_scalar_argument_reorder_and_missing_output_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + child = startup.parent / "leaves/carriers.rs" + original = child.read_text(encoding="utf-8") + for name in sorted(measure.SCALAR_TASK_EXPORTS): + start = original.index(f'pub unsafe extern "C" fn {name}(') + end = original.index("{", start) + lines = original[start:end].splitlines(keepends=True) + reordered = list(lines) + reordered[2], reordered[3] = reordered[3], reordered[2] + for changed in (reordered, lines[:-2] + lines[-1:]): + child.write_text(original[:start] + "".join(changed) + original[end:], encoding="utf-8") + with self.subTest(name=name, header=changed), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_scalar_cannot_move_to_ordinary_startup_or_private_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + child = startup.parent / "leaves/carriers.rs" + original = child.read_text(encoding="utf-8") + for name in sorted(measure.SCALAR_TASK_EXPORTS): + prefix = f'pub unsafe extern "C" fn {name}(' + start, end = original.index(prefix), original.index("{", original.index(prefix)) + header = original[start:end] + for destination in (startup, startup.parent / "leaves.rs", startup.parent / "test_archive_api.rs"): + prior = destination.read_text(encoding="utf-8") + child.write_text(original.replace(prefix, prefix.removeprefix("pub "), 1), encoding="utf-8") + destination.write_text(prior + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', encoding="utf-8") + with self.subTest(name=name, destination=destination), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + destination.write_text(prior, encoding="utf-8") + + def test_scalar_snapshot_mutation_refuses_before_tool_discovery_and_output(self) -> None: + for changed in ("width", "missing", "cfg", "reorder", "module"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = CarrierContractTests().fixture(root) + output = root / "fresh-measurement" + def snapshot_mutation(repo: Path, complete: bool = False) -> dict[str, measure.Json]: + self.assertEqual(repo, root) + self.assertFalse(complete) + text = child.read_text(encoding="utf-8") + name = "jett_rt_v1_resource_carrier_observe_scalar_task" + prefix = f'pub unsafe extern "C" fn {name}(' + start, end = text.index(prefix), text.index("{", text.index(prefix)) + header = text[start:end] + if changed == "module": + owner = child.parent.parent / "leaves.rs" + owner.write_text(owner.read_text(encoding="utf-8").replace( + "mod carrier_leaves;", "#[cfg(test)]\nmod carrier_leaves;"), encoding="utf-8") + else: + if changed == "width": + header = header.replace("argument_6: *mut u64", "argument_6: *mut u32") + elif changed == "missing": + header = header.replace(" argument_6: *mut u64,\n", "") + elif changed == "cfg": + header = "#[cfg(test)]\n" + header + else: + header = header.replace("argument_1: u64", "argument_1: u32").replace( + "argument_2: u32", "argument_2: u64") + child.write_text(text[:start] + header + text[end:], encoding="utf-8") + return {} + with patch.object(measure, "sources", side_effect=snapshot_mutation), \ + patch.object(measure, "executable", return_value=root / "unused-tool"), \ + patch.object(measure, "build_environment", return_value={}), \ + patch.object(measure, "toolchain") as toolchain, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + toolchain.assert_not_called() + self.assertFalse(output.exists()) + + +class GraphDescriptorContractTests(unittest.TestCase): + def test_exact_graph_symbol_joins_only_the_original_fifty_and_startup_five(self) -> None: + leaves, private = measure.export_names(measure.ROOT) + self.assertEqual((len(leaves), len(private)), (58, 8)) + self.assertEqual(len(leaves - {measure.GRAPH_DESCRIPTOR_EXPORT} - measure.STARTUP_EXPORTS + - measure.SCALAR_TASK_EXPORTS), 50) + self.assertEqual(measure.graph_descriptor_export( + measure.ROOT / "crates/jett_runtime/src/native_abi/resource"), {measure.GRAPH_DESCRIPTOR_EXPORT}) + symbols = "\n".join(f"{name} T 0 10" for name in sorted(leaves | private | {"main"})) + measure.check_symbols(symbols, leaves, private) + for invalid in (symbols.replace(measure.GRAPH_DESCRIPTOR_EXPORT + " T 0 10", ""), + symbols + "\n" + measure.GRAPH_DESCRIPTOR_EXPORT + " T 0 10", + symbols + "\njett_rt_v1_resource_foreign_graph T 0 10"): + with self.subTest(symbols=invalid), self.assertRaises(measure.MeasurementError): + measure.check_symbols(invalid, leaves, private) + + def test_missing_duplicate_conditional_public_and_redirected_proof_module_refuse(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + owner, child = copy_resource_export_fixture(root) + original = owner.read_text(encoding="utf-8") + for replacement in ("", "mod graph_descriptors;\nmod graph_descriptors;", + "#[cfg(test)]\nmod graph_descriptors;", + '#[path = "other.rs"]\nmod graph_descriptors;', + "pub mod graph_descriptors;"): + owner.write_text(original.replace("mod graph_descriptors;", replacement), encoding="utf-8") + with self.subTest(module=replacement), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + owner.write_text(original, encoding="utf-8") + (child.parent / "graph_descriptors.rs").unlink() + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_private_proof_cannot_be_public_conditional_or_acquire_scope_authority(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + child = startup.parent / "graph_descriptors.rs" + original = child.read_text(encoding="utf-8") + for invalid in ( + original.replace("pub(in crate::native_abi) struct CheckedGraphHookProof", "pub struct CheckedGraphHookProof"), + original.replace("#[derive(Clone)]", "#[cfg(test)]\n#[derive(Clone)]", 1), + "#![cfg(test)]\n" + original, + original.replace("context: ContextRegistryKey,", "context: usize,"), + original.replace("signature: u32,", "signature: u32,\n scope: ResourceHandleId,", 1), + ): + child.write_text(invalid, encoding="utf-8") + with self.subTest(proof=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_missing_duplicate_foreign_conditional_mangled_and_non_c_graph_export_refuse(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + leaf = startup.parent / "leaves.rs" + original = leaf.read_text(encoding="utf-8") + prefix = f'pub unsafe extern "C" fn {measure.GRAPH_DESCRIPTOR_EXPORT}(' + start, end = original.index(prefix), original.index("{", original.index(prefix)) + header = original[start:end] + for invalid in ( + original.replace(prefix, prefix.removeprefix("pub "), 1), + original + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', + original.replace(measure.GRAPH_DESCRIPTOR_EXPORT + "(", "jett_rt_v1_resource_foreign_graph(", 1), + original.replace(prefix, "#[cfg(test)]\n" + prefix, 1), + original[:start].removesuffix("#[unsafe(no_mangle)]\n") + original[start:], + original.replace(prefix, prefix.replace('extern "C"', 'extern "Rust"'), 1), + ): + leaf.write_text(invalid, encoding="utf-8") + with self.subTest(source=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_every_graph_argument_and_return_type_is_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + leaf = startup.parent / "leaves.rs" + original = leaf.read_text(encoding="utf-8") + start = original.index(f'pub unsafe extern "C" fn {measure.GRAPH_DESCRIPTOR_EXPORT}(') + end = original.index("{", start) + lines = original[start:end].splitlines(keepends=True) + for index, line in enumerate(lines): + if ": " not in line and "-> JettRuntimeStatusV1" not in line: + continue + changed = list(lines) + changed[index] = (line.split(": ", 1)[0] + ": usize,\n" if ": " in line + else line.replace("JettRuntimeStatusV1", "u32")) + leaf.write_text(original[:start] + "".join(changed) + original[end:], encoding="utf-8") + with self.subTest(parameter=index), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_graph_declaration_cannot_move_to_startup_or_private_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, startup = copy_resource_export_fixture(root) + leaf = startup.parent / "leaves.rs" + original = leaf.read_text(encoding="utf-8") + prefix = f'pub unsafe extern "C" fn {measure.GRAPH_DESCRIPTOR_EXPORT}(' + start, end = original.index(prefix), original.index("{", original.index(prefix)) + header = original[start:end] + for destination in (startup, startup.parent / "test_archive_api.rs"): + prior = destination.read_text(encoding="utf-8") + leaf.write_text(original.replace(prefix, prefix.removeprefix("pub "), 1), encoding="utf-8") + destination.write_text(prior + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', encoding="utf-8") + with self.subTest(destination=destination), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + destination.write_text(prior, encoding="utf-8") + + +class StartupContractTests(unittest.TestCase): + def test_module_missing_duplicate_conditional_or_redirected_is_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + owner, _ = copy_resource_export_fixture(root) + original = owner.read_text(encoding="utf-8") + for replacement in ("", "mod program_entry;\nmod program_entry;", + "#[cfg(test)]\nmod program_entry;", + '#[path = "other.rs"]\nmod program_entry;', + "pub mod program_entry;"): + owner.write_text(original.replace("mod program_entry;", replacement), encoding="utf-8") + with self.subTest(module=replacement), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_missing_child_and_inner_conditional_module_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + child.write_text("#![cfg(test)]\n" + original, encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + child.unlink() + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_missing_duplicate_foreign_conditional_and_mangled_exports_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + for name in sorted(measure.STARTUP_EXPORTS): + prefix = f'pub unsafe extern "C" fn {name}(' + start = original.index(prefix) + end = original.index("{", start) + header = original[start:end] + for invalid in ( + original.replace(prefix, prefix.removeprefix("pub "), 1), + original + '\n#[unsafe(no_mangle)]\n' + header + '{}\n', + original.replace(name + "(", "jett_rt_v1_resource_program_foreign(", 1), + original.replace(prefix, "#[cfg(test)]\n" + prefix, 1), + original[:start].removesuffix("#[unsafe(no_mangle)]\n") + original[start:], + original.replace(prefix, prefix.replace('extern "C"', 'extern "Rust"'), 1), + ): + child.write_text(invalid, encoding="utf-8") + with self.subTest(name=name, source=invalid), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_every_startup_argument_and_return_type_is_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + original = child.read_text(encoding="utf-8") + for name in sorted(measure.STARTUP_EXPORTS): + start = original.index(f'pub unsafe extern "C" fn {name}(') + end = original.index("{", start) + header = original[start:end] + lines = header.splitlines(keepends=True) + for index, line in enumerate(lines): + if ": " not in line and "-> JettRuntimeStatusV1" not in line: + continue + changed = list(lines) + if ": " in line: + parameter = line.split(": ", 1)[0] + changed[index] = parameter + ": usize,\n" + else: + changed[index] = line.replace("JettRuntimeStatusV1", "u32") + child.write_text(original[:start] + "".join(changed) + original[end:], encoding="utf-8") + with self.subTest(name=name, parameter=index), self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_startup_cannot_be_relocated_into_old_leaf_inventory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, child = copy_resource_export_fixture(root) + leaves = child.parent / "leaves.rs" + original = leaves.read_text(encoding="utf-8") + names = sorted(measure.export_names(root)[0] - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS) + leaves.write_text(original.replace(names[0] + "(", + "jett_rt_v1_resource_program_begin(", 1), encoding="utf-8") + with self.assertRaises(measure.MeasurementError): + measure.export_names(root) + + def test_invalid_startup_is_refused_before_tool_discovery_and_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, carriers = CarrierContractTests().fixture(root) + startup = carriers.parent.parent / "program_entry.rs" + startup.write_text(startup.read_text(encoding="utf-8").replace( + "out_attempt: *mut u64,", "out_attempt: *mut u32,"), encoding="utf-8") + output = root / "fresh-measurement" + with patch.object(measure, "executable") as executable, \ + self.assertRaises(measure.MeasurementError): + measure.prepare(root, output, "unused-cc", "unused-nm", "unused-readelf") + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_receipt_authentication_checks_startup_contract(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs: dict[str, measure.Json] = {"Cargo.lock": "frozen"} + receipt = ReceiptAndGateTests().fixture(root, inputs) + child = root / "crates/jett_runtime/src/native_abi/resource/program_entry.rs" + child.write_text(child.read_text(encoding="utf-8").replace( + "out_attempt: *mut u64,", "out_attempt: *mut u32,"), encoding="utf-8") + with patch.object(measure, "sources", return_value=inputs), \ + self.assertRaises(measure.MeasurementError): + measure.receipt_sources(receipt, measure.digest(receipt), root) + + +class MsvcObservationTests(unittest.TestCase): + flags = ["-C", "target-feature=+crt-static", "-C", "debuginfo=0"] + libraries = ["kernel32.lib", "advapi32.lib", "ntdll.lib", "userenv.lib", + "ws2_32.lib", "dbghelp.lib", "bcrypt.lib", "advapi32.lib", + "libvcruntime.lib", "libucrt.lib", "libcmt.lib"] + + raw_libraries = ["bcrypt.lib", "advapi32.lib", "gdi32.lib", "kernel32.lib", "msimg32.lib", + "opengl32.lib", "user32.lib", "winspool.lib", "kernel32.lib", "ntdll.lib", + "userenv.lib", "ws2_32.lib", "dbghelp.lib", "/defaultlib:libcmt"] + @staticmethod + def runtime_command(compiler: Path) -> str: + return (f'Running `"{compiler}" --crate-name jett_runtime --crate-type rlib ' + '--crate-type staticlib --target x86_64-pc-windows-msvc ' + '--cfg test --cfg jett_resource_native_test_archive ' + '-C target-feature=+crt-static -C debuginfo=0 --print=native-static-libs`') + + @staticmethod + def coff_observation() -> str: + # llvm-readobj prints one complete target record for every archive member. + return ('File: runtime.lib(runtime.o)\nFormat: COFF-x86-64\nArch: x86_64\n' + 'AddressSize: 64bit\nImageFileHeader {\n Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n}\n' + 'Directive(s): /DEFAULTLIB:libcmt /DEFAULTLIB:libvcruntime /DEFAULTLIB:libucrt\n' + 'File: runtime.lib(compiler_builtins.o)\nFormat: COFF-x86-64\nArch: x86_64\n' + 'AddressSize: 64bit\nImageFileHeader {\n Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n}\n') + + @staticmethod + def rust_archive_without_directives() -> str: + ordinary = MsvcObservationTests.coff_observation().replace( + "Directive(s): /DEFAULTLIB:libcmt /DEFAULTLIB:libvcruntime /DEFAULTLIB:libucrt\n", "") + imported = ("File: kernel32.dll\nFormat: COFF-import-file-x86-64\nType: code\n" + "Name type: name\nExport name: CloseHandle\n" + "Symbol: __imp_CloseHandle\nSymbol: CloseHandle\n") + return ordinary + imported + def test_windows_argv_preserves_paths_and_quoted_cfg_values(self) -> None: + command = (r'"C:\Program Files\Rust\rustc.exe" --cfg "feature=\"native-tests\"" ' + r'-L "C:\target with spaces\deps" --cfg test') + self.assertEqual(measure.windows_arguments(command), [ + r"C:\Program Files\Rust\rustc.exe", "--cfg", 'feature="native-tests"', + "-L", r"C:\target with spaces\deps", "--cfg", "test"]) + with self.assertRaises(measure.MeasurementError): + measure.windows_arguments(r'"C:\Program Files\Rust\rustc.exe') + + def test_msvc_library_observation_keeps_order_and_duplicates(self) -> None: + text = ("note: native-static-libs: kernel32.lib advapi32.lib ntdll.lib userenv.lib\n" + " ws2_32.lib dbghelp.lib bcrypt.lib advapi32.lib\n" + " libvcruntime.lib libucrt.lib libcmt.lib\nFinished build\n") + self.assertEqual(measure.msvc_native_libraries(text), self.libraries) + status = text.replace("Finished build", " Finished `dev` profile [optimized] target(s) in 8.3s") + self.assertEqual(measure.msvc_native_libraries(status), self.libraries) + self.assertEqual(self.libraries.count("advapi32.lib"), 2) + + def test_msvc_library_observation_refuses_foreign_crt_or_linker_options(self) -> None: + good = "note: native-static-libs: " + " ".join(self.libraries) + "\n" + invalid = ["", good + good, good.replace("libcmt.lib", "msvcrt.lib"), good.replace("libcmt.lib", ""), + good.replace("libucrt.lib", "ucrt.lib"), good + " /NODEFAULTLIB:libcmt\n", + good + " jett_runtime.lib\n", good + " jett_native_launcher.lib\n", + good.replace("libcmt.lib", "libcmt.a"), good + " @response.rsp\n"] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries(text) + + def test_selected_windows_compiler_owns_both_private_cfgs(self) -> None: + with tempfile.TemporaryDirectory(prefix="compiler identity ") as temporary: + compiler = Path(temporary) / "measured rustc.exe" + command = self.runtime_command(compiler) + dependency = (f'Running `"{compiler}" --crate-name dependency --crate-type rlib ' + '--target x86_64-pc-windows-msvc -C target-feature=+crt-static ' + '-C debuginfo=0 --cfg "feature=\\\"jett_resource_native_test_archive\\\""`') + measure.msvc_selected_cfgs(dependency + "\n" + command, compiler) + invalid = [ + command + "\n" + command, + command.replace("--crate-name jett_runtime", "--crate-name dependency"), + command.replace("--cfg test ", ""), + command.replace("--cfg test ", "--cfg test --cfg test "), + command.replace("--crate-type staticlib ", ""), + command.replace("--print=native-static-libs", "--test --print=native-static-libs"), + command.replace("target-feature=+crt-static", "target-feature=-crt-static"), + command.replace("debuginfo=0", "debuginfo=2"), + command.replace(str(compiler), str(compiler.parent / "foreign rustc.exe")), + command.replace('Running `"', 'Running `wrapper.exe "', 1), + command + "\n" + dependency.replace('--cfg "feature=\\\"jett_resource_native_test_archive\\\""', + '--cfg jett_resource_native_test_archive'), + ] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.msvc_selected_cfgs(text, compiler) + + def test_static_crt_fingerprint_requires_observed_flags_not_an_asserted_mode(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(root) + directory = root / "jett_runtime-hash" + directory.mkdir() + fingerprint = directory / "lib-jett_runtime.json" + fingerprint.write_text(json.dumps({"rustflags": self.flags}), encoding="utf-8") + records = measure.msvc_fingerprint_records(root) + self.assertEqual(len(records), 1) + self.assertEqual(records[0], {**measure.artifact(fingerprint), + "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}) + for flags in ([], self.flags[:-2], ["--cfg", "test", *self.flags], + ["-C", "target-feature=-crt-static", "-C", "debuginfo=0"], + ["-C", "target-feature=+crt-static", "-C", "debuginfo=2"]): + fingerprint.write_text(json.dumps({"rustflags": flags}), encoding="utf-8") + with self.subTest(flags=flags), self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(root) + + def test_every_coff_member_has_the_measured_target_and_static_crt_directives(self) -> None: + good = self.coff_observation() + measure.checked_coff_archive(good) + measure.checked_coff_archive(good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MT_StaticRelease"\n') + second = good.index("File: runtime.lib(compiler_builtins.o)") + invalid = [ + "", good[:second] + good[second:].replace("COFF-x86-64", "COFF-ARM64"), + good[:second] + good[second:].replace("x86_64", "aarch64"), + good[:second] + good[second:].replace("64bit", "32bit"), + good[:second] + good[second:].replace("IMAGE_FILE_MACHINE_AMD64 (0x8664)", + "IMAGE_FILE_MACHINE_I386 (0x14C)"), + good[:second] + good[second:].replace(" Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)\n", ""), + good + "Directive(s): /DEFAULTLIB:msvcrt\n", + good + 'Directive(s): /DEFAULTLIB:"MSVCRTD.LIB"\n', + good + "Directive(s): /DEFAULTLIB:ucrt\n", + good + "Directive(s): /NODEFAULTLIB:libcmt\n", + good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MD_DynamicRelease"\n', + good + 'Directive(s): /FAILIFMISMATCH:"RuntimeLibrary=MTd_StaticDebug"\n', + ] + for text in invalid: + with self.subTest(text=text), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(text) + + def test_msvc_environment_keeps_sdk_lib_verbatim_and_blocks_global_cfg_injection(self) -> None: + sdk = r"C:\VS Libraries\x64;C:\SDK\ucrt\x64;C:\SDK\um\x64;" + with patch.dict(os.environ, {"LIB": sdk, "CARGO_TERM_COLOR": "always"}, clear=True): + environment = measure.msvc_build_environment() + self.assertEqual(environment["LIB"], sdk) + self.assertEqual(environment["CARGO_INCREMENTAL"], "0") + self.assertEqual(environment["CARGO_TERM_COLOR"], "never") + self.assertEqual(environment["CARGO_PROFILE_DEV_DEBUG"], "0") + self.assertEqual(environment["CARGO_PROFILE_RELEASE_DEBUG"], "0") + for name, value in (("RUSTFLAGS", "--cfg test"), + ("CARGO_ENCODED_RUSTFLAGS", "--cfg\x1ftest"), + ("CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS", "-Ctarget-feature=-crt-static"), + ("RUSTC_WRAPPER", "wrapper.exe"), ("RUSTC", "foreign-rustc.exe")): + with self.subTest(name=name), patch.dict(os.environ, {"LIB": sdk, name: value}, clear=True): + with self.assertRaises(measure.MeasurementError): + measure.msvc_build_environment() + + def test_coff_system_import_members_need_their_own_target_and_dll_evidence(self) -> None: + # LLVM import members omit the ordinary Arch/AddressSize/FileHeader rows. + imported = ("File: kernel32.dll\nFormat: COFF-import-file-x86-64\nType: code\n" + "Name type: name\nExport name: CloseHandle\n" + "Symbol: __imp_CloseHandle\nSymbol: CloseHandle\n") + ordinary = self.coff_observation() + measure.checked_coff_archive(ordinary + imported) + measured_dlls = ("advapi32.dll", "api-ms-win-core-handle-l1-1-0.dll", + "api-ms-win-core-synch-l1-2-0.dll", "bcp47mrm.dll", "bcryptprimitives.dll", + "elscore.dll", "gdi32.dll", "icu.dll", "icuin.dll", "icuuc.dll", "kernel32.dll", + "normaliz.dll", "ntdll.dll", "oleaut32.dll", "user32.dll", "usp10.dll") + measure.checked_coff_archive(ordinary + "".join(imported.replace("kernel32.dll", name) + for name in measured_dlls)) + invalid = ( + imported.replace("COFF-import-file-x86-64", "COFF-import-file-i386"), + imported.replace("COFF-import-file-x86-64", "COFF-import-file-arm64"), + imported.replace("COFF-import-file-x86-64", "unknown-import-format"), + imported.replace("Type: code", "Type: data"), + imported.replace("Name type: name", "Name type: ordinal"), + imported.replace("kernel32.dll", "ucrtbase.dll"), + imported.replace("kernel32.dll", "vcruntime140.dll"), + imported.replace("kernel32.dll", "msvcrt.dll"), + imported.replace("kernel32.dll", "foreign-runtime.dll"), + ) + for value in invalid: + with self.subTest(imported=value), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(ordinary + value) + + def test_mixed_rust_archive_with_no_directive_records_has_valid_coff_evidence(self) -> None: + text = self.rust_archive_without_directives() + self.assertNotIn("Directive(s):", text) + measure.checked_coff_archive(text) + + def test_observed_directives_need_no_embedded_crt_library_but_must_not_conflict(self) -> None: + text = self.rust_archive_without_directives() + accepted = ("/DEFAULTLIB:kernel32", '/FAILIFMISMATCH:"_ITERATOR_DEBUG_LEVEL=0"', + '/FAILIFMISMATCH:"RuntimeLibrary=MT_StaticRelease"', "/DEFAULTLIB:libvcruntime") + rejected = ("/DEFAULTLIB:msvcrt", "/DEFAULTLIB:ucrt", "/DEFAULTLIB:libcmtd", + "/NODEFAULTLIB:libcmt", '/FAILIFMISMATCH:"RuntimeLibrary=MD_DynamicRelease"') + for directive in accepted: + with self.subTest(directive=directive): + observed = text.replace("File: kernel32.dll", f"Directive(s): {directive}\nFile: kernel32.dll") + self.assertNotIn("/DEFAULTLIB:libcmt", observed) + measure.checked_coff_archive(observed) + for directive in rejected: + with self.subTest(directive=directive), self.assertRaises(measure.MeasurementError): + measure.checked_coff_archive(text.replace("File: kernel32.dll", + f"Directive(s): {directive}\nFile: kernel32.dll")) + + def test_actual_native_library_vector_preserves_duplicate_and_raw_crt_flag(self) -> None: + text = ("note: native-static-libs: bcrypt.lib advapi32.lib gdi32.lib kernel32.lib " + "msimg32.lib opengl32.lib user32.lib winspool.lib kernel32.lib ntdll.lib " + "userenv.lib ws2_32.lib dbghelp.lib /defaultlib:libcmt\n") + self.assertEqual(measure.msvc_native_libraries(text), self.raw_libraries) + self.assertEqual(self.raw_libraries.count("kernel32.lib"), 2) + self.assertEqual(self.raw_libraries[-1], "/defaultlib:libcmt") + + def test_wrapped_safe_defaultlib_flag_is_validated_without_rewriting_it(self) -> None: + for flag in ("/defaultlib:libcmt", "/DEFAULTLIB:libcmt.lib", "/DefaultLib:LiBcMt.LiB"): + with self.subTest(flag=flag): + text = ("note: native-static-libs: " + " ".join(self.raw_libraries[:-1]) + + "\n " + flag + "\n Finished `dev` profile [optimized] target(s) in 8.3s\n") + self.assertEqual(measure.msvc_native_libraries(text), [*self.raw_libraries[:-1], flag]) + + def test_defaultlib_tokens_cannot_hide_dynamic_crt_paths_or_unknown_switches(self) -> None: + invalid = ("/defaultlib:msvcrt", "/DEFAULTLIB:UCRT.LIB", "/defaultlib:libcmtd", + "/defaultlib:vcruntime", "/defaultlib:libcmt.a", "/defaultlib:libcmt.lib.exe", + r"/defaultlib:C:\SDK\libcmt.lib", "/defaultlib:../libcmt.lib", r"/LIBPATH:C:\SDK", + "@native.rsp", "/WHOLEARCHIVE:libcmt.lib", "-defaultlib:libcmt", "/defaultlib:") + for flag in invalid: + for separator in (" ", "\n "): + with self.subTest(flag=flag, wrapped=separator != " "), self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries("note: native-static-libs: " + + " ".join(self.raw_libraries[:-1]) + separator + flag + "\n") + with self.assertRaises(measure.MeasurementError): + measure.msvc_native_libraries("note: native-static-libs: " + " ".join(self.raw_libraries[:-1]) + + "\n /defaultlib:libcmt @native.rsp\n") + +class MsvcReceiptTests(unittest.TestCase): + @staticmethod + def rewrite(path: Path, value: measure.Json) -> None: + path.write_text(json.dumps(value), encoding="utf-8") + + @staticmethod + def rows(receipt: Path) -> list[dict[str, measure.Json]]: + value = measure.read_object(receipt).get("archives") + if not isinstance(value, list) or any(not isinstance(row, dict) for row in value): + raise AssertionError("fixture must retain archive objects") + return [row for row in value if isinstance(row, dict)] + + def fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + # These are synthetic measurements, never copies of built Cargo artifacts. + helper = root / "tools/measure_resource_native_archives.py" + helper.parent.mkdir(parents=True) + helper.write_bytes(Path(measure.__file__).read_bytes()) + runtime = root / "crates/jett_runtime" + resource = runtime / "src/native_abi/resource" + copy_resource_export_fixture(root) + ordinary_values = runtime / "src/native_abi/values.rs" + ordinary_values.write_bytes( + (measure.ROOT / "crates/jett_runtime/src/native_abi/values.rs").read_bytes()) + leaves, private = measure.export_names(measure.ROOT) + for path, names in ((resource / "test_archive_api.rs", private),): + path.write_text("\n".join(f'pub unsafe extern "C" fn {name}() {{}}' for name in sorted(names)), + encoding="utf-8") + registration = runtime / "src/resource_custody/registration.rs" + registration.parent.mkdir(parents=True) + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 3;\n", + encoding="utf-8") + for path in (root / "Cargo.lock", root / "Cargo.toml", runtime / "Cargo.toml"): + path.write_bytes(b"synthetic retained source fixture") + compiler = root / "tool chain/rustc.exe" + compiler.parent.mkdir() + compiler.write_bytes(b"measured compiler fixture") + linker = root / "tool chain/link.exe" + linker.write_bytes(b"measured linker fixture") + environment = {"LIB": str(root / "SDK Libraries/x64") + ";" + str(root / "VS Libraries/x64") + ";", + "VCINSTALLDIR": str(root / "VS Libraries"), "VSCMD_ARG_TGT_ARCH": "x64", + "PATH": str(linker.parent)} + inputs = measure.sources(root) + archives: list[measure.Json] = [] + for profile in ("debug", "release"): + archive = root / f"{profile}-runtime.lib" + archive.write_bytes(("synthetic " + profile + " archive fixture").encode()) + fingerprint = root / f"{profile}-fingerprint.json" + self.rewrite(fingerprint, {"rustflags": MsvcObservationTests.flags}) + build_log = root / f"{profile}-build.log" + build_log.write_text(MsvcObservationTests.runtime_command(compiler).replace("--print=native-static-libs", + f'-C "linker={linker}" --print=native-static-libs') + "\n" + + "note: native-static-libs: " + " ".join(MsvcObservationTests.libraries) + "\n", + encoding="utf-8") + symbols = root / f"{profile}-symbols.log" + symbols.write_text("runtime.lib(runtime.o):\n" + "\n".join( + f"{name} T 0 10" for name in sorted( + leaves | private | {"main", measure.STRUCT_REPLACE_EXPORT, + measure.ACTOR_REPLACE_EXPORT})), encoding="utf-8") + coff = root / f"{profile}-coff.log" + coff.write_text(MsvcObservationTests.coff_observation(), encoding="utf-8") + build = root / f"{profile}-build.json" + measure.write_json(build, {"profile": profile, "target": "x86_64-pc-windows-msvc", + "runtime_sources": inputs, "archive": measure.artifact(archive), + "cfgs_selected_package_only": ["test", "jett_resource_native_test_archive"], + "measurement_script": measure.artifact(Path(measure.__file__)), + "cargo_build_log": measure.artifact(build_log), "build_environment": { + "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS": "-C target-feature=+crt-static -C debuginfo=0", + "CARGO_INCREMENTAL": "0", "CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_PROFILE_RELEASE_DEBUG": "0"}, + "toolchain": { + "rustc": {"invocation_path": str(compiler), "executable": measure.artifact(compiler)}}}) + libraries = root / f"{profile}-libraries.json" + measure.write_json(libraries, {"arguments": MsvcObservationTests.libraries, + "actual_print_log": measure.artifact(build_log), "order_and_duplication_preserved": True}) + crt = root / f"{profile}-crt.json" + measure.write_json(crt, {"mode": "static", "linker_path": str(linker), + "linker_executable": measure.artifact(linker), "sdk_LIB": environment["LIB"], + "developer_shell": {"VCINSTALLDIR": environment["VCINSTALLDIR"], "VSCMD_ARG_TGT_ARCH": "x64"}, + "actual_target_scoped_rustflags": MsvcObservationTests.flags, + "cargo_fingerprints": [{**measure.artifact(fingerprint), + "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}], + "complete_coff_archive_inventory": measure.artifact(coff), "coff_directive_record_count": 1, + "complete_symbol_inventory": measure.artifact(symbols), + "native_static_libs": measure.artifact(libraries)}) + archives.append({"profile": profile, "target": "x86_64-pc-windows-msvc", "runtime_abi": 1, + "layout_wire": 3, "private_cfgs": ["test", "jett_resource_native_test_archive"], + "crt_mode": "static", "archive_path": str(archive), "archive_sha256": measure.digest(archive), + "linker_path": str(linker), "native_library_args": MsvcObservationTests.libraries, + "build_receipt": measure.artifact(build), "native_static_libs_receipt": measure.artifact(libraries), + "crt_receipt": measure.artifact(crt)}) + receipt = root / "receipt.json" + measure.write_json(receipt, {"version": 1, "archives": archives}) + return receipt, environment + + def refresh_artifacts(self, value: measure.Json) -> None: + if isinstance(value, list): + for item in value: + self.refresh_artifacts(item) + elif isinstance(value, dict): + for item in value.values(): + self.refresh_artifacts(item) + path = value.get("path") + if isinstance(path, str) and isinstance(value.get("sha256"), str): + file = Path(path) + if file.suffix == ".json": + record = measure.read_object(file) + self.refresh_artifacts(record) + self.rewrite(file, record) + value["sha256"] = measure.digest(file) + if "raw_json_utf8" in value: + value["raw_json_utf8"] = file.read_text(encoding="utf-8") + + def reauthenticate(self, receipt: Path) -> str: + record = measure.read_object(receipt) + self.refresh_artifacts(record) + self.rewrite(receipt, record) + return measure.digest(receipt) + + def verify(self, receipt: Path, root: Path, environment: dict[str, str], expected: str | None = None) -> None: + with patch.dict(os.environ, environment, clear=True), \ + patch.object(measure.shutil, "which", return_value=str(root / "tool chain/link.exe")): + measure.receipt_sources(receipt, expected or measure.digest(receipt), root, + target="x86_64-pc-windows-msvc") + + def test_complete_msvc_receipt_retains_current_exports_and_sdk_evidence(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + self.verify(receipt, root, environment) + self.assertEqual([row["profile"] for row in self.rows(receipt)], ["debug", "release"]) + self.assertEqual(self.rows(receipt)[0]["native_library_args"], MsvcObservationTests.libraries) + leaves, private = measure.export_names(root) + self.assertEqual((len(leaves - measure.CARRIER_EXPORTS - measure.STARTUP_EXPORTS + - {measure.GRAPH_DESCRIPTOR_EXPORT}), + len(measure.CARRIER_EXPORTS), len(leaves & measure.STARTUP_EXPORTS), + int(measure.GRAPH_DESCRIPTOR_EXPORT in leaves), len(private)), + (37, 15, 5, 1, 8)) + + def test_receipt_pin_and_actual_runtime_or_helper_source_changes_are_refused(self) -> None: + for changed in ("Cargo.lock", "tools/measure_resource_native_archives.py", + "crates/jett_runtime/src/native_abi/resource/leaves.rs"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + expected = measure.digest(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, "0" * 64) + (root / changed).write_bytes(b"changed current source after measurement") + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_sdk_lib_and_developer_shell_cannot_change_after_measurement(self) -> None: + for name, replacement in (("LIB", "same directories;different exact vector"), + ("VSCMD_ARG_TGT_ARCH", "x86"), ("VCINSTALLDIR", "foreign installation")): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + environment[name] = environment[name].replace(";", ";;", 1) if name == "LIB" else replacement + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment) + + def test_rehashed_forged_observations_do_not_prove_static_crt_or_compiler_identity(self) -> None: + corruptions = { + "fingerprint": ("debug-fingerprint.json", '{"rustflags":[]}'), + "coff-crt": ("debug-coff.log", MsvcObservationTests.coff_observation() + "Directive(s): /DEFAULTLIB:msvcrt\n"), + "coff-member": ("debug-coff.log", MsvcObservationTests.coff_observation().replace("COFF-x86-64", "COFF-ARM64", 1)), + "symbols": ("debug-symbols.log", "main T 0 10\nmain T 0 10\n"), + } + for name, (filename, text) in corruptions.items(): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + (root / filename).write_text(text, encoding="utf-8") + if filename == "debug-coff.log": + crt_path = root / "debug-crt.json" + crt = measure.read_object(crt_path) + crt["coff_directive_record_count"] = text.count("Directive(s):") + self.rewrite(crt_path, crt) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + build_log = root / "debug-build.log" + build_log.write_text(build_log.read_text(encoding="utf-8").replace('Running `"', 'Running `wrapper.exe "'), + encoding="utf-8") + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_rehashed_receipt_cannot_change_order_duplicates_or_profile_contract(self) -> None: + changes: tuple[tuple[str, measure.Json], ...] = ( + ("native_library_args", list(reversed(MsvcObservationTests.libraries))), + ("native_library_args", list(dict.fromkeys(MsvcObservationTests.libraries))), + ("profile", "release"), ("target", "x86_64-unknown-linux-gnu"), ("runtime_abi", 2), + ("layout_wire", 2), ("private_cfgs", ["test"]), ("crt_mode", "dynamic")) + for field, value in changes: + with self.subTest(field=field, value=value), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0][field] = value + self.rewrite(receipt, record) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment) + + def test_changed_nested_archive_linker_or_observation_cannot_keep_original_pin(self) -> None: + for name in ("debug-runtime.lib", "tool chain/link.exe", "debug-build.log", "debug-fingerprint.json"): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + expected = measure.digest(receipt) + (root / name).write_bytes(b"changed independent artifact") + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_inherited_path_must_select_the_same_linker_without_an_override(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, environment = self.fixture(root) + linker = root / "tool chain/link.exe" + with patch.object(measure.shutil, "which", return_value=str(linker)) as which: + measure.checked_msvc_environment(environment, linker) + which.assert_called_once_with("link.exe", path=environment["PATH"]) + with patch.object(measure.shutil, "which", return_value=str(root / "foreign-link.exe")), \ + self.assertRaises(measure.MeasurementError): + measure.checked_msvc_environment(environment, linker) + environment["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(root / "foreign-link.exe") + with patch.object(measure.shutil, "which", return_value=str(linker)), \ + self.assertRaises(measure.MeasurementError): + measure.checked_msvc_environment(environment, linker) + + def test_existing_output_and_stale_source_contract_stop_before_tools(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.fixture(root) + with patch.object(measure, "executable") as executable, self.assertRaises(measure.MeasurementError): + measure.prepare_msvc(root, root) + executable.assert_not_called() + registration = root / "crates/jett_runtime/src/resource_custody/registration.rs" + registration.write_text("pub(crate) const NATIVE_RESOURCE_LAYOUT_WIRE_VERSION: u32 = 2;\n", + encoding="utf-8") + output = root / "fresh-output" + with patch.object(measure, "executable") as executable, self.assertRaises(measure.MeasurementError): + measure.prepare_msvc(root, output) + executable.assert_not_called() + self.assertFalse(output.exists()) + + def test_sources_changed_by_build_stop_before_archive_observation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + _, environment = self.fixture(root) + inputs = measure.sources(root) + leaves, private = measure.export_names(root) + directory = root / "measurement" + directory.mkdir() + tools = measure.MsvcTools(root / "tool chain/cargo.exe", root / "tool chain/rustc.exe", + root / "tool chain/llvm-nm.exe", root / "tool chain/llvm-readobj.exe", + root / "tool chain/link.exe") + def changed_build(argv: list[str], repo: Path, env: dict[str, str], + log: Path, timeout: int = 3600) -> str: + self.assertEqual(argv[1], "rustc") + self.assertEqual(repo, root) + self.assertEqual(env["LIB"], environment["LIB"]) + text = MsvcObservationTests.runtime_command(tools.rustc) + log.write_text(text, encoding="utf-8") + (root / "Cargo.lock").write_bytes(b"changed during the selected runtime build") + return text + with patch.object(measure, "logged", side_effect=changed_build) as logged, \ + self.assertRaises(measure.MeasurementError): + measure.measure_msvc_profile(tools, root, directory, "debug", environment, + inputs, {}, leaves, private) + self.assertEqual(logged.call_count, 1) + self.assertFalse((directory / "debug-symbols.log").exists()) + self.assertFalse((directory / "receipt.json").exists()) + + def test_rehashed_build_cannot_redirect_archive_or_assert_different_private_cfgs(self) -> None: + for change in ("archive", "cfgs"): + with self.subTest(change=change), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + build_path = root / "debug-build.json" + build = measure.read_object(build_path) + if change == "archive": + build["archive"] = measure.artifact(root / "release-runtime.lib") + else: + build["cfgs_selected_package_only"] = ["test", "test", "jett_resource_native_test_archive"] + self.rewrite(build_path, build) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def test_rehashed_library_metadata_cannot_override_the_observed_vector(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + changed = list(dict.fromkeys(MsvcObservationTests.libraries)) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0]["native_library_args"] = changed + self.rewrite(receipt, record) + library_path = root / "debug-libraries.json" + libraries = measure.read_object(library_path) + libraries["arguments"] = changed + self.rewrite(library_path, libraries) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def no_directive_fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + receipt, environment = self.fixture(root) + for profile in ("debug", "release"): + (root / f"{profile}-coff.log").write_text(MsvcObservationTests.rust_archive_without_directives(), + encoding="utf-8") + crt_path = root / f"{profile}-crt.json" + crt = measure.read_object(crt_path) + crt["coff_directive_record_count"] = 0 + self.rewrite(crt_path, crt) + self.reauthenticate(receipt) + return receipt, environment + + def test_receipt_can_record_zero_directives_with_independent_static_crt_proof(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.no_directive_fixture(root) + self.verify(receipt, root, environment) + for profile in ("debug", "release"): + self.assertEqual(measure.read_object(root / f"{profile}-crt.json")["coff_directive_record_count"], 0) + self.assertNotIn("Directive(s):", (root / f"{profile}-coff.log").read_text(encoding="utf-8")) + self.assertIn("libcmt.lib", MsvcObservationTests.libraries) + + def test_rehashed_receipt_cannot_forge_missing_or_noninteger_directive_counts(self) -> None: + for count in (1, -1, "0", False, None): + with self.subTest(count=count), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.no_directive_fixture(root) + crt_path = root / "debug-crt.json" + crt = measure.read_object(crt_path) + if count is None: + del crt["coff_directive_record_count"] + else: + crt["coff_directive_record_count"] = count + self.rewrite(crt_path, crt) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + def raw_defaultlib_fixture(self, root: Path) -> tuple[Path, dict[str, str]]: + receipt, environment = self.fixture(root) + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list): + self.fail("fixture must retain archive rows") + for row in archives: + if not isinstance(row, dict): + self.fail("fixture must retain archive objects") + row["native_library_args"] = MsvcObservationTests.raw_libraries + self.rewrite(receipt, record) + for profile in ("debug", "release"): + build_log = root / f"{profile}-build.log" + build_log.write_text(build_log.read_text(encoding="utf-8").replace( + " ".join(MsvcObservationTests.libraries), " ".join(MsvcObservationTests.raw_libraries)), + encoding="utf-8") + library_path = root / f"{profile}-libraries.json" + libraries = measure.read_object(library_path) + libraries["arguments"] = MsvcObservationTests.raw_libraries + self.rewrite(library_path, libraries) + self.reauthenticate(receipt) + return receipt, environment + + def test_receipt_accepts_raw_defaultlib_token_and_retains_the_original_vector(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.raw_defaultlib_fixture(root) + self.verify(receipt, root, environment) + for row in self.rows(receipt): + self.assertEqual(row["native_library_args"], MsvcObservationTests.raw_libraries) + + def test_rehashed_receipt_cannot_replace_observed_crt_flag_with_an_equivalent_library(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.raw_defaultlib_fixture(root) + normalized = [*MsvcObservationTests.raw_libraries[:-1], "libcmt.lib"] + record = measure.read_object(receipt) + archives = record.get("archives") + if not isinstance(archives, list) or not isinstance(archives[0], dict): + self.fail("fixture must retain archive objects") + archives[0]["native_library_args"] = normalized + self.rewrite(receipt, record) + libraries_path = root / "debug-libraries.json" + libraries = measure.read_object(libraries_path) + libraries["arguments"] = normalized + self.rewrite(libraries_path, libraries) + expected = self.reauthenticate(receipt) + with self.assertRaises(measure.MeasurementError): + self.verify(receipt, root, environment, expected) + + + def test_reauthenticated_receipt_rejects_missing_duplicate_and_nontext_actor_export(self) -> None: + symbol = measure.ACTOR_REPLACE_EXPORT + mutations = ("missing", "duplicate", "data", "weak", "local") + for mutation in mutations: + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + receipt, environment = self.fixture(root) + self.verify(receipt, root, environment) + symbols = root / "debug-symbols.log" + original = symbols.read_text(encoding="utf-8") + definition = f"{symbol} T 0 10" + self.assertEqual(original.splitlines().count(definition), 1) + if mutation == "missing": + changed = "\n".join(line for line in original.splitlines() if line != definition) + elif mutation == "duplicate": + changed = original + f"\n{definition}\n" + else: + kind = {"data": "D", "weak": "W", "local": "t"}[mutation] + changed = original.replace(definition, f"{symbol} {kind} 0 10") + symbols.write_text(changed, encoding="utf-8") + leaves, private = measure.export_names(root) + measure.check_symbols(changed, leaves, private) + measure.check_struct_replace_symbols(changed) + expected = self.reauthenticate(receipt) + with self.assertRaisesRegex(measure.MeasurementError, "ActorReplaceOccupied"): + self.verify(receipt, root, environment, expected) + + +class MsvcGateTests(unittest.TestCase): + occupied_tests = ( + "resource_execution::occupied_struct_harness::native_resource_occupied_struct_scripts_retire_before_teardown", + "resource_execution::occupied_struct_harness::native_resource_occupied_struct_scripts_reenter_with_same_provider_grant", + ) + + def run_fixture(self, root: Path, *, fresh: bool = False, mutate_gate: str | None = None, + execution: str = "running 1 test\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n") -> tuple[ + dict[str, measure.Json], list[dict[str, str]], list[dict[str, str]]]: + receipt, environment = MsvcReceiptTests().fixture(root) + workflow = root / ".github/workflows/native.yml" + workflow.parent.mkdir(parents=True) + workflow.write_bytes(b"synthetic complete Source fixture") + expected = measure.digest(receipt) + compiler = root / "tool chain/rustc.exe" + linker = root / "tool chain/link.exe" + binary = root / "compiled gate.exe" + target = root / "existing gate target" + target.mkdir() + compile_environments: list[dict[str, str]] = [] + runtime_environments: list[dict[str, str]] = [] + def compile_gate(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: BinaryIO, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertEqual(cwd, root) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertIn("--no-run", argv) + self.assertIn("--offline", argv) + self.assertEqual(argv[argv.index("--target") + 1], "x86_64-pc-windows-msvc") + self.assertEqual(argv[argv.index("--target-dir") + 1], str(target)) + self.assertNotIn("--cfg", argv) + compile_environments.append(dict(env)) + binary.write_bytes(b"synthetic freshly compiled gate") + binary.with_suffix(".d").write_text( + f"# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1={env[measure.PIN_ENV]}\n", + encoding="utf-8") + stderr.write((f'Running `"{compiler}" --crate-name native_conformance --test ' + '--target x86_64-pc-windows-msvc --emit=dep-info,link ' + f'-C target-feature=+crt-static -C debuginfo=0 -C "linker={linker}"`\n').encode()) + stdout.write((json.dumps({"reason": "compiler-artifact", "fresh": fresh, + "target": {"name": "native_conformance"}, "executable": str(binary)}) + "\n").encode()) + return subprocess.CompletedProcess(argv, 0) + def execute_gate(argv: list[str], repo: Path, env: dict[str, str], + log: Path, timeout: int = 3600) -> str: + self.assertEqual(repo, root) + if argv == [str(compiler), "-vV"]: + self.assertNotIn(measure.PIN_ENV, env) + text = "host: x86_64-pc-windows-msvc\n" + else: + self.assertEqual(argv, [str(binary), self.occupied_tests[len(runtime_environments)], + "--ignored", "--exact", "--test-threads=1", "--nocapture"]) + runtime_environments.append(dict(env)) + if mutate_gate == "binary": + binary.write_bytes(b"changed gate during execution") + elif mutate_gate == "dep-info": + binary.with_suffix(".d").write_bytes(b"changed compiled pin evidence during execution") + text = execution + log.write_text(text, encoding="utf-8") + return text + def tool(name: str) -> Path: + return root / "tool chain" / (name if name.endswith(".exe") else name + ".exe") + with patch.dict(os.environ, environment, clear=True), \ + patch.object(measure.shutil, "which", return_value=str(linker)), \ + patch.object(measure, "executable", side_effect=tool), \ + patch.object(measure.subprocess, "run", side_effect=compile_gate), \ + patch.object(measure, "logged", side_effect=execute_gate): + measure.run_gate(root, receipt, expected, target, target="x86_64-pc-windows-msvc", + suite="occupied-structs", reuse_target_dir=True) + return measure.read_object(root / "gate-acceptance.json"), compile_environments, runtime_environments + + def test_occupied_gates_compile_the_pin_and_execute_each_exact_test_once(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + accepted, compiled, executed = self.run_fixture(root) + self.assertEqual(len(compiled), 1) + self.assertEqual(len(executed), 2) + self.assertEqual(compiled[0][measure.PIN_ENV], accepted["compile_time_receipt_sha256"]) + self.assertNotIn(measure.RECEIPT_ENV, compiled[0]) + for environment in executed: + self.assertNotIn(measure.PIN_ENV, environment) + self.assertEqual(environment[measure.RECEIPT_ENV], str(root / "receipt.json")) + self.assertEqual(accepted["suite"], "occupied-structs") + observations = accepted["gate_executions"] + if not isinstance(observations, list): + self.fail("accepted executions must be an observed list") + self.assertEqual([row.get("test") for row in observations if isinstance(row, dict)], + list(self.occupied_tests)) + self.assertNotIn("cases", accepted) + self.assertNotIn("source_deleted_executions", accepted) + self.assertEqual(accepted["runtime_expected_pin_environment"], False) + + def test_reused_target_cannot_accept_cached_compilation_or_empty_test_selection(self) -> None: + for fresh, execution in ((True, "running 1 test\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n"), + (False, "running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored;\n"), + (False, "running 2 tests\ntest result: ok. 1 passed; 0 failed; 0 ignored;\n")): + with self.subTest(fresh=fresh, execution=execution), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + self.run_fixture(root, fresh=fresh, execution=execution) + self.assertFalse((root / "gate-acceptance.json").exists()) + + def test_compiler_dep_info_requires_one_exact_pin_and_fresh_artifact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + binary = Path(temporary) / "native_conformance.exe" + binary.write_bytes(b"synthetic test binary") + expected = "a" * 64 + observation: dict[str, measure.Json] = {"reason": "compiler-artifact", "fresh": False, + "target": {"name": "native_conformance"}, "executable": str(binary)} + message = json.dumps(observation) + dep_info = binary.with_suffix(".d") + exact_pin = f"# env-dep:JETT_RESOURCE_NATIVE_TEST_ARCHIVE_EXPECTED_SHA256_V1={expected}\n" + with self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(message, binary, expected) + dep_info.write_text(exact_pin, encoding="utf-8") + self.assertEqual(measure.checked_msvc_gate_artifact(message, binary, expected), dep_info) + for invalid in ("", exact_pin + exact_pin, exact_pin.replace(expected, "b" * 64)): + dep_info.write_text(invalid, encoding="utf-8") + with self.subTest(dep_info=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(message, binary, expected) + dep_info.write_text(exact_pin, encoding="utf-8") + cached = {**observation, "fresh": True} + missing_fresh = {key: value for key, value in observation.items() if key != "fresh"} + foreign = {**observation, "executable": str(binary.parent / "foreign.exe")} + for invalid in (json.dumps(cached), json.dumps(missing_fresh), json.dumps(foreign), message + "\n" + message): + with self.subTest(message=invalid), self.assertRaises(measure.MeasurementError): + measure.checked_msvc_gate_artifact(invalid, binary, expected) + + def test_unknown_suite_and_implicit_target_reuse_stop_before_compilation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + target = root / "existing" + target.mkdir() + with patch.object(measure, "receipt_sources") as receipt_sources, \ + patch.object(measure.subprocess, "run") as compile_gate: + for suite, reuse in (("foreign", True), ("occupied-structs", False)): + with self.subTest(suite=suite, reuse=reuse), self.assertRaises(measure.MeasurementError): + measure.run_gate(root, root / "absent-receipt.json", "a" * 64, target, + target="x86_64-pc-windows-msvc", suite=suite, reuse_target_dir=reuse) + receipt_sources.assert_not_called() + compile_gate.assert_not_called() + + def test_gate_binary_and_compiled_pin_evidence_cannot_change_during_execution(self) -> None: + for changed in ("binary", "dep-info"): + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaises(measure.MeasurementError): + self.run_fixture(root, mutate_gate=changed) + self.assertFalse((root / "gate-acceptance.json").exists()) + +class MsvcLinkerVersionTests(unittest.TestCase): + help_text = ("Microsoft (R) Incremental Linker Version 14.44.35213.0\n" + "Copyright (C) Microsoft Corporation. All rights reserved.\n\n" + "usage: LINK [options] [files] [@commandfile]\n\n" + " options:\n\n" + " /ALIGN:#\n /DEBUG\n") + + def observe(self, root: Path, text: str, returncode: int, *, generic: bool = False) -> tuple[str, int]: + linker = root / "measured link.exe" + linker.write_bytes(b"synthetic linker executable") + log = root / "linker-version.log" + environment = {"LIB": r"C:\SDK Libraries\x64;", "PATH": str(root)} + def version_probe(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: int, timeout: int) -> subprocess.CompletedProcess[bytes]: + self.assertEqual(argv, [str(linker), "/?"]) + self.assertEqual(cwd, root) + self.assertEqual(env, environment) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertEqual(stderr, subprocess.STDOUT) + stdout.write(text.encode("utf-8")) + return subprocess.CompletedProcess(argv, returncode) + with patch.object(measure.subprocess, "run", side_effect=version_probe) as run: + if generic: + result = measure.logged([str(linker), "/?"], root, environment, log, timeout=60) + observed = (result, returncode) + else: + observed = measure.msvc_linker_version(linker, root, environment, log) + run.assert_called_once() + self.assertEqual(log.read_text(encoding="utf-8"), text) + return observed + + def test_microsoft_help_exit_1100_and_zero_retain_the_actual_version_observation(self) -> None: + for returncode in (0, 1100): + with self.subTest(returncode=returncode), tempfile.TemporaryDirectory() as temporary: + self.assertEqual(self.observe(Path(temporary), self.help_text, returncode), + (self.help_text, returncode)) + + def test_error_exit_1100_cannot_authenticate_even_with_a_valid_help_prefix(self) -> None: + for diagnostic in ("LINK : fatal error LNK1158: cannot run 'rc.exe'\n", + "LINK : error LNK2001: unresolved external symbol main\n"): + for prefix in ("", self.help_text): + with self.subTest(diagnostic=diagnostic, help_prefix=bool(prefix)), \ + tempfile.TemporaryDirectory() as temporary, self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), prefix + diagnostic, 1100) + + def test_unsupported_status_or_incomplete_or_foreign_help_is_refused(self) -> None: + invalid = ((self.help_text, 1), (self.help_text, 2), + ("Microsoft (R) Incremental Linker Version 14.44.35213.0\n", 1100), + (self.help_text.replace("usage: LINK [options] [files] [@commandfile]\n", ""), 1100), + (self.help_text.replace(" options:\n", ""), 1100), + (self.help_text.replace("Microsoft (R) Incremental Linker Version 14.44.35213.0", + "LLVM Linker Version 21.0.0"), 1100)) + for text, returncode in invalid: + with self.subTest(text=text, returncode=returncode), tempfile.TemporaryDirectory() as temporary, \ + self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), text, returncode) + + def test_generic_logged_still_rejects_valid_help_with_nonzero_exit_status(self) -> None: + with tempfile.TemporaryDirectory() as temporary, self.assertRaises(measure.MeasurementError): + self.observe(Path(temporary), self.help_text, 1100, generic=True) + + def test_toolchain_receipt_records_the_actual_linker_help_status_and_hashed_log(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + directory = root / "observations" + directory.mkdir() + tools = measure.MsvcTools(root / "cargo.exe", root / "rustc.exe", root / "llvm-nm.exe", + root / "llvm-readobj.exe", root / "link.exe") + for file in (tools.cargo, tools.rustc, tools.nm, tools.readobj, tools.linker): + file.write_bytes(b"synthetic measured tool") + expected: dict[str, tuple[str, str, int]] = { + str(tools.cargo): ("-vV", "cargo 1.92.0\n", 0), + str(tools.rustc): ("-vV", "rustc 1.92.0\nhost: x86_64-pc-windows-msvc\n", 0), + str(tools.nm): ("--version", "LLVM version 21.0.0\n", 0), + str(tools.readobj): ("--version", "LLVM version 21.0.0\n", 0), + str(tools.linker): ("/?", self.help_text, 1100), + } + environment = {"LIB": r"C:\SDK\x64;", "PATH": str(root)} + def observe(argv: list[str], *, cwd: Path, env: dict[str, str], stdin: int, + stdout: BinaryIO, stderr: int, timeout: int) -> subprocess.CompletedProcess[bytes]: + argument, text, returncode = expected[argv[0]] + self.assertEqual(argv, [argv[0], argument]) + self.assertEqual(cwd, root) + self.assertEqual(env, environment) + self.assertEqual(stdin, subprocess.DEVNULL) + self.assertEqual(stderr, subprocess.STDOUT) + stdout.write(text.encode("utf-8")) + return subprocess.CompletedProcess(argv, returncode) + with patch.object(measure.subprocess, "run", side_effect=observe) as run: + recorded = measure.msvc_toolchain(tools, root, directory, environment) + self.assertEqual(run.call_count, 5) + linker = recorded["linker"] + if not isinstance(linker, dict): + self.fail("toolchain linker observation must be an object") + self.assertEqual(linker["returncode"], 1100) + self.assertEqual(linker["version_observation"], measure.artifact(directory / "linker-version.log")) + self.assertEqual(linker["executable"], measure.artifact(tools.linker)) + for name in ("rustc", "cargo", "nm", "readobj"): + observed = recorded[name] + if not isinstance(observed, dict): + self.fail("toolchain version observation must be an object") + self.assertEqual(observed["returncode"], 0) + +class BoundedMsvcCacheTests(unittest.TestCase): + def test_new_and_existing_caches_are_strict_siblings_of_evidence(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + cache, evidence = root / "cache", root / "evidence" + self.assertEqual(measure.bounded_msvc_build_cache(root, cache, evidence), cache) + self.assertFalse(cache.exists(), "path validation must not allocate a build cache") + cache.mkdir() + self.assertEqual(measure.bounded_msvc_build_cache(root, cache, evidence), cache) + + def test_relative_missing_escape_alias_and_overlapping_paths_are_refused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + cache, evidence = root / "cache", root / "evidence" + invalid = [ + (Path("relative-root"), cache, evidence), + (root, Path("relative-cache"), evidence), + (root, cache, Path("relative-evidence")), + (root / "missing", root / "missing/cache", root / "missing/evidence"), + (root, root.parent / "escaped-cache", evidence), + (root, root / ".." / "escaped-cache", evidence), + (root, cache, root.parent / "escaped-evidence"), + (root, root, evidence), + (root, cache, root), + (root, cache, cache), + (root, cache, cache / "evidence"), + (root, evidence / "cache", evidence), + ] + for arguments in invalid: + with self.subTest(arguments=arguments), self.assertRaises(measure.MeasurementError): + measure.bounded_msvc_build_cache(*arguments) + cache.write_bytes(b"a regular file cannot be a cache") + with self.assertRaises(measure.MeasurementError): + measure.bounded_msvc_build_cache(root, cache, evidence) + + def test_symlink_ancestors_cannot_supply_a_bounded_cache_or_evidence(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + actual = root / "actual" + actual.mkdir() + alias = root / "alias" + try: + alias.symlink_to(actual, target_is_directory=True) + except (OSError, NotImplementedError) as error: + self.skipTest(f"directory symlinks unavailable: {error}") + for arguments in ((root, alias / "cache", root / "evidence"), + (root, root / "cache", alias / "evidence"), + (alias, alias / "cache", alias / "evidence")): + with self.subTest(arguments=arguments), self.assertRaises(measure.MeasurementError): + measure.bounded_msvc_build_cache(*arguments) + + def test_prepare_cli_preserves_raw_output_ancestry_for_actual_cache_validation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + actual = root / "actual-evidence" + actual.mkdir() + alias = root / "alias-evidence" + try: + alias.symlink_to(actual, target_is_directory=True) + except (OSError, NotImplementedError) as error: + self.skipTest(f"directory symlinks unavailable: {error}") + cache, output = root / "cache", alias / "proof" + # Canonicalizing first would hide the prohibited symlink ancestor. + self.assertEqual(measure.bounded_msvc_build_cache(root, cache, output.resolve()), cache) + received: list[Path] = [] + + def validate_received_paths(repo: Path, evidence: Path, cc: str, nm: str, + readelf: str, *, target: str, readobj: str, + linker: str, msvc_cache_root: Path | None, + msvc_build_target_dir: Path | None) -> tuple[Path, str]: + self.assertEqual(target, measure.MSVC_TARGET) + if msvc_cache_root is None or msvc_build_target_dir is None: + self.fail("CLI lost its explicit cache boundary arguments") + received.append(evidence) + measure.bounded_msvc_build_cache(msvc_cache_root, msvc_build_target_dir, evidence) + raise AssertionError("CLI erased the output ancestry before real validation") + + arguments = ["measure_resource_native_archives.py", "prepare", "--repo", str(root), + "--output", str(output), "--target", measure.MSVC_TARGET, + "--msvc-cache-root", str(root), "--msvc-build-target-dir", str(cache)] + with patch("sys.argv", arguments), \ + patch.object(measure, "prepare", side_effect=validate_received_paths) as prepare: + with self.assertRaises(measure.MeasurementError): + measure.main() + prepare.assert_called_once() + self.assertEqual(received, [output]) + self.assertFalse(cache.exists()) + self.assertFalse(output.exists()) + + +class ImmutableMsvcSnapshotTests(unittest.TestCase): + def test_exclusive_snapshot_retains_bytes_and_hash_after_source_reuse(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + source, destination = root / "runtime.lib", root / "proof/runtime.lib" + source.write_bytes(b"first measured archive\x00\xff") + expected = measure.artifact(source) + self.assertEqual(measure.snapshot_artifact(source, destination), destination) + self.assertEqual(source.read_bytes(), destination.read_bytes()) + self.assertEqual(measure.digest(destination), expected["sha256"]) + source.write_bytes(b"next cache build") + self.assertEqual(destination.read_bytes(), b"first measured archive\x00\xff") + self.assertEqual(measure.digest(destination), expected["sha256"]) + with self.assertRaises(measure.MeasurementError): + measure.snapshot_artifact(source, destination) + self.assertEqual(destination.read_bytes(), b"first measured archive\x00\xff") + with self.assertRaises(measure.MeasurementError): + measure.snapshot_artifact(source, source) + with self.assertRaises(measure.MeasurementError): + measure.snapshot_artifact(root / "absent.lib", root / "absent-copy.lib") + + def test_source_change_during_real_copy_cannot_produce_a_snapshot(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + source, destination = root / "runtime.lib", root / "proof/runtime.lib" + source.write_bytes(b"original archive") + copy_bytes = measure.shutil.copyfileobj + + def copy_then_change(reader: BinaryIO, writer: BinaryIO, length: int = 0) -> None: + copy_bytes(reader, writer, length) + source.write_bytes(b"changed while copying") + + with patch.object(measure.shutil, "copyfileobj", side_effect=copy_then_change): + with self.assertRaises(measure.MeasurementError): + measure.snapshot_artifact(source, destination) + self.assertEqual(source.read_bytes(), b"changed while copying") + + def test_copied_fingerprint_preserves_raw_json_and_measured_flags(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + directory, snapshots = root / "cache", root / "evidence" + fingerprint = directory / "jett_runtime-real/lib-jett_runtime.json" + fingerprint.parent.mkdir(parents=True) + raw = json.dumps({"rustflags": list(measure.MSVC_RUSTFLAGS), "profile": 42}, + indent=3) + "\n" + fingerprint.write_bytes(raw.encode("utf-8")) + copied = snapshots / fingerprint.parent.name / fingerprint.name + records = measure.msvc_fingerprint_records(directory, snapshots) + self.assertEqual(records, [{**measure.artifact(copied), "raw_json_utf8": raw}]) + self.assertEqual(copied.read_bytes(), fingerprint.read_bytes()) + fingerprint.write_bytes(b'{"rustflags":[]}') + self.assertEqual(copied.read_bytes(), raw.encode("utf-8")) + self.assertEqual(records[0], {**measure.artifact(copied), "raw_json_utf8": raw}) + with self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(directory, root / "invalid-evidence") + + def test_fingerprint_validation_applies_to_the_immutable_copied_bytes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + directory = root / "cache" + fingerprint = directory / "jett_runtime-real/lib-jett_runtime.json" + fingerprint.parent.mkdir(parents=True) + fingerprint.write_text(json.dumps({"rustflags": list(measure.MSVC_RUSTFLAGS)}), + encoding="utf-8") + snapshot = measure.snapshot_artifact + + def snapshot_changed_flags(source: Path, destination: Path) -> Path: + source.write_bytes(b'{"rustflags":[]}') + return snapshot(source, destination) + + with patch.object(measure, "snapshot_artifact", side_effect=snapshot_changed_flags): + with self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(directory, root / "evidence") + + def test_selected_compiler_fingerprint_excludes_stale_cache_variants(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + directory = root / "cache" + fingerprint = directory / "jett_runtime-0123456789abcdef/lib-jett_runtime.json" + fingerprint.parent.mkdir(parents=True) + fingerprint.write_text(json.dumps({"rustflags": list(measure.MSVC_RUSTFLAGS)}), + encoding="utf-8") + stale = directory / "jett_runtime-fedcba9876543210/lib-jett_runtime.json" + stale.parent.mkdir() + stale.write_bytes(b'{"rustflags":[]}') + snapshots = root / "evidence" + records = measure.msvc_fingerprint_records( + directory, snapshots, selected_filename="jett_runtime-0123456789abcdef") + copied = snapshots / fingerprint.parent.name / fingerprint.name + self.assertEqual(records, [{**measure.artifact(copied), + "raw_json_utf8": fingerprint.read_text(encoding="utf-8")}]) + self.assertFalse((snapshots / stale.parent.name).exists()) + for selected in ("jett_runtime-1111111111111111", "../0123456789abcdef", + "-0123456789abcdef", "", "*", "/absolute"): + with self.subTest(selected=selected), self.assertRaises(measure.MeasurementError): + measure.msvc_fingerprint_records(directory, selected_filename=selected) + + +class StructReplaceMeasurementTests(unittest.TestCase): + source = '''macro_rules! leaves { + ($( $variant:ident, $name:ident, $cleanup:literal, ($($arg:ident: $rust:ty => $abi:ident),*), $ret:ty => $retabi:ident, $body:expr; )*) => { + pub fn parameters(self) -> &'static [AbiScalar] { match self { $( Self::$variant => &[AbiScalar::Pointer, $( AbiScalar::$abi, )*], )* } } + pub unsafe extern "C" fn $name(context: *const JettRuntimeContextV1, $( $arg: $rust ),*) -> $ret { + leaf(context, $cleanup, $body) + } + } +} +leaves! { + StructReplaceOccupied, jett_rt_v1_struct_replace_occupied, false, (value: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64), u64 => I64, + |s| { if owned > 1 { return Err(INVALID_STRUCT); } + Ok(s.replace_struct_field(value, index, NativeField { bits, owned: owned != 0, pending_depth: depth })?.bits) }; +} +''' + + def test_only_exact_ordinary_replace_signature_and_context_are_accepted(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + values = root / "crates/jett_runtime/src/native_abi/values.rs" + values.parent.mkdir(parents=True) + values.write_text(self.source, encoding="utf-8") + measure.checked_struct_replace_leaf(root) + invalid = [ + self.source.replace("leaves! {", "unused! {"), + self.source.replace(" StructReplaceOccupied,", "/*\n StructReplaceOccupied,") + .replace(" |s| {", "*/\n |s| {"), + self.source.replace("StructReplaceOccupied,", "AnotherVariant,"), + self.source.replace("jett_rt_v1_struct_replace_occupied,", "jett_rt_v1_foreign,"), + self.source.replace("occupied, false,", "occupied, true,"), + self.source.replace("value: u64 => I64", "value: u32 => I64"), + self.source.replace("index: u64 => I64", "index: u64 => I32"), + self.source.replace("bits: u64 => I64", "bits: u32 => I32"), + self.source.replace("owned: u32 => I32", "owned: u64 => I64"), + self.source.replace("depth: u64 => I64", "depth: u32 => I64"), + self.source.replace("owned: u32 => I32, depth: u64 => I64", + "depth: u64 => I64, owned: u32 => I32"), + self.source.replace(", depth: u64 => I64", ""), + self.source.replace("), u64 => I64,", "), u32 => I32,"), + self.source.replace("context: *const JettRuntimeContextV1", "context: u64"), + self.source.replace("context: *const JettRuntimeContextV1", + "context: *mut JettRuntimeContextV1"), + self.source + self.source, + ] + row = next(line for line in self.source.splitlines() + if line.strip().startswith("StructReplaceOccupied,")) + invalid.append(self.source.replace(row + "\n", "") + row + "\n") + for text in invalid: + values.write_text(text, encoding="utf-8") + with self.subTest(source=text), self.assertRaises(measure.MeasurementError): + measure.checked_struct_replace_leaf(root) + + def test_replace_requires_one_exact_global_text_symbol(self) -> None: + symbol = "jett_rt_v1_struct_replace_occupied" + valid = f"archive[member.o]:\n{symbol} T 0 10\nother_leaf T 0 10\n" + measure.check_struct_replace_symbols(valid) + for text in ("", valid.replace(symbol, symbol + "_foreign"), + valid + f"{symbol} T 0 10\n", valid.replace(f"{symbol} T", f"{symbol} D"), + valid.replace(f"{symbol} T", f"{symbol} W")): + with self.subTest(symbols=text), self.assertRaises(measure.MeasurementError): + measure.check_struct_replace_symbols(text) + + + +class ActorReplaceMeasurementTests(unittest.TestCase): + source = '''macro_rules! leaves { + ($( $variant:ident, $name:ident, $cleanup:literal, ($($arg:ident: $rust:ty => $abi:ident),*), $ret:ty => $retabi:ident, $body:expr; )*) => { + pub fn symbol(self) -> &'static str { match self { $( Self::$variant => stringify!($name), )* } } + pub fn parameters(self) -> &'static [AbiScalar] { match self { $( Self::$variant => &[AbiScalar::Pointer, $( AbiScalar::$abi, )*], )* } } + pub fn result(self) -> AbiScalar { match self { $( Self::$variant => AbiScalar::$retabi, )* } } + pub unsafe extern "C" fn $name(context: *const JettRuntimeContextV1, $( $arg: $rust ),*) -> $ret { + leaf(context, $cleanup, $body) + } + } +} +leaves! { + ActorReplaceOccupied, jett_rt_v1_actor_replace_occupied, false, (actor: u64 => I64, index: u64 => I64, bits: u64 => I64, owned: u32 => I32, depth: u64 => I64), u64 => I64, + |s| { Ok(s.replace_actor_field(actor, index, NativeField { bits, owned: owned != 0, pending_depth: depth })?.bits) }; +} +''' + + def test_actor_replace_authenticates_c_and_abi_widths_order_result_and_context(self) -> None: + changes = ( + ("ActorReplaceOccupied,", "AnotherVariant,"), + ("jett_rt_v1_actor_replace_occupied,", "jett_rt_v1_actor_replace_other,"), + ("occupied, false,", "occupied, true,"), + ("actor: u64 => I64", "actor: u32 => I64"), + ("actor: u64 => I64", "actor: u64 => I32"), + ("index: u64 => I64", "index: u32 => I32"), + ("bits: u64 => I64", "bits: u32 => I32"), + ("owned: u32 => I32", "owned: u64 => I32"), + ("owned: u32 => I32", "owned: u32 => I64"), + ("depth: u64 => I64", "depth: u32 => I32"), + ("actor: u64 => I64, index: u64 => I64", + "index: u64 => I64, actor: u64 => I64"), + ("owned: u32 => I32, depth: u64 => I64", + "depth: u64 => I64, owned: u32 => I32"), + (", depth: u64 => I64", ""), + ("depth: u64 => I64)", "depth: u64 => I64, extra: u64 => I64)"), + ("), u64 => I64,", "), u32 => I64,"), + ("), u64 => I64,", "), u64 => I32,"), + ("context: *const JettRuntimeContextV1", "context: u64"), + ("context: *const JettRuntimeContextV1", "context: *mut JettRuntimeContextV1"), + ) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + values = root / "crates/jett_runtime/src/native_abi/values.rs" + values.parent.mkdir(parents=True) + values.write_text(self.source, encoding="utf-8") + measure.checked_actor_replace_leaf(root) + for old, new in changes: + self.assertIn(old, self.source) + values.write_text(self.source.replace(old, new, 1), encoding="utf-8") + with self.subTest(forgery=new), self.assertRaises(measure.MeasurementError): + measure.checked_actor_replace_leaf(root) + + def test_actor_replace_authenticates_macro_mappings_and_generated_wrapper(self) -> None: + changes = ( + ("stringify!($name)", '"jett_rt_v1_foreign"'), + ("&[AbiScalar::Pointer, $( AbiScalar::$abi, )*]", + "&[AbiScalar::I64, $( AbiScalar::$abi, )*]"), + ("&[AbiScalar::Pointer, $( AbiScalar::$abi, )*]", + "&[AbiScalar::Pointer]"), + ("$( AbiScalar::$abi, )*", "$( AbiScalar::I64, )*"), + ("Self::$variant => AbiScalar::$retabi", "Self::$variant => AbiScalar::I32"), + ('pub unsafe extern "C" fn $name', 'pub unsafe extern "system" fn $name'), + ("$( $arg: $rust ),*) -> $ret", "$( $arg: u64 ),*) -> $ret"), + ("$( $arg: $rust ),*) -> $ret", "$( $arg: $rust ),*) -> u32"), + ("leaf(context, $cleanup, $body)", "leaf(context, true, $body)"), + ("leaf(context, $cleanup, $body)", "leaf(context, false, $body)"), + ) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + values = root / "crates/jett_runtime/src/native_abi/values.rs" + values.parent.mkdir(parents=True) + values.write_text(self.source, encoding="utf-8") + measure.checked_actor_replace_leaf(root) + for old, new in changes: + self.assertIn(old, self.source) + values.write_text(self.source.replace(old, new, 1), encoding="utf-8") + with self.subTest(forgery=new), self.assertRaises(measure.MeasurementError): + measure.checked_actor_replace_leaf(root) + + def test_actor_replace_row_must_belong_to_one_uncommented_canonical_catalog(self) -> None: + row = next(line for line in self.source.splitlines() + if line.strip().startswith("ActorReplaceOccupied,")) + invalid = ( + self.source.replace("leaves! {", "unused! {", 1), + self.source.replace(row, "//" + row, 1), + self.source.replace(row, "/*\n" + row + "\n*/", 1), + self.source.replace(row, row + "\n" + row, 1), + self.source.replace(row + "\n", "", 1) + row + "\n", + self.source + self.source, + self.source.replace("macro_rules! leaves", "macro_rules! unrelated", 1), + self.source.replace(" pub fn result", "/*\n pub fn result", 1) + .replace(" pub unsafe", "*/\n pub unsafe", 1), + ) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + values = root / "crates/jett_runtime/src/native_abi/values.rs" + values.parent.mkdir(parents=True) + values.write_text(self.source, encoding="utf-8") + measure.checked_actor_replace_leaf(root) + for changed in invalid: + values.write_text(changed, encoding="utf-8") + with self.subTest(source=changed), self.assertRaises(measure.MeasurementError): + measure.checked_actor_replace_leaf(root) + + def test_actor_replace_requires_one_exact_global_text_definition(self) -> None: + symbol = measure.ACTOR_REPLACE_EXPORT + valid = f"runtime.lib(member.o):\n{symbol} T 0 10\nother_leaf T 0 10\n" + measure.check_actor_replace_symbols(valid) + invalid = ( + "", + valid.replace(symbol, symbol + "_foreign"), + valid + f"{symbol} T 0 10\n", + valid + f"{symbol} D 0 10\n", + valid.replace(f"{symbol} T", f"{symbol} D"), + valid.replace(f"{symbol} T", f"{symbol} W"), + valid.replace(f"{symbol} T", f"{symbol} U"), + valid.replace(f"{symbol} T", f"{symbol} t"), + f"{symbol} T\n", + ) + for text in invalid: + with self.subTest(symbols=text), self.assertRaises(measure.MeasurementError): + measure.check_actor_replace_symbols(text) + + + +if __name__ == "__main__": + unittest.main()